DFlash2 против MTP: какое спекулятивное декодирование выбрать для Qwen 3.8 27B

Всем привет, с вами Низамов Илья. Спекулятивное декодирование — самый дешёвый способ ускорить локальную модель: ничего не квантуешь заново, качество ответа не трогаешь, просто добавляешь флаг. Методов спекуляции в vLLM целое семейство — от совсем дешёвого ngram, который обходится вообще без нейросети, до подключения отдельной модели-черновика. Я сравниваю два: MTP — основной режим для Qwen 3.8 27B, его голова едет прямо в чекпойнте модели, — и DFlash2, новый и пока экспериментальный, с отдельной черновой моделью из z-lab/Qwen3.8-27B-DFlash2. Оба прогнал на своём стенде из двух RTX 5070 Ti в кванте NVFP4 и сравнил на одинаковом потолке контекста.

Короткий ответ: DFlash2 быстрее на 25 %, и это честные 142.0 против 113.4 токенов в секунду. Но чтобы получить эти 25 %, пришлось урезать контекст с 168912 токенов до 8240 — в двадцать раз. На карте с 16 GB черновая модель на четыре миллиарда параметров в bf16 просто не оставляет места под KV-кэш.

Стенд тот же, что и в статье про пять конфигураций Qwen 3.8 27B: две RTX 5070 Ti по 16 GB (полезных 15.47 GiB на карту), Ryzen 5 3600, 76 GB оперативной памяти, Ubuntu 26.04, драйвер 595.71.05. Модель — unsloth/Qwen3.8-27B-NVFP4, тензорный параллелизм на две карты, KV-кэш в fp8.

Что такое спекулятивное декодирование и чем методы различаются

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

Различаются методы тем, кто и как пишет черновик. Два сравниваемых здесь расходятся принципиально.

MTP (multi-token prediction) — это голова, приделанная к самой модели. Один слой поверх последнего скрытого состояния, никакого отдельного KV-кэша, вес на карту 0.40 GiB. Она обучена предсказывать следующий токен и делает это хорошо: одна позиция принимается в 78–92 % случаев. Проблема начинается, когда голову просят написать блок подлиннее: она применяется несколько раз подряд к собственному же выходу, ошибка накапливается, и вторая позиция принимается уже в 18–46 %.

Как MTP ведёт себя на предыдущем поколении модели, я показывал в разборе Qwen 3.6 27B с MTP — там она удваивала скорость на llama.cpp.

DFlash2 идёт другим путём: рядом с большой моделью ставится маленькая самостоятельная, и пишет она сразу блок целиком.

Чем DFlash2 отличается от MTP?

Черновик — не голова, а полноценная модель со своей архитектурой DFlash2DraftModel. Вот что говорит её config.json:

num_hidden_layers                 = 5
hidden_size                       = 5120
intermediate_size                 = 17408
vocab_size                        = 248320
sliding_attention window          = 2048
dflash_config.block_size          = 8
dflash_config.selector_rank       = 256
dflash_config.selector_top_k      = 16
dflash_config.target_layer_ids    = [5, 19, 33, 47, 61]

Четыре вещи отсюда объясняют и её силу, и её цену.

Она читает таргет вглубь, а не только с конца. target_layer_ids — это пять уровней большой модели, с которых черновик забирает скрытые состояния. MTP видит только выход последнего слоя, то есть уже сжатую до одного вектора картину. DFlash2 получает срез на пяти глубинах — сигнала больше, и предсказание держится дальше по блоку.

Она пишет блок, а не токен за токеном. block_size 8 означает семь черновых токенов за один заход (седьмой — это num_speculative_tokens, то есть block_size − 1). Позиции не строятся рекурсивно одна на другой, как у многократно применённой головы MTP, — блок формируется сразу.

Кандидаты отбираются низкоранговым селектором. Словарь на 248320 токенов, честно считать по нему семь распределений было бы дороже самой экономии. selector_rank 256 и selector_top_k 16 — это узкое место, через которое пропускают по шестнадцать кандидатов на позицию. Так блок из семи токенов и остаётся дешёвым.

Ей не нужен весь контекст. sliding_attention с окном 2048: черновику хватает хвоста, он не обязан помнить всю переписку.

Расплата за всё это — размер. Пять слоёв по 5120, свои embed_tokens, свой lm_head на 248320 строк: чекпойнт весит 3.58 GiB, и он не квантован, это чистый bf16 рядом с четырёхбитным таргетом. Плюс собственный KV-кэш на пять слоёв. В логе это видно вторым предупреждением Add 4 padding layers, may waste at most 25.00% KV cache memory — у таргета своё, на 4.17 %.

Замеры: 142 против 113 токенов в секунду

Чтобы сравнение было честным, MTP я запустил не в его лучшей конфигурации, а в тех же условиях, в которых способен работать DFlash2: те же семь черновых токенов, тот же потолок контекста 8192, тот же квант, то же железо. Замеры — бенчмарком по методике версии 4, три прогона в серии, один запрос за раз.

