metrale_model_layers/layer/transformer_layer.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The `TransformerLayer` trait, the per-layer hooks the model drives (grouped
4//! into supertraits in the child modules), and the layout constants of the batched-verify
5//! WY pointer tables.
6//!
7//! Owner: model-layers.
8//! Invariants: none beyond the types.
9
10use anyhow::Result;
11use metrale_cache::kv_cache::PagedKvCache;
12use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
13
14use super::{ForwardContext, GdnPrefillBuffers, LayerState};
15
16mod aux_state;
17mod capabilities;
18mod default_loops;
19mod graph_hooks;
20mod split_prefill;
21mod weight_setup;
22mod write_on_accept;
23
24pub use aux_state::LayerAuxState;
25pub use capabilities::LayerCapabilities;
26pub use graph_hooks::LayerGraphHooks;
27pub use split_prefill::LayerSplitPrefill;
28pub use weight_setup::LayerWeightSetup;
29pub use write_on_accept::{GdnCarryBinding, LayerWriteOnAccept};
30
31/// 2026-09-25: Batched-verify WY pointer-table layout. `TransformerModel::upload_verify_wy_tables`
32/// (model-engine `verify_e2.rs`) writes it; the qwen3_ssm `GdnStates::Multi` arm and the
33/// write-on-accept fold read it.
34///
35/// Per GDN layer, `VERIFY_WY_TABLES_PER_LAYER` tables of `VERIFY_WY_TABLE_SEQS` u64
36/// entries lie back to back: table 0 holds each sequence's `h_state`, table `t + 1` its
37/// intermediate `t`. A k-row verify writes at most the first k tables and leaves every
38/// other entry zero. The strides do not depend on k.
39///
40/// This constant is the most sequences one batched verify holds: it sizes the tables and
41/// the verify hidden and catch-up stashes (`impl_a1/spec_buffers.rs`), the GDN carry and
42/// write-on-accept tables, and `can_batch_verify_dispatch` (`verify_e.rs`) admits at most
43/// this many. 2026-09-29: 128, the serve's widest batch (`DECODE_META_MAX_ROWS`), so one
44/// verify forward covers every speculating sequence: a verify split in chunks reads the MoE
45/// expert weights once per chunk. The tables cost 3,840 B per sequence; the SSM verify pools
46/// are sized by the dispatch cap, not by this (`ssm_reserve::mtp_state_slots`).
47pub const VERIFY_WY_TABLE_SEQS: usize = 128;
48/// 2026-09-25: Catch-up stash rows per sequence (`ModelLevers::mtp_kv_exact`). The batched
49/// verify step stashes at most this many accepted drafts per sequence
50/// (`verify_k4_batch_step.rs`).
51pub const MTP_CATCHUP_MAX: usize = 4;
52/// 2026-09-25: Tables per GDN layer: `h_state` plus up to 15 intermediates.
53/// `upload_verify_wy_tables` stages a verify only when k is in
54/// `2..=VERIFY_WY_TABLES_PER_LAYER`, and returns NULL otherwise.
55pub const VERIFY_WY_TABLES_PER_LAYER: usize = 16;
56/// 2026-09-25: Bytes between consecutive tables within a layer slice.
57pub const VERIFY_WY_TABLE_STRIDE_BYTES: usize = VERIFY_WY_TABLE_SEQS * 8;
58/// 2026-09-25: Bytes between consecutive GDN layers' table slices.
59pub const VERIFY_WY_LAYER_STRIDE_BYTES: usize =
60 VERIFY_WY_TABLES_PER_LAYER * VERIFY_WY_TABLE_STRIDE_BYTES;
61
62pub trait TransformerLayer:
63 Send
64 + Sync
65 + LayerCapabilities
66 + LayerWeightSetup
67 + LayerWriteOnAccept
68 + LayerGraphHooks
69 + LayerAuxState
70 + LayerSplitPrefill
71 + crate::circuit_exec::CircuitBindings
72{
73 /// 2026-09-25: Decode one token through this layer, updating `hidden` in place.
74 ///
75 /// * `hidden` - `[1, hidden_size]` BF16, read and written
76 /// * `residual` - `[1, hidden_size]` BF16 residual stream
77 /// * `state` - this layer's state for the sequence
78 /// * `seq_len` - the token's position
79 fn decode(
80 &self,
81 hidden: DevicePtr,
82 residual: DevicePtr,
83 state: &mut dyn LayerState,
84 kv_cache: &mut PagedKvCache,
85 seq_len: usize,
86 block_table: &mut Vec<u32>,
87 // 2026-09-25: `--high-speed-swap` disk block ids, one per logical block the sequence
88 // has had; `block_table` holds the newest of those blocks. Empty without the feature.
89 disk_block_ids: &mut Vec<u32>,
90 // 2026-09-25: `--high-speed-swap`: how many `disk_block_ids` entries each attention
91 // layer has offloaded, indexed by the layer's `attn_layer_idx`.
92 disk_last_offloaded_per_layer: &mut Vec<u32>,
93 ctx: &ForwardContext,
94 stream: u64,
95 ) -> Result<()>;
96
97 /// 2026-09-25: Prefill `num_tokens` tokens starting at position `seq_len_start`.
98 /// `hidden` and `residual` are `[num_tokens, hidden_size]` BF16. `kv_write_start` is
99 /// how many of these tokens already have their KV in the cache; the Qwen3 attention
100 /// cache-skip prefill writes KV only for the rest (`prefill/cache_skip.rs`). Default:
101 /// `decode` once per token (`default_loops::prefill_default`), which ignores
102 /// `kv_write_start`.
103 fn prefill(
104 &self,
105 hidden: DevicePtr,
106 residual: DevicePtr,
107 num_tokens: usize,
108 state: &mut dyn LayerState,
109 kv_cache: &mut PagedKvCache,
110 seq_len_start: usize,
111 block_table: &mut Vec<u32>,
112 disk_block_ids: &mut Vec<u32>,
113 disk_last_offloaded_per_layer: &mut Vec<u32>,
114 _kv_write_start: usize,
115 ctx: &ForwardContext,
116 stream: u64,
117 ) -> Result<()> {
118 default_loops::prefill_default(
119 self,
120 hidden,
121 residual,
122 num_tokens,
123 state,
124 kv_cache,
125 seq_len_start,
126 block_table,
127 disk_block_ids,
128 disk_last_offloaded_per_layer,
129 ctx,
130 stream,
131 )
132 }
133
134 /// 2026-09-25: Split SSM prefill, phase 1: norm, projections, gates, conv1d and L2
135 /// norm for `num_tokens` tokens, then the GDN inputs (packed QKV, gate/beta, Z) are
136 /// copied into `gdn_bufs` at `token_offset`. The recurrence runs later, in
137 /// `prefill_gdn_full`. Default: the whole `prefill`.
138 #[allow(clippy::too_many_arguments)]
139 fn prefill_phase1(
140 &self,
141 hidden: DevicePtr,
142 residual: DevicePtr,
143 num_tokens: usize,
144 state: &mut dyn LayerState,
145 kv_cache: &mut PagedKvCache,
146 seq_len_start: usize,
147 block_table: &mut Vec<u32>,
148 disk_block_ids: &mut Vec<u32>,
149 disk_last_offloaded_per_layer: &mut Vec<u32>,
150 kv_write_start: usize,
151 gdn_bufs: &GdnPrefillBuffers,
152 token_offset: usize,
153 ctx: &ForwardContext,
154 stream: u64,
155 ) -> Result<()> {
156 let _ = (gdn_bufs, token_offset);
157 self.prefill(
158 hidden,
159 residual,
160 num_tokens,
161 state,
162 kv_cache,
163 seq_len_start,
164 block_table,
165 disk_block_ids,
166 disk_last_offloaded_per_layer,
167 kv_write_start,
168 ctx,
169 stream,
170 )
171 }
172
173 /// 2026-09-25: Decode `num_tokens` consecutive tokens of one sequence, starting at
174 /// position `seq_len`. `hidden` and `residual` are `[num_tokens, hidden_size]` BF16.
175 /// Default: `decode` once per token (`default_loops::decode_batched_default`).
176 #[allow(clippy::too_many_arguments)]
177 fn decode_batched(
178 &self,
179 hidden: DevicePtr,
180 residual: DevicePtr,
181 num_tokens: usize,
182 state: &mut dyn LayerState,
183 kv_cache: &mut PagedKvCache,
184 seq_len: usize,
185 block_table: &mut Vec<u32>,
186 disk_block_ids: &mut Vec<u32>,
187 disk_last_offloaded_per_layer: &mut Vec<u32>,
188 ctx: &ForwardContext,
189 stream: u64,
190 ) -> Result<()> {
191 default_loops::decode_batched_default(
192 self,
193 hidden,
194 residual,
195 num_tokens,
196 state,
197 kv_cache,
198 seq_len,
199 block_table,
200 disk_block_ids,
201 disk_last_offloaded_per_layer,
202 ctx,
203 stream,
204 )
205 }
206
207 /// 2026-09-25: Decode one token for each of `num_seqs` rows, each with its own entry in
208 /// `seq_lens` and `block_tables`: one row per sequence in a batched decode, one per
209 /// verify row in a batched verify (`verify_e.rs`). `hidden` and `residual` are
210 /// `[num_seqs, hidden_size]` BF16. Default: `decode` once per row
211 /// (`default_loops::decode_multi_seq_default`).
212 #[allow(clippy::too_many_arguments)]
213 fn decode_multi_seq<'a, 'b: 'a>(
214 &self,
215 hidden: DevicePtr,
216 residual: DevicePtr,
217 num_seqs: usize,
218 states: &'a mut [&'b mut (dyn LayerState + 'static)],
219 kv_cache: &mut PagedKvCache,
220 seq_lens: &[usize],
221 block_tables: &[Vec<u32>],
222 ctx: &ForwardContext,
223 stream: u64,
224 ) -> Result<()> {
225 default_loops::decode_multi_seq_default(
226 self,
227 hidden,
228 residual,
229 num_seqs,
230 states,
231 kv_cache,
232 seq_lens,
233 block_tables,
234 ctx,
235 stream,
236 )
237 }
238
239 /// 2026-09-25: Batched verify of `ks[i]` rows for each of `n_seqs` sequences in one
240 /// sweep, the rows sequence-major and contiguous in `hidden`/`residual`. The verify
241 /// (`verify_e.rs`) runs attention layers through `decode_multi_seq` and every other
242 /// layer through this. `wy_tables` is this layer's slice of the staged WY pointer
243 /// tables (layout at [`VERIFY_WY_TABLE_SEQS`]), or NULL when none were staged or the
244 /// layer is not linear attention. The default returns an error.
245 #[allow(clippy::too_many_arguments)]
246 fn decode_verify_multi<'a, 'b: 'a>(
247 &self,
248 _hidden: DevicePtr,
249 _residual: DevicePtr,
250 _n_seqs: usize,
251 _ks: &[usize],
252 _states: &'a mut [&'b mut (dyn LayerState + 'static)],
253 _kv_cache: &mut PagedKvCache,
254 _wy_tables: DevicePtr,
255 _ctx: &ForwardContext,
256 _stream: u64,
257 ) -> Result<()> {
258 anyhow::bail!("decode_verify_multi: unsupported for this layer type")
259 }
260
261 /// 2026-09-25: Allocate this layer's per-sequence state. Sequence setup (model-engine
262 /// `trait_impl/meta.rs`) calls it for every layer except a linear-attention layer that
263 /// uses the SSM pool, which gets pool addresses instead.
264 fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>>;
265
266 /// 2026-09-25: Free the device memory this layer allocated for one sequence, in
267 /// `alloc_state` or attached to the state later. `LayerState` holds bare `DevicePtr`s,
268 /// so dropping it frees only the host struct.
269 ///
270 /// The teardown chokepoint `free_sequence_dispatch` calls it for every layer, logs an
271 /// error and continues. An implementation must be idempotent, null what it frees,
272 /// never free pool addresses, and decide by the state's type rather than rely on a
273 /// filter at the call site. The default does nothing, which suits state that owns no
274 /// device memory and pool-backed SSM state, released through the pool slot.
275 fn release_state(&self, _state: &mut dyn LayerState, _gpu: &dyn GpuBackend) -> Result<()> {
276 Ok(())
277 }
278}
279
280#[cfg(test)]
281mod verify_wy_table_tests {
282 use super::{
283 VERIFY_WY_LAYER_STRIDE_BYTES, VERIFY_WY_TABLE_SEQS, VERIFY_WY_TABLE_STRIDE_BYTES,
284 VERIFY_WY_TABLES_PER_LAYER,
285 };
286
287 /// 2026-09-25: `upload_verify_wy_tables` stages only k in
288 /// `2..=VERIFY_WY_TABLES_PER_LAYER`; this pins k = 16 as admitted.
289 #[test]
290 fn tables_per_layer_fund_the_widest_verify_arm() {
291 assert!(
292 (2..=VERIFY_WY_TABLES_PER_LAYER).contains(&16usize),
293 "VERIFY_WY_TABLES_PER_LAYER ({VERIFY_WY_TABLES_PER_LAYER}) must admit k=16"
294 );
295 }
296
297 /// 2026-09-25: The table-form wyN launch (`trait_decode_batched_conv_gdn_multi.rs`) and
298 /// the write-on-accept fold pass `VERIFY_WY_TABLE_SEQS` as the entry count between
299 /// slabs, which reaches slab t only when slabs are back to back at that many 8-byte
300 /// entries.
301 #[test]
302 fn slab_stride_matches_the_kernel_contract() {
303 assert_eq!(VERIFY_WY_TABLE_STRIDE_BYTES, VERIFY_WY_TABLE_SEQS * 8);
304 assert_eq!(
305 VERIFY_WY_LAYER_STRIDE_BYTES,
306 VERIFY_WY_TABLES_PER_LAYER * VERIFY_WY_TABLE_STRIDE_BYTES
307 );
308 }
309
310 /// 2026-09-25: A batch of 16 sequences fits one slab.
311 #[test]
312 fn seqs_capacity_covers_c16() {
313 const { assert!(VERIFY_WY_TABLE_SEQS >= 16) };
314 }
315}