spark_model/layer/transformer_layer.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! `TransformerLayer` trait — composable per-layer forward/decode hooks.
4
5use anyhow::Result;
6use avarok_core::config::ModelConfig;
7use spark_runtime::gpu::{DevicePtr, GpuBackend};
8use spark_runtime::kv_cache::PagedKvCache;
9
10use super::{BatchedAttnMetadata, ForwardContext, GdnPrefillBuffers, LayerState};
11
12mod default_loops;
13
14/// Batched-verify WY pointer-table layout (SSOT — writer:
15/// `TransformerModel::upload_verify_wy_tables`, reader: the qwen3_ssm
16/// `GdnStates::Multi` batched conv+WY arm).
17///
18/// Per GDN layer, four device pointer tables laid back-to-back:
19/// `[h_state | Hi0 | Hi1 | Hi2]`, each `VERIFY_WY_TABLE_SEQS` u64 entries
20/// (one per batched-verify sequence, unused tail entries zero). The
21/// per-layer slice handed to `decode_verify_multi` is
22/// `VERIFY_WY_LAYER_STRIDE_BYTES` long. At K<4 only the first `k` tables
23/// are filled (`[h | Hi_0..Hi_{k-2}]`); the layout/strides are constant so
24/// the reader's offsets never depend on the ladder step.
25///
26/// 32 (2026-07-30, spec at n=32): THE batched-verify sequence envelope —
27/// the stash slots, the `can_batch_verify` n-cap and these tables are all
28/// sized from this one const. History: 4 → 16 (at 4 the upload returned
29/// NULL for every n>4 batch, silently declining the cross-sequence conv+WY
30/// fast path at exactly the concurrencies it was built for) → 32 (the
31/// 32:1 ladder rung: n=32 × k=2 = 64 verify rows; kernels index a table by
32/// sequence and receive each table's base pointer separately, so widening
33/// is a pure host-side layout change). 48 GDN layers x 4 tables x 32
34/// entries x 8 B = 48 KB.
35pub const VERIFY_WY_TABLE_SEQS: usize = 32;
36/// Tables per GDN layer: h_state + Hi0..Hi14 (K=16 verify → 15
37/// intermediates). 4 → 16 (2026-09-01): the wyN pointer-table twins
38/// extend the cross-sequence batched verify to K=5..16, so the staging
39/// gate `(2..=THIS)` must admit gamma-width verifies; at 4 every K>4
40/// batch silently declined to the per-sequence loop (measured 09-01:
41/// 187,392 wy10 launches = 27.7% of GPU time in a C=16 prose window).
42/// Cost: 48 layers x 16 x 32 x 8 B = 192 KB of host+device tables.
43pub const VERIFY_WY_TABLES_PER_LAYER: usize = 16;
44/// Bytes between consecutive tables within a layer slice.
45pub const VERIFY_WY_TABLE_STRIDE_BYTES: usize = VERIFY_WY_TABLE_SEQS * 8;
46/// Bytes between consecutive GDN layers' table slices.
47pub const VERIFY_WY_LAYER_STRIDE_BYTES: usize =
48 VERIFY_WY_TABLES_PER_LAYER * VERIFY_WY_TABLE_STRIDE_BYTES;
49
50pub trait TransformerLayer: Send + Sync {
51 /// True when this layer's PREFILL attends only over the tokens it is
52 /// handed, so a prefix-cache skip would hide the cached prefix from
53 /// attention entirely. MLA layers on the paged path do; everything else
54 /// reads the paged cache and is unaffected.
55 fn uses_local_mla_prefill(&self) -> bool {
56 false
57 }
58
59 /// `&mut dyn Any` downcast hook for post-construction weight overlays (e.g.
60 /// the LoRA install walk). Default `None`; overlay-capable layers override.
61 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
62 None
63 }
64
65 /// Write-on-accept capability (GDN layers only): the per-sequence stash
66 /// size in f32 elements when this layer can run the K=4 write-on-accept
67 /// twin (kernels linked, write-on-accept not disabled), `None` otherwise.
68 /// The model sizes the stash from it and binds with [`Self::gdn_woa_bind`].
69 fn gdn_woa_stash_seq_floats(&self) -> Option<usize> {
70 None
71 }
72
73 /// Bind this layer's write-on-accept engaged word and stash slab (`seqs`
74 /// sequences of [`Self::gdn_woa_stash_seq_floats`] f32 each). Called once
75 /// by the model, pre-capture, on the first write-on-accept request; the
76 /// addresses are baked into verify graphs afterwards and never move.
77 fn gdn_woa_bind(&self, _flag: DevicePtr, _stash: DevicePtr, _seqs: usize) {}
78
79 /// Write-on-accept fold (GDN layers only): apply the accepted rows of the
80 /// last batched K=4 verify to this layer's h states. `h_table` is the
81 /// layer's slab-0 pointer table from the verify, `na_tab` a device
82 /// `u32[n]` of accepted row counts in batch order. `Ok(false)` when the
83 /// layer has nothing bound (not a GDN layer, or write-on-accept is off).
84 /// When the parent kernel ran for the last verify (engaged word 0) the
85 /// fold performs the parent's partial-accept restore instead.
86 fn gdn_fold_accepted(
87 &self,
88 _gpu: &dyn GpuBackend,
89 _h_table: DevicePtr,
90 _na_tab: DevicePtr,
91 _k_rows: usize,
92 _n: usize,
93 _stream: u64,
94 ) -> Result<bool> {
95 Ok(false)
96 }
97
98 /// Whether this layer's ONLINE FP8-KV calibration has frozen its scale.
99 /// `None` = this layer runs no online calibration (non-attention layer,
100 /// static checkpoint scales, or a non-FP8 KV dtype). The scheduler's
101 /// graph-suppression gate keys off this rather than a token count: the
102 /// scale freezes on the FIRST observe, so waiting `calibration_tokens`
103 /// tokens would run ~256+ eager steps for a calibration that finished
104 /// immediately.
105 fn fp8_calibration_frozen(&self) -> Option<bool> {
106 None
107 }
108
109 /// Hoisted per-step HOST work for layers that do host-side computation
110 /// at decode (PLE: n-gram hash + NVMe fault-in + slot upload). The
111 /// scheduler calls this every single-token decode step BEFORE any CUDA
112 /// graph replay/capture — the same phasing as the `token_ids` upload —
113 /// so the captured graph contains only kernels over stable device
114 /// buffers. Layers with no host-side decode work keep the no-op default.
115 fn decode_prestage(
116 &self,
117 _token: u32,
118 _state: &mut dyn LayerState,
119 _gpu: &dyn GpuBackend,
120 _stream: u64,
121 ) -> Result<()> {
122 Ok(())
123 }
124
125 /// Re-arm consumed prestage state so a failed CUDA-graph capture attempt
126 /// can re-run the SAME step eagerly. Must be idempotent, and must not
127 /// recompute (PLE's history already advanced in `decode_prestage`).
128 fn decode_prestage_rearm(&self, _state: &mut dyn LayerState) {}
129
130 /// True when this layer's decode can NEVER be captured into a CUDA
131 /// graph — e.g. the QSA indexer's host top-k round trip, whose captured
132 /// dense fallback would silently replay WRONG attention once selection
133 /// activates. The scheduler ORs this across layers once and keeps the
134 /// whole model eager.
135 fn decode_graph_unsupported(&self) -> bool {
136 false
137 }
138
139 /// True when this layer cannot serve a BATCHED multi-sequence decode step
140 /// — i.e. `decode_multi_seq`'s shared-`ForwardContext` loop would alias
141 /// per-sequence state across rows rather than merely run slowly.
142 ///
143 /// Mirrors [`Self::decode_graph_unsupported`] exactly: layer-level
144 /// statement, default `false`, ORed across layers by the caller and
145 /// consumed at the DISPATCH site. A `true` layer is NOT refused
146 /// concurrency — it is routed onto the per-sequence highway loop that
147 /// #753 item B already built for mHC models, so C>1 keeps serving.
148 ///
149 /// Wired at BOTH multi-seq callers (`decode_a2`'s `hc_perseq` and
150 /// `decode_b`'s `hc_qsa_perseq`), because `decode_b` is the single-GPU
151 /// fused decode+prefill path and a decision made only in `decode_a2`
152 /// leaves it exposed.
153 fn decode_multi_seq_unsupported(&self) -> bool {
154 false
155 }
156
157 /// True when this layer keeps per-sequence state that lowering the
158 /// sequence's KV cursor does NOT rewind, so a content-loop rollback
159 /// (`rollback_to_boundary`: drop the degenerate tail, lower `seq_len`,
160 /// re-steer) would regenerate on state still conditioned on the
161 /// dropped tokens. Pure paged-KV attention rewinds by cursor and keeps
162 /// the default; SSM layers rewind through the decode snapshot ring;
163 /// a layer that owns a monotonic cache count, a running compressor
164 /// group or an n-gram history answers `true` here and the scheduler
165 /// declines the rollback (hard stop) instead of corrupting the tail.
166 ///
167 /// Same shape as [`Self::decode_graph_unsupported`]: layer-level
168 /// statement, default `false`, ORed across layers by the model.
169 fn decode_rollback_unsupported(&self) -> bool {
170 false
171 }
172
173 /// True when this layer cannot serve a BATCHED multi-sequence VERIFY
174 /// sweep (`decode_verify_multi`). Consumed by
175 /// `can_batch_verify_dispatch`; a `true` layer falls back to the
176 /// per-sequence verify loop, which is the sealed single-sequence path.
177 ///
178 /// Separate from [`Self::decode_multi_seq_unsupported`] because the two
179 /// answers can differ: verify carries its rows on the `k` axis with its
180 /// own R-row metadata block, decode carries them on the sequence axis.
181 fn decode_verify_multi_unsupported(&self) -> bool {
182 false
183 }
184
185 /// Marconi aux state: host-serialized per-layer SEQUENCE state that must
186 /// travel with an SSM snapshot for a prefix-cache hit to be complete —
187 /// PLE's n-gram history + conv state, QSA's ingested indexer keys.
188 /// Without these a restored prefix would silently serve the PREVIOUS
189 /// request's lexical state. Called at chunk-boundary snapshot saves;
190 /// any D2H inside must be stream-ordered (`copy_d2h_on_stream`).
191 /// Default: the layer carries no aux sequence state.
192 fn snapshot_aux(
193 &self,
194 _state: &dyn LayerState,
195 _gpu: &dyn GpuBackend,
196 _stream: u64,
197 ) -> Result<Option<Vec<u8>>> {
198 Ok(None)
199 }
200
201 /// True when this layer WOULD produce aux state — restore sites use it
202 /// to decline snapshots that lack aux rather than restore a stale mix.
203 fn has_aux_state(&self) -> bool {
204 false
205 }
206
207 /// Restore the aux state captured by [`Self::snapshot_aux`] on a
208 /// prefix-cache hit, BEFORE the resumed prefill runs.
209 fn restore_aux(
210 &self,
211 _state: &mut dyn LayerState,
212 _blob: &[u8],
213 _gpu: &dyn GpuBackend,
214 _stream: u64,
215 ) -> Result<()> {
216 anyhow::bail!("restore_aux on a layer with no aux state")
217 }
218
219 /// Decode one token through this layer, modifying `hidden` in-place.
220 ///
221 /// # Arguments
222 /// * `hidden` - [1, hidden_size] BF16, read and written
223 /// * `residual` - [1, hidden_size] BF16, scratch for residual stream
224 /// * `state` - Per-layer state (empty for attention, SSM state for recurrent)
225 /// * `kv_cache` - Paged KV cache (may be mutated for block allocation)
226 /// * `seq_len` - Current sequence length (for position encoding + cache)
227 /// * `block_table` - Sequence's block table (may grow if new blocks needed)
228 /// * `ctx` - Shared forward context (buffers, gpu, config)
229 /// * `stream` - CUDA stream handle
230 fn decode(
231 &self,
232 hidden: DevicePtr,
233 residual: DevicePtr,
234 state: &mut dyn LayerState,
235 kv_cache: &mut PagedKvCache,
236 seq_len: usize,
237 block_table: &mut Vec<u32>,
238 // `--high-speed-swap` disk-side IDs parallel to `block_table` (Phase
239 // 6.1.c). Layer-agnostic: the same ID indexes a slot in every
240 // layer's on-disk file. Empty when the feature is disabled.
241 disk_block_ids: &mut Vec<u32>,
242 // Per-layer offload progress (Phase 6.1.d critical fix). Layer L
243 // reads/writes `disk_last_offloaded_per_layer[L]`. Each layer's
244 // offload runs independently because each layer writes its own
245 // K/V to a separate region of the on-disk file. Empty when HSS
246 // is disabled; SSM/MoE layers ignore it.
247 disk_last_offloaded_per_layer: &mut Vec<u32>,
248 ctx: &ForwardContext,
249 stream: u64,
250 ) -> Result<()>;
251
252 /// Prefill N tokens through this layer using GEMM-batched projections.
253 ///
254 /// Used during prompt processing: reads weight matrices once for all N
255 /// tokens (GEMM M=N) instead of N separate GEMV calls. Attention uses
256 /// Flash Attention on contiguous Q/K/V. SSM/GDN recurrence remains
257 /// sequential per-token.
258 ///
259 /// # Arguments
260 /// * `hidden` - [N, hidden_size] BF16, read and written
261 /// * `residual` - [N, hidden_size] BF16, scratch for residual stream
262 /// * `num_tokens` - Number of tokens (N)
263 /// * `state` - Per-layer state (SSM state updated sequentially)
264 /// * `kv_cache` - Paged KV cache (attention layers write K/V for all N)
265 /// * `seq_len_start` - Sequence position of first token (usually 0)
266 /// * `block_table` - Block table for KV cache (pre-allocated for N tokens)
267 /// * `ctx` - Shared forward context (buffers, gpu, config)
268 /// * `stream` - CUDA stream handle
269 ///
270 /// Default: falls back to sequential single-token decode calls.
271 ///
272 /// `kv_write_start`: number of tokens whose KV cache entries are already
273 /// populated (prefix caching). Attention layers skip KV writes for
274 /// positions `< kv_write_start`. SSM layers ignore this (recurrent).
275 #[allow(clippy::too_many_arguments)]
276 /// Does a captured decode graph go STALE when a new sequence takes this slot?
277 ///
278 /// 🔴 `decode_graph` is keyed by `slot_idx` on the premise that the only per-sequence
279 /// addresses a capture bakes live in the SSM pool, which is slot-addressed and stable.
280 /// A layer that allocates its own per-sequence state (GLM-5.3 allocates a fresh indexer
281 /// cache and KDA state per sequence) breaks that premise: the next sequence gets new
282 /// buffers and the old graph still reads and writes the freed ones — the second request
283 /// continues the first one's text. Such a layer says so here and `free_sequence` drops
284 /// the slot's graph, costing one re-capture per request.
285 fn graph_stale_on_new_sequence(&self) -> bool {
286 false
287 }
288
289 /// Reconcile whatever HOST-side per-sequence bookkeeping a step would have done, when
290 /// that step was served by a replayed CUDA graph instead of being run. `seq_len` is the
291 /// sequence length BEFORE this step's `k` rows.
292 ///
293 /// 🔴 A graph replay executes kernels and nothing else: the layer's `decode` never runs,
294 /// so a layer that tracks its own cache length on the host silently stops advancing and
295 /// every replayed step overwrites the same row.
296 ///
297 /// 🔴 It is a RECONCILE, not an advance. A K-row verify writes K rows and the scheduler
298 /// then keeps only the accepted prefix, so the counter has to be rewound to `seq_len`
299 /// first — exactly what `decode_k`'s own lockstep check does on the eager path. Advancing
300 /// blindly leaves the counter (k - accepted) ahead of the sequence on every rejected
301 /// draft, and that drift is ANOMALIES A56: the DRAFTER writes its indexer rows at
302 /// `state.len()`, so a counter running ahead lands them on rows the target then selects
303 /// over. Default is a no-op — only a layer with host-side state (GLM-5.3's DSA indexer
304 /// cache) needs this.
305 fn sync_replayed_step(
306 &self,
307 _state: &mut dyn LayerState,
308 _seq_len: usize,
309 _k: usize,
310 ) -> Result<()> {
311 Ok(())
312 }
313
314 /// Refuse a step whose writes would land past a host-tracked cache — BEFORE the graph
315 /// that performs them is replayed.
316 ///
317 /// 🔴 `sync_replayed_step` above is the RECONCILE and it deliberately runs AFTER
318 /// `launch_graph`, which is too late to prevent a write. A replayed `dsa_indexer_store`
319 /// places its row from a DEVICE position with no host code in the loop, so at the DSA
320 /// ceiling it writes one row past the buffer and the refusal arrives afterwards. The
321 /// resulting `CUDA_ERROR_ILLEGAL_ADDRESS (700)` is STICKY: it fails every later CUDA
322 /// call in the context, so one over-long sequence takes the serve down for every
323 /// subsequent request while the health endpoints keep answering 200. ANOMALIES A62.
324 ///
325 /// `seq_len` is the length BEFORE this step's `k` rows, so the step ends at
326 /// `seq_len + k` — the same post-condition `sync_replayed_step` reconciles to. Default
327 /// is a no-op: only a layer with host-side cache bookkeeping needs it.
328 fn check_replay_room(&self, _state: &dyn LayerState, _seq_len: usize, _k: usize) -> Result<()> {
329 Ok(())
330 }
331
332 fn prefill(
333 &self,
334 hidden: DevicePtr,
335 residual: DevicePtr,
336 num_tokens: usize,
337 state: &mut dyn LayerState,
338 kv_cache: &mut PagedKvCache,
339 seq_len_start: usize,
340 block_table: &mut Vec<u32>,
341 disk_block_ids: &mut Vec<u32>,
342 disk_last_offloaded_per_layer: &mut Vec<u32>,
343 _kv_write_start: usize,
344 ctx: &ForwardContext,
345 stream: u64,
346 ) -> Result<()> {
347 default_loops::prefill_default(
348 self,
349 hidden,
350 residual,
351 num_tokens,
352 state,
353 kv_cache,
354 seq_len_start,
355 block_table,
356 disk_block_ids,
357 disk_last_offloaded_per_layer,
358 ctx,
359 stream,
360 )
361 }
362
363 /// Two-phase SSM prefill — Phase 1: projections and GDN input staging.
364 ///
365 /// Runs RMS norm, QKVZ projection, BA+gates, conv1d, and L2 norm for a
366 /// chunk of `num_tokens` tokens, then copies the GDN inputs (packed QKV,
367 /// gate/beta, Z) into the full-sequence `gdn_bufs` at `token_offset`.
368 ///
369 /// Does NOT run the GDN recurrence — that happens in `prefill_gdn_full`
370 /// after all chunks have staged their inputs.
371 ///
372 /// Attention layers: default falls back to full `prefill` (no phasing).
373 #[allow(clippy::too_many_arguments)]
374 fn prefill_phase1(
375 &self,
376 hidden: DevicePtr,
377 residual: DevicePtr,
378 num_tokens: usize,
379 state: &mut dyn LayerState,
380 kv_cache: &mut PagedKvCache,
381 seq_len_start: usize,
382 block_table: &mut Vec<u32>,
383 disk_block_ids: &mut Vec<u32>,
384 disk_last_offloaded_per_layer: &mut Vec<u32>,
385 kv_write_start: usize,
386 gdn_bufs: &GdnPrefillBuffers,
387 token_offset: usize,
388 ctx: &ForwardContext,
389 stream: u64,
390 ) -> Result<()> {
391 // Default: fall back to full prefill (attention layers, non-SSM layers)
392 let _ = (gdn_bufs, token_offset);
393 self.prefill(
394 hidden,
395 residual,
396 num_tokens,
397 state,
398 kv_cache,
399 seq_len_start,
400 block_table,
401 disk_block_ids,
402 disk_last_offloaded_per_layer,
403 kv_write_start,
404 ctx,
405 stream,
406 )
407 }
408
409 /// M1 large-M batched Phase-1: token-parallel projections (RMS/QKVZ/BA-gates)
410 /// over ALL stacked tokens in one large-M GEMM each. SSM-only; the caller
411 /// runs `prefill_phase1_conv1d_one` per request then `prefill_phase1_l2_batched`.
412 fn prefill_phase1_proj_batched(
413 &self,
414 hidden_stacked: DevicePtr,
415 residual_stacked: DevicePtr,
416 total_tokens: usize,
417 gdn_bufs: &GdnPrefillBuffers,
418 ctx: &ForwardContext,
419 stream: u64,
420 ) -> Result<()> {
421 let _ = (
422 hidden_stacked,
423 residual_stacked,
424 total_tokens,
425 gdn_bufs,
426 ctx,
427 stream,
428 );
429 anyhow::bail!("prefill_phase1_proj_batched: only implemented for SSM layers")
430 }
431
432 /// M1: per-request conv1d tail (advances per-request conv_state), reading the
433 /// request's slice of the stacked QKVZ scratch and writing into gdn_bufs.qkv.
434 fn prefill_phase1_conv1d_one(
435 &self,
436 state: &mut dyn LayerState,
437 token_offset: usize,
438 len: usize,
439 gdn_bufs: &GdnPrefillBuffers,
440 ctx: &ForwardContext,
441 stream: u64,
442 ) -> Result<()> {
443 let _ = (state, token_offset, len, gdn_bufs, ctx, stream);
444 anyhow::bail!("prefill_phase1_conv1d_one: only implemented for SSM layers")
445 }
446
447 /// M1: batched L2 norm over the full stacked QKV buffer after all per-request
448 /// conv1d tails have written their slices.
449 fn prefill_phase1_l2_batched(
450 &self,
451 total_tokens: usize,
452 gdn_bufs: &GdnPrefillBuffers,
453 ctx: &ForwardContext,
454 stream: u64,
455 ) -> Result<()> {
456 let _ = (total_tokens, gdn_bufs, ctx, stream);
457 anyhow::bail!("prefill_phase1_l2_batched: only implemented for SSM layers")
458 }
459
460 /// Two-phase SSM prefill — Phase 2: GDN recurrence on the full sequence.
461 ///
462 /// Runs the WY4-persistent GDN kernel over all `total_len` tokens in
463 /// `gdn_bufs` in a single launch. The kernel reads packed QKV and
464 /// gate/beta from the full-sequence buffers and writes the GDN output.
465 ///
466 /// Only meaningful for SSM layers. Attention layers return `Ok(())`.
467 fn prefill_gdn_full(
468 &self,
469 _state: &mut dyn LayerState,
470 _gdn_bufs: &GdnPrefillBuffers,
471 _ctx: &ForwardContext,
472 _stream: u64,
473 ) -> Result<()> {
474 Ok(()) // No-op for attention layers
475 }
476
477 /// Q12 Path B: batched attention prefill across N stacked-input streams.
478 ///
479 /// Runs the full attention-layer prefill (rms_norm + residual, QKV proj,
480 /// RoPE, KV-write, batched attention compute, O proj, post-attn norm,
481 /// FFN, final residual) over `num_tokens = batch_size * chunk_len`
482 /// stacked tokens, using `batched_meta` for per-stream metadata
483 /// resolution.
484 ///
485 /// Default impl returns Err — only `Qwen3AttentionLayer` overrides.
486 /// SSM/dense layers don't override (they have their own batched paths
487 /// or work without batched metadata).
488 ///
489 /// Caller (model-level `prefill_attn_batched_layer`) is responsible for
490 /// ensuring all streams share the same chunk_len, seq_len_start
491 /// (q_offset), and that the layer is not MLA / not HDIM=512 / not HSS-
492 /// engaged. The override bails Err if any unsupported case is detected.
493 fn prefill_inner_batched_q12(
494 &self,
495 _hidden_stacked: DevicePtr,
496 _residual_stacked: DevicePtr,
497 _num_tokens: usize,
498 _kv_cache: &mut PagedKvCache,
499 _seq_len_start: usize,
500 _batched_meta: &BatchedAttnMetadata,
501 _ctx: &ForwardContext,
502 _stream: u64,
503 ) -> Result<()> {
504 anyhow::bail!("prefill_inner_batched_q12: not implemented for this layer type")
505 }
506
507 /// Q12 Path B: batched GDN recurrence across N streams.
508 ///
509 /// Runs the same WY32 / persistent / split4 GDN kernel as
510 /// `prefill_gdn_full` but with `batch_size = batch_size` and
511 /// `h_state_ptrs` pointing to a device array of N per-stream h_state
512 /// pointers (staged by `TransformerModel::stage_h_state_ptrs`).
513 /// `gdn_bufs.qkv` / `gate_beta` / `output` are stacked across N
514 /// streams contiguously: each stream's data lives at
515 /// `b * chunk_len * conv_dim` (BF16) within the buffer.
516 ///
517 /// Default impl returns `Err` — the SSM layer override implements the
518 /// actual batched dispatch using the kernel handles loaded in
519 /// commit `8d07ca4`. Attention layers don't override (they don't
520 /// have a GDN step).
521 fn prefill_gdn_full_batched(
522 &self,
523 _h_state_ptrs: DevicePtr,
524 _gdn_bufs: &GdnPrefillBuffers,
525 _batch_size: u32,
526 _chunk_len: u32,
527 _ctx: &ForwardContext,
528 _stream: u64,
529 ) -> Result<()> {
530 anyhow::bail!(
531 "prefill_gdn_full_batched: layer does not implement batched GDN \
532 — caller should fall back to per-stream prefill_gdn_full"
533 )
534 }
535
536 /// VARLEN batched GDN: process ragged co-dispatch lengths via `cu_seqlens` in
537 /// ONE `gdn_prefill_fla(batch=N, is_varlen)` call (replaces the non-uniform
538 /// per-request loop → fills chunk_delta_h's 32→32N CTAs). Returns `Ok(true)`
539 /// if it ran, `Ok(false)` if not eligible (caller falls back to the loop).
540 /// Default (non-SSM layers, or FLA disabled): `Ok(false)`.
541 #[allow(clippy::too_many_arguments)]
542 fn prefill_gdn_full_batched_fla_varlen(
543 &self,
544 _h_state_ptrs: DevicePtr,
545 _gdn_bufs: &GdnPrefillBuffers,
546 _batch_size: u32,
547 _cu_seqlens: DevicePtr,
548 _max_num_chunks: u32,
549 _total_nt: usize,
550 _max_seqlen: u32,
551 _ctx: &ForwardContext,
552 _stream: u64,
553 ) -> Result<bool> {
554 Ok(false)
555 }
556
557 /// Two-phase SSM prefill — Phase 3: post-GDN processing.
558 ///
559 /// Reads GDN output and Z gate from `gdn_bufs` at `token_offset`,
560 /// then runs gated RMS norm, output projection, residual add, and MoE
561 /// for the chunk of `num_tokens` tokens.
562 ///
563 /// Only meaningful for SSM layers. Attention layers return `Ok(())`.
564 #[allow(clippy::too_many_arguments)]
565 fn prefill_phase3(
566 &self,
567 _hidden: DevicePtr,
568 _residual: DevicePtr,
569 _num_tokens: usize,
570 _gdn_bufs: &GdnPrefillBuffers,
571 _token_offset: usize,
572 _ctx: &ForwardContext,
573 _stream: u64,
574 ) -> Result<()> {
575 Ok(()) // No-op for attention layers
576 }
577
578 /// Returns true if this layer is an SSM layer (supports two-phase prefill).
579 ///
580 /// When true, the model loop can use `prefill_phase1` / `prefill_gdn_full` /
581 /// `prefill_phase3` instead of the monolithic `prefill`.
582 fn is_ssm_layer(&self) -> bool {
583 false
584 }
585
586 /// Allocate the transposed MoE expert weights used by the coalesced
587 /// prefill GEMM kernels. Called as a post-load pass from `factory::build`
588 /// after LM-head NVFP4 quantization has freed BF16 headroom, so
589 /// memory-tight EP configurations (e.g. MiniMax M2.7-NVFP4 EP=2) can
590 /// fit the transpose that layer-0 preflight would otherwise reject.
591 ///
592 /// Default: no-op (non-MoE layers, and MoE layers whose loader already
593 /// called `MoeLayer::transpose_for_prefill` inline during construction).
594 fn transpose_moe_for_prefill(
595 &mut self,
596 _gpu: &dyn GpuBackend,
597 _config: &ModelConfig,
598 ) -> Result<()> {
599 Ok(())
600 }
601
602 /// Like `transpose_moe_for_prefill` but only transposes the gate+up
603 /// projections (skips the down projection), reducing the transpose cost
604 /// from 3× to 2× per expert. Used as a memory-tight fallback by the
605 /// MiniMax loader when full transpose doesn't fit.
606 fn transpose_moe_gate_up_for_prefill(
607 &mut self,
608 _gpu: &dyn GpuBackend,
609 _config: &ModelConfig,
610 ) -> Result<()> {
611 Ok(())
612 }
613
614 /// Wire a shared per-prefill `down_proj` transpose scratch into this
615 /// layer's MoE block. Used as a memory-tight alternative to the
616 /// persistent down transpose: factory allocates one shared scratch,
617 /// every MoE layer reuses it layer-by-layer during sequential
618 /// prefill. No-op for non-MoE layers and MoE layers that already
619 /// have a persistent transposed down.
620 fn set_moe_down_transpose_scratch(
621 &mut self,
622 _scratch_packed: DevicePtr,
623 _scratch_scale: DevicePtr,
624 _packed_ptrs_t: DevicePtr,
625 _scale_ptrs_t: DevicePtr,
626 ) {
627 }
628
629 /// Phase 8a unified-layout MoE transpose: build persistent transposed
630 /// gate/up/down for all experts and free the untransposed copies.
631 /// Phased flow keeps memory budget tight enough for MiniMax M2.7 EP=2.
632 /// After this call, the untransposed-layout decode kernels can no
633 /// longer execute correctly — `MoeLayer::use_t_layout_for_decode()` must
634 /// gate dispatch to the `_t` decode kernels. Default no-op.
635 fn transpose_moe_for_prefill_unified(
636 &mut self,
637 _gpu: &dyn GpuBackend,
638 _config: &ModelConfig,
639 ) -> Result<()> {
640 Ok(())
641 }
642
643 /// Block C Path 2 hybrid-layout MoE transpose: build persistent
644 /// transposed gate/up/down alongside the untransposed originals (no
645 /// frees). Doubles MoE-weight memory but recovers the ~15 % decode
646 /// regression of pure unified mode — decode + MTP verify dispatch
647 /// keeps using the warp-reduction kernels on the originals while
648 /// prefill (forward_batched) routes through transposed kernels.
649 /// Caller must verify enough free memory before invocation. Default
650 /// no-op for non-MoE layers.
651 fn transpose_moe_for_prefill_hybrid(
652 &mut self,
653 _gpu: &dyn GpuBackend,
654 _config: &ModelConfig,
655 ) -> Result<()> {
656 Ok(())
657 }
658
659 /// Decode K tokens through this layer using GEMM-batched projections.
660 ///
661 /// Used for speculative decode verification: processes multiple tokens
662 /// per layer with GEMM for weight-heavy projections (amortizes bandwidth)
663 /// and sequential ops for stateful/recurrent components.
664 ///
665 /// # Arguments
666 /// * `hidden` - [K, hidden_size] BF16, read and written (K tokens contiguous)
667 /// * `residual` - [K, hidden_size] BF16, scratch for residual stream
668 /// * `num_tokens` - Number of tokens (K)
669 /// * `state` - Per-layer state
670 /// * `kv_cache` - Paged KV cache
671 /// * `seq_len` - Starting sequence length (before these tokens)
672 /// * `block_table` - Block table for KV cache
673 /// * `ctx` - Shared context
674 /// * `stream` - CUDA stream
675 ///
676 /// Default: falls back to sequential single-token decode calls.
677 #[allow(clippy::too_many_arguments)]
678 fn decode_batched(
679 &self,
680 hidden: DevicePtr,
681 residual: DevicePtr,
682 num_tokens: usize,
683 state: &mut dyn LayerState,
684 kv_cache: &mut PagedKvCache,
685 seq_len: usize,
686 block_table: &mut Vec<u32>,
687 disk_block_ids: &mut Vec<u32>,
688 disk_last_offloaded_per_layer: &mut Vec<u32>,
689 ctx: &ForwardContext,
690 stream: u64,
691 ) -> Result<()> {
692 default_loops::decode_batched_default(
693 self,
694 hidden,
695 residual,
696 num_tokens,
697 state,
698 kv_cache,
699 seq_len,
700 block_table,
701 disk_block_ids,
702 disk_last_offloaded_per_layer,
703 ctx,
704 stream,
705 )
706 }
707
708 /// Decode N sequences through this layer in a single batched call.
709 ///
710 /// Each sequence contributes 1 token. The weight matrices are loaded
711 /// once and applied to all N sequences (amortizing memory bandwidth).
712 ///
713 /// # Arguments
714 /// * `hidden` - [N, hidden_size] BF16, contiguous
715 /// * `residual` - [N, hidden_size] BF16, contiguous
716 /// * `num_seqs` - Number of sequences (N)
717 /// * `states` - N per-layer states (one per sequence)
718 /// * `kv_cache` - Shared paged KV cache
719 /// * `ctx` - Forward context (attn_metadata contains N-sequence metadata)
720 /// * `stream` - CUDA stream
721 ///
722 /// Default: falls back to N sequential single-token decode calls.
723 #[allow(clippy::too_many_arguments)]
724 fn decode_multi_seq<'a, 'b: 'a>(
725 &self,
726 hidden: DevicePtr,
727 residual: DevicePtr,
728 num_seqs: usize,
729 states: &'a mut [&'b mut (dyn LayerState + 'static)],
730 kv_cache: &mut PagedKvCache,
731 seq_lens: &[usize],
732 block_tables: &[Vec<u32>],
733 ctx: &ForwardContext,
734 stream: u64,
735 ) -> Result<()> {
736 default_loops::decode_multi_seq_default(
737 self,
738 hidden,
739 residual,
740 num_seqs,
741 states,
742 kv_cache,
743 seq_lens,
744 block_tables,
745 ctx,
746 stream,
747 )
748 }
749
750 /// Batched MTP verify: `n_seqs` sequences × `k` tokens through this layer
751 /// in ONE weight sweep (rows seq-major, `r = i*k + j`, contiguous in
752 /// `hidden`/`residual`). Projections/FFN batch across all `n_seqs*k` rows;
753 /// the stateful recurrence (conv/GDN) runs per-sequence against
754 /// `states[i]` with row-offset buffer bases — per-sequence math is
755 /// byte-identical to the single-sequence `decode_batched` K-token body.
756 ///
757 /// Only SSM layers override (attention layers are handled by the caller
758 /// via `decode_multi_seq`, which already takes per-row block tables and
759 /// seq lens). Default: unsupported.
760 ///
761 /// `wy_tables`: this layer's slice of the model-staged WY pointer tables
762 /// (layout above, `VERIFY_WY_LAYER_STRIDE_BYTES`; refreshed pre-graph
763 /// every step) enabling the single-launch table-form WY batch. NULL →
764 /// the layer keeps its per-sequence WY path.
765 #[allow(clippy::too_many_arguments)]
766 fn decode_verify_multi<'a, 'b: 'a>(
767 &self,
768 _hidden: DevicePtr,
769 _residual: DevicePtr,
770 _n_seqs: usize,
771 _ks: &[usize],
772 _states: &'a mut [&'b mut (dyn LayerState + 'static)],
773 _kv_cache: &mut PagedKvCache,
774 _wy_tables: DevicePtr,
775 _ctx: &ForwardContext,
776 _stream: u64,
777 ) -> Result<()> {
778 anyhow::bail!("decode_verify_multi: unsupported for this layer type")
779 }
780
781 /// Allocate per-sequence state for this layer.
782 ///
783 /// Called once when a new sequence is created. Returns:
784 /// - `EmptyLayerState` for pure attention layers
785 /// - `SsmLayerState` for SSM/recurrent layers
786 fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>>;
787
788 /// Release the per-sequence state `alloc_state` produced, plus anything
789 /// the layer attached to it lazily afterwards.
790 ///
791 /// Called once per sequence from the teardown chokepoint
792 /// (`free_sequence_dispatch`). The default no-op is correct for layers
793 /// whose state owns no device memory (`EmptyLayerState`) and for state
794 /// that comes from a pool reclaimed by slot (`SsmLayerState`'s h/conv,
795 /// released via `ssm_pool.release_slot`).
796 ///
797 /// It exists because `LayerState` implementors hold BARE `DevicePtr`s:
798 /// dropping the box reclaims the host struct and leaks the device buffer.
799 /// The QSA indexer carry (~739 MB per request at 200K context across the
800 /// 12 full-attention layers) and the PLE conv carry both leaked this way.
801 /// On unified memory such a leak is invisible to RSS and reported as N/A
802 /// by `nvidia-smi`, so it surfaces only as the host exhausting RAM with no
803 /// process to blame.
804 ///
805 /// MUST be idempotent — teardown can run after a partial failure. Callers
806 /// log errors and continue rather than aborting: a sequence that cannot
807 /// free its state is still finished, and bailing would strand the rest.
808 /// Owns every device allocation reachable from this `LayerState` that the layer obtained
809 /// from `gpu.alloc`, whether in `alloc_state` or attached later. Idempotent; nulls what it
810 /// frees; never touches pool addresses.
811 ///
812 /// 🔴 Refuse by TYPE inside the impl, not by a filter at the call site. A call-site filter
813 /// is a second spelling of "is this pooled?" that can drift out of agreement with the
814 /// first; the type check lives where the knowledge is.
815 ///
816 /// 🔴 Invariant L2 (slot reuse), NOT a line order. It is tempting to write "the graph drop
817 /// must come before this call" — that over-states a call order as an invariant. The real
818 /// requirement is that when a slot is re-occupied, its graphs are destroyed AND its owned
819 /// pointers are freed and nulled. Nothing between the two blocks replays a graph, and
820 /// `destroy_graph` does not dereference baked pointers, so either order satisfies it.
821 /// ANOMALIES A56 is the history; slot reuse is the invariant.
822 fn release_state(&self, _state: &mut dyn LayerState, _gpu: &dyn GpuBackend) -> Result<()> {
823 Ok(())
824 }
825
826 /// Does this layer's recurrent state live in the shared SSM pool?
827 ///
828 /// `true` (the default) is the long-standing arrangement: sequence setup
829 /// sees `LayerType::LinearAttention` and hands the layer an `SsmLayerState`
830 /// pointing at pool-owned addresses, so `alloc_state` is never consulted.
831 ///
832 /// 🪤 A linear-attention mixer with its OWN state type must return `false`,
833 /// or it is handed an `SsmLayerState` and the downcast in its forward path
834 /// fails at layer 0 on the first request. GLM-5.3's KDA blocks are the case:
835 /// they are `linear_attention` in `layer_types` but carry
836 /// `Glm5NextLayerState::Kda`.
837 fn uses_ssm_pool(&self) -> bool {
838 true
839 }
840}
841
842#[cfg(test)]
843mod verify_wy_table_tests {
844 use super::{
845 VERIFY_WY_LAYER_STRIDE_BYTES, VERIFY_WY_TABLE_SEQS, VERIFY_WY_TABLE_STRIDE_BYTES,
846 VERIFY_WY_TABLES_PER_LAYER,
847 };
848
849 /// The batched verify's widest arm is K=16, which stages h + Hi0..Hi14
850 /// = 16 slabs. The staging gate in verify_e2.rs admits
851 /// `(2..=VERIFY_WY_TABLES_PER_LAYER)`, so this const IS the k ceiling:
852 /// if it shrinks below 16, every gamma>3 batch silently declines to the
853 /// per-sequence loop again (the 187,392-launch storm of 2026-09-01).
854 #[test]
855 fn tables_per_layer_fund_the_widest_verify_arm() {
856 assert!(
857 (2..=VERIFY_WY_TABLES_PER_LAYER).contains(&16usize),
858 "VERIFY_WY_TABLES_PER_LAYER ({VERIFY_WY_TABLES_PER_LAYER}) must admit k=16"
859 );
860 }
861
862 /// The table-form wyN kernels REINTERPRET the stride argument as
863 /// "pointer entries between Hi slabs" and the dispatcher passes
864 /// `VERIFY_WY_TABLE_SEQS`. That only reaches slab t iff slabs are laid
865 /// out back-to-back at exactly SEQS entries (8 bytes each). Pin the
866 /// layout the kernels assume.
867 #[test]
868 fn slab_stride_matches_the_kernel_contract() {
869 assert_eq!(VERIFY_WY_TABLE_STRIDE_BYTES, VERIFY_WY_TABLE_SEQS * 8);
870 assert_eq!(
871 VERIFY_WY_LAYER_STRIDE_BYTES,
872 VERIFY_WY_TABLES_PER_LAYER * VERIFY_WY_TABLE_STRIDE_BYTES
873 );
874 }
875
876 /// C=16 batches (plus ghost entries) must fit the per-slab entry count.
877 #[test]
878 fn seqs_capacity_covers_c16() {
879 const { assert!(VERIFY_WY_TABLE_SEQS >= 16) };
880 }
881}