Публикация была переведена автоматически. Исходный язык: Русский
Нейросети, возможно, не нуждаются в богатом непрерывном пространстве весов в той мере, как мы привыкли думать; им может хватать малого дискретного языка, если этот язык правильно организован.
— Аубакиров Арман
Зачем эта статья
Это полный пошаговый разбор исследовательской линии, которая началась с простого вопроса — действительно ли современным LLM нужны 16‑битные непрерывные веса? — и закончилась рабочим самодостаточным релизом Gemma 4 31B IT, сжатой в дискретный тернарный язык весов, вместе с runtime‑ядрами, скриптами воспроизводимости, статьёй на двух языках и публичной моделью на Hugging Face.
Я вёл заметки по ходу работы на каждом шаге. Здесь эти заметки сведены в один связный рассказ: откуда взялась идея, что не получилось, какие инструменты пришлось построить, какие вопросы каждый инструмент закрывал и какие в итоге получились цифры.
Шаг 1 — Отправная точка: «3‑битная» PTQ на Llama‑3.1‑8B
Проект начался в каталоге /mnt/hf_model_weights/arman/3bit/ с очень практичной задачи: опустить предобученную Llama‑3.1 до ~3 бит на вес без катастрофы в качестве.
Я собрал модифицированный форк llama.cpp (Llama‑iTurboQuant) с CUDA и замерил формат ITQ3_S на WikiText‑2:
| Формат | Размер | WikiText‑2 PPL | VRAM (self) |
| FP16 | 14.97 ГиБ | 6.2768 | 14.8 ГиБ |
| ITQ3_S (без imatrix) | 3.5 ГБ | 40.60 | ~3.8 ГиБ |
| ITQ3_S + imatrix | 3.43 ГиБ | 6.9547 | 3.8 ГиБ |
| ITQ3_S + imatrix + Q8 out/emb | 3.86 ГиБ | 6.9423 | 3.9 ГиБ |
| Q4_K_M + imatrix | 4.58 ГиБ | 6.4407 | 4.9 ГиБ |
| Q8_0 | 7.95 ГиБ | 6.2802 | 8.1 ГиБ |
То же самое на Llama 3.3 70B (полный WikiText‑2):
- F16: PPL 3.8405, VRAM 133 ГБ, диск 131 ГиБ.
- ITQ3_S + imatrix: PPL 4.4815, VRAM 30 ГБ, диск 28.8 ГиБ.
- Сжатие: ×4.57, умещается на одну H200.
Вывод: без importance‑matrix (imatrix) ITQ3_S непригоден (PPL ≈ 40). Увеличение времени квантизации не даёт ничего; важен источник калибровки. Q4_K_M + imatrix остался лучшим компромиссом качество/размер в чистом PTQ.
Это уже был результат, но всё ещё «просто числовая квантизация». Представление оставалось аналоговой сеткой уровней.
Шаг 2 — Попытка точного BitNet b1.58 на готовой Llama
Логично возникал вопрос: а если пойти до конца — до тернарных весов, как в BitNet b1.58 (arXiv 2402.17764)?
Прямые PTQ‑прокси на Llama‑3.1‑8B на WikiText‑2:
- Матричный absmean‑тернарий: PPL ≈ 588 638 (ранняя остановка).
- Строчный absmean‑тернарий: PPL ≈ 400 897 (ранняя остановка).
- STE‑пилот (bitnet_qat_pilot.py, 20 шагов QAT): PPL 138 479.
Направление улучшалось с QAT, но оставалось на порядки хуже пригодного уровня.
Вывод: BitNet b1.58 — это рецепт обучения с нуля, а не PTQ‑трюк. Наивное применение на готовой Llama убивает модель.
Нужен был другой путь к тернарности.
Шаг 3 — FWHT + построчное 8‑уровневое квантование как безопасный fallback
Прежде чем отказываться от малых алфавитов, я построил более тонкую метадату вокруг локальной структуры:
- rowwise_fwht_8lv_sub32: 4.0 bpw, полный PPL 7.58.
- rowwise_fwht_8lv_sub16: 5.0 bpw, полный PPL 6.86.
- rowwise_fwht_8lv_sub8: 7.0 bpw, полный PPL 6.38.
Вывод: чтобы получить PPL < 7 на предобученной Llama‑3.1‑8B без дообучения, квантизатору нужна более тонкая локальная метадата уровня ~3 бит. Одного глобального тернарного алфавита недостаточно — но много маленьких алфавитов могут сработать.
Из этой мысли и выросла Route B.
Шаг 4 — Идея «дискретного языка весов» (Route B)
Вместо того чтобы думать о квантизации как о «отобразить float в ближайший уровень», я стал относиться к каждой линейной матрице как к тексту на тернарном алфавите:
- Каждая строка W — это последовательность коротких тернарных слов (цифры {-1, 0, +1}) фиксированной или переменной длины.
- Декодирование: W[n,k] = row_scale[n] · ladder(codes[n,k]) · global_scale.
- Лестница (ladder) — маленький набор шкал на каждую семью (и позже на семью × глубину). Это словарь дискретного языка весов.
- Степень сжатия задаётся длиной слова (глубина 5 → ~3 бит/вес, 7 → ~4.5 бит).
Это ровно то, что написано в эпиграфе, но выраженное кодом. Эта линия и получила имя Route B и стала основной.
Шаг 5 — Конвейер Route B
Конвейер разбит на 4 воспроизводимых стадии (см. archiv.org/reproduce/):
- 01_fit_route_specs — калибровка по семье × глубине: для каждой группы {attn_qk, attn_vo, mlp_up, mlp_down, …} × {early, mid, late} подбираем маленькую лестницу тернарных шкал по сэмплам активаций. Реализация: ternary_route/specs.py + make_ternary_route_specs.py --mode family-depth.
- 02_export_ternary_route_checkpoint — кодирует каждый линейный вес в упакованные тернарные цифры + row_scale. Результат — один checkpoint.pt с uint16 base‑3 цифрами, stop_depth_u8 и fp16 строчными масштабами.
- 03_replace_packed_route_layers — подменяет все nn.Linear на PackedTernaryRouteLinear / FusedTernaryRouteLinear, которые декодируют «на лету» (Triton‑ядро).
- 04_validate_ppl — PPL на WikiText‑2 и бенчмарк скорости.
Все 4 стадии склеены в archiv.org/reproduce/run_all.sh.
Шаг 6 — Per‑row scale: расширение, закрывшее разрыв до dense
Первая чистая Route B‑тернаризация Gemma 4 31B IT уже работала на коротком гейте:
- Baseline 1×128: loss 12.09 / PPL 178 872.
- Route packed 1×128: loss 11.98 / PPL 158 971.
Но на длинном контексте (8×512) видимый разрыв до dense оставался. Чтобы его закрыть, я добавил per‑row scale (per_row_scale_extension):
samples = W / row_abs_max
codes = encode_ternary_route_codes(samples, spec)
row_scale[n] = row_abs_max[n] / global_scale # хранится как fp16
Runtime replace_packed_route_layers сам подхватывает это из ключа {name}.row_scale_fp16. Важно: row_scale сохраняется только если он реально уменьшает relMSE конкретного слоя — иначе слой остаётся обычной Route B. Это улучшение с предохранителем.
Результаты на Gemma 4 31B IT (gemma4_31b_it_all_linear_family_depth_20260416):
- 381 / 410 линейных слоёв приняли row_scale; 29 отклонены как регрессирующие.
- Средний relMSE по весам: 0.3313 % → 0.0667 % (×5).
- Rel‑err выхода matmul на первых 8 слоях: 4.54 % → 1.57 % (×2.89).
- PPL 8×512 на WikiText: dense 8.9196, packed 9.2035, packed + row_scale 9.1666 — закрывает ~13 % разрыва до dense.
- Полное перекодирование: ~55 с на одном H200 для 410 слоёв.
Шаг 7 — Depth‑aware против family‑wise калибровки
Я также пробовал сделать калибровку более зернистой, разбив каждую семью на early:0–19 / mid:20–39 / late:40–59:
- Family‑depth all‑linear: 1.8374 / 6.28.
- Family‑wise: 1.8341 / 6.26.
- Старый фиксированный all‑linear: 1.8583 / 6.41.
Вывод: зернистость важна, но простое разбиение по глубине с текущим фиттером не даёт выигрыша — часть групп family×depth просто схлопывается обратно к family‑wise. Следующий рычаг — либо более сильный фиттер, либо per‑layer масштабы, а не ещё одно bulk‑разбиение.
Также был прототип переменной глубины (max_depth=7, stop_threshold=0.001) с uint16 base‑3 + stop_depth_u8: вышло 1.7242 / 5.61 при dense 1.7112 / 5.54, ценой 24 bpw — это формат качества, а не выигрыш по сжатию.
Шаг 8 — Per‑layer поиск на Llama 3.3 70B
Чтобы понять вклад каждого слоя, я собрал два инструмента:
- experiments/run_single_layer_route_sweep.py — per‑layer свипы поверх базового Route B‑чекпоинта.
- experiments/split_route_checkpoint_by_layer.py — режет полный 560‑слойный all‑linear чекпоинт на per‑layer артефакты для быстрой итерации.
Полный свип attn_k по слоям 0..79 на Llama 3.3 70B нашёл L21, L25, L34, L53 как сильнейших кандидатов на коротком гейте; L21 — единственный, кто пережил более строгий гейт (4×256 → 1.4669 / 4.34). Так возник гибридный фронт из 241 слоя (attn_k late + L21 + gate+up L45), PPL 8.85 на более длинной проверке из 100 окон vs baseline 8.36 — честный +0.49 PPL.
Вывод: композиция не аддитивна. Хорошие одиночные add‑on часто регрессируют при объединении; победы на коротком гейте (0.85x) регулярно проваливаются на более длинных (4×256). Нужен жадный per‑layer поиск с несколькими гейтами.
Шаг 9 — Runtime: декодинг, Triton‑ядра, скорость
Runtime живёт в archiv.org/runtime/ и ternary_route/:
- PackedTernaryRouteLinear — референс‑декодер (uint16 base‑3 → fp16 ladder → matmul).
- FusedTernaryRouteLinear — Triton‑fused matmul, декодирующий на лету. Оба прозрачно поднимают опциональные row_scale_fp16 из чекпоинта.
- load_route_model() — один вызов: грузит Gemma 4 31B IT, патчит 410 слоёв, возвращает стандартную HF‑модель, готовую к генерации.
Скорость на одной H200:
- packed_triton_fused, bs=4, 31 ГБ resident: 37.2 tok/s.
- packed_cached_decode, bs=4: 42.04 tok/s (baseline 39.27) — фактически скорость dense, ценой отказа от выигрыша по активному VRAM (~60 ГБ).
Практическая деталь, найденная на Gemma 4: 10 «отсутствующих» слоёв v_proj (5, 11, 17, 23, 29, 35, 41, 47, 53, 59) — это архитектура, а не баг экспорта. При attention_k_eq_v and not sliding_attention v_proj=None, и values берутся из общего key/value‑пути. Манифест теперь отчитывается по эффективному покрытию = 410 экспортированных + 10 виртуальных shared‑value = 420.
Шаг 10 — Честное сравнение по downstream‑бенчмаркам
Запустил прямой lm_eval по Hugging Face на Gemma 4 31B IT dense vs Route B (cap‑128 отладочный сэмпл):
- MMLU‑Pro: baseline 0.5429 vs route 0.4500 (Route −0.0929).
- HumanEval: baseline 0.0854 vs route 0.0915 (Route +0.0061).
- GPQA: равенство 0.0625, но ограничение парсера.
- AIME24/25: нули на completions‑хорнесе — артефакт; ручной пробник первого сэмпла дал правильный ответ 33 и для dense, и для Route.
Полные MMLU‑Pro запуски (с max_length=8192, max_gen_toks=64) были подняты 17.04.2026 на GPU 2 и 3.
Вывод: текущий completions‑хорнес недооценивает Gemma 4 IT на AIME/GPQA. Для относительной регрессии на MMLU‑Pro / HumanEval он годится, и именно по ней оценивается Route B.
Шаг 11 — Упаковка: самодостаточный релиз
Релиз (archiv.org/) переорганизован так, чтобы быть полностью самодостаточным — без внешних симлинков и скрытых зависимостей:
- paper.md — полная статья, EN (Abstract → §9 + Приложения A/B).
- paper_ru.md — полный русский перевод.
- main.tex + sections/*.tex — arXiv‑дерево, валидированный минимальный source‑пакет arxiv_bundle/arxiv_source.tar.gz.
- runtime/ — вшитый ternary_route/ + load_model.py, generate.py, benchmark.py.
- reproduce/ — 4 шелл‑стадии + run_all.sh.
- kernel/prepare_hf_release.py — сборщик HF‑релиза (shutil.copytree + ignore_patterns, исключает checkpoint*.pt).
- hf_release/route_b_kernel_minimal/ — публичный минимальный HF‑бандл.
- hf_release/discrete_weight_language_full_model/ — полный all‑linear материализованный экспорт (~59 ГБ .safetensors).
17.04.2026 я физически заменил последние три симлинка (runtime/ternary_route → ../../ternary_route, runtime/checkpoint_baseline.pt, runtime/checkpoint_rowscale.pt) на реальную копию каталога / задокументированное отсутствие. Документация, CLI‑помощь, статья и LaTeX‑приложение приведены в соответствие с source‑only релизом. Zenodo‑zip archiv_org_zenodo_source_20260417.zip (228 КБ) собран через rsync с исключениями (*.pt, *.safetensors, hf_release/, arxiv_bundle/, кэши).
Публичные ссылки:
- Hugging Face (ядро + минимальный runtime): https://huggingface.co/armanibadboy/gemma-4-31b-it-route-b-perhead-k37
- Hugging Face (полная материализованная модель, all‑linear): https://huggingface.co/armanibadboy/discrete-weight-language-gemma-4-31b-it-all-linear
- GitHub (исходный пакет): https://github.com/AubakirovArman/discrete-weight-language-gemma4
Какой вопрос закрывал каждый инструмент
| Инструмент | Что он закрыл |
| bitnet_b158_llama31_search.py | Работает ли наивный BitNet b1.58 PTQ на готовой Llama? → Нет. |
| rowwise_refine_search.py | Достаточно ли более тонкой локальной метадаты, чтобы получить <7 PPL на ~3 бит без QAT? → Да, на ~5 bpw. |
| make_ternary_route_specs.py | Можно ли на лестницах family×depth построить рабочий «словарь весов»? → Да. |
| export_ternary_route_checkpoint.py | Можно ли закодировать все линейные слои как упакованные тернарные слова? → Да, 410 слоёв. |
| add_row_scale_to_checkpoint.py | Закрывает ли per‑row scale разрыв до dense? → Да, ~13 % PPL‑разрыва. |
| PackedTernaryRouteLinear / Triton‑fused декодер | Можно ли запустить это на практической скорости? → Да, 37 tok/s packed, 42 tok/s cached. |
| run_single_layer_route_sweep.py | Какие слои лучше всего переносят тернарность? → Узкая «горячая» полоса (21, 45). |
| split_route_checkpoint_by_layer.py | Как итерироваться по слоям без перезагрузки? → Разрезать один раз и переиспользовать. |
| prepare_hf_release.py | Можно ли собирать релиз воспроизводимо? → Да, с валидацией по манифесту. |
Честные ограничения
- Downstream‑покрытие неполное: полный прямой HF MMLU‑Pro на момент релиза ещё в процессе; GPQA / AIME нужны более строгие хорнесы.
- Activation‑aware выбор слоёв, поиск по переменной глубине и обрезка 7‑битного codebook описаны в §8 статьи, но пока не отгружены.
- Композиция per‑layer add‑on не аддитивна; принципиальная процедура поиска добавляемых слоёв пока не построена.
- Полный all‑linear экспорт (~59 ГБ) удобен, но большой; правильнее держать публичный packed‑only HF‑репо.
Зачем это всё
Практический итог: 31‑миллиардная LLM укладывается в маленький, хорошо организованный тернарный язык с умеренной потерей качества и с декодер‑ядрами, работающими почти со скоростью dense. Теоретический итог — ровно эпиграф статьи: если правильно спроектировать язык, непрерывное пространство весов, возможно, вообще не нужно.
Всё — статья, runtime, ядра, скрипты калибровки, инструменты per‑layer поиска, HF‑бандлы, Zenodo‑zip — опубликовано.
Проект Route B уже доступен как drop-in замена для библиотеки Transformers. Вы можете потрогать код и веса прямо сейчас:
- 📄 Статья с формулами и метриками (DOI): https://zenodo.org/records/19632982
- 💻 Репозиторий с ядром Triton: https://github.com/AubakirovArman/discrete-weight-language-gemma4
- 📦 Веса на Hugging Face: https://huggingface.co/armanibadboy/discrete-weight-language-gemma-4-31b-it-all-linear
— Аубакиров Арман
Нейросети, возможно, не нуждаются в богатом непрерывном пространстве весов в той мере, как мы привыкли думать; им может хватать малого дискретного языка, если этот язык правильно организован.
— Аубакиров Арман
Зачем эта статья
Это полный пошаговый разбор исследовательской линии, которая началась с простого вопроса — действительно ли современным LLM нужны 16‑битные непрерывные веса? — и закончилась рабочим самодостаточным релизом Gemma 4 31B IT, сжатой в дискретный тернарный язык весов, вместе с runtime‑ядрами, скриптами воспроизводимости, статьёй на двух языках и публичной моделью на Hugging Face.
Я вёл заметки по ходу работы на каждом шаге. Здесь эти заметки сведены в один связный рассказ: откуда взялась идея, что не получилось, какие инструменты пришлось построить, какие вопросы каждый инструмент закрывал и какие в итоге получились цифры.
Шаг 1 — Отправная точка: «3‑битная» PTQ на Llama‑3.1‑8B
Проект начался в каталоге /mnt/hf_model_weights/arman/3bit/ с очень практичной задачи: опустить предобученную Llama‑3.1 до ~3 бит на вес без катастрофы в качестве.
Я собрал модифицированный форк llama.cpp (Llama‑iTurboQuant) с CUDA и замерил формат ITQ3_S на WikiText‑2:
| Формат | Размер | WikiText‑2 PPL | VRAM (self) |
| FP16 | 14.97 ГиБ | 6.2768 | 14.8 ГиБ |
| ITQ3_S (без imatrix) | 3.5 ГБ | 40.60 | ~3.8 ГиБ |
| ITQ3_S + imatrix | 3.43 ГиБ | 6.9547 | 3.8 ГиБ |
| ITQ3_S + imatrix + Q8 out/emb | 3.86 ГиБ | 6.9423 | 3.9 ГиБ |
| Q4_K_M + imatrix | 4.58 ГиБ | 6.4407 | 4.9 ГиБ |
| Q8_0 | 7.95 ГиБ | 6.2802 | 8.1 ГиБ |
То же самое на Llama 3.3 70B (полный WikiText‑2):
- F16: PPL 3.8405, VRAM 133 ГБ, диск 131 ГиБ.
- ITQ3_S + imatrix: PPL 4.4815, VRAM 30 ГБ, диск 28.8 ГиБ.
- Сжатие: ×4.57, умещается на одну H200.
Вывод: без importance‑matrix (imatrix) ITQ3_S непригоден (PPL ≈ 40). Увеличение времени квантизации не даёт ничего; важен источник калибровки. Q4_K_M + imatrix остался лучшим компромиссом качество/размер в чистом PTQ.
Это уже был результат, но всё ещё «просто числовая квантизация». Представление оставалось аналоговой сеткой уровней.
Шаг 2 — Попытка точного BitNet b1.58 на готовой Llama
Логично возникал вопрос: а если пойти до конца — до тернарных весов, как в BitNet b1.58 (arXiv 2402.17764)?
Прямые PTQ‑прокси на Llama‑3.1‑8B на WikiText‑2:
- Матричный absmean‑тернарий: PPL ≈ 588 638 (ранняя остановка).
- Строчный absmean‑тернарий: PPL ≈ 400 897 (ранняя остановка).
- STE‑пилот (bitnet_qat_pilot.py, 20 шагов QAT): PPL 138 479.
Направление улучшалось с QAT, но оставалось на порядки хуже пригодного уровня.
Вывод: BitNet b1.58 — это рецепт обучения с нуля, а не PTQ‑трюк. Наивное применение на готовой Llama убивает модель.
Нужен был другой путь к тернарности.
Шаг 3 — FWHT + построчное 8‑уровневое квантование как безопасный fallback
Прежде чем отказываться от малых алфавитов, я построил более тонкую метадату вокруг локальной структуры:
- rowwise_fwht_8lv_sub32: 4.0 bpw, полный PPL 7.58.
- rowwise_fwht_8lv_sub16: 5.0 bpw, полный PPL 6.86.
- rowwise_fwht_8lv_sub8: 7.0 bpw, полный PPL 6.38.
Вывод: чтобы получить PPL < 7 на предобученной Llama‑3.1‑8B без дообучения, квантизатору нужна более тонкая локальная метадата уровня ~3 бит. Одного глобального тернарного алфавита недостаточно — но много маленьких алфавитов могут сработать.
Из этой мысли и выросла Route B.
Шаг 4 — Идея «дискретного языка весов» (Route B)
Вместо того чтобы думать о квантизации как о «отобразить float в ближайший уровень», я стал относиться к каждой линейной матрице как к тексту на тернарном алфавите:
- Каждая строка W — это последовательность коротких тернарных слов (цифры {-1, 0, +1}) фиксированной или переменной длины.
- Декодирование: W[n,k] = row_scale[n] · ladder(codes[n,k]) · global_scale.
- Лестница (ladder) — маленький набор шкал на каждую семью (и позже на семью × глубину). Это словарь дискретного языка весов.
- Степень сжатия задаётся длиной слова (глубина 5 → ~3 бит/вес, 7 → ~4.5 бит).
Это ровно то, что написано в эпиграфе, но выраженное кодом. Эта линия и получила имя Route B и стала основной.
Шаг 5 — Конвейер Route B
Конвейер разбит на 4 воспроизводимых стадии (см. archiv.org/reproduce/):
- 01_fit_route_specs — калибровка по семье × глубине: для каждой группы {attn_qk, attn_vo, mlp_up, mlp_down, …} × {early, mid, late} подбираем маленькую лестницу тернарных шкал по сэмплам активаций. Реализация: ternary_route/specs.py + make_ternary_route_specs.py --mode family-depth.
- 02_export_ternary_route_checkpoint — кодирует каждый линейный вес в упакованные тернарные цифры + row_scale. Результат — один checkpoint.pt с uint16 base‑3 цифрами, stop_depth_u8 и fp16 строчными масштабами.
- 03_replace_packed_route_layers — подменяет все nn.Linear на PackedTernaryRouteLinear / FusedTernaryRouteLinear, которые декодируют «на лету» (Triton‑ядро).
- 04_validate_ppl — PPL на WikiText‑2 и бенчмарк скорости.
Все 4 стадии склеены в archiv.org/reproduce/run_all.sh.
Шаг 6 — Per‑row scale: расширение, закрывшее разрыв до dense
Первая чистая Route B‑тернаризация Gemma 4 31B IT уже работала на коротком гейте:
- Baseline 1×128: loss 12.09 / PPL 178 872.
- Route packed 1×128: loss 11.98 / PPL 158 971.
Но на длинном контексте (8×512) видимый разрыв до dense оставался. Чтобы его закрыть, я добавил per‑row scale (per_row_scale_extension):
samples = W / row_abs_max
codes = encode_ternary_route_codes(samples, spec)
row_scale[n] = row_abs_max[n] / global_scale # хранится как fp16
Runtime replace_packed_route_layers сам подхватывает это из ключа {name}.row_scale_fp16. Важно: row_scale сохраняется только если он реально уменьшает relMSE конкретного слоя — иначе слой остаётся обычной Route B. Это улучшение с предохранителем.
Результаты на Gemma 4 31B IT (gemma4_31b_it_all_linear_family_depth_20260416):
- 381 / 410 линейных слоёв приняли row_scale; 29 отклонены как регрессирующие.
- Средний relMSE по весам: 0.3313 % → 0.0667 % (×5).
- Rel‑err выхода matmul на первых 8 слоях: 4.54 % → 1.57 % (×2.89).
- PPL 8×512 на WikiText: dense 8.9196, packed 9.2035, packed + row_scale 9.1666 — закрывает ~13 % разрыва до dense.
- Полное перекодирование: ~55 с на одном H200 для 410 слоёв.
Шаг 7 — Depth‑aware против family‑wise калибровки
Я также пробовал сделать калибровку более зернистой, разбив каждую семью на early:0–19 / mid:20–39 / late:40–59:
- Family‑depth all‑linear: 1.8374 / 6.28.
- Family‑wise: 1.8341 / 6.26.
- Старый фиксированный all‑linear: 1.8583 / 6.41.
Вывод: зернистость важна, но простое разбиение по глубине с текущим фиттером не даёт выигрыша — часть групп family×depth просто схлопывается обратно к family‑wise. Следующий рычаг — либо более сильный фиттер, либо per‑layer масштабы, а не ещё одно bulk‑разбиение.
Также был прототип переменной глубины (max_depth=7, stop_threshold=0.001) с uint16 base‑3 + stop_depth_u8: вышло 1.7242 / 5.61 при dense 1.7112 / 5.54, ценой 24 bpw — это формат качества, а не выигрыш по сжатию.
Шаг 8 — Per‑layer поиск на Llama 3.3 70B
Чтобы понять вклад каждого слоя, я собрал два инструмента:
- experiments/run_single_layer_route_sweep.py — per‑layer свипы поверх базового Route B‑чекпоинта.
- experiments/split_route_checkpoint_by_layer.py — режет полный 560‑слойный all‑linear чекпоинт на per‑layer артефакты для быстрой итерации.
Полный свип attn_k по слоям 0..79 на Llama 3.3 70B нашёл L21, L25, L34, L53 как сильнейших кандидатов на коротком гейте; L21 — единственный, кто пережил более строгий гейт (4×256 → 1.4669 / 4.34). Так возник гибридный фронт из 241 слоя (attn_k late + L21 + gate+up L45), PPL 8.85 на более длинной проверке из 100 окон vs baseline 8.36 — честный +0.49 PPL.
Вывод: композиция не аддитивна. Хорошие одиночные add‑on часто регрессируют при объединении; победы на коротком гейте (0.85x) регулярно проваливаются на более длинных (4×256). Нужен жадный per‑layer поиск с несколькими гейтами.
Шаг 9 — Runtime: декодинг, Triton‑ядра, скорость
Runtime живёт в archiv.org/runtime/ и ternary_route/:
- PackedTernaryRouteLinear — референс‑декодер (uint16 base‑3 → fp16 ladder → matmul).
- FusedTernaryRouteLinear — Triton‑fused matmul, декодирующий на лету. Оба прозрачно поднимают опциональные row_scale_fp16 из чекпоинта.
- load_route_model() — один вызов: грузит Gemma 4 31B IT, патчит 410 слоёв, возвращает стандартную HF‑модель, готовую к генерации.
Скорость на одной H200:
- packed_triton_fused, bs=4, 31 ГБ resident: 37.2 tok/s.
- packed_cached_decode, bs=4: 42.04 tok/s (baseline 39.27) — фактически скорость dense, ценой отказа от выигрыша по активному VRAM (~60 ГБ).
Практическая деталь, найденная на Gemma 4: 10 «отсутствующих» слоёв v_proj (5, 11, 17, 23, 29, 35, 41, 47, 53, 59) — это архитектура, а не баг экспорта. При attention_k_eq_v and not sliding_attention v_proj=None, и values берутся из общего key/value‑пути. Манифест теперь отчитывается по эффективному покрытию = 410 экспортированных + 10 виртуальных shared‑value = 420.
Шаг 10 — Честное сравнение по downstream‑бенчмаркам
Запустил прямой lm_eval по Hugging Face на Gemma 4 31B IT dense vs Route B (cap‑128 отладочный сэмпл):
- MMLU‑Pro: baseline 0.5429 vs route 0.4500 (Route −0.0929).
- HumanEval: baseline 0.0854 vs route 0.0915 (Route +0.0061).
- GPQA: равенство 0.0625, но ограничение парсера.
- AIME24/25: нули на completions‑хорнесе — артефакт; ручной пробник первого сэмпла дал правильный ответ 33 и для dense, и для Route.
Полные MMLU‑Pro запуски (с max_length=8192, max_gen_toks=64) были подняты 17.04.2026 на GPU 2 и 3.
Вывод: текущий completions‑хорнес недооценивает Gemma 4 IT на AIME/GPQA. Для относительной регрессии на MMLU‑Pro / HumanEval он годится, и именно по ней оценивается Route B.
Шаг 11 — Упаковка: самодостаточный релиз
Релиз (archiv.org/) переорганизован так, чтобы быть полностью самодостаточным — без внешних симлинков и скрытых зависимостей:
- paper.md — полная статья, EN (Abstract → §9 + Приложения A/B).
- paper_ru.md — полный русский перевод.
- main.tex + sections/*.tex — arXiv‑дерево, валидированный минимальный source‑пакет arxiv_bundle/arxiv_source.tar.gz.
- runtime/ — вшитый ternary_route/ + load_model.py, generate.py, benchmark.py.
- reproduce/ — 4 шелл‑стадии + run_all.sh.
- kernel/prepare_hf_release.py — сборщик HF‑релиза (shutil.copytree + ignore_patterns, исключает checkpoint*.pt).
- hf_release/route_b_kernel_minimal/ — публичный минимальный HF‑бандл.
- hf_release/discrete_weight_language_full_model/ — полный all‑linear материализованный экспорт (~59 ГБ .safetensors).
17.04.2026 я физически заменил последние три симлинка (runtime/ternary_route → ../../ternary_route, runtime/checkpoint_baseline.pt, runtime/checkpoint_rowscale.pt) на реальную копию каталога / задокументированное отсутствие. Документация, CLI‑помощь, статья и LaTeX‑приложение приведены в соответствие с source‑only релизом. Zenodo‑zip archiv_org_zenodo_source_20260417.zip (228 КБ) собран через rsync с исключениями (*.pt, *.safetensors, hf_release/, arxiv_bundle/, кэши).
Публичные ссылки:
- Hugging Face (ядро + минимальный runtime): https://huggingface.co/armanibadboy/gemma-4-31b-it-route-b-perhead-k37
- Hugging Face (полная материализованная модель, all‑linear): https://huggingface.co/armanibadboy/discrete-weight-language-gemma-4-31b-it-all-linear
- GitHub (исходный пакет): https://github.com/AubakirovArman/discrete-weight-language-gemma4
Какой вопрос закрывал каждый инструмент
| Инструмент | Что он закрыл |
| bitnet_b158_llama31_search.py | Работает ли наивный BitNet b1.58 PTQ на готовой Llama? → Нет. |
| rowwise_refine_search.py | Достаточно ли более тонкой локальной метадаты, чтобы получить <7 PPL на ~3 бит без QAT? → Да, на ~5 bpw. |
| make_ternary_route_specs.py | Можно ли на лестницах family×depth построить рабочий «словарь весов»? → Да. |
| export_ternary_route_checkpoint.py | Можно ли закодировать все линейные слои как упакованные тернарные слова? → Да, 410 слоёв. |
| add_row_scale_to_checkpoint.py | Закрывает ли per‑row scale разрыв до dense? → Да, ~13 % PPL‑разрыва. |
| PackedTernaryRouteLinear / Triton‑fused декодер | Можно ли запустить это на практической скорости? → Да, 37 tok/s packed, 42 tok/s cached. |
| run_single_layer_route_sweep.py | Какие слои лучше всего переносят тернарность? → Узкая «горячая» полоса (21, 45). |
| split_route_checkpoint_by_layer.py | Как итерироваться по слоям без перезагрузки? → Разрезать один раз и переиспользовать. |
| prepare_hf_release.py | Можно ли собирать релиз воспроизводимо? → Да, с валидацией по манифесту. |
Честные ограничения
- Downstream‑покрытие неполное: полный прямой HF MMLU‑Pro на момент релиза ещё в процессе; GPQA / AIME нужны более строгие хорнесы.
- Activation‑aware выбор слоёв, поиск по переменной глубине и обрезка 7‑битного codebook описаны в §8 статьи, но пока не отгружены.
- Композиция per‑layer add‑on не аддитивна; принципиальная процедура поиска добавляемых слоёв пока не построена.
- Полный all‑linear экспорт (~59 ГБ) удобен, но большой; правильнее держать публичный packed‑only HF‑репо.
Зачем это всё
Практический итог: 31‑миллиардная LLM укладывается в маленький, хорошо организованный тернарный язык с умеренной потерей качества и с декодер‑ядрами, работающими почти со скоростью dense. Теоретический итог — ровно эпиграф статьи: если правильно спроектировать язык, непрерывное пространство весов, возможно, вообще не нужно.
Всё — статья, runtime, ядра, скрипты калибровки, инструменты per‑layer поиска, HF‑бандлы, Zenodo‑zip — опубликовано.
Проект Route B уже доступен как drop-in замена для библиотеки Transformers. Вы можете потрогать код и веса прямо сейчас:
- 📄 Статья с формулами и метриками (DOI): https://zenodo.org/records/19632982
- 💻 Репозиторий с ядром Triton: https://github.com/AubakirovArman/discrete-weight-language-gemma4
- 📦 Веса на Hugging Face: https://huggingface.co/armanibadboy/discrete-weight-language-gemma-4-31b-it-all-linear
— Аубакиров Арман