Многие онлайн-сервисы предлагают доступ к проприетарным 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