Содержание курса 27 / 37

27 / 37 · Адаптация и эффективные модели

Квантизация

Хранить веса (а иногда и активации) как 8- или 4-битные целые с шагом и нулевой точкой вместо 32-битных чисел с плавающей точкой. Заметка выводит аффинное отображение и целочисленное произведение матриц, измеряет память, точность и скорость классификатора на 67 млн параметров в трёх реализациях, объясняет квантизацию весов GPTQ по калибровочным данным и ставит рядом дистилляцию и прунинг.

На этой странице 14

Обученная модель — набор матриц 32-битных чисел с плавающей точкой, и обслуживать её значит держать их в памяти и умножать на них для каждого токена. Модель на 7 млрд параметров занимает 28 ГБ в 32 битах и 14 ГБ в 16; при порождении затраты определяются чтением этих байтов, по разу на токен. Квантизация (quantization) хранит числа в меньшем числе бит. Эта заметка выводит, что это делает с произведением матриц, измеряет эффект на небольшой модели и объясняет, как метод масштабируется на большие языковые модели.

Обозначения. Вещественное значение xx в известном диапазоне [a,b][a, b]; его целочисленный код xq∈[aq,bq]x_q \in [a_q, b_q] (для int8 — [−128,127][-128, 127]); шаг ss и нулевая точка zz. Для линейного слоя Y=XW+bY = XW + b с X∈Rn×dX \in \mathbb R^{n \times d}.

Отправная точка: 32-битные числа везде#

Каждый вес — float32: 4 байта, около 7 значащих цифр, диапазон 10±3810^{\pm 38}. Ничто в обученной сети такой точности не использует; веса слоя обычно лежат в диапазоне нескольких единиц и неразличимы уже в третьем знаке после запятой. Половинная точность (float16 или bfloat16) вдвое сокращает память без видимых потерь и служит выбором по умолчанию для обслуживания. Ниже 16 бит эта заметка опускается с целочисленными кодами, а целым нужно отображение. Другой путь — 8-битные форматы с плавающей точкой: тензорные ядра FP8 появились в GPU NVIDIA Hopper (H100) в 2022 году, и то, какие форматы работают быстро, зависит от поколения оборудования. Здесь рассматриваются целочисленные аффинные коды (int8, int4) и кодовая книга NF4.

Аффинное отображение#

xq=round⁡ ⁣(xs+z),x≈s (xq−z),s=b−abq−aq,z=round⁡ ⁣(b aq−a bqb−a).x_q = \operatorname{round}\!\Big(\frac{x}{s} + z\Big), \qquad x \approx s\,(x_q - z), \qquad s = \frac{b - a}{b_q - a_q}, \quad z = \operatorname{round}\!\Big(\frac{b\,a_q - a\,b_q}{b - a}\Big).

Шаг (scale) — вещественная ширина одного целочисленного шага; нулевая точка (zero point) — целое, которое представляет 0.00.0, выбранное так, чтобы a↦aqa \mapsto a_q и b↦bqb \mapsto b_q. Коды обрезаются до [aq,bq][a_q, b_q]. Ошибка «туда и обратно» ∣x−s(xq−z)∣|x - s(x_q - z)| не больше s/2s/2 для xx внутри диапазона и не ограничена вне его, поэтому выбор [a,b][a, b] — всё искусство: слишком широкий диапазон тратит коды впустую, слишком узкий обрезает выбросы, которые важнее всего.

Разобранный пример#

Диапазон [a,b]=[−2,3][a, b] = [-2, 3], int8. Тогда s=5/255=0.0196s = 5/255 = 0.0196 и z=round⁡((3⋅(−128)−(−2)⋅127)/5)=round⁡(−26.0)=−26z = \operatorname{round}((3 \cdot (-128) - (-2) \cdot 127)/5) = \operatorname{round}(-26.0) = -26.

xxx/s+zx/s + zxqx_qs(xq−z)s(x_q - z)ошибка
−2-2−128.0-128.0−128-128−2.000-2.0000
0.370.37−7.13-7.13−7-70.37250.37250.00250.0025
1125.025.025251.0001.0000
33127.0127.01271273.0003.0000
Рисунок

