- BrainTools - https://www.braintools.ru -
Qwen-3-Coder-Next [1] — модель с 80 миллиардами параметров и весом 159,4 ГБ. Примерно столько RAM потребовалось бы для её запуска, и это ещё без учёта длинного контекстного окна. И эта модель не считается большой моделью! По слухам, у frontier-моделей более триллиона параметров, для которых понадобилось бы минимум 2 ТБ оперативной памяти [2]. Последний раз я видел столько RAM в одной машине — никогда.
Но что если я скажу, что можно сделать LLM в 4 раза меньше и в 2 раза быстрее — достаточно, чтобы запускать весьма мощные модели на ноутбуке, — при потере точности всего 5–10%?
В этом и заключается магия квантизации.
В этой статье вы узнаете:
Почему параметры модели делают её такой большой
Как работает точность чисел с плавающей точкой и чем жертвуют модели
Как сжимать числа с плавающей точкой с помощью квантизации
Как измерить потерю качества модели после квантизации
Если вы уже знаете, что такое параметры и как хранятся числа с плавающей точкой, можете сразу перейти к квантизации [3].
Параметры, которые также называют «весами», составляют большую часть того, чем LLM является в памяти или на диске. В своей статье о кешировании промптов [4] я писал, что LLM — это «огромный граф из миллиардов тщательно выстроенных операций». Как выглядят эти графы? Начнём с простейшего примера: 1 вход, 1 параметр, 1 выход.

Сам по себе он выглядит незамысловато, но это фундаментальный строительный блок современного AI. Он принимает входное значение 2.0, умножает на параметр 0.5 и получает выход 1.0.
Настоящие LLM, однако, намного больше. На практике у них миллиарды таких параметров. Один из способов, которым они становятся такими большими, — наличие «слоёв». Вот как это выглядит.

