AidenLabrecipe 05
MEASURED · 2026-10-18
MEASURED·10 min read·Sep 17, 2026

DeepSeek-V4.1-Flash on 4× DGX Spark with SGLang

The SGLang team's own DeepSeek-V4.1 image, untouched, plus an MIT overlay of import hooks that make it fit and run fast on GB10: Engram tables gathered from node-local NVMe inside the CUDA graph, the b12x SM12x kernels for the FP8 dense layers, two SM120 fixes, an allocator flush, and the same one-shot RDMA collectives as the vLLM recipe. It replaced the vLLM stack as production on 2026-09-17: prefill +30–45 %, decode level, a 23 % larger KV pool. Since production-1.2 (2026-09-23) it serves 16 requests at once; production-1.5 (2026-09-24) fixes a rank crash under long prompts and adds two GB10-specific speedups; production-1.7.1 (2026-09-29) makes Claude Code's follow-up turns reuse the prefix cache (about a minute to first token → about a second) and ships prompt-lookup drafting (+13–22 % on answers that repeat their context) behind a switch.

4× DGX Spark GB10 · TP4/EP4 · SGLang 1.7.1 · aidendle94/sparkrun-sglang-dsv41-gb10:production-1.7.1 · measured Sep 17, 2026

  • DeepSeek-V4.1-Flash
  • SGLang
  • multi-node
  • TP4
  • DGX Spark
  • FP8
  • MXFP4
  • MoE
3,068 / 2,580tok/sPrefill · 32K / 128Kunique random words, 10.7 s / 51.1 s to first token
94 / 68 / 32tok/sDecode · counting / code / prosesingle stream, greedy, thinking off
524KtokensContextneedles found at 32K and 128K
4.22MtokensKV poolauto-sized at fraction 0.80, no DCP

01Overview

What you need
  • 4× DGX Spark GB10 nodes, fabric-linked (200G RoCE, two ConnectX-7 functions per node)
  • aidendle94/sparkrun-sglang-dsv41-gb10:production-1.7.1 image (Docker Hub, arm64) = lmsysorg/sglang:dev-dsv41 plus the overlay, or build it from the repository''s Dockerfile
  • The overlay repository (MIT): the hooks, the C row store, the four-node launcher, watchdog and benches
  • deepseek-ai/DeepSeek-V4.1-Flash weights (510 GB) on one node, exported read-only over NFS to the other three
  • ~50 GB of local NVMe per node for the sparse copy of that rank''s Engram rows (the same copies the vLLM recipe makes), plus ~34 GB for the image
  • Root on every node (RDMA passthrough, memlock) and earlyoom tuned to fire before the kernel OOM killer

DeepSeek-V4.1-Flash served on four DGX Spark GB10 nodes with SGLang: TP4 with expert parallelism 4, the dsv4 attention backend, FlashInfer's MXFP4 MoE runner, DSpark speculative decoding with the checkpoint's own draft layers at block size 5, tools, images, thinking on at reasoning_effort max by default. The engine is lmsysorg/sglang:dev-dsv41, the SGLang team's own DeepSeek-V4.1 image, unmodified. Everything a Spark needs on top is an overlay of import hooks in one MIT repository — about 1,500 lines with the launcher — so the image can be rebuilt on a newer upstream tag without touching them, and the stock code paths stay available in the same image. On 2026-09-17 it replaced the vLLM stack of the previous recipe as production on this fleet: same port, same model names, same clients.

02Build

Per-node deployment

One container per node, host networking, RDMA passthrough, the model directory on NFS (the node that holds the checkpoint mounts it locally and reads its Engram rows straight from the shards; the others read their node-local copies). Workers start first, the head last; the launcher probes each node's RoCE-v2 GID index, refuses to start a rank while any GPU is still held by a previous engine, and prints the four commands with --dry-run. Rank 0 below; the workers differ in --node-rank, HOST_IP and their Engram source.

