1use crate::hash::hash64;
19use crate::mask::{MaskCatalog, TaskMask, decode_masks_section, encode_masks_section};
20use crate::quant::expected_nbytes;
21use crate::types::{ModelArch, QuantType, TensorDtype};
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::fs::File;
25use std::io::{self, BufWriter, Write};
26use std::path::{Path, PathBuf};
27
28pub const CMF_MAGIC: [u8; 4] = *b"CMF\x01";
29pub const CMF_VERSION: u32 = 2;
30pub const ENVELOPE_LEN: usize = 128;
31pub const DATA_ALIGNMENT: u64 = 4096;
33pub const TENSOR_ALIGNMENT: u64 = 64;
35pub const LARGE_TENSOR_MIN: u64 = 16 * 1024;
43pub const LARGE_TENSOR_ALIGN: u64 = 4096;
48pub const DIR_RECORD_LEN: usize = 56;
50pub const DIR_MAX_NDIM: usize = 6;
51
52pub mod features {
55 pub const TENSOR_DIR: u32 = 1 << 0;
56 pub const BINARY_MASKS: u32 = 1 << 1;
57 pub const QUANT_2F: u32 = 1 << 2;
58 pub const DELTA_MASKS: u32 = 1 << 3;
59 pub const HOT_PACKS: u32 = 1 << 4;
60
61 pub const SUPPORTED: u32 = TENSOR_DIR | BINARY_MASKS | QUANT_2F;
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct CmfHeader {
69 #[serde(default = "default_format")]
70 pub format: String,
71 pub version: u32,
72 pub arch: ModelArch,
73 pub quant_type: QuantType,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub provenance: Option<serde_json::Value>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub tokenizer_config: Option<TokenizerBundle>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub section_hashes: Option<SectionHashes>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub skills: Vec<SkillRecord>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub shard: Option<ShardInfo>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub calibration: Option<Calibration>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Calibration {
110 pub temperature: f32,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub ece_before: Option<f32>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub ece_after: Option<f32>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ShardInfo {
120 pub no: usize,
121 pub count: usize,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct SelectionDescriptor {
128 pub metric: String,
130 pub phi_layer: usize,
132 pub mean: String,
134 pub basis: String,
136 pub rank: usize,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SkillRecord {
142 pub id: String,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub name: Option<String>,
145 #[serde(default)]
147 pub layers: Vec<usize>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub selection: Option<SelectionDescriptor>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub input_mask_task: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub quality: Option<serde_json::Value>,
158}
159
160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
163pub struct SectionHashes {
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub masks: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub vocab: Option<String>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub index: Option<String>,
170}
171
172#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
174pub struct TokenizerBundle {
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub chat_template: Option<String>,
178 #[serde(default)]
180 pub eos_token_ids: Vec<u32>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub bos_token_id: Option<u32>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub pad_token_id: Option<u32>,
185}
186
187fn default_format() -> String {
188 "cmf".to_string()
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct TensorEntry {
194 pub name: String,
195 pub dtype: TensorDtype,
196 pub shape: Vec<usize>,
197 pub off: u64,
199 pub nbytes: u64,
200 pub shard: usize,
203 pub hash: u64,
205}
206
207impl TensorEntry {
208 pub fn n_elems(&self) -> usize {
209 self.shape.iter().product()
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct TensorSpec {
216 pub name: String,
217 pub dtype: TensorDtype,
218 pub shape: Vec<usize>,
219 pub data: Vec<u8>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct SparseIndexEntry {
225 pub task_id: u32,
226 pub layer_idx: usize,
227 pub active_ffn_groups: Vec<u16>,
229 pub active_heads: Vec<u8>,
231}
232
233#[derive(Debug, Clone, Copy, Default)]
235struct Envelope {
236 required_features: u32,
237 header: (u64, u64),
238 dir: (u64, u64),
239 data: (u64, u64),
240 masks: (u64, u64),
241 vocab: (u64, u64),
242 index: (u64, u64),
243 header_hash: u64,
245 dir_hash: u64,
247}
248
249enum Backing {
250 Mmap(memmap2::Mmap),
251 Owned(Vec<u8>),
252}
253
254impl Backing {
255 fn bytes(&self) -> &[u8] {
256 match self {
257 Backing::Mmap(m) => m,
258 Backing::Owned(v) => v,
259 }
260 }
261}
262
263pub struct CmfModel {
265 pub path: PathBuf,
266 pub header: CmfHeader,
267 pub required_features: u32,
268 pub tensors: Vec<TensorEntry>,
269 by_name: HashMap<u64, u32>,
276 name_overflow: Vec<u32>,
277 pub masks: MaskCatalog,
278 pub sparse_index: Vec<SparseIndexEntry>,
279 pub vocab: Option<Vec<u8>>,
281 backing: Backing,
282 data_off: u64,
283 envelope: Envelope,
284 extra_shards: Vec<(Backing, u64)>,
287}
288
289impl std::fmt::Debug for CmfModel {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 f.debug_struct("CmfModel")
292 .field("path", &self.path)
293 .field("arch", &self.header.arch.arch_name)
294 .field("tensors", &self.tensors.len())
295 .field("masks", &self.masks.masks.len())
296 .finish()
297 }
298}
299
300impl CmfModel {
301 pub fn open(path: impl AsRef<Path>) -> Result<Self, CmfError> {
304 let path = path.as_ref().to_path_buf();
305 if !path.exists() {
306 return Err(CmfError::FileNotFound(path.display().to_string()));
307 }
308 let file = File::open(&path)?;
309 let file_len = file.metadata()?.len();
310
311 let backing = match unsafe { memmap2::MmapOptions::new().map(&file) } {
312 Ok(m) => {
313 #[cfg(unix)]
323 {
324 if std::env::var("CMF_MMAP_ADVISE")
325 .map(|v| v != "0")
326 .unwrap_or(true)
327 {
328 let _ = m.advise(memmap2::Advice::WillNeed);
329 }
330 if std::env::var("CMF_MLOCK")
331 .map(|v| v == "1")
332 .unwrap_or(false)
333 {
334 if let Err(e) = m.lock() {
335 tracing::warn!(
336 "CMF_MLOCK=1: mlock refused ({e}) — continuing unpinned"
337 );
338 }
339 }
340 }
341 Backing::Mmap(m)
342 }
343 Err(e) => {
344 tracing::warn!("mmap failed ({e}), reading file into memory");
345 Backing::Owned(std::fs::read(&path)?)
346 }
347 };
348
349 let env = Self::parse_envelope(backing.bytes(), file_len)?;
350
351 let bytes = backing.bytes();
352 let section = |off: u64, len: u64| -> &[u8] { &bytes[off as usize..(off + len) as usize] };
353
354 let header: CmfHeader = serde_json::from_slice(section(env.header.0, env.header.1))
356 .map_err(|e| CmfError::Parse(format!("header JSON: {e}")))?;
357
358 let tensors = Self::decode_directory(section(env.dir.0, env.dir.1))?;
360 for t in &tensors {
361 if t.off % TENSOR_ALIGNMENT != 0 {
362 return Err(CmfError::Bounds(format!(
363 "tensor '{}': offset {} not 64-aligned",
364 t.name, t.off
365 )));
366 }
367 let tensor_end = t.off.checked_add(t.nbytes).ok_or_else(|| {
368 CmfError::Bounds(format!("tensor '{}': offset + length overflows", t.name))
369 })?;
370 if tensor_end > env.data.1 {
371 return Err(CmfError::Bounds(format!(
372 "tensor '{}': [{}, {}) exceeds data section ({} bytes)",
373 t.name, t.off, tensor_end, env.data.1
374 )));
375 }
376 t.shape
377 .iter()
378 .try_fold(1usize, |n, &dim| n.checked_mul(dim))
379 .ok_or_else(|| {
380 CmfError::Bounds(format!(
381 "tensor '{}': shape product overflows usize",
382 t.name
383 ))
384 })?;
385 if let Some(expect) = expected_nbytes(t.dtype, &t.shape) {
386 if expect as u64 != t.nbytes {
387 return Err(CmfError::Bounds(format!(
388 "tensor '{}': nbytes {} != expected {} for {:?}{:?}",
389 t.name, t.nbytes, expect, t.dtype, t.shape
390 )));
391 }
392 }
393 if matches!(t.dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
396 let payload = section(env.data.0 + t.off, t.nbytes);
397 crate::quant::validate_payload(t.dtype, &t.shape, payload)
398 .map_err(|e| CmfError::Bounds(format!("tensor '{}': {e}", t.name)))?;
399 }
400 }
401 let mut by_name: HashMap<u64, u32> = HashMap::with_capacity(tensors.len());
405 let mut name_overflow: Vec<u32> = Vec::new();
406 for i in 0..tensors.len() {
407 let h = hash64(tensors[i].name.as_bytes());
408 match by_name.get(&h) {
409 Some(&j) if tensors[j as usize].name == tensors[i].name => {
410 return Err(CmfError::Parse(format!(
411 "duplicate tensor name '{}' in directory",
412 tensors[i].name
413 )));
414 }
415 Some(_) => name_overflow.push(i as u32), None => {
417 by_name.insert(h, i as u32);
418 }
419 }
420 }
421
422 let masks = if env.masks.1 > 0 {
424 decode_masks_section(section(env.masks.0, env.masks.1), &header.arch)
425 .map_err(CmfError::Parse)?
426 } else {
427 MaskCatalog::empty()
428 };
429
430 let vocab = if env.vocab.1 > 0 {
432 Some(section(env.vocab.0, env.vocab.1).to_vec())
433 } else {
434 None
435 };
436
437 let sparse_index = if env.index.1 > 0 {
439 decode_sparse_index(section(env.index.0, env.index.1))?
440 } else {
441 vec![]
442 };
443
444 tracing::info!(
445 "Opened CMF v2: {} | {} tensors | {} masks | vocab {} | {:.1} MB",
446 header.arch.arch_name,
447 tensors.len(),
448 masks.masks.len(),
449 if vocab.is_some() { "embedded" } else { "none" },
450 file_len as f64 / 1e6
451 );
452
453 Ok(Self {
454 path,
455 header,
456 required_features: env.required_features,
457 tensors,
458 by_name,
459 name_overflow,
460 masks,
461 sparse_index,
462 vocab,
463 backing,
464 data_off: env.data.0,
465 envelope: env,
466 extra_shards: Vec::new(),
467 })
468 }
469
470 pub fn open_sharded(path: impl AsRef<Path>) -> Result<Self, CmfError> {
474 let path = path.as_ref();
475 let mut first = Self::open(path)?;
476 let Some(info) = first.header.shard.clone() else {
477 return Ok(first); };
479 if info.no != 1 {
480 return Err(CmfError::Parse(format!(
481 "open shard 1, not {} (of {})",
482 info.no, info.count
483 )));
484 }
485 let name = path
486 .file_name()
487 .and_then(|n| n.to_str())
488 .ok_or_else(|| CmfError::Parse("bad shard path".into()))?;
489 let tag1 = format!("-{:05}-of-{:05}.cmf", 1, info.count);
490 if !name.ends_with(&tag1) {
491 return Err(CmfError::Parse(format!(
492 "shard file must end with '{tag1}' (got '{name}')"
493 )));
494 }
495 let stem = &name[..name.len() - tag1.len()];
496 for no in 2..=info.count {
497 let sib = path.with_file_name(format!("{stem}-{:05}-of-{:05}.cmf", no, info.count));
498 let sh = Self::open(&sib)?;
499 match &sh.header.shard {
500 Some(si) if si.no == no && si.count == info.count => {}
501 other => {
502 return Err(CmfError::Parse(format!(
503 "{}: wrong shard coords {other:?}",
504 sib.display()
505 )));
506 }
507 }
508 let shard_idx = first.extra_shards.len() + 1;
509 first.extra_shards.push((sh.backing, sh.envelope.data.0));
510 for mut t in sh.tensors {
511 t.shard = shard_idx;
512 let idx = first.tensors.len() as u32;
513 let h = hash64(t.name.as_bytes());
514 match first.by_name.get(&h) {
515 Some(&j) if first.tensors[j as usize].name == t.name => {
516 return Err(CmfError::Parse(format!(
517 "duplicate tensor name '{}' across shards",
518 t.name
519 )));
520 }
521 Some(_) => first.name_overflow.push(idx),
522 None => {
523 first.by_name.insert(h, idx);
524 }
525 }
526 first.tensors.push(t);
527 }
528 }
529 tracing::info!(
530 "sharded model: {} files, {} tensors total",
531 info.count,
532 first.tensors.len()
533 );
534 Ok(first)
535 }
536
537 fn parse_envelope(bytes: &[u8], file_len: u64) -> Result<Envelope, CmfError> {
538 if bytes.len() < ENVELOPE_LEN {
539 return Err(CmfError::Bounds(format!(
540 "file too small for CMF envelope: {} bytes",
541 bytes.len()
542 )));
543 }
544 if bytes[0..4] != CMF_MAGIC {
545 return Err(CmfError::InvalidMagic);
546 }
547 let u32le = |o: usize| u32::from_le_bytes(bytes[o..o + 4].try_into().unwrap());
548 let u64le = |o: usize| u64::from_le_bytes(bytes[o..o + 8].try_into().unwrap());
549
550 let version = u32le(4);
551 if version != CMF_VERSION {
552 return Err(CmfError::UnsupportedVersion(version));
553 }
554 let _flags = u32le(8); let required_features = u32le(12);
556 let unknown = required_features & !features::SUPPORTED;
557 if unknown != 0 {
558 return Err(CmfError::UnsupportedFeature(unknown));
559 }
560
561 let env = Envelope {
562 required_features,
563 header: (u64le(0x10), u64le(0x18)),
564 dir: (u64le(0x20), u64le(0x28)),
565 data: (u64le(0x30), u64le(0x38)),
566 masks: (u64le(0x40), u64le(0x48)),
567 vocab: (u64le(0x50), u64le(0x58)),
568 index: (u64le(0x60), u64le(0x68)),
569 header_hash: u64le(0x70),
570 dir_hash: u64le(0x78),
571 };
572
573 for (name, (off, len), required) in [
574 ("header", env.header, true),
575 ("dir", env.dir, true),
576 ("data", env.data, false),
577 ("masks", env.masks, false),
578 ("vocab", env.vocab, false),
579 ("index", env.index, false),
580 ] {
581 if required && len == 0 {
582 return Err(CmfError::Bounds(format!("section '{name}' is required")));
583 }
584 if len > 0
585 && off
586 .checked_add(len)
587 .map(|end| end > file_len)
588 .unwrap_or(true)
589 {
590 return Err(CmfError::Bounds(format!(
591 "section '{name}' [{off}, {}) exceeds file ({file_len} bytes)",
592 off.saturating_add(len)
593 )));
594 }
595 if len > 0
596 && (usize::try_from(off).is_err()
597 || usize::try_from(len).is_err()
598 || usize::try_from(off + len).is_err())
599 {
600 return Err(CmfError::Bounds(format!(
601 "section '{name}' cannot be addressed on this platform"
602 )));
603 }
604 }
605 if env.data.1 > 0 && env.data.0 % DATA_ALIGNMENT != 0 {
606 return Err(CmfError::Bounds(format!(
607 "data section offset {} not {}-aligned",
608 env.data.0, DATA_ALIGNMENT
609 )));
610 }
611 Ok(env)
612 }
613
614 fn decode_directory(bytes: &[u8]) -> Result<Vec<TensorEntry>, CmfError> {
615 if bytes.len() < 16 {
616 return Err(CmfError::Parse("tensor directory too short".into()));
617 }
618 let count = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize;
619 let pool_off = u64::from_le_bytes(bytes[8..16].try_into().unwrap()) as usize;
620 let records_len = count
621 .checked_mul(DIR_RECORD_LEN)
622 .ok_or_else(|| CmfError::Parse("tensor directory record count overflows".into()))?;
623 let records_end = 16usize
624 .checked_add(records_len)
625 .ok_or_else(|| CmfError::Parse("tensor directory size overflows".into()))?;
626 if records_end > bytes.len() || pool_off > bytes.len() || pool_off < records_end {
627 return Err(CmfError::Parse(format!(
628 "tensor directory malformed: count={count}, pool_off={pool_off}, len={}",
629 bytes.len()
630 )));
631 }
632 let pool = &bytes[pool_off..];
633
634 let mut out = Vec::with_capacity(count);
635 for i in 0..count {
636 let r = &bytes[16 + i * DIR_RECORD_LEN..16 + (i + 1) * DIR_RECORD_LEN];
637 let name_off = u32::from_le_bytes(r[0..4].try_into().unwrap()) as usize;
638 let name_len = u16::from_le_bytes(r[4..6].try_into().unwrap()) as usize;
639 let dtype_id = r[6];
640 let ndim = r[7] as usize;
641 if ndim > DIR_MAX_NDIM {
642 return Err(CmfError::Parse(format!("tensor #{i}: ndim {ndim} > 6")));
643 }
644 let mut shape = Vec::with_capacity(ndim);
645 for d in 0..ndim {
646 shape.push(
647 u32::from_le_bytes(r[8 + d * 4..12 + d * 4].try_into().unwrap()) as usize,
648 );
649 }
650 let off = u64::from_le_bytes(r[32..40].try_into().unwrap());
651 let nbytes = u64::from_le_bytes(r[40..48].try_into().unwrap());
652 let hash = u64::from_le_bytes(r[48..56].try_into().unwrap());
653
654 let name_end = name_off
655 .checked_add(name_len)
656 .ok_or_else(|| CmfError::Parse(format!("tensor #{i}: name range overflows")))?;
657 if name_end > pool.len() {
658 return Err(CmfError::Parse(format!("tensor #{i}: name out of pool")));
659 }
660 let name = std::str::from_utf8(&pool[name_off..name_end])
661 .map_err(|_| CmfError::Parse(format!("tensor #{i}: name is not UTF-8")))?
662 .to_string();
663 let dtype = TensorDtype::from_id(dtype_id).ok_or(CmfError::UnknownDtype(dtype_id))?;
664
665 out.push(TensorEntry {
666 name,
667 dtype,
668 shape,
669 off,
670 nbytes,
671 shard: 0,
672 hash,
673 });
674 }
675 Ok(out)
676 }
677
678 pub fn arch(&self) -> &ModelArch {
681 &self.header.arch
682 }
683
684 pub fn tensor(&self, name: &str) -> Option<&TensorEntry> {
685 self.tensor_index(name).map(|i| &self.tensors[i])
686 }
687
688 pub fn tensor_index(&self, name: &str) -> Option<usize> {
694 let h = hash64(name.as_bytes());
695 if let Some(&i) = self.by_name.get(&h) {
696 if self.tensors[i as usize].name == name {
697 return Some(i as usize);
698 }
699 }
700 self.name_overflow
701 .iter()
702 .copied()
703 .find(|&i| self.tensors[i as usize].name == name)
704 .map(|i| i as usize)
705 }
706
707 pub fn resolve_tensor(&self, name: &str, skill: Option<&str>) -> Option<&TensorEntry> {
711 if let Some(sid) = skill {
712 if let Some(t) = self.tensor(&format!("skill.{sid}.{name}")) {
713 return Some(t);
714 }
715 }
716 self.tensor(name)
717 }
718
719 pub fn skill_tensors(&self, skill_id: &str) -> impl Iterator<Item = &TensorEntry> {
722 let prefix = format!("skill.{skill_id}.");
723 self.tensors
724 .iter()
725 .filter(move |t| t.name.starts_with(&prefix))
726 }
727
728 pub fn tensor_bytes(&self, name: &str) -> Result<&[u8], CmfError> {
730 let entry = self
731 .tensor(name)
732 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
733 Ok(self.entry_bytes(entry))
734 }
735
736 pub fn primary_bytes(&self) -> &[u8] {
739 self.backing.bytes()
740 }
741
742 pub fn entry_abs_offset(&self, entry: &TensorEntry) -> Option<usize> {
745 (entry.shard == 0).then(|| (self.data_off + entry.off) as usize)
746 }
747
748 pub fn entry_bytes(&self, entry: &TensorEntry) -> &[u8] {
749 let (bytes, data_off) = if entry.shard == 0 {
750 (self.backing.bytes(), self.data_off)
751 } else {
752 let (b, o) = &self.extra_shards[entry.shard - 1];
753 (b.bytes(), *o)
754 };
755 let start = (data_off + entry.off) as usize;
756 &bytes[start..start + entry.nbytes as usize]
757 }
758
759 pub fn layer_tensors(&self, layer_idx: usize) -> Vec<&TensorEntry> {
761 let prefix = format!("model.layers.{layer_idx}.");
762 self.tensors
763 .iter()
764 .filter(|t| t.name.starts_with(&prefix))
765 .collect()
766 }
767
768 pub fn total_param_count(&self) -> u64 {
770 self.tensors
771 .iter()
772 .filter(|t| t.shape.len() >= 2)
773 .map(|t| t.n_elems() as u64)
774 .sum()
775 }
776
777 pub fn verify(&self) -> Vec<String> {
780 let mut problems = Vec::new();
781
782 let bytes = self.backing.bytes();
784 let env = &self.envelope;
785 let sect = |(off, len): (u64, u64)| &bytes[off as usize..(off + len) as usize];
786 let check = |name: &str, stored: u64, span: (u64, u64)| -> Option<String> {
787 if stored != 0 && span.1 > 0 {
788 let actual = hash64(sect(span));
789 if actual != stored {
790 return Some(format!(
791 "section '{name}': hash mismatch (stored {stored:016x}, \
792 actual {actual:016x})"
793 ));
794 }
795 }
796 None
797 };
798 problems.extend(check("header", env.header_hash, env.header));
799 problems.extend(check("dir", env.dir_hash, env.dir));
800 if let Some(sh) = &self.header.section_hashes {
801 for (name, hex, span) in [
802 ("masks", &sh.masks, env.masks),
803 ("vocab", &sh.vocab, env.vocab),
804 ("index", &sh.index, env.index),
805 ] {
806 if let Some(hex) = hex {
807 match u64::from_str_radix(hex, 16) {
808 Ok(stored) => problems.extend(check(name, stored, span)),
809 Err(_) => {
810 problems.push(format!("section '{name}': malformed hash '{hex}'"))
811 }
812 }
813 }
814 }
815 }
816
817 for t in &self.tensors {
818 let actual = hash64(self.entry_bytes(t));
819 if actual != t.hash {
820 problems.push(format!(
821 "tensor '{}': hash mismatch (stored {:016x}, actual {:016x})",
822 t.name, t.hash, actual
823 ));
824 }
825 }
826 problems
827 }
828
829 pub fn compute_active_size(&self, mask: &TaskMask) -> u64 {
832 let arch = &self.header.arch;
833 let mut total = 0u64;
834 for li in 0..arch.num_layers {
835 if !mask.layer_alive(li) {
836 continue;
837 }
838 let ffn_frac = mask.ffn_active_count(li) as f64 / arch.intermediate_size.max(1) as f64;
839 let head_frac =
840 mask.active_head_count(li) as f64 / arch.num_attention_heads.max(1) as f64;
841 for t in self.layer_tensors(li) {
842 let frac = if t.name.contains(".mlp.") {
843 ffn_frac
844 } else if t.name.contains(".self_attn.") {
845 head_frac
846 } else {
847 1.0
848 };
849 total += (t.nbytes as f64 * frac) as u64;
850 }
851 }
852 total
853 }
854
855 pub fn write(
860 path: impl AsRef<Path>,
861 header: &CmfHeader,
862 tensors: &[TensorSpec],
863 masks: Option<&MaskCatalog>,
864 vocab: Option<&[u8]>,
865 ) -> Result<(), CmfError> {
866 let path = path.as_ref();
867
868 let mut entries = Vec::with_capacity(tensors.len());
870 let mut data_cursor = 0u64;
871 for t in tensors {
872 if t.shape.len() > DIR_MAX_NDIM {
873 return Err(CmfError::Parse(format!(
874 "tensor '{}': ndim {} > 6",
875 t.name,
876 t.shape.len()
877 )));
878 }
879 if let Some(expect) = expected_nbytes(t.dtype, &t.shape) {
880 if expect != t.data.len() {
881 return Err(CmfError::Bounds(format!(
882 "tensor '{}': data {} bytes != expected {} for {:?}{:?}",
883 t.name,
884 t.data.len(),
885 expect,
886 t.dtype,
887 t.shape
888 )));
889 }
890 }
891 let align = if t.data.len() as u64 >= LARGE_TENSOR_MIN {
892 LARGE_TENSOR_ALIGN
893 } else {
894 TENSOR_ALIGNMENT
895 };
896 data_cursor = align_to(data_cursor, align);
897 entries.push(TensorEntry {
898 name: t.name.clone(),
899 dtype: t.dtype,
900 shape: t.shape.clone(),
901 off: data_cursor,
902 nbytes: t.data.len() as u64,
903 shard: 0,
904 hash: hash64(&t.data),
905 });
906 data_cursor += t.data.len() as u64;
907 }
908 let data_len = data_cursor;
909
910 let dir_bytes = Self::encode_directory(&entries);
911
912 let masks_bytes = match masks {
913 Some(catalog) if !catalog.masks.is_empty() => {
914 Some(encode_masks_section(catalog, &header.arch).map_err(CmfError::Parse)?)
915 }
916 _ => None,
917 };
918 let index_bytes = match masks {
919 Some(catalog) if !catalog.masks.is_empty() => {
920 let idx = build_sparse_index(catalog, &header.arch);
921 Some(encode_sparse_index(&idx))
922 }
923 _ => None,
924 };
925
926 let hex = |b: Option<&[u8]>| b.map(|b| format!("{:016x}", hash64(b)));
929 let mut header = header.clone();
930 if masks_bytes.is_some() || vocab.is_some() || index_bytes.is_some() {
931 header.section_hashes = Some(SectionHashes {
932 masks: hex(masks_bytes.as_deref()),
933 vocab: hex(vocab),
934 index: hex(index_bytes.as_deref()),
935 });
936 }
937 let header_json =
938 serde_json::to_vec(&header).map_err(|e| CmfError::Parse(format!("header: {e}")))?;
939
940 let mut required_features = features::TENSOR_DIR;
941 if masks_bytes.is_some() {
942 required_features |= features::BINARY_MASKS;
943 }
944 if entries
945 .iter()
946 .any(|t| matches!(t.dtype, TensorDtype::Q8_2f | TensorDtype::Vbit))
947 {
948 required_features |= features::QUANT_2F;
949 }
950
951 let header_off = ENVELOPE_LEN as u64;
953 let dir_off = header_off + header_json.len() as u64;
954 let data_off = align_to(dir_off + dir_bytes.len() as u64, DATA_ALIGNMENT);
955 let masks_off = data_off + data_len;
956 let masks_len = masks_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
957 let vocab_off = masks_off + masks_len;
958 let vocab_len = vocab.map(|b| b.len() as u64).unwrap_or(0);
959 let index_off = vocab_off + vocab_len;
960 let index_len = index_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
961
962 let mut env = Vec::with_capacity(ENVELOPE_LEN);
964 env.extend_from_slice(&CMF_MAGIC);
965 env.extend_from_slice(&CMF_VERSION.to_le_bytes());
966 env.extend_from_slice(&0u32.to_le_bytes()); env.extend_from_slice(&required_features.to_le_bytes());
968 for (off, len) in [
969 (header_off, header_json.len() as u64),
970 (dir_off, dir_bytes.len() as u64),
971 (data_off, data_len),
972 (if masks_len > 0 { masks_off } else { 0 }, masks_len),
973 (if vocab_len > 0 { vocab_off } else { 0 }, vocab_len),
974 (if index_len > 0 { index_off } else { 0 }, index_len),
975 ] {
976 env.extend_from_slice(&off.to_le_bytes());
977 env.extend_from_slice(&len.to_le_bytes());
978 }
979 env.extend_from_slice(&hash64(&header_json).to_le_bytes());
981 env.extend_from_slice(&hash64(&dir_bytes).to_le_bytes());
982 env.resize(ENVELOPE_LEN, 0);
983
984 let mut f = BufWriter::new(File::create(path)?);
986 f.write_all(&env)?;
987 f.write_all(&header_json)?;
988 f.write_all(&dir_bytes)?;
989 let mut pos = dir_off + dir_bytes.len() as u64;
990 f.write_all(&zeros((data_off - pos) as usize))?;
991 pos = data_off;
992 for (spec, entry) in tensors.iter().zip(&entries) {
993 let target = data_off + entry.off;
994 f.write_all(&zeros((target - pos) as usize))?;
995 f.write_all(&spec.data)?;
996 pos = target + spec.data.len() as u64;
997 }
998 debug_assert_eq!(pos, data_off + data_len);
999 if let Some(mb) = &masks_bytes {
1000 f.write_all(mb)?;
1001 }
1002 if let Some(vb) = vocab {
1003 f.write_all(vb)?;
1004 }
1005 if let Some(ib) = &index_bytes {
1006 f.write_all(ib)?;
1007 }
1008 f.flush()?;
1009
1010 tracing::info!(
1011 "Wrote CMF v2: {} ({} tensors, {} masks, {:.1} MB)",
1012 path.display(),
1013 entries.len(),
1014 masks.map(|m| m.masks.len()).unwrap_or(0),
1015 std::fs::metadata(path)?.len() as f64 / 1e6
1016 );
1017 Ok(())
1018 }
1019
1020 fn encode_directory(entries: &[TensorEntry]) -> Vec<u8> {
1021 let mut pool = Vec::new();
1022 let mut name_offs = Vec::with_capacity(entries.len());
1023 for e in entries {
1024 name_offs.push((pool.len() as u32, e.name.len() as u16));
1025 pool.extend_from_slice(e.name.as_bytes());
1026 }
1027 let pool_off = 16 + entries.len() * DIR_RECORD_LEN;
1028
1029 let mut out = Vec::with_capacity(pool_off + pool.len());
1030 out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
1031 out.extend_from_slice(&(pool_off as u64).to_le_bytes());
1032 for (e, (noff, nlen)) in entries.iter().zip(&name_offs) {
1033 out.extend_from_slice(&noff.to_le_bytes());
1034 out.extend_from_slice(&nlen.to_le_bytes());
1035 out.push(e.dtype.id());
1036 out.push(e.shape.len() as u8);
1037 for d in 0..DIR_MAX_NDIM {
1038 out.extend_from_slice(&(e.shape.get(d).copied().unwrap_or(0) as u32).to_le_bytes());
1039 }
1040 out.extend_from_slice(&e.off.to_le_bytes());
1041 out.extend_from_slice(&e.nbytes.to_le_bytes());
1042 out.extend_from_slice(&e.hash.to_le_bytes());
1043 }
1044 out.extend_from_slice(&pool);
1045 out
1046 }
1047}
1048
1049fn align_to(x: u64, a: u64) -> u64 {
1050 x.div_ceil(a) * a
1051}
1052
1053fn zeros(n: usize) -> Vec<u8> {
1054 vec![0u8; n]
1055}
1056
1057pub fn build_sparse_index(catalog: &MaskCatalog, arch: &ModelArch) -> Vec<SparseIndexEntry> {
1062 let mut out = Vec::new();
1063 for m in &catalog.masks {
1064 for li in 0..arch.num_layers {
1065 if !m.layer_alive(li) {
1066 continue;
1067 }
1068 let mut groups = Vec::new();
1069 if let Some(bits) = m.ffn_masks.get(li) {
1070 let n_groups = arch.intermediate_size.div_ceil(32);
1071 for g in 0..n_groups {
1072 let active = bits[g * 4..(g * 4 + 4).min(bits.len())]
1074 .iter()
1075 .any(|&b| b != 0);
1076 if active {
1077 groups.push(g as u16);
1078 }
1079 }
1080 }
1081 let mut heads = Vec::new();
1082 if let Some(bits) = m.head_masks.get(li) {
1083 for h in 0..arch.num_attention_heads {
1084 if bits
1085 .get(h / 8)
1086 .map(|b| b & (1 << (h % 8)) != 0)
1087 .unwrap_or(false)
1088 {
1089 heads.push(h as u8);
1090 }
1091 }
1092 }
1093 out.push(SparseIndexEntry {
1094 task_id: m.task_id,
1095 layer_idx: li,
1096 active_ffn_groups: groups,
1097 active_heads: heads,
1098 });
1099 }
1100 }
1101 out
1102}
1103
1104pub fn encode_sparse_index(entries: &[SparseIndexEntry]) -> Vec<u8> {
1107 let mut out = Vec::new();
1108 out.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1109 out.extend_from_slice(&0u32.to_le_bytes());
1110 for e in entries {
1111 out.extend_from_slice(&e.task_id.to_le_bytes());
1112 out.extend_from_slice(&(e.layer_idx as u32).to_le_bytes());
1113 out.extend_from_slice(&(e.active_ffn_groups.len() as u32).to_le_bytes());
1114 out.extend_from_slice(&(e.active_heads.len() as u32).to_le_bytes());
1115 for g in &e.active_ffn_groups {
1116 out.extend_from_slice(&g.to_le_bytes());
1117 }
1118 out.extend_from_slice(&e.active_heads);
1119 while out.len() % 4 != 0 {
1120 out.push(0);
1121 }
1122 }
1123 out
1124}
1125
1126pub fn decode_sparse_index(bytes: &[u8]) -> Result<Vec<SparseIndexEntry>, CmfError> {
1127 let err = |msg: &str| CmfError::Parse(format!("sparse index: {msg}"));
1128 if bytes.len() < 8 {
1129 return Err(err("too short"));
1130 }
1131 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1132 let mut pos = 8usize;
1133 let mut out = Vec::with_capacity(n);
1134 for _ in 0..n {
1135 if pos + 16 > bytes.len() {
1136 return Err(err("entry header out of bounds"));
1137 }
1138 let task_id = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
1139 let layer_idx = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().unwrap()) as usize;
1140 let n_groups = u32::from_le_bytes(bytes[pos + 8..pos + 12].try_into().unwrap()) as usize;
1141 let n_heads = u32::from_le_bytes(bytes[pos + 12..pos + 16].try_into().unwrap()) as usize;
1142 pos += 16;
1143 if pos + n_groups * 2 + n_heads > bytes.len() {
1144 return Err(err("entry data out of bounds"));
1145 }
1146 let mut groups = Vec::with_capacity(n_groups);
1147 for g in 0..n_groups {
1148 groups.push(u16::from_le_bytes(
1149 bytes[pos + g * 2..pos + g * 2 + 2].try_into().unwrap(),
1150 ));
1151 }
1152 pos += n_groups * 2;
1153 let heads = bytes[pos..pos + n_heads].to_vec();
1154 pos += n_heads;
1155 pos = pos.div_ceil(4) * 4;
1156 out.push(SparseIndexEntry {
1157 task_id,
1158 layer_idx,
1159 active_ffn_groups: groups,
1160 active_heads: heads,
1161 });
1162 }
1163 Ok(out)
1164}
1165
1166#[derive(Debug, thiserror::Error)]
1168pub enum CmfError {
1169 #[error("File not found: {0}")]
1170 FileNotFound(String),
1171 #[error("Invalid CMF magic bytes")]
1172 InvalidMagic,
1173 #[error("Unsupported CMF version: {0}")]
1174 UnsupportedVersion(u32),
1175 #[error("File requires unsupported features (bits {0:#x})")]
1176 UnsupportedFeature(u32),
1177 #[error("Unknown tensor dtype id: {0}")]
1178 UnknownDtype(u8),
1179 #[error("Tensor not found: {0}")]
1180 MissingTensor(String),
1181 #[error("Bounds error: {0}")]
1182 Bounds(String),
1183 #[error("IO error: {0}")]
1184 Io(#[from] io::Error),
1185 #[error("Parse error: {0}")]
1186 Parse(String),
1187}