Два узла посередине — это слой. Оба показывают 1.0, потому что у обоих соединений параметр равен 0.5, то есть они представляют результат 2.0 * 0.5. Каждое соединение между двумя узлами получает свой параметр — итого 4 параметра. Когда 2 соединения приходят в один узел, значения складываются. Чтобы получить выходное значение 1.5, складываем 1.0 * 1.0 и 0.5 * 1.0.
Это всего лишь 6 параметров — до миллиардов современных LLM ещё далеко. Пример ниже содержит 2 входа, 3 слоя и 2 выхода, итого 64 параметра.
Современные LLM имеют сотни тысяч входов и выходов, десятки слоёв, каждый с тысячами узлов, плотно связанных между собой. Всё это перемножается и даёт миллиарды, а иногда триллионы параметров.
Компьютеры работают с 1 и 0, которые называются «битами». Вот как выглядит целое число (integer) в битовом представлении.
Каждый бит представляет степень двойки, и сумма этих значений даёт итоговое число.
Целые числа удобны тем, что они дискретны. Между 1 и 3 ровно одно число: 2. Компьютеры без труда представляют дискретные значения.
Сложнее становится, когда речь заходит о десятичных дробях. Сколько дробных чисел существует между 1 и 3? Бесконечно много. Компьютеры не могут представить бесконечное количество значений.
Поэтому компьютеры идут на компромисс. Они гарантируют точность до определённого количества значимых цифр, а всё остальное — на основе наилучшего приближения.
Например, 32-битные числа с плавающей точкой охватывают диапазон ±3,40×10³⁸ с точностью до 7 значимых цифр. Для этого 32 бита делятся на 3 части: 1 знаковый бит, 8 бит экспоненты и 23 бита мантиссы. Больше бит экспоненты — шире диапазон; больше бит мантиссы — больше значимых цифр.
Движение влево-вправо исследует весь диапазон значений. Кнопки «+» и «–» сверху перепрыгивают к следующему большему или меньшему представимому числу. 7 гарантированно точных значимых цифр подчёркнуты.
Каждый раз при нажатии «+» вы переходите к следующему наибольшему представимому значению. Обратите внимание [5] на цифру после подчёркнутых — иногда значение перепрыгивает через число. Это и есть компромисс точности в действии.
При нажатии «+» на очень маленьком значении шаг небольшой. На очень большом значении шаг больше. Размер шага меняется в зависимости от положения в диапазоне — значения распределены неравномерно.
Чтобы лучше проиллюстрировать это, ниже приведена гистограмма. Каждый столбец показывает срез диапазона 32-битных чисел с плавающей точкой. Между -0,5 и 0,5 можно представить более 2 миллиардов уникальных значений.
Большинство представимых 32-битных чисел с плавающей точкой — это малые значения. Для LLM это отлично, потому что параметры тоже, как правило, малы. Малые параметры лучше обобщают задачи, с которыми модель не сталкивалась при обучении [6], поэтому в процессе тренировки модели «поощряются» за уменьшение параметров.
Ниже ещё одна гистограмма — она построена на основе 6 популярных моделей с открытыми весами. Почти все параметры очень близки к 0.
Итак, большинство параметров моделей попадают в диапазон значений с плавающей точкой, который можно представить наиболее точно. Впрочем, небольшое число выбросов всё же есть. Я вернусь к ним чуть позже.
Действительно ли языковым моделям нужны 32-битные числа с плавающей точкой? Им не нужен широкий диапазон — это видно из гистограммы распределения параметров выше — и действительно ли им нужны 7 значимых цифр?
Ответ: нет. LLM прекрасно работают с меньшими, менее точными форматами. Ниже пример 16-битного числа с плавающей точкой. Оно работает так же, как 32-битное, но имеет только 5 бит экспоненты и 10 бит мантиссы. Представляет 3 значимые цифры точности и охватывает диапазон ±65504. Занимает вдвое меньше RAM и дискового пространства, чем 32-битный вариант.
Можно комбинировать разное количество бит экспоненты и мантиссы для разных соотношений точности и диапазона. Например, команда Google Brain создала формат bfloat16 с 8 битами экспоненты и только 7 битами мантиссы. Такой подход даёт очень широкий диапазон, но только 2 значимые цифры точности.
Google обнаружила, что 2 значимые цифры достаточно для создания LLM, а чрезвычайно широкий диапазон позволяет не беспокоиться о переполнении при вычислениях, которое может случаться в больших LLM с меньшими форматами чисел.
Есть и более экстремальные примеры, встречающиеся реже: float8 и float4. Ниже — лишь примерные конфигурации этих форматов; на практике люди комбинируют количество бит экспоненты и мантиссы под свои нужды.
Ещё один способ визуализировать точность этих форматов — посмотреть, насколько хорошо они аппроксимируют синусоиду.
На графике сравниваются идеальная синусоида и квантизованные аппроксимации для float32, float16, bfloat16, float8 и float4. Форматы с меньшей точностью выглядят более ступенчато и больше отклоняются от гладкой кривой.
Поговорим о том, как использовать эти знания для уменьшения размера моделей.
Квантизация — это процесс взятия значений из широкого диапазона и упаковки их в более узкий. Это форма сжатия с потерями.
При конвертации, например, из float16 в float8, значения обычно округляются до ближайшего представимого значения. Значения из диапазона float16 отображаются на ближайшее значение float8. Такой подход называется «round-to-nearest» и является одним из многих видов квантизации.
При движении отображается ближайшее представимое значение для каждого размера числа с плавающей точкой.
Это простой способ взять значение из одного диапазона и представить его в меньшем. Однако для LLM так делать нельзя.
Посмотрите на небольшую модель ниже. Обратите внимание, как меняются параметры и выход при округлении от bfloat16 до float8 и float4.
Округление до float8 вполне терпимо, но округление до float4 полностью ломает модель. Часть параметров становится равной 0. Поскольку нет ни одного пути от входа к выходу без умножения на 0, выход всегда будет нулём.
Это происходит потому, что мы неэффективно используем 4 бита. float4 охватывает диапазон от -3 до 3, тогда как наши параметры находятся в диапазоне от -0.89 до 0.16.

Кроме того, float4 может представлять Infinity и NaN. При квантизации это бесполезно.
Посмотрим, как можно эффективнее использовать 16 значений, которые даёт 4-битное число.
Что если вместо диапазона -3 до 3, как у float4, использовать более узкий диапазон, точнее соответствующий данным?
Один из способов — масштабирование данных в новый диапазон. Например, если данные лежат в диапазоне от -14 до 14 и нужно уместить их в -7 до 7, достаточно разделить на 2.

Чтобы вернуться к исходному значению из масштабированного, просто умножьте на 2. Нечётные значения станут непредставимыми — они будут округляться до ближайшего целого. Например, 5 / 2 = 2.5 округляется до 3 и декквантизируется в 3 * 2 = 6. Именно здесь и возникают потери при квантизации.

