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}