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, Seek, SeekFrom, 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
222pub struct TensorSpecRef<'a> {
224 pub name: String,
225 pub dtype: TensorDtype,
226 pub shape: Vec<usize>,
227 pub data: &'a [u8],
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct SparseIndexEntry {
233 pub task_id: u32,
234 pub layer_idx: usize,
235 pub active_ffn_groups: Vec<u16>,
237 pub active_heads: Vec<u8>,
239}
240
241#[derive(Debug, Clone, Copy, Default)]
243struct Envelope {
244 required_features: u32,
245 header: (u64, u64),
246 dir: (u64, u64),
247 data: (u64, u64),
248 masks: (u64, u64),
249 vocab: (u64, u64),
250 index: (u64, u64),
251 header_hash: u64,
253 dir_hash: u64,
255}
256
257enum Backing {
258 Mmap(memmap2::Mmap),
259 Owned(Vec<u8>),
260}
261
262impl Backing {
263 fn bytes(&self) -> &[u8] {
264 match self {
265 Backing::Mmap(m) => m,
266 Backing::Owned(v) => v,
267 }
268 }
269}
270
271pub struct CmfModel {
273 pub path: PathBuf,
274 pub header: CmfHeader,
275 pub required_features: u32,
276 pub tensors: Vec<TensorEntry>,
277 by_name: HashMap<u64, u32>,
284 name_overflow: Vec<u32>,
285 pub masks: MaskCatalog,
286 pub sparse_index: Vec<SparseIndexEntry>,
287 pub vocab: Option<Vec<u8>>,
289 backing: Backing,
290 data_off: u64,
291 envelope: Envelope,
292 extra_shards: Vec<(Backing, u64)>,
295}
296
297impl std::fmt::Debug for CmfModel {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 f.debug_struct("CmfModel")
300 .field("path", &self.path)
301 .field("arch", &self.header.arch.arch_name)
302 .field("tensors", &self.tensors.len())
303 .field("masks", &self.masks.masks.len())
304 .finish()
305 }
306}
307
308impl CmfModel {
309 pub fn open(path: impl AsRef<Path>) -> Result<Self, CmfError> {
312 let path = path.as_ref().to_path_buf();
313 if !path.exists() {
314 return Err(CmfError::FileNotFound(path.display().to_string()));
315 }
316 let file = File::open(&path)?;
317 let file_len = file.metadata()?.len();
318
319 let backing = match unsafe { memmap2::MmapOptions::new().map(&file) } {
320 Ok(m) => {
321 #[cfg(unix)]
331 {
332 if std::env::var("CMF_MMAP_ADVISE")
333 .map(|v| v != "0")
334 .unwrap_or(true)
335 {
336 let _ = m.advise(memmap2::Advice::WillNeed);
337 }
338 if std::env::var("CMF_MLOCK")
339 .map(|v| v == "1")
340 .unwrap_or(false)
341 {
342 if let Err(e) = m.lock() {
343 tracing::warn!(
344 "CMF_MLOCK=1: mlock refused ({e}) — continuing unpinned"
345 );
346 }
347 }
348 }
349 Backing::Mmap(m)
350 }
351 Err(e) => {
352 tracing::warn!("mmap failed ({e}), reading file into memory");
353 Backing::Owned(std::fs::read(&path)?)
354 }
355 };
356
357 let env = Self::parse_envelope(backing.bytes(), file_len)?;
358
359 let bytes = backing.bytes();
360 let section = |off: u64, len: u64| -> &[u8] { &bytes[off as usize..(off + len) as usize] };
361
362 let header: CmfHeader = serde_json::from_slice(section(env.header.0, env.header.1))
364 .map_err(|e| CmfError::Parse(format!("header JSON: {e}")))?;
365
366 let tensors = Self::decode_directory(section(env.dir.0, env.dir.1))?;
368 for t in &tensors {
369 if t.off % TENSOR_ALIGNMENT != 0 {
370 return Err(CmfError::Bounds(format!(
371 "tensor '{}': offset {} not 64-aligned",
372 t.name, t.off
373 )));
374 }
375 let tensor_end = t.off.checked_add(t.nbytes).ok_or_else(|| {
376 CmfError::Bounds(format!("tensor '{}': offset + length overflows", t.name))
377 })?;
378 if tensor_end > env.data.1 {
379 return Err(CmfError::Bounds(format!(
380 "tensor '{}': [{}, {}) exceeds data section ({} bytes)",
381 t.name, t.off, tensor_end, env.data.1
382 )));
383 }
384 t.shape
385 .iter()
386 .try_fold(1usize, |n, &dim| n.checked_mul(dim))
387 .ok_or_else(|| {
388 CmfError::Bounds(format!(
389 "tensor '{}': shape product overflows usize",
390 t.name
391 ))
392 })?;
393 if let Some(expect) = expected_nbytes(t.dtype, &t.shape) {
394 if expect as u64 != t.nbytes {
395 return Err(CmfError::Bounds(format!(
396 "tensor '{}': nbytes {} != expected {} for {:?}{:?}",
397 t.name, t.nbytes, expect, t.dtype, t.shape
398 )));
399 }
400 }
401 if matches!(t.dtype, TensorDtype::Vbit | TensorDtype::VbitRo) {
404 let payload = section(env.data.0 + t.off, t.nbytes);
405 crate::quant::validate_payload(t.dtype, &t.shape, payload)
406 .map_err(|e| CmfError::Bounds(format!("tensor '{}': {e}", t.name)))?;
407 }
408 }
409 let mut by_name: HashMap<u64, u32> = HashMap::with_capacity(tensors.len());
413 let mut name_overflow: Vec<u32> = Vec::new();
414 for i in 0..tensors.len() {
415 let h = hash64(tensors[i].name.as_bytes());
416 match by_name.get(&h) {
417 Some(&j) if tensors[j as usize].name == tensors[i].name => {
418 return Err(CmfError::Parse(format!(
419 "duplicate tensor name '{}' in directory",
420 tensors[i].name
421 )));
422 }
423 Some(_) => name_overflow.push(i as u32), None => {
425 by_name.insert(h, i as u32);
426 }
427 }
428 }
429
430 let masks = if env.masks.1 > 0 {
432 decode_masks_section(section(env.masks.0, env.masks.1), &header.arch)
433 .map_err(CmfError::Parse)?
434 } else {
435 MaskCatalog::empty()
436 };
437
438 let vocab = if env.vocab.1 > 0 {
440 Some(section(env.vocab.0, env.vocab.1).to_vec())
441 } else {
442 None
443 };
444
445 let sparse_index = if env.index.1 > 0 {
447 decode_sparse_index(section(env.index.0, env.index.1))?
448 } else {
449 vec![]
450 };
451
452 tracing::info!(
453 "Opened CMF v2: {} | {} tensors | {} masks | vocab {} | {:.1} MB",
454 header.arch.arch_name,
455 tensors.len(),
456 masks.masks.len(),
457 if vocab.is_some() { "embedded" } else { "none" },
458 file_len as f64 / 1e6
459 );
460
461 Ok(Self {
462 path,
463 header,
464 required_features: env.required_features,
465 tensors,
466 by_name,
467 name_overflow,
468 masks,
469 sparse_index,
470 vocab,
471 backing,
472 data_off: env.data.0,
473 envelope: env,
474 extra_shards: Vec::new(),
475 })
476 }
477
478 pub fn open_sharded(path: impl AsRef<Path>) -> Result<Self, CmfError> {
482 let path = path.as_ref();
483 let mut first = Self::open(path)?;
484 let Some(info) = first.header.shard.clone() else {
485 return Ok(first); };
487 if info.no != 1 {
488 return Err(CmfError::Parse(format!(
489 "open shard 1, not {} (of {})",
490 info.no, info.count
491 )));
492 }
493 let name = path
494 .file_name()
495 .and_then(|n| n.to_str())
496 .ok_or_else(|| CmfError::Parse("bad shard path".into()))?;
497 let tag1 = format!("-{:05}-of-{:05}.cmf", 1, info.count);
498 if !name.ends_with(&tag1) {
499 return Err(CmfError::Parse(format!(
500 "shard file must end with '{tag1}' (got '{name}')"
501 )));
502 }
503 let stem = &name[..name.len() - tag1.len()];
504 for no in 2..=info.count {
505 let sib = path.with_file_name(format!("{stem}-{:05}-of-{:05}.cmf", no, info.count));
506 let sh = Self::open(&sib)?;
507 match &sh.header.shard {
508 Some(si) if si.no == no && si.count == info.count => {}
509 other => {
510 return Err(CmfError::Parse(format!(
511 "{}: wrong shard coords {other:?}",
512 sib.display()
513 )));
514 }
515 }
516 let shard_idx = first.extra_shards.len() + 1;
517 first.extra_shards.push((sh.backing, sh.envelope.data.0));
518 for mut t in sh.tensors {
519 t.shard = shard_idx;
520 let idx = first.tensors.len() as u32;
521 let h = hash64(t.name.as_bytes());
522 match first.by_name.get(&h) {
523 Some(&j) if first.tensors[j as usize].name == t.name => {
524 return Err(CmfError::Parse(format!(
525 "duplicate tensor name '{}' across shards",
526 t.name
527 )));
528 }
529 Some(_) => first.name_overflow.push(idx),
530 None => {
531 first.by_name.insert(h, idx);
532 }
533 }
534 first.tensors.push(t);
535 }
536 }
537 tracing::info!(
538 "sharded model: {} files, {} tensors total",
539 info.count,
540 first.tensors.len()
541 );
542 Ok(first)
543 }
544
545 fn parse_envelope(bytes: &[u8], file_len: u64) -> Result<Envelope, CmfError> {
546 if bytes.len() < ENVELOPE_LEN {
547 return Err(CmfError::Bounds(format!(
548 "file too small for CMF envelope: {} bytes",
549 bytes.len()
550 )));
551 }
552 if bytes[0..4] != CMF_MAGIC {
553 return Err(CmfError::InvalidMagic);
554 }
555 let u32le = |o: usize| u32::from_le_bytes(bytes[o..o + 4].try_into().unwrap());
556 let u64le = |o: usize| u64::from_le_bytes(bytes[o..o + 8].try_into().unwrap());
557
558 let version = u32le(4);
559 if version != CMF_VERSION {
560 return Err(CmfError::UnsupportedVersion(version));
561 }
562 let _flags = u32le(8); let required_features = u32le(12);
564 let unknown = required_features & !features::SUPPORTED;
565 if unknown != 0 {
566 return Err(CmfError::UnsupportedFeature(unknown));
567 }
568
569 let env = Envelope {
570 required_features,
571 header: (u64le(0x10), u64le(0x18)),
572 dir: (u64le(0x20), u64le(0x28)),
573 data: (u64le(0x30), u64le(0x38)),
574 masks: (u64le(0x40), u64le(0x48)),
575 vocab: (u64le(0x50), u64le(0x58)),
576 index: (u64le(0x60), u64le(0x68)),
577 header_hash: u64le(0x70),
578 dir_hash: u64le(0x78),
579 };
580
581 for (name, (off, len), required) in [
582 ("header", env.header, true),
583 ("dir", env.dir, true),
584 ("data", env.data, false),
585 ("masks", env.masks, false),
586 ("vocab", env.vocab, false),
587 ("index", env.index, false),
588 ] {
589 if required && len == 0 {
590 return Err(CmfError::Bounds(format!("section '{name}' is required")));
591 }
592 if len > 0
593 && off
594 .checked_add(len)
595 .map(|end| end > file_len)
596 .unwrap_or(true)
597 {
598 return Err(CmfError::Bounds(format!(
599 "section '{name}' [{off}, {}) exceeds file ({file_len} bytes)",
600 off.saturating_add(len)
601 )));
602 }
603 if len > 0
604 && (usize::try_from(off).is_err()
605 || usize::try_from(len).is_err()
606 || usize::try_from(off + len).is_err())
607 {
608 return Err(CmfError::Bounds(format!(
609 "section '{name}' cannot be addressed on this platform"
610 )));
611 }
612 }
613 if env.data.1 > 0 && env.data.0 % DATA_ALIGNMENT != 0 {
614 return Err(CmfError::Bounds(format!(
615 "data section offset {} not {}-aligned",
616 env.data.0, DATA_ALIGNMENT
617 )));
618 }
619 Ok(env)
620 }
621
622 fn decode_directory(bytes: &[u8]) -> Result<Vec<TensorEntry>, CmfError> {
623 if bytes.len() < 16 {
624 return Err(CmfError::Parse("tensor directory too short".into()));
625 }
626 let count = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize;
627 let pool_off = u64::from_le_bytes(bytes[8..16].try_into().unwrap()) as usize;
628 let records_len = count
629 .checked_mul(DIR_RECORD_LEN)
630 .ok_or_else(|| CmfError::Parse("tensor directory record count overflows".into()))?;
631 let records_end = 16usize
632 .checked_add(records_len)
633 .ok_or_else(|| CmfError::Parse("tensor directory size overflows".into()))?;
634 if records_end > bytes.len() || pool_off > bytes.len() || pool_off < records_end {
635 return Err(CmfError::Parse(format!(
636 "tensor directory malformed: count={count}, pool_off={pool_off}, len={}",
637 bytes.len()
638 )));
639 }
640 let pool = &bytes[pool_off..];
641
642 let mut out = Vec::with_capacity(count);
643 for i in 0..count {
644 let r = &bytes[16 + i * DIR_RECORD_LEN..16 + (i + 1) * DIR_RECORD_LEN];
645 let name_off = u32::from_le_bytes(r[0..4].try_into().unwrap()) as usize;
646 let name_len = u16::from_le_bytes(r[4..6].try_into().unwrap()) as usize;
647 let dtype_id = r[6];
648 let ndim = r[7] as usize;
649 if ndim > DIR_MAX_NDIM {
650 return Err(CmfError::Parse(format!("tensor #{i}: ndim {ndim} > 6")));
651 }
652 let mut shape = Vec::with_capacity(ndim);
653 for d in 0..ndim {
654 shape.push(
655 u32::from_le_bytes(r[8 + d * 4..12 + d * 4].try_into().unwrap()) as usize,
656 );
657 }
658 let off = u64::from_le_bytes(r[32..40].try_into().unwrap());
659 let nbytes = u64::from_le_bytes(r[40..48].try_into().unwrap());
660 let hash = u64::from_le_bytes(r[48..56].try_into().unwrap());
661
662 let name_end = name_off
663 .checked_add(name_len)
664 .ok_or_else(|| CmfError::Parse(format!("tensor #{i}: name range overflows")))?;
665 if name_end > pool.len() {
666 return Err(CmfError::Parse(format!("tensor #{i}: name out of pool")));
667 }
668 let name = std::str::from_utf8(&pool[name_off..name_end])
669 .map_err(|_| CmfError::Parse(format!("tensor #{i}: name is not UTF-8")))?
670 .to_string();
671 let dtype = TensorDtype::from_id(dtype_id).ok_or(CmfError::UnknownDtype(dtype_id))?;
672
673 out.push(TensorEntry {
674 name,
675 dtype,
676 shape,
677 off,
678 nbytes,
679 shard: 0,
680 hash,
681 });
682 }
683 Ok(out)
684 }
685
686 pub fn arch(&self) -> &ModelArch {
689 &self.header.arch
690 }
691
692 pub fn tensor(&self, name: &str) -> Option<&TensorEntry> {
693 self.tensor_index(name).map(|i| &self.tensors[i])
694 }
695
696 pub fn tensor_index(&self, name: &str) -> Option<usize> {
702 let h = hash64(name.as_bytes());
703 if let Some(&i) = self.by_name.get(&h) {
704 if self.tensors[i as usize].name == name {
705 return Some(i as usize);
706 }
707 }
708 self.name_overflow
709 .iter()
710 .copied()
711 .find(|&i| self.tensors[i as usize].name == name)
712 .map(|i| i as usize)
713 }
714
715 pub fn resolve_tensor(&self, name: &str, skill: Option<&str>) -> Option<&TensorEntry> {
719 if let Some(sid) = skill {
720 if let Some(t) = self.tensor(&format!("skill.{sid}.{name}")) {
721 return Some(t);
722 }
723 }
724 self.tensor(name)
725 }
726
727 pub fn skill_tensors(&self, skill_id: &str) -> impl Iterator<Item = &TensorEntry> {
730 let prefix = format!("skill.{skill_id}.");
731 self.tensors
732 .iter()
733 .filter(move |t| t.name.starts_with(&prefix))
734 }
735
736 pub fn tensor_bytes(&self, name: &str) -> Result<&[u8], CmfError> {
738 let entry = self
739 .tensor(name)
740 .ok_or_else(|| CmfError::MissingTensor(name.to_string()))?;
741 Ok(self.entry_bytes(entry))
742 }
743
744 pub fn primary_bytes(&self) -> &[u8] {
747 self.backing.bytes()
748 }
749
750 pub fn entry_abs_offset(&self, entry: &TensorEntry) -> Option<usize> {
753 (entry.shard == 0).then(|| (self.data_off + entry.off) as usize)
754 }
755
756 pub fn entry_bytes(&self, entry: &TensorEntry) -> &[u8] {
757 let (bytes, data_off) = if entry.shard == 0 {
758 (self.backing.bytes(), self.data_off)
759 } else {
760 let (b, o) = &self.extra_shards[entry.shard - 1];
761 (b.bytes(), *o)
762 };
763 let start = (data_off + entry.off) as usize;
764 &bytes[start..start + entry.nbytes as usize]
765 }
766
767 pub fn layer_tensors(&self, layer_idx: usize) -> Vec<&TensorEntry> {
769 let prefix = format!("model.layers.{layer_idx}.");
770 self.tensors
771 .iter()
772 .filter(|t| t.name.starts_with(&prefix))
773 .collect()
774 }
775
776 pub fn total_param_count(&self) -> u64 {
778 self.tensors
779 .iter()
780 .filter(|t| t.shape.len() >= 2)
781 .map(|t| t.n_elems() as u64)
782 .sum()
783 }
784
785 pub fn verify(&self) -> Vec<String> {
788 let mut problems = Vec::new();
789
790 let bytes = self.backing.bytes();
792 let env = &self.envelope;
793 let sect = |(off, len): (u64, u64)| &bytes[off as usize..(off + len) as usize];
794 let check = |name: &str, stored: u64, span: (u64, u64)| -> Option<String> {
795 if stored != 0 && span.1 > 0 {
796 let actual = hash64(sect(span));
797 if actual != stored {
798 return Some(format!(
799 "section '{name}': hash mismatch (stored {stored:016x}, \
800 actual {actual:016x})"
801 ));
802 }
803 }
804 None
805 };
806 problems.extend(check("header", env.header_hash, env.header));
807 problems.extend(check("dir", env.dir_hash, env.dir));
808 if let Some(sh) = &self.header.section_hashes {
809 for (name, hex, span) in [
810 ("masks", &sh.masks, env.masks),
811 ("vocab", &sh.vocab, env.vocab),
812 ("index", &sh.index, env.index),
813 ] {
814 if let Some(hex) = hex {
815 match u64::from_str_radix(hex, 16) {
816 Ok(stored) => problems.extend(check(name, stored, span)),
817 Err(_) => {
818 problems.push(format!("section '{name}': malformed hash '{hex}'"))
819 }
820 }
821 }
822 }
823 }
824
825 for t in &self.tensors {
826 let actual = hash64(self.entry_bytes(t));
827 if actual != t.hash {
828 problems.push(format!(
829 "tensor '{}': hash mismatch (stored {:016x}, actual {:016x})",
830 t.name, t.hash, actual
831 ));
832 }
833 }
834 problems
835 }
836
837 pub fn compute_active_size(&self, mask: &TaskMask) -> u64 {
840 let arch = &self.header.arch;
841 let mut total = 0u64;
842 for li in 0..arch.num_layers {
843 if !mask.layer_alive(li) {
844 continue;
845 }
846 let ffn_frac = mask.ffn_active_count(li) as f64 / arch.intermediate_size.max(1) as f64;
847 let head_frac =
848 mask.active_head_count(li) as f64 / arch.num_attention_heads.max(1) as f64;
849 for t in self.layer_tensors(li) {
850 let frac = if t.name.contains(".mlp.") {
851 ffn_frac
852 } else if t.name.contains(".self_attn.") {
853 head_frac
854 } else {
855 1.0
856 };
857 total += (t.nbytes as f64 * frac) as u64;
858 }
859 }
860 total
861 }
862
863 pub fn write(
868 path: impl AsRef<Path>,
869 header: &CmfHeader,
870 tensors: &[TensorSpec],
871 masks: Option<&MaskCatalog>,
872 vocab: Option<&[u8]>,
873 ) -> Result<(), CmfError> {
874 let refs: Vec<TensorSpecRef> = tensors
875 .iter()
876 .map(|t| TensorSpecRef {
877 name: t.name.clone(),
878 dtype: t.dtype,
879 shape: t.shape.clone(),
880 data: &t.data,
881 })
882 .collect();
883 Self::write_ref(path, header, &refs, masks, vocab)
884 }
885
886 pub fn write_ref(
890 path: impl AsRef<Path>,
891 header: &CmfHeader,
892 tensors: &[TensorSpecRef],
893 masks: Option<&MaskCatalog>,
894 vocab: Option<&[u8]>,
895 ) -> Result<(), CmfError> {
896 let path = path.as_ref();
897
898 let mut entries = Vec::with_capacity(tensors.len());
900 let mut data_cursor = 0u64;
901 for t in tensors {
902 if t.shape.len() > DIR_MAX_NDIM {
903 return Err(CmfError::Parse(format!(
904 "tensor '{}': ndim {} > 6",
905 t.name,
906 t.shape.len()
907 )));
908 }
909 if let Some(expect) = expected_nbytes(t.dtype, &t.shape) {
910 if expect != t.data.len() {
911 return Err(CmfError::Bounds(format!(
912 "tensor '{}': data {} bytes != expected {} for {:?}{:?}",
913 t.name,
914 t.data.len(),
915 expect,
916 t.dtype,
917 t.shape
918 )));
919 }
920 }
921 let align = if t.data.len() as u64 >= LARGE_TENSOR_MIN {
922 LARGE_TENSOR_ALIGN
923 } else {
924 TENSOR_ALIGNMENT
925 };
926 data_cursor = align_to(data_cursor, align);
927 entries.push(TensorEntry {
928 name: t.name.clone(),
929 dtype: t.dtype,
930 shape: t.shape.clone(),
931 off: data_cursor,
932 nbytes: t.data.len() as u64,
933 shard: 0,
934 hash: hash64(&t.data),
935 });
936 data_cursor += t.data.len() as u64;
937 }
938 let data_len = data_cursor;
939
940 let dir_bytes = Self::encode_directory(&entries);
941
942 let masks_bytes = match masks {
943 Some(catalog) if !catalog.masks.is_empty() => {
944 Some(encode_masks_section(catalog, &header.arch).map_err(CmfError::Parse)?)
945 }
946 _ => None,
947 };
948 let index_bytes = match masks {
949 Some(catalog) if !catalog.masks.is_empty() => {
950 let idx = build_sparse_index(catalog, &header.arch);
951 Some(encode_sparse_index(&idx))
952 }
953 _ => None,
954 };
955
956 let hex = |b: Option<&[u8]>| b.map(|b| format!("{:016x}", hash64(b)));
959 let mut header = header.clone();
960 if masks_bytes.is_some() || vocab.is_some() || index_bytes.is_some() {
961 header.section_hashes = Some(SectionHashes {
962 masks: hex(masks_bytes.as_deref()),
963 vocab: hex(vocab),
964 index: hex(index_bytes.as_deref()),
965 });
966 }
967 let header_json =
968 serde_json::to_vec(&header).map_err(|e| CmfError::Parse(format!("header: {e}")))?;
969
970 let mut required_features = features::TENSOR_DIR;
971 if masks_bytes.is_some() {
972 required_features |= features::BINARY_MASKS;
973 }
974 if entries
975 .iter()
976 .any(|t| matches!(t.dtype, TensorDtype::Q8_2f | TensorDtype::Vbit))
977 {
978 required_features |= features::QUANT_2F;
979 }
980
981 let header_off = ENVELOPE_LEN as u64;
983 let dir_off = header_off + header_json.len() as u64;
984 let data_off = align_to(dir_off + dir_bytes.len() as u64, DATA_ALIGNMENT);
985 let masks_off = data_off + data_len;
986 let masks_len = masks_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
987 let vocab_off = masks_off + masks_len;
988 let vocab_len = vocab.map(|b| b.len() as u64).unwrap_or(0);
989 let index_off = vocab_off + vocab_len;
990 let index_len = index_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
991
992 let mut env = Vec::with_capacity(ENVELOPE_LEN);
994 env.extend_from_slice(&CMF_MAGIC);
995 env.extend_from_slice(&CMF_VERSION.to_le_bytes());
996 env.extend_from_slice(&0u32.to_le_bytes()); env.extend_from_slice(&required_features.to_le_bytes());
998 for (off, len) in [
999 (header_off, header_json.len() as u64),
1000 (dir_off, dir_bytes.len() as u64),
1001 (data_off, data_len),
1002 (if masks_len > 0 { masks_off } else { 0 }, masks_len),
1003 (if vocab_len > 0 { vocab_off } else { 0 }, vocab_len),
1004 (if index_len > 0 { index_off } else { 0 }, index_len),
1005 ] {
1006 env.extend_from_slice(&off.to_le_bytes());
1007 env.extend_from_slice(&len.to_le_bytes());
1008 }
1009 env.extend_from_slice(&hash64(&header_json).to_le_bytes());
1011 env.extend_from_slice(&hash64(&dir_bytes).to_le_bytes());
1012 env.resize(ENVELOPE_LEN, 0);
1013
1014 let mut f = BufWriter::new(File::create(path)?);
1016 f.write_all(&env)?;
1017 f.write_all(&header_json)?;
1018 f.write_all(&dir_bytes)?;
1019 let mut pos = dir_off + dir_bytes.len() as u64;
1020 f.write_all(&zeros((data_off - pos) as usize))?;
1021 pos = data_off;
1022 for (spec, entry) in tensors.iter().zip(&entries) {
1023 let target = data_off + entry.off;
1024 f.write_all(&zeros((target - pos) as usize))?;
1025 f.write_all(&spec.data)?;
1026 pos = target + spec.data.len() as u64;
1027 }
1028 debug_assert_eq!(pos, data_off + data_len);
1029 if let Some(mb) = &masks_bytes {
1030 f.write_all(mb)?;
1031 }
1032 if let Some(vb) = vocab {
1033 f.write_all(vb)?;
1034 }
1035 if let Some(ib) = &index_bytes {
1036 f.write_all(ib)?;
1037 }
1038 f.flush()?;
1039
1040 tracing::info!(
1041 "Wrote CMF v2: {} ({} tensors, {} masks, {:.1} MB)",
1042 path.display(),
1043 entries.len(),
1044 masks.map(|m| m.masks.len()).unwrap_or(0),
1045 std::fs::metadata(path)?.len() as f64 / 1e6
1046 );
1047 Ok(())
1048 }
1049
1050 pub(crate) fn encode_directory(entries: &[TensorEntry]) -> Vec<u8> {
1051 let mut pool = Vec::new();
1052 let mut name_offs = Vec::with_capacity(entries.len());
1053 for e in entries {
1054 name_offs.push((pool.len() as u32, e.name.len() as u16));
1055 pool.extend_from_slice(e.name.as_bytes());
1056 }
1057 let pool_off = 16 + entries.len() * DIR_RECORD_LEN;
1058
1059 let mut out = Vec::with_capacity(pool_off + pool.len());
1060 out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
1061 out.extend_from_slice(&(pool_off as u64).to_le_bytes());
1062 for (e, (noff, nlen)) in entries.iter().zip(&name_offs) {
1063 out.extend_from_slice(&noff.to_le_bytes());
1064 out.extend_from_slice(&nlen.to_le_bytes());
1065 out.push(e.dtype.id());
1066 out.push(e.shape.len() as u8);
1067 for d in 0..DIR_MAX_NDIM {
1068 out.extend_from_slice(&(e.shape.get(d).copied().unwrap_or(0) as u32).to_le_bytes());
1069 }
1070 out.extend_from_slice(&e.off.to_le_bytes());
1071 out.extend_from_slice(&e.nbytes.to_le_bytes());
1072 out.extend_from_slice(&e.hash.to_le_bytes());
1073 }
1074 out.extend_from_slice(&pool);
1075 out
1076 }
1077}
1078
1079fn align_to(x: u64, a: u64) -> u64 {
1080 x.div_ceil(a) * a
1081}
1082
1083fn zeros(n: usize) -> Vec<u8> {
1084 vec![0u8; n]
1085}
1086
1087pub struct ResumeState {
1106 pub names: Vec<String>,
1107 pub marks: Vec<String>,
1108}
1109
1110pub struct CmfStreamWriter {
1111 file: BufWriter<File>,
1112 path: PathBuf,
1113 data_off: u64,
1115 cursor: u64,
1117 entries: Vec<TensorEntry>,
1118 manifest: Option<BufWriter<File>>,
1122}
1123
1124impl CmfStreamWriter {
1125 pub fn head_reserve_for(n_tensors: usize, avg_name: usize) -> u64 {
1130 let dir = 16 + n_tensors * (DIR_RECORD_LEN + 6 + avg_name);
1131 let head = ENVELOPE_LEN + dir + (1 << 20);
1132 align_to(3 * head as u64, DATA_ALIGNMENT).max(1 << 20)
1137 }
1138
1139 pub fn new(path: impl AsRef<Path>, gap: u64) -> Result<Self, CmfError> {
1141 let path = path.as_ref().to_path_buf();
1142 let data_off = align_to(gap.max(ENVELOPE_LEN as u64 + 1), DATA_ALIGNMENT);
1143 let mut file = BufWriter::new(File::create(&path)?);
1144 file.write_all(&zeros(data_off as usize))?;
1145 Ok(Self {
1146 file,
1147 path,
1148 data_off,
1149 cursor: 0,
1150 entries: Vec::new(),
1151 manifest: None,
1152 })
1153 }
1154
1155 pub fn push(
1158 &mut self,
1159 name: &str,
1160 dtype: TensorDtype,
1161 shape: &[usize],
1162 data: &[u8],
1163 ) -> Result<(), CmfError> {
1164 if shape.len() > DIR_MAX_NDIM {
1165 return Err(CmfError::Parse(format!(
1166 "tensor '{}': ndim {} > {}",
1167 name,
1168 shape.len(),
1169 DIR_MAX_NDIM
1170 )));
1171 }
1172 if let Some(expect) = expected_nbytes(dtype, shape) {
1173 if expect != data.len() {
1174 return Err(CmfError::Bounds(format!(
1175 "tensor '{}': data {} bytes != expected {} for {:?}{:?}",
1176 name,
1177 data.len(),
1178 expect,
1179 dtype,
1180 shape
1181 )));
1182 }
1183 }
1184 let align = if data.len() as u64 >= LARGE_TENSOR_MIN {
1185 LARGE_TENSOR_ALIGN
1186 } else {
1187 TENSOR_ALIGNMENT
1188 };
1189 let off = align_to(self.cursor, align);
1190 self.file.write_all(&zeros((off - self.cursor) as usize))?;
1191 self.file.write_all(data)?;
1192 self.entries.push(TensorEntry {
1193 name: name.to_string(),
1194 dtype,
1195 shape: shape.to_vec(),
1196 off,
1197 nbytes: data.len() as u64,
1198 shard: 0,
1199 hash: hash64(data),
1200 });
1201 self.cursor = off + data.len() as u64;
1202 if let Some(m) = self.manifest.as_mut() {
1203 let e = self.entries.last().unwrap();
1204 writeln!(
1205 m,
1206 "{{\"name\":{},\"dtype\":{},\"shape\":{:?},\"off\":{},\"nbytes\":{},\"hash\":{}}}",
1207 serde_json::to_string(&e.name).unwrap_or_else(|_| "\"?\"".into()),
1208 dtype.id(),
1209 e.shape,
1210 e.off,
1211 e.nbytes,
1212 e.hash
1213 )?;
1214 m.flush()?;
1215 }
1216 Ok(())
1217 }
1218
1219 pub fn with_manifest(mut self, path: impl AsRef<Path>) -> Result<Self, CmfError> {
1222 let mut f = BufWriter::new(File::create(path)?);
1223 writeln!(f, "{{\"data_off\":{}}}", self.data_off)?;
1224 f.flush()?;
1225 self.manifest = Some(f);
1226 Ok(self)
1227 }
1228
1229 pub fn appending_manifest(mut self, path: impl AsRef<Path>) -> Result<Self, CmfError> {
1232 self.manifest = Some(BufWriter::new(
1233 std::fs::OpenOptions::new().append(true).open(path)?,
1234 ));
1235 Ok(self)
1236 }
1237
1238 pub fn resume(
1243 path: impl AsRef<Path>,
1244 manifest: impl AsRef<Path>,
1245 ) -> Result<(Self, ResumeState), CmfError> {
1246 let path = path.as_ref().to_path_buf();
1247 let text = std::fs::read_to_string(manifest.as_ref())?;
1248 let mut lines = text.lines();
1249 let first = lines
1250 .next()
1251 .ok_or_else(|| CmfError::Parse("manifest is empty".into()))?;
1252 let head: serde_json::Value = serde_json::from_str(first)
1253 .map_err(|e| CmfError::Parse(format!("manifest head: {e}")))?;
1254 let data_off = head["data_off"]
1255 .as_u64()
1256 .ok_or_else(|| CmfError::Parse("manifest head has no data_off".into()))?;
1257
1258 let mut entries = Vec::new();
1259 let mut names = Vec::new();
1260 let mut marks = Vec::new();
1261 let (mut safe_upto, mut safe_entries) = (0u64, 0usize);
1262 for (i, line) in lines.enumerate() {
1263 if line.trim().is_empty() {
1264 continue;
1265 }
1266 let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
1269 tracing::warn!("manifest line {} is truncated — ignoring it", i + 2);
1270 break;
1271 };
1272 if let Some(mark) = v["mark"].as_str() {
1273 marks.push(mark.to_string());
1274 safe_upto = v["at"].as_u64().unwrap_or(0);
1279 safe_entries = entries.len();
1280 continue;
1281 }
1282 let dtype = TensorDtype::from_id(v["dtype"].as_u64().unwrap_or(0) as u8)
1283 .ok_or_else(|| CmfError::Parse(format!("manifest line {}: dtype", i + 2)))?;
1284 let name = v["name"].as_str().unwrap_or_default().to_string();
1285 names.push(name.clone());
1286 entries.push(TensorEntry {
1287 name,
1288 dtype,
1289 shape: v["shape"]
1290 .as_array()
1291 .map(|a| a.iter().filter_map(|x| x.as_u64()).map(|x| x as usize).collect())
1292 .unwrap_or_default(),
1293 off: v["off"].as_u64().unwrap_or(0),
1294 nbytes: v["nbytes"].as_u64().unwrap_or(0),
1295 shard: 0,
1296 hash: v["hash"].as_u64().unwrap_or(0),
1297 });
1298 }
1299 entries.truncate(safe_entries);
1300 names.truncate(safe_entries);
1301 let cursor = safe_upto;
1302 debug_assert_eq!(
1303 entries.last().map(|e| e.off + e.nbytes).unwrap_or(0),
1304 cursor,
1305 "the last mark disagrees with the entries before it"
1306 );
1307 let on_disk = std::fs::metadata(&path)?.len();
1308 if on_disk < data_off + cursor {
1309 return Err(CmfError::Bounds(format!(
1310 "{} is {on_disk} bytes but its last checkpoint claims {} — \
1311 the file is shorter than its own record",
1312 path.display(),
1313 data_off + cursor
1314 )));
1315 }
1316 let mut file = std::fs::OpenOptions::new().write(true).open(&path)?;
1317 file.seek(SeekFrom::Start(data_off + cursor))?;
1318 Ok((
1319 Self {
1320 file: BufWriter::new(file),
1321 path,
1322 data_off,
1323 cursor,
1324 entries,
1325 manifest: None,
1326 },
1327 ResumeState { names, marks },
1328 ))
1329 }
1330
1331 pub fn mark(&mut self, note: &str) -> Result<(), CmfError> {
1336 self.file.flush()?;
1340 if let Some(m) = self.manifest.as_mut() {
1341 writeln!(
1342 m,
1343 "{{\"mark\":{},\"at\":{}}}",
1344 serde_json::to_string(note).unwrap_or_default(),
1345 self.cursor
1346 )?;
1347 m.flush()?;
1348 }
1349 Ok(())
1350 }
1351
1352 pub fn tensor_count(&self) -> usize {
1353 self.entries.len()
1354 }
1355
1356 pub fn data_len(&self) -> u64 {
1358 self.cursor
1359 }
1360
1361 pub fn finish(
1363 mut self,
1364 header: &CmfHeader,
1365 masks: Option<&MaskCatalog>,
1366 vocab: Option<&[u8]>,
1367 ) -> Result<(), CmfError> {
1368 let data_len = self.cursor;
1369
1370 let masks_bytes = match masks {
1371 Some(catalog) if !catalog.masks.is_empty() => {
1372 Some(encode_masks_section(catalog, &header.arch).map_err(CmfError::Parse)?)
1373 }
1374 _ => None,
1375 };
1376 let index_bytes = match masks {
1377 Some(catalog) if !catalog.masks.is_empty() => {
1378 Some(encode_sparse_index(&build_sparse_index(catalog, &header.arch)))
1379 }
1380 _ => None,
1381 };
1382 if let Some(mb) = &masks_bytes {
1383 self.file.write_all(mb)?;
1384 }
1385 if let Some(vb) = vocab {
1386 self.file.write_all(vb)?;
1387 }
1388 if let Some(ib) = &index_bytes {
1389 self.file.write_all(ib)?;
1390 }
1391 self.file.flush()?;
1392
1393 let dir_bytes = CmfModel::encode_directory(&self.entries);
1394
1395 let hex = |b: Option<&[u8]>| b.map(|b| format!("{:016x}", hash64(b)));
1396 let mut header = header.clone();
1397 if masks_bytes.is_some() || vocab.is_some() || index_bytes.is_some() {
1398 header.section_hashes = Some(SectionHashes {
1399 masks: hex(masks_bytes.as_deref()),
1400 vocab: hex(vocab),
1401 index: hex(index_bytes.as_deref()),
1402 });
1403 }
1404 let header_json =
1405 serde_json::to_vec(&header).map_err(|e| CmfError::Parse(format!("header: {e}")))?;
1406
1407 let mut required_features = features::TENSOR_DIR;
1408 if masks_bytes.is_some() {
1409 required_features |= features::BINARY_MASKS;
1410 }
1411 if self
1412 .entries
1413 .iter()
1414 .any(|t| matches!(t.dtype, TensorDtype::Q8_2f | TensorDtype::Vbit))
1415 {
1416 required_features |= features::QUANT_2F;
1417 }
1418
1419 let header_off = ENVELOPE_LEN as u64;
1420 let dir_off = header_off + header_json.len() as u64;
1421 let head_len = dir_off + dir_bytes.len() as u64;
1422 if head_len > self.data_off {
1423 return Err(CmfError::Parse(format!(
1424 "streamed head is {head_len} bytes but only {} were reserved — \
1425 the payloads are already on disk at a fixed offset, so this \
1426 file cannot be salvaged; re-run with a larger reserve",
1427 self.data_off
1428 )));
1429 }
1430 let data_off = self.data_off;
1431 let masks_off = data_off + data_len;
1432 let masks_len = masks_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
1433 let vocab_off = masks_off + masks_len;
1434 let vocab_len = vocab.map(|b| b.len() as u64).unwrap_or(0);
1435 let index_off = vocab_off + vocab_len;
1436 let index_len = index_bytes.as_ref().map(|b| b.len() as u64).unwrap_or(0);
1437
1438 let mut env = Vec::with_capacity(ENVELOPE_LEN);
1439 env.extend_from_slice(&CMF_MAGIC);
1440 env.extend_from_slice(&CMF_VERSION.to_le_bytes());
1441 env.extend_from_slice(&0u32.to_le_bytes());
1442 env.extend_from_slice(&required_features.to_le_bytes());
1443 for (off, len) in [
1444 (header_off, header_json.len() as u64),
1445 (dir_off, dir_bytes.len() as u64),
1446 (data_off, data_len),
1447 (if masks_len > 0 { masks_off } else { 0 }, masks_len),
1448 (if vocab_len > 0 { vocab_off } else { 0 }, vocab_len),
1449 (if index_len > 0 { index_off } else { 0 }, index_len),
1450 ] {
1451 env.extend_from_slice(&off.to_le_bytes());
1452 env.extend_from_slice(&len.to_le_bytes());
1453 }
1454 env.extend_from_slice(&hash64(&header_json).to_le_bytes());
1455 env.extend_from_slice(&hash64(&dir_bytes).to_le_bytes());
1456 env.resize(ENVELOPE_LEN, 0);
1457
1458 let mut f = self
1459 .file
1460 .into_inner()
1461 .map_err(|e| CmfError::Io(e.into_error()))?;
1462 f.seek(SeekFrom::Start(0))?;
1463 f.write_all(&env)?;
1464 f.write_all(&header_json)?;
1465 f.write_all(&dir_bytes)?;
1466 f.flush()?;
1467
1468 tracing::info!(
1469 "Wrote CMF v2 (streamed): {} ({} tensors, {:.1} MB)",
1470 self.path.display(),
1471 self.entries.len(),
1472 (data_off + data_len + masks_len + vocab_len + index_len) as f64 / 1e6
1473 );
1474 Ok(())
1475 }
1476}
1477
1478pub fn build_sparse_index(catalog: &MaskCatalog, arch: &ModelArch) -> Vec<SparseIndexEntry> {
1483 let mut out = Vec::new();
1484 for m in &catalog.masks {
1485 for li in 0..arch.num_layers {
1486 if !m.layer_alive(li) {
1487 continue;
1488 }
1489 let mut groups = Vec::new();
1490 if let Some(bits) = m.ffn_masks.get(li) {
1491 let n_groups = arch.intermediate_size.div_ceil(32);
1492 for g in 0..n_groups {
1493 let active = bits[g * 4..(g * 4 + 4).min(bits.len())]
1495 .iter()
1496 .any(|&b| b != 0);
1497 if active {
1498 groups.push(g as u16);
1499 }
1500 }
1501 }
1502 let mut heads = Vec::new();
1503 if let Some(bits) = m.head_masks.get(li) {
1504 for h in 0..arch.num_attention_heads {
1505 if bits
1506 .get(h / 8)
1507 .map(|b| b & (1 << (h % 8)) != 0)
1508 .unwrap_or(false)
1509 {
1510 heads.push(h as u8);
1511 }
1512 }
1513 }
1514 out.push(SparseIndexEntry {
1515 task_id: m.task_id,
1516 layer_idx: li,
1517 active_ffn_groups: groups,
1518 active_heads: heads,
1519 });
1520 }
1521 }
1522 out
1523}
1524
1525pub fn encode_sparse_index(entries: &[SparseIndexEntry]) -> Vec<u8> {
1528 let mut out = Vec::new();
1529 out.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1530 out.extend_from_slice(&0u32.to_le_bytes());
1531 for e in entries {
1532 out.extend_from_slice(&e.task_id.to_le_bytes());
1533 out.extend_from_slice(&(e.layer_idx as u32).to_le_bytes());
1534 out.extend_from_slice(&(e.active_ffn_groups.len() as u32).to_le_bytes());
1535 out.extend_from_slice(&(e.active_heads.len() as u32).to_le_bytes());
1536 for g in &e.active_ffn_groups {
1537 out.extend_from_slice(&g.to_le_bytes());
1538 }
1539 out.extend_from_slice(&e.active_heads);
1540 while out.len() % 4 != 0 {
1541 out.push(0);
1542 }
1543 }
1544 out
1545}
1546
1547pub fn decode_sparse_index(bytes: &[u8]) -> Result<Vec<SparseIndexEntry>, CmfError> {
1548 let err = |msg: &str| CmfError::Parse(format!("sparse index: {msg}"));
1549 if bytes.len() < 8 {
1550 return Err(err("too short"));
1551 }
1552 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1553 let mut pos = 8usize;
1554 let mut out = Vec::with_capacity(n);
1555 for _ in 0..n {
1556 if pos + 16 > bytes.len() {
1557 return Err(err("entry header out of bounds"));
1558 }
1559 let task_id = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
1560 let layer_idx = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().unwrap()) as usize;
1561 let n_groups = u32::from_le_bytes(bytes[pos + 8..pos + 12].try_into().unwrap()) as usize;
1562 let n_heads = u32::from_le_bytes(bytes[pos + 12..pos + 16].try_into().unwrap()) as usize;
1563 pos += 16;
1564 if pos + n_groups * 2 + n_heads > bytes.len() {
1565 return Err(err("entry data out of bounds"));
1566 }
1567 let mut groups = Vec::with_capacity(n_groups);
1568 for g in 0..n_groups {
1569 groups.push(u16::from_le_bytes(
1570 bytes[pos + g * 2..pos + g * 2 + 2].try_into().unwrap(),
1571 ));
1572 }
1573 pos += n_groups * 2;
1574 let heads = bytes[pos..pos + n_heads].to_vec();
1575 pos += n_heads;
1576 pos = pos.div_ceil(4) * 4;
1577 out.push(SparseIndexEntry {
1578 task_id,
1579 layer_idx,
1580 active_ffn_groups: groups,
1581 active_heads: heads,
1582 });
1583 }
1584 Ok(out)
1585}
1586
1587#[derive(Debug, thiserror::Error)]
1589pub enum CmfError {
1590 #[error("File not found: {0}")]
1591 FileNotFound(String),
1592 #[error("Invalid CMF magic bytes")]
1593 InvalidMagic,
1594 #[error("Unsupported CMF version: {0}")]
1595 UnsupportedVersion(u32),
1596 #[error("File requires unsupported features (bits {0:#x})")]
1597 UnsupportedFeature(u32),
1598 #[error("Unknown tensor dtype id: {0}")]
1599 UnknownDtype(u8),
1600 #[error("Tensor not found: {0}")]
1601 MissingTensor(String),
1602 #[error("Bounds error: {0}")]
1603 Bounds(String),
1604 #[error("IO error: {0}")]
1605 Io(#[from] io::Error),
1606 #[error("Parse error: {0}")]
1607 Parse(String),
1608}