Skip to main content

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}