cherenkov/qwen4_exp/
config.rs1use anyhow::{Context, Result};
4use serde::Deserialize;
5use std::path::Path;
6
7#[derive(Debug, Clone, Deserialize)]
9pub struct Qwen4ExpConfig {
10 pub hidden_size: usize,
11 pub num_hidden_layers: usize,
12 pub layer_types: Vec<String>,
13 pub num_attention_heads: usize,
14 pub num_key_value_heads: usize,
15 pub head_dim: usize,
16 pub linear_num_key_heads: usize,
17 pub linear_num_value_heads: usize,
18 pub linear_key_head_dim: usize,
19 pub linear_value_head_dim: usize,
20 pub linear_conv_kernel_dim: usize,
21 pub rms_norm_eps: f64,
22 pub vocab_size: usize,
23 pub partial_rotary_factor: f64,
24 pub rope_parameters: RopeParams,
25 #[serde(default = "four")]
26 pub hc_count: usize,
27 #[serde(default = "d320")]
28 pub hc_lowrank: usize,
29 pub num_experts: usize,
30 pub num_experts_per_tok: usize,
31 pub moe_intermediate_size: usize,
32 pub shared_expert_intermediate_size: usize,
33 #[serde(default = "yes")]
34 pub norm_topk_prob: bool,
35 #[serde(default)]
36 pub ple_layer_ids: Vec<usize>,
37 #[serde(default = "four")]
38 pub ple_conv_kernel_size: usize,
39 pub ple_embed_dim: usize,
40 #[serde(default = "three")]
41 pub ngram_size: usize,
42 #[serde(default = "eight")]
43 pub heads_per_ngram: usize,
44 pub indexer_n_heads: usize,
45 pub indexer_kv_heads: usize,
46 pub indexer_head_dim: usize,
47 pub indexer_budget: usize,
48 pub indexer_compress_ratio: usize,
49 #[serde(default = "silu")]
50 pub output_gate_type: String,
51 pub eos_token_id: u32,
52 #[serde(default)]
53 pub mtp_num_hidden_layers: usize,
54}
55
56#[derive(Debug, Clone, Deserialize)]
57pub struct RopeParams {
58 pub rope_theta: f64,
59}
60
61fn four() -> usize {
62 4
63}
64
65fn three() -> usize {
66 3
67}
68
69fn eight() -> usize {
70 8
71}
72
73fn d320() -> usize {
74 320
75}
76
77fn yes() -> bool {
78 true
79}
80
81fn silu() -> String {
82 "silu".into()
83}
84
85impl Qwen4ExpConfig {
86 pub fn load(model_dir: &Path) -> Result<Self> {
87 let path = model_dir.join("config.json");
88 let bytes = std::fs::read(&path).with_context(|| format!("reading {}", path.display()))?;
89 let root: serde_json::Value = serde_json::from_slice(&bytes).context("config.json")?;
90 let model_type = root["model_type"].as_str().context("model_type missing")?;
91
92 anyhow::ensure!(
93 matches!(model_type, "qwen4_exp" | "qwen4_exp_text"),
94 "unsupported model_type {model_type}; expected qwen4_exp"
95 );
96
97 let text = root.get("text_config").cloned().unwrap_or(root);
98
99 anyhow::ensure!(
100 text["model_type"] == "qwen4_exp_text",
101 "expected qwen4_exp_text configuration"
102 );
103
104 let cfg: Qwen4ExpConfig = serde_json::from_value(text).context("text_config")?;
105
106 anyhow::ensure!(
107 cfg.layer_types.len() == cfg.num_hidden_layers,
108 "layer_types length mismatch"
109 );
110 anyhow::ensure!(cfg.hc_count > 1, "hc_count must exceed 1");
111
112 Ok(cfg)
113 }
114
115 pub fn is_linear(&self, layer: usize) -> bool {
116 self.layer_types[layer] == "linear_attention"
117 }
118
119 pub fn ple_layer(&self) -> Option<usize> {
121 self.ple_layer_ids.first().map(|id| id - 1)
122 }
123
124 pub fn hc_hidden(&self) -> usize {
125 self.hc_count * self.hidden_size
126 }
127}
128
129#[cfg(test)]
130#[path = "../../tests/unit/qwen4_exp/config.rs"]
131mod tests;