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