spark_model/layers/glm5next_layer/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! `Glm5NextLayer` β€” the composite GLM-5.3 decoder layer that implements `TransformerLayer`.
4//!
5//! This is the piece that makes the model *bind*. Everything it dispatches to already existed
6//! and was numerically gated in Slices 1–13; what did not exist was a single type the loader can
7//! return 45 of, dispatching mixer (KDA | DSA) + MLP (dense | routed MoE) + mHC in the order
8//! [`crate::layers::glm5next_skeleton`] records as data.
9//!
10//! # The residual plan, executed
11//!
12//! Per site, exactly `ResidualStep`'s order:
13//!
14//! ```text
15//! layer 0 only:  hc_expand(hidden) -> streams        [hc_mult, hidden] FP32 highway
16//!
17//! attention site:  hc_pre(streams) -> y, post, comb
18//!                  rms_norm_vanilla(y, input_layernorm) -> normed
19//!                  mixer(normed) -> block_out
20//!                  hc_post(block_out, residual = streams, post, comb) -> streams
21//!
22//! FFN site:        the same, with post_attention_layernorm and the MLP
23//!
24//! last layer:    hc_head_mean(streams) -> hidden     UNWEIGHTED mean, no parameters
25//! ```
26//!
27//! πŸͺ€ **`hc_pre` does not modify `streams`.** That is what makes the skeleton's
28//! `ResidualStep::SaveResidual` free here β€” `hc_post` reads the same buffer as its residual and
29//! writes back over it. Snapshotting is only needed if something overwrites the highway between
30//! the two calls; nothing here does, and the ordering below is the guard.
31//!
32//! # πŸͺ€ The traps this file holds
33//!
34//! * **GLM's norms are PLAIN RMSNorm.** `rms_norm_vanilla` is `x * rms * w`; the other
35//!   `rms_norm` is `x * rms * (1 + w)`. Identical signatures, identical shapes, and picking the
36//!   wrong one is silent. Every norm here takes the vanilla entry point.
37//! * **The mHC head collapse is an UNWEIGHTED MEAN.** GLM's `Glm5NextTextHyperHead` has no
38//!   parameters and the checkpoint carries zero `hc_head` tensors, unlike DeepSeek-V4's learned
39//!   sigmoid-weighted sum. Reaching for `ops::hc_head` would look for weights that do not exist.
40//! * **The highway is indexed by TOKEN.** Prefill is overridden rather than left to the trait's
41//!   sequential default, because that default runs every token through layer 0 before layer 1 β€”
42//!   which with a single-slot highway would leave only the LAST token's streams alive. See
43//!   `Glm5NextLayer::prefill`.
44//! * **Both MLP arms leave a PARTIAL SUM** whenever TP or EP is on. The single `all_reduce` at
45//!   the end of the FFN site covers both, and it must happen *before* `hc_post` mixes the output
46//!   back into the highway.
47
48use std::sync::Arc;
49
50use anyhow::{Context, Result, bail};
51use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
52use spark_runtime::kernel_args::KernelLaunch;
53use spark_runtime::kv_cache::PagedKvCache;
54
55use crate::layer::{ForwardContext, LayerState, SsmLayerState, TransformerLayer};
56use crate::layers::glm5next_dsa::layer::Glm5NextDsaLayer;
57use crate::layers::glm5next_dsa::state::Glm5NextDsaState;
58use crate::layers::glm5next_kda::{
59    Glm5NextKdaConfig, Glm5NextKdaLayer, Glm5NextKdaWorkspace, KdaSeqState,
60};
61use crate::layers::glm5next_mlp::forward::{Glm5NextMlpWorkspace, forward_dense, forward_moe};
62use crate::layers::glm5next_mlp::weights::{Glm5NextDenseMlpWeights, Glm5NextMoeWeights};
63use crate::layers::glm5next_mlp::{Glm5NextMlpConfig, Glm5NextMlpKernels};
64// πŸͺ€ Through `ops`'s glob re-export, and by the GLM-prefixed names only: `ops` also exports
65// DeepSeek-V4's `hc_pre`/`hc_post`, which use a different mixing law and a different weight set.
66// The rename is what makes reaching for the wrong one a compile error instead of a silent
67// architecture swap.
68use crate::layers::ops::{
69    Glm5NextMhcKernels, Glm5NextMhcSiteWeights, glm_hc_expand, glm_hc_post, glm_hc_pre,
70    hc_head_mean,
71};
72
73pub mod state;
74
75pub mod profile;
76pub use state::alloc_kda_ssm_state;
77
78/// Which mixer this layer runs. Both halves already exist and are GPU-gated; this enum is the
79/// dispatch, not new math.
80///
81/// πŸͺ€ KDA's `decode`/`prefill` are **inherent** methods with their own signature, not the
82/// `TransformerLayer` ones β€” they take a `&KdaSeqState` and a workspace and leave the result in
83/// `ws.final_out`. DSA's `decode` *is* the trait method and writes its result back into the
84/// buffer it was handed. The two conventions differ; this is where they are reconciled.
85pub enum Glm5NextMixer {
86    Kda {
87        layer: Box<Glm5NextKdaLayer>,
88        /// Shared across every KDA layer β€” all 34 have identical geometry, so one workspace
89        /// serves them all. `Arc` because the layers are independent owners.
90        ws: Arc<Glm5NextKdaWorkspace>,
91        cfg: Glm5NextKdaConfig,
92    },
93    Dsa(Box<Glm5NextDsaLayer>),
94}
95
96/// Which MLP this layer runs. Layers `0..first_k_dense_replace` are dense; the rest route.
97pub enum Glm5NextMlpSite {
98    Dense(Glm5NextDenseMlpWeights),
99    Moe(Box<Glm5NextMoeWeights>),
100}
101
102/// This layer's hyper-connection: both sites' weights plus the kernels and the two scalars.
103pub struct Glm5NextMhc {
104    pub kernels: Glm5NextMhcKernels,
105    pub attn: Glm5NextMhcSiteWeights,
106    pub ffn: Glm5NextMhcSiteWeights,
107    pub hc_mult: usize,
108    pub sinkhorn_iters: usize,
109    pub hc_eps: f32,
110}
111
112/// One bound GLM-5.3 decoder layer.
113pub struct Glm5NextLayer {
114    pub layer_idx: usize,
115    pub mixer: Glm5NextMixer,
116    pub mlp: Glm5NextMlpSite,
117    pub mlp_cfg: Glm5NextMlpConfig,
118    pub mlp_kernels: Glm5NextMlpKernels,
119    /// πŸ”΄ SHARED, ONE allocation for the whole stack β€” not one per layer. This scratch scales
120    /// with the prefill sub-chunk width (β‰ˆ46 MB at 256 rows, β‰ˆ211 MB at 1024), and 45 private
121    /// copies of it are what made a wide-prefill LOAD dip under the host-memory guard's floor
122    /// (ANOMALIES A124/A127). It is pure per-call scratch on ONE stream β€” every buffer is
123    /// written before it is read inside a single `mlp_forward`, and nothing in it survives the
124    /// call β€” so the layers can share it exactly the way `Glm5NextKdaWorkspace` already does.
125    /// `AVAROK_GLM_MLP_WS_SHARED=0` restores one workspace per layer.
126    pub mlp_ws: Arc<Glm5NextMlpWorkspace>,
127    /// `None` only for a layer with no hyper-connection β€” i.e. the MTP layer, which carries zero
128    /// `hc_*` tensors. Every text layer has one.
129    pub mhc: Option<Glm5NextMhc>,
130    /// `input_layernorm.weight` / `post_attention_layernorm.weight`, both plain RMSNorm.
131    pub input_norm: DevicePtr,
132    pub post_attn_norm: DevicePtr,
133    /// πŸͺ€ `rms_norm_vanilla`, never `rms_norm`. See the module header.
134    pub rms_norm_k: KernelHandle,
135    /// `bf16_add_inplace`, the residual add the MTP layer needs and the text layers do not:
136    /// a text layer's residual lives in the mHC highway and `hc_post` folds the block output
137    /// into it. `0` on a target without the kernel, which the MTP path refuses.
138    pub add_k: KernelHandle,
139    pub rms_eps: f32,
140    pub hidden: usize,
141    /// πŸ”΄ Whether the MIXER output is a partial sum. Both mixers end in a **row-parallel**
142    /// `o_proj` (`KdaShard::ChannelCols` / `DsaShard::HeadCols`), so at TP>1 each rank holds
143    /// only part of the attention output and it must be all-reduced **before** `hc_post` folds
144    /// it into the highway. Reducing after would mix a half-answer into every later layer's
145    /// residual stream; not reducing at all is a plausible, wrong output with no shape error.
146    pub mixer_all_reduce: bool,
147    /// Expand the highway here. True for layer 0 only.
148    pub is_first: bool,
149    /// Collapse the highway here. True for the last TEXT layer only.
150    pub is_last: bool,
151}
152
153/// Tokens per batched prefill sub-chunk β€” the width `Glm5NextLayer::prefill` hands
154/// [`Glm5NextLayer::forward_k`].
155///
156/// πŸ”΄ **16, and the ceiling is still a KERNEL boundary, not a bandwidth knee.** Every dense
157/// projection on this path goes through [`ops::dense_mm_bf16`], whose batched-GEMV arm
158/// (`dense_gemv_bf16_batchm`) is **bit-identical to M serial GEMVs** and stops at
159/// `DENSE_GEMV_BATCHM_MAX_M`. Past it the same call falls to the tile GEMM, which both
160/// reassociates (so prefill stops being bit-identical to the per-token walk) and is the slower
161/// kernel at these widths β€” Atlas measured it 3.6x slower than the batched GEMV at M <= 8.
162///
163/// πŸ”΄ WIDENED 8 -> 16 (2026-09-02). The A65 measurement below β€” "R = 32 is no faster" β€” was
164/// TRUE AND MISATTRIBUTED. R = 32 lost because it left the batched GEMV for the tile GEMM, not
165/// because row batching stops paying. With the tier itself widened to 16, the same 12 GLM
166/// prefill shapes cost **1.36-1.98x less per token at M = 16 than at M = 8** with cold weights
167/// (11 of 12; shallow-K N4096 K128 is the one loser at 0.77x), worth a modelled **-17.6 s of a
168/// 173.9 s 9K TTFT**, bit-identical (`scripts/glm53-dense-bf16/bench_m16.cu`, spark-bench).
169///
170/// πŸͺ€ The routed MoE does NOT follow the width up. `glm5next_mlp::forward::forward_moe` splits a
171/// wider row group into even sub-groups of at most `MOE_ROW_BATCH_MAX_ROWS` β€” exact, because a
172/// row's expert sum depends only on its own top-k, never on which rows share the sweep β€” so the
173/// routed experts still amortize over 8 rows, not 16. Widening THAT is a separate and unmeasured
174/// question: the union kernel is a single block with an O(T^3) scan, and the tier's register
175/// cost was already 80 at R = 8.
176///
177/// πŸ”΄ MEASURED 2026-08-31, one image, one control, ~1,950-token prompt: control TTFT 125.3 s;
178/// R = 8 **43.4 s (2.89x) and byte-identical on all four probes**; R = 32 44.3 / 51.8 s β€” no
179/// faster, and it moves two of the four completions. The per-token-bytes model that first
180/// picked 32 (predicting 4.5x at R = 32 against 3.0x at R = 8) is REFUTED as a width law: it
181/// modelled weight traffic only, and above R = 8 the traffic saved is handed to a slower kernel.
182/// The routed experts do not amortize past R = 4 either way (`forward_moe`'s union arm caps
183/// there), so R = 8 takes the whole available win. ANOMALIES A65.
184pub(crate) const PREFILL_ROWS: usize = 16;
185
186/// Route GLM prefill's KDA mixer through the chunked scan instead of the per-token
187/// recurrent walk. `AVAROK_GLM_KDA_CHUNK_PREFILL=1` to enable.
188///
189/// Default OFF while it is measured: the chunked scan is NOT bit-identical to the
190/// recurrent walk, so this is a numerics change as well as a speed one, and the default
191/// does not move without evidence on both.
192fn kda_chunk_prefill() -> bool {
193    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
194    *E.get_or_init(|| {
195        let on = std::env::var("AVAROK_GLM_KDA_CHUNK_PREFILL").as_deref() == Ok("1");
196        if on {
197            tracing::warn!(
198                "AVAROK_GLM_KDA_CHUNK_PREFILL=1 - GLM prefill KDA uses the CHUNKED scan \
199                 (kda_chunk_prepare + kda_chunk_scan). Not bit-identical to the per-token \
200                 recurrent walk."
201            );
202        }
203        on
204    })
205}
206
207/// Route GLM projections WIDER than the bit-exact tier through cuBLASLt BF16 instead of
208/// the scalar tile GEMM. Shared by the KDA and DSA blocks.
209///
210/// πŸ”΄ DEFAULT ON. `AVAROK_GLM_CUBLAS_PROJ=0` is the kill-switch.
211///
212/// `dense_gemm_bf16` is, by its own doc, a "scalar strict-order BF16 GEMM, no
213/// reassociation" β€” correctness-first, one thread per output element, not a tensor-core
214/// kernel. With `kda_mixer` split three ways it was the largest leaf in prefill by a wide
215/// margin: `kda_front` 24.4% + `kda_back` 7.7% + `dsa_proj` 14.9% = 47% of prefill in
216/// projections alone, against the per-token KDA recurrence β€” the assumed culprit β€” at
217/// 4.6%. At 256 rows `kda_front` cost 19.8 ms per layer-call to move ~268 MB of weights,
218/// ~20x off a 273 GB/s part.
219///
220/// πŸͺ€ The M threshold is a NUMERICS boundary, not a tuning knob. `dense_mm_bf16` sends
221/// M=1 to the GEMV and M=2..=16 to the batched single-sweep kernel; both accumulate per
222/// row in FP32 in a fixed K order and are BIT-IDENTICAL. Only above
223/// `DENSE_GEMV_BATCHM_MAX_M` does it fall to the tile GEMM, which already reassociates.
224/// So this replaces one non-bit-exact path with another and leaves every bit-exact tier
225/// alone: decode is M=1 and a speculative verify is M<=16, so neither can reach here, and
226/// a verify still matches what decode produced.
227pub(crate) fn cublas_wide_proj() -> bool {
228    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
229    *E.get_or_init(|| {
230        let on = std::env::var("AVAROK_GLM_CUBLAS_PROJ").as_deref() != Ok("0");
231        tracing::warn!(
232            "AVAROK_GLM_CUBLAS_PROJ: wide GLM projections (M > {}) use {}",
233            crate::layers::ops::DENSE_GEMV_BATCHM_MAX_M,
234            if on {
235                "cuBLASLt BF16"
236            } else {
237                "the scalar tile GEMM"
238            }
239        );
240        on
241    })
242}
243
244/// Batch the DSA indexer `wq_b` projection across all prefill rows in one cuBLASLt GEMM
245/// instead of one M=1 GEMV per row. `AVAROK_GLM_DSA_BATCH_QIDX=1` opts in.
246///
247/// 🟑 DEFAULT OFF. Prefill-only (see the call site in `glm5next_dsa::layer`), so decode and
248/// the speculative verify already keep their bit-exact M=1 GEMV regardless of this flag. The
249/// batched GEMM changes prefill numerics enough to flip a tool-calling scenario (tool-eval
250/// TC-08) on GLM-5.3-Flash NVFP4 from pass to a partial call, so it ships opt-in rather than
251/// on-by-default; the M=1-per-row GEMV fallback is the default path and is the numerically
252/// reference-matching one.
253pub(crate) fn dsa_batch_qidx() -> bool {
254    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
255    *E.get_or_init(|| {
256        let on = std::env::var("AVAROK_GLM_DSA_BATCH_QIDX").as_deref() == Ok("1");
257        tracing::warn!(
258            "AVAROK_GLM_DSA_BATCH_QIDX: prefill DSA indexer q uses {}",
259            if on {
260                "ONE batched cuBLASLt GEMM"
261            } else {
262                "one M=1 GEMV per row"
263            }
264        );
265        on
266    })
267}
268
269/// `PREFILL_ROWS`, overridable at launch with `AVAROK_GLM_PREFILL_ROWS`.
270///
271/// πŸ”¬ Kept as the A/B lever it was built as. It found A65's real defect (the DSA attend read
272/// `seq_lens[row]` / `block_tables[row]` out of a single-row buffer) by sweeping width against a
273/// fixed control in ONE serve instead of one image per width. `1` restores the per-token walk
274/// exactly β€” the `rows > 1` gate in `prefill` falls through to `forward_one`.
275///
276/// πŸͺ€ Above `DENSE_GEMV_BATCHM_MAX_M` the dense projections leave the bit-identical batched-GEMV
277/// arm for the tile GEMM, so a width past it is a NUMERICS change as well as a speed one. Keep
278/// this lever at or below that constant.
279///
280/// πŸͺ€ Read once and cached: an env read per layer per sub-chunk would sit in the hot loop.
281pub(crate) fn prefill_rows() -> usize {
282    static ROWS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
283    *ROWS.get_or_init(|| {
284        let r = std::env::var("AVAROK_GLM_PREFILL_ROWS")
285            .ok()
286            .and_then(|v| v.parse::<usize>().ok())
287            .filter(|r| *r >= 1)
288            .unwrap_or(PREFILL_ROWS);
289        if r != PREFILL_ROWS {
290            tracing::warn!("GLM prefill sub-chunk overridden to {r} rows (default {PREFILL_ROWS})");
291        }
292        r
293    })
294}
295
296impl Glm5NextLayer {
297    /// RMSNorm over `rows` contiguous `[hidden]` rows.
298    ///
299    /// πŸͺ€ `rms_norm_vanilla`'s grid IS the token axis (`token = blockIdx.x`), so `rows > 1` is
300    /// one launch doing exactly what `rows` launches would do, block for block β€” bit-identical,
301    /// which is what lets a K-token verify take it.
302    fn norm(
303        &self,
304        gpu: &dyn GpuBackend,
305        x: DevicePtr,
306        w: DevicePtr,
307        out: DevicePtr,
308        rows: usize,
309        stream: u64,
310    ) -> Result<()> {
311        KernelLaunch::new(gpu, self.rms_norm_k)
312            .grid([rows as u32, 1, 1])
313            .block([(self.hidden.min(1024)) as u32, 1, 1])
314            .arg_ptr(x)
315            .arg_ptr(w)
316            .arg_ptr(out)
317            .arg_u32(self.hidden as u32)
318            .arg_f32(self.rms_eps)
319            .launch(stream)?;
320        Ok(())
321    }
322
323    /// Run the mixer on `normed`, returning the pointer that holds its output.
324    #[allow(clippy::too_many_arguments)]
325    fn mixer_forward(
326        &self,
327        normed: DevicePtr,
328        residual: DevicePtr,
329        st: &mut dyn LayerState,
330        kv_cache: &mut PagedKvCache,
331        seq_len: usize,
332        block_table: &mut Vec<u32>,
333        disk_block_ids: &mut Vec<u32>,
334        disk_offloaded: &mut Vec<u32>,
335        ctx: &ForwardContext,
336        stream: u64,
337    ) -> Result<DevicePtr> {
338        match &self.mixer {
339            Glm5NextMixer::Kda { layer, ws, .. } => {
340                let ssm = self.kda_state(st)?;
341                let kda = KdaSeqState {
342                    conv: ssm.conv_state,
343                    recurrent: ssm.h_state,
344                };
345                let t = profile::start();
346                layer.decode(ctx.gpu, normed, &kda, ws, stream)?;
347                profile::end(profile::KDA, t, ctx.gpu, stream);
348                Ok(ws.final_out)
349            }
350            Glm5NextMixer::Dsa(layer) => {
351                let dsa: &mut Glm5NextDsaState = self.dsa_state(st)?;
352                layer.decode(
353                    normed,
354                    residual,
355                    dsa,
356                    kv_cache,
357                    seq_len,
358                    block_table,
359                    disk_block_ids,
360                    disk_offloaded,
361                    ctx,
362                    stream,
363                )?;
364                // πŸͺ€ DSA writes its `o_proj` output back over the buffer it was handed.
365                Ok(normed)
366            }
367        }
368    }
369
370    /// PROFILING ONLY: a 2-byte collective that both ranks must reach before either leaves.
371    /// Charged to `bar`, it drains the per-call arrival jitter so the real reduce that follows
372    /// measures network + kernel rather than "network + how late the other rank was".
373    fn reduce_probe(&self, bar: usize, site: &str, ctx: &ForwardContext, stream: u64) {
374        if !profile::on() {
375            return;
376        }
377        let Some(comm) = ctx.comm else { return };
378        let t = profile::start_hot();
379        let p = profile::probe_buf(ctx.gpu);
380        if p != 0 {
381            let _ = comm.all_reduce_async(p, 2, stream);
382        }
383        let us = profile::end_us(bar, t, ctx.gpu, stream);
384        profile::trace_bar(
385            site,
386            self.layer_idx,
387            matches!(self.mlp, Glm5NextMlpSite::Moe(_)),
388            us,
389        );
390    }
391
392    /// `all_reduce(SUM)` a `[rows, hidden]` BF16 partial, when one is needed and a comm exists.
393    ///
394    /// πŸͺ€ One collective over `rows` contiguous rows, not `rows` collectives: `all_reduce(SUM)`
395    /// is linear and the rows are adjacent, so the result is identical and a K-token verify
396    /// pays the latency once.
397    fn reduce_partial(
398        &self,
399        p: DevicePtr,
400        rows: usize,
401        ctx: &ForwardContext,
402        stream: u64,
403    ) -> Result<()> {
404        if let Some(comm) = ctx.comm {
405            let bytes = rows * self.hidden * 2;
406            if ctx.graph_capture {
407                comm.all_reduce(p.0, bytes)?;
408            } else {
409                comm.all_reduce_async(p.0, bytes, stream)?;
410            }
411        }
412        Ok(())
413    }
414
415    /// Run the MLP on `rows` rows of `normed` into `out`, then reduce once if this rank holds
416    /// only part of the result.
417    ///
418    /// The dense FFN and the shared expert sweep their weights ONCE for all rows; only the
419    /// routed experts stay per-row, because their weight traffic genuinely scales with K (the
420    /// measured expert union over K consecutive tokens is 8.00 / 13.74 / 18.76 at K = 1..3).
421    fn mlp_forward(
422        &self,
423        normed: DevicePtr,
424        out: DevicePtr,
425        rows: usize,
426        ctx: &ForwardContext,
427        stream: u64,
428    ) -> Result<()> {
429        let t_dense = matches!(self.mlp, Glm5NextMlpSite::Dense(_))
430            .then(profile::start)
431            .flatten();
432        match &self.mlp {
433            Glm5NextMlpSite::Dense(w) => forward_dense(
434                ctx.gpu,
435                &self.mlp_kernels,
436                &self.mlp_cfg,
437                w,
438                self.mlp_cfg.local_dense_intermediate,
439                normed,
440                out,
441                rows,
442                &self.mlp_ws,
443                stream,
444            )?,
445            Glm5NextMlpSite::Moe(w) => forward_moe(
446                ctx.gpu,
447                &self.mlp_kernels,
448                &self.mlp_cfg,
449                w,
450                normed,
451                out,
452                rows,
453                &self.mlp_ws,
454                stream,
455            )?,
456        }
457        profile::end(profile::MLP_DENSE, t_dense, ctx.gpu, stream);
458        // πŸ”΄ ONE collective for both partials: the routed experts are EP-sharded and the
459        // dense/shared half is TP-sharded, and `all_reduce(SUM)` is linear. It must land here,
460        // before `hc_post` folds the output into the highway β€” reducing afterwards would mix a
461        // half-answer into the residual stream of every later layer.
462        if self.mlp_cfg.needs_all_reduce() {
463            self.reduce_probe(profile::REDUCE_MLP_BAR, "mlp", ctx, stream);
464            let t = profile::start_hot();
465            self.reduce_partial(out, rows, ctx, stream)?;
466            profile::end_nosync(profile::REDUCE_MLP_ENQ, t);
467            // Second span times ONLY the sync: device + network + rank skew.
468            let t = profile::start_hot();
469            profile::end(profile::REDUCE_MLP, t, ctx.gpu, stream);
470        }
471        Ok(())
472    }
473
474    /// `dst += src` over `n` BF16 elements.
475    fn add_inplace(
476        &self,
477        gpu: &dyn GpuBackend,
478        dst: DevicePtr,
479        src: DevicePtr,
480        n: usize,
481        stream: u64,
482    ) -> Result<()> {
483        if self.add_k.0 == 0 {
484            bail!(
485                "GLM layer {}: bf16_add_inplace is not loaded on this target; the MTP layer's \
486                 plain residual path needs it",
487                self.layer_idx
488            );
489        }
490        KernelLaunch::new(gpu, self.add_k)
491            .grid([(n as u32).div_ceil(256), 1, 1])
492            .block([256, 1, 1])
493            .arg_ptr(dst)
494            .arg_ptr(src)
495            .arg_i32(n as i32)
496            .launch(stream)
497    }
498
499    /// One token through a layer with NO hyper-connection β€” a plain pre-norm residual block.
500    ///
501    /// πŸ”΄ This is the MTP layer, and it is the only GLM-5.3 block shaped this way. Every text
502    /// layer carries `hc_*` tensors and its residual lives in the mHC highway, where `hc_post`
503    /// folds the block output back in; `layers.45` carries none, so it is an ordinary
504    /// `x = x + attn(norm(x))` / `x = x + mlp(norm(x))` block. That asymmetry is why
505    /// [`Glm5NextLayer::mhc`] is an `Option` rather than a field.
506    #[allow(clippy::too_many_arguments)]
507    fn forward_one_plain(
508        &self,
509        hidden: DevicePtr,
510        residual: DevicePtr,
511        st: &mut dyn LayerState,
512        kv_cache: &mut PagedKvCache,
513        seq_len: usize,
514        block_table: &mut Vec<u32>,
515        disk_block_ids: &mut Vec<u32>,
516        disk_offloaded: &mut Vec<u32>,
517        ctx: &ForwardContext,
518        stream: u64,
519    ) -> Result<()> {
520        let gpu = ctx.gpu;
521        let h = self.hidden;
522        let normed = ctx.buffers.norm_output();
523        let ffn_out = ctx.buffers.moe_output();
524
525        self.norm(gpu, hidden, self.input_norm, normed, 1, stream)?;
526        let attn_out = self.mixer_forward(
527            normed,
528            residual,
529            st,
530            kv_cache,
531            seq_len,
532            block_table,
533            disk_block_ids,
534            disk_offloaded,
535            ctx,
536            stream,
537        )?;
538        // πŸ”΄ Row-parallel `o_proj` β‡’ a PARTIAL SUM at TP>1. Reduce before it joins the
539        // residual, exactly as the mHC path reduces before `hc_post`.
540        if self.mixer_all_reduce {
541            self.reduce_partial(attn_out, 1, ctx, stream)?;
542        }
543        self.add_inplace(gpu, hidden, attn_out, h, stream)?;
544
545        self.norm(gpu, hidden, self.post_attn_norm, normed, 1, stream)?;
546        self.mlp_forward(normed, ffn_out, 1, ctx, stream)?;
547        self.add_inplace(gpu, hidden, ffn_out, h, stream)
548    }
549
550    /// One token through this layer for the MTP drafter.
551    ///
552    /// Only valid on a `mhc: None` block β€” the drafter's layer. `hidden` is read and written in
553    /// place (the plain residual path accumulates into it), and there are no disk tiers because
554    /// the drafter's KV pool is small, private and fully resident.
555    #[allow(clippy::too_many_arguments)]
556    pub fn decode_one_for_drafter(
557        &self,
558        hidden: DevicePtr,
559        state: &mut dyn LayerState,
560        kv_cache: &mut PagedKvCache,
561        seq_len: usize,
562        block_table: &mut Vec<u32>,
563        ctx: &ForwardContext,
564        stream: u64,
565    ) -> Result<()> {
566        if self.mhc.is_some() {
567            bail!(
568                "GLM layer {}: decode_one_for_drafter is the MTP block's path; this layer has a \
569                 hyper-connection",
570                self.layer_idx
571            );
572        }
573        let (mut disk_a, mut disk_b) = (Vec::new(), Vec::new());
574        self.forward_one_plain(
575            hidden,
576            hidden,
577            state,
578            kv_cache,
579            seq_len,
580            block_table,
581            &mut disk_a,
582            &mut disk_b,
583            ctx,
584            stream,
585        )
586    }
587
588    /// One drafter CONTEXT row: `input_norm` then the DSA caches only.
589    ///
590    /// `x` is the block input (post `eh_proj`) for a row whose OUTPUT is discarded β€” a prompt
591    /// or catch-up row. See [`Glm5NextDsaLayer::write_kv_row`] for why that is enough.
592    #[allow(clippy::too_many_arguments)]
593    pub fn drafter_write_kv_row(
594        &self,
595        x: DevicePtr,
596        state: &mut dyn LayerState,
597        kv_cache: &mut PagedKvCache,
598        seq_len: usize,
599        block_table: &mut Vec<u32>,
600        ctx: &ForwardContext,
601        stream: u64,
602    ) -> Result<()> {
603        let layer = match &self.mixer {
604            Glm5NextMixer::Dsa(l) => l,
605            _ => bail!(
606                "GLM layer {}: drafter_write_kv_row is the MTP block's path; this layer is not \
607                 a DSA layer",
608                self.layer_idx
609            ),
610        };
611        let normed = ctx.buffers.norm_output();
612        self.norm(ctx.gpu, x, self.input_norm, normed, 1, stream)?;
613        layer.write_kv_row(normed, state, kv_cache, seq_len, block_table, ctx, stream)
614    }
615
616    /// One token through the whole layer, using highway slot `slot`.
617    #[allow(clippy::too_many_arguments)]
618    fn forward_one(
619        &self,
620        hidden: DevicePtr,
621        residual: DevicePtr,
622        slot: usize,
623        st: &mut dyn LayerState,
624        kv_cache: &mut PagedKvCache,
625        seq_len: usize,
626        block_table: &mut Vec<u32>,
627        disk_block_ids: &mut Vec<u32>,
628        disk_offloaded: &mut Vec<u32>,
629        ctx: &ForwardContext,
630        stream: u64,
631    ) -> Result<()> {
632        let gpu = ctx.gpu;
633        let h = self.hidden;
634        let Some(mhc) = self.mhc.as_ref() else {
635            // No hyper-connection: the MTP layer, a plain pre-norm residual block.
636            return self.forward_one_plain(
637                hidden,
638                residual,
639                st,
640                kv_cache,
641                seq_len,
642                block_table,
643                disk_block_ids,
644                disk_offloaded,
645                ctx,
646                stream,
647            );
648        };
649        let hc = mhc.hc_mult;
650        let streams = ctx.buffers.hc_streams().offset(slot * hc * h * 4);
651        let post = ctx.buffers.hc_post().offset(slot * hc * 4);
652        let comb = ctx.buffers.hc_comb().offset(slot * hc * hc * 4);
653        let normed = ctx.buffers.norm_output();
654        let ffn_out = ctx.buffers.moe_output();
655
656        let t_mhc = profile::start();
657        if self.is_first {
658            glm_hc_expand(
659                gpu,
660                mhc.kernels.hc_expand,
661                hidden,
662                streams,
663                1,
664                h as u32,
665                hc as u32,
666                stream,
667            )?;
668        }
669
670        // ── attention site ──
671        glm_hc_pre(
672            gpu,
673            &mhc.kernels,
674            streams,
675            &mhc.attn,
676            hidden,
677            post,
678            comb,
679            1,
680            h as u32,
681            hc as u32,
682            mhc.sinkhorn_iters as u32,
683            self.rms_eps,
684            mhc.hc_eps,
685            stream,
686        )?;
687        profile::end(profile::MHC, t_mhc, gpu, stream);
688        let t_norm = profile::start();
689        self.norm(gpu, hidden, self.input_norm, normed, 1, stream)?;
690        profile::end(profile::NORM, t_norm, gpu, stream);
691        let attn_out = self.mixer_forward(
692            normed,
693            residual,
694            st,
695            kv_cache,
696            seq_len,
697            block_table,
698            disk_block_ids,
699            disk_offloaded,
700            ctx,
701            stream,
702        )?;
703        // πŸ”΄ Row-parallel `o_proj` β‡’ `attn_out` is a PARTIAL SUM at TP>1. Reduce it here,
704        // before it enters the highway.
705        if self.mixer_all_reduce {
706            self.reduce_probe(profile::REDUCE_ATTN_BAR, "attn", ctx, stream);
707            let t = profile::start_hot();
708            self.reduce_partial(attn_out, 1, ctx, stream)?;
709            profile::end_nosync(profile::REDUCE_ATTN_ENQ, t);
710            // Second span times ONLY the sync: device + network + rank skew.
711            let t = profile::start_hot();
712            profile::end(profile::REDUCE_ATTN, t, ctx.gpu, stream);
713        }
714        let t_mhc_post = profile::start();
715        glm_hc_post(
716            gpu,
717            mhc.kernels.hc_post,
718            attn_out,
719            streams,
720            post,
721            comb,
722            streams,
723            1,
724            h as u32,
725            hc as u32,
726            stream,
727        )?;
728        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
729
730        // ── FFN site ──
731        let t_mhc = profile::start();
732        glm_hc_pre(
733            gpu,
734            &mhc.kernels,
735            streams,
736            &mhc.ffn,
737            hidden,
738            post,
739            comb,
740            1,
741            h as u32,
742            hc as u32,
743            mhc.sinkhorn_iters as u32,
744            self.rms_eps,
745            mhc.hc_eps,
746            stream,
747        )?;
748        profile::end(profile::MHC, t_mhc, gpu, stream);
749        let t_norm = profile::start();
750        self.norm(gpu, hidden, self.post_attn_norm, normed, 1, stream)?;
751        profile::end(profile::NORM, t_norm, gpu, stream);
752        self.mlp_forward(normed, ffn_out, 1, ctx, stream)?;
753        let t_mhc_post = profile::start();
754        glm_hc_post(
755            gpu,
756            mhc.kernels.hc_post,
757            ffn_out,
758            streams,
759            post,
760            comb,
761            streams,
762            1,
763            h as u32,
764            hc as u32,
765            stream,
766        )?;
767
768        // πŸͺ€ UNWEIGHTED mean, no weights. Not DeepSeek-V4's learned collapse.
769        if self.is_last {
770            hc_head_mean(
771                gpu,
772                mhc.kernels.hc_head,
773                streams,
774                hidden,
775                1,
776                h as u32,
777                hc as u32,
778                stream,
779            )?;
780        }
781        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
782        if self.is_last {
783            profile::step();
784        }
785        Ok(())
786    }
787
788    /// K tokens of one sequence through a KDA layer, with ONE sweep over the weights.
789    ///
790    /// The site order is exactly [`Self::forward_one`]'s β€” `hc_pre -> norm -> mixer -> hc_post`
791    /// per site, the mHC highway collapsed at the last layer β€” but every stage runs over all K
792    /// rows at once instead of K times over one. The mHC kernels, `rms_norm_vanilla` and
793    /// `Glm5NextKdaLayer::decode_k` are each grid-parallel or batched over the token axis and
794    /// each is bit-identical to the K serial calls it replaces, so an accepted draft token is
795    /// the token the unspeculated engine would have emitted.
796    ///
797    /// πŸͺ€ Highway slots are `0..K` and MUST stay per-token β€” the mHC streams are a per-token
798    /// activation that has to survive across layers, so a shared slot would leave every layer
799    /// past the first reading the last token's highway for all K rows.
800    ///
801    /// Both mixers come here: each has its own `decode_k` that batches its projections and
802    /// keeps its per-token part (KDA's recurrence, DSA's selection and gather-attend) serial.
803    #[allow(clippy::too_many_arguments)]
804    fn forward_k(
805        &self,
806        hidden: DevicePtr,
807        k: usize,
808        state: &mut dyn LayerState,
809        kv_cache: &mut PagedKvCache,
810        seq_len: usize,
811        block_table: &mut Vec<u32>,
812        ctx: &ForwardContext,
813        stream: u64,
814        take_snapshots: bool,
815        slot_base: usize,
816        // TRUE only when this call is a PREFILL sub-chunk. `forward_k` is shared by prefill
817        // and by the speculative verify, and nothing in `ForwardContext` separates them:
818        // `decode_step` is false for both, and `graph_capture` is false for prefill AND for
819        // an eager verify. The DSA layer's batched selector is qualified on prefill only, so
820        // the distinction is carried explicitly rather than re-derived downstream.
821        is_prefill: bool,
822    ) -> Result<()> {
823        let gpu = ctx.gpu;
824        let h = self.hidden;
825        let Some(mhc) = self.mhc.as_ref() else {
826            bail!("GLM layer {}: no hyper-connection bound", self.layer_idx);
827        };
828        let hc = mhc.hc_mult;
829        // `slot_base`'s base: the per-slot strides are exactly these, so K contiguous slots
830        // from there ARE the `[K, ...]` the kernels want.
831        //
832        // πŸ”΄ The highway is a PER-TOKEN activation that must survive across layers, so the slot
833        // is the token's index within the whole forward, NOT its row within this call. A verify
834        // is one call at `slot_base = 0`; a batched prefill is `ceil(N / PREFILL_ROWS)` calls
835        // that must land on disjoint slots, or sub-chunk 1 overwrites sub-chunk 0's streams and
836        // every later layer reads the wrong token's highway. ANOMALIES A65.
837        let streams = ctx.buffers.hc_streams().offset(slot_base * hc * h * 4);
838        let post = ctx.buffers.hc_post().offset(slot_base * hc * 4);
839        let comb = ctx.buffers.hc_comb().offset(slot_base * hc * hc * 4);
840        let normed = ctx.buffers.norm_output();
841        let ffn_out = ctx.buffers.moe_output();
842        let (kt, ht, hct) = (k as u32, h as u32, hc as u32);
843
844        // Per-token state snapshots a partial accept rewinds to; row `t` writes slot `t`, and
845        // the last row needs none because a full accept never rolls back. KDA only β€” DSA's
846        // per-sequence state is the indexer cache, which rewinds by a host counter.
847        let kda_ctx = match &self.mixer {
848            Glm5NextMixer::Kda { ws, .. } => {
849                if k > ws.max_tokens() {
850                    bail!(
851                        "GLM layer {}: a {k}-token verify exceeds the KDA workspace built for {}",
852                        self.layer_idx,
853                        ws.max_tokens()
854                    );
855                }
856                let st = self.kda_state(state)?;
857                // πŸ”΄ PREFILL TAKES NONE. A verify needs a per-row rewind point, so it snapshots
858                // rows `0..k-1`; prefill is never rolled back, and the intermediates pool holds
859                // only `num_spec` slots β€” indexing it for a 32-row prefill chunk would run off
860                // the end. `decode_k` reads these with `snapshots.get(row)`, so an empty slice
861                // is a clean "take none", not a special case.
862                let snaps: Vec<(DevicePtr, DevicePtr)> = if take_snapshots {
863                    (0..k.saturating_sub(1))
864                        .map(|t| (st.h_state_intermediates[t], st.conv_state_intermediates[t]))
865                        .collect()
866                } else {
867                    Vec::new()
868                };
869                Some((
870                    KdaSeqState {
871                        conv: st.conv_state,
872                        recurrent: st.h_state,
873                    },
874                    snaps,
875                ))
876            }
877            Glm5NextMixer::Dsa(_) => None,
878        };
879
880        let t_mhc = profile::start();
881        if self.is_first {
882            glm_hc_expand(
883                gpu,
884                mhc.kernels.hc_expand,
885                hidden,
886                streams,
887                kt,
888                ht,
889                hct,
890                stream,
891            )?;
892        }
893
894        // ── attention site ──
895        glm_hc_pre(
896            gpu,
897            &mhc.kernels,
898            streams,
899            &mhc.attn,
900            hidden,
901            post,
902            comb,
903            kt,
904            ht,
905            hct,
906            mhc.sinkhorn_iters as u32,
907            self.rms_eps,
908            mhc.hc_eps,
909            stream,
910        )?;
911        profile::end(profile::MHC, t_mhc, gpu, stream);
912        let t_norm = profile::start();
913        self.norm(gpu, hidden, self.input_norm, normed, k, stream)?;
914        profile::end(profile::NORM, t_norm, gpu, stream);
915        let t = profile::start();
916        let attn_out = match (&self.mixer, &kda_ctx) {
917            (Glm5NextMixer::Kda { layer, ws, .. }, Some((kda, snaps))) => {
918                // πŸ”΄ The CHUNKED scan, on prefill only. `decode_k` walks `stateful_row` once
919                // per token in a host loop: at `AVAROK_GLM_PREFILL_ROWS=256` x 34 KDA layers
920                // that is 8,704 conv + 8,704 recurrent launches per sub-chunk, each a
921                // 1-warp-per-block kernel that re-reads and rewrites the WHOLE recurrent
922                // state (~4 MB/layer at 64 heads), fully serialised by the state dependency.
923                // A corrected profile puts `kda_mixer` at 53.5% of prefill β€” the largest
924                // single bucket by 5x. `kda_chunk_prepare` + `kda_chunk_scan` compute the
925                // same recurrence chunk-wise, touching the state once per `cfg.chunk` (32)
926                // tokens instead of once per token.
927                //
928                // The path was already written, numerically gated against the CPU reference,
929                // and its workspace buffers (`chunk_gc/u/w`, sized `t_pad`) are ALLOCATED in
930                // production β€” it simply had no caller outside a microtest harness. The
931                // loader's stale note ("prefill still runs token-by-token ... the mHC highway
932                // forces per-token anyway") predates `forward_k`, which already does K rows
933                // of mHC in one call.
934                //
935                // πŸͺ€ NOT bit-identical to `decode_k`: the chunked scan reassociates the
936                // recurrence, so it is refused for a speculative VERIFY, whose accepted
937                // tokens must match what decode would have produced. Three conditions keep
938                // it to prefill, and all three are stated rather than inferred:
939                //   * `is_prefill` β€” the caller's own flag, not `!decode_step` (a verify
940                //     also clears that) and not `k > 1` (a verify is K rows too).
941                //   * `snaps.is_empty()` β€” belt and braces. Prefill takes no per-row
942                //     snapshots (see `take_snapshots` above), and the chunked path CANNOT
943                //     produce them: it never materialises the state at interior rows. If a
944                //     caller ever asks for them here, fall back rather than silently drop
945                //     the rewind points.
946                //   * `k > 1` β€” a 1-row chunk has nothing to batch.
947                //
948                // πŸͺ€ Prefix caching: a Marconi replay of `[snap_tok, matched)` must not be
949                // recomputed by a DIFFERENT association than the pass that produced the
950                // cached blocks, or the state drifts and ratchets across turns. That holds
951                // here because the choice keys on `is_prefill` alone, so the original pass
952                // and its replay both take this arm.
953                if is_prefill && k > 1 && snaps.is_empty() && kda_chunk_prefill() {
954                    layer.prefill(gpu, normed, k, kda, ws, stream)?;
955                } else {
956                    layer.decode_k(gpu, normed, k, kda, ws, snaps, stream)?;
957                }
958                ws.final_out
959            }
960            (Glm5NextMixer::Dsa(layer), _) => {
961                // πŸͺ€ DSA writes its `o_proj` output back over the buffer it was handed.
962                layer.decode_k(
963                    normed,
964                    k,
965                    state,
966                    kv_cache,
967                    seq_len,
968                    block_table,
969                    ctx,
970                    stream,
971                    is_prefill,
972                )?;
973                normed
974            }
975            (Glm5NextMixer::Kda { .. }, None) => {
976                bail!("GLM layer {}: KDA mixer without KDA state", self.layer_idx)
977            }
978        };
979        profile::end(profile::KDA, t, gpu, stream);
980        if self.mixer_all_reduce {
981            self.reduce_probe(profile::REDUCE_ATTN_BAR, "attn", ctx, stream);
982            let t = profile::start_hot();
983            self.reduce_partial(attn_out, k, ctx, stream)?;
984            profile::end_nosync(profile::REDUCE_ATTN_ENQ, t);
985            let t = profile::start_hot();
986            profile::end(profile::REDUCE_ATTN, t, ctx.gpu, stream);
987        }
988        let t_mhc_post = profile::start();
989        glm_hc_post(
990            gpu,
991            mhc.kernels.hc_post,
992            attn_out,
993            streams,
994            post,
995            comb,
996            streams,
997            kt,
998            ht,
999            hct,
1000            stream,
1001        )?;
1002        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
1003
1004        // ── FFN site ──
1005        let t_mhc = profile::start();
1006        glm_hc_pre(
1007            gpu,
1008            &mhc.kernels,
1009            streams,
1010            &mhc.ffn,
1011            hidden,
1012            post,
1013            comb,
1014            kt,
1015            ht,
1016            hct,
1017            mhc.sinkhorn_iters as u32,
1018            self.rms_eps,
1019            mhc.hc_eps,
1020            stream,
1021        )?;
1022        profile::end(profile::MHC, t_mhc, gpu, stream);
1023        let t_norm = profile::start();
1024        self.norm(gpu, hidden, self.post_attn_norm, normed, k, stream)?;
1025        profile::end(profile::NORM, t_norm, gpu, stream);
1026        self.mlp_forward(normed, ffn_out, k, ctx, stream)?;
1027        let t_mhc_post = profile::start();
1028        glm_hc_post(
1029            gpu,
1030            mhc.kernels.hc_post,
1031            ffn_out,
1032            streams,
1033            post,
1034            comb,
1035            streams,
1036            kt,
1037            ht,
1038            hct,
1039            stream,
1040        )?;
1041        if self.is_last {
1042            hc_head_mean(
1043                gpu,
1044                mhc.kernels.hc_head,
1045                streams,
1046                hidden,
1047                kt,
1048                ht,
1049                hct,
1050                stream,
1051            )?;
1052        }
1053        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
1054        if self.is_last {
1055            profile::step();
1056        }
1057        Ok(())
1058    }
1059
1060    /// This KDA layer's recurrent + conv state.
1061    ///
1062    /// πŸ”΄ It is an [`SsmLayerState`] β€” the SAME type Qwen's GDN layers carry β€” and that is
1063    /// deliberate, not incidental. `rollback_ssm_states_dispatch` walks every
1064    /// `LayerType::LinearAttention` layer and downcasts to exactly this type to restore a
1065    /// rejected speculative draft; GLM's KDA blocks ARE `linear_attention` in `layer_types`,
1066    /// so carrying anything else means the first rejected draft is a hard error. The shapes
1067    /// line up with the pool's own math: `h = nvΒ·vdΒ·kdΒ·4` and
1068    /// `conv = (nkΒ·kdΒ·2 + nvΒ·vd)Β·d_convΒ·4` are byte-for-byte GLM's
1069    /// `recurrent_state_elems()Β·4` and `conv_state_elems()Β·4`, because the parser fills the
1070    /// `linear_*` fields from `linear_attn_config` and they are already TP-local.
1071    ///
1072    /// πŸͺ€ A mixer/state mismatch means the scheduler handed this layer another layer's slot.
1073    /// Refuse loudly: allocating a fresh state here would decode from a zero recurrent state.
1074    fn kda_state<'a>(&self, state: &'a mut dyn LayerState) -> Result<&'a mut SsmLayerState> {
1075        let st = state
1076            .as_any_mut()
1077            .downcast_mut::<SsmLayerState>()
1078            .ok_or_else(|| {
1079                anyhow::anyhow!(
1080                    "GLM layer {}: a KDA mixer was handed state that is not an SsmLayerState",
1081                    self.layer_idx
1082                )
1083            })?;
1084        // πŸ”΄ HF casts the KDA recurrent state to float32 and vLLM hardcodes `kda_state_dtype`.
1085        // A narrowed h slot (`--ssm-h-dtype f16`/`f16-pool`) is a deviation from the
1086        // reference, not a memory setting, and every KDA kernel reads FP32.
1087        if st.h_is_f16 || st.h_prefill_stage.is_some() {
1088            bail!(
1089                "GLM layer {}: KDA recurrent state is FP32-only; --ssm-h-dtype f16 narrowed it",
1090                self.layer_idx
1091            );
1092        }
1093        Ok(st)
1094    }
1095
1096    /// This DSA layer's indexer key cache.
1097    fn dsa_state<'a>(&self, state: &'a mut dyn LayerState) -> Result<&'a mut Glm5NextDsaState> {
1098        state
1099            .as_any_mut()
1100            .downcast_mut::<Glm5NextDsaState>()
1101            .ok_or_else(|| {
1102                anyhow::anyhow!(
1103                    "GLM layer {}: a DSA mixer was handed state that is not a Glm5NextDsaState",
1104                    self.layer_idx
1105                )
1106            })
1107    }
1108}
1109
1110impl TransformerLayer for Glm5NextLayer {
1111    /// πŸ”΄ GLM-5.3 CANNOT serve a batched multi-sequence decode step. Two
1112    /// independent row-0 aliases, both structural, either one sufficient:
1113    ///
1114    ///   * **D1 β€” mHC highway slot.** `Glm5NextLayer::forward_one` pins highway
1115    ///     slot 0. `decode_multi_seq`'s default loop shares one
1116    ///     `ForwardContext` across the batch, so every sequence would write and
1117    ///     then read the SAME highway stream, and each layer past the first
1118    ///     reads the last sequence's mHC state for all rows. This is the
1119    ///     sequence-axis twin of the token-axis argument already written on
1120    ///     [`Self::decode_batched`] above β€” the trait default is WRONG there
1121    ///     for exactly the same reason.
1122    ///   * **D2 β€” DSA `attn_metadata` row.** The DSA mixer reads metadata row 0
1123    ///     (`glm5next_dsa/layer.rs`: slot, positions, block_table, seq_len), so
1124    ///     every sequence in the batch would attend with sequence 0's page
1125    ///     table and length.
1126    ///
1127    /// Answering `true` does NOT cost concurrency: the caller routes GLM onto
1128    /// #753 item B's per-sequence highway loop, which serves C>1 correctly at
1129    /// C=1-equivalent per-request throughput.
1130    ///
1131    /// πŸ”’ This must stay `true` until the Stage 1 commit that adds a real
1132    /// `Glm5NextLayer::decode_multi_seq` (per-row `forward_one` with
1133    /// `meta_row_base` threading and `ctx.hc_row_offset` honoured as the
1134    /// highway base) flips it to `false` IN THE SAME COMMIT.
1135    fn decode_multi_seq_unsupported(&self) -> bool {
1136        true
1137    }
1138
1139    /// πŸ”΄ GLM-5.3 implements no `decode_verify_multi`, so the batched verify
1140    /// sweep must not be selected for it. The trait default already `bail!`s,
1141    /// but that is a mid-request abort; declaring it here makes
1142    /// `can_batch_verify_dispatch` route around it instead, leaving spec-on
1143    /// C>1 on the per-sequence verify loop β€” the sealed K=3 path.
1144    ///
1145    /// πŸ”’ Flipped to `false` by the PR-3 commit that adds
1146    /// `Glm5NextLayer::decode_verify_multi` (per-sequence `forward_k` sweep
1147    /// with `slot_base = meta_row_base = row_base`).
1148    fn decode_verify_multi_unsupported(&self) -> bool {
1149        true
1150    }
1151
1152    fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>> {
1153        Ok(match &self.mixer {
1154            // Pool-free fallback. `uses_ssm_pool()` is true for KDA, so the model hands these
1155            // layers a pool slot and never calls this β€” but the paths that build states
1156            // directly still need a correctly shaped, ZEROED one. A fresh sequence starts from
1157            // a zero recurrent state and an empty conv window; inheriting the previous
1158            // sequence's residue is a wrong answer that decays over a few tokens rather than
1159            // crashing.
1160            Glm5NextMixer::Kda { cfg, .. } => Box::new(alloc_kda_ssm_state(gpu, cfg)?),
1161            Glm5NextMixer::Dsa(l) => Box::new(Glm5NextDsaState::alloc(gpu, &l.cfg)?),
1162        })
1163    }
1164
1165    /// Release what `alloc_state` allocated β€” ANOMALIES A76. The DSA indexer cache is
1166    /// sized by `--max-seq-len`, not by the prompt (513 B/token/layer), so leaking one
1167    /// per request walks a unified-memory host into the ground.
1168    ///
1169    /// πŸ”΄ Type-driven on purpose. A KDA layer's state on the model path is POOL-owned
1170    /// (`uses_ssm_pool()` is true for `Kda`, so `alloc_sequence` hands it pool addresses
1171    /// and `free_sequence` skips it entirely) β€” but refusing by TYPE as well means a
1172    /// pool address can never reach `gpu.free` even if a future call site forgets the
1173    /// skip. `SsmLayerState` is therefore left alone here, always.
1174    fn release_state(&self, state: &mut dyn LayerState, gpu: &dyn GpuBackend) -> Result<()> {
1175        if let Some(dsa) = state.as_any_mut().downcast_mut::<Glm5NextDsaState>() {
1176            dsa.free(gpu)?;
1177        }
1178        Ok(())
1179    }
1180
1181    /// The DSA mixer allocates its per-sequence state with `gpu.alloc` in `alloc_state`, so
1182    /// the addresses a capture bakes belong to THAT sequence, not to the slot.
1183    ///
1184    /// πŸͺ€ Only DSA. The KDA mixer is POOL-backed on the model path β€” `uses_ssm_pool()` is true
1185    /// for `Kda`, so `meta.rs` hands it pool addresses and never calls its `alloc_state`. The
1186    /// `true` below is still correct (one owned mixer is enough); the previous wording claimed
1187    /// both mixers own their state, and that was wrong.
1188    fn graph_stale_on_new_sequence(&self) -> bool {
1189        true
1190    }
1191
1192    /// πŸ”΄ A CUDA-graph replay runs kernels and nothing else, so this layer's one piece of
1193    /// HOST-side per-sequence bookkeeping β€” the DSA indexer cache length β€” has to be advanced
1194    /// here. The inner `Glm5NextDsaLayer` implements this too, but the model's layer vec holds
1195    /// the COMPOSITE, so the inner impl is never reached and the default no-op left the
1196    /// counter frozen at its capture-time value.
1197    ///
1198    /// πŸͺ€ That was invisible on the spec-off path: after capture, `decode` never runs again, so
1199    /// nothing compared the counter to `seq_len`. The first EAGER step after a run of replays β€”
1200    /// which is exactly what a speculative verify is β€” then failed with "indexer cache holds 5
1201    /// tokens but the sequence is at 12". The rows were there; only the counter was stale.
1202    ///
1203    /// KDA keeps nothing on the host: its recurrent and conv state are device-resident and the
1204    /// replayed kernels update them in place.
1205    fn sync_replayed_step(
1206        &self,
1207        state: &mut dyn LayerState,
1208        seq_len: usize,
1209        k: usize,
1210    ) -> Result<()> {
1211        match &self.mixer {
1212            Glm5NextMixer::Dsa(_) => self.dsa_state(state)?.sync_to(seq_len, k),
1213            Glm5NextMixer::Kda { .. } => Ok(()),
1214        }
1215    }
1216
1217    /// The model's layer vec holds the COMPOSITE, so β€” exactly as with `sync_replayed_step`
1218    /// β€” the inner `Glm5NextDsaLayer` impl is never reached and this one is what runs. A62.
1219    fn check_replay_room(&self, state: &dyn LayerState, seq_len: usize, k: usize) -> Result<()> {
1220        match &self.mixer {
1221            Glm5NextMixer::Dsa(_) => state
1222                .as_any()
1223                .downcast_ref::<Glm5NextDsaState>()
1224                .ok_or_else(|| {
1225                    anyhow::anyhow!(
1226                        "GLM layer {}: a DSA mixer was handed state that is not a \
1227                         Glm5NextDsaState",
1228                        self.layer_idx
1229                    )
1230                })?
1231                .ensure_room_through(seq_len + k)
1232                // πŸ”΄ Both A62 routes raise the SAME refusal, so without a tag a log cannot
1233                // tell a pre-launch replay refusal from `indexer_forward`'s prefill one β€”
1234                // and "we proved the replay route" would rest on inference. Name it.
1235                .with_context(|| {
1236                    format!(
1237                        "DSA replay pre-check (layer {}, before launch_graph, seq_len \
1238                         {seq_len} + k {k})",
1239                        self.layer_idx
1240                    )
1241                }),
1242            Glm5NextMixer::Kda { .. } => Ok(()),
1243        }
1244    }
1245
1246    /// GLM's KDA blocks are `linear_attention` in `layer_types` AND carry the pool's
1247    /// `SsmLayerState`, so they take pool slots like any other recurrent layer. That is what
1248    /// buys the speculative-verify checkpoints and per-token intermediates for free β€”
1249    /// `meta.rs` only wires `h_state_checkpoint` / `h_state_intermediates` for layers that
1250    /// answer true here, and `rollback_ssm_states_dispatch` needs both.
1251    fn uses_ssm_pool(&self) -> bool {
1252        matches!(self.mixer, Glm5NextMixer::Kda { .. })
1253    }
1254
1255    /// Marconi aux state: the DSA indexer cache, and ONLY the DSA indexer cache.
1256    ///
1257    /// πŸͺ€ KDA is deliberately absent. Its recurrent and conv state live in the SSM pool
1258    /// (`uses_ssm_pool()` is true for `Kda`), and `SsmSnapshotPool` already captures and
1259    /// restores that region device-to-device for every snapshot. Carrying it here as well
1260    /// would serialize 148.75 MiB a snapshot to the host to duplicate a copy the pool
1261    /// already made.
1262    ///
1263    /// πŸ”΄ The indexer is the reason `per_sequence_state_is_kv_complete()` returns false for
1264    /// `glm5_next`: `k_normed`/`gate` are projections of the HIDDEN state, so a KV-only
1265    /// prefix-cache hit cannot reconstruct them. This hook is what would make the snapshot
1266    /// complete β€” the gate stays shut until that is proven on GPU.
1267    fn has_aux_state(&self) -> bool {
1268        matches!(self.mixer, Glm5NextMixer::Dsa(_))
1269    }
1270
1271    fn snapshot_aux(
1272        &self,
1273        state: &dyn LayerState,
1274        gpu: &dyn GpuBackend,
1275        stream: u64,
1276    ) -> Result<Option<Vec<u8>>> {
1277        if !matches!(self.mixer, Glm5NextMixer::Dsa(_)) {
1278            return Ok(None);
1279        }
1280        let st = state
1281            .as_any()
1282            .downcast_ref::<Glm5NextDsaState>()
1283            .ok_or_else(|| {
1284                anyhow::anyhow!(
1285                    "GLM layer {}: a DSA mixer was handed state that is not a Glm5NextDsaState",
1286                    self.layer_idx
1287                )
1288            })?;
1289        Ok(Some(st.snapshot_blob(gpu, stream)?))
1290    }
1291
1292    /// πŸ”΄ Refuses rather than skipping. `apply_aux_states` propagates this with `?`, so a
1293    /// blob that does not match this sequence's geometry or reservation fails the
1294    /// prefix-cache hit outright. The alternative β€” applying it anyway β€” selects over
1295    /// another sequence's indexer keys and answers HTTP 200 with the wrong text.
1296    fn restore_aux(
1297        &self,
1298        state: &mut dyn LayerState,
1299        blob: &[u8],
1300        gpu: &dyn GpuBackend,
1301        stream: u64,
1302    ) -> Result<()> {
1303        if !matches!(self.mixer, Glm5NextMixer::Dsa(_)) {
1304            bail!(
1305                "GLM layer {}: restore_aux on a KDA layer β€” KDA state is pool-backed and \
1306                 travels with the SSM snapshot, so a blob addressed here is a routing bug",
1307                self.layer_idx
1308            );
1309        }
1310        self.dsa_state(state)?.restore_blob(blob, gpu, stream)
1311    }
1312
1313    #[allow(clippy::too_many_arguments)]
1314    fn decode(
1315        &self,
1316        hidden: DevicePtr,
1317        residual: DevicePtr,
1318        state: &mut dyn LayerState,
1319        kv_cache: &mut PagedKvCache,
1320        seq_len: usize,
1321        block_table: &mut Vec<u32>,
1322        disk_block_ids: &mut Vec<u32>,
1323        disk_last_offloaded_per_layer: &mut Vec<u32>,
1324        ctx: &ForwardContext,
1325        stream: u64,
1326    ) -> Result<()> {
1327        self.forward_one(
1328            hidden,
1329            residual,
1330            0,
1331            state,
1332            kv_cache,
1333            seq_len,
1334            block_table,
1335            disk_block_ids,
1336            disk_last_offloaded_per_layer,
1337            ctx,
1338            stream,
1339        )
1340    }
1341
1342    /// Prefill, one token at a time β€” but with the highway indexed by token.
1343    ///
1344    /// πŸ”΄ The trait's default fallback would be WRONG here, not merely slow. It runs every token
1345    /// through this layer before the next layer sees any of them, so a single-slot highway would
1346    /// hold only the last token's streams by the time layer `n+1` reads it. The mHC highway is a
1347    /// per-token activation that must survive across layers, so each token gets its own slot.
1348    ///
1349    /// Per-token (rather than chunked) is deliberate for this slice: KDA's recurrence is
1350    /// sequential anyway, and the chunked `Glm5NextKdaLayer::prefill` needs a workspace sized
1351    /// for the chunk. That is an optimisation, explicitly out of scope.
1352    #[allow(clippy::too_many_arguments)]
1353    fn prefill(
1354        &self,
1355        hidden: DevicePtr,
1356        residual: DevicePtr,
1357        num_tokens: usize,
1358        state: &mut dyn LayerState,
1359        kv_cache: &mut PagedKvCache,
1360        seq_len_start: usize,
1361        block_table: &mut Vec<u32>,
1362        disk_block_ids: &mut Vec<u32>,
1363        disk_last_offloaded_per_layer: &mut Vec<u32>,
1364        _kv_write_start: usize,
1365        ctx: &ForwardContext,
1366        stream: u64,
1367    ) -> Result<()> {
1368        let cap = ctx.buffers.max_batch_tokens();
1369        if num_tokens > cap {
1370            bail!(
1371                "GLM layer {}: prefill of {num_tokens} tokens exceeds the {cap}-token mHC \
1372                 highway the buffer arena was sized for; each token needs its own slot",
1373                self.layer_idx
1374            );
1375        }
1376        // ── Batched sub-chunks: ONE weight sweep per PREFILL_ROWS tokens ──
1377        //
1378        // πŸ”΄ ANOMALIES A65. The per-token walk below pays a full sweep of this layer's weights
1379        // for EVERY token, which is why prefill ran at decode speed (15.2 tok/s measured, and
1380        // 97 % linear in the token count: TTFT 579.1 s at 9,000 tokens and 1,187.6 s at 18,000,
1381        // a ratio of 2.051 against 2.000 for pure-linear).
1382        //
1383        // `forward_k` is the SAME body the speculative verify uses and already sweeps once for
1384        // all its rows, so this is reuse, not a new path. What it does NOT yet amortize is the
1385        // routed MoE: `forward_moe`'s expert-union arm is capped at 4 rows (the union kernel
1386        // resolves `rows * top_k <= 64` ids in one block), so above that each row still pays its
1387        // own 8 experts. That caps the win here at ~5x β€” Atlas's own sizing note puts KDA at
1388        // 9,366 MB/token against ~2.1 GB/token of routed-expert traffic, so amortizing the
1389        // former is most of the prize and the latter needs a grouped MoE GEMM (separate lane).
1390        //
1391        // πŸͺ€ `mhc: None` is the MTP drafter block, whose plain residual path `forward_k` does
1392        // not implement β€” it bails on a missing highway. That block keeps the per-token walk.
1393        let rows = if self.mhc.is_some() {
1394            prefill_rows().min(cap)
1395        } else {
1396            1
1397        };
1398        if rows > 1 {
1399            let mut t = 0usize;
1400            while t < num_tokens {
1401                let k = rows.min(num_tokens - t);
1402                self.forward_k(
1403                    hidden.offset(t * self.hidden * 2),
1404                    k,
1405                    state,
1406                    kv_cache,
1407                    seq_len_start + t,
1408                    block_table,
1409                    ctx,
1410                    stream,
1411                    // Prefill is never rolled back, so it takes no per-row KDA snapshots.
1412                    false,
1413                    // Absolute slot within this prefill, so sub-chunks never share a slot.
1414                    t,
1415                    // This IS the prefill sub-chunk caller.
1416                    true,
1417                )?;
1418                t += k;
1419            }
1420            return Ok(());
1421        }
1422        for t in 0..num_tokens {
1423            let off = t * self.hidden * 2;
1424            self.forward_one(
1425                hidden.offset(off),
1426                residual.offset(off),
1427                t,
1428                state,
1429                kv_cache,
1430                seq_len_start + t,
1431                block_table,
1432                disk_block_ids,
1433                disk_last_offloaded_per_layer,
1434                ctx,
1435                stream,
1436            )?;
1437        }
1438        Ok(())
1439    }
1440
1441    /// K tokens of ONE sequence in a single call β€” the speculative-verify body.
1442    ///
1443    /// The same per-token walk as [`Self::prefill`] β€” same highway slots, same
1444    /// `seq_len + t` positions, same KV writes β€” plus the per-token KDA state snapshots that
1445    /// only a verify needs. Prefill is never rolled back, so it does not pay for them.
1446    ///
1447    /// πŸ”΄ The trait's default would be WRONG, not merely slow: it calls `decode` per token, and
1448    /// `decode` pins highway slot 0. K tokens would then overwrite each other's mHC streams and
1449    /// every layer past the first would read the last token's highway for all K rows.
1450    ///
1451    /// βœ… **Batched.** This delegates to `Self::forward_k`, which sweeps the weights ONCE for
1452    /// all K rows. (An earlier revision of this comment said "still one `forward_one` per row";
1453    /// that was stale β€” `forward_k` has been the body since the batched-verify work, and the
1454    /// measured K=3 step of ~101 ms against a ~63 ms single-row step is only explicable by it.)
1455    #[allow(clippy::too_many_arguments)]
1456    fn decode_batched(
1457        &self,
1458        hidden: DevicePtr,
1459        _residual: DevicePtr,
1460        num_tokens: usize,
1461        state: &mut dyn LayerState,
1462        kv_cache: &mut PagedKvCache,
1463        seq_len: usize,
1464        block_table: &mut Vec<u32>,
1465        _disk_block_ids: &mut Vec<u32>,
1466        _disk_last_offloaded_per_layer: &mut Vec<u32>,
1467        ctx: &ForwardContext,
1468        stream: u64,
1469    ) -> Result<()> {
1470        // A KDA layer must leave behind the state it held after EACH verify token, or a
1471        // partially-accepted draft has nothing to rewind to. `rollback_ssm_states_dispatch`
1472        // restores `h_state_intermediates[num_accepted - 1]`, so row `t` writes slot `t` and
1473        // the LAST row needs none (a full accept never rolls back).
1474        let kda_bytes = matches!(self.mixer, Glm5NextMixer::Kda { .. }).then_some(());
1475        if kda_bytes.is_some() && num_tokens > 1 {
1476            let st = self.kda_state(state)?;
1477            // πŸ”΄ Bail rather than skip. Skipping leaves `h_state` ADVANCED past the accepted
1478            // boundary with no error and no log line, which corrupts every subsequent decode
1479            // and surfaces much later as gibberish. Same check the Qwen verify arms make.
1480            if st.h_state_intermediates.len() + 1 < num_tokens
1481                || st.conv_state_intermediates.len() + 1 < num_tokens
1482            {
1483                bail!(
1484                    "GLM layer {}: a {num_tokens}-token verify needs {} per-token state \
1485                     snapshots but the pool has h={} conv={}. With none, this is the \
1486                     self-speculative / ngram path on a model whose MTP pool was never \
1487                     sized; with too few, --num-drafts exceeds the pool's tier.",
1488                    self.layer_idx,
1489                    num_tokens - 1,
1490                    st.h_state_intermediates.len(),
1491                    st.conv_state_intermediates.len(),
1492                );
1493            }
1494        }
1495
1496        // ONE sweep over the weights for all K rows β€” the whole reason speculation pays.
1497        self.forward_k(
1498            hidden,
1499            num_tokens,
1500            state,
1501            kv_cache,
1502            seq_len,
1503            block_table,
1504            ctx,
1505            stream,
1506            true,
1507            0,
1508            // A speculative verify, NOT a prefill sub-chunk β€” true here would hand an eager
1509            // verify the prefill-only batched DSA selector.
1510            false,
1511        )
1512    }
1513
1514    /// KDA layers carry recurrent state; DSA layers do not.
1515    fn is_ssm_layer(&self) -> bool {
1516        matches!(self.mixer, Glm5NextMixer::Kda { .. })
1517    }
1518}
1519
1520#[cfg(test)]
1521mod tests;