Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
1 change: 1 addition & 0 deletions onnxruntime/contrib_ops/cuda/bert/attention_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <gsl/gsl>
#include <iostream>
#include "core/framework/allocator.h"
#include "contrib_ops/cpu/bert/attention_common.h"
#include "contrib_ops/cpu/bert/attention_parameters.h"

Expand Down
112 changes: 90 additions & 22 deletions onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,68 @@ KVQuantizationType StringToKVQuantizationType(std::string s) {
}
} // namespace

#if USE_FLASH_ATTENTION
FlashAttentionSplitPlan GetFlashAttentionSplitPlan(
int batch_size,
int sequence_length,
int total_sequence_length,
int cache_capacity,
int num_heads,
int kv_num_heads,
int head_size,
int multi_processor_count,
int local_window_size,
bool use_fast_decode,
bool reserve_for_capture,
bool is_capturing) {
auto get_plan = [&](size_t kv_length, size_t heads_for_split) {
return onnxruntime::flash::get_num_splits_and_buffer_sizes(
static_cast<size_t>(batch_size), static_cast<size_t>(sequence_length), kv_length,
heads_for_split, static_cast<size_t>(head_size), static_cast<size_t>(multi_processor_count));
};

auto effective_kv_length = [&](int kv_length) {
size_t length = static_cast<size_t>(std::max(kv_length, 0));
if (use_fast_decode && local_window_size > 0) {
length = std::min(length, static_cast<size_t>(local_window_size));
}
return length;
};

const size_t heads_for_split = static_cast<size_t>(use_fast_decode ? kv_num_heads : num_heads);
const auto live_plan = get_plan(effective_kv_length(total_sequence_length), heads_for_split);
const size_t live_num_splits = std::get<0>(live_plan);
if (!use_fast_decode) {
return {live_num_splits, std::get<1>(live_plan), std::get<2>(live_plan)};
}

// Warm-up must reserve the replay-invariant workspace before capture starts, while eager launches
// continue using the live-length split count to avoid empty-split overhead at short context.
const bool needs_capture_workspace = reserve_for_capture || is_capturing;
size_t capture_num_splits = live_num_splits;
if (needs_capture_workspace) {
const auto capture_plan = get_plan(effective_kv_length(cache_capacity), heads_for_split);
capture_num_splits = std::get<0>(capture_plan);
}

const size_t num_splits = is_capturing ? capture_num_splits : live_num_splits;
const size_t workspace_num_splits =
needs_capture_workspace ? std::max(live_num_splits, capture_num_splits) : live_num_splits;
if (workspace_num_splits <= 1) {
return {num_splits, 0, 0};
}

const size_t lse_bytes = onnxruntime::flash::get_softmax_lse_accum_size(
workspace_num_splits, static_cast<size_t>(batch_size), static_cast<size_t>(num_heads),
static_cast<size_t>(sequence_length));
const size_t rounded_head_size = (static_cast<size_t>(head_size) + 31) / 32 * 32;
const size_t out_bytes = onnxruntime::flash::get_out_accum_size(
workspace_num_splits, static_cast<size_t>(batch_size), static_cast<size_t>(num_heads),
static_cast<size_t>(sequence_length), rounded_head_size);
return {num_splits, lse_bytes, out_bytes};
}
#endif

