GLM-5.3-Flash-NVFP4-DFlash2-2x-DGX-Spark README

The project's README from github.com/tonyd2wild/GLM-5.3-Flash-NVFP4-DFlash2-2x-DGX-Spark. As one markdown file: README.md.

GLM-5.3-Flash NVFP4 + DFlash2 on 2x NVIDIA DGX Spark

OpenAI-compatible vLLM serving of zai-org/GLM-5.3-Flash (320B total / 18B active MoE) across two DGX Spark (GB10, SM121) nodes at tensor-parallel 2, using the RedHatAI/GLM-5.3-Flash-NVFP4 compressed-tensors quant (the corruption-free default, see below), fp8 KV cache, and the incoai/GLM-5.3-Flash-DFlash2 speculative drafter. 262,144-token context. Deployed the same day the model dropped.


⭐ Checkpoint: RedHatAI/GLM-5.3-Flash-NVFP4 is now the default (corruption fix)

ModelOpt-quantized NVFP4 builds of GLM-5.3-Flash (LibertAIDAI/GLM-5.3-Flash-NVFP4 and the abliterated variants) emit intermittent corrupted token IDs (vLLM #54150). Nearly invisible in English, but when a corrupted token lands inside a tool-call block the parser desyncs and generation can spiral into a repetition lock.

We reproduced and fixed it on this exact cluster (Korean-Hangul probe, temperature 0, non-streaming, 3 passes):

checkpoint quant_method U+FFFD count (3 runs)
ModelOpt NVFP4 (LibertAIDAI / keys-ablit) modelopt 4 / 9 / 8
RedHatAI/GLM-5.3-Flash-NVFP4 compressed-tensors 0 / 0 / 0

Default checkpoint: RedHatAI/GLM-5.3-Flash-NVFP4. Ungated, same Glm5NextForConditionalGeneration arch, drop-in — no flag changes (--moe-backend marlin, DFlash2 k=7, fp8 KV all identical), just repoint the model path. Loads ~2x faster (11 large shards vs 120 small). Tradeoff: it also quantizes activations to 4-bit (W4A4) where the weight-only builds are W4A16, so expect a few points lower on hard reasoning — but the output is correct. Make sure the vision chat_template_mm.jinja is present in the weights dir or image requests 500.

Corruption first flagged by @ajclark (issue #10). Uncensored (abliterated) builds remain available but carry the ModelOpt corruption until a compressed-tensors abliteration exists.

Weights: censored or uncensored (drop-in)

Pick your weights: same launcher, same recipe, just point the model path at either. Both are NVFP4 and load identically.

HuggingFace notes
⭐ Default (recommended) RedHatAI/GLM-5.3-Flash-NVFP4 compressed-tensors, corruption-free (see fix above)
Censored (legacy) LibertAIDAI/GLM-5.3-Flash-NVFP4 stock NVFP4 weight-only — ⚠️ ModelOpt token corruption
Uncensored (abliterated) drowzeys/keys-GLM-5.3-Flash-NVFP4-ablit-l15-45-anchorstock abliterated (layers 15-45, anchor-stock), no refusals

Uncensored abliteration credit: drowzeys/keys.

Two firsts, as far as we can tell: the first working GLM-5.3-Flash deployment on DGX Spark (seven day-0 bugs deep — docs/DEPLOY-REPORT.md), and the first working DFlash2 deployment of this model on GB10 (docs/DFLASH2-SPECULATIVE-DECODING.md).

🔀 Running all four Sparks? The same images scale to TP4 with the model-native 1M context — see the sibling repo: GLM-5.3-Flash at TP4 · 1M KV · 4x DGX Spark →


Quickstart

1. Pull the images (GHCR, public, anonymous — no build required):

docker pull ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v11-dflash2   # with DFlash2 (recommended)
docker pull ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v8            # base, fp8 KV, no drafter

They contain only vLLM + our patches — no model weights (those bind-mount at runtime). The launchers reference the local radixark/… tag names, so retag once:

docker tag ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v8 ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v8
docker tag ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v11-dflash2 ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v11-dflash2

2. Fetch the weights to the same path on both nodes (or NFS-export from the head): RedHatAI/GLM-5.3-Flash-NVFP4 (default) → /var/tmp/glm-5.3-flash-nvfp4. For DFlash2, also fetch the drafter (2.2 GB) → /var/tmp/models/GLM-5.3-Flash-DFlash2.

3. Install the SM121 top-k fix on both nodes. Both published images still contain a decode-time top-k kernel that hard-kills the engine on any decode past ~24K context on GB10 (the launcher bind-mounts the fix over it):

mkdir -p ~/patches
cp docker/sparse_attn_indexer_kpool_sm121.py ~/patches/sparse_attn_indexer_kpool.py

Why, and what the crash looks like: docs/SM121-CRASH-FORENSICS.

4. Edit the launcher for your fabric. Set the IPs/paths at the top of launch-glm53-vllm-tp2-dflash2.sh, and the NCCL interface names inside the docker run body (NCCL_IB_HCA, NCCL_SOCKET_IFNAME, NCCL_IB_ADDR_RANGE) — wrong NIC names fail silently. ibdev2netdev and ip -br a will tell you what yours are.

Then launch — pre-launch ritual on BOTH nodes, every time (GB10 unified memory):

sync; echo 3 | sudo tee /proc/sys/vm/drop_caches      # both nodes
./launch-glm53-vllm-tp2-dflash2.sh 1    # worker FIRST
sleep 25
./launch-glm53-vllm-tp2-dflash2.sh 0    # then head — serves :8000

Without the drafter, use launch-glm53-vllm-tp2.sh (v8 image, MTP-4, ~21.8 tok/s) — same ritual.

5. Smoke test. Wait for readiness first (~15 min; the shard load dominates). Poll /health, never /v1/models — that returns 200 even with a dead engine:

until curl -sf http://<head>:8000/health >/dev/null; do sleep 20; done
curl http://<head>:8000/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"glm-5.3-flash","messages":[{"role":"user","content":"2+2=?"}],
       "max_tokens":40,"chat_template_kwargs":{"enable_thinking":false}}'

Full serve args, NCCL fabric env, and the rationale for every flag: docs/DEPLOY-REPORT.md.

Build the images yourself instead (only if you want to modify the patches)

The base is the public day-0 image:

docker pull vllm/vllm-openai:glm53-flash-arm64-cu130     # or -x86_64- for x86
cd docker
for i in $(seq 1 9); do
  docker build -f "Dockerfile.glm53-sm121-v$i" -t "ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v$i" . || break
done
docker save ghcr.io/tonyd2wild/vllm-glm53-flash:sm121-v8 | ssh <worker> docker load

The chain is mostly linear (v1→v3→v4→…→v9); v2 is an optional NaN-debug branch off v1 that nothing else builds on. For DFlash2, build overlay-dflash2/ on top of v8 afterwards.

Known issue: on main the FROM lines of v2–v6 still reference the original ad-hoc tag names (sm121-nope-mla, sm121-fi618, sm121-fi618-nccl, sm121-final) rather than sm121-v1…v5, so the loop above only works after #5 (thanks @ozskywalker) lands. Until then, tag each stage with the name the next Dockerfile expects.

Published image digests, so you can tell whether a local build differs: sm121-v8 → sha256:d77d375c742fc54f436dec5108b440f58f021bc6600052bf0e8fe5840357e78f · sm121-v11-dflash2 → sha256:4def0ef644cb2e9814136dcffd5e385e21bc594f48f3b292234051904abe85a6


Results (TP2, 2026-08-28)

DFlash2 + fp8 KV — the shipped configuration. All measured on our own hardware; the harness is probes/bench_c1c6.py, run as: python3 probes/bench_c1c6.py --url http://<head>:8000 --rounds 2 --max-tokens 400

value
Single-stream decode, code prompt, warm 46.9 tok/s at 74.1 % draft acceptance
Single-stream decode, structured output 54–61 tok/s (temp 0, 3 runs)
KV pool 581,040 tokens @ 262K context, profiler-sized (see the ceiling section)
Context 262,144
KV cost of the drafter zero — it slot-shares the MLA tensors
Boot ~15 min (shard load dominates)

Concurrency sweep — 2 waves/level, 400-token generations, mixed code+prose, zero failures:

C1 C2 C3 C4 C5 C6
aggregate tok/s 35.1 41.6 40.6 47.5 56.2 47.7
per-stream tok/s 35.1 23.2 17.3 15.3 17.5 13.3
accepted ÷ drafted 0.53 0.45 0.42 0.40 0.51 0.40

The progression, same fleet, same prompt style:

config decode note
bf16, no speculation 14.3 tok/s
fp8 KV + MTP-4 21.8 tok/s previous flagship
fp8 KV + DFlash2 46.9 tok/s 2.15x — this repo
fp8 KV + DFlash2 at TP4 68.5 tok/s four nodes, 1M context — sibling repo

Throughput tracks how predictable the output is, not just the config: structured/list/ tool-argument output drafts at ~0.9 acceptance, freeform prose nearer 0.33. Agentic traffic lives in the high-acceptance zone. Detail and how to read these: docs/BENCH-C1-C6-DFLASH2.md.

KV pool ceiling on TP2 (2026-08-28)

Let vLLM's profiler size the pool. Do not pin --kv-cache-memory.

That is the entire lesson, and it cost us a night of boots to learn. When you pass --kv-cache-memory, vLLM still runs the profile pass but never subtracts the measured activation peak (gpu_worker.py:475-495) — it hands you exactly the number you asked for and --gpu-memory-utilization becomes dead. Allocation succeeds, warmup succeeds, a short generation succeeds, and then the first long prompt has nowhere to put its activations and the engine dies. We reproduced that failure at four different pins.

Profiler-sized figures, all at --max-model-len 262144 so they are comparable:

config KV pool verified
DFlash2 + fp8 KV 581,040 tokens serving; survived a 28,818-token prompt with the engine healthy after
No drafter, fp8 KV 965,166 tokens allocated and booted; long-prompt survival not yet confirmed

The DFlash2 drafter costs ~4.8 GiB of KV headroom — far more than its 2.2 GiB of weights — which is a real trade nobody had priced: roughly +91 % decode speed for −40 % pool. Choose per workload.

Three traps worth knowing before you tune this yourself:

Operational note: on these 121 GiB unified-memory nodes, vm.swappiness=0 is mandatory and does not survive a reboot. With swap active the kernel pages vLLM out mid-load and triggers a UVM driver livelock — one worker thread spinning at 100 %+ CPU, UVM GPU kthread hot, GPU reporting high utilization at idle wattage, shard loading frozen at a reproducible point. It does not recover on its own.

A previous revision of this section published a 727,583-token figure at a 7 GiB pin as the usable ceiling. That configuration is unsafe (pinned, so no activation headroom), and no log of that measurement survived — it was quoted from a terminal session rather than a captured file. It has been withdrawn rather than restated.

Tuning notes


Status: work in progress

This repo is an active bring-up log, not a finished product. Everything published here is measured on our own hardware and dated — if a number is not in a dated table, treat it as unverified.

state
DFlash2 + fp8 KV, TP2 ✅ Proven. Benchmarked C1–C6, zero failures. This is the config to copy.
DFlash2 + fp8 KV, TP4 / 1M ctx ✅ Serving — 68.5 tok/s, 2,622,494-token pool. See the sibling repo.
DFlash2 + NVFP4 KV, TP2 ⚠️ Partial. Serves and drafts (35.9 tok/s, 0.563 acceptance, 334K pool), but prompts long enough to need chunked prefill (>~3K tokens) kill the rank-0 worker. Root cause open; the standalone drafter KV path is the suspect. Details.
InstantTensor fast load ⚠️ Experimental — 15x faster loads, unstable multi-node. See below.

Why this needs a patched image

The vLLM PR authors' day-0 image (vllm/vllm-openai:glm53-flash-arm64-cu130) works on B200. On GB10/SM121 it fails five separate ways. Our derivative (docker/Dockerfile.glm53-sm121*, applied in order) fixes those five, then adds a sixth patch that unlocks fp8 KV:

  1. NoPE MLA vs the SM12x sparse backend — the only stock capability-12 sparse-attention backend requires the packed fp8_ds_mla layout, which hardcodes DeepSeek's pe_dim=64. GLM-5.3 is NoPE (qk_rope_head_dim=0) → assert death in warmup. Fix: extend vLLM's SM90 NoPE sparse-MLA backend to SM121 with the FA2 path — probed on-GPU with the model's real shape before trusting it (probes/probe_sm121_nope_mla.py).
  2. FlashInfer 0.6.17 FA2 MLA NaN — the FA2 scheduler produces NaN for 64–256-row batches on SM121 (bisect: probes/probe_fa2_bisect.py), and normal prompts land exactly there. Fix: FlashInfer 0.6.18 nightly.
  3. The nightly's dependency sabotage — it silently downgrades nvidia-nccl-cu13 to 2.29.7 (NCCL "internal error" on the Spark IB fabric; re-pin 2.30.7) and skews nvidia-cutlass-dsl to a mixed 4.7.0/4.6.2 state (CuTeDSL warmup ICE; re-pin 4.6.2). Audit transitive pins after ANY pip install in these images.
  4. PDL on unvalidated silicon — vLLM enables Programmatic Dependent Launch for capability ≥ 9, including SM121, in the Triton kernels carrying KDA recurrent state. Gated off on SM12x.
  5. Indexer uninitialized top-k — the kpool top-k destination was torch.empty and the kernels only guarantee the first min(k, valid) entries; short rows carried garbage pool ids → bogus token indices → NaN lottery. Fix: init to -1 + clamp (docker/patch_v7.py).
  6. fp8 KV cache unlock (v8, docker/patch_v8_fp8.py) — see below.

Two serve-flag landmines, no code needed:

fp8 KV cache on GB10: a two-line fix (and, as far as we can tell, a first)

FlashInfer gates fp8 MLA KV to SM90, and naively relaxing the gate fails with CUDA "invalid argument" (probes/probe_fa2_fp8.py). The real cause (docker/patch_v8_fp8.py): the fa2 fp8 branch forces CTA_TILE_KV=32, a Hopper 228KB-smem assumption. On GB10's ~101KB opt-in max that over-requests shared memory (117,312 B > 101,376 B) at cudaFuncSetAttribute, before the kernel ever launches. Capping the tile instead of forcing it (fp8 keeps TKV=16 on 100KB-class devices — 91,680 B, fits) makes fp8 KV work: verified on-GPU, rel-err ~0.005 vs an fp32 reference, then end-to-end in production.

As far as we can tell this is the first fp8 KV cache for a NoPE-MLA model on any consumer Blackwell part. Upstream-ready issue drafts with receipts: docs/issue-flashinfer-fp8-mla-sm121.md, docs/issue-vllm-nope-fp8-ds-mla.md.


Hard-won operational rules


Deeper reading

doc what's in it
DEPLOY-REPORT the seven day-0 bugs, root causes, receipts, every serve flag
DFLASH2-SPECULATIVE-DECODING the drafter port: four patches, the KV-layout fix, nine boots of failure modes
BENCH-C1-C6-DFLASH2 full concurrency tables and how to read them
SM121-CRASH-FORENSICS why the fleet "randomly" died: a topk kernel bug and phantom KV backing
GB10-KV-MEMORY-LADDER why KV budgets above vLLM's suggestion die, and the driver-level mechanism
KV-HUNT-672K-TP2-RECORD the 8-attempt hunt past the 507K wall
OPEN-PROBLEMS everything we broke and could not fix — reproducible, with next probes. Start here if you want to contribute.

Debugging kit (reusable for any day-0 model on new silicon): probes/probe_sm121_nope_mla.py (probe a kernel with your real geometry before patching arch gates) · probes/probe_fa2_bisect.py (NaN bisect over batch shapes) · probes/probe_mhc.py (A/B a Triton kernel vs its torch reference) · probes/gb10_alloc_probe.py (map the allocation wall) · probes/bench_c1c6.py. The deploy rep

GLM-5.3-Flash-NVFP4-DFlash2-2x-DGX-Spark (tonyd2wild/GLM-5.3-Flash-NVFP4-DFlash2-2x-DGX-Spark)

Searching Database…

这是什么?一套部署方案和打过补丁的 Docker 镜像,用于在两个 NVIDIA DGX Spark 节点上以 OpenAI 兼容 API 的方式提供 GLM-5.3-Flash(总参数 320B / 激活 18B 的 MoE)服务,采用 vLLM 张量并行 2、NVFP4 量化和 DFlash2 投机解码。
这是一个可安装的包,还是一份实操部署方案?它是一份调通日志加部署方案:包含预构建的 GHCR Docker 镜像(内含打过补丁的 vLLM)、启动 shell 脚本、基准测试探针和详细的调试报告——不是一个可以导入的库。
怎么把服务跑起来?拉取打过补丁的 GHCR 镜像(无需构建),把 NVFP4 权重下载到两个节点的相同路径,应用 SM121 top-k 内核修复,在启动脚本中填入你的节点 IP 和网卡名称,然后在两个节点上运行启动脚本并轮询 /health 直到就绪。
运行它需要什么硬件?两台通过高速互联连接起来的 NVIDIA DGX Spark 节点(GB10 芯片,SM121 计算能力),用于张量并行 2,每个节点至少约 121 GiB 统一内存。
部署时主要有哪些坑?千万不要固定 --kv-cache-memory——它会静默禁用激活余量,并在长提示词上崩溃;每次开机都必须设置 vm.swappiness=0,否则内核会触发 UVM 驱动活锁;视觉对话模板文件必须存在,否则图像请求会返回 500。
这份部署指南面向谁?面向在 NVIDIA DGX Spark 硬件上运行大型 MoE 大模型的机器学习基础设施工程师,他们需要的是一套能跑通、经过实测的多节点 vLLM 配置,而不是泛泛的教程。
除了 DGX Spark 的 GB10 芯片,它还能在别的东西上运行吗?不能——镜像和补丁专门针对 SM121(GB10)计算能力;作者指出原版 day-0 镜像在 B200 上能跑,但在 GB10/SM121 上会以五种不同的方式失败,这正是这个打过补丁的镜像存在的原因。
DFlash2 是什么,为什么在这里很重要?DFlash2 是 GLM-5.3-Flash 的投机解码草稿模型,与主模型共享 KV;配合 fp8 KV 缓存,它能把解码吞吐量提高约一倍(46.9 tok/s,而普通 MTP-4 只有 21.8 tok/s),额外内存开销极小。
为什么改用 RedHatAI 的 NVFP4 检查点而不是原版的?原版 ModelOpt NVFP4 构建会间歇性输出损坏的 token ID,可能导致工具调用解析失步,而 RedHatAI 的 compressed-tensors NVFP4 检查点在反复测试中零损坏 token,加载速度也快了约一倍。
服务器能处理多大上下文,速度如何?在自带的 DFlash2 + fp8 KV 配置下,它可服务 262,144 token 的上下文,单流解码速度约 47-61 tok/s,并已用 28,818 token 的提示词确认稳定。
我能扩展到四台 DGX Spark 节点来获得更大上下文吗?可以——一个姊妹仓库用同样的镜像以张量并行 4 达到模型原生的 1M token 上下文和 68.5 tok/s 的解码速度。
Creator
tonyd2wild
Size
0.4 MB
Stars
236
Updated
CommitSep 30
Age
1mo
Commits · 90 Days