Skip to content

[CUDA] Support attention_bias in GroupQueryAttention via the unfused path - #29525

Merged
tianleiwu merged 5 commits into
microsoft:mainfrom
jiangzhuo:gqa-attention-bias-cuda
Jul 8, 2026
Merged

[CUDA] Support attention_bias in GroupQueryAttention via the unfused path#29525
tianleiwu merged 5 commits into
microsoft:mainfrom
jiangzhuo:gqa-attention-bias-cuda

Conversation

@jiangzhuo

Copy link
Copy Markdown
Contributor

Description

com.microsoft.GroupQueryAttention's optional attention_bias input (input #10) is implemented by the CPU EP (#23944) and the WebGPU EP (#25285, #26769), but the CUDA EP rejects it at runtime. This PR wires it through.

No new kernel is needed: the unfused GQA fallback added for #28195 calls LaunchUnfusedAttention, whose kernel already implements an additive bias with dim-0/dim-1 broadcast, per-batch seqlens_k, causal/sliding-window masking and softcap — the op just passed attn_bias=nullptr. The change is dispatch plumbing:

  • group_query_attention.cc — remove the blanket rejection; validate the bias element type; set broadcast_attn_bias_dim_0/1 from the bias shape (the fields already exist on AttentionParameters); add !has_attention_bias to the XQA / cuDNN SDPA / flash / flash-fast-decode / MEA eligibility so bias-carrying nodes dispatch to the unfused fallback; set data.attention_bias.
  • attention_data.h — add the attention_bias pointer to GroupQueryAttentionData.
  • group_query_attention_impl.cu — pass the real pointer and broadcast flags in UnfusedGqaAttention (previously hardcoded nullptr/false).

Why each fused path stays disqualified with a bias:

  • flash / flash fast-decode: flash_api.h has no bias parameter (same exclusion as MHA and the ONNX Attention-op CUDA kernel).
  • XQA: no bias parameter.
  • cuDNN SDPA: GQA's cuDNN path is bottom-right causal, which cuDNN documents as incompatible with a bias (multihead_attention.cc has the same restriction).
  • cutlass MEA: the wrapper computes the bias row stride from kv_sequence_length, which GQA sets to the KV-cache capacity (seqlen_present_kv_cache) rather than total_sequence_length, so rows would be misaligned under past/present buffer sharing. Left for a follow-up (needs an explicit attn_bias_strideM in MemoryEfficientAttentionParams).

Kept NOT_IMPLEMENTED (explicit, clear errors instead of the previous blanket rejection): bias × quantized KV cache (unfused requires T == U), bias × smooth-softmax/head_sink.

Tests

New TestGQAAttentionBias in test_gqa.py: prompt and past/decode parity across packed/unpacked QKV, shared/separate KV buffer, rotary, odd head sizes (40/80), and a subsequent multi-token prompt. The bias is non-zero random so a kernel that silently ignores the input fails parity (the harness previously modeled a zeros bias). Also fixes the test graph builder declaring the bias input's last dim as the KV-cache capacity instead of total_sequence_length (the shape the op validates).

Full test_gqa.py suite: 476 tests pass, no regressions (SM89, CUDA 12.8).

Motivation and Context

Fixes #29506.

Transformers.js-exported speech models carry non-causal attention patterns as attention_bias and currently cannot run on the CUDA EP at all, while running fine on WebGPU and CPU:

End-to-end validation with this patch on an RTX 4070 SUPER: the full Voxtral-Mini-4B-Realtime streaming pipeline (q4f16) transcribes correctly at RTF 0.23 (31 s clip, 25.6 tok/s sustained decode, 6.4 GB VRAM) — on this workload the unfused-attention path outperforms the same model on the WebGPU EP (RTF 0.26).

