pub mod base;
pub mod cache;
pub mod config;
pub mod dhara;
pub mod gated_delta;
pub mod gemma3;
pub mod gemma4;
pub mod laguna;
pub mod mamba2;
pub mod moe;
pub mod mtp;
pub mod nemotron;
pub mod qwen2;
pub mod qwen3;
pub mod qwen3_5;
use std::path::Path;
use cache::LayerCache;
use mtp::{BackboneOutput, MtpCaches, MtpStepOutput};
use serde_json::Value;
use crate::engine::{
Cancellation,
array::Array,
error::{Error, Result},
media::{audio::ProcessedAudio, image::ProcessedImage},
nn::WeightMap,
quant::Quantization,
weights,
};
pub enum Model {
Qwen2(qwen2::Qwen2Model),
Qwen3(qwen3::Qwen3Model),
Qwen35(qwen3_5::Qwen35Model),
Gemma3(gemma3::Gemma3Model),
Gemma4(gemma4::Gemma4Model),
NemotronH(nemotron::NemotronModel),
Dhara(dhara::DharaModel),
Laguna(laguna::LagunaModel),
}
impl Model {
pub fn load(model_dir: &Path) -> Result<Self> {
let runtime = crate::runtime::initialize_default_if_needed()
.map_err(|error| Error::Mlx(error.to_string()))?;
let mut snapshot = crate::model::layout::CheckpointSnapshot::open_in(
model_dir,
&runtime.home().join("temp"),
)
.map_err(|error| Error::Config(error.to_string()))?;
let allow_mtp = crate::engine::mtp_certification::model_is_certified(&snapshot)?;
Self::load_snapshot(&mut snapshot, model_dir, allow_mtp)
}
pub(crate) fn load_snapshot(
snapshot: &mut crate::model::layout::CheckpointSnapshot,
model_dir: &Path,
allow_mtp: bool,
) -> Result<Self> {
let config_json: Value = serde_json::from_slice(snapshot.config_bytes())
.map_err(|e| Error::Config(format!("bad config.json: {e}")))?;
let model_type = config_json
.get("model_type")
.and_then(|v| v.as_str())
.ok_or_else(|| {
Error::Config(
"config.json has no string model_type; refusing to guess an \
architecture"
.to_string(),
)
})?;
if model_type == "qwen3_5_mtp" {
return Err(Error::Model(
"model_type 'qwen3_5_mtp' is a standalone Qwen3.5 MTP sidecar \
artifact (no backbone, no embeddings/head) and is not a loadable \
model; load a converted dense checkpoint that ships its MTP weights \
under 'language_model.mtp.*' instead"
.into(),
));
}
config::validate_checkpoint_config(&config_json)?;
let quant = Quantization::from_config(&config_json)?;
let skip_multimodal = matches!(model_type, "gemma3" | "gemma3_text");
let tensors = weights::load_snapshot(snapshot, model_dir, |name| {
(allow_mtp || !weights::is_mtp_tensor_name(name))
&& !(skip_multimodal
&& (name.starts_with("vision_tower.")
|| name.starts_with("multi_modal_projector.")))
})?;
let mut weight_map = WeightMap::new(tensors, quant);
match model_type {
"qwen2" => {
let tie = config_json
.get("tie_word_embeddings")
.and_then(|v| v.as_bool())
.unwrap_or(true);
qwen2::sanitize(&mut weight_map, tie);
let model = qwen2::Qwen2Model::load(weight_map, &config_json)?;
Ok(Model::Qwen2(model))
}
"qwen3" => {
let tie = config_json
.get("tie_word_embeddings")
.and_then(|v| v.as_bool())
.unwrap_or(true);
qwen3::sanitize(&mut weight_map, tie);
let model = qwen3::Qwen3Model::load(weight_map, &config_json)?;
Ok(Model::Qwen3(model))
}
"gemma3" | "gemma3_text" => {
gemma3::sanitize(&mut weight_map);
let model = gemma3::Gemma3Model::load(weight_map, &config_json)?;
Ok(Model::Gemma3(model))
}
"gemma4" | "gemma4_text" | "gemma4_unified" | "gemma4_unified_text" => {
gemma4::sanitize(&mut weight_map);
let model = gemma4::Gemma4Model::load(weight_map, &config_json)?;
Ok(Model::Gemma4(model))
}
"qwen3_5" | "qwen3_5_text" | "qwen3_5_moe" | "qwen3_5_moe_text" => {
let text_cfg = config::text_config(&config_json);
let num_hidden_layers = config::require_i32(text_cfg, "num_hidden_layers")?;
let num_experts = config::get_i32(text_cfg, "num_experts", 0)?;
let mtp = if allow_mtp {
qwen3_5::detect_mtp(&weight_map)
} else {
mtp::MtpDetection::None
};
qwen3_5::sanitize(&mut weight_map, num_hidden_layers, num_experts, mtp);
let model = qwen3_5::Qwen35Model::load(weight_map, &config_json)?;
Ok(Model::Qwen35(model))
}
"nemotron_h" => {
nemotron::sanitize(&mut weight_map);
let model = nemotron::NemotronModel::load(weight_map, &config_json)?;
Ok(Model::NemotronH(model))
}
"llama" => {
let tie = config_json
.get("tie_word_embeddings")
.and_then(|v| v.as_bool())
.unwrap_or(true);
qwen2::sanitize(&mut weight_map, tie);
let model = qwen2::Qwen2Model::load(weight_map, &config_json)?;
Ok(Model::Qwen2(model))
}
"dhara_ar" => {
let tie = config_json
.get("tie_word_embeddings")
.and_then(|v| v.as_bool())
.unwrap_or(true);
dhara::sanitize(&mut weight_map, tie);
let model = dhara::DharaModel::load(weight_map, &config_json)?;
Ok(Model::Dhara(model))
}
"laguna" => {
let model = laguna::LagunaModel::load(weight_map, &config_json)?;
Ok(Model::Laguna(model))
}
other => Err(Error::Model(format!(
"unsupported model_type '{other}' (supported: qwen2, qwen3, qwen3_5, \
qwen3_5_moe, gemma3, gemma4, gemma4_unified, nemotron_h, llama, \
dhara_ar, laguna)"
))),
}
}
pub fn new_caches(&self) -> Vec<LayerCache> {
match self {
Model::Qwen2(m) => m.new_caches(),
Model::Qwen3(m) => m.new_caches(),
Model::Qwen35(m) => m.new_caches(),
Model::Gemma3(m) => m.new_caches(),
Model::Gemma4(m) => m.new_caches(),
Model::NemotronH(m) => m.new_caches(),
Model::Dhara(m) => m.new_caches(),
Model::Laguna(m) => m.new_caches(),
}
}
pub fn forward(&self, input_ids: &Array, caches: &mut [LayerCache]) -> Result<Array> {
match self {
Model::Qwen2(m) => m.forward(input_ids, caches),
Model::Qwen3(m) => m.forward(input_ids, caches),
Model::Qwen35(m) => m.forward(input_ids, caches),
Model::Gemma3(m) => m.forward(input_ids, caches),
Model::Gemma4(m) => m.forward(input_ids, caches),
Model::NemotronH(m) => m.forward(input_ids, caches),
Model::Dhara(m) => m.forward(input_ids, caches),
Model::Laguna(m) => m.forward(input_ids, caches),
}
}
pub fn has_mtp(&self) -> bool {
match self {
Model::Qwen35(m) => m.has_mtp(),
_ => false,
}
}
pub fn new_mtp_caches(&self) -> MtpCaches {
match self {
Model::Qwen35(m) => m.new_mtp_caches(),
_ => MtpCaches(Vec::new()),
}
}
pub fn forward_hidden(
&self,
input_ids: &Array,
caches: &mut [LayerCache],
) -> Result<BackboneOutput> {
match self {
Model::Qwen35(m) => m.forward_hidden(input_ids, caches),
_ => Err(Error::Model(
"forward_hidden: this architecture has no MTP support".into(),
)),
}
}
pub fn forward_mtp(
&self,
input_ids: &Array,
prev_hidden: &Array,
caches: &mut MtpCaches,
) -> Result<MtpStepOutput> {
match self {
Model::Qwen35(m) => m.forward_mtp(input_ids, prev_hidden, caches),
_ => Err(Error::Model(
"forward_mtp: this architecture has no MTP support".into(),
)),
}
}
pub fn debug_nemotron_layer_stats(&self, input_ids: &Array) -> Result<Vec<(f32, f32)>> {
match self {
Model::NemotronH(m) => m.debug_layer_stats(input_ids),
_ => Err(Error::Model(
"debug_nemotron_layer_stats: not a NemotronH model".into(),
)),
}
}
pub fn supports_images(&self) -> bool {
match self {
Model::Gemma4(m) => m.supports_images(),
Model::Qwen35(m) => m.supports_images(),
_ => false,
}
}
pub fn image_processing_params(&self) -> Option<(i32, i32, i32)> {
match self {
Model::Gemma4(m) => m.image_processing_params(),
Model::Qwen35(m) => m.image_processing_params(),
_ => None,
}
}
pub fn image_token_ids(&self) -> Option<(u32, u32, u32)> {
match self {
Model::Gemma4(m) => m.image_token_ids(),
Model::Qwen35(m) => m.image_token_ids(),
_ => None,
}
}
pub fn supports_audio(&self) -> bool {
match self {
Model::Gemma4(m) => m.supports_audio(),
_ => false,
}
}
pub fn audio_token_ids(&self) -> Option<(u32, u32, u32)> {
match self {
Model::Gemma4(m) => m.audio_token_ids(),
_ => None,
}
}
pub fn audio_samples_per_token(&self) -> Option<i32> {
match self {
Model::Gemma4(m) => m.audio_samples_per_token(),
_ => None,
}
}
pub fn video_token_id(&self) -> Option<u32> {
match self {
Model::Gemma4(m) => m.video_token_id(),
Model::Qwen35(m) => m.video_token_id(),
_ => None,
}
}
pub fn forward_with_images(
&self,
input_ids: &Array,
images: &[ProcessedImage],
caches: &mut [LayerCache],
) -> Result<Array> {
self.forward_with_media(input_ids, images, &[], caches)
}
pub fn forward_with_media(
&self,
input_ids: &Array,
images: &[ProcessedImage],
audios: &[ProcessedAudio],
caches: &mut [LayerCache],
) -> Result<Array> {
match self {
Model::Gemma4(m) => m.forward_with_media(input_ids, images, audios, caches),
Model::Qwen35(m) => {
if !audios.is_empty() {
return Err(Error::Model(
"qwen3.5: model has no audio support (no audio_config)".into(),
));
}
m.forward_with_media(input_ids, images, caches)
}
_ => Err(Error::Model(
"forward_with_media: model has no multimodal support".into(),
)),
}
}
pub(crate) fn forward_with_media_cancellable(
&self,
input_ids: &Array,
images: &[ProcessedImage],
audios: &[ProcessedAudio],
caches: &mut [LayerCache],
chunk_tokens: usize,
cancellation: Cancellation<'_>,
) -> Result<Array> {
if !cancellation.is_cooperative() {
return self.forward_with_media(input_ids, images, audios, caches);
}
match self {
Model::Gemma4(model) => model.forward_with_media_cancellable(
input_ids,
images,
audios,
caches,
chunk_tokens,
cancellation,
),
Model::Qwen35(model) => {
if !audios.is_empty() {
return Err(Error::Model(
"qwen3.5: model has no audio support (no audio_config)".into(),
));
}
model.forward_with_media_cancellable(
input_ids,
images,
caches,
chunk_tokens,
cancellation,
)
}
_ => Err(Error::Model(
"forward_with_media: model has no multimodal support".into(),
)),
}
}
}