Repository navigation
[Perf][QSA] SM70: score multi-request decode batches by request segments - #59
Merged
Merged
Conversation
The paged QSA scorer loads every 64-key tile once per query row, although the rows of one request share their page table. The new scorer loads each tile once per segment of up to four rows of one request and multiplies it by those rows together: four rows x four heads form one 128x16 query block, the paged kernel's own 64x128 by 128x16 dot shape, with the same 2 warps and the same software-pipelined tile loop, and each row is reduced over the same 16 lanes. Scores and visible lengths are bitwise equal to the paged scorer. Opt-in with VLLM_SM70_QSA_REQUEST_SEGMENTS=1 (default off). Only multi-request batches of at most 256 FP16 rows (H4/D128, int32 metadata) with at least two rows per request on average use it. Single-request batches keep the shared-key and cuBLAS paths; one-row-per-request batches (MTP drafting steps, also when padded for CUDA graphs) keep the paged scorer, where one-row segments measured up to 15% slower. Based on the request-segment prototype and its CPU checks from the research PRs #56 and #57. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
…nd off Runs qsa_select_paged_tokens with VLLM_SM70_QSA_REQUEST_SEGMENTS off and on over decode batches, one row per request, ragged rows, prefill next to decode (with and without the host request split), two logits chunks and CUDA-graph padding rows, eagerly and through graphs replayed after the inputs change. The token indices must be bitwise equal; --time adds the graph replay time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
The scorer route is chosen inside the qwen4_exp_qsa_with_output custom op at run time, so the traced graph does not depend on it. Hashing it would recompile every deployment when the variable is registered and again when it is switched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
The ReVolta FP16 QSA SM70 TP4 profile now sets VLLM_SM70_QSA_REQUEST_SEGMENTS=1, as it does for the shared-key scorer; an explicit value still wins. The acceleration report gains a qsa_request_segments entry with its guards and fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com>
This was referenced Oct 30, 2026
Merged
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.
Change
On SM70 the paged QSA scorer loads every 64-key tile of the compressed key cache once per query row. In a multi-request decode batch the rows of one request share a page table, so this PR adds a request-segmented scorer that loads each tile once per segment of up to four rows of one request and multiplies it by those rows together: four rows × four heads form one 128 × 16 query block, the paged kernel's own 64 × 128 by 128 × 16 dot, with the same 2 warps, the same pipelined tile loop and the same per-row 16-lane reduction (the other rows' lanes are zeroed). Scores, visible lengths and the selected top-512 blocks are bitwise equal to the paged scorer.
qsa_select_paged_tokensuses it for batches with two or more requests, at most 256 rows per logits chunk, at least two rows per request on average, FP16 H4/D128 and int32 metadata. Single-request batches keep the shared-key and cuBLAS paths. One-row-per-request batches (MTP drafting steps, also when padded for CUDA graphs) keep the paged scorer, where one-row segments measured up to 15 % slower. Segment descriptors are built on the device, so the call is CUDA-graph safe.VLLM_SM70_QSA_REQUEST_SEGMENTSswitches it. The ReVolta SM70 profile turns it on; an explicit value wins. The variable is not a torch.compile factor (the route is chosen inside the QSA custom op), so registering or switching it does not recompile a deployment. The acceleration report gains aqsa_request_segmentsentry.Validation
CPU: Triton-interpreter checks of the segment builder and scorer against the paged kernel and a NumPy oracle (11), dispatch tests (18), the compile-factor test and the profile tests pass; pre-commit and mypy 3.12 pass on the changed files.
GPU, one V100 PCIe at 250 W:
qsa_select_paged_tokenswith the variable off and on over 30 shapes (decode batches, one row per request, ragged rows, a prefill chunk next to decode rows with and without the host request split, two logits chunks, CUDA-graph padding rows): selected token indices identical, and the shapes that must keep the paged scorer do.Engine, our 4 × V100 PCIe-switch server at 250 W, the 0.1.5 release image with this PR's three runtime files, variable off and on in one window with the same compiled artifacts (no recompilation, same KV pool):
Variants tried and not taken: 8-row segments (two query blocks) spill at 2 warps; 4 or 8 warps fit but change the reduction order (about 3.7 % of score words differ) and were slower than this scorer in every measured shape; a two-pass 8-row variant at 2 warps is bitwise but not faster than four-row segments.
Attribution
The request-segment idea, the first prototype and its CPU interpreter checks come from Codex in the research PRs #56 and #57. Claude Code reworked the kernel until it was bitwise on SM70 (loop-invariant grouped queries inside the paged kernel's pipelined tile loop), integrated and tested it, and ran the GPU and engine validation. Commits are signed off under the DCO.
🤖 Generated with Claude Code