#define REGISTER_KERNEL_TYPED(T, U) \
ONNX_OPERATOR_TYPED_KERNEL_EX( \
GroupQueryAttention, \
Expand Down Expand Up @@ -94,6 +156,7 @@ constexpr int kHeadSinkInputIndex = 11;
template <typename T, typename U>
GroupQueryAttention<T, U>::GroupQueryAttention(const OpKernelInfo& info)
: CudaKernel(info) {
enable_cuda_graph_ = info.GetExecutionProvider()->IsGraphCaptureEnabled();
int64_t num_heads = 0;
int64_t kv_num_heads = 0;
ORT_ENFORCE(info.GetAttr("num_heads", &num_heads).IsOK() && num_heads > 0);
Expand Down Expand Up @@ -705,38 +768,40 @@ Status GroupQueryAttention<T, U>::ComputeInternal(OpKernelContext* context) cons
// Allocate Flash specific buffers (Softmax LSE, Accum)
size_t softmax_lse_bytes = onnxruntime::flash::get_softmax_lse_size(parameters.sequence_length, parameters.batch_size, parameters.num_heads);

int num_heads_for_split = data.use_flash_attention_fast_decode ? parameters.kv_num_heads : parameters.num_heads;
size_t sequence_length_for_split = static_cast<size_t>(parameters.total_sequence_length);
if (data.use_flash_attention_fast_decode && parameters.local_window_size > 0) {
sequence_length_for_split = std::min(sequence_length_for_split, static_cast<size_t>(parameters.local_window_size));
}

auto [num_splits, softmax_lse_accum_bytes, out_accum_bytes] = onnxruntime::flash::get_num_splits_and_buffer_sizes(
parameters.batch_size, parameters.sequence_length, sequence_length_for_split, num_heads_for_split,
parameters.head_size, device_prop.multiProcessorCount);

parameters.num_splits = static_cast<int>(num_splits);

if (data.use_flash_attention_fast_decode && num_splits > 1) {
// The heuristic used kv_num_heads to maximize occupancy for the GQA-aware kernel.
// However, the LSE and Accum buffers must store results for ALL num_heads.
softmax_lse_accum_bytes = onnxruntime::flash::get_softmax_lse_accum_size(num_splits, parameters.batch_size, parameters.num_heads, parameters.sequence_length);
auto round_multiple = [](size_t x, size_t m) { return (x + m - 1) / m * m; };
out_accum_bytes = onnxruntime::flash::get_out_accum_size(num_splits, parameters.batch_size, parameters.num_heads, parameters.sequence_length, round_multiple(parameters.head_size, 32));
const auto split_plan = GetFlashAttentionSplitPlan(
parameters.batch_size, parameters.sequence_length, parameters.total_sequence_length,
parameters.seqlen_present_kv_cache, parameters.num_heads, parameters.kv_num_heads,
parameters.head_size, device_prop.multiProcessorCount, parameters.local_window_size,
data.use_flash_attention_fast_decode,
enable_cuda_graph_,
onnxruntime::llm::common::isCapturing(Stream(context)));
parameters.num_splits = static_cast<int>(split_plan.num_splits);
size_t softmax_lse_accum_bytes = split_plan.softmax_lse_accum_bytes;
size_t out_accum_bytes = split_plan.out_accum_bytes;
size_t active_softmax_lse_accum_bytes = 0;
size_t active_out_accum_bytes = 0;
if (parameters.num_splits > 1) {
active_softmax_lse_accum_bytes = onnxruntime::flash::get_softmax_lse_accum_size(
split_plan.num_splits, parameters.batch_size, parameters.num_heads, parameters.sequence_length);
const size_t rounded_head_size = (static_cast<size_t>(parameters.head_size) + 31) / 32 * 32;
active_out_accum_bytes = onnxruntime::flash::get_out_accum_size(
split_plan.num_splits, parameters.batch_size, parameters.num_heads,
parameters.sequence_length, rounded_head_size);
}

softmax_lse_buffer = GetScratchBuffer<void>(softmax_lse_bytes, GetComputeStream(context));
softmax_lse_accum_buffer = GetScratchBuffer<void>(softmax_lse_accum_bytes, GetComputeStream(context));
out_accum_buffer = GetScratchBuffer<void>(out_accum_bytes, GetComputeStream(context));

auto cuda_stream = Stream(context);
if (softmax_lse_accum_bytes > 0) {
if (active_softmax_lse_accum_bytes > 0) {
// Initialize to 0 is fine because Flash kernel will write -inf to it if needed.
// However, the standard Flash kernel often doesn't zero it globally.
CUDA_RETURN_IF_ERROR(cudaMemsetAsync(softmax_lse_accum_buffer.get(), 0, softmax_lse_accum_bytes, cuda_stream));
CUDA_RETURN_IF_ERROR(
cudaMemsetAsync(softmax_lse_accum_buffer.get(), 0, active_softmax_lse_accum_bytes, cuda_stream));
}
if (out_accum_bytes > 0) {
CUDA_RETURN_IF_ERROR(cudaMemsetAsync(out_accum_buffer.get(), 0, out_accum_bytes, cuda_stream));
if (active_out_accum_bytes > 0) {
CUDA_RETURN_IF_ERROR(cudaMemsetAsync(out_accum_buffer.get(), 0, active_out_accum_bytes, cuda_stream));
}

data.softmax_lse = reinterpret_cast<CudaT*>(softmax_lse_buffer.get());
Expand Down Expand Up @@ -896,6 +961,9 @@ Status GroupQueryAttention<T, U>::ComputeInternal(OpKernelContext* context) cons
debug_info.use_flash_attention = data.use_flash_attention;
debug_info.use_efficient_attention = data.use_memory_efficient_attention;
debug_info.use_cudnn_flash_attention = data.use_cudnn_sdpa;
if (data.use_flash_attention) {
debug_info.num_splits = std::max(1, parameters.num_splits);
}

debug_info.Print("GroupQueryAttention",
this->Node().Name(),
Expand Down
1 change: 1 addition & 0 deletions onnxruntime/contrib_ops/cuda/bert/group_query_attention.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class GroupQueryAttention final : public CudaKernel {
bool enable_xqa_; // True when ORT_ENABLE_XQA != 0 (default: on) and T is fp16/bf16.
bool enable_cudnn_flash_attention_; // cuDNN SDPA explicitly enabled (env / sdpa_kernel)
bool auto_enable_cudnn_flash_attention_; // auto-prefer cuDNN SDPA on SM>=90 when no explicit kernel pinned
bool enable_cuda_graph_{false};

KVQuantizationType k_quant_type_;
KVQuantizationType v_quant_type_;
Expand Down
23 changes: 23 additions & 0 deletions onnxruntime/contrib_ops/cuda/bert/group_query_attention_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// Licensed under the MIT License.

#pragma once

#include <cstddef>

#include "core/providers/cuda/shared_inc/cuda_utils.h"
#include <cuda_fp16.h>
#include <cublas_v2.h>
Expand All @@ -15,6 +18,26 @@ namespace onnxruntime {
namespace contrib {
namespace cuda {

struct FlashAttentionSplitPlan {
size_t num_splits = 0;
size_t softmax_lse_accum_bytes = 0;
size_t out_accum_bytes = 0;
};

FlashAttentionSplitPlan GetFlashAttentionSplitPlan(
int batch_size,
int sequence_length,
int total_sequence_length,
int cache_capacity,
int num_heads,
int kv_num_heads,
int head_size,
int multi_processor_count,
int local_window_size,
bool use_fast_decode,
bool reserve_for_capture,
bool is_capturing);

template <typename T, typename U>
Status QkvToContext(
const cudaDeviceProp& device_prop,
Expand Down
Loading
Loading