DeepSeek 0731 на DGX Spark: разгоняю до 68 tok/s и разбираюсь, почему у автора карточки 57

от автора

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), для которых указываю суммарную скорость всех потоков вместе.

Два профиля нагрузки, потому что разброс между ними больше, чем эффект почти любой настройки:

профиль

что это

code

короткий системный промпт, генерация Python, 500 токенов вывода

heavy

около 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-токенном профиле: перезапуск обеих нод, пять прогревочных запросов, три измеренных прогона, проверка выбранного бэкенда в логе перед каждым замером. Медианы:

метрика

auto

flashinfer_b12x

дельта

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 профиль

auto

flashinfer_b12x

дельта

клиентская 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‑профиль

auto

flashinfer_b12x

дельта

клиентская 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%

auto → B12X, card‑like c=1

SSE‑derived +16.2%

−2.4%

auto → B12X, второй code c=1

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. Что не сработало

рычаг

результат

--max-num-seqs 12 → 32

Агрегат кода вырос с 205.4 до 208.6 и 212.7, то есть на 1.6–3.6%. Агрегат прозы при этом упал со 146.5 до 65.5 и 137.3 на двух прогонах одной и той же конфигурации. Настройку не принял.

--linear-backend flashinfer_b12x

движок не стартует, путь b12x в этой сборке только для MoE

rejection_sample_method: "block"

Включён в финальный 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/