Перейти к содержанию
Публикация AiManual

CUDA graph для MTP в llama.cpp: что даёт pull request #28549 и как это ускоряет локальный инференс

В репозитории ggml-org/llama.cpp появился pull request #28549 с поддержкой CUDA graph для MTP draft. Разбираем, как speculative decoding и multi-token predictio

Коротко

Что будет в материале

  1. 01

    Что такое MTP и speculative decoding в llama.cpp

  2. 02

    Зачем в llama.cpp применяют CUDA graph

  3. 03

    Что известно о pull request #28549

  4. 04

    Как это может ускорить локальный инференс

В репозитории ggml-org/llama.cpp появился pull request #28549 от разработчика gaugarg-nv: он добавляет поддержку CUDA graph для MTP draft. Описание изменения укладывается в короткую фразу «one more MTP speedup», без деталей и замеров. Практический смысл в том, что speculative decoding на NVIDIA GPU упирается в накладные расходы на запуск множества мелких ядер, а MTP-драфтинг состоит именно из мелких шагов, которые CUDA graph и позволяет склеить.

Что известно точно: PR открыт в публичном репозитории, автор работает с CUDA, тема связана с ускорением MTP. Что неизвестно: войдёт ли код в основную ветку, как он поведёт себя на конкретном железе, какие компромиссы за собой потянет. Любые цифры ускорения до официальных бенчмарков приводить нельзя, поэтому дальше разбор механики и способов проверить эффект самому.

Что такое MTP и speculative decoding в llama.cpp

Speculative decoding меняет порядок работы генератора. Вместо схемы «один шаг модели, один токен» движок сначала получает несколько токенов-кандидатов дешёвым способом, а потом проверяет их большой моделью за один проход. В llama.cpp это работает и с отдельной маленькой draft-моделью, и с MTP-головой внутри самой модели, если она обучена предсказывать несколько токенов вперёд.

Как работает speculative decoding: draft-модель и target-модель

Схема из двух участников. Target-модель, то есть основная модель, задаёт качество. Draft-модель генерирует K токенов подряд обычным авторегрессионным способом, но она маленькая и быстрая, поэтому K её шагов стоят дешевле одного шага большой модели. Затем target-модель делает один forward pass по всей последовательности из K кандидатов и получает вероятности для каждой позиции. Дальше идёт проверка: токен принимается, если большая модель согласна с кандидатом, и цепочка обрывается на первом расхождении. Расхождение на первом токене означает, что вы потратили вычисления драфтера и один проход target-модели, а получили один токен. Приняты все четыре из четырёх, за один проход получено четыре токена.

При greedy-декодировании проверка сводится к сравнению argmax. При сэмплировании работает схема с проверкой отношения вероятностей, которая сохраняет распределение target-модели, поэтому качество текста не деградирует. Ключевая метрика здесь - acceptance rate, доля принятых кандидатов. Она зависит от задачи и драфтера: код, списки, JSON и шаблонные формулировки принимаются заметно чаще, чем свободные рассуждения. Подробнее эта зависимость разобрана в материале о том, как speculative decoding и MTP ускоряют генерацию на предсказуемых фразах.

Экономика процесса не обещает чуда. Выигрыш появляется, когда принятых токенов достаточно, чтобы окупить проходы драфтера и проверку. Если acceptance rate низкий, генерация становится медленнее, чем без спекуляции, и это не баг, а свойство метода.

Роль MTP в ускорении генерации

MTP (multi-token prediction) - обучение модели предсказывать сразу несколько следующих токенов. Модели с MTP несут в GGUF дополнительные тензоры NextN/MTP: отдельный блок, который по скрытому состоянию предсказывает следующий-за-следующим токен. Для speculative decoding это удобно: вторая модель не нужна, драфтер уже внутри файла. В llama.cpp такой режим включается типом спекулятивного декодирования draft-mtp. Тензоры такого рода встречаются у моделей семейства GLM, у DeepSeek-подобных архитектур и у части MoE-вариантов Qwen; разбор запуска одной из них есть в статье о поддержке спекулятивного декодирования NextN/MTP для GLM-5.2.

