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;