Аффинная квантизация int8 диапазона [−2, 3]

Вещественные значения, отображённые в уровни int8Две горизонтальные числовые оси. Верхняя охватывает вещественный диапазон от −2 до 3, нижняя — целые числа от −128 до 127. Стрелки соединяют выбранные вещественные значения с их целочисленными уровнями, а подписи дают округлённое целое и деквантованное значение.вещественные x ∈ [-2, 3]-2-10123код int8 ∈ [-128, 127], z = -26-128-64064127z ↔ 00.37-7 → 0.3725Вещественные значения, отображённые в уровни int8Две горизонтальные числовые оси. Верхняя охватывает вещественный диапазон от −2 до 3, нижняя — целые числа от −128 до 127. Стрелки соединяют выбранные вещественные значения с их целочисленными уровнями, а подписи дают округлённое целое и деквантованное значение.вещественные x ∈ [-2, 3]-2-10123код int8 ∈ [-128, 127], z = -26-128-64064127z ↔ 00.37-7 → 0.3725
При a = −2 и b = 3 шаг s = (b − a)/255 = 0.0196, а нулевая точка z = -26, так что x = s (x_q − z) переводит −128 в -2, а 127 — в 3. Каждое вещественное значение (верхняя линия) округляется до ближайшего из 256 целочисленных уровней (нижняя линия); после деквантования оно оказывается не дальше s/2 = 0.0098 от исходного. 0.37 становится -7 и возвращается как 0.3725. Значения вне [a, b] обрезаются до концов отрезка.

Что происходит с произведением матриц#

Квантуем вход XX (шаг sXs_X, нулевая точка zXz_X), вес WW (sW,zWs_W, z_W) и смещение. Подставляя X=sX(Xq−zX)X = s_X(X_q - z_X) и W=sW(Wq−zW)W = s_W(W_q - z_W) в Yij=bj+∑kXikWkjY_{ij} = b_j + \sum_k X_{ik} W_{kj} и раскрывая произведение двух разностей:

Yij=sb(bq,j−zb)+sXsW[∑kXq,ikWq,kj−zW∑kXq,ik−zX∑kWq,kj+d zXzW].Y_{ij} = s_b (b_{q,j} - z_b) + s_X s_W \Big[\sum_k X_{q,ik} W_{q,kj} - z_W \sum_k X_{q,ik} - z_X \sum_k W_{q,kj} + d\, z_X z_W\Big].

Всё внутри скобок — целочисленная арифметика: произведение матриц int8 × int8 с накоплением в int32, две суммы по строкам и столбцам и константа. Вещественные числа появляются только при финальном масштабировании на sXsWs_X s_W и в смещении. Выход можно снова квантовать в (sY,zY)(s_Y, z_Y), ещё раз применив отображение. Именно это тождество делает квантизацию ускорением, а не только экономией памяти: когда у оборудования есть быстрые матричные блоки int8 (у современных GPU и CPU они есть), скобка вычисляется в несколько раз быстрее, чем произведение во float, а веса, читаемые из памяти, вчетверо меньше.

Рисунок

Квантованный линейный слой

