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 /// How many eager warm-up calls we've executed against the graph path.
523 /// Default warmup target is 2 (override via `ATLAS_DFLASH_PROPOSE_WARMUP_N`).
524 /// Two eager passes warm the PTX→SASS cache, ramp GB10 clocks to steady
525 /// state, and bring hot weight tiles into L2 before the capture freezes
526 /// SASS variants the driver picks. Shared across all subgraphs — every
527 /// subgraph captures on the same propose call after the warmup target
528 /// is hit.
529 pub propose_warmup_count: std::sync::atomic::AtomicUsize,
530
531 // Quantization mode (BF16 only for Phase 1).
532 pub quant: DflashQuantization,
533
534 // === DSpark heads (optional; None ⇒ plain DFlash behavior) ===
535 /// Markov head rank (0 when the drafter has no Markov head). RadixArk
536 /// Qwen3.8-27B-DSpark: 256.
537 pub markov_rank: usize,
538 /// `markov_w1`: `[vocab, rank]` BF16 prev-token embedding table.
539 pub markov_w1: Option<DenseWeight>,
540 /// `markov_w2`: `[vocab, rank]` BF16 latent→vocab projection
541 /// (`Linear(rank, vocab, bias=False).weight`, `[N, K]` GEMV layout).
542 pub markov_w2: Option<DenseWeight>,
543 /// Confidence head (`AcceptRatePredictor`) weight `[1, hidden(+rank)]`.
544 /// Loaded for the dynamic-K phase; not consumed by the Markov fixup.
545 pub confidence_proj: Option<DenseWeight>,
546 /// Confidence head bias `[1]`.
547 pub confidence_bias: Option<DenseWeight>,
548 /// Whether the confidence input is `[hidden ‖ markov_embed]` (true) or
549 /// hidden only (false). Mirrors `confidence_head_with_markov`.
550 pub confidence_with_markov: bool,
551 /// SpecForge shifted row convention (drafter config
552 /// `dflash_config.projector_type == "dspark"`): row j's output is the
553 /// token at position j+1, so the returned draft vector is rotated right
554 /// by one to line up with Atlas's z-lab-convention verify indexing.
555 /// Overridable for A/B via `ATLAS_DSPARK_SHIFT=0|1`.
556 pub shifted_rows: bool,
557
558 // === DFlash2 (None/0 ⇒ plain DFlash behavior) ===
559 /// Conv kernel size (2) — taps per conv application.
560 pub conv_kernel_size: usize,
561 /// Channels per conv group (16).
562 pub conv_group_size: usize,
563 /// Selector codebook rank (256).
564 pub selector_rank: usize,
565 /// Candidates per position for the selector walk (16; the kernels are
566 /// specialized to 16 — other values refuse to arm).
567 pub selector_top_k: usize,
568 /// `candidate_selector.predecessor_codebook` `[vocab, rank]`.
569 pub selector_pred: Option<DenseWeight>,
570 /// `candidate_selector.successor_codebook` `[vocab, rank]`.
571 pub selector_succ: Option<DenseWeight>,
572 /// `candidate_selector.hidden_projection.weight` `[rank, hidden]`.
573 pub selector_hidden_proj: Option<DenseWeight>,
574}
575
576mod dflash2;
577/// Whether the Option-B paged drafter cache is on. Default ON since the 54.5
578/// record config (#649); `ATLAS_DFLASH_OPTION_B=0` is the kill switch.
579///
580/// Split into a reader and a pure predicate because the POLARITY is the whole
581/// point and it has already been flipped by accident: a merge on 2026-08-30
582/// took #817's allocator region whole, #817 branched from a tree predating the
583/// flip, and `!= Some("0")` silently became `== Some("1")`. Measured cost of
584/// that one character-class: propose 19.8 -> 618.7 ms and 49.9 -> 5.5 tok/s,
585/// because the legacy path launches one `dense_gemv` per accumulated ctx row
586/// over a 262 MB `fc` weight. Nothing logged a change.
587pub(super) fn option_b_enabled() -> bool {
588 option_b_from(std::env::var("ATLAS_DFLASH_OPTION_B").ok().as_deref())
589}
590
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;
628mod markov;
629mod precompute_ctx_kv;
630mod propose;
631
632impl DraftProposer for BlockDiffusionDraftHead {
633 fn block_gamma(&self) -> Option<usize> {
634 Some(self.gamma)
635 }
636
637 fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn ProposerState>> {
638 self.alloc_state_windowed(gpu, usize::MAX)
639 }
640
641 fn alloc_state_for(
642 &self,
643 gpu: &dyn GpuBackend,
644 budget_tokens: usize,
645 ) -> Result<Box<dyn ProposerState>> {
646 self.alloc_state_windowed(gpu, budget_tokens)
647 }
648
649 fn propose(
650 &self,
651 last_token: u32,
652 target_hidden: spark_runtime::gpu::DevicePtr,
653 position: usize,
654 num_drafts: usize,
655 state: &mut dyn ProposerState,
656 ctx: &crate::layer::ForwardContext,
657 stream: u64,
658 draft_embed_target: Option<spark_runtime::gpu::DevicePtr>,
659 grammar_bitmask: Option<&[i32]>,
660 target_hidden_stack: Option<spark_runtime::gpu::DevicePtr>,
661 ) -> Result<Vec<u32>> {
662 self.propose_drafts(
663 last_token,
664 target_hidden,
665 position,
666 num_drafts,
667 state,
668 ctx,
669 stream,
670 draft_embed_target,
671 grammar_bitmask,
672 target_hidden_stack,
673 None,
674 )
675 }
676
677 /// Widest batch one drafter forward can carry. Bounded by the scratch
678 /// bands (`max_batch`); `1` means the batched path cannot run and the
679 /// caller stays on `propose`.
680 fn propose_batch_max(
681 &self,
682 _buffers: &spark_runtime::buffers::BufferArena,
683 _config: &atlas_core::config::ModelConfig,
684 ) -> usize {
685 if !self.dflash2_active() {
686 return 1;
687 }
688 // DEFAULT-ON. `ATLAS_DFLASH_BATCH_PROPOSE=<width>` overrides: `1`
689 // (or `0`) disables and restores the per-sequence loop, `N` caps the
690 // batch at N sequences. Numeric rather than boolean because
691 // bisecting the WIDTH against acceptance is what localises a banding
692 // bug — "correct at 2 bands, wrong at 4" is the observation that
693 // found the lm_head tile bound, and an on/off flag cannot ask it.
694 let want: usize = std::env::var("ATLAS_DFLASH_BATCH_PROPOSE")
695 .ok()
696 .and_then(|v| v.parse().ok())
697 .unwrap_or(usize::MAX);
698 if want < 2 {
699 return 1;
700 }
701 want.min(self.max_batch.max(1))
702 }
703
704 /// Cross-sequence batched propose: ONE drafter forward over `n * gamma`
705 /// rows instead of `n` forwards.
706 ///
707 /// Per-sequence preparation (ctx append, Option-B block growth, the
708 /// incremental ctx precompute) still runs per sequence — it is cheap,
709 /// touching only the uncommitted ctx tail — and it reuses
710 /// `propose_drafts`' own prep through the `collect_prep` sink so the two
711 /// paths cannot drift. The expensive part, the drafter layers plus an
712 /// lm_head against a 248k vocab, runs ONCE for the whole batch. That is
713 /// the entire win.
714 ///
715 /// Returns `Ok(None)` to decline, and the caller falls back to the
716 /// per-sequence loop — never a wrong answer.
717 fn propose_batch(
718 &self,
719 last_tokens: &[u32],
720 _target_hiddens: &[spark_runtime::gpu::DevicePtr],
721 positions: &[usize],
722 num_drafts: usize,
723 states: &mut [&mut dyn ProposerState],
724 ctx: &crate::layer::ForwardContext,
725 stream: u64,
726 _out_conf: Option<&mut Vec<Vec<f32>>>,
727 ) -> Result<Option<Vec<Vec<u32>>>> {
728 let n = last_tokens.len();
729 if n < 2
730 || n > self.max_batch
731 || positions.len() != n
732 || states.len() != n
733 || !self.dflash2_active()
734 {
735 return Ok(None);
736 }
737
738 // Phase 1 — per-sequence prep, collecting each sequence's paged
739 // descriptor. A sequence that cannot run Option B (drafter block pool
740 // exhausted, say) aborts the WHOLE batch to the per-sequence path
741 // rather than letting the rest draft against a missing band.
742 let mut prep: Vec<(spark_runtime::gpu::DevicePtr, u32)> = Vec::with_capacity(n);
743 for (i, st) in states.iter_mut().enumerate() {
744 let before = prep.len();
745 match self.propose_drafts(
746 last_tokens[i],
747 spark_runtime::gpu::DevicePtr::NULL,
748 positions[i],
749 num_drafts,
750 *st,
751 ctx,
752 stream,
753 None,
754 None,
755 None,
756 Some(&mut prep),
757 ) {
758 Ok(_) if prep.len() == before + 1 => {}
759 Ok(_) => return Ok(None),
760 Err(e) => {
761 tracing::warn!("DFlash batched propose prep (seq {i}): {e:#} — per-seq path");
762 return Ok(None);
763 }
764 }
765 }
766
767 // Phase 2 — ONE forward over every band.
768 let batch = DflashBatch {
769 last_tokens,
770 positions,
771 block_tables: prep.iter().map(|p| p.0).collect(),
772 ctx_counts: prep.iter().map(|p| p.1).collect(),
773 };
774 let all = match self.forward_block(
775 last_tokens[0],
776 positions[0],
777 ctx,
778 stream,
779 None,
780 Some(prep[0]),
781 Some(&batch),
782 ) {
783 Ok(v) => v,
784 Err(e) => {
785 tracing::warn!("DFlash batched forward_block: {e:#} — falling back to per-seq");
786 return Ok(None);
787 }
788 };
789 if all.len() < n * self.gamma {
790 tracing::warn!(
791 "DFlash batched forward returned {} rows, expected {} — per-seq path",
792 all.len(),
793 n * self.gamma
794 );
795 return Ok(None);
796 }
797
798 // Phase 3 — split bands. Row 0 of each band is the anchor echo the
799 // single-sequence path drops too; the rest are that sequence's drafts.
800 let cap = std::env::var("ATLAS_DFLASH_DRAFT_CAP")
801 .ok()
802 .and_then(|v| v.parse::<usize>().ok())
803 .unwrap_or(self.gamma);
804 let mut out: Vec<Vec<u32>> = Vec::with_capacity(n);
805 for (i, st) in states.iter_mut().enumerate() {
806 let band = &all[i * self.gamma..(i + 1) * self.gamma];
807 let drafts: Vec<u32> = if self.mask_token_id != 0 {
808 band.iter().skip(1).copied().take(cap).collect()
809 } else {
810 band.iter().copied().take(cap).collect()
811 };
812 if let Some(d) = st.as_any_mut().downcast_mut::<DflashProposerState>() {
813 d.last_num_drafted = drafts.len();
814 }
815 out.push(drafts);
816 }
817 Ok(Some(out))
818 }
819
820 fn after_verify(
821 &self,
822 num_accepted: usize,
823 state: &mut dyn ProposerState,
824 _stream: u64,
825 ) -> Result<()> {
826 let dstate = state
827 .as_any_mut()
828 .downcast_mut::<DflashProposerState>()
829 .ok_or_else(|| anyhow::anyhow!("Invalid DFlash proposer state"))?;
830 // Phase 1: no real KV trim because `propose()` is a stub. Phase 2
831 // adds the rollback that drops `(last_num_drafted - num_accepted)`
832 // tokens from each layer's paged cache.
833 //
834 // Phase I invariant: `ctx_committed` is the watermark of ctx slots
835 // already precomputed into the paged cache. It is monotonic only as
836 // long as `ctx_len` is monotonic (today it is — ctx is append-only
837 // and never rewound here). IF a future rollback ever shrinks the
838 // committed ctx (rewinds `ctx_len`), it MUST also reset
839 // `dstate.ctx_committed = dstate.ctx_len` so the next propose
840 // recomputes the rolled-back tail instead of reading stale K/V.
841 // The `.min(ctx_len)` clamp in propose() is the defensive backstop.
842 let _ = num_accepted;
843 dstate.last_num_drafted = 0;
844 Ok(())
845 }
846
847 fn free_state(&self, gpu: &dyn GpuBackend, state: &mut dyn ProposerState) -> Result<()> {
848 // Phase 2 (Option B) reclaim: return the drafter's lazily-allocated
849 // paged KV blocks to the pool on request completion. Without this the
850 // ~257-block Option-B drafter cache (allocated in propose.rs when
851 // block_table_dev.is_none()) is never freed, so the SECOND request to
852 // a long-lived server starts with zero free drafter blocks and floods
853 // "DFlash Option B: paged KV cache exhausted". Mirrors MtpHead::free_state.
854 let dstate = match state.as_any_mut().downcast_mut::<DflashProposerState>() {
855 Some(s) => s,
856 // Phase 1 / non-DFlash proposer state: nothing allocated, nothing to free.
857 None => return Ok(()),
858 };
859 if !dstate.block_table.is_empty() {
860 self.kv_cache.lock().free_blocks(&dstate.block_table);
861 dstate.block_table.clear();
862 }
863 // Free the per-seq ctx accumulator — the dominant per-request
864 // allocation (`max_seq_len × 5 × target_hidden` BF16; ~320 MB at
865 // max_seq_len=16384). `DevicePtr` has no Drop, so without this every
866 // finished sequence leaks it for the server's lifetime. Guarded on a
867 // non-null pointer so a double free_state is a no-op.
868 if dstate.ctx_hidden_acc.0 != 0 {
869 gpu.free(dstate.ctx_hidden_acc)?;
870 dstate.ctx_hidden_acc = DevicePtr(0);
871 }
872 // Free the device-side block table (lazily allocated in propose.rs).
873 if let Some(bt) = dstate.block_table_dev.take() {
874 gpu.free(bt)?;
875 }
876 // Reset the lazy-alloc guard + watermarks so the NEXT request's first
877 // propose re-allocates fresh blocks and re-precomputes ctx from a clean
878 // slate (propose.rs gates alloc on block_table_dev.is_none()).
879 dstate.max_ctx_count_drafter = 0;
880 dstate.ctx_count_drafter = 0;
881 dstate.ctx_committed = 0;
882 dstate.ctx_positions.clear();
883 dstate.seq_len = 0;
884 dstate.ctx_len = 0;
885 dstate.prefill_done = false;
886 dstate.last_num_drafted = 0;
887 dstate.last_num_accepted = 0;
888 dstate.skip_next_decode_append = false;
889 Ok(())
890 }
891}
892
893/// ATLAS_NO_DFLASH_FP8_RT=1 restores the tile-GEMM propose path for A/B
894/// (strict `== "1"`, matching the sibling ATLAS_NO_* levers). OnceLock so
895/// the kernel choice is stable across CUDA-graph capture.
896pub(crate) fn fp8_rt_enabled() -> bool {
897 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
898 *ON.get_or_init(|| std::env::var("ATLAS_NO_DFLASH_FP8_RT").as_deref() != Ok("1"))
899}
900
901/// The DFlash context-window bound, in tokens: the most recent target
902/// positions the drafter is allowed to accumulate and attend to.
903///
904/// SINGLE DEFINITION on purpose. Two buffers are sized from it — the
905/// per-sequence ctx accumulator here, and the model-level whole-prompt hidden
906/// capture (`impl_a1`) that feeds `prefill_drafter` — and the drafter cannot
907/// use more prompt than it can store, so capturing past this bound is dead
908/// memory. Letting the two drift is exactly the ceiling-vs-need bug this
909/// bound exists to close.
910///
911/// `ATLAS_DFLASH_CTX_CAP=<tokens>`; `0` disables the cap entirely.
912pub fn dflash_ctx_cap() -> usize {
913 std::env::var("ATLAS_DFLASH_CTX_CAP")
914 .ok()
915 .and_then(|v| v.parse::<usize>().ok())
916 .unwrap_or(16384)
917}
918
919impl BlockDiffusionDraftHead {
920 /// Allocate proposer state with the ctx accumulator sized to the smallest
921 /// of: this request's token budget, the ATLAS_DFLASH_CTX_CAP window, and
922 /// `--max-seq-len`.
923 fn alloc_state_windowed(
924 &self,
925 gpu: &dyn GpuBackend,
926 budget_tokens: usize,
927 ) -> Result<Box<dyn ProposerState>> {
928 // Per-seq ctx accumulator: `[max_seq_len, 5 * target_hidden] BF16`.
929 // Sized once, re-used across the seq's lifetime; reset on
930 // `free_state`. At max_seq_len=16384 and 5×2048 BF16: 320 MB per
931 // seq — tolerable on a single Spark with max_batch_size=1; for
932 // higher batch we may want to reduce to a smaller working window.
933 let bf16 = 2usize;
934 let ctx_slot_bytes = self.target_layer_ids.len() * self.target_hidden_size * bf16;
935 // WORKING WINDOW, not max_seq_len. This buffer is per SEQUENCE and
936 // scales with the context ceiling: at 128K x 5 layers x 5120 BF16 it
937 // is 6.7 GB EACH, so 8 concurrent sequences ask for 53.7 GB — lazily,
938 // as streams arrive, which is why it OOMs a long way past a clean
939 // boot rather than at startup. Capping the window bounds it to
940 // `cap * ctx_slot_bytes` per sequence (16K -> 839 MB, 8 seqs -> 6.7 GB).
941 //
942 // Correctness: `commit_ctx` already slides a watermark when the
943 // accumulator fills, keeping the NEWEST half and re-stamping
944 // ctx_positions, so a smaller window is an already-exercised path —
945 // the drafter conditions on recent context instead of the whole
946 // history. Raise with ATLAS_DFLASH_CTX_CAP=<tokens> (0 = uncapped,
947 // the pre-cap behaviour) if you have the memory and want the drafter
948 // to see further back.
949 let cap = dflash_ctx_cap();
950 let ceiling = if cap == 0 {
951 self.max_seq_len
952 } else {
953 self.max_seq_len.min(cap)
954 };
955 // The request's own reach (prompt + max_tokens) when the caller knows
956 // it: a 2K-token turn has no use for a 16K accumulator, and this
957 // buffer is paid PER SEQUENCE. `+ gamma + 1` covers the draft block
958 // and bonus slot the ctx accumulates past the last emitted token.
959 let window = ceiling.min(budget_tokens.saturating_add(self.gamma + 1));
960 if ceiling < self.max_seq_len {
961 static LOGGED: std::sync::atomic::AtomicBool =
962 std::sync::atomic::AtomicBool::new(false);
963 if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
964 tracing::info!(
965 "DFlash ctx window capped to {} of --max-seq-len {} ({} MB/seq instead of \
966 {} MB): the accumulator is PER SEQUENCE, so the uncapped size is what \
967 OOMs a high-concurrency long-context serve. Override with \
968 ATLAS_DFLASH_CTX_CAP=<tokens> (0 = uncapped).",
969 ceiling,
970 self.max_seq_len,
971 ceiling * ctx_slot_bytes / (1024 * 1024),
972 self.max_seq_len * ctx_slot_bytes / (1024 * 1024),
973 );
974 }
975 }
976 let total = window * ctx_slot_bytes;
977 let ctx_hidden_acc = gpu.alloc(total)?;
978 // Initialize to zero so stale data doesn't leak between sequences.
979 gpu.memset(ctx_hidden_acc, 0, total)?;
980 Ok(Box::new(DflashProposerState {
981 block_table: Vec::with_capacity(64),
982 seq_len: 0,
983 last_num_drafted: 0,
984 prefill_done: false,
985 ctx_hidden_acc,
986 ctx_len: 0,
987 last_num_accepted: 0,
988 skip_next_decode_append: false,
989 max_ctx_len: window,
990 ctx_slot_bytes,
991 // Phase 2 Option B: lazily allocated on first propose when
992 // ATLAS_DFLASH_OPTION_B=1. None until then to keep alloc_state
993 // cheap for sequences that never use Option B.
994 block_table_dev: None,
995 ctx_count_drafter: 0,
996 max_ctx_count_drafter: 0,
997 ctx_committed: 0,
998 ctx_positions: Vec::new(),
999 }))
1000 }
1001}