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);
53pub static KDA_FUSED6_E4M3_DISPATCHES: AtomicU64 = AtomicU64::new(0);
54
55/// Same door, BF16 operand arm (`qmatvec_kda6_bf16f32`, lane/glm5-decode-diet lever 3).
56/// Counted separately so a box A/B on the serving recipe (MEMRA_BF16_MMV=1, where the q8 arm
57/// refuses by design) can attribute engagement to the arm that actually ran.
58pub static KDA_FUSED6_BF16_DISPATCHES: AtomicU64 = AtomicU64::new(0);
59
60/// Same door, W8-MIRROR arm (`qmatvec_kda6_q8f32_rp_v2`, lane/b200-gemv-hbm-20260902 round 3).
61/// Counted separately for the same reason the bf16 arm is: a box A/B on the serving recipe must
62/// be able to attribute engagement to the arm that actually ran.
63pub static KDA_FUSED6_Q8RP_DISPATCHES: AtomicU64 = AtomicU64::new(0);
64
65/// The only head width `memra_kda_scan_s128` is instantiated for, and the only one glm5_next
66/// ships (`linear_attn_config.head_dim = 128`).
67pub const KDA_HEAD_DIM: usize = 128;
68/// The conv kernels hold their window in a fixed register array; wider kernels would silently
69/// read past it, so the loader refuses them.
70const KDA_MAX_CONV_KERNEL: usize = 8;
71/// FLA l2norm epsilon. Fixed at 1e-6 and INSIDE the sqrt — independent of the layer's rms eps,
72/// which is a different constant used by the output norm below.
73const KDA_L2_EPS: f32 = 1e-6;
74
75/// One loaded KDA mixer. Field names follow the reference's tensor roles, not the HF spellings.
76pub struct KdaAttnLayer {
77 pub plan: KimiDeltaNetPlan,
78 /// q/k/v projections, `[qkv, hidden]` each.
79 pub wq: GpuTensor,
80 pub wk: GpuTensor,
81 pub wv: GpuTensor,
82 /// Forget gate low-rank pair: `f_a [head_dim, hidden]`, `f_b [qkv, head_dim]`.
83 pub f_a: GpuTensor,
84 pub f_b: GpuTensor,
85 /// Output gate low-rank pair, same shapes as the forget pair.
86 pub g_a: GpuTensor,
87 pub g_b: GpuTensor,
88 /// Per-head beta projection, `[heads, hidden]`.
89 pub b_proj: GpuTensor,
90 /// Output projection, `[hidden, qkv]`.
91 pub wo: GpuTensor,
92 /// The three per-plane conv weights concatenated into `[3*qkv, kernel]` (see module header).
93 pub conv: CudaSlice<f32>,
94 /// `A_log [heads]`, `dt_bias [qkv]` (per CHANNEL, unlike GDN's per-head bias),
95 /// `o_norm [head_dim]`.
96 pub a_log: GpuTensor,
97 pub dt_bias: GpuTensor,
98 pub o_norm: GpuTensor,
99 /// glm5 TP-2 sidecar (`MEMRA_GLM5_TP`, lane/glm5-tp2). `Some` means THIS layer struct is
100 /// the ROOT-RANK HEAD SHARD (heads/2) and the sidecar carries the peer shard + runtime.
101 /// Every plain entry point REFUSES a sharded layer by name — only the TP walk
102 /// (`glm5_tp::kda_tp_*`) may execute it. `None` everywhere else (zero cost, zero change).
103 pub tp: Option<Box<crate::glm5_tp::Glm5TpKda>>,
104}
105
106impl KdaAttnLayer {
107 pub fn heads(&self) -> usize {
108 self.plan.num_heads as usize
109 }
110 pub fn head_dim(&self) -> usize {
111 self.plan.head_dim as usize
112 }
113 pub fn qkv(&self) -> usize {
114 self.heads() * self.head_dim()
115 }
116 pub fn conv_kernel(&self) -> usize {
117 self.plan.conv_kernel as usize
118 }
119 /// Fused conv ring width, matching `StatePlan::Recurrent { conv_width }` for this layer.
120 pub fn conv_width(&self) -> usize {
121 3 * self.qkv()
122 }
123 /// Recurrent state elements, matching `StatePlan::Recurrent { state_width }`.
124 pub fn state_width(&self) -> usize {
125 self.heads() * self.head_dim() * self.head_dim()
126 }
127
128 /// Load block `il`'s KDA tensors. Names are the ggml-dialect contract names from
129 /// `memra_gguf::tensor_contract::add_kda`; the safetensors source translates them.
130 pub fn load(
131 e: &Engine,
132 src: &dyn TensorSource,
133 il: u32,
134 plan: &KimiDeltaNetPlan,
135 ) -> Result<Self, Box<dyn std::error::Error>> {
136 let heads = plan.num_heads as usize;
137 let head_dim = plan.head_dim as usize;
138 let kernel = plan.conv_kernel as usize;
139 if head_dim != KDA_HEAD_DIM {
140 return Err(format!(
141 "blk.{il}: KDA head_dim {head_dim} is not the {KDA_HEAD_DIM} the scan kernel is \
142 instantiated for; a new memra_kda_scan_s<N> instantiation is required before \
143 this geometry can serve"
144 )
145 .into());
146 }
147 if heads == 0 {
148 return Err(format!("blk.{il}: KDA num_heads must be positive").into());
149 }
150 if !(2..=KDA_MAX_CONV_KERNEL).contains(&kernel) {
151 return Err(format!(
152 "blk.{il}: KDA conv_kernel {kernel} outside the 2..={KDA_MAX_CONV_KERNEL} window \
153 the conv kernels hold in registers"
154 )
155 .into());
156 }
157 let p = |s: &str| format!("blk.{il}.{s}");
158 let load = |name: String| GpuTensor::load_from_source(e, src, &name);
159
160 let qkv = heads * head_dim;
161 // Fuse the three per-plane conv weights into one [3*qkv, kernel] buffer (module header).
162 // Each source tensor is [qkv, kernel] channel-major, so the planes concatenate as whole
163 // row blocks and plane p lands at row p*qkv — the ring's own plane offset.
164 let mut conv = e.zeros(3 * qkv * kernel)?;
165 for (plane, name) in [
166 "kda_q_conv1d.weight",
167 "kda_k_conv1d.weight",
168 "kda_v_conv1d.weight",
169 ]
170 .into_iter()
171 .enumerate()
172 {
173 let w = load(p(name))?;
174 let src_data = w.float_data();
175 if src_data.len() != qkv * kernel {
176 return Err(format!(
177 "blk.{il}.{name}: {} elements, contract requires {}",
178 src_data.len(),
179 qkv * kernel
180 )
181 .into());
182 }
183 e.copy_into(&mut conv, plane * qkv * kernel, src_data, qkv * kernel)?;
184 }
185
186 Ok(Self {
187 plan: *plan,
188 wq: load(p("kda_q.weight"))?,
189 wk: load(p("kda_k.weight"))?,
190 wv: load(p("kda_v.weight"))?,
191 f_a: load(p("kda_f_a.weight"))?,
192 f_b: load(p("kda_f_b.weight"))?,
193 g_a: load(p("kda_g_a.weight"))?,
194 g_b: load(p("kda_g_b.weight"))?,
195 b_proj: load(p("kda_b.weight"))?,
196 wo: load(p("kda_out.weight"))?,
197 conv,
198 a_log: load(p("kda_a_log"))?,
199 dt_bias: load(p("kda_dt.bias"))?,
200 o_norm: load(p("kda_o_norm.weight"))?,
201 tp: None,
202 })
203 }
204}
205
206/// Which conv arm a call takes. `Prefill` reads the ring as a left pad and rolls it afterwards;
207/// `Decode` fuses assemble+conv+roll for the single new row. The two produce bit-identical
208/// values at T=1 (same ascending tap order over the same window) — the split exists so decode
209/// and the spec verify keep one dispatch class, per the cu/hybrid.cu decode==verify law.
210#[derive(Clone, Copy, PartialEq, Eq)]
211pub(crate) enum ConvArm {
212 Prefill,
213 Decode,
214}
215
216/// The scan-input buffers of one KDA step, STOLEN from the step instead of dropped
217/// (lane/glm5-loop-port, port 3 — the module doc's named GdnStash/ReplaySSM diet): the
218/// glm5 verify walk's rollback checkpoint keeps these ~160 KB of already-allocated
219/// buffers per row per layer and retires the per-row 4 MiB recurrent-state clones
220/// (~0.95 GiB transient at K=7). Replaying `kda_scan` over them from a pre-round state
221/// snapshot rebuilds the post-row state EXACTLY: each replay is the ORIGINAL t=1 launch
222/// re-issued — same kernel, same inputs, same shape — so the rebuilt state is
223/// byte-identical to the clone it replaces by construction, not by a numeric argument.
224pub struct KdaScanInputs {
225 pub q: CudaSlice<f32>,
226 pub k: CudaSlice<f32>,
227 pub v: CudaSlice<f32>,
228 pub g: CudaSlice<f32>,
229 pub beta: CudaSlice<f32>,
230}
231
232/// The rollback stash of one BATCHED verify-rows KDA call (lane/glm5-verify-batch): the
233/// per-layer t=K+1 twin of the per-row [`KdaScanInputs`] steal. Everything here is either
234/// stolen from buffers the call allocated anyway (`raws`, `scan` — zero copies) or one
235/// small clone per layer per round (`ring_snap`, `3*qkv*(kernel-1)` floats ~ 96 KiB).
236///
237/// Rollback to `keep` rows rebuilds both state planes EXACTLY:
238/// * conv ring: restore `ring_snap`, then re-issue `kda_conv_ring_roll` per plane over
239/// `raws` at T=keep — the roll is pure placement (no arithmetic), so the rebuilt ring
240/// is the sequential chain's ring after row keep-1 byte-for-byte.
241/// * ssm state: ONE `kda_scan` replay at T=keep from the caller's pre-round snapshot
242/// over the batched `scan` inputs (the kernel walks rows 0..keep of the [t, ..]
243/// buffers) — the in-kernel T-loop IS the chained t=1 program (register-resident
244/// state, identical per-step order), held by the scan-chain bit-gate.
245pub struct KdaRowsStash {
246 /// The fused conv ring BEFORE this call's rolls (one clone per layer per round).
247 pub ring_snap: CudaSlice<f32>,
248 /// RAW (pre-conv) q/k/v projection rows `[t, qkv]`, stolen post-roll (plane order).
249 pub raws: [CudaSlice<f32>; 3],
250 /// Batched scan inputs `[t, ..]`, stolen post-scan.
251 pub scan: KdaScanInputs,
252 /// Row count of the call that filled this stash; rollback validates `keep` against it.
253 pub rows: usize,
254}
255
256/// What a `kda_core` call is asked to leave behind for rollback — and, for `Rows`, which
257/// matmul class the call rides (the decode-exact rows classes, `matmul_rows_exact`).
258pub(crate) enum KdaStash<'a> {
259 /// No rollback stash (prefill / plain decode).
260 None,
261 /// Per-row t=1 steal (loop-port 3, the per-row verify walk).
262 Decode(&'a mut Option<KdaScanInputs>),
263 /// BATCHED verify-rows steal (lane/glm5-verify-batch): scan inputs + raw conv rows +
264 /// a pre-call ring snapshot; every matmul rides `matmul_rows_exact` so each row is
265 /// bit-identical to the t=1 decode program per the decode-exact class contracts.
266 Rows(&'a mut Option<KdaRowsStash>),
267}
268
269/// `MEMRA_KDA_STEP_TRACE=1` (gate-harness instrument, default OFF, never a serving flag): after
270/// each sub-step of the KDA core, print the non-finite element count of every live buffer on one
271/// line. memra#131 cell 9 placed the graph door's poison inside `kda_decode_cached` at layer 4
272/// (finite input, finite state, all-NaN mixer output after a capture); this names the kernel.
273fn kda_trace_on() -> bool {
274 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
275 *V.get_or_init(|| std::env::var("MEMRA_KDA_STEP_TRACE").as_deref() == Ok("1"))
276}
277fn kda_trace(e: &Engine, stage: &str, t: usize, bufs: &[(&str, &CudaSlice<f32>)]) {
278 if !kda_trace_on() {
279 return;
280 }
281 // A device-to-host copy is illegal inside an open CUDA graph capture (it invalidates the
282 // capture); box cell 11 (memra#131) hit exactly that on the session's first decode step,
283 // which the door captures. Print the stage with a note instead of counting.
284 if crate::glm5_graph_capture_open() {
285 eprintln!("[kda-step-trace] t={t} {stage}: (inside an open graph capture; not counted)");
286 return;
287 }
288 let mut line = format!("[kda-step-trace] t={t} {stage}:");
289 for (name, b) in bufs {
290 let n = match e.dtoh(b) {
291 Ok(v) => v.iter().filter(|x| !x.is_finite()).count(),
292 Err(_) => usize::MAX,
293 };
294 line.push_str(&format!(" {name}={n}/{}", b.len()));
295 }
296 eprintln!("{line}");
297}
298
299/// The whole mixer, stage for stage against `memra_reference::kimi_delta_net`.
300///
301/// `ring` is the fused `[3*qkv, kernel-1]` conv state (zeroed = fresh prefill's zero left pad)
302/// and is updated in place. `state_in`/`state_out` are the `[heads, 128, 128]` recurrent state
303/// in the kernel's transposed `M[col][i]` layout; they MUST be distinct buffers.
304#[allow(clippy::too_many_arguments)]
305// allow: the parameter list mirrors the kernel/FFI/call contract; bundling into a struct is a refactor, not a lint fix
306fn kda_core(
307 e: &Engine,
308 la: &KdaAttnLayer,
309 x: &CudaSlice<f32>,
310 t: usize,
311 eps: f32,
312 ring: &mut CudaSlice<f32>,
313 state_in: &CudaSlice<f32>,
314 state_out: &mut CudaSlice<f32>,
315 arm: ConvArm,
316 stash: KdaStash<'_>,
317 scan_clock: Option<&mut u64>,
318 pre_q8: KdaPreQ8<'_>,
319) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
320 // glm5 TP fail-closed choke point: every plain KDA entry (stateless, prime, decode,
321 // stash — INCLUDING the batched verify-rows walk, `kda_verify_rows_cached`) funnels
322 // through here. A TP-sharded layer holds heads/2 — running it on the plain path would
323 // compute a silently-halved mixer, so it refuses by name instead.
324 if la.tp.is_some() {
325 return Err(format!(
326 "KDA layer is glm5-TP-sharded (MEMRA_GLM5_TP): the plain mixer path is unwired \
327 for a head shard — only the TP decode/prime walk may execute it (t={t}, arm \
328 {})",
329 if arm == ConvArm::Decode {
330 "decode"
331 } else {
332 "prefill"
333 }
334 )
335 .into());
336 }
337 // Verify-batch wo seam (lane/glm5-verify-batch): the rows arm routes the output
338 // projection through the decode-exact classes, exactly like every projection inside
339 // the core — the wo dispatch moved into this wrapper with the TP split, its routing
340 // did not change.
341 let rows_exact = matches!(stash, KdaStash::Rows(_));
342 // MEMRA_KDA_ONORM_ZQ8: ask the core for the o_norm output's q8_1 pair when `wo` can take it.
343 let want_pair = !rows_exact && kda_onorm_zq8_on() && e.mmvq_fast_eligible(&la.wo, t);
344 let mut onorm_q8: KdaOnormQ8 = None;
345 let gated = kda_core_gated(
346 e,
347 la,
348 x,
349 t,
350 eps,
351 ring,
352 state_in,
353 state_out,
354 arm,
355 stash,
356 scan_clock,
357 pre_q8,
358 if want_pair { Some(&mut onorm_q8) } else { None },
359 )?;
360 if rows_exact {
361 let y = e.matmul_rows_exact(&la.wo, &gated, t);
362 // Door W: gated's last reader was the wo matmul above.
363 e.vws_recycle(gated);
364 y
365 } else {
366 if let Some((aq, ad)) = onorm_q8.as_ref()
367 && let Some(y) = e.matmul_q8_fast(&la.wo, aq, ad, t)?
368 {
369 if KDA_ONORM_ZQ8_DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 0 {
370 eprintln!(
371 "[kda-onorm-zq8] engaged: the KDA o_norm launch emits wo's q8_1 pair; the \
372 standalone quantize is gone (MEMRA_KDA_ONORM_ZQ8=1)"
373 );
374 }
375 return Ok(y);
376 }
377 e.matmul(&la.wo, &gated, t)
378 }
379}
380
381/// [`kda_core`] up to (and excluding) the output projection: returns the gated `[t, qkv]`
382/// mixer output. Split out for the glm5 TP-2 seam, whose column-parallel `wo` runs over the
383/// cross-rank GATHERED gated tensor rather than this shard's slice — the plain path is
384/// `kda_core` above, byte-for-byte the pre-split body (the wo matmul and its rows-exact
385/// routing moved, nothing else). This body is the CURRENT doored/batched core: it carries
386/// the `MEMRA_KDA_FUSED_PROJ` door and the verify-batch rows arm; the TP decode/prime walk
387/// calls it with `KdaStash::None`, the spec x TP verify walk (lane/glm5-composition) with
388/// `KdaStash::Rows` per rank, and the TP load preflight refuses the fused-proj door by
389/// name (unproven composition on head shards — see the FLAGS.md composition matrix).
390#[allow(clippy::too_many_arguments)] // mirrors kda_core's own contract-shaped list
391pub(crate) fn kda_core_gated(
392 e: &Engine,
393 la: &KdaAttnLayer,
394 x: &CudaSlice<f32>,
395 t: usize,
396 eps: f32,
397 ring: &mut CudaSlice<f32>,
398 state_in: &CudaSlice<f32>,
399 state_out: &mut CudaSlice<f32>,
400 arm: ConvArm,
401 stash: KdaStash<'_>,
402 mut scan_clock: Option<&mut u64>,
403 pre_q8: KdaPreQ8<'_>,
404 onorm_q8: Option<&mut KdaOnormQ8>,
405) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
406 let heads = la.heads();
407 let head_dim = la.head_dim();
408 let qkv = la.qkv();
409 let kernel = la.conv_kernel();
410 // The BATCHED verify-rows arm (lane/glm5-verify-batch): prefill conv dispatch (per-row
411 // bit-identical to the decode arm — same ascending taps over the same window values,
412 // held by the conv-arm bit-gate) + decode-exact matmul classes + the rows stash.
413 let rows_exact = matches!(stash, KdaStash::Rows(_));
414 if rows_exact && arm != ConvArm::Prefill {
415 return Err("KDA rows stash requires the prefill conv arm".into());
416 }
417 if arm == ConvArm::Decode && t != 1 {
418 return Err(format!("KDA decode arm requires t == 1, got {t}").into());
419 }
420 if ring.len() < la.conv_width() * (kernel - 1) {
421 return Err(format!(
422 "KDA conv ring holds {} floats, layer needs {}",
423 ring.len(),
424 la.conv_width() * (kernel - 1)
425 )
426 .into());
427 }
428 if state_in.len() < la.state_width() || state_out.len() < la.state_width() {
429 return Err(format!(
430 "KDA recurrent state holds {}/{} floats, layer needs {}",
431 state_in.len(),
432 state_out.len(),
433 la.state_width()
434 )
435 .into());
436 }
437
438 // Stage 1 — the six projections that read x directly. f_b/g_b are chained off their own
439 // down-projections below, exactly as the reference nests them.
440 //
441 // MEMRA_KDA_FUSED_PROJ=1 (default OFF): the six matvec calls collapse to one quantize +
442 // one `qmatvec_kda6_q8f32_mmvq` launch — the program shape both vLLM and SGLang ship for
443 // this trunk (ENGINE-SURVEY.md C1) and the step37 QKV_FUSED transfer (TRANSFER-MAP lever 1).
444 // `kda_proj_fused6` refuses (returns None) on any operand/env shape where its bit-identity
445 // claim would not hold, so the fall-through arm is always the unchanged program.
446 let mut g6 = match e.kda_proj_fused6_pre(la, x, t, pre_q8)? {
447 Some(outs) => outs,
448 None if rows_exact => {
449 // Verify-rows matmul class: per-weight decode-exact dispatch (the tcols /
450 // batched-MMVQ / per-token-linear classes — each row bit-identical to the
451 // t=1 program by the matmul_rows_exact contract).
452 [&la.wq, &la.wk, &la.wv, &la.f_a, &la.g_a, &la.b_proj]
453 .into_iter()
454 .map(|w| e.matmul_rows_exact(w, x, t))
455 .collect::<Result<Vec<_>, _>>()?
456 }
457 None => e.matmul_group(
458 &[&la.wq, &la.wk, &la.wv, &la.f_a, &la.g_a, &la.b_proj],
459 x,
460 t,
461 )?,
462 };
463 let beta_raw = g6.pop().unwrap(); // [T, heads]
464 let gate_down = g6.pop().unwrap(); // [T, head_dim]
465 let forget_down = g6.pop().unwrap(); // [T, head_dim]
466 if kda_trace_on() {
467 let mut v: Vec<(&str, &CudaSlice<f32>)> = vec![("x", x)];
468 let names = ["g6_0", "g6_1", "g6_2", "g6_3", "g6_4", "g6_5"];
469 for (k, b) in g6.iter().enumerate() {
470 v.push((names[k.min(5)], b));
471 }
472 v.push(("forget_down", &forget_down));
473 v.push(("gate_down", &gate_down));
474 v.push(("beta_raw", &beta_raw));
475 kda_trace(e, "proj", t, &v);
476 }
477 let v_raw = g6.pop().unwrap(); // [T, qkv]
478 let k_raw = g6.pop().unwrap();
479 let q_raw = g6.pop().unwrap();
480
481 // Rows stash: snapshot the ring BEFORE the rolls mutate it (one ~96 KiB clone per
482 // layer per round — the rollback's re-roll base). Door W: on the rows arm the snapshot
483 // (and every scratch below) is a pooled draw — vws_uninit == alloc_uninit with the
484 // door off, and the non-rows arms keep the plain allocs untouched.
485 let ring_snap = match &stash {
486 KdaStash::Rows(_) => {
487 let mut snap = e.vws_uninit(ring.len())?;
488 e.dtod_copy_into(ring, &mut snap, 0)?;
489 Some(snap)
490 }
491 _ => None,
492 };
493
494 // Stage 2 — per-plane causal short conv + SiLU. Planes are ordered q, k, v in both the fused
495 // weight buffer and the fused ring, which is the order the reference stores conv_state in.
496 let mut q_conv = if rows_exact {
497 e.vws_uninit(t * qkv)?
498 } else {
499 e.uninit(t * qkv)?
500 };
501 let mut k_conv = if rows_exact {
502 e.vws_uninit(t * qkv)?
503 } else {
504 e.uninit(t * qkv)?
505 };
506 let mut v_conv = if rows_exact {
507 e.vws_uninit(t * qkv)?
508 } else {
509 e.uninit(t * qkv)?
510 };
511 // MEMRA_KDA_CONV3 (lane/glm5-kda-conv3-20260904, default OFF): the decode arm's three
512 // per-plane launches as ONE (plane = blockIdx.y), bit-identical per channel; the prefill arm
513 // and the door-OFF decode keep the per-plane loop verbatim.
514 if arm == ConvArm::Decode && kda_conv3_on() && kernel <= 9 {
515 e.kda_conv_silu_decode3(
516 [&q_raw, &k_raw, &v_raw],
517 ring,
518 &la.conv,
519 [&mut q_conv, &mut k_conv, &mut v_conv],
520 qkv,
521 kernel,
522 )?;
523 } else {
524 for (plane, (raw, out)) in [
525 (&q_raw, &mut q_conv),
526 (&k_raw, &mut k_conv),
527 (&v_raw, &mut v_conv),
528 ]
529 .into_iter()
530 .enumerate()
531 {
532 match arm {
533 ConvArm::Prefill => {
534 e.kda_conv_silu(raw, &la.conv, ring, out, qkv, t, kernel, plane)?
535 }
536 ConvArm::Decode => {
537 e.kda_conv_silu_decode(raw, ring, &la.conv, out, qkv, kernel, plane)?
538 }
539 }
540 }
541 }
542 // The prefill arm reads the OLD ring for every token, so the roll runs only after all three
543 // planes have been convolved. The decode arm already rolled inside its fused kernel.
544 if arm == ConvArm::Prefill {
545 for (plane, raw) in [&q_raw, &k_raw, &v_raw].into_iter().enumerate() {
546 e.kda_conv_ring_roll(raw, ring, qkv, t, kernel, plane)?;
547 }
548 }
549
550 // Stage 3 — q/k L2 norm over head_dim (eps INSIDE the sqrt, fixed 1e-6). Rows of the
551 // token-major layout are contiguous head_dim runs, so no repack is needed.
552 let mut q_l2 = if rows_exact {
553 e.vws_uninit(t * qkv)?
554 } else {
555 e.uninit(t * qkv)?
556 };
557 let mut k_l2 = if rows_exact {
558 e.vws_uninit(t * qkv)?
559 } else {
560 e.uninit(t * qkv)?
561 };
562 e.l2_norm(&q_conv, &mut q_l2, head_dim, t * heads, KDA_L2_EPS)?;
563 e.l2_norm(&k_conv, &mut k_l2, head_dim, t * heads, KDA_L2_EPS)?;
564 kda_trace(
565 e,
566 "conv+l2",
567 t,
568 &[
569 ("q_conv", &q_conv),
570 ("k_conv", &k_conv),
571 ("v_conv", &v_conv),
572 ("q_l2", &q_l2),
573 ("k_l2", &k_l2),
574 ],
575 );
576 // Door W: the convs' last readers were the l2 norms (the ring rolls read the raws).
577 if rows_exact {
578 e.vws_recycle(q_conv);
579 e.vws_recycle(k_conv);
580 }
581
582 // Stage 4 — gates. forget: g = lower_bound * sigmoid(exp(A_log[h]) * (f_b(f_a(x)) + dt_bias)),
583 // emitted RAW (the scan applies expf). beta: per-head sigmoid of its own projection.
584 let forget = if rows_exact {
585 e.matmul_rows_exact(&la.f_b, &forget_down, t)?
586 } else {
587 matmul_lowrank(e, &la.f_b, &forget_down, t, "f_b")?
588 };
589 let mut g_log = if rows_exact {
590 e.vws_uninit(t * qkv)?
591 } else {
592 e.uninit(t * qkv)?
593 };
594 e.kda_gate(
595 &forget,
596 la.dt_bias.float_data(),
597 la.a_log.float_data(),
598 &mut g_log,
599 qkv,
600 t,
601 head_dim,
602 la.plan.gate_lower_bound,
603 )?;
604 let mut beta = if rows_exact {
605 e.vws_uninit(t * heads)?
606 } else {
607 e.uninit(t * heads)?
608 };
609 e.sigmoid(&beta_raw, &mut beta, t * heads)?;
610 kda_trace(e, "gates", t, &[("forget", &forget), ("beta", &beta)]);
611 // Door W: forget_down's last reader was the f_b matmul, forget's the gate kernel,
612 // beta_raw's the sigmoid.
613 if rows_exact {
614 e.vws_recycle(forget_down);
615 e.vws_recycle(forget);
616 e.vws_recycle(beta_raw);
617 }
618
619 // Stage 5 — the delta-rule recurrence. `scale` carries the reference's head_dim^-0.5 query
620 // scale: q feeds only the readout, never the state, so scaling the readout is exact.
621 // At t > 1 the kernel walks the T steps IN-KERNEL over register-resident state — the
622 // sequential chain preserved inside ONE launch (chained-t=1 identity by construction,
623 // held by the scan-chain bit-gate). `scan_clock` is the trace-level-2 instrument: it
624 // drains the stream around the launch so the sequential-class share lands in its own
625 // bucket (shares, never walls).
626 let scale = 1.0 / (head_dim as f32).sqrt();
627 let mut core = if rows_exact {
628 e.vws_uninit(t * qkv)?
629 } else {
630 e.uninit(t * qkv)?
631 };
632 let scan_t0 = scan_clock.as_ref().map(|_| {
633 let _ = e.stream().synchronize();
634 std::time::Instant::now()
635 });
636 e.kda_scan(
637 &q_l2, &k_l2, &v_conv, &g_log, &beta, state_in, state_out, &mut core, heads, t, scale,
638 )?;
639 kda_trace(
640 e,
641 "scan",
642 t,
643 &[
644 ("state_in", state_in),
645 ("core", &core),
646 ("state_out", state_out),
647 ],
648 );
649 if let (Some(ns), Some(t0)) = (scan_clock.take(), scan_t0) {
650 let _ = e.stream().synchronize();
651 *ns += t0.elapsed().as_nanos() as u64;
652 }
653
654 // Stage 6 — sigmoid-gated RMSNorm over head_dim (layer rms eps here, NOT the l2 eps), then
655 // the output projection.
656 let gate = if rows_exact {
657 e.matmul_rows_exact(&la.g_b, &gate_down, t)?
658 } else {
659 matmul_lowrank(e, &la.g_b, &gate_down, t, "g_b")?
660 };
661 let mut gated = if rows_exact {
662 e.vws_uninit(t * qkv)?
663 } else {
664 e.uninit(t * qkv)?
665 };
666 match onorm_q8 {
667 // MEMRA_KDA_ONORM_ZQ8: the fused norm+quantize twin hands `wo` its q8_1 pair.
668 Some(slot) if !rows_exact && head_dim.is_multiple_of(32) => {
669 let pair = e.kda_gated_rmsnorm_zq8(
670 &core,
671 la.o_norm.float_data(),
672 &gate,
673 &mut gated,
674 head_dim,
675 t * heads,
676 eps,
677 )?;
678 *slot = Some(pair);
679 }
680 _ => e.kda_gated_rmsnorm(
681 &core,
682 la.o_norm.float_data(),
683 &gate,
684 &mut gated,
685 head_dim,
686 t * heads,
687 eps,
688 )?,
689 }
690 kda_trace(e, "gated_norm", t, &[("gate", &gate), ("gated", &gated)]);
691 // Door W: gate_down's last reader was the g_b matmul; core's and gate's the
692 // gated-rmsnorm above.
693 if rows_exact {
694 e.vws_recycle(gate_down);
695 e.vws_recycle(gate);
696 e.vws_recycle(core);
697 }
698 // Steal the scan/conv inputs for the caller's rollback stash: stage 5 has consumed
699 // the scan inputs and the rolls were the raws' last readers — moving them out is
700 // free (no copy, no launch; the buffers were allocated this call either way).
701 match stash {
702 KdaStash::None => {}
703 KdaStash::Decode(s) => {
704 *s = Some(KdaScanInputs {
705 q: q_l2,
706 k: k_l2,
707 v: v_conv,
708 g: g_log,
709 beta,
710 });
711 }
712 KdaStash::Rows(s) => {
713 // Door W: the PREVIOUS round's stash dies here — its nine buffers restock
714 // the pool instead of falling to nine async frees (per layer per round).
715 if let Some(old) = s.take() {
716 e.vws_recycle(old.ring_snap);
717 for r in old.raws {
718 e.vws_recycle(r);
719 }
720 e.vws_recycle(old.scan.q);
721 e.vws_recycle(old.scan.k);
722 e.vws_recycle(old.scan.v);
723 e.vws_recycle(old.scan.g);
724 e.vws_recycle(old.scan.beta);
725 }
726 *s = Some(KdaRowsStash {
727 ring_snap: ring_snap.expect("rows arm snapshotted the ring above"),
728 raws: [q_raw, k_raw, v_raw],
729 scan: KdaScanInputs {
730 q: q_l2,
731 k: k_l2,
732 v: v_conv,
733 g: g_log,
734 beta,
735 },
736 rows: t,
737 });
738 }
739 }
740 Ok(gated)
741}
742
743/// STATELESS prefill from a zero conv ring and a zero recurrent state — the arm the logits-only
744/// forward paths take. Allocates and discards both state buffers.
745pub fn kda_attn(
746 e: &Engine,
747 la: &KdaAttnLayer,
748 x: &CudaSlice<f32>,
749 t: usize,
750 eps: f32,
751) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
752 let mut ring = e.zeros(la.conv_width() * (la.conv_kernel() - 1))?;
753 let state_in = e.zeros(la.state_width())?;
754 let mut state_out = e.zeros(la.state_width())?;
755 kda_core(
756 e,
757 la,
758 x,
759 t,
760 eps,
761 &mut ring,
762 &state_in,
763 &mut state_out,
764 ConvArm::Prefill,
765 KdaStash::None,
766 None,
767 None,
768 )
769}
770
771/// STATEFUL prefill: carries the ring forward and advances the recurrent state from `state_in`
772/// into `state_out`. Callers own the ping-pong; the two state buffers must be distinct.
773#[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
774pub fn kda_attn_prime(
775 e: &Engine,
776 la: &KdaAttnLayer,
777 x: &CudaSlice<f32>,
778 t: usize,
779 eps: f32,
780 ring: &mut CudaSlice<f32>,
781 state_in: &CudaSlice<f32>,
782 state_out: &mut CudaSlice<f32>,
783) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
784 kda_core(
785 e,
786 la,
787 x,
788 t,
789 eps,
790 ring,
791 state_in,
792 state_out,
793 ConvArm::Prefill,
794 KdaStash::None,
795 None,
796 None,
797 )
798}
799
800/// T=1 decode step. Same math as a one-token prime; separate conv arm so the fused
801/// assemble+conv+roll kernel keeps decode and the spec verify on one dispatch class.
802pub fn kda_attn_decode(
803 e: &Engine,
804 la: &KdaAttnLayer,
805 x: &CudaSlice<f32>,
806 eps: f32,
807 ring: &mut CudaSlice<f32>,
808 state_in: &CudaSlice<f32>,
809 state_out: &mut CudaSlice<f32>,
810) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
811 kda_core(
812 e,
813 la,
814 x,
815 1,
816 eps,
817 ring,
818 state_in,
819 state_out,
820 ConvArm::Decode,
821 KdaStash::None,
822 None,
823 None,
824 )
825}
826
827/// Stateful KDA against the shared recurrent-state carrier, in the eager GDN discipline: the
828/// scan reads `ssm_state` and writes the spare `ssm_state_alt`, then the two OWNED resident
829/// buffers swap in place. Stable pointers, no per-step alloc/free — the per-step scratch this
830/// replaced churned the stream-ordered pool and made decode run-to-run nondeterministic
831/// (crates/memra-kv `RecurLayer::ssm_state_alt`). NOT capture-safe: a captured graph bakes
832/// capture-time pointers and never re-runs the host swap, which is why the capture loops refuse.
833#[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
834fn kda_cached(
835 e: &Engine,
836 la: &KdaAttnLayer,
837 x: &CudaSlice<f32>,
838 t: usize,
839 eps: f32,
840 cache: &mut Cache,
841 il: usize,
842 arm: ConvArm,
843 stash: KdaStash<'_>,
844 scan_clock: Option<&mut u64>,
845 pre_q8: KdaPreQ8<'_>,
846) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
847 let rl = cache.recur[il].as_mut().ok_or_else(|| {
848 format!(
849 "blk.{il}: KDA layer has no recurrent state — the cache allocator saw a \
850 non-Recurrent StatePlan for a KDA layer"
851 )
852 })?;
853 let out = {
854 let RecurLayer {
855 conv_state,
856 ssm_state,
857 ssm_state_alt,
858 } = rl;
859 kda_core(
860 e,
861 la,
862 x,
863 t,
864 eps,
865 conv_state,
866 ssm_state,
867 ssm_state_alt,
868 arm,
869 stash,
870 scan_clock,
871 pre_q8,
872 )?
873 };
874 std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
875 Ok(out)
876}
877
878/// Stateful prefill of `t` tokens through the cache's KDA state for layer `il`.
879pub fn kda_prime_cached(
880 e: &Engine,
881 la: &KdaAttnLayer,
882 x: &CudaSlice<f32>,
883 t: usize,
884 eps: f32,
885 cache: &mut Cache,
886 il: usize,
887) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
888 kda_cached(
889 e,
890 la,
891 x,
892 t,
893 eps,
894 cache,
895 il,
896 ConvArm::Prefill,
897 KdaStash::None,
898 None,
899 None,
900 )
901}
902
903/// One decode step through the cache's KDA state for layer `il`.
904/// `MEMRA_KDA_CONV3` (lane/glm5-kda-conv3-20260904; default ON on sm_100a builds since 2026-09-04,
905/// OFF elsewhere, `=0`/`=1` override): the T=1 KDA conv+SiLU runs its three planes in one launch. Read PER CALL. Why and receipts: the kernel header in
906/// cu/kda.cu and docs/FLAGS.md.
907pub(crate) fn kda_conv3_on() -> bool {
908 kda_conv3_on_from(
909 std::env::var("MEMRA_KDA_CONV3").ok().as_deref(),
910 env!("MEMRA_BUILT_CUDA_ARCH"),
911 )
912}
913
914/// The pure parse behind [`kda_conv3_on`]: `1` arms, `0` disarms, unset follows the BUILD ARCH
915/// (ON for `100a`, OFF otherwise): the fused launch carries a 2x B200 receipt (+1.82% at c1,
916/// darklanes research/glm5-b200-20260902/LANE.md, convab) and no SM120 one.
917pub fn kda_conv3_on_from(v: Option<&str>, built_arch: &str) -> bool {
918 match v.map(str::trim) {
919 Some("1") => true,
920 Some("0") => false,
921 _ => built_arch == "100a",
922 }
923}
924
925/// Engagement counter for `MEMRA_KDA_CONV3`; gates take a delta.
926pub static KDA_CONV3_DISPATCHES: std::sync::atomic::AtomicU64 =
927 std::sync::atomic::AtomicU64::new(0);
928
929/// Snapshot of [`KDA_CONV3_DISPATCHES`].
930pub fn kda_conv3_dispatches() -> u64 {
931 KDA_CONV3_DISPATCHES.load(std::sync::atomic::Ordering::Relaxed)
932}
933
934/// A pre-quantized q8_1 view of the mixer input (`(aq, ad)` from `rms_norm_zq8_f32`), or
935/// `None` for the launcher to quantize itself. Threaded from the walk to the fused
936/// six-projection launcher (`MEMRA_GLM5_Q8_FUSE_ATTN`, lane/glm5-attn-norm-zq8-20260904).
937pub type KdaPreQ8<'a> = Option<(&'a CudaSlice<i8>, &'a CudaSlice<f32>)>;
938
939/// `MEMRA_KDA_ONORM_ZQ8=1` (lane/kda-onorm-zq8-20260905, default OFF pending its model-scale row):
940/// the decode (t=1, non-rows) KDA core emits the o_norm output's q8_1 pair from the gated-norm
941/// launch itself (`memra_kda_gated_rmsnorm_zq8_f32`) and hands it to the `wo` MMVQ through
942/// `matmul_q8_fast`, dropping the standalone `quantize_q8_1` launch (34 per token on
943/// GLM-5.3-Flash, in-graph). BIT-IDENTICAL: same norm bytes, same q8_1 arithmetic as
944/// `quantize_q8_1` (gate `tests/kda_onorm_zq8_gpu.rs` + the decode-graph fixture arm). When `wo`
945/// is not MMVQ-fast-eligible the pair is dropped and `matmul` runs unchanged. Read per call.
946pub fn kda_onorm_zq8_on() -> bool {
947 std::env::var("MEMRA_KDA_ONORM_ZQ8").as_deref() == Ok("1")
948}
949
950/// Launches of the fused o_norm+quantize kernel whose pair the `wo` MMVQ consumed.
951pub static KDA_ONORM_ZQ8_DISPATCHES: std::sync::atomic::AtomicU64 =
952 std::sync::atomic::AtomicU64::new(0);
953
954/// The o_norm q8_1 pair handed from [`kda_core_gated`] to the `wo` projection.
955pub type KdaOnormQ8 = Option<(CudaSlice<i8>, CudaSlice<f32>)>;
956
957/// `MEMRA_KDA_NARROW_Q8=1` (lane/kda-narrow-q8-20260905, default OFF pending its model-scale row):
958/// the decode KDA core's low-rank `f_b` / `g_b` projections (128-wide inputs) ride
959/// `qmatvec_q8_0_mmvq_f32in_narrow`, which quantizes the row inside the launch, instead of
960/// `quantize_q8_1` + `qmatvec_q8_0_mmvq` (two launches per projection, 68 per token, in-graph).
961/// BIT-IDENTICAL: quantize_q8_1's arithmetic then the mmvq body verbatim (gate
962/// `tests/q8_narrow_f32in_gpu.rs`). Shapes or tensors that do not fit keep `matmul`. Read per call.
963pub fn kda_narrow_q8_on() -> bool {
964 std::env::var("MEMRA_KDA_NARROW_Q8").as_deref() == Ok("1")
965}
966
967/// Launches of the narrow in-kernel-quantize MMVQ on the f_b / g_b sites.
968pub static KDA_NARROW_Q8_DISPATCHES: std::sync::atomic::AtomicU64 =
969 std::sync::atomic::AtomicU64::new(0);
970
971/// `matmul` for a decode-arm low-rank projection, through the narrow in-kernel-quantize twin
972/// when the door is on and the shape fits, else the unchanged `matmul`.
973fn matmul_lowrank(
974 e: &Engine,
975 w: &crate::model::GpuTensor,
976 x: &CudaSlice<f32>,
977 t: usize,
978 which: &str,
979) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
980 if kda_narrow_q8_on()
981 && let Some(y) = e.matmul_q8_narrow_f32in(w, x, t)?
982 {
983 if KDA_NARROW_Q8_DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 0 {
984 eprintln!(
985 "[kda-narrow-q8] engaged: the KDA {which} projection quantizes its {}-wide input \
986 inside the MMVQ launch (MEMRA_KDA_NARROW_Q8=1)",
987 w.in_features()
988 );
989 }
990 return Ok(y);
991 }
992 e.matmul(w, x, t)
993}
994
995/// [`kda_decode_cached`] with the mixer input's q8_1 view already emitted by the caller's norm
996/// (`MEMRA_GLM5_Q8_FUSE_ATTN`): identical launches minus the fused launcher's own quantize.
997pub fn kda_decode_cached_q8(
998 e: &Engine,
999 la: &KdaAttnLayer,
1000 x: &CudaSlice<f32>,
1001 pre_q8: KdaPreQ8<'_>,
1002 eps: f32,
1003 cache: &mut Cache,
1004 il: usize,
1005) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1006 kda_cached(
1007 e,
1008 la,
1009 x,
1010 1,
1011 eps,
1012 cache,
1013 il,
1014 ConvArm::Decode,
1015 KdaStash::None,
1016 None,
1017 pre_q8,
1018 )
1019}
1020
1021pub fn kda_decode_cached(
1022 e: &Engine,
1023 la: &KdaAttnLayer,
1024 x: &CudaSlice<f32>,
1025 eps: f32,
1026 cache: &mut Cache,
1027 il: usize,
1028) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1029 kda_cached(
1030 e,
1031 la,
1032 x,
1033 1,
1034 eps,
1035 cache,
1036 il,
1037 ConvArm::Decode,
1038 KdaStash::None,
1039 None,
1040 None,
1041 )
1042}
1043
1044/// [`kda_decode_cached`] with the step's scan inputs STOLEN for a rollback stash
1045/// (loop-port 3; doc on [`KdaScanInputs`]). Identical launches — the steal is a move of
1046/// buffers the step allocated either way.
1047pub fn kda_decode_cached_stash(
1048 e: &Engine,
1049 la: &KdaAttnLayer,
1050 x: &CudaSlice<f32>,
1051 eps: f32,
1052 cache: &mut Cache,
1053 il: usize,
1054) -> Result<(CudaSlice<f32>, KdaScanInputs), Box<dyn std::error::Error>> {
1055 let mut stash: Option<KdaScanInputs> = None;
1056 let out = kda_cached(
1057 e,
1058 la,
1059 x,
1060 1,
1061 eps,
1062 cache,
1063 il,
1064 ConvArm::Decode,
1065 KdaStash::Decode(&mut stash),
1066 None,
1067 None,
1068 )?;
1069 let stash = stash.ok_or("kda_core returned without filling the requested scan stash")?;
1070 Ok((out, stash))
1071}
1072
1073/// THE BATCHED VERIFY-ROWS KDA CALL (lane/glm5-verify-batch): one t=K+1 `kda_core` pass
1074/// per layer per round, replacing t per-row [`kda_decode_cached_stash`] calls. Projections,
1075/// gates and norms batch m=t through the decode-exact matmul classes (`matmul_rows_exact`);
1076/// the conv takes the prefill dispatch (per-token bit-identical to the decode arm's taps);
1077/// the recurrence stays SEQUENTIAL inside one `memra_kda_scan_s128` launch (the in-kernel
1078/// T-loop over register-resident state == the chained t=1 program). Per-row bit-identity
1079/// vs the t=1 chain is held by the walk gates (`glm5_tparallel_verify_gpu`) and the
1080/// kernel bit-gates (`glm5_verify_batch_gpu`).
1081///
1082/// The caller owns the pre-round ssm snapshot (`Glm5VerifyCkpt::kda_ssm_snap`, cloned
1083/// BEFORE this call); the returned [`KdaRowsStash`] carries everything else rollback
1084/// needs. `scan_clock`: the trace-level-2 sequential-class bucket (ns accumulated around
1085/// the scan launch with stream drains — an instrument, never a serving mode).
1086#[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kda_cached call contract plus the trace clock
1087pub fn kda_verify_rows_cached(
1088 e: &Engine,
1089 la: &KdaAttnLayer,
1090 x: &CudaSlice<f32>,
1091 t: usize,
1092 eps: f32,
1093 cache: &mut Cache,
1094 il: usize,
1095 scan_clock: Option<&mut u64>,
1096) -> Result<(CudaSlice<f32>, KdaRowsStash), Box<dyn std::error::Error>> {
1097 let mut stash: Option<KdaRowsStash> = None;
1098 let out = kda_cached(
1099 e,
1100 la,
1101 x,
1102 t,
1103 eps,
1104 cache,
1105 il,
1106 ConvArm::Prefill,
1107 KdaStash::Rows(&mut stash),
1108 scan_clock,
1109 None,
1110 )?;
1111 let stash = stash.ok_or("kda_core returned without filling the requested rows stash")?;
1112 Ok((out, stash))
1113}
1114
1115/// Roll layer `il` back to "after row `keep-1`" from a BATCHED verify-rows round
1116/// (lane/glm5-verify-batch; the [`KdaRowsStash`] doc states the two-plane contract):
1117/// restore the pre-round conv ring and re-roll `keep` raw rows (pure placement), then
1118/// replay the scan ONCE at T=keep from the pre-round ssm snapshot over the batched
1119/// inputs. Full accept (`keep == rows`) never calls this — the resident state IS the
1120/// state after the last kept row.
1121pub fn kda_verify_rollback_rows(
1122 e: &Engine,
1123 la: &KdaAttnLayer,
1124 snap: &CudaSlice<f32>,
1125 stash: &KdaRowsStash,
1126 keep: usize,
1127 cache: &mut Cache,
1128 il: usize,
1129) -> Result<(), Box<dyn std::error::Error>> {
1130 let rl = cache.recur[il]
1131 .as_mut()
1132 .ok_or_else(|| format!("blk.{il}: KDA rows rollback on a layer with no recurrent state"))?;
1133 kda_verify_rollback_rows_on(e, la, snap, stash, keep, rl, il)
1134}
1135
1136/// [`kda_verify_rollback_rows`] over a CALLER-OWNED state plane — the glm5 spec x TP seam
1137/// (lane/glm5-composition): under `MEMRA_GLM5_TP` each rank's shard-geometry conv ring +
1138/// ssm ping-pong lives in `cache.glm5_tp_recur[il][rank]` on that rank's engine, so the
1139/// rollback restores per rank through this entry with the rank's own `(engine, shard,
1140/// snapshot, stash)` tuple. The cache wrapper above delegates here — one body, byte-for-byte
1141/// the pre-refactor walk on the plain path.
1142pub fn kda_verify_rollback_rows_on(
1143 e: &Engine,
1144 la: &KdaAttnLayer,
1145 snap: &CudaSlice<f32>,
1146 stash: &KdaRowsStash,
1147 keep: usize,
1148 rl: &mut RecurLayer,
1149 il: usize,
1150) -> Result<(), Box<dyn std::error::Error>> {
1151 if keep == 0 || keep >= stash.rows {
1152 return Err(format!(
1153 "blk.{il}: KDA rows rollback keep={keep} outside 1..{} (full accept keeps the \
1154 resident state and never replays)",
1155 stash.rows
1156 )
1157 .into());
1158 }
1159 let qkv = la.qkv();
1160 let kernel = la.conv_kernel();
1161 let heads = la.heads();
1162 let scale = 1.0 / (la.head_dim() as f32).sqrt();
1163 // Conv ring: pre-round snapshot back, then re-roll the kept raw rows per plane. The
1164 // roll kernel reads every old slot into registers before any store, so T=keep < pad
1165 // mixes snapshot slots and kept rows exactly as the sequential chain's rolls did.
1166 e.copy_into(
1167 &mut rl.conv_state,
1168 0,
1169 &stash.ring_snap,
1170 stash.ring_snap.len(),
1171 )?;
1172 for (plane, raw) in stash.raws.iter().enumerate() {
1173 e.kda_conv_ring_roll(raw, &mut rl.conv_state, qkv, keep, kernel, plane)?;
1174 }
1175 // Recurrent state: ONE T=keep replay from the snapshot over the batched scan inputs
1176 // (the kernel walks rows 0..keep of the [t, ..] buffers); readout discarded. The
1177 // ping-pong ends with the rebuilt state under the `ssm_state` name, matching
1178 // `kda_cached`'s swap discipline.
1179 let mut o = e.uninit(keep * qkv)?;
1180 {
1181 let RecurLayer {
1182 ssm_state: _,
1183 ssm_state_alt,
1184 ..
1185 } = rl;
1186 e.kda_scan(
1187 &stash.scan.q,
1188 &stash.scan.k,
1189 &stash.scan.v,
1190 &stash.scan.g,
1191 &stash.scan.beta,
1192 snap,
1193 ssm_state_alt,
1194 &mut o,
1195 heads,
1196 keep,
1197 scale,
1198 )?;
1199 }
1200 std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
1201 Ok(())
1202}
1203
1204/// Rebuild layer `il`'s recurrent state to "after row `inputs.len()-1`" by REPLAYING the
1205/// stashed scan inputs from the pre-round snapshot `snap` (loop-port 3, the module-doc
1206/// diet made concrete): each replay is the original t=1 `memra_kda_scan_s128` launch
1207/// re-issued over the very buffers that step consumed, so the rebuilt state is
1208/// byte-identical to the per-row clone it replaces BY CONSTRUCTION. The readout is
1209/// discarded; the conv ring is not touched (the walk still clones it per row — 288 KiB
1210/// against the 4 MiB ssm plane this retires). The ping-pong rides the resident pair and
1211/// ends with the rebuilt state under the `ssm_state` name, matching `kda_cached`'s own
1212/// swap discipline.
1213pub fn kda_scan_replay(
1214 e: &Engine,
1215 la: &KdaAttnLayer,
1216 snap: &CudaSlice<f32>,
1217 inputs: &[KdaScanInputs],
1218 cache: &mut Cache,
1219 il: usize,
1220) -> Result<(), Box<dyn std::error::Error>> {
1221 if inputs.is_empty() {
1222 return Err(format!(
1223 "blk.{il}: KDA replay needs at least one stashed row (rollback keep >= 1; a \
1224 restore TO the snapshot itself is a different contract)"
1225 )
1226 .into());
1227 }
1228 if la.tp.is_some() {
1229 return Err(format!(
1230 "blk.{il}: KDA scan replay (the PER-ROW rollback seam) is unwired for a \
1231 glm5-TP-sharded layer — the spec x TP composition requires the BATCHED \
1232 verify walk, whose rollback rides kda_verify_rollback_rows_on per rank"
1233 )
1234 .into());
1235 }
1236 let heads = la.heads();
1237 let scale = 1.0 / (la.head_dim() as f32).sqrt();
1238 let qkv = la.qkv();
1239 let rl = cache.recur[il]
1240 .as_mut()
1241 .ok_or_else(|| format!("blk.{il}: KDA replay on a layer with no recurrent state"))?;
1242 let mut o = e.uninit(qkv)?; // discarded readout scratch, reused across rows
1243 for (r, inp) in inputs.iter().enumerate() {
1244 {
1245 let RecurLayer {
1246 ssm_state,
1247 ssm_state_alt,
1248 ..
1249 } = rl;
1250 let state_in: &CudaSlice<f32> = if r == 0 { snap } else { ssm_state };
1251 e.kda_scan(
1252 &inp.q,
1253 &inp.k,
1254 &inp.v,
1255 &inp.g,
1256 &inp.beta,
1257 state_in,
1258 ssm_state_alt,
1259 &mut o,
1260 heads,
1261 1,
1262 scale,
1263 )?;
1264 }
1265 std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
1266 }
1267 Ok(())
1268}
1269
1270impl Engine {
1271 /// Per-plane causal short conv + SiLU over a T-token chunk (cu/kda.cu).
1272 #[allow(clippy::too_many_arguments)]
1273 pub fn kda_conv_silu(
1274 &self,
1275 x_tm: &CudaSlice<f32>,
1276 w: &CudaSlice<f32>,
1277 ring: &CudaSlice<f32>,
1278 y_tm: &mut CudaSlice<f32>,
1279 qkv: usize,
1280 t: usize,
1281 kernel: usize,
1282 plane: usize,
1283 ) -> Result<(), Box<dyn std::error::Error>> {
1284 let f = self.func("memra_kda_conv_silu_f32");
1285 let cfg = LaunchConfig {
1286 grid_dim: (qkv.div_ceil(256) as u32, t as u32, 1),
1287 block_dim: (256, 1, 1),
1288 shared_mem_bytes: 0,
1289 };
1290 let (n, tt, k, p) = (qkv as i32, t as i32, kernel as i32, plane as i32);
1291 let stream = self.gpu.stream();
1292 let mut b = stream.launch_builder(&f);
1293 b.arg(x_tm)
1294 .arg(w)
1295 .arg(ring)
1296 .arg(&mut *y_tm)
1297 .arg(&n)
1298 .arg(&tt)
1299 .arg(&k)
1300 .arg(&p);
1301 unsafe { b.launch(cfg)? };
1302 Ok(())
1303 }
1304
1305 /// Roll one plane of the fused conv ring forward over a T-token chunk (cu/kda.cu).
1306 pub fn kda_conv_ring_roll(
1307 &self,
1308 x_tm: &CudaSlice<f32>,
1309 ring: &mut CudaSlice<f32>,
1310 qkv: usize,
1311 t: usize,
1312 kernel: usize,
1313 plane: usize,
1314 ) -> Result<(), Box<dyn std::error::Error>> {
1315 let f = self.func("memra_kda_conv_ring_roll_f32");
1316 let cfg = LaunchConfig {
1317 grid_dim: (qkv.div_ceil(256) as u32, 1, 1),
1318 block_dim: (256, 1, 1),
1319 shared_mem_bytes: 0,
1320 };
1321 let (n, tt, k, p) = (qkv as i32, t as i32, kernel as i32, plane as i32);
1322 let stream = self.gpu.stream();
1323 let mut b = stream.launch_builder(&f);
1324 b.arg(x_tm).arg(&mut *ring).arg(&n).arg(&tt).arg(&k).arg(&p);
1325 unsafe { b.launch(cfg)? };
1326 Ok(())
1327 }
1328
1329 /// T=1 fused assemble + conv + SiLU + ring roll for one plane (cu/kda.cu).
1330 #[allow(clippy::too_many_arguments)]
1331 /// The three-plane form of [`Engine::kda_conv_silu_decode`] (door `MEMRA_KDA_CONV3`,
1332 /// lane/glm5-kda-conv3-20260904): one launch with `plane = blockIdx.y` in place of the three
1333 /// per-plane launches; per channel the same body, so outputs and the ring are bit-identical
1334 /// (gate `tests/kda_conv3_gpu.rs`). `kernel` (K) must be at most 9 (the `win[8]` window).
1335 #[allow(clippy::too_many_arguments)]
1336 pub fn kda_conv_silu_decode3(
1337 &self,
1338 x: [&CudaSlice<f32>; 3],
1339 ring: &mut CudaSlice<f32>,
1340 w: &CudaSlice<f32>,
1341 y: [&mut CudaSlice<f32>; 3],
1342 qkv: usize,
1343 kernel: usize,
1344 ) -> Result<(), Box<dyn std::error::Error>> {
1345 if kernel == 0 || kernel > 9 {
1346 return Err("kda_conv_silu_decode3: kernel width outside the 8-wide window".into());
1347 }
1348 if KDA_CONV3_DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 0 {
1349 eprintln!(
1350 "[kda-conv3] engaged: the three KDA conv+SiLU decode planes run as one launch \
1351 (MEMRA_KDA_CONV3=1)"
1352 );
1353 }
1354 let f = self.func("memra_kda_conv_silu_decode3_f32");
1355 let cfg = LaunchConfig {
1356 grid_dim: (qkv.div_ceil(256) as u32, 3, 1),
1357 block_dim: (256, 1, 1),
1358 shared_mem_bytes: 0,
1359 };
1360 let (n, k) = (qkv as i32, kernel as i32);
1361 let [x0, x1, x2] = x;
1362 let [y0, y1, y2] = y;
1363 let stream = self.gpu.stream();
1364 let mut b = stream.launch_builder(&f);
1365 b.arg(x0)
1366 .arg(x1)
1367 .arg(x2)
1368 .arg(&mut *ring)
1369 .arg(w)
1370 .arg(&mut *y0)
1371 .arg(&mut *y1)
1372 .arg(&mut *y2)
1373 .arg(&n)
1374 .arg(&k);
1375 unsafe { b.launch(cfg)? };
1376 Ok(())
1377 }
1378
1379 #[allow(clippy::too_many_arguments)]
1380 pub fn kda_conv_silu_decode(
1381 &self,
1382 x_new: &CudaSlice<f32>,
1383 ring: &mut CudaSlice<f32>,
1384 w: &CudaSlice<f32>,
1385 y: &mut CudaSlice<f32>,
1386 qkv: usize,
1387 kernel: usize,
1388 plane: usize,
1389 ) -> Result<(), Box<dyn std::error::Error>> {
1390 let f = self.func("memra_kda_conv_silu_decode_f32");
1391 let cfg = LaunchConfig {
1392 grid_dim: (qkv.div_ceil(256) as u32, 1, 1),
1393 block_dim: (256, 1, 1),
1394 shared_mem_bytes: 0,
1395 };
1396 let (n, k, p) = (qkv as i32, kernel as i32, plane as i32);
1397 let stream = self.gpu.stream();
1398 let mut b = stream.launch_builder(&f);
1399 b.arg(x_new)
1400 .arg(&mut *ring)
1401 .arg(w)
1402 .arg(&mut *y)
1403 .arg(&n)
1404 .arg(&k)
1405 .arg(&p);
1406 unsafe { b.launch(cfg)? };
1407 Ok(())
1408 }
1409
1410 /// Per-channel forget gate, emitted as the RAW log-gate (cu/kda.cu).
1411 #[allow(clippy::too_many_arguments)]
1412 pub fn kda_gate(
1413 &self,
1414 forget: &CudaSlice<f32>,
1415 dt_bias: &CudaSlice<f32>,
1416 a_log: &CudaSlice<f32>,
1417 g: &mut CudaSlice<f32>,
1418 qkv: usize,
1419 t: usize,
1420 head_dim: usize,
1421 lower_bound: f32,
1422 ) -> Result<(), Box<dyn std::error::Error>> {
1423 let f = self.func("memra_kda_gate_f32");
1424 let cfg = LaunchConfig {
1425 grid_dim: (qkv.div_ceil(256) as u32, t as u32, 1),
1426 block_dim: (256, 1, 1),
1427 shared_mem_bytes: 0,
1428 };
1429 let (n, tt, hd, lb) = (qkv as i32, t as i32, head_dim as i32, lower_bound);
1430 let stream = self.gpu.stream();
1431 let mut b = stream.launch_builder(&f);
1432 b.arg(forget)
1433 .arg(dt_bias)
1434 .arg(a_log)
1435 .arg(&mut *g)
1436 .arg(&n)
1437 .arg(&tt)
1438 .arg(&hd)
1439 .arg(&lb);
1440 unsafe { b.launch(cfg)? };
1441 Ok(())
1442 }
1443
1444 /// The per-channel-decay delta-rule scan (cu/kda.cu). One warp per output column.
1445 #[allow(clippy::too_many_arguments)]
1446 pub fn kda_scan(
1447 &self,
1448 q: &CudaSlice<f32>,
1449 k: &CudaSlice<f32>,
1450 v: &CudaSlice<f32>,
1451 g: &CudaSlice<f32>,
1452 beta: &CudaSlice<f32>,
1453 state_in: &CudaSlice<f32>,
1454 state_out: &mut CudaSlice<f32>,
1455 o: &mut CudaSlice<f32>,
1456 heads: usize,
1457 t: usize,
1458 scale: f32,
1459 ) -> Result<(), Box<dyn std::error::Error>> {
1460 // Four columns per block keeps one warp per column at 128 threads, the same shape
1461 // gdn_scan_s128 launches with.
1462 const COLS_PER_BLOCK: u32 = 4;
1463 let f = self.func("memra_kda_scan_s128");
1464 let cfg = LaunchConfig {
1465 grid_dim: (
1466 heads as u32,
1467 1,
1468 (KDA_HEAD_DIM as u32).div_ceil(COLS_PER_BLOCK),
1469 ),
1470 block_dim: (32, COLS_PER_BLOCK, 1),
1471 shared_mem_bytes: 0,
1472 };
1473 let (h, tt, s) = (heads as i32, t as i32, scale);
1474 let stream = self.gpu.stream();
1475 let mut b = stream.launch_builder(&f);
1476 b.arg(q)
1477 .arg(k)
1478 .arg(v)
1479 .arg(g)
1480 .arg(beta)
1481 .arg(state_in)
1482 .arg(&mut *state_out)
1483 .arg(&mut *o)
1484 .arg(&h)
1485 .arg(&tt)
1486 .arg(&s);
1487 unsafe { b.launch(cfg)? };
1488 Ok(())
1489 }
1490
1491 /// Sigmoid-gated fp32 RMSNorm over head_dim (cu/kda.cu). GDN's `gated_rmsnorm` gates with
1492 /// SiLU; KDA's Glm5NextTextRMSNormGated hardcodes sigmoid.
1493 #[allow(clippy::too_many_arguments)]
1494 pub fn kda_gated_rmsnorm(
1495 &self,
1496 core: &CudaSlice<f32>,
1497 w: &CudaSlice<f32>,
1498 gate: &CudaSlice<f32>,
1499 dst: &mut CudaSlice<f32>,
1500 ncols: usize,
1501 nrows: usize,
1502 eps: f32,
1503 ) -> Result<(), Box<dyn std::error::Error>> {
1504 let f = self.func("memra_kda_gated_rmsnorm_f32");
1505 let cfg = LaunchConfig {
1506 grid_dim: (nrows as u32, 1, 1),
1507 block_dim: (256, 1, 1),
1508 shared_mem_bytes: 0,
1509 };
1510 let (nc, ep) = (ncols as i32, eps);
1511 let stream = self.gpu.stream();
1512 let mut b = stream.launch_builder(&f);
1513 b.arg(core)
1514 .arg(w)
1515 .arg(gate)
1516 .arg(&mut *dst)
1517 .arg(&nc)
1518 .arg(&ep);
1519 unsafe { b.launch(cfg)? };
1520 Ok(())
1521 }
1522
1523 /// [`Engine::kda_gated_rmsnorm`] emitting the q8_1 pair of `dst` beside it
1524 /// (`memra_kda_gated_rmsnorm_zq8_f32`): `dst` byte-identical to the plain kernel, the pair
1525 /// byte-identical to `quantize_q8_1(dst, t, heads*ncols)` (the `wo` MMVQ input). Returns
1526 /// `(q [nrows*ncols], d [nrows*ncols/32])`, which viewed per token is exactly the
1527 /// `[t, heads*ncols]` activation's q8_1 pair. Requires `ncols % 32 == 0`.
1528 #[allow(clippy::too_many_arguments)]
1529 pub fn kda_gated_rmsnorm_zq8(
1530 &self,
1531 core: &CudaSlice<f32>,
1532 w: &CudaSlice<f32>,
1533 gate: &CudaSlice<f32>,
1534 dst: &mut CudaSlice<f32>,
1535 ncols: usize,
1536 nrows: usize,
1537 eps: f32,
1538 ) -> Result<(CudaSlice<i8>, CudaSlice<f32>), Box<dyn std::error::Error>> {
1539 if !ncols.is_multiple_of(32) {
1540 return Err(format!("kda_gated_rmsnorm_zq8 needs ncols % 32 == 0, got {ncols}").into());
1541 }
1542 let mut q = self.alloc_i8_uninit(nrows * ncols)?;
1543 let mut d = self.uninit(nrows * ncols / 32)?;
1544 let f = self.func("memra_kda_gated_rmsnorm_zq8_f32");
1545 let cfg = LaunchConfig {
1546 grid_dim: (nrows as u32, 1, 1),
1547 block_dim: (256, 1, 1),
1548 shared_mem_bytes: 0,
1549 };
1550 let (nc, ep) = (ncols as i32, eps);
1551 let stream = self.gpu.stream();
1552 let mut b = stream.launch_builder(&f);
1553 b.arg(core)
1554 .arg(w)
1555 .arg(gate)
1556 .arg(&mut *dst)
1557 .arg(&mut q)
1558 .arg(&mut d)
1559 .arg(&nc)
1560 .arg(&ep);
1561 unsafe { b.launch(cfg)? };
1562 Ok((q, d))
1563 }
1564
1565 /// The `MEMRA_KDA_FUSED_PROJ` door: run the KDA stage-1 six-projection group as ONE
1566 /// `quantize_q8_1` + ONE `qmatvec_kda6_q8f32_mmvq` launch, or return `None` and let the
1567 /// caller take the unchanged `matmul_group` arm.
1568 ///
1569 /// ENGAGEMENT IS DELIBERATELY NARROW — every condition below exists so the door's numeric
1570 /// claim stays exactly what the gate proves (`tests/kda_fused_proj_gpu.rs`):
1571 /// * wq/wk/wv must be plain-layout Q8_0 (`rp: false`, no `rp4` mirror, `scale == 1.0`) —
1572 /// the fused kernel's per-(token,row) body is `qmatvec_q8_0_mmvq` VERBATIM, so those
1573 /// rows are BIT-IDENTICAL to the unfused MMVQ/batched arm; a repacked layout would ride
1574 /// the `_rp` twins instead and the claim would be against the wrong kernel.
1575 /// * f_a/g_a/b_proj must be f32 `Float` — their fused rows replace cuBLASLt with a
1576 /// deterministic warp tree: a reduction-order class change (the step37 QKV_FUSED class),
1577 /// measured and pinned in the gate.
1578 /// * t in 1..=15 (the batch cap), and the env classes under which the UNFUSED arm rides
1579 /// the MMVQ-class per-row program: `MEMRA_FAST!=0`, `mmvq_supports(Q8_0)`,
1580 /// `MEMRA_NO_BATCHED` unset for t>=2, `MEMRA_B8!=0` for t>=5. Outside those envs the
1581 /// unfused arm is a different kernel class (dp4a / Stage-A), so the door refuses rather
1582 /// than weakening its identity claim.
1583 ///
1584 /// The flag is read PER CALL (the `MEMRA_MOE_FUSED_EPI` rollback-seam precedent), so both
1585 /// arms alternate inside one process. Output order matches `matmul_group`'s:
1586 /// `[q, k, v, forget_down, gate_down, beta_raw]`.
1587 pub fn kda_proj_fused6(
1588 &self,
1589 la: &KdaAttnLayer,
1590 x: &CudaSlice<f32>,
1591 t: usize,
1592 ) -> Result<Option<Vec<CudaSlice<f32>>>, Box<dyn std::error::Error>> {
1593 self.kda_proj_fused6_pre(la, x, t, None)
1594 }
1595
1596 /// [`Engine::kda_proj_fused6`] with an optional pre-quantized activation for the W8 arm
1597 /// (`MEMRA_GLM5_Q8_FUSE_ATTN`); every other arm ignores it (they quantize per projection or
1598 /// run bf16) and stays the program it was.
1599 pub fn kda_proj_fused6_pre(
1600 &self,
1601 la: &KdaAttnLayer,
1602 x: &CudaSlice<f32>,
1603 t: usize,
1604 pre_q8: KdaPreQ8<'_>,
1605 ) -> Result<Option<Vec<CudaSlice<f32>>>, Box<dyn std::error::Error>> {
1606 if std::env::var("MEMRA_KDA_FUSED_PROJ").as_deref() != Ok("1") {
1607 return Ok(None);
1608 }
1609 // glm5 TP composition guard (#82 review): the load preflight refuses this door at
1610 // ARM time, but the flag is read PER CALL — a post-load `set` would otherwise
1611 // engage the fused six-projection group on head shards inside the TP walk, an
1612 // unproven composition (the door's gate ran on full-width projections). A shard
1613 // declines here and takes the caller's unchanged arm, announced once.
1614 if la.tp.is_some() {
1615 static TP_F6_DECLINE: std::sync::Once = std::sync::Once::new();
1616 TP_F6_DECLINE.call_once(|| {
1617 eprintln!(
1618 "[kda-fused-proj] DECLINED on a glm5-TP head shard: the door is gated \
1619 on full-width projections (the load preflight refuses the pair; this \
1620 is the per-call twin for a post-load flag set)"
1621 );
1622 });
1623 return Ok(None);
1624 }
1625 if !(1..=15).contains(&t) {
1626 return Ok(None);
1627 }
1628 // E4M3 SIX-GROUP ARM (lane/glm5-b200-mint-consume, 2026-09-04) — the operand class the
1629 // GLM-5.3-Flash B200 hybrid mint actually ships. That mint quantizes ALL SIX KDA
1630 // projections to per-tensor e4m3, so under MEMRA_ST_E4M3 (default ON) every one of them
1631 // is QT_F8_E4M3-resident at 1.0 B/weight: cheaper than the bf16 serving recipe's 2.0 and
1632 // cheaper than the Q8_0 re-encode's 1.0625, with no lossy re-quant hop. The two arms
1633 // below cannot serve that shape — they require a FloatBf16 trio plus an f32 trio — so
1634 // without this arm the mint's cheapest operand would fall to SIX separate launches on
1635 // each of the 34 KDA layers, with six redundant broadcasts of the same activation.
1636 //
1637 // This is an operand arm of an EXISTING door, not a new one: it rides
1638 // MEMRA_KDA_FUSED_PROJ=1 exactly as the bf16 and q8rp arms do, and it additionally
1639 // declines wherever the unfused e4m3 program it claims bit-identity against would not
1640 // be the shipped one (MEMRA_FAST=0, no MMVQ support for the qtype, or the
1641 // MEMRA_E4M3_DUAL=0 rollback that restores per-tensor launches).
1642 //
1643 // m=1 ONLY. `qmatvec_e4m3_mmvq_fused6` pins the token index at 0, matching the
1644 // `e4m3_mmvq_row1` body it shares with the pair and triple. t>1 keeps the caller's arm.
1645 let e4m3 = |w: &GpuTensor| -> Option<(usize, usize, f32)> {
1646 match w {
1647 GpuTensor::Quant {
1648 qtype: crate::QT_F8_E4M3,
1649 row_bytes,
1650 scale,
1651 rp: false,
1652 rp4: None,
1653 blk: None,
1654 ..
1655 } => Some((w.in_features(), *row_bytes, *scale)),
1656 _ => None,
1657 }
1658 };
1659 if let (Some(e_q), Some(e_k), Some(e_v), Some(e_fa), Some(e_ga), Some(e_b)) = (
1660 e4m3(&la.wq),
1661 e4m3(&la.wk),
1662 e4m3(&la.wv),
1663 e4m3(&la.f_a),
1664 e4m3(&la.g_a),
1665 e4m3(&la.b_proj),
1666 ) {
1667 let six = [e_q, e_k, e_v, e_fa, e_ga, e_b];
1668 let in_f = e_q.0;
1669 if t != 1
1670 || std::env::var("MEMRA_FAST").as_deref() == Ok("0")
1671 || !self.mmvq_supports(crate::QT_F8_E4M3)
1672 || !self.e4m3_dual_on()
1673 // Every range must share in_f and the q8_1 activation block, and an e4m3 row is
1674 // exactly in_f bytes — a row_bytes that disagrees means a padded or foreign
1675 // layout this kernel's single `row_bytes` cannot address.
1676 || six.iter().any(|&(i, rb, _)| i != in_f || rb != in_f)
1677 || !in_f.is_multiple_of(32)
1678 || x.len() < in_f
1679 {
1680 return Ok(None);
1681 }
1682 let dims = [
1683 la.wq.out_features(),
1684 la.wk.out_features(),
1685 la.wv.out_features(),
1686 la.f_a.out_features(),
1687 la.g_a.out_features(),
1688 la.b_proj.out_features(),
1689 ];
1690 let ws: [f32; 6] = std::array::from_fn(|i| six[i].2);
1691 fn e4m3_bytes(w: &GpuTensor) -> &CudaSlice<u8> {
1692 match w {
1693 GpuTensor::Quant { bytes, .. } => bytes,
1694 _ => unreachable!("e4m3() above only admits Quant"),
1695 }
1696 }
1697 let bytes = e4m3_bytes;
1698 let w = [
1699 bytes(&la.wq),
1700 bytes(&la.wk),
1701 bytes(&la.wv),
1702 bytes(&la.f_a),
1703 bytes(&la.g_a),
1704 bytes(&la.b_proj),
1705 ];
1706 // ONE activation quantize for all six ranges — the six-launch path pays this per
1707 // projection. `pre_q8` is the W8 posture's pre-quantized pair and does not apply to
1708 // this operand class, so the arm always quantizes here.
1709 let (aq, ad) = self.quantize_q8_1(x, 1, in_f)?;
1710 let mut outs = [
1711 self.uninit(dims[0])?,
1712 self.uninit(dims[1])?,
1713 self.uninit(dims[2])?,
1714 self.uninit(dims[3])?,
1715 self.uninit(dims[4])?,
1716 self.uninit(dims[5])?,
1717 ];
1718 self.e4m3_fused6_into(w, &aq, &ad, in_f, dims, in_f, ws, &mut outs)?;
1719 if KDA_FUSED6_E4M3_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1720 eprintln!(
1721 "[kda-fused6] engaged arm=e4m3 in_f={in_f} out={dims:?} t={t} (one launch \
1722 replaces the six per-tensor e4m3 projections and their six redundant \
1723 activation quantizes; MEMRA_KDA_FUSED_PROJ=1 MEMRA_ST_E4M3=1)"
1724 );
1725 }
1726 return Ok(Some(outs.into_iter().collect()));
1727 }
1728 // The f32 trio is common to both operand arms. Any mismatch = refuse; the caller's
1729 // arm is the shipped program.
1730 let f32w = |w: &GpuTensor| -> Option<usize> {
1731 match w {
1732 GpuTensor::Float { .. } => Some(w.in_features()),
1733 _ => None,
1734 }
1735 };
1736 let (Some(in_fa), Some(in_ga), Some(in_b)) =
1737 (f32w(&la.f_a), f32w(&la.g_a), f32w(&la.b_proj))
1738 else {
1739 return Ok(None);
1740 };
1741 // BF16 operand arm (lever 3 of the decode diet): the serving recipe (MEMRA_BF16_MMV=1)
1742 // admits wq/wk/wv to raw bf16 residency, where the Q8_0 arm below never binds. Its
1743 // bit-identity bar is against `matvec_bf16_f32acc_x4_rows` (matmul's FloatBf16
1744 // decode-tier arm), so it refuses wherever that arm would not be the unfused program:
1745 // MEMRA_BF16_MMV off (the chunked cuBLASLt GEMM class), the step37 W8 mirror doors on
1746 // (matvec_bf16_rows_into reroutes through the q8 mirror when BOTH are set), or
1747 // MEMRA_GLM5_W8 on (2026-09-02, lane/b200-glm5-w8: the SAME reroute, independent
1748 // door — this fused kernel's bit-identity claim is against the unmirrored bf16
1749 // program, so it must decline whichever door moved that program's target).
1750 let bf16 = |w: &GpuTensor| -> Option<usize> {
1751 match w {
1752 GpuTensor::FloatBf16 { .. } => Some(w.in_features()),
1753 _ => None,
1754 }
1755 };
1756 if let (Some(in_q), Some(in_k), Some(in_v)) = (bf16(&la.wq), bf16(&la.wk), bf16(&la.wv)) {
1757 // MEMRA_B200_BF16_GEMV_LT (lane/b200-gemv-hbm-20260902) reroutes the SAME
1758 // unfused target (`matvec_bf16_f32acc_x4_rows`) to a cuBLASLt reference GEMV, so
1759 // this fused arm declines for exactly the reason it declines for the W8 mirrors:
1760 // its bit-identity bar is against the unmirrored, unrerouted bf16 program. With
1761 // the door on, the three bf16 projections fall to the unfused group and each one
1762 // takes the library GEMV, which is what the reference door is there to measure.
1763 // W8 POSTURE FUSION (lane/b200-gemv-hbm-20260902 round 3). Under MEMRA_GLM5_W8 the
1764 // six projections each reroute through `matvec_bf16_via_q8_mirror`, so this group
1765 // runs as SIX separate launches plus six redundant quantizes of the same `x` — and
1766 // the bf16 fused arm below cannot serve it, because its bit-identity bar is against
1767 // the unmirrored bf16 program. `qmatvec_kda6_q8f32_rp_v2` is the fused twin for
1768 // that posture: three mirrored ranges on the rp v2 body (bit-identical to
1769 // `qmatvec_q8_0_mmvq_rp` per row) and three f32 ranges on the same deterministic
1770 // warp tree the q8 arm of this door already ships and has pinned. Gated on
1771 // MEMRA_B200_GEMV_V2 so it carries its own receipt; without that door W8 still
1772 // declines to the unfused path exactly as before.
1773 if crate::glm5_w8_on() && !(crate::step_tp_w8_on() && crate::w8_hybrid_on()) {
1774 if !Self::bf16_mmv_on() || !crate::b200_gemv_v2_on() {
1775 return Ok(None);
1776 }
1777 let in_f = in_q;
1778 if [in_k, in_v, in_fa, in_ga, in_b].iter().any(|&i| i != in_f)
1779 || !in_f.is_multiple_of(128)
1780 || x.len() < t * in_f
1781 || Engine::q8_v2_smem_bytes(in_f) > 48 * 1024
1782 {
1783 return Ok(None);
1784 }
1785 let dims = [
1786 la.wq.out_features(),
1787 la.wk.out_features(),
1788 la.wv.out_features(),
1789 la.f_a.out_features(),
1790 la.g_a.out_features(),
1791 la.b_proj.out_features(),
1792 ];
1793 let (
1794 GpuTensor::FloatBf16 { data: bq, .. },
1795 GpuTensor::FloatBf16 { data: bk, .. },
1796 GpuTensor::FloatBf16 { data: bv, .. },
1797 ) = (&la.wq, &la.wk, &la.wv)
1798 else {
1799 unreachable!("bf16() above only admits FloatBf16");
1800 };
1801 let (
1802 GpuTensor::Float { data: wfa, .. },
1803 GpuTensor::Float { data: wga, .. },
1804 GpuTensor::Float { data: wb, .. },
1805 ) = (&la.f_a, &la.g_a, &la.b_proj)
1806 else {
1807 unreachable!("f32w() above only admits Float");
1808 };
1809 let mut outs = [
1810 self.uninit(t * dims[0])?,
1811 self.uninit(t * dims[1])?,
1812 self.uninit(t * dims[2])?,
1813 self.uninit(t * dims[3])?,
1814 self.uninit(t * dims[4])?,
1815 self.uninit(t * dims[5])?,
1816 ];
1817 self.kda_proj_fused6_q8rp_raw_pre(
1818 bq,
1819 bk,
1820 bv,
1821 wfa,
1822 wga,
1823 wb,
1824 x,
1825 &mut outs,
1826 in_f,
1827 dims,
1828 t,
1829 crate::q8_row_ilp_on(),
1830 pre_q8,
1831 )?;
1832 if KDA_FUSED6_Q8RP_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1833 eprintln!(
1834 "[kda-fused6] engaged arm=q8rp_v2 in_f={in_f} out={dims:?} t={t} (one \
1835 launch replaces the six W8-mirror projections and their six redundant \
1836 activation quantizes; MEMRA_KDA_FUSED_PROJ=1 MEMRA_B200_GEMV_V2=1)"
1837 );
1838 }
1839 return Ok(Some(outs.into_iter().collect()));
1840 }
1841 if !Self::bf16_mmv_on()
1842 || (crate::step_tp_w8_on() && crate::w8_hybrid_on())
1843 || crate::b200_bf16_gemv_lt_on()
1844 {
1845 return Ok(None);
1846 }
1847 let in_f = in_q;
1848 if [in_k, in_v, in_fa, in_ga, in_b].iter().any(|&i| i != in_f)
1849 || !in_f.is_multiple_of(128)
1850 || x.len() < t * in_f
1851 {
1852 return Ok(None);
1853 }
1854 let dims = [
1855 la.wq.out_features(),
1856 la.wk.out_features(),
1857 la.wv.out_features(),
1858 la.f_a.out_features(),
1859 la.g_a.out_features(),
1860 la.b_proj.out_features(),
1861 ];
1862 let (
1863 GpuTensor::FloatBf16 { data: bq, .. },
1864 GpuTensor::FloatBf16 { data: bk, .. },
1865 GpuTensor::FloatBf16 { data: bv, .. },
1866 ) = (&la.wq, &la.wk, &la.wv)
1867 else {
1868 unreachable!("bf16() above only admits FloatBf16");
1869 };
1870 let (
1871 GpuTensor::Float { data: wfa, .. },
1872 GpuTensor::Float { data: wga, .. },
1873 GpuTensor::Float { data: wb, .. },
1874 ) = (&la.f_a, &la.g_a, &la.b_proj)
1875 else {
1876 unreachable!("f32w() above only admits Float");
1877 };
1878 let mut outs = [
1879 self.uninit(t * dims[0])?,
1880 self.uninit(t * dims[1])?,
1881 self.uninit(t * dims[2])?,
1882 self.uninit(t * dims[3])?,
1883 self.uninit(t * dims[4])?,
1884 self.uninit(t * dims[5])?,
1885 ];
1886 self.kda_proj_fused6_bf16_raw(bq, bk, bv, wfa, wga, wb, x, &mut outs, in_f, dims, t)?;
1887 if KDA_FUSED6_BF16_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1888 eprintln!(
1889 "[kda-fused6] engaged arm=bf16 in_f={in_f} out={dims:?} t={t} (one launch \
1890 replaces the six-projection group on the bf16-resident serving recipe; \
1891 MEMRA_KDA_FUSED_PROJ=1)"
1892 );
1893 }
1894 return Ok(Some(outs.into_iter().collect()));
1895 }
1896 // Dispatch-class envs: the bit-identity bar is against the MMVQ-class per-row program.
1897 if std::env::var("MEMRA_FAST").as_deref() == Ok("0")
1898 || !self.mmvq_supports(crate::QT_Q8_0)
1899 || (t >= 2 && std::env::var("MEMRA_NO_BATCHED").is_ok())
1900 || (t >= 5 && !Self::b8_enabled())
1901 {
1902 return Ok(None);
1903 }
1904 // Q8_0 operand classes (the non-BF16_MMV shapes).
1905 let q8 = |w: &GpuTensor| -> Option<(usize, usize)> {
1906 match w {
1907 GpuTensor::Quant {
1908 qtype: crate::QT_Q8_0,
1909 row_bytes,
1910 scale,
1911 rp: false,
1912 rp4: None,
1913 ..
1914 } if *scale == 1.0 => Some((w.in_features(), *row_bytes)),
1915 _ => None,
1916 }
1917 };
1918 let (Some((in_q, rb_q)), Some((in_k, rb_k)), Some((in_v, rb_v))) =
1919 (q8(&la.wq), q8(&la.wk), q8(&la.wv))
1920 else {
1921 return Ok(None);
1922 };
1923 let in_f = in_q;
1924 if [in_k, in_v, in_fa, in_ga, in_b].iter().any(|&i| i != in_f)
1925 || rb_k != rb_q
1926 || rb_v != rb_q
1927 || !in_f.is_multiple_of(128)
1928 || x.len() < t * in_f
1929 {
1930 return Ok(None);
1931 }
1932 let dims = [
1933 la.wq.out_features(),
1934 la.wk.out_features(),
1935 la.wv.out_features(),
1936 la.f_a.out_features(),
1937 la.g_a.out_features(),
1938 la.b_proj.out_features(),
1939 ];
1940 let (
1941 GpuTensor::Quant { bytes: bq, .. },
1942 GpuTensor::Quant { bytes: bk, .. },
1943 GpuTensor::Quant { bytes: bv, .. },
1944 ) = (&la.wq, &la.wk, &la.wv)
1945 else {
1946 unreachable!("q8() above only admits Quant");
1947 };
1948 let (
1949 GpuTensor::Float { data: wfa, .. },
1950 GpuTensor::Float { data: wga, .. },
1951 GpuTensor::Float { data: wb, .. },
1952 ) = (&la.f_a, &la.g_a, &la.b_proj)
1953 else {
1954 unreachable!("f32w() above only admits Float");
1955 };
1956
1957 let (aq, ad) = self.quantize_q8_1(x, t, in_f)?;
1958 let mut outs = [
1959 self.uninit(t * dims[0])?,
1960 self.uninit(t * dims[1])?,
1961 self.uninit(t * dims[2])?,
1962 self.uninit(t * dims[3])?,
1963 self.uninit(t * dims[4])?,
1964 self.uninit(t * dims[5])?,
1965 ];
1966 self.kda_proj_fused6_raw(
1967 bq, bk, bv, wfa, wga, wb, &aq, &ad, x, &mut outs, in_f, dims, t, rb_q,
1968 )?;
1969
1970 // Engagement receipt: counted at the arm's own call site, announced once per boot
1971 // (the [bf16-mmv] RESIDENT lesson: engagement lines are receipts, never inferred).
1972 if KDA_FUSED6_DISPATCHES.fetch_add(1, Ordering::Relaxed) == 0 {
1973 eprintln!(
1974 "[kda-fused6] engaged in_f={in_f} out={dims:?} t={t} (one launch replaces the \
1975 six-projection group; MEMRA_KDA_FUSED_PROJ=1)"
1976 );
1977 }
1978 Ok(Some(outs.into_iter().collect()))
1979 }
1980
1981 /// The raw fused-6 launch (`qmatvec_kda6_q8f32_mmvq`): three Q8_0 weights + three f32
1982 /// weights, one q8_1 activation pair + the raw f32 activation, six outputs, t token rows.
1983 /// Geometry-checked but POLICY-FREE: the gate's red arms drive mutations (transposed slice
1984 /// data, dropped ranges via `dims[i] = 0`) through this entry, so the mutation reaches the
1985 /// exact program the door serves.
1986 #[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
1987 pub fn kda_proj_fused6_raw(
1988 &self,
1989 wq: &CudaSlice<u8>,
1990 wk: &CudaSlice<u8>,
1991 wv: &CudaSlice<u8>,
1992 wfa: &CudaSlice<f32>,
1993 wga: &CudaSlice<f32>,
1994 wb: &CudaSlice<f32>,
1995 aq: &CudaSlice<i8>,
1996 ad: &CudaSlice<f32>,
1997 x: &CudaSlice<f32>,
1998 outs: &mut [CudaSlice<f32>; 6],
1999 in_f: usize,
2000 dims: [usize; 6],
2001 t: usize,
2002 row_bytes: usize,
2003 ) -> Result<(), Box<dyn std::error::Error>> {
2004 const ROWS_PER_BLOCK: usize = 4; // MEMRA_MMVQ_ROWS in qmatvec.cu
2005 if t == 0
2006 || !in_f.is_multiple_of(128)
2007 || x.len() < t * in_f
2008 || aq.len() < t * in_f
2009 || ad.len() < t * (in_f / 32)
2010 {
2011 return Err("kda_proj_fused6 geometry".into());
2012 }
2013 for (i, (w, want_rows)) in [(wq, dims[0]), (wk, dims[1]), (wv, dims[2])]
2014 .into_iter()
2015 .enumerate()
2016 {
2017 if w.len() < want_rows * row_bytes {
2018 return Err(format!(
2019 "kda_proj_fused6: q8 weight {i} holds {} bytes, needs {}",
2020 w.len(),
2021 want_rows * row_bytes
2022 )
2023 .into());
2024 }
2025 }
2026 for (i, (w, want_rows)) in [(wfa, dims[3]), (wga, dims[4]), (wb, dims[5])]
2027 .into_iter()
2028 .enumerate()
2029 {
2030 if w.len() < want_rows * in_f {
2031 return Err(format!(
2032 "kda_proj_fused6: f32 weight {} holds {} floats, needs {}",
2033 i + 3,
2034 w.len(),
2035 want_rows * in_f
2036 )
2037 .into());
2038 }
2039 }
2040 for (i, (o, want)) in outs.iter().zip(dims).enumerate() {
2041 if o.len() < t * want {
2042 return Err(format!("kda_proj_fused6: output {i} too small").into());
2043 }
2044 }
2045 let blocks: usize = dims.iter().map(|d| d.div_ceil(ROWS_PER_BLOCK)).sum();
2046 let f = self.func("qmatvec_kda6_q8f32_mmvq");
2047 let cfg = LaunchConfig {
2048 grid_dim: (blocks as u32, t as u32, 1),
2049 block_dim: (32, ROWS_PER_BLOCK as u32, 1),
2050 shared_mem_bytes: 0,
2051 };
2052 let inf = in_f as i32;
2053 let d = dims.map(|v| v as i32);
2054 let (mi, rb) = (t as i32, row_bytes as i64);
2055 let [o0, o1, o2, o3, o4, o5] = outs;
2056 let stream = self.gpu.stream();
2057 let mut b = stream.launch_builder(&f);
2058 b.arg(wq)
2059 .arg(wk)
2060 .arg(wv)
2061 .arg(wfa)
2062 .arg(wga)
2063 .arg(wb)
2064 .arg(aq)
2065 .arg(ad)
2066 .arg(x)
2067 .arg(&mut *o0)
2068 .arg(&mut *o1)
2069 .arg(&mut *o2)
2070 .arg(&mut *o3)
2071 .arg(&mut *o4)
2072 .arg(&mut *o5)
2073 .arg(&inf)
2074 .arg(&d[0])
2075 .arg(&d[1])
2076 .arg(&d[2])
2077 .arg(&d[3])
2078 .arg(&d[4])
2079 .arg(&d[5])
2080 .arg(&mi)
2081 .arg(&rb);
2082 unsafe { b.launch(cfg)? };
2083 Ok(())
2084 }
2085
2086 /// The raw BF16-arm fused-6 launch (`qmatvec_kda6_bf16f32`): three bf16-resident weights
2087 /// (raw checkpoint u16 bytes, the `admit=bf16_mmv` residency) + three f32 weights, one raw
2088 /// f32 activation, six outputs, t token rows. Block = `mmv_block()` — the SAME blockDim
2089 /// `matvec_bf16_rows_into` pins, because the bf16 body's shared-tree reduction shape (and
2090 /// therefore its bits) is a function of blockDim. Geometry-checked but POLICY-FREE: the
2091 /// gate's red arms drive mutations through this entry, exactly like the q8 raw above.
2092 #[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
2093 pub fn kda_proj_fused6_bf16_raw(
2094 &self,
2095 wq: &CudaSlice<u8>,
2096 wk: &CudaSlice<u8>,
2097 wv: &CudaSlice<u8>,
2098 wfa: &CudaSlice<f32>,
2099 wga: &CudaSlice<f32>,
2100 wb: &CudaSlice<f32>,
2101 x: &CudaSlice<f32>,
2102 outs: &mut [CudaSlice<f32>; 6],
2103 in_f: usize,
2104 dims: [usize; 6],
2105 t: usize,
2106 ) -> Result<(), Box<dyn std::error::Error>> {
2107 self.kda_proj_fused6_bf16_arm_raw(
2108 wq,
2109 wk,
2110 wv,
2111 wfa,
2112 wga,
2113 wb,
2114 x,
2115 outs,
2116 in_f,
2117 dims,
2118 t,
2119 crate::b200_gemv_v2_level(),
2120 )
2121 }
2122
2123 /// The same launch with the arm chosen EXPLICITLY instead of from the memoized
2124 /// `MEMRA_B200_GEMV_V2` door, so a bench or gate can drive every arm inside one process
2125 /// (`b200_matvec_bench`, the `_arm_raw` precedent).
2126 ///
2127 /// `arm`: `0` = the shipped `qmatvec_kda6_bf16f32`; `1` = `_v2`, whose three BF16 ranges take
2128 /// the eight-rows-per-block walk (activation loaded once and reused across the rows, ten
2129 /// 16 B loads in flight before the first fma, one barrier chain per block) instead of
2130 /// `kda6_bf16_rows4`'s four sequential rows; `2` = `_v3`, the same walk with its weight
2131 /// tiles staged through shared memory by `cp.async` so the in-flight budget stops being
2132 /// register-bound. `2` falls back to `1` when v3's dynamic smem would exceed the 48 KB
2133 /// default cap. Per row the arithmetic is unchanged in every arm, so all three are
2134 /// BIT-IDENTICAL to each other and to `matvec_bf16_f32acc_x4_rows`.
2135 #[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
2136 pub fn kda_proj_fused6_bf16_arm_raw(
2137 &self,
2138 wq: &CudaSlice<u8>,
2139 wk: &CudaSlice<u8>,
2140 wv: &CudaSlice<u8>,
2141 wfa: &CudaSlice<f32>,
2142 wga: &CudaSlice<f32>,
2143 wb: &CudaSlice<f32>,
2144 x: &CudaSlice<f32>,
2145 outs: &mut [CudaSlice<f32>; 6],
2146 in_f: usize,
2147 dims: [usize; 6],
2148 t: usize,
2149 arm: u8,
2150 ) -> Result<(), Box<dyn std::error::Error>> {
2151 if t == 0 || !in_f.is_multiple_of(128) || x.len() < t * in_f {
2152 return Err("kda_proj_fused6_bf16 geometry".into());
2153 }
2154 for (i, (w, want_rows)) in [(wq, dims[0]), (wk, dims[1]), (wv, dims[2])]
2155 .into_iter()
2156 .enumerate()
2157 {
2158 if w.len() < want_rows * in_f * 2 {
2159 return Err(format!(
2160 "kda_proj_fused6_bf16: bf16 weight {i} holds {} bytes, needs {}",
2161 w.len(),
2162 want_rows * in_f * 2
2163 )
2164 .into());
2165 }
2166 }
2167 for (i, (w, want_rows)) in [(wfa, dims[3]), (wga, dims[4]), (wb, dims[5])]
2168 .into_iter()
2169 .enumerate()
2170 {
2171 if w.len() < want_rows * in_f {
2172 return Err(format!(
2173 "kda_proj_fused6_bf16: f32 weight {} holds {} floats, needs {}",
2174 i + 3,
2175 w.len(),
2176 want_rows * in_f
2177 )
2178 .into());
2179 }
2180 }
2181 for (i, (o, want)) in outs.iter().zip(dims).enumerate() {
2182 if o.len() < t * want {
2183 return Err(format!("kda_proj_fused6_bf16: output {i} too small").into());
2184 }
2185 }
2186 // v3 declines to v2 when its staged tiles would not fit the 48 KB default dynamic
2187 // shared-memory cap (36 KB at the default mmv_block()=128, 72 KB at 256).
2188 let arm = if arm >= 2 && !crate::gemv_v3_fits() {
2189 1
2190 } else {
2191 arm
2192 };
2193 // Rows per block, and therefore the block partition of the six ranges: 4 for the
2194 // shipped kernel, `GEMV_V2_ROWS` for the v2/v3 twins. v2 takes the R-row reduction
2195 // window as DYNAMIC shared memory (R * blockDim.x floats); v3 takes that plus its
2196 // cp.async stage buffers.
2197 let nb = crate::mmv_block();
2198 let rpb = if arm >= 1 { crate::GEMV_V2_ROWS } else { 4 };
2199 let blocks: usize = dims.iter().map(|d| d.div_ceil(rpb)).sum();
2200 let f = self.func(match arm {
2201 0 => "qmatvec_kda6_bf16f32",
2202 1 => "qmatvec_kda6_bf16f32_v2",
2203 _ => "qmatvec_kda6_bf16f32_v3",
2204 });
2205 let cfg = LaunchConfig {
2206 grid_dim: (blocks as u32, t as u32, 1),
2207 block_dim: (nb, 1, 1),
2208 shared_mem_bytes: match arm {
2209 0 => 0,
2210 1 => (crate::GEMV_V2_ROWS as u32) * nb * 4,
2211 _ => crate::gemv_v3_smem_bytes(nb as usize) as u32,
2212 },
2213 };
2214 let inf = in_f as i32;
2215 let d = dims.map(|v| v as i32);
2216 let mi = t as i32;
2217 let [o0, o1, o2, o3, o4, o5] = outs;
2218 let stream = self.gpu.stream();
2219 let mut b = stream.launch_builder(&f);
2220 b.arg(wq)
2221 .arg(wk)
2222 .arg(wv)
2223 .arg(wfa)
2224 .arg(wga)
2225 .arg(wb)
2226 .arg(x)
2227 .arg(&mut *o0)
2228 .arg(&mut *o1)
2229 .arg(&mut *o2)
2230 .arg(&mut *o3)
2231 .arg(&mut *o4)
2232 .arg(&mut *o5)
2233 .arg(&inf)
2234 .arg(&d[0])
2235 .arg(&d[1])
2236 .arg(&d[2])
2237 .arg(&d[3])
2238 .arg(&d[4])
2239 .arg(&d[5])
2240 .arg(&mi);
2241 unsafe { b.launch(cfg)? };
2242 Ok(())
2243 }
2244}
2245
2246#[cfg(test)]
2247mod kda_conv3_default_tests {
2248 use super::kda_conv3_on_from;
2249
2250 #[test]
2251 fn arch_keyed_default_with_explicit_override() {
2252 assert!(kda_conv3_on_from(None, "100a"));
2253 assert!(!kda_conv3_on_from(None, "120a"));
2254 assert!(kda_conv3_on_from(Some("1"), "120a"));
2255 assert!(!kda_conv3_on_from(Some("0"), "100a"));
2256 }
2257}