Этот процесс применим к любому набору значений — нужно лишь найти правильный масштабный коэффициент. Для этого берётся наибольшее абсолютное значение в наборе данных и делится на наибольшее значение в квантизованном диапазоне. Для наших параметров это 0.89 / 7. Вот как это выглядит в JavaScript:
function quantize({ values, bits }) {
// Допустим:
// values = [-0.89, 0.16, 0.08, -0.13, 0.16, -0.54]
// bits = 4
const vmax = Math.max(...values.map(Math.abs)); // 0.89
const qmax = 2 ** (bits - 1) - 1; // 7
const scale = vmax / qmax; // 0.12714285714285714
return {
values: values.map((v) => Math.round(v / scale)),
scale,
};
}
function dequantize({ values, scale }) {
return values.map((v) => v * scale);
}
Код ниже применяет функцию quantize к нашим значениям параметров из небольшой модели выше:
const values = [-0.89, 0.16, 0.08, -0.13, 0.16, -0.54];
const quantized = quantize({ values, bits: 4 });
// { values: [-7, 1, 1, -1, 1, -4], scale: 0.12714285714285714 }
Малые числа с плавающей точкой отображены в квантизованный целочисленный диапазон. Вот как это выглядит визуально:

Теперь можно применить dequantize, умножив значения на scale, чтобы вернуться в исходный диапазон чисел с плавающей точкой:
const dequantized = dequantize(quantized);
// [-0.89, 0.1271, 0.1271, -0.1271, 0.1271, -0.5086]
И визуально:

Сравниваем с оригинальными значениями:
|
Оригинал |
Квантизованное |
Дельта |
Дельта % |
|---|---|---|---|
|
-0.89 |
-0.89 |
0 |
0,0% |
|
0.16 |
0.1271 |
-0.0329 |
-20,6% |
|
0.08 |
0.1271 |
0.0471 |
+58,9% |
|
-0.13 |
-0.1271 |
0.0029 |
-2,2% |
|
0.16 |
0.1271 |
-0.0329 |
-20,6% |
|
-0.54 |
-0.5086 |
0.0314 |
-5,8% |
|
Средняя ошибка [7] +18,0% |
|
|
|
Модель стала в 4 раза меньше (с 16-битной до 4-битной), и в среднем декквантизированные значения отличаются примерно на 18%. Неплохо!
Посмотрим, как ведёт себя небольшая модель при симметричной квантизации.
Итоговый выход quantized 4-bit отличается примерно на 30%. Это огромный прогресс по сравнению с поведением [8] float4 с округлением (выход всегда равен нулю), особенно с учётом того, что квантизованная модель требует в 4 раза меньше RAM, чем оригинал.
Но… можно сделать ещё лучше.
Симметричная квантизация называется так потому, что 0 всегда находится посередине. Мы выбираем масштаб и сжимаем значения вокруг 0. Посмотрим внимательнее на наши симметрично квантизованные значения.

На положительной стороне много незадействованного пространства, потому что максимальное значение — 0.16, тогда как минимальное — -0.89. Из-за симметрии [9] положительный диапазон уходит до 0.89, оставляя большой разрыв между 0.16 и 0.89. Пространство используется неэффективно.
Идея асимметричной квантизации — исправить это, разрешив неравные диапазоны. Вместо сжатия вокруг 0 асимметричная квантизация сжимает данные вокруг их середины и вычисляет «нулевую точку» для компенсации при деквантизации.
function quantize({ values, bits }) {
// Допустим:
// values = [-0.89, 0.16, 0.08, -0.13, 0.16, -0.54]
// bits = 4
const vmax = Math.max(...values); // 0.16
const vmin = Math.min(...values); // -0.89
const qmax = 2 ** (bits - 1) - 1; // 7
const qmin = -(2 ** (bits - 1)); // -8
const scale = (vmax - vmin) / (qmax - qmin); // 0.07
const zero = qmin - Math.round(vmin / scale); // 5
return {
values: values.map((x) => Math.round(x / scale + zero)),
scale,
zero,
};
}
Прогоним наши параметры через эту функцию:
values = [-0.89, 0.16, 0.08, -0.13, 0.16, -0.54];
const quantized = quantize({ values, bits: 4 });
// { values: [ -8, 7, 6, 3, 7, -3 ], scale: 0.07, zero: 5 }
Вот как выглядит это отображение визуально. Новая асимметричная функция quantize решила, что нулевой точкой должна быть 5.

