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;