spark_model/layers/ops/prefill_attn_main_a.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Auto-extracted from `ops.rs` during refactor wave 4a.
4
5#![allow(unused_imports)]
6
7use anyhow::Result;
8use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
9use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
10
11use crate::layers::moe;
12use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
13
14use super::*;
15
16/// Flash Attention v2 prefill on contiguous Q/K/V.
17///
18/// Kernel: `inferspark_prefill(Q, K, V, O, seq_len, num_q_heads, num_kv_heads,
19/// head_dim, inv_sqrt_d, causal)`
20/// Grid: (num_q_heads, ceil(seq_len/32), batch) Block: (128, 1, 1)
21///
22/// Layout: Q [batch, seq_len, num_q_heads, head_dim] BF16
23/// K/V [batch, seq_len, num_kv_heads, head_dim] BF16
24/// O [batch, seq_len, num_q_heads, head_dim] BF16
25#[allow(clippy::too_many_arguments)]
26pub fn prefill_attention(
27 gpu: &dyn GpuBackend,
28 kernel: KernelHandle,
29 q: DevicePtr,
30 k: DevicePtr,
31 v: DevicePtr,
32 output: DevicePtr,
33 seq_len: u32,
34 batch: u32,
35 num_q_heads: u32,
36 num_kv_heads: u32,
37 head_dim: u32,
38 inv_sqrt_d: f32,
39 causal: bool,
40 sliding_window: u32, // 0 = no sliding limit; >0 = mask keys where q - k >= window
41 stream: u64,
42) -> Result<()> {
43 // ★ SSOT: the kernel NAME and its BR must agree, and they are chosen in two
44 // different files — `init.rs` resolves the handle, this launches it. A grid
45 // computed from the wrong BR does not fail, it silently computes the wrong
46 // q-tiles. `wide_prefill_kernel()` is the one reader of that decision.
47 let br = if head_dim > 256 {
48 wide_prefill_kernel(gpu).1
49 } else {
50 32u32
51 };
52 KernelLaunch::new(gpu, kernel)
53 .grid([num_q_heads, div_ceil(seq_len, br), batch])
54 .block([128, 1, 1])
55 .arg_ptr(q)
56 .arg_ptr(k)
57 .arg_ptr(v)
58 .arg_ptr(output)
59 .arg_u32(seq_len)
60 .arg_u32(num_q_heads)
61 .arg_u32(num_kv_heads)
62 .arg_u32(head_dim)
63 .arg_f32(inv_sqrt_d)
64 .arg_u32(if causal { 1 } else { 0 })
65 .arg_u32(sliding_window)
66 .launch(stream)
67}
68
69/// DeepSeek-V4 full-attention (non-CSA) prefill with a per-head attention sink.
70///
71/// Same as [`prefill_attention`] for HDIM=512 (BR=16), but passes the per-head
72/// `sinks` logit so the softmax denominator matches the decode path (which
73/// always applies the sink). Launches the V4-specific `inferspark_prefill_512`
74/// kernel. `sinks` may be `DevicePtr::NULL` for no sink.
75#[allow(clippy::too_many_arguments)]
76pub fn prefill_attention_512_sink(
77 gpu: &dyn GpuBackend,
78 kernel: KernelHandle,
79 q: DevicePtr,
80 k: DevicePtr,
81 v: DevicePtr,
82 output: DevicePtr,
83 seq_len: u32,
84 batch: u32,
85 num_q_heads: u32,
86 num_kv_heads: u32,
87 head_dim: u32,
88 inv_sqrt_d: f32,
89 causal: bool,
90 sliding_window: u32,
91 sinks: DevicePtr,
92 stream: u64,
93) -> Result<()> {
94 KernelLaunch::new(gpu, kernel)
95 .grid([num_q_heads, div_ceil(seq_len, 16), batch])
96 .block([128, 1, 1])
97 .arg_ptr(q)
98 .arg_ptr(k)
99 .arg_ptr(v)
100 .arg_ptr(output)
101 .arg_u32(seq_len)
102 .arg_u32(num_q_heads)
103 .arg_u32(num_kv_heads)
104 .arg_u32(head_dim)
105 .arg_f32(inv_sqrt_d)
106 .arg_u32(if causal { 1 } else { 0 })
107 .arg_u32(sliding_window)
108 .arg_ptr(sinks)
109 .launch(stream)
110}
111
112/// Contiguous prefill Flash Attention — BF16, BR=64 (256 threads).
113///
114/// Larger tile size halves CTA count and causal KV iterations for long sequences.
115/// Grid: (num_q_heads, ceil(seq_len/64), batch) Block: (256, 1, 1)
116#[allow(clippy::too_many_arguments)]
117pub fn prefill_attention_64(
118 gpu: &dyn GpuBackend,
119 kernel: KernelHandle,
120 q: DevicePtr,
121 k: DevicePtr,
122 v: DevicePtr,
123 output: DevicePtr,
124 seq_len: u32,
125 batch: u32,
126 num_q_heads: u32,
127 num_kv_heads: u32,
128 head_dim: u32,
129 inv_sqrt_d: f32,
130 causal: bool,
131 sliding_window: u32,
132 stream: u64,
133) -> Result<()> {
134 // BR64 = query rows processed per CTA. The kernel clamps this to 32 on the
135 // AMD targets (gfx1151 64 KB LDS cap: inferspark_prefill.cu's
136 // `#if __SCALE__ || __HIP_PLATFORM_AMD__ #define BR64 32`). The grid stride
137 // MUST match the kernel's BR64, else CTAs are spaced 64 rows apart while each
138 // writes only 32 → query rows 32..63 of every 64-row band are silently left
139 // unwritten (gross attention corruption for any prompt >32 tokens). cfg!
140 // (atlas_scale) is set for both `strix` and `strix-hip`; NVIDIA keeps 64
141 // (byte-identical). See the @human-review note in inferspark_prefill.cu.
142 let br = if cfg!(atlas_scale) { 32u32 } else { 64u32 };
143 KernelLaunch::new(gpu, kernel)
144 .grid([num_q_heads, div_ceil(seq_len, br), batch])
145 .block([256, 1, 1])
146 .arg_ptr(q)
147 .arg_ptr(k)
148 .arg_ptr(v)
149 .arg_ptr(output)
150 .arg_u32(seq_len)
151 .arg_u32(num_q_heads)
152 .arg_u32(num_kv_heads)
153 .arg_u32(head_dim)
154 .arg_f32(inv_sqrt_d)
155 .arg_u32(if causal { 1 } else { 0 })
156 .arg_u32(sliding_window)
157 .launch(stream)
158}
159
160/// Contiguous prefill Flash Attention — FP8 E4M3 K/V variant (BR=64).
161///
162/// Q is BF16, K/V are FP8 E4M3 (dequantized to BF16 in shared memory).
163/// Halves K/V memory reads compared to the BF16 kernel.
164///
165/// Grid: (num_q_heads, ceil(seq_len/64), batch) Block: (256, 1, 1)
166#[allow(clippy::too_many_arguments)]
167pub fn prefill_attention_fp8kv(
168 gpu: &dyn GpuBackend,
169 kernel: KernelHandle,
170 q: DevicePtr,
171 k_fp8: DevicePtr,
172 v_fp8: DevicePtr,
173 output: DevicePtr,
174 seq_len: u32,
175 batch: u32,
176 num_q_heads: u32,
177 num_kv_heads: u32,
178 head_dim: u32,
179 inv_sqrt_d: f32,
180 causal: bool,
181 stream: u64,
182) -> Result<()> {
183 let br = 64u32;
184 KernelLaunch::new(gpu, kernel)
185 .grid([num_q_heads, div_ceil(seq_len, br), batch])
186 .block([256, 1, 1])
187 .arg_ptr(q)
188 .arg_ptr(k_fp8)
189 .arg_ptr(v_fp8)
190 .arg_ptr(output)
191 .arg_u32(seq_len)
192 .arg_u32(num_q_heads)
193 .arg_u32(num_kv_heads)
194 .arg_u32(head_dim)
195 .arg_f32(inv_sqrt_d)
196 .arg_u32(if causal { 1 } else { 0 })
197 .launch(stream)
198}
199
200/// Paged prefill Flash Attention — reads K/V from paged KV cache via block_table.
201///
202/// For chunked prefill chunk 1+: Q comes from GEMM (contiguous), K/V reside
203/// in the paged cache from prior chunks. Replaces per-token paged decode loop
204/// with a single Flash Attention pass (O(N) per chunk instead of O(N^2) total).
205///
206/// Kernel: `inferspark_prefill_paged(Q, K_cache, V_cache, O, block_table,
207/// q_len, kv_len, q_offset, nq, nkv, hd, block_size, inv_sqrt_d)`
208/// Grid: (num_q_heads, ceil(q_len/32), 1) Block: (128, 1, 1)
209#[allow(clippy::too_many_arguments)]
210pub fn prefill_attention_paged(
211 gpu: &dyn GpuBackend,
212 kernel: KernelHandle,
213 q: DevicePtr,
214 k_cache: DevicePtr,
215 v_cache: DevicePtr,
216 output: DevicePtr,
217 block_table: DevicePtr,
218 q_len: u32,
219 kv_len: u32,
220 q_offset: u32,
221 num_q_heads: u32,
222 num_kv_heads: u32,
223 head_dim: u32,
224 cache_block_size: u32,
225 sliding_window: u32,
226 inv_sqrt_d: f32,
227 stream: u64,
228) -> Result<()> {
229 let br = 32u32;
230 KernelLaunch::new(gpu, kernel)
231 .grid([num_q_heads, div_ceil(q_len, br), 1])
232 .block([128, 1, 1])
233 .arg_ptr(q)
234 .arg_ptr(k_cache)
235 .arg_ptr(v_cache)
236 .arg_ptr(output)
237 .arg_ptr(block_table)
238 .arg_u32(q_len)
239 .arg_u32(kv_len)
240 .arg_u32(q_offset)
241 .arg_u32(num_q_heads)
242 .arg_u32(num_kv_heads)
243 .arg_u32(head_dim)
244 .arg_u32(cache_block_size)
245 .arg_u32(sliding_window)
246 // causal_mask_enabled = 1 (default causal). DFlash γ-block kernels
247 // pass 0 via dedicated dispatchers (`prefill_attention_paged_dflash_*`).
248 .arg_u32(1u32)
249 .arg_f32(inv_sqrt_d)
250 .launch(stream)
251}
252
253/// Paged prefill Flash Attention — FP8 KV cache variant.
254#[allow(clippy::too_many_arguments)]
255pub fn prefill_attention_paged_fp8(
256 gpu: &dyn GpuBackend,
257 kernel: KernelHandle,
258 q: DevicePtr,
259 k_cache: DevicePtr,
260 v_cache: DevicePtr,
261 output: DevicePtr,
262 block_table: DevicePtr,
263 q_len: u32,
264 kv_len: u32,
265 q_offset: u32,
266 num_q_heads: u32,
267 num_kv_heads: u32,
268 head_dim: u32,
269 cache_block_size: u32,
270 sliding_window: u32,
271 inv_sqrt_d: f32,
272 k_scale: f32,
273 v_scale: f32,
274 cache_stride: u64,
275 stream: u64,
276) -> Result<()> {
277 let br = 32u32;
278 KernelLaunch::new(gpu, kernel)
279 .grid([num_q_heads, div_ceil(q_len, br), 1])
280 .block([128, 1, 1])
281 .arg_ptr(q)
282 .arg_ptr(k_cache)
283 .arg_ptr(v_cache)
284 .arg_ptr(output)
285 .arg_ptr(block_table)
286 .arg_u32(q_len)
287 .arg_u32(kv_len)
288 .arg_u32(q_offset)
289 .arg_u32(num_q_heads)
290 .arg_u32(num_kv_heads)
291 .arg_u32(head_dim)
292 .arg_u32(cache_block_size)
293 .arg_u32(sliding_window)
294 // causal_mask_enabled = 1 (default causal). DFlash γ-block kernels
295 // pass 0 via dedicated dispatchers (`prefill_attention_paged_dflash_*`).
296 .arg_u32(1u32)
297 .arg_f32(inv_sqrt_d)
298 .arg_f32(k_scale)
299 .arg_f32(v_scale)
300 .arg_u64(cache_stride)
301 .launch(stream)
302}
303
304/// DFlash γ-block paged Flash Attention — FP8 KV cache variant.
305///
306/// Same kernel binary as [`prefill_attention_paged_fp8`] but launched with
307/// `causal_mask_enabled = 0`, producing bidirectional attention within the
308/// γ-token query block. The prefix KV positions are still strictly less
309/// than `q_offset` so they need no causal mask in this mode (every prefix
310/// position is "older" than every query, which is the no-mask case anyway).
311///
312/// Used by `BlockDiffusionDraftHead::forward_block` once per drafter layer.
313/// `q_len` is γ (typically 16). `q_offset` is the absolute starting index
314/// of the γ-block in the drafter's logical sequence; the kernel uses it to
315/// skip the now-disabled causal compare against `kv_start+col`.
316#[allow(clippy::too_many_arguments)]
317pub fn prefill_attention_paged_fp8_dflash(
318 gpu: &dyn GpuBackend,
319 kernel: KernelHandle,
320 q: DevicePtr,
321 k_cache: DevicePtr,
322 v_cache: DevicePtr,
323 output: DevicePtr,
324 block_table: DevicePtr,
325 q_len: u32,
326 kv_len: u32,
327 q_offset: u32,
328 num_q_heads: u32,
329 num_kv_heads: u32,
330 head_dim: u32,
331 cache_block_size: u32,
332 sliding_window: u32,
333 inv_sqrt_d: f32,
334 k_scale: f32,
335 v_scale: f32,
336 cache_stride: u64,
337 stream: u64,
338) -> Result<()> {
339 let br = 32u32;
340 KernelLaunch::new(gpu, kernel)
341 .grid([num_q_heads, div_ceil(q_len, br), 1])
342 .block([128, 1, 1])
343 .arg_ptr(q)
344 .arg_ptr(k_cache)
345 .arg_ptr(v_cache)
346 .arg_ptr(output)
347 .arg_ptr(block_table)
348 .arg_u32(q_len)
349 .arg_u32(kv_len)
350 .arg_u32(q_offset)
351 .arg_u32(num_q_heads)
352 .arg_u32(num_kv_heads)
353 .arg_u32(head_dim)
354 .arg_u32(cache_block_size)
355 .arg_u32(sliding_window)
356 .arg_u32(0u32) // causal_mask_enabled = 0 (DFlash bidirectional)
357 .arg_f32(inv_sqrt_d)
358 .arg_f32(k_scale)
359 .arg_f32(v_scale)
360 .arg_u64(cache_stride)
361 .launch(stream)
362}
363
364/// DFlash γ-block paged Flash Attention — BF16 KV cache variant.
365///
366/// Same kernel binary as [`prefill_attention_paged`] but launched with
367/// `causal_mask_enabled = 0`, producing bidirectional attention within the
368/// γ-token query block. The prefix KV positions are strictly less than
369/// `q_offset` so they need no causal mask in this mode (every prefix
370/// position is "older" than every query, which is the no-mask case anyway).
371///
372/// Used by `BlockDiffusionDraftHead::forward_block` once per drafter layer
373/// when the drafter KV cache is BF16 (current default — FP8 acceptance
374/// collapses on SM12.x per `dflash_head.rs:82–86`).
375///
376/// `q_len` is γ (typically 16). `q_offset` is the absolute starting index
377/// of the γ-block in the drafter's logical sequence; the kernel uses it to
378/// skip the now-disabled causal compare against `kv_start+col`.
379#[allow(clippy::too_many_arguments)]
380pub fn prefill_attention_paged_dflash(
381 gpu: &dyn GpuBackend,
382 kernel: KernelHandle,
383 q: DevicePtr,
384 k_cache: DevicePtr,
385 v_cache: DevicePtr,
386 output: DevicePtr,
387 block_table: DevicePtr,
388 q_len: u32,
389 kv_len: u32,
390 q_offset: u32,
391 num_q_heads: u32,
392 num_kv_heads: u32,
393 head_dim: u32,
394 cache_block_size: u32,
395 sliding_window: u32,
396 inv_sqrt_d: f32,
397 stream: u64,
398) -> Result<()> {
399 let br = 32u32;
400 KernelLaunch::new(gpu, kernel)
401 .grid([num_q_heads, div_ceil(q_len, br), 1])
402 .block([128, 1, 1])
403 .arg_ptr(q)
404 .arg_ptr(k_cache)
405 .arg_ptr(v_cache)
406 .arg_ptr(output)
407 .arg_ptr(block_table)
408 .arg_u32(q_len)
409 .arg_u32(kv_len)
410 .arg_u32(q_offset)
411 .arg_u32(num_q_heads)
412 .arg_u32(num_kv_heads)
413 .arg_u32(head_dim)
414 .arg_u32(cache_block_size)
415 .arg_u32(sliding_window)
416 .arg_u32(0u32) // causal_mask_enabled = 0 (DFlash bidirectional)
417 .arg_f32(inv_sqrt_d)
418 // Non-indirect kernel: q_rope_pos is a local var (= q_offset) in the
419 // .cuh body — no extra kernel arg. Fix applies via indirect path only.
420 .launch(stream)
421}
422
423/// DFlash γ-block paged Flash Attention — BF16 KV cache, INDIRECT scalar args.
424///
425/// Phase 5 (CUDA graph) variant of [`prefill_attention_paged_dflash`]. Reads
426/// `kv_len`, `q_offset`, and `q_rope_pos` from device pointers at kernel entry
427/// instead of taking them as kernel scalar arguments. This makes the launch
428/// graph-friendly: the host writes the dynamic triple into `kv_len_q_offset_dev`
429/// (12 bytes: `[u32 kv_len, u32 q_offset, u32 q_rope_pos]`) BEFORE entering the
430/// captured region, and the captured graph node binds only the pointer — replays
431/// pick up whatever values the host wrote pre-launch.
432/// `q_offset` = ctx_count (cache-block addressing); `q_rope_pos` = absolute
433/// decode position (query RoPE rotation, decoupled from cache addressing).
434///
435/// Resolves to kernel `inferspark_prefill_paged_indirect`. The kernel binary
436/// is otherwise identical to `inferspark_prefill_paged` (`causal_mask_enabled
437/// = 0` is still hardcoded here on the launch side).
438///
439/// Phase B note: defined but NOT YET WIRED IN to forward_block_layer_paged.
440/// Phase C swaps the dispatcher; Phase D adds graph capture around it.
441#[allow(clippy::too_many_arguments)]
442pub fn prefill_attention_paged_dflash_bf16_indirect(
443 gpu: &dyn GpuBackend,
444 kernel: KernelHandle,
445 q: DevicePtr,
446 k_cache: DevicePtr,
447 v_cache: DevicePtr,
448 output: DevicePtr,
449 block_table: DevicePtr,
450 q_len: u32,
451 kv_len_q_offset_dev: DevicePtr,
452 num_q_heads: u32,
453 num_kv_heads: u32,
454 head_dim: u32,
455 cache_block_size: u32,
456 sliding_window: u32,
457 inv_sqrt_d: f32,
458 stream: u64,
459) -> Result<()> {
460 let br = 32u32;
461 // q_offset's only role in the captured-args path is grid sizing; we still
462 // pass q_len (a model constant, γ) directly. The kernel will overwrite its
463 // scalar `kv_len`/`q_offset` slots from the indirect buffer at entry, so
464 // we feed placeholder zeros for those two args here — the values are
465 // *ignored* once `KERNEL_PREAMBLE` runs in the .cu file.
466 KernelLaunch::new(gpu, kernel)
467 .grid([num_q_heads, div_ceil(q_len, br), 1])
468 .block([128, 1, 1])
469 .arg_ptr(q)
470 .arg_ptr(k_cache)
471 .arg_ptr(v_cache)
472 .arg_ptr(output)
473 .arg_ptr(block_table)
474 .arg_u32(q_len)
475 .arg_u32(0u32) // kv_len placeholder — overwritten by KERNEL_PREAMBLE
476 .arg_u32(0u32) // q_offset placeholder — overwritten by KERNEL_PREAMBLE
477 .arg_u32(num_q_heads)
478 .arg_u32(num_kv_heads)
479 .arg_u32(head_dim)
480 .arg_u32(cache_block_size)
481 .arg_u32(sliding_window)
482 .arg_u32(0u32) // causal_mask_enabled = 0 (DFlash bidirectional)
483 .arg_f32(inv_sqrt_d)
484 .arg_ptr(kv_len_q_offset_dev) // KERNEL_EXTRA_PARAMS: kv_len_ptr
485 .arg_ptr(kv_len_q_offset_dev.offset(4)) // KERNEL_EXTRA_PARAMS: q_offset_ptr
486 .arg_ptr(kv_len_q_offset_dev.offset(8)) // KERNEL_EXTRA_PARAMS: q_rope_pos_ptr
487 .launch(stream)
488}