You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The docs had drifted from the code in three ways.
Metal coverage was described as it stood several commits ago. a04cf93 and
5070c51 filled the slots the tables still marked CPU+CUDA-only: threshold_u8,
rows_count_above, copy_d2d_strided, max_pool2d/adaptive_avg_pool2d,
gather_rows/scatter_rows/scatter_rows_add, top_k_rows, conv_transpose1d/2d.
Metal now leaves 6 of 260 slots null (three host-scalar loss/RNG ops,
xavier_init, and the CUDA-only fused filtered_lrelu pair); CUDA leaves one,
filtered_lrelu_backward, which is the composite everywhere. State the counts
rather than a vague "a few inference-only ops". 82ffc5c also gave relu/tanh/
sigmoid and masked_mean_pool FP16/BF16 paths the FP16 column still read as "—".
Nine ops in the X-macro appeared nowhere in op-coverage.md:
flash_attention_gqa (bef5ca7), matmul_abt, softmax_rows, patch_unpack,
pixel_shuffle_upsample_2x, sample_logits_into, and the axpby / add_scalar /
add_channel_bias in-place family. Also fix downsample_avg_2x, attributed to
resize.h when it is declared in pooling.h, and the linear_batched_int8w_fp16
row label, whose real symbol is linear_forward_batched_int8w_fp16.
api.md claimed five public headers and documented six; cuda_graph.h was
absent entirely, which matters because resize()'s pointer-stability guarantee
exists to serve graph capture. Two outright errors: safetensors upload() is
dtype-preserving, not "as FP32", and the GGUF reader carries 16 types, not the
3 listed — carrier support is broader than fused-op support, so separate the
two claims. Add the runtime functions the table never picked up (shutdown,
device_mem_info/trim, device_product_name), the dtype_* helper family,
from_raw_bytes_on, zero(), and upload_as.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- **Training building blocks** — flash attention with backward, LSTM with full BPTT, LoRA adapters, StyleGAN3 generator primitives (modulated conv, upfirdn2d, filtered lrelu), SGD/Adam
25
25
- **Precision & quantization** — the CPU backend is the complete FP32 reference; the GPU backends add FP16/BF16 paths, INT8 weight-only matmul/conv (W8A16), and GGUF block-quant kernels (Q4_K / Q6_K / Q8_0)
26
26
- **Model loading** — mmap'd zero-copy readers for **safetensors** (also writes) and **GGUF**
27
+
- **CUDA graph capture** (`<brotensor/cuda_graph.h>`) — capture a fixed-shape step once and replay it with a single launch, amortising per-kernel launch overhead in tight decode loops. `Tensor::resize` keeps device pointers stable across shape cycles so captured buffers stay valid
27
28
28
29
See [docs/op-coverage.md](docs/op-coverage.md) for the full per-op coverage tables.
|`<brotensor/ops.h>`| The device-neutral op surface — an umbrella over the per-category headers in `<brotensor/ops/>` (see [op-coverage.md](op-coverage.md)) |
| `Tensor::zeros_on(dev, r, c, dt)` / `Tensor::empty_on(dev, r, c, dt)` | Same, pinned to an explicit device. |
34
39
| `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`. |
35
40
| `Tensor::from_host_on(dev, ptr, r, c)` (+ `_fp16_on` / `_bf16_on` / `_int8_on`) | Same, pinned to an explicit device. |
41
+
| `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)`. |
36
42
| `Tensor::mat(r, c)` / `Tensor::vec(n)` | Zero-filled FP32 **host (CPU)** tensors — build parameters on the host, then migrate with `to()`. |
37
43
| `Tensor::view(dev, ptr, r, c, dt)` | Non-owning view over an existing backend-resident pointer. `resize()` on a view throws. |
| `t.to_host_vector()` (+ `_fp16` / `_bf16`) | Read back to a `std::vector` (`float` / `uint16_t` bits). |
46
52
| `t.copy_to_host(dst)` (+ `_fp16` / `_bf16`) | Read back into a caller-owned buffer. |
47
-
| `t.resize(r, c, dt)` | Reallocate in place; contents **undefined** afterwards. Throws on a non-owning view. |
53
+
| `t.zero()` | memset the buffer to zero over `bytes()`. |
54
+
| `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. |
55
+
56
+
`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.
48
57
49
58
Call `sync(device)` / `sync_all()` before reading GPU results back to the host — GPU ops are asynchronous.
50
59
51
60
CPU-resident tensors additionally expose direct host accessors (`host_f32_mut()`, `at()`, `operator[]`, …) — see `tensor.h`.
52
61
62
+
### Dtype helpers
63
+
64
+
Free functions for sizing a buffer without special-casing the quant carriers:
65
+
66
+
| Helper | Meaning |
67
+
|---|---|
68
+
| `dtype_size_bytes(dt)` | Bytes per element. **Returns 0 for the block-quant dtypes** — they aren't element-addressable. |
69
+
| `dtype_block_size(dt)` | Elements per block (32 for the legacy quants, 256 for the K-quants, 1 otherwise). |
70
+
| `dtype_block_bytes(dt)` | Encoded bytes per block. |
71
+
| `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. |
72
+
| `dtype_is_quant(dt)` | Whether `dt` is a GGUF block-quant carrier. |
73
+
| `device_name(dev)` | The backend kind as a string (`"cpu"` / `"cuda"` / `"metal"`). |
74
+
53
75
### Bit-conversion helpers
54
76
55
77
`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.
| `init()` | Idempotent. Probes and registers the CUDA / Metal backends. CPU is always registered (static-init), so CPU-only code works without calling it. |
84
+
| `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()`**. |
62
85
| `default_device()` | Where no-suffix factories allocate. Best available: CUDA > Metal > CPU. |
63
86
| `set_default_device(dev)` | Global override. Also overridable per-process via the `BROTENSOR_DEFAULT_DEVICE` env var (`cpu` / `cuda` / `metal`). |
| `compute_dtype()` | The dtype a model loader should upload weights at for the current default device: FP32 on CPU, FP16 on a GPU. |
66
89
| `available_devices()` / `is_available(dev)` | Backends registered in this binary at runtime. |
67
90
| `sync(dev)` / `sync_all()` | Drain pending backend work (no-op on CPU). |
91
+
| `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`. |
92
+
| `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. |
93
+
| `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. |
94
+
95
+
**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.
96
+
97
+
**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.
68
98
69
99
## safetensors (`<brotensor/safetensors.h>`)
70
100
71
101
mmap'd zero-copy reader plus a writer. Namespace `brotensor::safetensors`.
72
102
73
103
- `File` — opens and mmaps a `.safetensors` file, parses the JSON header, exposes tensors by name as `TensorView`s (name, dtype, shape, raw byte span).
74
-
- Upload helpers (view → device `Tensor`):
75
-
- `upload(view, rows, cols, dst)` — as FP32;
76
-
- `upload_fp16(view, rows, cols, dst)` — as FP16;
77
-
- `upload_compute(view, rows, cols, dst)` — at `compute_dtype()` for the current default device;
78
-
- `upload_compute_checked(...)` — same, with shape validation.
79
-
- `write_file(path, entries)` — write a `.safetensors` file from host data.
104
+
- Upload helpers (view → device `Tensor`). All require an F32 / F16 / BF16 source view; brotensor is 2D-only, so the caller flattens higher-rank weights to the `(rows, cols)` layout the consuming op expects:
105
+
- `upload(view, rows, cols, dst)` — **dtype-preserving**: `dst` gets the brotensor dtype matching the view, zero conversion (a BF16 view yields a BF16 tensor, an F16 view an FP16 tensor);
106
+
- `upload_fp16(view, rows, cols, dst)` — always FP16, converting host-side from F32 if needed;
107
+
- `upload_as(view, rows, cols, want, dst)` — at an **explicit** arithmetic dtype, converting host-side. Lets one module pick a compute dtype different from the global one — e.g. Flux runs BF16 on a pipeline whose dtype is FP16, because its activations overflow FP16;
108
+
- `upload_compute(view, rows, cols, dst)` — at `compute_dtype()` for the current default device, so one checkpoint serves either backend (BF16 widens to FP32 on CPU, narrows to FP16 on a GPU);
109
+
- `upload_compute_checked(view, rows, cols, dst, name)` — same, but first validates the view's dtype and element count, throwing tagged with the caller-supplied `name` and the safetensors key.
110
+
- `write_file(path, entries)` — write a `.safetensors` file from host data. Each `WriteEntry` carries name / dtype / shape / host pointer / byte count; dtype defaults to `F16`.
- `shape_to_2d(shape)` — collapse a GGUF n-d shape to brotensor's `(rows, cols)`.
89
-
- `upload_raw(info, rows, cols, dst)` — upload a tensor's raw bytes to the device at its carrier dtype.
90
-
- Supported carriers: F32, F16, and the block-quant types Q4_K / Q6_K / Q8_0 — consumed directly by the fused dequant/matmul ops (see [op-coverage.md](op-coverage.md)) without dequantizing on the host.
117
+
- `File` — opens and mmaps a `.gguf` file, parses header + metadata, exposes tensors as `TensorInfo` (name, GGUF type, mapped brotensor `dtype`, `dtype_supported`, shape, `numel`, raw data span). Also `find_tensor()` / `get_tensor()` / `tensors()`, `version()`, `alignment()`, `tensor_count()`.
- `shape_to_2d(shape)` — collapse a GGUF n-d shape to brotensor's `(rows, cols)`. GGUF shapes are innermost-first, so `cols = shape[0]` and `rows = product(shape[1..])`; a 1-D shape gives `(shape[0], 1)`. Throws on an empty shape.
120
+
- `upload_raw(info, rows, cols, dst)` — upload a tensor's raw bytes to the device at its carrier dtype, no host-side dequantization. Throws if `info.dtype_supported` is false; for a quant carrier, `cols` must be a multiple of `dtype_block_size(dtype)`.
121
+
- **Carriers the reader maps:** F32, F16, BF16, the legacy blocks Q4_0 / Q4_1 / Q5_0 / Q5_1 / Q8_0 / Q8_1, and the K-quant superblocks Q2_K / Q3_K / Q4_K / Q5_K / Q6_K / Q8_K.
122
+
123
+
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](op-coverage.md)). Loading a Q5_K tensor succeeds; calling a matmul on it throws.
Copy file name to clipboardExpand all lines: docs/architecture.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -51,9 +51,9 @@ The vtable slot signature *is* the public signature: same argument order, same s
51
51
52
52
-**CPU** — scalar FP32, always compiled. It implements essentially the entire FP32 surface, forward *and* backward — including the diffusion samplers, flash attention, the audio family, and the vision primitives. It is the simple, correct, autovectorize-friendly reference that the parity tests measure the GPU backends against. By design it leaves the FP16 / BF16 / INT8-W8A16 / GGUF-quant slots null.
53
53
-**CUDA** (`BROTENSOR_WITH_CUDA=ON`) — mirrors the FP32 surface and adds the FP16/BF16 precision paths, batched-inference variants, W8A16 WMMA kernels, GGUF block-quant kernels, and fused inference kernels.
54
-
-**Metal** (`BROTENSOR_WITH_METAL=ON`) — same role as CUDA on Apple GPUs. A few inference-only ops are CPU+CUDA with the Metal slot left null (noted in the [coverage tables](op-coverage.md)).
54
+
-**Metal** (`BROTENSOR_WITH_METAL=ON`) — same role as CUDA on Apple GPUs, and at near-total parity with it: of the 260 slots Metal leaves six null (three host-scalar loss/RNG ops, `xavier_init`, and the CUDA-only fused `filtered_lrelu` pair). See the [coverage tables](op-coverage.md#backend-coverage).
55
55
56
-
A handful of "ops" are not vtable entries at all but device-agnostic compositions of public ops — LoRA (`ops/lora.h`, header-only) and the `filtered_lrelu` composite fallback — so they run on any backend automatically.
56
+
A handful of "ops" are not vtable entries at all but device-agnostic compositions of public ops — LoRA (`ops/lora.h`, header-only) and the `filtered_lrelu` composite — so they run on any backend automatically.
0 commit comments