Skip to main content

cram_codec/
lmodel3.rs

1//! Learned probabilities for TCG3 code-blocks, in **32-bit integer** arithmetic
2//! end to end, so a WGSL kernel (no `i64`) reproduces every probability.
3//!
4//! - Features: the TCG3-visible context (`Feats::raw`: block-local causal
5//!   neighbours, coarser-level parent, residual magnitudes) plus the class
6//!   one-hot, as signed `Q8` log-magnitudes (integer `lg_q8`).
7//! - Network: M1b's MLP (`learned::Net`, ReLU, Q8 weights, Q16 biases),
8//!   evaluated with `i32` accumulators: weights clamped to `±2047`
9//!   (±8.0), activations to `[0, 2^14]` (hidden) / `±2^14` (outputs), biases
10//!   to `±2^24`, so no partial sum can overflow (`28·2^11·2^14 + 2^24 < 2^31`).
11//! - Output: a 2-component discretized logistic mixture (π logit, μ₁, μ₂,
12//!   log₂ s₁, log₂ s₂, all Q8). CDF in Q16 from two integer tables (σ in
13//!   Q16 over `|z| ≤ 16`, `2^{f/256}` in Q16), built on the CPU with basic IEEE
14//!   operations only and uploaded to the GPU as data.
15//! - Binarization (as M1b, without the APM): zero flag, sign, exp-Golomb
16//!   bucket by unary tail ratios, mantissa by integer bisection; each
17//!   decision's `P(1)` is an integer ratio of Q16 CDF masses.
18
19use crate::gpuwav::{Feats, Sink};
20use crate::learned::{lg_q8, Net, NF, NO};
21use std::sync::OnceLock;
22use tang_expr::int::{IId, IType, IntGraph, Program};
23
24/// Features actually used (the rest of the `NF` inputs are zero).
25pub const NFL: usize = 28;
26/// `|z|` range of the σ table (Q8).
27pub const ZM: i32 = 16 * 256;
28
29/// σ table (Q16, `k/256`, `k = 0..=ZM`) and `2^{f/256}` table (Q16).
30pub fn tables() -> &'static (Vec<u32>, Vec<u32>) {
31    static T: OnceLock<(Vec<u32>, Vec<u32>)> = OnceLock::new();
32    T.get_or_init(|| {
33        let x = 1.0f64 / 256.0;
34        let (mut term, mut em) = (1.0f64, 1.0f64);
35        for k in 1..20 {
36            term = term * (-x) / k as f64;
37            em += term;
38        }
39        let mut sig = Vec::with_capacity(ZM as usize + 1);
40        let mut p = 1.0f64;
41        for _ in 0..=ZM {
42            sig.push((65536.0 / (1.0 + p)).round() as u32);
43            p *= em;
44        }
45        let mut r = 2.0f64;
46        for _ in 0..8 {
47            r = r.sqrt();
48        }
49        let mut pw = Vec::with_capacity(257);
50        let mut v = 1.0f64;
51        for _ in 0..=256 {
52            pw.push((v * 65536.0).round() as u32);
53            v *= r;
54        }
55        (sig, pw)
56    })
57}
58
59#[inline]
60fn slg(v: i32) -> i32 {
61    if v < 0 {
62        -lg_q8(v.unsigned_abs())
63    } else {
64        lg_q8(v as u32)
65    }
66}
67
68/// Integer feature vector of a coefficient.
69pub fn feat_vec3(f: &Feats, class: usize) -> [i32; NF] {
70    let r = &f.raw;
71    let mut x = [0i32; NF];
72    for i in 0..7 {
73        x[i] = slg(r[i]);
74    }
75    for i in 7..14 {
76        x[i] = lg_q8(r[i] as u32);
77    }
78    x[14] = r[14] * 64;
79    x[15] = r[15] * 32;
80    x[16] = r[16] * 32;
81    x[17] = r[17] * 256;
82    x[18 + class.min(9)] = 256;
83    x
84}
85
86/// A network prepared for 32-bit inference.
87#[derive(Clone, Debug)]
88pub struct Net32 {
89    /// Layer sizes (input = [`NFL`]).
90    pub sizes: Vec<usize>,
91    /// Clamped Q8 weights per layer (out × in).
92    pub w: Vec<Vec<i32>>,
93    /// Clamped Q16 biases.
94    pub b: Vec<Vec<i32>>,
95}
96
97impl Net32 {
98    /// From an M1b network (its first `NFL` inputs).
99    pub fn new(net: &Net) -> Self {
100        assert_eq!(net.sizes[0], NF);
101        assert_eq!(*net.sizes.last().unwrap(), NO);
102        let mut sizes = net.sizes.clone();
103        sizes[0] = NFL;
104        let w = (0..sizes.len() - 1)
105            .map(|l| {
106                let (ni_full, ni, no) = (net.sizes[l], sizes[l], sizes[l + 1]);
107                (0..no)
108                    .flat_map(|o| (0..ni).map(move |i| (o, i)))
109                    .map(|(o, i)| net.wq[l][o * ni_full + i].clamp(-2047, 2047))
110                    .collect()
111            })
112            .collect();
113        let b = net
114            .bq
115            .iter()
116            .map(|v| v.iter().map(|&x| x.clamp(-(1 << 24), 1 << 24)).collect())
117            .collect();
118        Self { sizes, w, b }
119    }
120
121    /// Integer forward pass.
122    pub fn infer(&self, x: &[i32; NF]) -> [i32; NO] {
123        let mut a = [0i32; 64];
124        let mut nb = [0i32; 64];
125        for i in 0..NFL {
126            a[i] = x[i].clamp(-(1 << 14), 1 << 14);
127        }
128        let nl = self.sizes.len() - 1;
129        for l in 0..nl {
130            let (ni, no) = (self.sizes[l], self.sizes[l + 1]);
131            for o in 0..no {
132                let mut acc = self.b[l][o];
133                for i in 0..ni {
134                    acc = acc.wrapping_add(self.w[l][o * ni + i].wrapping_mul(a[i]));
135                }
136                let v = acc.wrapping_add(128) >> 8;
137                nb[o] = if l + 1 < nl {
138                    v.clamp(0, 1 << 14)
139                } else {
140                    v.clamp(-(1 << 14), 1 << 14)
141                };
142            }
143            std::mem::swap(&mut a, &mut nb);
144        }
145        [a[0], a[1], a[2], a[3], a[4]]
146    }
147
148    /// The forward pass as a `tang-expr` integer graph: params are the
149    /// [`NFL`] clamped inputs, outputs the [`NO`] mixture parameters. Zero
150    /// weights are dropped (integer addition is associative, so no bit
151    /// changes). This one graph is lowered to the GPU kernel's MLP and
152    /// evaluated as the CPU reference ([`Net32::program`]).
153    pub fn graph(&self) -> (IntGraph, Vec<IId>) {
154        let mut g = IntGraph::new();
155        let mut a: Vec<IId> = (0..NFL).map(|i| g.param(i as u16, IType::I32)).collect();
156        let nl = self.sizes.len() - 1;
157        let (r, sh) = (g.i32(128), g.u32(8));
158        for l in 0..nl {
159            let (ni, no) = (self.sizes[l], self.sizes[l + 1]);
160            let (lo, hi) = if l + 1 < nl {
161                (g.i32(0), g.i32(1 << 14))
162            } else {
163                (g.i32(-(1 << 14)), g.i32(1 << 14))
164            };
165            a = (0..no)
166                .map(|o| {
167                    let mut acc = g.i32(self.b[l][o]);
168                    for (i, &ai) in a.iter().enumerate().take(ni) {
169                        let w = self.w[l][o * ni + i];
170                        if w != 0 {
171                            let w = g.i32(w);
172                            let t = g.mul(w, ai);
173                            acc = g.add(acc, t);
174                        }
175                    }
176                    let v = g.add(acc, r);
177                    let v = g.shr(v, sh);
178                    g.clamp(v, lo, hi)
179                })
180                .collect();
181        }
182        (g, a)
183    }
184
185    /// [`Net32::graph`] compiled for [`Net32::infer_ir`].
186    pub fn program(&self) -> Program {
187        let (g, o) = self.graph();
188        Program::new(&g, &o)
189    }
190
191    /// [`Net32::infer`] through the integer-graph interpreter (bit-identical).
192    pub fn infer_ir(&self, p: &mut Program, x: &[i32; NF]) -> [i32; NO] {
193        let mut inp = [0u32; NFL];
194        for i in 0..NFL {
195            inp[i] = x[i].clamp(-(1 << 14), 1 << 14) as u32;
196        }
197        let mut o = [0u32; NO];
198        p.run(&inp, &[], &mut o);
199        o.map(|v| v as i32)
200    }
201
202    /// Flattened `[nl, sizes…, w₀, b₀, w₁, b₁, …]` for the GPU.
203    pub fn flat(&self) -> Vec<u32> {
204        let mut v = vec![(self.sizes.len() - 1) as u32];
205        v.extend(self.sizes.iter().map(|&s| s as u32));
206        for l in 0..self.sizes.len() - 1 {
207            v.extend(self.w[l].iter().map(|&x| x as u32));
208            v.extend(self.b[l].iter().map(|&x| x as u32));
209        }
210        v
211    }
212}
213
214/// σ(z), z in Q8 → Q16.
215#[inline]
216pub fn sig16(z: i32) -> u32 {
217    let (s, _) = tables();
218    let a = z.unsigned_abs().min(ZM as u32) as usize;
219    if z >= 0 {
220        s[a]
221    } else {
222        65536 - s[a]
223    }
224}
225
226/// `d · 2^{−ls/256}` (both Q8; result Q8) saturated to `±(ZM+1)`, with
227/// `ls` clamped to `[−16·256, 24·256]`: `t = ⌊d·m/2^16⌉` with the Q16
228/// mantissa `m = 2^{(256−f)/256}`, then an arithmetic or saturating shift.
229#[inline]
230pub fn zval(d: i32, ls: i32) -> i32 {
231    let (_, pw) = tables();
232    let ls = ls.clamp(-16 * 256, 24 * 256);
233    let i = ls.div_euclid(256);
234    let f = ls.rem_euclid(256);
235    let m = pw[(256 - f) as usize] as i32;
236    let t = crate::gpuwav::mulsh(d.clamp(-(1 << 25), 1 << 25), m, 16);
237    let sh = i + 1;
238    if sh >= 0 {
239        t >> sh.min(31)
240    } else {
241        let k = -sh;
242        let lim = (ZM + 1) >> k;
243        if t > lim {
244            ZM + 1
245        } else if t < -lim {
246            -(ZM + 1)
247        } else {
248            t << k
249        }
250    }
251}
252
253/// Mixture parameters.
254#[derive(Clone, Copy, Debug)]
255pub struct Mix3 {
256    pi15: u32,
257    mu: [i32; 2],
258    ls: [i32; 2],
259}
260
261impl Mix3 {
262    /// From network outputs.
263    pub fn new(o: &[i32; NO]) -> Self {
264        Self {
265            pi15: sig16(o[0]) >> 1,
266            mu: [o[1], o[2]],
267            ls: [o[3], o[4]],
268        }
269    }
270
271    /// `P(r ≤ m)` in Q16.
272    #[inline]
273    pub fn cdf(&self, m: i32) -> u32 {
274        let t = m.clamp(-65535, 65535) * 256 + 128;
275        let c = |k: usize| sig16(zval(t - self.mu[k], self.ls[k]));
276        (self.pi15 * c(0) + (32768 - self.pi15) * c(1)) >> 15
277    }
278}
279
280/// `P(1)·2^16` from masses (`num ≤ den ≤ 2^16`), clamped for rANS.
281#[inline]
282pub fn ratio16(num: u32, den: u32) -> u32 {
283    let p = (num.min(den) * 65535).checked_div(den).unwrap_or(32768);
284    p.clamp(32, 65504)
285}
286
287/// Inverse of [`sig16`] on a `p >> 4` grid: the smallest Q8 `z` with
288/// `sig16(z) ≥ p` (`p = 16·i + 8`), so `stretch(p) ∈ [−ZM, ZM]`.
289pub fn stretch_tab() -> &'static Vec<i32> {
290    static T: OnceLock<Vec<i32>> = OnceLock::new();
291    T.get_or_init(|| {
292        let mut z = -ZM;
293        (0..4096u32)
294            .map(|i| {
295                let p = 16 * i + 8;
296                while z < ZM && sig16(z) < p {
297                    z += 1;
298                }
299                z
300            })
301            .collect()
302    })
303}
304
305/// Adaptive-layer settings (stream header).
306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
307pub struct AdCfg {
308    /// APM weight in sixteenths (0 = off).
309    pub wa: u32,
310    /// Count limit of the APM bins (rate floor `2^-⌊log₂(lim+2)⌋`).
311    pub lim: u32,
312    /// Decision mask: bit 0 zero flag, 1 sign, 2 bucket, 3 mantissa.
313    pub mask: u32,
314}
315
316/// APM bins per context (step 1.0 in the logistic domain, `±8`).
317pub const AB: usize = 17;
318/// APM contexts.
319pub const AC: usize = 8;
320
321/// A lane's fresh adaptive state as the kernels hold it: `(p << 8) | count`
322/// per bin (count 0), the layout of the adaptive residual graph's `adt`.
323pub fn ad_init() -> [u32; AC * AB] {
324    std::array::from_fn(|i| sig16(((i % AB) as i32 - 8) * 256) << 8)
325}
326
327/// Per-lane adaptive state over the MLP output: an adaptive probability map
328/// (secondary estimation) per decision type, interpolated over [`AB`]
329/// stretch-domain bins, count-based power-of-two rate. Integer only; reset per block.
330#[derive(Clone, Debug)]
331pub struct Adapt {
332    /// Settings.
333    pub c: AdCfg,
334    t: [u32; AC * AB],
335    n: [u8; AC * AB],
336}
337
338impl Adapt {
339    /// Fresh state.
340    pub fn new(c: AdCfg) -> Self {
341        let t = std::array::from_fn(|i| sig16(((i % AB) as i32 - 8) * 256));
342        Self {
343            c,
344            t,
345            n: [0; AC * AB],
346        }
347    }
348
349    /// Code `bit` with model `P(1) = p1` on context `ctx`.
350    #[inline]
351    fn bit<S: Sink>(&mut self, s: &mut S, lane: usize, ctx: usize, bit: bool, p1: u32) -> bool {
352        let grp = match ctx {
353            0 | 1 => ctx,
354            2..=5 => 2,
355            _ => 3,
356        };
357        if self.c.wa == 0 || (self.c.mask >> grp) & 1 == 0 {
358            return s.bit(lane, bit, p1);
359        }
360        let st = stretch_tab()[(p1 >> 4) as usize].clamp(-2048, 2047) + 2048;
361        let (j, w) = ((st >> 8) as usize, (st & 255) as u32);
362        let b = ctx * AB + j;
363        let pr = (self.t[b] * (256 - w) + self.t[b + 1] * w) >> 8;
364        let pf = (pr * self.c.wa + p1 * (16 - self.c.wa)) >> 4;
365        let got = s.bit(lane, bit, pf.clamp(32, 65504));
366        let k = if w < 128 { b } else { b + 1 };
367        let tgt = if got { 65535 } else { 0 };
368        let d = tgt - self.t[k] as i32;
369        // rate 2^-⌊log₂(n+2)⌋ (≈ 1/(n+1.5) within 2×, no divide on the GPU)
370        self.t[k] =
371            (self.t[k] as i32 + (d >> (31 - (self.n[k] as u32 + 2).leading_zeros()))) as u32;
372        if (self.n[k] as u32) < self.c.lim {
373            self.n[k] += 1;
374        }
375        got
376    }
377}
378
379/// Code residual `r` under `mx` on `lane`.
380pub fn code_learned<S: Sink>(s: &mut S, lane: usize, mx: &Mix3, r: i32) -> i32 {
381    let mut ad = Adapt::new(AdCfg {
382        wa: 0,
383        lim: 0,
384        mask: 0,
385    });
386    code_learned_ad(s, lane, mx, r, &mut ad)
387}
388
389/// [`code_learned`] through the lane's adaptive layer.
390pub fn code_learned_ad<S: Sink>(s: &mut S, lane: usize, mx: &Mix3, r: i32, ad: &mut Adapt) -> i32 {
391    let c0 = mx.cdf(0);
392    let cm1 = mx.cdf(-1);
393    let pz = c0.saturating_sub(cm1);
394    if !ad.bit(s, lane, 0, r != 0, ratio16(65536 - pz, 65536)) {
395        return 0;
396    }
397    let negm = cm1;
398    let posm = 65536 - c0;
399    let neg = ad.bit(s, lane, 1, r < 0, ratio16(negm, negm + posm));
400    let tail = |m: u32| -> u32 {
401        if m > 1 << 30 {
402            return 0;
403        }
404        if neg {
405            mx.cdf(-(m as i32))
406        } else {
407            65536 - mx.cdf(m as i32 - 1)
408        }
409    };
410    let mag = r.unsigned_abs();
411    let mut k = 0u32;
412    while k < 30 {
413        let a = tail(1 << k);
414        let bb = tail(1 << (k + 1));
415        if !ad.bit(
416            s,
417            lane,
418            2 + k.min(3) as usize,
419            mag >= 1 << (k + 1),
420            ratio16(bb, a),
421        ) {
422            break;
423        }
424        k += 1;
425    }
426    let (mut lo, mut hi) = (1u32 << k, ((1u64 << (k + 1)) - 1) as u32);
427    let mut first = true;
428    while lo < hi {
429        let mid = lo + (hi - lo).div_ceil(2);
430        let end = if hi == u32::MAX { 0 } else { tail(hi + 1) };
431        let up = tail(mid).saturating_sub(end);
432        let all = tail(lo).saturating_sub(end);
433        let ctx = if first { 6 } else { 7 };
434        first = false;
435        if ad.bit(s, lane, ctx, mag >= mid, ratio16(up, all)) {
436            lo = mid;
437        } else {
438            hi = mid - 1;
439        }
440    }
441    if neg {
442        (lo as i32).wrapping_neg()
443    } else {
444        lo as i32
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::gpuwav::{rans_encode, DecSink, RansDec, RecSink};
452
453    #[test]
454    fn ir_graph_matches_infer() {
455        let mut st = 7u64;
456        let mut rnd = || {
457            st = st
458                .wrapping_mul(6364136223846793005)
459                .wrapping_add(1442695040888963407);
460            (st >> 33) as i32
461        };
462        for sizes in [vec![NFL, 16, NO], vec![NFL, 32, 16, NO]] {
463            let w = (0..sizes.len() - 1)
464                .map(|l| {
465                    (0..sizes[l] * sizes[l + 1])
466                        .map(|k| if k % 5 == 0 { 0 } else { rnd() % 4095 - 2047 })
467                        .collect()
468                })
469                .collect();
470            let b = (1..sizes.len())
471                .map(|l| {
472                    (0..sizes[l])
473                        .map(|_| rnd() % (1 << 25) - (1 << 24))
474                        .collect()
475                })
476                .collect();
477            let net = Net32 { sizes, w, b };
478            let mut p = net.program();
479            for k in 0..5000 {
480                let mut x = [0i32; NF];
481                for v in x.iter_mut() {
482                    *v = match k % 3 {
483                        0 => rnd() % 40000 - 20000,
484                        1 => rnd() % 512,
485                        _ => [16384, -16384, i32::MAX, i32::MIN][(rnd() & 3) as usize],
486                    };
487                }
488                assert_eq!(net.infer_ir(&mut p, &x), net.infer(&x));
489            }
490        }
491    }
492
493    #[test]
494    fn learned_round_trip() {
495        let mut st = 99u64;
496        let mut rnd = || {
497            st = st
498                .wrapping_mul(6364136223846793005)
499                .wrapping_add(1442695040888963407);
500            (st >> 33) as i32
501        };
502        let cases: Vec<(Mix3, i32)> = (0..20000)
503            .map(|i| {
504                let o = [
505                    rnd() % 2000,
506                    rnd() % 3000,
507                    rnd() % 600,
508                    rnd() % 4000 - 600,
509                    rnd() % 2500,
510                ];
511                let r = match i % 5 {
512                    0 => 0,
513                    1 => rnd() % 7 - 3,
514                    2 => rnd() % 300 - 150,
515                    3 => rnd() % 200000 - 100000,
516                    _ => [i32::MAX, i32::MIN + 1, 65536, -65537][(i / 5) % 4],
517                };
518                (Mix3::new(&o), r)
519            })
520            .collect();
521        let mut s = RecSink {
522            syms: vec![Vec::new()],
523        };
524        for (m, r) in &cases {
525            assert_eq!(code_learned(&mut s, 0, m, *r), *r);
526        }
527        let w = rans_encode(&s.syms[0]);
528        let mut d = DecSink {
529            lanes: vec![RansDec::new(&w)],
530        };
531        for (m, r) in &cases {
532            assert_eq!(code_learned(&mut d, 0, m, 0), *r);
533        }
534    }
535}