metrale_model_arch/
attn_v41.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! 2026-09-25: DeepSeek-V4.1 Flash attention on the GPU: one forward for every layer.
5//!
6//! A layer's `LayerRole::ratio` selects its kind. Ratio 0 attends over a
7//! sliding window of its own fp8-rounded kv rows. Ratio > 0 attends over that
8//! window plus compressed positions chosen by an indexer from the latent cache
9//! the kv-source layers (`is_kv_source`) publish in `SharedV41`. The index
10//! selection is computed by the index-source layers and reused by the ratio > 0
11//! layers after them; the candidate-source layer also publishes a block-level
12//! candidate mask that later index sources apply.
13//!
14//! This is the device form of `deepseek_v41_ref::compress::attention_any`, and
15//! `attn_v41_tests.rs` compares it with the reference layer by layer:
16//! * projections on `dense_gemm_bf16` / `dense_gemv_bf16` (bf16 weights) or the
17//!   `Q2_K` K-quant kernels;
18//! * RMSNorm, RoPE, the fp8/fp4 quantisers, the compressor pooling, the
19//!   indexer scores, sparse attention with the sink and the `wo_a` column
20//!   slices on `kernels/gb10/deepseek-v4-flash/nvfp4/attn_v41.cu`;
21//! * the candidate-block and index top-k selections on the CPU through the
22//!   reference's `torch_cpu_topk_set`, which reproduces torch's CPU tie order
23//!   among equal scores; the scores come back in one stream-ordered download.
24//!
25//! Layer state (window ring, compressor partial group, the latent and
26//! index-key caches) lives on the device; the shared index selection and
27//! candidate mask in `SharedV41` are host vectors.
28//!
29//! Owner: model-arch, DeepSeek-V4.1.
30//! Invariants:
31//! - A ratio > 0 layer fails (does not attend) when no kv source has published
32//!   a latent cache (`forward`, `decode_prep_role`).
33//! - `forward` refuses a selection wider than `window + index_topk` or 2048,
34//!   the `sparse_attn` score buffer.
35
36use anyhow::Result;
37use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
38
39/// 2026-09-25: An attention projection on the device: bf16, or raw `Q2_K`
40/// blocks. `Q3_K` is refused by `AttnV41::gemm`.
41pub use metrale_model_layers::layers::ops::ResidentMat as AttnMat;
42
43const MODULE: &str = "attn_v41";
44const GEMM_MODULE: &str = "gemm";
45/// 2026-09-25: Quantiser blocks per launch block: one warp per block, 8 warps
46/// in the 256-thread launch block (`attn_v41_act_quant_fp8`, `attn_v41_fp4_quant`).
47const QUANT_BLOCKS_PER_LAUNCH: u32 = 8;
48
49/// 2026-09-25: Model-wide attention geometry, built from `ModelConfig` and the
50/// `METRALE_DS41_*` limits by the DeepSeek-V4.1 weight loader.
51#[derive(Clone, Debug)]
52pub struct AttnV41Cfg {
53    pub dim: usize,
54    pub n_heads: usize,
55    pub head_dim: usize,
56    pub rope_dim: usize,
57    pub q_rank: usize,
58    pub o_rank: usize,
59    pub groups: usize,
60    pub window: usize,
61    pub eps: f32,
62    pub index_heads: usize,
63    pub index_hd: usize,
64    pub index_topk: usize,
65    pub cand_topk_blocks: usize,
66    pub cand_block: usize,
67    pub max_seq: usize,
68    pub max_tokens: usize,
69    pub rope_theta: f32,
70    pub compress_rope_theta: f32,
71    pub rope_factor: f32,
72    pub orig_seq: usize,
73    pub beta_fast: f32,
74    pub beta_slow: f32,
75}
76
77impl AttnV41Cfg {
78    pub fn gw(&self) -> usize {
79        self.n_heads * self.head_dim / self.groups
80    }
81}
82
83/// 2026-09-25: A layer's compress ratio and which shared slots it writes or reads.
84#[derive(Clone, Copy, Debug, Default)]
85pub struct LayerRole {
86    pub ratio: usize,
87    pub is_kv_source: bool,
88    pub is_index_source: bool,
89    pub is_candidate_source: bool,
90    pub uses_candidates: bool,
91}
92
93/// 2026-09-25: Compressor weights (kv sources only). At ratio > 1 `kv` and
94/// `gate` are f32 `[hd, dim]`; at ratio 1 `kv` is bf16 and `gate` is `None`.
95/// `norm` is f32 `[hd]`.
96pub struct CompressorWeightsGpu {
97    pub kv: DevicePtr,
98    pub gate: Option<DevicePtr>,
99    pub norm: DevicePtr,
100}
101
102/// 2026-09-25: Indexer weights (index sources only): bf16 `wq_b`
103/// `[index_heads * index_hd, q_rank]` and `weights_proj` `[index_heads, dim]`;
104/// on a layer that is also a kv source, bf16 `wk` `[index_hd, hd]` and f32
105/// `k_norm` `[index_hd]`.
106pub struct IndexerWeightsGpu {
107    pub wq_b: DevicePtr,
108    pub weights_proj: DevicePtr,
109    pub wk: Option<DevicePtr>,
110    pub k_norm: Option<DevicePtr>,
111}
112
113/// 2026-09-25: One layer's resident attention weights. Projections are
114/// `[out, in]`: `wo_a` is `[groups * o_rank, gw]`, `wo_b` `[dim, groups *
115/// o_rank]`. `sink` (`[n_heads]`), `q_norm` (`[q_rank]`) and `kv_norm`
116/// (`[hd]`) are f32.
117pub struct AttnV41LayerWeights {
118    pub role: LayerRole,
119    pub sink: DevicePtr,
120    pub wq_a: AttnMat,
121    pub q_norm: DevicePtr,
122    pub wq_b: AttnMat,
123    pub wkv: AttnMat,
124    pub kv_norm: DevicePtr,
125    pub wo_a: AttnMat,
126    pub wo_b: AttnMat,
127    pub comp: Option<CompressorWeightsGpu>,
128    pub idx: Option<IndexerWeightsGpu>,
129}
130
131/// 2026-09-25: One sequence's attention state for one layer, across prefill
132/// and decode: the bf16 window ring `[window, hd]`; on kv sources also the
133/// compressor's partial group (f32 kv and score, `[ratio, hd]` each, used at
134/// ratio > 1), the bf16 latent cache `[max_seq / ratio, hd]` and the bf16
135/// index-key cache `[max_seq / ratio, index_hd]`.
136pub struct AttnV41LayerState {
137    window: DevicePtr,
138    comp_state: Option<(DevicePtr, DevicePtr)>,
139    compress_kv: Option<DevicePtr>,
140    index_k: Option<DevicePtr>,
141}
142
143impl AttnV41LayerState {
144    pub fn new(gpu: &dyn GpuBackend, c: &AttnV41Cfg, role: LayerRole) -> Result<Self> {
145        let window = gpu.alloc(c.window * c.head_dim * 2)?;
146        gpu.memset(window, 0, c.window * c.head_dim * 2)?;
147        let (comp_state, compress_kv, index_k) = if role.is_kv_source {
148            let ratio = role.ratio.max(1);
149            let groups = c.max_seq / ratio;
150            let a = gpu.alloc(ratio * c.head_dim * 4)?;
151            let b = gpu.alloc(ratio * c.head_dim * 4)?;
152            gpu.memset(a, 0, ratio * c.head_dim * 4)?;
153            // 2026-09-25: The score state starts at -inf, as
154            // `CompressorState::new` in the reference.
155            let neg: Vec<u8> =
156                std::iter::repeat_n(f32::NEG_INFINITY.to_le_bytes(), ratio * c.head_dim)
157                    .flatten()
158                    .collect();
159            gpu.copy_h2d(&neg, b)?;
160            let ckv = gpu.alloc(groups.max(1) * c.head_dim * 2)?;
161            gpu.memset(ckv, 0, groups.max(1) * c.head_dim * 2)?;
162            let ik = gpu.alloc(groups.max(1) * c.index_hd * 2)?;
163            gpu.memset(ik, 0, groups.max(1) * c.index_hd * 2)?;
164            (Some((a, b)), Some(ckv), Some(ik))
165        } else {
166            (None, None, None)
167        };
168        Ok(AttnV41LayerState {
169            window,
170            comp_state,
171            compress_kv,
172            index_k,
173        })
174    }
175
176    /// 2026-09-25: The window ring, bf16 `[window, hd]`: a pointer the captured
177    /// decode steps bake.
178    pub fn window(&self) -> DevicePtr {
179        self.window
180    }
181
182    /// 2026-09-25: Free this sequence's device buffers (the window ring and,
183    /// on kv sources, the two compressor-state buffers and both caches) and
184    /// clear the pointers. A second call frees nothing: the window is NULL
185    /// (`GpuBackend::free` ignores NULL) and the options are `None`.
186    pub fn free(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
187        let window = std::mem::replace(&mut self.window, DevicePtr::NULL);
188        gpu.free(window)?;
189        if let Some((a, b)) = self.comp_state.take() {
190            gpu.free(a)?;
191            gpu.free(b)?;
192        }
193        if let Some(p) = self.compress_kv.take() {
194            gpu.free(p)?;
195        }
196        if let Some(p) = self.index_k.take() {
197            gpu.free(p)?;
198        }
199        Ok(())
200    }
201}
202
203/// 2026-09-25: What source layers publish for the layers after them: the
204/// latest kv source's latent cache (`compress_kv`) and index-key cache
205/// (`index_k`); the latest index source's selection `topk_idxs`
206/// (`[queries][topk]`, offset past the window rows, -1 = absent); the
207/// candidate source's mask `candidates` (`[queries][cand_width]`).
208/// `compress_len` is the largest group count written so far and only grows.
209#[derive(Default)]
210pub struct SharedV41 {
211    pub compress_kv: Option<DevicePtr>,
212    pub compress_len: usize,
213    pub index_k: Option<DevicePtr>,
214    pub topk_idxs: Vec<i32>,
215    pub topk: usize,
216    pub candidates: Vec<bool>,
217    pub cand_width: usize,
218}
219
220/// 2026-09-25: The intermediates of one layer's forward. `q` and `o` are bf16
221/// `[tokens, n_heads, hd]` (`q` after RoPE, `o` before the inverse rotation);
222/// `rows_a` are the window rows (the chunk's own rows at `start_pos == 0`,
223/// the ring otherwise); `rows_b` the kv source's latent cache, of which
224/// `rows_b_len` rows are attended; `idx` is `[tokens][topk]`, window slots
225/// then compressed positions offset by `rows_a_len`; `out` is bf16 `[tokens, dim]`.
226pub struct AttnV41Run {
227    pub q: DevicePtr,
228    pub rows_a: DevicePtr,
229    pub rows_a_len: usize,
230    pub rows_b: Option<DevicePtr>,
231    pub rows_b_len: usize,
232    pub idx: Vec<i32>,
233    pub topk: usize,
234    pub o: DevicePtr,
235    pub out: DevicePtr,
236}
237
238struct Kernels {
239    gemm: KernelHandle,
240    // 2026-09-25: `gemv` serves bf16 projections at m = 1. For `AttnMat::Q2K`:
241    // q8_1 row quantisation + MMVQ at m <= 8, D2S6 tile quantisation + MMQ
242    // above; `mmvq_q2k_groups_w` does every `wo_a` output group in one launch
243    // and `mmvq_q2k_pair_w` does `wq_a` and `wkv` in one launch (both m <= 8).
244    gemv: KernelHandle,
245    q8_rows: KernelHandle,
246    mmvq_q2k_w: KernelHandle,
247    mmvq_q2k_groups_w: KernelHandle,
248    mmvq_q2k_pair_w: KernelHandle,
249    quant_d2s6: KernelHandle,
250    mmq_q2k_nc: KernelHandle,
251    mmq_q2k_wc: KernelHandle,
252    rmsnorm_bf16: KernelHandle,
253    rmsnorm_f32: KernelHandle,
254    rope: KernelHandle,
255    act_quant: KernelHandle,
256    fp4_quant: KernelHandle,
257    gemm_f32: KernelHandle,
258    // 2026-09-25: The compressor's f32 product at m = 1 when `dim % 8 == 0`;
259    // `gemm_f32` otherwise.
260    gemv_f32_staged: KernelHandle,
261    pool: KernelHandle,
262    index_score: KernelHandle,
263    sparse_attn: KernelHandle,
264    slice_cols: KernelHandle,
265    scatter_cols: KernelHandle,
266    scale_bf16: KernelHandle,
267    // 2026-09-25: The captured decode step's window ring write; the slot is
268    // `pos[0] % window`, read on the device.
269    ring_put: KernelHandle,
270}
271
272/// 2026-09-25: The attention runtime: kernels, RoPE tables, and workspaces for
273/// up to `max_tokens` positions per call.
274pub struct AttnV41 {
275    pub cfg: AttnV41Cfg,
276    k: Kernels,
277    fc_plain: DevicePtr,
278    fc_yarn: DevicePtr,
279    // 2026-09-25: q8_1 activations for the Q2_K projections, sized for the
280    // larger of the m <= 8 row layout and the MMQ tile layout.
281    a_q8: DevicePtr,
282    qr_raw: DevicePtr,
283    qr: DevicePtr,
284    q: DevicePtr,
285    kv_raw: DevicePtr,
286    kv: DevicePtr,
287    o: DevicePtr,
288    o_rot: DevicePtr,
289    og: DevicePtr,
290    slice_in: DevicePtr,
291    slice_out: DevicePtr,
292    out: DevicePtr,
293    pos: DevicePtr,
294    head_pos: DevicePtr,
295    idx_pos: DevicePtr,
296    grp_pos: DevicePtr,
297    idx_dev: DevicePtr,
298    // 2026-09-25: The captured step's selection for ratio-0 layers (window
299    // only); `idx_dev` holds the ratio > 0 selection, so one multi-layer
300    // capture can read both classes.
301    idx_dev_win: DevicePtr,
302    ckv: DevicePtr,
303    cscore: DevicePtr,
304    pooled: DevicePtr,
305    latent_raw: DevicePtr,
306    latent: DevicePtr,
307    ik_raw: DevicePtr,
308    ik: DevicePtr,
309    iq: DevicePtr,
310    iw_raw: DevicePtr,
311    iw: DevicePtr,
312    score: DevicePtr,
313    // 2026-09-25: What `decode_prep_role` last uploaded into `pos` /
314    // `head_pos`, `idx_dev` and `idx_dev_win`, so a step re-uploads only what
315    // changed. `None` = unknown, upload.
316    decode_pos: Option<usize>,
317    decode_idx: Option<Vec<i32>>,
318    decode_idx_win: Option<Vec<i32>>,
319}
320
321fn upload_f32(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
322    let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
323    let p = gpu.alloc(bytes.len().max(4))?;
324    gpu.copy_h2d(&bytes, p)?;
325    Ok(p)
326}
327
328fn upload_i32(gpu: &dyn GpuBackend, dst: DevicePtr, v: &[i32]) -> Result<()> {
329    let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
330    gpu.copy_h2d(&bytes, dst)
331}
332
333/// 2026-09-25: Enqueue an upload of `v` on `stream` (`GpuBackend::copy_h2d_async`,
334/// so `v` may be dropped on return), for the captured decode step's per-token inputs.
335fn upload_i32_async(gpu: &dyn GpuBackend, dst: DevicePtr, v: &[i32], stream: u64) -> Result<()> {
336    let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
337    gpu.copy_h2d_async(&bytes, dst, stream)
338}
339
340fn at(p: DevicePtr, byte_off: usize) -> DevicePtr {
341    DevicePtr(p.0 + byte_off as u64)
342}
343
344mod decode;
345mod forward;
346mod init;
347mod primitives;
348mod sources;
349
350#[cfg(test)]
351#[path = "attn_v41_tests.rs"]
352mod tests;