spark_model/weight_loader/dflash_loader/config.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Drafter HF `config.json` schema: [`DflashConfig`] plus the nested
4//! [`DflashRopeScaling`] / [`DflashSubConfig`] blocks and their serde
5//! defaults. Split out of `dflash_loader.rs` to stay under the 500-LoC
6//! cap; field semantics are documented on the types themselves.
7
8use serde::Deserialize;
9
10/// Drafter HF `config.json` (subset Atlas consumes). Mirrors
11/// `z-lab/Qwen3.6-35B-A3B-DFlash/config.json` field names verbatim so
12/// `serde_json::from_str` works directly on the raw file.
13#[derive(Debug, Clone, Deserialize)]
14pub struct DflashConfig {
15 pub hidden_size: usize,
16 pub num_hidden_layers: usize,
17 pub intermediate_size: usize,
18 pub num_attention_heads: usize,
19 pub num_key_value_heads: usize,
20 pub head_dim: usize,
21 pub vocab_size: usize,
22 #[serde(default)]
23 pub draft_vocab_size: Option<usize>,
24 #[serde(default)]
25 pub tie_word_embeddings: bool,
26 /// Block size γ. Qwen3.6-DFlash ships `block_size: 16`.
27 #[serde(default = "default_block_size")]
28 pub block_size: usize,
29 /// DFlash-specific nested config object.
30 #[serde(default)]
31 pub dflash_config: Option<DflashSubConfig>,
32 /// Drafter base RoPE θ. Defaults to 10M (matches Qwen3.6-DFlash).
33 #[serde(default = "default_rope_theta")]
34 pub rope_theta: f32,
35 /// HF-style `rope_scaling` block. `None` ⇒ plain RoPE (the v2 2026-04-27
36 /// Qwen3.6-DFlash drafter ships `rope_scaling: null`). When present and
37 /// `rope_type == "yarn"`, the drafter's YaRN parameters are used to
38 /// build the inv_freq table at construction time.
39 /// `alias = "rope_parameters"`: newer transformers releases (RadixArk
40 /// DSpark, incoai DFlash2) ship the block under that key — without the
41 /// alias Atlas silently drops the scaling (the RadixArk config.json had
42 /// to be hand-patched before this).
43 #[serde(default, alias = "rope_parameters")]
44 pub rope_scaling: Option<DflashRopeScaling>,
45 /// DSpark Markov head rank. `0` (default) ⇒ plain DFlash drafter with no
46 /// Markov head. RadixArk `Qwen3.8-27B-DSpark` ships `markov_rank: 256`
47 /// top-level (SpecForge `DSparkConfig` convention — see the checkpoint's
48 /// `dspark.py`: DSpark fields are declared as top-level config attrs).
49 #[serde(default)]
50 pub markov_rank: usize,
51 /// DSpark Markov head flavor. Only `"vanilla"` (low-rank learned bigram
52 /// bias) is defined by the reference; anything else is rejected at load.
53 #[serde(default)]
54 pub markov_head_type: Option<String>,
55 /// DSpark confidence head (`AcceptRatePredictor`): a `Linear(input, 1)`
56 /// predicting per-draft-position acceptance probability, used for
57 /// adaptive block length. Loaded when present; consumed by the dynamic-K
58 /// scheduling phase (the Markov fixup works without it).
59 #[serde(default)]
60 pub enable_confidence_head: bool,
61 /// When true the confidence head's input is `[hidden ‖ markov_embed]`
62 /// (input_dim = hidden_size + markov_rank); when false, hidden only.
63 #[serde(default = "default_true")]
64 pub confidence_head_with_markov: bool,
65}
66
67fn default_true() -> bool {
68 true
69}
70
71fn default_rope_theta() -> f32 {
72 10_000_000.0
73}
74
75/// Subset of HF `rope_scaling` block consumed by Atlas. Mirrors the field
76/// names in `transformers`' Qwen3 config so `serde_json::from_str` works
77/// directly on the drafter's `config.json`.
78#[derive(Debug, Clone, Deserialize)]
79pub struct DflashRopeScaling {
80 /// Currently only `"yarn"` is recognised; anything else falls back to
81 /// plain RoPE with a warning logged at construction time.
82 #[serde(default)]
83 pub rope_type: Option<String>,
84 #[serde(default)]
85 pub factor: Option<f32>,
86 #[serde(default)]
87 pub beta_fast: Option<f32>,
88 #[serde(default)]
89 pub beta_slow: Option<f32>,
90 #[serde(default)]
91 pub original_max_position_embeddings: Option<f32>,
92}
93
94fn default_block_size() -> usize {
95 16
96}
97
98/// Nested `dflash_config` block in the drafter's `config.json`.
99#[derive(Debug, Clone, Deserialize)]
100pub struct DflashSubConfig {
101 /// Token id used to fill the γ "to-be-predicted" positions during draft
102 /// inference. `248070` for Qwen3.6-DFlash.
103 pub mask_token_id: u32,
104 /// Target-model layer indices to capture intermediate hidden states from.
105 /// `[1, 10, 19, 28, 37]` for Qwen3.6-35B-A3B-DFlash. Order matters:
106 /// shallow-to-deep concatenation is what `fc` expects.
107 pub target_layer_ids: Vec<usize>,
108 /// Draft flavor tag. `"dspark"` marks a SpecForge-lineage drafter, whose
109 /// row convention is SHIFTED (row j's output = token at position j+1; the
110 /// anchor row's output is draft #1) versus the z-lab convention Atlas's
111 /// forward was built for (row j predicts at j, row 0 = echo). The runtime
112 /// keys the draft-vector rotation off this tag. DFlash2 checkpoints have
113 /// no tag and keep the z-lab convention (verified against z-lab
114 /// `dflash/model.py::dflash_generate` — anchor-row output discarded,
115 /// mask rows fill in place).
116 #[serde(default)]
117 pub projector_type: Option<String>,
118
119 // ── DFlash2 fields (incoai/z-lab `DFlash2DraftModel`); absent = DFlash1 ──
120 /// Two-tap dynamic conv kernel size (2 for DFlash2).
121 #[serde(default)]
122 pub conv_kernel_size: usize,
123 /// Channels per conv group (16 for DFlash2 → hidden/16 groups).
124 #[serde(default)]
125 pub conv_group_size: usize,
126 /// Selector codebook rank (256 for DFlash2).
127 #[serde(default)]
128 pub selector_rank: usize,
129 /// Candidates kept per position for the selector walk (16 for DFlash2).
130 #[serde(default)]
131 pub selector_top_k: usize,
132 /// Block size γ as the drafter was TRAINED, when the checkpoint states it
133 /// here. DFlash2 ships `block_size: 8` inside `dflash_config`; DFlash1
134 /// ships it top-level only. `None` = not stated, fall back to the
135 /// top-level field. Read through [`DflashConfig::effective_block_size`].
136 #[serde(default)]
137 pub block_size: Option<usize>,
138}
139
140impl DflashConfig {
141 /// Resolved block size γ: the drafter's own trained value when the
142 /// checkpoint states it, else the top-level field.
143 ///
144 /// The top-level `block_size` defaults to 16, and serde fills that default
145 /// happily for a checkpoint that never mentioned it — so a DFlash2 drafter
146 /// trained at 8 comes up as 16 unless the sub-config is consulted first.
147 /// That is not a cosmetic mismatch: the serve then runs num_drafts=15
148 /// against an 8-block drafter, which measured 0% accept on EVERY verify
149 /// step, and sizes the drafter's per-sequence pools for twice the block it
150 /// will ever use. `--dflash-gamma` still overrides both.
151 pub fn effective_block_size(&self) -> usize {
152 self.dflash_config
153 .as_ref()
154 .and_then(|c| c.block_size)
155 .unwrap_or(self.block_size)
156 }
157}