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