cmf / SPEC.ru.md
infosave's picture
spec: LOOP_MASKS (bit 5) + SKILL_FILE (bit 6) + §9.1 standalone skill files
76cb853 verified
|
Raw
History Blame Contribute Delete
49.6 kB

CMF v2 — Спецификация формата

Языки: English · Русский · 中文

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\x01InvalidMagic;
  • version ≠ 2 → UnsupportedVersion (v1 мертва: реальных файлов v1 не существует, программа поддержки не будет запущена);
  • установлен неизвестный бит required_featuresUnsupportedFeature;
  • любая секция выходит за EOF, data_off не кратен 4096, off + nbytes тензора превышает data_lenBounds;
  • имя тензора не UTF-8, dtype неизвестен, ndim > 6Parse.

Проверка хэша тензоров — по требованию (cortiq verify, флаг загрузчика), не при каждом open: страницы mmap читаются лениво.

2. JSON-заголовок

UTF-8 JSON, без выравнивания. Критичные для машины данные живут в бинарных секциях; JSON несёт архитектуру и происхождение — те части, что читает человек.

{
  "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 объявляет:

"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 объявляет:

"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-мета маски

{
  "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)

Файл — а не рантайм-бинарник — определяет поведение чата. Заголовок несёт опциональный блок (аддитивная эволюция, без бита фичи):

"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-заголовок, аддитивный:

"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, чей набор тензоров — только то, что изменил бейк (плюс каталог масок), привязанный к базе ключами идентичности в записи реестра:

"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 работает на любом отдельном шарде без его соседей.

Заголовок каждого шарда несёт:

"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)

Маска (§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 (CMF против других форматов моделей), README проекта (обзор и быстрый старт), python/cmf_reader.py (автономный ридер: stdlib + numpy, читает каждый dtype, шарды, скиллы, verify).