← Research Research · Serving · 14 min read
13changes between pip install and a healthy serve

Hosting GLM-5.2 (755B) on RTX PRO 6000 Blackwell with vLLM

A plain pip install does not serve this model on SM120. Why vLLM's backend selection fails here, the five failures in the order they appear, the model-specific parts explained, every patch and flag, two-node bring-up and recovery, and the route comparison against the community b12x image.

model
GLM-5.2 · FP8 · 755 GB · 141 shards
silicon
2 VMs × 8 RTX PRO 6000 Blackwell 96 GB · SM120
stack
vLLM 0.28.0 · torch 2.13.0 · FlashInfer 0.6.16.post3 · Ray 2.50.0 · CUDA 13
published
November 2026

We serve GLM-5.2 in FP8, about 755 GB in 141 shards, on two G4-class VMs with sixteen RTX PRO 6000 Blackwell GPUs between them, using vLLM and Ray behind LiteLLM so it is usable from pi and Claude Code. A plain pip install vllm && vllm serve does not work on this hardware for this model. This page explains why, lists every patch and flag we needed, and shows how the whole thing is brought up, validated and recovered.

13
changes between pip install and a healthy serve
5
failures, each hidden by the last
755 GB
FP8 weights · 4-minute load
262K
context window
thingvalue
GPUs2 VMs × 8 NVIDIA RTX PRO 6000 Blackwell 96 GB (SM120), same VPC
Weightsfull 141-shard FP8 copy on local SSD on both VMs
Pythona dedicated venv on Python 3.11 that carries every patch below
Stackvllm 0.28.0, torch 2.13.0, flashinfer 0.6.16.post3, ray 2.50.0, fastsafetensors, litellm 1.98.0, CUDA 13 with nvcc
EndpointsvLLM on the head VM at :8000/v1; LiteLLM in front at :4000/v1
Local SSD on these VMs is ephemeral. Snapshot the patched venv as a tarball together with this guide to a bucket you own; the venv is where all the patches live. Verify every shard's size against the Hugging Face manifest after any re-download, since a truncated transfer leaves the file count looking fine and fails only at load.

Why pip install fails here

vLLM ships around fifteen attention backends and picks one by GPU compute capability. Its tables are written for what vendors test. H100 and H200 are SM90, B200 is SM100, and both have mature kernels for everything GLM-5.2 needs. The RTX PRO 6000 is SM120, where GLM-5.2-class sparse-MLA kernels barely exist: one new, shape-limited FlashInfer module.

GLM-5.2 is not a vanilla transformer. It combines multi-head latent attention with a DeepSeek-V3.2-style sparse "lightning indexer", where each query attends to its top-2048 keys, chosen per layer. Serving it needs sparse-MLA kernels, which are GPU-specific CUDA. On SM120 the only implementation JIT-compiles at startup, so it needs nvcc and ninja on PATH, and its decode kernels exist only for a hard-coded shape table of heads in {8, 16, 32, 64, 128} by top-k in {128, 512, 1024, 2048}.

dense MLAsparse MLA · prefillsparse MLA · decode at 4 heads/GPUSM90 · H100 / H200maturematurematureSM100 · B200maturematurematureSM120 · RTX PRO 6000works (Triton)JIT · shape tableno instantiation
Figure 1. Kernel coverage for GLM-5.2's attention by GPU architecture. On SM120 the sparse decode kernel has no instantiation for 4 heads per GPU, which is exactly where tensor-parallel 16 lands.

The result is that the default pipeline fails in five layers, each invisible until you hit it.

The five failures, in order

