Repository navigation
[Perf] [MI355X] glm53: Score a request's rows together and select the exact top-2048 on up to 64 slices (preview) - #18
Closed
heslami wants to merge 4 commits into
Conversation
added 4 commits
October 28, 2026 02:48
… index caches test/kernels/glm53/check_topk.py runs indexer, dense or layer-0 launches at chosen positions, on random, tied, zero and recency-heavy index caches, with the cached scales of each step's own keys poisoned first. For every live row it checks the launch's scores against an f64 reference and its top-k list, entry by entry, against the radix select's list of the launch's own scores; negative controls (a wrong slot, two entries swapped, a flipped score bit) must each fail. --dump writes digests of every row's scores and list for check_layer.py --compare. test_glm53_kernels.py runs it at TP4 and TP8. check_layer.py's steps give each row its own req_ids entry, as the spec does (each row has its request's page-table row); before, a second request's rows read the first request's row.
…s first The cache task normalized, roped and stored each row's index key after its KV row; it is now an index_key task of its own, on the row's q_norm CTA after the CTA's index-query tile. The q_norm CTAs run their own index-query tile right after q_norm, and a q_b CTA issues its index-query tile's weights with q_b's, before the wait, and runs that tile before its q_b tile (the index chain waits on it). Same values in the same order. The fused indexer for an unpaged index cache goes: every indexer build reads SGLang's paged index cache.
Each indexer row re-read every key: a CTA scored tiles of 64 keys of one row. Now up to four consecutive rows of one request score its keys in one pass. The request's pages are split into runs, one a CTA; a CTA copies its run into LDS eight pages at a time, the next batch's loads in flight, and each row's waves score the batch with the row's query in registers. A key this launch writes is patched into the batch from its row's index_k8 pairs (its FP8 values and scale as cached), for every row of the request in the launch. Each row's index query is rotated and Hadamard-transformed once, by index_qh tasks (8 heads a task), instead of on every scoring CTA. Every score is computed as before.
A row's selection ran on one CTA, or past 32768 keys on up to 16 slices whose histograms one CTA added. Now every row selects on up to 64 slices (a slice per 1,024 keys), each a run of its keys in position order, by a radix select of 6-bit digits from the top: per digit every slice publishes the histogram of its keys under the prefix found so far and reads every slice's. Once the prefix has 12 or 24 bits and its keys fit the candidate room, or has all 32, each slice places its keys above it and stages its keys under it, with their cache slots, for the row's first slice, which resolves them in two 10-bit passes. The lists are the one-CTA radix select's, in the same order. Short rows' slices interleave on one range of CTAs. layout.select_slices gives a row's slices; the kernel and the CPU test call it.
Collaborator
Author
|
Sent upstream as sgl-project#74. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Stacked on sgl-project#68, sgl-project#69 and the coverage PR (
[Feat] [MI355X] glm53: Cover layers 0-2 and the logits). Merge those first. This PR's diff is its last four commits, and it uses sgl-project#68'stest/kernels/glm53/tools.On an indexer launch the attention waits for the index chain: index query, key scoring, then the exact top-2048. That covers 18 of the 75 MoE layer launches of a step, plus layer 0 and the dense layers 1–2, which reuse the indexer stages. Two parts of the chain grow with context:
This PR scores a request's rows together, reading each key once from LDS batches of whole pages. It then selects every row's top-2048 on up to 64 slices with all-gathered 6-bit digit histograms. The selection stays exact: the same keys, in the same order and with the same ties, as the base's radix select. Every launch's outputs are bitwise equal to the base's (Accuracy Tests).
Result on MI355X, base (the coverage PR) → this PR:
time_layers.py, E4M3 MLA cache, TP8, µs a launch at 4 rows:The gain grows with context because both rewritten stages scale with the keys a row reads.
Modifications
The first commit adds a check. The next three are the changes, each measured on top of the one before (Speed Tests, "Each change on its own").
0. An exact top-k check (
test/kernels/glm53/check_topk.py,check_layer.py,test_glm53_kernels.py)check_topk.pyruns indexer launches under torchrun at TP4 or TP8, at chosen positions (--pos).--kind denseand--kind embedrun layer 0 and the dense layers.check_layer.py's random keys;ties);zeros);recent).serving, astime_layers.py);--posand below, some under 2,048 keys (mixed);chain).check.poison_new_index_scales). A launch that scores a key this launch writes from the cache therefore fails.check.launch_scores) against an f64 reference. The reference uses the launch's index query (itsindex_qmailbox, rotated and transformed in f32 as the kernel does, then bf16), its head weights, and the row's keys as the launch leaves the index cache. The error is taken over the row's largest reference score; the bound is 1e-4.indptr), entry by entry, against the radix select's list of the launch's own scores. That list is the keys above the 2048th key's 12-, 24- or 32-bit prefix in position order, then the prefix's keys above the 2048th, then its ties in position order.missedalso counts the keys of the exact top-2048 (score descending, then position) that the list lacks, ascheck.py'slayer.topk_exactdoes.--dump DIRwrites SHA-256 digests of every row's scores and list incheck_layer.py's format, socheck_layer.py --comparecan match two trees.test_glm53_kernels.pyruns it at TP4 (widths 2, 4) and TP8 (2, 4, 8):check_layer.py's steps gave each row its request'sreq_idsentry, while the page table has a row per step row. At 8 verify rows, the second request's rows read the first request's page-table row. Each row now gets its own entry, as the spec passes them (req_ids= row index,page_table= the DSA backend's row per step row).1. Each row's index key on its own task, and the index-query tiles first (
kernel.py,layout.py)index_keytask of its own, on the row's q_norm CTA.runner.pypassesindex_paged=key.with_indexer).index_keytask's pairs.2. A request's rows scored together, each key read once (
kernel.py,layout.py)index_keytask also publishes its row's key as cached, FP8 value words and scale, inindex_k8pairs. A batch holding any of those keys patches them in from the pairs, so the cache store need not be visible yet.check_topk.py'schainrowset at 8 rows checks it. With each group patching only its own rows, that check fails (Accuracy Tests).index_qhtasks rope and Hadamard-transform each row's index query once and publish it in bf16; the scoring CTAs poll it into LDS.index_readyflag and theindex_k_newpairs.3. Every row's exact top-2048 on up to 64 slices (
kernel.py,layout.py)layout.select_slicesgives the split, and the kernel and the CPU test both call it. Each slice is a run of the row's keys in position order, held as radix keys in LDS.sel_histandsel_cand. The indexer runtimes of widths 2, 4 and 8 together go from 163.4 to 135.2 MiB a rank at TP8, and from 76.5 to 64.4 MiB at TP4 (widths 2, 4).test/unit/test_glm53_select.pycoversselect_slices: the slices tile a row's keys once, in position order, none empty, at most 64, a slice's keys within its LDS room and its 16-bit counts. It also covers the stage order.test_glm53_server.py's parallel-top-k case asserts that its rows reach all 64 slices.Accuracy Tests
check_topk.py, E4M3 MLA cache, 2 indexer launches × 2 steps).chainset fails at 8K and 128K on every index cache. The launch scores the earlier rows' new keys from their poisoned cached scales, giving a score error of about 1e30.check_layer.py --dump,--compare).shared,index,model(the first 8 launches),denseandfull(layer 0, the dense layers 1–2 and the tail); bf16 and E4M3 MLA cache; every width the TP builds; one request a row, the same with a pad row, and MTP's verify rows; 3 steps.check_layer.pywith its runtimes built before the layers' weights, as [Perf] [MI355X] glm53: Shorter attention chain, one-pass 8-row front, cheaper routing hand-off (2/2) sgl-project/sgl-amd-mono#69's tool did. With the base's owncheck_layer.py, which since the coverage PR draws every layer's weights first, two TP8 width-2 configurations (E4M3 index with verify rows, bf16 model with a pad row) vary from run to run on the base itself, with the same kernel code objects. This is under investigation separately and is not from this PR.layer.topk_owncompares with SGLang's selection from FP8-q scores, while the kernel scores the bf16 query. At a threshold tie, the two can pick a different key, as on the base.SGLANG_AMD_MONO_CHECK=1:layer.topk_exactandlayer.topk_ownare 0 in every indexer check;layer.topk.relis at most 0.048 andlayer.index.cosineat least 0.99997.test/e2e/test_glm53_server.py, TP8, on this head):PYTHONPATHwith a pure-Python stand-in for its compilededitdistance, as for [Perf] [MI355X] glm53: Cheaper hand-offs and placement: the shared expert before the routing, uncached scratch, tagged selected tokens (1/2) sgl-project/sgl-amd-mono#68;mainthe same way, as in [Perf] [MI355X] glm53: Cheaper hand-offs and placement: the shared expert before the routing, uncached scratch, tagged selected tokens (1/2) sgl-project/sgl-amd-mono#68 and [Perf] [MI355X] glm53: Shorter attention chain, one-pass 8-row front, cheaper routing hand-off (2/2) sgl-project/sgl-amd-mono#69: no fault at address 0x0 reaches the log (the test'sassertTrue(faults)fails), and the server log shows SGLang's watchdog stopping it first.Speed Tests and Profiling
Setup: as sgl-project#68: one node with 8 × MI355X; image
lmsysorg/sglang-rocm:v0.5.22-rocm720-mi35x-20261025(squashfssha256:fb07e0ef78fe), AITER 285579b6, FlyDSL 0.4.0;zai-org/GLM-5.3atba32f8c1(FP8); the e2e test's server flags with--kv-cache-dtype fp8_e4m3.python/sglangat5044779f.--enable-amd-monokerneland the plugin's spec registration from before [Feat] [MI355X] dsv4: Add whole-layer DeepSeek-V4-Pro mono kernels (TP8) sgl-project/sgl-amd-mono#48.check.pyandspec.py.Serving: as sgl-project#68.
SGLANG_SIMULATE_ACC_LEN=2.99; measured 2.99 on every server). A cycle is a request's decode time over its verify steps.sglang.benchmark.servingdoes not import on this image, so a small streaming client against/generatemeasures TTFT, TPOT and the verify count frommeta_info.TP8, FP8 E4M3 KV, MTP 3-1-4 at AL 2.99, cycle in ms (median over requests):
The requests of a two-request wave fall into two groups. Each prompt's prefill is its own chunk, so one request decodes for part of its output beside the other's prefill. Both groups are given here; the median over all of a wave's requests depends on how many fall in each.
TP8, FP8 E4M3 KV, MTP off, 8K, median TPOT in ms:
Kernel-only (
time_layers.py)torchrun --nproc-per-node <TP> test/kernels/glm53/time_layers.py --kind <kind> --rows <rows> --kv fp8 --pos <8447 | 131071>, µs a launch, base → this PR. Each number is the mean of two passes in alternating order in one allocation; each pass is the median of 7 trials of 50 replays. The passes agree within 0.8 µs (1.3 µs on TP4's 4-row indexer launch).TP8, E4M3 MLA cache:
TP4, E4M3 MLA cache, indexer launch:
Each change on its own
Each commit against the one before it, TP8, E4M3 MLA cache, indexer launch, µs at 2 / 4 / 8 rows. Two passes in alternating order, one allocation.
Registers
The coverage PR's compile survey: every key the spec builds (
stop=True) and the same keys as the tests build them (stop=False). That is TP4 (2, 4 rows) and TP8 (2, 4, 8); indexer, shared, dense, layer-0 and tail builds; bf16 and E4M3 MLA cache; 90 builds.glm5_monokernel_0, as on the base.stop=True(spec)stop=False(tests)Tests
test_glm53_kernels.pywith 8 GPUs (TP4 and TP8): 8 passed (17 min 32 s), including the new exact top-k test.Checklist
pre-commit run --all-filespasses.test/cover the change..github/CODEOWNERSthat names its owner. (No new model.)