Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0db9578
Update GQA to support Gemma4
apsonawane Apr 25, 2026
ec041db
Fix the op
apsonawane Apr 27, 2026
bcd8243
Merge branch 'main' into asonawane/gemma4
apsonawane Apr 27, 2026
7139053
Fix lint error
apsonawane Apr 27, 2026
0bf6394
Fix webgpu build
apsonawane Apr 27, 2026
3c07e1d
fix webgpu build
apsonawane Apr 27, 2026
f905196
Address copilot comments
apsonawane Apr 27, 2026
d2ead38
Make GQA present_key/present_value outputs optional for KV-shared layers
apsonawane Apr 28, 2026
aad74ef
Fix tests
apsonawane Apr 28, 2026
64005dd
Update the docs
apsonawane Apr 29, 2026
2b3a2ce
Address comments
apsonawane May 1, 2026
9a14803
Address copilot comments
apsonawane May 1, 2026
2ef269c
Address comments
apsonawane May 1, 2026
6cbe62c
Fix unit tests
apsonawane May 1, 2026
3db82c6
Fix comments
apsonawane May 1, 2026
bd023f1
Address comments
apsonawane May 4, 2026
f0035aa
Fix unit tests
apsonawane May 4, 2026
0afa1c9
fix cuda tests
apsonawane May 4, 2026
ab0ddfb
address comments
apsonawane May 4, 2026
ec50731
Support KV-shared decode with separate Q/KV sequence lengths
apsonawane May 5, 2026
1069d55
Address copilot comment:
apsonawane May 5, 2026
b1c6271
fix cuda pipeline
apsonawane May 5, 2026
e428155
[GQA] Support KV-shared layers with empty K/V inputs (kv_sequence_len…
apsonawane May 6, 2026
9d4e8c6
Add unit test and fix documentation
apsonawane May 6, 2026
17ec243
Merge branch 'main' into asonawane/gemma4
apsonawane May 7, 2026
4ca15f9
Merge branch 'main' into asonawane/gemma4
apsonawane May 8, 2026
17a155a
Fix comments
apsonawane May 8, 2026
2dd66c4
Fix
apsonawane May 8, 2026
01a1ef6
Apply copilot comments
apsonawane May 8, 2026
24afd1e
revert docs
apsonawane May 8, 2026
29bd4cb
Address comments
apsonawane May 11, 2026
0481093
Merge branch 'main' into asonawane/gemma4
apsonawane May 11, 2026
85209d6
Fix comments
apsonawane May 11, 2026
d819b56
address copilot comments
apsonawane May 11, 2026
3b996e4
Improve
apsonawane May 11, 2026
42af4d7
address comments
apsonawane May 11, 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
77 changes: 54 additions & 23 deletions onnxruntime/contrib_ops/cpu/bert/gqa_attention_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class GQAAttentionBase {
const bool is_prompt = parameters.is_first_prompt;
const int batch_size = parameters.batch_size;
const int sequence_length = parameters.sequence_length;
const int kv_sequence_length = parameters.kv_sequence_length;
const int total_sequence_length = parameters.total_sequence_length;
const int head_size = parameters.head_size;
const int hidden_size = parameters.hidden_size;
Expand All @@ -85,7 +86,9 @@ class GQAAttentionBase {
if (past_key != nullptr && past_value != nullptr) {
seqlen_past_kv_cache = static_cast<int>(past_key->Shape().GetDims()[2]);
}
int seqlen_present_kv_cache = static_cast<int>(present_key->Shape().GetDims()[2]);
int seqlen_present_kv_cache = present_key != nullptr
? static_cast<int>(present_key->Shape().GetDims()[2])
: parameters.total_sequence_length;

// Compute the attention score.
bool gqa_mlas_supported = MlasGQASupported<T>(CblasNoTrans, CblasTrans) &&
Expand All @@ -110,28 +113,28 @@ class GQAAttentionBase {

if (gqa_mlas_supported) {
ComputeAttentionProbs(static_cast<T*>(attention_probs), Q, k, head_sink, seqlens_k->Data<int32_t>(), attention_bias_data,
batch_size, sequence_length, total_sequence_length, attention_bias_shape, seqlen_past_kv_cache,
batch_size, sequence_length, kv_sequence_length, total_sequence_length, attention_bias_shape, seqlen_past_kv_cache,
seqlen_present_kv_cache, head_size, past_key_data, present_key_data, output_qk_buffer,
past_present_share_buffer, packed_qkv, is_prompt, tp, allocator);

// Compute the attentionScore * Value: out(B, N, S, H_v) = attention_probs(B, N, S, T) x V(B, N, T, H_v)
const T* v = packed_qkv ? Q + (num_heads_ + kv_num_heads_) * sequence_length * head_size : V;
ComputeVxAttentionScore(output->MutableData<T>(), static_cast<T*>(attention_probs), v,
seqlens_k->Data<int32_t>(),
batch_size, sequence_length, seqlen_past_kv_cache, seqlen_present_kv_cache, head_size,
batch_size, sequence_length, kv_sequence_length, seqlen_past_kv_cache, seqlen_present_kv_cache, head_size,
hidden_size, past_value_data, present_value_data, past_present_share_buffer, packed_qkv,
is_prompt, tp, allocator);
} else {
ComputeAttentionProbs(static_cast<float*>(attention_probs), Q, k, head_sink, seqlens_k->Data<int32_t>(), attention_bias_data,
batch_size, sequence_length, total_sequence_length, attention_bias_shape, seqlen_past_kv_cache,
batch_size, sequence_length, kv_sequence_length, total_sequence_length, attention_bias_shape, seqlen_past_kv_cache,
seqlen_present_kv_cache, head_size, past_key_data, present_key_data, output_qk_buffer,
past_present_share_buffer, packed_qkv, is_prompt, tp, allocator);

// Compute the attentionScore * Value: out(B, N, S, H_v) = attention_probs(B, N, S, T) x V(B, N, T, H_v)
const T* v = packed_qkv ? Q + (num_heads_ + kv_num_heads_) * sequence_length * head_size : V;
ComputeVxAttentionScore(output->MutableData<T>(), static_cast<float*>(attention_probs), v,
seqlens_k->Data<int32_t>(),
batch_size, sequence_length, seqlen_past_kv_cache, seqlen_present_kv_cache, head_size,
batch_size, sequence_length, kv_sequence_length, seqlen_past_kv_cache, seqlen_present_kv_cache, head_size,
hidden_size, past_value_data, present_value_data, past_present_share_buffer, packed_qkv,
is_prompt, tp, allocator);
}
Expand All @@ -145,15 +148,16 @@ class GQAAttentionBase {
// attention_probs(B, N, S, T) = Softmax(attention_probs)
// If T is float32, U is float32. If T is float16, U could be float16 or float32.
template <typename T, typename U>
void ComputeAttentionProbs(U* attention_probs, // output buffer with size BxNxSxT
const T* Q, // Q data. Its size is BxNxSxH
const T* K, // k data. Its size is BxNxLxH
const T* head_sink, // for smooth softmax. Its size is N.
const int32_t* seqlens_k, // total - 1 sequence lengths tensor
const T* attention_bias, // optional attention bias
const size_t batch_size, // batch size of self-attention
const size_t sequence_length, // sequence length of self-attention (S)
const size_t total_sequence_length, // total sequence length (T)
void ComputeAttentionProbs(U* attention_probs,
Comment thread
apsonawane marked this conversation as resolved.
Outdated
const T* Q,
const T* K,
const T* head_sink,
const int32_t* seqlens_k,
const T* attention_bias,
const size_t batch_size,
const size_t sequence_length,
const size_t kv_sequence_length,
const size_t total_sequence_length,
const gsl::span<const int64_t> attention_bias_shape, // shape of the attention bias
const size_t past_buffer_sequence_length, // sequence length of past state
const size_t present_buffer_sequence_length, // sequence length of present state
Expand All @@ -171,11 +175,11 @@ class GQAAttentionBase {
: SafeInt<ptrdiff_t>(0);
const size_t kv_num_heads_factor = num_heads_ / kv_num_heads_;
const size_t q_input_chunk_length = sequence_length * head_size; // S x H
const size_t kv_input_chunk_length = sequence_length * head_size; // L x H
const size_t kv_input_chunk_length = kv_sequence_length * head_size; // L x H
const size_t past_buff_chunk_length = past_buffer_sequence_length * head_size; // L x H
Comment thread
tianleiwu marked this conversation as resolved.
Outdated
const size_t present_buff_chunk_length = present_buffer_sequence_length * head_size; // T x H

if (!past_present_share_buffer) {
if (present_key && !past_present_share_buffer) {
Comment thread
tianleiwu marked this conversation as resolved.
memset((void*)present_key,
0,
batch_size * kv_num_heads_ * present_buffer_sequence_length * head_size * sizeof(T));
Expand Down Expand Up @@ -207,7 +211,24 @@ class GQAAttentionBase {
const size_t batch_index = i / num_heads_;
const size_t head_index = i % num_heads_;
const size_t total_seqlen = SafeInt<size_t>(seqlens_k[batch_index]) + 1;
const size_t past_seqlen = is_prompt ? 0 : total_seqlen - sequence_length; // Assume no padding sequence length
// past_seqlen: how much data to copy from past buffer in ConcatStateChunkGQA.
// causal_past_seqlen: offset for causal masking (seq_causal_length = causal_past_seqlen + seq + 1).
// These differ for shared KV prompt: copy all past data, but causal starts at 0.
size_t past_seqlen;
size_t causal_past_seqlen;
if (past_key == nullptr) {
past_seqlen = 0;
causal_past_seqlen = 0;
} else if (kv_sequence_length == 0) {
Comment thread
apsonawane marked this conversation as resolved.
past_seqlen = total_seqlen; // Copy all KV data from past (shared KV)
causal_past_seqlen = is_prompt ? 0 : total_seqlen - sequence_length;
} else if (is_prompt) {
past_seqlen = 0;
causal_past_seqlen = 0;
} else {
past_seqlen = total_seqlen - sequence_length;
causal_past_seqlen = past_seqlen;
}
const size_t past_chunk_length = SafeInt<size_t>(past_seqlen) * head_size;

const ptrdiff_t output_offset = SafeInt<ptrdiff_t>(i) * sequence_length * present_buffer_sequence_length;
Expand Down Expand Up @@ -300,7 +321,7 @@ class GQAAttentionBase {
// compute Softmax
U* output_softmax = output;
for (size_t seq = 0; seq < sequence_length; seq++) {
size_t seq_causal_length = past_seqlen + seq + 1;
size_t seq_causal_length = causal_past_seqlen + seq + 1;

const bool should_apply_local_window = local_window_size_ >= 0 &&
seq_causal_length > static_cast<size_t>(local_window_size_);
Expand Down Expand Up @@ -382,9 +403,10 @@ class GQAAttentionBase {
const T* V, // V value with size BxN_kvxSxH
const int32_t* seqlens_k, // total - 1 sequence lengths tensor
const size_t batch_size, // batch size
const size_t sequence_length, // sequence length
const size_t sequence_length, // sequence length of Q
const size_t kv_sequence_length, // sequence length of K/V input
const size_t past_buffer_sequence_length, // sequence length in past state
const size_t present_buffer_sequence_length, // sequence length in past state
const size_t present_buffer_sequence_length, // sequence length in present state
const size_t head_size, // head size of Q, K, V
const size_t hidden_size, // hidden size of Output
const T* past_value, // past value only
Expand All @@ -398,11 +420,11 @@ class GQAAttentionBase {
packed_qkv ? SafeInt<ptrdiff_t>(num_heads_ + 2 * kv_num_heads_) * sequence_length * head_size
: SafeInt<ptrdiff_t>(0);
const size_t kv_num_heads_factor = num_heads_ / kv_num_heads_;
const size_t kv_input_chunk_length = sequence_length * head_size; // L x H
const size_t kv_input_chunk_length = kv_sequence_length * head_size; // L x H
Comment thread
apsonawane marked this conversation as resolved.
Outdated
const size_t past_buff_chunk_length = past_buffer_sequence_length * head_size; // L x H
const size_t present_buff_chunk_length = present_buffer_sequence_length * head_size; // T x H

if (!past_present_share_buffer) {
if (present_value && !past_present_share_buffer) {
memset((void*)present_value,
0,
batch_size * kv_num_heads_ * present_buffer_sequence_length * head_size * sizeof(T));
Expand Down Expand Up @@ -441,7 +463,16 @@ class GQAAttentionBase {
const size_t batch_index = i / num_heads_;
const size_t head_index = i % num_heads_;
const size_t total_seqlen = SafeInt<size_t>(seqlens_k[batch_index]) + 1;
const size_t past_seqlen = is_prompt ? 0 : total_seqlen - sequence_length; // Assume no padding sequence length
size_t past_seqlen;
if (past_value == nullptr) {
past_seqlen = 0;
} else if (kv_sequence_length == 0) {
past_seqlen = total_seqlen;
} else if (is_prompt) {
past_seqlen = 0;
} else {
past_seqlen = total_seqlen - sequence_length;
}
const size_t past_chunk_length = SafeInt<size_t>(past_seqlen) * head_size;

const T* v;
Expand Down
29 changes: 18 additions & 11 deletions onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,17 @@ Status GroupQueryAttention<T>::Compute(OpKernelContext* context) const {
OrtValue Q;
OrtValue K;
OrtValue V;
const int kv_sequence_length = parameters.kv_sequence_length;
if (packed_qkv) {
ORT_RETURN_IF_ERROR(MaybeTransposeToBNSH<T>(
allocator, batch_size, num_heads_ + 2 * kv_num_heads_, sequence_length, head_size, query, Q));
} else {
ORT_RETURN_IF_ERROR(MaybeTransposeToBNSH<T>(
allocator, batch_size, num_heads_, sequence_length, head_size, query, Q));
ORT_RETURN_IF_ERROR(MaybeTransposeToBNSH<T>(
allocator, batch_size, kv_num_heads_, sequence_length, head_size, key, K));
allocator, batch_size, kv_num_heads_, kv_sequence_length, head_size, key, K));
ORT_RETURN_IF_ERROR(MaybeTransposeToBNSH<T>(
allocator, batch_size, kv_num_heads_, sequence_length, head_size, value, V));
allocator, batch_size, kv_num_heads_, kv_sequence_length, head_size, value, V));
}

OrtValue RotaryQKV;
Expand All @@ -143,6 +144,7 @@ Status GroupQueryAttention<T>::Compute(OpKernelContext* context) const {
T* q_rotary = Q.GetMutable<Tensor>()->MutableData<T>();
T* k_rotary = packed_qkv ? nullptr : K.GetMutable<Tensor>()->MutableData<T>();
if (do_rotary_) {
// When kv_sequence_length == 0 (shared KV), only Q needs RoPE — K is skipped below.
ORT_ENFORCE(cos_cache != nullptr && sin_cache != nullptr, "cos_cache and sin_cache must be provided when do_rotary is true");
// Initialize rotary parameters
rotary_embedding_helper::RotaryParameters rotary_params = {};
Expand Down Expand Up @@ -200,19 +202,22 @@ Status GroupQueryAttention<T>::Compute(OpKernelContext* context) const {
q_rotary = RotaryQ.GetMutable<Tensor>()->MutableData<T>();
k_rotary = RotaryK.GetMutable<Tensor>()->MutableData<T>();
}
// Run rotary embedding for Q and K
// Run rotary embedding for Q
ORT_RETURN_IF_ERROR(RunRotaryEmbedding<T>(tp, rotary_params, q_input,
pos_ids_data, cos_cache->Data<T>(),
sin_cache->Data<T>(), q_rotary, rotary_interleaved_));

rotary_params.num_heads = kv_num_heads_;
rotary_params.hidden_size = parameters.kv_hidden_size;
if (!packed_qkv) {
rotary_params.batch_stride = kv_num_heads_ * rotary_params.head_stride;
// Run rotary embedding for K (skip when kv_sequence_length == 0, i.e. shared KV with no new tokens)
if (kv_sequence_length > 0) {
rotary_params.num_heads = kv_num_heads_;
rotary_params.hidden_size = parameters.kv_hidden_size;
if (!packed_qkv) {
Comment thread
apsonawane marked this conversation as resolved.
rotary_params.batch_stride = kv_num_heads_ * rotary_params.head_stride;
}
ORT_RETURN_IF_ERROR(RunRotaryEmbedding<T>(tp, rotary_params, k_input,
pos_ids_data, cos_cache->Data<T>(),
sin_cache->Data<T>(), k_rotary, rotary_interleaved_));
}
ORT_RETURN_IF_ERROR(RunRotaryEmbedding<T>(tp, rotary_params, k_input,
pos_ids_data, cos_cache->Data<T>(),
sin_cache->Data<T>(), k_rotary, rotary_interleaved_));
// Pack V into rotary QKV buffer
if (packed_qkv) {
const T* v_input = k_input + kv_num_heads_ * sequence_length * head_size;
Expand All @@ -233,7 +238,9 @@ Status GroupQueryAttention<T>::Compute(OpKernelContext* context) const {
const T* head_sink_data = (head_sink != nullptr) ? head_sink->Data<T>() : nullptr;

// Compute the attention score and apply the score to V
return ApplyAttention(q_rotary, packed_qkv ? nullptr : k_rotary, packed_qkv ? nullptr : V.Get<Tensor>().Data<T>(),
const T* k_data = packed_qkv ? nullptr : k_rotary;
const T* v_data = packed_qkv ? nullptr : V.Get<Tensor>().Data<T>();
return ApplyAttention(q_rotary, k_data, v_data,
head_sink_data, attention_bias, past_key, past_value, output, present_k, present_v,
output_qk, seqlens_k, parameters, allocator, context);
Comment thread
apsonawane marked this conversation as resolved.
}
Expand Down
Loading
Loading