Skip to main content

memra_engine/
dflash.rs

1//! DFlash block-diffusion drafter (DFLASH-BRINGUP-PLAN.md, 2026-07-13).
2//!
3//! 5-layer qwen3-class mini-transformer that drafts a 16-token block in ONE non-causal
4//! forward, conditioned on the TARGET's hidden states at 6 tapped layers (concatenated
5//! through `fc` + `hidden_norm`). No embed / lm_head of its own — the round reuses the
6//! target's. Reference: z-lab/dflash `dflash/model.py` (semantics frozen in the plan doc);
7//! oracle: tools/dflash_oracle.py -> /data/cache/dflash-oracle.npz.
8//!
9//! FIRST LIGHT = f32-resident weights + fresh full-context forward (no draft KV cache) —
10//! correctness vs the oracle, then the cache/quant/window arms land measurement-gated.
11
12use crate::Engine;
13use crate::model::GpuTensor;
14use cudarc::driver::CudaSlice;
15
16pub struct DflashCfg {
17    pub hidden: usize,        // 5376
18    pub n_head: usize,        // 64
19    pub n_kv: usize,          // 8
20    pub head_dim: usize,      // 128
21    pub n_ff: usize,          // 10752
22    pub n_layer: usize,       // 5
23    pub eps: f32,             // 1e-6
24    pub rope_theta: f32,      // 1e6
25    pub block_size: usize,    // 16
26    pub mask_token_id: u32,   // 4
27    pub target_layer_ids: Vec<usize>, // [1,12,23,35,46,57]
28    pub sliding_window: usize,        // 2048
29    /// true = sliding_attention for that layer (4x true + 1x false on the 31B draft).
30    pub layer_sliding: Vec<bool>,
31}
32
33pub struct DflashLayer {
34    pub wq: GpuTensor,   // [nh*hd, hidden] row-major (out_f rows)
35    pub wk: GpuTensor,   // [nkv*hd, hidden]
36    pub wv: GpuTensor,   // [nkv*hd, hidden]
37    pub wo: GpuTensor,   // [hidden, nh*hd]
38    pub w_gate: GpuTensor, // [n_ff, hidden]
39    pub w_up: GpuTensor,   // [n_ff, hidden]
40    pub w_down: GpuTensor, // [hidden, n_ff]
41    pub ln_in: CudaSlice<f32>,   // [hidden]
42    pub ln_post: CudaSlice<f32>, // [hidden]
43    pub q_norm: CudaSlice<f32>,  // [hd]
44    pub k_norm: CudaSlice<f32>,  // [hd]
45}
46
47pub struct DflashDraft {
48    pub cfg: DflashCfg,
49    pub layers: Vec<DflashLayer>,
50    pub fc: GpuTensor,               // [hidden, n_taps*hidden]
51    pub hidden_norm: CudaSlice<f32>, // [hidden]
52    pub norm: CudaSlice<f32>,        // [hidden]
53    /// DSpark semi-AR markov head (present in the repo-root checkpoint variant):
54    /// draft logits at position k get + W2(W1[prev_realized_token]) — left-to-right
55    /// within the block (the patch's _markov_semiar_sample_block semantics, greedy).
56    /// w1 = raw bf16 [V, rank] (row-gathered by device token id); w2 = q8_0 [rank->V].
57    pub markov: Option<MarkovHead>,
58}
59
60pub struct MarkovHead {
61    pub w1_bf16: CudaSlice<u8>,   // [V, rank] bf16 raw
62    pub w2: GpuTensor,            // [rank -> V] q8_0
63    pub rank: usize,
64    pub vocab: usize,
65}
66
67fn bf16_to_f32(bytes: &[u8]) -> Vec<f32> {
68    bytes
69        .chunks_exact(2)
70        .map(|c| f32::from_bits((u16::from_le_bytes([c[0], c[1]]) as u32) << 16))
71        .collect()
72}
73
74/// Host q8_0 encode (ggml block layout: [d f16][32 x i8] = 34B/32 vals). The drafter's
75/// weights ride the dp4a fast path at 1.6GB resident (bf16 3.1GB + the 31B trunk OOM'd
76/// 24GB; f32 6.2GB worse). Drafter quantization moves ACCEPTANCE only — verify exactness
77/// is structural.
78fn encode_q8_0(vals: &[f32]) -> Vec<u8> {
79    let mut out = Vec::with_capacity(vals.len() / 32 * 34);
80    for blk in vals.chunks_exact(32) {
81        let amax = blk.iter().fold(0f32, |a, v| a.max(v.abs()));
82        let d = amax / 127.0;
83        let id = if d > 0.0 { 1.0 / d } else { 0.0 };
84        let dh = half_from_f32(d);
85        out.extend_from_slice(&dh.to_le_bytes());
86        for &v in blk {
87            out.push(((v * id).round().clamp(-127.0, 127.0)) as i8 as u8);
88        }
89    }
90    out
91}
92
93/// Host q4_0 encode (ggml: [d f16][16B packed nibbles] = 18B/32 vals; q = round(v/d)+8,
94/// d = amax/-7 sign trick NOT used — plain amax/7? ggml uses d = max/-8 .. follow ggml:
95/// d = amax / -8 when the max is negative-dominant; reference quantize_row_q4_0: d =
96/// max(|v|)/-8 signed-max form). Implemented to match ggml quantize_row_q4_0_ref.
97fn encode_q4_0(vals: &[f32]) -> Vec<u8> {
98    let mut out = Vec::with_capacity(vals.len() / 32 * 18);
99    for blk in vals.chunks_exact(32) {
100        // ggml ref: pick the value with the LARGEST |v| (keeping sign), d = that / -8
101        let mut amax = 0f32; let mut mx = 0f32;
102        for &v in blk { if v.abs() > amax { amax = v.abs(); mx = v; } }
103        let d = mx / -8.0;
104        let id = if d != 0.0 { 1.0 / d } else { 0.0 };
105        out.extend_from_slice(&half_from_f32(d).to_le_bytes());
106        for j in 0..16 {
107            let x0 = (blk[j] * id + 8.5).clamp(0.0, 15.0) as u8;
108            let x1 = (blk[j + 16] * id + 8.5).clamp(0.0, 15.0) as u8;
109            out.push(x0 | (x1 << 4));
110        }
111    }
112    out
113}
114
115fn half_from_f32(v: f32) -> u16 {
116    // f32 -> IEEE f16 (round-to-nearest-even; range of q8_0 d values is tame)
117    let b = v.to_bits();
118    let sign = ((b >> 16) & 0x8000) as u16;
119    let exp = ((b >> 23) & 0xff) as i32 - 127 + 15;
120    let man = b & 0x7fffff;
121    if exp <= 0 { return sign; }              // flush tiny d to zero
122    if exp >= 31 { return sign | 0x7c00; }    // inf (unreachable for sane d)
123    let mut h = sign | ((exp as u16) << 10) | ((man >> 13) as u16);
124    // round to nearest even on the truncated 13 bits
125    let rem = man & 0x1fff;
126    if rem > 0x1000 || (rem == 0x1000 && (h & 1) == 1) { h += 1; }
127    h
128}
129
130impl DflashDraft {
131    /// Load the backbone-only checkpoint dir (config.json + model.safetensors, bf16).
132    /// Config scalars ride a minimal extractor (no json dep in-tree — HfConfig precedent).
133    pub fn load(e: &Engine, dir: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
134        let txt = std::fs::read_to_string(dir.join("config.json"))?;
135        fn num(txt: &str, key: &str) -> Option<f64> {
136            let i = txt.find(&format!("\"{key}\""))?;
137            let rest = &txt[i..];
138            let colon = rest.find(':')?;
139            let val: String = rest[colon + 1..].trim_start().chars()
140                .take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == 'e' || *c == 'E' || *c == '+')
141                .collect();
142            val.parse().ok()
143        }
144        fn num_list(txt: &str, key: &str) -> Vec<usize> {
145            let Some(i) = txt.find(&format!("\"{key}\"")) else { return Vec::new() };
146            let rest = &txt[i..];
147            let (Some(a), Some(b)) = (rest.find('['), rest.find(']')) else { return Vec::new() };
148            rest[a + 1..b].split(',').filter_map(|s| s.trim().parse().ok()).collect()
149        }
150        let g = |k: &str| num(&txt, k).unwrap_or_else(|| panic!("config missing {k}")) as usize;
151        // layer_types order: count entries, mark sliding ones
152        let layer_sliding: Vec<bool> = {
153            let i = txt.find("\"layer_types\"").expect("layer_types");
154            let rest = &txt[i..];
155            let (a, b) = (rest.find('[').unwrap(), rest.find(']').unwrap());
156            rest[a + 1..b].split(',').map(|s| s.contains("sliding_attention")).collect()
157        };
158        let cfg = DflashCfg {
159            hidden: g("hidden_size"),
160            n_head: g("num_attention_heads"),
161            n_kv: g("num_key_value_heads"),
162            head_dim: g("head_dim"),
163            n_ff: g("intermediate_size"),
164            n_layer: g("num_hidden_layers"),
165            eps: num(&txt, "rms_norm_eps").expect("rms_norm_eps") as f32,
166            rope_theta: num(&txt, "rope_theta").expect("rope_theta") as f32,
167            block_size: g("block_size"),
168            mask_token_id: g("mask_token_id") as u32,
169            target_layer_ids: num_list(&txt, "target_layer_ids"),
170            sliding_window: g("sliding_window"),
171            layer_sliding,
172        };
173        let st = memra_gguf::safetensors::StModel::open(&dir.join("model.safetensors"))?;
174        // 1D norm weights ride raw slices; 2D matmul weights ride GpuTensor::Float
175        // (cuBLASLt f32 arm — the Stage-A numeric class, right for oracle parity).
176        let up = |name: &str| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
177            let (_info, bytes) = st.raw(name).ok_or_else(|| format!("missing tensor {name}"))?;
178            Ok(e.htod(&bf16_to_f32(bytes))?)
179        };
180        // Precision policy (MEMRA_DFLASH_PREC seam): "q8" = all q8_0 (1.6GB, default);
181        // "mixed" = bf16 attn+fc (the ctx-conditioning path) + q8_0 ffn (~2.2GB — fits the
182        // ~2.8GB headroom beside the 31B trunk); "bf16" = all bf16 (parity runs, no target).
183        let prec = std::env::var("MEMRA_DFLASH_PREC").unwrap_or_else(|_| "q8".into());
184        let upw = |name: &str| -> Result<GpuTensor, Box<dyn std::error::Error>> {
185            let (info, bytes) = st.raw(name).ok_or_else(|| format!("missing tensor {name}"))?;
186            let shape = info.ne(); // ggml order: ne[0]=in_f, ne[1]=out_f
187            let in_f = shape[0] as usize;
188            let is_ffn = name.contains(".mlp.");
189            let bf16 = prec == "bf16" || (prec == "mixed" && !is_ffn)
190                || (prec == "fc" && name == "fc.weight");
191            if bf16 {
192                return Ok(GpuTensor::FloatBf16 { data: e.upload_u8(bytes)?, ne: shape.to_vec() });
193            }
194            let f32s = bf16_to_f32(bytes);
195            if prec == "q4" {
196                let q = encode_q4_0(&f32s);
197                return Ok(GpuTensor::Quant {
198                    bytes: e.upload_u8(&q)?, qtype: crate::QT_Q4_0,
199                    row_bytes: in_f / 32 * 18, ne: shape.to_vec(), scale: 1.0, rp: false,
200                    #[cfg(memra_cutlass)]
201                    cutlass: None,
202                    fp8: None, blk: None, rp4: None, f16: None,
203                });
204            }
205            let q = encode_q8_0(&f32s);
206            Ok(GpuTensor::Quant {
207                bytes: e.upload_u8(&q)?, qtype: crate::QT_Q8_0,
208                row_bytes: in_f / 32 * 34, ne: shape.to_vec(), scale: 1.0, rp: false,
209                #[cfg(memra_cutlass)]
210                cutlass: None,
211                fp8: None, blk: None, rp4: None, f16: None,
212            })
213        };
214        let mut layers = Vec::with_capacity(cfg.n_layer);
215        for i in 0..cfg.n_layer {
216            let p = |s: &str| format!("layers.{i}.{s}");
217            layers.push(DflashLayer {
218                wq: upw(&p("self_attn.q_proj.weight"))?,
219                wk: upw(&p("self_attn.k_proj.weight"))?,
220                wv: upw(&p("self_attn.v_proj.weight"))?,
221                wo: upw(&p("self_attn.o_proj.weight"))?,
222                w_gate: upw(&p("mlp.gate_proj.weight"))?,
223                w_up: upw(&p("mlp.up_proj.weight"))?,
224                w_down: upw(&p("mlp.down_proj.weight"))?,
225                ln_in: up(&p("input_layernorm.weight"))?,
226                ln_post: up(&p("post_attention_layernorm.weight"))?,
227                q_norm: up(&p("self_attn.q_norm.weight"))?,
228                k_norm: up(&p("self_attn.k_norm.weight"))?,
229            });
230        }
231        let markov = if let Some((info, bytes)) = st.raw("markov_head.markov_w1.weight") {
232            let sh = info.ne(); // [rank, vocab] in ggml order (safetensors [V, rank] reversed)
233            let (rank, vocab) = (sh[0] as usize, sh[1] as usize);
234            let (_i2, b2) = st.raw("markov_head.markov_w2.weight")
235                .ok_or("markov_w2 missing beside markov_w1")?;
236            let w2f = bf16_to_f32(b2);
237            let w2q = encode_q8_0(&w2f);
238            Some(MarkovHead {
239                w1_bf16: e.upload_u8(bytes)?,
240                w2: GpuTensor::Quant {
241                    bytes: e.upload_u8(&w2q)?, qtype: crate::QT_Q8_0,
242                    row_bytes: rank / 32 * 34, ne: vec![rank as u64, vocab as u64],
243                    scale: 1.0, rp: false,
244                    #[cfg(memra_cutlass)]
245                    cutlass: None,
246                    fp8: None, blk: None, rp4: None, f16: None,
247                },
248                rank, vocab,
249            })
250        } else { None };
251        Ok(Self {
252            fc: upw("fc.weight")?,
253            hidden_norm: up("hidden_norm.weight")?,
254            norm: up("norm.weight")?,
255            cfg,
256            layers,
257            markov,
258        })
259    }
260
261    /// f32 GEMM helper via the engine Float arm (cuBLASLt): y[t, out_f].
262    fn mm(&self, e: &Engine, w: &GpuTensor, x: &CudaSlice<f32>, t: usize, _in_f: usize,
263          _out_f: usize) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
264        Ok(e.matmul(w, x, t)?)
265    }
266
267    /// FIRST-LIGHT forward (oracle contract): full non-causal attention over
268    /// [ctx_features ; block], NO draft KV cache, NO sliding window (the oracle bypasses
269    /// the reference mask machinery the same way — window/caching land in the round arm).
270    ///
271    /// `target_hidden`: [ctx, n_taps*hidden] (f32, device)  — raw tapped states.
272    /// `noise_emb`:     [block, hidden] — target embed rows for [accepted, MASK x b-1].
273    /// `pos`:           absolute positions for ctx rows THEN block rows (ctx+block i32).
274    /// Returns final normed hidden [block, hidden] (feed target lm_head for draft logits).
275    /// ctx features for `t` tapped rows: hidden_norm(fc(taps)) — the drafter's context
276    /// representation, cacheable across rounds (append-only in committed-token order).
277    pub fn ctx_features(&self, e: &Engine, taps: &CudaSlice<f32>, t: usize)
278                        -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
279        let c = &self.cfg;
280        let n_taps = c.target_layer_ids.len();
281        let fc_out = self.mm(e, &self.fc, taps, t, n_taps * c.hidden, c.hidden)?;
282        let mut out = e.uninit(t * c.hidden)?;
283        e.rms_norm(&fc_out, &self.hidden_norm, &mut out, c.hidden, t, c.eps)?;
284        Ok(out)
285    }
286
287    pub fn forward(
288        &self, e: &Engine, target_hidden: &CudaSlice<f32>, noise_emb: &CudaSlice<f32>,
289        pos: &[i32], ctx: usize,
290    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
291        let ctx_f = self.ctx_features(e, target_hidden, ctx)?;
292        if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
293            let v = e.dtoh(&ctx_f)?;
294            let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
295            std::fs::write(format!("{dir}/memra-ctx_features.f32"), bytes)?;
296        }
297        self.forward_block(e, &ctx_f, noise_emb, pos, ctx)
298    }
299
300    /// Block forward over PRECOMPUTED ctx features (the round arm's entry: features are
301    /// cached across rounds; only the block work repeats).
302    pub fn forward_block(
303        &self, e: &Engine, ctx_f: &CudaSlice<f32>, noise_emb: &CudaSlice<f32>,
304        pos: &[i32], ctx: usize,
305    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
306        let c = &self.cfg;
307        let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
308        let b = c.block_size;
309        assert_eq!(pos.len(), ctx + b, "pos covers ctx rows then block rows");
310
311        let pos_blk = e.htod_i32(&pos[ctx..])?;
312
313        let mut x = e.clone_dtod(noise_emb)?; // [b, hidden] residual stream
314        for (li, l) in self.layers.iter().enumerate() {
315            let _ = li;
316            // input_layernorm on the block rows only (ctx features are norm-free per ref:
317            // k/v project the SAME ctx_f every layer, un-layernormed).
318            let mut xn = e.uninit(b * h)?;
319            e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
320
321            // q from block; k/v from [ctx_f ; block-normed]
322            let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
323            let k0c = self.mm(e, &l.wk, ctx_f, ctx, h, nkv * hd)?;
324            let v0c = self.mm(e, &l.wv, ctx_f, ctx, h, nkv * hd)?;
325            let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
326            let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
327
328            // per-head q/k rms norm (v passes through: ones weight trick not needed — the
329            // qkv kernel norms rq+rk rows; concatenate k first).
330            let mut k0 = e.uninit((ctx + b) * nkv * hd)?;
331            e.copy_into(&mut k0, 0, &k0c, ctx * nkv * hd)?;
332            e.copy_into(&mut k0, ctx * nkv * hd, &k0b, b * nkv * hd)?;
333            let mut v = e.uninit((ctx + b) * nkv * hd)?;
334            e.copy_into(&mut v, 0, &v0c, ctx * nkv * hd)?;
335            e.copy_into(&mut v, ctx * nkv * hd, &v0b, b * nkv * hd)?;
336
337            if li == 0 { if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
338                let v = e.dtoh(&q0)?;
339                let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
340                std::fs::write(format!("{dir}/memra-l0_q0.f32"), bytes)?;
341            }}
342            let mut q = e.uninit(b * nh * hd)?;
343            let mut k = e.uninit((ctx + b) * nkv * hd)?;
344            // rms over head_dim rows: q has b*nh rows, k has (ctx+b)*nkv rows.
345            e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
346            if li == 0 { if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
347                let v = e.dtoh(&q)?;
348                let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
349                std::fs::write(format!("{dir}/memra-l0_qn.f32"), bytes)?;
350            }}
351            e.rms_norm(&k0, &l.k_norm, &mut k, hd, (ctx + b) * nkv, c.eps)?;
352
353            // rope: q at block positions, k at ctx-then-block positions (absolute).
354            let norope = std::env::var("MEMRA_DFLASH_NOROPE").is_ok();
355            if !norope { e.rope_neox(&mut q, &pos_blk, hd, hd, nh, b, c.rope_theta, 1.0)?; }
356            if li == 0 { if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
357                let dump = |name: &str, t: &cudarc::driver::CudaSlice<f32>| -> Result<(), Box<dyn std::error::Error>> {
358                    let v = e.dtoh(t)?;
359                    let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
360                    std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
361                    Ok(())
362                };
363                dump("xn", &xn)?; dump("q_prerope", &q)?;
364            }}
365            // k rows are laid out [row, nkv, hd] with row-major tokens — rope_neox expects
366            // (n_heads, n_tokens); ctx and block ropes run as one call over ctx+b tokens.
367            let pos_all = e.htod_i32(pos)?;
368            if !norope { e.rope_neox(&mut k, &pos_all, hd, hd, nkv, ctx + b, c.rope_theta, 1.0)?; }
369
370            // full non-causal attention: every block query sees all ctx+b keys.
371            let mut attn = e.uninit(b * nh * hd)?;
372            let scale = 1.0f32 / (hd as f32).sqrt();
373            // NAIVE SDPA for first light: fa_prefill's NON-CAUSAL arm with T != T_kv is
374            // BROKEN (attn maxdiff 0.34 vs the torch oracle; q/k inputs bit-close — no
375            // existing caller exercises that shape class, jsonl 2026-07-13). The 16 x
376            // (ctx+16) block attention is tiny; the fa arm returns behind this seam once
377            // its kernel is fixed + parity-gated.
378            if std::env::var("MEMRA_DFLASH_FA").is_ok() {
379                e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
380            } else {
381                e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
382            }
383
384            let o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
385            let mut x1 = e.uninit(b * h)?;
386            e.add(&o, &x, &mut x1, b * h)?;
387            if li == 0 { if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
388                let dump = |name: &str, t: &cudarc::driver::CudaSlice<f32>| -> Result<(), Box<dyn std::error::Error>> {
389                    let v = e.dtoh(t)?;
390                    let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
391                    std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
392                    Ok(())
393                };
394                dump("q", &q)?; dump("k", &k)?; dump("attn", &attn)?; dump("x1", &x1)?;
395            }}
396
397            // mlp
398            let mut x1n = e.uninit(b * h)?;
399            e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
400            let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
401            let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
402            let mut act = e.uninit(b * c.n_ff)?;
403            e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
404            let down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
405            let mut x2 = e.uninit(b * h)?;
406            e.add(&down, &x1, &mut x2, b * h)?;
407            x = x2;
408            if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
409                let v = e.dtoh(&x)?;
410                let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
411                std::fs::write(format!("{dir}/memra-layer{li}_out.f32"), bytes)?;
412            }
413        }
414        let mut out = e.uninit(b * h)?;
415        e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
416        Ok(out)
417    }
418}
419
420/// Draft KV cache (round-cost fix, 2026-07-13): per-layer normed+roped ctx K and raw ctx V,
421/// append-only in committed order. Block K/V land TRANSIENTLY at [len..len+b] each round
422/// (never committed — the reference crops them identically). Kills the per-round full-ctx
423/// projection recompute (first light was O(ctx)/round -> 7 tok/s).
424pub struct DflashKv {
425    pub k: Vec<CudaSlice<f32>>,   // per layer [cap + block, nkv*hd]
426    pub v: Vec<CudaSlice<f32>>,
427    pub len: usize,
428    pub cap: usize,
429}
430
431impl DflashKv {
432    pub fn new(e: &Engine, cfg: &DflashCfg, cap: usize) -> Result<Self, Box<dyn std::error::Error>> {
433        let rowsz = cfg.n_kv * cfg.head_dim;
434        let mut k = Vec::with_capacity(cfg.n_layer);
435        let mut v = Vec::with_capacity(cfg.n_layer);
436        for _ in 0..cfg.n_layer {
437            k.push(e.uninit((cap + cfg.block_size) * rowsz)?);
438            v.push(e.uninit((cap + cfg.block_size) * rowsz)?);
439        }
440        Ok(Self { k, v, len: 0, cap })
441    }
442}
443
444impl DflashDraft {
445    /// Ingest `t` NEW ctx-feature rows (committed order, absolute positions `pos_new`) into
446    /// the draft KV: per layer k/v projections + k head-norm + rope, appended at kv.len.
447    pub fn ingest_ctx(&self, e: &Engine, kv: &mut DflashKv, feats: &CudaSlice<f32>,
448                      pos_new: &[i32], t: usize) -> Result<(), Box<dyn std::error::Error>> {
449        let c = &self.cfg;
450        let (h, nkv, hd) = (c.hidden, c.n_kv, c.head_dim);
451        assert!(kv.len + t <= kv.cap, "draft kv overflow");
452        let pos_d = e.htod_i32(pos_new)?;
453        for (li, l) in self.layers.iter().enumerate() {
454            let k0 = self.mm(e, &l.wk, feats, t, h, nkv * hd)?;
455            let v0 = self.mm(e, &l.wv, feats, t, h, nkv * hd)?;
456            let mut kn = e.uninit(t * nkv * hd)?;
457            e.rms_norm(&k0, &l.k_norm, &mut kn, hd, t * nkv, c.eps)?;
458            e.rope_neox(&mut kn, &pos_d, hd, hd, nkv, t, c.rope_theta, 1.0)?;
459            e.copy_into(&mut kv.k[li], kv.len * nkv * hd, &kn, t * nkv * hd)?;
460            e.copy_into(&mut kv.v[li], kv.len * nkv * hd, &v0, t * nkv * hd)?;
461        }
462        kv.len += t;
463        Ok(())
464    }
465
466    /// Block forward over the CACHED ctx KV: only the 16 block rows are projected per layer;
467    /// block K/V land transiently at kv[len..len+b]. Bit-class-identical to forward_block
468    /// (same kernels, same per-row programs; ONLY the ctx K/V recompute is cached).
469    pub fn forward_round(&self, e: &Engine, kv: &mut DflashKv, noise_emb: &CudaSlice<f32>,
470                         pos_block: &[i32]) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
471        let c = &self.cfg;
472        let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
473        let b = c.block_size;
474        assert_eq!(pos_block.len(), b);
475        let ctx = kv.len;
476        let pos_blk = e.htod_i32(pos_block)?;
477        let mut x = e.clone_dtod(noise_emb)?;
478        for (li, l) in self.layers.iter().enumerate() {
479            let mut xn = e.uninit(b * h)?;
480            e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
481            let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
482            let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
483            let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
484            let mut q = e.uninit(b * nh * hd)?;
485            let mut kb = e.uninit(b * nkv * hd)?;
486            e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
487            e.rms_norm(&k0b, &l.k_norm, &mut kb, hd, b * nkv, c.eps)?;
488            e.rope_neox(&mut q, &pos_blk, hd, hd, nh, b, c.rope_theta, 1.0)?;
489            e.rope_neox(&mut kb, &pos_blk, hd, hd, nkv, b, c.rope_theta, 1.0)?;
490            e.copy_into(&mut kv.k[li], ctx * nkv * hd, &kb, b * nkv * hd)?;
491            e.copy_into(&mut kv.v[li], ctx * nkv * hd, &v0b, b * nkv * hd)?;
492            let mut attn = e.uninit(b * nh * hd)?;
493            let scale = 1.0f32 / (hd as f32).sqrt();
494            if std::env::var("MEMRA_DFLASH_FA").is_ok() {
495                e.fa_prefill(&q, &kv.k[li], &kv.v[li], &mut attn, hd, nh, nkv, b, ctx + b,
496                             scale, false)?;
497            } else {
498                e.sdpa_naive(&q, &kv.k[li], &kv.v[li], &mut attn, hd, nh, nkv, b, ctx + b,
499                             scale, false)?;
500            }
501            let o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
502            let mut x1 = e.uninit(b * h)?;
503            e.add(&o, &x, &mut x1, b * h)?;
504            let mut x1n = e.uninit(b * h)?;
505            e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
506            let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
507            let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
508            let mut act = e.uninit(b * c.n_ff)?;
509            e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
510            let down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
511            let mut x2 = e.uninit(b * h)?;
512            e.add(&down, &x1, &mut x2, b * h)?;
513            x = x2;
514        }
515        let mut out = e.uninit(b * h)?;
516        e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
517        Ok(out)
518    }
519}
520
521// ================= DFlash spec round (greedy, first light) =================
522// Exact contract: identical output stream to plain greedy decode BY CONSTRUCTION — the
523// target's batched verify argmax decides every committed token; the drafter only proposes.
524// (Same verify+rewind pattern as generate_spec_gemma's eager round; t=16 verify rides the
525// straddle-split-safe fa_decode_rows.)
526impl crate::hybrid::HybridModel {
527    pub fn generate_spec_dflash(
528        &self, e: &Engine, draft: &DflashDraft, prompt: &[u32], max_new: usize, eos: &[u32],
529    ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
530        use crate::cache::{Cache, DflashTapSink};
531        let n_embd = self.cfg.n_embd as usize;
532        let c = &draft.cfg;
533        assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
534        let b = c.block_size;
535        let n_taps = c.target_layer_ids.len();
536        let max_ctx = prompt.len() + max_new + b + 8;
537        // First light holds ctx <= sliding_window: the draft was trained with 4 sliding
538        // layers (window 2048) and the first-light attention is windowless full — inside
539        // the window the two are identical. The depth cell (1736 + 128) fits.
540        assert!(max_ctx <= c.sliding_window,
541                "first-light dflash round is windowless — ctx cap {} exceeds the draft window {}",
542                max_ctx, c.sliding_window);
543        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
544
545        // ---- prime with taps armed ----
546        let tp = prompt.len();
547        cache.dflash_taps = Some(DflashTapSink {
548            layer_ids: c.target_layer_ids.clone(),
549            buf: e.uninit(tp * n_taps * n_embd)?,
550            hidden: n_embd, t: tp,
551        });
552        let t_prime = std::time::Instant::now();
553        let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
554        let mut last = crate::forward::argmax(&logits) as u32;
555        // draft KV cache: ingest the prompt's ctx features once; per round only the kept
556        // rows ingest + the block projects (round cost O(block), not O(ctx)).
557        let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
558        {
559            // CHUNKED ingest (depth OOM fix): the 1736-row prompt tap buffer is ~224MB f32;
560            // running fc + 5-layer k/v projection over it in one shot stacks another
561            // ~300MB of transients on the ~21.3GB trunk peak. 256-row windows bound the
562            // transient set; identical values (row-independent ops).
563            let taps = cache.dflash_taps.take().unwrap();
564            let n_taps_h = n_taps * n_embd;
565            let mut r0 = 0usize;
566            while r0 < tp {
567                let t_c = (tp - r0).min(256);
568                let tv = e.view(&taps.buf, tp * n_taps_h);
569                let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
570                let mut chunk = e.uninit(t_c * n_taps_h)?;
571                e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
572                let f = draft.ctx_features(e, &chunk, t_c)?;
573                let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
574                draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
575                r0 += t_c;
576            }
577        }
578        let mut ctx_len = tp;
579        e.stream().synchronize()?;
580        // published prime wall (the run-spec/gemma-gate timing contract subtracts it)
581        crate::PRIME_NANOS.store(t_prime.elapsed().as_nanos() as u64,
582                                 std::sync::atomic::Ordering::Relaxed);
583
584        // embed-scale seam (MEMRA_DFLASH_EMB_SCALE): gemma trunks scale embeddings by
585        // sqrt(n_embd) INSIDE the forward; whether the z-lab gemma4 training fed the
586        // drafter scaled or raw embed rows is not visible from the reference (qwen path
587        // uses raw embed_tokens). Acceptance arbitrates; default raw.
588        let emb_scale = if std::env::var("MEMRA_DFLASH_EMB_SCALE").as_deref() == Ok("1") {
589            (n_embd as f32).sqrt()
590        } else { 1.0 };
591
592        let mut out = Vec::with_capacity(max_new);
593        let n_vocab = self.output.out_features();
594        // VERIFY WIDTH (MEMRA_DFLASH_VERIFY_T, default 8): the drafter always drafts a full
595        // block (its trained mask pattern) but only the first vt rows go through the target
596        // verify — the t=16 verify rides the untuned b16 tier at ~32% of the byte wall
597        // (65ms/verify) while b8 rides the tuned r2 tier; with ~2.7 committed/round the
598        // deep block positions almost never survive anyway. Exactness unaffected (verify
599        // still decides every committed token).
600        let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T").ok()
601            .and_then(|v| v.parse().ok()).unwrap_or(8).clamp(2, b);
602        // adaptive verify width (MEMRA_DFLASH_ADAPT!=0, MTP accepted+1 recipe): next round
603        // verifies one past this round's accepted run, clamped [3, cap].
604        let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
605        let mut vt = vt_cap;
606        let mut attempted = 0usize;
607        let mut accepted = 0usize;
608        // The whole round runs in the decode-exact matmul scope: the m=16 draft mms were
609        // otherwise falling into the prefill-GEMM class (770us/matmul, 17% of the depth
610        // round). Prime (before this loop) keeps the prefill GEMM path.
611        e.set_verify_exact(true);
612        'outer: while out.len() < max_new {
613            let start = cache.pos; // committed length
614            // ---- draft: block = [last, MASK x b-1] ----
615            let mut block: Vec<u32> = vec![c.mask_token_id; b];
616            block[0] = last;
617            let mut noise = e.htod(&self.embd.gather(n_embd, &block))?;
618            if emb_scale != 1.0 { e.scale_inplace(&mut noise, emb_scale, b * n_embd)?; }
619            if std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1") && start == cache.pos {
620                let nv = e.dtoh(&noise)?;
621                let r0: f32 = nv[..n_embd].iter().map(|x| x * x).sum::<f32>().sqrt();
622                let r1: f32 = nv[n_embd..2 * n_embd].iter().map(|x| x * x).sum::<f32>().sqrt();
623                eprintln!("[dflash noise] |row0(last)|={r0:.3} |row1(MASK id {})|={r1:.3}",
624                          c.mask_token_id);
625            }
626            let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
627            let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
628            // draft tokens = argmax(lm_head(h rows 1..b))
629            let mut rows = e.uninit((b - 1) * n_embd)?;
630            {
631                let dv = e.view(&dh, b * n_embd);
632                let tail = dv.slice(n_embd..b * n_embd);
633                e.copy_view_into(&mut rows, 0, &tail, (b - 1) * n_embd)?;
634            }
635            let mut dl = e.matmul(&self.output, &rows, b - 1)?;
636            // SEMI-AR MARKOV CHAIN (DSpark head, when present + MEMRA_DFLASH_MARKOV!=0):
637            // left-to-right, logits_k += W2(W1[prev realized token]) — the whole chain
638            // stays on-device (chain_d[0] = the pending token; argmax k writes
639            // chain_d[k+1], the k+1 bias gathers from it). Greedy mirror of the patch's
640            // _markov_semiar_sample_block.
641            let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
642            let mut chain_d = e.stream().alloc_zeros::<u32>(b)?;
643            if let (Some(mk), true) = (&draft.markov, markov_on) {
644                e.set_u32_one(&mut chain_d, last)?;
645                for k in 0..(b - 1) {
646                    let mut f = e.uninit(mk.rank)?;
647                    e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
648                    let bias = e.matmul(&mk.w2, &f, 1)?;
649                    e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
650                    e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
651                }
652            } else {
653                for i in 0..(b - 1) {
654                    e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
655                }
656            }
657            let chain = e.dtoh_u32(&chain_d)?;
658            let dtoks = &chain[1..];
659            for (i, &dt) in dtoks.iter().enumerate() { block[i + 1] = dt; }
660            let dbg = std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1");
661
662            // ---- verify: one t=vt target forward with taps armed ----
663            let vblock = &block[..vt];
664            cache.dflash_taps = Some(DflashTapSink {
665                layer_ids: c.target_layer_ids.clone(),
666                buf: e.uninit(vt * n_taps * n_embd)?,
667                hidden: n_embd, t: vt,
668            });
669            let (vam, _vh) = self.gemma4_decode_step_t_am(e, vblock, start, &mut cache)?;
670            let taps = cache.dflash_taps.take().unwrap();
671            if dbg {
672                eprintln!("[dflash r] start={start} last={last}\n  draft={:?}\n  vam  ={:?}",
673                          &block[1..], &vam);
674            }
675
676            // ---- accept ----
677            let mut m = 0usize;
678            while m < vt - 1 && block[m + 1] as usize == vam[m] as usize { m += 1; }
679            attempted += vt - 1;
680            accepted += m;
681            out.push(last);
682            if eos.contains(&last) { break 'outer; }
683            for &dt in &block[1..=m] {
684                out.push(dt);
685                if eos.contains(&dt) { break 'outer; }
686                if out.len() >= max_new { break 'outer; }
687            }
688            let next = vam[m] as u32;
689
690            // ---- commit/rollback: keep m+1 of the b appended rows ----
691            let keep = m + 1;
692            for kvl in cache.kv.iter_mut().flatten() {
693                kvl.len -= vt - keep;
694                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
695            }
696            cache.pos -= vt - keep;
697
698            // ---- ingest the kept rows' ctx features into the draft KV ----
699            {
700                let tv = e.view(&taps.buf, vt * n_taps * n_embd);
701                let keep_view = tv.slice(0..keep * n_taps * n_embd);
702                let mut kept = e.uninit(keep * n_taps * n_embd)?;
703                e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
704                let f = draft.ctx_features(e, &kept, keep)?;
705                let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
706                draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
707                ctx_len += keep;
708            }
709            last = next;
710            if adapt { vt = (m + 2).clamp(3, vt_cap); }
711        }
712        e.set_verify_exact(false);
713        if std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1") {
714            eprintln!("[dflash] acceptance {accepted}/{attempted} = {:.3}",
715                      accepted as f64 / attempted.max(1) as f64);
716        }
717        Ok(out)
718    }
719}