1use crate::qtensor::QTensor;
126use crate::tokenizer::Tokenizer;
127use cortiq_core::format::TensorEntry;
128use cortiq_core::{CmfModel, TensorDtype};
129use serde_json::Value;
130use std::path::{Path, PathBuf};
131use std::sync::Arc;
132
133pub const MIMO_MM_ARCH: &str = "mimo_v2_mm";
135pub const MIMO_BASE_ARCH: &str = "mimo_v2";
137pub const MM_CONFIG_BLOB: &str = "mm.config_json";
139pub const AUDIO_TOKENIZER_CONFIG_BLOB: &str = "audio_tokenizer.config_json";
141pub const AUDIO_TOKENIZER_PREFIX: &str = "audio_tokenizer.";
143pub const MM_SUFFIX: &str = ".mm.cmf";
145
146pub const MIMO_SPECIAL_TOKENS: [(&str, u32); 9] = [
149 ("<|vision_start|>", 151652),
150 ("<|vision_end|>", 151653),
151 ("<|image_pad|>", 151655),
152 ("<|video_pad|>", 151656),
153 ("<|audio_pad|>", 151669),
154 ("<|mimo_video_start|>", 151670),
155 ("<|mimo_video_end|>", 151671),
156 ("<|mimo_audio_start|>", 151673),
157 ("<|mimo_audio_end|>", 151674),
158];
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub struct MimoTokenIds {
163 pub vision_start: u32,
164 pub vision_end: u32,
165 pub image_pad: u32,
166 pub video_pad: u32,
167 pub audio_pad: u32,
168 pub video_start: u32,
169 pub video_end: u32,
170 pub audio_start: u32,
171 pub audio_end: u32,
172}
173
174impl MimoTokenIds {
175 pub const PINNED: Self = Self {
177 vision_start: 151652,
178 vision_end: 151653,
179 image_pad: 151655,
180 video_pad: 151656,
181 audio_pad: 151669,
182 video_start: 151670,
183 video_end: 151671,
184 audio_start: 151673,
185 audio_end: 151674,
186 };
187
188 pub fn named(&self) -> [(&'static str, u32); 9] {
190 [
191 (MIMO_SPECIAL_TOKENS[0].0, self.vision_start),
192 (MIMO_SPECIAL_TOKENS[1].0, self.vision_end),
193 (MIMO_SPECIAL_TOKENS[2].0, self.image_pad),
194 (MIMO_SPECIAL_TOKENS[3].0, self.video_pad),
195 (MIMO_SPECIAL_TOKENS[4].0, self.audio_pad),
196 (MIMO_SPECIAL_TOKENS[5].0, self.video_start),
197 (MIMO_SPECIAL_TOKENS[6].0, self.video_end),
198 (MIMO_SPECIAL_TOKENS[7].0, self.audio_start),
199 (MIMO_SPECIAL_TOKENS[8].0, self.audio_end),
200 ]
201 }
202
203 pub fn from_config(cfg: &Value) -> Result<Self, String> {
206 let top = |k: &str| -> Result<u32, String> {
207 cfg.get(k)
208 .and_then(Value::as_u64)
209 .map(|v| v as u32)
210 .ok_or_else(|| format!("config.json: missing integer '{k}'"))
211 };
212 let proc = |k: &str| -> Result<u32, String> {
213 cfg.get("processor_config")
214 .and_then(|p| p.get(k))
215 .and_then(Value::as_u64)
216 .map(|v| v as u32)
217 .ok_or_else(|| format!("config.json: missing integer 'processor_config.{k}'"))
218 };
219 Ok(Self {
220 vision_start: top("vision_start_token_id")?,
221 vision_end: top("vision_end_token_id")?,
222 image_pad: top("image_token_id")?,
223 video_pad: top("video_token_id")?,
224 audio_pad: top("audio_token_id")?,
225 video_start: proc("video_start_token_id")?,
226 video_end: proc("video_end_token_id")?,
227 audio_start: top("audio_start_token_id")?,
228 audio_end: top("audio_end_token_id")?,
229 })
230 }
231
232 pub fn check_pinned(&self, what: &str) -> Result<(), String> {
234 for ((name, got), (_, want)) in self.named().iter().zip(Self::PINNED.named()) {
235 if *got != want {
236 return Err(format!(
237 "{what}: special token {name} has id {got}, the MiMo layout pins {want}"
238 ));
239 }
240 }
241 Ok(())
242 }
243}
244
245fn cfg_usize(cfg: &Value, key: &str, what: &str) -> Result<usize, String> {
246 match cfg.get(key) {
247 Some(Value::Number(n)) => n
248 .as_u64()
249 .map(|v| v as usize)
250 .ok_or_else(|| format!("{what}.{key}: not a non-negative integer")),
251 Some(Value::String(s)) => s
253 .trim()
254 .parse()
255 .map_err(|_| format!("{what}.{key}: '{s}' is not an integer")),
256 _ => Err(format!("{what}: missing integer '{key}'")),
257 }
258}
259
260fn cfg_usize_or(cfg: &Value, key: &str, what: &str, default: usize) -> Result<usize, String> {
261 if cfg.get(key).is_none_or(Value::is_null) {
262 Ok(default)
263 } else {
264 cfg_usize(cfg, key, what)
265 }
266}
267
268fn cfg_f64_or(cfg: &Value, key: &str, default: f64) -> f64 {
269 match cfg.get(key) {
270 Some(Value::Number(n)) => n.as_f64().unwrap_or(default),
271 Some(Value::String(s)) => s.trim().parse().unwrap_or(default),
272 _ => default,
273 }
274}
275
276fn cfg_bool_or(cfg: &Value, key: &str, default: bool) -> bool {
277 cfg.get(key).and_then(Value::as_bool).unwrap_or(default)
278}
279
280fn cfg_usize_list(cfg: &Value, key: &str, what: &str) -> Result<Vec<usize>, String> {
281 cfg.get(key)
282 .and_then(Value::as_array)
283 .ok_or_else(|| format!("{what}: missing list '{key}'"))?
284 .iter()
285 .map(|v| {
286 v.as_u64()
287 .map(|x| x as usize)
288 .ok_or_else(|| format!("{what}.{key}: non-integer entry"))
289 })
290 .collect()
291}
292
293fn per_channel(cfg: &Value, key: &str, channels: usize) -> Result<Vec<usize>, String> {
296 let bad = || format!("audio_config.{key}: expected an integer or 'a-b-…' list");
297 let one = |s: &str| s.trim().parse::<usize>().map_err(|_| bad());
298 match cfg.get(key) {
299 Some(Value::Number(n)) => Ok(vec![n.as_u64().ok_or_else(bad)? as usize; channels]),
300 Some(Value::String(s)) if s.contains('-') => {
301 let v = s.split('-').map(one).collect::<Result<Vec<_>, _>>()?;
302 if v.len() != channels {
303 return Err(format!(
304 "audio_config.{key}: {} entries for {channels} channels",
305 v.len()
306 ));
307 }
308 Ok(v)
309 }
310 Some(Value::String(s)) => Ok(vec![one(s)?; channels]),
311 _ => Err(bad()),
312 }
313}
314
315#[derive(Clone, Debug, PartialEq)]
317pub struct MimoVisionGeom {
318 pub depth: usize,
319 pub hidden: usize,
321 pub intermediate: usize,
322 pub heads: usize,
323 pub kv_heads: usize,
324 pub head_dim: usize,
326 pub out_hidden: usize,
328 pub in_channels: usize,
329 pub patch: usize,
330 pub temporal_patch: usize,
331 pub merge: usize,
332 pub fullatt: Vec<usize>,
334 pub window_types: Vec<i64>,
336 pub band: usize,
338 pub use_sink: bool,
339 pub rms_eps: f64,
340}
341
342impl MimoVisionGeom {
343 pub fn from_config(cfg: &Value) -> Result<Self, String> {
344 let v = cfg
345 .get("vision_config")
346 .ok_or("config.json: no vision_config")?;
347 let w = "vision_config";
348 let depth = cfg_usize(v, "depth", w)?;
349 let heads = cfg_usize(v, "num_heads", w)?;
350 let window_types: Vec<i64> = match v.get("vit_window_attn_types") {
351 Some(Value::Array(a)) => a
352 .iter()
353 .map(|x| {
354 x.as_i64()
355 .ok_or("vision_config.vit_window_attn_types: non-integer")
356 })
357 .collect::<Result<_, _>>()?,
358 _ => vec![-1; depth],
359 };
360 if window_types.len() != depth {
361 return Err(format!(
362 "vision_config: {} vit_window_attn_types for depth {depth}",
363 window_types.len()
364 ));
365 }
366 let fullatt = match v.get("fullatt_block_indexes") {
367 Some(Value::Array(_)) => cfg_usize_list(v, "fullatt_block_indexes", w)?,
368 _ => Vec::new(),
369 };
370 if let Some(&bad) = fullatt.iter().find(|&&i| i >= depth) {
371 return Err(format!(
372 "vision_config: full-attention block {bad} >= depth {depth}"
373 ));
374 }
375 let band = match v.get("visual_token_window_size").and_then(Value::as_i64) {
376 Some(b) if b > 0 => b as usize,
377 _ => return Err("vision_config: visual_token_window_size must be positive".into()),
378 };
379 let g = Self {
380 depth,
381 hidden: cfg_usize(v, "hidden_size", w)?,
382 intermediate: cfg_usize(v, "intermediate_size", w)?,
383 heads,
384 kv_heads: cfg_usize_or(v, "num_key_value_heads", w, heads)?,
385 head_dim: cfg_usize_or(v, "qk_channels", w, 64)?,
386 out_hidden: cfg_usize(v, "out_hidden_size", w)?,
387 in_channels: match v.get("in_channels") {
388 Some(x) if !x.is_null() => cfg_usize(v, "in_channels", w)?,
389 _ => cfg_usize_or(v, "in_chans", w, 3)?,
390 },
391 patch: cfg_usize(v, "patch_size", w)?,
392 temporal_patch: cfg_usize(v, "temporal_patch_size", w)?,
393 merge: cfg_usize_or(v, "spatial_merge_size", w, 2)?,
394 fullatt,
395 window_types,
396 band,
397 use_sink: cfg_bool_or(v, "use_sink", false),
398 rms_eps: cfg_f64_or(v, "rms_norm_eps", 1e-6),
399 };
400 if g.heads == 0 || g.kv_heads == 0 || g.heads % g.kv_heads != 0 {
401 return Err(format!(
402 "vision_config: {} heads over {} kv heads",
403 g.heads, g.kv_heads
404 ));
405 }
406 if g.head_dim == 0 || g.head_dim % 4 != 0 {
407 return Err(format!(
408 "vision_config: head_dim {} must be a positive multiple of 4 (2-D RoPE)",
409 g.head_dim
410 ));
411 }
412 Ok(g)
413 }
414
415 pub fn qkv_rows(&self) -> usize {
417 (self.heads + 2 * self.kv_heads) * self.head_dim
418 }
419
420 pub fn merge_width(&self) -> usize {
422 self.hidden * self.merge * self.merge
423 }
424
425 pub fn patch_width(&self) -> usize {
427 self.in_channels * self.temporal_patch * self.patch * self.patch
428 }
429
430 pub fn has_sink(&self, i: usize) -> bool {
432 self.use_sink && !self.fullatt.contains(&i)
433 }
434}
435
436#[derive(Clone, Debug, PartialEq)]
438pub struct MimoAudioGeom {
439 pub channels: usize,
441 pub group_size: usize,
443 pub local_dim: usize,
445 pub local_layers: usize,
446 pub local_heads: usize,
447 pub local_head_dim: usize,
448 pub local_intermediate: usize,
449 pub rope_theta: f64,
450 pub partial_rotary: f64,
451 pub full_attention: bool,
453 pub post_norm: bool,
454 pub projection_layers: usize,
455 pub out_hidden: usize,
457 pub segment_size: usize,
458 pub speech_vocab: Vec<usize>,
460 pub speech_zero: Vec<usize>,
462}
463
464impl MimoAudioGeom {
465 pub fn from_config(cfg: &Value) -> Result<Self, String> {
466 let a = cfg
467 .get("audio_config")
468 .ok_or("config.json: no audio_config")?;
469 let w = "audio_config";
470 let channels = cfg_usize(a, "audio_channels", w)?;
471 let heads = cfg_usize(a, "input_local_attn_heads", w)?;
472 let dim = cfg_usize(a, "input_local_dim", w)?;
473 let g = Self {
474 channels,
475 group_size: cfg_usize(a, "group_size", w)?,
476 local_dim: dim,
477 local_layers: cfg_usize(a, "input_local_layers", w)?,
478 local_heads: heads,
479 local_head_dim: cfg_usize_or(a, "input_local_head_dim", w, dim / heads.max(1))?,
480 local_intermediate: cfg_usize(a, "input_local_intermediate_size", w)?,
481 rope_theta: cfg_f64_or(a, "rope_theta", 640000.0),
482 partial_rotary: cfg_f64_or(a, "partial_rotary_factor", 1.0),
483 full_attention: cfg_bool_or(a, "input_full_attention", true),
484 post_norm: cfg_bool_or(a, "add_post_norm", true),
485 projection_layers: cfg_usize_or(a, "projection_layers", w, 2)?,
486 out_hidden: cfg_usize(a, "out_hidden_size", w)?,
487 segment_size: cfg_usize_or(a, "audio_segment_size", w, 6000)?,
488 speech_vocab: per_channel(a, "speech_vocab_size", channels)?,
489 speech_zero: per_channel(a, "speech_zeroemb_idx", channels)?,
490 };
491 if g.local_heads * g.local_head_dim != g.local_dim {
492 return Err(format!(
493 "audio_config: {} heads × {} != input_local_dim {}",
494 g.local_heads, g.local_head_dim, g.local_dim
495 ));
496 }
497 if !matches!(g.projection_layers, 1 | 2) {
498 return Err(format!(
499 "audio_config: projection_layers {} (expected 1 or 2)",
500 g.projection_layers
501 ));
502 }
503 Ok(g)
504 }
505
506 pub fn proj_in(&self) -> usize {
508 self.local_dim * self.group_size
509 }
510}
511
512#[derive(Clone, Debug, PartialEq)]
514pub struct MimoAudioTokenizerGeom {
515 pub d_model: usize,
516 pub layers: usize,
517 pub heads: usize,
518 pub ffn: usize,
519 pub n_mels: usize,
520 pub kernel_size: usize,
521 pub stride: usize,
522 pub avg_pooler: usize,
523 pub skip_layer_id: usize,
526 pub causal: bool,
527 pub window: (i64, i64),
529 pub hybrid_attention: bool,
530 pub swa_per_block: usize,
531 pub rope_theta: f64,
532 pub codebook_sizes: Vec<usize>,
534 pub sampling_rate: usize,
535 pub hop_length: usize,
536 pub nfft: usize,
537 pub window_size: usize,
538}
539
540impl MimoAudioTokenizerGeom {
541 pub fn from_config(at: &Value) -> Result<Self, String> {
542 let w = "audio_tokenizer/config.json";
543 let win = at
544 .get("encoder_attn_window_size")
545 .and_then(Value::as_array)
546 .filter(|a| a.len() == 2)
547 .and_then(|a| Some((a[0].as_i64()?, a[1].as_i64()?)))
548 .ok_or_else(|| format!("{w}: encoder_attn_window_size must be [left, right]"))?;
549 let ln = at
550 .get("ln_type")
551 .and_then(Value::as_str)
552 .unwrap_or("LayerNorm");
553 if ln != "LayerNorm" {
554 return Err(format!("{w}: ln_type '{ln}' (only LayerNorm)"));
555 }
556 let num_q = cfg_usize(at, "num_quantizers", w)?;
557 let codebook_sizes = cfg_usize_list(at, "codebook_size", w)?;
558 if codebook_sizes.len() != num_q {
559 return Err(format!(
560 "{w}: {} codebook sizes for {num_q} quantizers",
561 codebook_sizes.len()
562 ));
563 }
564 let g = Self {
565 d_model: cfg_usize(at, "d_model", w)?,
566 layers: cfg_usize(at, "encoder_layers", w)?,
567 heads: cfg_usize(at, "encoder_attention_heads", w)?,
568 ffn: cfg_usize(at, "encoder_ffn_dim", w)?,
569 n_mels: cfg_usize(at, "n_mels", w)?,
570 kernel_size: cfg_usize(at, "kernel_size", w)?,
571 stride: cfg_usize(at, "stride_size", w)?,
572 avg_pooler: cfg_usize_or(at, "avg_pooler", w, 1)?,
573 skip_layer_id: cfg_usize(at, "encoder_skip_layer_id", w)?,
574 causal: cfg_bool_or(at, "encoder_causal", false),
575 window: win,
576 hybrid_attention: cfg_bool_or(at, "hybrid_attention", false),
577 swa_per_block: cfg_usize_or(at, "swa_per_block", w, 1)?,
578 rope_theta: cfg_f64_or(at, "rope_theta", 10000.0),
579 codebook_sizes,
580 sampling_rate: cfg_usize(at, "sampling_rate", w)?,
581 hop_length: cfg_usize(at, "hop_length", w)?,
582 nfft: cfg_usize(at, "nfft", w)?,
583 window_size: cfg_usize(at, "window_size", w)?,
584 };
585 if g.heads == 0 || g.d_model % g.heads != 0 {
586 return Err(format!("{w}: d_model {} over {} heads", g.d_model, g.heads));
587 }
588 if at.get("scale_embedding").and_then(Value::as_bool) == Some(true) {
589 return Err(format!("{w}: scale_embedding=true is not supported"));
590 }
591 Ok(g)
592 }
593}
594
595#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
597pub enum MimoTowerGroup {
598 Vision,
599 AudioEncoder,
600 SpeechEmbeddings,
601 AudioTokenizer,
602}
603
604impl MimoTowerGroup {
605 pub const ALL: [Self; 4] = [
606 Self::Vision,
607 Self::AudioEncoder,
608 Self::SpeechEmbeddings,
609 Self::AudioTokenizer,
610 ];
611
612 pub fn of(name: &str) -> Option<Self> {
615 if name.starts_with("visual.") {
616 Some(Self::Vision)
617 } else if name.starts_with("audio_encoder.") {
618 Some(Self::AudioEncoder)
619 } else if name.starts_with("speech_embeddings.") {
620 Some(Self::SpeechEmbeddings)
621 } else if name.starts_with("audio_tokenizer.encoder.") {
622 Some(Self::AudioTokenizer)
623 } else {
624 None
625 }
626 }
627
628 pub fn label(self) -> &'static str {
629 match self {
630 Self::Vision => "visual",
631 Self::AudioEncoder => "audio_encoder",
632 Self::SpeechEmbeddings => "speech_embeddings",
633 Self::AudioTokenizer => "audio_tokenizer",
634 }
635 }
636}
637
638pub fn is_codebook(name: &str) -> bool {
640 name.starts_with("audio_tokenizer.encoder.quantizer.") && name.ends_with("._codebook.embed")
641}
642
643pub mod names {
645 use super::AUDIO_TOKENIZER_PREFIX;
646
647 pub const VIS_PATCH_EMBED: &str = "visual.patch_embed.proj.weight";
648 pub const VIS_MERGER_NORM: &str = "visual.merger.ln_q.weight";
649 pub const VIS_MERGER_FC1: &str = "visual.merger.mlp.0.weight";
650 pub const VIS_MERGER_FC2: &str = "visual.merger.mlp.2.weight";
651 pub const AUD_LOCAL_NORM: &str = "audio_encoder.input_local_transformer.norm.weight";
652 pub const AUD_PROJ_FC1: &str = "audio_encoder.projection.mlp.0.weight";
653 pub const AUD_PROJ_FC2: &str = "audio_encoder.projection.mlp.2.weight";
654 pub const AUD_PROJ_SINGLE: &str = "audio_encoder.projection.weight";
656
657 pub fn vis_block(i: usize, leaf: &str) -> String {
659 format!("visual.blocks.{i}.{leaf}")
660 }
661
662 pub fn aud_layer(l: usize, leaf: &str) -> String {
664 format!("audio_encoder.input_local_transformer.layers.{l}.{leaf}")
665 }
666
667 pub fn speech_embedding(c: usize) -> String {
669 format!("speech_embeddings.{c}.weight")
670 }
671
672 pub fn at(leaf: &str) -> String {
674 format!("{AUDIO_TOKENIZER_PREFIX}encoder.{leaf}")
675 }
676
677 pub fn at_layer(l: usize, leaf: &str) -> String {
679 format!("{AUDIO_TOKENIZER_PREFIX}encoder.layers.{l}.{leaf}")
680 }
681
682 pub fn at_codebook(q: usize) -> String {
684 format!("{AUDIO_TOKENIZER_PREFIX}encoder.quantizer.vq.layers.{q}._codebook.embed")
685 }
686}
687
688pub fn mimo_tower_inventory(
692 config: &Value,
693 at_config: &Value,
694) -> Result<Vec<(String, Vec<usize>)>, String> {
695 let v = MimoVisionGeom::from_config(config)?;
696 let a = MimoAudioGeom::from_config(config)?;
697 let t = MimoAudioTokenizerGeom::from_config(at_config)?;
698 let mut out: Vec<(String, Vec<usize>)> = Vec::new();
699 let mut push = |n: String, s: Vec<usize>| out.push((n, s));
700
701 push(
703 names::VIS_PATCH_EMBED.into(),
704 vec![v.hidden, v.in_channels, v.temporal_patch, v.patch, v.patch],
705 );
706 let (h, i, hd) = (v.hidden, v.intermediate, v.head_dim);
707 for b in 0..v.depth {
708 let n = |leaf: &str| names::vis_block(b, leaf);
709 push(n("norm1.weight"), vec![h]);
710 push(n("norm2.weight"), vec![h]);
711 push(n("attn.qkv.weight"), vec![v.qkv_rows(), h]);
712 push(n("attn.qkv.bias"), vec![v.qkv_rows()]);
713 push(n("attn.proj.weight"), vec![h, v.heads * hd]);
714 push(n("attn.proj.bias"), vec![h]);
715 if v.has_sink(b) {
716 push(n("attn.sinks"), vec![v.heads]);
717 }
718 push(n("mlp.gate_proj.weight"), vec![i, h]);
719 push(n("mlp.gate_proj.bias"), vec![i]);
720 push(n("mlp.up_proj.weight"), vec![i, h]);
721 push(n("mlp.up_proj.bias"), vec![i]);
722 push(n("mlp.down_proj.weight"), vec![h, i]);
723 push(n("mlp.down_proj.bias"), vec![h]);
724 }
725 push(names::VIS_MERGER_NORM.into(), vec![h]);
726 push(
727 names::VIS_MERGER_FC1.into(),
728 vec![v.merge_width(), v.merge_width()],
729 );
730 push(
731 names::VIS_MERGER_FC2.into(),
732 vec![v.out_hidden, v.merge_width()],
733 );
734
735 let (d, ai) = (a.local_dim, a.local_intermediate);
737 let qd = a.local_heads * a.local_head_dim;
738 for l in 0..a.local_layers {
739 let n = |leaf: &str| names::aud_layer(l, leaf);
740 push(n("input_layernorm.weight"), vec![d]);
741 push(n("post_attention_layernorm.weight"), vec![d]);
742 for p in ["q_proj", "k_proj", "v_proj"] {
743 push(n(&format!("self_attn.{p}.weight")), vec![qd, d]);
744 push(n(&format!("self_attn.{p}.bias")), vec![qd]);
745 }
746 push(n("self_attn.o_proj.weight"), vec![d, qd]);
747 push(n("mlp.gate_proj.weight"), vec![ai, d]);
748 push(n("mlp.up_proj.weight"), vec![ai, d]);
749 push(n("mlp.down_proj.weight"), vec![d, ai]);
750 }
751 if a.post_norm {
752 push(names::AUD_LOCAL_NORM.into(), vec![d]);
753 }
754 if a.projection_layers == 2 {
755 push(
756 names::AUD_PROJ_FC1.into(),
757 vec![4 * a.proj_in(), a.proj_in()],
758 );
759 push(
760 names::AUD_PROJ_FC2.into(),
761 vec![a.out_hidden, 4 * a.proj_in()],
762 );
763 } else {
764 push(
765 names::AUD_PROJ_SINGLE.into(),
766 vec![a.out_hidden, a.proj_in()],
767 );
768 }
769 for c in 0..a.channels {
770 push(names::speech_embedding(c), vec![a.speech_vocab[c], d]);
771 }
772
773 let (m, f) = (t.d_model, t.ffn);
775 push(names::at("conv1.weight"), vec![m, t.n_mels, t.kernel_size]);
776 push(names::at("conv1.bias"), vec![m]);
777 push(names::at("conv2.weight"), vec![m, m, t.kernel_size]);
778 push(names::at("conv2.bias"), vec![m]);
779 for l in 0..t.layers {
780 let n = |leaf: &str| names::at_layer(l, leaf);
781 push(n("self_attn.k_proj.weight"), vec![m, m]);
782 for p in ["q_proj", "v_proj", "out_proj"] {
783 push(n(&format!("self_attn.{p}.weight")), vec![m, m]);
784 push(n(&format!("self_attn.{p}.bias")), vec![m]);
785 }
786 push(n("self_attn_layer_norm.weight"), vec![m]);
787 push(n("self_attn_layer_norm.bias"), vec![m]);
788 push(n("final_layer_norm.weight"), vec![m]);
789 push(n("final_layer_norm.bias"), vec![m]);
790 push(n("fc1.weight"), vec![f, m]);
791 push(n("fc1.bias"), vec![f]);
792 push(n("fc2.weight"), vec![m, f]);
793 push(n("fc2.bias"), vec![m]);
794 }
795 push(names::at("layer_norm.weight"), vec![m]);
796 push(names::at("layer_norm.bias"), vec![m]);
797 if t.avg_pooler != 1 {
798 push(
799 names::at("down_sample_layer.0.weight"),
800 vec![m, m, t.avg_pooler],
801 );
802 push(names::at("down_sample_norm.weight"), vec![m]);
803 push(names::at("down_sample_norm.bias"), vec![m]);
804 }
805 for (q, &bins) in t.codebook_sizes.iter().enumerate() {
806 push(names::at_codebook(q), vec![bins, m]);
807 }
808 Ok(out)
809}
810
811fn json_blob(model: &CmfModel, name: &str) -> Result<Value, String> {
812 let e = model
813 .tensor(name)
814 .ok_or_else(|| format!("{}: no '{name}' tensor", model.path.display()))?;
815 if e.dtype != TensorDtype::U8 {
816 return Err(format!("'{name}' is {:?}, expected U8 JSON bytes", e.dtype));
817 }
818 serde_json::from_slice(model.entry_bytes(e)).map_err(|e| format!("'{name}': {e}"))
819}
820
821#[derive(Clone, Copy, Debug, PartialEq, Eq)]
823pub enum MimoMmSource {
824 Companion,
826 SingleFile,
828}
829
830pub struct MimoMm {
835 model: Arc<CmfModel>,
836 pub source: MimoMmSource,
837 pub config: Value,
839 pub audio_tokenizer_config: Value,
841 pub vision: MimoVisionGeom,
842 pub audio: MimoAudioGeom,
843 pub audio_tokenizer: MimoAudioTokenizerGeom,
844 pub tokens: MimoTokenIds,
845 pub hidden_size: usize,
847 pub provenance: Option<Value>,
850}
851
852impl std::fmt::Debug for MimoMm {
853 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854 f.debug_struct("MimoMm")
855 .field("path", &self.model.path)
856 .field("source", &self.source)
857 .field("hidden_size", &self.hidden_size)
858 .finish()
859 }
860}
861
862impl MimoMm {
863 pub fn open(path: impl AsRef<Path>) -> Result<Self, String> {
865 let path = path.as_ref();
866 let model = CmfModel::open(path)
867 .map_err(|e| format!("open MiMo companion {}: {e}", path.display()))?;
868 Self::from_model(&Arc::new(model))
869 }
870
871 pub fn from_model(model: &Arc<CmfModel>) -> Result<Self, String> {
875 let arch = model.arch();
876 let where_ = model.path.display().to_string();
877 let prov = model
878 .header
879 .provenance
880 .as_ref()
881 .and_then(|p| p.get("mimo_mm"))
882 .cloned();
883 let source = match arch.arch_name.as_str() {
884 MIMO_MM_ARCH => {
885 let base = prov
886 .as_ref()
887 .and_then(|p| p.get("base_arch"))
888 .and_then(Value::as_str);
889 if base != Some(MIMO_BASE_ARCH) {
890 return Err(format!(
891 "{where_}: companion base_arch {base:?}, expected '{MIMO_BASE_ARCH}'"
892 ));
893 }
894 MimoMmSource::Companion
895 }
896 MIMO_BASE_ARCH => {
897 if model.tensor(MM_CONFIG_BLOB).is_none() {
898 return Err(format!(
899 "{where_}: text-only mimo_v2 file (no '{MM_CONFIG_BLOB}'); \
900 attach a {MIMO_MM_ARCH} companion instead"
901 ));
902 }
903 MimoMmSource::SingleFile
904 }
905 other => {
906 return Err(format!(
907 "{where_}: arch '{other}' is neither '{MIMO_MM_ARCH}' nor '{MIMO_BASE_ARCH}'"
908 ));
909 }
910 };
911 let config = json_blob(model, MM_CONFIG_BLOB)?;
912 let at_config = json_blob(model, AUDIO_TOKENIZER_CONFIG_BLOB)?;
913 let mt = config.get("model_type").and_then(Value::as_str);
914 if mt != Some(MIMO_BASE_ARCH) {
915 return Err(format!(
916 "{where_}: {MM_CONFIG_BLOB} model_type {mt:?}, expected '{MIMO_BASE_ARCH}'"
917 ));
918 }
919 let hidden = cfg_usize(&config, "hidden_size", "config.json")?;
920 if hidden != arch.hidden_size {
921 return Err(format!(
922 "{where_}: header hidden_size {} != config hidden_size {hidden}",
923 arch.hidden_size
924 ));
925 }
926 let tokens = MimoTokenIds::from_config(&config)?;
927 tokens.check_pinned(&where_)?;
928 let vision = MimoVisionGeom::from_config(&config)?;
929 let audio = MimoAudioGeom::from_config(&config)?;
930 let audio_tokenizer = MimoAudioTokenizerGeom::from_config(&at_config)?;
931 if vision.out_hidden != hidden || audio.out_hidden != hidden {
932 return Err(format!(
933 "{where_}: tower outputs (vision {}, audio {}) != LLM hidden {hidden}",
934 vision.out_hidden, audio.out_hidden
935 ));
936 }
937
938 let inv = mimo_tower_inventory(&config, &at_config)?;
941 let mut missing = Vec::new();
942 for (name, shape) in &inv {
943 match model.tensor(name) {
944 None => missing.push(name.clone()),
945 Some(e) if &e.shape != shape => {
946 return Err(format!(
947 "{where_}: '{name}' has shape {:?}, expected {shape:?}",
948 e.shape
949 ));
950 }
951 Some(e) if is_codebook(name) && e.dtype != TensorDtype::F32 => {
952 return Err(format!(
953 "{where_}: RVQ codebook '{name}' is {:?}; it must be F32",
954 e.dtype
955 ));
956 }
957 Some(_) => {}
958 }
959 }
960 if !missing.is_empty() {
961 return Err(format!(
962 "{where_}: {} tower tensors missing, e.g. {:?}",
963 missing.len(),
964 &missing[..missing.len().min(4)]
965 ));
966 }
967 let known: std::collections::HashSet<&str> = inv.iter().map(|(n, _)| n.as_str()).collect();
968 let unknown: Vec<&str> = model
969 .tensors
970 .iter()
971 .map(|t| t.name.as_str())
972 .filter(|n| MimoTowerGroup::of(n).is_some() && !known.contains(n))
973 .collect();
974 if !unknown.is_empty() {
975 return Err(format!(
976 "{where_}: {} unexpected tower tensors, e.g. {:?}",
977 unknown.len(),
978 &unknown[..unknown.len().min(4)]
979 ));
980 }
981 if source == MimoMmSource::Companion {
982 let extra: Vec<&str> = model
983 .tensors
984 .iter()
985 .map(|t| t.name.as_str())
986 .filter(|n| {
987 MimoTowerGroup::of(n).is_none()
988 && *n != MM_CONFIG_BLOB
989 && *n != AUDIO_TOKENIZER_CONFIG_BLOB
990 })
991 .collect();
992 if !extra.is_empty() {
993 return Err(format!(
994 "{where_}: companion carries non-tower tensors, e.g. {:?}",
995 &extra[..extra.len().min(4)]
996 ));
997 }
998 }
999 Ok(Self {
1000 model: model.clone(),
1001 source,
1002 config,
1003 audio_tokenizer_config: at_config,
1004 vision,
1005 audio,
1006 audio_tokenizer,
1007 tokens,
1008 hidden_size: hidden,
1009 provenance: prov,
1010 })
1011 }
1012
1013 pub fn discover(text_path: &Path, explicit: Option<&Path>) -> Result<Option<PathBuf>, String> {
1018 if let Some(p) = explicit {
1019 if !p.is_file() {
1020 return Err(format!("--mm {}: no such file", p.display()));
1021 }
1022 return Ok(Some(p.to_path_buf()));
1023 }
1024 let dir = match text_path.parent() {
1025 Some(d) if !d.as_os_str().is_empty() => d.to_path_buf(),
1026 _ => PathBuf::from("."),
1027 };
1028 let file = text_path
1029 .file_name()
1030 .map(|f| f.to_string_lossy().into_owned())
1031 .unwrap_or_default();
1032 if file.ends_with(MM_SUFFIX) {
1033 return Ok(None);
1034 }
1035 let stem = file.strip_suffix(".cmf").unwrap_or(&file);
1036 let stem = strip_shard_suffix(stem);
1037 let sibling = dir.join(format!("{stem}{MM_SUFFIX}"));
1038 if sibling.is_file() {
1039 return Ok(Some(sibling));
1040 }
1041 let mut found: Vec<PathBuf> = std::fs::read_dir(&dir)
1042 .map_err(|e| format!("scan {} for {MM_SUFFIX}: {e}", dir.display()))?
1043 .filter_map(|e| e.ok().map(|e| e.path()))
1044 .filter(|p| {
1045 p.is_file()
1046 && p.file_name()
1047 .is_some_and(|f| f.to_string_lossy().ends_with(MM_SUFFIX))
1048 })
1049 .collect();
1050 found.sort();
1051 match found.len() {
1052 0 => Ok(None),
1053 1 => Ok(found.pop()),
1054 _ => Err(format!(
1055 "{} MiMo companions next to {} ({}); pass --mm PATH",
1056 found.len(),
1057 text_path.display(),
1058 found
1059 .iter()
1060 .map(|p| p.display().to_string())
1061 .collect::<Vec<_>>()
1062 .join(", ")
1063 )),
1064 }
1065 }
1066
1067 pub fn attach(text: &Arc<CmfModel>, explicit: Option<&Path>) -> Result<Option<Self>, String> {
1073 if text.arch().arch_name != MIMO_BASE_ARCH {
1074 if let Some(p) = explicit {
1075 return Err(format!(
1076 "--mm {}: the model is '{}', not {MIMO_BASE_ARCH}",
1077 p.display(),
1078 text.arch().arch_name
1079 ));
1080 }
1081 return Ok(None);
1082 }
1083 if explicit.is_none() && text.tensor(MM_CONFIG_BLOB).is_some() {
1084 let mm = Self::from_model(text)?;
1085 mm.validate_text_model(text)?;
1086 return Ok(Some(mm));
1087 }
1088 let Some(path) = Self::discover(&text.path, explicit)? else {
1089 return Ok(None);
1090 };
1091 let mm = Self::open(&path)?;
1092 mm.validate_text_model(text)?;
1093 Ok(Some(mm))
1094 }
1095
1096 pub fn validate_text_model(&self, text: &CmfModel) -> Result<(), String> {
1099 let vocab = text.vocab.as_deref().ok_or_else(|| {
1100 format!(
1101 "{}: no embedded tokenizer — the MiMo special ids cannot be checked",
1102 text.path.display()
1103 )
1104 })?;
1105 let tok = Tokenizer::from_bytes(vocab)
1106 .map_err(|e| format!("{}: tokenizer: {e}", text.path.display()))?;
1107 self.validate_text(text, &tok)
1108 }
1109
1110 pub fn validate_text(&self, text: &CmfModel, tok: &Tokenizer) -> Result<(), String> {
1114 let a = text.arch();
1115 let where_ = text.path.display();
1116 if a.arch_name != MIMO_BASE_ARCH {
1117 return Err(format!(
1118 "{where_}: arch '{}' — MiMo towers attach only to {MIMO_BASE_ARCH}",
1119 a.arch_name
1120 ));
1121 }
1122 if a.hidden_size != self.hidden_size {
1123 return Err(format!(
1124 "{where_}: text hidden_size {} != tower hidden_size {} ({})",
1125 a.hidden_size,
1126 self.hidden_size,
1127 self.model.path.display()
1128 ));
1129 }
1130 for (name, want) in self.tokens.named() {
1131 match tok.token_to_id(name) {
1132 Some(got) if got == want => {}
1133 got => {
1134 return Err(format!(
1135 "{where_}: tokenizer maps {name} to {got:?}, the towers need {want}"
1136 ));
1137 }
1138 }
1139 if want as usize >= a.vocab_size {
1140 return Err(format!(
1141 "{where_}: special id {want} ({name}) outside vocab_size {}",
1142 a.vocab_size
1143 ));
1144 }
1145 }
1146 Ok(())
1147 }
1148
1149 pub fn model(&self) -> &Arc<CmfModel> {
1151 &self.model
1152 }
1153
1154 pub fn has(&self, name: &str) -> bool {
1155 self.model.tensor(name).is_some()
1156 }
1157
1158 pub fn entry(&self, name: &str) -> Result<&TensorEntry, String> {
1160 self.model
1161 .tensor(name)
1162 .ok_or_else(|| format!("{}: missing tensor '{name}'", self.model.path.display()))
1163 }
1164
1165 pub fn dtype(&self, name: &str) -> Option<TensorDtype> {
1167 self.model.tensor(name).map(|e| e.dtype)
1168 }
1169
1170 pub fn linear(&self, name: &str, rows: usize, cols: usize) -> Result<QTensor, String> {
1173 let e = self.entry(name)?;
1174 if e.shape != [rows, cols] {
1175 return Err(format!(
1176 "'{name}' has shape {:?}, expected [{rows}, {cols}]",
1177 e.shape
1178 ));
1179 }
1180 QTensor::from_model(&self.model, name)
1181 }
1182
1183 #[allow(dead_code)] pub(crate) fn proj(
1187 &self,
1188 name: &str,
1189 rows: usize,
1190 cols: usize,
1191 ) -> Result<crate::dit::Proj, String> {
1192 self.linear(name, rows, cols)?;
1193 crate::dit::Proj::from_model(&self.model, name)
1194 }
1195
1196 pub fn f32(&self, name: &str) -> Result<Vec<f32>, String> {
1198 crate::dit::cmf_f32(&self.model, name)
1199 }
1200
1201 pub fn f32_shaped(&self, name: &str, shape: &[usize]) -> Result<Vec<f32>, String> {
1203 let e = self.entry(name)?;
1204 if e.shape != shape {
1205 return Err(format!(
1206 "'{name}' has shape {:?}, expected {shape:?}",
1207 e.shape
1208 ));
1209 }
1210 self.f32(name)
1211 }
1212
1213 pub fn group_codec(&self, group: MimoTowerGroup) -> Option<&str> {
1215 self.provenance
1216 .as_ref()?
1217 .get("codec")?
1218 .get(group.label())?
1219 .get("matrices")?
1220 .as_str()
1221 }
1222}
1223
1224fn strip_shard_suffix(stem: &str) -> &str {
1226 let b = stem.as_bytes();
1227 if b.len() > 15 {
1229 let tail = &stem[stem.len() - 15..];
1230 let t = tail.as_bytes();
1231 let digits = |r: std::ops::Range<usize>| t[r].iter().all(u8::is_ascii_digit);
1232 if t[0] == b'-' && digits(1..6) && &tail[6..10] == "-of-" && digits(10..15) {
1233 return &stem[..stem.len() - 15];
1234 }
1235 }
1236 stem
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242
1243 #[test]
1244 fn shard_suffix_is_stripped_only_when_exact() {
1245 assert_eq!(strip_shard_suffix("mimo-q4tp-00001-of-00004"), "mimo-q4tp");
1246 assert_eq!(strip_shard_suffix("mimo-q4tp"), "mimo-q4tp");
1247 assert_eq!(strip_shard_suffix("x-0001-of-00004"), "x-0001-of-00004");
1248 }
1249
1250 #[test]
1251 fn per_channel_accepts_scalar_string_and_list() {
1252 let c = serde_json::json!({"a": "1280", "b": 7, "c": "1-2-3"});
1253 assert_eq!(per_channel(&c, "a", 2).unwrap(), vec![1280, 1280]);
1254 assert_eq!(per_channel(&c, "b", 3).unwrap(), vec![7, 7, 7]);
1255 assert_eq!(per_channel(&c, "c", 3).unwrap(), vec![1, 2, 3]);
1256 assert!(per_channel(&c, "c", 2).is_err());
1257 assert!(per_channel(&c, "missing", 2).is_err());
1258 }
1259}