docker run · one rank (rank 0 shown)
docker run -d --name sgldsv41 --restart no --network host --ipc host --cap-add IPC_LOCK --gpus all \
  --shm-size 32g --memory 112g --memory-swap 112g --ulimit memlock=-1:-1 --ulimit stack=67108864 \
  --device /dev/infiniband:/dev/infiniband --oom-score-adj 500 \
  -v <hf-repo-dir>:/models/repo:ro -v <writable-cache-dir>:/root/.cache \
  -v <engram-local-dir>:/engram-local:ro -e SPARK_ENGRAM_DIR=/engram-local \
  # the node that holds the checkpoint reads its rows from the shards instead of a copy:
  #   -e SPARK_ENGRAM_DIR=/models/repo/snapshots/<snapshot> -e SPARK_ENGRAM_ROWS=1:<lo>:<hi>,14:<lo>:<hi>
  -e SPARK_ENGRAM_MODE=hostnode -e SPARK_ENGRAM_EARLY=1 -e SPARK_ENGRAM_PREFETCH=1 -e SPARK_PAGE_CACHE_RELEASE=1 \
  -e SPARK_LATE_TAIL_SKIP=1 -e SPARK_LATE_TAIL_SKIP_ALL=1 -e SPARK_SM120_REAL_HEADS=1 -e SPARK_ENGRAM_THREADS=64 -e SPARK_ENGRAM_MAX_IDS=65536 -e SPARK_MXFP8_BACKEND=b12x \
  -e SPARK_SERVED_ALIASES=<other-model-ids,comma-separated> \
  -e SPARK_ROCE_AR=1 -e SPARK_ROCE_AR_MAX=1MB -e SPARK_ROCE_AG_MAX=16MB \
  -e B12X_ROCE_HCA=<rdma-devices> -e B12X_ROCE_GID_INDEX=<this-node-roce-v2-gid-index> -e B12X_ROCE_SPIN_LIMIT=300000000 \
  -e SGLANG_ENABLE_DSV41_ENGRAM_HOST_TABLE=0 -e SGLANG_FLASHINFER_MOE_FUSED_FINALIZE=0 -e SGLANG_DSV41_REASONING_EFFORT=100 \
  -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:False -e CUDA_DEVICE_ORDER=PCI_BUS_ID -e HOST_IP=<this-node-fabric-ip> \
  -e NCCL_NET=IB -e NCCL_IB_DISABLE=0 -e NCCL_IB_HCA=<rdma-devices> -e NCCL_SOCKET_IFNAME=<fabric-iface> -e GLOO_SOCKET_IFNAME=<fabric-iface> \
  -e NCCL_IB_ROCE_VERSION_NUM=2 -e NCCL_IB_ADDR_FAMILY=AF_INET -e NCCL_IB_ADDR_RANGE=<fabric-subnet>/24 -e NCCL_IB_SUBNET_PREFIX_LEN=24 -e NCCL_IB_GID_INDEX=<gid-index> \
  -e NCCL_CROSS_NIC=1 -e NCCL_IB_MERGE_NICS=0 -e NCCL_IB_SUBNET_AWARE_ROUTING=1 -e NCCL_CUMEM_ENABLE=0 -e NCCL_P2P_DISABLE=1 -e NCCL_SHM_DISABLE=1 \
  -e NCCL_BUFFSIZE=1048576 -e NCCL_LL128_BUFFSIZE=262144 -e NCCL_PROTO=^LL128 -e NCCL_MAX_NCHANNELS=8 \
  --entrypoint python3 aidendle94/sparkrun-sglang-dsv41-gb10:production-1.7.1 -m sglang.launch_server \
    --model-path /models/repo/snapshots/<snapshot> --served-model-name deepseek-v4.1-flash --trust-remote-code --load-format safetensors \
    --tp 4 --ep-size 4 --nnodes 4 --node-rank 0 --dist-init-addr <head-fabric-ip>:20100 --host 0.0.0.0 --port 8210 \
    --attention-backend dsv4 --moe-runner-backend flashinfer_mxfp4 --fp8-gemm-backend flashinfer_cutlass \
    --mem-fraction-static 0.80 --chunked-prefill-size 2048 --context-length 524288 --max-running-requests 16 --cuda-graph-max-bs-decode 16 \
    --random-seed 0 --enable-decoder-swa-bounded-replay --speculative-algorithm DSPARK --speculative-dspark-block-size 5 \
    --tool-call-parser deepseekv41 --reasoning-parser deepseek-v41 --watchdog-timeout 1800 --min-free-slots-delay 1 \
    --default-chat-template-kwargs '{"thinking":true}'
  1. Pull the image on every node (arm64; or docker build the repository's Dockerfile on lmsysorg/sglang:dev-dsv41 — it compiles the row store, vendors the b12x RoCE runtime and runs the CPU tests). Export the checkpoint's HuggingFace repo directory from the node that holds it and mount it on the others.
  2. Make each rank's node-local Engram rows with the vLLM recipe's engram_local.py — one contiguous range per rank so the four ranges partition each table; the hook reads the range from the copy's manifest. The node with the checkpoint skips the copy and passes its range as SPARK_ENGRAM_ROWS.
  3. --fp8-gemm-backend flashinfer_cutlass is required for the MXFP8 route (it selects the block-scaled path the hook wraps); the b12x kernel refuses the K = 576 projections and those fall back to CUTLASS per shape.
  4. Healthy in ~10 minutes. Check four lines before trusting it: DSV4 memory calculation … full_token= (the KV pool, 4.2–6.6M depending on the boot), RoCE collectives on: world=4, Engram local layer=1 … gather_us_per_call near 1,000 at idle, and free -g on every node.
  5. Run something that checks answers: count to 20 (exact), a needle at 32K and 128K, an image, a tool call. Then arm the watchdog (launch/watchdog.sh: /health plus a request probe with a busy grace, orchestrated relaunch with pre-teardown log capture).
thinking is on by default at max; per request
curl -s http://<head>:8210/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "deepseek-v4.1-flash", "max_tokens": 300, "temperature": 0,
  "messages": [{"role": "user", "content": "Is 91 a prime number? Answer briefly."}],
  "chat_template_kwargs": {"thinking": true, "reasoning_effort": 25}}'
