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