DFlash2, 7 токеновMTP, 7 токеновРазница
Скорость одного запроса141.96 ток/сек113.38 ток/сек+25.2 %
То же со строгим форматом (JSON-схема)109.83 ток/сек94.00 ток/сек+16.8 %
Цена строгого формата−22.6 %−17.1 %
Потолок контекста в замере81928192
Идентификатор прогона20260830-13080620260830-132751

Двадцать пять процентов — это ощутимо: на ответе в тысячу токенов разница между двумя методами составляет примерно 1.8 секунды, 7.0 против 8.8. В чате с человеком за пультом такое видно глазом.

Одна оговорка про сборку движка, и её надо назвать прямо: версии vLLM в двух прогонах разные. DFlash2 в релиз ещё не приехал, метод живёт в пулл-реквесте, поэтому черновая модель крутилась на сборке 0.26.1rc1.dev1045+g3406ec1da из PR 52816, а MTP — на релизной 0.27.1. Часть разницы теоретически может приходиться на сам движок. Проверить это можно будет, когда dflash доедет до релиза; отдельного смысла гонять MTP на PR-ветке я не увидел.

Про строгий формат стоит сказать отдельно, потому что цифра неприятная. Ответ по JSON-схеме стоит DFlash2 почти 23 % скорости против 17 % у MTP — и это логично. Грамматика запрещает часть токенов на каждом шаге, а значит черновик, который эту грамматику не знает, промахивается чаще. Чем длиннее блок он пишет, тем больше выбрасывается. Так что если вся ваша нагрузка — это вызовы инструментов и структурированные ответы, преимущество DFlash2 сжимается почти вдвое.

Приёмка по позициям: где DFlash2 действительно сильнее

Скорость — это следствие, а причина видна в метриках спекуляции. vLLM печатает их прямо в лог:

SpecDecoding metrics: Mean acceptance length: 5.22, Avg draft acceptance rate: 60.3%
Per-position acceptance rate: 0.878, 0.755, 0.673, 0.571, 0.510, 0.469, 0.367
Accepted: 207 tokens, Drafted: 343 tokens

Читается это так. Средняя длина принятого блока — 5.22 токена: за один проход большой модели получается в пять с лишним раз больше текста, чем без спекуляции. Приёмка по позициям падает плавно, и даже седьмой токен берётся в 36.7 % случаев. У головы MTP на второй позиции уже 18–46 %, а третью и дальше в этих прогонах отдельно не мерили — но приёмка идёт цепочкой, и выше второй позиции хвост быть не может по построению.

Важно, что приёмка идёт цепочкой: позиция засчитывается только если приняты все, что стоят перед ней. Поэтому обвал в середине обесценивает весь хвост — и поэтому у MTP длинный блок почти не окупается, а у DFlash2 окупается.

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

Цена черновика в гигабайтах

Теперь про то, за что всё это платится. Строка Model loading took в логе — общий вес того, что легло на карту:

Без спекуляцииMTP, 1 токенDFlash2, 7 токенов
Веса на карту (Model loading took)10.24 GiB10.64 GiB12.45 GiB
Прибавка за спекуляцию+0.40 GiB+2.21 GiB
Осталось под KV (Available KV cache memory)3.31 GiB2.84 GiB0.96 GiB
Максимум контекста2621441689128240

Черновик DFlash2 дороже головы MTP в 5.5 раза по весам — и это ещё не всё, он вдобавок держит собственный кэш на пять слоёв. В сумме под контекст остаётся меньше гигабайта, и потолок падает до 8240 токенов. Ещё одна цифра из лога, которую легко пропустить: Maximum concurrency for 8240 tokens per request: 1.04x. Машина тянет ровно один запрос. Ни о каком обслуживании нескольких пользователей речи уже нет.

Итоговые команды запуска

DFlash2. Отдельное окружение обязательно — метода dflash в релизных сборках vLLM пока нет:

uv venv --python 3.12 .venv-dflash
source .venv-dflash/bin/activate
pip install -U "vllm @ git+https://github.com/vllm-project/vllm.git@refs/pull/52816/head"
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CUDA_DEVICE_ORDER=PCI_BUS_ID \
CUDA_VISIBLE_DEVICES=1,2 PYTHONUNBUFFERED=1 \
vllm serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000 \
  --tensor-parallel-size 2 \
  --language-model-only --mamba-ssm-cache-dtype bfloat16 \
  --max-num-seqs 2 --max-num-batched-tokens 1024 \
  --kv-cache-dtype fp8_e4m3 --gpu-memory-utilization 0.965 \
  --kv-cache-memory-bytes 640000000 \
  --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
  --enable-prompt-tokens-details \
  --speculative-config '{"method": "dflash", "model": "incoai/Qwen3.8-27B-DFlash2", "num_speculative_tokens": 7}' \
  --max-model-len 8192 \
  --chat-template /home/ilya/ai/qwen38-nvfp4-fixed.jinja

