use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Clone, Deserialize)]
pub struct Qwen3Config {
pub vocab_size: usize,
pub hidden_size: usize,
pub intermediate_size: usize,
pub num_hidden_layers: usize,
pub num_attention_heads: usize,
pub num_key_value_heads: usize,
pub head_dim: usize,
pub max_position_embeddings: usize,
#[serde(default = "default_rms_norm_eps")]
pub rms_norm_eps: f64,
#[serde(default = "default_rope_theta")]
pub rope_theta: f64,
#[serde(default = "default_hidden_act")]
pub hidden_act: String,
#[serde(default)]
pub tie_word_embeddings: bool,
#[serde(default)]
pub attention_bias: bool,
#[serde(default = "default_qk_norm")]
pub qk_norm: bool,
#[serde(default)]
pub sliding_window: Option<usize>,
#[serde(default = "default_max_window_layers")]
pub max_window_layers: usize,
#[serde(default)]
pub use_sliding_window: bool,
#[serde(default, alias = "n_routed_experts")]
pub num_experts: usize,
#[serde(default, alias = "num_experts_per_tok")]
pub num_experts_used: usize,
#[serde(default)]
pub expert_ffn_size: usize,
#[serde(default)]
pub shared_expert_ffn_size: usize,
#[serde(default = "default_expert_weights_scale")]
pub expert_weights_scale: f32,
}
fn default_expert_weights_scale() -> f32 {
1.0
}
fn default_rms_norm_eps() -> f64 {
1e-6
}
fn default_rope_theta() -> f64 {
1_000_000.0
}
fn default_hidden_act() -> String {
"silu".into()
}
fn default_max_window_layers() -> usize {
usize::MAX
}
fn default_qk_norm() -> bool {
true
}
impl Qwen3Config {
pub fn from_file(path: &Path) -> anyhow::Result<Self> {
let data = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&data)?)
}
pub fn kv_group_size(&self) -> usize {
self.num_attention_heads / self.num_key_value_heads
}
pub fn q_proj_dim(&self) -> usize {
self.num_attention_heads * self.head_dim
}
pub fn kv_proj_dim(&self) -> usize {
self.num_key_value_heads * self.head_dim
}
pub fn is_moe(&self) -> bool {
self.num_experts > 0
}
pub fn expert_ffn_dim(&self) -> usize {
if self.expert_ffn_size > 0 {
self.expert_ffn_size
} else if self.num_experts_used > 0 {
self.intermediate_size
.checked_div(self.num_experts_used)
.unwrap_or(self.intermediate_size)
} else {
self.intermediate_size
}
}
pub fn shared_expert_ffn_dim(&self) -> usize {
if self.shared_expert_ffn_size > 0 {
self.shared_expert_ffn_size
} else {
0
}
}
pub fn layer_uses_swa(&self, idx: usize) -> bool {
self.use_sliding_window && self.sliding_window.is_some() && idx >= self.max_window_layers
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_qwen3_0_6b_like() {
let json = r#"{
"vocab_size": 151936,
"hidden_size": 1024,
"intermediate_size": 3072,
"num_hidden_layers": 28,
"num_attention_heads": 16,
"num_key_value_heads": 8,
"head_dim": 128,
"max_position_embeddings": 32768,
"rope_theta": 1000000.0,
"tie_word_embeddings": true
}"#;
let cfg: Qwen3Config = serde_json::from_str(json).unwrap();
assert_eq!(cfg.kv_group_size(), 2);
assert_eq!(cfg.q_proj_dim(), 2048);
assert_eq!(cfg.kv_proj_dim(), 1024);
assert!(cfg.tie_word_embeddings);
assert_eq!(cfg.rms_norm_eps, 1e-6);
}
#[test]
fn sliding_window_off_by_default() {
let json = r#"{
"vocab_size": 100,
"hidden_size": 64,
"intermediate_size": 128,
"num_hidden_layers": 2,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"head_dim": 16,
"max_position_embeddings": 512
}"#;
let cfg: Qwen3Config = serde_json::from_str(json).unwrap();
assert!(!cfg.layer_uses_swa(0));
assert!(!cfg.layer_uses_swa(1));
}
}