Многие онлайн-сервисы предлагают доступ к проприетарным LLM. Однако по различным причинам может возникнуть необходимость использовать эти модели на своем оборудовании. Аренда серверов, особенно с GPU, может быть дорогой и зависит от требований к RAM/VRAM. Квантование моделей помогает снизить эти требования.

Итак, в этой статье мы:

  1. Расскажем о квантовании и как оно помогает в выборе оборудования

  2. Рассмотрим основные типы квантов в llama.cpp 

  3. Проведем ряд экспериментов на русскоязычном тексте

  4. Сравним качество и скорость обработки и генерации текста (инференса) 

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)


  1. andnotor
    10.10.2024 21:55

    Очень интересно версию про GPU почитать.


  1. 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гб компе.