Привет, хабровчане!
Признаюсь, я не большой любитель vLLM, Triton Inference Server и всяких там NeMo, вместо них я предпочитаю ollama вообще и llama.cpp в частности, поскольку придерживаюсь мнения, что 1-2% потери в точности и отсутствие некоторых плюшек - не так важно, по сравнению с удобством деплоя, спекулятивным декодингом, многократным приростом скорости, динамическим оффлодом в память системы и возможностью запускать модели на любом "ведре", навроде древних зионов, андройдофонов, малинок или, скажем, макбуков.
Поэтому вполне ожидаемым для меня является, когда авторы моделей заморачиваются с конвертацией оных в GGUF - особом формате сжатия весов моделей, пригодном для запуска через упомянутые выше ollama и llama.cpp.
Однако реальность обычно немного отличается от ожиданий, и конвертацию в GGUF с последующей квантизацией приходится делать самостоятельно, а чтобы качество работы модели не падало, желательно генерировать imatrix через калибровочный датасет, о чём я и хочу рассказать в данной публикации.

The Мотивация™
Поскольку большинство вендоров (и наши соотечественники в том числе) частенько пренебрегают данным форматом, приходится брать рога за быка и конвертировать, а затем квантовать модельки в GGUF самостоятельно. Но у конвертации в данный формат с дальнейшей квантизацией "в лоб" есть один фатальный недостаток - такие квантованные модели обычно проседают в точности, так как сжатие выполняется наивно, без учёта архитектуры и знаний, заложенных при обучении модели.
Более правильным методом является квантизация модели при помощи калибровочного датасета, содержащего в себе только необходимые для нас данные, данный приём можно условно описать как "LoRA наоборот", так как мы не обучаем модель на специализированном корпусе, а наблюдаем за её поведением на близких к продуктовым задачам кейсах, затем отсекаем всё ненужное. Нечто подобное применяется в методе GPTQ, но поскольку формат GPTQ так и не обрёл популярности, о его поддержке в llama.cpp (и, как следствие, ollama) не может быть и речи.
Да и зачем он нужен, когда в llama.cpp сравнительно недавно добавили утилиту imatrix?
Вот небольшая справка от ChatGPT обогащённая контекcтом и моими советами:
Инструмент
llama-imatrix
в llama.cpp предназначен для вычисления и сохранения матрицы важности (importance matrix), отражающей, насколько сильно различные тензоры модели (веса слоёв) влияют на результат при обработке реального текстового корпуса, эта статистика используется при квантизации, чтобы сохранить качество модели - более "важные" параметры квантуются с меньшими потерями.llama-imatrix
анализирует активации модели на заданных текстах, накапливает показатели важности для каждого тензора, сохраняет их в файл, после чего квантизаторllama-quantize
может учитывать эти данные для адаптивного понижения точности весов.
Поэтому решил разобраться сам и рассказать вам, как конвертировать, а затем квантовать в GGUF на примере модельки ai-sage/GigaChat-20B-A3B-instruct, используя для калибровки датасет wikimedia/wikipedia, а точнее - первые 500
элементов сабсета 20231101.ru
.
Run, GGUF, run
Для начала понадобится скачать и конвертировать модель ai-sage/GigaChat-20B-A3B-instruct в GGUF без квантизации, в формате F16
. Для этого скачаем репозиторий llama.cpp
, затем создадим в нём .venv
, поставим пакеты и выполним сборку llama.cpp:
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Скомпилируем бинарники:
cmake -S . -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
Подробнее про компиляцию llama.cpp под разные платформы, в том числе и CUDA в README проекта.
Поставим ещё пару пакетов до кучи:
pip install "datasets>=4.1" "transformers>=4.56"
Песочница готова, так что теперь можем скачать веса модели и выполнить конвертацию:
hf download ai-sage/GigaChat-20B-A3B-instruct --local-dir ./models/ai-sage/GigaChat-20B-A3B-instruct
python convert_hf_to_gguf.py models/ai-sage/GigaChat-20B-A3B-instruct
Результат будет сохранён тут:
./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-F16.gguf
Запомним и вернёмся к этому чуть позже.
Калибруй это
Зная формат данных датасета wikimedia/wikipedia и зная, как обычно тренеры моделей формируют из семплов датасета обучающие примеры (спойлер: при помощи метода apply_chat_template объекта токенизатора), создадим небольшой скриптик, обзовём его wikipedia_generator.py, положим его в папку с llama.cpp и запустим вот так:
python wikipedia_generator.py \
--model ai-sage/GigaChat-20B-A3B-instruct \
--subset 20231101.ru \
--split train \
--limit 500 \
--output ./calib.txt
Рядом появится файлик ./calib.txt
, его и будем использовать далее.
Теперь вызовем скомпилированный ранее бинарник llama-imatrix
, передадим на вход путь до калибровачного датасета, созданного шагом ранее, и путь до квантованной в GGUF модели:
./build/bin/llama-imatrix \
-m ./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-F16.gguf \
-f ./calib.txt \
-o ./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-F16.imatrix.gguf
--n-gpu-layers 10
И идём читать arXiv пару часов, так как процедура обычно не быстрая.
По прошествии некоторого времени результат работы скрипта будет сохранён тут:
./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-F16.imatrix.gguf
Размером примерно 41 мегабайт, для разных моделей размер может варьироваться от 1-2 до 30-40 мегабайт, как правило у классических dense моделей размер этого файла меньше чем у MoE моделей, вероятно в силу особенностей архитектуры.
Теперь мы можем при помощи скомпилированной утилиты llama-quantize
выполнить квантизацию модели до уровня q4_k_m
, но не абы как, а с учётом поправок полученных на датасете wikimedia/wikipedia
при помощи llama-imatrix
.
Пишем команду:
./build/bin/llama-quantize \
--imatrix ./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-F16.imatrix.gguf \
./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-F16.gguf \
./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-Q4_K_M.gguf \
Q4_K_M
Квантованная в q4_k_m
модель будет сохранена тут:
./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-Q4_K_M.gguf
Аналогичным образом будет выглядеть процедура квантизации до других уровней, только циферки меняй, полный список всех доступных уровней квантизации тут.
Добавляем в ollama
Отлично, у нас есть моделька, конвертированная в GGUF и квантованная до q4_k_m, что дальше? Дальше можно запустить её, например, через llama-server
или llama-cli
и попробовать повыполнять на ней запросы, а можно пойти ещё дальше и импортировать её в локальную ollama.
Для этого создадим файл Modelfile
следующего содержимого:
FROM ./models/ai-sage/GigaChat-20B-A3B-instruct/GigaChat-20B-A3B-instruct-Q4_K_M.gguf
TEMPLATE """
{{- if .System }}
<s>{{ .System }}<|message_sep|>
{{- end }}
{{- range .Messages }}
{{- if eq .Role "user" }}
{{ .Role }}<|role_sep|>{{ .Content }}<|message_sep|>
available functions<|role_sep|>[]<|message_sep|>
{{- else if eq .Role "assistant" }}
{{ .Role }}<|role_sep|>{{ .Content }}<|message_sep|>
{{- end }}
{{- end }}
{{- if not .Response }}assistant<|role_sep|>
{{- end }}
"""
Поле TEMPLATE
обычно отличается у разных моделей.
Подробнее про создание Modelfile в документации ollama.
Далее импортируем этот Modelfile в нашу локальную ollama:
ollama create -f Modelfile GigaChat-20B-A3B-instruct:Q4_K_M
Вместо GigaChat-20B-A3B-instruct:Q4_K_M
можно указать любое другое название, это проссто тег, которым ollama пометит эту модель.
Попробуем запустить модель:
ollama run GigaChat-20B-A3B-instruct:Q4_K_M
И можем пообщаться.
Про интеграцию через API рассказывать уже не буду, это и так понятно, на http://localhost:11434 ходим клиентом ollama, на http://localhost:11434/v1 клиентом openai, в поле model
узказываем GigaChat-20B-A3B-instruct:Q4_K_M
.
Заключение
Если коротко то история вида: конвертация в GGUF > калибровка imatrix > квантизация > запуск в llama.cpp/ollama помогает нам масштабироваться "вниз", не требуя дата-центров и дорогих GPU. Используя imatrix мы целенаправленно "подгоняем" квантизацию под свои кейсы (через калибровочный корпус), срезая лишнее, сохраняя только нужное, получаем быстрый деплой и переносимость, но при этом не теряя качество.
Надеюсь моя небольшая инструкция пригодится вам в ваших начинаниях!
Если вам интересно, как это развивать дальше, то заглядывайте ко мне в Телегу @evilfreelancer, и само собой задавайте вопросы под постом и/или в личку, буду рад пообщаться.