Технологии

Формат GGUF: что внутри файла модели и почему он победил

Разбор контейнера GGUF: заголовок, метаданные, тензоры и шаблон чата внутри одного файла — и почему именно шаблон решает половину проблем со странными ответами модели.

Типичная жалоба на форуме выглядит так: «скачал модель, она отвечает сама себе, дописывает мои реплики, вставляет какие-то теги». Модель при этом рабочая, железо в порядке, квантизация нормальная. Сломан один элемент — шаблон чата, и лежит он внутри файла модели.

Понять, почему так бывает, проще всего разобрав, что вообще лежит в файле GGUF. Это не архив с весами, это контейнер, в котором веса — только одна из четырёх частей.

#Короткий ответ

GGUF — однофайловый контейнер для запуска модели: метаданные архитектуры, токенизатор, шаблон диалога и сами тензоры в одном файле. Формат придуман в проекте llama.cpp взамен предыдущих GGML и GGJT и стал де-факто стандартом для локального запуска.

Победил он по одной прагматичной причине: чтобы запустить модель, нужен ровно один файл. Никаких config.json, tokenizer.json, generation_config.json, special_tokens_map.json рядом — всё внутри. Скачал, указал путь, работает.

#Как это устроено

Файл GGUF читается строго последовательно и состоит из четырёх частей.

[ Заголовок          ]  магия "GGUF", версия формата, счётчики
[ Метаданные (KV)    ]  архитектура, токенизатор, шаблон чата, всё остальное
[ Оглавление тензоров]  имя, форма, тип, смещение — по каждому тензору
[ Данные тензоров    ]  собственно веса, выровненные по границе блока

Заголовок — несколько десятков байт: четыре байта сигнатуры GGUF, номер версии формата, число тензоров и число записей метаданных. По сигнатуре инструменты и отличают GGUF от чего угодно другого.

Метаданные — список пар «ключ — значение» с типами. Здесь самое интересное. Типичный набор ключей:

Ключ Что означает
general.architecture семейство модели: llama, qwen2, gemma2, phi3
general.file_type уровень квантизации файла
{arch}.block_count число слоёв — то самое, что вы указываете в -ngl
{arch}.attention.head_count_kv число KV-голов, от него зависит расход памяти на контекст
{arch}.context_length максимальный контекст, на который модель обучена
{arch}.rope.freq_base база позиционного кодирования
tokenizer.ggml.model тип токенизатора
tokenizer.ggml.tokens сам словарь токенов
tokenizer.ggml.bos_token_id идентификаторы служебных токенов
tokenizer.chat_template шаблон, который превращает диалог в строку

Обратите внимание: почти всё, что нужно для расчёта требований к памяти, лежит прямо здесь. block_count и head_count_kv — это те самые числа из формулы KV-кэша, и их не надо искать на странице модели. Расход памяти на контекст считается именно по ним, и дамп метаданных отвечает на этот вопрос быстрее, чем страница модели.

Оглавление тензоров — по записи на каждый тензор: имя вида blk.17.attn_q.weight, размерность, тип квантизации и смещение в файле. Тип указан для каждого тензора отдельно — именно поэтому в K-квантах разные слои могут храниться с разной точностью, о чём подробнее в разборе квантизации LLM.

Данные тензоров — выровненный блок весов. Выравнивание нужно, чтобы файл можно было отобразить в память напрямую (mmap): движок не читает 5 гигабайт в оперативную память, а поручает это операционной системе, которая подгружает нужные страницы по требованию. Отсюда быстрый повторный запуск: второй раз файл уже в кэше страниц.

#Шаблон чата — самая недооценённая часть файла

Модель не понимает «диалог». Она видит одну строку текста и продолжает её. Превращение списка реплик в такую строку выполняет шаблон — Jinja-выражение, записанное в tokenizer.chat_template.

Для одной модели правильная строка выглядит так:

<|im_start|>system
Ты полезный ассистент.<|im_end|>
<|im_start|>user
Привет<|im_end|>
<|im_start|>assistant

Для другой — совершенно иначе, со своими маркерами и своим порядком блоков. Если применить чужой шаблон, модель получит текст в незнакомом ей формате. Она всё равно что-то ответит — обученная продолжать текст модель всегда что-то продолжает, — но поведение поедет: она начнёт дописывать реплики пользователя, игнорировать системный промпт, не останавливаться вовремя.

