Опубликовано
От 96K до следующей лаборатории: Lucebox, EAGLE-3, MLX и ROCm
Автор: Гусев Николай [портфолио]
Обновлено
Темы: llm, inference, long-context, windows
Автор: Гусев Николай. Серия про спекулятивное декодирование на 8 ГБ: часть 1, часть 2 - DFlash, часть 3 - EAGLE-3.
PFlash - сжатие prefill из research-демона Lucebox. Вендорские заявления: 6-10× на префилле. Забавная деталь: репозиторий исследовательский, Linux-only, релизов нет. Готовой Windows-сборки в ходе работы я не нашёл - поэтому собрал демон из исходников. Эта часть серии начинается с измеренного результата, а заканчивается тем, что я бы ещё проверил на другом классе железа.
Сначала про память: миф о «не влезает»
Общепринятая мудрость: длинный контекст на 8 ГБ невозможен. Замер это опроверг - для Qwen3.5. У модели гибридное внимание (Gated DeltaNet), KV весит ~35 КБ на токен в f16 - вчетверо компактнее плотных трансформеров.
| Конфигурация | Контекст | Prefill | VRAM пик |
|---|---|---|---|
| KV f16 | 64K | 27.9 с | 5765 МиБ |
| KV q8_0 | 64K | 29.3 с | 5025 МиБ |
| KV q4_0 | 96K | 51.0 с | 4954 МиБ |
| KV q4_0 с выгрузкой в RAM | 96K | 51.2 с | 4946 МиБ |
64K влезает даже с f16, 96K - с квантованным KV. Выгрузка в RAM не понадобилась вплоть до 96K - буфер и так помещался. Точность NIAH 4/4 во всех ячейках, включая q4_0 на 96K. Узкое место - не VRAM, а время prefill: 2000 токенов в секунду на моей карте. И именно тут живёт PFlash.
Что такое prefill и почему его вообще надо сжимать
При длинном запросе модель сначала читает весь вход - это prefill. Только после него начинается decode, который пользователь воспринимает как печать ответа. На коротком prompt decode доминирует и ускорение prefill почти не видно. На 96K токенов чтение входа занимает десятки секунд, поэтому отдельный выигрыш на этой фазе становится end-to-end выигрышем.
Упрощённая схема:
prefill: [system][history][document][question] -> KV cache
|
v
decode: answer token 1 -> token 2 -> token 3 ...
PFlash оптимизирует именно первую стрелку. Это не магический ускоритель каждой генерации и не замена MTP/DFlash/EAGLE для decode. В длинном контексте эти слои могут сочетаться концептуально: PFlash обрабатывает вход, а выбранный decode-метод обслуживает ответ. В нашем замере R3b-2 сознательно фиксировались плечи AR и PFLASH, чтобы не смешивать эффекты.
Как строился NIAH-корпус
Для каждой длины генерировался длинный нейтральный filler и один needle - специальная строка с ключом и числом. В конце запроса модель должна была вернуть только найденное значение. Критерий был машинным: правильный magic number или ошибка.
{
"id": 2,
"prompt_tokens": 98302,
"needle": "The special magic jcxxyrpi number is 4625423.",
"expected": "4625423"
}
Корпус имел точки 16K, 32K, 64K и 96K. Для каждой точки было несколько разных needles, чтобы один удачный пример не выглядел как доказательство. На 96K prompt template добавлял служебные токены, поэтому контекстный предел выставлялся с запасом: --max-ctx 100352.
Сборка несуществующего
Сборка не лежит в GitHub как бинарный релиз: test_dflash.exe и CUDA DLL занимают сотни мегабайт, а часть зависимостей привязана к локальной версии CUDA. Исходники Lucebox лежат в vendor/lucebox/ материалов проекта - сам движок в git не заливается, почему и куда смотреть, разобрано ниже. Там же журнал сборки и патчи. Для Windows я использовал CMake, Ninja, CUDA Toolkit и Visual Studio Build Tools.
Минимальный путь для Linux/WSL2, близкий к upstream-инструкции:
cd vendor/lucebox
cmake -B server/build -S server -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CUDA_ARCHITECTURES=89
cmake --build server/build --target test_dflash -j
Для сервера:
cmake --build server/build --target luce_server -j
./server/build/luce_server --help
Для PFlash-ветки отдельная инструкция лежит в vendor/lucebox/optimizations/pflash/README.md, общая — в vendor/lucebox/server/README.md. Обе ссылки ведут на репозиторий Lucebox целиком; сам движок — сторонний код (~130 МБ зависимостей, cublasLt-библиотеки), поэтому в наш репозиторий он не заливается и лежит на диске в vendor/lucebox/ (это зафиксировано в vendor/SOURCES.md). На Windows к их инструкциям добавляются наши конкретные поправки: WriteFile вместо CRT _write, forward-slash для временных путей, совместимый output.weight и правки заголовков CCCL. Они перечислены в журнале lucebox-build.log.
После сборки бинарник запускается только с подходящей парой target/draft; обычный Qwen3.5 GGUF не является автоматически совместимым Lucebox DFlash-драфтом. Это ограничение важно сохранить: собрать движок - не значит получить рабочий DFlash на любой модели.
Кривая: чем длиннее, тем лучше
NIAH-корпус, плечи AR (обычный префилл) и PFLASH (сжатие), медианы по 4 задачам:
| Контекст | AR | PFLASH | Ускорение | К upstream |
|---|---|---|---|---|
| 16K | 9.5 с | 4.4 с | 2.2× | 1.22× |
| 32K | 24.6 с | 8.0 с | 3.1× | 1.47× |
| 64K | 72 с | 16.7 с | 4.3× | 1.67× |
| 96K | 148.1 с | 27.7 с | 5.34× | 1.84× |
Два сюжета в этой таблице.
Первый: выигрыш растёт быстрее линейного - сжатие префилла съедает именно ту часть, которая масштабируется квадратично. На 96K PFlash внутри рантайма ускоряет в 5.3 раза.
Второй: до 64K демон проигрывал обычному llama.cpp на AR-плече (их граф не оптимизирован под sm_89 - мою карту), и PFlash выигрывал только за счёт сжатия. На 96K сжатие перевесило: 27.7 секунды PFlash против 51 секунды upstream AR - PFlash обгоняет обычный инференс напрямую, 1.84× end-to-end. Точность NIAH 100% во всех точках, ответы совпадают с upstream до символа.
Что бы я еще проверил на другом классе железа
На этом месте важно не притворяться, что эксперимент закончен там, где закончилась VRAM. Я бы еще проверил три ветки, для которых уже можно описать методику, критерии и ожидаемый практический смысл, но нельзя честно выдать результат на текущем стенде.
1. Собственная голова EAGLE-3
Наш отрицательный результат относится к доступной голове AngelSlim и нашей связке Qwen3-4B. Q4 и Q8 дали почти одинаковый acceptance, поэтому простое объяснение «всё сломала квантизация» не прошло. Рабочая гипотеза - голова не совпадает с ревизией таргета или интерфейсом скрытых состояний. Гипотеза не превращается в факт от того, что красиво звучит.
Проверка требует обучить голову на тех же весах, токенизаторе и шаблоне, которые используются при инференсе:
target revision + exact tokenizer + exact chat template
|
hidden-state capture
|
EAGLE-3 head
|
GGUF conversion
|
acceptance smoke + speed matrix
Практический пайплайн такой:
- Зафиксировать ревизию Qwen3-4B, GGUF-хэш, токенизатор и
enable_thinking=false. - Собрать 50-100 тысяч диалогов и задач с тем же шаблоном, который будет на сервере.
- Прогнать таргет и сохранить fused features нескольких глубин.
- Обучить EAGLE-3 голову на непрерывном принятии, а не только на token-level accuracy. Здесь полезна связь с PARD-2: для speculative decoding важна длина принимаемого участка.
- Конвертировать голову в GGUF, проверить размерности и прогнать frozen smoke.
- Сравнить BF16/Q8/Q4 таргеты на одинаковом корпусе, затем отдельно проверить русский NL, код и JSON.
Gate заранее формальный: голова должна показать acceptance не ниже 0.5 на контрольном корпусе и дать положительный correct/min против baseline. Если она выигрывает только на обучающем домене, это не универсальный фикс, а доменная голова.
На RTX 4060 я этого не проверял: таргет, голова, активации и батч обучения упираются в 8 ГБ. Теоретически можно разнести части по RAM и ждать, но это уже другой эксперимент с неизвестной скоростью и не основание обещать результат. На другом классе GPU я бы сначала повторил эту проверку с запасом по памяти, а не объявлял ветку выполненной по одному удачному запуску.
2. MLX: не переносить CUDA-выводы на Apple Silicon
Вторая ветка - возможный запуск на Mac M3 Ultra. MLX - не «ещё один llama.cpp». У Apple Silicon единая память, Metal-граф и другие точки насыщения. На RTX 4060 мы видели, что batch может резко менять acceptance DFlash на естественно-языковых задачах. На MLX я бы проверил, что именно делает сервер: настоящий continuous batching, очередь, time-sharing или смесь этих режимов.
План сравнения:
server: mlx-openai-server / mlx_lm.server / llama-server
model: one fixed MLX quant
load: single, batch 2, 4, 6, 8
input: 1K, 4K, 16K, 64K, 100K
modes: baseline, native MTP, speculative if supported
metrics: TTFT, ITL, aggregate tok/s, per-stream tok/s,
correct/min, memory, acceptance, fairness
На каждом сервере сначала проверяется, какая модель действительно загружена. В чужом MLX-бенчмарке уже был показательный инцидент: автодетект подменил 4-битную модель на 8-битную из кэша, и скачок производительности выглядел как оптимизация. Поэтому model_alias, хэш и server log являются частью результата, а не служебной мелочью.
В предварительном плане я бы сравнил mlx-openai-server с mlx_lm.server и llama-server: первый интересен заявлениями о continuous batching и speculative decoding, второй нельзя заранее объявлять плохим, а третий нужен как OpenAI-совместимый контрольный путь. Риск contamination KV-cache при большом числе параллельных запросов я бы проверял на нашем корпусе, а не повторял как легенду.
Особенно важен fairness-тест: короткий запрос, пришедший одновременно с 100K документом, не должен ждать весь prefill длинного соседа без измерения этого хвоста. Поэтому в матрице нужны сдвиги стартов, запросы разной длины и per-stream TTFT, а не только суммарный wall throughput.
Теоретическое ожидание простое: unified memory снимает часть ограничений вместимости, но не отменяет стоимость чтения KV и prefill. Если модель и контекст помещаются, это ещё не означает, что latency годится для пользователя. Именно это на RTX 4060 мы уже увидели в другом виде: 96K влезает, но upstream prefill занимает 51 секунду.
3. Ryzen AI Max+ 395 и ROCm
Третья ветка - возможная проверка альтернативного узла на AMD Ryzen AI Max+ 395 / Strix Halo с Radeon 8060S и общей памятью 96-128 ГБ. Его нельзя описывать как «RTX 4060, только быстрее». Это APU с unified memory, другой GPU-бэкенд, другая зрелость kernels и другой профиль копирования.
Зачем я бы его проверил:
- большой объём общей памяти для моделей и KV без отдельного CUDA-узла;
- проверка длинного контекста в 4-6 битах на 64-256K;
- возможность держать несколько моделей и не выгружать их между запросами;
- потенциально более дешёвый узел для data-parallel фермы;
- ROCm-стенд для проверки, какие CUDA-ориентированные методы реально портируются.
Минимальная матрица переноса:
hardware: Ryzen AI Max+ 395 / Radeon 8060S / 96 or 128 GB
backend: ROCm version pinned in manifest
runtime: llama.cpp HIP/ROCm, vLLM/ROCm if compatible
models: same Qwen family, same quant where possible
context: 16K, 32K, 64K, 96K, 128K, 256K
load: single, batch 2, 4, 8
Сначала я бы снял baseline без спекуляции. Затем отдельно проверил MTP, если конкретный runtime умеет native heads. DFlash, EAGLE-3 и PFlash пошли бы отдельными ветками только после compatibility gate. Если Lucebox требует CUDA-специфичные ядра, перенос на ROCm не считался бы «почти готовым»: это новый backend и новая серия проверок.
Критерии выбора AMD-узла:
- правильность NIAH 4/4 на каждой длине;
- TTFT и prefill tok/s;
- decode tok/s и per-stream скорость под batch;
- реальное потребление общей памяти;
- время загрузки и выгрузки моделей;
- положительный correct/min против уже работающего узла;
- стоимость одного полезного запроса, а не пиковая скорость в пустом single-user режиме.
ROCm здесь интересен не как религиозная замена CUDA, а как способ проверить, где у нас настоящий алгоритмический выигрыш, а где выигрыш принадлежит конкретным CUDA kernels. Если PFlash на CUDA даёт 5.34× внутри Lucebox, а на ROCm недоступен, это не опровержение метода. Это ограничение backend. Но для сравнения узлов это всё равно важный эксплуатационный факт.
Как бы я проверял это без самообмана
Перенёс бы корпус вместе с дисциплиной:
- manifest фиксировал бы железо, runtime, модели, кванты, хэши и версии драйверов;
- baseline снимался бы на каждом узле до speculation;
- неподдержанная ветка получала бы
unsupportedили оставалась бы за пределами вывода; - сырые логи сохранялись бы до агрегации;
- результат сравнивался бы по correct/min и per-stream latency;
- модель и хэш сверялись бы по server log;
- один плохой запуск не переписывал бы ТЗ и не отменял остальные ячейки.
То есть я бы начинал не с покупки красивого бокса, а с инвентаризации доступного железа и его фактической утилизации. Если bottleneck - память, нужен большой unified-memory узел. Если bottleneck - prefill, CUDA/NVIDIA может оказаться полезнее для Lucebox и обучения голов. Если bottleneck - плохой batching, сначала проверяется сервер и маршрутизация, а не заказывается новая машина.
Итог
На RTX 4060 я закрыл то, что можно было закрыть честно: MTP, DFlash на разных доменах, EAGLE-3 с доступной головой, PFlash и 96K NIAH. PFlash дал 5.34× внутри Lucebox и 1.84× end-to-end против upstream на 96K; длинный контекст оказался ограничен временем prefill, а не VRAM.
На другом классе железа я бы отдельно проверил:
- обучение EAGLE-3 head под точную ревизию модели;
- MLX и multi-user поведение на Apple Silicon;
- Ryzen AI Max+ 395 / ROCm с той же frozen-матрицей;
- перенос PFlash и DFlash после compatibility gate.
Это не список уже выполненных экспериментов и не обещание собственной лаборатории. Это список следующих проверок, которые имеют смысл, если появится доступ к подходящему железу. До тех пор корректная формулировка одна: на RTX 4060 эти ветки не измерены, а их методика и критерии успеха описаны заранее. И это лучше, чем объявить недоступный эксперимент выполненным, а потом заменить запуск очередным разговором о том, как было бы интересно когда-нибудь запустить.
DFlash-плечо внутри Lucebox не заработало: демон требует драфт своего формата, доступный GGUF конвертируется под upstream, после переименования метаданных падает на assert - разная схема fusion. Официальный safetensors закрыт (401). Ветка зафиксирована как blocked, не подменена.
И главное: 6-10× из блога вендора на моём железе не воспроизводятся в заявленном виде. Те цифры сняты на 33B-модели, RTX 3090 и 256K контекста. На 4060 с 4B-моделью воспроизводится скромный, но реальный 1.2-1.8×, растущий с длиной. Это не обман вендора - это другой стенд. Но читать чужие бенчмарки без специфики железа - та же ошибка, что ставить фронтирную модель на пульт стенда.
Вывод
Длинный контекст на 8 ГБ - не «невозможно», а «влезает до ~96K с квантованным KV, и узкое место переносится из памяти в prefill». Prefill - сжимаемая величина. Формула для агентных сценариев: гибридно-внимательная модель, квантованный KV и сжатие префилла на длинных контекстах. Все патчи и скрипты - в репозитории проекта.