Beyond the Transistor

Key concepts

Learn the handful of rules that every toolkit call follows, so that the examples and your own code make sense.

This page explains the ideas that the toolkit API is built on. Each section links a rule to the function or error you will meet in practice.

The device runs forward passes only

The NPU computes inference. It doesn’t compute gradients. Every function in the ai module ends in _fprop, short for forward propagation.

To use a model on the NPU, train it on the host first, for example with PyTorch on your CPU or GPU. Then pass the trained weights to the toolkit for inference. The Fourier network example shows this pattern with PyTorch layers that switch backend in eval mode.

Data is bfloat16

Every input array must be a NumPy array of dtype ml_dtypes.bfloat16, and every output comes back as bfloat16. The ml_dtypes package provides the type.

import numpy as np
from ml_dtypes import bfloat16

x = np.random.rand(128, 32).astype(bfloat16)

If you pass float32, the toolkit raises an error before any computation:

TypeError: features must be of dtype <class 'ml_dtypes.bfloat16'> but is float32

bfloat16 keeps the range of float32 but stores only 8 bits of mantissa. That gives about three significant decimal digits. For example, 1.2345678 is stored as 1.234375. Expect small differences when you compare toolkit output with a float32 reference. The introduction example measures them for each operation.

Device lifecycle

The generic module manages devices.

FunctionWhat it does
generic.get_available_npus()Returns a dictionary that maps each device ID to a serial number.
generic.init_npu(id)Initialises a device. The docstring says this is optional and not necessary for computation.
generic.release_npu(id)Releases a device and frees its memory. The docstring says this matters when several users share a device.

Every operation takes an optional device_id argument, which defaults to 0:

import qant_native_computing_toolkit as q

q.generic.init_npu(0)
out = q.ai.relu_fprop(x, device_id=0)
q.generic.release_npu(0)

On the CPU emulator, get_available_npus() returns {0: 'cpu_backend'}, and init_npu, release_npu and device_id have no effect.

Align buffers to page boundaries

The toolkit documentation says the NPU requires all input buffers to be aligned to a 4096-byte page boundary. If a buffer is not aligned, the runtime makes an extra copy, which costs time.

To avoid the copy, align your arrays once with utils.align_ndarray_page_boundary(). It returns the array unchanged if it is already aligned, and an aligned copy otherwise:

features = np.random.rand(128, 32).astype(bfloat16)
weights = np.random.rand(64, 32).astype(bfloat16)

features = q.utils.align_ndarray_page_boundary(features)
weights = q.utils.align_ndarray_page_boundary(weights)

out = q.ai.linear_fprop(features, weights)

Align arrays that you reuse, such as weights, when you load them, not before every call.

Linear weights are transposed

linear_fprop computes features @ weights.T, not features @ weights. The weights have shape (n_out, n_in), the same layout as a PyTorch nn.Linear weight.

ArgumentShape
features(..., n_in). Extra leading dimensions are batch dimensions.
weights(n_out, n_in)
Result(..., n_out)

This means you can pass a PyTorch state dict weight directly after converting it to bfloat16.

Native and built operations

The NPU hardware computes a small set of primitives. In the toolkit’s driver interface, these are:

  • element-wise multiplication
  • batched matrix-vector multiplication, which covers matrix-vector and matrix-matrix products
  • the scaled periodic nonlinearity, a cosine-like function

The native module exposes them as mul_elementwise, linear_fprop and calc_scaled_periodic_nl_fprop.

The ai module builds layers on top of these primitives in the toolkit’s Rust core. In the 2.3.1 source:

OperationHow the Rust core computes it
linear_fprop, conv_fprop, conv_transpose_fpropBatched matrix-vector multiplication on the device. Convolutions are rearranged into matrix products first.
batchnorm2d_fpropNormalises on the host, then scales with element-wise multiplication on the device.
calc_kan_layer_fpropThe scaled periodic nonlinearity on the device.
relu_fprop, sigmoid_fprop, softmax_fprop, add_bias_fprop, poolingComputed in the Rust core on the host, in bfloat16. These don’t call the device primitives.

You call all of them the same way, so this split only matters when you reason about which parts of a model the optical hardware computes.

The cosine-like nonlinearity

calc_scaled_periodic_nl_fprop(u, v) computes tcos(u) * v for each pair of elements. The toolkit describes tcos as a 2π-periodic function with values between -1 and 1, similar to a cosine. On hardware, it is the shape of the optical nonlinearity. On the CPU emulator, it is an exact cosine.

The KAN layer

ai.calc_kan_layer_fprop builds a Kolmogorov-Arnold Network (KAN) layer from tcos. Each output is a sum of cosine-like terms with learnable phases and amplitudes:

y_j = Σ over i and l of  tcos(ks_l · x_i + φ_jil) · ampls_jil
y = q.ai.calc_kan_layer_fprop(features, phis, ampls, ks)
ArgumentShapeMeaning
features(batch, n_in)The input, written x in the formula
phis(n_out, n_in, n_k)Phase offsets
ampls(n_out, n_in, n_k)Amplitudes
ks(n_k,)Frequencies, often 1, 2, 3 and so on
Result(batch, n_out)The output, written y in the formula

Softmax takes one row at a time

softmax_fprop does not accept batched input. If you pass a 2D array with more than one row, the toolkit raises:

RuntimeError: Rust core error: Toolkit Error: does not support batched input.

Call it once per row, or pass a 1D array. The docstring says that for multidimensional input the first dimension must be 1.

conv_transpose_fprop has the same limit. A 4D input with more than one image in the batch raises the same error on the emulator, so loop over the images instead.

Deprecated multiply functions

The native module still includes mul_i16 and mul_f32 from earlier versions. mul_i16 raises a DeprecationWarning that tells you to use mul_elementwise instead, and a comment in the Rust source says the int16 path will be removed in 2.4. Use mul_elementwise with bfloat16 arrays in new code.

What’s next