Repository navigation
feat(glm53): v3 HiCache W4AFP8 image that keeps the HTTP event loop free - #308
Conversation
The GLM-5.3 Flash engines answer /health, /metrics and chat requests from one uvicorn event loop, so any work done synchronously on it also stops /health, and the CVM proxy's 3 s probes miss. This adds three patches to docker/sglang-glm53-hicache-w4afp8: - sglang-pr30771.diff: upstream sgl-project/sglang PR #30771 (open), byte-identical to its diff at head 60a56aa. Chat template rendering and tokenization run in a single-worker request-preprocessor thread. - shm-off-loop.diff (new, applied after it): the dispatch-time /dev/shm copy of multimodal features, about 141 MiB per large image, runs on its own sglang-mm-shm thread. A request cancelled during the copy still has its segments unlinked once the copy finishes. - event-loop-stall-dump.diff (new, diagnostic, on by default): after 30 s without a loop tick, every thread's stack goes to stderr with the prefix [event-loop-stall]; SGLANG_EVENT_LOOP_STALL_DUMP_SECS=0 disables it. It targets the multi-minute gpu02 hangs, which have never left a stack. Evidence (gpu32 lab, 2026-09-25, same source bytes bind-mounted over v1, one request at a time, worst /health probe): - 4.7 MB text chat: 3,444 ms stock, 38 ms with PR #30771 alone. - With PR #30771 alone, each 64-image request still blocked the loop once for 3.7 to over 5 s, mostly in the shm copy (8.8-8.9 GiB per request). - All three patches, stall dump at 30 s: 30 / 163 / 551 / 1,258 ms for the text and the 64 x 9 / 48 / 169 MP requests, 0 of 3,211 probes over 3 s. The stock arm in the same half hour: 3,033 / 3,098 / 2,976 / 4,062 ms. prompt_tokens identical, no /dev/shm leak, no errors, no stall dumps. - Stall dump: 58/58 CPU checks. Live on a GPU engine at a 2 s threshold it named the shm copy on the loop thread and logged the recovery. apply-patches.py now checks and applies each patch in order, because shm-off-loop.diff edits lines that sglang-pr30771.diff adds and cannot be checked against the unpatched base. source-manifest.json gains http_server.py, serving_base.py, tokenizer_manager.py (base to final; PROVENANCE records the hash between the two patches), the new utils module and the PR's three test files. test-cpu.sh gains steps 5-7: the PR's own tests, a check of the shm move, and the stall-dump test, shipped as test_stall_dump.py. Run against its base 3eccc307 in a CPU-only container, the recipe passes sha256sum --check, apply-patches.py and test-cpu.sh steps 1-7. The publish workflow pins the new hashes and defaults to glm53-hicache-w4afp8-v3, so the v1 and v2 tags stay as they are (its tag input description, which still said v1, now says v3). No compose file changes here: r2 is pinned to v3 in a follow-up once the image is published.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
lloydmak99
left a comment
There was a problem hiding this comment.
Recipe-only change that adds the v3 HiCache W4AFP8 image files and bumps the publish workflow's default tag/hashes; it publishes and deploys nothing on its own (deployment is a separate pin PR), so merge risk to running replicas is nil. Mechanical integrity and patch logic check out.
Optional follow-up (non-blocking):
shm-off-loop.diff:54— on cancel-during-copy,discard_shm_featuresis submitted fire-and-forget and its Future is never retrieved, so a failing/dev/shmunlink would be silently swallowed and could leak a segment. Rare edge case matching the documented intent; a warning-log on the discard result would make any leak visible.
Checks: sha256sum --check --strict SHA256SUMS → 12/12 OK; recomputed Dockerfile/PROVENANCE/SHA256SUMS hashes match the workflow's updated EXPECTED_* values; sglang-pr30771.diff byte-identical to upstream gh pr diff 30771; PATCHES dict hashes match the .diff files; confirmed discard_shm_features/wrap_shm_features exist upstream and wrap is a no-op without mm_inputs; apply-patches.py/test_stall_dump.py compile, workflow YAML parses, bash -n test-cpu.sh OK; CI Validate Compose Files = success (the code-review "fail" was a concurrency-cancelled job, not a real failure). Full image build and apply-patches.py against the real multi-GB base were skipped locally — covered by the publish workflow's in-image test-cpu.sh.
Summary
Adds three patches to
docker/sglang-glm53-hicache-w4afp8and makespublish-glm53-admission-reserve.yamlbuild them asglm53-hicache-w4afp8-v3. The v1 and v2 tags and digests are left alone. No compose file changes here: pinning v3 on a replica is a follow-up PR once the image is published (plan and pre-validation below).v3 is v2 plus these three patches. The recipe directory is unchanged since v2 (
8ff1a487…) was built from it at 556482c (#300): same base3eccc307…, same loader, pool-clamp and DSA indexer split diffs, and no other build step. Since #307, both long-tier replicas run v2 at chunk 8192 withSGLANG_DSA_INDEXER_QSPLIT=1(r1 at--prefill-decode-interval1, r2 at 2). Moving one replica from v2 to v3 therefore changes only these patches, so the replica's before/after is a single-variable comparison.sglang-pr30771.diff60a56aa(re-checked today). Offloads request preprocessing: chat template rendering and tokenization in_convert_to_internal_request, and the regular-tokenizer path ofTokenizerManager._tokenize_texts, run in a single-worker request-preprocessor thread instead of on the uvicorn event loop. Ships the PR's three CPU tests.shm-off-loop.diffwrap_shm_features:posix_fallocate+copy_into/dev/shm, about 141 MiB per large image, 8.8-8.9 GiB for a 64-image request) to its ownsglang-mm-shmthread. A request cancelled during the copy still has its segments unlinked once the copy finishes.event-loop-stall-dump.diff[event-loop-stall], repeats every 60 s while the stall lasts, and logsevent loop recovered after N s. Output is stack frames only (file, line, function, source line) plus pid, stall age and loop-thread CPU: no local variables and no request content. faulthandler covers a loop stuck in one GIL-holding C call.SGLANG_EVENT_LOOP_STALL_DUMP_SECS(0 disables),SGLANG_EVENT_LOOP_STALL_DUMP_REPEAT_SECSWhy
The
sglang servemain process answers/health,/metricsand/v1/chat/completionsfrom one uvicorn event loop, so anything it does synchronously also delays/health. The CVM proxy marks a replica unhealthy after 3 consecutive missed probes (3 s timeout, 5 s interval). On 2026-09-25 the gpu02 long tier flapped this way under image-heavy multi-MB traffic from one customer's agent workload. Separately, the tier has had hangs of 14-155 min in which the engine stopped answering/healthand left no stack in any log.Evidence
Lab: gpu32 (bare metal, CC off, 8x H200), 2026-09-25. The same source bytes were bind-mounted over the v1 image
fde25985…; one request at a time;/healthprobed every 200 ms; the stock arm was measured in the same half hour. All request bodies are synthetic (seeded generators, no customer data). Internal report:~/Documents/cloud/reports/glm53-hang-lab-2026-09-25/(results/RESULTS-phase3a.md,results/RESULTS-phase3b.md); raw data on gpu32 under/data/glm53-dyntok-20260925.Worst
/healthprobe per request, stock vs all three patches (stall dump at its 30 s default):prompt_tokensidentical to stock on every request (653,502 / 604,876 / 610,730 / 610,172), all HTTP 200./dev/shmwas back to the same bytes and segment names after every request. NoTraceback, noAlready borrowed, and no stall dump in the engine log.ShmPointerMMData.__init__on the loop thread: 64 segments of about 141 MiB each. Withshm-off-loop.diff, py-spy shows the loop idle whilesglang-mm-shm_0does the copy.event loop recovered after 5.3s. Each dump was about 15 KB (11 threads). At the 30 s default it logged nothing through all the runs above.Not covered:
--enable-dynamic-batch-tokenizer; production does not (Encode long-tier prompts off the HTTP event loop (--enable-dynamic-batch-tokenizer) #305 proposes it). Without that flag, the tokenizer manager's second tokenization pass goes through PR #30771's_tokenize_textschange instead. The PR's unit test covers that path, but the canary is its first run under GPU load.Recipe mechanics
apply-patches.pynow checks and applies each patch inPATCHESorder.shm-off-loop.diffedits lines thatsglang-pr30771.diffadds, so it cannot begit apply --checked against the unpatched base. Before-hashes are still verified before any patch; after-hashes andast.parseare checked after all of them.source-manifest.jsongainshttp_server.py,serving_base.py,tokenizer_manager.py(base to final), the new stall-dump module and the PR's three test files ("before": null= new file).tokenizer_manager.pyis the only file two patches touch;PROVENANCErecords its hash between them (d32617ee…). The event-loop before-hashes were checked in the real base3eccc307…and equal fc91d24.test-cpu.shgains:/dev/shm(the workflow's test container keeps Docker's 64 MB default). It checks that the copy runs onsglang-mm-shm, that the loop keeps ticking, and that a cancel mid-copy leads to a discard;test_stall_dump.py(58 checks, shipped in the recipe).sglang-pr30771.diffa102977de7bb02776cee1053ff04979b740526fe99c59d2c63e2f3962df3c95d(=gh pr diff 30771)shm-off-loop.diff4bd4cc65d68deacd1b56b95e32a2242853d20bd3cfa4188e8c1a20777cc79a94event-loop-stall-dump.diff367dbcdec432586ae4c8ab984c08caafb2f148d62cebf257d12e89c4c32f72abtest_stall_dump.py59cf2daae55e46b4f597f004ca647e235bcea41de4d6485dc386c98b5e088abbPublish workflow pins (
hicache-w4afp8case): Dockerfilef5136afd…, PROVENANCE720fd00c…, SHA256SUMS2f74f65b…,DEFAULT_TAG=glm53-hicache-w4afp8-v3.Checks run (after rebasing onto #307)
nearaidev/sglang@sha256:3eccc307…(pulled for the check, then removed). This reproduces the Dockerfile'sRUNstep and then runs the workflow's test step with the same flags (--runtime=runc --network=none --memory=16g):sha256sum --check --strict SHA256SUMS: 12/12 OK;apply-patches.py:Verified and applied 6 patches across 11 source files;test-cpu.shsteps 1-7 all OK (step 55 passed, step 7SUMMARY: 58 passed, 0 failed); 94 s in total.git archiveof the commit:PATCHESandPROVENANCEhashes match the diffs;COPYlist equals the directory, andSHA256SUMScovers every file exceptDockerfileand itself.validate-compose.yaml, run locally in CI's own images, all pass:validate_public_metrics.pyandvalidate_streaming_keepalive.py(nginx -tin the pinned images);--check, plus their unit tests (HiCache 34, W4AFP8 base 10, W4AFP8 canary 11, W4AFP8 long-context 13);docker compose configon all 36 compose files with the CI dummy env;test-cpu.shin it, then scans, attests and signs it.Rollout
publish-glm53-admission-reserve.yamlwithvariant=hicache-w4afp8,source_revision=<merge sha>and an empty tag (defaults toglm53-hicache-w4afp8-v3). Record the run id and digest.docs/glm53-w4afp8-long-context-rollout.md:compose/down, thencompose/up, withservices: ["model-sg-glm53-w4afp8-tp4-r2"];dry_runfirst; it must plan exactly that service;event-loop stall dump armed: stall=30s repeat=60s (pid N, loop thread 0x…), and no[event-loop-stall]lines while idle.Backend marked unhealthy/Backend recoveredtransitions for r2.prompt_tokensagainst r1, and anyAlready borrowedor tokenizer error.[event-loop-stall]block: keep it with the matching proxy transitions. It is the stack the hang investigation has been missing.docker/sglang-glm53-w4afp8(basee9d29a1c…, no HiCache). Carrying the fix there means porting the three diffs into that recipe in its own PR, after checking the before-hashes in that base.Rollback
["model-sg-glm53-w4afp8-tp4-r2"](compose/down, thencompose/up). That restores v2 with the same chunk, pdi and split in one step, and r1 keeps serving.SGLANG_EVENT_LOOP_STALL_DUMP_SECS=0on the replica. In this repo that is a generator and validator change plus a recreate, so the full rollback above is quicker.Risks
/healthstays fast.shm-off-loop.diffadds an await betweenprepare_for_dispatch_asyncand the dispatch, so a text request can now dispatch ahead of an image request whose copy is still running. The lab saw no effect. Cancellation during the copy is handled (shield plus a FIFO discard).process_mm_data_asyncreturns just before the copy. It is the 163 / 551 / 1,258 ms above, on the same line as the stock arm. It stays under the 3 s probe timeout but grows with source image size.Follow-up PR: pin v3 on r2 (after publish)
Dry-run today on a scratch copy of this branch with a placeholder digest. After the edits below and
--write, all of the checks above pass. The regeneratedprod/GLM-5.3-Flash-SGL-TP4-W4AFP8-LongContext.yamlchanges only r2'simage, itsengine_imageandconfig_varianttelemetry, and the header. r1 is untouched.scripts/prepare_glm53_w4afp8_long_context.py:R2_IMAGEbecomes the v3 digest andR2_ENGINE_IMAGE_LABELits first 12 hex;VARIANTS[2]gainsoffloop-afterqsplit-, so dashboards split r2 byconfig_variant;scripts/validate_glm53_prod_config.rb:W4AFP8_LONG_CONTEXT_R2_IMAGEbecomes the v3 digest;W4AFP8_LONG_CONTEXT_IMAGE(v2); since Put the DSA indexer split on both long-tier replicas, revert r2 to chunk 8192 #307 both entries point at the r2 constant;SGLANG_DSA_INDEXER_QSPLIT=1requires the patched image) must accept both v2 and v3, since both carry the split. Today it requires exactly the r2 image, which would reject r1;OFFLOOPplaceholder ("1"on r2,nilon r1).docs/glm53-w4afp8-long-context-rollout.mdneeds a canary section. It still describes the pre-Put the DSA indexer split on both long-tier replicas, revert r2 to chunk 8192 #307 layout (r2 at chunk 16384, r1 onfde25985).