avarok_kernels/attn_splitk.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! The paged-decode attention SPLIT-K policy: how many KV splits a launch
4//! uses, as a pure function of CONFIGURATION.
5//!
6//! # The defect (#928)
7//!
8//! `crates/spark-model/.../decode/run_paged_decode.rs` imported
9//! `avarok_core::device::sm121::NUM_SMS` — the GB10 constant, 48 — and picked
10//!
11//! ```text
12//! current_ctas = num_q_heads * split_ref_seqs(num_seqs, max_decode_seqs);
13//! num_splits = if current_ctas >= NUM_SMS { 1 } else { NUM_SMS / current_ctas };
14//! ```
15//!
16//! On Qwen3.8-27B at `--max-batch-size 16` that is `24 * 16 = 384 >= 48`, so
17//! `num_splits` was 1 at EVERY batch size including C=1. nsys on 1xH100 80GB
18//! HBM3 (round 13 cell T1N) has the receipt: `paged_decode_attn_fp8` runs
19//! `grid=(24,1,1)` — 24 CTAs on 132 SMs — 231.51 us/launch for 9.93 MB of KV,
20//! i.e. 42.9 GB/s = **1.28% of HBM**, and with the BF16-KV sibling the pair is
21//! 3.79 ms of a 16.69 ms C=1 decode step (22.7%). The source comment
22//! dismissing occupancy here ("attention occupancy is NOT the long-ctx
23//! bottleneck") records a GB10 A/B on a 48-SM part.
24//!
25//! Two things were wrong and both are fixed here: the SM count is a property
26//! of the compiled TARGET (`kernels/<hw>/HARDWARE.toml` `[hardware] sm_count`,
27//! baked as [`crate::TARGET_SM_COUNT`]), and the occupancy the rule should
28//! size for is the SINGLE-STREAM shape, which is the one that starves.
29//!
30//! # The determinism invariant — why this module is pure
31//!
32//! The online-softmax split-merge is NON-ASSOCIATIVE. If `num_splits` moved
33//! with the runtime co-batched count, one sequence would traverse a different
34//! reduction tree alone than beside fifteen others and flip its temp-0 argmax
35//! — the nondeterminism `split_ref_seqs` was introduced to stop
36//! (`tasks/determinism_investigation.md`). So [`SplitkPolicy::Auto`] and
37//! [`SplitkPolicy::Pinned`] read `(sm_count, num_q_heads, max_decode_seqs)`
38//! and NOTHING else: the split count is fixed for the life of a serve.
39//! [`SplitkPolicy::Legacy`] is the pre-#928 rule preserved verbatim, including
40//! its dependence on `split_ref_seqs`, because every target but Hopper still
41//! runs it and "unchanged" has to mean unchanged.
42//!
43//! Short contexts are handled INSIDE the kernel
44//! (`kernels/hopper/common/paged_decode_splitk_hopper.cuh`,
45//! `PD_MIN_KV_PER_SPLIT`), from each sequence's own `seq_len`: the host may not
46//! branch on `seq_lens`, which is device memory and behind a captured CUDA
47//! graph, and a per-sequence rule stays co-batch invariant where a per-batch
48//! one would not.
49
50/// Waves of CTAs the `auto` policy aims to put on the device at the
51/// single-stream shape.
52///
53/// TWO, not one: the attention CTAs are memory-latency bound (a paged K/V
54/// gather, a shuffle reduction and an `__expf` per position), so one CTA per
55/// SM leaves the SM stalled on loads. Two resident waves is the smallest
56/// number that lets one cover the other's misses, and it is also where the
57/// split stops being free — every extra split is another partial for the
58/// reduce to merge and another eighth-of-a-CTA of Q load to repeat. Round 14
59/// measures the curve: the microtest prints GB/s for `num_splits` 1/2/4/6 at
60/// three context lengths.
61pub const SPLITK_TARGET_WAVES: u32 = 2;
62
63/// Hard ceiling on the split count, and the bound the split-K workspace is
64/// sized against.
65///
66/// A cap rather than a pure occupancy answer because the workspace is
67/// `rows * num_q_heads * num_splits * (head_dim + 2)` F32 — linear in this
68/// number — and because a model with very few q heads (an MQA decode head, a
69/// draft model) would otherwise ask for a split per KV block. 16 covers
70/// `2 * 148 / 24 = 13` on the widest declared target
71/// (`kernels/b200`, 148 SMs) with room, and an explicit
72/// `attn_decode_splitk = N` is clamped to it.
73pub const MAX_DECODE_SPLITS: u32 = 16;
74
75/// How a target picks its paged-decode split count.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum SplitkPolicy {
78 /// The pre-#928 rule: `sm_count / (num_q_heads * split_ref_seqs)`, or 1
79 /// when that product already covers the device. Reads the runtime
80 /// reference batch, which is why it is not the default anywhere new.
81 Legacy,
82 /// Fill [`SPLITK_TARGET_WAVES`] waves at the SINGLE-STREAM shape:
83 /// `clamp(ceil(WAVES * sm_count / num_q_heads), 1, MAX_DECODE_SPLITS)`.
84 Auto,
85 /// An operator-pinned count, clamped to `1..=MAX_DECODE_SPLITS`. `0` and
86 /// `off` arrive here as `Pinned(1)` — one split IS no split-K.
87 Pinned(u32),
88}
89
90impl SplitkPolicy {
91 /// The spelling this policy round-trips through [`parse`], for the serve
92 /// log's `target defaults (<hw>): …` line.
93 pub fn label(self) -> String {
94 match self {
95 SplitkPolicy::Legacy => "legacy".to_string(),
96 SplitkPolicy::Auto => "auto".to_string(),
97 SplitkPolicy::Pinned(n) => n.to_string(),
98 }
99 }
100}
101
102/// `legacy` | `auto` | `0`/`off`/`false`/`no` | a decimal count.
103///
104/// `None` for anything else — the caller keeps the target's declaration rather
105/// than guessing, because a typo that silently resolved to `auto` would arm a
106/// geometry change on a card with no receipt for it.
107pub fn parse(spelling: &str) -> Option<SplitkPolicy> {
108 match spelling.trim().to_ascii_lowercase().as_str() {
109 "legacy" => Some(SplitkPolicy::Legacy),
110 "auto" => Some(SplitkPolicy::Auto),
111 "0" | "off" | "false" | "no" => Some(SplitkPolicy::Pinned(1)),
112 other => other
113 .parse::<u32>()
114 .ok()
115 .map(|n| SplitkPolicy::Pinned(n.clamp(1, MAX_DECODE_SPLITS))),
116 }
117}
118
119/// The target's declaration, overridden by `AVAROK_ATTN_DECODE_SPLITK`.
120///
121/// Returns `(policy, came_from_env)`. THE one resolution rule: both the
122/// dispatch (via `spark_model`'s `target_defaults::resolve`) and the buffer
123/// arena (via [`policy_from_env`]) call this, so the split count a launch uses
124/// and the split count the workspace was sized for cannot disagree.
125pub fn resolve_policy(declared: &str, env_raw: Option<&str>) -> (SplitkPolicy, bool) {
126 let declared = parse(declared).unwrap_or(SplitkPolicy::Legacy);
127 match env_raw.and_then(parse) {
128 Some(p) => (p, true),
129 None => (declared, false),
130 }
131}
132
133/// [`resolve_policy`] against the process environment and this binary's baked
134/// declaration.
135///
136/// For the buffer-sizing call site, which lives below `spark-model` in the
137/// dependency graph and so cannot reach its resolver. It is the SAME pure
138/// rule, not a second one; the serve log still reports the value once, from
139/// `spark-model`.
140pub fn policy_from_env() -> SplitkPolicy {
141 resolve_policy(
142 crate::TARGET_DEFAULTS.attn_decode_splitk,
143 std::env::var("AVAROK_ATTN_DECODE_SPLITK").ok().as_deref(),
144 )
145 .0
146}
147
148/// `clamp(ceil(WAVES * sm_count / num_q_heads), 1, MAX_DECODE_SPLITS)`.
149///
150/// The single-stream occupancy answer: at C=1 the grid is
151/// `(num_q_heads, num_splits, 1)`, so this is the split count that puts
152/// `WAVES * sm_count` CTAs on the device with one sequence in flight. At a
153/// wider batch the same count over-subscribes — total WORK is unchanged, only
154/// its partition is — which is the trade this policy takes deliberately: a
155/// C=1 step is 24 CTAs without it and a C=16 step is already 3 waves with it.
156pub fn auto_splits(sm_count: u32, num_q_heads: u32) -> u32 {
157 let heads = num_q_heads.max(1);
158 let target = SPLITK_TARGET_WAVES.saturating_mul(sm_count.max(1));
159 target.div_ceil(heads).clamp(1, MAX_DECODE_SPLITS)
160}
161
162/// The pre-#928 rule, preserved verbatim for every target that still declares
163/// it. `ref_seqs` is `split_ref_seqs(num_seqs, max_decode_seqs)`.
164pub fn legacy_splits(sm_count: u32, num_q_heads: u32, ref_seqs: u32) -> u32 {
165 let current_ctas = num_q_heads.max(1).saturating_mul(ref_seqs.max(1));
166 if current_ctas >= sm_count {
167 1
168 } else {
169 sm_count / current_ctas
170 }
171}
172
173/// The split count for a launch.
174///
175/// ⚠️ `legacy_ref_seqs` is read by [`SplitkPolicy::Legacy`] ONLY. Every other
176/// arm is a pure function of configuration, which is the determinism invariant
177/// this module exists to hold — see the module header, and
178/// `the_auto_split_count_does_not_move_with_the_co_batched_count`.
179pub fn num_splits(
180 policy: SplitkPolicy,
181 sm_count: u32,
182 num_q_heads: u32,
183 legacy_ref_seqs: u32,
184) -> u32 {
185 match policy {
186 SplitkPolicy::Legacy => legacy_splits(sm_count, num_q_heads, legacy_ref_seqs),
187 SplitkPolicy::Auto => auto_splits(sm_count, num_q_heads),
188 SplitkPolicy::Pinned(n) => n.clamp(1, MAX_DECODE_SPLITS),
189 }
190}
191
192/// `[o[head_dim], m, l]` slots the split-K workspace must hold.
193///
194/// The split-K kernel addresses
195/// `((seq * num_q_heads) + head) * num_splits + split`, so the arena has to
196/// cover the WIDEST batch the decode-metadata layout will accept
197/// (`DecodeMetaLayout::rows()`), not the pinned max batch — a short
198/// allocation here is an out-of-bounds device write, silently.
199///
200/// `Legacy` keeps its old constant bound and its old arena: that rule picks
201/// `sm_count / (num_q_heads * max(max_batch, num_seqs))`, so
202/// `num_seqs * num_q_heads * num_splits <= sm_count` for every batch, which is
203/// exactly what `sizes.rs` allocated before #928.
204pub fn workspace_slots(
205 policy: SplitkPolicy,
206 sm_count: u32,
207 num_q_heads: u32,
208 max_rows: u32,
209 legacy_ref_seqs: u32,
210) -> u32 {
211 match policy {
212 SplitkPolicy::Legacy => sm_count.max(1),
213 other => max_rows
214 .max(1)
215 .saturating_mul(num_q_heads.max(1))
216 .saturating_mul(num_splits(other, sm_count, num_q_heads, legacy_ref_seqs)),
217 }
218}
219
220// ════════════════════════════════════════════════════════════════════════════
221// GQA-PACKED PAGED DECODE — the other half of the same launch geometry.
222// ════════════════════════════════════════════════════════════════════════════
223//
224// Hosted in this module rather than a new one because it answers the same
225// question the split policy answers — HOW MANY CTAs a paged-decode launch
226// gets, and over what — and because the two are coupled: the packed kernel
227// divides the CTA count by [`DECODE_GQA_PACK_WIDTH`], which is only safe where
228// the split policy has already resolved one split. One module, one answer.
229//
230// # What the packed kernel is
231//
232// `paged_decode_attn{,_fp8}` launch `grid (num_q_heads, num_seqs)`. With
233// `gqa_ratio = num_q_heads / num_kv_heads` heads sharing a KV head, the
234// `gqa_ratio` CTAs of a group each walk that head's WHOLE K and V
235// independently — `kv_head = q_head / gqa_ratio` is all that separates their
236// address streams, and it is equal across the group. The packed twins
237// (`kernels/gb10/common/paged_decode_attn_{fp8,bf16}_gqa.cu`) put the whole
238// group in ONE CTA, hold its query vectors in registers, and read each K and
239// V row once: `grid (num_kv_heads, num_seqs)`.
240//
241// # Why it is restricted to the non-split arm
242//
243// Packing divides the CTA count by the pack width, so a shape that needed
244// split-K to fill the device needs MORE splits after packing, derived from
245// `num_kv_heads` instead of `num_q_heads`. A different split count is a
246// different partition of the KV range, hence a different online-softmax merge
247// tree, hence different output bytes — the exact non-associativity the split
248// policy above is pinned against. The non-split arm has no such coupling, so
249// the packed kernel there is BIT-IDENTICAL to the unpacked one and needs no
250// gate record re-opened. Extending packing to the split arm is a separate,
251// numerics-visible change and is deliberately not made here.
252
253// # The fused last-CTA reduce, CONSIDERED AND DECLINED (2026-09-20)
254//
255// The obvious companion change is to fold `paged_decode_attn_reduce_fp8` into
256// the split kernel — an arrival counter per `(seq, head)`, last CTA merges —
257// removing one launch per layer per token. It is not done, and the reason is
258// this module's own arithmetic rather than a difficulty:
259//
260// * On every target that declares [`SplitkPolicy::Legacy`] — which is all of
261// them but Hopper — `legacy_splits(48, 24, ref_seqs)` is `1` for every
262// `ref_seqs >= 2`, and `ref_seqs` is `max(max_batch_size, num_seqs)`. So on
263// GB10 the reduce kernel is LAUNCHED ONLY AT `--max-batch 1`. Every
264// co-batched shape a campaign measures takes the non-split arm, where there
265// is no second launch to remove.
266// * The saving it targets is a LAUNCH, and the launch it removes is inside a
267// captured CUDA graph, where launch cost is already the cheap case. What it
268// adds is a `__threadfence` and an atomic on every split CTA, plus a merge
269// serialised into the last arriver while the rest of the device drains.
270// * The counter has nowhere to live inside this change's blast radius. The
271// split-K workspace is sized by [`workspace_slots`] and allocated in
272// `spark-runtime`'s buffer arena, so a counter region means changing the
273// arena's layout; a module-scope `__device__` array instead would need a
274// compile-time capacity bound with no configuration to derive it from, and
275// would be shared — unsynchronised — by any two launches of the same kernel
276// in flight on different streams.
277//
278// Worth recording that it would be BIT-IDENTICAL if done: the separate reduce
279// merges splits in INDEX order `0..num_splits`, and a last-CTA reduction that
280// keeps that order does the same additions in the same sequence. The reason
281// not to do it is that it is small, and small on an arm GB10 does not take.
282
283/// Query heads one packed CTA carries.
284///
285/// A COMPILE-TIME shape on both sides: the kernels size `q_reg`/`o_reg`
286/// register arrays from `#define PD_GQA`, because a runtime bound would put
287/// them in local memory and defeat the change. This constant is the Rust
288/// spelling of that `#define`, and
289/// `cuda_sources_declare_the_pack_width_rust_dispatches_on` fails if the two
290/// drift.
291///
292/// 6 is the Qwen3.8-27B decode ratio (`nq = 24`, `nkv = 4`). A model with any
293/// other ratio is REFUSED by [`gqa_pack_shape_ok`] and keeps the unpacked
294/// kernel; it is not approximated.
295pub const DECODE_GQA_PACK_WIDTH: u32 = 6;
296
297/// The head dim the packed kernels are compiled for.
298///
299/// Same reason as the split-K siblings' [`crate::attn_splitk`] head-dim pin:
300/// the sources fix `PD_HDIM` at 256 and derive every lane's element count from
301/// it, so a 128- or 512-wide head would have every lane stride past its own
302/// row.
303pub const DECODE_GQA_PACK_HEAD_DIM: u32 = 256;
304
305/// The packed kernels are OFF unless explicitly armed.
306///
307/// Declared here rather than in `kernels/<hw>/HARDWARE.toml` because there is
308/// no receipt for this arm on any target yet: it is a launch-geometry change
309/// whose payoff (`DECODE_GQA_PACK_WIDTH`x fewer KV load instructions) trades
310/// against a measured occupancy cost — ptxas reports 227 registers for the FP8
311/// twin and 243 for the BF16 one, zero spills, i.e. ONE resident CTA per SM,
312/// against 48/56 registers and up to five for the unpacked kernels. That trade
313/// can only be settled by an A/B on the box. `AVAROK_ATTN_DECODE_GQA_PACK=1`
314/// is how that A/B is run — the same mechanism `AVAROK_ATTN_DECODE_SPLITK=auto`
315/// uses for the policy above. A target that wins the A/B moves this to a
316/// `[defaults]` row with the receipt beside it.
317pub const DECODE_GQA_PACK_DECLARED: bool = false;
318
319/// `1`/`on`/`true`/`yes` | `0`/`off`/`false`/`no`.
320///
321/// `None` for anything else, so a typo keeps the declaration rather than
322/// silently arming (or disarming) a geometry change — same rule as [`parse`].
323pub fn parse_gqa_pack(spelling: &str) -> Option<bool> {
324 match spelling.trim().to_ascii_lowercase().as_str() {
325 "1" | "on" | "true" | "yes" => Some(true),
326 "0" | "off" | "false" | "no" => Some(false),
327 _ => None,
328 }
329}
330
331/// [`DECODE_GQA_PACK_DECLARED`], overridden by `AVAROK_ATTN_DECODE_GQA_PACK`.
332///
333/// Pure in its arguments so the resolution is testable without touching the
334/// process environment; [`gqa_pack_enabled`] is the one site that reads it.
335pub fn resolve_gqa_pack(declared: bool, env_raw: Option<&str>) -> bool {
336 env_raw.and_then(parse_gqa_pack).unwrap_or(declared)
337}
338
339/// The armed/disarmed answer for this process, resolved ONCE.
340///
341/// `OnceLock`, not a per-call `env::var`: this is read on the decode path, per
342/// layer per token, and `spark_model`'s `hot_path_env_guards` exists because
343/// that read takes the process-wide environment lock and costs 5.76 us at 16
344/// threads. Resolving once is also what CUDA graph capture requires — the
345/// kernel a captured graph holds cannot depend on an environment read that
346/// might answer differently on replay.
347pub fn gqa_pack_enabled() -> bool {
348 static ARMED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
349 *ARMED.get_or_init(|| {
350 resolve_gqa_pack(
351 DECODE_GQA_PACK_DECLARED,
352 std::env::var("AVAROK_ATTN_DECODE_GQA_PACK").ok().as_deref(),
353 )
354 })
355}
356
357/// Whether this launch's SHAPE is one the packed kernels can serve.
358///
359/// Every condition is a hard precondition of the kernel, not a preference:
360///
361/// * `num_kv_heads > 0` — it is the grid's x extent and the divisor below.
362/// * `num_q_heads == num_kv_heads * DECODE_GQA_PACK_WIDTH` — the kernel
363/// recovers its heads as `kv_head * PD_GQA + h`, the exact inverse of the
364/// unpacked kernel's `q_head / gqa_ratio`. Any other ratio reads and writes
365/// the wrong heads, silently, so it is refused rather than clamped. Stated
366/// as a MULTIPLICATION on purpose: `num_q_heads / num_kv_heads` truncates,
367/// so 25 q heads over 4 kv heads would pass a division test and then have
368/// the last head read past the group.
369/// * `head_dim == DECODE_GQA_PACK_HEAD_DIM` — see that constant.
370///
371/// The caller keeps the unpacked kernel when this is false. That is a
372/// documented route, not a fallback that hides an error: the unpacked kernel
373/// is the one every target has been serving.
374pub fn gqa_pack_shape_ok(num_q_heads: u32, num_kv_heads: u32, head_dim: u32) -> bool {
375 num_kv_heads > 0
376 && num_q_heads == num_kv_heads.saturating_mul(DECODE_GQA_PACK_WIDTH)
377 && head_dim == DECODE_GQA_PACK_HEAD_DIM
378}
379
380/// CTAs a packed launch puts on the device, against the unpacked count.
381///
382/// Exposed because it is the whole trade and it belongs next to the constant
383/// that sets it, not only in a comment: `(nkv, num_seqs)` against
384/// `(nq, num_seqs)` is a `DECODE_GQA_PACK_WIDTH`-fold cut in CTAs for an
385/// unchanged amount of work. At the GB10 shape (`nkv = 4`, 48 SMs) the packed
386/// grid stops under-filling the device at `num_seqs >= 12`; below that the
387/// launch leaves SMs idle, which is the cost side of the A/B this is gated
388/// behind.
389pub fn gqa_pack_ctas(num_kv_heads: u32, num_seqs: u32) -> u32 {
390 num_kv_heads.saturating_mul(num_seqs)
391}
392
393#[cfg(test)]
394#[path = "attn_splitk_tests.rs"]
395mod tests;