Skip to main content

Use ClikaRT from Python

The clika-runtime wheel puts the runtime behind one import: import clika_runtime as crt loads libClikaRT.so and its backends from inside the wheel, no environment setup. The Python surface is deliberately PyTorch-shaped, and NumPy plays exactly three roles: data entry, data exit, and the independent oracle you check results against. Compute rides the runtime.

This guide assumes the wheel is installed; First steps covers getting it. Everything below is one script's worth of ground: the NumPy boundary, operator chains, device placement, a model as nn.Module, and the error contract.

NumPy in, NumPy out

crt.tensor(array) is the entry, t.numpy() the exit, and both edges are explicit about copying: entry copies, so mutating the source afterwards changes nothing.

boundary.py
import numpy as np
import clika_runtime as crt

a = np.ones((2, 5), dtype=np.float32)
t = crt.tensor(a)

print(t.shape, t.dtype, t.device) # (2, 5) DataType.Float32 Device(CPU:-1)
assert t.dtype == crt.float32 # the short alias IS the dtype object
assert t.dtype == crt.DataType.Float32
assert t.device == crt.Device.cpu()

a[0, 0] = 999.0 # entry copied: the tensor is unmoved
assert t.numpy()[0, 0] == 1.0

t64 = crt.tensor(np.ones(3, dtype=np.float64)) # float64 enters natively
t32 = t64.to(crt.float32) # narrowing is explicit
print(t32.dtype) # DataType.Float32

Every NumPy-native dtype enters as itself (the float family, the signed ints, uint8, bool); a dtype with no tensor twin, complex64 for example, is refused with a TypeError that names the routes out. Payload dtypes NumPy cannot spell, bfloat16 among them, cross through bytes() / from_bytes() instead of the array bridge.

Math that reads as math

Operators compose the way the expression reads: Python numbers broadcast, @ is matmul, and method chains mirror the functional forms. Check anything against NumPy; that is what the oracle role means.

math.py
import numpy as np
import clika_runtime as crt

x = crt.tensor(np.arange(6, dtype=np.float32).reshape(2, 3))

y = 2 * x + 3 # scalars broadcast
z = (y - 3).abs().amax().item() # method chain down to one float
print(z) # 10.0: |2 * 5| after the -3

a = crt.tensor(np.ones((2, 3), dtype=np.float32))
b = crt.tensor(np.ones((3, 2), dtype=np.float32))
print((a @ b).numpy()) # [[3. 3.] [3. 3.]]

Placement is a constructor argument or a move: crt.tensor(arr, device=...) lands data where you say, .to(device) moves it, and crt.Device.gpu() is the probe-with-fallback (a loaded accelerator when the machine has one, else the CPU), so the same script runs everywhere.

A model is an nn.Module

Assigning a layer in __init__ registers it, exactly as in PyTorch: load_state_dict binds dotted names, named_parameters() enumerates them, state_dict() exports the same names back out, and the instance is callable.

model.py
import numpy as np
import clika_runtime as crt
import clika_runtime.nn as nn

class TinyMlp(nn.Module):
def __init__(self, d_in: int, d_hidden: int, d_out: int) -> None:
super().__init__()
# The first layer fuses its activation as an epilogue.
self.up = nn.Linear(d_in, d_hidden, activation=crt.Activation.Gelu)
self.down = nn.Linear(d_hidden, d_out, bias=False)

def forward(self, x: crt.Tensor) -> crt.Tensor:
return self.down(self.up(x))

model = TinyMlp(4, 8, 2)
model.load_state_dict({
"up.weight": crt.tensor(np.full((8, 4), 0.1, dtype=np.float32)),
"up.bias": crt.tensor(np.zeros(8, dtype=np.float32)),
"down.weight": crt.tensor(np.full((2, 8), 0.1, dtype=np.float32)),
})

y = model(crt.tensor(np.ones((3, 4), dtype=np.float32)))
print(y.shape) # (3, 2)

exported = model.state_dict() # the same dotted names back out
print(sorted(exported)) # ['down.weight', 'up.bias', 'up.weight']

load_state_dict is strict: a missing or unexpected key raises with both name sets in the message. The state_dict() -> fresh load_state_dict() round trip reproduces the forward exactly, which is the portable way to hand weights between processes.

When it fails

A runtime failure raises RuntimeError (from operator expressions too), and the message carries a bracketed [code: NAME] suffix: the machine channel. Branch on the code name, never the message text: an argument mistake reads as a sentence naming the operation and the values, an E<digits> message is an internal fault code specific to the build that produced it (report it verbatim with the runtime version), and the code name is the channel that is stable across builds.

errors.py
import numpy as np
import clika_runtime as crt

def code_name_of(err: RuntimeError) -> str:
text = str(err)
marker = text.rfind("[code: ")
return text[marker + 7:].rstrip("]") if marker >= 0 else ""

try:
a = crt.tensor(np.ones((2, 3), dtype=np.float32))
b = crt.tensor(np.ones((4, 5), dtype=np.float32))
_ = a @ b # shape mismatch
except RuntimeError as e:
print(f"failed with code {code_name_of(e)!r}")

From here, the rest of the Python surface follows the same grammar: GGUF and quantized weights and tokenizers have Python arms on their pages, ONNX models compile and run, and tracing turns eager functions into graphs. The wheel's own example chapters (sixteen scripts, each self-asserting) double as a smoke suite for an installed wheel.