Опубликовано
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 повтора на точку, медианы.
| Конфигурация | Задач/мин | К baseline | Acceptance |
|---|---|---|---|
| Baseline | 59.4 | 1.00 | - |
| DFlash n=4, серийно | 105.4 | 1.77× | 0.81 |
| DFlash n=4, 4 параллельных | 111.9 | 1.81× | 0.81 |
| DFlash n=15, серийно | 104.8 | 1.76× | 0.36 |
| DFlash n=15, 4 параллельных | 115.9 | 1.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 роутингов. Тот же таргет, тот же драфтер.
| Режим | Задач/мин | К baseline | Acceptance |
|---|---|---|---|
| Baseline серийно | 32.8 | 1.00 | - |
| DFlash серийно | 18.2 | 0.55× | 0.17 |
| Baseline под нагрузкой | 48.1 | 1.00 | - |
| DFlash под нагрузкой | 11.4 | 0.25× | 0.06 |
Не «нет выигрыша» - уверенное замедление. Драфт предлагает, таргет отвергает, за каждый сгенерированный токен платим проверкой трёх-четырёх непринятых. Под нагрузкой acceptance падает ещё втрое, и метод ест GPU вчетверо быстрее.
Граница: факторный эксперимент
Что именно рушит acceptance - длина промпта, параллелизм или содержание задачи? Собрал различающий эксперимент: один и тот же детерминированный корпус в двух вариантах (короткий ~60 токенов и длинный ~3.3K с архивным филлером), обе длины серийно и под нагрузкой, плюс прямой замер acceptance на документах.
| Корпус | c=1 | c=4 | Коллапс |
|---|---|---|---|
| Код, короткие задачи | 0.3645 | 0.3645 | нет |
| Код с 3.3K филлером | 0.3355 | 0.3355 | нет |
| Документы (NL-анализ) | 0.1714 | 0.0611 | да, -64% |
Три факта, каждый из которых убивает по одной гипотезе:
- Параллелизм сам по себе acceptance не трогает - на коротких и длинных детерминированных задачах цифры совпадают до четвёртого знака.
- Длина промпта - мягкий фактор: -8% серийно, и всё.
- Коллапс требует сочетания: естественно-языковая генерация поверх длинного контекста плюс батчинг. Ни один фактор по отдельности его не воспроизводит.
Это сходится с 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=1 | c=4 |
|---|---|---|
| короткий код | 0.3645 | 0.3645 |
| код с длинным филлером | 0.3355 | 0.3355 |
| естественно-языковые документы | 0.1714 | 0.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 эту потребность сам.