API reference

The portable public surface lives in five headers:

Header Contents
<brotensor/tensor.h> Tensor, Dtype, Device, factories, migration, host accessors, dtype + bit-conversion helpers
<brotensor/runtime.h> init() / shutdown(), device policy, compute_dtype(), device memory, sync
<brotensor/ops.h> The device-neutral op surface — an umbrella over the per-category headers in <brotensor/ops/> (see op-coverage.md)
<brotensor/safetensors.h> safetensors reader + writer
<brotensor/gguf.h> GGUF reader

Two further headers are backend-specific and compile only in a build that enables that backend:

Header Contents
<brotensor/cuda_graph.h> CUDA graph capture / replay (CudaGraph, CudaGraphCapture) — CUDA-only; gate on BROTENSOR_HAS_CUDA
<brotensor/metal_interop.h> Metal custom-kernel surface — Obj-C++ / .mm consumers only

All preconditions and dispatch failures throw std::runtime_error with a "brotensor: <op>: <reason>" message.

Tensor

A row-major (rows, cols) buffer with runtime Dtype + Device tags. Copyable (device-aware deep copy) and movable. Rank-1 data is (N, 1); higher-rank layouts are flattened per each op's shape contract.

enum class Device { CPU, CUDA, Metal };
enum class Dtype  { FP32, FP16, INT8, INT32, BF16, F64,
                    Q4_0, Q4_1, Q5_0, Q5_1, Q8_0, Q8_1,        // GGUF legacy blocks
                    Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, Q8_K };       // GGUF K-quant superblocks

Factories

Factory Meaning
Tensor::zeros(r, c, dt = FP32) / Tensor::empty(r, c, dt = FP32) Allocate on the default device. zeros zero-fills; empty leaves contents undefined.
Tensor::zeros_on(dev, r, c, dt) / Tensor::empty_on(dev, r, c, dt) Same, pinned to an explicit device.
Tensor::from_host(ptr, r, c) (+ _fp16 / _bf16 / _int8 variants) Copy a host buffer to a new tensor on the default device. FP16/BF16 take uint16_t bit patterns, INT8 takes int8_t.
Tensor::from_host_on(dev, ptr, r, c) (+ _fp16_on / _bf16_on / _int8_on) Same, pinned to an explicit device.
Tensor::from_raw_bytes_on(dev, src, r, c, dt, nbytes) Dtype-agnostic byte-level bootstrap. Copies raw bytes rather than interpreting elements, so unlike from_host* it works for any dtype including the opaque GGUF block-quant carriers. nbytes must equal dtype_storage_bytes(dt, r*c).
Tensor::mat(r, c) / Tensor::vec(n) Zero-filled FP32 host (CPU) tensors — build parameters on the host, then migrate with to().
Tensor::view(dev, ptr, r, c, dt) Non-owning view over an existing backend-resident pointer. resize() on a view throws.

Migration and readback

Member Meaning
t.to(device) Returns a copy migrated to another backend; the source is unchanged.
t.clone() Device-preserving deep copy.
t.to_host_vector() (+ _fp16 / _bf16) Read back to a std::vector (float / uint16_t bits).
t.copy_to_host(dst) (+ _fp16 / _bf16) Read back into a caller-owned buffer.
t.zero() memset the buffer to zero over bytes().
t.resize(r, c, dt) Reshape in place; contents undefined afterwards (call zero() if needed), device preserved. Throws on a negative dimension or a non-owning view.

resize() reuses storage whenever the requested shape fits the existing allocation — capacity is the high-water mark of the tensor's past sizes — and reallocates only when growing past it. A no-op when the shape and dtype already match. So a scratch buffer cycling through shapes stabilises at its largest size instead of reallocating every call, and its device pointer stays stable — which is what makes a tensor reusable across a CUDA-graph-captured op sequence.

Call sync(device) / sync_all() before reading GPU results back to the host — GPU ops are asynchronous.

CPU-resident tensors additionally expose direct host accessors (host_f32_mut(), at(), operator[], …) — see tensor.h.

Dtype helpers

Free functions for sizing a buffer without special-casing the quant carriers:

Helper Meaning
dtype_size_bytes(dt) Bytes per element. Returns 0 for the block-quant dtypes — they aren't element-addressable.
dtype_block_size(dt) Elements per block (32 for the legacy quants, 256 for the K-quants, 1 otherwise).
dtype_block_bytes(dt) Encoded bytes per block.
dtype_storage_bytes(dt, n) Bytes needed for n elements. Use this for buffer sizes — it's correct for quant and non-quant dtypes alike.
dtype_is_quant(dt) Whether dt is a GGUF block-quant carrier.
device_name(dev) The backend kind as a string ("cpu" / "cuda" / "metal").

Bit-conversion helpers

fp32_to_fp16_bits / fp16_bits_to_fp32 / fp32_to_bf16_bits / bf16_bits_to_fp32 — pure-CPU scalar conversions between FP32 and half/bfloat bit patterns, for tests and small host-side preprocessing.

Runtime

Function Meaning
init() Idempotent. Probes and registers the CUDA / Metal backends. CPU is always registered (static-init), so CPU-only code works without calling it.
shutdown() Joins the CPU backend's worker threads. Idempotent, and safe even if init() was never called. See the note below — call it before returning from main().
default_device() Where no-suffix factories allocate. Best available: CUDA > Metal > CPU.
set_default_device(dev) Global override. Also overridable per-process via the BROTENSOR_DEFAULT_DEVICE env var (cpu / cuda / metal).
DeviceScope scope(dev) RAII per-scope default-device override.
compute_dtype() The dtype a model loader should upload weights at for the current default device: FP32 on CPU, FP16 on a GPU.
available_devices() / is_available(dev) Backends registered in this binary at runtime.
sync(dev) / sync_all() Drain pending backend work (no-op on CPU).
device_mem_info(dev, free, total) Device-wide free/total bytes. Returns false (outputs untouched) when the backend can't report; CPU always returns false.
device_mem_trim(dev, keep_bytes = 0) Return the allocator's cached-but-unused memory to the driver, keeping at most keep_bytes. Syncs the device first so stream-ordered frees are reclaimable.
device_product_name(dev) The card's human-readable name (e.g. "NVIDIA GeForce RTX 4090") — distinct from device_name(), which is the backend kind. "" if unavailable.

Shutdown. The CPU backend's worker threads otherwise live until the thread pool's Meyers-singleton destructor runs during static destruction, by which point the OS has already suspended every other thread. A worker suspended mid-op while holding a global lock (e.g. the Debug CRT's iterator-checking mutex) can deadlock the main thread's own exit-time destructors on that same lock. shutdown() makes the teardown deterministic instead.

Trimming. device_mem_trim is worth calling between pipeline phases with very different scratch shapes: cached blocks count against device residency, and on Windows (WDDM) sustained near-full commit makes the OS demote large resident allocations to shared memory — silently turning weight reads into PCIe traffic.

safetensors (<brotensor/safetensors.h>)

mmap'd zero-copy reader plus a writer. Namespace brotensor::safetensors.

GGUF (<brotensor/gguf.h>)

mmap'd reader for GGUF model files. Namespace brotensor::gguf.

Note that carrier support is broader than op support: the reader will load any of the above, but only Q4_K / Q6_K / Q8_0 are consumed by the fused dequant / matmul kernels (see op-coverage.md). Loading a Q5_K tensor succeeds; calling a matmul on it throws.