Skip to content

[WebGPU] Fuse unmasked ViT attention into MultiHeadAttention - #33854

Open
xhcao wants to merge 8 commits into
microsoft:mainfrom
xhcao:webgpu-vit-mha-fusion
Open

xhcao wants to merge 8 commits into
microsoft:mainfrom
xhcao:webgpu-vit-mha-fusion

Conversation

@xhcao

@xhcao xhcao commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Description

Adds MultiHeadAttentionFusion, a Level2 graph transformer for the WebGPU EP. It is registered only when the WebGPU EP has a com.microsoft.MultiHeadAttention kernel. It rewrites the unmasked scaled-dot-product attention that ViT/DINOv2 exporters emit with separate Q/K/V projections:

Q/K/V -> Reshape[*,*,N,H] -> Transpose (K: 0,2,3,1) -> MatMul(Q, K^T) -> Div|Mul(scalar) -> Softmax(last axis)
      -> MatMul(V) -> Transpose(0,2,1,3) -> Reshape

into com.microsoft.MultiHeadAttention (num_heads = N, scale taken from the Div/Mul). Each projection gets a Reshape[0,0,-1] back to BSD; WebGPU's Reshape aliases its input, so this costs nothing. The output Reshape is kept and reads the MHA output, which has the same elements in the same order.

Motivation and Context

On WebGPU these ViT attention blocks run as separate kernels that write and re-read the full score tensor.

xhcao and others added 4 commits November 2, 2026 12:29
Add MultiHeadAttentionFusion, which rewrites the scaled dot-product
attention emitted by ViT/DINOv2 exporters with separate Q/K/V projections

  Reshape -> Transpose -> MatMul(Q, K^T) -> Div|Mul -> Softmax
          -> MatMul(V) -> Transpose -> Reshape

into com.microsoft.MultiHeadAttention so the WebGPU EP runs it through
FlashAttention instead of materializing the [B, N, S, S] score tensor.

The fusion is registered at Level2 for the WebGPU EP only. It is skipped
when the head size is not a multiple of 8 (the non-flash MHA path is
slower and does not support graph capture) and when the query length is
statically known to be below 32 (the split-reduce decode kernel is slower
than the unfused graph there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	onnxruntime/core/optimizer/graph_transformer_utils.cc
Copilot AI balanced review requested due to automatic review settings November 4, 2026 06:31
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@xhcao

xhcao commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Jie Chen (@jchen10), PTAL, thanks.

<style> </style>
models performance(%)
depth-anything-base-fp32.onnx -33.2
depth-anything-base-fp16-pow-fp32.onnx -12.5
sam-vit-b-decoder-static-fp16-demo -5.8

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.

🟡 Changes recommended

Missing adapter head-count limits and batch-compatibility checks allow fusion to turn valid attention graphs into fused nodes that fail at runtime.

3 open issues
What changed in this PR

Adds a Level2 WebGPU graph optimization that replaces unmasked ViT-style attention with com.microsoft.MultiHeadAttention, aiming to avoid materializing the full attention-score tensor.

Changes:

  • Matches separate Q/K/V attention patterns and folds scalar scaling into MHA.
  • Registers the fusion only when WebGPU exposes the required kernel.
  • Adds graph-matching, rejection, and fused-versus-unfused numerical tests.
File Description
onnxruntime/​test/​optimizer/​graph_transform_test.cc Adds fusion coverage and a WebGPU numerical comparison.
onnxruntime/​core/​optimizer/​multi_head_attention_fusion.h Declares the transformer and documents its supported pattern.
onnxruntime/​core/​optimizer/​multi_head_attention_fusion.cc Implements pattern matching, eligibility checks, and graph rewriting.
onnxruntime/​core/​optimizer/​graph_transformer_utils.cc Registers the fusion and shares WebGPU kernel-availability checks.

Review effort: Balanced

xhcao and others added 3 commits November 4, 2026 14:45
Projecting the same tensor only proves equal batches when the projection
keeps the leading dims. A constant weight or bias with its own leading
dims (e.g. a [2, D, D] Q weight on a [1, S, D] input) broadcasts the Q
batch, so the fused MultiHeadAttention would reject the unequal batches
at run time.

- Decide on statically comparable inferred batch dims first, so known
  conflicts are rejected.
- Only follow projections with a 2D constant weight and a bias of rank 1
  or less when falling back to the shared-source proof.
- Add regression tests for a batched Q weight (static and dynamic dims),
  a batched Q bias, and a 1D bias that still fuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WebGPU MultiHeadAttention kernel rejects num_heads above
maxComputeInvocationsPerWorkgroup, which is only guaranteed to be 256,
and the adapter limit is not known at graph optimization time. A graph
with more heads that passes the other guards would fuse and then fail
at run time, so skip it.

Make the test head count configurable and add a 257-head regression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 16-key K/V tile estimate matches the WebGPU dense-prefill storage
calculation on every adapter except Qualcomm, whose extra output tile can
exceed 16 KB on 16 KB adapters. The adapter is not known at graph
optimization time, and a conservative bound covering that case would
stop fusing fp32 head size 64 everywhere, while a miss only costs speed.

Co-Authored-By: Claude Opus 5.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.

🟡 Changes recommended

The fusion’s batch-equality checks incorrectly accept inferred batch dimensions and transposed-A Gemm chains, converting valid broadcastable attention graphs into fused operations that reject the unequal batches.

2 open issues
3 resolved since last review

Review effort: Balanced

Comment on lines +105 to +107
if (node == nullptr || (node->OpType() != "MatMul" && node->OpType() != "Gemm") || node->InputDefs().size() < 2) {
return nullptr;
}
Comment on lines +139 to +140
return a_source != nullptr && a_source == GetProjectionSource(graph, b) && GetConstantShape(graph, a, a_dims) &&
GetConstantShape(graph, b, b_dims) && a_dims[0] == b_dims[0] && a_dims[1] == b_dims[1];
- Drop the Gemm branch from the projection-source walk. MatMulAddFusion
  only emits Gemm for static shapes, whose batch dims are compared
  directly, so the branch was never consulted.
- Check the computed scale for finiteness, which also catches a tiny
  divisor whose reciprocal overflows.
- Require an explicit EP set in the constructor, since the guards are
  specific to the WebGPU kernel.
- Avoid a const_cast when wiring the BSD Reshapes.
- Name the kernel functions the WebGPU guards mirror.
- Tests: add a current-opset regression, the unscaled MatMul -> Softmax
  form and an overflowing scale; check the Reshape count and that
  MultiHeadAttention reads the [0, 0, -1] Reshapes; clarify which guard
  rejects the static batched-weight case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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