GB300 DeepSeek Flash 4.1 Testing
dsv41-feat branch pins the 189 GiB Engram tables in Grace RAM and reads 70 GiB of routed experts over CUDA UVA; the in-checkpoint DSpark drafter (mtp.*, block 5) is loaded from the same weights. Decode is content-dependent: 82 tok/s single-stream on prose (the tight, repeatable instrument), 130–150 on code, shell and tool JSON where the drafter accepts 64–91%. A short request no longer waits behind a long prefill (one scheduler flag; 18 s → 1 s). Under the real Hermes agent loop it called tools on 10 of 10 turns that needed one. The config has not changed since last night; an overnight of further levers adopted nothing and retracted one claim — see the instrument note. KV pool holds 4.79M tokens, enough for 4.5 concurrent full-1M requests. Cold boot is 8 minutes from local NVMe (it was 92 this morning over SMB). Wired into Hermes as an experiment provider and passing real tool-use turns at 1M. Not promoted; the production lane on this Station is untouched.
Head-to-head, same box, same weights, same fixture
| Fixture (131K ctx, temp 0) | SGLang OffloaderV2 | vLLM UVA |
|---|---|---|
| C1 single stream | 3.3 tok/s | 85.2 tok/s |
| C4 aggregate / per stream | 13.0 / 3.25 | 172.7 / 43.2 |
| C8 aggregate / per stream | 25.8 / 3.23 | 183.5 / 22.9 |
| 300-word paragraph (~313 tok) | 94.9 s | 3.5 s |
| Count 1–60 (120 tok) | 36.9 s | 1.5 s |
17 × 19 → 323 | 22.4 s (cold) | 0.09 s |
Tool call get_weather | parsed | parsed |
Thinking, low effort → 36 | 25 reasoning tok | 24 reasoning tok |
Concurrency sweep: where is the knee?
The first head-to-head used 128-token generations, short enough that prefill and scheduling noise dominated at C8. Re-run with 192 forced output tokens, two reps per point, two server configs. max-num-seqs matters: at the default 8, anything above eight streams queues, so C12/C16 read as a flat ~270 that is a scheduler cap, not a bandwidth wall.
| Concurrency | seqs=8 agg / per-stream | seqs=16 agg / per-stream |
|---|---|---|
| 1 | 89.3 / 89.3 | 93.2 / 93.2 |
| 2 | 123.6 / 61.8 | 134.4 / 67.2 |
| 4 | 155.6 / 38.9 | 155.7 / 38.9 |
| 8 | 269.2 / 33.7 | 255.4 / 31.9 |
| 12 | 234.8 / 19.6 (queued) | 239.3 / 19.9 |
| 16 | 270.1 / 16.9 (queued) | 336.4 / 21.0 |
Aggregate is still rising at sixteen streams, so the UVA fetch ceiling is further out than the first C8 number suggested. Two things are not noise and are on the list: C4 is bimodal (122 vs 189 on back-to-back reps in both configs), and C8 dips slightly under seqs=16. Both smell like expert-page locality on the 60 GiB UVA tier between runs rather than kernel behavior.
DSpark: the speedup depends on what you are generating
The V4.1 DSpark drafter ships inside the target checkpoint (mtp.{0,1,2}.*, 2,401 tensors in shards 44–46). vLLM loads it with one flag, --speculative-config '{"method":"dspark","num_speculative_tokens":5}', at a cost of ~4 GiB of HBM for draft weights and graphs. Offload went from 60 to 70 GiB to pay for it, and KV came out ahead (15.56 GiB vs 12.03) because the draft's memory profile is small.
The first sweep was a disappointment: on the paragraph-generator fixture, k=5 was 10–18% slower than no speculation at every concurrency. The /metrics counters explained it. Over 7,998 draft steps, acceptance by position was 59.5 / 30.4 / 13.9 / 6.1 / 2.8%, 22.5% overall, 2.13 tokens per verify step. A verify step runs six tokens through forty MoE layers and fetches six times the expert rows over UVA, so landing two of them is a loss. Meanwhile the "count to sixty" probe went from 79 to 240 tok/s on the same server: ~4.5 tokens per step accepted. Same engine, same flags, 3× apart. The fixture decides.
So I built a fixture shaped like what a Hermes agent actually emits and measured acceptance per category:
| Category (C1, temp 0) | No spec | DSpark k=5 | DSpark k=3 | k=5 acceptance |
|---|---|---|---|---|
| Shell command sequences | 93 | 144–150 | 128 | 90.8% · 4.54 tok/step |
| Code (Python, bash) | 93 | 139–146 | 130 | 63.8% · 3.19 |
Tool-call JSON (real tools=) | 93 | ~130 | ~120 | 87.8% · 4.39 |
| Structured (tables, status bullets) | 93 | 111–116 | 108 | 49.5% · 2.48 |
| Prose | 93 | 89–93 | 95 | 29.8% · 1.49 |
| Weighted acceptance | — | 59.6% | 70.1% |
k=3 accepts a higher fraction of what it drafts (positions 3 and 4 were landing 6% and 3% on prose) but drafts less, so it nets fewer tokens per step on agent text and slightly more on prose. Neither dominates; for an agent lane, k=5 is the right default because the workload is code, tools, and shell. The published 79–86% acceptance numbers from other builds are on synthetic or code-heavy text, and the 4× DGX Spark repo's per-category table (code 73.8 vs prose 24.4 tok/s) is the same phenomenon at a lower floor. Nobody's number is wrong; "C1 tok/s" without a fixture description is now an incomplete statement for this model.
Two things that broke and how: the first k=5 boot re-ran the 74-minute autotune because the new engine-config hash directory was empty for the 60 seconds it took me to copy the cache in; restart with the seeded directory bound in 20 minutes and tuned only the 42 new draft shapes (168 → 210 configs). And each DSpark boot does two full passes over the checkpoint (target, then draft), which over SMB was 35 minutes of loading. That was the argument for the next section.
Local NVMe: cold boot 92 → 8 minutes
Two 8 TB WD SN850X went into the Station's M.2 slots as RAID0 (15 TB XFS at /models). Staged the full 510,313,343,553-byte checkpoint from the SHA-pinned archive, byte-verified every file, and relaunched with the same flags:
| Phase | SMB (this afternoon) | Local NVMe |
|---|---|---|
| Target shards + pinning | 1,246 s | 133 s (shards alone: 21 s) |
| DSpark draft pass | ~600 s | 20 s |
| Graph capture | 31 s | 34 s |
| Autotune (cached) | ~1 min | ~1 min |
| Launch → bound | ~35 min | 8–12 min |
Decode did not change (count 252.7, prose 80.7 tok/s on the smoke, within noise of the SMB build). Boot is now cheap enough that flag experiments are a coffee break, not an afternoon. The staged-shard trick from this morning (Engram shards local, everything else symlinked to the NAS) is retired.
1M context
131K was a defensive choice from when we were 4 GiB short on HBM. The model's max_position_embeddings is 1,048,576 and the KV pool was already holding 3M tokens, so the only question was what the 1M profiling pass would cost:
| 131K | 1M | |
|---|---|---|
| CUDA graphs | 1.00 GiB | 1.52 GiB |
| KV pool | 15.56 GiB | 10.05 GiB |
| KV tokens | 3.07M | 4.79M (sparser layout at 1M) |
| Max concurrent at full length | 23× at 131K | 4.5× at 1M |
| C1 code / shell / prose | 139 / 144 / 89 | 146 / 150 / 93 |
| C16 aggregate (prose) | 276 | 287 |
Costs 5.5 GiB of KV headroom, changes nothing about decode speed, and the Hermes provider now advertises context_length: 1048576. Two real turns at --reasoning low and high came back clean.
Acceptance on real transcripts, not fixtures
The agent fixture above was my guess at what Hermes emits. To check it, I pulled 24 turns from today's actual session (12 that ended in a tool call, 12 text) out of the Hermes session store, replayed each with its preceding context and the real terminal / read_file / web_search tool schemas, temperature 0, thinking off, 400-token cap, and read DSpark acceptance from /metrics deltas per turn.
| Turn type | n | tok/s | Draft acceptance | Tokens/step |
|---|---|---|---|---|
| Ended in tool call (original) | 12 | 79.5 | 51.7% | 2.58 |
| Text | 12 | 98.4 | 46.8% | 2.34 |
| All | 24 | 88.1 | 49.1% | 2.46 |
Per turn the spread is 5–171 tok/s. The turns where V4.1 actually emitted a tool call ran at 138–171 tok/s with 65–86% acceptance, matching the synthetic fixture. The turns that dragged the mean down were ones where the model wrote prose about what it was going to do instead of calling the tool, and prose accepts at 30–45%. Net: on real traffic, DSpark k=5 is a wash against no speculation (88 vs 93), with a large upside on the turns that are genuinely code or JSON. It stays on. The 150 number is real but it describes a fixture, not a day.
The replay also surfaced something that is not a speed result: in this bare setup (two-line system prompt, no Hermes harness), V4.1 produced a tool call on only 4 of the 12 turns where the original model had. That was a flag, not a verdict. Resolved the next morning: under the real harness it is 10/10 (below).
Correction, September 11. The 88.1 tok/s figure in this table should not be read as a decode number. Rerunning the same 24-turn replay three times on the same idle server, same flags, temperature 0, gave 90.6 / 119.4 / 124.3 tok/s. Batched MoE plus speculative decode is not bit-deterministic, the outputs differ by hundreds of tokens between runs, and the mean swings ±17. The acceptance percentages hold up; the tok/s does not. The concurrency sweep above runs each point twice and the pair agrees within about 1% — that is the instrument. This paragraph exists because I published an overnight "win" on the replay number and the sweep contradicted it. Details in the ledger below.
Mixed workload: a short request behind a long prefill
Everything to this point was one request at a time. The failure mode that matters for a shared lane is a 480K-token prompt arriving and every other user waiting. Measured: fire a 480K cold prefill, then a one-sentence question every 5 seconds behind it.
| Short request fired at | Default scheduler | --long-prefill-token-threshold 6144 |
|---|---|---|
| +5 s into the long prefill | 18.2 s TTFT | 0.97 s |
| +10 s | 13.2 s | 1.09 s |
| +15 s | 8.2 s | 0.89 s |
| +20 s | 3.2 s | 0.92 s |
| Long request TTFT | 23.1 s | 26.8 s (+16%) |
| Solo short TTFT | 0.10 s | |
Default vLLM runs the long request's 8192-token chunks back to back and admits nothing else until it finishes; every short request landed at exactly the moment the long prefill completed. Under a 972K prompt that is 85 seconds of dead air for everyone. The fix is one flag: capping a long request's per-step chunk at 6144 leaves 2048 tokens of each step for other requests, and short requests get a first token in about a second. They still share the GPU with the long chunk, so their decode during the overlap is slow (~2.5 tok/s vs 150 solo); interleaved, not isolated. The long request pays 16%. That is the right trade for a shared lane, and it means the 1M config can be the daily lane rather than a separate one. The older --max-num-partial-prefills knobs are gone from this vLLM build.
Offload notch at 1M
With boot at 8 minutes, the 60-vs-70 question was cheap to answer. At 1M with DSpark: offload 60 loads 226.77 GiB into HBM and dies at Available KV cache memory: −2.61 GiB. Offload 70 is the floor for this configuration; the 10 GiB difference buys 12.7 GiB of KV once the draft model, its graphs, and the 1M profile are accounted for.
| Offload | 131K, no spec | 131K + DSpark | 1M + DSpark |
|---|---|---|---|
| 40 GiB | KV −6.96 | — | — |
| 60 GiB | 12.03 ✓ | — | −2.61 ✗ |
| 70 GiB | — | 15.56 ✓ | 10.05 ✓ reference |
Prefill: a 972K-token prompt in 85 seconds
Everything above is decode. Cold prefill measured on the v10 reference config with the skill's probe: a random nonce at the start of the prompt (so no prefix-cache hit), random-word filler, max_tokens=1, thinking off, rate = prompt tokens / wall time. That is effective time-to-first-token, not a kernel counter; the server's --max-num-batched-tokens 8192 chunking is included.
| Prompt tokens | TTFT | Prefill tok/s | Reps |
|---|---|---|---|
| 6,538 | 0.37 s | 17,900 | 2, identical |
| 25,920 | 1.54 s (one cold outlier 4.5 s) | 16,900 | 2 |
| 51,797 | 2.67 s (outlier 5.5 s) | 19,400 | 2 |
| 103,814 | 5.36 s | 19,300 | 2, identical |
| 207,339 | 11.3 s | 18,300 | 2, identical |
| 414,624 | 24.9 s | 16,700 | 2, identical |
| 972,435 | 85.0 s | 11,400 | 1 |
Flat at roughly 18K tok/s from 6K to 200K, easing to 16.7K at 415K and 11.4K at 972K as the sparse indexer's top-k over a longer candidate set starts to cost. Zero preemptions; HBM stayed at 243 GiB through the 972K request, so the 10 GiB KV pool is genuinely holding a full-length sequence. The two outliers at 26K and 52K were first-touch of new UVA expert pages after idle and did not recur.
For scale: the 8× B200 report on the 0731 predecessor measured 1M TTFT of 84.7 s at c=8 under DP8 and 314.6 s under TP8. One Station at c=1 landing at 85 s for 972K with experts on UVA is the same order as an eight-GPU node's best case, which says the encoder half (8B active per prompt token) is doing what the architecture promised. What this does not say: nothing here is measured with concurrent short requests alongside a long prefill, and vLLM's partial-prefill knobs were reported not to help on that class of model. The 1M lane is a one-user-at-a-time lane until that is measured.
reasoning_effort: medium (HTTP 400 … must be low, high, xhigh, max, or an integer within [1, 100]). If your agent's default effort is "medium," every request fails until you override it per-provider.The autotune cache key trap
FlashInfer keys its tuned-config cache on a hash of the engine config. Changing --max-num-seqs from 8 to 16 produced a fresh hash with an empty directory, and the second boot was about to spend another 74 minutes re-tuning kernels whose shapes had not changed (the tuner runs at 8192 prefill tokens and the graph batch sizes, neither of which depends on the sequence cap). Copying autotune_configs.json from the old hash directory into the new one was accepted: Loaded 168 configs, graph capture in 8 s, bind in 20 minutes instead of 92. Worth knowing before touching any launch flag on a Blackwell box. I tripped this three more times today (--max-num-seqs, the speculative config, and --max-model-len each change the hash); the copy has to land before the tuner reads the directory, which is roughly two minutes after the KV gate. Seed it before launch, or disable autotune for smoke boots.
Hermes on it: the tool-calling gate
Added as an experiment provider (custom[dsv41] → :30006). The first smoke was five mixed turns, all clean. The bare replay above then raised the question that matters more than any speed: will it actually call the tool, or narrate calling it? So the gate is the real agent loop, not the raw API: hermes chat --provider custom[dsv41] --reasoning low -t hermes-cli, ten prompts each of which cannot be answered honestly without a tool — count lines in a file, read a secret string, parse a port from JSON, run uname, write a file and confirm it, append a line and report the new count, compute 1234×5678 in a shell. Scored from the harness's own tool_turns= log line plus a ground-truth check of the answer or the file on disk.
| Prompts | Turns that called a tool | Correct answer / side effect | Wall per prompt |
|---|---|---|---|
| 10 | 10 | 10 | 8–14 s incl. agent bootstrap |
The 8-of-12 narration in the bare replay was a property of a two-line system prompt, not of the model. Under the real preamble and tool schemas it just calls the tool. Two things you need for this: --tool-call-parser deepseek_v41 --reasoning-parser deepseek_v41 --enable-auto-tool-choice on the server, and --reasoning low (or high) on the client — the V4.1 chat template rejects medium with a 400. Still not routed as a default; it is a lane you pick, but the reason it couldn't be is gone.
Overnight, September 10–11: five levers, nothing adopted, one lesson
With the config settled I wrote a plan and handed the box to a second agent for the night: measure prefix-cache TTFT, then try transparent huge pages, unpinned host memory (VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY=1), --language-model-only, and the Rust frontend, each with a mechanism check and a win bar. The operator ran it exactly as written and reported two wins. In the morning I reran the win metric three times and it was noise (previous section). Re-read on the concurrency sweep:
| Experiment | Mechanism check | C1 vs 82.1 | Verdict |
|---|---|---|---|
| Prefix cache, 8.8K-token system prompt | hits 0 → 34k | — | TTFT 0.665 → 0.360 s warm. Measurement, works as advertised. |
THP enabled=always | AnonHugePages 0.5 → 2.5 GB (needed ≥50) | 78.4 | Did not engage. The 354 GB of offloaded weights sit in shmem, and shmem_enabled is never; the enabled knob was never the relevant one. Tried again on the unpinned path: same 2.5 GB, 77.3 tok/s. |
| Unpinned host memory (ATS) | host used 450 → 384 GB | 76.3 (−7%) | Real mechanism, wrong direction. 66 GB of RAM back that nothing needs, at a decode cost. Reverted. |
--language-model-only | KV 10.05 → 11.14 GiB | 76.2 | +1.1 GiB, not the 3 needed to make offload 65 fit. |
VLLM_USE_RUST_FRONTEND=1 | tool parser survived | 76.0 | No decode gain; 6.5K-token TTFT 0.37 → 0.52 s. Reverted. |
The lane went back to last night's reference at 5:56 AM and re-measured 80.1 tok/s at C1. The lesson is not about any of the flags. It is that I set the win bar on the instrument I had, not the one that was tight, and a diligent operator hit the bar honestly. Bars go on the sweep now, and the recipe says so out loud.
The recipe
All of this is now a formal, machine-checked recipe in J&M Recipes: dgx-station-gb300/deepseek-v4.1-flash-vllm-uva-dspark — pinned image digest and model revision, the launch script with the knobs, every script that produced a number here, the provenance bundle, the five gates with pass dates, and a failure ledger that includes the offload bracket, the four autotune misses, and the overnight above. That is the version to reproduce from; this post is the story.
Why 26×: bytes moved per token
Both engines keep Engram on the Grace side. The difference is how the offloaded experts get to the GPU. SGLang's OffloaderV2 in cpu mode copies the entire expert tensor for each offloaded layer, every token: 15 layers × 6.72 GiB ≈ 100 GiB across the C2C link per decode step. At roughly 330 GB/s that is about 300 ms, which is exactly the 3.3 tok/s we measured. The GPU showed 100% utilization because it was stalled on the copy stream, and batching scaled perfectly because the copy was amortized. The profile said "bandwidth," not "compute."
vLLM's UVA backend maps the pinned host tensor into GPU address space and lets the MoE kernel read the rows the router selected: 6 of 384 experts per layer, about 1.6 GB per token instead of 100 GiB. We had already seen this exact trade on this exact box with full GLM-5.3 (1.4 tok/s SGLang OffloaderV2 → 33 tok/s vLLM UVA), so the prediction was cheap; the day was spent on memory, not on the idea.
The memory ladder
Ten launches to find the envelope. Every failure was a different wall.
| Run | Engine | Offload | Died at |
|---|---|---|---|
| A | SGLang | Engram host table only | HBM OOM at layer construction; the second tier is mandatory |
| b1 | SGLang | 13 expert layers | HBM OOM in the post-load FP4 shuffle, ~4 GiB short |
| b2 | SGLang | 19 layers | Kernel OOM killer: ~490 GB shmem+anon on a 494 GB Grace |
| b3 | SGLang | 15 layers | Bound. 3.3 tok/s. |
| b4 | SGLang | 13 layers + expandable segments | HBM OOM again; the shuffle scratch is real |
| v1 | vLLM | 105 GiB, matcher experts.* | HBM OOM: vLLM nests MoE weights under routed_experts.; the matcher silently hit nothing |
| v2 | vLLM | 105 GiB | An hour of CIFS thrash: pinned host tensors page-fault the checkpoint through a 4 GB page cache, one SMB read at a time. Killed. |
| v3 | vLLM | 105 GiB, Engram shards on NVMe | OOM killer at 486 GB shmem: pinned copies plus mmap'd shards double-count during fill |
| v4 | vLLM | 40 GiB | Cleared everything, then Available KV cache memory: −6.96 GiB |
| v5 | vLLM | 60 GiB | Bound. 85 tok/s. 219 GiB weights, 12 GiB KV, host 430/494 GB. |
| v6 | vLLM | 60 GiB, seqs 16 | Bound. 93 / 336 tok/s (C1 / C16). Autotune hash changed; cache copied. |
| v7 | vLLM | 70 GiB, DSpark k=5 | Bound. KV 15.56 GiB. Agent fixture 131–144, prose 89. |
| v8 | vLLM | 70 GiB, DSpark k=3 | Bound. Higher acceptance, fewer tokens per step; not better on agent text. |
| v9 | vLLM | 70 GiB, k=5, local NVMe | Bound in 12 min cold. Same decode as v7. |
| v10 | vLLM | 70 GiB, k=5, 1M ctx, NVMe | Bound in 8 min. 146–150 code/shell, 93 prose, 287 at C16. KV 4.79M tokens. Short requests starve behind long prefill. |
| v11 | vLLM | v10 + --long-prefill-token-threshold 6144 | Bound. Short TTFT behind a 480K prefill 18 s → 1 s. Current reference. |
| v12 | vLLM | v11 at 60 GiB | KV −2.61 GiB. 70 is the floor at 1M. |
First-boot autotune
FlashInfer autotunes the MXFP4 MoE kernel by running every tactic on the real hardware at 8192 tokens and keeping the fastest. With experts on UVA, each profiled run also pays the C2C fetch, so a single candidate took ~39 ms and the whole pass took 74 minutes with nothing in the log but 100% GPU. A py-spy stack (choose_one → trtllm_fp4_block_scale_moe_op) is what separated "working" from "hung." The 168 tuned configs are cached under /root/.cache/vllm, so this cost is paid once per image.
History: the SGLang first boot (morning of September 10)
Why this was uncertain
The release is 552B backbone plus a 196B Engram table, shipped as a mixed FP4/FP8/BF16 checkpoint. HBM on this Station is 250.7 GiB visible. The published SGLang recipe for GB300 is tensor-parallel across four GPUs; nobody had posted a TP=1 boot on one Station. The arithmetic said it was close: move the Engram tables to host memory as DeepSeek's report intends, and the resident set still overshoots HBM by tens of GiB before any KV cache. So a second offload tier was mandatory, and SGLang's DeepSeek-V4 model file did not wire one.
What the SGLang boot took
| Step | Result |
|---|---|
Archive to Milo-Ark, SHA-pinned df42c109f1def… | 510,313,345,254 bytes, every file byte-matched against the Hub tree. NAS-direct, provenance written first. |
Pull lmsysorg/sglang:dev-dsv41 | 48.5 GB preview image. Ships SGLANG_ENABLE_DSV41_ENGRAM_HOST_TABLE and the grouped expert offloader (V2). |
| Read the model code | deepseek_v4.py builds layers without offloader_kwargs; only the V2/V3 family passes them. Wrote a 23-line patch mirroring the V2 wiring with the TRT-LLM MXFP4 parameter names (w13_weight, w2_weight, *_weight_scale_inv). Bind-mounted read-only into the container; image untouched. |
| Stage A: Engram host table only | CUDA OOM at layer construction. Confirms the second tier is required. |
| b1: offload 13 layers (group 3, 1 per group) | All 475 GiB loaded from the NAS; Engram pinned; then CUDA OOM inside the post-load FP4 weight shuffle, roughly 4 GiB short. Fit is that close. |
| b2: offload 19 layers (group 2, 1 per group) | Kernel OOM killer took the scheduler: ~190 GiB anonymous plus ~280 GiB shared on a 494 GiB Grace. Two pinned Engram shards, 19 layers of pinned expert copies, and loader staging do not co-exist. |
b3: offload 15 layers (group 8, 3 per group), mem fraction 0.85, expandable_segments | Bound. Weights in 1,614 s. 60.9 GB HBM free after weights, 37.3 GB after KV pool, 29.4 GB after decode CUDA graphs (bs 1/2/4/8). Host at 429 of 494 GB. |
SGLang smoke results
| Probe | Result |
|---|---|
| 17 × 19, integer only | 323, 2 tokens, 22.4 s wall (first request; includes JIT and cold path) |
| Count 1–60 | Correct sequence, 120 tokens in 36.9 s → 3.2 tok/s |
| 300-word paragraph | 314 tokens in 94.9 s → 3.3 tok/s, coherent, terminated cleanly |
Tool call (get_weather) | finish_reason=tool_calls, arguments {"location": "Telluride"}, parsed by the auto tool parser |
Thinking on, reasoning_effort=low | 25 reasoning tokens, correct answer 36 for 15% of 240 |
Correctness, tool-call parsing, and reasoning-mode plumbing all pass on the first boot of a day-zero preview image. Speed does not. With 15 of 40 expert layers streaming from LPDDR5X through a synchronous one-step prefetch, decode is bandwidth-bound on the Grace side. The 16B-active design keeps the bytes per token small, but the offloader path as shipped is not built for latency.
Verdict
Next
- REAP expert pruning, 384 → 256. The one structural lever left: all experts in HBM, no C2C term in decode. Multi-day, nobody has done it on V4.1, and it would be a post of its own.
- Huge pages the right way.
transparent_hugepage/shmem_enabled, notenabled, is where the weights actually live. One boot, sweep-gated. - C4 bimodality. Still reproducible, still uncharacterized, still not actionable.
Reproduce (vLLM, the fast path)
docker run -d --name dsv41-vllm --gpus all --ipc host --network host \
--ulimit memlock=-1 --cap-add IPC_LOCK \
-v /models/DeepSeek-V4.1-Flash-df42c109f1defefcbfcedbe7d905718a12266e40:/model:ro \
-v /path/to/vllm-cache:/root/.cache/vllm \
vllm/vllm-openai:deepseekv41-flash-0909 \
--model /model --trust-remote-code --tensor-parallel-size 1 \
--offload-backend uva --cpu-offload-gb 70 \
--cpu-offload-params routed_experts.w13_weight routed_experts.w2_weight \
--engram-config '{"cpu_offload": true}' \
--speculative-config '{"method":"dspark","num_speculative_tokens":5}' \
--max-model-len 1048576 --max-num-seqs 16 --max-num-batched-tokens 8192 \
--long-prefill-token-threshold 6144 \
--gpu-memory-utilization 0.94 \
--tool-call-parser deepseek_v41 --reasoning-parser deepseek_v41 --enable-auto-tool-choice \
--port 30006
Whole checkpoint on local NVMe; pinned host copies page-fault from the checkpoint after load, DSpark reads it twice, and over CIFS neither converges. The matcher is routed_experts.*, not experts.*. Expect ~8–12 min to bind with a warm autotune cache, ~80 cold. Every launch-flag change makes a new autotune hash directory: seed it with an existing autotune_configs.json before launch or you pay the 74 minutes again. Drop --speculative-config and go back to --cpu-offload-gb 60 for a no-spec build. Do not send reasoning_effort: medium.
Reproduce (SGLang, the 3.3 tok/s history)
docker run -d --name dsv41-dryrun --gpus all --ipc host --network host \
--ulimit memlock=-1 --cap-add IPC_LOCK \
-v /path/to/DeepSeek-V4.1-Flash:/model:ro \
-v /path/to/patched/deepseek_v4.py:/sgl-workspace/sglang/python/sglang/srt/models/deepseek_v4.py:ro \
-e SGLANG_ENABLE_DSV41_ENGRAM_HOST_TABLE=1 \
-e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
lmsysorg/sglang:dev-dsv41 \
python3 -m sglang.launch_server --trust-remote-code --model-path /model --tp 1 \
--mem-fraction-static 0.85 --context-length 131072 \
--chunked-prefill-size 8192 --max-running-requests 8 \
--cpu-offload-gb 0 --offload-mode cpu \
--offload-group-size 8 --offload-num-in-group 3 --offload-prefetch-step 1 \
--reasoning-parser auto --tool-call-parser auto --port 30005
The patch adds offloader_kwargs to the make_layers call in DeepseekV4Model, copied from DeepseekV2Model, whitelisting the fused-MoE weight and scale tensors. Drop the page cache before launch; the Engram pre-fault needs the RAM. Expect roughly 27 minutes to first token over 10GbE storage.
Credits
- 0xSero — the 4× RTX PRO 6000 build with a 45-case published sweep and DSpark acceptance numbers; the receipt that DSpark works on the shipped weights.
- vLLM — the
dsv41-featbranch and the recipe page:EngramConfig.cpu_offloadand the UVA offloader are the whole reason the second number exists. - DeepSeek — weights, tech report, and the Engram design that makes host placement legitimate rather than a hack.
- SGLang / LMSYS — day-zero support, the Engram host-table path, and the grouped offloader.
- Tony (tonyd2wild) — the parallel 4× DGX Spark vLLM effort, which framed the memory math we checked against.