Функция dequantize чуть сложнее симметричной версии — на этот раз требует вычитания помимо умножения:
function dequantize({ values, scale, zero }) {
return values.map((x) => scale * (x - zero));
}
dequantize(quantized);
// [-0.91, 0.14, 0.07, -0.14, 0.14, -0.56]
Как это соотносится со средней ошибкой при симметричной квантизации?
|
Оригинал |
Квантизованное |
Дельта |
Дельта % |
|---|---|---|---|
|
-0.89 |
-0.91 |
-0.02 |
+2,2% |
|
0.16 |
0.14 |
-0.02 |
-12,5% |
|
0.08 |
0.07 |
-0.01 |
-12,5% |
|
-0.13 |
-0.14 |
-0.01 |
+7,7% |
|
0.16 |
0.14 |
-0.02 |
-12,5% |
|
-0.54 |
-0.56 |
-0.02 |
+3,7% |
|
Средняя ошибка +8,5% |
|
|
|
Намного лучше! На 10% меньше ошибки при том же количестве бит. Посмотрим, как это работает на нашей модели.
Итоговый выход всё ещё отличается примерно на 10%, но это заметное улучшение по сравнению с симметричной квантизацией.
Именно это и происходит с параметрами моделей при квантизации до размеров, подходящих для запуска на ноутбуке. Вместо чисел с плавающей точкой в память загружаются малые целые числа. Когда приходит время использовать квантизованные значения — например, для генерации ответа на вопрос — они декквантизируются на лету. Казалось бы, это должно быть медленнее, но позже мы увидим, что на практике это оказывается ещё и быстрее, а не только компактнее.
Неужели кто-то берёт LLM со многими сотнями миллиардов параметров, находит среди них наибольшее и наименьшее значение и квантизирует всю модель за один раз?
Нет.
Я намекал на причину раньше. Вернёмся к графику параметров 6 разных моделей с открытыми весами — на этот раз посмотрим на длинный хвост значений-выбросов.
У всех моделей есть небольшое количество параметров-выбросов — значений, которые намного больше или меньше большинства остальных. Выбросы очень плохо влияют на квантизацию. Посмотрите, что происходит при попытке квантизировать наши параметры с добавленным выбросом 10:

