Skip to main content

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
56unsafe extern "C" {
57    pub fn memra_mla_rope_interleaved_f32(
58        x: *mut f32,
59        n_pos: i32,
60        n_vec: i32,
61        d_rope: i32,
62        positions: *const i32,
63        base: f32,
64        stream: *mut c_void,
65    ) -> i32;
66    pub fn memra_mla_split_latent_f32(
67        kv: *const f32,
68        c_kv: *mut f32,
69        k_pe: *mut f32,
70        t: i32,
71        kv_rank: i32,
72        d_rope: i32,
73        stream: *mut c_void,
74    ) -> i32;
75    pub fn memra_mla_append_latent_f32(
76        cache: *mut f32,
77        c_kv: *const f32,
78        k_pe: *const f32,
79        slot: i32,
80        t: i32,
81        kv_rank: i32,
82        d_rope: i32,
83        stream: *mut c_void,
84    ) -> i32;
85    pub fn memra_mla_absorb_q_f32(
86        q_nope: *const f32,
87        wk_b: *const f32,
88        q_lat: *mut f32,
89        t_q: i32,
90        n_head: i32,
91        d_nope: i32,
92        kv_rank: i32,
93        stream: *mut c_void,
94    ) -> i32;
95    pub fn memra_mla_decompress_v_f32(
96        o_lat: *const f32,
97        wv_b: *const f32,
98        out: *mut f32,
99        t_q: i32,
100        n_head: i32,
101        d_v: i32,
102        kv_rank: i32,
103        stream: *mut c_void,
104    ) -> i32;
105    /// Decode-split twin of `memra_mla_absorb_q_f32` (MEMRA_MLA_DECODE_SPLIT): the same
106    /// per-output serial dot, its output range split across `split` blocks — bit-identical
107    /// by construction, gated in `tests/mla_decode_split_gpu.rs`.
108    #[allow(clippy::too_many_arguments)]
109    pub fn memra_mla_absorb_q_split_f32(
110        q_nope: *const f32,
111        wk_b: *const f32,
112        q_lat: *mut f32,
113        t_q: i32,
114        n_head: i32,
115        d_nope: i32,
116        kv_rank: i32,
117        split: i32,
118        stream: *mut c_void,
119    ) -> i32;
120    /// Decode-split twin of `memra_mla_decompress_v_f32` (see above).
121    #[allow(clippy::too_many_arguments)]
122    pub fn memra_mla_decompress_v_split_f32(
123        o_lat: *const f32,
124        wv_b: *const f32,
125        out: *mut f32,
126        t_q: i32,
127        n_head: i32,
128        d_v: i32,
129        kv_rank: i32,
130        split: i32,
131        stream: *mut c_void,
132    ) -> i32;
133    pub fn memra_mla_attn_absorbed_f32(
134        q_lat: *const f32,
135        q_pe: *const f32,
136        cache: *const f32,
137        o_lat: *mut f32,
138        n_head: i32,
139        kv_rank: i32,
140        d_rope: i32,
141        t_q: i32,
142        t_kv: i32,
143        scale: f32,
144        stream: *mut c_void,
145    ) -> i32;
146    pub fn memra_mla_index_append_ring_f32(
147        plane: *mut f32,
148        a: *const f32,
149        b: *const f32,
150        slot: i32,
151        t: i32,
152        wa: i32,
153        wb: i32,
154        rows: i32,
155        stream: *mut c_void,
156    ) -> i32;
157    pub fn memra_mla_kpool_pool_keys_f32(
158        state: *const f32,
159        ape: *const f32,
160        pool_keys: *mut f32,
161        pool_begin: i32,
162        n_pools: i32,
163        pool: i32,
164        d: i32,
165        state_rows: i32,
166        stream: *mut c_void,
167    ) -> i32;
168    pub fn memra_mla_kpool_score_f32(
169        q: *const f32,
170        pool_keys: *const f32,
171        hw: *const f32,
172        score: *mut f32,
173        t_q: i32,
174        heads: i32,
175        d: i32,
176        n_pools: i32,
177        pool: i32,
178        first_pos: i32,
179        qk_scale: f32,
180        head_scale: f32,
181        stream: *mut c_void,
182    ) -> i32;
183    pub fn memra_mla_kpool_score_ref_f32(
184        q: *const f32,
185        pool_keys: *const f32,
186        hw: *const f32,
187        score: *mut f32,
188        t_q: i32,
189        heads: i32,
190        d: i32,
191        n_pools: i32,
192        pool: i32,
193        first_pos: i32,
194        qk_scale: f32,
195        head_scale: f32,
196        stream: *mut c_void,
197    ) -> i32;
198    pub fn memra_mla_kpool_select_f32(
199        score: *const f32,
200        idx: *mut i32,
201        t_q: i32,
202        n_pools: i32,
203        pool: i32,
204        select_k: i32,
205        width: i32,
206        first_pos: i32,
207        always_tail: i32,
208        stream: *mut c_void,
209    ) -> i32;
210    pub fn memra_mla_kpool_select_ref_f32(
211        score: *const f32,
212        idx: *mut i32,
213        t_q: i32,
214        n_pools: i32,
215        pool: i32,
216        select_k: i32,
217        width: i32,
218        first_pos: i32,
219        always_tail: i32,
220        stream: *mut c_void,
221    ) -> i32;
222    pub fn memra_mla_attn_gathered_f32(
223        q_lat: *const f32,
224        q_pe: *const f32,
225        cache: *const f32,
226        idx: *const i32,
227        o_lat: *mut f32,
228        n_head: i32,
229        kv_rank: i32,
230        d_rope: i32,
231        t_q: i32,
232        n_slots: i32,
233        scale: f32,
234        stream: *mut c_void,
235    ) -> i32;
236    /// Strided-batched BF16 tensor-core GEMM (cu/f16_prefill.cu): per batch b,
237    /// `y_b[m, n] = x_b[m, k] @ w_b[n, k]^T`, f32 accumulate, y f32 or bf16 by flag.
238    /// The MEMRA_MLA_TC_PREFILL absorb/decompress engine (one launch replaces the
239    /// per-position absorb_q / decompress_v kernels at prefill widths).
240    fn memra_bf16_gemm_sb(
241        w_bf16: *const c_void,
242        x_bf16: *const c_void,
243        y: *mut c_void,
244        m: i32,
245        n: i32,
246        k: i32,
247        x_rs: i64,
248        x_bs: i64,
249        y_rs: i64,
250        y_bs: i64,
251        batch: i32,
252        y_is_bf16: i32,
253        ws: *mut c_void,
254        ws_bytes: usize,
255        stream: *mut c_void,
256    ) -> i32;
257}
258
259type Res<T> = Result<T, Box<dyn std::error::Error>>;
260
261/// Turn a launcher's status band into a named error. Every MLA launch goes through this —
262/// a silently-ignored non-zero status is how a contract violation becomes garbage activations.
263fn ck(what: &str, rc: i32) -> Res<()> {
264    if rc == 0 {
265        return Ok(());
266    }
267    let detail = match rc {
268        40001 => " (d_rope must be even — interleaved rope rotates (2j, 2j+1) pairs)",
269        40002 => " (kv_rank exceeds the kernel's MLA_MAX_RANK shared-memory ceiling)",
270        40003 => " (d_rope exceeds the kernel's MLA_MAX_ROPE ceiling)",
271        40004 => " (t_q > t_kv — queries must be a suffix of the latent cache)",
272        40010 => " (k-pool size out of range — 1..=MLA_MAX_POOL)",
273        40011 => " (indexer head count out of range — 1..=1024, one thread per head)",
274        40012 => " (t_q * n_pools exceeds the grid.x contract)",
275        40017 => " (indexer head dim must be positive)",
276        40013 => {
277            " (always_select_tail=false: queries before the first complete pool would have an \
278             empty candidate set, which the memra-reference oracle refuses outright)"
279        }
280        40014 => " (index-list width is narrower than select_k * pool + pool - 1)",
281        40015 => " (empty gathered candidate list — a zero softmax denominator)",
282        r if (10000..20000).contains(&r) => " (cudaError)",
283        _ => "",
284    };
285    Err(format!("mla kernel `{what}` failed: rc {rc}{detail}").into())
286}
287
288impl Engine {
289    /// Interleaved ("NORM") RoPE in place over `x` laid out [n_pos][n_vec][d_rope].
290    /// `d_rope == 0` (NoPE, glm5_next) is a no-op — the caller must still not pass an empty
291    /// slice through a path that dereferences it, which is why the rope plane is skipped
292    /// entirely in the forward arm rather than launched with a zero extent.
293    pub fn mla_rope_interleaved(
294        &self,
295        x: &mut CudaSlice<f32>,
296        pos_d: &CudaSlice<i32>,
297        n_pos: usize,
298        n_vec: usize,
299        d_rope: usize,
300        base: f32,
301    ) -> Res<()> {
302        if d_rope == 0 {
303            return Ok(());
304        }
305        let s = self.stream();
306        unsafe {
307            ck(
308                "rope_interleaved",
309                memra_mla_rope_interleaved_f32(
310                    x.device_ptr_mut(&s).0 as *mut f32,
311                    n_pos as i32,
312                    n_vec as i32,
313                    d_rope as i32,
314                    pos_d.device_ptr(&s).0 as *const i32,
315                    base,
316                    s.cu_stream() as *mut c_void,
317                ),
318            )
319        }
320    }
321
322    /// Split the `wkv_a` output rows [t][kv_rank + d_rope] into `c_kv` and `k_pe` planes.
323    pub fn mla_split_latent(
324        &self,
325        kv: &CudaSlice<f32>,
326        c_kv: &mut CudaSlice<f32>,
327        k_pe: &mut CudaSlice<f32>,
328        t: usize,
329        kv_rank: usize,
330        d_rope: usize,
331    ) -> Res<()> {
332        let s = self.stream();
333        unsafe {
334            ck(
335                "split_latent",
336                memra_mla_split_latent_f32(
337                    kv.device_ptr(&s).0 as *const f32,
338                    c_kv.device_ptr_mut(&s).0 as *mut f32,
339                    k_pe.device_ptr_mut(&s).0 as *mut f32,
340                    t as i32,
341                    kv_rank as i32,
342                    d_rope as i32,
343                    s.cu_stream() as *mut c_void,
344                ),
345            )
346        }
347    }
348
349    /// Append `t` latent rows `[c_kv | k_pe]` to the cache plane starting at row `slot`.
350    #[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
351    pub fn mla_append_latent(
352        &self,
353        cache: &mut CudaSlice<f32>,
354        c_kv: &CudaSlice<f32>,
355        k_pe: &CudaSlice<f32>,
356        slot: usize,
357        t: usize,
358        kv_rank: usize,
359        d_rope: usize,
360    ) -> Res<()> {
361        let s = self.stream();
362        unsafe {
363            ck(
364                "append_latent",
365                memra_mla_append_latent_f32(
366                    cache.device_ptr_mut(&s).0 as *mut f32,
367                    c_kv.device_ptr(&s).0 as *const f32,
368                    k_pe.device_ptr(&s).0 as *const f32,
369                    slot as i32,
370                    t as i32,
371                    kv_rank as i32,
372                    d_rope as i32,
373                    s.cu_stream() as *mut c_void,
374                ),
375            )
376        }
377    }
378
379    /// Absorb: `q_lat[i][h][:] = w_uk[h]ᵀ · q_nope[i][h][:]` (rank space).
380    #[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
381    pub fn mla_absorb_q(
382        &self,
383        q_nope: &CudaSlice<f32>,
384        wk_b: &CudaSlice<f32>,
385        q_lat: &mut CudaSlice<f32>,
386        t_q: usize,
387        n_head: usize,
388        d_nope: usize,
389        kv_rank: usize,
390    ) -> Res<()> {
391        let s = self.stream();
392        // MEMRA_MLA_DECODE_SPLIT door: same bytes at any split (see mla_decode_split_for).
393        if let Some(split) = mla_decode_split_for(t_q * n_head, kv_rank) {
394            mla_split_announce("absorb_q", t_q, n_head, split);
395            return unsafe {
396                ck(
397                    "absorb_q_split",
398                    memra_mla_absorb_q_split_f32(
399                        q_nope.device_ptr(&s).0 as *const f32,
400                        wk_b.device_ptr(&s).0 as *const f32,
401                        q_lat.device_ptr_mut(&s).0 as *mut f32,
402                        t_q as i32,
403                        n_head as i32,
404                        d_nope as i32,
405                        kv_rank as i32,
406                        split,
407                        s.cu_stream() as *mut c_void,
408                    ),
409                )
410            };
411        }
412        unsafe {
413            ck(
414                "absorb_q",
415                memra_mla_absorb_q_f32(
416                    q_nope.device_ptr(&s).0 as *const f32,
417                    wk_b.device_ptr(&s).0 as *const f32,
418                    q_lat.device_ptr_mut(&s).0 as *mut f32,
419                    t_q as i32,
420                    n_head as i32,
421                    d_nope as i32,
422                    kv_rank as i32,
423                    s.cu_stream() as *mut c_void,
424                ),
425            )
426        }
427    }
428
429    /// Decompress: `out[i][h][:] = w_uv[h] · o_lat[i][h][:]`.
430    #[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
431    pub fn mla_decompress_v(
432        &self,
433        o_lat: &CudaSlice<f32>,
434        wv_b: &CudaSlice<f32>,
435        out: &mut CudaSlice<f32>,
436        t_q: usize,
437        n_head: usize,
438        d_v: usize,
439        kv_rank: usize,
440    ) -> Res<()> {
441        let s = self.stream();
442        // MEMRA_MLA_DECODE_SPLIT door: same bytes at any split (see mla_decode_split_for).
443        if let Some(split) = mla_decode_split_for(t_q * n_head, d_v) {
444            mla_split_announce("decompress_v", t_q, n_head, split);
445            return unsafe {
446                ck(
447                    "decompress_v_split",
448                    memra_mla_decompress_v_split_f32(
449                        o_lat.device_ptr(&s).0 as *const f32,
450                        wv_b.device_ptr(&s).0 as *const f32,
451                        out.device_ptr_mut(&s).0 as *mut f32,
452                        t_q as i32,
453                        n_head as i32,
454                        d_v as i32,
455                        kv_rank as i32,
456                        split,
457                        s.cu_stream() as *mut c_void,
458                    ),
459                )
460            };
461        }
462        unsafe {
463            ck(
464                "decompress_v",
465                memra_mla_decompress_v_f32(
466                    o_lat.device_ptr(&s).0 as *const f32,
467                    wv_b.device_ptr(&s).0 as *const f32,
468                    out.device_ptr_mut(&s).0 as *mut f32,
469                    t_q as i32,
470                    n_head as i32,
471                    d_v as i32,
472                    kv_rank as i32,
473                    s.cu_stream() as *mut c_void,
474                ),
475            )
476        }
477    }
478
479    /// Absorbed-form MQA attention over the latent cache. `q_pe` is ignored when
480    /// `d_rope == 0`; callers on the NoPE path may pass any allocated slice.
481    #[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
482    pub fn mla_attn_absorbed(
483        &self,
484        q_lat: &CudaSlice<f32>,
485        q_pe: &CudaSlice<f32>,
486        cache: &CudaSlice<f32>,
487        o_lat: &mut CudaSlice<f32>,
488        n_head: usize,
489        kv_rank: usize,
490        d_rope: usize,
491        t_q: usize,
492        t_kv: usize,
493        scale: f32,
494    ) -> Res<()> {
495        let s = self.stream();
496        unsafe {
497            ck(
498                "attn_absorbed",
499                memra_mla_attn_absorbed_f32(
500                    q_lat.device_ptr(&s).0 as *const f32,
501                    q_pe.device_ptr(&s).0 as *const f32,
502                    cache.device_ptr(&s).0 as *const f32,
503                    o_lat.device_ptr_mut(&s).0 as *mut f32,
504                    n_head as i32,
505                    kv_rank as i32,
506                    d_rope as i32,
507                    t_q as i32,
508                    t_kv as i32,
509                    scale,
510                    s.cu_stream() as *mut c_void,
511                ),
512            )
513        }
514    }
515}
516
517/// Safe wrappers for the DSA k-pool indexer (`cu/mla_attn.cu`, "DSA k-pool indexer" section).
518/// Numeric truth is `memra_reference::kpool_allowed_tokens`; the gate is
519/// `tests/glm5_kpool_indexer_gpu.rs`.
520impl Engine {
521    /// Collapse pools `[pool_begin, n_pools)` of `pool` cached indexer rows each into one key by a
522    /// learned per-channel softmax over (gate score + positional embedding).
523    /// `state` rows are `[k | gate]`, `2 * d` wide; `ape` is `[pool][d]` row-major.
524    ///
525    /// `pool_begin` is the RESIDENCY seam: a pool's key depends only on its own `pool` state rows
526    /// (append-only, never rewritten) and the constant `ape`, so it is final the instant the
527    /// pool's last row lands. Pools below `pool_begin` are already resident and are left alone —
528    /// bit-identically to what rebuilding them would produce. Pass 0 for a full rebuild.
529    ///
530    /// `state_rows` is the indexer plane's TAIL-RING size in rows (0 = flat, absolute
531    /// addressing). It is always a multiple of `pool`, so a pool's members stay contiguous
532    /// across the wrap and the collapse reads the same values in the same order either way.
533    #[allow(clippy::too_many_arguments)]
534    pub fn mla_kpool_pool_keys(
535        &self,
536        state: &CudaSlice<f32>,
537        ape: &CudaSlice<f32>,
538        pool_keys: &mut CudaSlice<f32>,
539        pool_begin: usize,
540        n_pools: usize,
541        pool: usize,
542        d: usize,
543        state_rows: usize,
544    ) -> Res<()> {
545        let s = self.stream();
546        unsafe {
547            ck(
548                "kpool_pool_keys",
549                memra_mla_kpool_pool_keys_f32(
550                    state.device_ptr(&s).0 as *const f32,
551                    ape.device_ptr(&s).0 as *const f32,
552                    pool_keys.device_ptr_mut(&s).0 as *mut f32,
553                    pool_begin as i32,
554                    n_pools as i32,
555                    pool as i32,
556                    d as i32,
557                    state_rows as i32,
558                    s.cu_stream() as *mut c_void,
559                ),
560            )
561        }
562    }
563
564    /// Append `t` packed indexer rows `[k_norm | gate]` at absolute row `slot`, wrapping mod
565    /// `rows` when the plane is a TAIL RING (`rows == 0` is the flat plane).
566    ///
567    /// SEPARATE from [`Engine::mla_append_latent`] on purpose: the latent plane is re-read by
568    /// every later query through the gathered attention walk and is NOT a ring, so the two planes
569    /// must not share a row-addressing contract even though they share a row shape.
570    #[allow(clippy::too_many_arguments)]
571    ///
572    /// `src_row` is the first SOURCE row of `a`/`b` to append: the call's `k_norm`/`gate` are
573    /// computed once for the whole call, and the tail-ring drain (`mla_kpool_indices`) walks them
574    /// in sub-ranges. `src_row` 0 is the whole-call append.
575    pub fn mla_index_append(
576        &self,
577        plane: &mut CudaSlice<f32>,
578        a: &CudaSlice<f32>,
579        b: &CudaSlice<f32>,
580        src_row: usize,
581        slot: usize,
582        t: usize,
583        wa: usize,
584        wb: usize,
585        rows: usize,
586    ) -> Res<()> {
587        let s = self.stream();
588        unsafe {
589            ck(
590                "index_append_ring",
591                memra_mla_index_append_ring_f32(
592                    plane.device_ptr_mut(&s).0 as *mut f32,
593                    (a.device_ptr(&s).0 as *const f32).add(src_row * wa),
594                    (b.device_ptr(&s).0 as *const f32).add(src_row * wb),
595                    slot as i32,
596                    t as i32,
597                    wa as i32,
598                    wb as i32,
599                    rows as i32,
600                    s.cu_stream() as *mut c_void,
601                ),
602            )
603        }
604    }
605
606    /// Head-mixed pool scores, `-inf` on pools whose last token is not visible to the query.
607    /// `first_pos` is the absolute cache row of query 0 (queries are the cache's last `t_q` rows).
608    ///
609    /// Register-tiled fused GEMM+head-reduce: the pool-key tile stays resident in shared memory
610    /// across the head loop, so `pool_keys` is read once per query TILE instead of once per
611    /// query, and the head mix lands in the accumulator instead of costing a second pass over a
612    /// `[t_q * heads, n_pools]` plane (17 GB at the shipped 1M/512 shape). BIT-IDENTICAL to
613    /// [`Engine::mla_kpool_score_ref`] by construction — same six-step rounding sequence, spelled
614    /// with explicit intrinsics — and gated so
615    /// (`gpu_kpool_scoring_is_byte_identical_to_the_reference_kernel`). See the scoring section
616    /// of `cu/mla_attn.cu` for why that identity is the requirement and not a nicety.
617    #[allow(clippy::too_many_arguments)]
618    pub fn mla_kpool_score(
619        &self,
620        q: &CudaSlice<f32>,
621        pool_keys: &CudaSlice<f32>,
622        head_weights: &CudaSlice<f32>,
623        score: &mut CudaSlice<f32>,
624        t_q: usize,
625        heads: usize,
626        d: usize,
627        n_pools: usize,
628        pool: usize,
629        first_pos: usize,
630        qk_scale: f32,
631        head_scale: f32,
632    ) -> Res<()> {
633        let s = self.stream();
634        unsafe {
635            ck(
636                "kpool_score",
637                memra_mla_kpool_score_f32(
638                    q.device_ptr(&s).0 as *const f32,
639                    pool_keys.device_ptr(&s).0 as *const f32,
640                    head_weights.device_ptr(&s).0 as *const f32,
641                    score.device_ptr_mut(&s).0 as *mut f32,
642                    t_q as i32,
643                    heads as i32,
644                    d as i32,
645                    n_pools as i32,
646                    pool as i32,
647                    first_pos as i32,
648                    qk_scale,
649                    head_scale,
650                    s.cu_stream() as *mut c_void,
651                ),
652            )
653        }
654    }
655
656    /// The RETAINED reference scorer: block per (query, pool), one thread per head, head sum
657    /// walked sequentially by thread 0. It defines the arithmetic [`Engine::mla_kpool_score`]
658    /// reproduces, and it is the only consumer-visible reason this crate still builds the slow
659    /// kernel. Not a serving path — `O(t_q * n_pools)` blocks of `heads` threads.
660    #[allow(clippy::too_many_arguments)]
661    pub fn mla_kpool_score_ref(
662        &self,
663        q: &CudaSlice<f32>,
664        pool_keys: &CudaSlice<f32>,
665        head_weights: &CudaSlice<f32>,
666        score: &mut CudaSlice<f32>,
667        t_q: usize,
668        heads: usize,
669        d: usize,
670        n_pools: usize,
671        pool: usize,
672        first_pos: usize,
673        qk_scale: f32,
674        head_scale: f32,
675    ) -> Res<()> {
676        let s = self.stream();
677        unsafe {
678            ck(
679                "kpool_score_ref",
680                memra_mla_kpool_score_ref_f32(
681                    q.device_ptr(&s).0 as *const f32,
682                    pool_keys.device_ptr(&s).0 as *const f32,
683                    head_weights.device_ptr(&s).0 as *const f32,
684                    score.device_ptr_mut(&s).0 as *mut f32,
685                    t_q as i32,
686                    heads as i32,
687                    d as i32,
688                    n_pools as i32,
689                    pool as i32,
690                    first_pos as i32,
691                    qk_scale,
692                    head_scale,
693                    s.cu_stream() as *mut c_void,
694                ),
695            )
696        }
697    }
698
699    /// Top-`select_k` pools per query expanded to ascending cache rows, tail appended, -1 padded.
700    ///
701    /// Radix select on the 64-bit order key `(desc32(score) << 32) | pool_index`, whose ascending
702    /// order IS the oracle's "score descending, pool index ascending" — see the ORDER contract
703    /// block in `cu/mla_attn.cu`. `O(8 * n_pools / threads)` per query, independent of `select_k`.
704    #[allow(clippy::too_many_arguments)]
705    pub fn mla_kpool_select(
706        &self,
707        score: &CudaSlice<f32>,
708        idx: &mut CudaSlice<i32>,
709        t_q: usize,
710        n_pools: usize,
711        pool: usize,
712        select_k: usize,
713        width: usize,
714        first_pos: usize,
715        always_tail: bool,
716    ) -> Res<()> {
717        let s = self.stream();
718        unsafe {
719            ck(
720                "kpool_select",
721                memra_mla_kpool_select_f32(
722                    score.device_ptr(&s).0 as *const f32,
723                    idx.device_ptr_mut(&s).0 as *mut i32,
724                    t_q as i32,
725                    n_pools as i32,
726                    pool as i32,
727                    select_k as i32,
728                    width as i32,
729                    first_pos as i32,
730                    i32::from(always_tail),
731                    s.cu_stream() as *mut c_void,
732                ),
733            )
734        }
735    }
736
737    /// The `select_k`-rounds reference selection — the DEFINITION of the order the radix kernel
738    /// above must reproduce. NOT a serving path: it is `O(select_k * n_pools / threads)` and
739    /// exists so `gpu_kpool_radix_selection_is_byte_identical_to_the_reference_kernel` can hold
740    /// the fast kernel to it at shapes the micro fixture cannot reach.
741    #[allow(clippy::too_many_arguments)]
742    pub fn mla_kpool_select_ref(
743        &self,
744        score: &CudaSlice<f32>,
745        idx: &mut CudaSlice<i32>,
746        t_q: usize,
747        n_pools: usize,
748        pool: usize,
749        select_k: usize,
750        width: usize,
751        first_pos: usize,
752        always_tail: bool,
753    ) -> Res<()> {
754        let s = self.stream();
755        unsafe {
756            ck(
757                "kpool_select_ref",
758                memra_mla_kpool_select_ref_f32(
759                    score.device_ptr(&s).0 as *const f32,
760                    idx.device_ptr_mut(&s).0 as *mut i32,
761                    t_q as i32,
762                    n_pools as i32,
763                    pool as i32,
764                    select_k as i32,
765                    width as i32,
766                    first_pos as i32,
767                    i32::from(always_tail),
768                    s.cu_stream() as *mut c_void,
769                ),
770            )
771        }
772    }
773
774    /// Strided-batched BF16 tensor-core GEMM over per-head planes — the
775    /// MEMRA_MLA_TC_PREFILL absorb/decompress engine. Per head `b` in `0..batch`:
776    /// `y_b[m, n] = x_b[m, k] @ w_b[n, k]^T`, f32 accumulate.
777    ///
778    /// `w` is the bf16 conversion-split weight plane: per-head `[n, k]` row-major,
779    /// batch stride `n * k` (baked into the C side). `x` is a bf16 VIEW of a
780    /// `[m, batch, k]` activation plane: per-head row stride `x_rs`, per-head base
781    /// offset `x_bs` — for the canonical `[t, n_head, d]` layout that is
782    /// `x_rs = batch * k`, `x_bs = k`. `y` mirrors that with `y_rs`/`y_bs` over `n`.
783    ///
784    /// `y_bf16` selects the output dtype: `true` writes bf16 (feeds the TC attention
785    /// kernel directly, one fewer convert), `false` writes f32 (re-enters the f32
786    /// stream). The caller passes `y` as raw bytes either way; an f32 output slice
787    /// is viewed through its byte layout by the caller (`mla_bf16_gemm_sb_f32out`).
788    ///
789    /// rc 2xxxx (no cuBLASLt heuristic for the shape) is a DECLINE class the caller
790    /// may fall back on; everything else is a hard error.
791    #[allow(clippy::too_many_arguments)]
792    pub fn mla_bf16_gemm_sb_raw(
793        &self,
794        w_bf16: &CudaSlice<u8>,
795        x_bf16: &CudaSlice<u8>,
796        y_ptr: u64,
797        m: usize,
798        n: usize,
799        k: usize,
800        x_rs: usize,
801        x_bs: usize,
802        y_rs: usize,
803        y_bs: usize,
804        batch: usize,
805        y_bf16: bool,
806    ) -> Res<i32> {
807        // Workspace from the shared f16/bf16 Lt scratch (bf16_tc_gemm pattern).
808        let mut guard = self.f16_scratch.lock().unwrap();
809        if guard.is_none() {
810            *guard = Some(crate::f16_ffi::F16Scratch::with_capacity(self, 2)?);
811        }
812        let s_scr = guard.as_mut().unwrap();
813        let s = self.stream();
814        let rc = unsafe {
815            memra_bf16_gemm_sb(
816                w_bf16.device_ptr(&s).0 as *const c_void,
817                x_bf16.device_ptr(&s).0 as *const c_void,
818                y_ptr as *mut c_void,
819                m as i32,
820                n as i32,
821                k as i32,
822                x_rs as i64,
823                x_bs as i64,
824                y_rs as i64,
825                y_bs as i64,
826                batch as i32,
827                i32::from(y_bf16),
828                s_scr.ws.device_ptr_mut(&s).0 as *mut c_void,
829                crate::f16_ffi::F16_WS_BYTES,
830                s.cu_stream() as *mut c_void,
831            )
832        };
833        Ok(rc)
834    }
835
836    /// [`Engine::mla_bf16_gemm_sb_raw`] with a bf16 output plane (absorb: feeds the TC
837    /// attention kernel). Non-decline errors are named; a 2xxxx decline is returned as
838    /// `Ok(false)` so the door can fall back to the per-position kernels.
839    #[allow(clippy::too_many_arguments)]
840    pub fn mla_bf16_gemm_sb_bf16out(
841        &self,
842        w_bf16: &CudaSlice<u8>,
843        x_bf16: &CudaSlice<u8>,
844        y_bf16: &mut CudaSlice<u8>,
845        m: usize,
846        n: usize,
847        k: usize,
848        x_rs: usize,
849        x_bs: usize,
850        y_rs: usize,
851        y_bs: usize,
852        batch: usize,
853    ) -> Res<bool> {
854        let s = self.stream();
855        let (y_ptr, _gy) = y_bf16.device_ptr_mut(&s);
856        let rc = self.mla_bf16_gemm_sb_raw(
857            w_bf16, x_bf16, y_ptr, m, n, k, x_rs, x_bs, y_rs, y_bs, batch, true,
858        )?;
859        match rc {
860            0 => Ok(true),
861            r if (20000..30000).contains(&r) => Ok(false),
862            r => Err(format!(
863                "mla bf16 strided-batched GEMM (bf16 out) failed: rc {r} \
864                 (m={m} n={n} k={k} batch={batch})"
865            )
866            .into()),
867        }
868    }
869
870    /// [`Engine::mla_bf16_gemm_sb_raw`] with an f32 output plane (decompress: re-enters
871    /// the f32 stream). Same decline contract as the bf16-out twin.
872    #[allow(clippy::too_many_arguments)]
873    pub fn mla_bf16_gemm_sb_f32out(
874        &self,
875        w_bf16: &CudaSlice<u8>,
876        x_bf16: &CudaSlice<u8>,
877        y_f32: &mut CudaSlice<f32>,
878        m: usize,
879        n: usize,
880        k: usize,
881        x_rs: usize,
882        x_bs: usize,
883        y_rs: usize,
884        y_bs: usize,
885        batch: usize,
886    ) -> Res<bool> {
887        let s = self.stream();
888        let (y_ptr, _gy) = y_f32.device_ptr_mut(&s);
889        let rc = self.mla_bf16_gemm_sb_raw(
890            w_bf16, x_bf16, y_ptr, m, n, k, x_rs, x_bs, y_rs, y_bs, batch, false,
891        )?;
892        match rc {
893            0 => Ok(true),
894            r if (20000..30000).contains(&r) => Ok(false),
895            r => Err(format!(
896                "mla bf16 strided-batched GEMM (f32 out) failed: rc {r} \
897                 (m={m} n={n} k={k} batch={batch})"
898            )
899            .into()),
900        }
901    }
902
903    /// Absorbed-form MQA attention over a GATHERED index list (one list per query, shared across
904    /// heads). Same body as `mla_attn_absorbed`; only the cache walk differs.
905    #[allow(clippy::too_many_arguments)]
906    pub fn mla_attn_gathered(
907        &self,
908        q_lat: &CudaSlice<f32>,
909        q_pe: &CudaSlice<f32>,
910        cache: &CudaSlice<f32>,
911        idx: &CudaSlice<i32>,
912        o_lat: &mut CudaSlice<f32>,
913        n_head: usize,
914        kv_rank: usize,
915        d_rope: usize,
916        t_q: usize,
917        n_slots: usize,
918        scale: f32,
919    ) -> Res<()> {
920        let s = self.stream();
921        unsafe {
922            ck(
923                "attn_gathered",
924                memra_mla_attn_gathered_f32(
925                    q_lat.device_ptr(&s).0 as *const f32,
926                    q_pe.device_ptr(&s).0 as *const f32,
927                    cache.device_ptr(&s).0 as *const f32,
928                    idx.device_ptr(&s).0 as *const i32,
929                    o_lat.device_ptr_mut(&s).0 as *mut f32,
930                    n_head as i32,
931                    kv_rank as i32,
932                    d_rope as i32,
933                    t_q as i32,
934                    n_slots as i32,
935                    scale,
936                    s.cu_stream() as *mut c_void,
937                ),
938            )
939        }
940    }
941}