Целочисленное умножение матриц с одним масштабированиемВход X во float квантуется со своим шагом и нулевой точкой; заранее квантованные веса хранятся в int8. Целочисленное произведение с накоплением в int32, поправки на суммы строк и столбцов и константа, затем одно умножение на произведение двух шагов и прибавление смещения дают выход Y во float.Xвход, floatквантоватьшаг и нуль XW, int8квантована заранееint8 × int8накопление в int32суммы, константанулевые точки× два шага + bвыход Y, floatЦелочисленное умножение матриц с одним масштабированиемВход X во float квантуется со своим шагом и нулевой точкой; заранее квантованные веса хранятся в int8. Целочисленное произведение с накоплением в int32, поправки на суммы строк и столбцов и константа, затем одно умножение на произведение двух шагов и прибавление смещения дают выход Y во float.Xвход, floatквантоватьшаг и нуль XW, int8квантована заранееint8 × int8накопление в int32суммы, константанулевые точки× два шага + bвыход Y, float
Формула из текста как конвейер. Синие блоки — целочисленная арифметика: произведение int8 × int8 с накоплением в int32, поправки на суммы строк и столбцов, умноженные на нулевые точки, и постоянный член. Вещественные числа появляются только на концах: вход квантуется по своему диапазону, а результат один раз умножается на произведение двух шагов и к нему прибавляется смещение. Веса квантуются заранее и читаются из памяти вчетверо меньшими, чем во float32; ускорение даёт быстрое ядро int8 × int8 на целевом оборудовании.

Чтобы ускорение появилось, нужны две вещи: для оборудования должно существовать ядро, а диапазоны активаций должны быть известны. Динамическая квантизация измеряет диапазон XX во время работы, для каждого батча, а веса квантует заранее; калибровочных данных ей не нужно, а на каждом батче она платит небольшую цену. Статическая квантизация заранее фиксирует диапазоны активаций по калибровочной выборке из нескольких сотен входов, так что весь граф выполняется в целых числах; она быстрее и капризнее. Обучение с учётом квантизации (quantization-aware training, QAT) имитирует округление при дообучении, чтобы веса к нему приспособились; это возвращает точность при самых малых разрядностях ценой прогона обучения.

Измерения на небольшом классификаторе#

Модель — DistilBERT (66.96 млн параметров), дообученная на тональности IMDb две эпохи; тестовая выборка — 25 000 рецензий. Базовый уровень: 0.249 ГиБ во float32, доля правильных ответов 0.9313. Память учитывает параметры и буферы и приводится в ГиБ (2302^{30} байт).

РеализацияЧто квантуетсяПамять, ГиБДоля верныхВремя
float32ничего0.2490.931324.5 с на батч из 64 (CPU); 2 мин 4 с на тестовую выборку (GPU)
Написанные вручную линейные слои int8 (формула выше)веса и входы линейных слоёв0.129—медленнее float (нет ядра для целых)
Динамическая квантизация PyTorch (torch.ao)линейные слои, int80.089, заниженное—20.4 с на батч (CPU)
bitsandbytes 8 бит (LLM.int8())линейные слои, int8 на GPU0.0850.92663 мин 50 с на тестовую выборку (GPU)

Как читать таблицу:

  • Память вдвое меньше с написанными вручную слоями: таблицы эмбеддингов, 36% параметров, остаются во float32 и составляют две трети квантованной модели. С библиотечными числами нужна осторожность. Динамическая квантизация хранит свои веса int8 как упакованные объекты, которые подсчёт параметров и буферов не учитывает, поэтому её 0.089 — это по сути одни эмбеддинги во float; настоящий размер близок к 0.129 у ручной версии. bitsandbytes загружает всё, что не квантует, во float16, поэтому опускается ниже 0.129. Если квантовать и эмбеддинги, можно пойти дальше.
  • Доля правильных ответов падает на полпункта при 8 битах без всякого переобучения; это типично для квантизации int8 энкодеров после обучения.
  • Скорость — то, в чём реализации различаются. Написанный вручную слой int8 в 24 раза медленнее float32 (1.23 мс против 51 мкс для входа 256×128256 \times 128 на слое 128×128128 \times 128), потому что у 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) делает такую компенсацию слой за слоем.