Именно поэтому шаблон положили внутрь файла. До GGUF его приходилось задавать вручную в каждом клиенте, и «странные ответы» были нормой.

Посмотреть, что за шаблон лежит в вашем файле, можно так (пакет gguf ставится через pip install gguf; имя скрипта между версиями менялось — сверьтесь с документацией пакета):

gguf-dump --no-tensors model.gguf

Для моделей, установленных через Ollama, шаблон видно короче:

ollama show --template qwen2.5:7b

#Что из этого следует на практике

Один файл — одна модель, и это не всегда удобно. Модели больше 40–50 ГБ разбивают на части с именами вида model-00001-of-00003.gguf. Указывать движку нужно первую часть — остальные он подтянет сам, но лежать они должны рядом.

Метаданные можно менять, не трогая веса. Если шаблон в файле неверный или устаревший, его переопределяют снаружи: в llama.cpp флагом --chat-template, в Ollama — директивой TEMPLATE в Modelfile. Как собрать такой пресет, показывает гайд по Modelfile в Ollama.

GGUF — формат для инференса, не для обучения. Дообучать модель в GGUF нельзя: квантованные веса не годятся для обратного распространения. Обучают в исходном формате, потом конвертируют. Порядок конвертации и загрузки результата разбирает материал о переводе модели в GGUF и запуске в Ollama.

Файл GGUF самодостаточен, но версионируется. Формат развивается, и старые файлы иногда перестают читаться новыми сборками — или наоборот, свежий файл требует свежего llama.cpp. Ошибка вида «unknown model architecture» почти всегда означает не битый файл, а устаревший движок.

#Где интуиция подводит

«GGUF — это формат квантизации». Нет. GGUF — контейнер, квантизация — то, как записаны тензоры внутри. В GGUF можно положить и неквантованные веса FP16, и это регулярно делают как промежуточный шаг перед квантизацией.

«GGUF и safetensors — конкуренты, надо выбрать». Они решают разные задачи и обычно сосуществуют. Safetensors — формат экосистемы обучения: только тензоры плюс минимальный заголовок, а конфигурация и токенизатор лежат отдельными файлами рядом. GGUF — формат доставки готовой модели пользователю. Разбор второго формата — в справке о safetensors.

«Если модель в GGUF, её запустит любой инструмент». Запустит любой, построенный на llama.cpp: сама llama.cpp, Ollama, LM Studio, Jan. Не запустят движки другой линии — например, vLLM работает с safetensors, и поддержка GGUF там ограниченная. Что даёт прямая работа с llama.cpp, показывает гайд по её сборке и параметрам.

«Метаданные — служебная информация, читать её незачем». Как раз наоборот: block_count подскажет, сколько слоёв выгружать, context_length — сколько контекста имеет смысл просить, head_count_kv — во что обойдётся длинный диалог. Одна команда дампа отвечает на три вопроса, которые иначе приходится гадать.

#Частые вопросы

#Чем GGUF отличается от GGML?

GGML — предыдущий формат того же проекта, без расширяемых метаданных: изменение структуры ломало совместимость, а токенизатор и параметры приходилось задавать снаружи. GGUF добавил типизированный блок «ключ — значение», поэтому новые поля можно добавлять, не ломая старые файлы. Файлы GGML сегодня считаются устаревшими.

#Можно ли открыть GGUF и посмотреть, что внутри, без запуска модели?

Да, дампом метаданных из пакета gguf с флагом отключения тензоров. Читается только заголовок и метаданные, гигабайты весов не трогаются, операция занимает доли секунды даже на файле в 40 ГБ.

#Почему модель отвечает от лица пользователя?

В подавляющем большинстве случаев — из-за несовпадения шаблона чата. Клиент применил не тот формат, модель не увидела ожидаемых маркеров конца реплики и продолжила текст дальше. Проверьте, какой шаблон реально используется, и при необходимости задайте его явно.

#Что делать, если движок пишет «unsupported model architecture»?

Обновить движок. Поле general.architecture содержит имя семейства, и поддержка нового семейства появляется в llama.cpp отдельным изменением кода. Файл при этом целый — просто ваша сборка о такой архитектуре ещё не знает.

#Занимает ли GGUF больше места, чем исходные веса?

При равной точности — примерно столько же, разница в единицы процентов за счёт метаданных и выравнивания. Экономия достигается не форматом, а квантизацией: тот же контейнер с четырёхбитными тензорами будет вчетверо меньше шестнадцатибитного.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт