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
the spec in all three languages, links repaired
Browse files- SPEC.md +1 -1
- SPEC.ru.md +684 -0
- SPEC.zh.md +611 -0
SPEC.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# CMF v2 — Format Specification
|
| 2 |
|
| 3 |
-
*Languages: **English** · [Русский](
|
| 4 |
|
| 5 |
**Cortiq Model Format** — a single file carrying everything needed for
|
| 6 |
sparse, task-routed inference: quantized weights, tokenizer, per-task
|
|
|
|
| 1 |
# CMF v2 — Format Specification
|
| 2 |
|
| 3 |
+
*Languages: **English** · [Русский](SPEC.ru.md) · [中文](SPEC.zh.md)*
|
| 4 |
|
| 5 |
**Cortiq Model Format** — a single file carrying everything needed for
|
| 6 |
sparse, task-routed inference: quantized weights, tokenizer, per-task
|
SPEC.ru.md
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CMF v2 — Спецификация формата
|
| 2 |
+
|
| 3 |
+
*Языки: [English](SPEC.md) · **Русский** · [中文](SPEC.zh.md)*
|
| 4 |
+
|
| 5 |
+
**Cortiq Model Format** — единый файл, несущий всё необходимое для
|
| 6 |
+
разреженного, маршрутизируемого по задачам инференса: квантованные веса,
|
| 7 |
+
токенизатор, маски по задачам, предвычисленный разреженный
|
| 8 |
+
индекс — и, что уникально, **рой скиллов**, разделяющих одну базовую модель
|
| 9 |
+
(Patent 15).
|
| 10 |
+
|
| 11 |
+
> Нормативный источник: этот документ. Референсные
|
| 12 |
+
> реализации: Rust-ридер/рантайм (`crates/cortiq-core`,
|
| 13 |
+
> `crates/cortiq-engine`), Python-райтер (`converter/`) и
|
| 14 |
+
> ридер на Python без зависимостей (`python/cmf_reader.py`, только numpy).
|
| 15 |
+
|
| 16 |
+
Три требования, в порядке приоритета:
|
| 17 |
+
|
| 18 |
+
1. **Корректность.** Никаких режимов тихого повреждения: строгий magic,
|
| 19 |
+
версия, `required_features`, границы на каждую секцию, 64-битный хэш на
|
| 20 |
+
каждый тензор. Файл либо валиден, либо open() возвращает ошибку — третьего
|
| 21 |
+
состояния нет.
|
| 22 |
+
2. **Скорость.** Секция весов выровнена по странице для mmap, каждый тензор
|
| 23 |
+
выровнен по 64 байтам (SIMD без копирования), каталог тензоров бинарный —
|
| 24 |
+
читается без разбора. Холодные (замаскированные) веса не стоят RSS.
|
| 25 |
+
3. **Компактность.** Маски упакованы побитово (1 бит на нейрон), веса —
|
| 26 |
+
q4/q8/переменной битности, весь файл адресуется одним 128-байтовым
|
| 27 |
+
конвертом.
|
| 28 |
+
|
| 29 |
+
Гармония достигается не количеством фич, а **единым каноном**: одна
|
| 30 |
+
раскладка на уровень (конверт, каталог, квант-блок, маска), байт-в-байт
|
| 31 |
+
совместимая с валидированным форматом `.vmfc` v2 там, где домены
|
| 32 |
+
пересекаются (каталог тензоров, раскладки квантования, `hash64`). Никогда
|
| 33 |
+
двух определений одного и того же.
|
| 34 |
+
|
| 35 |
+
Физическая основа (VMF): модель — это вакуумный конденсат 𝒲; скилл — его
|
| 36 |
+
регулярный кор выше критической плотности; маска задачи выбирает активное
|
| 37 |
+
подмножество, не меняя веса. Формат несёт следствия
|
| 38 |
+
этой физики (двухполевое 𝒲×θ квантование, Born-важность, критический порог
|
| 39 |
+
маски) — но **только те, что подтверждены измерением**.
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## 1. Конверт (фиксированные 128 байт)
|
| 44 |
+
|
| 45 |
+
Все целые числа — little-endian.
|
| 46 |
+
|
| 47 |
+
```
|
| 48 |
+
[0x00 : 0x04] magic = b"CMF\x01" (4 bytes)
|
| 49 |
+
[0x04 : 0x08] version : u32 = 2
|
| 50 |
+
[0x08 : 0x0C] flags : u32 (reserved, 0)
|
| 51 |
+
[0x0C : 0x10] required_features : u32 (bitmask, §1.1)
|
| 52 |
+
[0x10 : 0x18] header_off : u64 (= 128)
|
| 53 |
+
[0x18 : 0x20] header_len : u64 — JSON header (§2)
|
| 54 |
+
[0x20 : 0x28] dir_off : u64 — tensor directory (§3)
|
| 55 |
+
[0x28 : 0x30] dir_len : u64
|
| 56 |
+
[0x30 : 0x38] data_off : u64 — weight blob; multiple of 4096 (§4)
|
| 57 |
+
[0x38 : 0x40] data_len : u64
|
| 58 |
+
[0x40 : 0x48] masks_off : u64 — masks section (§5); 0 = absent
|
| 59 |
+
[0x48 : 0x50] masks_len : u64
|
| 60 |
+
[0x50 : 0x58] vocab_off : u64 — tokenizer (§6); 0 = absent
|
| 61 |
+
[0x58 : 0x60] vocab_len : u64
|
| 62 |
+
[0x60 : 0x68] index_off : u64 — sparse index (§7); 0 = absent
|
| 63 |
+
[0x68 : 0x70] index_len : u64
|
| 64 |
+
[0x70 : 0x80] reserved : 16 bytes (§8.1: header/dir hashes)
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Порядок секций на диске: конверт → JSON-заголовок → каталог → **блоб
|
| 68 |
+
весов (выровнен по 4096)** → маски → словарь → разреженный индекс. Ридер
|
| 69 |
+
ОБЯЗАН адресовать секции ТОЛЬКО через конверт, никогда не предполагая
|
| 70 |
+
порядка.
|
| 71 |
+
|
| 72 |
+
### 1.1 `required_features`
|
| 73 |
+
|
| 74 |
+
Бит, которого ридер не знает → ошибка `UnsupportedFeature` (fail-fast;
|
| 75 |
+
никакого «читаем как можем»).
|
| 76 |
+
|
| 77 |
+
| bit | name | значение |
|
| 78 |
+
|-----|----------------|---------|
|
| 79 |
+
| 0 | `TENSOR_DIR` | бинарный каталог тензоров (всегда установлен в v2) |
|
| 80 |
+
| 1 | `BINARY_MASKS` | секция масок (§5) присутствует |
|
| 81 |
+
| 2 | `QUANT_2F` | каталог содержит тензоры `q8_2f`/`vbit` (двухполевое 𝒲×θ квантование) |
|
| 82 |
+
| 3 | `DELTA_MASKS` | reserved: XOR-дельты масок от родителя |
|
| 83 |
+
| 4 | `HOT_PACKS` | reserved: материализованные плотные срезы |
|
| 84 |
+
|
| 85 |
+
Неизвестные поля **header-JSON** игнорируются (аддитивная эволюция);
|
| 86 |
+
ломающие изменения проходят только через биты фич или инкремент `version`.
|
| 87 |
+
|
| 88 |
+
### 1.2 Правила валидации (нормативные)
|
| 89 |
+
|
| 90 |
+
Ридер ОБЯЗАН вернуть ошибку (не значение по умолчанию, не предупреждение),
|
| 91 |
+
когда:
|
| 92 |
+
|
| 93 |
+
- magic ≠ `CMF\x01` → `InvalidMagic`;
|
| 94 |
+
- `version` ≠ 2 → `UnsupportedVersion` (v1 мертва: реальных файлов v1
|
| 95 |
+
не существует, программа поддержки не будет запущена);
|
| 96 |
+
- установлен неизвестный бит `required_features` → `UnsupportedFeature`;
|
| 97 |
+
- любая секция выходит за EOF, `data_off` не кратен 4096,
|
| 98 |
+
`off + nbytes` тензора превышает `data_len` → `Bounds`;
|
| 99 |
+
- имя тензора не UTF-8, dtype неизвестен, `ndim > 6` → `Parse`.
|
| 100 |
+
|
| 101 |
+
Проверка хэша тензоров — по требованию (`cortiq verify`, флаг загрузчика),
|
| 102 |
+
не при каждом open: страницы mmap читаются лениво.
|
| 103 |
+
|
| 104 |
+
## 2. JSON-заголовок
|
| 105 |
+
|
| 106 |
+
UTF-8 JSON, без выравнивания. Критичные для машины данные живут в бинарных
|
| 107 |
+
секциях; JSON несёт архитектуру и происхождение — те части, что читает
|
| 108 |
+
человек.
|
| 109 |
+
|
| 110 |
+
```jsonc
|
| 111 |
+
{
|
| 112 |
+
"format": "cmf",
|
| 113 |
+
"version": 2,
|
| 114 |
+
"arch": {
|
| 115 |
+
"arch_name": "qwen3.5",
|
| 116 |
+
"hidden_size": 5120, "intermediate_size": 17408,
|
| 117 |
+
"num_layers": 64, "num_attention_heads": 24, "num_kv_heads": 4,
|
| 118 |
+
"head_dim": 256, "vocab_size": 248320,
|
| 119 |
+
"layer_types": ["LinearAttention", "...", "FullAttention"],
|
| 120 |
+
"rms_norm_eps": 1e-6,
|
| 121 |
+
"norm_style": "qwen", // "qwen": x̂·w | "gemma": x̂·(1+w)
|
| 122 |
+
"rope_theta": 1000000.0,
|
| 123 |
+
"tie_word_embeddings": false,
|
| 124 |
+
"max_position_embeddings": 262144,
|
| 125 |
+
"linear_conv_kernel_dim": 4,
|
| 126 |
+
"linear_num_key_heads": 16, "linear_num_value_heads": 48
|
| 127 |
+
},
|
| 128 |
+
"quant_type": "Q4_BLOCK", // informational default; truth = per-tensor dtype in the directory
|
| 129 |
+
"provenance": { "tool": "…", "source_model": "…" } // optional, free-form
|
| 130 |
+
}
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
`norm_style` обязателен для движка: gemma-стиль `(1+w)`, применённый к
|
| 134 |
+
весам Qwen, — это тихий мусор во всех ~130 нормализациях
|
| 135 |
+
прямого прохода.
|
| 136 |
+
|
| 137 |
+
Диспетчеризация возможностей **управляется наличием тензоров**: движок
|
| 138 |
+
решает по-слойно, какие операторы применять, по тому, что есть в каталоге
|
| 139 |
+
(q/k-смещения, qk-нормы, выходной гейт по ширине проекции, MoE-роутер,
|
| 140 |
+
GDN-проекции) — а не по совпадению имён моделей. Новые модели известного
|
| 141 |
+
семейства загружаются с нулевыми изменениями движка.
|
| 142 |
+
|
| 143 |
+
### 2.1 MTP — предсказание нескольких токенов (опционально)
|
| 144 |
+
|
| 145 |
+
Если модель несёт MTP-голову (в стиле DeepSeek/Qwen), arch объявляет:
|
| 146 |
+
|
| 147 |
+
```jsonc
|
| 148 |
+
"mtp": { "num_layers": 1, "share_lm_head": true, "share_embed": true }
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
Тензоры MTP — обычные записи каталога под каноническими именами
|
| 152 |
+
(`model.mtp.*`): `enorm.weight`, `hnorm.weight`,
|
| 153 |
+
`eh_proj.weight [hidden, 2·hidden]`, `layers.{i}.*` (стандартный
|
| 154 |
+
блок трансформера), `norm.weight`.
|
| 155 |
+
|
| 156 |
+
Семантика: `x = eh_proj·[enorm(embed(t_{p+1})); hnorm(h_p)]` — эмбеддинг
|
| 157 |
+
ПЕРВЫМ (проверено оракулом: обратный порядок даёт ровно 0% принятия)
|
| 158 |
+
→ блок → общий lm_head → черновик токена `t_{p+2}`. Ридер не
|
| 159 |
+
обязан исполнять MTP (метаданные + обычные тензоры, аддитивная
|
| 160 |
+
эволюция, без бита фичи); рантайм CMF использует голову для
|
| 161 |
+
спекулятивного декодинга со строгой гарантией: **выход в точности равен
|
| 162 |
+
обычному жадному декодин��у** — отклонённый черновик откатывается из KV.
|
| 163 |
+
|
| 164 |
+
### 2.2 MoE — FFN со смесью экспертов (опционально)
|
| 165 |
+
|
| 166 |
+
Если модель несёт MoE-слои (Qwen2-MoE / Qwen3-MoE / Qwen3.5-MoE),
|
| 167 |
+
arch объявляет:
|
| 168 |
+
|
| 169 |
+
```jsonc
|
| 170 |
+
"moe": {
|
| 171 |
+
"num_experts": 256, "top_k": 8, "moe_intermediate_size": 512,
|
| 172 |
+
"norm_topk_prob": true, // Qwen2-MoE: false
|
| 173 |
+
"shared_expert_intermediate_size": 512 // absent if no shared expert
|
| 174 |
+
}
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
Тензоры — обычные записи каталога под HF-именами:
|
| 178 |
+
|
| 179 |
+
```
|
| 180 |
+
model.layers.{i}.mlp.gate.weight [num_experts, hidden] router
|
| 181 |
+
model.layers.{i}.mlp.experts.{e}.{gate,up,down}_proj.weight
|
| 182 |
+
model.layers.{i}.mlp.shared_expert.{gate,up,down}_proj.weight
|
| 183 |
+
model.layers.{i}.mlp.shared_expert_gate.weight [1, hidden]
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
Какие слои являются MoE — определяется ПРИСУТСТВИЕМ роутера в
|
| 187 |
+
каталоге (по-слойно, не по-модельно): `mlp_only_layers`/
|
| 188 |
+
`decoder_sparse_step` у Qwen2-MoE дают смешанные модели, и плотные
|
| 189 |
+
слои сохраняют обычные `mlp.*_proj`.
|
| 190 |
+
|
| 191 |
+
Семантика исполнения (паритет с HF, под воротами `tests/moe_parity.sh` по
|
| 192 |
+
четырём семействам, включая фьюженную раскладку AgentWorld): softmax по ВСЕМ
|
| 193 |
+
логитам роутера → top-k (ничьи: меньший индекс, порядок torch.topk) → если
|
| 194 |
+
`norm_topk_prob`, перенормировка выбранных k → Σwₑ·FFNₑ(x); общий
|
| 195 |
+
эксперт всегда добавляется с весом `sigmoid(shared_expert_gate·x)`.
|
| 196 |
+
Эксперты остаются квантованными в mmap; на токен затрагиваются только
|
| 197 |
+
страницы выбранных k — та же история резидентности, что и у скиллов.
|
| 198 |
+
Райтерам СЛЕДУЕТ раскладывать тензоры экспертов слоя ролево-смежно
|
| 199 |
+
(все `gate_proj` экспертов 0…N−1 подряд, затем все `up_proj`, затем все
|
| 200 |
+
`down_proj`) — GPU-бэкенды тогда работают с банком экспертов слоя как с
|
| 201 |
+
одним регионом вместо сборки сотен слайсов; нативный импортёр и
|
| 202 |
+
`moe-defrag` оба пишут такой порядок. Каждый
|
| 203 |
+
эксперт — отдельная запись каталога со СВОИМ dtype: это и есть носитель
|
| 204 |
+
по-экспертного распределения битов (P15 claim 12) — реализовано, под воротами
|
| 205 |
+
`tests/moe_vbit.sh`; B-поле (частоты выбора роутером через
|
| 206 |
+
`--route-stats`) измерено end-to-end на модели 35B.
|
| 207 |
+
|
| 208 |
+
## 3. Каталог тензоров
|
| 209 |
+
|
| 210 |
+
Байт-в-байт раскладка `.vmfc` v2 (единый канон, общий референсный
|
| 211 |
+
парсер):
|
| 212 |
+
|
| 213 |
+
```
|
| 214 |
+
[0 : 8 ] count : u64
|
| 215 |
+
[8 : 16] pool_off : u64 (name-pool offset from section start)
|
| 216 |
+
[16 : 16 + count·56] 56-byte records:
|
| 217 |
+
name_off : u32 (relative to pool_off)
|
| 218 |
+
name_len : u16
|
| 219 |
+
dtype : u8 (§3.1)
|
| 220 |
+
ndim : u8 (≤ 6)
|
| 221 |
+
shape : u32 × 6 (zero-padded tail)
|
| 222 |
+
off : u64 (RELATIVE to data_off; multiple of 64)
|
| 223 |
+
nbytes : u64
|
| 224 |
+
hash : u64 (hash64 of the tensor bytes, §8)
|
| 225 |
+
[pool_off : …] UTF-8 name pool
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
Имена тензоров **1:1 с исходной моделью**
|
| 229 |
+
(`model.layers.{i}.mlp.gate_proj.weight`, `model.embed_tokens.weight`,
|
| 230 |
+
`lm_head.weight`, …). Формат не предписывает набор тензоров:
|
| 231 |
+
каталог — единственный источник истины о том, что содержит блоб.
|
| 232 |
+
«Вычисляемой раскладки» не существует.
|
| 233 |
+
|
| 234 |
+
### 3.1 `dtype`
|
| 235 |
+
|
| 236 |
+
Нумерация общая с `.vmfc` (id никогда не переиспользуются):
|
| 237 |
+
|
| 238 |
+
| id | name | статус в CMF v2 |
|
| 239 |
+
|----|-----------|------------------|
|
| 240 |
+
| 0 | `f32` | ✅ чтение/запись |
|
| 241 |
+
| 1 | `f16` | ✅ чтение/запись (нормы и 1-D всегда f16) |
|
| 242 |
+
| 2 | `bf16` | ✅ чтение/запись |
|
| 243 |
+
| 3 | `q8_row` | ✅ чтение/запись |
|
| 244 |
+
| 4 | `q4_block`| ✅ чтение/запись |
|
| 245 |
+
| 5 | `mix8_4` | reserved |
|
| 246 |
+
| 6 | `u8` | reserved |
|
| 247 |
+
| 7 | `q4_col` | reserved |
|
| 248 |
+
| 8 | `vbit` | ✅ чтение/запись (бит `QUANT_2F`), переменная 3–8 бит |
|
| 249 |
+
| 9 | `q8_2f` | ✅ чтение/запись (бит `QUANT_2F`), 𝒲×θ |
|
| 250 |
+
| 10 | `vbit_ro` | ✅ чтение/запись — `vbit` + таблица row-offsets в файле (O(1) доступ к строке); дефолт конвертера для `--quant vbit` |
|
| 251 |
+
| 11 | `q4_tiled`| ✅ чтение/запись — q4 интерливными тайлами `[f16 scale][16B nibbles]` (`--quant q4t`) |
|
| 252 |
+
| 12 | `q1` | ✅ чтение/запись — 1-битные бинарные веса, только для 1-бит-ОБУЧЕННЫХ моделей (`--quant q1`); тайл `[f16 scale][4B бит-знаков]` на 32-группу, `w = s·(2·bit−1)` |
|
| 253 |
+
| 13 | `q1s` | ✅ чтение/запись — база `q1` + разреженный оверлей выбросов высокой точности (1-битный PTQ обычных чекпойнтов) |
|
| 254 |
+
| 14 | `q1t` | ✅ чтение/запись — тернарные `{−s, 0, +s}` base-3-тайлы + построчный оверлей выбросов (~2.25 bpw + оверлей) |
|
| 255 |
+
| 15 | `q4tp` | ✅ чтение/запись — ниббли `q4_tiled`, но масштаб тайла — 5-битная ступень на лестнице строки (`--quant q4tp`, либо `requant` на месте) |
|
| 256 |
+
|
| 257 |
+
### 3.2 Раскладки квантования (канон = `.vmfc`: «сначала кванты, затем скейлы»)
|
| 258 |
+
|
| 259 |
+
- **`q8_row`** (только 2-D `[out, in]`):
|
| 260 |
+
`[int8 : out·in][f16 : out]` — один скейл на строку,
|
| 261 |
+
`w = q[o,i]·scale[o]`, `scale[o] = absmax(row_o)/127`.
|
| 262 |
+
- **`q4_block`**: группы по 32 над уплощённым тензором, с нулевым добиванием;
|
| 263 |
+
`[u8 : ceil(n/32)·16][f16 : ceil(n/32)]`.
|
| 264 |
+
- **`q4tp`** (только 2-D, `in % 32 == 0`):
|
| 265 |
+
`[ниббли: rows·gpr·16][параметры строки: rows × (f16 lo, f16 step)]
|
| 266 |
+
[коды: rows × ceil(gpr·5/8), 5 бит LSB-first, выровнено по строке]`,
|
| 267 |
+
`gpr = in/32`. Масштаб тайла — `2^(lo[r] + code·step[r])`, поэтому читатель
|
| 268 |
+
раскрывает 32-ступенчатую лестницу строки один раз и дальше берёт масштабы
|
| 269 |
+
из таблицы. Значения и порядок нибблов совпадают с `q4_tiled`, отличается
|
| 270 |
+
только представление масштаба: 4.17 бит/вес против 4.50 — f16-масштаб
|
| 271 |
+
занимал 11% q4t-файла. `lo`/`step` берутся из точных min/max логарифма
|
| 272 |
+
масштаба по строке, поэтому код никогда не выходит за диапазон и
|
| 273 |
+
escape-механизм не нужен. Кодировщик ОБЯЗАН округлить `lo`/`step` до f16
|
| 274 |
+
**перед** выбором кодов и квантовать ниббли по восстановленному масштабу —
|
| 275 |
+
иначе писатель и читатель разойдутся (та же ловушка, из-за которой q4-кодер
|
| 276 |
+
сначала округляет свой масштаб).
|
| 277 |
+
Ниблы: элемент `2k` младший, `2k+1` старший; `w = (q − 8)·scale`,
|
| 278 |
+
`scale = absmax(group)/7`.
|
| 279 |
+
- **1-D тензоры и тензоры < 32 элементов всегда `f16`**
|
| 280 |
+
(точность нормализации при максимальном сжатии матриц).
|
| 281 |
+
- **`q8_2f`**: `[int8][f16 row-scale][f16 col-field]`,
|
| 282 |
+
`w = q·scale[o]·col[i]` — двухполевой Madelung-сплит 𝒲×θ, валидирован
|
| 283 |
+
в vmfcore (+37% при равном размере; восстанавливает ~75% разрыва q8→f16
|
| 284 |
+
на выбросных входных каналах).
|
| 285 |
+
- **`vbit`** (только 2-D, `in % 32 == 0`; P13 FIG.3):
|
| 286 |
+
`[u8 bits: rows][f16 scales: rows·in/32][bit-packed rows, MSB-first,
|
| 287 |
+
each row padded to a byte]`; `w = (u − L)·scale[r,g]`,
|
| 288 |
+
`L = 2^{b−1}−1`, уровни b ∈ {3,4,5,6,8}, пол 3 (claim 13).
|
| 289 |
+
Распределение b_r: water-filling по логарифму амплитуды строки к
|
| 290 |
+
средней битности тензора; для MoE-экспертов бюджет ОБЩИЙ на всё
|
| 291 |
+
семейство (слой × проекция): сдвиг `ā_expert − ā_family`
|
| 292 |
+
эквивалентен совместному water-filling по строкам всех экспертов — громкий
|
| 293 |
+
эксперт получает больше бит, тихий прижимается к полу (P15
|
| 294 |
+
claim 12; ворота `tests/moe_vbit.sh`). Опционально распределение берёт
|
| 295 |
+
произведение с B-полем — частотами выбора роутером, собранными
|
| 296 |
+
на калибровке (`b ∝ log2(A·B)`, усечённый Fisher).
|
| 297 |
+
- **`q1s`** (только 2-D, `in % 32 == 0`): база `q1` (те же 6-байтные
|
| 298 |
+
тайлы; выбросы ИСКЛЮЧЕНЫ из группового скейла), затем разреженный
|
| 299 |
+
оверлей: `[u32 count]` и `count × { [u32 плоский-индекс][f16 значение] }`
|
| 300 |
+
— веса-выбросы, сохранённые в полной точности (голографический
|
| 301 |
+
перенос / стиль SpQR) и восстанавливаемые дословно при деквантовании.
|
| 302 |
+
Переменная длина: `expected_nbytes` не определён, ридер доверяет
|
| 303 |
+
записанному в каталоге размеру. Позволяет ОБЫЧНОМУ чекпойнту пережить
|
| 304 |
+
1 бит там, где чистый `q1` не может.
|
| 305 |
+
- **`q1t`** (только 2-D, `in % 32 == 0`, `in` должен помещаться в
|
| 306 |
+
`u16`): тернарный BitNet-b1.58-стиль `{−s, 0, +s}`. База:
|
| 307 |
+
`на 32-группу { [f16 scale][7B base-3 коды] }` — 9-байтные тайлы,
|
| 308 |
+
5 тернарных значений на байт (3⁵ = 243 ≤ 256; код 0 → 0, 1 → +s,
|
| 309 |
+
2 → −s), ~2.25 бит/вес. Затем построчный оверлей выбросов:
|
| 310 |
+
`[u32 row_ptr[rows+1]]` и записи `{ [u16 col][f16 значение] }`,
|
| 311 |
+
сгруппированные по строкам (выбросы строки `r` — диапазон
|
| 312 |
+
`[row_ptr[r], row_ptr[r+1])`; `col` — индекс внутри строки) — 4
|
| 313 |
+
байта на выброс, без бинарного поиска. Точное сохранение множества
|
| 314 |
+
почти нулевых весов — решающий PTQ-выигрыш над бинарным. Переменная
|
| 315 |
+
длина, правило размера как у `q1s`.
|
| 316 |
+
|
| 317 |
+
## 4. Блоб весов
|
| 318 |
+
|
| 319 |
+
`data_off` кратен 4096 (выровненный по странице mmap); каждый тензор
|
| 320 |
+
внутри начинается на границе 64 байт (SIMD-загрузки, кэш-линии). Нулевое
|
| 321 |
+
добивание между тензорами. Ридер интерпретирует блоб только через
|
| 322 |
+
каталог.
|
| 323 |
+
|
| 324 |
+
## 5. Секция масок
|
| 325 |
+
|
| 326 |
+
Маска задачи = битовые поля «что активно» над общими весами
|
| 327 |
+
(веса не меняются — принцип VMF: скилл выбирает подмножество
|
| 328 |
+
конденсата).
|
| 329 |
+
|
| 330 |
+
```
|
| 331 |
+
[0 : 4] n_masks : u32
|
| 332 |
+
[4 : 8] meta_len : u32
|
| 333 |
+
[8 : 8 + meta_len] JSON meta (§5.1)
|
| 334 |
+
[…] mask blobs, each aligned to 8 from the section start
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
Один блоб маски (размеры выводятся из arch, без внутренних заголовков):
|
| 338 |
+
|
| 339 |
+
```
|
| 340 |
+
[n_layers × ffn_bytes] FFN bitfields ffn_bytes = ceil(intermediate_size / 8)
|
| 341 |
+
[n_layers × head_bytes] head bitfields head_bytes = ceil(num_attention_heads / 8)
|
| 342 |
+
[gates_bytes] layer_gates gates_bytes = ceil(num_layers / 8)
|
| 343 |
+
[n_layers × expert_bytes] expert bitfields ОПЦИОНАЛЬНО — только когда мета маски
|
| 344 |
+
ставит "has_expert_fields": true;
|
| 345 |
+
expert_bytes = ceil(moe.num_experts / 8)
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
Порядок битов LSB-first: нейрон `i` = бит `i % 8` байта `i / 8`; бит
|
| 349 |
+
установлен → активен. **Хвостовые биты за пределами размерности ОБЯЗАНЫ
|
| 350 |
+
быть нулём** (иначе popcount видит фантомные нейроны/головы).
|
| 351 |
+
|
| 352 |
+
Опциональная экспертная область (аддитивно: старые ридеры не читают
|
| 353 |
+
дальше gates, а `blob_len` каждой маски явный) позволяет задачной маске
|
| 354 |
+
сужать РОУТИНГ MoE: бит `e` строки слоя `l` установлен → эксперт `e`
|
| 355 |
+
маршрутизируем для этой задачи; выбор идёт только по маршрутизируемым,
|
| 356 |
+
softmax роутера ренормализуется по ним. Это переключаемый в рантайме
|
| 357 |
+
близнец физического дефрага экспертов из §11.1 — один файл с полным
|
| 358 |
+
набором экспертов обслуживает много специалистов (`cortiq moe-mask`
|
| 359 |
+
пишет такие маски, `run --task <имя>` активирует; проверено
|
| 360 |
+
токен-в-токен с эквивалентным рантайм-ограничением). Слой со строкой
|
| 361 |
+
все-единицы — без ограничения; маска без области ничего не сужает.
|
| 362 |
+
|
| 363 |
+
### 5.1 JSON-мета маски
|
| 364 |
+
|
| 365 |
+
```jsonc
|
| 366 |
+
{
|
| 367 |
+
"default_task": "general",
|
| 368 |
+
"masks": [{
|
| 369 |
+
"task_id": 0, "name": "general", "description": null,
|
| 370 |
+
"sparsity": 0.62,
|
| 371 |
+
"quality": { // null = NOT MEASURED (declaring 1.0 is forbidden)
|
| 372 |
+
"metric": "heldout_ppl_ratio", "value": 0.97,
|
| 373 |
+
"baseline_dense": 6.10, "n_samples": 512, "dataset_sha256": "…"
|
| 374 |
+
},
|
| 375 |
+
"parent": null, "priority": "Fallback", "has_hot_pack": false,
|
| 376 |
+
"blob_off": 4096, "blob_len": 139328 // relative to section start
|
| 377 |
+
}]
|
| 378 |
+
}
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
`quality` — это **контракт на held-out**, а не декларация: конвертер
|
| 382 |
+
без измеренной метрики пишет `null`; рантайм логирует предупреждение при
|
| 383 |
+
переключении на неизмеренную маску.
|
| 384 |
+
|
| 385 |
+
## 6. Секция токенизатора
|
| 386 |
+
|
| 387 |
+
Байты HuggingFace `tokenizer.json`, дословно. Модель
|
| 388 |
+
самодостаточна: один файл = одна единица распространения. Sidecar-файл
|
| 389 |
+
остаётся отладочным запасным вариантом.
|
| 390 |
+
|
| 391 |
+
### 6.1 Chat-бандл (`header.tokenizer_config`)
|
| 392 |
+
|
| 393 |
+
Файл — а не рантайм-бинарник — определяет поведение чата. Заголовок
|
| 394 |
+
несёт опциональный блок (аддитивная эволюция, без бита фичи):
|
| 395 |
+
|
| 396 |
+
```json
|
| 397 |
+
"tokenizer_config": {
|
| 398 |
+
"chat_template": "<Jinja template from chat_template.jinja or tokenizer_config.json>",
|
| 399 |
+
"eos_token_ids": [248044, 248045],
|
| 400 |
+
"bos_token_id": null,
|
| 401 |
+
"pad_token_id": 248055
|
| 402 |
+
}
|
| 403 |
+
```
|
| 404 |
+
|
| 405 |
+
Рантайм рендерит шаблон с семантикой HF (trim_blocks,
|
| 406 |
+
lstrip_blocks, управление циклами, строковые методы Python) и
|
| 407 |
+
останавливает генерацию на любом id из `eos_token_ids`. Ворота:
|
| 408 |
+
`tests/chat_template_parity.sh` — рендер рантайма равен референсному
|
| 409 |
+
jinja2 байт-в-байт. Файлы без блока получают ChatML-fallback.
|
| 410 |
+
|
| 411 |
+
## 7. Разреженный индекс
|
| 412 |
+
|
| 413 |
+
Предвычисленный мост «маска → пропуск вычислений»: активные квант-группы
|
| 414 |
+
FFN (по 32 нейрона каждая) и головы, на пару (задача, слой).
|
| 415 |
+
|
| 416 |
+
> Честный статус: движок берёт активные индексы напрямую из битовых
|
| 417 |
+
> полей маски; индекс читается и отображается CLI, но в исполнении не
|
| 418 |
+
> использовался никогда. **Кандидат на деприкацию**: райтерам СЛЕДУЕТ
|
| 419 |
+
> перестать его писать (ридеры продолжают понимать существующие файлы);
|
| 420 |
+
> он оживёт, только если путь «маски × квантованный mmap»
|
| 421 |
+
> материализуется с измеренным выигрышем.
|
| 422 |
+
|
| 423 |
+
```
|
| 424 |
+
[0 : 4] n_entries : u32
|
| 425 |
+
[4 : 8] reserved : u32 (0)
|
| 426 |
+
entry (4-aligned):
|
| 427 |
+
task_id : u32
|
| 428 |
+
layer_idx : u32
|
| 429 |
+
n_groups : u32
|
| 430 |
+
n_heads : u32
|
| 431 |
+
[u16 × n_groups] active FFN-group indices (sorted)
|
| 432 |
+
[u8 × n_heads] active head indices (sorted)
|
| 433 |
+
zero padding to a multiple of 4
|
| 434 |
+
```
|
| 435 |
+
|
| 436 |
+
Группа активна, если содержит хотя бы один активный бит маски.
|
| 437 |
+
|
| 438 |
+
## 8. `hash64`
|
| 439 |
+
|
| 440 |
+
Некриптографический 64-битный хэш байтов тензора: murmur3 `fmix64` над
|
| 441 |
+
64-битными LE-словами с позиционной солью `i·0x9E3779B97F4A7C15`, XOR-свёртка,
|
| 442 |
+
`xor len`, финальный `fmix64`. Бит-в-бит совместим с
|
| 443 |
+
`vmfcore.hash64` (Python) и `vmfcore::hash64` (Rust) — хэши
|
| 444 |
+
общих тензоров совпадают между `.cmf` и `.vmfc` (дедупликация базовой модели между
|
| 445 |
+
файлами скиллов бесплатна).
|
| 446 |
+
|
| 447 |
+
Использование: `cortiq verify` (детекция повреждений), дедупликация, ключи кэша.
|
| 448 |
+
|
| 449 |
+
### 8.1 Хэши секций
|
| 450 |
+
|
| 451 |
+
Целостность метаданных (не только тензоров):
|
| 452 |
+
|
| 453 |
+
- Резерв конверта `[0x70:0x78]` = hash64(JSON-заголовок), `[0x78:0x80]` =
|
| 454 |
+
hash64(каталог). Ноль = «отсутствует» (старые файлы проходят).
|
| 455 |
+
- JSON-заголовок несёт `section_hashes` — hex hash64
|
| 456 |
+
масок/словаря/индекса (u64 как JSON-число потерял бы точность за
|
| 457 |
+
2^53). Хэш заголовка в конверте транзитивно покрывает их.
|
| 458 |
+
- Сам конверт (первые 0x70 байт) не хэшируется: хэш не может
|
| 459 |
+
защитить сам себя; повреждённые смещения ловятся границами/хэшами дальше
|
| 460 |
+
по цепочке.
|
| 461 |
+
- `cortiq verify` проверяет всю цепочку; единственный перевёрнутый байт
|
| 462 |
+
заголовка — это ошибка.
|
| 463 |
+
|
| 464 |
+
### 8.2 Отсоединённая подпись (аутентичность, опционально)
|
| 465 |
+
|
| 466 |
+
Хэш-цепочка доказывает целостность, но не авторство. `cortiq sign`
|
| 467 |
+
пишет отсоединённый `<model>.sig` — JSON `{alg: "ed25519-sha256",
|
| 468 |
+
pubkey, sha256, sig}`, Ed25519 поверх SHA-256 файла — сам контейнер не
|
| 469 |
+
переписывается, старый инструментарий не затронут. `cortiq verify`
|
| 470 |
+
проверяет подпись автоматически, когда `.sig` лежит рядом с моделью;
|
| 471 |
+
отсутствие — не ошибка. Ключ — файл с 32-байтовым hex-сидом, который
|
| 472 |
+
подписант хранит приватно.
|
| 473 |
+
|
| 474 |
+
## Анти-фичи — чего формат намеренно НЕ имеет
|
| 475 |
+
|
| 476 |
+
- **Вычисляемой раскладки весов** — баг-класс #1 в v1 (райтер и ридер
|
| 477 |
+
«вычисляли» раскладку независимо и расходились).
|
| 478 |
+
- **Тихих fallback'ов** — v1 интерпретировала любой мусорный файл как «модель
|
| 479 |
+
27B»; v2 обязана падать.
|
| 480 |
+
- **JSON для битовых данных** — маски v1 в JSON раздувались в 3–4×.
|
| 481 |
+
- **Декларативных полей** — `quality_score: 1.0` по умолчанию, «ёмкости» по
|
| 482 |
+
закону площади, Born-множители в динамике: метафора не становится
|
| 483 |
+
полем формата, пока не измерена.
|
| 484 |
+
|
| 485 |
+
## 9. Скиллы — рой в одном файле (Patent 15, claims 2/12/15)
|
| 486 |
+
|
| 487 |
+
Одна общая базовая модель + K записей на скилл; ни одна запись не хранит полную
|
| 488 |
+
модель. Хранилище масштабируется как |backbone| + Σ|deltas|.
|
| 489 |
+
|
| 490 |
+
**Замещающие тензоры** — обычные записи каталога с именами
|
| 491 |
+
`skill.{skill_id}.{name_of_replaced_tensor}`, напр.
|
| 492 |
+
`skill.sql.model.layers.3.mlp.gate_proj.weight`. Полная логическая форма
|
| 493 |
+
замещаемого тензора (full-shape — НЕ low-rank, НЕ список diff, НЕ
|
| 494 |
+
маска), в любой кодировке §3. Помасочный delta-индекс (claim 2)
|
| 495 |
+
материализуется каталогом: префиксный фильтр даёт скилл →
|
| 496 |
+
байт-смещения; ленивая пейджинг = mmap-доступ ровно к тем смещениям
|
| 497 |
+
(claim 12).
|
| 498 |
+
|
| 499 |
+
**Реестр** — JSON-заголовок, аддитивный:
|
| 500 |
+
|
| 501 |
+
```json
|
| 502 |
+
"skills": [{
|
| 503 |
+
"id": "sql",
|
| 504 |
+
"name": "SQL assistant",
|
| 505 |
+
"layers": [3, 4, 5],
|
| 506 |
+
"selection": {"metric": "mse", "phi_layer": 20,
|
| 507 |
+
"mean": "<f16 base64>", "basis": "<f16 base64>"},
|
| 508 |
+
"input_mask_task": null,
|
| 509 |
+
"quality": {"metric": "ppl", "backbone": 21.4, "overlaid": 17.9,
|
| 510 |
+
"dataset_sha256": "…"}
|
| 511 |
+
}]
|
| 512 |
+
```
|
| 513 |
+
|
| 514 |
+
`selection` держит параметры аффинного подпространства для маршрутизации
|
| 515 |
+
recon-argmin (`E = ‖r − BBᵀr‖²/‖φ‖²`, выбирается скилл с минимальным E);
|
| 516 |
+
файл самодостаточен для отбора. `quality` — честный контракт claim-16
|
| 517 |
+
(overlaid против backbone на held-out данных).
|
| 518 |
+
|
| 519 |
+
**Семантика исполнения (claims 1/3/18)**: индирекция источника тензора — для
|
| 520 |
+
каждого тензора рантайм читает ЛИБО запись базовой модели, ЛИБО
|
| 521 |
+
`skill.{active}.{name}`, если она есть; замещение вместо сложения,
|
| 522 |
+
полная модель на скилл никогда не собирается (все тензоры — указатели в
|
| 523 |
+
один mmap). Мягкая суперпозиция (claim 14): смешанные рабочие тензоры
|
| 524 |
+
`Σwᵢ·Tᵢ`, `wᵢ = softmax(−E/T)`.
|
| 525 |
+
|
| 526 |
+
**Только-добавочный рост (claim 11)**: добавление скилла = дописывание новых
|
| 527 |
+
тензоров в хвост файла + переиздание каталога/заголовка/индекса в
|
| 528 |
+
хвост + обновление смещений конверта на месте (смещение 0 фиксировано). Байты и
|
| 529 |
+
смещения ранее записанных тензоров никогда не меняются; старые байты
|
| 530 |
+
dir/header становятся мёртвыми хвостами секций (совместимо: ридеры навигируют
|
| 531 |
+
только через конверт). Компактификация (`converter/cmf_compact.py`) = обычная
|
| 532 |
+
перезапись.
|
| 533 |
+
|
| 534 |
+
Статус: полностью реализовано и под воротами (контейнер + индирекция,
|
| 535 |
+
продакшн-рецепты, маршрутизация recon-argmin, только-добавление + компактификация,
|
| 536 |
+
soft-blend); claim 16 подтверждён измерением (−24.9% task-PPL в
|
| 537 |
+
рантайме).
|
| 538 |
+
|
| 539 |
+
## 10. Шардинг — модель в N файлах
|
| 540 |
+
|
| 541 |
+
Именование: `{base}-{no:05}-of-{count:05}.cmf` (духовно совместимо с
|
| 542 |
+
safetensors). Пользователь открывает ЛЮБОЕ имя; рантайм нормализует к
|
| 543 |
+
шарду 1 и подбирает соседей по паттерну.
|
| 544 |
+
|
| 545 |
+
**Каждый шард — самодостаточный валидный .cmf**: полный конверт, JSON-заголовок,
|
| 546 |
+
каталог СВОИХ тензоров, свой блоб данных, свои хэши
|
| 547 |
+
(`section_hashes` + по-тензорные). `cortiq verify` работает на любом отдельном
|
| 548 |
+
шарде без его соседей.
|
| 549 |
+
|
| 550 |
+
Заголовок каждого шарда несёт:
|
| 551 |
+
|
| 552 |
+
```json
|
| 553 |
+
"shard": { "no": 1, "count": 5 }
|
| 554 |
+
```
|
| 555 |
+
|
| 556 |
+
Нет блока = обычный одиночный файл (обратно совместимо: старые ридеры видят
|
| 557 |
+
шард 1 как валидную, но неполную модель и честно падают на отсутствующем
|
| 558 |
+
тензоре).
|
| 559 |
+
|
| 560 |
+
**Распределение содержимого**: тензоры разбиваются жадно в каноническом порядке
|
| 561 |
+
(порог `--shard-max-gb`, грубый размер f32); секции масок/словаря/разреженного
|
| 562 |
+
индекса, `tokenizer_config` (chat-бандл) и реестр `skills`
|
| 563 |
+
живут ТОЛЬКО в шарде 1 — у остальных секции пустые и
|
| 564 |
+
`tokenizer_config: null`. Тензоры скиллов (`skill.{id}.*`) распределяются
|
| 565 |
+
как обычные записи каталога — реестр шарда 1 ссылается на них по
|
| 566 |
+
имени через объединённый каталог.
|
| 567 |
+
|
| 568 |
+
**Загрузка** (`CmfModel::open_sharded`): открыть шард 1 → mmap всех соседей
|
| 569 |
+
→ слияние каталогов (каждая запись помнит свой индекс шарда — рантайм-поле,
|
| 570 |
+
никогда не пишется на диск) → рантайм далее работает как с одним
|
| 571 |
+
файлом. Ошибки: прямое открытие не-первого шарда, отсутствующий сосед,
|
| 572 |
+
несовпадение `count`.
|
| 573 |
+
|
| 574 |
+
Ворота (Qwen3.5-0.8B q8_2f, 5 шардов ≤ 0.6 GB): шардированная PPL ==
|
| 575 |
+
нешардированной байт-в-байт на том же бинарнике; `verify` зелёный на каждом шарде
|
| 576 |
+
по отдельности.
|
| 577 |
+
|
| 578 |
+
## 11. Дефрагментация — физический прунинг (USPTO App. 19/452,464, claims 9/10 — [PATENTS.md](../PATENTS.md))
|
| 579 |
+
|
| 580 |
+
Маска (§5) — это **виртуальная разреженность**: вырезанные нейроны
|
| 581 |
+
помечены, но хранятся целиком (все задачи делят один бэкбон — резать
|
| 582 |
+
физически нельзя, пока не выбрана ОДНА задача). Дефрагментация
|
| 583 |
+
превращает виртуальную разреженность в **физическое сжатие**: мёртвые
|
| 584 |
+
FFN-нейроны выбрасываются из файла — они **не хранятся и не
|
| 585 |
+
вычисляются**.
|
| 586 |
+
|
| 587 |
+
**Представление — без нового фичебита, обратная совместимость.**
|
| 588 |
+
Физический прунинг выражается ТОЛЬКО меньшими формами тензоров в
|
| 589 |
+
каталоге (§3 «нет вычислимой раскладки»; каталог — единственный
|
| 590 |
+
авторитет форм). Рантайм выводит размер FFN из формы тензора
|
| 591 |
+
(`gate_proj.rows()`), а не из `arch.intermediate_size`, поэтому
|
| 592 |
+
дефрагированный файл — обычная меньшая dense-модель, которую
|
| 593 |
+
существующие ридеры грузят без изменений. Секция масок в
|
| 594 |
+
дефрагированном файле **отсутствует** (после прунинга маска —
|
| 595 |
+
тождество). `arch.intermediate_size` становится номиналом (= максимум
|
| 596 |
+
по слоям); истинный размер живёт в каждом тензоре — и каждый слой
|
| 597 |
+
сжимается до СВОЕГО числа живых нейронов (лучше патента, который обязан
|
| 598 |
+
резать все слои под `max(active)`).
|
| 599 |
+
|
| 600 |
+
**Инварианты (обязательные):** тройка слоя `gate_proj.rows() ==
|
| 601 |
+
up_proj.rows() == down_proj.cols() == inter'ₗ` и `down_proj.rows() ==
|
| 602 |
+
hidden_size`; ось нейронов — строки для `gate/up`, столбцы ��ля `down`;
|
| 603 |
+
квант-группа 32: `down_proj` с `inter' % 32 ≠ 0` пишется как `q8_2f`
|
| 604 |
+
(конвертер даунгрейдит сам); это НЕ байтовое усечение — деквант → выбор
|
| 605 |
+
живых → **переквант** в меньшей форме, хэши пересчитываются;
|
| 606 |
+
`hidden_size`, эмбеддинги, `lm_head` и нормы не трогаются.
|
| 607 |
+
|
| 608 |
+
**Одна задача — автономный файл.** Дефраг деструктивен: один `.cmf`
|
| 609 |
+
запекает ровно одну задачу. Мульти-задачность остаётся на масках (§5)
|
| 610 |
+
или скиллах (§9). Провенанс — честный контракт в
|
| 611 |
+
`provenance.defrag {source_skill, pre_intermediate,
|
| 612 |
+
post_intermediate_max, kept_per_layer, pruned_ratio}`.
|
| 613 |
+
|
| 614 |
+
Производство (нативный Rust): `cortiq convert --model … --defrag
|
| 615 |
+
<skill_dir> --quant q8_2f --output model.cmf`.
|
| 616 |
+
|
| 617 |
+
**Охват:** здесь — dense-FFN-нейроны; MoE-эксперты — в §11.1. Прунинг
|
| 618 |
+
голов внимания вне охвата.
|
| 619 |
+
|
| 620 |
+
### 11.1 MoE-дефраг экспертов (`cortiq moe-defrag`)
|
| 621 |
+
|
| 622 |
+
MoE-близнец §11, управляемый B-полем роутинга вместо нейронной маски:
|
| 623 |
+
использование экспертов сильно задачно (замерено на кодере 34.7B:
|
| 624 |
+
top-64 наборы экспертов для кода и прозы пересекаются с Jaccard 0.25),
|
| 625 |
+
поэтому однозадачный файл может выбросить экспертов, к которым задача
|
| 626 |
+
не маршрутизирует. Из дампа `CMF_MOE_STATS` (пер-слойные счётчики
|
| 627 |
+
выбора экспертов на репрезентативном прогоне) для каждого слоя
|
| 628 |
+
сохраняется минимальный top-набор, покрывающий `--cover` записанной
|
| 629 |
+
массы роутинга; остальные выбрасываются.
|
| 630 |
+
|
| 631 |
+
**Представление — та же философия, без фичебита.**
|
| 632 |
+
|
| 633 |
+
- Оставленные эксперты перенумеровываются в ПЛОТНЫЙ пер-слойный префикс
|
| 634 |
+
`mlp.experts.0 … mlp.experts.{k−1}` с сохранением относительного
|
| 635 |
+
порядка; ридер перечисляет экспертов слоя по НАЛИЧИЮ тензоров до
|
| 636 |
+
`arch.moe.num_experts`, который становится номиналом (= исходное
|
| 637 |
+
число) — зеркально правилу §11 для `intermediate_size`.
|
| 638 |
+
- Строки роутера собираются в соответствие в том же порядке:
|
| 639 |
+
`mlp.gate.weight` становится `[kept_l, hidden]`, и
|
| 640 |
+
`router.rows() == (число присутствующих экспертов)` — инвариант
|
| 641 |
+
загрузки. `top_k` клампится к пер-слойному числу экспертов.
|
| 642 |
+
- Семантика выбора не меняется (§2.2): softmax просто ренормализуется
|
| 643 |
+
по оставшимся. То же ограничение применимо В РАНТАЙМЕ без перезаписи
|
| 644 |
+
файла (`CMF_MOE_MASK=<stats.json>` + `CMF_MOE_MASK_COVER`) —
|
| 645 |
+
математически идентично, так cover-уровень гейтится перплексией до
|
| 646 |
+
физического выброса.
|
| 647 |
+
- Payload'ы экспертов копируются дословно (переквант не нужен — ось
|
| 648 |
+
экспертов режет целые тензоры, не квант-группы), выжившие веса
|
| 649 |
+
байт-в-байт равны исходным, перезапись стримится из mmap источника.
|
| 650 |
+
|
| 651 |
+
Замеренный референс (KAT-Coder 34.7B-A3B, кодовая калибровка, cover
|
| 652 |
+
0.95): 19.6 → 12.7 ГБ (−35%), ppl held-out кода +2.8%, а на машине с
|
| 653 |
+
24 ГБ — где полная модель пейджилась — декод ×1.8, префилл ×3.3.
|
| 654 |
+
|
| 655 |
+
Вне задачи качество падает по построению; как и в §11, один
|
| 656 |
+
дефрагированный файл запекает одну задачу.
|
| 657 |
+
|
| 658 |
+
## 12. Pipeline-контейнеры — text-to-image в одном файле
|
| 659 |
+
|
| 660 |
+
Та же машинерия конверт/каталог/блоб несёт не-LLM конвейеры. Отличия —
|
| 661 |
+
только тег `arch_name` и неймспейсы имён тензоров; ни новых секций, ни
|
| 662 |
+
фичебита (ридер, не исполняющий конвейер, всё равно валидирует и
|
| 663 |
+
инспектирует файл).
|
| 664 |
+
|
| 665 |
+
Текущий экземпляр — `arch_name: "lumina2-image"` (Lumina-Image 2.0,
|
| 666 |
+
`cortiq imagine-pack` / `cortiq imagine`): один файл содержит весь
|
| 667 |
+
text-to-image стек.
|
| 668 |
+
|
| 669 |
+
- **Неймспейсы**: `te.*` — трансформер текст-энкодера (LLM класса
|
| 670 |
+
Gemma-2; блок `arch` заголовка описывает ИМЕННО эту компоненту, так
|
| 671 |
+
что общие инструменты читают осмысленные размерности), `dit.*` —
|
| 672 |
+
денойзер Next-DiT, `vae.*` — VAE-декодер. Config-JSON'ы компонент
|
| 673 |
+
едут u8-тензорами `{prefix}.config_json` — файл самодостаточен.
|
| 674 |
+
- **Квантование**: пер-тензорное как всегда (§3, каталог — истина) —
|
| 675 |
+
обычно q4t/q8 матрицы для te/dit, f16 для свёрток VAE и норм.
|
| 676 |
+
- **Секция токенизатора** (§6) несёт токенизатор текст-энкодера;
|
| 677 |
+
`provenance.pipeline` + `provenance.components` называют рецепт.
|
| 678 |
+
|
| 679 |
+
---
|
| 680 |
+
|
| 681 |
+
*Связанное: [COMPARISON.md](COMPARISON.md) (CMF против других форматов моделей),
|
| 682 |
+
[README проекта](../README.md) (обзор и быстрый старт),
|
| 683 |
+
`python/cmf_reader.py` (автономный ридер: stdlib + numpy, читает каждый
|
| 684 |
+
dtype, шарды, скиллы, verify).*
|
SPEC.zh.md
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CMF v2 — 格式规范
|
| 2 |
+
|
| 3 |
+
*语言:[English](SPEC.md) · [Русский](SPEC.ru.md) · **中文***
|
| 4 |
+
|
| 5 |
+
**Cortiq Model Format** — 一个文件即可承载稀疏、任务路由推理所需的一切:
|
| 6 |
+
量化权重、分词器、逐任务掩码、预计算的稀疏索引 —— 以及独一无二的、
|
| 7 |
+
共享同一骨干的**技能集群**(Patent 15)。
|
| 8 |
+
|
| 9 |
+
> 规范来源:本文档。参考实现:Rust 读取器/运行时
|
| 10 |
+
> (`crates/cortiq-core`、`crates/cortiq-engine`)、Python 写入器
|
| 11 |
+
> (`converter/`),以及一个无依赖的 Python 读取器
|
| 12 |
+
> (`python/cmf_reader.py`,仅需 numpy)。
|
| 13 |
+
|
| 14 |
+
三项要求,按优先级排序:
|
| 15 |
+
|
| 16 |
+
1. **正确。** 没有静默损坏模式:严格的魔数、版本、
|
| 17 |
+
`required_features`、每个区段的边界检查、每个张量的 64 位哈希。
|
| 18 |
+
文件要么有效,要么 open() 返回错误 —— 不存在
|
| 19 |
+
第三种状态。
|
| 20 |
+
2. **快速。** 权重区段按页对齐以便 mmap,每个张量
|
| 21 |
+
都按 64 字节对齐(零拷贝 SIMD),张量目录为二进制格式 ——
|
| 22 |
+
无需解析即可读取。冷(被掩掉的)权重不占用 RSS。
|
| 23 |
+
3. **紧凑。** 掩码按位打包(每个神经元 1 比特),权重采用
|
| 24 |
+
q4/q8/可变位宽,整个文件由一个 128 字节的
|
| 25 |
+
信封寻址。
|
| 26 |
+
|
| 27 |
+
和谐并非来自特性数量,而是来自**单一正典**:每个层级只有一种
|
| 28 |
+
布局(信封、目录、量化块、掩码),在各自领域重叠之处
|
| 29 |
+
(张量目录、量化布局、`hash64`)与经过验证的 `.vmfc` v2 格式逐字节
|
| 30 |
+
兼容。永远不存在同一事物的两种
|
| 31 |
+
定义。
|
| 32 |
+
|
| 33 |
+
物理基础(VMF):模型是一个真空凝聚态 𝒲;技能是它在临界密度之上的
|
| 34 |
+
规则核;任务掩码在不改变权重的情况下选取一个活跃
|
| 35 |
+
子集。格式承载了该物理的推论(双场 𝒲×θ 量化、Born 重要性、临界掩码
|
| 36 |
+
阈值)—— 但**仅限那些经测量确认的部分**。
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## 1. 信封(固定 128 字节)
|
| 41 |
+
|
| 42 |
+
所有整数均为小端序。
|
| 43 |
+
|
| 44 |
+
```
|
| 45 |
+
[0x00 : 0x04] magic = b"CMF\x01" (4 bytes)
|
| 46 |
+
[0x04 : 0x08] version : u32 = 2
|
| 47 |
+
[0x08 : 0x0C] flags : u32 (reserved, 0)
|
| 48 |
+
[0x0C : 0x10] required_features : u32 (bitmask, §1.1)
|
| 49 |
+
[0x10 : 0x18] header_off : u64 (= 128)
|
| 50 |
+
[0x18 : 0x20] header_len : u64 — JSON header (§2)
|
| 51 |
+
[0x20 : 0x28] dir_off : u64 — tensor directory (§3)
|
| 52 |
+
[0x28 : 0x30] dir_len : u64
|
| 53 |
+
[0x30 : 0x38] data_off : u64 — weight blob; multiple of 4096 (§4)
|
| 54 |
+
[0x38 : 0x40] data_len : u64
|
| 55 |
+
[0x40 : 0x48] masks_off : u64 — masks section (§5); 0 = absent
|
| 56 |
+
[0x48 : 0x50] masks_len : u64
|
| 57 |
+
[0x50 : 0x58] vocab_off : u64 — tokenizer (§6); 0 = absent
|
| 58 |
+
[0x58 : 0x60] vocab_len : u64
|
| 59 |
+
[0x60 : 0x68] index_off : u64 — sparse index (§7); 0 = absent
|
| 60 |
+
[0x68 : 0x70] index_len : u64
|
| 61 |
+
[0x70 : 0x80] reserved : 16 bytes (§8.1: header/dir hashes)
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
磁盘上的区段顺序:信封 → 头部 JSON → 目录 → **权重
|
| 65 |
+
blob(对齐到 4096)** → 掩码 → 词表 → 稀疏索引。读取器必须
|
| 66 |
+
仅通过信封来寻址区段,绝不假设其顺序。
|
| 67 |
+
|
| 68 |
+
### 1.1 `required_features`
|
| 69 |
+
|
| 70 |
+
读取器不认识的某一位 → `UnsupportedFeature` 错误(快速失败;
|
| 71 |
+
不做"尽力读取")。
|
| 72 |
+
|
| 73 |
+
| bit | name | 含义 |
|
| 74 |
+
|-----|----------------|---------|
|
| 75 |
+
| 0 | `TENSOR_DIR` | 二进制张量目录(v2 中始终置位) |
|
| 76 |
+
| 1 | `BINARY_MASKS` | 存在掩码区段(§5) |
|
| 77 |
+
| 2 | `QUANT_2F` | 目录包含 `q8_2f`/`vbit` 张量(双场 𝒲×θ 量化) |
|
| 78 |
+
| 3 | `DELTA_MASKS` | 保留:来自父级的 XOR 掩码增量 |
|
| 79 |
+
| 4 | `HOT_PACKS` | 保留:物化的稠密切片 |
|
| 80 |
+
|
| 81 |
+
未知的**头部 JSON** 字段被忽略(增量式演进);
|
| 82 |
+
破坏性变更只通过特性位或 `version` 递增来引入。
|
| 83 |
+
|
| 84 |
+
### 1.2 校验规则(规范性)
|
| 85 |
+
|
| 86 |
+
在以下情况下,读取器必须返回错误(而非默认值、也非警告):
|
| 87 |
+
|
| 88 |
+
- magic ≠ `CMF\x01` → `InvalidMagic`;
|
| 89 |
+
- `version` ≠ 2 → `UnsupportedVersion`(v1 已死:不存在真实的 v1
|
| 90 |
+
文件,不会启动任何支持计划);
|
| 91 |
+
- 置位了未知的 `required_features` 位 → `UnsupportedFeature`;
|
| 92 |
+
- 任何区段越过 EOF、`data_off` 不是 4096 的倍数、某张量的
|
| 93 |
+
`off + nbytes` 超过 `data_len` → `Bounds`;
|
| 94 |
+
- 张量名非 UTF-8、dtype 未知、`ndim > 6` → `Parse`。
|
| 95 |
+
|
| 96 |
+
张量哈希校验按需进行(`cortiq verify`、加载器标志),
|
| 97 |
+
并非每次打开都做:mmap 页面按需惰性读取。
|
| 98 |
+
|
| 99 |
+
## 2. 头部 JSON
|
| 100 |
+
|
| 101 |
+
UTF-8 JSON,不对齐。机器关键数据存放于二进制区段;
|
| 102 |
+
JSON 承载架构与来源信息 —— 供人阅读的部分。
|
| 103 |
+
|
| 104 |
+
```jsonc
|
| 105 |
+
{
|
| 106 |
+
"format": "cmf",
|
| 107 |
+
"version": 2,
|
| 108 |
+
"arch": {
|
| 109 |
+
"arch_name": "qwen3.5",
|
| 110 |
+
"hidden_size": 5120, "intermediate_size": 17408,
|
| 111 |
+
"num_layers": 64, "num_attention_heads": 24, "num_kv_heads": 4,
|
| 112 |
+
"head_dim": 256, "vocab_size": 248320,
|
| 113 |
+
"layer_types": ["LinearAttention", "...", "FullAttention"],
|
| 114 |
+
"rms_norm_eps": 1e-6,
|
| 115 |
+
"norm_style": "qwen", // "qwen": x̂·w | "gemma": x̂·(1+w)
|
| 116 |
+
"rope_theta": 1000000.0,
|
| 117 |
+
"tie_word_embeddings": false,
|
| 118 |
+
"max_position_embeddings": 262144,
|
| 119 |
+
"linear_conv_kernel_dim": 4,
|
| 120 |
+
"linear_num_key_heads": 16, "linear_num_value_heads": 48
|
| 121 |
+
},
|
| 122 |
+
"quant_type": "Q4_BLOCK", // informational default; truth = per-tensor dtype in the directory
|
| 123 |
+
"provenance": { "tool": "…", "source_model": "…" } // optional, free-form
|
| 124 |
+
}
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
`norm_style` 对引擎而言是强制的:把 Gemma 风格的 `(1+w)` 应用到
|
| 128 |
+
Qwen 权重上,会在一次前向传播的全部约 130 次归一化中产生静默的垃圾结果。
|
| 129 |
+
|
| 130 |
+
能力分派由**张量存在与否驱动**:引擎根据目录中存在什么来
|
| 131 |
+
逐层决定算子(q/k 偏置、qk 归一化、由投影宽度决定的输出门、MoE 路由器、GDN 投影)
|
| 132 |
+
—— 而非通过匹配模型名称。已知家族的新模型可以在
|
| 133 |
+
零引擎改动的情况下加载。
|
| 134 |
+
|
| 135 |
+
### 2.1 MTP —— 多 token 预测(可选)
|
| 136 |
+
|
| 137 |
+
如果模型携带 MTP 头(DeepSeek/Qwen 风格),arch 声明:
|
| 138 |
+
|
| 139 |
+
```jsonc
|
| 140 |
+
"mtp": { "num_layers": 1, "share_lm_head": true, "share_embed": true }
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
MTP 张量是使用规范名称的普通目录条目
|
| 144 |
+
(`model.mtp.*`):`enorm.weight`、`hnorm.weight`、
|
| 145 |
+
`eh_proj.weight [hidden, 2·hidden]`、`layers.{i}.*`(一个标准的
|
| 146 |
+
transformer 块)、`norm.weight`。
|
| 147 |
+
|
| 148 |
+
语义:`x = eh_proj·[enorm(embed(t_{p+1})); hnorm(h_p)]` —— 嵌入
|
| 149 |
+
在先(oracle 验证:反过来的顺序会得到恰好 0% 的接受率)
|
| 150 |
+
→ 块 → 共享 lm_head → 对 token `t_{p+2}` 的草稿。读取器无需
|
| 151 |
+
执行 MTP(元数据 + 普通张量,增量式
|
| 152 |
+
演进,无特性位);CMF 运行时将该头用于
|
| 153 |
+
推测解码,并有严格保证:**输出与纯贪婪解码完全相等** ——
|
| 154 |
+
被拒绝的草稿会从 KV 中回滚。
|
| 155 |
+
|
| 156 |
+
### 2.2 MoE —— 专家混合 FFN(可选)
|
| 157 |
+
|
| 158 |
+
如果模型携带 MoE 层(Qwen2-MoE / Qwen3-MoE / Qwen3.5-MoE),
|
| 159 |
+
arch 声明:
|
| 160 |
+
|
| 161 |
+
```jsonc
|
| 162 |
+
"moe": {
|
| 163 |
+
"num_experts": 256, "top_k": 8, "moe_intermediate_size": 512,
|
| 164 |
+
"norm_topk_prob": true, // Qwen2-MoE: false
|
| 165 |
+
"shared_expert_intermediate_size": 512 // absent if no shared expert
|
| 166 |
+
}
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
张量是使用 HF 名称的普通目录条目:
|
| 170 |
+
|
| 171 |
+
```
|
| 172 |
+
model.layers.{i}.mlp.gate.weight [num_experts, hidden] router
|
| 173 |
+
model.layers.{i}.mlp.experts.{e}.{gate,up,down}_proj.weight
|
| 174 |
+
model.layers.{i}.mlp.shared_expert.{gate,up,down}_proj.weight
|
| 175 |
+
model.layers.{i}.mlp.shared_expert_gate.weight [1, hidden]
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
哪些层是 MoE,由目录中路由器的**存在与否**决定
|
| 179 |
+
(逐层而非逐模型):Qwen2-MoE 的
|
| 180 |
+
`mlp_only_layers`/`decoder_sparse_step` 会产生混合模型,稠密
|
| 181 |
+
层保留普通的 `mlp.*_proj`。
|
| 182 |
+
|
| 183 |
+
执行语义(HF 对齐,由 `tests/moe_parity.sh` 在
|
| 184 |
+
四个家族上把关,包括融合的 AgentWorld 布局):对全部
|
| 185 |
+
路由器 logits 做 softmax → top-k(平局时:取较低索引,torch.topk 顺序)→ 若
|
| 186 |
+
`norm_topk_prob`,对所选的 k 个重归一化 → Σwₑ·FFNₑ(x);共享
|
| 187 |
+
专家总是以权重 `sigmoid(shared_expert_gate·x)` 加入。
|
| 188 |
+
专家在 mmap 中保持量化状态;每个 token 只触及所选
|
| 189 |
+
k 个的页面 —— 与技能相同的驻留机制。写入器应当把一层的专家张量按角色
|
| 190 |
+
连续排布(专家 0…N−1 的全部 `gate_proj` 相邻,然后全部 `up_proj`,再
|
| 191 |
+
全部 `down_proj`)——GPU 后端便可把一层的专家库当作一个区域处理,而
|
| 192 |
+
不是收集数百个切片;原生导入器与 `moe-defrag` 都按此顺序写出。每个专家是一个
|
| 193 |
+
单独的目录条目,各有**自己的** dtype:这正是
|
| 194 |
+
逐专家位宽分配的载体(P15 claim 12)—— 已实现,由
|
| 195 |
+
`tests/moe_vbit.sh` 把关;B 场(通过 `--route-stats` 得到的路由器选择频率)
|
| 196 |
+
已在一个 35B 模型上端到端测量。
|
| 197 |
+
|
| 198 |
+
## 3. 张量目录
|
| 199 |
+
|
| 200 |
+
逐字节采用 `.vmfc` v2 布局(单一正典,共享参考
|
| 201 |
+
解析器):
|
| 202 |
+
|
| 203 |
+
```
|
| 204 |
+
[0 : 8 ] count : u64
|
| 205 |
+
[8 : 16] pool_off : u64 (name-pool offset from section start)
|
| 206 |
+
[16 : 16 + count·56] 56-byte records:
|
| 207 |
+
name_off : u32 (relative to pool_off)
|
| 208 |
+
name_len : u16
|
| 209 |
+
dtype : u8 (§3.1)
|
| 210 |
+
ndim : u8 (≤ 6)
|
| 211 |
+
shape : u32 × 6 (zero-padded tail)
|
| 212 |
+
off : u64 (RELATIVE to data_off; multiple of 64)
|
| 213 |
+
nbytes : u64
|
| 214 |
+
hash : u64 (hash64 of the tensor bytes, §8)
|
| 215 |
+
[pool_off : …] UTF-8 name pool
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
张量名与源模型**一一对应**
|
| 219 |
+
(`model.layers.{i}.mlp.gate_proj.weight`、`model.embed_tokens.weight`、
|
| 220 |
+
`lm_head.weight`、…)。格式不规定张量集合:
|
| 221 |
+
目录是 blob 内容的唯一真相来源。
|
| 222 |
+
不存在"可计算布局"。
|
| 223 |
+
|
| 224 |
+
### 3.1 `dtype`
|
| 225 |
+
|
| 226 |
+
编号与 `.vmfc` 共享(id 从不重用):
|
| 227 |
+
|
| 228 |
+
| id | name | 在 CMF v2 中的状态 |
|
| 229 |
+
|----|-----------|------------------|
|
| 230 |
+
| 0 | `f32` | ✅ 读/写 |
|
| 231 |
+
| 1 | `f16` | ✅ 读/写(归一化及 1 维张量始终为 f16) |
|
| 232 |
+
| 2 | `bf16` | ✅ 读/写 |
|
| 233 |
+
| 3 | `q8_row` | ✅ 读/写 |
|
| 234 |
+
| 4 | `q4_block`| ✅ 读/写 |
|
| 235 |
+
| 5 | `mix8_4` | 保留 |
|
| 236 |
+
| 6 | `u8` | 保留 |
|
| 237 |
+
| 7 | `q4_col` | 保留 |
|
| 238 |
+
| 8 | `vbit` | ✅ 读/写(`QUANT_2F` 位),可变 3–8 位 |
|
| 239 |
+
| 9 | `q8_2f` | ✅ 读/写(`QUANT_2F` 位),𝒲×θ |
|
| 240 |
+
| 10 | `vbit_ro` | ✅ 读/写——`vbit` + 文件内行偏移表(O(1) 行访问);`--quant vbit` 的转换器默认 |
|
| 241 |
+
| 11 | `q4_tiled`| ✅ 读/写——交错平铺的 q4 `[f16 scale][16B nibbles]`(`--quant q4t`) |
|
| 242 |
+
| 12 | `q1` | ✅ 读/写——1 位二值权重,仅用于按 1 位训练的模型(`--quant q1`);每 32 组一个 `[f16 scale][4B 符号位]` 平铺,`w = s·(2·bit−1)` |
|
| 243 |
+
| 13 | `q1s` | ✅ 读/写——`q1` 基底 + 稀疏高精度离群值覆盖层(普通检查点的 1 位 PTQ):基底之后是 `[u32 count]` 与 `count × { [u32 扁平索引][f16 值] }`;变长,读取器信任目录中记录的字节数 |
|
| 244 |
+
| 14 | `q1t` | ✅ 读/写——三值 `{−s, 0, +s}`(BitNet b1.58 风格):每 32 组 `[f16 scale][7B base-3 码]`(每字节 5 个三值,3⁵=243≤256;码 0→0、1→+s、2→−s,约 2.25 bpw),随后按行的离群值覆盖层 `[u32 row_ptr[rows+1]]` + `{ [u16 col][f16 值] }`(第 r 行的离群值在 `[row_ptr[r], row_ptr[r+1])`,`col` 为行内索引);变长,规则同 `q1s` |
|
| 245 |
+
| 15 | `q4tp` | ✅ 读/写——`q4_tiled` 的 nibble,但每块 scale 改为按行阶梯上的 5 位档位(`--quant q4tp`,或用 `requant` 就地转换) |
|
| 246 |
+
|
| 247 |
+
### 3.2 量化布局(正典 = `.vmfc`:"先量化值,后 scale")
|
| 248 |
+
|
| 249 |
+
- **`q8_row`**(仅限二维 `[out, in]`):
|
| 250 |
+
`[int8 : out·in][f16 : out]` —— 每行一个 scale,
|
| 251 |
+
`w = q[o,i]·scale[o]`,`scale[o] = absmax(row_o)/127`。
|
| 252 |
+
- **`q4tp`**(仅限二维,`in % 32 == 0`):
|
| 253 |
+
`[nibbles: rows·gpr·16][行参数: rows × (f16 lo, f16 step)]
|
| 254 |
+
[codes: rows × ceil(gpr·5/8),5 位 LSB-first,按行对齐]`,`gpr = in/32`。
|
| 255 |
+
每块 scale 为 `2^(lo[r] + code·step[r])`,因此读取方只需展开该行的 32 级
|
| 256 |
+
阶梯一次,之后按表查 scale。nibble 的取值与顺序同 `q4_tiled`,仅 scale 的
|
| 257 |
+
表示不同:4.17 bit/weight 对 4.50——f16 scale 曾占 q4t 文件的 11%。
|
| 258 |
+
`lo`/`step` 取自该行 log-scale 的精确 min/max,故码值绝不会越界,格式无需
|
| 259 |
+
逃逸机制。编码器**必须**先把 `lo`/`step` 舍入到 f16 再选码,并按重建后的
|
| 260 |
+
scale 量化 nibble——否则写入方与读取方会不一致(这正是 q4 编码器必须先舍入
|
| 261 |
+
scale 的同一个陷阱)。
|
| 262 |
+
- **`q4_block`**:在展平张量上按 32 分组,零填充;
|
| 263 |
+
`[u8 : ceil(n/32)·16][f16 : ceil(n/32)]`。
|
| 264 |
+
半字节:元素 `2k` 为低位,`2k+1` 为高位;`w = (q − 8)·scale`,
|
| 265 |
+
`scale = absmax(group)/7`。
|
| 266 |
+
- **一维张量以及元素数 < 32 的张量始终为 `f16`**
|
| 267 |
+
(在矩阵最大压缩下保持归一化精度)。
|
| 268 |
+
- **`q8_2f`**:`[int8][f16 row-scale][f16 col-field]`,
|
| 269 |
+
`w = q·scale[o]·col[i]` —— 双场 Madelung 拆分 𝒲×θ,在
|
| 270 |
+
vmfcore 中已验证(同尺寸下 +37%;在离群输入通道上恢复了 q8→f16 差距的约 75%)。
|
| 271 |
+
- **`vbit`**(仅限二维,`in % 32 == 0`;P13 FIG.3):
|
| 272 |
+
`[u8 bits: rows][f16 scales: rows·in/32][bit-packed rows, MSB-first,
|
| 273 |
+
each row padded to a byte]`;`w = (u − L)·scale[r,g]`,
|
| 274 |
+
`L = 2^{b−1}−1`,位宽 b ∈ {3,4,5,6,8},下限 3(claim 13)。
|
| 275 |
+
分配 b_r:在 log2 行幅度上朝张量的平均预算做注水;
|
| 276 |
+
对于 MoE 专家,预算在**家族内共享**(层 × 投影):偏移
|
| 277 |
+
`ā_expert − ā_family` 等价于在所有专家的行上做联合注水 —— 一个响亮的
|
| 278 |
+
专家获得更多位,一个安静的被钉在下限(P15
|
| 279 |
+
claim 12;gate `tests/moe_vbit.sh`)。分配可选地
|
| 280 |
+
与一个 B 场取积 —— 在标定时收集的路由器选择频率
|
| 281 |
+
(`b ∝ log2(A·B)`,截断 Fisher)。
|
| 282 |
+
|
| 283 |
+
## 4. 权重 blob
|
| 284 |
+
|
| 285 |
+
`data_off` 是 4096 的倍数(按页对齐的 mmap);其中每个张量
|
| 286 |
+
都从 64 字节边界开始(SIMD 加载、缓存行)。张量之间为零
|
| 287 |
+
填充。读取器只能通过目录来解释该 blob。
|
| 288 |
+
|
| 289 |
+
## 5. 掩码区段
|
| 290 |
+
|
| 291 |
+
任务掩码 = 在共享权重之上"哪些活跃"的位字段(权重不变
|
| 292 |
+
—— VMF 原则:技能选取凝聚态的一个
|
| 293 |
+
子集)。
|
| 294 |
+
|
| 295 |
+
```
|
| 296 |
+
[0 : 4] n_masks : u32
|
| 297 |
+
[4 : 8] meta_len : u32
|
| 298 |
+
[8 : 8 + meta_len] JSON meta (§5.1)
|
| 299 |
+
[…] mask blobs, each aligned to 8 from the section start
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
一个掩码 blob(尺寸由 arch 推导,无内部头部):
|
| 303 |
+
|
| 304 |
+
```
|
| 305 |
+
[n_layers × ffn_bytes] FFN bitfields ffn_bytes = ceil(intermediate_size / 8)
|
| 306 |
+
[n_layers × head_bytes] head bitfields head_bytes = ceil(num_attention_heads / 8)
|
| 307 |
+
[gates_bytes] layer_gates gates_bytes = ceil(num_layers / 8)
|
| 308 |
+
[n_layers × expert_bytes] expert bitfields 可选——仅当该掩码的 meta 设置
|
| 309 |
+
"has_expert_fields": true;
|
| 310 |
+
expert_bytes = ceil(moe.num_experts / 8)
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
位序为 LSB 优先:神经元 `i` = 字节 `i / 8` 的第 `i % 8` 位;置位
|
| 314 |
+
→ 活跃。**超出维度的尾部位必须为零**(否则
|
| 315 |
+
popcount 会看到幻影神经元/头)。
|
| 316 |
+
|
| 317 |
+
可选的专家区域(增量式:旧读取器从不读 gates 之后的内容,且每个掩码
|
| 318 |
+
的 `blob_len` 是显式的)让任务掩码收窄 MoE **路由**:第 `l` 层行的第
|
| 319 |
+
`e` 位置位 → 专家 `e` 对该任务可路由;选择只在可路由集合上进行,
|
| 320 |
+
路由器 softmax 在其上重新归一化。这是 §11.1 物理专家碎片整理的运行时
|
| 321 |
+
可切换孪生——一个携带完整专家集的文件服务多个专才(`cortiq moe-mask`
|
| 322 |
+
写入此类掩码,`run --task <名称>` 激活;已验证与等价的运行时限制逐
|
| 323 |
+
词元一致)。整行为 1 的层不受限制;不带该区域的掩码不做任何收窄。
|
| 324 |
+
|
| 325 |
+
### 5.1 掩码 JSON meta
|
| 326 |
+
|
| 327 |
+
```jsonc
|
| 328 |
+
{
|
| 329 |
+
"default_task": "general",
|
| 330 |
+
"masks": [{
|
| 331 |
+
"task_id": 0, "name": "general", "description": null,
|
| 332 |
+
"sparsity": 0.62,
|
| 333 |
+
"quality": { // null = NOT MEASURED (declaring 1.0 is forbidden)
|
| 334 |
+
"metric": "heldout_ppl_ratio", "value": 0.97,
|
| 335 |
+
"baseline_dense": 6.10, "n_samples": 512, "dataset_sha256": "…"
|
| 336 |
+
},
|
| 337 |
+
"parent": null, "priority": "Fallback", "has_hot_pack": false,
|
| 338 |
+
"blob_off": 4096, "blob_len": 139328 // relative to section start
|
| 339 |
+
}]
|
| 340 |
+
}
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
`quality` 是一份**留出集契约**,而非一个声明:没有实测指标的
|
| 344 |
+
转换器写入 `null`;运行时在切换到未测量掩码时会记录警告。
|
| 345 |
+
|
| 346 |
+
## 6. 分词器区段
|
| 347 |
+
|
| 348 |
+
HuggingFace `tokenizer.json` 的字节,逐字保留。模型
|
| 349 |
+
自包含:一个文件 = 一个分发单元。附带文件
|
| 350 |
+
仍作为调试回退。
|
| 351 |
+
|
| 352 |
+
### 6.1 聊天捆绑(`header.tokenizer_config`)
|
| 353 |
+
|
| 354 |
+
是文件 —— 而非运行时二进制 —— 定义了聊天行为。头部
|
| 355 |
+
携带一个可选块(增量式演进,无特性位):
|
| 356 |
+
|
| 357 |
+
```json
|
| 358 |
+
"tokenizer_config": {
|
| 359 |
+
"chat_template": "<Jinja template from chat_template.jinja or tokenizer_config.json>",
|
| 360 |
+
"eos_token_ids": [248044, 248045],
|
| 361 |
+
"bos_token_id": null,
|
| 362 |
+
"pad_token_id": 248055
|
| 363 |
+
}
|
| 364 |
+
```
|
| 365 |
+
|
| 366 |
+
运行时以 HF 语义渲染模板(trim_blocks、
|
| 367 |
+
lstrip_blocks、循环控制、Python 字符串方法),并在
|
| 368 |
+
`eos_token_ids` 中的任一 id 处停止生成。Gate:
|
| 369 |
+
`tests/chat_template_parity.sh` —— 运行时渲染结果与参考
|
| 370 |
+
jinja2 逐字节相等。没有该块的文件获得 ChatML 回退。
|
| 371 |
+
|
| 372 |
+
## 7. 稀疏索引
|
| 373 |
+
|
| 374 |
+
一座预计算的"掩码 → 计算跳过"桥梁:逐 (task, layer) 对的
|
| 375 |
+
活跃 FFN 量化组(每组 32 个神经元)与头。
|
| 376 |
+
|
| 377 |
+
> 诚实的状态:引擎直接从掩码位字段取活跃索引;索引会被 CLI 读取并
|
| 378 |
+
> 显示,但从未用于执行。**待弃用**:写入器应停止生成它(读取器继续
|
| 379 |
+
> 解析既有文件);只有当"掩码 × 量化 mmap"路径以实测收益落地时才会
|
| 380 |
+
> 复活。
|
| 381 |
+
|
| 382 |
+
```
|
| 383 |
+
[0 : 4] n_entries : u32
|
| 384 |
+
[4 : 8] reserved : u32 (0)
|
| 385 |
+
entry (4-aligned):
|
| 386 |
+
task_id : u32
|
| 387 |
+
layer_idx : u32
|
| 388 |
+
n_groups : u32
|
| 389 |
+
n_heads : u32
|
| 390 |
+
[u16 × n_groups] active FFN-group indices (sorted)
|
| 391 |
+
[u8 × n_heads] active head indices (sorted)
|
| 392 |
+
zero padding to a multiple of 4
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
一个组只要包含至少一个活跃掩码位即为活跃。
|
| 396 |
+
|
| 397 |
+
## 8. `hash64`
|
| 398 |
+
|
| 399 |
+
张量字节的非加密 64 位哈希:在 64 位 LE 字上做 murmur3
|
| 400 |
+
`fmix64`,带位置盐 `i·0x9E3779B97F4A7C15`,XOR 折叠、
|
| 401 |
+
`xor len`、最终 `fmix64`。与
|
| 402 |
+
`vmfcore.hash64`(Python)及 `vmfcore::hash64`(Rust)逐位兼容 ——
|
| 403 |
+
`.cmf` 与 `.vmfc` 之间共享张量的哈希相符(跨技能文件的
|
| 404 |
+
骨干去重是免费的)。
|
| 405 |
+
|
| 406 |
+
用途:`cortiq verify`(损坏检测)、去重、缓存键。
|
| 407 |
+
|
| 408 |
+
### 8.1 区段哈希
|
| 409 |
+
|
| 410 |
+
元数据完整性(不仅是张量):
|
| 411 |
+
|
| 412 |
+
- 信封保留区 `[0x70:0x78]` = hash64(header JSON),`[0x78:0x80]` =
|
| 413 |
+
hash64(directory)。零 = "缺失"(旧文件可通过)。
|
| 414 |
+
- 头部 JSON 携带 `section_hashes` —— masks/vocab/index 的十六进制
|
| 415 |
+
hash64(u64 若作为 JSON 数字,超过 2^53 会损失
|
| 416 |
+
精度)。信封中的头部哈希传递性地覆盖它们。
|
| 417 |
+
- 信封自身(前 0x70 字节)不被哈希:哈希无法
|
| 418 |
+
保护自己;损坏的偏移量会被链条下游的边界/哈希捕获。
|
| 419 |
+
- `cortiq verify` 检查整条链;单个被翻转的头部字节
|
| 420 |
+
就是一个错误。
|
| 421 |
+
|
| 422 |
+
### 8.2 分离式签名(真实性,可选)
|
| 423 |
+
|
| 424 |
+
哈希链证明完整性,但不证明作者身份。`cortiq sign` 写出分离的
|
| 425 |
+
`<model>.sig` —— JSON `{alg: "ed25519-sha256", pubkey, sha256, sig}`,
|
| 426 |
+
即对文件 SHA-256 的 Ed25519 签名——容器本身从不被重写,旧工具不受
|
| 427 |
+
影响。当 `.sig` 位于模型旁边时,`cortiq verify` 自动校验签名;缺失
|
| 428 |
+
不算错误。密钥是签名者私存的 32 字节十六进制种子文件。
|
| 429 |
+
|
| 430 |
+
## 反特性 —— 格式刻意不具备的东西
|
| 431 |
+
|
| 432 |
+
- **可计算权重布局** —— v1 的一号 bug 类别(写入器与读取器
|
| 433 |
+
各自"计算"布局并发生分歧)。
|
| 434 |
+
- **静默回退** —— v1 会把任何垃圾文件解释为"一个 27B
|
| 435 |
+
模型";v2 必须失败。
|
| 436 |
+
- **用 JSON 存位数据** —— v1 掩码用 JSON 存储会膨胀 3–4 倍。
|
| 437 |
+
- **声明字段** —— 默认的 `quality_score: 1.0`、面积律
|
| 438 |
+
"容量"、动力学中的 Born 乘子:隐喻在被测量之前
|
| 439 |
+
不会成为格式字段。
|
| 440 |
+
|
| 441 |
+
## 9. 技能 —— 一个文件中的集群(Patent 15,claims 2/12/15)
|
| 442 |
+
|
| 443 |
+
一个共享骨干 + K 条逐技能记录;没有任何记录存储完整
|
| 444 |
+
模型。存储量按 |backbone| + Σ|deltas| 伸缩。
|
| 445 |
+
|
| 446 |
+
**替换张量**是命名为
|
| 447 |
+
`skill.{skill_id}.{name_of_replaced_tensor}` 的普通目录条目,例如
|
| 448 |
+
`skill.sql.model.layers.3.mlp.gate_proj.weight`。被替换张量的完整逻辑形状
|
| 449 |
+
(full-shape —— 非低秩、非差异列表、非
|
| 450 |
+
掩码),可采用 §3 的任意编码。逐技能增量索引(claim 2)由
|
| 451 |
+
目录物化:一次前缀过滤得到 技能 →
|
| 452 |
+
字节偏移;惰性分页 = mmap 精确访问那些偏移
|
| 453 |
+
(claim 12)。
|
| 454 |
+
|
| 455 |
+
**注册表** —— 头部 JSON,增量式:
|
| 456 |
+
|
| 457 |
+
```json
|
| 458 |
+
"skills": [{
|
| 459 |
+
"id": "sql",
|
| 460 |
+
"name": "SQL assistant",
|
| 461 |
+
"layers": [3, 4, 5],
|
| 462 |
+
"selection": {"metric": "mse", "phi_layer": 20,
|
| 463 |
+
"mean": "<f16 base64>", "basis": "<f16 base64>"},
|
| 464 |
+
"input_mask_task": null,
|
| 465 |
+
"quality": {"metric": "ppl", "backbone": 21.4, "overlaid": 17.9,
|
| 466 |
+
"dataset_sha256": "…"}
|
| 467 |
+
}]
|
| 468 |
+
```
|
| 469 |
+
|
| 470 |
+
`selection` 保存用于 recon-argmin 路由的仿射子空间参数
|
| 471 |
+
(`E = ‖r − BBᵀr‖²/‖φ‖²`,选择 E 最小的技能);
|
| 472 |
+
文件对于选择是自足的。`quality` 是诚实的 claim-16
|
| 473 |
+
契约(留出数据上的叠加态 vs 骨干)。
|
| 474 |
+
|
| 475 |
+
**执行语义(claims 1/3/18)**:张量来源间接寻址 —— 对于
|
| 476 |
+
每个张量,运行时读取**要么**骨干条目,**要么**存在时的
|
| 477 |
+
`skill.{active}.{name}`;是替换而非相加,从不组装
|
| 478 |
+
完整的逐技能模型(所有张量都是指向
|
| 479 |
+
同一 mmap 的指针)。软叠加(claim 14):混合工作张量
|
| 480 |
+
`Σwᵢ·Tᵢ`,`wᵢ = softmax(−E/T)`。
|
| 481 |
+
|
| 482 |
+
**仅追加式增长(claim 11)**:添加一个技能 = 在文件尾部追加新
|
| 483 |
+
张量 + 在尾部重新发出 目录/头部/索引 + 就地
|
| 484 |
+
更新信封偏移量(偏移 0 固定)。已写入张量的字节与
|
| 485 |
+
偏移量永不改变;旧的 dir/header 字节
|
| 486 |
+
成为死区段尾(兼容:读取器只通过
|
| 487 |
+
信封导航)。压实(`converter/cmf_compact.py`)= 一次普通重写。
|
| 488 |
+
|
| 489 |
+
状态:完全实现并把关(容器 + 间接寻址、
|
| 490 |
+
生产配方、recon-argmin 路由、仅追加 + 压实、
|
| 491 |
+
软混合);claim 16 由测量满足(运行时任务 PPL −24.9%)。
|
| 492 |
+
|
| 493 |
+
## 10. 分片 —— 一个模型分为 N 个文件
|
| 494 |
+
|
| 495 |
+
命名:`{base}-{no:05}-of-{count:05}.cmf`(在精神上与
|
| 496 |
+
safetensors 兼容)。用户可打开**任意**名称;运行时归一化到分片 1
|
| 497 |
+
并按模式拾取兄弟文件。
|
| 498 |
+
|
| 499 |
+
**每个分片都是一个独立的有效 .cmf**:完整信封、头部 JSON、
|
| 500 |
+
一份属于**自己**张量的目录、自己的数据 blob、自己的哈希
|
| 501 |
+
(`section_hashes` + 逐张量)。`cortiq verify` 可在没有兄弟文件的情况下
|
| 502 |
+
对任意单个分片工作。
|
| 503 |
+
|
| 504 |
+
每个分片的头部携带:
|
| 505 |
+
|
| 506 |
+
```json
|
| 507 |
+
"shard": { "no": 1, "count": 5 }
|
| 508 |
+
```
|
| 509 |
+
|
| 510 |
+
没有该块 = 一个普通的单文件(向后兼容:旧读取器把
|
| 511 |
+
分片 1 视为一个有效但不完整的模型,并在缺失张量处诚实地失败)。
|
| 512 |
+
|
| 513 |
+
**内容分布**:张量按规范顺序贪婪地拆分
|
| 514 |
+
(`--shard-max-gb` 阈值,粗略的 f32 尺寸);masks/vocab/稀疏
|
| 515 |
+
索引区段、`tokenizer_config`(聊天捆绑)以及 `skills`
|
| 516 |
+
注册表**仅**存在于分片 1 中 —— 其余分片的这些区段为空,且
|
| 517 |
+
`tokenizer_config: null`。技能张量(`skill.{id}.*`)作为
|
| 518 |
+
普通目录条目分布 —— 分片 1 的注册表通过合并后的目录按名称
|
| 519 |
+
引用它们。
|
| 520 |
+
|
| 521 |
+
**加载**(`CmfModel::open_sharded`):打开分片 1 → mmap 所有兄弟文件
|
| 522 |
+
→ 合并目录(每个条目记住其分片索引 —— 一个运行时
|
| 523 |
+
字段,绝不写入磁盘)→ 运行时随后如同处理单个
|
| 524 |
+
文件一般工作。错误:直接打开一个非首分片、缺失一个兄弟文件、
|
| 525 |
+
`count` 不匹配。
|
| 526 |
+
|
| 527 |
+
Gate(Qwen3.5-0.8B q8_2f,5 个分片 ≤ 0.6 GB):分片化 PPL == 未分片化,
|
| 528 |
+
在同一二进制上逐字节相等;`verify` 在每个单独分片上皆为绿。
|
| 529 |
+
|
| 530 |
+
## 11. 碎片整理——物理剪枝(USPTO App. 19/452,464,claims 9/10 —— [PATENTS.md](../PATENTS.md))
|
| 531 |
+
|
| 532 |
+
掩码(§5)是**虚拟稀疏**:被剪掉的神经元只是被标记,仍完整存储(所有
|
| 533 |
+
任务共享一个骨干——在承诺单一任务之前无法物理切除)。碎片整理把虚拟
|
| 534 |
+
稀疏变成**物理压缩**:死 FFN 神经元从文件中删除——**既不存储也不计算**。
|
| 535 |
+
|
| 536 |
+
**表示——无新特性位,��后兼容。** 物理剪枝只通过目录中更小的张量形状
|
| 537 |
+
表达(§3"无可计算布局";目录是形状的唯一权威)。运行时从张量形状
|
| 538 |
+
(`gate_proj.rows()`)推导 FFN 尺寸,而非 `arch.intermediate_size`,
|
| 539 |
+
因此整理后的文件就是一个普通的更小稠密模型,现有读取器无需修改即可
|
| 540 |
+
加载。整理后的文件**没有**掩码节(剪枝后掩码即恒等)。
|
| 541 |
+
`arch.intermediate_size` 变为名义值(= 各层最大值);真实尺寸在每个
|
| 542 |
+
张量里——且每层收缩到自己的存活神经元数。
|
| 543 |
+
|
| 544 |
+
**强制不变量:** 每层三元组 `gate_proj.rows() == up_proj.rows() ==
|
| 545 |
+
down_proj.cols() == inter'ₗ` 且 `down_proj.rows() == hidden_size`;
|
| 546 |
+
神经元轴:`gate/up` 为行、`down` 为列;量化组 32:`inter'` 非 32 倍数
|
| 547 |
+
时 `down_proj` 写为 `q8_2f`(转换器自动降级);这不是字节截断——
|
| 548 |
+
反量化 → 收集存活神经元 → 在更小形状上**重量化**,张量哈希重新计算;
|
| 549 |
+
`hidden_size`、嵌入、`lm_head` 与归一化权重不动。
|
| 550 |
+
|
| 551 |
+
**一个任务,一个独立文件。** 碎片整理是破坏性的:一个 `.cmf` 只烘焙
|
| 552 |
+
一个任务。多任务服务仍走掩码(§5)或技能(§9)。溯源写在
|
| 553 |
+
`provenance.defrag` 中(诚实契约)。
|
| 554 |
+
|
| 555 |
+
**覆盖范围:** 本节为稠密 FFN 神经元;MoE 专家见 §11.1。注意力头
|
| 556 |
+
剪枝不在范围内。
|
| 557 |
+
|
| 558 |
+
### 11.1 MoE 专家碎片整理(`cortiq moe-defrag`)
|
| 559 |
+
|
| 560 |
+
§11 的 MoE 孪生,由路由 B 场而非神经元掩码驱动:专家使用高度依赖
|
| 561 |
+
任务(在 34.7B 代码模型上实测:代码与散文的 top-64 专家集合 Jaccard
|
| 562 |
+
仅 0.25),因此单任务文件可以丢弃该任务从不路由到的专家。从
|
| 563 |
+
`CMF_MOE_STATS` 转储(代表性任务运行的逐层专家选择计数)出发,每层
|
| 564 |
+
保留达到 `--cover` 路由质量分数的最小 top 专家集,丢弃其余。
|
| 565 |
+
|
| 566 |
+
**表示——同样的哲学,无特性位。**
|
| 567 |
+
|
| 568 |
+
- 保留的专家重新编号为**连续的逐层前缀** `mlp.experts.0 …
|
| 569 |
+
mlp.experts.{k−1}`,保持相对顺序;读取器按张量**存在性**枚举一层
|
| 570 |
+
的专家,上限为 `arch.moe.num_experts`(变为名义值 = 原始数量)——
|
| 571 |
+
与 §11 对 `intermediate_size` 的规则互为镜像。
|
| 572 |
+
- 路由器的行按同一顺序收集:`mlp.gate.weight` 变为 `[kept_l,
|
| 573 |
+
hidden]`,且 `router.rows() ==(存在的专家条目数)`是加载期不变量。
|
| 574 |
+
`top_k` 收缩到逐层专家数。
|
| 575 |
+
- 选择语义不变(§2.2):softmax 在保留集合上重新归一化。同一限制可
|
| 576 |
+
在**运行时**应用而无需重写文件(`CMF_MOE_MASK=<stats.json>` +
|
| 577 |
+
`CMF_MOE_MASK_COVER`)——两者在数学上相等,据此在物理切除前用
|
| 578 |
+
困惑度为 cover 水平把关。
|
| 579 |
+
- 专家载荷逐字节复制(无需重量化——专家轴切的是整张量而非量化组),
|
| 580 |
+
幸存权重与源逐字节相同,重写从源 mmap 流式进行。
|
| 581 |
+
|
| 582 |
+
实测参考(KAT-Coder 34.7B-A3B,代码校准,cover 0.95):19.6 → 12.7
|
| 583 |
+
GB(−35%),保留集代码困惑度 +2.8%;在 24 GB 内存机器上——完整模型
|
| 584 |
+
需要换页——解码 ×1.8、prefill ×3.3。
|
| 585 |
+
|
| 586 |
+
任务外质量按设计下降;与 §11 一样,一个整理后的文件只烘焙一个任务。
|
| 587 |
+
|
| 588 |
+
## 12. 流水线容器——单文件文生图
|
| 589 |
+
|
| 590 |
+
同一套信封/目录/数据块机制承载非 LLM 流水线。区别只有 `arch_name`
|
| 591 |
+
标签和张量名的命名空间;没有新节、没有特性位(不执行流水线的读取器
|
| 592 |
+
仍可验证与检视文件)。
|
| 593 |
+
|
| 594 |
+
当前实例——`arch_name: "lumina2-image"`(Lumina-Image 2.0,
|
| 595 |
+
`cortiq imagine-pack` / `cortiq imagine`):一个文件装下整个文生图栈。
|
| 596 |
+
|
| 597 |
+
- **命名空间**:`te.*`——文本编码器 Transformer(Gemma-2 类 LLM;
|
| 598 |
+
头部的 `arch` 块描述的正是该组件,通用工具因此能读到有意义的
|
| 599 |
+
维度)、`dit.*`——Next-DiT 去噪器、`vae.*`——VAE 解码器。各组件的
|
| 600 |
+
config JSON 以 `{prefix}.config_json` u8 张量随行——文件自足。
|
| 601 |
+
- **量化**:一如既往逐张量(§3 目录即真相)——通常 te/dit 矩阵为
|
| 602 |
+
q4t/q8,VAE 卷积与归一化为 f16。
|
| 603 |
+
- **分词器节**(§6)携带文本编码器的分词器;`provenance.pipeline` +
|
| 604 |
+
`provenance.components` 记录配方。
|
| 605 |
+
|
| 606 |
+
---
|
| 607 |
+
|
| 608 |
+
*相关:[COMPARISON.md](COMPARISON.md)(CMF 与其他模型格式的对比)、
|
| 609 |
+
[项目 README](../README.md)(概览与快速上手)、
|
| 610 |
+
`python/cmf_reader.py`(独立读取器:标准库 + numpy,读取所有
|
| 611 |
+
dtype、分片、技能、verify)。*
|