metrale_model_layers/layers/qwen3_attention/
trait_impl.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `TransformerLayer` for `Qwen3AttentionLayer`: forwards decode,
4//! prefill and multi-sequence decode to their inner implementations, carries the
5//! QSA indexer's per-sequence state, and runs the MoE transposes on both FFN slots.
6//!
7//! Owner: model-layers (qwen3 attention).
8//! Invariants:
9//! - `decode_graph_unsupported` and `has_aux_state` are true exactly when the
10//!   layer has a QSA indexer.
11//! - `release_state` leaves `AttnLayerState::qsa` empty on every return, `Err`
12//!   included: the carry is taken before the fallible free.
13
14use anyhow::Result;
15use metrale_cache::kv_cache::PagedKvCache;
16use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
17
18use super::Qwen3AttentionLayer;
19use crate::layer::{
20    BatchedAttnMetadata, EmptyLayerState, ForwardContext, LayerState, TransformerLayer,
21};
22use crate::layer::{
23    LayerAuxState, LayerCapabilities, LayerGraphHooks, LayerSplitPrefill, LayerWeightSetup,
24    LayerWriteOnAccept,
25};
26use crate::layers::FfnComponent;
27
28mod decode_inner;
29pub(in crate::layers::qwen3_attention) mod multi_seq;
30mod prefill_inner;
31
32/// 2026-09-25: Debug: synchronise `stream`, read a BF16 tensor back and log its
33/// L2 norm, max |x| and first four values. A failed copy logs nothing.
34pub(super) fn diag_norm(
35    gpu: &dyn GpuBackend,
36    ptr: DevicePtr,
37    n_elements: usize,
38    stream: u64,
39    label: &str,
40) {
41    let _ = gpu.synchronize(stream);
42    let mut buf = vec![0u16; n_elements];
43    // 2026-09-25: SAFETY: `buf` is `vec![0u16; n_elements]` on the line above, so
44    // `buf.len() == n_elements` and `n_elements * 2 == buf.len() *
45    // size_of::<u16>()` — the span is exactly the Vec's buffer, all of it
46    // zero-initialised. `bytes` is the sole reference derived from `buf` while it
47    // is live: it is dead after the `copy_d2h` below, before `buf.iter()` runs.
48    let bytes =
49        unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, n_elements * 2) };
50    if gpu.copy_d2h(ptr, bytes).is_err() {
51        return;
52    }
53    let vals: Vec<f32> = buf
54        .iter()
55        .map(|&b| f32::from_bits((b as u32) << 16))
56        .collect();
57    let norm: f32 = vals.iter().map(|v| v * v).sum::<f32>().sqrt();
58    let max_abs: f32 = vals.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
59    let f4 = if vals.len() >= 4 {
60        format!(
61            "[{:.4},{:.4},{:.4},{:.4}]",
62            vals[0], vals[1], vals[2], vals[3]
63        )
64    } else {
65        format!("{:?}", &vals[..vals.len().min(4)])
66    };
67    tracing::info!("DIAG {label}: norm={norm:.4} max={max_abs:.4} first4={f4} n={n_elements}");
68}
69
70/// 2026-09-25: Debug: the FP32 twin of `diag_norm`. Its callers are the
71/// DeepSeek-V4 prefill and multi-sequence decode diagnostics, which log the
72/// hyper-connection `post` and `comb` tensors.
73pub fn diag_norm_f32(
74    gpu: &dyn GpuBackend,
75    ptr: DevicePtr,
76    n_elements: usize,
77    stream: u64,
78    label: &str,
79) {
80    let _ = gpu.synchronize(stream);
81    let mut buf = vec![0f32; n_elements];
82    // 2026-09-25: SAFETY: `buf` is `vec![0f32; n_elements]` on the line above, so
83    // `buf.len() == n_elements` and `n_elements * 4 == buf.len() *
84    // size_of::<f32>()` — the span is exactly the Vec's buffer, all of it
85    // zero-initialised. `bytes` is the sole reference derived from `buf` while it
86    // is live: it is dead after the `copy_d2h` below, before `buf.iter()` runs.
87    let bytes =
88        unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, n_elements * 4) };
89    if gpu.copy_d2h(ptr, bytes).is_err() {
90        return;
91    }
92    let norm: f32 = buf.iter().map(|v| v * v).sum::<f32>().sqrt();
93    let max_abs: f32 = buf.iter().map(|v| v.abs()).fold(0.0f32, f32::max);
94    let f4 = if buf.len() >= 4 {
95        format!("[{:.4},{:.4},{:.4},{:.4}]", buf[0], buf[1], buf[2], buf[3])
96    } else {
97        format!("{:?}", &buf[..buf.len().min(4)])
98    };
99    tracing::info!(
100        "DIAG {label}: norm={norm:.4} max={max_abs:.4} first4={f4} n={n_elements} (FP32)"
101    );
102}
103
104impl TransformerLayer for Qwen3AttentionLayer {
105    fn decode(
106        &self,
107        hidden: DevicePtr,
108        residual: DevicePtr,
109        state: &mut dyn LayerState,
110        kv_cache: &mut PagedKvCache,
111        seq_len: usize,
112        block_table: &mut Vec<u32>,
113        disk_block_ids: &mut Vec<u32>,
114        disk_last_offloaded_per_layer: &mut Vec<u32>,
115        ctx: &ForwardContext,
116        stream: u64,
117    ) -> Result<()> {
118        self.decode_inner(
119            hidden,
120            residual,
121            state,
122            kv_cache,
123            seq_len,
124            block_table,
125            disk_block_ids,
126            disk_last_offloaded_per_layer,
127            ctx,
128            stream,
129        )
130    }
131
132    #[allow(clippy::too_many_arguments)]
133    fn prefill(
134        &self,
135        hidden: DevicePtr,
136        residual: DevicePtr,
137        num_tokens: usize,
138        state: &mut dyn LayerState,
139        kv_cache: &mut PagedKvCache,
140        seq_len_start: usize,
141        block_table: &mut Vec<u32>,
142        disk_block_ids: &mut Vec<u32>,
143        disk_last_offloaded_per_layer: &mut Vec<u32>,
144        kv_write_start: usize,
145        ctx: &ForwardContext,
146        stream: u64,
147    ) -> Result<()> {
148        self.prefill_inner(
149            hidden,
150            residual,
151            num_tokens,
152            state,
153            kv_cache,
154            seq_len_start,
155            block_table,
156            disk_block_ids,
157            disk_last_offloaded_per_layer,
158            kv_write_start,
159            None,
160            ctx,
161            stream,
162        )
163    }
164
165    #[allow(clippy::too_many_arguments)]
166    fn decode_multi_seq<'a, 'b: 'a>(
167        &self,
168        hidden: DevicePtr,
169        residual: DevicePtr,
170        num_seqs: usize,
171        states: &'a mut [&'b mut (dyn LayerState + 'static)],
172        kv_cache: &mut PagedKvCache,
173        seq_lens: &[usize],
174        block_tables: &[Vec<u32>],
175        ctx: &ForwardContext,
176        stream: u64,
177    ) -> Result<()> {
178        self.decode_multi_seq_inner(
179            hidden,
180            residual,
181            num_seqs,
182            states,
183            kv_cache,
184            seq_lens,
185            block_tables,
186            ctx,
187            stream,
188        )
189    }
190
191    fn alloc_state(&self, _gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>> {
192        Ok(Box::new(crate::layer::AttnLayerState::default()))
193    }
194
195    /// 2026-09-25: Free the QSA indexer carry this sequence attached on first use.
196    ///
197    /// `alloc_state` returns an empty `AttnLayerState`; the carry is created
198    /// later by `helpers::qsa_seq_state` (or by `restore_aux`). `take()` makes
199    /// this idempotent and leaves the state as `alloc_state` produced it.
200    ///
201    /// A layer with no QSA indexer never populates the field, so `take()`
202    /// yields `None` and this returns `Ok(())`.
203    fn release_state(&self, state: &mut dyn LayerState, gpu: &dyn GpuBackend) -> Result<()> {
204        let Some(attn) = state
205            .as_any_mut()
206            .downcast_mut::<crate::layer::AttnLayerState>()
207        else {
208            return Ok(());
209        };
210        let Some(mut st) = attn.qsa.take() else {
211            return Ok(());
212        };
213        let Some(qsa) = self.qsa.as_ref() else {
214            // 2026-09-25: A carry without an indexer cannot be sized or freed,
215            // so this is an error rather than a guess.
216            anyhow::bail!("release_state: QSA seq state present but layer has no QSA indexer");
217        };
218        qsa.release_seq_state(&mut st, gpu)
219    }
220}
221
222impl LayerCapabilities for Qwen3AttentionLayer {
223    fn uses_local_mla_prefill(&self) -> bool {
224        self.mla.is_some()
225    }
226
227    /// 2026-10-01: `prefill_inner` splits the attention step at a replay-tail capture
228    /// (`prefill_inner/replay_tail.rs`). The mHC body, MLA and the QSA indexer do not.
229    fn supports_replay_tail_split(&self) -> bool {
230        self.hc.is_none() && self.mla.is_none() && self.qsa.is_none()
231    }
232
233    fn fp8_calibration_frozen(&self) -> Option<bool> {
234        self.fp8_calibration
235            .as_ref()
236            .map(|cal| !cal.is_calibrating())
237    }
238
239    /// 2026-09-25: QSA selection copies the scores to the host and sorts them
240    /// there (`qsa_select.rs`), which a captured graph cannot contain. Inside the
241    /// inert bound the layer runs dense attention, so a graph captured then
242    /// would replay dense attention after selection starts.
243    fn decode_graph_unsupported(&self) -> bool {
244        self.qsa.is_some()
245    }
246}
247
248impl LayerWeightSetup for Qwen3AttentionLayer {
249    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
250        Some(self)
251    }
252
253    fn transpose_moe_for_prefill(
254        &mut self,
255        gpu: &dyn GpuBackend,
256        config: &metrale_config::ModelConfig,
257    ) -> Result<()> {
258        if let FfnComponent::Moe(moe) = &mut self.ffn {
259            moe.transpose_for_prefill(gpu, config)?;
260        }
261        if let Some(FfnComponent::Moe(moe)) = self.moe_ffn.as_mut() {
262            moe.transpose_for_prefill(gpu, config)?;
263        }
264        Ok(())
265    }
266
267    fn transpose_moe_gate_up_for_prefill(
268        &mut self,
269        gpu: &dyn GpuBackend,
270        config: &metrale_config::ModelConfig,
271    ) -> Result<()> {
272        if let FfnComponent::Moe(moe) = &mut self.ffn {
273            moe.transpose_gate_up_for_prefill(gpu, config)?;
274        }
275        if let Some(FfnComponent::Moe(moe)) = self.moe_ffn.as_mut() {
276            moe.transpose_gate_up_for_prefill(gpu, config)?;
277        }
278        Ok(())
279    }
280
281    fn set_moe_down_transpose_scratch(
282        &mut self,
283        scratch_packed: DevicePtr,
284        scratch_scale: DevicePtr,
285        packed_ptrs_t: DevicePtr,
286        scale_ptrs_t: DevicePtr,
287    ) {
288        if let FfnComponent::Moe(moe) = &mut self.ffn {
289            moe.set_down_transpose_scratch(
290                scratch_packed,
291                scratch_scale,
292                packed_ptrs_t,
293                scale_ptrs_t,
294            );
295        }
296        if let Some(FfnComponent::Moe(moe)) = self.moe_ffn.as_mut() {
297            moe.set_down_transpose_scratch(
298                scratch_packed,
299                scratch_scale,
300                packed_ptrs_t,
301                scale_ptrs_t,
302            );
303        }
304    }
305
306    fn transpose_moe_for_prefill_unified(
307        &mut self,
308        gpu: &dyn GpuBackend,
309        config: &metrale_config::ModelConfig,
310    ) -> Result<()> {
311        if let FfnComponent::Moe(moe) = &mut self.ffn {
312            moe.transpose_for_prefill_unified(gpu, config)?;
313        }
314        if let Some(FfnComponent::Moe(moe)) = self.moe_ffn.as_mut() {
315            moe.transpose_for_prefill_unified(gpu, config)?;
316        }
317        Ok(())
318    }
319
320    fn transpose_moe_for_prefill_hybrid(
321        &mut self,
322        gpu: &dyn GpuBackend,
323        config: &metrale_config::ModelConfig,
324    ) -> Result<()> {
325        if let FfnComponent::Moe(moe) = &mut self.ffn {
326            moe.transpose_for_prefill_hybrid(gpu, config)?;
327        }
328        if let Some(FfnComponent::Moe(moe)) = self.moe_ffn.as_mut() {
329            moe.transpose_for_prefill_hybrid(gpu, config)?;
330        }
331        Ok(())
332    }
333}
334
335impl LayerWriteOnAccept for Qwen3AttentionLayer {}
336impl LayerGraphHooks for Qwen3AttentionLayer {}
337
338impl LayerAuxState for Qwen3AttentionLayer {
339    fn has_aux_state(&self) -> bool {
340        self.qsa.is_some()
341    }
342
343    fn snapshot_aux(
344        &self,
345        state: &dyn LayerState,
346        gpu: &dyn GpuBackend,
347        stream: u64,
348    ) -> Result<Option<Vec<u8>>> {
349        let Some(qsa) = self.qsa.as_ref() else {
350            return Ok(None);
351        };
352        let attn = state
353            .as_any()
354            .downcast_ref::<crate::layer::AttnLayerState>()
355            .ok_or_else(|| anyhow::anyhow!("QSA host layer state is not AttnLayerState"))?;
356        match attn.qsa.as_ref() {
357            Some(st) => Ok(Some(qsa.snapshot_aux(st, gpu, stream)?)),
358            // 2026-09-25: The carry is created on the sequence's first QSA use
359            // (`helpers::qsa_seq_state`); without it there is nothing to carry.
360            None => Ok(None),
361        }
362    }
363
364    fn restore_aux(
365        &self,
366        state: &mut dyn LayerState,
367        blob: &[u8],
368        gpu: &dyn GpuBackend,
369        stream: u64,
370    ) -> Result<()> {
371        let qsa = self
372            .qsa
373            .as_ref()
374            .ok_or_else(|| anyhow::anyhow!("restore_aux: no QSA on this layer"))?;
375        let attn = state
376            .as_any_mut()
377            .downcast_mut::<crate::layer::AttnLayerState>()
378            .ok_or_else(|| anyhow::anyhow!("QSA host layer state is not AttnLayerState"))?;
379        if attn.qsa.is_none() {
380            attn.qsa = Some(qsa.new_seq_state(gpu)?);
381        }
382        qsa.restore_aux(attn.qsa.as_mut().expect("just created"), blob, gpu, stream)
383    }
384}
385
386impl LayerSplitPrefill for Qwen3AttentionLayer {
387    /// 2026-09-25: Batched attention prefill over stacked streams: `prefill_inner`
388    /// with `batched_meta = Some`, an empty layer state and empty per-stream
389    /// block and disk lists. The streams' block tables travel in
390    /// `BatchedAttnMetadata::block_table_ptrs`. The caller is the model's
391    /// `prefill_attn_batched_layer`.
392    fn prefill_inner_batched_q12(
393        &self,
394        hidden_stacked: DevicePtr,
395        residual_stacked: DevicePtr,
396        num_tokens: usize,
397        kv_cache: &mut PagedKvCache,
398        seq_len_start: usize,
399        batched_meta: &BatchedAttnMetadata,
400        ctx: &ForwardContext,
401        stream: u64,
402    ) -> Result<()> {
403        let mut empty_state = EmptyLayerState;
404        let mut empty_block_table: Vec<u32> = Vec::new();
405        let mut empty_disk_block_ids: Vec<u32> = Vec::new();
406        let mut empty_disk_last: Vec<u32> = Vec::new();
407        self.prefill_inner(
408            hidden_stacked,
409            residual_stacked,
410            num_tokens,
411            &mut empty_state,
412            kv_cache,
413            seq_len_start,
414            &mut empty_block_table,
415            &mut empty_disk_block_ids,
416            &mut empty_disk_last,
417            0,
418            Some(batched_meta),
419            ctx,
420            stream,
421        )
422    }
423}