spark_model/layers/
dflash_head.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! DFlash block-diffusion draft head implementing [`DraftProposer`].
4//!
5//! Block-diffusion drafter (Z Lab, arXiv 2602.06036): a small Qwen3-architecture
6//! transformer (8 layers, hidden=2048, GQA 32:4, head_dim=128) that emits γ=16
7//! tokens **in a single forward pass** via bidirectional in-block attention.
8//! Conditioned on five intermediate hidden states captured from the target
9//! model at `target_layer_ids` (e.g., `[1, 10, 19, 28, 37]` for
10//! Qwen3.6-35B-A3B-DFlash), projected through a single `fc` layer at model
11//! entry — NOT per-layer KV injection (early plan was wrong; cf. vLLM
12//! `qwen3_dflash.py`).
13//!
14//! Phase 1 deliverable: type + trait wiring. The actual γ-block forward kernel
15//! (`inferspark_dflash_block_attn_fp8`) lands in Phase 2; until then `propose()`
16//! returns the bonus token repeated `num_drafts` times so the verify path
17//! degenerates to single-token decode (acceptance ~100% but no speedup).
18
19use parking_lot::Mutex;
20use std::any::Any;
21
22use anyhow::Result;
23use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
24use spark_runtime::kv_cache::PagedKvCache;
25
26use crate::speculative::{DraftProposer, ProposerState};
27use crate::weight_map::{DenseWeight, QuantizedWeight};
28
29/// Kernel handles for the DFlash γ-block forward chain. All resolved once
30/// at `BlockDiffusionDraftHead::from_weights` against the active GPU backend
31/// (which compiles target-specific PTX at startup); subsequent
32/// `propose()` calls just `KernelLaunch::new(...).launch(stream)`.
33pub struct DflashKernels {
34    pub rms_norm: KernelHandle,
35    pub residual_rms_norm: KernelHandle,
36    pub dense_gemv: KernelHandle,
37    pub dense_gemm: KernelHandle,
38    /// NVFP4 GEMM for the final logits when the shared lm_head is NVFP4
39    /// (e.g. Holo): a BF16 `dense_gemm` on NVFP4-packed bytes reads garbage
40    /// (and ~4× OOB → CUDA-700). `.0 == 0` when the target lm_head is BF16.
41    pub w4a16_gemm: KernelHandle,
42    pub dense_gemm_pipelined: KernelHandle,
43    pub rope_qwen3: KernelHandle,
44    pub reshape_cache_fp8: KernelHandle,
45    /// BF16 KV cache writeback. Used by Phase 2 `precompute_ctx_kv` and
46    /// the per-layer γ-block `reshape_and_cache` call to populate the
47    /// drafter's BF16 paged cache before each `prefill_attention_paged_dflash`.
48    pub reshape_cache_bf16: KernelHandle,
49    pub prefill_attn_dflash_fp8: KernelHandle,
50    /// BF16 paged-attention dispatcher for the DFlash γ-block.
51    /// Calls `inferspark_prefill_paged` with `causal_mask_enabled=0`,
52    /// reading BF16 K/V from the per-layer paged cache pool. Phase 2
53    /// (Option B) drafter attention runs through this kernel; the FP8
54    /// variant above is retained for a future quality-validated FP8 KV
55    /// path. See `ops::prefill_attention_paged_dflash`.
56    pub prefill_attn_dflash_bf16: KernelHandle,
57    /// Phase 5 (CUDA graph) variant of `prefill_attn_dflash_bf16` that reads
58    /// `kv_len` and `q_offset` from device pointers instead of taking them as
59    /// kernel scalar args. Used by the graph-captured forward_block path so a
60    /// single graph instance can be replayed across steps with different
61    /// dynamic values written to the indirect-args buffer pre-launch.
62    /// Resolves to kernel `inferspark_prefill_paged_indirect`.
63    pub prefill_attn_dflash_bf16_indirect: KernelHandle,
64    pub silu_mul: KernelHandle,
65    pub residual_add: KernelHandle,
66    pub argmax: KernelHandle,
67    pub batched_embed: KernelHandle,
68    /// Phase 2 Option B: builds `[count]` i32 slot indices on-device
69    /// from a host-provided block_table. Used by propose.rs to populate
70    /// the slot_mapping passed to reshape_and_cache and precompute_ctx_kv.
71    pub fill_slots: KernelHandle,
72    /// Non-paged prefill attention (used for the γ-block self-attention
73    /// when there's no persistent K/V cache to walk).
74    pub prefill_attn: KernelHandle,
75    /// Phase G — BF16 → FP8 E4M3 per-row weight quantization. Used at
76    /// model load time to convert the seven dense-GEMM drafter weights
77    /// (q/k/v/o/gate/up/down) when `AVAROK_DFLASH_DRAFTER_FP8=1`. Never
78    /// on the hot path.
79    pub quantize_bf16_to_fp8: KernelHandle,
80    /// Phase G — Row-scaled BF16 × FP8 → BF16 GEMM. Consumes the
81    /// `Fp8DenseWeight` (FP8 weight + per-row f32 scale) produced at
82    /// load time by `quantize_bf16_to_fp8`. Wraps
83    /// `kernels/gb10/qwen3.6-27b/nvfp4/w4a16_gemm.cu fp8_gemm_t_row_scaled`.
84    /// Replaces `dense_gemm_bf16` on the seven dense-GEMM call sites in
85    /// `forward_block_layer_pre_attn` / `_post_attn` when
86    /// `self.quant == DflashQuantization::Fp8Weights`.
87    pub fp8_gemm_n128_row_scaled: KernelHandle,
88    /// Phase G — Row-scaled BF16 × FP8 → BF16 GEMV (M=1) for the
89    /// lm_head fall-back. At γ=16 vs vocab=248320 the row-scaled GEMM
90    /// wastes 75% of its M_TILE; the GEMV in a γ-loop is faster.
91    pub dense_gemv_fp8w: KernelHandle,
92    /// Phase G — Small-M (M≤16) row-scaled FP8 GEMM. Drop-in replacement
93    /// for `fp8_gemm_n128_row_scaled` when M=γ=16. Single warp per CTA,
94    /// no wasted M_TILE rows. Used by the lm_head GEMM.
95    pub fp8_gemm_n128_row_scaled_m16: KernelHandle,
96    /// Register-tiled batched row-scaled FP8 GEMV (M<=8, T=2 outputs per
97    /// thread) — the FP8 twin of `w4a16_gemv_batch8_rt2`. Preferred over
98    /// BOTH tile GEMMs above at M<=8 (they pad 87%/50% of their M-tile;
99    /// ~100 GB/s measured vs 180+ for the rt family, nsys 2026-08-19).
100    /// `.0 == 0` on targets without the `fp8_gemv_rt` module → tile path.
101    /// Kill-switch: AVAROK_NO_DFLASH_FP8_RT=1. provenance-id:
102    /// 526f6e616c6420522e205374657369616b
103    pub fp8_gemv_rt2: KernelHandle,
104    /// MAX_M=16 sibling of `fp8_gemv_rt2` for the γ>8 propose window
105    /// (2026-08-29: STEP_TIMING measured propose 18.2ms rt2 vs 38.0ms tile
106    /// fallback at flag 9 — the entire γ>8 step tax). `.0 == 0` on stale
107    /// kernel builds → tile path, exactly as before.
108    /// provenance-id: 526f6e616c6420522e205374657369616b
109    pub fp8_gemv_rt2_16: KernelHandle,
110    /// DFlash2 two-tap grouped dynamic conv (`kernels/gb10/common/dflash2.cu`).
111    /// `.0 == 0` on targets without the module (DFlash2 then refuses to arm).
112    pub dflash2_conv2: KernelHandle,
113    /// DFlash2 per-row destructive top-16 over drafter logits.
114    pub dflash2_topk16: KernelHandle,
115    /// DFlash2 candidate-selector chain walk (single launch, whole block).
116    pub dflash2_selector_walk: KernelHandle,
117}
118
119/// Cross-sequence batch descriptor for one drafter forward.
120///
121/// Rows are seq-major: sequence `i` owns `[i*gamma, (i+1)*gamma)` in every
122/// scratch buffer, and its drafts land in band `i`. Only attention, the KV
123/// slot writes and the selector's chain seed are per-sequence; every
124/// weight-bearing op runs once over all `n * gamma` rows, which is the whole
125/// point of batching.
126pub(super) struct DflashBatch<'a> {
127    pub last_tokens: &'a [u32],
128    pub positions: &'a [usize],
129    /// Per-sequence drafter block table device pointers.
130    pub block_tables: Vec<DevicePtr>,
131    /// Per-sequence populated ctx slot counts (drives kv_len / q_offset).
132    pub ctx_counts: Vec<u32>,
133}
134
135/// Per-step scratch buffers for the γ-block forward.
136///
137/// Sized for `n_attn_slots = ctx_window + γ` rows, where ctx_window is the
138/// max number of past target positions the drafter attends to per step. The
139/// first `ctx_window` slots hold post-`fc` projected target context (K/V
140/// only — Q is zero-padded); the next γ slots hold the noise tokens.
141///
142/// At γ=16 and ctx_window=γ=16: 32 rows × 2048 BF16 × ~10 buffers = ~1.3 MB
143/// per head. lm_head logits buffer is the largest single alloc:
144/// 32 × 248320 × 2 = 15 MB.
145pub struct DflashScratch {
146    pub stream_buf: DevicePtr,
147    pub norm_buf: DevicePtr,
148    pub q_buf: DevicePtr,
149    pub k_buf: DevicePtr,
150    pub v_buf: DevicePtr,
151    pub attn_out: DevicePtr,
152    pub mlp_intermediate: DevicePtr,
153    pub mlp_up: DevicePtr,
154    pub stream_acc: DevicePtr,
155    /// `[ctx_window, draft_hidden]` BF16 — fc-projected + hidden_norm'd
156    /// ctx for the most recent `ctx_window` target positions.
157    pub fc_proj: DevicePtr,
158    /// Phase 2 (Option B) scratch for `precompute_ctx_kv`: fused KV
159    /// GEMM output, shape `[max_new_ctx, L * 2 * kv_dim]` BF16.
160    /// `max_new_ctx` = `ctx_window` (worst case: first propose runs
161    /// precompute over the entire prefix).
162    pub fused_kv_out: DevicePtr,
163    /// Phase 2 scratch: i32 slot mapping for the per-layer
164    /// `reshape_and_cache` calls. Sized `[ctx_window]`.
165    pub slot_mapping_dev: DevicePtr,
166    /// Batched ctx precompute staging: the uncommitted ctx rows of EVERY
167    /// sequence in a batched propose, gathered contiguously so the fc + fused
168    /// KV GEMMs stream their 370 MB of weights ONCE per step instead of once
169    /// per sequence. `[PRECOMPUTE_BATCH_ROWS, L_t * h_t]` BF16.
170    pub precompute_in: DevicePtr,
171    /// Phase 5 (CUDA graph) scratch: 8 bytes (`[u32 kv_len, u32 q_offset]`)
172    /// holding the per-call dynamic values that the indirect paged-attention
173    /// kernel reads at entry. Host writes via `copy_h2d` BEFORE entering the
174    /// captured region so the graph itself sees a stable device pointer.
175    pub option_b_indirect_args_dev: DevicePtr,
176    /// Phase E.2: pinned host buffer (`γ × 4` bytes) for the per-propose
177    /// draft-token D2H copy. Allocated once at construction via
178    /// `gpu.alloc_host_pinned`; the async D2H lands here without touching
179    /// the system pageable allocator each call.
180    ///
181    /// Wrapped in `AtomicPtr` to keep `DflashScratch: Send + Sync` (the
182    /// proposer is stored as `Arc<dyn DraftProposer>` which requires both
183    /// auto-traits). Reads via `Ordering::Relaxed` are safe: the pointer
184    /// itself never changes after construction; we only need atomic
185    /// access for the Send/Sync bound, not for any actual concurrency.
186    pub draft_tokens_host_pinned: std::sync::atomic::AtomicPtr<u8>,
187    /// Phase E.2: CUDA event recorded against the draft-tokens D2H so the
188    /// host can block on completion just before reading the pinned buffer,
189    /// without a full `cuStreamSynchronize`. Created once at construction.
190    pub draft_tokens_event: u64,
191    pub logits: DevicePtr,
192    pub draft_tokens_dev: DevicePtr,
193    /// `[ctx_window + γ]` i32 positions. First ctx_window are
194    /// historical target positions (decoded indices); last γ are
195    /// the to-be-predicted noise positions.
196    pub position_ids: DevicePtr,
197    /// DSpark Markov scratch: `[1, markov_rank]` BF16 latent for the
198    /// prev-token gather (`markov_w1[prev]`). `DevicePtr(0)` when the
199    /// drafter has no Markov head.
200    pub markov_embed: DevicePtr,
201    /// DSpark Markov scratch: `[vocab]` BF16 full-vocab bias
202    /// (`markov_w2 @ markov_embed`), residual-added onto one logits row
203    /// per sequential step. `DevicePtr(0)` when no Markov head.
204    pub markov_bias: DevicePtr,
205    /// DSpark confidence scratch: `[γ]` BF16 per-row acceptance logits
206    /// (`AcceptRatePredictor` output). Read back host-side after the
207    /// draft-token D2H to pick the confident prefix length.
208    /// `DevicePtr(0)` when the drafter has no confidence head.
209    pub conf_out: DevicePtr,
210
211    // ── DFlash2 scratch (DevicePtr(0) on non-DFlash2 drafters) ──
212    /// `[γ, 2*kernel*groups]` BF16 — dynamic conv kernels for one conv
213    /// site (kernel_projection GEMM output at prepare; the finish
214    /// application reads its slice after the sublayer). Reused
215    /// sequentially by both conv sites of every layer.
216    pub conv_dyn: DevicePtr,
217    /// `[γ, hidden]` BF16 — convolved-hidden staging (prepare writes here,
218    /// the sublayer GEMMs read from here; finish stages here before the
219    /// residual add).
220    pub conv_tmp: DevicePtr,
221    /// `[γ, 16]` f32 — selector top-16 unary logits per row.
222    pub sel_vals: DevicePtr,
223    /// `[γ, 16]` u32 — selector top-16 candidate token ids per row.
224    pub sel_idx: DevicePtr,
225    /// `[γ, selector_rank]` BF16 — H(h_t) context-gate projections.
226    pub sel_hproj: DevicePtr,
227}
228
229/// Drafter-side weight precision. Defaults to BF16. **Phase G (2026-05-28)**
230/// adds `Fp8Weights`, gated by env var `AVAROK_DFLASH_DRAFTER_FP8`. The
231/// historical SM12.x acceptance collapse note applied to drafter FP8 KV
232/// cache (different concern — bidirectional attention math); Phase G
233/// targets weight FP8 only, so the risk surface is dynamic-range loss
234/// in MLP intermediate activations, which per-row scales mitigate.
235/// `--mtp-quantization fp8` is still not honored for the DFlash drafter.
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum DflashQuantization {
238    Bf16,
239    /// Weight-only FP8: q/k/v/o/gate/up/down BF16 → FP8 E4M3 with per-row
240    /// f32 scales at model load. Activations stay BF16; KV cache stays
241    /// BF16. GEMMs use `fp8_gemm_n128` (BF16 × FP8 → BF16).
242    Fp8Weights,
243}
244
245/// Per-drafter-layer Qwen3-style weights. Phase 1 is BF16-only; **Phase G**
246/// (2026-05-28) adds optional FP8 weight fields populated at model load
247/// when `AVAROK_DFLASH_DRAFTER_FP8=1`. The BF16 fields are always present
248/// (Fp8 path falls back to them for any GEMM whose Fp8 weight is None).
249#[allow(dead_code)]
250pub struct DflashLayer {
251    // Norms
252    pub input_layernorm: DenseWeight,
253    pub post_attention_layernorm: DenseWeight,
254    // Attention (Qwen3: per-head Q/K RMSNorm)
255    pub q_proj: DenseWeight,
256    pub k_proj: DenseWeight,
257    pub v_proj: DenseWeight,
258    pub o_proj: DenseWeight,
259    pub q_norm: DenseWeight,
260    pub k_norm: DenseWeight,
261    // MLP
262    pub gate_proj: DenseWeight,
263    pub up_proj: DenseWeight,
264    pub down_proj: DenseWeight,
265
266    // Phase G — optional FP8 mirrors of the seven dense-GEMM weights.
267    // Populated at load time when `AVAROK_DFLASH_DRAFTER_FP8=1`, consumed
268    // by forward_block_layer_pre_attn / _post_attn when self.quant ==
269    // DflashQuantization::Fp8Weights. None when BF16 path is active.
270    pub q_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
271    pub k_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
272    pub v_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
273    pub o_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
274    pub gate_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
275    pub up_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
276    pub down_proj_fp8: Option<crate::weight_map::Fp8DenseWeight>,
277
278    // DFlash2 grouped dynamic causal convs (None on DFlash1/DSpark).
279    /// `attention_conv.base_kernel` `[2 applications, kernel_size, hidden]`.
280    pub attention_conv_base: Option<DenseWeight>,
281    /// `attention_conv.kernel_projection.weight` `[2*kernel*groups, hidden]`.
282    pub attention_conv_proj: Option<DenseWeight>,
283    /// `mlp_conv.base_kernel`, same shape as attention_conv_base.
284    pub mlp_conv_base: Option<DenseWeight>,
285    /// `mlp_conv.kernel_projection.weight`, same shape as attention_conv_proj.
286    pub mlp_conv_proj: Option<DenseWeight>,
287}
288
289/// Per-sequence DFlash drafter state. One paged KV cache per drafter layer
290/// (8 typical), shared block table across layers since attention shape is
291/// identical layer-to-layer for a vanilla Qwen3 architecture. Mirrors
292/// `MtpProposerState` in spirit; the multi-layer cache keeps it distinct.
293pub struct DflashProposerState {
294    /// Block table for the drafter's KV cache (shared across all drafter layers).
295    pub block_table: Vec<u32>,
296    /// Current logical sequence length in the drafter's KV cache. Tracks how
297    /// many target-aligned positions have been written via
298    /// `precompute_and_store_context_kv`.
299    pub seq_len: usize,
300    /// Drafts produced in the last `propose()` call. `after_verify` consults
301    /// this to know how many KV positions to roll back when the accept
302    /// prefix is shorter than γ.
303    pub last_num_drafted: usize,
304    /// Whether the prompt-time `precompute_and_store_context_kv` has been
305    /// called. The first `propose()` after model build needs to run prefill
306    /// over the full prompt's captured hiddens; subsequent steps incrementally
307    /// append the latest accepted tokens' projections.
308    pub prefill_done: bool,
309    /// Multi-token accumulator for captured target hidden states. Layout:
310    /// `[max_ctx_len, 5 * target_hidden]` BF16 packed. The scheduler appends
311    /// the model's `dflash_hidden_save` (latest decoded position's 5 hiddens)
312    /// into slot `ctx_len` after each successful verify. `propose()` reads
313    /// the full populated prefix and projects all positions through `fc`
314    /// at forward time. Sized for `max_seq_len` total positions; not
315    /// circular — fail-fast if exceeded (drafter can't handle longer
316    /// context than allocated).
317    pub ctx_hidden_acc: DevicePtr,
318    /// Number of populated slots in `ctx_hidden_acc`. Capped at `max_ctx_len`.
319    pub ctx_len: usize,
320    /// Drafts accepted in the verify that immediately preceded this propose.
321    /// Set by `after_verify` so propose can label row-0 with its TRUE position.
322    pub last_num_accepted: usize,
323    /// EAGLE-fix one-shot: when set, the next `propose()` skips its internal
324    /// decode-append because the verify step (K=2 accept) already appended
325    /// row 0 + row 1 in EAGLE order before calling propose. Consumed (reset to
326    /// false) by propose. Set on the EAGLE-fix path, which is DEFAULT-ON
327    /// (`AVAROK_DFLASH_EAGLE_FIX=0` is the kill switch, not `=1` the opt-in ,
328    /// see verify_k2_step.rs and verify_dflash_step.rs, both `!= Some("0")`).
329    pub skip_next_decode_append: bool,
330    /// Allocation cap for `ctx_hidden_acc` (in slot count). Mirrors the
331    /// `max_seq_len` build arg so we can clamp without re-fetching it.
332    pub max_ctx_len: usize,
333    /// Width (bytes) of one `ctx_hidden_acc` slot — `5 * target_hidden * bf16`.
334    /// Stored to avoid re-deriving on every append.
335    pub ctx_slot_bytes: usize,
336
337    // ─── Phase 2 Option B fields (paged KV cache for ctx) ───────────────
338    /// Device-side block table for the drafter's paged KV cache. Allocated
339    /// once at first propose with enough u32 slots to cover `max_seq_len`
340    /// at block_size=16. Read by `prefill_attention_paged_dflash` to map
341    /// logical block indices to physical pool block indices. Mirrors the
342    /// host-side `block_table` Vec, copied to GPU after each `alloc_block`.
343    pub block_table_dev: Option<DevicePtr>,
344    /// Number of paged-cache slots populated with ctx K/V for this sequence.
345    /// Distinct from `ctx_len` (which counts target_hidden_acc slots). The
346    /// drafter writes one ctx K/V slot per accepted target token; the
347    /// γ-block then attends over `[0..ctx_count_drafter+γ)`. Bumped by γ
348    /// per propose (γ slots written for the noise rows) and trimmed in
349    /// `after_verify` by `(γ - num_accepted)`.
350    pub ctx_count_drafter: usize,
351    /// Cap for `ctx_count_drafter`. Mirrors `block_table.len() * block_size`.
352    pub max_ctx_count_drafter: usize,
353    /// Phase I — incremental ctx precompute watermark. Number of ctx slots
354    /// `[0..ctx_committed)` whose K/V is already valid in the paged cache
355    /// from a prior propose. Each step we only precompute the new tail
356    /// `[ctx_committed..ctx_len)` instead of rebuilding the whole prefix
357    /// (the old O(ctx_len²) waste — see design doc §18). Reset to the
358    /// current `ctx_len` on any rewind so stale slots can't be read.
359    /// `0` forces a full rebuild (first propose, or the debug escape hatch).
360    pub ctx_committed: usize,
361    /// Phase I (v2) — per-slot TRUE absolute decoded position, stamped once
362    /// when a ctx slot is appended and never recomputed. Indexed by ctx
363    /// slot (parallel to `ctx_hidden_acc` slots, len == `ctx_len`). This is
364    /// the vLLM convention: a cached token's rope position is fixed at
365    /// insert time, so committed slots never go stale when later accepts
366    /// shift the live `position`. Replaces the sliding `absolute_start_pos
367    /// + i` formula in `precompute_ctx_kv`. Prefill positions are seeded
368    /// `0..prompt_len` in `update_dflash_ctx_len_after_prefill`.
369    pub ctx_positions: Vec<i32>,
370}
371
372impl ProposerState for DflashProposerState {
373    fn as_any(&self) -> &dyn Any {
374        self
375    }
376    fn as_any_mut(&mut self) -> &mut dyn Any {
377        self
378    }
379}
380
381/// Block-diffusion draft head. Public API is the [`DraftProposer`] trait.
382///
383/// The drafter shares `embed_tokens` and `lm_head` with the target — these
384/// are NOT in the drafter's safetensors checkpoint (verified against
385/// `z-lab/Qwen3.6-35B-A3B-DFlash` commit 42d3b34). The constructor takes
386/// the target's `embed_tokens_shared` and `lm_head_shared` device pointers
387/// at build time and slots them in alongside the drafter's own `fc`,
388/// `hidden_norm`, `norm`, and per-layer weights.
389#[allow(dead_code)]
390/// Per-block-width propose graph state (see `BlockDiffusionDraftHead::propose_graphs`).
391/// `warmup` counts the eager passes run at a width before its capture
392/// (default target 2, `AVAROK_DFLASH_PROPOSE_WARMUP_N`): two eager passes warm
393/// the PTX->SASS cache, ramp GB10 clocks and bring hot weight tiles into L2
394/// before the capture freezes the SASS variants the driver picks.
395#[derive(Default)]
396pub struct ProposeGraphs {
397    pub by_width: std::collections::HashMap<usize, Vec<spark_runtime::gpu::GraphHandle>>,
398    pub warmup: std::collections::HashMap<usize, usize>,
399}
400
401pub struct BlockDiffusionDraftHead {
402    // Drafter-architecture config (mirrors the drafter's HF config.json).
403    pub num_layers: usize,
404    pub hidden_size: usize,
405    pub intermediate_size: usize,
406    pub num_q_heads: usize,
407    pub num_kv_heads: usize,
408    pub head_dim: usize,
409    pub vocab_size: usize,
410    pub draft_vocab_size: usize,
411    /// The drafter's WIDEST block (rows per sequence): the launch `--dflash-gamma`
412    /// or the checkpoint default. Every gamma-sized buffer (scratch bands, the
413    /// paged KV growth, the pinned draft staging) is allocated from THIS. The
414    /// block a given propose actually runs is [`Self::block_g`], which the
415    /// scheduler's gamma resolver moves per step and which never exceeds it.
416    pub gamma: usize,
417    /// ACTIVE block width for the propose in flight, `2..=gamma` (rows per
418    /// sequence, anchor + block_g-1 masks => block_g-1 drafts, verify K =
419    /// block_g). Written by `set_block_g` at the top of every propose from the
420    /// scheduler's `num_drafts`; read by the forward path through `block_g()`
421    /// instead of `gamma`. An atomic only because the head is shared: the
422    /// scheduler proposes one step at a time on one stream, so there is never
423    /// a concurrent writer.
424    /// provenance-id: 526f6e616c6420522e205374657369616b
425    pub(super) block_gamma: std::sync::atomic::AtomicUsize,
426    /// Widest cross-sequence batch the scratch bands can hold.
427    pub(super) max_batch: usize,
428    pub mask_token_id: u32,
429    pub window_size: Option<usize>,
430    /// `target_layer_ids`. Same data as `TransformerModel::dflash_capture_layers`,
431    /// repeated here so the loader is the single source of truth; the model
432    /// reads these to size its capture buffer.
433    pub target_layer_ids: Vec<usize>,
434    /// Target-side hidden_size (used for the `fc` projection input width:
435    /// `target_layer_ids.len() * target_hidden_size`).
436    pub target_hidden_size: usize,
437
438    // === Weights shared with the target ===
439    /// Target's embed_tokens GPU pointer. The drafter's checkpoint has no
440    /// own embeddings — both vocab and embedding dim must match the target
441    /// (Qwen3.6-35B-A3B-DFlash: vocab=248320, hidden=2048 — same as target).
442    pub embed_tokens_shared: DevicePtr,
443    /// Target's lm_head GPU pointer. Used for the drafter's per-position
444    /// argmax over `[γ, vocab]` logits. Valid only when the target lm_head is
445    /// BF16; when `lm_head_nvfp4` is `Some`, the NVFP4 path is used instead.
446    pub lm_head_shared: DevicePtr,
447    /// Target's NVFP4 lm_head (packed + scales), shared with the drafter for
448    /// the final logits GEMM. `Some` when the target ships an NVFP4 lm_head
449    /// (e.g. Holo) — required because a BF16 `dense_gemm` on the NVFP4 buffer
450    /// reads garbage and OOB. `None` → use the BF16 `lm_head_shared`.
451    pub lm_head_nvfp4: Option<QuantizedWeight>,
452    /// Phase G — optional FP8 mirror of the shared lm_head weight,
453    /// `[vocab_size, hidden_size]` FP8 E4M3 + per-row f32 scales.
454    /// Built at model load when `AVAROK_DFLASH_DRAFTER_FP8=1`. Owned by
455    /// the drafter (separate allocation from the shared BF16 ptr) since
456    /// it must not mutate the target model's lm_head. `None` on the
457    /// BF16 path.
458    pub lm_head_shared_fp8: Option<crate::weight_map::Fp8DenseWeight>,
459
460    // === Weights from the drafter checkpoint ===
461    /// Hidden-norm applied to the projected target context before mixing
462    /// with the embedded tokens (Qwen3-DFlash convention; see vLLM
463    /// `DFlashQwen3Model.hidden_norm`).
464    pub hidden_norm: DenseWeight,
465    /// Final RMSNorm before LM head.
466    pub norm: DenseWeight,
467    /// `fc` projection — `[draft_hidden, target_layer_ids.len() * target_hidden_size]`
468    /// BF16. Maps the stack of captured target hiddens to drafter's input space
469    /// once at model entry. Replaces the earlier (incorrect) "per-layer KV
470    /// injection" design.
471    pub fc: DenseWeight,
472    /// Optional draft-vocab-id → target-vocab-id remap. `None` when the
473    /// drafter shares vocab with the target (Qwen3.6-35B-A3B-DFlash case:
474    /// vocab_size == draft_vocab_size == 248320).
475    pub draft_id_to_target_id: Option<DevicePtr>,
476    /// Drafter transformer layers (8 for Qwen3.6-35B-A3B-DFlash).
477    pub layers: Vec<DflashLayer>,
478
479    /// Phase 2 (Option B) fused K/V projection across all L drafter layers.
480    /// Shape: `[L × 2 × kv_dim, h]` BF16 — concatenated `[K0; V0; K1; V1; …]`
481    /// (per-layer K then V interleaved). Built once at construction by
482    /// `copy_d2d`-stitching the per-layer `k_proj.weight` and `v_proj.weight`
483    /// pointers from `layers[i]`. Lets `precompute_ctx_kv` derive every
484    /// drafter layer's ctx K/V via a single `dense_gemm` of shape
485    /// `[new_ctx_count, h] × [h, L·2·kv_dim]` instead of 2·L per-layer GEMMs.
486    ///
487    /// `None` until Phase 2 lands the build (stage 1: kernel/dispatcher
488    /// scaffolding; stage 2: this allocation + the precompute_ctx_kv module;
489    /// stage 3: pyref bit-exact diff). Layout (K then V per layer) chosen
490    /// to match vLLM's `_fused_kv_weight` in `qwen3_dflash.py:381-389`.
491    pub fused_kv_weight: Option<DevicePtr>,
492
493    /// Paged FP8 KV cache. One cache holding all `num_layers` drafter layers,
494    /// laid out the same way the target's KV cache is — block-table-keyed,
495    /// `num_layers × num_kv_heads × head_dim` per slot. Allocating a single
496    /// multi-layer cache (vs. one per drafter layer) matches Atlas's existing
497    /// `PagedKvCache` ABI and lets us reuse the existing `reshape_and_cache`
498    /// kernel without per-layer dispatch overhead.
499    pub kv_cache: Mutex<PagedKvCache>,
500
501    /// Per-step scratch buffers (allocated once at construction, reused).
502    pub scratch: DflashScratch,
503
504    /// All kernel handles needed by `propose()` and the eventual prefill
505    /// projection (`precompute_and_store_context_kv`).
506    pub kernels: DflashKernels,
507
508    /// Per-sequence ctx accumulator capacity (mirrors model's `max_seq_len`).
509    /// Used by `alloc_state` to size each new sequence's `ctx_hidden_acc`.
510    pub max_seq_len: usize,
511
512    /// Pre-computed yarn inv_freq table (`[head_dim/2]` f32 on GPU).
513    /// Drafter rope_scaling: factor=64, beta_fast=32, beta_slow=1,
514    /// original_max_position_embeddings=4096 (per drafter config.json).
515    pub yarn_inv_freq: DevicePtr,
516
517    /// rope_theta (10000000 for Qwen3.6-DFlash). Stored to pass into the
518    /// rope_yarn kernel each step.
519    pub rope_theta: f32,
520
521    /// rotary_dim. Drafter uses full-rotation (rotary_dim = head_dim = 128).
522    pub rotary_dim: usize,
523
524    /// RMSNorm epsilon (drafter inherits Qwen3 default 1e-6).
525    pub rms_norm_eps: f32,
526
527    /// Max number of past target positions injected into the drafter's K/V
528    /// per step. Default γ — drafter sees at most γ ctx + γ noise = 2γ
529    /// attention positions per step. ctx_window=0 disables ctx conditioning
530    /// (degraded quality, ablation only).
531    pub ctx_window: usize,
532
533    // === Phase D (CUDA graph capture) → Phase F (piecewise) ===
534    /// Per-subgraph captured handles. `None` until warm-up completes and
535    /// the first capture pass lands; on the capture pass we fill this
536    /// `Vec` with `2 × num_layers + 1` handles laid out as
537    /// `[pre_0, post_0, pre_1, post_1, ..., pre_{N-1}, post_{N-1}, tail]`.
538    /// Slot index = `layer_idx * 2 + half` for the layer halves
539    /// (half = 0 for pre_attn, 1 for post_attn) and `num_layers * 2` for
540    /// the tail (final norm + lm_head + argmax). `GraphHandle(0)` is the
541    /// "empty capture" sentinel and means that slot replays eager.
542    ///
543    /// Phase F.2 (2026-05-28): replaces the single full-region capture
544    /// with one capture per subgraph. Attention is NEVER captured —
545    /// it's the natural sync barrier between captured subgraphs
546    /// (vLLM piecewise convention). See design doc §15.
547    ///
548    /// Keyed by the ACTIVE block width (`block_g`): a captured subgraph bakes
549    /// in its row count, so each width the gamma resolver runs gets its own
550    /// capture (once, lazily) and switching widths afterwards costs nothing.
551    pub propose_graphs: Mutex<ProposeGraphs>,
552    /// When set, all `forward_block` calls run eagerly. Mirrors target-model
553    /// `TransformerModel::suppress_graphs` so external code can disable
554    /// graphs at runtime (e.g. while calibrating FP8 KV).
555    pub suppress_graphs: std::sync::atomic::AtomicBool,
556    /// Diagnostic and A/B levers, resolved from the environment ONCE when
557    /// this head was built. `forward_block` and its per-layer helpers read
558    /// these instead of the environment — see [`levers::DFlashLevers`].
559    pub levers: levers::DFlashLevers,
560    /// How many eager warm-up calls we've executed against the graph path.
561    /// Default warmup target is 2 (override via `AVAROK_DFLASH_PROPOSE_WARMUP_N`).
562    /// Two eager passes warm the PTX→SASS cache, ramp GB10 clocks to steady
563    /// state, and bring hot weight tiles into L2 before the capture freezes
564    /// SASS variants the driver picks. Shared across all subgraphs — every
565    /// subgraph captures on the same propose call after the warmup target
566    /// is hit.
567    pub propose_warmup_count: std::sync::atomic::AtomicUsize,
568
569    // Quantization mode (BF16 only for Phase 1).
570    pub quant: DflashQuantization,
571
572    // === DSpark heads (optional; None ⇒ plain DFlash behavior) ===
573    /// Markov head rank (0 when the drafter has no Markov head). RadixArk
574    /// Qwen3.8-27B-DSpark: 256.
575    pub markov_rank: usize,
576    /// `markov_w1`: `[vocab, rank]` BF16 prev-token embedding table.
577    pub markov_w1: Option<DenseWeight>,
578    /// `markov_w2`: `[vocab, rank]` BF16 latent→vocab projection
579    /// (`Linear(rank, vocab, bias=False).weight`, `[N, K]` GEMV layout).
580    pub markov_w2: Option<DenseWeight>,
581    /// Confidence head (`AcceptRatePredictor`) weight `[1, hidden(+rank)]`.
582    /// Loaded for the dynamic-K phase; not consumed by the Markov fixup.
583    pub confidence_proj: Option<DenseWeight>,
584    /// Confidence head bias `[1]`.
585    pub confidence_bias: Option<DenseWeight>,
586    /// Whether the confidence input is `[hidden ‖ markov_embed]` (true) or
587    /// hidden only (false). Mirrors `confidence_head_with_markov`.
588    pub confidence_with_markov: bool,
589    /// SpecForge shifted row convention (drafter config
590    /// `dflash_config.projector_type == "dspark"`): row j's output is the
591    /// token at position j+1, so the returned draft vector is rotated right
592    /// by one to line up with Atlas's z-lab-convention verify indexing.
593    /// Overridable for A/B via `AVAROK_DSPARK_SHIFT=0|1`.
594    pub shifted_rows: bool,
595
596    // === DFlash2 (None/0 ⇒ plain DFlash behavior) ===
597    /// Conv kernel size (2) — taps per conv application.
598    pub conv_kernel_size: usize,
599    /// Channels per conv group (16).
600    pub conv_group_size: usize,
601    /// Selector codebook rank (256).
602    pub selector_rank: usize,
603    /// Candidates per position for the selector walk (16; the kernels are
604    /// specialized to 16 — other values refuse to arm).
605    pub selector_top_k: usize,
606    /// `candidate_selector.predecessor_codebook` `[vocab, rank]`.
607    pub selector_pred: Option<DenseWeight>,
608    /// `candidate_selector.successor_codebook` `[vocab, rank]`.
609    pub selector_succ: Option<DenseWeight>,
610    /// `candidate_selector.hidden_projection.weight` `[rank, hidden]`.
611    pub selector_hidden_proj: Option<DenseWeight>,
612}
613
614mod dflash2;
615/// Whether the Option-B paged drafter cache is on. Default ON since the 54.5
616/// record config (#649); `AVAROK_DFLASH_OPTION_B=0` is the kill switch.
617///
618/// Split into a reader and a pure predicate because the POLARITY is the whole
619/// point and it has already been flipped by accident: a merge on 2026-08-30
620/// took #817's allocator region whole, #817 branched from a tree predating the
621/// flip, and `!= Some("0")` silently became `== Some("1")`. Measured cost of
622/// that one character-class: propose 19.8 -> 618.7 ms and 49.9 -> 5.5 tok/s,
623/// because the legacy path launches one `dense_gemv` per accumulated ctx row
624/// over a 262 MB `fc` weight. Nothing logged a change.
625/// Rows the batched ctx precompute staging holds per step (C=16 at gamma 4
626/// commits ~2-4 rows per sequence per step; 256 covers 16 x 16).
627pub(super) const PRECOMPUTE_BATCH_ROWS: usize = 256;
628
629/// Batched ctx precompute (one fc + fused-KV pass over every sequence's
630/// uncommitted ctx rows) is the default on the batched propose path.
631/// `AVAROK_DFLASH_NO_BATCHED_PRECOMPUTE=1` restores the per-sequence loop.
632pub(super) fn batched_precompute_enabled() -> bool {
633    std::env::var("AVAROK_DFLASH_NO_BATCHED_PRECOMPUTE")
634        .ok()
635        .as_deref()
636        != Some("1")
637}
638
639/// The predicate itself, pure over the raw value so a test can exercise the
640/// PRODUCTION code rather than a copy of it. `set_var` is unsafe and
641/// process-global, so a test that mutated the environment would race every
642/// other test in this binary.
643pub(super) fn option_b_from(v: Option<&str>) -> bool {
644    v != Some("0")
645}
646
647#[cfg(test)]
648mod option_b_tests {
649    use super::option_b_from;
650
651    #[test]
652    fn option_b_defaults_on_and_only_zero_turns_it_off() {
653        // THE REGRESSION, and the reason this test exists: unset must mean ON.
654        // A bare `--dflash` launch is the record path with no env block (#649).
655        // When a merge turned this into opt-in, the only symptom was a run
656        // nine times slower.
657        assert!(
658            option_b_from(None),
659            "unset must be ON — this is the 9x line"
660        );
661        assert!(option_b_from(Some("1")));
662        // House convention: `=0` is the kill switch, and nothing else is.
663        assert!(!option_b_from(Some("0")));
664        assert!(
665            option_b_from(Some("true")),
666            "only the exact string 0 disables"
667        );
668        assert!(option_b_from(Some("")), "empty is not a kill switch");
669    }
670}
671
672mod forward_block;
673mod forward_block_layer;
674mod forward_block_layer_paged;
675mod from_weights;
676pub mod levers;
677mod markov;
678mod precompute_ctx_kv;
679mod precompute_ctx_kv_batched;
680mod propose;
681
682impl DraftProposer for BlockDiffusionDraftHead {
683    fn block_gamma(&self) -> Option<usize> {
684        Some(self.gamma)
685    }
686
687    fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn ProposerState>> {
688        self.alloc_state_windowed(gpu, usize::MAX)
689    }
690
691    fn alloc_state_for(
692        &self,
693        gpu: &dyn GpuBackend,
694        budget_tokens: usize,
695    ) -> Result<Box<dyn ProposerState>> {
696        self.alloc_state_windowed(gpu, budget_tokens)
697    }
698
699    fn propose(
700        &self,
701        last_token: u32,
702        target_hidden: spark_runtime::gpu::DevicePtr,
703        position: usize,
704        num_drafts: usize,
705        state: &mut dyn ProposerState,
706        ctx: &crate::layer::ForwardContext,
707        stream: u64,
708        draft_embed_target: Option<spark_runtime::gpu::DevicePtr>,
709        grammar_bitmask: Option<&[i32]>,
710        target_hidden_stack: Option<spark_runtime::gpu::DevicePtr>,
711    ) -> Result<Vec<u32>> {
712        self.propose_drafts(
713            last_token,
714            target_hidden,
715            position,
716            num_drafts,
717            state,
718            ctx,
719            stream,
720            draft_embed_target,
721            grammar_bitmask,
722            target_hidden_stack,
723            None,
724        )
725    }
726
727    /// Widest batch one drafter forward can carry. Bounded by the scratch
728    /// bands (`max_batch`); `1` means the batched path cannot run and the
729    /// caller stays on `propose`.
730    fn propose_batch_max(
731        &self,
732        _buffers: &spark_runtime::buffers::BufferArena,
733        _config: &avarok_core::config::ModelConfig,
734    ) -> usize {
735        if !self.dflash2_active() {
736            return 1;
737        }
738        // DEFAULT-ON. `AVAROK_DFLASH_BATCH_PROPOSE=<width>` overrides: `1`
739        // (or `0`) disables and restores the per-sequence loop, `N` caps the
740        // batch at N sequences. Numeric rather than boolean because
741        // bisecting the WIDTH against acceptance is what localises a banding
742        // bug — "correct at 2 bands, wrong at 4" is the observation that
743        // found the lm_head tile bound, and an on/off flag cannot ask it.
744        let want = self.levers.batch_propose_width;
745        if want < 2 {
746            return 1;
747        }
748        want.min(self.max_batch.max(1))
749    }
750
751    /// Cross-sequence batched propose: ONE drafter forward over `n * gamma`
752    /// rows instead of `n` forwards.
753    ///
754    /// Per-sequence preparation (ctx append, Option-B block growth, the
755    /// incremental ctx precompute) still runs per sequence — it is cheap,
756    /// touching only the uncommitted ctx tail — and it reuses
757    /// `propose_drafts`' own prep through the `collect_prep` sink so the two
758    /// paths cannot drift. The expensive part, the drafter layers plus an
759    /// lm_head against a 248k vocab, runs ONCE for the whole batch. That is
760    /// the entire win.
761    ///
762    /// Returns `Ok(None)` to decline, and the caller falls back to the
763    /// per-sequence loop — never a wrong answer.
764    fn propose_batch(
765        &self,
766        last_tokens: &[u32],
767        _target_hiddens: &[spark_runtime::gpu::DevicePtr],
768        positions: &[usize],
769        num_drafts: usize,
770        states: &mut [&mut dyn ProposerState],
771        ctx: &crate::layer::ForwardContext,
772        stream: u64,
773        _out_conf: Option<&mut Vec<Vec<f32>>>,
774    ) -> Result<Option<Vec<Vec<u32>>>> {
775        let n = last_tokens.len();
776        if n < 2
777            || n > self.max_batch
778            || positions.len() != n
779            || states.len() != n
780            || !self.dflash2_active()
781        {
782            return Ok(None);
783        }
784
785        // Phase 1 — per-sequence prep, collecting each sequence's paged
786        // descriptor. A sequence that cannot run Option B (drafter block pool
787        // exhausted, say) aborts the WHOLE batch to the per-sequence path
788        // rather than letting the rest draft against a missing band.
789        // Gamma resolver: this step's block width, from the scheduler's draft
790        // count (C>=2 lands on the K=4 write-on-accept verify).
791        self.set_block_g(num_drafts);
792        let mut prep: Vec<(spark_runtime::gpu::DevicePtr, u32)> = Vec::with_capacity(n);
793        for (i, st) in states.iter_mut().enumerate() {
794            let before = prep.len();
795            match self.propose_drafts(
796                last_tokens[i],
797                spark_runtime::gpu::DevicePtr::NULL,
798                positions[i],
799                num_drafts,
800                *st,
801                ctx,
802                stream,
803                None,
804                None,
805                None,
806                Some(&mut prep),
807            ) {
808                Ok(_) if prep.len() == before + 1 => {}
809                Ok(_) => return Ok(None),
810                Err(e) => {
811                    tracing::warn!("DFlash batched propose prep (seq {i}): {e:#} — per-seq path");
812                    return Ok(None);
813                }
814            }
815        }
816
817        // Phase 1b — ONE ctx precompute over every sequence's uncommitted
818        // rows (the prep above deferred it). On failure nothing was
819        // committed, so the per-sequence path recomputes safely.
820        if batched_precompute_enabled()
821            && let Err(e) = self.precompute_ctx_kv_batched(states, ctx, stream)
822        {
823            tracing::warn!("DFlash batched ctx precompute: {e:#} — per-seq path");
824            return Ok(None);
825        }
826
827        // Phase 2 — ONE forward over every band.
828        let batch = DflashBatch {
829            last_tokens,
830            positions,
831            block_tables: prep.iter().map(|p| p.0).collect(),
832            ctx_counts: prep.iter().map(|p| p.1).collect(),
833        };
834        let all = match self.forward_block(
835            last_tokens[0],
836            positions[0],
837            ctx,
838            stream,
839            None,
840            Some(prep[0]),
841            Some(&batch),
842        ) {
843            Ok(v) => v,
844            Err(e) => {
845                tracing::warn!("DFlash batched forward_block: {e:#} — falling back to per-seq");
846                return Ok(None);
847            }
848        };
849        let g = self.block_g();
850        if all.len() < n * g {
851            tracing::warn!(
852                "DFlash batched forward returned {} rows, expected {} — per-seq path",
853                all.len(),
854                n * g
855            );
856            return Ok(None);
857        }
858
859        // Phase 3 — split bands. Row 0 of each band is the anchor echo the
860        // single-sequence path drops too; the rest are that sequence's drafts.
861        let cap = self.levers.draft_cap.unwrap_or(g);
862        let mut out: Vec<Vec<u32>> = Vec::with_capacity(n);
863        for (i, st) in states.iter_mut().enumerate() {
864            let band = &all[i * g..(i + 1) * g];
865            let drafts: Vec<u32> = if self.mask_token_id != 0 {
866                band.iter().skip(1).copied().take(cap).collect()
867            } else {
868                band.iter().copied().take(cap).collect()
869            };
870            if let Some(d) = st.as_any_mut().downcast_mut::<DflashProposerState>() {
871                d.last_num_drafted = drafts.len();
872            }
873            out.push(drafts);
874        }
875        Ok(Some(out))
876    }
877
878    fn after_verify(
879        &self,
880        num_accepted: usize,
881        state: &mut dyn ProposerState,
882        _stream: u64,
883    ) -> Result<()> {
884        let dstate = state
885            .as_any_mut()
886            .downcast_mut::<DflashProposerState>()
887            .ok_or_else(|| anyhow::anyhow!("Invalid DFlash proposer state"))?;
888        // Phase 1: no real KV trim because `propose()` is a stub. Phase 2
889        // adds the rollback that drops `(last_num_drafted - num_accepted)`
890        // tokens from each layer's paged cache.
891        //
892        // Phase I invariant: `ctx_committed` is the watermark of ctx slots
893        // already precomputed into the paged cache. It is monotonic only as
894        // long as `ctx_len` is monotonic (today it is — ctx is append-only
895        // and never rewound here). IF a future rollback ever shrinks the
896        // committed ctx (rewinds `ctx_len`), it MUST also reset
897        // `dstate.ctx_committed = dstate.ctx_len` so the next propose
898        // recomputes the rolled-back tail instead of reading stale K/V.
899        // The `.min(ctx_len)` clamp in propose() is the defensive backstop.
900        let _ = num_accepted;
901        dstate.last_num_drafted = 0;
902        Ok(())
903    }
904
905    fn free_state(&self, gpu: &dyn GpuBackend, state: &mut dyn ProposerState) -> Result<()> {
906        // Phase 2 (Option B) reclaim: return the drafter's lazily-allocated
907        // paged KV blocks to the pool on request completion. Without this the
908        // ~257-block Option-B drafter cache (allocated in propose.rs when
909        // block_table_dev.is_none()) is never freed, so the SECOND request to
910        // a long-lived server starts with zero free drafter blocks and floods
911        // "DFlash Option B: paged KV cache exhausted". Mirrors MtpHead::free_state.
912        let dstate = match state.as_any_mut().downcast_mut::<DflashProposerState>() {
913            Some(s) => s,
914            // Phase 1 / non-DFlash proposer state: nothing allocated, nothing to free.
915            None => return Ok(()),
916        };
917        if !dstate.block_table.is_empty() {
918            self.kv_cache.lock().free_blocks(&dstate.block_table);
919            dstate.block_table.clear();
920        }
921        // Free the per-seq ctx accumulator — the dominant per-request
922        // allocation (`max_seq_len × 5 × target_hidden` BF16; ~320 MB at
923        // max_seq_len=16384). `DevicePtr` has no Drop, so without this every
924        // finished sequence leaks it for the server's lifetime. Guarded on a
925        // non-null pointer so a double free_state is a no-op.
926        if dstate.ctx_hidden_acc.0 != 0 {
927            gpu.free(dstate.ctx_hidden_acc)?;
928            dstate.ctx_hidden_acc = DevicePtr(0);
929        }
930        // Free the device-side block table (lazily allocated in propose.rs).
931        if let Some(bt) = dstate.block_table_dev.take() {
932            gpu.free(bt)?;
933        }
934        // Reset the lazy-alloc guard + watermarks so the NEXT request's first
935        // propose re-allocates fresh blocks and re-precomputes ctx from a clean
936        // slate (propose.rs gates alloc on block_table_dev.is_none()).
937        dstate.max_ctx_count_drafter = 0;
938        dstate.ctx_count_drafter = 0;
939        dstate.ctx_committed = 0;
940        dstate.ctx_positions.clear();
941        dstate.seq_len = 0;
942        dstate.ctx_len = 0;
943        dstate.prefill_done = false;
944        dstate.last_num_drafted = 0;
945        dstate.last_num_accepted = 0;
946        dstate.skip_next_decode_append = false;
947        Ok(())
948    }
949}
950
951/// AVAROK_NO_DFLASH_FP8_RT=1 restores the tile-GEMM propose path for A/B
952/// (strict `== "1"`, matching the sibling AVAROK_NO_* levers). OnceLock so
953/// the kernel choice is stable across CUDA-graph capture.
954pub(crate) fn fp8_rt_enabled() -> bool {
955    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
956    *ON.get_or_init(|| std::env::var("AVAROK_NO_DFLASH_FP8_RT").as_deref() != Ok("1"))
957}
958
959/// The DFlash context-window bound, in tokens: the most recent target
960/// positions the drafter is allowed to accumulate and attend to.
961///
962/// SINGLE DEFINITION on purpose. Two buffers are sized from it — the
963/// per-sequence ctx accumulator here, and the model-level whole-prompt hidden
964/// capture (`impl_a1`) that feeds `prefill_drafter` — and the drafter cannot
965/// use more prompt than it can store, so capturing past this bound is dead
966/// memory. Letting the two drift is exactly the ceiling-vs-need bug this
967/// bound exists to close.
968///
969/// `AVAROK_DFLASH_CTX_CAP=<tokens>`; `0` disables the cap entirely.
970pub fn dflash_ctx_cap() -> usize {
971    std::env::var("AVAROK_DFLASH_CTX_CAP")
972        .ok()
973        .and_then(|v| v.parse::<usize>().ok())
974        .unwrap_or(16384)
975}
976
977impl BlockDiffusionDraftHead {
978    /// Block width (rows per sequence) of the propose in flight. See `block_gamma`.
979    #[inline]
980    pub fn block_g(&self) -> usize {
981        self.block_gamma.load(std::sync::atomic::Ordering::Relaxed)
982    }
983
984    /// Arm the block width for the next propose from the scheduler's draft
985    /// count: `num_drafts + 1` rows (anchor + masks), clamped to `2..=gamma`
986    /// so a resolver that asks for more than the head was sized for gets the
987    /// widest block rather than an out-of-band scratch write.
988    #[inline]
989    pub(super) fn set_block_g(&self, num_drafts: usize) {
990        let g = (num_drafts + 1).clamp(2, self.gamma.max(2));
991        self.block_gamma
992            .store(g, std::sync::atomic::Ordering::Relaxed);
993    }
994
995    /// Allocate proposer state with the ctx accumulator sized to the smallest
996    /// of: this request's token budget, the AVAROK_DFLASH_CTX_CAP window, and
997    /// `--max-seq-len`.
998    fn alloc_state_windowed(
999        &self,
1000        gpu: &dyn GpuBackend,
1001        budget_tokens: usize,
1002    ) -> Result<Box<dyn ProposerState>> {
1003        // Per-seq ctx accumulator: `[max_seq_len, 5 * target_hidden] BF16`.
1004        // Sized once, re-used across the seq's lifetime; reset on
1005        // `free_state`. At max_seq_len=16384 and 5×2048 BF16: 320 MB per
1006        // seq — tolerable on a single Spark with max_batch_size=1; for
1007        // higher batch we may want to reduce to a smaller working window.
1008        let bf16 = 2usize;
1009        let ctx_slot_bytes = self.target_layer_ids.len() * self.target_hidden_size * bf16;
1010        // WORKING WINDOW, not max_seq_len. This buffer is per SEQUENCE and
1011        // scales with the context ceiling: at 128K x 5 layers x 5120 BF16 it
1012        // is 6.7 GB EACH, so 8 concurrent sequences ask for 53.7 GB — lazily,
1013        // as streams arrive, which is why it OOMs a long way past a clean
1014        // boot rather than at startup. Capping the window bounds it to
1015        // `cap * ctx_slot_bytes` per sequence (16K -> 839 MB, 8 seqs -> 6.7 GB).
1016        //
1017        // Correctness: `commit_ctx` already slides a watermark when the
1018        // accumulator fills, keeping the NEWEST half and re-stamping
1019        // ctx_positions, so a smaller window is an already-exercised path —
1020        // the drafter conditions on recent context instead of the whole
1021        // history. Raise with AVAROK_DFLASH_CTX_CAP=<tokens> (0 = uncapped,
1022        // the pre-cap behaviour) if you have the memory and want the drafter
1023        // to see further back.
1024        let cap = dflash_ctx_cap();
1025        let ceiling = if cap == 0 {
1026            self.max_seq_len
1027        } else {
1028            self.max_seq_len.min(cap)
1029        };
1030        // The request's own reach (prompt + max_tokens) when the caller knows
1031        // it: a 2K-token turn has no use for a 16K accumulator, and this
1032        // buffer is paid PER SEQUENCE. `+ gamma + 1` covers the draft block
1033        // and bonus slot the ctx accumulates past the last emitted token.
1034        let window = ceiling.min(budget_tokens.saturating_add(self.gamma + 1));
1035        if ceiling < self.max_seq_len {
1036            static LOGGED: std::sync::atomic::AtomicBool =
1037                std::sync::atomic::AtomicBool::new(false);
1038            if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
1039                tracing::info!(
1040                    "DFlash ctx window capped to {} of --max-seq-len {} ({} MB/seq instead of \
1041                     {} MB): the accumulator is PER SEQUENCE, so the uncapped size is what \
1042                     OOMs a high-concurrency long-context serve. Override with \
1043                     AVAROK_DFLASH_CTX_CAP=<tokens> (0 = uncapped).",
1044                    ceiling,
1045                    self.max_seq_len,
1046                    ceiling * ctx_slot_bytes / (1024 * 1024),
1047                    self.max_seq_len * ctx_slot_bytes / (1024 * 1024),
1048                );
1049            }
1050        }
1051        let total = window * ctx_slot_bytes;
1052        let ctx_hidden_acc = gpu.alloc(total)?;
1053        // Initialize to zero so stale data doesn't leak between sequences.
1054        gpu.memset(ctx_hidden_acc, 0, total)?;
1055        Ok(Box::new(DflashProposerState {
1056            block_table: Vec::with_capacity(64),
1057            seq_len: 0,
1058            last_num_drafted: 0,
1059            prefill_done: false,
1060            ctx_hidden_acc,
1061            ctx_len: 0,
1062            last_num_accepted: 0,
1063            skip_next_decode_append: false,
1064            max_ctx_len: window,
1065            ctx_slot_bytes,
1066            // Phase 2 Option B: lazily allocated on first propose when
1067            // AVAROK_DFLASH_OPTION_B=1. None until then to keep alloc_state
1068            // cheap for sequences that never use Option B.
1069            block_table_dev: None,
1070            ctx_count_drafter: 0,
1071            max_ctx_count_drafter: 0,
1072            ctx_committed: 0,
1073            ctx_positions: Vec::new(),
1074        }))
1075    }
1076}