
Подняли DeepSeek‑V4‑Flash на двух GB10, упёрлись в потолок consumer Blackwell, прошли три тупика со спекулятивным декодингом — и в итоге получили параллельную работу ресёрч‑агента и длинной генерации без очереди. Цифры — из Grafana.
Что реально сработало
После всех тупиков остались скучные, но работающие изменения.
Главное — поднять --max-num-seqs с 1 до 8. Единица означала, что в рантайме одновременно живёт ровно один запрос; всё остальное ждало в очереди, и второй пользователь получал первый токен через 14–15 секунд.
Откуда взялась эта единица — отдельная история. Когда мы только подняли V4 и дали ей два параллельных запроса с длинным контекстом, скорость генерации падала почти до нуля: видеокарта загружена полностью, а на выходе доли токена в секунду. Проще всего было убрать симптом, запретив параллельность, — что мы и сделали, заодно решив, что GB10 параллельную нагрузку не вывозит. Диагноз оказался неверным. Дело было не в параллельности, а в том, что под SM121 не был включён нужный attention‑путь (sparse‑MLA, о нём ниже), и размер батча работал против нас. Единица ничего из этого не лечила — она прятала симптом, выключая ровно ту возможность, ради которой всё затевалось. Стоило включить sparse‑MLA и вернуть нормальный размер пула — провал не вернулся.
Sparse‑MLA под SM121 — VLLM_TRITON_MLA_SPARSE=1. Включает sparse‑MLA путь, заточенный под эту линию GB10. Без него модель либо не стартует, либо сваливается в неподходящий attention‑бэкенд.
Prefix caching — --enable-prefix-caching. Для агентных нагрузок критично. Ресёрч‑агент гоняет один и тот же длинный контекст по кругу между шагами. Без кэша каждый запрос заново прогоняет prefill на сорок с лишним тысяч токенов, и кластер занят не генерацией, а пережёвыванием одного и того же. В бою доля попаданий в кэш дошла до 88% — к этой цифре вернёмся в разделе с метриками.
Штатная MTP‑спекуляция DeepSeek на нашей нагрузке работала: acceptance держался в районе 55–64%, то есть драфтер угадывал заметную часть токенов, а не просто добавлял накладные расходы.
Отдельно про проверку. После смены MoE‑ или attention‑пути смотреть надо не на код ответа, а на его связность. Наш sanity‑check был предельно простым: спросить столицу Франции и убедиться, что в ответе Париж. curl вернёт 200 и тогда, когда численный путь сломан, а модель выдаёт грамматически правильный, но бессмысленный текст.

