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;