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