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;