metrale_model_arch/dflash_head/
from_weights.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Construction of the DFlash drafter head from its loaded weights: kernel
4//! handles, the paged BF16 drafter KV pool, scratch, the RoPE table, the fused K/V
5//! weight and, by default, FP8 weight copies.
6//!
7//! Owner: model-arch (DFlash drafter).
8//! Invariants: none beyond the types.
9
10mod fp8_weights;
11mod kernel_handles;
12mod rope_table;
13
14use anyhow::Result;
15use metrale_cache::kv_cache::{KvCacheConfig, KvCacheDtype, PagedKvCache};
16use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
17use parking_lot::Mutex;
18
19use super::{BlockDiffusionDraftHead, DflashLayer, DflashQuantization, DflashScratch};
20use crate::weight_loader::DflashWeights;
21
22impl BlockDiffusionDraftHead {
23    pub fn from_weights(
24        weights: DflashWeights,
25        embed_tokens_shared: DevicePtr,
26        lm_head_shared: DevicePtr,
27        lm_head_nvfp4: Option<metrale_model_layers::weight_map::QuantizedWeight>,
28        lm_head_native_fp8: Option<(metrale_model_layers::weight_map::Fp8DenseWeight, usize)>,
29        target_hidden_size: usize,
30        gamma: Option<usize>,
31        window_size: Option<usize>,
32        gpu: &dyn GpuBackend,
33        max_seq_len: usize,
34        // 2026-09-25: The widest cross-sequence batch one forward serves. The
35        // gamma-sized scratch holds this many bands of gamma rows, and the drafter KV
36        // pool holds this many sequences' blocks.
37        max_batch_size: usize,
38    ) -> Result<Self> {
39        let nb = max_batch_size.max(1);
40        let target_layer_ids = weights
41            .config
42            .dflash_config
43            .as_ref()
44            .map(|c| c.target_layer_ids.clone())
45            .unwrap_or_default();
46        let mask_token_id = weights
47            .config
48            .dflash_config
49            .as_ref()
50            .map(|c| c.mask_token_id)
51            .unwrap_or(0);
52
53        if target_layer_ids.is_empty() {
54            anyhow::bail!(
55                "DFlash drafter config.json has no `dflash_config.target_layer_ids` — \
56                 cannot determine which target hidden states to capture"
57            );
58        }
59
60        let _ = target_hidden_size;
61
62        let num_layers = weights.config.num_hidden_layers;
63        let hidden_size = weights.config.hidden_size;
64        let intermediate_size = weights.config.intermediate_size;
65        let num_q_heads = weights.config.num_attention_heads;
66        let num_kv_heads = weights.config.num_key_value_heads;
67        let head_dim = weights.config.head_dim;
68        let vocab_size = weights.config.vocab_size;
69        // 2026-09-25: `gamma` is `--dflash-gamma` when given, else `default_dflash_gamma`
70        // of the drafter's `effective_block_size()` (`dflash_config.block_size`, else the
71        // top-level `block_size`, which defaults to 16). 2026-09-30: one resolver
72        // (`resolve_dflash_gamma`) for this, the factory's pools and the server's reserve. The
73        // serve reads the head's gamma back through `block_gamma()`.
74        let gamma_val = metrale_model_layers::layers::qwen3_ssm::resolve_dflash_gamma(
75            gamma,
76            Some(weights.config.effective_block_size()),
77        );
78
79        // 2026-09-25: The drafter's paged KV cache: one cache for all drafter layers,
80        // in 16-token blocks.
81        let block_size = 16;
82        let kv_config = KvCacheConfig {
83            block_size,
84            num_kv_heads,
85            head_dim,
86            num_layers,
87            // 2026-09-25: BF16, the type the drafter's paged attention
88            // (`attn_prefill_paged_indirect`) reads and `reshape_and_cache_flash` writes.
89            dtype: KvCacheDtype::Bf16,
90            layer_dtypes: vec![],
91            layer_dims: vec![],
92            cache_blocks_per_seq: None,
93        };
94        // 2026-09-25: Each sequence's lazy block allocation in propose.rs takes
95        // `ceil((max_ctx_len + gamma + 1) / 16)` blocks, and `max_ctx_len <=
96        // max_seq_len`, so `per_seq_blocks` covers one sequence. The pool holds
97        // `max_batch_size` of them plus one spare block.
98        // provenance-id: 526f6e616c6420522e205374657369616b
99        let per_seq_blocks = (max_seq_len + gamma_val + 1).div_ceil(block_size);
100        let num_blocks = per_seq_blocks * max_batch_size.max(1) + 1;
101        tracing::info!(
102            "DFlash drafter paged KV pool: {} blocks ({} per-seq x max_batch_size {})",
103            num_blocks,
104            per_seq_blocks,
105            max_batch_size.max(1)
106        );
107        let kv_cache = PagedKvCache::new(kv_config, num_blocks, gpu)?;
108
109        // 2026-09-25: A `gpu.kernel(..)?` handle is required and fails construction
110        // when absent; a `try_kernel` handle is `KernelHandle(0)` when absent.
111        let kernels = kernel_handles::load_kernels(gpu)?;
112
113        // 2026-09-25: Scratch, allocated once. The row buffers hold `rows_max` rows: the
114        // non-paged path's `ctx_window` ctx rows plus `gamma` block rows, or `nb` bands of
115        // `gamma` rows for a batched propose, whichever is more.
116        let bf16 = 2usize;
117        let g = gamma_val;
118        // 2026-09-25: `ctx_window` bounds the ctx rows the non-paged path attends to and
119        // the rows one ctx precompute call takes. METRALE_DFLASH_CTX_WINDOW (default
120        // 4096), read here at construction. The row scratch grows linearly with it.
121        let ctx_window: usize = std::env::var("METRALE_DFLASH_CTX_WINDOW")
122            .ok()
123            .and_then(|s| s.parse().ok())
124            .unwrap_or(4096);
125        tracing::info!(
126            "DFlash ctx_window = {} (set METRALE_DFLASH_CTX_WINDOW to override; \
127             drafter trained on full captured prefix — larger is better, \
128             scratch grows linearly)",
129            ctx_window
130        );
131        let n_attn = g + ctx_window;
132        let rows_max = n_attn.max(nb * gamma_val);
133        let q_dim = num_q_heads * head_dim;
134        let kv_dim = num_kv_heads * head_dim;
135        let scratch = DflashScratch {
136            stream_buf: gpu.alloc(rows_max * hidden_size * bf16)?,
137            norm_buf: gpu.alloc(rows_max * hidden_size * bf16)?,
138            q_buf: gpu.alloc(rows_max * q_dim * bf16)?,
139            k_buf: gpu.alloc(rows_max * kv_dim * bf16)?,
140            v_buf: gpu.alloc(rows_max * kv_dim * bf16)?,
141            attn_out: gpu.alloc(rows_max * q_dim * bf16)?,
142            mlp_intermediate: gpu.alloc(rows_max * intermediate_size * bf16)?,
143            mlp_up: gpu.alloc(rows_max * intermediate_size * bf16)?,
144            stream_acc: gpu.alloc(rows_max * hidden_size * bf16)?,
145            fc_proj: gpu.alloc(ctx_window * hidden_size * bf16)?,
146            // 2026-09-25: The precompute's fused K/V output: up to `ctx_window` rows
147            // (`precompute_ctx_kv` refuses more), `L * 2 * kv_dim` BF16 per row.
148            fused_kv_out: gpu
149                .alloc(ctx_window * num_layers * 2 * num_kv_heads * head_dim * bf16)?,
150            // 2026-09-25: `ctx_window` i64 cache slots (`long long*` in
151            // `fill_slots_from_block_table`): the precompute's ctx rows, and the block
152            // rows in `forward_block`.
153            slot_mapping_dev: gpu.alloc(ctx_window * 8)?,
154            precompute_in: gpu.alloc(
155                super::PRECOMPUTE_BATCH_ROWS * target_layer_ids.len() * target_hidden_size * bf16,
156            )?,
157            // 2026-09-25: One `[kv_len, q_offset, q_rope_pos]` u32 triple (12 bytes) per
158            // band, read by the indirect paged attention at kernel entry.
159            option_b_indirect_args_dev: gpu.alloc(nb * 12)?,
160            // 2026-09-25: Host staging for the drafted tokens (`nb * gamma` u32; page-locked
161            // on CUDA), and the event `forward_block` records after copying into it.
162            draft_tokens_host_pinned: std::sync::atomic::AtomicPtr::new(
163                gpu.alloc_host_pinned(nb * gamma_val * 4)?,
164            ),
165            draft_tokens_event: gpu.create_event()?,
166            // 2026-09-25: `nb * gamma` rows, not `rows_max`: the lm_head writes one row per
167            // block row, and no path indexes the logits by a ctx offset.
168            logits: gpu.alloc(nb * g * vocab_size * bf16)?,
169            draft_tokens_dev: gpu.alloc(rows_max * 4)?,
170            position_ids: gpu.alloc(rows_max * 4)?,
171            // 2026-09-25: DSpark Markov scratch, allocated only when the drafter config
172            // declares a Markov head (`markov_rank > 0`); `DevicePtr(0)` otherwise.
173            markov_embed: if weights.config.markov_rank > 0 {
174                gpu.alloc(weights.config.markov_rank * bf16)?
175            } else {
176                DevicePtr(0)
177            },
178            markov_bias: if weights.config.markov_rank > 0 {
179                gpu.alloc(vocab_size * bf16)?
180            } else {
181                DevicePtr(0)
182            },
183            conf_out: if weights.confidence_proj.is_some() {
184                gpu.alloc(g * bf16)?
185            } else {
186                DevicePtr(0)
187            },
188            // 2026-09-25: DFlash2 scratch, allocated only when the checkpoint ships the
189            // selector. `dflash2_active` requires the selector, so the convs and the walk
190            // never run without these buffers.
191            conv_dyn: if weights.selector_pred.is_some() {
192                let cfg = weights.config.dflash_config.as_ref();
193                let ksz = cfg.map(|c| c.conv_kernel_size).unwrap_or(0).max(1);
194                let gsz = cfg.map(|c| c.conv_group_size).unwrap_or(0).max(1);
195                gpu.alloc(nb * g * 2 * ksz * (hidden_size / gsz) * bf16)?
196            } else {
197                DevicePtr(0)
198            },
199            conv_tmp: if weights.selector_pred.is_some() {
200                gpu.alloc(nb * g * hidden_size * bf16)?
201            } else {
202                DevicePtr(0)
203            },
204            sel_vals: if weights.selector_pred.is_some() {
205                gpu.alloc(nb * g * 16 * 4)?
206            } else {
207                DevicePtr(0)
208            },
209            sel_idx: if weights.selector_pred.is_some() {
210                gpu.alloc(nb * g * 16 * 4)?
211            } else {
212                DevicePtr(0)
213            },
214            sel_hproj: if weights.selector_pred.is_some() {
215                let rank = weights
216                    .config
217                    .dflash_config
218                    .as_ref()
219                    .map(|c| c.selector_rank)
220                    .unwrap_or(256)
221                    .max(1);
222                gpu.alloc(nb * g * rank * bf16)?
223            } else {
224                DevicePtr(0)
225            },
226        };
227
228        // 2026-09-25: The RoPE inverse-frequency table, from the drafter config's
229        // `rope_scaling`: absent gives plain RoPE (`1 / theta^(2j / dim)`),
230        // `rope_type = "yarn"` the YaRN-blended table, and any other value plain RoPE
231        // with a warning. RoPE rotates the whole head (`rotary_dim = head_dim`).
232        let rope_theta = weights.config.rope_theta;
233        let rotary_dim = head_dim;
234        let inv_freq_table = rope_table::rope_inv_freq_table(&weights, rope_theta, rotary_dim);
235
236        let inv_freq_bytes: Vec<u8> = inv_freq_table
237            .iter()
238            .flat_map(|v| v.to_le_bytes())
239            .collect();
240        let yarn_inv_freq = gpu.alloc(inv_freq_bytes.len())?;
241        gpu.copy_h2d(&inv_freq_bytes, yarn_inv_freq)?;
242
243        // 2026-09-25: The fused K/V weight `[L * 2 * kv_dim, hidden]` BF16, laid out
244        // `[K_0, V_0, K_1, V_1, ...]` and copied device to device from each layer's
245        // `k_proj` and `v_proj`, so `precompute_ctx_kv` computes every layer's ctx K/V in
246        // one GEMM. `kv_dim_bytes` is the size of one layer's K (or V) weight.
247        let kv_dim_bytes = num_kv_heads * head_dim * hidden_size * bf16;
248        let fused_total_bytes = num_layers * 2 * kv_dim_bytes;
249        let fused_kv_weight = gpu.alloc(fused_total_bytes)?;
250        for (l, layer) in weights.layers.iter().enumerate() {
251            let layer_base = l * 2 * kv_dim_bytes;
252            gpu.copy_d2d(
253                layer.k_proj.weight,
254                fused_kv_weight.offset(layer_base),
255                kv_dim_bytes,
256            )?;
257            gpu.copy_d2d(
258                layer.v_proj.weight,
259                fused_kv_weight.offset(layer_base + kv_dim_bytes),
260                kv_dim_bytes,
261            )?;
262        }
263        tracing::info!(
264            "DFlash fused_kv_weight: {} bytes ({} layers × 2 × kv_dim × h × bf16), \
265             layout [K0,V0,K1,V1,…] to match vLLM precompute_and_store_context_kv",
266            fused_total_bytes,
267            num_layers,
268        );
269
270        let mut head = Self {
271            // 2026-09-25: The drafter's `DFlashLevers`, resolved from the environment once,
272            // here.
273            levers: super::levers::DFlashLevers::from_env(),
274            num_layers,
275            hidden_size,
276            intermediate_size,
277            num_q_heads,
278            num_kv_heads,
279            head_dim,
280            vocab_size,
281            draft_vocab_size: weights.config.draft_vocab_size.unwrap_or(vocab_size),
282            gamma: gamma_val,
283            block_gamma: std::sync::atomic::AtomicUsize::new(gamma_val),
284            max_batch: nb,
285            mask_token_id,
286            window_size,
287            target_layer_ids,
288            target_hidden_size,
289
290            embed_tokens_shared,
291            lm_head_shared,
292            lm_head_nvfp4,
293            lm_head_shared_fp8: None,
294            hidden_norm: weights.hidden_norm,
295            norm: weights.norm,
296            fc: weights.fc,
297            draft_id_to_target_id: None,
298            layers: weights
299                .layers
300                .into_iter()
301                .map(|l| DflashLayer {
302                    input_layernorm: l.input_layernorm,
303                    post_attention_layernorm: l.post_attention_layernorm,
304                    q_proj: l.q_proj,
305                    k_proj: l.k_proj,
306                    v_proj: l.v_proj,
307                    o_proj: l.o_proj,
308                    q_norm: l.q_norm,
309                    k_norm: l.k_norm,
310                    gate_proj: l.gate_proj,
311                    up_proj: l.up_proj,
312                    down_proj: l.down_proj,
313                    // 2026-09-25: Filled below when the FP8 drafter path is on.
314                    q_proj_fp8: None,
315                    k_proj_fp8: None,
316                    v_proj_fp8: None,
317                    o_proj_fp8: None,
318                    gate_proj_fp8: None,
319                    up_proj_fp8: None,
320                    down_proj_fp8: None,
321                    attention_conv_base: l.attention_conv_base,
322                    attention_conv_proj: l.attention_conv_proj,
323                    mlp_conv_base: l.mlp_conv_base,
324                    mlp_conv_proj: l.mlp_conv_proj,
325                })
326                .collect(),
327            fused_kv_weight: Some(fused_kv_weight),
328            kv_cache: Mutex::new(kv_cache),
329            scratch,
330            kernels,
331            max_seq_len,
332            yarn_inv_freq,
333            rope_theta,
334            rotary_dim,
335            rms_norm_eps: 1e-6,
336            ctx_window,
337            propose_graphs: parking_lot::Mutex::new(super::ProposeGraphs::default()),
338            suppress_graphs: std::sync::atomic::AtomicBool::new(false),
339            propose_warmup_count: std::sync::atomic::AtomicUsize::new(0),
340            quant: DflashQuantization::Bf16,
341            // 2026-09-25: `markov_rank` is 0 when the checkpoint has no `markov_w1`.
342            markov_rank: if weights.markov_w1.is_some() {
343                weights.config.markov_rank
344            } else {
345                0
346            },
347            markov_w1: weights.markov_w1,
348            markov_w2: weights.markov_w2,
349            confidence_proj: weights.confidence_proj,
350            confidence_bias: weights.confidence_bias,
351            confidence_with_markov: weights.config.confidence_head_with_markov,
352            shifted_rows: weights
353                .config
354                .dflash_config
355                .as_ref()
356                .and_then(|c| c.projector_type.as_deref())
357                == Some("dspark"),
358            conv_kernel_size: weights
359                .config
360                .dflash_config
361                .as_ref()
362                .map(|c| c.conv_kernel_size)
363                .unwrap_or(0),
364            conv_group_size: weights
365                .config
366                .dflash_config
367                .as_ref()
368                .map(|c| c.conv_group_size)
369                .unwrap_or(0),
370            selector_rank: weights
371                .config
372                .dflash_config
373                .as_ref()
374                .map(|c| c.selector_rank)
375                .unwrap_or(0),
376            selector_top_k: weights
377                .config
378                .dflash_config
379                .as_ref()
380                .map(|c| c.selector_top_k)
381                .unwrap_or(0),
382            selector_pred: weights.selector_pred,
383            selector_succ: weights.selector_succ,
384            selector_hidden_proj: weights.selector_hidden_proj,
385        };
386        if head.selector_pred.is_some() {
387            tracing::info!(
388                "DFlash2 armed: conv k={} group={} selector rank={} top_k={} \
389                 (kernels present: conv={} topk={} walk={})",
390                head.conv_kernel_size,
391                head.conv_group_size,
392                head.selector_rank,
393                head.selector_top_k,
394                head.kernels.dflash2_conv2.0 != 0,
395                head.kernels.dflash2_topk16.0 != 0,
396                head.kernels.dflash2_selector_walk.0 != 0,
397            );
398        }
399        if head.shifted_rows {
400            tracing::info!(
401                "DSpark drafter: SpecForge shifted row convention active \
402                 (projector_type=dspark) — draft vector rotates right by 1"
403            );
404        }
405
406        tracing::info!(
407            "BlockDiffusionDraftHead loaded: {} layers, hidden={}, intermediate={}, \
408             GQA {}/{}, head_dim={}, γ={}, vocab={}, mask_token_id={}, target_layers={:?}",
409            head.num_layers,
410            head.hidden_size,
411            head.intermediate_size,
412            head.num_q_heads,
413            head.num_kv_heads,
414            head.head_dim,
415            head.gamma,
416            head.vocab_size,
417            head.mask_token_id,
418            head.target_layer_ids,
419        );
420
421        // 2026-09-25: FP8 drafter weights, on unless METRALE_DFLASH_DRAFTER_FP8=0:
422        // quantize each layer's seven dense GEMM weights (q/k/v/o/gate/up/down) to FP8
423        // E4M3 with one f32 scale per row, give the lm_head an FP8 copy, and set
424        // `quant = Fp8Weights`. Needs the row-scaled FP8 GEMM and its m16 variant;
425        // without them the drafter stays on BF16 with a warning.
426        let fp8_requested =
427            std::env::var("METRALE_DFLASH_DRAFTER_FP8").ok().as_deref() != Some("0");
428        let fp8_kernels_present = head.kernels.fp8_gemm_n128_row_scaled.0 != 0
429            && head.kernels.fp8_gemm_n128_row_scaled_m16.0 != 0;
430        if fp8_requested && !fp8_kernels_present {
431            tracing::warn!(
432                "METRALE_DFLASH_DRAFTER_FP8=1 but fp8_gemm_t_row_scaled(_m16) kernels are \
433                 not in this target's w4a16 PTX module — staying on the BF16 drafter path. \
434                 Port the Phase G kernels from kernels/gb10/qwen3.6-27b/nvfp4/w4a16_gemm.cu."
435            );
436        }
437        if fp8_requested && fp8_kernels_present {
438            fp8_weights::quantize_drafter_fp8(&mut head, gpu, q_dim, kv_dim, lm_head_native_fp8)?;
439        }
440
441        Ok(head)
442    }
443
444    /// 2026-09-25: Errors when `target_hidden_size` differs from the value the head was
445    /// built with, which sets the `fc` input width
446    /// (`target_layer_ids.len() * target_hidden_size`). Nothing calls it.
447    pub fn validate_against_target(&self, target_hidden_size: usize) -> Result<()> {
448        if self.target_hidden_size != target_hidden_size {
449            anyhow::bail!(
450                "DFlash drafter target_hidden_size mismatch: drafter expects {}, target is {}",
451                self.target_hidden_size,
452                target_hidden_size
453            );
454        }
455        Ok(())
456    }
457}