Fix packed-QKV and broadcast-head bias strides in quantized GQA flash attention - #28963
Conversation
…ized GQA The quantized KV-cache flash-attention path used a hardcoded Q batch stride (num_heads*S*H) and a num_heads-based attention-bias batch stride, producing incorrect results for batch_size > 1 with packed QKV or [B, 1, S, T] bias. Add a caller-supplied q_batch_stride to MlasFlashAttentionQuantizedKVArgs and compute the bias batch stride from the actual head extent (1 when the head dim is broadcast). Mirrors the FP32 fixes from #28962. Adds a regression test for batch > 1 with head-broadcast bias.
There was a problem hiding this comment.
Pull request overview
This PR fixes two correctness issues in the CPU quantized KV-cache flash-attention path for the com.microsoft.GroupQueryAttention contrib op that can produce wrong results when batch_size > 1: (1) query (Q) batch stride handling for packed QKV layouts, and (2) attention-bias batch stride computation when the head dimension is broadcast (bias shape [B, 1, S, T]). It also adds a Python regression test to cover the previously-missed multi-batch broadcast-head bias case.
Changes:
- Add
q_batch_stridetoMlasFlashAttentionQuantizedKVArgsand use it in the quantized flash-attention kernels to support both standard and packed-QKV layouts. - Fix attention-bias batch-stride calculation in quantized flash attention to account for head broadcasting (
bias_head_extent = 1when broadcast). - Add a new quantized Python regression test for
[B, 1, S, T]bias withbatch_size > 1.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
onnxruntime/core/mlas/inc/mlas_qkv_quant.h |
Extends quantized flash-attention args with q_batch_stride for correct Q addressing across batches/layouts. |
onnxruntime/core/mlas/lib/flashattn_qkv.cpp |
Uses caller-provided Q batch stride and fixes bias batch stride when head dim is broadcast (tiled + decoding paths). |
onnxruntime/contrib_ops/cpu/bert/gqa_attention_base.h |
Wires q_batch_stride from the op dispatch and fixes per-batch bias slicing stride for broadcast-head bias. |
onnxruntime/test/python/transformers/test_gqa_cpu_quantized.py |
Adds a multi-batch regression test for broadcast-head attention bias. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Issues FoundMinor: q_batch_stride in unified path not using SafeInt — In gqa_attention_base.h line 834, the packed case uses static_cast<size_t>(packed_batch_stride) without SafeInt, while the unpacked case correctly uses SafeInt<size_t>(num_heads_) * sequence_length * head_size. The packed_batch_stride is already computed with SafeInt elsewhere (so it's fine in practice), but it's inconsistent. Very minor. Style nit: Line 900 is fairly long (batch_bias += static_cast<size_t>(SafeInt<size_t>(b) * bias_head_extent * sequence_length * attention_bias_seqlen_stride);). Could benefit from breaking across lines, but this is purely cosmetic. Negative tests / edge cases:No explicit negative tests (e.g., invalid input shapes). This is acceptable — the PR fixes stride bugs, not input validation. Test gap: No test for packed-QKV combined with broadcast-head bias and batch_size > 1. This would exercise both fixes simultaneously. However, the two bugs are independent (different code paths), so separate tests are adequate. Approve. The PR correctly fixes two real bugs that produce incorrect attention output for batch_size > 1 in the quantized GQA flash-attention path. The fixes are the exact counterpart of what was already landed in PR #28962 for the FP32 path. Arithmetic uses SafeInt where it matters, all review comments have been addressed, and the new regression tests directly target the fixed bugs. No memory safety, OOB, or exception safety concerns. |
Review — multi-model review teamI ran a parallel review (readability, code, correctness, deep-semantic, and cross-module integration). Overall: the fix is correct, complete, and well-tested. No blocking issues found. Findings below are all Minor/Nit or optional follow-ups. Correctness — verified ✅
Cross-module integration — verified ✅Every consumer of Minor
Nits
Optional follow-up (pre-existing, not introduced here)
Reviewed by a multi-model team (Claude Opus, GPT-5.x, Gemini 3 Pro). All correctness/invariant claims grounded in the source; no test runs were needed for the addressing math. |
… attention (#28963) ## Description The quantized KV-cache flash-attention path for the CPU `GroupQueryAttention` contrib op carried two latent batch-stride bugs that produced incorrect results for `batch_size > 1`. This PR ports the fixes already landed for the FP32 path (PR #28962) to the quantized path, and adds a regression test that exercises the previously-uncovered scenario. ## Summary of Changes ### Bug fixes | File | Change | |------|--------| | `onnxruntime/core/mlas/inc/mlas_qkv_quant.h` | Add `q_batch_stride` field to `MlasFlashAttentionQuantizedKVArgs` so the kernel uses a caller-supplied Q batch stride instead of assuming the unpacked `num_heads*S*H` layout. | | `onnxruntime/core/mlas/lib/flashattn_qkv.cpp` | Use `args->q_batch_stride` for the Q pointer in both the main tiled kernel and the flash-decoding kernel; compute the attention-bias batch stride from `bias_head_extent = broadcast_head ? 1 : num_heads` (two sites) instead of always using `num_heads`. | | `onnxruntime/contrib_ops/cpu/bert/gqa_attention_base.h` | Set `q_batch_stride` to `(num_heads + 2*kv_num_heads)*S*H` for packed QKV (else `num_heads*S*H`) in the unified dispatch; correct the per-batch Q offset and bias slice to use the packed stride and head extent. | ### Tests - Add `test_int8_bias_broadcast_head_multi_batch` to `test_gqa_cpu_quantized.py` covering `[B, 1, S, T]` bias with `batch_size > 1`. The existing `test_int8_bias_broadcast_head` used `batch_size == 1`, which masked the head-broadcast batch-stride bug. ## Testing - `cd onnxruntime/test/python/transformers && python -m pytest test_gqa_cpu_quantized.py -q` → 21 passed, 2 skipped. - Verified the new test catches the bug: temporarily reverting the bias-stride fix makes `test_int8_bias_broadcast_head_multi_batch` fail (mismatches starting at batch index 1) while the `batch_size == 1` variant still passes; restoring the fix makes all tests pass. - `lintrunner -a` → no lint issues. ## Motivation and Context Follow-up to PR #28962, which fixed the identical two bugs (packed-QKV Q batch stride and attention-bias head-broadcast batch stride) in the non-quantized FP32 CPU GQA path. The quantized path was left untouched there as out of scope; existing quantized parity tests did not cover `batch > 1` with `[B, 1, S, T]` bias, so the bugs went undetected.
Description
The quantized KV-cache flash-attention path for the CPU
GroupQueryAttentioncontrib op carried two latent batch-stride bugs that produced incorrect results forbatch_size > 1. This PR ports the fixes already landed for the FP32 path (PR #28962) to the quantized path, and adds a regression test that exercises the previously-uncovered scenario.Summary of Changes
Bug fixes
onnxruntime/core/mlas/inc/mlas_qkv_quant.hq_batch_stridefield toMlasFlashAttentionQuantizedKVArgsso the kernel uses a caller-supplied Q batch stride instead of assuming the unpackednum_heads*S*Hlayout.onnxruntime/core/mlas/lib/flashattn_qkv.cppargs->q_batch_stridefor the Q pointer in both the main tiled kernel and the flash-decoding kernel; compute the attention-bias batch stride frombias_head_extent = broadcast_head ? 1 : num_heads(two sites) instead of always usingnum_heads.onnxruntime/contrib_ops/cpu/bert/gqa_attention_base.hq_batch_strideto(num_heads + 2*kv_num_heads)*S*Hfor packed QKV (elsenum_heads*S*H) in the unified dispatch; correct the per-batch Q offset and bias slice to use the packed stride and head extent.Tests
test_int8_bias_broadcast_head_multi_batchtotest_gqa_cpu_quantized.pycovering[B, 1, S, T]bias withbatch_size > 1. The existingtest_int8_bias_broadcast_headusedbatch_size == 1, which masked the head-broadcast batch-stride bug.Testing
cd onnxruntime/test/python/transformers && python -m pytest test_gqa_cpu_quantized.py -q→ 21 passed, 2 skipped.test_int8_bias_broadcast_head_multi_batchfail (mismatches starting at batch index 1) while thebatch_size == 1variant still passes; restoring the fix makes all tests pass.lintrunner -a→ no lint issues.Motivation and Context
Follow-up to PR #28962, which fixed the identical two bugs (packed-QKV Q batch stride and attention-bias head-broadcast batch stride) in the non-quantized FP32 CPU GQA path. The quantized path was left untouched there as out of scope; existing quantized parity tests did not cover
batch > 1with[B, 1, S, T]bias, so the bugs went undetected.