Перейти к содержанию
Публикация AiManual

BitNet и Bonsai на CPU: какие low-bit GGUF-файлы дают корректный вывод и как быстро работают

Независимая проверка low-bit GGUF на CPU: bitnet.cpp выдаёт неверный вывод для BitNet b1.58 2B4T, а Bonsai и Ternary-Bonsai 1.7B корректны в llama.cpp, Prism и

Коротко

Что будет в материале

  1. 01

    Что проверяли и почему «на глаз» здесь не работает

  2. 02

    BitNet b1.58 2B4T: почему bitnet.cpp даёт неверный вывод

  3. 03

    Как починить BitNet: патчи, #642 и вариант без патчей

  4. 04

    Bonsai и Ternary-Bonsai 1.7B: где вывод корректен

Что проверяли и почему «на глаз» здесь не работает

Независимая проверка low-bit моделей на CPU, опубликованная в r/LocalLLaMA, дала два противоположных результата. Основная ветка bitnet.cpp (коммит 0b341e5) выдаёт некорректный вывод для BitNet b1.58 2B4T. Файлы Bonsai и Ternary-Bonsai 1.7B, наоборот, дают корректный результат во всех трёх рантаймах, где загружаются: llama.cpp, форк Prism от PrismML и ik_llama.cpp.

Ловушка в том, что на x86 текст BitNet в bitnet.cpp выглядит связно. Читаешь абзац, он грамматически нормальный, и повода сомневаться нет. Перплексия на тех же токенах при этом примерно в 6 раз выше эталона из HF transformers. На ARM модель вообще печатает «????» вместо текста. Обе причины были описаны в issue задолго до проверки, так что это подтверждение известных багов, а не свежая находка (разбор с числами и коммитами).

Методика простая и жёсткая: сравнение с эталоном, а не оценка качества на глаз. Для Bonsai считали KL-дивергенцию против точного F16-эталона на ARM и x86. Для BitNet сравнивали перплексию с HF transformers на одинаковых токенах. Связное предложение на выходе ничего не доказывает: распределение вероятностей следующего токена может уехать далеко от эталонного, и внешне это почти незаметно.

Что попало в проверку:

  • модели: BitNet b1.58 2B4T, Bonsai и Ternary-Bonsai 1.7B, Bonsai и Ternary-Bonsai 27B;
  • рантаймы: bitnet.cpp, llama.cpp, форк Prism, ik_llama.cpp;
  • железо: ARM-сервер на Neoverse-N1 и x86-виртуалки с AVX2 (Intel Xeon, AMD EPYC).

У каждого числа зафиксированы коммит рантайма, sha256 файла и флаги запуска. Один скрипт перезапускает основные измерения. Это не бюрократия: без такой тройки метаданных цифры скорости несравнимы, потому что один и тот же файл в разных сборках отличается в десятки раз.

BitNet b1.58 2B4T: почему bitnet.cpp даёт неверный вывод

Модель обучена с активацией ReLU², то есть с квадратом ReLU. Основная ветка bitnet.cpp применяет на этом месте SiLU. Функции похожи по форме, но ведут себя по-разному: SiLU пропускает небольшие отрицательные значения и иначе масштабирует положительные. Веса модели подогнаны под ReLU², поэтому подмена меняет распределение выходов на каждом слое, и ошибка накапливается по глубине сети.

Вторая причина касается ARM. Там нужен отдельный кернел под эту архитектуру, а без него вычисления ломаются настолько, что модель выдаёт «????». Два независимых дефекта дают два разных симптома: на x86 тихая деградация качества, на ARM явный мусор на выходе.

Что именно ломается: SiLU вместо ReLU² и ARM-ядро

Проблему активации зафиксировали в issue #588, проблему ARM в issue #470. Оба отчёта существовали до проверки, то есть тест подтвердил давно известное состояние основной ветки, а не открыл новую ошибку.

Коварство ситуации в асимметрии симптомов. Пользователь на x86 видит нормальный текст, не замечает разницы в 6 раз по перплексии и делает вывод, что всё работает. Пользователь на ARM видит «????» и сразу идёт искать причину. Хуже всего первому: он получает поток связных, но статистически неверных ответов и не имеет повода ему не доверять.

Как починить BitNet: патчи, #642 и вариант без патчей

