metrale_model_layers/
layer.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Types every [`TransformerLayer`] shares: per-sequence layer state, the
4//! per-pass `ForwardContext`, and the attention and GDN metadata a pass hands its layers.
5//!
6//! Owner: model-layers.
7//! Invariants: none beyond the types.
8
9use std::any::Any;
10
11use metrale_config::ModelConfig;
12use metrale_gpu_runtime::buffers::BufferArena;
13use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
14
15mod transformer_layer;
16pub use transformer_layer::{
17    GdnCarryBinding, LayerAuxState, LayerCapabilities, LayerGraphHooks, LayerSplitPrefill,
18    LayerWeightSetup, LayerWriteOnAccept, MTP_CATCHUP_MAX, TransformerLayer,
19    VERIFY_WY_LAYER_STRIDE_BYTES, VERIFY_WY_TABLE_SEQS, VERIFY_WY_TABLE_STRIDE_BYTES,
20    VERIFY_WY_TABLES_PER_LAYER,
21};
22
23/// 2026-09-25: One layer's per-sequence state, reached through `as_any` downcasts.
24/// Implementors here: [`EmptyLayerState`], [`AttnLayerState`] and [`SsmLayerState`].
25pub trait LayerState: Send + Sync {
26    fn as_any(&self) -> &dyn Any;
27    fn as_any_mut(&mut self) -> &mut dyn Any;
28}
29
30/// 2026-09-25: State of a layer that keeps nothing per sequence.
31pub struct EmptyLayerState;
32
33/// 2026-09-25: Per-sequence state of a Qwen3 attention layer. The KV cache lives
34/// outside it; the one field is the QSA indexer carry, created on first use and only
35/// on a layer that has a QSA indexer.
36#[derive(Default)]
37pub struct AttnLayerState {
38    pub qsa: Option<crate::layers::qsa::QsaSeqState>,
39}
40
41impl LayerState for AttnLayerState {
42    fn as_any(&self) -> &dyn Any {
43        self
44    }
45    fn as_any_mut(&mut self) -> &mut dyn Any {
46        self
47    }
48}
49
50impl LayerState for EmptyLayerState {
51    fn as_any(&self) -> &dyn Any {
52        self
53    }
54    fn as_any_mut(&mut self) -> &mut dyn Any {
55        self
56    }
57}
58
59/// 2026-09-25: Per-sequence state of a recurrent (SSM/GDN) layer: the recurrent h state
60/// and the conv1d window, plus the copies speculative decode rolls back to.
61pub struct SsmLayerState {
62    /// 2026-09-25: Recurrent state; FP32, or FP16 when `h_is_f16`.
63    pub h_state: DevicePtr,
64    /// 2026-09-25: Conv1d window, FP32 `[conv_dim, d_conv]`.
65    pub conv_state: DevicePtr,
66    /// 2026-09-25: Copy of `h_state` restored when a verify accepts no draft. A pool
67    /// address, or allocated on first use by the checkpoint save
68    /// (`checkpoint_ssm_states_dispatch`, `start_checkpoint_async_dispatch`).
69    pub h_state_checkpoint: Option<DevicePtr>,
70    /// 2026-09-25: Copy of `conv_state`, restored with `h_state_checkpoint`.
71    pub conv_state_checkpoint: Option<DevicePtr>,
72    /// 2026-09-25: Per-token h states of a verify: element i holds the state after
73    /// verify token i. A rollback to `n` accepted tokens copies element `n - 1`.
74    pub h_state_intermediates: Vec<DevicePtr>,
75    /// 2026-09-25: Per-token conv states of a verify, indexed like
76    /// `h_state_intermediates`.
77    pub conv_state_intermediates: Vec<DevicePtr>,
78    /// 2026-09-25: Storage format of `h_state`: `false` = FP32, `true` = FP16.
79    ///
80    /// A sequence on an f16-sized pool (`h_prefill_stage` is `Some`) starts `true`: the
81    /// slot holds only 2 bytes per element. On an FP32-sized pool it starts `false`, and
82    /// `TransformerModel::ssm_h_to_f16_dispatch` converts the state and sets it `true` on
83    /// the first decode step when FP16 h-state is enabled.
84    pub h_is_f16: bool,
85    /// 2026-09-25: FP32 staging blob for this sequence's slot when the pool is f16-sized
86    /// (`--ssm-h-dtype f16-pool`). GDN prefill widens `h_state` into it before its FP32
87    /// kernels and narrows it back after (`qwen3_ssm::ssm_h_fp16`). `None` means the slot
88    /// is FP32 and prefill writes `h_state` in place. One blob per slot, shared by every
89    /// layer of the sequence.
90    pub h_prefill_stage: Option<DevicePtr>,
91    /// 2026-09-25: PLE per-sequence carry (token history and dilated-conv state), created
92    /// on first use, only on a layer that hosts a `PleLayer`.
93    pub ple: Option<crate::layers::ple::PleSeqState>,
94}
95
96impl LayerState for SsmLayerState {
97    fn as_any(&self) -> &dyn Any {
98        self
99    }
100    fn as_any_mut(&mut self) -> &mut dyn Any {
101        self
102    }
103}
104
105/// 2026-09-25: Device addresses of one pass's attention metadata, uploaded by the model
106/// before its layer loop. On a decode pass each row is one sequence, with one entry (or one
107/// block-table row) per row in each array. A prefill pass has `num_seqs == 1` and one
108/// `positions` and `slot` entry per token.
109#[derive(Clone, Copy)]
110pub struct AttnMetadataDev {
111    /// 2026-09-25: Positions, u32, one per token row. Under MRoPE this is the temporal (T)
112    /// stream.
113    pub positions: DevicePtr,
114    /// 2026-09-25: MRoPE height (H) positions. The same pointer as `positions` except on a
115    /// prefill pass that uses MRoPE (`prefill_b/forward_layers.rs`).
116    pub positions_h: DevicePtr,
117    /// 2026-09-25: MRoPE width (W) positions, set like `positions_h`.
118    pub positions_w: DevicePtr,
119    /// 2026-09-25: KV write slot of each token row, i64.
120    pub slot: DevicePtr,
121    /// 2026-09-25: Sequence lengths, one 32-bit entry per row. On decode each is the length
122    /// including the token being decoded (`seq_len + 1`).
123    pub seq_len: DevicePtr,
124    /// 2026-09-25: Block tables, 32-bit entries, row-major.
125    pub block_table: DevicePtr,
126    /// 2026-09-25: Entries per row of `block_table`.
127    pub max_blocks_per_seq: u32,
128    /// 2026-09-25: Rows in the batch, padding rows included; 1 for single-sequence decode.
129    pub num_seqs: u32,
130    /// 2026-09-25: Per-row LoRA adapter slot, i32 (one per token on prefill), at a fixed
131    /// address, so the batched bgmv can run inside a captured decode graph. A negative slot
132    /// applies no delta (`lora_bgmv.cu`); padding rows are `-1`. `DevicePtr(0)` when no LoRA
133    /// weights are loaded, or on a single-request pass whose request uses the active adapter
134    /// (`upload_seq_slot_uniform`); the apply sites then skip the bgmv.
135    pub seq_slot: DevicePtr,
136    /// 2026-09-25: Per-row adapter map for the batched-decode MoE expert fold, `[num_seqs]`
137    /// i32 in the fixed-address `TransformerModel::moe_row_adapter_buf`, built by
138    /// [`crate::lora::build_moe_row_adapter_decode`]. `< 0` means no fold on that row;
139    /// `>= 0` folds the active adapter's per-expert delta. `DevicePtr(0)` when no LoRA
140    /// weights are loaded and on every path that does not upload it. Unlike `seq_slot`,
141    /// no row resolves a negative slot to the active adapter.
142    pub moe_row_adapter: DevicePtr,
143}
144
145/// 2026-09-25: Device metadata for one kernel-batched prefill chunk over several streams,
146/// built by `stage_batched_attn_metadata` (model-engine `prefill_b/stage_batched.rs`) and
147/// passed to `prefill_attn_batched_layer` and `prefill_ssm_batched_layer`. Positions and
148/// KV slots are stacked in `cu_seqlens` order; block tables and sequence lengths are
149/// per-stream pointer arrays.
150pub struct BatchedAttnMetadata {
151    /// 2026-09-25: Stacked positions, `[total_tokens]` u32.
152    pub positions_stacked: DevicePtr,
153    /// 2026-09-25: Stacked MRoPE H positions; the same pointer as `positions_stacked`
154    /// without MRoPE.
155    pub positions_h_stacked: DevicePtr,
156    /// 2026-09-25: Stacked MRoPE W positions; the same pointer as `positions_stacked`
157    /// without MRoPE.
158    pub positions_w_stacked: DevicePtr,
159    /// 2026-09-25: Stacked KV write slots, `[total_tokens]` i64.
160    pub slot_stacked: DevicePtr,
161    /// 2026-09-25: `[batch_size]` device pointers, each to one stream's block table.
162    pub block_table_ptrs: DevicePtr,
163    /// 2026-09-25: `[batch_size]` device pointers, each to one stream's seq_len.
164    pub seq_len_ptrs: DevicePtr,
165    // 2026-09-25: The per-stream `h_state` pointers are per layer, so they are not
166    // here: `prefill_ssm_batched_layer` stages them for each SSM layer after this block.
167    /// 2026-09-25: Number of streams.
168    pub batch_size: u32,
169    /// 2026-09-25: The largest per-stream token count. Per-stream counts are in
170    /// `cu_seqlens`.
171    pub chunk_len: u32,
172    /// 2026-09-25: Tokens stacked across all streams (`cu_seqlens_host[batch_size]`).
173    pub total_tokens: u32,
174    /// 2026-09-25: `[batch_size + 1]` i32 prefix sum of per-stream token counts, on the
175    /// device.
176    pub cu_seqlens: DevicePtr,
177    /// 2026-09-25: Host copy of `cu_seqlens`; the model slices per-stream token ranges
178    /// with it.
179    pub cu_seqlens_host: Vec<i32>,
180    /// 2026-09-25: Per-stream KV length, `[batch_size]` i32 on the device: the stream's
181    /// chunk start plus its token count. The batched attention needs it per stream; one
182    /// value at the maximum would give a short stream the longest stream's causal bound
183    /// and block count.
184    pub kv_lens: DevicePtr,
185    /// 2026-09-25: Host copy of `kv_lens`.
186    pub kv_lens_host: Vec<i32>,
187    /// 2026-09-25: The largest block count of any stream.
188    pub max_blocks_per_seq: u32,
189    /// 2026-09-25: Bytes this block occupies in scratch, from the offset it was staged at
190    /// through the end of `kv_lens`. The caller advances its scratch cursor by it before
191    /// placing the per-layer `h_state` pointer array (`prefill_b/batch_kernel.rs`), so an
192    /// undercount would put that array over live metadata.
193    pub staged_bytes: usize,
194}
195
196/// 2026-09-25: Whole-prompt GDN buffers for the split SSM prefill: `prefill_phase1`
197/// writes the inputs, `prefill_gdn_full` runs the recurrence into `output`, and
198/// `prefill_phase3` reads `output` and `z`.
199///
200/// Each token's QKV is packed as `conv_dim` BF16 elements, `[Q(key_dim) | K(key_dim) |
201/// V(value_dim)]`, and the GDN kernels read Q, K and V with a stride of `conv_dim`.
202pub struct GdnPrefillBuffers {
203    /// 2026-09-25: Packed Q/K/V, `[total_len, conv_dim]` BF16.
204    pub qkv: DevicePtr,
205    /// 2026-09-25: Gate and beta, `[total_len, 2 * num_v_heads]` FP32: per token
206    /// `[gate(nv) | beta(nv)]`.
207    pub gate_beta: DevicePtr,
208    /// 2026-09-25: GDN recurrence output, `[total_len, value_dim]` BF16.
209    pub output: DevicePtr,
210    /// 2026-09-25: Z gate for the gated RMSNorm, `[total_len, value_dim]` BF16.
211    pub z: DevicePtr,
212    /// 2026-09-25: Token rows the buffers hold for this call.
213    pub total_len: usize,
214}
215
216/// 2026-09-25: What one forward pass hands every layer: the GPU, buffers, config, the
217/// model's switches and the pass's metadata.
218pub struct ForwardContext<'a> {
219    pub buffers: &'a BufferArena,
220    /// 2026-09-25: First mHC highway row of this pass. The prefill chunk of a fused
221    /// decode+prefill step uses `padded_n` (`decode_b.rs`), so its highway rows sit above
222    /// the decode rows as in hidden/residual. Every other constructor passes 0 or forwards
223    /// the value of the context it wraps.
224    pub hc_row_offset: usize,
225    pub gpu: &'a dyn GpuBackend,
226    pub config: &'a ModelConfig,
227    /// 2026-09-25: This model's GEMM selection per projection (`layers::ops::GemmDispatch`).
228    pub dispatch: &'a crate::layers::ops::GemmDispatch,
229    /// 2026-09-25: This model's memo of re-encoded weight copies
230    /// (`layers::ops::DerivedWeights`).
231    pub derived: &'a crate::layers::ops::DerivedWeights,
232    /// 2026-09-25: This model's kernel-path switches other than GEMM selection
233    /// (`layers::ops::ModelLevers`).
234    pub levers: &'a crate::layers::ops::ModelLevers,
235    /// 2026-09-25: This model's diagnostic counters and one-shot latches
236    /// (`layers::ops::ModelStats`). They belong to the model, so none spans a model swap.
237    pub stats: &'a crate::layers::ops::ModelStats,
238    /// 2026-09-25: This pass's attention metadata, or `None`. The kernel-batched prefill
239    /// passes `None` and hands its layers a `BatchedAttnMetadata` instead.
240    pub attn_metadata: Option<AttnMetadataDev>,
241    /// 2026-09-25: Profiling: the layers that read it synchronize after each timed step and
242    /// log its time.
243    pub profile: bool,
244    /// 2026-09-25: Collective backend for cross-rank reductions, such as the MoE
245    /// expert-parallel all-reduce; `None` when the pass has none.
246    pub comm: Option<&'a dyn metrale_comm::CommBackend>,
247    /// 2026-09-25: True when the pass runs under CUDA graphs (`use_graphs` in `decode_a.rs`).
248    /// The MoE expert-parallel all-reduce then uses `all_reduce`, and `all_reduce_async`
249    /// otherwise (`layers/moe/forward.rs`).
250    pub graph_capture: bool,
251    /// 2026-09-25: True on decode steps (one token per sequence: `decode_a.rs`,
252    /// `decode_a2.rs`), whose `attn_metadata` holds one row per sequence.
253    ///
254    /// `prefill_default` runs a layer that has no `prefill` of its own by calling its
255    /// `decode` once per token with the prefill context, whose `positions` and `slot` are
256    /// per-token arrays and whose `block_table` and `seq_len` can be NULL. A layer that
257    /// reads them as decode metadata checks this flag, not `attn_metadata.is_some()`.
258    pub decode_step: bool,
259    /// 2026-09-25: True when this prefill pass continues from a restored SSM snapshot (a
260    /// warm prefix-cache hit). The Qwen3 SSM prefill then skips the FLA chunked GDN
261    /// kernels (`trait_prefill_recur.rs`, `trait_prefill_gdn.rs`).
262    pub gdn_exact_replay: bool,
263    /// 2026-09-25: Asks the batched GDN verify for write-on-accept: a layer that engages it
264    /// runs `gated_delta_rule_wy4_woa`, and the caller folds the accepted rows through
265    /// `Model::gdn_fold_accepted` after the verdict. True only for a batched verify that
266    /// requested it (`VerifyBatchedOpts::write_on_accept`, set by the DFlash batch step) and
267    /// had the WY tables staged and the stash bound (`verify_e.rs`).
268    pub gdn_write_on_accept: bool,
269    /// 2026-09-25: Device `[num_tokens]` u32 ids of this pass's tokens, in row order, at a
270    /// stable address; single-token decode uploads the id before any graph replay.
271    /// Hash-routed MoE layers (`tid2eid`) read it, and PLE does when `host_token_ids` is
272    /// `None`. `None` on passes that stage no ids.
273    pub token_ids: Option<DevicePtr>,
274    /// 2026-09-25: Host copy of this pass's token ids when the caller has them: a decode
275    /// step whose tokens are on the host, and the prefill chunk of a fused decode+prefill
276    /// step (`decode_b.rs`); `None` on other passes. PLE hashes n-gram ids on the host and
277    /// prefers this slice; without it, PLE reads `token_ids` back from the device, which
278    /// it refuses during graph capture (`ple/layer.rs`).
279    pub host_token_ids: Option<&'a [u32]>,
280    /// 2026-09-25: The request slot's LoRA pairs, indexed by global layer
281    /// (`len == num_hidden_layers`). `Some` only on a prefill pass whose request routes to
282    /// a non-active slot (`TransformerModel::routed_slot_layers`). The K/V/O prefill apply
283    /// sites then apply the request slot's pair (`lora::select_routed_pair`) instead of
284    /// the per-row bgmv.
285    pub routed_lora_layers: Option<&'a [Option<crate::lora::LoraLayerWeights>]>,
286    /// 2026-09-25: Mid-chunk SSM tail capture for this prefill pass (model-engine
287    /// `prefill_b/midchunk_capture.rs`); `None` when the pass plans none. SSM layers split
288    /// their conv and recurrence kernels at `cap_local` and copy the state at that point
289    /// into the reserved snapshot slot.
290    pub midchunk_capture: Option<MidchunkCapture<'a>>,
291    /// 2026-09-25: MoE-LoRA fold decision for this pass. `TransformerModel` passes
292    /// `moe_lora_route` for a request's prefill, `decode_moe_route` on decode, verify and
293    /// draft passes, and `Refuse` on a kernel-batched multi-stream prefill. Read by the MoE
294    /// LoRA fold hooks (`layers/moe/lora.rs`).
295    pub moe_lora_route: MoeLoraRoute,
296}
297
298/// 2026-09-25: Per-pass descriptor for mid-chunk SSM tail capture: the reserved snapshot
299/// slot's per-SSM-layer destinations and the split point in pass-local token coordinates.
300///
301/// Each SSM layer's prefill takes one ordinal from `ssm_layer_counter`, in model order, and
302/// uses it to index `h_dsts`/`conv_dsts`.
303pub struct MidchunkCapture<'a> {
304    /// 2026-09-25: Capture the state after this many tokens of the pass
305    /// (`tb - proc_start`).
306    pub cap_local: usize,
307    /// 2026-09-25: Per-SSM-layer h_state destination in the reserved slot.
308    pub h_dsts: &'a [DevicePtr],
309    /// 2026-09-25: Per-SSM-layer conv_state destination in the reserved slot.
310    pub conv_dsts: &'a [DevicePtr],
311    /// 2026-09-25: Bytes per layer of h_state.
312    pub h_bytes: usize,
313    /// 2026-09-25: Bytes per layer of conv_state.
314    pub conv_bytes: usize,
315    /// 2026-09-25: Per-pass SSM-layer ordinal counter, starting at 0.
316    pub ssm_layer_counter: &'a std::sync::atomic::AtomicUsize,
317    /// 2026-09-25: Split point of a second capture one KV block earlier, at
318    /// `tb - block_size`; `Some` only when the pass covers that point and a second snapshot
319    /// slot was reserved.
320    pub cap_local_early: Option<usize>,
321    /// 2026-09-25: Per-SSM-layer h_state destination in the `tb - block_size` slot.
322    pub h_dsts_early: &'a [DevicePtr],
323    /// 2026-09-25: Per-SSM-layer conv_state destination in the `tb - block_size` slot.
324    pub conv_dsts_early: &'a [DevicePtr],
325    /// 2026-10-01: The rows from `cap_local` on must get the bits a prefill pass starting
326    /// at `cap_local` would give them, the pass a later request runs when it restores the
327    /// captured state (a tail-split capture, `prefill_plan::tail_split_point`): the GDN
328    /// recurrence after the split takes the exact-replay arm, and attention runs those rows
329    /// as their own paged call. False for the `metrale_scale` tail capture.
330    pub replay_tail: bool,
331}
332
333impl AttnMetadataDev {
334    /// 2026-10-01: The metadata of rows `row..` of a prefill pass: every per-row array
335    /// (positions, slots, the LoRA slot) starts `row` rows later; the block table and
336    /// sequence length are the pass's own.
337    pub fn rows_from(self, row: usize) -> Self {
338        let per_row = |p: DevicePtr, bytes: usize| {
339            if p.is_null() {
340                p
341            } else {
342                p.offset(row * bytes)
343            }
344        };
345        Self {
346            positions: per_row(self.positions, 4),
347            positions_h: per_row(self.positions_h, 4),
348            positions_w: per_row(self.positions_w, 4),
349            slot: per_row(self.slot, 8),
350            seq_slot: per_row(self.seq_slot, 4),
351            ..self
352        }
353    }
354}
355
356/// 2026-09-25: MoE-LoRA fold decision for one forward pass. One MoE adapter is active at
357/// a time; `MoeLayer::moe_route_gate` (`layers/moe/lora.rs`) folds on `Fold`, skips on
358/// `Skip` and returns an error on `Refuse`. `lora::resolve_moe_lora_route` maps a
359/// request's `adapter_slot` to a route.
360#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
361pub enum MoeLoraRoute {
362    /// 2026-09-25: The request uses the active MoE adapter, or no MoE delta is installed.
363    /// The default.
364    #[default]
365    Fold,
366    /// 2026-09-25: No fold. `resolve_moe_lora_route` returns it for a base request
367    /// (`adapter_slot < 0`).
368    Skip,
369    /// 2026-09-25: The request routes to an adapter other than the active one, or the pass
370    /// is a kernel-batched multi-stream prefill; the fold returns an error instead of
371    /// applying the active adapter to rows it does not own.
372    Refuse,
373}
374
375#[cfg(test)]
376mod tests;
377
378#[cfg(test)]
379#[path = "layer/release_contract_tests.rs"]
380mod release_contract_tests;
381
382#[cfg(test)]
383#[path = "layer/rows_from_tests.rs"]
384mod rows_from_tests;