Многие онлайн-сервисы предлагают доступ к проприетарным LLM. Однако по различным причинам может возникнуть необходимость использовать эти модели на своем оборудовании. Аренда серверов, особенно с GPU, может быть дорогой и зависит от требований к RAM/VRAM. Квантование моделей помогает снизить эти требования.
Итак, в этой статье мы:
Расскажем о квантовании и как оно помогает в выборе оборудования
Рассмотрим основные типы квантов в llama.cpp
Проведем ряд экспериментов на русскоязычном тексте
Сравним качество и скорость обработки и генерации текста (инференса)
LLM
В статье мы используем фреймворк llama.cpp, позволяющий запускать LLM практически на любом оборудовании и популярных ОС (включая Android). llama.cpp работает с моделями в формате gguf, которые можно получить, преобразовав torch-модели с помощью python-скриптов из репозитория llama.cpp или скачав уже квантованные модели, например, с Hugging Face.
Мы видим, что разные файлы занимают разный объем памяти. Каждый файл на скриншоте представляет собой модель нейросети. "Meta-Llama-3.1-8B-Instruct" — это название модели, а то, что следует за ним, например IQ2_M, обозначает тип квантования. Но что же такое квантование?
Квантование
Традиционно при обучении моделей их веса хранятся в формате чисел с плавающей точкой (floating point FP).
От количества двоичных разрядов, выделенных на число, зависит точность. Обычно, когда речь идет о числах с плавающей точкой в LLM, подразумеваются форматы FP16 или BF16. Такая точность требует много памяти (16 бит на вес). Процесс квантования большой языковой модели заключается в конвертации весов и активаций, например, в восьмибитные целочисленные значения, уменьшая таким образом размер модели вдвое.
Llama.cpp поддерживает различные варианты квантования (1, 2, 3, 4, 5, 6 и 8 бит), позволяя значительно уменьшить размер модели, занимаемый на диске и в оперативной памяти.
Инференс больших языковых моделей часто ограничен объемом и пропускной способностью памяти. Поэтому при применении квантования скорость инференса может значительно вырасти. Однако уменьшение точности весов может негативно сказаться на качестве модели.
К-кванты
K-кванты — это линейная квантизация весов. Работает за счет того, что модели обучаются с layernorm. Теоретически могут быть проблемы с весами, значения которых далеки от средних.
I-кванты
В I-квантах строится importance-матрица, которая позволяет находить “важные” веса и особым образом квантовать. Веса, которые имеют большее влияние на выход модели, квантуются с меньшим количеством бит (агрессивнее), а менее важные веса — с большим количеством бит. Звучит хорошо, но на каких данных строится importance-матрица?
Сравнение качества
Для количественного анализа потенциального снижения качества генерации токенов при квантовании весов можно использовать метрику перплексии (perplexity или PPL). Перплексия определяется как экспонента от кросс-энтропии и может быть выражена следующей формулой:
где X = (x1, x2, …, xt) — это сгенерированная последовательность.
Низкие значения перплексии говорят о более высокой уверенности модели в предсказании следующего токена.
Примечание: использовать эту метрику для сравнения разных моделей (например, Llama3 с Mistral) будет неуместным из-за влияния на вероятности множества факторов, включая перечень задач, для решения которых натренированы модели, использованные для этого данные, размер словаря и т. д. В идеале мы хотим оценить, насколько хорошо модель решает поставленную задачу до и после квантования по более сложным метрикам (например, качество написанного кода или математические способности).
Практика
Постановка эксперимента
Итак, мы видим, что есть 2 разных подхода к квантованию и способ оценки качества. Скорость будем проверять через llama_bench, а качество — через llama_perplexity. Мы проведем эксперименты на модели llama3.1 8B и узнаем, какое квантование лучше.
Установим llama.cpp для запуска LLM на CPU в linux:
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make -j
Утилита llama-quantize позволяет квантовать во множество форматов (см. спойлер).
Возможные форматы
QUANT_OPTIONS = {
{ "Q4_0", LLAMA_FTYPE_MOSTLY_Q4_0, " 4.34G, +0.4685 ppl @ Llama-3-8B", },
{ "Q4_1", LLAMA_FTYPE_MOSTLY_Q4_1, " 4.78G, +0.4511 ppl @ Llama-3-8B", },
{ "Q5_0", LLAMA_FTYPE_MOSTLY_Q5_0, " 5.21G, +0.1316 ppl @ Llama-3-8B", },
{ "Q5_1", LLAMA_FTYPE_MOSTLY_Q5_1, " 5.65G, +0.1062 ppl @ Llama-3-8B", },
{ "IQ2_XXS", LLAMA_FTYPE_MOSTLY_IQ2_XXS, " 2.06 bpw quantization", },
{ "IQ2_XS", LLAMA_FTYPE_MOSTLY_IQ2_XS, " 2.31 bpw quantization", },
{ "IQ2_S", LLAMA_FTYPE_MOSTLY_IQ2_S, " 2.5 bpw quantization", },
{ "IQ2_M", LLAMA_FTYPE_MOSTLY_IQ2_M, " 2.7 bpw quantization", },
{ "IQ1_S", LLAMA_FTYPE_MOSTLY_IQ1_S, " 1.56 bpw quantization", },
{ "IQ1_M", LLAMA_FTYPE_MOSTLY_IQ1_M, " 1.75 bpw quantization", },
{ "Q2_K", LLAMA_FTYPE_MOSTLY_Q2_K, " 2.96G, +3.5199 ppl @ Llama-3-8B", },
{ "Q2_K_S", LLAMA_FTYPE_MOSTLY_Q2_K_S, " 2.96G, +3.1836 ppl @ Llama-3-8B", },
{ "IQ3_XXS", LLAMA_FTYPE_MOSTLY_IQ3_XXS, " 3.06 bpw quantization", },
{ "IQ3_S", LLAMA_FTYPE_MOSTLY_IQ3_S, " 3.44 bpw quantization", },
{ "IQ3_M", LLAMA_FTYPE_MOSTLY_IQ3_M, " 3.66 bpw quantization mix", },
{ "Q3_K", LLAMA_FTYPE_MOSTLY_Q3_K_M, "alias for Q3_K_M" },
{ "IQ3_XS", LLAMA_FTYPE_MOSTLY_IQ3_XS, " 3.3 bpw quantization", },
{ "Q3_K_S", LLAMA_FTYPE_MOSTLY_Q3_K_S, " 3.41G, +1.6321 ppl @ Llama-3-8B", },
{ "Q3_K_M", LLAMA_FTYPE_MOSTLY_Q3_K_M, " 3.74G, +0.6569 ppl @ Llama-3-8B", },
{ "Q3_K_L", LLAMA_FTYPE_MOSTLY_Q3_K_L, " 4.03G, +0.5562 ppl @ Llama-3-8B", },
{ "IQ4_NL", LLAMA_FTYPE_MOSTLY_IQ4_NL, " 4.50 bpw non-linear quantization", },
{ "IQ4_XS", LLAMA_FTYPE_MOSTLY_IQ4_XS, " 4.25 bpw non-linear quantization", },
{ "Q4_K", LLAMA_FTYPE_MOSTLY_Q4_K_M, "alias for Q4_K_M", },
{ "Q4_K_S", LLAMA_FTYPE_MOSTLY_Q4_K_S, " 4.37G, +0.2689 ppl @ Llama-3-8B", },
{ "Q4_K_M", LLAMA_FTYPE_MOSTLY_Q4_K_M, " 4.58G, +0.1754 ppl @ Llama-3-8B", },
{ "Q5_K", LLAMA_FTYPE_MOSTLY_Q5_K_M, "alias for Q5_K_M", },
{ "Q5_K_S", LLAMA_FTYPE_MOSTLY_Q5_K_S, " 5.21G, +0.1049 ppl @ Llama-3-8B", },
{ "Q5_K_M", LLAMA_FTYPE_MOSTLY_Q5_K_M, " 5.33G, +0.0569 ppl @ Llama-3-8B", },
{ "Q6_K", LLAMA_FTYPE_MOSTLY_Q6_K, " 6.14G, +0.0217 ppl @ Llama-3-8B", },
{ "Q8_0", LLAMA_FTYPE_MOSTLY_Q8_0, " 7.96G, +0.0026 ppl @ Llama-3-8B", },
{ "Q4_0_4_4", LLAMA_FTYPE_MOSTLY_Q4_0_4_4, " 4.34G, +0.4685 ppl @ Llama-3-8B", },
{ "Q4_0_4_8", LLAMA_FTYPE_MOSTLY_Q4_0_4_8, " 4.34G, +0.4685 ppl @ Llama-3-8B", },
{ "Q4_0_8_8", LLAMA_FTYPE_MOSTLY_Q4_0_8_8, " 4.34G, +0.4685 ppl @ Llama-3-8B", },
{ "F16", LLAMA_FTYPE_MOSTLY_F16, "14.00G, +0.0020 ppl @ Mistral-7B", },
{ "BF16", LLAMA_FTYPE_MOSTLY_BF16, "14.00G, -0.0050 ppl @ Mistral-7B", },
{ "F32", LLAMA_FTYPE_ALL_F32, "26.00G @ 7B", },
// Note: Ensure COPY comes after F32 to avoid ftype 0 from matching.
{ "COPY", LLAMA_FTYPE_ALL_F32, "only copy tensors, no quantizing", },
}
В llama.cpp реализовано большое число квантов. Рассмотрим все примеры из репозитория llama3.1 8B. Для автоматизации экспериментов пишем код на bash, а не на python, чтобы можно было запускать этот код без установки дополнительных зависимостей. Код доступен на github.
Скорость
Для оценки скорости обработки промпта и скорости генерации запускаем llama-bench на всех файлах. По умолчанию устанавливаются значения 512 для prompt processing (модель “прочитает” 512 токенов) и 128 для text generation (модель сгенерирует 128 токенов). Каждый эксперимент проводится 5 раз и высчитывается среднее значение. Код программы для выполнения эксперимента на всех моделях в папке:
#!/bin/bash
llama_bench_path="$HOME/llama.cpp/llama-bench"
results_file="llama_bench_raw_results.md"
output_csv="llama_bench_aggregated_output.csv"
model_dir="."
ngl=100
if [ ! -f "$llama_bench_path" ]; then
echo "Error: llama-bench not found at $llama_bench_path"
exit 1
fi
echo "model_name,size,pp512,tg128" > "$output_csv"
parse_and_append_to_csv() {
local model_name="$1"
local output="$2"
local size="" pp512="" tg128=""
while IFS= read -r line; do
[[ $line =~ \|\ *llama.*\ *\|\ *([0-9.]+)\ GiB\ *\|\ *.*\ *\|\ *(pp512|tg128)\ *\|\ *([0-9.]+) ]] && {
size="${BASH_REMATCH[1]}"
[[ ${BASH_REMATCH[2]} == "pp512" ]] && pp512="${BASH_REMATCH[3]}" || tg128="${BASH_REMATCH[3]}"
}
done <<< "$output"
echo "$model_name,$size,$pp512,$tg128" >> "$output_csv"
}
find "$model_dir" -name "*.gguf" -print0 | while IFS= read -r -d '' model; do
model_name=$(basename "$model" .gguf)
echo "Running llama-bench on $model_name..."
output=$("$llama_bench_path" -m "$model" -ngl "$ngl")
{
echo -e "Results for $model_name:\n------------------------\n$output\n\n"
} >> "$results_file"
parse_and_append_to_csv "$model_name" "$output"
done
echo "All results have been combined into $results_file"
echo "CSV file has been created at $output_csv."
Данные сохраняются в CSV-файл, а если будут нужны подробности каждого запуска, то они сохраняются в llama_bench_raw_results.md
Качество
В роли тестового текста для измерения перплексии возьмем произведение “Пиковая дама” А.С. Пушкина. Код скрипта для оценки перплексии, который сохраняет имя модели и перплексию для каждого gguf файла в заданной папке в llama_perplexity_results.csv
#!/bin/bash
llama_perplexity="$HOME/llama.cpp/llama-perplexity"
test_file="pikovaya_dama.txt"
gguf_folder="."
ngl=100
output_file="llama_perplexity_results.csv"
> "$output_file"
for gguf_file in "$gguf_folder"/*.gguf; do
file_name=$(basename "$gguf_file" .gguf)
output=$(eval "$llama_perplexity -f $test_file -m $gguf_file -ngl $ngl")
final_estimate=$(echo "$output" | grep -o 'Final estimate: PPL = [0-9.]*' | sed 's/Final estimate: PPL = //')
echo "$file_name,$final_estimate" >> "$output_file"
done
Результаты
Все результаты мы собрали в одну таблицу:
Модель |
Размер модели (GiB) |
Скорость tg128 на CPU (t/s) |
Скорость pp512 на CPU (t/s) |
PPL |
Meta-Llama-3.1-8B-Instruct-f32 |
29.92 |
5.5 |
129.24 |
10.0608 |
Meta-Llama-3.1-8B-Instruct-Q8_0 |
7.95 |
18.25 |
445.93 |
10.0614 |
Meta-Llama-3.1-8B-Instruct-Q6_K |
6.14 |
20.92 |
539.37 |
10.098 |
Meta-Llama-3.1-8B-Instruct-Q6_K_L |
6.37 |
20.84 |
546.71 |
10.0982 |
Meta-Llama-3.1-8B-Instruct-Q5_K_L |
5.63 |
22.23 |
622.19 |
10.1468 |
Meta-Llama-3.1-8B-Instruct-Q5_K_M |
5.33 |
22.74 |
777.02 |
10.1508 |
Meta-Llama-3.1-8B-Instruct-Q5_K_S |
5.21 |
23.23 |
759.19 |
10.1614 |
Meta-Llama-3.1-8B-Instruct-Q4_K_L |
4.94 |
25.35 |
716.21 |
10.3221 |
Meta-Llama-3.1-8B-Instruct-Q4_K_M |
4.58 |
25.68 |
712.83 |
10.365 |
Meta-Llama-3.1-8B-Instruct-Q4_K_S |
4.36 |
26.09 |
948.55 |
10.398 |
Meta-Llama-3.1-8B-Instruct-IQ4_XS |
4.13 |
26.24 |
776.49 |
10.4468 |
Meta-Llama-3.1-8B-Instruct-Q3_K_XL |
4.45 |
26.39 |
979.99 |
10.7521 |
Meta-Llama-3.1-8B-Instruct-Q3_K_L |
4.02 |
27.45 |
782.53 |
10.8216 |
Meta-Llama-3.1-8B-Instruct-Q3_K_M |
3.74 |
29.29 |
1006.48 |
11.0046 |
Meta-Llama-3.1-8B-Instruct-IQ3_M |
3.52 |
26.61 |
890.29 |
11.3089 |
Meta-Llama-3.1-8B-Instruct-IQ3_XS |
3.27 |
28.47 |
926.26 |
11.6679 |
Meta-Llama-3.1-8B-Instruct-Q3_K_S |
3.41 |
29.44 |
1114.8 |
12.4374 |
Meta-Llama-3.1-8B-Instruct-Q2_K |
2.95 |
33.29 |
1137.28 |
15.0171 |
Meta-Llama-3.1-8B-Instruct-IQ2_M |
2.74 |
31.68 |
1316.96 |
15.6223 |
Meta-Llama-3.1-8B-Instruct-Q2_K_L |
3.43 |
34.04 |
965.15 |
16.0856 |
Как и ожидалось, перплексия увеличивается при уменьшении точности весов. По графику видно, что Q5_K_S версия квантования показывает очень хороший результат. При незначительном увеличении перплексии вес модели уменьшился в 6 (!) раз. Это значит, что требуется в 6 раз меньше оперативной памяти для работы модели при использовании этого метода квантования. Далее рассмотрим Q5_K_S поближе.
Скорость обработки промпта модели Q5_K_S увеличивалась почти так же в 6 раз (759 t/s по сравнению с 129 t/s у неквантованной модели). Хоть эта модель и не самая быстрая, не нужно забывать о перплексии (см. таблицу).
В случае со скоростью генерации версия Q5_K_S выдает 23 токенов в секунду при 5,5 токенах в секунду у неквантованной модели, что также выделяет ее среди версий с незначительной потерей в показателе PPL.
Вывод
При аренде оборудования имеет смысл рассмотреть квантованные модели, так как LLM занимают большой объем RAM и требовательны к ее производительности. При квантовании моделей получится уменьшить требования к объему RAM/VRAM и ускорить работу LLM за счет снижения качества генерации/обработки промпта. Это снижение качества может быть незначительным, особенно при использовании Q5* и Q6*.
Для большей уверенности имеет смысл проверить и на других бенчмарках, релевантных для конкретных задач. В следующей статье рассмотрим изменение производительности на GPU (результаты вас удивят).
P.S. напишите в комментариях, как бы вы выбрали оптимальный способ квантования алгоритмически?
Что ещё почитать:
An Empirical Study of LLaMA3 Quantization: From LLMs to MLLMs https://arxiv.org/pdf/2404.14047
Автор: Кононова Валентина, ML инженер
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
–15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS
Комментарии (2)
Theio
10.10.2024 21:55Да, очень интересно то же самое, но на GPU. Из своего опыта, делал замеры 8b модельки, но не в llama.cpp, а торч+transformers в fp16/bf16/q8(bnb), bf16 просаживало скорость на процентов 20, q8 замедляло работу раза в два. Torchao в fp8 почему-то работает в 2 раза медленнее q8, vllm в fp16/fp8 работает на порядок быстрее торча. Использование всяких compile, flash_attention и прочего доступного в transformers ускорения не давало. Тестил на x2 4060ti 16гб компе.
andnotor
Очень интересно версию про GPU почитать.