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.6.4: 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 the C++ library you built for arm64. Kotlin code can also skip the custom library entirely: the io.clika:clika-runtime artifact ships its own JNI bridge and every runtime library, and calls the runtime directly. 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.
What the app ships beside the library, the build levels it declares (compileSdk 36, minSdk 28, the Android Gradle plugin 9.3.2 on gradle 9.5 or newer), the model library's own libClikaRT_modelverse.so when the app runs a model from the library, and the two facts it places before the first call (the CPU worker count CLIKA_RT_NUM_THREADS, read once before the first compute, and the cache root) are on Package ClikaRT in an Android app.
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.