Skip to main content

cram_codec/
learned.rs

1//! Learned coefficient models: a small MLP predicts each residual's
2//! distribution — a two-component **discretized logistic mixture** — from the
3//! same causal context features the hand model uses ([`Feat`]: neighbour and
4//! parent magnitudes, neighbour signs, prediction magnitude, band class and
5//! orientation).
6//!
7//! # Bit-exact inference
8//!
9//! Everything from features to coded bits is integer arithmetic:
10//!
11//! - features are `Q8` fixed-point `log₂(1+v)` computed with integer ops;
12//! - the network runs on `i32` weights (`Q8`) and `i64` accumulators, with
13//!   `>> 8` rescaling and integer ReLU;
14//! - the mixture CDF uses integer tables (logistic in `Q24`, `2^{f/256}` in
15//!   `Q16`) built once from IEEE `+ − × / √` only — correctly rounded, so
16//!   identical on every conforming backend (no `exp`/`pow` from libm);
17//! - the residual is binarized (zero, sign, exp-Golomb bucket, mantissa) and
18//!   each binary probability is an integer ratio of CDF masses, clamped and fed
19//!   to the range coder (optionally refined by an integer adaptive
20//!   probability map).
21//!
22//! A GPU decoder that reproduces these integer ops reproduces the bits.
23//!
24//! # Training
25//!
26//! Float training minimizes the code length (cross-entropy, in bits) of the
27//! actual quantized residuals under the mixture, by minibatch Adam
28//! ([`tang_optim::Adam`]) with hand-written backprop. `tang-ad`'s scalar tape
29//! records one node per scalar op; at ~10⁹ ops per epoch that is far too slow,
30//! so the trainer differentiates the fixed MLP + mixture likelihood directly
31//! (checked against finite differences in the tests).
32//!
33//! # Modes
34//!
35//! - **Pretrained** ([`LearnedSpec::Pretrained`]): a network trained offline on
36//!   other fields; the stream carries its 64-bit fingerprint, and the decoder
37//!   resolves it from [`register`]ed networks.
38//! - **Overfitted** ([`LearnedSpec::Overfit`]): trained at encode time on the
39//!   file's own residuals and **shipped in the stream** (entropy-coded `Q8`
40//!   weights, counted in the rate) — Cool-chic style.
41
42use crate::abac::{BitModel, Decoder, Encoder, UintModel};
43use crate::bitio::{ByteReader, ByteWriter, DecodeError};
44use crate::coefcode::{visit, Bits, CoefModel, Feat, Pred, NCLASS};
45use crate::wavelet::Pyramid;
46use std::sync::{Arc, OnceLock};
47
48/// Input features.
49pub const NF: usize = 37;
50/// Network outputs: π logit, μ₁, μ₂, log₂ s₁, log₂ s₂.
51pub const NO: usize = 5;
52const Q: i64 = 256; // Q8
53const CDF_BITS: u32 = 24;
54const TOT: u64 = 1 << CDF_BITS;
55const ZMAX: i64 = 16 * 256; // logistic table covers |z| ≤ 16 (Q8)
56
57/// Integer `Q8` log₂(1 + v).
58#[inline]
59pub fn lg_q8(v: u32) -> i32 {
60    let w = v as u64 + 1;
61    let e = w.ilog2();
62    let m = if e >= 8 {
63        (w >> (e - 8)) & 0xFF
64    } else {
65        (w << (8 - e)) & 0xFF
66    };
67    (e as i32) * 256 + m as i32
68}
69
70/// Integer feature vector (`Q8`).
71pub fn feat_vec(f: &Feat) -> [i32; NF] {
72    let slg = |v: i32| {
73        if v < 0 {
74            -lg_q8(v.unsigned_abs())
75        } else {
76            lg_q8(v as u32)
77        }
78    };
79    let mut x = [0i32; NF];
80    for i in 0..9 {
81        x[i] = slg(f.sv[i]);
82    }
83    for i in 9..crate::coefcode::NSV {
84        x[19 + i] = slg(f.sv[i]);
85    }
86    x[9] = lg_q8(f.pnb as u32);
87    x[10] = slg(f.pv);
88    // magnitude activity summaries (redundant with the signed inputs, but
89    // cheap for a small ReLU net to use)
90    let a: u32 = f.nb[..3].iter().map(|&v| v as u32).sum();
91    let b: u32 = f.nb[3..].iter().map(|&v| v as u32).sum();
92    x[11] = lg_q8(a);
93    x[12] = lg_q8(b);
94    x[13] = lg_q8(f.par as u32);
95    x[14] = f.has_par as i32 * 256;
96    x[15 + f.class as usize] = 256;
97    for a in 0..3 {
98        x[15 + NCLASS + a] = ((f.mask >> a) & 1) as i32 * 256;
99    }
100    x
101}
102
103// ------------------------------------------------------------------ integer tables
104
105fn tables() -> &'static (Vec<u32>, Vec<u32>) {
106    static T: OnceLock<(Vec<u32>, Vec<u32>)> = OnceLock::new();
107    T.get_or_init(|| {
108        // e^{-1/256} by a fixed Taylor series (basic IEEE ops only)
109        let x = 1.0f64 / 256.0;
110        let mut term = 1.0f64;
111        let mut em = 1.0f64;
112        for k in 1..20 {
113            term = term * (-x) / k as f64;
114            em += term;
115        }
116        // logistic σ(k/256), k = 0..=ZMAX, Q24, by cumulative products
117        let mut sig = Vec::with_capacity(ZMAX as usize + 1);
118        let mut p = 1.0f64;
119        for _ in 0..=ZMAX {
120            sig.push(((TOT as f64) / (1.0 + p)).round() as u32);
121            p *= em;
122        }
123        // 2^{f/256}, f = 0..=256, Q16, from 8 square roots of 2
124        let mut r = 2.0f64;
125        for _ in 0..8 {
126            r = r.sqrt();
127        }
128        let mut pw = Vec::with_capacity(257);
129        let mut v = 1.0f64;
130        for _ in 0..=256 {
131            pw.push((v * 65536.0).round() as u32);
132            v *= r;
133        }
134        (sig, pw)
135    })
136}
137
138/// `σ(z)` with `z` in Q8, result in Q24.
139#[inline]
140fn sigmoid_q(z: i64) -> u64 {
141    let (sig, _) = tables();
142    let a = z.unsigned_abs().min(ZMAX as u64) as usize;
143    let s = sig[a] as u64;
144    if z >= 0 {
145        s
146    } else {
147        TOT - s
148    }
149}
150
151/// `2^{−ls/256}` in Q16 (saturating), `ls` = log₂ s in Q8.
152#[inline]
153fn inv_scale_q16(ls: i64) -> u64 {
154    let (_, pw) = tables();
155    // 2^{-ls/256} = 2^{-(i+1)} · 2^{(256−f)/256}, ls = 256 i + f
156    let ls = ls.clamp(-20 * 256, 40 * 256);
157    let i = ls.div_euclid(256);
158    let f = ls.rem_euclid(256);
159    let m = pw[(256 - f) as usize] as u64;
160    let sh = i + 1;
161    if sh >= 0 {
162        m >> sh.min(63)
163    } else {
164        (m << (-sh).min(20)).min(1 << 40)
165    }
166}
167
168/// Integer mixture parameters of one coefficient.
169#[derive(Clone, Copy, Debug)]
170struct Mix {
171    pi: u64,
172    mu: [i64; 2],
173    inv: [u64; 2],
174}
175
176impl Mix {
177    fn from_out(o: &[i32; NO]) -> Self {
178        Self {
179            pi: sigmoid_q(o[0] as i64),
180            mu: [o[1] as i64, o[2] as i64],
181            inv: [inv_scale_q16(o[3] as i64), inv_scale_q16(o[4] as i64)],
182        }
183    }
184    /// `P(r ≤ m)` in Q24.
185    #[inline]
186    fn cdf(&self, m: i64) -> u64 {
187        let t = m.saturating_mul(Q).saturating_add(Q / 2);
188        let c = |k: usize| {
189            let d = (t as i128 - self.mu[k] as i128) * self.inv[k] as i128;
190            let z = (d >> 16).clamp(-(ZMAX as i128) - 1, ZMAX as i128 + 1) as i64;
191            sigmoid_q(z)
192        };
193        (self.pi * c(0) + (TOT - self.pi) * c(1)) >> CDF_BITS
194    }
195}
196
197#[inline]
198fn ratio(num: u64, den: u64) -> u32 {
199    if den == 0 {
200        return 1 << 15;
201    }
202    ((num.min(den) as u128 * 65536 / den as u128) as u32).min(65535)
203}
204
205/// Code residual `v` under mixture `mx`: zero flag, sign, exp-Golomb bucket
206/// (unary), mantissa (bisection) — each an integer ratio of CDF masses.
207fn code_mix<B: Bits>(b: &mut B, mx: &Mix, v: i32, apm: &mut Apm, cls: usize) -> i32 {
208    let c0 = mx.cdf(0);
209    let cm1 = mx.cdf(-1);
210    let p_zero = c0.saturating_sub(cm1);
211    b.tag(0);
212    let nz = apm.code(b, v != 0, ratio(TOT - p_zero.min(TOT), TOT), cls * 4);
213    if !nz {
214        return 0;
215    }
216    let neg_mass = cm1;
217    let pos_mass = TOT - c0.min(TOT);
218    b.tag(3);
219    let neg = apm.code(b, v < 0, ratio(neg_mass, neg_mass + pos_mass), cls * 4 + 1);
220    let tail = |m: u64| -> u64 {
221        if m > (1u64 << 40) {
222            return 0;
223        }
224        if neg {
225            mx.cdf(-(m as i64))
226        } else {
227            TOT - mx.cdf(m as i64 - 1).min(TOT)
228        }
229    };
230    let mag = v.unsigned_abs() as u64;
231    let mut k = 0u32;
232    b.tag(1);
233    while k < 30 {
234        let a = tail(1 << k);
235        let bb = tail(1 << (k + 1));
236        if !apm.code(b, mag >= (1 << (k + 1)), ratio(bb, a), cls * 4 + 2) {
237            break;
238        }
239        k += 1;
240    }
241    let (mut lo, mut hi) = (1u64 << k, (1u64 << (k + 1)) - 1);
242    b.tag(2);
243    let mut first = true;
244    while lo < hi {
245        let mid = (lo + hi).div_ceil(2);
246        let end = tail(hi + 1);
247        let up = tail(mid).saturating_sub(end);
248        let all = tail(lo).saturating_sub(end);
249        let p = ratio(up, all);
250        let bit = if first {
251            apm.code(b, mag >= mid, p, cls * 4 + 3)
252        } else {
253            b.bitp(mag >= mid, p)
254        };
255        first = false;
256        if bit {
257            lo = mid;
258        } else {
259            hi = mid - 1;
260        }
261    }
262    if neg {
263        -(lo as i32)
264    } else {
265        lo as i32
266    }
267}
268
269/// Adaptive probability map (secondary estimation): refines a model
270/// probability with an adaptive table over 33 stretch-domain buckets per
271/// (class, decision type), linearly interpolated, averaged with the input.
272/// Integer only.
273#[derive(Clone, Debug)]
274struct Apm {
275    on: bool,
276    t: Vec<BitModel>,
277}
278
279const APM_N: usize = 33;
280
281fn stretch_tab() -> &'static Vec<i32> {
282    // bucket centres p = σ((i−16)/2), Q16, from the Q24 logistic table
283    static S: OnceLock<Vec<i32>> = OnceLock::new();
284    S.get_or_init(|| {
285        (0..APM_N)
286            .map(|i| (sigmoid_q((i as i64 - 16) * 128) >> 8) as i32)
287            .collect()
288    })
289}
290
291impl Apm {
292    fn new(on: bool) -> Self {
293        let st = stretch_tab();
294        let t = (0..(NCLASS + 1) * 4)
295            .flat_map(|_| st.iter().map(|&p| BitModel::new(p as u32)))
296            .collect();
297        Self { on, t }
298    }
299    #[inline]
300    fn code<B: Bits>(&mut self, b: &mut B, bit: bool, p1: u32, ctx: usize) -> bool {
301        if !self.on {
302            return b.bitp(bit, p1);
303        }
304        let st = stretch_tab();
305        let p = p1 as i32;
306        let j = st.partition_point(|&s| s <= p).clamp(1, APM_N - 1) - 1;
307        let (a, c) = (st[j], st[j + 1]);
308        let w = if c > a {
309            ((p - a).clamp(0, c - a) as i64 * 4096 / (c - a) as i64) as u32
310        } else {
311            0
312        };
313        let base = ctx * APM_N;
314        let pa = self.t[base + j].p1();
315        let pc = self.t[base + j + 1].p1();
316        let pr = (pa * (4096 - w) + pc * w) >> 12;
317        let pf = (pr + p1) / 2;
318        let got = b.bitp(bit, pf);
319        let jj = if w < 2048 { j } else { j + 1 };
320        self.t[base + jj].update(got);
321        got
322    }
323}
324
325// ------------------------------------------------------------------ network
326
327/// An integer network (`Q8` weights, `Q16` biases).
328#[derive(Clone, Debug)]
329pub struct Net {
330    /// Layer sizes, `[NF, h…, NO]`.
331    pub sizes: Vec<usize>,
332    /// Q8 weights per layer (row-major out × in).
333    pub wq: Vec<Vec<i32>>,
334    /// Q16 biases per layer.
335    pub bq: Vec<Vec<i32>>,
336    /// Whether coding uses the adaptive probability map.
337    pub apm: bool,
338}
339
340impl Net {
341    /// Integer forward pass (outputs in Q8).
342    pub fn infer(&self, x: &[i32; NF]) -> [i32; NO] {
343        let mut a = [0i64; 256];
344        let mut b = [0i64; 256];
345        for (d, &v) in a.iter_mut().zip(x) {
346            *d = v as i64;
347        }
348        let nl = self.sizes.len() - 1;
349        for l in 0..nl {
350            let (ni, no) = (self.sizes[l], self.sizes[l + 1]);
351            let w = &self.wq[l];
352            let src = &a[..ni];
353            for o in 0..no {
354                let row = &w[o * ni..(o + 1) * ni];
355                let mut acc = self.bq[l][o] as i64;
356                for (&wi, &ai) in row.iter().zip(src) {
357                    acc += wi as i64 * ai;
358                }
359                let v = (acc + 128) >> 8;
360                b[o] = if l + 1 < nl {
361                    v.clamp(0, 1 << 22)
362                } else {
363                    v.clamp(-(1 << 22), 1 << 22)
364                };
365            }
366            std::mem::swap(&mut a, &mut b);
367        }
368        let mut o = [0i32; NO];
369        for k in 0..NO {
370            o[k] = a[k] as i32;
371        }
372        o
373    }
374
375    /// 64-bit FNV-1a fingerprint of the integer model.
376    pub fn fingerprint(&self) -> u64 {
377        self.to_bytes()
378            .iter()
379            .fold(0xcbf2_9ce4_8422_2325u64, |h, &b| {
380                (h ^ b as u64).wrapping_mul(0x100_0000_01b3)
381            })
382    }
383
384    /// Number of parameters.
385    pub fn num_params(&self) -> usize {
386        self.wq.iter().map(Vec::len).sum::<usize>() + self.bq.iter().map(Vec::len).sum::<usize>()
387    }
388
389    fn write_sizes(&self, w: &mut ByteWriter) {
390        w.varint(self.sizes.len() as u64);
391        for &s in &self.sizes {
392            w.varint(s as u64);
393        }
394        w.u8(self.apm as u8);
395    }
396
397    fn read_sizes(r: &mut ByteReader) -> Result<(Vec<usize>, bool), DecodeError> {
398        let nl = r.varint()? as usize;
399        if !(2..=6).contains(&nl) {
400            return Err(DecodeError::Invalid("net layers"));
401        }
402        let mut sizes = Vec::new();
403        for _ in 0..nl {
404            let s = r.varint()? as usize;
405            if s == 0 || s > 256 {
406                return Err(DecodeError::Invalid("net size"));
407            }
408            sizes.push(s);
409        }
410        if sizes[0] != NF || sizes[nl - 1] != NO {
411            return Err(DecodeError::Invalid("net io"));
412        }
413        Ok((sizes, r.u8()? != 0))
414    }
415
416    /// Serialize (plain varints) — the pretrained-model file format.
417    pub fn to_bytes(&self) -> Vec<u8> {
418        let mut w = ByteWriter::new();
419        self.write_sizes(&mut w);
420        for l in 0..self.wq.len() {
421            for &v in self.wq[l].iter().chain(&self.bq[l]) {
422                w.svarint(v as i64);
423            }
424        }
425        w.buf
426    }
427
428    /// Parse [`Net::to_bytes`].
429    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
430        let mut r = ByteReader::new(b);
431        let (sizes, apm) = Self::read_sizes(&mut r)?;
432        let mut wq = Vec::new();
433        let mut bq = Vec::new();
434        for l in 0..sizes.len() - 1 {
435            let mut w = Vec::new();
436            for _ in 0..sizes[l] * sizes[l + 1] {
437                w.push(r.svarint()?.clamp(-(1 << 30), 1 << 30) as i32);
438            }
439            let mut b = Vec::new();
440            for _ in 0..sizes[l + 1] {
441                b.push(r.svarint()?.clamp(-(1 << 30), 1 << 30) as i32);
442            }
443            wq.push(w);
444            bq.push(b);
445        }
446        Ok(Self { sizes, wq, bq, apm })
447    }
448
449    /// Entropy-coded serialization for shipping in a stream.
450    fn write_coded(&self, w: &mut ByteWriter) {
451        self.write_sizes(w);
452        let mut e = Encoder::new();
453        let mut mw = UintModel::default();
454        let mut mb = UintModel::default();
455        for l in 0..self.wq.len() {
456            for &v in &self.wq[l] {
457                mw.encode(&mut e, zz(v as i64));
458            }
459            for &v in &self.bq[l] {
460                mb.encode(&mut e, zz(v as i64));
461            }
462        }
463        w.block(&e.finish());
464    }
465
466    fn read_coded(r: &mut ByteReader) -> Result<Self, DecodeError> {
467        let (sizes, apm) = Self::read_sizes(r)?;
468        let buf = r.block()?;
469        let mut d = Decoder::new(buf);
470        let mut mw = UintModel::default();
471        let mut mb = UintModel::default();
472        let mut wq = Vec::new();
473        let mut bq = Vec::new();
474        for l in 0..sizes.len() - 1 {
475            wq.push(
476                (0..sizes[l] * sizes[l + 1])
477                    .map(|_| unzz(mw.decode(&mut d)).clamp(-(1 << 30), 1 << 30) as i32)
478                    .collect(),
479            );
480            bq.push(
481                (0..sizes[l + 1])
482                    .map(|_| unzz(mb.decode(&mut d)).clamp(-(1 << 30), 1 << 30) as i32)
483                    .collect(),
484            );
485        }
486        Ok(Self { sizes, wq, bq, apm })
487    }
488}
489
490fn zz(v: i64) -> u64 {
491    ((v << 1) ^ (v >> 63)) as u64
492}
493
494fn unzz(u: u64) -> i64 {
495    ((u >> 1) as i64) ^ -((u & 1) as i64)
496}
497
498/// The coefficient model driven by a network.
499pub struct NetModel {
500    net: Arc<Net>,
501    cache: Vec<Option<[i32; NO]>>,
502    apm: Apm,
503}
504
505impl NetModel {
506    /// Wrap a network.
507    pub fn new(net: Arc<Net>) -> Self {
508        let apm = Apm::new(net.apm);
509        Self {
510            net,
511            cache: vec![None; NCLASS * 8 * 2],
512            apm,
513        }
514    }
515}
516
517impl CoefModel for NetModel {
518    fn code<B: Bits>(&mut self, b: &mut B, f: &Feat, v: i32) -> i32 {
519        let quiet = f.sv.iter().all(|&a| a == 0) && f.pm == 0;
520        let o = if quiet {
521            let k = (f.class as usize * 8 + f.mask as usize) * 2 + f.has_par as usize;
522            *self.cache[k].get_or_insert_with(|| self.net.infer(&feat_vec(f)))
523        } else {
524            self.net.infer(&feat_vec(f))
525        };
526        code_mix(b, &Mix::from_out(&o), v, &mut self.apm, f.class as usize)
527    }
528}
529
530// ------------------------------------------------------------------ specs / stream
531
532/// How the learned model is obtained.
533#[derive(Clone, Debug)]
534pub enum LearnedSpec {
535    /// A network trained offline; only its fingerprint goes in the stream.
536    Pretrained(Arc<Net>),
537    /// Train on this file at encode time and ship the weights.
538    Overfit(TrainConfig),
539}
540
541const TAG_PRETRAINED: u8 = 1;
542const TAG_OVERFIT: u8 = 2;
543
544impl LearnedSpec {
545    /// Write the model section (tag + model) and return the model.
546    pub fn prepare(&self, p: &Pyramid, q: &[i32], preds: &[Pred], w: &mut ByteWriter) -> NetModel {
547        match self {
548            LearnedSpec::Pretrained(net) => {
549                w.u8(TAG_PRETRAINED);
550                w.bytes(&net.fingerprint().to_le_bytes());
551                NetModel::new(net.clone())
552            }
553            LearnedSpec::Overfit(cfg) => {
554                let samples = collect(p, q, preds);
555                let net = train(&samples, cfg);
556                w.u8(TAG_OVERFIT);
557                net.write_coded(w);
558                NetModel::new(Arc::new(net))
559            }
560        }
561    }
562}
563
564/// Registry of pretrained networks the decoder resolves by fingerprint.
565fn registry() -> &'static std::sync::Mutex<Vec<Arc<Net>>> {
566    static R: OnceLock<std::sync::Mutex<Vec<Arc<Net>>>> = OnceLock::new();
567    R.get_or_init(|| std::sync::Mutex::new(Vec::new()))
568}
569
570/// Make a pretrained network available to decoders in this process.
571pub fn register(net: Arc<Net>) {
572    let mut r = registry().lock().unwrap();
573    if !r.iter().any(|n| n.fingerprint() == net.fingerprint()) {
574        r.push(net);
575    }
576}
577
578/// A registered pretrained network by fingerprint.
579pub fn lookup(fp: u64) -> Option<Arc<Net>> {
580    registry()
581        .lock()
582        .unwrap()
583        .iter()
584        .find(|n| n.fingerprint() == fp)
585        .cloned()
586}
587
588/// Read the model section for `tag` (after the tag byte).
589pub fn read_model(tag: u8, r: &mut ByteReader) -> Result<NetModel, DecodeError> {
590    match tag {
591        TAG_PRETRAINED => {
592            let fp = u64::from_le_bytes(r.take(8)?.try_into().unwrap());
593            let reg = registry().lock().unwrap();
594            let net = reg
595                .iter()
596                .find(|n| n.fingerprint() == fp)
597                .ok_or(DecodeError::Invalid("pretrained model not registered"))?;
598            Ok(NetModel::new(net.clone()))
599        }
600        TAG_OVERFIT => Ok(NetModel::new(Arc::new(Net::read_coded(r)?))),
601        _ => Err(DecodeError::Invalid("model tag")),
602    }
603}
604
605/// Parse a CLI model argument: `hand`, `overfit[:h1[,h2]]`, or
606/// `pretrained:PATH` (the network is also registered for decoding).
607pub fn parse_model_arg(s: &str) -> crate::wavcodec::ModelChoice {
608    use crate::wavcodec::ModelChoice;
609    if s == "hand" {
610        return ModelChoice::Hand;
611    }
612    if let Some(rest) = s.strip_prefix("overfit") {
613        let mut cfg = TrainConfig::overfit();
614        if let Some(h) = rest.strip_prefix(':') {
615            cfg.hidden = h.split(',').map(|v| v.parse().unwrap()).collect();
616        }
617        return ModelChoice::Learned(LearnedSpec::Overfit(cfg));
618    }
619    if let Some(path) = s.strip_prefix("pretrained:") {
620        let net = Arc::new(
621            Net::from_bytes(&std::fs::read(path).expect("read model")).expect("parse model"),
622        );
623        register(net.clone());
624        return ModelChoice::Learned(LearnedSpec::Pretrained(net));
625    }
626    panic!("unknown model {s}")
627}
628
629// ------------------------------------------------------------------ training
630
631/// One training sample.
632#[derive(Clone, Copy, Debug)]
633pub struct Sample {
634    /// Integer features.
635    pub x: [i32; NF],
636    /// Residual.
637    pub r: i32,
638}
639
640/// Collect `(features, residual)` for every coefficient of `q`, in coding
641/// order (the features the coder will see).
642pub fn collect(p: &Pyramid, q: &[i32], preds: &[Pred]) -> Vec<Sample> {
643    let mut out = Vec::with_capacity(q.len());
644    visit(p, q, preds, |f, r| out.push(Sample { x: feat_vec(f), r }));
645    out
646}
647
648/// Trainer settings.
649#[derive(Clone, Debug)]
650pub struct TrainConfig {
651    /// Hidden layer widths.
652    pub hidden: Vec<usize>,
653    /// Passes.
654    pub epochs: usize,
655    /// Samples per pass (a seeded random subset when fewer than available).
656    pub per_epoch: usize,
657    /// Minibatch size.
658    pub batch: usize,
659    /// Adam learning rate (cosine-decayed to 10%).
660    pub lr: f64,
661    /// Use the adaptive probability map at coding time.
662    pub apm: bool,
663    /// Seed.
664    pub seed: u64,
665}
666
667impl TrainConfig {
668    /// Small network for per-file overfitting (its weights are shipped).
669    pub fn overfit() -> Self {
670        Self {
671            hidden: vec![16],
672            epochs: 4,
673            per_epoch: 200_000,
674            batch: 256,
675            lr: 4e-3,
676            apm: true,
677            seed: 1,
678        }
679    }
680    /// Larger network for offline pretraining.
681    pub fn pretrain() -> Self {
682        Self {
683            hidden: vec![32, 16],
684            epochs: 10,
685            per_epoch: 500_000,
686            batch: 256,
687            lr: 3e-3,
688            apm: true,
689            seed: 7,
690        }
691    }
692}
693
694struct FNet {
695    sizes: Vec<usize>,
696    off: Vec<(usize, usize)>, // per layer: (W offset, b offset)
697    n: usize,
698}
699
700impl FNet {
701    fn new(sizes: Vec<usize>) -> Self {
702        let mut off = Vec::new();
703        let mut n = 0;
704        for l in 0..sizes.len() - 1 {
705            off.push((n, n + sizes[l] * sizes[l + 1]));
706            n += sizes[l] * sizes[l + 1] + sizes[l + 1];
707        }
708        Self { sizes, off, n }
709    }
710    fn forward(&self, th: &[f64], x: &[f64]) -> Vec<Vec<f64>> {
711        let mut acts = vec![x.to_vec()];
712        let nl = self.sizes.len() - 1;
713        for l in 0..nl {
714            let (ni, no) = (self.sizes[l], self.sizes[l + 1]);
715            let (wo, bo) = self.off[l];
716            let a = acts.last().unwrap();
717            let mut out = vec![0.0; no];
718            for o in 0..no {
719                let row = &th[wo + o * ni..wo + (o + 1) * ni];
720                let mut s = th[bo + o];
721                for i in 0..ni {
722                    s += row[i] * a[i];
723                }
724                out[o] = if l + 1 < nl { s.max(0.0) } else { s };
725            }
726            acts.push(out);
727        }
728        acts
729    }
730    fn backward(&self, th: &[f64], acts: &[Vec<f64>], gout: &[f64], g: &mut [f64]) {
731        let nl = self.sizes.len() - 1;
732        let mut delta = gout.to_vec();
733        for l in (0..nl).rev() {
734            let (ni, no) = (self.sizes[l], self.sizes[l + 1]);
735            let (wo, bo) = self.off[l];
736            let a = &acts[l];
737            let mut dprev = vec![0.0; ni];
738            for o in 0..no {
739                let d = delta[o];
740                if d == 0.0 {
741                    continue;
742                }
743                g[bo + o] += d;
744                let row = wo + o * ni;
745                for i in 0..ni {
746                    g[row + i] += d * a[i];
747                    dprev[i] += d * th[row + i];
748                }
749            }
750            if l > 0 {
751                for i in 0..ni {
752                    if acts[l][i] <= 0.0 {
753                        dprev[i] = 0.0;
754                    }
755                }
756            }
757            delta = dprev;
758        }
759    }
760}
761
762#[inline]
763fn sigm(z: f64) -> f64 {
764    1.0 / (1.0 + (-z).exp())
765}
766
767/// Code length (nats) of residual `r` under float outputs `o`, and dL/do.
768fn nll_grad(o: &[f64], r: i32) -> (f64, [f64; NO]) {
769    let pi = sigm(o[0]);
770    let mut d = [0.0; 2];
771    let mut dmu = [0.0; 2];
772    let mut dls = [0.0; 2];
773    for c in 0..2 {
774        let mu = o[1 + c];
775        let ls = o[3 + c].clamp(-20.0, 40.0);
776        let s = ls.exp2();
777        let (za, zb) = ((r as f64 + 0.5 - mu) / s, (r as f64 - 0.5 - mu) / s);
778        let (a, b) = (sigm(za), sigm(zb));
779        d[c] = a - b;
780        let (ga, gb) = (a * (1.0 - a), b * (1.0 - b));
781        dmu[c] = -(ga - gb) / s;
782        dls[c] = -(ga * za - gb * zb) * std::f64::consts::LN_2;
783    }
784    let p = (pi * d[0] + (1.0 - pi) * d[1]).max(1e-12);
785    let g = [
786        -(d[0] - d[1]) * pi * (1.0 - pi) / p,
787        -pi * dmu[0] / p,
788        -(1.0 - pi) * dmu[1] / p,
789        -pi * dls[0] / p,
790        -(1.0 - pi) * dls[1] / p,
791    ];
792    (-p.ln(), g)
793}
794
795fn lcg(s: &mut u64) -> u64 {
796    *s = s
797        .wrapping_mul(6364136223846793005)
798        .wrapping_add(1442695040888963407);
799    *s >> 11
800}
801
802/// Train a network on samples (deterministic for a given seed); returns the
803/// quantized integer network.
804pub fn train(samples: &[Sample], cfg: &TrainConfig) -> Net {
805    let mut sizes = vec![NF];
806    sizes.extend(&cfg.hidden);
807    sizes.push(NO);
808    let fnet = FNet::new(sizes.clone());
809    let mut seed = cfg.seed;
810    let mut th = vec![0.0f64; fnet.n];
811    for l in 0..sizes.len() - 1 {
812        let (wo, bo) = fnet.off[l];
813        let scale = (6.0 / sizes[l] as f64).sqrt() * 0.5;
814        for v in &mut th[wo..bo] {
815            *v = ((lcg(&mut seed) % 2_000_001) as f64 / 1_000_000.0 - 1.0) * scale;
816        }
817    }
818    let n = samples.len();
819    let mean_abs = samples
820        .iter()
821        .map(|s| s.r.unsigned_abs() as f64)
822        .sum::<f64>()
823        / n.max(1) as f64;
824    let (_, bo) = fnet.off[sizes.len() - 2];
825    th[bo + 3] = -1.5;
826    th[bo + 4] = (mean_abs + 0.5).log2();
827    if n == 0 {
828        return quantize_net(&fnet, &th, cfg.apm);
829    }
830    let mut adam = tang_optim::Adam::new(cfg.lr);
831    let mut params = tang_la::DVec::from_vec(th);
832    let mut idx: Vec<usize> = (0..n).collect();
833    let m = cfg.per_epoch.min(n);
834    let steps_total = cfg.epochs * m.div_ceil(cfg.batch);
835    let mut step = 0usize;
836    for _ in 0..cfg.epochs {
837        for i in (1..n).rev() {
838            let j = (lcg(&mut seed) % (i as u64 + 1)) as usize;
839            idx.swap(i, j);
840        }
841        for chunk in idx[..m].chunks(cfg.batch) {
842            let th = params.as_slice().to_vec();
843            let mut g = vec![0.0; fnet.n];
844            for &i in chunk {
845                let x = samples[i].x.map(|v| v as f64 / 256.0);
846                let acts = fnet.forward(&th, &x);
847                let (_, go) = nll_grad(acts.last().unwrap(), samples[i].r);
848                fnet.backward(&th, &acts, &go, &mut g);
849            }
850            let inv = 1.0 / chunk.len() as f64;
851            for v in g.iter_mut() {
852                *v *= inv;
853            }
854            let t = step as f64 / steps_total.max(1) as f64;
855            adam.lr = cfg.lr * (0.1 + 0.45 * (1.0 + (std::f64::consts::PI * t).cos()));
856            adam.step(&mut params, &tang_la::DVec::from_vec(g));
857            step += 1;
858        }
859    }
860    quantize_net(&fnet, params.as_slice(), cfg.apm)
861}
862
863fn quantize_net(f: &FNet, th: &[f64], apm: bool) -> Net {
864    let mut wq = Vec::new();
865    let mut bq = Vec::new();
866    for l in 0..f.sizes.len() - 1 {
867        let (wo, bo) = f.off[l];
868        let no = f.sizes[l + 1];
869        wq.push(
870            th[wo..bo]
871                .iter()
872                .map(|v| (v * 256.0).round().clamp(-1e9, 1e9) as i32)
873                .collect(),
874        );
875        bq.push(
876            th[bo..bo + no]
877                .iter()
878                .map(|v| (v * 65536.0).round().clamp(-1e9, 1e9) as i32)
879                .collect(),
880        );
881    }
882    Net {
883        sizes: f.sizes.clone(),
884        wq,
885        bq,
886        apm,
887    }
888}
889
890/// Mean ideal code length (bits/sample) of `samples` under the float view of
891/// an integer net (for reporting; the coder's actual bits include binarization
892/// and probability rounding).
893pub fn mean_bits_float(net: &Net, samples: &[Sample]) -> f64 {
894    let f = FNet::new(net.sizes.clone());
895    let mut th = vec![0.0; f.n];
896    for l in 0..net.sizes.len() - 1 {
897        let (wo, bo) = f.off[l];
898        for (i, &v) in net.wq[l].iter().enumerate() {
899            th[wo + i] = v as f64 / 256.0;
900        }
901        for (i, &v) in net.bq[l].iter().enumerate() {
902            th[bo + i] = v as f64 / 65536.0;
903        }
904    }
905    let s: f64 = samples
906        .iter()
907        .map(|s| {
908            nll_grad(
909                f.forward(&th, &s.x.map(|v| v as f64 / 256.0))
910                    .last()
911                    .unwrap(),
912                s.r,
913            )
914            .0
915        })
916        .sum();
917    s / samples.len().max(1) as f64 / std::f64::consts::LN_2
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923    use crate::coefcode::{code_bands, fit_predictors};
924    use crate::wavelet::{forward_97, MAX_LEVELS};
925
926    #[test]
927    fn lg_is_monotone_and_exact_at_powers() {
928        assert_eq!(lg_q8(0), 0);
929        assert_eq!(lg_q8(1), 256);
930        assert_eq!(lg_q8(3), 512);
931        let mut last = -1;
932        for v in 0..5000 {
933            let l = lg_q8(v);
934            assert!(l >= last);
935            last = l;
936        }
937    }
938
939    #[test]
940    fn tables_are_sane() {
941        assert_eq!(sigmoid_q(0), TOT / 2);
942        assert!(sigmoid_q(ZMAX) > TOT - 4);
943        assert_eq!(inv_scale_q16(0), 65536);
944        assert_eq!(inv_scale_q16(256), 32768);
945        assert_eq!(inv_scale_q16(-256), 131072);
946    }
947
948    #[test]
949    fn cdf_monotone_and_saturates() {
950        let mx = Mix::from_out(&[100, -300, 900, -200, 700]);
951        let mut last = 0;
952        for m in -2000..2000 {
953            let c = mx.cdf(m);
954            assert!(c >= last && c <= TOT);
955            last = c;
956        }
957        assert!(mx.cdf(-1_000_000) < 1000 && mx.cdf(1_000_000) > TOT - 1000);
958    }
959
960    #[test]
961    fn gradient_matches_finite_differences() {
962        let o = [0.3, -0.7, 2.1, 0.4, 1.9];
963        for r in [-5, 0, 1, 7] {
964            let (_, g) = nll_grad(&o, r);
965            for k in 0..NO {
966                let mut a = o;
967                let mut b = o;
968                a[k] += 1e-6;
969                b[k] -= 1e-6;
970                let fd = (nll_grad(&a, r).0 - nll_grad(&b, r).0) / 2e-6;
971                assert!(
972                    (fd - g[k]).abs() < 1e-4 * (1.0 + fd.abs()),
973                    "r={r} k={k}: {fd} vs {}",
974                    g[k]
975                );
976            }
977        }
978    }
979
980    #[test]
981    fn overfit_model_round_trips_bit_exact() {
982        let d = [10, 24, 30];
983        let p = Pyramid::new(d, MAX_LEVELS);
984        let mut x: Vec<f64> = (0..p.len())
985            .map(|i| ((i as f64) * 0.021).sin() * 50.0 + ((i * 7) % 13) as f64)
986            .collect();
987        forward_97(&p, &mut x);
988        let mut q: Vec<i32> = x.iter().map(|v| (v / 0.9).round() as i32).collect();
989        q[3] = 2_000_000; // far tail
990        let preds = fit_predictors(&p, &q);
991        let cfg = TrainConfig {
992            hidden: vec![8],
993            epochs: 2,
994            per_epoch: 4000,
995            batch: 128,
996            lr: 5e-3,
997            apm: true,
998            seed: 3,
999        };
1000        let mut w = ByteWriter::new();
1001        let mut m = LearnedSpec::Overfit(cfg.clone()).prepare(&p, &q, &preds, &mut w);
1002        let mut e = Encoder::new();
1003        let mut qq = q.clone();
1004        code_bands(&p, &mut qq, &preds, &mut e, &mut m);
1005        let bytes = e.finish();
1006        let mut r = ByteReader::new(&w.buf);
1007        let tag = r.u8().unwrap();
1008        let mut m2 = read_model(tag, &mut r).unwrap();
1009        let mut d2 = Decoder::new(&bytes);
1010        let mut out = vec![0i32; q.len()];
1011        code_bands(&p, &mut out, &preds, &mut d2, &mut m2);
1012        assert_eq!(out, q);
1013        let mut w2 = ByteWriter::new();
1014        LearnedSpec::Overfit(cfg).prepare(&p, &q, &preds, &mut w2);
1015        assert_eq!(w.buf, w2.buf, "deterministic training");
1016    }
1017}