Tensors and operators
Part 1 made one tensor; this part covers the compute vocabulary you will use everywhere: building tensors, transforming them with ops::, and reading values back. Same project as part 1; only main.cpp changes.
The program
- C++
- Python
- Kotlin
#include <cstdio>
#include <vector>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;
int main() {
// Factories build tensors from a shape and a dtype.
const Tensor threes = Tensor::full({2, 3}, 3.0, DataType::Float32);
// from_data copies host bytes into a tensor of the given shape + dtype.
const float host[6] = {0, 1, 2, 3, 4, 5};
const Tensor x = Tensor::from_data(host, {2, 3}, DataType::Float32);
// ops:: free functions return their result directly. Elementwise math
// broadcasts, and a scalar binds wherever a tensor does.
const Tensor y = ops::add(ops::mul(x, 2.0), threes); // y = 2x + 3
// Shape ops are views: the same bytes behind a new layout, no copy.
const Tensor yt = ops::permute(y, {1, 0}); // 2x3 -> 3x2
const Tensor flat = ops::reshape(y, {6});
// Tensor carries operator and method sugar over the same ops, so a
// chain reads like the math it computes.
const Tensor m = (y - 3.0).abs().max();
// Reading back: to_string() for a summary, item<T>() for the single
// element of a one-element tensor, item_as_vec<T>() for a 0-D/1-D tensor.
std::printf("y = %s\n", y.to_string().c_str());
std::printf("y^T = %s\n", yt.to_string().c_str());
std::printf("max|y - 3| = %.0f\n", m.item<float>());
const std::vector<float> v = flat.item_as_vec<float>();
std::printf("flat = [");
for (std::size_t i = 0; i < v.size(); ++i) std::printf("%s%.0f", i ? ", " : "", v[i]);
std::printf("]\n");
return 0;
}
import clika_runtime as crt
def main() -> None:
# Factories build tensors directly; dtypes are attributes on the package.
threes = crt.full((2, 3), 3.0)
x = crt.arange(6, dtype=crt.float32).reshape(2, 3)
# Elementwise math reads as operators; scalars broadcast.
y = 2 * x + threes # y = 2x + 3
# Shape methods are views: the same bytes behind a new layout, no copy.
yt = y.permute(1, 0) # 2x3 -> 3x2
flat = y.reshape(-1)
# Method chains: |y - 3| reduced to its global maximum. amax is the
# global reduce; max(dim) is the dim-wise form returning values and
# indices.
m = (y - 3).abs().amax()
# Reading back: repr(t) is the summary, item() reads a scalar, and
# numpy() on a CPU tensor is a zero-copy view when an array is wanted.
print(f"y = {y}")
print(f"y^T = {yt}")
print(f"max|y - 3| = {m.item():.0f}")
v = flat.numpy()
print("flat = [" + ", ".join(f"{e:.0f}" for e in v) + "]")
if __name__ == "__main__":
main()
import io.clika.runtime.ClikaRt
import io.clika.runtime.Ops
import io.clika.runtime.Tensors
import java.util.Locale
fun main() {
ClikaRt.load()
// Factories build tensors from a shape or from host values; float32 is
// the default dtype, and Tensors.of copies the values in.
val threes = Tensors.full(longArrayOf(2, 3), 3.0)
val x = Tensors.of(floatArrayOf(0f, 1f, 2f, 3f, 4f, 5f), longArrayOf(2, 3))
// Ops carries one function per operator. Elementwise math broadcasts,
// and a number binds wherever a tensor does.
val scaled = Ops.mul(x, 2.0)
val y = Ops.add(scaled, threes) // y = 2x + 3
// Shape operators are views: the same bytes behind a new layout, no copy.
val yt = Ops.permute(y, longArrayOf(1, 0)) // 2x3 -> 3x2
val flat = Ops.reshape(y, longArrayOf(6))
// A chain of operators reads like the math it computes: max|y - 3| = 10.
val d = Ops.sub(y, 3.0)
val ad = Ops.abs(d)
val m = Ops.amax(ad)
// Reading back: summary() renders the shape, dtype, device and values;
// item() reads the one element of a one-element tensor; toFloatArray()
// copies a tensor's values out.
println("y = ${y.summary()}")
println("y^T = ${yt.summary()}")
println("max|y - 3| = ${"%.0f".format(Locale.ROOT, m.item())}")
println("flat = [${flat.toFloatArray().joinToString(", ") { "%.0f".format(Locale.ROOT, it) }}]")
listOf(x, threes, scaled, y, yt, flat, d, ad, m).forEach { it.release() }
}
y = Tensor(shape=[2, 3], dtype=Float32, device=CPU, numel=6, data=[3, 5, 7, 9, 11, 13])
y^T = Tensor(shape=[3, 2], dtype=Float32, device=CPU, numel=6, data=[3, 9, 5, 11, 7, 13])
max|y - 3| = 10
flat = [3, 5, 7, 9, 11, 13]
Dtypes
DataType names the element format. The everyday set is Float32, Float16, BFloat16, Float64, the signed and unsigned integer widths (Int8 ... Int64, UInt8 ... UInt64) and Bool; beyond it are the sub-byte integers (Int4, Int2) and the narrow float families (FP8, FP6, FP4) that quantized models use. ClikaRT::data_type_name(t.dtype()) prints one; t.to(DataType::Float16) casts. Factories take the dtype explicitly. Nothing defaults behind your back.
Copies are handles
A Tensor copy is a cheap reference to the same underlying data, not a deep copy. Writes through one copy are visible through the others, and the data stays alive as long as any copy does. For independent data, build a fresh tensor (a factory or from_data, which copies the source bytes and does not retain the pointer).
The ops:: library
Every operator is a free function in ClikaRT::ops, taking tensors and returning a tensor: elementwise math, reductions, matrix products, convolutions, attention, indexing. This is the operator set a model needs. Shape ops (reshape, permute, narrow) return views, metadata over the source's bytes, no copy. For the common ones, Tensor adds sugar. Arithmetic operators (y - 3.0) and chainable methods (.abs(), .relu(), .max(), .matmul(...)) forward to the same ops:: functions with the same error contract as part 1. The API reference documents every operator.
Reading values back
Three host-side reads, in increasing weight. to_string() is an infallible summary (shape, dtype, device, first values) for logging. item<T>() reads the single element of a one-element tensor, typically a reduction result. item_as_vec<T>() reads all elements of a 0-D or 1-D tensor, contiguous and dtype-matched. A wrong T or a wrong element count raises ClikaRT::Error. Every one of these reads is also a synchronization point; part 3 explains what that means.
Next: part 3, devices, and the asynchrony you have been using without noticing.