Два флага здесь неочевидны, и оба появились не сразу. --mamba-ssm-cache-dtype bfloat16 — единственный рычаг, который в этой связке реально дал память: доступный KV поднялся с 0.72 до 0.96 GiB, а заодно размер страницы внимания упал с 1648 до 880 токенов, что на кэше меньше гигабайта существенно само по себе. --kv-cache-memory-bytes 640000000 ставится не чтобы выжать больше кэша, а наоборот — чтобы отдать часть памяти обратно рантайму: без него KV-кэш выделялся успешно, а потом не влезал захват CUDA-графов.

MTP в тех же условиях — обычное релизное окружение, флагов заметно меньше:

PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True CUDA_DEVICE_ORDER=PCI_BUS_ID \
CUDA_VISIBLE_DEVICES=1,2 \
vllm serve unsloth/Qwen3.8-27B-NVFP4 --host 0.0.0.0 --port 8000 \
  --tensor-parallel-size 2 --gpu-memory-utilization 0.965 \
  --language-model-only --mamba-ssm-cache-dtype bfloat16 \
  --max-num-seqs 2 --max-num-batched-tokens 1024 \
  --kv-cache-dtype fp8_e4m3 \
  --speculative-config '{"method": "mtp", "num_speculative_tokens": 7}' \
  --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
  --enable-prompt-tokens-details --max-model-len 8192 \
  --chat-template /home/ilya/ai/qwen38-nvfp4-fixed.jinja

Про --chat-template в обеих командах: встроенный jinja-шаблон Qwen 3.8 отвечает 400 на системное сообщение в середине диалога, а без этого не живёт ни один агент. Разбор поломки и исправленный шаблон — в статье про запуск Claude Code на локальной модели, файл там один на оба движка.

Кому подойдёт DFlash2, а кому лучше MTP?

Считаем по-честному. DFlash2 даёт 142.0 токена в секунду на восьми тысячах контекста. MTP с одним черновым токеном даёт 84 токена в секунду на 168912 токенах контекста. Двадцать пять процентов скорости против двадцатикратного контекста — размен, в котором на этом железе выигрывает MTP почти при любой задаче.

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

Что могло бы поменять расклад:

  1. Квантованный черновик. 2.21 GiB на карту в bf16 против примерно 0.6 GiB в fp8 или nvfp4 вернули бы порядка 1.6 GiB под кэш — это примерно втрое-вчетверо по контексту. Готового квантованного Qwen3.8-27B-DFlash2 пока нет, и это главный рычаг.
  2. Меньше черновых токенов. Буферы спекуляции масштабируются от произведения размера пачки на 1 + num_speculative_tokens. Три токена вместо семи сэкономили бы часть буферов ценой приёмки. Я не замерял.
  3. Четырёхбитный KV-кэш. Даёт почти двукратный выигрыш по контексту, но со спекуляцией MTP он молча генерировал мусор. С dflash не проверено, и проверять надо текстом ответа, а не долей принятых токенов — на сломанной связке она была выше, чем на исправной.
  4. Карта побольше. На 24 GB черновик в 2.21 GiB перестаёт быть проблемой, и весь этот разбор теряет смысл: там DFlash2 просто выигрывает.

Оба прогона лежат в общем разделе бенчмарков рядом с остальными конфигурациями этого же стенда, так что сравнивать можно построчно. Соседние замеры на том же железе — в статьях про пять конфигураций Qwen 3.8 27B и про Qwen 3.8 Flash Next на llama.cpp; про выбор самого движка — в сравнении vLLM и llama.cpp, про выбор карт — в RTX 5070 Ti против RTX 3090.

Что осталось непроверенным

Честный список, чтобы никого не вводить в заблуждение:

  • Влияние версии движка. DFlash2 гонялся на PR-сборке, MTP — на релизе. Какая доля из 25 % приходится на сам метод, а какая на изменения между версиями, я не знаю.
  • Длинный промпт. Контекста хватает на восемь тысяч токенов, поэтому деградацию скорости по длине на DFlash2 замерить не на чем.
  • Конкурентные запросы. При параллельности 1.04 их просто нет.
  • Длинная генерация. Проверял на ответах в пределах нескольких сотен токенов.
  • Минимальный безопасный --kv-cache-memory-bytes. 640000000 подобрано с запасом, где проходит настоящая граница — не выяснял.

Локальная модель сама по себе — это ещё не результат, а фундамент. Польза появляется, когда она встроена в рабочий процесс: читает документы, отвечает клиентам, заполняет данные в учётной системе. Как довести дело до работающей интеграции — в курсе «Применение искусственного интеллекта ChatGPT для 1С»: от локального движка до агента внутри вашей 1С.

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