spark_model/layers/dflash_head/from_weights.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! DFlash drafter constructor + target-config validation.
4//!
5//! Split out of `dflash_head.rs` for file-size budget. Contains
6//! [`BlockDiffusionDraftHead::from_weights`] (kernel resolution + KV
7//! cache setup) and [`BlockDiffusionDraftHead::validate_against_target`].
8
9use anyhow::Result;
10use parking_lot::Mutex;
11use spark_runtime::gpu::{DevicePtr, GpuBackend};
12use spark_runtime::kv_cache::{KvCacheConfig, KvCacheDtype, PagedKvCache};
13
14use super::{
15 BlockDiffusionDraftHead, DflashKernels, DflashLayer, DflashQuantization, DflashScratch,
16};
17use crate::weight_loader::DflashWeights;
18
19impl BlockDiffusionDraftHead {
20 pub fn from_weights(
21 weights: DflashWeights,
22 embed_tokens_shared: DevicePtr,
23 lm_head_shared: DevicePtr,
24 lm_head_nvfp4: Option<crate::weight_map::QuantizedWeight>,
25 lm_head_native_fp8: Option<(crate::weight_map::Fp8DenseWeight, usize)>,
26 target_hidden_size: usize,
27 gamma: Option<usize>,
28 window_size: Option<usize>,
29 gpu: &dyn GpuBackend,
30 max_seq_len: usize,
31 // Widest cross-sequence batch the drafter must serve in ONE forward.
32 // The gamma-sized scratch below is allocated in per-sequence BANDS of
33 // gamma rows so a batched propose can stack n sequences; nb == 1 keeps
34 // the original single-band sizes byte for byte. #817 also sizes the
35 // drafter KV pool from it, so one sequence can no longer take every
36 // block and strand the rest on serial decode.
37 max_batch_size: usize,
38 ) -> Result<Self> {
39 let nb = max_batch_size.max(1);
40 // Drafter's `fc` is `[draft_hidden, len(target_layer_ids) * target_hidden]`.
41 // We rely on the drafter config's `hidden_size` and the parsed
42 // `target_layer_ids` to derive the expected target_hidden, then
43 // validate it matches what the caller provided.
44 let target_layer_ids = weights
45 .config
46 .dflash_config
47 .as_ref()
48 .map(|c| c.target_layer_ids.clone())
49 .unwrap_or_default();
50 let mask_token_id = weights
51 .config
52 .dflash_config
53 .as_ref()
54 .map(|c| c.mask_token_id)
55 .unwrap_or(0);
56
57 if target_layer_ids.is_empty() {
58 anyhow::bail!(
59 "DFlash drafter config.json has no `dflash_config.target_layer_ids` — \
60 cannot determine which target hidden states to capture"
61 );
62 }
63
64 let _ = target_hidden_size;
65
66 let num_layers = weights.config.num_hidden_layers;
67 let hidden_size = weights.config.hidden_size;
68 let intermediate_size = weights.config.intermediate_size;
69 let num_q_heads = weights.config.num_attention_heads;
70 let num_kv_heads = weights.config.num_key_value_heads;
71 let head_dim = weights.config.head_dim;
72 let vocab_size = weights.config.vocab_size;
73 // The HEAD's gamma is the SSOT the serve layer reads back, so the
74 // drafter's checkpoint must drive the default here, `block_size`
75 // alone is the top-level field, whose serde default of 16 silently
76 // shadows a DFlash2 checkpoint that states 8 in `dflash_config`.
77 //
78 // Default = `default_dflash_gamma` (trained block + 2, clamped), the
79 // SSOT the serve preflights size their pools and reserves from too.
80 // Resolve it HERE ONLY through that helper: an independent spelling
81 // here is how the head came to run gamma 10 against intermediates
82 // reserved for K=9. An explicit --dflash-gamma still wins.
83 let gamma_val = gamma.unwrap_or_else(|| {
84 crate::layers::qwen3_ssm::default_dflash_gamma(weights.config.effective_block_size())
85 });
86
87 // Allocate the drafter's paged FP8 KV cache. One multi-layer cache,
88 // sized for `max_seq_len + γ + 1` positions (prompt + γ drafts +
89 // 1 bonus). Block size 16 matches the rest of Atlas.
90 let block_size = 16;
91 let kv_config = KvCacheConfig {
92 block_size,
93 num_kv_heads,
94 head_dim,
95 num_layers,
96 // Phase 2 (Option B): flip drafter KV cache to BF16. The BF16
97 // paged-attn dispatcher `prefill_attention_paged_dflash` reads
98 // contiguous BF16 K/V from the layer pool; FP8 here would force
99 // either an FP8 attn kernel (acceptance collapses on SM12.x —
100 // see dflash_head.rs:82–86) or a dtype-mismatched read. BF16
101 // first to land correctness; FP8 KV is a follow-up once the
102 // architecture is right.
103 dtype: KvCacheDtype::Bf16,
104 layer_dtypes: vec![],
105 layer_dims: vec![],
106 cache_blocks_per_seq: None,
107 };
108 // Concurrency sizing (2026-08-29, C=16 probe): this pool was sized
109 // for exactly ONE sequence — pool == per-seq demand, so the first
110 // stream to propose took every block and streams 2..N fell back to
111 // serial decode ("paged KV cache exhausted at block 0/257" flood,
112 // measured live at C=16). Per-seq demand mirrors propose.rs's lazy
113 // alloc: ceil((max_ctx + γ + 1)/block_size). Multiply by
114 // max_batch_size so every admitted sequence can speculate; +1 spare.
115 // provenance-id: 526f6e616c6420522e205374657369616b
116 let per_seq_blocks = (max_seq_len + gamma_val + 1).div_ceil(block_size);
117 let num_blocks = per_seq_blocks * max_batch_size.max(1) + 1;
118 tracing::info!(
119 "DFlash drafter paged KV pool: {} blocks ({} per-seq x max_batch_size {})",
120 num_blocks,
121 per_seq_blocks,
122 max_batch_size.max(1)
123 );
124 let kv_cache = PagedKvCache::new(kv_config, num_blocks, gpu)?;
125
126 // Resolve kernel handles. All BF16 paths since drafter weights are
127 // BF16 (DflashQuantization::Bf16); FP8 cache uses the FP8 reshape +
128 // FP8-aware paged-attention kernel. Module/function names verified
129 // against existing Atlas resolutions in `qwen3_attention/mod.rs` and
130 // `mtp_head.rs` plus the `extern "C" __global__` declarations under
131 // `kernels/gb10/common/`.
132 let kernels = DflashKernels {
133 // DFlash drafter uses HF's vanilla RMSNorm convention
134 // (`out = x * w / RMS(x)`), NOT Atlas's default offset-from-1
135 // form (`out = x * (1 + w) / RMS(x)`). Atlas's standard
136 // `rms_norm` kernel includes the `+1` for Qwen3-Next-style
137 // checkpoints; we must use `rms_norm_vanilla` for the drafter
138 // to match the drafter's HF-trained weights exactly.
139 rms_norm: gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?,
140 // `rms_norm_residual` lands the post-attn / post-MLP add+norm in
141 // a single launch. Atlas exposes this as a separate kernel — see
142 // `mtp_head.rs:469` for the established lookup.
143 residual_rms_norm: gpu
144 .kernel("norm", "rms_norm_residual")
145 .or_else(|_| gpu.kernel("residual_add", "bf16_residual_add"))?,
146 dense_gemv: gpu.kernel("gemv", "dense_gemv_bf16")?,
147 dense_gemm: gpu.kernel("gemm", "dense_gemm_bf16")?,
148 w4a16_gemm: super::super::try_kernel(gpu, "w4a16", "w4a16_gemm"),
149 dense_gemm_pipelined: gpu.kernel("gemm", "dense_gemm_bf16_pipelined")?,
150 // Qwen3.6-DFlash uses yarn RoPE — confirmed in the drafter
151 // `config.json:rope_scaling.rope_type="yarn"`. Atlas's yarn
152 // kernel is `rope::rope_forward_yarn`.
153 rope_qwen3: gpu.kernel("rope", "rope_forward_yarn")?,
154 // FP8 KV cache writeback. Module name is the .cu stem
155 // `reshape_and_cache`, function is `reshape_and_cache_flash_fp8`
156 // (qwen3_attention/mod.rs:377-378 uses the same path).
157 reshape_cache_fp8: gpu.kernel("reshape_and_cache", "reshape_and_cache_flash_fp8")?,
158 // BF16 KV writeback — same module as the FP8 variant, different
159 // function symbol. Used by precompute_ctx_kv + the per-layer
160 // γ-block cache write that feeds prefill_attention_paged_dflash.
161 reshape_cache_bf16: gpu.kernel("reshape_and_cache", "reshape_and_cache_flash")?,
162 // The Phase-2 γ-block kernel — same module as the existing
163 // FP8 paged-prefill kernel (we just pass `causal_mask_enabled=0`
164 // via a different dispatcher).
165 prefill_attn_dflash_fp8: gpu
166 .kernel("prefill_paged_fp8", "inferspark_prefill_paged_fp8")?,
167 // Phase 2 (Option B) BF16 γ-block paged-attention. Same kernel
168 // module as the target's BF16 prefill (`prefill_paged`); the
169 // Rust dispatcher `ops::prefill_attention_paged_dflash` passes
170 // `causal_mask_enabled=0` for bidirectional γ-block attention.
171 prefill_attn_dflash_bf16: gpu.kernel("prefill_paged", "inferspark_prefill_paged")?,
172 // Phase 5 (CUDA graph): indirect-args BF16 paged dispatcher. Same
173 // kernel as `prefill_attn_dflash_bf16` except `kv_len` and
174 // `q_offset` are read from device pointers at kernel entry, so the
175 // graph-captured launch can be replayed with new dynamic values
176 // without re-capture. See `inferspark_prefill_paged_indirect.cu`.
177 prefill_attn_dflash_bf16_indirect: gpu.kernel(
178 "prefill_paged_indirect",
179 "inferspark_prefill_paged_indirect",
180 )?,
181 silu_mul: gpu.kernel("moe_silu_mul", "moe_silu_mul")?,
182 residual_add: gpu.kernel("residual_add", "bf16_residual_add")?,
183 argmax: gpu.kernel("argmax", "argmax_bf16")?,
184 batched_embed: gpu.kernel("embed_from_argmax", "batched_embed")?,
185 // Phase 2 Option B: slot_mapping builder. Same kernel the
186 // target model uses for its KV cache writeback (see
187 // crates/spark-model/src/model/impl_a1.rs:92).
188 fill_slots: gpu.kernel("metadata_fill", "fill_slots_from_block_table")?,
189 // Drafter has head_dim=128, but the target's
190 // `inferspark_prefill` is compiled with HDIM=256. Using that
191 // kernel produces corrupted attn_out for the drafter (kernel
192 // reads 256 elements per head when only 128 are valid →
193 // garbage in the back half of SMEM tiles → per-head sign-flip
194 // pattern across q-heads). The HDIM=128 specialization
195 // `inferspark_prefill_h128.cu` lives in the shared kernel
196 // dir (`kernels/<hw>/common/`) so every target gets it.
197 prefill_attn: gpu
198 .kernel("inferspark_prefill_h128", "inferspark_prefill_h128")
199 .map_err(|e| {
200 anyhow::anyhow!(
201 "{e}\n\nDFlash needs the HDIM=128 prefill kernel \
202 (`inferspark_prefill_h128`) compiled for this target. \
203 The kernel source lives at \
204 `kernels/<hw>/common/inferspark_prefill_h128.cu`. \
205 If you've added a new hardware target, copy the \
206 .cu file there."
207 )
208 })?,
209 // Phase G — BF16→FP8 weight quant kernel. Already in tree via
210 // dense_gemv_fp8w.cu:36 under namespace "gemv_fp8w". Used at
211 // load time only.
212 quantize_bf16_to_fp8: gpu.kernel("gemv_fp8w", "quantize_bf16_to_fp8")?,
213 // Phase G — Row-scaled BF16 × FP8 → BF16 GEMM. Atlas custom
214 // kernel `fp8_gemm_t_row_scaled` appended to w4a16_gemm.cu
215 // for Phase G (module namespace "w4a16").
216 // try_kernel: absent on targets whose w4a16 module predates
217 // Phase G — the FP8 drafter path is then skipped at the
218 // AVAROK_DFLASH_DRAFTER_FP8 gate below (BF16 fallback).
219 // 2026-09-03: `_p4` = same kernel with a 4-stage cp.async ring
220 // (byte-identical output; 2-stage ran at ~1/3 of DRAM peak at
221 // M=64). `AVAROK_DFLASH_FP8_GEMM_P2=1` pins the 2-stage original.
222 fp8_gemm_n128_row_scaled: {
223 let pin_p2 =
224 std::env::var("AVAROK_DFLASH_FP8_GEMM_P2").ok().as_deref() == Some("1");
225 // Preference: `_k64` (64 B per weight row per K step) ->
226 // `_p4` (4-stage ring) -> original. All three byte-identical.
227 let pin_p4 =
228 std::env::var("AVAROK_DFLASH_FP8_GEMM_P4").ok().as_deref() == Some("1");
229 let mut h = spark_runtime::gpu::KernelHandle(0);
230 if !pin_p2 && !pin_p4 {
231 h = crate::layers::try_kernel(gpu, "w4a16", "fp8_gemm_t_row_scaled_k64");
232 if h.0 != 0 {
233 tracing::info!(
234 "DFlash drafter FP8 GEMM: fp8_gemm_t_row_scaled_k64 (deep-K)"
235 );
236 }
237 }
238 if h.0 == 0 && !pin_p2 {
239 h = crate::layers::try_kernel(gpu, "w4a16", "fp8_gemm_t_row_scaled_p4");
240 if h.0 != 0 {
241 tracing::info!(
242 "DFlash drafter FP8 GEMM: fp8_gemm_t_row_scaled_p4 (4-stage ring)"
243 );
244 }
245 }
246 if h.0 == 0 {
247 h = crate::layers::try_kernel(gpu, "w4a16", "fp8_gemm_t_row_scaled");
248 }
249 h
250 },
251 // Phase G — Row-scaled BF16 × FP8 → BF16 GEMV (M=1). Used
252 // by the lm_head GEMM swap in a γ-loop, since the
253 // fp8_gemm_n128 GEMM kernel wastes 75% of its M_TILE at
254 // M=γ=16 against vocab=248320.
255 dense_gemv_fp8w: gpu.kernel("gemv_fp8w", "dense_gemv_fp8w")?,
256 // Phase G — Small-M (M≤16) row-scaled FP8 GEMM for lm_head.
257 // Single warp per CTA, no M_TILE waste. Custom kernel in
258 // w4a16_gemm.cu, module namespace "w4a16".
259 fp8_gemm_n128_row_scaled_m16: crate::layers::try_kernel(
260 gpu,
261 "w4a16",
262 "fp8_gemm_t_row_scaled_m16",
263 ),
264 // Register-tiled M<=8 FP8 GEMV (fp8_gemv_rt.cu, common) —
265 // preferred over both tile GEMMs above for the M=γ propose
266 // GEMMs; try_kernel so targets without the module fall back.
267 fp8_gemv_rt2: crate::layers::try_kernel(
268 gpu,
269 "fp8_gemv_rt",
270 "fp8_gemv_rowscale_batch8_rt2",
271 ),
272 // MAX_M=16 sibling for the γ>8 propose window (2026-08-29).
273 fp8_gemv_rt2_16: crate::layers::try_kernel(
274 gpu,
275 "fp8_gemv_rt",
276 "fp8_gemv_rowscale_batch16_rt2",
277 ),
278 // DFlash2 kernels (kernels/gb10/common/dflash2.cu). try_kernel:
279 // absent on stale kernel builds — DFlash2 then refuses to arm
280 // rather than failing DFlash1/DSpark drafters at load.
281 dflash2_conv2: crate::layers::try_kernel(gpu, "dflash2", "dflash2_conv2"),
282 dflash2_topk16: crate::layers::try_kernel(gpu, "dflash2", "dflash2_topk16"),
283 dflash2_selector_walk: crate::layers::try_kernel(
284 gpu,
285 "dflash2",
286 "dflash2_selector_walk",
287 ),
288 };
289
290 // Per-step scratch buffers. BF16 = 2 bytes/element.
291 //
292 // Sized for `n_attn_slots = ctx_window + γ` rows in the attention
293 // path. The first `ctx_window` slots hold projected target ctx
294 // (K/V only — Q is zero-padded so its attention output is
295 // discarded). The next γ slots hold the noise tokens. lm_head +
296 // logits + argmax tail still operates on γ rows (offset past ctx).
297 let bf16 = 2usize;
298 let g = gamma_val;
299 // Phase 2.5n: ctx_window controls how many captured target positions
300 // the drafter attends to per step. The drafter was trained over the
301 // FULL captured prefix (paper §A.1), but capping at γ=16 cripples it
302 // on prompts past a tiny window — Atlas's 6-10% acceptance vs the
303 // paper's 70% is dominated by this cap. Default raised 512 → 4096
304 // (2026-07-08): long generations (MinHeap ~2.6k tok) blow past 512
305 // captured rows → truncated prefix → accept collapse + droop.
306 // AVAROK_DFLASH_CTX_WINDOW overrides at construction time.
307 //
308 // Memory cost: attention-path scratch scales linearly with
309 // `n_attn = γ + cw`. At cw=4096: stream/norm/acc ≈ 16.8 MB each;
310 // mlp_intermediate/mlp_up = 4112 × 6144 × 2 ≈ 50.5 MB each (must
311 // stay n_attn: contiguous path runs MLP over all rows, and
312 // precompute_ctx_kv borrows mlp_intermediate as all_k_stage
313 // [L×n×kv_dim ≈ 21 MB]); fused_kv_out ≈ 42 MB. logits is γ-rows
314 // only (see alloc below). Total scratch ≈ 250 MB per head.
315 let ctx_window: usize = std::env::var("AVAROK_DFLASH_CTX_WINDOW")
316 .ok()
317 .and_then(|s| s.parse().ok())
318 .unwrap_or(4096);
319 tracing::info!(
320 "DFlash ctx_window = {} (set AVAROK_DFLASH_CTX_WINDOW to override; \
321 drafter trained on full captured prefix — larger is better, \
322 scratch grows linearly)",
323 ctx_window
324 );
325 let n_attn = g + ctx_window; // total attention slots
326 // Rows the scratch must hold: the legacy ctx+gamma window, or nb
327 // bands of gamma for a batched propose, whichever is larger.
328 let rows_max = n_attn.max(nb * gamma_val);
329 let q_dim = num_q_heads * head_dim;
330 let kv_dim = num_kv_heads * head_dim;
331 let scratch = DflashScratch {
332 stream_buf: gpu.alloc(rows_max * hidden_size * bf16)?,
333 norm_buf: gpu.alloc(rows_max * hidden_size * bf16)?,
334 q_buf: gpu.alloc(rows_max * q_dim * bf16)?,
335 k_buf: gpu.alloc(rows_max * kv_dim * bf16)?,
336 v_buf: gpu.alloc(rows_max * kv_dim * bf16)?,
337 attn_out: gpu.alloc(rows_max * q_dim * bf16)?,
338 mlp_intermediate: gpu.alloc(rows_max * intermediate_size * bf16)?,
339 mlp_up: gpu.alloc(rows_max * intermediate_size * bf16)?,
340 stream_acc: gpu.alloc(rows_max * hidden_size * bf16)?,
341 fc_proj: gpu.alloc(ctx_window * hidden_size * bf16)?,
342 // Phase 2 (Option B) precompute scratch. Worst-case the
343 // first propose runs precompute over the whole captured
344 // prefix up to `ctx_window`, so size for that. Per row:
345 // `L * 2 * kv_dim * bf16` bytes. At L=5, kv_dim=512,
346 // ctx_window=512: 5·2·512·512·2 = 5.24 MB.
347 fused_kv_out: gpu
348 .alloc(ctx_window * num_layers * 2 * num_kv_heads * head_dim * bf16)?,
349 // i64 slot mapping for reshape_and_cache (kernel takes
350 // `long long*`). One entry per new ctx row.
351 slot_mapping_dev: gpu.alloc(ctx_window * 8)?,
352 precompute_in: gpu.alloc(
353 super::PRECOMPUTE_BATCH_ROWS * target_layer_ids.len() * target_hidden_size * bf16,
354 )?,
355 // 12 bytes of device memory holding the per-call triple
356 // `[u32 kv_len, u32 q_offset, u32 q_rope_pos]` that the indirect
357 // paged-attention kernel reads at entry. Host writes via H2D
358 // BEFORE entering the captured region.
359 option_b_indirect_args_dev: gpu.alloc(nb * 12)?,
360 // Phase E.2: pinned host buffer + event for the per-propose
361 // drafter D2H. Pinned memory lets cuMemcpyDtoHAsync issue a
362 // true async DMA on the caller's stream (vs. the synchronous
363 // staging fallback the driver picks for pageable destinations).
364 // The event lets us wait on the *copy*, not the whole stream,
365 // so target-model verify work issued on the same stream can
366 // proceed in parallel.
367 draft_tokens_host_pinned: std::sync::atomic::AtomicPtr::new(
368 gpu.alloc_host_pinned(nb * gamma_val * 4)?,
369 ),
370 draft_tokens_event: gpu.create_event()?,
371 // γ rows only — NOT n_attn. The lm_head GEMM writes M=γ rows,
372 // argmax + BLOCK_DUMP read rows 0..γ, and no path indexes logits
373 // by ctx offset. Sizing at n_attn×vocab would cost 2.04 GB at
374 // cw=4096 for rows nothing ever touches (γ rows ≈ 8.4 MB).
375 logits: gpu.alloc(nb * g * vocab_size * bf16)?,
376 draft_tokens_dev: gpu.alloc(rows_max * 4)?,
377 position_ids: gpu.alloc(rows_max * 4)?,
378 // DSpark Markov scratch. Only allocated when the drafter config
379 // declares a Markov head; `DevicePtr(0)` otherwise so the plain
380 // DFlash path costs nothing.
381 markov_embed: if weights.config.markov_rank > 0 {
382 gpu.alloc(weights.config.markov_rank * bf16)?
383 } else {
384 DevicePtr(0)
385 },
386 markov_bias: if weights.config.markov_rank > 0 {
387 gpu.alloc(vocab_size * bf16)?
388 } else {
389 DevicePtr(0)
390 },
391 conf_out: if weights.confidence_proj.is_some() {
392 gpu.alloc(g * bf16)?
393 } else {
394 DevicePtr(0)
395 },
396 // DFlash2 scratch — only when the checkpoint ships the selector
397 // (conv-only checkpoints are not a thing in the DFlash2 lineage).
398 conv_dyn: if weights.selector_pred.is_some() {
399 let cfg = weights.config.dflash_config.as_ref();
400 let ksz = cfg.map(|c| c.conv_kernel_size).unwrap_or(0).max(1);
401 let gsz = cfg.map(|c| c.conv_group_size).unwrap_or(0).max(1);
402 gpu.alloc(nb * g * 2 * ksz * (hidden_size / gsz) * bf16)?
403 } else {
404 DevicePtr(0)
405 },
406 conv_tmp: if weights.selector_pred.is_some() {
407 gpu.alloc(nb * g * hidden_size * bf16)?
408 } else {
409 DevicePtr(0)
410 },
411 sel_vals: if weights.selector_pred.is_some() {
412 gpu.alloc(nb * g * 16 * 4)?
413 } else {
414 DevicePtr(0)
415 },
416 sel_idx: if weights.selector_pred.is_some() {
417 gpu.alloc(nb * g * 16 * 4)?
418 } else {
419 DevicePtr(0)
420 },
421 sel_hproj: if weights.selector_pred.is_some() {
422 let rank = weights
423 .config
424 .dflash_config
425 .as_ref()
426 .map(|c| c.selector_rank)
427 .unwrap_or(256)
428 .max(1);
429 gpu.alloc(nb * g * rank * bf16)?
430 } else {
431 DevicePtr(0)
432 },
433 };
434
435 // Pre-compute inv_freq table for drafter RoPE.
436 //
437 // The drafter's `config.json:rope_scaling` is the source of truth:
438 // * `None` ⇒ plain RoPE (`inv_freq[j] = 1 / θ^(2j/dim)`). The
439 // v2 2026-04-27 Qwen3.6-DFlash drafter ships `rope_scaling: null`.
440 // * `Some(yarn)` ⇒ YaRN-scaled table (Mistral-Small-4 lineage).
441 //
442 // Historical bug (Friday/Atlas 2026-05): this loader unconditionally
443 // applied YaRN with factor=64 / orig_max_pos=4096 hardcoded, which
444 // mis-scaled every low-frequency RoPE pair (pairs 0..11 divided by
445 // 64, pairs 11..26 ramped). Result: drafter Q/K rotations landed in
446 // the wrong angular basis at every layer → 0% draft acceptance. Now
447 // we read the drafter's own scaling block instead of guessing.
448 let rope_theta = weights.config.rope_theta;
449 let rotary_dim = head_dim; // Qwen3.6-DFlash applies rope to full head_dim
450 let dim_f = rotary_dim as f32;
451 let n_pairs = rotary_dim / 2;
452 let mut inv_freq_table = vec![0.0f32; n_pairs];
453
454 // Default = plain RoPE. Overwritten in the YaRN arm below.
455 for j in 0..n_pairs {
456 inv_freq_table[j] = 1.0 / rope_theta.powf((2 * j) as f32 / dim_f);
457 }
458
459 let rope_kind: &str;
460 if let Some(scaling) = weights.config.rope_scaling.as_ref() {
461 match scaling.rope_type.as_deref() {
462 Some("yarn") => {
463 let factor = scaling.factor.unwrap_or(1.0);
464 let beta_fast = scaling.beta_fast.unwrap_or(32.0);
465 let beta_slow = scaling.beta_slow.unwrap_or(1.0);
466 let orig_max_pos = scaling.original_max_position_embeddings.unwrap_or(4096.0);
467 let find_correction_dim = |num_rot: f32| -> f32 {
468 (dim_f * (orig_max_pos / (num_rot * 2.0 * std::f32::consts::PI)).ln())
469 / (2.0 * rope_theta.ln())
470 };
471 let low = find_correction_dim(beta_fast).floor().max(0.0);
472 let high = find_correction_dim(beta_slow)
473 .ceil()
474 .min((rotary_dim - 1) as f32);
475 let ramp_denom = if (high - low).abs() < 1e-6 {
476 high - low + 0.001
477 } else {
478 high - low
479 };
480 for j in 0..n_pairs {
481 let pos_freq = rope_theta.powf((2 * j) as f32 / dim_f);
482 let inv_freq_extrap = 1.0 / pos_freq;
483 let inv_freq_interp = 1.0 / (factor * pos_freq);
484 let ramp = ((j as f32 - low) / ramp_denom).clamp(0.0, 1.0);
485 let extrap_factor = 1.0 - ramp;
486 inv_freq_table[j] = inv_freq_interp * (1.0 - extrap_factor)
487 + inv_freq_extrap * extrap_factor;
488 }
489 tracing::info!(
490 "DFlash RoPE = YaRN: theta={rope_theta}, factor={factor}, \
491 beta_fast={beta_fast}, beta_slow={beta_slow}, \
492 max_pos={orig_max_pos}, low_dim={low:.1}, high_dim={high:.1}",
493 );
494 rope_kind = "yarn";
495 }
496 Some(other) => {
497 tracing::warn!(
498 "DFlash drafter config has rope_scaling.rope_type={other:?} which Atlas \
499 doesn't recognise — falling back to plain RoPE (theta={rope_theta})."
500 );
501 rope_kind = "plain (unknown rope_type)";
502 }
503 None => {
504 tracing::warn!(
505 "DFlash drafter config has rope_scaling without rope_type — \
506 falling back to plain RoPE (theta={rope_theta})."
507 );
508 rope_kind = "plain (no rope_type)";
509 }
510 }
511 } else {
512 tracing::info!(
513 "DFlash RoPE = plain (no rope_scaling in drafter config), theta={rope_theta}, \
514 {n_pairs} pairs",
515 );
516 rope_kind = "plain";
517 }
518 let _ = rope_kind; // logged above, retained for future debug surfaces
519
520 let inv_freq_bytes: Vec<u8> = inv_freq_table
521 .iter()
522 .flat_map(|v| v.to_le_bytes())
523 .collect();
524 let yarn_inv_freq = gpu.alloc(inv_freq_bytes.len())?;
525 gpu.copy_h2d(&inv_freq_bytes, yarn_inv_freq)?;
526
527 // ── Phase 2 (Option B) fused KV weight build ──────────────
528 //
529 // Concatenate every drafter layer's `k_proj.weight` and
530 // `v_proj.weight` into a single `[L * 2 * kv_dim, h]` BF16
531 // tensor laid out as `[K_0, V_0, K_1, V_1, …, K_{L-1}, V_{L-1}]`.
532 // Lets `precompute_ctx_kv` derive all layers' ctx K/V in one
533 // fused `dense_gemm` instead of `2 * L` separate calls per
534 // propose. Layout matches vLLM's `_fused_kv_weight` in
535 // `qwen3_dflash.py:301-303`.
536 //
537 // Memory layout reasoning: per-layer K weight is `[kv_dim, h]`
538 // BF16 = `kv_dim * h * 2` bytes; V weight is the same shape.
539 // Fused buffer total = `L * 2 * kv_dim * h * 2` bytes.
540 // At L=5, kv_dim=512, h=2048: 5·2·512·2048·2 = 20.97 MB.
541 //
542 // We rely on the existing per-layer `DenseWeight.weight`
543 // device pointers — no host roundtrip, just GPU `copy_d2d`.
544 let kv_dim_bytes = num_kv_heads * head_dim * hidden_size * bf16; // K or V per layer
545 let fused_total_bytes = num_layers * 2 * kv_dim_bytes;
546 let fused_kv_weight = gpu.alloc(fused_total_bytes)?;
547 for (l, layer) in weights.layers.iter().enumerate() {
548 let layer_base = l * 2 * kv_dim_bytes;
549 // K slot for layer l.
550 gpu.copy_d2d(
551 layer.k_proj.weight,
552 fused_kv_weight.offset(layer_base),
553 kv_dim_bytes,
554 )?;
555 // V slot for layer l (immediately after K).
556 gpu.copy_d2d(
557 layer.v_proj.weight,
558 fused_kv_weight.offset(layer_base + kv_dim_bytes),
559 kv_dim_bytes,
560 )?;
561 }
562 tracing::info!(
563 "DFlash fused_kv_weight: {} bytes ({} layers × 2 × kv_dim × h × bf16), \
564 layout [K0,V0,K1,V1,…] to match vLLM precompute_and_store_context_kv",
565 fused_total_bytes,
566 num_layers,
567 );
568
569 let mut head = Self {
570 // ★ The ONE environment read for the drafter's diagnostic levers.
571 levers: super::levers::DFlashLevers::from_env(),
572 num_layers,
573 hidden_size,
574 intermediate_size,
575 num_q_heads,
576 num_kv_heads,
577 head_dim,
578 vocab_size,
579 draft_vocab_size: weights.config.draft_vocab_size.unwrap_or(vocab_size),
580 gamma: gamma_val,
581 block_gamma: std::sync::atomic::AtomicUsize::new(gamma_val),
582 max_batch: nb,
583 mask_token_id,
584 window_size,
585 target_layer_ids,
586 target_hidden_size,
587
588 embed_tokens_shared,
589 lm_head_shared,
590 lm_head_nvfp4,
591 lm_head_shared_fp8: None,
592 hidden_norm: weights.hidden_norm,
593 norm: weights.norm,
594 fc: weights.fc,
595 draft_id_to_target_id: None,
596 layers: weights
597 .layers
598 .into_iter()
599 .map(|l| DflashLayer {
600 input_layernorm: l.input_layernorm,
601 post_attention_layernorm: l.post_attention_layernorm,
602 q_proj: l.q_proj,
603 k_proj: l.k_proj,
604 v_proj: l.v_proj,
605 o_proj: l.o_proj,
606 q_norm: l.q_norm,
607 k_norm: l.k_norm,
608 gate_proj: l.gate_proj,
609 up_proj: l.up_proj,
610 down_proj: l.down_proj,
611 // Phase G — populated below if AVAROK_DFLASH_DRAFTER_FP8=1.
612 q_proj_fp8: None,
613 k_proj_fp8: None,
614 v_proj_fp8: None,
615 o_proj_fp8: None,
616 gate_proj_fp8: None,
617 up_proj_fp8: None,
618 down_proj_fp8: None,
619 attention_conv_base: l.attention_conv_base,
620 attention_conv_proj: l.attention_conv_proj,
621 mlp_conv_base: l.mlp_conv_base,
622 mlp_conv_proj: l.mlp_conv_proj,
623 })
624 .collect(),
625 // Phase 2 stage 2: fused KV weight built above by copy_d2d
626 // from each layer's k_proj/v_proj. precompute_ctx_kv will
627 // GEMM against it in stage 3 once we wire the call site.
628 fused_kv_weight: Some(fused_kv_weight),
629 kv_cache: Mutex::new(kv_cache),
630 scratch,
631 kernels,
632 max_seq_len,
633 yarn_inv_freq,
634 rope_theta,
635 rotary_dim,
636 rms_norm_eps: 1e-6,
637 ctx_window,
638 // Phase F: per-subgraph graph state — empty until the first
639 // capture pass lands. Layout: [pre_0, post_0, ..., tail].
640 propose_graphs: parking_lot::Mutex::new(super::ProposeGraphs::default()),
641 suppress_graphs: std::sync::atomic::AtomicBool::new(false),
642 // main added this counter after this line was cut; the graph path
643 // reads it to decide when the eager warm-up is done, so it is a
644 // required field rather than a nicety.
645 propose_warmup_count: std::sync::atomic::AtomicUsize::new(0),
646 quant: DflashQuantization::Bf16,
647 // DSpark heads. `markov_rank` is zeroed when the tensors are
648 // absent so the runtime gate is a single field check.
649 markov_rank: if weights.markov_w1.is_some() {
650 weights.config.markov_rank
651 } else {
652 0
653 },
654 markov_w1: weights.markov_w1,
655 markov_w2: weights.markov_w2,
656 confidence_proj: weights.confidence_proj,
657 confidence_bias: weights.confidence_bias,
658 confidence_with_markov: weights.config.confidence_head_with_markov,
659 shifted_rows: weights
660 .config
661 .dflash_config
662 .as_ref()
663 .and_then(|c| c.projector_type.as_deref())
664 == Some("dspark"),
665 conv_kernel_size: weights
666 .config
667 .dflash_config
668 .as_ref()
669 .map(|c| c.conv_kernel_size)
670 .unwrap_or(0),
671 conv_group_size: weights
672 .config
673 .dflash_config
674 .as_ref()
675 .map(|c| c.conv_group_size)
676 .unwrap_or(0),
677 selector_rank: weights
678 .config
679 .dflash_config
680 .as_ref()
681 .map(|c| c.selector_rank)
682 .unwrap_or(0),
683 selector_top_k: weights
684 .config
685 .dflash_config
686 .as_ref()
687 .map(|c| c.selector_top_k)
688 .unwrap_or(0),
689 selector_pred: weights.selector_pred,
690 selector_succ: weights.selector_succ,
691 selector_hidden_proj: weights.selector_hidden_proj,
692 };
693 if head.selector_pred.is_some() {
694 tracing::info!(
695 "DFlash2 armed: conv k={} group={} selector rank={} top_k={} \
696 (kernels present: conv={} topk={} walk={})",
697 head.conv_kernel_size,
698 head.conv_group_size,
699 head.selector_rank,
700 head.selector_top_k,
701 head.kernels.dflash2_conv2.0 != 0,
702 head.kernels.dflash2_topk16.0 != 0,
703 head.kernels.dflash2_selector_walk.0 != 0,
704 );
705 }
706 if head.shifted_rows {
707 tracing::info!(
708 "DSpark drafter: SpecForge shifted row convention active \
709 (projector_type=dspark) — draft vector rotates right by 1"
710 );
711 }
712
713 tracing::info!(
714 "BlockDiffusionDraftHead loaded: {} layers, hidden={}, intermediate={}, \
715 GQA {}/{}, head_dim={}, γ={}, vocab={}, mask_token_id={}, target_layers={:?}",
716 head.num_layers,
717 head.hidden_size,
718 head.intermediate_size,
719 head.num_q_heads,
720 head.num_kv_heads,
721 head.head_dim,
722 head.gamma,
723 head.vocab_size,
724 head.mask_token_id,
725 head.target_layer_ids,
726 );
727
728 // Phase G — opt-in drafter MLP FP8. Quantize the seven dense-GEMM
729 // weights per layer (q/k/v/o/gate/up/down) BF16 → FP8 E4M3 with
730 // per-row f32 scales. One-shot at model load; runtime hot path
731 // consumes the Fp8DenseWeight via fp8_gemm_n128 in pre/post_attn
732 // (wired in G.3). Default OFF — bit-identical to F.2 baseline.
733 //
734 // Acceptance gate (G.4 design doc §16.7): bench must hold
735 // ≥43% accept (vs 44.9% BF16) AND ≥11.0 tok/s (vs 8.70). If hard
736 // fail, layer-by-layer ablation; skip layer 0 first.
737 // Default ON since the 54.5 record config (2026-08-19): FP8 drafter
738 // weights are the proven speed lane (accept gate held). `=0` reverts
739 // to the BF16 drafter path.
740 let fp8_requested = std::env::var("AVAROK_DFLASH_DRAFTER_FP8").ok().as_deref() != Some("0");
741 let fp8_kernels_present = head.kernels.fp8_gemm_n128_row_scaled.0 != 0
742 && head.kernels.fp8_gemm_n128_row_scaled_m16.0 != 0;
743 if fp8_requested && !fp8_kernels_present {
744 tracing::warn!(
745 "AVAROK_DFLASH_DRAFTER_FP8=1 but fp8_gemm_t_row_scaled(_m16) kernels are \
746 not in this target's w4a16 PTX module — staying on the BF16 drafter path. \
747 Port the Phase G kernels from kernels/gb10/qwen3.6-27b/nvfp4/w4a16_gemm.cu."
748 );
749 }
750 if fp8_requested && fp8_kernels_present {
751 tracing::info!(
752 "DFlash Phase G: quantizing drafter weights to FP8 E4M3 ({} layers × 7 GEMMs)",
753 head.num_layers
754 );
755 let stream = 0u64; // default stream — load-time, no concurrency
756 let q_dim_local = q_dim;
757 let kv_dim_local = kv_dim;
758 let h = head.hidden_size;
759 let inter = head.intermediate_size;
760 let quant_k = head.kernels.quantize_bf16_to_fp8;
761 for (layer_idx, layer) in head.layers.iter_mut().enumerate() {
762 // Q proj: [q_dim, h]
763 layer.q_proj_fp8 =
764 Some(
765 layer
766 .q_proj
767 .quantize_to_fp8(gpu, quant_k, q_dim_local, h, stream)?,
768 );
769 // K proj: [kv_dim, h]
770 layer.k_proj_fp8 =
771 Some(
772 layer
773 .k_proj
774 .quantize_to_fp8(gpu, quant_k, kv_dim_local, h, stream)?,
775 );
776 // V proj: [kv_dim, h]
777 layer.v_proj_fp8 =
778 Some(
779 layer
780 .v_proj
781 .quantize_to_fp8(gpu, quant_k, kv_dim_local, h, stream)?,
782 );
783 // O proj: [h, q_dim]
784 layer.o_proj_fp8 =
785 Some(
786 layer
787 .o_proj
788 .quantize_to_fp8(gpu, quant_k, h, q_dim_local, stream)?,
789 );
790 // Gate proj: [inter, h]
791 layer.gate_proj_fp8 = Some(
792 layer
793 .gate_proj
794 .quantize_to_fp8(gpu, quant_k, inter, h, stream)?,
795 );
796 // Up proj: [inter, h]
797 layer.up_proj_fp8 = Some(
798 layer
799 .up_proj
800 .quantize_to_fp8(gpu, quant_k, inter, h, stream)?,
801 );
802 // Down proj: [h, inter]
803 layer.down_proj_fp8 = Some(
804 layer
805 .down_proj
806 .quantize_to_fp8(gpu, quant_k, h, inter, stream)?,
807 );
808 tracing::debug!("DFlash Phase G: layer {} quantized", layer_idx);
809 }
810 // Phase G — the shared lm_head, the largest GEMM in the drafter
811 // (vocab × hidden = 248320 × 5120 ≈ 1.27B weights, ~14× any
812 // per-layer GEMM).
813 //
814 // Preferred: SHARE the checkpoint's NATIVE FP8 lm_head
815 // (`lm_head_native_fp8`, built in factory/lm_head_setup.rs).
816 // Checkpoints like unsloth Qwen3.8-27B-NVFP4 ship the head as
817 // FP8 E4M3 + per-row scale, and those bytes stay resident in the
818 // adopted weight store anyway — re-quantizing the BF16 dequant
819 // was a lossy FP8→BF16→FP8 round trip AND a duplicate 1.27 GB
820 // allocation. The row_scale convention is identical (per-row f32
821 // multiplier), so the tail GEMM kernels are unchanged.
822 //
823 // Fallback (BF16-native checkpoints): runtime-quantize a SEPARATE
824 // FP8 buffer so the target's BF16 lm_head ptr stays valid.
825 if let Some((shared, rows)) = lm_head_native_fp8 {
826 anyhow::ensure!(
827 rows == head.vocab_size,
828 "native FP8 lm_head share rows ({rows}) != drafter vocab ({}) — \
829 the drafter's tail GEMM iterates head.vocab_size rows",
830 head.vocab_size
831 );
832 tracing::info!(
833 "DFlash Phase G: sharing the checkpoint's NATIVE FP8 lm_head \
834 [{} × {}] (1.27 GB runtime mirror skipped)",
835 head.vocab_size,
836 head.hidden_size
837 );
838 head.lm_head_shared_fp8 = Some(shared);
839 } else {
840 tracing::info!(
841 "DFlash Phase G: quantizing shared lm_head [{} × {}]",
842 head.vocab_size,
843 head.hidden_size
844 );
845 let lm_head_bf16 = crate::weight_map::DenseWeight {
846 weight: head.lm_head_shared,
847 };
848 head.lm_head_shared_fp8 = Some(lm_head_bf16.quantize_to_fp8(
849 gpu,
850 quant_k,
851 head.vocab_size,
852 head.hidden_size,
853 stream,
854 )?);
855 }
856 head.quant = DflashQuantization::Fp8Weights;
857 tracing::info!(
858 "DFlash Phase G: drafter weights ready as FP8 (quant = Fp8Weights). \
859 Set AVAROK_DFLASH_DRAFTER_FP8=0 to revert to BF16."
860 );
861 }
862
863 Ok(head)
864 }
865
866 /// Borrow-validate the drafter dimensions against the target's hidden_size
867 /// at construction time. Mismatch is a hard error — the `fc` projection
868 /// width is baked from `target_hidden_size` and a runtime mismatch would
869 /// produce silent garbage (vLLM's loader hits this same check).
870 pub fn validate_against_target(&self, target_hidden_size: usize) -> Result<()> {
871 if self.target_hidden_size != target_hidden_size {
872 anyhow::bail!(
873 "DFlash drafter target_hidden_size mismatch: drafter expects {}, target is {}",
874 self.target_hidden_size,
875 target_hidden_size
876 );
877 }
878 Ok(())
879 }
880}