GGUF в transformers: кванты llama.cpp теперь нативно
Запустить GGUF-модель до сих пор означало выйти из Python. Ollama, LM Studio и Jan построены вокруг движка llama.cpp, который умеет читать квантованные веса. Hugging Face убирает эту границу: GGUF-чекпойнт теперь загружается прямо в transformers через обычный from_pretrained, и генерация идёт на вашем Mac без второго рантайма.
Что такое GGUF
GGUF, это формат от команды llama.cpp, который упаковывает веса модели, токенизатор и опциональный chat template в один файл. Главная его особенность, уровни квантования: вариант Q4_K_M держит большинство тензоров в 4 битах, а чувствительные слои оставляет в более высокой точности.
Упаковка в один файл это не мелочь: конфиг, веса и токенизатор больше не нужно собирать по кусочкам, а сам файл легко переносить между машинами и приложениями. Именно поэтому GGUF стал стандартом де-факто для локального инференса, а тысячи готовых квантов лежат на Hugging Face у Unsloth, bartowski и LM Studio Community.
Загружать GGUF в transformers умели и раньше, но загрузчик разворачивал веса в полную точность. Экономия памяти испарялась, и смысл формата пропадал. Новый путь оставляет веса упакованными и переиспользует ядра llama.cpp.
Насколько квантование экономит память, видно на Unsloth Qwen3.5-4B:
| Вариант GGUF | Размер файла | Компромисс |
|---|---|---|
| BF16 | 8,42 ГБ | Эталон без квантования |
| Q6_K | 3,53 ГБ | Точнее компактных вариантов |
| Q5_K_M | 3,14 ГБ | Середина между размером и точностью |
| Q4_K_M | 2,74 ГБ | Практичная отправная точка |
Разница между эталоном и Q4_K_M больше чем в три раза: 8,42 против 2,74 ГБ. Авторы советуют начинать с Q4_K_M, а при запасе памяти пробовать Q5_K_M или Q6_K. Квантовать агрессивнее стоит ради того, чтобы влезла модель побольше: качество зависит от задачи, так что проверять эффект нужно на своих сценариях.
Как загрузить
Для начала нужны Apple Silicon Mac, свежий PyTorch из поддерживаемых сборок ggml-ядер и актуальные версии transformers с kernels. Сборки ядер собираются под конкретные релизы PyTorch, обычно доступны две последние, так что сильно отставать от апдейтов не выйдет.
pip install -U "git+https://github.com/huggingface/transformers.git" kernels
Дальше вся интеграция сводится к одному аргументу, gguf_file:
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "unsloth/Qwen3.5-4B-GGUF"
filename = "Qwen3.5-4B-Q4_K_M.gguf"
tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=filename)
model = AutoModelForCausalLM.from_pretrained(model_id, gguf_file=filename)
Всё остальное, как с обычной моделью: apply_chat_template, generate. Если веса остаются упакованными на Metal, transformers сам подхватывает совместимые ggml-ядра и использует ggml-attn как реализацию внимания. Если ядро не подтянется, загрузчик развернёт веса с предупреждением, и модель займёт заметно больше памяти. Форсировать запасной путь можно явно через attn_implementation="sdpa".
Для серверного сценария есть transformers serve с OpenAI-совместимым API:
transformers serve "unsloth/Qwen3.5-4B-GGUF:Qwen3.5-4B-Q4_K_M.gguf"
Идентификатор в формате repo:file выбирает конкретный квант из репозитория, где их может быть несколько. Подключить клиент вроде Jan или Pi можно как обычный OpenAI-провайдер: Base URL http://localhost:8000/v1 и тот же model id. Счёт идёт на вашем Mac, а интерфейс рисует клиент. Это удобно, когда приложение уже умеет говорить с OpenAI API: менять код не придётся, достаточно подменить base URL. Типичный сценарий, локальный сервер для экспериментов, когда облако не подходит по приватности или цене.
Почему это быстро
Скорость не появилась сама собой. Hugging Face не стала писать собственные ядра, а переиспользовала ggml-ядра llama.cpp и раздаёт их через библиотеку kernels. Библиотека решает старую боль PyTorch-экосистемы: нативные расширения обычно собираются на машине пользователя, а тут готовые сборки под конкретную платформу раздаются с Hub и подтягиваются по требованию. Пользователь получает одну зависимость в requirements, а мейнтейнеры один канал доставки бинарников.
Ядро, это маленькая программа для GPU; специализированное ядро делает меньше работы, склеивает несколько операций или читает квантованные веса прямо в упакованном виде.
| Ядро | Что делает |
|---|---|
| ggml-quantization | Читает упакованные квантованные веса для матричных операций, включая выбранных экспертов в MoE-модели |
| ggml-norm | Фьюзит нормализацию, включая zero-centered RMSNorm из Qwen3.5 и Qwen3.8 |
| ggml-attn | Metal-реализация flash attention для обработки промпта и декодирования |
| ggml-gated-delta-net | Ускоряет gated delta net в линейных слоях внимания гибридных архитектур Qwen |
| topk | Выбирает экспертов для каждого токена, объединяя softmax и top-k роутинг |
Главное из этого набора, ggml-quantization. Ядро не разворачивает всю матрицу весов перед каждым шагом декодирования, а читает её как есть. В K-квантах тензор делится на суперблоки, и у каждого свои масштабы, поэтому веса нельзя привести к float одной операцией: распаковка идёт по ходу вычисления, внутри матричного умножения. Специализированное ядро делает это на лету и не тратит ни лишней памяти, ни отдельного прохода по данным.
Для MoE-моделей это критично, ведь на каждый токен подгружаются только выбранные эксперты. Дело не только в памяти: локальный инференс упирается в пропускную способность памяти, поэтому чем меньше байтов гоняется между памятью и чипом, тем выше скорость.
Вторая половина ускорения живёт в самом generate, и она помогает всем моделям transformers, а не только GGUF-файлам. Ненужная маска внимания отбрасывается в начале генерации (PR #48814), а проверка условия остановки копируется асинхронно и потребляется на следующем шаге (PR #47975). Проще говоря, CPU перестаёт ждать GPU на каждом токене.
Почему первой платформой стал Apple Silicon? У ggml годами оттачивались Metal-ядра под чипы Apple, а MacBook с объединённой памятью, пожалуй, самый массовый класс машин, где модели запускают локально: десятки гигабайт под веса доступны без дискретной видеокарты. Для авторов это ещё и удобная точка старта: единая платформа Metal вместо россыпи конфигураций CUDA, на которых пришлось бы выверять каждое ядро.
Бенчмарки против llama.cpp
Авторы сравнили три чекпойнта (маленькую dense-модель, крупную dense и MoE) с результатами llama-bench на MacBook Pro M2 Max с 32 ГБ памяти. Картина такая: transformers близок к llama.cpp на всех трёх. Авторы сами оговариваются, что условия не идентичны, замер transformers включает prefill, а llama-bench сообщает только скорость декодирования. Так что это сравнение порядка, а не соревнование на миллисекунды.
Практический смысл важнее самих цифр. В интерактивной работе решает не абсолютная скорость, а ровность потока: пока токены идут без пауз, разница в несколько процентов не ощущается, а заминки замечаются сразу.
Две детали выдают серьёзность замеров. Всё работает в eager-режиме, без torch.compile, то есть без пауз на компиляцию и рекомпиляцию при смене форм входа. А в скрипте бенчмарка есть пауза на остывание машины: подряд идущие прогоны теряют больше 10% из-за троттлинга.
Что с этим можно делать
Хуки на промежуточные активации, правки forward-прохода и прототипы своих слоёв теперь работают и с квантованными чекпойнтами. Оценка качества квантов идёт привычными eval-пайплайнами transformers. Разработчикам это даёт простой способ проверять корректность GGUF-конверсий: загрузить оригинал и его квант в одном окружении и сравнить. Ещё можно писать кастомные logits processors и stopping criteria прямо в generate. И наконец, из GGUF можно дообучать: GgufConfig(dequantize=True) разворачивает веса, и дальше идёт обычный training workflow.
А есть и ставка побольше: ggml-ядра полезны не только для GGUF-файлов. Ядро работает с тензорами и не требует, чтобы модель целиком приехала из GGUF. Это открывает путь к ускорению моделей, которых в llama.cpp нет: новых архитектур, исследовательских и кастомных вариантов. У transformers уже есть их PyTorch-реализации, к которым можно подключать совместимые ядра без полного порта в llama.cpp. Та же логика применима к компьютерному зрению, аудио и мультимодальным моделям: внимание, нормализация и матричные умножения у них устроены похоже.
Зачем это Hugging Face
Мотив компании понятен: transformers должен оставаться универсальным слоем описания моделей, а llama.cpp остаётся лучшим рантаймом для локального запуска. Проекты не конкурируют, а склеиваются: ggml-ядра теперь распространяются через Hub как обычные артефакты, а transformers получает скорость без второго рантайма. Пользователю это даёт меньше развилок: один формат файлов, один способ загрузки, а рантайм выбирается по задаче.
Ограничения
Упакованный путь пока работает только на Apple Silicon через MPS. Поддержка формата не означает, что ядра доступны на любом железе: на других платформах остаётся путь через разворачивание весов, а он занимает больше памяти.
Покрыты архитектуры Qwen3.5 dense и MoE, включая совместимые чекпойнты Qwen3.8. Остальные обещают добавлять постепенно, приоритеты расставляют по issue с реальными чекпойнтами.
С паддингом и батчингом ещё есть работа: непаддированные входы получают оптимизацию с маской, а паддированные батчи могут тормозить. generate_batch на MPS пока в планах.
Общая картина такая: поддержка GGUF это первый шаг, а цель шире, ускорять через ggml-ядра любые модели transformers, включая те, что в llama.cpp не появятся. Так что список ограничений со временем должен сокращаться.
Часто задаваемые вопросы
Нужно ли теперь отказываться от llama.cpp?
Нет. llama.cpp остаётся рекомендованным движком для эффективного локального инференса: у него собственный рантайм, управление памятью и широкая поддержка железа. Интеграция решает другую задачу: работать с теми же GGUF-чекпойнтами внутри Python и PyTorch, а не заменять llama.cpp целиком.
Работает ли это на NVIDIA и Windows?
Пока нет. Упакованные ggml-ядра задействуются только на Apple Silicon через Metal. На остальных платформах доступен путь через разворачивание весов: он занимает больше памяти, и о близкой к llama.cpp скорости речи не идёт.
Чем Q4_K_M отличается от старых Q4_0 и IQ-квантов?
Буква K означает K-кванты: тензор делится на суперблоки, и у каждого свои масштабы. При похожем размере это даёт меньше потерь качества, чем старые схемы вроде Q4_0. Варианты IQ жмут ещё сильнее, но подбираются под конкретную модель. Если не хочется разбираться, берите Q4_K_M: это компромисс, который выбирает большинство.
Можно ли дообучить модель из GGUF?
Да. Передайте GgufConfig(dequantize=True) вместе с dtype=torch.bfloat16, веса развернутся в полную точность, и дальше можно запускать стандартный training-цикл transformers.
Итог
GGUF перестал быть форматом одного движка. Любой чекпойнт из экосистемы llama.cpp теперь открывается в transformers одной строкой gguf_file=..., скорость на Apple Silicon близка к llama.cpp, и всё это без выхода из Python. Если хочется покрутить квантованную модель в коде, а не в чат-клиенте, ставьте kernels, берите Q4_K_M и пробуйте на своих задачах. А если вашей архитектуры в списке ещё нет, авторы просят открыть issue с чекпойнтом.