Квантование весов
Как хранить веса в 8 и 4 битах почти без потери качества — симметричное и асимметричное квантование, масштабы на тензор, канал и группу, GPTQ и AWQ, форматы GGUF и обучение с учётом квантования в Kimi K2 Thinking и DeepSeek-V4.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Llama 3 70B в BF16 весит 141 ГБ: не помещается даже в H100 на 80 ГБ. В 4 битах она весит около 40 ГБ — это две ваши RTX 4080. И дело не только в объёме. Из урока 7.1.1 вы знаете, что скорость decode равна пропускной способности памяти, делённой на байты весов. В 3–4 раза меньше байт — в 3–4 раза быстрее генерация.
КвантованиеквантованиеEN quantization — это хранение чисел меньшим числом бит: вместо 16-битного числа с плавающей точкой — маленькое целое плюс общий для группы чисел масштаб. Форматы с плавающей точкой (BF16, FP8, FP4) мы разобрали в уроке 5.4.1. Здесь главные герои — целые INT8 и INT4, на которых работает почти весь локальный запуск LLM.
Квантовать можно три вещи:
- веса — больше всего памяти и главный выигрыш для decode;
- активации — чтобы само умножение шло в INT8/FP8 и ускорялся prefill;
- KV-кэш — чтобы влезли длинный контекст и большой батч.
Сетка целых чисел
Заголовок раздела «Сетка целых чисел»INT8 хранит целые от −128 до 127, INT4 — от −8 до 7, то есть всего 16 значений. Общее правило: бит дают разных кодов; если нужен знак, это целые от до (для : от −8 до 7). Квантование — это наложение такой сетки на вещественные числа. Каждое число заменяется ближайшим узлом, и ошибка — это округление.
Симметричное квантование (absmax)
Заголовок раздела «Симметричное квантование (absmax)»Самая простая схема — симметричное квантованиесимметричное квантование (absmax)EN symmetric (absmax) quantizationСетка целых чисел симметрична относительно нуля: scale = max|x| / qmax, q = round(x / scale). Ноль кодируется точно. Стандарт для весов.. Растягиваем сетку так, чтобы самое большое по модулю число попало в крайний узел (127 для INT8, 7 для INT4). Код −128 (или −8 в INT4) симметричная схема не использует: так сетка одинакова по обе стороны от нуля.
Хранится целое и одно вещественное число — масштабмасштаб квантования (scale)EN quantization scaleЧисло, на которое делят вещественные значения перед округлением и умножают после. Хранится рядом с целыми кодами — на тензор, на канал или на группу. (scale). Ошибка любого числа не больше половины шага сетки, .
Пример: .
- INT8: , коды . Восстанавливается всё точно.
- INT4: , коды , восстановленные значения . Число 0.02 превратилось в ноль, а 0.5 — в 0.363: шаг сетки огромен.
Асимметричное квантование (с нулевой точкой)
Заголовок раздела «Асимметричное квантование (с нулевой точкой)»Если данные не симметричны (например, все от −1 до 3), заметная часть симметричной сетки пропадает: уровни от −7 до −3 никогда не используются. асимметричное квантованиеасимметричное квантование (с нулевой точкой)EN asymmetric (zero-point) quantizationСетка 0 … 2^b − 1 натягивается на отрезок [min, max]. Не тратит уровни впустую, если данные не симметричны (например, после ReLU). натягивает сетку на отрезок :
Целое — нулевая точканулевая точка (zero point)EN zero pointЦелый код, который соответствует вещественному нулю в асимметричном квантовании. Восстановление — x̂ = (q − zero_point) · scale.: код, который в точности означает вещественный ноль. Это важно: нули встречаются постоянно (паддинг, выходы ReLU), и ошибаться на них нельзя. Чтобы ноль всегда попадал в сетку, отрезок расширяют до нуля: берут не больше 0, а — не меньше 0 (так сделано и в задании урока).
Пример: в 8 битах. , , коды . Ноль закодирован как 64 и восстанавливается точно.
Веса LLM почти симметричны вокруг нуля, поэтому для них обычно берут симметричную схему. Асимметричную чаще применяют к активациям, а в некоторых форматах (например, k-квантах llama.cpp) у групп весов хранят и масштаб, и минимум.
Гранулярность: на тензор, на канал, на группу
Заголовок раздела «Гранулярность: на тензор, на канал, на группу»Матрица весов слоя содержит миллионы чисел. Один масштаб на всю матрицу — дёшево, но опасно: если где-то есть выбросвыброс (outlier)EN outlierЗначение, во много раз большее типичного. Задаёт масштаб своей группы и «сплющивает» остальные числа к нулю. В активациях LLM от ~6.7B параметров появляются целые каналы-выбросы (LLM.int8()). в 50 раз больше типичного веса, он задаёт масштаб, и все обычные веса округляются в ноль.
Поэтому масштаб делают мельче:
- на тензор — один масштаб на матрицу;
- на каналпоканальное квантованиеEN per-channel quantizationСвой масштаб у каждой строки матрицы весов (выходного канала). Точнее, чем один масштаб на весь тензор, почти без накладных расходов. — свой масштаб у каждой строки (выходного канала);
- на группуквантование по группамEN group-wise quantizationСвой масштаб у каждых g соседних весов (обычно g = 32, 64 или 128). Плата — 16/g лишних бит на вес за FP16-масштаб. Основа INT4-форматов и GGUF. — свой масштаб у каждых соседних весов строки, обычно , 64 или 128.
Это та же идея, что поблочное FP8 в DeepSeek-V3 (мелкозернистое (поблочное) квантованиемелкозернистое (поблочное) квантованиеEN fine-grained quantizationСвой масштаб у каждой маленькой группы чисел, а не у всего тензора. В DeepSeek-V3: тайлы 1×128 для активаций и блоки 128×128 для весов. Выброс портит только свою группу., урок 5.4.1). Выброс портит только свою группу.
Цифры из задания этого урока: матрица из нормального распределения ; для столбца «С выбросом» один её вес заменён на 1.0. Относительная ошибка восстановления (здесь — норма Фробениусанорма ФробениусаEN Frobenius normКорень из суммы квадратов всех элементов матрицы — «L2-норма матрицы, вытянутой в вектор». Равна √(Σ σᵢ²).: корень из суммы квадратов всех элементов матрицы; ошибка 1% значит, что «длина» матрицы ошибок — сотая доля «длины» самой матрицы весов):
| Схема | Бит на вес | Без выброса | С выбросом |
|---|---|---|---|
| INT8, на тензор | 8 | 1.03% | 11.03% |
| INT8, на канал | 8.03 | 0.75% | 1.53% |
| INT8, группы по 32 | 8.5 | 0.54% | 0.62% |
| INT4, на тензор | 4 | 18.60% | 96.31% |
| INT4, на канал | 4.03 | 13.54% | 18.07% |
| INT4, группы по 128 | 4.125 | 11.85% | 13.08% |
| INT4, группы по 32 | 4.5 | 9.82% | 10.00% |
В строке «INT4 на тензор с выбросом» ошибка 96%: почти все веса стали нулями. Группы по 32 убирают проблему полностью, а платим за это бита на вес за FP16-масштаб каждой группы:
Llama 3 70B (70.6 млрд параметров) в INT4 с группами по 128 займёт ГБ, с группами по 32 — 39.7 ГБ.
Выбросы в активациях: что квантовать
Заголовок раздела «Выбросы в активациях: что квантовать»С весами всё неплохо: они распределены почти нормально. С активациями сложнее. Тим Деттмерс с соавторами (LLM.int8(), 2022) обнаружили, что начиная примерно с 6.7 млрд параметров в активациях появляются систематические выбросы: несколько каналов скрытого состояния во всех слоях и на большинстве токенов во много раз больше остальных. Один масштаб на токен «сплющивает» все остальные каналы. Их решение — разложение: каналы-выбросы считаются в FP16, остальное — в INT8.
Позже SmoothQuant (MIT HAN Lab, 2022) предложил переносить «трудность» из активаций в веса: умножить столбцы весов на поканальные масштабы и разделить на них активации. Результат умножения не меняется, а выбросы сглаживаются. Так появилась рабочая схема W8A8: и веса, и активации в 8 битах.
На практике сложились три схемы:
| Схема | Что в низкой точности | Что ускоряет | Где встречается |
|---|---|---|---|
| W4A16 | только веса (4 бита) | decode при малом батче | llama.cpp и GGUF, AWQ и GPTQ в vLLM, локальный запуск |
| W8A8 (INT8 или FP8) | веса и активации | и prefill, и decode, большие батчи | серверы; DeepSeek-V3 публикует веса в FP8 |
| KV-кэш в 8 или 4 битах | ключи и значения | длинный контекст, больше пользователей | vLLM, SGLang, LMDeploy, Ollama |
Почему квантование только весовквантование только весов (W4A16)EN weight-only quantizationВеса хранятся в 4 или 8 битах, активации остаются в 16 битах; веса восстанавливаются «на лету» внутри ядра умножения. Ускоряет decode, который упирается в чтение весов. ускоряет decode, но не prefill? Ядро умножения читает 4-битные веса, распаковывает их прямо в регистрах GPU (самой быстрой и самой маленькой памяти внутри вычислительных блоков) в 16 бит и умножает как обычно. Decode упирается в чтение весов — оно стало почти вчетверо короче. Prefill упирается в вычисления, а они остались 16-битными, да ещё и с распаковкой сверху. Чтобы ускорить prefill, нужно, чтобы считало само железо в 8 битах — это W8A8 и FP8.
PTQ: как округлять умнее
Заголовок раздела «PTQ: как округлять умнее»Всё, что выше, — квантование после обученияквантование после обучения (PTQ)EN post-training quantization (PTQ)Готовую модель квантуют без обучения, иногда с помощью небольшого калибровочного набора. RTN, GPTQ, AWQ, k-кванты llama.cpp — всё это PTQ. (post-training quantization, PTQ): берём готовую модель и округляем. Простейший вариант — RTN (round-to-nearest): каждое число — к ближайшему узлу. Но цель на самом деле другая: важна не ошибка весов, а ошибка выхода слоя на реальных входах:
где — активации, собранные на нескольких сотнях текстов (калибровочные данныекалибровочные данныеEN calibration dataНебольшой набор текстов (обычно сотня-другая примеров), на которых собирают статистику активаций для GPTQ, AWQ или imatrix. Для русскоязычного применения берите русские тексты.). Два самых популярных метода решают эту задачу по-разному.
GPTQ: ошибку одного столбца исправляют другие
Заголовок раздела «GPTQ: ошибку одного столбца исправляют другие»GPTQGPTQEN GPTQМетод PTQ (2022) — квантует столбцы весов по одному и переносит ошибку округления на ещё не квантованные столбцы, используя статистику входов H = 2XᵀX. (Франтар и соавторы, IST Austria и ETH Zürich, 2022) квантует столбцы весов (входные каналы) по одному. Округлили столбец — получили ошибку. Если вход коррелирует с ещё не квантованными входами , то ошибку можно частично компенсировать, слегка подвинув веса столбцов . Насколько двигать, подсказывает матрица — она описывает, как входы связаны между собой (это матрица вторых производных ошибки выхода по весам; для понимания идеи достаточно смысла «насколько входы похожи»). Идея пришла из классических методов прореживания сетей — удаления части весов (pruning), например Optimal Brain Surgeon.
Метод делает один проход без обучения, квантует модель на 175B параметров примерно за четыре GPU-часа и держит качество
в 3–4 битах намного лучше RTN. GPTQ-версии моделей (например, Qwen2.5-7B-Instruct-GPTQ-Int4) запускаются в vLLM и SGLang.
AWQ: защитить важный 1% весов
Заголовок раздела «AWQ: защитить важный 1% весов»AWQAWQ (квантование с учётом активаций)EN activation-aware weight quantization (AWQ)Метод PTQ (MIT HAN Lab, 2023) — около 1% весов, которые умножаются на большие активации, защищают поканальным масштабированием перед округлением. (MIT HAN Lab, 2023, лучшая статья MLSys 2024) начинается с наблюдения: важны не все веса одинаково. Если оставить в FP16 всего около 1% весов, качество 4-битной модели почти восстанавливается. Но выбирать их надо по активациям, а не по самим весам: важен вес, который умножается на большой вход.
Держать 1% весов в другом формате неудобно для железа. Поэтому AWQ делает трюк: умножает столбец важного входного канала на , а вход делит на . Произведение не меняется, но вес стал крупнее относительно шага сетки, и его относительная ошибка округления уменьшилась примерно в раз. Масштаб считают как , а подбирают по сетке на калибровочных данных. Посмотрим, зачем нужен подбор:
# AWQ на пальцах: защищаем веса, которые умножаются на большие активацииimport numpy as np
rng = np.random.default_rng(0)d_in, d_out, G = 512, 256, 128W = rng.normal(0, 0.02, size=(d_out, d_in)) # веса слояX = rng.normal(0, 1, size=(1024, d_in)) # «калибровочные» активацииsalient = rng.choice(d_in, size=5, replace=False) # 1% входных каналов с большими активациямиX[:, salient] *= 20
def quant_groups(w, bits=4, g=G): """Симметричное INT4 по группам из g соседних входных каналов (как в задании урока).""" qmax = 2 ** (bits - 1) - 1 wg = w.reshape(w.shape[0], -1, g) # [строки, число групп, g]: каждая строка нарезана на группы scale = np.abs(wg).max(-1, keepdims=True) / qmax # свой масштаб у каждой группы return (np.clip(np.round(wg / scale), -qmax, qmax) * scale).reshape(w.shape)
def out_err(W_hat): """Ошибка на выходе слоя — то, что на самом деле важно.""" return np.linalg.norm(X @ W.T - X @ W_hat.T) / np.linalg.norm(X @ W.T)
print(f"RTN (просто округлить): {out_err(quant_groups(W)):.4f}")a = np.abs(X).mean(0) # важность канала = средний |x|for alpha in [0.25, 0.5, 0.75, 1.0]: s = a ** alpha # масштаб канала # W * s: s — вектор длины d_in, broadcasting умножает столбец j на s_j (это и есть W·diag(s)); # после квантования делим обратно — в реальном AWQ деление переносят на активации W_hat = quant_groups(W * s) / s print(f"AWQ-масштабы, α = {alpha:4.2f}: {out_err(W_hat):.4f}")RTN (просто округлить): 0.1172AWQ-масштабы, α = 0.25: 0.0773AWQ-масштабы, α = 0.50: 0.0979AWQ-масштабы, α = 0.75: 0.1764AWQ-масштабы, α = 1.00: 0.2824При ошибка выхода на треть меньше, чем у RTN. А при — в 2.4 раза больше: раздутый важный вес становится максимумом своей группы, шаг сетки растёт, и остальные веса группы квантуются грубее. Отсюда поиск по сетке. Упрощённые AWQ и GPTQ вы реализуете сами на уровне Boss домашней работы модуля.
Форматы: GGUF и k-кванты
Заголовок раздела «Форматы: GGUF и k-кванты»Для локального запуска стандарт де-факто — GGUFGGUF (формат файлов llama.cpp)EN GGUFОдин файл с весами (обычно квантованными), токенизатором, шаблоном чата и метаданными. Типы квантования — Q8_0, Q6_K, Q4_K_M и др. Используют llama.cpp, Ollama, LM Studio.: один файл, в котором лежат квантованные веса, токенизатор, chat-шаблон и метаданные. Его читают llama.cpp, Ollama и LM Studio. Названия типов выглядят загадочно, но устроены просто:
| Тип | Как устроен | ≈ бит на вес |
|---|---|---|
| Q8_0 | блоки по 32 веса, один FP16-масштаб | 8.5 |
| Q4_0 | блоки по 32 веса, один FP16-масштаб | 4.5 |
| Q4_K | суперблок 256 весов = 8 блоков по 32; у блоков 6-битные масштабы и минимумы | 4.5 |
| Q5_K / Q6_K | похожие схемы для 5 и 6 бит | 5.5 / 6.56 |
| Q4_K_M | смесь: в основном Q4_K, но часть самых чувствительных матриц (V во внимании, выход FFN) — в Q6_K | ≈4.8–4.9 |
| Q3_K_M, Q2_K, IQ-типы | 3- и 2-битные k-кванты; IQ-типы (от IQ1 до IQ4) особенно нуждаются в матрице важности (imatrix) | ≈1.5–4 |
Буква K — «k-кванты» с двухуровневыми масштабами: масштабы маленьких блоков сами квантованы и хранятся относительно
масштаба суперблока. Суффиксы S, M, L говорят, сколько тензоров получили повышенную точность. imatrix — матрица
важности, собранная на калибровочном тексте, — та же идея, что в AWQ: точнее квантовать то, что сильнее влияет на выход.
Кроме GGUF вы встретите AWQ- и GPTQ-чекпоинты в формате safetensors (стандартный формат файлов весов на Hugging Face; для vLLM и SGLang), FP8-версии моделей и NF4 из bitsandbytes — его мы разбирали в уроке о QLoRA.
flowchart LR HF["Веса на Hugging Face<br/>(BF16, safetensors)"] --> C["convert_hf_to_gguf.py<br/>→ F16 GGUF"] C --> Q["llama-quantize<br/>Q8_0 / Q4_K_M / …"] CAL["Калибровочный текст<br/>(русский!)"] --> IM["llama-imatrix"] IM -.->|"--imatrix"| Q Q --> RUN["llama.cpp, Ollama,<br/>LM Studio"] HF --> AW["AWQ / GPTQ<br/>(калибровка)"] --> V["vLLM, SGLang,<br/>LMDeploy"]
QAT: обучение с учётом квантования
Заголовок раздела «QAT: обучение с учётом квантования»Ниже 4 бит PTQ упирается в стену: сколько ни подбирай масштабы, у сетки слишком мало узлов. Выход — дать модели привыкнуть к округлению. В обучении с учётом квантованияобучение с учётом квантования (QAT)EN quantization-aware training (QAT)В прямом проходе модель видит уже округлённые (например, 4-битные) веса и учится быть к этому устойчивой. Так получены INT4-веса Kimi K2 Thinking. (QAT) в прямом проходе используются уже округлённые веса, и модель учится работать именно с ними.
Есть проблема: у округления нулевая производная почти везде, и градиент не пройдёт. Используют «прямой» оценщик градиента«прямой» оценщик градиента (STE)EN straight-through estimator (STE)Приём QAT — в прямом проходе веса округляются, а в обратном округление считается тождественной функцией, и градиент проходит «насквозь». (STE): в обратном проходе делаем вид, что округления не было. В PyTorch это одна строка:
где — stop-gradient (.detach()). Вперёд проходит , а градиент течёт в как через тождество.
Проверим на маленькой сети, которая различает три «спирали» на плоскости:
# PTQ против QAT: маленькая сеть; в 2 битах у весов всего три значения — −1, 0, +1 (× масштаб строки)import torchimport torch.nn as nnimport torch.nn.functional as F
torch.manual_seed(0)torch.set_num_threads(1) # сеть крошечная: один поток быстрее# Данные: 3 класса-«спирали» на плоскостиn = 600t = torch.linspace(0, 1, n // 3)X, y = [], []for c in range(3): r = t * 2 ang = t * 4 + c * 2.1 + torch.randn(n // 3) * 0.15 X.append(torch.stack([r * torch.sin(ang), r * torch.cos(ang)], 1)) y.append(torch.full((n // 3,), c))X, y = torch.cat(X), torch.cat(y)
def fake_quant(w, bits=2): """Квантуем и сразу восстанавливаем: absmax по строкам. Градиент — «прямо насквозь» (STE).""" qmax = 2 ** (bits - 1) - 1 scale = w.abs().amax(dim=1, keepdim=True) / qmax w_q = torch.clamp(torch.round(w / scale), -qmax, qmax) * scale return w + (w_q - w).detach() # вперёд — w_q, назад — как будто тождество
class MLP(nn.Module): def __init__(self, quant_bits=None): super().__init__() self.l1, self.l2, self.l3 = nn.Linear(2, 64), nn.Linear(64, 64), nn.Linear(64, 3) self.quant_bits = quant_bits # None — обычные веса, иначе fake-quant в прямом проходе
def forward(self, x): for i, layer in enumerate([self.l1, self.l2, self.l3]): w = fake_quant(layer.weight, self.quant_bits) if self.quant_bits else layer.weight x = F.linear(x, w, layer.bias) # то же, что x @ w.T + bias, но с нашими (квантованными) весами if i < 2: x = F.relu(x) return x
def train(model, steps, lr=0.01): opt = torch.optim.Adam(model.parameters(), lr=lr) for _ in range(steps): loss = F.cross_entropy(model(X), y) opt.zero_grad() loss.backward() opt.step()
def acc(model): with torch.no_grad(): return (model(X).argmax(1) == y).float().mean().item()
model = MLP()train(model, 1500)print(f"{'FP32':<26} точность {acc(model):.1%}")for bits in (8, 4, 3, 2): # PTQ: просто округлили обученные веса model.quant_bits = bits print(f"{f'PTQ, {bits}-битные веса':<26} точность {acc(model):.1%}")train(model, 300, lr=0.003) # QAT: дообучаем, видя округлённые 2-битные весаprint(f"{'QAT, 2 бита (+300 шагов)':<26} точность {acc(model):.1%}")FP32 точность 99.7%PTQ, 8-битные веса точность 99.7%PTQ, 4-битные веса точность 98.5%PTQ, 3-битные веса точность 46.0%PTQ, 2-битные веса точность 28.3%QAT, 2 бита (+300 шагов) точность 99.2%Картина типичная: 8 и 4 бита почти без потерь, на 3 битах обрыв, на 2 битах — уровень случайного угадывания (33%). Но 300 шагов QAT возвращают 2-битной сети почти всю точность. У этой сети веса троичные — , умноженные на масштаб строки. Такие же троичные веса у BitNet b1.58 (Microsoft Research Asia, Пекин, arXiv:2402.17764), только её обучают в троичных весах с нуля.
QAT дороже PTQ — нужно обучение, но для больших лабораторий это копейки по сравнению с претрейном. Поэтому в 2025–2026 годах точность весов стала частью архитектуры, которую выбирают при обучении (см. «Китайский угол»).
Качество против памяти
Заголовок раздела «Качество против памяти»| Точность весов | Что происходит с качеством | Когда брать |
|---|---|---|
| 16 бит (BF16) | эталон | если память позволяет |
| 8 бит (Q8_0, INT8, FP8) | практически без потерь | сервер, качество важнее всего |
| 5–6 бит (Q5_K_M, Q6_K) | почти неотличимо от 16 бит | локально, если влезает |
| 4 бита (Q4_K_M, AWQ, GPTQ) | небольшая потеря, лучший компромисс | стандарт локального запуска |
| 3 бита | заметное падение | огромные модели, которые иначе не влезают |
| 2 бита и ниже | сильное падение без QAT | только с QAT или для очень больших MoE |
Важный вывод из работы Деттмерса и Зеттлмойера «The case for 4-bit precision» (arXiv:2212.09720): при фиксированном объёме памяти 4 бита почти всегда оптимальны. Большая модель в 4 битах обычно лучше маленькой в 16: Qwen3-32B в Q4_K_M (≈20 ГБ), как правило, сильнее Qwen3-8B в BF16 (16 ГБ). Чувствительнее всего к квантованию математика, код и длинные рассуждения, поэтому проверяйте модель на своей задаче.
Практика
Заголовок раздела «Практика»Реализуйте симметричное и асимметричное квантование, квантование по группам, замер ошибки и подсчёт бит на вес. Кнопка «Запустить» выведет сравнение «на тензор против групп» для матрицы с выбросом.
import numpy as np
def quantize_symmetric(x, bits=8):
"""Симметричное квантование (absmax) с одним масштабом на весь массив.
qmax = 2**(bits - 1) - 1 (127 для INT8, 7 для INT4)
scale = max|x| / qmax (если все x равны 0 — scale = 1.0)
q = clip(round(x / scale), -qmax, qmax), тип np.int32
Возвращает (q, scale). Пример: [0.5, -1.0, 2.54, 0.02] в INT8 → scale = 0.02, q = [25, -50, 127, 1].
"""
# TODO: ваш код
...
def dequantize(q, scale):
"""Восстановление: x̂ = q · scale (scale может быть числом или массивом — работает broadcasting)."""
# TODO: ваш код
...
def quantize_asymmetric(x, bits=8):
"""Асимметричное квантование с нулевой точкой: сетка 0 … 2**bits − 1 натягивается на [min, max].
Диапазон расширяем так, чтобы он включал 0: lo = min(x.min(), 0), hi = max(x.max(), 0).
scale = (hi − lo) / (2**bits − 1)
zero_point = int(round(−lo / scale))
q = clip(round(x / scale) + zero_point, 0, 2**bits − 1), тип np.int32
Восстановление: x̂ = (q − zero_point) · scale.
Возвращает (q, scale, zero_point). Пример: [-1, 0, 0.5, 3] в 8 битах → zero_point = 64, q = [0, 64, 96, 255].
"""
# TODO: ваш код
...
def quantize_groups(w, bits=4, group_size=32):
"""Симметричное квантование по группам: свой масштаб у каждых group_size соседних чисел строки.
w — матрица (rows, cols), cols делится на group_size.
Возвращает (q той же формы, что w; scales формы (rows, cols // group_size)).
Подсказка: w.reshape(rows, cols // group_size, group_size) и max по последней оси.
"""
# TODO: ваш код
...
def dequantize_groups(q, scales, group_size=32):
"""Обратная операция к quantize_groups: каждую группу умножаем на её масштаб, форма как у q."""
# TODO: ваш код
...
def relative_error(ref, approx):
"""Относительная ошибка ‖ref − approx‖ / ‖ref‖ (норма Фробениуса, np.linalg.norm)."""
# TODO: ваш код
...
def bits_per_weight(bits, group_size=None, scale_bits=16, zero_bits=0):
"""Сколько бит в среднем уходит на один вес с учётом масштабов (и нулевых точек) групп.
Без групп (group_size=None) накладными расходами пренебрегаем: ответ — bits.
С группами: bits + (scale_bits + zero_bits) / group_size.
bits_per_weight(4, 32) == 4.5 (как формат Q4_0 в GGUF); bits_per_weight(4, 128) == 4.125
"""
# TODO: ваш код
...
def demo():
"""Матрица весов 64×512 с одним выбросом: per-tensor против групп."""
rng = np.random.default_rng(0)
w = rng.normal(0, 0.02, size=(64, 512))
w[3, 100] = 1.0 # выброс в 50 раз больше типичного веса
for bits in (8, 4):
q, s = quantize_symmetric(w, bits)
e_tensor = relative_error(w, dequantize(q, s))
qg, sg = quantize_groups(w, bits, 32)
e_group = relative_error(w, dequantize_groups(qg, sg, 32))
print(f"INT{bits}: на тензор {e_tensor:.3%}, группы по 32: {e_group:.3%}, "
f"бит на вес {bits_per_weight(bits, 32):.2f}")
try:
print(quantize_symmetric(np.array([0.5, -1.0, 2.54, 0.02]), 8))
print(quantize_asymmetric(np.array([-1.0, 0.0, 0.5, 3.0]), 8))
demo()
except TypeError as e: # функции пока возвращают None
print("Сначала реализуйте функции:", e)
Проверь себя
Заголовок раздела «Проверь себя»- Квантование заменяет числа целыми кодами и масштабом. Симметричная схема (absmax) — стандарт для весов, асимметричная с нулевой точкой — для несимметричных данных.
- Чем мельче гранулярность масштаба (тензор → канал → группа из 32–128), тем меньше ущерб от выбросов; плата — бит на вес.
- W4A16 ускоряет decode и уменьшает память весов в 3–4 раза (с учётом масштабов); W8A8 и FP8 ускоряют и вычисления; KV-кэш тоже квантуют.
- GPTQ компенсирует ошибку через соседние столбцы, AWQ защищает веса при больших активациях; оба используют калибровочные данные — берите их на своём языке. GGUF с k-квантами — стандарт локального запуска.
- QAT учит модель жить с округлением и открывает путь к 4 битам и ниже: Kimi K2 Thinking (INT4), DeepSeek-V4 (FP4, по обзорам техотчёта).
Дальше: запускаем Qwen и DeepSeek на своём компьютере — llama.cpp и Ollama, выбор размера модели под память, OpenAI-совместимый сервер и замер токенов в секунду.