Skip to main content

cortiq_core/
blr.rs

1//! Binary low-rank weights (`blr`, dtype 19) — docs/BLR_NVG.md §2.
2//!
3//! A linear layer `W [d_out, d_in]` is stored as `P ∈ {1, 2}` paths, each
4//! `diag(h) · S_U · diag(ℓ) · S_Vᵀ · diag(g)` with `S_U ∈ {±1}^{d_out×r}`,
5//! `S_V ∈ {±1}^{d_in×r}` and f16 scales. The factorization layout is the one
6//! of LittleBit (Lee et al., NeurIPS 2025); here it is only a storage
7//! container — which signs and scales go into it is the encoder's business
8//! (`cortiq convert --quant blr`, NVG or the dual-SVID baseline).
9//!
10//! Payload (little-endian):
11//!
12//! ```text
13//! header 16 B: u8 magic 0x4C, u8 version 1, u8 P, u8 0, u32 r, u32 d_out, u32 d_in
14//! per path p: h f16[d_out] | g f16[d_in] | ℓ f16[r]
15//!             U: d_out rows × r/8 bytes   bit (i,k) = byte[i·r/8 + k/8] >> (k%8) & 1
16//!             V: r rows × d_in/8 bytes    bit (k,j) = byte[k·d_in/8 + j/8] >> (j%8) & 1
17//! ```
18//!
19//! Bit 1 → +1, bit 0 → −1; sign(0) encodes as 1. `V` is stored transposed (one
20//! row per latent) so both kernel stages stream contiguous rows. The magic
21//! byte and the layout are byte-identical to the research branch's files, so
22//! those stay readable.
23
24use crate::quant::{f16_to_f32, f32_to_f16};
25
26pub const BLR_MAGIC: u8 = 0x4C;
27pub const BLR_VERSION: u8 = 1;
28pub const BLR_HEADER: usize = 16;
29
30/// Parsed `blr` header.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct BlrHeader {
33    pub paths: usize,
34    pub rank: usize,
35    pub d_out: usize,
36    pub d_in: usize,
37}
38
39/// Byte offsets of one path's sections inside the payload.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct BlrPathOffsets {
42    pub h: usize,
43    pub g: usize,
44    pub l: usize,
45    pub u: usize,
46    pub v: usize,
47}
48
49/// Bytes of one path: `2(d_out + d_in + r) + r(d_out + d_in)/8`.
50pub fn blr_path_bytes(d_out: usize, d_in: usize, r: usize) -> usize {
51    2 * (d_out + d_in + r) + d_out * (r / 8) + r * (d_in / 8)
52}
53
54/// Exact payload length for `paths` paths of rank `r`.
55pub fn blr_payload_len(paths: usize, r: usize, d_out: usize, d_in: usize) -> usize {
56    BLR_HEADER + paths * blr_path_bytes(d_out, d_in, r)
57}
58
59/// Strict bit count of the container (scales at 16 bits; the 16-byte header
60/// is not counted): `P · [r(d_out + d_in) + 16(d_out + d_in) + 16 r]`.
61pub fn blr_bits(d_out: usize, d_in: usize, r: usize, paths: usize) -> u64 {
62    paths as u64 * (r as u64 * (d_out + d_in) as u64 + 16 * (d_out + d_in) as u64 + 16 * r as u64)
63}
64
65/// Rank rule (docs/BLR_NVG.md §1): float `r*` for the budget `b` bits per
66/// weight, times `mult` (the K/V factor) before rounding, floored to a
67/// multiple of 8, at least 8 and at most `min(d_out, d_in)` rounded down to 8.
68pub fn blr_rank(d_out: usize, d_in: usize, b: f64, paths: usize, mult: f64) -> usize {
69    let (m, n) = (d_out as f64, d_in as f64);
70    let s = if paths == 2 {
71        (b * m * n - 32.0 * (m + n)) / (2.0 * (m + n + 16.0))
72    } else {
73        (b * m * n - 16.0 * (m + n)) / (m + n + 16.0)
74    };
75    let r = ((s * mult / 8.0).floor() * 8.0).max(8.0) as usize;
76    let cap = (d_out.min(d_in) / 8) * 8;
77    r.min(cap.max(8))
78}
79
80/// Parse and sanity-check the 16-byte header (no length check).
81pub fn blr_parse_header(bytes: &[u8]) -> Result<BlrHeader, String> {
82    if bytes.len() < BLR_HEADER {
83        return Err(format!("blr payload {} bytes < 16-byte header", bytes.len()));
84    }
85    if bytes[0] != BLR_MAGIC {
86        return Err(format!("blr magic {:#04x} != 0x4c", bytes[0]));
87    }
88    if bytes[1] != BLR_VERSION {
89        return Err(format!("blr version {} unsupported (want 1)", bytes[1]));
90    }
91    let paths = bytes[2] as usize;
92    if !(1..=2).contains(&paths) {
93        return Err(format!("blr paths {paths} not in {{1,2}}"));
94    }
95    if bytes[3] != 0 {
96        return Err("blr reserved byte must be 0".into());
97    }
98    let rd = |o: usize| u32::from_le_bytes(bytes[o..o + 4].try_into().unwrap()) as usize;
99    Ok(BlrHeader {
100        paths,
101        rank: rd(4),
102        d_out: rd(8),
103        d_in: rd(12),
104    })
105}
106
107/// Section offsets of path `p`.
108pub fn blr_path_offsets(h: &BlrHeader, p: usize) -> BlrPathOffsets {
109    let base = BLR_HEADER + p * blr_path_bytes(h.d_out, h.d_in, h.rank);
110    let hh = base;
111    let g = hh + 2 * h.d_out;
112    let l = g + 2 * h.d_in;
113    let u = l + 2 * h.rank;
114    let v = u + h.d_out * (h.rank / 8);
115    BlrPathOffsets { h: hh, g, l, u, v }
116}
117
118/// Full validation against the directory shape: header fields, constraints,
119/// exact length — before any slice is taken.
120pub fn validate_blr(shape: &[usize], bytes: &[u8]) -> Result<BlrHeader, String> {
121    if shape.len() != 2 {
122        return Err(format!("blr tensor must be 2-D, got {shape:?}"));
123    }
124    let h = blr_parse_header(bytes)?;
125    if h.d_out != shape[0] || h.d_in != shape[1] {
126        return Err(format!(
127            "blr header dims {}x{} != shape {shape:?}",
128            h.d_out, h.d_in
129        ));
130    }
131    if h.d_out == 0 || h.d_in == 0 || h.d_in % 8 != 0 {
132        return Err(format!(
133            "blr d_in {} must be a positive multiple of 8",
134            h.d_in
135        ));
136    }
137    if h.rank < 8 || h.rank % 8 != 0 || h.rank > h.d_out.min(h.d_in) {
138        return Err(format!(
139            "blr rank {} must be a multiple of 8 in [8, min({}, {})]",
140            h.rank, h.d_out, h.d_in
141        ));
142    }
143    let want = blr_payload_len(h.paths, h.rank, h.d_out, h.d_in);
144    if bytes.len() != want {
145        return Err(format!(
146            "blr payload length {} != computed {want} (P {} r {} {}x{})",
147            bytes.len(),
148            h.paths,
149            h.rank,
150            h.d_out,
151            h.d_in
152        ));
153    }
154    Ok(h)
155}
156
157/// Read `n` f16 values at `off` as f32.
158pub fn blr_f16s(bytes: &[u8], off: usize, n: usize) -> Vec<f32> {
159    (0..n)
160        .map(|i| {
161            f16_to_f32(u16::from_le_bytes([
162                bytes[off + 2 * i],
163                bytes[off + 2 * i + 1],
164            ]))
165        })
166        .collect()
167}
168
169#[inline]
170fn bit(bytes: &[u8], off: usize, idx: usize) -> bool {
171    (bytes[off + idx / 8] >> (idx % 8)) & 1 == 1
172}
173
174/// Reference dequant: `W[i,:] = Σ_p h_p[i] Σ_k ℓ_p[k] U_p(i,k) (V_p(k,:) ⊙ g_p)`.
175pub fn dequant_blr(bytes: &[u8], rows: usize, cols: usize, dst: &mut [f32]) -> Result<(), String> {
176    let h = validate_blr(&[rows, cols], bytes)?;
177    if dst.len() != rows * cols {
178        return Err(format!("blr dst len {} != {}", dst.len(), rows * cols));
179    }
180    dst.iter_mut().for_each(|v| *v = 0.0);
181    let r = h.rank;
182    let mut vg = vec![0f32; r * cols];
183    for p in 0..h.paths {
184        let o = blr_path_offsets(&h, p);
185        let hv = blr_f16s(bytes, o.h, rows);
186        let gv = blr_f16s(bytes, o.g, cols);
187        let lv = blr_f16s(bytes, o.l, r);
188        for k in 0..r {
189            let row = &mut vg[k * cols..(k + 1) * cols];
190            for j in 0..cols {
191                let s = if bit(bytes, o.v + k * (cols / 8), j) {
192                    1.0
193                } else {
194                    -1.0
195                };
196                row[j] = s * gv[j];
197            }
198        }
199        let mut acc = vec![0f32; cols];
200        for i in 0..rows {
201            acc.iter_mut().for_each(|v| *v = 0.0);
202            for k in 0..r {
203                let s = if bit(bytes, o.u + i * (r / 8), k) {
204                    lv[k]
205                } else {
206                    -lv[k]
207                };
208                let src = &vg[k * cols..(k + 1) * cols];
209                for j in 0..cols {
210                    acc[j] += s * src[j];
211                }
212            }
213            let d = &mut dst[i * cols..(i + 1) * cols];
214            for j in 0..cols {
215                d[j] += hv[i] * acc[j];
216            }
217        }
218    }
219    Ok(())
220}
221
222/// One path to encode. Signs as bools (`true` = +1), `su[i·r + k]` and
223/// `sv[j·r + k]` (both row-major over their own rows, latent index fastest).
224pub struct BlrPathIn<'a> {
225    pub h: &'a [f32],
226    pub g: &'a [f32],
227    pub l: &'a [f32],
228    pub su: &'a [bool],
229    pub sv: &'a [bool],
230}
231
232/// Pack paths into a `blr` payload. Scales are written as given (the encoder
233/// rebalances them before rounding).
234pub fn encode_blr(d_out: usize, d_in: usize, r: usize, paths: &[BlrPathIn]) -> Vec<u8> {
235    assert!(matches!(paths.len(), 1 | 2));
236    assert!(r % 8 == 0 && r >= 8 && d_in % 8 == 0);
237    let mut out = Vec::with_capacity(blr_payload_len(paths.len(), r, d_out, d_in));
238    out.push(BLR_MAGIC);
239    out.push(BLR_VERSION);
240    out.push(paths.len() as u8);
241    out.push(0);
242    out.extend_from_slice(&(r as u32).to_le_bytes());
243    out.extend_from_slice(&(d_out as u32).to_le_bytes());
244    out.extend_from_slice(&(d_in as u32).to_le_bytes());
245    for p in paths {
246        assert_eq!(p.h.len(), d_out);
247        assert_eq!(p.g.len(), d_in);
248        assert_eq!(p.l.len(), r);
249        assert_eq!(p.su.len(), d_out * r);
250        assert_eq!(p.sv.len(), d_in * r);
251        for v in p.h.iter().chain(p.g).chain(p.l) {
252            out.extend_from_slice(&f32_to_f16(*v).to_le_bytes());
253        }
254        for i in 0..d_out {
255            for kb in 0..r / 8 {
256                let mut b = 0u8;
257                for t in 0..8 {
258                    if p.su[i * r + kb * 8 + t] {
259                        b |= 1 << t;
260                    }
261                }
262                out.push(b);
263            }
264        }
265        for k in 0..r {
266            for jb in 0..d_in / 8 {
267                let mut b = 0u8;
268                for t in 0..8 {
269                    if p.sv[(jb * 8 + t) * r + k] {
270                        b |= 1 << t;
271                    }
272                }
273                out.push(b);
274            }
275        }
276    }
277    debug_assert_eq!(out.len(), blr_payload_len(paths.len(), r, d_out, d_in));
278    out
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    struct Rng(u64);
286    impl Rng {
287        fn next(&mut self) -> u64 {
288            self.0 = self.0.wrapping_add(0x9E3779B97F4A7C15);
289            let mut z = self.0;
290            z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
291            z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
292            z ^ (z >> 31)
293        }
294        fn f(&mut self) -> f32 {
295            (self.next() >> 40) as f32 / (1u64 << 24) as f32
296        }
297    }
298
299    #[test]
300    fn rank_rule_anchors() {
301        assert_eq!(blr_rank(4096, 4096, 0.55, 2, 1.0), 544);
302        assert_eq!(blr_rank(4096, 11008, 0.10, 2, 1.0), 128);
303        // Qwen3-0.6B shapes
304        assert_eq!(blr_rank(2048, 1024, 1.0, 2, 1.0), 320);
305        assert_eq!(blr_rank(1024, 1024, 1.0, 2, 1.0), 232);
306        assert_eq!(blr_rank(3072, 1024, 1.0, 2, 1.0), 360);
307        assert_eq!(blr_rank(2048, 1024, 0.55, 2, 1.0), 168);
308        assert_eq!(blr_rank(1024, 1024, 0.55, 2, 1.0), 120);
309        assert_eq!(blr_rank(1024, 3072, 0.55, 2, 1.0), 192);
310        assert_eq!(blr_rank(1024, 1024, 0.55, 2, 4.0), 488);
311        assert_eq!(blr_rank(1024, 1024, 0.1, 2, 1.0), 8);
312        // clamp to min(d_out, d_in)
313        assert_eq!(blr_rank(64, 64, 8.0, 2, 1.0), 64);
314        let b = blr_bits(4096, 4096, 544, 2) as f64 / (4096.0 * 4096.0);
315        assert!((b - 0.5479).abs() < 1e-4, "{b}");
316    }
317
318    fn random_paths(
319        rng: &mut Rng,
320        d_out: usize,
321        d_in: usize,
322        r: usize,
323        p: usize,
324    ) -> Vec<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<bool>, Vec<bool>)> {
325        (0..p)
326            .map(|_| {
327                let h: Vec<f32> = (0..d_out).map(|_| 0.5 + rng.f()).collect();
328                let g: Vec<f32> = (0..d_in).map(|_| 0.5 + rng.f()).collect();
329                let l: Vec<f32> = (0..r).map(|_| 0.01 + 0.1 * rng.f()).collect();
330                let su: Vec<bool> = (0..d_out * r).map(|_| rng.next() & 1 == 1).collect();
331                let sv: Vec<bool> = (0..d_in * r).map(|_| rng.next() & 1 == 1).collect();
332                (h, g, l, su, sv)
333            })
334            .collect()
335    }
336
337    #[test]
338    fn encode_dequant_roundtrip() {
339        let mut rng = Rng(7);
340        for &(d_out, d_in, r, p) in &[
341            (24usize, 16usize, 8usize, 1usize),
342            (40, 32, 16, 2),
343            (16, 48, 16, 2),
344        ] {
345            let ps = random_paths(&mut rng, d_out, d_in, r, p);
346            let ins: Vec<BlrPathIn> = ps
347                .iter()
348                .map(|(h, g, l, su, sv)| BlrPathIn { h, g, l, su, sv })
349                .collect();
350            let bytes = encode_blr(d_out, d_in, r, &ins);
351            assert_eq!(bytes.len(), blr_payload_len(p, r, d_out, d_in));
352            let mut w = vec![0f32; d_out * d_in];
353            dequant_blr(&bytes, d_out, d_in, &mut w).unwrap();
354            let q = |x: f32| f16_to_f32(f32_to_f16(x));
355            for i in 0..d_out {
356                for j in 0..d_in {
357                    let mut want = 0f64;
358                    for (h, g, l, su, sv) in &ps {
359                        let mut s = 0f64;
360                        for k in 0..r {
361                            let a = if su[i * r + k] { 1.0 } else { -1.0 };
362                            let b = if sv[j * r + k] { 1.0 } else { -1.0 };
363                            s += a * b * q(l[k]) as f64;
364                        }
365                        want += q(h[i]) as f64 * q(g[j]) as f64 * s;
366                    }
367                    let got = w[i * d_in + j] as f64;
368                    assert!(
369                        (got - want).abs() <= 1e-5 * (1.0 + want.abs()),
370                        "{i},{j}: {got} vs {want}"
371                    );
372                }
373            }
374        }
375    }
376
377    #[test]
378    fn validate_rejects_each_field() {
379        let mut rng = Rng(3);
380        let (d_out, d_in, r) = (32usize, 32usize, 16usize);
381        let ps = random_paths(&mut rng, d_out, d_in, r, 2);
382        let ins: Vec<BlrPathIn> = ps
383            .iter()
384            .map(|(h, g, l, su, sv)| BlrPathIn { h, g, l, su, sv })
385            .collect();
386        let good = encode_blr(d_out, d_in, r, &ins);
387        assert!(validate_blr(&[d_out, d_in], &good).is_ok());
388        assert!(validate_blr(&[d_out, d_in + 8], &good).is_err());
389        assert!(validate_blr(&[d_out * d_in], &good).is_err());
390        assert!(validate_blr(&[d_out, d_in], &good[..good.len() - 1]).is_err());
391        let mut long = good.clone();
392        long.push(0);
393        assert!(validate_blr(&[d_out, d_in], &long).is_err());
394        assert!(validate_blr(&[d_out, d_in], &good[..10]).is_err());
395        let set = |at: usize, v: u8| {
396            let mut b = good.clone();
397            b[at] = v;
398            b
399        };
400        assert!(validate_blr(&[d_out, d_in], &set(0, 0x4D)).is_err()); // magic
401        assert!(validate_blr(&[d_out, d_in], &set(1, 2)).is_err()); // version
402        assert!(validate_blr(&[d_out, d_in], &set(2, 3)).is_err()); // paths
403        assert!(validate_blr(&[d_out, d_in], &set(2, 1)).is_err()); // length no longer matches
404        assert!(validate_blr(&[d_out, d_in], &set(3, 1)).is_err()); // reserved
405        assert!(validate_blr(&[d_out, d_in], &set(4, 12)).is_err()); // r % 8
406        assert!(validate_blr(&[d_out, d_in], &set(4, 40)).is_err()); // r > min
407        assert!(validate_blr(&[d_out, d_in], &set(8, 33)).is_err()); // d_out
408        assert!(validate_blr(&[d_out, d_in], &set(12, 33)).is_err()); // d_in
409    }
410
411    /// The generic payload gate and the generic decoder both route dtype 19
412    /// here: `validate_payload` is what `CmfModel::open`/`verify` run, and
413    /// `dequant_tensor` is the dense fallback of every engine path.
414    #[test]
415    fn generic_codec_entry_points_handle_blr() {
416        use crate::format::TensorEntry;
417        use crate::types::TensorDtype;
418        let mut rng = Rng(5);
419        let (d_out, d_in, r) = (48usize, 64usize, 16usize);
420        let ps = random_paths(&mut rng, d_out, d_in, r, 2);
421        let ins: Vec<BlrPathIn> = ps
422            .iter()
423            .map(|(h, g, l, su, sv)| BlrPathIn { h, g, l, su, sv })
424            .collect();
425        let bytes = encode_blr(d_out, d_in, r, &ins);
426        let d = TensorDtype::BinaryLowRank;
427        assert_eq!(TensorDtype::from_id(19), Some(d));
428        assert_eq!(d.id(), 19);
429        assert_eq!(d.name(), "blr");
430        assert!(d.is_supported());
431        assert_eq!(crate::quant::expected_nbytes(d, &[d_out, d_in]), None);
432        assert!(crate::quant::validate_payload(d, &[d_out, d_in], &bytes).is_ok());
433        assert!(crate::quant::validate_payload(d, &[d_out, d_in], &bytes[1..]).is_err());
434        let entry = TensorEntry {
435            name: "model.layers.0.mlp.down_proj.weight".into(),
436            dtype: d,
437            shape: vec![d_out, d_in],
438            off: 0,
439            nbytes: bytes.len() as u64,
440            shard: 0,
441            hash: 0,
442        };
443        let mut a = vec![0f32; d_out * d_in];
444        let mut b = vec![0f32; d_out * d_in];
445        crate::quant::dequant_tensor(&entry, &bytes, &mut a).unwrap();
446        dequant_blr(&bytes, d_out, d_in, &mut b).unwrap();
447        assert_eq!(a, b);
448    }
449}