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 // ATLAS_DFLASH_DRAFTER_FP8 gate below (BF16 fallback).
219 fp8_gemm_n128_row_scaled: crate::layers::try_kernel(
220 gpu,
221 "w4a16",
222 "fp8_gemm_t_row_scaled",
223 ),
224 // Phase G — Row-scaled BF16 × FP8 → BF16 GEMV (M=1). Used
225 // by the lm_head GEMM swap in a γ-loop, since the
226 // fp8_gemm_n128 GEMM kernel wastes 75% of its M_TILE at
227 // M=γ=16 against vocab=248320.
228 dense_gemv_fp8w: gpu.kernel("gemv_fp8w", "dense_gemv_fp8w")?,
229 // Phase G — Small-M (M≤16) row-scaled FP8 GEMM for lm_head.
230 // Single warp per CTA, no M_TILE waste. Custom kernel in
231 // w4a16_gemm.cu, module namespace "w4a16".
232 fp8_gemm_n128_row_scaled_m16: crate::layers::try_kernel(
233 gpu,
234 "w4a16",
235 "fp8_gemm_t_row_scaled_m16",
236 ),
237 // Register-tiled M<=8 FP8 GEMV (fp8_gemv_rt.cu, common) —
238 // preferred over both tile GEMMs above for the M=γ propose
239 // GEMMs; try_kernel so targets without the module fall back.
240 fp8_gemv_rt2: crate::layers::try_kernel(
241 gpu,
242 "fp8_gemv_rt",
243 "fp8_gemv_rowscale_batch8_rt2",
244 ),
245 // MAX_M=16 sibling for the γ>8 propose window (2026-08-29).
246 fp8_gemv_rt2_16: crate::layers::try_kernel(
247 gpu,
248 "fp8_gemv_rt",
249 "fp8_gemv_rowscale_batch16_rt2",
250 ),
251 // DFlash2 kernels (kernels/gb10/common/dflash2.cu). try_kernel:
252 // absent on stale kernel builds — DFlash2 then refuses to arm
253 // rather than failing DFlash1/DSpark drafters at load.
254 dflash2_conv2: crate::layers::try_kernel(gpu, "dflash2", "dflash2_conv2"),
255 dflash2_topk16: crate::layers::try_kernel(gpu, "dflash2", "dflash2_topk16"),
256 dflash2_selector_walk: crate::layers::try_kernel(
257 gpu,
258 "dflash2",
259 "dflash2_selector_walk",
260 ),
261 };
262
263 // Per-step scratch buffers. BF16 = 2 bytes/element.
264 //
265 // Sized for `n_attn_slots = ctx_window + γ` rows in the attention
266 // path. The first `ctx_window` slots hold projected target ctx
267 // (K/V only — Q is zero-padded so its attention output is
268 // discarded). The next γ slots hold the noise tokens. lm_head +
269 // logits + argmax tail still operates on γ rows (offset past ctx).
270 let bf16 = 2usize;
271 let g = gamma_val;
272 // Phase 2.5n: ctx_window controls how many captured target positions
273 // the drafter attends to per step. The drafter was trained over the
274 // FULL captured prefix (paper §A.1), but capping at γ=16 cripples it
275 // on prompts past a tiny window — Atlas's 6-10% acceptance vs the
276 // paper's 70% is dominated by this cap. Default raised 512 → 4096
277 // (2026-07-08): long generations (MinHeap ~2.6k tok) blow past 512
278 // captured rows → truncated prefix → accept collapse + droop.
279 // ATLAS_DFLASH_CTX_WINDOW overrides at construction time.
280 //
281 // Memory cost: attention-path scratch scales linearly with
282 // `n_attn = γ + cw`. At cw=4096: stream/norm/acc ≈ 16.8 MB each;
283 // mlp_intermediate/mlp_up = 4112 × 6144 × 2 ≈ 50.5 MB each (must
284 // stay n_attn: contiguous path runs MLP over all rows, and
285 // precompute_ctx_kv borrows mlp_intermediate as all_k_stage
286 // [L×n×kv_dim ≈ 21 MB]); fused_kv_out ≈ 42 MB. logits is γ-rows
287 // only (see alloc below). Total scratch ≈ 250 MB per head.
288 let ctx_window: usize = std::env::var("ATLAS_DFLASH_CTX_WINDOW")
289 .ok()
290 .and_then(|s| s.parse().ok())
291 .unwrap_or(4096);
292 tracing::info!(
293 "DFlash ctx_window = {} (set ATLAS_DFLASH_CTX_WINDOW to override; \
294 drafter trained on full captured prefix — larger is better, \
295 scratch grows linearly)",
296 ctx_window
297 );
298 let n_attn = g + ctx_window; // total attention slots
299 // Rows the scratch must hold: the legacy ctx+gamma window, or nb
300 // bands of gamma for a batched propose, whichever is larger.
301 let rows_max = n_attn.max(nb * gamma_val);
302 let q_dim = num_q_heads * head_dim;
303 let kv_dim = num_kv_heads * head_dim;
304 let scratch = DflashScratch {
305 stream_buf: gpu.alloc(rows_max * hidden_size * bf16)?,
306 norm_buf: gpu.alloc(rows_max * hidden_size * bf16)?,
307 q_buf: gpu.alloc(rows_max * q_dim * bf16)?,
308 k_buf: gpu.alloc(rows_max * kv_dim * bf16)?,
309 v_buf: gpu.alloc(rows_max * kv_dim * bf16)?,
310 attn_out: gpu.alloc(rows_max * q_dim * bf16)?,
311 mlp_intermediate: gpu.alloc(rows_max * intermediate_size * bf16)?,
312 mlp_up: gpu.alloc(rows_max * intermediate_size * bf16)?,
313 stream_acc: gpu.alloc(rows_max * hidden_size * bf16)?,
314 fc_proj: gpu.alloc(ctx_window * hidden_size * bf16)?,
315 // Phase 2 (Option B) precompute scratch. Worst-case the
316 // first propose runs precompute over the whole captured
317 // prefix up to `ctx_window`, so size for that. Per row:
318 // `L * 2 * kv_dim * bf16` bytes. At L=5, kv_dim=512,
319 // ctx_window=512: 5·2·512·512·2 = 5.24 MB.
320 fused_kv_out: gpu
321 .alloc(ctx_window * num_layers * 2 * num_kv_heads * head_dim * bf16)?,
322 // i64 slot mapping for reshape_and_cache (kernel takes
323 // `long long*`). One entry per new ctx row.
324 slot_mapping_dev: gpu.alloc(ctx_window * 8)?,
325 // 12 bytes of device memory holding the per-call triple
326 // `[u32 kv_len, u32 q_offset, u32 q_rope_pos]` that the indirect
327 // paged-attention kernel reads at entry. Host writes via H2D
328 // BEFORE entering the captured region.
329 option_b_indirect_args_dev: gpu.alloc(nb * 12)?,
330 // Phase E.2: pinned host buffer + event for the per-propose
331 // drafter D2H. Pinned memory lets cuMemcpyDtoHAsync issue a
332 // true async DMA on the caller's stream (vs. the synchronous
333 // staging fallback the driver picks for pageable destinations).
334 // The event lets us wait on the *copy*, not the whole stream,
335 // so target-model verify work issued on the same stream can
336 // proceed in parallel.
337 draft_tokens_host_pinned: std::sync::atomic::AtomicPtr::new(
338 gpu.alloc_host_pinned(nb * gamma_val * 4)?,
339 ),
340 draft_tokens_event: gpu.create_event()?,
341 // γ rows only — NOT n_attn. The lm_head GEMM writes M=γ rows,
342 // argmax + BLOCK_DUMP read rows 0..γ, and no path indexes logits
343 // by ctx offset. Sizing at n_attn×vocab would cost 2.04 GB at
344 // cw=4096 for rows nothing ever touches (γ rows ≈ 8.4 MB).
345 logits: gpu.alloc(nb * g * vocab_size * bf16)?,
346 draft_tokens_dev: gpu.alloc(rows_max * 4)?,
347 position_ids: gpu.alloc(rows_max * 4)?,
348 // DSpark Markov scratch. Only allocated when the drafter config
349 // declares a Markov head; `DevicePtr(0)` otherwise so the plain
350 // DFlash path costs nothing.
351 markov_embed: if weights.config.markov_rank > 0 {
352 gpu.alloc(weights.config.markov_rank * bf16)?
353 } else {
354 DevicePtr(0)
355 },
356 markov_bias: if weights.config.markov_rank > 0 {
357 gpu.alloc(vocab_size * bf16)?
358 } else {
359 DevicePtr(0)
360 },
361 conf_out: if weights.confidence_proj.is_some() {
362 gpu.alloc(g * bf16)?
363 } else {
364 DevicePtr(0)
365 },
366 // DFlash2 scratch — only when the checkpoint ships the selector
367 // (conv-only checkpoints are not a thing in the DFlash2 lineage).
368 conv_dyn: if weights.selector_pred.is_some() {
369 let cfg = weights.config.dflash_config.as_ref();
370 let ksz = cfg.map(|c| c.conv_kernel_size).unwrap_or(0).max(1);
371 let gsz = cfg.map(|c| c.conv_group_size).unwrap_or(0).max(1);
372 gpu.alloc(nb * g * 2 * ksz * (hidden_size / gsz) * bf16)?
373 } else {
374 DevicePtr(0)
375 },
376 conv_tmp: if weights.selector_pred.is_some() {
377 gpu.alloc(nb * g * hidden_size * bf16)?
378 } else {
379 DevicePtr(0)
380 },
381 sel_vals: if weights.selector_pred.is_some() {
382 gpu.alloc(nb * g * 16 * 4)?
383 } else {
384 DevicePtr(0)
385 },
386 sel_idx: if weights.selector_pred.is_some() {
387 gpu.alloc(nb * g * 16 * 4)?
388 } else {
389 DevicePtr(0)
390 },
391 sel_hproj: if weights.selector_pred.is_some() {
392 let rank = weights
393 .config
394 .dflash_config
395 .as_ref()
396 .map(|c| c.selector_rank)
397 .unwrap_or(256)
398 .max(1);
399 gpu.alloc(nb * g * rank * bf16)?
400 } else {
401 DevicePtr(0)
402 },
403 };
404
405 // Pre-compute inv_freq table for drafter RoPE.
406 //
407 // The drafter's `config.json:rope_scaling` is the source of truth:
408 // * `None` ⇒ plain RoPE (`inv_freq[j] = 1 / θ^(2j/dim)`). The
409 // v2 2026-04-27 Qwen3.6-DFlash drafter ships `rope_scaling: null`.
410 // * `Some(yarn)` ⇒ YaRN-scaled table (Mistral-Small-4 lineage).
411 //
412 // Historical bug (Friday/Avarok 2026-05): this loader unconditionally
413 // applied YaRN with factor=64 / orig_max_pos=4096 hardcoded, which
414 // mis-scaled every low-frequency RoPE pair (pairs 0..11 divided by
415 // 64, pairs 11..26 ramped). Result: drafter Q/K rotations landed in
416 // the wrong angular basis at every layer → 0% draft acceptance. Now
417 // we read the drafter's own scaling block instead of guessing.
418 let rope_theta = weights.config.rope_theta;
419 let rotary_dim = head_dim; // Qwen3.6-DFlash applies rope to full head_dim
420 let dim_f = rotary_dim as f32;
421 let n_pairs = rotary_dim / 2;
422 let mut inv_freq_table = vec![0.0f32; n_pairs];
423
424 // Default = plain RoPE. Overwritten in the YaRN arm below.
425 for j in 0..n_pairs {
426 inv_freq_table[j] = 1.0 / rope_theta.powf((2 * j) as f32 / dim_f);
427 }
428
429 let rope_kind: &str;
430 if let Some(scaling) = weights.config.rope_scaling.as_ref() {
431 match scaling.rope_type.as_deref() {
432 Some("yarn") => {
433 let factor = scaling.factor.unwrap_or(1.0);
434 let beta_fast = scaling.beta_fast.unwrap_or(32.0);
435 let beta_slow = scaling.beta_slow.unwrap_or(1.0);
436 let orig_max_pos = scaling.original_max_position_embeddings.unwrap_or(4096.0);
437 let find_correction_dim = |num_rot: f32| -> f32 {
438 (dim_f * (orig_max_pos / (num_rot * 2.0 * std::f32::consts::PI)).ln())
439 / (2.0 * rope_theta.ln())
440 };
441 let low = find_correction_dim(beta_fast).floor().max(0.0);
442 let high = find_correction_dim(beta_slow)
443 .ceil()
444 .min((rotary_dim - 1) as f32);
445 let ramp_denom = if (high - low).abs() < 1e-6 {
446 high - low + 0.001
447 } else {
448 high - low
449 };
450 for j in 0..n_pairs {
451 let pos_freq = rope_theta.powf((2 * j) as f32 / dim_f);
452 let inv_freq_extrap = 1.0 / pos_freq;
453 let inv_freq_interp = 1.0 / (factor * pos_freq);
454 let ramp = ((j as f32 - low) / ramp_denom).clamp(0.0, 1.0);
455 let extrap_factor = 1.0 - ramp;
456 inv_freq_table[j] = inv_freq_interp * (1.0 - extrap_factor)
457 + inv_freq_extrap * extrap_factor;
458 }
459 tracing::info!(
460 "DFlash RoPE = YaRN: theta={rope_theta}, factor={factor}, \
461 beta_fast={beta_fast}, beta_slow={beta_slow}, \
462 max_pos={orig_max_pos}, low_dim={low:.1}, high_dim={high:.1}",
463 );
464 rope_kind = "yarn";
465 }
466 Some(other) => {
467 tracing::warn!(
468 "DFlash drafter config has rope_scaling.rope_type={other:?} which Atlas \
469 doesn't recognise — falling back to plain RoPE (theta={rope_theta})."
470 );
471 rope_kind = "plain (unknown rope_type)";
472 }
473 None => {
474 tracing::warn!(
475 "DFlash drafter config has rope_scaling without rope_type — \
476 falling back to plain RoPE (theta={rope_theta})."
477 );
478 rope_kind = "plain (no rope_type)";
479 }
480 }
481 } else {
482 tracing::info!(
483 "DFlash RoPE = plain (no rope_scaling in drafter config), theta={rope_theta}, \
484 {n_pairs} pairs",
485 );
486 rope_kind = "plain";
487 }
488 let _ = rope_kind; // logged above, retained for future debug surfaces
489
490 let inv_freq_bytes: Vec<u8> = inv_freq_table
491 .iter()
492 .flat_map(|v| v.to_le_bytes())
493 .collect();
494 let yarn_inv_freq = gpu.alloc(inv_freq_bytes.len())?;
495 gpu.copy_h2d(&inv_freq_bytes, yarn_inv_freq)?;
496
497 // ── Phase 2 (Option B) fused KV weight build ──────────────
498 //
499 // Concatenate every drafter layer's `k_proj.weight` and
500 // `v_proj.weight` into a single `[L * 2 * kv_dim, h]` BF16
501 // tensor laid out as `[K_0, V_0, K_1, V_1, …, K_{L-1}, V_{L-1}]`.
502 // Lets `precompute_ctx_kv` derive all layers' ctx K/V in one
503 // fused `dense_gemm` instead of `2 * L` separate calls per
504 // propose. Layout matches vLLM's `_fused_kv_weight` in
505 // `qwen3_dflash.py:301-303`.
506 //
507 // Memory layout reasoning: per-layer K weight is `[kv_dim, h]`
508 // BF16 = `kv_dim * h * 2` bytes; V weight is the same shape.
509 // Fused buffer total = `L * 2 * kv_dim * h * 2` bytes.
510 // At L=5, kv_dim=512, h=2048: 5·2·512·2048·2 = 20.97 MB.
511 //
512 // We rely on the existing per-layer `DenseWeight.weight`
513 // device pointers — no host roundtrip, just GPU `copy_d2d`.
514 let kv_dim_bytes = num_kv_heads * head_dim * hidden_size * bf16; // K or V per layer
515 let fused_total_bytes = num_layers * 2 * kv_dim_bytes;
516 let fused_kv_weight = gpu.alloc(fused_total_bytes)?;
517 for (l, layer) in weights.layers.iter().enumerate() {
518 let layer_base = l * 2 * kv_dim_bytes;
519 // K slot for layer l.
520 gpu.copy_d2d(
521 layer.k_proj.weight,
522 fused_kv_weight.offset(layer_base),
523 kv_dim_bytes,
524 )?;
525 // V slot for layer l (immediately after K).
526 gpu.copy_d2d(
527 layer.v_proj.weight,
528 fused_kv_weight.offset(layer_base + kv_dim_bytes),
529 kv_dim_bytes,
530 )?;
531 }
532 tracing::info!(
533 "DFlash fused_kv_weight: {} bytes ({} layers × 2 × kv_dim × h × bf16), \
534 layout [K0,V0,K1,V1,…] to match vLLM precompute_and_store_context_kv",
535 fused_total_bytes,
536 num_layers,
537 );
538
539 let mut head = Self {
540 num_layers,
541 hidden_size,
542 intermediate_size,
543 num_q_heads,
544 num_kv_heads,
545 head_dim,
546 vocab_size,
547 draft_vocab_size: weights.config.draft_vocab_size.unwrap_or(vocab_size),
548 gamma: gamma_val,
549 max_batch: nb,
550 mask_token_id,
551 window_size,
552 target_layer_ids,
553 target_hidden_size,
554
555 embed_tokens_shared,
556 lm_head_shared,
557 lm_head_nvfp4,
558 lm_head_shared_fp8: None,
559 hidden_norm: weights.hidden_norm,
560 norm: weights.norm,
561 fc: weights.fc,
562 draft_id_to_target_id: None,
563 layers: weights
564 .layers
565 .into_iter()
566 .map(|l| DflashLayer {
567 input_layernorm: l.input_layernorm,
568 post_attention_layernorm: l.post_attention_layernorm,
569 q_proj: l.q_proj,
570 k_proj: l.k_proj,
571 v_proj: l.v_proj,
572 o_proj: l.o_proj,
573 q_norm: l.q_norm,
574 k_norm: l.k_norm,
575 gate_proj: l.gate_proj,
576 up_proj: l.up_proj,
577 down_proj: l.down_proj,
578 // Phase G — populated below if ATLAS_DFLASH_DRAFTER_FP8=1.
579 q_proj_fp8: None,
580 k_proj_fp8: None,
581 v_proj_fp8: None,
582 o_proj_fp8: None,
583 gate_proj_fp8: None,
584 up_proj_fp8: None,
585 down_proj_fp8: None,
586 attention_conv_base: l.attention_conv_base,
587 attention_conv_proj: l.attention_conv_proj,
588 mlp_conv_base: l.mlp_conv_base,
589 mlp_conv_proj: l.mlp_conv_proj,
590 })
591 .collect(),
592 // Phase 2 stage 2: fused KV weight built above by copy_d2d
593 // from each layer's k_proj/v_proj. precompute_ctx_kv will
594 // GEMM against it in stage 3 once we wire the call site.
595 fused_kv_weight: Some(fused_kv_weight),
596 kv_cache: Mutex::new(kv_cache),
597 scratch,
598 kernels,
599 max_seq_len,
600 yarn_inv_freq,
601 rope_theta,
602 rotary_dim,
603 rms_norm_eps: 1e-6,
604 ctx_window,
605 // Phase F: per-subgraph graph state — empty until the first
606 // capture pass lands. Layout: [pre_0, post_0, ..., tail].
607 propose_graphs: parking_lot::Mutex::new(None),
608 suppress_graphs: std::sync::atomic::AtomicBool::new(false),
609 propose_warmup_count: std::sync::atomic::AtomicUsize::new(0),
610 quant: DflashQuantization::Bf16,
611 // DSpark heads. `markov_rank` is zeroed when the tensors are
612 // absent so the runtime gate is a single field check.
613 markov_rank: if weights.markov_w1.is_some() {
614 weights.config.markov_rank
615 } else {
616 0
617 },
618 markov_w1: weights.markov_w1,
619 markov_w2: weights.markov_w2,
620 confidence_proj: weights.confidence_proj,
621 confidence_bias: weights.confidence_bias,
622 confidence_with_markov: weights.config.confidence_head_with_markov,
623 shifted_rows: weights
624 .config
625 .dflash_config
626 .as_ref()
627 .and_then(|c| c.projector_type.as_deref())
628 == Some("dspark"),
629 conv_kernel_size: weights
630 .config
631 .dflash_config
632 .as_ref()
633 .map(|c| c.conv_kernel_size)
634 .unwrap_or(0),
635 conv_group_size: weights
636 .config
637 .dflash_config
638 .as_ref()
639 .map(|c| c.conv_group_size)
640 .unwrap_or(0),
641 selector_rank: weights
642 .config
643 .dflash_config
644 .as_ref()
645 .map(|c| c.selector_rank)
646 .unwrap_or(0),
647 selector_top_k: weights
648 .config
649 .dflash_config
650 .as_ref()
651 .map(|c| c.selector_top_k)
652 .unwrap_or(0),
653 selector_pred: weights.selector_pred,
654 selector_succ: weights.selector_succ,
655 selector_hidden_proj: weights.selector_hidden_proj,
656 };
657 if head.selector_pred.is_some() {
658 tracing::info!(
659 "DFlash2 armed: conv k={} group={} selector rank={} top_k={} \
660 (kernels present: conv={} topk={} walk={})",
661 head.conv_kernel_size,
662 head.conv_group_size,
663 head.selector_rank,
664 head.selector_top_k,
665 head.kernels.dflash2_conv2.0 != 0,
666 head.kernels.dflash2_topk16.0 != 0,
667 head.kernels.dflash2_selector_walk.0 != 0,
668 );
669 }
670 if head.shifted_rows {
671 tracing::info!(
672 "DSpark drafter: SpecForge shifted row convention active \
673 (projector_type=dspark) — draft vector rotates right by 1"
674 );
675 }
676
677 tracing::info!(
678 "BlockDiffusionDraftHead loaded: {} layers, hidden={}, intermediate={}, \
679 GQA {}/{}, head_dim={}, γ={}, vocab={}, mask_token_id={}, target_layers={:?}",
680 head.num_layers,
681 head.hidden_size,
682 head.intermediate_size,
683 head.num_q_heads,
684 head.num_kv_heads,
685 head.head_dim,
686 head.gamma,
687 head.vocab_size,
688 head.mask_token_id,
689 head.target_layer_ids,
690 );
691
692 // Phase G — opt-in drafter MLP FP8. Quantize the seven dense-GEMM
693 // weights per layer (q/k/v/o/gate/up/down) BF16 → FP8 E4M3 with
694 // per-row f32 scales. One-shot at model load; runtime hot path
695 // consumes the Fp8DenseWeight via fp8_gemm_n128 in pre/post_attn
696 // (wired in G.3). Default OFF — bit-identical to F.2 baseline.
697 //
698 // Acceptance gate (G.4 design doc §16.7): bench must hold
699 // ≥43% accept (vs 44.9% BF16) AND ≥11.0 tok/s (vs 8.70). If hard
700 // fail, layer-by-layer ablation; skip layer 0 first.
701 // Default ON since the 54.5 record config (2026-08-19): FP8 drafter
702 // weights are the proven speed lane (accept gate held). `=0` reverts
703 // to the BF16 drafter path.
704 let fp8_requested = std::env::var("ATLAS_DFLASH_DRAFTER_FP8").ok().as_deref() != Some("0");
705 let fp8_kernels_present = head.kernels.fp8_gemm_n128_row_scaled.0 != 0
706 && head.kernels.fp8_gemm_n128_row_scaled_m16.0 != 0;
707 if fp8_requested && !fp8_kernels_present {
708 tracing::warn!(
709 "ATLAS_DFLASH_DRAFTER_FP8=1 but fp8_gemm_t_row_scaled(_m16) kernels are \
710 not in this target's w4a16 PTX module — staying on the BF16 drafter path. \
711 Port the Phase G kernels from kernels/gb10/qwen3.6-27b/nvfp4/w4a16_gemm.cu."
712 );
713 }
714 if fp8_requested && fp8_kernels_present {
715 tracing::info!(
716 "DFlash Phase G: quantizing drafter weights to FP8 E4M3 ({} layers × 7 GEMMs)",
717 head.num_layers
718 );
719 let stream = 0u64; // default stream — load-time, no concurrency
720 let q_dim_local = q_dim;
721 let kv_dim_local = kv_dim;
722 let h = head.hidden_size;
723 let inter = head.intermediate_size;
724 let quant_k = head.kernels.quantize_bf16_to_fp8;
725 for (layer_idx, layer) in head.layers.iter_mut().enumerate() {
726 // Q proj: [q_dim, h]
727 layer.q_proj_fp8 =
728 Some(
729 layer
730 .q_proj
731 .quantize_to_fp8(gpu, quant_k, q_dim_local, h, stream)?,
732 );
733 // K proj: [kv_dim, h]
734 layer.k_proj_fp8 =
735 Some(
736 layer
737 .k_proj
738 .quantize_to_fp8(gpu, quant_k, kv_dim_local, h, stream)?,
739 );
740 // V proj: [kv_dim, h]
741 layer.v_proj_fp8 =
742 Some(
743 layer
744 .v_proj
745 .quantize_to_fp8(gpu, quant_k, kv_dim_local, h, stream)?,
746 );
747 // O proj: [h, q_dim]
748 layer.o_proj_fp8 =
749 Some(
750 layer
751 .o_proj
752 .quantize_to_fp8(gpu, quant_k, h, q_dim_local, stream)?,
753 );
754 // Gate proj: [inter, h]
755 layer.gate_proj_fp8 = Some(
756 layer
757 .gate_proj
758 .quantize_to_fp8(gpu, quant_k, inter, h, stream)?,
759 );
760 // Up proj: [inter, h]
761 layer.up_proj_fp8 = Some(
762 layer
763 .up_proj
764 .quantize_to_fp8(gpu, quant_k, inter, h, stream)?,
765 );
766 // Down proj: [h, inter]
767 layer.down_proj_fp8 = Some(
768 layer
769 .down_proj
770 .quantize_to_fp8(gpu, quant_k, h, inter, stream)?,
771 );
772 tracing::debug!("DFlash Phase G: layer {} quantized", layer_idx);
773 }
774 // Phase G — the shared lm_head, the largest GEMM in the drafter
775 // (vocab × hidden = 248320 × 5120 ≈ 1.27B weights, ~14× any
776 // per-layer GEMM).
777 //
778 // Preferred: SHARE the checkpoint's NATIVE FP8 lm_head
779 // (`lm_head_native_fp8`, built in factory/lm_head_setup.rs).
780 // Checkpoints like unsloth Qwen3.8-27B-NVFP4 ship the head as
781 // FP8 E4M3 + per-row scale, and those bytes stay resident in the
782 // adopted weight store anyway — re-quantizing the BF16 dequant
783 // was a lossy FP8→BF16→FP8 round trip AND a duplicate 1.27 GB
784 // allocation. The row_scale convention is identical (per-row f32
785 // multiplier), so the tail GEMM kernels are unchanged.
786 //
787 // Fallback (BF16-native checkpoints): runtime-quantize a SEPARATE
788 // FP8 buffer so the target's BF16 lm_head ptr stays valid.
789 if let Some((shared, rows)) = lm_head_native_fp8 {
790 anyhow::ensure!(
791 rows == head.vocab_size,
792 "native FP8 lm_head share rows ({rows}) != drafter vocab ({}) — \
793 the drafter's tail GEMM iterates head.vocab_size rows",
794 head.vocab_size
795 );
796 tracing::info!(
797 "DFlash Phase G: sharing the checkpoint's NATIVE FP8 lm_head \
798 [{} × {}] (1.27 GB runtime mirror skipped)",
799 head.vocab_size,
800 head.hidden_size
801 );
802 head.lm_head_shared_fp8 = Some(shared);
803 } else {
804 tracing::info!(
805 "DFlash Phase G: quantizing shared lm_head [{} × {}]",
806 head.vocab_size,
807 head.hidden_size
808 );
809 let lm_head_bf16 = crate::weight_map::DenseWeight {
810 weight: head.lm_head_shared,
811 };
812 head.lm_head_shared_fp8 = Some(lm_head_bf16.quantize_to_fp8(
813 gpu,
814 quant_k,
815 head.vocab_size,
816 head.hidden_size,
817 stream,
818 )?);
819 }
820 head.quant = DflashQuantization::Fp8Weights;
821 tracing::info!(
822 "DFlash Phase G: drafter weights ready as FP8 (quant = Fp8Weights). \
823 Set ATLAS_DFLASH_DRAFTER_FP8=0 to revert to BF16."
824 );
825 }
826
827 Ok(head)
828 }
829
830 /// Borrow-validate the drafter dimensions against the target's hidden_size
831 /// at construction time. Mismatch is a hard error — the `fc` projection
832 /// width is baked from `target_hidden_size` and a runtime mismatch would
833 /// produce silent garbage (vLLM's loader hits this same check).
834 pub fn validate_against_target(&self, target_hidden_size: usize) -> Result<()> {
835 if self.target_hidden_size != target_hidden_size {
836 anyhow::bail!(
837 "DFlash drafter target_hidden_size mismatch: drafter expects {}, target is {}",
838 self.target_hidden_size,
839 target_hidden_size
840 );
841 }
842 Ok(())
843 }
844}