# "thinking": false turns it off; reasoning_effort: low 25 / high 50 / xhigh 75 / max 100 or any integer 1–100

03Deep dive

What stock SGLang does on a Spark, and what the overlay changes

Stock dev-dsv41 loads the two Engram tables into pinned host memory — 203 GB, the same wall the vLLM recipe hit, since on a Spark host memory is the GPU pool — and two of its SM120 paths were written for a page size and an indexer schedule that V4.1's ratio-1/2 attention sources do not use, so the first boots died in graph capture and in the sparse prefill. Five 40-minute windows fixed one thing each; two more brought the collectives and the Engram gather cost over from the vLLM work. This is what ended up in the overlay, each piece behind an import hook that runs once, right after its target module loads.

HookWhat it doesWhy
engram_store + engram_rows.creplaces EngramEmbedding._owned_rows: each rank owns one contiguous range of the 384M rows per table (the partition the node-local copies already have); the hash ids go to the host, a cudaLaunchHostFunc node runs the C gather — memory-mapped shard, MADV_RANDOM, a posix_fadvise(WILLNEED) prepass, a persistent 64-thread pool — into pinned staging, back to the GPU, and SGLang's own Triton kernel dequantizes; unowned rows are zeroed as the stock reduction expects. Since 1.2 each layer's gather starts on a side stream as soon as the step's hash ids exist, overlapping the layers before itthe tables never load; the gather runs inside the captured decode graph; 1.8–3.9 ms per single-stream decode step for both layers
engram_prefetchthe scheduler knows every prompt, so when it launches an extend batch the hook hashes the *following* chunk of each request on the CPU with SGLang's compute_engram_hash_ids and asks the store to page those rows inthe next chunk finds its rows in the page cache; per-token gather cost 10.7–15.5 → 3.8–4.8 µs together with the pool
mxfp8_kernelroutes the 32×32-block FP8 dense projections to FlashInfer's b12x SM12x kernel (backend='b12x'), per-shape fallback to CUTLASS where the kernel refuses (K = 576)the CUTLASS tile pads a 6-token decode step to 128 rows
indexer_scheduleforces the DeepGEMM schedule for the ratio-1 and ratio-2 indexer sources on SM12xSGLang's guard skipped the planner the FP4 indexer kernel needs; graph capture died in fp8_fp4_paged_mqa_logits
sm120_prefill_pagesre-pages the ratio-2 KV source to 64-token pages for FlashInfer's SM12x sparse-prefill kernel, with SGLang's own page-split kernels; since 1.4 also keeps decode attention on each GPU's 16 real heads instead of padding them to 64the kernel rejects 128-token extra pages; SGLang only re-paged the primary source. The 64-head pad exists for datacenter FlashMLA; FlashInfer's SM12x decode kernel has a 16-head build
prefill_flushreturns the allocator's cached blocks after each long prefill chunkreserved memory grew with the square of the prompt: a 128K prefill took every node to zero free and earlyoom killed two ranks
roce_collectivesattaches the b12x one-shot RDMA runtime to SGLang's tp group coordinator: vote over gloo, route all-reduces up to 1 MB and all-gathers, prepare and pin the stream inside graph_capture, fail-stop health check after every forwardthe ~80 decode-step all-reduces leave NCCL; DCP/EP groups and the 64 MB prefill all-reduces stay
served_aliaseslists extra model ids on /v1/modelsSGLang serves one --served-model-name; the fleet's clients ask by several
page_cache_releasedrops the checkpoint's files from the page cache once the KV pool is allocated; the Engram row store keeps its own cached rows under a 4 GB budgeton GB10 the GPU driver fails an allocation instead of evicting cached files; 12–17 GB of stale checkpoint cache plus the Engram rows left 2–6 GB free, and a failed allocation inside cuBLAS killed a rank mid-prefill
late_tailskips the 22 layers after the last KV-source layer on prefill chunks that end before the prompt's last 128 tokensV4.1 caches its long-range KV only at layers 2, 8, 14 and 20 (YOCO-style); decode never reads those later layers' state for earlier chunks
inline_systemon /v1/messages, system notes a client sends in the middle of the conversation stay where they were, folded into the next user message, instead of being hoisted into the system prompt at the topClaude Code adds one per turn; hoisting changed the prompt ~4K tokens in, so every turn re-prefilled the whole conversation (about a minute); now follow-ups reuse the cache
lookup_draft + lookup_wideoff unless WIDE=1: a device token table per request and a Triton matcher; DSpark drafts its 5 tokens and a row whose block continues an earlier 8-token match verifies up to 10 more tokens of it (compact ragged verify)+13–22 % on answers that repeat their context; opt-in since the 2026-09-29 incident (fixed in 1.7.1)
request_guardrejects, with HTTP 400, a class of request the engine cannot serve under V4.1's decoder sliding-window bounded replaysuch a request raised inside the model and stopped the whole server
step_timersoff unless SPARK_STEP_TIMERS is set: capture-safe CUDA events around attention, MoE and Engram inside the decode graphs, or the gaps between graphsdiagnosis; how the decode-step breakdown below was measured

