GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 26 / 0 / 26
Functions: 100.0% 11 / 1 / 12
Branches: 100.0% 2 / 0 / 2

include/brotensor/tensor.h
Line Branch Exec Source
1 #pragma once
2
3 #include <cstddef>
4 #include <cstdint>
5 #include <vector>
6
7 namespace brotensor {
8
9 // ─── Dtype ─────────────────────────────────────────────────────────────────
10 //
11 // brotensor's tensor type carries a dtype tag so ops can pick the right
12 // kernel without a parallel tensor type per precision. Storage stays as a
13 // single raw `void*` (`data`); typed access is via the host_f32 / host_fp16
14 // accessors. GPU backends reinterpret the same allocation for FP16 / INT8.
15 //
16 // Element sizes are fixed: FP32 = 4 bytes, FP16 = 2 bytes, BF16 = 2 bytes,
17 // INT8 = 1 byte, INT32 = 4 bytes. Allocation, clone, zero, and resize all use
18 // dtype-aware byte counts. BF16 (IEEE 754 bfloat16 — the high 16 bits of an
19 // FP32) is an arithmetic dtype carried only by the GPU backends; like FP16 it
20 // is stored as a uint16_t bit pattern on the host. Arithmetic ops dispatch on
21 // FP32/FP16/BF16. INT8 is currently only carried by weight-only quantised ops
22 // (W8A16 matmul/conv2d). INT32 is likewise a pure storage carrier — used for
23 // device-resident index/offset buffers (e.g. per-head offset tables for
24 // softmax_xent_fused_batched); no arithmetic op dispatches on it.
25 enum class Dtype : int {
26 FP32 = 0,
27 FP16 = 1,
28 INT8 = 2,
29 INT32 = 3,
30 BF16 = 4,
31 F64 = 5,
32 // GGUF legacy quants — 32-element blocks, opaque storage carriers only.
33 Q4_0 = 10,
34 Q4_1 = 11,
35 Q5_0 = 12,
36 Q5_1 = 13,
37 Q8_0 = 14,
38 Q8_1 = 15,
39 // GGUF K-quants — 256-element superblocks, opaque storage carriers only.
40 Q2_K = 20,
41 Q3_K = 21,
42 Q4_K = 22,
43 Q5_K = 23,
44 Q6_K = 24,
45 Q8_K = 25,
46 };
47
48 // Bytes per scalar element. Returns 0 for quant dtypes (they aren't
49 // element-addressable — use dtype_storage_bytes() instead).
50 int dtype_size_bytes(Dtype);
51
52 // Elements per block. 1 for non-quant types; 32 for the legacy GGUF quants
53 // (Q4_0..Q8_1); 256 for the K-quants.
54 int dtype_block_size(Dtype);
55
56 // Bytes per block. Equals dtype_size_bytes(d) for non-quant dtypes; for quant
57 // dtypes it's the on-disk block size (e.g. Q4_K = 144).
58 int dtype_block_bytes(Dtype);
59
60 // Byte count for a tensor of `numel` elements stored as `d`. For non-quant
61 // types it's numel * dtype_size_bytes(d). For quant types `numel` must be a
62 // multiple of dtype_block_size(d) (throws std::runtime_error otherwise) and
63 // the result is (numel / block_size) * block_bytes.
64 std::size_t dtype_storage_bytes(Dtype d, std::int64_t numel);
65
66 // True iff `d` is a quant block carrier (Q*_*).
67 bool dtype_is_quant(Dtype);
68
69 // ─── Device ────────────────────────────────────────────────────────────────
70 //
71 // Runtime backend tag carried on every Tensor. CPU is always available;
72 // CUDA / Metal are registered at runtime by `brotensor::init()` if the
73 // corresponding backend was compiled into this binary. Multi-GPU within a
74 // single backend is deliberately out of scope for now (no Device::CUDA(idx)).
75 enum class Device { CPU, CUDA, Metal };
76
77 const char* device_name(Device);
78
79 // ─── Tensor ────────────────────────────────────────────────────────────────
80 //
81 // Unified tensor: a row-major (rows, cols) buffer tagged with both a Dtype
82 // and a Device. Storage is a single opaque `void*` allocated through the
83 // backend's alloc vtable (see detail/dispatch.h); the destructor frees via
84 // the same vtable. Rank is fixed at 2 (matrix) or 1 (vector — cols == 1).
85 //
86 // Copyable and movable: the copy ctor / copy assignment perform a
87 // device-aware deep copy (identical to clone()); move transfers ownership
88 // of the underlying buffer. Copying a GPU-resident tensor therefore
89 // allocates and copies on-device — pass by reference on hot paths and use
90 // clone() where the copy should be explicit. The CPU backend allocates
91 // plain host memory through the same vtable interface so the storage layout
92 // is uniform across devices; for CPU tensors the typed host accessors
93 // (host_f32, host_fp16, at, to_host_vector) give ergonomic access without a
94 // device sync.
95 struct Tensor {
96 79811 void* data = nullptr;
97 79811 int rows = 0; // rank-1 tensors: rows = N, cols = 1
98 79811 int cols = 0;
99 79811 Dtype dtype = Dtype::FP32;
100 79811 Device device = Device::CPU;
101
102 239433 Tensor() = default;
103 ~Tensor();
104
105 // Copyable + movable. The copy ctor / copy assignment perform a
106 // device-aware deep copy — identical to clone() — so a Tensor can be
107 // used with value semantics (caches, std::vector storage, by-value
108 // params). clone() remains for call sites that want the copy to be
109 // explicit. Copying a GPU-resident tensor allocates + copies on-device.
110 Tensor(const Tensor&);
111 Tensor& operator=(const Tensor&);
112 Tensor(Tensor&&) noexcept;
113 Tensor& operator=(Tensor&&) noexcept;
114
115 // ─── Factories ─────────────────────────────────────────────────────────
116 //
117 // zeros / empty allocate on the current default device (see runtime.h —
118 // controlled by set_default_device() / DeviceScope, or the
119 // BROTENSOR_DEFAULT_DEVICE env var). `zeros` memset-zeros the buffer
120 // via the backend's memset_zero hook; `empty` leaves contents undefined.
121 static Tensor zeros(int r, int c, Dtype dt = Dtype::FP32);
122 static Tensor empty(int r, int c, Dtype dt = Dtype::FP32);
123
124 // Explicit-device variants — bypass the thread-local default. Useful for
125 // tests, multi-device pipelines, and any code that wants to pin storage
126 // to a specific backend regardless of caller policy.
127 static Tensor zeros_on(Device, int r, int c, Dtype dt = Dtype::FP32);
128 static Tensor empty_on(Device, int r, int c, Dtype dt = Dtype::FP32);
129
130 // Host (CPU) FP32 factories. Always allocate zero-filled storage pinned
131 // to Device::CPU regardless of the current default device — a parameter-
132 // bearing layer builds its weights on the host, then migrates the whole
133 // layer with to(Device). `mat` is a (rows, cols) matrix; `vec` is a
134 // rank-1 (n, 1) column vector.
135 5786 static Tensor mat(int r, int c) { return zeros_on(Device::CPU, r, c); }
136 1073 static Tensor vec(int n) { return zeros_on(Device::CPU, n, 1); }
137
138 // Host bootstrap. Allocates on the current default device and uploads
139 // `r * c` floats (FP32) or uint16_t bit patterns (FP16) from `src`.
140 // For non-CPU defaults this performs a host→device copy via the
141 // backend's memcpy_h2d hook; for the CPU default it's a plain memcpy.
142 static Tensor from_host(const float* src, int r, int c);
143 static Tensor from_host_fp16(const uint16_t* src, int r, int c);
144 static Tensor from_host_bf16(const uint16_t* src, int r, int c);
145 // INT8 weights (W8A16): `r * c` int8_t values, e.g. the output of
146 // quantize_int8_per_row_host paired with FP32 per-row dequant scales.
147 static Tensor from_host_int8(const int8_t* src, int r, int c);
148
149 // Variant that pins to a specific device, bypassing the default.
150 static Tensor from_host_on(Device, const float* src, int r, int c);
151 static Tensor from_host_fp16_on(Device, const uint16_t* src, int r, int c);
152 static Tensor from_host_bf16_on(Device, const uint16_t* src, int r, int c);
153 static Tensor from_host_int8_on(Device, const int8_t* src, int r, int c);
154
155 // Non-owning view over an existing backend-resident pointer. The
156 // returned tensor's destructor will NOT free `data`. Caller is
157 // responsible for lifetime. Mirrors the legacy GpuTensor::view pattern.
158 static Tensor view(Device, void* data, int rows, int cols, Dtype = Dtype::FP32);
159
160 // Dtype-agnostic host bootstrap: allocates on `target` and copies
161 // `nbytes` raw bytes from `src` — a plain memcpy for Device::CPU, a
162 // single memcpy_h2d otherwise. Unlike from_host*_on, this works for any
163 // Dtype including the opaque GGUF block-quant carriers, since it copies
164 // bytes() rather than interpreting elements. `nbytes` must equal the
165 // resulting tensor's bytes() (i.e. dtype_storage_bytes(dt, r*c)).
166 static Tensor from_raw_bytes_on(Device target, const void* src,
167 int r, int c, Dtype dt,
168 std::size_t nbytes);
169
170 // ─── Migration ─────────────────────────────────────────────────────────
171
172 // Returns a fresh tensor on `target` with the same shape/dtype/contents
173 // as `*this`. No-op clone() if already on the target device. The source
174 // tensor is unchanged. Uses the backend pair's memcpy_h2d / memcpy_d2h /
175 // memcpy_d2d hooks as appropriate.
176 Tensor to(Device target) const;
177
178 // Device-preserving deep copy.
179 Tensor clone() const;
180
181 // ─── Mutators ──────────────────────────────────────────────────────────
182
183 // memset-zero the buffer over bytes(). Dispatches through the backend's
184 // memset_zero hook.
185 void zero();
186
187 // Reshapes to (r, c, dt); leaves contents undefined (call zero()
188 // afterwards if needed). Device is preserved. Storage is kept whenever
189 // the requested shape fits the existing allocation (capacity = the
190 // high-water mark of this tensor's past sizes), so a scratch buffer
191 // cycling through shapes stabilises at its largest size instead of
192 // reallocating every call — which also keeps its device pointer stable,
193 // a requirement for CUDA-graph-captured op sequences. Reallocates only
194 // when growing past capacity. A no-op if the shape and dtype already
195 // match. Throws std::runtime_error on a negative dimension, or if called
196 // on a non-owning view (a tensor from view()) — reshaping a view would
197 // silently sever it, so allocate a fresh tensor instead.
198 void resize(int r, int c, Dtype dt = Dtype::FP32);
199
200 // ─── Accessors ─────────────────────────────────────────────────────────
201
202 46298138 int size() const { return rows * cols; }
203 std::size_t bytes() const;
204 1 bool is_host() const { return device == Device::CPU; }
205
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4423 times.
4427 bool empty() const { return data == nullptr || size() == 0; }
206
207 // Host-side typed accessors. Throw std::runtime_error if device != CPU.
208 // `host_f32` additionally throws if dtype != FP32; `host_fp16` if
209 // dtype != FP16. `host_raw` is dtype-agnostic.
210 float* host_f32_mut();
211 const float* host_f32() const;
212 uint16_t* host_fp16_mut();
213 const uint16_t* host_fp16() const;
214 uint16_t* host_bf16_mut();
215 const uint16_t* host_bf16() const;
216 void* host_raw_mut();
217 const void* host_raw() const;
218
219 // Element access helpers (host-only, FP32-only — convenience for tests).
220 // Throw if device != CPU or dtype != FP32 or indices out of range.
221 float& at(int r, int c);
222 float at(int r, int c) const;
223
224 // Host (CPU) FP32 convenience accessors. Thin aliases over the typed
225 // host accessors above — they throw via the same checks if device != CPU
226 // or dtype != FP32. `ptr` is the raw row-major base pointer; operator()
227 // is bounds-checked (r, c) access; operator[] is flat element access.
228 42535008 float* ptr() { return host_f32_mut(); }
229 const float* ptr() const { return host_f32(); }
230 7985 float& operator()(int r, int c) { return at(r, c); }
231 1024 float operator()(int r, int c) const { return at(r, c); }
232 3007993 float& operator[](int i) { return host_f32_mut()[i]; }
233 8447822 float operator[](int i) const { return host_f32()[i]; }
234
235 // ─── Host roundtrip helpers ────────────────────────────────────────────
236 //
237 // `to_host_vector*` downloads (if on a GPU backend) and returns a
238 // std::vector containing the buffer's contents in the matching scalar
239 // type. The copy_to_host variants write into a caller-supplied buffer
240 // of at least size() elements.
241 std::vector<float> to_host_vector() const; // FP32 only
242 std::vector<uint16_t> to_host_vector_fp16() const; // FP16 only
243 std::vector<uint16_t> to_host_vector_bf16() const; // BF16 only
244 void copy_to_host(float* dst) const; // FP32 only
245 void copy_to_host_fp16(uint16_t* dst) const; // FP16 only
246 void copy_to_host_bf16(uint16_t* dst) const; // BF16 only
247
248 private:
249 79811 bool owns_ = false;
250 // Bytes actually allocated behind `data` when owns_ is true — resize()
251 // keeps the existing storage whenever the requested size fits, so the
252 // capacity is the high-water mark of past sizes. 0 for views, released,
253 // and default-constructed tensors.
254 79811 std::size_t cap_bytes_ = 0;
255 void release_();
256 };
257
258 // ─── FP16 / BF16 ↔ FP32 host-side conversion helpers ───────────────────────
259 //
260 // Pure-CPU conversion. `fp16` is IEEE 754 binary16; `bf16` is bfloat16 — the
261 // high 16 bits of an FP32 with round-to-nearest-even. Useful for tests and
262 // small preprocessing where a GPU roundtrip would be wasteful. Not intended
263 // for hot loops.
264 uint16_t fp32_to_fp16_bits(float v);
265 float fp16_bits_to_fp32(uint16_t bits);
266 uint16_t fp32_to_bf16_bits(float v);
267 float bf16_bits_to_fp32(uint16_t bits);
268
269 } // namespace brotensor
270