Deploy to mobile
Your code does not change. The program from part 4 of the series, byte for byte, compiled for Android and pushed to a phone, prints the same numbers there that it printed on your workstation, on the phone's GPU when it has one and on its CPU otherwise. This page is the complete path: cross-compile, push, run, and the few lines of Kotlin an app adds around it.
You need the bundle (its android-arm64 dist ships the CPU and Vulkan backends), the Android NDK, and a phone with USB debugging enabled.
1. Cross-compile
Same project, same main.cpp. The configure line adds the NDK's toolchain file and the ABI; the bundle's CMake package selects the Android dist on its own:
cmake -S . -B build-android \
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 \
-DClikaRT_DIR="$CLIKART_BUNDLE_DIR/cmake"
cmake --build build-android
-- ClikaRT 0.5.1: dist android-arm64 (backends: cpu;vulkan)
2. Push and run
The binary plus the two libraries from the Android dist go to the device; the phone needs nothing else installed:
adb shell mkdir -p /data/local/tmp/clikart
adb push build-android/hello \
"$CLIKART_BUNDLE_DIR/lib/libClikaRT.so" \
"$CLIKART_BUNDLE_DIR/lib/libClikaRT_vulkan.so" \
/data/local/tmp/clikart/
adb shell "cd /data/local/tmp/clikart && chmod +x hello && \
CLIKA_RT_LICENSE=CLIKA1-... LD_LIBRARY_PATH=. ./hello"
The program runs compute, so it needs a license credential like any other ClikaRT program, and a binary under adb shell reads it from the environment of the shell that starts it (Get ClikaRT). Push the credential to a file on the device and give CLIKA_RT_LICENSE that path when you would rather keep it off the command line.
On a Galaxy S24 Ultra:
y = Tensor(shape=[2, 4], dtype=Float32, device=Vulkan:0, numel=8, data=[4.25, 4.25, 4.25, 4.25, 4.25, 4.25, ...])
row means = [4.25, 4.25] (expected 8*1*0.5 + 0.25 = 4.25)
The workstation printed device=CUDA:0; the phone prints device=Vulkan:0. Same program, same numbers. pick_device() from part 3 found the phone's GPU the same way it found the workstation's; on this phone the part 3 discovery loop lists:
CPU 0: ARM
Vulkan 0: Adreno (TM) 750
3. From an app: a few lines of Kotlin
ClikaRT is a C++ library, and an Android app reaches it through the app's own native code. Kotlin loads your library and calls your function; ClikaRT stays on the native side:
object Pipeline {
init { System.loadLibrary("pipeline") }
external fun run(): String
}
#include <jni.h>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_Pipeline_run(JNIEnv* env, jobject) {
const Tensor x = Tensor::ones({2, 8}, DataType::Float32);
const Tensor w = Tensor::full({4, 8}, 0.5, DataType::Float32);
const Tensor y = ops::relu(ops::linear(x, w));
return env->NewStringUTF(y.to_string().c_str());
}
The CMake side adds one target next to hello:
add_library(pipeline SHARED pipeline_jni.cpp)
target_link_libraries(pipeline PRIVATE ClikaRT::ClikaRT)
Beneath the JNI boundary the native side is whatever you built: C++, C and Rust all compile for arm64. Kotlin code can also skip the custom library entirely: the io.clika:clika-runtime artifact ships its own JNI bridge and calls the runtime directly from the app's jniLibs. Python stays on the server and desktop side.
An app has no shell to export a variable from and no per-user license file, so the credential is an argument instead. ClikaRtAndroid.load(context, license = "CLIKA1-...") places it for the process before the binding loads. A credential given there replaces one already in the process environment, and the default, null, leaves the environment as it is. Ship the credential the way you ship any other secret your app needs at start-up.
Every platform works this way: one bundle, one toolchain file, the same code. A Jetson needs no cross-compile at all: it is an arm64 Linux machine and runs the linux-arm64 build directly. The system requirements table lists the platforms and their backends. The tutorial ends here; what to read next.