sitecustomize.py installs them through a meta-path finder and is inert unless SPARK_ENGRAM_DIR is set. Everything model-specific lives upstream where the SGLang team maintains it.

Two ways to move the rows

The overlay ships two Engram implementations behind one switch (SPARK_ENGRAM_MODE). The default gathers each layer's rows inside the CUDA graph: a host-function node calls the C row store while the GPU waits, so nothing has to know the tokens ahead of time — which matters because SGLang's DSpark verify step gets its draft tokens on the GPU. The alternative is a port of our vLLM design: a hook before every forward hashes the batch with the model's own kernel, syncs once, gathers both layers' rows with a Python thread pool and stages them, and the embedding reads them by position. It needs no C library. Measured on the same nodes the same day, one boot each:

host-node (default)staged (ported from vLLM)
prefill 32K / 128K, unique text3,308 / 2,208 tok/s (3,068 / 2,580 on the previous boot)2,639 / 2,071 tok/s
prefill 32K / 128K, repetitive filler3,470 / 3,3804,157 / 3,478
decode counting / code / prose, single stream107 / 87 / 36 tok/s110 / 89 / 38
what it costsa C library built in the image; the GPU waits on the host node inside the graphone sync per forward: on prefill the next chunk cannot be launched while the CPU gathers, so the GPU idles between chunks

Decode figures from bench/decode_bench.py (first to last token, best of two). The staged design wins decode by 3 % and loses unique-text prefill by 14–20 %, which is why host-node stays the default; the 32K/128K prefill numbers swing ±10–15 % between boots on GB10 (the 128K needle was 51.1 s on the previous host-node boot and 59.3 s on this one).

04Performance

Measured 2026-09-17 (windows 5–7) · 2026-09-21 (production-1.1: clean-room rewrite, persistent C pool, staged mode) · 2026-09-23 (production-1.2: 16 running requests, early Engram staging) · 2026-09-24 (1.3 page-cache crash fix, 1.4 real attention heads, 1.5 YOCO prefill skip) · 2026-09-28 (1.7 prompt-lookup wide mode, 90-minute soak) · 2026-09-29 (1.7.1 Claude Code prefix caching). Four-node TP4/EP4 · lmsysorg/sglang:dev-dsv41 + the MIT overlay · DSpark block 5 · chunked prefill 2,048 · b12x MXFP8 dense · RoCE collectives · Engram rows node-local. Same prompts, nodes and day as the vLLM recipe: salted needles of unique random words for prefill (tok/s = prompt tokens / time to first token), the counting / code / prose decode probe (greedy, thinking off, completion_tokens), the four-stream prose story for aggregate; the KV pool is the engine's own log line. Image: aidendle94/sparkrun-sglang-dsv41-gb10:production-1.7.1.

Prefill · 32K / 128K3,068 / 2,580tok/sunique random words, 10.7 s / 51.1 s to first token; vLLM production 2,094–2,362 / 2,152
Prefill · repetitive filler4,063 / 3,435tok/s32K / 128K; vLLM 2,652 / 2,550
Decode · counting / code / prose94 / 68 / 32tok/ssingle stream, greedy, thinking off; vLLM 93 / 67 / 31 (best boot 95 / 75 / 36)
Aggregate72tok/sfour prose streams (30.0 single); vLLM 69.6
KV pool4.22Mtokensauto-sized at fraction 0.80, no DCP; 4.2–5.8M across boots; vLLM DCP4 3.42M pinned
Context524Ktokensneedles found at 32K and 128K
Engram gather3.8–4.8µs / tokenper layer, in-graph timer; 10.7–15.5 with per-call thread spawn; decode step 0.9–1.1 ms per layer
Host memory floor11–14GB freeevery node through a 128K prefill at 2,048-token chunks (16–20 at 1,024)
Boot to healthy583sJIT caches warm; 604 s on the first working boot
Overlay10hooksMIT; base image unmodified; inert without SPARK_ENGRAM_DIR; provenance-reviewed before release
Decode, 1.1 (persistent pool)107 / 87 / 36tok/scounting / code / prose, bench/decode_bench.py; staged mode 110 / 89 / 38; previous pool 101 / 82 / 35
Decode, 1.2 (early Engram staging)110 / 94 / 37tok/scounting / code / prose, bench/decode_bench.py; Engram per single-stream step 4.4–7.0 → 1.8–3.9 ms
Concurrency, 1.257 / 211 / 337tok/saggregate at 1 / 8 / 16 streams, fixed prompt set; 64 / 31 / 26 per stream; cap 16 running requests (8 until 2026-09-22)
Concurrency, 1.4 / 1.556 / 220 / 331tok/saggregate at 1 / 8 / 16 streams; decode attention on the 16 real heads per GPU (8-stream attention kernel 9.6 → 3.4 ms per step)
Prefill, 1.58.0 / 45.0s32K / 128K prompt to first token on the serving fleet (1.2: 9.1 / 52.2 s); the late layers are skipped on chunks decode never reads them for
Claude Code follow-up turn, 1.7.11,536 / 165,888tokensprefilled / reused from the prefix cache on a ~168K-token session: under a second to first token (every earlier release re-prefilled ~153K, about a minute)
Prompt-lookup wide mode (WIDE=1)+13 / +22%63K-token coding conversation / copy-heavy answers vs production-1.6 (plain chat level; 10 concurrent 133 → 161 tok/s); off by default since 2026-09-29
Free memory, 1.3+13–20GBper node while serving, after dropping the checkpoint page cache (was 2–6 GB; the GPU driver fails rather than evict page cache)
Aggregate throughput by concurrent streams, production-1.5tok/s · higher is better
1 stream55.8
8 streams220
16 streamsshipped331