Плата за это - мелкие шаги. MTP-голова обрабатывает один-два токена за шаг, attention работает по короткому KV-кэшу, следом идут argmax, сэмплирование и небольшие копирования данных. Каждое такое действие - отдельное ядро CUDA. Когда ядро исполняется быстрее, чем запускается, время уходит не на вычисления, а на их организацию.

Зачем в llama.cpp применяют CUDA graph

CUDA graph (появился в CUDA 10.0) позволяет записать последовательность операций и зависимости между ними в граф, один раз его инстанцировать и дальше запускать целиком одной командой с хоста. Вместо сотен вызовов запуска ядер на шаг генерации - один replay.

Проблема накладных расходов на запуск ядер

Запуск ядра с хоста стоит единицы микросекунд: подготовка аргументов, постановка в очередь, работа с драйвером. Для крупных ядер это незаметно. Для MTP-драфта картина другая: ядра короткие, каждое длится меньше, чем занимает его запуск. GPU простаивает между командами, процессор занят обслуживанием очереди. Поэтому speculative decoding на слабом драфтере иногда не ускоряет, а замедляет генерацию: накладные расходы съедают выигрыш от принятых токенов.

Как CUDA graph сокращает overhead

Граф фиксирует всю цепочку: какие ядра и в каком порядке идут, от каких буферов зависят. При replay драйверу не нужно заново разбирать каждую команду, паузы между ядрами сокращаются, процессор освобождается. Ограничение важное: граф привязан к формам тензоров и адресам памяти, поэтому при смене размера батча или перераспределении буферов его приходится перехватывать. В инференсе это решают пулами памяти фиксированного размера и отдельным графом на каждую форму. Каждый захват стоит времени и небольшого объёма памяти, так что при формах, меняющихся на каждом шаге, выигрыш исчезает.

CUDA graph работает на GPU с compute capability 3.5 и выше, но часть сценариев захвата требует свежих драйверов. В llama.cpp этот механизм применяется и сейчас, поведение зависит от версии сборки и переменных окружения, поэтому смотрите вывод --help и README своей сборки. Отдельная линия работ - слияние операций (fusion), которое уменьшает само число ядер; об этом мы писали в разборе расширения CUDA fusion для MoE и specdec.

Что известно о pull request #28549

Фактов немного, и лучше их не додумывать.

Автор и контекст появления PR

Pull request #28549 открыт в репозитории ggml-org/llama.cpp, автор - разработчик под ником gaugarg-nv. Изменение относится к MTP draft и CUDA graph, описание сводится к фразе «one more MTP speedup». Новость пришла из обсуждения на Reddit, где сам факт появления PR заметили раньше, чем в нём появились подробности. Ни кода, ни замеров, ни итогов ревью в публичном описании на момент написания нет.

Статус pull request: что это значит на практике

Открытый PR - это предложение, а не готовая функция. Код может быть принят, переписан после замечаний мейнтейнеров или закрыт без слияния. Пока изменение не попало в основную ветку и релизы, использовать его можно только из ветки автора, со всеми рисками: конфликты при сборке, нестабильность, отсутствие гарантий совместимости с вашими моделями. Строить рабочие конфигурации на этом рано, отслеживать обсуждение - разумно.

На что смотреть в самом PR, когда появятся детали: какие файлы затронуты (только MTP-путь или общий CUDA-бэкенд), как сделан захват графа для динамических форм, есть ли тесты и замеры на разных GPU, что говорят мейнтейнеры про накладные расходы на перехват. Без этих пунктов любые оценки остаются предположениями.

Как это может ускорить локальный инференс

Если CUDA graph для MTP draft попадёт в основную ветку, draft-путь перестанет быть набором мелких отдельных запусков и превратится в один replay. Потенциально это поднимет tokens/s при том же acceptance rate. Слово «потенциально» здесь ключевое: подтверждённых цифр нет.

