Repository navigation
[Perf] CP: QSA Context Parallel Optimization - #43792
Merged
Fridge003 merged 9 commits intoOct 22, 2026
Merged
Conversation
Each QSA layer on the CP prefill path synchronized the host with the device four times (five with a cached prefix). Three of them sit after an all-gather, so every rank's CPU waited for the slowest rank's gather each layer and the GPU then idled while the CPU relaunched. Remove them: - S1 apply_rope's positions.max().item(): the CP indexer skips the per-call cos/sin cache sizing; the load-time reservation already covers the positions that this layer's attention reads without a check. - S2 get_prefill_mqa_inputs' .tolist(): extend metadata carries the host sequence lengths (prefill_sequence_lengths_cpu) from seq_lens_cpu. - S3 prefix pack's req_pool_indices.tolist(): use the scheduler's req_pool_indices_cpu. - S4 cu_k torch.tensor(host list): built on device from seq_lens. - S5 sparse_gqa_fwd_interface_triton_ck's max_q .item(): optional host max_q, fed from the zigzag metadata's max_seqlen_q_combined. Measured with a 12-QSA-layer CP4 benchmark on 8x B200 (real kernels and NCCL, Qwen3.8-Flash-Next shapes): 8K 29.7 -> 21.3 ms, 8K chunk on a 56K prefix 35.6 -> 26.2 ms, 4x8K 66.3 -> 57.5 ms, 32K 68.5 -> 60.4 ms. Host syncs per layer go from 4 (5 with a prefix) to 0; the written caches, every top-k selection input and the attention output match the old path bitwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the host syncs gone, the layer's K/V all-gather sits on the QSA CP critical path ahead of sparse attention. It does not feed compression, MQA scoring or top-k, so start it on a side stream right after the indexer's key gather and let the attention wait for it: - QSAIndexer.forward_cuda_cp takes on_key_gather_issued (plumbed through forward_cuda as cp_on_key_gather_issued), called right after the key gather is enqueued, so every rank issues the CP group's collectives in one order. - QwenSparseAttnBackend.prefetch_cp_kv issues the gather: every rank sends its padded rows, and one index_select per half restores global token order through a per-forward index built from the zigzag host metadata. All buffers are allocated on the consumer stream and kept alive until the wait, so no record_stream is needed. _forward_extend_cp consumes a prefetched gather and otherwise gathers inline; init_forward_metadata drains an unconsumed one. The key gather stays inline: it is short, and its waits are the previous layer's sparse-attention imbalance, which overlap cannot hide. The Qwen4-Exp call site (cp/4) enables it by passing cp_on_key_gather_issued=partial(backend.prefetch_cp_kv, self.attn, k, v, forward_batch) to the indexer. Measured with a 12-QSA-layer CP4 benchmark on 8x B200 against the previous commit: 8K chunk on a 56K prefix 26.25 -> 25.64 ms, 4x8K 57.52 -> 55.85 ms, 32K 60.43 -> 58.77 ms, 128K 425.8 -> 419.1 ms; the CPU-bound 8K first chunk is +0.2 ms. The written caches, every top-k selection input and the attention output match the inline gather bitwise, padded and ragged batches included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shunkangz
requested review from
Fridge003,
HaiShaw,
Qiaolin-Yu,
hebiao064,
ispobock and
merrymercy
as code owners
October 12, 2026 07:30
Move the side-stream gather into CP utilities with generic contiguous output splits. Reuse strategy-owned token ordering for zigzag and interleave while preserving QSA launch timing, deferred waits, and buffer lifetime. Validated with 114 unit tests and 20 subtests, all pre-commit checks, and six two-GPU PyNccl cases covering ordering, output splits, stream dependencies, and asynchronous launch.
Shunkangz
requested review from
BBuf,
Edwardf0t1,
Ying1123 and
ch-wan
as code owners
October 13, 2026 01:57
15 of 21 tasks
The prefetch had its own gather: a side-stream all_gather into buffers allocated on the main stream, an index_select by a per-forward gather index, and generic CPAsyncGather/CPGatherHandle/build_gather_index helpers in layers/cp. Run the inline gather path instead (cp_materialize_global_token_order) on the qsa_cp_kv stream. The side stream waits for the main stream when the indexer's key gather has been issued; the layer's _forward_extend_cp waits for the side stream before reading the gathered K/V, and the side stream, which allocated them, waits for those reads before they are released. The caller keeps k/v alive through the join. No record_stream, no keep-alive tuple, and layers/cp is back to main. This also removes the gather index's per-forward host-to-device copy, which synchronized the host once per forward. A single pending slot replaces the per-layer dict; prefetch_cp_kv asserts it is empty, and init_forward_metadata joins one a failed forward left behind. The cached-prefix pack reads slots from the per-forward token_slot_table and cu_k from the per-forward int32 sequence_lengths, instead of req_pool_indices(_cpu) and a per-layer cast of seq_lens. With --enable-symm-mem the gather's symmetric buffer now comes from the side stream's share of the NCCL pool, separate from the forward stream's preallocation: one more segment of about the largest K/V gather (2 KiB per token for Qwen3.8-Flash-Next). Measured with the 12-QSA-layer CP4 benchmark on 8x H200, before the release wait and the cu_k reuse were added: the written caches, every top-k selection input and the attention output match the inline gather bitwise (8193, 8K on a 56K prefix, 4x8K, 3000+5192 on a 64-token prefix); host syncs per layer stay at 0; with the join skipped and the gathers delayed, the check fails on every rank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The QSA backend now fills prefill_sequence_lengths_cpu for every non-speculative extend batch when it builds the forward metadata. Every breakable prefill CUDA graph capture and replay rebuilds that metadata from the live batch before the indexer's eager break runs, so the per-layer msgspec.structs.replace in _compute_qsa_topk_indices_eager rebuilt the same tuple. The backend's copy slices seq_lens_cpu by seq_lens' length, as the indexer's device fallback does; the override sliced by batch_size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
/tag-run-ci-label |
Conflict in qwen4_exp.py _compute_qsa_topk_indices_eager. Main's cp/4 (sgl-project#39724) added the cp_global_rope_positions indexer kwargs next to the use_host_prefill_lengths override that this branch drops. Kept main's CP kwargs and dropped the override: the QSA backend already fills prefill_sequence_lengths_cpu for every non-speculative extend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
/rerun-failed-ci |
8 of 11 tasks
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Collaborator
|
Can you please paste a screenshot of profile traces, which shows the overlap of kv all gather and indexer computation? |
Fridge003
reviewed
Oct 20, 2026
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Contributor
Author
|
/rerun-failed-ci |
Fridge003
approved these changes
Oct 22, 2026
Collaborator
|
Can merge after CI pass |
This was referenced Oct 24, 2026
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
Under prefill context parallelism, each QSA layer runs two all-gathers: one for the index keys and one for the layer's K/V. The following changes reduce synchronization overhead and overlap communication with computation.
1. Remove Host–Device Synchronizations
The first change removes four host–device synchronizations per layer on this path, or five when using a cached prefix:
positions.max().item()call in the indexer's RoPE..tolist()conversion of sequence lengths..tolist()conversion, when using a cached prefix.torch.tensorcopy that builds the sparse-attention kernel's K offsets.max_q.item()call.Three of these synchronizations, or four with a cached prefix, occurred immediately after an all-gather. As a result, every rank's CPU waited for the slowest rank on each layer, and the GPU then sat idle while the CPU relaunched work. Removing these synchronizations lets the CPU run ahead.
This saves:
There is no numeric change.
2. Overlap the K/V All-Gather with the Indexer
The second change overlaps the K/V all-gather with the indexer. A hook fires immediately after the indexer issues its own key gather and starts the K/V gather on a side stream.
The K/V gather then runs during:
Attention waits for the gather only when it needs the result.
The index-key gather stays inline: it is short, and its long waits come from load imbalance, which overlap cannot hide.
This provides additional latency reductions:
Modifications
Accuracy Tests
Speed Tests and Profiling
CP4
CP8
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ❌ Run #39539850826
Latest PR Test (Extra): ❌ Run #39539850792
Latest PR Test (AMD ROCm 10): ❌ Run #39539850831