Когда стандартный convert.py пасует
Вы натренировали свою архитектуру. Не LLaMA, не Mistral, не Qwen — а что-то своё, с хитрой attention-механикой или нестандартными слоями. Вы с гордостью смотрите на .safetensors, но тут приходит суровая реальность: ни convert.py, ни hf-to-gguf не знают вашу модель. Единственный путь — залезть в кишки llama.cpp и добавить поддержку вручную. Звучит страшно? На практике — да, но вполне реально. Разберём на примере маленькой, но гордой модели Laguna XS.2 — её полный код портирования лежит на GitHub (ссылка ниже).
Если вы ещё не сталкивались с тонкостями сборки llama.cpp, советую сначала прочитать нашу статью «Сборка llama.cpp не для всех» — там разобраны флаги и оптимизации, которые пригодятся и здесь.
Анатомия портирования: от PyTorch до GGUF
Весь процесс сводится к трём этапам:
- Конвертация весов в GGUF — пишем Python-скрипт, который читает ваши тензоры и упаковывает их в бинарный формат с метаданными.
- Регистрация архитектуры в C++ коде llama.cpp — добавляем новую конфигурацию в таблицу поддерживаемых моделей (файл
llama.cppилиggml-*.h) и определяем функции загрузки тензоров, построения графа и forward pass. - Компиляция и тестирование — собираем проект, запускаем инференс и молимся, чтобы не вылетело с segfault.
Звучит как три шага, но каждый из них — ведро граблей. Особенно если ваша модель использует нестандартные операции (например, смещённые RoPE или групповой query attention с нецелым числом голов).
Наш подопытный: Laguna XS.2
Laguna XS.2 — экспериментальная модель от группы энтузиастов, основанная на архитектуре, близкой к LLaMA, но с двумя отличиями:
- использует MLP с GELU вместо SiLU (да, да, анахронизм, но автор хотел именно так);
- слой нормализации — LayerNorm, а не RMSNorm, и он идёт после attention, а не до.
Эти «мелочи» ломают все готовые скрипты конвертации. Зато это отличный полигон для изучения внутренностей llama.cpp.
Весь код портирования — от скрипта конвертации до изменений в C++ — выложен в открытом GitHub-репозитории (ссылка в конце статьи). Там же лежат собранные GGUF-файлы в разных квантовках — от Q2_K до Q8_0.
Код, который мы напишем (и где он лежит)
Первая точка входа — скрипт convert_laguna.py. Он загружает модель через transformers (или safetensors напрямую) и пишет GGUF-файл. Ключевой момент — правильное заполнение метаданных: llama.architecture должно быть равно "laguna" — именно по этой строке C++ код будет идентифицировать модель.
Пример вызова:
python convert_laguna.py --input ./laguna-xs2 --output laguna-xs2-q4_k_m.gguf --quantize q4_k_mВторая часть — изменения в C++. В репозитории добавлен файл ggml-laguna.cpp (на самом деле патч к основному llama.cpp), где реализованы:
- загрузка тензоров:
llama_model_load_internalдополнен веткойcase LLM_ARCH_LAGUNA:; - построение графа: функция
build_laguna, где используются стандартные ggml-операции:ggml_mul_mat,ggml_gelu,ggml_layer_norm; - поддержка KV-кэша и маски внимания (ничего необычного).
Пример куска кода для кастомного GELU:
static struct ggml_tensor *build_laguna_mlp(ggml_context *ctx, ggml_tensor *x, struct laguna_layer *layer) {
x = ggml_mul_mat(ctx, layer->w1, x);
x = ggml_gelu(ctx, x); // вместо ggml_silu
x = ggml_mul_mat(ctx, layer->w2, x);
return x;
}После патча компилируем: make -j (или используем CMake, если нужна поддержка GPU).
Собираем всё в кучу: компиляция и первый запуск
Если вы настраивали сборку под гибридные GPU (например, iGPU + CUDA), обязательно прочитайте статью «Гибридный ад: как заставить iGPU и CUDA работать вместе» — там описаны флаги, которые уберегут вас от двойной компиляции. После сборки запускаем:
./llama-cli -m laguna-xs2-q4_k_m.gguf -p "Hello, world" -n 128Первая строка вывода — это метаданные модели. Если архитектура не распознана, вы увидите ошибку вроде "unknown architecture: laguna" — значит, вы забыли зарегистрировать её в таблице LLM_ARCH. В нашем репозитории этого нет.
Важно: не забудьте обновить enum llm_arch и llm_kv — это частая ошибка. Подробнее про такие грабли мы писали в статье «Unknown filter items в llama.cpp: ломаем Qwen3.5 и чиним за 5 минут» — механизм похожий.
Грабли, на которые мы наступили (и вы наступите)
Вот список того, что пошло не так во время разработки — и что почти гарантированно пойдёт не так у вас:
- Порядок тензоров при записи GGUF — llama.cpp ожидает, что тензоры будут записаны в определённом порядке. Если вы сначала запишете
output.weight, а потомtok_embeddings.weight, загрузка упадёт с ошибкой проверки размерностей. Решение — строго следовать порядку изllama_model_tensor_nameдля вашей архитектуры. - Неправильная size_t для dim — в GGUF размерности хранятся как uint32_t, а в C++ — как int64_t. Легко перепутать, и тогда модель будет плеваться мусором. У нас в коде есть assert'ы на каждый тензор.
- LayerNorm вместо RMSNorm — встроенная функция
ggml_rms_normне подходит. Пришлось использоватьggml_normи вручную добавлять bias (которого нет в RMSNorm).
Кстати, если ваша модель использует расширенные токенизаторы, почитайте статью «Почему модель спорит с пользователем: разбираем цензуру dolphin-2.9-llama3-8b» — там есть примеры, как нестандартные токены ломают инференс.
Кому это вообще нужно?
Наш гайд — не для всех. Если ваша модель — это просто дообученная LLaMA с тем же токенизатором, используйте готовые скрипты. Но если вы:
- разрабатываете новую архитектуру и хотите потестировать её на реальном железе, а не только в PyTorch;
- хотите запустить старую модель (например, GPT-2 или BLOOM) локально без зависимостей;
- просто любите копаться в C++ коде llama.cpp, чтобы понять, как он работает «под капотом» —
этот подход станет вашим рабочим инструментом. А Laguna XS.2 — удобный «учебный стенд», потому что она маленькая (около 800M параметров) и быстро компилируется.
Весь код — в репозитории github.com/example/laguna-llamacpp. Там же — инструкция по запуску, Makefile с патчами и примеры вывода. Форкайте, экспериментируйте, добавляйте свои архитектуры. И помните: если что-то пошло не так, stack overflow и исходники llama.cpp — ваши лучшие друзья.