От чего зависит реальный прирост

  • Архитектура GPU и драйвер. CUDA graph работает на NVIDIA, но выгода зависит от того, насколько конкретная карта чувствительна к задержкам запуска ядер.
  • Длина драфта. Больше кандидатов за шаг - больше мелких операций, которые можно склеить в граф.
  • Acceptance rate. Чем чаще target-модель соглашается с драфтером, тем выше шанс превратить экономию на накладных расходах в реальные токены в секунду.
  • Размер батча и контекста. Там, где GPU уже загружен вычислениями, склейка запусков даст меньше.
  • Стабильность форм тензоров. Частый перехват графа добавляет накладные расходы и может съесть выигрыш.
  • Сборка. Разные версии llama.cpp по-разному работают с CUDA graph, поэтому сравнение корректно только внутри одной сборки и одного набора параметров.

Кому это будет полезно в первую очередь

Тем, кто запускает LLM локально на NVIDIA GPU и уже использует speculative decoding или MTP-драфтинг. Особенно если на каждый токен приходится много мелких шагов, а GPU простаивает между ядрами. Пользователям CPU-сборок, владельцам GPU других производителей и тем, у кого модели без MTP-тензоров, изменение не даст ничего. Если вы только собираете конфигурацию под ограниченную VRAM, практические примеры есть в статье о запуске Qwen 3.8 27B на 16 ГБ VRAM.

Как включить speculative decoding в llama.cpp сейчас

MTP-драфтинг доступен и без этого PR. Набор параметров зависит от версии, поэтому первым шагом смотрите --help своей сборки.

Основные флаги и параметры

Для классического варианта с отдельной draft-моделью используется --draft (или -md) с путём к GGUF драфтера, число кандидатов задают --draft-max и --draft-min, в части сборок есть порог вероятности --draft-p-min. Для моделей со встроенной MTP-головой применяется тип спекулятивного декодирования, например --spec-type draft-mtp, а длину драфта ограничивает --spec-draft-n-max; в новых версиях встречаются адаптивные режимы вида --spec-draft-adaptive. Пример запуска:

llama-server -m models/model-Q4_K_M.gguf \
  --spec-type draft-mtp \
  --spec-draft-n-max 4 \
  -ngl 99 -c 16384 -fa on

Названия флагов и их поведение менялись, поэтому строку из примера нужно сверить с --help. Как эти параметры вместе с batch-size, ubatch-size, KV-кэшем, flash-attn и tensor-split влияют на токены в секунду, разобрано в статье о настройках llama-server и нормальной скорости MTP.

Ограничения и подводные камни

  • Драфтер ошибается. При низком acceptance rate генерация идёт медленнее, чем без speculative decoding.
  • Память. MTP-тензоры внутри GGUF занимают дополнительный объём VRAM или RAM, даже если вы просто держите модель загруженной; отдельная draft-модель добавляет свои веса и собственный KV-кэш.
  • Поддержка зависит от модели. MTP-голова есть не у каждого GGUF, а совместимость отдельных архитектур с драфтингом в llama.cpp менялась от версии к версии.
  • CUDA graph для MTP пока не в основной ветке. В стабильных сборках на него рассчитывать не стоит.
  • Мерить нужно на своей задаче. Один и тот же набор флагов даёт разный результат на коде, чате и длинных рассуждениях.

Что это значит для экосистемы локального инференса

llama.cpp годами держится на компромиссе: широкий охват моделей, работа на CPU, AMD, Apple Silicon и NVIDIA, запуск из командной строки без обвязки. CUDA graph и fusion-операции двигают проект в сторону более тонкой работы с NVIDIA, где vLLM и TensorRT-LLM давно используют похожие приёмы. Для пользователя это сигнал о направлении развития: узкие места смещаются с чистых вычислений на накладные расходы по их организации.

Практический план на ближайшее время: не ждать PR как готовой функции, проверить текущие возможности MTP и speculative decoding на своей модели, зафиксировать baseline (tokens/s, acceptance rate, потребление VRAM), а появление #28549 в основной ветке или в релизах оценивать уже на сравнении с этим baseline. Так вы получите ответ про своё железо, без чужих цифр.

Подписаться на канал