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 /// Checkpoint training-strategy census (`dspark_strategy_census` over the raw
32 /// config.json): true = a SpecForge DSPARK-strategy export (shifted labels, ALL rows
33 /// supervised — the q38 arm-a family). Keys the HARVEST DEFAULT strategy-keyed,
34 /// never env-keyed (owner-ratified 2026-08-20 after B1 confirmed H1 ×5;
35 /// DSPARK-POSTMORTEM-20260820.md B0 default-flip plan).
36 pub strategy_dspark: bool,
37 /// Explicit top-level `is_causal` from config.json (z-lab reference: an explicit
38 /// value OVERRIDES the per-layer-type default). The DFlash2 q38 checkpoint carries
39 /// `"is_causal": false` — every sliding layer is NON-causal with a symmetric
40 /// +/-2048 window (model.py `_attention_mask`). None = key absent (historical
41 /// exports; the windowless-assert arm keeps handling those byte-identically).
42 pub is_causal: Option<bool>,
43}
44
45pub struct DflashLayer {
46 pub wq: GpuTensor, // [nh*hd, hidden] row-major (out_f rows)
47 pub wk: GpuTensor, // [nkv*hd, hidden]
48 pub wv: GpuTensor, // [nkv*hd, hidden]
49 pub wo: GpuTensor, // [hidden, nh*hd]
50 pub w_gate: GpuTensor, // [n_ff, hidden]
51 pub w_up: GpuTensor, // [n_ff, hidden]
52 pub w_down: GpuTensor, // [hidden, n_ff]
53 pub ln_in: CudaSlice<f32>, // [hidden]
54 pub ln_post: CudaSlice<f32>, // [hidden]
55 pub q_norm: CudaSlice<f32>, // [hd]
56 pub k_norm: CudaSlice<f32>, // [hd]
57}
58
59pub struct DflashDraft {
60 pub cfg: DflashCfg,
61 pub layers: Vec<DflashLayer>,
62 pub fc: GpuTensor, // [hidden, n_taps*hidden]
63 pub hidden_norm: CudaSlice<f32>, // [hidden]
64 pub norm: CudaSlice<f32>, // [hidden]
65 /// DSpark semi-AR markov head (present in the repo-root checkpoint variant):
66 /// draft logits at position k get + W2(W1[prev_realized_token]) — left-to-right
67 /// within the block (the patch's _markov_semiar_sample_block semantics, greedy).
68 /// w1 = raw bf16 [V, rank] (row-gathered by device token id); w2 = q8_0 [rank->V].
69 pub markov: Option<MarkovHead>,
70 /// DSpark accept-rate head (trained with confidence loss). sglang's DSPARK planner
71 /// consumes it to SIZE VERIFY WINDOWS (cumprod survival — v0.5.16 headline; the
72 /// earlier "reference serving loop never consumes it" note matched SpecForge's
73 /// legacy spec_generate only). memra schedules with it under
74 /// `MEMRA_DSPARK_VT=confidence` (the H4 fix, DSPARK-POSTMORTEM-20260820.md:
75 /// per-round verify window from cumprod survival, `dspark_confidence_vt`) and
76 /// keeps it census+parity-only under the default ladder. Host-resident (5k floats).
77 pub confidence: Option<ConfidenceHead>,
78 /// YaRN rope (q38 arm-a inherits the target's rope_parameters: rope_type yarn,
79 /// factor 32, original 8192, beta 32/1). ff = per-dim divisors for rope_neox_ff
80 /// (effective inv_freq_j = base^(-2j/d)/ff[j] = the HF-yarn remapped frequency,
81 /// verified vs Qwen3RotaryEmbedding to 1.6e-7), mscale = attention_scaling
82 /// (0.1*ln(factor)+1) applied to q/k post-rope — cos/sin scaling distributes onto
83 /// the rotated vector exactly. None = plain rope (gemma/z-lab drafters).
84 pub rope_yarn: Option<(CudaSlice<f32>, f32)>,
85 /// DFlash2 head (z-lab `DFlash2DraftModel`, DFLASH2-EVAL-20260820.md): grouped
86 /// dynamic causal convs around EVERY sublayer + the candidate path selector that
87 /// replaces the markov chain. A DISTINCT semantic program from the DSpark head
88 /// (no-generic-support law): present iff config `architectures` names
89 /// `DFlash2DraftModel`, and then ALL 23 family tensors are REQUIRED — loading the
90 /// 58 backbone tensors alone computes an untrained model (the census trap).
91 pub dflash2: Option<Dflash2Head>,
92}
93
94/// One `GroupedDynamicCausalConv` module (reference model.py): a causal 2-tap
95/// depthwise conv over the BLOCK rows (block-local — row 0 zero-pads its missing
96/// predecessor; stateless across rounds), with per-position dynamic per-group
97/// coefficients projected from the module INPUT. `prepare` convolves the sublayer
98/// input with base_kernel[0] + dyn half 0; `finish` convolves the sublayer OUTPUT
99/// with base_kernel[1] + dyn half 1 (both dyn halves come from the SAME projection
100/// of the pre-conv input).
101pub struct Dflash2Conv {
102 /// base_kernel [2, k, hidden] flattened f32 (half-major: prepare then finish).
103 pub base: CudaSlice<f32>,
104 /// kernel_projection.weight [2*k*groups, hidden] (row layout = view(2, k, groups)).
105 pub proj: GpuTensor,
106}
107
108pub struct Dflash2Head {
109 pub attn_conv: Vec<Dflash2Conv>, // per layer
110 pub mlp_conv: Vec<Dflash2Conv>, // per layer
111 /// candidate_selector.hidden_projection.weight [rank, hidden].
112 pub hidden_proj: GpuTensor,
113 /// Codebooks [V, rank] raw bf16, HOST-resident: the walk gathers ~1+16 rows per
114 /// draft slot (~70KB/round) — host math beside the round's existing chain dtoh,
115 /// no device residency for 2x127MB tables. Checkpoint quirk: stored WITHOUT the
116 /// `.weight` suffix (reference from_pretrained installs a key_mapping).
117 pub pred_codebook: Vec<u8>,
118 pub succ_codebook: Vec<u8>,
119 pub rank: usize, // selector_rank 256
120 pub top_k: usize, // selector_top_k 16
121 pub conv_k: usize, // conv_kernel_size 2
122 pub group_size: usize, // conv_group_size 16
123 pub vocab: usize, // codebook rows (248320)
124}
125
126/// Resolve the named DFlash weight program. Keep this separate from loading so a typo cannot
127/// silently select q8 and invalidate a performance/default receipt.
128fn dflash_precision(raw: Option<&str>) -> Result<&str, String> {
129 let prec = raw.unwrap_or("q4");
130 match prec {
131 "q4" | "q8" | "mixed" | "bf16" | "fc" => Ok(prec),
132 other => Err(format!(
133 "MEMRA_DFLASH_PREC={other:?}: want q4, q8, mixed, bf16, or fc \
134 (q5 was measured defective and is not a serving mode)"
135 )),
136 }
137}
138
139/// One bf16 codebook row -> f32 (exact widening).
140fn cb_row(cb: &[u8], tok: usize, rank: usize) -> Vec<f32> {
141 bf16_to_f32(&cb[tok * rank * 2..(tok + 1) * rank * 2])
142}
143
144/// Greedy selector walk (reference `CandidateSelector.select` at T=0): per draft
145/// slot p, score(k) = unary[p,k] + <pred_codebook[prev] .* hidden_proj_row[p],
146/// succ_codebook[cand[p,k]]>, argmax over the top-k candidate set; the CHOSEN
147/// candidate seeds the next slot (sequential — the chain is the semantics, not an
148/// optimization). Host math (~nd*k*rank fused ops per round) over host-resident bf16
149/// codebooks; ties break to the LOWEST k (torch argmax convention). Pure so the
150/// selector semantics are CPU-gateable.
151///
152/// `unary`/`cand`: [nd, top_k] row-major; `hproj`: [nd, rank] row-major.
153#[allow(clippy::too_many_arguments)]
154pub fn dflash2_walk_greedy(
155 pred_codebook: &[u8],
156 succ_codebook: &[u8],
157 vocab: usize,
158 rank: usize,
159 top_k: usize,
160 unary: &[f32],
161 cand: &[u32],
162 hproj: &[f32],
163 anchor: u32,
164 nd: usize,
165) -> Vec<u32> {
166 dflash2_walk_greedy_q(
167 pred_codebook,
168 succ_codebook,
169 vocab,
170 rank,
171 top_k,
172 unary,
173 cand,
174 hproj,
175 anchor,
176 nd,
177 )
178 .0
179}
180
181/// [`dflash2_walk_greedy`] with the per-slot CONFIDENCE recorded (lane/glm5-loop-port,
182/// 2026-08-30): q[p] = softmax over the slot's candidate-set scores at T=1, of the chosen
183/// candidate — the greedy twin of `dflash2_walk_sampled`'s recorded `q_chosen` (same
184/// statistic family the owner's "take only high confidence offers" tau gate thresholds on
185/// the dspark route). The argmax selection is UNCHANGED (q is bookkeeping over the same
186/// scores, ~top_k exps per slot on host), so every existing greedy caller is byte-identical
187/// through the delegating wrapper. Pure, CPU-gateable like its siblings.
188#[allow(clippy::too_many_arguments)]
189pub fn dflash2_walk_greedy_q(
190 pred_codebook: &[u8],
191 succ_codebook: &[u8],
192 vocab: usize,
193 rank: usize,
194 top_k: usize,
195 unary: &[f32],
196 cand: &[u32],
197 hproj: &[f32],
198 anchor: u32,
199 nd: usize,
200) -> (Vec<u32>, Vec<f32>) {
201 let (kk, r) = (top_k, rank);
202 assert_eq!(unary.len(), nd * kk, "walk: unary shape");
203 assert_eq!(cand.len(), nd * kk, "walk: candidate shape");
204 assert_eq!(hproj.len(), nd * r, "walk: hidden-projection shape");
205 let mut path = Vec::with_capacity(nd);
206 let mut q_chosen = Vec::with_capacity(nd);
207 let mut prev = anchor;
208 for p in 0..nd {
209 assert!(
210 (prev as usize) < vocab,
211 "walk: predecessor token {prev} outside codebook vocab {vocab}"
212 );
213 let pr = cb_row(pred_codebook, prev as usize, r);
214 let hp = &hproj[p * r..(p + 1) * r];
215 // gate = pred_row .* hidden_proj (shared across the candidate set)
216 let gate: Vec<f32> = pr.iter().zip(hp).map(|(a, b)| a * b).collect();
217 let mut scores = vec![0f32; kk];
218 let (mut best, mut bi) = (f32::NEG_INFINITY, 0usize);
219 for (k, s) in scores.iter_mut().enumerate() {
220 let c = cand[p * kk + k] as usize;
221 assert!(c < vocab, "walk: candidate {c} outside codebook vocab");
222 let sr = cb_row(succ_codebook, c, r);
223 let mut acc = unary[p * kk + k];
224 for j in 0..r {
225 acc += gate[j] * sr[j];
226 }
227 *s = acc;
228 if acc > best {
229 best = acc;
230 bi = k;
231 }
232 }
233 // Recorded confidence: softmax at T=1 over the candidate set (f64 accumulation,
234 // the sampled walk's numeric discipline), of the argmaxed candidate.
235 let mut z = 0f64;
236 for &s in &scores {
237 z += ((s - best) as f64).exp();
238 }
239 q_chosen.push(if z > 0.0 { (1.0 / z) as f32 } else { 1.0 });
240 prev = cand[p * kk + bi];
241 path.push(prev);
242 }
243 (path, q_chosen)
244}
245
246impl Dflash2Head {
247 /// Greedy selector walk over this head's codebooks — see `dflash2_walk_greedy`.
248 pub fn walk_greedy(
249 &self,
250 unary: &[f32],
251 cand: &[u32],
252 hproj: &[f32],
253 anchor: u32,
254 nd: usize,
255 ) -> Vec<u32> {
256 dflash2_walk_greedy(
257 &self.pred_codebook,
258 &self.succ_codebook,
259 self.vocab,
260 self.rank,
261 self.top_k,
262 unary,
263 cand,
264 hproj,
265 anchor,
266 nd,
267 )
268 }
269
270 /// Greedy walk with the per-slot confidence recorded — see `dflash2_walk_greedy_q`.
271 pub fn walk_greedy_q(
272 &self,
273 unary: &[f32],
274 cand: &[u32],
275 hproj: &[f32],
276 anchor: u32,
277 nd: usize,
278 ) -> (Vec<u32>, Vec<f32>) {
279 dflash2_walk_greedy_q(
280 &self.pred_codebook,
281 &self.succ_codebook,
282 self.vocab,
283 self.rank,
284 self.top_k,
285 unary,
286 cand,
287 hproj,
288 anchor,
289 nd,
290 )
291 }
292
293 /// Sampled (T>0) selector walk — see `dflash2_walk_sampled`.
294 #[allow(clippy::too_many_arguments)]
295 pub fn walk_sampled(
296 &self,
297 unary: &[f32],
298 cand: &[u32],
299 hproj: &[f32],
300 anchor: u32,
301 nd: usize,
302 temp: f32,
303 uniforms: &mut dyn FnMut() -> f32,
304 ) -> (Vec<u32>, Vec<f32>, Vec<f32>) {
305 dflash2_walk_sampled(
306 &self.pred_codebook,
307 &self.succ_codebook,
308 self.vocab,
309 self.rank,
310 self.top_k,
311 unary,
312 cand,
313 hproj,
314 anchor,
315 nd,
316 temp,
317 uniforms,
318 )
319 }
320}
321
322/// AcceptRatePredictor: raw linear proj over [hidden ; markov_prev_embedding(rank)]
323/// (with_markov=true on the q38 arm-a export) — output is the PRE-sigmoid scalar.
324pub struct ConfidenceHead {
325 pub w: Vec<f32>, // [in_dim]
326 pub b: f32,
327 pub in_dim: usize,
328 pub with_markov: bool,
329}
330
331impl ConfidenceHead {
332 /// Host dot: the PRE-sigmoid accept score for one draft slot. `hidden` = the
333 /// drafter output row the slot is harvested from (the same row its logits use);
334 /// `emb` = the markov `w1` row of the slot's PREVIOUS chain token (required iff
335 /// `with_markov`) — the exact input contract the parity gate pins (prev ids =
336 /// `[anchor, chain[..nd-1]]`, dspark_q38_parity.rs stage 5).
337 pub fn raw_score(&self, hidden: &[f32], emb: Option<&[f32]>) -> f32 {
338 let mut acc = self.b;
339 for (w, x) in self.w.iter().zip(hidden) {
340 acc += w * x;
341 }
342 if self.with_markov {
343 let emb = emb.expect("with_markov confidence head scored without the markov embedding");
344 debug_assert_eq!(hidden.len() + emb.len(), self.in_dim);
345 for (w, x) in self.w[hidden.len()..].iter().zip(emb) {
346 acc += w * x;
347 }
348 } else {
349 debug_assert_eq!(hidden.len(), self.in_dim);
350 }
351 acc
352 }
353}
354
355pub struct MarkovHead {
356 pub w1_bf16: CudaSlice<u8>, // [V, rank] bf16 raw
357 pub w2: GpuTensor, // [rank -> V] q8_0
358 pub rank: usize,
359 pub vocab: usize,
360}
361
362/// Draft-row harvest convention for DFlash-family block drafters
363/// (darklanes research/deepseek-flash-20260818/DSPARK-POSTMORTEM-20260820.md).
364///
365/// The DFlash and DSpark SpecForge training strategies supervise DIFFERENT rows of the
366/// same `[anchor, MASK x b-1]` block, so the row -> trunk-position mapping is a property
367/// of the CHECKPOINT's training strategy, not of the loader:
368///
369/// - **Dflash** (mask-fill; z-lab dflash / SpecForge `OnlineDFlashModel`): row k is
370/// trained to predict the token AT position anchor+k — "Labels: same-position
371/// prediction", `weight_mask *= (pos_in_block > 0)` excludes the anchor row
372/// (SpecForge `specforge/algorithms/common/dflash_family_model.py:453-472`).
373/// Drafts = rows 1..b-1; the anchor row's output is untrained.
374/// - **Dspark** (shifted; SpecForge `OnlineDSparkModel`, `training.strategy: dspark` —
375/// the q38 arm-a export): row k is trained to predict the token at anchor+k+1, ALL
376/// rows supervised INCLUDING the anchor row (`label_offsets = arange(1,
377/// block_size+1)`, `dflash_family_model.py:816`). sglang's DSPARK worker — the stack
378/// every arm-a bank number was measured on — harvests gamma = block_size drafts with
379/// the anchor row's output as draft 1 (verified on the v0.5.17 eval-pin tag:
380/// `dspark_components/dspark_draft.py:248,260,318`; `dspark_config.py:269`).
381///
382/// Mismatching the convention verifies every slot against a position the row was never
383/// trained for — the q38 accept collapse (2.9 -> 1.43) in the postmortem.
384#[derive(Clone, Copy, PartialEq, Eq, Debug)]
385pub enum DsparkHarvest {
386 /// mask-fill: drafts = rows 1..b-1, row k fills position anchor+k.
387 Dflash,
388 /// shifted: drafts = rows 0..b-1, row k predicts position anchor+k+1.
389 Dspark,
390}
391
392impl DsparkHarvest {
393 /// The served resolution: explicit `MEMRA_DSPARK_HARVEST={dflash|dspark}` wins
394 /// (unknown values REFUSE loudly — a typo silently reverting the convention would
395 /// re-open the postmortem's misalignment); UNSET defers to the CHECKPOINT's own
396 /// training-strategy census — the owner-ratified default flip (2026-08-20, after
397 /// B1 confirmed H1 interleaved ×5 on serving-class hardware: accept 1.38→2.41
398 /// agentic / 1.53→3.66 math, E2E ALL EXACT both arms). Strategy-keyed, not
399 /// env-keyed, per the B0 plan: a DSPARK-strategy export harvests shifted
400 /// (all-rows), a mask-fill export keeps the historical dflash arm byte-identical.
401 pub fn resolve(cfg: &DflashCfg) -> Self {
402 Self::resolve_value(
403 std::env::var("MEMRA_DSPARK_HARVEST").ok().as_deref(),
404 cfg.strategy_dspark,
405 )
406 }
407
408 pub fn resolve_value(v: Option<&str>, strategy_dspark: bool) -> Self {
409 match v {
410 None | Some("") => {
411 if strategy_dspark {
412 DsparkHarvest::Dspark
413 } else {
414 DsparkHarvest::Dflash
415 }
416 }
417 set => Self::from_env_value(set),
418 }
419 }
420
421 /// ENV-ONLY parser (no checkpoint census): unset = `Dflash`, the historical arm.
422 /// Kept for the explicit-value path of [`Self::resolve_value`] and the seam tests;
423 /// round arms resolve through [`Self::resolve`] so the default stays strategy-keyed.
424 pub fn from_env_value(v: Option<&str>) -> Self {
425 match v {
426 None | Some("") | Some("dflash") => DsparkHarvest::Dflash,
427 Some("dspark") => DsparkHarvest::Dspark,
428 Some(other) => panic!(
429 "MEMRA_DSPARK_HARVEST={other}: unknown harvest convention (dflash|dspark); \
430 refusing — a wrong convention verifies every draft slot against a position \
431 the drafter row was not trained for (DSPARK-POSTMORTEM-20260820.md)"
432 ),
433 }
434 }
435
436 /// Resolve the harvest convention for a LOADED drafter — FAMILY-keyed first, then
437 /// STRATEGY-keyed (v0.100 train merge of the two ratified keyings):
438 /// - DFlash2 is a mask-fill-family drafter by construction (reference
439 /// `dflash_generate` harvests rows `1-verify_size:`; the card says "block size 8
440 /// (7 draft tokens per verification step)" — DFLASH2-EVAL-20260820.md §3). An env
441 /// value that CONTRADICTS the census REFUSES rather than silently re-keying the
442 /// round.
443 /// - Every other checkpoint rides [`Self::resolve_value`]: explicit env wins (typos
444 /// refuse loudly), unset defers to the checkpoint's own training-strategy census
445 /// (the owner-ratified 2026-08-20 default flip).
446 pub fn for_draft(draft: &DflashDraft) -> Self {
447 Self::for_family_value(
448 draft.dflash2.is_some(),
449 std::env::var("MEMRA_DSPARK_HARVEST").ok().as_deref(),
450 draft.cfg.strategy_dspark,
451 )
452 }
453
454 pub fn for_family_value(is_dflash2: bool, env: Option<&str>, strategy_dspark: bool) -> Self {
455 if is_dflash2 {
456 if env == Some("dspark") {
457 panic!(
458 "MEMRA_DSPARK_HARVEST=dspark with a DFlash2 checkpoint: DFlash2 \
459 is mask-fill (b-1 drafts, anchor row is not a draft — reference \
460 dflash_generate rows 1-verify_size:); the shifted harvest would \
461 verify every slot one position early. Refusing (census-keyed, \
462 not env-keyed)."
463 );
464 }
465 return DsparkHarvest::Dflash;
466 }
467 Self::resolve_value(env, strategy_dspark)
468 }
469
470 /// Manifest/serialized name (the oracle geometry manifest's `harvest` field).
471 pub fn name(self) -> &'static str {
472 match self {
473 DsparkHarvest::Dflash => "dflash",
474 DsparkHarvest::Dspark => "dspark",
475 }
476 }
477
478 pub fn from_name(v: &str) -> Option<Self> {
479 match v {
480 "dflash" => Some(DsparkHarvest::Dflash),
481 "dspark" => Some(DsparkHarvest::Dspark),
482 _ => None,
483 }
484 }
485
486 /// First drafter OUTPUT row consumed as a draft candidate.
487 pub fn first_row(self) -> usize {
488 match self {
489 DsparkHarvest::Dflash => 1,
490 DsparkHarvest::Dspark => 0,
491 }
492 }
493
494 /// Drafted tokens harvested per round from a `b`-row block.
495 pub fn n_drafts(self, b: usize) -> usize {
496 match self {
497 DsparkHarvest::Dflash => b - 1,
498 DsparkHarvest::Dspark => b,
499 }
500 }
501
502 /// The position offset (relative to the round anchor at the block's row 0) that
503 /// drafter output row `row` is TRAINED to predict under this convention.
504 pub fn trained_offset_of_row(self, row: usize) -> usize {
505 match self {
506 DsparkHarvest::Dflash => row,
507 DsparkHarvest::Dspark => row + 1,
508 }
509 }
510}
511
512/// Checkpoint training-strategy census over the raw config.json text (the loader's
513/// minimal-extractor idiom — no json dep in-tree). TRUE iff the export declares the
514/// DSPARK strategy: `architectures` naming a DSpark model class (`Qwen3DSparkModel`,
515/// the SpecForge OnlineDSparkModel export form) or `dflash_config.projector_type ==
516/// "dspark"`. z-lab / OnlineDFlashModel mask-fill exports carry neither signal. Pure,
517/// so the census is testable against config fragments without files.
518pub fn dspark_strategy_census(txt: &str) -> bool {
519 let arch = txt
520 .find("\"architectures\"")
521 .and_then(|i| {
522 let rest = &txt[i..];
523 let a = rest.find('[')?;
524 let b = rest.find(']')?;
525 Some(rest[a..b].contains("DSpark"))
526 })
527 .unwrap_or(false);
528 let proj = txt
529 .find("\"projector_type\"")
530 .map(|i| {
531 let rest = &txt[i..];
532 let after = rest.find(':').map(|c| &rest[c + 1..]).unwrap_or("");
533 after.trim_start().starts_with("\"dspark\"")
534 })
535 .unwrap_or(false);
536 arch || proj
537}
538
539/// Accepted-prefix length of a round's candidates against the trunk's verify argmaxes:
540/// `cand[0]` = the round anchor (already decided), `cand[1..]` = the drafts;
541/// `vam[j]` = the trunk's argmax prediction for position anchor+j+1. Returns m =
542/// number of accepted drafts (`cand[1..=m]` committed, `vam[m]` becomes the next
543/// anchor). Pure so the harvest-alignment fixture can exercise it CPU-side.
544pub fn dspark_accept_prefix(cand: &[u32], vam: &[u32], vt: usize) -> usize {
545 let mut m = 0usize;
546 while m < vt - 1 && cand[m + 1] == vam[m] {
547 m += 1;
548 }
549 m
550}
551
552/// Verify-window policy for the dspark round (H4, DSPARK-POSTMORTEM-20260820.md §3).
553///
554/// B2 measured the structural fork: the fixed full-block window (vt=8) buys 95–100%
555/// of the sglang accept bank but LOSES wall speed to the reactive ladder everywhere
556/// except math — at 0.2–0.5 slot rates, full-block verify pays 5–6 empty rows per
557/// round. The confidence policy is the mechanism both leading engines schedule with
558/// (sglang v0.5.16 `dspark_planner.py` cumprod survival; vLLM #47808): size EACH
559/// round's window from the drafter's own trained accept-rate head, so windows open
560/// on confident streaks (math/code) and shrink on bursty text without a 4-round
561/// ladder climb.
562#[derive(Clone, Copy, PartialEq, Debug)]
563pub enum DsparkVtPolicy {
564 /// The shipped reactive ladder: `vt = (m+2).clamp(3, vt_cap)` per round
565 /// (`MEMRA_DFLASH_ADAPT=0` pins vt at `vt_cap` = the fixed-window arm).
566 Ladder,
567 /// `MEMRA_DSPARK_VT=confidence`: per-round window from cumprod survival of the
568 /// confidence head's sigmoid scores, thresholded at `tau`
569 /// (`MEMRA_DSPARK_VT_TAU`, default 0.5). Raw sigmoid — no STS sidecar
570 /// calibration exists for this export; the postmortem names this the starting
571 /// policy.
572 Confidence { tau: f32 },
573 /// `MEMRA_DSPARK_VT=confidence-slot` (owner directive, 2026-08-20: "take only
574 /// high confidence offers"): submit only the longest draft PREFIX whose every
575 /// slot clears `tau` on its own sigmoid — the low-confidence tail never enters
576 /// verify. Same tau env. vs `Confidence`: if the head's per-row score is the
577 /// MARGINAL accept probability (it already sinks with depth), cumprod survival
578 /// double-counts the decay and over-truncates; if it is the CONDITIONAL,
579 /// per-slot under-truncates. Which statistic the q38 head emits is empirical —
580 /// both arms ride the A/B.
581 ConfidenceSlot { tau: f32 },
582}
583
584impl DsparkVtPolicy {
585 /// The served resolution: explicit `MEMRA_DSPARK_VT={ladder|confidence|
586 /// confidence-slot}` wins (unknown values REFUSE loudly — a typo silently
587 /// reverting the window policy would invalidate an A/B without a trace); UNSET
588 /// defaults to **`confidence-slot` at τ = `MEMRA_DSPARK_VT_TAU` (default 0.5)** —
589 /// the owner-ratified H4 flip (2026-08-20; cell 2's 4-arm A/B ×5 + cell 3's tau
590 /// ladder put the knee at τ=.5 for the slot arm: 94–98% of the fixed-8 accept bank
591 /// at wall ≥ the reactive ladder, exactness 11/11 ALL EXACT). Census-keyed per the
592 /// capacity-keyed-defaults law: a checkpoint WITHOUT an accept-rate head has no
593 /// signal to schedule with, so unset-env resolves to the ladder there (loudly, at
594 /// load) instead of panicking on a default; `MEMRA_DFLASH_ADAPT=0` (an explicit
595 /// fixed-window request) also keeps the ladder-family arm.
596 pub fn resolve(has_confidence_head: bool) -> Self {
597 Self::resolve_value(
598 std::env::var("MEMRA_DSPARK_VT").ok().as_deref(),
599 std::env::var("MEMRA_DSPARK_VT_TAU").ok().as_deref(),
600 std::env::var("MEMRA_DFLASH_ADAPT").ok().as_deref(),
601 has_confidence_head,
602 )
603 }
604
605 pub fn resolve_value(
606 vt: Option<&str>,
607 tau: Option<&str>,
608 adapt: Option<&str>,
609 has_confidence_head: bool,
610 ) -> Self {
611 match vt {
612 None | Some("") => {
613 if adapt == Some("0") || !has_confidence_head {
614 DsparkVtPolicy::Ladder
615 } else {
616 // The ratified default rides the SAME tau parse as the explicit
617 // arm (a bad MEMRA_DSPARK_VT_TAU refuses, never silently ignored).
618 Self::from_env_value(Some("confidence-slot"), tau, adapt)
619 }
620 }
621 set => Self::from_env_value(set, tau, adapt),
622 }
623 }
624
625 /// ENV-ONLY parser (no head census): unset = `Ladder`. Kept for the explicit-value
626 /// path of [`Self::resolve_value`] and the policy-gate tests; round arms resolve
627 /// through [`Self::resolve`] so the default stays head-census-keyed.
628 pub fn from_env_value(vt: Option<&str>, tau: Option<&str>, adapt: Option<&str>) -> Self {
629 match vt {
630 None | Some("") | Some("ladder") => DsparkVtPolicy::Ladder,
631 Some(mode @ ("confidence" | "confidence-slot")) => {
632 if adapt == Some("0") {
633 panic!(
634 "MEMRA_DSPARK_VT={mode} together with MEMRA_DFLASH_ADAPT=0 is \
635 contradictory (a pinned fixed window vs a per-round confidence \
636 window); unset one — refuse-on-ambiguity"
637 );
638 }
639 let tau = tau
640 .map(|t| {
641 t.parse::<f32>()
642 .unwrap_or_else(|_| panic!("MEMRA_DSPARK_VT_TAU={t}: not a float"))
643 })
644 .unwrap_or(0.5);
645 assert!(
646 tau > 0.0 && tau < 1.0,
647 "MEMRA_DSPARK_VT_TAU={tau}: confidence threshold must be in (0,1)"
648 );
649 if mode == "confidence" {
650 DsparkVtPolicy::Confidence { tau }
651 } else {
652 DsparkVtPolicy::ConfidenceSlot { tau }
653 }
654 }
655 Some(other) => panic!(
656 "MEMRA_DSPARK_VT={other}: unknown verify-window policy \
657 (ladder|confidence|confidence-slot); refusing — a wrong policy \
658 silently reverts the H4 arm (DSPARK-POSTMORTEM-20260820.md)"
659 ),
660 }
661 }
662
663 /// True for every head-scheduled arm (the loops gate the head requirement and
664 /// the embedding stash on this).
665 pub fn is_confidence(&self) -> bool {
666 !matches!(self, DsparkVtPolicy::Ladder)
667 }
668
669 /// Size this round's verify window from the head's pre-sigmoid slot scores.
670 /// `None` under the ladder (the caller keeps its carried vt).
671 pub fn size_window(&self, raws: &[f32], vt_cap: usize) -> Option<usize> {
672 match *self {
673 DsparkVtPolicy::Ladder => None,
674 DsparkVtPolicy::Confidence { tau } => Some(dspark_confidence_vt(raws, tau, vt_cap)),
675 DsparkVtPolicy::ConfidenceSlot { tau } => {
676 Some(dspark_slot_confidence_vt(raws, tau, vt_cap))
677 }
678 }
679 }
680}
681
682/// H4 window sizing (the sglang-planner/vLLM-#47808 mechanism, thresholded): `raws[k]`
683/// = the accept-rate head's PRE-sigmoid score for draft slot k+1; survival
684/// `S_k = prod_{j<=k} sigmoid(raws[j])`; the window keeps leading slots while
685/// `S_k >= tau`. Returns `vt` = 1 (anchor) + kept drafts, clamped to `[2, vt_cap]`:
686/// the draft forward is already paid, so at least one draft rides every verify — one
687/// extra verify row costs less than a guaranteed empty round. Pure, so the policy's
688/// knee is testable CPU-side like `dspark_accept_prefix`.
689pub fn dspark_confidence_vt(raws: &[f32], tau: f32, vt_cap: usize) -> usize {
690 let mut surv = 1.0f32;
691 let mut kept = 0usize;
692 for &r in raws {
693 surv *= 1.0 / (1.0 + (-r).exp());
694 if surv < tau {
695 break;
696 }
697 kept += 1;
698 }
699 (1 + kept).clamp(2, vt_cap.max(2))
700}
701
702/// Owner-directive arm (2026-08-20, "take only high confidence offers"): keep the
703/// longest draft PREFIX whose EVERY slot clears `tau` on its own sigmoid — truncate
704/// at the first sub-threshold slot, so the low-confidence tail (B2 measured 0.2–0.5
705/// slot rates at depth) never enters verify. Prefix truncation is forced by the
706/// accept rule anyway (`dspark_accept_prefix` stops at the first miss — a kept slot
707/// after a dropped one could never commit); the policy fork vs `dspark_confidence_vt`
708/// is only the stopping statistic (per-slot marginal vs cumulative survival). Same
709/// floor/cap contract.
710pub fn dspark_slot_confidence_vt(raws: &[f32], tau: f32, vt_cap: usize) -> usize {
711 let mut kept = 0usize;
712 for &r in raws {
713 let p = 1.0 / (1.0 + (-r).exp());
714 if p < tau {
715 break;
716 }
717 kept += 1;
718 }
719 (1 + kept).clamp(2, vt_cap.max(2))
720}
721
722// ================= SAMPLED ADMISSION (T>0) — lane/dspark-sampled-admission-20260820 =====
723// True rejection sampling for the dspark route (mystery A of DSPARK-POSTMORTEM-20260820):
724// draft slot j is DRAWN from a recorded proposal distribution q_j, the trunk's verify column
725// arbitrates with the Leviathan/Chen rule (accept x_j while u_j*q_j(x_j) < p_j(x_j); on
726// reject resample from norm(max(0, p-q)); on full accept the bonus ~ p at the last column),
727// so the committed stream's distribution equals trunk-only sampling from the FILTERED target
728// p — the same contract the frspec/MTP route ships (spec.rs sampled accept walk; kernels
729// oracled by sample_check). T==0/None keeps every greedy path byte-identical (the exactness
730// instrument and the kill-switch are the same code).
731//
732// Two proposal families, each recording the TRUE distribution its drafts were drawn from:
733// - Rows (dspark/dflash strategy checkpoints): per-slot FILTERED softmax of the draft-logits
734// row — markov-corrected in place when the head is present (the sglang DSPARK worker's
735// "chain rejection sampling over markov-corrected draft probs"), plain rows otherwise
736// (the z-lab reference's independent-row T>0 arm).
737// - Selector (DFlash2): the candidate-path selector's per-slot softmax over its top-k
738// candidate set at temperature ONLY — the reference applies no top-k/top-p to selector
739// scores (z-lab model.py `CandidateSelector.select`: `_sampling_probs(scores, temperature)`
740// with default filters) — with the candidate-set residual (`scatter_add_` of -q, clamped).
741
742/// Rejection-sampling prefix walk: accept draft j while `u_j * q_j < p_j` (strict, f64 —
743/// byte-identical to the frspec accept test). `p`/`q` are the FILTERED target/proposal
744/// probabilities of the drafted tokens; `u` the per-slot uniforms. Pure so the composition
745/// gate can pin the rule on CPU.
746pub fn rejection_accept_len(p: &[f32], q: &[f32], u: &[f32]) -> usize {
747 assert!(
748 q.len() >= p.len() && u.len() >= p.len(),
749 "accept walk shape"
750 );
751 let mut m = 0usize;
752 while m < p.len() && (u[m] as f64) * (q[m] as f64) < p[m] as f64 {
753 m += 1;
754 }
755 m
756}
757
758/// Sampled selector walk (reference `CandidateSelector.select`, temperature>0 arm): per
759/// draft slot the pair scores over the top-k candidate set become a softmax at `temp`
760/// (temperature ONLY — the reference passes no top-k/top-p here), one uniform draws the
761/// candidate (fixed-order CDF walk), and the CHOSEN candidate seeds the next slot exactly
762/// like the greedy chain. Returns (path, q_chosen[nd], q_rows[nd*top_k]) — q_rows are the
763/// recorded per-slot candidate probabilities (the residual's `scatter_add_` input), and
764/// q_chosen[j] == q_rows[j*top_k + chosen_j] is the accept-test q. Pure (uniforms injected)
765/// so the T->0 limit, the chain conditioning, and the recorded-q contract are CPU-gateable.
766#[allow(clippy::too_many_arguments)]
767pub fn dflash2_walk_sampled(
768 pred_codebook: &[u8],
769 succ_codebook: &[u8],
770 vocab: usize,
771 rank: usize,
772 top_k: usize,
773 unary: &[f32],
774 cand: &[u32],
775 hproj: &[f32],
776 anchor: u32,
777 nd: usize,
778 temp: f32,
779 uniforms: &mut dyn FnMut() -> f32,
780) -> (Vec<u32>, Vec<f32>, Vec<f32>) {
781 assert!(
782 temp > 0.0,
783 "sampled walk is the T>0 arm; T=0 is walk_greedy"
784 );
785 let (kk, r) = (top_k, rank);
786 assert_eq!(unary.len(), nd * kk, "walk: unary shape");
787 assert_eq!(cand.len(), nd * kk, "walk: candidate shape");
788 assert_eq!(hproj.len(), nd * r, "walk: hidden-projection shape");
789 let mut path = Vec::with_capacity(nd);
790 let mut q_chosen = Vec::with_capacity(nd);
791 let mut q_rows = Vec::with_capacity(nd * kk);
792 let mut prev = anchor;
793 for p in 0..nd {
794 assert!(
795 (prev as usize) < vocab,
796 "walk: predecessor token {prev} outside codebook vocab {vocab}"
797 );
798 let pr = cb_row(pred_codebook, prev as usize, r);
799 let hp = &hproj[p * r..(p + 1) * r];
800 let gate: Vec<f32> = pr.iter().zip(hp).map(|(a, b)| a * b).collect();
801 let mut scores = vec![0f32; kk];
802 for (k, s) in scores.iter_mut().enumerate() {
803 let c = cand[p * kk + k] as usize;
804 assert!(c < vocab, "walk: candidate {c} outside codebook vocab");
805 let sr = cb_row(succ_codebook, c, r);
806 let mut acc = unary[p * kk + k];
807 for j in 0..r {
808 acc += gate[j] * sr[j];
809 }
810 *s = acc;
811 }
812 // softmax over the candidate set at temp (f64 internals; recorded probs are the
813 // f32 values the CDF walk actually samples from — recorded q IS the proposal).
814 let mx = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
815 let mut z = 0f64;
816 let ex: Vec<f64> = scores
817 .iter()
818 .map(|&s| {
819 let e0 = (((s - mx) / temp) as f64).exp();
820 z += e0;
821 e0
822 })
823 .collect();
824 let probs: Vec<f32> = ex.iter().map(|&e0| (e0 / z) as f32).collect();
825 let u = uniforms() as f64;
826 let mut acc = 0f64;
827 // fp-residue fallback (u >= f32-accumulated mass, ~2^-24 events): the max-prob
828 // candidate — never a zero-prob one (host_u01's range includes 1.0 exactly).
829 let mut bi = probs
830 .iter()
831 .enumerate()
832 .max_by(|a, b| a.1.total_cmp(b.1))
833 .map(|(k, _)| k)
834 .unwrap_or(0);
835 for (k, &pk) in probs.iter().enumerate() {
836 acc += pk as f64;
837 if u < acc {
838 bi = k;
839 break;
840 }
841 }
842 prev = cand[p * kk + bi];
843 path.push(prev);
844 q_chosen.push(probs[bi]);
845 q_rows.extend_from_slice(&probs);
846 }
847 (path, q_chosen, q_rows)
848}
849
850/// `dflash2_propose_sampled`'s wire: (path, q_chosen, candidate ids, q_rows).
851pub(crate) type Dflash2SampledProposal = (Vec<u32>, Vec<f32>, Vec<u32>, Vec<f32>);
852
853/// Per-round proposal record for the sampled dspark round — everything the rejection
854/// walk needs to evaluate the TRUE per-slot proposal distribution q.
855pub(crate) enum DsparkDraftSample {
856 /// q lives in the round's draft-logits buffer `dl` (markov-biased in place when the
857 /// head is armed); per-slot FILTERED stats retained device-contiguous for the accept
858 /// gather + host-mirrored for the reject-slot residual.
859 Rows {
860 th: CudaSlice<f32>, // [nd] filter thresholds (e-units), slot-indexed
861 z: CudaSlice<f32>, // [nd] renorm masses
862 stats: Vec<(f32, f32, f32)>, // host (mx, th, z) per slot
863 },
864 /// DFlash2 candidate-path selector: q is the recorded candidate-set distribution.
865 Selector {
866 cand: Vec<u32>, // [nd*top_k] candidate ids
867 q_rows: Vec<f32>, // [nd*top_k] per-slot candidate probs
868 q_chosen: Vec<f32>, // [nd] prob of the drawn candidate (accept-test q)
869 top_k: usize,
870 },
871}
872
873/// The sampled round's verify+accept: filtered p gathered from the trunk's verify logits
874/// (row j arbitrates draft `cand[j+1]` — the position mapping the greedy prefix walk uses),
875/// the rejection walk over host uniforms, then `next` = bonus (full accept: filtered-Gumbel
876/// from the LAST verify row with its OWN fresh stats — the sampfix-20260805 law: that row is
877/// one past the gathered set) or the residual sample at the reject slot (family-keyed q:
878/// full-row logits for Rows, sparse candidate-set probs for Selector). Returns (m, next) —
879/// the exact (accepted-drafts, next-anchor) contract of the greedy `dspark_accept_prefix` +
880/// `vam[m]` pair, so both round bodies commit identically downstream.
881///
882/// PENALIZED SAMPLED (lane/dspark-penalized-sampled-20260821): when the request carries
883/// non-identity penalties, the vt verify columns are materialized ONCE into a penalized
884/// copy where row j's Keskar pass runs over `pen_win ++ cand[1..=j]` (window-capped) —
885/// the tokens committed before position j ON EVERY PATH WHERE ROW j IS CONSULTED,
886/// same-round accepts included (row j is only read when drafts 1..j were all accepted,
887/// i.e. exactly when `cand[1..=j]` is the committed prefix; the bonus row vt-1 is only
888/// read on full accept, when all nq drafts are committed). Every p read — the batched
889/// stats+gather, the bonus draw, the reject-slot residual column — points at that buffer,
890/// so p is the true penalized per-state target and the committed stream equals plain
891/// penalized sampling (the composition gate's penalty fixtures, self-hit included).
892/// q stays the RECORDED proposal the drafts were actually drawn from (unpenalized):
893/// rejection sampling is unbiased for ANY proposal with `u·q(x) < p(x)` + residual
894/// `norm(max(0, p−q))`; penalizing q would only buy acceptance overlap and would cost an
895/// evolving-history pass inside the sync-free device chain. `pen_win` is the caller's
896/// session window ALREADY trimmed to `min(penalty_last_n, PEN_WINDOW_MAX)` (empty when
897/// penalties are off — the unpenalized path is byte-untouched).
898#[allow(clippy::too_many_arguments)]
899pub(crate) fn dspark_accept_sampled(
900 e: &Engine,
901 tlogits: &CudaSlice<f32>,
902 cand: &[u32],
903 vt: usize,
904 n_vocab: usize,
905 dl: &CudaSlice<f32>,
906 prop: &DsparkDraftSample,
907 sp: &crate::spec::SpecSampling,
908 pen_win: &[u32],
909 sctr: &mut u32,
910 uctr: &mut u32,
911) -> Result<(usize, u32), Box<dyn std::error::Error>> {
912 let nq = vt - 1; // drafts under this round's verify window
913 debug_assert!(nq >= 1 && cand.len() > nq, "sampled accept shape");
914 // --- penalized verify columns (identity penalties: no copy, no launch, raw tlogits) ---
915 let pen_on = sp.pen_on();
916 let ptl: Option<CudaSlice<f32>> = if pen_on {
917 let win = sp.penalty_last_n.min(crate::spec::PEN_WINDOW_MAX);
918 debug_assert!(pen_win.len() <= win, "pen_win must arrive pre-trimmed");
919 let mut hist: Vec<u32> = Vec::with_capacity(pen_win.len() + nq);
920 hist.extend_from_slice(pen_win);
921 hist.extend_from_slice(&cand[1..=nq]); // drafted tokens: row j reads the first j
922 let hd = e.htod_u32_v(&hist)?;
923 let mut buf = e.clone_dtod(tlogits)?;
924 e.penalize_logits_rows_inc(
925 &mut buf,
926 &hd,
927 pen_win.len(),
928 sp.penalty_repeat,
929 sp.penalty_freq,
930 sp.penalty_present,
931 n_vocab,
932 vt,
933 win,
934 )?;
935 Some(buf)
936 } else {
937 None
938 };
939 let p_src: &CudaSlice<f32> = ptl.as_ref().unwrap_or(tlogits);
940 // --- filtered p at the drafted tokens (one batched stats + gather over rows 0..nq-1) ---
941 let rows: Vec<i32> = (0..nq as i32).collect();
942 let ids: Vec<u32> = cand[1..=nq].to_vec();
943 let rowsd = e.htod_i32(&rows)?;
944 let idsd = e.htod_u32_v(&ids)?;
945 let (mut pth, mut pz, mut pmx) = (e.zeros(nq)?, e.zeros(nq)?, e.zeros(nq)?);
946 e.filter_stats(
947 p_src, n_vocab, &rowsd, &mut pth, &mut pz, &mut pmx, n_vocab, nq, sp.temp, sp.top_k,
948 sp.top_p, sp.min_p,
949 )?;
950 let mut pj_d = e.zeros(nq)?;
951 e.softmax_gather_filtered(
952 p_src, n_vocab, &idsd, &rowsd, &pth, &pz, &mut pj_d, n_vocab, nq, sp.temp,
953 )?;
954 let pj = e.dtoh(&pj_d)?;
955 let (pthv, pzv, pmxv) = (e.dtoh(&pth)?, e.dtoh(&pz)?, e.dtoh(&pmx)?);
956 // --- q at the drafted tokens (the recorded proposal distribution) ---
957 let qj: Vec<f32> = match prop {
958 DsparkDraftSample::Rows { th, z, .. } => {
959 // dl row j is draft j's (bias-corrected) logits row; th/z are slot-indexed, and
960 // rows 0..nq-1 index both the buffer rows and the stat pairs.
961 let mut qd = e.zeros(nq)?;
962 e.softmax_gather_filtered(
963 dl, n_vocab, &idsd, &rowsd, th, z, &mut qd, n_vocab, nq, sp.temp,
964 )?;
965 e.dtoh(&qd)?
966 }
967 DsparkDraftSample::Selector { q_chosen, .. } => q_chosen[..nq].to_vec(),
968 };
969 // --- the rejection walk ---
970 let mut us = Vec::with_capacity(nq);
971 for _ in 0..nq {
972 us.push(crate::spec::host_u01(sp.seed, *uctr));
973 *uctr = uctr.wrapping_add(1);
974 }
975 let m = rejection_accept_len(&pj[..nq], &qj[..nq], &us);
976 // --- next anchor: bonus or residual ---
977 let next = if m == nq {
978 // FULL ACCEPT: bonus ~ filtered p at verify row vt-1 — fresh stats for THIS row.
979 // Under penalties p_src row vt-1 carries the FULL drafted block in its window
980 // (all nq drafts are committed on this path — the "drafted token penalizes its
981 // own successor" case the composition gate's self-hit fixture pins).
982 let rows_l = e.htod_i32(&[(vt - 1) as i32])?;
983 let (mut th1, mut z1, mut mx1) = (e.zeros(1)?, e.zeros(1)?, e.zeros(1)?);
984 e.filter_stats(
985 p_src, n_vocab, &rows_l, &mut th1, &mut z1, &mut mx1, n_vocab, 1, sp.temp, sp.top_k,
986 sp.top_p, sp.min_p,
987 )?;
988 let mut pb = e.zeros(n_vocab)?;
989 e.gumbel_perturb_filtered_col(
990 p_src,
991 vt - 1,
992 &mut pb,
993 n_vocab,
994 sp.seed,
995 *sctr,
996 sp.temp,
997 &mx1,
998 &th1,
999 0,
1000 )?;
1001 *sctr = sctr.wrapping_add(1);
1002 let td = e.argmax_token_device(&pb, n_vocab)?;
1003 e.dtoh_u32_one(&td)?
1004 } else {
1005 // REJECT at slot m: token ~ norm(max(0, p_m - q_m)); p row m's stats come from the
1006 // gathered set (rows 0..nq-1 cover every reject slot). Under penalties the column
1007 // copy MUST come from p_src (the penalized buffer) — a raw-tlogits residual is the
1008 // composition gate's "residual reads unpenalized p" tooth.
1009 let mut col = e.zeros(n_vocab)?;
1010 e.copy_view_into(
1011 &mut col,
1012 0,
1013 &p_src.slice(m * n_vocab..(m + 1) * n_vocab),
1014 n_vocab,
1015 )?;
1016 let p_stats = (pmxv[m], pthv[m], pzv[m]);
1017 let mut tok_d = e.alloc_u32_zeroed(1)?;
1018 let sc = *sctr;
1019 *sctr = sctr.wrapping_add(1);
1020 match prop {
1021 DsparkDraftSample::Rows { stats, .. } => {
1022 let mut qbuf = e.zeros(n_vocab)?;
1023 e.copy_view_into(
1024 &mut qbuf,
1025 0,
1026 &dl.slice(m * n_vocab..(m + 1) * n_vocab),
1027 n_vocab,
1028 )?;
1029 e.residual_sample_filtered(
1030 &col,
1031 Some(&qbuf),
1032 n_vocab,
1033 sp.temp,
1034 sp.seed,
1035 sc,
1036 p_stats,
1037 stats[m],
1038 &mut tok_d,
1039 )?;
1040 }
1041 DsparkDraftSample::Selector {
1042 cand: cids,
1043 q_rows,
1044 top_k,
1045 ..
1046 } => {
1047 let k = *top_k;
1048 let ids_m = e.htod_u32_v(&cids[m * k..(m + 1) * k])?;
1049 let qs_m = e.htod(&q_rows[m * k..(m + 1) * k])?;
1050 e.residual_sample_sparse_q(
1051 &col, &ids_m, &qs_m, k, n_vocab, sp.temp, sp.seed, sc, p_stats, &mut tok_d,
1052 )?;
1053 }
1054 }
1055 e.dtoh_u32(&tok_d)?[0]
1056 };
1057 Ok((m, next))
1058}
1059
1060/// Clip door for the DFlash2 windowed round attention (lane/dflash2-longctx, §10.6(c)).
1061/// Default ON: the lo-clipped kernel — byte-identical output (kernel_check
1062/// `sdpa_naive_w_lo`), O(window) key scan, and no T_kv*4-byte shared-mem launch bound, so
1063/// the route survives past ~12k ctx (GATES-SMOKE-20260821 B2: DriverError(
1064/// CUDA_ERROR_INVALID_VALUE) at ctx 16,571/30,157, last success 9,510).
1065/// MEMRA_DFLASH2_SDPA_CLIP=0 = the legacy full-scan kernel byte-for-byte — the rollback
1066/// seam and the long-ctx gate's crash-reproduction arm.
1067fn dflash2_sdpa_clip_on() -> bool {
1068 std::env::var("MEMRA_DFLASH2_SDPA_CLIP")
1069 .map(|v| v != "0")
1070 .unwrap_or(true)
1071}
1072
1073/// The DFlash2 round attention over the non-causal symmetric window: one seam for both the
1074/// first-light (`forward_block`) and cached (`forward_round`) arms, dispatching the clipped
1075/// kernel unless the rollback door is thrown.
1076#[allow(clippy::too_many_arguments)]
1077fn d2_windowed_attn(
1078 e: &Engine,
1079 q: &CudaSlice<f32>,
1080 k: &CudaSlice<f32>,
1081 v: &CudaSlice<f32>,
1082 attn: &mut CudaSlice<f32>,
1083 hd: usize,
1084 nh: usize,
1085 nkv: usize,
1086 t: usize,
1087 t_kv: usize,
1088 scale: f32,
1089 c: &DflashCfg,
1090) -> Result<(), Box<dyn std::error::Error>> {
1091 if dflash2_sdpa_clip_on() {
1092 e.sdpa_naive_w_lo(
1093 q,
1094 k,
1095 v,
1096 attn,
1097 hd,
1098 nh,
1099 nkv,
1100 t,
1101 t_kv,
1102 scale,
1103 false,
1104 c.sliding_window,
1105 )
1106 } else {
1107 e.sdpa_naive_w(
1108 q,
1109 k,
1110 v,
1111 attn,
1112 hd,
1113 nh,
1114 nkv,
1115 t,
1116 t_kv,
1117 scale,
1118 false,
1119 c.sliding_window,
1120 )
1121 }
1122}
1123
1124fn bf16_to_f32(bytes: &[u8]) -> Vec<f32> {
1125 bytes
1126 .chunks_exact(2)
1127 .map(|c| f32::from_bits((u16::from_le_bytes([c[0], c[1]]) as u32) << 16))
1128 .collect()
1129}
1130
1131/// Host q8_0 encode (ggml block layout: [d f16][32 x i8] = 34B/32 vals). The drafter's
1132/// weights ride the dp4a fast path at 1.6GB resident (bf16 3.1GB + the 31B trunk OOM'd
1133/// 24GB; f32 6.2GB worse). Drafter quantization moves ACCEPTANCE only — verify exactness
1134/// is structural.
1135fn encode_q8_0(vals: &[f32]) -> Vec<u8> {
1136 let mut out = Vec::with_capacity(vals.len() / 32 * 34);
1137 for blk in vals.chunks_exact(32) {
1138 let amax = blk.iter().fold(0f32, |a, v| a.max(v.abs()));
1139 let d = amax / 127.0;
1140 let id = if d > 0.0 { 1.0 / d } else { 0.0 };
1141 let dh = half_from_f32(d);
1142 out.extend_from_slice(&dh.to_le_bytes());
1143 for &v in blk {
1144 out.push(((v * id).round().clamp(-127.0, 127.0)) as i8 as u8);
1145 }
1146 }
1147 out
1148}
1149
1150/// Host q4_0 encode (ggml: [d f16][16B packed nibbles] = 18B/32 vals; q = round(v/d)+8,
1151/// d = amax/-7 sign trick NOT used — plain amax/7? ggml uses d = max/-8 .. follow ggml:
1152/// d = amax / -8 when the max is negative-dominant; reference quantize_row_q4_0: d =
1153/// max(|v|)/-8 signed-max form). Implemented to match ggml quantize_row_q4_0_ref.
1154fn encode_q4_0(vals: &[f32]) -> Vec<u8> {
1155 let mut out = Vec::with_capacity(vals.len() / 32 * 18);
1156 for blk in vals.chunks_exact(32) {
1157 // ggml ref: pick the value with the LARGEST |v| (keeping sign), d = that / -8
1158 let mut amax = 0f32;
1159 let mut mx = 0f32;
1160 for &v in blk {
1161 if v.abs() > amax {
1162 amax = v.abs();
1163 mx = v;
1164 }
1165 }
1166 let d = mx / -8.0;
1167 let id = if d != 0.0 { 1.0 / d } else { 0.0 };
1168 out.extend_from_slice(&half_from_f32(d).to_le_bytes());
1169 for j in 0..16 {
1170 let x0 = (blk[j] * id + 8.5).clamp(0.0, 15.0) as u8;
1171 let x1 = (blk[j + 16] * id + 8.5).clamp(0.0, 15.0) as u8;
1172 out.push(x0 | (x1 << 4));
1173 }
1174 }
1175 out
1176}
1177
1178fn half_from_f32(v: f32) -> u16 {
1179 // f32 -> IEEE f16 (round-to-nearest-even; range of q8_0 d values is tame)
1180 let b = v.to_bits();
1181 let sign = ((b >> 16) & 0x8000) as u16;
1182 let exp = ((b >> 23) & 0xff) as i32 - 127 + 15;
1183 let man = b & 0x7fffff;
1184 if exp <= 0 {
1185 return sign;
1186 } // flush tiny d to zero
1187 if exp >= 31 {
1188 return sign | 0x7c00;
1189 } // inf (unreachable for sane d)
1190 let mut h = sign | ((exp as u16) << 10) | ((man >> 13) as u16);
1191 // round to nearest even on the truncated 13 bits
1192 let rem = man & 0x1fff;
1193 if rem > 0x1000 || (rem == 0x1000 && (h & 1) == 1) {
1194 h += 1;
1195 }
1196 h
1197}
1198
1199impl DflashDraft {
1200 /// Load the backbone-only checkpoint dir (config.json + model.safetensors, bf16).
1201 /// Config scalars ride a minimal extractor (no json dep in-tree — HfConfig precedent).
1202 pub fn load(e: &Engine, dir: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
1203 let txt = std::fs::read_to_string(dir.join("config.json"))?;
1204 fn num(txt: &str, key: &str) -> Option<f64> {
1205 let i = txt.find(&format!("\"{key}\""))?;
1206 let rest = &txt[i..];
1207 let colon = rest.find(':')?;
1208 let val: String = rest[colon + 1..]
1209 .trim_start()
1210 .chars()
1211 .take_while(|c| {
1212 c.is_ascii_digit()
1213 || *c == '.'
1214 || *c == '-'
1215 || *c == 'e'
1216 || *c == 'E'
1217 || *c == '+'
1218 })
1219 .collect();
1220 val.parse().ok()
1221 }
1222 fn num_list(txt: &str, key: &str) -> Vec<usize> {
1223 let Some(i) = txt.find(&format!("\"{key}\"")) else {
1224 return Vec::new();
1225 };
1226 let rest = &txt[i..];
1227 let (Some(a), Some(b)) = (rest.find('['), rest.find(']')) else {
1228 return Vec::new();
1229 };
1230 rest[a + 1..b]
1231 .split(',')
1232 .filter_map(|s| s.trim().parse().ok())
1233 .collect()
1234 }
1235 /// Substring of the JSON OBJECT value of a top-level key (brace-balanced) —
1236 /// the explicit scoped parse the DFlash2 census demands: `dflash_config` and
1237 /// `rope_parameters` are nested objects, and finding their keys by global
1238 /// `txt.find` is luck, not a contract (DFLASH2-EVAL-20260820.md §5.1).
1239 fn scope<'a>(txt: &'a str, key: &str) -> Option<&'a str> {
1240 let i = txt.find(&format!("\"{key}\""))?;
1241 let rest = &txt[i..];
1242 let open = rest.find('{')?;
1243 let mut depth = 0usize;
1244 for (j, ch) in rest[open..].char_indices() {
1245 match ch {
1246 '{' => depth += 1,
1247 '}' => {
1248 depth -= 1;
1249 if depth == 0 {
1250 return Some(&rest[open..open + j + 1]);
1251 }
1252 }
1253 _ => {}
1254 }
1255 }
1256 None
1257 }
1258 // Family detection is the ARCHITECTURES string, not tensor presence: a DFlash2
1259 // checkpoint whose new tensors were stripped must REFUSE, not degrade into the
1260 // 58-tensor untrained program (DFLASH2-EVAL-20260820.md §3).
1261 let is_dflash2 = {
1262 let arch = scope_list(&txt, "architectures");
1263 arch.contains("DFlash2DraftModel")
1264 };
1265 fn scope_list(txt: &str, key: &str) -> String {
1266 let Some(i) = txt.find(&format!("\"{key}\"")) else {
1267 return String::new();
1268 };
1269 let rest = &txt[i..];
1270 match (rest.find('['), rest.find(']')) {
1271 (Some(a), Some(b)) if a < b => rest[a + 1..b].to_string(),
1272 _ => String::new(),
1273 }
1274 }
1275 // DFlash2 scalars parse from their OWN scopes; other families keep the
1276 // historical global-find behavior byte-identically.
1277 let d2_cfg_txt: Option<&str> = if is_dflash2 {
1278 Some(scope(&txt, "dflash_config").unwrap_or_else(|| {
1279 panic!("DFlash2DraftModel config.json has no dflash_config object — refusing")
1280 }))
1281 } else {
1282 None
1283 };
1284 let g = |k: &str| num(&txt, k).unwrap_or_else(|| panic!("config missing {k}")) as usize;
1285 let g2 = |k: &str| -> usize {
1286 let t = d2_cfg_txt.expect("dflash2 scope");
1287 num(t, k).unwrap_or_else(|| panic!("dflash_config missing {k} — refusing")) as usize
1288 };
1289 // layer_types order: count entries, mark sliding ones
1290 let layer_sliding: Vec<bool> = {
1291 let i = txt.find("\"layer_types\"").expect("layer_types");
1292 let rest = &txt[i..];
1293 let (a, b) = (rest.find('[').unwrap(), rest.find(']').unwrap());
1294 rest[a + 1..b]
1295 .split(',')
1296 .map(|s| s.contains("sliding_attention"))
1297 .collect()
1298 };
1299 // sliding_window is null on all-full-attention exports (q38 arm-a); the window
1300 // only constrains rounds when a sliding layer exists (reference: resolve_dflash_
1301 // attention_layout returns None when no layer slides).
1302 let sliding_window = if layer_sliding.iter().any(|&s| s) {
1303 g("sliding_window")
1304 } else {
1305 num(&txt, "sliding_window")
1306 .map(|v| v as usize)
1307 .unwrap_or(usize::MAX)
1308 };
1309 // Explicit top-level is_causal (z-lab reference: overrides the layer-type
1310 // default). Parsed as a bare bool; absent = None (historical arms unchanged).
1311 let is_causal = txt
1312 .find("\"is_causal\"")
1313 .and_then(|i| txt[i..].find(':').map(|c| i + c + 1))
1314 .map(|v| txt[v..].trim_start().starts_with("true"));
1315 let cfg = DflashCfg {
1316 hidden: g("hidden_size"),
1317 n_head: g("num_attention_heads"),
1318 n_kv: g("num_key_value_heads"),
1319 head_dim: g("head_dim"),
1320 n_ff: g("intermediate_size"),
1321 n_layer: g("num_hidden_layers"),
1322 eps: num(&txt, "rms_norm_eps").expect("rms_norm_eps") as f32,
1323 // DFlash2 (transformers-5 style): rope_theta lives in the nested
1324 // rope_parameters object — parse it from its scope, not by global find.
1325 rope_theta: if is_dflash2 {
1326 let rp = scope(&txt, "rope_parameters")
1327 .unwrap_or_else(|| panic!("DFlash2 config has no rope_parameters — refusing"));
1328 assert!(
1329 rp.contains("\"default\""),
1330 "DFlash2 rope_parameters rope_type is not \"default\" — the port \
1331 implements plain neox rope only; refusing ({rp})"
1332 );
1333 num(rp, "rope_theta").expect("rope_parameters.rope_theta") as f32
1334 } else {
1335 num(&txt, "rope_theta").expect("rope_theta") as f32
1336 },
1337 block_size: if is_dflash2 {
1338 g2("block_size")
1339 } else {
1340 g("block_size")
1341 },
1342 mask_token_id: if is_dflash2 {
1343 g2("mask_token_id")
1344 } else {
1345 g("mask_token_id")
1346 } as u32,
1347 target_layer_ids: if is_dflash2 {
1348 num_list(d2_cfg_txt.expect("dflash2 scope"), "target_layer_ids")
1349 } else {
1350 num_list(&txt, "target_layer_ids")
1351 },
1352 sliding_window,
1353 layer_sliding,
1354 strategy_dspark: dspark_strategy_census(&txt),
1355 is_causal,
1356 };
1357 if is_dflash2 {
1358 // The windowed round arm implements the reference's NON-causal symmetric
1359 // window only (config `is_causal: false` on the q38 DFlash2 export). A
1360 // causal DFlash2 variant is a different mask program — refuse it rather
1361 // than run the wrong one fluently.
1362 assert_eq!(
1363 cfg.is_causal,
1364 Some(false),
1365 "DFlash2 port requires explicit config is_causal=false \
1366 (non-causal symmetric sliding window); got {is_causal:?} — refusing"
1367 );
1368 assert!(
1369 cfg.layer_sliding.iter().all(|&s| s),
1370 "DFlash2 port expects all layers sliding_attention (q38 export); \
1371 got {:?} — refusing (unverified mask program)",
1372 cfg.layer_sliding
1373 );
1374 assert!(
1375 cfg.block_size <= cfg.sliding_window,
1376 "DFlash2 block {} exceeds the sliding window {} — the windowed SDPA \
1377 omits the future-side mask because block rows stay within the window",
1378 cfg.block_size,
1379 cfg.sliding_window
1380 );
1381 }
1382 let st = memra_gguf::safetensors::StModel::open(&dir.join("model.safetensors"))?;
1383 // 1D norm weights ride raw slices; 2D matmul weights ride GpuTensor::Float
1384 // (cuBLASLt f32 arm — the Stage-A numeric class, right for oracle parity).
1385 let up = |name: &str| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1386 let (_info, bytes) = st
1387 .raw(name)
1388 .ok_or_else(|| format!("missing tensor {name}"))?;
1389 e.htod(&bf16_to_f32(bytes))
1390 };
1391 // Precision policy (MEMRA_DFLASH_PREC seam): "q4" = all q4_0 (DEFAULT since
1392 // lane/dflash2-head-trim 2026-08-25, owner-ratified): measured on BOTH engaging
1393 // card classes at unchanged acceptance — RTX PRO 6000 dspark_q38_gate x3
1394 // interleaved 157.9 vs q8 152.4 spec tok/s (accept 0.662 vs 0.656, ALL EXACT;
1395 // darklanes research/dflash2-pro6000-20260824/prec-ladder + trim cells) and the
1396 // 5090 rig cell that shipped the arm (PR #41). "q8" = all q8_0 (1.6GB, the
1397 // pre-flip default = the rollback seam); "mixed" = bf16 attn+fc (the
1398 // ctx-conditioning path) + q8_0 ffn (~2.2GB — fits the ~2.8GB headroom beside
1399 // the 31B trunk); "bf16" = all bf16 (parity runs, no target). The asymmetric
1400 // "q5" arm was measured DEFECTIVE (acceptance 0.656 -> 0.424) and never landed.
1401 let prec_env = std::env::var("MEMRA_DFLASH_PREC").ok();
1402 let prec = dflash_precision(prec_env.as_deref())?;
1403 let upw = |name: &str| -> Result<GpuTensor, Box<dyn std::error::Error>> {
1404 let (info, bytes) = st
1405 .raw(name)
1406 .ok_or_else(|| format!("missing tensor {name}"))?;
1407 let shape = info.ne(); // ggml order: ne[0]=in_f, ne[1]=out_f
1408 let in_f = shape[0] as usize;
1409 let is_ffn = name.contains(".mlp.");
1410 let bf16 = prec == "bf16"
1411 || (prec == "mixed" && !is_ffn)
1412 || (prec == "fc" && name == "fc.weight");
1413 if bf16 {
1414 return Ok(GpuTensor::FloatBf16 {
1415 data: e.upload_u8(bytes)?,
1416 ne: shape.to_vec(),
1417 });
1418 }
1419 let f32s = bf16_to_f32(bytes);
1420 if prec == "q4" {
1421 let q = encode_q4_0(&f32s);
1422 return Ok(GpuTensor::Quant {
1423 bytes: e.upload_u8(&q)?,
1424 qtype: crate::QT_Q4_0,
1425 row_bytes: in_f / 32 * 18,
1426 ne: shape.to_vec(),
1427 scale: 1.0,
1428 rp: false,
1429 #[cfg(memra_cutlass)]
1430 cutlass: None,
1431 fp8: None,
1432 blk: None,
1433 rp4: None,
1434 f16: None,
1435 });
1436 }
1437 let q = encode_q8_0(&f32s);
1438 Ok(GpuTensor::Quant {
1439 bytes: e.upload_u8(&q)?,
1440 qtype: crate::QT_Q8_0,
1441 row_bytes: in_f / 32 * 34,
1442 ne: shape.to_vec(),
1443 scale: 1.0,
1444 rp: false,
1445 #[cfg(memra_cutlass)]
1446 cutlass: None,
1447 fp8: None,
1448 blk: None,
1449 rp4: None,
1450 f16: None,
1451 })
1452 };
1453 let mut layers = Vec::with_capacity(cfg.n_layer);
1454 for i in 0..cfg.n_layer {
1455 let p = |s: &str| format!("layers.{i}.{s}");
1456 layers.push(DflashLayer {
1457 wq: upw(&p("self_attn.q_proj.weight"))?,
1458 wk: upw(&p("self_attn.k_proj.weight"))?,
1459 wv: upw(&p("self_attn.v_proj.weight"))?,
1460 wo: upw(&p("self_attn.o_proj.weight"))?,
1461 w_gate: upw(&p("mlp.gate_proj.weight"))?,
1462 w_up: upw(&p("mlp.up_proj.weight"))?,
1463 w_down: upw(&p("mlp.down_proj.weight"))?,
1464 ln_in: up(&p("input_layernorm.weight"))?,
1465 ln_post: up(&p("post_attention_layernorm.weight"))?,
1466 q_norm: up(&p("self_attn.q_norm.weight"))?,
1467 k_norm: up(&p("self_attn.k_norm.weight"))?,
1468 });
1469 }
1470 let markov = if let Some((info, bytes)) = st.raw("markov_head.markov_w1.weight") {
1471 let sh = info.ne(); // [rank, vocab] in ggml order (safetensors [V, rank] reversed)
1472 let (rank, vocab) = (sh[0] as usize, sh[1] as usize);
1473 let (i2, b2) = st
1474 .raw("markov_head.markov_w2.weight")
1475 .ok_or("markov_w2 missing beside markov_w1")?;
1476 // w2 follows the precision seam: bf16 for parity runs (the q8_0 encode is a
1477 // serving-size choice and would put quant error inside the markov-logits gate),
1478 // q8_0 otherwise (acceptance-only impact, like the trunk weights).
1479 let w2 = if prec == "bf16" {
1480 GpuTensor::FloatBf16 {
1481 data: e.upload_u8(b2)?,
1482 ne: i2.ne().to_vec(),
1483 }
1484 } else {
1485 let w2f = bf16_to_f32(b2);
1486 let w2q = encode_q8_0(&w2f);
1487 GpuTensor::Quant {
1488 bytes: e.upload_u8(&w2q)?,
1489 qtype: crate::QT_Q8_0,
1490 row_bytes: rank / 32 * 34,
1491 ne: vec![rank as u64, vocab as u64],
1492 scale: 1.0,
1493 rp: false,
1494 #[cfg(memra_cutlass)]
1495 cutlass: None,
1496 fp8: None,
1497 blk: None,
1498 rp4: None,
1499 f16: None,
1500 }
1501 };
1502 Some(MarkovHead {
1503 w1_bf16: e.upload_u8(bytes)?,
1504 w2,
1505 rank,
1506 vocab,
1507 })
1508 } else {
1509 None
1510 };
1511 let confidence = if let Some((info, bytes)) = st.raw("confidence_head.proj.weight") {
1512 let sh = info.ne(); // ggml order: ne[0]=in_dim, ne[1]=1
1513 let in_dim = sh[0] as usize;
1514 let (_bi, bb) = st
1515 .raw("confidence_head.proj.bias")
1516 .ok_or("confidence bias missing beside weight")?;
1517 let with_markov = markov
1518 .as_ref()
1519 .map(|m| in_dim == cfg.hidden + m.rank)
1520 .unwrap_or(false);
1521 if !with_markov && in_dim != cfg.hidden {
1522 panic!(
1523 "confidence_head in_dim {in_dim} matches neither hidden {} nor hidden+rank",
1524 cfg.hidden
1525 );
1526 }
1527 Some(ConfidenceHead {
1528 w: bf16_to_f32(bytes),
1529 b: bf16_to_f32(bb)[0],
1530 in_dim,
1531 with_markov,
1532 })
1533 } else {
1534 None
1535 };
1536 // ---- DFlash2 family tensors (DFLASH2-EVAL-20260820.md §2): 10 conv modules
1537 // (base_kernel + kernel_projection around attention AND mlp in EVERY layer) +
1538 // the candidate path selector (hidden_projection + two codebooks). REQUIRED
1539 // when the arch says DFlash2DraftModel: a missing tensor is a refusal (`?`),
1540 // never a degraded program.
1541 let dflash2 = if is_dflash2 {
1542 assert!(
1543 markov.is_none() && confidence.is_none(),
1544 "DFlash2 checkpoint carries markov/confidence tensors — no such \
1545 variant exists in the family (census refuses the ambiguity)"
1546 );
1547 let rank = g2("selector_rank");
1548 let top_k = g2("selector_top_k");
1549 let conv_k = g2("conv_kernel_size");
1550 let group_size = g2("conv_group_size");
1551 let groups = cfg.hidden / group_size;
1552 let load_conv = |name: &str| -> Result<Dflash2Conv, Box<dyn std::error::Error>> {
1553 let (bi, bb) = st
1554 .raw(&format!("{name}.base_kernel"))
1555 .ok_or_else(|| format!("DFlash2 census: missing {name}.base_kernel"))?;
1556 // safetensors [2, k, hidden] -> ggml ne reversed [hidden, k, 2]
1557 let bne = bi.ne();
1558 assert_eq!(
1559 (bne[0] as usize, bne[1] as usize, bne[2] as usize),
1560 (cfg.hidden, conv_k, 2),
1561 "{name}.base_kernel shape != [2, conv_kernel_size, hidden]"
1562 );
1563 let pname = format!("{name}.kernel_projection.weight");
1564 let (pi, _pb) = st
1565 .raw(&pname)
1566 .ok_or_else(|| format!("DFlash2 census: missing {pname}"))?;
1567 let pne = pi.ne(); // ggml: [in_f=hidden, out_f=2*k*groups]
1568 assert_eq!(
1569 (pne[0] as usize, pne[1] as usize),
1570 (cfg.hidden, 2 * conv_k * groups),
1571 "{pname} shape != [2*conv_kernel_size*groups, hidden]"
1572 );
1573 Ok(Dflash2Conv {
1574 base: e.htod(&bf16_to_f32(bb))?,
1575 proj: upw(&pname)?,
1576 })
1577 };
1578 let mut attn_conv = Vec::with_capacity(cfg.n_layer);
1579 let mut mlp_conv = Vec::with_capacity(cfg.n_layer);
1580 for i in 0..cfg.n_layer {
1581 attn_conv.push(load_conv(&format!("layers.{i}.attention_conv"))?);
1582 mlp_conv.push(load_conv(&format!("layers.{i}.mlp_conv"))?);
1583 }
1584 // Codebooks: stored WITHOUT `.weight` (checkpoint quirk; reference
1585 // from_pretrained maps the keys). Host-resident raw bf16.
1586 let cb = |name: &str| -> Result<(Vec<u8>, usize), Box<dyn std::error::Error>> {
1587 let (ci, cbytes) = st
1588 .raw(&format!("candidate_selector.{name}"))
1589 .ok_or_else(|| format!("DFlash2 census: missing candidate_selector.{name}"))?;
1590 let ne = ci.ne(); // ggml: [rank, V]
1591 assert_eq!(ne[0] as usize, rank, "candidate_selector.{name} rank");
1592 Ok((cbytes.to_vec(), ne[1] as usize))
1593 };
1594 let (pred_codebook, v1) = cb("predecessor_codebook")?;
1595 let (succ_codebook, v2) = cb("successor_codebook")?;
1596 assert_eq!(v1, v2, "codebook vocab mismatch");
1597 let hp_name = "candidate_selector.hidden_projection.weight";
1598 let (hi, _hb) = st
1599 .raw(hp_name)
1600 .ok_or_else(|| format!("DFlash2 census: missing {hp_name}"))?;
1601 assert_eq!(
1602 (hi.ne()[0] as usize, hi.ne()[1] as usize),
1603 (cfg.hidden, rank),
1604 "{hp_name} shape != [rank, hidden]"
1605 );
1606 Some(Dflash2Head {
1607 attn_conv,
1608 mlp_conv,
1609 hidden_proj: upw(hp_name)?,
1610 pred_codebook,
1611 succ_codebook,
1612 rank,
1613 top_k,
1614 conv_k,
1615 group_size,
1616 vocab: v1,
1617 })
1618 } else {
1619 None
1620 };
1621 // CENSUS GATE: every tensor in the export must be consumed by the map above.
1622 // DSpark-class checkpoints (markov head present) and DFlash2 checkpoints
1623 // REFUSE on unrecognized names — an unmapped tensor is a semantic program we
1624 // would silently drop (house law). Plain dflash checkpoints keep the
1625 // historical warn-only behavior.
1626 {
1627 let mut consumed: std::collections::HashSet<String> = std::collections::HashSet::new();
1628 for i in 0..cfg.n_layer {
1629 for s in [
1630 "self_attn.q_proj.weight",
1631 "self_attn.k_proj.weight",
1632 "self_attn.v_proj.weight",
1633 "self_attn.o_proj.weight",
1634 "self_attn.q_norm.weight",
1635 "self_attn.k_norm.weight",
1636 "input_layernorm.weight",
1637 "post_attention_layernorm.weight",
1638 "mlp.gate_proj.weight",
1639 "mlp.up_proj.weight",
1640 "mlp.down_proj.weight",
1641 ] {
1642 consumed.insert(format!("layers.{i}.{s}"));
1643 }
1644 if dflash2.is_some() {
1645 for s in [
1646 "attention_conv.base_kernel",
1647 "attention_conv.kernel_projection.weight",
1648 "mlp_conv.base_kernel",
1649 "mlp_conv.kernel_projection.weight",
1650 ] {
1651 consumed.insert(format!("layers.{i}.{s}"));
1652 }
1653 }
1654 }
1655 for s in [
1656 "fc.weight",
1657 "hidden_norm.weight",
1658 "norm.weight",
1659 "markov_head.markov_w1.weight",
1660 "markov_head.markov_w2.weight",
1661 "confidence_head.proj.weight",
1662 "confidence_head.proj.bias",
1663 ] {
1664 consumed.insert(s.into());
1665 }
1666 if dflash2.is_some() {
1667 for s in [
1668 "candidate_selector.hidden_projection.weight",
1669 "candidate_selector.predecessor_codebook",
1670 "candidate_selector.successor_codebook",
1671 ] {
1672 consumed.insert(s.into());
1673 }
1674 }
1675 let leftovers: Vec<&String> = st.names().filter(|n| !consumed.contains(*n)).collect();
1676 if !leftovers.is_empty() {
1677 if markov.is_some() || dflash2.is_some() {
1678 panic!("dspark/dflash2 census: unrecognized tensors {leftovers:?}");
1679 }
1680 eprintln!("[dflash census] unmapped tensors (ignored): {leftovers:?}");
1681 }
1682 }
1683 // YaRN rope from config rope_parameters (HF _compute_yarn_parameters, verified
1684 // numerically vs Qwen3RotaryEmbedding on the arm-a export).
1685 let rope_yarn =
1686 if txt.contains("\"rope_type\": \"yarn\"") || txt.contains("\"rope_type\":\"yarn\"") {
1687 let factor = num(&txt, "factor").expect("yarn factor") as f64;
1688 let orig = num(&txt, "original_max_position_embeddings").expect("yarn orig");
1689 let beta_fast = num(&txt, "beta_fast").expect("beta_fast");
1690 let beta_slow = num(&txt, "beta_slow").expect("beta_slow");
1691 let base = cfg.rope_theta as f64;
1692 let d = cfg.head_dim as f64;
1693 let corr =
1694 |r: f64| d * (orig / (r * 2.0 * std::f64::consts::PI)).ln() / (2.0 * base.ln());
1695 let low = corr(beta_fast).floor().max(0.0);
1696 let high = corr(beta_slow).ceil().min(d - 1.0);
1697 let half = cfg.head_dim / 2;
1698 let mut ff = Vec::with_capacity(half);
1699 for j in 0..half {
1700 let base_inv = base.powf(-2.0 * j as f64 / d);
1701 let ramp = (((j as f64) - low) / (high - low)).clamp(0.0, 1.0);
1702 let ex = 1.0 - ramp; // extrapolation share
1703 let yarn_inv = (base_inv / factor) * (1.0 - ex) + base_inv * ex;
1704 ff.push((base_inv / yarn_inv) as f32);
1705 }
1706 let mscale = (0.1 * factor.ln() + 1.0) as f32;
1707 Some((e.htod(&ff)?, mscale))
1708 } else {
1709 None
1710 };
1711 let fc = upw("fc.weight")?;
1712 // Ratified-default receipts (capacity-keyed-defaults law: the active program is
1713 // NAMED at load, never inferred from silence). The boot output-sample gate greps
1714 // these lines; a run whose log lacks them did not load this code.
1715 eprintln!(
1716 "[dspark] precision={prec} (MEMRA_DFLASH_PREC {})",
1717 if prec_env.is_some() { "set" } else { "unset" },
1718 );
1719 eprintln!(
1720 "[dspark] harvest={} (checkpoint census dflash2={} strategy_dspark={}, \
1721 MEMRA_DSPARK_HARVEST {})",
1722 DsparkHarvest::for_family_value(
1723 dflash2.is_some(),
1724 std::env::var("MEMRA_DSPARK_HARVEST").ok().as_deref(),
1725 cfg.strategy_dspark,
1726 )
1727 .name(),
1728 dflash2.is_some(),
1729 cfg.strategy_dspark,
1730 match std::env::var("MEMRA_DSPARK_HARVEST") {
1731 Ok(v) if !v.is_empty() => "set",
1732 _ => "unset",
1733 },
1734 );
1735 eprintln!(
1736 "[dspark] verify-window={:?} (accept-rate head {}, MEMRA_DSPARK_VT {})",
1737 DsparkVtPolicy::resolve(confidence.is_some()),
1738 if confidence.is_some() {
1739 "present"
1740 } else {
1741 "ABSENT -> ladder"
1742 },
1743 match std::env::var("MEMRA_DSPARK_VT") {
1744 Ok(v) if !v.is_empty() => "set",
1745 _ => "unset",
1746 },
1747 );
1748 Ok(Self {
1749 fc,
1750 hidden_norm: up("hidden_norm.weight")?,
1751 norm: up("norm.weight")?,
1752 cfg,
1753 layers,
1754 markov,
1755 confidence,
1756 rope_yarn,
1757 dflash2,
1758 })
1759 }
1760
1761 /// Rope q or k rows in place: yarn (ff divisors + post-rope mscale) when the config
1762 /// carries it, plain neox otherwise. One primitive for all five drafter rope sites.
1763 fn rope_rows(
1764 &self,
1765 e: &Engine,
1766 x: &mut CudaSlice<f32>,
1767 pos_d: &CudaSlice<i32>,
1768 n_heads: usize,
1769 n_tokens: usize,
1770 ) -> Result<(), Box<dyn std::error::Error>> {
1771 let c = &self.cfg;
1772 match &self.rope_yarn {
1773 Some((ff, mscale)) => {
1774 e.rope_neox_ff(
1775 x,
1776 pos_d,
1777 c.head_dim,
1778 c.head_dim,
1779 n_heads,
1780 n_tokens,
1781 c.rope_theta,
1782 1.0,
1783 ff,
1784 )?;
1785 e.scale_inplace(x, *mscale, n_tokens * n_heads * c.head_dim)?;
1786 }
1787 None => {
1788 e.rope_neox(
1789 x,
1790 pos_d,
1791 c.head_dim,
1792 c.head_dim,
1793 n_heads,
1794 n_tokens,
1795 c.rope_theta,
1796 1.0,
1797 )?;
1798 }
1799 }
1800 Ok(())
1801 }
1802
1803 /// f32 GEMM helper via the engine Float arm (cuBLASLt): y[t, out_f].
1804 fn mm(
1805 &self,
1806 e: &Engine,
1807 w: &GpuTensor,
1808 x: &CudaSlice<f32>,
1809 t: usize,
1810 _in_f: usize,
1811 _out_f: usize,
1812 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1813 e.matmul(w, x, t)
1814 }
1815
1816 /// DFlash2 conv `prepare` (reference GroupedDynamicCausalConv.prepare): projects
1817 /// the pre-conv rows to BOTH dynamic kernels, convolves the rows with base half 0
1818 /// + dyn half 0, and returns (convolved rows, the dyn projection) — `finish`
1819 /// reuses the SAME projection's half 1. Block-local causal shift (row 0 zero-pads).
1820 pub fn d2_conv_prepare(
1821 &self,
1822 e: &Engine,
1823 conv: &Dflash2Conv,
1824 xn: &CudaSlice<f32>,
1825 rows: usize,
1826 ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
1827 let d2 = self
1828 .dflash2
1829 .as_ref()
1830 .expect("d2_conv on a non-dflash2 draft");
1831 let h = self.cfg.hidden;
1832 let groups = h / d2.group_size;
1833 let dyn_ = self.mm(e, &conv.proj, xn, rows, h, 2 * d2.conv_k * groups)?;
1834 let mut out = e.uninit(rows * h)?;
1835 e.dflash2_dynconv(
1836 xn,
1837 &dyn_,
1838 &conv.base,
1839 &mut out,
1840 rows,
1841 h,
1842 d2.group_size,
1843 d2.conv_k,
1844 0,
1845 )?;
1846 Ok((out, dyn_))
1847 }
1848
1849 /// DFlash2 conv `finish`: convolves the sublayer OUTPUT rows with base half 1 +
1850 /// dyn half 1 (dyn from the matching `prepare`).
1851 pub fn d2_conv_finish(
1852 &self,
1853 e: &Engine,
1854 conv: &Dflash2Conv,
1855 y: &CudaSlice<f32>,
1856 dyn_: &CudaSlice<f32>,
1857 rows: usize,
1858 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1859 let d2 = self
1860 .dflash2
1861 .as_ref()
1862 .expect("d2_conv on a non-dflash2 draft");
1863 let h = self.cfg.hidden;
1864 let mut out = e.uninit(rows * h)?;
1865 e.dflash2_dynconv(
1866 y,
1867 dyn_,
1868 &conv.base,
1869 &mut out,
1870 rows,
1871 h,
1872 d2.group_size,
1873 d2.conv_k,
1874 1,
1875 )?;
1876 Ok(out)
1877 }
1878
1879 /// DFlash2 proposal (reference `DFlash2DraftModel.propose`, greedy arm): device
1880 /// top-k over the draft logits + the rank-`r` hidden projection, ONE small dtoh
1881 /// (~nd*(2k+rank) floats — the same per-round sync slot the markov chain's token
1882 /// readback occupies), then the host codebook walk. Returns the nd drafted tokens
1883 /// (mask-fill rows 1..b-1; the anchor row is not a draft).
1884 #[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI/call contract; bundling into a struct is a refactor, not a lint fix
1885 pub fn dflash2_propose_greedy(
1886 &self,
1887 e: &Engine,
1888 dl: &CudaSlice<f32>,
1889 rows: &CudaSlice<f32>,
1890 nd: usize,
1891 n_vocab: usize,
1892 anchor: u32,
1893 d2t: Option<&[u32]>,
1894 ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
1895 Ok(self
1896 .dflash2_propose_greedy_q(e, dl, rows, nd, n_vocab, anchor, d2t)?
1897 .0)
1898 }
1899
1900 /// [`Self::dflash2_propose_greedy`] with the walk's per-slot confidence returned
1901 /// (lane/glm5-loop-port, 2026-08-30): q[p] = the chosen candidate's softmax mass over
1902 /// its slot's candidate set at T=1 — the statistic the glm5 loop's MEMRA_SPEC_PMIN
1903 /// tau-slot truncation thresholds on. Same walk, same path, same one-DtoH sync slot.
1904 #[allow(clippy::too_many_arguments)]
1905 // allow: mirrors the greedy propose contract it wraps
1906 pub fn dflash2_propose_greedy_q(
1907 &self,
1908 e: &Engine,
1909 dl: &CudaSlice<f32>,
1910 rows: &CudaSlice<f32>,
1911 nd: usize,
1912 n_vocab: usize,
1913 anchor: u32,
1914 d2t: Option<&[u32]>,
1915 ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
1916 let d2 = self
1917 .dflash2
1918 .as_ref()
1919 .expect("dflash2_propose on a non-dflash2 draft");
1920 assert!(
1921 n_vocab <= d2.vocab,
1922 "target head vocab {n_vocab} exceeds the selector codebooks ({})",
1923 d2.vocab
1924 );
1925 let (vals_d, idx_d) = e.topk_rows(dl, nd, n_vocab, d2.top_k)?;
1926 let hproj_d = e.matmul(&d2.hidden_proj, rows, nd)?;
1927 let unary = e.dtoh(&vals_d)?;
1928 let mut cand = e.dtoh_u32(&idx_d)?;
1929 // TRIMMED draft head (lane/dflash2-head-trim, 2026-08-25): `dl` was scored over the
1930 // FR-Spec-gathered rows, so candidate index i names trimmed row i — remap to the true
1931 // token id BEFORE the selector walk (the codebooks and the verify block index the full
1932 // vocabulary). Same permute-the-proposal law as the MTP arm's spec.rs d2t map; verify
1933 // stays full-vocab, so the trim moves acceptance only, never output.
1934 if let Some(map) = d2t {
1935 for c in cand.iter_mut() {
1936 *c = map[*c as usize];
1937 }
1938 }
1939 let hproj = e.dtoh(&hproj_d)?;
1940 Ok(d2.walk_greedy_q(&unary, &cand, &hproj, anchor, nd))
1941 }
1942
1943 /// DFlash2 proposal, SAMPLED arm (reference `DFlash2DraftModel.propose` at T>0): same
1944 /// device top-k + hidden projection + one dtoh as the greedy arm, then the host
1945 /// candidate-set softmax walk (`dflash2_walk_sampled`) drawing one host-Philox uniform
1946 /// per slot from the session's `uctr` stream. Returns (path, q_chosen, cand, q_rows).
1947 #[allow(clippy::too_many_arguments)]
1948 pub(crate) fn dflash2_propose_sampled(
1949 &self,
1950 e: &Engine,
1951 dl: &CudaSlice<f32>,
1952 rows: &CudaSlice<f32>,
1953 nd: usize,
1954 n_vocab: usize,
1955 anchor: u32,
1956 temp: f32,
1957 seed: u64,
1958 uctr: &mut u32,
1959 d2t: Option<&[u32]>,
1960 ) -> Result<Dflash2SampledProposal, Box<dyn std::error::Error>> {
1961 let d2 = self
1962 .dflash2
1963 .as_ref()
1964 .expect("dflash2_propose on a non-dflash2 draft");
1965 assert!(
1966 n_vocab <= d2.vocab,
1967 "target head vocab {n_vocab} exceeds the selector codebooks ({})",
1968 d2.vocab
1969 );
1970 let (vals_d, idx_d) = e.topk_rows(dl, nd, n_vocab, d2.top_k)?;
1971 let hproj_d = e.matmul(&d2.hidden_proj, rows, nd)?;
1972 let unary = e.dtoh(&vals_d)?;
1973 let mut cand = e.dtoh_u32(&idx_d)?;
1974 // Trimmed-head remap — see the greedy arm. The q the walk reports is the softmax
1975 // over the candidate SET it actually proposed (ids are labels, not indices into a
1976 // distribution), so the rejection-verify contract is unchanged by the remap.
1977 if let Some(map) = d2t {
1978 for c in cand.iter_mut() {
1979 *c = map[*c as usize];
1980 }
1981 }
1982 let hproj = e.dtoh(&hproj_d)?;
1983 let mut draw = || {
1984 let u = crate::spec::host_u01(seed, *uctr);
1985 *uctr = uctr.wrapping_add(1);
1986 u
1987 };
1988 let (path, q_chosen, q_rows) =
1989 d2.walk_sampled(&unary, &cand, &hproj, anchor, nd, temp, &mut draw);
1990 Ok((path, q_chosen, cand, q_rows))
1991 }
1992
1993 /// Sampled draft chain for the Rows families (T>0 twin of the greedy markov chain):
1994 /// slot k gets the markov bias of the PREVIOUS chain token added in place (when the
1995 /// head is armed — the sglang DSPARK worker's markov-corrected draft probs), then ONE
1996 /// draw from the row's FILTERED softmax (filter_stats -> device-stat gumbel perturb ->
1997 /// argmax into the chain buffer — the frspec eager-chain composition, stats kept on
1998 /// device so the chain stays sync-free like the greedy arm). Without a markov head the
1999 /// rows sample independently (the z-lab reference's T>0 arm for plain DFlash). `dl` is
2000 /// biased IN PLACE and retained by the caller: it is the accept walk's q source.
2001 #[allow(clippy::too_many_arguments)]
2002 pub(crate) fn dspark_chain_sampled(
2003 &self,
2004 e: &Engine,
2005 dl: &mut CudaSlice<f32>,
2006 nd: usize,
2007 n_vocab: usize,
2008 anchor: u32,
2009 sp: &crate::spec::SpecSampling,
2010 sctr: &mut u32,
2011 // H4 confidence-policy stash (v0.100 train merge): Some = copy each slot's
2012 // markov prev-token embedding (the exact `w1` row the chain gathers) into a
2013 // [nd, rank] buffer — the same d2d stash the greedy chain carries, so the
2014 // confidence window sizes identically at T>0.
2015 mut conf_emb: Option<&mut CudaSlice<f32>>,
2016 ) -> Result<(Vec<u32>, DsparkDraftSample), Box<dyn std::error::Error>> {
2017 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
2018 let mut chain_d = e.stream().alloc_zeros::<u32>(nd + 1)?;
2019 e.set_u32_one(&mut chain_d, anchor)?;
2020 let mut th_all = e.zeros(nd)?;
2021 let mut z_all = e.zeros(nd)?;
2022 let mut mx_all = e.zeros(nd)?;
2023 let mut pb = e.zeros(n_vocab)?;
2024 for k in 0..nd {
2025 if let (Some(mk), true) = (&self.markov, markov_on) {
2026 let mut f = e.uninit(mk.rank)?;
2027 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
2028 if let Some(ce) = conf_emb.as_deref_mut() {
2029 let fv = e.view(&f, mk.rank);
2030 e.copy_view_into(ce, k * mk.rank, &fv, mk.rank)?;
2031 }
2032 let bias = e.matmul(&mk.w2, &f, 1)?;
2033 e.add_row_inplace(dl, &bias, n_vocab, k * n_vocab)?;
2034 } else if let (Some(ce), Some(mk)) = (conf_emb.as_deref_mut(), &self.markov) {
2035 // MARKOV=0 arm still stashes the embedding for the confidence head —
2036 // the greedy chain's exact behavior.
2037 let mut f = e.uninit(mk.rank)?;
2038 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
2039 let fv = e.view(&f, mk.rank);
2040 e.copy_view_into(ce, k * mk.rank, &fv, mk.rank)?;
2041 }
2042 let rows_k = e.htod_i32(&[k as i32])?;
2043 let (mut th1, mut z1, mut mx1) = (e.zeros(1)?, e.zeros(1)?, e.zeros(1)?);
2044 e.filter_stats(
2045 dl, n_vocab, &rows_k, &mut th1, &mut z1, &mut mx1, n_vocab, 1, sp.temp, sp.top_k,
2046 sp.top_p, sp.min_p,
2047 )?;
2048 e.gumbel_perturb_filtered_col(
2049 dl, k, &mut pb, n_vocab, sp.seed, *sctr, sp.temp, &mx1, &th1, 0,
2050 )?;
2051 *sctr = sctr.wrapping_add(1);
2052 e.argmax_token_device_col(&pb, 0, n_vocab, &mut chain_d, k + 1)?;
2053 e.copy_into(&mut th_all, k, &th1, 1)?;
2054 e.copy_into(&mut z_all, k, &z1, 1)?;
2055 e.copy_into(&mut mx_all, k, &mx1, 1)?;
2056 }
2057 let chain = e.dtoh_u32(&chain_d)?;
2058 let (thv, zv, mxv) = (e.dtoh(&th_all)?, e.dtoh(&z_all)?, e.dtoh(&mx_all)?);
2059 let stats = (0..nd).map(|i| (mxv[i], thv[i], zv[i])).collect();
2060 Ok((
2061 chain[1..].to_vec(),
2062 DsparkDraftSample::Rows {
2063 th: th_all,
2064 z: z_all,
2065 stats,
2066 },
2067 ))
2068 }
2069
2070 /// Family dispatch for the sampled proposal: Selector for DFlash2, Rows otherwise.
2071 /// Returns the drafted tokens (the round's `cand` tail) + the proposal record.
2072 #[allow(clippy::too_many_arguments)]
2073 pub(crate) fn dspark_propose_sampled(
2074 &self,
2075 e: &Engine,
2076 dl: &mut CudaSlice<f32>,
2077 rows: &CudaSlice<f32>,
2078 nd: usize,
2079 n_vocab: usize,
2080 anchor: u32,
2081 sp: &crate::spec::SpecSampling,
2082 sctr: &mut u32,
2083 uctr: &mut u32,
2084 conf_emb: Option<&mut CudaSlice<f32>>,
2085 d2t: Option<&[u32]>,
2086 ) -> Result<(Vec<u32>, DsparkDraftSample), Box<dyn std::error::Error>> {
2087 if let Some(d2) = self.dflash2.as_ref() {
2088 // The confidence stash is a markov-family program; DFlash2 has no
2089 // accept-rate head (the policy resolver never arms it for this family).
2090 debug_assert!(
2091 conf_emb.is_none(),
2092 "conf_emb stash requested on a DFlash2 selector proposal"
2093 );
2094 let (path, q_chosen, cand, q_rows) = self.dflash2_propose_sampled(
2095 e, dl, rows, nd, n_vocab, anchor, sp.temp, sp.seed, uctr, d2t,
2096 )?;
2097 Ok((
2098 path,
2099 DsparkDraftSample::Selector {
2100 cand,
2101 q_rows,
2102 q_chosen,
2103 top_k: d2.top_k,
2104 },
2105 ))
2106 } else {
2107 self.dspark_chain_sampled(e, dl, nd, n_vocab, anchor, sp, sctr, conf_emb)
2108 }
2109 }
2110
2111 /// FIRST-LIGHT forward (oracle contract): full non-causal attention over
2112 /// [ctx_features ; block], NO draft KV cache, NO sliding window (the oracle bypasses
2113 /// the reference mask machinery the same way — window/caching land in the round arm).
2114 ///
2115 /// `target_hidden`: [ctx, n_taps*hidden] (f32, device) — raw tapped states.
2116 /// `noise_emb`: [block, hidden] — target embed rows for [accepted, MASK x b-1].
2117 /// `pos`: absolute positions for ctx rows THEN block rows (ctx+block i32).
2118 /// Returns final normed hidden [block, hidden] (feed target lm_head for draft logits).
2119 /// ctx features for `t` tapped rows: hidden_norm(fc(taps)) — the drafter's context
2120 /// representation, cacheable across rounds (append-only in committed-token order).
2121 pub fn ctx_features(
2122 &self,
2123 e: &Engine,
2124 taps: &CudaSlice<f32>,
2125 t: usize,
2126 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2127 let c = &self.cfg;
2128 let n_taps = c.target_layer_ids.len();
2129 let fc_out = self.mm(e, &self.fc, taps, t, n_taps * c.hidden, c.hidden)?;
2130 let mut out = e.uninit(t * c.hidden)?;
2131 e.rms_norm(&fc_out, &self.hidden_norm, &mut out, c.hidden, t, c.eps)?;
2132 Ok(out)
2133 }
2134
2135 pub fn forward(
2136 &self,
2137 e: &Engine,
2138 target_hidden: &CudaSlice<f32>,
2139 noise_emb: &CudaSlice<f32>,
2140 pos: &[i32],
2141 ctx: usize,
2142 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2143 let ctx_f = self.ctx_features(e, target_hidden, ctx)?;
2144 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
2145 let v = e.dtoh(&ctx_f)?;
2146 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2147 std::fs::write(format!("{dir}/memra-ctx_features.f32"), bytes)?;
2148 }
2149 self.forward_block(e, &ctx_f, noise_emb, pos, ctx)
2150 }
2151
2152 /// Block forward over PRECOMPUTED ctx features (the round arm's entry: features are
2153 /// cached across rounds; only the block work repeats).
2154 pub fn forward_block(
2155 &self,
2156 e: &Engine,
2157 ctx_f: &CudaSlice<f32>,
2158 noise_emb: &CudaSlice<f32>,
2159 pos: &[i32],
2160 ctx: usize,
2161 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2162 let c = &self.cfg;
2163 let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
2164 let b = c.block_size;
2165 assert_eq!(pos.len(), ctx + b, "pos covers ctx rows then block rows");
2166
2167 let pos_blk = e.htod_i32(&pos[ctx..])?;
2168
2169 let mut x = e.clone_dtod(noise_emb)?; // [b, hidden] residual stream
2170 for (li, l) in self.layers.iter().enumerate() {
2171 // input_layernorm on the block rows only (ctx features are norm-free per ref:
2172 // k/v project the SAME ctx_f every layer, un-layernormed).
2173 let mut xn = e.uninit(b * h)?;
2174 e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
2175 // DFlash2: dynamic conv WRAPS attention — q/k_noise/v_noise all project the
2176 // CONVOLVED block rows (reference decoder layer: prepare -> self_attn ->
2177 // finish, all inside the residual branch). ctx_f is never convolved.
2178 let mut attn_dyn: Option<CudaSlice<f32>> = None;
2179 if let Some(d2) = &self.dflash2 {
2180 let (xc, dyn_) = self.d2_conv_prepare(e, &d2.attn_conv[li], &xn, b)?;
2181 xn = xc;
2182 attn_dyn = Some(dyn_);
2183 }
2184
2185 // q from block; k/v from [ctx_f ; block-normed]
2186 let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
2187 let k0c = self.mm(e, &l.wk, ctx_f, ctx, h, nkv * hd)?;
2188 let v0c = self.mm(e, &l.wv, ctx_f, ctx, h, nkv * hd)?;
2189 let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
2190 let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
2191
2192 // per-head q/k rms norm (v passes through: ones weight trick not needed — the
2193 // qkv kernel norms rq+rk rows; concatenate k first).
2194 let mut k0 = e.uninit((ctx + b) * nkv * hd)?;
2195 e.copy_into(&mut k0, 0, &k0c, ctx * nkv * hd)?;
2196 e.copy_into(&mut k0, ctx * nkv * hd, &k0b, b * nkv * hd)?;
2197 let mut v = e.uninit((ctx + b) * nkv * hd)?;
2198 e.copy_into(&mut v, 0, &v0c, ctx * nkv * hd)?;
2199 e.copy_into(&mut v, ctx * nkv * hd, &v0b, b * nkv * hd)?;
2200
2201 if li == 0
2202 && let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP")
2203 {
2204 let v = e.dtoh(&q0)?;
2205 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2206 std::fs::write(format!("{dir}/memra-l0_q0.f32"), bytes)?;
2207 }
2208 let mut q = e.uninit(b * nh * hd)?;
2209 let mut k = e.uninit((ctx + b) * nkv * hd)?;
2210 // rms over head_dim rows: q has b*nh rows, k has (ctx+b)*nkv rows.
2211 e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
2212 if li == 0
2213 && let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP")
2214 {
2215 let v = e.dtoh(&q)?;
2216 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2217 std::fs::write(format!("{dir}/memra-l0_qn.f32"), bytes)?;
2218 }
2219 e.rms_norm(&k0, &l.k_norm, &mut k, hd, (ctx + b) * nkv, c.eps)?;
2220
2221 // rope: q at block positions, k at ctx-then-block positions (absolute).
2222 let norope = std::env::var("MEMRA_DFLASH_NOROPE").is_ok();
2223 if !norope {
2224 self.rope_rows(e, &mut q, &pos_blk, nh, b)?;
2225 }
2226 if li == 0
2227 && let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP")
2228 {
2229 let dump = |name: &str,
2230 t: &cudarc::driver::CudaSlice<f32>|
2231 -> Result<(), Box<dyn std::error::Error>> {
2232 let v = e.dtoh(t)?;
2233 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2234 std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
2235 Ok(())
2236 };
2237 dump("xn", &xn)?;
2238 dump("q_prerope", &q)?;
2239 }
2240 // k rows are laid out [row, nkv, hd] with row-major tokens — rope_neox expects
2241 // (n_heads, n_tokens); ctx and block ropes run as one call over ctx+b tokens.
2242 let pos_all = e.htod_i32(pos)?;
2243 if !norope {
2244 self.rope_rows(e, &mut k, &pos_all, nkv, ctx + b)?;
2245 }
2246
2247 // full non-causal attention: every block query sees all ctx+b keys.
2248 let mut attn = e.uninit(b * nh * hd)?;
2249 let scale = 1.0f32 / (hd as f32).sqrt();
2250 // NAIVE SDPA for first light: fa_prefill's NON-CAUSAL arm with T != T_kv is
2251 // BROKEN (attn maxdiff 0.34 vs the torch oracle; q/k inputs bit-close — no
2252 // existing caller exercises that shape class, jsonl 2026-07-13). The 16 x
2253 // (ctx+16) block attention is tiny; the fa arm returns behind this seam once
2254 // its kernel is fixed + parity-gated.
2255 if self.dflash2.is_some() && c.layer_sliding[li] {
2256 // DFlash2 non-causal symmetric window (config is_causal=false, all
2257 // layers sliding). The kernel masks only keys OLDER than
2258 // q_pos-(window-1); the future side (k - q < window) never binds
2259 // because keys reach at most q_pos + block <= q_pos + window
2260 // (asserted at load). Positions must be contiguous — q_pos is derived
2261 // in-kernel as (T_kv - T) + qt.
2262 debug_assert!(pos.windows(2).all(|w| w[1] == w[0] + 1));
2263 d2_windowed_attn(e, &q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, c)?;
2264 } else if std::env::var("MEMRA_DFLASH_FA").is_ok() {
2265 e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
2266 } else {
2267 e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, b, ctx + b, scale, false)?;
2268 }
2269
2270 let mut o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
2271 if let (Some(d2), Some(dyn_)) = (&self.dflash2, &attn_dyn) {
2272 o = self.d2_conv_finish(e, &d2.attn_conv[li], &o, dyn_, b)?;
2273 }
2274 let mut x1 = e.uninit(b * h)?;
2275 e.add(&o, &x, &mut x1, b * h)?;
2276 if li == 0
2277 && let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP")
2278 {
2279 let dump = |name: &str,
2280 t: &cudarc::driver::CudaSlice<f32>|
2281 -> Result<(), Box<dyn std::error::Error>> {
2282 let v = e.dtoh(t)?;
2283 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2284 std::fs::write(format!("{dir}/memra-l0_{name}.f32"), bytes)?;
2285 Ok(())
2286 };
2287 dump("q", &q)?;
2288 dump("k", &k)?;
2289 dump("attn", &attn)?;
2290 dump("x1", &x1)?;
2291 }
2292
2293 // mlp (DFlash2: the same conv wrap — prepare on the post-ln rows, mlp on
2294 // the convolved rows, finish on the mlp output, then the residual add)
2295 let mut x1n = e.uninit(b * h)?;
2296 e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
2297 let mut mlp_dyn: Option<CudaSlice<f32>> = None;
2298 if let Some(d2) = &self.dflash2 {
2299 let (xc, dyn_) = self.d2_conv_prepare(e, &d2.mlp_conv[li], &x1n, b)?;
2300 x1n = xc;
2301 mlp_dyn = Some(dyn_);
2302 }
2303 let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
2304 let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
2305 let mut act = e.uninit(b * c.n_ff)?;
2306 e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
2307 let mut down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
2308 if let (Some(d2), Some(dyn_)) = (&self.dflash2, &mlp_dyn) {
2309 down = self.d2_conv_finish(e, &d2.mlp_conv[li], &down, dyn_, b)?;
2310 }
2311 let mut x2 = e.uninit(b * h)?;
2312 e.add(&down, &x1, &mut x2, b * h)?;
2313 x = x2;
2314 if let Ok(dir) = std::env::var("MEMRA_DFLASH_DUMP") {
2315 let v = e.dtoh(&x)?;
2316 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2317 std::fs::write(format!("{dir}/memra-layer{li}_out.f32"), bytes)?;
2318 }
2319 }
2320 let mut out = e.uninit(b * h)?;
2321 e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
2322 Ok(out)
2323 }
2324}
2325
2326/// Draft KV cache (round-cost fix, 2026-07-13): per-layer normed+roped ctx K and raw ctx V,
2327/// append-only in committed order. Block K/V land TRANSIENTLY at [len..len+b] each round
2328/// (never committed — the reference crops them identically). Kills the per-round full-ctx
2329/// projection recompute (first light was O(ctx)/round -> 7 tok/s).
2330pub struct DflashKv {
2331 pub k: Vec<CudaSlice<f32>>, // per layer [cap + block, nkv*hd]
2332 pub v: Vec<CudaSlice<f32>>,
2333 pub len: usize,
2334 pub cap: usize,
2335 /// Trailing rows the drafter can still observe: `sliding_window + block_size`. Carried on
2336 /// the KV (not recomputed at call sites) so an export and an import cannot disagree about
2337 /// the geometry — see `DsparkSpecSession::draft_tail_rows`.
2338 window_rows: usize,
2339 /// `n_kv * head_dim * size_of::<f32>()` — the row unit for tail copies.
2340 row_bytes: usize,
2341}
2342
2343impl DflashKv {
2344 pub fn new(
2345 e: &Engine,
2346 cfg: &DflashCfg,
2347 cap: usize,
2348 ) -> Result<Self, Box<dyn std::error::Error>> {
2349 let rowsz = cfg.n_kv * cfg.head_dim;
2350 let mut k = Vec::with_capacity(cfg.n_layer);
2351 let mut v = Vec::with_capacity(cfg.n_layer);
2352 for _ in 0..cfg.n_layer {
2353 k.push(e.uninit((cap + cfg.block_size) * rowsz)?);
2354 v.push(e.uninit((cap + cfg.block_size) * rowsz)?);
2355 }
2356 Ok(Self {
2357 k,
2358 v,
2359 len: 0,
2360 cap,
2361 window_rows: cfg.sliding_window.saturating_add(cfg.block_size),
2362 row_bytes: rowsz * std::mem::size_of::<f32>(),
2363 })
2364 }
2365}
2366
2367impl DflashDraft {
2368 /// Ingest `t` NEW ctx-feature rows (committed order, absolute positions `pos_new`) into
2369 /// the draft KV: per layer k/v projections + k head-norm + rope, appended at kv.len.
2370 pub fn ingest_ctx(
2371 &self,
2372 e: &Engine,
2373 kv: &mut DflashKv,
2374 feats: &CudaSlice<f32>,
2375 pos_new: &[i32],
2376 t: usize,
2377 ) -> Result<(), Box<dyn std::error::Error>> {
2378 let c = &self.cfg;
2379 let (h, nkv, hd) = (c.hidden, c.n_kv, c.head_dim);
2380 assert!(kv.len + t <= kv.cap, "draft kv overflow");
2381 let pos_d = e.htod_i32(pos_new)?;
2382 for (li, l) in self.layers.iter().enumerate() {
2383 let k0 = self.mm(e, &l.wk, feats, t, h, nkv * hd)?;
2384 let v0 = self.mm(e, &l.wv, feats, t, h, nkv * hd)?;
2385 let mut kn = e.uninit(t * nkv * hd)?;
2386 e.rms_norm(&k0, &l.k_norm, &mut kn, hd, t * nkv, c.eps)?;
2387 self.rope_rows(e, &mut kn, &pos_d, nkv, t)?;
2388 e.copy_into(&mut kv.k[li], kv.len * nkv * hd, &kn, t * nkv * hd)?;
2389 e.copy_into(&mut kv.v[li], kv.len * nkv * hd, &v0, t * nkv * hd)?;
2390 }
2391 kv.len += t;
2392 Ok(())
2393 }
2394
2395 /// Block forward over the CACHED ctx KV: only the 16 block rows are projected per layer;
2396 /// block K/V land transiently at kv[len..len+b]. Bit-class-identical to forward_block
2397 /// (same kernels, same per-row programs; ONLY the ctx K/V recompute is cached).
2398 pub fn forward_round(
2399 &self,
2400 e: &Engine,
2401 kv: &mut DflashKv,
2402 noise_emb: &CudaSlice<f32>,
2403 pos_block: &[i32],
2404 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2405 let c = &self.cfg;
2406 let (h, nh, nkv, hd) = (c.hidden, c.n_head, c.n_kv, c.head_dim);
2407 let b = c.block_size;
2408 assert_eq!(pos_block.len(), b);
2409 let ctx = kv.len;
2410 let pos_blk = e.htod_i32(pos_block)?;
2411 let mut x = e.clone_dtod(noise_emb)?;
2412 for (li, l) in self.layers.iter().enumerate() {
2413 let mut xn = e.uninit(b * h)?;
2414 e.rms_norm(&x, &l.ln_in, &mut xn, h, b, c.eps)?;
2415 // DFlash2: dynamic conv wraps attention (see forward_block).
2416 let mut attn_dyn: Option<CudaSlice<f32>> = None;
2417 if let Some(d2) = &self.dflash2 {
2418 let (xc, dyn_) = self.d2_conv_prepare(e, &d2.attn_conv[li], &xn, b)?;
2419 xn = xc;
2420 attn_dyn = Some(dyn_);
2421 }
2422 let q0 = self.mm(e, &l.wq, &xn, b, h, nh * hd)?;
2423 let k0b = self.mm(e, &l.wk, &xn, b, h, nkv * hd)?;
2424 let v0b = self.mm(e, &l.wv, &xn, b, h, nkv * hd)?;
2425 let mut q = e.uninit(b * nh * hd)?;
2426 let mut kb = e.uninit(b * nkv * hd)?;
2427 e.rms_norm(&q0, &l.q_norm, &mut q, hd, b * nh, c.eps)?;
2428 e.rms_norm(&k0b, &l.k_norm, &mut kb, hd, b * nkv, c.eps)?;
2429 self.rope_rows(e, &mut q, &pos_blk, nh, b)?;
2430 self.rope_rows(e, &mut kb, &pos_blk, nkv, b)?;
2431 e.copy_into(&mut kv.k[li], ctx * nkv * hd, &kb, b * nkv * hd)?;
2432 e.copy_into(&mut kv.v[li], ctx * nkv * hd, &v0b, b * nkv * hd)?;
2433 let mut attn = e.uninit(b * nh * hd)?;
2434 let scale = 1.0f32 / (hd as f32).sqrt();
2435 if self.dflash2.is_some() && c.layer_sliding[li] {
2436 // Non-causal symmetric window (config is_causal=false): kv row index
2437 // == absolute position for BOTH ctx rows (committed order) and the
2438 // transient block rows, so the kernel's q_pos = (T_kv - T) + qt is the
2439 // absolute position and the old-side mask is exact. The future side
2440 // never binds (block <= window, asserted at load).
2441 d2_windowed_attn(
2442 e,
2443 &q,
2444 &kv.k[li],
2445 &kv.v[li],
2446 &mut attn,
2447 hd,
2448 nh,
2449 nkv,
2450 b,
2451 ctx + b,
2452 scale,
2453 c,
2454 )?;
2455 } else if std::env::var("MEMRA_DFLASH_FA").is_ok() {
2456 e.fa_prefill(
2457 &q,
2458 &kv.k[li],
2459 &kv.v[li],
2460 &mut attn,
2461 hd,
2462 nh,
2463 nkv,
2464 b,
2465 ctx + b,
2466 scale,
2467 false,
2468 )?;
2469 } else {
2470 e.sdpa_naive(
2471 &q,
2472 &kv.k[li],
2473 &kv.v[li],
2474 &mut attn,
2475 hd,
2476 nh,
2477 nkv,
2478 b,
2479 ctx + b,
2480 scale,
2481 false,
2482 )?;
2483 }
2484 let mut o = self.mm(e, &l.wo, &attn, b, nh * hd, h)?;
2485 if let (Some(d2), Some(dyn_)) = (&self.dflash2, &attn_dyn) {
2486 o = self.d2_conv_finish(e, &d2.attn_conv[li], &o, dyn_, b)?;
2487 }
2488 let mut x1 = e.uninit(b * h)?;
2489 e.add(&o, &x, &mut x1, b * h)?;
2490 let mut x1n = e.uninit(b * h)?;
2491 e.rms_norm(&x1, &l.ln_post, &mut x1n, h, b, c.eps)?;
2492 let mut mlp_dyn: Option<CudaSlice<f32>> = None;
2493 if let Some(d2) = &self.dflash2 {
2494 let (xc, dyn_) = self.d2_conv_prepare(e, &d2.mlp_conv[li], &x1n, b)?;
2495 x1n = xc;
2496 mlp_dyn = Some(dyn_);
2497 }
2498 let gate = self.mm(e, &l.w_gate, &x1n, b, h, c.n_ff)?;
2499 let up_ = self.mm(e, &l.w_up, &x1n, b, h, c.n_ff)?;
2500 let mut act = e.uninit(b * c.n_ff)?;
2501 e.silu_mul(&gate, &up_, &mut act, b * c.n_ff)?;
2502 let mut down = self.mm(e, &l.w_down, &act, b, c.n_ff, h)?;
2503 if let (Some(d2), Some(dyn_)) = (&self.dflash2, &mlp_dyn) {
2504 down = self.d2_conv_finish(e, &d2.mlp_conv[li], &down, dyn_, b)?;
2505 }
2506 let mut x2 = e.uninit(b * h)?;
2507 e.add(&down, &x1, &mut x2, b * h)?;
2508 x = x2;
2509 }
2510 let mut out = e.uninit(b * h)?;
2511 e.rms_norm(&x, &self.norm, &mut out, h, b, c.eps)?;
2512 Ok(out)
2513 }
2514}
2515
2516/// Emit an accepted draft run under the `max_new` budget: check BEFORE each push — at
2517/// real acceptance the final round often accepts a draft at the boundary, and
2518/// push-then-check emitted max_new+1 tokens (plain emits exactly max_new; the E2E gate
2519/// read it as a length divergence at index max_new with the shared prefix
2520/// byte-identical). f8300340cd fixed generate_spec_dspark this way; generate_spec_dflash
2521/// kept the buggy shape until the hermes sweep (fixed 2026-08-23) — both now share this
2522/// one helper. Returns true when the caller must break (budget reached or EOS emitted).
2523fn emit_accepted_run(out: &mut Vec<u32>, accepted: &[u32], eos: &[u32], max_new: usize) -> bool {
2524 for &dt in accepted {
2525 if out.len() >= max_new {
2526 return true;
2527 }
2528 out.push(dt);
2529 if eos.contains(&dt) {
2530 return true;
2531 }
2532 }
2533 false
2534}
2535
2536#[cfg(test)]
2537mod emit_budget_tests {
2538 use super::emit_accepted_run;
2539
2540 #[test]
2541 fn accepted_run_never_exceeds_max_new() {
2542 // TOOTH (hermes finding, fixed 2026-08-23): the dflash accept loop pushed THEN
2543 // checked, emitting max_new+1 whenever the final round accepted at the boundary.
2544 let mut out = vec![1, 2, 3]; // 3 committed, budget 4: exactly ONE slot left
2545 let stop = emit_accepted_run(&mut out, &[10, 11, 12], &[], 4);
2546 assert!(stop, "hitting the budget must break the round loop");
2547 assert_eq!(
2548 out,
2549 vec![1, 2, 3, 10],
2550 "exactly max_new tokens, never max_new+1"
2551 );
2552 // EOS inside the run stops after emitting it (unchanged semantics).
2553 let mut out = vec![1];
2554 let stop = emit_accepted_run(&mut out, &[10, 99, 12], &[99], 8);
2555 assert!(stop);
2556 assert_eq!(out, vec![1, 10, 99]);
2557 // A run fitting the budget with no EOS lets the round continue.
2558 let mut out = vec![1];
2559 assert!(!emit_accepted_run(&mut out, &[10, 11], &[], 8));
2560 assert_eq!(out, vec![1, 10, 11]);
2561 }
2562}
2563
2564// ================= DFlash spec round (greedy, first light) =================
2565// Exact contract: identical output stream to plain greedy decode BY CONSTRUCTION — the
2566// target's batched verify argmax decides every committed token; the drafter only proposes.
2567// (Same verify+rewind pattern as generate_spec_gemma's eager round; t=16 verify rides the
2568// straddle-split-safe fa_decode_rows.)
2569impl crate::hybrid::HybridModel {
2570 pub fn generate_spec_dflash(
2571 &self,
2572 e: &Engine,
2573 draft: &DflashDraft,
2574 prompt: &[u32],
2575 max_new: usize,
2576 eos: &[u32],
2577 ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
2578 self.refuse_hyper("generate_spec_dflash")?;
2579 use crate::cache::{Cache, DflashTapSink};
2580 let n_embd = self.cfg.n_embd as usize;
2581 let c = &draft.cfg;
2582 assert!(
2583 draft.dflash2.is_none(),
2584 "DFlash2 drafters ride the qwen-hybrid dspark round (selector + windowed \
2585 attention); the gemma arm has no consumer for the family's ops"
2586 );
2587 assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
2588 let b = c.block_size;
2589 let n_taps = c.target_layer_ids.len();
2590 let max_ctx = prompt.len() + max_new + b + 8;
2591 // First light holds ctx <= sliding_window: the draft was trained with 4 sliding
2592 // layers (window 2048) and the first-light attention is windowless full — inside
2593 // the window the two are identical. The depth cell (1736 + 128) fits.
2594 assert!(
2595 max_ctx <= c.sliding_window,
2596 "first-light dflash round is windowless — ctx cap {} exceeds the draft window {}",
2597 max_ctx,
2598 c.sliding_window
2599 );
2600 let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
2601
2602 // ---- prime with taps armed ----
2603 let tp = prompt.len();
2604 cache.dflash_taps = Some(DflashTapSink {
2605 layer_ids: c.target_layer_ids.clone(),
2606 buf: e.uninit(tp * n_taps * n_embd)?,
2607 hidden: n_embd,
2608 t: tp,
2609 base: 0,
2610 });
2611 let t_prime = std::time::Instant::now();
2612 let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
2613 let mut last = crate::forward::argmax(&logits) as u32;
2614 // draft KV cache: ingest the prompt's ctx features once; per round only the kept
2615 // rows ingest + the block projects (round cost O(block), not O(ctx)).
2616 let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
2617 {
2618 // CHUNKED ingest (depth OOM fix): the 1736-row prompt tap buffer is ~224MB f32;
2619 // running fc + 5-layer k/v projection over it in one shot stacks another
2620 // ~300MB of transients on the ~21.3GB trunk peak. 256-row windows bound the
2621 // transient set; identical values (row-independent ops).
2622 let taps = cache.dflash_taps.take().unwrap();
2623 let n_taps_h = n_taps * n_embd;
2624 let mut r0 = 0usize;
2625 while r0 < tp {
2626 let t_c = (tp - r0).min(256);
2627 let tv = e.view(&taps.buf, tp * n_taps_h);
2628 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
2629 let mut chunk = e.uninit(t_c * n_taps_h)?;
2630 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
2631 let f = draft.ctx_features(e, &chunk, t_c)?;
2632 let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
2633 draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
2634 r0 += t_c;
2635 }
2636 }
2637 let mut ctx_len = tp;
2638 e.stream().synchronize()?;
2639 // published prime wall (the run-spec/gemma-gate timing contract subtracts it)
2640 crate::PRIME_NANOS.store(
2641 t_prime.elapsed().as_nanos() as u64,
2642 std::sync::atomic::Ordering::Relaxed,
2643 );
2644
2645 // embed-scale seam (MEMRA_DFLASH_EMB_SCALE): gemma trunks scale embeddings by
2646 // sqrt(n_embd) INSIDE the forward; whether the z-lab gemma4 training fed the
2647 // drafter scaled or raw embed rows is not visible from the reference (qwen path
2648 // uses raw embed_tokens). Acceptance arbitrates; default raw.
2649 let emb_scale = if std::env::var("MEMRA_DFLASH_EMB_SCALE").as_deref() == Ok("1") {
2650 (n_embd as f32).sqrt()
2651 } else {
2652 1.0
2653 };
2654
2655 let mut out = Vec::with_capacity(max_new);
2656 let n_vocab = self.output.out_features();
2657 // VERIFY WIDTH (MEMRA_DFLASH_VERIFY_T, default 8): the drafter always drafts a full
2658 // block (its trained mask pattern) but only the first vt rows go through the target
2659 // verify — the t=16 verify rides the untuned b16 tier at ~32% of the byte wall
2660 // (65ms/verify) while b8 rides the tuned r2 tier; with ~2.7 committed/round the
2661 // deep block positions almost never survive anyway. Exactness unaffected (verify
2662 // still decides every committed token).
2663 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
2664 .ok()
2665 .and_then(|v| v.parse().ok())
2666 .unwrap_or(8)
2667 .clamp(2, b);
2668 // adaptive verify width (MEMRA_DFLASH_ADAPT!=0, MTP accepted+1 recipe): next round
2669 // verifies one past this round's accepted run, clamped [3, cap].
2670 let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
2671 let mut vt = vt_cap;
2672 let mut attempted = 0usize;
2673 let mut accepted = 0usize;
2674 // The whole round runs in the decode-exact matmul scope: the m=16 draft mms were
2675 // otherwise falling into the prefill-GEMM class (770us/matmul, 17% of the depth
2676 // round). Prime (before this loop) keeps the prefill GEMM path. RAII: a `?` exit
2677 // anywhere in the loop restores the pre-scope value instead of latching exact ON
2678 // engine-wide (hermes finding, fixed 2026-08-23).
2679 let exact_scope = e.exact_scope(true);
2680 'outer: while out.len() < max_new {
2681 let start = cache.pos; // committed length
2682 // ---- draft: block = [last, MASK x b-1] ----
2683 let mut block: Vec<u32> = vec![c.mask_token_id; b];
2684 block[0] = last;
2685 let mut noise = e.htod(&self.embd.gather(n_embd, &block))?;
2686 if emb_scale != 1.0 {
2687 e.scale_inplace(&mut noise, emb_scale, b * n_embd)?;
2688 }
2689 if std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1") && start == cache.pos {
2690 let nv = e.dtoh(&noise)?;
2691 let r0: f32 = nv[..n_embd].iter().map(|x| x * x).sum::<f32>().sqrt();
2692 let r1: f32 = nv[n_embd..2 * n_embd]
2693 .iter()
2694 .map(|x| x * x)
2695 .sum::<f32>()
2696 .sqrt();
2697 eprintln!(
2698 "[dflash noise] |row0(last)|={r0:.3} |row1(MASK id {})|={r1:.3}",
2699 c.mask_token_id
2700 );
2701 }
2702 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
2703 let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
2704 // draft tokens = argmax(lm_head(h rows 1..b))
2705 let mut rows = e.uninit((b - 1) * n_embd)?;
2706 {
2707 let dv = e.view(&dh, b * n_embd);
2708 let tail = dv.slice(n_embd..b * n_embd);
2709 e.copy_view_into(&mut rows, 0, &tail, (b - 1) * n_embd)?;
2710 }
2711 let mut dl = e.matmul(&self.output, &rows, b - 1)?;
2712 // SEMI-AR MARKOV CHAIN (DSpark head, when present + MEMRA_DFLASH_MARKOV!=0):
2713 // left-to-right, logits_k += W2(W1[prev realized token]) — the whole chain
2714 // stays on-device (chain_d[0] = the pending token; argmax k writes
2715 // chain_d[k+1], the k+1 bias gathers from it). Greedy mirror of the patch's
2716 // _markov_semiar_sample_block.
2717 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
2718 let mut chain_d = e.stream().alloc_zeros::<u32>(b)?;
2719 if let (Some(mk), true) = (&draft.markov, markov_on) {
2720 e.set_u32_one(&mut chain_d, last)?;
2721 for k in 0..(b - 1) {
2722 let mut f = e.uninit(mk.rank)?;
2723 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
2724 let bias = e.matmul(&mk.w2, &f, 1)?;
2725 e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
2726 e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
2727 }
2728 } else {
2729 for i in 0..(b - 1) {
2730 e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
2731 }
2732 }
2733 let chain = e.dtoh_u32(&chain_d)?;
2734 let dtoks = &chain[1..];
2735 for (i, &dt) in dtoks.iter().enumerate() {
2736 block[i + 1] = dt;
2737 }
2738 let dbg = std::env::var("MEMRA_DFLASH_DEBUG").as_deref() == Ok("1");
2739
2740 // ---- verify: one t=vt target forward with taps armed ----
2741 let vblock = &block[..vt];
2742 cache.dflash_taps = Some(DflashTapSink {
2743 layer_ids: c.target_layer_ids.clone(),
2744 buf: e.uninit(vt * n_taps * n_embd)?,
2745 hidden: n_embd,
2746 t: vt,
2747 base: 0,
2748 });
2749 let (vam, _vh) = self.gemma4_decode_step_t_am(e, vblock, start, &mut cache)?;
2750 let taps = cache.dflash_taps.take().unwrap();
2751 if dbg {
2752 eprintln!(
2753 "[dflash r] start={start} last={last}\n draft={:?}\n vam ={:?}",
2754 &block[1..],
2755 vam
2756 );
2757 }
2758
2759 // ---- accept ----
2760 let mut m = 0usize;
2761 while m < vt - 1 && block[m + 1] as usize == vam[m] as usize {
2762 m += 1;
2763 }
2764 attempted += vt - 1;
2765 accepted += m;
2766 out.push(last);
2767 if eos.contains(&last) {
2768 break 'outer;
2769 }
2770 if emit_accepted_run(&mut out, &block[1..=m], eos, max_new) {
2771 break 'outer;
2772 }
2773 let next = vam[m];
2774
2775 // ---- commit/rollback: keep m+1 of the b appended rows ----
2776 let keep = m + 1;
2777 for kvl in cache.kv.iter_mut().flatten() {
2778 kvl.len -= vt - keep;
2779 e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
2780 }
2781 cache.pos -= vt - keep;
2782
2783 // ---- ingest the kept rows' ctx features into the draft KV ----
2784 {
2785 let tv = e.view(&taps.buf, vt * n_taps * n_embd);
2786 let keep_view = tv.slice(0..keep * n_taps * n_embd);
2787 let mut kept = e.uninit(keep * n_taps * n_embd)?;
2788 e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
2789 let f = draft.ctx_features(e, &kept, keep)?;
2790 let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
2791 draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
2792 ctx_len += keep;
2793 }
2794 last = next;
2795 if adapt {
2796 vt = (m + 2).clamp(3, vt_cap);
2797 }
2798 }
2799 drop(exact_scope);
2800 if std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1") {
2801 eprintln!(
2802 "[dflash] acceptance {accepted}/{attempted} = {:.3}",
2803 accepted as f64 / attempted.max(1) as f64
2804 );
2805 }
2806 Ok(out)
2807 }
2808}
2809
2810// ================= Engine-bundle slice 1: batched GDN state snapshot ====================
2811// DSF-ROUNDCOST-20260820 §1.1 measured the dspark round's `cache.snapshot(e)` at 0.67 ms
2812// native wall — 48 linear layers x {conv, ssm} x (alloc_zeros + memcpy_dtod) of pure
2813// dispatch serialization, zero kernels. This batcher holds ONE persistent CacheSnapshot
2814// (buffers allocated on round 1, reused every round — kills the per-round alloc/memset
2815// churn) plus device pointer tables, so a round's snap is one small H2D table refresh
2816// (the ssm handles ping-pong per verify row, so live pointers are re-read each round;
2817// conv handles are rolled in place and never move) + TWO `copy_batch_uniform_f32`
2818// launches. Bytes, buffers and stream order are identical to `Cache::snapshot`; only the
2819// dispatch count changes, so acceptance and streams stay bit-identical (E2E-gated).
2820// `MEMRA_STATE_COPY_BATCH=0` reverts to the legacy per-layer snapshot.
2821
2822pub(crate) struct DsparkSnapBatch {
2823 pub(crate) snap: crate::cache::CacheSnapshot,
2824 /// Linear-attention layer indices, in `conv_table`/`ssm_table` order.
2825 lin: Vec<usize>,
2826 /// [src_0..src_{n-1}, dst_0..dst_{n-1}] — live conv states -> snapshot conv buffers.
2827 conv_table: CudaSlice<u64>,
2828 ssm_table: CudaSlice<u64>,
2829 host_ssm: Vec<u64>,
2830 conv_words: usize,
2831 ssm_words: usize,
2832}
2833
2834impl DsparkSnapBatch {
2835 /// Build from a fresh full snapshot (this IS round 1's snap — the caller uses
2836 /// `self.snap` directly after `new`). Returns None when the cache has no linear
2837 /// layers or their state sizes are non-uniform (a future hybrid shape) — the caller
2838 /// then stays on the legacy per-layer snapshot rather than copying wrong byte counts.
2839 pub(crate) fn new(
2840 e: &Engine,
2841 cache: &crate::cache::Cache,
2842 ) -> Result<Option<Self>, Box<dyn std::error::Error>> {
2843 use cudarc::driver::DevicePtr;
2844 let snap = cache.snapshot(e)?;
2845 let lin: Vec<usize> = (0..cache.recur.len())
2846 .filter(|&il| cache.recur[il].is_some())
2847 .collect();
2848 if lin.is_empty() {
2849 return Ok(None);
2850 }
2851 let first = cache.recur[lin[0]].as_ref().unwrap();
2852 let (conv_words, ssm_words) = (first.conv_state.len(), first.ssm_state.len());
2853 for &il in &lin {
2854 let rl = cache.recur[il].as_ref().unwrap();
2855 if rl.conv_state.len() != conv_words || rl.ssm_state.len() != ssm_words {
2856 return Ok(None);
2857 }
2858 }
2859 let n = lin.len();
2860 let mut host_conv = vec![0u64; 2 * n];
2861 let mut host_ssm = vec![0u64; 2 * n];
2862 {
2863 let s = &e.gpu.stream();
2864 for (k, &il) in lin.iter().enumerate() {
2865 let rl = cache.recur[il].as_ref().unwrap();
2866 let (pc, _g0) = rl.conv_state.device_ptr(s);
2867 let (ps, _g1) = rl.ssm_state.device_ptr(s);
2868 let (dc, _g2) = snap.conv[il].as_ref().unwrap().device_ptr(s);
2869 let (ds, _g3) = snap.ssm[il].as_ref().unwrap().device_ptr(s);
2870 host_conv[k] = pc;
2871 host_conv[n + k] = dc;
2872 host_ssm[k] = ps;
2873 host_ssm[n + k] = ds;
2874 }
2875 }
2876 let conv_table = e.htod_u64(&host_conv)?;
2877 let ssm_table = e.htod_u64(&host_ssm)?;
2878 Ok(Some(Self {
2879 snap,
2880 lin,
2881 conv_table,
2882 ssm_table,
2883 host_ssm,
2884 conv_words,
2885 ssm_words,
2886 }))
2887 }
2888
2889 /// The per-round snap: refresh kv lens/pos host-side (as `snapshot_into` does),
2890 /// re-read the live ssm handles into the table (gdn ping-pong moves them; the conv
2891 /// handles and every snapshot dst are stable), then two batched-copy launches.
2892 pub(crate) fn refresh(
2893 &mut self,
2894 e: &Engine,
2895 cache: &crate::cache::Cache,
2896 ) -> Result<(), Box<dyn std::error::Error>> {
2897 use cudarc::driver::DevicePtr;
2898 for il in 0..cache.kv.len() {
2899 self.snap.kv_len[il] = cache.kv[il].as_ref().map(|kvl| kvl.len);
2900 }
2901 self.snap.pos = cache.pos;
2902 let n = self.lin.len();
2903 {
2904 let s = &e.gpu.stream();
2905 for (k, &il) in self.lin.iter().enumerate() {
2906 let rl = cache.recur[il].as_ref().unwrap();
2907 let (ps, _g) = rl.ssm_state.device_ptr(s);
2908 self.host_ssm[k] = ps;
2909 }
2910 }
2911 e.htod_u64_into(&self.host_ssm, &mut self.ssm_table)?;
2912 e.copy_batch_uniform_f32(&self.conv_table, n, self.conv_words)?;
2913 e.copy_batch_uniform_f32(&self.ssm_table, n, self.ssm_words)?;
2914 Ok(())
2915 }
2916}
2917
2918// ================= DSpark spec round, QWEN-HYBRID target (lane/dspark-q38-recover) =====
2919// The q38 twin of generate_spec_dflash. Same drafter machinery (rounds, markov chain,
2920// draft KV, adaptive verify width); the TARGET side swaps gemma4's dense verify for the
2921// qwen serving-class verify funnel (dspark_verify_t_am) + snapshot/rollback, because the
2922// hybrid GDN conv/ssm state mutates in place — dense KV truncation cannot roll it back.
2923// Exactness contract unchanged: identical stream to plain greedy BY CONSTRUCTION (the
2924// target's verify argmax decides every committed token).
2925impl crate::hybrid::HybridModel {
2926 #[allow(clippy::type_complexity)] // allow: one-shot composite type; naming it would hide the shape that matters at the call site
2927 pub fn generate_spec_dspark(
2928 &self,
2929 e: &Engine,
2930 draft: &DflashDraft,
2931 prompt: &[u32],
2932 max_new: usize,
2933 eos: &[u32],
2934 sampling: Option<&crate::spec::SpecSampling>,
2935 ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
2936 self.refuse_hyper("generate_spec_dspark")?;
2937 use crate::cache::{Cache, DflashTapSink};
2938 assert!(
2939 !self.uses_gemma_program(),
2940 "gemma4 targets use generate_spec_dflash; this is the qwen-hybrid arm"
2941 );
2942 // SAMPLED ADMISSION (T>0, lane/dspark-sampled-admission-20260820): Some+temp>0
2943 // routes the round's proposal/accept through the rejection-sampling arms; None or
2944 // temp==0 keeps every greedy path byte-identical (the exactness instrument).
2945 let sp_on: Option<&crate::spec::SpecSampling> = sampling.filter(|s| s.temp > 0.0);
2946 // PENALTIES AT T==0 ARE A LOUD REFUSAL (lane/dspark-penalized-sampled-20260821):
2947 // the greedy walk argmaxes RAW verify columns, so a temp==0 config carrying
2948 // non-identity penalties would silently serve the UNPENALIZED greedy stream —
2949 // exactly the H-class silent-program-switch this route refuses everywhere else.
2950 // Penalized greedy stays on the plain path (worker admission owns the exclusion).
2951 if let Some(s) = sampling
2952 && s.temp <= 0.0
2953 && s.pen_on()
2954 {
2955 return Err(
2956 "dspark spec at temp==0 is the greedy route and would silently drop \
2957 the request's penalties; penalized greedy is served on the plain path"
2958 .into(),
2959 );
2960 }
2961 // Penalized-sampled state: the session window (pen_window_seed — one definition
2962 // across both spec routes), extended with every committed token; each round's
2963 // accept receives the trimmed tail (min(penalty_last_n, PEN_WINDOW_MAX)).
2964 let pen_on = sp_on.is_some_and(|s| s.pen_on());
2965 let mut pen_hist: Vec<u32> = if pen_on {
2966 crate::spec::pen_window_seed(&[], prompt, sp_on.unwrap().penalty_last_n)
2967 } else {
2968 Vec::new()
2969 };
2970 let (mut sctr, mut uctr) = (0u32, 0u32);
2971 let n_embd = self.cfg.n_embd as usize;
2972 let c = &draft.cfg;
2973 assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
2974 let b = c.block_size;
2975 let n_taps = c.target_layer_ids.len();
2976 let max_ctx = prompt.len() + max_new + b + 8;
2977 // DFlash2 implements the reference's non-causal symmetric sliding window in
2978 // the round attention (sdpa_naive_w), so depth past the window is admitted;
2979 // other families keep the historical windowless contract.
2980 assert!(
2981 draft.dflash2.is_some() || max_ctx <= c.sliding_window,
2982 "dspark round is windowless — ctx cap {} exceeds the draft window {}",
2983 max_ctx,
2984 c.sliding_window
2985 );
2986 let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
2987
2988 // ---- prime with taps armed (chunked prime writes at chunk offsets via sink.base) ----
2989 let tp = prompt.len();
2990 cache.dflash_taps = Some(DflashTapSink {
2991 layer_ids: c.target_layer_ids.clone(),
2992 buf: e.uninit(tp * n_taps * n_embd)?,
2993 hidden: n_embd,
2994 t: tp,
2995 base: 0,
2996 });
2997 let t_prime = std::time::Instant::now();
2998 let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
2999 // Boundary token: greedy takes the argmax (byte contract); sampled draws it from
3000 // the request's own filtered target through the session Philox stream — the same
3001 // shipped composition the frspec route uses (sample_check arm 9 oracles it).
3002 let mut last = match sp_on {
3003 Some(sp) => crate::spec::sample_boundary_token(
3004 e,
3005 &logits,
3006 sp,
3007 &pen_hist,
3008 &mut sctr,
3009 "dspark-prime",
3010 )?,
3011 None => crate::forward::argmax(&logits) as u32,
3012 };
3013 let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
3014 {
3015 let taps = cache.dflash_taps.take().unwrap();
3016 let n_taps_h = n_taps * n_embd;
3017 let mut r0 = 0usize;
3018 while r0 < tp {
3019 let t_c = (tp - r0).min(256);
3020 let tv = e.view(&taps.buf, tp * n_taps_h);
3021 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
3022 let mut chunk = e.uninit(t_c * n_taps_h)?;
3023 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
3024 let f = draft.ctx_features(e, &chunk, t_c)?;
3025 let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
3026 draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
3027 r0 += t_c;
3028 }
3029 }
3030 let mut ctx_len = tp;
3031 e.stream().synchronize()?;
3032 crate::PRIME_NANOS.store(
3033 t_prime.elapsed().as_nanos() as u64,
3034 std::sync::atomic::Ordering::Relaxed,
3035 );
3036
3037 let mut out = Vec::with_capacity(max_new);
3038 let n_vocab = self.output.out_features();
3039 // Harvest convention (DSPARK-POSTMORTEM-20260820.md): which drafter output rows
3040 // become draft candidates. nd = drafts/round; verify carries [anchor, drafts]
3041 // = up to nd+1 rows. FAMILY-keyed for DFlash2 (mask-fill by construction),
3042 // else default = the CHECKPOINT's own strategy census (owner-ratified flip,
3043 // 2026-08-20); explicit env still wins (contradiction refuses).
3044 let harvest = DsparkHarvest::for_draft(draft);
3045 let nd = harvest.n_drafts(b);
3046 let r0 = harvest.first_row();
3047 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
3048 .ok()
3049 .and_then(|v| v.parse().ok())
3050 .unwrap_or(nd + 1)
3051 .clamp(2, nd + 1);
3052 let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
3053 // Verify-window policy (H4, DSPARK-POSTMORTEM-20260820.md): default =
3054 // confidence-slot tau=.5 when the checkpoint carries an accept-rate head
3055 // (owner-ratified flip 2026-08-20; cell-3 tau ladder knee) — each round's
3056 // window is sized from the head's own slot scores, post-draft pre-verify.
3057 // Head-less checkpoints and MEMRA_DFLASH_ADAPT=0 keep the reactive ladder.
3058 let vt_policy = DsparkVtPolicy::resolve(draft.confidence.is_some());
3059 if vt_policy.is_confidence() {
3060 assert!(
3061 draft.confidence.is_some(),
3062 "MEMRA_DSPARK_VT={vt_policy:?} needs a checkpoint with an accept-rate \
3063 head (confidence_head.* absent in this export)"
3064 );
3065 }
3066 let mut vt = vt_cap;
3067 let mut attempted = 0usize;
3068 let mut accepted = 0usize;
3069 // Engine-bundle slice 1: persistent batched snapshot (None until round 1; stays
3070 // None — legacy per-layer snapshot — under MEMRA_STATE_COPY_BATCH=0 or when the
3071 // batcher declines the cache shape).
3072 let mut snapb: Option<DsparkSnapBatch> = None;
3073 let mut snapb_off = !crate::spec::state_copy_batch_on();
3074 // Engine-bundle slice 2: deferred chain readback needs the resident embed table
3075 // (verify then embeds chain_d directly). Ladder/stash arms only — the confidence
3076 // policies size vt from a pre-verify head readback and keep the legacy order.
3077 let defer_rb = crate::spec::dspark_defer_readback_on() && !vt_policy.is_confidence();
3078 let (embd_qt, embd_rb) = self.embd.qt_and_row_bytes(n_embd);
3079 let embd_gpu = if !defer_rb || crate::spec::spec_host_embd() {
3080 None
3081 } else {
3082 Some(
3083 self.embd_gpu
3084 .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload")),
3085 )
3086 };
3087 // Engine-bundle slice 3: per-(segment, vt) verify graphs for the linear-layer runs
3088 // (rides the slice-2 deferred path only — device tokens keep the whole verify off
3089 // the host). PERSISTENT across generations on the model (rebuilding per call
3090 // re-captured ~80 graphs per prompt — measured 97.8 -> 79.1 tok/s e2e); the
3091 // captured bodies are cache-independent: all state reads go through per-round
3092 // refreshed pointer tables and ctx-owned slabs. None = eager walk, byte-identical.
3093 let mut vg_guard = self.dspark_vgraphs.lock().unwrap();
3094 if vg_guard.is_none() && embd_gpu.is_some() && crate::spec::dspark_verify_graph_on() {
3095 *vg_guard = crate::spec::DsparkVerifyGraphs::new(e, &cache, vt_cap, n_embd)?;
3096 }
3097 let vgraphs: &mut Option<crate::spec::DsparkVerifyGraphs> = &mut vg_guard;
3098 // per-phase economics counters (ns) — the verify-toll dataset
3099 let (mut ns_draft, mut ns_snap, mut ns_verify, mut ns_roll, mut ns_ingest) =
3100 (0u64, 0u64, 0u64, 0u64, 0u64);
3101 let mut rounds = 0usize;
3102 let stats = std::env::var("MEMRA_SPEC_STATS").as_deref() == Ok("1");
3103 let clock = |on: bool, e: &Engine| -> std::time::Instant {
3104 if on {
3105 let _ = e.stream().synchronize();
3106 }
3107 std::time::Instant::now()
3108 };
3109 'outer: while out.len() < max_new {
3110 rounds += 1;
3111 let start = cache.pos; // committed length
3112 // ---- draft: block = [last, MASK x b-1] (decode-exact class for the m=b mms) ----
3113 let t0 = clock(stats, e);
3114 // RAII: a `?` exit restores the pre-scope value instead of latching exact
3115 // ON engine-wide (hermes finding, fixed 2026-08-23).
3116 let exact_scope = e.exact_scope(true);
3117 let mut block: Vec<u32> = vec![c.mask_token_id; b];
3118 block[0] = last;
3119 let noise = e.htod(&self.embd.gather(n_embd, &block))?;
3120 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
3121 let dh = draft.forward_round(e, &mut dkv, &noise, &pos_block)?;
3122 // Harvest: logits over rows r0..r0+nd (Dflash: mask rows 1..b-1, fill
3123 // semantics; Dspark: ALL b rows, shifted semantics — row k predicts
3124 // anchor+k+1, so col k of `dl` is the draft for position start+k+1).
3125 let mut rows = e.uninit(nd * n_embd)?;
3126 {
3127 let dv = e.view(&dh, b * n_embd);
3128 let src = dv.slice(r0 * n_embd..(r0 + nd) * n_embd);
3129 e.copy_view_into(&mut rows, 0, &src, nd * n_embd)?;
3130 }
3131 // TRIMMED DRAFT HEAD (lane/dflash2-head-trim, 2026-08-25): DFlash2 family
3132 // only — the selector consumes (value, candidate-id) pairs, so a d2t remap
3133 // after top-k restores true ids; the markov/chain arms argmax dl columns
3134 // into token ids DIRECTLY and must keep the full head. Reuses the FR-Spec
3135 // self-trim the load path builds on the MTP struct (MEMRA_FRSPEC_TRIM):
3136 // gathered rows of the target's own head, zero requant. Verify stays
3137 // full-vocab, so the trim moves draft acceptance only, never output.
3138 let trim = if draft.dflash2.is_some() {
3139 self.mtp
3140 .as_ref()
3141 .filter(|m| m.d2t_from_target_head)
3142 .and_then(|m| m.shared_head_head.as_ref().zip(m.d2t.as_ref()))
3143 // MEMRA_MTP_SKIP stub: the same target-head trimmed rows, parked in
3144 // `dflash_trim` because the embedded MTP block was skipped (hybrid.rs;
3145 // rows are target-head by construction; the loader refuses otherwise).
3146 .or_else(|| self.dflash_trim.as_ref().map(|t| (&t.head, &t.d2t)))
3147 .filter(|(_, d2t)| !d2t.is_empty())
3148 } else {
3149 None
3150 };
3151 let (dl_head, dl_vocab) = match trim {
3152 Some((head, d2t)) => (head, d2t.len()),
3153 None => (&self.output, n_vocab),
3154 };
3155 let trim_d2t = trim.map(|(_, d2t)| d2t.as_slice());
3156 let mut dl = e.matmul(dl_head, &rows, nd)?;
3157 // Family/sampling-keyed proposal (v0.100 train merge of the port and H4/
3158 // engine-bundle stacks — BOTH programs preserved):
3159 // - SAMPLED (sp_on): rejection-sampling proposal, records the true per-slot
3160 // q (family-keyed inside: selector for DFlash2, markov-corrected rows
3161 // otherwise). Host CDF/readback syncs inside — slice-2 deferral N/A.
3162 // - DFlash2 greedy: the candidate path selector REPLACES the markov chain
3163 // (reference DFlash2DraftModel.propose — greedy arm).
3164 // - markov/plain greedy chain: the engine-bundle arm; slice-2 readback
3165 // deferral decided below (needs the ckpt arm reads).
3166 // Confidence policy: stash each slot's markov prev-token embedding (the
3167 // exact `w1` row the chain gathers) into a [nd, rank] buffer — d2d async,
3168 // read back beside `rows` in one host sync after the chain.
3169 let want_conf_emb = vt_policy.is_confidence()
3170 && draft.confidence.as_ref().is_some_and(|ch| ch.with_markov);
3171 let mut conf_emb: Option<CudaSlice<f32>> = match (&draft.markov, want_conf_emb) {
3172 (Some(mk), true) => Some(e.uninit(nd * mk.rank)?),
3173 (None, true) => unreachable!(
3174 "with_markov confidence head without a markov table — the loader forbids it"
3175 ),
3176 _ => None,
3177 };
3178 let mut cand: Vec<u32> = Vec::with_capacity(nd + 1);
3179 let mut prop: Option<DsparkDraftSample> = None;
3180 let mut chain_dev: Option<CudaSlice<u32>> = None;
3181 if let Some(sp) = sp_on {
3182 let (tail, ds) = draft.dspark_propose_sampled(
3183 e,
3184 &mut dl,
3185 &rows,
3186 nd,
3187 dl_vocab,
3188 last,
3189 sp,
3190 &mut sctr,
3191 &mut uctr,
3192 conf_emb.as_mut(),
3193 trim_d2t,
3194 )?;
3195 drop(exact_scope);
3196 cand.push(last);
3197 cand.extend_from_slice(&tail);
3198 prop = Some(ds);
3199 } else if draft.dflash2.is_some() {
3200 let path =
3201 draft.dflash2_propose_greedy(e, &dl, &rows, nd, dl_vocab, last, trim_d2t)?;
3202 drop(exact_scope);
3203 cand.push(last);
3204 cand.extend_from_slice(&path);
3205 } else {
3206 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
3207 let mut chain_d = e.stream().alloc_zeros::<u32>(nd + 1)?;
3208 if let (Some(mk), true) = (&draft.markov, markov_on) {
3209 e.set_u32_one(&mut chain_d, last)?;
3210 for k in 0..nd {
3211 let mut f = e.uninit(mk.rank)?;
3212 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
3213 if let Some(ce) = conf_emb.as_mut() {
3214 let fv = e.view(&f, mk.rank);
3215 e.copy_view_into(ce, k * mk.rank, &fv, mk.rank)?;
3216 }
3217 let bias = e.matmul(&mk.w2, &f, 1)?;
3218 e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
3219 e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
3220 }
3221 } else {
3222 if want_conf_emb {
3223 // chain_d[0] must carry the anchor — slot 0's prev token.
3224 e.set_u32_one(&mut chain_d, last)?;
3225 }
3226 for i in 0..nd {
3227 if let (Some(ce), Some(mk)) = (conf_emb.as_mut(), &draft.markov) {
3228 let mut f = e.uninit(mk.rank)?;
3229 e.gather_row_bf16(&mk.w1_bf16, &chain_d, i, &mut f, mk.rank)?;
3230 let fv = e.view(&f, mk.rank);
3231 e.copy_view_into(ce, i * mk.rank, &fv, mk.rank)?;
3232 }
3233 e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
3234 }
3235 }
3236 drop(exact_scope);
3237 chain_dev = Some(chain_d);
3238 }
3239 // MEMRA_DSPARK_CKPT (default 1): verify with the MTP column-stash armed so a
3240 // partial accept restores state directly. =0 keeps the snapshot+replay arm
3241 // (the oracle the stash arm is gated against — MEMRA_DSPARK_CKPT_GATE=1 runs
3242 // BOTH per partial round and byte-compares the resulting cache state).
3243 // Read here (was at the verify site) — slice 2's deferral needs the arm
3244 // choice before deciding whether the chain readback can move past verify.
3245 let ckpt_on = std::env::var("MEMRA_DSPARK_CKPT").as_deref() != Ok("0");
3246 let ckpt_gate = std::env::var("MEMRA_DSPARK_CKPT_GATE").as_deref() == Ok("1");
3247 // SAMPLED x ckpt-gate refusal: the gate compares verify argmaxes across a
3248 // replay — a greedy-exactness instrument (port lane). Refuse loudly.
3249 if sp_on.is_some() && ckpt_gate {
3250 return Err(
3251 "MEMRA_DSPARK_CKPT_GATE compares verify argmaxes across a replay \
3252 — a greedy-exactness instrument; unset it for T>0 dspark rounds"
3253 .into(),
3254 );
3255 }
3256 // Slice 2: under the stash/gate arms with a resident embed table, the GREEDY
3257 // chain readback is DEFERRED past verify dispatch and merged with the argmax
3258 // readback into one sync. The replay arm (CKPT=0) verifies host tokens and
3259 // keeps the legacy order; the sampled and DFlash2 proposals already synced
3260 // at the walk (chain_dev is None there).
3261 let deferred = chain_dev.is_some() && embd_gpu.is_some() && (ckpt_on || ckpt_gate);
3262 // ---- H4 confidence window: size THIS round's verify from the head ----
3263 if vt_policy.is_confidence() {
3264 let ch = draft.confidence.as_ref().expect("asserted at loop entry");
3265 let (rows_h, emb_h) = match conf_emb.as_ref() {
3266 Some(ce) => {
3267 let (a, b2) = e.dtoh_pair(&rows, ce)?;
3268 (a, Some(b2))
3269 }
3270 None => (e.dtoh(&rows)?, None),
3271 };
3272 let rank = draft.markov.as_ref().map(|m| m.rank).unwrap_or(0);
3273 let mut raws = Vec::with_capacity(nd);
3274 for k in 0..nd {
3275 let hrow = &rows_h[k * n_embd..(k + 1) * n_embd];
3276 let emb = emb_h.as_ref().map(|eh| &eh[k * rank..(k + 1) * rank]);
3277 raws.push(ch.raw_score(hrow, emb));
3278 }
3279 vt = vt_policy
3280 .size_window(&raws, vt_cap)
3281 .expect("confidence policies always size the window");
3282 }
3283 // Verify candidates: [anchor, draft 1..nd]. Under Dflash this is the
3284 // historical `block` content; under Dspark it is one longer than the
3285 // drafter's input block (nd = b drafts + the anchor). The sampled/DFlash2
3286 // proposals built `cand` at the walk; deferred greedy rounds build it after
3287 // the merged readback — the bytes are identical (chain_d is written before
3288 // either sync).
3289 if let Some(chain_d) = chain_dev.as_ref()
3290 && !deferred
3291 {
3292 let chain = e.dtoh_u32(chain_d)?;
3293 cand.push(last);
3294 cand.extend_from_slice(&chain[1..]);
3295 }
3296 ns_draft += clock(stats, e).duration_since(t0).as_nanos() as u64;
3297
3298 // ---- snapshot (GDN conv/ssm state + KV lens), then verify t=vt ----
3299 let t1 = std::time::Instant::now();
3300 // Slice 1: batched snap (one table refresh + two copy launches) with the
3301 // legacy per-layer snapshot as the kill-switch / non-uniform fallback.
3302 let mut snap_legacy: Option<crate::cache::CacheSnapshot> = None;
3303 if !snapb_off && snapb.is_none() {
3304 snapb = DsparkSnapBatch::new(e, &cache)?;
3305 snapb_off = snapb.is_none();
3306 } else if let Some(sb) = snapb.as_mut() {
3307 sb.refresh(e, &cache)?;
3308 }
3309 let snap: &crate::cache::CacheSnapshot = match snapb.as_ref() {
3310 Some(sb) => &sb.snap,
3311 None => {
3312 snap_legacy = Some(cache.snapshot(e)?);
3313 snap_legacy.as_ref().unwrap()
3314 }
3315 };
3316 let _ = &snap_legacy;
3317 ns_snap += clock(stats, e).duration_since(t1).as_nanos() as u64;
3318 let t2 = std::time::Instant::now();
3319 // Slice 3: the tap-sink buffer is persistent per vt in the graphs ctx
3320 // (captured segments bake its address); fully rewritten by every verify.
3321 let tap_buf = match vgraphs.as_mut().and_then(|g| g.tap_bufs.remove(&vt)) {
3322 Some(buf) => buf,
3323 None => e.uninit(vt * n_taps * n_embd)?,
3324 };
3325 cache.dflash_taps = Some(DflashTapSink {
3326 layer_ids: c.target_layer_ids.clone(),
3327 buf: tap_buf,
3328 hidden: n_embd,
3329 t: vt,
3330 base: 0,
3331 });
3332 // The whole fallible verify window runs inside a closure so the Err path can
3333 // return the sink buffer to the ctx pool before propagating (v0.98 review
3334 // carry-over): five `?`s span the window, and an early return would drop
3335 // `cache.dflash_taps` — freeing the buffer whose ADDRESS the model-persistent
3336 // captured graphs bake, so the next generation's replayed tap copies would
3337 // write freed memory. The never-orphan invariant below now holds on EVERY
3338 // exit, not just the EOS/budget break.
3339 let verify_res = (|cache: &mut crate::cache::Cache,
3340 cand: &mut Vec<u32>,
3341 vgraphs: &mut Option<crate::spec::DsparkVerifyGraphs>|
3342 -> Result<
3343 (
3344 Vec<u32>,
3345 Option<CudaSlice<f32>>,
3346 Option<crate::spec::DsparkVerifyCkpt>,
3347 ),
3348 Box<dyn std::error::Error>,
3349 > {
3350 if sp_on.is_some() {
3351 // SAMPLED: keep the raw verify logits — the accept walk gathers
3352 // filtered p from them (argmaxes are the greedy arm's instrument,
3353 // not this one's).
3354 if ckpt_on {
3355 let (tl, vck) =
3356 self.dspark_verify_t_logits_ckpt(e, &cand[..vt], start, cache)?;
3357 Ok((Vec::new(), Some(tl), Some(vck)))
3358 } else {
3359 Ok((
3360 Vec::new(),
3361 Some(self.dspark_verify_t_logits(e, &cand[..vt], start, cache)?),
3362 None,
3363 ))
3364 }
3365 } else if deferred {
3366 // Slice 2: verify embeds the DEVICE chain (cand layout by construction:
3367 // chain_d[0] = anchor, chain_d[1..] = drafts), then ONE host sync reads
3368 // chain + verify argmaxes together — the host dispatched snap + all of
3369 // verify while the draft was still executing.
3370 let chain_d = chain_dev.as_ref().expect("deferred implies greedy chain");
3371 let g = embd_gpu.expect("deferred implies resident embed");
3372 let (am_d, vck) = self.dspark_verify_t_am_ckpt_dev(
3373 e,
3374 chain_d,
3375 vt,
3376 start,
3377 cache,
3378 (g, embd_qt, embd_rb),
3379 vgraphs.as_mut(),
3380 )?;
3381 let ch = e.stream().clone_dtoh(chain_d)?;
3382 let am = e.stream().clone_dtoh(&am_d)?;
3383 e.stream().synchronize()?;
3384 cand.push(last);
3385 cand.extend_from_slice(&ch[1..]);
3386 Ok((am, None, Some(vck)))
3387 } else if ckpt_on || ckpt_gate {
3388 let (vam, vck) = self.dspark_verify_t_am_ckpt(e, &cand[..vt], start, cache)?;
3389 Ok((vam, None, Some(vck)))
3390 } else {
3391 Ok((
3392 self.dspark_verify_t_am(e, &cand[..vt], start, cache)?,
3393 None,
3394 None,
3395 ))
3396 }
3397 })(&mut cache, &mut cand, vgraphs);
3398 let (vam, tl, vck) = match verify_res {
3399 Ok(v) => v,
3400 Err(err) => {
3401 if let (Some(g), Some(taps)) = (vgraphs.as_mut(), cache.dflash_taps.take()) {
3402 g.tap_bufs.insert(vt, taps.buf);
3403 }
3404 return Err(err);
3405 }
3406 };
3407 let taps = cache.dflash_taps.take().unwrap();
3408 // Return the tap buffer to the ctx pool IMMEDIATELY — an EOS/budget break
3409 // between accept and ingest must never orphan an address the captured
3410 // graphs bake (the next generation would alloc a fresh buffer and the
3411 // replayed tap copies would write freed memory). Ingest reads it borrowed.
3412 let tap_local: Option<CudaSlice<f32>> = match vgraphs.as_mut() {
3413 Some(g) => {
3414 g.tap_bufs.insert(vt, taps.buf);
3415 None
3416 }
3417 None => Some(taps.buf),
3418 };
3419 let tap_ref: &CudaSlice<f32> = match &tap_local {
3420 Some(b) => b,
3421 None => &vgraphs.as_ref().expect("ctx present above").tap_bufs[&vt],
3422 };
3423 ns_verify += clock(stats, e).duration_since(t2).as_nanos() as u64;
3424
3425 // ---- accept ----
3426 // Penalized-sampled: the anchor `last` is committed THIS round unconditionally
3427 // (the out.push below), so it joins the window before the accept walk — verify
3428 // row 0's state includes it. Accepted drafts extend the window after the walk;
3429 // `next` joins as the anchor of ITS round.
3430 if pen_on {
3431 pen_hist.push(last);
3432 }
3433 let (m, next) = match (sp_on, tl.as_ref()) {
3434 (Some(sp), Some(tl)) => {
3435 let w0 = pen_hist
3436 .len()
3437 .saturating_sub(sp.penalty_last_n.min(crate::spec::PEN_WINDOW_MAX));
3438 dspark_accept_sampled(
3439 e,
3440 tl,
3441 &cand,
3442 vt,
3443 n_vocab,
3444 &dl,
3445 prop.as_ref()
3446 .expect("sampled round without a proposal record"),
3447 sp,
3448 &pen_hist[w0..],
3449 &mut sctr,
3450 &mut uctr,
3451 )?
3452 }
3453 _ => {
3454 let m = dspark_accept_prefix(&cand, &vam, vt);
3455 (m, vam[m])
3456 }
3457 };
3458 if pen_on {
3459 pen_hist.extend_from_slice(&cand[1..=m]);
3460 }
3461 attempted += vt - 1;
3462 accepted += m;
3463 out.push(last);
3464 if eos.contains(&last) {
3465 break 'outer;
3466 }
3467 if emit_accepted_run(&mut out, &cand[1..=m], eos, max_new) {
3468 break 'outer;
3469 }
3470
3471 // ---- commit/rollback: hybrid state cannot truncate — restore + replay kept ----
3472 let keep = m + 1;
3473 let t3 = std::time::Instant::now();
3474 // Slice 3: rounds whose linear column stash lives in the graphs ctx's slabs
3475 // commit through the slab twin (same semantics, slab-addressed sources).
3476 let slab_commit = vgraphs.as_ref().map(|g| g.round_slab).unwrap_or(false);
3477 if keep < vt {
3478 if ckpt_gate {
3479 // GATE ARM: stash-restore, snapshot S1; then the replay oracle, snapshot
3480 // S2; the two cache states must match BIT-FOR-BIT (kv lens, pos, every
3481 // conv/ssm buffer). Continue from the replay state (proven identical).
3482 if slab_commit {
3483 self.dspark_commit_prefix_slab(
3484 e,
3485 &mut cache,
3486 snap,
3487 vgraphs.as_ref().expect("slab_commit implies ctx"),
3488 keep,
3489 )?;
3490 } else {
3491 let vck = vck.as_ref().expect("gate arm always fills the ckpt");
3492 self.dspark_commit_prefix(e, &mut cache, snap, vck, keep)?;
3493 }
3494 // host-side state capture (NO device snapshot copies — two extra
3495 // device snapshots per round OOM'd beside the 15GB trunk)
3496 #[allow(clippy::type_complexity)]
3497 // allow: one-shot composite type; naming it would hide the shape that matters at the call site
3498 let capture = |cache: &Cache| -> Result<
3499 (usize, Vec<Option<usize>>, Vec<(Vec<f32>, Vec<f32>)>),
3500 Box<dyn std::error::Error>,
3501 > {
3502 let mut lens = Vec::new();
3503 let mut states = Vec::new();
3504 for il in 0..cache.kv.len() {
3505 lens.push(cache.kv[il].as_ref().map(|k| k.len));
3506 if let Some(rl) = &cache.recur[il] {
3507 states.push((e.dtoh(&rl.conv_state)?, e.dtoh(&rl.ssm_state)?));
3508 }
3509 }
3510 Ok((cache.pos, lens, states))
3511 };
3512 let (p1, l1, st1) = capture(&cache)?;
3513 crate::pp::restore_cache_checkpoint(e, self, None, &mut cache, snap)?;
3514 let ram = self.dspark_verify_t_am(e, &cand[..keep], start, &mut cache)?;
3515 assert_eq!(
3516 &ram[..],
3517 &vam[..keep],
3518 "prefix replay must reproduce the verify argmaxes"
3519 );
3520 let (p2, l2, st2) = capture(&cache)?;
3521 assert_eq!(p1, p2, "ckpt-gate: pos mismatch");
3522 assert_eq!(l1, l2, "ckpt-gate: kv_len mismatch");
3523 for (il, ((c1, s1v), (c2, s2v))) in st1.iter().zip(&st2).enumerate() {
3524 let bits = |a: &[f32], b: &[f32]| {
3525 a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
3526 };
3527 assert!(
3528 bits(c1, c2),
3529 "ckpt-gate: linear layer {il} conv state differs"
3530 );
3531 assert!(
3532 bits(s1v, s2v),
3533 "ckpt-gate: linear layer {il} ssm state differs"
3534 );
3535 }
3536 } else if slab_commit {
3537 // STASH ARM, slab twin (slice 3): same restore, slab-addressed.
3538 self.dspark_commit_prefix_slab(
3539 e,
3540 &mut cache,
3541 snap,
3542 vgraphs.as_ref().expect("slab_commit implies ctx"),
3543 keep,
3544 )?;
3545 } else if let Some(vck) = vck.as_ref() {
3546 // STASH ARM (default): column-state restore, no replay forward.
3547 self.dspark_commit_prefix(e, &mut cache, snap, vck, keep)?;
3548 } else {
3549 // REPLAY ARM (MEMRA_DSPARK_CKPT=0): the original snapshot+replay oracle.
3550 crate::pp::restore_cache_checkpoint(e, self, None, &mut cache, snap)?;
3551 debug_assert_eq!(cache.pos, start, "rollback landed off the round start");
3552 let ram = self.dspark_verify_t_am(e, &cand[..keep], start, &mut cache)?;
3553 if sp_on.is_none() {
3554 // the argmax-reproduction oracle is greedy-only; the sampled arm
3555 // replays purely to rebuild the cache state.
3556 debug_assert_eq!(
3557 &ram[..],
3558 &vam[..keep],
3559 "prefix replay must reproduce the verify argmaxes"
3560 );
3561 }
3562 }
3563 }
3564 ns_roll += clock(stats, e).duration_since(t3).as_nanos() as u64;
3565
3566 // ---- ingest the kept rows' ctx features into the draft KV ----
3567 let t4 = std::time::Instant::now();
3568 {
3569 let tv = e.view(tap_ref, vt * n_taps * n_embd);
3570 let keep_view = tv.slice(0..keep * n_taps * n_embd);
3571 let mut kept = e.uninit(keep * n_taps * n_embd)?;
3572 e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
3573 let f = draft.ctx_features(e, &kept, keep)?;
3574 let pos_k: Vec<i32> = ((ctx_len as i32)..(ctx_len + keep) as i32).collect();
3575 draft.ingest_ctx(e, &mut dkv, &f, &pos_k, keep)?;
3576 ctx_len += keep;
3577 }
3578 ns_ingest += clock(stats, e).duration_since(t4).as_nanos() as u64;
3579 last = next;
3580 // Ladder update only — under the confidence policies vt is recomputed
3581 // from the head every round, post-draft pre-verify.
3582 if !vt_policy.is_confidence() && adapt {
3583 vt = (m + 2).clamp(3, vt_cap);
3584 }
3585 }
3586 if stats {
3587 let ms = |n: u64| n as f64 / 1e6;
3588 eprintln!(
3589 "[dspark-q38] acceptance {accepted}/{attempted} = {:.3} rounds={rounds} \
3590 draft={:.1}ms snap={:.1}ms verify={:.1}ms rollback+replay={:.1}ms ingest={:.1}ms",
3591 accepted as f64 / attempted.max(1) as f64,
3592 ms(ns_draft),
3593 ms(ns_snap),
3594 ms(ns_verify),
3595 ms(ns_roll),
3596 ms(ns_ingest)
3597 );
3598 }
3599 Ok(out)
3600 }
3601}
3602
3603// ================= DSpark SERVING session (lane/dspark-q38-recover serve route) =========
3604// Burst-scoped state for the worker's dspark spec arm — the qwen-hybrid twin of
3605// GemmaSpecSession. Holds the trunk cache + draft KV + the round loop's carry state
3606// (`last`, ctx_len, adaptive vt) so the scheduler round-robins other sessions between
3607// bursts. The round body is generate_spec_dspark's loop, hoisted; that bin arm stays the
3608// banked oracle (E2E gate), and the serve-route smoke gates this twin byte-identical to
3609// a spec-off boot over the real HTTP surface. Exactness contract unchanged: the target's
3610// verify argmax decides every committed token, so the stream equals plain greedy BY
3611// CONSTRUCTION on every accept path (ckpt stash, gate, replay).
3612fn take_dspark_prefix_capture(
3613 slot: &mut Option<crate::spec::SpecBoundaryCapture>,
3614) -> Option<crate::spec::SpecBoundaryCapture> {
3615 slot.take()
3616}
3617
3618/// Deterministic preflight for the serving session's prompt-headroom requirement. Kept pure so
3619/// the worker can make the same decision before choosing whether to consume a prefix entry.
3620pub fn dspark_spec_prompt_fits(
3621 prompt_len: usize,
3622 ctx_cap: usize,
3623 block_size: usize,
3624 sliding_window: usize,
3625 is_dflash2: bool,
3626) -> bool {
3627 // PRIME FLOOR (incident 2026-08-25, second hit — the one that actually took prod down
3628 // twice). This predicate is the ONE admission gate the worker consumes for the dspark
3629 // route, and it only ever checked the ctx CEILING. A prompt shorter than
3630 // `PRIME_MIN_T` was therefore admitted and then panicked inside the cold prime, because
3631 // `prime_cache`'s batched arm asserts `T >= PRIME_MIN_T` and has no tokenwise twin that
3632 // fills the DFlash tap sink. A panic there is not a failed request: the GPU worker
3633 // exits 70 (poisoned-context contract) and every live session on the box dies, then the
3634 // guard relaunches into the same prompt — 20 panics and ~5 min of edge 502s on box10,
3635 // and a second loop on BOTH boxes when the route was redeployed. The trigger is
3636 // ordinary traffic: "Say OK." is 5 tokens, and our own watchdog sends that class.
3637 // Below the floor the route simply declines and the request serves on the plain path.
3638 if prompt_len < crate::hybrid_forward::PRIME_MIN_T {
3639 return false;
3640 }
3641 let max_ctx = if is_dflash2 {
3642 ctx_cap
3643 } else {
3644 ctx_cap.min(sliding_window)
3645 };
3646 prompt_len
3647 .checked_add(block_size)
3648 .and_then(|n| n.checked_add(8))
3649 .is_some_and(|need| need <= max_ctx)
3650}
3651
3652pub struct DsparkSpecSession {
3653 pub cache: crate::cache::Cache,
3654 /// One-shot prompt-end state for the worker's cross-request prefix cache. DFlash has no
3655 /// restorable draft plane, so this capture deliberately carries trunk snapshot + logits
3656 /// only; low-load DFlash requests ignore the resulting trunk-only entry while a later
3657 /// shed-to-plain request can consume it.
3658 prefix_capture: Option<crate::spec::SpecBoundaryCapture>,
3659 dkv: DflashKv,
3660 last: u32,
3661 ctx_len: usize,
3662 vt: usize,
3663 pub rounds: usize,
3664 max_ctx: usize,
3665 done: bool,
3666 /// Engine-bundle slice 1: persistent batched snapshot (buffers + pointer tables live
3667 /// with the session so bursts reuse them). None until the first round; stays None —
3668 /// legacy per-layer snapshot — when `snapb_off`.
3669 snapb: Option<DsparkSnapBatch>,
3670 snapb_off: bool,
3671 /// SAMPLED ADMISSION (T>0, lane/dspark-sampled-admission-20260820): the request's
3672 /// sampling config (None/temp==0 = the greedy route, byte-identical). Fixed for the
3673 /// session — the worker's admission owns the sampler identity.
3674 sampling: Option<crate::spec::SpecSampling>,
3675 /// Philox event counters, session-owned so randomness never repeats across bursts
3676 /// (the frspec session-continuity law): `sctr` = device sampling events (boundary,
3677 /// draft chain, bonus, residual), `uctr` = host uniforms (selector walk, accept tests).
3678 sctr: u32,
3679 uctr: u32,
3680 /// Penalized-sampled window (lane/dspark-penalized-sampled-20260821): seeded from
3681 /// the prompt tail (`pen_window_seed`), extended with every committed token, carried
3682 /// across bursts so a burst boundary never resets the stream the client asked us to
3683 /// penalize. Empty (and never touched) when the request carries no penalties.
3684 pen_hist: Vec<u32>,
3685}
3686
3687fn dspark_commit_limit(
3688 accepted_keep: usize,
3689 burst_out_len: usize,
3690 request_room: usize,
3691) -> (usize, bool) {
3692 let public_room = request_room.saturating_sub(burst_out_len);
3693 debug_assert!(public_room > 0);
3694 let keep = accepted_keep.min(public_room);
3695 (keep, keep < accepted_keep)
3696}
3697
3698impl DsparkSpecSession {
3699 /// How many trailing draft-KV rows a restore must carry for the drafter to be
3700 /// indistinguishable from one that cold-primed: the sliding window plus one block.
3701 ///
3702 /// WHY A TAIL IS SUFFICIENT, and why this is a fact about THIS export rather than a hope:
3703 /// every DFlash2 draft layer is `sliding_attention` (the port asserts
3704 /// `cfg.layer_sliding.iter().all(|&s| s)` at load and refuses otherwise), so the windowed
3705 /// SDPA never reads a key below the current block's window floor
3706 /// (`sdpa_naive_w_lo`, whose bit-identity at Tkv 4104 and legacy launch failure are both
3707 /// pinned by kernel_check). A round at context `pos` therefore reads rows
3708 /// `[pos - window + 1, pos + block)` and nothing older. Storing that tail is storing
3709 /// everything the drafter can observe.
3710 ///
3711 /// SIZE, the reason this is affordable at all: 5 layers x (2048 + 16) rows x 8 kv x 128
3712 /// dim x 4 B x 2 (k+v) is ~85 MB, against ~1,057 MB for the trunk planes of a
3713 /// 30k-token entry. Storing the FULL draft history instead would be ~1,229 MB — more than
3714 /// the trunk entry itself — which is what makes the tail the only viable form.
3715 pub fn draft_tail_rows(&self) -> usize {
3716 self.dkv.cfg_window_rows()
3717 }
3718
3719 /// The drafter's KV, for a worker publishing the tail into its cross-request prefix cache.
3720 pub fn draft_kv(&self) -> &DflashKv {
3721 &self.dkv
3722 }
3723}
3724
3725/// The tail-import refusal arms, PURE so they are testable without CUDA. These are the fence
3726/// in front of the deliberate uninitialised-rows-below-`base` design: rows the import does not
3727/// copy are unreadable ONLY if the tail actually covers the drafter's window ending exactly at
3728/// the logical length — every arm here is what makes that "only if" hold. A refusal that
3729/// silently stopped firing would let a session attend garbage without crashing, which is the
3730/// silent-quality-loss class, so each arm names itself.
3731#[allow(clippy::too_many_arguments)]
3732pub fn tail_geometry_ok(
3733 tail_layers: usize,
3734 tail_row_bytes: usize,
3735 tail_base: usize,
3736 tail_rows: usize,
3737 tail_len: usize,
3738 kv_layers: usize,
3739 kv_row_bytes: usize,
3740 kv_window_rows: usize,
3741 cap: usize,
3742) -> Result<(), &'static str> {
3743 if tail_layers != kv_layers {
3744 return Err("layer count differs from the live drafter");
3745 }
3746 if tail_row_bytes != kv_row_bytes {
3747 return Err("row geometry differs from the live drafter");
3748 }
3749 if tail_len > cap {
3750 return Err("logical length exceeds the session cap");
3751 }
3752 if tail_base + tail_rows != tail_len {
3753 return Err("tail does not end at its own logical length");
3754 }
3755 // The whole point of the tail: it must cover everything a round can read. A shorter
3756 // tail than the window is only acceptable when the tail IS the entire history.
3757 if tail_rows < kv_window_rows.min(tail_len) {
3758 return Err("tail shorter than the drafter's readable window");
3759 }
3760 Ok(())
3761}
3762
3763/// A DFlash draft-KV tail, per drafter layer, ready to ride a cross-request prefix-cache
3764/// entry: `(k, v)` f32 rows covering absolute positions `[base, base + rows)`.
3765///
3766/// Only the tail travels, and that is a fact about this export rather than an optimisation:
3767/// every DFlash2 draft layer is `sliding_attention` (the port asserts it at load), so a round
3768/// at context `pos` reads rows `[pos - window + 1, pos + block)` and nothing older. Storing
3769/// the whole history for a 30k-token prompt would be ~1,229 MB — MORE than the ~1,057 MB of
3770/// trunk planes it would ride with; the tail is ~85 MB.
3771pub struct DflashKvTail {
3772 pub layers: Vec<(CudaSlice<f32>, CudaSlice<f32>)>,
3773 /// Absolute position of the first stored row.
3774 pub base: usize,
3775 /// Rows stored per layer.
3776 pub rows: usize,
3777 /// Logical length the KV had when exported (`= pos`), so an import can restore the same
3778 /// absolute row addressing the rope positions were baked against.
3779 pub len: usize,
3780 /// Bytes per row per layer, carried so an import cannot disagree about the geometry.
3781 pub row_bytes: usize,
3782}
3783
3784impl DflashKvTail {
3785 pub fn bytes(&self) -> usize {
3786 self.layers.len() * self.rows * self.row_bytes * 2
3787 }
3788}
3789
3790impl DflashKv {
3791 /// Copy out the readable tail ending at `upto` (see `DflashKvTail`). `None` when there is
3792 /// nothing to publish or an allocation fails — publication is always optional.
3793 ///
3794 /// `upto` IS NOT `self.len`, and conflating them was the bug the first exactness-gate run
3795 /// caught: publication happens at the scheduler's drain sweep, by which time the session
3796 /// has committed generated rows, so `len` had run 35 rows past the capture boundary and
3797 /// every restore was refused with `draft KV len 30364 != prompt 30329`. The trunk planes
3798 /// are copied at the capture `pos` for the same reason; the tail must agree with them.
3799 pub fn export_tail(&self, e: &Engine, upto: usize) -> Option<DflashKvTail> {
3800 if upto == 0 || upto > self.len {
3801 return None;
3802 }
3803 let rowsz = self.row_bytes / std::mem::size_of::<f32>();
3804 let rows = self.window_rows.min(upto);
3805 let base = upto - rows;
3806 let mut layers = Vec::with_capacity(self.k.len());
3807 for li in 0..self.k.len() {
3808 let (Ok(mut k), Ok(mut v)) = (e.uninit(rows * rowsz), e.uninit(rows * rowsz)) else {
3809 return None;
3810 };
3811 if e.copy_range_into(&mut k, 0, &self.k[li], base * rowsz, rows * rowsz)
3812 .is_err()
3813 || e.copy_range_into(&mut v, 0, &self.v[li], base * rowsz, rows * rowsz)
3814 .is_err()
3815 {
3816 return None;
3817 }
3818 layers.push((k, v));
3819 }
3820 Some(DflashKvTail {
3821 layers,
3822 base,
3823 rows,
3824 len: upto,
3825 row_bytes: self.row_bytes,
3826 })
3827 }
3828
3829 /// Rebuild a draft KV from a published tail: a fresh allocation at `cap`, the tail copied
3830 /// back to the SAME absolute rows it came from, and `len` restored so the next round
3831 /// addresses positions exactly as a cold-primed session would.
3832 ///
3833 /// Rows below `tail.base` are ZEROED, not left uninitialised. The clipped SDPA never reads
3834 /// below the block's window floor, but the legacy full-scan kernel
3835 /// (`MEMRA_DFLASH2_SDPA_CLIP=0`, the rollback seam) scans EVERY row into the score and the
3836 /// output, relying on masked rows contributing exactly zero — an identity that holds only
3837 /// for finite data (`0.0 * NaN = NaN`, and an uninit K row can produce a NaN score that
3838 /// poisons the softmax sum). Zeros keep that identity on both kernel arms, so a clip
3839 /// rollback on a restore-armed box stays byte-exact instead of decoding silent garbage
3840 /// (review round 3). Rows above `tail.len` stay uninit — equally unwritten and unread in
3841 /// the cold path, so restored matches cold there.
3842 ///
3843 /// This function still REFUSES rather than trusts the window math — if the tail does not
3844 /// cover the window, the caller gets `None` and must cold-prime.
3845 pub fn from_tail(e: &Engine, cfg: &DflashCfg, cap: usize, tail: &DflashKvTail) -> Option<Self> {
3846 let mut kv = Self::new(e, cfg, cap).ok()?;
3847 if let Err(why) = tail_geometry_ok(
3848 tail.layers.len(),
3849 tail.row_bytes,
3850 tail.base,
3851 tail.rows,
3852 tail.len,
3853 kv.k.len(),
3854 kv.row_bytes,
3855 kv.window_rows,
3856 cap,
3857 ) {
3858 eprintln!("[dspark] tail import refused: {why}");
3859 return None;
3860 }
3861 let rowsz = kv.row_bytes / std::mem::size_of::<f32>();
3862 for li in 0..kv.k.len() {
3863 let (src_k, src_v) = &tail.layers[li];
3864 if tail.base > 0 {
3865 // Finite zeros below the tail: the legacy full-scan kernel reads these rows
3866 // (see the doc above); NaN in either K or V poisons the row's contribution.
3867 e.memset_zeros_view(&mut kv.k[li].slice_mut(0..tail.base * rowsz))
3868 .ok()?;
3869 e.memset_zeros_view(&mut kv.v[li].slice_mut(0..tail.base * rowsz))
3870 .ok()?;
3871 }
3872 e.copy_range_into(
3873 &mut kv.k[li],
3874 tail.base * rowsz,
3875 src_k,
3876 0,
3877 tail.rows * rowsz,
3878 )
3879 .ok()?;
3880 e.copy_range_into(
3881 &mut kv.v[li],
3882 tail.base * rowsz,
3883 src_v,
3884 0,
3885 tail.rows * rowsz,
3886 )
3887 .ok()?;
3888 }
3889 kv.len = tail.len;
3890 Some(kv)
3891 }
3892
3893 /// Rows a restore must carry (see `DsparkSpecSession::draft_tail_rows`). Stored here
3894 /// because `DflashKv` owns the row geometry; the value comes from the drafter cfg.
3895 pub fn cfg_window_rows(&self) -> usize {
3896 self.window_rows
3897 }
3898
3899 /// Bytes per row per layer (`n_kv * head_dim * 4`), the unit both the export and the
3900 /// import address rows in.
3901 pub fn row_bytes(&self) -> usize {
3902 self.row_bytes
3903 }
3904
3905 /// Number of draft layers, i.e. how many per-layer planes an export produces.
3906 pub fn n_layer(&self) -> usize {
3907 self.k.len()
3908 }
3909}
3910
3911impl DsparkSpecSession {
3912 pub fn cache_max_ctx(&self) -> usize {
3913 self.max_ctx
3914 }
3915 pub fn finished(&self) -> bool {
3916 self.done
3917 }
3918 pub fn pos(&self) -> usize {
3919 self.cache.pos
3920 }
3921 /// Drain the prompt-end prefix capture exactly once. Publication is worker-owned so it can
3922 /// apply namespace isolation, dedupe and the shared byte budget at the scheduler boundary.
3923 pub fn take_prefix_capture(&mut self) -> Option<crate::spec::SpecBoundaryCapture> {
3924 take_dspark_prefix_capture(&mut self.prefix_capture)
3925 }
3926 /// DEMOTION HANDOFF (lane/dspark-spec-gate-demote, 2026-08-24): consume this session and
3927 /// hand its trunk cache + next-token prediction to the plain batched-decode path — the
3928 /// dspark twin of [`crate::spec::SpecSession::into_demoted`].
3929 ///
3930 /// WHY THIS IS EXACT (greedy). The burst-boundary invariant is `cache.pos == prompt rows
3931 /// + emitted tokens`: each round commits exactly `m+1` trunk rows (anchor + accepted
3932 /// drafts) and emits exactly those `m+1` tokens, so every emitted token has its KV row
3933 /// and nothing else does. `last` is the verify argmax at the LAST committed row — and
3934 /// verify-column argmax equality with plain decode is the very property the dspark E2E
3935 /// byte-identity gate pins (`dspark_q38_gate`: ALL EXACT). Handing (cache, last) to the
3936 /// batched path therefore continues the stream from a state indistinguishable from one
3937 /// the batched path produced itself.
3938 ///
3939 /// Unlike the MTP twin there is no carried-pending shape: the round commits its bonus
3940 /// inside the burst, so a session at a burst boundary is ALWAYS in handoff shape. The
3941 /// caller still cross-checks `pos()` against its fed-token count (a budget-clamped
3942 /// overshoot leaves cache rows past the public stream — those sessions finish, never
3943 /// demote). The draft KV, snapshot buffers and philox counters are DROPPED here
3944 /// (freeing their VRAM): the batched path never drafts, and the handoff is one-way.
3945 ///
3946 /// Sampled sessions must not be demoted (the caller excludes them, mirroring the MTP
3947 /// gate): their committed stream depends on the session-owned philox counters, and the
3948 /// plain batched sampler is a different random program mid-request.
3949 pub fn into_demoted(self) -> (crate::cache::Cache, u32) {
3950 (self.cache, self.last)
3951 }
3952}
3953
3954impl crate::hybrid::HybridModel {
3955 /// Turn-1 prime: trunk prefill with taps armed + chunked ctx ingest into the draft KV.
3956 /// Mirrors generate_spec_dspark's prime block exactly (chunk offsets via sink.base are
3957 /// handled inside prime_cache's tick loop; the 256-row ingest chunks match the bin arm).
3958 pub fn dspark_spec_session_new(
3959 &self,
3960 e: &Engine,
3961 draft: &DflashDraft,
3962 prompt: &[u32],
3963 ctx_cap: usize,
3964 sampling: Option<crate::spec::SpecSampling>,
3965 capture_prefix: bool,
3966 ) -> Result<DsparkSpecSession, Box<dyn std::error::Error>> {
3967 use crate::cache::{Cache, DflashTapSink};
3968 assert!(
3969 !self.uses_gemma_program(),
3970 "gemma4 targets use the assistant-drafter route; dspark is the qwen-hybrid arm"
3971 );
3972 // Penalized SAMPLED requests are IN scope (lane/dspark-penalized-sampled-20260821:
3973 // p-side penalties over the true per-state window, q the recorded proposal — the
3974 // accept walk's penalty arm). Penalties at temp==0 stay a LOUD refusal: the greedy
3975 // walk argmaxes RAW columns and would silently drop them — penalized greedy is
3976 // served exactly on the plain path (worker admission owns that exclusion).
3977 if let Some(sp) = sampling.as_ref()
3978 && sp.temp <= 0.0
3979 && sp.pen_on()
3980 {
3981 return Err(
3982 "dspark spec at temp==0 is the greedy route and would silently drop \
3983 the request's penalties; penalized greedy is served on the plain path"
3984 .into(),
3985 );
3986 }
3987 let n_embd = self.cfg.n_embd as usize;
3988 let c = &draft.cfg;
3989 assert_eq!(n_embd, c.hidden, "draft hidden must match target n_embd");
3990 let b = c.block_size;
3991 let n_taps = c.target_layer_ids.len();
3992 // The dspark round is windowless: every position the session will ever hold must
3993 // fit the draft window. Clamp the session ctx to it and refuse prompts that
3994 // cannot take even one round — admission falls back to the plain path.
3995 // DFlash2 rounds implement the reference's symmetric sliding window
3996 // (sdpa_naive_w), so its sessions take the full ctx cap.
3997 let is_dflash2 = draft.dflash2.is_some();
3998 let max_ctx = if is_dflash2 {
3999 ctx_cap
4000 } else {
4001 ctx_cap.min(c.sliding_window)
4002 };
4003 if !dspark_spec_prompt_fits(prompt.len(), ctx_cap, b, c.sliding_window, is_dflash2) {
4004 let need = prompt.len().saturating_add(b).saturating_add(8);
4005 return Err(format!(
4006 "dspark session needs {need} ctx (prompt {} + block {b} + 8), cap {max_ctx}",
4007 prompt.len()
4008 )
4009 .into());
4010 }
4011 let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
4012 let tp = prompt.len();
4013 cache.dflash_taps = Some(DflashTapSink {
4014 layer_ids: c.target_layer_ids.clone(),
4015 buf: e.uninit(tp * n_taps * n_embd)?,
4016 hidden: n_embd,
4017 t: tp,
4018 base: 0,
4019 });
4020 let (logits, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
4021 // Boundary token: greedy argmax (byte contract) or the request's own filtered
4022 // draw through the session Philox stream (the frspec boundary composition) —
4023 // penalized over the prompt window when the request carries penalties.
4024 let mut sctr0 = 0u32;
4025 let pen_hist: Vec<u32> = match sampling.as_ref().filter(|s| s.temp > 0.0 && s.pen_on()) {
4026 Some(sp) => crate::spec::pen_window_seed(&[], prompt, sp.penalty_last_n),
4027 None => Vec::new(),
4028 };
4029 let last = match sampling.as_ref().filter(|s| s.temp > 0.0) {
4030 Some(sp) => crate::spec::sample_boundary_token(
4031 e,
4032 &logits,
4033 sp,
4034 &pen_hist,
4035 &mut sctr0,
4036 "dspark-prime",
4037 )?,
4038 None => crate::forward::argmax(&logits) as u32,
4039 };
4040 let mut dkv = DflashKv::new(e, &draft.cfg, max_ctx)?;
4041 {
4042 let taps = cache.dflash_taps.take().unwrap();
4043 let n_taps_h = n_taps * n_embd;
4044 let mut r0 = 0usize;
4045 while r0 < tp {
4046 let t_c = (tp - r0).min(256);
4047 let tv = e.view(&taps.buf, tp * n_taps_h);
4048 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
4049 let mut chunk = e.uninit(t_c * n_taps_h)?;
4050 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
4051 let f = draft.ctx_features(e, &chunk, t_c)?;
4052 let pos_c: Vec<i32> = ((r0 as i32)..(r0 + t_c) as i32).collect();
4053 draft.ingest_ctx(e, &mut dkv, &f, &pos_c, t_c)?;
4054 r0 += t_c;
4055 }
4056 }
4057 e.stream().synchronize()?;
4058 // FULL-PROMPT ONLY. Unlike MTP, DFlash cannot restore its draft plane from a trunk
4059 // prefix, so there is no LCP/message-boundary split arm here. Mandatory draft-KV
4060 // allocation + ingest has already succeeded; the optional snapshot can no longer turn
4061 // a session that would have fit into a draft-allocation failure. Capture remains before
4062 // any speculative burst mutates the recurrent state.
4063 let prefix_capture = if capture_prefix {
4064 cache
4065 .snapshot(e)
4066 .ok()
4067 .map(|snap| crate::spec::SpecBoundaryCapture {
4068 snap,
4069 pos: tp,
4070 logits: logits.clone(),
4071 last_h: Vec::new(),
4072 })
4073 } else {
4074 None
4075 };
4076 // Verify carries [anchor, drafts] = up to n_drafts+1 rows (harvest-dependent;
4077 // DSPARK-POSTMORTEM-20260820.md; family-keyed for DFlash2, else checkpoint
4078 // strategy census).
4079 let nd = DsparkHarvest::for_draft(draft).n_drafts(b);
4080 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
4081 .ok()
4082 .and_then(|v| v.parse().ok())
4083 .unwrap_or(nd + 1)
4084 .clamp(2, nd + 1);
4085 Ok(DsparkSpecSession {
4086 cache,
4087 prefix_capture,
4088 dkv,
4089 last,
4090 ctx_len: tp,
4091 vt: vt_cap,
4092 rounds: 0,
4093 max_ctx,
4094 done: false,
4095 snapb: None,
4096 snapb_off: !crate::spec::state_copy_batch_on(),
4097 sampling,
4098 sctr: sctr0,
4099 uctr: 0,
4100 pen_hist,
4101 })
4102 }
4103
4104 /// One scheduler burst: dspark rounds until >= `burst_target` tokens are committed,
4105 /// EOS lands, or the ctx cap is reached. `request_room` is the request's remaining
4106 /// public budget, which may be larger than the per-tick scheduler quantum. Returns
4107 /// (tokens, drafted, accepted) for this burst — mid-request quantum overshoot stays
4108 /// public, while only the true request boundary clamps the committed cache prefix.
4109 /// ADMISSION DEBT of this model's verify-graph pool, in bytes (lane/
4110 /// hermes-perf-fixes, 2026-08-23): the projected remaining growth the serve admission
4111 /// gate must reserve so sessions admitted while the pool is cold do not overcommit VRAM
4112 /// the pool will hold (it grows monotonically with no eviction by design — the pool's
4113 /// high-water is per-export and unknown until observed on the serving box; the 1.5 GiB
4114 /// SPEC_SHRINK_RESERVE never covered it). Projection contract and the self-measuring
4115 /// arithmetic live on [`crate::spec::dspark_vg_debt_projection`]; the observed bytes
4116 /// come from the device graph mem pool (`Engine::device_graph_mem_reserved`).
4117 ///
4118 /// CHARGED BY STRUCT, not by which route filled it (lane/graph-launch-guard-sweep-
4119 /// 20260831, fleet-peer refuted-read fix): the MTP spec route's verify-graph door
4120 /// (`MEMRA_SPEC_VERIFY_GRAPH`, family default for GDN+MoE) fills the SAME
4121 /// `dspark_vgraphs` pool with the same monotonic growth, and used to escape charging
4122 /// because the door check named only the dspark flags. 0 when EVERY door is closed
4123 /// (`MEMRA_DSPARK_VERIFY_GRAPH=0` and the MTP door off), frozen
4124 /// (`MEMRA_DSPARK_VG_MAX=0`), or the pool has not captured yet.
4125 pub fn dspark_vg_admission_debt(&self, e: &Engine) -> usize {
4126 let dspark_door =
4127 crate::spec::dspark_verify_graph_serve_on() || crate::spec::dspark_verify_graph_on();
4128 let mtp_door =
4129 crate::spec::spec_verify_graph_env().unwrap_or_else(|| self.vgraph_family_default());
4130 if !dspark_door && !mtp_door {
4131 return 0;
4132 }
4133 let reserved = e.device_graph_mem_reserved();
4134 self.dspark_vgraphs
4135 .lock()
4136 .unwrap()
4137 .as_mut()
4138 .map(|g| g.admission_debt(reserved))
4139 .unwrap_or(0)
4140 }
4141
4142 /// MULTI-TURN RESUME (lane/dflash2-session-reuse, 2026-08-25): continue a parked
4143 /// dspark session with the next turn's suffix — the dspark twin of the MTP pool
4144 /// resume. Trunk rows for the committed stream are already resident in `cache` and
4145 /// their ctx features in `dkv`, so turn N+1 primes ONLY its delta instead of
4146 /// re-priming the whole conversation (the route previously served every turn cold —
4147 /// a full-prompt prime whose cost grows with the conversation).
4148 ///
4149 /// EXACTNESS. The suffix prime is the same session-continuation `prime_cache` the
4150 /// serve path uses for split prompts and LCP restores (chunk N+1 attends chunk N's
4151 /// resident KV); the tap sink collects the suffix rows prompt-relative and the dkv
4152 /// ingest lands them at their absolute positions, exactly as the burst's per-round
4153 /// keep-ingest does. The boundary token re-derives as the cold prime does: greedy
4154 /// argmax of the suffix's last row, or the request's filtered draw through the
4155 /// SESSION's own Philox stream (`sctr` continues — the frspec session-continuity
4156 /// law), penalized over the session+suffix window. A resumed stream is therefore
4157 /// byte-identical to the stream a cold prime of the full concatenation produces —
4158 /// the verify arbitrates every committed token either way.
4159 ///
4160 /// EOS in the committed history is fine (a finished turn parks with EOS committed;
4161 /// the new user turn continues past it) — `done` resets here. Callers must pass a
4162 /// NON-EMPTY suffix for a `done` session (an empty-suffix continuation of a finished
4163 /// stream would re-emit from a terminal state); the worker's probe enforces it.
4164 /// Re-arm a dspark session from a RESTORED trunk cache plus a published draft tail —
4165 /// the long-answer half of lane/dspark-draft-plane-20260827.
4166 ///
4167 /// WHY THIS EXISTS. `dspark_spec_session_new` must prime the full prompt, because the draft
4168 /// KV derives from trunk hidden FEATURES the prime produces as a side effect. A cache hit
4169 /// returns trunk K/V, not features, so before this a speculating request had to discard even
4170 /// a full-prompt hit and re-prefill (~10 s at 30k tokens). With the drafter's readable tail
4171 /// travelling on the entry, both halves are restorable and the discard is unnecessary.
4172 ///
4173 /// WHY IT IS EQUIVALENT TO A COLD PRIME, field by field:
4174 /// * `cache` — the caller's restored trunk cache, already at `prompt.len()` with recurrent
4175 /// state, which is why only WHOLE-ENTRY hits are eligible (a GDN trunk cannot rebuild
4176 /// recurrent state mid-sequence, so there is no LCP arm here — same restriction as the
4177 /// cold path's full-prompt-only rule).
4178 /// * `dkv` — byte-copied from the tail into the SAME absolute rows, so rope positions and
4179 /// every row the windowed SDPA can read are identical to what the prime produced.
4180 /// * `last` — drawn from the entry's boundary logits with the request's own sampler, the
4181 /// same composition the cold path applies to its prime logits.
4182 /// * `pen_hist` / `sctr` / `uctr` — seeded exactly as a cold session's are: the penalty
4183 /// window from this prompt, the Philox counters fresh, because randomness is
4184 /// session-owned by the frspec continuity law and a restore is a NEW session.
4185 /// * `prefix_capture` — `None`: the entry this restored FROM already exists, so
4186 /// republishing the same key would be dropped by the worker's dedupe anyway.
4187 ///
4188 /// Refuses (rather than asserting) whenever the rebuilt draft KV and the cache disagree, so
4189 /// a caller that gets `Err` simply cold-primes.
4190 #[allow(clippy::too_many_arguments)]
4191 pub fn dspark_spec_session_from_restored(
4192 &self,
4193 e: &Engine,
4194 draft: &DflashDraft,
4195 cache: crate::cache::Cache,
4196 prompt: &[u32],
4197 // Draft KV ALREADY rebuilt from the entry's tail by the caller (`DflashKv::from_tail`)
4198 // while the prefix cache was borrowable. Taking the built KV rather than the tail is
4199 // what keeps the ~85 MB tail in the entry for other requests — `from_tail` copies OUT
4200 // of it, so no clone of the tail is ever needed.
4201 dkv: DflashKv,
4202 boundary_logits: &[f32],
4203 sampling: Option<crate::spec::SpecSampling>,
4204 ctx_cap: usize,
4205 ) -> Result<DsparkSpecSession, Box<dyn std::error::Error>> {
4206 assert!(
4207 !self.uses_gemma_program(),
4208 "gemma4 targets use the assistant-drafter route; dspark is the qwen-hybrid arm"
4209 );
4210 if let Some(sp) = sampling.as_ref()
4211 && sp.temp <= 0.0
4212 && sp.pen_on()
4213 {
4214 return Err("penalized greedy is served on the plain path".into());
4215 }
4216 let c = &draft.cfg;
4217 let b = c.block_size;
4218 let is_dflash2 = draft.dflash2.is_some();
4219 let max_ctx = if is_dflash2 {
4220 ctx_cap
4221 } else {
4222 ctx_cap.min(c.sliding_window)
4223 };
4224 let tp = prompt.len();
4225 if !dspark_spec_prompt_fits(tp, ctx_cap, b, c.sliding_window, is_dflash2) {
4226 return Err(format!("restored dspark session does not fit ctx {max_ctx}").into());
4227 }
4228 if cache.pos != tp {
4229 return Err(format!(
4230 "restored dspark session needs a whole-entry trunk cache: cache.pos {} != prompt {tp}",
4231 cache.pos
4232 )
4233 .into());
4234 }
4235 if dkv.len != tp {
4236 return Err(format!("restored draft KV len {} != prompt {tp}", dkv.len).into());
4237 }
4238 if dkv.cap != max_ctx {
4239 return Err(
4240 format!("restored draft KV cap {} != session ctx {max_ctx}", dkv.cap).into(),
4241 );
4242 }
4243 if boundary_logits.is_empty() {
4244 return Err("restored dspark session needs the entry's boundary logits".into());
4245 }
4246 let mut sctr0 = 0u32;
4247 let pen_hist: Vec<u32> = match sampling.as_ref().filter(|s| s.temp > 0.0 && s.pen_on()) {
4248 Some(sp) => crate::spec::pen_window_seed(&[], prompt, sp.penalty_last_n),
4249 None => Vec::new(),
4250 };
4251 let last = match sampling.as_ref().filter(|s| s.temp > 0.0) {
4252 Some(sp) => crate::spec::sample_boundary_token(
4253 e,
4254 boundary_logits,
4255 sp,
4256 &pen_hist,
4257 &mut sctr0,
4258 "dspark-restore",
4259 )?,
4260 None => crate::forward::argmax(boundary_logits) as u32,
4261 };
4262 let nd = DsparkHarvest::for_draft(draft).n_drafts(b);
4263 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
4264 .ok()
4265 .and_then(|v| v.parse().ok())
4266 .unwrap_or(nd + 1)
4267 .clamp(2, nd + 1);
4268 Ok(DsparkSpecSession {
4269 cache,
4270 prefix_capture: None,
4271 dkv,
4272 last,
4273 ctx_len: tp,
4274 vt: vt_cap,
4275 rounds: 0,
4276 max_ctx,
4277 done: false,
4278 snapb: None,
4279 snapb_off: !crate::spec::state_copy_batch_on(),
4280 sampling,
4281 sctr: sctr0,
4282 uctr: 0,
4283 pen_hist,
4284 })
4285 }
4286
4287 pub fn dspark_spec_session_resume(
4288 &self,
4289 e: &Engine,
4290 draft: &DflashDraft,
4291 sess: &mut DsparkSpecSession,
4292 suffix: &[u32],
4293 ) -> Result<(), Box<dyn std::error::Error>> {
4294 use crate::cache::DflashTapSink;
4295 let n_embd = self.cfg.n_embd as usize;
4296 let c = &draft.cfg;
4297 let b = c.block_size;
4298 let n_taps = c.target_layer_ids.len();
4299 let pos0 = sess.cache.pos;
4300 debug_assert_eq!(
4301 sess.ctx_len, pos0,
4302 "dspark resume: draft KV rows != trunk cache rows"
4303 );
4304 if suffix.is_empty() {
4305 return Err(
4306 "dspark resume needs a non-empty suffix (worker probe owns the \
4307 empty-suffix exact-continuation case)"
4308 .into(),
4309 );
4310 }
4311 // SHORT-SUFFIX FLOOR (incident 2026-08-25, box10 crash loop). The suffix prime goes
4312 // through `prime_cache`, which asserts `T >= PRIME_MIN_T` — the batched prefill arm
4313 // has no tokenwise twin that also fills the DFlash tap sink. A resumed turn shorter
4314 // than that floor (the watchdog's "Say OK." class, and any brief agent follow-up)
4315 // therefore PANICKED the GPU worker, which exits 70 and takes every session on the
4316 // box with it: 20 panics and ~5 minutes of 502s on box10 before MEMRA_REUSE_POOL=0
4317 // stopped it. The worker probe declines these before it ever gets here (its own
4318 // guard is the one that keeps the request on the cold path, which is exactly the
4319 // pre-lane behavior); this is the engine-side backstop so no future caller can
4320 // reintroduce the panic, and it is a refusal rather than an assert because a
4321 // too-short turn is ordinary traffic, not a bug.
4322 if suffix.len() < crate::hybrid_forward::PRIME_MIN_T {
4323 return Err(format!(
4324 "dspark resume suffix {} < PRIME_MIN_T {} (prime_cache has no tokenwise \
4325 tap-filling twin); serve this turn cold",
4326 suffix.len(),
4327 crate::hybrid_forward::PRIME_MIN_T
4328 )
4329 .into());
4330 }
4331 let need = pos0
4332 .saturating_add(suffix.len())
4333 .saturating_add(b)
4334 .saturating_add(8);
4335 if need > sess.max_ctx {
4336 return Err(format!(
4337 "dspark resume needs {need} ctx (resident {pos0} + suffix {} + block {b} + 8), \
4338 cap {}",
4339 suffix.len(),
4340 sess.max_ctx
4341 )
4342 .into());
4343 }
4344 let tp = suffix.len();
4345 sess.cache.dflash_taps = Some(DflashTapSink {
4346 layer_ids: c.target_layer_ids.clone(),
4347 buf: e.uninit(tp * n_taps * n_embd)?,
4348 hidden: n_embd,
4349 t: tp,
4350 base: 0,
4351 });
4352 let (logits, _h_seed, _hiddens) = self.prime_cache(e, suffix, &mut sess.cache, 0)?;
4353 let sp_pen = sess.sampling.filter(|s| s.temp > 0.0 && s.pen_on());
4354 if let Some(sp) = sp_pen.as_ref() {
4355 sess.pen_hist = crate::spec::pen_window_seed(&sess.pen_hist, suffix, sp.penalty_last_n);
4356 }
4357 let last = match sess.sampling.filter(|s| s.temp > 0.0) {
4358 Some(sp) => crate::spec::sample_boundary_token(
4359 e,
4360 &logits,
4361 &sp,
4362 &sess.pen_hist,
4363 &mut sess.sctr,
4364 "dspark-resume",
4365 )?,
4366 None => crate::forward::argmax(&logits) as u32,
4367 };
4368 {
4369 let taps = sess.cache.dflash_taps.take().unwrap();
4370 let n_taps_h = n_taps * n_embd;
4371 let mut r0 = 0usize;
4372 while r0 < tp {
4373 let t_c = (tp - r0).min(256);
4374 let tv = e.view(&taps.buf, tp * n_taps_h);
4375 let win = tv.slice(r0 * n_taps_h..(r0 + t_c) * n_taps_h);
4376 let mut chunk = e.uninit(t_c * n_taps_h)?;
4377 e.copy_view_into(&mut chunk, 0, &win, t_c * n_taps_h)?;
4378 let f = draft.ctx_features(e, &chunk, t_c)?;
4379 let pos_c: Vec<i32> = (((pos0 + r0) as i32)..((pos0 + r0 + t_c) as i32)).collect();
4380 draft.ingest_ctx(e, &mut sess.dkv, &f, &pos_c, t_c)?;
4381 r0 += t_c;
4382 }
4383 }
4384 e.stream().synchronize()?;
4385 sess.ctx_len += tp;
4386 sess.last = last;
4387 sess.done = false;
4388 Ok(())
4389 }
4390
4391 pub fn dspark_spec_session_burst(
4392 &self,
4393 e: &Engine,
4394 draft: &DflashDraft,
4395 sess: &mut DsparkSpecSession,
4396 burst_target: usize,
4397 request_room: usize,
4398 eos: &[u32],
4399 ) -> Result<(Vec<u32>, usize, usize), Box<dyn std::error::Error>> {
4400 use crate::cache::DflashTapSink;
4401 let n_embd = self.cfg.n_embd as usize;
4402 let c = &draft.cfg;
4403 let b = c.block_size;
4404 let n_taps = c.target_layer_ids.len();
4405 let n_vocab = self.output.out_features();
4406 // Harvest convention (DSPARK-POSTMORTEM-20260820.md) — identical to the bin arm
4407 // (family-keyed for DFlash2, else checkpoint strategy census; owner-ratified
4408 // flip 2026-08-20).
4409 let harvest = DsparkHarvest::for_draft(draft);
4410 let nd = harvest.n_drafts(b);
4411 let r0 = harvest.first_row();
4412 let vt_cap: usize = std::env::var("MEMRA_DFLASH_VERIFY_T")
4413 .ok()
4414 .and_then(|v| v.parse().ok())
4415 .unwrap_or(nd + 1)
4416 .clamp(2, nd + 1);
4417 let adapt = std::env::var("MEMRA_DFLASH_ADAPT").as_deref() != Ok("0");
4418 // Verify-window policy (H4, DSPARK-POSTMORTEM-20260820.md) — identical to the
4419 // bin arm: default = confidence-slot tau=.5 on a head-carrying checkpoint
4420 // (owner-ratified flip 2026-08-20); head-less (incl. the DFlash2 family) and
4421 // ADAPT=0 keep the ladder.
4422 let vt_policy = DsparkVtPolicy::resolve(draft.confidence.is_some());
4423 if vt_policy.is_confidence() {
4424 assert!(
4425 draft.confidence.is_some(),
4426 "MEMRA_DSPARK_VT={vt_policy:?} needs a checkpoint with an accept-rate \
4427 head (confidence_head.* absent in this export)"
4428 );
4429 }
4430 // SAMPLED ADMISSION (T>0): session-fixed config; counters live on the session so
4431 // randomness never repeats across bursts. None/temp==0 = the greedy route.
4432 let sp_on: Option<crate::spec::SpecSampling> = sess.sampling.filter(|s| s.temp > 0.0);
4433 let pen_on = sp_on.as_ref().is_some_and(|s| s.pen_on());
4434 let mut out: Vec<u32> = Vec::with_capacity(burst_target + b);
4435 let mut drafted = 0usize;
4436 let mut accepted_n = 0usize;
4437 // Engine-bundle slice 2 — identical to the bin arm: deferred chain readback under
4438 // the stash arm with a resident embed table (ladder policy only).
4439 let defer_rb = crate::spec::dspark_defer_readback_on() && !vt_policy.is_confidence();
4440 let (embd_qt, embd_rb) = self.embd.qt_and_row_bytes(n_embd);
4441 let embd_gpu = if !defer_rb || crate::spec::spec_host_embd() {
4442 None
4443 } else {
4444 Some(
4445 self.embd_gpu
4446 .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload")),
4447 )
4448 };
4449 // Slice 3/4c SERVE ENGAGEMENT (graphs-serve lane; DSF-ROUNDCOST §9.3 -> §10): the
4450 // verify-graph pool lives on the MODEL (`dspark_vgraphs`, one per process) and its
4451 // keys — (segment, vt) and (vt, rung, hi) — carry NOTHING session-scoped, so ANY
4452 // session whose round matches a key replays the same capture (this is the
4453 // cache-reuse-pool the old bin-arm-only note asked for). Sharing is sound because
4454 // every per-session-varying address the captured bodies touch is indirect:
4455 // conv/ssm state and the ckpt stash resolve through the per-verify refreshed
4456 // pointer table (refresh_tables + copy_indirect_src_f32 — the slice-3
4457 // parity/lifetime law; a baked address is the known 12/12-divergence class), kv
4458 // bases through fa_table, residual/pos/tap through ctx-owned staging rewritten
4459 // every round; per-row t_kv derives in-kernel from pos_seq, and the per-round
4460 // host bookkeeping (parity swap, len bump) runs on THIS session's cache. The
4461 // guard spans the burst: the slab stash is live verify -> commit inside each
4462 // round, and the worker drives bursts from one scheduler thread
4463 // (step_dspark_spec), so sessions interleave at burst boundaries only.
4464 // DEFAULT ON on the serve route since the v0.103 train (owner-ratified
4465 // 2026-08-22, §10 re-gate at flip): MEMRA_DSPARK_VERIFY_GRAPH=0 is the
4466 // kill-switch that keeps this None — the eager walk, byte-identical (the
4467 // kill-switch arm of the serve battery). The bin arm keeps its own opt-in.
4468 let mut vg_guard = self.dspark_vgraphs.lock().unwrap();
4469 if vg_guard.is_none() && embd_gpu.is_some() && crate::spec::dspark_verify_graph_serve_on() {
4470 *vg_guard = crate::spec::DsparkVerifyGraphs::new(e, &sess.cache, vt_cap, n_embd)?;
4471 if vg_guard.is_some() {
4472 // Engagement receipt (the §8 dead-arm lesson): prove the door is LIVE on
4473 // the serve surface — S6b banked the tip server carrying zero door strings.
4474 eprintln!("[dspark-vg] serve pool ENGAGED (vt_cap={vt_cap})");
4475 }
4476 }
4477 let vgraphs: &mut Option<crate::spec::DsparkVerifyGraphs> = &mut vg_guard;
4478 'outer: while out.len() < burst_target && !sess.done {
4479 let start = sess.cache.pos;
4480 if start + nd + 1 > sess.max_ctx {
4481 sess.done = true;
4482 break;
4483 }
4484 sess.rounds += 1;
4485 let mut vt = sess.vt;
4486 // ---- draft: block = [last, MASK x b-1] (identical to the bin arm) ----
4487 // RAII: a `?` exit restores the pre-scope value instead of latching exact
4488 // ON engine-wide across every later request (hermes finding, fixed
4489 // 2026-08-23 — this burst had several `?`s between the manual true/false).
4490 let exact_scope = e.exact_scope(true);
4491 let mut block: Vec<u32> = vec![c.mask_token_id; b];
4492 block[0] = sess.last;
4493 let noise = e.htod(&self.embd.gather(n_embd, &block))?;
4494 let pos_block: Vec<i32> = ((start as i32)..(start + b) as i32).collect();
4495 let dh = draft.forward_round(e, &mut sess.dkv, &noise, &pos_block)?;
4496 // Harvest: logits over rows r0..r0+nd (see the bin arm / the postmortem).
4497 let mut rows = e.uninit(nd * n_embd)?;
4498 {
4499 let dv = e.view(&dh, b * n_embd);
4500 let src = dv.slice(r0 * n_embd..(r0 + nd) * n_embd);
4501 e.copy_view_into(&mut rows, 0, &src, nd * n_embd)?;
4502 }
4503 // TRIMMED DRAFT HEAD (lane/dflash2-head-trim, 2026-08-25): DFlash2 family
4504 // only — the selector consumes (value, candidate-id) pairs, so a d2t remap
4505 // after top-k restores true ids; the markov/chain arms argmax dl columns
4506 // into token ids DIRECTLY and must keep the full head. Reuses the FR-Spec
4507 // self-trim the load path builds on the MTP struct (MEMRA_FRSPEC_TRIM):
4508 // gathered rows of the target's own head, zero requant. Verify stays
4509 // full-vocab, so the trim moves draft acceptance only, never output.
4510 let trim = if draft.dflash2.is_some() {
4511 self.mtp
4512 .as_ref()
4513 .filter(|m| m.d2t_from_target_head)
4514 .and_then(|m| m.shared_head_head.as_ref().zip(m.d2t.as_ref()))
4515 // MEMRA_MTP_SKIP stub: the same target-head trimmed rows, parked in
4516 // `dflash_trim` because the embedded MTP block was skipped (hybrid.rs;
4517 // rows are target-head by construction; the loader refuses otherwise).
4518 .or_else(|| self.dflash_trim.as_ref().map(|t| (&t.head, &t.d2t)))
4519 .filter(|(_, d2t)| !d2t.is_empty())
4520 } else {
4521 None
4522 };
4523 let (dl_head, dl_vocab) = match trim {
4524 Some((head, d2t)) => (head, d2t.len()),
4525 None => (&self.output, n_vocab),
4526 };
4527 let trim_d2t = trim.map(|(_, d2t)| d2t.as_slice());
4528 let mut dl = e.matmul(dl_head, &rows, nd)?;
4529 // Family/sampling-keyed proposal — identical to the bin arm (see there for
4530 // the program law: sampled records the true q, DFlash2 rides the selector,
4531 // the markov/plain greedy chain keeps the slice-2 deferral). Confidence
4532 // policy: stash markov prev-token embeddings d2d during the chain, one host
4533 // readback after — identical to the bin arm.
4534 let want_conf_emb = vt_policy.is_confidence()
4535 && draft.confidence.as_ref().is_some_and(|ch| ch.with_markov);
4536 let mut conf_emb: Option<CudaSlice<f32>> = match (&draft.markov, want_conf_emb) {
4537 (Some(mk), true) => Some(e.uninit(nd * mk.rank)?),
4538 (None, true) => unreachable!(
4539 "with_markov confidence head without a markov table — the loader forbids it"
4540 ),
4541 _ => None,
4542 };
4543 let mut cand: Vec<u32> = Vec::with_capacity(nd + 1);
4544 let mut prop: Option<DsparkDraftSample> = None;
4545 let mut chain_dev: Option<CudaSlice<u32>> = None;
4546 // Slice 2: arm choice read before the chain readback (see the bin arm; the
4547 // serve arm has no CKPT_GATE oracle — the bin arm carries it).
4548 let ckpt_on = std::env::var("MEMRA_DSPARK_CKPT").as_deref() != Ok("0");
4549 let mut deferred = false;
4550 if let Some(sp) = sp_on.as_ref() {
4551 // SAMPLED proposal (family-keyed; identical to the bin arm).
4552 let (tail, ds) = draft.dspark_propose_sampled(
4553 e,
4554 &mut dl,
4555 &rows,
4556 nd,
4557 dl_vocab,
4558 sess.last,
4559 sp,
4560 &mut sess.sctr,
4561 &mut sess.uctr,
4562 conf_emb.as_mut(),
4563 trim_d2t,
4564 )?;
4565 drop(exact_scope);
4566 cand.push(sess.last);
4567 cand.extend_from_slice(&tail);
4568 prop = Some(ds);
4569 } else if draft.dflash2.is_some() {
4570 // DFlash2: candidate path selector replaces the markov chain
4571 // (identical to the bin arm).
4572 let path = draft
4573 .dflash2_propose_greedy(e, &dl, &rows, nd, dl_vocab, sess.last, trim_d2t)?;
4574 drop(exact_scope);
4575 cand.push(sess.last);
4576 cand.extend_from_slice(&path);
4577 } else {
4578 let markov_on = std::env::var("MEMRA_DFLASH_MARKOV").as_deref() != Ok("0");
4579 let mut chain_d = e.stream().alloc_zeros::<u32>(nd + 1)?;
4580 if let (Some(mk), true) = (&draft.markov, markov_on) {
4581 e.set_u32_one(&mut chain_d, sess.last)?;
4582 for k in 0..nd {
4583 let mut f = e.uninit(mk.rank)?;
4584 e.gather_row_bf16(&mk.w1_bf16, &chain_d, k, &mut f, mk.rank)?;
4585 if let Some(ce) = conf_emb.as_mut() {
4586 let fv = e.view(&f, mk.rank);
4587 e.copy_view_into(ce, k * mk.rank, &fv, mk.rank)?;
4588 }
4589 let bias = e.matmul(&mk.w2, &f, 1)?;
4590 e.add_row_inplace(&mut dl, &bias, n_vocab, k * n_vocab)?;
4591 e.argmax_token_device_col(&dl, k, n_vocab, &mut chain_d, k + 1)?;
4592 }
4593 } else {
4594 if want_conf_emb {
4595 // chain_d[0] must carry the anchor — slot 0's prev token.
4596 e.set_u32_one(&mut chain_d, sess.last)?;
4597 }
4598 for i in 0..nd {
4599 if let (Some(ce), Some(mk)) = (conf_emb.as_mut(), &draft.markov) {
4600 let mut f = e.uninit(mk.rank)?;
4601 e.gather_row_bf16(&mk.w1_bf16, &chain_d, i, &mut f, mk.rank)?;
4602 let fv = e.view(&f, mk.rank);
4603 e.copy_view_into(ce, i * mk.rank, &fv, mk.rank)?;
4604 }
4605 e.argmax_token_device_col(&dl, i, n_vocab, &mut chain_d, i + 1)?;
4606 }
4607 }
4608 drop(exact_scope);
4609 deferred = embd_gpu.is_some() && ckpt_on;
4610 chain_dev = Some(chain_d);
4611 }
4612 // ---- H4 confidence window: size THIS round's verify from the head ----
4613 if vt_policy.is_confidence() {
4614 let ch = draft.confidence.as_ref().expect("asserted at burst entry");
4615 let (rows_h, emb_h) = match conf_emb.as_ref() {
4616 Some(ce) => {
4617 let (a, b2) = e.dtoh_pair(&rows, ce)?;
4618 (a, Some(b2))
4619 }
4620 None => (e.dtoh(&rows)?, None),
4621 };
4622 let rank = draft.markov.as_ref().map(|m| m.rank).unwrap_or(0);
4623 let mut raws = Vec::with_capacity(nd);
4624 for k in 0..nd {
4625 let hrow = &rows_h[k * n_embd..(k + 1) * n_embd];
4626 let emb = emb_h.as_ref().map(|eh| &eh[k * rank..(k + 1) * rank]);
4627 raws.push(ch.raw_score(hrow, emb));
4628 }
4629 vt = vt_policy
4630 .size_window(&raws, vt_cap)
4631 .expect("confidence policies always size the window");
4632 }
4633 // Non-deferred greedy chain readback (the sampled and DFlash2 proposals
4634 // built `cand` at the walk; deferred rounds build it after the merged
4635 // readback — bytes identical, chain_d written before either sync).
4636 if let Some(chain_d) = chain_dev.as_ref()
4637 && !deferred
4638 {
4639 let chain = e.dtoh_u32(chain_d)?;
4640 cand.push(sess.last);
4641 cand.extend_from_slice(&chain[1..]);
4642 }
4643
4644 // ---- snapshot, then verify t=vt (ckpt stash default; oracle arms kept) ----
4645 // Slice 1: batched snap (see DsparkSnapBatch) with the legacy per-layer
4646 // snapshot as the kill-switch / non-uniform fallback.
4647 let mut snap_legacy: Option<crate::cache::CacheSnapshot> = None;
4648 if !sess.snapb_off && sess.snapb.is_none() {
4649 sess.snapb = DsparkSnapBatch::new(e, &sess.cache)?;
4650 sess.snapb_off = sess.snapb.is_none();
4651 } else if let Some(sb) = sess.snapb.as_mut() {
4652 sb.refresh(e, &sess.cache)?;
4653 }
4654 let snap: &crate::cache::CacheSnapshot = match sess.snapb.as_ref() {
4655 Some(sb) => &sb.snap,
4656 None => {
4657 snap_legacy = Some(sess.cache.snapshot(e)?);
4658 snap_legacy.as_ref().unwrap()
4659 }
4660 };
4661 let _ = &snap_legacy;
4662 // Slice 3: the tap-sink buffer is persistent per vt in the graphs ctx
4663 // (captured segments bake its address — a per-round alloc here would make
4664 // every session's replayed tap copies write freed memory); fully rewritten
4665 // by every verify, so pool ownership changes no bytes.
4666 let tap_buf = match vgraphs.as_mut().and_then(|g| g.tap_bufs.remove(&vt)) {
4667 Some(buf) => buf,
4668 None => e.uninit(vt * n_taps * n_embd)?,
4669 };
4670 sess.cache.dflash_taps = Some(DflashTapSink {
4671 layer_ids: c.target_layer_ids.clone(),
4672 buf: tap_buf,
4673 hidden: n_embd,
4674 t: vt,
4675 base: 0,
4676 });
4677 // Composition guard (sampled admission × model-owned pool, this train's
4678 // cross-product): the slab flag is a per-round statement, but only the
4679 // graphs-aware verify (`_am_ckpt_dev`) clears it. Serve sessions MIX arms
4680 // within one process-lifetime pool — a SAMPLED round rides the raw-logits
4681 // twins (no graphs param) and must not inherit `round_slab=true` from a
4682 // previous greedy session's captured round, or its commit is steered at
4683 // slabs the round never wrote. Clear at the round boundary; the deferred
4684 // arm re-derives it inside the verify. (The bin arm has the same shape but
4685 // fixes its sampling mode per process, so no mixed rounds exist there.)
4686 if let Some(g) = vgraphs.as_mut() {
4687 g.round_slab = false;
4688 }
4689 // The whole fallible verify window runs inside a closure so the Err path
4690 // can return the sink buffer to the ctx pool before propagating — the
4691 // serve-surface twin of the EOS-orphan lesson: a mid-verify error
4692 // propagates OUT of the burst, the request dies, the session's cache is
4693 // dropped — but the PROCESS (and the pool, with the tap-buffer address
4694 // baked into its captures) lives on. Recover the ctx-owned buffer before
4695 // the error escapes, or the next session's replayed tap copies write
4696 // freed memory. The bin arm has no such path (a gate-binary error ends
4697 // the process).
4698 #[allow(clippy::type_complexity)] // allow: one-shot composite type; naming it would hide the shape that matters at the call site
4699 let verify_out = (|| -> Result<
4700 (
4701 Vec<u32>,
4702 Option<CudaSlice<f32>>,
4703 Option<crate::spec::DsparkVerifyCkpt>,
4704 ),
4705 Box<dyn std::error::Error>,
4706 > {
4707 if sp_on.is_some() {
4708 // SAMPLED: raw verify logits for the rejection walk (bin-arm twin).
4709 if ckpt_on {
4710 let (tl, vck) = self.dspark_verify_t_logits_ckpt(
4711 e,
4712 &cand[..vt],
4713 start,
4714 &mut sess.cache,
4715 )?;
4716 Ok((Vec::new(), Some(tl), Some(vck)))
4717 } else {
4718 Ok((
4719 Vec::new(),
4720 Some(self.dspark_verify_t_logits(
4721 e,
4722 &cand[..vt],
4723 start,
4724 &mut sess.cache,
4725 )?),
4726 None,
4727 ))
4728 }
4729 } else if deferred {
4730 // Slice 2: device-token verify + ONE merged readback (see the bin arm).
4731 let chain_d = chain_dev.as_ref().expect("deferred implies greedy chain");
4732 let g = embd_gpu.expect("deferred implies resident embed");
4733 let (am_d, vck) = self.dspark_verify_t_am_ckpt_dev(
4734 e,
4735 chain_d,
4736 vt,
4737 start,
4738 &mut sess.cache,
4739 (g, embd_qt, embd_rb),
4740 vgraphs.as_mut(),
4741 )?;
4742 let ch = e.stream().clone_dtoh(chain_d)?;
4743 let am = e.stream().clone_dtoh(&am_d)?;
4744 e.stream().synchronize()?;
4745 cand.push(sess.last);
4746 cand.extend_from_slice(&ch[1..]);
4747 Ok((am, None, Some(vck)))
4748 } else if ckpt_on {
4749 let (vam, vck) =
4750 self.dspark_verify_t_am_ckpt(e, &cand[..vt], start, &mut sess.cache)?;
4751 Ok((vam, None, Some(vck)))
4752 } else {
4753 Ok((
4754 self.dspark_verify_t_am(e, &cand[..vt], start, &mut sess.cache)?,
4755 None,
4756 None,
4757 ))
4758 }
4759 })();
4760 let (vam, tl, vck) = match verify_out {
4761 Ok(v) => v,
4762 Err(err) => {
4763 if let Some(taps) = sess.cache.dflash_taps.take()
4764 && let Some(g) = vgraphs.as_mut()
4765 {
4766 g.tap_bufs.insert(vt, taps.buf);
4767 }
4768 return Err(err);
4769 }
4770 };
4771 let taps = sess.cache.dflash_taps.take().unwrap();
4772 // Return the tap buffer to the ctx pool IMMEDIATELY — an EOS/budget break
4773 // between accept and ingest must never orphan an address the captured graphs
4774 // bake (the bin arm's lesson, and it holds doubly here: the pool outlives
4775 // the SESSION, not just the round). Ingest reads it borrowed.
4776 let tap_local: Option<CudaSlice<f32>> = match vgraphs.as_mut() {
4777 Some(g) => {
4778 g.tap_bufs.insert(vt, taps.buf);
4779 None
4780 }
4781 None => Some(taps.buf),
4782 };
4783 let tap_ref: &CudaSlice<f32> = match &tap_local {
4784 Some(b) => b,
4785 None => &vgraphs.as_ref().expect("ctx present above").tap_bufs[&vt],
4786 };
4787
4788 // ---- accept ----
4789 // Penalized-sampled: anchor joins the window before the walk (committed this
4790 // round via the out.push below); accepted drafts extend it after — identical
4791 // to the bin arm.
4792 if pen_on {
4793 sess.pen_hist.push(sess.last);
4794 }
4795 let (m, next) = match (sp_on.as_ref(), tl.as_ref()) {
4796 (Some(sp), Some(tl)) => {
4797 let w0 = sess
4798 .pen_hist
4799 .len()
4800 .saturating_sub(sp.penalty_last_n.min(crate::spec::PEN_WINDOW_MAX));
4801 dspark_accept_sampled(
4802 e,
4803 tl,
4804 &cand,
4805 vt,
4806 n_vocab,
4807 &dl,
4808 prop.as_ref()
4809 .expect("sampled round without a proposal record"),
4810 sp,
4811 &sess.pen_hist[w0..],
4812 &mut sess.sctr,
4813 &mut sess.uctr,
4814 )?
4815 }
4816 _ => {
4817 let m = dspark_accept_prefix(&cand, &vam, vt);
4818 (m, vam[m])
4819 }
4820 };
4821 drafted += vt - 1;
4822 accepted_n += m;
4823 // keep = the rows this round adds to the PUBLIC stream. Without eos that is
4824 // the anchor + all accepted drafts (m+1). With eos it is the anchor + drafts
4825 // UP TO AND INCLUDING eos: the walk may accept real tokens past eos (they are
4826 // the model's own continuation), but emission stops at eos, and a parked
4827 // session whose cache holds rows past the public stream can never resume —
4828 // the park gate `pos() == fed` would refuse every eos-terminated stream
4829 // (measured: 7/8 turns on the mtreuse gate, overshoot 1-6 rows). Truncating
4830 // the commit at eos uses the SAME prefix-commit machinery as a mid-round
4831 // rejection, so the hybrid (GDN) state is exact by the same argument.
4832 // Emitted bytes are untouched — this only changes post-eos cache state.
4833 let mut keep = m + 1;
4834 let mut terminal = false;
4835 if eos.contains(&sess.last) {
4836 terminal = true;
4837 keep = 1;
4838 } else {
4839 for (j, &dt) in cand[1..=m].iter().enumerate() {
4840 if eos.contains(&dt) {
4841 terminal = true;
4842 keep = j + 2; // anchor + drafts through eos
4843 break;
4844 }
4845 }
4846 }
4847 // The request's max_tokens boundary is also a commit boundary, not merely an
4848 // output slice. It is NOT the scheduler's smaller per-tick burst quantum: accepted
4849 // surplus crossing that quantum stays public and the session remains live. Only at
4850 // the true request boundary do we keep the publishable prefix so cache.pos == fed at
4851 // retire and mark the session terminal until a non-empty next-turn suffix resumes
4852 // it. This uses the same prefix-commit machinery as EOS/rejection and makes
4853 // max-token sessions safe to park instead of permanently cold (Hermes
4854 // `f22a180d1638b95a`).
4855 let (bounded_keep, budget_terminal) =
4856 dspark_commit_limit(keep, out.len(), request_room);
4857 keep = bounded_keep;
4858 terminal |= budget_terminal;
4859 out.push(sess.last);
4860 out.extend_from_slice(&cand[1..keep]);
4861 sess.done = terminal;
4862 if pen_on {
4863 // Only the PUBLIC drafts feed the penalty window — tokens accepted past
4864 // eos never reach the stream, and a resumed session must not penalize
4865 // ghosts (the parked pen_hist seeds the resume's window).
4866 sess.pen_hist.extend_from_slice(&cand[1..keep]);
4867 }
4868
4869 // ---- commit/rollback (stash arm default; replay oracle kept) ----
4870 // Slice 3: rounds whose linear column stash lives in the graphs ctx's slabs
4871 // commit through the slab twin (same semantics, slab-addressed sources) —
4872 // identical to the bin arm's dispatch.
4873 let slab_commit = vgraphs.as_ref().map(|g| g.round_slab).unwrap_or(false);
4874 if keep < vt {
4875 if slab_commit {
4876 self.dspark_commit_prefix_slab(
4877 e,
4878 &mut sess.cache,
4879 snap,
4880 vgraphs.as_ref().expect("slab_commit implies ctx"),
4881 keep,
4882 )?;
4883 } else if let Some(vck) = vck.as_ref() {
4884 self.dspark_commit_prefix(e, &mut sess.cache, snap, vck, keep)?;
4885 } else {
4886 crate::pp::restore_cache_checkpoint(e, self, None, &mut sess.cache, snap)?;
4887 debug_assert_eq!(sess.cache.pos, start, "rollback landed off the round start");
4888 let ram = self.dspark_verify_t_am(e, &cand[..keep], start, &mut sess.cache)?;
4889 if sp_on.is_none() {
4890 // greedy-only oracle; the sampled arm replays to rebuild state.
4891 debug_assert_eq!(
4892 &ram[..],
4893 &vam[..keep],
4894 "prefix replay must reproduce the verify argmaxes"
4895 );
4896 }
4897 }
4898 }
4899
4900 // ---- ingest the kept rows' ctx features into the draft KV ----
4901 {
4902 let tv = e.view(tap_ref, vt * n_taps * n_embd);
4903 let keep_view = tv.slice(0..keep * n_taps * n_embd);
4904 let mut kept = e.uninit(keep * n_taps * n_embd)?;
4905 e.copy_view_into(&mut kept, 0, &keep_view, keep * n_taps * n_embd)?;
4906 let f = draft.ctx_features(e, &kept, keep)?;
4907 let pos_k: Vec<i32> =
4908 ((sess.ctx_len as i32)..(sess.ctx_len + keep) as i32).collect();
4909 draft.ingest_ctx(e, &mut sess.dkv, &f, &pos_k, keep)?;
4910 sess.ctx_len += keep;
4911 }
4912 if sess.done {
4913 // EOS or the public budget landed this round: cache, draft KV and ctx_len are
4914 // all clamped to the public stream (park shape); `next` is beyond the terminal
4915 // boundary and must not become the anchor of a resumed session.
4916 break 'outer;
4917 }
4918 sess.last = next;
4919 // Ladder update only — the confidence policies recompute vt from the
4920 // head every round, post-draft pre-verify; their carry just keeps
4921 // observability (sess.vt = the last confidence-sized window).
4922 if vt_policy.is_confidence() {
4923 sess.vt = vt;
4924 } else if adapt {
4925 sess.vt = (m + 2).clamp(3, vt_cap);
4926 }
4927 }
4928 Ok((out, drafted, accepted_n))
4929 }
4930}
4931
4932// ================= Harvest-convention gate (CPU; DSPARK-POSTMORTEM-20260820.md) =========
4933// The parity oracle is row-count-agnostic (it reproduces the markov MODULE on whatever
4934// rows it is fed) and the E2E gate is harvest-independent (verify-side truth), so
4935// NEITHER can catch a wrong row->position mapping — that blindness is how the q38
4936// misalignment shipped. These tests pin the convention itself as logic the round
4937// consumes, so a mutation back to the mask-fill harvest under the Dspark variant fails
4938// HERE, naming the convention.
4939#[cfg(test)]
4940mod dflash2_tests {
4941
4942 /// The tail-import refusal arms (lane/dspark-draft-plane-20260827 review finding: these
4943 /// were claimed tested and were not). Pure, so they run everywhere; the geometry mirrors
4944 /// the served DFlash2 drafter (5 layers, 8 kv x 128 dim f32 rows, window 2048 + block 8).
4945 #[test]
4946 fn tail_import_refuses_every_geometry_disagreement_and_accepts_the_exported_shape() {
4947 let rb = 8 * 128 * 4; // n_kv * head_dim * f32
4948 let win = 2048 + 8; // window_rows = sliding_window + block
4949 // THE EXPORTED SHAPE: window_rows ending exactly at len, same geometry — accepted.
4950 assert!(
4951 super::tail_geometry_ok(5, rb, 30_329 - win, win, 30_329, 5, rb, win, 34_433).is_ok()
4952 );
4953 // A short history where the tail IS the whole history — accepted.
4954 assert!(super::tail_geometry_ok(5, rb, 0, 100, 100, 5, rb, win, 34_433).is_ok());
4955 // Every refusal arm, each by name:
4956 let arm =
4957 |l, r, b, rows, len, cap| super::tail_geometry_ok(l, r, b, rows, len, 5, rb, win, cap);
4958 assert_eq!(
4959 arm(4, rb, 30_329 - win, win, 30_329, 34_433).unwrap_err(),
4960 "layer count differs from the live drafter"
4961 );
4962 assert_eq!(
4963 arm(5, rb - 4, 30_329 - win, win, 30_329, 34_433).unwrap_err(),
4964 "row geometry differs from the live drafter"
4965 );
4966 assert_eq!(
4967 arm(5, rb, 30_329 - win, win, 30_329, 30_000).unwrap_err(),
4968 "logical length exceeds the session cap"
4969 );
4970 // THE RUN-2 BUG, pinned: a tail whose base+rows lands past its own logical length —
4971 // the export-at-current-length defect the gate caught on the box.
4972 assert_eq!(
4973 arm(5, rb, 30_364 - win, win, 30_329, 34_433).unwrap_err(),
4974 "tail does not end at its own logical length"
4975 );
4976 assert_eq!(
4977 arm(5, rb, 30_329 - (win - 100), win - 100, 30_329, 34_433).unwrap_err(),
4978 "tail shorter than the drafter's readable window"
4979 );
4980 }
4981
4982 use super::{
4983 DsparkHarvest, dflash2_walk_greedy, dflash2_walk_sampled, dspark_commit_limit,
4984 rejection_accept_len,
4985 };
4986
4987 #[test]
4988 fn max_tokens_caps_the_committed_prefix_not_only_the_visible_slice() {
4989 // A round crossing the scheduler's 32-token quantum is not terminal when the
4990 // request still has room. The whole accepted prefix stays public and committed.
4991 assert_eq!(dspark_commit_limit(5, 30, 100), (5, false));
4992 // The same round at the true request boundary is clamped and terminal so the
4993 // parked cache cannot contain rows the worker did not publish.
4994 assert_eq!(dspark_commit_limit(5, 30, 33), (3, true));
4995 assert_eq!(dspark_commit_limit(2, 3, 10), (2, false));
4996 assert_eq!(dspark_commit_limit(1, 0, 1), (1, false));
4997 }
4998
4999 /// f32 -> bf16 bytes (truncation; test values are bf16-exact small integers).
5000 fn bf16(vals: &[f32]) -> Vec<u8> {
5001 vals.iter()
5002 .flat_map(|v| ((v.to_bits() >> 16) as u16).to_le_bytes())
5003 .collect()
5004 }
5005
5006 const V: usize = 8; // test vocab
5007 const R: usize = 2; // selector rank
5008 const K: usize = 2; // top_k
5009
5010 /// Codebooks for the chain tests: pred rows are one-hot-ish, succ rows chosen so
5011 /// the slot-1 winner FLIPS with the slot-0 choice.
5012 #[allow(clippy::identity_op)] // allow: the explicit +0/*1/>>0 terms document the lane/byte symmetry of the reference layout
5013 fn books() -> (Vec<u8>, Vec<u8>) {
5014 let mut pred = vec![0f32; V * R];
5015 pred[0] = 1.0; // tok 0: [1, 0] (the anchor)
5016 pred[1 * R + 1] = 1.0; // tok 1: [0, 1]
5017 pred[2 * R] = 1.0; // tok 2: [1, 0]
5018 let mut succ = vec![0f32; V * R];
5019 succ[1 * R] = 2.0; // tok 1: [2, 0]
5020 succ[2 * R + 1] = 5.0; // tok 2: [0, 5]
5021 succ[3 * R + 1] = 3.0; // tok 3: [0, 3]
5022 succ[4 * R] = 10.0; // tok 4: [10, 0]
5023 (bf16(&pred), bf16(&succ))
5024 }
5025
5026 #[test]
5027 fn selector_walk_is_a_chain_not_per_slot_argmax() {
5028 let (pred, succ) = books();
5029 // slot 0 candidates {1, 2}, slot 1 candidates {3, 4}; hproj all-ones.
5030 let cand: Vec<u32> = vec![1, 2, 3, 4];
5031 let hproj = vec![1.0f32; 2 * R];
5032 // Anchor 0 (pred [1,0]): slot 0 scores = <[1,0],succ> -> tok1: 2, tok2: 0
5033 // -> picks 1. Slot 1 must then walk from pred[1]=[0,1]: tok3 scores 3,
5034 // tok4 scores 0 -> picks 3. A mutation that seeds every slot from the ANCHOR
5035 // (pred[0]=[1,0]) scores tok3: 0 / tok4: 10 and picks 4 instead — the chain
5036 // IS the semantics (reference CandidateSelector.select: `predecessor` is the
5037 // previously CHOSEN candidate, seeded by anchor_ids).
5038 let path = dflash2_walk_greedy(&pred, &succ, V, R, K, &[0.0; 4], &cand, &hproj, 0, 2);
5039 assert_eq!(
5040 path,
5041 vec![1, 3],
5042 "walk must seed slot p from slot p-1's CHOSEN candidate \
5043 (z-lab model.py CandidateSelector.select)"
5044 );
5045 }
5046
5047 #[test]
5048 fn selector_walk_unary_term_participates() {
5049 let (pred, succ) = books();
5050 let cand: Vec<u32> = vec![1, 2, 3, 4];
5051 let hproj = vec![1.0f32; 2 * R];
5052 // unary +10 on slot-0 candidate 2 overrides the bilinear 2-vs-0 margin;
5053 // the chain then walks from pred[2]=[1,0] and slot 1 flips to tok 4.
5054 let path = dflash2_walk_greedy(
5055 &pred,
5056 &succ,
5057 V,
5058 R,
5059 K,
5060 &[0.0, 10.0, 0.0, 0.0],
5061 &cand,
5062 &hproj,
5063 0,
5064 2,
5065 );
5066 assert_eq!(
5067 path,
5068 vec![2, 4],
5069 "score = unary + bilinear (reference: `unary[:, position] + einsum(...)`); \
5070 dropping the unary term picks tok 1 here"
5071 );
5072 }
5073
5074 #[test]
5075 fn selector_walk_hidden_gate_participates() {
5076 let (pred, succ) = books();
5077 let cand: Vec<u32> = vec![1, 2, 3, 4];
5078 // hproj [0, .] zeroes the pred[0]=[1,0] gate for slot 0: tok1's bilinear 2
5079 // vanishes, and the unary tiebreak (+1 on tok2) decides. The chain from tok2
5080 // (pred [1,0]) with slot-1 hproj [1,1] then picks tok4 (10 vs 0).
5081 let hproj = vec![0.0f32, 1.0, 1.0, 1.0];
5082 let path = dflash2_walk_greedy(
5083 &pred,
5084 &succ,
5085 V,
5086 R,
5087 K,
5088 &[0.0, 1.0, 0.0, 0.0],
5089 &cand,
5090 &hproj,
5091 0,
5092 2,
5093 );
5094 assert_eq!(
5095 path,
5096 vec![2, 4],
5097 "the bilinear gate is pred_row .* HIDDEN_PROJECTION (reference: \
5098 `predecessor_codebook(predecessor) * hidden[:, position]`); ignoring \
5099 hproj leaves tok1's margin standing"
5100 );
5101 }
5102
5103 #[test]
5104 fn dflash2_harvest_is_census_keyed() {
5105 // DFlash2 is mask-fill BY CONSTRUCTION (reference dflash_generate harvests
5106 // rows 1-verify_size:; card: "7 draft tokens per verification step").
5107 assert_eq!(
5108 DsparkHarvest::for_family_value(true, None, false),
5109 DsparkHarvest::Dflash
5110 );
5111 assert_eq!(
5112 DsparkHarvest::for_family_value(true, Some("dflash"), false),
5113 DsparkHarvest::Dflash
5114 );
5115 // The family key BEATS the strategy census: a (hypothetical) DFlash2 export
5116 // whose config also strategy-censuses dspark still harvests mask-fill.
5117 assert_eq!(
5118 DsparkHarvest::for_family_value(true, None, true),
5119 DsparkHarvest::Dflash
5120 );
5121 // An env override to the SHIFTED harvest contradicts the census — REFUSE,
5122 // never re-key (the postmortem's misalignment class in reverse).
5123 assert!(
5124 std::panic::catch_unwind(|| DsparkHarvest::for_family_value(
5125 true,
5126 Some("dspark"),
5127 false
5128 ))
5129 .is_err(),
5130 "MEMRA_DSPARK_HARVEST=dspark on a DFlash2 checkpoint must refuse"
5131 );
5132 // Non-DFlash2 checkpoints ride the strategy-keyed resolution (env wins).
5133 assert_eq!(
5134 DsparkHarvest::for_family_value(false, Some("dspark"), false),
5135 DsparkHarvest::Dspark
5136 );
5137 assert_eq!(
5138 DsparkHarvest::for_family_value(false, None, false),
5139 DsparkHarvest::Dflash
5140 );
5141 assert_eq!(
5142 DsparkHarvest::for_family_value(false, None, true),
5143 DsparkHarvest::Dspark,
5144 "unset env on a DSPARK-strategy export must keep the ratified census flip"
5145 );
5146 }
5147
5148 // ============ SAMPLED ADMISSION (T>0) gates — lane/dspark-sampled-admission-20260820 =
5149 // The device kernels are oracled by sample_check (filter_stats/gumbel/residual arms);
5150 // these pin the HOST math the route ships — the selector's sampled walk, the accept
5151 // rule, and the round COMPOSITION (accept + residual + bonus must reproduce the target
5152 // distribution p exactly; a mis-composition leaves every kernel individually correct,
5153 // which is why the composition arm exists — sample_check arm 6's lesson).
5154
5155 #[test]
5156 fn sampled_walk_tiny_temp_matches_greedy() {
5157 // T->0 continuity: at tiny temperature the candidate softmax concentrates on the
5158 // argmax and the sampled walk must reproduce the greedy chain token-for-token
5159 // (the frspec gate-(1) shape). Same fixture as the chain test.
5160 let (pred, succ) = books();
5161 let cand: Vec<u32> = vec![1, 2, 3, 4];
5162 let hproj = vec![1.0f32; 2 * R];
5163 let greedy = dflash2_walk_greedy(&pred, &succ, V, R, K, &[0.0; 4], &cand, &hproj, 0, 2);
5164 let mut u = || 0.5f32;
5165 let (path, q_chosen, q_rows) = dflash2_walk_sampled(
5166 &pred, &succ, V, R, K, &[0.0; 4], &cand, &hproj, 0, 2, 1e-6, &mut u,
5167 );
5168 assert_eq!(
5169 path, greedy,
5170 "tiny-T sampled walk must equal the greedy chain"
5171 );
5172 assert_eq!(q_rows.len(), 2 * K);
5173 for (p, &q) in path.iter().zip(&q_chosen) {
5174 let _ = p;
5175 assert!(
5176 q > 0.999,
5177 "tiny-T chosen-candidate prob must be ~1, got {q}"
5178 );
5179 }
5180 }
5181
5182 #[test]
5183 fn sampled_walk_records_the_distribution_it_samples() {
5184 // The recorded q IS the proposal: per slot the q_rows sum to ~1, q_chosen is the
5185 // row value at the drawn candidate, and the CDF walk picks the candidate whose
5186 // cumulative bracket contains the uniform.
5187 let (pred, succ) = books();
5188 let cand: Vec<u32> = vec![1, 2, 3, 4];
5189 let hproj = vec![1.0f32; 2 * R];
5190 // slot-0 scores at anchor 0: tok1 = 2.0, tok2 = 0.0; at T=2.0 the softmax is
5191 // e^1/(e^1+e^0) ~= 0.731 for tok1.
5192 let q1 = (1f64.exp() / (1f64.exp() + 1.0)) as f32;
5193 for (u0, want0) in [(q1 - 0.01, 1u32), (q1 + 0.01, 2u32)] {
5194 let mut seq = vec![u0, 0.0f32].into_iter();
5195 let mut u = move || seq.next().unwrap();
5196 let (path, q_chosen, q_rows) = dflash2_walk_sampled(
5197 &pred, &succ, V, R, K, &[0.0; 4], &cand, &hproj, 0, 2, 2.0, &mut u,
5198 );
5199 assert_eq!(
5200 path[0], want0,
5201 "CDF walk must place u={u0} in the right candidate bracket"
5202 );
5203 let row0: f32 = q_rows[..K].iter().sum();
5204 assert!(
5205 (row0 - 1.0).abs() < 1e-5,
5206 "slot-0 q must sum to 1, got {row0}"
5207 );
5208 let ci = cand[..K].iter().position(|&c| c == path[0]).unwrap();
5209 assert_eq!(
5210 q_chosen[0], q_rows[ci],
5211 "q_chosen must be the recorded row prob of the drawn candidate"
5212 );
5213 assert!(
5214 (q_rows[0] - q1).abs() < 1e-4,
5215 "slot-0 tok1 prob must be softmax(scores/T), got {} want {q1}",
5216 q_rows[0]
5217 );
5218 }
5219 }
5220
5221 #[test]
5222 fn sampled_walk_chains_the_drawn_candidate() {
5223 // The chain conditions on the DRAWN candidate, not the argmax: forcing the
5224 // low-prob slot-0 candidate (tok 2) flips slot 1's winner (tok 4 over tok 3),
5225 // exactly like the greedy chain test — a walk that seeds every slot from the
5226 // anchor (or the argmax) fails here.
5227 let (pred, succ) = books();
5228 let cand: Vec<u32> = vec![1, 2, 3, 4];
5229 let hproj = vec![1.0f32; 2 * R];
5230 let mut seq = vec![0.99f32, 0.01].into_iter();
5231 let mut u = move || seq.next().unwrap();
5232 let (path, _, _) = dflash2_walk_sampled(
5233 &pred, &succ, V, R, K, &[0.0; 4], &cand, &hproj, 0, 2, 2.0, &mut u,
5234 );
5235 assert_eq!(path[0], 2, "u=0.99 must draw the low-prob candidate");
5236 assert_eq!(
5237 path[1], 4,
5238 "slot 1 must walk from pred[2] (the DRAWN token), which scores tok4 at 10 \
5239 — chaining from the anchor or the argmax picks tok3"
5240 );
5241 }
5242
5243 #[test]
5244 fn rejection_accept_walk_is_the_leviathan_rule() {
5245 // accept while u*q < p, strict, prefix-stop at the first reject.
5246 assert_eq!(
5247 rejection_accept_len(&[0.5, 0.5], &[0.5, 0.5], &[0.9, 0.9]),
5248 2
5249 );
5250 assert_eq!(
5251 rejection_accept_len(&[0.5, 0.5], &[0.5, 0.5], &[1.0, 0.0]),
5252 0
5253 );
5254 // u*q == p is a REJECT (strict <) — the frspec test byte-for-byte.
5255 assert_eq!(rejection_accept_len(&[0.25], &[0.5], &[0.5]), 0);
5256 // q == 0 with p > 0 accepts unconditionally (the skey exactness signature).
5257 assert_eq!(rejection_accept_len(&[1e-6], &[0.0], &[0.999]), 1);
5258 // prefix stop: slot 1 rejects, slot 2 never tested.
5259 assert_eq!(
5260 rejection_accept_len(&[0.9, 0.0, 0.9], &[0.1, 0.9, 0.1], &[0.5, 0.5, 0.5]),
5261 1
5262 );
5263 }
5264
5265 // ---- round composition: the committed-token distribution must equal the target p ----
5266 // CPU mirror of the shipped rule for the FIRST post-anchor slot: draft x ~ q, accept
5267 // iff u*q(x) < p(x) (rejection_accept_len — the shipped fn), else commit a residual
5268 // sample ~ norm(max(0, p - q)). The marginal of the committed token is exactly p —
5269 // for ANY q — which is the whole correctness claim of the route's sampled admission.
5270
5271 fn tv(a: &[f64], b: &[f64]) -> f64 {
5272 a.iter().zip(b).map(|(x, y)| (x - y).abs()).sum::<f64>() / 2.0
5273 }
5274
5275 /// One composed trial with an injectable accept rule; returns the committed token.
5276 #[allow(clippy::neg_cmp_op_on_partial_ord)] // allow: NaN must take this branch; !(a > b) is not a <= b under IEEE comparisons
5277 fn compose_once(
5278 p: &[f32],
5279 q: &[f32],
5280 u_draw: f32,
5281 u_accept: f32,
5282 u_resid: f32,
5283 invert_accept: bool,
5284 skip_q_in_residual: bool,
5285 ) -> usize {
5286 let n = p.len();
5287 // draft ~ q (CDF walk, the walk_sampled convention)
5288 let mut acc = 0f64;
5289 let mut x = n - 1;
5290 for (i, &qi) in q.iter().enumerate() {
5291 acc += qi as f64;
5292 if (u_draw as f64) < acc {
5293 x = i;
5294 break;
5295 }
5296 }
5297 let accepted = if invert_accept {
5298 !((u_accept as f64) * (q[x] as f64) < p[x] as f64)
5299 } else {
5300 rejection_accept_len(&p[x..=x], &q[x..=x], &[u_accept]) == 1
5301 };
5302 if accepted {
5303 return x;
5304 }
5305 // residual ~ norm(max(0, p - q)) (the device kernel's fixed-order CDF walk)
5306 let r: Vec<f64> = p
5307 .iter()
5308 .zip(q)
5309 .map(|(&pi, &qi)| {
5310 let qq = if skip_q_in_residual { 0.0 } else { qi as f64 };
5311 (pi as f64 - qq).max(0.0)
5312 })
5313 .collect();
5314 let total: f64 = r.iter().sum();
5315 let mut acc = 0f64;
5316 let target = u_resid as f64 * total;
5317 for (i, &ri) in r.iter().enumerate() {
5318 acc += ri;
5319 if acc >= target && ri > 0.0 {
5320 return i;
5321 }
5322 }
5323 n - 1
5324 }
5325
5326 fn compose_tv(q: &[f32], invert_accept: bool, skip_q_in_residual: bool) -> f64 {
5327 // target p: a spread-out 8-token distribution
5328 let p: Vec<f32> = vec![0.30, 0.22, 0.15, 0.12, 0.09, 0.06, 0.04, 0.02];
5329 let trials = 200_000usize;
5330 let mut counts = [0f64; V];
5331 for t in 0..trials {
5332 // three independent uniforms per trial off the host Philox stream
5333 let u_draw = crate::spec::host_u01(7, (t * 3) as u32);
5334 let u_accept = crate::spec::host_u01(7, (t * 3 + 1) as u32);
5335 let u_resid = crate::spec::host_u01(7, (t * 3 + 2) as u32);
5336 counts[compose_once(
5337 &p,
5338 q,
5339 u_draw,
5340 u_accept,
5341 u_resid,
5342 invert_accept,
5343 skip_q_in_residual,
5344 )] += 1.0;
5345 }
5346 let emp: Vec<f64> = counts.iter().map(|c| c / trials as f64).collect();
5347 let pf: Vec<f64> = p.iter().map(|&v| v as f64).collect();
5348 tv(&emp, &pf)
5349 }
5350
5351 #[test]
5352 fn sampled_round_composition_matches_the_target() {
5353 // Monte-Carlo floor at 200k draws over 8 tokens ~ 0.004 TV; bound 0.01.
5354 // (a) full-vocab q (the Rows families' shape), far from p;
5355 let q_rows: Vec<f32> = vec![0.02, 0.04, 0.06, 0.09, 0.12, 0.15, 0.22, 0.30];
5356 // (b) SPARSE candidate-set q (the DFlash2 selector shape: support on 2 of 8).
5357 let q_sparse: Vec<f32> = vec![0.0, 0.7, 0.0, 0.3, 0.0, 0.0, 0.0, 0.0];
5358 for (name, q) in [("rows", &q_rows), ("sparse", &q_sparse)] {
5359 let d = compose_tv(q, false, false);
5360 assert!(
5361 d < 0.01,
5362 "composition[{name}]: committed-token distribution must equal p \
5363 (TV {d:.4} >= 0.01)"
5364 );
5365 }
5366 }
5367
5368 #[test]
5369 fn composition_teeth_inverted_accept_fails() {
5370 // DECISIVE teeth: the same harness with the accept inequality inverted must
5371 // MISS the target — otherwise the composition gate is vacuous.
5372 let q: Vec<f32> = vec![0.02, 0.04, 0.06, 0.09, 0.12, 0.15, 0.22, 0.30];
5373 let d = compose_tv(&q, true, false);
5374 assert!(
5375 d > 0.05,
5376 "inverted accept rule must fail the composition bound (TV {d:.4})"
5377 );
5378 }
5379
5380 #[test]
5381 fn composition_teeth_residual_without_q_fails() {
5382 // Sampling the reject slot from p instead of norm(max(0, p-q)) double-counts
5383 // the overlap mass min(p,q) — the committed distribution leaves p.
5384 let q: Vec<f32> = vec![0.0, 0.7, 0.0, 0.3, 0.0, 0.0, 0.0, 0.0];
5385 let d = compose_tv(&q, false, true);
5386 assert!(
5387 d > 0.05,
5388 "residual that skips the q subtraction must fail the bound (TV {d:.4})"
5389 );
5390 }
5391
5392 // ---- PENALIZED round composition (lane/dspark-penalized-sampled-20260821) ----
5393 // Multi-slot rounds where the penalty state EVOLVES within the round. The base trunk
5394 // logits are state-independent, so ALL context dependence flows through penalties —
5395 // the sharpest fixture for "verify row j's target is penalized by the tokens accepted
5396 // before j in the same round", and the proposal concentrates on ONE token, so the
5397 // dominant drafted block is a self-hit (a drafted token penalizing its own successor
5398 // — the within-round case a frozen round-start window cannot see). The reference is
5399 // EXACT (analytic chain p1(a)·p2(b|a), the plain sampler's semantics); the spec arm
5400 // is the shipped round rule — chain draw from q, `rejection_accept_len`, residual
5401 // norm(max(0, p−q)) at the reject slot, bonus from the one-past row on full accept —
5402 // with per-slot penalized p (mirroring penalize_logits_rows_inc_f32's window rule).
5403
5404 const PV: usize = 6;
5405 const PEN_REP: f32 = 1.6;
5406 const PEN_FREQ: f32 = 0.8;
5407 const PEN_PRESENT: f32 = 1.2;
5408
5409 fn pen_base() -> Vec<f32> {
5410 vec![1.5, 0.8, 0.3, -0.2, -0.7, -1.2]
5411 }
5412
5413 /// The proposal: heavy on token 0 so drafted blocks repeat it (the self-hit case).
5414 fn pen_q() -> Vec<f32> {
5415 vec![0.85, 0.06, 0.04, 0.03, 0.01, 0.01]
5416 }
5417
5418 /// CPU mirror of penalize_logits_f32 / the plain sampler's apply_penalties: first
5419 /// occurrence does the whole adjustment, cnt = occurrences in the window, rep
5420 /// divides positive logits and multiplies negative ones.
5421 fn pen_apply(logits: &mut [f32], window: &[u32]) {
5422 let mut seen: Vec<u32> = Vec::new();
5423 for &id in window {
5424 if seen.contains(&id) {
5425 continue;
5426 }
5427 seen.push(id);
5428 let cnt = window.iter().filter(|&&h| h == id).count() as f32;
5429 let v = &mut logits[id as usize];
5430 if *v > 0.0 {
5431 *v /= PEN_REP;
5432 } else {
5433 *v *= PEN_REP;
5434 }
5435 *v -= PEN_FREQ * cnt + PEN_PRESENT;
5436 }
5437 }
5438
5439 /// Penalized target at history `window` (temp 1.0, no truncation filters — those are
5440 /// orthogonal and covered by the unpenalized composition tests + kernel oracles).
5441 fn pen_target(window: &[u32]) -> Vec<f64> {
5442 let mut l = pen_base();
5443 pen_apply(&mut l, window);
5444 let mx = l.iter().cloned().fold(f32::NEG_INFINITY, f32::max) as f64;
5445 let ex: Vec<f64> = l.iter().map(|&v| ((v as f64) - mx).exp()).collect();
5446 let z: f64 = ex.iter().sum();
5447 ex.iter().map(|v| v / z).collect()
5448 }
5449
5450 /// Which penalty-arm mutation the harness runs — `None` is the shipped rule.
5451 #[derive(Clone, Copy, PartialEq)]
5452 enum PenMutation {
5453 None,
5454 /// All rows (walk + bonus) penalized with the ROUND-START window only — the
5455 /// within-round update dropped (the frspec per-round posture; what a flat
5456 /// `penalize_logits_rows` launch would ship).
5457 FrozenWindow,
5458 /// Reject-slot residual computed from the UNPENALIZED p (a raw-tlogits column
5459 /// copy instead of the penalized buffer).
5460 UnpenalizedResidual,
5461 /// Full-accept bonus drawn from the UNPENALIZED one-past row.
5462 UnpenalizedBonus,
5463 }
5464
5465 /// Emit `want` committed tokens through spec rounds of `k` drafts (the shipped round
5466 /// rule, penalty-aware) and return them. `hist0` = the pre-stream window (the prompt
5467 /// seed); uniforms come off the injected stream.
5468 fn pen_round_stream(
5469 hist0: &[u32],
5470 k: usize,
5471 want: usize,
5472 mutation: PenMutation,
5473 next_u: &mut dyn FnMut() -> f32,
5474 ) -> Vec<u32> {
5475 let q = pen_q();
5476 let mut hist: Vec<u32> = hist0.to_vec();
5477 let mut committed: Vec<u32> = Vec::new();
5478 while committed.len() < want {
5479 // draft k tokens ~ q (fixed-order CDF walk, the walk_sampled convention)
5480 let drafted: Vec<u32> = (0..k)
5481 .map(|_| {
5482 let u = next_u() as f64;
5483 let mut acc = 0f64;
5484 let mut bi = 0usize;
5485 for (i, &qi) in q.iter().enumerate() {
5486 acc += qi as f64;
5487 if u < acc {
5488 bi = i;
5489 break;
5490 }
5491 }
5492 bi as u32
5493 })
5494 .collect();
5495 // per-slot penalized p at the drafted ids (row j's window = hist ++ drafted[..j])
5496 let pj: Vec<f32> = (0..k)
5497 .map(|j| {
5498 let win: Vec<u32> = if mutation == PenMutation::FrozenWindow {
5499 hist.clone()
5500 } else {
5501 hist.iter()
5502 .copied()
5503 .chain(drafted[..j].iter().copied())
5504 .collect()
5505 };
5506 pen_target(&win)[drafted[j] as usize] as f32
5507 })
5508 .collect();
5509 let qj: Vec<f32> = drafted.iter().map(|&d| q[d as usize]).collect();
5510 let us: Vec<f32> = (0..k).map(|_| next_u()).collect();
5511 let m = rejection_accept_len(&pj, &qj, &us);
5512 committed.extend_from_slice(&drafted[..m]);
5513 hist.extend_from_slice(&drafted[..m]);
5514 let next: u32 = if m == k {
5515 // bonus ~ p at the one-past row (window carries the WHOLE drafted block)
5516 let win: Vec<u32> = if matches!(
5517 mutation,
5518 PenMutation::FrozenWindow | PenMutation::UnpenalizedBonus
5519 ) {
5520 if mutation == PenMutation::UnpenalizedBonus {
5521 Vec::new() // raw row: no penalties at all
5522 } else {
5523 hist[..hist.len() - m].to_vec() // round-start window
5524 }
5525 } else {
5526 hist.clone()
5527 };
5528 let p = pen_target(&win);
5529 let u = next_u() as f64;
5530 let mut acc = 0f64;
5531 let mut bi = PV - 1;
5532 for (i, &pi) in p.iter().enumerate() {
5533 acc += pi;
5534 if u < acc {
5535 bi = i;
5536 break;
5537 }
5538 }
5539 bi as u32
5540 } else {
5541 // residual ~ norm(max(0, p_m − q)) at the reject slot's state
5542 let win: Vec<u32> = match mutation {
5543 PenMutation::UnpenalizedResidual => Vec::new(),
5544 PenMutation::FrozenWindow => hist[..hist.len() - m].to_vec(),
5545 _ => hist.clone(),
5546 };
5547 let p = pen_target(&win);
5548 let r: Vec<f64> = p
5549 .iter()
5550 .zip(&q)
5551 .map(|(&pi, &qi)| (pi - qi as f64).max(0.0))
5552 .collect();
5553 let total: f64 = r.iter().sum();
5554 let target = next_u() as f64 * total;
5555 let mut acc = 0f64;
5556 let mut bi = PV - 1;
5557 for (i, &ri) in r.iter().enumerate() {
5558 acc += ri;
5559 if acc >= target && ri > 0.0 {
5560 bi = i;
5561 break;
5562 }
5563 }
5564 bi as u32
5565 };
5566 committed.push(next);
5567 hist.push(next);
5568 }
5569 committed.truncate(want);
5570 committed
5571 }
5572
5573 /// Joint TV of the spec arm's first two committed tokens vs the EXACT penalized
5574 /// chain p1(a)·p2(b|a) — the plain sampler's distribution over the same two steps.
5575 fn pen_compose_tv(hist0: &[u32], k: usize, mutation: PenMutation) -> f64 {
5576 let trials = 300_000usize;
5577 let mut counts = vec![0f64; PV * PV];
5578 for t in 0..trials {
5579 // stride 64: a k<=2 round consumes <=2k+1 uniforms, <=2 rounds per trial
5580 let mut ctr = (t as u32) * 64;
5581 let mut next_u = move || {
5582 let u = crate::spec::host_u01(11, ctr);
5583 ctr = ctr.wrapping_add(1);
5584 u
5585 };
5586 let s = pen_round_stream(hist0, k, 2, mutation, &mut next_u);
5587 counts[s[0] as usize * PV + s[1] as usize] += 1.0;
5588 }
5589 let p1 = pen_target(hist0);
5590 let mut tv = 0f64;
5591 for a in 0..PV {
5592 let mut w: Vec<u32> = hist0.to_vec();
5593 w.push(a as u32);
5594 let p2 = pen_target(&w);
5595 for b in 0..PV {
5596 let refp = p1[a] * p2[b];
5597 tv += (counts[a * PV + b] / trials as f64 - refp).abs();
5598 }
5599 }
5600 tv / 2.0
5601 }
5602
5603 #[test]
5604 fn penalized_round_composition_matches_the_penalized_chain() {
5605 // MC floor at 300k trials over 36 cells ~ 0.004 TV; bound 0.01. Fixture (a):
5606 // k=2, empty prompt window — the drafted pair (0,0) dominates, so slot 2's
5607 // accept is the SELF-HIT case (its own predecessor was drafted this round).
5608 // Fixture (b): k=1, prompt window [1,1] — the bonus is the successor of a
5609 // same-round accepted draft, and cnt>1 exercises the freq×count path.
5610 for (name, hist0, k) in [
5611 ("k2-selfhit", vec![], 2usize),
5612 ("k1-bonus-successor", vec![1u32, 1u32], 1usize),
5613 ] {
5614 let d = pen_compose_tv(&hist0, k, PenMutation::None);
5615 eprintln!("penalized composition[{name}]: TV {d:.4} (bound 0.01)");
5616 assert!(
5617 d < 0.01,
5618 "penalized composition[{name}]: committed-token distribution must equal \
5619 the penalized chain (TV {d:.4} >= 0.01)"
5620 );
5621 }
5622 }
5623
5624 #[test]
5625 fn penalized_composition_teeth_frozen_window_fails() {
5626 // DECISIVE teeth: penalizing every verify row with the ROUND-START window —
5627 // dropping the within-round penalty update, i.e. a flat penalize_logits_rows
5628 // launch where the route ships penalize_logits_rows_inc — must MISS the
5629 // penalized chain, or the composition gate cannot see the one thing this lane
5630 // adds over the frozen-window prior art.
5631 let d = pen_compose_tv(&[], 2, PenMutation::FrozenWindow);
5632 eprintln!("penalized teeth[frozen-window]: TV {d:.4} (must exceed 0.05)");
5633 assert!(
5634 d > 0.05,
5635 "within-round penalty update dropped (frozen round-start window) must FAIL \
5636 the composition bound (TV {d:.4})"
5637 );
5638 }
5639
5640 #[test]
5641 fn penalized_composition_teeth_unpenalized_residual_fails() {
5642 // The reject-slot residual must read the PENALIZED column: a raw-tlogits column
5643 // copy (p_raw − q) commits from the wrong measure. Non-empty prompt window so
5644 // even round-start reject slots hit the mutation (an empty-window fixture only
5645 // sees it on within-round rejects and the margin thins to ~0.055).
5646 let d = pen_compose_tv(&[1, 1], 2, PenMutation::UnpenalizedResidual);
5647 eprintln!("penalized teeth[unpenalized-residual]: TV {d:.4} (must exceed 0.05)");
5648 assert!(
5649 d > 0.05,
5650 "residual computed from the unpenalized p must FAIL the composition bound \
5651 (TV {d:.4})"
5652 );
5653 }
5654
5655 #[test]
5656 fn penalized_composition_teeth_unpenalized_bonus_fails() {
5657 // The full-accept bonus row must carry the whole drafted block in its window:
5658 // a raw one-past row draw commits the unpenalized measure right after a
5659 // same-round accept.
5660 let d = pen_compose_tv(&[1, 1], 1, PenMutation::UnpenalizedBonus);
5661 eprintln!("penalized teeth[unpenalized-bonus]: TV {d:.4} (must exceed 0.05)");
5662 assert!(
5663 d > 0.05,
5664 "bonus drawn from the unpenalized one-past row must FAIL the composition \
5665 bound (TV {d:.4})"
5666 );
5667 }
5668}
5669
5670#[cfg(test)]
5671mod dspark_harvest_tests {
5672 use super::{DsparkHarvest, DsparkVtPolicy, dspark_accept_prefix, dspark_strategy_census};
5673
5674 const B: usize = 7; // q38 arm-a block_size
5675
5676 #[test]
5677 fn dspark_strategy_requires_shifted_harvest() {
5678 let h = DsparkHarvest::Dspark;
5679 assert_eq!(
5680 h.first_row(),
5681 0,
5682 "DSPARK-strategy checkpoints (SpecForge OnlineDSparkModel, \
5683 training.strategy=dspark — the q38 arm-a export) supervise ALL rows with \
5684 SHIFTED labels: label_offsets = arange(1, block_size+1), i.e. the ANCHOR \
5685 row's output is draft 1 (specforge/algorithms/common/\
5686 dflash_family_model.py:816; sglang v0.5.17 dspark_draft.py:248,260). \
5687 Harvesting from row 1 re-opens the DSPARK-POSTMORTEM-20260820 slot \
5688 misalignment (accept 2.9 -> 1.43)."
5689 );
5690 assert_eq!(
5691 h.n_drafts(B),
5692 B,
5693 "DSpark harvests gamma = block_size drafts per round (sglang \
5694 dspark_config.py:269, verify_num_draft_tokens = gamma+1); b-1 is the \
5695 DFlash mask-fill count and drops the best-trained slot \
5696 (DSPARK-POSTMORTEM-20260820.md §3-H1)."
5697 );
5698 for row in 0..B {
5699 assert_eq!(
5700 h.trained_offset_of_row(row),
5701 row + 1,
5702 "OnlineDSparkModel trains row k to predict anchor+k+1 \
5703 (dflash_family_model.py:816); a same-position (mask-fill) mapping \
5704 here verifies every slot one position early — the postmortem's \
5705 collapse."
5706 );
5707 }
5708 }
5709
5710 #[test]
5711 fn dflash_strategy_keeps_mask_fill_harvest() {
5712 // Guards the reverse mutation: z-lab dflash checkpoints (the gemma arm) are
5713 // mask-fill — row k FILLS anchor+k, the anchor row is loss-excluded
5714 // (dflash_family_model.py:453-472). Shifting THEM would break the gemma arm.
5715 let h = DsparkHarvest::Dflash;
5716 assert_eq!(h.first_row(), 1, "DFlash drafts start at mask row 1");
5717 assert_eq!(h.n_drafts(B), B - 1, "DFlash harvests block_size-1 drafts");
5718 for row in 1..B {
5719 assert_eq!(h.trained_offset_of_row(row), row);
5720 }
5721 }
5722
5723 #[test]
5724 fn every_candidate_verifies_the_position_its_row_was_trained_for() {
5725 // The round's invariant: draft candidate i (1-based; verified against the
5726 // trunk's prediction for anchor+i) is filled from drafter output row
5727 // first_row + i - 1. Alignment == that row was TRAINED for offset i.
5728 for h in [DsparkHarvest::Dflash, DsparkHarvest::Dspark] {
5729 for i in 1..=h.n_drafts(B) {
5730 let row = h.first_row() + i - 1;
5731 assert_eq!(
5732 h.trained_offset_of_row(row),
5733 i,
5734 "{h:?}: candidate {i} rides row {row}, which is trained for \
5735 offset {} — harvest misaligned",
5736 h.trained_offset_of_row(row)
5737 );
5738 }
5739 }
5740 }
5741
5742 #[test]
5743 fn env_seam_parses_and_refuses() {
5744 assert_eq!(
5745 DsparkHarvest::from_env_value(None),
5746 DsparkHarvest::Dflash,
5747 "the ENV-ONLY parser keeps the historical arm; the ratified strategy-keyed \
5748 default lives in resolve_value (checkpoint census), not here"
5749 );
5750 assert_eq!(
5751 DsparkHarvest::from_env_value(Some("dspark")),
5752 DsparkHarvest::Dspark
5753 );
5754 assert_eq!(
5755 DsparkHarvest::from_env_value(Some("dflash")),
5756 DsparkHarvest::Dflash
5757 );
5758 assert!(
5759 std::panic::catch_unwind(|| DsparkHarvest::from_env_value(Some("shifted"))).is_err(),
5760 "unknown harvest values must REFUSE, not default"
5761 );
5762 assert_eq!(
5763 DsparkHarvest::from_name("dspark"),
5764 Some(DsparkHarvest::Dspark)
5765 );
5766 assert_eq!(
5767 DsparkHarvest::from_name("dflash"),
5768 Some(DsparkHarvest::Dflash)
5769 );
5770 assert_eq!(DsparkHarvest::from_name("mask-fill"), None);
5771 }
5772
5773 /// The owner-ratified default flips (2026-08-20). Each assertion names its
5774 /// evidence; mutating either resolve back to the old default fails these.
5775 #[test]
5776 fn ratified_default_harvest_is_strategy_keyed() {
5777 // DSPARK-strategy checkpoint + unset env = the shifted harvest (B1: accept
5778 // 1.38->2.41 agentic / 1.53->3.66 math, E2E ALL EXACT x5, interleaved x5).
5779 assert_eq!(
5780 DsparkHarvest::resolve_value(None, true),
5781 DsparkHarvest::Dspark,
5782 "owner-ratified 2026-08-20: unset env defaults a DSPARK-strategy \
5783 checkpoint to the shifted harvest (DSPARK-POSTMORTEM-20260820.md B1)"
5784 );
5785 // mask-fill checkpoint + unset env = the historical arm, byte-identical.
5786 assert_eq!(
5787 DsparkHarvest::resolve_value(None, false),
5788 DsparkHarvest::Dflash
5789 );
5790 assert_eq!(
5791 DsparkHarvest::resolve_value(Some(""), false),
5792 DsparkHarvest::Dflash
5793 );
5794 // Explicit env overrides the census in BOTH directions (the A/B seam).
5795 assert_eq!(
5796 DsparkHarvest::resolve_value(Some("dflash"), true),
5797 DsparkHarvest::Dflash
5798 );
5799 assert_eq!(
5800 DsparkHarvest::resolve_value(Some("dspark"), false),
5801 DsparkHarvest::Dspark
5802 );
5803 // Unknown values still REFUSE through the resolve path.
5804 assert!(
5805 std::panic::catch_unwind(|| DsparkHarvest::resolve_value(Some("shifted"), true))
5806 .is_err()
5807 );
5808 }
5809
5810 #[test]
5811 fn strategy_census_reads_the_checkpoint_not_the_env() {
5812 // The q38 arm-a export shape: both signals present.
5813 let q38 = r#"{"architectures": ["Qwen3DSparkModel"], "block_size": 7,
5814 "dflash_config": {"projector_type": "dspark", "markov_rank": 256}}"#;
5815 assert!(dspark_strategy_census(q38));
5816 // Either signal alone suffices.
5817 assert!(dspark_strategy_census(
5818 r#"{"architectures": ["Qwen3DSparkModel"]}"#
5819 ));
5820 assert!(dspark_strategy_census(
5821 r#"{"dflash_config": {"projector_type": "dspark"}}"#
5822 ));
5823 // A mask-fill DFlash export carries neither -> historical default.
5824 let dflash = r#"{"architectures": ["Qwen3DFlashModel"],
5825 "dflash_config": {"attention_mode": "gqa"}}"#;
5826 assert!(!dspark_strategy_census(dflash));
5827 assert!(!dspark_strategy_census("{}"));
5828 }
5829
5830 #[test]
5831 fn ratified_default_vt_is_confidence_slot_tau_half() {
5832 // Head-carrying checkpoint + unset env = confidence-slot tau=.5 (H4 cell 3:
5833 // the tau ladder's knee; cell 2: 93.9%/97.7% of fixed-8 accept at wall >=
5834 // the reactive ladder, exactness 11/11 ALL EXACT).
5835 assert_eq!(
5836 DsparkVtPolicy::resolve_value(None, None, None, true),
5837 DsparkVtPolicy::ConfidenceSlot { tau: 0.5 },
5838 "owner-ratified 2026-08-20: unset MEMRA_DSPARK_VT defaults to \
5839 confidence-slot tau=.5 on a head-carrying checkpoint (H4 cells 2-3)"
5840 );
5841 // tau env still steers the default arm (and a bad tau still refuses).
5842 assert_eq!(
5843 DsparkVtPolicy::resolve_value(None, Some("0.35"), None, true),
5844 DsparkVtPolicy::ConfidenceSlot { tau: 0.35 }
5845 );
5846 assert!(
5847 std::panic::catch_unwind(|| DsparkVtPolicy::resolve_value(
5848 None,
5849 Some("nan-ish"),
5850 None,
5851 true
5852 ))
5853 .is_err()
5854 );
5855 // Census: no accept-rate head -> nothing to schedule with -> ladder.
5856 assert_eq!(
5857 DsparkVtPolicy::resolve_value(None, None, None, false),
5858 DsparkVtPolicy::Ladder
5859 );
5860 // MEMRA_DFLASH_ADAPT=0 is an explicit fixed-window request: honored.
5861 assert_eq!(
5862 DsparkVtPolicy::resolve_value(None, None, Some("0"), true),
5863 DsparkVtPolicy::Ladder
5864 );
5865 // Explicit values keep their exact prior semantics through resolve.
5866 assert_eq!(
5867 DsparkVtPolicy::resolve_value(Some("ladder"), None, None, true),
5868 DsparkVtPolicy::Ladder
5869 );
5870 assert_eq!(
5871 DsparkVtPolicy::resolve_value(Some("confidence"), Some("0.35"), None, true),
5872 DsparkVtPolicy::Confidence { tau: 0.35 }
5873 );
5874 // Explicit confidence mode with ADAPT=0 stays a refusal.
5875 assert!(
5876 std::panic::catch_unwind(|| DsparkVtPolicy::resolve_value(
5877 Some("confidence-slot"),
5878 None,
5879 Some("0"),
5880 true
5881 ))
5882 .is_err()
5883 );
5884 }
5885
5886 /// End-to-end alignment fixture in miniature: a mock drafter whose row r argmaxes
5887 /// to token BASE + (its trained offset under the DSPARK strategy), and a mock trunk
5888 /// whose prediction for anchor+j is BASE + j. The DSpark harvest accepts the whole
5889 /// block; feeding the same drafter through the mask-fill harvest accepts ZERO —
5890 /// the postmortem's collapse reproduced as pure logic.
5891 #[test]
5892 fn dspark_trained_rows_through_mask_fill_harvest_accept_nothing() {
5893 const BASE: u32 = 1000;
5894 let anchor: u32 = BASE; // token at the round anchor position (offset 0)
5895 // trunk verify argmaxes: vam[j] = prediction for anchor offset j+1
5896 let vam: Vec<u32> = (1..=B as u32 + 1).map(|j| BASE + j).collect();
5897 // drafter rows trained under the DSPARK strategy: row r predicts offset r+1
5898 let dspark_trained_row_argmax =
5899 |r: usize| BASE + DsparkHarvest::Dspark.trained_offset_of_row(r) as u32;
5900
5901 // Correct (shifted) harvest: candidate i <- row i-1.
5902 let h = DsparkHarvest::Dspark;
5903 let mut cand = vec![anchor];
5904 for i in 1..=h.n_drafts(B) {
5905 cand.push(dspark_trained_row_argmax(h.first_row() + i - 1));
5906 }
5907 let vt = h.n_drafts(B) + 1;
5908 assert_eq!(
5909 dspark_accept_prefix(&cand, &vam, vt),
5910 vt - 1,
5911 "aligned harvest must accept the full block"
5912 );
5913
5914 // Mask-fill harvest of the SAME dspark-trained drafter: candidate i <- row i,
5915 // which was trained for offset i+1 — every slot one position late.
5916 let wrong = DsparkHarvest::Dflash;
5917 let mut cand_wrong = vec![anchor];
5918 for i in 1..=wrong.n_drafts(B) {
5919 cand_wrong.push(dspark_trained_row_argmax(wrong.first_row() + i - 1));
5920 }
5921 let vt_wrong = wrong.n_drafts(B) + 1;
5922 assert_eq!(
5923 dspark_accept_prefix(&cand_wrong, &vam, vt_wrong),
5924 0,
5925 "mask-fill harvest of a dspark-trained drafter verifies every slot against \
5926 a position the row was not trained for (DSPARK-POSTMORTEM-20260820.md)"
5927 );
5928 }
5929}
5930
5931// ================= Verify-window policy gate (CPU; H4, DSPARK-POSTMORTEM-20260820.md) ===
5932// Pins the confidence-vt semantics as logic the round consumes: cumprod survival over
5933// sigmoid scores, thresholded, anchor + kept drafts, floor 2 / cap vt_cap — and the env
5934// seam's refuse-on-ambiguity. Mutating the policy (per-slot threshold instead of
5935// survival, off-by-one on the anchor, silent unknown-value fallback) fails HERE.
5936#[cfg(test)]
5937mod dspark_vt_tests {
5938 use super::{ConfidenceHead, DsparkVtPolicy, dspark_confidence_vt, dspark_slot_confidence_vt};
5939
5940 /// Pre-sigmoid logit for a target probability: sigmoid(logit(p)) == p.
5941 fn logit(p: f32) -> f32 {
5942 (p / (1.0 - p)).ln()
5943 }
5944
5945 #[test]
5946 fn confidence_vt_is_cumprod_survival_not_per_slot_threshold() {
5947 // sigmoids = [0.9, 0.8, 0.9, ...]: every PER-SLOT score clears tau=0.5, but
5948 // cumulative survival sinks below it at slot 6 (0.9, 0.72, 0.648, 0.583,
5949 // 0.525, then 0.472 < 0.5) — the window must stop where the EXPECTED
5950 // accepted-prefix stops paying, not where a slot looks locally fine.
5951 let raws: Vec<f32> = [0.9, 0.8, 0.9, 0.9, 0.9, 0.9, 0.9]
5952 .iter()
5953 .map(|&p| logit(p))
5954 .collect();
5955 assert_eq!(
5956 dspark_confidence_vt(&raws, 0.5, 8),
5957 6,
5958 "keeps 5 drafts + anchor"
5959 );
5960 // Tighter threshold closes the window sooner; looser opens it to the cap.
5961 assert_eq!(
5962 dspark_confidence_vt(&raws, 0.7, 8),
5963 3,
5964 "tau=0.7 keeps 2 drafts"
5965 );
5966 assert_eq!(
5967 dspark_confidence_vt(&raws, 0.05, 8),
5968 8,
5969 "tau→0 = full block"
5970 );
5971 }
5972
5973 #[test]
5974 fn slot_arm_truncates_at_first_low_confidence_slot() {
5975 // Owner directive (2026-08-20): submit only the longest prefix whose EVERY
5976 // slot clears tau on its own sigmoid. On the survival test's raws
5977 // ([0.9, 0.8, 0.9 x5], tau=0.5) every slot clears per-slot, so the slot arm
5978 // opens the full block where survival stopped at 6 — the two stopping
5979 // statistics must stay distinct arms.
5980 let raws: Vec<f32> = [0.9, 0.8, 0.9, 0.9, 0.9, 0.9, 0.9]
5981 .iter()
5982 .map(|&p| logit(p))
5983 .collect();
5984 assert_eq!(dspark_slot_confidence_vt(&raws, 0.5, 8), 8);
5985 assert_eq!(dspark_confidence_vt(&raws, 0.5, 8), 6);
5986 // A low-confidence tail never enters verify: [0.9, 0.9, 0.3, 0.9, ...]
5987 // truncates at slot 3 REGARDLESS of the confident slots behind it — a kept
5988 // slot after a dropped one could never commit (prefix accept rule).
5989 let tail: Vec<f32> = [0.9, 0.9, 0.3, 0.9, 0.9, 0.9, 0.9]
5990 .iter()
5991 .map(|&p| logit(p))
5992 .collect();
5993 assert_eq!(
5994 dspark_slot_confidence_vt(&tail, 0.5, 8),
5995 3,
5996 "2 drafts + anchor"
5997 );
5998 // Tighter tau keeps less.
5999 assert_eq!(
6000 dspark_slot_confidence_vt(&tail, 0.95, 8),
6001 2,
6002 "floor at tau=0.95"
6003 );
6004 }
6005
6006 #[test]
6007 fn confidence_vt_floor_and_cap() {
6008 // A hopeless round still verifies ONE draft (the draft forward is paid;
6009 // vt=1 would guarantee an empty round at the same cost class).
6010 let cold: Vec<f32> = [0.1f32, 0.1, 0.1].iter().map(|&p| logit(p)).collect();
6011 assert_eq!(
6012 dspark_confidence_vt(&cold, 0.5, 8),
6013 2,
6014 "floor = anchor + 1 draft"
6015 );
6016 assert_eq!(
6017 dspark_slot_confidence_vt(&cold, 0.5, 8),
6018 2,
6019 "slot arm same floor"
6020 );
6021 // The MEMRA_DFLASH_VERIFY_T cap still binds a confident round.
6022 let hot: Vec<f32> = vec![logit(0.99); 7];
6023 assert_eq!(dspark_confidence_vt(&hot, 0.5, 5), 5, "vt_cap binds");
6024 assert_eq!(
6025 dspark_confidence_vt(&hot, 0.5, 8),
6026 8,
6027 "full block when confident"
6028 );
6029 assert_eq!(
6030 dspark_slot_confidence_vt(&hot, 0.5, 5),
6031 5,
6032 "slot arm same cap"
6033 );
6034 // No scores (defensive): floor.
6035 assert_eq!(dspark_confidence_vt(&[], 0.5, 8), 2);
6036 assert_eq!(dspark_slot_confidence_vt(&[], 0.5, 8), 2);
6037 }
6038
6039 #[test]
6040 fn vt_policy_env_seam_parses_and_refuses() {
6041 assert_eq!(
6042 DsparkVtPolicy::from_env_value(None, None, None),
6043 DsparkVtPolicy::Ladder,
6044 "default stays the shipped ladder — the H4 arm is opt-in"
6045 );
6046 assert_eq!(
6047 DsparkVtPolicy::from_env_value(Some(""), None, None),
6048 DsparkVtPolicy::Ladder
6049 );
6050 assert_eq!(
6051 DsparkVtPolicy::from_env_value(Some("ladder"), None, Some("0")),
6052 DsparkVtPolicy::Ladder,
6053 "ladder + ADAPT=0 = the fixed-window arm, untouched"
6054 );
6055 assert_eq!(
6056 DsparkVtPolicy::from_env_value(Some("confidence"), None, None),
6057 DsparkVtPolicy::Confidence { tau: 0.5 },
6058 "tau defaults to 0.5 (raw sigmoid, no STS sidecar — postmortem §3-H4)"
6059 );
6060 assert_eq!(
6061 DsparkVtPolicy::from_env_value(Some("confidence"), Some("0.35"), Some("1")),
6062 DsparkVtPolicy::Confidence { tau: 0.35 }
6063 );
6064 assert_eq!(
6065 DsparkVtPolicy::from_env_value(Some("confidence-slot"), Some("0.6"), None),
6066 DsparkVtPolicy::ConfidenceSlot { tau: 0.6 },
6067 "the owner-directive per-slot arm parses with the same tau env"
6068 );
6069 assert!(
6070 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(
6071 Some("confidence-slot"),
6072 None,
6073 Some("0")
6074 ))
6075 .is_err(),
6076 "confidence-slot + MEMRA_DFLASH_ADAPT=0 must REFUSE like confidence"
6077 );
6078 assert!(
6079 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(Some("static"), None, None))
6080 .is_err(),
6081 "unknown policy values must REFUSE, not default — a typo silently \
6082 reverting the window policy invalidates an A/B"
6083 );
6084 assert!(
6085 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(
6086 Some("confidence"),
6087 None,
6088 Some("0")
6089 ))
6090 .is_err(),
6091 "confidence + MEMRA_DFLASH_ADAPT=0 is contradictory and must REFUSE"
6092 );
6093 for bad in ["0", "1", "1.5", "-0.1", "nan"] {
6094 assert!(
6095 std::panic::catch_unwind(|| DsparkVtPolicy::from_env_value(
6096 Some("confidence"),
6097 Some(bad),
6098 None
6099 ))
6100 .is_err(),
6101 "tau={bad} must REFUSE (survival threshold lives in (0,1))"
6102 );
6103 }
6104 }
6105
6106 #[test]
6107 fn raw_score_matches_the_parity_gate_dot() {
6108 // The head is a raw linear proj over [hidden ; markov_prev_embedding] + b —
6109 // the exact stage-5 contract in dspark_q38_parity.rs.
6110 let ch = ConfidenceHead {
6111 w: vec![0.5, -1.0, 2.0, 0.25, -0.5],
6112 b: 0.125,
6113 in_dim: 5,
6114 with_markov: true,
6115 };
6116 let hidden = [1.0f32, 2.0, 3.0];
6117 let emb = [4.0f32, 8.0];
6118 let want = 0.125 + 0.5 * 1.0 - 1.0 * 2.0 + 2.0 * 3.0 + 0.25 * 4.0 - 0.5 * 8.0;
6119 assert_eq!(ch.raw_score(&hidden, Some(&emb)), want);
6120 let ch_plain = ConfidenceHead {
6121 w: vec![0.5, -1.0, 2.0],
6122 b: -0.25,
6123 in_dim: 3,
6124 with_markov: false,
6125 };
6126 let want_plain = -0.25 + 0.5 * 1.0 - 1.0 * 2.0 + 2.0 * 3.0;
6127 assert_eq!(ch_plain.raw_score(&hidden, None), want_plain);
6128 }
6129}
6130
6131#[cfg(test)]
6132mod dspark_prefix_capture_tests {
6133 use super::{dspark_spec_prompt_fits, take_dspark_prefix_capture};
6134
6135 /// INCIDENT REGRESSION (2026-08-25). This gate is the only admission check the dspark
6136 /// route has, and it checked the ctx ceiling ONLY — so a short prompt was admitted and
6137 /// then panicked in the cold prime (`prime_cache needs T >= 16`), inside the GPU worker
6138 /// thread, which exits 70 and kills every live session on the box. Two crash loops and
6139 /// ~5 minutes of customer 502s came from a 5-token "Say OK." — the class our own
6140 /// watchdog sends. The floor belongs HERE, in the gate, not in each caller.
6141 #[test]
6142 fn a_prompt_below_the_prime_floor_never_enters_the_dspark_route() {
6143 let floor = crate::hybrid_forward::PRIME_MIN_T;
6144 for short in [1usize, 5, floor - 1] {
6145 assert!(
6146 !dspark_spec_prompt_fits(short, 262_144, 8, 2_048, true),
6147 "a {short}-token prompt must decline to the plain path, not prime"
6148 );
6149 }
6150 // At and above the floor the route admits exactly as before (ceiling still applies).
6151 assert!(dspark_spec_prompt_fits(floor, 262_144, 8, 2_048, true));
6152 assert!(dspark_spec_prompt_fits(512, 262_144, 8, 2_048, true));
6153 assert!(!dspark_spec_prompt_fits(512, 300, 8, 2_048, true));
6154 }
6155
6156 #[test]
6157 fn session_prompt_preflight_matches_dflash2_and_windowed_caps() {
6158 // DFlash2 uses the request ctx cap: prompt + block + 8 fits exactly, one row less does not.
6159 assert!(dspark_spec_prompt_fits(96, 111, 7, 2_048, true));
6160 assert!(!dspark_spec_prompt_fits(96, 110, 7, 2_048, true));
6161
6162 // Legacy/windowed drafts are additionally bounded by their own sliding window.
6163 assert!(dspark_spec_prompt_fits(113, 8_192, 7, 128, false));
6164 assert!(!dspark_spec_prompt_fits(114, 8_192, 7, 128, false));
6165 assert!(!dspark_spec_prompt_fits(
6166 usize::MAX,
6167 usize::MAX,
6168 7,
6169 usize::MAX,
6170 true,
6171 ));
6172 }
6173
6174 #[test]
6175 fn prompt_end_capture_is_full_prompt_and_one_shot() {
6176 let prompt_len = 96;
6177 let mut slot = Some(crate::spec::SpecBoundaryCapture {
6178 snap: crate::cache::CacheSnapshot {
6179 kv_len: Vec::new(),
6180 tp_kv_len: Vec::new(),
6181 conv: Vec::new(),
6182 ssm: Vec::new(),
6183 pos: prompt_len,
6184 },
6185 pos: prompt_len,
6186 logits: vec![1.0, 2.0],
6187 last_h: Vec::new(),
6188 });
6189
6190 let capture = take_dspark_prefix_capture(&mut slot).expect("first drain gets capture");
6191 assert_eq!(capture.pos, prompt_len, "capture is at full prompt end");
6192 assert_eq!(capture.snap.pos, prompt_len);
6193 assert!(
6194 capture.last_h.is_empty(),
6195 "DFlash publishes no hidden anchor"
6196 );
6197 assert!(
6198 take_dspark_prefix_capture(&mut slot).is_none(),
6199 "capture drains exactly once",
6200 );
6201 }
6202}
6203
6204#[cfg(test)]
6205mod dflash_precision_tests {
6206 use super::dflash_precision;
6207
6208 #[test]
6209 fn default_and_supported_precision_programs_are_explicit() {
6210 assert_eq!(dflash_precision(None), Ok("q4"));
6211 for prec in ["q4", "q8", "mixed", "bf16", "fc"] {
6212 assert_eq!(dflash_precision(Some(prec)), Ok(prec));
6213 }
6214 }
6215
6216 #[test]
6217 fn q5_and_typos_refuse_instead_of_silently_selecting_q8() {
6218 for prec in ["q5", "Q4", "", "typo"] {
6219 let err = dflash_precision(Some(prec)).unwrap_err();
6220 assert!(err.contains("want q4, q8, mixed, bf16, or fc"));
6221 }
6222 }
6223}