1use 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#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum Refusal {
37 Mixer { layer: usize, what: &'static str },
39 Ffn { layer: usize, what: &'static str },
41 Norm { layer: usize, what: &'static str },
43 Attention { layer: usize, what: &'static str },
45 EmbeddingScale,
47 LogitSoftcap,
49 MissingTensor(String),
51 Format { tensor: String, format: &'static str },
53 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
72pub 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
118fn float(ty: GgmlType) -> bool {
120 matches!(ty, GgmlType::F32 | GgmlType::F16 | GgmlType::Bf16)
121}
122
123pub 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
158pub 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 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 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
191fn 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
206pub 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
235pub enum Verdict {
236 Admitted,
238 Refused(Vec<String>),
240 Undescribed(String),
242}
243
244pub 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
275pub struct Row {
276 pub checkpoint: String,
278 pub verdict: Verdict,
279}
280
281pub 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#[derive(Clone, Debug, PartialEq)]
302pub struct Needs {
303 pub engine: EngineParams,
304 pub streamed: Vec<GgmlType>,
305}
306
307pub 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#[derive(Clone, Debug, PartialEq, Eq)]
323pub enum Gap {
324 Short { what: &'static str, has: usize, needs: usize },
326 Format { format: GgmlType, has: Option<usize>, needs: usize },
328 Streamed { format: GgmlType },
330 Recurrence { has: Option<(usize, StateFlush)>, needs: (usize, StateFlush) },
332 Attention { has: Option<(usize, usize, usize, bool)>, needs: (usize, usize, usize, bool) },
335 Folds,
337}
338
339fn 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
382pub 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
456pub 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;