|
Оригинал |
Квантизованное |
Дельта |
Дельта % |
|---|---|---|---|
|
-0.89 |
0.726 |
1.616 |
-181,6% |
|
0.16 |
0 |
-0.16 |
-100,0% |
|
0.08 |
0 |
-0.08 |
-100,0% |
|
-0.13 |
0 |
0.13 |
-100,0% |
|
0.16 |
0 |
-0.16 |
-100,0% |
|
-0.54 |
0.726 |
1.266 |
-234,4% |
|
10 |
10.164 |
0.164 |
+1,6% |
|
Средняя ошибка +116,8% |
|
|
|
Всё сжимается в небольшое количество бакетов, и средняя ошибка резко возрастает. Если квантизировать всю модель за один раз — она будет уничтожена. На практике применяется квантизация блоками, обычно по 32–256 параметров за раз. Так воздействие выбросов оказывается локализованным.
Для декквантизации нужно сохранять значение scale (для симметричной схемы) и scale + zero (для асимметричной). Они хранятся рядом с каждым блоком и составляют накладные расходы. Больший размер блока снижает эти расходы, но более широкие блоки в среднем имеют более широкий диапазон значений, что увеличивает ошибку. Это компромисс.
Странно, правда? Я сам был увлечён этими значениями какое-то время. Если хотите узнать больше, рекомендую:
Эту статью [10] от Apple
Этот пост [11] Тима Деттмерса
Если коротко: никто не знает наверняка, но небольшая доля этих выбросов критически важна для качества модели. Удаление даже одного «супервеса» (super weight, как их называет Apple) может привести к тому, что модель начнёт генерировать полную бессмыслицу.
Из-за их важности реальные схемы квантизации порой проделывают дополнительную работу для сохранения выбросов. Их могут либо не квантизировать вовсе, либо сохранять их расположение и значение в отдельной таблице, а затем убирать, чтобы их блок не был испорчен. При декквантизации эта таблица используется для восстановления выбросов.
В этом разделе я покажу несколько способов измерить потерю качества в LLM. У каждого из них есть плюсы и минусы. Если вы оцениваете квантизованные модели для критически важного сценария, ничто не заменит создания собственного бенчмарка для конкретной задачи.
Оговорки сделаны. Все следующие тесты выполнялись на модели Qwen3.5 9B [12]. Подробности о командах — в приложении в конце статьи.
LLM под капотом строят распределения вероятностей наиболее вероятного следующего «токена» для заданного промпта. Например, при промпте The answer to 2 + 2 is Qwen3.5 9B выдаёт такие вероятности для следующего токена:
|
Токен |
Вероятность |
|---|---|
|
4 |
92,29% |
|
5 |
3,23% |
|
3 |
1,15% |
|
1 |
0,90% |
|
2 |
0,85% |
|
И много других менее вероятных вариантов… |
|
Токену 4 присвоена высокая вероятность — что логично [13], ведь это правильный ответ. Немного беспокоит, что в 3% случаев модель скажет 5, но не будем отвлекаться.
Идея «перплексии» как метрики — свернуть эти распределения вероятностей в одно число, с которым удобно работать. Расчёт перплексии требует немного математики [14], но это не страшно. Для одного предсказания выше — The answer to 2 + 2 is — берём вероятность правильного токена 4 и делаем следующее:
pCorrect = 0.9229; // 92.28%, probability for `4
perplexity = Math.exp(-Math.log(pCorrect)); // 1.08
Меньшее значение лучше. Читается так: «модель считает, что существует ~1,08 правдоподобных токенов, завершающих этот промпт». Чем ниже перплексия, тем выше вероятность, назначенная правильному токену.
При промпте And then I вероятности у Qwen3.5 9B распределены шире:
|
Токен |
Процент |
|---|---|
|
have |
3,02% |
|
was |
3,00% |
|
realized |
2,98% |
|
found |
2,73% |
|
‘m |
2,73% |
|
got |
2,63% |
|
will |
2,54% |
|
‘ll |
2,43% |
|
thought |
2,38% |
|
saw |
2,33% |
|
went |
2,16% |
|
И много других примерно равновероятных вариантов… |
|
Предположив, что правильный следующий токен — was, расчёт перплексии для этого распределения выглядит так:
pCorrect = 0.03; // 3%, probability for `was`
perplexity = Math.exp(-Math.log(pCorrect)); // 33.33
Значительно выше — но что бы вы предсказали после And then I? Это куда более неоднозначный промпт, и вполне логично, что модель менее уверена в предсказании.
Для измерения перплексии Qwen 3.5 9B на разных уровнях квантизации я использовал инструмент llama-perplexity из проекта llama.cpp [15]. Он принимает текст-эталон, скользит по нему окном токенов как промптом и использует следующий токен из эталона как правильный ответ.
Чтобы проиллюстрировать идею скользящего окна, ниже показаны токены промпта и правильный токен. Используйте кнопки «→» и «←» для перемещения окна.
На каждом шаге накапливается -Math.log(pCorrect), и в конце вычисляется Math.exp от среднего. Если собрать вероятности правильных токенов для всего текста в массив probs, итоговый расчёт выглядит так:
let total = 0;
for (const prob of probs) {
total += -Math.log(prob);
}
const perplexity = Math.exp(total / probs.length);
Проект llama.cpp использует тестовый датасет wikitext-2 [16] в качестве эталонного текста — я сделал то же самое. Это просто содержимое страницы Википедии о Роберте Булутере [17]. Понятия не имею, почему именно она. Интересно, знает ли он, что используется для бенчмаркинга квантизованных LLM…
Результаты:
|
Формат |
Перплексия |
|---|---|
|
bfloat16 |
8,186 |
|
8-bit симметричная |
8,193 (+0,1%) |
|
4-bit асимметричная |
8,563 (+4,6%) |
|
4-bit симметричная |
8,71 (+6,4%) |
|
2-bit асимметричная |
66,1 (+707,5%) |
При 8-битной симметричной — почти никаких изменений, при 4-битных вариантах — небольшая деградация, при 2-битной — почти полный коллапс. Квантизация заставила модель быть менее уверенной, в среднем рассматривая более широкий набор токенов.
Важно: перплексия учитывает только вероятность правильного токена. Вероятности всех остальных токенов не используются. Поэтому перплексия не даёт полной картины того, как квантизация повлияла на модель.
KL-дивергенция (Kullback-Leibler divergence) — метрика, показывающая, насколько хорошо 2 вероятностных распределения совпадают.
KL-дивергенция равна 0 только тогда, когда два распределения идентичны. Чем больше они расходятся, тем выше KL-дивергенция.
Не только горизонтальное смещение повышает KL-дивергенцию — любое несовпадение оказывает этот эффект.
Эту метрику можно применить к распределениям вероятностей токенов, которые выдаёт LLM. На графике ниже показаны вероятности каждой цифры от 0 до 9 для продолжения промпта The answer to 2 + 2 is.
Интуитивно понять значение KL-дивергенции нет простого способа — лишь «чем выше, тем хуже». У неё нет естественного максимума (нельзя сказать, что «KL-дивергенция всегда от 0 до 1»), она зависит от свойств модели. Поэтому сравнивать её имеет смысл только между разными квантизациями одной и той же модели.
Инструмент llama-perplexity также измеряет KL-дивергенцию через флаг --kl-divergence, подробности в приложении. В качестве эталонного текста я снова использовал wikitext-2 [16].
|
Формат |
Средняя KL-дивергенция |
|---|---|
|
8-bit симметричная |
0,0008 |
|
4-bit асимметричная |
0,0593 |
|
4-bit симметричная |
0,0675 |
|
2-bit асимметричная |
2,1447 |
Несмотря на сложность интерпретации и ограничение на сравнение только квантизаций одной модели, KL-дивергенция имеет преимущество перед перплексией: она учитывает всё распределение вероятностей каждого предсказания.
Перплексия заботится только о вероятности правильного токена. Вероятности всех остальных токенов могут измениться, но если правильный остался прежним, перплексия не изменится. При KL-дивергенции любое изменение всего распределения повысит оценку. KL-дивергенция даёт более полную картину того, как квантизация изменила поведение [18] модели.
Я написал отдельную статью [19] про бенчмаркинг LLM. Один из способов измерить влияние квантизации — сравнить оценку модели на бенчмарках до и после.
Для этой статьи я решил запустить бенчмарк GPQA Diamond [20]. Я писал об этом бенчмарке [21] подробнее, но здесь достаточно знать: это набор из 198 очень сложных вопросов с несколькими вариантами ответа по биологии, химии и физике. У каждого вопроса 4 варианта, случайное угадывание даёт в среднем 25%. Подробности о запуске — в приложении.
|
Формат |
Правильный ответ |
Неправильный ответ |
Нет ответа |
|---|---|---|---|
|
bfloat16 |
66,7% |
33,3% |
0% |
|
8-bit симметричная |
73,2% |
26,8% |
0% |
|
4-bit асимметричная |
62,6% |
36,4% |
1% |
|
4-bit симметричная |
66,2% |
29,3% |
4,5% |
|
2-bit асимметричная |
1% |
2% |
97% |
Если вас смутили результаты — не волнуйтесь, меня тоже. 8-битная квантизация даёт более высокий балл, чем оригинальная модель в bfloat16. Трудно сказать, почему именно так происходит — возможно, это просто удача. При вопросах с несколькими вариантами всегда есть 25% шанс угадать случайно.
Вывод из этих результатов: 8- и 4-битная квантизация работают хорошо, а 2-битная — совсем плохо. На 97% вопросов ответ не был найден, что могло означать зацикленность модели или непонимание задачи.
Последний тест — самый простой и наименее строгий: просто поговорить с моделью. Я задал один и тот же вопрос всем уровням квантизации: «Какова столица Англии, Великобритания?»
|
Формат |
Ответ |
|---|---|
|
Оригинал bfloat16 |
The capital city of England and the United Kingdom is London. |
|
8-bit симметричная |
The capital city of England and the United Kingdom is London. |
|
4-bit асимметричная |
The capital city of England is London. It is also the capital city of the entire United Kingdom. |
|
4-bit симметричная |
The capital city of England is London. |
|
2-bit асимметричная |
<нет ответа> |
Все правильно, кроме 2-битной квантизации, которая почти каждый раз отказывалась отвечать. «Reasoning trace» при этом немного сошёл с ума:
The capital city of England is London.
The capital city of England is London.
The capital city of England is London.
The capital city of England is London.
The capital city of England is London.
The capital city of England is London.
The capital city of England is London.
The capital city of England is London.
The capital city of London
The capital city of London
The capital city of London
The capital city of London
The capital city of London
The capital city of London
The capital city of London
The capital city of London
После чего модель выдавала пустой ответ. Думаю, можно уверенно сказать: 2-битная квантизация — это слишком большая потеря информации для Qwen3.5 9B, и на таком уровне сжатия модель не годится для использования.
Очевидно, это не строгий научный [22] тест, но он помогает поставить все остальные оценки в контекст.
Последнее, о чём хочу поговорить, — скорость модели. Меньший размер квантизации, как правило, также делает модель быстрее. Это объясняется главным образом тем, что данных меньше и перемещать их внутри GPU быстрее.
Проект llama.cpp [15] предоставляет инструмент llama-bench, который я запускал на MacBook Pro M1 Max и на арендованном облачном H100 SXM GPU. Производительность измеряется в «токенах в секунду» — то есть насколько быстро модель генерирует ответы.
|
Формат |
M1 Max |
H100 |
|---|---|---|
|
bfloat16 |
19,45 |
106,85 |
|
8-bit симметричная |
32,36 |
141,61 |
|
4-bit асимметричная |
43,32 |
175,70 |
|
4-bit симметричная |
46,05 |
177,06 |
|
2-bit асимметричная |
40,25 |
166,90 |
|
|
|
|
|
Единицы: токенов/сек |
|
|
Между оригинальным bfloat16 и 8- и 4-битными квантизациями огромная разница в производительности. Почему 2-битная квантизация оказалась медленнее 4-битной — мне непонятно, я ожидал обратного. Если знаете почему, напишите мне [23]!
Главное, что я хочу донести этой статьёй: квантизованные модели, если честно, вполне хорошие.
Когда я только захотел написать эту статью, я ничего не знал о квантизации. Я предполагал, что качество модели деградирует линейно по мере сжатия. То есть 8-битная квантизация bfloat16 будет вдвое хуже, затем 4-битная вдвое хуже 8-битной, и так далее.
Это оказалось не так.
Похоже, переход с 16-битной до 8-битной квантизации несёт почти нулевые потери качества. Переход с 16-битной до 4-битной более заметен, но это точно не «в четыре раза хуже оригинала». Ближе к 90%, в зависимости от метрики.
Не бойтесь запускать локальные квантизованные модели. Обрыв качества существует, но теперь у вас есть инструменты, чтобы понять, где он находится. Пока вы не упали с этого обрыва, квантизованные модели работают хорошо, и не стоит избегать их только из-за того, что они сжаты.
Экспериментируя с квантизованными локальными моделями, попробуйте AI gateway [24] от ngrok. С его помощью можно маршрутизировать LLM-запросы к этим локальным моделям [25], работающим как на вашем ноутбуке, так и на арендованном GPU в облаке.
Эта статья целиком посвящена «квантизации после обучения» (post-training quantization, PTQ). Некоторые модели сегодня проходят «квантизацию во время обучения» (quantization aware training, QAT), при которой квантизация вводится в процессе предобучения и помогает модели устанавливать параметры, удобные для квантизации.
Я также рассмотрел лишь пару методов квантизации — есть множество более сложных альтернатив с другими компромиссами. Рекомендую изучить AWQ и GPTQ.
Квантизация — лишь один из методов уменьшения размера LLM. Есть ещё прунинг параметров и дистилляция. Статья Efficient Large Language Models: A Survey [26] немного устарела (май 2024), но даёт хороший обзор методов повышения эффективности LLM.