TL;DR
Железо: 2× NVIDIA DGX Spark: GB10, Grace‑Blackwell, SM121, по 128 ГБ unified memory на ноду, связь через QSFP 200G / RoCEv2 / RDMA.
Модель: DeepSeek‑V4‑Flash — 284B total / ~13B active MoE: 256 экспертов, 6 активных на токен + 1 shared, 43 слоя, MLA attention, нативные FP8+MXFP4 веса. Важно: 284B — это Flash; 671B — это полная DeepSeek‑V4, не эта конфигурация.
Параллелизм: tensor parallel
TP=2на две ноды.Главный фикс:
--max-num-seqs 1 → 8. До этого любой второй запрос фактически вставал в очередь.Что реально помогло:
max-num-seqs, sparse‑MLA path для SM121, prefix caching и штатная MTP‑спекуляция DeepSeek.Что не помогло: MRV2, Expert Parallel поверх B12X MoE и внешний DFlash‑драфтер.
Боевые цифры: 220 запросов за 2 часа, средний prompt 42 565 токенов, пик 4 running / 0 waiting, KV‑cache до 44.6%, peak aggregate 71 tok/s, prefix‑cache hit 88%.
1. Зачем мы вообще полезли в это болото
У нас есть кластер из двух DGX Spark. GB10 — это не датацентровый Blackwell уровня B200/GB200, а «настольный» Grace‑Blackwell с compute capability SM121 и unified memory: CPU и GPU делят один пул памяти на 128 ГБ.
Unified memory тут одновременно и причина, по которой всё это вообще затевалось, и источник большей части боли. Плюс очевиден: крупная модель видит большой общий пул и не упирается в привычную стену «сколько VRAM на одной карте». Минус всплывает чуть позже: добрая половина оптимизированных CUDA‑кернелов и готовых контейнеров писалась под датацентровый Blackwell, и про SM121 они либо не знают, либо знают, но криво.
Задача была сугубо прикладная: поднять сильную reasoning‑модель как общий inference‑backend на несколько потребителей. От неё хотелось сразу:
агент‑ресёрчер с длинным контекстом;
интерактивную генерацию;
отсутствие очереди при нескольких пользователях;
связные ответы, а не просто HTTP 200 от
/v1/chat/completions.
DeepSeek‑V4‑Flash под это подходила почти идеально на бумаге: MoE с ~13B активных параметров на токен, компактный KV за счёт MLA, веса FP8+MXFP4 примерно на 149 ГБ и MTP‑спекуляция из коробки. Оставалось «всего лишь» её запустить.
2. Почему «просто поставить vLLM» не получилось
Главная засада: upstream vLLM на тот момент к DeepSeek‑V4 на consumer Blackwell готов не был.
Сама архитектура DeepSeek‑V4 в vLLM уже зарегистрирована — model class, tokenizer/parser, MTP и сопутствующие куски на месте. Но аппаратный путь под V4 заточен под датацентр: Hopper H200, Blackwell B200/B300/GB200, AMD MI355X. Внутри живут SM100-only оптимизации: DeepGEMM, FlashMLA sparse, Lightning Indexer.
На SM121 всё это рассыпается. В vLLM issue #45317 симптоматика видна как на ладони: все MLA‑бэкенды по очереди отвергаются, и в конце прилетает классика:
text
No valid attention backend found ... compute capability not supported
Рабочий путь для GB10 на сегодня — community‑форк jasl/vllm, где для SM12x добавлены Triton fallback‑кернелы: sparse‑MLA, FP8 lightning‑indexer и замена SM100‑DeepGEMM. Мы взяли community‑образ aidendle94/sparkrun-vllm-ds4-gb10 из этой линии, vLLM 0.21.1rc1.dev.
NGC‑контейнеры тоже мимо: образы 26.03+ требуют драйвер ≥590, а на Spark у нас стоял 580. На 590 нас встретили UMA leak и CUDAGraph deadlock, так что драйвер пришлось прибить гвоздями к 580.

Резюме: DeepSeek‑V4‑Flash на DGX Spark — это bleeding edge на неслитом PR. Оно работает, но только если держишь в голове, какие пути исполнения на SM121 реально доступны, а какие лишь делают вид.
3. Три тупика спекулятивного декодинга
Мы хотели разогнать single‑stream и по дороге снять «бесплатный» прирост со свежих оптимизаций. Сняли вместо этого три тупика. Каждый скушал время — но каждый и показал, как на этом железе реально устроен исполнительный путь.

