Your first pipeline
Part 4 puts the pieces together into the shape every real ClikaRT program has: load weights and input from disk, place them on a device, compute, read results back. It is self-contained. Stage 1 writes the files it needs, standing in for a real export.
The program
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <string>
#include <vector>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Device;
using ClikaRT::NamedTensors;
using ClikaRT::Tensor;
namespace io = ClikaRT::io;
namespace ops = ClikaRT::ops;
namespace {
// The layer's geometry and the checkpoint's names, each spelled once.
constexpr std::int64_t kFeatures = 8; // columns of x, columns of W
constexpr std::int64_t kOutputs = 4; // rows of W, the activations out
constexpr std::int64_t kBatch = 2;
constexpr const char* kWeight = "mlp.weight";
constexpr const char* kBias = "mlp.bias";
Device pick_device() { // from part 3
namespace device = ClikaRT::device;
if (device::is_cuda_available()) return Device::cuda();
if (device::is_vulkan_available()) return Device::vulkan();
if (device::is_metal_available()) return Device::metal();
return Device::cpu();
}
} // namespace
int main() {
const std::filesystem::path dir = std::filesystem::temp_directory_path();
const std::string ckpt = (dir / "first_program.safetensors").string();
const std::string input = (dir / "first_program_input.npy").string();
// ── Stage 1: write the artifacts (a stand-in for a real export) ──
// A checkpoint is a NamedTensors: a name -> tensor map.
NamedTensors weights;
weights.set(kWeight, Tensor::full({kOutputs, kFeatures}, 0.5, DataType::Float32));
weights.set(kBias, Tensor::full({kOutputs}, 0.25, DataType::Float32));
io::save_safetensors(weights, ckpt);
io::save_npy(Tensor::ones({kBatch, kFeatures}, DataType::Float32), input); // two rows of ones
// ── Stage 2: load, compute, read back ──
// Loaders take the target device: the tensors land there directly.
const Device dev = pick_device();
const NamedTensors loaded = io::load_safetensors(ckpt, dev);
const Tensor x = io::load_npy(input, dev);
// One MLP layer: y = relu(x * W^T + b), [2,8] x [4,8]^T -> [2,4].
const Tensor y = ops::relu(ops::linear(x, loaded.get(kWeight), loaded.get(kBias)));
// Per-row mean, then back to the host (the reads from part 3).
const std::vector<float> pooled = ops::mean(y, {1}).item_as_vec<float>();
std::printf("y = %s\n", y.to_string().c_str());
std::printf("row means = [%.2f, %.2f] (expected 8*1*0.5 + 0.25 = 4.25)\n",
pooled[0], pooled[1]);
return 0;
}
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include "clika_rt/clika_rt_core.h"
/* The api-table bootstrap shared by every C sample: dlopen the runtime,
* handshake at the header's ABI version and api-table layout fingerprint,
* exit loudly on any runtime error. */
static const clika_rt_api* api;
static void bootstrap(void) {
void* so = dlopen("libClikaRT.so", RTLD_NOW | RTLD_GLOBAL);
if (so == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(1); }
const clika_rt_api* (*get_api)(uint32_t) =
(const clika_rt_api* (*)(uint32_t))dlsym(so, "clika_rt_get_api");
api = get_api != NULL ? get_api(CLIKA_RT_ABI_VERSION) : NULL;
if (api == NULL) { fprintf(stderr, "clika_rt_get_api: handshake failed\n"); exit(1); }
if (strcmp(api->layout_fingerprint, CLIKA_RT_ABI_LAYOUT_FINGERPRINT) != 0) {
fprintf(stderr, "clika_rt_get_api: the runtime's api-table layout does not match "
"this header; rebuild against the runtime's header\n");
exit(1);
}
}
static void check(clika_rt_error* e, const char* where) {
if (e == NULL) return;
char msg[512];
size_t len = sizeof msg;
if (api->error_message(e, msg, &len) != CLIKA_RT_STATUS_OK) msg[0] = '\0';
fprintf(stderr, "%s failed: %s\n", where, msg);
api->error_free(e);
exit(1);
}
/* The C ABI loads safetensors but has no save member, so the checkpoint is
* written by hand: a little-endian u64 header length, the JSON header, then
* the payloads back to back (a stand-in for a real export). mlp.weight is
* 4x8 floats of 0.5 (128 bytes), mlp.bias 4 floats of 0.25 (16 bytes). */
static void write_checkpoint(const char* path) {
static const char header[] =
"{\"mlp.weight\":{\"dtype\":\"F32\",\"shape\":[4,8],\"data_offsets\":[0,128]},"
"\"mlp.bias\":{\"dtype\":\"F32\",\"shape\":[4],\"data_offsets\":[128,144]}}";
const uint64_t hlen = sizeof header - 1;
unsigned char len_le[8];
float v;
int i;
FILE* f = fopen(path, "wb");
if (f == NULL) { perror(path); exit(1); }
for (i = 0; i < 8; ++i) len_le[i] = (unsigned char)(hlen >> (8 * i));
fwrite(len_le, 1, sizeof len_le, f);
fwrite(header, 1, (size_t)hlen, f);
v = 0.5F; /* payloads are little-endian IEEE floats, the host's own layout here */
for (i = 0; i < 4 * 8; ++i) fwrite(&v, sizeof v, 1, f);
v = 0.25F;
for (i = 0; i < 4; ++i) fwrite(&v, sizeof v, 1, f);
fclose(f);
}
int main(void) {
bootstrap();
const clika_rt_stream_or_device dflt = {CLIKA_RT_STREAM_OR_DEVICE_DEFAULT};
/* temp_directory_path answers a one-entry string list (a mutating
* member never rides the sized two-call protocol). */
clika_rt_string_list* tl = NULL;
check(api->temp_directory_path(&tl), "temp_directory_path");
char dir[1024];
size_t dlen = sizeof dir;
api->string_list_get_at(tl, 0, dir, &dlen);
api->string_list_free(tl);
char ckpt[1200], input[1200];
snprintf(ckpt, sizeof ckpt, "%s/first_program.safetensors", dir);
snprintf(input, sizeof input, "%s/first_program_input.npy", dir);
/* ── Stage 1: write the artifacts (a stand-in for a real export) ── */
/* A checkpoint is a name -> tensor map on disk; see write_checkpoint. */
write_checkpoint(ckpt);
float batch[2 * 8];
for (int i = 0; i < 2 * 8; ++i) batch[i] = 1.0F; /* two rows of ones */
clika_rt_tensor* xin = NULL;
check(api->tensor_from_data(batch, (const int64_t[]){2, 8}, 2,
CLIKA_RT_DATA_TYPE_FLOAT32, dflt, dflt, &xin), "from_data");
check(api->save_npy(xin, input, strlen(input)), "save_npy");
api->tensor_release(xin);
/* ── Stage 2: load, compute, read back ── */
/* Loaders take the target device: the tensors land there directly. */
const clika_rt_device dev = api->device_gpu(0); /* the part 3 probe */
clika_rt_stream_or_device where = {0};
where.kind = CLIKA_RT_STREAM_OR_DEVICE_DEVICE;
where.device = dev;
clika_rt_tensors_container* loaded = NULL;
check(api->load_safetensors_to_tensors_container(ckpt, strlen(ckpt),
where, 0, &loaded), "load");
clika_rt_tensor* x = NULL;
check(api->load_npy(input, strlen(input), dev, &x), "load_npy");
/* One MLP layer: y = relu(x * W^T + b), [2,8] x [4,8]^T -> [2,4]. */
clika_rt_tensor* lw = NULL;
clika_rt_tensor* lb = NULL;
check(api->tensors_container_get(loaded, "mlp.weight", 10, &lw), "get");
check(api->tensors_container_get(loaded, "mlp.bias", 8, &lb), "get");
clika_rt_tensor* y = NULL;
check(api->op_linear(x, lw, lb, 1, CLIKA_RT_ACTIVATION_RELU, 0.0, 0.0, &y),
"linear");
/* Per-row mean, then back to the host (the reads from part 3). */
api->tensor_retain(y);
clika_rt_tensor* pooled = NULL;
check(api->op_mean(y, (const int64_t[]){1}, 1, 0,
CLIKA_RT_DATA_TYPE_UNDEFINED, &pooled), "mean");
clika_rt_tensor* host = NULL;
check(api->tensor_to_device(pooled, api->device_cpu(0), &host), "to_cpu");
const void* p = NULL;
check(api->tensor_const_data_ptr(host, &p), "const_data_ptr");
char buf[512];
size_t blen = sizeof buf;
api->tensor_to_string(y, buf, &blen);
printf("y = %s\n", buf);
printf("row means = [%.2f, %.2f] (expected 8*1*0.5 + 0.25 = 4.25)\n",
((const float*)p)[0], ((const float*)p)[1]);
api->tensor_release(host);
api->tensor_release(pooled);
api->tensor_release(y);
api->tensors_container_free(loaded);
return 0;
}
import tempfile
import clika_runtime as crt
import clika_runtime.nn.functional as F
def main() -> None:
tmp = tempfile.gettempdir()
ckpt = f"{tmp}/first_program.safetensors"
inp = f"{tmp}/first_program_input.npy"
# -- Stage 1: write the artifacts (a stand-in for a real export) --
# A checkpoint is a plain dict: a name -> tensor map.
weights = {
"mlp.weight": crt.full((4, 8), 0.5),
"mlp.bias": crt.full((4,), 0.25),
}
crt.io.save_safetensors(weights, ckpt)
crt.io.save_npy(crt.ones(2, 8), inp) # two rows of ones
# -- Stage 2: load, compute, read back --
# Loaders take the target device: the tensors land there directly.
dev = crt.Device.gpu() # the part 3 probe
loaded = crt.io.load_safetensors(ckpt, device=dev)
x = crt.io.load_npy(inp, device=dev)
# One MLP layer: y = relu(x * W^T + b), [2,8] x [4,8]^T -> [2,4].
# nn.functional (imported as F by convention) carries the stateless
# neural-network operations; the same functions the nn modules call.
y = F.relu(F.linear(x, loaded["mlp.weight"], loaded["mlp.bias"]))
# Per-row mean, then back to the host (the reads from part 3).
pooled = y.mean([1]).to("cpu").numpy()
print(f"y = {y}")
print(f"row means = [{pooled[0]:.2f}, {pooled[1]:.2f}] "
"(expected 8*1*0.5 + 0.25 = 4.25)")
if __name__ == "__main__":
main()
The typed Kotlin tier does not reach checkpoint files yet: safetensors and npy loading exist only on the generated low-level tier (raw handles that do not mix with the typed Tensor), and the save-side members are not in the C ABI it rides. The compute pieces of this part are typed today (F.linear, F.relu, F.mean, Tensor.to(device)); the C++ arm is the pipeline story.
//go:build ignore
package main
import (
"encoding/binary"
"fmt"
"log"
"math"
"os"
"unsafe"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
// The C ABI loads safetensors but has no save member, so the checkpoint is
// written by hand: a little-endian u64 header length, the JSON header, then
// the payloads back to back (a stand-in for a real export). mlp.weight is
// 4x8 floats of 0.5 (128 bytes), mlp.bias 4 floats of 0.25 (16 bytes).
func writeCheckpoint(path string) error {
header := `{"mlp.weight":{"dtype":"F32","shape":[4,8],"data_offsets":[0,128]},` +
`"mlp.bias":{"dtype":"F32","shape":[4],"data_offsets":[128,144]}}`
buf := binary.LittleEndian.AppendUint64(nil, uint64(len(header)))
buf = append(buf, header...)
for i := 0; i < 4*8; i++ {
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(0.5))
}
for i := 0; i < 4; i++ {
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(0.25))
}
return os.WriteFile(path, buf, 0o644)
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
ckpt := os.TempDir() + "/first_program.safetensors"
input := os.TempDir() + "/first_program_input.npy"
// -- Stage 1: write the artifacts (a stand-in for a real export) --
// A checkpoint is a name -> tensor map on disk; see writeCheckpoint.
if err := writeCheckpoint(ckpt); err != nil {
log.Fatal(err)
}
batch := make([]float32, 2*8)
for i := range batch {
batch[i] = 1 // two rows of ones
}
xin := must(clikart.TensorOf(api, batch, []int64{2, 8}))
if err := api.SaveNpy(xin, input); err != nil {
log.Fatal(err)
}
// -- Stage 2: load, compute, read back --
// Loaders take the target placement: the tensors land there directly.
dev := api.DeviceGpu(0) // the part 3 probe
loaded := must(api.LoadSafetensorsToTensorsContainer(ckpt,
clikart.StreamOrDevice{Kind: clikart.StreamOrDeviceDevice, Device: dev}, false))
x := must(api.LoadNpy(input, dev))
// One MLP layer: y = relu(x * W^T + b), [2,8] x [4,8]^T -> [2,4].
lw := must(api.TensorsContainerGet(loaded, "mlp.weight"))
lb := must(api.TensorsContainerGet(loaded, "mlp.bias"))
y := must(must(clikart.Linear(x, lw, lb, nil)).Relu())
// Per-row mean, then back to the host (the reads from part 3).
pooledDev := must(clikart.Mean(y, []int64{1}, false))
pooled := must(api.TensorToDevice(pooledDev, api.DeviceCpu(0)))
p := unsafe.Slice((*float32)(must(api.TensorConstDataPtr(pooled))), 2)
fmt.Printf("y = %s\n", y)
fmt.Printf("row means = [%.2f, %.2f] (expected 8*1*0.5 + 0.25 = 4.25)\n", p[0], p[1])
}
use std::io::Write;
use clika_rt::{sys, Api};
// The crate has no safetensors save API (the C ABI it rides has none yet);
// a minimal checkpoint is a u64 LE header length, a JSON header, then the
// payloads back to back. The upstream rust chapter writes it by hand too.
fn write_checkpoint(path: &std::path::Path,
entries: &[(&str, &[i64], f32)]) -> std::io::Result<()> {
let mut header = String::from("{");
let mut offset = 0usize;
for (i, (name, shape, _)) in entries.iter().enumerate() {
let numel: i64 = shape.iter().product();
let end = offset + numel as usize * 4;
let dims = shape.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(",");
if i > 0 { header.push(','); }
header.push_str(&format!(
"\"{name}\":{{\"dtype\":\"F32\",\"shape\":[{dims}],\"data_offsets\":[{offset},{end}]}}"
));
offset = end;
}
header.push('}');
let mut f = std::fs::File::create(path)?;
f.write_all(&(header.len() as u64).to_le_bytes())?;
f.write_all(header.as_bytes())?;
for (_, shape, value) in entries {
let numel: i64 = shape.iter().product();
for _ in 0..numel { f.write_all(&value.to_le_bytes())?; }
}
Ok(())
}
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let tmp = std::env::temp_dir();
let ckpt = tmp.join("first_program.safetensors");
let input = tmp.join("first_program_input.npy");
// -- Stage 1: write the artifacts (a stand-in for a real export) --
write_checkpoint(&ckpt, &[("mlp.weight", &[4, 8], 0.5),
("mlp.bias", &[4], 0.25)]).expect("write checkpoint");
let batch = [1.0f32; 2 * 8]; // two rows of ones
let x_host = api.tensor_from_slice(&batch, &[2, 8])?;
api.f_save_npy(&x_host, input.to_str().expect("utf-8 temp path"))?;
// -- Stage 2: load, compute, read back --
// Loaders take a placement policy: the tensors land on that device.
let dev = api.device_gpu(0); // the part 3 probe
let policy = sys::clika_rt_stream_or_device {
kind: sys::clika_rt_stream_or_device_kind_CLIKA_RT_STREAM_OR_DEVICE_DEVICE as i32,
device: dev,
stream: std::ptr::null_mut(),
};
let loaded = api.f_load_safetensors_to_tensors_container(
ckpt.to_str().expect("utf-8 temp path"), policy, false)?;
let x = api.f_load_npy(input.to_str().expect("utf-8 temp path"), dev)?;
// One MLP layer with the relu fused in: y = relu(x * W^T + b),
// [2,8] x [4,8]^T -> [2,4]. Explicit calls consume their tensor args.
let lw = api.f_tensors_container_get(&loaded, "mlp.weight")?;
let lb = api.f_tensors_container_get(&loaded, "mlp.bias")?;
let y = api.f_op_linear(x, lw, lb, true,
sys::clika_rt_activation_CLIKA_RT_ACTIVATION_RELU, 0.0, 0.0)?;
// Per-row mean, then back to the host (the reads from part 3);
// the shallow_clone keeps y live for the print below.
let pooled = api.f_op_mean(y.shallow_clone(), &[1], false,
sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_UNDEFINED)?;
let host = api.f_tensor_to_device(&pooled, api.device_cpu(0))?;
let p = api.f_tensor_const_data_ptr(&host)? as *const f32;
println!("y = {}", api.tensor_to_string(&y)?);
let (r0, r1) = unsafe { (*p, *p.add(1)) };
println!("row means = [{r0:.2}, {r1:.2}] (expected 8*1*0.5 + 0.25 = 4.25)");
Ok(())
}
On a machine with an NVIDIA GPU (the device line follows what pick_device() found):
y = Tensor(shape=[2, 4], dtype=Float32, device=CUDA: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)
Every element of y is 8 * 1 * 0.5 + 0.25 = 4.25, so both row means print 4.25, on whatever device the machine offered.
Model checkpoints are NamedTensors
A model's weights travel as a NamedTensors, a name -> tensor map with set / get / size / for_each. io::save_safetensors / io::load_safetensors round-trip it; io::load_gguf returns the same map plus the file's metadata; io::load_npy reads a single array. The loaders take the target device, so weights stream to where they will be used with no separate .to() step. That is the placement lesson from part 3, folded into I/O.
The pipeline shape
Stage 2 is the skeleton to keep: discover -> load onto the device -> compute -> read back. Scaling it up changes the sizes, not the shape (more names in the checkpoint, a deeper stack of ops:: calls between load and read). The pieces this series did not need live in the examples. The runtime project wraps this exact shape in sessions, continuous batching and pipelines, and io's later chapters cover GGUF and images.
You have a complete, device-portable ClikaRT program. Next: part 5, the same model behind the serving runtime.