Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a3b0e64
[WebGPU] Add opset 24 kernel registrations for Cast and Shape ops
feich-ms May 14, 2026
b6c0c5d
Address review: fix dummy-K buffer size and add const_cast safety guard
feich-ms May 14, 2026
000c9d1
Fix MHA CrossAttention regression: use num_heads_ for non-GQA paths
feich-ms May 14, 2026
b943678
Add ORT_ENFORCE for past KV non-null in kv_empty path
feich-ms May 18, 2026
bf017c8
[WebGPU] Optimize GQA kv_empty path: Q-only extraction and rotary
feich-ms May 19, 2026
8b65eea
Apply suggestion from @qjia7
feich-ms May 25, 2026
e57dee1
Skip wasteful present_key/value allocation for kv_empty path
feich-ms May 25, 2026
194cc08
update comments
feich-ms May 25, 2026
c3f727d
Apply suggestion from @github-actions[bot]
feich-ms May 25, 2026
9ecffd4
Remove model-specific references from code comments
feich-ms May 25, 2026
334c3d5
Simplify WebGPU shared KV tests and error message
feich-ms May 25, 2026
587a177
Fix CI: add use_webgpu parameter to shared KV test helpers
feich-ms May 25, 2026
e98641e
Optimize Q-only extraction dispatch size for shared KV layers
feich-ms May 25, 2026
a7c5929
Remove unused ExtractQFromPackedQKV and q_only shader path
feich-ms May 25, 2026
2097f03
simplify RunRotaryEmbeddingQOnly impl
feich-ms May 25, 2026
8624ea1
Fix kv_empty layers failing with sliding window on long sequences
feich-ms May 26, 2026
fc0bf73
update comments
feich-ms May 26, 2026
dcb6c64
Replace RunRotaryEmbeddingQOnly with standalone RotaryEmbeddingWithOf…
feich-ms May 27, 2026
ae1c542
Add rotary prefill and multi-batch tests for RotaryEmbeddingWithOffse…
feich-ms May 27, 2026
1644ba9
Fix code formatting per linter suggestions
feich-ms May 27, 2026
3e25e15
Reuse RotaryEmbeddingProgram for GQA kv_empty rotary path
feich-ms Jun 1, 2026
182f689
Compute rotary position in shader via position_offset uniform
feich-ms Jun 1, 2026
4234919
Fix missing position_offset uniform in contrib RotaryEmbedding caller
feich-ms Jun 2, 2026
e56f00a
Revert GenerateShaderCode to single if-else structure
feich-ms Jun 2, 2026
9f3383c
fix white spaces issue
feich-ms Jun 2, 2026
08240cb
reshape the code structure of GenerateShaderCode
feich-ms Jun 2, 2026
33057d4
fix CI failures
feich-ms Jun 2, 2026
9363e77
fix lint error
feich-ms Jun 2, 2026
e1a64e1
Fix shader input order in RotaryEmbeddingProgram
feich-ms Jun 2, 2026
c5f5f8b
Refactor RotaryEmbedding: share RunRotaryEmbedding across GQA and ONN…
feich-ms Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion onnxruntime/contrib_ops/webgpu/bert/attention_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ struct WebgpuAttentionParameters {
explicit WebgpuAttentionParameters(onnxruntime::contrib::GroupQueryAttentionParameters parameters) : is_gqa_(true),
batch_size_(parameters.batch_size),
sequence_length_(parameters.sequence_length),
kv_sequence_length_(parameters.sequence_length),
kv_sequence_length_(parameters.kv_sequence_length),
past_sequence_length_(parameters.seqlen_past_kv_cache),
total_sequence_length_(parameters.total_sequence_length),
hidden_size_(parameters.hidden_size),
Expand Down
44 changes: 33 additions & 11 deletions onnxruntime/contrib_ops/webgpu/bert/flash_attention.cc
Original file line number Diff line number Diff line change
Expand Up @@ -422,26 +422,25 @@ Status ApplyFlashAttention(const Tensor* Q, const Tensor* K, const Tensor* V, co
const Tensor* cos_cache, const Tensor* sin_cache, const Tensor* head_sink) {
constexpr uint32_t tile_size = 64;

// Create present_key and present_value tensors if they are nullptr
// Create present_key and present_value tensors if they are nullptr.
// Skip allocation for kv_empty — present will be aliased to past below.
Tensor internal_present_key;
Tensor internal_present_value;
if (present_key == nullptr) {
TensorShapeVector present_kv_shape({parameters.batch_size_, parameters.num_heads_,
const int present_kv_heads = parameters.is_gqa_ ? parameters.kv_num_heads_ : parameters.num_heads_;
const bool kv_empty = (parameters.kv_sequence_length_ == 0);
if (!kv_empty && present_key == nullptr) {
TensorShapeVector present_kv_shape({parameters.batch_size_, present_kv_heads,
parameters.total_sequence_length_, parameters.head_size_});
internal_present_key = context.CreateGPUTensor(Q->DataType(), TensorShape(present_kv_shape));
present_key = &internal_present_key;
}
if (present_value == nullptr) {
TensorShapeVector present_kv_shape({parameters.batch_size_, parameters.num_heads_,
if (!kv_empty && present_value == nullptr) {
TensorShapeVector present_kv_shape({parameters.batch_size_, present_kv_heads,
parameters.total_sequence_length_, parameters.head_size_});
internal_present_value = context.CreateGPUTensor(Q->DataType(), TensorShape(present_kv_shape));
present_value = &internal_present_value;
}

// Extract present_sequence_length directly from present_key tensor shape:
// (batch_size, num_heads, total_sequence_length/max_sequence_length, head_size)
const uint32_t present_sequence_length = static_cast<uint32_t>(present_key->Shape()[2]);

const bool use_seqlen_k = seqlen_k != nullptr && context.IsGraphCaptureEnabled();

// Declare query_output at function scope to ensure it persists throughout the function
Expand All @@ -452,7 +451,8 @@ Status ApplyFlashAttention(const Tensor* Q, const Tensor* K, const Tensor* V, co
Tensor indirect_buffer;

// Prepare indirect dispatch buffer for decode path with static KV cache
const bool use_indirect_dispatch = parameters.sequence_length_ == 1 &&
const bool use_indirect_dispatch = !kv_empty &&
parameters.sequence_length_ == 1 &&
parameters.past_present_share_buffer_ &&
seqlen_k != nullptr &&
context.IsGraphCaptureEnabled();
Expand All @@ -464,7 +464,24 @@ Status ApplyFlashAttention(const Tensor* Q, const Tensor* K, const Tensor* V, co

const bool do_rotary = (cos_cache != nullptr && sin_cache != nullptr);

if (do_rotary) {
if (kv_empty) {
// kv_sequence_length==0: K/V inputs are empty (shared KV layer).
// Skip CopyKVCache and fused split+rotary+copyKV.
// Use past_key/past_value directly as the present buffers for attention.
// Note: do_rotary is always false here because GQA passes cos_cache=nullptr, sin_cache=nullptr
// for kv_empty layers (rotary is applied to Q separately in GQA before calling ApplyFlashAttention).
ORT_ENFORCE(!do_rotary, "Fused SplitPackedQKVWithRotaryEmbeddingAndCopyKV should not be used with kv_sequence_length==0.");
Comment thread
feich-ms marked this conversation as resolved.
ORT_ENFORCE(past_key != nullptr && past_value != nullptr,
"kv_empty path requires past KV context (KV-shared layers reuse another layer's cache).");
// When past_present_share_buffer_ is true (MayInplace optimization), present already
// shares the past buffer. No aliasing needed — the data is already in place.
if (!parameters.past_present_share_buffer_) {
// Alias past as present — flash attention only reads present_key/present_value,
// and CopyKVCache is skipped when kv_empty, so no writes occur through these pointers.
present_key = const_cast<Tensor*>(past_key);
present_value = const_cast<Tensor*>(past_value);
}
} else if (do_rotary) {
ORT_ENFORCE(parameters.is_packed_qkv_, "Fused SplitPackedQKVWithRotaryEmbeddingAndCopyKV requires packed QKV input.");
ORT_ENFORCE(parameters.past_present_share_buffer_, "Fused SplitPackedQKVWithRotaryEmbeddingAndCopyKV requires static KV cache.");

Expand All @@ -481,6 +498,11 @@ Status ApplyFlashAttention(const Tensor* Q, const Tensor* K, const Tensor* V, co
ORT_RETURN_IF_ERROR(CopyKVCache(context, parameters, K, past_key, present_key, V, past_value, present_value, tile_size, use_seqlen_k ? seqlen_k : nullptr, indirect_buffer_ptr));
}

// Extract present_sequence_length directly from present_key tensor shape
// after kv_empty aliasing ensures present_key is valid:
// (batch_size, num_heads, total_sequence_length/max_sequence_length, head_size)
const uint32_t present_sequence_length = static_cast<uint32_t>(present_key->Shape()[2]);

if (parameters.sequence_length_ > 1) {
bool has_attention_bias = attention_bias != nullptr;
bool is_qualcomm = context.AdapterInfo().vendor == std::string_view{"qualcomm"};
Expand Down
55 changes: 45 additions & 10 deletions onnxruntime/contrib_ops/webgpu/bert/group_query_attention.cc
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,12 @@
Tensor* present_key = context.Output(1, present_kv_shape);
Tensor* present_value = context.Output(2, present_kv_shape);

// WebGPU flash attention requires present_key/present_value as working KV buffers.
if (present_key == nullptr || present_value == nullptr) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"WebGPU GroupQueryAttention requires present_key and present_value outputs. "
"Optional present outputs are supported on CPU and CUDA EPs only.");
}

parameters.past_present_share_buffer_ = past_key != nullptr && past_value != nullptr && past_key->DataRaw() == present_key->DataRaw() && past_value->DataRaw() == present_value->DataRaw();
// When present_key/present_value outputs are not requested (nullptr), this is a
// KV-shared layer. Flash attention will create internal GPU buffers as needed.
parameters.past_present_share_buffer_ = present_key != nullptr && present_value != nullptr &&
past_key != nullptr && past_value != nullptr &&
past_key->DataRaw() == present_key->DataRaw() &&
past_value->DataRaw() == present_value->DataRaw();

ORT_ENFORCE(parameters.total_sequence_length_ <= parameters.seqlen_present_kv_cache_, "Total sequence length cannot be greater than the existing KV cache length.");

Expand All @@ -259,17 +257,47 @@
Tensor qRotary;
Tensor kRotary;

// kv_sequence_length==0 fast path: K/V inputs are empty (shared KV layer).
// Skip all K/V processing; only apply RoPE to Q if needed.
// Use past_key/past_value directly as the KV context.
const bool kv_empty = (parameters.kv_sequence_length_ == 0);

// Use a sliding window if the total sequence exceeds the window's length.
bool use_sliding_window = (local_window_size_ != -1 && local_window_size_ < parameters.total_sequence_length_);
bool will_use_flash_attention = false;
if (!use_smooth_softmax_ && !use_sliding_window) {
// For kv_empty layers (shared KV), sliding window is irrelevant — there's no new KV to window
// over, the layer reuses another layer's already-computed KV cache. Flash attention is required
// for these layers, so we bypass the sliding window check to allow it.
if (!use_smooth_softmax_ && (!use_sliding_window || kv_empty)) {
// Create a temporary parameters copy with is_packed_qkv_ set to false to check if flash attention can be applied after unpacking
WebgpuAttentionParameters temp_params = parameters;
temp_params.is_packed_qkv_ = false;
will_use_flash_attention = CanApplyFlashAttention(temp_params, context);
}

if (parameters.is_packed_qkv_ && do_rotary_) {
if (kv_empty) {
// KV inputs are empty - shared KV layer. Only need to optionally apply RoPE to Q.
ORT_ENFORCE(!parameters.is_packed_qkv_, "Packed QKV is not supported with kv_sequence_length==0 (shared KV layers).");
if (do_rotary_) {
// Apply RoPE to Q only — K doesn't need rotation since we reuse another layer's already-rotated KV cache.
qRotary = context.CreateGPUTensor(query->DataType(), query->Shape());
// Query is BSD (3 dims): [batch, sequence, hidden]. Strides for bsnh layout:
// {batch_stride, hidden_size, head_size, 1}.
const auto batch_stride = static_cast<uint32_t>(parameters.sequence_length_ * parameters.hidden_size_);
const std::vector<uint32_t> q_input_output_strides{

Check warning on line 287 in onnxruntime/contrib_ops/webgpu/bert/group_query_attention.cc

View workflow job for this annotation

GitHub Actions / Optional Lint C++

[cpplint] reported by reviewdog 🐶 Add #include <vector> for vector<> [build/include_what_you_use] [4] Raw Output: onnxruntime/contrib_ops/webgpu/bert/group_query_attention.cc:287: Add #include <vector> for vector<> [build/include_what_you_use] [4]
batch_stride,
static_cast<uint32_t>(parameters.hidden_size_),
static_cast<uint32_t>(parameters.head_size_),
1u};
ORT_RETURN_IF_ERROR(RunRotaryEmbedding(context,
query, seqlen_k, cos_cache, sin_cache, &qRotary,
parameters.batch_size_, parameters.sequence_length_,
parameters.hidden_size_, parameters.head_size_,
parameters.scale_, parameters.rotary_interleaved_,
/*use_seqlens_for_position=*/true, q_input_output_strides));
query = &qRotary;
}
} else if (parameters.is_packed_qkv_ && do_rotary_) {
// Use the ultimate fused operation when FlashAttention and static KV cache is enabled.
if (will_use_flash_attention && parameters.past_present_share_buffer_) {
// Directly call ApplyFlashAttention with fused split/rotary/copyKV enabled
Expand Down Expand Up @@ -322,6 +350,13 @@
present_value, parameters, context, seqlen_k, nullptr, nullptr, head_sink);
}

// Non-flash attention path does not support kv_sequence_length==0 (shared KV layers).
if (kv_empty) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"WebGPU non-flash attention path does not support kv_sequence_length==0 (shared KV layers). "
"Flash attention is required for KV-shared decoder layers.");
}

TensorShapeVector q_new_dims({parameters.batch_size_, parameters.num_heads_,
parameters.sequence_length_, parameters.head_size_});
TensorShape q_new_shape(q_new_dims);
Expand Down
Loading
Loading