Skip to content

Add an opt-in DSA indexer query split to the HiCache W4AFP8 image - #300

Merged
pranavraja99 merged 1 commit into
mainfrom
pranavraja99/glm53-dsa-indexer-qsplit
Sep 24, 2026
Merged

pranavraja99 merged 1 commit into
mainfrom
pranavraja99/glm53-dsa-indexer-qsplit

Conversation

@pranavraja99

Copy link
Copy Markdown
Contributor

Summary

Adds a third source patch, dsa-indexer-qsplit.diff, to docker/sglang-glm53-hicache-w4afp8 and bumps the publish default tag to glm53-hicache-w4afp8-v2. No prod file changes.

What it fixes: the DSA K-pool indexer is a ReplicatedLinear, so every TP rank computes fp8_mqa_logits and the top-k for all rows of a prefill chunk.

How: with SGLANG_DSA_INDEXER_QSPLIT=1, each rank scores 1/TP of the rows, then the int32 top-k indices are all-gathered.

  • It applies only to chunks of at least SGLANG_DSA_INDEXER_QSPLIT_MIN_ROWS rows (default 1024).
  • It is off by default: unset, the image behaves byte-for-byte like v1.

Evidence (gpu31, 2026-09-23/24)

  • Cold prefill (chunk 16384): 1.04× / 1.08× / 1.12× / 1.16× faster at 128K / 256K / 384K / 512K.

  • Stored traffic (three rounds, both island orders): TTFT 0.81–0.85×.

  • Memory: the indexer score buffer shrinks by TP×, which makes chunk 16384 safe. Minimum free was 9.3–9.5 GB, vs 0.04 GB without the split (the setting that preceded the gpu02 crash).

  • Ship qualification of the full candidate (this image with the split on + pdi 2 + chunk 16384) vs GLM-5.3 Flash: W4AFP8 + HiCache long-context file for gpu02 (both replicas = campaign-2 L2) #294, same GPUs, stored traffic:

    Workload TTFT ratio TPOT ratio
    Burst, 8 × ~720K 0.76 0.72–0.76
    Burst + hammer 0.77–0.78 0.78–0.85
    long-a2 0.61–0.69 0.53–0.56

    0 restarts, 0 OOMs, 0 fatal log lines.

  • Quality:

    • GSM8K 97.41 vs 97.37; MMLU 87.66 vs 87.66; passkey to ~450K 12/12 on both.
    • Teacher-forced top-1 agreement vs control matches control-vs-control at 29K / 117K / 352K.
    • Logits are bit-identical. Differing selections are ties in the top-k, which the unsplit kernel flips too.

Write-up: Notion

Verification

  • The image built on gpu31 from exactly this branch's recipe (BUILD_SOURCE_REVISION=99bed39): "Verified and applied 3 patches", sha256sum --check OK.
  • The workflow's EXPECTED_* hashes match the recipe's Dockerfile, PROVENANCE and SHA256SUMS.
  • test-cpu.sh: 4/4 steps pass on that image. The new step 4 covers opt-in, the min-rows reload, and the method being present.
  • Label: nearai.sglang.dsa_indexer_qsplit=v1-opt-in.

After merge

  1. Dispatch the publish workflow (variant=hicache-w4afp8, source_revision=<merge commit>).
  2. A follow-up PR puts the published digest, SGLANG_DSA_INDEXER_QSPLIT=1 and chunk 16384 on the r2 canary from Canary --prefill-decode-interval 2 on the W4AFP8 long-context r2 #299.
  3. That PR's validator must reject chunk 16384 unless SGLANG_DSA_INDEXER_QSPLIT=1 is also set.

Untested: the all-gather's cost under CC/PPCIe in a CVM. The r2 canary is where that shows.

🤖 Generated with Claude Code

GLM-5.3 Flash's DSA indexer is ReplicatedLinear, so every attention-TP
rank computes fp8_mqa_logits and the K-pool top-k for all prefill query
rows; at long context that is the only prefill cost that grows with
context. With SGLANG_DSA_INDEXER_QSPLIT=1 each rank scores a quarter of
the rows against the same gathered keys and the int32 top-k rows are
all-gathered. It also shrinks the per-rank logits scratch, which is what
lets 16384-token chunks fit on the long tier. Inert unless enabled.

Evidence (gpu31/gpu32, 2026-09-24, every A/B in both island orders):
stored long-tier traffic paired TTFT 0.81-0.85x with 100% completion; the
chunk-16384 burst keeps 9.5 GB free vs 0.65 GB without; GSM8K, passkey
retrieval and teacher-forced agreement are at the noise floor. The image
built from this recipe passes test-cpu.sh (new step 4) and reproduces the
cold-prefill speedup on GPU (+16.4% at 512K, chunk 16384).

The publish workflow pins move to the new recipe hashes and the default
tag to glm53-hicache-w4afp8-v2, so the v1 image #294 pins is untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

✅ OpenCodeReview: Review failed: 0 finding(s); 7 of 7 selected item(s) failed.

