TL;DR
-
Железо: 2× NVIDIA DGX Spark (GB10, SM121), 128 GB unified номинально и около 122 GiB видимых системе на ноду, QSFP 200G / RoCEv2, TP=2, драйвер 580.159.03.
-
Основную часть расхождения объяснила смена runtime‑образа. Переход со сборки на базе vLLM 0.21.1 на образ с 0.25.2 дал около +22% на том же чекпоинте. Конкретный коммит или компонент я не изолировал: поменялся весь пакет.
-
Механика неочевидная: новый образ снизил расчётную частоту verification‑шагов примерно на 17%, но поднял число выходных токенов за шаг с 3.27 до 4.80.
-
Главная находка в конфиге:
--moe-backend flashinfer_b12xне выбирается режимомauto. Явное включение дало +13.3% по среднему пяти прогонов на card‑like профиле (59.7 против 67.6) и +16.6% по медиане. -
B12X работает иначе: поднимает SSE‑derived частоту verification‑шагов на 16–18%, при этом выходные токены за шаг снижаются примерно на 2.5% в обоих профилях. Итоговый прирост клиентской decode‑метрики составил 13.3–14.6% по средним и 15.6–16.6% по медианам.
-
Итог: 67.6 tok/s на одиночном запросе, 260 tok/s суммарно на 12 параллельных запросов.
-
Отрицательные результаты, погрешности и границы применимости вынесены в отдельные разделы.
В карточке drowzeys/DeepSeek-V4-Flash-DSpark-Abliterated-Uncensored указано:
| C1 pure decode | ~57 tok/s (code, 128-tok) |
Разворачиваю ту же модель на своей паре DGX Spark, получаю 42.5 tok/s. Мой результат оказался примерно на 25% ниже заявленного.
К концу разбирательства я вышел в тот же диапазон по совместимой клиентской метрике, а затем на другом чекпоинте и с явно выбранным MoE‑бэкендом получил 67.6 tok/s на том же card‑like профиле. Ниже разбор: какие гипотезы не подтвердились, что оказалось причиной расхождения, и почему два изменения ускоряют модель принципиально разными способами.
В репозитории опубликованы рецепт запуска, харнессы, манифест с digest образа и ревизиями моделей, а также сырые логи парного B12X‑теста и c=1-контроля: dspark-0731-gb10
1. Стенд и метрики
Два DGX Spark, соединённые напрямую по QSFP 200G, NCCL поверх RoCEv2, тензорный параллелизм на две ноды. Полный чекпоинт занимает около 156 GiB на диске и закеширован на обеих нодах. В рантайме модель работает в TP=2, поэтому каждый процесс загружает свой tensor‑parallel shard.
Считаю usage.completion_tokens из ответа API, а не количество событий стрима.
Полезное наблюдение про стрим. В использованной сборке под спекулятивным декодированием vLLM отдавал один SSE‑чанк на verification step, и в нём приезжают все принятые за этот шаг токены. Я сверил это со счётчиком движка на одном контрольном запросе: после начального content‑чанка оставшиеся 65 совпали с num_drafts_total = 65, при 66 чанках всего. Поэтому в этой сборке я использую content_chunks − 1 как proxy числа verification‑шагов. Дальше называю такую величину SSE‑derived: это proxy, проверенный на одном запросе, а не документированный контракт vLLM. Поэтому для контрольной серии я считал шаги напрямую по чанкам. Универсальным контрактом vLLM это не считаю, проверяйте на своей сборке.
Клиентская decode‑метрика, совместимая с той, что цитируют карточки:
decode tok/s = (completion_tokens − first_chunk_tokens) / (t_last_content − t_first_content)steps/s = (content_chunks − 1) / (t_last_content − t_first_content)
first_chunk_tokens я не предполагаю, а считаю через серверный эндпоинт /tokenize. На этом стенде первый content‑чанк во всех контрольных прогонах содержал 'Here' и токенизировался в один токен. Последующие чанки несли по несколько токенов; профильные средние составили примерно 4,3–4,7 выходного токена на decode‑шаг.
Отдельно про число выходных токенов за шаг. Метрики движка дают:
output tokens per verification step = 1 + num_accepted_tokens_total / num_drafts_total
Ведущая единица соответствует собственному токену target‑модели, который производится всегда. Принятых draft‑токенов за шаг соответственно на единицу меньше. Дальше в статье я везде использую именно «выходных токенов за шаг», чтобы не путать эти две величины.
Скорость я привожу в двух режимах: одиночный запрос (c=1) и 12 параллельных запросов (c=12), для которых указываю суммарную скорость всех потоков вместе.
Два профиля нагрузки, потому что разброс между ними больше, чем эффект почти любой настройки:
|
профиль |
что это |
|---|---|
|
|
короткий системный промпт, генерация Python, 500 токенов вывода |
|
|
около 4K токенов русского технического контекста, ответ прозой, 500 токенов |
Код содержит много предсказуемых последовательностей, драфтер угадывает их длинными кусками. В heavy‑профиле таких последовательностей заметно меньше.
2. Из чего складывается скорость
DeepSeek‑V4-Flash представляет собой MoE‑модель: 284B параметров всего, из них около 13B активируются при обработке каждого токена. Декодирование во многом упирается в чтение весов, но простой расчёт «пропускная способность делить на активные параметры» здесь недостаточен. Экспертные веса хранятся в FP4, остальные тензоры в другой точности, TP=2 шардирует вычисления, добавляется межузловой обмен, а разные MoE‑кернелы сами меняют стоимость шага. Паспортные 273 GB/s задают только грубый ориентир.
Практическую картину даёт разложение:
tok/s ≈ verification steps/s × output tokens per verification step
Второй множитель обеспечивает спекулятивное декодирование. К target‑чекпоинту прикреплён отдельный DSpark speculative module. В моём финальном профиле он предлагает пять токенов за шаг; размер блока задаётся конфигурацией рантайма и в других рецептах равен трём или семи.
Оба множителя подвижны. Дальше два изменения, каждое двигает свой множитель, причём в разные стороны.
3. Гипотезы о расхождении
Температура сэмплинга
T=0.7 → 42.5 tok/sT=0 → 42.6 tok/s
Разницы нет. Draft path использованной сборки 0.21.1 работал в greedy‑режиме. Acceptance по двум температурам отдельно я не сохранял, а вывод про vLLM вообще делать нельзя: в более поздних версиях документирован draft_sample_method="probabilistic".
Учёт prefill
e2e: 42.5 tok/sклиентская decode: 43.2 tok/s
TTFT около 200 мс при длительности запроса 10.5 с. Методика влияет, но объясняет мало.
Длина контекста
короткий контекст: 48.3 tok/sмой контекст: 43.2 tok/s
Около 12%. До 57 всё ещё далеко.
Число спекулятивных токенов
Официальная карточка релиза 0731 рекомендует num_speculative_tokens=7. Валидатор моей сборки отклонил это значение:
ValidationError: num_speculative_tokens:7 must be divisible by n_predict=5
Значения 3 и 7 не стартовали. Отмечу границу: это ограничение конкретной ревизии образа и чекпоинта, а не свойство DSpark. Официальная карточка 0731 рекомендует 7, а публичные рецепты на той же линии образов используют 3.
Смена runtime‑образа
Карточка drowzeys указывает другой Stage‑C runtime. Чтобы проверить влияние runtime‑стека, я перешёл на образ ghcr.io/anemll/dspark-vllm-gx10:0.1.1 с vLLM 0.25.2, оставив прежний чекпоинт и остальные основные флаги:
|
|
образ на 0.21.1 |
образ на 0.25.2 |
|---|---|---|
|
code, c=1 |
42.5 |
51.8 |
|
выходных токенов за шаг |
3.27 |
4.80 |
|
расчётная частота шагов |
13.0/с |
10.8/с |
|
позиция драфта |
0.21.1 |
0.25.2 |
|---|---|---|
|
pos0 |
85.9% |
95.2% |
|
pos1 |
62.1% |
86.0% |
|
pos2 |
40.8% |
74.8% |
|
pos3 |
24.3% |
66.5% |
|
pos4 |
13.4% |
57.0% |
Значение каждой позиции показывает долю verification‑блоков, в которых она была принята. Они достигаются по порядку, поэтому ряд убывает:
output tokens per step = 1 + Σ(position rates)0.21.1: 1 + 0.859 + 0.621 + 0.408 + 0.243 + 0.134 = 3.270.25.2: 1 + 0.952 + 0.860 + 0.748 + 0.665 + 0.570 = 4.80
Декомпозиция:
выходных токенов за шаг: 3.27 → 4.80 (+46.8%)расчётная частота шагов: 13.0 → 10.8 (−17.0%)итог tok/s: 42.5 → 51.8 (+21.9%)
Величины 13.0 и 10.8 получены делением измеренного throughput на число выходных токенов за шаг. Прямой server‑side счётчик verification‑шагов для этих двух прогонов я не сохранял; методику прямого счёта по SSE‑чанкам я освоил позже и применял уже к финальной конфигурации.
Сверка с карточкой. На новом образе и коротком code‑профиле получаю 58.3 tok/s против примерно 57 заявленных, то есть выхожу в тот же диапазон по совместимой клиентской метрике. Полного совпадения протокола при этом нет: карточка указывает Stage‑C рантайм, kv_cache_dtype=nvfp4_ds_mla и обратный порядок запуска рангов, а мои прогоны шли на fp8 KV.
4. Новый релиз 0731
Официальная карточка описывает 0731 как релиз с той же структурой и существенно усиленными агентными результатами: DeepSWE 7.3 → 54.4, Terminal Bench 2.1 61.8 → 82.7. Ключи DSpark в config.json на месте, контекст 1048576.
На моём профиле, при том же образе и auto бэкенде:
|
|
предыдущий чекпоинт |
0731 |
|---|---|---|
|
code, c=1 |
51.8 |
59.3 |
|
выходных токенов за шаг |
4.80 |
4.68 |
|
расчётная частота шагов |
10.8/с |
12.7/с |
Число выходных токенов за шаг слегка упало, а скорость выросла за счёт частоты шагов. Причину я не изолировал.
Важная оговорка: это не чистая абляция релиза. Одновременно сменились upstream‑чекпоинт и рецепт производного artifact. Раньше я использовал abliterated‑производную от drowzeys, теперь от apetersson. Последняя описана автором как экспериментальный model‑surgery артефакт с 36 правлеными тензорами, включая стадии DSpark. Поэтому я фиксирую измеренную разницу, но не приписываю её одному конкретному изменению.
5. Флаг, который не включается автоматически
Дальше я показал конфиг коллеге, и он посоветовал перепроверить. Это оказалось самым полезным советом за всю историю.
Сразу оговорюсь про первенство. Параллельно со мной этой же связкой занимался tonyd2wild, и в его рецепте B12X тоже отмечен как обязательный к явному включению. Я пришёл к этому своим путём и мерил по своей методике, поэтому и числа у нас разные. Он приводит для своего стека ~29 tok/s на фолбэке против ~50-60+ с включённым B12X, то есть около двух раз. У меня на контролируемом парном протоколе получилось 13–17%, причём и база другая: мой auto даёт 59.7, а не 29. Мы включаем B12X разными способами (у него env‑переменная VLLM_USE_B12X_MOE, у меня CLI‑флаг на другом образе), так что это скорее два разных стека, чем спор о величине. Ниже мои замеры и мой способ их снимать.
Я оставил штатный --moe-backend auto: в документации vLLM этот режим описан как автоматический выбор backend по модели и железу. Но запускал я не mainline vLLM, а кастомный community‑образ со своей таблицей выбора. Читаю исходники внутри контейнера:
docker run --rm --entrypoint bash ghcr.io/anemll/dspark-vllm-gx10:0.1.1 \ -lc 'grep -rn "b12x" /usr/local/lib/python3.12/dist-packages/vllm/model_executor/layers/fused_moe/oracle/mxfp4.py'
# B12X backend for DeepSeek V4 native MXFP4 weights on SM120/GB10# ... excluded from auto-selection ...# use moe_backend='flashinfer_b12x' to opt in explicitly.
В этой сборке B12X исключён из auto‑selection. В логе движка выбранный путь виден прямо:
Using 'DEEPGEMM_MXFP4' Mxfp4 MoE backend
Добавляю --moe-backend flashinfer_b12x:
Using 'B12X_MXFP4' Mxfp4 MoE backendUsing B12xExpertsPrewarmed B12X route-pack capacities (experts=256, topk=6)
Сначала парная серия на 500-токенном профиле: перезапуск обеих нод, пять прогревочных запросов, три измеренных прогона, проверка выбранного бэкенда в логе перед каждым замером. Медианы:
|
метрика |
|
|
дельта |
|---|---|---|---|
|
code, c=1 |
57.0 |
65.5 |
+14.9% |
|
code, 12 параллельных, суммарно |
218.3 |
260.4 |
+19.3% |
|
heavy, c=1 |
39.4 |
43.7 |
+10.9% |
|
heavy, 12 параллельных, суммарно |
119.2 |
156.3 |
+31.1% |
|
клиентская decode, short‑context case, 500 токенов, среднее трёх streamed runs |
64.4 |
72.7 |
+12.9% |
Отдельно я снял контрольную серию именно для c=1, с раздельными снимками метрик и прямым счётом verification‑шагов по SSE‑чанкам. Профиль card‑like, приближённый к описанию карточки: max_tokens=128, пять прогонов на конфигурацию.
|
card‑like профиль |
|
|
дельта |
|---|---|---|---|
|
клиентская decode, среднее |
59.7 |
67.6 |
+13.3% |
|
клиентская decode, медиана |
59.8 |
69.7 |
+16.6% |
|
SSE‑derived шагов/с |
13.63 |
15.83 |
+16.2% |
|
выходных токенов за decode‑шаг |
4.38 |
4.27 |
−2.4% |
|
второй code‑профиль |
|
|
дельта |
|---|---|---|---|
|
клиентская decode, среднее |
62.8 |
71.9 |
+14.6% |
|
клиентская decode, медиана |
63.9 |
73.9 |
+15.6% |
|
SSE‑derived шагов/с |
13.34 |
15.69 |
+17.6% |
|
выходных токенов за decode‑шаг |
4.70 |
4.58 |
−2.6% |
Все три величины в каждой таблице посчитаны по одному и тому же decode‑окну, то есть выходные токены за шаг здесь равны отношению decode‑throughput к частоте шагов, а не completion_tokens / chunks.
Оба профиля дают согласованную картину: частота шагов растёт на 16–18%, а выходные токены за шаг снижаются примерно на 2.5%. То есть в этих двух c=1-профилях B12X не показал улучшения числа выходных токенов за decode‑шаг: основной прирост пришёлся на SSE‑derived частоту шагов. Этого хватает для итогового прироста в 13.3–14.6% по средним и 15.6–16.6% по медианам.
Про разброс стоит сказать отдельно, потому что он несимметричен. На card‑like профиле серия auto укладывается в 59,0–60,1, то есть размах около 2%. У B12X прогоны дали 69.7, 65.1, 62.6, 69.7 и 71.0, размах около 12%. Из‑за этого среднее равно 67.6, а медиана 69.7, и прирост составляет +13.3% по среднему либо +16.6% по медиане. Пяти прогонов недостаточно, чтобы надёжно охарактеризовать распределение B12X, поэтому я привожу обе статистики, а не выбираю удобную.
Два изменения, два множителя:
|
изменение |
частота шагов |
выходных токенов за шаг |
|---|---|---|
|
образ 0.21.1 → 0.25.2 |
расчётно −17% |
+47% |
|
|
SSE‑derived +16.2% |
−2.4% |
|
|
SSE‑derived +17.6% |
−2.6% |
Теперь о разбросе. В парной 500-токенной B12X‑серии суммарная скорость 12 параллельных запросов расходилась на 18–24% между одинаковыми прогонами, а один прогон b12x на code c=1 дал 47.0 против кластера 65–67. Прирост throughput наблюдался во всех показанных профилях, но при таком числе повторов его точная величина неустойчива. Защищаемая формулировка: около +13% на клиентской decode‑метрике, около +15% на одиночном запросе; на параллельной нагрузке прирост больше, но и шум выше.
6. Матрица экспериментов
Чтобы числа не смешивались, вот сводная матрица основных прогонов, формирующих центральную линию статьи:
|
этап |
чекпоинт |
образ |
KV |
MoE |
профиль |
метрика |
результат |
|---|---|---|---|---|---|---|---|
|
исходный |
drowzeys |
0.21.1 |
fp8 |
auto |
code |
e2e |
42.5 |
|
смена образа |
drowzeys |
0.25.2 |
fp8 |
auto |
code |
e2e |
51.8 |
|
сверка с карточкой |
drowzeys |
0.25.2 |
fp8 |
auto |
code, короткий ctx |
клиентская decode |
58.3 |
|
новый чекпоинт |
apetersson 0731 |
0.25.2 |
fp8 |
auto |
code |
e2e |
59.3 |
|
парный, auto |
apetersson 0731 |
0.25.2 |
fp8 |
auto |
code, 500 ток. |
клиентская decode |
64.4 |
|
парный, B12X |
apetersson 0731 |
0.25.2 |
fp8 |
B12X |
code, 500 ток. |
клиентская decode |
72.7 |
|
контроль c=1, auto |
apetersson 0731 |
0.25.2 |
fp8 |
auto |
code, 128 ток. |
клиентская decode, среднее 5 |
59.7 |
|
контроль c=1, B12X |
apetersson 0731 |
0.25.2 |
fp8 |
B12X |
code, 128 ток. |
клиентская decode, среднее 5 |
67.6 (медиана 69.7) |
Первые два прогона сделаны до того, как я стандартизировал протокол прогрева, поэтому они сравнимы между собой, но не напрямую с остальными. Парная серия снималась на 500-токенном профиле и по медианам трёх прогонов; контрольная серия для c=1 на card‑like 128-токенном профиле, по средним пяти прогонов и со счётом шагов по SSE‑чанкам. Именно контрольные числа я считаю основными: серия auto имеет размах около 2%, серия B12X около 12%, тогда как 500-токенные замеры параллельной нагрузки расходились на 18–24%.
7. Что не сработало
|
рычаг |
результат |
|---|---|
|
|
Агрегат кода вырос с 205.4 до 208.6 и 212.7, то есть на 1.6–3.6%. Агрегат прозы при этом упал со 146.5 до 65.5 и 137.3 на двух прогонах одной и той же конфигурации. Настройку не принял. |
|
|
движок не стартует, путь b12x в этой сборке только для MoE |
|
|
Включён в финальный launcher. Отдельного чистого A/B‑прогона с опубликованным raw‑логом у меня нет, поэтому прироста не заявляю. |
|
температура запроса 0 против 0.7 |
42.6 против 42.5 на сборке 0.21.1 |
|
обновление DGX OS и драйвера |
актуальная DGX OS несёт тот же 580.159.03, который уже установлен |
Отдельно про разброс. Два прогона конфигурации max-num-seqs 32 дали на профиле прозы суммарные 65.5 и 137.3 tok/s, то есть скорость отличалась вдвое при одинаковых настройках. Это и есть причина, по которой я не стал её принимать: дело не в среднем значении, а в невоспроизводимости. Практическое правило, которое я вынес из этой серии: после автоматической правки конфига проверяйте не то, что скрипт отработал без ошибки, а то, что изменение видно в логе запуска движка. У меня один из sed‑паттернов не сматчился и не вернул ошибку, из‑за чего два эксперимента оказались повтором одной конфигурации.
8. Итоговая конфигурация
# HEAD (rank 0). Peer запускается той же командой с --node-rank 1 и --headless,# но только после того, как на head порт rendezvous перешёл в LISTEN.docker run --gpus all --privileged --network host --ipc host --shm-size 10g \ --ulimit memlock=-1 --device /dev/infiniband:/dev/infiniband \ -v "$HOME/.cache/huggingface:/cache/huggingface" \ -e HF_HOME=/cache/huggingface -e VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 \ -e NCCL_IB_DISABLE=0 -e NCCL_IB_HCA=rocep1s0f0 -e NCCL_IB_GID_INDEX=3 \ -e NCCL_SOCKET_IFNAME=enp1s0f0np0 -e GLOO_SOCKET_IFNAME=enp1s0f0np0 \ ghcr.io/anemll/dspark-vllm-gx10@sha256:a83948492cf13df455170fb42885f5ef4db54fefe0feff0f841ecbff464ac9d8 \ apetersson/DeepSeek-V4-Flash-0731-Abliterated-FP8 \ --served-model-name dspark --host 0.0.0.0 --port 8000 --trust-remote-code \ --tensor-parallel-size 2 --kv-cache-dtype fp8 --block-size 256 \ --max-model-len 1048576 --max-num-seqs 12 --max-num-batched-tokens 8192 \ --gpu-memory-utilization 0.86 --enable-prefix-caching \ --moe-backend flashinfer_b12x \ --speculative-config '{"method":"dspark","num_speculative_tokens":5,"rejection_sample_method":"block"}' \ --tokenizer-mode deepseek_v4 --distributed-executor-backend mp \ --tool-call-parser deepseek_v4 --enable-auto-tool-choice --reasoning-parser deepseek_v4 \ --nnodes 2 --node-rank 0 --master-addr <HEAD_IP> --master-port 29701
|
метрика |
значение |
|---|---|
|
клиентская decode, c=1, card‑like профиль |
67.6 tok/s |
|
SSE‑derived частота шагов |
15.83/с |
|
code, c=1 e2e (500-токенный профиль) |
65.5 tok/s |
|
code, 12 параллельных, суммарно |
260.4 tok/s |
|
heavy, c=1 |
43.7 tok/s |
|
heavy, 12 параллельных, суммарно |
156.3 tok/s |
9. Границы применимости
-
1M контекста сконфигурирован, но не измерен. Сервер запускается с
--max-model-len 1048576, все замеры скорости сделаны на коротком и примерно 4K профилях. -
Меряется только пропускная способность. Оценки качества вывода не проводились.
-
Один экземпляр железа, одна тепловая обстановка, небольшое число повторов.
-
Abliterated‑checkpoint является экспериментальным производным артефактом. Никаких утверждений о его поведении помимо скорости я не делаю.
Чеклист, если повторяете
-
Проверьте строку выбора MoE‑бэкенда в своём логе:
grep "Mxfp4 MoE backend". Для использованного здесь образа и чекпоинта строкаDEEPGEMM_MXFP4означает, что специализированный B12X‑путь не выбран. На другой версии образа правила auto‑selection могут отличаться. -
Считайте
usage.completion_tokens, а не события стрима. Под спекулятивным декодированием один чанк несёт несколько токенов. -
Замерьте свой разброс до того, как поверите в прирост. На параллельной нагрузке он может существенно превышать 20%: у меня одна конфигурация дала 65.5 и 137.3 на двух одинаковых прогонах.
-
Сверяйте условия чужого бенчмарка целиком: профиль, длину контекста, методику подсчёта, KV dtype и версию образа.
-
Ограничения
num_speculative_tokensпроверяйте у своей конкретной сборки. -
Запускайте head первым, peer вторым.
Итог
Основную часть первоначального расхождения объяснила смена runtime‑образа, оставшийся разрыв закрыло приближение профиля к описанному в карточке. Уже после выравнивания runtime ещё один существенный и управляемый прирост дал явный выбор MoE‑бэкенда.
Эти изменения работают по‑разному. Новый образ поднял число выходных токенов за verification step ценой расчётной частоты шагов. B12X повысил SSE‑derived частоту шагов на 16–18%, при этом выходные токены за шаг в обоих c=1-профилях снизились примерно на 2.5%. Разложение tok/s на два множителя позволяет это различать; без него оба изменения выглядели бы одинаково.
В репозитории опубликованы рецепт запуска, харнессы, манифест окружения, а также raw‑логи парного B12X‑теста и финального c=1-контроля. Ранние проверки 43.2, 48.3 и 58.3 сохранились как сводные результаты без отдельных raw‑файлов. Всё лежит в репозитории.
ссылка на оригинал статьи https://habr.com/ru/articles/1066164/