1no attentionbackend2ninjamissing3CHECKnum_tokens4long-contextgarbage5abortrace+ packaging bugindex_topk_pattern: nulleach failure is invisible until the previous one is fixed; 3, 4 and 5 appear only after the 4-minute weight load
Figure 2. The order the failures appear in. Three of them only show up after the four-minute weight load, which is what makes each attempt expensive.
#errorcausefix
1No valid attention backend found for cudahas_flashinfer() needs nvcc on PATH, and Ray workers inherit the raylet's environment, not vllm serve's.Export the venv bin and /usr/local/cuda/bin when starting Ray, on every node.
2FileNotFoundError: 'ninja'FlashInfer's sampler JIT needs ninja. Surfaces later, during startup profiling.Same environment fix; ninja ships inside the venv.
3CHECK failed: num_tokens > 64At TP16 the indexer's 64 heads split to 4 per GPU. 4 is not in the kernel shape table, so decode falls through to the prefill kernel and crashes hard.Sidestep the sparse decode kernel; see the shape table below and patch A.
4Long-context output is garbageEven after 1 to 3 are fixed, stock FlashInfer 0.6.x SM120 sparse output is gibberish or loops beyond about 1K tokens. Deterministic, and short prompts look fine, which makes it brutal to debug. Unfixable on stock 0.6.x.Dense fallback: patch A.
5Engine dies on client disconnectAn abort races the scheduler (KeyError in update_from_output) and takes the whole engine down when a user hits Esc.Defensive patch B.

Plus a model-packaging bug that is independent of the hardware: the checkpoint's config.json ships index_topk_pattern: null, while vLLM's sparse path reads only that field. The expensive indexer then runs on all 78 layers when only 21 should, attention corrupts, and you get the "France France France" looping bug.

The model-specific bits explained

The layer map. GLM-5.2 has 78 layers. 21 of them are "full" and run the indexer; 57 are "shared" and skip it. The checkpoint stores this in indexer_types, but vLLM reads index_topk_pattern, which ships null. Derive the pattern from your checkpoint's config and pass it in. Do not copy the string from a guide; pruned variants differ.

python3 -c "import json; print(''.join('F' if t=='full' else 'S' for t in json.load(open('config.json'))['indexer_types']))"
# → 78-char F/S string, passed via:
--hf-overrides '{"index_topk_pattern":"FFFSSS..."}'
78 layers · F = runs the lightning indexer (21) · S = shared, skips it (57)11020304050607078with index_topk_pattern: null, vLLM runs the indexer on all 78 → corrupt attention and the "France France France" loop
Figure 3. The 78-layer indexer map for the full zai-org GLM-5.2-FP8 checkpoint, derived from its config. Three full layers up front, then one in every four.

The shape dispatch table. The SM120 sparse decode kernel is instantiated only for the head counts and top-k values in Figure 4. Tensor-parallel 16 gives 4 heads per GPU, which is not dispatchable. There are three ways out. Pad the heads from 4 to 8 with zero rows, which is mathematically exact and which we validated against a pure-PyTorch reference. Add a (4, 2048) instantiation, which we tried: the kernel was nondeterministic and wrong, so do not trust an untested shape. Or drop to tensor-parallel 8 for 8 heads per GPU, which is impossible here because 755 GB of weights do not fit in one VM's 768 GB once the KV cache is counted.

top-k keys per query12851210242048heads / GPU81632641284GLM-5.2 at TP16: 64 heads ÷ 16 GPUs = 4 → not in the table
Figure 4. The decode kernel's shape table on SM120. Every cell that exists is a tested combination; GLM-5.2 at TP16 sits outside it.
8 × 96 GB768 GB VRAM, one VMGLM-5.2 FP8755 GB weights+ KV cache, activationsdropping to TP8 to get 8 heads per GPU does not fit on one VM, so the shape table cannot be dodged that way
Figure 5. The memory wall that rules out tensor-parallel 8 on a single eight-GPU VM.

Dense fallback. With everything patched, long-context output is still garbage on stock FlashInfer 0.6.x. The community b12x image (see the last section) has working sparse kernels; stock does not. Dense TRITON_MLA is correct at every length we tested, from 30 to 23K tokens. The trade-off is quadratic long-context prefill and a ceiling around 256K.

The patches

Four small edits, each tagged in a comment so they can be re-applied after an upgrade.

A. Dense fallback. In vllm/model_executor/models/deepseek_v2.py, at both occurrences:

self.is_v32 = False  # DENSE_FALLBACK: sparse SM120 kernels corrupt long context

plus export VLLM_ATTENTION_BACKEND=TRITON_MLA.

B. Abort race. In vllm/v1/core/sched/scheduler.py:

# req_index = model_runner_output.req_id_to_index[req_id]
req_index = model_runner_output.req_id_to_index.get(req_id)
if req_index is None: continue  # ABORT_RACE_FIX

