spark_model/layers/attn_v41.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! DeepSeek-V4.1 Flash attention on the GPU: one forward for every layer.
5//!
6//! V4.1 has three kinds of attention layer, told apart by `compress_ratio`:
7//! ratio 0 attends over a sliding window of its own fp8 latent rows; ratio 1
8//! and ratio 2 attend over that window PLUS a set of compressed positions chosen
9//! by an indexer from a latent cache that only the four `kv_source_layer_ids`
10//! produce (the `SharedAttentionRuntime`: four layers write, forty read). The
11//! index selection itself is computed by eight `index_source_layer_ids` and
12//! reused by the layers between them; layer `candidate_source_layer` also
13//! publishes a block-level candidate mask the later index sources respect.
14//!
15//! This module is the production form of `deepseek_v41_ref::compress::attention_any`,
16//! stage for stage, and is held to it layer by layer in `attn_v41_tests.rs`:
17//! * GEMMs on `common/dense_gemm_bf16` (f32 accumulate, bf16 out = `linear_bf16`);
18//! * RMSNorm, RoPE, the fp8/fp4 quantisers, the compressor pooling, the
19//! indexer scores, sparse attention with the sink and the grouped output
20//! projection on `kernels/gb10/deepseek-v4-flash/nvfp4/attn_v41.cu`;
21//! * the two top-k selections (candidate blocks, index positions) on the CPU
22//! through the reference's `torch_cpu_topk_set`, because the reference
23//! returns torch's CPU tie order among equal scores and a kernel that picks
24//! any other set diverges from it on exactly those queries. The scores are
25//! bf16-valued and few (`[tokens, width]`), so this costs a download, not a
26//! kernel.
27//!
28//! Layer state (window ring, compressor partial group, the two caches) and the
29//! shared slots live on the device; the shared index selection and candidate
30//! mask are host vectors, as the reference keeps them.
31//!
32//! Split into submodules:
33//! - `init`: `AttnV41::new` (kernels, RoPE tables, workspaces) and `free`
34//! - `primitives`: the GEMM/GEMV, RMSNorm, RoPE and quantiser launch wrappers
35//! - `sources`: the kv-source compressor and the index-source indexer
36//! - `forward`: one layer's attention forward
37
38use anyhow::Result;
39use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
40
41/// An attention projection on the device: bf16 or the GGUF's raw `Q2_K`
42/// blocks (`Q3_K` is never an attention type here and is refused).
43pub use crate::layers::ops::ResidentMat as AttnMat;
44
45const MODULE: &str = "attn_v41";
46const GEMM_MODULE: &str = "gemm";
47/// One warp per quantiser block; 8 warps per launch block.
48const QUANT_BLOCKS_PER_LAUNCH: u32 = 8;
49
50/// Model-wide attention geometry, from `ModelConfig`.
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/// What one layer is, in the shared runtime.
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/// Compressor weights (kv sources only). At ratio > 1 `kv` and `gate` are f32
94/// `[hd, dim]` as the checkpoint stores them; at ratio 1 `kv` is bf16.
95pub struct CompressorWeightsGpu {
96 pub kv: DevicePtr,
97 pub gate: Option<DevicePtr>,
98 /// f32 `[hd]`
99 pub norm: DevicePtr,
100}
101
102/// Indexer weights (index sources only); `wk` / `k_norm` on kv sources.
103pub struct IndexerWeightsGpu {
104 /// bf16 `[index_heads * index_hd, q_rank]`
105 pub wq_b: DevicePtr,
106 /// bf16 `[index_heads, dim]`
107 pub weights_proj: DevicePtr,
108 /// bf16 `[index_hd, hd]`
109 pub wk: Option<DevicePtr>,
110 /// f32 `[index_hd]`
111 pub k_norm: Option<DevicePtr>,
112}
113
114/// One layer's resident attention weights. bf16 `[out, in]` unless noted.
115pub struct AttnV41LayerWeights {
116 pub role: LayerRole,
117 /// f32 `[n_heads]`
118 pub sink: DevicePtr,
119 pub wq_a: AttnMat,
120 /// f32 `[q_rank]`
121 pub q_norm: DevicePtr,
122 pub wq_b: AttnMat,
123 pub wkv: AttnMat,
124 /// f32 `[hd]`
125 pub kv_norm: DevicePtr,
126 /// `[groups * o_rank, gw]`
127 pub wo_a: AttnMat,
128 /// `[dim, groups * o_rank]`
129 pub wo_b: AttnMat,
130 pub comp: Option<CompressorWeightsGpu>,
131 pub idx: Option<IndexerWeightsGpu>,
132}
133
134/// One layer's device state across prefill and decode.
135pub struct AttnV41LayerState {
136 /// bf16 `[window, hd]`
137 window: DevicePtr,
138 /// f32 `[ratio, hd]` x 2, the partial group of a ratio-2 compressor
139 comp_state: Option<(DevicePtr, DevicePtr)>,
140 /// bf16 `[max_groups, hd]` (kv sources)
141 compress_kv: Option<DevicePtr>,
142 /// bf16 `[max_groups, index_hd]` (kv sources)
143 index_k: Option<DevicePtr>,
144}
145
146impl AttnV41LayerState {
147 pub fn new(gpu: &dyn GpuBackend, c: &AttnV41Cfg, role: LayerRole) -> Result<Self> {
148 let window = gpu.alloc(c.window * c.head_dim * 2)?;
149 gpu.memset(window, 0, c.window * c.head_dim * 2)?;
150 let (comp_state, compress_kv, index_k) = if role.is_kv_source {
151 let ratio = role.ratio.max(1);
152 let groups = c.max_seq / ratio;
153 let a = gpu.alloc(ratio * c.head_dim * 4)?;
154 let b = gpu.alloc(ratio * c.head_dim * 4)?;
155 gpu.memset(a, 0, ratio * c.head_dim * 4)?;
156 // score state starts at -inf as the reference's does
157 let neg: Vec<u8> =
158 std::iter::repeat_n(f32::NEG_INFINITY.to_le_bytes(), ratio * c.head_dim)
159 .flatten()
160 .collect();
161 gpu.copy_h2d(&neg, b)?;
162 let ckv = gpu.alloc(groups.max(1) * c.head_dim * 2)?;
163 gpu.memset(ckv, 0, groups.max(1) * c.head_dim * 2)?;
164 let ik = gpu.alloc(groups.max(1) * c.index_hd * 2)?;
165 gpu.memset(ik, 0, groups.max(1) * c.index_hd * 2)?;
166 (Some((a, b)), Some(ckv), Some(ik))
167 } else {
168 (None, None, None)
169 };
170 Ok(AttnV41LayerState {
171 window,
172 comp_state,
173 compress_kv,
174 index_k,
175 })
176 }
177
178 /// The window ring, bf16 `[window, hd]`: a pointer a captured decode step bakes.
179 pub fn window(&self) -> DevicePtr {
180 self.window
181 }
182
183 /// Free the four device buffers this sequence owns and clear the
184 /// pointers. Idempotent: `gpu.free` is null-safe and the taken options
185 /// stay `None`, so a second call frees nothing. Takes `&mut self` because
186 /// the state reaches `release_state` as `&mut dyn LayerState` and cannot
187 /// be moved out of the box.
188 pub fn free(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
189 let window = std::mem::replace(&mut self.window, DevicePtr::NULL);
190 gpu.free(window)?;
191 if let Some((a, b)) = self.comp_state.take() {
192 gpu.free(a)?;
193 gpu.free(b)?;
194 }
195 if let Some(p) = self.compress_kv.take() {
196 gpu.free(p)?;
197 }
198 if let Some(p) = self.index_k.take() {
199 gpu.free(p)?;
200 }
201 Ok(())
202 }
203}
204
205/// The `SharedAttentionRuntime`: what source layers publish for the layers
206/// below them.
207#[derive(Default)]
208pub struct SharedV41 {
209 /// the current kv source's latent cache and how many groups it holds
210 pub compress_kv: Option<DevicePtr>,
211 pub compress_len: usize,
212 /// the current kv source's index-key cache
213 pub index_k: Option<DevicePtr>,
214 /// `[queries][topk]`, offset for the window rows, -1 = absent
215 pub topk_idxs: Vec<i32>,
216 pub topk: usize,
217 /// `[queries][cand_width]`
218 pub candidates: Vec<bool>,
219 pub cand_width: usize,
220}
221
222/// The intermediates of one layer's forward, for oracles and for the caller.
223pub struct AttnV41Run {
224 /// bf16 `[tokens, n_heads, hd]`, after RoPE
225 pub q: DevicePtr,
226 /// window rows: the chunk's own rows on prefill, the ring on decode
227 pub rows_a: DevicePtr,
228 pub rows_a_len: usize,
229 /// compressed rows (the kv source's cache) and how many are attended
230 pub rows_b: Option<DevicePtr>,
231 pub rows_b_len: usize,
232 /// `[tokens][topk]`, window slots then compressed positions (offset by `rows_a_len`)
233 pub idx: Vec<i32>,
234 pub topk: usize,
235 /// bf16 `[tokens, n_heads, hd]`, pre inverse rotation
236 pub o: DevicePtr,
237 /// bf16 `[tokens, dim]`
238 pub out: DevicePtr,
239}
240
241struct Kernels {
242 gemm: KernelHandle,
243 /// `dense_gemv_bf16` for the single-token step: the tiled GEMM spends
244 /// 15 of its 16 rows idle at m = 1 (326 us a launch on GB10 vs the
245 /// GEMV's bandwidth-bound pass over the same `[N, K]` weight)
246 gemv: KernelHandle,
247 /// the K-quant path for `AttnMat::Q2K`: q8_1 row quant + GEMV at m <= 8,
248 /// D2S6 tile quant + MMQ above
249 q8_rows: KernelHandle,
250 mmvq_q2k_w: KernelHandle,
251 /// The eight output groups of `wo_a` in one launch (decode, m <= 8).
252 mmvq_q2k_groups_w: KernelHandle,
253 /// `wq_a` and `wkv` of one token in one launch (decode, m <= 8).
254 mmvq_q2k_pair_w: KernelHandle,
255 quant_d2s6: KernelHandle,
256 mmq_q2k_nc: KernelHandle,
257 mmq_q2k_wc: KernelHandle,
258 rmsnorm_bf16: KernelHandle,
259 rmsnorm_f32: KernelHandle,
260 rope: KernelHandle,
261 act_quant: KernelHandle,
262 fp4_quant: KernelHandle,
263 gemm_f32: KernelHandle,
264 /// the compressor's f32 product at m = 1, staged, same bits as `gemm_f32`
265 gemv_f32_staged: KernelHandle,
266 pool: KernelHandle,
267 index_score: KernelHandle,
268 sparse_attn: KernelHandle,
269 slice_cols: KernelHandle,
270 scatter_cols: KernelHandle,
271 scale_bf16: KernelHandle,
272 /// the decode step's window ring write with the slot read on the device
273 ring_put: KernelHandle,
274}
275
276/// The attention runtime: kernels, RoPE tables, and workspaces for up to
277/// `max_tokens` positions per call.
278pub struct AttnV41 {
279 pub cfg: AttnV41Cfg,
280 k: Kernels,
281 fc_plain: DevicePtr,
282 fc_yarn: DevicePtr,
283 // workspaces
284 /// q8_1 activations for the Q2_K projections (plain rows or MMQ tiles)
285 a_q8: DevicePtr,
286 qr_raw: DevicePtr,
287 qr: DevicePtr,
288 q: DevicePtr,
289 kv_raw: DevicePtr,
290 kv: DevicePtr,
291 o: DevicePtr,
292 o_rot: DevicePtr,
293 og: DevicePtr,
294 slice_in: DevicePtr,
295 slice_out: DevicePtr,
296 out: DevicePtr,
297 pos: DevicePtr,
298 head_pos: DevicePtr,
299 idx_pos: DevicePtr,
300 grp_pos: DevicePtr,
301 idx_dev: DevicePtr,
302 /// The captured step's selection for ratio-0 layers (window only), so a
303 /// multi-layer capture can hold both selection classes at once.
304 idx_dev_win: DevicePtr,
305 ckv: DevicePtr,
306 cscore: DevicePtr,
307 pooled: DevicePtr,
308 latent_raw: DevicePtr,
309 latent: DevicePtr,
310 ik_raw: DevicePtr,
311 ik: DevicePtr,
312 iq: DevicePtr,
313 iw_raw: DevicePtr,
314 iw: DevicePtr,
315 score: DevicePtr,
316 /// What the graph-captured decode step last uploaded into `pos` /
317 /// `head_pos` (the position) and `idx_dev` (the padded selection), so a
318 /// step re-uploads only what changed. `None` = unknown, upload.
319 decode_pos: Option<usize>,
320 decode_idx: Option<Vec<i32>>,
321 decode_idx_win: Option<Vec<i32>>,
322}
323
324fn upload_f32(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
325 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
326 let p = gpu.alloc(bytes.len().max(4))?;
327 gpu.copy_h2d(&bytes, p)?;
328 Ok(p)
329}
330
331fn upload_i32(gpu: &dyn GpuBackend, dst: DevicePtr, v: &[i32]) -> Result<()> {
332 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
333 gpu.copy_h2d(&bytes, dst)
334}
335
336/// Stream-ordered upload from a transient host vector (staged by the driver,
337/// no stream drain), for the per-token inputs of the captured decode step.
338fn upload_i32_async(gpu: &dyn GpuBackend, dst: DevicePtr, v: &[i32], stream: u64) -> Result<()> {
339 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
340 gpu.copy_h2d_async(&bytes, dst, stream)
341}
342
343fn at(p: DevicePtr, byte_off: usize) -> DevicePtr {
344 DevicePtr(p.0 + byte_off as u64)
345}
346
347mod decode;
348mod forward;
349mod init;
350mod primitives;
351mod sources;
352
353#[cfg(test)]
354#[path = "attn_v41_tests.rs"]
355mod tests;