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, 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;
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/// `TensorSpec` with a borrowed payload — see [`CmfModel::write_ref`].
223pub struct TensorSpecRef<'a> {
224    pub name: String,
225    pub dtype: TensorDtype,
226    pub shape: Vec<usize>,
227    pub data: &'a [u8],
228}
229
230/// Sparse index entry — precomputed per-task per-layer active group IDs.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct SparseIndexEntry {
233    pub task_id: u32,
234    pub layer_idx: usize,
235    /// Active quant-group indices for FFN (sorted, group = 32 neurons).
236    pub active_ffn_groups: Vec<u16>,
237    /// Active head indices for attention (sorted).
238    pub active_heads: Vec<u8>,
239}
240
241/// Section ranges parsed from the fixed envelope.
242#[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    /// hash64 of the header JSON bytes (reserved [0x70]); 0 = absent.
252    header_hash: u64,
253    /// hash64 of the tensor-directory bytes (reserved [0x78]); 0 = absent.
254    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
271/// A loaded CMF model: metadata owned, weights zero-copy via mmap.
272pub struct CmfModel {
273    pub path: PathBuf,
274    pub header: CmfHeader,
275    pub required_features: u32,
276    pub tensors: Vec<TensorEntry>,
277    /// name-hash → tensor index. Keying on the hash (not the name) avoids
278    /// cloning every tensor name into the map at `open()` — that halves the
279    /// open-time allocations and the map's footprint, which matters for large
280    /// MoE / skills files with tens of thousands of tensors. A genuine 64-bit
281    /// hash collision between two *distinct* names — astronomically unlikely —
282    /// lands in `name_overflow`, so lookups stay exact.
283    by_name: HashMap<u64, u32>,
284    name_overflow: Vec<u32>,
285    pub masks: MaskCatalog,
286    pub sparse_index: Vec<SparseIndexEntry>,
287    /// Embedded tokenizer.json bytes, if present.
288    pub vocab: Option<Vec<u8>>,
289    backing: Backing,
290    data_off: u64,
291    envelope: Envelope,
292    /// Shards 2..N (spec §10): (backing, data_off) per extra file;
293    /// `TensorEntry.shard` 0 = this file, i>0 = extra_shards[i-1].
294    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    /// Open and strictly validate a CMF v2 file. Any inconsistency is an
310    /// error — this function never substitutes defaults.
311    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                // Decode touches every weight page each token, so tell
322                // the kernel up front: WillNeed front-loads readahead
323                // (first-token page-fault storm becomes streaming I/O —
324                // this is TTFT on phones, where the file is a large
325                // share of RAM). Advisory only: a memory-pressured
326                // kernel is free to ignore it. CMF_MMAP_ADVISE=0 turns
327                // it off; CMF_MLOCK=1 additionally tries to pin the
328                // mapping (needs RLIMIT_MEMLOCK headroom — refusal is
329                // logged, not fatal).
330                #[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        // Header JSON
363        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        // Tensor directory
367        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            // Payload-dependent lengths (vbit): exact check against the
402            // width header, bounds-before-slice (roadmap §4.9).
403            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        // Duplicate names would silently shadow each other in the
410        // HashMap (directory scan and by_name would disagree) — refuse
411        // the file instead (roadmap §4.9).
412        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), // hash collision of distinct names
424                None => {
425                    by_name.insert(h, i as u32);
426                }
427            }
428        }
429
430        // Masks
431        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        // Vocab (tokenizer.json)
439        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        // Sparse index
446        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    /// Open a sharded model (spec §10): pass shard 1; siblings found by
479    /// the `-{no:05}-of-{count:05}.cmf` convention. Directories merge;
480    /// masks/vocab/index/skills come from shard 1.
481    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); // not sharded — plain open
486        };
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); // reserved
563        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    // ───────────────────────── access ─────────────────────────
687
688    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    /// Directory index of a tensor by name (same resolution as
697    /// [`Self::tensor`] — engines must not re-scan the directory). O(1) via the
698    /// name-hash index; the name is verified against the entry so a hash
699    /// collision can never return the wrong tensor, and the rare distinct-name
700    /// collision falls back to the tiny overflow list.
701    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    /// Tensor-source indirection (spec §9, Patent 15 fig3/302): the
716    /// skill's replacement is read IN PLACE OF the backbone tensor —
717    /// either/or, never combined. None skill → backbone directly.
718    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    /// The per-skill delta index view (claim 2): directory entries of
728    /// one skill — exactly the byte ranges lazy loading pages in.
729    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    /// Zero-copy bytes of a tensor from the mmap'd data section.
737    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    /// All bytes of the primary mapping (GPU path: no-copy Metal buffer
745    /// over the same mmap — unified memory, zero copying).
746    pub fn primary_bytes(&self) -> &[u8] {
747        self.backing.bytes()
748    }
749
750    /// Absolute offset of the tensor within the primary mapping
751    /// (None for tensors from sibling shards).
752    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    /// Tensors belonging to layer `i` (prefix `model.layers.{i}.`).
768    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    /// Total parameter count estimated from matrix tensors (ndim ≥ 2).
777    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    /// Recompute all tensor hashes; returns human-readable problems
786    /// (empty = file intact).
787    pub fn verify(&self) -> Vec<String> {
788        let mut problems = Vec::new();
789
790        // Section-level integrity (spec §8.1). Zero/absent = legacy file.
791        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    /// Approximate active weight bytes under a mask, from real tensor
838    /// sizes in the directory (not from a formula).
839    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    // ───────────────────────── writer ─────────────────────────
864
865    /// Write a CMF v2 file. Offsets, alignment, hashes and the sparse
866    /// index are computed here — the caller supplies content only.
867    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    /// `write` with BORROWED tensor payloads — repack tools slice the
887    /// source file's mmap directly, so a 19 GB container rewrites without
888    /// materializing its tensors in RAM (the OS streams pages through).
889    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        // Directory + data layout.
899        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        // Section hashes go INTO the header (so the envelope's header
957        // hash transitively covers them), then the header is serialized.
958        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        // Section offsets.
982        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        // Envelope.
993        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()); // flags
997        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        // Reserved bytes carry header/dir integrity (spec §8.1).
1010        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        // Write out.
1015        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
1087// ─────────────────── one-pass streaming writer (§8.4) ───────────────────
1088
1089/// Writes a CMF file in a single pass, payloads first.
1090///
1091/// [`CmfModel::write_ref`] needs every payload addressable at once, so a
1092/// converter has to hold the whole encoded model — RAM, or a spill file it
1093/// then copies into the output. For a 300B-class MoE that is ~120 GB written
1094/// twice. This writer instead reserves a gap at the head of the file, appends
1095/// each payload the moment it is encoded, and patches the envelope, header and
1096/// directory into that gap at the end. The bytes are written once and peak
1097/// disk cost is the finished file.
1098///
1099/// The gap is the one thing that can go wrong: the directory is not sized
1100/// until the last tensor arrives. [`CmfStreamWriter::finish`] therefore
1101/// refuses loudly if the head does not fit rather than truncating it, and
1102/// [`CmfStreamWriter::head_reserve_for`] gives callers a safe estimate.
1103/// What a resumed writer recovers from its manifest: the tensors already on
1104/// disk and any milestones the producer noted.
1105pub 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    /// Absolute offset of the weight blob — also the size of the reserved gap.
1114    data_off: u64,
1115    /// Write cursor, relative to `data_off`.
1116    cursor: u64,
1117    entries: Vec<TensorEntry>,
1118    /// Append-only sidecar describing every payload already on disk. A Colab
1119    /// box can vanish mid-conversion; with this the finished payloads can be
1120    /// turned into a valid file instead of re-encoding for hours.
1121    manifest: Option<BufWriter<File>>,
1122}
1123
1124impl CmfStreamWriter {
1125    /// A gap that comfortably holds the head for `n_tensors` whose names run
1126    /// to `avg_name` bytes: the directory's fixed records, the name pool, the
1127    /// envelope, and a header JSON with room for arch metadata — then doubled,
1128    /// because being wrong here costs a whole re-run.
1129    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        // Tripled, on top of a megabyte of slack that is already ~20x a
1133        // small model's directory. The asymmetry is deliberate: an
1134        // over-estimate costs zeros at the head of the file, an
1135        // under-estimate costs the entire conversion that produced it.
1136        align_to(3 * head as u64, DATA_ALIGNMENT).max(1 << 20)
1137    }
1138
1139    /// `gap` bytes are reserved for envelope + header + directory.
1140    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    /// Append one tensor. The payload is consumed here, so the caller can drop
1156    /// it immediately — that is the entire point of this writer.
1157    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    /// Start recording a sidecar manifest at `path`. One JSON line per
1220    /// tensor, flushed as it goes, plus a first line pinning the gap size.
1221    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    /// Keep recording into an existing manifest — for a writer from
1230    /// [`CmfStreamWriter::resume`], whose earlier lines must survive.
1231    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    /// Rebuild a writer over an output file whose payloads are already on
1239    /// disk, from the manifest that recorded them. The file is reopened for
1240    /// writing without truncation and the cursor is placed after the last
1241    /// recorded tensor, so `finish` can complete a conversion that died.
1242    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            // A truncated last line is expected if the process was killed
1267            // mid-write; it is dropped, not an error.
1268            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                // Everything up to here is durable; anything the manifest
1275                // records after the LAST mark belongs to a shard that was
1276                // interrupted and will be redone, so it must not be kept —
1277                // otherwise the redo appends those tensors a second time.
1278                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    /// Note a milestone in the manifest — a source shard fully consumed, say.
1332    /// Resume reads these back, which is what lets a restart skip work whose
1333    /// payloads are already in the file rather than only skipping tensors it
1334    /// happens to recognise by name.
1335    pub fn mark(&mut self, note: &str) -> Result<(), CmfError> {
1336        // The payloads must be on disk BEFORE the mark claims they are.
1337        // Without this the manifest runs ahead of a buffered writer, and a
1338        // kill in between leaves a record of bytes that were never written.
1339        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    /// Bytes of weight blob written so far.
1357    pub fn data_len(&self) -> u64 {
1358        self.cursor
1359    }
1360
1361    /// Write the trailing sections, then patch the head into the reserved gap.
1362    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
1478// ───────────────────── sparse index (§7 of the spec) ─────────────────────
1479
1480/// Build the sparse index from mask bitfields: a 32-neuron FFN group is
1481/// active if it contains at least one active bit.
1482pub 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                    // Group g covers bits [g*32, g*32+32) = bytes [g*4, g*4+4).
1494                    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
1525/// `[u32 n_entries][u32 reserved]` then per entry:
1526/// `[u32 task][u32 layer][u32 n_groups][u32 n_heads][u16×g][u8×h][pad→4]`.
1527pub 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/// Errors from CMF operations. Every failure mode is loud.
1588#[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}