@lloydmak99 lloydmak99 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additive, opt-in change: adds a v2 HiCache W4AFP8 image with a DSA indexer query-split patch gated behind SGLANG_DSA_INDEXER_QSPLIT (default off), nothing in this PR enables it and no prod compose file is touched. The hash chain is fully self-consistent and the qsplit patch's out_rows=per padding checks out against upstream, so this is safe to merge.

Optional follow-ups (non-blocking):

  • .github/workflows/publish-glm53-admission-reserve.yaml:18 — the tag input help text still lists glm53-hicache-w4afp8-v1, but the case statement now defaults to -v2. Cosmetic UI string only; no effect on the computed default.
  • docker/sglang-glm53-hicache-w4afp8/dsa-indexer-qsplit.diff (module-level _QSPLIT_MIN_ROWS = int(os.environ.get(...))) — a non-integer SGLANG_DSA_INDEXER_QSPLIT_MIN_ROWS would ValueError at import; worth a guard if this env var is ever surfaced to operators. Default "1024" is safe and unset here.

Checks: sha256sum -c SHA256SUMS 8/8 OK and recomputed Dockerfile/PROVENANCE/SHA256SUMS match the workflow's EXPECTED_*; git apply --numstat on the patch clean with consistent hunks; repo validators (validate_glm53_prod_config.rb, validate_glm53_dcgm_metrics.rb, validate_public_metrics.py, validate_streaming_keepalive.py, prepare_glm53_w4afp8_base.py --check + tests) all OK; CI green on head; patch semantics cross-checked against upstream sglang. Not run: in-container image build / test-cpu.sh (needs the private base image and a TP>1 GPU host) — covered by build-time hash enforcement and the author's gpu31 4/4 build at head.

@pranavraja99
pranavraja99 merged commit 556482c into main Sep 24, 2026
3 checks passed
@pranavraja99
pranavraja99 deleted the pranavraja99/glm53-dsa-indexer-qsplit branch September 24, 2026 23:31
pranavraja99 added a commit that referenced this pull request Sep 25, 2026
…302)

* Canary the DSA indexer split and chunk 16384 on the long-context r2

Pins r2 to the v2 engine image published from #300 (8ff1a487b98a), sets
SGLANG_DSA_INDEXER_QSPLIT=1 and raises its prefill chunk to 16384. r1 is left
completely untouched -- same #294 image, chunk 8192, pdi 1, split unset -- so it
stays a live control and a targeted `compose up` recreates r2 alone.

Structurally r2 now needs its own image AND environment: a YAML merge key
replaces a list rather than extending it, so r2 cannot inherit the anchor's
environment and add one variable. The generator derives r2's block from the
anchor at generation time rather than duplicating it as a literal, so the two
cannot drift.

The chunk and the split are one change. At 16384 without the split a concurrent
long burst left 0.04-0.65 GB free per GPU, the condition that preceded the gpu02
crash; with it, 9.3-9.5 GB. The validator now rejects --chunked-prefill-size
16384 on any replica that does not also set SGLANG_DSA_INDEXER_QSPLIT=1, and
rejects the variable on any image that does not carry the patch. Both gates were
verified by mutation, including the case where the expected spec itself is
edited -- the first placement of the chunk gate sat in validate_w4afp8_base,
where it could never fire.

