docs(examples): GLM-5.2-FP8 PD on gfx942 — the docker recipe and its Kubernetes translation - #96
Conversation
A `docker` + shell deployment of GLM-5.2-FP8 on 2 x MI300X was brought up and benchmarked, but nothing carried it into Kubernetes. Anyone repeating it had to re-derive the manifest from four launch scripts, and the parts that are easiest to get wrong there are exactly the parts that fail quietly -- a KV transfer that falls back to TCP, an L3 tier that is written and never read, a hicache page silently rejected for being larger than its tablespace slot. This is that deployment as one InferaDeployment: router + prefill (with a kvd sidecar) + decode, TP8/DP8 with DP-attention, MTP, fp8 KV, Mooncake RDMA between the legs, and KV offload to host RAM and node-local NVMe. Every engine, router and kvd flag is the docker recipe's, unchanged. The README's section 6 lists each place the substrate forced something different and why, so a reader can check the claim rather than trust it. Three of those are worth calling out because they are not mechanical: kvd runs as a NATIVE sidecar (initContainers + restartPolicy: Always, hence k8s 1.29+). The engine probes the kvd socket once with a 5 s timeout and refuses to start if nothing answers -- it does not retry -- so "kvd first" has to be a scheduling guarantee, not a convention. An ordinary container makes it a race. The kvd sidecar gets 136Gi, not the 64G its --max-bytes suggests. kvd holds two independent budgets: --max-bytes caps the inline store, while the shared arena is sized separately, defaults to the same value, and is mmap'd and mlocked whole at startup. Sizing the limit to one of them gets the sidecar OOM-killed mid-run, which reads as a kvd bug. The RDMA rail and GID index are placeholders, not the validated cluster's mlx5_0 and 3. leg.sh takes both as required variables with no fallback, and the GID index is documented as per-node -- so there are two of those, one per leg. Hard-coding them would have been less faithful to the recipe, not more. This recipe also does not use the overlay, which every other one does. GLM-5.2 on the v0.5.16 gfx942 base needs a rebuilt Mooncake engine.so and four SGLang source patches; the payload carries patches/vllm/ only and its patch loop is gated on `import vllm`, so on an SGLang base it does not run at all. Section 1 covers what breaks without each patch and how to read the markers out of a built image. Not yet run in its Kubernetes form. The validation table says so per row rather than in a footnote, and the two docker-side numbers still being re-measured are marked TODO instead of quoted. Signed-off-by: liyingli <liyingli@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com>
… bug Rebasing onto main brought in f9a6a12, which teaches kvd's storage classifier to fall back to a major:minor walk through sysfs when lsblk cannot name a device. That is the exact failure the recipe cites as its reason for pinning --io-mode direct: an LVM mount seen from inside a container had no name lsblk could open, so the probe answered "unknown device" and the region took the conservative buffered branch. sysfs is indexed by numbers rather than names and the block layer is not namespaced, so auto reaches direct on its own now -- including from an unprivileged container, which is what the kvd sidecar is. The flag stays. It is the value the docker recipe ran and therefore the validated one, and a misclassification is a silent 4x on write-back, so pinning it is worth the line. Only the justification changes: it was a workaround, it is now a pin, and a reader who checks the classifier against the old wording would find they disagree. Left alone: the KVD_L3_DIR row's claim that a shared filesystem classifies as buffered. Nothing in that path resolves to a block device at all, so the sysfs fallback does not reach it. Also left alone is the "LVM-over-7-NVMe" figure -- it traces to the bench that produced the 3.70 / 14.56 GB/s pair on this same gfx942 mount, and the 8-NVMe rig in f9a6a12's message is where that fix was verified, not this node. Signed-off-by: liyingli <liyingli@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The Kubernetes recipe under examples/recipes/glm5.2-fp8-gfx942/ lifts every flag from a `docker` + shell deployment of GLM-5.2-FP8 on 2 x MI300X, and then had nothing to point at: the scripts that deployment was tuned with lived in a working directory. A reader could see the values but not how they were reached, and could not run the thing the manifest is a translation of. This is that deployment: env.sh plus five launch scripts for etcd, an optional kvd daemon, the two SGLang legs and the Infera router, then verify.sh and bench.sh. Every value in env.sh is the measured one rather than SGLang's default, and carries the measurement that chose it -- chunked prefill at 1,024/rank (-23.8% duration, -34.4% TTFT against 16,384/rank), MTP 5/1/6 (-8.5% duration at 4.64 acceptance on a 4.00 break-even), the rust router (same routing decisions, 27% faster end to end), one RDMA rail (striping measured 11.9% slower), dp-attention on both legs (pure TP8 prefill 25.9% slower). Together, -45% on the trace they were tuned against. Two things shaped the scripts more than the flags did. Every interesting failure in this stack is silent. A KV hand-off that drops the prefix returns fluent text about something else, not an HTTP error; kv-aware without a tokenizer routes on load and looks healthy; a disagg pair that disagrees on the MTP shape just stops speculating. So verify.sh asserts an arithmetic answer only reachable through an intact prefix, greps the router's own pick log for a non-zero block count, and reads spec_accept_length off the decode leg -- five checks, each aimed at one of those, exiting non-zero rather than printing. kvd ships here but defaults to KVD=0, which is the measured-best shape for this workload and not a hedge: the tier cost 12% and served zero reads, the 54 GB/rank device pool already answering ~100% of the reuse the trace had. README section 6 says when to turn it on and how to tell, and env.sh refuses a KVD value that is neither 0 nor 1 -- KVD=true would otherwise skip the daemon and produce a baseline run wearing the kvd name. bench.sh is deliberately the simple one: sglang.bench_serving on random prompts, which sizes the deployment and reproduces none of the figures above. The agentic multi-turn trace that produced them is being generalised into a standalone tool and is not part of this. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: liyingli <liyingli@amd.com>
…manifest The manifest landed with the docker recipe's values as they stood before its tuning sweep finished, and claimed they were identical. Two are not, and both are the sweep's largest levers. --chunked-prefill-size 131072 is 16,384 per rank under DP-attention, which splits the value dp_size ways. 8192 -- 1,024 per rank -- measured 23.8% lower duration and 34.4% lower TTFT on the trace both recipes are tuned against. The aggregate-vs-per-rank division is also why the number looks alarming either way, so the README now shows how to read back what actually took effect from /get_server_info rather than trusting the engine's "adjusted from ... to ..." line. EAGLE 3/1/4 -> 5/1/6, worth 8.5% duration and 13.8% TPOT. Each extra draft step costs 6.53 ms here, so the shape pays only while acceptance outruns it: 5 steps accept 4.64 against a 4.00 break-even, and 7/1/8 both misses its break-even and runs prefill out of activation memory. Applied to both legs -- SGLang rejects a disaggregated pair whose speculative configs differ. Section 5's kvd A/B no longer says the measurement is pending: KVD=1 ran 12.0% slower with gets_total = 0, 100.8 GB written to L3 and not one page read back, because the 54 GB/rank device pool had already taken ~100% of the reuse that trace offered. That is a statement about the workload, and the section now says so along with what to check before paying for the tier. Section 6 also stops claiming every router flag is identical; the manifest runs --router-backend python because the Rust binary supports only etcd discovery while the operator's is kubernetes, which is a substrate translation like the rest and now sits with them. The intro links the docker recipe it is a translation of, now that examples/glm5.2_gfx942/ gives it somewhere to point. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: liyingli <liyingli@amd.com>
There was a problem hiding this comment.
🟡 Changes recommended
There are a couple of concrete doc/config consistency issues (validation-status wording and a risky silent default in env.sh) that should be corrected to avoid misleading or misconfiguring users.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds a fully-specified GLM-5.2-FP8 (gfx942 / MI300X-class) prefill/decode-disaggregated deployment recipe to examples/, including both the validated Docker bring-up scripts and a Kubernetes manifest translation intended to preserve the same engine/router/kvd settings.
Changes:
- Added a new Kubernetes recipe (
InferaDeployment) for GLM-5.2-FP8 gfx942 disaggregated PD withinfera-kvdsidecar and RDMA KV transfer. - Added a new Docker + shell bring-up package (env + launch + verify + bench + stop) for the same topology and tuned settings.
- Updated the recipes index/readme to reference the new recipe and clarify “combo shape” availability for pinned/validated configs.
File summaries
| File | Description |
|---|---|
| examples/recipes/README.md | Adds the GLM-5.2-FP8 (gfx942) recipe entry and clarifies combo-shape expectations. |
| examples/recipes/glm5.2-fp8-gfx942/README.md | New Kubernetes recipe documentation, prerequisites, and docker→k8s translation notes. |
| examples/recipes/glm5.2-fp8-gfx942/disaggregated-kvd/deploy.yaml | New InferaDeployment manifest for router + prefill(+kvd sidecar) + decode on separate nodes. |
| examples/glm5.2_gfx942/README.md | New end-to-end Docker bring-up documentation for the gfx942 PD deployment. |
| examples/glm5.2_gfx942/env.sh | New shared configuration for all scripts (tuned engine/router/kvd settings). |
| examples/glm5.2_gfx942/host_container.sh | New host-side helper to create/manage the long-lived engine container with correct mounts/ulimits. |
| examples/glm5.2_gfx942/build_image.sh | New host-side helper to build the required patched engine image. |
| examples/glm5.2_gfx942/preflight_rdma.sh | New host-side RDMA visibility + optional cross-node fabric preflight. |
| examples/glm5.2_gfx942/launch/launch_etcd.sh | New host-side launcher for etcd used by the Docker PD deployment. |
| examples/glm5.2_gfx942/launch/launch_kvd.sh | New in-container launcher for optional infera-kvd daemon. |
| examples/glm5.2_gfx942/launch/launch_prefill.sh | New in-container launcher for the SGLang prefill leg (DP-attention, MTP, RDMA). |
| examples/glm5.2_gfx942/launch/launch_decode.sh | New in-container launcher for the SGLang decode leg matching prefill settings. |
| examples/glm5.2_gfx942/launch/launch_router.sh | New in-container launcher for the kv-aware router (rust/python backend selection). |
| examples/glm5.2_gfx942/verify.sh | New verification script to assert correctness + kv-aware steering + MTP + optional kvd writes. |
| examples/glm5.2_gfx942/bench.sh | New simple throughput sizing benchmark wrapper (random dataset) through the router. |
| examples/glm5.2_gfx942/stop.sh | New in-container shutdown script to stop engines/router and avoid VRAM relaunch races. |
| examples/glm5.2_gfx942/.gitignore | Ignores generated logs/ and results/ for the new example package. |
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| export PREFILL_IP="${PREFILL_IP:-$(getent ahostsv4 "$PREFILL_NODE" 2>/dev/null | awk 'NR==1{print $1}')}" | ||
| export DECODE_IP="${DECODE_IP:-$(getent ahostsv4 "$DECODE_NODE" 2>/dev/null | awk 'NR==1{print $1}')}" | ||
| : "${PREFILL_IP:=127.0.0.1}" | ||
| : "${DECODE_IP:=$PREFILL_IP}" |
|
|
||
| | What | Status | | ||
| |---|---| | ||
| | This configuration in its **`docker` form** | brought up and benchmarked on 2 × MI300X. **TODO — numbers withheld pending a re-run.** Request count, cache efficiency and the kvd counters are being re-measured on one consistent run; quote nothing here until they land | |
…g to loopback env.sh fell back to 127.0.0.1 when a node name did not resolve, and DECODE_IP fell back to PREFILL_IP on top of that. Both are the wrong shape of default for a deployment whose two halves find each other only through the addresses they register. The full fallback hides itself on the node you are watching: etcd runs on the prefill node, so 127.0.0.1:2379 resolves there, the prefill leg registers, the router comes up, and nothing on that node looks wrong. The decode leg is the one that fails, on the other machine, and not until 20 minutes in -- registration happens after the weights load and the graphs capture. Setting only PREFILL_IP is the likelier trap and the worse one. DECODE_IP silently becomes the prefill node's address, the decode leg advertises it, and both legs register: the worker count verify.sh checks first is correct, and only a real request finds that nothing is listening at the address decode published. So require_ips(), called by the six scripts that dial one of these addresses and by no other. A blanket check in env.sh would have been shorter and wrong: all twelve scripts source it, but build_image.sh and host_container.sh need no address at all, and the README has the image build (§1.3) before the cluster adaptation (§2) that sets them. Failing there would trade this footgun for one in the first documented step. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: liyingli <liyingli@amd.com>
…gates The row said the docker-form figures were being re-measured and that nothing should be quoted until they landed. They landed, and the section 5 rewrite earlier on this branch quotes them -- including the kvd counters the row names explicitly. A reader hitting both has no way to tell which one is current. State what is actually true instead: the figures come from the sweep, and the kvd counters from its single KVD=1 run. The other two rows are unchanged and still say "not run", because the Kubernetes form still has not been. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: liyingli <liyingli@amd.com>
kzjeef
left a comment
There was a problem hiding this comment.
please add one k8s config receipt too.
jiejingzhangamd
left a comment
There was a problem hiding this comment.
please add the k8s config too.
Description
GLM-5.2-FP8 was brought up, tuned and benchmarked on 2 × MI300X with SGLang
prefill/decode disaggregation, and nothing in the repo carried that deployment.
Anyone repeating it had to re-derive it, and the parts easiest to get wrong are
exactly the parts that fail quietly — a KV transfer that falls back to TCP, an L3
tier that is written and never read, kv-aware routing degraded to load balancing.
This lands the deployment twice: as the
docker+ shell scripts it was actuallyvalidated with, and as the Kubernetes manifest that translates them. Every engine
and kvd flag in the manifest is the docker recipe's; section 6 of the recipe README
enumerates each place the substrate forced a difference, so a reader can check the
claim rather than trust it.
The values are the substance of this PR. They are not SGLang defaults, and each one
carries the measurement that chose it, taken one axis at a time on an agentic
multi-turn trace (32 conversations / 225 turns, CONC=16, ~68k-token median input):
ROUTER_BACKENDrustpython, 27% faster end to endCHUNK8192(1,024/rank)5/1/6IB_DEVICEKVD0Together: 764 s → 420 s (−45%), output throughput 64.8 → 118.0 tok/s. One metric
moved the wrong way — ITL p90 rose 13.7%, because a deeper draft emits tokens in
burstier groups.
Type of change
Nothing outside
examples/changes.Changes
examples/recipes/glm5.2-fp8-gfx942/— new Kubernetes recipe. The deploymentas one
InferaDeployment: router + prefill (with a kvd sidecar) + decode, TP8/DP8with DP-attention, MTP, fp8 KV, Mooncake RDMA between the legs, KV offload to host
RAM and node-local NVMe. kvd runs as a native sidecar (
initContainers+restartPolicy: Always, hence k8s 1.29+): the engine probes the kvd socket oncewith a 5 s timeout and refuses to start if nothing answers, so "kvd first" has to
be a scheduling guarantee rather than a convention.
--io-mode directrationale, corrected. The kvd storageclassifier now falls back to a
major:minorwalk through sysfs whenlsblkcannotname a device, which is the exact failure the recipe cited as its reason for
pinning the flag. The flag stays — it is the validated value and a
misclassification is a silent 4× — but it is a pin now, not a workaround.
examples/glm5.2_gfx942/— new docker + shell recipe.env.shplus fivelaunch scripts (etcd, optional kvd daemon, both SGLang legs, the Infera router),
then
verify.shandbench.sh. Every failure this stack produces silently gets anassertion: an arithmetic answer only reachable through an intact prefix, a non-zero
block count in the router's own pick log,
spec_accept_lengthon the decode leg.kvd ships here but defaults to
KVD=0, which is the measured-best shape for thisworkload rather than a hedge;
env.shrefuses aKVDvalue that is neither 0 nor1, since
KVD=truewould otherwise skip the daemon and produce a baseline runwearing the kvd name.
recipe's pre-sweep values while claiming they were identical, and both are the
sweep's largest levers:
--chunked-prefill-size131072 → 8192 (16,384 → 1,024 perrank, since DP-attention splits the value
dp_sizeways) and EAGLE 3/1/4 → 5/1/6,on both legs, because SGLang rejects a disaggregated pair whose speculative configs
differ. Section 5's kvd A/B also stops saying the measurement is pending:
KVD=1ran 12.0% slower with
gets_total = 0— 100.8 GB written to L3 and not one pageread back — because the 54 GB/rank device pool had already taken ~100% of the reuse
that trace offered.
Out of scope, deliberately: the agentic multi-turn benchmark that produced the
figures above.
bench.shhere issglang.bench_servingon random prompts, whichsizes the deployment and reproduces none of them; the trace harness is being
generalised into a standalone tool for a later PR.
Validation
Run against the live 2 × MI300X deployment:
verify.sh— 5/5 checks pass (both legs registered, correct answer through the PDpair, 1561 blocks hashed on the largest router pick,
spec_accept_lengthpresenton the decode leg, Mooncake RDMA transport in the decode log).
bench.sh— completes and writesresults/<tag>.{json,log}; the ~0% cache-hitline is expected on a random dataset and is documented as such.
launch/launch_kvd.shon a scratch socketand directory with small quotas: readiness probe and
io_modereadout both behave.KVD=0is a clean no-op,KVD=trueis rejected with exit 1, andKVD=1without adaemon makes
launch_prefill.shexit before it touches the running engines.bash -n; trailing whitespace, end-of-file newlines and filesizes are clean against the repo's pre-commit hooks.
Checklist: