use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::path::Path;
fn default_max_checkpoints() -> usize { 10 }
fn default_model_name() -> String { "model".to_string() }
pub use crate::distributed::sync::DistributedConfig;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LitConfig {
pub model: ModelConfig,
pub trainer: TrainerConfig,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ModelConfig {
#[serde(default = "default_model_name")]
pub name: String,
pub hidden_size: usize,
pub num_layers: usize,
pub num_heads: usize,
pub kv_heads: Option<usize>,
pub kv_latent_dim: Option<usize>,
pub q_latent_dim: Option<usize>,
pub d_rope: Option<usize>,
pub num_experts: Option<usize>,
pub experts_per_tok: Option<usize>,
#[allow(dead_code)] pub shared_expert_enabled: Option<bool>,
pub intermediate_size: Option<usize>,
pub mamba_layers: Option<Vec<usize>>,
pub mamba2_num_heads: Option<usize>, pub mamba2_head_dim: Option<usize>, pub mamba2_state_size: Option<usize>, pub mamba2_chunk_size: Option<usize>, pub mamba2_n_groups: Option<usize>, pub mamba2_conv_kernel: Option<usize>, pub mamba2_expand: Option<usize>,
pub mamba3_enabled: Option<bool>, pub mamba3_complex_rope: Option<bool>, pub mamba3_mimo_rank: Option<usize>, pub mamba3_use_conv: Option<bool>,
pub vocab_size: usize,
pub max_seq_len: usize,
pub rope_theta: f32,
#[serde(default)]
pub dtype: DTypePrecision,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TargetDevice {
#[default]
Gpu,
Cpu,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DTypePrecision {
#[default]
F32,
F16,
BF16,
}
impl DTypePrecision {
pub fn to_candle_dtype(&self) -> candle_core::DType {
match self {
DTypePrecision::F32 => candle_core::DType::F32,
DTypePrecision::F16 => candle_core::DType::F16,
DTypePrecision::BF16 => candle_core::DType::BF16,
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"f32" | "fp32" | "float32" => Some(Self::F32),
"f16" | "fp16" | "float16" => Some(Self::F16),
"bf16" | "bfloat16" => Some(Self::BF16),
_ => None,
}
}
}
impl std::fmt::Display for DTypePrecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DTypePrecision::F32 => write!(f, "f32"),
DTypePrecision::F16 => write!(f, "f16"),
DTypePrecision::BF16 => write!(f, "bf16"),
}
}
}
impl ModelConfig {
pub fn checkpoint_prefix(&self) -> String {
let params = self.estimate_params_short();
format!("{}_{}_{}_{}",
self.name.to_lowercase(),
self.max_seq_len,
params,
self.dtype.to_string().to_lowercase()
)
}
fn estimate_params_short(&self) -> String {
let head_dim = self.hidden_size / self.num_heads;
let kv_heads = self.kv_heads.unwrap_or(self.num_heads);
let kv_size = kv_heads * head_dim;
let intermediate_size = self.intermediate_size.unwrap_or(self.hidden_size * 4);
let embed = self.vocab_size * self.hidden_size;
let attn = self.hidden_size * self.hidden_size + self.hidden_size * kv_size + self.hidden_size * kv_size + self.hidden_size * self.hidden_size;
let mlp = self.hidden_size * intermediate_size * 3;
let layer = attn + mlp + 2 * self.hidden_size;
let head = self.hidden_size * self.vocab_size + self.hidden_size;
let total = embed + self.num_layers * layer + head;
if total >= 1_000_000_000 {
format!("{:.1}b", total as f64 / 1_000_000_000.0)
} else {
format!("{}m", total / 1_000_000)
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrainerConfig {
pub learning_rate: f64,
pub batch_size: usize,
pub max_steps: usize,
pub num_epochs: usize, pub gradient_accumulation: usize, pub checkpoint_dir: String,
pub log_interval: usize,
pub save_interval: usize,
#[serde(default = "default_max_checkpoints")]
pub max_checkpoints: usize, pub load_balance_alpha: f64,
#[serde(default)]
pub target_device: TargetDevice,
#[serde(default)]
pub distributed: DistributedConfig,
#[allow(dead_code)] pub group_size: usize, #[allow(dead_code)] pub beta_kl: f64, }
impl LitConfig {
pub fn from_yaml<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let content = fs::read_to_string(path)?;
let config: LitConfig = serde_yaml::from_str(&content)?;
config.validate()?;
Ok(config)
}
fn validate(&self) -> anyhow::Result<()> {
if let (Some(num_heads), Some(head_dim), Some(expand)) = (
self.model.mamba2_num_heads,
self.model.mamba2_head_dim,
self.model.mamba2_expand,
) {
let expected = num_heads * head_dim;
let actual = self.model.hidden_size * expand;
if actual != expected {
anyhow::bail!(
"Mamba2 config invalid: hidden_size * expand ({}) != num_heads * head_dim ({})",
actual,
expected
);
}
}
let has_mamba_layers = self.model.mamba_layers.as_ref().map_or(false, |v| !v.is_empty());
let has_mamba2_config = self.model.mamba2_num_heads.is_some();
if has_mamba_layers && !has_mamba2_config {
eprintln!("⚠️ WARNING: Using Mamba-1 (sequential SSM) which is 5-10x slower than Mamba-2.");
eprintln!(" Mamba-1 uses a sequential for-loop over sequence length, bottlenecking GPU utilization.");
eprintln!(" To enable Mamba-2 (State Space Duality), add these fields to your config:");
eprintln!(" mamba2_num_heads: 48");
eprintln!(" mamba2_head_dim: 16");
eprintln!(" mamba2_state_size: 64");
eprintln!(" mamba2_chunk_size: 64");
eprintln!(" mamba2_n_groups: 1");
eprintln!(" mamba2_conv_kernel: 4");
eprintln!(" mamba2_expand: 2");
eprintln!(" See models/nano.yaml for the default Mamba-2 configuration.");
eprintln!();
}
Ok(())
}
#[allow(dead_code)] pub fn nano_baseline() -> Self {
Self {
model: ModelConfig {
name: "nano-baseline".to_string(),
hidden_size: 512,
num_layers: 8,
num_heads: 8,
kv_heads: Some(4), kv_latent_dim: None,
q_latent_dim: None,
d_rope: None,
num_experts: None,
experts_per_tok: None,
shared_expert_enabled: None,
intermediate_size: Some(2048), mamba_layers: None,
mamba2_num_heads: None,
mamba2_head_dim: None,
mamba2_state_size: None,
mamba2_chunk_size: None,
mamba2_n_groups: None,
mamba2_conv_kernel: None,
mamba2_expand: None,
mamba3_enabled: None,
mamba3_complex_rope: None,
mamba3_mimo_rank: None,
mamba3_use_conv: None,
vocab_size: 128354, max_seq_len: 512,
rope_theta: 10000.0,
dtype: DTypePrecision::F32,
},
trainer: TrainerConfig {
learning_rate: 3e-4,
batch_size: 2, max_steps: 5000,
num_epochs: 2, gradient_accumulation: 1, checkpoint_dir: "./checkpoints".to_string(),
log_interval: 10,
save_interval: 500,
max_checkpoints: 10, load_balance_alpha: 0.0, target_device: TargetDevice::Gpu,
distributed: DistributedConfig::default(),
group_size: 4, beta_kl: 0.04, },
}
}
#[allow(dead_code)] pub fn nano() -> Self {
Self {
model: ModelConfig {
name: "nano".to_string(),
hidden_size: 384,
num_layers: 8,
num_heads: 6,
kv_heads: None,
kv_latent_dim: Some(192), q_latent_dim: Some(192), d_rope: Some(16),
num_experts: Some(4), experts_per_tok: Some(2), shared_expert_enabled: Some(true),
intermediate_size: Some(1536),
mamba_layers: Some(vec![0, 1, 2, 4, 5, 6]),
mamba2_num_heads: Some(48),
mamba2_head_dim: Some(16),
mamba2_state_size: Some(64),
mamba2_chunk_size: Some(64),
mamba2_n_groups: Some(1),
mamba2_conv_kernel: Some(4),
mamba2_expand: Some(2),
mamba3_enabled: None,
mamba3_complex_rope: None,
mamba3_mimo_rank: None,
mamba3_use_conv: None,
vocab_size: 128354, max_seq_len: 512, rope_theta: 10000.0,
dtype: DTypePrecision::F32,
},
trainer: TrainerConfig {
learning_rate: 3e-4,
batch_size: 1,
max_steps: 5000,
num_epochs: 2, gradient_accumulation: 1,
checkpoint_dir: "./checkpoints".to_string(),
log_interval: 10,
save_interval: 500,
max_checkpoints: 10, load_balance_alpha: 0.01, target_device: TargetDevice::Gpu,
distributed: DistributedConfig::default(),
group_size: 4, beta_kl: 0.04, },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointMeta {
pub step: usize,
pub timestamp: String,
pub model_config_hash: String,
pub data_file_path: String,
pub data_file_hash: String,
pub checkpoint_file: String,
#[serde(default = "default_dtype_string")]
pub dtype: String,
}
fn default_dtype_string() -> String {
"f32".to_string()
}
impl CheckpointMeta {
pub fn new(
step: usize,
model_config: &ModelConfig,
data_file_path: &str,
checkpoint_file: &str,
) -> anyhow::Result<Self> {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let model_config_hash = compute_model_config_hash(model_config);
let data_file_hash = compute_file_hash(data_file_path)?;
Ok(Self {
step,
timestamp,
model_config_hash,
data_file_path: data_file_path.to_string(),
data_file_hash,
checkpoint_file: checkpoint_file.to_string(),
dtype: model_config.dtype.to_string(),
})
}
pub fn save(&self, checkpoint_dir: &str) -> anyhow::Result<()> {
let meta_path = format!("{}/checkpoint_step_{}.meta.json", checkpoint_dir, self.step);
let json = serde_json::to_string_pretty(self)?;
fs::write(&meta_path, json)?;
Ok(())
}
pub fn load(meta_path: &str) -> anyhow::Result<Self> {
let content = fs::read_to_string(meta_path)?;
let meta: CheckpointMeta = serde_json::from_str(&content)?;
Ok(meta)
}
pub fn validate(
&self,
model_config: &ModelConfig,
data_file_path: &str,
) -> anyhow::Result<CheckpointValidation> {
let current_model_hash = compute_model_config_hash(model_config);
let current_data_hash = compute_file_hash(data_file_path)?;
let current_dtype = model_config.dtype.to_string();
let model_matches = self.model_config_hash == current_model_hash;
let data_matches = self.data_file_hash == current_data_hash;
let dtype_matches = self.dtype == current_dtype;
Ok(CheckpointValidation {
model_matches,
data_matches,
dtype_matches,
checkpoint_model_hash: self.model_config_hash.clone(),
current_model_hash,
checkpoint_data_hash: self.data_file_hash.clone(),
current_data_hash,
checkpoint_dtype: self.dtype.clone(),
current_dtype,
})
}
}
#[derive(Debug)]
pub struct CheckpointValidation {
pub model_matches: bool,
pub data_matches: bool,
pub dtype_matches: bool,
pub checkpoint_model_hash: String,
pub current_model_hash: String,
pub checkpoint_data_hash: String,
pub current_data_hash: String,
pub checkpoint_dtype: String,
pub current_dtype: String,
}
impl CheckpointValidation {
pub fn is_valid(&self) -> bool {
self.model_matches && self.data_matches
}
}
pub fn compute_model_config_hash(config: &ModelConfig) -> String {
let json = serde_json::to_string(config).unwrap_or_default();
let mut hasher = DefaultHasher::new();
json.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn compute_file_hash(path: &str) -> anyhow::Result<String> {
let mut file = fs::File::open(path)?;
let metadata = file.metadata()?;
let file_size = metadata.len();
let mut buffer = vec![0u8; 1024 * 1024.min(file_size as usize)];
file.read_exact(&mut buffer)?;
let mut hasher = DefaultHasher::new();
buffer.hash(&mut hasher);
file_size.hash(&mut hasher);
Ok(format!("{:016x}", hasher.finish()))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlazrConfig {
pub hidden_size: usize,
pub num_layers: usize,
pub vocab_size: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba2_num_heads: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba2_head_dim: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba2_state_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba2_chunk_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba2_expand: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba2_conv_kernel: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba3_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba3_complex_rope: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba3_mimo_rank: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mamba3_use_conv: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub num_attention_heads: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kv_latent_dim: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub q_latent_dim: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub d_rope: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub num_experts: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub experts_per_tok: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shared_expert_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub intermediate_size: Option<usize>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub mamba_layers: Vec<usize>,
pub rms_norm_eps: f64,
pub max_seq_len: usize,
}
impl BlazrConfig {
pub fn from_model_config(cfg: &ModelConfig) -> Self {
Self {
hidden_size: cfg.hidden_size,
num_layers: cfg.num_layers,
vocab_size: cfg.vocab_size,
mamba2_num_heads: cfg.mamba2_num_heads,
mamba2_head_dim: cfg.mamba2_head_dim,
mamba2_state_size: cfg.mamba2_state_size,
mamba2_chunk_size: cfg.mamba2_chunk_size,
mamba2_expand: cfg.mamba2_expand,
mamba2_conv_kernel: cfg.mamba2_conv_kernel,
mamba3_enabled: cfg.mamba3_enabled,
mamba3_complex_rope: cfg.mamba3_complex_rope,
mamba3_mimo_rank: cfg.mamba3_mimo_rank,
mamba3_use_conv: cfg.mamba3_use_conv,
num_attention_heads: Some(cfg.num_heads),
kv_latent_dim: cfg.kv_latent_dim,
q_latent_dim: cfg.q_latent_dim,
d_rope: cfg.d_rope,
num_experts: cfg.num_experts,
experts_per_tok: cfg.experts_per_tok,
shared_expert_enabled: cfg.shared_expert_enabled,
intermediate_size: cfg.intermediate_size,
mamba_layers: cfg.mamba_layers.clone().unwrap_or_default(),
rms_norm_eps: 1e-5,
max_seq_len: cfg.max_seq_len,
}
}
pub fn save(&self, checkpoint_dir: &str) -> anyhow::Result<()> {
let config_path = format!("{}/config.json", checkpoint_dir);
let json = serde_json::to_string_pretty(self)?;
fs::write(&config_path, json)?;
Ok(())
}
pub fn exists(checkpoint_dir: &str) -> bool {
Path::new(&format!("{}/config.json", checkpoint_dir)).exists()
}
}
pub fn find_latest_checkpoint(checkpoint_dir: &str) -> anyhow::Result<Option<(String, String, usize)>> {
let dir = Path::new(checkpoint_dir);
if !dir.exists() {
return Ok(None);
}
let mut latest_step = 0usize;
let mut latest_checkpoint: Option<String> = None;
let mut latest_meta: Option<String> = None;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
if filename.contains("_step_") && filename.ends_with(".safetensors") {
if let Some(step_part) = filename.split("_step_").last() {
if let Some(step_str) = step_part.strip_suffix(".safetensors") {
if let Ok(step) = step_str.parse::<usize>() {
if step > latest_step {
let meta_path = format!("{}/checkpoint_step_{}.meta.json", checkpoint_dir, step);
if Path::new(&meta_path).exists() {
latest_step = step;
latest_checkpoint = Some(path.to_string_lossy().to_string());
latest_meta = Some(meta_path);
}
}
}
}
}
}
}
}
match (latest_checkpoint, latest_meta) {
(Some(ckpt), Some(meta)) => Ok(Some((ckpt, meta, latest_step))),
_ => Ok(None),
}
}