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}