1use crate::Engine;
13use crate::model::GpuTensor;
14use cudarc::driver::CudaSlice;
15
16pub struct DflashCfg {
17 pub hidden: usize, pub n_head: usize, pub n_kv: usize, pub head_dim: usize, pub n_ff: usize, pub n_layer: usize, pub eps: f32, pub rope_theta: f32, pub block_size: usize, pub mask_token_id: u32, pub target_layer_ids: Vec<usize>, pub sliding_window: usize, pub layer_sliding: Vec<bool>,
31 pub strategy_dspark: bool,
37}
38
39pub struct DflashLayer {
40 pub wq: GpuTensor, pub wk: GpuTensor, pub wv: GpuTensor, pub wo: GpuTensor, pub w_gate: GpuTensor, pub w_up: GpuTensor, pub w_down: GpuTensor, pub ln_in: CudaSlice<f32>, pub ln_post: CudaSlice<f32>, pub q_norm: CudaSlice<f32>, pub k_norm: CudaSlice<f32>, }
52
53pub struct DflashDraft {
54 pub cfg: DflashCfg,
55 pub layers: Vec<DflashLayer>,
56 pub fc: GpuTensor, pub hidden_norm: CudaSlice<f32>, pub norm: CudaSlice<f32>, pub markov: Option<MarkovHead>,
64 pub confidence: Option<ConfidenceHead>,
72 pub rope_yarn: Option<(CudaSlice<f32>, f32)>,
79}
80
81pub struct ConfidenceHead {
84 pub w: Vec<f32>, pub b: f32,
86 pub in_dim: usize,
87 pub with_markov: bool,
88}
89
90impl ConfidenceHead {
91 pub fn raw_score(&self, hidden: &[f32], emb: Option<&[f32]>) -> f32 {
97 let mut acc = self.b;
98 for (w, x) in self.w.iter().zip(hidden) {
99 acc += w * x;
100 }
101 if self.with_markov {
102 let emb = emb.expect("with_markov confidence head scored without the markov embedding");
103 debug_assert_eq!(hidden.len() + emb.len(), self.in_dim);
104 for (w, x) in self.w[hidden.len()..].iter().zip(emb) {
105 acc += w * x;
106 }
107 } else {
108 debug_assert_eq!(hidden.len(), self.in_dim);
109 }
110 acc
111 }
112}
113
114pub struct MarkovHead {
115 pub w1_bf16: CudaSlice<u8>, pub w2: GpuTensor, pub rank: usize,
118 pub vocab: usize,
119}
120
121#[derive(Clone, Copy, PartialEq, Eq, Debug)]
144pub enum DsparkHarvest {
145 Dflash,
147 Dspark,
149}
150
151impl DsparkHarvest {
152 pub fn resolve(cfg: &DflashCfg) -> Self {
161 Self::resolve_value(
162 std::env::var("MEMRA_DSPARK_HARVEST").ok().as_deref(),
163 cfg.strategy_dspark,
164 )
165 }
166
167 pub fn resolve_value(v: Option<&str>, strategy_dspark: bool) -> Self {
168 match v {
169 None | Some("") => {
170 if strategy_dspark {
171 DsparkHarvest::Dspark
172 } else {
173 DsparkHarvest::Dflash
174 }
175 }
176 set => Self::from_env_value(set),
177 }
178 }
179
180 pub fn from_env_value(v: Option<&str>) -> Self {
184 match v {
185 None | Some("") | Some("dflash") => DsparkHarvest::Dflash,
186 Some("dspark") => DsparkHarvest::Dspark,
187 Some(other) => panic!(
188 "MEMRA_DSPARK_HARVEST={other}: unknown harvest convention (dflash|dspark); \
189 refusing — a wrong convention verifies every draft slot against a position \
190 the drafter row was not trained for (DSPARK-POSTMORTEM-20260820.md)"
191 ),
192 }
193 }
194
195 pub fn name(self) -> &'static str {
197 match self {
198 DsparkHarvest::Dflash => "dflash",
199 DsparkHarvest::Dspark => "dspark",
200 }
201 }
202
203 pub fn from_name(v: &str) -> Option<Self> {
204 match v {
205 "dflash" => Some(DsparkHarvest::Dflash),
206 "dspark" => Some(DsparkHarvest::Dspark),
207 _ => None,
208 }
209 }
210
211 pub fn first_row(self) -> usize {
213 match self {
214 DsparkHarvest::Dflash => 1,
215 DsparkHarvest::Dspark => 0,
216 }
217 }
218
219 pub fn n_drafts(self, b: usize) -> usize {
221 match self {
222 DsparkHarvest::Dflash => b - 1,
223 DsparkHarvest::Dspark => b,
224 }
225 }
226
227 pub fn trained_offset_of_row(self, row: usize) -> usize {
230 match self {
231 DsparkHarvest::Dflash => row,
232 DsparkHarvest::Dspark => row + 1,
233 }
234 }
235}
236
237pub fn dspark_strategy_census(txt: &str) -> bool {
244 let arch = txt
245 .find("\"architectures\"")
246 .and_then(|i| {
247 let rest = &txt[i..];
248 let a = rest.find('[')?;
249 let b = rest.find(']')?;
250 Some(rest[a..b].contains("DSpark"))
251 })
252 .unwrap_or(false);
253 let proj = txt
254 .find("\"projector_type\"")
255 .map(|i| {
256 let rest = &txt[i..];
257 let after = rest.find(':').map(|c| &rest[c + 1..]).unwrap_or("");
258 after.trim_start().starts_with("\"dspark\"")
259 })
260 .unwrap_or(false);
261 arch || proj
262}
263
264pub fn dspark_accept_prefix(cand: &[u32], vam: &[u32], vt: usize) -> usize {
270 let mut m = 0usize;
271 while m < vt - 1 && cand[m + 1] == vam[m] {
272 m += 1;
273 }
274 m
275}
276
277#[derive(Clone, Copy, PartialEq, Debug)]
288pub enum DsparkVtPolicy {
289 Ladder,
292 Confidence { tau: f32 },
298 ConfidenceSlot { tau: f32 },
307}
308
309impl DsparkVtPolicy {
310 pub fn resolve(has_confidence_head: bool) -> Self {
322 Self::resolve_value(
323 std::env::var("MEMRA_DSPARK_VT").ok().as_deref(),
324 std::env::var("MEMRA_DSPARK_VT_TAU").ok().as_deref(),
325 std::env::var("MEMRA_DFLASH_ADAPT").ok().as_deref(),
326 has_confidence_head,
327 )
328 }
329
330 pub fn resolve_value(
331 vt: Option<&str>,
332 tau: Option<&str>,
333 adapt: Option<&str>,
334 has_confidence_head: bool,
335 ) -> Self {
336 match vt {
337 None | Some("") => {
338 if adapt == Some("0") || !has_confidence_head {
339 DsparkVtPolicy::Ladder
340 } else {
341 Self::from_env_value(Some("confidence-slot"), tau, adapt)
344 }
345 }
346 set => Self::from_env_value(set, tau, adapt),
347 }
348 }
349
350 pub fn from_env_value(vt: Option<&str>, tau: Option<&str>, adapt: Option<&str>) -> Self {
354 match vt {
355 None | Some("") | Some("ladder") => DsparkVtPolicy::Ladder,
356 Some(mode @ ("confidence" | "confidence-slot")) => {
357 if adapt == Some("0") {
358 panic!(
359 "MEMRA_DSPARK_VT={mode} together with MEMRA_DFLASH_ADAPT=0 is \
360 contradictory (a pinned fixed window vs a per-round confidence \
361 window); unset one — refuse-on-ambiguity"
362 );
363 }
364 let tau = tau
365 .map(|t| {
366 t.parse::<f32>()
367 .unwrap_or_else(|_| panic!("MEMRA_DSPARK_VT_TAU={t}: not a float"))
368 })
369 .unwrap_or(0.5);
370 assert!(
371 tau > 0.0 && tau < 1.0,
372 "MEMRA_DSPARK_VT_TAU={tau}: confidence threshold must be in (0,1)"
373 );
374 if mode == "confidence" {
375 DsparkVtPolicy::Confidence { tau }
376 } else {
377 DsparkVtPolicy::ConfidenceSlot { tau }
378 }
379 }
380 Some(other) => panic!(
381 "MEMRA_DSPARK_VT={other}: unknown verify-window policy \
382 (ladder|confidence|confidence-slot); refusing — a wrong policy \
383 silently reverts the H4 arm (DSPARK-POSTMORTEM-20260820.md)"
384 ),
385 }
386 }
387
388 pub fn is_confidence(&self) -> bool {
391 !matches!(self, DsparkVtPolicy::Ladder)
392 }
393
394 pub fn size_window(&self, raws: &[f32], vt_cap: usize) -> Option<usize> {
397 match *self {
398 DsparkVtPolicy::Ladder => None,
399 DsparkVtPolicy::Confidence { tau } => Some(dspark_confidence_vt(raws, tau, vt_cap)),
400 DsparkVtPolicy::ConfidenceSlot { tau } => {
401 Some(dspark_slot_confidence_vt(raws, tau, vt_cap))
402 }
403 }
404 }
405}
406
407pub fn dspark_confidence_vt(raws: &[f32], tau: f32, vt_cap: usize) -> usize {
415 let mut surv = 1.0f32;
416 let mut kept = 0usize;
417 for &r in raws {
418 surv *= 1.0 / (1.0 + (-r).exp());
419 if surv < tau {
420 break;
421 }
422 kept += 1;
423 }
424 (1 + kept).clamp(2, vt_cap.max(2))
425}
426
427pub fn dspark_slot_confidence_vt(raws: &[f32], tau: f32, vt_cap: usize) -> usize {
436 let mut kept = 0usize;
437 for &r in raws {
438 let p = 1.0 / (1.0 + (-r).exp());
439 if p < tau {
440 break;
441 }
442 kept += 1;
443 }
444 (1 + kept).clamp(2, vt_cap.max(2))
445}
446
447fn bf16_to_f32(bytes: &[u8]) -> Vec<f32> {
448 bytes
449 .chunks_exact(2)
450 .map(|c| f32::from_bits((u16::from_le_bytes([c[0], c[1]]) as u32) << 16))
451 .collect()
452}
453
454fn encode_q8_0(vals: &[f32]) -> Vec<u8> {
459 let mut out = Vec::with_capacity(vals.len() / 32 * 34);
460 for blk in vals.chunks_exact(32) {
461 let amax = blk.iter().fold(0f32, |a, v| a.max(v.abs()));
462 let d = amax / 127.0;
463 let id = if d > 0.0 { 1.0 / d } else { 0.0 };
464 let dh = half_from_f32(d);
465 out.extend_from_slice(&dh.to_le_bytes());
466 for &v in blk {
467 out.push(((v * id).round().clamp(-127.0, 127.0)) as i8 as u8);
468 }
469 }
470 out
471}
472
473fn encode_q4_0(vals: &[f32]) -> Vec<u8> {
478 let mut out = Vec::with_capacity(vals.len() / 32 * 18);
479 for blk in vals.chunks_exact(32) {
480 let mut amax = 0f32;
482 let mut mx = 0f32;
483 for &v in blk {
484 if v.abs() > amax {
485 amax = v.abs();
486 mx = v;
487 }
488 }
489 let d = mx / -8.0;
490 let id = if d != 0.0 { 1.0 / d } else { 0.0 };
491 out.extend_from_slice(&half_from_f32(d).to_le_bytes());
492 for j in 0..16 {
493 let x0 = (blk[j] * id + 8.5).clamp(0.0, 15.0) as u8;
494 let x1 = (blk[j + 16] * id + 8.5).clamp(0.0, 15.0) as u8;
495 out.push(x0 | (x1 << 4));
496 }
497 }
498 out
499}
500
501fn half_from_f32(v: f32) -> u16 {
502 let b = v.to_bits();
504 let sign = ((b >> 16) & 0x8000) as u16;
505 let exp = ((b >> 23) & 0xff) as i32 - 127 + 15;
506 let man = b & 0x7fffff;
507 if exp <= 0 {
508 return sign;
509 } if exp >= 31 {
511 return sign | 0x7c00;
512 } let mut h = sign | ((exp as u16) << 10) | ((man >> 13) as u16);
514 let rem = man & 0x1fff;
516 if rem > 0x1000 || (rem == 0x1000 && (h & 1) == 1) {
517 h += 1;
518 }
519 h
520}
521
522impl DflashDraft {
523 pub fn load(e: &Engine, dir: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
526 let txt = std::fs::read_to_string(dir.join("config.json"))?;
527 fn num(txt: &str, key: &str) -> Option<f64> {
528 let i = txt.find(&format!("\"{key}\""))?;
529 let rest = &txt[i..];
530 let colon = rest.find(':')?;
531 let val: String = rest[colon + 1..]
532 .trim_start()
533 .chars()
534 .take_while(|c| {
535 c.is_ascii_digit()
536 || *c == '.'
537 || *c == '-'
538 || *c == 'e'
539 || *c == 'E'
540 || *c == '+'
541 })
542 .collect();
543 val.parse().ok()
544 }
545 fn num_list(txt: &str, key: &str) -> Vec<usize> {
546 let Some(i) = txt.find(&format!("\"{key}\"")) else {
547 return Vec::new();
548 };
549 let rest = &txt[i..];
550 let (Some(a), Some(b)) = (rest.find('['), rest.find(']')) else {
551 return Vec::new();
552 };
553 rest[a + 1..b]
554 .split(',')
555 .filter_map(|s| s.trim().parse().ok())
556 .collect()
557 }
558 let g = |k: &str| num(&txt, k).unwrap_or_else(|| panic!("config missing {k}")) as usize;
559 let layer_sliding: Vec<bool> = {
561 let i = txt.find("\"layer_types\"").expect("layer_types");
562 let rest = &txt[i..];
563 let (a, b) = (rest.find('[').unwrap(), rest.find(']').unwrap());
564 rest[a + 1..b]
565 .split(',')
566 .map(|s| s.contains("sliding_attention"))
567 .collect()
568 };
569 let sliding_window = if layer_sliding.iter().any(|&s| s) {
573 g("sliding_window")
574 } else {
575 num(&txt, "sliding_window")
576 .map(|v| v as usize)
577 .unwrap_or(usize::MAX)
578 };
579 let cfg = DflashCfg {
580 hidden: g("hidden_size"),
581 n_head: g("num_attention_heads"),
582 n_kv: g("num_key_value_heads"),
583 head_dim: g("head_dim"),
584 n_ff: g("intermediate_size"),
585 n_layer: g("num_hidden_layers"),
586 eps: num(&txt, "rms_norm_eps").expect("rms_norm_eps") as f32,
587 rope_theta: num(&txt, "rope_theta").expect("rope_theta") as f32,
588 block_size: g("block_size"),
589 mask_token_id: g("mask_token_id") as u32,
590 target_layer_ids: num_list(&txt, "target_layer_ids"),
591 sliding_window,
592 layer_sliding,
593 strategy_dspark: dspark_strategy_census(&txt),
594 };
595 let st = memra_gguf::safetensors::StModel::open(&dir.join("model.safetensors"))?;
596 let up = |name: &str| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
599 let (_info, bytes) = st
600 .raw(name)
601 .ok_or_else(|| format!("missing tensor {name}"))?;
602 Ok(e.htod(&bf16_to_f32(bytes))?)
603 };
604 let prec = std::env::var("MEMRA_DFLASH_PREC").unwrap_or_else(|_| "q8".into());
608 let upw = |name: &str| -> Result<GpuTensor, Box<dyn std::error::Error>> {
609 let (info, bytes) = st
610 .raw(name)
611 .ok_or_else(|| format!("missing tensor {name}"))?;
612 let shape = info.ne(); let in_f = shape[0] as usize;
614 let is_ffn = name.contains(".mlp.");
615 let bf16 = prec == "bf16"
616 || (prec == "mixed" && !is_ffn)
617 || (prec == "fc" && name == "fc.weight");
618 if bf16 {
619 return Ok(GpuTensor::FloatBf16 {
620 data: e.upload_u8(bytes)?,
621 ne: shape.to_vec(),
622 });
623 }
624 let f32s = bf16_to_f32(bytes);
625 if prec == "q4" {
626 let q = encode_q4_0(&f32s);
627 return Ok(GpuTensor::Quant {
628 bytes: e.upload_u8(&q)?,
629 qtype: crate::QT_Q4_0,
630 row_bytes: in_f / 32 * 18,
631 ne: shape.to_vec(),
632 scale: 1.0,
633 rp: false,
634 #[cfg(memra_cutlass)]
635 cutlass: None,
636 fp8: None,
637 blk: None,
638 rp4: None,
639 f16: None,
640 });
641 }
642 let q = encode_q8_0(&f32s);
643 Ok(GpuTensor::Quant {
644 bytes: e.upload_u8(&q)?,
645 qtype: crate::QT_Q8_0,
646 row_bytes: in_f / 32 * 34,
647 ne: shape.to_vec(),
648 scale: 1.0,
649 rp: false,
650 #[cfg(memra_cutlass)]
651 cutlass: None,
652 fp8: None,
653 blk: None,
654 rp4: None,
655 f16: None,
656 })
657 };
658 let mut layers = Vec::with_capacity(cfg.n_layer);
659 for i in 0..cfg.n_layer {
660 let p = |s: &str| format!("layers.{i}.{s}");
661 layers.push(DflashLayer {
662 wq: upw(&p("self_attn.q_proj.weight"))?,
663 wk: upw(&p("self_attn.k_proj.weight"))?,
664 wv: upw(&p("self_attn.v_proj.weight"))?,
665 wo: upw(&p("self_attn.o_proj.weight"))?,
666 w_gate: upw(&p("mlp.gate_proj.weight"))?,
667 w_up: upw(&p("mlp.up_proj.weight"))?,
668 w_down: upw(&p("mlp.down_proj.weight"))?,
669 ln_in: up(&p("input_layernorm.weight"))?,
670 ln_post: up(&p("post_attention_layernorm.weight"))?,
671 q_norm: up(&p("self_attn.q_norm.weight"))?,
672 k_norm: up(&p("self_attn.k_norm.weight"))?,
673 });
674 }
675 let markov = if let Some((info, bytes)) = st.raw("markov_head.markov_w1.weight") {
676 let sh = info.ne(); let (rank, vocab) = (sh[0] as usize, sh[1] as usize);
678 let (i2, b2) = st
679 .raw("markov_head.markov_w2.weight")
680 .ok_or("markov_w2 missing beside markov_w1")?;
681 let w2 = if prec == "bf16" {
685 GpuTensor::FloatBf16 {
686 data: e.upload_u8(b2)?,
687 ne: i2.ne().to_vec(),
688 }
689 } else {
690 let w2f = bf16_to_f32(b2);
691 let w2q = encode_q8_0(&w2f);
692 GpuTensor::Quant {
693 bytes: e.upload_u8(&w2q)?,
694 qtype: crate::QT_Q8_0,
695 row_bytes: rank / 32 * 34,
696 ne: vec![rank as u64, vocab as u64],
697 scale: 1.0,
698 rp: false,
699 #[cfg(memra_cutlass)]
700 cutlass: None,
701 fp8: None,
702 blk: None,
703 rp4: None,
704 f16: None,
705 }
706 };
707 Some(MarkovHead {
708 w1_bf16: e.upload_u8(bytes)?,
709 w2,
710 rank,
711 vocab,
712 })
713 } else {
714 None
715 };
716 let confidence = if let Some((info, bytes)) = st.raw("confidence_head.proj.weight") {
717 let sh = info.ne(); let in_dim = sh[0] as usize;
719 let (_bi, bb) = st
720 .raw("confidence_head.proj.bias")
721 .ok_or("confidence bias missing beside weight")?;
722 let with_markov = markov
723 .as_ref()
724 .map(|m| in_dim == cfg.hidden + m.rank)
725 .unwrap_or(false);
726 if !with_markov && in_dim != cfg.hidden {
727 panic!(
728 "confidence_head in_dim {in_dim} matches neither hidden {} nor hidden+rank",
729 cfg.hidden
730 );
731 }
732 Some(ConfidenceHead {
733 w: bf16_to_f32(bytes),
734 b: bf16_to_f32(bb)[0],
735 in_dim,
736 with_markov,
737 })
738 } else {
739 None
740 };
741 {
746 let mut consumed: std::collections::HashSet<String> = std::collections::HashSet::new();
747 for i in 0..cfg.n_layer {
748 for s in [
749 "self_attn.q_proj.weight",
750 "self_attn.k_proj.weight",
751 "self_attn.v_proj.weight",
752 "self_attn.o_proj.weight",
753 "self_attn.q_norm.weight",
754 "self_attn.k_norm.weight",
755 "input_layernorm.weight",
756 "post_attention_layernorm.weight",
757 "mlp.gate_proj.weight",
758 "mlp.up_proj.weight",
759 "mlp.down_proj.weight",
760 ] {
761 consumed.insert(format!("layers.{i}.{s}"));
762 }
763 }
764 for s in [
765 "fc.weight",
766 "hidden_norm.weight",
767 "norm.weight",
768 "markov_head.markov_w1.weight",
769 "markov_head.markov_w2.weight",
770 "confidence_head.proj.weight",
771 "confidence_head.proj.bias",
772 ] {
773 consumed.insert(s.into());
774 }
775 let leftovers: Vec<&String> = st.names().filter(|n| !consumed.contains(*n)).collect();
776 if !leftovers.is_empty() {
777 if markov.is_some() {
778 panic!("dspark census: unrecognized tensors {leftovers:?}");
779 }
780 eprintln!("[dflash census] unmapped tensors (ignored): {leftovers:?}");
781 }
782 }
783 let rope_yarn =
786 if txt.contains("\"rope_type\": \"yarn\"") || txt.contains("\"rope_type\":\"yarn\"") {
787 let factor = num(&txt, "factor").expect("yarn factor") as f64;
788 let orig = num(&txt, "original_max_position_embeddings").expect("yarn orig");
789 let beta_fast = num(&txt, "beta_fast").expect("beta_fast");
790 let beta_slow = num(&txt, "beta_slow").expect("beta_slow");
791 let base = cfg.rope_theta as f64;
792 let d = cfg.head_dim as f64;
793 let corr =
794 |r: f64| d * (orig / (r * 2.0 * std::f64::consts::PI)).ln() / (2.0 * base.ln());
795 let low = corr(beta_fast).floor().max(0.0);
796 let high = corr(beta_slow).ceil().min(d - 1.0);
797 let half = cfg.head_dim / 2;
798 let mut ff = Vec::with_capacity(half);
799 for j in 0..half {
800 let base_inv = base.powf(-2.0 * j as f64 / d);
801 let ramp = (((j as f64) - low) / (high - low)).clamp(0.0, 1.0);
802 let ex = 1.0 - ramp; let yarn_inv = (base_inv / factor) * (1.0 - ex) + base_inv * ex;
804 ff.push((base_inv / yarn_inv) as f32);
805 }
806 let mscale = (0.1 * factor.ln() + 1.0) as f32;
807 Some((e.htod(&ff)?, mscale))
808 } else {
809 None
810 };
811 eprintln!(
815 "[dspark] harvest={} (checkpoint census strategy_dspark={}, MEMRA_DSPARK_HARVEST {})",
816 DsparkHarvest::resolve_value(
817 std::env::var("MEMRA_DSPARK_HARVEST").ok().as_deref(),
818 cfg.strategy_dspark,
819 )
820 .name(),
821 cfg.strategy_dspark,
822 match std::env::var("MEMRA_DSPARK_HARVEST") {
823 Ok(v) if !v.is_empty() => "set",
824 _ => "unset",
825 },
826 );
827 eprintln!(
828 "[dspark] verify-window={:?} (accept-rate head {}, MEMRA_DSPARK_VT {})",
829 DsparkVtPolicy::resolve(confidence.is_some()),
830 if confidence.is_some() {
831 "present"
832 } else {
833 "ABSENT -> ladder"
834 },
835 match std::env::var("MEMRA_DSPARK_VT") {
836 Ok(v) if !v.is_empty() => "set",
837 _ => "unset",
838 },
839 );
840 Ok(Self {
841 fc: upw("fc.weight")?,
842 hidden_norm: up("hidden_norm.weight")?,
843 norm: up("norm.weight")?,
844 cfg,
845 layers,
846 markov,
847 confidence,
848 rope_yarn,
849 })
850 }
851
852 fn rope_rows(
855 &self,
856 e: &Engine,
857 x: &mut CudaSlice<f32>,
858 pos_d: &CudaSlice<i32>,
859 n_heads: usize,
860 n_tokens: usize,
861 ) -> Result<(), Box<dyn std::error::Error>> {
862 let c = &self.cfg;
863 match &self.rope_yarn {
864 Some((ff, mscale)) => {
865 e.rope_neox_ff(
866 x,
867 pos_d,
868 c.head_dim,
869 c.head_dim,
870 n_heads,
871 n_tokens,
872 c.rope_theta,
873 1.0,
874 ff,
875 )?;
876 e.scale_inplace(x, *mscale, n_tokens * n_heads * c.head_dim)?;
877 }
878 None => {
879 e.rope_neox(
880 x,
881 pos_d,
882 c.head_dim,
883 c.head_dim,
884 n_heads,
885 n_tokens,
886 c.rope_theta,
887 1.0,
888 )?;
889 }
890 }
891 Ok(())
892 }
893
894 fn mm(
896 &self,
897 e: &Engine,
898 w: &GpuTensor,
899 x: &CudaSlice<f32>,
900 t: usize,
901 _in_f: usize,
902 _out_f: usize,
903 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
904 Ok(e.matmul(w, x, t)?)
905 }
906
907 pub fn ctx_features(
918 &self,
919 e: &Engine,
920 taps: &CudaSlice<f32>,
921 t: usize,
922 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
923 let c = &self.cfg;
924 let n_taps = c.target_layer_ids.len();
925 let fc_out = self.mm(e, &self.fc, taps, t, n_taps * c.hidden, c.hidden)?;
926 let mut out = e.uninit(t * c.hidden)?;
927 e.rms_norm(&fc_out, &self.hidden_norm, &mut out, c.hidden, t, c.eps)?;
928 Ok(out)
929 }
930
931 pub fn forward(
932 &self,
933 e: &Engine,
934 target_hidden: &CudaSlice<f32>,
935 noise_emb: &CudaSlice<f32>,
936 pos: &[i32],
937 ctx: usize,
938 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
939 let ctx_f = self.ctx_features(e, target_hidden, ctx)?;
940 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
941 let v = e.dtoh(&ctx_f)?;
942 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
943 std::fs::write(format!("{dir}/memra-ctx_features.f32"), bytes)?;
944 }
945 self.forward_block(e, &ctx_f, noise_emb, pos, ctx)
946 }
947
948 pub fn forward_block(
951 &self,
952 e: &Engine,
953 ctx_f: &CudaSlice<f32>,
954 noise_emb: &CudaSlice<f32>,
955 pos: &[i32],
956 ctx: usize,
957 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
958 let c = &self.cfg;
959 let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
960 let b = c.block_size;
961 assert_eq!(pos.len(), ctx + b, "pos covers ctx rows then block rows");
962
963 let pos_blk = e.htod_i32(&pos[ctx..])?;
964
965 let mut x = e.clone_dtod(noise_emb)?; for (li, l) in self.layers.iter().enumerate() {
967 let _ = li;
968 let mut xn = e.uninit(b * h)?;
971 e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
972
973 let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
975 let k0c = self.mm(e, &l.wk, ctx_f, ctx, h, nkv * hd)?;
976 let v0c = self.mm(e, &l.wv, ctx_f, ctx, h, nkv * hd)?;
977 let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
978 let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
979
980 let mut k0 = e.uninit((ctx + b) * nkv * hd)?;
983 e.copy_into(&mut k0, 0, &k0c, ctx * nkv * hd)?;
984 e.copy_into(&mut k0, ctx * nkv * hd, &k0b, b * nkv * hd)?;
985 let mut v = e.uninit((ctx + b) * nkv * hd)?;
986 e.copy_into(&mut v, 0, &v0c, ctx * nkv * hd)?;
987 e.copy_into(&mut v, ctx * nkv * hd, &v0b, b * nkv * hd)?;
988
989 if li == 0 {
990 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
991 let v = e.dtoh(&q0)?;
992 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
993 std::fs::write(format!("{dir}/memra-l0_q0.f32"), bytes)?;
994 }
995 }
996 let mut q = e.uninit(b * nh * hd)?;
997 let mut k = e.uninit((ctx + b) * nkv * hd)?;
998 e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
1000 if li == 0 {
1001 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
1002 let v = e.dtoh(&q)?;
1003 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
1004 std::fs::write(format!("{dir}/memra-l0_qn.f32"), bytes)?;
1005 }
1006 }
1007 e.rms_norm(&k0, &l.k_norm, &mut k, hd, (ctx + b) * nkv, c.eps)?;
1008
1009 let norope = std::env::var("MEMRA_DFLASH_NOROPE").is_ok();
1011 if !norope {
1012 self.rope_rows(e, &mut q, &pos_blk, nh, b)?;
1013 }
1014 if li == 0 {
1015 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
1016 let dump = |name: &str,
1017 t: &cudarc::driver::CudaSlice<f32>|
1018 -> Result<(), Box<dyn std::error::Error>> {
1019 let v = e.dtoh(t)?;
1020 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
1021 std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
1022 Ok(())
1023 };
1024 dump("xn", &xn)?;
1025 dump("q_prerope", &q)?;
1026 }
1027 }
1028 let pos_all = e.htod_i32(pos)?;
1031 if !norope {
1032 self.rope_rows(e, &mut k, &pos_all, nkv, ctx + b)?;
1033 }
1034
1035 let mut attn = e.uninit(b * nh * hd)?;
1037 let scale = 1.0f32 / (hd as f32).sqrt();
1038 if std::env::var("MEMRA_DFLASH_FA").is_ok() {
1044 e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
1045 } else {
1046 e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
1047 }
1048
1049 let o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
1050 let mut x1 = e.uninit(b * h)?;
1051 e.add(&o, &x, &mut x1, b * h)?;
1052 if li == 0 {
1053 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
1054 let dump = |name: &str,
1055 t: &cudarc::driver::CudaSlice<f32>|
1056 -> Result<(), Box<dyn std::error::Error>> {
1057 let v = e.dtoh(t)?;
1058 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
1059 std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
1060 Ok(())
1061 };
1062 dump("q", &q)?;
1063 dump("k", &k)?;
1064 dump("attn", &attn)?;
1065 dump("x1", &x1)?;
1066 }
1067 }
1068
1069 let mut x1n = e.uninit(b * h)?;
1071 e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
1072 let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
1073 let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
1074 let mut act = e.uninit(b * c.n_ff)?;
1075 e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
1076 let down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
1077 let mut x2 = e.uninit(b * h)?;
1078 e.add(&down, &x1, &mut x2, b * h)?;
1079 x = x2;
1080 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
1081 let v = e.dtoh(&x)?;
1082 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
1083 std::fs::write(format!("{dir}/memra-layer{li}_out.f32"), bytes)?;
1084 }
1085 }
1086 let mut out = e.uninit(b * h)?;
1087 e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
1088 Ok(out)
1089 }
1090}
1091
1092pub struct DflashKv {
1097 pub k: Vec<CudaSlice<f32>>, pub v: Vec<CudaSlice<f32>>,
1099 pub len: usize,
1100 pub cap: usize,
1101}
1102
1103impl DflashKv {
1104 pub fn new(
1105 e: &Engine,
1106 cfg: &DflashCfg,
1107 cap: usize,
1108 ) -> Result<Self, Box<dyn std::error::Error>> {
1109 let rowsz = cfg.n_kv * cfg.head_dim;
1110 let mut k = Vec::with_capacity(cfg.n_layer);
1111 let mut v = Vec::with_capacity(cfg.n_layer);
1112 for _ in 0..cfg.n_layer {
1113 k.push(e.uninit((cap + cfg.block_size) * rowsz)?);
1114 v.push(e.uninit((cap + cfg.block_size) * rowsz)?);
1115 }
1116 Ok(Self { k, v, len: 0, cap })
1117 }
1118}
1119
1120impl DflashDraft {
1121 pub fn ingest_ctx(
1124 &self,
1125 e: &Engine,
1126 kv: &mut DflashKv,
1127 feats: &CudaSlice<f32>,
1128 pos_new: &[i32],
1129 t: usize,
1130 ) -> Result<(), Box<dyn std::error::Error>> {
1131 let c = &self.cfg;
1132 let (h, nkv, hd) = (c.hidden, c.n_kv, c.head_dim);
1133 assert!(kv.len + t <= kv.cap, "draft kv overflow");
1134 let pos_d = e.htod_i32(pos_new)?;
1135 for (li, l) in self.layers.iter().enumerate() {
1136 let k0 = self.mm(e, &l.wk, feats, t, h, nkv * hd)?;
1137 let v0 = self.mm(e, &l.wv, feats, t, h, nkv * hd)?;
1138 let mut kn = e.uninit(t * nkv * hd)?;
1139 e.rms_norm(&k0, &l.k_norm, &mut kn, hd, t * nkv, c.eps)?;
1140 self.rope_rows(e, &mut kn, &pos_d, nkv, t)?;
1141 e.copy_into(&mut kv.k[li], kv.len * nkv * hd, &kn, t * nkv * hd)?;
1142 e.copy_into(&mut kv.v[li], kv.len * nkv * hd, &v0, t * nkv * hd)?;
1143 }
1144 kv.len += t;
1145 Ok(())
1146 }
1147
1148 pub fn forward_round(
1152 &self,
1153 e: &Engine,
1154 kv: &mut DflashKv,
1155 noise_emb: &CudaSlice<f32>,
1156 pos_block: &[i32],
1157 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1158 let c = &self.cfg;
1159 let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
1160 let b = c.block_size;
1161 assert_eq!(pos_block.len(), b);
1162 let ctx = kv.len;
1163 let pos_blk = e.htod_i32(pos_block)?;
1164 let mut x = e.clone_dtod(noise_emb)?;
1165 for (li, l) in self.layers.iter().enumerate() {
1166 let mut xn = e.uninit(b * h)?;
1167 e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
1168 let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
1169 let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
1170 let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
1171 let mut q = e.uninit(b * nh * hd)?;
1172 let mut kb = e.uninit(b * nkv * hd)?;
1173 e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
1174 e.rms_norm(&k0b, &l.k_norm, &mut kb, hd, b * nkv, c.eps)?;
1175 self.rope_rows(e, &mut q, &pos_blk, nh, b)?;
1176 self.rope_rows(e, &mut kb, &pos_blk, nkv, b)?;
1177 e.copy_into(&mut kv.k[li], ctx * nkv * hd, &kb, b * nkv * hd)?;
1178 e.copy_into(&mut kv.v[li], ctx * nkv * hd, &v0b, b * nkv * hd)?;
1179 let mut attn = e.uninit(b * nh * hd)?;
1180 let scale = 1.0f32 / (hd as f32).sqrt();
1181 if std::env::var("MEMRA_DFLASH_FA").is_ok() {
1182 e.fa_prefill(
1183 &q,
1184 &kv.k[li],
1185 &kv.v[li],
1186 &mut attn,
1187 hd,
1188 nh,
1189 nkv,
1190 b,
1191 ctx + b,
1192 scale,
1193 false,
1194 )?;
1195 } else {
1196 e.sdpa_naive(
1197 &q,
1198 &kv.k[li],
1199 &kv.v[li],
1200 &mut attn,
1201 hd,
1202 nh,
1203 nkv,
1204 b,
1205 ctx + b,
1206 scale,
1207 false,
1208 )?;
1209 }
1210 let o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
1211 let mut x1 = e.uninit(b * h)?;
1212 e.add(&o, &x, &mut x1, b * h)?;
1213 let mut x1n = e.uninit(b * h)?;
1214 e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
1215 let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
1216 let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
1217 let mut act = e.uninit(b * c.n_ff)?;
1218 e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
1219 let down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
1220 let mut x2 = e.uninit(b * h)?;
1221 e.add(&down, &x1, &mut x2, b * h)?;
1222 x = x2;
1223 }
1224 let mut out = e.uninit(b * h)?;
1225 e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
1226 Ok(out)
1227 }
1228}
1229
1230impl crate::hybrid::HybridModel {
1236 pub fn generate_spec_dflash(
1237 &self,
1238 e: &Engine,
1239 draft: &DflashDraft,
1240 prompt: &[u32],
1241 max_new: usize,
1242 eos: &[u32],
1243 ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
1244 use crate::cache::{Cache, DflashTapSink};
1245 let n_embd = self.cfg.n_embd as usize;
1246 let c = &draft.cfg;
1247 assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
1248 let b = c.block_size;
1249 let n_taps = c.target_layer_ids.len();
1250 let max_ctx = prompt.len() + max_new + b + 8;
1251 assert!(
1255 max_ctx <= c.sliding_window,
1256 "first-light dflash round is windowless — ctx cap {} exceeds the draft window {}",
1257 max_ctx,
1258 c.sliding_window
1259 );
1260 let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
1261
1262 let tp = prompt.len();
1264 cache.dflash_taps = Some(DflashTapSink {
1265 layer_ids: c.target_layer_ids.clone(),
1266 buf: e.uninit(tp * n_taps * n_embd)?,
1267 hidden: n_embd,
1268 t: tp,
1269 base: 0,
1270 });
1271 let t_prime = std::time::Instant::now();
1272 let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
1273 let mut last = crate::forward::argmax(&logits) as u32;
1274 let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
1277 {
1278 let taps = cache.dflash_taps.take().unwrap();
1283 let n_taps_h = n_taps * n_embd;
1284 let mut r0 = 0usize;
1285 while r0 < tp {
1286 let t_c = (tp - r0).min(256);
1287 let tv = e.view(&taps.buf, tp * n_taps_h);
1288 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
1289 let mut chunk = e.uninit(t_c * n_taps_h)?;
1290 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
1291 let f = draft.ctx_features(e, &chunk, t_c)?;
1292 let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
1293 draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
1294 r0 += t_c;
1295 }
1296 }
1297 let mut ctx_len = tp;
1298 e.stream().synchronize()?;
1299 crate::PRIME_NANOS.store(
1301 t_prime.elapsed().as_nanos() as u64,
1302 std::sync::atomic::Ordering::Relaxed,
1303 );
1304
1305 let emb_scale = if std::env::var("MEMRA_DFLASH_EMB_SCALE").as_deref() == Ok("1") {
1310 (n_embd as f32).sqrt()
1311 } else {
1312 1.0
1313 };
1314
1315 let mut out = Vec::with_capacity(max_new);
1316 let n_vocab = self.output.out_features();
1317 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
1324 .ok()
1325 .and_then(|v| v.parse().ok())
1326 .unwrap_or(8)
1327 .clamp(2, b);
1328 let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
1331 let mut vt = vt_cap;
1332 let mut attempted = 0usize;
1333 let mut accepted = 0usize;
1334 e.set_verify_exact(true);
1338 'outer: while out.len() < max_new {
1339 let start = cache.pos; let mut block: Vec<u32> = vec![c.mask_token_id; b];
1342 block[0] = last;
1343 let mut noise = e.htod(&self.embd.gather(n_embd, &block))?;
1344 if emb_scale != 1.0 {
1345 e.scale_inplace(&mut noise, emb_scale, b * n_embd)?;
1346 }
1347 if std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1") && start == cache.pos {
1348 let nv = e.dtoh(&noise)?;
1349 let r0: f32 = nv[..n_embd].iter().map(|x| x * x).sum::<f32>().sqrt();
1350 let r1: f32 = nv[n_embd..2 * n_embd]
1351 .iter()
1352 .map(|x| x * x)
1353 .sum::<f32>()
1354 .sqrt();
1355 eprintln!(
1356 "[dflash noise] |row0(last)|={r0:.3} |row1(MASK id {})|={r1:.3}",
1357 c.mask_token_id
1358 );
1359 }
1360 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
1361 let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
1362 let mut rows = e.uninit((b - 1) * n_embd)?;
1364 {
1365 let dv = e.view(&dh, b * n_embd);
1366 let tail = dv.slice(n_embd..b * n_embd);
1367 e.copy_view_into(&mut rows, 0, &tail, (b - 1) * n_embd)?;
1368 }
1369 let mut dl = e.matmul(&self.output, &rows, b - 1)?;
1370 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
1376 let mut chain_d = e.stream().alloc_zeros::<u32>(b)?;
1377 if let (Some(mk), true) = (&draft.markov, markov_on) {
1378 e.set_u32_one(&mut chain_d, last)?;
1379 for k in 0..(b - 1) {
1380 let mut f = e.uninit(mk.rank)?;
1381 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
1382 let bias = e.matmul(&mk.w2, &f, 1)?;
1383 e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
1384 e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
1385 }
1386 } else {
1387 for i in 0..(b - 1) {
1388 e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
1389 }
1390 }
1391 let chain = e.dtoh_u32(&chain_d)?;
1392 let dtoks = &chain[1..];
1393 for (i, &dt) in dtoks.iter().enumerate() {
1394 block[i + 1] = dt;
1395 }
1396 let dbg = std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1");
1397
1398 let vblock = &block[..vt];
1400 cache.dflash_taps = Some(DflashTapSink {
1401 layer_ids: c.target_layer_ids.clone(),
1402 buf: e.uninit(vt * n_taps * n_embd)?,
1403 hidden: n_embd,
1404 t: vt,
1405 base: 0,
1406 });
1407 let (vam, _vh) = self.gemma4_decode_step_t_am(e, vblock, start, &mut cache)?;
1408 let taps = cache.dflash_taps.take().unwrap();
1409 if dbg {
1410 eprintln!(
1411 "[dflash r] start={start} last={last}\n draft={:?}\n vam ={:?}",
1412 &block[1..],
1413 &vam
1414 );
1415 }
1416
1417 let mut m = 0usize;
1419 while m < vt - 1 && block[m + 1] as usize == vam[m] as usize {
1420 m += 1;
1421 }
1422 attempted += vt - 1;
1423 accepted += m;
1424 out.push(last);
1425 if eos.contains(&last) {
1426 break 'outer;
1427 }
1428 for &dt in &block[1..=m] {
1429 out.push(dt);
1430 if eos.contains(&dt) {
1431 break 'outer;
1432 }
1433 if out.len() >= max_new {
1434 break 'outer;
1435 }
1436 }
1437 let next = vam[m] as u32;
1438
1439 let keep = m + 1;
1441 for kvl in cache.kv.iter_mut().flatten() {
1442 kvl.len -= vt - keep;
1443 e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
1444 }
1445 cache.pos -= vt - keep;
1446
1447 {
1449 let tv = e.view(&taps.buf, vt * n_taps * n_embd);
1450 let keep_view = tv.slice(0..keep * n_taps * n_embd);
1451 let mut kept = e.uninit(keep * n_taps * n_embd)?;
1452 e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
1453 let f = draft.ctx_features(e, &kept, keep)?;
1454 let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
1455 draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
1456 ctx_len += keep;
1457 }
1458 last = next;
1459 if adapt {
1460 vt = (m + 2).clamp(3, vt_cap);
1461 }
1462 }
1463 e.set_verify_exact(false);
1464 if std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1") {
1465 eprintln!(
1466 "[dflash] acceptance {accepted}/{attempted} = {:.3}",
1467 accepted as f64 / attempted.max(1) as f64
1468 );
1469 }
1470 Ok(out)
1471 }
1472}
1473
1474pub(crate) struct DsparkSnapBatch {
1487 pub(crate) snap: crate::cache::CacheSnapshot,
1488 lin: Vec<usize>,
1490 conv_table: CudaSlice<u64>,
1492 ssm_table: CudaSlice<u64>,
1493 host_ssm: Vec<u64>,
1494 conv_words: usize,
1495 ssm_words: usize,
1496}
1497
1498impl DsparkSnapBatch {
1499 pub(crate) fn new(
1504 e: &Engine,
1505 cache: &crate::cache::Cache,
1506 ) -> Result<Option<Self>, Box<dyn std::error::Error>> {
1507 use cudarc::driver::DevicePtr;
1508 let snap = cache.snapshot(e)?;
1509 let lin: Vec<usize> = (0..cache.recur.len())
1510 .filter(|&il| cache.recur[il].is_some())
1511 .collect();
1512 if lin.is_empty() {
1513 return Ok(None);
1514 }
1515 let first = cache.recur[lin[0]].as_ref().unwrap();
1516 let (conv_words, ssm_words) = (first.conv_state.len(), first.ssm_state.len());
1517 for &il in &lin {
1518 let rl = cache.recur[il].as_ref().unwrap();
1519 if rl.conv_state.len() != conv_words || rl.ssm_state.len() != ssm_words {
1520 return Ok(None);
1521 }
1522 }
1523 let n = lin.len();
1524 let mut host_conv = vec![0u64; 2 * n];
1525 let mut host_ssm = vec![0u64; 2 * n];
1526 {
1527 let s = &e.gpu.stream();
1528 for (k, &il) in lin.iter().enumerate() {
1529 let rl = cache.recur[il].as_ref().unwrap();
1530 let (pc, _g0) = rl.conv_state.device_ptr(s);
1531 let (ps, _g1) = rl.ssm_state.device_ptr(s);
1532 let (dc, _g2) = snap.conv[il].as_ref().unwrap().device_ptr(s);
1533 let (ds, _g3) = snap.ssm[il].as_ref().unwrap().device_ptr(s);
1534 host_conv[k] = pc as u64;
1535 host_conv[n + k] = dc as u64;
1536 host_ssm[k] = ps as u64;
1537 host_ssm[n + k] = ds as u64;
1538 }
1539 }
1540 let conv_table = e.htod_u64(&host_conv)?;
1541 let ssm_table = e.htod_u64(&host_ssm)?;
1542 Ok(Some(Self {
1543 snap,
1544 lin,
1545 conv_table,
1546 ssm_table,
1547 host_ssm,
1548 conv_words,
1549 ssm_words,
1550 }))
1551 }
1552
1553 pub(crate) fn refresh(
1557 &mut self,
1558 e: &Engine,
1559 cache: &crate::cache::Cache,
1560 ) -> Result<(), Box<dyn std::error::Error>> {
1561 use cudarc::driver::DevicePtr;
1562 for il in 0..cache.kv.len() {
1563 self.snap.kv_len[il] = cache.kv[il].as_ref().map(|kvl| kvl.len);
1564 }
1565 self.snap.pos = cache.pos;
1566 let n = self.lin.len();
1567 {
1568 let s = &e.gpu.stream();
1569 for (k, &il) in self.lin.iter().enumerate() {
1570 let rl = cache.recur[il].as_ref().unwrap();
1571 let (ps, _g) = rl.ssm_state.device_ptr(s);
1572 self.host_ssm[k] = ps as u64;
1573 }
1574 }
1575 e.htod_u64_into(&self.host_ssm, &mut self.ssm_table)?;
1576 e.copy_batch_uniform_f32(&self.conv_table, n, self.conv_words)?;
1577 e.copy_batch_uniform_f32(&self.ssm_table, n, self.ssm_words)?;
1578 Ok(())
1579 }
1580}
1581
1582impl crate::hybrid::HybridModel {
1590 pub fn generate_spec_dspark(
1591 &self,
1592 e: &Engine,
1593 draft: &DflashDraft,
1594 prompt: &[u32],
1595 max_new: usize,
1596 eos: &[u32],
1597 ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
1598 use crate::cache::{Cache, DflashTapSink};
1599 assert!(
1600 self.cfg.gemma4.is_none(),
1601 "gemma4 targets use generate_spec_dflash; this is the qwen-hybrid arm"
1602 );
1603 let n_embd = self.cfg.n_embd as usize;
1604 let c = &draft.cfg;
1605 assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
1606 let b = c.block_size;
1607 let n_taps = c.target_layer_ids.len();
1608 let max_ctx = prompt.len() + max_new + b + 8;
1609 assert!(
1610 max_ctx <= c.sliding_window,
1611 "dspark round is windowless — ctx cap {} exceeds the draft window {}",
1612 max_ctx,
1613 c.sliding_window
1614 );
1615 let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
1616
1617 let tp = prompt.len();
1619 cache.dflash_taps = Some(DflashTapSink {
1620 layer_ids: c.target_layer_ids.clone(),
1621 buf: e.uninit(tp * n_taps * n_embd)?,
1622 hidden: n_embd,
1623 t: tp,
1624 base: 0,
1625 });
1626 let t_prime = std::time::Instant::now();
1627 let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
1628 let mut last = crate::forward::argmax(&logits) as u32;
1629 let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
1630 {
1631 let taps = cache.dflash_taps.take().unwrap();
1632 let n_taps_h = n_taps * n_embd;
1633 let mut r0 = 0usize;
1634 while r0 < tp {
1635 let t_c = (tp - r0).min(256);
1636 let tv = e.view(&taps.buf, tp * n_taps_h);
1637 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
1638 let mut chunk = e.uninit(t_c * n_taps_h)?;
1639 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
1640 let f = draft.ctx_features(e, &chunk, t_c)?;
1641 let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
1642 draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
1643 r0 += t_c;
1644 }
1645 }
1646 let mut ctx_len = tp;
1647 e.stream().synchronize()?;
1648 crate::PRIME_NANOS.store(
1649 t_prime.elapsed().as_nanos() as u64,
1650 std::sync::atomic::Ordering::Relaxed,
1651 );
1652
1653 let mut out = Vec::with_capacity(max_new);
1654 let n_vocab = self.output.out_features();
1655 let harvest = DsparkHarvest::resolve(&draft.cfg);
1660 let nd = harvest.n_drafts(b);
1661 let r0 = harvest.first_row();
1662 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
1663 .ok()
1664 .and_then(|v| v.parse().ok())
1665 .unwrap_or(nd + 1)
1666 .clamp(2, nd + 1);
1667 let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
1668 let vt_policy = DsparkVtPolicy::resolve(draft.confidence.is_some());
1674 if vt_policy.is_confidence() {
1675 assert!(
1676 draft.confidence.is_some(),
1677 "MEMRA_DSPARK_VT={vt_policy:?} needs a checkpoint with an accept-rate \
1678 head (confidence_head.* absent in this export)"
1679 );
1680 }
1681 let mut vt = vt_cap;
1682 let mut attempted = 0usize;
1683 let mut accepted = 0usize;
1684 let mut snapb: Option<DsparkSnapBatch> = None;
1688 let mut snapb_off = !crate::spec::state_copy_batch_on();
1689 let defer_rb = crate::spec::dspark_defer_readback_on() && !vt_policy.is_confidence();
1693 let (embd_qt, embd_rb) = self.embd.qt_and_row_bytes(n_embd);
1694 let embd_gpu = if !defer_rb || crate::spec::spec_host_embd() {
1695 None
1696 } else {
1697 Some(
1698 self.embd_gpu
1699 .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload")),
1700 )
1701 };
1702 let mut vg_guard = self.dspark_vgraphs.lock().unwrap();
1709 if vg_guard.is_none() && embd_gpu.is_some() && crate::spec::dspark_verify_graph_on() {
1710 *vg_guard = crate::spec::DsparkVerifyGraphs::new(e, &cache, vt_cap, n_embd)?;
1711 }
1712 let vgraphs: &mut Option<crate::spec::DsparkVerifyGraphs> = &mut vg_guard;
1713 let (mut ns_draft, mut ns_snap, mut ns_verify, mut ns_roll, mut ns_ingest) =
1715 (0u64, 0u64, 0u64, 0u64, 0u64);
1716 let mut rounds = 0usize;
1717 let stats = std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1");
1718 let clock = |on: bool, e: &Engine| -> std::time::Instant {
1719 if on {
1720 let _ = e.stream().synchronize();
1721 }
1722 std::time::Instant::now()
1723 };
1724 'outer: while out.len() < max_new {
1725 rounds += 1;
1726 let start = cache.pos; let t0 = clock(stats, e);
1729 e.set_verify_exact(true);
1730 let mut block: Vec<u32> = vec![c.mask_token_id; b];
1731 block[0] = last;
1732 let noise = e.htod(&self.embd.gather(n_embd, &block))?;
1733 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
1734 let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
1735 let mut rows = e.uninit(nd * n_embd)?;
1739 {
1740 let dv = e.view(&dh, b * n_embd);
1741 let src = dv.slice(r0 * n_embd..(r0 + nd) * n_embd);
1742 e.copy_view_into(&mut rows, 0, &src, nd * n_embd)?;
1743 }
1744 let mut dl = e.matmul(&self.output, &rows, nd)?;
1745 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
1746 let mut chain_d = e.stream().alloc_zeros::<u32>(nd + 1)?;
1747 let want_conf_emb = vt_policy.is_confidence()
1751 && draft.confidence.as_ref().is_some_and(|ch| ch.with_markov);
1752 let mut conf_emb: Option<CudaSlice<f32>> = match (&draft.markov, want_conf_emb) {
1753 (Some(mk), true) => Some(e.uninit(nd * mk.rank)?),
1754 (None, true) => unreachable!(
1755 "with_markov confidence head without a markov table — the loader forbids it"
1756 ),
1757 _ => None,
1758 };
1759 if let (Some(mk), true) = (&draft.markov, markov_on) {
1760 e.set_u32_one(&mut chain_d, last)?;
1761 for k in 0..nd {
1762 let mut f = e.uninit(mk.rank)?;
1763 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
1764 if let Some(ce) = conf_emb.as_mut() {
1765 let fv = e.view(&f, mk.rank);
1766 e.copy_view_into(ce, k * mk.rank, &fv, mk.rank)?;
1767 }
1768 let bias = e.matmul(&mk.w2, &f, 1)?;
1769 e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
1770 e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
1771 }
1772 } else {
1773 if want_conf_emb {
1774 e.set_u32_one(&mut chain_d, last)?;
1776 }
1777 for i in 0..nd {
1778 if let (Some(ce), Some(mk)) = (conf_emb.as_mut(), &draft.markov) {
1779 let mut f = e.uninit(mk.rank)?;
1780 e.gather_row_bf16(&mk.w1_bf16, &chain_d, i, &mut f, mk.rank)?;
1781 let fv = e.view(&f, mk.rank);
1782 e.copy_view_into(ce, i * mk.rank, &fv, mk.rank)?;
1783 }
1784 e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
1785 }
1786 }
1787 e.set_verify_exact(false);
1788 let ckpt_on = std::env::var("MEMRA_DSPARK_CKPT").as_deref() != Ok("0");
1795 let ckpt_gate = std::env::var("MEMRA_DSPARK_CKPT_GATE").as_deref() == Ok("1");
1796 let deferred = embd_gpu.is_some() && (ckpt_on || ckpt_gate);
1801 if vt_policy.is_confidence() {
1803 let ch = draft.confidence.as_ref().expect("asserted at loop entry");
1804 let (rows_h, emb_h) = match conf_emb.as_ref() {
1805 Some(ce) => {
1806 let (a, b2) = e.dtoh_pair(&rows, ce)?;
1807 (a, Some(b2))
1808 }
1809 None => (e.dtoh(&rows)?, None),
1810 };
1811 let rank = draft.markov.as_ref().map(|m| m.rank).unwrap_or(0);
1812 let mut raws = Vec::with_capacity(nd);
1813 for k in 0..nd {
1814 let hrow = &rows_h[k * n_embd..(k + 1) * n_embd];
1815 let emb = emb_h.as_ref().map(|eh| &eh[k * rank..(k + 1) * rank]);
1816 raws.push(ch.raw_score(hrow, emb));
1817 }
1818 vt = vt_policy
1819 .size_window(&raws, vt_cap)
1820 .expect("confidence policies always size the window");
1821 }
1822 let mut cand: Vec<u32> = Vec::with_capacity(nd + 1);
1828 if !deferred {
1829 let chain = e.dtoh_u32(&chain_d)?;
1830 cand.push(last);
1831 cand.extend_from_slice(&chain[1..]);
1832 }
1833 ns_draft += clock(stats, e).duration_since(t0).as_nanos() as u64;
1834
1835 let t1 = std::time::Instant::now();
1837 let mut snap_legacy: Option<crate::cache::CacheSnapshot> = None;
1840 if !snapb_off && snapb.is_none() {
1841 snapb = DsparkSnapBatch::new(e, &cache)?;
1842 snapb_off = snapb.is_none();
1843 } else if let Some(sb) = snapb.as_mut() {
1844 sb.refresh(e, &cache)?;
1845 }
1846 let snap: &crate::cache::CacheSnapshot = match snapb.as_ref() {
1847 Some(sb) => &sb.snap,
1848 None => {
1849 snap_legacy = Some(cache.snapshot(e)?);
1850 snap_legacy.as_ref().unwrap()
1851 }
1852 };
1853 let _ = &snap_legacy;
1854 ns_snap += clock(stats, e).duration_since(t1).as_nanos() as u64;
1855 let t2 = std::time::Instant::now();
1856 let tap_buf = match vgraphs.as_mut().and_then(|g| g.tap_bufs.remove(&vt)) {
1859 Some(buf) => buf,
1860 None => e.uninit(vt * n_taps * n_embd)?,
1861 };
1862 cache.dflash_taps = Some(DflashTapSink {
1863 layer_ids: c.target_layer_ids.clone(),
1864 buf: tap_buf,
1865 hidden: n_embd,
1866 t: vt,
1867 base: 0,
1868 });
1869 let verify_res = (|cache: &mut crate::cache::Cache,
1877 cand: &mut Vec<u32>,
1878 vgraphs: &mut Option<crate::spec::DsparkVerifyGraphs>|
1879 -> Result<
1880 (Vec<u32>, Option<crate::spec::DsparkVerifyCkpt>),
1881 Box<dyn std::error::Error>,
1882 > {
1883 if deferred {
1884 let g = embd_gpu.expect("deferred implies resident embed");
1889 let (am_d, vck) = self.dspark_verify_t_am_ckpt_dev(
1890 e,
1891 &chain_d,
1892 vt,
1893 start,
1894 cache,
1895 (g, embd_qt, embd_rb),
1896 vgraphs.as_mut(),
1897 )?;
1898 let ch = e.stream().clone_dtoh(&chain_d)?;
1899 let am = e.stream().clone_dtoh(&am_d)?;
1900 e.stream().synchronize()?;
1901 cand.push(last);
1902 cand.extend_from_slice(&ch[1..]);
1903 Ok((am, Some(vck)))
1904 } else if ckpt_on || ckpt_gate {
1905 let (vam, vck) = self.dspark_verify_t_am_ckpt(e, &cand[..vt], start, cache)?;
1906 Ok((vam, Some(vck)))
1907 } else {
1908 Ok((self.dspark_verify_t_am(e, &cand[..vt], start, cache)?, None))
1909 }
1910 })(&mut cache, &mut cand, vgraphs);
1911 let (vam, vck) = match verify_res {
1912 Ok(v) => v,
1913 Err(err) => {
1914 if let (Some(g), Some(taps)) = (vgraphs.as_mut(), cache.dflash_taps.take()) {
1915 g.tap_bufs.insert(vt, taps.buf);
1916 }
1917 return Err(err);
1918 }
1919 };
1920 let taps = cache.dflash_taps.take().unwrap();
1921 let tap_local: Option<CudaSlice<f32>> = match vgraphs.as_mut() {
1926 Some(g) => {
1927 g.tap_bufs.insert(vt, taps.buf);
1928 None
1929 }
1930 None => Some(taps.buf),
1931 };
1932 let tap_ref: &CudaSlice<f32> = match &tap_local {
1933 Some(b) => b,
1934 None => &vgraphs.as_ref().expect("ctx present above").tap_bufs[&vt],
1935 };
1936 ns_verify += clock(stats, e).duration_since(t2).as_nanos() as u64;
1937
1938 let m = dspark_accept_prefix(&cand, &vam, vt);
1940 attempted += vt - 1;
1941 accepted += m;
1942 out.push(last);
1943 if eos.contains(&last) {
1944 break 'outer;
1945 }
1946 for &dt in &cand[1..=m] {
1947 if out.len() >= max_new {
1952 break 'outer;
1953 }
1954 out.push(dt);
1955 if eos.contains(&dt) {
1956 break 'outer;
1957 }
1958 }
1959 let next = vam[m];
1960
1961 let keep = m + 1;
1963 let t3 = std::time::Instant::now();
1964 let slab_commit = vgraphs.as_ref().map(|g| g.round_slab).unwrap_or(false);
1967 if keep < vt {
1968 if ckpt_gate {
1969 if slab_commit {
1973 self.dspark_commit_prefix_slab(
1974 e,
1975 &mut cache,
1976 snap,
1977 vgraphs.as_ref().expect("slab_commit implies ctx"),
1978 keep,
1979 )?;
1980 } else {
1981 let vck = vck.as_ref().expect("gate arm always fills the ckpt");
1982 self.dspark_commit_prefix(e, &mut cache, snap, vck, keep)?;
1983 }
1984 let capture = |cache: &Cache| -> Result<
1987 (usize, Vec<Option<usize>>, Vec<(Vec<f32>, Vec<f32>)>),
1988 Box<dyn std::error::Error>,
1989 > {
1990 let mut lens = Vec::new();
1991 let mut states = Vec::new();
1992 for il in 0..cache.kv.len() {
1993 lens.push(cache.kv[il].as_ref().map(|k| k.len));
1994 if let Some(rl) = &cache.recur[il] {
1995 states.push((e.dtoh(&rl.conv_state)?, e.dtoh(&rl.ssm_state)?));
1996 }
1997 }
1998 Ok((cache.pos, lens, states))
1999 };
2000 let (p1, l1, st1) = capture(&cache)?;
2001 crate::pp::restore_cache_checkpoint(e, &self.cfg, None, &mut cache, snap)?;
2002 let ram = self.dspark_verify_t_am(e, &cand[..keep], start, &mut cache)?;
2003 assert_eq!(
2004 &ram[..],
2005 &vam[..keep],
2006 "prefix replay must reproduce the verify argmaxes"
2007 );
2008 let (p2, l2, st2) = capture(&cache)?;
2009 assert_eq!(p1, p2, "ckpt-gate: pos mismatch");
2010 assert_eq!(l1, l2, "ckpt-gate: kv_len mismatch");
2011 for (il, ((c1, s1v), (c2, s2v))) in st1.iter().zip(&st2).enumerate() {
2012 let bits = |a: &[f32], b: &[f32]| {
2013 a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
2014 };
2015 assert!(
2016 bits(c1, c2),
2017 "ckpt-gate: linear layer {il} conv state differs"
2018 );
2019 assert!(
2020 bits(s1v, s2v),
2021 "ckpt-gate: linear layer {il} ssm state differs"
2022 );
2023 }
2024 } else if slab_commit {
2025 self.dspark_commit_prefix_slab(
2027 e,
2028 &mut cache,
2029 snap,
2030 vgraphs.as_ref().expect("slab_commit implies ctx"),
2031 keep,
2032 )?;
2033 } else if let Some(vck) = vck.as_ref() {
2034 self.dspark_commit_prefix(e, &mut cache, snap, vck, keep)?;
2036 } else {
2037 crate::pp::restore_cache_checkpoint(e, &self.cfg, None, &mut cache, snap)?;
2039 debug_assert_eq!(cache.pos, start, "rollback landed off the round start");
2040 let ram = self.dspark_verify_t_am(e, &cand[..keep], start, &mut cache)?;
2041 debug_assert_eq!(
2042 &ram[..],
2043 &vam[..keep],
2044 "prefix replay must reproduce the verify argmaxes"
2045 );
2046 }
2047 }
2048 ns_roll += clock(stats, e).duration_since(t3).as_nanos() as u64;
2049
2050 let t4 = std::time::Instant::now();
2052 {
2053 let tv = e.view(tap_ref, vt * n_taps * n_embd);
2054 let keep_view = tv.slice(0..keep * n_taps * n_embd);
2055 let mut kept = e.uninit(keep * n_taps * n_embd)?;
2056 e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
2057 let f = draft.ctx_features(e, &kept, keep)?;
2058 let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
2059 draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
2060 ctx_len += keep;
2061 }
2062 ns_ingest += clock(stats, e).duration_since(t4).as_nanos() as u64;
2063 last = next;
2064 if !vt_policy.is_confidence() && adapt {
2067 vt = (m + 2).clamp(3, vt_cap);
2068 }
2069 }
2070 if stats {
2071 let ms = |n: u64| n as f64 / 1e6;
2072 eprintln!(
2073 "[dspark-q38] acceptance {accepted}/{attempted} = {:.3} rounds={rounds} \
2074 draft={:.1}ms snap={:.1}ms verify={:.1}ms rollback+replay={:.1}ms ingest={:.1}ms",
2075 accepted as f64 / attempted.max(1) as f64,
2076 ms(ns_draft),
2077 ms(ns_snap),
2078 ms(ns_verify),
2079 ms(ns_roll),
2080 ms(ns_ingest)
2081 );
2082 }
2083 Ok(out)
2084 }
2085}
2086
2087pub struct DsparkSpecSession {
2097 pub cache: crate::cache::Cache,
2098 dkv: DflashKv,
2099 last: u32,
2100 ctx_len: usize,
2101 vt: usize,
2102 pub rounds: usize,
2103 max_ctx: usize,
2104 done: bool,
2105 snapb: Option<DsparkSnapBatch>,
2109 snapb_off: bool,
2110}
2111
2112impl DsparkSpecSession {
2113 pub fn cache_max_ctx(&self) -> usize {
2114 self.max_ctx
2115 }
2116 pub fn finished(&self) -> bool {
2117 self.done
2118 }
2119 pub fn pos(&self) -> usize {
2120 self.cache.pos
2121 }
2122}
2123
2124impl crate::hybrid::HybridModel {
2125 pub fn dspark_spec_session_new(
2129 &self,
2130 e: &Engine,
2131 draft: &DflashDraft,
2132 prompt: &[u32],
2133 ctx_cap: usize,
2134 ) -> Result<DsparkSpecSession, Box<dyn std::error::Error>> {
2135 use crate::cache::{Cache, DflashTapSink};
2136 assert!(
2137 self.cfg.gemma4.is_none(),
2138 "gemma4 targets use the assistant-drafter route; dspark is the qwen-hybrid arm"
2139 );
2140 let n_embd = self.cfg.n_embd as usize;
2141 let c = &draft.cfg;
2142 assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
2143 let b = c.block_size;
2144 let n_taps = c.target_layer_ids.len();
2145 let max_ctx = ctx_cap.min(c.sliding_window);
2149 if prompt.len() + b + 8 > max_ctx {
2150 return Err(format!(
2151 "dspark session needs {} ctx (prompt {} + block {b} + 8), cap {max_ctx}",
2152 prompt.len() + b + 8,
2153 prompt.len()
2154 )
2155 .into());
2156 }
2157 let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
2158 let tp = prompt.len();
2159 cache.dflash_taps = Some(DflashTapSink {
2160 layer_ids: c.target_layer_ids.clone(),
2161 buf: e.uninit(tp * n_taps * n_embd)?,
2162 hidden: n_embd,
2163 t: tp,
2164 base: 0,
2165 });
2166 let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
2167 let last = crate::forward::argmax(&logits) as u32;
2168 let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
2169 {
2170 let taps = cache.dflash_taps.take().unwrap();
2171 let n_taps_h = n_taps * n_embd;
2172 let mut r0 = 0usize;
2173 while r0 < tp {
2174 let t_c = (tp - r0).min(256);
2175 let tv = e.view(&taps.buf, tp * n_taps_h);
2176 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
2177 let mut chunk = e.uninit(t_c * n_taps_h)?;
2178 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
2179 let f = draft.ctx_features(e, &chunk, t_c)?;
2180 let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
2181 draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
2182 r0 += t_c;
2183 }
2184 }
2185 e.stream().synchronize()?;
2186 let nd = DsparkHarvest::resolve(&draft.cfg).n_drafts(b);
2189 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
2190 .ok()
2191 .and_then(|v| v.parse().ok())
2192 .unwrap_or(nd + 1)
2193 .clamp(2, nd + 1);
2194 Ok(DsparkSpecSession {
2195 cache,
2196 dkv,
2197 last,
2198 ctx_len: tp,
2199 vt: vt_cap,
2200 rounds: 0,
2201 max_ctx,
2202 done: false,
2203 snapb: None,
2204 snapb_off: !crate::spec::state_copy_batch_on(),
2205 })
2206 }
2207
2208 pub fn dspark_spec_session_burst(
2213 &self,
2214 e: &Engine,
2215 draft: &DflashDraft,
2216 sess: &mut DsparkSpecSession,
2217 burst_target: usize,
2218 eos: &[u32],
2219 ) -> Result<(Vec<u32>, usize, usize), Box<dyn std::error::Error>> {
2220 use crate::cache::DflashTapSink;
2221 let n_embd = self.cfg.n_embd as usize;
2222 let c = &draft.cfg;
2223 let b = c.block_size;
2224 let n_taps = c.target_layer_ids.len();
2225 let n_vocab = self.output.out_features();
2226 let harvest = DsparkHarvest::resolve(&draft.cfg);
2229 let nd = harvest.n_drafts(b);
2230 let r0 = harvest.first_row();
2231 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
2232 .ok()
2233 .and_then(|v| v.parse().ok())
2234 .unwrap_or(nd + 1)
2235 .clamp(2, nd + 1);
2236 let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
2237 let vt_policy = DsparkVtPolicy::resolve(draft.confidence.is_some());
2241 if vt_policy.is_confidence() {
2242 assert!(
2243 draft.confidence.is_some(),
2244 "MEMRA_DSPARK_VT={vt_policy:?} needs a checkpoint with an accept-rate \
2245 head (confidence_head.* absent in this export)"
2246 );
2247 }
2248 let mut out: Vec<u32> = Vec::with_capacity(burst_target + b);
2249 let mut drafted = 0usize;
2250 let mut accepted_n = 0usize;
2251 let defer_rb = crate::spec::dspark_defer_readback_on() && !vt_policy.is_confidence();
2254 let (embd_qt, embd_rb) = self.embd.qt_and_row_bytes(n_embd);
2255 let embd_gpu = if !defer_rb || crate::spec::spec_host_embd() {
2256 None
2257 } else {
2258 Some(
2259 self.embd_gpu
2260 .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload")),
2261 )
2262 };
2263 'outer: while out.len() < burst_target && !sess.done {
2264 let start = sess.cache.pos;
2265 if start + nd + 1 > sess.max_ctx {
2266 sess.done = true;
2267 break;
2268 }
2269 sess.rounds += 1;
2270 let mut vt = sess.vt;
2271 e.set_verify_exact(true);
2273 let mut block: Vec<u32> = vec![c.mask_token_id; b];
2274 block[0] = sess.last;
2275 let noise = e.htod(&self.embd.gather(n_embd, &block))?;
2276 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
2277 let dh = draft.forward_round(e, &mut sess.dkv, &noise, &pos_block)?;
2278 let mut rows = e.uninit(nd * n_embd)?;
2280 {
2281 let dv = e.view(&dh, b * n_embd);
2282 let src = dv.slice(r0 * n_embd..(r0 + nd) * n_embd);
2283 e.copy_view_into(&mut rows, 0, &src, nd * n_embd)?;
2284 }
2285 let mut dl = e.matmul(&self.output, &rows, nd)?;
2286 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
2287 let mut chain_d = e.stream().alloc_zeros::<u32>(nd + 1)?;
2288 let want_conf_emb = vt_policy.is_confidence()
2291 && draft.confidence.as_ref().is_some_and(|ch| ch.with_markov);
2292 let mut conf_emb: Option<CudaSlice<f32>> = match (&draft.markov, want_conf_emb) {
2293 (Some(mk), true) => Some(e.uninit(nd * mk.rank)?),
2294 (None, true) => unreachable!(
2295 "with_markov confidence head without a markov table — the loader forbids it"
2296 ),
2297 _ => None,
2298 };
2299 if let (Some(mk), true) = (&draft.markov, markov_on) {
2300 e.set_u32_one(&mut chain_d, sess.last)?;
2301 for k in 0..nd {
2302 let mut f = e.uninit(mk.rank)?;
2303 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
2304 if let Some(ce) = conf_emb.as_mut() {
2305 let fv = e.view(&f, mk.rank);
2306 e.copy_view_into(ce, k * mk.rank, &fv, mk.rank)?;
2307 }
2308 let bias = e.matmul(&mk.w2, &f, 1)?;
2309 e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
2310 e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
2311 }
2312 } else {
2313 if want_conf_emb {
2314 e.set_u32_one(&mut chain_d, sess.last)?;
2316 }
2317 for i in 0..nd {
2318 if let (Some(ce), Some(mk)) = (conf_emb.as_mut(), &draft.markov) {
2319 let mut f = e.uninit(mk.rank)?;
2320 e.gather_row_bf16(&mk.w1_bf16, &chain_d, i, &mut f, mk.rank)?;
2321 let fv = e.view(&f, mk.rank);
2322 e.copy_view_into(ce, i * mk.rank, &fv, mk.rank)?;
2323 }
2324 e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
2325 }
2326 }
2327 e.set_verify_exact(false);
2328 let ckpt_on = std::env::var("MEMRA_DSPARK_CKPT").as_deref() != Ok("0");
2331 let deferred = embd_gpu.is_some() && ckpt_on;
2332 if vt_policy.is_confidence() {
2334 let ch = draft.confidence.as_ref().expect("asserted at burst entry");
2335 let (rows_h, emb_h) = match conf_emb.as_ref() {
2336 Some(ce) => {
2337 let (a, b2) = e.dtoh_pair(&rows, ce)?;
2338 (a, Some(b2))
2339 }
2340 None => (e.dtoh(&rows)?, None),
2341 };
2342 let rank = draft.markov.as_ref().map(|m| m.rank).unwrap_or(0);
2343 let mut raws = Vec::with_capacity(nd);
2344 for k in 0..nd {
2345 let hrow = &rows_h[k * n_embd..(k + 1) * n_embd];
2346 let emb = emb_h.as_ref().map(|eh| &eh[k * rank..(k + 1) * rank]);
2347 raws.push(ch.raw_score(hrow, emb));
2348 }
2349 vt = vt_policy
2350 .size_window(&raws, vt_cap)
2351 .expect("confidence policies always size the window");
2352 }
2353 let mut cand: Vec<u32> = Vec::with_capacity(nd + 1);
2354 if !deferred {
2355 let chain = e.dtoh_u32(&chain_d)?;
2356 cand.push(sess.last);
2357 cand.extend_from_slice(&chain[1..]);
2358 }
2359
2360 let mut snap_legacy: Option<crate::cache::CacheSnapshot> = None;
2364 if !sess.snapb_off && sess.snapb.is_none() {
2365 sess.snapb = DsparkSnapBatch::new(e, &sess.cache)?;
2366 sess.snapb_off = sess.snapb.is_none();
2367 } else if let Some(sb) = sess.snapb.as_mut() {
2368 sb.refresh(e, &sess.cache)?;
2369 }
2370 let snap: &crate::cache::CacheSnapshot = match sess.snapb.as_ref() {
2371 Some(sb) => &sb.snap,
2372 None => {
2373 snap_legacy = Some(sess.cache.snapshot(e)?);
2374 snap_legacy.as_ref().unwrap()
2375 }
2376 };
2377 let _ = &snap_legacy;
2378 sess.cache.dflash_taps = Some(DflashTapSink {
2379 layer_ids: c.target_layer_ids.clone(),
2380 buf: e.uninit(vt * n_taps * n_embd)?,
2381 hidden: n_embd,
2382 t: vt,
2383 base: 0,
2384 });
2385 let (vam, vck) = if deferred {
2386 let g = embd_gpu.expect("deferred implies resident embed");
2388 let (am_d, vck) = self.dspark_verify_t_am_ckpt_dev(
2391 e,
2392 &chain_d,
2393 vt,
2394 start,
2395 &mut sess.cache,
2396 (g, embd_qt, embd_rb),
2397 None,
2398 )?;
2399 let ch = e.stream().clone_dtoh(&chain_d)?;
2400 let am = e.stream().clone_dtoh(&am_d)?;
2401 e.stream().synchronize()?;
2402 cand.push(sess.last);
2403 cand.extend_from_slice(&ch[1..]);
2404 (am, Some(vck))
2405 } else if ckpt_on {
2406 let (vam, vck) =
2407 self.dspark_verify_t_am_ckpt(e, &cand[..vt], start, &mut sess.cache)?;
2408 (vam, Some(vck))
2409 } else {
2410 (
2411 self.dspark_verify_t_am(e, &cand[..vt], start, &mut sess.cache)?,
2412 None,
2413 )
2414 };
2415 let taps = sess.cache.dflash_taps.take().unwrap();
2416
2417 let m = dspark_accept_prefix(&cand, &vam, vt);
2419 drafted += vt - 1;
2420 accepted_n += m;
2421 out.push(sess.last);
2422 if eos.contains(&sess.last) {
2423 sess.done = true;
2424 break 'outer;
2425 }
2426 for &dt in &cand[1..=m] {
2427 out.push(dt);
2428 if eos.contains(&dt) {
2429 sess.done = true;
2430 break 'outer;
2431 }
2432 }
2433 let next = vam[m];
2434
2435 let keep = m + 1;
2437 if keep < vt {
2438 if let Some(vck) = vck.as_ref() {
2439 self.dspark_commit_prefix(e, &mut sess.cache, snap, vck, keep)?;
2440 } else {
2441 crate::pp::restore_cache_checkpoint(e, &self.cfg, None, &mut sess.cache, snap)?;
2442 debug_assert_eq!(sess.cache.pos, start, "rollback landed off the round start");
2443 let ram = self.dspark_verify_t_am(e, &cand[..keep], start, &mut sess.cache)?;
2444 debug_assert_eq!(
2445 &ram[..],
2446 &vam[..keep],
2447 "prefix replay must reproduce the verify argmaxes"
2448 );
2449 }
2450 }
2451
2452 {
2454 let tv = e.view(&taps.buf, vt * n_taps * n_embd);
2455 let keep_view = tv.slice(0..keep * n_taps * n_embd);
2456 let mut kept = e.uninit(keep * n_taps * n_embd)?;
2457 e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
2458 let f = draft.ctx_features(e, &kept, keep)?;
2459 let pos_k: Vec<i32> =
2460 ((sess.ctx_len as i32)..(sess.ctx_len + keep) as i32).collect();
2461 draft.ingest_ctx(e, &mut sess.dkv, &f, &pos_k, keep)?;
2462 sess.ctx_len += keep;
2463 }
2464 sess.last = next;
2465 if vt_policy.is_confidence() {
2469 sess.vt = vt;
2470 } else if adapt {
2471 sess.vt = (m + 2).clamp(3, vt_cap);
2472 }
2473 }
2474 Ok((out, drafted, accepted_n))
2475 }
2476}
2477
2478#[cfg(test)]
2486mod dspark_harvest_tests {
2487 use super::{DsparkHarvest, DsparkVtPolicy, dspark_accept_prefix, dspark_strategy_census};
2488
2489 const B: usize = 7; #[test]
2492 fn dspark_strategy_requires_shifted_harvest() {
2493 let h = DsparkHarvest::Dspark;
2494 assert_eq!(
2495 h.first_row(),
2496 0,
2497 "DSPARK-strategy checkpoints (SpecForge OnlineDSparkModel, \
2498 training.strategy=dspark — the q38 arm-a export) supervise ALL rows with \
2499 SHIFTED labels: label_offsets = arange(1, block_size+1), i.e. the ANCHOR \
2500 row's output is draft 1 (specforge/algorithms/common/\
2501 dflash_family_model.py:816; sglang v0.5.17 dspark_draft.py:248,260). \
2502 Harvesting from row 1 re-opens the DSPARK-POSTMORTEM-20260820 slot \
2503 misalignment (accept 2.9 -> 1.43)."
2504 );
2505 assert_eq!(
2506 h.n_drafts(B),
2507 B,
2508 "DSpark harvests gamma = block_size drafts per round (sglang \
2509 dspark_config.py:269, verify_num_draft_tokens = gamma+1); b-1 is the \
2510 DFlash mask-fill count and drops the best-trained slot \
2511 (DSPARK-POSTMORTEM-20260820.md §3-H1)."
2512 );
2513 for row in 0..B {
2514 assert_eq!(
2515 h.trained_offset_of_row(row),
2516 row + 1,
2517 "OnlineDSparkModel trains row k to predict anchor+k+1 \
2518 (dflash_family_model.py:816); a same-position (mask-fill) mapping \
2519 here verifies every slot one position early — the postmortem's \
2520 collapse."
2521 );
2522 }
2523 }
2524
2525 #[test]
2526 fn dflash_strategy_keeps_mask_fill_harvest() {
2527 let h = DsparkHarvest::Dflash;
2531 assert_eq!(h.first_row(), 1, "DFlash drafts start at mask row 1");
2532 assert_eq!(h.n_drafts(B), B - 1, "DFlash harvests block_size-1 drafts");
2533 for row in 1..B {
2534 assert_eq!(h.trained_offset_of_row(row), row);
2535 }
2536 }
2537
2538 #[test]
2539 fn every_candidate_verifies_the_position_its_row_was_trained_for() {
2540 for h in [DsparkHarvest::Dflash, DsparkHarvest::Dspark] {
2544 for i in 1..=h.n_drafts(B) {
2545 let row = h.first_row() + i - 1;
2546 assert_eq!(
2547 h.trained_offset_of_row(row),
2548 i,
2549 "{h:?}: candidate {i} rides row {row}, which is trained for \
2550 offset {} — harvest misaligned",
2551 h.trained_offset_of_row(row)
2552 );
2553 }
2554 }
2555 }
2556
2557 #[test]
2558 fn env_seam_parses_and_refuses() {
2559 assert_eq!(
2560 DsparkHarvest::from_env_value(None),
2561 DsparkHarvest::Dflash,
2562 "the ENV-ONLY parser keeps the historical arm; the ratified strategy-keyed \
2563 default lives in resolve_value (checkpoint census), not here"
2564 );
2565 assert_eq!(
2566 DsparkHarvest::from_env_value(Some("dspark")),
2567 DsparkHarvest::Dspark
2568 );
2569 assert_eq!(
2570 DsparkHarvest::from_env_value(Some("dflash")),
2571 DsparkHarvest::Dflash
2572 );
2573 assert!(
2574 std::panic::catch_unwind(|| DsparkHarvest::from_env_value(Some("shifted"))).is_err(),
2575 "unknown harvest values must REFUSE, not default"
2576 );
2577 assert_eq!(
2578 DsparkHarvest::from_name("dspark"),
2579 Some(DsparkHarvest::Dspark)
2580 );
2581 assert_eq!(
2582 DsparkHarvest::from_name("dflash"),
2583 Some(DsparkHarvest::Dflash)
2584 );
2585 assert_eq!(DsparkHarvest::from_name("mask-fill"), None);
2586 }
2587
2588 #[test]
2591 fn ratified_default_harvest_is_strategy_keyed() {
2592 assert_eq!(
2595 DsparkHarvest::resolve_value(None, true),
2596 DsparkHarvest::Dspark,
2597 "owner-ratified 2026-08-20: unset env defaults a DSPARK-strategy \
2598 checkpoint to the shifted harvest (DSPARK-POSTMORTEM-20260820.md B1)"
2599 );
2600 assert_eq!(
2602 DsparkHarvest::resolve_value(None, false),
2603 DsparkHarvest::Dflash
2604 );
2605 assert_eq!(
2606 DsparkHarvest::resolve_value(Some(""), false),
2607 DsparkHarvest::Dflash
2608 );
2609 assert_eq!(
2611 DsparkHarvest::resolve_value(Some("dflash"), true),
2612 DsparkHarvest::Dflash
2613 );
2614 assert_eq!(
2615 DsparkHarvest::resolve_value(Some("dspark"), false),
2616 DsparkHarvest::Dspark
2617 );
2618 assert!(
2620 std::panic::catch_unwind(|| DsparkHarvest::resolve_value(Some("shifted"), true))
2621 .is_err()
2622 );
2623 }
2624
2625 #[test]
2626 fn strategy_census_reads_the_checkpoint_not_the_env() {
2627 let q38 = r#"{"architectures": ["Qwen3DSparkModel"], "block_size": 7,
2629 "dflash_config": {"projector_type": "dspark", "markov_rank": 256}}"#;
2630 assert!(dspark_strategy_census(q38));
2631 assert!(dspark_strategy_census(
2633 r#"{"architectures": ["Qwen3DSparkModel"]}"#
2634 ));
2635 assert!(dspark_strategy_census(
2636 r#"{"dflash_config": {"projector_type": "dspark"}}"#
2637 ));
2638 let dflash = r#"{"architectures": ["Qwen3DFlashModel"],
2640 "dflash_config": {"attention_mode": "gqa"}}"#;
2641 assert!(!dspark_strategy_census(dflash));
2642 assert!(!dspark_strategy_census("{}"));
2643 }
2644
2645 #[test]
2646 fn ratified_default_vt_is_confidence_slot_tau_half() {
2647 assert_eq!(
2651 DsparkVtPolicy::resolve_value(None, None, None, true),
2652 DsparkVtPolicy::ConfidenceSlot { tau: 0.5 },
2653 "owner-ratified 2026-08-20: unset MEMRA_DSPARK_VT defaults to \
2654 confidence-slot tau=.5 on a head-carrying checkpoint (H4 cells 2-3)"
2655 );
2656 assert_eq!(
2658 DsparkVtPolicy::resolve_value(None, Some("0.35"), None, true),
2659 DsparkVtPolicy::ConfidenceSlot { tau: 0.35 }
2660 );
2661 assert!(
2662 std::panic::catch_unwind(|| DsparkVtPolicy::resolve_value(
2663 None,
2664 Some("nan-ish"),
2665 None,
2666 true
2667 ))
2668 .is_err()
2669 );
2670 assert_eq!(
2672 DsparkVtPolicy::resolve_value(None, None, None, false),
2673 DsparkVtPolicy::Ladder
2674 );
2675 assert_eq!(
2677 DsparkVtPolicy::resolve_value(None, None, Some("0"), true),
2678 DsparkVtPolicy::Ladder
2679 );
2680 assert_eq!(
2682 DsparkVtPolicy::resolve_value(Some("ladder"), None, None, true),
2683 DsparkVtPolicy::Ladder
2684 );
2685 assert_eq!(
2686 DsparkVtPolicy::resolve_value(Some("confidence"), Some("0.35"), None, true),
2687 DsparkVtPolicy::Confidence { tau: 0.35 }
2688 );
2689 assert!(
2691 std::panic::catch_unwind(|| DsparkVtPolicy::resolve_value(
2692 Some("confidence-slot"),
2693 None,
2694 Some("0"),
2695 true
2696 ))
2697 .is_err()
2698 );
2699 }
2700
2701 #[test]
2707 fn dspark_trained_rows_through_mask_fill_harvest_accept_nothing() {
2708 const BASE: u32 = 1000;
2709 let anchor: u32 = BASE; let vam: Vec<u32> = (1..=B as u32 + 1).map(|j| BASE + j).collect();
2712 let dspark_trained_row_argmax =
2714 |r: usize| BASE + DsparkHarvest::Dspark.trained_offset_of_row(r) as u32;
2715
2716 let h = DsparkHarvest::Dspark;
2718 let mut cand = vec![anchor];
2719 for i in 1..=h.n_drafts(B) {
2720 cand.push(dspark_trained_row_argmax(h.first_row() + i - 1));
2721 }
2722 let vt = h.n_drafts(B) + 1;
2723 assert_eq!(
2724 dspark_accept_prefix(&cand, &vam, vt),
2725 vt - 1,
2726 "aligned harvest must accept the full block"
2727 );
2728
2729 let wrong = DsparkHarvest::Dflash;
2732 let mut cand_wrong = vec![anchor];
2733 for i in 1..=wrong.n_drafts(B) {
2734 cand_wrong.push(dspark_trained_row_argmax(wrong.first_row() + i - 1));
2735 }
2736 let vt_wrong = wrong.n_drafts(B) + 1;
2737 assert_eq!(
2738 dspark_accept_prefix(&cand_wrong, &vam, vt_wrong),
2739 0,
2740 "mask-fill harvest of a dspark-trained drafter verifies every slot against \
2741 a position the row was not trained for (DSPARK-POSTMORTEM-20260820.md)"
2742 );
2743 }
2744}
2745
2746#[cfg(test)]
2752mod dspark_vt_tests {
2753 use super::{ConfidenceHead, DsparkVtPolicy, dspark_confidence_vt, dspark_slot_confidence_vt};
2754
2755 fn logit(p: f32) -> f32 {
2757 (p / (1.0 - p)).ln()
2758 }
2759
2760 #[test]
2761 fn confidence_vt_is_cumprod_survival_not_per_slot_threshold() {
2762 let raws: Vec<f32> = [0.9, 0.8, 0.9, 0.9, 0.9, 0.9, 0.9]
2767 .iter()
2768 .map(|&p| logit(p))
2769 .collect();
2770 assert_eq!(
2771 dspark_confidence_vt(&raws, 0.5, 8),
2772 6,
2773 "keeps 5 drafts + anchor"
2774 );
2775 assert_eq!(
2777 dspark_confidence_vt(&raws, 0.7, 8),
2778 3,
2779 "tau=0.7 keeps 2 drafts"
2780 );
2781 assert_eq!(
2782 dspark_confidence_vt(&raws, 0.05, 8),
2783 8,
2784 "tau→0 = full block"
2785 );
2786 }
2787
2788 #[test]
2789 fn slot_arm_truncates_at_first_low_confidence_slot() {
2790 let raws: Vec<f32> = [0.9, 0.8, 0.9, 0.9, 0.9, 0.9, 0.9]
2796 .iter()
2797 .map(|&p| logit(p))
2798 .collect();
2799 assert_eq!(dspark_slot_confidence_vt(&raws, 0.5, 8), 8);
2800 assert_eq!(dspark_confidence_vt(&raws, 0.5, 8), 6);
2801 let tail: Vec<f32> = [0.9, 0.9, 0.3, 0.9, 0.9, 0.9, 0.9]
2805 .iter()
2806 .map(|&p| logit(p))
2807 .collect();
2808 assert_eq!(
2809 dspark_slot_confidence_vt(&tail, 0.5, 8),
2810 3,
2811 "2 drafts + anchor"
2812 );
2813 assert_eq!(
2815 dspark_slot_confidence_vt(&tail, 0.95, 8),
2816 2,
2817 "floor at tau=0.95"
2818 );
2819 }
2820
2821 #[test]
2822 fn confidence_vt_floor_and_cap() {
2823 let cold: Vec<f32> = [0.1f32, 0.1, 0.1].iter().map(|&p| logit(p)).collect();
2826 assert_eq!(
2827 dspark_confidence_vt(&cold, 0.5, 8),
2828 2,
2829 "floor = anchor + 1 draft"
2830 );
2831 assert_eq!(
2832 dspark_slot_confidence_vt(&cold, 0.5, 8),
2833 2,
2834 "slot arm same floor"
2835 );
2836 let hot: Vec<f32> = vec![logit(0.99); 7];
2838 assert_eq!(dspark_confidence_vt(&hot, 0.5, 5), 5, "vt_cap binds");
2839 assert_eq!(
2840 dspark_confidence_vt(&hot, 0.5, 8),
2841 8,
2842 "full block when confident"
2843 );
2844 assert_eq!(
2845 dspark_slot_confidence_vt(&hot, 0.5, 5),
2846 5,
2847 "slot arm same cap"
2848 );
2849 assert_eq!(dspark_confidence_vt(&[], 0.5, 8), 2);
2851 assert_eq!(dspark_slot_confidence_vt(&[], 0.5, 8), 2);
2852 }
2853
2854 #[test]
2855 fn vt_policy_env_seam_parses_and_refuses() {
2856 assert_eq!(
2857 DsparkVtPolicy::from_env_value(None, None, None),
2858 DsparkVtPolicy::Ladder,
2859 "default stays the shipped ladder — the H4 arm is opt-in"
2860 );
2861 assert_eq!(
2862 DsparkVtPolicy::from_env_value(Some(""), None, None),
2863 DsparkVtPolicy::Ladder
2864 );
2865 assert_eq!(
2866 DsparkVtPolicy::from_env_value(Some("ladder"), None, Some("0")),
2867 DsparkVtPolicy::Ladder,
2868 "ladder + ADAPT=0 = the fixed-window arm, untouched"
2869 );
2870 assert_eq!(
2871 DsparkVtPolicy::from_env_value(Some("confidence"), None, None),
2872 DsparkVtPolicy::Confidence { tau: 0.5 },
2873 "tau defaults to 0.5 (raw sigmoid, no STS sidecar — postmortem §3-H4)"
2874 );
2875 assert_eq!(
2876 DsparkVtPolicy::from_env_value(Some("confidence"), Some("0.35"), Some("1")),
2877 DsparkVtPolicy::Confidence { tau: 0.35 }
2878 );
2879 assert_eq!(
2880 DsparkVtPolicy::from_env_value(Some("confidence-slot"), Some("0.6"), None),
2881 DsparkVtPolicy::ConfidenceSlot { tau: 0.6 },
2882 "the owner-directive per-slot arm parses with the same tau env"
2883 );
2884 assert!(
2885 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(
2886 Some("confidence-slot"),
2887 None,
2888 Some("0")
2889 ))
2890 .is_err(),
2891 "confidence-slot + MEMRA_DFLASH_ADAPT=0 must REFUSE like confidence"
2892 );
2893 assert!(
2894 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(Some("static"), None, None))
2895 .is_err(),
2896 "unknown policy values must REFUSE, not default — a typo silently \
2897 reverting the window policy invalidates an A/B"
2898 );
2899 assert!(
2900 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(
2901 Some("confidence"),
2902 None,
2903 Some("0")
2904 ))
2905 .is_err(),
2906 "confidence + MEMRA_DFLASH_ADAPT=0 is contradictory and must REFUSE"
2907 );
2908 for bad in ["0", "1", "1.5", "-0.1", "nan"] {
2909 assert!(
2910 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(
2911 Some("confidence"),
2912 Some(bad),
2913 None
2914 ))
2915 .is_err(),
2916 "tau={bad} must REFUSE (survival threshold lives in (0,1))"
2917 );
2918 }
2919 }
2920
2921 #[test]
2922 fn raw_score_matches_the_parity_gate_dot() {
2923 let ch = ConfidenceHead {
2926 w: vec![0.5, -1.0, 2.0, 0.25, -0.5],
2927 b: 0.125,
2928 in_dim: 5,
2929 with_markov: true,
2930 };
2931 let hidden = [1.0f32, 2.0, 3.0];
2932 let emb = [4.0f32, 8.0];
2933 let want = 0.125 + 0.5 * 1.0 - 1.0 * 2.0 + 2.0 * 3.0 + 0.25 * 4.0 - 0.5 * 8.0;
2934 assert_eq!(ch.raw_score(&hidden, Some(&emb)), want);
2935 let ch_plain = ConfidenceHead {
2936 w: vec![0.5, -1.0, 2.0],
2937 b: -0.25,
2938 in_dim: 3,
2939 with_markov: false,
2940 };
2941 let want_plain = -0.25 + 0.5 * 1.0 - 1.0 * 2.0 + 2.0 * 3.0;
2942 assert_eq!(ch_plain.raw_score(&hidden, None), want_plain);
2943 }
2944}