Тупик 1. Model Runner V2 + reasoning
Model Runner V2 — новый execution layer vLLM: GPU‑native Triton kernels, async scheduling, аккуратнее работа с KV и обещанный прирост throughput. Включается переменной окружения:
bash
VLLM_USE_V2_MODEL_RUNNER=1
На нашей сборке с reasoning‑моделью vLLM падал прямо на старте:
text
pydantic ValidationError: VLLM_USE_V2_MODEL_RUNNER does not yet support: reasoning budget enforcement
Самое интересное тут даже не текст ошибки. Для DeepSeek‑V4 MRV2 в принципе не является обязательным путём: такие MoE/MLA‑модели vLLM всё равно уводит на Model Runner v1. То есть даже гипотетически удачный запуск не дал бы того прироста, ради которого мы туда полезли. Мораль предсказуемая: «ускоряющая» фича из чейнджлога не обязана иметь отношение к твоей конкретной архитектуре.
Тупик 2. Expert Parallel + B12X MoE
Для MoE рука сама тянется включить Expert Parallel:
bash
--enable-expert-parallel
На нашей конфигурации это легло так:
text
ValueError: Mxfp4 MoE backend 'B12X' does not support ... ep_size=2 ... use_ep=True
B12X MoE‑кернел — это тот самый fallback для SM121, который в нашем случае отвечает за численную корректность. Без него последний MoE‑слой умеет выдавать грамматически безупречный, но абсолютно бессмысленный текст. А с Expert Parallel он не дружит: community‑деплои с EP обычно идут через native DeepGEMM path, которого на нашем SM121 попросту нет.
Выбор оказался бинарный: либо EP, либо ответы, которые что‑то значат. Мы выбрали второе.
Тупик 3. DFlash‑драфтер, который перезалили «вчера»
Параллельно мы пробовали разогнать через внешний DFlash‑драфтер соседнюю модель того же кластера. Запуск падал на assert:
text
unify_kv_cache_spec_page_size
Раскопки показали, что мейнтейнер перезалил драфтер с другой head‑геометрией: 64 КиБ на блок против 32 КиБ у target‑модели. Старый драфтер работал, новый — нет, и между этими двумя состояниями не было ничего, кроме молчаливого апдейта в репозитории.
Отсюда простой вывод на будущее: внешний драфтер — это не «маленькая модель где‑то рядом», а полноценная зависимость, которую могут тихо обновить и сломать. В production его нужно пиновать по ревизии, как любой другой артефакт, от которого ты зависишь.
4. Что реально сработало
После всех тупиков в сухом остатке оказались скучные, зато работающие изменения.
4.1. max‑num‑seqs 1 → 8
Тот самый главный фикс.
Мы держали:
bash
--max-num-seqs 1
В переводе на человеческий это значило: в рантайме одновременно живёт ровно один запрос. Любой второй запрос вставал в очередь, а пользователь любовался TTFT в 14–15 секунд.
После перехода на:
bash
--max-num-seqs 8
появилась настоящая параллельность — не та, что красиво выглядит на графике, а нормальный multi‑user: несколько запросов идут одновременно, очередь не растёт, latency становится предсказуемым.
4.2. Sparse‑MLA для SM121
Второй важный флаг:
bash
VLLM_TRITON_MLA_SPARSE=1
Он включает sparse‑MLA path, заточенный именно под эту SM121-линию. Без него модель либо не стартует вовсе, либо сваливается в неподходящий attention backend.
4.3. Prefix caching
Для агентных нагрузок — без преувеличения критично:
bash
--enable-prefix-caching
Ресёрч‑агент гоняет один и тот же длинный контекст между шагами по кругу. Если каждый раз заново прогонять prefill на 40K+ токенов, кластер будет занят не генерацией, а бесконечным пережёвыванием одного и того же промпта.
В бою prefix‑cache hit дошёл до 88% — к этой цифре мы ещё вернёмся в разделе с метриками.
4.4. Штатная MTP‑спекуляция DeepSeek
MTP у DeepSeek‑V4‑Flash на нашей нагрузке оказался вполне живым: acceptance rate держался в районе 55–64%. То есть драфтер угадывал заметную часть будущих токенов, а не просто добавлял overhead ради галочки.
Один нюанс, который легко забыть: после смены MoE/attention‑пути проверять надо не HTTP‑код, а связность ответа. У нас sanity check был максимально тупой:
text
Вопрос: столица Франции? Ожидаемый ответ: Париж
curl радостно вернёт 200 и тогда, когда numerical path сломан, а модель выдаёт грамматически валидный мусор.
5. Замеры: синтетика против боевой нагрузки
Дальше — две принципиально разные истории: стендовый benchmark на пустом кластере и реальные метрики из Prometheus/Grafana. Путать их не стоит.

