Skip to main content

cortiq_core/
format.rs

1//! CMF v2 binary container — envelope, tensor directory, mmap access.
2//!
3//! See `docs/CMF_V2_SPEC.md`. Layout summary:
4//!
5//! ```text
6//! [0x00]  magic "CMF\x01" | version u32 = 2 | flags u32 | required_features u32
7//! [0x10]  header_off/len | dir_off/len | data_off/len   (u64 LE each)
8//! [0x40]  masks_off/len  | vocab_off/len | index_off/len
9//! [0x70]  16 reserved bytes (zero)
10//! [0x80]  header JSON → tensor directory → weight blob (4096-aligned,
11//!         tensors 64-aligned) → masks → vocab → sparse index
12//! ```
13//!
14//! The tensor directory is the ONLY source of truth for the weight blob
15//! layout — there is no computable layout, by design (v1 bug class #1).
16//! Every validation failure is a hard error: no silent fallbacks.
17
18use 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;
31/// Weight blob is page-aligned for mmap.
32pub const DATA_ALIGNMENT: u64 = 4096;
33/// Every tensor inside the blob is 64-byte aligned (SIMD / cache line).
34pub const TENSOR_ALIGNMENT: u64 = 64;
35/// Tensors at least this large are additionally page-aligned to
36/// [`LARGE_TENSOR_ALIGN`], so a cold skill / MoE-expert / mask weight sits on
37/// its own page(s): the "unused weights cost 0 RSS" guarantee then holds at
38/// page granularity (a lazily-paged tensor pulls exactly its own bytes, not a
39/// neighbour's), and per-layer `madvise(WILLNEED)` covers clean ranges. Small
40/// tensors (norms, biases, 1-D f16) keep the 64-byte SIMD alignment so the
41/// padding stays negligible.
42pub const LARGE_TENSOR_MIN: u64 = 16 * 1024;
43/// Page alignment applied to large tensors. 4096 is the common page size
44/// (x86, most ARM/Android); it is a multiple of [`TENSOR_ALIGNMENT`], so
45/// existing readers — which only require `off % 64 == 0` — accept these files
46/// unchanged. Purely a writer-side, backward-compatible layout choice.
47pub const LARGE_TENSOR_ALIGN: u64 = 4096;
48/// One directory record is 56 bytes (see `.vmfc` v2).
49pub const DIR_RECORD_LEN: usize = 56;
50pub const DIR_MAX_NDIM: usize = 6;
51
52/// `required_features` bits. A reader MUST refuse a file with any bit
53/// it does not support.
54pub 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    /// Features this reader implements today.
62    pub const SUPPORTED: u32 = TENSOR_DIR | BINARY_MASKS | QUANT_2F;
63}
64
65/// JSON header — architecture and provenance (human-readable part;
66/// machine-critical data lives in binary sections).
67#[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    /// Informational default; per-tensor truth is in the directory.
74    pub quant_type: QuantType,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub provenance: Option<serde_json::Value>,
77    /// Chat/eos bundle (spec §6.1): the file — not the binary — defines
78    /// chat behavior. Additive: absent in older files.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub tokenizer_config: Option<TokenizerBundle>,
81    /// Section-level integrity (spec §8.1): hex hash64 of the raw bytes
82    /// of the optional sections. header/dir hashes live in the envelope
83    /// reserved bytes — JSON cannot protect the JSON that carries it.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub section_hashes: Option<SectionHashes>,
86    /// Per-skill records (spec §9): replacement tensors live in the
87    /// directory as `skill.{id}.{name}`; this registry carries the
88    /// selection descriptor and the honest quality contract.
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub skills: Vec<SkillRecord>,
91    /// Sharding (spec §10): this file is shard `no` of `count`; every
92    /// shard is a standalone valid .cmf carrying a tensor subset.
93    /// Naming convention: `…-{no:05}-of-{count:05}.cmf`.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub shard: Option<ShardInfo>,
96    /// Measured confidence calibration (B1): a temperature fit on held-out
97    /// so the displayed Born-mass confidence is a true property of the
98    /// model (softmax(logits/T)), not a raw estimate. Additive; absent =
99    /// use raw (T=1). Written by `set_calibration.py` after `cortiq
100    /// calibrate` measures the reliability/ECE.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub calibration: Option<Calibration>,
103}
104
105/// Confidence-calibration record (spec §6.2). `temperature` scales the
106/// logits before softmax when reporting confidence; `ece_before`/`after`
107/// are the measured Expected Calibration Error (honest provenance).
108#[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/// Shard coordinates (1-based, gguf-split style).
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ShardInfo {
120    pub no: usize,
121    pub count: usize,
122}
123
124/// Recon-argmin routing parameters (spec §9; P1 signal-consistency):
125/// E = ‖(φ−mean) − B·Bᵀ(φ−mean)‖² / ‖φ−mean‖²; pick argmin over skills.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct SelectionDescriptor {
128    /// "mse" (normalized reconstruction error) — the only metric today.
129    pub metric: String,
130    /// Backbone layer whose mean-pooled hidden is φ(x).
131    pub phi_layer: usize,
132    /// Subspace mean, f16 LE base64, len = hidden.
133    pub mean: String,
134    /// Orthonormal basis rows, f16 LE base64, len = rank·hidden.
135    pub basis: String,
136    pub rank: usize,
137}
138
139/// One skill of the swarm (spec §9; Patent 15 per-skill record).
140#[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    /// Layers this skill specializes (a proper subset).
146    #[serde(default)]
147    pub layers: Vec<usize>,
148    /// Selection descriptor for recon-argmin routing (208c, P1):
149    /// per-skill affine subspace over φ(x) = mean-pooled hidden state.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub selection: Option<SelectionDescriptor>,
152    /// Optional input-mask task name (208b), applied with the skill.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub input_mask_task: Option<String>,
155    /// Measured quality (claim 16): overlaid vs backbone, held-out.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub quality: Option<serde_json::Value>,
158}
159
160/// Hex-encoded hash64 per optional section (u64 as JSON number would
161/// lose precision past 2^53).
162#[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/// Chat template + generation stop tokens carried by the container.
173#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
174pub struct TokenizerBundle {
175    /// Jinja chat template (chat_template.jinja / tokenizer_config.json)
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub chat_template: Option<String>,
178    /// All ids that terminate generation (generation_config + im_end)
179    #[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/// One tensor directory entry.
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct TensorEntry {
194    pub name: String,
195    pub dtype: TensorDtype,
196    pub shape: Vec<usize>,
197    /// Offset relative to the OWNING shard's `data_off`, multiple of 64.
198    pub off: u64,
199    pub nbytes: u64,
200    /// Runtime-only: which shard's mmap holds the bytes (0 for the
201    /// single-file case; not part of the 56-byte record).
202    pub shard: usize,
203    /// `hash64` of the tensor bytes.
204    pub hash: u64,
205}
206
207impl TensorEntry {
208    pub fn n_elems(&self) -> usize {
209        self.shape.iter().product()
210    }
211}
212
213/// Input for the Rust writer: one tensor with its encoded bytes.
214#[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/// Sparse index entry — precomputed per-task per-layer active group IDs.
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct SparseIndexEntry {
225    pub task_id: u32,
226    pub layer_idx: usize,
227    /// Active quant-group indices for FFN (sorted, group = 32 neurons).
228    pub active_ffn_groups: Vec<u16>,
229    /// Active head indices for attention (sorted).
230    pub active_heads: Vec<u8>,
231}
232
233/// Section ranges parsed from the fixed envelope.
234#[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    /// hash64 of the header JSON bytes (reserved [0x70]); 0 = absent.
244    header_hash: u64,
245    /// hash64 of the tensor-directory bytes (reserved [0x78]); 0 = absent.
246    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
263/// A loaded CMF model: metadata owned, weights zero-copy via mmap.
264pub struct CmfModel {
265    pub path: PathBuf,
266    pub header: CmfHeader,
267    pub required_features: u32,
268    pub tensors: Vec<TensorEntry>,
269    /// name-hash → tensor index. Keying on the hash (not the name) avoids
270    /// cloning every tensor name into the map at `open()` — that halves the
271    /// open-time allocations and the map's footprint, which matters for large
272    /// MoE / skills files with tens of thousands of tensors. A genuine 64-bit
273    /// hash collision between two *distinct* names — astronomically unlikely —
274    /// lands in `name_overflow`, so lookups stay exact.
275    by_name: HashMap<u64, u32>,
276    name_overflow: Vec<u32>,
277    pub masks: MaskCatalog,
278    pub sparse_index: Vec<SparseIndexEntry>,
279    /// Embedded tokenizer.json bytes, if present.
280    pub vocab: Option<Vec<u8>>,
281    backing: Backing,
282    data_off: u64,
283    envelope: Envelope,
284    /// Shards 2..N (spec §10): (backing, data_off) per extra file;
285    /// `TensorEntry.shard` 0 = this file, i>0 = extra_shards[i-1].
286    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    /// Open and strictly validate a CMF v2 file. Any inconsistency is an
302    /// error — this function never substitutes defaults.
303    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                // Decode touches every weight page each token, so tell
314                // the kernel up front: WillNeed front-loads readahead
315                // (first-token page-fault storm becomes streaming I/O —
316                // this is TTFT on phones, where the file is a large
317                // share of RAM). Advisory only: a memory-pressured
318                // kernel is free to ignore it. CMF_MMAP_ADVISE=0 turns
319                // it off; CMF_MLOCK=1 additionally tries to pin the
320                // mapping (needs RLIMIT_MEMLOCK headroom — refusal is
321                // logged, not fatal).
322                #[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        // Header JSON
355        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        // Tensor directory
359        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            // Payload-dependent lengths (vbit): exact check against the
394            // width header, bounds-before-slice (roadmap §4.9).
395            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        // Duplicate names would silently shadow each other in the
402        // HashMap (directory scan and by_name would disagree) — refuse
403        // the file instead (roadmap §4.9).
404        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), // hash collision of distinct names
416                None => {
417                    by_name.insert(h, i as u32);
418                }
419            }
420        }
421
422        // Masks
423        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        // Vocab (tokenizer.json)
431        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        // Sparse index
438        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    /// Open a sharded model (spec §10): pass shard 1; siblings found by
471    /// the `-{no:05}-of-{count:05}.cmf` convention. Directories merge;
472    /// masks/vocab/index/skills come from shard 1.
473    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); // not sharded — plain open
478        };
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); // reserved
555        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    // ───────────────────────── access ─────────────────────────
679
680    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    /// Directory index of a tensor by name (same resolution as
689    /// [`Self::tensor`] — engines must not re-scan the directory). O(1) via the
690    /// name-hash index; the name is verified against the entry so a hash
691    /// collision can never return the wrong tensor, and the rare distinct-name
692    /// collision falls back to the tiny overflow list.
693    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    /// Tensor-source indirection (spec §9, Patent 15 fig3/302): the
708    /// skill's replacement is read IN PLACE OF the backbone tensor —
709    /// either/or, never combined. None skill → backbone directly.
710    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    /// The per-skill delta index view (claim 2): directory entries of
720    /// one skill — exactly the byte ranges lazy loading pages in.
721    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    /// Zero-copy bytes of a tensor from the mmap'd data section.
729    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    /// All bytes of the primary mapping (GPU path: no-copy Metal buffer
737    /// over the same mmap — unified memory, zero copying).
738    pub fn primary_bytes(&self) -> &[u8] {
739        self.backing.bytes()
740    }
741
742    /// Absolute offset of the tensor within the primary mapping
743    /// (None for tensors from sibling shards).
744    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    /// Tensors belonging to layer `i` (prefix `model.layers.{i}.`).
760    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    /// Total parameter count estimated from matrix tensors (ndim ≥ 2).
769    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    /// Recompute all tensor hashes; returns human-readable problems
778    /// (empty = file intact).
779    pub fn verify(&self) -> Vec<String> {
780        let mut problems = Vec::new();
781
782        // Section-level integrity (spec §8.1). Zero/absent = legacy file.
783        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    /// Approximate active weight bytes under a mask, from real tensor
830    /// sizes in the directory (not from a formula).
831    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    // ───────────────────────── writer ─────────────────────────
856
857    /// Write a CMF v2 file. Offsets, alignment, hashes and the sparse
858    /// index are computed here — the caller supplies content only.
859    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        // Directory + data layout.
869        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        // Section hashes go INTO the header (so the envelope's header
927        // hash transitively covers them), then the header is serialized.
928        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        // Section offsets.
952        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        // Envelope.
963        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()); // flags
967        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        // Reserved bytes carry header/dir integrity (spec §8.1).
980        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        // Write out.
985        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
1057// ───────────────────── sparse index (§7 of the spec) ─────────────────────
1058
1059/// Build the sparse index from mask bitfields: a 32-neuron FFN group is
1060/// active if it contains at least one active bit.
1061pub 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                    // Group g covers bits [g*32, g*32+32) = bytes [g*4, g*4+4).
1073                    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
1104/// `[u32 n_entries][u32 reserved]` then per entry:
1105/// `[u32 task][u32 layer][u32 n_groups][u32 n_heads][u16×g][u8×h][pad→4]`.
1106pub 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/// Errors from CMF operations. Every failure mode is loud.
1167#[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}