1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The element type a dequantisation kernel writes.
//!
//! Every kernel in this crate computes in `f32` and narrows on the way out.
//! Until v0.7.3 that narrowing was hard-coded to `BF16`, which was assumed
//! rather than decided: no design note ever defended it. [`OutputElement`]
//! turns it into a caller-chosen parameter, monomorphised exactly like a C++
//! template argument, with a single runtime `match` at the public boundary so
//! the CLI and the Phase 8 Python bindings can still choose from a string.
//!
//! # Why this matters numerically
//!
//! `BF16` keeps 8 significand bits. A `Q8_0` value is an `f16` scale (11-bit
//! significand) times an `int8`, so it needs up to ~18 bits; `Q6_K` needs 24.
//! Measured on `SmolLM2-135M-Q4_K_M`, only **3 to 20 %** of dequantised values
//! are exactly `BF16`-representable and the rest are rounded, at up to half a
//! `BF16` `ULP` (`2⁻⁸`, about 0.39 % relative).
//!
//! The usual defence, that quantisation error dwarfs the rounding, holds for
//! `Q4_K` and below but **not** for the high-precision types: `Q8_0`'s own
//! quantisation step is about 1/254, the *same order* as `BF16`'s half-`ULP`.
//! For those types the crate was adding rounding comparable to the error the
//! format exists to avoid.
//!
//! [`F32Out`] is the only width that adds **no narrowing step of its own**, so
//! its output *is* the `f32` the reference implementation produces.
//!
//! # What "exact" does and does not mean
//!
//! Two senses get conflated, and only the first is what this module delivers:
//!
//! 1. **Exact against the reference.** [`F32Out`] removes anamnesis's own
//! narrowing, so the emitted value is the `f32` that `gguf-py` and
//! `ggml-quants.c` produce. This is the claim the cross-validation tests.
//! 2. **Exact in the real-number sense.** True for the pure-product kernels
//! (`Q8_0`, `Q5_0`, and `Q6_K`, whose 11 + 7 + 6 = 24 significand bits land
//! exactly on `f32`). **Not** guaranteed for the min-offset K-quants
//! (`Q2_K`, `Q4_K`, `Q5_K`), whose final `d·q - dmin·m` subtracts two `f32`
//! values at different exponents and can itself round. There, `f32` is the
//! reference's own value, not a mathematically exact one.
//!
//! # Why the trait carries the loop, not a per-element hook
//!
//! [`OutputElement::write_scratch`] converts a whole block rather than one
//! element. Each implementation therefore owns a complete, monomorphic loop
//! with no generic indirection inside it, which is what lets each carry its own
//! `// VECTORIZED:` annotation and be verified independently in
//! `cargo-show-asm`. A per-element hook would put a slice-length check on every
//! element and leave one generic loop whose codegen would have to be inspected
//! three times anyway.
//!
//! **v0.7.4 tested that claim rather than inheriting it, and it held.** The
//! four `remember` families narrow inside their hot loops, so routing them
//! through `write_scratch` costs an `f32` intermediate the `GGUF` kernels never
//! pay. A `write_one(value: f32, out: &mut [u8])` hook was implemented in full
//! and benchmarked against the pre-v0.7.4 baseline binary, interleaved, on an
//! idle machine:
//!
//! | family | `write_one` | with `write_scratch` + register tiles |
//! |---|---:|---:|
//! | `AWQ` | 0.98× | 1.04× |
//! | `BnB` `NF4` | 1.00× | 1.04× |
//! | `BnB` `INT8` | 1.01× | 1.09× |
//! | `FP8` fine-grained | **1.46×** | 1.06× |
//! | `GPTQ` `INT4` | **4.25×** | 1.09× |
//!
//! Three kernels reached parity and two collapsed. `--emit=asm` on the
//! `Bf16Out` monomorphisations shows why: `GPTQ`'s pass 2 emitted scalar
//! `vsubss` / `vmulss` where it had emitted `vsubps` / `vmulps` on `%ymm`, and
//! `FP8` likewise fell to scalar `vmulss`. The `copy_from_slice` inside
//! `write_one` carries a length check that `LLVM` eliminates for some callers
//! and not others, because the chunk width comes from the generic `E::BYTES`
//! rather than the literal the pre-v0.7.4 loops used. **Only the implementation
//! can supply a literal chunk width**, which is exactly the property
//! `write_scratch` has and a per-element hook cannot.
//!
//! So the trade is a uniform, predictable 1.04–1.09× against a bimodal
//! 0.98×–4.25×, and the design note stands. See `docs/perf-experiments.md` for
//! the full numbers.
use crateDtype;
use cratef32_bits_to_bf16_bits;
/// Elements a fused-narrowing kernel hands to [`OutputElement::write_scratch`]
/// per call.
///
/// **Calibrated, not guessed, and the reason v0.7.4's split is nearly free.**
/// The four `remember` families (`FP8`, `GPTQ`, `AWQ`, `BnB`) narrowed inside
/// their hot loops before v0.7.4. Splitting that into an arithmetic pass plus a
/// narrowing pass introduces an `f32` intermediate, and where that intermediate
/// goes decides the whole cost:
///
/// - **Row-sized** (the first draft: `out_features × 4` = 44 KB at
/// `out_features = 11008`) the `f32`s reach memory between the passes.
/// Measured 1.115× against the pre-v0.7.4 fused kernel on `BnB` `INT8`.
/// - **Register-sized** (this constant) they stay in `ymm` registers, because
/// 32 `f32`s is four AVX2 vectors and both loops fully unroll. `FP8`
/// fine-grained went from 1.43× to **1.06×** across this change plus hoisting
/// its buffer out of the per-block call.
///
/// 32 rather than 8 is measured too: an 8-element tile left more loop-setup
/// overhead per element than the register pressure it saved.
///
/// Not feature-gated: the always-on `FP8` family uses it, so it is live in
/// every build. Contrast [`MAX_OUTPUT_BYTES`], which is a `GGUF`-only concept.
pub const VECTOR_TILE: usize = 32;
/// Widest output element in bytes, over every [`OutputElement`] implementation.
///
/// The block runners in `remember::gguf` size their stack output buffers at
/// `QK × MAX_OUTPUT_BYTES` and then sub-slice to `QK × E::BYTES`, because
/// `[0u8; QK * E::BYTES]` would need `generic_const_exprs`, which is unstable.
/// The `const` block below proves this bound holds for all three types, so the
/// sub-slice can never be short. [`OutputElement`] being sealed is what makes
/// those three the complete set.
///
/// Feature-gated with its consumer. Those two runners are the only code that
/// needs it, so without `gguf` it is genuinely dead — and on the MSRV
/// toolchain, provably so: rustc 1.88's dead-code analysis does **not** count
/// the reference from the `const` assertion block below as a use, while current
/// stable does. Gating it here rather than reaching for `#[allow(dead_code)]`
/// keeps the lint meaningful.
///
/// v0.7.3 expected v0.7.4 to widen this gate once `FP8` / `GPTQ` / `AWQ` /
/// `BnB` went generic. **It did not, and the gate is correct as it stands.**
/// Those four families tile through an **`f32` scratch** and hand it to
/// [`OutputElement::write_scratch`], so their scratch is sized in `f32`s and
/// never in output bytes; only the `GGUF` runners build a byte tile up front,
/// because their kernels write into a fixed `[f32; QK]` and the output buffer
/// has to be materialised beside it. The constant stays a `GGUF` concept.
pub const MAX_OUTPUT_BYTES: usize = 4;
/// Compile-time proof that [`MAX_OUTPUT_BYTES`] really does bound every
/// implementation. Adding a wider output type without raising the constant
/// fails the build here rather than silently truncating tensor data.
///
/// Gated alongside the constant: the bound only guards buffers that exist in a
/// `gguf` build.
const _: = ;
/// Private supertrait that makes [`OutputElement`] sealed.
/// The element type a dequantisation kernel writes.
///
/// Implemented by [`Bf16Out`], [`F32Out`] and [`F16Out`], and **sealed**: the
/// contract is a byte-level invariant that the crate's cross-validation depends
/// on (write exactly [`BYTES`](Self::BYTES) little-endian bytes per input
/// value, describing itself truthfully via [`DTYPE`](Self::DTYPE)), and an
/// outside implementation could violate it silently while every test stayed
/// green. Sealing is also the reversible direction: un-sealing later is not a
/// breaking change, sealing later would be.
///
/// The kernels never name this trait. They fill an `[f32; QK]` scratch buffer
/// and hand it to [`write_scratch`](Self::write_scratch), so all 24 `GGUF`
/// kernel functions are untouched by the choice of output type.
// The `sealed::Sealed` supertrait lives in a private module: that privacy *is*
// the seal. A `#[doc(hidden)] pub mod sealed` would still let an outside crate
// name and implement `Sealed`, which is the thing being prevented.
//
// No `#[allow(private_bounds)]` here: the lint does not fire on this shape, and
// carrying an allow for a lint that never triggers would tell the next reader
// there is a suppression to preserve when there is not. Verified by removing it
// and rebuilding clean.
/// `BF16` output: 2 bytes per value, round-to-nearest-even. **The default**,
/// and the only width the crate emitted before v0.7.3.
///
/// The dtype the safetensors and Hugging Face ecosystem serves weights in, and
/// at 2 bytes per element it halves memory traffic on a path that is bandwidth
/// bound end to end. See the module docs for what that costs in precision.
;
/// `F32` output: 4 bytes per value, and **no narrowing step at all**.
///
/// The kernels already compute in `f32`, so this writes the value they
/// computed. That makes the output bit-identical to the reference
/// implementation's own `f32`, which is what the phrase "removes a rounding
/// step" means precisely. It doubles output bytes on a bandwidth-bound path,
/// so expect it to be slower than [`Bf16Out`]; that is the honest cost of the
/// precision, not a defect.
;
/// `F16` output: 2 bytes per value, IEEE 754 binary16, round-to-nearest-even.
///
/// **`F16` is not uniformly the better 2-byte choice.** Against [`Bf16Out`] it
/// buys 3 significand bits (11 versus 8) and pays a far narrower exponent
/// range. `BF16` shares `f32`'s range; `F16` saturates at 65504 and flushes to
/// zero below about `2⁻²⁴`.
///
/// That range is reachable in real data, not just in theory: `MXFP4`'s `E8M0`
/// scale spans `2⁻¹²⁸` to `2¹²⁷`, and a `Q8_0` block whose `f16` scale is large
/// can exceed 65504 once multiplied by an `int8` of up to 127.
///
/// # Out-of-range policy
///
/// Plain IEEE semantics, via `half::f16::from_f32`: values above the maximum
/// become infinity, values below the smallest subnormal flush to zero, and
/// everything between rounds to nearest even. This is deliberately **not**
/// saturating. Saturation would keep outputs finite but would fabricate a
/// value no reference implementation produces, and would put the `F16`
/// cross-validation permanently at odds with `NumPy` and `PyTorch`, which both
/// produce infinity here.
;
// `float_cmp` is allowed deliberately, not as a blanket concession: every
// comparison below asserts an *exactly representable* value, which is the
// property under test. An epsilon comparison would defeat the point, because a
// rounding bug that shifted one `ULP` is exactly what these tests exist to
// catch.