Рабочих путей два, и они разной степени удобства. Первый - патчить bitnet.cpp, второй - не трогать его вообще и взять другой рантайм.

Однострочная правка активации и ARM-ядро

Правка активации из #588 меняет одну строку, и этого достаточно, чтобы вернуть корректность на x86. Альтернатива: применить #605 и выставить runtime key. Для ARM нужен кернел из isHuangXin/llama.cpp#2.

Если не хочется собирать два патча по отдельности, есть #642: в третьей версии от 15 октября он объединяет однострочную правку активации с тем же ARM-ядром и корректен и на ARM, и на x86. Для тех, кто готов собирать из исходников, это самый цельный вариант.

Вариант без патчей: конвертация в iq2_bn_r4 для ik_llama.cpp

Без патчей корректный вывод даёт ik_llama.cpp после однократной конвертации файла в формат iq2_bn_r4. Ключевое слово: однократной. Конвертация делается один раз, дальше вы работаете с готовым файлом и не возвращаетесь к сборке патчей.

Оговорка по-честному: это обходной путь, а не исправление bitnet.cpp. Основная ветка bitnet.cpp на момент проверки для BitNet b1.58 2B4T непригодна, и патч в апстрим ситуацию на вашей машине сам не изменит. Если нужен другой CPU-only движок под ту же задачу, есть отдельный разбор Project Zero на чистом C99 с mmap-загрузкой GGUF и собственным VBMI-ядром.

Bonsai и Ternary-Bonsai 1.7B: где вывод корректен

С файлами Bonsai картина проще и приятнее. Проверка KL против точного F16-эталона на ARM и x86 показала: файлы 1.7B дают корректный вывод везде, где загружаются, будь то llama.cpp, форк Prism или ik_llama.cpp. Для 27B результаты Prism и ik_llama.cpp согласуются между собой. Проблема здесь не в корректности, а в скорости и в том, какой файл вообще загрузится в конкретной сборке.

Какие файлы брать: PQ2_0 и Q2_0_g64

Проверенные и рекомендуемые к работе файлы: PQ2_0 для Prism и ik_llama.cpp, Q2_0_g64 для llama.cpp, Prism и ik_llama.cpp. У второго варианта есть важная дата. 13 октября llama.cpp смержил AVX2-ядро, ускорение около 6 раз. В тот же день ik_llama.cpp добавил поддержку Q2_0 с ускорением примерно в 15 раз относительно Prism, корректно на ARM и x86.

Отсюда практическое правило: смотрите на дату коммита рантайма. Сборка недельной давности и свежая сборка с одним и тем же файлом дадут разницу в разы, и вы спишете её на железо.

Ловушка с файлом «recommended»

На карточке Ternary-Bonsai-1.7B есть файл Q2_0 с пометкой «recommended». Он не загружается ни в llama.cpp, ни в Prism, ни в ik_llama.cpp. Три рантайма, один результат: файл просто не подхватывается.

Вывод неприятный, но полезный: бейдж на карточке модели не заменяет проверку. Сверяйтесь с sha256 файла и коммитом рантайма, а не с надписью «recommended». Это же правило спасает при сравнении чисел из чужих бенчмарков.

Скорость на CPU: цифры и почему размер файла не главное

Главный тезис проверки: скорость определяется комбинацией файл × рантайм × CPU, а не размером файла. Цифры prompt processing для Ternary-Bonsai 1.7B на x86:

ФайлРантаймPrompt processing
PQ2_0ik_llama.cpp с флагом -rtr71 t/s
PQ2_0Prism28 t/s
Q2_0 g64Prism2 t/s

Тот же файл PQ2_0 разгоняется с 28 до 71 t/s, и разница здесь не в модели и не в железе, а в рантайме и флаге. Для 27B картина ещё нагляднее: меньший файл PTQ1_0 примерно в 4,4 раза медленнее PQ2_0 по prompt processing в Prism на AVX2, замер через llama-bench. Меньше по размеру не значит быстрее. Похожая логика работает и в других сборках: в разборе ускорения MoE-инференса в llama.cpp прирост тоже дают патчи под конкретное железо, а не смена размера весов.

Флаг -rtr: главный переключатель скорости в ik_llama.cpp