tonyd2wild's fixed prompt set, eight categories (1.4 bench; 1.5 changes prefill only). 1.2: 57.2 / 211.2 / 337.4; the 8-request cap until 2026-09-22 stopped at 172.6 (6 streams).

Prefill by stack — same nodes, same prompts, 2026-09-17tok/s · higher is better
vLLM production-1.2 (DCP4, RoCE)SGLang + overlay (production-1.0)
32K unique words2,3623,068+30%
128K unique words2,1522,580+20%
32K repetitive filler2,6524,063+53%
128K repetitive filler2,5503,435+35%

vLLM at its best recorded boots; SGLang window 7. Prompts unique per run, one output token counted at first-token time.

Windows 5 → 6 → 7 — what each carry-over boughttok/s · higher is better
window 5 (NCCL, 1K chunks, per-call spawn)window 7 (RoCE, 2K chunks, pool + prefetch)
decode counting86.294.4+10%
decode code59.567.8+14%
decode prose30.932.4+5%
prose × 4 streams (aggregate)66.072.3+10%
prefill 32K (÷100)23.330.7+32%
prefill 128K (÷100)20.925.8+23%

Decode moved in window 6 (RoCE collectives), prefill in window 7 (Engram pool + prefetch + chunk size); each on its own left the other unchanged.

Engram gather cost per prompt token, per layerµs · lower is better
per-call thread spawn, 1,024-token chunks (window 6, worst minute)15.5
per-call thread spawn, 1,024-token chunks (best minute)10.7
pool + prefetch, 2,048-token chunks (window 7, worst minute)4.8
pool + prefetch, 2,048-token chunks (best minute)shipped3.8

Rank 0, the C gather timed inside the graph node; one-minute windows during the 32K and 128K prefills.

KV pool by stacktokens · higher is better
vLLM production-1.0 (8 GiB per rank, no DCP)1,263,118
vLLM production-1.2, DCP4, 500K max-model-len3,418,145
SGLang + overlay, fraction 0.80, auto-sized (this boot)shipped4,219,904

SGLang leaves 7.15 GB per rank for KV at fraction 0.80 and its per-token cost is lower; without DCP the pool moves with the page cache between boots (4.2–5.8M).

Changelog · vLLM production-1.2 (DCP4, RoCE) → SGLang + overlay production-1.0

vLLM production-1.2 (DCP4, RoCE)SGLang + overlay production-1.0
EnginevLLM dsv41-feat branch + a 28-file patch set baked into the imagelmsysorg/sglang:dev-dsv41 unmodified + 8 import hooks (MIT)
Engram tablesdisk rows, memory-mapped gather staged before the stepdisk rows gathered by a host-function node inside the decode graph; scheduler-side next-chunk prefetch
Prefill 32K / 128K2,094–2,362 / 2,152 tok/s3,068 / 2,580 tok/s
Decode counting / code / prose93 / 67 / 31 (best boot 95 / 75 / 36)94 / 68 / 32 — level
KV pool3,418,145 (DCP4, 6 GiB pinned)4,219,904 (auto, no DCP)
Context500,000524,288
CollectivesRoCEnante for the TP groupsame runtime, same limits, through an adapter on the SGLang group coordinator
Images, tools, thinking, model aliases, port, watchdogonon — same port, same names, same clients
Not carried overDCP2/DCP4none needed for the pool; an SGLang port would be weeks

Same weights and nodes on both sides unless the row says otherwise.

Across recipes

Single-stream decode, by recipetok/s · higher is better
DeepSeek-V4.1-Flash · SGLang (1.2)shipped94.1
DeepSeek-V4.1-Flash (1.1, DCP2)60.8
GLM-5.3-Flash (1.2)89.7
DeepSeek-V4-Flash (3.76.1)71.0
DeepSeek-V4-Flash-Vision (3.73)67.9

Different prompt sets, all single stream at temperature 0 with completion_tokens accounting: exact within a recipe, indicative across them. DeepSeek-V4.1-Flash · SGLang (1.2): code prompts, DSpark block 5, RoCE collectives, early Engram staging; 110 counting, 37 prose · DeepSeek-V4.1-Flash (1.1, DCP2): code prompts, DSpark k=5, b12x MoE, DCP2 (77.5 on 1.0 with a different prompt set) · GLM-5.3-Flash (1.2): structured output, thinking off · DeepSeek-V4-Flash (3.76.1): mixed prompts, 8-rep mean · DeepSeek-V4-Flash-Vision (3.73): code prompts, DCP2 + DSpark

