[XPU][FMHA] Add reverse-odd-heads Q-tile scheduler for causal prefill#363
[XPU][FMHA] Add reverse-odd-heads Q-tile scheduler for causal prefill#363pengzhao-intel wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an alternative Q-tile scheduling strategy for Xe2 FlashAttention2 chunk-prefill to reduce per-wave load imbalance in causal prefill by reversing Q-tile dispatch order for odd-indexed heads, with runtime selection controlled via a new reverse_q_order argument and an env-var/heuristic in the Xe2 entrypoint.
Changes:
- Add
XeFHMAIndividualReverseOrderTileSchedulerthat flipsBlockIdxYfor odd heads to mix light/heavy tiles across heads. - Add
chunk_prefill_args_t::reverse_q_orderand dispatch logic to select the new scheduler when enabled. - Parse
VLLM_XPU_FA_REVERSE_ODD_HEADS(with an “auto” heuristic) and add additional benchmark shape configs for MQA prefill/long-context scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| csrc/xpu/attn/xe_2/fmha_xe2.cpp | Adds env-var parsing + heuristic to set args.reverse_q_order for selecting the new scheduler. |
| csrc/xpu/attn/xe_2/collective/chunk_prefill_scheduler.hpp | Introduces the reverse-order per-head Q-tile scheduler implementation. |
| csrc/xpu/attn/xe_2/chunk_prefill.hpp | Adds reverse_q_order flag to args and selects scheduler in kernel_dispatch. |
| benchmark/src/get_model_config.py | Adds benchmark configs covering MQA causal prefill and long-context shapes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static const char* env = std::getenv("VLLM_XPU_FA_REVERSE_ODD_HEADS"); | ||
| std::string mode = "auto"; | ||
| if (env != nullptr) { | ||
| mode.assign(env); | ||
| for (auto& c : mode) c = static_cast<char>(std::tolower(c)); |
| bool reverse; | ||
| if (mode == "1" || mode == "on" || mode == "true" || mode == "yes") { | ||
| reverse = true; | ||
| } else if (mode == "0" || mode == "off" || mode == "false" || | ||
| mode == "no") { | ||
| reverse = false; | ||
| } else { // "auto" or anything unrecognized | ||
| reverse = is_causal && !is_local && !is_sink && num_heads_q >= 2; | ||
| } | ||
| args.reverse_q_order = reverse; | ||
| } |
|
@copilot fix the comment issues |
Addressed in commit e9e8ac0.
|
4f72116 to
dd14f99
Compare
|
I think later we can also try longest processing time scheduling (process longer request firstly from all heads), even for single head, it could help avoid load imbalance issue. |
Yes, I think current implementation is only one way and we may provide different scheduler for different situation. |
|
@jikunshang we may need a more proper way to handle env variables. |
|
Let's wait for benchmark to finally merge this PR. |
Let me know when the benchmark is done |
adf8df4 to
b531241
Compare
b531241 to
2752fbf
Compare
Add XeFHMAIndividualReverseOrderTileScheduler, a variant of the
XeFHMAIndividualTileScheduler that flips the Q-tile dispatch order
for odd-indexed heads. This mitigates the causal-prefill load
imbalance where head 0's first wave is dominated by the lightly
loaded upper-triangle tiles, by letting odd heads consume the heavy
lower-triangle tiles in parallel with the remaining light tiles of
even heads.
Selection is runtime-controlled by chunk_prefill_args_t::reverse_q_order,
set from the FA2 op based on VLLM_XPU_FA_REVERSE_ODD_HEADS:
unset / 'auto' : shape-based heuristic (default)
'1' / 'on' / 'true' : force enabled
'0' / 'off' / 'false' : force disabled
The 'auto' heuristic enables the scheduler whenever the kernel is
running causal prefill with num_heads_q >= 2 and no local/sink
masking. An FA2 varlen MQA sweep on Arc B580 across batch in
{1,2,4} and per-seq qlen in [2048, 131072] showed only a single
near-noise (-0.7%) result and speedups up to +50% kernel time, so
no workload-size guard is required.
Also adds a set of MQA(q=2, kv=1) chunked-prefill and long-context
shapes to gen_cutlass_flash_attn_varlen_perf_configs() so the new
scheduler can be regression-tested via benchmark_cutlass_flash_attn_varlen.
Signed-off-by: pengzhao-intel <patric.zhao@intel.com>
Signed-off-by: pengzhao-intel <patric.zhao@intel.com>
Use a generic mix of MHA / GQA-4:1 / GQA-8:1 prefill shapes (head=128, fp16, causal) at round token counts up to 16k instead of the previous MQA(q=2, kv=1) long-context set. Same intent (exercise the reverse odd-heads scheduler) but covers a broader, more representative shape mix and runs faster. Signed-off-by: pengzhao-intel <patric.zhao@intel.com>
For MHA (num_heads_q == num_heads_kv), reversing Q-tile order for odd heads scatters memory accesses across the sequence, causing L2 cache thrashing when the GPU is fully saturated. GQA/MQA workloads are immune because KV is shared across Q-heads regardless of access order. Tighten the auto-heuristic: always enable for GQA/MQA, but for MHA only enable when the GPU is under-saturated (total_wgs < 4 * sm_count). Fixes regression on Wan-AI (40,40) diffusion model config: 1624us -> ~948us. Signed-off-by: Patric Zhao <patric.zhao@intel.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: pengzhao-intel <patric.zhao@intel.com>
Signed-off-by: Kunshang Ji <kunshang.ji@intel.com>
Motivation
Thanks cong provides the optimization idea.
In FA2 chunk-prefill on Xe2, the default
XeFHMAIndividualTileSchedulerdispatches Q-tiles in the same order for every head. For causal prefill this creates a per-head load imbalance:(head=h, q_blk=0)only attends the diagonal block of KV — light work.(head=h, q_blk=last)attends nearly the full KV history — heavy work.Because all heads start from
q_blk=0, the first wave is uniformly light and the last wave is uniformly heavy. XE cores idle waiting for the heaviest wave to drain.Change
Introduce
XeFHMAIndividualReverseOrderTileScheduler, a drop-in scheduler variant that reverses the Q-tile order for odd-indexed heads. Adjacent heads now mix light and heavy tiles in the same wave, smoothing per-wave occupancy.Selection is per-call via a new bool on the args struct:
The FA2 op sets it from env var
VLLM_XPU_FA_REVERSE_ODD_HEADS:unset/auto1/on/true0/off/falseAuto heuristic:
reverse = is_causal && !is_local && !is_sink && num_heads_q >= 2;Performance evidence
Intel Arc B580, FA2 varlen, MQA q=2 / kv=1, head_size=128, fp16, causal. Numbers are
FlashAttention_Kernel_Time(us)speedup ofonoveroff.Threshold sweep (bs ∈ {1, 2, 4}, per-seq qlen ∈ [2048, 32768])
Long-context shapes (total kvlen up to 262k)
Worst observed: -0.7 % on bs=4, qlen=4k non-paged — within run-to-run noise. Every other config across 28 measured shape variants showed a speedup.
API / backwards compatibility
reverse_q_orderdefaults tofalse, so callers that constructchunk_prefill_args_twithout going through fmha_xe2.cpp are unaffected.auto, i.e. enabled for causal multi-head prefill. To restore strict pre-PR behavior setVLLM_XPU_FA_REVERSE_ODD_HEADS=off.Testing
pytest tests/flash_attn/test_flash_attn_varlen_func.pywith bothVLLM_XPU_FA_REVERSE_ODD_HEADS=onandoffto confirm numeric equivalence.