Instructions to use infosave/cmf with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- cortiq
How to use infosave/cmf with cortiq:
# one Rust binary, no additional dependencies cargo install cortiq-cli # or a prebuilt binary from github.com/infosave2007/cmf/releases hf download infosave/cmf --include "*.cmf" --local-dir . ls *.cmf # some repos ship more than one quantization
cortiq run FILE.cmf --prompt "What is the capital of France?"
cortiq serve FILE.cmf --port 8080 # OpenAI-compatible server
- Notebooks
- Google Colab
- Kaggle
|
Download SPEC.ru.md from infosave/cmf: direct link, hf CLI and curl.
- Browser
- Download file 49.6 kB
-
https://huggingface.co/infosave/cmf/resolve/main/SPEC.ru.md
- Command line
-
hf download hf://infosave/cmf/SPEC.ru.md
-
curl -L -o SPEC.ru.md https://huggingface.co/infosave/cmf/resolve/main/SPEC.ru.md
49.6 kB
| # CMF v2 — Спецификация формата | |
| *Языки: [English](SPEC.md) · **Русский** · [中文](SPEC.zh.md)* | |
| **Cortiq Model Format** — единый файл, несущий всё необходимое для | |
| разреженного, маршрутизируемого по задачам инференса: квантованные веса, | |
| токенизатор, маски по задачам, предвычисленный разреженный | |
| индекс — и, что уникально, **рой скиллов**, разделяющих одну базовую модель | |
| (Patent 15). | |
| > Нормативный источник: этот документ. Референсные | |
| > реализации: Rust-ридер/рантайм (`crates/cortiq-core`, | |
| > `crates/cortiq-engine`), Python-райтер (`converter/`) и | |
| > ридер на Python без зависимостей (`python/cmf_reader.py`, только numpy). | |
| Три требования, в порядке приоритета: | |
| 1. **Корректность.** Никаких режимов тихого повреждения: строгий magic, | |
| версия, `required_features`, границы на каждую секцию, 64-битный хэш на | |
| каждый тензор. Файл либо валиден, либо open() возвращает ошибку — третьего | |
| состояния нет. | |
| 2. **Скорость.** Секция весов выровнена по странице для mmap, каждый тензор | |
| выровнен по 64 байтам (SIMD без копирования), каталог тензоров бинарный — | |
| читается без разбора. Холодные (замаскированные) веса не стоят RSS. | |
| 3. **Компактность.** Маски упакованы побитово (1 бит на нейрон), веса — | |
| q4/q8/переменной битности, весь файл адресуется одним 128-байтовым | |
| конвертом. | |
| Гармония достигается не количеством фич, а **единым каноном**: одна | |
| раскладка на уровень (конверт, каталог, квант-блок, маска), байт-в-байт | |
| совместимая с валидированным форматом `.vmfc` v2 там, где домены | |
| пересекаются (каталог тензоров, раскладки квантования, `hash64`). Никогда | |
| двух определений одного и того же. | |
| Физическая основа (VMF): модель — это вакуумный конденсат 𝒲; скилл — его | |
| регулярный кор выше критической плотности; маска задачи выбирает активное | |
| подмножество, не меняя веса. Формат несёт следствия | |
| этой физики (двухполевое 𝒲×θ квантование, Born-важность, критический порог | |
| маски) — но **только те, что подтверждены измерением**. | |
| --- | |
| ## 1. Конверт (фиксированные 128 байт) | |
| Все целые числа — little-endian. | |
| ``` | |
| [0x00 : 0x04] magic = b"CMF\x01" (4 bytes) | |
| [0x04 : 0x08] version : u32 = 2 | |
| [0x08 : 0x0C] flags : u32 (reserved, 0) | |
| [0x0C : 0x10] required_features : u32 (bitmask, §1.1) | |
| [0x10 : 0x18] header_off : u64 (= 128) | |
| [0x18 : 0x20] header_len : u64 — JSON header (§2) | |
| [0x20 : 0x28] dir_off : u64 — tensor directory (§3) | |
| [0x28 : 0x30] dir_len : u64 | |
| [0x30 : 0x38] data_off : u64 — weight blob; multiple of 4096 (§4) | |
| [0x38 : 0x40] data_len : u64 | |
| [0x40 : 0x48] masks_off : u64 — masks section (§5); 0 = absent | |
| [0x48 : 0x50] masks_len : u64 | |
| [0x50 : 0x58] vocab_off : u64 — tokenizer (§6); 0 = absent | |
| [0x58 : 0x60] vocab_len : u64 | |
| [0x60 : 0x68] index_off : u64 — sparse index (§7); 0 = absent | |
| [0x68 : 0x70] index_len : u64 | |
| [0x70 : 0x80] reserved : 16 bytes (§8.1: header/dir hashes) | |
| ``` | |
| Порядок секций на диске: конверт → JSON-заголовок → каталог → **блоб | |
| весов (выровнен по 4096)** → маски → словарь → разреженный индекс. Ридер | |
| ОБЯЗАН адресовать секции ТОЛЬКО через конверт, никогда не предполагая | |
| порядка. | |
| ### 1.1 `required_features` | |
| Бит, которого ридер не знает → ошибка `UnsupportedFeature` (fail-fast; | |
| никакого «читаем как можем»). | |
| | bit | name | значение | | |
| |-----|----------------|---------| | |
| | 0 | `TENSOR_DIR` | бинарный каталог тензоров (всегда установлен в v2) | | |
| | 1 | `BINARY_MASKS` | секция масок (§5) присутствует | | |
| | 2 | `QUANT_2F` | каталог содержит тензоры `q8_2f`/`vbit` (двухполевое 𝒲×θ квантование) | | |
| | 3 | `DELTA_MASKS` | reserved: XOR-дельты масок от родителя | | |
| | 4 | `HOT_PACKS` | reserved: материализованные плотные срезы | | |
| | 5 | `LOOP_MASKS` | ряды масок хранятся ПО ВИЗИТАМ (физические слои × циклы, проход-мажорно) — у зацикленного трансформера каждый проход несёт независимую маску (§5.1) | | |
| | 6 | `SKILL_FILE` | файл — САМОСТОЯТЕЛЬНЫЙ СКИЛЛ: частичный набор тензоров, вырезанный против конкретной базы и привязанный ключом `SkillRecord.base_dir_hash` (§9.1). Не запускается — прикладывается через `cortiq skill apply` | | |
| Неизвестные поля **header-JSON** игнорируются (аддитивная эволюция); | |
| ломающие изменения проходят только через биты фич или инкремент `version`. | |
| ### 1.2 Правила валидации (нормативные) | |
| Ридер ОБЯЗАН вернуть ошибку (не значение по умолчанию, не предупреждение), | |
| когда: | |
| - magic ≠ `CMF\x01` → `InvalidMagic`; | |
| - `version` ≠ 2 → `UnsupportedVersion` (v1 мертва: реальных файлов v1 | |
| не существует, программа поддержки не будет запущена); | |
| - установлен неизвестный бит `required_features` → `UnsupportedFeature`; | |
| - любая секция выходит за EOF, `data_off` не кратен 4096, | |
| `off + nbytes` тензора превышает `data_len` → `Bounds`; | |
| - имя тензора не UTF-8, dtype неизвестен, `ndim > 6` → `Parse`. | |
| Проверка хэша тензоров — по требованию (`cortiq verify`, флаг загрузчика), | |
| не при каждом open: страницы mmap читаются лениво. | |
| ## 2. JSON-заголовок | |
| UTF-8 JSON, без выравнивания. Критичные для машины данные живут в бинарных | |
| секциях; JSON несёт архитектуру и происхождение — те части, что читает | |
| человек. | |
| ```jsonc | |
| { | |
| "format": "cmf", | |
| "version": 2, | |
| "arch": { | |
| "arch_name": "qwen3.5", | |
| "hidden_size": 5120, "intermediate_size": 17408, | |
| "num_layers": 64, "num_attention_heads": 24, "num_kv_heads": 4, | |
| "head_dim": 256, "vocab_size": 248320, | |
| "layer_types": ["LinearAttention", "...", "FullAttention"], | |
| "rms_norm_eps": 1e-6, | |
| "norm_style": "qwen", // "qwen": x̂·w | "gemma": x̂·(1+w) | |
| "rope_theta": 1000000.0, | |
| "tie_word_embeddings": false, | |
| "max_position_embeddings": 262144, | |
| "linear_conv_kernel_dim": 4, | |
| "linear_num_key_heads": 16, "linear_num_value_heads": 48 | |
| }, | |
| "quant_type": "Q4_BLOCK", // informational default; truth = per-tensor dtype in the directory | |
| "provenance": { "tool": "…", "source_model": "…" } // optional, free-form | |
| } | |
| ``` | |
| `norm_style` обязателен для движка: gemma-стиль `(1+w)`, применённый к | |
| весам Qwen, — это тихий мусор во всех ~130 нормализациях | |
| прямого прохода. | |
| Диспетчеризация возможностей **управляется наличием тензоров**: движок | |
| решает по-слойно, какие операторы применять, по тому, что есть в каталоге | |
| (q/k-смещения, qk-нормы, выходной гейт по ширине проекции, MoE-роутер, | |
| GDN-проекции) — а не по совпадению имён моделей. Новые модели известного | |
| семейства загружаются с нулевыми изменениями движка. | |
| ### 2.1 MTP — предсказание нескольких токенов (опционально) | |
| Если модель несёт MTP-голову (в стиле DeepSeek/Qwen), arch объявляет: | |
| ```jsonc | |
| "mtp": { "num_layers": 1, "share_lm_head": true, "share_embed": true } | |
| ``` | |
| Тензоры MTP — обычные записи каталога под каноническими именами | |
| (`model.mtp.*`): `enorm.weight`, `hnorm.weight`, | |
| `eh_proj.weight [hidden, 2·hidden]`, `layers.{i}.*` (стандартный | |
| блок трансформера), `norm.weight`. | |
| Семантика: `x = eh_proj·[enorm(embed(t_{p+1})); hnorm(h_p)]` — эмбеддинг | |
| ПЕРВЫМ (проверено оракулом: обратный порядок даёт ровно 0% принятия) | |
| → блок → общий lm_head → черновик токена `t_{p+2}`. Ридер не | |
| обязан исполнять MTP (метаданные + обычные тензоры, аддитивная | |
| эволюция, без бита фичи); рантайм CMF использует голову для | |
| спекулятивного декодинга со строгой гарантией: **выход в точности равен | |
| обычному жадному декодингу** — отклонённый черновик откатывается из KV. | |
| ### 2.2 MoE — FFN со смесью экспертов (опционально) | |
| Если модель несёт MoE-слои (Qwen2-MoE / Qwen3-MoE / Qwen3.5-MoE), | |
| arch объявляет: | |
| ```jsonc | |
| "moe": { | |
| "num_experts": 256, "top_k": 8, "moe_intermediate_size": 512, | |
| "norm_topk_prob": true, // Qwen2-MoE: false | |
| "shared_expert_intermediate_size": 512 // absent if no shared expert | |
| } | |
| ``` | |
| Тензоры — обычные записи каталога под HF-именами: | |
| ``` | |
| model.layers.{i}.mlp.gate.weight [num_experts, hidden] router | |
| model.layers.{i}.mlp.experts.{e}.{gate,up,down}_proj.weight | |
| model.layers.{i}.mlp.shared_expert.{gate,up,down}_proj.weight | |
| model.layers.{i}.mlp.shared_expert_gate.weight [1, hidden] | |
| ``` | |
| Какие слои являются MoE — определяется ПРИСУТСТВИЕМ роутера в | |
| каталоге (по-слойно, не по-модельно): `mlp_only_layers`/ | |
| `decoder_sparse_step` у Qwen2-MoE дают смешанные модели, и плотные | |
| слои сохраняют обычные `mlp.*_proj`. | |
| Семантика исполнения (паритет с HF, под воротами `tests/moe_parity.sh` по | |
| четырём семействам, включая фьюженную раскладку AgentWorld): softmax по ВСЕМ | |
| логитам роутера → top-k (ничьи: меньший индекс, порядок torch.topk) → если | |
| `norm_topk_prob`, перенормировка выбранных k → Σwₑ·FFNₑ(x); общий | |
| эксперт всегда добавляется с весом `sigmoid(shared_expert_gate·x)`. | |
| Эксперты остаются квантованными в mmap; на токен затрагиваются только | |
| страницы выбранных k — та же история резидентности, что и у скиллов. | |
| Райтерам СЛЕДУЕТ раскладывать тензоры экспертов слоя ролево-смежно | |
| (все `gate_proj` экспертов 0…N−1 подряд, затем все `up_proj`, затем все | |
| `down_proj`) — GPU-бэкенды тогда работают с банком экспертов слоя как с | |
| одним регионом вместо сборки сотен слайсов; нативный импортёр и | |
| `moe-defrag` оба пишут такой порядок. Каждый | |
| эксперт — отдельная запись каталога со СВОИМ dtype: это и есть носитель | |
| по-экспертного распределения битов (P15 claim 12) — реализовано, под воротами | |
| `tests/moe_vbit.sh`; B-поле (частоты выбора роутером через | |
| `--route-stats`) измерено end-to-end на модели 35B. | |
| ## 3. Каталог тензоров | |
| Байт-в-байт раскладка `.vmfc` v2 (единый канон, общий референсный | |
| парсер): | |
| ``` | |
| [0 : 8 ] count : u64 | |
| [8 : 16] pool_off : u64 (name-pool offset from section start) | |
| [16 : 16 + count·56] 56-byte records: | |
| name_off : u32 (relative to pool_off) | |
| name_len : u16 | |
| dtype : u8 (§3.1) | |
| ndim : u8 (≤ 6) | |
| shape : u32 × 6 (zero-padded tail) | |
| off : u64 (RELATIVE to data_off; multiple of 64) | |
| nbytes : u64 | |
| hash : u64 (hash64 of the tensor bytes, §8) | |
| [pool_off : …] UTF-8 name pool | |
| ``` | |
| Имена тензоров **1:1 с исходной моделью** | |
| (`model.layers.{i}.mlp.gate_proj.weight`, `model.embed_tokens.weight`, | |
| `lm_head.weight`, …). Формат не предписывает набор тензоров: | |
| каталог — единственный источник истины о том, что содержит блоб. | |
| «Вычисляемой раскладки» не существует. | |
| ### 3.1 `dtype` | |
| Нумерация общая с `.vmfc` (id никогда не переиспользуются): | |
| | id | name | статус в CMF v2 | | |
| |----|-----------|------------------| | |
| | 0 | `f32` | ✅ чтение/запись | | |
| | 1 | `f16` | ✅ чтение/запись (нормы и 1-D всегда f16) | | |
| | 2 | `bf16` | ✅ чтение/запись | | |
| | 3 | `q8_row` | ✅ чтение/запись | | |
| | 4 | `q4_block`| ✅ чтение/запись | | |
| | 5 | `mix8_4` | reserved | | |
| | 6 | `u8` | reserved | | |
| | 7 | `q4_col` | reserved | | |
| | 8 | `vbit` | ✅ чтение/запись (бит `QUANT_2F`), переменная 3–8 бит | | |
| | 9 | `q8_2f` | ✅ чтение/запись (бит `QUANT_2F`), 𝒲×θ | | |
| | 10 | `vbit_ro` | ✅ чтение/запись — `vbit` + таблица row-offsets в файле (O(1) доступ к строке); дефолт конвертера для `--quant vbit` | | |
| | 11 | `q4_tiled`| ✅ чтение/запись — q4 интерливными тайлами `[f16 scale][16B nibbles]` (`--quant q4t`) | | |
| | 12 | `q1` | ✅ чтение/запись — 1-битные бинарные веса, только для 1-бит-ОБУЧЕННЫХ моделей (`--quant q1`); тайл `[f16 scale][4B бит-знаков]` на 32-группу, `w = s·(2·bit−1)` | | |
| | 13 | `q1s` | ✅ чтение/запись — база `q1` + разреженный оверлей выбросов высокой точности (1-битный PTQ обычных чекпойнтов) | | |
| | 14 | `q1t` | ✅ чтение/запись — тернарные `{−s, 0, +s}` base-3-тайлы + построчный оверлей выбросов (~2.25 bpw + оверлей) | | |
| | 15 | `q4tp` | ✅ чтение/запись — ниббли `q4_tiled`, но масштаб тайла — 5-битная ступень на лестнице строки (`--quant q4tp`, либо `requant` на месте) | | |
| ### 3.2 Раскладки квантования (канон = `.vmfc`: «сначала кванты, затем скейлы») | |
| - **`q8_row`** (только 2-D `[out, in]`): | |
| `[int8 : out·in][f16 : out]` — один скейл на строку, | |
| `w = q[o,i]·scale[o]`, `scale[o] = absmax(row_o)/127`. | |
| - **`q4_block`**: группы по 32 над уплощённым тензором, с нулевым добиванием; | |
| `[u8 : ceil(n/32)·16][f16 : ceil(n/32)]`. | |
| - **`q4tp`** (только 2-D, `in % 32 == 0`): | |
| `[ниббли: rows·gpr·16][параметры строки: rows × (f16 lo, f16 step)] | |
| [коды: rows × ceil(gpr·5/8), 5 бит LSB-first, выровнено по строке]`, | |
| `gpr = in/32`. Масштаб тайла — `2^(lo[r] + code·step[r])`, поэтому читатель | |
| раскрывает 32-ступенчатую лестницу строки один раз и дальше берёт масштабы | |
| из таблицы. Значения и порядок нибблов совпадают с `q4_tiled`, отличается | |
| только представление масштаба: 4.17 бит/вес против 4.50 — f16-масштаб | |
| занимал 11% q4t-файла. `lo`/`step` берутся из точных min/max логарифма | |
| масштаба по строке, поэтому код никогда не выходит за диапазон и | |
| escape-механизм не нужен. Кодировщик ОБЯЗАН округлить `lo`/`step` до f16 | |
| **перед** выбором кодов и квантовать ниббли по восстановленному масштабу — | |
| иначе писатель и читатель разойдутся (та же ловушка, из-за которой q4-кодер | |
| сначала округляет свой масштаб). | |
| Ниблы: элемент `2k` младший, `2k+1` старший; `w = (q − 8)·scale`, | |
| `scale = absmax(group)/7`. | |
| - **1-D тензоры и тензоры < 32 элементов всегда `f16`** | |
| (точность нормализации при максимальном сжатии матриц). | |
| - **`q8_2f`**: `[int8][f16 row-scale][f16 col-field]`, | |
| `w = q·scale[o]·col[i]` — двухполевой Madelung-сплит 𝒲×θ, валидирован | |
| в vmfcore (+37% при равном размере; восстанавливает ~75% разрыва q8→f16 | |
| на выбросных входных каналах). | |
| - **`vbit`** (только 2-D, `in % 32 == 0`; P13 FIG.3): | |
| `[u8 bits: rows][f16 scales: rows·in/32][bit-packed rows, MSB-first, | |
| each row padded to a byte]`; `w = (u − L)·scale[r,g]`, | |
| `L = 2^{b−1}−1`, уровни b ∈ {3,4,5,6,8}, пол 3 (claim 13). | |
| Распределение b_r: water-filling по логарифму амплитуды строки к | |
| средней битности тензора; для MoE-экспертов бюджет ОБЩИЙ на всё | |
| семейство (слой × проекция): сдвиг `ā_expert − ā_family` | |
| эквивалентен совместному water-filling по строкам всех экспертов — громкий | |
| эксперт получает больше бит, тихий прижимается к полу (P15 | |
| claim 12; ворота `tests/moe_vbit.sh`). Опционально распределение берёт | |
| произведение с B-полем — частотами выбора роутером, собранными | |
| на калибровке (`b ∝ log2(A·B)`, усечённый Fisher). | |
| - **`q1s`** (только 2-D, `in % 32 == 0`): база `q1` (те же 6-байтные | |
| тайлы; выбросы ИСКЛЮЧЕНЫ из группового скейла), затем разреженный | |
| оверлей: `[u32 count]` и `count × { [u32 плоский-индекс][f16 значение] }` | |
| — веса-выбросы, сохранённые в полной точности (голографический | |
| перенос / стиль SpQR) и восстанавливаемые дословно при деквантовании. | |
| Переменная длина: `expected_nbytes` не определён, ридер доверяет | |
| записанному в каталоге размеру. Позволяет ОБЫЧНОМУ чекпойнту пережить | |
| 1 бит там, где чистый `q1` не может. | |
| - **`q1t`** (только 2-D, `in % 32 == 0`, `in` должен помещаться в | |
| `u16`): тернарный BitNet-b1.58-стиль `{−s, 0, +s}`. База: | |
| `на 32-группу { [f16 scale][7B base-3 коды] }` — 9-байтные тайлы, | |
| 5 тернарных значений на байт (3⁵ = 243 ≤ 256; код 0 → 0, 1 → +s, | |
| 2 → −s), ~2.25 бит/вес. Затем построчный оверлей выбросов: | |
| `[u32 row_ptr[rows+1]]` и записи `{ [u16 col][f16 значение] }`, | |
| сгруппированные по строкам (выбросы строки `r` — диапазон | |
| `[row_ptr[r], row_ptr[r+1])`; `col` — индекс внутри строки) — 4 | |
| байта на выброс, без бинарного поиска. Точное сохранение множества | |
| почти нулевых весов — решающий PTQ-выигрыш над бинарным. Переменная | |
| длина, правило размера как у `q1s`. | |
| ## 4. Блоб весов | |
| `data_off` кратен 4096 (выровненный по странице mmap); каждый тензор | |
| внутри начинается на границе 64 байт (SIMD-загрузки, кэш-линии). Нулевое | |
| добивание между тензорами. Ридер интерпретирует блоб только через | |
| каталог. | |
| ## 5. Секция масок | |
| Маска задачи = битовые поля «что активно» над общими весами | |
| (веса не меняются — принцип VMF: скилл выбирает подмножество | |
| конденсата). | |
| ``` | |
| [0 : 4] n_masks : u32 | |
| [4 : 8] meta_len : u32 | |
| [8 : 8 + meta_len] JSON meta (§5.1) | |
| […] mask blobs, each aligned to 8 from the section start | |
| ``` | |
| Один блоб маски (размеры выводятся из arch, без внутренних заголовков): | |
| ``` | |
| [n_layers × ffn_bytes] FFN bitfields ffn_bytes = ceil(intermediate_size / 8) | |
| [n_layers × head_bytes] head bitfields head_bytes = ceil(num_attention_heads / 8) | |
| [gates_bytes] layer_gates gates_bytes = ceil(num_layers / 8) | |
| [n_layers × expert_bytes] expert bitfields ОПЦИОНАЛЬНО — только когда мета маски | |
| ставит "has_expert_fields": true; | |
| expert_bytes = ceil(moe.num_experts / 8) | |
| ``` | |
| Порядок битов LSB-first: нейрон `i` = бит `i % 8` байта `i / 8`; бит | |
| установлен → активен. **Хвостовые биты за пределами размерности ОБЯЗАНЫ | |
| быть нулём** (иначе popcount видит фантомные нейроны/головы). | |
| Опциональная экспертная область (аддитивно: старые ридеры не читают | |
| дальше gates, а `blob_len` каждой маски явный) позволяет задачной маске | |
| сужать РОУТИНГ MoE: бит `e` строки слоя `l` установлен → эксперт `e` | |
| маршрутизируем для этой задачи; выбор идёт только по маршрутизируемым, | |
| softmax роутера ренормализуется по ним. Это переключаемый в рантайме | |
| близнец физического дефрага экспертов из §11.1 — один файл с полным | |
| набором экспертов обслуживает много специалистов (`cortiq moe-mask` | |
| пишет такие маски, `run --task <имя>` активирует; проверено | |
| токен-в-токен с эквивалентным рантайм-ограничением). Слой со строкой | |
| все-единицы — без ограничения; маска без области ничего не сужает. | |
| ### 5.1 JSON-мета маски | |
| ```jsonc | |
| { | |
| "default_task": "general", | |
| "masks": [{ | |
| "task_id": 0, "name": "general", "description": null, | |
| "sparsity": 0.62, | |
| "quality": { // null = NOT MEASURED (declaring 1.0 is forbidden) | |
| "metric": "heldout_ppl_ratio", "value": 0.97, | |
| "baseline_dense": 6.10, "n_samples": 512, "dataset_sha256": "…" | |
| }, | |
| "parent": null, "priority": "Fallback", "has_hot_pack": false, | |
| "blob_off": 4096, "blob_len": 139328 // relative to section start | |
| }] | |
| } | |
| ``` | |
| `quality` — это **контракт на held-out**, а не декларация: конвертер | |
| без измеренной метрики пишет `null`; рантайм логирует предупреждение при | |
| переключении на неизмеренную маску. | |
| ## 6. Секция токенизатора | |
| Байты HuggingFace `tokenizer.json`, дословно. Модель | |
| самодостаточна: один файл = одна единица распространения. Sidecar-файл | |
| остаётся отладочным запасным вариантом. | |
| ### 6.1 Chat-бандл (`header.tokenizer_config`) | |
| Файл — а не рантайм-бинарник — определяет поведение чата. Заголовок | |
| несёт опциональный блок (аддитивная эволюция, без бита фичи): | |
| ```json | |
| "tokenizer_config": { | |
| "chat_template": "<Jinja template from chat_template.jinja or tokenizer_config.json>", | |
| "eos_token_ids": [248044, 248045], | |
| "bos_token_id": null, | |
| "pad_token_id": 248055 | |
| } | |
| ``` | |
| Рантайм рендерит шаблон с семантикой HF (trim_blocks, | |
| lstrip_blocks, управление циклами, строковые методы Python) и | |
| останавливает генерацию на любом id из `eos_token_ids`. Ворота: | |
| `tests/chat_template_parity.sh` — рендер рантайма равен референсному | |
| jinja2 байт-в-байт. Файлы без блока получают ChatML-fallback. | |
| ## 7. Разреженный индекс | |
| Предвычисленный мост «маска → пропуск вычислений»: активные квант-группы | |
| FFN (по 32 нейрона каждая) и головы, на пару (задача, слой). | |
| > Честный статус: движок берёт активные индексы напрямую из битовых | |
| > полей маски; индекс читается и отображается CLI, но в исполнении не | |
| > использовался никогда. **Кандидат на деприкацию**: райтерам СЛЕДУЕТ | |
| > перестать его писать (ридеры продолжают понимать существующие файлы); | |
| > он оживёт, только если путь «маски × квантованный mmap» | |
| > материализуется с измеренным выигрышем. | |
| ``` | |
| [0 : 4] n_entries : u32 | |
| [4 : 8] reserved : u32 (0) | |
| entry (4-aligned): | |
| task_id : u32 | |
| layer_idx : u32 | |
| n_groups : u32 | |
| n_heads : u32 | |
| [u16 × n_groups] active FFN-group indices (sorted) | |
| [u8 × n_heads] active head indices (sorted) | |
| zero padding to a multiple of 4 | |
| ``` | |
| Группа активна, если содержит хотя бы один активный бит маски. | |
| ## 8. `hash64` | |
| Некриптографический 64-битный хэш байтов тензора: murmur3 `fmix64` над | |
| 64-битными LE-словами с позиционной солью `i·0x9E3779B97F4A7C15`, XOR-свёртка, | |
| `xor len`, финальный `fmix64`. Бит-в-бит совместим с | |
| `vmfcore.hash64` (Python) и `vmfcore::hash64` (Rust) — хэши | |
| общих тензоров совпадают между `.cmf` и `.vmfc` (дедупликация базовой модели между | |
| файлами скиллов бесплатна). | |
| Использование: `cortiq verify` (детекция повреждений), дедупликация, ключи кэша. | |
| ### 8.1 Хэши секций | |
| Целостность метаданных (не только тензоров): | |
| - Резерв конверта `[0x70:0x78]` = hash64(JSON-заголовок), `[0x78:0x80]` = | |
| hash64(каталог). Ноль = «отсутствует» (старые файлы проходят). | |
| - JSON-заголовок несёт `section_hashes` — hex hash64 | |
| масок/словаря/индекса (u64 как JSON-число потерял бы точность за | |
| 2^53). Хэш заголовка в конверте транзитивно покрывает их. | |
| - Сам конверт (первые 0x70 байт) не хэшируется: хэш не может | |
| защитить сам себя; повреждённые смещения ловятся границами/хэшами дальше | |
| по цепочке. | |
| - `cortiq verify` проверяет всю цепочку; единственный перевёрнутый байт | |
| заголовка — это ошибка. | |
| ### 8.2 Отсоединённая подпись (аутентичность, опционально) | |
| Хэш-цепочка доказывает целостность, но не авторство. `cortiq sign` | |
| пишет отсоединённый `<model>.sig` — JSON `{alg: "ed25519-sha256", | |
| pubkey, sha256, sig}`, Ed25519 поверх SHA-256 файла — сам контейнер не | |
| переписывается, старый инструментарий не затронут. `cortiq verify` | |
| проверяет подпись автоматически, когда `.sig` лежит рядом с моделью; | |
| отсутствие — не ошибка. Ключ — файл с 32-байтовым hex-сидом, который | |
| подписант хранит приватно. | |
| ## Анти-фичи — чего формат намеренно НЕ имеет | |
| - **Вычисляемой раскладки весов** — баг-класс #1 в v1 (райтер и ридер | |
| «вычисляли» раскладку независимо и расходились). | |
| - **Тихих fallback'ов** — v1 интерпретировала любой мусорный файл как «модель | |
| 27B»; v2 обязана падать. | |
| - **JSON для битовых данных** — маски v1 в JSON раздувались в 3–4×. | |
| - **Декларативных полей** — `quality_score: 1.0` по умолчанию, «ёмкости» по | |
| закону площади, Born-множители в динамике: метафора не становится | |
| полем формата, пока не измерена. | |
| ## 9. Скиллы — рой в одном файле (Patent 15, claims 2/12/15) | |
| Одна общая базовая модель + K записей на скилл; ни одна запись не хранит полную | |
| модель. Хранилище масштабируется как |backbone| + Σ|deltas|. | |
| **Замещающие тензоры** — обычные записи каталога с именами | |
| `skill.{skill_id}.{name_of_replaced_tensor}`, напр. | |
| `skill.sql.model.layers.3.mlp.gate_proj.weight`. Полная логическая форма | |
| замещаемого тензора (full-shape — НЕ low-rank, НЕ список diff, НЕ | |
| маска), в любой кодировке §3. Помасочный delta-индекс (claim 2) | |
| материализуется каталогом: префиксный фильтр даёт скилл → | |
| байт-смещения; ленивая пейджинг = mmap-доступ ровно к тем смещениям | |
| (claim 12). | |
| **Реестр** — JSON-заголовок, аддитивный: | |
| ```json | |
| "skills": [{ | |
| "id": "sql", | |
| "name": "SQL assistant", | |
| "layers": [3, 4, 5], | |
| "selection": {"metric": "mse", "phi_layer": 20, | |
| "mean": "<f16 base64>", "basis": "<f16 base64>"}, | |
| "input_mask_task": null, | |
| "quality": {"metric": "ppl", "backbone": 21.4, "overlaid": 17.9, | |
| "dataset_sha256": "…"} | |
| }] | |
| ``` | |
| `selection` держит параметры аффинного подпространства для маршрутизации | |
| recon-argmin (`E = ‖r − BBᵀr‖²/‖φ‖²`, выбирается скилл с минимальным E); | |
| файл самодостаточен для отбора. `quality` — честный контракт claim-16 | |
| (overlaid против backbone на held-out данных). | |
| **Семантика исполнения (claims 1/3/18)**: индирекция источника тензора — для | |
| каждого тензора рантайм читает ЛИБО запись базовой модели, ЛИБО | |
| `skill.{active}.{name}`, если она есть; замещение вместо сложения, | |
| полная модель на скилл никогда не собирается (все тензоры — указатели в | |
| один mmap). Мягкая суперпозиция (claim 14): смешанные рабочие тензоры | |
| `Σwᵢ·Tᵢ`, `wᵢ = softmax(−E/T)`. | |
| **Только-добавочный рост (claim 11)**: добавление скилла = дописывание новых | |
| тензоров в хвост файла + переиздание каталога/заголовка/индекса в | |
| хвост + обновление смещений конверта на месте (смещение 0 фиксировано). Байты и | |
| смещения ранее записанных тензоров никогда не меняются; старые байты | |
| dir/header становятся мёртвыми хвостами секций (совместимо: ридеры навигируют | |
| только через конверт). Компактификация (`converter/cmf_compact.py`) = обычная | |
| перезапись. | |
| ### 9.1 Самостоятельные скилл-файлы (`SKILL_FILE`, бит 6) | |
| Скилл может путешествовать БЕЗ своей базы: `.cmf`, чей набор тензоров — | |
| только то, что изменил бейк (плюс каталог масок), привязанный к базе | |
| ключами идентичности в записи реестра: | |
| ```json | |
| "skills": [{ | |
| "id": "gfx-html", | |
| "layers": [0, 1, "...", 21], | |
| "base_dir_hash": "9f22593eb458bc6f", | |
| "base_arch": "nanbeige", | |
| "task": "specialist", | |
| "provenance": {"corpus": "…", "tensors": 30} | |
| }] | |
| ``` | |
| - `base_dir_hash` — hex `hash64` байтов каталога тензоров БАЗЫ (то же | |
| значение конверт несёт по адресу `[0x78]`). Скилл — дельта против | |
| точных байтов, а не против архитектуры: `apply` ОБЯЗАН отвергнуть | |
| базу с другим хешем каталога (явный `--force` может переопределить; | |
| результат вне спецификации). | |
| - `base_arch`, `task`, `provenance` — информационные ключи: человеческая | |
| проверка, задача каталога масок, происхождение. | |
| Любая запись с `base_dir_hash` поднимает бит 6: читатель до этого бита | |
| откажет громко, а рантайм, знающий бит, откажется ЗАПУСКАТЬ файл | |
| (частичный набор тензоров — не модель) и укажет на | |
| `cortiq skill apply <база> <скилл> -o out.cmf`, который проверит ключ, | |
| наложит тензоры и маски поверх базы и запишет полный файл — | |
| байт-эквивалент специалиста, из которого скилл был вырезан. | |
| Жизненный цикл: `skill bake` (специалист) → `skill export --base` | |
| (дельта + ключи) → публикация маленького файла → `skill apply` на любой | |
| копии базы. | |
| Статус: полностью реализовано и под воротами (контейнер + индирекция, | |
| продакшн-рецепты, маршрутизация recon-argmin, только-добавление + компактификация, | |
| soft-blend); claim 16 подтверждён измерением (−24.9% task-PPL в | |
| рантайме). | |
| ## 10. Шардинг — модель в N файлах | |
| Именование: `{base}-{no:05}-of-{count:05}.cmf` (духовно совместимо с | |
| safetensors). Пользователь открывает ЛЮБОЕ имя; рантайм нормализует к | |
| шарду 1 и подбирает соседей по паттерну. | |
| **Каждый шард — самодостаточный валидный .cmf**: полный конверт, JSON-заголовок, | |
| каталог СВОИХ тензоров, свой блоб данных, свои хэши | |
| (`section_hashes` + по-тензорные). `cortiq verify` работает на любом отдельном | |
| шарде без его соседей. | |
| Заголовок каждого шарда несёт: | |
| ```json | |
| "shard": { "no": 1, "count": 5 } | |
| ``` | |
| Нет блока = обычный одиночный файл (обратно совместимо: старые ридеры видят | |
| шард 1 как валидную, но неполную модель и честно падают на отсутствующем | |
| тензоре). | |
| **Распределение содержимого**: тензоры разбиваются жадно в каноническом порядке | |
| (порог `--shard-max-gb`, грубый размер f32); секции масок/словаря/разреженного | |
| индекса, `tokenizer_config` (chat-бандл) и реестр `skills` | |
| живут ТОЛЬКО в шарде 1 — у остальных секции пустые и | |
| `tokenizer_config: null`. Тензоры скиллов (`skill.{id}.*`) распределяются | |
| как обычные записи каталога — реестр шарда 1 ссылается на них по | |
| имени через объединённый каталог. | |
| **Загрузка** (`CmfModel::open_sharded`): открыть шард 1 → mmap всех соседей | |
| → слияние каталогов (каждая запись помнит свой индекс шарда — рантайм-поле, | |
| никогда не пишется на диск) → рантайм далее работает как с одним | |
| файлом. Ошибки: прямое открытие не-первого шарда, отсутствующий сосед, | |
| несовпадение `count`. | |
| Ворота (Qwen3.5-0.8B q8_2f, 5 шардов ≤ 0.6 GB): шардированная PPL == | |
| нешардированной байт-в-байт на том же бинарнике; `verify` зелёный на каждом шарде | |
| по отдельности. | |
| ## 11. Дефрагментация — физический прунинг (USPTO App. 19/452,464, claims 9/10 — [PATENTS.md](../PATENTS.md)) | |
| Маска (§5) — это **виртуальная разреженность**: вырезанные нейроны | |
| помечены, но хранятся целиком (все задачи делят один бэкбон — резать | |
| физически нельзя, пока не выбрана ОДНА задача). Дефрагментация | |
| превращает виртуальную разреженность в **физическое сжатие**: мёртвые | |
| FFN-нейроны выбрасываются из файла — они **не хранятся и не | |
| вычисляются**. | |
| **Представление — без нового фичебита, обратная совместимость.** | |
| Физический прунинг выражается ТОЛЬКО меньшими формами тензоров в | |
| каталоге (§3 «нет вычислимой раскладки»; каталог — единственный | |
| авторитет форм). Рантайм выводит размер FFN из формы тензора | |
| (`gate_proj.rows()`), а не из `arch.intermediate_size`, поэтому | |
| дефрагированный файл — обычная меньшая dense-модель, которую | |
| существующие ридеры грузят без изменений. Секция масок в | |
| дефрагированном файле **отсутствует** (после прунинга маска — | |
| тождество). `arch.intermediate_size` становится номиналом (= максимум | |
| по слоям); истинный размер живёт в каждом тензоре — и каждый слой | |
| сжимается до СВОЕГО числа живых нейронов (лучше патента, который обязан | |
| резать все слои под `max(active)`). | |
| **Инварианты (обязательные):** тройка слоя `gate_proj.rows() == | |
| up_proj.rows() == down_proj.cols() == inter'ₗ` и `down_proj.rows() == | |
| hidden_size`; ось нейронов — строки для `gate/up`, столбцы для `down`; | |
| квант-группа 32: `down_proj` с `inter' % 32 ≠ 0` пишется как `q8_2f` | |
| (конвертер даунгрейдит сам); это НЕ байтовое усечение — деквант → выбор | |
| живых → **переквант** в меньшей форме, хэши пересчитываются; | |
| `hidden_size`, эмбеддинги, `lm_head` и нормы не трогаются. | |
| **Одна задача — автономный файл.** Дефраг деструктивен: один `.cmf` | |
| запекает ровно одну задачу. Мульти-задачность остаётся на масках (§5) | |
| или скиллах (§9). Провенанс — честный контракт в | |
| `provenance.defrag {source_skill, pre_intermediate, | |
| post_intermediate_max, kept_per_layer, pruned_ratio}`. | |
| Производство (нативный Rust): `cortiq convert --model … --defrag | |
| <skill_dir> --quant q8_2f --output model.cmf`. | |
| **Охват:** здесь — dense-FFN-нейроны; MoE-эксперты — в §11.1. Прунинг | |
| голов внимания вне охвата. | |
| ### 11.1 MoE-дефраг экспертов (`cortiq moe-defrag`) | |
| MoE-близнец §11, управляемый B-полем роутинга вместо нейронной маски: | |
| использование экспертов сильно задачно (замерено на кодере 34.7B: | |
| top-64 наборы экспертов для кода и прозы пересекаются с Jaccard 0.25), | |
| поэтому однозадачный файл может выбросить экспертов, к которым задача | |
| не маршрутизирует. Из дампа `CMF_MOE_STATS` (пер-слойные счётчики | |
| выбора экспертов на репрезентативном прогоне) для каждого слоя | |
| сохраняется минимальный top-набор, покрывающий `--cover` записанной | |
| массы роутинга; остальные выбрасываются. | |
| **Представление — та же философия, без фичебита.** | |
| - Оставленные эксперты перенумеровываются в ПЛОТНЫЙ пер-слойный префикс | |
| `mlp.experts.0 … mlp.experts.{k−1}` с сохранением относительного | |
| порядка; ридер перечисляет экспертов слоя по НАЛИЧИЮ тензоров до | |
| `arch.moe.num_experts`, который становится номиналом (= исходное | |
| число) — зеркально правилу §11 для `intermediate_size`. | |
| - Строки роутера собираются в соответствие в том же порядке: | |
| `mlp.gate.weight` становится `[kept_l, hidden]`, и | |
| `router.rows() == (число присутствующих экспертов)` — инвариант | |
| загрузки. `top_k` клампится к пер-слойному числу экспертов. | |
| - Семантика выбора не меняется (§2.2): softmax просто ренормализуется | |
| по оставшимся. То же ограничение применимо В РАНТАЙМЕ без перезаписи | |
| файла (`CMF_MOE_MASK=<stats.json>` + `CMF_MOE_MASK_COVER`) — | |
| математически идентично, так cover-уровень гейтится перплексией до | |
| физического выброса. | |
| - Payload'ы экспертов копируются дословно (переквант не нужен — ось | |
| экспертов режет целые тензоры, не квант-группы), выжившие веса | |
| байт-в-байт равны исходным, перезапись стримится из mmap источника. | |
| Замеренный референс (KAT-Coder 34.7B-A3B, кодовая калибровка, cover | |
| 0.95): 19.6 → 12.7 ГБ (−35%), ppl held-out кода +2.8%, а на машине с | |
| 24 ГБ — где полная модель пейджилась — декод ×1.8, префилл ×3.3. | |
| Вне задачи качество падает по построению; как и в §11, один | |
| дефрагированный файл запекает одну задачу. | |
| ## 12. Pipeline-контейнеры — text-to-image в одном файле | |
| Та же машинерия конверт/каталог/блоб несёт не-LLM конвейеры. Отличия — | |
| только тег `arch_name` и неймспейсы имён тензоров; ни новых секций, ни | |
| фичебита (ридер, не исполняющий конвейер, всё равно валидирует и | |
| инспектирует файл). | |
| Текущий экземпляр — `arch_name: "lumina2-image"` (Lumina-Image 2.0, | |
| `cortiq imagine-pack` / `cortiq imagine`): один файл содержит весь | |
| text-to-image стек. | |
| - **Неймспейсы**: `te.*` — трансформер текст-энкодера (LLM класса | |
| Gemma-2; блок `arch` заголовка описывает ИМЕННО эту компоненту, так | |
| что общие инструменты читают осмысленные размерности), `dit.*` — | |
| денойзер Next-DiT, `vae.*` — VAE-декодер. Config-JSON'ы компонент | |
| едут u8-тензорами `{prefix}.config_json` — файл самодостаточен. | |
| - **Квантование**: пер-тензорное как всегда (§3, каталог — истина) — | |
| обычно q4t/q8 матрицы для te/dit, f16 для свёрток VAE и норм. | |
| - **Секция токенизатора** (§6) несёт токенизатор текст-энкодера; | |
| `provenance.pipeline` + `provenance.components` называют рецепт. | |
| --- | |
| *Связанное: [COMPARISON.md](COMPARISON.md) (CMF против других форматов моделей), | |
| [README проекта](../README.md) (обзор и быстрый старт), | |
| `python/cmf_reader.py` (автономный ридер: stdlib + numpy, читает каждый | |
| dtype, шарды, скиллы, verify).* | |