Skip to main content

memra_gguf/
execution_manifest.rs

1//! Static operation-level manifests for tuned execution programs.
2//!
3//! These tables describe implemented kernel programs, not model families. A new model can use a
4//! program when every operation in its compiled `ModelPlan` is present. Missing operations fail
5//! closed and remain visible as capability blockers.
6
7use crate::model_plan::{ModelPlan, OperationKind, OperationSupport, PlanCapabilities};
8use sha2::{Digest, Sha256};
9use std::collections::BTreeSet;
10use std::fmt::Write as _;
11use std::path::Path;
12
13#[derive(Clone, Copy)]
14pub struct KernelManifest {
15    pub name: &'static str,
16    support: fn(OperationKind) -> OperationSupport,
17}
18
19impl KernelManifest {
20    pub const fn new(name: &'static str, support: fn(OperationKind) -> OperationSupport) -> Self {
21        Self { name, support }
22    }
23
24    pub fn support(self, operation: OperationKind) -> OperationSupport {
25        (self.support)(operation)
26    }
27
28    pub fn capabilities(self, plan: &ModelPlan) -> PlanCapabilities {
29        plan.derive_capabilities(|operation| self.support(operation))
30    }
31
32    pub fn trunk_capabilities(self, plan: &ModelPlan) -> PlanCapabilities {
33        plan.derive_trunk_capabilities(|operation| self.support(operation))
34    }
35
36    pub fn multimodal_prefill_capabilities(self, plan: &ModelPlan) -> PlanCapabilities {
37        plan.derive_multimodal_prefill_capabilities(|operation| self.support(operation))
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
42pub enum RewriteSurface {
43    CarriedPrime,
44    DecodeEager,
45    DecodeBatch,
46    DecodeGraph,
47    MtpSpec,
48    /// glm5_next T-parallel speculative loop (embedded MLA-mixer MTP draft + one-walk
49    /// verify over the HyperConnections trunk). A DISTINCT program from `MtpSpec` — the
50    /// two manifests' operation tables are disjoint on the mixer/residual classes, so no
51    /// plan is ever eligible for both.
52    Glm5Spec,
53    Pipeline,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct RewriteQualifications {
58    pub plan_sha256: String,
59    passed: BTreeSet<RewriteSurface>,
60}
61
62impl RewriteQualifications {
63    pub fn load(bundle: &Path, plan: &ModelPlan) -> Result<Self, String> {
64        let expected = execution_rewrites(plan);
65        let artifact_lock = std::fs::read(bundle.join("artifact.lock"))
66            .map_err(|error| format!("read artifact.lock: {error}"))?;
67        let artifact_lock_sha256 = hex_sha256(&artifact_lock);
68        let index = std::fs::read_to_string(bundle.join("rewrite-receipts.tsv"))
69            .map_err(|error| format!("read rewrite receipt index: {error}"))?;
70        let mut passed = BTreeSet::new();
71        for line in index.lines().skip(1) {
72            let columns: Vec<_> = line.split('\t').collect();
73            if columns.len() != 4 || columns[3] != "passed" {
74                return Err(format!("malformed rewrite receipt index row {line:?}"));
75            }
76            let rewrite = expected
77                .iter()
78                .find(|rewrite| rewrite.id == columns[0])
79                .ok_or_else(|| format!("receipt names unknown rewrite {}", columns[0]))?;
80            if !rewrite.eligible() || columns[1] != rewrite.plan_sha256 {
81                return Err(format!(
82                    "receipt {} is not eligible for plan {}",
83                    rewrite.id, rewrite.plan_sha256
84                ));
85            }
86            let receipt_path = bundle
87                .join("rewrite-receipts")
88                .join(format!("{}.tsv", rewrite.id));
89            let receipt = std::fs::read(&receipt_path)
90                .map_err(|error| format!("read {}: {error}", receipt_path.display()))?;
91            if hex_sha256(&receipt) != columns[2] {
92                return Err(format!("rewrite receipt hash mismatch for {}", rewrite.id));
93            }
94            let text = std::str::from_utf8(&receipt)
95                .map_err(|error| format!("rewrite receipt is not UTF-8: {error}"))?;
96            for (key, value) in [
97                ("status", "passed"),
98                ("rewrite", rewrite.id),
99                ("surface", rewrite.surface.as_str()),
100                ("implementation", rewrite.implementation),
101                ("plan_sha256", rewrite.plan_sha256.as_str()),
102                ("artifact_lock_sha256", artifact_lock_sha256.as_str()),
103                ("first_violation", "none"),
104            ] {
105                if !text.lines().any(|line| line == format!("{key}\t{value}")) {
106                    return Err(format!(
107                        "rewrite receipt {} does not bind {key}={value}",
108                        rewrite.id
109                    ));
110                }
111            }
112            passed.insert(rewrite.surface);
113        }
114        Ok(Self {
115            plan_sha256: plan_sha256(plan),
116            passed,
117        })
118    }
119
120    pub fn allows(&self, surface: RewriteSurface) -> bool {
121        self.passed.contains(&surface)
122    }
123
124    pub fn all_eligible(&self, plan: &ModelPlan) -> bool {
125        execution_rewrites(plan)
126            .into_iter()
127            .filter(ExecutionRewrite::eligible)
128            .all(|rewrite| self.allows(rewrite.surface))
129    }
130}
131
132impl RewriteSurface {
133    pub const fn as_str(self) -> &'static str {
134        match self {
135            Self::CarriedPrime => "carried-prime",
136            Self::DecodeEager => "decode-eager",
137            Self::DecodeBatch => "decode-batch",
138            Self::DecodeGraph => "decode-graph",
139            Self::MtpSpec => "mtp-spec",
140            Self::Glm5Spec => "glm5-spec",
141            Self::Pipeline => "pipeline",
142        }
143    }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ExecutionRewrite {
148    pub id: &'static str,
149    pub surface: RewriteSurface,
150    pub implementation: &'static str,
151    pub plan_sha256: String,
152    pub canonical_operations: Vec<OperationKind>,
153    pub blockers: Vec<OperationKind>,
154}
155
156impl ExecutionRewrite {
157    pub fn eligible(&self) -> bool {
158        self.blockers.is_empty()
159    }
160
161    pub fn verify_logits(
162        &self,
163        implementation_sha256: &str,
164        reference: &[f32],
165        candidate: &[f32],
166        policy: RewriteParityPolicy,
167    ) -> Result<RewriteParityReceipt, String> {
168        if !self.eligible() {
169            return Err(format!(
170                "rewrite {} is blocked by {:?}",
171                self.id, self.blockers
172            ));
173        }
174        if !is_sha256(implementation_sha256) {
175            return Err("rewrite implementation identity must be a lowercase SHA-256".into());
176        }
177        if reference.len() != candidate.len() || reference.is_empty() {
178            return Err(format!(
179                "rewrite parity requires equal non-empty streams (reference={} candidate={})",
180                reference.len(),
181                candidate.len()
182            ));
183        }
184        let mut max_abs = 0.0f32;
185        let mut max_rel = 0.0f32;
186        let mut first_violation = None;
187        for (index, (&expected, &actual)) in reference.iter().zip(candidate).enumerate() {
188            if !expected.is_finite() || !actual.is_finite() {
189                return Err(format!("rewrite parity has a non-finite value at {index}"));
190            }
191            let absolute = (expected - actual).abs();
192            let relative = absolute / expected.abs().max(1e-6);
193            max_abs = max_abs.max(absolute);
194            max_rel = max_rel.max(relative);
195            let allowed = policy.max_abs + policy.max_rel * expected.abs();
196            if absolute > allowed && first_violation.is_none() {
197                first_violation = Some(index);
198            }
199        }
200        let reference_argmax = stable_argmax(reference);
201        let candidate_argmax = stable_argmax(candidate);
202        let passed = first_violation.is_none()
203            && (!policy.require_argmax || reference_argmax == candidate_argmax);
204        Ok(RewriteParityReceipt {
205            rewrite_id: self.id,
206            surface: self.surface,
207            implementation: self.implementation,
208            implementation_sha256: implementation_sha256.to_string(),
209            plan_sha256: self.plan_sha256.clone(),
210            artifact_lock_sha256: None,
211            reference_sha256: f32_stream_sha256(reference),
212            candidate_sha256: f32_stream_sha256(candidate),
213            value_kind: RewriteValueKind::LogitsF32,
214            values: reference.len(),
215            max_abs,
216            max_rel,
217            reference_argmax,
218            candidate_argmax,
219            policy,
220            passed,
221            first_violation,
222        })
223    }
224
225    pub fn verify_tokens(
226        &self,
227        implementation_sha256: &str,
228        reference: &[u32],
229        candidate: &[u32],
230    ) -> Result<RewriteParityReceipt, String> {
231        if !self.eligible() {
232            return Err(format!(
233                "rewrite {} is blocked by {:?}",
234                self.id, self.blockers
235            ));
236        }
237        if !is_sha256(implementation_sha256) {
238            return Err("rewrite implementation identity must be a lowercase SHA-256".into());
239        }
240        if reference.len() != candidate.len() || reference.is_empty() {
241            return Err(format!(
242                "rewrite parity requires equal non-empty token streams (reference={} candidate={})",
243                reference.len(),
244                candidate.len()
245            ));
246        }
247        let first_violation = reference
248            .iter()
249            .zip(candidate)
250            .position(|(expected, actual)| expected != actual);
251        let max_abs = reference
252            .iter()
253            .zip(candidate)
254            .map(|(&expected, &actual)| expected.abs_diff(actual) as f32)
255            .fold(0.0f32, f32::max);
256        Ok(RewriteParityReceipt {
257            rewrite_id: self.id,
258            surface: self.surface,
259            implementation: self.implementation,
260            implementation_sha256: implementation_sha256.to_string(),
261            plan_sha256: self.plan_sha256.clone(),
262            artifact_lock_sha256: None,
263            reference_sha256: u32_stream_sha256(reference),
264            candidate_sha256: u32_stream_sha256(candidate),
265            value_kind: RewriteValueKind::TokenIdsU32,
266            values: reference.len(),
267            max_abs,
268            max_rel: 0.0,
269            reference_argmax: 0,
270            candidate_argmax: 0,
271            policy: RewriteParityPolicy {
272                max_abs: 0.0,
273                max_rel: 0.0,
274                require_argmax: false,
275            },
276            passed: first_violation.is_none(),
277            first_violation,
278        })
279    }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq)]
283pub struct RewriteParityPolicy {
284    pub max_abs: f32,
285    pub max_rel: f32,
286    pub require_argmax: bool,
287}
288
289#[derive(Debug, Clone, PartialEq)]
290pub struct RewriteParityReceipt {
291    pub rewrite_id: &'static str,
292    pub surface: RewriteSurface,
293    pub implementation: &'static str,
294    pub implementation_sha256: String,
295    pub plan_sha256: String,
296    pub artifact_lock_sha256: Option<String>,
297    pub reference_sha256: String,
298    pub candidate_sha256: String,
299    pub value_kind: RewriteValueKind,
300    pub values: usize,
301    pub max_abs: f32,
302    pub max_rel: f32,
303    pub reference_argmax: usize,
304    pub candidate_argmax: usize,
305    pub policy: RewriteParityPolicy,
306    pub passed: bool,
307    pub first_violation: Option<usize>,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum RewriteValueKind {
312    LogitsF32,
313    TokenIdsU32,
314}
315
316impl RewriteValueKind {
317    pub const fn as_str(self) -> &'static str {
318        match self {
319            Self::LogitsF32 => "logits-f32",
320            Self::TokenIdsU32 => "token-ids-u32",
321        }
322    }
323}
324
325impl RewriteParityReceipt {
326    pub fn bind_artifact_lock(mut self, artifact_lock: &[u8]) -> Self {
327        self.artifact_lock_sha256 = Some(hex_sha256(artifact_lock));
328        self
329    }
330
331    pub fn validate_for(&self, rewrite: &ExecutionRewrite) -> Result<(), String> {
332        if self.rewrite_id != rewrite.id
333            || self.surface != rewrite.surface
334            || self.implementation != rewrite.implementation
335            || self.plan_sha256 != rewrite.plan_sha256
336        {
337            return Err(format!(
338                "rewrite receipt identity does not match {} for plan {}",
339                rewrite.id, rewrite.plan_sha256
340            ));
341        }
342        if !rewrite.eligible() {
343            return Err(format!("rewrite {} is no longer eligible", rewrite.id));
344        }
345        if !self.passed {
346            return Err(format!("rewrite {} parity receipt failed", rewrite.id));
347        }
348        Ok(())
349    }
350
351    pub fn to_tsv(&self) -> String {
352        let mut output = String::new();
353        writeln!(output, "format\tmemra-rewrite-parity-v1").unwrap();
354        writeln!(
355            output,
356            "status\t{}",
357            if self.passed { "passed" } else { "failed" }
358        )
359        .unwrap();
360        writeln!(output, "rewrite\t{}", self.rewrite_id).unwrap();
361        writeln!(output, "surface\t{}", self.surface.as_str()).unwrap();
362        writeln!(output, "implementation\t{}", self.implementation).unwrap();
363        writeln!(
364            output,
365            "implementation_sha256\t{}",
366            self.implementation_sha256
367        )
368        .unwrap();
369        writeln!(output, "plan_sha256\t{}", self.plan_sha256).unwrap();
370        if let Some(hash) = self.artifact_lock_sha256.as_ref() {
371            writeln!(output, "artifact_lock_sha256\t{hash}").unwrap();
372        }
373        writeln!(output, "reference_sha256\t{}", self.reference_sha256).unwrap();
374        writeln!(output, "candidate_sha256\t{}", self.candidate_sha256).unwrap();
375        writeln!(output, "value_kind\t{}", self.value_kind.as_str()).unwrap();
376        writeln!(output, "values\t{}", self.values).unwrap();
377        writeln!(output, "max_abs\t{}", self.max_abs).unwrap();
378        writeln!(output, "max_rel\t{}", self.max_rel).unwrap();
379        writeln!(output, "reference_argmax\t{}", self.reference_argmax).unwrap();
380        writeln!(output, "candidate_argmax\t{}", self.candidate_argmax).unwrap();
381        writeln!(output, "atol\t{}", self.policy.max_abs).unwrap();
382        writeln!(output, "rtol\t{}", self.policy.max_rel).unwrap();
383        writeln!(output, "require_argmax\t{}", self.policy.require_argmax).unwrap();
384        writeln!(
385            output,
386            "first_violation\t{}",
387            self.first_violation
388                .map_or_else(|| "none".to_string(), |index| index.to_string())
389        )
390        .unwrap();
391        output
392    }
393}
394
395pub fn execution_rewrites(plan: &ModelPlan) -> Vec<ExecutionRewrite> {
396    let plan_sha256 = plan_sha256(plan);
397    let trunk = plan.trunk_operations();
398    let mut spec_operations = plan
399        .draft_operations()
400        .unwrap_or_else(|| vec![OperationKind::DraftPlan]);
401    spec_operations.extend(plan.trunk_operations());
402    let selections = [
403        (
404            "carried-prime.v1",
405            RewriteSurface::CarriedPrime,
406            CARRIED_PRIME,
407            trunk.clone(),
408            CARRIED_PRIME.trunk_capabilities(plan).batch,
409        ),
410        (
411            "decode-eager.v1",
412            RewriteSurface::DecodeEager,
413            NATIVE_EAGER,
414            trunk.clone(),
415            NATIVE_EAGER.trunk_capabilities(plan).batch,
416        ),
417        (
418            "decode-batch.v1",
419            RewriteSurface::DecodeBatch,
420            DECODE_BATCH,
421            trunk.clone(),
422            DECODE_BATCH.trunk_capabilities(plan).batch,
423        ),
424        (
425            "decode-graph.v1",
426            RewriteSurface::DecodeGraph,
427            DECODE_GRAPH,
428            trunk.clone(),
429            DECODE_GRAPH.trunk_capabilities(plan).cuda_graph,
430        ),
431        (
432            "mtp-spec.v1",
433            RewriteSurface::MtpSpec,
434            MTP_SPEC,
435            spec_operations.clone(),
436            MTP_SPEC.capabilities(plan).speculative,
437        ),
438        (
439            "glm5-spec.v1",
440            RewriteSurface::Glm5Spec,
441            GLM5_SPEC,
442            spec_operations,
443            GLM5_SPEC.capabilities(plan).speculative,
444        ),
445        (
446            "pipeline.v1",
447            RewriteSurface::Pipeline,
448            PIPELINE,
449            trunk,
450            PIPELINE.capabilities(plan).pipeline,
451        ),
452    ];
453    selections
454        .into_iter()
455        .map(
456            |(id, surface, manifest, canonical_operations, capability)| ExecutionRewrite {
457                id,
458                surface,
459                implementation: manifest.name,
460                plan_sha256: plan_sha256.clone(),
461                canonical_operations,
462                blockers: capability.blockers,
463            },
464        )
465        .collect()
466}
467
468fn plan_sha256(plan: &ModelPlan) -> String {
469    hex_sha256(format!("{plan:#?}\n").as_bytes())
470}
471
472fn f32_stream_sha256(values: &[f32]) -> String {
473    let mut bytes = Vec::with_capacity(values.len() * 4);
474    for value in values {
475        bytes.extend_from_slice(&value.to_bits().to_le_bytes());
476    }
477    hex_sha256(&bytes)
478}
479
480fn u32_stream_sha256(values: &[u32]) -> String {
481    let mut bytes = Vec::with_capacity(values.len() * 4);
482    for value in values {
483        bytes.extend_from_slice(&value.to_le_bytes());
484    }
485    hex_sha256(&bytes)
486}
487
488fn hex_sha256(bytes: &[u8]) -> String {
489    let digest = Sha256::digest(bytes);
490    digest.iter().map(|byte| format!("{byte:02x}")).collect()
491}
492
493fn is_sha256(value: &str) -> bool {
494    value.len() == 64
495        && value
496            .bytes()
497            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
498}
499
500fn stable_argmax(values: &[f32]) -> usize {
501    values
502        .iter()
503        .enumerate()
504        .max_by(|(left_index, left), (right_index, right)| {
505            left.total_cmp(right)
506                .then_with(|| right_index.cmp(left_index))
507        })
508        .map(|(index, _)| index)
509        .unwrap_or(0)
510}
511
512fn carried_prime_support(operation: OperationKind) -> OperationSupport {
513    let mut support = OperationSupport::none();
514    support.batch = matches!(
515        operation,
516        OperationKind::Embedding
517            | OperationKind::RmsNorm
518            | OperationKind::FullAttention
519            | OperationKind::GatedDeltaNet
520            | OperationKind::FusedAttentionGate
521            | OperationKind::DenseMlp
522            | OperationKind::SiluActivation
523            | OperationKind::SerialResidual
524            | OperationKind::KvState
525            | OperationKind::RecurrentState
526            | OperationKind::LogitsMask
527            | OperationKind::OutputProjection
528    );
529    support
530}
531
532pub const CARRIED_PRIME: KernelManifest =
533    KernelManifest::new("carried-prime-batch", carried_prime_support);
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
536pub enum DecodeBatchProgram {
537    Generic,
538    SlidingGatedMoe,
539    Gemma,
540}
541
542pub fn decode_batch_program(plan: &ModelPlan) -> DecodeBatchProgram {
543    let operations = plan.trunk_operations();
544    if operations.contains(&OperationKind::GemmaResidual)
545        || operations.contains(&OperationKind::GemmaParallelMoeResidual)
546    {
547        DecodeBatchProgram::Gemma
548    } else if operations.contains(&OperationKind::SlidingWindowAttention)
549        && operations.contains(&OperationKind::SeparateAttentionGate)
550        && operations.contains(&OperationKind::MoeMlp)
551        && operations.contains(&OperationKind::SigmoidRouter)
552    {
553        DecodeBatchProgram::SlidingGatedMoe
554    } else {
555        DecodeBatchProgram::Generic
556    }
557}
558
559/// Does this plan's residual topology have NO converted batched-decode arm?
560///
561/// `HyperConnections` carries `streams` parallel residual streams collapsed per layer; every
562/// batched path (`decode_step_batch*`, `prime_cache_batch`, `prime_graph_new`, the graph-capture
563/// and speculative entry points) runs a serial residual and refuses this topology at the engine
564/// boundary rather than computing a different model. The converted set is `forward`,
565/// `forward_last`, `prime_cache`, `decode_step` — all reachable from the per-session eager route.
566///
567/// This is the plan-level twin of the `Gemma` arm of [`decode_batch_program`]: the scheduler needs
568/// to know a model has no batched arm BEFORE it places the session, so the topology is read from
569/// the `ModelPlan` rather than re-derived from engine internals.
570pub fn decode_batch_unconverted(plan: &ModelPlan) -> bool {
571    plan.trunk_operations()
572        .contains(&OperationKind::HyperConnections)
573}
574
575pub fn gdn_dspark_compatible(plan: &ModelPlan) -> bool {
576    let operations = plan.trunk_operations();
577    operations.contains(&OperationKind::GatedDeltaNet)
578        && operations.contains(&OperationKind::FusedAttentionGate)
579}
580
581fn decode_graph_support(operation: OperationKind) -> OperationSupport {
582    let mut support = OperationSupport::none();
583    support.cuda_graph = matches!(
584        operation,
585        OperationKind::Embedding
586            | OperationKind::RmsNorm
587            | OperationKind::FullAttention
588            | OperationKind::GatedDeltaNet
589            | OperationKind::FusedAttentionGate
590            | OperationKind::DenseMlp
591            | OperationKind::MoeMlp
592            | OperationKind::SharedMlp
593            | OperationKind::SoftmaxRouter
594            | OperationKind::SigmoidRouter
595            | OperationKind::SiluActivation
596            | OperationKind::SerialResidual
597            | OperationKind::KvState
598            | OperationKind::RecurrentState
599            | OperationKind::LogitsMask
600            | OperationKind::OutputProjection
601    );
602    support
603}
604
605pub const DECODE_GRAPH: KernelManifest =
606    KernelManifest::new("decode-cuda-graph", decode_graph_support);
607
608/// PIPELINE (stage-split trunk + boundary state transport).
609///
610/// THE GLM5_NEXT BLOCK BELOW WAS ADDED BY A MERGE, AND THE MERGE IS WHY IT WAS MISSING.
611/// The plan-admission refusal in `hybrid.rs` ("pipeline placement is unsupported for plan
612/// operations ...") and the glm5_next PP-N program were developed on OPPOSITE SIDES of the
613/// `lane/glm53-flash-bringup` x `main` merge: the refusal landed on main, which carries no
614/// `glm5-*-ppn-gate` at all, while the gates that exercise stage-split glm5_next live only on the
615/// lane. So neither side was red alone and the first tree that could see it was the merge, where
616/// EVERY glm5.3-flash PP-N load refused before uploading a shard — including the PP4
617/// splits-13,26,39 posture that is the only demonstrated 1M-context configuration and that had
618/// run on box B the same day (`research/glm53-flash-bringup-20260827/1m-battery-20260901/`).
619/// Attribution, not blame: an operation-level admission gate is only as wide as its table, and a
620/// table cannot know about an architecture whose gates are on another branch.
621///
622/// WHAT THE ADDED CLASSES ARE, and what covers them (`research/.../bankfix-consol-20260901/`):
623/// the glm5_next trunk class — hc residual topology on every trunk layer, KDA and MLA(+kpool)
624/// mixers with their recurrent / latent-KV state, MoE+shared FFN, pre-clamped SwiGLU. Their PP
625/// receipts are `glm5-spec-ppn-gate` (n2/n3 even/split1/split3/asym x streams/overlap arms),
626/// `glm5-hyper-ppn-gate` (n2/n3/n4, incl. the shard0 and longer-prompt arms) and
627/// `glm5-hyper-batch-gate` (the B<=15 ladder at ppn 1/2/4) — the ladders that prove boundary
628/// transport carries THIS state across a stage cut, re-run on the merged tree over four real
629/// devices rather than one card's emulation.
630///
631/// Deliberately still absent, so every other family stays fail-closed here (no-generic-support
632/// law): `SharedSparseIndex` — the ladders' fixture plan carries `SparseIndex` and the refusal
633/// named only that, so admitting the shared variant too would be claiming coverage nothing ran; if
634/// a real artifact needs it the load refuses BY NAME, which is the visible outcome we want.
635/// `GatedDeltaNet` (qwen35/step35 recurrent mixers have no PP ladder), `CompressedMlaAttention`
636/// (the dsv4 class), `SoftmaxRouter`, `GemmaResidual`/`GemmaParallelMoeResidual` (gemma keeps its
637/// separately gated PP2 program, grandfathered in `hybrid.rs` by fence length), `LogitsSoftcap`.
638/// A family that wants stage splitting brings its own ladder; being architecturally adjacent to
639/// one that has one is not a receipt.
640fn pipeline_support(operation: OperationKind) -> OperationSupport {
641    let mut support = OperationSupport::none();
642    support.pipeline = matches!(
643        operation,
644        OperationKind::Embedding
645            | OperationKind::RmsNorm
646            | OperationKind::FullAttention
647            | OperationKind::SlidingWindowAttention
648            | OperationKind::SeparateAttentionGate
649            | OperationKind::DenseMlp
650            | OperationKind::MoeMlp
651            | OperationKind::SharedMlp
652            | OperationKind::SigmoidRouter
653            | OperationKind::SiluActivation
654            | OperationKind::SwiGluClampedActivation
655            | OperationKind::SerialResidual
656            | OperationKind::KvState
657            | OperationKind::SlidingKvState
658            | OperationKind::Mtp
659            | OperationKind::MtpFusion
660            | OperationKind::MtpHead
661            | OperationKind::LogitsMask
662            | OperationKind::OutputProjection
663            | OperationKind::PipelineBoundary
664            // ---- glm5_next trunk class (see the doc comment above for the covering ladders) ----
665            | OperationKind::KimiDeltaNet
666            | OperationKind::RecurrentState
667            | OperationKind::LatentMlaAttention
668            | OperationKind::SparseIndex
669            | OperationKind::LatentKvState
670            | OperationKind::HyperConnections
671            | OperationKind::SwiGluPreClampedActivation
672    );
673    support
674}
675
676pub const PIPELINE: KernelManifest =
677    KernelManifest::new("pipeline-state-transport", pipeline_support);
678
679fn decode_batch_support(operation: OperationKind) -> OperationSupport {
680    let mut support = OperationSupport::none();
681    support.batch = matches!(
682        operation,
683        OperationKind::Embedding
684            | OperationKind::RmsNorm
685            | OperationKind::FullAttention
686            | OperationKind::SlidingWindowAttention
687            | OperationKind::GatedDeltaNet
688            | OperationKind::FusedAttentionGate
689            | OperationKind::SeparateAttentionGate
690            | OperationKind::DenseMlp
691            | OperationKind::MoeMlp
692            | OperationKind::SharedMlp
693            | OperationKind::SoftmaxRouter
694            | OperationKind::SigmoidRouter
695            | OperationKind::SiluActivation
696            | OperationKind::GeluTanhActivation
697            | OperationKind::SwiGluClampedActivation
698            | OperationKind::SerialResidual
699            | OperationKind::GemmaResidual
700            | OperationKind::GemmaParallelMoeResidual
701            | OperationKind::KvState
702            | OperationKind::SlidingKvState
703            | OperationKind::RecurrentState
704            | OperationKind::LogitsSoftcap
705            | OperationKind::LogitsMask
706            | OperationKind::OutputProjection
707    );
708    support
709}
710
711pub const DECODE_BATCH: KernelManifest = KernelManifest::new("decode-batch", decode_batch_support);
712
713fn native_eager_support(operation: OperationKind) -> OperationSupport {
714    let mut support = OperationSupport::none();
715    support.batch = matches!(
716        operation,
717        OperationKind::Embedding
718            | OperationKind::RmsNorm
719            | OperationKind::FullAttention
720            | OperationKind::DenseMlp
721            | OperationKind::SiluActivation
722            | OperationKind::SerialResidual
723            | OperationKind::KvState
724            | OperationKind::LogitsSoftcap
725            | OperationKind::LogitsMask
726            | OperationKind::OutputProjection
727    );
728    support
729}
730
731pub const NATIVE_EAGER: KernelManifest = KernelManifest::new("native-eager", native_eager_support);
732
733fn mtp_spec_support(operation: OperationKind) -> OperationSupport {
734    let mut support = OperationSupport::none();
735    let common = matches!(
736        operation,
737        OperationKind::Embedding
738            | OperationKind::RmsNorm
739            | OperationKind::FullAttention
740            | OperationKind::SlidingWindowAttention
741            | OperationKind::GatedDeltaNet
742            | OperationKind::FusedAttentionGate
743            | OperationKind::SeparateAttentionGate
744            | OperationKind::DenseMlp
745            | OperationKind::MoeMlp
746            | OperationKind::SharedMlp
747            | OperationKind::SoftmaxRouter
748            | OperationKind::SigmoidRouter
749            | OperationKind::SiluActivation
750            | OperationKind::SwiGluClampedActivation
751            | OperationKind::SerialResidual
752            | OperationKind::KvState
753            | OperationKind::SlidingKvState
754            | OperationKind::RecurrentState
755            | OperationKind::LogitsMask
756            | OperationKind::OutputProjection
757    );
758    support.spec_draft = common
759        || matches!(
760            operation,
761            OperationKind::Mtp | OperationKind::MtpFusion | OperationKind::MtpHead
762        );
763    support.spec_verify = common;
764    support
765}
766
767pub const MTP_SPEC: KernelManifest = KernelManifest::new("mtp-spec", mtp_spec_support);
768
769/// glm5_next T-parallel speculative loop (lane/glm5-spec-routing, 2026-08-30): the gated
770/// program is `glm_spec.rs` — draft K through the embedded MLA-mixer MTP block
771/// (`mtp_head_forward_mla_cached`), verify in ONE t=K+1 walk over the HyperConnections
772/// trunk in the batched-decode kernel classes (`glm5_verify_rows`), greedy or
773/// rejection-sampled accept, per-plane rollback (`glm5_verify_rollback`).
774///
775/// The table below names EXACTLY the operation classes those gates cover — the
776/// glm5_next class (hc residual topology on every trunk layer + KDA/MLA(+kpool) mixers,
777/// MoE/dense FFN) with a serial-residual MLA-mixer MoE MTP block. Deliberately absent, so
778/// every other family stays fail-closed here (no-generic-support law; each carries its own
779/// gated program or none):
780///   * `FullAttention`/`SlidingWindowAttention`/`GatedDeltaNet`/`KvState`/... — the qwen35 /
781///     step35 / gemma classes route through `MTP_SPEC`, never here;
782///   * `CompressedMlaAttention` — the dsv4 class (its own dspark program);
783///   * `SerialResidual`/`Gemma*Residual` as TRUNK residuals — `glm5_verify_rows` requires
784///     the hc topology (the walk's `hyper::pre_exact`/`post` glue IS the program), so a
785///     non-hc trunk must block. `SerialResidual` is spec_draft-only: the NextN block
786///     carries no hc_* tensors (mtp-draft LANE.md) and is serial by plan.
787///   * `DenseMlp` on the DRAFT side — `mtp_head_forward_mla_cached` refuses Dense-FFN MTP
788///     blocks by name; the trunk's dense L0 is in the gated walk (`hyper_ffn_branch_batch`).
789fn glm5_spec_support(operation: OperationKind) -> OperationSupport {
790    let mut support = OperationSupport::none();
791    let common = matches!(
792        operation,
793        OperationKind::Embedding
794            | OperationKind::RmsNorm
795            | OperationKind::LatentMlaAttention
796            | OperationKind::SparseIndex
797            | OperationKind::SharedSparseIndex
798            | OperationKind::KimiDeltaNet
799            | OperationKind::RecurrentState
800            | OperationKind::LatentKvState
801            | OperationKind::MoeMlp
802            | OperationKind::SigmoidRouter
803            | OperationKind::SharedMlp
804            | OperationKind::SwiGluPreClampedActivation
805            | OperationKind::OutputProjection
806    );
807    support.spec_verify = common
808        || matches!(
809            operation,
810            OperationKind::HyperConnections | OperationKind::DenseMlp
811        );
812    support.spec_draft = common
813        || matches!(
814            operation,
815            OperationKind::Mtp
816                | OperationKind::MtpFusion
817                | OperationKind::MtpHead
818                | OperationKind::SerialResidual
819        );
820    support
821}
822
823pub const GLM5_SPEC: KernelManifest = KernelManifest::new("glm5-spec", glm5_spec_support);
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828    use crate::config::{HfConfig, ModelConfig};
829
830    fn plan(json: &str) -> ModelPlan {
831        ModelPlan::compile(&ModelConfig::from_hf(&HfConfig::parse(json))).unwrap()
832    }
833
834    #[test]
835    fn manifest_reports_operation_blockers_instead_of_model_names() {
836        let dense = plan(
837            r#"{"model_type":"qwen3","num_hidden_layers":2,"hidden_size":64,
838            "num_attention_heads":2,"num_key_value_heads":1,"head_dim":32,
839            "intermediate_size":128,"vocab_size":16,"max_position_embeddings":128}"#,
840        );
841        assert!(CARRIED_PRIME.trunk_capabilities(&dense).batch.supported);
842        assert!(DECODE_BATCH.trunk_capabilities(&dense).batch.supported);
843        assert!(DECODE_GRAPH.trunk_capabilities(&dense).cuda_graph.supported);
844
845        let gemma = plan(
846            r#"{"model_type":"gemma4","num_hidden_layers":2,"hidden_size":64,
847            "num_attention_heads":2,"num_key_value_heads":1,"head_dim":32,
848            "global_head_dim":32,"intermediate_size":128,"vocab_size":16,
849            "max_position_embeddings":128,"sliding_window":64,
850            "layer_types":["sliding_attention","full_attention"],
851            "rope_parameters":{"full_attention":{"rope_theta":10000},
852            "sliding_attention":{"rope_theta":10000}}}"#,
853        );
854        let capability = CARRIED_PRIME.trunk_capabilities(&gemma).batch;
855        assert!(!capability.supported);
856        assert!(
857            capability
858                .blockers
859                .contains(&OperationKind::SlidingWindowAttention)
860        );
861        assert!(capability.blockers.contains(&OperationKind::GemmaResidual));
862        assert_eq!(decode_batch_program(&gemma), DecodeBatchProgram::Gemma);
863        assert!(!DECODE_GRAPH.trunk_capabilities(&gemma).cuda_graph.supported);
864
865        let mut sliding_gated_moe = plan(
866            r#"{"model_type":"qwen3_moe","num_hidden_layers":2,"hidden_size":64,
867            "num_attention_heads":2,"num_key_value_heads":1,"head_dim":32,
868            "intermediate_size":128,"vocab_size":16,"max_position_embeddings":128,
869            "num_experts":4,"num_experts_per_tok":2,"moe_intermediate_size":32}"#,
870        );
871        let crate::model_plan::AttentionPlan::Full(mut attention) =
872            sliding_gated_moe.layers[0].attention.clone()
873        else {
874            unreachable!()
875        };
876        attention.output_gate = crate::config::AttentionGateKind::SeparateHead;
877        sliding_gated_moe.layers[0].attention = crate::model_plan::AttentionPlan::SlidingWindow {
878            attention,
879            window: 64,
880        };
881        let crate::model_plan::MlpPlan::Moe(moe) = &mut sliding_gated_moe.layers[0].mlp else {
882            unreachable!()
883        };
884        moe.router = crate::model_plan::RouterPlan::Sigmoid {
885            normalize_selected: true,
886            scaling_factor: 1.0,
887            selection_bias: false,
888        };
889        assert_eq!(
890            decode_batch_program(&sliding_gated_moe),
891            DecodeBatchProgram::SlidingGatedMoe
892        );
893        assert!(
894            !DECODE_GRAPH
895                .trunk_capabilities(&sliding_gated_moe)
896                .cuda_graph
897                .supported
898        );
899        assert_eq!(decode_batch_program(&dense), DecodeBatchProgram::Generic);
900        assert_eq!(
901            MTP_SPEC.capabilities(&dense).speculative.blockers,
902            vec![OperationKind::DraftPlan]
903        );
904
905        let qwen35 = crate::model_packs::by_alias("qwen35")
906            .unwrap()
907            .compile_tiny_plan()
908            .unwrap();
909        assert!(MTP_SPEC.capabilities(&qwen35).speculative.supported);
910
911        let dsv4 = crate::model_packs::by_alias("deepseek_v4_dspark")
912            .unwrap()
913            .compile_tiny_plan()
914            .unwrap();
915        let dsv4_batch = DECODE_BATCH.trunk_capabilities(&dsv4).batch;
916        assert!(!dsv4_batch.supported);
917        assert!(
918            dsv4_batch
919                .blockers
920                .contains(&OperationKind::CompressedMlaAttention)
921        );
922        assert!(!MTP_SPEC.capabilities(&dsv4).speculative.supported);
923
924        let root = std::env::temp_dir().join(format!(
925            "memra-plan-backend-external-draft-{}",
926            std::process::id()
927        ));
928        std::fs::create_dir_all(&root).unwrap();
929        let trunk_path = root.join("trunk.gguf");
930        let draft_path = root.join("draft.gguf");
931        crate::micro_gguf::write_step35_meta_only(&trunk_path).unwrap();
932        crate::micro_gguf::write_step35_mtp_meta_only(&draft_path).unwrap();
933        let trunk_cfg = ModelConfig::from_gguf(&crate::GgufFile::open(&trunk_path).unwrap());
934        let draft_cfg = ModelConfig::from_gguf(&crate::GgufFile::open(&draft_path).unwrap());
935        let mut external = crate::model_packs::for_config(&trunk_cfg)
936            .unwrap()
937            .compile_plan(&trunk_cfg)
938            .unwrap();
939        let draft = crate::model_packs::for_config(&draft_cfg)
940            .unwrap()
941            .compile_plan(&draft_cfg)
942            .unwrap();
943        assert_eq!(
944            external.draft_source,
945            crate::model_plan::DraftSourcePlan::ExternalArtifact
946        );
947        external.attach_external_draft(&draft).unwrap();
948        assert!(MTP_SPEC.capabilities(&external).speculative.supported);
949        std::fs::remove_dir_all(root).unwrap();
950    }
951
952    #[test]
953    fn rewrite_receipts_bind_plan_program_binary_and_outputs() {
954        let dense = plan(
955            r#"{"model_type":"qwen3","num_hidden_layers":2,"hidden_size":64,
956            "num_attention_heads":2,"num_key_value_heads":1,"head_dim":32,
957            "intermediate_size":128,"vocab_size":16,"max_position_embeddings":128}"#,
958        );
959        let rewrites = execution_rewrites(&dense);
960        let batch = rewrites
961            .iter()
962            .find(|rewrite| rewrite.surface == RewriteSurface::DecodeBatch)
963            .unwrap();
964        assert!(batch.eligible());
965        let spec = rewrites
966            .iter()
967            .find(|rewrite| rewrite.surface == RewriteSurface::MtpSpec)
968            .unwrap();
969        assert_eq!(spec.blockers, vec![OperationKind::DraftPlan]);
970
971        let policy = RewriteParityPolicy {
972            max_abs: 0.01,
973            max_rel: 0.01,
974            require_argmax: true,
975        };
976        let implementation = "00".repeat(32);
977        let artifact_lock = b"format_version=2\nfamily=qwen3\n";
978        let receipt = batch
979            .verify_logits(
980                &implementation,
981                &[0.0, 1.0, -1.0],
982                &[0.0, 1.001, -1.001],
983                policy,
984            )
985            .unwrap()
986            .bind_artifact_lock(artifact_lock);
987        assert!(receipt.passed);
988        receipt.validate_for(batch).unwrap();
989        let tsv = receipt.to_tsv();
990        assert!(tsv.contains("rewrite\tdecode-batch.v1"));
991        assert!(tsv.contains(&format!("plan_sha256\t{}", batch.plan_sha256)));
992        assert!(tsv.contains(&format!("implementation_sha256\t{implementation}")));
993
994        let root = std::env::temp_dir().join(format!(
995            "memra-rewrite-qualification-{}",
996            std::process::id()
997        ));
998        let receipts = root.join("rewrite-receipts");
999        std::fs::create_dir_all(&receipts).unwrap();
1000        std::fs::write(root.join("artifact.lock"), artifact_lock).unwrap();
1001        std::fs::write(receipts.join("decode-batch.v1.tsv"), &tsv).unwrap();
1002        std::fs::write(
1003            root.join("rewrite-receipts.tsv"),
1004            format!(
1005                "rewrite\tplan_sha256\treceipt_sha256\tstatus\n\
1006                 decode-batch.v1\t{}\t{}\tpassed\n",
1007                batch.plan_sha256,
1008                hex_sha256(tsv.as_bytes())
1009            ),
1010        )
1011        .unwrap();
1012        let qualifications = RewriteQualifications::load(&root, &dense).unwrap();
1013        assert!(qualifications.allows(RewriteSurface::DecodeBatch));
1014        assert!(!qualifications.allows(RewriteSurface::DecodeGraph));
1015        std::fs::write(receipts.join("decode-batch.v1.tsv"), "tampered").unwrap();
1016        assert!(RewriteQualifications::load(&root, &dense).is_err());
1017        std::fs::remove_dir_all(root).unwrap();
1018
1019        let failed = batch
1020            .verify_logits(
1021                &implementation,
1022                &[0.0, 1.0, -1.0],
1023                &[2.0, 1.0, -1.0],
1024                policy,
1025            )
1026            .unwrap();
1027        assert!(!failed.passed);
1028        assert!(failed.validate_for(batch).is_err());
1029
1030        let other = plan(
1031            r#"{"model_type":"qwen3","num_hidden_layers":3,"hidden_size":64,
1032            "num_attention_heads":2,"num_key_value_heads":1,"head_dim":32,
1033            "intermediate_size":128,"vocab_size":16,"max_position_embeddings":128}"#,
1034        );
1035        let other_batch = execution_rewrites(&other)
1036            .into_iter()
1037            .find(|rewrite| rewrite.surface == RewriteSurface::DecodeBatch)
1038            .unwrap();
1039        assert!(receipt.validate_for(&other_batch).is_err());
1040
1041        let qwen35 = crate::model_packs::by_alias("qwen35")
1042            .unwrap()
1043            .compile_tiny_plan()
1044            .unwrap();
1045        let qwen35_spec = execution_rewrites(&qwen35)
1046            .into_iter()
1047            .find(|rewrite| rewrite.surface == RewriteSurface::MtpSpec)
1048            .unwrap();
1049        assert!(qwen35_spec.eligible());
1050    }
1051}
1052
1053/// glm5-spec class matrix (lane/glm5-spec-routing, 2026-08-30): the GLM5_SPEC manifest must
1054/// declare speculative support for EXACTLY the glm5_next class with an MTP block present, and
1055/// stay fail-closed everywhere else — including for glm5_next under the MTP_SPEC manifest
1056/// (the qwen program), so the two spec programs can never both claim one plan.
1057#[cfg(test)]
1058mod glm5_spec_class_matrix {
1059    use super::*;
1060    use crate::model_plan::OperationKind;
1061
1062    /// The tparallel-verify / mtp-draft gate fixture family's config (4 trunk layers:
1063    /// KDA + DSA(MLA+kpool) alternating, dense L0 + sigmoid noaux_tc MoE, mHC streams)
1064    /// plus `nextn` MTP block(s). The REAL GLM-5.3-Flash text_config carries the same
1065    /// operation classes (silu+swiglu_limit -> SwiGluPreClamped, sigmoid router, shared
1066    /// expert, dense first layers, hc residuals, 1 NextN block).
1067    fn glm5_plan(nextn: u32) -> crate::model_plan::ModelPlan {
1068        let json = format!(
1069            r#"{{
1070          "model_type": "glm5_next_text", "num_hidden_layers": 4,
1071          "num_nextn_predict_layers": {nextn},
1072          "hidden_size": 128, "intermediate_size": 64, "vocab_size": 32,
1073          "max_position_embeddings": 512, "rms_norm_eps": 1e-05, "hidden_act": "silu",
1074          "swiglu_limit": 10.0, "tie_word_embeddings": true, "hc_mult": 4, "hc_eps": 1e-06,
1075          "hc_sinkhorn_iters": 20, "mhc": true,
1076          "layer_types": ["linear_attention", "deepseek_sparse_attention",
1077                          "linear_attention", "deepseek_sparse_attention"],
1078          "mlp_layer_types": ["dense", "sparse", "sparse", "sparse"],
1079          "first_k_dense_replace": 1, "indexer_types": ["full", "full", "full", "full"],
1080          "linear_attn_config": {{"num_heads": 1, "head_dim": 128, "short_conv_kernel_size": 4,
1081            "gate_lower_bound": -5.0, "kda_layers": [0, 2], "full_attn_layers": [1, 3]}},
1082          "num_attention_heads": 2, "num_key_value_heads": 2, "q_lora_rank": 16,
1083          "kv_lora_rank": 16, "qk_head_dim": 16, "qk_nope_head_dim": 16, "qk_rope_head_dim": 0,
1084          "v_head_dim": 16, "mla_use_nope": true, "index_n_heads": 1, "index_head_dim": 8,
1085          "index_topk": 8, "index_kpool": 4, "index_kpool_always_select_tail": true,
1086          "index_kpool_compress": true, "indexer_rope_interleave": true,
1087          "index_share_for_mtp_iteration": true, "n_routed_experts": 4, "num_experts_per_tok": 2,
1088          "moe_intermediate_size": 64, "n_shared_experts": 1, "scoring_func": "sigmoid",
1089          "topk_method": "noaux_tc", "routed_scaling_factor": 2.5, "norm_topk_prob": true,
1090          "n_group": 1, "topk_group": 1, "head_dim": 0, "attention_bias": false,
1091          "moe_router_dtype": "float32", "dtype": "bfloat16"
1092        }}"#
1093        );
1094        let config = crate::config::ModelConfig::from_hf(&crate::config::HfConfig::parse(&json));
1095        crate::model_packs::for_config(&config)
1096            .unwrap()
1097            .compile_plan(&config)
1098            .unwrap()
1099    }
1100
1101    #[test]
1102    fn glm5_with_mtp_block_is_supported_and_the_rewrite_row_is_eligible() {
1103        let plan = glm5_plan(1);
1104        let capability = GLM5_SPEC.capabilities(&plan).speculative;
1105        assert!(
1106            capability.supported,
1107            "glm5_next + MTP block must be the supported class; blockers: {:?}",
1108            capability.blockers
1109        );
1110        let rewrite = execution_rewrites(&plan)
1111            .into_iter()
1112            .find(|rewrite| rewrite.surface == RewriteSurface::Glm5Spec)
1113            .unwrap();
1114        assert!(
1115            rewrite.eligible(),
1116            "glm5-spec.v1 must be eligible for this plan"
1117        );
1118        // A sealed bundle for this plan now OWES a glm5-spec receipt: the surface is
1119        // eligible, so `RewriteQualifications::allows(Glm5Spec)` is false until the
1120        // real-artifact qualification lane banks one — fail-closed for production bundles.
1121        assert_eq!(rewrite.id, "glm5-spec.v1");
1122    }
1123
1124    #[test]
1125    fn glm5_without_an_mtp_block_fails_closed_on_the_draft_plan() {
1126        let plan = glm5_plan(0);
1127        let capability = GLM5_SPEC.capabilities(&plan).speculative;
1128        assert!(!capability.supported);
1129        assert_eq!(
1130            capability.blockers,
1131            vec![OperationKind::DraftPlan],
1132            "no NextN block = no draft program; the blocker must name it"
1133        );
1134    }
1135
1136    #[test]
1137    fn the_two_spec_programs_never_both_claim_one_plan() {
1138        // glm5_next stays UNSUPPORTED under the qwen-class MTP_SPEC manifest (its hc/KDA/MLA
1139        // classes are deliberately absent there) — the worker's mtp_spec_capable therefore
1140        // remains false for glm5 plans and the qwen spec route can never take them.
1141        let glm5 = glm5_plan(1);
1142        let mtp_spec = MTP_SPEC.capabilities(&glm5).speculative;
1143        assert!(!mtp_spec.supported);
1144        assert!(
1145            mtp_spec.blockers.contains(&OperationKind::HyperConnections),
1146            "the hc topology must be among MTP_SPEC's named blockers: {:?}",
1147            mtp_spec.blockers
1148        );
1149
1150        // And the qwen35 class stays UNSUPPORTED under GLM5_SPEC (Full/GDN/KvState/Serial
1151        // trunk classes absent from its table) — qwen routing is unchanged by this lane.
1152        let qwen35 = crate::model_packs::by_alias("qwen35")
1153            .unwrap()
1154            .compile_tiny_plan()
1155            .unwrap();
1156        assert!(MTP_SPEC.capabilities(&qwen35).speculative.supported);
1157        let qwen_glm5 = GLM5_SPEC.capabilities(&qwen35).speculative;
1158        assert!(!qwen_glm5.supported);
1159        assert!(
1160            qwen_glm5.blockers.contains(&OperationKind::SerialResidual),
1161            "a serial-residual trunk must block the hc walk program: {:?}",
1162            qwen_glm5.blockers
1163        );
1164    }
1165
1166    #[test]
1167    fn foreign_state_classes_fail_closed_under_glm5_spec() {
1168        // dsv4: CompressedMlaAttention class — its spec program is dspark, never this walk.
1169        let dsv4 = crate::model_packs::by_alias("deepseek_v4_dspark")
1170            .unwrap()
1171            .compile_tiny_plan()
1172            .unwrap();
1173        let dsv4_cap = GLM5_SPEC.capabilities(&dsv4).speculative;
1174        assert!(!dsv4_cap.supported);
1175        assert!(
1176            dsv4_cap
1177                .blockers
1178                .contains(&OperationKind::CompressedMlaAttention)
1179        );
1180
1181        // Dense qwen3: no draft plan AND a foreign trunk class.
1182        let dense = crate::model_packs::by_alias("qwen3")
1183            .unwrap()
1184            .compile_tiny_plan()
1185            .unwrap();
1186        assert!(!GLM5_SPEC.capabilities(&dense).speculative.supported);
1187    }
1188}