Многие онлайн-сервисы предлагают доступ к проприетарным LLM. Однако по различным причинам может возникнуть необходимость использовать эти модели на своем оборудовании. Аренда серверов, особенно с GPU, может быть дорогой и зависит от требований к RAM/VRAM. Квантование моделей помогает снизить эти требования.
Итак, в этой статье мы:
Расскажем о квантовании и как оно помогает в выборе оборудования
Рассмотрим основные типы квантов в llama.cpp
Проведем ряд экспериментов на русскоязычном тексте
Сравним качество и скорость обработки и генерации текста (инференса)
В статье мы используем фреймворк 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-квантах строится 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