perf(rope): fix token/head-parallel dispatch threshold on CDNA3#264
Merged
Merged
Conversation
The RoPE dispatchers chose between the token-parallel and head-parallel cos/sin kernels using an occupancy-derived threshold (num_blocks_per_sm * num_sms), which set the bar at ~8-16x the CU count. This effectively never selected the token-parallel kernel even at sequence lengths where it is substantially faster: it keeps each block's accesses contiguous across heads, whereas the head-parallel kernel scatters one head across tokens (strided DRAM). Retune to nblks_x >= 2 * num_sms, the measured crossover on gfx942 where the token-parallel grid has enough blocks to fill the device. Also drops the per-launch gpuOccupancyMaxActiveBlocksPerMultiprocessor query from the hot path. Measured 1.4-1.8x speedup at seq_len 8192-16384 on MI300X for both the cos/sin-cache and on-the-fly paths; small and very large sizes unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR retunes the ROCm RoPE kernel launch-time dispatch heuristic so the faster token-parallel implementation is selected at realistic mid/large problem sizes on CDNA3, removing an overly strict occupancy-derived threshold that almost always forced the slower head-parallel path.
Changes:
- Replaced the occupancy-based token/head-parallel crossover condition with a simpler
nblks_x >= 2 * num_smsthreshold in both cos/sin-cache and on-the-fly RoPE paths. - Removed the per-launch
gpuOccupancyMaxActiveBlocksPerMultiprocessorquery from these dispatch sites and documented the rationale/tradeoff inline.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Summary
The RoPE kernels pick between a token-parallel and a head-parallel implementation at launch time, but the selection threshold was miscalibrated so the faster token-parallel kernel almost never ran. Retuning the threshold yields a measured 1.4–1.8x speedup at mid-to-large sequence lengths on MI300X (gfx942), with no change to small or very large sizes and no kernel logic changes.
What changed
include/flashinfer/attention/generic/pos_enc.cuh— corrected the token- vs head-parallel dispatch threshold inBatchQKApplyRotaryPosIdsCosSinCache(cos/sin-cache path) andBatchQKApplyRotaryPosIds(on-the-fly path). The old threshold wasnblks_x >= num_blocks_per_sm * num_sms, wherenum_blocks_per_smcame fromgpuOccupancyMaxActiveBlocksPerMultiprocessor(8–16 on gfx942) — putting the bar at ~8–16x the CU count. The new threshold isnblks_x >= 2 * num_sms. This also removes the per-launch occupancy query from the hot path.Architecture / design notes
The two kernels differ only in their grid mapping and resulting memory access pattern:
kernel_0)(nblks_x,)bdycontiguous tokens across all heads → contiguous DRAMkernel_1)(nblks_x, num_heads)The token-parallel layout streams memory contiguously and is markedly faster, but it launches
num_headsx fewer blocks, so below a certain size it under-fills the device and the head-parallel form wins. The crossover is where the token-parallel grid has roughly enough blocks to fill all CUs, measured atnblks_x ~= 2 * num_smson CDNA3. The threshold is device-count-relative (num_sms), so it adapts to other architectures; the2xconstant was tuned on gfx942 and is worth reconfirming on gfx950.The old occupancy-derived threshold was both too high (suppressing the fast path) and pathological at specific sizes — e.g. the baseline
apply_rope_with_cos_sin_cachewas slower at seq_len 16384 (395 us) than at 32768 (388 us) because 16384 fell on the wrong side of it.Benchmark results
Shape: head_size=128, rotary_dim=128, num_q_heads=32, num_kv_heads=8, bf16, batch=2, MI300X / gfx942. Latency in microseconds, median of
bench_gpu_time.apply_rope_with_cos_sin_cache_inplace(neox):apply_rope_pos_ids(on-the-fly, neox):Sizes at or below 2048 are unchanged (they correctly remain head-parallel). The largest sizes were already crossing to token-parallel under the old threshold, so they are unchanged.
Test plan
pytest tests/rocm_tests/test_rope_hip.py— 13080 passedpre-commit run --files include/flashinfer/attention/generic/pos_enc.cuh— passed (incl. clang-format)