← На главную

Опубликовано

DFlash 2: ускоряет код в 1.8 раза и роняет документы в 0.25 - где проходит граница

Автор: Гусев Николай [портфолио]

Обновлено

Темы: llm, benchmarks, inference, speculative-decoding

Автор: Гусев Николай. Серия про спекулятивное декодирование на 8 ГБ: часть 1 - общая картина, часть 3 - EAGLE-3, часть 4 - 96K и Lucebox.

DFlash 2 - блочный диффузионный драфтер от Z Lab. Заявление: 2-3.4× на детерминированных задачах - код, JSON, математика. Замер на моей 4060 это подтвердил ровно наполовину, и вторая половина интереснее первой.

Где он работает

Корпус - 11 детерминированных задач (JSON с точным сравнением, код, счёт), таргет Qwen3.5-4B Q4_K_M, драфтер AtomicChat DFlash Q8_0, greedy. Сравнение с baseline той же сессии, 3 повтора на точку, медианы.

КонфигурацияЗадач/минК baselineAcceptance
Baseline59.41.00-
DFlash n=4, серийно105.41.77×0.81
DFlash n=4, 4 параллельных111.91.81×0.81
DFlash n=15, серийно104.81.76×0.36
DFlash n=15, 4 параллельных115.91.87×0.36

На детерминированном классе DFlash работает именно так, как обещано: ~1.8× и серийно, и под нагрузкой, без деградации. Acceptance 0.81 - модель угадывает 4 из 5 драфт-токенов, драфт практически бесплатный.

Любопытно, что n=15 с acceptance 0.36 не проигрывает n=4 с acceptance 0.81: меньше acceptance компенсируется большим окном. Но это на коде.

Тонкая настройка: p_min-порог (принимать драфт-блок только если модель уверена минимум на p) поднимает acceptance с 0.36 до 0.89 при p_min=0.9 - ценой отката до 1.56×. Регулятор рабочий, но не волшебный.

Где он роняет

Второй корпус - 33 задачи реального сервиса: 11 документов ~3.3K токенов, 11 tool-вызовов, 11 роутингов. Тот же таргет, тот же драфтер.

РежимЗадач/минК baselineAcceptance
Baseline серийно32.81.00-
DFlash серийно18.20.55×0.17
Baseline под нагрузкой48.11.00-
DFlash под нагрузкой11.40.25×0.06

Не «нет выигрыша» - уверенное замедление. Драфт предлагает, таргет отвергает, за каждый сгенерированный токен платим проверкой трёх-четырёх непринятых. Под нагрузкой acceptance падает ещё втрое, и метод ест GPU вчетверо быстрее.

Граница: факторный эксперимент

Что именно рушит acceptance - длина промпта, параллелизм или содержание задачи? Собрал различающий эксперимент: один и тот же детерминированный корпус в двух вариантах (короткий ~60 токенов и длинный ~3.3K с архивным филлером), обе длины серийно и под нагрузкой, плюс прямой замер acceptance на документах.

Корпусc=1c=4Коллапс
Код, короткие задачи0.36450.3645нет
Код с 3.3K филлером0.33550.3355нет
Документы (NL-анализ)0.17140.0611да, -64%

Три факта, каждый из которых убивает по одной гипотезе:

  1. Параллелизм сам по себе acceptance не трогает - на коротких и длинных детерминированных задачах цифры совпадают до четвёртого знака.
  2. Длина промпта - мягкий фактор: -8% серийно, и всё.
  3. Коллапс требует сочетания: естественно-языковая генерация поверх длинного контекста плюс батчинг. Ни один фактор по отдельности его не воспроизводит.

Это сходится с AgentSpec - там деградация спекуляции под ростом батча объясняется ростом rejection rate драфта. И с PARD-2 - objective драфтера должен быть длиной непрерывного принятия, а не token accuracy. Точный механизм - интерливинг verify-слотов, зависание блока на EOS-переходах - остаётся открытым. Граница найдена, причина - нет. Я так и не говорю «разобрались», это не разбора статья.

Что именно делает DFlash

Обычный драфтер пытается идти токен за токеном. DFlash предлагает блок продолжения и использует таргет для проверки этого блока. На бумаге это похоже на обычную speculative decoding, но инженерно меняется профиль ошибки: главный показатель здесь - длина непрерывного участка, который таргет согласится принять.

Упрощённо цикл выглядит так:

1. draft строит блок из n токенов
2. target проверяет блок одним или несколькими проходами
3. принимаем префикс до первого несовпадения
4. отклонённый хвост строится заново
5. измеряем: предложено, принято, время

Параметр n_max задаёт ширину блока. Мы специально прогнали n=4, n=8 и n=15, потому что максимальная ширина из рекламного примера не обязана быть оптимальной для конкретного корпуса.