Lab evidence (gpu31, candidate vs #294): TTFT 0.61-0.78x, TPOT 0.53-0.85x,
minimum free device memory 3.7 -> 9.3 GB, quality at the noise floor.

The all-gather's cost under CC/PPCIe inside a CVM is still unmeasured; gpu31 is
bare metal with CC off, so this canary is the first measurement of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make the generated header describe the canary, and enforce marker counts

Review found the header still claimed "8192-token prefill chunks", "Both pin
fde25985...", "flags differ only in --dist-init-addr and r2's canary pdi 2" and
"(16384 fell to 0.04 GiB and is rejected)" -- all false once r2 moved to the v2
image at chunk 16384. That header is the rollback and incident reference for a
file operators are told not to hand-edit, so a stale one is worse than none: it
would have pointed an on-call at the wrong image during a rollback. It now
states plainly that the replicas are not identical and lists each one's image,
chunk, interval and provenance, plus why the chunk and the split are one change.

Also enforce the occurrence count in resolve_engine_image_labels instead of
unpacking it and ignoring it. find() silently takes the first hit, so if an
"instance:N" marker ever stopped being replica-unique the nearest-placeholder
binding could attach the wrong replica's image label. It now fails loudly,
matching the exact-count discipline replace_exact uses elsewhere in the script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
pranavraja99 added a commit that referenced this pull request Sep 25, 2026
…unk 8192 (#307)

* Isolation arm: keep the DSA indexer split, drop r2 back to chunk 8192

The 2026-09-25 gpu02 canary ran split + c16384 + pdi 2 and inverted the lab
result at the tail. Over a 4 h steady-state window (cold start excluded):

  TTFT p95   r1 47.3 s   r2 76.9 s   1.62x  WORSE
  TTFT p50   r1 3.654 s  r2 3.768 s  1.03
  ITL  p50   r1 18.08ms  r2 17.53ms  0.97   (decode win reproduced)
  ITL  p90   r1 78.6 ms  r2 68.8 ms  0.87   (decode win reproduced)

The lab predicted TTFT 0.61-0.78x. Not explained by traffic volume (681 vs 688
req/h), cache warmth (79.7% vs 81.6% hit) or queueing (r2's average queue depth
was LOWER, 1.23 vs 1.56). r2 did draw ~23% more uncached prefill tokens per
request, which it absorbed for +3% median TTFT but +62% at p95: worse only at
the tail, i.e. only on the largest prefills.

The chunk and the split both bite exactly there, and the canary changed both at
once. This arm holds the split and returns the chunk to 8192 so they can be told
apart: if p95 recovers the chunk was at fault, if it stays bad the all-gather
under CC/PPCIe is -- the risk #300 flagged as untested, and which gpu31 cannot
test with CC off.

The validator gate is one-directional by design (c16384 REQUIRES the split, not
the reverse), so this combination is permitted without weakening the check that
guards the 0.04-0.65 GB free crash condition.

Tests: the two c8192/c16384 asymmetry cases are vacuous now that both replicas
run 8192, replaced with drift cases off 8192. The pairing-gate test now
CONSTRUCTS the forbidden combination (raise chunk, drop split) and asserts the
specific pairing error, instead of passing because an unrelated equality check
fired first. 13 tests; the 2 failures under local Ruby 2.6 are the pre-existing
filter_map gap, identical on origin/main.

gpu02 was rolled back to v0.0.443 before this was written; r1 was never touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Put the DSA indexer split on both long-tier replicas, revert r2 to chunk 8192

Two changes, one cause.

r1 crashed on 2026-09-25 at 18:07:58. deep_gemm.fp8_mqa_logits
(dsa_indexer_kpool.py:919) asked for one fp32 buffer of new_tokens x
total_context -- 11.96 GiB at ~392K context with an 8192 chunk -- against
11.88 GiB free. That is an ordinary request for this tier, not a pathological
one. r1 lacked the indexer split, so all four ranks built the full buffer.

So both replicas move to the v2 image with SGLANG_DSA_INDEXER_QSPLIT=1, set on
the anchor rather than per-replica so there is exactly one definition. Each rank
then scores 1/TP of the rows and that allocation falls to roughly 3 GiB.

And r2 returns to chunk 8192. The 2026-09-25 canary ran the split at 16384 and
inverted the lab result at the tail: TTFT p95 76.9 s against r1's 47.3 s (1.62x)
over a 4 h steady-state window, though the decode win did reproduce (ITL p90
0.87x). Traffic volume (681 vs 688 req/h), cache warmth (79.7% vs 81.6%) and
queue depth (r2's was lower) did not explain it. gpu02 was already rolled back to
v0.0.443; this brings the repo back in line and keeps the crash headroom.

THE SPLIT IS A MITIGATION, NOT THE FIX. It divides the buffer by TP; it does not
bound it, so the same crash returns at ~4x the context or at TP1. The fix is
calling _should_chunk_mqa_logits -- defined but never called, at
dsa_indexer_kpool.py:862 -- and chunking the logits against free memory, which
dsa_indexer.py already does for the non-kpool path at :1165. That is a separate
change and not a copy-paste: the kpool path uses a different layout (pooled seq
lens, ragged q ks/ke, topk offsets) and needs correctness tests against the
unchunked path.

Also worth recording: r1 was not healthy when it died. 1.29M pending tokens, a
full queue, and a 20 MB mapping had just failed with 1.5 MB free. Chunking bounds
the spike but load contributed, so neither change should be read as making that
exact scenario survivable.

This leaves no unsplit control on the tier, so the split's cost under CC/PPCIe is
now measured against pre-change history rather than a live sibling. Deploy r2
first and watch p95 before moving r1.

Tests: the image-patch case previously swapped R2_IMAGE for IMAGE, which is a
no-op now that both are v2 and would have passed without exercising anything; it
names the v1 digest explicitly instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
henrypark133 added a commit that referenced this pull request Sep 25, 2026
Brings in #299, #300, #302 and #307. Conflicts in the W4AFP8 long-context
generator, validator and test are resolved by taking main's per-replica
logic (both replicas on the v2 QSPLIT image at chunk 8192, r1 pdi 1 / r2
pdi 2, r2's own environment list derived from the anchor) and re-applying
this branch's tracing additions on top: --enable-trace and the OTLP
endpoint on both replicas, SGLANG_TRACE_ASYNC=1 / SGLANG_TRACE_LEVEL=0 in
the anchor environment (and therefore in r2's derived list), the
-trace-async-armed variant suffix on both variants, and the
glm53-trace-control job. The compose file is regenerated, not hand-merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants