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