Cold prefill, by recipetok/s · unique content · higher is better
DeepSeek-V4.1-Flash · SGLang (1.5)shipped4,113
DeepSeek-V4.1-Flash (1.0)2,143
GLM-5.3-Flash (1.2)2,070
DeepSeek-V4-Flash (3.76.1)2,200
DeepSeek-V4-Flash-Vision (3.73)2,380

Cold means the prompt has never been seen by the server; with prefix caching on, a repeated prompt is a cache hit and reads 3–25× higher. DeepSeek-V4.1-Flash · SGLang (1.5): 32K depth, unique random words (2,913 at 128K) · DeepSeek-V4.1-Flash (1.0): 36K depth (1,940 at 132K) · GLM-5.3-Flash (1.2): 32K depth · DeepSeek-V4-Flash (3.76.1): 334K depth · DeepSeek-V4-Flash-Vision (3.73): 92K depth (2,088 at 334K)

production-1.7 → 1.7.1 — prompt lookup, and Claude Code's minute-long turns

1.7 (2026-09-28) added prompt-lookup drafting on top of DSpark: when the answer repeats its own context — a file being edited, JSON being restated, a passage being quoted — the continuation of the earlier occurrence is verified along with DSpark's block. DSpark keeps drafting its 5 tokens; a row whose block continues an earlier 8-token match gets up to 10 more tokens of it, and only that row verifies the longer block (SGLang's compact ragged verify, graphs captured at 6b / 6b+10 / 6b+20 tokens). Putting lookup tokens in place of DSpark's lost; DSpark already takes its whole block on most copied text, so the gain is past its block.

90-minute soak, same fleetproduction-1.6prompt-lookup wide mode
10-turn coding conversation over ~63K tokens of source65.9 tok/s74.4 (+13 %)
copy-heavy answers (edit a file, restate JSON, quote a document)69.785.3 (+22 %)
plain chat53.754.2
10 concurrent requests132.9161.0 (+21 %)

Single-stream greedy unless noted. News replay, long-prefill stress, needle-style checks, images and batched-correctness checks all passed. Extensions fired on ~5 % of steps and added ~7.8 accepted tokens each.

It ran as production for one day and is now off by default (WIDE=1 turns it on). Twice the fleet went down on 1.7: once a node reset itself (its journal simply stops; no kernel, GPU or thermal line, the same silent reset another node showed before) and the watchdog relaunched; once every rank hit a device-side assert during a Claude Code session. Prompts with screenshots store image placeholder ids (1,000,000 and up) in the lookup's token table; a continuation ran into one and the sampled one-hot draft distribution was indexed with it. 1.7.1 stops every continuation at the first id outside the vocabulary (a CPU test reproduces the exact out-of-bounds scatter without it); wide mode comes back after a window that samples through prompts with images.

The same session answered a question that predates 1.7: why Claude Code waited about a minute on every turn. Each /v1/messages request was ~158K tokens and the prefix cache matched 4,096 of them. Two consecutive requests, captured with SGLang's runtime request dump, first differed at token 4,437: Claude Code adds a system note to the conversation on every turn (<total_tokens>… tokens left</total_tokens>), and SGLang, finding no mid-conversation system turn in V4.1's prompt format, hoists every such note into the system prompt at the top. One new line near the top moves every later token, so the whole conversation was prefilled again. The inline_system hook leaves the notes where they were, folded into the next user message; the prompt now only grows at the end.

Claude Code, follow-up turn of a ~168K-token sessionproduction-1.7 (and every release before)production-1.7.1
tokens reused from the prefix cache~4K165,888
tokens prefilled~153K1,536
time to first tokenabout a minuteunder a second