5.1. Стендовый benchmark и ошибка в методике
Гоняли тяжёлый prompt на генерацию production‑кода, ~600 токенов вывода. Кластер пустой: num_requests_running=0. Токены считали через usage.completion_tokens.
Concurrency | Per‑user, tok/s | Aggregate, tok/s |
|---|---|---|
1 | 44.9 | 44.9 |
4 | 20.0 | 79.9 |
8 | 18.0 | 144.1 |
Single‑stream дал 44.9 tok/s. На concurrency 8 вышли на 144.1 tok/s aggregate — уже рядом с community‑бенчами на той же паре GB10.
И тут была главная грабля. Первая версия нашего бенча считала SSE‑события стрима — +1 за каждый chunk. При MTP‑спекуляции это враньё: в одном chunk может лежать сразу 2–3 принятых токена. В итоге счётчик занижал throughput примерно в 2.5 раза и показывал те самые «18 t/s» вместо реальных 44.9 t/s. Grafana, кстати, с самого начала рисовала правдоподобную картину — мы просто ей сначала не поверили.
Правильно — считать usage.completion_tokens или смотреть Prometheus‑метрику vllm:generation_tokens_total.
5.2. Реальная нагрузка: 2 часа, 220 запросов
Теперь не синтетика, а живая работа: агент‑ресёрчер с большим контекстом плюс интерактивная генерация.
Метрика | Значение |
|---|---|
Запросов за окно | 220 |
Средний prompt на запрос | 42 565 токенов |
Средняя генерация на запрос | 443 токена |
Пик параллельных запросов | 4 running / 0 waiting |
Пик KV‑cache | 44.6% |
Пик aggregate throughput | 71 tok/s |
Inter‑token latency, avg | 84 мс |
Prefix‑cache hit | 8.23M из 9.36M prompt‑токенов = 88% |
Несколько вещей, на которые стоит посмотреть внимательнее.
Средний prompt в 42K токенов — это не игрушка, а нормальная агентная нагрузка, и кластер держал на ней 4 параллельных запроса без очереди. Для нашей задачи именно это было критерием «годится / не годится».
KV‑cache доходил до 44.6%. Короткие синтетические тесты в этом месте умеют показывать смешные 5–6% и создавать ложное ощущение бездонного запаса; на длинных агентных контекстах картина резко другая.
Prefix‑cache hit 88% — пожалуй, ключевая цифра всего эксперимента. Агент переиспользует длинный контекст между шагами, и кэш съедает почти весь повторный prefill, который иначе сжёг бы кластер впустую.
Inter‑token latency 84 мс хорошо объясняет субъективные «спотыкания» в потоке. При параллельном decode ресурсы делятся между запросами, а MTP acceptance плавает, поэтому поток не идеально ровный. Но это именно лёгкая неровность, а не затыки и очередь, которые были при max-num-seqs=1.
6. Чеклист для тех, кто повторяет на DGX Spark
DeepSeek‑V4‑Flash на GB10 сейчас требует community‑форк. Stock vLLM 0.22/0.23 и «универсальные» образы без SM121-кернелов дадут crash или garbled output.
Не проверяйте модель по HTTP 200. Сломанный numerical path спокойно возвращает валидный JSON и бессмысленный текст.
Не оставляйте
--max-num-seqs 1, если нужен multi‑user.Включайте
--enable-prefix-cachingдля агентных нагрузок. На переиспользовании длинного контекста экономия prefill огромна.Expert Parallel с B12X MoE fallback несовместим. На SM121 пришлось выбирать между EP и корректностью; мы выбрали корректность.
MRV2 — не серебряная пуля для MoE/MLA. Для DeepSeek‑V4 этот путь не дал практического выигрыша и упёрся в reasoning‑budget enforcement.
Не путайте NCCL compile‑time и runtime. У нас
torch.cuda.nccl.version()показывал 2.28.9, а runtime в образе был 2.30.4 — важно помнить при диагностике RoCE‑зависаний.На GB10 не доверяйте привычным NVML‑панелям памяти.
nvidia-smi --query-gpu=memory.usedумеет возвращать[N/A]. Реальную занятость unified memory честнее смотреть черезfree, compute apps и отдельные Prometheus‑метрики.Пиньте внешние драфтеры. Их могут перезалить и сломать head‑геометрию или KV layout без единого предупреждения.
Итог
DeepSeek‑V4‑Flash на двух DGX Spark — рабочая конфигурация для параллельного multi‑user inference. Но не в режиме «скачал официальный образ и поехали»: для GB10/SM121 нужны community‑кернелы, осознанный выбор execution path и нормальная проверка связности ответа.
Самый большой выигрыш дали не экзотические оптимизации, а скучная инженерия: поднять max-num-seqs, включить prefix caching, использовать sparse‑MLA под SM121 и не ломать корректный MoE backend ради Expert Parallel.
По синтетике вышло 44.9 tok/s single и 144.1 tok/s aggregate на concurrency 8. В реальной нагрузке важнее другое: средний prompt 42K токенов, 4 параллельных запроса без очереди, KV‑cache до 44.6% и 88% prefix‑cache hit. Именно эти цифры и показывают, что конфигурация живёт не только в демо, но и в настоящем агентном сценарии.
Цикл статей про DGX Spark и self‑hosted inference:
Как я собрал на DGX Spark приватный AI‑сервер — из чего собран стек AGmind и кластер из двух Spark.
Мониторинг unified memory, когда NVML и dcgm‑exporter молчат — почему на GB10
nvidia-smiотдаёт[N/A]и как это обойти.Кириллица в LLM: почему русский стоит дороже и работает медленнее — токенизация, цена контекста и выбор моделей под русский.
DGX Spark на 256K контексте: конфигурации vLLM и почему NVFP4 в mainline сломан — шесть конфигов vLLM на SM121, включая DFlash.
