memra_engine/mla_ffi.rs
1//! FFI declarations + safe Engine wrappers for the MLA CUDA forward (`cu/mla_attn.cu`).
2//!
3//! House pattern (mmq_ffi / dsv4_ffi kind): C-ABI host launchers in the `libmemra_mmq.a`
4//! static lib, returning 0 ok / 10000+cudaError / 40000+contract; the stream rides as
5//! `*mut c_void` (`stream.cu_stream()`).
6//!
7//! The numeric truth for the dense core is `crate::mla` (the CPU f32 oracle), gated in
8//! `tests/mla_gpu_forward.rs`. The truth for the DSA k-pool indexer wrappers at the bottom of
9//! this file is `memra_reference::kpool_allowed_tokens`, gated in
10//! `tests/glm5_kpool_indexer_gpu.rs`.
11
12use crate::Engine;
13use cudarc::driver::{CudaSlice, DevicePtr, DevicePtrMut};
14use std::os::raw::c_void;
15
16/// Engagement counter for the MLA decode-split door (`MEMRA_MLA_DECODE_SPLIT`): counted at
17/// the arm's own call site, announced once per boot — the receipt a box A/B arm must show.
18pub static MLA_DECODE_SPLIT_DISPATCHES: std::sync::atomic::AtomicU64 =
19 std::sync::atomic::AtomicU64::new(0);
20
21/// `MEMRA_MLA_DECODE_SPLIT=1` (default OFF, read per call — rollback seam): the absorb /
22/// decompress launchers split each (token, head) block's output range across several blocks.
23/// PURE LAUNCH GEOMETRY: every output element keeps the same one-thread serial dot, so the
24/// bytes are identical for every split value (asserted in `tests/mla_decode_split_gpu.rs`);
25/// only occupancy changes — 64 blocks at t=1 on the glm5 geometry is single-digit-percent
26/// occupancy on the serving card class, the census's ~211 us/layer absorb+decompress pair.
27fn mla_decode_split_on() -> bool {
28 std::env::var("MEMRA_MLA_DECODE_SPLIT").as_deref() == Ok("1")
29}
30
31/// The split policy: engage only in the block-starved regime (fewer than 1024 (token, head)
32/// blocks — decode and short verify widths; prefill widths already fill the card and the TC
33/// prefill chain owns them anyway), aiming for ~1024 blocks while keeping at least 32 outputs
34/// per block. The OUTPUT BYTES ARE SPLIT-INVARIANT by construction, so this arithmetic is a
35/// throughput policy, never a numerics decision.
36fn mla_decode_split_for(blocks: usize, out_dim: usize) -> Option<i32> {
37 if !mla_decode_split_on() || blocks == 0 || blocks >= 1024 {
38 return None;
39 }
40 let want = 1024usize.div_ceil(blocks);
41 let cap = (out_dim / 32).max(1);
42 let split = want.min(cap);
43 if split <= 1 { None } else { Some(split as i32) }
44}
45
46fn mla_split_announce(kind: &str, t_q: usize, n_head: usize, split: i32) {
47 use std::sync::atomic::Ordering;
48 if MLA_DECODE_SPLIT_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
49 eprintln!(
50 "[mla-decode-split] engaged {kind} t={t_q} heads={n_head} split={split} \
51 (output-range split of the (token, head) blocks; MEMRA_MLA_DECODE_SPLIT=1)"
52 );
53 }
54}
55
56/// Engagement counter for the B200 decode arm (`MEMRA_B200_MLA_DECODE_ARM`), announced once
57/// per boot: the receipt a B200 box A/B arm must show.
58pub static MLA_B200_DECODE_ARM_DISPATCHES: std::sync::atomic::AtomicU64 =
59 std::sync::atomic::AtomicU64::new(0);
60
61/// `MEMRA_B200_MLA_DECODE_ARM=1` (default OFF, read per call: the rollback seam), compile-time
62/// gated to sm_100a builds (`cfg!(memra_sm100_tcgen05)`, set by build.rs for
63/// `MEMRA_CUDA_ARCH=100a`): on a 120a/90a/89 build this is `false` unconditionally, so naked
64/// non-B200 commands and the flag census see no behavior change from a var they cannot even
65/// engage. The arch guard is a compile-time fact here, not a per-call detection cost.
66///
67/// Owner order 2026-09-02: "hardly improve the decode on these cards, before the full 1M."
68/// This is a genuinely separate door from `MEMRA_MLA_DECODE_SPLIT` (glm5-decode-diet lever 4,
69/// rig-generic, target ~1024 blocks, PRO6000-tuned) rather than a rename of it, per the
70/// per-hardware-arm-selection law in CLAUDE.md: B200 SXM carries more SMs per device than the
71/// PRO6000 pair that door was tuned on, and this arm ALSO covers `attn_gathered`, which the
72/// generic split door never touched (no independent-output split existed for it before this
73/// lane; see `memra_mla_attn_gathered_split_kernel` in cu/mla_attn.cu).
74fn mla_b200_decode_arm_on() -> bool {
75 cfg!(memra_sm100_tcgen05) && std::env::var("MEMRA_B200_MLA_DECODE_ARM").as_deref() == Ok("1")
76}
77
78/// The three kernels the B200 arm covers. The gate bin (`mla_decode_arm_gate.rs`) walks this
79/// same enum and the same table below, so its regression check and the serving policy cannot
80/// disagree about which split a t_q gets.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum MlaB200Kernel {
83 AbsorbQ,
84 DecompressV,
85 AttnGathered,
86}
87
88impl MlaB200Kernel {
89 pub const ALL: [MlaB200Kernel; 3] = [
90 MlaB200Kernel::AbsorbQ,
91 MlaB200Kernel::DecompressV,
92 MlaB200Kernel::AttnGathered,
93 ];
94
95 pub fn name(self) -> &'static str {
96 match self {
97 MlaB200Kernel::AbsorbQ => "absorb_q",
98 MlaB200Kernel::DecompressV => "decompress_v",
99 MlaB200Kernel::AttnGathered => "attn_gathered",
100 }
101 }
102}
103
104/// Widest query width the B200 arm keys on. Wider widths fall through untouched: the generic
105/// `MEMRA_MLA_DECODE_SPLIT` door if set, else the shipped kernels (t >= 16 reaches
106/// `MEMRA_MLA_TC_PREFILL` before either).
107pub const MLA_B200_ARM_T_MAX: usize = 8;
108
109/// The B200 arm's split tables, keyed on t_q (index = t_q in 1..=MLA_B200_ARM_T_MAX; index 0
110/// is unused and always 1). A cell of 1 means THE SHIPPED KERNEL: the wrapper falls through to
111/// the unsplit launcher and the split twin is never launched with split=1, so "shipped" is the
112/// shipped binary path, not a re-implementation of it. Any other cell is the output-range
113/// split factor handed to the bit-identical split twin.
114///
115/// Why a table and not a block-count target: the first cut of this door aimed at ~2048 blocks
116/// at every t_q <= 8 and the real box refuted that shape. Measured 2026-09-02 on the 2x B200
117/// SXM pair (sm_100a), `mla-decode-arm-gate` device 0, geometry nh=64 kv_rank=512 d_nope=256
118/// d_v=256 d_rope=0 n_slots=2048 pool_rows=32768, N=5, every arm BIT-IDENTICAL to shipped:
119///
120/// | kernel | t_q | shipped | arm | verdict |
121/// |---------------|-----|----------|---------------|---------------------------|
122/// | absorb_q | 1 | 81.8 us | split=4 49.1 | win |
123/// | decompress_v | 1 | 82.2 us | split=4 48.0 | win |
124/// | attn_gathered | 1 | 564.6 us | split=2 516.4 | win |
125/// | absorb_q | 4 | 150.3 us | split=4 133.2 | win |
126/// | decompress_v | 4 | 150.4 us | split=4 246.6 | REGRESSION, shipped wins |
127/// | attn_gathered | 4 | 665.3 us | split=2 822.7 | REGRESSION, shipped wins |
128///
129/// t_q=4..8 is the DFlash2 spec-verify shape the box serves, so a target-driven policy that
130/// splits there costs the spec route. The tables ship exactly what that run showed and nothing
131/// it did not: the measured winner at t_q=1 for all three kernels, absorb_q's measured split=4
132/// win at t_q=4, and the shipped kernel everywhere else (t_q=2,3,5..8 are unmeasured, and
133/// unmeasured behavior does not go on). The gate times every split in {1,2,4,8} at every t_q
134/// in {1,2,4,8} for all three kernels, prints the per-t winner table, and FAILS (`REGRESSION`,
135/// exit 1) when a cell of THESE tables is slower than shipped by more than
136/// `MLA_B200_ARM_REGRESSION_MARGIN`, so a box run either confirms the tables or names the cell
137/// to change. Cite the box run in this comment when editing a cell.
138pub const MLA_B200_ABSORB_Q_SPLIT: [i32; MLA_B200_ARM_T_MAX + 1] = [1, 4, 1, 1, 4, 1, 1, 1, 1];
139pub const MLA_B200_DECOMPRESS_V_SPLIT: [i32; MLA_B200_ARM_T_MAX + 1] = [1, 4, 1, 1, 1, 1, 1, 1, 1];
140pub const MLA_B200_ATTN_GATHERED_SPLIT: [i32; MLA_B200_ARM_T_MAX + 1] = [1, 2, 1, 1, 1, 1, 1, 1, 1];
141
142/// The gate's regression bar: at every measured t_q the table's arm may not be slower than
143/// shipped by more than 5% (arm/shipped above this ratio fails `mla-decode-arm-gate`).
144pub const MLA_B200_ARM_REGRESSION_MARGIN: f64 = 1.05;
145
146/// Table lookup, independent of the door: 1 (shipped) outside 1..=MLA_B200_ARM_T_MAX. Pure,
147/// so the gate bin can read the table on any build, including the 120a builds where the door
148/// itself cannot engage.
149pub fn mla_b200_arm_table_split(kernel: MlaB200Kernel, t_q: usize) -> i32 {
150 if t_q == 0 || t_q > MLA_B200_ARM_T_MAX {
151 return 1;
152 }
153 match kernel {
154 MlaB200Kernel::AbsorbQ => MLA_B200_ABSORB_Q_SPLIT[t_q],
155 MlaB200Kernel::DecompressV => MLA_B200_DECOMPRESS_V_SPLIT[t_q],
156 MlaB200Kernel::AttnGathered => MLA_B200_ATTN_GATHERED_SPLIT[t_q],
157 }
158}
159
160/// The serving policy: door on, table cell above 1, and the cell legal for this geometry (the
161/// split twins need `split <= out_dim`; this keeps at least 32 outputs per block, the same
162/// floor as the generic door). A cell the geometry cannot honour falls through to the shipped
163/// kernel rather than clamping to a split the box never measured. The tables were measured on
164/// the glm5 geometry (kv_rank=512, d_v=256); `None` here means "shipped path", and the caller
165/// falls through in order to the generic split door, then the unsplit launcher.
166fn mla_b200_split_for(kernel: MlaB200Kernel, t_q: usize, out_dim: usize) -> Option<i32> {
167 if !mla_b200_decode_arm_on() {
168 return None;
169 }
170 let split = mla_b200_arm_table_split(kernel, t_q);
171 let cap = (out_dim / 32).max(1) as i32;
172 if split <= 1 || split > cap {
173 None
174 } else {
175 Some(split)
176 }
177}
178
179fn mla_b200_split_announce(kind: &str, t_q: usize, n_head: usize, split: i32) {
180 use std::sync::atomic::Ordering;
181 if MLA_B200_DECODE_ARM_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
182 eprintln!(
183 "[mla-b200-decode-arm] engaged {kind} t={t_q} heads={n_head} split={split} \
184 (sm_100a output-range split; MEMRA_B200_MLA_DECODE_ARM=1)"
185 );
186 }
187}
188
189/// Engagement counter for the DSA decode door (`MEMRA_B200_DSA_DECODE`), announced once per
190/// boot per arm: the receipt a B200 box A/B has to show.
191pub static MLA_DSA_DECODE_DISPATCHES: std::sync::atomic::AtomicU64 =
192 std::sync::atomic::AtomicU64::new(0);
193
194/// `MEMRA_B200_DSA_DECODE` (default OFF = 0, read per call: the rollback seam), compile-time
195/// gated to sm_100a builds exactly like its sibling `MEMRA_B200_MLA_DECODE_ARM`, so a
196/// 120a/90a/89 build sees no behavior change from a var it cannot engage.
197///
198/// THE DOOR IS A LEVEL, not a boolean, and the level is the numeric-class boundary:
199///
200/// * `0` (default) - nothing engages. Every kernel is the shipped one.
201/// * `1` - the BIT-IDENTICAL arms only: `memra_mla_attn_gathered_dsa_kernel` (same fold, same
202/// lane stride, same shuffle tree; what changes is that each tile's KV rows are staged once
203/// into shared memory with float4 loads and serve BOTH the score dot and the PV accumulate,
204/// and that the 8 tile exponentials are hoisted into registers instead of being recomputed
205/// 3x per thread per tile) and `memra_mla_kpool_score_dsa_kernel` (head-blocked decode
206/// scorer; c-ascending dot, h-ascending mix, all six rounding steps spelled with explicit
207/// intrinsics). Both are asserted bytewise by `dsa-decode-gate`, not merely argued.
208/// * `2` - additionally admits the WARP-ONLINE gathered arm
209/// (`memra_mla_dsa_attn_warp_kernel` + `memra_mla_dsa_attn_combine_kernel`), numeric class
210/// **`dsa-warp-online-f32`**: one warp owns one (token, head, slot-chunk) and holds the whole
211/// kv_rank-wide accumulator in registers, so every KV element is read from memory ONCE and
212/// consumed twice from registers, there is not a single `__syncthreads`, and the two `expf`
213/// per slot replace ~196k per warp per layer. It folds PER SLOT and merges `chunks` partials,
214/// where the shipped kernel folds in 8-slot tiles: same sum in real arithmetic, different
215/// rounding, so `dsa-decode-gate` holds it to an ARGMAX gate plus a maxdiff/max-relative bound
216/// on real-shaped inputs and never to bit identity. It exists because at t_q=1 the gathered
217/// attention has exactly 64 independent (token, head) outputs for 148 SMs and the slot axis is
218/// the only one that buys parallelism without duplicating the walk. ADMISSIBLE ONLY AT
219/// `t_q <= MLA_DSA_NAMED_CLASS_T_MAX` = 1 (plain decode): the 2026-09-03 B200 run saw this
220/// class move 1 of 256 latent-row argmaxes at kv=131072 / t_q=4, and t_q=4..8 is the DFlash2
221/// spec-verify shape where a moved argmax is a moved draft acceptance. Enforced by
222/// `mla_dsa_attn_arm_effective`, not by table convention.
223///
224/// Rollback seam: unset the var (or set 0). Both arms are read per call, so a rollback is the
225/// next request, not a restart.
226fn mla_dsa_decode_level() -> u32 {
227 if !cfg!(memra_sm100_tcgen05) {
228 return 0;
229 }
230 match std::env::var("MEMRA_B200_DSA_DECODE").as_deref() {
231 Ok("1") => 1,
232 Ok("2") => 2,
233 _ => 0,
234 }
235}
236
237/// Widest query width the DSA decode door keys on. Wider widths fall through untouched: the
238/// `MEMRA_B200_MLA_DECODE_ARM` split door if set, then the shipped kernels (t >= 16 reaches
239/// `MEMRA_MLA_TC_PREFILL` before either).
240pub const MLA_DSA_ARM_T_MAX: usize = 8;
241
242/// Which gathered-attention arm the door selects, keyed on t_q (index = t_q in
243/// 1..=MLA_DSA_ARM_T_MAX; index 0 unused):
244///
245/// * `0` - the SHIPPED kernel (`memra_mla_attn_gathered_f32`). The door does nothing here.
246/// * `1` - the single-pass BIT-IDENTICAL kernel (`memra_mla_attn_gathered_dsa_f32`).
247/// * `n >= 2` - the WARP-ONLINE arm with `n` slot chunks, numeric class
248/// `dsa-warp-online-f32`. Level 2 only.
249///
250/// B200-MEASURED, 2026-09-03, and the widths are not free: read the width rule below before
251/// editing a cell. `dsa-decode-gate` on the 2x B200 SXM pair (sm_100a), device 0, N=5
252/// interleaved, engine commit f3a0091cd; banked log
253/// `darklanes:research/glm5-b200-20260902/box/gates/gate-dsa-decode.txt`. Means in us, and the
254/// gathered stage is depth-flat so the three contexts agree to a few percent:
255///
256/// | t_q | context | shipped | single-pass (1) | c=4 | c=8 | c=16 | **c=32** |
257/// |---|---|---|---|---|---|---|---|
258/// | 1 | 128k | 556.3 | 573.1 | 272.9 | 136.9 | 80.5 | **57.1** |
259/// | 1 | 256k | 553.3 | 572.4 | 273.3 | 136.7 | 79.7 | **54.8** |
260/// | 1 | 1M | 552.1 | 571.5 | 273.1 | 139.0 | 79.9 | **54.3** |
261/// | 4 | 128k | 641.3 | **618.8** | 276.9 | 156.0 | 159.6 | 131.3 |
262/// | 4 | 256k | 663.1 | **616.6** | 277.1 | 154.7 | 158.9 | 131.8 |
263/// | 4 | 1M | 667.6 | **618.5** | 277.5 | 156.5 | 160.1 | 132.3 |
264///
265/// THE WIDTH RULE, and it is a correctness rule, not a tuning one. The named class
266/// (`dsa-warp-online-f32`, arm >= 2) is admissible ONLY at `t_q <= MLA_DSA_NAMED_CLASS_T_MAX`
267/// = 1, i.e. plain decode. The same box run that produced the table above ALSO recorded the
268/// class moving an argmax: at `kv=131072, t_q=4` every swept chunk count (4, 8, 16, 32) moved
269/// **1 of 256** latent rows, maxdiff ~1.7e-6. It was argmax-clean at t_q=1 in every measured
270/// cell (0 of 64, three contexts on the box plus five on the 5090) and clean at t_q=4 at 256k
271/// and 1M, but "clean in the cells we measured" is not a proof, and t_q=4..8 is the DFlash2
272/// SPEC-VERIFY shape: a moved argmax there is a moved draft acceptance. So the spec-verify
273/// batch never sees the named class. `mla_dsa_attn_arm_effective` enforces this in code, not by
274/// table convention: a cell >= 2 at any width above the rule is demoted to 0 (the shipped
275/// kernel, the always-safe path), never silently run and never quietly promoted to the
276/// single-pass arm at a width where nobody measured it.
277///
278/// The cells therefore ship exactly what the box measured, under that rule:
279///
280/// * `t_q=1` -> **32**. The fastest arm at every context (54.3-57.1 us, a 10.2x on the shipped
281/// 552.1 us at 1M) and argmax-clean at every context. 32 beats 16 by ~1.45x here where it lost
282/// to 16 on the 5090 -- 148 SMs want `64 * 32` = 2048 warps, an 82-SM laptop part does not.
283/// That disagreement is the per-hardware-arm-selection law working as intended.
284/// * `t_q=4` -> **1**, the BIT-IDENTICAL single-pass kernel. On the B200 it is a 3.5-7.4% WIN
285/// (618.5 vs 667.6 us at 1M), the opposite sign from the 5090, where it lost by 30% and this
286/// table shipped 0. Same code, different machine: on 148 SMs the shared-memory staging pays
287/// for itself where on 82 it did not. Bit-identical, so this cell carries no numeric risk at
288/// the spec-verify width at all. Banked evidence covers 128k/256k/1M; the two shallow contexts
289/// were not in the log this cell was set from, and the kernel is depth-flat.
290/// * `t_q=2,3,5..8` -> **0** (shipped). Unmeasured, and unmeasured behavior does not go on.
291///
292/// The door is default OFF and arm >= 2 additionally needs level 2, so nothing here reaches a
293/// request without two deliberate acts. `dsa-decode-gate` FAILS with a `REGRESSION` line if a
294/// cell is slower than shipped by more than `MLA_DSA_REGRESSION_MARGIN` on a later run, so the
295/// next box run either confirms these cells or names the one to change. Cite the run here when
296/// a cell moves.
297pub const MLA_DSA_ATTN_ARM: [i32; MLA_DSA_ARM_T_MAX + 1] = [0, 32, 0, 0, 1, 0, 0, 0, 0];
298
299/// Widest query width at which the NAMED numeric class (`dsa-warp-online-f32`, arm >= 2) may be
300/// selected. 1: plain decode only. Above it the door takes a bit-identical arm or the shipped
301/// kernel, so the DFlash2 spec-verify batch (t_q=4..8) never runs a rounding program that could
302/// move a draft acceptance. Set by the 2026-09-03 B200 run, which observed the class move 1 of
303/// 256 latent-row argmaxes at kv=131072 / t_q=4 for every swept chunk count. Raising this needs
304/// its own argmax evidence at the widths it opens, not an inference from t_q=1.
305pub const MLA_DSA_NAMED_CLASS_T_MAX: usize = 1;
306
307/// Chunk counts `dsa-decode-gate` sweeps for the warp-online arm. The warp arm puts
308/// `t_q * n_head * chunks` WARPS on the die, so chunks is the whole occupancy knob at t_q=1
309/// (64 pairs alone is 8 CTAs of 8 warps); 64 is the kernel's ceiling (`MLA_DSA_MAX_CHUNKS`).
310pub const MLA_DSA_ATTN_CHUNK_SWEEP: [i32; 4] = [4, 8, 16, 32];
311
312/// The decode scorer engages only from this pool count up. Below it the block count
313/// (`n_pools / (128 * 2)`) cannot fill the die and the shipped dispatch's own measured
314/// crossover already sends small-pool decode to the reference kernel, which wins there
315/// (cu/mla_attn.cu, MLA_KPOOL_SMALL_TILE_MIN_POOLS note). 4096 pools = 16 blocks = 16k context
316/// at the shipped pool size 4.
317pub const MLA_DSA_SCORE_MIN_POOLS: usize = 4096;
318
319/// The gate's regression bar, shared with the sibling arm's: an arm may not be slower than the
320/// kernel it replaces by more than 5%.
321pub const MLA_DSA_REGRESSION_MARGIN: f64 = 1.05;
322
323/// The gathered-attention arm code at this width (see [`MLA_DSA_ATTN_ARM`]). Pure, so the gate
324/// can read the policy on any build including the 120a ones where the door is dead.
325pub fn mla_dsa_attn_arm(t_q: usize) -> i32 {
326 if t_q == 0 || t_q > MLA_DSA_ARM_T_MAX {
327 return 0;
328 }
329 MLA_DSA_ATTN_ARM[t_q]
330}
331
332/// The arm the door may actually run at this width: the table cell, with the named-class width
333/// rule enforced in CODE rather than by table convention. A cell >= 2 above
334/// `MLA_DSA_NAMED_CLASS_T_MAX` is demoted to 0 (the shipped kernel), not to the single-pass arm
335/// — a width nobody measured gets the path that cannot be wrong, not the path that happens to
336/// be bit-identical. The gate reads this same function, so an edit that violates the rule shows
337/// up as the gate timing a shipped cell, never as a silently-served numeric class.
338pub fn mla_dsa_attn_arm_effective(t_q: usize) -> i32 {
339 let arm = mla_dsa_attn_arm(t_q);
340 if arm >= 2 && t_q > MLA_DSA_NAMED_CLASS_T_MAX {
341 return 0;
342 }
343 arm
344}
345
346/// Geometry refusals from the DSA launchers: the door has nothing for this shape, so the
347/// caller falls through to the shipped kernel instead of failing the request. Every other
348/// non-zero rc (a real cudaError included) still goes through `ck` and surfaces.
349/// Engagement counter for the k-pool SELECT door (`MEMRA_B200_DSA_SELECT`), announced once per
350/// boot: the receipt a B200 A/B has to show.
351pub static MLA_DSA_SELECT_DISPATCHES: std::sync::atomic::AtomicU64 =
352 std::sync::atomic::AtomicU64::new(0);
353
354/// `MEMRA_B200_DSA_SELECT=1` (default OFF, read per call: the rollback seam), compile-time gated
355/// to sm_100a builds exactly like its two siblings, so a 120a/90a/89 build sees no behaviour
356/// change from a var it cannot engage.
357///
358/// WHAT IT REPLACES. `memra_mla_kpool_select_kernel` grids `t_q` blocks, so plain decode runs it
359/// on ONE CTA -- 0.68% of a 148-SM die -- sweeping `n_pools` up to ten times (8 MSB-first radix
360/// passes, an optional unique-resolution scan, then the membership count and the emit). It is
361/// depth-LINEAR in `n_pools = t_kv / pool` and it is what the `MEMRA_B200_DSA_DECODE` lane's
362/// scorer fix stopped hiding.
363///
364/// THE CLASS IS EXACT, not banded, and that is a construction rather than a hope. The emitted
365/// plane is a pure function of ONE 64-bit number: the `select_k`-th smallest order key
366/// `(desc32(score) << 32) | pool_index`. That key is a strictly decreasing injection composed
367/// with a unique index, so keys are DISTINCT and "the k-th smallest" is unambiguous; reproducing
368/// it bit-for-bit reproduces the selection bit-for-bit. The parallel pipeline computes the same
369/// key and runs the same `key(p) <= thr` test, so this is a launch-geometry change with an exact
370/// answer. `dsa-select-gate` asserts the `idx` plane byte-identical to the shipped kernel and
371/// carries a RED ARM that must fail first.
372fn mla_dsa_select_on() -> bool {
373 cfg!(memra_sm100_tcgen05) && std::env::var("MEMRA_B200_DSA_SELECT").as_deref() == Ok("1")
374}
375
376/// The select door engages only from this pool count up, and the value is MEASURED, not
377/// inherited. The first `dsa-select-gate` run (RTX 5090, N=3 interleaved -- an exactness rig, so
378/// direction only) put the crossover between 32768 and 65536 pools, i.e. between 128k and 256k
379/// of context:
380///
381/// | n_pools | context | shipped | parallel | ratio |
382/// |---|---|---|---|---|
383/// | 8192 | 32k | 30.3 us (t=4) | 140.9 us | **0.22x** |
384/// | 32768 | 128k | 82.9 us (t=1) | 92.7 us | 0.89x |
385/// | **65536** | **256k** | **150.9 us (t=1)** | **99.5 us** | **1.52x** |
386/// | 262144 | 1M | 554.5 us (t=1) | 174.7 us | **3.17x** |
387///
388/// The pipeline is SIX launches where the shipped kernel is one, so below the crossover that
389/// fixed cost is simply larger than the sweep it removes. That is the honest reason this is a
390/// DEPTH door and not a decode door, and it is why the constant is not a round number copied
391/// from a sibling: an initial guess of 4096 (inherited from `MLA_DSA_SCORE_MIN_POOLS`) would
392/// have shipped a measured 4.6x REGRESSION at 32k, and the gate's regression bar is what caught
393/// it. 65536 is the first swept cell that wins at BOTH measured widths (1.52x at t_q=1, 1.07x
394/// at t_q=4). A B200 run confirms it or names the cell to change.
395pub const MLA_DSA_SELECT_MIN_POOLS: usize = 65_536;
396
397/// Widest query width the select door keys on: decode and the spec-verify batch. Wider widths
398/// already have `t_q` CTAs of parallelism and fall through to the shipped kernel untouched.
399pub const MLA_DSA_SELECT_T_MAX: usize = 8;
400
401/// Whether the serving policy engages the parallel selector at this shape. Pure, so the gate
402/// reads the same predicate the wrapper does and the two cannot drift apart.
403pub fn mla_dsa_select_engages(t_q: usize, n_pools: usize) -> bool {
404 (1..=MLA_DSA_SELECT_T_MAX).contains(&t_q) && n_pools >= MLA_DSA_SELECT_MIN_POOLS
405}
406
407fn mla_dsa_select_announce(t_q: usize, n_pools: usize, n_ctas: i32) {
408 use std::sync::atomic::Ordering;
409 if MLA_DSA_SELECT_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
410 eprintln!(
411 "[mla-b200-dsa-select] engaged kpool_select t={t_q} pools={n_pools} ctas={n_ctas} \
412 class=exact (sm_100a; MEMRA_B200_DSA_SELECT=1)"
413 );
414 }
415}
416
417fn mla_dsa_geometry_refusal(rc: i32) -> bool {
418 matches!(rc, 40020 | 40021 | 40023)
419}
420
421fn mla_dsa_announce(kind: &str, t_q: usize, detail: &str) {
422 use std::sync::atomic::Ordering;
423 if MLA_DSA_DECODE_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
424 eprintln!(
425 "[mla-b200-dsa-decode] engaged {kind} t={t_q} {detail} \
426 (sm_100a; MEMRA_B200_DSA_DECODE)"
427 );
428 }
429}
430
431unsafe extern "C" {
432 pub fn memra_mla_rope_interleaved_f32(
433 x: *mut f32,
434 n_pos: i32,
435 n_vec: i32,
436 d_rope: i32,
437 positions: *const i32,
438 base: f32,
439 stream: *mut c_void,
440 ) -> i32;
441 pub fn memra_mla_split_latent_f32(
442 kv: *const f32,
443 c_kv: *mut f32,
444 k_pe: *mut f32,
445 t: i32,
446 kv_rank: i32,
447 d_rope: i32,
448 stream: *mut c_void,
449 ) -> i32;
450 pub fn memra_mla_append_latent_f32(
451 cache: *mut f32,
452 c_kv: *const f32,
453 k_pe: *const f32,
454 slot: i32,
455 t: i32,
456 kv_rank: i32,
457 d_rope: i32,
458 stream: *mut c_void,
459 ) -> i32;
460 pub fn memra_mla_absorb_q_f32(
461 q_nope: *const f32,
462 wk_b: *const f32,
463 q_lat: *mut f32,
464 t_q: i32,
465 n_head: i32,
466 d_nope: i32,
467 kv_rank: i32,
468 stream: *mut c_void,
469 ) -> i32;
470 pub fn memra_mla_decompress_v_f32(
471 o_lat: *const f32,
472 wv_b: *const f32,
473 out: *mut f32,
474 t_q: i32,
475 n_head: i32,
476 d_v: i32,
477 kv_rank: i32,
478 stream: *mut c_void,
479 ) -> i32;
480 /// Decode-split twin of `memra_mla_absorb_q_f32` (MEMRA_MLA_DECODE_SPLIT): the same
481 /// per-output serial dot, its output range split across `split` blocks — bit-identical
482 /// by construction, gated in `tests/mla_decode_split_gpu.rs`.
483 #[allow(clippy::too_many_arguments)]
484 pub fn memra_mla_absorb_q_split_f32(
485 q_nope: *const f32,
486 wk_b: *const f32,
487 q_lat: *mut f32,
488 t_q: i32,
489 n_head: i32,
490 d_nope: i32,
491 kv_rank: i32,
492 split: i32,
493 stream: *mut c_void,
494 ) -> i32;
495 /// Decode-split twin of `memra_mla_decompress_v_f32` (see above).
496 #[allow(clippy::too_many_arguments)]
497 pub fn memra_mla_decompress_v_split_f32(
498 o_lat: *const f32,
499 wv_b: *const f32,
500 out: *mut f32,
501 t_q: i32,
502 n_head: i32,
503 d_v: i32,
504 kv_rank: i32,
505 split: i32,
506 stream: *mut c_void,
507 ) -> i32;
508 pub fn memra_mla_attn_absorbed_f32(
509 q_lat: *const f32,
510 q_pe: *const f32,
511 cache: *const f32,
512 o_lat: *mut f32,
513 n_head: i32,
514 kv_rank: i32,
515 d_rope: i32,
516 t_q: i32,
517 t_kv: i32,
518 scale: f32,
519 stream: *mut c_void,
520 ) -> i32;
521 pub fn memra_mla_index_append_ring_f32(
522 plane: *mut f32,
523 a: *const f32,
524 b: *const f32,
525 slot: i32,
526 t: i32,
527 wa: i32,
528 wb: i32,
529 rows: i32,
530 stream: *mut c_void,
531 ) -> i32;
532 pub fn memra_mla_kpool_pool_keys_f32(
533 state: *const f32,
534 ape: *const f32,
535 pool_keys: *mut f32,
536 pool_begin: i32,
537 n_pools: i32,
538 pool: i32,
539 d: i32,
540 state_rows: i32,
541 stream: *mut c_void,
542 ) -> i32;
543 pub fn memra_mla_kpool_score_f32(
544 q: *const f32,
545 pool_keys: *const f32,
546 hw: *const f32,
547 score: *mut f32,
548 t_q: i32,
549 heads: i32,
550 d: i32,
551 n_pools: i32,
552 pool: i32,
553 first_pos: i32,
554 qk_scale: f32,
555 head_scale: f32,
556 stream: *mut c_void,
557 ) -> i32;
558 pub fn memra_mla_kpool_score_ref_f32(
559 q: *const f32,
560 pool_keys: *const f32,
561 hw: *const f32,
562 score: *mut f32,
563 t_q: i32,
564 heads: i32,
565 d: i32,
566 n_pools: i32,
567 pool: i32,
568 first_pos: i32,
569 qk_scale: f32,
570 head_scale: f32,
571 stream: *mut c_void,
572 ) -> i32;
573 /// Ints of scratch one query needs for the parallel selector, given its CTA count.
574 pub fn memra_mla_kpool_select_ws_ints(n_ctas: i32) -> i64;
575 /// CTA count the parallel selector launches per query. The host sizes the workspace from
576 /// this same entry point, so a mismatch is impossible by construction.
577 pub fn memra_mla_kpool_select_ctas(n_pools: i32) -> i32;
578 /// Exact multi-CTA k-pool selection (`MEMRA_B200_DSA_SELECT`): same threshold key, same
579 /// membership test, same emit order, byte-identical `idx`.
580 #[allow(clippy::too_many_arguments)]
581 pub fn memra_mla_kpool_select_dsa_f32(
582 score: *const f32,
583 idx: *mut i32,
584 ws: *mut i32,
585 t_q: i32,
586 n_pools: i32,
587 pool: i32,
588 select_k: i32,
589 width: i32,
590 first_pos: i32,
591 always_tail: i32,
592 stream: *mut c_void,
593 ) -> i32;
594 /// RED ARM for `dsa-select-gate`, never a serving path: the exact pipeline with the resolved
595 /// threshold deliberately bumped, so the gate can prove its byte comparison actually fails
596 /// on a wrong selection before it is allowed to pass the real kernel.
597 #[allow(clippy::too_many_arguments)]
598 pub fn memra_mla_kpool_select_dsa_redarm_f32(
599 score: *const f32,
600 idx: *mut i32,
601 ws: *mut i32,
602 t_q: i32,
603 n_pools: i32,
604 pool: i32,
605 select_k: i32,
606 width: i32,
607 first_pos: i32,
608 always_tail: i32,
609 bump: i32,
610 stream: *mut c_void,
611 ) -> i32;
612 pub fn memra_mla_kpool_select_f32(
613 score: *const f32,
614 idx: *mut i32,
615 t_q: i32,
616 n_pools: i32,
617 pool: i32,
618 select_k: i32,
619 width: i32,
620 first_pos: i32,
621 always_tail: i32,
622 stream: *mut c_void,
623 ) -> i32;
624 pub fn memra_mla_kpool_select_ref_f32(
625 score: *const f32,
626 idx: *mut i32,
627 t_q: i32,
628 n_pools: i32,
629 pool: i32,
630 select_k: i32,
631 width: i32,
632 first_pos: i32,
633 always_tail: i32,
634 stream: *mut c_void,
635 ) -> i32;
636 pub fn memra_mla_attn_gathered_f32(
637 q_lat: *const f32,
638 q_pe: *const f32,
639 cache: *const f32,
640 idx: *const i32,
641 o_lat: *mut f32,
642 n_head: i32,
643 kv_rank: i32,
644 d_rope: i32,
645 t_q: i32,
646 n_slots: i32,
647 scale: f32,
648 stream: *mut c_void,
649 ) -> i32;
650 /// B200 decode-arm twin of `memra_mla_attn_gathered_f32` (MEMRA_B200_MLA_DECODE_ARM): same
651 /// per-l accumulate chain, its output range [0, kv_rank) split across `split` blocks; the
652 /// shared score/softmax tile walk (m, dsum) is recomputed IN FULL, unchanged, by every
653 /// split block — bit-identical by construction, gated in `mla_decode_arm_gate.rs`.
654 #[allow(clippy::too_many_arguments)]
655 /// Single-pass bit-identical rewrite of `memra_mla_attn_gathered_f32`
656 /// (`MEMRA_B200_DSA_DECODE>=1`): each tile's KV rows staged once into shared memory with
657 /// float4 loads and read back for BOTH the score dot and the PV accumulate, the 8 tile
658 /// exponentials hoisted into registers. Same grid, same fold, same bits. Returns 40020
659 /// (width not a multiple of 4) or 40021 (staging over the smem cap) for a geometry it
660 /// refuses, and the caller falls through to the shipped kernel.
661 pub fn memra_mla_attn_gathered_dsa_f32(
662 q_lat: *const f32,
663 q_pe: *const f32,
664 cache: *const f32,
665 idx: *const i32,
666 o_lat: *mut f32,
667 n_head: i32,
668 kv_rank: i32,
669 d_rope: i32,
670 t_q: i32,
671 n_slots: i32,
672 scale: f32,
673 stream: *mut c_void,
674 ) -> i32;
675 /// Slot-per-chunk span the partial kernel walks. The host MUST size the workspace and
676 /// launch from this, never from its own division, so the two cannot disagree.
677 pub fn memra_mla_dsa_attn_chunk_span(n_slots: i32, chunks: i32) -> i32;
678 /// Warp-online slot-split gathered attention, numeric class `dsa-warp-online-f32`
679 /// (`MEMRA_B200_DSA_DECODE=2`). `part_m` / `part_d` hold `t_q * n_head * chunks` floats
680 /// each; `part_acc` holds `t_q * n_head * chunks * kv_rank`. Returns 40023 for a
681 /// (kv_rank, d_rope) with no template instantiation, and the caller takes the shipped path.
682 pub fn memra_mla_dsa_attn_split_f32(
683 q_lat: *const f32,
684 q_pe: *const f32,
685 cache: *const f32,
686 idx: *const i32,
687 o_lat: *mut f32,
688 part_m: *mut f32,
689 part_d: *mut f32,
690 part_acc: *mut f32,
691 n_head: i32,
692 kv_rank: i32,
693 d_rope: i32,
694 t_q: i32,
695 n_slots: i32,
696 chunks: i32,
697 scale: f32,
698 stream: *mut c_void,
699 ) -> i32;
700 /// Head-blocked decode pool scorer (`MEMRA_B200_DSA_DECODE>=1`), bit-identical to
701 /// `memra_mla_kpool_score_ref_f32`. Returns 40023 when this (heads, d) has no
702 /// instantiation, and the caller falls through to the shipped dispatch.
703 pub fn memra_mla_kpool_score_dsa_f32(
704 q: *const f32,
705 pool_keys: *const f32,
706 hw: *const f32,
707 score: *mut f32,
708 t_q: i32,
709 heads: i32,
710 d: i32,
711 n_pools: i32,
712 pool: i32,
713 first_pos: i32,
714 qk_scale: f32,
715 head_scale: f32,
716 stream: *mut c_void,
717 ) -> i32;
718 pub fn memra_mla_attn_gathered_split_f32(
719 q_lat: *const f32,
720 q_pe: *const f32,
721 cache: *const f32,
722 idx: *const i32,
723 o_lat: *mut f32,
724 n_head: i32,
725 kv_rank: i32,
726 d_rope: i32,
727 t_q: i32,
728 n_slots: i32,
729 scale: f32,
730 split: i32,
731 stream: *mut c_void,
732 ) -> i32;
733 /// Strided-batched BF16 tensor-core GEMM (cu/f16_prefill.cu): per batch b,
734 /// `y_b[m, n] = x_b[m, k] @ w_b[n, k]^T`, f32 accumulate, y f32 or bf16 by flag.
735 /// The MEMRA_MLA_TC_PREFILL absorb/decompress engine (one launch replaces the
736 /// per-position absorb_q / decompress_v kernels at prefill widths).
737 fn memra_bf16_gemm_sb(
738 w_bf16: *const c_void,
739 x_bf16: *const c_void,
740 y: *mut c_void,
741 m: i32,
742 n: i32,
743 k: i32,
744 x_rs: i64,
745 x_bs: i64,
746 y_rs: i64,
747 y_bs: i64,
748 batch: i32,
749 y_is_bf16: i32,
750 ws: *mut c_void,
751 ws_bytes: usize,
752 stream: *mut c_void,
753 ) -> i32;
754}
755
756type Res<T> = Result<T, Box<dyn std::error::Error>>;
757
758/// Turn a launcher's status band into a named error. Every MLA launch goes through this —
759/// a silently-ignored non-zero status is how a contract violation becomes garbage activations.
760fn ck(what: &str, rc: i32) -> Res<()> {
761 if rc == 0 {
762 return Ok(());
763 }
764 let detail = match rc {
765 40001 => " (d_rope must be even — interleaved rope rotates (2j, 2j+1) pairs)",
766 40002 => " (kv_rank exceeds the kernel's MLA_MAX_RANK shared-memory ceiling)",
767 40003 => " (d_rope exceeds the kernel's MLA_MAX_ROPE ceiling)",
768 40004 => " (t_q > t_kv — queries must be a suffix of the latent cache)",
769 40010 => " (k-pool size out of range — 1..=MLA_MAX_POOL)",
770 40011 => " (indexer head count out of range — 1..=1024, one thread per head)",
771 40012 => " (t_q * n_pools exceeds the grid.x contract)",
772 40017 => " (indexer head dim must be positive)",
773 40013 => {
774 " (always_select_tail=false: queries before the first complete pool would have an \
775 empty candidate set, which the memra-reference oracle refuses outright)"
776 }
777 40014 => " (index-list width is narrower than select_k * pool + pool - 1)",
778 40015 => " (empty gathered candidate list — a zero softmax denominator)",
779 40020 => " (latent row width is not a multiple of 4 — the DSA float4 staging needs it)",
780 40021 => " (DSA tile staging exceeds MLA_DSA_KV_SMEM_MAX)",
781 40022 => " (DSA slot-chunk count out of range — 1..=64)",
782 40023 => " (no DSA scorer instantiation for this (heads, d))",
783 r if (10000..20000).contains(&r) => " (cudaError)",
784 _ => "",
785 };
786 Err(format!("mla kernel `{what}` failed: rc {rc}{detail}").into())
787}
788
789impl Engine {
790 /// Interleaved ("NORM") RoPE in place over `x` laid out [n_pos][n_vec][d_rope].
791 /// `d_rope == 0` (NoPE, glm5_next) is a no-op — the caller must still not pass an empty
792 /// slice through a path that dereferences it, which is why the rope plane is skipped
793 /// entirely in the forward arm rather than launched with a zero extent.
794 pub fn mla_rope_interleaved(
795 &self,
796 x: &mut CudaSlice<f32>,
797 pos_d: &CudaSlice<i32>,
798 n_pos: usize,
799 n_vec: usize,
800 d_rope: usize,
801 base: f32,
802 ) -> Res<()> {
803 if d_rope == 0 {
804 return Ok(());
805 }
806 let s = self.stream();
807 unsafe {
808 ck(
809 "rope_interleaved",
810 memra_mla_rope_interleaved_f32(
811 x.device_ptr_mut(&s).0 as *mut f32,
812 n_pos as i32,
813 n_vec as i32,
814 d_rope as i32,
815 pos_d.device_ptr(&s).0 as *const i32,
816 base,
817 s.cu_stream() as *mut c_void,
818 ),
819 )
820 }
821 }
822
823 /// Split the `wkv_a` output rows [t][kv_rank + d_rope] into `c_kv` and `k_pe` planes.
824 pub fn mla_split_latent(
825 &self,
826 kv: &CudaSlice<f32>,
827 c_kv: &mut CudaSlice<f32>,
828 k_pe: &mut CudaSlice<f32>,
829 t: usize,
830 kv_rank: usize,
831 d_rope: usize,
832 ) -> Res<()> {
833 let s = self.stream();
834 unsafe {
835 ck(
836 "split_latent",
837 memra_mla_split_latent_f32(
838 kv.device_ptr(&s).0 as *const f32,
839 c_kv.device_ptr_mut(&s).0 as *mut f32,
840 k_pe.device_ptr_mut(&s).0 as *mut f32,
841 t as i32,
842 kv_rank as i32,
843 d_rope as i32,
844 s.cu_stream() as *mut c_void,
845 ),
846 )
847 }
848 }
849
850 /// Append `t` latent rows `[c_kv | k_pe]` to the cache plane starting at row `slot`.
851 #[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI/call contract; bundling into a struct is a refactor, not a lint fix
852 pub fn mla_append_latent(
853 &self,
854 cache: &mut CudaSlice<f32>,
855 c_kv: &CudaSlice<f32>,
856 k_pe: &CudaSlice<f32>,
857 slot: usize,
858 t: usize,
859 kv_rank: usize,
860 d_rope: usize,
861 ) -> Res<()> {
862 let s = self.stream();
863 unsafe {
864 ck(
865 "append_latent",
866 memra_mla_append_latent_f32(
867 cache.device_ptr_mut(&s).0 as *mut f32,
868 c_kv.device_ptr(&s).0 as *const f32,
869 k_pe.device_ptr(&s).0 as *const f32,
870 slot as i32,
871 t as i32,
872 kv_rank as i32,
873 d_rope as i32,
874 s.cu_stream() as *mut c_void,
875 ),
876 )
877 }
878 }
879
880 /// Absorb: `q_lat[i][h][:] = w_uk[h]ᵀ · q_nope[i][h][:]` (rank space).
881 #[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI/call contract; bundling into a struct is a refactor, not a lint fix
882 pub fn mla_absorb_q(
883 &self,
884 q_nope: &CudaSlice<f32>,
885 wk_b: &CudaSlice<f32>,
886 q_lat: &mut CudaSlice<f32>,
887 t_q: usize,
888 n_head: usize,
889 d_nope: usize,
890 kv_rank: usize,
891 ) -> Res<()> {
892 let s = self.stream();
893 // MEMRA_B200_MLA_DECODE_ARM door (checked first; split from the t_q-keyed table
894 // MLA_B200_ABSORB_Q_SPLIT, a 1 cell falls through to the doors below; the split twin is
895 // the same kernel the generic door launches, so this is only a policy pick).
896 if let Some(split) = mla_b200_split_for(MlaB200Kernel::AbsorbQ, t_q, kv_rank) {
897 mla_b200_split_announce("absorb_q", t_q, n_head, split);
898 return unsafe {
899 ck(
900 "absorb_q_split_b200",
901 memra_mla_absorb_q_split_f32(
902 q_nope.device_ptr(&s).0 as *const f32,
903 wk_b.device_ptr(&s).0 as *const f32,
904 q_lat.device_ptr_mut(&s).0 as *mut f32,
905 t_q as i32,
906 n_head as i32,
907 d_nope as i32,
908 kv_rank as i32,
909 split,
910 s.cu_stream() as *mut c_void,
911 ),
912 )
913 };
914 }
915 // MEMRA_MLA_DECODE_SPLIT door: same bytes at any split (see mla_decode_split_for).
916 if let Some(split) = mla_decode_split_for(t_q * n_head, kv_rank) {
917 mla_split_announce("absorb_q", t_q, n_head, split);
918 return unsafe {
919 ck(
920 "absorb_q_split",
921 memra_mla_absorb_q_split_f32(
922 q_nope.device_ptr(&s).0 as *const f32,
923 wk_b.device_ptr(&s).0 as *const f32,
924 q_lat.device_ptr_mut(&s).0 as *mut f32,
925 t_q as i32,
926 n_head as i32,
927 d_nope as i32,
928 kv_rank as i32,
929 split,
930 s.cu_stream() as *mut c_void,
931 ),
932 )
933 };
934 }
935 unsafe {
936 ck(
937 "absorb_q",
938 memra_mla_absorb_q_f32(
939 q_nope.device_ptr(&s).0 as *const f32,
940 wk_b.device_ptr(&s).0 as *const f32,
941 q_lat.device_ptr_mut(&s).0 as *mut f32,
942 t_q as i32,
943 n_head as i32,
944 d_nope as i32,
945 kv_rank as i32,
946 s.cu_stream() as *mut c_void,
947 ),
948 )
949 }
950 }
951
952 /// Decompress: `out[i][h][:] = w_uv[h] · o_lat[i][h][:]`.
953 #[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI/call contract; bundling into a struct is a refactor, not a lint fix
954 pub fn mla_decompress_v(
955 &self,
956 o_lat: &CudaSlice<f32>,
957 wv_b: &CudaSlice<f32>,
958 out: &mut CudaSlice<f32>,
959 t_q: usize,
960 n_head: usize,
961 d_v: usize,
962 kv_rank: usize,
963 ) -> Res<()> {
964 let s = self.stream();
965 // MEMRA_B200_MLA_DECODE_ARM door (checked first, table MLA_B200_DECOMPRESS_V_SPLIT; see
966 // mla_absorb_q above).
967 if let Some(split) = mla_b200_split_for(MlaB200Kernel::DecompressV, t_q, d_v) {
968 mla_b200_split_announce("decompress_v", t_q, n_head, split);
969 return unsafe {
970 ck(
971 "decompress_v_split_b200",
972 memra_mla_decompress_v_split_f32(
973 o_lat.device_ptr(&s).0 as *const f32,
974 wv_b.device_ptr(&s).0 as *const f32,
975 out.device_ptr_mut(&s).0 as *mut f32,
976 t_q as i32,
977 n_head as i32,
978 d_v as i32,
979 kv_rank as i32,
980 split,
981 s.cu_stream() as *mut c_void,
982 ),
983 )
984 };
985 }
986 // MEMRA_MLA_DECODE_SPLIT door: same bytes at any split (see mla_decode_split_for).
987 if let Some(split) = mla_decode_split_for(t_q * n_head, d_v) {
988 mla_split_announce("decompress_v", t_q, n_head, split);
989 return unsafe {
990 ck(
991 "decompress_v_split",
992 memra_mla_decompress_v_split_f32(
993 o_lat.device_ptr(&s).0 as *const f32,
994 wv_b.device_ptr(&s).0 as *const f32,
995 out.device_ptr_mut(&s).0 as *mut f32,
996 t_q as i32,
997 n_head as i32,
998 d_v as i32,
999 kv_rank as i32,
1000 split,
1001 s.cu_stream() as *mut c_void,
1002 ),
1003 )
1004 };
1005 }
1006 unsafe {
1007 ck(
1008 "decompress_v",
1009 memra_mla_decompress_v_f32(
1010 o_lat.device_ptr(&s).0 as *const f32,
1011 wv_b.device_ptr(&s).0 as *const f32,
1012 out.device_ptr_mut(&s).0 as *mut f32,
1013 t_q as i32,
1014 n_head as i32,
1015 d_v as i32,
1016 kv_rank as i32,
1017 s.cu_stream() as *mut c_void,
1018 ),
1019 )
1020 }
1021 }
1022
1023 /// Absorbed-form MQA attention over the latent cache. `q_pe` is ignored when
1024 /// `d_rope == 0`; callers on the NoPE path may pass any allocated slice.
1025 #[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI/call contract; bundling into a struct is a refactor, not a lint fix
1026 pub fn mla_attn_absorbed(
1027 &self,
1028 q_lat: &CudaSlice<f32>,
1029 q_pe: &CudaSlice<f32>,
1030 cache: &CudaSlice<f32>,
1031 o_lat: &mut CudaSlice<f32>,
1032 n_head: usize,
1033 kv_rank: usize,
1034 d_rope: usize,
1035 t_q: usize,
1036 t_kv: usize,
1037 scale: f32,
1038 ) -> Res<()> {
1039 let s = self.stream();
1040 unsafe {
1041 ck(
1042 "attn_absorbed",
1043 memra_mla_attn_absorbed_f32(
1044 q_lat.device_ptr(&s).0 as *const f32,
1045 q_pe.device_ptr(&s).0 as *const f32,
1046 cache.device_ptr(&s).0 as *const f32,
1047 o_lat.device_ptr_mut(&s).0 as *mut f32,
1048 n_head as i32,
1049 kv_rank as i32,
1050 d_rope as i32,
1051 t_q as i32,
1052 t_kv as i32,
1053 scale,
1054 s.cu_stream() as *mut c_void,
1055 ),
1056 )
1057 }
1058 }
1059}
1060
1061/// Safe wrappers for the DSA k-pool indexer (`cu/mla_attn.cu`, "DSA k-pool indexer" section).
1062/// Numeric truth is `memra_reference::kpool_allowed_tokens`; the gate is
1063/// `tests/glm5_kpool_indexer_gpu.rs`.
1064impl Engine {
1065 /// Collapse pools `[pool_begin, n_pools)` of `pool` cached indexer rows each into one key by a
1066 /// learned per-channel softmax over (gate score + positional embedding).
1067 /// `state` rows are `[k | gate]`, `2 * d` wide; `ape` is `[pool][d]` row-major.
1068 ///
1069 /// `pool_begin` is the RESIDENCY seam: a pool's key depends only on its own `pool` state rows
1070 /// (append-only, never rewritten) and the constant `ape`, so it is final the instant the
1071 /// pool's last row lands. Pools below `pool_begin` are already resident and are left alone —
1072 /// bit-identically to what rebuilding them would produce. Pass 0 for a full rebuild.
1073 ///
1074 /// `state_rows` is the indexer plane's TAIL-RING size in rows (0 = flat, absolute
1075 /// addressing). It is always a multiple of `pool`, so a pool's members stay contiguous
1076 /// across the wrap and the collapse reads the same values in the same order either way.
1077 #[allow(clippy::too_many_arguments)]
1078 pub fn mla_kpool_pool_keys(
1079 &self,
1080 state: &CudaSlice<f32>,
1081 ape: &CudaSlice<f32>,
1082 pool_keys: &mut CudaSlice<f32>,
1083 pool_begin: usize,
1084 n_pools: usize,
1085 pool: usize,
1086 d: usize,
1087 state_rows: usize,
1088 ) -> Res<()> {
1089 let s = self.stream();
1090 unsafe {
1091 ck(
1092 "kpool_pool_keys",
1093 memra_mla_kpool_pool_keys_f32(
1094 state.device_ptr(&s).0 as *const f32,
1095 ape.device_ptr(&s).0 as *const f32,
1096 pool_keys.device_ptr_mut(&s).0 as *mut f32,
1097 pool_begin as i32,
1098 n_pools as i32,
1099 pool as i32,
1100 d as i32,
1101 state_rows as i32,
1102 s.cu_stream() as *mut c_void,
1103 ),
1104 )
1105 }
1106 }
1107
1108 /// Append `t` packed indexer rows `[k_norm | gate]` at absolute row `slot`, wrapping mod
1109 /// `rows` when the plane is a TAIL RING (`rows == 0` is the flat plane).
1110 ///
1111 /// SEPARATE from [`Engine::mla_append_latent`] on purpose: the latent plane is re-read by
1112 /// every later query through the gathered attention walk and is NOT a ring, so the two planes
1113 /// must not share a row-addressing contract even though they share a row shape.
1114 #[allow(clippy::too_many_arguments)]
1115 ///
1116 /// `src_row` is the first SOURCE row of `a`/`b` to append: the call's `k_norm`/`gate` are
1117 /// computed once for the whole call, and the tail-ring drain (`mla_kpool_indices`) walks them
1118 /// in sub-ranges. `src_row` 0 is the whole-call append.
1119 pub fn mla_index_append(
1120 &self,
1121 plane: &mut CudaSlice<f32>,
1122 a: &CudaSlice<f32>,
1123 b: &CudaSlice<f32>,
1124 src_row: usize,
1125 slot: usize,
1126 t: usize,
1127 wa: usize,
1128 wb: usize,
1129 rows: usize,
1130 ) -> Res<()> {
1131 let s = self.stream();
1132 unsafe {
1133 ck(
1134 "index_append_ring",
1135 memra_mla_index_append_ring_f32(
1136 plane.device_ptr_mut(&s).0 as *mut f32,
1137 (a.device_ptr(&s).0 as *const f32).add(src_row * wa),
1138 (b.device_ptr(&s).0 as *const f32).add(src_row * wb),
1139 slot as i32,
1140 t as i32,
1141 wa as i32,
1142 wb as i32,
1143 rows as i32,
1144 s.cu_stream() as *mut c_void,
1145 ),
1146 )
1147 }
1148 }
1149
1150 /// Head-mixed pool scores, `-inf` on pools whose last token is not visible to the query.
1151 /// `first_pos` is the absolute cache row of query 0 (queries are the cache's last `t_q` rows).
1152 ///
1153 /// Register-tiled fused GEMM+head-reduce: the pool-key tile stays resident in shared memory
1154 /// across the head loop, so `pool_keys` is read once per query TILE instead of once per
1155 /// query, and the head mix lands in the accumulator instead of costing a second pass over a
1156 /// `[t_q * heads, n_pools]` plane (17 GB at the shipped 1M/512 shape). BIT-IDENTICAL to
1157 /// [`Engine::mla_kpool_score_ref`] by construction — same six-step rounding sequence, spelled
1158 /// with explicit intrinsics — and gated so
1159 /// (`gpu_kpool_scoring_is_byte_identical_to_the_reference_kernel`). See the scoring section
1160 /// of `cu/mla_attn.cu` for why that identity is the requirement and not a nicety.
1161 #[allow(clippy::too_many_arguments)]
1162 pub fn mla_kpool_score(
1163 &self,
1164 q: &CudaSlice<f32>,
1165 pool_keys: &CudaSlice<f32>,
1166 head_weights: &CudaSlice<f32>,
1167 score: &mut CudaSlice<f32>,
1168 t_q: usize,
1169 heads: usize,
1170 d: usize,
1171 n_pools: usize,
1172 pool: usize,
1173 first_pos: usize,
1174 qk_scale: f32,
1175 head_scale: f32,
1176 ) -> Res<()> {
1177 let s = self.stream();
1178 // MEMRA_B200_DSA_DECODE door (level >= 1): the head-blocked decode scorer. Engages only
1179 // at decode widths and only from MLA_DSA_SCORE_MIN_POOLS up, where the block count can
1180 // fill the die; below that the shipped dispatch's own measured crossover already sends
1181 // decode to the reference kernel, which wins there. Bit-identical, so this is a speed
1182 // choice and nothing else. See research/b200-dsa-decode-20260902/ROOFLINE.md §2.
1183 if mla_dsa_decode_level() >= 1
1184 && (1..=MLA_DSA_ARM_T_MAX).contains(&t_q)
1185 && n_pools >= MLA_DSA_SCORE_MIN_POOLS
1186 {
1187 let rc = unsafe {
1188 memra_mla_kpool_score_dsa_f32(
1189 q.device_ptr(&s).0 as *const f32,
1190 pool_keys.device_ptr(&s).0 as *const f32,
1191 head_weights.device_ptr(&s).0 as *const f32,
1192 score.device_ptr_mut(&s).0 as *mut f32,
1193 t_q as i32,
1194 heads as i32,
1195 d as i32,
1196 n_pools as i32,
1197 pool as i32,
1198 first_pos as i32,
1199 qk_scale,
1200 head_scale,
1201 s.cu_stream() as *mut c_void,
1202 )
1203 };
1204 if !mla_dsa_geometry_refusal(rc) {
1205 mla_dsa_announce(
1206 "kpool_score",
1207 t_q,
1208 &format!("arm=head-blocked heads={heads} pools={n_pools} class=bit-identical"),
1209 );
1210 return ck("kpool_score_dsa", rc);
1211 }
1212 }
1213 unsafe {
1214 ck(
1215 "kpool_score",
1216 memra_mla_kpool_score_f32(
1217 q.device_ptr(&s).0 as *const f32,
1218 pool_keys.device_ptr(&s).0 as *const f32,
1219 head_weights.device_ptr(&s).0 as *const f32,
1220 score.device_ptr_mut(&s).0 as *mut f32,
1221 t_q as i32,
1222 heads as i32,
1223 d as i32,
1224 n_pools as i32,
1225 pool as i32,
1226 first_pos as i32,
1227 qk_scale,
1228 head_scale,
1229 s.cu_stream() as *mut c_void,
1230 ),
1231 )
1232 }
1233 }
1234
1235 /// The RETAINED reference scorer: block per (query, pool), one thread per head, head sum
1236 /// walked sequentially by thread 0. It defines the arithmetic [`Engine::mla_kpool_score`]
1237 /// reproduces, and it is the only consumer-visible reason this crate still builds the slow
1238 /// kernel. Not a serving path — `O(t_q * n_pools)` blocks of `heads` threads.
1239 #[allow(clippy::too_many_arguments)]
1240 pub fn mla_kpool_score_ref(
1241 &self,
1242 q: &CudaSlice<f32>,
1243 pool_keys: &CudaSlice<f32>,
1244 head_weights: &CudaSlice<f32>,
1245 score: &mut CudaSlice<f32>,
1246 t_q: usize,
1247 heads: usize,
1248 d: usize,
1249 n_pools: usize,
1250 pool: usize,
1251 first_pos: usize,
1252 qk_scale: f32,
1253 head_scale: f32,
1254 ) -> Res<()> {
1255 let s = self.stream();
1256 unsafe {
1257 ck(
1258 "kpool_score_ref",
1259 memra_mla_kpool_score_ref_f32(
1260 q.device_ptr(&s).0 as *const f32,
1261 pool_keys.device_ptr(&s).0 as *const f32,
1262 head_weights.device_ptr(&s).0 as *const f32,
1263 score.device_ptr_mut(&s).0 as *mut f32,
1264 t_q as i32,
1265 heads as i32,
1266 d as i32,
1267 n_pools as i32,
1268 pool as i32,
1269 first_pos as i32,
1270 qk_scale,
1271 head_scale,
1272 s.cu_stream() as *mut c_void,
1273 ),
1274 )
1275 }
1276 }
1277
1278 /// Top-`select_k` pools per query expanded to ascending cache rows, tail appended, -1 padded.
1279 ///
1280 /// Radix select on the 64-bit order key `(desc32(score) << 32) | pool_index`, whose ascending
1281 /// order IS the oracle's "score descending, pool index ascending" — see the ORDER contract
1282 /// block in `cu/mla_attn.cu`. `O(8 * n_pools / threads)` per query, independent of `select_k`.
1283 #[allow(clippy::too_many_arguments)]
1284 pub fn mla_kpool_select(
1285 &self,
1286 score: &CudaSlice<f32>,
1287 idx: &mut CudaSlice<i32>,
1288 t_q: usize,
1289 n_pools: usize,
1290 pool: usize,
1291 select_k: usize,
1292 width: usize,
1293 first_pos: usize,
1294 always_tail: bool,
1295 ) -> Res<()> {
1296 let s = self.stream();
1297 // MEMRA_B200_DSA_SELECT door: the exact multi-CTA selector. Byte-identical output, so
1298 // this is a speed choice and nothing else; it engages only where the single-CTA kernel
1299 // has parallelism to gain (see MLA_DSA_SELECT_MIN_POOLS).
1300 if mla_dsa_select_on() && mla_dsa_select_engages(t_q, n_pools) {
1301 let n_ctas = unsafe { memra_mla_kpool_select_ctas(n_pools as i32) };
1302 let stride = unsafe { memra_mla_kpool_select_ws_ints(n_ctas) };
1303 let mut ws = self.uninit_i32(t_q * stride as usize)?;
1304 mla_dsa_select_announce(t_q, n_pools, n_ctas);
1305 return unsafe {
1306 ck(
1307 "kpool_select_dsa",
1308 memra_mla_kpool_select_dsa_f32(
1309 score.device_ptr(&s).0 as *const f32,
1310 idx.device_ptr_mut(&s).0 as *mut i32,
1311 ws.device_ptr_mut(&s).0 as *mut i32,
1312 t_q as i32,
1313 n_pools as i32,
1314 pool as i32,
1315 select_k as i32,
1316 width as i32,
1317 first_pos as i32,
1318 i32::from(always_tail),
1319 s.cu_stream() as *mut c_void,
1320 ),
1321 )
1322 };
1323 }
1324 unsafe {
1325 ck(
1326 "kpool_select",
1327 memra_mla_kpool_select_f32(
1328 score.device_ptr(&s).0 as *const f32,
1329 idx.device_ptr_mut(&s).0 as *mut i32,
1330 t_q as i32,
1331 n_pools as i32,
1332 pool as i32,
1333 select_k as i32,
1334 width as i32,
1335 first_pos as i32,
1336 i32::from(always_tail),
1337 s.cu_stream() as *mut c_void,
1338 ),
1339 )
1340 }
1341 }
1342
1343 /// The `select_k`-rounds reference selection — the DEFINITION of the order the radix kernel
1344 /// above must reproduce. NOT a serving path: it is `O(select_k * n_pools / threads)` and
1345 /// exists so `gpu_kpool_radix_selection_is_byte_identical_to_the_reference_kernel` can hold
1346 /// the fast kernel to it at shapes the micro fixture cannot reach.
1347 #[allow(clippy::too_many_arguments)]
1348 pub fn mla_kpool_select_ref(
1349 &self,
1350 score: &CudaSlice<f32>,
1351 idx: &mut CudaSlice<i32>,
1352 t_q: usize,
1353 n_pools: usize,
1354 pool: usize,
1355 select_k: usize,
1356 width: usize,
1357 first_pos: usize,
1358 always_tail: bool,
1359 ) -> Res<()> {
1360 let s = self.stream();
1361 unsafe {
1362 ck(
1363 "kpool_select_ref",
1364 memra_mla_kpool_select_ref_f32(
1365 score.device_ptr(&s).0 as *const f32,
1366 idx.device_ptr_mut(&s).0 as *mut i32,
1367 t_q as i32,
1368 n_pools as i32,
1369 pool as i32,
1370 select_k as i32,
1371 width as i32,
1372 first_pos as i32,
1373 i32::from(always_tail),
1374 s.cu_stream() as *mut c_void,
1375 ),
1376 )
1377 }
1378 }
1379
1380 /// Strided-batched BF16 tensor-core GEMM over per-head planes — the
1381 /// MEMRA_MLA_TC_PREFILL absorb/decompress engine. Per head `b` in `0..batch`:
1382 /// `y_b[m, n] = x_b[m, k] @ w_b[n, k]^T`, f32 accumulate.
1383 ///
1384 /// `w` is the bf16 conversion-split weight plane: per-head `[n, k]` row-major,
1385 /// batch stride `n * k` (baked into the C side). `x` is a bf16 VIEW of a
1386 /// `[m, batch, k]` activation plane: per-head row stride `x_rs`, per-head base
1387 /// offset `x_bs` — for the canonical `[t, n_head, d]` layout that is
1388 /// `x_rs = batch * k`, `x_bs = k`. `y` mirrors that with `y_rs`/`y_bs` over `n`.
1389 ///
1390 /// `y_bf16` selects the output dtype: `true` writes bf16 (feeds the TC attention
1391 /// kernel directly, one fewer convert), `false` writes f32 (re-enters the f32
1392 /// stream). The caller passes `y` as raw bytes either way; an f32 output slice
1393 /// is viewed through its byte layout by the caller (`mla_bf16_gemm_sb_f32out`).
1394 ///
1395 /// rc 2xxxx (no cuBLASLt heuristic for the shape) is a DECLINE class the caller
1396 /// may fall back on; everything else is a hard error.
1397 #[allow(clippy::too_many_arguments)]
1398 pub fn mla_bf16_gemm_sb_raw(
1399 &self,
1400 w_bf16: &CudaSlice<u8>,
1401 x_bf16: &CudaSlice<u8>,
1402 y_ptr: u64,
1403 m: usize,
1404 n: usize,
1405 k: usize,
1406 x_rs: usize,
1407 x_bs: usize,
1408 y_rs: usize,
1409 y_bs: usize,
1410 batch: usize,
1411 y_bf16: bool,
1412 ) -> Res<i32> {
1413 // Workspace from the shared f16/bf16 Lt scratch (bf16_tc_gemm pattern).
1414 let mut guard = self.f16_scratch.lock().unwrap();
1415 if guard.is_none() {
1416 *guard = Some(crate::f16_ffi::F16Scratch::with_capacity(self, 2)?);
1417 }
1418 let s_scr = guard.as_mut().unwrap();
1419 let s = self.stream();
1420 let rc = unsafe {
1421 memra_bf16_gemm_sb(
1422 w_bf16.device_ptr(&s).0 as *const c_void,
1423 x_bf16.device_ptr(&s).0 as *const c_void,
1424 y_ptr as *mut c_void,
1425 m as i32,
1426 n as i32,
1427 k as i32,
1428 x_rs as i64,
1429 x_bs as i64,
1430 y_rs as i64,
1431 y_bs as i64,
1432 batch as i32,
1433 i32::from(y_bf16),
1434 s_scr.ws.device_ptr_mut(&s).0 as *mut c_void,
1435 crate::f16_ffi::F16_WS_BYTES,
1436 s.cu_stream() as *mut c_void,
1437 )
1438 };
1439 Ok(rc)
1440 }
1441
1442 /// [`Engine::mla_bf16_gemm_sb_raw`] with a bf16 output plane (absorb: feeds the TC
1443 /// attention kernel). Non-decline errors are named; a 2xxxx decline is returned as
1444 /// `Ok(false)` so the door can fall back to the per-position kernels.
1445 #[allow(clippy::too_many_arguments)]
1446 pub fn mla_bf16_gemm_sb_bf16out(
1447 &self,
1448 w_bf16: &CudaSlice<u8>,
1449 x_bf16: &CudaSlice<u8>,
1450 y_bf16: &mut CudaSlice<u8>,
1451 m: usize,
1452 n: usize,
1453 k: usize,
1454 x_rs: usize,
1455 x_bs: usize,
1456 y_rs: usize,
1457 y_bs: usize,
1458 batch: usize,
1459 ) -> Res<bool> {
1460 let s = self.stream();
1461 let (y_ptr, _gy) = y_bf16.device_ptr_mut(&s);
1462 let rc = self.mla_bf16_gemm_sb_raw(
1463 w_bf16, x_bf16, y_ptr, m, n, k, x_rs, x_bs, y_rs, y_bs, batch, true,
1464 )?;
1465 match rc {
1466 0 => Ok(true),
1467 r if (20000..30000).contains(&r) => Ok(false),
1468 r => Err(format!(
1469 "mla bf16 strided-batched GEMM (bf16 out) failed: rc {r} \
1470 (m={m} n={n} k={k} batch={batch})"
1471 )
1472 .into()),
1473 }
1474 }
1475
1476 /// [`Engine::mla_bf16_gemm_sb_raw`] with an f32 output plane (decompress: re-enters
1477 /// the f32 stream). Same decline contract as the bf16-out twin.
1478 #[allow(clippy::too_many_arguments)]
1479 pub fn mla_bf16_gemm_sb_f32out(
1480 &self,
1481 w_bf16: &CudaSlice<u8>,
1482 x_bf16: &CudaSlice<u8>,
1483 y_f32: &mut CudaSlice<f32>,
1484 m: usize,
1485 n: usize,
1486 k: usize,
1487 x_rs: usize,
1488 x_bs: usize,
1489 y_rs: usize,
1490 y_bs: usize,
1491 batch: usize,
1492 ) -> Res<bool> {
1493 let s = self.stream();
1494 let (y_ptr, _gy) = y_f32.device_ptr_mut(&s);
1495 let rc = self.mla_bf16_gemm_sb_raw(
1496 w_bf16, x_bf16, y_ptr, m, n, k, x_rs, x_bs, y_rs, y_bs, batch, false,
1497 )?;
1498 match rc {
1499 0 => Ok(true),
1500 r if (20000..30000).contains(&r) => Ok(false),
1501 r => Err(format!(
1502 "mla bf16 strided-batched GEMM (f32 out) failed: rc {r} \
1503 (m={m} n={n} k={k} batch={batch})"
1504 )
1505 .into()),
1506 }
1507 }
1508
1509 /// Absorbed-form MQA attention over a GATHERED index list (one list per query, shared across
1510 /// heads). Same body as `mla_attn_absorbed`; only the cache walk differs.
1511 #[allow(clippy::too_many_arguments)]
1512 pub fn mla_attn_gathered(
1513 &self,
1514 q_lat: &CudaSlice<f32>,
1515 q_pe: &CudaSlice<f32>,
1516 cache: &CudaSlice<f32>,
1517 idx: &CudaSlice<i32>,
1518 o_lat: &mut CudaSlice<f32>,
1519 n_head: usize,
1520 kv_rank: usize,
1521 d_rope: usize,
1522 t_q: usize,
1523 n_slots: usize,
1524 scale: f32,
1525 ) -> Res<()> {
1526 let s = self.stream();
1527 // MEMRA_B200_DSA_DECODE door, checked FIRST: its arms fight the same 64-CTA t_q=1
1528 // geometry the output-range split below does, without repeating the slot walk.
1529 // THE TWO LEVELS DIFFER TODAY, and the difference is this PR's headline: the shipped
1530 // table is [0, 32, 0, 0, 1, ...], so at t_q=1 level 1 takes arm 0 (falls through to
1531 // the sibling split door) while level 2 takes warp-online chunks=32 -- +9.7% vs
1532 // +43.1% in the 256k serving A/B. The `a >= 2 && dsa_level < 2` guard below exists
1533 // precisely because they differ, and the level boundary IS the numeric-class
1534 // admission boundary. See research/b200-dsa-decode-20260902/ROOFLINE.md.
1535 let dsa_level = mla_dsa_decode_level();
1536 let dsa_arm = if dsa_level >= 1 && t_q <= MLA_DSA_ARM_T_MAX {
1537 // `_effective` already enforces the named-class width rule (plain decode only, so
1538 // the spec-verify batch never sees `dsa-warp-online-f32`); level 2 is the second,
1539 // independent admission for the same class.
1540 let a = mla_dsa_attn_arm_effective(t_q);
1541 if a >= 2 && dsa_level < 2 { 0 } else { a }
1542 } else {
1543 0
1544 };
1545 if dsa_arm >= 2 {
1546 let cells = t_q * n_head * dsa_arm as usize;
1547 let mut part_m = self.uninit(cells)?;
1548 let mut part_d = self.uninit(cells)?;
1549 let mut part_acc = self.uninit(cells * kv_rank)?;
1550 let rc = unsafe {
1551 memra_mla_dsa_attn_split_f32(
1552 q_lat.device_ptr(&s).0 as *const f32,
1553 q_pe.device_ptr(&s).0 as *const f32,
1554 cache.device_ptr(&s).0 as *const f32,
1555 idx.device_ptr(&s).0 as *const i32,
1556 o_lat.device_ptr_mut(&s).0 as *mut f32,
1557 part_m.device_ptr_mut(&s).0 as *mut f32,
1558 part_d.device_ptr_mut(&s).0 as *mut f32,
1559 part_acc.device_ptr_mut(&s).0 as *mut f32,
1560 n_head as i32,
1561 kv_rank as i32,
1562 d_rope as i32,
1563 t_q as i32,
1564 n_slots as i32,
1565 dsa_arm,
1566 scale,
1567 s.cu_stream() as *mut c_void,
1568 )
1569 };
1570 if !mla_dsa_geometry_refusal(rc) {
1571 mla_dsa_announce(
1572 "attn_gathered",
1573 t_q,
1574 &format!("arm=warp-online chunks={dsa_arm} class=dsa-warp-online-f32"),
1575 );
1576 return ck("attn_gathered_dsa_warp", rc);
1577 }
1578 } else if dsa_arm == 1 {
1579 let rc = unsafe {
1580 memra_mla_attn_gathered_dsa_f32(
1581 q_lat.device_ptr(&s).0 as *const f32,
1582 q_pe.device_ptr(&s).0 as *const f32,
1583 cache.device_ptr(&s).0 as *const f32,
1584 idx.device_ptr(&s).0 as *const i32,
1585 o_lat.device_ptr_mut(&s).0 as *mut f32,
1586 n_head as i32,
1587 kv_rank as i32,
1588 d_rope as i32,
1589 t_q as i32,
1590 n_slots as i32,
1591 scale,
1592 s.cu_stream() as *mut c_void,
1593 )
1594 };
1595 if !mla_dsa_geometry_refusal(rc) {
1596 mla_dsa_announce("attn_gathered", t_q, "arm=single-pass class=bit-identical");
1597 return ck("attn_gathered_dsa", rc);
1598 }
1599 }
1600 // MEMRA_B200_MLA_DECODE_ARM door: output-range split from the t_q-keyed table
1601 // MLA_B200_ATTN_GATHERED_SPLIT. This twin repeats the score/softmax walk per split
1602 // block, unlike the absorb/decompress splits, which is why the B200 run found it a win
1603 // at t_q=1 only (see the table comment); every other cell is the shipped kernel.
1604 if let Some(split) = mla_b200_split_for(MlaB200Kernel::AttnGathered, t_q, kv_rank) {
1605 mla_b200_split_announce("attn_gathered", t_q, n_head, split);
1606 return unsafe {
1607 ck(
1608 "attn_gathered_split_b200",
1609 memra_mla_attn_gathered_split_f32(
1610 q_lat.device_ptr(&s).0 as *const f32,
1611 q_pe.device_ptr(&s).0 as *const f32,
1612 cache.device_ptr(&s).0 as *const f32,
1613 idx.device_ptr(&s).0 as *const i32,
1614 o_lat.device_ptr_mut(&s).0 as *mut f32,
1615 n_head as i32,
1616 kv_rank as i32,
1617 d_rope as i32,
1618 t_q as i32,
1619 n_slots as i32,
1620 scale,
1621 split,
1622 s.cu_stream() as *mut c_void,
1623 ),
1624 )
1625 };
1626 }
1627 unsafe {
1628 ck(
1629 "attn_gathered",
1630 memra_mla_attn_gathered_f32(
1631 q_lat.device_ptr(&s).0 as *const f32,
1632 q_pe.device_ptr(&s).0 as *const f32,
1633 cache.device_ptr(&s).0 as *const f32,
1634 idx.device_ptr(&s).0 as *const i32,
1635 o_lat.device_ptr_mut(&s).0 as *mut f32,
1636 n_head as i32,
1637 kv_rank as i32,
1638 d_rope as i32,
1639 t_q as i32,
1640 n_slots as i32,
1641 scale,
1642 s.cu_stream() as *mut c_void,
1643 ),
1644 )
1645 }
1646 }
1647}