Как уместить большие языковые модели в ограниченной памяти квантование

Как уместить языковые модели в ограниченной памяти с помощью квантования

Большие языковые модели могут использоваться для генерации текста, перевода, задач вопрос-ответ и т. д. Однако ЯММы также очень большие (очевидно, Большие языковые модели) и требуют много памяти. Это может сделать их сложными для маленьких устройств, таких как телефоны и планшеты.

Умножьте параметры на выбранный размер точности, чтобы определить размер модели в байтах. Допустим, что выбранная нами точность – float16 (16 бит = 2 байта). Предположим, мы хотим использовать модель BLOOM-176B. Нам нужно 176 миллиардов параметров * 2 байта = 352 ГБ, чтобы загрузить модель!

Большие языковые модели

Другими словами, для загрузки всех весов параметров нам потребуется 12(!) машин по 32 ГБ! Это слишком много, если мы хотим сделать ЯММы портативными. Были разработаны техники для сокращения объема памяти ЯММов, чтобы преодолеть такую борьбу. Самые популярные техники:

  • Квантизация включает преобразование весов ЯММов в формат с меньшей точностью, что уменьшает требуемую память для их хранения.
  • Передача знаний включает обучение более маленькой ЯММ, чтобы она имитировала поведение более большой ЯММ. Это можно сделать, передав знания от более большой ЯММ на более маленькую ЯММ.

Благодаря этим техникам стало возможным уместить ЯММы в небольшую память. Это открыло новые возможности для использования ЯММов на различных устройствах. Сегодня мы поговорим о квантизации (оставайтесь на связи для передачи знаний).

Квантизация

Начнем с простого примера. Нам нужно преобразовать число 2023 в двоичную систему:

Преобразование числа в двоичную систему

Как видно, процесс относительно простой. Чтобы сохранить число 2023, нам понадобится 12+ бит (1 бит для знака + или —). Для числа мы могли бы использовать тип int16.

Есть большая разница между хранением int в двоичном виде и хранением float таким же. Попробуем преобразовать число 20.23 в двоичную систему:

Преобразование числа с плавающей запятой в двоичную систему

Как видно, дробная часть (мантисса) вычисляется как комбинация 1/2^n и не может быть вычислена очень точно, даже если выделить 10 бит для дробной части. Целая часть числа (показатель) устанавливается на 5 битов, охватывая все числа до 32. Всего мы используем 16 битов (FP16) для хранения наиболее близкого значения к 20.23, но это наиболее эффективный способ хранения чисел с плавающей запятой? Что, если целая часть числа намного больше, скажем, 202.3?

Если мы посмотрим на стандартные типы чисел с плавающей запятой, мы заметим, что для хранения 202.3 нам понадобится использовать FP32, что с вычислительной точки зрения далеко не разумно. Вместо этого мы можем использовать bfloat16 для сохранения диапазона (показателя) в виде 8 бит и 7 бит для точности (мантиссы). Это позволяет нам расширить возможные десятичные дроби без потери многой точности.

Типы данных FP32, FP16, BFloat16

Чтобы быть ясным, во время обучения нам нужна как можно большая точность. Но приоритет скорости и размера над 6-й десятичной запятой имеет смысл для вывода.

Можем ли мы уменьшить использование памяти от bfloat16 до, скажем, int8?

Квантизация нулевой точки и абсолютного максимума

На самом деле, мы можем и есть несколько подходов для такой квантования:

  • Квантование с нулевой точкой экономит половину памяти, преобразуя фиксированный диапазон (-1, 1) в int8 (-127, 127), а затем обратно преобразовывая int8 в bfloat16.
Квантование с нулевой точкой
  • Квантование abs-max похоже на квантование с нулевой точкой, но вместо установки пользовательского диапазона (-1,1) мы устанавливаем его как (-abs(max), abs(max)).
Квантование abs-max

Давайте посмотрим, как эти методы используются на примере умножения матриц:

Точное умножение матриц

Квантование с нулевой точкой:

Квантование с нулевой точкой для умножения матриц

Квантование abs-max:

Квантование abs-max для умножения матриц

Как можно заметить, оценка для больших значений [-1579, -1780] довольно низкая ([-1579, -1752] для нулевой точки и [-1565,-1786] для abs-max). Чтобы преодолеть такие проблемы, мы можем отделить умножение выбросов:

Отделение умножения выбросов

Как видите, результаты гораздо ближе к истинным значениям.

Но есть ли способ использовать еще меньше пространства, не потеряв при этом много качества?

К моему большому удивлению, такой способ есть! Что, если мы, вместо независимого преобразования каждого числа в более низкий тип, учтем ошибку и будем использовать ее для корректировки? Эта техника называется GPTQ.

Как и в предыдущих методах квантования, мы находим ближайшее совпадение для десятичных долей, сохраняя общую ошибку преобразования как можно ближе к нулю.

Приближение GPTQ. Шаг 1

Мы заполняем матрицу строками таким образом.

Приближение GPTQ. Шаг 2

Результат, в сочетании с вычислениями по отдельности для аномалий, дает довольно приемлемые результаты:

Умножение матрицы GPTQ с отфильтрованными выбросами

Теперь мы можем сравнить все методы:

Сравнение результатов

Методы LLM.int8() работают довольно хорошо! Подход GPTQ теряет качество, но позволяет использовать в два раза больше памяти GPU, чем метод int8.

В коде вы можете найти что-то подобное следующему:

from transformers import BitsAndBytesConfig# Настройте BitsAndBytesConfig для квантования 4 битаbnb_config = BitsAndBytesConfig(    load_in_4bit=True,    bnb_4bit_use_double_quant=True,    bnb_4bit_quant_type="nf4",    bnb_4bit_compute_dtype=torch.bfloat16,)# Загрузка модели в предварительно заданной конфигурацииpretrained_model = AutoModelForCausalLM.from_pretrained(    model_id,    quantization_config=bnb_config,)

Флаг load_in_4bit указывает, что модель должна загружаться с точностью 4 бита. Флаг bnb_4bit_use_double_quant указывает, что должно использоваться двойное квантование. Флаг bnb_4bit_quant_type указывает тип квантования. Флаг bnb_4bit_compute_dtype указывает тип вычисления.

В итоге мы узнали, как числа с плавающей запятой хранятся в памяти, как уменьшить использование памяти с некоторой потерей точности и как запускать выбранные модели с квантованием 4 бита.

Оригинальная статья была опубликована на моей странице LinkedIn.