В этом разделе принята ориентация статьи. Слой вычисляет WXcalW X_{\text{cal}}, где W∈Rdout×dinW \in \mathbb R^{d_{\text{out}} \times d_{\text{in}}} — транспонированная WW из формул выше, а Xcal=X⊤∈Rdin×NX_{\text{cal}} = X^\top \in \mathbb R^{d_{\text{in}} \times N} хранит NN калибровочных векторов токенов как столбцы. Строка w∈R1×dinw \in \mathbb R^{1 \times d_{\text{in}}} матрицы WW — веса одного выхода, а quant⁡(wq)\operatorname{quant}(w_q) — вес wqw_q, округлённый до ближайшего уровня.

  1. Прогнать через модель небольшую калибровочную выборку (обычно рекомендуют 128 последовательностей) и собрать XcalX_{\text{cal}}. Изменение строки на δ\delta меняет этот выход на δXcal\delta X_{\text{cal}}, поэтому квадрат ошибки выхода равен ∥δXcal∥2=12 δHδ⊤\|\delta X_{\text{cal}}\|^2 = \tfrac12\, \delta H \delta^\top с гессианом H=2XcalXcal⊤∈Rdin×dinH = 2 X_{\text{cal}} X_{\text{cal}}^\top \in \mathbb R^{d_{\text{in}} \times d_{\text{in}}}, одним и тем же для всех строк.
  2. Квантовать входные координаты (столбцы WW) по одной, сразу во всех строках, потому что HH у них общий. Пусть FF — множество ещё не квантованных координат, а HFH_F — гессиан, ограниченный на них. Округление координаты qq переводит её из wqw_q в quant⁡(wq)\operatorname{quant}(w_q), и поправка координат из FF, минимизирующая ошибку выхода когда эта координата закреплена, — это шаг Optimal Brain Surgeon (Hassibi and Stork, 1993; Frantar et al., 2023, раздел 3, формула 2): δF=−wq−quant⁡(wq)[HF−1]qq (HF−1):,q.\delta_F = -\frac{w_q - \operatorname{quant}(w_q)}{[H_F^{-1}]_{qq}}\, (H_F^{-1})_{:,q}. Её qq-я компонента равна −(wq−quant⁡(wq))-(w_q - \operatorname{quant}(w_q)), что ставит координату qq точно на её уровень; после этого qq покидает FF.
  3. Проходить координаты в фиксированном порядке, с ленивым пакетным обновлением и небольшим демпфированием HH для численной устойчивости.

Разобранный пример (игрушечные числа). Два сильно коррелированных входа с XcalXcal⊤=(10.90.91)X_{\text{cal}} X_{\text{cal}}^\top = \begin{pmatrix} 1 & 0.9 \\ 0.9 & 1 \end{pmatrix}, строка w=(0.3,0.4)w = (0.3, 0.4) и целые уровни. Если округлить каждый вес отдельно, получится (0,0)(0, 0), ошибка δ=(−0.3,−0.4)\delta = (-0.3, -0.4) и ∥δXcal∥2=0.09+0.16+2⋅0.9⋅0.12=0.466\|\delta X_{\text{cal}}\|^2 = 0.09 + 0.16 + 2 \cdot 0.9 \cdot 0.12 = 0.466. GPTQ округляет w1w_1 до 00. Первый столбец обратного гессиана, делённый на свой первый элемент, равен (1,−0.9)(1, -0.9), поэтому δ=−0.3⋅(1,−0.9)=(−0.3,0.27)\delta = -0.3 \cdot (1, -0.9) = (-0.3, 0.27) и w2w_2 сдвигается в 0.670.67, что округляется до 11. Результат (0,1)(0, 1) даёт ошибку (−0.3,0.6)(-0.3, 0.6) и ∥δXcal∥2=0.09+0.36−2⋅0.9⋅0.18=0.126\|\delta X_{\text{cal}}\|^2 = 0.09 + 0.36 - 2 \cdot 0.9 \cdot 0.18 = 0.126 — меньше трети ошибки отдельного округления. Если перевернуть знак поправки, w2w_2 сдвинулся бы в 0.130.13 и снова округлился бы до 00.

Получается 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"""""""". Одно предложение из nn токенов даёт nn векторов токенов, так что ранг его HH не больше nn — намного меньше входных размерностей слоёв (768 или 3072 в OPT-125M); он вырожден и обратим только благодаря демпфированию. Правдоподобное объяснение — компенсация опирается тогда на несколько плохо оценённых направлений и сдвигает оставшиеся веса туда, куда настоящие входы не ведут. Спектр HH в этом прогоне не измерен, поэтому это гипотеза, как и общий совет, что калибровочных данных должно быть достаточно и они должны быть разнообразны. Заранее квантованная модель чата на 7 млрд из Hub загружается в 4-битные слои и порождает нормально.

