memra_engine/kda.rs
1//! Kimi Delta Attention (KDA) — the glm5_next (GLM-5.3-Flash) linear-attention mixer.
2//!
3//! Arithmetic contract: `memra_reference::kimi_delta_net`, pinned by
4//! `kimi_delta_net_matches_hand_derived_three_token_recurrence`. Every step below cites the
5//! reference stage it reproduces; the GPU-vs-reference gate is
6//! `crates/memra-engine/tests/kda_fixture_gpu.rs`.
7//!
8//! Geometry (research/glm53-flash-bringup-20260827/CENSUS.md): 64 heads x 128, q/k/v all the
9//! same width, short conv kernel 4, forget-gate lower bound -5.0. Symmetric widths and no GQA
10//! repeat mean channel `c == h*head_dim + i` IS the (head, dim) pair, so every per-token tensor
11//! stays token-major end to end — there is no analogue of GDN's qkv_to_gdn_repack scatter here.
12//!
13//! PREFILL DISPATCH — SEQUENTIAL SCAN, not the chunked UT transform (deliberate).
14//! `memra_kda_scan_s128` runs prefill and decode alike, which is exactly the shipped
15//! GDN arrangement next door: `gdn_scan_s128` IS the default prefill path and the chunked WY
16//! kernels sit behind `MEMRA_GDN_CHUNKED`. One kernel for both also keeps the decode==verify
17//! dispatch identity that cu/hybrid.cu's headers require. A chunked twin exists but is
18//! SHELVED, ATTRIBUTED-NEGATIVE — it is not a pending tuning follow-up. It was built as L3
19//! of the prefill-gap plan (`MEMRA_KDA_CHUNKED`, unmerged branch lane/glm5-kda-chunk-scan),
20//! and the box prefill census then attributed the wall elsewhere: on a cold 4626-token prime
21//! the whole kda family is 221.6 GPU ms of 6598 (3.4%, "confirms L3's ATTRIBUTED-NEGATIVE:
22//! scan ~2.4%") while mla-prefill-attn owns 75.8% — receipts
23//! `research/glm53-flash-bringup-20260827/launch-diet-20260830/WINDOW-20260830.md` §4 and
24//! `box-receipts-20260830/census-analysis.txt`. No A/B is owed on the scan; a revival needs
25//! a new attribution first. The algebra stays banked for that day: it is NOT a transcription
26//! of the GDN K1-K5 chain — KDA's decay is per channel, so the chunk form needs a per-channel
27//! cumulative log gate `Gcum[t][i]` with `k` scaled by `exp(-Gcum)` and `q` by `exp(+Gcum)`
28//! (banked `chunk_kimi_delta_attention` in
29//! research/glm53-flash-bringup-20260827/modular_glm5_next-ref.py), where GDN gets away with
30//! one scalar `G` per (token, head).
31//!
32//! CONV FUSION — fused WEIGHTS and a fused RING, per-plane launches. The checkpoint ships three
33//! per-plane conv weights; they are concatenated once at load into one `[3*qkv, kernel]` f32
34//! buffer, because the plan already declares the state carrier fused (`StatePlan::Recurrent`
35//! `conv_width = 3*qkv`) and that makes a plane's weight offset and its ring offset the same
36//! `plane*qkv` arithmetic. The three PROJECTIONS stay separate: they are independently
37//! quantized tensors, and concatenating them would mean dequantizing to build one matmul.
38//! Applying each plane's taps to its own plane is the fused grouped conv exactly (the reference
39//! says so in-line), so nothing is approximated by the split.
40
41use crate::Engine;
42use crate::cache::{Cache, RecurLayer};
43use crate::model::GpuTensor;
44use cudarc::driver::{CudaSlice, LaunchConfig, PushKernelArg};
45use memra_gguf::model_plan::KimiDeltaNetPlan;
46use memra_gguf::source::TensorSource;
47use std::sync::atomic::{AtomicU64, Ordering};
48
49/// Engagement counter for the fused 6-way projection door (`MEMRA_KDA_FUSED_PROJ`), the
50/// grouped-prefill `moe_grouped_prefill_dispatches` precedent: gates and box A/B arms count
51/// dispatches at the arm's own call site instead of inferring engagement from a 200.
52pub static KDA_FUSED6_DISPATCHES: AtomicU64 = AtomicU64::new(0);
53
54/// Same door, BF16 operand arm (`qmatvec_kda6_bf16f32`, lane/glm5-decode-diet lever 3).
55/// Counted separately so a box A/B on the serving recipe (MEMRA_BF16_MMV=1, where the q8 arm
56/// refuses by design) can attribute engagement to the arm that actually ran.
57pub static KDA_FUSED6_BF16_DISPATCHES: AtomicU64 = AtomicU64::new(0);
58
59/// Same door, W8-MIRROR arm (`qmatvec_kda6_q8f32_rp_v2`, lane/b200-gemv-hbm-20260902 round 3).
60/// Counted separately for the same reason the bf16 arm is: a box A/B on the serving recipe must
61/// be able to attribute engagement to the arm that actually ran.
62pub static KDA_FUSED6_Q8RP_DISPATCHES: AtomicU64 = AtomicU64::new(0);
63
64/// The only head width `memra_kda_scan_s128` is instantiated for, and the only one glm5_next
65/// ships (`linear_attn_config.head_dim = 128`).
66pub const KDA_HEAD_DIM: usize = 128;
67/// The conv kernels hold their window in a fixed register array; wider kernels would silently
68/// read past it, so the loader refuses them.
69const KDA_MAX_CONV_KERNEL: usize = 8;
70/// FLA l2norm epsilon. Fixed at 1e-6 and INSIDE the sqrt — independent of the layer's rms eps,
71/// which is a different constant used by the output norm below.
72const KDA_L2_EPS: f32 = 1e-6;
73
74/// One loaded KDA mixer. Field names follow the reference's tensor roles, not the HF spellings.
75pub struct KdaAttnLayer {
76 pub plan: KimiDeltaNetPlan,
77 /// q/k/v projections, `[qkv, hidden]` each.
78 pub wq: GpuTensor,
79 pub wk: GpuTensor,
80 pub wv: GpuTensor,
81 /// Forget gate low-rank pair: `f_a [head_dim, hidden]`, `f_b [qkv, head_dim]`.
82 pub f_a: GpuTensor,
83 pub f_b: GpuTensor,
84 /// Output gate low-rank pair, same shapes as the forget pair.
85 pub g_a: GpuTensor,
86 pub g_b: GpuTensor,
87 /// Per-head beta projection, `[heads, hidden]`.
88 pub b_proj: GpuTensor,
89 /// Output projection, `[hidden, qkv]`.
90 pub wo: GpuTensor,
91 /// The three per-plane conv weights concatenated into `[3*qkv, kernel]` (see module header).
92 pub conv: CudaSlice<f32>,
93 /// `A_log [heads]`, `dt_bias [qkv]` (per CHANNEL, unlike GDN's per-head bias),
94 /// `o_norm [head_dim]`.
95 pub a_log: GpuTensor,
96 pub dt_bias: GpuTensor,
97 pub o_norm: GpuTensor,
98 /// glm5 TP-2 sidecar (`MEMRA_GLM5_TP`, lane/glm5-tp2). `Some` means THIS layer struct is
99 /// the ROOT-RANK HEAD SHARD (heads/2) and the sidecar carries the peer shard + runtime.
100 /// Every plain entry point REFUSES a sharded layer by name — only the TP walk
101 /// (`glm5_tp::kda_tp_*`) may execute it. `None` everywhere else (zero cost, zero change).
102 pub tp: Option<Box<crate::glm5_tp::Glm5TpKda>>,
103}
104
105impl KdaAttnLayer {
106 pub fn heads(&self) -> usize {
107 self.plan.num_heads as usize
108 }
109 pub fn head_dim(&self) -> usize {
110 self.plan.head_dim as usize
111 }
112 pub fn qkv(&self) -> usize {
113 self.heads() * self.head_dim()
114 }
115 pub fn conv_kernel(&self) -> usize {
116 self.plan.conv_kernel as usize
117 }
118 /// Fused conv ring width, matching `StatePlan::Recurrent { conv_width }` for this layer.
119 pub fn conv_width(&self) -> usize {
120 3 * self.qkv()
121 }
122 /// Recurrent state elements, matching `StatePlan::Recurrent { state_width }`.
123 pub fn state_width(&self) -> usize {
124 self.heads() * self.head_dim() * self.head_dim()
125 }
126
127 /// Load block `il`'s KDA tensors. Names are the ggml-dialect contract names from
128 /// `memra_gguf::tensor_contract::add_kda`; the safetensors source translates them.
129 pub fn load(
130 e: &Engine,
131 src: &dyn TensorSource,
132 il: u32,
133 plan: &KimiDeltaNetPlan,
134 ) -> Result<Self, Box<dyn std::error::Error>> {
135 let heads = plan.num_heads as usize;
136 let head_dim = plan.head_dim as usize;
137 let kernel = plan.conv_kernel as usize;
138 if head_dim != KDA_HEAD_DIM {
139 return Err(format!(
140 "blk.{il}: KDA head_dim {head_dim} is not the {KDA_HEAD_DIM} the scan kernel is \
141 instantiated for; a new memra_kda_scan_s<N> instantiation is required before \
142 this geometry can serve"
143 )
144 .into());
145 }
146 if heads == 0 {
147 return Err(format!("blk.{il}: KDA num_heads must be positive").into());
148 }
149 if !(2..=KDA_MAX_CONV_KERNEL).contains(&kernel) {
150 return Err(format!(
151 "blk.{il}: KDA conv_kernel {kernel} outside the 2..={KDA_MAX_CONV_KERNEL} window \
152 the conv kernels hold in registers"
153 )
154 .into());
155 }
156 let p = |s: &str| format!("blk.{il}.{s}");
157 let load = |name: String| GpuTensor::load_from_source(e, src, &name);
158
159 let qkv = heads * head_dim;
160 // Fuse the three per-plane conv weights into one [3*qkv, kernel] buffer (module header).
161 // Each source tensor is [qkv, kernel] channel-major, so the planes concatenate as whole
162 // row blocks and plane p lands at row p*qkv — the ring's own plane offset.
163 let mut conv = e.zeros(3 * qkv * kernel)?;
164 for (plane, name) in [
165 "kda_q_conv1d.weight",
166 "kda_k_conv1d.weight",
167 "kda_v_conv1d.weight",
168 ]
169 .into_iter()
170 .enumerate()
171 {
172 let w = load(p(name))?;
173 let src_data = w.float_data();
174 if src_data.len() != qkv * kernel {
175 return Err(format!(
176 "blk.{il}.{name}: {} elements, contract requires {}",
177 src_data.len(),
178 qkv * kernel
179 )
180 .into());
181 }
182 e.copy_into(&mut conv, plane * qkv * kernel, src_data, qkv * kernel)?;
183 }
184
185 Ok(Self {
186 plan: *plan,
187 wq: load(p("kda_q.weight"))?,
188 wk: load(p("kda_k.weight"))?,
189 wv: load(p("kda_v.weight"))?,
190 f_a: load(p("kda_f_a.weight"))?,
191 f_b: load(p("kda_f_b.weight"))?,
192 g_a: load(p("kda_g_a.weight"))?,
193 g_b: load(p("kda_g_b.weight"))?,
194 b_proj: load(p("kda_b.weight"))?,
195 wo: load(p("kda_out.weight"))?,
196 conv,
197 a_log: load(p("kda_a_log"))?,
198 dt_bias: load(p("kda_dt.bias"))?,
199 o_norm: load(p("kda_o_norm.weight"))?,
200 tp: None,
201 })
202 }
203}
204
205/// Which conv arm a call takes. `Prefill` reads the ring as a left pad and rolls it afterwards;
206/// `Decode` fuses assemble+conv+roll for the single new row. The two produce bit-identical
207/// values at T=1 (same ascending tap order over the same window) — the split exists so decode
208/// and the spec verify keep one dispatch class, per the cu/hybrid.cu decode==verify law.
209#[derive(Clone, Copy, PartialEq, Eq)]
210pub(crate) enum ConvArm {
211 Prefill,
212 Decode,
213}
214
215/// The scan-input buffers of one KDA step, STOLEN from the step instead of dropped
216/// (lane/glm5-loop-port, port 3 — the module doc's named GdnStash/ReplaySSM diet): the
217/// glm5 verify walk's rollback checkpoint keeps these ~160 KB of already-allocated
218/// buffers per row per layer and retires the per-row 4 MiB recurrent-state clones
219/// (~0.95 GiB transient at K=7). Replaying `kda_scan` over them from a pre-round state
220/// snapshot rebuilds the post-row state EXACTLY: each replay is the ORIGINAL t=1 launch
221/// re-issued — same kernel, same inputs, same shape — so the rebuilt state is
222/// byte-identical to the clone it replaces by construction, not by a numeric argument.
223pub struct KdaScanInputs {
224 pub q: CudaSlice<f32>,
225 pub k: CudaSlice<f32>,
226 pub v: CudaSlice<f32>,
227 pub g: CudaSlice<f32>,
228 pub beta: CudaSlice<f32>,
229}
230
231/// The rollback stash of one BATCHED verify-rows KDA call (lane/glm5-verify-batch): the
232/// per-layer t=K+1 twin of the per-row [`KdaScanInputs`] steal. Everything here is either
233/// stolen from buffers the call allocated anyway (`raws`, `scan` — zero copies) or one
234/// small clone per layer per round (`ring_snap`, `3*qkv*(kernel-1)` floats ~ 96 KiB).
235///
236/// Rollback to `keep` rows rebuilds both state planes EXACTLY:
237/// * conv ring: restore `ring_snap`, then re-issue `kda_conv_ring_roll` per plane over
238/// `raws` at T=keep — the roll is pure placement (no arithmetic), so the rebuilt ring
239/// is the sequential chain's ring after row keep-1 byte-for-byte.
240/// * ssm state: ONE `kda_scan` replay at T=keep from the caller's pre-round snapshot
241/// over the batched `scan` inputs (the kernel walks rows 0..keep of the [t, ..]
242/// buffers) — the in-kernel T-loop IS the chained t=1 program (register-resident
243/// state, identical per-step order), held by the scan-chain bit-gate.
244pub struct KdaRowsStash {
245 /// The fused conv ring BEFORE this call's rolls (one clone per layer per round).
246 pub ring_snap: CudaSlice<f32>,
247 /// RAW (pre-conv) q/k/v projection rows `[t, qkv]`, stolen post-roll (plane order).
248 pub raws: [CudaSlice<f32>; 3],
249 /// Batched scan inputs `[t, ..]`, stolen post-scan.
250 pub scan: KdaScanInputs,
251 /// Row count of the call that filled this stash; rollback validates `keep` against it.
252 pub rows: usize,
253}
254
255/// What a `kda_core` call is asked to leave behind for rollback — and, for `Rows`, which
256/// matmul class the call rides (the decode-exact rows classes, `matmul_rows_exact`).
257pub(crate) enum KdaStash<'a> {
258 /// No rollback stash (prefill / plain decode).
259 None,
260 /// Per-row t=1 steal (loop-port 3, the per-row verify walk).
261 Decode(&'a mut Option<KdaScanInputs>),
262 /// BATCHED verify-rows steal (lane/glm5-verify-batch): scan inputs + raw conv rows +
263 /// a pre-call ring snapshot; every matmul rides `matmul_rows_exact` so each row is
264 /// bit-identical to the t=1 decode program per the decode-exact class contracts.
265 Rows(&'a mut Option<KdaRowsStash>),
266}
267
268/// The whole mixer, stage for stage against `memra_reference::kimi_delta_net`.
269///
270/// `ring` is the fused `[3*qkv, kernel-1]` conv state (zeroed = fresh prefill's zero left pad)
271/// and is updated in place. `state_in`/`state_out` are the `[heads, 128, 128]` recurrent state
272/// in the kernel's transposed `M[col][i]` layout; they MUST be distinct buffers.
273#[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
274fn kda_core(
275 e: &Engine,
276 la: &KdaAttnLayer,
277 x: &CudaSlice<f32>,
278 t: usize,
279 eps: f32,
280 ring: &mut CudaSlice<f32>,
281 state_in: &CudaSlice<f32>,
282 state_out: &mut CudaSlice<f32>,
283 arm: ConvArm,
284 stash: KdaStash<'_>,
285 scan_clock: Option<&mut u64>,
286) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
287 // glm5 TP fail-closed choke point: every plain KDA entry (stateless, prime, decode,
288 // stash — INCLUDING the batched verify-rows walk, `kda_verify_rows_cached`) funnels
289 // through here. A TP-sharded layer holds heads/2 — running it on the plain path would
290 // compute a silently-halved mixer, so it refuses by name instead.
291 if la.tp.is_some() {
292 return Err(format!(
293 "KDA layer is glm5-TP-sharded (MEMRA_GLM5_TP): the plain mixer path is unwired \
294 for a head shard — only the TP decode/prime walk may execute it (t={t}, arm \
295 {})",
296 if arm == ConvArm::Decode {
297 "decode"
298 } else {
299 "prefill"
300 }
301 )
302 .into());
303 }
304 // Verify-batch wo seam (lane/glm5-verify-batch): the rows arm routes the output
305 // projection through the decode-exact classes, exactly like every projection inside
306 // the core — the wo dispatch moved into this wrapper with the TP split, its routing
307 // did not change.
308 let rows_exact = matches!(stash, KdaStash::Rows(_));
309 let gated = kda_core_gated(
310 e, la, x, t, eps, ring, state_in, state_out, arm, stash, scan_clock,
311 )?;
312 if rows_exact {
313 let y = e.matmul_rows_exact(&la.wo, &gated, t);
314 // Door W: gated's last reader was the wo matmul above.
315 e.vws_recycle(gated);
316 y
317 } else {
318 e.matmul(&la.wo, &gated, t)
319 }
320}
321
322/// [`kda_core`] up to (and excluding) the output projection: returns the gated `[t, qkv]`
323/// mixer output. Split out for the glm5 TP-2 seam, whose column-parallel `wo` runs over the
324/// cross-rank GATHERED gated tensor rather than this shard's slice — the plain path is
325/// `kda_core` above, byte-for-byte the pre-split body (the wo matmul and its rows-exact
326/// routing moved, nothing else). This body is the CURRENT doored/batched core: it carries
327/// the `MEMRA_KDA_FUSED_PROJ` door and the verify-batch rows arm; the TP decode/prime walk
328/// calls it with `KdaStash::None`, the spec x TP verify walk (lane/glm5-composition) with
329/// `KdaStash::Rows` per rank, and the TP load preflight refuses the fused-proj door by
330/// name (unproven composition on head shards — see the FLAGS.md composition matrix).
331#[allow(clippy::too_many_arguments)] // mirrors kda_core's own contract-shaped list
332pub(crate) fn kda_core_gated(
333 e: &Engine,
334 la: &KdaAttnLayer,
335 x: &CudaSlice<f32>,
336 t: usize,
337 eps: f32,
338 ring: &mut CudaSlice<f32>,
339 state_in: &CudaSlice<f32>,
340 state_out: &mut CudaSlice<f32>,
341 arm: ConvArm,
342 stash: KdaStash<'_>,
343 mut scan_clock: Option<&mut u64>,
344) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
345 let heads = la.heads();
346 let head_dim = la.head_dim();
347 let qkv = la.qkv();
348 let kernel = la.conv_kernel();
349 // The BATCHED verify-rows arm (lane/glm5-verify-batch): prefill conv dispatch (per-row
350 // bit-identical to the decode arm — same ascending taps over the same window values,
351 // held by the conv-arm bit-gate) + decode-exact matmul classes + the rows stash.
352 let rows_exact = matches!(stash, KdaStash::Rows(_));
353 if rows_exact && arm != ConvArm::Prefill {
354 return Err("KDA rows stash requires the prefill conv arm".into());
355 }
356 if arm == ConvArm::Decode && t != 1 {
357 return Err(format!("KDA decode arm requires t == 1, got {t}").into());
358 }
359 if ring.len() < la.conv_width() * (kernel - 1) {
360 return Err(format!(
361 "KDA conv ring holds {} floats, layer needs {}",
362 ring.len(),
363 la.conv_width() * (kernel - 1)
364 )
365 .into());
366 }
367 if state_in.len() < la.state_width() || state_out.len() < la.state_width() {
368 return Err(format!(
369 "KDA recurrent state holds {}/{} floats, layer needs {}",
370 state_in.len(),
371 state_out.len(),
372 la.state_width()
373 )
374 .into());
375 }
376
377 // Stage 1 — the six projections that read x directly. f_b/g_b are chained off their own
378 // down-projections below, exactly as the reference nests them.
379 //
380 // MEMRA_KDA_FUSED_PROJ=1 (default OFF): the six matvec calls collapse to one quantize +
381 // one `qmatvec_kda6_q8f32_mmvq` launch — the program shape both vLLM and SGLang ship for
382 // this trunk (ENGINE-SURVEY.md C1) and the step37 QKV_FUSED transfer (TRANSFER-MAP lever 1).
383 // `kda_proj_fused6` refuses (returns None) on any operand/env shape where its bit-identity
384 // claim would not hold, so the fall-through arm is always the unchanged program.
385 let mut g6 = match e.kda_proj_fused6(la, x, t)? {
386 Some(outs) => outs,
387 None if rows_exact => {
388 // Verify-rows matmul class: per-weight decode-exact dispatch (the tcols /
389 // batched-MMVQ / per-token-linear classes — each row bit-identical to the
390 // t=1 program by the matmul_rows_exact contract).
391 [&la.wq, &la.wk, &la.wv, &la.f_a, &la.g_a, &la.b_proj]
392 .into_iter()
393 .map(|w| e.matmul_rows_exact(w, x, t))
394 .collect::<Result<Vec<_>, _>>()?
395 }
396 None => e.matmul_group(
397 &[&la.wq, &la.wk, &la.wv, &la.f_a, &la.g_a, &la.b_proj],
398 x,
399 t,
400 )?,
401 };
402 let beta_raw = g6.pop().unwrap(); // [T, heads]
403 let gate_down = g6.pop().unwrap(); // [T, head_dim]
404 let forget_down = g6.pop().unwrap(); // [T, head_dim]
405 let v_raw = g6.pop().unwrap(); // [T, qkv]
406 let k_raw = g6.pop().unwrap();
407 let q_raw = g6.pop().unwrap();
408
409 // Rows stash: snapshot the ring BEFORE the rolls mutate it (one ~96 KiB clone per
410 // layer per round — the rollback's re-roll base). Door W: on the rows arm the snapshot
411 // (and every scratch below) is a pooled draw — vws_uninit == alloc_uninit with the
412 // door off, and the non-rows arms keep the plain allocs untouched.
413 let ring_snap = match &stash {
414 KdaStash::Rows(_) => {
415 let mut snap = e.vws_uninit(ring.len())?;
416 e.dtod_copy_into(ring, &mut snap, 0)?;
417 Some(snap)
418 }
419 _ => None,
420 };
421
422 // Stage 2 — per-plane causal short conv + SiLU. Planes are ordered q, k, v in both the fused
423 // weight buffer and the fused ring, which is the order the reference stores conv_state in.
424 let mut q_conv = if rows_exact {
425 e.vws_uninit(t * qkv)?
426 } else {
427 e.uninit(t * qkv)?
428 };
429 let mut k_conv = if rows_exact {
430 e.vws_uninit(t * qkv)?
431 } else {
432 e.uninit(t * qkv)?
433 };
434 let mut v_conv = if rows_exact {
435 e.vws_uninit(t * qkv)?
436 } else {
437 e.uninit(t * qkv)?
438 };
439 for (plane, (raw, out)) in [
440 (&q_raw, &mut q_conv),
441 (&k_raw, &mut k_conv),
442 (&v_raw, &mut v_conv),
443 ]
444 .into_iter()
445 .enumerate()
446 {
447 match arm {
448 ConvArm::Prefill => e.kda_conv_silu(raw, &la.conv, ring, out, qkv, t, kernel, plane)?,
449 ConvArm::Decode => {
450 e.kda_conv_silu_decode(raw, ring, &la.conv, out, qkv, kernel, plane)?
451 }
452 }
453 }
454 // The prefill arm reads the OLD ring for every token, so the roll runs only after all three
455 // planes have been convolved. The decode arm already rolled inside its fused kernel.
456 if arm == ConvArm::Prefill {
457 for (plane, raw) in [&q_raw, &k_raw, &v_raw].into_iter().enumerate() {
458 e.kda_conv_ring_roll(raw, ring, qkv, t, kernel, plane)?;
459 }
460 }
461
462 // Stage 3 — q/k L2 norm over head_dim (eps INSIDE the sqrt, fixed 1e-6). Rows of the
463 // token-major layout are contiguous head_dim runs, so no repack is needed.
464 let mut q_l2 = if rows_exact {
465 e.vws_uninit(t * qkv)?
466 } else {
467 e.uninit(t * qkv)?
468 };
469 let mut k_l2 = if rows_exact {
470 e.vws_uninit(t * qkv)?
471 } else {
472 e.uninit(t * qkv)?
473 };
474 e.l2_norm(&q_conv, &mut q_l2, head_dim, t * heads, KDA_L2_EPS)?;
475 e.l2_norm(&k_conv, &mut k_l2, head_dim, t * heads, KDA_L2_EPS)?;
476 // Door W: the convs' last readers were the l2 norms (the ring rolls read the raws).
477 if rows_exact {
478 e.vws_recycle(q_conv);
479 e.vws_recycle(k_conv);
480 }
481
482 // Stage 4 — gates. forget: g = lower_bound * sigmoid(exp(A_log[h]) * (f_b(f_a(x)) + dt_bias)),
483 // emitted RAW (the scan applies expf). beta: per-head sigmoid of its own projection.
484 let forget = if rows_exact {
485 e.matmul_rows_exact(&la.f_b, &forget_down, t)?
486 } else {
487 e.matmul(&la.f_b, &forget_down, t)?
488 };
489 let mut g_log = if rows_exact {
490 e.vws_uninit(t * qkv)?
491 } else {
492 e.uninit(t * qkv)?
493 };
494 e.kda_gate(
495 &forget,
496 la.dt_bias.float_data(),
497 la.a_log.float_data(),
498 &mut g_log,
499 qkv,
500 t,
501 head_dim,
502 la.plan.gate_lower_bound,
503 )?;
504 let mut beta = if rows_exact {
505 e.vws_uninit(t * heads)?
506 } else {
507 e.uninit(t * heads)?
508 };
509 e.sigmoid(&beta_raw, &mut beta, t * heads)?;
510 // Door W: forget_down's last reader was the f_b matmul, forget's the gate kernel,
511 // beta_raw's the sigmoid.
512 if rows_exact {
513 e.vws_recycle(forget_down);
514 e.vws_recycle(forget);
515 e.vws_recycle(beta_raw);
516 }
517
518 // Stage 5 — the delta-rule recurrence. `scale` carries the reference's head_dim^-0.5 query
519 // scale: q feeds only the readout, never the state, so scaling the readout is exact.
520 // At t > 1 the kernel walks the T steps IN-KERNEL over register-resident state — the
521 // sequential chain preserved inside ONE launch (chained-t=1 identity by construction,
522 // held by the scan-chain bit-gate). `scan_clock` is the trace-level-2 instrument: it
523 // drains the stream around the launch so the sequential-class share lands in its own
524 // bucket (shares, never walls).
525 let scale = 1.0 / (head_dim as f32).sqrt();
526 let mut core = if rows_exact {
527 e.vws_uninit(t * qkv)?
528 } else {
529 e.uninit(t * qkv)?
530 };
531 let scan_t0 = scan_clock.as_ref().map(|_| {
532 let _ = e.stream().synchronize();
533 std::time::Instant::now()
534 });
535 e.kda_scan(
536 &q_l2, &k_l2, &v_conv, &g_log, &beta, state_in, state_out, &mut core, heads, t, scale,
537 )?;
538 if let (Some(ns), Some(t0)) = (scan_clock.take(), scan_t0) {
539 let _ = e.stream().synchronize();
540 *ns += t0.elapsed().as_nanos() as u64;
541 }
542
543 // Stage 6 — sigmoid-gated RMSNorm over head_dim (layer rms eps here, NOT the l2 eps), then
544 // the output projection.
545 let gate = if rows_exact {
546 e.matmul_rows_exact(&la.g_b, &gate_down, t)?
547 } else {
548 e.matmul(&la.g_b, &gate_down, t)?
549 };
550 let mut gated = if rows_exact {
551 e.vws_uninit(t * qkv)?
552 } else {
553 e.uninit(t * qkv)?
554 };
555 e.kda_gated_rmsnorm(
556 &core,
557 la.o_norm.float_data(),
558 &gate,
559 &mut gated,
560 head_dim,
561 t * heads,
562 eps,
563 )?;
564 // Door W: gate_down's last reader was the g_b matmul; core's and gate's the
565 // gated-rmsnorm above.
566 if rows_exact {
567 e.vws_recycle(gate_down);
568 e.vws_recycle(gate);
569 e.vws_recycle(core);
570 }
571 // Steal the scan/conv inputs for the caller's rollback stash: stage 5 has consumed
572 // the scan inputs and the rolls were the raws' last readers — moving them out is
573 // free (no copy, no launch; the buffers were allocated this call either way).
574 match stash {
575 KdaStash::None => {}
576 KdaStash::Decode(s) => {
577 *s = Some(KdaScanInputs {
578 q: q_l2,
579 k: k_l2,
580 v: v_conv,
581 g: g_log,
582 beta,
583 });
584 }
585 KdaStash::Rows(s) => {
586 // Door W: the PREVIOUS round's stash dies here — its nine buffers restock
587 // the pool instead of falling to nine async frees (per layer per round).
588 if let Some(old) = s.take() {
589 e.vws_recycle(old.ring_snap);
590 for r in old.raws {
591 e.vws_recycle(r);
592 }
593 e.vws_recycle(old.scan.q);
594 e.vws_recycle(old.scan.k);
595 e.vws_recycle(old.scan.v);
596 e.vws_recycle(old.scan.g);
597 e.vws_recycle(old.scan.beta);
598 }
599 *s = Some(KdaRowsStash {
600 ring_snap: ring_snap.expect("rows arm snapshotted the ring above"),
601 raws: [q_raw, k_raw, v_raw],
602 scan: KdaScanInputs {
603 q: q_l2,
604 k: k_l2,
605 v: v_conv,
606 g: g_log,
607 beta,
608 },
609 rows: t,
610 });
611 }
612 }
613 Ok(gated)
614}
615
616/// STATELESS prefill from a zero conv ring and a zero recurrent state — the arm the logits-only
617/// forward paths take. Allocates and discards both state buffers.
618pub fn kda_attn(
619 e: &Engine,
620 la: &KdaAttnLayer,
621 x: &CudaSlice<f32>,
622 t: usize,
623 eps: f32,
624) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
625 let mut ring = e.zeros(la.conv_width() * (la.conv_kernel() - 1))?;
626 let state_in = e.zeros(la.state_width())?;
627 let mut state_out = e.zeros(la.state_width())?;
628 kda_core(
629 e,
630 la,
631 x,
632 t,
633 eps,
634 &mut ring,
635 &state_in,
636 &mut state_out,
637 ConvArm::Prefill,
638 KdaStash::None,
639 None,
640 )
641}
642
643/// STATEFUL prefill: carries the ring forward and advances the recurrent state from `state_in`
644/// into `state_out`. Callers own the ping-pong; the two state buffers must be distinct.
645#[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
646pub fn kda_attn_prime(
647 e: &Engine,
648 la: &KdaAttnLayer,
649 x: &CudaSlice<f32>,
650 t: usize,
651 eps: f32,
652 ring: &mut CudaSlice<f32>,
653 state_in: &CudaSlice<f32>,
654 state_out: &mut CudaSlice<f32>,
655) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
656 kda_core(
657 e,
658 la,
659 x,
660 t,
661 eps,
662 ring,
663 state_in,
664 state_out,
665 ConvArm::Prefill,
666 KdaStash::None,
667 None,
668 )
669}
670
671/// T=1 decode step. Same math as a one-token prime; separate conv arm so the fused
672/// assemble+conv+roll kernel keeps decode and the spec verify on one dispatch class.
673pub fn kda_attn_decode(
674 e: &Engine,
675 la: &KdaAttnLayer,
676 x: &CudaSlice<f32>,
677 eps: f32,
678 ring: &mut CudaSlice<f32>,
679 state_in: &CudaSlice<f32>,
680 state_out: &mut CudaSlice<f32>,
681) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
682 kda_core(
683 e,
684 la,
685 x,
686 1,
687 eps,
688 ring,
689 state_in,
690 state_out,
691 ConvArm::Decode,
692 KdaStash::None,
693 None,
694 )
695}
696
697/// Stateful KDA against the shared recurrent-state carrier, in the eager GDN discipline: the
698/// scan reads `ssm_state` and writes the spare `ssm_state_alt`, then the two OWNED resident
699/// buffers swap in place. Stable pointers, no per-step alloc/free — the per-step scratch this
700/// replaced churned the stream-ordered pool and made decode run-to-run nondeterministic
701/// (crates/memra-kv `RecurLayer::ssm_state_alt`). NOT capture-safe: a captured graph bakes
702/// capture-time pointers and never re-runs the host swap, which is why the capture loops refuse.
703#[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
704fn kda_cached(
705 e: &Engine,
706 la: &KdaAttnLayer,
707 x: &CudaSlice<f32>,
708 t: usize,
709 eps: f32,
710 cache: &mut Cache,
711 il: usize,
712 arm: ConvArm,
713 stash: KdaStash<'_>,
714 scan_clock: Option<&mut u64>,
715) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
716 let rl = cache.recur[il].as_mut().ok_or_else(|| {
717 format!(
718 "blk.{il}: KDA layer has no recurrent state — the cache allocator saw a \
719 non-Recurrent StatePlan for a KDA layer"
720 )
721 })?;
722 let out = {
723 let RecurLayer {
724 conv_state,
725 ssm_state,
726 ssm_state_alt,
727 } = rl;
728 kda_core(
729 e,
730 la,
731 x,
732 t,
733 eps,
734 conv_state,
735 ssm_state,
736 ssm_state_alt,
737 arm,
738 stash,
739 scan_clock,
740 )?
741 };
742 std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
743 Ok(out)
744}
745
746/// Stateful prefill of `t` tokens through the cache's KDA state for layer `il`.
747pub fn kda_prime_cached(
748 e: &Engine,
749 la: &KdaAttnLayer,
750 x: &CudaSlice<f32>,
751 t: usize,
752 eps: f32,
753 cache: &mut Cache,
754 il: usize,
755) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
756 kda_cached(
757 e,
758 la,
759 x,
760 t,
761 eps,
762 cache,
763 il,
764 ConvArm::Prefill,
765 KdaStash::None,
766 None,
767 )
768}
769
770/// One decode step through the cache's KDA state for layer `il`.
771pub fn kda_decode_cached(
772 e: &Engine,
773 la: &KdaAttnLayer,
774 x: &CudaSlice<f32>,
775 eps: f32,
776 cache: &mut Cache,
777 il: usize,
778) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
779 kda_cached(
780 e,
781 la,
782 x,
783 1,
784 eps,
785 cache,
786 il,
787 ConvArm::Decode,
788 KdaStash::None,
789 None,
790 )
791}
792
793/// [`kda_decode_cached`] with the step's scan inputs STOLEN for a rollback stash
794/// (loop-port 3; doc on [`KdaScanInputs`]). Identical launches — the steal is a move of
795/// buffers the step allocated either way.
796pub fn kda_decode_cached_stash(
797 e: &Engine,
798 la: &KdaAttnLayer,
799 x: &CudaSlice<f32>,
800 eps: f32,
801 cache: &mut Cache,
802 il: usize,
803) -> Result<(CudaSlice<f32>, KdaScanInputs), Box<dyn std::error::Error>> {
804 let mut stash: Option<KdaScanInputs> = None;
805 let out = kda_cached(
806 e,
807 la,
808 x,
809 1,
810 eps,
811 cache,
812 il,
813 ConvArm::Decode,
814 KdaStash::Decode(&mut stash),
815 None,
816 )?;
817 let stash = stash.ok_or("kda_core returned without filling the requested scan stash")?;
818 Ok((out, stash))
819}
820
821/// THE BATCHED VERIFY-ROWS KDA CALL (lane/glm5-verify-batch): one t=K+1 `kda_core` pass
822/// per layer per round, replacing t per-row [`kda_decode_cached_stash`] calls. Projections,
823/// gates and norms batch m=t through the decode-exact matmul classes (`matmul_rows_exact`);
824/// the conv takes the prefill dispatch (per-token bit-identical to the decode arm's taps);
825/// the recurrence stays SEQUENTIAL inside one `memra_kda_scan_s128` launch (the in-kernel
826/// T-loop over register-resident state == the chained t=1 program). Per-row bit-identity
827/// vs the t=1 chain is held by the walk gates (`glm5_tparallel_verify_gpu`) and the
828/// kernel bit-gates (`glm5_verify_batch_gpu`).
829///
830/// The caller owns the pre-round ssm snapshot (`Glm5VerifyCkpt::kda_ssm_snap`, cloned
831/// BEFORE this call); the returned [`KdaRowsStash`] carries everything else rollback
832/// needs. `scan_clock`: the trace-level-2 sequential-class bucket (ns accumulated around
833/// the scan launch with stream drains — an instrument, never a serving mode).
834#[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kda_cached call contract plus the trace clock
835pub fn kda_verify_rows_cached(
836 e: &Engine,
837 la: &KdaAttnLayer,
838 x: &CudaSlice<f32>,
839 t: usize,
840 eps: f32,
841 cache: &mut Cache,
842 il: usize,
843 scan_clock: Option<&mut u64>,
844) -> Result<(CudaSlice<f32>, KdaRowsStash), Box<dyn std::error::Error>> {
845 let mut stash: Option<KdaRowsStash> = None;
846 let out = kda_cached(
847 e,
848 la,
849 x,
850 t,
851 eps,
852 cache,
853 il,
854 ConvArm::Prefill,
855 KdaStash::Rows(&mut stash),
856 scan_clock,
857 )?;
858 let stash = stash.ok_or("kda_core returned without filling the requested rows stash")?;
859 Ok((out, stash))
860}
861
862/// Roll layer `il` back to "after row `keep-1`" from a BATCHED verify-rows round
863/// (lane/glm5-verify-batch; the [`KdaRowsStash`] doc states the two-plane contract):
864/// restore the pre-round conv ring and re-roll `keep` raw rows (pure placement), then
865/// replay the scan ONCE at T=keep from the pre-round ssm snapshot over the batched
866/// inputs. Full accept (`keep == rows`) never calls this — the resident state IS the
867/// state after the last kept row.
868pub fn kda_verify_rollback_rows(
869 e: &Engine,
870 la: &KdaAttnLayer,
871 snap: &CudaSlice<f32>,
872 stash: &KdaRowsStash,
873 keep: usize,
874 cache: &mut Cache,
875 il: usize,
876) -> Result<(), Box<dyn std::error::Error>> {
877 let rl = cache.recur[il]
878 .as_mut()
879 .ok_or_else(|| format!("blk.{il}: KDA rows rollback on a layer with no recurrent state"))?;
880 kda_verify_rollback_rows_on(e, la, snap, stash, keep, rl, il)
881}
882
883/// [`kda_verify_rollback_rows`] over a CALLER-OWNED state plane — the glm5 spec x TP seam
884/// (lane/glm5-composition): under `MEMRA_GLM5_TP` each rank's shard-geometry conv ring +
885/// ssm ping-pong lives in `cache.glm5_tp_recur[il][rank]` on that rank's engine, so the
886/// rollback restores per rank through this entry with the rank's own `(engine, shard,
887/// snapshot, stash)` tuple. The cache wrapper above delegates here — one body, byte-for-byte
888/// the pre-refactor walk on the plain path.
889pub fn kda_verify_rollback_rows_on(
890 e: &Engine,
891 la: &KdaAttnLayer,
892 snap: &CudaSlice<f32>,
893 stash: &KdaRowsStash,
894 keep: usize,
895 rl: &mut RecurLayer,
896 il: usize,
897) -> Result<(), Box<dyn std::error::Error>> {
898 if keep == 0 || keep >= stash.rows {
899 return Err(format!(
900 "blk.{il}: KDA rows rollback keep={keep} outside 1..{} (full accept keeps the \
901 resident state and never replays)",
902 stash.rows
903 )
904 .into());
905 }
906 let qkv = la.qkv();
907 let kernel = la.conv_kernel();
908 let heads = la.heads();
909 let scale = 1.0 / (la.head_dim() as f32).sqrt();
910 // Conv ring: pre-round snapshot back, then re-roll the kept raw rows per plane. The
911 // roll kernel reads every old slot into registers before any store, so T=keep < pad
912 // mixes snapshot slots and kept rows exactly as the sequential chain's rolls did.
913 e.copy_into(
914 &mut rl.conv_state,
915 0,
916 &stash.ring_snap,
917 stash.ring_snap.len(),
918 )?;
919 for (plane, raw) in stash.raws.iter().enumerate() {
920 e.kda_conv_ring_roll(raw, &mut rl.conv_state, qkv, keep, kernel, plane)?;
921 }
922 // Recurrent state: ONE T=keep replay from the snapshot over the batched scan inputs
923 // (the kernel walks rows 0..keep of the [t, ..] buffers); readout discarded. The
924 // ping-pong ends with the rebuilt state under the `ssm_state` name, matching
925 // `kda_cached`'s swap discipline.
926 let mut o = e.uninit(keep * qkv)?;
927 {
928 let RecurLayer {
929 ssm_state: _,
930 ssm_state_alt,
931 ..
932 } = rl;
933 e.kda_scan(
934 &stash.scan.q,
935 &stash.scan.k,
936 &stash.scan.v,
937 &stash.scan.g,
938 &stash.scan.beta,
939 snap,
940 ssm_state_alt,
941 &mut o,
942 heads,
943 keep,
944 scale,
945 )?;
946 }
947 std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
948 Ok(())
949}
950
951/// Rebuild layer `il`'s recurrent state to "after row `inputs.len()-1`" by REPLAYING the
952/// stashed scan inputs from the pre-round snapshot `snap` (loop-port 3, the module-doc
953/// diet made concrete): each replay is the original t=1 `memra_kda_scan_s128` launch
954/// re-issued over the very buffers that step consumed, so the rebuilt state is
955/// byte-identical to the per-row clone it replaces BY CONSTRUCTION. The readout is
956/// discarded; the conv ring is not touched (the walk still clones it per row — 288 KiB
957/// against the 4 MiB ssm plane this retires). The ping-pong rides the resident pair and
958/// ends with the rebuilt state under the `ssm_state` name, matching `kda_cached`'s own
959/// swap discipline.
960pub fn kda_scan_replay(
961 e: &Engine,
962 la: &KdaAttnLayer,
963 snap: &CudaSlice<f32>,
964 inputs: &[KdaScanInputs],
965 cache: &mut Cache,
966 il: usize,
967) -> Result<(), Box<dyn std::error::Error>> {
968 if inputs.is_empty() {
969 return Err(format!(
970 "blk.{il}: KDA replay needs at least one stashed row (rollback keep >= 1; a \
971 restore TO the snapshot itself is a different contract)"
972 )
973 .into());
974 }
975 if la.tp.is_some() {
976 return Err(format!(
977 "blk.{il}: KDA scan replay (the PER-ROW rollback seam) is unwired for a \
978 glm5-TP-sharded layer — the spec x TP composition requires the BATCHED \
979 verify walk, whose rollback rides kda_verify_rollback_rows_on per rank"
980 )
981 .into());
982 }
983 let heads = la.heads();
984 let scale = 1.0 / (la.head_dim() as f32).sqrt();
985 let qkv = la.qkv();
986 let rl = cache.recur[il]
987 .as_mut()
988 .ok_or_else(|| format!("blk.{il}: KDA replay on a layer with no recurrent state"))?;
989 let mut o = e.uninit(qkv)?; // discarded readout scratch, reused across rows
990 for (r, inp) in inputs.iter().enumerate() {
991 {
992 let RecurLayer {
993 ssm_state,
994 ssm_state_alt,
995 ..
996 } = rl;
997 let state_in: &CudaSlice<f32> = if r == 0 { snap } else { ssm_state };
998 e.kda_scan(
999 &inp.q,
1000 &inp.k,
1001 &inp.v,
1002 &inp.g,
1003 &inp.beta,
1004 state_in,
1005 ssm_state_alt,
1006 &mut o,
1007 heads,
1008 1,
1009 scale,
1010 )?;
1011 }
1012 std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
1013 }
1014 Ok(())
1015}
1016
1017impl Engine {
1018 /// Per-plane causal short conv + SiLU over a T-token chunk (cu/kda.cu).
1019 #[allow(clippy::too_many_arguments)]
1020 pub fn kda_conv_silu(
1021 &self,
1022 x_tm: &CudaSlice<f32>,
1023 w: &CudaSlice<f32>,
1024 ring: &CudaSlice<f32>,
1025 y_tm: &mut CudaSlice<f32>,
1026 qkv: usize,
1027 t: usize,
1028 kernel: usize,
1029 plane: usize,
1030 ) -> Result<(), Box<dyn std::error::Error>> {
1031 let f = self.func("memra_kda_conv_silu_f32");
1032 let cfg = LaunchConfig {
1033 grid_dim: (qkv.div_ceil(256) as u32, t as u32, 1),
1034 block_dim: (256, 1, 1),
1035 shared_mem_bytes: 0,
1036 };
1037 let (n, tt, k, p) = (qkv as i32, t as i32, kernel as i32, plane as i32);
1038 let stream = self.gpu.stream();
1039 let mut b = stream.launch_builder(&f);
1040 b.arg(x_tm)
1041 .arg(w)
1042 .arg(ring)
1043 .arg(&mut *y_tm)
1044 .arg(&n)
1045 .arg(&tt)
1046 .arg(&k)
1047 .arg(&p);
1048 unsafe { b.launch(cfg)? };
1049 Ok(())
1050 }
1051
1052 /// Roll one plane of the fused conv ring forward over a T-token chunk (cu/kda.cu).
1053 pub fn kda_conv_ring_roll(
1054 &self,
1055 x_tm: &CudaSlice<f32>,
1056 ring: &mut CudaSlice<f32>,
1057 qkv: usize,
1058 t: usize,
1059 kernel: usize,
1060 plane: usize,
1061 ) -> Result<(), Box<dyn std::error::Error>> {
1062 let f = self.func("memra_kda_conv_ring_roll_f32");
1063 let cfg = LaunchConfig {
1064 grid_dim: (qkv.div_ceil(256) as u32, 1, 1),
1065 block_dim: (256, 1, 1),
1066 shared_mem_bytes: 0,
1067 };
1068 let (n, tt, k, p) = (qkv as i32, t as i32, kernel as i32, plane as i32);
1069 let stream = self.gpu.stream();
1070 let mut b = stream.launch_builder(&f);
1071 b.arg(x_tm).arg(&mut *ring).arg(&n).arg(&tt).arg(&k).arg(&p);
1072 unsafe { b.launch(cfg)? };
1073 Ok(())
1074 }
1075
1076 /// T=1 fused assemble + conv + SiLU + ring roll for one plane (cu/kda.cu).
1077 #[allow(clippy::too_many_arguments)]
1078 pub fn kda_conv_silu_decode(
1079 &self,
1080 x_new: &CudaSlice<f32>,
1081 ring: &mut CudaSlice<f32>,
1082 w: &CudaSlice<f32>,
1083 y: &mut CudaSlice<f32>,
1084 qkv: usize,
1085 kernel: usize,
1086 plane: usize,
1087 ) -> Result<(), Box<dyn std::error::Error>> {
1088 let f = self.func("memra_kda_conv_silu_decode_f32");
1089 let cfg = LaunchConfig {
1090 grid_dim: (qkv.div_ceil(256) as u32, 1, 1),
1091 block_dim: (256, 1, 1),
1092 shared_mem_bytes: 0,
1093 };
1094 let (n, k, p) = (qkv as i32, kernel as i32, plane as i32);
1095 let stream = self.gpu.stream();
1096 let mut b = stream.launch_builder(&f);
1097 b.arg(x_new)
1098 .arg(&mut *ring)
1099 .arg(w)
1100 .arg(&mut *y)
1101 .arg(&n)
1102 .arg(&k)
1103 .arg(&p);
1104 unsafe { b.launch(cfg)? };
1105 Ok(())
1106 }
1107
1108 /// Per-channel forget gate, emitted as the RAW log-gate (cu/kda.cu).
1109 #[allow(clippy::too_many_arguments)]
1110 pub fn kda_gate(
1111 &self,
1112 forget: &CudaSlice<f32>,
1113 dt_bias: &CudaSlice<f32>,
1114 a_log: &CudaSlice<f32>,
1115 g: &mut CudaSlice<f32>,
1116 qkv: usize,
1117 t: usize,
1118 head_dim: usize,
1119 lower_bound: f32,
1120 ) -> Result<(), Box<dyn std::error::Error>> {
1121 let f = self.func("memra_kda_gate_f32");
1122 let cfg = LaunchConfig {
1123 grid_dim: (qkv.div_ceil(256) as u32, t as u32, 1),
1124 block_dim: (256, 1, 1),
1125 shared_mem_bytes: 0,
1126 };
1127 let (n, tt, hd, lb) = (qkv as i32, t as i32, head_dim as i32, lower_bound);
1128 let stream = self.gpu.stream();
1129 let mut b = stream.launch_builder(&f);
1130 b.arg(forget)
1131 .arg(dt_bias)
1132 .arg(a_log)
1133 .arg(&mut *g)
1134 .arg(&n)
1135 .arg(&tt)
1136 .arg(&hd)
1137 .arg(&lb);
1138 unsafe { b.launch(cfg)? };
1139 Ok(())
1140 }
1141
1142 /// The per-channel-decay delta-rule scan (cu/kda.cu). One warp per output column.
1143 #[allow(clippy::too_many_arguments)]
1144 pub fn kda_scan(
1145 &self,
1146 q: &CudaSlice<f32>,
1147 k: &CudaSlice<f32>,
1148 v: &CudaSlice<f32>,
1149 g: &CudaSlice<f32>,
1150 beta: &CudaSlice<f32>,
1151 state_in: &CudaSlice<f32>,
1152 state_out: &mut CudaSlice<f32>,
1153 o: &mut CudaSlice<f32>,
1154 heads: usize,
1155 t: usize,
1156 scale: f32,
1157 ) -> Result<(), Box<dyn std::error::Error>> {
1158 // Four columns per block keeps one warp per column at 128 threads, the same shape
1159 // gdn_scan_s128 launches with.
1160 const COLS_PER_BLOCK: u32 = 4;
1161 let f = self.func("memra_kda_scan_s128");
1162 let cfg = LaunchConfig {
1163 grid_dim: (
1164 heads as u32,
1165 1,
1166 (KDA_HEAD_DIM as u32).div_ceil(COLS_PER_BLOCK),
1167 ),
1168 block_dim: (32, COLS_PER_BLOCK, 1),
1169 shared_mem_bytes: 0,
1170 };
1171 let (h, tt, s) = (heads as i32, t as i32, scale);
1172 let stream = self.gpu.stream();
1173 let mut b = stream.launch_builder(&f);
1174 b.arg(q)
1175 .arg(k)
1176 .arg(v)
1177 .arg(g)
1178 .arg(beta)
1179 .arg(state_in)
1180 .arg(&mut *state_out)
1181 .arg(&mut *o)
1182 .arg(&h)
1183 .arg(&tt)
1184 .arg(&s);
1185 unsafe { b.launch(cfg)? };
1186 Ok(())
1187 }
1188
1189 /// Sigmoid-gated fp32 RMSNorm over head_dim (cu/kda.cu). GDN's `gated_rmsnorm` gates with
1190 /// SiLU; KDA's Glm5NextTextRMSNormGated hardcodes sigmoid.
1191 #[allow(clippy::too_many_arguments)]
1192 pub fn kda_gated_rmsnorm(
1193 &self,
1194 core: &CudaSlice<f32>,
1195 w: &CudaSlice<f32>,
1196 gate: &CudaSlice<f32>,
1197 dst: &mut CudaSlice<f32>,
1198 ncols: usize,
1199 nrows: usize,
1200 eps: f32,
1201 ) -> Result<(), Box<dyn std::error::Error>> {
1202 let f = self.func("memra_kda_gated_rmsnorm_f32");
1203 let cfg = LaunchConfig {
1204 grid_dim: (nrows as u32, 1, 1),
1205 block_dim: (256, 1, 1),
1206 shared_mem_bytes: 0,
1207 };
1208 let (nc, ep) = (ncols as i32, eps);
1209 let stream = self.gpu.stream();
1210 let mut b = stream.launch_builder(&f);
1211 b.arg(core)
1212 .arg(w)
1213 .arg(gate)
1214 .arg(&mut *dst)
1215 .arg(&nc)
1216 .arg(&ep);
1217 unsafe { b.launch(cfg)? };
1218 Ok(())
1219 }
1220
1221 /// The `MEMRA_KDA_FUSED_PROJ` door: run the KDA stage-1 six-projection group as ONE
1222 /// `quantize_q8_1` + ONE `qmatvec_kda6_q8f32_mmvq` launch, or return `None` and let the
1223 /// caller take the unchanged `matmul_group` arm.
1224 ///
1225 /// ENGAGEMENT IS DELIBERATELY NARROW — every condition below exists so the door's numeric
1226 /// claim stays exactly what the gate proves (`tests/kda_fused_proj_gpu.rs`):
1227 /// * wq/wk/wv must be plain-layout Q8_0 (`rp: false`, no `rp4` mirror, `scale == 1.0`) —
1228 /// the fused kernel's per-(token,row) body is `qmatvec_q8_0_mmvq` VERBATIM, so those
1229 /// rows are BIT-IDENTICAL to the unfused MMVQ/batched arm; a repacked layout would ride
1230 /// the `_rp` twins instead and the claim would be against the wrong kernel.
1231 /// * f_a/g_a/b_proj must be f32 `Float` — their fused rows replace cuBLASLt with a
1232 /// deterministic warp tree: a reduction-order class change (the step37 QKV_FUSED class),
1233 /// measured and pinned in the gate.
1234 /// * t in 1..=15 (the batch cap), and the env classes under which the UNFUSED arm rides
1235 /// the MMVQ-class per-row program: `MEMRA_FAST!=0`, `mmvq_supports(Q8_0)`,
1236 /// `MEMRA_NO_BATCHED` unset for t>=2, `MEMRA_B8!=0` for t>=5. Outside those envs the
1237 /// unfused arm is a different kernel class (dp4a / Stage-A), so the door refuses rather
1238 /// than weakening its identity claim.
1239 ///
1240 /// The flag is read PER CALL (the `MEMRA_MOE_FUSED_EPI` rollback-seam precedent), so both
1241 /// arms alternate inside one process. Output order matches `matmul_group`'s:
1242 /// `[q, k, v, forget_down, gate_down, beta_raw]`.
1243 pub fn kda_proj_fused6(
1244 &self,
1245 la: &KdaAttnLayer,
1246 x: &CudaSlice<f32>,
1247 t: usize,
1248 ) -> Result<Option<Vec<CudaSlice<f32>>>, Box<dyn std::error::Error>> {
1249 if std::env::var("MEMRA_KDA_FUSED_PROJ").as_deref() != Ok("1") {
1250 return Ok(None);
1251 }
1252 // glm5 TP composition guard (#82 review): the load preflight refuses this door at
1253 // ARM time, but the flag is read PER CALL — a post-load `set` would otherwise
1254 // engage the fused six-projection group on head shards inside the TP walk, an
1255 // unproven composition (the door's gate ran on full-width projections). A shard
1256 // declines here and takes the caller's unchanged arm, announced once.
1257 if la.tp.is_some() {
1258 static TP_F6_DECLINE: std::sync::Once = std::sync::Once::new();
1259 TP_F6_DECLINE.call_once(|| {
1260 eprintln!(
1261 "[kda-fused-proj] DECLINED on a glm5-TP head shard: the door is gated \
1262 on full-width projections (the load preflight refuses the pair; this \
1263 is the per-call twin for a post-load flag set)"
1264 );
1265 });
1266 return Ok(None);
1267 }
1268 if !(1..=15).contains(&t) {
1269 return Ok(None);
1270 }
1271 // The f32 trio is common to both operand arms. Any mismatch = refuse; the caller's
1272 // arm is the shipped program.
1273 let f32w = |w: &GpuTensor| -> Option<usize> {
1274 match w {
1275 GpuTensor::Float { .. } => Some(w.in_features()),
1276 _ => None,
1277 }
1278 };
1279 let (Some(in_fa), Some(in_ga), Some(in_b)) =
1280 (f32w(&la.f_a), f32w(&la.g_a), f32w(&la.b_proj))
1281 else {
1282 return Ok(None);
1283 };
1284 // BF16 operand arm (lever 3 of the decode diet): the serving recipe (MEMRA_BF16_MMV=1)
1285 // admits wq/wk/wv to raw bf16 residency, where the Q8_0 arm below never binds. Its
1286 // bit-identity bar is against `matvec_bf16_f32acc_x4_rows` (matmul's FloatBf16
1287 // decode-tier arm), so it refuses wherever that arm would not be the unfused program:
1288 // MEMRA_BF16_MMV off (the chunked cuBLASLt GEMM class), the step37 W8 mirror doors on
1289 // (matvec_bf16_rows_into reroutes through the q8 mirror when BOTH are set), or
1290 // MEMRA_GLM5_W8 on (2026-09-02, lane/b200-glm5-w8: the SAME reroute, independent
1291 // door — this fused kernel's bit-identity claim is against the unmirrored bf16
1292 // program, so it must decline whichever door moved that program's target).
1293 let bf16 = |w: &GpuTensor| -> Option<usize> {
1294 match w {
1295 GpuTensor::FloatBf16 { .. } => Some(w.in_features()),
1296 _ => None,
1297 }
1298 };
1299 if let (Some(in_q), Some(in_k), Some(in_v)) = (bf16(&la.wq), bf16(&la.wk), bf16(&la.wv)) {
1300 // MEMRA_B200_BF16_GEMV_LT (lane/b200-gemv-hbm-20260902) reroutes the SAME
1301 // unfused target (`matvec_bf16_f32acc_x4_rows`) to a cuBLASLt reference GEMV, so
1302 // this fused arm declines for exactly the reason it declines for the W8 mirrors:
1303 // its bit-identity bar is against the unmirrored, unrerouted bf16 program. With
1304 // the door on, the three bf16 projections fall to the unfused group and each one
1305 // takes the library GEMV, which is what the reference door is there to measure.
1306 // W8 POSTURE FUSION (lane/b200-gemv-hbm-20260902 round 3). Under MEMRA_GLM5_W8 the
1307 // six projections each reroute through `matvec_bf16_via_q8_mirror`, so this group
1308 // runs as SIX separate launches plus six redundant quantizes of the same `x` — and
1309 // the bf16 fused arm below cannot serve it, because its bit-identity bar is against
1310 // the unmirrored bf16 program. `qmatvec_kda6_q8f32_rp_v2` is the fused twin for
1311 // that posture: three mirrored ranges on the rp v2 body (bit-identical to
1312 // `qmatvec_q8_0_mmvq_rp` per row) and three f32 ranges on the same deterministic
1313 // warp tree the q8 arm of this door already ships and has pinned. Gated on
1314 // MEMRA_B200_GEMV_V2 so it carries its own receipt; without that door W8 still
1315 // declines to the unfused path exactly as before.
1316 if crate::glm5_w8_on() && !(crate::step_tp_w8_on() && crate::w8_hybrid_on()) {
1317 if !Self::bf16_mmv_on() || !crate::b200_gemv_v2_on() {
1318 return Ok(None);
1319 }
1320 let in_f = in_q;
1321 if [in_k, in_v, in_fa, in_ga, in_b].iter().any(|&i| i != in_f)
1322 || !in_f.is_multiple_of(128)
1323 || x.len() < t * in_f
1324 || Engine::q8_v2_smem_bytes(in_f) > 48 * 1024
1325 {
1326 return Ok(None);
1327 }
1328 let dims = [
1329 la.wq.out_features(),
1330 la.wk.out_features(),
1331 la.wv.out_features(),
1332 la.f_a.out_features(),
1333 la.g_a.out_features(),
1334 la.b_proj.out_features(),
1335 ];
1336 let (
1337 GpuTensor::FloatBf16 { data: bq, .. },
1338 GpuTensor::FloatBf16 { data: bk, .. },
1339 GpuTensor::FloatBf16 { data: bv, .. },
1340 ) = (&la.wq, &la.wk, &la.wv)
1341 else {
1342 unreachable!("bf16() above only admits FloatBf16");
1343 };
1344 let (
1345 GpuTensor::Float { data: wfa, .. },
1346 GpuTensor::Float { data: wga, .. },
1347 GpuTensor::Float { data: wb, .. },
1348 ) = (&la.f_a, &la.g_a, &la.b_proj)
1349 else {
1350 unreachable!("f32w() above only admits Float");
1351 };
1352 let mut outs = [
1353 self.uninit(t * dims[0])?,
1354 self.uninit(t * dims[1])?,
1355 self.uninit(t * dims[2])?,
1356 self.uninit(t * dims[3])?,
1357 self.uninit(t * dims[4])?,
1358 self.uninit(t * dims[5])?,
1359 ];
1360 self.kda_proj_fused6_q8rp_raw(
1361 bq, bk, bv, wfa, wga, wb, x, &mut outs, in_f, dims, t,
1362 )?;
1363 if KDA_FUSED6_Q8RP_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1364 eprintln!(
1365 "[kda-fused6] engaged arm=q8rp_v2 in_f={in_f} out={dims:?} t={t} (one \
1366 launch replaces the six W8-mirror projections and their six redundant \
1367 activation quantizes; MEMRA_KDA_FUSED_PROJ=1 MEMRA_B200_GEMV_V2=1)"
1368 );
1369 }
1370 return Ok(Some(outs.into_iter().collect()));
1371 }
1372 if !Self::bf16_mmv_on()
1373 || (crate::step_tp_w8_on() && crate::w8_hybrid_on())
1374 || crate::b200_bf16_gemv_lt_on()
1375 {
1376 return Ok(None);
1377 }
1378 let in_f = in_q;
1379 if [in_k, in_v, in_fa, in_ga, in_b].iter().any(|&i| i != in_f)
1380 || !in_f.is_multiple_of(128)
1381 || x.len() < t * in_f
1382 {
1383 return Ok(None);
1384 }
1385 let dims = [
1386 la.wq.out_features(),
1387 la.wk.out_features(),
1388 la.wv.out_features(),
1389 la.f_a.out_features(),
1390 la.g_a.out_features(),
1391 la.b_proj.out_features(),
1392 ];
1393 let (
1394 GpuTensor::FloatBf16 { data: bq, .. },
1395 GpuTensor::FloatBf16 { data: bk, .. },
1396 GpuTensor::FloatBf16 { data: bv, .. },
1397 ) = (&la.wq, &la.wk, &la.wv)
1398 else {
1399 unreachable!("bf16() above only admits FloatBf16");
1400 };
1401 let (
1402 GpuTensor::Float { data: wfa, .. },
1403 GpuTensor::Float { data: wga, .. },
1404 GpuTensor::Float { data: wb, .. },
1405 ) = (&la.f_a, &la.g_a, &la.b_proj)
1406 else {
1407 unreachable!("f32w() above only admits Float");
1408 };
1409 let mut outs = [
1410 self.uninit(t * dims[0])?,
1411 self.uninit(t * dims[1])?,
1412 self.uninit(t * dims[2])?,
1413 self.uninit(t * dims[3])?,
1414 self.uninit(t * dims[4])?,
1415 self.uninit(t * dims[5])?,
1416 ];
1417 self.kda_proj_fused6_bf16_raw(bq, bk, bv, wfa, wga, wb, x, &mut outs, in_f, dims, t)?;
1418 if KDA_FUSED6_BF16_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1419 eprintln!(
1420 "[kda-fused6] engaged arm=bf16 in_f={in_f} out={dims:?} t={t} (one launch \
1421 replaces the six-projection group on the bf16-resident serving recipe; \
1422 MEMRA_KDA_FUSED_PROJ=1)"
1423 );
1424 }
1425 return Ok(Some(outs.into_iter().collect()));
1426 }
1427 // Dispatch-class envs: the bit-identity bar is against the MMVQ-class per-row program.
1428 if std::env::var("MEMRA_FAST").as_deref() == Ok("0")
1429 || !self.mmvq_supports(crate::QT_Q8_0)
1430 || (t >= 2 && std::env::var("MEMRA_NO_BATCHED").is_ok())
1431 || (t >= 5 && !Self::b8_enabled())
1432 {
1433 return Ok(None);
1434 }
1435 // Q8_0 operand classes (the non-BF16_MMV shapes).
1436 let q8 = |w: &GpuTensor| -> Option<(usize, usize)> {
1437 match w {
1438 GpuTensor::Quant {
1439 qtype: crate::QT_Q8_0,
1440 row_bytes,
1441 scale,
1442 rp: false,
1443 rp4: None,
1444 ..
1445 } if *scale == 1.0 => Some((w.in_features(), *row_bytes)),
1446 _ => None,
1447 }
1448 };
1449 let (Some((in_q, rb_q)), Some((in_k, rb_k)), Some((in_v, rb_v))) =
1450 (q8(&la.wq), q8(&la.wk), q8(&la.wv))
1451 else {
1452 return Ok(None);
1453 };
1454 let in_f = in_q;
1455 if [in_k, in_v, in_fa, in_ga, in_b].iter().any(|&i| i != in_f)
1456 || rb_k != rb_q
1457 || rb_v != rb_q
1458 || !in_f.is_multiple_of(128)
1459 || x.len() < t * in_f
1460 {
1461 return Ok(None);
1462 }
1463 let dims = [
1464 la.wq.out_features(),
1465 la.wk.out_features(),
1466 la.wv.out_features(),
1467 la.f_a.out_features(),
1468 la.g_a.out_features(),
1469 la.b_proj.out_features(),
1470 ];
1471 let (
1472 GpuTensor::Quant { bytes: bq, .. },
1473 GpuTensor::Quant { bytes: bk, .. },
1474 GpuTensor::Quant { bytes: bv, .. },
1475 ) = (&la.wq, &la.wk, &la.wv)
1476 else {
1477 unreachable!("q8() above only admits Quant");
1478 };
1479 let (
1480 GpuTensor::Float { data: wfa, .. },
1481 GpuTensor::Float { data: wga, .. },
1482 GpuTensor::Float { data: wb, .. },
1483 ) = (&la.f_a, &la.g_a, &la.b_proj)
1484 else {
1485 unreachable!("f32w() above only admits Float");
1486 };
1487
1488 let (aq, ad) = self.quantize_q8_1(x, t, in_f)?;
1489 let mut outs = [
1490 self.uninit(t * dims[0])?,
1491 self.uninit(t * dims[1])?,
1492 self.uninit(t * dims[2])?,
1493 self.uninit(t * dims[3])?,
1494 self.uninit(t * dims[4])?,
1495 self.uninit(t * dims[5])?,
1496 ];
1497 self.kda_proj_fused6_raw(
1498 bq, bk, bv, wfa, wga, wb, &aq, &ad, x, &mut outs, in_f, dims, t, rb_q,
1499 )?;
1500
1501 // Engagement receipt: counted at the arm's own call site, announced once per boot
1502 // (the [bf16-mmv] RESIDENT lesson: engagement lines are receipts, never inferred).
1503 if KDA_FUSED6_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1504 eprintln!(
1505 "[kda-fused6] engaged in_f={in_f} out={dims:?} t={t} (one launch replaces the \
1506 six-projection group; MEMRA_KDA_FUSED_PROJ=1)"
1507 );
1508 }
1509 Ok(Some(outs.into_iter().collect()))
1510 }
1511
1512 /// The raw fused-6 launch (`qmatvec_kda6_q8f32_mmvq`): three Q8_0 weights + three f32
1513 /// weights, one q8_1 activation pair + the raw f32 activation, six outputs, t token rows.
1514 /// Geometry-checked but POLICY-FREE: the gate's red arms drive mutations (transposed slice
1515 /// data, dropped ranges via `dims[i] = 0`) through this entry, so the mutation reaches the
1516 /// exact program the door serves.
1517 #[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
1518 pub fn kda_proj_fused6_raw(
1519 &self,
1520 wq: &CudaSlice<u8>,
1521 wk: &CudaSlice<u8>,
1522 wv: &CudaSlice<u8>,
1523 wfa: &CudaSlice<f32>,
1524 wga: &CudaSlice<f32>,
1525 wb: &CudaSlice<f32>,
1526 aq: &CudaSlice<i8>,
1527 ad: &CudaSlice<f32>,
1528 x: &CudaSlice<f32>,
1529 outs: &mut [CudaSlice<f32>; 6],
1530 in_f: usize,
1531 dims: [usize; 6],
1532 t: usize,
1533 row_bytes: usize,
1534 ) -> Result<(), Box<dyn std::error::Error>> {
1535 const ROWS_PER_BLOCK: usize = 4; // MEMRA_MMVQ_ROWS in qmatvec.cu
1536 if t == 0
1537 || !in_f.is_multiple_of(128)
1538 || x.len() < t * in_f
1539 || aq.len() < t * in_f
1540 || ad.len() < t * (in_f / 32)
1541 {
1542 return Err("kda_proj_fused6 geometry".into());
1543 }
1544 for (i, (w, want_rows)) in [(wq, dims[0]), (wk, dims[1]), (wv, dims[2])]
1545 .into_iter()
1546 .enumerate()
1547 {
1548 if w.len() < want_rows * row_bytes {
1549 return Err(format!(
1550 "kda_proj_fused6: q8 weight {i} holds {} bytes, needs {}",
1551 w.len(),
1552 want_rows * row_bytes
1553 )
1554 .into());
1555 }
1556 }
1557 for (i, (w, want_rows)) in [(wfa, dims[3]), (wga, dims[4]), (wb, dims[5])]
1558 .into_iter()
1559 .enumerate()
1560 {
1561 if w.len() < want_rows * in_f {
1562 return Err(format!(
1563 "kda_proj_fused6: f32 weight {} holds {} floats, needs {}",
1564 i + 3,
1565 w.len(),
1566 want_rows * in_f
1567 )
1568 .into());
1569 }
1570 }
1571 for (i, (o, want)) in outs.iter().zip(dims).enumerate() {
1572 if o.len() < t * want {
1573 return Err(format!("kda_proj_fused6: output {i} too small").into());
1574 }
1575 }
1576 let blocks: usize = dims.iter().map(|d| d.div_ceil(ROWS_PER_BLOCK)).sum();
1577 let f = self.func("qmatvec_kda6_q8f32_mmvq");
1578 let cfg = LaunchConfig {
1579 grid_dim: (blocks as u32, t as u32, 1),
1580 block_dim: (32, ROWS_PER_BLOCK as u32, 1),
1581 shared_mem_bytes: 0,
1582 };
1583 let inf = in_f as i32;
1584 let d = dims.map(|v| v as i32);
1585 let (mi, rb) = (t as i32, row_bytes as i64);
1586 let [o0, o1, o2, o3, o4, o5] = outs;
1587 let stream = self.gpu.stream();
1588 let mut b = stream.launch_builder(&f);
1589 b.arg(wq)
1590 .arg(wk)
1591 .arg(wv)
1592 .arg(wfa)
1593 .arg(wga)
1594 .arg(wb)
1595 .arg(aq)
1596 .arg(ad)
1597 .arg(x)
1598 .arg(&mut *o0)
1599 .arg(&mut *o1)
1600 .arg(&mut *o2)
1601 .arg(&mut *o3)
1602 .arg(&mut *o4)
1603 .arg(&mut *o5)
1604 .arg(&inf)
1605 .arg(&d[0])
1606 .arg(&d[1])
1607 .arg(&d[2])
1608 .arg(&d[3])
1609 .arg(&d[4])
1610 .arg(&d[5])
1611 .arg(&mi)
1612 .arg(&rb);
1613 unsafe { b.launch(cfg)? };
1614 Ok(())
1615 }
1616
1617 /// The raw BF16-arm fused-6 launch (`qmatvec_kda6_bf16f32`): three bf16-resident weights
1618 /// (raw checkpoint u16 bytes, the `admit=bf16_mmv` residency) + three f32 weights, one raw
1619 /// f32 activation, six outputs, t token rows. Block = `mmv_block()` — the SAME blockDim
1620 /// `matvec_bf16_rows_into` pins, because the bf16 body's shared-tree reduction shape (and
1621 /// therefore its bits) is a function of blockDim. Geometry-checked but POLICY-FREE: the
1622 /// gate's red arms drive mutations through this entry, exactly like the q8 raw above.
1623 #[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
1624 pub fn kda_proj_fused6_bf16_raw(
1625 &self,
1626 wq: &CudaSlice<u8>,
1627 wk: &CudaSlice<u8>,
1628 wv: &CudaSlice<u8>,
1629 wfa: &CudaSlice<f32>,
1630 wga: &CudaSlice<f32>,
1631 wb: &CudaSlice<f32>,
1632 x: &CudaSlice<f32>,
1633 outs: &mut [CudaSlice<f32>; 6],
1634 in_f: usize,
1635 dims: [usize; 6],
1636 t: usize,
1637 ) -> Result<(), Box<dyn std::error::Error>> {
1638 self.kda_proj_fused6_bf16_arm_raw(
1639 wq,
1640 wk,
1641 wv,
1642 wfa,
1643 wga,
1644 wb,
1645 x,
1646 outs,
1647 in_f,
1648 dims,
1649 t,
1650 crate::b200_gemv_v2_level(),
1651 )
1652 }
1653
1654 /// The same launch with the arm chosen EXPLICITLY instead of from the memoized
1655 /// `MEMRA_B200_GEMV_V2` door, so a bench or gate can drive every arm inside one process
1656 /// (`b200_matvec_bench`, the `_arm_raw` precedent).
1657 ///
1658 /// `arm`: `0` = the shipped `qmatvec_kda6_bf16f32`; `1` = `_v2`, whose three BF16 ranges take
1659 /// the eight-rows-per-block walk (activation loaded once and reused across the rows, ten
1660 /// 16 B loads in flight before the first fma, one barrier chain per block) instead of
1661 /// `kda6_bf16_rows4`'s four sequential rows; `2` = `_v3`, the same walk with its weight
1662 /// tiles staged through shared memory by `cp.async` so the in-flight budget stops being
1663 /// register-bound. `2` falls back to `1` when v3's dynamic smem would exceed the 48 KB
1664 /// default cap. Per row the arithmetic is unchanged in every arm, so all three are
1665 /// BIT-IDENTICAL to each other and to `matvec_bf16_f32acc_x4_rows`.
1666 #[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
1667 pub fn kda_proj_fused6_bf16_arm_raw(
1668 &self,
1669 wq: &CudaSlice<u8>,
1670 wk: &CudaSlice<u8>,
1671 wv: &CudaSlice<u8>,
1672 wfa: &CudaSlice<f32>,
1673 wga: &CudaSlice<f32>,
1674 wb: &CudaSlice<f32>,
1675 x: &CudaSlice<f32>,
1676 outs: &mut [CudaSlice<f32>; 6],
1677 in_f: usize,
1678 dims: [usize; 6],
1679 t: usize,
1680 arm: u8,
1681 ) -> Result<(), Box<dyn std::error::Error>> {
1682 if t == 0 || !in_f.is_multiple_of(128) || x.len() < t * in_f {
1683 return Err("kda_proj_fused6_bf16 geometry".into());
1684 }
1685 for (i, (w, want_rows)) in [(wq, dims[0]), (wk, dims[1]), (wv, dims[2])]
1686 .into_iter()
1687 .enumerate()
1688 {
1689 if w.len() < want_rows * in_f * 2 {
1690 return Err(format!(
1691 "kda_proj_fused6_bf16: bf16 weight {i} holds {} bytes, needs {}",
1692 w.len(),
1693 want_rows * in_f * 2
1694 )
1695 .into());
1696 }
1697 }
1698 for (i, (w, want_rows)) in [(wfa, dims[3]), (wga, dims[4]), (wb, dims[5])]
1699 .into_iter()
1700 .enumerate()
1701 {
1702 if w.len() < want_rows * in_f {
1703 return Err(format!(
1704 "kda_proj_fused6_bf16: f32 weight {} holds {} floats, needs {}",
1705 i + 3,
1706 w.len(),
1707 want_rows * in_f
1708 )
1709 .into());
1710 }
1711 }
1712 for (i, (o, want)) in outs.iter().zip(dims).enumerate() {
1713 if o.len() < t * want {
1714 return Err(format!("kda_proj_fused6_bf16: output {i} too small").into());
1715 }
1716 }
1717 // v3 declines to v2 when its staged tiles would not fit the 48 KB default dynamic
1718 // shared-memory cap (36 KB at the default mmv_block()=128, 72 KB at 256).
1719 let arm = if arm >= 2 && !crate::gemv_v3_fits() {
1720 1
1721 } else {
1722 arm
1723 };
1724 // Rows per block, and therefore the block partition of the six ranges: 4 for the
1725 // shipped kernel, `GEMV_V2_ROWS` for the v2/v3 twins. v2 takes the R-row reduction
1726 // window as DYNAMIC shared memory (R * blockDim.x floats); v3 takes that plus its
1727 // cp.async stage buffers.
1728 let nb = crate::mmv_block();
1729 let rpb = if arm >= 1 { crate::GEMV_V2_ROWS } else { 4 };
1730 let blocks: usize = dims.iter().map(|d| d.div_ceil(rpb)).sum();
1731 let f = self.func(match arm {
1732 0 => "qmatvec_kda6_bf16f32",
1733 1 => "qmatvec_kda6_bf16f32_v2",
1734 _ => "qmatvec_kda6_bf16f32_v3",
1735 });
1736 let cfg = LaunchConfig {
1737 grid_dim: (blocks as u32, t as u32, 1),
1738 block_dim: (nb, 1, 1),
1739 shared_mem_bytes: match arm {
1740 0 => 0,
1741 1 => (crate::GEMV_V2_ROWS as u32) * nb * 4,
1742 _ => crate::gemv_v3_smem_bytes(nb as usize) as u32,
1743 },
1744 };
1745 let inf = in_f as i32;
1746 let d = dims.map(|v| v as i32);
1747 let mi = t as i32;
1748 let [o0, o1, o2, o3, o4, o5] = outs;
1749 let stream = self.gpu.stream();
1750 let mut b = stream.launch_builder(&f);
1751 b.arg(wq)
1752 .arg(wk)
1753 .arg(wv)
1754 .arg(wfa)
1755 .arg(wga)
1756 .arg(wb)
1757 .arg(x)
1758 .arg(&mut *o0)
1759 .arg(&mut *o1)
1760 .arg(&mut *o2)
1761 .arg(&mut *o3)
1762 .arg(&mut *o4)
1763 .arg(&mut *o5)
1764 .arg(&inf)
1765 .arg(&d[0])
1766 .arg(&d[1])
1767 .arg(&d[2])
1768 .arg(&d[3])
1769 .arg(&d[4])
1770 .arg(&d[5])
1771 .arg(&mi);
1772 unsafe { b.launch(cfg)? };
1773 Ok(())
1774 }
1775}