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}