27 / 37 · Адаптация и эффективные модели
Квантизация
Хранить веса (а иногда и активации) как 8- или 4-битные целые с шагом и нулевой точкой вместо 32-битных чисел с плавающей точкой. Заметка выводит аффинное отображение и целочисленное произведение матриц, измеряет память, точность и скорость классификатора на 67 млн параметров в трёх реализациях, объясняет квантизацию весов GPTQ по калибровочным данным и ставит рядом дистилляцию и прунинг.
Обучение на инструкциях и выравнивание · Архитектура трансформера · Линейная алгебра ↗
На этой странице 14
Обученная модель — набор матриц 32-битных чисел с плавающей точкой, и обслуживать её значит держать их в памяти и умножать на них для каждого токена. Модель на 7 млрд параметров занимает 28 ГБ в 32 битах и 14 ГБ в 16; при порождении затраты определяются чтением этих байтов, по разу на токен. Квантизация (quantization) хранит числа в меньшем числе бит. Эта заметка выводит, что это делает с произведением матриц, измеряет эффект на небольшой модели и объясняет, как метод масштабируется на большие языковые модели.
Обозначения. Вещественное значение в известном диапазоне ; его целочисленный код (для int8 — ); шаг и нулевая точка . Для линейного слоя с .
Отправная точка: 32-битные числа везде#
Каждый вес — float32: 4 байта, около 7 значащих цифр, диапазон . Ничто в обученной сети такой точности не использует; веса слоя обычно лежат в диапазоне нескольких единиц и неразличимы уже в третьем знаке после запятой. Половинная точность (float16 или bfloat16) вдвое сокращает память без видимых потерь и служит выбором по умолчанию для обслуживания. Ниже 16 бит эта заметка опускается с целочисленными кодами, а целым нужно отображение. Другой путь — 8-битные форматы с плавающей точкой: тензорные ядра FP8 появились в GPU NVIDIA Hopper (H100) в 2022 году, и то, какие форматы работают быстро, зависит от поколения оборудования. Здесь рассматриваются целочисленные аффинные коды (int8, int4) и кодовая книга NF4.
Аффинное отображение#
Шаг (scale) — вещественная ширина одного целочисленного шага; нулевая точка (zero point) — целое, которое представляет , выбранное так, чтобы и . Коды обрезаются до . Ошибка «туда и обратно» не больше для внутри диапазона и не ограничена вне его, поэтому выбор — всё искусство: слишком широкий диапазон тратит коды впустую, слишком узкий обрезает выбросы, которые важнее всего.
Разобранный пример#
Диапазон , int8. Тогда и .
| ошибка | ||||
|---|---|---|---|---|
| 0 | ||||
| 0 | ||||
| 0 |
Аффинная квантизация int8 диапазона [−2, 3]
Что происходит с произведением матриц#
Квантуем вход (шаг , нулевая точка ), вес () и смещение. Подставляя и в и раскрывая произведение двух разностей:
Всё внутри скобок — целочисленная арифметика: произведение матриц int8 × int8 с накоплением в int32, две суммы по строкам и столбцам и константа. Вещественные числа появляются только при финальном масштабировании на и в смещении. Выход можно снова квантовать в , ещё раз применив отображение. Именно это тождество делает квантизацию ускорением, а не только экономией памяти: когда у оборудования есть быстрые матричные блоки int8 (у современных GPU и CPU они есть), скобка вычисляется в несколько раз быстрее, чем произведение во float, а веса, читаемые из памяти, вчетверо меньше.
Квантованный линейный слой
Чтобы ускорение появилось, нужны две вещи: для оборудования должно существовать ядро, а диапазоны активаций должны быть известны. Динамическая квантизация измеряет диапазон во время работы, для каждого батча, а веса квантует заранее; калибровочных данных ей не нужно, а на каждом батче она платит небольшую цену. Статическая квантизация заранее фиксирует диапазоны активаций по калибровочной выборке из нескольких сотен входов, так что весь граф выполняется в целых числах; она быстрее и капризнее. Обучение с учётом квантизации (quantization-aware training, QAT) имитирует округление при дообучении, чтобы веса к нему приспособились; это возвращает точность при самых малых разрядностях ценой прогона обучения.
Измерения на небольшом классификаторе#
Модель — DistilBERT (66.96 млн параметров), дообученная на тональности IMDb две эпохи; тестовая выборка — 25 000 рецензий. Базовый уровень: 0.249 ГиБ во float32, доля правильных ответов 0.9313. Память учитывает параметры и буферы и приводится в ГиБ ( байт).
| Реализация | Что квантуется | Память, ГиБ | Доля верных | Время |
|---|---|---|---|---|
| float32 | ничего | 0.249 | 0.9313 | 24.5 с на батч из 64 (CPU); 2 мин 4 с на тестовую выборку (GPU) |
| Написанные вручную линейные слои int8 (формула выше) | веса и входы линейных слоёв | 0.129 | — | медленнее float (нет ядра для целых) |
Динамическая квантизация PyTorch (torch.ao) | линейные слои, int8 | 0.089, заниженное | — | 20.4 с на батч (CPU) |
bitsandbytes 8 бит (LLM.int8()) | линейные слои, int8 на GPU | 0.085 | 0.9266 | 3 мин 50 с на тестовую выборку (GPU) |
Как читать таблицу:
- Память вдвое меньше с написанными вручную слоями: таблицы эмбеддингов, 36% параметров, остаются во float32 и составляют две трети квантованной модели. С библиотечными числами нужна осторожность. Динамическая квантизация хранит свои веса int8 как упакованные объекты, которые подсчёт параметров и буферов не учитывает, поэтому её 0.089 — это по сути одни эмбеддинги во float; настоящий размер близок к 0.129 у ручной версии.
bitsandbytesзагружает всё, что не квантует, во float16, поэтому опускается ниже 0.129. Если квантовать и эмбеддинги, можно пойти дальше. - Доля правильных ответов падает на полпункта при 8 битах без всякого переобучения; это типично для квантизации int8 энкодеров после обучения.
- Скорость — то, в чём реализации различаются. Написанный вручную слой int8 в 24 раза медленнее float32 (1.23 мс против 51 мкс для входа на слое ), потому что у PyTorch на этом пути CPU нет оптимизированного умножения матриц int8, а суммы считаются обычными операциями int32; арифметика верна (средняя абсолютная ошибка 0.0011 относительно выхода во float), не хватает ядра. Встроенная динамическая квантизация на CPU на 17% быстрее float. На GPU, если сравнивать полные проходы по тестовой выборке на одном устройстве, 8-битная модель оказывается медленнее: 3 мин 50 с против 2 мин 4 с во float32. Для модели на 67 млн параметров, которую не ограничивает обмен с памятью, ядра int8 и обработка выбросов в LLM.int8() стоят больше, чем экономят. Квантизация окупается только с ядром, которое выигрывает на целевом оборудовании при данном размере модели; иначе это инструмент экономии памяти.
Путь torch.ao к тому же вообще не работает на GPU (динамическая операция линейного слоя int8 есть только для CPU), и это одна из причин, по которым для трансформеров используют экосистемы bitsandbytes, GPTQ и AWQ, а не собственную квантизацию фреймворка.
Квантизация только весов для больших языковых моделей#
Порождение большим декодером упирается в память: каждый токен требует прочитать все веса по разу и выполняет мало арифметики на байт. Рычаг — сократить байты, поэтому LLM обычно квантуют только веса: веса в 4 или 8 битах, активации остаются в 16 битах, а ядра деквантуют веса на лету внутри произведения матриц. Господствуют две схемы:
LLM.int8() (Dettmers et al., 2022) квантует веса и активации в int8 по строкам и столбцам, кроме нескольких столбцов активаций с выбросами (большими величинами, которые появляются в моделях больше нескольких миллиардов параметров): их держат в 16 битах и умножают отдельно. Без отдельного пути для выбросов точность в 8 битах обваливается начиная с 6 млрд параметров; с ним она совпадает с 16-битной.
NF4 (Dettmers et al., 2023) — 4-битный формат, 16 уровней которого расставлены по квантилям стандартного нормального распределения, а не равномерно, потому что обученные веса распределены примерно нормально, и равномерные уровни тратят коды на хвосты. Вместе с двойной квантизацией (квантованием самих шагов блоков до 8 бит) он хранит модель на 7 млрд примерно в 3.5 ГБ и лежит в основе QLoRA в заметке о PEFT.
GPTQ: квантизация весов по калибровочным данным#
Округление каждого веса до ближайшего уровня не учитывает, что веса слоя работают вместе: ошибку округления одного веса можно скомпенсировать, подправив остальные. GPTQ (Frantar et al., 2023) делает такую компенсацию слой за слоем.
В этом разделе принята ориентация статьи. Слой вычисляет , где — транспонированная из формул выше, а хранит калибровочных векторов токенов как столбцы. Строка матрицы — веса одного выхода, а — вес , округлённый до ближайшего уровня.
- Прогнать через модель небольшую калибровочную выборку (обычно рекомендуют 128 последовательностей) и собрать . Изменение строки на меняет этот выход на , поэтому квадрат ошибки выхода равен с гессианом , одним и тем же для всех строк.
- Квантовать входные координаты (столбцы ) по одной, сразу во всех строках, потому что у них общий. Пусть — множество ещё не квантованных координат, а — гессиан, ограниченный на них. Округление координаты переводит её из в , и поправка координат из , минимизирующая ошибку выхода когда эта координата закреплена, — это шаг Optimal Brain Surgeon (Hassibi and Stork, 1993; Frantar et al., 2023, раздел 3, формула 2): Её -я компонента равна , что ставит координату точно на её уровень; после этого покидает .
- Проходить координаты в фиксированном порядке, с ленивым пакетным обновлением и небольшим демпфированием для численной устойчивости.
Разобранный пример (игрушечные числа). Два сильно коррелированных входа с , строка и целые уровни. Если округлить каждый вес отдельно, получится , ошибка и . GPTQ округляет до . Первый столбец обратного гессиана, делённый на свой первый элемент, равен , поэтому и сдвигается в , что округляется до . Результат даёт ошибку и — меньше трети ошибки отдельного округления. Если перевернуть знак поправки, сдвинулся бы в и снова округлился бы до .
Получается 4-битная (или 3-битная) модель, выходы слоёв которой как можно ближе — в смысле наименьших квадратов на калибровочных данных — к выходам исходной. Шаги хранятся на группу из 128 весов вдоль строки (group_size=128) — компромисс между одним шагом на тензор (грубо) и шагом на каждый вес (никакой экономии). Квантованные веса упаковываются по восемь 4-битных кодов в int32 (qweight), нулевые точки упаковываются так же (qzeros), а читают их специализированные ядра (ExLlama).
Калибровочные данные важны. Модель на 125 млн параметров (OPT-125M), квантованная до 4 бит с несколькими сотнями последовательностей WikiText-2 в калибровочной выборке, порождает гладкий текст («Hello my name is Alex and I’m a 25 year old male…»); та же модель, откалиброванная на одном предложении, порождает My name is "M"""""""". Одно предложение из токенов даёт векторов токенов, так что ранг его не больше — намного меньше входных размерностей слоёв (768 или 3072 в OPT-125M); он вырожден и обратим только благодаря демпфированию. Правдоподобное объяснение — компенсация опирается тогда на несколько плохо оценённых направлений и сдвигает оставшиеся веса туда, куда настоящие входы не ведут. Спектр в этом прогоне не измерен, поэтому это гипотеза, как и общий совет, что калибровочных данных должно быть достаточно и они должны быть разнообразны. Заранее квантованная модель чата на 7 млрд из Hub загружается в 4-битные слои и порождает нормально.
Дистилляция и прунинг#
Квантизация сохраняет архитектуру и уменьшает числа. Два других пути сжатия меняют архитектуру:
Дистилляция знаний (knowledge distillation; Hinton, Vinyals and Dean, 2015) обучает меньшего ученика повторять выходное распределение большего учителя, а не только жёсткие метки:
где — логиты учителя и ученика, а температура смягчает оба распределения, так что ученик узнаёт и то, какие неверные классы учитель считает близкими. Множитель сохраняет величину градиента сравнимой при разных температурах. Сам DistilBERT, измеренный выше, — это BERT-base, дистиллированный до 6 слоёв, с параметрами на 40% меньше и 97% результата на GLUE (Sanh et al., 2019). Для языковых моделей дистилляция принимает и форму обучения на тексте, порождённом большой моделью (путь Alpaca из заметки об обучении на инструкциях).
Прунинг (pruning) удаляет веса (неструктурированный) или целые головы, нейроны или слои (структурированный). Неструктурированная разреженность экономит память только с разреженными ядрами; структурированный прунинг даёт меньшую плотную модель и требует дообучения для восстановления.
Компромиссы#
| Бит | Память относительно fp16 | Типичное изменение качества | Что нужно |
|---|---|---|---|
| 16 | 1× | нет | ничего |
| 8 (веса + активации) | ½ | небольшое (здесь < 1 пункта) | ядро int8; обработка выбросов начиная с ~6 млрд |
| 4 (только веса, GPTQ / NF4 / AWQ) | ¼ | от небольшого до умеренного; растёт для малых моделей | калибровочные данные (GPTQ) или ничего (NF4); деквантующее ядро |
| 3 и меньше | < ¼ | заметное; зависит от задачи | тщательная калибровка, часто QAT |
Пропускная способность следует за памятью только при наличии ядра, а результаты точности на одном бенчмарке не переносятся на другой: модель, квантованная с перплексией в роли критерия, может потерять на рассуждениях или длинном порождении больше, чем подсказывает перплексия. Любое конкретное число для современной модели — снимок; сохранить стоит механизм и форму компромисса.
Что изменилось#
| Модель во float | Квантованная модель | |
|---|---|---|
| Хранение веса | 32 или 16 бит | 8 или 4 бита + шаг на группу |
| Произведение матриц | во float | целочисленная скобка + одно масштабирование во float |
| Точность | эталон | небольшая потеря при 8 битах, больше при 4 |
| Скорость | эталон | быстрее только с аппаратным ядром |
| Обучение | обычное | заморожено (PTQ) или с имитацией округления (QAT); LoRA поверх (QLoRA) |
Ограничения#
- Диапазон и выбросы. Несколько больших активаций могут навязать широкий диапазон, на котором большинство кодов пропадает впустую; стандартная заплатка — смешанная точность для столбцов с выбросами.
- Ядра. Экономия памяти универсальна, ускорение — нет. Без подходящего ядра квантованные слои могут оказаться медленнее float.
- Малые модели страдают больше. У меньшего числа параметров меньше избыточности, чтобы поглотить округление; 4 бита — обычное дело при 7 млрд и риск при 100 млн.
- Дрейф бенчмарков. Квантизацию оценивают по одной метрике; потери в другом месте легко пропустить.
- Необратимость. Исходная точность потеряна; контрольную точку во float нужно сохранить.
Что нужно запомнить#
- Аффинное отображение: , , где ; ошибка внутри диапазона, обрезка вне его.
- Квантованный линейный слой — целочисленное произведение матриц плюс суммы по строкам и столбцам, один раз масштабированное на .
- DistilBERT на IMDb: 0.249 → 0.085 ГиБ и 0.9313 → 0.9266 при 8 битах; на GPU 8-битная модель оказалась медленнее float32, потому что скорость целиком зависит от ядра и размера модели.
- LLM квантуют только по весам (int8 со столбцами выбросов, NF4, 4-битный GPTQ с группами по 128); GPTQ компенсирует ошибки округления по одной входной координате с помощью гессиана по калибровочным данным, и в этом прогоне калибровочная выборка из одного предложения испортила выход.
- Дистилляция обучает малую модель на мягких выходах большой; прунинг удаляет параметры; оба меняют архитектуру, квантизация — нет.
Ноутбук#
Два ноутбука. Номера ячеек считают все ячейки, включая текстовые, начиная с 1. Приведённые числа взяты из прогона, сохранённого в курсе; у ячеек, изменённых с тех пор под текущие версии библиотек, сохранённого выхода нет.
week12_pt0_Torch_and_ONNX_Quantization_+_PEFT.ipynb:
- Ячейка 4 приводит функцию потерь дистилляции; ячейки 6–18 загружают IMDb и дообученный на нём DistilBERT (теперь публичную
lvwerra/distilbert-imdb, поэтому текущие доли правильных ответов немного отличаются; числа здесь — с собственной контрольной точки курса, обученной две эпохи), строят батчи по 64, печатают 66 955 010 параметров, 0.249 ГиБ (память — этоget_memory_footprint(), делённое на ) и долю правильных ответов 0.9313 (2 мин 4 с на тестовую выборку на GPU). - Ячейка 19 выводит приведённое выше целочисленное произведение матриц; ячейки 20–22 реализуют
QuantizedTensor(диапазон min–max на тензор с запасом ) иQuantizedLinear; ячейки 24–29 замеряют слой (51 мкс во float, 1.23 мс в квантованном виде) и среднюю абсолютную ошибку 0.0011; ячейки 31–32 заменяют каждый линейный слой и печатают 0.129 ГиБ. - Ячейки 36–44 применяют
torch.ao.quantization.quantize_dynamic(0.089 ГиБ по подсчётуget_memory_footprint(), который пропускает упакованные веса int8; 20.4 с против 24.5 с на батч на CPU); ячейка 46 показывает отказ динамической операции int8 на GPU (он проявляется только при наличии устройства CUDA). - Ячейки 50–57 загружают модель с
BitsAndBytesConfig(load_in_8bit=True): 0.085 ГиБ, доля правильных ответов 0.9266, 3 мин 50 с на тестовую выборку на GPU; текстовая ячейка после этого заявляет ускорение в 1.5 раза, сравнивая один батч на GPU (3.6 с) с одним батчем на CPU; сравнение на одном устройстве дают две полные оценки на тестовой выборке на GPU (3 мин 50 с против 2 мин 4 с). - Ячейки 59–69 выполняют статическую квантизацию через ONNX Runtime: экспорт в ONNX, диапазоны min–max по 200 дополненным калибровочным примерам с LayerNorm, GELU, остаточными сложениями и softmax во float, доля правильных ответов квантованной модели и для сравнения динамический int8 через тот же конвейер. Текстовая ячейка 68 сообщает около 0.80 у статического int8 против 0.91 у модели во float на том же подмножестве рецензий — цена простой калибровки min–max; в версии курса эти ячейки были помечены как неработающие и не имели выходов.
- Ячейки 71–76 — шаблон QLoRA, описанный в заметке о PEFT.
week12_pt1_Distillation_and_GPTQ_Quantization.ipynb:
- Ячейки 19–23 квантуют
facebook/opt-125mдо 4 бит сGPTQConfig(bits=4, group_size=128, dataset="wikitext2", desc_act=False), смотрят на упакованные буферы int32qweightиqzerosи порождают приведённое выше гладкое предложение. - Ячейки 26–28 повторяют то же с калибровочной выборкой из одного предложения и порождают вырожденный выход.
- Ячейки 32–38 загружают заранее квантованную модель чата Llama-2-7B, печатают слои
QuantLinearи конфигурацию квантизации (4 бита, группа 128, ядро ExLlama) и порождают 50 токенов. - Ячейки 41–51 дообучают её с LoRA, как описано в заметке о PEFT.
Дальше#
Курс уже построил, предобучил, адаптировал и сжал модели. Следующий раздел обращается к задачам, для которых эти модели используются, и для каждой спрашивает, каковы вход и выход, какой метрикой задача измеряется, каким был классический базовый уровень и что изменил подход эпохи трансформеров. Открывает раздел заметка о классификации текстов — с задачей, которая проходит через весь курс, — и собирает все результаты на AG News в одну таблицу.
Литература#
- B. Jacob et al. Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. CVPR, 2018. Аффинная схема и целочисленное произведение матриц.
- T. Dettmers, M. Lewis, Y. Belkada, L. Zettlemoyer. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. NeurIPS, 2022.
- T. Dettmers, A. Pagnoni, A. Holtzman, L. Zettlemoyer. QLoRA: Efficient Finetuning of Quantized LLMs. NeurIPS, 2023. NF4 и двойная квантизация.
- E. Frantar, S. Ashkboos, T. Hoefler, D. Alistarh. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. ICLR, 2023.
- B. Hassibi, D. G. Stork. Second order derivatives for network pruning: Optimal Brain Surgeon. NeurIPS, 1993.
- J. Lin et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. MLSys, 2024.
- G. Hinton, O. Vinyals, J. Dean. Distilling the Knowledge in a Neural Network. NeurIPS Deep Learning Workshop, 2015.
- V. Sanh, L. Debut, J. Chaumond, T. Wolf. DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter. arXiv, 2019.
- Hugging Face. Quantization overview. Какой бэкенд поддерживает какое устройство и разрядность.