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    /// DSpark accept-rate head (trained with confidence loss; the reference serving
59    /// loop — SpecForge spec_generate — never consumes it, so serving ignores it too.
60    /// Loaded for census completeness + the parity gate; host-resident (5k floats).
61    pub confidence: Option<ConfidenceHead>,
62    /// YaRN rope (q38 arm-a inherits the target's rope_parameters: rope_type yarn,
63    /// factor 32, original 8192, beta 32/1). ff = per-dim divisors for rope_neox_ff
64    /// (effective inv_freq_j = base^(-2j/d)/ff[j] = the HF-yarn remapped frequency,
65    /// verified vs Qwen3RotaryEmbedding to 1.6e-7), mscale = attention_scaling
66    /// (0.1*ln(factor)+1) applied to q/k post-rope — cos/sin scaling distributes onto
67    /// the rotated vector exactly. None = plain rope (gemma/z-lab drafters).
68    pub rope_yarn: Option<(CudaSlice<f32>, f32)>,
69}
70
71/// AcceptRatePredictor: raw linear proj over [hidden ; markov_prev_embedding(rank)]
72/// (with_markov=true on the q38 arm-a export) — output is the PRE-sigmoid scalar.
73pub struct ConfidenceHead {
74    pub w: Vec<f32>, // [in_dim]
75    pub b: f32,
76    pub in_dim: usize,
77    pub with_markov: bool,
78}
79
80pub struct MarkovHead {
81    pub w1_bf16: CudaSlice<u8>, // [V, rank] bf16 raw
82    pub w2: GpuTensor,          // [rank -> V] q8_0
83    pub rank: usize,
84    pub vocab: usize,
85}
86
87fn bf16_to_f32(bytes: &[u8]) -> Vec<f32> {
88    bytes
89        .chunks_exact(2)
90        .map(|c| f32::from_bits((u16::from_le_bytes([c[0], c[1]]) as u32) << 16))
91        .collect()
92}
93
94/// Host q8_0 encode (ggml block layout: [d f16][32 x i8] = 34B/32 vals). The drafter's
95/// weights ride the dp4a fast path at 1.6GB resident (bf16 3.1GB + the 31B trunk OOM'd
96/// 24GB; f32 6.2GB worse). Drafter quantization moves ACCEPTANCE only — verify exactness
97/// is structural.
98fn encode_q8_0(vals: &[f32]) -> Vec<u8> {
99    let mut out = Vec::with_capacity(vals.len() / 32 * 34);
100    for blk in vals.chunks_exact(32) {
101        let amax = blk.iter().fold(0f32, |a, v| a.max(v.abs()));
102        let d = amax / 127.0;
103        let id = if d > 0.0 { 1.0 / d } else { 0.0 };
104        let dh = half_from_f32(d);
105        out.extend_from_slice(&dh.to_le_bytes());
106        for &v in blk {
107            out.push(((v * id).round().clamp(-127.0, 127.0)) as i8 as u8);
108        }
109    }
110    out
111}
112
113/// Host q4_0 encode (ggml: [d f16][16B packed nibbles] = 18B/32 vals; q = round(v/d)+8,
114/// d = amax/-7 sign trick NOT used — plain amax/7? ggml uses d = max/-8 .. follow ggml:
115/// d = amax / -8 when the max is negative-dominant; reference quantize_row_q4_0: d =
116/// max(|v|)/-8 signed-max form). Implemented to match ggml quantize_row_q4_0_ref.
117fn encode_q4_0(vals: &[f32]) -> Vec<u8> {
118    let mut out = Vec::with_capacity(vals.len() / 32 * 18);
119    for blk in vals.chunks_exact(32) {
120        // ggml ref: pick the value with the LARGEST |v| (keeping sign), d = that / -8
121        let mut amax = 0f32;
122        let mut mx = 0f32;
123        for &v in blk {
124            if v.abs() > amax {
125                amax = v.abs();
126                mx = v;
127            }
128        }
129        let d = mx / -8.0;
130        let id = if d != 0.0 { 1.0 / d } else { 0.0 };
131        out.extend_from_slice(&half_from_f32(d).to_le_bytes());
132        for j in 0..16 {
133            let x0 = (blk[j] * id + 8.5).clamp(0.0, 15.0) as u8;
134            let x1 = (blk[j + 16] * id + 8.5).clamp(0.0, 15.0) as u8;
135            out.push(x0 | (x1 << 4));
136        }
137    }
138    out
139}
140
141fn half_from_f32(v: f32) -> u16 {
142    // f32 -> IEEE f16 (round-to-nearest-even; range of q8_0 d values is tame)
143    let b = v.to_bits();
144    let sign = ((b >> 16) & 0x8000) as u16;
145    let exp = ((b >> 23) & 0xff) as i32 - 127 + 15;
146    let man = b & 0x7fffff;
147    if exp <= 0 {
148        return sign;
149    } // flush tiny d to zero
150    if exp >= 31 {
151        return sign | 0x7c00;
152    } // inf (unreachable for sane d)
153    let mut h = sign | ((exp as u16) << 10) | ((man >> 13) as u16);
154    // round to nearest even on the truncated 13 bits
155    let rem = man & 0x1fff;
156    if rem > 0x1000 || (rem == 0x1000 && (h & 1) == 1) {
157        h += 1;
158    }
159    h
160}
161
162impl DflashDraft {
163    /// Load the backbone-only checkpoint dir (config.json + model.safetensors, bf16).
164    /// Config scalars ride a minimal extractor (no json dep in-tree — HfConfig precedent).
165    pub fn load(e: &Engine, dir: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
166        let txt = std::fs::read_to_string(dir.join("config.json"))?;
167        fn num(txt: &str, key: &str) -> Option<f64> {
168            let i = txt.find(&format!("\"{key}\""))?;
169            let rest = &txt[i..];
170            let colon = rest.find(':')?;
171            let val: String = rest[colon + 1..]
172                .trim_start()
173                .chars()
174                .take_while(|c| {
175                    c.is_ascii_digit()
176                        || *c == '.'
177                        || *c == '-'
178                        || *c == 'e'
179                        || *c == 'E'
180                        || *c == '+'
181                })
182                .collect();
183            val.parse().ok()
184        }
185        fn num_list(txt: &str, key: &str) -> Vec<usize> {
186            let Some(i) = txt.find(&format!("\"{key}\"")) else {
187                return Vec::new();
188            };
189            let rest = &txt[i..];
190            let (Some(a), Some(b)) = (rest.find('['), rest.find(']')) else {
191                return Vec::new();
192            };
193            rest[a + 1..b]
194                .split(',')
195                .filter_map(|s| s.trim().parse().ok())
196                .collect()
197        }
198        let g = |k: &str| num(&txt, k).unwrap_or_else(|| panic!("config missing {k}")) as usize;
199        // layer_types order: count entries, mark sliding ones
200        let layer_sliding: Vec<bool> = {
201            let i = txt.find("\"layer_types\"").expect("layer_types");
202            let rest = &txt[i..];
203            let (a, b) = (rest.find('[').unwrap(), rest.find(']').unwrap());
204            rest[a + 1..b]
205                .split(',')
206                .map(|s| s.contains("sliding_attention"))
207                .collect()
208        };
209        // sliding_window is null on all-full-attention exports (q38 arm-a); the window
210        // only constrains rounds when a sliding layer exists (reference: resolve_dflash_
211        // attention_layout returns None when no layer slides).
212        let sliding_window = if layer_sliding.iter().any(|&s| s) {
213            g("sliding_window")
214        } else {
215            num(&txt, "sliding_window")
216                .map(|v| v as usize)
217                .unwrap_or(usize::MAX)
218        };
219        let cfg = DflashCfg {
220            hidden: g("hidden_size"),
221            n_head: g("num_attention_heads"),
222            n_kv: g("num_key_value_heads"),
223            head_dim: g("head_dim"),
224            n_ff: g("intermediate_size"),
225            n_layer: g("num_hidden_layers"),
226            eps: num(&txt, "rms_norm_eps").expect("rms_norm_eps") as f32,
227            rope_theta: num(&txt, "rope_theta").expect("rope_theta") as f32,
228            block_size: g("block_size"),
229            mask_token_id: g("mask_token_id") as u32,
230            target_layer_ids: num_list(&txt, "target_layer_ids"),
231            sliding_window,
232            layer_sliding,
233        };
234        let st = memra_gguf::safetensors::StModel::open(&dir.join("model.safetensors"))?;
235        // 1D norm weights ride raw slices; 2D matmul weights ride GpuTensor::Float
236        // (cuBLASLt f32 arm — the Stage-A numeric class, right for oracle parity).
237        let up = |name: &str| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
238            let (_info, bytes) = st
239                .raw(name)
240                .ok_or_else(|| format!("missing tensor {name}"))?;
241            Ok(e.htod(&bf16_to_f32(bytes))?)
242        };
243        // Precision policy (MEMRA_DFLASH_PREC seam): "q8" = all q8_0 (1.6GB, default);
244        // "mixed" = bf16 attn+fc (the ctx-conditioning path) + q8_0 ffn (~2.2GB — fits the
245        // ~2.8GB headroom beside the 31B trunk); "bf16" = all bf16 (parity runs, no target).
246        let prec = std::env::var("MEMRA_DFLASH_PREC").unwrap_or_else(|_| "q8".into());
247        let upw = |name: &str| -> Result<GpuTensor, Box<dyn std::error::Error>> {
248            let (info, bytes) = st
249                .raw(name)
250                .ok_or_else(|| format!("missing tensor {name}"))?;
251            let shape = info.ne(); // ggml order: ne[0]=in_f, ne[1]=out_f
252            let in_f = shape[0] as usize;
253            let is_ffn = name.contains(".mlp.");
254            let bf16 = prec == "bf16"
255                || (prec == "mixed" && !is_ffn)
256                || (prec == "fc" && name == "fc.weight");
257            if bf16 {
258                return Ok(GpuTensor::FloatBf16 {
259                    data: e.upload_u8(bytes)?,
260                    ne: shape.to_vec(),
261                });
262            }
263            let f32s = bf16_to_f32(bytes);
264            if prec == "q4" {
265                let q = encode_q4_0(&f32s);
266                return Ok(GpuTensor::Quant {
267                    bytes: e.upload_u8(&q)?,
268                    qtype: crate::QT_Q4_0,
269                    row_bytes: in_f / 32 * 18,
270                    ne: shape.to_vec(),
271                    scale: 1.0,
272                    rp: false,
273                    #[cfg(memra_cutlass)]
274                    cutlass: None,
275                    fp8: None,
276                    blk: None,
277                    rp4: None,
278                    f16: None,
279                });
280            }
281            let q = encode_q8_0(&f32s);
282            Ok(GpuTensor::Quant {
283                bytes: e.upload_u8(&q)?,
284                qtype: crate::QT_Q8_0,
285                row_bytes: in_f / 32 * 34,
286                ne: shape.to_vec(),
287                scale: 1.0,
288                rp: false,
289                #[cfg(memra_cutlass)]
290                cutlass: None,
291                fp8: None,
292                blk: None,
293                rp4: None,
294                f16: None,
295            })
296        };
297        let mut layers = Vec::with_capacity(cfg.n_layer);
298        for i in 0..cfg.n_layer {
299            let p = |s: &str| format!("layers.{i}.{s}");
300            layers.push(DflashLayer {
301                wq: upw(&p("self_attn.q_proj.weight"))?,
302                wk: upw(&p("self_attn.k_proj.weight"))?,
303                wv: upw(&p("self_attn.v_proj.weight"))?,
304                wo: upw(&p("self_attn.o_proj.weight"))?,
305                w_gate: upw(&p("mlp.gate_proj.weight"))?,
306                w_up: upw(&p("mlp.up_proj.weight"))?,
307                w_down: upw(&p("mlp.down_proj.weight"))?,
308                ln_in: up(&p("input_layernorm.weight"))?,
309                ln_post: up(&p("post_attention_layernorm.weight"))?,
310                q_norm: up(&p("self_attn.q_norm.weight"))?,
311                k_norm: up(&p("self_attn.k_norm.weight"))?,
312            });
313        }
314        let markov = if let Some((info, bytes)) = st.raw("markov_head.markov_w1.weight") {
315            let sh = info.ne(); // [rank, vocab] in ggml order (safetensors [V, rank] reversed)
316            let (rank, vocab) = (sh[0] as usize, sh[1] as usize);
317            let (i2, b2) = st
318                .raw("markov_head.markov_w2.weight")
319                .ok_or("markov_w2 missing beside markov_w1")?;
320            // w2 follows the precision seam: bf16 for parity runs (the q8_0 encode is a
321            // serving-size choice and would put quant error inside the markov-logits gate),
322            // q8_0 otherwise (acceptance-only impact, like the trunk weights).
323            let w2 = if prec == "bf16" {
324                GpuTensor::FloatBf16 {
325                    data: e.upload_u8(b2)?,
326                    ne: i2.ne().to_vec(),
327                }
328            } else {
329                let w2f = bf16_to_f32(b2);
330                let w2q = encode_q8_0(&w2f);
331                GpuTensor::Quant {
332                    bytes: e.upload_u8(&w2q)?,
333                    qtype: crate::QT_Q8_0,
334                    row_bytes: rank / 32 * 34,
335                    ne: vec![rank as u64, vocab as u64],
336                    scale: 1.0,
337                    rp: false,
338                    #[cfg(memra_cutlass)]
339                    cutlass: None,
340                    fp8: None,
341                    blk: None,
342                    rp4: None,
343                    f16: None,
344                }
345            };
346            Some(MarkovHead {
347                w1_bf16: e.upload_u8(bytes)?,
348                w2,
349                rank,
350                vocab,
351            })
352        } else {
353            None
354        };
355        let confidence = if let Some((info, bytes)) = st.raw("confidence_head.proj.weight") {
356            let sh = info.ne(); // ggml order: ne[0]=in_dim, ne[1]=1
357            let in_dim = sh[0] as usize;
358            let (_bi, bb) = st
359                .raw("confidence_head.proj.bias")
360                .ok_or("confidence bias missing beside weight")?;
361            let with_markov = markov
362                .as_ref()
363                .map(|m| in_dim == cfg.hidden + m.rank)
364                .unwrap_or(false);
365            if !with_markov && in_dim != cfg.hidden {
366                panic!(
367                    "confidence_head in_dim {in_dim} matches neither hidden {} nor hidden+rank",
368                    cfg.hidden
369                );
370            }
371            Some(ConfidenceHead {
372                w: bf16_to_f32(bytes),
373                b: bf16_to_f32(bb)[0],
374                in_dim,
375                with_markov,
376            })
377        } else {
378            None
379        };
380        // CENSUS GATE: every tensor in the export must be consumed by the map above.
381        // DSpark-class checkpoints (markov head present) REFUSE on unrecognized names —
382        // an unmapped tensor is a semantic program we would silently drop (house law).
383        // Plain dflash checkpoints keep the historical warn-only behavior.
384        {
385            let mut consumed: std::collections::HashSet<String> = std::collections::HashSet::new();
386            for i in 0..cfg.n_layer {
387                for s in [
388                    "self_attn.q_proj.weight",
389                    "self_attn.k_proj.weight",
390                    "self_attn.v_proj.weight",
391                    "self_attn.o_proj.weight",
392                    "self_attn.q_norm.weight",
393                    "self_attn.k_norm.weight",
394                    "input_layernorm.weight",
395                    "post_attention_layernorm.weight",
396                    "mlp.gate_proj.weight",
397                    "mlp.up_proj.weight",
398                    "mlp.down_proj.weight",
399                ] {
400                    consumed.insert(format!("layers.{i}.{s}"));
401                }
402            }
403            for s in [
404                "fc.weight",
405                "hidden_norm.weight",
406                "norm.weight",
407                "markov_head.markov_w1.weight",
408                "markov_head.markov_w2.weight",
409                "confidence_head.proj.weight",
410                "confidence_head.proj.bias",
411            ] {
412                consumed.insert(s.into());
413            }
414            let leftovers: Vec<&String> = st.names().filter(|n| !consumed.contains(*n)).collect();
415            if !leftovers.is_empty() {
416                if markov.is_some() {
417                    panic!("dspark census: unrecognized tensors {leftovers:?}");
418                }
419                eprintln!("[dflash census] unmapped tensors (ignored): {leftovers:?}");
420            }
421        }
422        // YaRN rope from config rope_parameters (HF _compute_yarn_parameters, verified
423        // numerically vs Qwen3RotaryEmbedding on the arm-a export).
424        let rope_yarn =
425            if txt.contains("\"rope_type\": \"yarn\"") || txt.contains("\"rope_type\":\"yarn\"") {
426                let factor = num(&txt, "factor").expect("yarn factor") as f64;
427                let orig = num(&txt, "original_max_position_embeddings").expect("yarn orig");
428                let beta_fast = num(&txt, "beta_fast").expect("beta_fast");
429                let beta_slow = num(&txt, "beta_slow").expect("beta_slow");
430                let base = cfg.rope_theta as f64;
431                let d = cfg.head_dim as f64;
432                let corr =
433                    |r: f64| d * (orig / (r * 2.0 * std::f64::consts::PI)).ln() / (2.0 * base.ln());
434                let low = corr(beta_fast).floor().max(0.0);
435                let high = corr(beta_slow).ceil().min(d - 1.0);
436                let half = cfg.head_dim / 2;
437                let mut ff = Vec::with_capacity(half);
438                for j in 0..half {
439                    let base_inv = base.powf(-2.0 * j as f64 / d);
440                    let ramp = (((j as f64) - low) / (high - low)).clamp(0.0, 1.0);
441                    let ex = 1.0 - ramp; // extrapolation share
442                    let yarn_inv = (base_inv / factor) * (1.0 - ex) + base_inv * ex;
443                    ff.push((base_inv / yarn_inv) as f32);
444                }
445                let mscale = (0.1 * factor.ln() + 1.0) as f32;
446                Some((e.htod(&ff)?, mscale))
447            } else {
448                None
449            };
450        Ok(Self {
451            fc: upw("fc.weight")?,
452            hidden_norm: up("hidden_norm.weight")?,
453            norm: up("norm.weight")?,
454            cfg,
455            layers,
456            markov,
457            confidence,
458            rope_yarn,
459        })
460    }
461
462    /// Rope q or k rows in place: yarn (ff divisors + post-rope mscale) when the config
463    /// carries it, plain neox otherwise. One primitive for all five drafter rope sites.
464    fn rope_rows(
465        &self,
466        e: &Engine,
467        x: &mut CudaSlice<f32>,
468        pos_d: &CudaSlice<i32>,
469        n_heads: usize,
470        n_tokens: usize,
471    ) -> Result<(), Box<dyn std::error::Error>> {
472        let c = &self.cfg;
473        match &self.rope_yarn {
474            Some((ff, mscale)) => {
475                e.rope_neox_ff(
476                    x,
477                    pos_d,
478                    c.head_dim,
479                    c.head_dim,
480                    n_heads,
481                    n_tokens,
482                    c.rope_theta,
483                    1.0,
484                    ff,
485                )?;
486                e.scale_inplace(x, *mscale, n_tokens * n_heads * c.head_dim)?;
487            }
488            None => {
489                e.rope_neox(
490                    x,
491                    pos_d,
492                    c.head_dim,
493                    c.head_dim,
494                    n_heads,
495                    n_tokens,
496                    c.rope_theta,
497                    1.0,
498                )?;
499            }
500        }
501        Ok(())
502    }
503
504    /// f32 GEMM helper via the engine Float arm (cuBLASLt): y[t, out_f].
505    fn mm(
506        &self,
507        e: &Engine,
508        w: &GpuTensor,
509        x: &CudaSlice<f32>,
510        t: usize,
511        _in_f: usize,
512        _out_f: usize,
513    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
514        Ok(e.matmul(w, x, t)?)
515    }
516
517    /// FIRST-LIGHT forward (oracle contract): full non-causal attention over
518    /// [ctx_features ; block], NO draft KV cache, NO sliding window (the oracle bypasses
519    /// the reference mask machinery the same way — window/caching land in the round arm).
520    ///
521    /// `target_hidden`: [ctx, n_taps*hidden] (f32, device)  — raw tapped states.
522    /// `noise_emb`:     [block, hidden] — target embed rows for [accepted, MASK x b-1].
523    /// `pos`:           absolute positions for ctx rows THEN block rows (ctx+block i32).
524    /// Returns final normed hidden [block, hidden] (feed target lm_head for draft logits).
525    /// ctx features for `t` tapped rows: hidden_norm(fc(taps)) — the drafter's context
526    /// representation, cacheable across rounds (append-only in committed-token order).
527    pub fn ctx_features(
528        &self,
529        e: &Engine,
530        taps: &CudaSlice<f32>,
531        t: usize,
532    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
533        let c = &self.cfg;
534        let n_taps = c.target_layer_ids.len();
535        let fc_out = self.mm(e, &self.fc, taps, t, n_taps * c.hidden, c.hidden)?;
536        let mut out = e.uninit(t * c.hidden)?;
537        e.rms_norm(&fc_out, &self.hidden_norm, &mut out, c.hidden, t, c.eps)?;
538        Ok(out)
539    }
540
541    pub fn forward(
542        &self,
543        e: &Engine,
544        target_hidden: &CudaSlice<f32>,
545        noise_emb: &CudaSlice<f32>,
546        pos: &[i32],
547        ctx: usize,
548    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
549        let ctx_f = self.ctx_features(e, target_hidden, ctx)?;
550        if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
551            let v = e.dtoh(&ctx_f)?;
552            let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
553            std::fs::write(format!("{dir}/memra-ctx_features.f32"), bytes)?;
554        }
555        self.forward_block(e, &ctx_f, noise_emb, pos, ctx)
556    }
557
558    /// Block forward over PRECOMPUTED ctx features (the round arm's entry: features are
559    /// cached across rounds; only the block work repeats).
560    pub fn forward_block(
561        &self,
562        e: &Engine,
563        ctx_f: &CudaSlice<f32>,
564        noise_emb: &CudaSlice<f32>,
565        pos: &[i32],
566        ctx: usize,
567    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
568        let c = &self.cfg;
569        let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
570        let b = c.block_size;
571        assert_eq!(pos.len(), ctx + b, "pos covers ctx rows then block rows");
572
573        let pos_blk = e.htod_i32(&pos[ctx..])?;
574
575        let mut x = e.clone_dtod(noise_emb)?; // [b, hidden] residual stream
576        for (li, l) in self.layers.iter().enumerate() {
577            let _ = li;
578            // input_layernorm on the block rows only (ctx features are norm-free per ref:
579            // k/v project the SAME ctx_f every layer, un-layernormed).
580            let mut xn = e.uninit(b * h)?;
581            e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
582
583            // q from block; k/v from [ctx_f ; block-normed]
584            let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
585            let k0c = self.mm(e, &l.wk, ctx_f, ctx, h, nkv * hd)?;
586            let v0c = self.mm(e, &l.wv, ctx_f, ctx, h, nkv * hd)?;
587            let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
588            let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
589
590            // per-head q/k rms norm (v passes through: ones weight trick not needed — the
591            // qkv kernel norms rq+rk rows; concatenate k first).
592            let mut k0 = e.uninit((ctx + b) * nkv * hd)?;
593            e.copy_into(&mut k0, 0, &k0c, ctx * nkv * hd)?;
594            e.copy_into(&mut k0, ctx * nkv * hd, &k0b, b * nkv * hd)?;
595            let mut v = e.uninit((ctx + b) * nkv * hd)?;
596            e.copy_into(&mut v, 0, &v0c, ctx * nkv * hd)?;
597            e.copy_into(&mut v, ctx * nkv * hd, &v0b, b * nkv * hd)?;
598
599            if li == 0 {
600                if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
601                    let v = e.dtoh(&q0)?;
602                    let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
603                    std::fs::write(format!("{dir}/memra-l0_q0.f32"), bytes)?;
604                }
605            }
606            let mut q = e.uninit(b * nh * hd)?;
607            let mut k = e.uninit((ctx + b) * nkv * hd)?;
608            // rms over head_dim rows: q has b*nh rows, k has (ctx+b)*nkv rows.
609            e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
610            if li == 0 {
611                if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
612                    let v = e.dtoh(&q)?;
613                    let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
614                    std::fs::write(format!("{dir}/memra-l0_qn.f32"), bytes)?;
615                }
616            }
617            e.rms_norm(&k0, &l.k_norm, &mut k, hd, (ctx + b) * nkv, c.eps)?;
618
619            // rope: q at block positions, k at ctx-then-block positions (absolute).
620            let norope = std::env::var("MEMRA_DFLASH_NOROPE").is_ok();
621            if !norope {
622                self.rope_rows(e, &mut q, &pos_blk, nh, b)?;
623            }
624            if li == 0 {
625                if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
626                    let dump = |name: &str,
627                                t: &cudarc::driver::CudaSlice<f32>|
628                     -> Result<(), Box<dyn std::error::Error>> {
629                        let v = e.dtoh(t)?;
630                        let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
631                        std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
632                        Ok(())
633                    };
634                    dump("xn", &xn)?;
635                    dump("q_prerope", &q)?;
636                }
637            }
638            // k rows are laid out [row, nkv, hd] with row-major tokens — rope_neox expects
639            // (n_heads, n_tokens); ctx and block ropes run as one call over ctx+b tokens.
640            let pos_all = e.htod_i32(pos)?;
641            if !norope {
642                self.rope_rows(e, &mut k, &pos_all, nkv, ctx + b)?;
643            }
644
645            // full non-causal attention: every block query sees all ctx+b keys.
646            let mut attn = e.uninit(b * nh * hd)?;
647            let scale = 1.0f32 / (hd as f32).sqrt();
648            // NAIVE SDPA for first light: fa_prefill's NON-CAUSAL arm with T != T_kv is
649            // BROKEN (attn maxdiff 0.34 vs the torch oracle; q/k inputs bit-close — no
650            // existing caller exercises that shape class, jsonl 2026-07-13). The 16 x
651            // (ctx+16) block attention is tiny; the fa arm returns behind this seam once
652            // its kernel is fixed + parity-gated.
653            if std::env::var("MEMRA_DFLASH_FA").is_ok() {
654                e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
655            } else {
656                e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
657            }
658
659            let o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
660            let mut x1 = e.uninit(b * h)?;
661            e.add(&o, &x, &mut x1, b * h)?;
662            if li == 0 {
663                if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
664                    let dump = |name: &str,
665                                t: &cudarc::driver::CudaSlice<f32>|
666                     -> Result<(), Box<dyn std::error::Error>> {
667                        let v = e.dtoh(t)?;
668                        let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
669                        std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
670                        Ok(())
671                    };
672                    dump("q", &q)?;
673                    dump("k", &k)?;
674                    dump("attn", &attn)?;
675                    dump("x1", &x1)?;
676                }
677            }
678
679            // mlp
680            let mut x1n = e.uninit(b * h)?;
681            e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
682            let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
683            let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
684            let mut act = e.uninit(b * c.n_ff)?;
685            e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
686            let down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
687            let mut x2 = e.uninit(b * h)?;
688            e.add(&down, &x1, &mut x2, b * h)?;
689            x = x2;
690            if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
691                let v = e.dtoh(&x)?;
692                let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
693                std::fs::write(format!("{dir}/memra-layer{li}_out.f32"), bytes)?;
694            }
695        }
696        let mut out = e.uninit(b * h)?;
697        e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
698        Ok(out)
699    }
700}
701
702/// Draft KV cache (round-cost fix, 2026-07-13): per-layer normed+roped ctx K and raw ctx V,
703/// append-only in committed order. Block K/V land TRANSIENTLY at [len..len+b] each round
704/// (never committed — the reference crops them identically). Kills the per-round full-ctx
705/// projection recompute (first light was O(ctx)/round -> 7 tok/s).
706pub struct DflashKv {
707    pub k: Vec<CudaSlice<f32>>, // per layer [cap + block, nkv*hd]
708    pub v: Vec<CudaSlice<f32>>,
709    pub len: usize,
710    pub cap: usize,
711}
712
713impl DflashKv {
714    pub fn new(
715        e: &Engine,
716        cfg: &DflashCfg,
717        cap: usize,
718    ) -> Result<Self, Box<dyn std::error::Error>> {
719        let rowsz = cfg.n_kv * cfg.head_dim;
720        let mut k = Vec::with_capacity(cfg.n_layer);
721        let mut v = Vec::with_capacity(cfg.n_layer);
722        for _ in 0..cfg.n_layer {
723            k.push(e.uninit((cap + cfg.block_size) * rowsz)?);
724            v.push(e.uninit((cap + cfg.block_size) * rowsz)?);
725        }
726        Ok(Self { k, v, len: 0, cap })
727    }
728}
729
730impl DflashDraft {
731    /// Ingest `t` NEW ctx-feature rows (committed order, absolute positions `pos_new`) into
732    /// the draft KV: per layer k/v projections + k head-norm + rope, appended at kv.len.
733    pub fn ingest_ctx(
734        &self,
735        e: &Engine,
736        kv: &mut DflashKv,
737        feats: &CudaSlice<f32>,
738        pos_new: &[i32],
739        t: usize,
740    ) -> Result<(), Box<dyn std::error::Error>> {
741        let c = &self.cfg;
742        let (h, nkv, hd) = (c.hidden, c.n_kv, c.head_dim);
743        assert!(kv.len + t <= kv.cap, "draft kv overflow");
744        let pos_d = e.htod_i32(pos_new)?;
745        for (li, l) in self.layers.iter().enumerate() {
746            let k0 = self.mm(e, &l.wk, feats, t, h, nkv * hd)?;
747            let v0 = self.mm(e, &l.wv, feats, t, h, nkv * hd)?;
748            let mut kn = e.uninit(t * nkv * hd)?;
749            e.rms_norm(&k0, &l.k_norm, &mut kn, hd, t * nkv, c.eps)?;
750            self.rope_rows(e, &mut kn, &pos_d, nkv, t)?;
751            e.copy_into(&mut kv.k[li], kv.len * nkv * hd, &kn, t * nkv * hd)?;
752            e.copy_into(&mut kv.v[li], kv.len * nkv * hd, &v0, t * nkv * hd)?;
753        }
754        kv.len += t;
755        Ok(())
756    }
757
758    /// Block forward over the CACHED ctx KV: only the 16 block rows are projected per layer;
759    /// block K/V land transiently at kv[len..len+b]. Bit-class-identical to forward_block
760    /// (same kernels, same per-row programs; ONLY the ctx K/V recompute is cached).
761    pub fn forward_round(
762        &self,
763        e: &Engine,
764        kv: &mut DflashKv,
765        noise_emb: &CudaSlice<f32>,
766        pos_block: &[i32],
767    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
768        let c = &self.cfg;
769        let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
770        let b = c.block_size;
771        assert_eq!(pos_block.len(), b);
772        let ctx = kv.len;
773        let pos_blk = e.htod_i32(pos_block)?;
774        let mut x = e.clone_dtod(noise_emb)?;
775        for (li, l) in self.layers.iter().enumerate() {
776            let mut xn = e.uninit(b * h)?;
777            e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
778            let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
779            let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
780            let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
781            let mut q = e.uninit(b * nh * hd)?;
782            let mut kb = e.uninit(b * nkv * hd)?;
783            e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
784            e.rms_norm(&k0b, &l.k_norm, &mut kb, hd, b * nkv, c.eps)?;
785            self.rope_rows(e, &mut q, &pos_blk, nh, b)?;
786            self.rope_rows(e, &mut kb, &pos_blk, nkv, b)?;
787            e.copy_into(&mut kv.k[li], ctx * nkv * hd, &kb, b * nkv * hd)?;
788            e.copy_into(&mut kv.v[li], ctx * nkv * hd, &v0b, b * nkv * hd)?;
789            let mut attn = e.uninit(b * nh * hd)?;
790            let scale = 1.0f32 / (hd as f32).sqrt();
791            if std::env::var("MEMRA_DFLASH_FA").is_ok() {
792                e.fa_prefill(
793                    &q,
794                    &kv.k[li],
795                    &kv.v[li],
796                    &mut attn,
797                    hd,
798                    nh,
799                    nkv,
800                    b,
801                    ctx + b,
802                    scale,
803                    false,
804                )?;
805            } else {
806                e.sdpa_naive(
807                    &q,
808                    &kv.k[li],
809                    &kv.v[li],
810                    &mut attn,
811                    hd,
812                    nh,
813                    nkv,
814                    b,
815                    ctx + b,
816                    scale,
817                    false,
818                )?;
819            }
820            let o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
821            let mut x1 = e.uninit(b * h)?;
822            e.add(&o, &x, &mut x1, b * h)?;
823            let mut x1n = e.uninit(b * h)?;
824            e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
825            let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
826            let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
827            let mut act = e.uninit(b * c.n_ff)?;
828            e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
829            let down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
830            let mut x2 = e.uninit(b * h)?;
831            e.add(&down, &x1, &mut x2, b * h)?;
832            x = x2;
833        }
834        let mut out = e.uninit(b * h)?;
835        e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
836        Ok(out)
837    }
838}
839
840// ================= DFlash spec round (greedy, first light) =================
841// Exact contract: identical output stream to plain greedy decode BY CONSTRUCTION — the
842// target's batched verify argmax decides every committed token; the drafter only proposes.
843// (Same verify+rewind pattern as generate_spec_gemma's eager round; t=16 verify rides the
844// straddle-split-safe fa_decode_rows.)
845impl crate::hybrid::HybridModel {
846    pub fn generate_spec_dflash(
847        &self,
848        e: &Engine,
849        draft: &DflashDraft,
850        prompt: &[u32],
851        max_new: usize,
852        eos: &[u32],
853    ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
854        use crate::cache::{Cache, DflashTapSink};
855        let n_embd = self.cfg.n_embd as usize;
856        let c = &draft.cfg;
857        assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
858        let b = c.block_size;
859        let n_taps = c.target_layer_ids.len();
860        let max_ctx = prompt.len() + max_new + b + 8;
861        // First light holds ctx <= sliding_window: the draft was trained with 4 sliding
862        // layers (window 2048) and the first-light attention is windowless full — inside
863        // the window the two are identical. The depth cell (1736 + 128) fits.
864        assert!(
865            max_ctx <= c.sliding_window,
866            "first-light dflash round is windowless — ctx cap {} exceeds the draft window {}",
867            max_ctx,
868            c.sliding_window
869        );
870        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
871
872        // ---- prime with taps armed ----
873        let tp = prompt.len();
874        cache.dflash_taps = Some(DflashTapSink {
875            layer_ids: c.target_layer_ids.clone(),
876            buf: e.uninit(tp * n_taps * n_embd)?,
877            hidden: n_embd,
878            t: tp,
879            base: 0,
880        });
881        let t_prime = std::time::Instant::now();
882        let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
883        let mut last = crate::forward::argmax(&logits) as u32;
884        // draft KV cache: ingest the prompt's ctx features once; per round only the kept
885        // rows ingest + the block projects (round cost O(block), not O(ctx)).
886        let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
887        {
888            // CHUNKED ingest (depth OOM fix): the 1736-row prompt tap buffer is ~224MB f32;
889            // running fc + 5-layer k/v projection over it in one shot stacks another
890            // ~300MB of transients on the ~21.3GB trunk peak. 256-row windows bound the
891            // transient set; identical values (row-independent ops).
892            let taps = cache.dflash_taps.take().unwrap();
893            let n_taps_h = n_taps * n_embd;
894            let mut r0 = 0usize;
895            while r0 < tp {
896                let t_c = (tp - r0).min(256);
897                let tv = e.view(&taps.buf, tp * n_taps_h);
898                let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
899                let mut chunk = e.uninit(t_c * n_taps_h)?;
900                e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
901                let f = draft.ctx_features(e, &chunk, t_c)?;
902                let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
903                draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
904                r0 += t_c;
905            }
906        }
907        let mut ctx_len = tp;
908        e.stream().synchronize()?;
909        // published prime wall (the run-spec/gemma-gate timing contract subtracts it)
910        crate::PRIME_NANOS.store(
911            t_prime.elapsed().as_nanos() as u64,
912            std::sync::atomic::Ordering::Relaxed,
913        );
914
915        // embed-scale seam (MEMRA_DFLASH_EMB_SCALE): gemma trunks scale embeddings by
916        // sqrt(n_embd) INSIDE the forward; whether the z-lab gemma4 training fed the
917        // drafter scaled or raw embed rows is not visible from the reference (qwen path
918        // uses raw embed_tokens). Acceptance arbitrates; default raw.
919        let emb_scale = if std::env::var("MEMRA_DFLASH_EMB_SCALE").as_deref() == Ok("1") {
920            (n_embd as f32).sqrt()
921        } else {
922            1.0
923        };
924
925        let mut out = Vec::with_capacity(max_new);
926        let n_vocab = self.output.out_features();
927        // VERIFY WIDTH (MEMRA_DFLASH_VERIFY_T, default 8): the drafter always drafts a full
928        // block (its trained mask pattern) but only the first vt rows go through the target
929        // verify — the t=16 verify rides the untuned b16 tier at ~32% of the byte wall
930        // (65ms/verify) while b8 rides the tuned r2 tier; with ~2.7 committed/round the
931        // deep block positions almost never survive anyway. Exactness unaffected (verify
932        // still decides every committed token).
933        let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
934            .ok()
935            .and_then(|v| v.parse().ok())
936            .unwrap_or(8)
937            .clamp(2, b);
938        // adaptive verify width (MEMRA_DFLASH_ADAPT!=0, MTP accepted+1 recipe): next round
939        // verifies one past this round's accepted run, clamped [3, cap].
940        let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
941        let mut vt = vt_cap;
942        let mut attempted = 0usize;
943        let mut accepted = 0usize;
944        // The whole round runs in the decode-exact matmul scope: the m=16 draft mms were
945        // otherwise falling into the prefill-GEMM class (770us/matmul, 17% of the depth
946        // round). Prime (before this loop) keeps the prefill GEMM path.
947        e.set_verify_exact(true);
948        'outer: while out.len() < max_new {
949            let start = cache.pos; // committed length
950            // ---- draft: block = [last, MASK x b-1] ----
951            let mut block: Vec<u32> = vec![c.mask_token_id; b];
952            block[0] = last;
953            let mut noise = e.htod(&self.embd.gather(n_embd, &block))?;
954            if emb_scale != 1.0 {
955                e.scale_inplace(&mut noise, emb_scale, b * n_embd)?;
956            }
957            if std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1") && start == cache.pos {
958                let nv = e.dtoh(&noise)?;
959                let r0: f32 = nv[..n_embd].iter().map(|x| x * x).sum::<f32>().sqrt();
960                let r1: f32 = nv[n_embd..2 * n_embd]
961                    .iter()
962                    .map(|x| x * x)
963                    .sum::<f32>()
964                    .sqrt();
965                eprintln!(
966                    "[dflash noise] |row0(last)|={r0:.3} |row1(MASK id {})|={r1:.3}",
967                    c.mask_token_id
968                );
969            }
970            let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
971            let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
972            // draft tokens = argmax(lm_head(h rows 1..b))
973            let mut rows = e.uninit((b - 1) * n_embd)?;
974            {
975                let dv = e.view(&dh, b * n_embd);
976                let tail = dv.slice(n_embd..b * n_embd);
977                e.copy_view_into(&mut rows, 0, &tail, (b - 1) * n_embd)?;
978            }
979            let mut dl = e.matmul(&self.output, &rows, b - 1)?;
980            // SEMI-AR MARKOV CHAIN (DSpark head, when present + MEMRA_DFLASH_MARKOV!=0):
981            // left-to-right, logits_k += W2(W1[prev realized token]) — the whole chain
982            // stays on-device (chain_d[0] = the pending token; argmax k writes
983            // chain_d[k+1], the k+1 bias gathers from it). Greedy mirror of the patch's
984            // _markov_semiar_sample_block.
985            let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
986            let mut chain_d = e.stream().alloc_zeros::<u32>(b)?;
987            if let (Some(mk), true) = (&draft.markov, markov_on) {
988                e.set_u32_one(&mut chain_d, last)?;
989                for k in 0..(b - 1) {
990                    let mut f = e.uninit(mk.rank)?;
991                    e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
992                    let bias = e.matmul(&mk.w2, &f, 1)?;
993                    e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
994                    e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
995                }
996            } else {
997                for i in 0..(b - 1) {
998                    e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
999                }
1000            }
1001            let chain = e.dtoh_u32(&chain_d)?;
1002            let dtoks = &chain[1..];
1003            for (i, &dt) in dtoks.iter().enumerate() {
1004                block[i + 1] = dt;
1005            }
1006            let dbg = std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1");
1007
1008            // ---- verify: one t=vt target forward with taps armed ----
1009            let vblock = &block[..vt];
1010            cache.dflash_taps = Some(DflashTapSink {
1011                layer_ids: c.target_layer_ids.clone(),
1012                buf: e.uninit(vt * n_taps * n_embd)?,
1013                hidden: n_embd,
1014                t: vt,
1015                base: 0,
1016            });
1017            let (vam, _vh) = self.gemma4_decode_step_t_am(e, vblock, start, &mut cache)?;
1018            let taps = cache.dflash_taps.take().unwrap();
1019            if dbg {
1020                eprintln!(
1021                    "[dflash r] start={start} last={last}\n  draft={:?}\n  vam  ={:?}",
1022                    &block[1..],
1023                    &vam
1024                );
1025            }
1026
1027            // ---- accept ----
1028            let mut m = 0usize;
1029            while m < vt - 1 && block[m + 1] as usize == vam[m] as usize {
1030                m += 1;
1031            }
1032            attempted += vt - 1;
1033            accepted += m;
1034            out.push(last);
1035            if eos.contains(&last) {
1036                break 'outer;
1037            }
1038            for &dt in &block[1..=m] {
1039                out.push(dt);
1040                if eos.contains(&dt) {
1041                    break 'outer;
1042                }
1043                if out.len() >= max_new {
1044                    break 'outer;
1045                }
1046            }
1047            let next = vam[m] as u32;
1048
1049            // ---- commit/rollback: keep m+1 of the b appended rows ----
1050            let keep = m + 1;
1051            for kvl in cache.kv.iter_mut().flatten() {
1052                kvl.len -= vt - keep;
1053                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
1054            }
1055            cache.pos -= vt - keep;
1056
1057            // ---- ingest the kept rows' ctx features into the draft KV ----
1058            {
1059                let tv = e.view(&taps.buf, vt * n_taps * n_embd);
1060                let keep_view = tv.slice(0..keep * n_taps * n_embd);
1061                let mut kept = e.uninit(keep * n_taps * n_embd)?;
1062                e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
1063                let f = draft.ctx_features(e, &kept, keep)?;
1064                let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
1065                draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
1066                ctx_len += keep;
1067            }
1068            last = next;
1069            if adapt {
1070                vt = (m + 2).clamp(3, vt_cap);
1071            }
1072        }
1073        e.set_verify_exact(false);
1074        if std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1") {
1075            eprintln!(
1076                "[dflash] acceptance {accepted}/{attempted} = {:.3}",
1077                accepted as f64 / attempted.max(1) as f64
1078            );
1079        }
1080        Ok(out)
1081    }
1082}
1083
1084// ================= DSpark spec round, QWEN-HYBRID target (lane/dspark-q38-recover) =====
1085// The q38 twin of generate_spec_dflash. Same drafter machinery (rounds, markov chain,
1086// draft KV, adaptive verify width); the TARGET side swaps gemma4's dense verify for the
1087// qwen serving-class verify funnel (dspark_verify_t_am) + snapshot/rollback, because the
1088// hybrid GDN conv/ssm state mutates in place — dense KV truncation cannot roll it back.
1089// Exactness contract unchanged: identical stream to plain greedy BY CONSTRUCTION (the
1090// target's verify argmax decides every committed token).
1091impl crate::hybrid::HybridModel {
1092    pub fn generate_spec_dspark(
1093        &self,
1094        e: &Engine,
1095        draft: &DflashDraft,
1096        prompt: &[u32],
1097        max_new: usize,
1098        eos: &[u32],
1099    ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
1100        use crate::cache::{Cache, DflashTapSink};
1101        assert!(
1102            self.cfg.gemma4.is_none(),
1103            "gemma4 targets use generate_spec_dflash; this is the qwen-hybrid arm"
1104        );
1105        let n_embd = self.cfg.n_embd as usize;
1106        let c = &draft.cfg;
1107        assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
1108        let b = c.block_size;
1109        let n_taps = c.target_layer_ids.len();
1110        let max_ctx = prompt.len() + max_new + b + 8;
1111        assert!(
1112            max_ctx <= c.sliding_window,
1113            "dspark round is windowless — ctx cap {} exceeds the draft window {}",
1114            max_ctx,
1115            c.sliding_window
1116        );
1117        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
1118
1119        // ---- prime with taps armed (chunked prime writes at chunk offsets via sink.base) ----
1120        let tp = prompt.len();
1121        cache.dflash_taps = Some(DflashTapSink {
1122            layer_ids: c.target_layer_ids.clone(),
1123            buf: e.uninit(tp * n_taps * n_embd)?,
1124            hidden: n_embd,
1125            t: tp,
1126            base: 0,
1127        });
1128        let t_prime = std::time::Instant::now();
1129        let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
1130        let mut last = crate::forward::argmax(&logits) as u32;
1131        let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
1132        {
1133            let taps = cache.dflash_taps.take().unwrap();
1134            let n_taps_h = n_taps * n_embd;
1135            let mut r0 = 0usize;
1136            while r0 < tp {
1137                let t_c = (tp - r0).min(256);
1138                let tv = e.view(&taps.buf, tp * n_taps_h);
1139                let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
1140                let mut chunk = e.uninit(t_c * n_taps_h)?;
1141                e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
1142                let f = draft.ctx_features(e, &chunk, t_c)?;
1143                let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
1144                draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
1145                r0 += t_c;
1146            }
1147        }
1148        let mut ctx_len = tp;
1149        e.stream().synchronize()?;
1150        crate::PRIME_NANOS.store(
1151            t_prime.elapsed().as_nanos() as u64,
1152            std::sync::atomic::Ordering::Relaxed,
1153        );
1154
1155        let mut out = Vec::with_capacity(max_new);
1156        let n_vocab = self.output.out_features();
1157        let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
1158            .ok()
1159            .and_then(|v| v.parse().ok())
1160            .unwrap_or(b)
1161            .clamp(2, b);
1162        let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
1163        let mut vt = vt_cap;
1164        let mut attempted = 0usize;
1165        let mut accepted = 0usize;
1166        // per-phase economics counters (ns) — the verify-toll dataset
1167        let (mut ns_draft, mut ns_snap, mut ns_verify, mut ns_roll, mut ns_ingest) =
1168            (0u64, 0u64, 0u64, 0u64, 0u64);
1169        let mut rounds = 0usize;
1170        let stats = std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1");
1171        let clock = |on: bool, e: &Engine| -> std::time::Instant {
1172            if on {
1173                let _ = e.stream().synchronize();
1174            }
1175            std::time::Instant::now()
1176        };
1177        'outer: while out.len() < max_new {
1178            rounds += 1;
1179            let start = cache.pos; // committed length
1180            // ---- draft: block = [last, MASK x b-1] (decode-exact class for the m=b mms) ----
1181            let t0 = clock(stats, e);
1182            e.set_verify_exact(true);
1183            let mut block: Vec<u32> = vec![c.mask_token_id; b];
1184            block[0] = last;
1185            let noise = e.htod(&self.embd.gather(n_embd, &block))?;
1186            let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
1187            let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
1188            let mut rows = e.uninit((b - 1) * n_embd)?;
1189            {
1190                let dv = e.view(&dh, b * n_embd);
1191                let tail = dv.slice(n_embd..b * n_embd);
1192                e.copy_view_into(&mut rows, 0, &tail, (b - 1) * n_embd)?;
1193            }
1194            let mut dl = e.matmul(&self.output, &rows, b - 1)?;
1195            let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
1196            let mut chain_d = e.stream().alloc_zeros::<u32>(b)?;
1197            if let (Some(mk), true) = (&draft.markov, markov_on) {
1198                e.set_u32_one(&mut chain_d, last)?;
1199                for k in 0..(b - 1) {
1200                    let mut f = e.uninit(mk.rank)?;
1201                    e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
1202                    let bias = e.matmul(&mk.w2, &f, 1)?;
1203                    e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
1204                    e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
1205                }
1206            } else {
1207                for i in 0..(b - 1) {
1208                    e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
1209                }
1210            }
1211            e.set_verify_exact(false);
1212            let chain = e.dtoh_u32(&chain_d)?;
1213            for (i, &dt) in chain[1..].iter().enumerate() {
1214                block[i + 1] = dt;
1215            }
1216            ns_draft += clock(stats, e).duration_since(t0).as_nanos() as u64;
1217
1218            // ---- snapshot (GDN conv/ssm state + KV lens), then verify t=vt ----
1219            let t1 = std::time::Instant::now();
1220            let snap = cache.snapshot(e)?;
1221            ns_snap += clock(stats, e).duration_since(t1).as_nanos() as u64;
1222            let t2 = std::time::Instant::now();
1223            let vblock = &block[..vt];
1224            cache.dflash_taps = Some(DflashTapSink {
1225                layer_ids: c.target_layer_ids.clone(),
1226                buf: e.uninit(vt * n_taps * n_embd)?,
1227                hidden: n_embd,
1228                t: vt,
1229                base: 0,
1230            });
1231            // MEMRA_DSPARK_CKPT (default 1): verify with the MTP column-stash armed so a
1232            // partial accept restores state directly. =0 keeps the snapshot+replay arm
1233            // (the oracle the stash arm is gated against — MEMRA_DSPARK_CKPT_GATE=1 runs
1234            // BOTH per partial round and byte-compares the resulting cache state).
1235            let ckpt_on = std::env::var("MEMRA_DSPARK_CKPT").as_deref() != Ok("0");
1236            let ckpt_gate = std::env::var("MEMRA_DSPARK_CKPT_GATE").as_deref() == Ok("1");
1237            let (vam, vck) = if ckpt_on || ckpt_gate {
1238                let (vam, vck) = self.dspark_verify_t_am_ckpt(e, vblock, start, &mut cache)?;
1239                (vam, Some(vck))
1240            } else {
1241                (self.dspark_verify_t_am(e, vblock, start, &mut cache)?, None)
1242            };
1243            let taps = cache.dflash_taps.take().unwrap();
1244            ns_verify += clock(stats, e).duration_since(t2).as_nanos() as u64;
1245
1246            // ---- accept ----
1247            let mut m = 0usize;
1248            while m < vt - 1 && block[m + 1] == vam[m] {
1249                m += 1;
1250            }
1251            attempted += vt - 1;
1252            accepted += m;
1253            out.push(last);
1254            if eos.contains(&last) {
1255                break 'outer;
1256            }
1257            for &dt in &block[1..=m] {
1258                // budget check BEFORE the push: at real acceptance the final round often
1259                // accepts a draft at the boundary, and push-then-check emitted max_new+1
1260                // tokens (plain emits exactly max_new — the E2E gate read it as a length
1261                // divergence at index max_new with the shared prefix byte-identical).
1262                if out.len() >= max_new {
1263                    break 'outer;
1264                }
1265                out.push(dt);
1266                if eos.contains(&dt) {
1267                    break 'outer;
1268                }
1269            }
1270            let next = vam[m];
1271
1272            // ---- commit/rollback: hybrid state cannot truncate — restore + replay kept ----
1273            let keep = m + 1;
1274            let t3 = std::time::Instant::now();
1275            if keep < vt {
1276                if ckpt_gate {
1277                    // GATE ARM: stash-restore, snapshot S1; then the replay oracle, snapshot
1278                    // S2; the two cache states must match BIT-FOR-BIT (kv lens, pos, every
1279                    // conv/ssm buffer). Continue from the replay state (proven identical).
1280                    let vck = vck.as_ref().expect("gate arm always fills the ckpt");
1281                    self.dspark_commit_prefix(e, &mut cache, &snap, vck, keep)?;
1282                    // host-side state capture (NO device snapshot copies — two extra
1283                    // device snapshots per round OOM'd beside the 15GB trunk)
1284                    let capture = |cache: &Cache| -> Result<
1285                        (usize, Vec<Option<usize>>, Vec<(Vec<f32>, Vec<f32>)>),
1286                        Box<dyn std::error::Error>,
1287                    > {
1288                        let mut lens = Vec::new();
1289                        let mut states = Vec::new();
1290                        for il in 0..cache.kv.len() {
1291                            lens.push(cache.kv[il].as_ref().map(|k| k.len));
1292                            if let Some(rl) = &cache.recur[il] {
1293                                states.push((e.dtoh(&rl.conv_state)?, e.dtoh(&rl.ssm_state)?));
1294                            }
1295                        }
1296                        Ok((cache.pos, lens, states))
1297                    };
1298                    let (p1, l1, st1) = capture(&cache)?;
1299                    crate::pp::restore_cache_checkpoint(e, &self.cfg, None, &mut cache, &snap)?;
1300                    let ram = self.dspark_verify_t_am(e, &block[..keep], start, &mut cache)?;
1301                    assert_eq!(
1302                        &ram[..],
1303                        &vam[..keep],
1304                        "prefix replay must reproduce the verify argmaxes"
1305                    );
1306                    let (p2, l2, st2) = capture(&cache)?;
1307                    assert_eq!(p1, p2, "ckpt-gate: pos mismatch");
1308                    assert_eq!(l1, l2, "ckpt-gate: kv_len mismatch");
1309                    for (il, ((c1, s1v), (c2, s2v))) in st1.iter().zip(&st2).enumerate() {
1310                        let bits = |a: &[f32], b: &[f32]| {
1311                            a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
1312                        };
1313                        assert!(
1314                            bits(c1, c2),
1315                            "ckpt-gate: linear layer {il} conv state differs"
1316                        );
1317                        assert!(
1318                            bits(s1v, s2v),
1319                            "ckpt-gate: linear layer {il} ssm state differs"
1320                        );
1321                    }
1322                } else if let Some(vck) = vck.as_ref() {
1323                    // STASH ARM (default): column-state restore, no replay forward.
1324                    self.dspark_commit_prefix(e, &mut cache, &snap, vck, keep)?;
1325                } else {
1326                    // REPLAY ARM (MEMRA_DSPARK_CKPT=0): the original snapshot+replay oracle.
1327                    crate::pp::restore_cache_checkpoint(e, &self.cfg, None, &mut cache, &snap)?;
1328                    debug_assert_eq!(cache.pos, start, "rollback landed off the round start");
1329                    let ram = self.dspark_verify_t_am(e, &block[..keep], start, &mut cache)?;
1330                    debug_assert_eq!(
1331                        &ram[..],
1332                        &vam[..keep],
1333                        "prefix replay must reproduce the verify argmaxes"
1334                    );
1335                }
1336            }
1337            ns_roll += clock(stats, e).duration_since(t3).as_nanos() as u64;
1338
1339            // ---- ingest the kept rows' ctx features into the draft KV ----
1340            let t4 = std::time::Instant::now();
1341            {
1342                let tv = e.view(&taps.buf, vt * n_taps * n_embd);
1343                let keep_view = tv.slice(0..keep * n_taps * n_embd);
1344                let mut kept = e.uninit(keep * n_taps * n_embd)?;
1345                e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
1346                let f = draft.ctx_features(e, &kept, keep)?;
1347                let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
1348                draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
1349                ctx_len += keep;
1350            }
1351            ns_ingest += clock(stats, e).duration_since(t4).as_nanos() as u64;
1352            last = next;
1353            if adapt {
1354                vt = (m + 2).clamp(3, vt_cap);
1355            }
1356        }
1357        if stats {
1358            let ms = |n: u64| n as f64 / 1e6;
1359            eprintln!(
1360                "[dspark-q38] acceptance {accepted}/{attempted} = {:.3} rounds={rounds} \
1361                 draft={:.1}ms snap={:.1}ms verify={:.1}ms rollback+replay={:.1}ms ingest={:.1}ms",
1362                accepted as f64 / attempted.max(1) as f64,
1363                ms(ns_draft),
1364                ms(ns_snap),
1365                ms(ns_verify),
1366                ms(ns_roll),
1367                ms(ns_ingest)
1368            );
1369        }
1370        Ok(out)
1371    }
1372}
1373
1374// ================= DSpark SERVING session (lane/dspark-q38-recover serve route) =========
1375// Burst-scoped state for the worker's dspark spec arm — the qwen-hybrid twin of
1376// GemmaSpecSession. Holds the trunk cache + draft KV + the round loop's carry state
1377// (`last`, ctx_len, adaptive vt) so the scheduler round-robins other sessions between
1378// bursts. The round body is generate_spec_dspark's loop, hoisted; that bin arm stays the
1379// banked oracle (E2E gate), and the serve-route smoke gates this twin byte-identical to
1380// a spec-off boot over the real HTTP surface. Exactness contract unchanged: the target's
1381// verify argmax decides every committed token, so the stream equals plain greedy BY
1382// CONSTRUCTION on every accept path (ckpt stash, gate, replay).
1383pub struct DsparkSpecSession {
1384    pub cache: crate::cache::Cache,
1385    dkv: DflashKv,
1386    last: u32,
1387    ctx_len: usize,
1388    vt: usize,
1389    pub rounds: usize,
1390    max_ctx: usize,
1391    done: bool,
1392}
1393
1394impl DsparkSpecSession {
1395    pub fn cache_max_ctx(&self) -> usize {
1396        self.max_ctx
1397    }
1398    pub fn finished(&self) -> bool {
1399        self.done
1400    }
1401    pub fn pos(&self) -> usize {
1402        self.cache.pos
1403    }
1404}
1405
1406impl crate::hybrid::HybridModel {
1407    /// Turn-1 prime: trunk prefill with taps armed + chunked ctx ingest into the draft KV.
1408    /// Mirrors generate_spec_dspark's prime block exactly (chunk offsets via sink.base are
1409    /// handled inside prime_cache's tick loop; the 256-row ingest chunks match the bin arm).
1410    pub fn dspark_spec_session_new(
1411        &self,
1412        e: &Engine,
1413        draft: &DflashDraft,
1414        prompt: &[u32],
1415        ctx_cap: usize,
1416    ) -> Result<DsparkSpecSession, Box<dyn std::error::Error>> {
1417        use crate::cache::{Cache, DflashTapSink};
1418        assert!(
1419            self.cfg.gemma4.is_none(),
1420            "gemma4 targets use the assistant-drafter route; dspark is the qwen-hybrid arm"
1421        );
1422        let n_embd = self.cfg.n_embd as usize;
1423        let c = &draft.cfg;
1424        assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
1425        let b = c.block_size;
1426        let n_taps = c.target_layer_ids.len();
1427        // The dspark round is windowless: every position the session will ever hold must
1428        // fit the draft window. Clamp the session ctx to it and refuse prompts that
1429        // cannot take even one round — admission falls back to the plain path.
1430        let max_ctx = ctx_cap.min(c.sliding_window);
1431        if prompt.len() + b + 8 > max_ctx {
1432            return Err(format!(
1433                "dspark session needs {} ctx (prompt {} + block {b} + 8), cap {max_ctx}",
1434                prompt.len() + b + 8,
1435                prompt.len()
1436            )
1437            .into());
1438        }
1439        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
1440        let tp = prompt.len();
1441        cache.dflash_taps = Some(DflashTapSink {
1442            layer_ids: c.target_layer_ids.clone(),
1443            buf: e.uninit(tp * n_taps * n_embd)?,
1444            hidden: n_embd,
1445            t: tp,
1446            base: 0,
1447        });
1448        let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
1449        let last = crate::forward::argmax(&logits) as u32;
1450        let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
1451        {
1452            let taps = cache.dflash_taps.take().unwrap();
1453            let n_taps_h = n_taps * n_embd;
1454            let mut r0 = 0usize;
1455            while r0 < tp {
1456                let t_c = (tp - r0).min(256);
1457                let tv = e.view(&taps.buf, tp * n_taps_h);
1458                let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
1459                let mut chunk = e.uninit(t_c * n_taps_h)?;
1460                e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
1461                let f = draft.ctx_features(e, &chunk, t_c)?;
1462                let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
1463                draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
1464                r0 += t_c;
1465            }
1466        }
1467        e.stream().synchronize()?;
1468        let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
1469            .ok()
1470            .and_then(|v| v.parse().ok())
1471            .unwrap_or(b)
1472            .clamp(2, b);
1473        Ok(DsparkSpecSession {
1474            cache,
1475            dkv,
1476            last,
1477            ctx_len: tp,
1478            vt: vt_cap,
1479            rounds: 0,
1480            max_ctx,
1481            done: false,
1482        })
1483    }
1484
1485    /// One scheduler burst: dspark rounds until >= `burst_target` tokens are committed,
1486    /// EOS lands, or the ctx cap is reached. Returns (tokens, drafted, accepted) for this
1487    /// burst — the worker clamps the public slice (engine overshoot within a round stays
1488    /// in the session cache, exactly the gemma-burst contract).
1489    pub fn dspark_spec_session_burst(
1490        &self,
1491        e: &Engine,
1492        draft: &DflashDraft,
1493        sess: &mut DsparkSpecSession,
1494        burst_target: usize,
1495        eos: &[u32],
1496    ) -> Result<(Vec<u32>, usize, usize), Box<dyn std::error::Error>> {
1497        use crate::cache::DflashTapSink;
1498        let n_embd = self.cfg.n_embd as usize;
1499        let c = &draft.cfg;
1500        let b = c.block_size;
1501        let n_taps = c.target_layer_ids.len();
1502        let n_vocab = self.output.out_features();
1503        let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
1504            .ok()
1505            .and_then(|v| v.parse().ok())
1506            .unwrap_or(b)
1507            .clamp(2, b);
1508        let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
1509        let mut out: Vec<u32> = Vec::with_capacity(burst_target + b);
1510        let mut drafted = 0usize;
1511        let mut accepted_n = 0usize;
1512        'outer: while out.len() < burst_target && !sess.done {
1513            let start = sess.cache.pos;
1514            if start + b + 1 > sess.max_ctx {
1515                sess.done = true;
1516                break;
1517            }
1518            sess.rounds += 1;
1519            let vt = sess.vt;
1520            // ---- draft: block = [last, MASK x b-1] (identical to the bin arm) ----
1521            e.set_verify_exact(true);
1522            let mut block: Vec<u32> = vec![c.mask_token_id; b];
1523            block[0] = sess.last;
1524            let noise = e.htod(&self.embd.gather(n_embd, &block))?;
1525            let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
1526            let dh = draft.forward_round(e, &mut sess.dkv, &noise, &pos_block)?;
1527            let mut rows = e.uninit((b - 1) * n_embd)?;
1528            {
1529                let dv = e.view(&dh, b * n_embd);
1530                let tail = dv.slice(n_embd..b * n_embd);
1531                e.copy_view_into(&mut rows, 0, &tail, (b - 1) * n_embd)?;
1532            }
1533            let mut dl = e.matmul(&self.output, &rows, b - 1)?;
1534            let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
1535            let mut chain_d = e.stream().alloc_zeros::<u32>(b)?;
1536            if let (Some(mk), true) = (&draft.markov, markov_on) {
1537                e.set_u32_one(&mut chain_d, sess.last)?;
1538                for k in 0..(b - 1) {
1539                    let mut f = e.uninit(mk.rank)?;
1540                    e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
1541                    let bias = e.matmul(&mk.w2, &f, 1)?;
1542                    e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
1543                    e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
1544                }
1545            } else {
1546                for i in 0..(b - 1) {
1547                    e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
1548                }
1549            }
1550            e.set_verify_exact(false);
1551            let chain = e.dtoh_u32(&chain_d)?;
1552            for (i, &dt) in chain[1..].iter().enumerate() {
1553                block[i + 1] = dt;
1554            }
1555
1556            // ---- snapshot, then verify t=vt (ckpt stash default; oracle arms kept) ----
1557            let snap = sess.cache.snapshot(e)?;
1558            let vblock = &block[..vt];
1559            sess.cache.dflash_taps = Some(DflashTapSink {
1560                layer_ids: c.target_layer_ids.clone(),
1561                buf: e.uninit(vt * n_taps * n_embd)?,
1562                hidden: n_embd,
1563                t: vt,
1564                base: 0,
1565            });
1566            let ckpt_on = std::env::var("MEMRA_DSPARK_CKPT").as_deref() != Ok("0");
1567            let (vam, vck) = if ckpt_on {
1568                let (vam, vck) = self.dspark_verify_t_am_ckpt(e, vblock, start, &mut sess.cache)?;
1569                (vam, Some(vck))
1570            } else {
1571                (
1572                    self.dspark_verify_t_am(e, vblock, start, &mut sess.cache)?,
1573                    None,
1574                )
1575            };
1576            let taps = sess.cache.dflash_taps.take().unwrap();
1577
1578            // ---- accept ----
1579            let mut m = 0usize;
1580            while m < vt - 1 && block[m + 1] == vam[m] {
1581                m += 1;
1582            }
1583            drafted += vt - 1;
1584            accepted_n += m;
1585            out.push(sess.last);
1586            if eos.contains(&sess.last) {
1587                sess.done = true;
1588                break 'outer;
1589            }
1590            for &dt in &block[1..=m] {
1591                out.push(dt);
1592                if eos.contains(&dt) {
1593                    sess.done = true;
1594                    break 'outer;
1595                }
1596            }
1597            let next = vam[m];
1598
1599            // ---- commit/rollback (stash arm default; replay oracle kept) ----
1600            let keep = m + 1;
1601            if keep < vt {
1602                if let Some(vck) = vck.as_ref() {
1603                    self.dspark_commit_prefix(e, &mut sess.cache, &snap, vck, keep)?;
1604                } else {
1605                    crate::pp::restore_cache_checkpoint(
1606                        e,
1607                        &self.cfg,
1608                        None,
1609                        &mut sess.cache,
1610                        &snap,
1611                    )?;
1612                    debug_assert_eq!(sess.cache.pos, start, "rollback landed off the round start");
1613                    let ram = self.dspark_verify_t_am(e, &block[..keep], start, &mut sess.cache)?;
1614                    debug_assert_eq!(
1615                        &ram[..],
1616                        &vam[..keep],
1617                        "prefix replay must reproduce the verify argmaxes"
1618                    );
1619                }
1620            }
1621
1622            // ---- ingest the kept rows' ctx features into the draft KV ----
1623            {
1624                let tv = e.view(&taps.buf, vt * n_taps * n_embd);
1625                let keep_view = tv.slice(0..keep * n_taps * n_embd);
1626                let mut kept = e.uninit(keep * n_taps * n_embd)?;
1627                e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
1628                let f = draft.ctx_features(e, &kept, keep)?;
1629                let pos_k: Vec<i32> =
1630                    ((sess.ctx_len as i32)..(sess.ctx_len + keep) as i32).collect();
1631                draft.ingest_ctx(e, &mut sess.dkv, &f, &pos_k, keep)?;
1632                sess.ctx_len += keep;
1633            }
1634            sess.last = next;
1635            if adapt {
1636                sess.vt = (m + 2).clamp(3, vt_cap);
1637            }
1638        }
1639        Ok((out, drafted, accepted_n))
1640    }
1641}