metrale_model_arch/weight_loader/
dflash_loader.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: DFlash drafter loader: parses the drafter's `config.json` and
4//! loads its weights into [`DflashWeights`] for
5//! [`crate::dflash_head::BlockDiffusionDraftHead`].
6//!
7//! The loader reads no embedding or LM head: `BlockDiffusionDraftHead::from_weights`
8//! takes the target model's. Tensor names may be bare or `model.`-prefixed.
9//!
10//! Owner: model-arch weight loader.
11//! Invariants: none beyond the types.
12
13use anyhow::{Context, Result};
14use metrale_gpu_runtime::gpu::GpuBackend;
15use metrale_model_weights::weights::WeightStore;
16
17use metrale_model_layers::weight_map::{DenseWeight, dense};
18
19mod config;
20pub use config::*;
21
22/// 2026-09-25: Load a drafter GEMM weight as BF16. A U8 tensor is packed NVFP4
23/// `[n, k/2]` under the same name a BF16 tensor would have; it must be 2-D
24/// and is dequantized to BF16 on the GPU. Any other dtype is returned as the
25/// store's pointer, unconverted.
26fn dense_bf16_or_nvfp4(
27    store: &WeightStore,
28    name: &str,
29    gpu: &dyn GpuBackend,
30) -> Result<DenseWeight> {
31    let t = store.get(name)?;
32    if t.dtype == metrale_model_weights::weights::WeightDtype::UInt8 {
33        let prefix = name.strip_suffix(".weight").unwrap_or(name);
34        anyhow::ensure!(
35            t.shape.len() == 2,
36            "NVFP4-packed drafter tensor {name} has shape {:?} (want 2-D)",
37            t.shape
38        );
39        let (n, half_k) = (t.shape[0], t.shape[1]);
40        tracing::info!(
41            "DFlash drafter: dequantizing NVFP4 {name} [{n}, {}] -> BF16",
42            half_k * 2
43        );
44        return metrale_model_layers::weight_map::fp8_lut::dequant_nvfp4_to_bf16(
45            store,
46            prefix,
47            n,
48            half_k * 2,
49            gpu,
50        );
51    }
52    Ok(DenseWeight { weight: t.ptr })
53}
54
55/// 2026-09-25: The drafter weights `load_dflash_weights` returns. It holds no
56/// embedding or LM head.
57#[allow(dead_code)]
58pub struct DflashWeights {
59    pub config: DflashConfig,
60
61    pub fc: DenseWeight,
62    pub hidden_norm: DenseWeight,
63    pub norm: DenseWeight,
64
65    pub layers: Vec<DflashLayerWeights>,
66
67    /// 2026-09-25: `Some` (and empty) when the store has a `d2t` or
68    /// `draft_id_to_target_id` tensor; the table itself is not loaded.
69    pub draft_id_to_target_id: Option<Vec<i64>>,
70
71    /// 2026-09-25: `markov_head.markov_w1.weight`. `markov_w1` and `markov_w2` are
72    /// `Some` when `markov_rank > 0` and the store has `markov_w1`.
73    pub markov_w1: Option<DenseWeight>,
74    /// 2026-09-25: `markov_head.markov_w2.weight`.
75    pub markov_w2: Option<DenseWeight>,
76    /// 2026-09-25: `confidence_head.proj.weight`. The weight and the bias are
77    /// `Some` when `enable_confidence_head` is set and the store has the weight.
78    pub confidence_proj: Option<DenseWeight>,
79    /// 2026-09-25: `confidence_head.proj.bias`.
80    pub confidence_bias: Option<DenseWeight>,
81
82    /// 2026-09-25: `candidate_selector.predecessor_codebook`. The three selector
83    /// tensors are `Some` when `dflash_config` sets `selector_rank` and
84    /// `selector_top_k` and the store has this codebook.
85    pub selector_pred: Option<DenseWeight>,
86    /// 2026-09-25: `candidate_selector.successor_codebook`.
87    pub selector_succ: Option<DenseWeight>,
88    /// 2026-09-25: `candidate_selector.hidden_projection.weight`.
89    pub selector_hidden_proj: Option<DenseWeight>,
90}
91
92/// 2026-09-25: One drafter layer's weights. The projections go through
93/// `dense_bf16_or_nvfp4`; the norms and conv tensors are the store's pointers.
94#[allow(dead_code)]
95pub struct DflashLayerWeights {
96    pub input_layernorm: DenseWeight,
97    pub post_attention_layernorm: DenseWeight,
98    pub q_proj: DenseWeight,
99    pub k_proj: DenseWeight,
100    pub v_proj: DenseWeight,
101    pub o_proj: DenseWeight,
102    pub q_norm: DenseWeight,
103    pub k_norm: DenseWeight,
104    pub gate_proj: DenseWeight,
105    pub up_proj: DenseWeight,
106    pub down_proj: DenseWeight,
107
108    /// 2026-09-25: `attention_conv.base_kernel`. The four conv tensors are `Some`
109    /// on every layer or on none (see `load_dflash_weights`).
110    pub attention_conv_base: Option<DenseWeight>,
111    /// 2026-09-25: `attention_conv.kernel_projection.weight`.
112    pub attention_conv_proj: Option<DenseWeight>,
113    /// 2026-09-25: `mlp_conv.base_kernel`.
114    pub mlp_conv_base: Option<DenseWeight>,
115    /// 2026-09-25: `mlp_conv.kernel_projection.weight`.
116    pub mlp_conv_proj: Option<DenseWeight>,
117}
118
119/// 2026-09-25: True when the store has `fc.weight` or `model.fc.weight`. Loads
120/// nothing.
121pub fn store_has_dflash_weights(store: &WeightStore) -> bool {
122    store.contains("fc.weight") || store.contains("model.fc.weight")
123}
124
125/// 2026-09-25: Parse a drafter's `config.json` into a [`DflashConfig`]. The
126/// error is "Parsing DFlash drafter config.json" with the serde error as its
127/// cause.
128pub fn parse_dflash_config(json: &str) -> Result<DflashConfig> {
129    serde_json::from_str(json).context("Parsing DFlash drafter config.json")
130}
131
132/// 2026-09-25: Load the drafter's weights from the drafter's own
133/// [`WeightStore`].
134///
135/// Returns `Ok(None)` when `store_has_dflash_weights` is false. Otherwise
136/// `fc`, the two model norms and `num_hidden_layers` layers of norms and
137/// projections are required; the conv, selector, Markov and confidence
138/// tensors are optional as their fields describe. `_tp_size` is unused, so
139/// every rank loads the whole drafter.
140pub fn load_dflash_weights(
141    drafter_store: &WeightStore,
142    drafter_config: &DflashConfig,
143    _gpu: &dyn GpuBackend,
144    _tp_size: usize,
145) -> Result<Option<DflashWeights>> {
146    if !store_has_dflash_weights(drafter_store) {
147        tracing::debug!("DFlash drafter store has no `fc.weight` — skipping");
148        return Ok(None);
149    }
150
151    let prefix = if drafter_store.contains("model.fc.weight") {
152        "model."
153    } else {
154        ""
155    };
156
157    let fc = dense_bf16_or_nvfp4(drafter_store, &format!("{prefix}fc.weight"), _gpu)
158        .context("DFlash drafter: load fc.weight")?;
159    let hidden_norm = dense(drafter_store, &format!("{prefix}hidden_norm.weight"))
160        .context("DFlash drafter: load hidden_norm.weight")?;
161    let norm = dense(drafter_store, &format!("{prefix}norm.weight"))
162        .context("DFlash drafter: load norm.weight")?;
163
164    // 2026-09-25: The conv tensors load on every layer when `dflash_config`
165    // sets `conv_kernel_size` and `conv_group_size` and layer 0 has
166    // `attention_conv.base_kernel`; a later layer missing one is an error.
167    let dflash2_conv = drafter_config
168        .dflash_config
169        .as_ref()
170        .map(|c| c.conv_kernel_size > 0 && c.conv_group_size > 0)
171        .unwrap_or(false)
172        && drafter_store.contains(&format!("{prefix}layers.0.attention_conv.base_kernel"));
173
174    let layer_count = drafter_config.num_hidden_layers;
175    let mut layers = Vec::with_capacity(layer_count);
176    for i in 0..layer_count {
177        let lp = format!("{prefix}layers.{i}");
178        let (attention_conv_base, attention_conv_proj, mlp_conv_base, mlp_conv_proj) =
179            if dflash2_conv {
180                (
181                    Some(dense(
182                        drafter_store,
183                        &format!("{lp}.attention_conv.base_kernel"),
184                    )?),
185                    Some(dense(
186                        drafter_store,
187                        &format!("{lp}.attention_conv.kernel_projection.weight"),
188                    )?),
189                    Some(dense(drafter_store, &format!("{lp}.mlp_conv.base_kernel"))?),
190                    Some(dense(
191                        drafter_store,
192                        &format!("{lp}.mlp_conv.kernel_projection.weight"),
193                    )?),
194                )
195            } else {
196                (None, None, None, None)
197            };
198        let layer = DflashLayerWeights {
199            input_layernorm: dense(drafter_store, &format!("{lp}.input_layernorm.weight"))?,
200            post_attention_layernorm: dense(
201                drafter_store,
202                &format!("{lp}.post_attention_layernorm.weight"),
203            )?,
204            q_proj: dense_bf16_or_nvfp4(
205                drafter_store,
206                &format!("{lp}.self_attn.q_proj.weight"),
207                _gpu,
208            )?,
209            k_proj: dense_bf16_or_nvfp4(
210                drafter_store,
211                &format!("{lp}.self_attn.k_proj.weight"),
212                _gpu,
213            )?,
214            v_proj: dense_bf16_or_nvfp4(
215                drafter_store,
216                &format!("{lp}.self_attn.v_proj.weight"),
217                _gpu,
218            )?,
219            o_proj: dense_bf16_or_nvfp4(
220                drafter_store,
221                &format!("{lp}.self_attn.o_proj.weight"),
222                _gpu,
223            )?,
224            q_norm: dense(drafter_store, &format!("{lp}.self_attn.q_norm.weight"))?,
225            k_norm: dense(drafter_store, &format!("{lp}.self_attn.k_norm.weight"))?,
226            gate_proj: dense_bf16_or_nvfp4(
227                drafter_store,
228                &format!("{lp}.mlp.gate_proj.weight"),
229                _gpu,
230            )?,
231            up_proj: dense_bf16_or_nvfp4(drafter_store, &format!("{lp}.mlp.up_proj.weight"), _gpu)?,
232            down_proj: dense_bf16_or_nvfp4(
233                drafter_store,
234                &format!("{lp}.mlp.down_proj.weight"),
235                _gpu,
236            )?,
237            attention_conv_base,
238            attention_conv_proj,
239            mlp_conv_base,
240            mlp_conv_proj,
241        };
242        layers.push(layer);
243    }
244
245    // 2026-09-25: The two codebook tensor names have no `.weight` suffix.
246    let selector_key = format!("{prefix}candidate_selector.predecessor_codebook");
247    let (selector_pred, selector_succ, selector_hidden_proj) = if drafter_config
248        .dflash_config
249        .as_ref()
250        .map(|c| c.selector_rank > 0 && c.selector_top_k > 0)
251        .unwrap_or(false)
252        && drafter_store.contains(&selector_key)
253    {
254        (
255            Some(
256                dense(drafter_store, &selector_key)
257                    .context("DFlash2: load candidate_selector.predecessor_codebook")?,
258            ),
259            Some(
260                dense(
261                    drafter_store,
262                    &format!("{prefix}candidate_selector.successor_codebook"),
263                )
264                .context("DFlash2: load candidate_selector.successor_codebook")?,
265            ),
266            Some(
267                dense(
268                    drafter_store,
269                    &format!("{prefix}candidate_selector.hidden_projection.weight"),
270                )
271                .context("DFlash2: load candidate_selector.hidden_projection.weight")?,
272            ),
273        )
274    } else {
275        (None, None, None)
276    };
277    if dflash2_conv || selector_pred.is_some() {
278        tracing::info!(
279            "DFlash2 heads loaded: convs={} (k={}, group={}), selector={} (rank={}, top_k={})",
280            dflash2_conv,
281            drafter_config
282                .dflash_config
283                .as_ref()
284                .map(|c| c.conv_kernel_size)
285                .unwrap_or(0),
286            drafter_config
287                .dflash_config
288                .as_ref()
289                .map(|c| c.conv_group_size)
290                .unwrap_or(0),
291            selector_pred.is_some(),
292            drafter_config
293                .dflash_config
294                .as_ref()
295                .map(|c| c.selector_rank)
296                .unwrap_or(0),
297            drafter_config
298                .dflash_config
299                .as_ref()
300                .map(|c| c.selector_top_k)
301                .unwrap_or(0),
302        );
303    }
304
305    let draft_id_to_target_id = if drafter_store.contains(&format!("{prefix}d2t"))
306        || drafter_store.contains(&format!("{prefix}draft_id_to_target_id"))
307    {
308        tracing::warn!(
309            "DFlash drafter has draft-id→target-id mapping; remapping path is not yet wired (Phase 2.5 follow-up)"
310        );
311        Some(Vec::new())
312    } else {
313        None
314    };
315
316    let markov_key = format!("{prefix}markov_head.markov_w1.weight");
317    let (markov_w1, markov_w2) =
318        if drafter_config.markov_rank > 0 && drafter_store.contains(&markov_key) {
319            if let Some(kind) = drafter_config.markov_head_type.as_deref()
320                && kind != "vanilla"
321            {
322                anyhow::bail!(
323                    "DSpark drafter declares markov_head_type={kind:?}; only \"vanilla\" \
324                 (low-rank bigram bias) is defined by the reference implementation"
325                );
326            }
327            let w1 = dense(drafter_store, &markov_key)
328                .context("DSpark drafter: load markov_head.markov_w1.weight")?;
329            let w2 = dense(
330                drafter_store,
331                &format!("{prefix}markov_head.markov_w2.weight"),
332            )
333            .context("DSpark drafter: load markov_head.markov_w2.weight")?;
334            (Some(w1), Some(w2))
335        } else {
336            if drafter_config.markov_rank > 0 {
337                tracing::warn!(
338                    "DSpark drafter config declares markov_rank={} but the checkpoint has \
339                 no {markov_key} — running as plain DFlash (Markov bias disabled)",
340                    drafter_config.markov_rank,
341                );
342            }
343            (None, None)
344        };
345    let conf_key = format!("{prefix}confidence_head.proj.weight");
346    let (confidence_proj, confidence_bias) =
347        if drafter_config.enable_confidence_head && drafter_store.contains(&conf_key) {
348            let w = dense(drafter_store, &conf_key)
349                .context("DSpark drafter: load confidence_head.proj.weight")?;
350            let b = dense(drafter_store, &format!("{prefix}confidence_head.proj.bias"))
351                .context("DSpark drafter: load confidence_head.proj.bias")?;
352            (Some(w), Some(b))
353        } else {
354            (None, None)
355        };
356    if markov_w1.is_some() || confidence_proj.is_some() {
357        tracing::info!(
358            "DSpark heads loaded: markov={} (rank={}), confidence={} (with_markov={})",
359            markov_w1.is_some(),
360            drafter_config.markov_rank,
361            confidence_proj.is_some(),
362            drafter_config.confidence_head_with_markov,
363        );
364    }
365
366    tracing::info!(
367        "DFlash drafter loaded: {} layers, hidden={}, vocab={}, γ={}, target_layers={:?}",
368        layers.len(),
369        drafter_config.hidden_size,
370        drafter_config.vocab_size,
371        drafter_config.block_size,
372        drafter_config
373            .dflash_config
374            .as_ref()
375            .map(|c| c.target_layer_ids.as_slice())
376            .unwrap_or(&[]),
377    );
378
379    Ok(Some(DflashWeights {
380        config: drafter_config.clone(),
381        fc,
382        hidden_norm,
383        norm,
384        layers,
385        draft_id_to_target_id,
386        markov_w1,
387        markov_w2,
388        confidence_proj,
389        confidence_bias,
390        selector_pred,
391        selector_succ,
392        selector_hidden_proj,
393    }))
394}
395
396#[cfg(test)]
397#[path = "dflash_loader/loader_tests.rs"]
398mod loader_tests;