Skip to content

[Perf][Kernel] Size cooperative_topk's single pass by row length, not stride - #62171

Merged
tlrmchlsmth merged 4 commits into
vllm-project:mainfrom
haosdent:perf/cooperative-topk-row-length
Oct 23, 2026
Merged

tlrmchlsmth merged 4 commits into
vllm-project:mainfrom
haosdent:perf/cooperative-topk-row-length

Conversation

@haosdent

@haosdent haosdent commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Overview

cooperative_topk now chooses between its single-pass and two-pass paths from each row's length instead of the
logits row stride. Rows that fit the single-pass staging no longer stream through TMA twice just because the
buffer is wide (max_model_len).

Claims

Validation

InferenceX AgentX setup and results for all GLM-5.3 PRs (4x GB200)

InferenceX c801c9f2d AgentX scenario (pinned AIPerf), 20-minute fast mode, GLM-5.3-NVFP4 rev e3b8f9f2, TP4, MTP 3
with synthetic acceptance 2.99, native KV offload 135 GB per rank, Rust frontend, max-num-seqs = 2 x
concurrency. vLLM main dcd0796 with its default backends and CUDA graph config, plus the PRs cumulatively in
the order below (FlashInfer with flashinfer-ai/flashinfer#6630, flashinfer-ai/flashinfer#6631 and
flashinfer-ai/flashinfer#6632 from #62174 on). One FlashInfer MoE autotune per concurrency, shared by all
variants. A fresh server per point, with the page cache dropped before each, and main run again at the end of
each concurrency as an A/A check. SGLang a916d91cd3 with IMEX, TRT-LLM
all-reduce fusion and --mem-fraction-static 0.90.

Each cell is the median, over the turns both runs finished (150 to 520), of the per-turn ratio to the previous
variant, with a 95% paired bootstrap CI. Decode is request latency minus TTFT. The bootstrap does not include
run-to-run variance; the main-vs-main row does.

decode time per turn c=1 c=4 c=8 c=16
+ #62170, #62171, #62172 -2.8% [-3.1, -2.6] -2.5% [-3.1, -1.7] -1.5% [-2.2, -1.1] -1.5% [-2.4, +0.3]
+ #62174 -1.6% [-1.8, -1.5] -1.8% [-2.5, -0.9] -3.3% [-3.9, -2.9] -2.1% [-3.8, -0.9]
+ #62173 +0.5% (no-op here) -0.3% [-0.5, -0.1] -0.1% [-0.4, +0.3] -0.9% [-2.0, +0.2]
all PRs vs main -3.7% [-3.9, -3.5] -4.3% [-4.8, -4.0] -5.3% [-6.4, -4.8] -4.8% [-6.5, -3.8]
main vs main (noise) -0.1% -0.7% +0.3% -0.3%
e2e latency per turn c=1 c=4 c=8 c=16
+ #62170, #62171, #62172 -3.3% -1.8% -1.2% -0.6%
+ #62174 -1.4% -1.4% -2.7% -1.6%
+ #62173 +0.9% -0.3% -0.7% -1.1%
all PRs vs main -4.1% -3.4% -4.7% -3.8%
main vs main (noise) -0.6% -1.2% +0.4% -0.4%

KV cache tokens per GPU (c=1 / 4 / 8 / 16): main 1,176,832 / 1,162,880 / 1,143,872 / 1,105,984; with all PRs
1,178,496 / 1,168,512 / 1,157,824 / 1,103,936. Window throughput (tok/s/GPU) moves up to 2.4% between runs in fast
mode, since it counts whichever long-context turns finish inside the 20 minutes; with all PRs it is +3.0%, -1.4%,
+4.8% and +1.2% at c=1/4/8/16.

vLLM vs SGLang, decode time per turn on the same turns (positive = vLLM slower):

c=1 c=4 c=8 c=16
main +10.4% +12.7% +13.7% -2.9%
all PRs +6.2% +7.5% +5.7% -6.9%

#62170, #62171 and #62172 were measured as one group. vllm bench forward (target verify forward, spread < 0.1%)
splits their gain about 1/6, 1/2 and 1/3. #62173 changes only decode steps of 3, 5 or 7 requests; steps with 3 or
more requests are 0%, 4%, 13% and 37% of all steps at c=1/4/8/16. At c=1 both runs capture the same graphs, so
its +0.5% there is noise. At c=16, three more runs each of + #62174 and + #62173 (4 runs each, compared as
runs): decode time -0.54% [-0.93, -0.16], with every + #62173 run faster than every + #62174 run (permutation
p = 2/70); e2e latency -0.29% [-1.41, +0.82].

Details

The DSA indexer allocates its decode logits with max_model_len columns, and cooperative_topk gets that as
stride. The kernel sized per_block (elements per CTA of the cluster) from the stride. But large_topk splits
the row by its actual length (seq_len, in 16-byte units across the cluster ranks). With a long max_model_len,
every row took the two-pass streaming path even when its context fit the all-stages-resident single pass. For
example, 16 rows use 4-CTA clusters, and a 256K stride gives 64K elements per block, above the 48K single-pass
staging, while a 40K-token row needs only 10K per block.

per_block is now ceil(seq_len / CS), so each row picks its path from its own length. large_topk rounds each
block to 16-byte units, but the single-pass limit and the stage size are multiples of that unit, so this gives the
same path and stage count as the exact split. Rows in one launch can take different paths. Each row is its own
cluster, and the barrier setup already follows the chosen path.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Comment thread csrc/cooperative_topk.cuh Outdated
// staging.
const uint32_t per_block =
(params.stride + CS - 1) / CS; // how many elements per block
((static_cast<uint32_t>(sl) + 3) / 4 + CS - 1) / CS * 4;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could be

(static_cast<uint32_t>(sl) + CS - 1) / CS

@LopezCastroRoberto LopezCastroRoberto 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.

LGTM, thanks!

Comment thread tests/kernels/test_top_k_per_row.py Resolved
… stride

cooperative_topk chose between its single-pass (all TMA stages resident)
and two-pass streaming paths from the logits row stride. The indexer's
decode logits are allocated with max_model_len columns, so with a long
max_model_len every row streamed twice even when its context fit the
single-pass staging. Size the decision by the row's own length, the way
large_topk already partitions it.

Signed-off-by: haosdent <haosdent@gmail.com>
kFusedMax and kSizePerStage are multiples of the 16-byte TMA unit, so
rounding the per-block length up to that unit (as large_topk splits the
row) picks the same path and stage count.

Signed-off-by: haosdent <haosdent@gmail.com>
Signed-off-by: haosdent <haosdent@gmail.com>
@haosdent
haosdent force-pushed the perf/cooperative-topk-row-length branch from fe1f9c9 to 4aa3313 Compare October 23, 2026 11:30
@LopezCastroRoberto LopezCastroRoberto added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 23, 2026
@github-actions

Copy link
Copy Markdown

✅ @haosdent, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Run commands test your branch with every commit currently on its upstream target branch. If it is behind, the bot merges the target branch into it first (pull before pushing again). Append --allow-stale to a run command to test an outdated branch as is, at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@LopezCastroRoberto

Copy link
Copy Markdown
Contributor

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #96880 for commit 789fb34874f4.

@tlrmchlsmth
tlrmchlsmth merged commit 404c2b5 into vllm-project:main Oct 23, 2026
489 checks passed
bantianyue pushed a commit to bantianyue/vllm that referenced this pull request Oct 28, 2026
… stride (vllm-project#62171)

Signed-off-by: haosdent <haosdent@gmail.com>
Co-authored-by: Roberto L. Castro <38211239+LopezCastroRoberto@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants