1use crate::hash::hash64;
19use crate::mask::{decode_masks_section, encode_masks_section, MaskCatalog, TaskMask};
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 DIR_RECORD_LEN: usize = 56;
37pub const DIR_MAX_NDIM: usize = 6;
38
39pub mod features {
42 pub const TENSOR_DIR: u32 = 1 << 0;
43 pub const BINARY_MASKS: u32 = 1 << 1;
44 pub const QUANT_2F: u32 = 1 << 2;
45 pub const DELTA_MASKS: u32 = 1 << 3;
46 pub const HOT_PACKS: u32 = 1 << 4;
47
48 pub const SUPPORTED: u32 = TENSOR_DIR | BINARY_MASKS | QUANT_2F;
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct CmfHeader {
56 #[serde(default = "default_format")]
57 pub format: String,
58 pub version: u32,
59 pub arch: ModelArch,
60 pub quant_type: QuantType,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub provenance: Option<serde_json::Value>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub tokenizer_config: Option<TokenizerBundle>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub section_hashes: Option<SectionHashes>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub skills: Vec<SkillRecord>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub shard: Option<ShardInfo>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub calibration: Option<Calibration>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Calibration {
97 pub temperature: f32,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub ece_before: Option<f32>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub ece_after: Option<f32>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ShardInfo {
107 pub no: usize,
108 pub count: usize,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct SelectionDescriptor {
115 pub metric: String,
117 pub phi_layer: usize,
119 pub mean: String,
121 pub basis: String,
123 pub rank: usize,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SkillRecord {
129 pub id: String,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub name: Option<String>,
132 #[serde(default)]
134 pub layers: Vec<usize>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub selection: Option<SelectionDescriptor>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub input_mask_task: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub quality: Option<serde_json::Value>,
145}
146
147#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150pub struct SectionHashes {
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub masks: Option<String>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub vocab: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub index: Option<String>,
157}
158
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
161pub struct TokenizerBundle {
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub chat_template: Option<String>,
165 #[serde(default)]
167 pub eos_token_ids: Vec<u32>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub bos_token_id: Option<u32>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub pad_token_id: Option<u32>,
172}
173
174fn default_format() -> String {
175 "cmf".to_string()
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct TensorEntry {
181 pub name: String,
182 pub dtype: TensorDtype,
183 pub shape: Vec<usize>,
184 pub off: u64,
186 pub nbytes: u64,
187 pub shard: usize,
190 pub hash: u64,
192}
193
194impl TensorEntry {
195 pub fn n_elems(&self) -> usize {
196 self.shape.iter().product()
197 }
198}
199
200#[derive(Debug, Clone)]
202pub struct TensorSpec {
203 pub name: String,
204 pub dtype: TensorDtype,
205 pub shape: Vec<usize>,
206 pub data: Vec<u8>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct SparseIndexEntry {
212 pub task_id: u32,
213 pub layer_idx: usize,
214 pub active_ffn_groups: Vec<u16>,
216 pub active_heads: Vec<u8>,
218}
219
220#[derive(Debug, Clone, Copy, Default)]
222struct Envelope {
223 required_features: u32,
224 header: (u64, u64),
225 dir: (u64, u64),
226 data: (u64, u64),
227 masks: (u64, u64),
228 vocab: (u64, u64),
229 index: (u64, u64),
230 header_hash: u64,
232 dir_hash: u64,
234}
235
236enum Backing {
237 Mmap(memmap2::Mmap),
238 Owned(Vec<u8>),
239}
240
241impl Backing {
242 fn bytes(&self) -> &[u8] {
243 match self {
244 Backing::Mmap(m) => m,
245 Backing::Owned(v) => v,
246 }
247 }
248}
249
250pub struct CmfModel {
252 pub path: PathBuf,
253 pub header: CmfHeader,
254 pub required_features: u32,
255 pub tensors: Vec<TensorEntry>,
256 by_name: HashMap<String, usize>,
257 pub masks: MaskCatalog,
258 pub sparse_index: Vec<SparseIndexEntry>,
259 pub vocab: Option<Vec<u8>>,
261 backing: Backing,
262 data_off: u64,
263 envelope: Envelope,
264 extra_shards: Vec<(Backing, u64)>,
267}
268
269impl std::fmt::Debug for CmfModel {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("CmfModel")
272 .field("path", &self.path)
273 .field("arch", &self.header.arch.arch_name)
274 .field("tensors", &self.tensors.len())
275 .field("masks", &self.masks.masks.len())
276 .finish()
277 }
278}
279
280impl CmfModel {
281 pub fn open(path: impl AsRef<Path>) -> Result<Self, CmfError> {
284 let path = path.as_ref().to_path_buf();
285 if !path.exists() {
286 return Err(CmfError::FileNotFound(path.display().to_string()));
287 }
288 let file = File::open(&path)?;
289 let file_len = file.metadata()?.len();
290
291 let backing = match unsafe { memmap2::MmapOptions::new().map(&file) } {
292 Ok(m) => Backing::Mmap(m),
293 Err(e) => {
294 tracing::warn!("mmap failed ({e}), reading file into memory");
295 Backing::Owned(std::fs::read(&path)?)
296 }
297 };
298
299 let env = Self::parse_envelope(backing.bytes(), file_len)?;
300
301 let bytes = backing.bytes();
302 let section = |off: u64, len: u64| -> &[u8] {
303 &bytes[off as usize..(off + len) as usize]
304 };
305
306 let header: CmfHeader = serde_json::from_slice(section(env.header.0, env.header.1))
308 .map_err(|e| CmfError::Parse(format!("header JSON: {e}")))?;
309
310 let tensors = Self::decode_directory(section(env.dir.0, env.dir.1))?;
312 for t in &tensors {
313 if t.off % TENSOR_ALIGNMENT != 0 {
314 return Err(CmfError::Bounds(format!(
315 "tensor '{}': offset {} not 64-aligned",
316 t.name, t.off
317 )));
318 }
319 if t.off + t.nbytes > env.data.1 {
320 return Err(CmfError::Bounds(format!(
321 "tensor '{}': [{}, {}) exceeds data section ({} bytes)",
322 t.name,
323 t.off,
324 t.off + t.nbytes,
325 env.data.1
326 )));
327 }
328 if let Some(expect) = expected_nbytes(t.dtype, &t.shape) {
329 if expect as u64 != t.nbytes {
330 return Err(CmfError::Bounds(format!(
331 "tensor '{}': nbytes {} != expected {} for {:?}{:?}",
332 t.name, t.nbytes, expect, t.dtype, t.shape
333 )));
334 }
335 }
336 }
337 let by_name = tensors
338 .iter()
339 .enumerate()
340 .map(|(i, t)| (t.name.clone(), i))
341 .collect();
342
343 let masks = if env.masks.1 > 0 {
345 decode_masks_section(section(env.masks.0, env.masks.1), &header.arch)
346 .map_err(CmfError::Parse)?
347 } else {
348 MaskCatalog::empty()
349 };
350
351 let vocab = if env.vocab.1 > 0 {
353 Some(section(env.vocab.0, env.vocab.1).to_vec())
354 } else {
355 None
356 };
357
358 let sparse_index = if env.index.1 > 0 {
360 decode_sparse_index(section(env.index.0, env.index.1))?
361 } else {
362 vec![]
363 };
364
365 tracing::info!(
366 "Opened CMF v2: {} | {} tensors | {} masks | vocab {} | {:.1} MB",
367 header.arch.arch_name,
368 tensors.len(),
369 masks.masks.len(),
370 if vocab.is_some() { "embedded" } else { "none" },
371 file_len as f64 / 1e6
372 );
373
374 Ok(Self {
375 path,
376 header,
377 required_features: env.required_features,
378 tensors,
379 by_name,
380 masks,
381 sparse_index,
382 vocab,
383 backing,
384 data_off: env.data.0,
385 envelope: env,
386 extra_shards: Vec::new(),
387 })
388 }
389
390 pub fn open_sharded(path: impl AsRef<Path>) -> Result<Self, CmfError> {
394 let path = path.as_ref();
395 let mut first = Self::open(path)?;
396 let Some(info) = first.header.shard.clone() else {
397 return Ok(first); };
399 if info.no != 1 {
400 return Err(CmfError::Parse(format!(
401 "open shard 1, not {} (of {})",
402 info.no, info.count
403 )));
404 }
405 let name = path
406 .file_name()
407 .and_then(|n| n.to_str())
408 .ok_or_else(|| CmfError::Parse("bad shard path".into()))?;
409 let tag1 = format!("-{:05}-of-{:05}.cmf", 1, info.count);
410 if !name.ends_with(&tag1) {
411 return Err(CmfError::Parse(format!(
412 "shard file must end with '{tag1}' (got '{name}')"
413 )));
414 }
415 let stem = &name[..name.len() - tag1.len()];
416 for no in 2..=info.count {
417 let sib = path.with_file_name(format!(
418 "{stem}-{:05}-of-{:05}.cmf",
419 no, info.count
420 ));
421 let sh = Self::open(&sib)?;
422 match &sh.header.shard {
423 Some(si) if si.no == no && si.count == info.count => {}
424 other => {
425 return Err(CmfError::Parse(format!(
426 "{}: wrong shard coords {other:?}",
427 sib.display()
428 )));
429 }
430 }
431 let shard_idx = first.extra_shards.len() + 1;
432 first.extra_shards.push((sh.backing, sh.envelope.data.0));
433 for mut t in sh.tensors {
434 t.shard = shard_idx;
435 first.by_name.insert(t.name.clone(), first.tensors.len());
436 first.tensors.push(t);
437 }
438 }
439 tracing::info!(
440 "sharded model: {} files, {} tensors total",
441 info.count,
442 first.tensors.len()
443 );
444 Ok(first)
445 }
446
447 fn parse_envelope(bytes: &[u8], file_len: u64) -> Result<Envelope, CmfError> {
448 if bytes.len() < ENVELOPE_LEN {
449 return Err(CmfError::Bounds(format!(
450 "file too small for CMF envelope: {} bytes",
451 bytes.len()
452 )));
453 }
454 if bytes[0..4] != CMF_MAGIC {
455 return Err(CmfError::InvalidMagic);
456 }
457 let u32le = |o: usize| u32::from_le_bytes(bytes[o..o + 4].try_into().unwrap());
458 let u64le = |o: usize| u64::from_le_bytes(bytes[o..o + 8].try_into().unwrap());
459
460 let version = u32le(4);
461 if version != CMF_VERSION {
462 return Err(CmfError::UnsupportedVersion(version));
463 }
464 let _flags = u32le(8); let required_features = u32le(12);
466 let unknown = required_features & !features::SUPPORTED;
467 if unknown != 0 {
468 return Err(CmfError::UnsupportedFeature(unknown));
469 }
470
471 let env = Envelope {
472 required_features,
473 header: (u64le(0x10), u64le(0x18)),
474 dir: (u64le(0x20), u64le(0x28)),
475 data: (u64le(0x30), u64le(0x38)),
476 masks: (u64le(0x40), u64le(0x48)),
477 vocab: (u64le(0x50), u64le(0x58)),
478 index: (u64le(0x60), u64le(0x68)),
479 header_hash: u64le(0x70),
480 dir_hash: u64le(0x78),
481 };
482
483 for (name, (off, len), required) in [
484 ("header", env.header, true),
485 ("dir", env.dir, true),
486 ("data", env.data, false),
487 ("masks", env.masks, false),
488 ("vocab", env.vocab, false),
489 ("index", env.index, false),
490 ] {
491 if required && len == 0 {
492 return Err(CmfError::Bounds(format!("section '{name}' is required")));
493 }
494 if len > 0 && off.checked_add(len).map(|end| end > file_len).unwrap_or(true) {
495 return Err(CmfError::Bounds(format!(
496 "section '{name}' [{off}, {}) exceeds file ({file_len} bytes)",
497 off.saturating_add(len)
498 )));
499 }
500 }
501 if env.data.1 > 0 && env.data.0 % DATA_ALIGNMENT != 0 {
502 return Err(CmfError::Bounds(format!(
503 "data section offset {} not {}-aligned",
504 env.data.0, DATA_ALIGNMENT
505 )));
506 }
507 Ok(env)
508 }
509
510 fn decode_directory(bytes: &[u8]) -> Result<Vec<TensorEntry>, CmfError> {
511 if bytes.len() < 16 {
512 return Err(CmfError::Parse("tensor directory too short".into()));
513 }
514 let count = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize;
515 let pool_off = u64::from_le_bytes(bytes[8..16].try_into().unwrap()) as usize;
516 let records_end = 16 + count * DIR_RECORD_LEN;
517 if records_end > bytes.len() || pool_off > bytes.len() || pool_off < records_end {
518 return Err(CmfError::Parse(format!(
519 "tensor directory malformed: count={count}, pool_off={pool_off}, len={}",
520 bytes.len()
521 )));
522 }
523 let pool = &bytes[pool_off..];
524
525 let mut out = Vec::with_capacity(count);
526 for i in 0..count {
527 let r = &bytes[16 + i * DIR_RECORD_LEN..16 + (i + 1) * DIR_RECORD_LEN];
528 let name_off = u32::from_le_bytes(r[0..4].try_into().unwrap()) as usize;
529 let name_len = u16::from_le_bytes(r[4..6].try_into().unwrap()) as usize;
530 let dtype_id = r[6];
531 let ndim = r[7] as usize;
532 if ndim > DIR_MAX_NDIM {
533 return Err(CmfError::Parse(format!("tensor #{i}: ndim {ndim} > 6")));
534 }
535 let mut shape = Vec::with_capacity(ndim);
536 for d in 0..ndim {
537 shape.push(u32::from_le_bytes(r[8 + d * 4..12 + d * 4].try_into().unwrap()) as usize);
538 }
539 let off = u64::from_le_bytes(r[32..40].try_into().unwrap());
540 let nbytes = u64::from_le_bytes(r[40..48].try_into().unwrap());
541 let hash = u64::from_le_bytes(r[48..56].try_into().unwrap());
542
543 if name_off + name_len > pool.len() {
544 return Err(CmfError::Parse(format!("tensor #{i}: name out of pool")));
545 }
546 let name = std::str::from_utf8(&pool[name_off..name_off + name_len])
547 .map_err(|_| CmfError::Parse(format!("tensor #{i}: name is not UTF-8")))?
548 .to_string();
549 let dtype = TensorDtype::from_id(dtype_id).ok_or(CmfError::UnknownDtype(dtype_id))?;
550
551 out.push(TensorEntry {
552 name,
553 dtype,
554 shape,
555 off,
556 nbytes,
557 shard: 0,
558 hash,
559 });
560 }
561 Ok(out)
562 }
563
564 pub fn arch(&self) -> &ModelArch {
567 &self.header.arch
568 }
569
570 pub fn tensor(&self, name: &str) -> Option<&TensorEntry> {
571 self.by_name.get(name).map(|&i| &self.tensors[i])
572 }
573
574 pub fn resolve_tensor(&self, name: &str, skill: Option<&str>) -> Option<&TensorEntry> {
578 if let Some(sid) = skill {
579 if let Some(t) = self.tensor(&format!("skill.{sid}.{name}")) {
580 return Some(t);
581 }
582 }
583 self.tensor(name)
584 }
585
586 pub fn skill_tensors(&self, skill_id: &str) -> impl Iterator<Item = &TensorEntry> {
589 let prefix = format!("skill.{skill_id}.");
590 self.tensors.iter().filter(move |t| t.name.starts_with(&prefix))
591 }
592
593 pub fn tensor_bytes(&self, name: &str) -> Result<&[u8], CmfError> {
595 let entry = self
596 .tensor(name)
597 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
598 Ok(self.entry_bytes(entry))
599 }
600
601 pub fn primary_bytes(&self) -> &[u8] {
604 self.backing.bytes()
605 }
606
607 pub fn entry_abs_offset(&self, entry: &TensorEntry) -> Option<usize> {
610 (entry.shard == 0).then(|| (self.data_off + entry.off) as usize)
611 }
612
613 pub fn entry_bytes(&self, entry: &TensorEntry) -> &[u8] {
614 let (bytes, data_off) = if entry.shard == 0 {
615 (self.backing.bytes(), self.data_off)
616 } else {
617 let (b, o) = &self.extra_shards[entry.shard - 1];
618 (b.bytes(), *o)
619 };
620 let start = (data_off + entry.off) as usize;
621 &bytes[start..start + entry.nbytes as usize]
622 }
623
624 pub fn layer_tensors(&self, layer_idx: usize) -> Vec<&TensorEntry> {
626 let prefix = format!("model.layers.{layer_idx}.");
627 self.tensors
628 .iter()
629 .filter(|t| t.name.starts_with(&prefix))
630 .collect()
631 }
632
633 pub fn total_param_count(&self) -> u64 {
635 self.tensors
636 .iter()
637 .filter(|t| t.shape.len() >= 2)
638 .map(|t| t.n_elems() as u64)
639 .sum()
640 }
641
642 pub fn verify(&self) -> Vec<String> {
645 let mut problems = Vec::new();
646
647 let bytes = self.backing.bytes();
649 let env = &self.envelope;
650 let sect = |(off, len): (u64, u64)| &bytes[off as usize..(off + len) as usize];
651 let check = |name: &str, stored: u64, span: (u64, u64)| -> Option<String> {
652 if stored != 0 && span.1 > 0 {
653 let actual = hash64(sect(span));
654 if actual != stored {
655 return Some(format!(
656 "section '{name}': hash mismatch (stored {stored:016x}, \
657 actual {actual:016x})"
658 ));
659 }
660 }
661 None
662 };
663 problems.extend(check("header", env.header_hash, env.header));
664 problems.extend(check("dir", env.dir_hash, env.dir));
665 if let Some(sh) = &self.header.section_hashes {
666 for (name, hex, span) in [
667 ("masks", &sh.masks, env.masks),
668 ("vocab", &sh.vocab, env.vocab),
669 ("index", &sh.index, env.index),
670 ] {
671 if let Some(hex) = hex {
672 match u64::from_str_radix(hex, 16) {
673 Ok(stored) => problems.extend(check(name, stored, span)),
674 Err(_) => problems.push(format!(
675 "section '{name}': malformed hash '{hex}'"
676 )),
677 }
678 }
679 }
680 }
681
682 for t in &self.tensors {
683 let actual = hash64(self.entry_bytes(t));
684 if actual != t.hash {
685 problems.push(format!(
686 "tensor '{}': hash mismatch (stored {:016x}, actual {:016x})",
687 t.name, t.hash, actual
688 ));
689 }
690 }
691 problems
692 }
693
694 pub fn compute_active_size(&self, mask: &TaskMask) -> u64 {
697 let arch = &self.header.arch;
698 let mut total = 0u64;
699 for li in 0..arch.num_layers {
700 if !mask.layer_alive(li) {
701 continue;
702 }
703 let ffn_frac = mask.ffn_active_count(li) as f64 / arch.intermediate_size.max(1) as f64;
704 let head_frac =
705 mask.active_head_count(li) as f64 / arch.num_attention_heads.max(1) as f64;
706 for t in self.layer_tensors(li) {
707 let frac = if t.name.contains(".mlp.") {
708 ffn_frac
709 } else if t.name.contains(".self_attn.") {
710 head_frac
711 } else {
712 1.0
713 };
714 total += (t.nbytes as f64 * frac) as u64;
715 }
716 }
717 total
718 }
719
720 pub fn write(
725 path: impl AsRef<Path>,
726 header: &CmfHeader,
727 tensors: &[TensorSpec],
728 masks: Option<&MaskCatalog>,
729 vocab: Option<&[u8]>,
730 ) -> Result<(), CmfError> {
731 let path = path.as_ref();
732
733 let mut entries = Vec::with_capacity(tensors.len());
735 let mut data_cursor = 0u64;
736 for t in tensors {
737 if t.shape.len() > DIR_MAX_NDIM {
738 return Err(CmfError::Parse(format!(
739 "tensor '{}': ndim {} > 6",
740 t.name,
741 t.shape.len()
742 )));
743 }
744 if let Some(expect) = expected_nbytes(t.dtype, &t.shape) {
745 if expect != t.data.len() {
746 return Err(CmfError::Bounds(format!(
747 "tensor '{}': data {} bytes != expected {} for {:?}{:?}",
748 t.name,
749 t.data.len(),
750 expect,
751 t.dtype,
752 t.shape
753 )));
754 }
755 }
756 data_cursor = align_to(data_cursor, TENSOR_ALIGNMENT);
757 entries.push(TensorEntry {
758 name: t.name.clone(),
759 dtype: t.dtype,
760 shape: t.shape.clone(),
761 off: data_cursor,
762 nbytes: t.data.len() as u64,
763 shard: 0,
764 hash: hash64(&t.data),
765 });
766 data_cursor += t.data.len() as u64;
767 }
768 let data_len = data_cursor;
769
770 let dir_bytes = Self::encode_directory(&entries);
771
772 let masks_bytes = match masks {
773 Some(catalog) if !catalog.masks.is_empty() => {
774 Some(encode_masks_section(catalog, &header.arch).map_err(CmfError::Parse)?)
775 }
776 _ => None,
777 };
778 let index_bytes = match masks {
779 Some(catalog) if !catalog.masks.is_empty() => {
780 let idx = build_sparse_index(catalog, &header.arch);
781 Some(encode_sparse_index(&idx))
782 }
783 _ => None,
784 };
785
786 let hex = |b: Option<&[u8]>| b.map(|b| format!("{:016x}", hash64(b)));
789 let mut header = header.clone();
790 if masks_bytes.is_some() || vocab.is_some() || index_bytes.is_some() {
791 header.section_hashes = Some(SectionHashes {
792 masks: hex(masks_bytes.as_deref()),
793 vocab: hex(vocab),
794 index: hex(index_bytes.as_deref()),
795 });
796 }
797 let header_json =
798 serde_json::to_vec(&header).map_err(|e| CmfError::Parse(format!("header: {e}")))?;
799
800 let mut required_features = features::TENSOR_DIR;
801 if masks_bytes.is_some() {
802 required_features |= features::BINARY_MASKS;
803 }
804 if entries
805 .iter()
806 .any(|t| matches!(t.dtype, TensorDtype::Q8_2f | TensorDtype::Vbit))
807 {
808 required_features |= features::QUANT_2F;
809 }
810
811 let header_off = ENVELOPE_LEN as u64;
813 let dir_off = header_off + header_json.len() as u64;
814 let data_off = align_to(dir_off + dir_bytes.len() as u64, DATA_ALIGNMENT);
815 let masks_off = data_off + data_len;
816 let masks_len = masks_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
817 let vocab_off = masks_off + masks_len;
818 let vocab_len = vocab.map(|b| b.len() as u64).unwrap_or(0);
819 let index_off = vocab_off + vocab_len;
820 let index_len = index_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
821
822 let mut env = Vec::with_capacity(ENVELOPE_LEN);
824 env.extend_from_slice(&CMF_MAGIC);
825 env.extend_from_slice(&CMF_VERSION.to_le_bytes());
826 env.extend_from_slice(&0u32.to_le_bytes()); env.extend_from_slice(&required_features.to_le_bytes());
828 for (off, len) in [
829 (header_off, header_json.len() as u64),
830 (dir_off, dir_bytes.len() as u64),
831 (data_off, data_len),
832 (if masks_len > 0 { masks_off } else { 0 }, masks_len),
833 (if vocab_len > 0 { vocab_off } else { 0 }, vocab_len),
834 (if index_len > 0 { index_off } else { 0 }, index_len),
835 ] {
836 env.extend_from_slice(&off.to_le_bytes());
837 env.extend_from_slice(&len.to_le_bytes());
838 }
839 env.extend_from_slice(&hash64(&header_json).to_le_bytes());
841 env.extend_from_slice(&hash64(&dir_bytes).to_le_bytes());
842 env.resize(ENVELOPE_LEN, 0);
843
844 let mut f = BufWriter::new(File::create(path)?);
846 f.write_all(&env)?;
847 f.write_all(&header_json)?;
848 f.write_all(&dir_bytes)?;
849 let mut pos = dir_off + dir_bytes.len() as u64;
850 f.write_all(&zeros((data_off - pos) as usize))?;
851 pos = data_off;
852 for (spec, entry) in tensors.iter().zip(&entries) {
853 let target = data_off + entry.off;
854 f.write_all(&zeros((target - pos) as usize))?;
855 f.write_all(&spec.data)?;
856 pos = target + spec.data.len() as u64;
857 }
858 debug_assert_eq!(pos, data_off + data_len);
859 if let Some(mb) = &masks_bytes {
860 f.write_all(mb)?;
861 }
862 if let Some(vb) = vocab {
863 f.write_all(vb)?;
864 }
865 if let Some(ib) = &index_bytes {
866 f.write_all(ib)?;
867 }
868 f.flush()?;
869
870 tracing::info!(
871 "Wrote CMF v2: {} ({} tensors, {} masks, {:.1} MB)",
872 path.display(),
873 entries.len(),
874 masks.map(|m| m.masks.len()).unwrap_or(0),
875 std::fs::metadata(path)?.len() as f64 / 1e6
876 );
877 Ok(())
878 }
879
880 fn encode_directory(entries: &[TensorEntry]) -> Vec<u8> {
881 let mut pool = Vec::new();
882 let mut name_offs = Vec::with_capacity(entries.len());
883 for e in entries {
884 name_offs.push((pool.len() as u32, e.name.len() as u16));
885 pool.extend_from_slice(e.name.as_bytes());
886 }
887 let pool_off = 16 + entries.len() * DIR_RECORD_LEN;
888
889 let mut out = Vec::with_capacity(pool_off + pool.len());
890 out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
891 out.extend_from_slice(&(pool_off as u64).to_le_bytes());
892 for (e, (noff, nlen)) in entries.iter().zip(&name_offs) {
893 out.extend_from_slice(&noff.to_le_bytes());
894 out.extend_from_slice(&nlen.to_le_bytes());
895 out.push(e.dtype.id());
896 out.push(e.shape.len() as u8);
897 for d in 0..DIR_MAX_NDIM {
898 out.extend_from_slice(&(e.shape.get(d).copied().unwrap_or(0) as u32).to_le_bytes());
899 }
900 out.extend_from_slice(&e.off.to_le_bytes());
901 out.extend_from_slice(&e.nbytes.to_le_bytes());
902 out.extend_from_slice(&e.hash.to_le_bytes());
903 }
904 out.extend_from_slice(&pool);
905 out
906 }
907}
908
909fn align_to(x: u64, a: u64) -> u64 {
910 (x + a - 1) / a * a
911}
912
913fn zeros(n: usize) -> Vec<u8> {
914 vec![0u8; n]
915}
916
917pub fn build_sparse_index(catalog: &MaskCatalog, arch: &ModelArch) -> Vec<SparseIndexEntry> {
922 let mut out = Vec::new();
923 for m in &catalog.masks {
924 for li in 0..arch.num_layers {
925 if !m.layer_alive(li) {
926 continue;
927 }
928 let mut groups = Vec::new();
929 if let Some(bits) = m.ffn_masks.get(li) {
930 let n_groups = (arch.intermediate_size + 31) / 32;
931 for g in 0..n_groups {
932 let active = bits[g * 4..(g * 4 + 4).min(bits.len())]
934 .iter()
935 .any(|&b| b != 0);
936 if active {
937 groups.push(g as u16);
938 }
939 }
940 }
941 let mut heads = Vec::new();
942 if let Some(bits) = m.head_masks.get(li) {
943 for h in 0..arch.num_attention_heads {
944 if bits.get(h / 8).map(|b| b & (1 << (h % 8)) != 0).unwrap_or(false) {
945 heads.push(h as u8);
946 }
947 }
948 }
949 out.push(SparseIndexEntry {
950 task_id: m.task_id,
951 layer_idx: li,
952 active_ffn_groups: groups,
953 active_heads: heads,
954 });
955 }
956 }
957 out
958}
959
960pub fn encode_sparse_index(entries: &[SparseIndexEntry]) -> Vec<u8> {
963 let mut out = Vec::new();
964 out.extend_from_slice(&(entries.len() as u32).to_le_bytes());
965 out.extend_from_slice(&0u32.to_le_bytes());
966 for e in entries {
967 out.extend_from_slice(&e.task_id.to_le_bytes());
968 out.extend_from_slice(&(e.layer_idx as u32).to_le_bytes());
969 out.extend_from_slice(&(e.active_ffn_groups.len() as u32).to_le_bytes());
970 out.extend_from_slice(&(e.active_heads.len() as u32).to_le_bytes());
971 for g in &e.active_ffn_groups {
972 out.extend_from_slice(&g.to_le_bytes());
973 }
974 out.extend_from_slice(&e.active_heads);
975 while out.len() % 4 != 0 {
976 out.push(0);
977 }
978 }
979 out
980}
981
982pub fn decode_sparse_index(bytes: &[u8]) -> Result<Vec<SparseIndexEntry>, CmfError> {
983 let err = |msg: &str| CmfError::Parse(format!("sparse index: {msg}"));
984 if bytes.len() < 8 {
985 return Err(err("too short"));
986 }
987 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
988 let mut pos = 8usize;
989 let mut out = Vec::with_capacity(n);
990 for _ in 0..n {
991 if pos + 16 > bytes.len() {
992 return Err(err("entry header out of bounds"));
993 }
994 let task_id = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
995 let layer_idx = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().unwrap()) as usize;
996 let n_groups = u32::from_le_bytes(bytes[pos + 8..pos + 12].try_into().unwrap()) as usize;
997 let n_heads = u32::from_le_bytes(bytes[pos + 12..pos + 16].try_into().unwrap()) as usize;
998 pos += 16;
999 if pos + n_groups * 2 + n_heads > bytes.len() {
1000 return Err(err("entry data out of bounds"));
1001 }
1002 let mut groups = Vec::with_capacity(n_groups);
1003 for g in 0..n_groups {
1004 groups.push(u16::from_le_bytes(bytes[pos + g * 2..pos + g * 2 + 2].try_into().unwrap()));
1005 }
1006 pos += n_groups * 2;
1007 let heads = bytes[pos..pos + n_heads].to_vec();
1008 pos += n_heads;
1009 pos = (pos + 3) / 4 * 4;
1010 out.push(SparseIndexEntry {
1011 task_id,
1012 layer_idx,
1013 active_ffn_groups: groups,
1014 active_heads: heads,
1015 });
1016 }
1017 Ok(out)
1018}
1019
1020#[derive(Debug, thiserror::Error)]
1022pub enum CmfError {
1023 #[error("File not found: {0}")]
1024 FileNotFound(String),
1025 #[error("Invalid CMF magic bytes")]
1026 InvalidMagic,
1027 #[error("Unsupported CMF version: {0}")]
1028 UnsupportedVersion(u32),
1029 #[error("File requires unsupported features (bits {0:#x})")]
1030 UnsupportedFeature(u32),
1031 #[error("Unknown tensor dtype id: {0}")]
1032 UnknownDtype(u8),
1033 #[error("Tensor not found: {0}")]
1034 MissingTensor(String),
1035 #[error("Bounds error: {0}")]
1036 Bounds(String),
1037 #[error("IO error: {0}")]
1038 Io(#[from] io::Error),
1039 #[error("Parse error: {0}")]
1040 Parse(String),
1041}