C. Cross-node allreduce deadlock. In vllm/distributed/device_communicators/custom_all_reduce.py, an early return at the top of _init_mnnvl_buffer, tagged PATCHED-NO-CROSSNODE-SYMM. vLLM falls back to pynccl.

D. Python 3.11 syntax. In flashinfer/comm/fd_exchange.py, add from __future__ import annotations as line 1.

Two more edits sit in our venv but are inert on the dense path: HEAD_PAD_TP16 and MASKED_MHA_NA in flashinfer_mla_sparse_sm120.py. They are only needed if sparse is re-enabled.

All changes at a glance

#changetypewhy
1nvcc and ninja on PATH when starting Rayenvfailures 1 and 2
2unset NCCL_GRAPH_FILE, NCCL_P2P_DISABLE=1, VLLM_ENABLE_PCIE_ALLREDUCE=0, NCCL_SOCKET_IFNAME=eth0envNCCL on virtualised PCIe; an empty-string graph file breaks NCCL
3ulimit -n $(ulimit -Hn)envthe default 1024 file descriptors crash the raylet
4VLLM_CACHE_ROOT on local SSDenva compile cache on NFS home means stale-handle crash loops
5is_v32 = False (×2) and VLLM_ATTENTION_BACKEND=TRITON_MLApatchfailure 4
6scheduler .get(req_id) and skippatchfailure 5
7_init_mnnvl_buffer early returnpatchtwo-node allreduce deadlock
8fd_exchange.py future importpatchPython 3.11
9--hf-overrides index_topk_patternflagnull layer map → looping
10--reasoning-parser glm47flag<think> leaks into answers otherwise
11--enable-auto-tool-choice --tool-call-parser glm47flagtool clients get 400 without it; the parser is glm47, not glm47_moe, despite the filename
12--block-size 64 and chunked prefill at 8192flagsparse page-size requirement and bounded attention allocation
13--max-model-len 262144flagcompaction and agents need 111K plus 20K of headroom

Launch and two-node recovery

The exact command that works after the patches. The pattern string is for the full zai-org GLM-5.2-FP8 checkpoint; derive your own for other variants.

vllm serve <model-dir> \
  --served-model-name glm-5.2 \
  --tensor-parallel-size 16 --distributed-executor-backend ray \
  --max-model-len 262144 \
  --enable-chunked-prefill --max-num-batched-tokens 8192 \
  --max-num-seqs 8 --block-size 64 \
  --gpu-memory-utilization 0.92 \
  --load-format fastsafetensors \
  --reasoning-parser glm47 \
  --enable-auto-tool-choice --tool-call-parser glm47 \
  --hf-overrides '{"index_topk_pattern":"FFFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSSFSSS"}'

Two-node bring-up:

# both VMs: ray stop --force, verify nvidia-smi shows 0 compute procs
# VM A: ray start --head --port=6380   (env block: PATH incl venv bin + cuda, NCCL flags, ulimit)
# VM B: ray start --address=<HEAD_IP>:6380
# verify: ray.init() → ALIVE=2 GPUS=16
# VM A: nohup bash launch_vllm_multi.sh > vllm_serve.log 2>&1 &

Startup takes six to ten minutes: the JIT compile plus about four minutes of weight load. The success markers in the log are Using TRITON_MLA attention backend, Setting kv cache block size to 64 and Application startup complete.

Recovery gotchas we hit both of. A crashed serve leaves orphan Ray workers holding every GPU, so the next launch hangs forever at "waiting for placement group": run ray stop --force on both VMs, verify zero GPU processes, then relaunch, and never run a second vllm serve while one is healthy. And changing max_model_len while the compile cache sits on NFS home produces an OSError: Stale file handle crash loop: keep the cache on local SSD.

Clients

LiteLLM on the head VM fronts vLLM with both wire formats; set your own API key in its config. One config line matters for speed:

extra_body: {chat_template_kwargs: {enable_thinking: false}}

That disables hidden reasoning and makes responses about 2.3× faster. Remove it for hard reasoning tasks.

  • pi: add a provider pointing at the LiteLLM endpoint with the OpenAI-completions API, then pi --model <provider>/glm-5.2, or /model in-session.
  • Claude Code: set ANTHROPIC_BASE_URL to the LiteLLM endpoint, ANTHROPIC_AUTH_TOKEN to your key, and ANTHROPIC_MODEL=glm-5.2.

