---
title: Tensors and operators
sidebar_label: 2. Tensors and operators
description: "Factories and dtypes, the ops:: library, views, operator sugar, and reading values back to the host."
---

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](01-your-first-program.mdx); only `main.cpp` changes.

## The program

<LangTabs>

<LangTab value="cpp">

```cpp title="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;
}
```

</LangTab>

<LangTab value="python">

```python title="main.py"
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()
```

</LangTab>

<LangTab value="kotlin">

```kotlin title="Main.kt"
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() }
}
```

</LangTab>

</LangTabs>

```text
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](/api/index.md) 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](03-devices-and-async.mdx) explains what that means.

Next: [part 3](03-devices-and-async.mdx), devices, and the asynchrony you have been using without noticing.