Дистилляция и прунинг#

Квантизация сохраняет архитектуру и уменьшает числа. Два других пути сжатия меняют архитектуру:

Дистилляция знаний (knowledge distillation; Hinton, Vinyals and Dean, 2015) обучает меньшего ученика повторять выходное распределение большего учителя, а не только жёсткие метки:

L=α LCE(y,ps)+(1−α) T2 DKL(softmax⁡(zt/T) ∥ softmax⁡(zs/T)),\mathcal L = \alpha\, \mathcal L_{\text{CE}}(y, p_s) + (1 - \alpha)\, T^2\, D_{\mathrm{KL}}\big(\operatorname{softmax}(z_t / T)\,\|\,\operatorname{softmax}(z_s / T)\big),

где zt,zsz_t, z_s — логиты учителя и ученика, а температура T>1T > 1 смягчает оба распределения, так что ученик узнаёт и то, какие неверные классы учитель считает близкими. Множитель T2T^2 сохраняет величину градиента сравнимой при разных температурах. Сам DistilBERT, измеренный выше, — это BERT-base, дистиллированный до 6 слоёв, с параметрами на 40% меньше и 97% результата на GLUE (Sanh et al., 2019). Для языковых моделей дистилляция принимает и форму обучения на тексте, порождённом большой моделью (путь Alpaca из заметки об обучении на инструкциях).

Прунинг (pruning) удаляет веса (неструктурированный) или целые головы, нейроны или слои (структурированный). Неструктурированная разреженность экономит память только с разреженными ядрами; структурированный прунинг даёт меньшую плотную модель и требует дообучения для восстановления.

Компромиссы#

БитПамять относительно fp16Типичное изменение качестваЧто нужно
161×нетничего
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 нужно сохранить.

Что нужно запомнить#

  • Аффинное отображение: xq=round⁡(x/s+z)x_q = \operatorname{round}(x/s + z), x≈s(xq−z)x \approx s(x_q - z), где s=(b−a)/(bq−aq)s = (b - a)/(b_q - a_q); ошибка ≤s/2\le s/2 внутри диапазона, обрезка вне его.
  • Квантованный линейный слой — целочисленное произведение матриц плюс суммы по строкам и столбцам, один раз масштабированное на sXsWs_X s_W.
  • 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(), делённое на 2302^{30}) и долю правильных ответов 0.9313 (2 мин 4 с на тестовую выборку на GPU).
  • Ячейка 19 выводит приведённое выше целочисленное произведение матриц; ячейки 20–22 реализуют QuantizedTensor (диапазон min–max на тензор с запасом 10−610^{-6}) и QuantizedLinear; ячейки 24–29 замеряют слой 128→128128 \to 128 (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), смотрят на упакованные буферы int32 qweight и qzeros и порождают приведённое выше гладкое предложение.
  • Ячейки 26–28 повторяют то же с калибровочной выборкой из одного предложения и порождают вырожденный выход.
  • Ячейки 32–38 загружают заранее квантованную модель чата Llama-2-7B, печатают слои QuantLinear и конфигурацию квантизации (4 бита, группа 128, ядро ExLlama) и порождают 50 токенов.
  • Ячейки 41–51 дообучают её с LoRA, как описано в заметке о PEFT.

Дальше#

Курс уже построил, предобучил, адаптировал и сжал модели. Следующий раздел обращается к задачам, для которых эти модели используются, и для каждой спрашивает, каковы вход и выход, какой метрикой задача измеряется, каким был классический базовый уровень и что изменил подход эпохи трансформеров. Открывает раздел заметка о классификации текстов — с задачей, которая проходит через весь курс, — и собирает все результаты на AG News в одну таблицу.

Литература#