Skip to content

perf(rope): fix token/head-parallel dispatch threshold on CDNA3#264

Merged
demandal25 merged 1 commit into
amd-integrationfrom
perf-rope-dispatch-token-parallel
Jun 24, 2026
Merged

perf(rope): fix token/head-parallel dispatch threshold on CDNA3#264
demandal25 merged 1 commit into
amd-integrationfrom
perf-rope-dispatch-token-parallel

Conversation

@demandal25

Copy link
Copy Markdown
Collaborator

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 in BatchQKApplyRotaryPosIdsCosSinCache (cos/sin-cache path) and BatchQKApplyRotaryPosIds (on-the-fly path). The old threshold was nblks_x >= num_blocks_per_sm * num_sms, where num_blocks_per_sm came from gpuOccupancyMaxActiveBlocksPerMultiprocessor (8–16 on gfx942) — putting the bar at ~8–16x the CU count. The new threshold is nblks_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 Grid Per-block access pattern
token-parallel (kernel_0) (nblks_x,) bdy contiguous tokens across all heads → contiguous DRAM
head-parallel (kernel_1) (nblks_x, num_heads) one head scattered across tokens → strided DRAM

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 at nblks_x ~= 2 * num_sms on CDNA3. The threshold is device-count-relative (num_sms), so it adapts to other architectures; the 2x constant 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_cache was 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):

seq_len before after speedup
4096 76.4 70.8 1.08x
8192 168.5 117.3 1.44x
16384 395.4 216.3 1.83x
32768 387.8 387.5 1.00x
65536 731.3 731.3 1.00x

apply_rope_pos_ids (on-the-fly, neox):

seq_len before after speedup
4096 101.2 69.8 1.45x
8192 198.5 123.4 1.61x
16384 394.6 215.2 1.83x

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 passed
  • Before/after benchmarks above are real A/B measurements (stash baseline vs. tuned), not estimates
  • pre-commit run --files include/flashinfer/attention/generic/pos_enc.cuh — passed (incl. clang-format)

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>
Copilot AI review requested due to automatic review settings June 24, 2026 05:11

Copilot AI 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.

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_sms threshold in both cos/sin-cache and on-the-fly RoPE paths.
  • Removed the per-launch gpuOccupancyMaxActiveBlocksPerMultiprocessor query from these dispatch sites and documented the rationale/tradeoff inline.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@demandal25 demandal25 merged commit 618e025 into amd-integration Jun 24, 2026
2 checks passed
@demandal25 demandal25 deleted the perf-rope-dispatch-token-parallel branch June 24, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants