[FEAT] Context parallel indexer for GLM-5.x - #15
Open
minhpham-duc wants to merge 4 commits into
Open
minhpham-duc wants to merge 4 commits into
minhpham-duc wants to merge 4 commits into
Conversation
minhpham-duc
marked this pull request as ready for review
September 15, 2026 03:04
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.
Purpose
This PR splits the DSA sparse-attention indexer prefill work across tensor-parallel (TP) ranks on ROCm/AITER. It adds two new env vars to control it.
The problem. In
rocm_aiter_sparse_attn_indexer, every TP rank does the same full work. Each rank computesrocm_fp8_mqa_logitsand top-k over allMquery rows. The work is copied on every rank, so TP does not make the indexer faster.The fix. We split the rows across ranks. Rank
rtakes rows[block + r*stripe, block + (r+1)*stripe)for each block oftp_world_size * striperows. So each rank does only part of the work.Why stripes and not simple chunks. With a causal mask, later rows cost more than earlier rows. If we gave each rank one big chunk, rank 0 would get all the cheap rows and rank 7 would get all the expensive rows. That is not balanced. With stripes, every rank gets a mix of cheap and expensive rows. So each rank does about
M²/2/tpwork.How ranks combine results. We use
all_reduce(MAX)ontopk_indices_buffer. Rows that a rank did not write are still-1, because the buffer is reset to-1at the start of the call. So MAX picks the real value and gives the full result. Theall_reduceruns once per prefill, outside the chunk loop.New env vars:
VLLM_ROCM_USE_AITER_CP_INDEXERFalseVLLM_ROCM_USE_AITER_CP_INDEXER_STRIPE_SIZE512We skip the split and use the old path when
tp_world_size == 1orchunk_m < tp_world_size.Related work
vLLM #46103 [ROCm][Perf] TP-shard Lightning indexer prefill
The same problem on the vLLM side: on ROCm/gfx950 the DSA (Lightning) indexer prefill,
rocm_fp8_mqa_logitsplus per-row top-k, runs replicated on every TP rank and becomes the dominant, TP-non-scaling term at long context. vllm-project#46103 shards it along the query-token (M) dimension as well, gated ontp_size > 1andnum_prefill_tokens >= 512 * tp_size, behindVLLM_ROCM_USE_AITER_MLA_SPARSE_TP_SHARD(default on) with an optional..._FOLDmode. It reports, on 8x MI355X TP8, a 2.1x throughput gain and ~60% lower TTFT for GLM-5.1-FP8 at 199K tokens, +40% throughput for DeepSeek-V4-Pro at 512K, and a neutral ±2-3% at ≤8K context. The PR is still open (needs rebase) at the time of writing.Test Plan
Hardware: 8x MI355X (gfx950, 288 GB each), ROCm 7.2.3. Model:
GLM-5.2-MXFP4(GlmMoeDsaForCausalLM,index_topk=2048), TP=8.Three configurations, each a fresh server boot:
main@8c34a3723. The env var does not exist on that branch.VLLM_ROCM_USE_AITER_CP_INDEXER=1,VLLM_ROCM_USE_AITER_CP_INDEXER_STRIPE_SIZE=512.VLLM_ROCM_USE_AITER_CP_INDEXER_STRIPE_SIZE=1024.We benchmarked two
_STRIPE_SIZEvalues,512and1024, because different stripe sizes can yield different performance results.We ran the baseline on
mainrather than on this branch with the flag set to0, so the baseline cannot accidentally include any of this code.All three use the same
vllm servecommand:Env vars for all runs:
VLLM_USE_V2_MODEL_RUNNER=1 VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1 VLLM_ROCM_USE_AITER_FP8BMM=0 VLLM_ROCM_USE_AITER_FP4BMM=0.We turned prefix caching off with
--no-enable-prefix-caching. So every request does a full prefill. We checked this two ways: the engine log showsenable_prefix_caching=False, and every result hastotal_input_tokens == num_prompts * ISL.Benchmark command for each point:
Test points: ISL/OSL 8192/1024 and 51200/1024, each at concurrency 4, 8, 16, 32, 64, plus 128 and 256 for 8192. 12 points per configuration, 36 runs in total.
Three correctness evals were run against the served model (the server config above is unchanged; these are all HTTP clients).
gsm8k — full 1319-question test set via lm-eval, 5-shot, greedy, thinking off:
GPQA-Diamond — 198 questions via sglang's
run_eval(NeMo-Skills mcq prompt +eval_mcqgrader), thinking on:python3 -m sglang.test.run_eval --eval-name gpqa \ --model amd/GLM-5.2-MXFP4 \ --host 127.0.0.1 --port 8586 --num-threads 32 --max-tokens 100000 \ --chat-template-kwargs '{"enable_thinking": true, "think_end_token": "</think>"}'RULER / NIAH —
niah_single_2at 64k and 128k via lm-eval, thinking on:RULER needs
tokenizer=because it sizes its own prompts to exact token counts.timeoutis aiohttp's total per-request budget including time queued behindnum_concurrent, so it must exceed the whole run's wall time, not just one request.Test Result
Correctness
All three evals were run on both configurations.
GPQA-Diamond (198 questions,
sglang.test.run_eval, thinking enabled,--max-tokens 100000):main)The 0.025 gap is 5 questions out of 198 and is not a regression. Truncation moved by a similar amount in the same direction: 68 baseline responses against 75 indexer responses were cut off at the token cap and scored zero, a 7-question difference. At a ~35% truncation rate this eval is partly measuring whether the reasoning fits in the budget, so treat the table as a like-for-like comparison at a fixed budget rather than an absolute GPQA score.
gsm8k (1319 questions, 5-shot, greedy, thinking off):
main)exact_match, strict-matchexact_match, flexible-extractRULER / NIAH (
niah_single_2, 500 documents per length, thinking on):main)Performance
"before" = baseline (
main), "after" = this branch with the context parallel indexer on. In every table below, Δ is signed so that positive always means the context parallel indexer is better — for throughput (higher is better) and for TTFT/TPOT (lower is better) alike.Results with
_STRIPE_SIZE= 512ISL 8192 / OSL 1024 — Output throughput (tok/s)
ISL 8192 / OSL 1024 — Mean TTFT (ms)
ISL 8192 / OSL 1024 — Mean TPOT (ms)
ISL 51200 / OSL 1024 — Output throughput (tok/s)
ISL 51200 / OSL 1024 — Mean TTFT (ms)
ISL 51200 / OSL 1024 — Mean TPOT (ms)
Results with
_STRIPE_SIZE= 1024ISL 8192 / OSL 1024 — Output throughput (tok/s)
ISL 8192 / OSL 1024 — Mean TTFT (ms)
ISL 8192 / OSL 1024 — Mean TPOT (ms)
ISL 51200 / OSL 1024 — Output throughput (tok/s)
ISL 51200 / OSL 1024 — Mean TTFT (ms)
ISL 51200 / OSL 1024 — Mean TPOT (ms)
Stripe-size comparison, ISL 51200 / OSL 1024 / conc 4
_STRIPE_SIZESummary:
Treat differences under about 1% as noise. The 50k gain holds across five concurrency levels and both stripe sizes.
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)