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