Repository navigation
Optimize AMD GQA verify attention path - #26283
fanxingran wants to merge 1 commit into
Conversation
Add a HIP few-query GQA packed split-K attention path for EAGLE/MTP verify workloads, reducing repeated KV loads and improving long-context verify performance on AMD GPUs.
There was a problem hiding this comment.
Code Review
This pull request introduces a GQA-grouped, split-K optimization for the extend_attention Triton kernels, specifically targeting 'few query token' verify shapes on HIP-based systems. The implementation adds two new kernels, _fwd_kernel_gqa_split and _fwd_kernel_gqa_combine, to improve HBM utilization. Review feedback highlights the need to align split boundaries to BLOCK_N to avoid undefined behavior, ensure numerical precision by using float32 accumulation in dot products, and optimize performance by using fixed block sizes to prevent kernel recompilation and pre-allocating intermediate buffers.
| split_len = tl.cdiv(cur_seq_len_prefix, NUM_KV_SPLITS) | ||
| split_start = cur_split * split_len | ||
| split_end = tl.minimum(split_start + split_len, cur_seq_len_prefix) |
There was a problem hiding this comment.
The current calculation of split_len does not guarantee that split_start is a multiple of BLOCK_N. This violates the tl.multiple_of(start_n, BLOCK_N) hint on line 667, which can lead to undefined behavior, incorrect memory access, or compilation errors. The split boundaries should be aligned to BLOCK_N boundaries.
| split_len = tl.cdiv(cur_seq_len_prefix, NUM_KV_SPLITS) | |
| split_start = cur_split * split_len | |
| split_end = tl.minimum(split_start + split_len, cur_seq_len_prefix) | |
| num_blocks = tl.cdiv(cur_seq_len_prefix, BLOCK_N) | |
| blocks_per_split = tl.cdiv(num_blocks, NUM_KV_SPLITS) | |
| split_start = cur_split * blocks_per_split * BLOCK_N | |
| split_end = tl.minimum(split_start + blocks_per_split * BLOCK_N, cur_seq_len_prefix) |
| mask=(mask_n[None, :]) & (mask_d[:, None]), | ||
| other=0.0, | ||
| ) | ||
| qk = tl.dot(q.to(k.dtype), k) |
There was a problem hiding this comment.
The tl.dot call is missing out_dtype=tl.float32. For attention score computation, especially when using fp8 or fp16 inputs, accumulating in float32 is critical for precision and numerical stability. This is also inconsistent with the _fwd_kernel_gqa_combine kernel at line 891 which explicitly uses out_dtype=tl.float32.
| qk = tl.dot(q.to(k.dtype), k) | |
| qk = tl.dot(q.to(k.dtype), k, out_dtype=tl.float32) |
| other=0.0, | ||
| ) | ||
| p = p.to(v.dtype) | ||
| acc = acc * re_scale[:, None] + tl.dot(p, v) * v_scale |
There was a problem hiding this comment.
Similar to the QK dot product, the accumulation of acc should explicitly use out_dtype=tl.float32 to maintain precision during the reduction, especially when inputs are low-precision types like fp8 or fp16.
| acc = acc * re_scale[:, None] + tl.dot(p, v) * v_scale | |
| acc = acc * re_scale[:, None] + tl.dot(p, v, out_dtype=tl.float32) * v_scale |
| if use_gqa_packed: | ||
| kv_head_num = head_num // kv_group_num | ||
| # whole extend region packs into one M block: kv_group_num * draft <= BLOCK_M | ||
| BLOCK_M_PACKED = triton.next_power_of_2(kv_group_num * max_len_extend) |
There was a problem hiding this comment.
BLOCK_M_PACKED depends on max_len_extend, which can vary between batches. Since BLOCK_M is a tl.constexpr in the Triton kernel, this will trigger a kernel recompilation whenever the maximum sequence length in a batch changes to a new power-of-2 bucket. Consider using a fixed BLOCK_M (e.g., 128) and masking out unused rows to avoid recompilation overhead during inference.
| mid_o = torch.empty( | ||
| (batch_size, kv_head_num, NUM_KV_SPLITS, BLOCK_M_PACKED, BLOCK_DV + 2), | ||
| dtype=torch.float32, | ||
| device=q_extend.device, | ||
| ) |
There was a problem hiding this comment.
Allocating a large intermediate buffer mid_o using torch.empty on every call to extend_attention_fwd can introduce significant overhead and memory fragmentation, especially in high-throughput verify workloads. It is highly recommended to use a pre-allocated workspace buffer managed by the attention backend to avoid repeated allocations.
|
Thanks @fanxingran. Closing this because it has had no updates in 104 days. Reopen it if the work is still relevant. Some directories moved recently, so an older branch may need retargeting: |
Add a HIP few-query GQA packed split-K attention path for EAGLE/MTP verify workloads, reducing repeated KV loads and improving long-context verify performance on AMD GPUs.
Motivation
SGLang's Triton
extend_attention_fwdis currently a bottleneck for speculative decoding verify workloads on AMD ROCm, especially for EAGLE3 / MTP style shapes where a small number of draft tokens attend over a long paged KV prefix.For GQA models, the existing kernel launches one program per query head. This causes every query head in the same GQA group to re-read the same KV head from HBM, creating up to
kv_group_numtimes redundant memory traffic. The default few-query tiling is also suboptimal on HIP:BLOCK_M=64wastes most rows when the verify length is only a few tokens, andwaves_per_eu=1does not hide memory latency well on long-prefix workloads.This PR optimizes the AMD few-query GQA verify path by reducing redundant KV reads and improving GPU occupancy. On MI355X with MiniMax-M2.5, fp8 KV, and EAGLE3 verify shapes, the optimized path improves the extend-attention microbenchmark from about 1598us to 331us per call, and reduces long-context E2E median ITL from about 32.6ms to 14.2ms, while preserving the existing behavior for general extend/prefill paths.
Modifications
This PR updates
python/sglang/srt/layers/attention/triton_ops/extend_attention.py.The main changes are:
Add HIP-specific few-query tuning for
extend_attention_fwd. When running on HIP withmax_len_extend <= 16, the kernel usesBLOCK_M=16,BLOCK_N=128,num_warps=4, andwaves_per_eu=4, which better matches EAGLE3 / MTP verify shapes.Add a GQA-grouped split-K extend-attention path for AMD few-query verify workloads. The new
_fwd_kernel_gqa_splitkernel computes prefix attention by grouping all query heads that share the same KV head, so each KV head is loaded once instead of once per query head. The long prefix is split across multiple KV splits to preserve GPU occupancy.Add
_fwd_kernel_gqa_combineto reduce the per-split online-softmax states and then compute the small stage-2 extend-token triangle before storing the final output.Add host-side dispatch and intermediate buffer allocation for the optimized GQA path. The new path is enabled only for HIP few-query GQA shapes without sliding window attention, xai temperature, or prefix custom masks. Other shapes continue to use the existing
_fwd_kernel.Add environment knobs for benchmarking and retuning:
EXT_GQA_SPLITSto tune the number of KV splitsEXT_GQA_BLOCK_Nto tune the prefix block sizeEXT_GQA_WPEto tune HIPwaves_per_euThe CUDA path and unsupported attention feature combinations are left unchanged by the gating logic.
Accuracy Tests
Speed Tests and Profiling
off eagle



on eagle
on eagle + Optimize AMD GQA verify attention path
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ❌ Run #26392134415
Latest PR Test (Extra): ❌ Run #26392134359