Skip to main content

logicaffeine_compile/spatial/decode_engine/
capability.rs

1//! What the decode engine runs: a checkpoint held, feature by feature and tensor by tensor, to the program
2//! [`super::program::lower`] reads off the model's one program and the units compute, and every gap refused by name.
3//!
4//! The lowering reads a [`ModelSpec`] and writes the commands of a Qwen3.5/3.8-shaped step: RMSNorm before each
5//! block, a Gated-DeltaNet or grouped-query attention mixer, a dense SwiGLU feed-forward block, the residual adds, the
6//! final norm and the head. A model with anything else would lower to a program that computes something other than the
7//! model, and nothing downstream would notice: the tensors of a GELU block are the tensors of a SwiGLU block. So
8//! before a checkpoint becomes a program, [`refusals`] names each feature the engine does not carry
9//! ([`spec_refusals`]), and then each tensor the program reads that the checkpoint lacks or stores in a format no unit
10//! reads: the array reads [`numerics::reads`]'s block formats, the vector unit plain floats.
11//!
12//! An empty list is the engine's claim that it computes the model; [`admit`] is that claim as a gate.
13//!
14//! A checkpoint the engine computes runs on an engine build when the build has every unit, width and format its step
15//! needs ([`needs`]): [`covers`] holds them against the build's [`EngineParams`], the dual of [`EngineParams::union`]
16//! (a build covers a step exactly when the union of the two is the build), and names every gap. [`runs_on`] is both
17//! at once: whether a checkpoint's header runs on a build, in the time it takes to read the header.
18
19use std::fmt;
20
21use serde::{Deserialize, Serialize};
22
23use crate::model::quant::GgmlType;
24use crate::model::spec::{Act, Ffn, GateActivation, Mixer, ModelSpec, RopeScaling};
25
26use super::machine::{self, Layout, StoreLayout};
27use super::model::{EngineNumerics, FACTORS};
28use super::numerics;
29use super::program::lower;
30use super::rtl::engine::EngineParams;
31use crate::spatial::decode::GgufHeader;
32use crate::spatial::serving::StateFlush;
33
34/// One thing a checkpoint needs that the engine does not do.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum Refusal {
37    /// A block's token mixer the engine has no unit for.
38    Mixer { layer: usize, what: &'static str },
39    /// A feed-forward block other than the dense SwiGLU the lowering writes.
40    Ffn { layer: usize, what: &'static str },
41    /// A norm the lowering does not apply.
42    Norm { layer: usize, what: &'static str },
43    /// An attention variant the attention unit does not compute.
44    Attention { layer: usize, what: &'static str },
45    /// A scale the token embedding is read with.
46    EmbeddingScale,
47    /// Soft-capped logits.
48    LogitSoftcap,
49    /// A tensor the program reads that the checkpoint does not hold.
50    MissingTensor(String),
51    /// A tensor the program reads in a format no unit reads.
52    Format { tensor: String, format: &'static str },
53    /// The model's one program does not lower to the engine, for the reason given.
54    Program(String),
55}
56
57impl fmt::Display for Refusal {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Refusal::Mixer { layer, .. } | Refusal::Ffn { layer, .. } | Refusal::Norm { layer, .. } | Refusal::Attention { layer, .. } => {
61                write!(f, "layer {layer}: {}", self.gap())
62            }
63            Refusal::EmbeddingScale => write!(f, "a scaled token embedding"),
64            Refusal::LogitSoftcap => write!(f, "soft-capped logits"),
65            Refusal::MissingTensor(t) => write!(f, "tensor `{t}` is not in the checkpoint"),
66            Refusal::Format { tensor, format } => write!(f, "tensor `{tensor}` is {format}, which no unit reads"),
67            Refusal::Program(why) => write!(f, "the model's program does not lower to the engine: {why}"),
68        }
69    }
70}
71
72/// Each feature of `spec` the lowering does not carry, layer by layer, then the model's own.
73pub fn spec_refusals(spec: &ModelSpec) -> Vec<Refusal> {
74    let mut out = Vec::new();
75    for (layer, l) in spec.layers.iter().enumerate() {
76        match &l.mixer {
77            Mixer::GatedDelta(g) if g.gate == GateActivation::Sigmoid => out.push(Refusal::Mixer { layer, what: "a Gated DeltaNet gated by a sigmoid" }),
78            Mixer::GatedDelta(_) => {}
79            Mixer::Mla(_) => out.push(Refusal::Mixer { layer, what: "multi-head latent attention" }),
80            Mixer::Attention(a) => {
81                let mut attention = |cond: bool, what: &'static str| {
82                    if cond {
83                        out.push(Refusal::Attention { layer, what });
84                    }
85                };
86                attention(a.bias || a.output_bias, "projection biases");
87                attention(a.fused_qkv, "one fused query, key and value projection");
88                attention(a.window.is_some(), "a sliding window");
89                attention(a.sinks, "attention sinks");
90                attention(a.softcap.is_some(), "soft-capped scores");
91                attention(a.temperature.is_some(), "a per-position query temperature");
92                attention(a.qk_unit_norm, "a weightless QK norm");
93                attention(a.qk_norm_whole, "a QK norm across every head");
94            }
95        }
96        match &l.ffn {
97            Ffn::Dense { act: Act::Silu, .. } => {}
98            Ffn::Dense { act: Act::GeluTanh, .. } => out.push(Refusal::Ffn { layer, what: "GELU" }),
99            Ffn::Dense { act: Act::SwigluOai { .. }, .. } => out.push(Refusal::Ffn { layer, what: "gpt-oss's clamped SwiGLU" }),
100            Ffn::Moe(_) => out.push(Refusal::Ffn { layer, what: "experts" }),
101        }
102        if l.post_mixer_norm {
103            out.push(Refusal::Norm { layer, what: "a norm on the mixer's output" });
104        }
105        if l.post_ffn_norm {
106            out.push(Refusal::Norm { layer, what: "a norm on the feed-forward output" });
107        }
108    }
109    if spec.embedding_scale.is_some() {
110        out.push(Refusal::EmbeddingScale);
111    }
112    if spec.final_logit_softcap.is_some() {
113        out.push(Refusal::LogitSoftcap);
114    }
115    out
116}
117
118/// A format of plain floats, which the vector unit reads.
119fn float(ty: GgmlType) -> bool {
120    matches!(ty, GgmlType::F32 | GgmlType::F16 | GgmlType::Bf16)
121}
122
123/// Every gap between the checkpoint and the engine: [`spec_refusals`], and when there are none, the frequency factors a
124/// rotation scaled by them turns by (`rope_freqs.weight`, written into its rotary tables) when `tensors` (the
125/// checkpoint's directory, name and format) lacks them, and each tensor the program reads that `tensors` lacks or
126/// stores in a format no unit reads.
127pub fn refusals(spec: &ModelSpec, tensors: &[(String, GgmlType)]) -> Vec<Refusal> {
128    let mut out = spec_refusals(spec);
129    if !out.is_empty() {
130        return out;
131    }
132    let program = match lower(spec, EngineNumerics::precise(), spec.ctx_len) {
133        Ok(lowered) => lowered.program,
134        Err(e) => {
135            out.push(Refusal::Program(e.to_string()));
136            return out;
137        }
138    };
139    let factored = spec.layers.iter().any(|l| match &l.mixer {
140        Mixer::Attention(a) => a.rope.as_ref().is_some_and(|r| r.scaling == RopeScaling::Factors),
141        _ => false,
142    });
143    if factored && !tensors.iter().any(|(n, _)| n == FACTORS) {
144        out.push(Refusal::MissingTensor(FACTORS.to_string()));
145    }
146    for name in program.tensors() {
147        match tensors.iter().find(|(n, _)| n == name) {
148            None => out.push(Refusal::MissingTensor(name.to_string())),
149            Some((_, ty)) if !numerics::reads(*ty) && !float(*ty) => {
150                out.push(Refusal::Format { tensor: name.to_string(), format: ty.name() })
151            }
152            Some(_) => {}
153        }
154    }
155    out
156}
157
158/// The engine's claim that it computes the checkpoint, or the gaps that stand in the way.
159pub fn admit(spec: &ModelSpec, tensors: &[(String, GgmlType)]) -> Result<(), Vec<Refusal>> {
160    let r = refusals(spec, tensors);
161    if r.is_empty() {
162        Ok(())
163    } else {
164        Err(r)
165    }
166}
167
168impl Refusal {
169    /// The layer the refusal is about, for those that are a layer's.
170    fn layer(&self) -> Option<usize> {
171        match self {
172            Refusal::Mixer { layer, .. } | Refusal::Ffn { layer, .. } | Refusal::Norm { layer, .. } | Refusal::Attention { layer, .. } => {
173                Some(*layer)
174            }
175            _ => None,
176        }
177    }
178
179    /// The refusal without its layer: the words the engine's shim ([`super::shim::EngineShim`]) refuses the op that
180    /// carries it in.
181    pub fn gap(&self) -> String {
182        match self {
183            Refusal::Mixer { what, .. } | Refusal::Norm { what, .. } => what.to_string(),
184            Refusal::Ffn { what, .. } => format!("a feed-forward block of {what}"),
185            Refusal::Attention { what, .. } => format!("attention with {what}"),
186            other => other.to_string(),
187        }
188    }
189}
190
191/// `0–4, 6–10, 12` for a sorted list of layers.
192fn ranges(layers: &[usize]) -> String {
193    let mut out: Vec<String> = Vec::new();
194    let mut i = 0;
195    while i < layers.len() {
196        let mut j = i;
197        while j + 1 < layers.len() && layers[j + 1] == layers[j] + 1 {
198            j += 1;
199        }
200        out.push(if i == j { layers[i].to_string() } else { format!("{}–{}", layers[i], layers[j]) });
201        i = j + 1;
202    }
203    out.join(", ")
204}
205
206/// Each distinct gap once, in the order first met, with the layers it stands in: `every layer` when it is all of a
207/// model's `layers`.
208pub fn summarize(refusals: &[Refusal], layers: usize) -> Vec<String> {
209    let mut gaps: Vec<(String, Vec<usize>)> = Vec::new();
210    for r in refusals {
211        let gap = r.gap();
212        let k = match gaps.iter().position(|(g, _)| *g == gap) {
213            Some(k) => k,
214            None => {
215                gaps.push((gap, Vec::new()));
216                gaps.len() - 1
217            }
218        };
219        if let Some(l) = r.layer() {
220            gaps[k].1.push(l);
221        }
222    }
223    gaps.into_iter()
224        .map(|(gap, ls)| match ls.len() {
225            0 => gap,
226            n if n == layers => format!("{gap} (every layer)"),
227            1 => format!("{gap} (layer {})", ls[0]),
228            _ => format!("{gap} (layers {})", ranges(&ls)),
229        })
230        .collect()
231}
232
233/// The engine's verdict on a checkpoint described by its header.
234#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
235pub enum Verdict {
236    /// The engine computes it.
237    Admitted,
238    /// What stands in the way, each a sentence.
239    Refused(Vec<String>),
240    /// The header describes no model this crate reads.
241    Undescribed(String),
242}
243
244/// The verdict on the checkpoint `header` describes: its architecture read as a [`ModelSpec`], its tensor directory
245/// as the formats it stores. A tensor of a type id no reader knows is refused as such.
246pub fn judge(header: &GgufHeader) -> Verdict {
247    let spec = match header.spec() {
248        Ok(spec) => spec,
249        Err(e) => return Verdict::Undescribed(e.to_string()),
250    };
251    let mut known = Vec::with_capacity(header.tensors.len());
252    let mut unknown = Vec::new();
253    for t in &header.tensors {
254        match GgmlType::from_id(t.type_id) {
255            Ok(ty) => known.push((t.name.clone(), ty)),
256            Err(id) => unknown.push((t.name.clone(), id)),
257        }
258    }
259    match admit(&spec, &known) {
260        Ok(()) => Verdict::Admitted,
261        Err(refused) => Verdict::Refused(
262            summarize(&refused, spec.layers.len())
263                .into_iter()
264                .map(|line| match unknown.iter().find(|(n, _)| line == Refusal::MissingTensor(n.clone()).to_string()) {
265                    Some((name, id)) => format!("tensor `{name}` is of type id {id}, which no reader knows"),
266                    None => line,
267                })
268                .collect(),
269        ),
270    }
271}
272
273/// One checkpoint's row of the capability matrix.
274#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
275pub struct Row {
276    /// The header ledger's file stem.
277    pub checkpoint: String,
278    pub verdict: Verdict,
279}
280
281/// The verdict on every header ledger (`*.json`) in `dir`, by file name.
282pub fn matrix(dir: &std::path::Path) -> Result<Vec<Row>, String> {
283    let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
284        .map_err(|e| format!("{}: {e}", dir.display()))?
285        .filter_map(|e| e.ok().map(|e| e.path()))
286        .filter(|p| p.extension().is_some_and(|x| x == "json"))
287        .collect();
288    paths.sort();
289    paths
290        .iter()
291        .map(|p| {
292            let header = GgufHeader::read_ledger(p).map_err(|e| format!("{}: {e}", p.display()))?;
293            let checkpoint = p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
294            Ok(Row { checkpoint, verdict: judge(&header) })
295        })
296        .collect()
297}
298
299/// What a checkpoint's step needs of an engine build: the engine its jobs need ([`EngineParams::of`]) and the block
300/// formats of the matrices a hybrid build streams, in their ggml ids' order.
301#[derive(Clone, Debug, PartialEq)]
302pub struct Needs {
303    pub engine: EngineParams,
304    pub streamed: Vec<GgmlType>,
305}
306
307/// What the step of the checkpoint `header` describes needs, at the engine's fastest point, one sequence over at most
308/// `ctx` positions.
309pub fn needs(header: &GgufHeader, ctx: usize) -> Result<Needs, String> {
310    let spec = header.spec().map_err(|e| e.to_string())?;
311    let n = EngineNumerics::fastest();
312    let steps = lower(&spec, n, spec.ctx_len.min(ctx)).map_err(|e| e.to_string())?.program;
313    let layout = Layout::of(&spec, n, &steps, header, 1, spec.ctx_len.min(ctx)).map_err(|e| format!("{e:?}"))?;
314    let jobs = machine::lower(&steps, &layout, spec.rms_eps as f32);
315    let mut streamed: Vec<GgmlType> = layout.tensors.iter().filter(|t| t.streamed).map(|t| t.ty).collect();
316    streamed.sort_by_key(|t| t.id());
317    streamed.dedup();
318    Ok(Needs { engine: EngineParams::of(&layout, &StoreLayout::of(&jobs, &layout, 1), &jobs), streamed })
319}
320
321/// One thing a step needs that an engine build does not have.
322#[derive(Clone, Debug, PartialEq, Eq)]
323pub enum Gap {
324    /// More of something the build holds a fixed number of.
325    Short { what: &'static str, has: usize, needs: usize },
326    /// Rows of a block format the build's matrix unit does not read, or reads only in rows of fewer blocks.
327    Format { format: GgmlType, has: Option<usize>, needs: usize },
328    /// A block format the build's streaming unit does not decode.
329    Streamed { format: GgmlType },
330    /// A recurrence unit of other heads (their width and how their memories are carried), or none.
331    Recurrence { has: Option<(usize, StateFlush)>, needs: (usize, StateFlush) },
332    /// An attention unit of other heads (query width, value width, positions, whether the math runs on the array), or
333    /// none, or one over fewer positions.
334    Attention { has: Option<(usize, usize, usize, bool)>, needs: (usize, usize, usize, bool) },
335    /// A step whose matrix jobs fold their sums into their output slots, on a build whose matrix unit writes each as it is.
336    Folds,
337}
338
339/// A recurrence unit's log as a gap names it: the tokens a log holds before they are replayed (one for a memory rewritten
340/// every pass), or the windows a trailing flush replays a window behind.
341fn log_of(flush: StateFlush) -> String {
342    match flush {
343        StateFlush::EveryPass => "a log of 1 token(s)".into(),
344        StateFlush::Every { tokens } => format!("a log of {tokens} token(s)"),
345        StateFlush::Trailing { tokens } => format!("a log of {tokens}-token windows replayed a window behind"),
346    }
347}
348
349impl fmt::Display for Gap {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        match self {
352            Gap::Short { what, has, needs } => write!(f, "{needs} {what}, where the build holds {has}"),
353            Gap::Format { format, has: None, needs } => {
354                write!(f, "{} rows of {needs} blocks, a format the build's matrix unit does not read", format.name())
355            }
356            Gap::Format { format, has: Some(has), needs } => {
357                write!(f, "{} rows of {needs} blocks, where the build's matrix unit reads rows of {has}", format.name())
358            }
359            Gap::Streamed { format } => write!(f, "{} matrices, a format the build's streaming unit does not decode", format.name()),
360            Gap::Recurrence { has, needs: (width, flush) } => {
361                write!(f, "a recurrence unit for heads of {width} with {}", log_of(*flush))?;
362                match has {
363                    None => write!(f, ", where the build has none"),
364                    Some((w, flush)) => write!(f, ", where the build's is for heads of {w} with {}", log_of(*flush)),
365                }
366            }
367            Gap::Attention { has, needs: (q, v, ctx, array) } => {
368                let on = |array: bool| if array { "on the array" } else { "on the vector unit" };
369                write!(f, "an attention unit for heads of {q} and {v} over {ctx} positions, its math {}", on(*array))?;
370                match has {
371                    None => write!(f, ", where the build has none"),
372                    Some((q2, v2, ctx2, array2)) => {
373                        write!(f, ", where the build's is for heads of {q2} and {v2} over {ctx2} positions, its math {}", on(*array2))
374                    }
375                }
376            }
377            Gap::Folds => write!(f, "matrix sums folded into their output slots, where the build's matrix unit writes each as it is"),
378        }
379    }
380}
381
382/// Whether the engine `build` runs the step `step` needs, or every gap: each width it holds at least as many of, each
383/// block format its matrix unit reads in rows at least as long (the vector side's registers among them, as deep as the
384/// step's deepest job that cannot run a chunk at a time), the recurrence unit of the same heads, the attention
385/// unit of the same heads over at least as many positions, and on a hybrid build every streamed format decoded. A
386/// build covers a step exactly when the union of the two ([`EngineParams::union`]) is the build. The vector unit's
387/// function units set how fast a function row runs, not what runs, so they leave no gap. Every field of the step's
388/// engine is named here, so a field `EngineParams` gains does not compile until it is judged.
389pub fn covers(build: &EngineParams, step: &Needs) -> Result<(), Vec<Gap>> {
390    let EngineParams {
391        rows,
392        vector_rows,
393        side_rows,
394        pair_rows,
395        store_beats,
396        job_words,
397        tensors,
398        layers,
399        ropes,
400        formats,
401        delta,
402        attend,
403        function_units: _,
404        folds,
405        streaming: _,
406    } = &step.engine;
407    let mut gaps = Vec::new();
408    for (what, has, needs) in [
409        ("token-rows", build.rows, *rows),
410        ("vector rows", build.vector_rows, *vector_rows),
411        ("pair rows", build.pair_rows, *pair_rows),
412        ("side rows", build.side_rows, *side_rows),
413        ("store beats", build.store_beats, *store_beats),
414        ("job words", build.job_words, *job_words),
415        ("tensor-table entries", build.tensors, *tensors),
416        ("layers", build.layers, *layers),
417        ("rotary tables", build.ropes, *ropes),
418    ] {
419        if needs > has {
420            gaps.push(Gap::Short { what, has, needs });
421        }
422    }
423    for &(format, needs) in formats {
424        let has = build.formats.iter().find(|(t, _)| *t == format).map(|&(_, b)| b);
425        if has.is_none_or(|b| b < needs) {
426            gaps.push(Gap::Format { format, has, needs });
427        }
428    }
429    if let Some(needs) = *delta {
430        if build.delta != Some(needs) {
431            gaps.push(Gap::Recurrence { has: build.delta, needs });
432        }
433    }
434    if let Some(needs @ (q, v, ctx, array)) = *attend {
435        if !build.attend.is_some_and(|(q2, v2, ctx2, array2)| (q2, v2, array2) == (q, v, array) && ctx2 >= ctx) {
436            gaps.push(Gap::Attention { has: build.attend, needs });
437        }
438    }
439    if *folds && !build.folds {
440        gaps.push(Gap::Folds);
441    }
442    if let Some(streaming) = &build.streaming {
443        for &format in &step.streamed {
444            if !streaming.set.formats.contains(&format) {
445                gaps.push(Gap::Streamed { format });
446            }
447        }
448    }
449    if gaps.is_empty() {
450        Ok(())
451    } else {
452        Err(gaps)
453    }
454}
455
456/// Whether the checkpoint `header` describes runs on the engine `build` at `ctx` positions: the engine computes it
457/// ([`judge`]) and the build covers its step ([`covers`]). Otherwise every reason, a sentence each.
458pub fn runs_on(header: &GgufHeader, build: &EngineParams, ctx: usize) -> Result<(), Vec<String>> {
459    match judge(header) {
460        Verdict::Admitted => {}
461        Verdict::Refused(gaps) => return Err(gaps),
462        Verdict::Undescribed(why) => return Err(vec![why]),
463    }
464    let step = needs(header, ctx).map_err(|e| vec![e])?;
465    covers(build, &step).map_err(|gaps| gaps.iter().map(Gap::to_string).collect())
466}
467
468#[cfg(test)]
469#[path = "../../../../logicaffeine_tests/unit/logicaffeine_compile/spatial/decode_engine/capability/tests.rs"]
470mod tests;