allsmartllm.com does not run on rented cloud GPUs. It runs on four NVIDIA DGX Spark machines in our own office. One of them, which we call spark-3, has served two language models side by side since September 2026: Qwen3.6-35B-A3B as the general-purpose model and Nemotron 3.5 Lightning as the fast one next to it.
This is an operations report. It covers the configuration that runs today, how we got there, and the places where we were wrong along the way. We kept the wrong turns in on purpose. If you run a Spark yourself, they are probably more useful than the parts that worked.
Results
| Qwen3.6-35B-A3B | Nemotron 3.5 Lightning | |
|---|---|---|
| Weights | NVFP4 MoE experts, FP8 for the other layers | NVFP4, plus DSpark draft model |
| Single stream, decode, other model idle | 78.6 tok/s | 111.7 tok/s |
| 8 concurrent requests, aggregate, other model idle | 263.7 tok/s | 293.5 tok/s |
| 8 concurrent requests, aggregate, other model also under load | 107.7 tok/s | 149.6 tok/s |
| Context | 262,144 tokens | 262,144 tokens |
| Share of GPU memory | 0.45 | 0.28 |
| Crashes since current config | none since 27 Sep 2026 | none since 6 Sep 2026 |
The throughput rows were measured on 19 October 2026 with short prompts (see below). The two models share one memory bus. When both are busy at once, Qwen keeps about 40 % and Nemotron about half of what each reaches alone.
For comparison: on 5 September 2026, Qwen3.6-35B in FP8 with vLLM 0.18 reached 52.6 tok/s single stream. That was not a measurement on an otherwise empty machine. A third model, LFM2.5-8B, was loaded alongside it at the time.
How we measure
All throughput numbers in this article come from the same in-house script, run directly on the Spark against vLLM's OpenAI-compatible endpoint, not through our gateway. The script is published here: bench.py.
- Requests are streamed (SSE). Reasoning tokens and answer tokens are counted together.
- Output: up to 512 tokens per request,
temperature 0, thinking enabled. At 512 tokens the models are still in their reasoning phase, so the numbers describe reasoning output. - Input: ten short German prompts from our real workload (code, SQL, ops diagnostics, explanations), 39 to 72 input tokens each. A single-stream run uses the first prompt (48 tokens), a run with 8 concurrent requests uses the first eight.
- Single stream: decode rate only, computed as (completion tokens − 1) / (time of last chunk − time of first chunk). Prefill and time to first token are excluded.
- Concurrent runs: aggregate throughput, total completion tokens divided by wall time, so prefill is included.
- Each configuration is run three times; we report the median.
- "Other model idle" means no benchmark load on the second model. We read its request counter before and after each run: during the Qwen runs Nemotron received no other requests, during the Nemotron runs Qwen received 2. For the older measurements from 6 and 27 September we checked the access logs afterwards: the other model received no requests during the measurement windows.
Short prompts are the favourable case. Real requests often carry thousands of tokens of context, and then prefill dominates the waiting time. We therefore measured long inputs separately, see "Long inputs" below.
One lesson about measuring through a gateway: our nightly benchmark goes through the full request path. For a week in October it showed half the usual throughput for both spark-3 models. Measured directly on the machine, nothing had changed. Other requests were running in parallel during the nightly window. A benchmark through a shared gateway measures the total load, not the machine.
The machine, and why it behaves differently
A DGX Spark has a GB10 chip with 128 GB of memory shared between CPU and GPU, of which CUDA sees 121.7 GiB, and about 273 GB/s of memory bandwidth. In this article, measured memory values are given in GiB (as free and nvidia-smi report them, converted from MiB). Vendor figures and file sizes stay in GB. Two properties drive everything else:
- There is no separate GPU memory. Whatever vLLM reserves for weights and KV cache is gone for the operating system. When memory runs out, the machine does not return an error. It freezes and needs a power cycle. This happened to us on another Spark before we had a watchdog.
- Bandwidth is the bottleneck. For every generated token, the active weights have to pass through memory once. That is why mixture-of-experts models with few active parameters are much faster on the Spark than dense models of similar total size. It is also why two models on one machine slow each other down as soon as both are busy.
Where the memory goes
The two --gpu-memory-utilization values add up to 0.73 of 121.7 GiB, which is 88.8 GiB. free shows more than that as used. A snapshot on 19 October 2026 with both models idle:
| GiB | |
|---|---|
Used according to free |
104.5 |
Both vLLM processes according to nvidia-smi (Qwen 52.9, Nemotron 36.7) |
89.7 |
Ordinary process memory (AnonPages: Python, Docker, system services) |
10.3 |
Kernel (Slab, PageTables, KernelStack) |
1.5 |
| Not attributed | 3.1 |
Available according to free |
17.1 |
The vLLM processes take slightly more than their configured share (89.7 instead of 88.8 GiB), because the CUDA context and the compiled graphs come on top. The rest is ordinary process memory that every vLLM process also needs on the CPU side. Page cache does not count as used. The kernel can reclaim it, which is why "available" is larger than "free". Another 4.5 GiB of process memory sat in swap at the time of the snapshot.
Operating rules
- Keep at least 12 GiB available, better 15. On spark-3 today it is 17.1 GiB.
- Start every new configuration the first time under a watchdog (described below).
- Trust only
freeandnvidia-smi. On 19 Octoberdocker statsshowed 7.7 GiB for the Qwen container, whilenvidia-smishowed 52.9 GiB for the same process. CUDA allocations on unified memory do not show up in the container's cgroup accounting. - Disable proactive memory compaction:
vm.compaction_proactiveness = 0. On our two other Sparks, which run a model with tensor parallelism across both machines, decode speed first started to split into fast and slow runs after 28 days of uptime and dropped from 48 to 34 tok/s after 43 days. We found the cause by recording the CPU time of the kernel threadkcompactd0during each benchmark run: every slow run coincided withkcompactd0running at close to 100 % on one of the two machines. It was trying, endlessly and mostly without success, to move memory pages pinned by the GPU. With the setting at 0, all ten test runs were back at 47 to 48 tok/s. spark-3 showed the same history in its counters, so it got the same setting.
The watchdog
The watchdog is a small shell script that runs next to the first start of a new configuration. Every second it reads MemAvailable and swap usage from /proc/meminfo and writes both to a log. It kills the container with docker kill, without a grace period, as soon as one of two things happens:
MemAvailabledrops below 12,000 MiB, or- swap usage grows by more than 8,000 MiB compared to the value at start.
The second condition matters because on this machine memory pressure often shows up in swap first, while MemAvailable still looks fine. By default it runs for 90 minutes, long enough for model loading, compilation and a first benchmark. Script: vllm-start-watchdog.sh.
Our first version checked every five seconds and stopped the container gracefully. That turned out to be too slow on one occasion, so the current version checks every second and kills.
Step 1: from 52 to 78 tok/s, after a wrong conclusion
Our first research on NVFP4 (4-bit weights instead of 8-bit) concluded that it was slower than FP8 on the Spark. A third-party benchmark showed −32 %, and the technical explanation sounded plausible: a missing instruction in the compile target and a detour through BF16.
That conclusion was wrong for our case. One day later we tried anyway, with two differences: NVIDIA's mixed-precision checkpoint (NVFP4 for the experts, FP8 for the remaining layers) and vLLM 0.28 instead of 0.18. Measured on 5 September 2026:
| FP8, vLLM 0.18 | NVFP4, vLLM 0.28 | |
|---|---|---|
| Single stream | 52.6 tok/s | 77.9 tok/s (+48 %) |
| 8 concurrent | 150.9 tok/s | 261.7 tok/s (+74 %) |
| Memory used on the machine | about 96 GiB | about 81 GiB |
Both columns were measured with LFM2.5-8B (0.16) loaded alongside and idle. The memory row is the whole machine at that time: Qwen at 0.55 (FP8) or 0.45 (NVFP4), plus LFM. LFM was retired one day later to make room for Nemotron.
Neither change was enough on its own. vLLM 0.28 with the FP8 checkpoint was slower than 0.18 (−5 % single stream, −17 % at 16 concurrent requests). The third-party source that had promised 97 tok/s for NVFP4 listed 28 to 30 tok/s for FP8, while we had measured 52.6. Its baseline did not match ours. Measure your own baseline before you compare against other people's numbers.
A related trap: the log line that names the selected kernel backend (TRITON, DEEPGEMM, MARLIN) told us almost nothing about speed on this hardware. We drew the wrong conclusion from it twice, once too optimistic, once too pessimistic.
Step 2: the biggest quality lever has nothing to do with speed
Qwen3.6 reasons before it answers. Without a limit it reasons for so long that most answers never arrive: the whole token budget goes into the reasoning phase.
vLLM can cap the reasoning phase per request (thinking_token_budget). We measured the effect on 19 October 2026 on the configuration that runs today (NVFP4, vLLM 0.28): the same ten German prompts, max_tokens 2048, temperature 0, no streaming. "Complete" means the model ended the answer itself instead of hitting max_tokens. Language was detected with a simple function-word heuristic on the first 300 characters of each answer.
| Budget | no answer at all | complete answer | answer in wrong language |
|---|---|---|---|
| none | 7/10 | 1/10 | n/a |
| 512 | 0/10 | 7/10 | 6/10 |
| 768 | 0/10 | 7/10 | 2/10 |
| 1536 | 0/10 | 1/10 | 0/10 |
The 1536 row looks good in the language column, but 9 of 10 answers were cut off: after 1536 tokens of reasoning, only about 500 are left for the answer. 512 is the fastest, but in 6 of 10 cases the model switched to English. 768 is the compromise we use, although 3 of 10 answers were still cut off at max_tokens 2048. In production our gateway sets no max_tokens for these two models; the caller decides, and without a value vLLM allows output up to the remaining context. Our first series, on 5 September with FP8 and vLLM 0.18, pointed the same way: 5 of 10 answers in the wrong language at 512, 2 of 10 at 768.
The heuristic is crude. On the FP8 data from 5 September it reproduces every documented value except one (1536: 4 instead of 3). Read the language column as a trend, not as an exact rate.
For comparison, NVIDIA's model card for the NVFP4 checkpoint lists MMLU Pro at 85.0 against 85.6 for BF16. The reasoning budget took the share of prompts without an answer from 7 out of 10 to zero. How the model is allowed to reason mattered far more for us than the number format.
Our gateway sends the same budget, 768, to Nemotron. We have not measured a budget series for Nemotron.
Two practical notes. We found no server-side default for this value in the vLLM versions we checked: thinking_token_budget has to be sent with each request, which our gateway does. And the server has to be started with --reasoning-config, otherwise it rejects the field.
Step 3: a second model on the same machine
With NVFP4, Qwen needed only 0.45 of the memory, which left room for a second model.
We chose Nemotron 3.5 Lightning (30B total, 3B active, a Mamba-2/MoE hybrid) because NVIDIA ships a DSpark draft model for it, built specifically for the DGX Spark. The draft model proposes three tokens ahead and the main model verifies them in one pass. Tokens the main model would not have produced are rejected, so the output follows the main model's distribution. We have not checked whether the output is bit-identical to a run without the draft model. Measured on 6 September 2026, with Qwen idle, temperature 0, reasoning output:
| without draft model | with DSpark | |
|---|---|---|
| Single stream | 79.0 tok/s | 111.7 tok/s (+41 %) |
| 4 concurrent | 173.6 tok/s | 206.7 tok/s (+19 %) |
| 8 concurrent | 237.5 tok/s | 295.0 tok/s (+24 %) |
| 16 concurrent | 327.3 tok/s | 435.2 tok/s (+33 %) |
How much a draft model helps depends on how predictable the text is. vLLM counts proposed and accepted tokens. During our benchmark runs on 19 October (short prompts, reasoning output, temperature 0, Qwen idle), the main model accepted 55.4 % of the proposed tokens, 2.66 tokens per verification step on average over 7,694 steps. We have no clean value for production traffic: vLLM's counter runs since the start on 6 September and also contains our own benchmark runs.
The draft model is 1.3 GB on disk. The other flags from NVIDIA's recipe (--mamba-backend flashinfer, --moe-backend marlin) made no measurable difference (78.4 vs. 79.0 tok/s). We kept them because they are the vendor's recommendation.
Without a German system prompt, Nemotron answered 6 out of 10 German prompts in English. With "Antworte immer auf Deutsch." it was 1 out of 10. Our gateway adds that system prompt.
Nemotron is faster, but not smarter. On the Artificial Analysis Intelligence Index (version 4.3.2, retrieved 19 October 2026), Qwen3.6-35B-A3B scores 18 and Nemotron 3.5 Lightning 13. That is why both run.
Both models under load
The numbers above were measured with the other model idle. In production both receive requests at the same time. On 19 October we therefore measured each model while the other one was kept busy with the same number of concurrent requests in a loop:
| other model idle | other model under the same load | |
|---|---|---|
| Qwen, 4 concurrent | 185.2 tok/s | 75.8 tok/s |
| Qwen, 8 concurrent | 263.7 tok/s | 107.7 tok/s |
| Nemotron, 4 concurrent | 214.1 tok/s | 108.1 tok/s |
| Nemotron, 8 concurrent | 293.5 tok/s | 149.6 tok/s |
Qwen keeps about 41 % of its throughput when Nemotron is busy at the same time, Nemotron about 51 %. Time to first token roughly doubles (Qwen at 8 concurrent: 419 instead of 212 ms; Nemotron: 370 instead of 215 ms). We did not measure single streams under load. Together, both models reach 257 tok/s under load (107.7 + 149.6, from two runs with the roles swapped), less than either model alone at 8 concurrent requests. Two models on one Spark buy flexibility, not extra throughput. For planning, the right-hand column is the more honest number.
Long inputs
With 48 input tokens, prefill hardly matters. With long documents or long conversations it decides how long a user waits for the first token. We sent the beginning of a public-domain German novel (Goethe, "Die Leiden des jungen Werther", from Project Gutenberg) cut to about 8,000 and 32,000 tokens, asked for a three-sentence summary (reasoning budget 64, up to 256 output tokens) and measured time to first token and decode rate. Each request started with a different random prefix so that no prefix cache could help. Three runs for Qwen, six for Nemotron, other model idle, 19 October 2026:
| input tokens | time to first token | decode | |
|---|---|---|---|
| Qwen | 8,047 | 1.12 s | 77.1 tok/s |
| Qwen | 32,045 | 5.43 s | 71.0 tok/s |
| Nemotron | 8,051 | 1.03 s | 86.8 tok/s (79.5 to 100.2) |
| Nemotron | 32,052 | 4.72 s | 83.1 tok/s (79.6 to 103.0) |
Prefill runs at roughly 5,900 to 7,800 tokens per second. With 32,000 input tokens a user therefore waits about five seconds for the first token, compared with 0.06 (Qwen) and 0.09 seconds (Nemotron) for the short prompts. Qwen's decode rate drops slightly with long context: 78.6 tok/s with the short prompt, 77.1 at 8,000 and 71.0 at 32,000 tokens. Nemotron loses much more: from 111.7 tok/s with the short prompt to 83 to 87 tok/s (median of six runs, single runs between 80 and 103), not far above its short-prompt value without the draft model (79.0). The reason is visible in vLLM's counters: the main model accepted 37 to 40 % of the proposed tokens here, against 55 % with the short prompts. The answers were short (138 to 256 tokens), which adds noise. A first attempt with a synthetic text, one German sentence repeated many times, had given Nemotron 117 to 157 tok/s. With repetitive input the draft model guesses almost everything right. That is a good reminder that speculative decoding numbers depend on the text.
What we rejected, with numbers
- MTP (built-in speculative decoding) on Qwen3.6-35B. First attempt: memory ran full, swap 15 of 15 GiB, SSH unreachable at times. The reason:
--gpu-memory-utilizationdoes not cover the MTP module. For this model the MTP layer brings its own 256 experts, by our estimate about 15 GB on top. Second, clean attempt: 0 to 5 % more speed in exchange for 44 % less concurrency. Reverted. - Atlas Inference Engine 0.4.0 (
avarok/atlas-gb10). Faster for a single stream (95.6 tok/s with NVFP4), 44 % slower at 16 concurrent requests (208.9 vs. 373.1 tok/s). The vendor configuration (gpu-memory-utilization 0.88) drove available memory down to 8.4 GiB (8,552 MiB) within five seconds; our watchdog stopped it. The installer also set up and started a management agent (atlasctl-agent.service) listening on0.0.0.0:34334, reachable from the whole LAN. We stopped and disabled it. - A dense 27B model: Qwen3.8-27B in Q4_K_M under Ollama reached 13.2 tok/s on another Spark (6 September 2026). This is not a like-for-like comparison with vLLM and NVFP4. Two community measurements of the same model in NVFP4 with vLLM and MTP on one Spark report 23 to 26 tok/s in chat use; one of them reaches 33.7 tok/s for greedy completions without a chat template. That is still well below the MoE models above.
- A third model on spark-3. There is no room for it, and we stopped trying.
Crashes: three explanations so far
On 6 September 2026 the Qwen service died during a single request and stayed down until someone restarted it manually. Why it stayed down was quickly clear: after an internal error (EngineDeadError), vLLM shuts down cleanly and exits with code 0. systemd does not treat that as a failure, so Restart=on-failure never triggers. Both services now run with Restart=always. Since then, a crash costs a few minutes of model loading instead of hours.
Why it crashed took longer. The timeline was clear, though. Before the switch to NVFP4 on 5 September, the service had crashed twice in 72 days (27 June and 17 August). In the six days after the switch it crashed five times. In total, nine crashes are on record after the switch.
First explanation (12 September): prefix caching. An open vLLM issue (#54173) described our error chain, and with prefix caching disabled the service went quiet. We documented the problem as solved. One trap along the way: in vLLM 0.28, prefix caching is enabled by default. Removing the flag changes nothing; you need --no-enable-prefix-caching. We only noticed because a repeated prompt came back faster than it could without a cache.
It was not solved. Between 19 and 26 September four more crashes followed.
Second explanation (27 September): FlashInfer issue #3566. vLLM calls the FP8 matrix multiplication with backend="auto". On the GB10, "auto" selects cuDNN, and its FP8 kernel occasionally fails there for small batches, exactly when only one to three requests are running. Prefix caching had only made it more frequent. A small patch forces the backend to cuBLAS (see below). Measured throughput did not drop (79.2 vs. 77.4 tok/s single stream before the patch). On 27 September we wrote that this explained all crashes.
Third explanation, or rather a correction of the second: going through all nine tracebacks one by one showed that seven matched #3566 and two did not. One was a cuBLAS error in the vision encoder during a request with an image. The other was a CUDA "operation not permitted" error whose origin the log does not reveal. We had not read each traceback before. Those two crashes remain unexplained.
Current state: since the patch, Qwen has run for 22 days without a crash, and Nemotron for 43 days since it was set up. That is promising, but by our own standard it is not yet proof. Before NVFP4 the service crashed about once every 36 days, and two of the nine crashes remain unexplained. We will consider the problem solved after 72 days without a crash, twice the old interval, which would be 8 December 2026. Until then our gateway catches an outage by routing to a fallback model.
Our configuration
Both services run as Docker containers from the image ghcr.io/timothystewart6/vllm-gb10:v0.28.0-gb10.2 (vLLM 0.28.1, built for sm_121a). At the time, the official vLLM image did not work for us because its PyTorch build only went up to sm_120. systemd starts the containers.
vLLM itself has no authentication. Never expose it directly to the internet. In our setup, an inference gateway with API keys sits in front of both services, and the vLLM ports are only reachable from the internal network. The examples below bind to 127.0.0.1, which is right when the gateway runs on the same machine. If your gateway runs on another host, add the address of the internal interface (-p <internal IP>:8001:8001) and restrict access to the gateway with a firewall. That is what we do.
Qwen3.6-35B-A3B (NVFP4)
docker run --name vllm-mid --gpus all --ipc=host --ulimit memlock=-1 \
-v /srv/models:/models \
-v /srv/models/.vllm-compile-cache-mid:/root/.cache/vllm \
-v /srv/vllm-patches/flashinfer_fp8_cublas.py:/usr/local/lib/python3.12/dist-packages/vllm/utils/flashinfer.py:ro \
-p 127.0.0.1:8001:8001 \
ghcr.io/timothystewart6/vllm-gb10:v0.28.0-gb10.2 \
vllm serve /models/Qwen3.6-35B-A3B-NVFP4 \
--served-model-name qwen3.6-35b \
--host 0.0.0.0 --port 8001 \
--gpu-memory-utilization 0.45 \
--max-model-len 262144 \
--max-num-seqs 16 \
--max-num-batched-tokens 8192 \
--kv-cache-dtype fp8 \
--reasoning-parser qwen3 \
--enable-auto-tool-choice --tool-call-parser qwen3_coder \
--no-enable-prefix-caching \
--reasoning-config '{"reasoning_start_str":"<think>","reasoning_end_str":"</think>"}'
--host 0.0.0.0 refers to the inside of the container. What is reachable from outside is decided by -p.
Nemotron 3.5 Lightning (NVFP4) with DSpark
docker run --name vllm-nemotron --gpus all --ipc=host --ulimit memlock=-1 \
-v /srv/models:/models \
-v /srv/models/.vllm-compile-cache-nemotron:/root/.cache/vllm \
-p 127.0.0.1:8002:8002 \
ghcr.io/timothystewart6/vllm-gb10:v0.28.0-gb10.2 \
vllm serve /models/Nemotron-3.5-Lightning-30B-A3B-NVFP4 \
--served-model-name nemotron-3.5-lightning \
--host 0.0.0.0 --port 8002 \
--gpu-memory-utilization 0.28 \
--max-model-len 262144 \
--max-num-seqs 16 \
--max-num-batched-tokens 8192 \
--kv-cache-dtype fp8 \
--mamba-cache-mode align \
--mamba-backend flashinfer \
--moe-backend marlin \
--reasoning-parser nemotron_v3 \
--reasoning-config '{"reasoning_start_str":"<think>","reasoning_end_str":"</think>"}' \
--tool-call-parser qwen3_coder --enable-auto-tool-choice \
--enable-prefix-caching \
--speculative_config.model /models/Nemotron-3.5-Lightning-DSpark \
--speculative_config.num_speculative_tokens 3
Notes on these choices:
- Memory: 0.45 + 0.28 = 0.73 of 121.7 GiB, which is 88.8 GiB. NVIDIA's recipe for Nemotron uses 0.85, which assumes a machine running only that one model.
- Prefix caching is off for Qwen (see the crash section) and on for Nemotron, which uses a different architecture and never had the problem.
- Both use the tool-call parser
qwen3_coder. For Nemotron, NVIDIA's model card recommends exactly that parser. For Qwen, NVIDIA's model card recommendsqwen3_xml. In vLLM 0.28.1 both names point to the same parser class (Qwen3EngineToolParser), so the choice makes no difference there. In older vLLM versions they were separate implementations. - No
--rm. When a start fails, the container and its log stay available for analysis. - Clients use the name from
--served-model-name, never the model path. We have swapped the checkpoint behind that name twice without any client noticing.
The cuBLAS patch
The patch copies vllm/utils/flashinfer.py out of the image and replaces "auto" with "cublas" in the two FP8 calls. "cublas" is FlashInfer's own default for this function. The script checks first that the file contains exactly the two expected lines and refuses to patch otherwise. It cleans up its temporary container even when a step fails. Download: make-flashinfer-cublas-patch.sh.
#!/usr/bin/env bash
set -euo pipefail
IMG="ghcr.io/timothystewart6/vllm-gb10:v0.28.0-gb10.2"
SRC="/usr/local/lib/python3.12/dist-packages/vllm/utils/flashinfer.py"
TARGET_DIR="${1:-/srv/vllm-patches}"
OUT="$TARGET_DIR/flashinfer_fp8_cublas.py"
WORK="$(mktemp -d)"
CID=""
cleanup() {
[ -n "$CID" ] && docker rm -f "$CID" >/dev/null 2>&1 || true
rm -rf "$WORK"
}
trap cleanup EXIT
CID="$(docker create "$IMG")"
docker cp "$CID:$SRC" "$WORK/flashinfer.py"
cp "$WORK/flashinfer.py" "$WORK/flashinfer_fp8_cublas.py"
count="$(grep -c '^ "auto",$' "$WORK/flashinfer_fp8_cublas.py" || true)"
if [ "$count" != "2" ]; then
echo "Expected 2 matching lines, found $count. Not patching." >&2
exit 1
fi
sed -i 's/^ "auto",$/ "cublas",/' "$WORK/flashinfer_fp8_cublas.py"
echo "Changes against the original:"
diff "$WORK/flashinfer.py" "$WORK/flashinfer_fp8_cublas.py" || true
mkdir -p "$TARGET_DIR"
install -m 0644 "$WORK/flashinfer_fp8_cublas.py" "$OUT"
echo "Written: $OUT"
/srv/vllm-patches usually needs root to create. Create it once and hand it to the user who runs the script, then run the script:
sudo install -d -o "$USER" -m 0755 /srv/vllm-patches
./make-flashinfer-cublas-patch.sh
The diff output should show exactly two changed lines. Then mount the file read-only into the container, as in the docker run example above.
The patch belongs to this exact image. With every image update, generate it again from the new file and check first whether FlashInfer #3566 has been fixed there. Mounting an old copy over a newer vLLM version will at best break the start and at worst run with mismatched code.
systemd and sysctl
The same two drop-ins exist for both services, under vllm-mid.service.d/ and vllm-nemotron.service.d/:
# /etc/systemd/system/vllm-mid.service.d/restart-always.conf
# /etc/systemd/system/vllm-nemotron.service.d/restart-always.conf
# vLLM exits with code 0 after EngineDeadError, so on-failure never triggers.
[Service]
Restart=always
RestartSec=30
# /etc/systemd/system/vllm-mid.service.d/startlimit.conf
# /etc/systemd/system/vllm-nemotron.service.d/startlimit.conf
# At most 5 starts in 10 minutes, so a start that fails at once does not loop forever.
[Unit]
StartLimitIntervalSec=600
StartLimitBurst=5
# /etc/sysctl.d/99-no-proactive-compaction.conf
vm.compaction_proactiveness = 0
What we would do differently
- Measure our own baseline first, then read other people's numbers. That would have saved us the wrong NVFP4 conclusion.
- Calculate memory instead of estimating it, and plan add-ons like MTP heads separately.
--gpu-memory-utilizationdoes not cover them. - Start every new configuration under a watchdog that checks every second and kills the container immediately.
- Measure two models on one machine together, not only one at a time. The single-model numbers describe the best case.
- Call a fix confirmed only after an observation period clearly longer than the usual interval between failures.
- Write everything down, including the mistakes. This article is a cleaned-up version of our operations handbook. Because the wrong assumptions are marked as wrong there, we have more than once avoided making them a second time.
Sources
- FlashInfer issue #3566, cuDNN FP8 GEMM on sm_121a: https://github.com/flashinfer-ai/flashinfer/issues/3566
- vLLM issue #54173, prefix caching and GDN on GB10: https://github.com/vllm-project/vllm/issues/54173
- NVIDIA model card, Qwen3.6-35B-A3B-NVFP4 (source of the MMLU Pro numbers): https://huggingface.co/nvidia/Qwen3.6-35B-A3B-NVFP4
- NVIDIA model card, Nemotron 3.5 Lightning 30B-A3B NVFP4: https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4
- NVIDIA model card, Nemotron 3.5 Lightning DSpark draft model: https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DSpark
- vLLM images for GB10: https://github.com/timothystewart6/vllm-gb10
- Artificial Analysis, Qwen3.6-35B-A3B (Intelligence Index v4.3.2, retrieved 19 October 2026): https://artificialanalysis.ai/models/qwen3-6-35b-a3b
- Artificial Analysis, Nemotron 3.5 Lightning (Intelligence Index v4.3.2, retrieved 19 October 2026): https://artificialanalysis.ai/models/nemotron-3-5-lightning
- Community measurement, Qwen3.8-27B NVFP4 with vLLM 0.27.1 and MTP on one DGX Spark: https://gist.github.com/Mirrdhyn/d476d17723c9d68eac72f03ec9447f2b
- NVIDIA developer forum, Qwen3.8-27B NVFP4 on a single DGX Spark with vLLM 0.26.1 and MTP: https://forums.developer.nvidia.com/t/qwen3-8-27b-nvfp4-on-a-single-dgx-spark-up-to-1m-context-vllm-mtp-measurements/380244
Both models are available on allsmartllm.com under the aliases mid (Qwen3.6-35B) and swift (Nemotron 3.5 Lightning). New accounts include 10 million free test tokens.