|
|
1 неделя назад | |
|---|---|---|
| .. | ||
| adaptive-k | 1 неделя назад | |
| arx | 1 неделя назад | |
| compose | 1 неделя назад | |
| draft-trunc | 1 неделя назад | |
| fixes | 1 неделя назад | |
| megamoe | 1 неделя назад | |
| quality | 1 неделя назад | |
| snapshot | 1 неделя назад | |
| tp3 | 1 неделя назад | |
| README.md | 1 неделя назад | |
Compose overrides that stack on compose/glm53.yaml. Each one mounts files
over the image this repo builds (vLLM nightly ddd6fbca); several replace whole vLLM or
FlashInfer files, so they only match that image.
Requirements: the image built from this tree (image/build.sh) and the normal four-node setup from the main README (the same compose/glm53.yaml and a per-node compose/.env on every box). The overrides replace files inside that image, so they do not apply to other images.
rdma in mentatd's MENTAT_ANNOUNCE_IFACES (main README, step 4), or set FABRIC_SUBNETS to both subnets in each node's compose/.env. The entrypoint derives NCCL_IB_HCA and NCCL_IB_GID_INDEX from it, and arx and arxbig use the same two devices. Each rank's log shows them (arx all-reduce: rank r/4 on [dev0, dev1]). With only one device they log a warning and fall back to NCCL.Start the stack with every override, on every node: ./glm53 up -d from the repo root. The script holds the list of compose files, in order (later files win).
The first boot loads the checkpoint normally (~8 min), compiles the CUDA extensions, and writes a processed-weight snapshot per rank under weight-snapshots in the cache mount (CACHE_HOME, or the glm53_cache volume), ~48 GB per node. Later boots restore it (~3.5 min).
Check: bash smoketest/run.sh http://<head>:8002 should pass 8/8, and each rank's log should show arx all-reduce: rank r/4 and arxbig all-gather: rank r/4.
Leave a compose file out to drop that piece, or set its switch in .env (every node must use the same values):
| Switch | Default | What it does |
|---|---|---|
VLLM_ARXBIG |
1 | arxbig RDMA collectives for prefill (arx.yaml) |
VLLM_ARXBIG_AG |
0 | route prefill all-gathers through arxbig; off because its output sits in pinned memory, where GEMMs reading it run 3.7x slower |
VLLM_ARXBIG_RS |
1 | RDMA reduce-scatter buffers (~0.5 GB pinned per rank); needed by VLLM_GLM_SP_MOE_FUSED |
VLLM_ARX_RING |
0 (1 with TP=RING4) |
for boxes cabled in a ring with no switch: arx and arxbig open QPs only to rank r-1 and r+1, and at four ranks the rank between two others relays their data, with the same results as over the switch. arx takes rings of 2, 3 or 4; arxbig takes 2 or 4, so a ring of 3 needs VLLM_ARXBIG=0 |
ARX_RING_PREV_HCAS |
from mentat with TP=RING4 |
with VLLM_ARX_RING=1: the two RDMA devices of the port cabled to rank r-1. Each device uses the GID of its own IPv4 address. Device r must share its subnet with device r of the neighbour's list for that cable: root 0 then root 1 when every box is cabled the same way, root 1 first on a box whose cabling is mirrored |
ARX_RING_NEXT_HCAS |
unset | the same for the port cabled to rank r+1 |
VLLM_GLM_SP_TP |
1 | sequence parallelism for forwards of VLLM_GLM_SP_MIN_TOKENS (1024) or more (sp.yaml) |
VLLM_GLM_SP_FP8_GATHER |
1 | gather KDA attention inputs as FP8 |
VLLM_GLM_SP_MOE_FUSED |
1 | MoE combine feeding the RDMA reduce-scatter; needs arx.yaml with VLLM_ARXBIG_RS=1 and megamoe.yaml with VLLM_MOE_PREFILL=1 |
VLLM_GLM_SP_MOE_QUANT_GATHER |
1 | with VLLM_GLM_SP_MOE_FUSED: route each rank's own rows and gather the MoE input as NVFP4 + FP8 instead of bf16 (same results) |
VLLM_GLM_ARX_PREFETCH |
1 | L2 prefetch during decode all-reduces; needs arx.yaml |
VLLM_MEGAMOE |
1 | decode MoE kernel for batches of up to VLLM_MEGAMOE_MAX_TOKENS (8) |
VLLM_MOE_PREFILL |
1 | prefill MoE kernel for batches of VLLM_MOE_PREFILL_MIN_TOKENS (1024) or more |
VLLM_MOE_PREFILL_Y8 |
1 | FP8 per-expert rows in the fused prefill MoE |
VLLM_TRITON_SPARSE_MLA |
1 | Triton sparse MLA instead of FlashInfer's (fixes.yaml) |
MAX_NUM_SEQS |
50 | requests decoding at once (fixes.yaml); about 50 KDA states fit in the pinned KV pool, against the stock 32 |
VLLM_GLM5NEXT_RECOVERSSM |
1 | with recoverssm.yaml: KDA drafts verified from one saved state per request, bit-exact; raises MAX_NUM_SEQS to 64 at TP=4 and 16 at TP=2 |
VLLM_GLM5NEXT_DRAFT_POOL |
1 | the drafter's KV in its own small pool instead of a page in every KV block: ~40% more KV tokens (fixes.yaml) |
VLLM_DENSE_W4 |
in_proj, o_proj, shared experts, drafter | regex of dense layers stored as NVFP4 (the rest are FP8); add \|lm_head$ for the opt-in NVFP4 lm_head |
VLLM_DENSE_FP8_LM_HEAD |
1 | FP8 lm_head |
VLLM_ADAPTIVE_K_ONLINE |
1 | adaptive-k learns the step cost while serving; 0 uses the fixed table below |
VLLM_ADAPTIVE_K_MODEL |
fitted on this stack | starting step cost: ms, ms per expert touched, ms per verified token |
VLLM_ADAPTIVE_K_COST_MS |
measured on this stack | with VLLM_ADAPTIVE_K_ONLINE=0: step cost per verify length |
Debug switches: VLLM_MOE_PREFILL_CHECK=N and VLLM_GLM_SP_MOE_CHECK=N also run the stock path on the first N prefill batches and log the difference.
A snapshot holds processed weights. Its key covers the checkpoint, the TP layout, the MoE backend and max_num_batched_tokens. Two kinds of change need more:
weight_snapshot.py, dense_fp8.py, the loader patches): bump SNAPSHOT_VERSION in weight_snapshot.py. Snapshot names start with v<version>-, and each boot deletes the snapshots of every other version from its snapshot directory, for all TP sizes.VLLM_DENSE_W4, VLLM_DENSE_FP8*): a new VLLM_WEIGHT_SNAPSHOT_TAG (set in fp8.yaml). Snapshots under the old tag stay on disk until the next version bump.A shape change fails the restore with an error. A same-shape change (such as which layers are NVFP4) would restore stale weights without one.
experimental/quality/quality.py (GSM8K and HumanEval against a running server; see its docstring for the data files and the no-network HumanEval run).
experimental/quality/prefill_block.py measures what a long prefill does to other requests: gaps in a stream that is already generating, and time to first token for short requests arriving meanwhile.
experimental/quality/agent_tools.py runs five small agent tasks (list, read, search and write files in an in-memory tree) streamed and not streamed, and checks the result; python3 agent_tools.py http://<head>:8002.
Single stream, thinking off, 512 tokens (dev/repro/decode.py prompts):
| structured | code | prose | |
|---|---|---|---|
| stock, DFlash2 k=7 | 121.9 | 91.3 | 38.7 |
| all overrides | 170.3 | 120.8 | 65.8 |
Concurrent streams, aggregate tok/s at 1/2/4/8 streams: stock 85.5/65.4/99.5/147.8, all overrides 126/102/146/198 (16 streams: 271). With a different prompt per stream, code runs 317 tok/s at 16 streams and 451 at 32, and 50 mixed streams run 404. RigMark (reasoning=low) code / prose / structured 107.9 / 61.7 / 157.1 tok/s. Needle retrieval 12/12 up to 507k tokens. KV cache 3.63M tokens per rank at the 26 GiB pin and 50 sequences (2.63M before the drafter got its own pool).
Cold prefill, tok/s (random-word prompts, nothing cached):
| 32k | 128k | |
|---|---|---|
| stock | 2,730 | 2,679 |
| all overrides | 4,946 | 4,750 |
Boot goes from about 8 minutes to about 3.5 once snapshots exist.
arx/): all-reduce over RDMA for tensors up to 256 KB, without
NCCL. It takes 13-27 us where NCCL takes 80-90. The GPU publishes a
sequence number, and a CPU thread posts the RDMA writes over both ConnectX
roots.snapshot/weight_snapshot.py, base_loader.py): the first
boot writes each rank's processed weights to disk. Later boots build the
model on dummy weights through the normal path, then overwrite every tensor
in place (about 15 s for 46 GiB). modelopt.py and
flashinfer_cutlass_moe.py are left from an earlier design and may no
longer be needed.snapshot/dense_fp8.py): the NVFP4 checkpoint leaves about 4 GB
per rank of dense linears in bf16, and decode reads them every step. This
converts them to FP8 after load (per-channel weight scales, per-token
activation scales, CUTLASS scaled_mm). Prefill-sized batches go through in
2048-row pieces when the weight is over 16 MiB: CUTLASS rereads the whole
weight for every row of tiles, and past L2 that comes from DRAM, so one call
on a 16k x 6416 x 4096 GEMM runs at 68 TFLOPS and the pieces at 169. +14-20% decode, and about +0.5% NLL
on prose. Layers matching VLLM_DENSE_W4 (by default the KDA in_proj and
every drafter layer, attention o_proj and the shared experts) go to NVFP4 instead, through megamoe/megadense4.cu
(W4A16 at the 4-bit roofline, about twice as fast as the FP8 GEMM). They
keep an FP8 copy for batches of more than 32 tokens. That is +6-10% decode.
The in_proj costs about as much NLL again as FP8 did. The drafter layers only
change acceptance. NVFP4 on every dense layer cost 2-3x more NLL, so it is
not the default.megamoe/): an NVFP4 MoE kernel for batches of up to 8 tokens.
It reads the CUTLASS backend's own tensors, so CUTLASS still handles
prefill. Activations stay 16-bit, which is exact in the weights, where
CUTLASS rounds them to FP4. With VLLM_MOE_PREFILL=1 (the default here),
batches of 1024+ tokens take megamoe/moe_prefill.cu: the same W4A4 math as
CUTLASS on the same tensors, with the token gather, SwiGLU and FP4
requantization folded into the fc1 GEMM (hand-written block-scaled
mma.sync NVFP4). About 18 ms per layer at 16k tokens against CUTLASS's
22-26, equally close to an fp32 reference. +7-8% prefill.adaptive-k/adaptive_k.py): a scheduler that picks how many
of the 7 DFlash2 drafts each step verifies (2, 3, 4, 5 or 7). It uses recent
per-position acceptance, per request and averaged over all requests, and a
step cost it learns from measured step times: a fixed part, the MoE experts
the step's tokens touch, and a per-token part. Weight reads follow the
experts touched, so a 128-token step costs far less than a straight line
through small ones, and at 16+ streams the scheduler verifies more drafts
than a fixed table would allow. The drafter uses Triton attention, which
avoids a mid-step host sync.
The scheduler picks the step's draft count before the drafts exist. Once
they do, draft-trunc/ cuts each request's drafts where their predicted
survival falls below VLLM_DRAFT_TRUNC_TAU (0.3). The prediction is vLLM's
acceptance estimator, which scores each draft from the logit of its most
likely token (AUC 0.91-0.93 per position on logged steps). Dead drafts keep
the verify shape, route to expert -1 (the MoE kernels skip them) and are
rejected, so greedy output is unchanged, token for token and logprob for
logprob. A cut step grades only the drafts the estimator expected to
survive, and fitting on those steps drifted it low over a long mixed
workload. So every 4th step (VLLM_DRAFT_TRUNC_EXPLORE) skips the cut, and
only those steps train the estimator. On TP=4, paired with
gate/bench/bench.py after a long mixed warm-up and net of drift: code
+3.3%, prose +3.2%, mixed streams +3.5 to 6.9%, structured -1.1%.
VLLM_DRAFT_TRUNC_TAU=0 turns it off. draft-trunc/ holds whole copies of
five vLLM files, so it checks the image's vLLM commit and stops the engine
on any other.fixes/recoverssm.py, compose/recoverssm.yaml;
VLLM_GLM5NEXT_RECOVERSSM=0 turns it off): the KDA layers keep one recurrent state
per request instead of one per draft position, and after sampling replay
the accepted tokens through the stock kernel, so outputs are bit-identical.
Ported from vLLM's Kimi-K3 RecoverSSM. At TP=2, 16 requests run at once
instead of 3, and 8 streams decode 44% faster. At TP=4, 61 run at 64
streams instead of 49 (+28% aggregate).fixes/): things found by profiling.
gb10_sparse_mla.py is a Triton kernel for GLM's NoPE MLA (one
program per query, all 16 heads, 32 gathered keys per block) that runs
3.3x faster. VLLM_TRITON_SPARSE_MLA=0 goes back to FlashInfer.
At decode sizes (a request's 1 + 7 draft tokens) one program per query
left most of the 48 SMs idle, and FlashInfer decoded 3-6% faster; below
96 query tokens the kernel now splits each token's rows across programs
and combines them (4.4x faster at 8 tokens), which recovers most of that
without FlashInfer's per-step host sync and keeps Triton's prefill.causal_conv1d.py gains an out_group argument that
writes each group of features as its own dense tensor, and kda.py uses
it. The outputs are bit-identical; prefill +2-3%.kv_cache_utils.py, kv_cache_coordinator.py, kv_cache_interface.py,
worker_utils.py and warmup.py give the drafter's group its own pool
(sized for max_num_seqs windows plus one prefill chunk, 105 blocks,
0.58 GiB) and keep its block ids out of the target's zeroing and warmup.
At the same 26 GiB pin: 1,416 -> 1,976 blocks, 2.63M -> 3.67M KV tokens
at 32 sequences (3.63M at the 50 fixes.yaml sets).
Draft acceptance and speed are unchanged. VLLM_GLM5NEXT_DRAFT_POOL=0
goes back; VLLM_GLM5NEXT_DRAFT_POOL_BLOCKS sets the pool's size.fixes/model.py): sequence parallelism for prefill. A forward of
at least 1024 tokens keeps the residual stream split across the TP ranks, so
mHC and the norms run on a quarter of the tokens, with an all-gather before
and a reduce-scatter after attention and the MLP. +17% prefill. Decode stays
plain TP: applied to every batch, SP cost decode 10-15%. The KDA layers'
attention inputs are gathered as per-token FP8, half the bytes, since
in_proj's FP8 GEMM would quantize the same rows the same way (+2%).arx/arxbig.cu, VLLM_ARXBIG, VLLM_ARXBIG_RS): all-gather and
reduce-scatter over RoCE for prefill-sized SP collectives, from pinned
buffers the ConnectX writes directly. The all-gather is ~10% faster than
NCCL's (187 vs 165 Gb/s), but its result lands in pinned memory, and on
GB10 a GEMM that reads its operand from pinned memory runs 3.7x slower, so
prefill gathers stay on NCCL (VLLM_ARXBIG_AG=0). The point is the
reduce-scatter. With VLLM_GLM_SP_MOE_FUSED, MoE layers under SP run the
router, the shared expert and moe_prefill's fc1/fc2 in model.py, and one
kernel writes shared + scaled routed sum (fp32, rounded once) straight into
the send buffer, publishing rows as it goes, so the network runs under the
finalize. It replaces the runner's scale and add passes and NCCL's
reduce-scatter: ~12.5 -> ~8.4 ms per MoE layer at 16k tokens, prefill +2.5-3%.
With VLLM_MOE_PREFILL_Y8 (default) fc2 writes its per-expert rows as e4m3
with a scale per 128 columns, halving the 1.07 GB each layer writes and reads
back: prefill +5%, GSM8K/HumanEval/NLL/tool-call results unchanged.
With VLLM_GLM_SP_MOE_QUANT_GATHER (default) each rank routes its own rows
and quantizes them the way the experts would (NVFP4 for the routed experts,
per-token FP8 for the shared expert's GEMM), and the gather moves those
instead of bf16: 0.56 + 1 bytes per value instead of 2, and a quarter of the
fp32 router GEMM per rank. The layer output matches the bf16 gather to
within 1e-4 relative; prefill +3-4%.
All ranks write to all peers at once and this fabric has no PFC, so
incast drops packets and go-back-N retransmits make individual calls vary
(4-9 ms); per-destination serialization was worse (two QPs cannot fill a
link).TP=RING4; image/fabric_ring.py, ring mode in arx/): TP=4 on
four boxes cabled in a loop. A ConnectX-7 can't forward RoCE for another
box, because RoCE addressed to its MAC goes to its own RDMA engine, so every
collective uses only neighbour links. mentat 0.17's ring claim places rank i
on member i in cable order and gives each rank its interface toward each
neighbour. fabric_ring.py runs at each rank's Python start and turns that
into NCCL's ring algorithm with a channel graph that receives on the port
toward the previous rank and sends on the port toward the next, and into
arx's devices. The graph carries the speeds NCCL computes for its own ring:
with others it picked other protocols, the reductions summed in another
order, and a greedy text diverged. arx and arxbig relay data for the
opposite box through a neighbour, raw, so every rank sums the same values
in rank order and results stay bit-identical. Each cable needs its own
subnet for each PCIe root. On a switch, with both neighbours on the one
port, arx/test_ring.py passed bit for bit, decode was ~1% slower from the
relay and prefill 3-4% slower (2026-09-28). Not yet measured on cables.
Three boxes, one cable per pair, also form a ring, and there both other
ranks are neighbours, so arx sends every partial direct and relays nothing.
arx's ring mode takes 3 ranks; arxbig's does not, because its ring
reduce-scatter splits the rank two away across both neighbours, so a ring
of 3 runs with VLLM_ARXBIG=0. The entrypoint has no three-box ring mode
(only TP=RING4), so the NCCL ring and ARX_RING_*_HCAS are set by hand.
fabric_ring.py lists each port's devices in PCI order, root 0 first,
which fits boxes that are all cabled the same way; on a box cabled
mirrored, root 0 of a port faces the neighbour's root 1, and its lists
must put root 1 first. On cables, arx/test_ring.py --mode ring at
WORLD_SIZE=3, cut down to the arx all-reduce checks, the all-reduce
burst and the latency bench, passed 85 checks per rank with
12.2 / 19.7 / 42.8 / 69.1 us at 8 / 64 / 256 / 512 KB
(#52).VLLM_GLM_ARX_PREFETCH): while a decode all-reduce waits
for its peers, its threads ask L2 for the weights the next kernels read (the
router gate and shared expert after attention, the next in_proj after the
MoE). About -0.6 ms per 32 ms step.dense_fp8.simulate_nvfp4 is a diagnostic. It rounds the dense weights
through NVFP4 to measure the quality cost: about +1% NLL on prose.
Most of fixes/, and a few files elsewhere, are vLLM or FlashInfer files with
our changes, bind-mounted over the originals in the image. They keep their
Apache-2.0 headers. The rest is ours.
| Origin | Files |
|---|---|
| vLLM, changed | arx/cuda_communicator.py; fixes/attention.py, causal_conv1d.py, flashinfer_mla_sparse_sm90.py, gdn_attn.py, kda.py, kv_cache_coordinator.py, kv_cache_interface.py, kv_cache_utils.py, model.py, tilelang_kernels.py, warmup.py, worker_utils.py; snapshot/base_loader.py, flashinfer_cutlass_moe.py, modelopt.py |
| FlashInfer, changed | fixes/_fa_common.py |
| ours | arx/arx.py, arx_vllm.cu, arxbig.cu; adaptive-k/adaptive_k.py; fixes/gb10_sparse_mla.py, recoverssm.py (after vLLM's Kimi-K3 RecoverSSM); megamoe/; snapshot/weight_snapshot.py, dense_fp8.py; quality/ |
Other people's work in here:
tilelang_kernels.py carries vLLM's copy of SGLang's mHC kernel, and
causal_conv1d.py vLLM's adaptation of Tri Dao's causal-conv1d, both
credited in the file.gb10_sparse_mla.py is the Flash-Decoding scheme (Dao
et al., 2023): split each query's keys across programs, then merge the
partial softmax results.quality/quality.py (GSM8K first 250 and HumanEval, greedy, thinking off;
HumanEval programs run by quality/run_he.py in a no-network container),
against stock on the same boxes:
| stock | all overrides | |
|---|---|---|
| GSM8K (250) | 97.2% | 97.2% |
| HumanEval pass@1 | 156/164 | 156/164 |
| count to 200, thinking off, 5 runs | 0 corrupt | 0 corrupt |
| tool call at 42k context, 40 greedy runs | 10 diverge | 0 diverge |
| NLL vs bf16 dense layers | within ±0.006 | prose +0.008 to +0.025, code within ±0.007 |