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}