Skip to main content

logicaffeine_compile/spatial/decode_engine/
program.rs

1//! The decode engine's program: one step of a checkpoint as the commands its units run, in order.
2//!
3//! A step holds its token-rows — every row of every sequence the step advances — in the token-row store, each row a
4//! vector in every [`Slot`]. A command is one unit's job over every row of the step, in the step's row order, finished
5//! before the next command starts: [`Command::Matvec`] streams one matrix through the array and the block-scale lanes
6//! once for all the rows; the norms, the gates and the products are the vector unit's micro-programs;
7//! [`Command::Conv`] and [`Command::Delta`] carry a Gated-DeltaNet block's ring and memory, and
8//! [`Command::CacheAppend`] and [`Command::Attend`] an attention block's cache, each over every row's own sequence. A
9//! command that carries a sequence's state takes its rows in order, so a sequence's rows see one another as a prompt
10//! run a token at a time does.
11//!
12//! [`lower`] reads the step off the model's one program — the decode step [`author`](crate::model::author) writes,
13//! lifted into the one IR, fused by the shared table under the engine's shim and read block by block
14//! ([`super::lower_graph`]) — and [`super::model::EngineModel`] runs exactly that list: the program is the model, so
15//! what the sequencer issues and the cycle twin prices is what the model computes. [`hand_lowering`] writes the same
16//! commands from the spec directly; it is kept only as the oracle the step-graph reading is held to.
17
18use std::collections::HashMap;
19use std::fmt;
20use std::sync::{Arc, Mutex, OnceLock};
21
22use crate::model::author::NotYet;
23use crate::model::program::{Cut, Identity};
24use crate::model::spec::{Attention, GatedDelta, Mixer, ModelSpec, Rope};
25use crate::tensor_graph::fuse::optimize;
26use crate::tensor_graph::graph::{GraphError, Storage, TensorGraph};
27
28use super::lower_graph::{lower_graph, LowerError};
29use super::shim::EngineShim;
30use super::model::{CacheFormat, EngineNumerics};
31
32/// A vector each token-row of a step carries, one of the token-row store's slots.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
34pub enum Slot {
35    /// The residual stream.
36    Residual,
37    /// The residual normalized, a block's input.
38    Normed,
39    /// A block's output, before it joins the residual.
40    Out,
41    /// A Gated-DeltaNet block's fused query, key and value projection.
42    Qkv,
43    /// The projection through the causal convolution and SiLU.
44    Mixed,
45    /// The block's output gate, before its SiLU.
46    Z,
47    /// The projections the decay and the update gate are made from, one element a value head.
48    Alpha,
49    Beta,
50    /// Per value head: the decay's exponent, the decay, the update gate.
51    LogDecay,
52    Decay,
53    Gate,
54    /// Queries and keys, a head after another: a Gated-DeltaNet block's L2-normalized, an attention block's
55    /// normalized and turned.
56    Query,
57    Key,
58    /// An attention block's values, a head after another.
59    Value,
60    /// An attention block's query projection, each head's queries and output gate interleaved.
61    Projected,
62    /// An attention block's output gate, a head after another.
63    OutGate,
64    /// The read of every head: the memory's read-out, or the attended values.
65    Read,
66    /// The FFN's gate and up projections, and their gated product.
67    FfnGate,
68    FfnUp,
69    Hidden,
70    /// The vocabulary's logits.
71    Logits,
72}
73
74/// One command of a step.
75#[derive(Clone, Debug, PartialEq)]
76pub enum Command {
77    /// Each row's [`Slot::Residual`] starts as its token's row of the embedding table.
78    Embed { table: String },
79    /// RMSNorm of each row of `from` by the weights `weight` into `to`.
80    Norm { weight: String, from: Slot, to: Slot },
81    /// The matrix `tensor` times each row of `from`, into `to`.
82    Matvec { tensor: String, from: Slot, to: Slot },
83    /// Each row of `from` added into `to`, element by element.
84    Add { from: Slot, to: Slot },
85    /// `silu(gate)·up` into `to`, element by element.
86    SwiGlu { gate: Slot, up: Slot, to: Slot },
87    /// Layer `layer`'s causal convolution through SiLU of each row of `from` into `to`, against each row's sequence's
88    /// ring of the rows before it; the row as the state stores it then joins the ring.
89    Conv { layer: usize, kernel: String, taps: usize, from: Slot, to: Slot },
90    /// `heads` heads of `width` elements from element `at` of each row of `from`, each L2-normalized, into `to`.
91    L2Heads { from: Slot, at: usize, heads: usize, width: usize, to: Slot },
92    /// Per value head, from each row's `alpha` and `beta`: the decay's exponent `softplus(α + dt)·a`, the decay, the
93    /// update gate `σ(β)`.
94    Gates { alpha: Slot, beta: Slot, dt: String, a: String, log_decay: Slot, decay: Slot, gate: Slot },
95    /// Layer `layer`'s memory through each row, a value head a memory: its query and key group's `query` and `key`,
96    /// its values from element `value_at` of `value`, its decay and gate; each value head's read-out into `to`.
97    Delta {
98        layer: usize,
99        query: Slot,
100        key: Slot,
101        value: Slot,
102        value_at: usize,
103        log_decay: Slot,
104        decay: Slot,
105        gate: Slot,
106        k_heads: usize,
107        v_heads: usize,
108        width: usize,
109        to: Slot,
110    },
111    /// Each head of `width` elements of `from` RMS-normed by `weight` and gated by `silu(z)`, into `to`.
112    GatedNorm { weight: String, from: Slot, z: Slot, width: usize, to: Slot },
113    /// Each row of `from`, `heads` heads of `width` queries each followed by its `width` gates, split into `query`
114    /// and `gate`.
115    Split { from: Slot, heads: usize, width: usize, query: Slot, gate: Slot },
116    /// Each head of `width` elements of `slot` RMS-normed by `weight`, in place.
117    RmsHeads { weight: String, slot: Slot, width: usize },
118    /// Each head of `width` elements of `slot` turned by its row's position ([`super::numerics::rope`]).
119    Rotate { slot: Slot, width: usize, rope: Rope },
120    /// Each head of `width` elements of `slot` rotated by the Walsh–Hadamard transform, in place.
121    Hadamard { slot: Slot, width: usize },
122    /// Each row's keys and values, `kv_heads` heads each, appended to layer `layer`'s cache of the row's sequence in
123    /// the cache's format.
124    CacheAppend { layer: usize, key: Slot, value: Slot, kv_heads: usize, width: usize, value_width: usize },
125    /// Layer `layer`'s attention of each row's `heads` query heads over its sequence's cache to the row's own
126    /// position, the heads in groups over the `kv_heads` cached heads, scores scaled by `scale`; each head's attended
127    /// values into `to`, rotated back when the cache is.
128    Attend { layer: usize, query: Slot, heads: usize, kv_heads: usize, width: usize, value_width: usize, scale: f32, to: Slot },
129    /// Each element of `slot` times `σ(gate)`, in place.
130    SigmoidGate { slot: Slot, gate: Slot },
131    /// The positions of every sequence the step advances move past its rows.
132    Advance,
133}
134
135/// One step of a checkpoint at a knob point.
136#[derive(Clone, Debug, PartialEq)]
137pub struct Program {
138    pub numerics: EngineNumerics,
139    pub commands: Vec<Command>,
140}
141
142/// Why the engine has no program for a model: the author cannot write its step, the front end cannot lift it, or its
143/// step graph holds a block the engine has no unit for.
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub enum ProgramError {
146    NotYet(NotYet),
147    Lift(String),
148    Graph(GraphError),
149    Lower(LowerError),
150}
151
152impl fmt::Display for ProgramError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            ProgramError::NotYet(e) => write!(f, "{e}"),
156            ProgramError::Lift(e) => write!(f, "the front end does not lift the step: {e}"),
157            ProgramError::Graph(e) => write!(f, "{e}"),
158            ProgramError::Lower(e) => write!(f, "{e}"),
159        }
160    }
161}
162
163impl std::error::Error for ProgramError {}
164
165/// A program and the cut of the model's one program it was read off.
166#[derive(Clone, Debug, PartialEq)]
167pub struct Lowered {
168    pub program: Program,
169    pub identity: Identity,
170}
171
172/// The engine's step of `spec` at `numerics`, read off the decode step the author writes over a cache of `ctx`
173/// positions: the model's one program, lifted by the shared front end into its step graph and read block by block.
174/// The commands do not depend on `ctx` (the cache's length is the layout's); the identity records the cut read.
175pub fn lower(spec: &ModelSpec, numerics: EngineNumerics, ctx: usize) -> Result<Lowered, ProgramError> {
176    let (graph, identity) = lifted(spec, numerics, ctx)?;
177    let program = lower_graph(&graph, spec, numerics).map_err(ProgramError::Lower)?;
178    Ok(Lowered { program, identity })
179}
180
181/// The step graph of `spec`'s decode step over a cache of `ctx` positions, and its identity. The graph is the same at
182/// every knob point, so it is lifted once a process for each spec and cache length — keyed by the spec's whole
183/// contents, never a summary of them — and every point reads the one lift.
184fn lifted(spec: &ModelSpec, numerics: EngineNumerics, ctx: usize) -> Result<(Arc<TensorGraph>, Identity), ProgramError> {
185    static LIFTED: OnceLock<Mutex<HashMap<(String, usize), (Arc<TensorGraph>, Identity)>>> = OnceLock::new();
186    let key = (format!("{spec:?}"), ctx);
187    let cache = LIFTED.get_or_init(Default::default);
188    if let Some((graph, identity)) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
189        return Ok((Arc::clone(graph), *identity));
190    }
191    crate::model::author::check(spec).map_err(ProgramError::NotYet)?;
192    let lift = TensorGraph::of_cut(spec, Cut::Decode { ctx }, None, &|_| Storage::Values).map_err(|e| match e {
193        GraphError::Unwalked { why } => ProgramError::Lift(why),
194        e => ProgramError::Graph(e),
195    })?;
196    let identity = lift.identity.expect("a cut's graph records its identity");
197    let graph = Arc::new(optimize(&lift, &EngineShim { spec, numerics }, &[]).graph);
198    cache.lock().unwrap_or_else(|e| e.into_inner()).insert(key, (Arc::clone(&graph), identity));
199    Ok((graph, identity))
200}
201
202/// The step of `spec` at `numerics` written from the spec directly: the embedding, each layer's mixer and FFN around
203/// the residual, the final norm and the head. The oracle [`lower`] is held to, never a program the engine is given.
204pub fn hand_lowering(spec: &ModelSpec, numerics: EngineNumerics) -> Program {
205    let mut c = vec![Command::Embed { table: "token_embd.weight".into() }];
206    for (l, layer) in spec.layers.iter().enumerate() {
207        c.push(Command::Norm { weight: format!("blk.{l}.attn_norm.weight"), from: Slot::Residual, to: Slot::Normed });
208        match &layer.mixer {
209            Mixer::GatedDelta(gd) => delta(&mut c, l, gd),
210            Mixer::Attention(a) => attention(&mut c, l, a, numerics),
211            Mixer::Mla(_) => unreachable!("the engine refuses multi-head latent attention"),
212        }
213        c.push(Command::Add { from: Slot::Out, to: Slot::Residual });
214        c.push(Command::Norm { weight: format!("blk.{l}.{}.weight", spec.ffn_norm), from: Slot::Residual, to: Slot::Normed });
215        c.push(Command::Matvec { tensor: format!("blk.{l}.ffn_gate.weight"), from: Slot::Normed, to: Slot::FfnGate });
216        c.push(Command::Matvec { tensor: format!("blk.{l}.ffn_up.weight"), from: Slot::Normed, to: Slot::FfnUp });
217        c.push(Command::SwiGlu { gate: Slot::FfnGate, up: Slot::FfnUp, to: Slot::Hidden });
218        c.push(Command::Matvec { tensor: format!("blk.{l}.ffn_down.weight"), from: Slot::Hidden, to: Slot::Out });
219        c.push(Command::Add { from: Slot::Out, to: Slot::Residual });
220    }
221    c.push(Command::Advance);
222    c.push(Command::Norm { weight: "output_norm.weight".into(), from: Slot::Residual, to: Slot::Normed });
223    let head = if spec.tied_output { "token_embd.weight" } else { "output.weight" };
224    c.push(Command::Matvec { tensor: head.into(), from: Slot::Normed, to: Slot::Logits });
225    Program { numerics, commands: c }
226}
227
228/// A Gated-DeltaNet block: the four projections, the convolution, the queries and keys L2-normalized per key head,
229/// the gates, the memory, the read normalized and gated, the output projection.
230fn delta(c: &mut Vec<Command>, l: usize, gd: &GatedDelta) {
231    let (hd, nk, nv) = (gd.head_dim, gd.k_heads, gd.v_heads);
232    for (tensor, to) in [("attn_qkv", Slot::Qkv), ("attn_gate", Slot::Z), ("ssm_beta", Slot::Beta), ("ssm_alpha", Slot::Alpha)] {
233        c.push(Command::Matvec { tensor: format!("blk.{l}.{tensor}.weight"), from: Slot::Normed, to });
234    }
235    c.push(Command::Conv { layer: l, kernel: format!("blk.{l}.ssm_conv1d.weight"), taps: gd.conv_taps, from: Slot::Qkv, to: Slot::Mixed });
236    c.push(Command::L2Heads { from: Slot::Mixed, at: 0, heads: nk, width: hd, to: Slot::Query });
237    c.push(Command::L2Heads { from: Slot::Mixed, at: nk * hd, heads: nk, width: hd, to: Slot::Key });
238    c.push(Command::Gates {
239        alpha: Slot::Alpha,
240        beta: Slot::Beta,
241        dt: format!("blk.{l}.ssm_dt.bias"),
242        a: format!("blk.{l}.ssm_a"),
243        log_decay: Slot::LogDecay,
244        decay: Slot::Decay,
245        gate: Slot::Gate,
246    });
247    c.push(Command::Delta {
248        layer: l,
249        query: Slot::Query,
250        key: Slot::Key,
251        value: Slot::Mixed,
252        value_at: 2 * nk * hd,
253        log_decay: Slot::LogDecay,
254        decay: Slot::Decay,
255        gate: Slot::Gate,
256        k_heads: nk,
257        v_heads: nv,
258        width: hd,
259        to: Slot::Read,
260    });
261    c.push(Command::GatedNorm { weight: format!("blk.{l}.ssm_norm.weight"), from: Slot::Read, z: Slot::Z, width: hd, to: Slot::Read });
262    c.push(Command::Matvec { tensor: format!("blk.{l}.ssm_out.weight"), from: Slot::Read, to: Slot::Out });
263}
264
265/// An attention block: the projections, the queries split from their gates, queries and keys normed per head and
266/// turned, rotated by the Walsh–Hadamard transform when the cache is, the cache appended, the attention, the gate,
267/// the output projection.
268fn attention(c: &mut Vec<Command>, l: usize, a: &Attention, numerics: EngineNumerics) {
269    let (hd, vd) = (a.head_dim, a.value_dim);
270    let rotated = numerics.cache == CacheFormat::Int8Hadamard;
271    let projected = if a.output_gate { Slot::Projected } else { Slot::Query };
272    c.push(Command::Matvec { tensor: format!("blk.{l}.attn_q.weight"), from: Slot::Normed, to: projected });
273    c.push(Command::Matvec { tensor: format!("blk.{l}.attn_k.weight"), from: Slot::Normed, to: Slot::Key });
274    c.push(Command::Matvec { tensor: format!("blk.{l}.attn_v.weight"), from: Slot::Normed, to: Slot::Value });
275    if a.output_gate {
276        c.push(Command::Split { from: Slot::Projected, heads: a.heads, width: hd, query: Slot::Query, gate: Slot::OutGate });
277    }
278    for (slot, name) in [(Slot::Query, "attn_q_norm"), (Slot::Key, "attn_k_norm")] {
279        if a.qk_norm {
280            c.push(Command::RmsHeads { weight: format!("blk.{l}.{name}.weight"), slot, width: hd });
281        }
282        if let Some(rope) = &a.rope {
283            c.push(Command::Rotate { slot, width: hd, rope: rope.clone() });
284        }
285    }
286    if rotated {
287        c.push(Command::Hadamard { slot: Slot::Key, width: hd });
288        c.push(Command::Hadamard { slot: Slot::Value, width: vd });
289    }
290    c.push(Command::CacheAppend { layer: l, key: Slot::Key, value: Slot::Value, kv_heads: a.kv_heads, width: hd, value_width: vd });
291    if rotated {
292        c.push(Command::Hadamard { slot: Slot::Query, width: hd });
293    }
294    c.push(Command::Attend {
295        layer: l,
296        query: Slot::Query,
297        heads: a.heads,
298        kv_heads: a.kv_heads,
299        width: hd,
300        value_width: vd,
301        scale: a.scale as f32,
302        to: Slot::Read,
303    });
304    if a.output_gate {
305        c.push(Command::SigmoidGate { slot: Slot::Read, gate: Slot::OutGate });
306    }
307    c.push(Command::Matvec { tensor: format!("blk.{l}.attn_output.weight"), from: Slot::Read, to: Slot::Out });
308}
309
310impl Program {
311    /// Every tensor the program reads, each once, in the order it first reads them: the weight image's consumption
312    /// order.
313    pub fn tensors(&self) -> Vec<&str> {
314        let mut seen = std::collections::HashSet::new();
315        let mut out = Vec::new();
316        for c in &self.commands {
317            let names: Vec<&str> = match c {
318                Command::Embed { table } => vec![table.as_str()],
319                Command::Norm { weight, .. } | Command::RmsHeads { weight, .. } | Command::GatedNorm { weight, .. } => vec![weight.as_str()],
320                Command::Matvec { tensor, .. } => vec![tensor.as_str()],
321                Command::Conv { kernel, .. } => vec![kernel.as_str()],
322                Command::Gates { dt, a, .. } => vec![dt.as_str(), a.as_str()],
323                _ => vec![],
324            };
325            for n in names {
326                if seen.insert(n) {
327                    out.push(n);
328                }
329            }
330        }
331        out
332    }
333
334    /// The slots each command reads and the slots it writes.
335    pub fn dataflow(command: &Command) -> (Vec<Slot>, Vec<Slot>) {
336        use Command as C;
337        match command {
338            C::Embed { .. } => (vec![], vec![Slot::Residual]),
339            C::Norm { from, to, .. } | C::Matvec { from, to, .. } | C::Conv { from, to, .. } | C::L2Heads { from, to, .. } => (vec![*from], vec![*to]),
340            C::Add { from, to } => (vec![*from, *to], vec![*to]),
341            C::SwiGlu { gate, up, to } => (vec![*gate, *up], vec![*to]),
342            C::Gates { alpha, beta, log_decay, decay, gate, .. } => (vec![*alpha, *beta], vec![*log_decay, *decay, *gate]),
343            C::Delta { query, key, value, log_decay, decay, gate, to, .. } => (vec![*query, *key, *value, *log_decay, *decay, *gate], vec![*to]),
344            C::GatedNorm { from, z, to, .. } => (vec![*from, *z], vec![*to]),
345            C::Split { from, query, gate, .. } => (vec![*from], vec![*query, *gate]),
346            C::RmsHeads { slot, .. } | C::Rotate { slot, .. } | C::Hadamard { slot, .. } => (vec![*slot], vec![*slot]),
347            C::CacheAppend { key, value, .. } => (vec![*key, *value], vec![]),
348            C::Attend { query, to, .. } => (vec![*query], vec![*to]),
349            C::SigmoidGate { slot, gate } => (vec![*slot, *gate], vec![*slot]),
350            C::Advance => (vec![], vec![]),
351        }
352    }
353}
354
355// ── The sequencer's words ───────────────────────────────────────────────────────────────────────────
356
357/// A command as the sequencer reads it: 256 bits, `[7:0]` the command, `[15:8]` its layer, seven 5-bit slots from bit
358/// 16, two 20-bit tensors from bit 51 (each its index in [`Program::tensors`], the weight image's order), four 16-bit
359/// counts from bit 91, a 20-bit element offset from bit 155, an 8-bit rotary table from bit 175 (its index in
360/// [`Program::ropes`]) and a 32-bit f32 constant from bit 183.
361pub type Word = [u64; 4];
362
363const SLOTS: [Slot; 21] = [
364    Slot::Residual,
365    Slot::Normed,
366    Slot::Out,
367    Slot::Qkv,
368    Slot::Mixed,
369    Slot::Z,
370    Slot::Alpha,
371    Slot::Beta,
372    Slot::LogDecay,
373    Slot::Decay,
374    Slot::Gate,
375    Slot::Query,
376    Slot::Key,
377    Slot::Value,
378    Slot::Projected,
379    Slot::OutGate,
380    Slot::Read,
381    Slot::FfnGate,
382    Slot::FfnUp,
383    Slot::Hidden,
384    Slot::Logits,
385];
386
387/// The fields of a word, as the encoder fills them and the decoder reads them.
388#[derive(Clone, Copy, Debug, Default, PartialEq)]
389struct Fields {
390    op: u8,
391    layer: u8,
392    slots: [u8; 7],
393    tensors: [u32; 2],
394    counts: [u16; 4],
395    at: u32,
396    rope: u8,
397    constant: u32,
398}
399
400fn slot_code(s: Slot) -> u8 {
401    s.code()
402}
403
404impl Slot {
405    /// Every slot, in the order of its code.
406    pub const ALL: [Slot; 21] = SLOTS;
407
408    /// The slot's five-bit code in the sequencer's words.
409    pub fn code(self) -> u8 {
410        SLOTS.iter().position(|&x| x == self).expect("every slot has a code") as u8
411    }
412
413    /// The slot a code stands for.
414    pub fn of_code(code: u8) -> Option<Slot> {
415        SLOTS.get(code as usize).copied()
416    }
417}
418
419fn put(w: &mut Word, at: usize, bits: usize, v: u64) {
420    assert!(bits == 64 || v < 1 << bits, "{v} past {bits} bits");
421    for i in 0..bits {
422        if v >> i & 1 == 1 {
423            w[(at + i) / 64] |= 1 << ((at + i) % 64);
424        }
425    }
426}
427
428fn get(w: &Word, at: usize, bits: usize) -> u64 {
429    (0..bits).fold(0, |v, i| v | ((w[(at + i) / 64] >> ((at + i) % 64) & 1) << i))
430}
431
432impl Fields {
433    fn word(&self) -> Word {
434        let mut w = [0u64; 4];
435        put(&mut w, 0, 8, self.op as u64);
436        put(&mut w, 8, 8, self.layer as u64);
437        for (i, &s) in self.slots.iter().enumerate() {
438            put(&mut w, 16 + 5 * i, 5, s as u64);
439        }
440        for (i, &t) in self.tensors.iter().enumerate() {
441            put(&mut w, 51 + 20 * i, 20, t as u64);
442        }
443        for (i, &c) in self.counts.iter().enumerate() {
444            put(&mut w, 91 + 16 * i, 16, c as u64);
445        }
446        put(&mut w, 155, 20, self.at as u64);
447        put(&mut w, 175, 8, self.rope as u64);
448        put(&mut w, 183, 32, self.constant as u64);
449        w
450    }
451
452    fn of(w: &Word) -> Fields {
453        let mut f = Fields { op: get(w, 0, 8) as u8, layer: get(w, 8, 8) as u8, ..Fields::default() };
454        for i in 0..7 {
455            f.slots[i] = get(w, 16 + 5 * i, 5) as u8;
456        }
457        for i in 0..2 {
458            f.tensors[i] = get(w, 51 + 20 * i, 20) as u32;
459        }
460        for i in 0..4 {
461            f.counts[i] = get(w, 91 + 16 * i, 16) as u16;
462        }
463        f.at = get(w, 155, 20) as u32;
464        f.rope = get(w, 175, 8) as u8;
465        f.constant = get(w, 183, 32) as u32;
466        f
467    }
468}
469
470impl Program {
471    /// Every rotary table the program turns heads by, each once, in the order it first does.
472    pub fn ropes(&self) -> Vec<&Rope> {
473        let mut out: Vec<&Rope> = Vec::new();
474        for c in &self.commands {
475            if let Command::Rotate { rope, .. } = c {
476                if !out.contains(&rope) {
477                    out.push(rope);
478                }
479            }
480        }
481        out
482    }
483
484    /// The program as the sequencer's words.
485    pub fn encode(&self) -> Vec<Word> {
486        let tensors: std::collections::HashMap<&str, u32> = self.tensors().into_iter().enumerate().map(|(i, n)| (n, i as u32)).collect();
487        let ropes = self.ropes();
488        let t = |name: &str| *tensors.get(name).expect("a tensor the program reads");
489        let s = slot_code;
490        let n16 = |v: usize| u16::try_from(v).expect("a count within 16 bits");
491        let n8 = |v: usize| u8::try_from(v).expect("a layer within 8 bits");
492        self.commands
493            .iter()
494            .map(|c| {
495                let mut f = Fields::default();
496                match c {
497                    Command::Embed { table } => (f.op, f.tensors[0]) = (0, t(table)),
498                    Command::Norm { weight, from, to } => (f.op, f.tensors[0], f.slots[0], f.slots[1]) = (1, t(weight), s(*from), s(*to)),
499                    Command::Matvec { tensor, from, to } => (f.op, f.tensors[0], f.slots[0], f.slots[1]) = (2, t(tensor), s(*from), s(*to)),
500                    Command::Add { from, to } => (f.op, f.slots[0], f.slots[1]) = (3, s(*from), s(*to)),
501                    Command::SwiGlu { gate, up, to } => (f.op, f.slots[0], f.slots[1], f.slots[2]) = (4, s(*gate), s(*up), s(*to)),
502                    Command::Conv { layer, kernel, taps, from, to } => {
503                        (f.op, f.layer, f.tensors[0], f.counts[0], f.slots[0], f.slots[1]) = (5, n8(*layer), t(kernel), n16(*taps), s(*from), s(*to))
504                    }
505                    Command::L2Heads { from, at, heads, width, to } => {
506                        (f.op, f.slots[0], f.at, f.counts[0], f.counts[1], f.slots[1]) = (6, s(*from), *at as u32, n16(*heads), n16(*width), s(*to))
507                    }
508                    Command::Gates { alpha, beta, dt, a, log_decay, decay, gate } => {
509                        f.op = 7;
510                        f.tensors = [t(dt), t(a)];
511                        f.slots[..5].copy_from_slice(&[s(*alpha), s(*beta), s(*log_decay), s(*decay), s(*gate)]);
512                    }
513                    Command::Delta { layer, query, key, value, value_at, log_decay, decay, gate, k_heads, v_heads, width, to } => {
514                        f.op = 8;
515                        f.layer = n8(*layer);
516                        f.slots = [s(*query), s(*key), s(*value), s(*log_decay), s(*decay), s(*gate), s(*to)];
517                        f.at = *value_at as u32;
518                        f.counts[..3].copy_from_slice(&[n16(*k_heads), n16(*v_heads), n16(*width)]);
519                    }
520                    Command::GatedNorm { weight, from, z, width, to } => {
521                        (f.op, f.tensors[0], f.slots[0], f.slots[1], f.counts[0], f.slots[2]) = (9, t(weight), s(*from), s(*z), n16(*width), s(*to))
522                    }
523                    Command::Split { from, heads, width, query, gate } => {
524                        (f.op, f.slots[0], f.counts[0], f.counts[1], f.slots[1], f.slots[2]) = (10, s(*from), n16(*heads), n16(*width), s(*query), s(*gate))
525                    }
526                    Command::RmsHeads { weight, slot, width } => (f.op, f.tensors[0], f.slots[0], f.counts[0]) = (11, t(weight), s(*slot), n16(*width)),
527                    Command::Rotate { slot, width, rope } => {
528                        let r = ropes.iter().position(|x| *x == rope).expect("a rotary table the program turns by");
529                        (f.op, f.slots[0], f.counts[0], f.rope) = (12, s(*slot), n16(*width), r as u8)
530                    }
531                    Command::Hadamard { slot, width } => (f.op, f.slots[0], f.counts[0]) = (13, s(*slot), n16(*width)),
532                    Command::CacheAppend { layer, key, value, kv_heads, width, value_width } => {
533                        f.op = 14;
534                        f.layer = n8(*layer);
535                        f.slots[..2].copy_from_slice(&[s(*key), s(*value)]);
536                        f.counts[..3].copy_from_slice(&[n16(*kv_heads), n16(*width), n16(*value_width)]);
537                    }
538                    Command::Attend { layer, query, heads, kv_heads, width, value_width, scale, to } => {
539                        f.op = 15;
540                        f.layer = n8(*layer);
541                        f.slots[..2].copy_from_slice(&[s(*query), s(*to)]);
542                        f.counts = [n16(*heads), n16(*kv_heads), n16(*width), n16(*value_width)];
543                        f.constant = scale.to_bits();
544                    }
545                    Command::SigmoidGate { slot, gate } => (f.op, f.slots[0], f.slots[1]) = (16, s(*slot), s(*gate)),
546                    Command::Advance => f.op = 17,
547                }
548                f.word()
549            })
550            .collect()
551    }
552
553    /// The commands the words stand for, with the tensors and rotary tables they index.
554    pub fn decode(words: &[Word], tensors: &[&str], ropes: &[&Rope]) -> Vec<Command> {
555        let sl = |c: u8| SLOTS[c as usize];
556        words
557            .iter()
558            .map(|w| {
559                let f = Fields::of(w);
560                let t = |i: usize| tensors[f.tensors[i] as usize].to_string();
561                let [s0, s1, s2, s3, s4, s5, s6] = f.slots.map(sl);
562                let [c0, c1, c2, c3] = f.counts.map(|c| c as usize);
563                let layer = f.layer as usize;
564                match f.op {
565                    0 => Command::Embed { table: t(0) },
566                    1 => Command::Norm { weight: t(0), from: s0, to: s1 },
567                    2 => Command::Matvec { tensor: t(0), from: s0, to: s1 },
568                    3 => Command::Add { from: s0, to: s1 },
569                    4 => Command::SwiGlu { gate: s0, up: s1, to: s2 },
570                    5 => Command::Conv { layer, kernel: t(0), taps: c0, from: s0, to: s1 },
571                    6 => Command::L2Heads { from: s0, at: f.at as usize, heads: c0, width: c1, to: s1 },
572                    7 => Command::Gates { alpha: s0, beta: s1, dt: t(0), a: t(1), log_decay: s2, decay: s3, gate: s4 },
573                    8 => Command::Delta {
574                        layer,
575                        query: s0,
576                        key: s1,
577                        value: s2,
578                        value_at: f.at as usize,
579                        log_decay: s3,
580                        decay: s4,
581                        gate: s5,
582                        k_heads: c0,
583                        v_heads: c1,
584                        width: c2,
585                        to: s6,
586                    },
587                    9 => Command::GatedNorm { weight: t(0), from: s0, z: s1, width: c0, to: s2 },
588                    10 => Command::Split { from: s0, heads: c0, width: c1, query: s1, gate: s2 },
589                    11 => Command::RmsHeads { weight: t(0), slot: s0, width: c0 },
590                    12 => Command::Rotate { slot: s0, width: c0, rope: ropes[f.rope as usize].clone() },
591                    13 => Command::Hadamard { slot: s0, width: c0 },
592                    14 => Command::CacheAppend { layer, key: s0, value: s1, kv_heads: c0, width: c1, value_width: c2 },
593                    15 => Command::Attend { layer, query: s0, heads: c0, kv_heads: c1, width: c2, value_width: c3, scale: f32::from_bits(f.constant), to: s1 },
594                    16 => Command::SigmoidGate { slot: s0, gate: s1 },
595                    17 => Command::Advance,
596                    op => panic!("no command {op}"),
597                }
598            })
599            .collect()
600    }
601}