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    pub mlp_ws: Glm5NextMlpWorkspace,
120    /// `None` only for a layer with no hyper-connection β€” i.e. the MTP layer, which carries zero
121    /// `hc_*` tensors. Every text layer has one.
122    pub mhc: Option<Glm5NextMhc>,
123    /// `input_layernorm.weight` / `post_attention_layernorm.weight`, both plain RMSNorm.
124    pub input_norm: DevicePtr,
125    pub post_attn_norm: DevicePtr,
126    /// πŸͺ€ `rms_norm_vanilla`, never `rms_norm`. See the module header.
127    pub rms_norm_k: KernelHandle,
128    /// `bf16_add_inplace`, the residual add the MTP layer needs and the text layers do not:
129    /// a text layer's residual lives in the mHC highway and `hc_post` folds the block output
130    /// into it. `0` on a target without the kernel, which the MTP path refuses.
131    pub add_k: KernelHandle,
132    pub rms_eps: f32,
133    pub hidden: usize,
134    /// πŸ”΄ Whether the MIXER output is a partial sum. Both mixers end in a **row-parallel**
135    /// `o_proj` (`KdaShard::ChannelCols` / `DsaShard::HeadCols`), so at TP>1 each rank holds
136    /// only part of the attention output and it must be all-reduced **before** `hc_post` folds
137    /// it into the highway. Reducing after would mix a half-answer into every later layer's
138    /// residual stream; not reducing at all is a plausible, wrong output with no shape error.
139    pub mixer_all_reduce: bool,
140    /// Expand the highway here. True for layer 0 only.
141    pub is_first: bool,
142    /// Collapse the highway here. True for the last TEXT layer only.
143    pub is_last: bool,
144}
145
146/// Tokens per batched prefill sub-chunk β€” the width `Glm5NextLayer::prefill` hands
147/// [`Glm5NextLayer::forward_k`].
148///
149/// πŸ”΄ **16, and the ceiling is still a KERNEL boundary, not a bandwidth knee.** Every dense
150/// projection on this path goes through [`ops::dense_mm_bf16`], whose batched-GEMV arm
151/// (`dense_gemv_bf16_batchm`) is **bit-identical to M serial GEMVs** and stops at
152/// `DENSE_GEMV_BATCHM_MAX_M`. Past it the same call falls to the tile GEMM, which both
153/// reassociates (so prefill stops being bit-identical to the per-token walk) and is the slower
154/// kernel at these widths β€” Atlas measured it 3.6x slower than the batched GEMV at M <= 8.
155///
156/// πŸ”΄ WIDENED 8 -> 16 (2026-09-02). The A65 measurement below β€” "R = 32 is no faster" β€” was
157/// TRUE AND MISATTRIBUTED. R = 32 lost because it left the batched GEMV for the tile GEMM, not
158/// because row batching stops paying. With the tier itself widened to 16, the same 12 GLM
159/// prefill shapes cost **1.36-1.98x less per token at M = 16 than at M = 8** with cold weights
160/// (11 of 12; shallow-K N4096 K128 is the one loser at 0.77x), worth a modelled **-17.6 s of a
161/// 173.9 s 9K TTFT**, bit-identical (`scripts/glm53-dense-bf16/bench_m16.cu`, spark-bench).
162///
163/// πŸͺ€ The routed MoE does NOT follow the width up. `glm5next_mlp::forward::forward_moe` splits a
164/// wider row group into even sub-groups of at most `MOE_ROW_BATCH_MAX_ROWS` β€” exact, because a
165/// row's expert sum depends only on its own top-k, never on which rows share the sweep β€” so the
166/// routed experts still amortize over 8 rows, not 16. Widening THAT is a separate and unmeasured
167/// question: the union kernel is a single block with an O(T^3) scan, and the tier's register
168/// cost was already 80 at R = 8.
169///
170/// πŸ”΄ MEASURED 2026-08-31, one image, one control, ~1,950-token prompt: control TTFT 125.3 s;
171/// R = 8 **43.4 s (2.89x) and byte-identical on all four probes**; R = 32 44.3 / 51.8 s β€” no
172/// faster, and it moves two of the four completions. The per-token-bytes model that first
173/// picked 32 (predicting 4.5x at R = 32 against 3.0x at R = 8) is REFUTED as a width law: it
174/// modelled weight traffic only, and above R = 8 the traffic saved is handed to a slower kernel.
175/// The routed experts do not amortize past R = 4 either way (`forward_moe`'s union arm caps
176/// there), so R = 8 takes the whole available win. ANOMALIES A65.
177pub(crate) const PREFILL_ROWS: usize = 16;
178
179/// `PREFILL_ROWS`, overridable at launch with `ATLAS_GLM_PREFILL_ROWS`.
180///
181/// πŸ”¬ Kept as the A/B lever it was built as. It found A65's real defect (the DSA attend read
182/// `seq_lens[row]` / `block_tables[row]` out of a single-row buffer) by sweeping width against a
183/// fixed control in ONE serve instead of one image per width. `1` restores the per-token walk
184/// exactly β€” the `rows > 1` gate in `prefill` falls through to `forward_one`.
185///
186/// πŸͺ€ Above `DENSE_GEMV_BATCHM_MAX_M` the dense projections leave the bit-identical batched-GEMV
187/// arm for the tile GEMM, so a width past it is a NUMERICS change as well as a speed one. Keep
188/// this lever at or below that constant.
189///
190/// πŸͺ€ Read once and cached: an env read per layer per sub-chunk would sit in the hot loop.
191pub(crate) fn prefill_rows() -> usize {
192    static ROWS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
193    *ROWS.get_or_init(|| {
194        let r = std::env::var("ATLAS_GLM_PREFILL_ROWS")
195            .ok()
196            .and_then(|v| v.parse::<usize>().ok())
197            .filter(|r| *r >= 1)
198            .unwrap_or(PREFILL_ROWS);
199        if r != PREFILL_ROWS {
200            tracing::warn!("GLM prefill sub-chunk overridden to {r} rows (default {PREFILL_ROWS})");
201        }
202        r
203    })
204}
205
206impl Glm5NextLayer {
207    /// RMSNorm over `rows` contiguous `[hidden]` rows.
208    ///
209    /// πŸͺ€ `rms_norm_vanilla`'s grid IS the token axis (`token = blockIdx.x`), so `rows > 1` is
210    /// one launch doing exactly what `rows` launches would do, block for block β€” bit-identical,
211    /// which is what lets a K-token verify take it.
212    fn norm(
213        &self,
214        gpu: &dyn GpuBackend,
215        x: DevicePtr,
216        w: DevicePtr,
217        out: DevicePtr,
218        rows: usize,
219        stream: u64,
220    ) -> Result<()> {
221        KernelLaunch::new(gpu, self.rms_norm_k)
222            .grid([rows as u32, 1, 1])
223            .block([(self.hidden.min(1024)) as u32, 1, 1])
224            .arg_ptr(x)
225            .arg_ptr(w)
226            .arg_ptr(out)
227            .arg_u32(self.hidden as u32)
228            .arg_f32(self.rms_eps)
229            .launch(stream)?;
230        Ok(())
231    }
232
233    /// Run the mixer on `normed`, returning the pointer that holds its output.
234    #[allow(clippy::too_many_arguments)]
235    fn mixer_forward(
236        &self,
237        normed: DevicePtr,
238        residual: DevicePtr,
239        st: &mut dyn LayerState,
240        kv_cache: &mut PagedKvCache,
241        seq_len: usize,
242        block_table: &mut Vec<u32>,
243        disk_block_ids: &mut Vec<u32>,
244        disk_offloaded: &mut Vec<u32>,
245        ctx: &ForwardContext,
246        stream: u64,
247    ) -> Result<DevicePtr> {
248        match &self.mixer {
249            Glm5NextMixer::Kda { layer, ws, .. } => {
250                let ssm = self.kda_state(st)?;
251                let kda = KdaSeqState {
252                    conv: ssm.conv_state,
253                    recurrent: ssm.h_state,
254                };
255                let t = profile::start();
256                layer.decode(ctx.gpu, normed, &kda, ws, stream)?;
257                profile::end(profile::KDA, t, ctx.gpu, stream);
258                Ok(ws.final_out)
259            }
260            Glm5NextMixer::Dsa(layer) => {
261                let dsa: &mut Glm5NextDsaState = self.dsa_state(st)?;
262                layer.decode(
263                    normed,
264                    residual,
265                    dsa,
266                    kv_cache,
267                    seq_len,
268                    block_table,
269                    disk_block_ids,
270                    disk_offloaded,
271                    ctx,
272                    stream,
273                )?;
274                // πŸͺ€ DSA writes its `o_proj` output back over the buffer it was handed.
275                Ok(normed)
276            }
277        }
278    }
279
280    /// PROFILING ONLY: a 2-byte collective that both ranks must reach before either leaves.
281    /// Charged to `bar`, it drains the per-call arrival jitter so the real reduce that follows
282    /// measures network + kernel rather than "network + how late the other rank was".
283    fn reduce_probe(&self, bar: usize, site: &str, ctx: &ForwardContext, stream: u64) {
284        if !profile::on() {
285            return;
286        }
287        let Some(comm) = ctx.comm else { return };
288        let t = profile::start_hot();
289        let p = profile::probe_buf(ctx.gpu);
290        if p != 0 {
291            let _ = comm.all_reduce_async(p, 2, stream);
292        }
293        let us = profile::end_us(bar, t, ctx.gpu, stream);
294        profile::trace_bar(
295            site,
296            self.layer_idx,
297            matches!(self.mlp, Glm5NextMlpSite::Moe(_)),
298            us,
299        );
300    }
301
302    /// `all_reduce(SUM)` a `[rows, hidden]` BF16 partial, when one is needed and a comm exists.
303    ///
304    /// πŸͺ€ One collective over `rows` contiguous rows, not `rows` collectives: `all_reduce(SUM)`
305    /// is linear and the rows are adjacent, so the result is identical and a K-token verify
306    /// pays the latency once.
307    fn reduce_partial(
308        &self,
309        p: DevicePtr,
310        rows: usize,
311        ctx: &ForwardContext,
312        stream: u64,
313    ) -> Result<()> {
314        if let Some(comm) = ctx.comm {
315            let bytes = rows * self.hidden * 2;
316            if ctx.graph_capture {
317                comm.all_reduce(p.0, bytes)?;
318            } else {
319                comm.all_reduce_async(p.0, bytes, stream)?;
320            }
321        }
322        Ok(())
323    }
324
325    /// Run the MLP on `rows` rows of `normed` into `out`, then reduce once if this rank holds
326    /// only part of the result.
327    ///
328    /// The dense FFN and the shared expert sweep their weights ONCE for all rows; only the
329    /// routed experts stay per-row, because their weight traffic genuinely scales with K (the
330    /// measured expert union over K consecutive tokens is 8.00 / 13.74 / 18.76 at K = 1..3).
331    fn mlp_forward(
332        &self,
333        normed: DevicePtr,
334        out: DevicePtr,
335        rows: usize,
336        ctx: &ForwardContext,
337        stream: u64,
338    ) -> Result<()> {
339        let t_dense = matches!(self.mlp, Glm5NextMlpSite::Dense(_))
340            .then(profile::start)
341            .flatten();
342        match &self.mlp {
343            Glm5NextMlpSite::Dense(w) => forward_dense(
344                ctx.gpu,
345                &self.mlp_kernels,
346                &self.mlp_cfg,
347                w,
348                self.mlp_cfg.local_dense_intermediate,
349                normed,
350                out,
351                rows,
352                &self.mlp_ws,
353                stream,
354            )?,
355            Glm5NextMlpSite::Moe(w) => forward_moe(
356                ctx.gpu,
357                &self.mlp_kernels,
358                &self.mlp_cfg,
359                w,
360                normed,
361                out,
362                rows,
363                &self.mlp_ws,
364                stream,
365            )?,
366        }
367        profile::end(profile::MLP_DENSE, t_dense, ctx.gpu, stream);
368        // πŸ”΄ ONE collective for both partials: the routed experts are EP-sharded and the
369        // dense/shared half is TP-sharded, and `all_reduce(SUM)` is linear. It must land here,
370        // before `hc_post` folds the output into the highway β€” reducing afterwards would mix a
371        // half-answer into the residual stream of every later layer.
372        if self.mlp_cfg.needs_all_reduce() {
373            self.reduce_probe(profile::REDUCE_MLP_BAR, "mlp", ctx, stream);
374            let t = profile::start_hot();
375            self.reduce_partial(out, rows, ctx, stream)?;
376            profile::end_nosync(profile::REDUCE_MLP_ENQ, t);
377            // Second span times ONLY the sync: device + network + rank skew.
378            let t = profile::start_hot();
379            profile::end(profile::REDUCE_MLP, t, ctx.gpu, stream);
380        }
381        Ok(())
382    }
383
384    /// `dst += src` over `n` BF16 elements.
385    fn add_inplace(
386        &self,
387        gpu: &dyn GpuBackend,
388        dst: DevicePtr,
389        src: DevicePtr,
390        n: usize,
391        stream: u64,
392    ) -> Result<()> {
393        if self.add_k.0 == 0 {
394            bail!(
395                "GLM layer {}: bf16_add_inplace is not loaded on this target; the MTP layer's \
396                 plain residual path needs it",
397                self.layer_idx
398            );
399        }
400        KernelLaunch::new(gpu, self.add_k)
401            .grid([(n as u32).div_ceil(256), 1, 1])
402            .block([256, 1, 1])
403            .arg_ptr(dst)
404            .arg_ptr(src)
405            .arg_i32(n as i32)
406            .launch(stream)
407    }
408
409    /// One token through a layer with NO hyper-connection β€” a plain pre-norm residual block.
410    ///
411    /// πŸ”΄ This is the MTP layer, and it is the only GLM-5.3 block shaped this way. Every text
412    /// layer carries `hc_*` tensors and its residual lives in the mHC highway, where `hc_post`
413    /// folds the block output back in; `layers.45` carries none, so it is an ordinary
414    /// `x = x + attn(norm(x))` / `x = x + mlp(norm(x))` block. That asymmetry is why
415    /// [`Glm5NextLayer::mhc`] is an `Option` rather than a field.
416    #[allow(clippy::too_many_arguments)]
417    fn forward_one_plain(
418        &self,
419        hidden: DevicePtr,
420        residual: DevicePtr,
421        st: &mut dyn LayerState,
422        kv_cache: &mut PagedKvCache,
423        seq_len: usize,
424        block_table: &mut Vec<u32>,
425        disk_block_ids: &mut Vec<u32>,
426        disk_offloaded: &mut Vec<u32>,
427        ctx: &ForwardContext,
428        stream: u64,
429    ) -> Result<()> {
430        let gpu = ctx.gpu;
431        let h = self.hidden;
432        let normed = ctx.buffers.norm_output();
433        let ffn_out = ctx.buffers.moe_output();
434
435        self.norm(gpu, hidden, self.input_norm, normed, 1, stream)?;
436        let attn_out = self.mixer_forward(
437            normed,
438            residual,
439            st,
440            kv_cache,
441            seq_len,
442            block_table,
443            disk_block_ids,
444            disk_offloaded,
445            ctx,
446            stream,
447        )?;
448        // πŸ”΄ Row-parallel `o_proj` β‡’ a PARTIAL SUM at TP>1. Reduce before it joins the
449        // residual, exactly as the mHC path reduces before `hc_post`.
450        if self.mixer_all_reduce {
451            self.reduce_partial(attn_out, 1, ctx, stream)?;
452        }
453        self.add_inplace(gpu, hidden, attn_out, h, stream)?;
454
455        self.norm(gpu, hidden, self.post_attn_norm, normed, 1, stream)?;
456        self.mlp_forward(normed, ffn_out, 1, ctx, stream)?;
457        self.add_inplace(gpu, hidden, ffn_out, h, stream)
458    }
459
460    /// One token through this layer for the MTP drafter.
461    ///
462    /// Only valid on a `mhc: None` block β€” the drafter's layer. `hidden` is read and written in
463    /// place (the plain residual path accumulates into it), and there are no disk tiers because
464    /// the drafter's KV pool is small, private and fully resident.
465    #[allow(clippy::too_many_arguments)]
466    pub fn decode_one_for_drafter(
467        &self,
468        hidden: DevicePtr,
469        state: &mut dyn LayerState,
470        kv_cache: &mut PagedKvCache,
471        seq_len: usize,
472        block_table: &mut Vec<u32>,
473        ctx: &ForwardContext,
474        stream: u64,
475    ) -> Result<()> {
476        if self.mhc.is_some() {
477            bail!(
478                "GLM layer {}: decode_one_for_drafter is the MTP block's path; this layer has a \
479                 hyper-connection",
480                self.layer_idx
481            );
482        }
483        let (mut disk_a, mut disk_b) = (Vec::new(), Vec::new());
484        self.forward_one_plain(
485            hidden,
486            hidden,
487            state,
488            kv_cache,
489            seq_len,
490            block_table,
491            &mut disk_a,
492            &mut disk_b,
493            ctx,
494            stream,
495        )
496    }
497
498    /// One drafter CONTEXT row: `input_norm` then the DSA caches only.
499    ///
500    /// `x` is the block input (post `eh_proj`) for a row whose OUTPUT is discarded β€” a prompt
501    /// or catch-up row. See [`Glm5NextDsaLayer::write_kv_row`] for why that is enough.
502    #[allow(clippy::too_many_arguments)]
503    pub fn drafter_write_kv_row(
504        &self,
505        x: DevicePtr,
506        state: &mut dyn LayerState,
507        kv_cache: &mut PagedKvCache,
508        seq_len: usize,
509        block_table: &mut Vec<u32>,
510        ctx: &ForwardContext,
511        stream: u64,
512    ) -> Result<()> {
513        let layer = match &self.mixer {
514            Glm5NextMixer::Dsa(l) => l,
515            _ => bail!(
516                "GLM layer {}: drafter_write_kv_row is the MTP block's path; this layer is not \
517                 a DSA layer",
518                self.layer_idx
519            ),
520        };
521        let normed = ctx.buffers.norm_output();
522        self.norm(ctx.gpu, x, self.input_norm, normed, 1, stream)?;
523        layer.write_kv_row(normed, state, kv_cache, seq_len, block_table, ctx, stream)
524    }
525
526    /// One token through the whole layer, using highway slot `slot`.
527    #[allow(clippy::too_many_arguments)]
528    fn forward_one(
529        &self,
530        hidden: DevicePtr,
531        residual: DevicePtr,
532        slot: usize,
533        st: &mut dyn LayerState,
534        kv_cache: &mut PagedKvCache,
535        seq_len: usize,
536        block_table: &mut Vec<u32>,
537        disk_block_ids: &mut Vec<u32>,
538        disk_offloaded: &mut Vec<u32>,
539        ctx: &ForwardContext,
540        stream: u64,
541    ) -> Result<()> {
542        let gpu = ctx.gpu;
543        let h = self.hidden;
544        let Some(mhc) = self.mhc.as_ref() else {
545            // No hyper-connection: the MTP layer, a plain pre-norm residual block.
546            return self.forward_one_plain(
547                hidden,
548                residual,
549                st,
550                kv_cache,
551                seq_len,
552                block_table,
553                disk_block_ids,
554                disk_offloaded,
555                ctx,
556                stream,
557            );
558        };
559        let hc = mhc.hc_mult;
560        let streams = ctx.buffers.hc_streams().offset(slot * hc * h * 4);
561        let post = ctx.buffers.hc_post().offset(slot * hc * 4);
562        let comb = ctx.buffers.hc_comb().offset(slot * hc * hc * 4);
563        let normed = ctx.buffers.norm_output();
564        let ffn_out = ctx.buffers.moe_output();
565
566        let t_mhc = profile::start();
567        if self.is_first {
568            glm_hc_expand(
569                gpu,
570                mhc.kernels.hc_expand,
571                hidden,
572                streams,
573                1,
574                h as u32,
575                hc as u32,
576                stream,
577            )?;
578        }
579
580        // ── attention site ──
581        glm_hc_pre(
582            gpu,
583            &mhc.kernels,
584            streams,
585            &mhc.attn,
586            hidden,
587            post,
588            comb,
589            1,
590            h as u32,
591            hc as u32,
592            mhc.sinkhorn_iters as u32,
593            self.rms_eps,
594            mhc.hc_eps,
595            stream,
596        )?;
597        profile::end(profile::MHC, t_mhc, gpu, stream);
598        let t_norm = profile::start();
599        self.norm(gpu, hidden, self.input_norm, normed, 1, stream)?;
600        profile::end(profile::NORM, t_norm, gpu, stream);
601        let attn_out = self.mixer_forward(
602            normed,
603            residual,
604            st,
605            kv_cache,
606            seq_len,
607            block_table,
608            disk_block_ids,
609            disk_offloaded,
610            ctx,
611            stream,
612        )?;
613        // πŸ”΄ Row-parallel `o_proj` β‡’ `attn_out` is a PARTIAL SUM at TP>1. Reduce it here,
614        // before it enters the highway.
615        if self.mixer_all_reduce {
616            self.reduce_probe(profile::REDUCE_ATTN_BAR, "attn", ctx, stream);
617            let t = profile::start_hot();
618            self.reduce_partial(attn_out, 1, ctx, stream)?;
619            profile::end_nosync(profile::REDUCE_ATTN_ENQ, t);
620            // Second span times ONLY the sync: device + network + rank skew.
621            let t = profile::start_hot();
622            profile::end(profile::REDUCE_ATTN, t, ctx.gpu, stream);
623        }
624        let t_mhc_post = profile::start();
625        glm_hc_post(
626            gpu,
627            mhc.kernels.hc_post,
628            attn_out,
629            streams,
630            post,
631            comb,
632            streams,
633            1,
634            h as u32,
635            hc as u32,
636            stream,
637        )?;
638        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
639
640        // ── FFN site ──
641        let t_mhc = profile::start();
642        glm_hc_pre(
643            gpu,
644            &mhc.kernels,
645            streams,
646            &mhc.ffn,
647            hidden,
648            post,
649            comb,
650            1,
651            h as u32,
652            hc as u32,
653            mhc.sinkhorn_iters as u32,
654            self.rms_eps,
655            mhc.hc_eps,
656            stream,
657        )?;
658        profile::end(profile::MHC, t_mhc, gpu, stream);
659        let t_norm = profile::start();
660        self.norm(gpu, hidden, self.post_attn_norm, normed, 1, stream)?;
661        profile::end(profile::NORM, t_norm, gpu, stream);
662        self.mlp_forward(normed, ffn_out, 1, ctx, stream)?;
663        let t_mhc_post = profile::start();
664        glm_hc_post(
665            gpu,
666            mhc.kernels.hc_post,
667            ffn_out,
668            streams,
669            post,
670            comb,
671            streams,
672            1,
673            h as u32,
674            hc as u32,
675            stream,
676        )?;
677
678        // πŸͺ€ UNWEIGHTED mean, no weights. Not DeepSeek-V4's learned collapse.
679        if self.is_last {
680            hc_head_mean(
681                gpu,
682                mhc.kernels.hc_head,
683                streams,
684                hidden,
685                1,
686                h as u32,
687                hc as u32,
688                stream,
689            )?;
690        }
691        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
692        if self.is_last {
693            profile::step();
694        }
695        Ok(())
696    }
697
698    /// K tokens of one sequence through a KDA layer, with ONE sweep over the weights.
699    ///
700    /// The site order is exactly [`Self::forward_one`]'s β€” `hc_pre -> norm -> mixer -> hc_post`
701    /// per site, the mHC highway collapsed at the last layer β€” but every stage runs over all K
702    /// rows at once instead of K times over one. The mHC kernels, `rms_norm_vanilla` and
703    /// `Glm5NextKdaLayer::decode_k` are each grid-parallel or batched over the token axis and
704    /// each is bit-identical to the K serial calls it replaces, so an accepted draft token is
705    /// the token the unspeculated engine would have emitted.
706    ///
707    /// πŸͺ€ Highway slots are `0..K` and MUST stay per-token β€” the mHC streams are a per-token
708    /// activation that has to survive across layers, so a shared slot would leave every layer
709    /// past the first reading the last token's highway for all K rows.
710    ///
711    /// Both mixers come here: each has its own `decode_k` that batches its projections and
712    /// keeps its per-token part (KDA's recurrence, DSA's selection and gather-attend) serial.
713    #[allow(clippy::too_many_arguments)]
714    fn forward_k(
715        &self,
716        hidden: DevicePtr,
717        k: usize,
718        state: &mut dyn LayerState,
719        kv_cache: &mut PagedKvCache,
720        seq_len: usize,
721        block_table: &mut Vec<u32>,
722        ctx: &ForwardContext,
723        stream: u64,
724        take_snapshots: bool,
725        slot_base: usize,
726        // TRUE only when this call is a PREFILL sub-chunk. `forward_k` is shared by prefill
727        // and by the speculative verify, and nothing in `ForwardContext` separates them:
728        // `decode_step` is false for both, and `graph_capture` is false for prefill AND for
729        // an eager verify. The DSA layer's batched selector is qualified on prefill only, so
730        // the distinction is carried explicitly rather than re-derived downstream.
731        is_prefill: bool,
732    ) -> Result<()> {
733        let gpu = ctx.gpu;
734        let h = self.hidden;
735        let Some(mhc) = self.mhc.as_ref() else {
736            bail!("GLM layer {}: no hyper-connection bound", self.layer_idx);
737        };
738        let hc = mhc.hc_mult;
739        // `slot_base`'s base: the per-slot strides are exactly these, so K contiguous slots
740        // from there ARE the `[K, ...]` the kernels want.
741        //
742        // πŸ”΄ The highway is a PER-TOKEN activation that must survive across layers, so the slot
743        // is the token's index within the whole forward, NOT its row within this call. A verify
744        // is one call at `slot_base = 0`; a batched prefill is `ceil(N / PREFILL_ROWS)` calls
745        // that must land on disjoint slots, or sub-chunk 1 overwrites sub-chunk 0's streams and
746        // every later layer reads the wrong token's highway. ANOMALIES A65.
747        let streams = ctx.buffers.hc_streams().offset(slot_base * hc * h * 4);
748        let post = ctx.buffers.hc_post().offset(slot_base * hc * 4);
749        let comb = ctx.buffers.hc_comb().offset(slot_base * hc * hc * 4);
750        let normed = ctx.buffers.norm_output();
751        let ffn_out = ctx.buffers.moe_output();
752        let (kt, ht, hct) = (k as u32, h as u32, hc as u32);
753
754        // Per-token state snapshots a partial accept rewinds to; row `t` writes slot `t`, and
755        // the last row needs none because a full accept never rolls back. KDA only β€” DSA's
756        // per-sequence state is the indexer cache, which rewinds by a host counter.
757        let kda_ctx = match &self.mixer {
758            Glm5NextMixer::Kda { ws, .. } => {
759                if k > ws.max_tokens() {
760                    bail!(
761                        "GLM layer {}: a {k}-token verify exceeds the KDA workspace built for {}",
762                        self.layer_idx,
763                        ws.max_tokens()
764                    );
765                }
766                let st = self.kda_state(state)?;
767                // πŸ”΄ PREFILL TAKES NONE. A verify needs a per-row rewind point, so it snapshots
768                // rows `0..k-1`; prefill is never rolled back, and the intermediates pool holds
769                // only `num_spec` slots β€” indexing it for a 32-row prefill chunk would run off
770                // the end. `decode_k` reads these with `snapshots.get(row)`, so an empty slice
771                // is a clean "take none", not a special case.
772                let snaps: Vec<(DevicePtr, DevicePtr)> = if take_snapshots {
773                    (0..k.saturating_sub(1))
774                        .map(|t| (st.h_state_intermediates[t], st.conv_state_intermediates[t]))
775                        .collect()
776                } else {
777                    Vec::new()
778                };
779                Some((
780                    KdaSeqState {
781                        conv: st.conv_state,
782                        recurrent: st.h_state,
783                    },
784                    snaps,
785                ))
786            }
787            Glm5NextMixer::Dsa(_) => None,
788        };
789
790        let t_mhc = profile::start();
791        if self.is_first {
792            glm_hc_expand(
793                gpu,
794                mhc.kernels.hc_expand,
795                hidden,
796                streams,
797                kt,
798                ht,
799                hct,
800                stream,
801            )?;
802        }
803
804        // ── attention site ──
805        glm_hc_pre(
806            gpu,
807            &mhc.kernels,
808            streams,
809            &mhc.attn,
810            hidden,
811            post,
812            comb,
813            kt,
814            ht,
815            hct,
816            mhc.sinkhorn_iters as u32,
817            self.rms_eps,
818            mhc.hc_eps,
819            stream,
820        )?;
821        profile::end(profile::MHC, t_mhc, gpu, stream);
822        let t_norm = profile::start();
823        self.norm(gpu, hidden, self.input_norm, normed, k, stream)?;
824        profile::end(profile::NORM, t_norm, gpu, stream);
825        let t = profile::start();
826        let attn_out = match (&self.mixer, &kda_ctx) {
827            (Glm5NextMixer::Kda { layer, ws, .. }, Some((kda, snaps))) => {
828                layer.decode_k(gpu, normed, k, kda, ws, snaps, stream)?;
829                ws.final_out
830            }
831            (Glm5NextMixer::Dsa(layer), _) => {
832                // πŸͺ€ DSA writes its `o_proj` output back over the buffer it was handed.
833                layer.decode_k(
834                    normed,
835                    k,
836                    state,
837                    kv_cache,
838                    seq_len,
839                    block_table,
840                    ctx,
841                    stream,
842                    is_prefill,
843                )?;
844                normed
845            }
846            (Glm5NextMixer::Kda { .. }, None) => {
847                bail!("GLM layer {}: KDA mixer without KDA state", self.layer_idx)
848            }
849        };
850        profile::end(profile::KDA, t, gpu, stream);
851        if self.mixer_all_reduce {
852            self.reduce_probe(profile::REDUCE_ATTN_BAR, "attn", ctx, stream);
853            let t = profile::start_hot();
854            self.reduce_partial(attn_out, k, ctx, stream)?;
855            profile::end_nosync(profile::REDUCE_ATTN_ENQ, t);
856            let t = profile::start_hot();
857            profile::end(profile::REDUCE_ATTN, t, ctx.gpu, stream);
858        }
859        let t_mhc_post = profile::start();
860        glm_hc_post(
861            gpu,
862            mhc.kernels.hc_post,
863            attn_out,
864            streams,
865            post,
866            comb,
867            streams,
868            kt,
869            ht,
870            hct,
871            stream,
872        )?;
873        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
874
875        // ── FFN site ──
876        let t_mhc = profile::start();
877        glm_hc_pre(
878            gpu,
879            &mhc.kernels,
880            streams,
881            &mhc.ffn,
882            hidden,
883            post,
884            comb,
885            kt,
886            ht,
887            hct,
888            mhc.sinkhorn_iters as u32,
889            self.rms_eps,
890            mhc.hc_eps,
891            stream,
892        )?;
893        profile::end(profile::MHC, t_mhc, gpu, stream);
894        let t_norm = profile::start();
895        self.norm(gpu, hidden, self.post_attn_norm, normed, k, stream)?;
896        profile::end(profile::NORM, t_norm, gpu, stream);
897        self.mlp_forward(normed, ffn_out, k, ctx, stream)?;
898        let t_mhc_post = profile::start();
899        glm_hc_post(
900            gpu,
901            mhc.kernels.hc_post,
902            ffn_out,
903            streams,
904            post,
905            comb,
906            streams,
907            kt,
908            ht,
909            hct,
910            stream,
911        )?;
912        if self.is_last {
913            hc_head_mean(
914                gpu,
915                mhc.kernels.hc_head,
916                streams,
917                hidden,
918                kt,
919                ht,
920                hct,
921                stream,
922            )?;
923        }
924        profile::end(profile::MHC_POST, t_mhc_post, gpu, stream);
925        if self.is_last {
926            profile::step();
927        }
928        Ok(())
929    }
930
931    /// This KDA layer's recurrent + conv state.
932    ///
933    /// πŸ”΄ It is an [`SsmLayerState`] β€” the SAME type Qwen's GDN layers carry β€” and that is
934    /// deliberate, not incidental. `rollback_ssm_states_dispatch` walks every
935    /// `LayerType::LinearAttention` layer and downcasts to exactly this type to restore a
936    /// rejected speculative draft; GLM's KDA blocks ARE `linear_attention` in `layer_types`,
937    /// so carrying anything else means the first rejected draft is a hard error. The shapes
938    /// line up with the pool's own math: `h = nvΒ·vdΒ·kdΒ·4` and
939    /// `conv = (nkΒ·kdΒ·2 + nvΒ·vd)Β·d_convΒ·4` are byte-for-byte GLM's
940    /// `recurrent_state_elems()Β·4` and `conv_state_elems()Β·4`, because the parser fills the
941    /// `linear_*` fields from `linear_attn_config` and they are already TP-local.
942    ///
943    /// πŸͺ€ A mixer/state mismatch means the scheduler handed this layer another layer's slot.
944    /// Refuse loudly: allocating a fresh state here would decode from a zero recurrent state.
945    fn kda_state<'a>(&self, state: &'a mut dyn LayerState) -> Result<&'a mut SsmLayerState> {
946        let st = state
947            .as_any_mut()
948            .downcast_mut::<SsmLayerState>()
949            .ok_or_else(|| {
950                anyhow::anyhow!(
951                    "GLM layer {}: a KDA mixer was handed state that is not an SsmLayerState",
952                    self.layer_idx
953                )
954            })?;
955        // πŸ”΄ HF casts the KDA recurrent state to float32 and vLLM hardcodes `kda_state_dtype`.
956        // A narrowed h slot (`--ssm-h-dtype f16`/`f16-pool`) is a deviation from the
957        // reference, not a memory setting, and every KDA kernel reads FP32.
958        if st.h_is_f16 || st.h_prefill_stage.is_some() {
959            bail!(
960                "GLM layer {}: KDA recurrent state is FP32-only; --ssm-h-dtype f16 narrowed it",
961                self.layer_idx
962            );
963        }
964        Ok(st)
965    }
966
967    /// This DSA layer's indexer key cache.
968    fn dsa_state<'a>(&self, state: &'a mut dyn LayerState) -> Result<&'a mut Glm5NextDsaState> {
969        state
970            .as_any_mut()
971            .downcast_mut::<Glm5NextDsaState>()
972            .ok_or_else(|| {
973                anyhow::anyhow!(
974                    "GLM layer {}: a DSA mixer was handed state that is not a Glm5NextDsaState",
975                    self.layer_idx
976                )
977            })
978    }
979}
980
981impl TransformerLayer for Glm5NextLayer {
982    /// πŸ”΄ GLM-5.3 CANNOT serve a batched multi-sequence decode step. Two
983    /// independent row-0 aliases, both structural, either one sufficient:
984    ///
985    ///   * **D1 β€” mHC highway slot.** `Glm5NextLayer::forward_one` pins highway
986    ///     slot 0. `decode_multi_seq`'s default loop shares one
987    ///     `ForwardContext` across the batch, so every sequence would write and
988    ///     then read the SAME highway stream, and each layer past the first
989    ///     reads the last sequence's mHC state for all rows. This is the
990    ///     sequence-axis twin of the token-axis argument already written on
991    ///     [`Self::decode_batched`] above β€” the trait default is WRONG there
992    ///     for exactly the same reason.
993    ///   * **D2 β€” DSA `attn_metadata` row.** The DSA mixer reads metadata row 0
994    ///     (`glm5next_dsa/layer.rs`: slot, positions, block_table, seq_len), so
995    ///     every sequence in the batch would attend with sequence 0's page
996    ///     table and length.
997    ///
998    /// Answering `true` does NOT cost concurrency: the caller routes GLM onto
999    /// #753 item B's per-sequence highway loop, which serves C>1 correctly at
1000    /// C=1-equivalent per-request throughput.
1001    ///
1002    /// πŸ”’ This must stay `true` until the Stage 1 commit that adds a real
1003    /// `Glm5NextLayer::decode_multi_seq` (per-row `forward_one` with
1004    /// `meta_row_base` threading and `ctx.hc_row_offset` honoured as the
1005    /// highway base) flips it to `false` IN THE SAME COMMIT.
1006    fn decode_multi_seq_unsupported(&self) -> bool {
1007        true
1008    }
1009
1010    /// πŸ”΄ GLM-5.3 implements no `decode_verify_multi`, so the batched verify
1011    /// sweep must not be selected for it. The trait default already `bail!`s,
1012    /// but that is a mid-request abort; declaring it here makes
1013    /// `can_batch_verify_dispatch` route around it instead, leaving spec-on
1014    /// C>1 on the per-sequence verify loop β€” the sealed K=3 path.
1015    ///
1016    /// πŸ”’ Flipped to `false` by the PR-3 commit that adds
1017    /// `Glm5NextLayer::decode_verify_multi` (per-sequence `forward_k` sweep
1018    /// with `slot_base = meta_row_base = row_base`).
1019    fn decode_verify_multi_unsupported(&self) -> bool {
1020        true
1021    }
1022
1023    fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>> {
1024        Ok(match &self.mixer {
1025            // Pool-free fallback. `uses_ssm_pool()` is true for KDA, so the model hands these
1026            // layers a pool slot and never calls this β€” but the paths that build states
1027            // directly still need a correctly shaped, ZEROED one. A fresh sequence starts from
1028            // a zero recurrent state and an empty conv window; inheriting the previous
1029            // sequence's residue is a wrong answer that decays over a few tokens rather than
1030            // crashing.
1031            Glm5NextMixer::Kda { cfg, .. } => Box::new(alloc_kda_ssm_state(gpu, cfg)?),
1032            Glm5NextMixer::Dsa(l) => Box::new(Glm5NextDsaState::alloc(gpu, &l.cfg)?),
1033        })
1034    }
1035
1036    /// Release what `alloc_state` allocated β€” ANOMALIES A76. The DSA indexer cache is
1037    /// sized by `--max-seq-len`, not by the prompt (513 B/token/layer), so leaking one
1038    /// per request walks a unified-memory host into the ground.
1039    ///
1040    /// πŸ”΄ Type-driven on purpose. A KDA layer's state on the model path is POOL-owned
1041    /// (`uses_ssm_pool()` is true for `Kda`, so `alloc_sequence` hands it pool addresses
1042    /// and `free_sequence` skips it entirely) β€” but refusing by TYPE as well means a
1043    /// pool address can never reach `gpu.free` even if a future call site forgets the
1044    /// skip. `SsmLayerState` is therefore left alone here, always.
1045    fn release_state(&self, state: &mut dyn LayerState, gpu: &dyn GpuBackend) -> Result<()> {
1046        if let Some(dsa) = state.as_any_mut().downcast_mut::<Glm5NextDsaState>() {
1047            dsa.free(gpu)?;
1048        }
1049        Ok(())
1050    }
1051
1052    /// The DSA mixer allocates its per-sequence state with `gpu.alloc` in `alloc_state`, so
1053    /// the addresses a capture bakes belong to THAT sequence, not to the slot.
1054    ///
1055    /// πŸͺ€ Only DSA. The KDA mixer is POOL-backed on the model path β€” `uses_ssm_pool()` is true
1056    /// for `Kda`, so `meta.rs` hands it pool addresses and never calls its `alloc_state`. The
1057    /// `true` below is still correct (one owned mixer is enough); the previous wording claimed
1058    /// both mixers own their state, and that was wrong.
1059    fn graph_stale_on_new_sequence(&self) -> bool {
1060        true
1061    }
1062
1063    /// πŸ”΄ A CUDA-graph replay runs kernels and nothing else, so this layer's one piece of
1064    /// HOST-side per-sequence bookkeeping β€” the DSA indexer cache length β€” has to be advanced
1065    /// here. The inner `Glm5NextDsaLayer` implements this too, but the model's layer vec holds
1066    /// the COMPOSITE, so the inner impl is never reached and the default no-op left the
1067    /// counter frozen at its capture-time value.
1068    ///
1069    /// πŸͺ€ That was invisible on the spec-off path: after capture, `decode` never runs again, so
1070    /// nothing compared the counter to `seq_len`. The first EAGER step after a run of replays β€”
1071    /// which is exactly what a speculative verify is β€” then failed with "indexer cache holds 5
1072    /// tokens but the sequence is at 12". The rows were there; only the counter was stale.
1073    ///
1074    /// KDA keeps nothing on the host: its recurrent and conv state are device-resident and the
1075    /// replayed kernels update them in place.
1076    fn sync_replayed_step(
1077        &self,
1078        state: &mut dyn LayerState,
1079        seq_len: usize,
1080        k: usize,
1081    ) -> Result<()> {
1082        match &self.mixer {
1083            Glm5NextMixer::Dsa(_) => self.dsa_state(state)?.sync_to(seq_len, k),
1084            Glm5NextMixer::Kda { .. } => Ok(()),
1085        }
1086    }
1087
1088    /// The model's layer vec holds the COMPOSITE, so β€” exactly as with `sync_replayed_step`
1089    /// β€” the inner `Glm5NextDsaLayer` impl is never reached and this one is what runs. A62.
1090    fn check_replay_room(&self, state: &dyn LayerState, seq_len: usize, k: usize) -> Result<()> {
1091        match &self.mixer {
1092            Glm5NextMixer::Dsa(_) => state
1093                .as_any()
1094                .downcast_ref::<Glm5NextDsaState>()
1095                .ok_or_else(|| {
1096                    anyhow::anyhow!(
1097                        "GLM layer {}: a DSA mixer was handed state that is not a \
1098                         Glm5NextDsaState",
1099                        self.layer_idx
1100                    )
1101                })?
1102                .ensure_room_through(seq_len + k)
1103                // πŸ”΄ Both A62 routes raise the SAME refusal, so without a tag a log cannot
1104                // tell a pre-launch replay refusal from `indexer_forward`'s prefill one β€”
1105                // and "we proved the replay route" would rest on inference. Name it.
1106                .with_context(|| {
1107                    format!(
1108                        "DSA replay pre-check (layer {}, before launch_graph, seq_len \
1109                         {seq_len} + k {k})",
1110                        self.layer_idx
1111                    )
1112                }),
1113            Glm5NextMixer::Kda { .. } => Ok(()),
1114        }
1115    }
1116
1117    /// GLM's KDA blocks are `linear_attention` in `layer_types` AND carry the pool's
1118    /// `SsmLayerState`, so they take pool slots like any other recurrent layer. That is what
1119    /// buys the speculative-verify checkpoints and per-token intermediates for free β€”
1120    /// `meta.rs` only wires `h_state_checkpoint` / `h_state_intermediates` for layers that
1121    /// answer true here, and `rollback_ssm_states_dispatch` needs both.
1122    fn uses_ssm_pool(&self) -> bool {
1123        matches!(self.mixer, Glm5NextMixer::Kda { .. })
1124    }
1125
1126    #[allow(clippy::too_many_arguments)]
1127    fn decode(
1128        &self,
1129        hidden: DevicePtr,
1130        residual: DevicePtr,
1131        state: &mut dyn LayerState,
1132        kv_cache: &mut PagedKvCache,
1133        seq_len: usize,
1134        block_table: &mut Vec<u32>,
1135        disk_block_ids: &mut Vec<u32>,
1136        disk_last_offloaded_per_layer: &mut Vec<u32>,
1137        ctx: &ForwardContext,
1138        stream: u64,
1139    ) -> Result<()> {
1140        self.forward_one(
1141            hidden,
1142            residual,
1143            0,
1144            state,
1145            kv_cache,
1146            seq_len,
1147            block_table,
1148            disk_block_ids,
1149            disk_last_offloaded_per_layer,
1150            ctx,
1151            stream,
1152        )
1153    }
1154
1155    /// Prefill, one token at a time β€” but with the highway indexed by token.
1156    ///
1157    /// πŸ”΄ The trait's default fallback would be WRONG here, not merely slow. It runs every token
1158    /// through this layer before the next layer sees any of them, so a single-slot highway would
1159    /// hold only the last token's streams by the time layer `n+1` reads it. The mHC highway is a
1160    /// per-token activation that must survive across layers, so each token gets its own slot.
1161    ///
1162    /// Per-token (rather than chunked) is deliberate for this slice: KDA's recurrence is
1163    /// sequential anyway, and the chunked `Glm5NextKdaLayer::prefill` needs a workspace sized
1164    /// for the chunk. That is an optimisation, explicitly out of scope.
1165    #[allow(clippy::too_many_arguments)]
1166    fn prefill(
1167        &self,
1168        hidden: DevicePtr,
1169        residual: DevicePtr,
1170        num_tokens: usize,
1171        state: &mut dyn LayerState,
1172        kv_cache: &mut PagedKvCache,
1173        seq_len_start: usize,
1174        block_table: &mut Vec<u32>,
1175        disk_block_ids: &mut Vec<u32>,
1176        disk_last_offloaded_per_layer: &mut Vec<u32>,
1177        _kv_write_start: usize,
1178        ctx: &ForwardContext,
1179        stream: u64,
1180    ) -> Result<()> {
1181        let cap = ctx.buffers.max_batch_tokens();
1182        if num_tokens > cap {
1183            bail!(
1184                "GLM layer {}: prefill of {num_tokens} tokens exceeds the {cap}-token mHC \
1185                 highway the buffer arena was sized for; each token needs its own slot",
1186                self.layer_idx
1187            );
1188        }
1189        // ── Batched sub-chunks: ONE weight sweep per PREFILL_ROWS tokens ──
1190        //
1191        // πŸ”΄ ANOMALIES A65. The per-token walk below pays a full sweep of this layer's weights
1192        // for EVERY token, which is why prefill ran at decode speed (15.2 tok/s measured, and
1193        // 97 % linear in the token count: TTFT 579.1 s at 9,000 tokens and 1,187.6 s at 18,000,
1194        // a ratio of 2.051 against 2.000 for pure-linear).
1195        //
1196        // `forward_k` is the SAME body the speculative verify uses and already sweeps once for
1197        // all its rows, so this is reuse, not a new path. What it does NOT yet amortize is the
1198        // routed MoE: `forward_moe`'s expert-union arm is capped at 4 rows (the union kernel
1199        // resolves `rows * top_k <= 64` ids in one block), so above that each row still pays its
1200        // own 8 experts. That caps the win here at ~5x β€” Atlas's own sizing note puts KDA at
1201        // 9,366 MB/token against ~2.1 GB/token of routed-expert traffic, so amortizing the
1202        // former is most of the prize and the latter needs a grouped MoE GEMM (separate lane).
1203        //
1204        // πŸͺ€ `mhc: None` is the MTP drafter block, whose plain residual path `forward_k` does
1205        // not implement β€” it bails on a missing highway. That block keeps the per-token walk.
1206        let rows = if self.mhc.is_some() {
1207            prefill_rows().min(cap)
1208        } else {
1209            1
1210        };
1211        if rows > 1 {
1212            let mut t = 0usize;
1213            while t < num_tokens {
1214                let k = rows.min(num_tokens - t);
1215                self.forward_k(
1216                    hidden.offset(t * self.hidden * 2),
1217                    k,
1218                    state,
1219                    kv_cache,
1220                    seq_len_start + t,
1221                    block_table,
1222                    ctx,
1223                    stream,
1224                    // Prefill is never rolled back, so it takes no per-row KDA snapshots.
1225                    false,
1226                    // Absolute slot within this prefill, so sub-chunks never share a slot.
1227                    t,
1228                    // This IS the prefill sub-chunk caller.
1229                    true,
1230                )?;
1231                t += k;
1232            }
1233            return Ok(());
1234        }
1235        for t in 0..num_tokens {
1236            let off = t * self.hidden * 2;
1237            self.forward_one(
1238                hidden.offset(off),
1239                residual.offset(off),
1240                t,
1241                state,
1242                kv_cache,
1243                seq_len_start + t,
1244                block_table,
1245                disk_block_ids,
1246                disk_last_offloaded_per_layer,
1247                ctx,
1248                stream,
1249            )?;
1250        }
1251        Ok(())
1252    }
1253
1254    /// K tokens of ONE sequence in a single call β€” the speculative-verify body.
1255    ///
1256    /// The same per-token walk as [`Self::prefill`] β€” same highway slots, same
1257    /// `seq_len + t` positions, same KV writes β€” plus the per-token KDA state snapshots that
1258    /// only a verify needs. Prefill is never rolled back, so it does not pay for them.
1259    ///
1260    /// πŸ”΄ The trait's default would be WRONG, not merely slow: it calls `decode` per token, and
1261    /// `decode` pins highway slot 0. K tokens would then overwrite each other's mHC streams and
1262    /// every layer past the first would read the last token's highway for all K rows.
1263    ///
1264    /// βœ… **Batched.** This delegates to `Self::forward_k`, which sweeps the weights ONCE for
1265    /// all K rows. (An earlier revision of this comment said "still one `forward_one` per row";
1266    /// that was stale β€” `forward_k` has been the body since the batched-verify work, and the
1267    /// measured K=3 step of ~101 ms against a ~63 ms single-row step is only explicable by it.)
1268    #[allow(clippy::too_many_arguments)]
1269    fn decode_batched(
1270        &self,
1271        hidden: DevicePtr,
1272        _residual: DevicePtr,
1273        num_tokens: usize,
1274        state: &mut dyn LayerState,
1275        kv_cache: &mut PagedKvCache,
1276        seq_len: usize,
1277        block_table: &mut Vec<u32>,
1278        _disk_block_ids: &mut Vec<u32>,
1279        _disk_last_offloaded_per_layer: &mut Vec<u32>,
1280        ctx: &ForwardContext,
1281        stream: u64,
1282    ) -> Result<()> {
1283        // A KDA layer must leave behind the state it held after EACH verify token, or a
1284        // partially-accepted draft has nothing to rewind to. `rollback_ssm_states_dispatch`
1285        // restores `h_state_intermediates[num_accepted - 1]`, so row `t` writes slot `t` and
1286        // the LAST row needs none (a full accept never rolls back).
1287        let kda_bytes = matches!(self.mixer, Glm5NextMixer::Kda { .. }).then_some(());
1288        if kda_bytes.is_some() && num_tokens > 1 {
1289            let st = self.kda_state(state)?;
1290            // πŸ”΄ Bail rather than skip. Skipping leaves `h_state` ADVANCED past the accepted
1291            // boundary with no error and no log line, which corrupts every subsequent decode
1292            // and surfaces much later as gibberish. Same check the Qwen verify arms make.
1293            if st.h_state_intermediates.len() + 1 < num_tokens
1294                || st.conv_state_intermediates.len() + 1 < num_tokens
1295            {
1296                bail!(
1297                    "GLM layer {}: a {num_tokens}-token verify needs {} per-token state \
1298                     snapshots but the pool has h={} conv={}. With none, this is the \
1299                     self-speculative / ngram path on a model whose MTP pool was never \
1300                     sized; with too few, --num-drafts exceeds the pool's tier.",
1301                    self.layer_idx,
1302                    num_tokens - 1,
1303                    st.h_state_intermediates.len(),
1304                    st.conv_state_intermediates.len(),
1305                );
1306            }
1307        }
1308
1309        // ONE sweep over the weights for all K rows β€” the whole reason speculation pays.
1310        self.forward_k(
1311            hidden,
1312            num_tokens,
1313            state,
1314            kv_cache,
1315            seq_len,
1316            block_table,
1317            ctx,
1318            stream,
1319            true,
1320            0,
1321            // A speculative verify, NOT a prefill sub-chunk β€” true here would hand an eager
1322            // verify the prefill-only batched DSA selector.
1323            false,
1324        )
1325    }
1326
1327    /// KDA layers carry recurrent state; DSA layers do not.
1328    fn is_ssm_layer(&self) -> bool {
1329        matches!(self.mixer, Glm5NextMixer::Kda { .. })
1330    }
1331}
1332
1333#[cfg(test)]
1334mod tests;