(While validating, an unrelated pre-existing issue surfaced: GroupQueryAttentionFusion breaks graphs whose GQA nodes carry >9 inputs — filed as #29524.)

…path

The GQA CUDA op rejected the optional attention_bias input (input 10)
outright, while the CPU and WebGPU EPs implement it. The unfused
fallback kernel (LaunchUnfusedAttention, issue microsoft#28195) already takes an
additive bias with dim-0/dim-1 broadcast and per-batch seqlens - the op
just never passed it. This wires the bias through:

- group_query_attention.cc: drop the blanket rejection; validate bias
  element type; set broadcast_attn_bias_dim_0/1 from the bias shape;
  disqualify the bias-incapable fused paths (XQA, cuDNN SDPA, flash,
  flash fast-decode, cutlass MEA) so dispatch reaches the unfused
  fallback; keep bias x quantized-KV and bias x smooth-softmax/head_sink
  as NOT_IMPLEMENTED.
- attention_data.h: add attention_bias pointer to GroupQueryAttentionData.
- group_query_attention_impl.cu: pass the real bias pointer and
  broadcast flags in UnfusedGqaAttention.

MEA is left disqualified for now: the cutlass wrapper derives the bias
row stride from kv_sequence_length, which GQA sets to the KV-cache
capacity rather than total_sequence_length, so it would read misaligned
rows under past/present buffer sharing. Can be enabled later with an
explicit bias stride.

Tests: TestGQAAttentionBias (prompt + past/decode, packed/unpacked,
shared/separate KV buffer, rotary, odd head sizes, subsequent prompt)
with non-zero random bias so a kernel ignoring the input fails parity.
Also fixes the test builder declaring the bias input with the KV-cache
capacity instead of total_sequence_length as its last dim.

Fixes microsoft#29506

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 3, 2026 04:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 adds CUDA EP support for the optional attention_bias input of com.microsoft.GroupQueryAttention by routing bias-carrying nodes to the existing unfused attention fallback (the fused CUDA paths don’t accept a bias parameter).

Changes:

  • Plumbs attention_bias through CUDA GQA dispatch and data structures, and disables fused-path eligibility when bias is present so execution falls back to the unfused implementation.
  • Extends the unfused GQA CUDA path to pass the bias pointer and broadcast flags into LaunchUnfusedAttention.
  • Updates test_gqa.py to declare the bias shape correctly for past/present and adds focused CUDA parity coverage with non-zero random bias.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc Accepts/validates attention_bias, carries broadcast flags, and ensures fused paths are skipped when bias is present.
onnxruntime/contrib_ops/cuda/bert/attention_data.h Adds attention_bias pointer to GroupQueryAttentionData.
onnxruntime/contrib_ops/cuda/bert/group_query_attention_impl.cu Passes attention_bias and broadcast flags into the unfused attention launch.
onnxruntime/test/python/transformers/test_gqa.py Fixes bias input shape declaration, uses non-zero bias generation, and adds focused CUDA parity tests.

Comment thread onnxruntime/test/python/transformers/test_gqa.py
Comment thread onnxruntime/test/python/transformers/test_gqa.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc
Comment thread onnxruntime/test/python/transformers/test_gqa.py

@tianleiwu tianleiwu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the dispatch plumbing that routes attention_bias-carrying GQA nodes to the unfused fallback. The approach is sound and the key correctness claims hold up against the code:

  • All four fused paths correctly gate on !has_attention_bias (XQA, cuDNN SDPA, flash — and flash-fast-decode transitively via use_flash_attention, plus MEA), so a bias-carrying node deterministically reaches UnfusedGqaAttention.
  • The unfused softmax indexes the bias row with stride total_kv_length (== parameters.total_sequence_length), which matches the bias tensor's last dim, and the per-batch seqlens_k cutoff keeps reads within [0, total_sequence_length). So the past/present buffer-sharing hazard that disqualifies MEA (row stride derived from the KV-cache capacity) does not apply on this path — the MEA exclusion rationale is accurate.
  • masked_bias_value stays at its default 0 for GQA (no composed mask sentinel), so the fully-masked-row guard degrades to the intended zero-row behavior via the s_max == -inf branch.

One test-coverage gap is worth addressing before merge (inline comment). I also concur with the two review notes already open on the current head, which still apply:

  • TestGQAAttentionBias is missing a CUDA skip guard; sibling CUDA classes such as TestGQAQKNorm all carry @unittest.skipIf(not has_cuda_device(80), ...). Without it, CPU-only / no-GPU runs will fail allocating CUDA tensors.
  • No NumDimensions() == 4 guard exists before the 4-D attention_bias shape is indexed (TensorShape::operator[] is unchecked, so a malformed rank-<4 bias is UB rather than a clean INVALID_ARGUMENT). This is a pre-existing gap in the shared CheckCustomAttentionInputs helper (also reached by the CPU/WebGPU EPs), but this PR is the first to exercise it on CUDA.

Comment thread onnxruntime/test/python/transformers/test_gqa.py
…coverage

- CheckCustomAttentionInputs: validate attention_bias is 4D before indexing
  its dims (TensorShape::operator[] is unchecked; a rank<4 bias was UB
  instead of a clean INVALID_ARGUMENT). Shared helper, so the CPU and WebGPU
  EPs get the same guard.
- TestGQAAttentionBias: add the missing skipIf gate (has_cuda_device(53),
  matching TestMemoryEfficientGQA — the unfused path has no arch floor).
  Fixes the CPU-torch CI failures.
- Cover both untested broadcast-flag combinations in BOTH suites:
  batch-broadcast biases (dim0 == 1, prompt + past incl. shared and separate
  KV buffer) and per-head biases (dim1 == num_heads, prompt + past). A
  batch-shared bias cannot carry the harness's per-batch -10000 tails, so
  the dim0-broadcast past cases skip them — passing parity there also
  verifies the kernel's seqlens_k cutoff and the reference mask never read
  past each batch's valid length.
Comment thread onnxruntime/test/python/transformers/test_gqa.py Fixed
tianleiwu
tianleiwu previously approved these changes Jul 5, 2026
@skottmckay

Copy link
Copy Markdown
Contributor

Can the C++ unit tests be updated to also validate the new code? AFAIK the python tests are only run post-merge, so a change that broke would not be detected in a PR.

onnxruntime\test\contrib_ops\group_query_attention_op_test.cc

The Python parity tests need a CUDA-enabled torch that the PR CI agents don't
have, so they only exercise the CUDA kernel post-merge. Add a C++ gtest that
runs in the GPU CI on every PR: it runs the same prompt case with a non-null
attention_bias on CUDA (fp16) and CPU (fp32, the reference EP that already
implements the input) and compares the outputs, covering the three bias shapes
that drive broadcast_attn_bias_dim_0/1 — [batch,1,S,S] (default), [1,1,S,S]
(dim0 broadcast) and [batch,heads,S,S] (per-head). Skips cleanly when no CUDA
EP is available.

Verified on SM 8.9: the new test passes at fp16 tolerance 0.02, full
GroupQueryAttentionTest suite 56 passed / 12 skipped (WebGPU) / 0 failed.
@jiangzhuo

jiangzhuo commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@tianleiwu the earlier review feedback (broadcast-flag coverage, the TestGQAAttentionBias CUDA skip guard, and the 4D rank guard in CheckCustomAttentionInputs) plus @skottmckay's request for PR-time C++ coverage are all addressed now. Could you approve-and-run the workflows on the latest commit when you get a chance? Fork PRs stay gated until a maintainer runs them.

Added in b3d4a79CudaAttentionBiasParityVsCpu in group_query_attention_op_test.cc. It runs the same prompt case with a non-null attention_bias on CUDA (fp16) and CPU (fp32, the reference EP that already implements the input) and compares the outputs, covering the three shapes that drive broadcast_attn_bias_dim_0/1: [batch, 1, S, S] (default), [1, 1, S, S] (dim0 broadcast) and [batch, num_heads, S, S] (per-head). It skips cleanly when no CUDA EP is available, so it runs in the GPU CI on every PR rather than only post-merge.

Verified locally on SM 8.9: passes at fp16 tolerance 0.02, and the full GroupQueryAttentionTest suite is 56 passed / 12 skipped (WebGPU) / 0 failed.

@jiangzhuo
jiangzhuo requested a review from tianleiwu July 7, 2026 18:55
@tianleiwu
tianleiwu enabled auto-merge (squash) July 8, 2026 02:52
@tianleiwu
tianleiwu merged commit 2cd5705 into microsoft:main Jul 8, 2026
86 checks passed
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.

[Feature Request] CUDA EP: support attention_bias in GroupQueryAttention (last EP missing it)

5 participants