Validate before serving to humans

checkguards against
Loop guard: greedy "What is the capital of France?" returns one clean sentencethe looping bug
Long-context retrieval: about 20K tokens of filler, then "what animal is mentioned?" answers correctlyfailure 4; short prompts pass even when long context is broken
Abort safety: start a long generation and kill the client; the engine stays healthy with /health at 200 and no fatal errorsfailure 5

Performance

The first configuration that served correctly, the one above, decoded at about 27 tok/s for a single request. The two-node allreduce over Ethernet is the ceiling there. Further tuning on the same two VMs has since taken single-stream decode to 85 to 90 tok/s, with about 640 tok/s aggregate across parallel requests. Those changes are not written up yet; they get their own page once they are reproduced from a clean VM.

85–90 tok/s
single-stream decode, tuned config
~640 tok/s
aggregate, parallel requests
0.6 s
TTFT at 21K with prefix cache
~10 min
111K cold prefill, dense
0255075100dense MLA · first working config27 tok/sdense MLA · after tuning (follow-up)88 tok/sb12x sparse · NVFP4 pruned · 1 VM × 8 GPUs44 tok/sdecode tok/s, single stream
Figure 6. Single-stream decode for the first dense config, the tuned dense config, and the b12x route. The dense bars are FP8 over sixteen GPUs on two VMs; the b12x bar is a pruned NVFP4 checkpoint on eight GPUs in one VM.
10 min30 min60 min50K100K150K200K250K111K cold prefill · ~10 min (measured)21K · 0.6 s TTFT with prefix cachedashed = quadratic extrapolation to the 262K limitcontext length (tokens)
Figure 7. The dense trade-off: prefill time against context length, two measured points and the quadratic extrapolation to the 262K limit. Prefix caching hides this for agent workloads that reuse a long prefix.

The alternative: the b12x route

Manuel Parra's guide at th3nolo.com, Serving GLM-5.2-504B on 8× RTX PRO 6000, keeps sparse attention fully working with a community image. The checkpoint is 0xSero/GLM-5.2-504B, REAP-pruned by Cerebras to NVFP4 at 288 GB, which fits one eight-GPU VM. The image is a vLLM built for sm_120a with working B12X sparse kernels, published by voipmonitor.

--attention-backend B12X_MLA_SPARSE --moe-backend b12x --kv-cache-dtype fp8 --quantization modelopt_fp4
--enable-chunked-prefill --max-num-batched-tokens 8192 --decode-context-parallel-size 8
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0   # plus the same index_topk_pattern fix, and clear VLLM_B12X_MLA_EXTEND_MAX_CHUNKS

The long-context breakthrough on that route: the b12x indexer locks a workspace of about 708 MB, and the fix is bounding per-step work with chunked prefill at 8192, decode context parallel of 8 and the profiler estimate switched off. Cold 48K, 128K and 240K prompts then come back correct at 43 to 45 tok/s. Add fastsafetensors for four-minute loads and a scale-to-zero watchdog.

dense route (ours)Parra's b12x route
Softwarestock vLLM 0.28 plus four small patchescommunity Docker image, pinned
Modelfull FP8, 755 GBREAP-pruned NVFP4, 288 GB
Hardware2 VMs × 8 GPUs1 VM × 8 GPUs
Attentiondense MLA, correct to 256Ksparse, correct to 240K, about 45 tok/s
Best forthe full checkpoint, standard tooling, multi-VMmaximum long-context performance on a single VM

Both routes need the same index_topk_pattern fix, the same NCCL discipline, and the same three validation tests.

References and credits

  • th3nolo.com, Serving GLM-5.2-504B on 8× RTX PRO 6000 GPUs (Manuel Parra): the pattern fix, the NCCL flags, dense-fallback isolation, the b12x route and the DCP long-context config.
  • 0xSero for the REAP-pruned checkpoint and recipe, voipmonitor for the b12x image, Cerebras for REAP, and the FlashInfer and vLLM SM120 contributors.
SM120 support in FlashInfer and vLLM is a moving target; re-run the three validation tests after any upgrade. And a note on authorship: the first draft of this guide was written by GLM-5.2 itself, running on the deployment it describes, with a human operator and a coding agent doing the typing, the patching and the 3 a.m. debugging.