From the serving log (#cached-token per prefill chunk). Later turns in a tool loop prefill a few thousand tokens each (the previous answer is recomputed; decoded tokens are not inserted into the cache at chunk granularity), 1–2 s.

production-1.3 → 1.5 — a crash fix, real attention heads, YOCO prefill

Three releases on 2026-09-24. 1.3 fixed a rank crash under long prompts. 1.4 stopped padding each GPU's 16 attention heads to 64 during decode and started trimming the late layers' prefill work. 1.5 skips that work outright. Each went live through a gated test window: needles, a long-prompt stress run, buried-fact checks on eight 4K–47K-token prompts, and the fixed-prompt bench.

production-1.2production-1.5
32K / 128K prompt, time to first token9.1 / 52.2 s8.0 / 45.0 s
aggregate at 1 / 8 / 16 streams (tok/s)57.2 / 211.2 / 337.4 (199–211 at 8 streams across the week)55.8 / 220.3 / 331.3
attention kernel per verify step, 8 streams9.6 ms3.4 ms
free memory per node while serving2–6 GB13–20 GB
rank crashes under long promptstwo in three daysnone in the stress runs (0 driver out-of-memory events)

1.2 numbers are its window of 2026-09-23 (the 128K needle from 1.3, same prefill path). 1.5 prefill measured on the serving fleet with salted needles; the bench numbers are 1.4, which 1.5 does not change (its difference is prefill-only).

Decode. Up to 64 query tokens, SGLang pads each rank's 16 heads to 64 because datacenter FlashMLA only has 64- and 128-head kernels. On GB10 the call goes to FlashInfer's decode_dsv4, which has 8/16/32/64/128-head builds, so the pad was four times the attention work; 16 streams (96 tokens, unpadded) ran faster than 8 (48 tokens, padded). Prefill. V4.1 builds its long-range KV only at layers 2, 8, 14 and 20. SGLang's bounded replay already ran the 22 later layers on only the last 128 tokens of each chunk, but decode reads their state only for the prompt's last 128 positions, so for every earlier chunk that pass (80 ms of a ~560 ms chunk) produced nothing anyone reads. 1.5 skips it. 1.6 (2026-09-25) adds a guard after a new crash: once, the CUDA driver refused to load a freshly compiled SGLang kernel mid-serving ("operation not permitted") and the fleet went down; a replay of the load did not reproduce it. The guard waits for the GPU and retries such a load; it has not had to act since.

production-1.2 — sixteen streams, and where a decode step goes

The request cap was 8 until 2026-09-22, far below what the KV pool allows. Decode is bound by reading the MoE weights, so a bigger batch amortizes every read: at 16 running requests the fleet delivers 1.9× the old ceiling. Capture-safe CUDA events inside the decode graphs then split a verify step into its parts, which is how the Engram stall was found and removed.

Fixed prompt set, aggregate / per stream (tok/s)cap 8 (until 2026-09-22)cap 16cap 16 + early Engram staging (production-1.2)
1 stream55.0 / 61.355.0 / 61.357.2 / 63.6
8 streams—207.7 / 30.2211.2 / 30.9
16 streams—329.4 / 25.3337.4 / 25.7
single-stream decode, counting / code / prose107 / 87 / 36105 / 88 / 35110 / 94 / 37

tonyd2wild's fixed-prompt bench (eight categories, counting ceiling excluded). Host memory held 12 GB free or more on every node at 16 streams.

Per verify step, rank 01 stream (6 tokens)8 streams (48)16 streams (96)
target graph54–57 ms140 ms157 ms
MoE3290102
attention (43 layers, with the indexer)153533
Engram (2 layers), before → after early staging4.4–7.0 → 1.8–3.99.012.2
draft graph4.38.210.5
GPU idle between graphs (median)0.10.10.1

The MoE is at the memory-bandwidth wall and the CPU is never the bottleneck: almost no launch finds the GPU idle. The Engram lookups were stalling the GPU while the host gathered rows, although a step's ids exist before layer 0; since 1.2 both layers' gathers start on a side stream right after hashing (verified byte-identical to the inline gather on all four ranks).

Measured and not adoptedResultWhy not
b12x fused MoE instead of SGLang's CUTLASS MXFP4 kernel1.3–3× slower per rank at decode sizesthe stock kernel already reads the experts at ~250 of ~273 GB/s, and EP4 beats b12x on tensor-parallel slices
compressor on a side stream during verify (SGLang early sources, gate lifted for SM12x)attention 14.7 → 14.8 ms; 211 → 203 tok/s at 8 streamsboth streams read weights from the same memory, so they split the bandwidth; Engram staging got slower
attention output projection wo_a in MXFP8 instead of BF16 (exact weight requantization)+3 % single stream, +2.5 % per stream at 16the activation is rounded to FP8 and greedy outputs change; not worth giving up BF16 in this projection
wo_a as W8A16: exact FP8 weights, BF16 activations, Triton kernel (2026-09-24)208 vs 220 tok/s at 8 streams; +3 % single streamhalf the weight bytes, but decoding them in the kernel cost more than the bandwidth saved (58 vs 19 µs per layer at 48 tokens)

Numbers, method and the microbenchmark are in the repository's docs/results.md.

Measured — same nodes, same prompts, same day as the vLLM stack

SGLang + overlay (production-1.0)vLLM production-1.2 (DCP4, RoCE)
prefill 32K, unique random words (salted needle)3,068 tok/s · 10.7 s2,094–2,362 tok/s · 14.2–15.5 s
prefill 128K, unique random words2,580 tok/s · 51.1 s2,152 tok/s · 60.5 s
prefill 32K / 128K, repetitive filler4,063 / 3,435 tok/s2,652 / 2,550 tok/s
decode, single stream (counting / code / prose, greedy, thinking off)94.4 / 67.8 / 32.4 tok/s93.0 / 66.6 / 31.3 (best boot 94.5 / 74.5 / 35.9)
prose story, 1 / 2 / 3 / 4 streams, aggregate30.0 / 49.6 / 62.2 / 72.3 tok/s30.0 / – / – / 69.6
KV pool4,219,904 tokens (auto-sized at fraction 0.80; 4.2–5.8M across boots)3,418,145 (6 GiB pinned per rank, DCP4)
context524,288500,000
host memory floor during the 128K prefill11–14 GB free per node6–9 GB
boot to healthy583 s~10.5 min
needles 32K / 128K, count to 20, images, toolsfound / found, exact, pass, passsame

Window 7 on 2026-09-17 (chunked prefill 2,048, RoCE collectives, Engram pool + prefetch) against the production vLLM boots of the same week. Prefill tok/s = prompt tokens / time to first token, prompts unique per run so no prefix cache can help. The KV pool is larger without any DCP because SGLang leaves 7.15 GB per rank for it at fraction 0.80 and its per-token cost is lower; it moves with the page cache, so pin it with --max-total-tokens if identical boots matter.

Engram gather, per layer (rank 0, in-graph timer)per-call thread spawn, 1,024-token chunks (window 6)persistent pool + next-chunk prefetch, 2,048-token chunks (window 7)
prefill, per call10.9–15.9 ms7.7–9.9 ms
prefill, per prompt token10.7–15.5 µs3.8–4.8 µs
decode step, per call (~80 owned rows)1.17–1.38 ms0.86–1.09 ms

The prefetch only removes the page faults — the callback still copies the rows — and the larger chunk halves the number of calls; the pool removes 64 thread creations per call.

NCCL (window 5)RoCE collectives (window 6, same config)
decode counting / code / prose86.2 / 59.5 / 30.9 tok/s94.5 / 67.9 / 32.6
prose story 1 / 2 / 3 / 4 streams, aggregate29.9 / 43.2 / 51.7 / 66.030.0 / 47.0 / 58.9 / 74.3
prefill 32K / 128K14.0 s / 63.1 s14.5 s / 63.3 s

The same runtime and the same fabric numbers as the vLLM recipe's RoCE section (48 KB all-reduce 100 → 21 µs in graph replay, crossover near 1.2 MB, so the route stops at 1 MB). +6–14 % single stream and +13 % at four streams; prefill unchanged by construction. ROCE_AR=0 returns to NCCL.

Bring-up — seven windows

WindowWhat broke or changedOutcome
1pinned staging allocated on the ambient (CUDA) device — "Only dense CPU tensors can be pinned"explicit device=cpu for the staging; the zero-size parameters stay on the model device
2graph capture died in fp8_fp4_paged_mqa_logits (no schedule metadata for the ratio-1/2 indexers)indexer_schedule
3server warm-up: "Unsupported sparse-MLA prefill configuration … extra_page_block_size=128"sm120_prefill_pages
4128K prefill exhausted host memory on every node; earlyoom killed two ranks, NCCL watchdog abortprefill_flush + 1,024-token chunks; 32K had reached 2,774 tok/s at 2,048 chunks before the crash
5first full boot: 604 s; needles 32K 14.0 s (2,333 tok/s) / 128K 63.1 s (2,087); decode 86 / 60 / 31; KV 4.4–5.8Mworks; already ahead of vLLM on prefill at 32K, level at 128K
6RoCE collectives on (roce_collectives); a rank launched before the previous engine released the GPU hung the fleet in initdecode 94.5 / 67.9 / 32.6; launcher waits for every GPU to be free
7Engram worker pool + scheduler prefetch + 2,048-token chunksprefill 3,068 / 2,580 tok/s; decode unchanged; 11–14 GB free at 128K — production since 22:00, image production-1.0

All on 2026-09-17, each window ~45 minutes of production downtime with the vLLM fleet restored in between.

05Notes

Operational caveats

  • The base tag moves.dev-dsv41 is a moving tag: upstream's dsv4.1 branch was rebuilt on main on 2026-09-17 (new sglang-kernel, and the _owned_rows hook point is gone). The image is pinned by digest in the recipe repository; rebase the overlay on a new tag deliberately, with the CPU hook test first.
  • Chunk size is a host-memory knob.2,048-token chunks hold 11–14 GB free per node through a 128K prefill with the allocator flush; 1,024 holds 16–20 GB. Larger chunks are untested. earlyoom on these nodes fires at 2.4 GB, and a killed rank takes the fleet down.
  • No decode-context parallelism in this engine for V4.1.The KV pool is larger than the vLLM DCP4 pool anyway, but it is auto-sized: 4.2–6.6M tokens between boots depending on the page cache. --max-total-tokens pins it.
  • One served name.SGLang serves a single --served-model-name; chat and completion requests with any model are answered, the alias hook only makes the other ids appear on /v1/models and /v1/models/{id}.
  • RoCE-v2 GID index is per node.One of our four nodes has it at index 4 where the others have 3; a wrong index fails every queue-pair connect and the route falls back to NCCL. The launcher probes it; so should any other one.
  • Never start a rank on a busy GPU.A rank that starts while the previous engine still holds the device dies with "CUDA-capable device(s) is/are busy" and the other three hang in torch.distributed init until the watchdog timeout.
  • The slowest rank paces every step, and GB10 has a slow state.Same as the vLLM recipe: one unit is clock-capped, and step time swings between GPU states nvidia-smi does not show. Compare configurations on the same minute.
© 2026 AidenLabBuilt on measured results, not vendor claims.