Друзья! Перевод этой статьи подготовила команда ТГК «AI for Devs» — канала, где мы рассказываем про AI-агентов, плагины для IDE, делимся практическими кейсами и свежими новостями из мира ИИ. Подписывайтесь [27], чтобы быть в курсе и ничего не упустить!
brew install llama.cpp
llama-server -hf unsloth/Qwen3.5-9B-GGUF:BF16 --port 8000
Эта команда загружает версию Qwen 3.5 9B в bfloat16 и запускает OpenAI-совместимый сервер на порту 8000.
Склонируйте официальный репозиторий GPQA [20] и распакуйте набор вопросов:
git clone git@github.com:idavidrein/gpqa.git
cd gpqa
uv venv --python 3.9
uv pip install -r requirements.txt
unzip -P deserted-untie-orchid dataset.zip
Оригинальный репозиторий не позволяет запускать бенчмарк для произвольной локальной модели. Я внёс некоторые изменения [28], чтобы запускать:
OPENAI_BASE_URL=http://localhost:8000/v1 uv run python baselines/run_baseline.py main
--model_name qwen3.5-9b-bf16
--data_filename dataset/gpqa_diamond.csv
--prompt_type zero_shot
--max-tokens 30000
--verbose
Бенчмарк запускался на llama-server с 4096 токенами для reasoning:
llama-server -hf unsloth/Qwen3.5-9B-GGUF:BF16 --reasoning-budget 4096
cd ~/Library/Caches/llama.cpp
llama-quantize unsloth_Qwen3.5-9B-GGUF_Qwen3.5-9B-BF16.gguf unsloth_Qwen3.5-9B-GGUF_Qwen3.5-9B-Q8_0.gguf Q8_0
Разберём по частям:
~/Library/Caches/llama.cpp — каталог, где llama.cpp хранит загруженные модели.
unsloth_Qwen3.5-9B-GGUF_Qwen3.5-9B-BF16.gguf — имя файла модели, загруженной командой llama-server -hf unsloth/Qwen3.5-9B-GGUF:BF16 ранее.
unsloth_Qwen3.5-9B-GGUF_Qwen3.5-9B-Q8_0.gguf — имя файла для новой квантизованной модели.
Q8_0 — формат квантизации: «Q8» означает 8-битную квантизацию, «_0» — симметричную.
Команда выполнилась за минуту на ноутбуке, и результирующий файл занимает чуть больше половины размера оригинала.
Строки формата для каждого уровня квантизации, использованного в статье:
|
Формат |
Строка |
|---|---|
|
8-bit симметричная |
Q8_0 |
|
4-bit асимметричная |
Q4_1 |
|
4-bit симметричная |
Q4_0 |
|
2-bit асимметричная |
Q2_K |
В llama.cpp, насколько я могу судить, нет опций для 8-битной асимметричной или 2-битной симметричной квантизации.
Также замечу, что 2-битная квантизация в моих тестах отличается от остальных. Я посмотрел отличное видео reverse-engineering GGUF [29] от Джулии Турк, но пришлось выбирать между K-quant и I-quant — для 2-битной не оказалось доступного legacy quant. Пошёл на компромисс. Подозреваю, это как-то связано с тем, почему результаты llama-bench для 2-битной квантизации оказались странными.
Для загрузки тестового датасета wikitext-2 я использовал скрипт get-wikitext-2.sh [30] из репозитория llama.cpp, затем запустил:
llama-perplexity -hf unsloth/Qwen3.5-9B-GGUF:BF16 -f wikitext-2-raw/wiki.test.raw -c 512 --kl-divergence-base ~/reference.kld
Это сохраняет базовые данные KL-дивергенции, необходимые при измерении квантизованных версий модели. Используйте их так:
llama-perplexity -m path/to/quantized/model.gguf -f wikitext-2-raw/wiki.test.raw -c 512 --kl-divergence-base ~/reference.kld --kl-divergence
llama-cli -hf unsloth/Qwen3.5-9B-GGUF:BF16
llama-bench -hf unsloth/Qwen3.5-9B-GGUF:BF16
Автор: python_leader
Источник [31]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/27791
URLs in this post:
[1] Qwen-3-Coder-Next: https://huggingface.co/Qwen/Qwen3-Coder-Next
[2] памяти: http://www.braintools.ru/article/4140
[3] перейти к квантизации: #what-is-quantization
[4] кешировании промптов: https://ngrok.com/blog/prompt-caching
[5] внимание: http://www.braintools.ru/article/7595
[6] обучении: http://www.braintools.ru/article/5125
[7] ошибка: http://www.braintools.ru/article/4192
[8] поведением: http://www.braintools.ru/article/9372
[9] симметрии: http://www.braintools.ru/article/3088
[10] Эту статью: https://machinelearning.apple.com/research/super-weight
[11] Этот пост: https://timdettmers.com/2022/08/17/llm-int8-and-emergent-features/
[12] Qwen3.5 9B: https://huggingface.co/Qwen/Qwen3.5-9B
[13] логично: http://www.braintools.ru/article/7640
[14] математики: http://www.braintools.ru/article/7620
[15] llama.cpp: https://github.com/ggml-org/llama.cpp
[16] wikitext-2: https://github.com/ggml-org/llama.cpp/blob/master/scripts/get-wikitext-2.sh
[17] Роберте Булутере: https://en.wikipedia.org/wiki/Robert_Boulter
[18] поведение: http://www.braintools.ru/article/5593
[19] отдельную статью: https://ngrok.com/blog/ai-benchmarks
[20] GPQA Diamond: https://github.com/idavidrein/gpqa
[21] писал об этом бенчмарке: https://ngrok.com/blog/ai-benchmarks#gpqa-diamond
[22] научный: http://www.braintools.ru/article/7634
[23] напишите мне: mailto:s.rose@ngrok.com
[24] AI gateway: https://ngrok.ai/
[25] маршрутизировать LLM-запросы к этим локальным моделям: https://ngrok.com/docs/ai-gateway/concepts/custom-providers
[26] Efficient Large Language Models: A Survey: https://openreview.net/pdf?id=bsCCJHbO8A
[27] Подписывайтесь: https://t.me/+coiwkOGkUdE1ZGFi
[28] внёс некоторые изменения: https://github.com/samwho/gpqa/commit/5100232b529fe4cede08d9970232fb7a6e93c42b
[29] reverse-engineering GGUF: https://www.youtube.com/watch?v=vW30o4U9BFE
[30] get-wikitext-2.sh: http://get-wikitext-2.sh
[31] Источник: https://habr.com/ru/articles/1015510/?utm_campaign=1015510&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.