git clone https://github.com/NikolayGusev-astra/poc-node-utilization.git
cd poc-node-utilization
python validation-v3/r2b_nmax_sweep.py

Скрипт не принимает аргументов: n_max берётся из жёсткого набора [4, 8, 15], таргет и драфт зафиксированы под Qwen3-4B + DFlash, повторы = 2. Он импортирует общий харнесс из run_spec_matrix.py и run_validated.py, сам создаёт сессию в measurements/r2b-nmax-sweep-<timestamp>/, прогоняет три ширины блока и сохраняет сырые логи и JSON-результаты. Для запуска на другом компьютере нужны локально скачанные Qwen3-4B-Q4_K_M.gguf, AtomicChat-Qwen3-4B-DFlash.Q8_0.gguf и llama-server.exe — пути к ним читаются из run_config.json. Полный файл: r2b_nmax_sweep.py.

Почему код и документы надо разделять

В коде и JSON ответ часто имеет жёсткую локальную структуру: после "language": с большой вероятностью идёт допустимое значение, после открывающей скобки - ключ или закрытие. В свободном анализе на русском языке распределение продолжений шире: модель может начать пояснение, сменить синтаксический ритм, закончить абзац или перейти к оговорке.

DFlash не знает, что для пользователя «похожее начало» ещё не является полезным ответом. Таргет проверяет конкретные токены. Поэтому мы не переносили цифру с code-корпуса на doc-корпус, а построили два независимых набора.

Детерминированная часть состояла из 11 задач: код, JSON и счёт. Для каждой был строгий проверяющий код. Документная часть состояла из 33 задач сервиса: документы, tool-вызовы и маршрутизация. В ней сохранялся тот же таргет и тот же драфтер, но появлялось естественно-языковое продолжение и длинный вход.

n_max: почему больше не всегда лучше

На code-корпусе n=4 дал acceptance 0.4307 в отдельном sweep, n=8 - 0.2803, n=15 - 0.1798. Это не означает, что n=15 автоматически медленнее: большой блок иногда компенсирует низкий acceptance. В нашей полной code-серии n=15 всё ещё оказался около 1.8×.

Но acceptance на токен и скорость - разные показатели. Если смотреть только на acceptance, можно выбрать слишком короткий блок. Если смотреть только на requests/min, можно не заметить, что запас метода держится на случайной структуре конкретного корпуса.

Попробовали также p_min: принимать только достаточно уверенные предложения. На code-корпусе p_min=0.9 поднял acceptance до 0.89, но скорость снизилась примерно до 1.56×. Это полезный регулятор для роутера, но не универсальный фикс: за высокую уверенность платим дополнительными откатами.

Различающий эксперимент

Оставалось понять, что именно вызывает провал на документах: длина контекста, параллельность или содержание. Мы сделали минимальный факторный эксперимент. Для одной и той же детерминированной задачи подготовили короткую версию примерно на 60 токенов и длинную примерно на 3.3K токенов. Филлер был фиксированным и не содержал ответа.

for task in code_tasks:
    short = task.prompt
    long = task.prompt + filler_to_target_tokens(task.prompt, 3300)
    emit(task.id, "short", short)
    emit(task.id, "long", long)

Затем добавили настоящий doc-корпус и измерили каждую группу при c=1 и c=4:

Корпусc=1c=4
короткий код0.36450.3645
код с длинным филлером0.33550.3355
естественно-языковые документы0.17140.0611

Вывод аккуратнее популярной фразы «batch ломает speculative decoding». Batch сам по себе на наших детерминированных задачах ничего не сломал. Длина сама по себе тоже дала только мягкое снижение. Коллапс появился в сочетании длинного естественно-языкового входа и параллельной подачи.

Это согласуется с AgentSpec, где обсуждаются rejection rate и недоиспользование динамического бюджета при росте batch. PARD-2 объясняет, почему token accuracy драфтера не равна длине принятого фрагмента. Но наши данные не доказывают, какой именно внутренний механизм отвечает за падение: интерливинг слотов, EOS-переходы или устройство конкретного verify-пути остаются открытыми.

Формула применения

Роутерная схема, которая следует из замеров: детерминированный класс (код, JSON, извлечение) - DFlash-канал; естественно-языковой анализ и общение - baseline или MTP. На моём стенде роутер стоит одной строкой: классификатор задачи на входе, флаг на перезапуске воркера - и не нужен никакой ретрейн.

Практический потолок: даже в идеальных условиях DFlash даёт 1.8× на третьей части трафика. MTP даёт 1.4× на всём трафике бесплатно. Прежде чем заводить отдельный DFlash-канал, посчитайте, что именно у вас детерминированного и не закроет ли MTP эту потребность сам.