Skip to main content

cram_codec/
shipdec.rs

1//! Streams that carry their own decoder (`TCS1`).
2//!
3//! A `TCS1` container is `"TCS1"`, a varint length, a decoder program, then
4//! an ordinary TCG3 stream. The program replaces the receiver's built-in
5//! coding path: the rANS residual decoders ([`crate::irdec`]) and optionally
6//! the learned model's MLP (its weights as graph constants, so the receiver
7//! needs no model registry). The receiver verifies it
8//! ([`tang_expr::int::IntGraph::from_bytes`] plus the role ABIs below),
9//! lowers it to WGSL (bounds-checked loads) and to the CPU interpreter, and
10//! caches the result by content hash. Plain TCG3 streams decode as before.
11//!
12//! Program layout: `"TCDP"`, version byte (1), flags byte (bit 0 static
13//! residual graph, bit 1 learned residual graph, bit 2 MLP graph, bit 3
14//! adaptive learned residual graph), then per present section a varint
15//! length and a `TIR1` graph. A missing residual section falls back to the
16//! built-in graph.
17//!
18//! Role ABIs (anything else is rejected):
19//! - static residual: params `u32 × 8`, buffers `mt`, `stream` (`u32`),
20//!   outputs `u32, u32, i32` (see [`crate::irdec`]);
21//! - learned residual: params `u32 × 3, i32 × 5, u32 × 2`, same buffers and
22//!   outputs;
23//! - MLP: params `i32 × NFL`, no buffers, outputs `i32 × NO`;
24//! - adaptive learned residual (streams with the per-lane APM layer):
25//!   params `u32 × 3, i32 × 5, u32 × 6`, buffers `mt`, `stream` and the
26//!   scratch buffer `adt` (136 words), outputs `u32, u32, i32, u32`.
27
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex, OnceLock};
30
31use crate::bitio::DecodeError;
32use crate::irdec::{BUFS, LEARNED_AD_PARAMS, LEARNED_PARAMS, OUTS, OUTS_AD, RW_AD, STATIC_PARAMS};
33use crate::learned::NO;
34use crate::lmodel3::{Net32, NFL};
35use tang_expr::int::{IId, IType, IntGraph, Limits, Program};
36
37/// Container magic.
38pub const MAGIC: &[u8; 4] = b"TCS1";
39const PMAGIC: &[u8; 4] = b"TCDP";
40/// Largest program accepted (bytes).
41pub const MAX_PROGRAM: usize = 1 << 20;
42
43/// A graph with its outputs.
44pub type Graph = (IntGraph, Vec<IId>);
45
46/// A verified decoder program, ready for both backends.
47pub struct Decoder {
48    /// Static residual graph.
49    pub stat: Graph,
50    /// Learned residual graph.
51    pub learned: Graph,
52    /// Adaptive learned residual graph.
53    pub learned_ad: Graph,
54    /// Shipped MLP (replaces the stream's model).
55    pub mlp: Option<Graph>,
56    /// WGSL for the kernel's RES region (bounds-checked loads).
57    pub wgsl_res: String,
58    /// The RES region for adaptive streams.
59    pub wgsl_res_ad: String,
60    /// WGSL `fn mlp_gen()` when the MLP is shipped.
61    pub wgsl_mlp: Option<String>,
62    /// Content hash of the program bytes.
63    pub key: u64,
64    /// Program size in bytes.
65    pub size: usize,
66}
67
68impl std::fmt::Debug for Decoder {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(
71            f,
72            "Decoder {{ key: {:016x}, size: {}, mlp: {} }}",
73            self.key,
74            self.size,
75            self.mlp.is_some()
76        )
77    }
78}
79
80impl Decoder {
81    /// CPU interpreter for the MLP, if shipped.
82    pub fn mlp_program(&self) -> Option<Program> {
83        self.mlp.as_ref().map(|(g, o)| Program::new(g, o))
84    }
85}
86
87/// FNV-1a 64.
88pub fn fnv1a(b: &[u8]) -> u64 {
89    let mut h = 0xcbf29ce484222325u64;
90    for &x in b {
91        h ^= x as u64;
92        h = h.wrapping_mul(0x100000001b3);
93    }
94    h
95}
96
97fn put(o: &mut Vec<u8>, mut v: u64) {
98    loop {
99        let b = (v & 0x7f) as u8;
100        v >>= 7;
101        if v == 0 {
102            o.push(b);
103            return;
104        }
105        o.push(b | 0x80);
106    }
107}
108
109fn get(b: &[u8], p: &mut usize) -> Result<u64, DecodeError> {
110    let mut v = 0u64;
111    for s in 0..10 {
112        let x = *b.get(*p).ok_or(DecodeError::Truncated)?;
113        *p += 1;
114        v |= ((x & 0x7f) as u64) << (7 * s);
115        if x & 0x80 == 0 {
116            return Ok(v);
117        }
118    }
119    Err(DecodeError::Invalid("varint"))
120}
121
122/// Serialize a decoder program from graphs (`None` sections are left to the
123/// receiver's built-ins).
124pub fn program_bytes(
125    stat: Option<&Graph>,
126    learned: Option<&Graph>,
127    mlp: Option<&Graph>,
128    learned_ad: Option<&Graph>,
129) -> Vec<u8> {
130    let mut o = PMAGIC.to_vec();
131    o.push(1);
132    let secs = [stat, learned, mlp, learned_ad];
133    o.push(
134        secs.iter()
135            .enumerate()
136            .map(|(i, s)| (s.is_some() as u8) << i)
137            .sum(),
138    );
139    for (g, outs) in secs.into_iter().flatten() {
140        let b = g.to_bytes(outs);
141        put(&mut o, b.len() as u64);
142        o.extend(b);
143    }
144    o
145}
146
147/// The built-in decoder as a program: the static residual graph, the
148/// learned one (adaptive when `ad`, for streams with the APM layer), plus
149/// `net`'s MLP when given.
150pub fn builtin_program(net: Option<&Net32>, ad: bool) -> Vec<u8> {
151    let mlp = net.map(|n| n.graph());
152    let (l, la) = if ad {
153        (None, Some(crate::irdec::learned_ad_graph()))
154    } else {
155        (Some(crate::irdec::learned_graph()), None)
156    };
157    program_bytes(
158        Some(&crate::irdec::static_graph()),
159        l.as_ref(),
160        mlp.as_ref(),
161        la.as_ref(),
162    )
163}
164
165/// Wrap a TCG3 stream with a decoder program.
166pub fn wrap(stream: &[u8], program: &[u8]) -> Vec<u8> {
167    let mut o = MAGIC.to_vec();
168    put(&mut o, program.len() as u64);
169    o.extend_from_slice(program);
170    // the stream starts on a 16-bit word boundary (lane offsets are words)
171    if o.len() % 2 == 1 {
172        o.push(0);
173    }
174    o.extend_from_slice(stream);
175    o
176}
177
178fn res_limits() -> Limits {
179    Limits {
180        max_nodes: 1 << 14,
181        max_loops: 16,
182        max_depth: 2,
183        max_trip: 64,
184        max_cost: 1 << 16,
185    }
186}
187
188fn mlp_limits() -> Limits {
189    Limits {
190        max_nodes: 1 << 16,
191        max_loops: 0,
192        max_depth: 0,
193        max_trip: 0,
194        max_cost: 1 << 16,
195    }
196}
197
198/// Verify and lower a program (no cache).
199pub fn compile(prog: &[u8]) -> Result<Decoder, DecodeError> {
200    let bad = |_| DecodeError::Invalid("decoder program rejected by the verifier");
201    if prog.len() > MAX_PROGRAM {
202        return Err(DecodeError::Invalid("decoder program too large"));
203    }
204    if prog.len() < 6 || &prog[..4] != PMAGIC || prog[4] != 1 {
205        return Err(DecodeError::Invalid("decoder program header"));
206    }
207    let flags = prog[5];
208    if flags & !15 != 0 {
209        return Err(DecodeError::Invalid("decoder program flags"));
210    }
211    let mut p = 6;
212    let sec = |p: &mut usize| -> Result<&[u8], DecodeError> {
213        let n = get(prog, p)? as usize;
214        let s = prog
215            .get(*p..p.checked_add(n).ok_or(DecodeError::Truncated)?)
216            .ok_or(DecodeError::Truncated)?;
217        *p += n;
218        Ok(s)
219    };
220    let stat = match flags & 1 {
221        0 => crate::irdec::static_graph(),
222        _ => IntGraph::from_bytes(sec(&mut p)?, &STATIC_PARAMS, &BUFS, &OUTS, &res_limits())
223            .map_err(bad)?,
224    };
225    let learned = match flags & 2 {
226        0 => crate::irdec::learned_graph(),
227        _ => IntGraph::from_bytes(sec(&mut p)?, &LEARNED_PARAMS, &BUFS, &OUTS, &res_limits())
228            .map_err(bad)?,
229    };
230    let mlp = match flags & 4 {
231        0 => None,
232        _ => Some(
233            IntGraph::from_bytes(
234                sec(&mut p)?,
235                &[IType::I32; NFL],
236                &[],
237                &[IType::I32; NO],
238                &mlp_limits(),
239            )
240            .map_err(bad)?,
241        ),
242    };
243    let learned_ad = match flags & 8 {
244        0 => crate::irdec::learned_ad_graph(),
245        _ => IntGraph::from_bytes_rw(
246            sec(&mut p)?,
247            &LEARNED_AD_PARAMS,
248            &BUFS,
249            &RW_AD,
250            &OUTS_AD,
251            &res_limits(),
252        )
253        .map_err(bad)?,
254    };
255    if p != prog.len() {
256        return Err(DecodeError::Invalid("decoder program trailing bytes"));
257    }
258    let wgsl_res = crate::irdec::wgsl((&stat.0, &stat.1), (&learned.0, &learned.1), true);
259    let wgsl_res_ad = crate::irdec::wgsl((&stat.0, &stat.1), (&learned_ad.0, &learned_ad.1), true);
260    let wgsl_mlp = mlp.as_ref().map(|(g, o)| {
261        let params: Vec<String> = (0..NFL).map(|i| format!("na[{i}]")).collect();
262        let dests: Vec<String> = (0..o.len()).map(|k| format!("na[{k}]")).collect();
263        let pr: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
264        let dr: Vec<&str> = dests.iter().map(|s| s.as_str()).collect();
265        format!(
266            "fn mlp_gen() {{\n{}}}\n",
267            g.to_wgsl_body_guarded(o, &pr, &dr, 1)
268        )
269    });
270    Ok(Decoder {
271        stat,
272        learned,
273        learned_ad,
274        mlp,
275        wgsl_res,
276        wgsl_res_ad,
277        wgsl_mlp,
278        key: fnv1a(prog),
279        size: prog.len(),
280    })
281}
282
283type Cache = Mutex<HashMap<u64, Vec<(Vec<u8>, Arc<Decoder>)>>>;
284
285fn cache() -> &'static Cache {
286    static C: OnceLock<Cache> = OnceLock::new();
287    C.get_or_init(Default::default)
288}
289
290/// [`compile`] through the process-wide cache (keyed by FNV-1a 64 of the
291/// program, confirmed by a full byte comparison, so a hash collision can
292/// never select another program).
293pub fn compile_cached(prog: &[u8]) -> Result<Arc<Decoder>, DecodeError> {
294    let k = fnv1a(prog);
295    if let Some(v) = cache().lock().unwrap().get(&k) {
296        if let Some((_, d)) = v.iter().find(|(b, _)| b == prog) {
297            return Ok(d.clone());
298        }
299    }
300    let d = Arc::new(compile(prog)?);
301    let mut c = cache().lock().unwrap();
302    let v = c.entry(k).or_default();
303    if v.len() < 16 {
304        v.push((prog.to_vec(), d.clone()));
305    }
306    Ok(d)
307}
308
309/// Split a stream: `(Some(decoder), inner TCG3)` for `TCS1`, `(None, bytes)`
310/// otherwise.
311pub fn open(bytes: &[u8]) -> Result<(Option<Arc<Decoder>>, &[u8]), DecodeError> {
312    if bytes.len() < 4 || &bytes[..4] != MAGIC {
313        return Ok((None, bytes));
314    }
315    let mut p = 4;
316    let n = get(bytes, &mut p)? as usize;
317    if n > MAX_PROGRAM {
318        return Err(DecodeError::Invalid("decoder program too large"));
319    }
320    let prog = bytes.get(p..p + n).ok_or(DecodeError::Truncated)?;
321    let start = (p + n).next_multiple_of(2);
322    let inner = bytes.get(start..).ok_or(DecodeError::Truncated)?;
323    Ok((Some(compile_cached(prog)?), inner))
324}
325
326/// Placeholder net for a stream whose model is shipped as a graph (its
327/// weights are never used; the shipped MLP runs instead).
328pub fn placeholder_net() -> Net32 {
329    Net32 {
330        sizes: vec![NFL, NO],
331        w: vec![vec![0; NFL * NO]],
332        b: vec![vec![0; NO]],
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::gpuwav3::{decode3_box, encode3_f32, parse3, Gpu3Config, Model3};
340    use crate::ErrorBound;
341
342    fn field(d: [usize; 3]) -> Vec<f32> {
343        let mut v = Vec::with_capacity(d.iter().product());
344        for z in 0..d[0] {
345            for y in 0..d[1] {
346                for x in 0..d[2] {
347                    let (fz, fy, fx) = (z as f32 * 0.21, y as f32 * 0.13, x as f32 * 0.17);
348                    v.push(
349                        (fz.sin() * fy.cos()
350                            + (fx * 1.3).sin() * 0.5
351                            + (fx * fy * 0.05).cos() * 0.25)
352                            * 3.0,
353                    );
354                }
355            }
356        }
357        v
358    }
359
360    fn streams() -> Vec<(String, Vec<u8>)> {
361        let d = [20, 37, 45];
362        let x = field(d);
363        let mut out = Vec::new();
364        for (lanes, bound) in [
365            (1, ErrorBound::Psnr(60.0)),
366            (8, ErrorBound::Psnr(100.0)),
367            (8, ErrorBound::MaxAbs(1e-4)),
368        ] {
369            let cfg = Gpu3Config {
370                block: 16,
371                lanes,
372                tile: 16,
373                ..Gpu3Config::new(bound)
374            };
375            out.push((
376                format!("static lanes {lanes} {bound:?}"),
377                encode3_f32(&x, &d, &cfg, 2).0,
378            ));
379            let model = Model3::Overfit {
380                hidden: vec![8],
381                per_epoch: 20_000,
382                epochs: 2,
383            };
384            let cfg = Gpu3Config {
385                block: 16,
386                lanes,
387                tile: 16,
388                model,
389                adapt: None,
390                ..Gpu3Config::new(bound)
391            };
392            out.push((
393                format!("learned lanes {lanes} {bound:?}"),
394                encode3_f32(&x, &d, &cfg, 2).0,
395            ));
396            let model = Model3::Overfit {
397                hidden: vec![8],
398                per_epoch: 20_000,
399                epochs: 2,
400            };
401            let cfg = Gpu3Config {
402                block: 16,
403                lanes,
404                tile: 16,
405                model,
406                ..Gpu3Config::new(bound)
407            };
408            out.push((
409                format!("adaptive lanes {lanes} {bound:?}"),
410                encode3_f32(&x, &d, &cfg, 2).0,
411            ));
412        }
413        out
414    }
415
416    fn shipped(s: &[u8], with_mlp: bool) -> Vec<u8> {
417        let ps = parse3(s).unwrap();
418        let net = if with_mlp { ps.net.as_ref() } else { None };
419        wrap(s, &builtin_program(net, ps.net.is_some() && ps.ad.wa > 0))
420    }
421
422    /// Adaptive (per-lane APM) streams decode through the IR graphs, with the
423    /// adaptive role shipped or (older programs without it) built in.
424    #[test]
425    fn adaptive_streams_decode_through_the_ir() {
426        let d = [20, 37, 45];
427        let x = field(d);
428        let model = Model3::Overfit {
429            hidden: vec![8],
430            per_epoch: 20_000,
431            epochs: 2,
432        };
433        let cfg = Gpu3Config {
434            block: 16,
435            lanes: 8,
436            tile: 16,
437            model,
438            ..Gpu3Config::new(ErrorBound::Psnr(80.0))
439        };
440        let s = encode3_f32(&x, &d, &cfg, 2).0;
441        let ps = parse3(&s).unwrap();
442        assert!(ps.ad.wa > 0);
443        let want = decode3_box(&s, [0; 3], d, 2).unwrap().bits;
444        for (mlp, ad) in [(false, true), (true, true), (false, false), (true, false)] {
445            let w = wrap(
446                &s,
447                &builtin_program(if mlp { ps.net.as_ref() } else { None }, ad),
448            );
449            assert!(parse3(&w).unwrap().dec.is_some());
450            assert!(
451                decode3_box(&w, [0; 3], d, 2).unwrap().bits == want,
452                "mlp {mlp} ad {ad}"
453            );
454        }
455    }
456
457    #[test]
458    fn shipped_decoder_matches_builtin_on_cpu() {
459        for (name, s) in streams() {
460            let want = decode3_box(&s, [0; 3], [20, 37, 45], 2).unwrap().bits;
461            for mlp in [false, true] {
462                let w = shipped(&s, mlp);
463                let ps = parse3(&w).unwrap();
464                assert!(ps.dec.is_some());
465                let got = decode3_box(&w, [0; 3], [20, 37, 45], 2).unwrap().bits;
466                assert!(got == want, "{name} mlp {mlp}");
467                // random access too
468                let sub = decode3_box(&w, [3, 5, 7], [11, 30, 20], 1).unwrap();
469                let base = decode3_box(&s, [3, 5, 7], [11, 30, 20], 1).unwrap();
470                assert!(sub.bits == base.bits, "{name} box");
471            }
472        }
473    }
474
475    #[cfg(feature = "gpu")]
476    #[test]
477    fn shipped_decoder_matches_builtin_on_gpu() {
478        let Ok(g) = crate::gpudec::GpuDecoder::new(8, 128) else {
479            eprintln!("no GPU adapter; skipping");
480            return;
481        };
482        for (name, s) in streams() {
483            let want = decode3_box(&s, [0; 3], [20, 37, 45], 1).unwrap().bits;
484            assert!(g.decode3(&s).unwrap().0 == want, "{name} builtin gpu");
485            for mlp in [false, true] {
486                let w = shipped(&s, mlp);
487                assert!(
488                    g.decode3(&w).unwrap().0 == want,
489                    "{name} shipped gpu mlp {mlp}"
490                );
491            }
492        }
493    }
494
495    /// A pretrained-model stream (only the fingerprint inside) decodes with no
496    /// registered model once the MLP ships as a graph.
497    #[test]
498    fn shipped_mlp_needs_no_registry() {
499        let path = std::env::var("TC_TEST_TCM").unwrap_or_else(|_| {
500            "/Users/cam/Developer/cram-codec/bench/models/pre3v4_16.tcm".into()
501        });
502        let Ok(tcm) = std::fs::read(&path) else {
503            eprintln!("no {path}; skipping");
504            return;
505        };
506        let net = Arc::new(crate::learned::Net::from_bytes(&tcm).unwrap());
507        let d = [20, 37, 45];
508        let cfg = Gpu3Config {
509            block: 16,
510            lanes: 8,
511            tile: 16,
512            model: Model3::Pretrained(net.clone()),
513            adapt: None,
514            ..Gpu3Config::new(ErrorBound::Psnr(80.0))
515        };
516        let (s, _) = encode3_f32(&field(d), &d, &cfg, 2);
517        assert!(parse3(&s).is_err(), "model must not be registered yet");
518        let prog = builtin_program(Some(&Net32::new(&net)), false);
519        let w = wrap(&s, &prog);
520        let got = decode3_box(&w, [0; 3], d, 2).unwrap().bits;
521        #[cfg(feature = "gpu")]
522        let gpu = crate::gpudec::GpuDecoder::new(8, 64)
523            .ok()
524            .map(|g| g.decode3(&w).unwrap().0);
525        crate::learned::register(net);
526        let want = decode3_box(&s, [0; 3], d, 2).unwrap().bits;
527        assert!(got == want);
528        #[cfg(feature = "gpu")]
529        if let Some(gv) = gpu {
530            assert!(gv == want, "gpu, shipped MLP");
531        }
532    }
533
534    #[test]
535    fn corrupt_programs_are_rejected_not_run() {
536        let s = &streams()[1].1;
537        let prog = builtin_program(parse3(s).unwrap().net.as_ref(), false);
538        let mut seed = 5u64;
539        let mut rnd = || {
540            seed = seed
541                .wrapping_mul(6364136223846793005)
542                .wrapping_add(1442695040888963407);
543            (seed >> 33) as usize
544        };
545        let (mut ok, mut bad) = (0, 0);
546        for _ in 0..300 {
547            let mut p = prog.clone();
548            let i = 6 + rnd() % (p.len() - 6);
549            p[i] = rnd() as u8;
550            match compile(&p) {
551                // a mutated but well-formed program is total: decoding returns (any bits or an error)
552                Ok(_) => {
553                    ok += 1;
554                    let _ = decode3_box(&wrap(s, &p), [0; 3], [20, 37, 45], 1);
555                }
556                Err(_) => bad += 1,
557            }
558        }
559        assert!(bad > 0);
560        eprintln!("mutations: {ok} accepted, {bad} rejected");
561        // truncations
562        for n in [0, 3, 6, 7, prog.len() / 2, prog.len() - 1] {
563            assert!(compile(&prog[..n]).is_err());
564        }
565        // an uncapped loop cannot be expressed on the wire; a huge cap is refused
566        let spin = |cap: u32| {
567            let mut g = IntGraph::new();
568            for (n, e) in BUFS {
569                g.buffer(n, e);
570            }
571            let x = g.param(0, IType::U32);
572            let out = g.while_loop(&[x], |g, s| {
573                let t = g.bool(true);
574                let one = g.u32(1);
575                (t, vec![g.add(s[0], one)])
576            });
577            g.cap_loop(out[0], cap);
578            let z = g.i32(0);
579            program_bytes(Some(&(g, vec![out[0], x, z])), None, None, None)
580        };
581        assert!(compile(&spin(1 << 20)).is_err());
582        // within the caps an infinite loop just stops (and garbage decodes to an error or garbage)
583        let d = compile(&spin(64)).unwrap();
584        assert!(d.wgsl_res.contains("if c0 >= 64u"));
585        let _ = decode3_box(&wrap(s, &spin(64)), [0; 3], [20, 37, 45], 1);
586    }
587}