Skip to main content

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

main.cpp
#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;
}
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.