Skip to main content

cortiq_core/
quant.rs

1//! Canonical quantization layouts and scalar dequantization.
2//!
3//! Layouts are byte-identical to `.vmfc` ("quants first, then scales"):
4//! - `q8_row`  (2-D `[out, in]`): `[int8: out·in][f16: out]`,
5//!   `w = q[o,i] · scale[o]`, `scale[o] = absmax(row_o) / 127`.
6//! - `q4_block`: groups of 32 over the flattened tensor (zero-padded),
7//!   `[u8: ceil(n/32)·16][f16: ceil(n/32)]`, nibbles low-first,
8//!   `w = (q − 8) · scale`, `scale = absmax(group) / 7`.
9//!
10//! 1-D tensors (norms) are always stored `f16`.
11
12use crate::format::TensorEntry;
13use crate::types::TensorDtype;
14
15pub const GROUP_SIZE: usize = 32;
16
17/// IEEE half → f32.
18#[inline]
19pub fn f16_to_f32(h: u16) -> f32 {
20    let sign = (h >> 15) as u32;
21    let exp = ((h >> 10) & 0x1F) as u32;
22    let mant = (h & 0x3FF) as u32;
23    let bits = if exp == 0 {
24        if mant == 0 {
25            sign << 31
26        } else {
27            // subnormal: normalize. A subnormal half equals mant·2^-24; shifting
28            // its MSB up to bit 10 takes e = 10-b shifts (b = MSB position), so
29            // the true exponent is b-24 and the f32 biased exponent is b+103 =
30            // 113-e. (The old `127-15-e` form was off by one — it halved every
31            // subnormal, which corrupts K-quant super-block scales.)
32            let mut e = 0u32;
33            let mut m = mant;
34            while m & 0x400 == 0 {
35                m <<= 1;
36                e += 1;
37            }
38            m &= 0x3FF;
39            (sign << 31) | ((113 - e) << 23) | (m << 13)
40        }
41    } else if exp == 0x1F {
42        (sign << 31) | (0xFF << 23) | (mant << 13)
43    } else {
44        (sign << 31) | ((exp + 127 - 15) << 23) | (mant << 13)
45    };
46    f32::from_bits(bits)
47}
48
49/// f32 → IEEE half (round-to-nearest-even). Used by the Rust writer.
50#[inline]
51pub fn f32_to_f16(x: f32) -> u16 {
52    let bits = x.to_bits();
53    let sign = ((bits >> 16) & 0x8000) as u16;
54    let mut exp = ((bits >> 23) & 0xFF) as i32;
55    let mant = bits & 0x7F_FFFF;
56
57    if exp == 0xFF {
58        // Inf / NaN
59        return sign | 0x7C00 | if mant != 0 { 0x200 } else { 0 };
60    }
61    exp -= 127 - 15;
62    if exp >= 0x1F {
63        return sign | 0x7C00; // overflow → Inf
64    }
65    if exp <= 0 {
66        if exp < -10 {
67            return sign; // underflow → 0
68        }
69        // subnormal
70        let m = mant | 0x80_0000;
71        let shift = (14 - exp) as u32;
72        let half = m >> shift;
73        let round = (m >> (shift - 1)) & 1;
74        return sign | ((half + round) as u16);
75    }
76    let half = ((exp as u32) << 10) | (mant >> 13);
77    let round = (mant >> 12) & 1;
78    // round-to-nearest-even: bump if round bit set and (sticky or odd)
79    let sticky = (mant & 0xFFF) != 0;
80    let bump = round & (sticky as u32 | (half & 1));
81    sign | ((half + bump) as u16)
82}
83
84/// bfloat16 → f32.
85#[inline]
86pub fn bf16_to_f32(h: u16) -> f32 {
87    f32::from_bits((h as u32) << 16)
88}
89
90/// Dequantize a full `q8_row` tensor: `[int8: out·in][f16: out]`.
91pub fn dequant_q8_row(bytes: &[u8], out_dim: usize, in_dim: usize, dst: &mut [f32]) {
92    debug_assert_eq!(bytes.len(), out_dim * in_dim + out_dim * 2);
93    debug_assert_eq!(dst.len(), out_dim * in_dim);
94    let (q, scales) = bytes.split_at(out_dim * in_dim);
95    for o in 0..out_dim {
96        let s = f16_to_f32(u16::from_le_bytes([scales[o * 2], scales[o * 2 + 1]]));
97        let row = &q[o * in_dim..(o + 1) * in_dim];
98        let out = &mut dst[o * in_dim..(o + 1) * in_dim];
99        for (d, &b) in out.iter_mut().zip(row) {
100            *d = (b as i8) as f32 * s;
101        }
102    }
103}
104
105/// Dequantize a full `q8_2f` tensor (two-field 𝒲×θ, dtype 9):
106/// `[int8: out·in][f16 row_scale: out][f16 col: in]`,
107/// `w[o,i] = q[o,i] · row_scale[o] · col[i]`. The column field absorbs
108/// outlier input channels — validated in vmfcore (+37% at equal size
109/// for the two-field family; q8_2f recovers ~75% of the q8→f16 gap).
110pub fn dequant_q8_2f(bytes: &[u8], out_dim: usize, in_dim: usize, dst: &mut [f32]) {
111    debug_assert_eq!(bytes.len(), out_dim * in_dim + out_dim * 2 + in_dim * 2);
112    debug_assert_eq!(dst.len(), out_dim * in_dim);
113    let (q, rest) = bytes.split_at(out_dim * in_dim);
114    let (scales, cols) = rest.split_at(out_dim * 2);
115    let col: Vec<f32> = (0..in_dim)
116        .map(|i| f16_to_f32(u16::from_le_bytes([cols[i * 2], cols[i * 2 + 1]])))
117        .collect();
118    for o in 0..out_dim {
119        let s = f16_to_f32(u16::from_le_bytes([scales[o * 2], scales[o * 2 + 1]]));
120        let row = &q[o * in_dim..(o + 1) * in_dim];
121        let out = &mut dst[o * in_dim..(o + 1) * in_dim];
122        for i in 0..in_dim {
123            out[i] = (row[i] as i8) as f32 * s * col[i];
124        }
125    }
126}
127
128/// Dequantize a full `q4_block` tensor into `dst` (`dst.len()` = real
129/// element count; the trailing pad group elements are discarded).
130pub fn dequant_q4_block(bytes: &[u8], dst: &mut [f32]) {
131    let n_groups = dst.len().div_ceil(GROUP_SIZE);
132    let packed_len = n_groups * GROUP_SIZE / 2;
133    debug_assert_eq!(bytes.len(), packed_len + n_groups * 2);
134    let (packed, scales) = bytes.split_at(packed_len);
135    for g in 0..n_groups {
136        let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
137        let base = g * GROUP_SIZE;
138        let pk = &packed[g * 16..(g + 1) * 16];
139        for (k, &byte) in pk.iter().enumerate() {
140            let i0 = base + k * 2;
141            let i1 = i0 + 1;
142            if i0 < dst.len() {
143                dst[i0] = ((byte & 0x0F) as f32 - 8.0) * s;
144            }
145            if i1 < dst.len() {
146                dst[i1] = (((byte >> 4) & 0x0F) as f32 - 8.0) * s;
147            }
148        }
149    }
150}
151
152/// Dequantize a full `vbit` tensor (P13 FIG.3, grouped variant):
153/// [u8 bits: rows][f16 scales: rows·cols/32][bit-packed rows MSB-first,
154/// each row padded to a whole byte]. w = (u − L)·scale, L = 2^{b−1}−1.
155pub fn dequant_vbit(bytes: &[u8], rows: usize, cols: usize, dst: &mut [f32]) -> Result<(), String> {
156    if cols % GROUP_SIZE != 0 {
157        return Err(format!("vbit: cols {cols} not a multiple of {GROUP_SIZE}"));
158    }
159    let ng = cols / GROUP_SIZE;
160    let bits = &bytes[..rows];
161    if let Some(&b) = bits.iter().find(|&&b| !(3..=8).contains(&b)) {
162        return Err(format!("vbit: bit-width {b} outside safe range 3..=8"));
163    }
164    let sc_off = rows;
165    let data_off = sc_off + rows * ng * 2;
166    let mut off = data_off;
167    for r in 0..rows {
168        let b = bits[r] as usize;
169        let l = ((1usize << (b - 1)) - 1) as f32;
170        let rowlen = (cols * b).div_ceil(8);
171        let data = &bytes[off..off + rowlen];
172        let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
173        for i in 0..cols {
174            while nbits < b {
175                acc = (acc << 8) | data[idx] as u64;
176                idx += 1;
177                nbits += 8;
178            }
179            let u = ((acc >> (nbits - b)) & ((1u64 << b) - 1)) as f32;
180            nbits -= b;
181            let so = (r * ng + i / GROUP_SIZE) * 2;
182            let s = f16_to_f32(u16::from_le_bytes([
183                bytes[sc_off + so],
184                bytes[sc_off + so + 1],
185            ]));
186            dst[r * cols + i] = (u - l) * s;
187        }
188        off += rowlen;
189    }
190    Ok(())
191}
192
193/// Bytes per q4_tiled group tile: 2 (f16 scale) + 16 (nibbles).
194pub const Q4_TILE: usize = 18;
195
196/// Bytes per q1 group tile: 2 (f16 scale) + 4 (32 sign bits).
197pub const Q1_TILE: usize = 6;
198
199/// Dequantize a full `q1` tensor: per 32-group `[f16 scale][4B bits]`,
200/// bit k of byte j (LSB-first) is weight j·8+k of the group;
201/// value = scale · (2·bit − 1) ∈ {−s, +s}. 1-bit-TRAINED models only —
202/// see the dtype doc.
203pub fn dequant_q1(bytes: &[u8], dst: &mut [f32]) {
204    let n_groups = dst.len().div_ceil(GROUP_SIZE);
205    debug_assert_eq!(bytes.len(), n_groups * Q1_TILE);
206    for g in 0..n_groups {
207        let tile = &bytes[g * Q1_TILE..(g + 1) * Q1_TILE];
208        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
209        let base = g * GROUP_SIZE;
210        for (j, &byte) in tile[2..].iter().enumerate() {
211            for k in 0..8 {
212                let i = base + j * 8 + k;
213                if i < dst.len() {
214                    dst[i] = if (byte >> k) & 1 == 1 { s } else { -s };
215                }
216            }
217        }
218    }
219}
220
221/// Dequantize a full `q1s` tensor (1-bit + sparse outlier overlay): the
222/// leading `n_groups·6` bytes are a plain `q1` base, then `[u32 count]`
223/// and `count × [u32 flat-index][f16 value]` restore the salient weights
224/// the two-field mask kept at full precision (they overwrite the ±s base).
225pub fn dequant_q1s(bytes: &[u8], dst: &mut [f32]) {
226    let n_groups = dst.len().div_ceil(GROUP_SIZE);
227    let base_len = n_groups * Q1_TILE;
228    dequant_q1(&bytes[..base_len.min(bytes.len())], dst);
229    let mut off = base_len;
230    if off + 4 > bytes.len() {
231        return;
232    }
233    let count =
234        u32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]]) as usize;
235    off += 4;
236    for _ in 0..count {
237        if off + 6 > bytes.len() {
238            break;
239        }
240        let idx = u32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
241            as usize;
242        let val = f16_to_f32(u16::from_le_bytes([bytes[off + 4], bytes[off + 5]]));
243        if idx < dst.len() {
244            dst[idx] = val;
245        }
246        off += 6;
247    }
248}
249
250/// Ternary tile: 2 (f16 scale) + 7 (32 base-3 codes, 5 ternary values per
251/// byte, `3^5 = 243 ≤ 256`) = 9 bytes ⇒ ~2.25 bpw base (vs the old 2-bit
252/// 10-byte tile). The packed codes carry the same `{0,+s,−s}` values, so
253/// the reconstruction is bit-identical — this is pure size, no quality change.
254pub const Q1T_TILE: usize = 9;
255const Q1T_POW3: [u16; 5] = [1, 3, 9, 27, 81];
256
257/// Base-3 code (0/1/2) of ternary weight `k` from a group's packed 7 bytes.
258#[inline]
259pub fn q1t_code(codes: &[u8], k: usize) -> u8 {
260    ((codes[k / 5] as u16 / Q1T_POW3[k % 5]) % 3) as u8
261}
262
263/// Pack one base-3 code into the group's 7-byte code block (accumulative;
264/// start from a zeroed block, call for k = 0..32 in order).
265#[inline]
266pub fn q1t_pack(codes: &mut [u8; 7], k: usize, code: u8) {
267    codes[k / 5] += code * Q1T_POW3[k % 5] as u8;
268}
269
270/// Dequantize a full `q1t` tensor (ternary + sparse outlier overlay): per
271/// 32-group `[f16 scale][7B base-3 codes]` (0 → 0, 1 → +s, 2 → −s), then
272/// `[u32 count]` and `count × [u32 index][f16 value]`.
273/// `rows`×`cols` shape is needed for the per-row overlay (`[u32 row_ptr[rows+1]]`
274/// then `[(u16 col, f16 val)]` grouped by row — 4 B/outlier, no flat index).
275pub fn dequant_q1t(bytes: &[u8], rows: usize, cols: usize, dst: &mut [f32]) {
276    let n_groups = dst.len().div_ceil(GROUP_SIZE);
277    let base_len = n_groups * Q1T_TILE;
278    for g in 0..n_groups {
279        let off = g * Q1T_TILE;
280        if off + Q1T_TILE > bytes.len() {
281            break;
282        }
283        let s = f16_to_f32(u16::from_le_bytes([bytes[off], bytes[off + 1]]));
284        let codes = &bytes[off + 2..off + Q1T_TILE];
285        let base = g * GROUP_SIZE;
286        for k in 0..GROUP_SIZE {
287            let i = base + k;
288            if i < dst.len() {
289                dst[i] = match q1t_code(codes, k) {
290                    1 => s,
291                    2 => -s,
292                    _ => 0.0,
293                };
294            }
295        }
296    }
297    // Overlay: [u32 row_ptr[rows+1]] then entries grouped by row.
298    let entries = base_len + (rows + 1) * 4;
299    if entries > bytes.len() {
300        return;
301    }
302    let rp = |r: usize| -> usize {
303        let o = base_len + r * 4;
304        u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize
305    };
306    for r in 0..rows {
307        for p in rp(r)..rp(r + 1) {
308            let e = entries + p * 4;
309            if e + 4 > bytes.len() {
310                return;
311            }
312            let col = u16::from_le_bytes([bytes[e], bytes[e + 1]]) as usize;
313            let val = f16_to_f32(u16::from_le_bytes([bytes[e + 2], bytes[e + 3]]));
314            let i = r * cols + col;
315            if i < dst.len() {
316                dst[i] = val;
317            }
318        }
319    }
320}
321
322/// Dequantize a full `q4_tiled` tensor: per 32-group
323/// `[f16 scale][16B nibbles]`, nibbles low-first inside each byte —
324/// the same values/order as `q4_block`, only the placement of the
325/// scale differs.
326pub fn dequant_q4_tiled(bytes: &[u8], dst: &mut [f32]) {
327    let n_groups = dst.len().div_ceil(GROUP_SIZE);
328    debug_assert_eq!(bytes.len(), n_groups * Q4_TILE);
329    for g in 0..n_groups {
330        let tile = &bytes[g * Q4_TILE..(g + 1) * Q4_TILE];
331        let s = f16_to_f32(u16::from_le_bytes([tile[0], tile[1]]));
332        let base = g * GROUP_SIZE;
333        for (k, &byte) in tile[2..].iter().enumerate() {
334            let i0 = base + k * 2;
335            let i1 = i0 + 1;
336            if i0 < dst.len() {
337                dst[i0] = ((byte & 0x0F) as f32 - 8.0) * s;
338            }
339            if i1 < dst.len() {
340                dst[i1] = (((byte >> 4) & 0x0F) as f32 - 8.0) * s;
341            }
342        }
343    }
344}
345
346/// Byte layout of a `vbit_ro` payload (roadmap §4.2):
347/// `[u8 bits: rows][f16 scales: rows·cols/32][u32 row_offsets: rows+1]
348///  [bit-packed rows]` — offsets are relative to the packed area, so
349/// `offsets[r]..offsets[r+1]` is row r without any prefix scan.
350/// Returns (scales_off, offsets_off, packed_off).
351pub fn vbit_ro_sections(rows: usize, cols: usize) -> (usize, usize, usize) {
352    let ng = cols / GROUP_SIZE;
353    let scales_off = rows;
354    let offsets_off = scales_off + rows * ng * 2;
355    let packed_off = offsets_off + (rows + 1) * 4;
356    (scales_off, offsets_off, packed_off)
357}
358
359/// Read one u32 row offset from a `vbit_ro` offsets table.
360#[inline]
361pub fn vbit_ro_offset(bytes: &[u8], offsets_off: usize, r: usize) -> usize {
362    let o = offsets_off + r * 4;
363    u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize
364}
365
366/// Dequantize a full `vbit_ro` tensor — same math as `dequant_vbit`,
367/// rows addressed through the stored offset table.
368pub fn dequant_vbit_ro(
369    bytes: &[u8],
370    rows: usize,
371    cols: usize,
372    dst: &mut [f32],
373) -> Result<(), String> {
374    if cols % GROUP_SIZE != 0 {
375        return Err(format!(
376            "vbit_ro: cols {cols} not a multiple of {GROUP_SIZE}"
377        ));
378    }
379    let ng = cols / GROUP_SIZE;
380    let (sc_off, off_off, packed_off) = vbit_ro_sections(rows, cols);
381    let bits = &bytes[..rows];
382    for r in 0..rows {
383        let b = bits[r] as usize;
384        if !matches!(b, 3..=6 | 8) {
385            return Err(format!(
386                "vbit_ro row {r}: bit width {b} outside {{3,4,5,6,8}}"
387            ));
388        }
389        let l = ((1usize << (b - 1)) - 1) as f32;
390        let start = packed_off + vbit_ro_offset(bytes, off_off, r);
391        let end = packed_off + vbit_ro_offset(bytes, off_off, r + 1);
392        let data = &bytes[start..end];
393        let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
394        for i in 0..cols {
395            while nbits < b {
396                acc = (acc << 8) | data[idx] as u64;
397                idx += 1;
398                nbits += 8;
399            }
400            let u = ((acc >> (nbits - b)) & ((1u64 << b) - 1)) as f32;
401            nbits -= b;
402            let so = (r * ng + i / GROUP_SIZE) * 2;
403            let sc = f16_to_f32(u16::from_le_bytes([
404                bytes[sc_off + so],
405                bytes[sc_off + so + 1],
406            ]));
407            dst[r * cols + i] = (u - l) * sc;
408        }
409    }
410    Ok(())
411}
412
413/// Expected byte length of a tensor given dtype and element count.
414pub fn expected_nbytes(dtype: TensorDtype, shape: &[usize]) -> Option<usize> {
415    let n = shape
416        .iter()
417        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))?;
418    match dtype {
419        TensorDtype::F32 => n.checked_mul(4),
420        TensorDtype::F16 | TensorDtype::Bf16 => n.checked_mul(2),
421        TensorDtype::Q8Row => {
422            let out = *shape.first()?;
423            n.checked_add(out.checked_mul(2)?)
424        }
425        TensorDtype::Q4Block => {
426            let groups = n.div_ceil(GROUP_SIZE);
427            groups.checked_mul(18)
428        }
429        TensorDtype::Q4Tiled => {
430            // Interleaved tiles: [f16 scale][16B nibbles] per 32-group.
431            n.div_ceil(GROUP_SIZE).checked_mul(18)
432        }
433        TensorDtype::Q1 => {
434            // Interleaved tiles: [f16 scale][4B bits] per 32-group.
435            n.div_ceil(GROUP_SIZE).checked_mul(Q1_TILE)
436        }
437        TensorDtype::Q8_2f => {
438            let out = *shape.first()?;
439            let inn = n / out.max(1);
440            n.checked_add(out.checked_mul(2)?)?
441                .checked_add(inn.checked_mul(2)?)
442        }
443        _ => None, // variable/reserved dtypes: size not defined by this reader
444    }
445}
446
447fn has_fixed_payload_size(dtype: TensorDtype) -> bool {
448    matches!(
449        dtype,
450        TensorDtype::F32
451            | TensorDtype::F16
452            | TensorDtype::Bf16
453            | TensorDtype::Q8Row
454            | TensorDtype::Q4Block
455            | TensorDtype::Q4Tiled
456            | TensorDtype::Q1
457            | TensorDtype::Q8_2f
458    )
459}
460
461/// Validate a tensor payload against its directory entry (roadmap
462/// §4.9): every length is checked BEFORE any slice is taken, so a
463/// corrupted or truncated file fails loudly at `open()` instead of
464/// panicking in a kernel. For fixed-size dtypes this is the
465/// `expected_nbytes` equality; for vbit — whose payload length depends
466/// on the per-row bit widths stored in the payload itself — the exact
467/// length is computed from the (validated) width header.
468pub fn validate_payload(dtype: TensorDtype, shape: &[usize], bytes: &[u8]) -> Result<(), String> {
469    if dtype == TensorDtype::VbitRo {
470        if shape.len() != 2 {
471            return Err(format!("vbit_ro tensor must be 2-D, got {shape:?}"));
472        }
473        let (rows, cols) = (shape[0], shape[1]);
474        if cols == 0 || cols % GROUP_SIZE != 0 {
475            return Err(format!(
476                "vbit_ro cols {cols} not a positive multiple of {GROUP_SIZE}"
477            ));
478        }
479        let (_, off_off, packed_off) = vbit_ro_sections(rows, cols);
480        if bytes.len() < packed_off {
481            return Err(format!(
482                "vbit_ro payload {} bytes cannot hold headers ({packed_off})",
483                bytes.len()
484            ));
485        }
486        if vbit_ro_offset(bytes, off_off, 0) != 0 {
487            return Err("vbit_ro offsets[0] must be 0".to_string());
488        }
489        for r in 0..rows {
490            let b = bytes[r];
491            if !matches!(b, 3..=6 | 8) {
492                return Err(format!(
493                    "vbit_ro row {r}: bit width {b} outside {{3,4,5,6,8}}"
494                ));
495            }
496            let want = (cols * b as usize).div_ceil(8);
497            let got = vbit_ro_offset(bytes, off_off, r + 1)
498                .checked_sub(vbit_ro_offset(bytes, off_off, r))
499                .ok_or_else(|| format!("vbit_ro offsets not monotonic at row {r}"))?;
500            if want != got {
501                return Err(format!(
502                    "vbit_ro row {r}: offset span {got} != {want} for width {b}"
503                ));
504            }
505        }
506        let total = packed_off + vbit_ro_offset(bytes, off_off, rows);
507        if total != bytes.len() {
508            return Err(format!(
509                "vbit_ro payload length {} != computed {total}",
510                bytes.len()
511            ));
512        }
513        return Ok(());
514    }
515    if dtype == TensorDtype::Vbit {
516        if shape.len() != 2 {
517            return Err(format!("vbit tensor must be 2-D, got {shape:?}"));
518        }
519        let (rows, cols) = (shape[0], shape[1]);
520        if cols == 0 || cols % GROUP_SIZE != 0 {
521            return Err(format!(
522                "vbit cols {cols} not a positive multiple of {GROUP_SIZE}"
523            ));
524        }
525        // bits header: bounds BEFORE slicing.
526        if bytes.len() < rows {
527            return Err(format!(
528                "vbit payload {} bytes cannot hold the {rows}-byte width header",
529                bytes.len()
530            ));
531        }
532        let ng = cols / GROUP_SIZE;
533        let mut total = rows + rows * ng * 2;
534        for (r, &b) in bytes[..rows].iter().enumerate() {
535            if !matches!(b, 3..=6 | 8) {
536                return Err(format!("vbit row {r}: bit width {b} outside {{3,4,5,6,8}}"));
537            }
538            total += (cols * b as usize).div_ceil(8);
539        }
540        if total != bytes.len() {
541            return Err(format!(
542                "vbit payload length {} != computed {} (rows {rows}, cols {cols})",
543                bytes.len(),
544                total
545            ));
546        }
547        return Ok(());
548    }
549    if has_fixed_payload_size(dtype) && expected_nbytes(dtype, shape).is_none() {
550        return Err(format!(
551            "tensor size overflows usize for {dtype:?}{shape:?}"
552        ));
553    }
554    if let Some(expect) = expected_nbytes(dtype, shape) {
555        if expect != bytes.len() {
556            return Err(format!(
557                "payload length {} != expected {expect} for {dtype:?}{shape:?}",
558                bytes.len()
559            ));
560        }
561    }
562    Ok(())
563}
564
565/// Dequantize any supported tensor into f32.
566pub fn dequant_tensor(entry: &TensorEntry, bytes: &[u8], dst: &mut [f32]) -> Result<(), String> {
567    let n: usize = entry.shape.iter().product();
568    if dst.len() != n {
569        return Err(format!(
570            "dst len {} != tensor elems {} for '{}'",
571            dst.len(),
572            n,
573            entry.name
574        ));
575    }
576    match entry.dtype {
577        TensorDtype::F32 => {
578            for (i, d) in dst.iter_mut().enumerate() {
579                *d = f32::from_le_bytes(bytes[i * 4..i * 4 + 4].try_into().unwrap());
580            }
581        }
582        TensorDtype::F16 => {
583            for (i, d) in dst.iter_mut().enumerate() {
584                *d = f16_to_f32(u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]));
585            }
586        }
587        TensorDtype::Bf16 => {
588            for (i, d) in dst.iter_mut().enumerate() {
589                *d = bf16_to_f32(u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]));
590            }
591        }
592        TensorDtype::Q8Row => {
593            if entry.shape.len() != 2 {
594                return Err(format!("q8_row tensor '{}' must be 2-D", entry.name));
595            }
596            dequant_q8_row(bytes, entry.shape[0], entry.shape[1], dst);
597        }
598        TensorDtype::Q4Block => dequant_q4_block(bytes, dst),
599        TensorDtype::Q4Tiled => dequant_q4_tiled(bytes, dst),
600        TensorDtype::Q1 => dequant_q1(bytes, dst),
601        TensorDtype::Q1S => dequant_q1s(bytes, dst),
602        TensorDtype::Q1T => {
603            if entry.shape.len() != 2 {
604                return Err(format!("q1t tensor '{}' must be 2-D", entry.name));
605            }
606            dequant_q1t(bytes, entry.shape[0], entry.shape[1], dst);
607        }
608        TensorDtype::Vbit => {
609            if entry.shape.len() != 2 {
610                return Err(format!("vbit tensor '{}' must be 2-D", entry.name));
611            }
612            dequant_vbit(bytes, entry.shape[0], entry.shape[1], dst)?;
613        }
614        TensorDtype::VbitRo => {
615            if entry.shape.len() != 2 {
616                return Err(format!("vbit_ro tensor '{}' must be 2-D", entry.name));
617            }
618            dequant_vbit_ro(bytes, entry.shape[0], entry.shape[1], dst)?;
619        }
620        TensorDtype::Q8_2f => {
621            if entry.shape.len() != 2 {
622                return Err(format!("q8_2f tensor '{}' must be 2-D", entry.name));
623            }
624            dequant_q8_2f(bytes, entry.shape[0], entry.shape[1], dst);
625        }
626        other => {
627            return Err(format!(
628                "dtype {} of '{}' is reserved — not decodable by this runtime",
629                other.name(),
630                entry.name
631            ));
632        }
633    }
634    Ok(())
635}
636
637/// Approximate stored bytes per weight for a dtype (informational).
638pub fn bytes_per_weight(dtype: TensorDtype) -> f32 {
639    match dtype {
640        TensorDtype::F32 => 4.0,
641        TensorDtype::F16 | TensorDtype::Bf16 => 2.0,
642        TensorDtype::Q8Row | TensorDtype::Q8_2f => 1.0,
643        TensorDtype::Q4Block | TensorDtype::Q4Col | TensorDtype::Mix84 | TensorDtype::Q4Tiled => {
644            0.5625
645        }
646        TensorDtype::Vbit | TensorDtype::VbitRo => 0.5,
647        TensorDtype::Q1 => 0.1875, // 6 bytes per 32 weights
648        // q1 base + a small sparse f16 overlay (informational; the true
649        // size is the stored span, which grows with the outlier budget).
650        TensorDtype::Q1S => 0.3125,
651        TensorDtype::Q1T => 0.281_25, // 9 bytes per 32 weights (base-3 packed)
652        TensorDtype::U8 => 1.0,
653    }
654}
655
656#[cfg(test)]
657mod f16_tests {
658    use super::{GROUP_SIZE, f16_to_f32, f32_to_f16, validate_payload};
659
660    #[test]
661    fn f16_subnormals_decode_correctly() {
662        // Smallest positive subnormal: 2^-24.
663        assert!((f16_to_f32(0x0001) - 5.9604645e-8).abs() < 1e-12);
664        // Largest subnormal: 1023 * 2^-24.
665        assert!((f16_to_f32(0x03FF) - 6.097_555e-5).abs() < 1e-9);
666        // The value that exposed the halving bug (mant=299, subnormal).
667        assert!((f16_to_f32(0x812b) - -1.7821789e-5).abs() < 1e-9);
668        // Smallest normal (boundary) still correct: 2^-14.
669        assert!((f16_to_f32(0x0400) - 6.1035156e-5).abs() < 1e-9);
670    }
671
672    #[test]
673    fn f16_roundtrip_including_subnormals() {
674        for &v in &[
675            0.0f32, 1.0, -2.5, 6.097e-5, 3.0e-5, 5.96e-8, -1.782e-5, 65504.0,
676        ] {
677            let back = f16_to_f32(f32_to_f16(v));
678            let tol = (v.abs() * 1e-3).max(1e-9);
679            assert!((back - v).abs() <= tol, "roundtrip {v} -> {back}");
680        }
681    }
682    /// §4.9: vbit payload validation — exact length from the width
683    /// header, bounds before any slice, width whitelist.
684    #[test]
685    fn validate_payload_vbit_contract() {
686        use crate::types::TensorDtype as D;
687        let (rows, cols) = (3usize, 64usize);
688        let ng = cols / GROUP_SIZE;
689        let bits = [4u8, 3, 8];
690        let mut good = bits.to_vec();
691        good.extend(std::iter::repeat_n(0u8, rows * ng * 2)); // scales
692        for &b in &bits {
693            good.extend(std::iter::repeat_n(0u8, (cols * b as usize).div_ceil(8)));
694        }
695        assert!(validate_payload(D::Vbit, &[rows, cols], &good).is_ok());
696
697        // Truncated: shorter than the width header itself.
698        assert!(validate_payload(D::Vbit, &[rows, cols], &good[..2]).is_err());
699        // One byte short / one byte long.
700        assert!(validate_payload(D::Vbit, &[rows, cols], &good[..good.len() - 1]).is_err());
701        let mut long = good.clone();
702        long.push(0);
703        assert!(validate_payload(D::Vbit, &[rows, cols], &long).is_err());
704        // Forbidden width (7).
705        let mut bad = good.clone();
706        bad[0] = 7;
707        assert!(validate_payload(D::Vbit, &[rows, cols], &bad).is_err());
708        // Non-2D / non-multiple-of-group cols.
709        assert!(validate_payload(D::Vbit, &[rows * cols], &good).is_err());
710        assert!(validate_payload(D::Vbit, &[rows, 33], &good).is_err());
711
712        // Fixed-size dtype goes through expected_nbytes.
713        let q8 = vec![0u8; 2 * 8 + 2 * 2];
714        assert!(validate_payload(D::Q8Row, &[2, 8], &q8).is_ok());
715        assert!(validate_payload(D::Q8Row, &[2, 8], &q8[..q8.len() - 1]).is_err());
716    }
717
718    /// §4.9: vbit is a first-class supported dtype.
719    #[test]
720    fn vbit_is_supported() {
721        assert!(crate::types::TensorDtype::Vbit.is_supported());
722    }
723}