-rtr выключен по умолчанию. Без него ik_llama.cpp часто оказывается самым медленным из трёх рантаймов, с ним на том же x86 выдаёт 71 t/s против 28 в Prism. Это самый дешёвый способ ускориться: проверить флаг до того, как менять рантайм или файл.

Практический риск очевиден. Человек запускает ik_llama.cpp без -rtr, видит худший результат, делает вывод «рантайм плохой» и уходит. Флаг при этом никто не включал.

Почему Q2_0 g64 в Prism даёт 2 t/s

У Prism пока нет SIMD-ядра под Q2_0 g64. 2 t/s здесь не свойство файла, а отсутствие быстрого кернела: вычисления идут по медленному пути. Когда ядро появляется, картина меняется радикально, как это случилось 13 октября в llama.cpp (AVX2-ядро, около 6 раз) и в ik_llama.cpp с поддержкой Q2_0 (примерно в 15 раз быстрее Prism). Смотреть на дату коммита рантайма полезнее, чем на таблицу квантизаций.

Границы теста: что осталось открытым вопросом

Все приведённые числа получены на ARM Neoverse-N1 и на x86 с AVX2 (Intel Xeon, AMD EPYC). Результаты для AVX-512, Zen 4, Graviton и Raspberry Pi 5 не проверялись и остаются открытым вопросом. Apple Silicon в тест тоже не попал, а скрипты пока только под Linux.

Это ограничение самой проверки, и переносить цифры на своё железо без замеров нельзя. Разница между AVX2 и AVX-512 в ядрах инференса может быть кратной, особенно там, где появляются новые SIMD-пути. Числа из таблицы выше описывают конкретные машины и конкретные коммиты, а не «CPU в целом».

Как воспроизвести проверку самому

Автор проверки оставил путь для повторения на своём железе: ноутбук прогоняет подмножество измерений на бесплатной виртуалке Kaggle или Colab за 30-60 минут и сравнивает результат с опубликованными числами. Плюс один скрипт, который перезапускает основные измерения.

Минимальный набор для честного сравнения:

  1. зафиксировать коммит рантайма, сборка из main без хеша ничего не доказывает;
  2. сверить sha256 файла модели, разные файлы под одним именем встречаются;
  3. записать флаги запуска, включая -rtr и параметры контекста;
  4. сравнивать prompt processing отдельно от генерации, это разные метрики.

На бесплатной VM прогоняется подмножество, а не весь набор измерений, и это нормально для первичной сверки. Автор отдельно оговаривает, что английский для него не родной и что для анализа и формулировок использовался Claude Code, а прогоны шли на его ARM-сервере и бесплатных Kaggle/Colab VM, числа проверены. Это стоит держать в голове, оценивая статус материала: независимая проверка, но не рецензируемая статья.

Кому подходят low-bit модели на CPU: практический вывод

Для Bonsai и Ternary-Bonsai 1.7B решение простое. Берите PQ2_0 для Prism и ik_llama.cpp либо Q2_0_g64 для llama.cpp, Prism и ik_llama.cpp с 13 октября. В ik_llama.cpp не забудьте про -rtr, иначе получите самый медленный вариант из доступных. Файл Q2_0 с пометкой «recommended» пропускайте: он не загружается ни в одном из трёх рантаймов.

Для BitNet b1.58 2B4T выбора два. Либо #642 в третьей версии от 15 октября (однострочная правка активации плюс ARM-ядро, корректно на ARM и x86), либо ik_llama.cpp после однократной конвертации файла в iq2_bn_r4. Основную ветку bitnet.cpp для этой модели использовать не стоит.

Ограничения, о которых нужно помнить до того, как строить на этом продакшен:

  • тесты не покрывают AVX-512, Zen 4, Graviton, Raspberry Pi 5 и Apple Silicon;
  • файл «recommended» на карточке модели не загружается;
  • меньший файл может быть в 4,4 раза медленнее большего по prompt processing;
  • без -rtr ik_llama.cpp часто проигрывает остальным;
  • скрипты воспроизведения пока только под Linux.

Low-bit модели на CPU уже дают корректный вывод, но требуют внимания к деталям сборки. Если ваш сценарий ближе к GPU с ограниченной VRAM, пригодится отдельное сравнение Bonsai Ternary PQ2 против Qwen3.8 27B IQ3_XXS на 16 ГБ: там качество совпало, а разница по скорости и расходу токенов оказалась трёхкратной.

Подписаться на канал