Типичная жалоба на форуме выглядит так: «скачал модель, она отвечает сама себе, дописывает мои реплики, вставляет какие-то теги». Модель при этом рабочая, железо в порядке, квантизация нормальная. Сломан один элемент — шаблон чата, и лежит он внутри файла модели.
Понять, почему так бывает, проще всего разобрав, что вообще лежит в файле 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 больше места, чем исходные веса?
При равной точности — примерно столько же, разница в единицы процентов за счёт метаданных и выравнивания. Экономия достигается не форматом, а квантизацией: тот же контейнер с четырёхбитными тензорами будет вчетверо меньше шестнадцатибитного.