[CUDA] GroupQueryAttention operator using FlashAttention (#17674)

### Description
Added Group Query Attention op, supporting integer multiple number of
heads for Q / KV. As of now, this op can only use FlashAttention kernel,
meaning it only supports sm>=80 on Linux.

Results from onnxruntime/test/python/transformers/benchmark_gqa.py show
an on-average ~37% speed-up over Decoder Masked Multi-Head Attention,
with even greater improvements for long past sequence lengths.

```
op      batch   s_kv    heads   h_dim   ms      TFLOPS
gqa     16      2048    8       32      0.34    0.10
dmmha   16      2048    8       32      0.39    0.09
---------
gqa     16      2048    8       64      0.45    0.15
dmmha   16      2048    8       64      0.61    0.11
---------
gqa     16      2048    8       128     0.54    0.25
dmmha   16      2048    8       128     0.83    0.16
---------
gqa     16      2048    16      32      0.45    0.15
dmmha   16      2048    16      32      0.69    0.10
---------
gqa     16      2048    16      64      0.69    0.19
dmmha   16      2048    16      64      0.83    0.16
---------
gqa     16      2048    16      128     0.71    0.38
dmmha   16      2048    16      128     1.28    0.21
---------
gqa     16      2048    32      32      0.58    0.23
dmmha   16      2048    32      32      0.77    0.17
---------
gqa     16      2048    32      64      0.58    0.46
dmmha   16      2048    32      64      1.25    0.21
---------
gqa     16      2048    32      128     0.76    0.71
dmmha   16      2048    32      128     2.15    0.25
---------
gqa     16      2048    64      32      0.68    0.39
dmmha   16      2048    64      32      1.23    0.22
---------
gqa     16      2048    64      64      0.77    0.70
dmmha   16      2048    64      64      2.11    0.25
---------
gqa     16      2048    64      128     1.10    0.97
dmmha   16      2048    64      128     4.06    0.26
---------
gqa     16      2048    128     32      1.00    0.54
dmmha   16      2048    128     32      2.09    0.26
---------
gqa     16      2048    128     64      1.10    0.97
dmmha   16      2048    128     64      4.08    0.26
```


### Motivation and Context
As of now, this op is targeted for use on LLama models, as it supports
kv-caching and different number of heads for Q and KV (Grouped Query
Attention). We plan to add support for more platforms, input formats,
etc. in the future.

---------

Co-authored-by: Tianlei Wu <tlwu@microsoft.com>
Co-authored-by: tlwu@microsoft.com <tlwu@a100.crj0ad2y1kku1j4yxl4sj10o4e.gx.internal.cloudapp.net>
This commit is contained in:
aciddelgado 2023-10-09 12:43:12 -07:00 committed by GitHub
parent ba72bb6f98
commit 406cd324e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 3446 additions and 189 deletions

View file

@ -42,6 +42,7 @@ Do not modify directly.*
* <a href="#com.microsoft.GreedySearch">com.microsoft.GreedySearch</a>
* <a href="#com.microsoft.GridSample">com.microsoft.GridSample</a>
* <a href="#com.microsoft.GroupNorm">com.microsoft.GroupNorm</a>
* <a href="#com.microsoft.GroupQueryAttention">com.microsoft.GroupQueryAttention</a>
* <a href="#com.microsoft.Inverse">com.microsoft.Inverse</a>
* <a href="#com.microsoft.Irfft">com.microsoft.Irfft</a>
* <a href="#com.microsoft.LongformerAttention">com.microsoft.LongformerAttention</a>
@ -1170,9 +1171,9 @@ This version of the operator has been available since version 1 of the 'com.micr
<dt><tt>output</tt> : T</dt>
<dd>3D output tensor with shape (batch_size, sequence_length, v_hidden_size)</dd>
<dt><tt>present_key</tt> (optional) : T</dt>
<dd>past state for key with shape (batch_size, num_heads, total_sequence_length, head_size). If past_present_share_buffer is set, its shape is (batch_size, num_heads, max_sequence_length, head_size), while effective_seq_length = (past_sequence_length + kv_sequence_length).</dd>
<dd>present state for key with shape (batch_size, num_heads, total_sequence_length, head_size). If past_present_share_buffer is set, its shape is (batch_size, num_heads, max_sequence_length, head_size), while effective_seq_length = (past_sequence_length + kv_sequence_length).</dd>
<dt><tt>present_value</tt> (optional) : T</dt>
<dd>past state for value with shape (batch_size, num_heads, total_sequence_length, head_size). If past_present_share_buffer is set, its shape is (batch_size, num_heads, max_sequence_length, head_size), while effective_seq_length = (past_sequence_length + kv_sequence_length).</dd>
<dd>present state for value with shape (batch_size, num_heads, total_sequence_length, head_size). If past_present_share_buffer is set, its shape is (batch_size, num_heads, max_sequence_length, head_size), while effective_seq_length = (past_sequence_length + kv_sequence_length).</dd>
</dl>
#### Type Constraints
@ -2268,6 +2269,69 @@ This version of the operator has been available since version 1 of the 'com.micr
</dl>
### <a name="com.microsoft.GroupQueryAttention"></a><a name="com.microsoft.groupqueryattention">**com.microsoft.GroupQueryAttention**</a>
Group Query Self/Cross Attention.
Supports different number of heads for q and kv.
#### Version
This version of the operator has been available since version 1 of the 'com.microsoft' operator set.
#### Attributes
<dl>
<dt><tt>is_past_bsnh</tt> : int</dt>
<dd>Whether past kv uses BSNH, otherwise BNSH. Default value is 1 (BSNH).</dd>
<dt><tt>kv_num_heads</tt> : int (required)</dt>
<dd>Number of attention heads for k and v</dd>
<dt><tt>num_heads</tt> : int (required)</dt>
<dd>Number of attention heads for q</dd>
<dt><tt>scale</tt> : float</dt>
<dd>Custom scale will be used if specified. Default value is 1/sqrt(head_size)</dd>
<dt><tt>unidirectional</tt> : int</dt>
<dd>Whether every token can only attend to previous tokens. Default value is 1.</dd>
</dl>
#### Inputs (3 - 6)
<dl>
<dt><tt>query</tt> : T</dt>
<dd>Query with shape (batch_size, sequence_length, hidden_size)</dd>
<dt><tt>key</tt> : T</dt>
<dd>Key with shape (batch_size, kv_sequence_length, kv_hidden_size) </dd>
<dt><tt>value</tt> : T</dt>
<dd>Value with shape (batch_size, kv_sequence_length, kv_hidden_size)</dd>
<dt><tt>past_key</tt> (optional) : T</dt>
<dd>past state key with support for format BSNH or BNSH. When past_key uses same tensor as present_key(k-v cache), it is of length max_sequence_length... otherwise of length past_sequence_length.</dd>
<dt><tt>past_value</tt> (optional) : T</dt>
<dd>past state value with support for format BSNH or BNSH. When past_value uses same tensor as present_value(k-v cache), it is of length max_sequence_length... otherwise of length past_sequence_length.</dd>
<dt><tt>past_sequence_length</tt> (optional) : M</dt>
<dd>When buffered past_key and past_value is used (present_key uses same tensor as past_key), requiredto specify past_sequence_length (could be 0). Otherwise, past_sequence_length inferred from past_key.</dd>
</dl>
#### Outputs (1 - 3)
<dl>
<dt><tt>output</tt> : T</dt>
<dd>3D output tensor with shape (batch_size, sequence_length, hidden_size)</dd>
<dt><tt>present_key</tt> (optional) : T</dt>
<dd>present state key with support for format BSNH or BNSH. When past_key uses same tensor as present_key(k-v buffer), it is of length max_sequence_length... otherwise of length past_sequence_length +kv_sequence_length.</dd>
<dt><tt>present_value</tt> (optional) : T</dt>
<dd>present state value with support for format BSNH or BNSH. When past_value uses same tensor as present_value(k-v buffer), it is of length max_sequence_length... otherwise of length past_sequence_length +kv_sequence_length.</dd>
</dl>
#### Type Constraints
<dl>
<dt><tt>T</tt> : tensor(float16)</dt>
<dd>Constrain input and output to float tensors.</dd>
<dt><tt>M</tt> : tensor(int32), tensor(int64)</dt>
<dd>Constrain past sequence length to int tensor.</dd>
</dl>
### <a name="com.microsoft.Inverse"></a><a name="com.microsoft.inverse">**com.microsoft.Inverse**</a>
#### Version

View file

@ -840,6 +840,7 @@ Do not modify directly.*
|GreedySearch|*in* input_ids:**I**<br> *in* max_length:**I**<br> *in* min_length:**I**<br> *in* repetition_penalty:**T**<br> *in* vocab_mask:**I**<br> *in* prefix_vocab_mask:**I**<br> *in* attention_mask:**I**<br> *out* sequences:**I**|1+|**T** = tensor(float), tensor(float16)|
|GridSample|*in* X:**T1**<br> *in* Grid:**T1**<br> *out* Y:**T2**|1+|**T1** = tensor(float)<br/> **T2** = tensor(float)|
|GroupNorm|*in* X:**T**<br> *in* gamma:**M**<br> *in* beta:**M**<br> *out* Y:**T**|1+|**T** = tensor(float), tensor(float16)|
|GroupQueryAttention|*in* query:**T**<br> *in* key:**T**<br> *in* value:**T**<br> *in* past_key:**T**<br> *in* past_value:**T**<br> *in* past_sequence_length:**M**<br> *out* output:**T**<br> *out* present_key:**T**<br> *out* present_value:**T**|1+|**M** = tensor(int32), tensor(int64)<br/> **T** = tensor(float16)|
|Inverse|*in* X:**T**<br> *out* Y:**T**|1+|**T** = tensor(double), tensor(float), tensor(float16)|
|Irfft|*in* X:**T**<br> *out* Y:**T**|1+|**T** = tensor(double), tensor(float), tensor(float16)|
|LongformerAttention|*in* input:**T**<br> *in* weight:**T**<br> *in* bias:**T**<br> *in* mask:**T**<br> *in* global_weight:**T**<br> *in* global_bias:**T**<br> *in* global:**G**<br> *out* output:**T**|1+|**T** = tensor(float), tensor(float16)|

View file

@ -82,6 +82,26 @@ struct PackedAttentionParameters {
bool broadcast_res_pos_bias;
};
// Parameters deduced from node attributes and inputs/outputs.
struct GroupQueryAttentionParameters {
int batch_size;
int sequence_length;
int past_sequence_length; // actual sequence length of past_key and past_value
int kv_sequence_length; // sequence length of key and value (or new_k and new_v when past is present)
int present_sequence_length; // past_sequence_length + kv_sequence_length
int max_sequence_length; // allocated length of past_key and past_value
int hidden_size;
int num_heads;
int head_size;
int kv_hidden_size;
int kv_num_heads;
bool is_unidirectional; // causal
float scale;
int num_splits; // number of splits for splitkv
AttentionQkvFormat qkv_format;
AttentionQkvFormat past_kv_format;
};
namespace attention {
// Environment variable to enable or disable TRT fused self attention kernel. Default is 0 (enabled).
constexpr const char* kDisableFusedSelfAttention = "ORT_DISABLE_FUSED_ATTENTION";

View file

@ -287,9 +287,9 @@ __global__ void AddBiasTransposeQKV(int M, const T* input, const T* biases, T* o
T* k_smem = q_smem + rotary_embedding_dim;
const int half_rotary_dim = rotary_embedding_dim / 2;
const int half_idx = (head_idx) / half_rotary_dim;
const int intra_half_idx = (head_idx) % half_rotary_dim;
const int smem_pitch = half_rotary_dim;
const int half_idx = (head_idx) / half_rotary_dim;
const int intra_half_idx = (head_idx) % half_rotary_dim;
const int smem_pitch = half_rotary_dim;
if (do_rotary) {
*reinterpret_cast<Vec_t*>(q_smem + half_idx * smem_pitch + intra_half_idx) = q;
@ -441,7 +441,6 @@ __global__ void AddBiasTransposeQKVLarge(const int head_size, const T* input, co
}
}
template <typename T>
__global__ void AddBiasTransposeCutlass(const T* input, const T* biases, T* output, int v_head_size) {
// Format 3 for cutlass memory efficient attention
@ -651,7 +650,7 @@ void InvokeAddBiasTranspose(
if (format != 1 && format != 2 && format != 3) {
ORT_THROW("format must be 1, 2 or 3 for rotary attention");
}
if (qk_head_size != 64 && qk_head_size !=128) {
if (qk_head_size != 64 && qk_head_size != 128) {
ORT_THROW("qk_head_size must be 64 or 128 for rotary attention");
}
if (v_head_size != -1 && qk_head_size != v_head_size) {

View file

@ -367,32 +367,32 @@ __global__ void __launch_bounds__(kMAX_THREADS_PER_BLOCK)
const int* attention_masks,
const int batch_size,
const int sequence_length) {
typedef cub::BlockReduce<int, kMAX_THREADS_PER_BLOCK> BlockReduce;
__shared__ typename BlockReduce::TempStorage temp_storage;
typedef cub::BlockReduce<int, kMAX_THREADS_PER_BLOCK> BlockReduce;
__shared__ typename BlockReduce::TempStorage temp_storage;
const int batch_id = blockIdx.x;
const int* batch_mask = attention_masks + (batch_id * sequence_length);
const bool leftmost_non_zero = (batch_mask[0] != 0);
int biggest_position = 0;
const int batch_id = blockIdx.x;
const int* batch_mask = attention_masks + (batch_id * sequence_length);
const bool leftmost_non_zero = (batch_mask[0] != 0);
int biggest_position = 0;
for (int i = threadIdx.x; i < sequence_length; i += blockDim.x) {
if (leftmost_non_zero == (batch_mask[i] != 0)) {
biggest_position = i;
} else {
break;
}
for (int i = threadIdx.x; i < sequence_length; i += blockDim.x) {
if (leftmost_non_zero == (batch_mask[i] != 0)) {
biggest_position = i;
} else {
break;
}
}
int last_leading_position = BlockReduce(temp_storage).Reduce(biggest_position, cub::Max(), blockDim.x);
int last_leading_position = BlockReduce(temp_storage).Reduce(biggest_position, cub::Max(), blockDim.x);
if (threadIdx.x == 0) {
int batch_offset = batch_id * sequence_length;
trt_mha_padding_offset[2 * batch_id] = batch_offset;
trt_mha_padding_offset[2 * batch_id + 1] = batch_offset + last_leading_position + 1;
if (batch_id == gridDim.x - 1) {
trt_mha_padding_offset[2 * batch_id + 2] = batch_offset + sequence_length;
}
if (threadIdx.x == 0) {
int batch_offset = batch_id * sequence_length;
trt_mha_padding_offset[2 * batch_id] = batch_offset;
trt_mha_padding_offset[2 * batch_id + 1] = batch_offset + last_leading_position + 1;
if (batch_id == gridDim.x - 1) {
trt_mha_padding_offset[2 * batch_id + 2] = batch_offset + sequence_length;
}
}
}
// only support simple left padding with mask 0s on leading left,

View file

@ -86,10 +86,10 @@ __global__ void MaskIndexKernel(int sequence_length, const int* mask, int* mask_
}
inline Status ComputeMaskIndex(cudaStream_t stream,
const int sequence_length,
const int batch_size,
const int* mask,
int* mask_index) {
const int sequence_length,
const int batch_size,
const int* mask,
int* mask_index) {
// Mask idx is of length batch_size and assumes the valid region is contiguous starting
// from the beginning of the sequence
@ -133,7 +133,7 @@ __global__ void EmbedLayerNormKernel(
}
if (nullptr == position_ids) {
position_id = blockIdx.x;
} else if (broadcast_position_ids){
} else if (broadcast_position_ids) {
position_id = position_ids[sequence_position % gridDim.x];
} else {
position_id = position_ids[sequence_position];
@ -212,13 +212,12 @@ Status LaunchEmbedLayerNormKernel(
void* embedding_sum,
const int* position_ids,
const bool broadcast_position_ids) {
if (mask_index != nullptr) {
if (nullptr == input_mask) {
CUDA_RETURN_IF_ERROR(cudaMemsetAsync(mask_index, 0, sizeof(int) * batch_size, stream));
} else {
ORT_RETURN_IF_ERROR(
ComputeMaskIndex(stream, sequence_length, batch_size, input_mask, static_cast<int*>(mask_index)));
ComputeMaskIndex(stream, sequence_length, batch_size, input_mask, static_cast<int*>(mask_index)));
}
}

View file

@ -66,7 +66,7 @@ __global__ void FastGeluKernel2(const half2 a, const half2 b, const half2 c, int
template <>
Status LaunchFastGeluKernel(const cudaDeviceProp& prop, cudaStream_t stream, int input_length, int bias_length,
const float* input, const float* bias, float* output, bool /*use_half2*/) {
const float* input, const float* bias, float* output, bool /*use_half2*/) {
constexpr int blockSize = 256;
const int gridSize = (input_length + blockSize - 1) / blockSize;
FastGeluKernel<float, blockSize><<<gridSize, blockSize, 0, stream>>>(A, B, C, input_length, bias_length,
@ -77,7 +77,7 @@ Status LaunchFastGeluKernel(const cudaDeviceProp& prop, cudaStream_t stream, int
template <>
Status LaunchFastGeluKernel(const cudaDeviceProp& prop, cudaStream_t stream, int input_length, int bias_length,
const half* input, const half* bias, half* output, bool use_half2) {
const half* input, const half* bias, half* output, bool use_half2) {
constexpr int blockSize = 256;
if (use_half2 && 0 == (bias_length & 1) && prop.major >= 7) {
const int n = input_length / 2;
@ -101,7 +101,7 @@ Status LaunchFastGeluKernel(const cudaDeviceProp& prop, cudaStream_t stream, int
template <>
Status LaunchFastGeluKernel(const cudaDeviceProp& prop, cudaStream_t stream, int input_length, int bias_length,
const BFloat16* input, const BFloat16* bias, BFloat16* output, bool /*use_half2*/) {
const BFloat16* input, const BFloat16* bias, BFloat16* output, bool /*use_half2*/) {
constexpr int blockSize = 256;
// remove nv_bfloat162 implementation for now to fix build issue

View file

@ -12,9 +12,13 @@ struct BlockInfo {
template <typename Params>
__device__ BlockInfo(const Params& params, const int bidb)
: sum_s_q(!Varlen || params.cu_seqlens_q == nullptr ? -1 : params.cu_seqlens_q[bidb]),
sum_s_k(!Varlen || params.cu_seqlens_k == nullptr ? -1 : params.cu_seqlens_k[bidb]),
actual_seqlen_q(!Varlen || params.cu_seqlens_q == nullptr ? params.seqlen_q : params.cu_seqlens_q[bidb + 1] - sum_s_q),
actual_seqlen_k(!Varlen || params.cu_seqlens_k == nullptr ? params.seqlen_k : params.cu_seqlens_k[bidb + 1] - sum_s_k) {
sum_s_k(!Varlen || params.cu_seqlens_k == nullptr || !params.is_seqlens_k_cumulative ? -1 : params.cu_seqlens_k[bidb]),
actual_seqlen_q(!Varlen || params.cu_seqlens_q == nullptr ? params.seqlen_q : params.cu_seqlens_q[bidb + 1] - sum_s_q)
// If is_seqlens_k_cumulative, then seqlen_k is cu_seqlens_k[bidb + 1] - cu_seqlens_k[bidb].
// Otherwise it's cu_seqlens_k[bidb], i.e., we use cu_seqlens_k to store the sequence lengths of K.
,
seqlen_k_cache(!Varlen || params.cu_seqlens_k == nullptr ? params.seqlen_k : (params.is_seqlens_k_cumulative ? params.cu_seqlens_k[bidb + 1] - sum_s_k : params.cu_seqlens_k[bidb])),
actual_seqlen_k(seqlen_k_cache + (params.knew_ptr == nullptr ? 0 : params.seqlen_knew)) {
}
template <typename index_t>
@ -30,6 +34,8 @@ struct BlockInfo {
const int sum_s_q;
const int sum_s_k;
const int actual_seqlen_q;
// We have to have seqlen_k_cache declared before actual_seqlen_k, otherwise actual_seqlen_k is set to 0.
const int seqlen_k_cache;
const int actual_seqlen_k;
};

View file

@ -45,6 +45,7 @@ struct Qkv_params {
struct Flash_fwd_params : public Qkv_params {
// The O matrix (output).
void* __restrict__ o_ptr;
void* __restrict__ oaccum_ptr;
// The stride between rows of O.
index_t o_batch_stride;
@ -56,9 +57,10 @@ struct Flash_fwd_params : public Qkv_params {
// The pointer to the softmax sum.
void* __restrict__ softmax_lse_ptr;
void* __restrict__ softmax_lseaccum_ptr;
// The dimensions.
int b, seqlen_q, seqlen_k, d, seqlen_q_rounded, seqlen_k_rounded, d_rounded;
int b, seqlen_q, seqlen_k, seqlen_knew, d, seqlen_q_rounded, seqlen_k_rounded, d_rounded;
// The scaling factors for the kernel.
float scale_softmax;
@ -70,9 +72,26 @@ struct Flash_fwd_params : public Qkv_params {
int* __restrict__ blockmask;
// The K_new and V_new matrices.
void* __restrict__ knew_ptr;
void* __restrict__ vnew_ptr;
// The stride between rows of the Q, K and V matrices.
index_t knew_batch_stride;
index_t vnew_batch_stride;
index_t knew_row_stride;
index_t vnew_row_stride;
index_t knew_head_stride;
index_t vnew_head_stride;
bool is_bf16 = false;
bool is_causal;
// If is_seqlens_k_cumulative, then seqlen_k is cu_seqlens_k[bidb + 1] - cu_seqlens_k[bidb].
// Otherwise it's cu_seqlens_k[bidb], i.e., we use cu_seqlens_k to store the sequence lengths of K.
bool is_seqlens_k_cumulative;
int num_splits; // For split-KV version
const cudaDeviceProp* dprops;
};
@ -80,6 +99,8 @@ struct Flash_fwd_params : public Qkv_params {
template <typename T, int Headdim>
void run_mha_fwd_(Flash_fwd_params& params, cudaStream_t stream);
template <typename T, int Headdim>
void run_mha_fwd_splitkv_dispatch(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
} // namespace onnxruntime

View file

@ -34,24 +34,37 @@ void set_params_fprop(Flash_fwd_params& params,
void* p_d,
void* softmax_lse_d,
float softmax_scale,
bool is_causal) {
bool is_causal,
bool kv_bsnh = true) {
// Set the pointers and strides.
params.q_ptr = q;
params.k_ptr = k;
params.v_ptr = v;
params.o_ptr = out;
// All stride are in elements, not bytes.
params.q_row_stride = num_heads * head_size;
params.k_row_stride = num_heads_k * head_size;
params.v_row_stride = num_heads * head_size;
params.q_head_stride = head_size;
params.k_head_stride = head_size;
params.v_head_stride = head_size;
params.o_row_stride = num_heads * head_size;
params.o_head_stride = head_size;
params.is_bf16 = false;
// All stride are in elements, not bytes.
if (kv_bsnh) {
params.q_row_stride = num_heads * head_size;
params.k_row_stride = num_heads_k * head_size;
params.v_row_stride = num_heads_k * head_size;
params.q_head_stride = head_size;
params.k_head_stride = head_size;
params.v_head_stride = head_size;
params.o_row_stride = num_heads * head_size;
params.o_head_stride = head_size;
} else {
params.q_row_stride = num_heads * head_size;
params.k_row_stride = head_size;
params.v_row_stride = head_size;
params.q_head_stride = head_size;
params.k_head_stride = seqlen_k * head_size;
params.v_head_stride = seqlen_k * head_size;
params.o_row_stride = num_heads * head_size;
params.o_head_stride = head_size;
}
if (cu_seqlens_q_d == nullptr) {
params.q_batch_stride = seqlen_q * num_heads * head_size; // stride(0)
params.k_batch_stride = seqlen_k * num_heads_k * head_size; // stride(0)
@ -90,6 +103,7 @@ void set_params_fprop(Flash_fwd_params& params,
params.scale_softmax_log2 = softmax_scale * M_LOG2E;
params.is_causal = is_causal;
params.is_seqlens_k_cumulative = true;
}
size_t get_softmax_lse_size(int seqlen, int batch_size, int num_heads) {
@ -97,14 +111,85 @@ size_t get_softmax_lse_size(int seqlen, int batch_size, int num_heads) {
return bytes;
}
void run_mha_fwd(Flash_fwd_params& params, cudaStream_t stream) {
size_t get_softmax_lse_accum_size(int num_splits, int batch_size, int num_heads, int seqlen_q) {
size_t bytes = sizeof(float) * num_splits * batch_size * seqlen_q * num_heads;
return bytes;
}
size_t get_out_accum_size(int num_splits, int batch_size, int num_heads, int seqlen_q, int head_size_rounded) {
size_t bytes = sizeof(float) * num_splits * batch_size * seqlen_q * num_heads * head_size_rounded;
return bytes;
}
void run_mha_fwd(Flash_fwd_params& params, cudaStream_t stream, bool force_split_kernel = false) {
FP16_SWITCH(!params.is_bf16, [&] {
FWD_HEADDIM_SWITCH(params.d, [&] {
run_mha_fwd_<elem_type, kHeadDim>(params, stream);
if (params.num_splits <= 1 && !force_split_kernel) { // If we don't set it num_splits == 0
run_mha_fwd_<elem_type, kHeadDim>(params, stream);
} else {
run_mha_fwd_splitkv_dispatch<elem_type, kHeadDim>(params, stream);
}
});
});
}
// Find the number of splits that maximizes the occupancy. For example, if we have
// batch * n_heads = 48 and we have 108 SMs, having 2 splits (efficiency = 0.89) is
// better than having 3 splits (efficiency = 0.67). However, we also don't want too many
// splits as that would incur more HBM reads/writes.
// So we find the best efficiency, then find the smallest number of splits that gets 85%
// of the best efficiency.
int num_splits_heuristic(int batch_size, int seqlen_q, int seqlen_k, int num_heads, int head_size, int num_SMs,
int max_splits, bool new_kv, bool is_sm8x) {
// This needs to match with run_mha_fwd_splitkv_dispatch
const int block_n = is_sm8x ? (head_size <= 64 ? 256 : (head_size <= 128 ? 128 : 64))
: (head_size <= 64 ? 256 : (head_size <= 160 ? 128 : 64));
const int num_n_blocks = (seqlen_k + (!new_kv ? 0 : seqlen_q) + block_n - 1) / block_n;
// Technically kBlockM = 64 only for the splitKV kernels, not the standard kernel.
// In any case we don't expect seqlen_q to be larger than 64 for inference.
const int num_m_blocks = (seqlen_q + 64 - 1) / 64;
int batch_nheads_mblocks = batch_size * num_heads * num_m_blocks;
// If we have enough to almost fill the SMs, then just use 1 split
if (batch_nheads_mblocks >= 0.8f * num_SMs) {
return 1;
}
max_splits = std::min({max_splits, num_SMs, num_n_blocks});
float max_efficiency = 0.f;
std::vector<float> efficiency;
efficiency.reserve(max_splits);
auto ceildiv = [](int a, int b) { return (a + b - 1) / b; };
// Some splits are not eligible. For example, if we have 64 blocks and choose 11 splits,
// we'll have 6 * 10 + 4 blocks. If we choose 12 splits, we'll have 6 * 11 + (-2) blocks
// (i.e. it's 11 splits anyway).
// So we check if the number of blocks per split is the same as the previous num_splits.
auto is_split_eligible = [&ceildiv, &num_n_blocks](int num_splits) {
return num_splits == 1 || ceildiv(num_n_blocks, num_splits) != ceildiv(num_n_blocks, num_splits - 1);
};
for (int num_splits = 1; num_splits <= max_splits; num_splits++) {
if (!is_split_eligible(num_splits)) {
efficiency.push_back(0.f);
} else {
float n_waves = float(batch_nheads_mblocks * num_splits) / num_SMs;
float eff = n_waves / ceil(n_waves);
// printf("num_splits = %d, eff = %f\n", num_splits, eff);
if (eff > max_efficiency) {
max_efficiency = eff;
}
efficiency.push_back(eff);
}
}
for (int num_splits = 1; num_splits <= max_splits; num_splits++) {
if (!is_split_eligible(num_splits)) {
continue;
}
if (efficiency[num_splits - 1] >= 0.85 * max_efficiency) {
// printf("num_splits chosen = %d\n", num_splits);
return num_splits;
}
}
return 1;
}
Status mha_fwd(const cudaDeviceProp& dprops,
cudaStream_t stream,
void* q, // batch_size x seqlen_q x num_heads x head_size
@ -119,7 +204,11 @@ Status mha_fwd(const cudaDeviceProp& dprops,
int seqlen_q,
int seqlen_k,
float softmax_scale,
bool is_causal) {
bool is_causal,
int num_splits,
void* softmax_lse_accum, // num_splits x batch_size x seqlen_q x num_heads
void* out_accum, // num_splits x batch_size x seqlen_q x num_heads x head_size_rounded
bool kv_bsnh) {
auto round_multiple = [](int x, int m) { return (x + m - 1) / m * m; };
const int head_size_rounded = round_multiple(head_size, 32);
const int seqlen_q_rounded = round_multiple(seqlen_q, 128);
@ -139,7 +228,26 @@ Status mha_fwd(const cudaDeviceProp& dprops,
nullptr,
softmax_lse,
softmax_scale,
is_causal);
is_causal,
kv_bsnh);
params.knew_ptr = nullptr;
params.vnew_ptr = nullptr;
params.knew_batch_stride = 0;
params.vnew_batch_stride = 0;
params.knew_row_stride = 0;
params.vnew_row_stride = 0;
params.knew_head_stride = 0;
params.vnew_head_stride = 0;
params.num_splits = num_splits;
if (params.num_splits > 1 && softmax_lse_accum != nullptr && out_accum != nullptr) {
params.softmax_lseaccum_ptr = softmax_lse_accum;
params.oaccum_ptr = out_accum;
} else {
params.softmax_lseaccum_ptr = nullptr;
params.oaccum_ptr = nullptr;
}
run_mha_fwd(params, stream);
return Status::OK();
@ -192,6 +300,101 @@ bool is_supported(const cudaDeviceProp& dprops, int head_size, int num_heads, in
return (is_sm8x || is_sm90) && (head_size % 8 == 0) && (head_size <= 256) && (num_heads % num_heads_k == 0);
}
// This API is used when past key and value are present... since cached, these are assumed to have sequence length
// of max_sequence_length, so seqlen_k == max_sequence_length. The actual past sequence length is held in seqlens_k_.
Status mha_fwd_kvcache(const cudaDeviceProp& dprops,
cudaStream_t stream,
void* q, // batch_size x seqlen_q x num_heads x head_size
void* kcache, // batch_size x seqlen_k x num_heads_k x head_size or batch_size x num_heads_k seqlen_k x head_size
void* vcache, // batch_size x seqlen_k x num_heads_k x head_size or batch_size x num_heads_k seqlen_k x head_size
void* k, // (optional) batch_size x seqlen_k_new x num_heads_k x head_size
void* v, // (optional) batch_size x seqlen_k_new x num_heads_k x head_size
void* out, // batch_size x seqlen_q x num_heads x head_size
void* softmax_lse, // batch_size x num_heads x seqlen_q
void* seqlens_k_, // batch_size
int batch_size,
int num_heads,
int num_heads_k,
int head_size,
int seqlen_q,
int seqlen_k,
int seqlen_k_new,
const float softmax_scale,
bool is_causal,
bool past_bsnh, // otherwise bnsh
int num_splits,
void* softmax_lse_accum, // num_splits x batch_size x seqlen_q x num_heads
void* out_accum // num_splits x batch_size x seqlen_q x num_heads x head_size_rounded
) {
if (seqlen_q == 1) {
is_causal = false;
} // causal=true is the same as causal=false in this case
auto round_multiple = [](int x, int m) { return (x + m - 1) / m * m; };
const int head_size_rounded = round_multiple(head_size, 32);
const int seqlen_q_rounded = round_multiple(seqlen_q, 128);
const int seqlen_k_rounded = round_multiple(seqlen_k, 128);
Flash_fwd_params params;
params.dprops = &dprops;
set_params_fprop(params,
batch_size,
seqlen_q, seqlen_k,
seqlen_q_rounded, seqlen_k_rounded,
num_heads, num_heads_k,
head_size, head_size_rounded,
q, kcache, vcache, out,
/*cu_seqlens_q_d=*/nullptr,
/*cu_seqlens_k_d=*/nullptr,
/*p_ptr=*/nullptr,
softmax_lse,
softmax_scale,
is_causal,
past_bsnh);
if (k != nullptr && v != nullptr) {
params.seqlen_knew = seqlen_k_new;
params.knew_ptr = k;
params.vnew_ptr = v;
// All stride are in elements, not bytes.
params.knew_batch_stride = seqlen_k_new * num_heads_k * head_size;
params.vnew_batch_stride = seqlen_k_new * num_heads_k * head_size;
params.knew_row_stride = num_heads_k * head_size;
params.vnew_row_stride = num_heads_k * head_size;
params.knew_head_stride = head_size;
params.vnew_head_stride = head_size;
} else {
params.seqlen_knew = 0;
params.knew_ptr = nullptr;
params.vnew_ptr = nullptr;
params.knew_batch_stride = 0;
params.vnew_batch_stride = 0;
params.knew_row_stride = 0;
params.vnew_row_stride = 0;
params.knew_head_stride = 0;
params.vnew_head_stride = 0;
}
params.is_seqlens_k_cumulative = seqlens_k_ == nullptr;
if (seqlens_k_ != nullptr) {
params.cu_seqlens_k = static_cast<int*>(seqlens_k_);
}
params.num_splits = num_splits;
if (params.num_splits > 1 && softmax_lse_accum != nullptr && out_accum != nullptr) {
params.softmax_lseaccum_ptr = softmax_lse_accum;
params.oaccum_ptr = out_accum;
} else {
params.softmax_lseaccum_ptr = nullptr;
params.oaccum_ptr = nullptr;
}
// Only split kernel supports appending to KV cache
run_mha_fwd(params, stream, /*force_split_kernel=*/k != nullptr);
return Status::OK();
}
} // namespace flash
} // namespace onnxruntime

View file

@ -34,6 +34,7 @@
namespace onnxruntime {
namespace flash {
Status mha_fwd(const cudaDeviceProp& dprops,
cudaStream_t stream,
void* q, // batch_size x seqlen_q x num_heads x head_size
@ -48,7 +49,11 @@ Status mha_fwd(const cudaDeviceProp& dprops,
int seqlen_q,
int seqlen_k,
float softmax_scale,
bool is_causal);
bool is_causal,
int num_splits = 0,
void* softmax_lse_accum = nullptr, // num_splits x batch_size x seqlen_q x num_heads
void* out_accum = nullptr, // num_splits x batch_size x seqlen_q x num_heads x head_size_rounded
bool kv_bsnh = true);
Status mha_varlen_fwd(const cudaDeviceProp& dprops,
cudaStream_t stream,
@ -68,7 +73,36 @@ Status mha_varlen_fwd(const cudaDeviceProp& dprops,
float softmax_scale,
bool is_causal);
Status mha_fwd_kvcache(const cudaDeviceProp& dprops,
cudaStream_t stream,
void* q, // batch_size x seqlen_q x num_heads x head_size
void* kcache, // batch_size x seqlen_k x num_heads_k x head_size or batch_size x num_heads_k seqlen_k x x head_size
void* vcache, // batch_size x seqlen_k x num_heads_k x head_size or batch_size x num_heads_k seqlen_k x x head_size
void* k, // batch_size x seqlen_k_new x num_heads_k x head_size
void* v, // batch_size x seqlen_k_new x num_heads_k x head_size
void* out, // batch_size x seqlen_q x num_heads x head_size
void* softmax_lse, // batch_size x num_heads x seqlen_q
void* seqlens_k_, // batch_size
int batch_size,
int num_heads,
int num_heads_k,
int head_size,
int seqlen_q,
int seqlen_k,
int seqlen_k_new,
const float softmax_scale,
bool is_causal,
bool past_bsnh, // otherwise bnsh
int num_splits = 0,
void* softmax_lse_accum = nullptr, // num_splits x batch_size x seqlen_q x num_heads
void* out_accum = nullptr // num_splits x batch_size x seqlen_q x num_heads x head_size_rounded
);
size_t get_softmax_lse_size(int max_seqlen_q, int batch_size, int num_heads);
size_t get_softmax_lse_accum_size(int num_splits, int batch_size, int num_heads, int seqlen_q);
size_t get_out_accum_size(int num_splits, int batch_size, int num_heads, int seqlen_q, int head_size_rounded);
int num_splits_heuristic(int batch_size, int seqlen_q, int seqlen_k, int num_heads, int head_size, int num_SMs, int max_splits, bool new_kv, bool is_sm8x);
bool is_supported(const cudaDeviceProp& dprops, int head_size, int num_heads, int num_heads_k);

View file

@ -79,7 +79,7 @@ inline __device__ void softmax_rescale_o(Tensor0& scores, Tensor1& scores_max, T
flash::reduce_sum(scores, scores_sum);
} else {
cute::Tensor scores_max_prev = make_fragment_like(scores_max);
copy(scores_max, scores_max_prev);
cute::copy(scores_max, scores_max_prev);
flash::template reduce_max</*zero_init=*/false>(scores, scores_max);
// Reshape acc_o from (MMA=4, MMA_M, MMA_K) to (nrow=(2, MMA_M), ncol=(2, MMA_K))
cute::Tensor acc_o_rowcol = make_tensor(acc_o.data(), flash::convert_layout_acc_rowcol(acc_o.layout()));
@ -109,7 +109,7 @@ inline __device__ void softmax_rescale_o(Tensor0& scores, Tensor1& scores_max, T
template <typename Engine0, typename Layout0, typename Engine1, typename Layout1, typename TiledCopy>
inline __device__ void write_softmax_to_gmem(
cute::Tensor<Engine0, Layout0> const& tOrP, cute::Tensor<Engine1, Layout1>& tPgP, TiledCopy gmem_thr_copy_P) {
cute::Tensor<Engine0, Layout0> const& tOrP, cute::Tensor<Engine1, Layout1>& tPgP, TiledCopy gmem_tiled_copy_P) {
// Reshape tOrP from (8, MMA_M, MMA_N) to (8, MMA_M * MMA_N)
cute::Layout l = tOrP.layout();
cute::Tensor tPrP = make_tensor(tOrP.data(), make_layout(get<0>(l), make_layout(get<1>(l), get<2>(l))));
@ -117,7 +117,7 @@ inline __device__ void write_softmax_to_gmem(
CUTE_STATIC_ASSERT_V(cute::size<1>(tPrP) == cute::size<1>(tPgP));
#pragma unroll
for (int mi = 0; mi < cute::size<1>(tPrP); ++mi) {
copy(gmem_thr_copy_P, tPrP(_, mi), tPgP(_, mi, 0));
cute::copy(gmem_tiled_copy_P, tPrP(_, mi), tPgP(_, mi, 0));
}
};
@ -147,6 +147,45 @@ inline __device__ void compute_attn_1rowblock(const Params& params, const int bi
int n_block_max = cute::ceil_div(binfo.actual_seqlen_k, kBlockN);
if (Is_causal) {
n_block_max = std::min(n_block_max, cute::ceil_div((m_block + 1) * kBlockM + binfo.actual_seqlen_k - binfo.actual_seqlen_q, kBlockN));
// We exit early and write 0 to gO and gLSE.
// Otherwise we might read OOB elements from gK and gV.
if (n_block_max <= 0) {
const index_t row_offset_o = binfo.q_offset(params.o_batch_stride, params.o_row_stride, bidb) + m_block * kBlockM * params.o_row_stride + bidh * params.o_head_stride;
const index_t row_offset_lse = (bidb * params.h + bidh) * params.seqlen_q + m_block * kBlockM;
Tensor gO = make_tensor(make_gmem_ptr(reinterpret_cast<Element*>(params.o_ptr) + row_offset_o),
Shape<Int<kBlockM>, Int<kHeadDim>>{},
make_stride(params.o_row_stride, _1{}));
Tensor gLSE = make_tensor(make_gmem_ptr(reinterpret_cast<ElementAccum*>(params.softmax_lse_ptr) + row_offset_lse),
Shape<Int<kBlockM>>{}, Stride<_1>{});
typename Kernel_traits::GmemTiledCopyO gmem_tiled_copy_O;
auto gmem_thr_copy_O = gmem_tiled_copy_O.get_thread_slice(tidx);
Tensor tOgO = gmem_thr_copy_O.partition_D(gO);
Tensor tOrO = make_tensor<Element>(shape(tOgO));
clear(tOrO);
// Construct identity layout for sO
Tensor cO = make_identity_tensor(make_shape(size<0>(gO), size<1>(gO))); // (BLK_M,BLK_K) -> (blk_m,blk_k)
// Repeat the partitioning with identity layouts
Tensor tOcO = gmem_thr_copy_O.partition_D(cO);
Tensor tOpO = make_tensor<bool>(make_shape(size<2>(tOgO)));
if (!Is_even_K) {
#pragma unroll
for (int k = 0; k < size(tOpO); ++k) {
tOpO(k) = get<1>(tOcO(0, 0, k)) < params.d;
}
}
// Clear_OOB_K must be false since we don't want to write zeros to gmem
flash::copy<Is_even_MN, Is_even_K, /*Clear_OOB_MN=*/false, /*Clear_OOB_K=*/false>(
gmem_tiled_copy_O, tOrO, tOgO, tOcO, tOpO, binfo.actual_seqlen_q - m_block * kBlockM);
#pragma unroll
for (int m = 0; m < size<1>(tOgO); ++m) {
const int row = get<0>(tOcO(0, m, 0));
if (row < binfo.actual_seqlen_q - m_block * kBlockM && get<1>(tOcO(0, m, 0)) == 0) {
gLSE(row) = INFINITY;
}
}
return;
}
}
// We iterate over the blocks in reverse order. This is because the last block is the only one
@ -504,6 +543,494 @@ inline __device__ void compute_attn_1rowblock(const Params& params, const int bi
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Kernel_traits, bool Is_causal, bool Is_even_MN, bool Is_even_K, bool Split, bool Append_KV, typename Params>
inline __device__ void compute_attn_1rowblock_splitkv(const Params& params, const int bidb, const int bidh, const int m_block, const int n_split_idx, const int num_n_splits) {
using Element = typename Kernel_traits::Element;
using ElementAccum = typename Kernel_traits::ElementAccum;
using index_t = typename Kernel_traits::index_t;
// Shared memory.
extern __shared__ char smem_[];
// The thread index.
const int tidx = threadIdx.x;
constexpr int kBlockM = Kernel_traits::kBlockM;
constexpr int kBlockN = Kernel_traits::kBlockN;
constexpr int kHeadDim = Kernel_traits::kHeadDim;
constexpr int kNWarps = Kernel_traits::kNWarps;
using GmemTiledCopyO = std::conditional_t<
!Split,
typename Kernel_traits::GmemTiledCopyOaccum,
typename Kernel_traits::GmemTiledCopyO>;
using ElementO = std::conditional_t<!Split, Element, ElementAccum>;
const BlockInfo</*Varlen=*/!Is_even_MN> binfo(params, bidb);
// if (threadIdx.x == 0 && blockIdx.y == 0 && blockIdx.z == 0) { printf("Is_even_MN = %d, is_cumulativ = %d, seqlen_k_cache = %d, actual_seqlen_k = %d\n", Is_even_MN, params.is_seqlens_k_cumulative, binfo.seqlen_k_cache, binfo.actual_seqlen_k); }
// if (threadIdx.x == 0 && blockIdx.y == 1 && blockIdx.z == 0) { printf("params.knew_ptr = %p, seqlen_k_cache + seqlen_knew = %d\n", params.knew_ptr, binfo.seqlen_k_cache + (params.knew_ptr == nullptr ? 0 : params.seqlen_knew)); }
if (m_block * kBlockM >= binfo.actual_seqlen_q) return;
const int n_blocks_per_split = ((params.seqlen_k + kBlockN - 1) / kBlockN + num_n_splits - 1) / num_n_splits;
const int n_block_min = n_split_idx * n_blocks_per_split;
int n_block_max = std::min(cute::ceil_div(binfo.actual_seqlen_k, kBlockN), (n_split_idx + 1) * n_blocks_per_split);
if (Is_causal) {
n_block_max = std::min(n_block_max,
cute::ceil_div((m_block + 1) * kBlockM + binfo.actual_seqlen_k - binfo.actual_seqlen_q, kBlockN));
}
if (n_block_min >= n_block_max) { // This also covers the case where n_block_max <= 0
// We exit early and write 0 to gOaccum and -inf to gLSEaccum.
// Otherwise we might read OOB elements from gK and gV,
// or get wrong results when we combine gOaccum from different blocks.
const index_t row_offset_o = binfo.q_offset(params.o_batch_stride, params.o_row_stride, bidb) + m_block * kBlockM * params.o_row_stride + bidh * params.o_head_stride;
const index_t row_offset_oaccum = (((n_split_idx * params.b + bidb) * params.h + bidh) * params.seqlen_q + m_block * kBlockM) * params.d_rounded;
const index_t row_offset_lseaccum = ((n_split_idx * params.b + bidb) * params.h + bidh) * params.seqlen_q + m_block * kBlockM;
Tensor gOaccum = make_tensor(make_gmem_ptr(reinterpret_cast<ElementO*>(Split ? params.oaccum_ptr : params.o_ptr) + (Split ? row_offset_oaccum : row_offset_o)),
Shape<Int<kBlockM>, Int<kHeadDim>>{},
make_stride(Split ? kHeadDim : params.o_row_stride, _1{}));
Tensor gLSEaccum = make_tensor(make_gmem_ptr(reinterpret_cast<ElementAccum*>(Split ? params.softmax_lseaccum_ptr : params.softmax_lse_ptr) + row_offset_lseaccum),
Shape<Int<kBlockM>>{}, Stride<_1>{});
GmemTiledCopyO gmem_tiled_copy_Oaccum;
auto gmem_thr_copy_Oaccum = gmem_tiled_copy_Oaccum.get_thread_slice(tidx);
Tensor tOgOaccum = gmem_thr_copy_Oaccum.partition_D(gOaccum);
Tensor tOrOaccum = make_tensor<ElementO>(shape(tOgOaccum));
clear(tOrOaccum);
// Construct identity layout for sO
Tensor cO = make_identity_tensor(make_shape(size<0>(gOaccum), size<1>(gOaccum))); // (BLK_M,BLK_K) -> (blk_m,blk_k)
// Repeat the partitioning with identity layouts
Tensor tOcO = gmem_thr_copy_Oaccum.partition_D(cO);
Tensor tOpO = make_tensor<bool>(make_shape(size<2>(tOgOaccum)));
if (!Is_even_K) {
#pragma unroll
for (int k = 0; k < size(tOpO); ++k) {
tOpO(k) = get<1>(tOcO(0, 0, k)) < params.d;
}
}
// Clear_OOB_K must be false since we don't want to write zeros to gmem
flash::copy<Is_even_MN, Is_even_K, /*Clear_OOB_MN=*/false, /*Clear_OOB_K=*/false>(
gmem_tiled_copy_Oaccum, tOrOaccum, tOgOaccum, tOcO, tOpO, binfo.actual_seqlen_q - m_block * kBlockM);
#pragma unroll
for (int m = 0; m < size<1>(tOgOaccum); ++m) {
const int row = get<0>(tOcO(0, m, 0));
if (row < binfo.actual_seqlen_q - m_block * kBlockM && get<1>(tOcO(0, m, 0)) == 0) {
gLSEaccum(row) = Split ? -INFINITY : INFINITY;
}
}
return;
}
// We iterate over the blocks in reverse order. This is because the last block is the only one
// that needs masking when we read K and V from global memory. Moreover, iterating in reverse
// might save us 1 register (we just need n_block instead of both n_block and n_block_max).
const index_t row_offset_q = binfo.q_offset(params.q_batch_stride, params.q_row_stride, bidb) + m_block * kBlockM * params.q_row_stride + bidh * params.q_head_stride;
// We move K and V to the last block.
const index_t row_offset_k = binfo.k_offset(params.k_batch_stride, params.k_row_stride, bidb) + (n_block_max - 1) * kBlockN * params.k_row_stride + (bidh / params.h_h_k_ratio) * params.k_head_stride;
const index_t row_offset_v = binfo.k_offset(params.v_batch_stride, params.v_row_stride, bidb) + (n_block_max - 1) * kBlockN * params.v_row_stride + (bidh / params.h_h_k_ratio) * params.v_head_stride;
const index_t row_offset_knew = binfo.k_offset(params.knew_batch_stride, params.knew_row_stride, bidb) + ((n_block_max - 1) * kBlockN) * params.knew_row_stride + (bidh / params.h_h_k_ratio) * params.knew_head_stride;
const index_t row_offset_vnew = binfo.k_offset(params.vnew_batch_stride, params.vnew_row_stride, bidb) + ((n_block_max - 1) * kBlockN) * params.vnew_row_stride + (bidh / params.h_h_k_ratio) * params.vnew_head_stride;
Tensor gQ = make_tensor(make_gmem_ptr(reinterpret_cast<Element*>(params.q_ptr) + row_offset_q),
Shape<Int<kBlockM>, Int<kHeadDim>>{},
make_stride(params.q_row_stride, _1{}));
Tensor gK = make_tensor(make_gmem_ptr(reinterpret_cast<Element*>(params.k_ptr) + row_offset_k),
Shape<Int<kBlockN>, Int<kHeadDim>>{},
make_stride(params.k_row_stride, _1{}));
// if (threadIdx.x == 0 && blockIdx.y == 0 && blockIdx.z == 0) { printf("k_ptr = %p, row_offset_k = %d, gK_ptr = %p\n", params.k_ptr, row_offset_k, gK.data()); }
Tensor gV = make_tensor(make_gmem_ptr(reinterpret_cast<Element*>(params.v_ptr) + row_offset_v),
Shape<Int<kBlockN>, Int<kHeadDim>>{},
make_stride(params.v_row_stride, _1{}));
// Subtract seqlen_k_cache * row stride so that conceptually gK and gKnew "line up". When we access them,
// e.g. if gK has 128 rows and gKnew has 64 rows, we access gK[:128] and gKNew[128:128 + 64].
// This maps to accessing the first 64 rows of knew_ptr.
Tensor gKnew = make_tensor(make_gmem_ptr(reinterpret_cast<Element*>(params.knew_ptr) + row_offset_knew - binfo.seqlen_k_cache * params.knew_row_stride),
Shape<Int<kBlockN>, Int<kHeadDim>>{},
make_stride(params.knew_row_stride, _1{}));
// if (threadIdx.x == 0 && blockIdx.y == 0 && blockIdx.z == 0) { printf("knew_ptr = %p, row_offset_knew = %d, gKnew_ptr = %p\n", params.knew_ptr, row_offset_knew, gKnew.data()); }
Tensor gVnew = make_tensor(make_gmem_ptr(reinterpret_cast<Element*>(params.vnew_ptr) + row_offset_vnew - binfo.seqlen_k_cache * params.vnew_row_stride),
Shape<Int<kBlockN>, Int<kHeadDim>>{},
make_stride(params.vnew_row_stride, _1{}));
Tensor sQ = make_tensor(make_smem_ptr(reinterpret_cast<Element*>(smem_)),
typename Kernel_traits::SmemLayoutQ{});
Tensor sK = make_tensor(sQ.data() + size(sQ), typename Kernel_traits::SmemLayoutKV{});
Tensor sV = make_tensor(sK.data() + size(sK), typename Kernel_traits::SmemLayoutKV{});
Tensor sVt = make_tensor(sV.data(), typename Kernel_traits::SmemLayoutVtransposed{});
Tensor sVtNoSwizzle = make_tensor(sV.data(), typename Kernel_traits::SmemLayoutVtransposedNoSwizzle{});
typename Kernel_traits::GmemTiledCopyQKV gmem_tiled_copy_QKV;
auto gmem_thr_copy_QKV = gmem_tiled_copy_QKV.get_thread_slice(tidx);
Tensor tQgQ = gmem_thr_copy_QKV.partition_S(gQ);
Tensor tQsQ = gmem_thr_copy_QKV.partition_D(sQ);
Tensor tKgK = gmem_thr_copy_QKV.partition_S(gK); // (KCPY, KCPY_N, KCPY_K)
Tensor tKgKnew = gmem_thr_copy_QKV.partition_S(gKnew); // (KCPY, KCPY_N, KCPY_K)
Tensor tKsK = gmem_thr_copy_QKV.partition_D(sK);
Tensor tVgV = gmem_thr_copy_QKV.partition_S(gV); // (VCPY, VCPY_N, VCPY_K)
Tensor tVgVnew = gmem_thr_copy_QKV.partition_S(gVnew); // (VCPY, VCPY_N, VCPY_K)
Tensor tVsV = gmem_thr_copy_QKV.partition_D(sV);
typename Kernel_traits::TiledMma tiled_mma;
auto thr_mma = tiled_mma.get_thread_slice(tidx);
Tensor tSrQ = thr_mma.partition_fragment_A(sQ); // (MMA,MMA_M,MMA_K)
Tensor tSrK = thr_mma.partition_fragment_B(sK); // (MMA,MMA_N,MMA_K)
Tensor tOrVt = thr_mma.partition_fragment_B(sVtNoSwizzle); // (MMA, MMA_K,MMA_N)
Tensor acc_o = partition_fragment_C(tiled_mma, Shape<Int<kBlockM>, Int<kHeadDim>>{}); // MMA, MMA_M, MMA_K
//
// Copy Atom retiling
//
auto smem_tiled_copy_Q = make_tiled_copy_A(typename Kernel_traits::SmemCopyAtom{}, tiled_mma);
auto smem_thr_copy_Q = smem_tiled_copy_Q.get_thread_slice(tidx);
Tensor tSsQ = smem_thr_copy_Q.partition_S(sQ);
auto smem_tiled_copy_K = make_tiled_copy_B(typename Kernel_traits::SmemCopyAtom{}, tiled_mma);
auto smem_thr_copy_K = smem_tiled_copy_K.get_thread_slice(tidx);
Tensor tSsK = smem_thr_copy_K.partition_S(sK);
auto smem_tiled_copy_V = make_tiled_copy_B(typename Kernel_traits::SmemCopyAtomTransposed{}, tiled_mma);
auto smem_thr_copy_V = smem_tiled_copy_V.get_thread_slice(tidx);
Tensor tOsVt = smem_thr_copy_V.partition_S(sVt);
// TODO: this might need to change if we change the mma instruction in SM70
Tensor scores_max = make_tensor<ElementAccum>(Shape<Int<2 * size<1>(acc_o)>>{});
Tensor scores_sum = make_fragment_like(scores_max);
//
// PREDICATES
//
// // Allocate predicate tensors for m and n
// Tensor tQpQ = make_tensor<bool>(make_shape(size<1>(tQsQ), size<2>(tQsQ)), Stride<_1,_0>{});
// Tensor tKVpKV = make_tensor<bool>(make_shape(size<1>(tKsK), size<2>(tKsK)), Stride<_1,_0>{});
// Construct identity layout for sQ and sK
Tensor cQ = make_identity_tensor(make_shape(size<0>(sQ), size<1>(sQ))); // (BLK_M,BLK_K) -> (blk_m,blk_k)
Tensor cKV = make_identity_tensor(make_shape(size<0>(sK), size<1>(sK))); // (BLK_N,BLK_K) -> (blk_n,blk_k)
// Repeat the partitioning with identity layouts
Tensor tQcQ = gmem_thr_copy_QKV.partition_S(cQ); // (ACPY,ACPY_M,ACPY_K) -> (blk_m,blk_k)
Tensor tKVcKV = gmem_thr_copy_QKV.partition_S(cKV); // (BCPY,BCPY_N,BCPY_K) -> (blk_n,blk_k)
// Allocate predicate tensors for k
Tensor tQpQ = make_tensor<bool>(make_shape(size<2>(tQsQ)));
Tensor tKVpKV = make_tensor<bool>(make_shape(size<2>(tKsK)));
// Set predicates for k bounds
if (!Is_even_K) {
#pragma unroll
for (int k = 0; k < size(tQpQ); ++k) {
tQpQ(k) = get<1>(tQcQ(0, 0, k)) < params.d;
}
#pragma unroll
for (int k = 0; k < size(tKVpKV); ++k) {
tKVpKV(k) = get<1>(tKVcKV(0, 0, k)) < params.d;
}
}
// Prologue
Tensor tQrQ = make_fragment_like(tQgQ);
// We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
flash::copy<Is_even_MN, Is_even_K>(gmem_tiled_copy_QKV, tQgQ, tQsQ, tQcQ, tQpQ,
binfo.actual_seqlen_q - m_block * kBlockM);
int n_block = n_block_max - 1;
// We don't need to clear the sK smem tiles since we'll mask out the scores anyway.
flash::copy_2_sources</*Is_2_sources=*/Append_KV, Is_even_MN, Is_even_K>(
gmem_tiled_copy_QKV, tKgK, tKgKnew, tKsK, tKVcKV, tKVpKV,
binfo.actual_seqlen_k - n_block * kBlockN, binfo.seqlen_k_cache - n_block * kBlockN);
cute::cp_async_fence();
// flash::cp_async_wait<0>();
// __syncthreads();
// if (tidx == 0 && blockIdx.y == 0 && blockIdx.z == 0) { print(tKsK); }
// __syncthreads();
clear(acc_o);
// For performance reason, we separate out two kinds of iterations:
// those that need masking on S, and those that don't.
// We need masking on S for the very last block when K and V has length not multiple of kBlockN.
// We also need masking on S if it's causal, for the last ceil_div(kBlockM, kBlockN) blocks.
// We will have at least 1 "masking" iteration.
// If not even_N, then seqlen_k might end in the middle of a block. In that case we need to
// mask 2 blocks (e.g. when kBlockM == kBlockN), not just 1.
constexpr int n_masking_steps = !Is_causal
? 1
: (Is_even_MN ? cute::ceil_div(kBlockM, kBlockN) : cute::ceil_div(kBlockM, kBlockN) + 1);
#pragma unroll
for (int masking_step = 0; masking_step < n_masking_steps; ++masking_step, --n_block) {
Tensor acc_s = partition_fragment_C(tiled_mma, Shape<Int<kBlockM>, Int<kBlockN>>{}); // (MMA=4, MMA_M, MMA_N)
clear(acc_s);
flash::cp_async_wait<0>();
__syncthreads();
if constexpr (Append_KV) {
// if (cute::thread0()) { print(tKgK); }
// if (cute::thread0()) { print(tKsK); }
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("seqlen_k_cache = %d, (nblock + 1) * kBlockN = %d\n", binfo.seqlen_k_cache, (n_block + 1) * kBlockN); }
if (bidh % params.h_h_k_ratio == 0 && binfo.seqlen_k_cache < (n_block + 1) * kBlockN) {
flash::copy_w_min_idx<Is_even_K>(
tKsK, tKgK, tKVcKV, tKVpKV, binfo.actual_seqlen_k - n_block * kBlockN, binfo.seqlen_k_cache - n_block * kBlockN);
}
// __syncthreads();
// if (cute::thread0()) { print(tKgK); }
// __syncthreads();
}
// Advance gV
if (masking_step > 0) {
tVgV.data() = tVgV.data() + (-int(kBlockN * params.v_row_stride));
if (Append_KV) {
tVgVnew.data() = tVgVnew.data() + (-int(kBlockN * params.vnew_row_stride));
}
flash::copy_2_sources</*Is_2_sources=*/Append_KV, /*Is_even_MN=*/true, Is_even_K>(
gmem_tiled_copy_QKV, tVgV, tVgVnew, tVsV, tKVcKV, tKVpKV, 0, binfo.seqlen_k_cache - n_block * kBlockN);
} else {
// Clear the smem tiles to account for predicated off loads
flash::copy_2_sources</*Is_2_sources=*/Append_KV, Is_even_MN, Is_even_K, /*Clear_OOB_MN=*/true>(
gmem_tiled_copy_QKV, tVgV, tVgVnew, tVsV, tKVcKV, tKVpKV,
binfo.actual_seqlen_k - n_block * kBlockN, binfo.seqlen_k_cache - n_block * kBlockN);
}
cute::cp_async_fence();
flash::gemm(
acc_s, tSrQ, tSrK, tSsQ, tSsK, tiled_mma, smem_tiled_copy_Q, smem_tiled_copy_K,
smem_thr_copy_Q, smem_thr_copy_K);
// if (cute::thread0()) { print(acc_s); }
// Reshape acc_s from (MMA=4, MMA_M, MMA_N) to (nrow=(2, MMA_M), ncol=(2, MMA_N))
Tensor scores = make_tensor(acc_s.data(), flash::convert_layout_acc_rowcol(acc_s.layout()));
// if (cute::thread0()) { print(scores); }
// We don't put the masking before the matmul S = Q K^T because we don't clear sK
// for rows outside actual_seqlen_k. So those rows could have Inf / NaN, and the matmul
// can produce Inf / NaN.
if (!Is_causal) {
if (!Is_even_MN) {
flash::apply_mask(scores, binfo.actual_seqlen_k - n_block * kBlockN);
}
} else {
flash::apply_mask_causal(scores, n_block * kBlockN, binfo.actual_seqlen_k,
m_block * kBlockM + (tidx / 32) * 16 + (tidx % 32) / 4,
binfo.actual_seqlen_q,
kNWarps * 16);
}
flash::cp_async_wait<0>();
__syncthreads();
// if (tidx == 0 && blockIdx.y == 0 && blockIdx.z == 0) { print(tVsV); }
// __syncthreads();
// if (tidx == 0 && blockIdx.y == 1 && blockIdx.z == 0) { printf("n_block = %d, n_block_min = %d\n", n_block, n_block_min); }
if constexpr (Append_KV) {
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("n_split_idx = %d, bidh = %d, params.h_h_k_ratio = %d, seqlen_k_cache = %d, (nblock + 1) * kBlockN = %d\n", n_split_idx, bidh, params.h_h_k_ratio, binfo.seqlen_k_cache, (n_block + 1) * kBlockN); }
if (bidh % params.h_h_k_ratio == 0 && binfo.seqlen_k_cache < (n_block + 1) * kBlockN) {
flash::copy_w_min_idx<Is_even_K>(
tVsV, tVgV, tKVcKV, tKVpKV, binfo.actual_seqlen_k - n_block * kBlockN, binfo.seqlen_k_cache - n_block * kBlockN);
}
}
if (n_block > n_block_min) {
// Advance gK
// if (tidx == 0 && blockIdx.y == 1 && blockIdx.z == 0) { printf("tKgKnew = %p\n", tKgKnew.data()); }
tKgK.data() = tKgK.data() + (-int(kBlockN * params.k_row_stride));
if (Append_KV) {
tKgKnew.data() = tKgKnew.data() + (-int(kBlockN * params.knew_row_stride));
}
// if (tidx == 0 && blockIdx.y == 1 && blockIdx.z == 0) { printf("tKgKnew = %p, row_idx_switch = %d\n", tKgKnew.data(), binfo.seqlen_k_cache - (n_block - 1) * kBlockN); }
flash::copy_2_sources</*Is_2_sources=*/Append_KV, /*Is_even_MN=*/true, Is_even_K>(
gmem_tiled_copy_QKV, tKgK, tKgKnew, tKsK, tKVcKV, tKVpKV, 0,
binfo.seqlen_k_cache - (n_block - 1) * kBlockN);
// This cp_async_fence needs to be in the if block, otherwise the synchronization
// isn't right and we get race conditions.
cute::cp_async_fence();
}
// We have key_padding_mask so we'll need to Check_inf
masking_step == 0
? softmax_rescale_o</*Is_first=*/true, /*Check_inf=*/Is_causal || !Is_even_MN>(scores, scores_max, scores_sum, acc_o, params.scale_softmax_log2)
: softmax_rescale_o</*Is_first=*/false, /*Check_inf=*/Is_causal || !Is_even_MN>(scores, scores_max, scores_sum, acc_o, params.scale_softmax_log2);
// if (cute::thread0()) { print(scores_max); print(scores_sum); print(scores); }
// Convert scores from fp32 to fp16/bf16
Tensor rP = flash::convert_type<Element>(scores);
// Reshape rP from (nrow=(2, MMA_M), ncol=(2, MMA_N)) to ((2, 2, 2), MMA_M, MMA_N / 2)
// if using m16n8k16 or ((2, 2, 1), MMA_M, MMA_N) if using m16n8k8.
Tensor tOrP = make_tensor(rP.data(), flash::convert_layout_rowcol_Aregs<Kernel_traits::TiledMma>(rP.layout()));
flash::gemm_A_in_regs(acc_o, tOrP, tOrVt, tOsVt, tiled_mma, smem_tiled_copy_V, smem_thr_copy_V);
// if (cute::thread0()) { print(scores); }
// This check is at the end of the loop since we always have at least 1 iteration
if (n_masking_steps > 1 && n_block <= n_block_min) {
--n_block;
break;
}
}
// These are the iterations where we don't need masking on S
for (; n_block >= n_block_min; --n_block) {
Tensor acc_s = partition_fragment_C(tiled_mma, Shape<Int<kBlockM>, Int<kBlockN>>{}); // (MMA=4, MMA_M, MMA_N)
clear(acc_s);
flash::cp_async_wait<0>();
__syncthreads();
if constexpr (Append_KV) {
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("n_split_idx = %d, bidh = %d, params.h_h_k_ratio = %d, seqlen_k_cache = %d, (nblock + 1) * kBlockN = %d\n", n_split_idx, bidh, params.h_h_k_ratio, binfo.seqlen_k_cache, (n_block + 1) * kBlockN); }
if (bidh % params.h_h_k_ratio == 0 && binfo.seqlen_k_cache < (n_block + 1) * kBlockN) {
flash::copy_w_min_idx<Is_even_K>(
tKsK, tKgK, tKVcKV, tKVpKV, binfo.actual_seqlen_k - n_block * kBlockN, binfo.seqlen_k_cache - n_block * kBlockN);
}
}
// Advance gV
tVgV.data() = tVgV.data() + (-int(kBlockN * params.v_row_stride));
if (Append_KV) {
tVgVnew.data() = tVgVnew.data() + (-int(kBlockN * params.vnew_row_stride));
}
flash::copy_2_sources</*Is_2_sources=*/Append_KV, /*Is_even_MN=*/true, Is_even_K>(
gmem_tiled_copy_QKV, tVgV, tVgVnew, tVsV, tKVcKV, tKVpKV, 0, binfo.seqlen_k_cache - n_block * kBlockN);
cute::cp_async_fence();
flash::gemm(
acc_s, tSrQ, tSrK, tSsQ, tSsK, tiled_mma, smem_tiled_copy_Q, smem_tiled_copy_K,
smem_thr_copy_Q, smem_thr_copy_K);
flash::cp_async_wait<0>();
__syncthreads();
if constexpr (Append_KV) {
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("seqlen_k_cache = %d, (nblock + 1) * kBlockN = %d\n", binfo.seqlen_k_cache, (n_block + 1) * kBlockN); }
if (bidh % params.h_h_k_ratio == 0 && binfo.seqlen_k_cache < (n_block + 1) * kBlockN) {
flash::copy_w_min_idx<Is_even_K>(
tVsV, tVgV, tKVcKV, tKVpKV, binfo.actual_seqlen_k - n_block * kBlockN, binfo.seqlen_k_cache - n_block * kBlockN);
}
}
if (n_block > n_block_min) {
// Advance gK
tKgK.data() = tKgK.data() + (-int(kBlockN * params.k_row_stride));
if (Append_KV) {
tKgKnew.data() = tKgKnew.data() + (-int(kBlockN * params.knew_row_stride));
}
flash::copy_2_sources</*Is_2_sources=*/Append_KV, /*Is_even_MN=*/true, Is_even_K>(
gmem_tiled_copy_QKV, tKgK, tKgKnew, tKsK, tKVcKV, tKVpKV, 0,
binfo.seqlen_k_cache - (n_block - 1) * kBlockN);
// This cp_async_fence needs to be in the if block, otherwise the synchronization
// isn't right and we get race conditions.
cute::cp_async_fence();
}
// Reshape acc_s from (MMA=4, MMA_M, MMA_N) to (nrow=(2, MMA_M), ncol=(2, MMA_N))
Tensor scores = make_tensor(acc_s.data(), flash::convert_layout_acc_rowcol(acc_s.layout()));
softmax_rescale_o</*Is_first=*/false>(scores, scores_max, scores_sum, acc_o, params.scale_softmax_log2);
Tensor rP = flash::convert_type<Element>(scores);
// Reshape rP from (nrow=(2, MMA_M), ncol=(2, MMA_N)) to ((2, 2, 2), MMA_M, MMA_N / 2)
// if using m16n8k16 or ((2, 2, 1), MMA_M, MMA_N) if using m16n8k8.
Tensor tOrP = make_tensor(rP.data(), flash::convert_layout_rowcol_Aregs<Kernel_traits::TiledMma>(rP.layout()));
flash::gemm_A_in_regs(acc_o, tOrP, tOrVt, tOsVt, tiled_mma, smem_tiled_copy_V, smem_thr_copy_V);
}
// Epilogue
// Reshape acc_o from (MMA=4, MMA_M, MMA_K) to (nrow=(2, MMA_M), ncol=(2, MMA_K))
Tensor acc_o_rowcol = make_tensor(acc_o.data(), flash::convert_layout_acc_rowcol(acc_o.layout()));
// if (cute::thread0()) { print(acc_o_rowcol); }
Tensor lse = make_fragment_like(scores_sum);
#pragma unroll
for (int mi = 0; mi < size<0>(acc_o_rowcol); ++mi) {
float sum = scores_sum(mi);
float inv_sum = (sum == 0.f || sum != sum) ? 1.f : 1.f / sum;
lse(mi) = (sum == 0.f || sum != sum) ? (Split ? -INFINITY : INFINITY) : scores_max(mi) * params.scale_softmax + __logf(sum);
float scale = inv_sum;
#pragma unroll
for (int ni = 0; ni < size<1>(acc_o_rowcol); ++ni) {
acc_o_rowcol(mi, ni) *= scale;
}
}
// if (cute::thread0()) { print(lse); }
// if (cute::thread0()) { print(acc_o_rowcol); }
Tensor sOaccum = make_tensor(make_smem_ptr(reinterpret_cast<ElementO*>(smem_)), typename Kernel_traits::SmemLayoutO{}); // (SMEM_M,SMEM_N)
// Partition sO to match the accumulator partitioning
using SmemTiledCopyO = std::conditional_t<
!Split,
typename Kernel_traits::SmemCopyAtomO,
typename Kernel_traits::SmemCopyAtomOaccum>;
auto smem_tiled_copy_Oaccum = make_tiled_copy_C(SmemTiledCopyO{}, tiled_mma);
auto smem_thr_copy_Oaccum = smem_tiled_copy_Oaccum.get_thread_slice(tidx);
Tensor rO = flash::convert_type<ElementO>(acc_o);
Tensor taccOrOaccum = smem_thr_copy_Oaccum.retile_S(rO); // ((Atom,AtomNum), MMA_M, MMA_N)
Tensor taccOsOaccum = smem_thr_copy_Oaccum.partition_D(sOaccum); // ((Atom,AtomNum),PIPE_M,PIPE_N)
// sOaccum is larger than sQ, so we need to syncthreads here
// TODO: allocate enough smem for sOaccum
if constexpr (Split) {
__syncthreads();
}
cute::copy(smem_tiled_copy_Oaccum, taccOrOaccum, taccOsOaccum);
const index_t row_offset_o = binfo.q_offset(params.o_batch_stride, params.o_row_stride, bidb) + m_block * kBlockM * params.o_row_stride + bidh * params.o_head_stride;
const index_t row_offset_oaccum = (((n_split_idx * params.b + bidb) * params.h + bidh) * params.seqlen_q + m_block * kBlockM) * params.d_rounded;
const index_t row_offset_lseaccum = ((n_split_idx * params.b + bidb) * params.h + bidh) * params.seqlen_q + m_block * kBlockM;
Tensor gOaccum = make_tensor(make_gmem_ptr(reinterpret_cast<ElementO*>(Split ? params.oaccum_ptr : params.o_ptr) + (Split ? row_offset_oaccum : row_offset_o)),
Shape<Int<kBlockM>, Int<kHeadDim>>{},
make_stride(Split ? kHeadDim : params.o_row_stride, _1{}));
Tensor gLSEaccum = make_tensor(make_gmem_ptr(reinterpret_cast<ElementAccum*>(Split ? params.softmax_lseaccum_ptr : params.softmax_lse_ptr) + row_offset_lseaccum),
Shape<Int<kBlockM>>{}, Stride<_1>{});
// if (tidx == 0) { printf("row_offset_o = %d, bidh = %d, gOaccum = %p\n", row_offset_o, bidh, gOaccum.data()); }
GmemTiledCopyO gmem_tiled_copy_Oaccum;
auto gmem_thr_copy_Oaccum = gmem_tiled_copy_Oaccum.get_thread_slice(tidx);
Tensor tOsOaccum = gmem_thr_copy_Oaccum.partition_S(sOaccum); // ((Atom,AtomNum),ATOM_M,ATOM_N)
Tensor tOgOaccum = gmem_thr_copy_Oaccum.partition_D(gOaccum);
__syncthreads();
Tensor tOrOaccum = make_tensor<ElementO>(shape(tOgOaccum));
cute::copy(gmem_tiled_copy_Oaccum, tOsOaccum, tOrOaccum);
Tensor caccO = make_identity_tensor(Shape<Int<kBlockM>, Int<kHeadDim>>{}); // (BLK_M,BLK_K) -> (blk_m,blk_k)
Tensor taccOcO = thr_mma.partition_C(caccO); // (MMA,MMA_M,MMA_K)
static_assert(decltype(size<0>(taccOcO))::value == 4);
// Convert to ((2, 2), MMA_M, MMA_K) then take only the row indices.
Tensor taccOcO_row = logical_divide(taccOcO, Shape<_2>{})(make_coord(0, _), _, 0);
CUTE_STATIC_ASSERT_V(size(lse) == size(taccOcO_row)); // MMA_M
if (get<1>(taccOcO_row(0)) == 0) {
#pragma unroll
for (int mi = 0; mi < size(lse); ++mi) {
const int row = get<0>(taccOcO_row(mi));
if (row < binfo.actual_seqlen_q - m_block * kBlockM) {
gLSEaccum(row) = lse(mi);
}
}
}
// Construct identity layout for sO
Tensor cO = make_identity_tensor(make_shape(size<0>(sOaccum), size<1>(sOaccum))); // (BLK_M,BLK_K) -> (blk_m,blk_k)
// Repeat the partitioning with identity layouts
Tensor tOcO = gmem_thr_copy_Oaccum.partition_D(cO); // (ACPY,ACPY_M,ACPY_K) -> (blk_m,blk_k)
Tensor tOpO = make_tensor<bool>(make_shape(size<2>(tOgOaccum)));
if (!Is_even_K) {
#pragma unroll
for (int k = 0; k < size(tOpO); ++k) {
tOpO(k) = get<1>(tOcO(0, 0, k)) < params.d;
}
}
// Clear_OOB_K must be false since we don't want to write zeros to gmem
flash::copy<Is_even_MN, Is_even_K, /*Clear_OOB_MN=*/false, /*Clear_OOB_K=*/false>(
gmem_tiled_copy_Oaccum, tOrOaccum, tOgOaccum, tOcO, tOpO, binfo.actual_seqlen_q - m_block * kBlockM);
// __syncthreads();
// if (cute::thread0()) { print(tOgOaccum); }
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Kernel_traits, bool Is_causal, bool Is_even_MN, bool Is_even_K, bool Return_softmax, typename Params>
inline __device__ void compute_attn(const Params& params) {
const int m_block = blockIdx.x;
@ -524,6 +1051,187 @@ inline __device__ void compute_attn(const Params& params) {
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Kernel_traits, bool Is_causal, bool Is_even_MN, bool Is_even_K, bool Split, bool Append_KV, typename Params>
inline __device__ void compute_attn_splitkv(const Params& params) {
const int m_block = blockIdx.x;
// The block index for the batch.
const int bidb = Split ? blockIdx.z / params.h : blockIdx.y;
// The block index for the head.
const int bidh = Split ? blockIdx.z - bidb * params.h : blockIdx.z;
const int n_split_idx = Split ? blockIdx.y : 0;
const int num_n_splits = Split ? gridDim.y : 1;
flash::compute_attn_1rowblock_splitkv<Kernel_traits, Is_causal, Is_even_MN, Is_even_K, Split, Append_KV>(params, bidb, bidh, m_block, n_split_idx, num_n_splits);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename Kernel_traits, int Log_max_splits, bool Is_even_K, typename Params>
inline __device__ void combine_attn_seqk_parallel(const Params& params) {
using Element = typename Kernel_traits::Element;
using ElementAccum = typename Kernel_traits::ElementAccum;
using index_t = typename Kernel_traits::index_t;
constexpr int kMaxSplits = 1 << Log_max_splits;
constexpr int kBlockM = 16;
constexpr int kHeadDim = Kernel_traits::kHeadDim;
static_assert(kMaxSplits <= 128, "kMaxSplits must be <= 128");
// static_assert(kMaxSplits <= 8, "kMaxSplits must be <= 8 for now, will extend layer");
static_assert(kBlockM == 16 || kBlockM == 32, "kBlockM must be 16 or 32");
static_assert(Kernel_traits::kNThreads == 128, "We assume that each block has 128 threads");
// Shared memory.
// kBlockM + 1 instead of kBlockM to reduce bank conflicts.
__shared__ ElementAccum sLSE[kMaxSplits][kBlockM + 1];
// The thread and block index.
const int tidx = threadIdx.x;
const int bidx = blockIdx.x;
const index_t row_offset_lse = bidx * kBlockM;
Tensor gLSEaccum = make_tensor(make_gmem_ptr(reinterpret_cast<ElementAccum*>(params.softmax_lseaccum_ptr) + row_offset_lse),
Shape<Int<kMaxSplits>, Int<kBlockM>>{},
make_stride(params.b * params.h * params.seqlen_q, _1{}));
Tensor gLSE = make_tensor(make_gmem_ptr(reinterpret_cast<ElementAccum*>(params.softmax_lse_ptr) + row_offset_lse),
Shape<Int<kBlockM>>{}, Stride<_1>{});
constexpr int kNLsePerThread = (kMaxSplits * kBlockM + Kernel_traits::kNThreads - 1) / Kernel_traits::kNThreads;
// Read the LSE values from gmem and store them in shared memory, then tranpose them.
constexpr int kRowsPerLoadLSE = Kernel_traits::kNThreads / kBlockM;
#pragma unroll
for (int l = 0; l < kNLsePerThread; ++l) {
const int row = l * kRowsPerLoadLSE + tidx / kBlockM;
const int col = tidx % kBlockM;
ElementAccum lse = (row < params.num_splits && col < params.b * params.h * params.seqlen_q - bidx * kBlockM) ? gLSEaccum(row, col) : -INFINITY;
if (row < kMaxSplits) {
sLSE[row][col] = lse;
}
// if (bidx == 0 && tidx < 32) { printf("tidx = %d, row = %d, col = %d, lse = %f\n", tidx, row, col, lse_accum(l)); }
}
// if (bidx == 1 && tidx < 32) { printf("tidx = %d, row_offset_lse = %d, lse = %f\n", tidx, row_offset_lse, lse_accum(0)); }
__syncthreads();
Tensor lse_accum = make_tensor<ElementAccum>(Shape<Int<kNLsePerThread>>{});
constexpr int kRowsPerLoadTranspose = std::min(kRowsPerLoadLSE, kMaxSplits);
// To make sure that kMaxSplits is within 1 warp: we decide how many elements within kMaxSplits
// each thread should hold. If kMaxSplits = 16, then each thread holds 2 elements (128 threads,
// 16 rows, so each time we load we can load 8 rows).
// constexpr int kThreadsPerSplit = kMaxSplits / kRowsPerLoadTranspose;
// static_assert(kThreadsPerSplit <= 32);
static_assert(kRowsPerLoadTranspose <= 32);
static_assert(kNLsePerThread * kRowsPerLoadTranspose <= kMaxSplits);
#pragma unroll
for (int l = 0; l < kNLsePerThread; ++l) {
const int row = l * kRowsPerLoadTranspose + tidx % kRowsPerLoadTranspose;
const int col = tidx / kRowsPerLoadTranspose;
lse_accum(l) = (row < kMaxSplits && col < kBlockM) ? sLSE[row][col] : -INFINITY;
// if (bidx == 0 && tidx < 32) { printf("tidx = %d, row = %d, col = %d, lse = %f\n", tidx, row, col, lse_accum(l)); }
}
// Compute the logsumexp of the LSE along the split dimension.
ElementAccum lse_max = lse_accum(0);
#pragma unroll
for (int l = 1; l < kNLsePerThread; ++l) {
lse_max = max(lse_max, lse_accum(l));
}
MaxOp<float> max_op;
lse_max = Allreduce<kRowsPerLoadTranspose>::run(lse_max, max_op);
lse_max = lse_max == -INFINITY ? 0.0f : lse_max; // In case all local LSEs are -inf
float lse_sum = expf(lse_accum(0) - lse_max);
#pragma unroll
for (int l = 1; l < kNLsePerThread; ++l) {
lse_sum += expf(lse_accum(l) - lse_max);
}
SumOp<float> sum_op;
lse_sum = Allreduce<kRowsPerLoadTranspose>::run(lse_sum, sum_op);
// For the case where all local lse == -INFINITY, we want to set lse_logsum to INFINITY. Otherwise
// lse_logsum is log(0.0) = -INFINITY and we get NaN when we do lse_accum(l) - lse_logsum.
ElementAccum lse_logsum = (lse_sum == 0.f || lse_sum != lse_sum) ? INFINITY : logf(lse_sum) + lse_max;
// if (bidx == 0 && tidx < 32) { printf("tidx = %d, lse = %f, lse_max = %f, lse_logsum = %f\n", tidx, lse_accum(0), lse_max, lse_logsum); }
if (tidx % kRowsPerLoadTranspose == 0 && tidx / kRowsPerLoadTranspose < kBlockM) {
gLSE(tidx / kRowsPerLoadTranspose) = lse_logsum;
}
// Store the scales exp(lse - lse_logsum) in shared memory.
#pragma unroll
for (int l = 0; l < kNLsePerThread; ++l) {
const int row = l * kRowsPerLoadTranspose + tidx % kRowsPerLoadTranspose;
const int col = tidx / kRowsPerLoadTranspose;
if (row < params.num_splits && col < kBlockM) {
sLSE[row][col] = expf(lse_accum(l) - lse_logsum);
}
}
__syncthreads();
const index_t row_offset_oaccum = bidx * kBlockM * params.d_rounded;
Tensor gOaccum = make_tensor(make_gmem_ptr(reinterpret_cast<ElementAccum*>(params.oaccum_ptr) + row_offset_oaccum),
Shape<Int<kBlockM>, Int<kHeadDim>>{},
Stride<Int<kHeadDim>, _1>{});
typename Kernel_traits::GmemTiledCopyOaccum gmem_tiled_copy_Oaccum;
auto gmem_thr_copy_Oaccum = gmem_tiled_copy_Oaccum.get_thread_slice(tidx);
Tensor tOgOaccum = gmem_thr_copy_Oaccum.partition_S(gOaccum);
Tensor tOrO = make_tensor<ElementAccum>(shape(tOgOaccum));
Tensor tOrOaccum = make_tensor<ElementAccum>(shape(tOgOaccum));
clear(tOrO);
// Predicates
Tensor cOaccum = make_identity_tensor(Shape<Int<kBlockM>, Int<kHeadDim>>{});
// Repeat the partitioning with identity layouts
Tensor tOcOaccum = gmem_thr_copy_Oaccum.partition_S(cOaccum);
Tensor tOpOaccum = make_tensor<bool>(make_shape(size<2>(tOgOaccum)));
if (!Is_even_K) {
#pragma unroll
for (int k = 0; k < size(tOpOaccum); ++k) {
tOpOaccum(k) = get<1>(tOcOaccum(0, 0, k)) < params.d;
}
}
// Load Oaccum in then scale and accumulate to O
#pragma unroll 2
for (int split = 0; split < params.num_splits; ++split) {
flash::copy</*Is_even_MN=*/false, Is_even_K>(
gmem_tiled_copy_Oaccum, tOgOaccum, tOrOaccum, tOcOaccum, tOpOaccum, params.b * params.h * params.seqlen_q - bidx * kBlockM);
#pragma unroll
for (int m = 0; m < size<1>(tOrOaccum); ++m) {
int row = get<0>(tOcOaccum(0, m, 0));
ElementAccum lse_scale = sLSE[split][row];
#pragma unroll
for (int k = 0; k < size<2>(tOrOaccum); ++k) {
#pragma unroll
for (int i = 0; i < size<0>(tOrOaccum); ++i) {
tOrO(i, m, k) += lse_scale * tOrOaccum(i, m, k);
}
}
// if (cute::thread0()) { printf("lse_scale = %f, %f\n", sLSE[split][0], sLSE[split][1]); print(tOrOaccum); print(tOrO); }
}
tOgOaccum.data() = tOgOaccum.data() + params.b * params.h * params.seqlen_q * params.d_rounded;
}
// if (cute::thread0()) { print(tOrO); }
Tensor rO = flash::convert_type<Element>(tOrO);
// Write to gO
#pragma unroll
for (int m = 0; m < size<1>(rO); ++m) {
const int idx = bidx * kBlockM + get<0>(tOcOaccum(0, m, 0));
if (idx < params.b * params.h * params.seqlen_q) {
const int batch_idx = idx / (params.h * params.seqlen_q);
const int head_idx = (idx - batch_idx * (params.h * params.seqlen_q)) / params.seqlen_q;
// The index to the rows of Q
const int row = idx - batch_idx * (params.h * params.seqlen_q) - head_idx * params.seqlen_q;
auto o_ptr = reinterpret_cast<Element*>(params.o_ptr) + batch_idx * params.o_batch_stride + head_idx * params.o_head_stride + row * params.o_row_stride;
#pragma unroll
for (int k = 0; k < size<2>(rO); ++k) {
if (Is_even_K || tOpOaccum(k)) {
const int col = get<1>(tOcOaccum(0, m, k));
Tensor gO = make_tensor(make_gmem_ptr(o_ptr + col),
Shape<Int<decltype(size<0>(rO))::value>>{}, Stride<_1>{});
// TODO: Should check if this is using vectorized store, but it seems pretty fast
copy(rO(_, m, k), gO);
// if (bidx == 0 && tidx == 0) { printf("tidx = %d, idx = %d, batch_idx = %d, head_idx = %d, row = %d, col = %d\n", tidx, idx, batch_idx, head_idx, row, col); print(rO(_, m, k)); print(gO); }
// reinterpret_cast<uint64_t *>(o_ptr)[col / 4] = recast<uint64_t>(rO)(0, m, k);
}
}
}
}
}
} // namespace flash
} // namespace onnxruntime

View file

@ -15,6 +15,17 @@ __global__ void flash_fwd_kernel(Flash_fwd_params params) {
flash::compute_attn<Kernel_traits, Is_causal, Is_even_MN, Is_even_K, Return_softmax>(params);
}
template <typename Kernel_traits, bool Is_causal, bool Is_even_MN, bool Is_even_K, bool Split, bool Append_KV>
__global__ void flash_fwd_splitkv_kernel(Flash_fwd_params params) {
flash::compute_attn_splitkv<Kernel_traits, Is_causal, Is_even_MN, Is_even_K, Split, Append_KV>(params);
}
template <typename Kernel_traits, int Log_max_splits, bool Is_even_K>
__global__ void flash_fwd_splitkv_combine_kernel(Flash_fwd_params params) {
static_assert(Log_max_splits >= 1);
flash::combine_attn_seqk_parallel<Kernel_traits, Log_max_splits, Is_even_K>(params);
}
template <typename Kernel_traits, bool Is_causal>
void run_flash_fwd(Flash_fwd_params& params, cudaStream_t stream) {
constexpr size_t smem_size = Kernel_traits::kSmemSize;
@ -25,8 +36,6 @@ void run_flash_fwd(Flash_fwd_params& params, cudaStream_t stream) {
const int num_m_block = (params.seqlen_q + Kernel_traits::kBlockM - 1) / Kernel_traits::kBlockM;
dim3 grid(num_m_block, params.b, params.h);
// We also use is_even_N to set Unpadded in the BlockInfo constructor, so we need to check
// for cu_seqlens_q as well.
const bool is_even_MN = params.cu_seqlens_q == nullptr && params.cu_seqlens_k == nullptr && params.seqlen_k % Kernel_traits::kBlockN == 0 && params.seqlen_q % Kernel_traits::kBlockM == 0;
const bool is_even_K = params.d == Kernel_traits::kHeadDim;
BOOL_SWITCH(is_even_MN, IsEvenMNConst, [&] {
@ -40,9 +49,7 @@ void run_flash_fwd(Flash_fwd_params& params, cudaStream_t stream) {
// ORT_ENFORCE(cudaFuncSetAttribute(
// kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size));
}
int ctas_per_sm;
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&ctas_per_sm, kernel, Kernel_traits::kNThreads, smem_size);
// int ctas_per_sm;
// cudaError status_ = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
// &ctas_per_sm, kernel, Kernel_traits::kNThreads, smem_size);
// printf("smem_size = %d, CTAs per SM = %d\n", int(smem_size), ctas_per_sm);
@ -51,6 +58,72 @@ void run_flash_fwd(Flash_fwd_params& params, cudaStream_t stream) {
});
}
template <typename Kernel_traits>
void run_flash_splitkv_fwd(Flash_fwd_params& params, cudaStream_t stream) {
static_assert(!Kernel_traits::Is_Q_in_regs, "SplitKV implementation does not support Is_Q_in_regs");
static_assert(!Kernel_traits::Share_Q_K_smem, "SplitKV implementation does not support Share_Q_K_smem");
constexpr size_t smem_size = Kernel_traits::kSmemSize;
const int num_m_block = (params.seqlen_q + Kernel_traits::kBlockM - 1) / Kernel_traits::kBlockM;
dim3 grid(num_m_block, params.num_splits > 1 ? params.num_splits : params.b, params.num_splits > 1 ? params.b * params.h : params.h);
const bool is_even_MN = params.cu_seqlens_q == nullptr && params.cu_seqlens_k == nullptr && params.seqlen_k % Kernel_traits::kBlockN == 0 && params.seqlen_q % Kernel_traits::kBlockM == 0;
const bool is_even_K = params.d == Kernel_traits::kHeadDim;
BOOL_SWITCH(params.is_causal, Is_causal, [&] {
BOOL_SWITCH(is_even_MN, IsEvenMNConst, [&] {
BOOL_SWITCH(is_even_K, IsEvenKConst, [&] {
BOOL_SWITCH(params.num_splits > 1, Split, [&] {
BOOL_SWITCH(params.knew_ptr != nullptr, Append_KV, [&] {
// If Append_KV, then we must have seqlen_offsets, which means cu_seqlens_k != nullptr.
// printf("About to launch, Split = %d, Append_KV = %d, knew_ptr = %p\n", Split, Append_KV, params.knew_ptr);
auto kernel = &flash_fwd_splitkv_kernel < Kernel_traits, Is_causal, IsEvenMNConst && !Append_KV, IsEvenKConst, Split, Append_KV > ;
// auto kernel = &flash_fwd_splitkv_kernel<Kernel_traits, Is_causal, false, true, Split, Append_KV>;
// auto kernel = &flash_fwd_splitkv_kernel<Kernel_traits, Is_causal, false, IsEvenKConst>;
if (smem_size >= 48 * 1024) {
cudaFuncSetAttribute(
kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size);
}
kernel<<<grid, Kernel_traits::kNThreads, smem_size, stream>>>(params);
});
});
});
});
});
if (params.num_splits > 1) {
dim3 grid_combine((params.b * params.h * params.seqlen_q + 16 - 1) / 16);
BOOL_SWITCH(is_even_K, IsEvenKConst, [&] {
if (params.num_splits <= 2) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 1, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
} else if (params.num_splits <= 4) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 2, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
} else if (params.num_splits <= 8) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 3, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
} else if (params.num_splits <= 16) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 4, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
} else if (params.num_splits <= 32) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 5, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
} else if (params.num_splits <= 64) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 6, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
} else if (params.num_splits <= 128) {
flash_fwd_splitkv_combine_kernel<Kernel_traits, 7, IsEvenKConst><<<grid_combine, Kernel_traits::kNThreads, 0, stream>>>(params);
}
});
}
}
template <typename T, int Headdim>
void run_mha_fwd_splitkv_dispatch(Flash_fwd_params& params, cudaStream_t stream) {
bool is_sm8x = params.dprops->major == 8 && params.dprops->minor > 0;
constexpr int kBlockM = 64; // Fixed for all head dimensions
if (!is_sm8x) { // A100, H100
// TD [2023-08-28]: nvcc segfaults for headdim 96 with block size 64 x 256,
// and for headdim 192 with block size 64 x 128.
constexpr int kBlockN = Headdim <= 64 ? 256 : (Headdim <= 160 ? 128 : 64);
run_flash_splitkv_fwd<Flash_fwd_kernel_traits<Headdim, kBlockM, kBlockN, 4, false, false, T>>(params, stream);
} else { // Only 99KB of smem, so we have to set kBlockN smaller for Headdim 160 and above
constexpr int kBlockN = Headdim <= 64 ? 256 : (Headdim <= 128 ? 128 : 64);
run_flash_splitkv_fwd<Flash_fwd_kernel_traits<Headdim, kBlockM, kBlockN, 4, false, false, T>>(params, stream);
}
}
template <typename T>
void run_mha_fwd_hdim32(Flash_fwd_params& params, cudaStream_t stream) {
constexpr int Headdim = 32;

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 128>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 160>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 192>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 224>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 256>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 32>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 64>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -0,0 +1,15 @@
// Copyright (c) 2023, Tri Dao.
// Splitting the different head dimensions to different files to speed up compilation.
#if USE_FLASH_ATTENTION
#include "contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h"
namespace onnxruntime {
namespace flash {
template void run_mha_fwd_splitkv_dispatch<cutlass::half_t, 96>(Flash_fwd_params& params, cudaStream_t stream);
} // namespace flash
} // namespace onnxruntime
#endif

View file

@ -111,7 +111,8 @@ struct Flash_fwd_kernel_traits : public Base {
using SmemLayoutO = decltype(tile_to_shape(
SmemLayoutAtomO{},
Shape<Int<kBlockM>, Int<kHeadDim>>{}));
using SmemCopyAtomO = Copy_Atom<DefaultCopy, elem_type>;
using SmemCopyAtomO = Copy_Atom<DefaultCopy, Element>;
using SmemCopyAtomOaccum = Copy_Atom<DefaultCopy, ElementAccum>;
static constexpr int kSmemQCount = cute::size(SmemLayoutQ{});
static constexpr int kSmemKVCount = cute::size(SmemLayoutKV{}) * 2;
@ -139,18 +140,28 @@ struct Flash_fwd_kernel_traits : public Base {
DefaultCopy>;
using GmemTiledCopyQKV = decltype(make_tiled_copy(Copy_Atom<Gmem_copy_struct, elem_type>{},
GmemLayoutAtom{},
Layout<Shape<_1, _8>>{})); // Val layout, 8 vals per read
cute::Layout<cute::Shape<_1, _8>>{})); // Val layout, 8 vals per read
using GmemTiledCopyO = decltype(make_tiled_copy(Copy_Atom<DefaultCopy, elem_type>{},
GmemLayoutAtom{},
Layout<Shape<_1, _8>>{})); // Val layout, 8 vals per store
cute::Layout<cute::Shape<_1, _8>>{})); // Val layout, 8 vals per store
static constexpr int kGmemThreadsPerRowP = kBlockN / kGmemElemsPerLoad;
static_assert(kNThreads % kGmemThreadsPerRowP == 0, "kNThreads must be a multiple of kGmemThreadsPerRowP");
using GmemLayoutAtomP = Layout<Shape<Int<kNThreads / kGmemThreadsPerRowP>, Int<kGmemThreadsPerRowP>>,
Stride<Int<kGmemThreadsPerRowP>, _1>>;
using GmemLayoutAtomP = cute::Layout<cute::Shape<cute::Int<kNThreads / kGmemThreadsPerRowP>, cute::Int<kGmemThreadsPerRowP>>,
cute::Stride<cute::Int<kGmemThreadsPerRowP>, _1>>;
using GmemTiledCopyP = decltype(make_tiled_copy(Copy_Atom<DefaultCopy, elem_type>{},
GmemLayoutAtomP{},
Layout<Shape<_1, _8>>{})); // Val layout, 8 vals per store
cute::Layout<cute::Shape<_1, _8>>{})); // Val layout, 8 vals per store
using GmemLayoutAtomOaccum = std::conditional_t<
kBlockKSmem == 32,
cute::Layout<cute::Shape<_16, _8>, // Thread layout, 8 threads per row
cute::Stride<_8, _1>>,
cute::Layout<cute::Shape<_8, _16>, // Thread layout, 16 threads per row
cute::Stride<_16, _1>>>;
using GmemTiledCopyOaccum = decltype(make_tiled_copy(Copy_Atom<DefaultCopy, ElementAccum>{},
GmemLayoutAtomOaccum{},
cute::Layout<cute::Shape<_1, _4>>{})); // Val layout, 4 vals per store
};
// Is_V_in_regs is an option to reduce smem usage, but will increase register pressue.
@ -289,13 +300,13 @@ struct Flash_bwd_kernel_traits : public Base {
static constexpr int kSmemdSCount = cute::size(SmemLayoutPdS{});
static constexpr int kSmemPCount = cute::size(SmemLayoutPdS{});
static constexpr int kSmemdQCount = cute::size(SmemLayoutdQ{});
static constexpr int kSmemdPsumCount = kBlockM;
// static constexpr int kSmemdPsumCount = kBlockM;
static constexpr int kSmemQdOSize = kSmemQdOCount * sizeof(Element);
static constexpr int kSmemKVSize = kSmemKVCount * sizeof(Element);
static constexpr int kSmemdSSize = kSmemdSCount * sizeof(Element);
static constexpr int kSmemPSize = kSmemPCount * sizeof(Element);
static constexpr int kSmemdQSize = kSmemdQCount * sizeof(Element);
static constexpr int kSmemdPsumSize = kSmemdPsumCount * sizeof(ElementAccum);
// static constexpr int kSmemdPsumSize = kSmemdPsumCount * sizeof(ElementAccum);
static constexpr int kSmemSize = kSmemQdOSize + (!Is_V_in_regs
? kSmemKVSize + kSmemdSSize + std::max(kSmemPSize, kSmemdQSize)
: std::max(kSmemKVSize, kSmemKVSize / 2 + kSmemdSSize + std::max(kSmemPSize, kSmemdQSize)));

View file

@ -96,46 +96,6 @@ inline __device__ uint32_t convert_relu2<cutlass::bfloat16_t>(const float2 x) {
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename T>
inline __device__ float2 half2_unpack(uint32_t a);
template <>
inline __device__ float2 half2_unpack<__half>(uint32_t a) {
return __half22float2(reinterpret_cast<__half2(&)>(a));
}
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800
template <>
inline __device__ float2 half2_unpack<__nv_bfloat16>(uint32_t a) {
return __bfloat1622float2(reinterpret_cast<__nv_bfloat162(&)>(a));
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////
// Convert two half2's or bf162's into float, then take their dot product.
template <typename T>
inline __device__ float hfma2_to_float(const uint32_t a, const uint32_t b) {
float2 af = flash::half2_unpack<T>(a);
float2 bf = flash::half2_unpack<T>(b);
return af.x * bf.x + af.y * bf.y;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Converted two vectors of 8 half's or bf16's into float, then take their dot product.
template <typename T>
inline __device__ float hmulsum8(const uint4 a, const uint4 b) {
float sum;
sum = flash::hfma2_to_float<T>(a.x, b.x);
sum += flash::hfma2_to_float<T>(a.y, b.y);
sum += flash::hfma2_to_float<T>(a.z, b.z);
sum += flash::hfma2_to_float<T>(a.w, b.w);
return sum;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename T>
struct MaxOp {
__device__ inline T operator()(T const& x, T const& y) { return x > y ? x : y; }
@ -245,7 +205,10 @@ inline __device__ auto convert_layout_acc_rowcol(Layout acc_layout) {
static_assert(decltype(size<0>(acc_layout))::value == 4);
static_assert(decltype(rank(acc_layout))::value == 3);
auto l = logical_divide(acc_layout, Shape<_2>{}); // ((2, 2), MMA_M, MMA_N)
return make_layout(make_layout(get<0, 1>(l), get<1>(l)), make_layout(get<0, 0>(l), get<2>(l)));
// TD [2023-08-13]: Idk why but get<0, 1>(l) doesn't work for Cutlass 3.2, I'm getting
// "int_tuple.hpp(74): error: conversion to inaccessible base class"
// return make_layout(make_layout(get<0, 1>(l), get<1>(l)), make_layout(get<0, 0>(l), get<2>(l)));
return make_layout(make_layout(get<1>(get<0>(l)), get<1>(l)), make_layout(get<0>(get<0>(l)), get<2>(l)));
};
////////////////////////////////////////////////////////////////////////////////////////////////////
@ -261,9 +224,13 @@ inline __device__ auto convert_layout_rowcol_Aregs(Layout rowcol_layout) {
static_assert(mma_shape_K == 8 || mma_shape_K == 16);
constexpr int MMA_N_divisor = mma_shape_K == 8 ? 1 : 2;
auto l = logical_divide(rowcol_layout, Shape<X, Shape<X, Int<MMA_N_divisor>>>{}); // ((2, MMA_M), (2, (2, MMA_N / 2)))
return make_layout(make_layout(get<1, 0>(l), get<0, 0>(l), get<1, 1, 0>(l)),
get<0, 1>(l),
get<1, 1, 1>(l));
// TD [2023-08-13]: Same error as above on Cutlass 3.2
// return make_layout(make_layout(get<1, 0>(l), get<0, 0>(l), get<1, 1, 0>(l)),
// get<0, 1>(l),
// get<1, 1, 1>(l));
return make_layout(make_layout(get<0>(get<1>(l)), get<0>(get<0>(l)), get<0>(get<1>(get<1>(l)))),
get<1>(get<0>(l)),
get<1>(get<1>(get<1>(l))));
};
////////////////////////////////////////////////////////////////////////////////////////////////////
@ -338,7 +305,7 @@ CUTE_HOST_DEVICE void cp_async_wait() {
template <bool Is_even_MN = true, bool Is_even_K = true, bool Clear_OOB_MN = false, bool Clear_OOB_K = true,
typename TiledCopy, typename Engine0, typename Layout0, typename Engine1, typename Layout1,
typename Engine2, typename Layout2, typename Engine3, typename Layout3>
inline __device__ void copy(TiledCopy thr_copy, Tensor<Engine0, Layout0> const& S,
inline __device__ void copy(TiledCopy tiled_copy, Tensor<Engine0, Layout0> const& S,
Tensor<Engine1, Layout1>& D, Tensor<Engine2, Layout2> const& identity_MN,
Tensor<Engine3, Layout3> const& predicate_K, int max_MN = 0) {
CUTE_STATIC_ASSERT_V(rank(S) == Int<3>{});
@ -354,13 +321,80 @@ inline __device__ void copy(TiledCopy thr_copy, Tensor<Engine0, Layout0> const&
#pragma unroll
for (int k = 0; k < size<2>(S); ++k) {
if (Is_even_K || predicate_K(k)) {
copy(thr_copy, S(_, m, k), D(_, m, k));
cute::copy(tiled_copy, S(_, m, k), D(_, m, k));
} else if (Clear_OOB_K) {
clear(D(_, m, k));
cute::clear(D(_, m, k));
}
}
} else if (Clear_OOB_MN) {
clear(D(_, m, _));
cute::clear(D(_, m, _));
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <bool Is_2_sources = false, bool Is_even_MN = true, bool Is_even_K = true, bool Clear_OOB_MN = false, bool Clear_OOB_K = true,
typename TiledCopy, typename Engine0, typename Layout0, typename Engine1, typename Layout1,
typename Engine2, typename Layout2, typename Engine3, typename Layout3>
inline __device__ void copy_2_sources(TiledCopy tiled_copy, Tensor<Engine0, Layout0> const& S0,
Tensor<Engine0, Layout0> const& S1,
Tensor<Engine1, Layout1>& D, Tensor<Engine2, Layout2> const& identity_MN,
Tensor<Engine3, Layout3> const& predicate_K,
const int max_MN = 0, const int row_idx_switch = 0) {
CUTE_STATIC_ASSERT_V(rank(S0) == Int<3>{} && rank(S1) == Int<3>{});
CUTE_STATIC_ASSERT_V(rank(D) == Int<3>{});
CUTE_STATIC_ASSERT_V(size<0>(S0) == size<0>(D) && size<0>(S1) == size<0>(D)); // MMA
CUTE_STATIC_ASSERT_V(size<1>(S0) == size<1>(D) && size<1>(S1) == size<1>(D)); // MMA_M
CUTE_STATIC_ASSERT_V(size<2>(S0) == size<2>(D) && size<2>(S1) == size<2>(D)); // MMA_K
// There's no case where !Clear_OOB_K && Clear_OOB_MN
static_assert(!(Clear_OOB_MN && !Clear_OOB_K));
// if (threadIdx.x == 0 && blockIdx.y == 1 && blockIdx.z == 0) { printf("Is_2_sources = %d, max_MN = %d, row_idx_switch = %d\n", Is_2_sources, max_MN, row_idx_switch); }
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("blockIdx.y = %d, Is_2_sources = %d, max_MN = %d, row_idx_switch = %d\n", blockIdx.y, Is_2_sources, max_MN, row_idx_switch); }
#pragma unroll
for (int m = 0; m < size<1>(S0); ++m) {
auto& S = !Is_2_sources || get<0>(identity_MN(0, m, 0)) < row_idx_switch ? S0 : S1;
if (Is_even_MN || get<0>(identity_MN(0, m, 0)) < max_MN) {
#pragma unroll
for (int k = 0; k < size<2>(S0); ++k) {
if (Is_even_K || predicate_K(k)) {
cute::copy(tiled_copy, S(_, m, k), D(_, m, k));
} else if (Clear_OOB_K) {
cute::clear(D(_, m, k));
}
}
} else if (Clear_OOB_MN) {
cute::clear(D(_, m, _));
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
template <bool Is_even_K = true,
typename Engine0, typename Layout0, typename Engine1, typename Layout1,
typename Engine2, typename Layout2, typename Engine3, typename Layout3>
inline __device__ void copy_w_min_idx(Tensor<Engine0, Layout0> const& S,
Tensor<Engine1, Layout1>& D, Tensor<Engine2, Layout2> const& identity_MN,
Tensor<Engine3, Layout3> const& predicate_K,
const int max_MN = 0, const int min_MN = 0) {
CUTE_STATIC_ASSERT_V(rank(S) == Int<3>{});
CUTE_STATIC_ASSERT_V(rank(D) == Int<3>{});
CUTE_STATIC_ASSERT_V(size<0>(S) == size<0>(D)); // MMA
CUTE_STATIC_ASSERT_V(size<1>(S) == size<1>(D)); // MMA_M
CUTE_STATIC_ASSERT_V(size<2>(S) == size<2>(D)); // MMA_K
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("blockIdx.y = %d, max_MN = %d, min_MN = %d\n", blockIdx.y, max_MN, min_MN); }
#pragma unroll
for (int m = 0; m < size<1>(S); ++m) {
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("blockIdx.y = %d, m = %d\n", blockIdx.y, get<0>(identity_MN(0, m, 0))); }
if (get<0>(identity_MN(0, m, 0)) >= min_MN && get<0>(identity_MN(0, m, 0)) < max_MN) {
// if (threadIdx.x == 0 && blockIdx.z == 0) { printf("Inner loop, blockIdx.y = %d, m = %d\n", blockIdx.y, get<0>(identity_MN(0, m, 0))); }
#pragma unroll
for (int k = 0; k < size<2>(S); ++k) {
if (Is_even_K || predicate_K(k)) {
cute::copy(S(_, m, k), D(_, m, k));
}
}
}
}
}

View file

@ -0,0 +1,185 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "core/providers/cuda/cuda_common.h"
#include "core/platform/env_var_utils.h"
#include "contrib_ops/cuda/bert/group_query_attention_impl.h"
#include "contrib_ops/cuda/bert/group_query_attention.h"
#include "contrib_ops/cuda/bert/group_query_attention_helper.h"
#include "contrib_ops/cuda/bert/flash_attention/flash_api.h"
// #include "contrib_ops/cuda/transformers/dump_cuda_tensor.h"
// #include "contrib_ops/cpu/utils/console_dumper.h"
using namespace onnxruntime::cuda;
using namespace ::onnxruntime::common;
using namespace ONNX_NAMESPACE;
namespace onnxruntime {
namespace contrib {
namespace cuda {
#define REGISTER_KERNEL_TYPED(T) \
ONNX_OPERATOR_TYPED_KERNEL_EX( \
GroupQueryAttention, \
kMSDomain, \
1, \
T, \
kCudaExecutionProvider, \
(*KernelDefBuilder::Create()) \
.TypeConstraint("T", DataTypeImpl::GetTensorType<T>()) \
.TypeConstraint("M", {DataTypeImpl::GetTensorType<int32_t>(), DataTypeImpl::GetTensorType<int64_t>()}) \
.MayInplace(3, 1) \
.MayInplace(4, 2) \
.InputMemoryType(OrtMemTypeCPUInput, 5), \
GroupQueryAttention<T>);
// REGISTER_KERNEL_TYPED(float)
REGISTER_KERNEL_TYPED(MLFloat16)
template <typename T>
GroupQueryAttention<T>::GroupQueryAttention(const OpKernelInfo& info)
: CudaKernel(info) {
int64_t num_heads = 0;
int64_t kv_num_heads = 0;
ORT_ENFORCE(info.GetAttr("num_heads", &num_heads).IsOK() && num_heads > 0);
ORT_ENFORCE(info.GetAttr("kv_num_heads", &kv_num_heads).IsOK() && kv_num_heads > 0 && num_heads % kv_num_heads == 0);
num_heads_ = static_cast<int>(num_heads);
kv_num_heads_ = static_cast<int>(kv_num_heads);
is_unidirectional_ = info.GetAttrOrDefault<int64_t>("unidirectional", 1) == 1;
is_past_bsnh_ = info.GetAttrOrDefault<int64_t>("is_past_bsnh", 1) == 1;
scale_ = info.GetAttrOrDefault<float>("scale", 0.0f);
#if USE_FLASH_ATTENTION
disable_flash_attention_ = sizeof(T) != 2 ||
ParseEnvironmentVariableWithDefault<bool>(attention::kDisableFlashAttention, false);
#else
disable_flash_attention_ = true;
#endif
}
template <typename T>
Status GroupQueryAttention<T>::ComputeInternal(OpKernelContext* context) const {
const Tensor* query = context->Input<Tensor>(0);
const Tensor* key = context->Input<Tensor>(1);
const Tensor* value = context->Input<Tensor>(2);
const Tensor* past_key = context->Input<Tensor>(3);
const Tensor* past_value = context->Input<Tensor>(4);
const Tensor* past_seq_len = context->Input<Tensor>(5);
auto& device_prop = GetDeviceProp();
GroupQueryAttentionParameters parameters;
typedef typename ToCudaType<T>::MappedType CudaT;
GroupQueryAttentionData<CudaT> data;
ORT_RETURN_IF_ERROR(group_query_attention_helper::CheckInputs(query,
key,
value,
past_key,
past_value,
&parameters,
num_heads_,
kv_num_heads_,
past_seq_len,
is_past_bsnh_,
scale_,
device_prop.maxThreadsPerBlock));
parameters.is_unidirectional = is_unidirectional_;
int sequence_length = parameters.sequence_length;
TensorShapeVector output_shape(3);
output_shape[0] = static_cast<int64_t>(parameters.batch_size);
output_shape[1] = static_cast<int64_t>(sequence_length);
output_shape[2] = static_cast<int64_t>(parameters.hidden_size);
Tensor* output = context->Output(0, output_shape);
std::vector<int64_t> present_dims;
if (parameters.past_kv_format == AttentionQkvFormat::Q_K_V_BSNH) {
present_dims = {
parameters.batch_size, parameters.present_sequence_length, parameters.kv_num_heads, parameters.head_size};
} else { // BNSH
present_dims = {
parameters.batch_size, parameters.kv_num_heads, parameters.present_sequence_length, parameters.head_size};
}
TensorShape present_shape(present_dims);
Tensor* present_key = context->Output(1, present_shape);
Tensor* present_value = context->Output(2, present_shape);
#if USE_FLASH_ATTENTION
bool use_flash_attention = !disable_flash_attention_ &&
onnxruntime::flash::is_supported(device_prop,
parameters.head_size,
parameters.num_heads,
parameters.kv_num_heads);
// Allocate buffers
size_t softmax_lse_bytes = 0;
size_t softmax_lse_accum_bytes = 0;
size_t out_accum_bytes = 0;
size_t seqlens_k_bytes = 0;
if (use_flash_attention) {
softmax_lse_bytes = onnxruntime::flash::get_softmax_lse_size(parameters.sequence_length, parameters.batch_size, parameters.num_heads);
// split kv buffers
parameters.num_splits = onnxruntime::flash::num_splits_heuristic(
parameters.batch_size, parameters.sequence_length, parameters.kv_sequence_length, parameters.num_heads,
parameters.head_size, device_prop.multiProcessorCount, 128, false,
device_prop.major == 8 && device_prop.minor > 0);
if (parameters.num_splits > 1) {
// softmax_lse_accum buffer
softmax_lse_accum_bytes = onnxruntime::flash::get_softmax_lse_accum_size(
parameters.num_splits, parameters.batch_size, parameters.num_heads, parameters.sequence_length);
// out_accum buffer
auto round_multiple = [](int x, int m) { return (x + m - 1) / m * m; };
const int head_size_rounded = round_multiple(parameters.head_size, 32);
out_accum_bytes = onnxruntime::flash::get_out_accum_size(
parameters.num_splits, parameters.batch_size, parameters.num_heads, parameters.sequence_length, head_size_rounded);
}
// seqlens_k buffer
if (past_key != nullptr) {
seqlens_k_bytes = sizeof(int) * parameters.batch_size;
}
}
auto softmax_lse_buffer = GetScratchBuffer<void>(softmax_lse_bytes, context->GetComputeStream());
auto softmax_lse_accum_buffer = GetScratchBuffer<void>(softmax_lse_accum_bytes, context->GetComputeStream());
auto out_accum_buffer = GetScratchBuffer<void>(out_accum_bytes, context->GetComputeStream());
auto seqlens_k_buffer = GetScratchBuffer<void>(seqlens_k_bytes, context->GetComputeStream());
#else
constexpr bool use_flash_attention = false;
auto softmax_lse_buffer = GetScratchBuffer<void>(0, context->GetComputeStream()); // nullptr
auto softmax_lse_accum_buffer = GetScratchBuffer<void>(0, context->GetComputeStream()); // nullptr
auto out_accum_buffer = GetScratchBuffer<void>(0, context->GetComputeStream()); // nullptr
auto seqlens_k_buffer = GetScratchBuffer<void>(0, context->GetComputeStream()); // nullptr
#endif
// only kernel implemented for gqa right now
ORT_ENFORCE(use_flash_attention);
data.query = reinterpret_cast<const CudaT*>(query->Data<T>());
data.key = reinterpret_cast<const CudaT*>(key->Data<T>());
data.value = reinterpret_cast<const CudaT*>(value->Data<T>());
data.past_key = (nullptr == past_key) ? nullptr : reinterpret_cast<const CudaT*>(past_key->Data<T>());
data.past_value = (nullptr == past_value) ? nullptr : reinterpret_cast<const CudaT*>(past_value->Data<T>());
data.output = reinterpret_cast<CudaT*>(output->MutableData<T>());
data.present_key = (nullptr == present_key) ? nullptr : reinterpret_cast<CudaT*>(present_key->MutableData<T>());
data.present_value = (nullptr == present_value) ? nullptr : reinterpret_cast<CudaT*>(present_value->MutableData<T>());
data.use_flash_attention = use_flash_attention;
if (softmax_lse_buffer != nullptr) {
data.softmax_lse = reinterpret_cast<CudaT*>(softmax_lse_buffer.get());
}
if (softmax_lse_accum_buffer != nullptr) {
data.softmax_lse_accum = reinterpret_cast<CudaT*>(softmax_lse_accum_buffer.get());
}
if (out_accum_buffer != nullptr) {
data.out_accum = reinterpret_cast<CudaT*>(out_accum_buffer.get());
}
if (seqlens_k_buffer != nullptr) {
data.seqlens_k = reinterpret_cast<int*>(seqlens_k_buffer.get());
}
cublasHandle_t cublas = GetCublasHandle(context);
return QkvToContext<CudaT>(
device_prop, cublas, context->GetComputeStream(), parameters, data);
}
} // namespace cuda
} // namespace contrib
} // namespace onnxruntime

View file

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include <memory>
#include "core/providers/cuda/cuda_kernel.h"
#include "contrib_ops/cuda/bert/group_query_attention_impl.h"
namespace onnxruntime {
namespace contrib {
namespace cuda {
using namespace onnxruntime::cuda;
template <typename T>
class GroupQueryAttention final : public CudaKernel {
public:
GroupQueryAttention(const OpKernelInfo& info);
Status ComputeInternal(OpKernelContext* context) const override;
protected:
int num_heads_; // number of attention heads
int kv_num_heads_; // different for k and v for group query attention
int past_sequence_length_;
bool is_unidirectional_; // causal
bool is_past_bsnh_;
float scale_;
bool disable_flash_attention_;
};
} // namespace cuda
} // namespace contrib
} // namespace onnxruntime

View file

@ -0,0 +1,253 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "core/common/common.h"
#include "core/providers/common.h"
#include "contrib_ops/cpu/bert/attention_common.h"
namespace onnxruntime {
namespace contrib {
namespace group_query_attention_helper {
Status CheckInputs(const Tensor* query,
const Tensor* key,
const Tensor* value,
const Tensor* past_key,
const Tensor* past_value,
void* parameters,
int num_heads,
int kv_num_heads,
const Tensor* past_seq_len,
bool is_past_bsnh,
float scale) {
// Note: Here S* is max_sequence_length, S- is past_sequence_length, S+ is kv_sequence_length
// past_key : (B, S*, N_k, H) or (B, N_k, S*, H) or (B, S-, N_k, H) or (B, N_k, S-, H)
// past_value : (B, S*, N_k, H) or (B, N_k, S*, H) or (B, S-, N_k, H) or (B, N_k, S-, H)
// no packing for q/k/v:
// query (Q) : (B, S, D)
// key (K) : (B, S+, D_kv)
// value (V) : (B, S+, D_kv)
AttentionQkvFormat qkv_format = Q_K_V_BSNH;
AttentionQkvFormat past_kv_format = Q_K_V_BSNH;
const auto& query_dims = query->Shape().GetDims();
const auto& key_dims = key->Shape().GetDims();
const auto& value_dims = value->Shape().GetDims();
if (query_dims.size() != 3) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Input 'query' is expected to have 3 dimensions, got ",
query_dims.size());
}
int batch_size = static_cast<int>(query_dims[0]);
int sequence_length = static_cast<int>(query_dims[1]);
int q_hidden_size = static_cast<int>(query_dims[2]);
int head_size = static_cast<int>(q_hidden_size) / num_heads;
int kv_sequence_length = sequence_length;
int kv_hidden_size = (key_dims.size() == 3)
? static_cast<int>(key_dims[2])
: (kv_num_heads * static_cast<int>(key_dims[3]));
int max_sequence_length = 0;
if (past_key != nullptr && past_value != nullptr) {
const auto& past_key_dims = past_key->Shape().GetDims();
const auto& past_value_dims = past_value->Shape().GetDims();
if (past_key_dims.size() != 4) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_key' is expected to have 4 dimensions, got ",
past_key_dims.size());
}
if (past_value_dims.size() != 4) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_value' is expected to have 4 dimensions, got ",
past_value_dims.size());
}
if (past_key_dims[0] != batch_size) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_key' dimension 0 should be batch_size, got ",
past_key_dims[0]);
}
if (past_value_dims[0] != batch_size) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_value' dimension 0 should be batch_size, got ",
past_value_dims[0]);
}
// BNSH
if (!is_past_bsnh) {
past_kv_format = Q_K_V_BNSH;
if (past_key_dims[2] != past_value_dims[2]) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"BNSH Input 'past_key' and 'past_value' should have same dimension 2 (max sequence"
"length or past sequence length), got ",
past_key_dims[1]);
}
if (past_key_dims[1] != kv_num_heads) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_key' shall have kv_num_heads");
}
if (past_value_dims[1] != kv_num_heads) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_value' shall have kv_num_heads");
}
// We assume all sequence in past kv are left-padded to max or past sequence length
max_sequence_length = static_cast<int>(past_key_dims[2]);
// BSNH
} else {
past_kv_format = Q_K_V_BSNH;
if (past_key_dims[1] != past_value_dims[1]) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"BNSH Input 'past_key' and 'past_value' should have same dimension 1 (max sequence"
"length or past sequence length), got ",
past_key_dims[1]);
}
if (past_key_dims[2] != kv_num_heads) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_key' shall have kv_num_heads");
}
if (past_value_dims[2] != kv_num_heads) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_value' shall have kv_num_heads");
}
// We assume all sequence in past kv are left-padded to max or past sequence length
max_sequence_length = static_cast<int>(past_key_dims[1]);
}
if (past_key_dims[3] != head_size) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_key' dimension 3 should be same as head_size, got ",
past_key_dims[3]);
}
if (past_value_dims[3] != head_size) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_value' dimension 3 should be same as head_size, got ",
past_value_dims[3]);
}
} else if (past_key != nullptr || past_value != nullptr) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'past_key' and 'past_value' shall be both present or both absent");
}
if (key != nullptr) {
const auto& key_dims = key->Shape().GetDims();
if (key_dims.size() != 3) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Input 'key' is expected to have 3 dimensions, got ",
key_dims.size());
}
if (query_dims[0] != key_dims[0]) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'query' and 'key' shall have same dim 0 (batch size)");
}
if (num_heads % kv_num_heads != 0) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"num_heads must be a multiple of kv_num_heads. Got num_heads % kv_num_heads == ",
num_heads % kv_num_heads);
}
if (key_dims[2] != value_dims[2]) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'key' and 'value' shall have same dim 2 (kv_hidden_size)");
}
qkv_format = Q_K_V_BSNH;
kv_sequence_length = static_cast<int>(key_dims[1]);
} else {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Missing key tensor.");
}
if (value != nullptr) {
const auto& value_dims = value->Shape().GetDims();
if (value_dims.size() != 3) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Input 'value' is expected to have 3 dimensions, got ",
value_dims.size());
}
if (query_dims[0] != value_dims[0]) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'query' and 'value' shall have same dim 0 (batch_size)");
}
if (static_cast<int64_t>(kv_sequence_length) != value_dims[1]) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Input 'key' and 'value' shall have the same dim 1 (kv_sequence_length)");
}
if (value_dims[2] != kv_hidden_size) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Input 'value' is expected to have same hidden size as key.");
}
} else {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"Missing value tensor.");
}
// When kv-cache, we take past_seq_len as an argument... otherwise we use sequence length of past kv directly.
int32_t past_sequence_length = 0;
int present_sequence_length = 0;
if (past_seq_len != nullptr) {
if (!onnxruntime::IsScalarOr1ElementVector(past_seq_len)) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"past_sequence_length tensor must be of one element when using past kv.");
}
if (past_seq_len->GetElementType() == ONNX_NAMESPACE::TensorProto_DataType_INT32) {
past_sequence_length = *((*past_seq_len).template Data<int32_t>());
} else {
past_sequence_length = static_cast<int32_t>(*((*past_seq_len).template Data<int64_t>()));
}
present_sequence_length = max_sequence_length;
} else if (past_key != nullptr) {
past_sequence_length = max_sequence_length; // this is the length of past_key tensor
present_sequence_length = past_sequence_length + kv_sequence_length;
}
if (parameters != nullptr) {
GroupQueryAttentionParameters* output_parameters = reinterpret_cast<GroupQueryAttentionParameters*>(parameters);
output_parameters->batch_size = batch_size;
output_parameters->sequence_length = sequence_length;
output_parameters->past_sequence_length = past_sequence_length;
output_parameters->kv_sequence_length = kv_sequence_length;
output_parameters->present_sequence_length = present_sequence_length;
output_parameters->max_sequence_length = max_sequence_length;
output_parameters->hidden_size = q_hidden_size;
output_parameters->num_heads = num_heads;
output_parameters->head_size = q_hidden_size / num_heads;
output_parameters->kv_hidden_size = kv_hidden_size;
output_parameters->kv_num_heads = kv_num_heads;
output_parameters->is_unidirectional = true;
output_parameters->scale = scale;
output_parameters->qkv_format = qkv_format;
output_parameters->past_kv_format = past_kv_format;
}
return Status::OK();
}
template <typename T>
Status CheckInputs(const T* query,
const T* key,
const T* value,
const T* past_key,
const T* past_value,
void* parameters,
int num_heads,
int kv_num_heads,
const T* past_seq_len,
bool is_past_bsnh,
float scale,
int max_threads_per_block) {
if (max_threads_per_block > 0 && num_heads > max_threads_per_block) {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "num_heads should be no larger than ", max_threads_per_block);
}
return CheckInputs(query, key, value, past_key, past_value, parameters, num_heads, kv_num_heads, past_seq_len, is_past_bsnh, scale);
}
} // namespace group_query_attention_helper
} // namespace contrib
} // namespace onnxruntime

View file

@ -0,0 +1,279 @@
/*
The implementation of this file is based on our Multi-Head Attention impl.cu file,
which is based on qkvToContext plugin in TensorRT demo:
https://github.com/NVIDIA/TensorRT/tree/release/5.1/demo/BERT/
Copyright 2019 NVIDIA Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Modifications:
// (1) support GPT-2 past state, unidirectional mask (causal)
// (2) use flash attention kernel from (https://github.com/Dao-AILab/flash-attention)
// (3) support different number of heads for Q and KV
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include <cassert>
#include <cuda_fp16.h>
#include <cub/cub.cuh>
#include "core/providers/cuda/cu_inc/common.cuh"
#include "core/providers/cuda/cuda_common.h"
#include "core/providers/cuda/shared_inc/fpgeneric.h"
#include "contrib_ops/cuda/bert/attention_softmax.h"
#include "contrib_ops/cuda/bert/transformer_common.h"
#include "contrib_ops/cuda/bert/add_bias_transpose.h"
#include "contrib_ops/cpu/bert/attention_base.h"
#include "contrib_ops/cuda/bert/bert_padding.h"
#include "contrib_ops/cuda/transformers/dump_cuda_tensor.h"
#include "contrib_ops/cuda/bert/flash_attention/flash_api.h"
#include "contrib_ops/cuda/bert/group_query_attention_impl.h"
#include "contrib_ops/cuda/bert/attention_impl.h"
using namespace onnxruntime::cuda;
namespace onnxruntime {
namespace contrib {
namespace cuda {
// Kernel for seqlens_k
__global__ void repeat_seqlen(int32_t* seqlens_k, int32_t seqlen, int batch_size) {
int id = blockDim.x * blockIdx.x + threadIdx.x;
if (id < batch_size) seqlens_k[id] = seqlen;
}
// Kernel to append new and past kv in either BSNH or BNSH format
// Adapted from ConcatTensorToTensor kernel in attention_kv_cache.cu file
template <typename T>
__global__ void ConcatNewToPastKV(const int new_seqlen,
const T* past_kv,
const T* new_kv,
T* present_kv,
const bool is_bsnh) { // refers to past; otherwise bnsh
const int h = threadIdx.x;
const int n = threadIdx.y;
const int s = blockIdx.x;
const int b = blockIdx.y;
const int present_seqlen = gridDim.x;
const int num_heads = blockDim.y;
const int H = blockDim.x;
const int present_batch_stride = present_seqlen * num_heads * H;
const int row_stride = is_bsnh ? num_heads * H : H;
const int present_head_stride = is_bsnh ? H : present_seqlen * H;
// past_kv: BPNH or BNPH
// new_kv: BLNH or BNLH
// present_kv: BTNH or BNTH, where T = P + L
const int past_seqlen = present_seqlen - new_seqlen;
int out_offset = b * present_batch_stride + s * row_stride + n * present_head_stride + h;
if (s < past_seqlen) {
const int past_batch_stride = past_seqlen * num_heads * H;
const int past_head_stride = is_bsnh ? H : past_seqlen * H;
const int in_offset = b * past_batch_stride + s * row_stride + n * past_head_stride + h;
present_kv[out_offset] = past_kv[in_offset];
} else if (s < present_seqlen) {
// Note: new KV always BSNH
const int new_batch_stride = new_seqlen * num_heads * H;
const int new_row_stride = num_heads * H;
const int new_head_stride = H;
const int in_offset = b * new_batch_stride + (s - past_seqlen) * new_row_stride + n * new_head_stride + h;
present_kv[out_offset] = new_kv[in_offset];
}
}
template <typename T>
__global__ void ConcatNewToPastKVLarge(const int new_seqlen,
const int H,
const T* past_kv,
const T* new_kv,
T* present_kv,
const bool is_bsnh) {
// Use when (H*)*num_heads > 1024
int h = threadIdx.x;
const int n = threadIdx.y;
const int s = blockIdx.x;
const int b = blockIdx.y;
const int present_seqlen = gridDim.x;
const int num_heads = blockDim.y;
const int thread_stride = blockDim.x;
const int present_batch_stride = present_seqlen * num_heads * H;
const int row_stride = is_bsnh ? num_heads * H : H;
const int present_head_stride = is_bsnh ? H : present_seqlen * H;
// past_kv: BPNH or BNPH
// new_kv: BLNH or BNLH
// present_kv: BTNH or BNTH, where T = P + L
const int past_seqlen = present_seqlen - new_seqlen;
while (h < H) {
int out_offset = b * present_batch_stride + s * row_stride + n * present_head_stride + h;
if (s < past_seqlen) {
const int past_batch_stride = past_seqlen * num_heads * H;
const int past_head_stride = is_bsnh ? H : past_seqlen * H;
const int in_offset = b * past_batch_stride + s * row_stride + n * past_head_stride + h;
present_kv[out_offset] = past_kv[in_offset];
} else if (s < present_seqlen) {
const int new_batch_stride = new_seqlen * num_heads * H;
const int new_row_stride = num_heads * H;
const int new_head_stride = H;
const int in_offset = b * new_batch_stride + (s - past_seqlen) * new_row_stride + n * new_head_stride + h;
present_kv[out_offset] = new_kv[in_offset];
}
h += thread_stride;
}
}
template <typename T>
Status QkvToContext(
const cudaDeviceProp& device_prop,
cublasHandle_t& cublas,
Stream* ort_stream,
contrib::GroupQueryAttentionParameters& parameters,
GroupQueryAttentionData<T>& data) {
assert(data.use_flash_attention);
#if USE_FLASH_ATTENTION
auto stream = static_cast<cudaStream_t>(ort_stream->GetHandle());
const int max_threads_per_block = device_prop.maxThreadsPerBlock;
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 present_sequence_length = parameters.present_sequence_length;
const int num_heads = parameters.num_heads;
const int kv_num_heads = parameters.kv_num_heads;
const int head_size = parameters.head_size;
AttentionQkvFormat past_kv_format = parameters.past_kv_format;
const float scale = parameters.scale == 0.0f ? 1.f / sqrt(static_cast<float>(head_size)) : parameters.scale;
if (data.use_flash_attention) {
assert(parameters.qkv_format == AttentionQkvFormat::Q_K_V_BSNH);
assert(parameters.num_heads % parameters.kv_num_heads == 0);
void* query = reinterpret_cast<void*>(const_cast<T*>(data.query));
void* key = reinterpret_cast<void*>(const_cast<T*>(data.key));
void* value = reinterpret_cast<void*>(const_cast<T*>(data.value));
bool is_causal = parameters.is_unidirectional;
if (data.past_key == nullptr && data.present_key == nullptr) {
ORT_RETURN_IF_ERROR(onnxruntime::flash::mha_fwd(
device_prop, stream, query, key, value, data.output, reinterpret_cast<void*>(data.softmax_lse),
parameters.batch_size, parameters.num_heads, parameters.kv_num_heads, head_size,
parameters.sequence_length, parameters.kv_sequence_length, scale, is_causal, parameters.num_splits,
reinterpret_cast<void*>(data.softmax_lse_accum), reinterpret_cast<void*>(data.out_accum)));
} else if (data.past_key == data.present_key) {
// Assume past and present kv share buffer.
assert(past_kv_format == AttentionQkvFormat::Q_K_V_BSNH || past_kv_format == AttentionQkvFormat::Q_K_V_BNSH);
assert(parameters.past_sequence_length >= 0);
assert(data.past_value != nullptr);
void* present_key = reinterpret_cast<void*>(const_cast<T*>(data.present_key));
void* present_value = reinterpret_cast<void*>(const_cast<T*>(data.present_value));
// Launch kernel to copy seqlen
int thr_per_blk = 256;
int blk_in_grid = ceil(float(batch_size) / thr_per_blk);
repeat_seqlen<<<blk_in_grid, thr_per_blk, 0, stream>>>(data.seqlens_k, parameters.past_sequence_length, batch_size);
DUMP_TENSOR_INIT();
DUMP_TENSOR("seqlens_k", data.seqlens_k, 1, batch_size);
bool past_bsnh = past_kv_format == AttentionQkvFormat::Q_K_V_BSNH;
ORT_RETURN_IF_ERROR(onnxruntime::flash::mha_fwd_kvcache(
device_prop, stream, query, present_key, present_value, key, value, data.output, reinterpret_cast<void*>(data.softmax_lse),
reinterpret_cast<void*>(data.seqlens_k), batch_size, num_heads, kv_num_heads,
head_size, sequence_length, present_sequence_length, kv_sequence_length,
scale, is_causal, past_bsnh, parameters.num_splits, reinterpret_cast<void*>(data.softmax_lse_accum),
reinterpret_cast<void*>(data.out_accum)));
} else if (data.present_key != nullptr && (data.past_key != nullptr || kv_sequence_length == present_sequence_length)) {
assert(past_kv_format == AttentionQkvFormat::Q_K_V_BSNH || past_kv_format == AttentionQkvFormat::Q_K_V_BNSH);
// Note that Flash Attention kv-caching operates in place on a buffer... therefore this path is inneficient
if (head_size % 4 != 0) {
return ORT_MAKE_STATUS(ONNXRUNTIME, NOT_IMPLEMENTED, "requires head_size be divisible by 4");
}
const int H = head_size / 4;
if (H * kv_num_heads <= max_threads_per_block) {
const dim3 grid(present_sequence_length, batch_size, 1);
const dim3 block(H, kv_num_heads, 1);
ConcatNewToPastKV<float2><<<grid, block, 0, stream>>>(kv_sequence_length,
reinterpret_cast<const float2*>(data.past_key),
reinterpret_cast<const float2*>(data.key),
reinterpret_cast<float2*>(data.present_key),
past_kv_format == AttentionQkvFormat::Q_K_V_BSNH);
ConcatNewToPastKV<float2><<<grid, block, 0, stream>>>(kv_sequence_length,
reinterpret_cast<const float2*>(data.past_value),
reinterpret_cast<const float2*>(data.value),
reinterpret_cast<float2*>(data.present_value),
past_kv_format == AttentionQkvFormat::Q_K_V_BSNH);
} else {
const dim3 grid(present_sequence_length, batch_size, 1);
const dim3 block(max_threads_per_block / kv_num_heads, kv_num_heads, 1);
ConcatNewToPastKVLarge<float2><<<grid, block, 0, stream>>>(kv_sequence_length,
H,
reinterpret_cast<const float2*>(data.past_key),
reinterpret_cast<const float2*>(data.key),
reinterpret_cast<float2*>(data.present_key),
past_kv_format == AttentionQkvFormat::Q_K_V_BSNH);
ConcatNewToPastKVLarge<float2><<<grid, block, 0, stream>>>(kv_sequence_length,
H,
reinterpret_cast<const float2*>(data.past_value),
reinterpret_cast<const float2*>(data.value),
reinterpret_cast<float2*>(data.present_value),
past_kv_format == AttentionQkvFormat::Q_K_V_BSNH);
}
void* present_key = reinterpret_cast<void*>(const_cast<T*>(data.present_key));
void* present_value = reinterpret_cast<void*>(const_cast<T*>(data.present_value));
// Launch kernel to copy seqlen
int thr_per_blk = 256;
int blk_in_grid = ceil(float(batch_size) / thr_per_blk);
repeat_seqlen<<<blk_in_grid, thr_per_blk, 0, stream>>>(data.seqlens_k, parameters.past_sequence_length, batch_size);
bool past_bsnh = past_kv_format == AttentionQkvFormat::Q_K_V_BSNH;
ORT_RETURN_IF_ERROR(onnxruntime::flash::mha_fwd(
device_prop, stream, query, present_key, present_value, data.output, reinterpret_cast<void*>(data.softmax_lse),
batch_size, num_heads, kv_num_heads, head_size,
sequence_length, present_sequence_length, scale, is_causal, parameters.num_splits,
reinterpret_cast<void*>(data.softmax_lse_accum), reinterpret_cast<void*>(data.out_accum), past_bsnh));
}
DUMP_TENSOR_INIT();
DUMP_TENSOR("flash attention output", data.output, batch_size, sequence_length, num_heads, head_size);
return Status::OK();
}
#endif
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Unfused Group Query Attention not implemented yet.");
}
template struct GroupQueryAttentionData<half>;
template Status QkvToContext<half>(
const cudaDeviceProp& device_prop,
cublasHandle_t& cublas,
Stream* ort_stream,
contrib::GroupQueryAttentionParameters& parameters,
GroupQueryAttentionData<half>& data);
} // namespace cuda
} // namespace contrib
} // namespace onnxruntime

View file

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "core/providers/cuda/shared_inc/cuda_utils.h"
#include <cuda_fp16.h>
#include <cublas_v2.h>
#include "contrib_ops/cpu/bert/attention_common.h"
#include "core/framework/allocator.h"
namespace onnxruntime {
namespace contrib {
namespace cuda {
template <typename T>
struct GroupQueryAttentionData {
const T* query = nullptr;
const T* key = nullptr;
const T* value = nullptr;
const T* past_key = nullptr;
const T* past_value = nullptr;
T* softmax_lse = nullptr;
T* softmax_lse_accum = nullptr;
T* out_accum = nullptr;
int* seqlens_k = nullptr;
T* output = nullptr;
T* present_key = nullptr;
T* present_value = nullptr;
bool use_flash_attention = false;
};
template <typename T>
Status QkvToContext(
const cudaDeviceProp& device_prop,
cublasHandle_t& cublas,
Stream* stream,
contrib::GroupQueryAttentionParameters& parameters,
GroupQueryAttentionData<T>& data);
} // namespace cuda
} // namespace contrib
} // namespace onnxruntime

View file

@ -924,55 +924,55 @@ Status LongformerQkvToContext(
if (disable_compact_memory) {
ORT_RETURN_IF_ERROR(LaunchLongformerSoftmaxSimpleKernel(
stream,
cublas,
workspace,
q,
k,
v,
attention_mask,
global_q,
global_k,
global_v,
global_attention,
global_index,
batch_global_num,
pinned_buffer,
temp_output,
rsqrt_head_size,
batch_size,
sequence_length,
num_heads,
head_size,
window,
element_size));
stream,
cublas,
workspace,
q,
k,
v,
attention_mask,
global_q,
global_k,
global_v,
global_attention,
global_index,
batch_global_num,
pinned_buffer,
temp_output,
rsqrt_head_size,
batch_size,
sequence_length,
num_heads,
head_size,
window,
element_size));
} else {
ORT_ENFORCE(max_num_global <= window);
ORT_RETURN_IF_ERROR(LaunchLongformerSoftmaxKernel(
stream,
cublas,
workspace,
q,
k,
v,
attention_mask,
max_num_global,
compact_global_q,
global_q,
global_k,
global_v,
global_attention,
global_index,
batch_global_num,
pinned_buffer,
temp_output,
rsqrt_head_size,
batch_size,
sequence_length,
num_heads,
head_size,
window,
element_size));
stream,
cublas,
workspace,
q,
k,
v,
attention_mask,
max_num_global,
compact_global_q,
global_q,
global_k,
global_v,
global_attention,
global_index,
batch_global_num,
pinned_buffer,
temp_output,
rsqrt_head_size,
batch_size,
sequence_length,
num_heads,
head_size,
window,
element_size));
}
// The temp_output is BxNxSxH, transpose it to final output BxSxNxH

View file

@ -90,7 +90,6 @@ __global__ void SkipLayerNormKernel(
// reduce x and x^2
cub::KeyValuePair<T, T> thread_data(0, 0);
for (int i = threadIdx.x; i < ld; i += TPB) {
const int idx = offset + i;
@ -130,10 +129,10 @@ __global__ void SkipLayerNormKernelSmall(
*input_val = *reinterpret_cast<const VecT*>(&input[idx]);
VecT* skip_val = reinterpret_cast<VecT*>(&skip_v);
if (skip_broadcasted){
*skip_val = *reinterpret_cast<const VecT*>(&skip[idx % skip_size]);
}else{
*skip_val = *reinterpret_cast<const VecT*>(&skip[idx]);
if (skip_broadcasted) {
*skip_val = *reinterpret_cast<const VecT*>(&skip[idx % skip_size]);
} else {
*skip_val = *reinterpret_cast<const VecT*>(&skip[idx]);
}
if (hasBias) {

View file

@ -71,6 +71,7 @@ class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kOnnxDomain,
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kOnnxDomain, 1, MLFloat16, Crop);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, float, MultiHeadAttention);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, MLFloat16, MultiHeadAttention);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, MLFloat16, GroupQueryAttention);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, float, DecoderAttention);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, MLFloat16, DecoderAttention);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kOnnxDomain, 1, int32_t, DynamicSlice);
@ -221,6 +222,7 @@ Status RegisterCudaContribKernels(KernelRegistry& kernel_registry) {
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kOnnxDomain, 1, MLFloat16, Crop)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, float, MultiHeadAttention)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, MLFloat16, MultiHeadAttention)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, MLFloat16, GroupQueryAttention)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, float, DecoderAttention)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kMSDomain, 1, MLFloat16, DecoderAttention)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCudaExecutionProvider, kOnnxDomain, 1, int32_t, DynamicSlice)>,

View file

@ -233,6 +233,59 @@ void MultiHeadAttentionTypeAndShapeInference(ONNX_NAMESPACE::InferenceContext& c
}
}
void GroupQueryAttentionTypeAndShapeInference(ONNX_NAMESPACE::InferenceContext& ctx, int past_key_index) {
// Output 0 has shape (batch_size, sequence_length, hidden_size)
// Q, K and V:
// Input 0 (query) has shape (batch_size, sequence_length, hidden_size)
// Input 1 (key) has shape (batch_size, kv_sequence_length, kv_hidden_size)
// Input 2 (value) has shape (batch_size, kv_sequence_length, kv_hidden_size)
// Type inference
ONNX_NAMESPACE::propagateElemTypeFromInputToOutput(ctx, 0, 0);
// Shape inference
if (hasInputShape(ctx, 0)) {
auto& query_shape = getInputShape(ctx, 0);
auto& query_dims = query_shape.dim();
if (query_dims.size() != 3) {
fail_shape_inference("Inputs 0 (query) shall be 3 dimensions");
}
if (hasInputShape(ctx, 2)) {
auto& value_shape = getInputShape(ctx, 2);
auto& value_dims = value_shape.dim();
if (value_dims.size() != 3) {
fail_shape_inference("Inputs 2 (value) shall be 3 dimensions");
}
ONNX_NAMESPACE::TensorShapeProto output_shape;
*output_shape.add_dim() = query_dims[0];
*output_shape.add_dim() = query_dims[1];
*output_shape.add_dim() = query_dims[2];
updateOutputShape(ctx, 0, output_shape);
return;
} else {
fail_shape_inference("Missing input 2 (value)");
}
}
if (ctx.getNumOutputs() > 1) { // has present output
if (hasInputShape(ctx, past_key_index)) {
auto& past_shape = getInputShape(ctx, past_key_index);
auto& past_dims = past_shape.dim();
if (past_dims.size() != 4) {
fail_shape_inference("The past_key input shall be 4 dimensions");
}
ONNX_NAMESPACE::propagateElemTypeFromInputToOutput(ctx, past_key_index, 1);
ONNX_NAMESPACE::propagateElemTypeFromInputToOutput(ctx, static_cast<size_t>(past_key_index) + 1, 2);
ONNX_NAMESPACE::propagateShapeFromInputToOutput(ctx, past_key_index, 1);
ONNX_NAMESPACE::propagateShapeFromInputToOutput(ctx, static_cast<size_t>(past_key_index) + 1, 2);
}
}
}
constexpr const char* Attention_ver1_doc = R"DOC(
Multi-Head Attention that can be either unidirectional (like GPT-2) or bidirectional (like BERT).
@ -823,7 +876,7 @@ ONNX_MS_OPERATOR_SET_SCHEMA(
"T")
.Output(1,
"present_key",
"past state for key with shape (batch_size, num_heads, total_sequence_length, head_size). "
"present state for key with shape (batch_size, num_heads, total_sequence_length, head_size). "
"If past_present_share_buffer is set, "
"its shape is (batch_size, num_heads, max_sequence_length, head_size), "
"while effective_seq_length = (past_sequence_length + kv_sequence_length).",
@ -831,7 +884,7 @@ ONNX_MS_OPERATOR_SET_SCHEMA(
OpSchema::Optional)
.Output(2,
"present_value",
"past state for value with shape (batch_size, num_heads, total_sequence_length, head_size). "
"present state for value with shape (batch_size, num_heads, total_sequence_length, head_size). "
"If past_present_share_buffer is set, "
"its shape is (batch_size, num_heads, max_sequence_length, head_size), "
"while effective_seq_length = (past_sequence_length + kv_sequence_length).",
@ -930,6 +983,84 @@ ONNX_MS_OPERATOR_SET_SCHEMA(
MultiHeadAttentionTypeAndShapeInference(ctx, 6);
}));
constexpr const char* GroupQueryAttention_ver1_doc = R"DOC(
Group Query Self/Cross Attention.
Supports different number of heads for q and kv.
)DOC";
ONNX_MS_OPERATOR_SET_SCHEMA(
GroupQueryAttention, 1,
OpSchema()
.SetDoc(GroupQueryAttention_ver1_doc)
.Attr("num_heads", "Number of attention heads for q", AttributeProto::INT)
.Attr("kv_num_heads", "Number of attention heads for k and v", AttributeProto::INT)
.Attr("unidirectional",
"Whether every token can only attend to previous tokens. Default value is 1.",
AttributeProto::INT,
static_cast<int64_t>(1))
.Attr("is_past_bsnh",
"Whether past kv uses BSNH, otherwise BNSH. Default value is 1 (BSNH).",
AttributeProto::INT,
static_cast<int64_t>(1))
.Attr("scale",
"Custom scale will be used if specified. Default value is 1/sqrt(head_size)",
AttributeProto::FLOAT,
OPTIONAL_VALUE)
.Input(0,
"query",
"Query with shape (batch_size, sequence_length, hidden_size)",
"T")
.Input(1,
"key",
"Key with shape (batch_size, kv_sequence_length, kv_hidden_size) ",
"T")
.Input(2,
"value",
"Value with shape (batch_size, kv_sequence_length, kv_hidden_size)",
"T")
.Input(3,
"past_key",
"past state key with support for format BSNH or BNSH. When past_key uses same tensor as present_key"
"(k-v cache), it is of length max_sequence_length... otherwise of length past_sequence_length.",
"T",
OpSchema::Optional)
.Input(4,
"past_value",
"past state value with support for format BSNH or BNSH. When past_value uses same tensor as present_value"
"(k-v cache), it is of length max_sequence_length... otherwise of length past_sequence_length.",
"T",
OpSchema::Optional)
.Input(5,
"past_sequence_length",
"When buffered past_key and past_value is used (present_key uses same tensor as past_key), required"
"to specify past_sequence_length (could be 0). Otherwise, past_sequence_length inferred from past_key.",
"M",
OpSchema::Optional)
.Output(0,
"output",
"3D output tensor with shape (batch_size, sequence_length, hidden_size)",
"T")
.Output(1,
"present_key",
"present state key with support for format BSNH or BNSH. When past_key uses same tensor as present_key"
"(k-v buffer), it is of length max_sequence_length... otherwise of length past_sequence_length +"
"kv_sequence_length.",
"T",
OpSchema::Optional)
.Output(2,
"present_value",
"present state value with support for format BSNH or BNSH. When past_value uses same tensor as present_value"
"(k-v buffer), it is of length max_sequence_length... otherwise of length past_sequence_length +"
"kv_sequence_length.",
"T",
OpSchema::Optional)
.TypeConstraint("T", {"tensor(float16)"}, "Constrain input and output to float tensors.")
.TypeConstraint("M", {"tensor(int32)", "tensor(int64)"}, "Constrain past sequence length to int tensor.")
.TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) {
GroupQueryAttentionTypeAndShapeInference(ctx, 3);
}));
constexpr const char* Longformer_Attention_doc = R"DOC(
Longformer Self Attention with a local context and a global context. Tokens attend locally: Each token
attends to its W previous tokens and W succeeding tokens with W being the window length. A selected few tokens

View file

@ -83,6 +83,7 @@ class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MatMulFpQ4);
#endif
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MaxpoolWithMask);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MultiHeadAttention);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, GroupQueryAttention);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MurmurHash3);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, NGramRepeatBlock);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, Pad);
@ -182,6 +183,7 @@ class OpSet_Microsoft_ver1 {
#endif
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MaxpoolWithMask)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MultiHeadAttention)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, GroupQueryAttention)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MurmurHash3)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, NGramRepeatBlock)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, Pad)>());

View file

@ -0,0 +1,339 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
# --------------------------------------------------------------------------
"""
Benchmark performance of MultiHeadAttention with Nvidia GPU of Compute Capability 8.0, 8.6 or 8.9 in Linux:
sh benchmark_mha.sh
"""
import math
import random
import statistics
import time
import torch
from onnx import TensorProto, helper
from onnxruntime import InferenceSession, OrtValue, SessionOptions
class InputFormats:
QKV_BSNH = 0
QKV_BNSH = 1
class Config:
batch_size = 0
sequence_length = 0
kv_sequence_length = 0
past_sequence_length = 0
num_heads = 0
kv_num_heads = 0
head_size = 0
def __init__(self, b, s, s2, sp, n, n2, h):
self.batch_size = b
self.sequence_length = s
self.kv_sequence_length = s2
self.past_sequence_length = sp
self.num_heads = n
self.kv_num_heads = n2
self.head_size = h
def create_group_query_attention_graph_past(
config, causal=False, past_kv_format=InputFormats.QKV_BSNH, share_buffer=True
):
past_kv_seqlen = config.kv_sequence_length if share_buffer else config.past_sequence_length
present_kv_seqlen = (
config.kv_sequence_length if share_buffer else config.past_sequence_length + config.sequence_length
)
nodes = [
helper.make_node(
"GroupQueryAttention",
[
"query",
"key",
"value",
"past_key",
"past_value",
"past_sequence_length" if share_buffer else "",
],
["output", "present_key", "present_value"],
"GroupQueryAttention_0",
num_heads=config.num_heads,
kv_num_heads=config.kv_num_heads,
unidirectional=1 if causal else 0,
is_past_bsnh=1 if past_kv_format == InputFormats.QKV_BSNH else 0,
domain="com.microsoft",
),
]
graph_input = [
helper.make_tensor_value_info(
"query",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"key",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.kv_num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"value",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.kv_num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"past_key",
TensorProto.FLOAT16,
[
config.batch_size,
past_kv_seqlen if past_kv_format == InputFormats.QKV_BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == InputFormats.QKV_BSNH else past_kv_seqlen,
config.head_size,
],
),
helper.make_tensor_value_info(
"past_value",
TensorProto.FLOAT16,
[
config.batch_size,
past_kv_seqlen if past_kv_format == InputFormats.QKV_BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == InputFormats.QKV_BSNH else past_kv_seqlen,
config.head_size,
],
),
]
if share_buffer:
graph_input += [
helper.make_tensor_value_info(
"past_sequence_length",
TensorProto.INT32,
[1],
)
]
graph_output = [
helper.make_tensor_value_info(
"output",
TensorProto.FLOAT16,
[config.batch_size, config.sequence_length, config.num_heads * config.head_size],
),
helper.make_tensor_value_info(
"present_key",
TensorProto.FLOAT16,
[
config.batch_size,
present_kv_seqlen if past_kv_format == InputFormats.QKV_BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == InputFormats.QKV_BSNH else present_kv_seqlen,
config.head_size,
],
),
helper.make_tensor_value_info(
"present_value",
TensorProto.FLOAT16,
[
config.batch_size,
present_kv_seqlen if past_kv_format == InputFormats.QKV_BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == InputFormats.QKV_BSNH else present_kv_seqlen,
config.head_size,
],
),
]
graph = helper.make_graph(
nodes,
"GroupQueryAttention_Graph",
graph_input,
graph_output,
)
model = helper.make_model(graph)
return model.SerializeToString()
def create_gqa_session(
config: Config,
causal: bool = False,
past_format=InputFormats.QKV_BSNH,
share_buffer: bool = True,
) -> InferenceSession:
onnx_model_str = create_group_query_attention_graph_past(config, causal, past_format, share_buffer)
sess_options = SessionOptions()
ort_session = InferenceSession(onnx_model_str, sess_options, providers=["CUDAExecutionProvider"])
return ort_session
def bind_io(io_binding, input_dict, device, share_buffer=True):
io_binding.bind_cpu_input("query", input_dict["query"])
io_binding.bind_cpu_input("key", input_dict["key"])
io_binding.bind_cpu_input("value", input_dict["value"])
io_binding.bind_input(
"past_key", "cuda", 0, "float16", input_dict["past_key"].shape(), input_dict["past_key"].data_ptr()
)
io_binding.bind_input(
"past_value",
"cuda",
0,
"float16",
input_dict["past_value"].shape(),
input_dict["past_value"].data_ptr(),
)
io_binding.bind_output("output")
if share_buffer:
io_binding.bind_cpu_input("past_sequence_length", input_dict["past_sequence_length"])
io_binding.bind_output(
"present_key",
device_type="cuda",
device_id=device,
element_type="float16",
shape=input_dict["past_key"].shape(),
buffer_ptr=input_dict["past_key"].data_ptr(),
)
io_binding.bind_output(
"present_value",
device_type="cuda",
device_id=device,
element_type="float16",
shape=input_dict["past_value"].shape(),
buffer_ptr=input_dict["past_value"].data_ptr(),
)
else:
io_binding.bind_output("present_key")
io_binding.bind_output("present_value")
def measure_latency(ort_session, io_binding):
start = time.time()
_ = ort_session.run_with_iobinding(io_binding)
end = time.time()
return end - start
def flops(batch, q_seqlen, kv_seqlen, head_size, num_heads):
return 4 * batch * q_seqlen * kv_seqlen * num_heads * head_size
def tflops_per_second(flop, time):
return (flop / time / 10**12) if not math.isnan(time) else 0.0
def benchmark_op(session, io_binding, repeats=100):
# warm up session
_ = measure_latency(session, io_binding)
latency_list = []
for _ in range(repeats):
latency = measure_latency(session, io_binding)
latency_list.append(latency)
return statistics.mean(latency_list)
def run_tflops_test(dtype=torch.float16, repeats: int = 100):
device_id = torch.cuda.current_device()
device = torch.device("cuda", device_id)
print("---- GQA BSNH vs GQA BNSH ----")
print("op\tbatch\ts_kv\theads\th_dim\tms\tTFLOPS")
mean_bsnh_lat = 0
mean_bnsh_lat = 0
num_trials = 0
share_buffer = True
random.seed(69)
for b in [1, 3, 8, 16]:
for s_q, s_kv in [(1, 128), (128, 256), (512, 512), (128, 1024), (1, 2048)]:
for n_q, n_kv in [(8, 8), (16, 8), (32, 32), (12, 3), (128, 64)]:
for h in [32, 64, 128]:
sp = random.randint(1, s_kv - 1) if s_kv - 1 > 0 else 0
config = Config(b, s_q, s_kv, sp, n_q, n_kv, h)
bsnh_session = create_gqa_session(
config,
causal=False,
past_format=InputFormats.QKV_BSNH,
share_buffer=share_buffer,
)
bnsh_session = create_gqa_session(
config,
causal=False,
past_format=InputFormats.QKV_BNSH,
share_buffer=share_buffer,
)
q = torch.randn(b, s_q, n_q * h, device=device, dtype=dtype)
kv = torch.randn(b, s_q, 2, n_kv * h, device=device, dtype=dtype)
k, v = kv.unbind(dim=2)
past_kv = torch.rand(b, s_kv if share_buffer else sp, 2, n_kv, h, device=device, dtype=dtype)
past_k, past_v = past_kv.unbind(dim=2)
input_dict_bsnh = {
"query": q.detach().cpu().numpy(),
"key": k.detach().cpu().numpy(),
"value": v.detach().cpu().numpy(),
"past_key": OrtValue.ortvalue_from_numpy(past_k.detach().cpu().numpy(), "cuda", device_id),
"past_value": OrtValue.ortvalue_from_numpy(past_v.detach().cpu().numpy(), "cuda", device_id),
}
input_dict_bnsh = {
"query": q.detach().cpu().numpy(),
"key": k.detach().cpu().numpy(),
"value": v.detach().cpu().numpy(),
"past_key": OrtValue.ortvalue_from_numpy(
past_k.transpose(1, 2).detach().cpu().numpy(), "cuda", 0
),
"past_value": OrtValue.ortvalue_from_numpy(
past_v.transpose(1, 2).detach().cpu().numpy(), "cuda", 0
),
}
if share_buffer:
input_dict_bsnh["past_sequence_length"] = (
torch.tensor([sp], device="cuda", dtype=torch.int32).detach().cpu().numpy()
)
input_dict_bnsh["past_sequence_length"] = (
torch.tensor([sp], device="cuda", dtype=torch.int32).detach().cpu().numpy()
)
io_binding_bsnh = bsnh_session.io_binding()
io_binding_bnsh = bnsh_session.io_binding()
bind_io(io_binding_bsnh, input_dict_bsnh, device_id, share_buffer)
bind_io(io_binding_bnsh, input_dict_bnsh, device_id, share_buffer)
average_gqa_bsnh_latency = benchmark_op(bsnh_session, io_binding_bsnh, repeats)
average_gqa_bnsh_latency = benchmark_op(bnsh_session, io_binding_bnsh, repeats)
del bsnh_session
del bnsh_session
# compute TFLOPS per second
bsnh_speed = tflops_per_second(flops(b, s_q, s_kv, h, n_q), average_gqa_bsnh_latency)
print(f"bsnh\t{b}\t{s_kv}\t{n_q}\t{h}\t{average_gqa_bsnh_latency * 1000:.2f}\t{bsnh_speed:.2f}")
bnsh_speed = tflops_per_second(flops(b, s_q, s_kv, h, n_q), average_gqa_bnsh_latency)
print(f"bnsh\t{b}\t{s_kv}\t{n_q}\t{h}\t{average_gqa_bnsh_latency * 1000:.2f}\t{bnsh_speed:.2f}")
print("---------")
if average_gqa_bsnh_latency > 10 * average_gqa_bnsh_latency:
continue
num_trials += 1
mean_bsnh_lat += average_gqa_bsnh_latency
mean_bnsh_lat += average_gqa_bnsh_latency
mean_bsnh_lat /= num_trials
mean_bnsh_lat /= num_trials
print(f"average bsnh latency:\t{mean_bsnh_lat}")
print(f"average bnsh latency:\t{mean_bnsh_lat}")
if __name__ == "__main__":
run_tflops_test()

View file

@ -10,6 +10,7 @@
# license information.
# -------------------------------------------------------------------------
import math
import random
import numpy
import torch
@ -17,23 +18,32 @@ from bert_padding import pad_input, unpad_input
from einops import rearrange, repeat
from onnx import TensorProto, helper
from onnxruntime import InferenceSession, SessionOptions
from onnxruntime import InferenceSession, OrtValue, SessionOptions
torch.manual_seed(0)
class Formats:
BSNH = 0
BNSH = 1
class Config:
batch_size = 0
sequence_length = 0
kv_sequence_length = 0
past_sequence_length = 0
num_heads = 0
kv_num_heads = 0
head_size = 0
def __init__(self, b, s, s2, n, h):
def __init__(self, b, s, s2, sp, n, n2, h):
self.batch_size = b
self.sequence_length = s
self.kv_sequence_length = s2
self.past_sequence_length = sp
self.num_heads = n
self.kv_num_heads = n2
self.head_size = h
@ -149,6 +159,196 @@ def create_multihead_attention_graph(config):
return model.SerializeToString()
def create_group_query_attention_graph_no_past(config, causal=False):
nodes = [
helper.make_node(
"GroupQueryAttention",
[
"query",
"key",
"value",
],
["output"],
"GroupQueryAttention_0",
num_heads=config.num_heads,
kv_num_heads=config.kv_num_heads,
unidirectional=1 if causal else 0,
domain="com.microsoft",
),
]
graph_input = [
helper.make_tensor_value_info(
"query",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"key",
TensorProto.FLOAT16,
[
config.batch_size,
config.kv_sequence_length,
config.kv_num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"value",
TensorProto.FLOAT16,
[
config.batch_size,
config.kv_sequence_length,
config.kv_num_heads * config.head_size,
],
),
]
graph_output = [
helper.make_tensor_value_info(
"output",
TensorProto.FLOAT16,
[config.batch_size, config.sequence_length, config.num_heads * config.head_size],
),
]
graph = helper.make_graph(
nodes,
"GroupQueryAttention_Graph",
graph_input,
graph_output,
)
model = helper.make_model(graph)
return model.SerializeToString()
def create_group_query_attention_graph_past(config, causal=False, past_kv_format=Formats.BSNH, share_buffer=True):
past_kv_seqlen = config.kv_sequence_length if share_buffer else config.past_sequence_length
present_kv_seqlen = (
config.kv_sequence_length if share_buffer else config.past_sequence_length + config.sequence_length
)
nodes = [
helper.make_node(
"GroupQueryAttention",
[
"query",
"key",
"value",
"past_key",
"past_value",
"past_sequence_length" if share_buffer else "",
],
["output", "present_key", "present_value"],
"GroupQueryAttention_0",
num_heads=config.num_heads,
kv_num_heads=config.kv_num_heads,
unidirectional=1 if causal else 0,
is_past_bsnh=1 if past_kv_format == Formats.BSNH else 0,
domain="com.microsoft",
),
]
graph_input = [
helper.make_tensor_value_info(
"query",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"key",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.kv_num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"value",
TensorProto.FLOAT16,
[
config.batch_size,
config.sequence_length,
config.kv_num_heads * config.head_size,
],
),
helper.make_tensor_value_info(
"past_key",
TensorProto.FLOAT16,
[
config.batch_size,
past_kv_seqlen if past_kv_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == Formats.BSNH else past_kv_seqlen,
config.head_size,
],
),
helper.make_tensor_value_info(
"past_value",
TensorProto.FLOAT16,
[
config.batch_size,
past_kv_seqlen if past_kv_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == Formats.BSNH else past_kv_seqlen,
config.head_size,
],
),
]
if share_buffer:
graph_input += [
helper.make_tensor_value_info(
"past_sequence_length",
TensorProto.INT32,
[1],
)
]
graph_output = [
helper.make_tensor_value_info(
"output",
TensorProto.FLOAT16,
[config.batch_size, config.sequence_length, config.num_heads * config.head_size],
),
helper.make_tensor_value_info(
"present_key",
TensorProto.FLOAT16,
[
config.batch_size,
present_kv_seqlen if past_kv_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == Formats.BSNH else present_kv_seqlen,
config.head_size,
],
),
helper.make_tensor_value_info(
"present_value",
TensorProto.FLOAT16,
[
config.batch_size,
present_kv_seqlen if past_kv_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_kv_format == Formats.BSNH else present_kv_seqlen,
config.head_size,
],
),
]
graph = helper.make_graph(
nodes,
"GroupQueryAttention_Graph",
graph_input,
graph_output,
)
model = helper.make_model(graph)
return model.SerializeToString()
def generate_random_padding_mask(max_seqlen, batch_size, device, mode="random"):
assert mode in ["full", "random", "third"]
if mode == "full":
@ -314,6 +514,7 @@ def generate_token_offset(cu_seqlens, max_seqlen):
return numpy.asarray(token_offset + token_padset, dtype=numpy.int32)
# TODO(aciddelgado): rename
def flash_attn_varlen_qkvpacked_func(qkv_unpad, cu_seqlens, token_offset, config, causal=False):
onnx_model_str = create_packed_multihead_attention_graph(config)
qkv_unpad = torch.swapdims(qkv_unpad, 1, 2)
@ -329,7 +530,7 @@ def flash_attn_varlen_qkvpacked_func(qkv_unpad, cu_seqlens, token_offset, config
return output
def flash_attn_func(q, k, v, config, causal=False):
def mha_func(q, k, v, config):
onnx_model_str = create_multihead_attention_graph(config)
q = torch.reshape(q, (config.batch_size, config.sequence_length, -1))
k = torch.reshape(k, (config.batch_size, config.kv_sequence_length, -1))
@ -342,10 +543,108 @@ def flash_attn_func(q, k, v, config, causal=False):
sess_options = SessionOptions()
ort_session = InferenceSession(onnx_model_str, sess_options, providers=["CUDAExecutionProvider"])
ort_output = ort_session.run(None, ort_inputs)
ort_output = numpy.array(ort_output)
output = torch.tensor(ort_output)
return output
def gqa_no_past_func(q, k, v, config, causal=True):
onnx_model_str = create_group_query_attention_graph_no_past(config, causal)
q = torch.reshape(q, (config.batch_size, config.sequence_length, -1))
k = torch.reshape(k, (config.batch_size, config.kv_sequence_length, -1))
v = torch.reshape(v, (config.batch_size, config.kv_sequence_length, -1))
ort_inputs = {
"query": q.detach().cpu().numpy(),
"key": k.detach().cpu().numpy(),
"value": v.detach().cpu().numpy(),
}
sess_options = SessionOptions()
ort_session = InferenceSession(onnx_model_str, sess_options, providers=["CUDAExecutionProvider"])
ort_output = ort_session.run(None, ort_inputs)
ort_output = numpy.array(ort_output)
output = torch.tensor(ort_output)
return output
def gqa_past_func(q, k, v, config, new_k, new_v, past_kv_format=Formats.BSNH, causal=False, share_buffer=True):
onnx_model_str = create_group_query_attention_graph_past(config, causal, past_kv_format, share_buffer)
q = torch.reshape(q, (config.batch_size, config.sequence_length, -1))
past_k = k.clone()
past_v = v.clone()
new_k = torch.reshape(new_k, (config.batch_size, config.sequence_length, -1))
new_v = torch.reshape(new_v, (config.batch_size, config.sequence_length, -1))
if share_buffer:
ort_inputs = {
"query": q.detach().cpu().numpy(),
"key": new_k.detach().cpu().numpy(),
"value": new_v.detach().cpu().numpy(),
"past_key": OrtValue.ortvalue_from_numpy(past_k.detach().cpu().numpy(), "cuda", 0),
"past_value": OrtValue.ortvalue_from_numpy(past_v.detach().cpu().numpy(), "cuda", 0),
"past_sequence_length": torch.tensor([config.past_sequence_length], dtype=torch.int32)
.detach()
.cpu()
.numpy(),
}
sess_options = SessionOptions()
ort_session = InferenceSession(onnx_model_str, sess_options, providers=["CUDAExecutionProvider"])
io_binding = ort_session.io_binding()
io_binding.bind_cpu_input("query", ort_inputs["query"])
io_binding.bind_cpu_input("key", ort_inputs["key"])
io_binding.bind_cpu_input("value", ort_inputs["value"])
io_binding.bind_input(
"past_key", "cuda", 0, numpy.float16, ort_inputs["past_key"].shape(), ort_inputs["past_key"].data_ptr()
)
io_binding.bind_input(
"past_value",
"cuda",
0,
numpy.float16,
ort_inputs["past_value"].shape(),
ort_inputs["past_value"].data_ptr(),
)
io_binding.bind_cpu_input("past_sequence_length", ort_inputs["past_sequence_length"])
io_binding.bind_output("output")
io_binding.bind_ortvalue_output("present_key", ort_inputs["past_key"])
io_binding.bind_ortvalue_output("present_value", ort_inputs["past_value"])
ort_session.run_with_iobinding(io_binding)
ort_output, present_k, present_v = io_binding.copy_outputs_to_cpu()
ort_output = numpy.array(ort_output)
output = torch.tensor(ort_output)
return output, present_k, present_v
else:
ort_inputs = {
"query": q.detach().cpu().numpy(),
"key": new_k.detach().cpu().numpy(),
"value": new_v.detach().cpu().numpy(),
"past_key": past_k.detach().cpu().numpy(),
"past_value": past_v.detach().cpu().numpy(),
}
sess_options = SessionOptions()
ort_session = InferenceSession(onnx_model_str, sess_options, providers=["CUDAExecutionProvider"])
io_binding = ort_session.io_binding()
io_binding.bind_cpu_input("query", ort_inputs["query"])
io_binding.bind_cpu_input("key", ort_inputs["key"])
io_binding.bind_cpu_input("value", ort_inputs["value"])
io_binding.bind_cpu_input("past_key", ort_inputs["past_key"])
io_binding.bind_cpu_input("past_value", ort_inputs["past_value"])
io_binding.bind_output("output")
io_binding.bind_output("present_key")
io_binding.bind_output("present_value")
ort_session.run_with_iobinding(io_binding)
ort_output, present_k, present_v = io_binding.copy_outputs_to_cpu()
ort_output = numpy.array(ort_output)
output = torch.tensor(ort_output)
return output, present_k, present_v
def construct_causal_mask(seqlen_q, seqlen_k, query_padding_mask=None, key_padding_mask=None, device=None):
row_idx = rearrange(torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1")
col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
sk = seqlen_k if key_padding_mask is None else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
sq = seqlen_q if query_padding_mask is None else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
return col_idx > row_idx + sk - sq
def attention_ref(
q,
k,
@ -390,10 +689,17 @@ def attention_ref(
if key_padding_mask is not None:
scores.masked_fill_(rearrange(~key_padding_mask, "b s -> b 1 1 s"), float("-inf"))
if causal:
causal_mask = torch.triu(torch.ones(seqlen_q, seqlen_k, dtype=torch.bool, device=q.device), 1)
# causal_mask = torch.triu(
# torch.ones(seqlen_q, seqlen_k, dtype=torch.bool, device=q.device), 1
# )
causal_mask = construct_causal_mask(seqlen_q, seqlen_k, query_padding_mask, key_padding_mask, q.device)
scores.masked_fill_(causal_mask, float("-inf"))
attention = torch.softmax(scores, dim=-1)
if causal: # Some rows are completely masked out so we fill them with zero instead of NaN
attention = attention.masked_fill(torch.all(causal_mask, dim=-1, keepdim=True), 0.0)
dropout_scaling = 1.0 / (1 - dropout_p)
# attention_drop = attention.masked_fill(~dropout_mask, 0.0) * dropout_scaling
# output = torch.einsum('bhts,bshd->bthd', attention_drop , v)
if dropout_mask is not None:
attention_drop = attention.masked_fill(~dropout_mask, 0.0)
else:
@ -422,7 +728,7 @@ def attention_qkvpacked_ref(
)
def parity_check(
def parity_check_mha(
config,
packed,
rtol=1e-3,
@ -456,7 +762,7 @@ def parity_check(
k = torch.randn(
config.batch_size,
config.kv_sequence_length,
config.num_heads,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
@ -465,19 +771,20 @@ def parity_check(
v = torch.randn(
config.batch_size,
config.kv_sequence_length,
config.num_heads,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
out = flash_attn_func(q, k, v, config)
out = mha_func(q, k, v, config)
out = torch.squeeze(out, 0)
out = torch.reshape(out, (config.batch_size, config.sequence_length, config.num_heads, config.head_size))
out = out.detach().cpu().numpy()
# Pytorch to compare
out_ref, _ = attention_ref(q, k, v, None, None, 0.0, None)
out_ref, _ = attention_ref(q, k, v, None, None, 0.0, None, causal=False)
out_ref = out_ref.detach().cpu().numpy()
# Compare results
print(
" B:",
@ -486,6 +793,340 @@ def parity_check(
config.sequence_length,
" N:",
config.num_heads,
" kvN:",
config.kv_num_heads,
" h:",
config.head_size,
" Mean Error:",
numpy.mean(numpy.abs(out - out_ref)),
numpy.allclose(
out,
out_ref,
rtol=rtol,
atol=atol,
equal_nan=True,
),
)
def parity_check_gqa_no_past(
config,
causal=False,
rtol=1e-3,
atol=1e-3,
):
q = torch.randn(
config.batch_size,
config.sequence_length,
config.num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
k = torch.randn(
config.batch_size,
config.kv_sequence_length,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
v = torch.randn(
config.batch_size,
config.kv_sequence_length,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
# Pytorch to compare
out_ref, _ = attention_ref(q, k, v, None, None, 0.0, None, causal=causal)
out_ref = out_ref.detach().cpu().numpy()
# Flash function
out = gqa_no_past_func(q, k, v, config, causal=causal)
out = torch.squeeze(out, 0)
out = torch.reshape(out, (config.batch_size, config.sequence_length, config.num_heads, config.head_size))
out = out.detach().cpu().numpy()
# Compare results
print(
" causal:",
causal,
" B:",
config.batch_size,
" S:",
config.sequence_length,
" kv S:",
config.kv_sequence_length,
" N:",
config.num_heads,
" kv N:",
config.kv_num_heads,
" h:",
config.head_size,
" Mean Error:",
numpy.mean(numpy.abs(out - out_ref)),
numpy.allclose(
out,
out_ref,
rtol=rtol,
atol=atol,
equal_nan=True,
),
)
def parity_check_gqa_past(
config,
causal=False,
past_format=Formats.BSNH,
rtol=1e-3,
atol=1e-3,
):
q = torch.randn(
config.batch_size,
config.sequence_length,
config.num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
k = torch.randn(
config.batch_size,
config.kv_sequence_length if past_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_format == Formats.BSNH else config.kv_sequence_length,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
v = torch.randn(
config.batch_size,
config.kv_sequence_length if past_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_format == Formats.BSNH else config.kv_sequence_length,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
new_k = torch.randn(
config.batch_size,
config.sequence_length,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
new_v = torch.randn(
config.batch_size,
config.sequence_length,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
# Pytorch to compare
k_cache_ref = k.clone()
v_cache_ref = v.clone()
if past_format == Formats.BNSH:
k_cache_ref = k_cache_ref.transpose(1, 2)
v_cache_ref = v_cache_ref.transpose(1, 2)
cache_seqlens = torch.tensor([config.past_sequence_length], device="cuda").repeat(config.batch_size)
arange = rearrange(torch.arange(config.kv_sequence_length, device="cuda"), "s -> 1 s")
cache_seqlens_expanded = rearrange(cache_seqlens, "b -> b 1")
update_mask = torch.logical_and(
cache_seqlens_expanded <= arange, arange < cache_seqlens_expanded + config.sequence_length
)
k_cache_ref[update_mask] = rearrange(new_k, "b s ... -> (b s) ...")
v_cache_ref[update_mask] = rearrange(new_v, "b s ... -> (b s) ...")
k_cache_rep = repeat(k_cache_ref, "b s h d -> b s (h g) d", g=config.num_heads // config.kv_num_heads)
v_cache_rep = repeat(v_cache_ref, "b s h d -> b s (h g) d", g=config.num_heads // config.kv_num_heads)
key_padding_mask = arange < cache_seqlens_expanded + config.sequence_length
out_ref, _ = attention_ref(q, k_cache_rep, v_cache_rep, None, key_padding_mask, 0.0, None, causal=causal)
out_ref = out_ref.detach().cpu().numpy()
if past_format == Formats.BNSH:
k_cache_ref = k_cache_ref.transpose(1, 2)
v_cache_ref = v_cache_ref.transpose(1, 2)
# Flash function
out, present_k, present_v = gqa_past_func(q, k, v, config, new_k, new_v, past_format, causal, True)
out = torch.squeeze(out, 0)
out = torch.reshape(out, (config.batch_size, config.sequence_length, config.num_heads, config.head_size))
out = out.detach().cpu().numpy()
# Make sure past-present buffer updating correctly
assert numpy.allclose(present_k, k_cache_ref.detach().cpu().numpy(), rtol=rtol, atol=atol, equal_nan=True)
assert numpy.allclose(present_v, v_cache_ref.detach().cpu().numpy(), rtol=rtol, atol=atol, equal_nan=True)
# Compare results
print(
"KV-buffer",
"past kv format:",
"BSNH" if past_format == Formats.BSNH else "BNSH",
" causal:",
causal,
" B:",
config.batch_size,
" S:",
config.sequence_length,
" kv S:",
config.kv_sequence_length,
" N:",
config.num_heads,
" kv N:",
config.kv_num_heads,
" h:",
config.head_size,
" Mean Error:",
numpy.mean(numpy.abs(out - out_ref)),
numpy.allclose(
out,
out_ref,
rtol=rtol,
atol=atol,
equal_nan=True,
),
)
def parity_check_gqa_past_no_buff(
config,
causal=False,
past_format=Formats.BSNH,
rtol=1e-3,
atol=1e-3,
):
q = torch.randn(
config.batch_size,
config.sequence_length,
config.num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
k = torch.randn(
config.batch_size,
config.past_sequence_length if past_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_format == Formats.BSNH else config.past_sequence_length,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
v = torch.randn(
config.batch_size,
config.past_sequence_length if past_format == Formats.BSNH else config.kv_num_heads,
config.kv_num_heads if past_format == Formats.BSNH else config.past_sequence_length,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
new_k = torch.randn(
config.batch_size,
config.sequence_length,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
new_v = torch.randn(
config.batch_size,
config.sequence_length,
config.kv_num_heads,
config.head_size,
device="cuda",
dtype=torch.float16,
requires_grad=False,
)
# Pytorch to compare
k_cache_ref = k.clone()
v_cache_ref = v.clone()
if past_format == Formats.BNSH:
k_cache_ref = k_cache_ref.transpose(1, 2)
v_cache_ref = v_cache_ref.transpose(1, 2)
k_cache_ref = torch.cat((k_cache_ref, new_k), 1)
v_cache_ref = torch.cat((v_cache_ref, new_v), 1)
k_cache_rep = repeat(k_cache_ref, "b s h d -> b s (h g) d", g=config.num_heads // config.kv_num_heads)
v_cache_rep = repeat(v_cache_ref, "b s h d -> b s (h g) d", g=config.num_heads // config.kv_num_heads)
key_padding_mask = None
out_ref, _ = attention_ref(q, k_cache_rep, v_cache_rep, None, key_padding_mask, 0.0, None, causal=causal)
out_ref = out_ref.detach().cpu().numpy()
if past_format == Formats.BNSH:
k_cache_ref = k_cache_ref.transpose(1, 2)
v_cache_ref = v_cache_ref.transpose(1, 2)
# Flash function
out, present_k, present_v = gqa_past_func(q, k, v, config, new_k, new_v, past_format, causal, False)
out = torch.squeeze(out, 0)
out = torch.reshape(out, (config.batch_size, config.sequence_length, config.num_heads, config.head_size))
out = out.detach().cpu().numpy()
# print(present_k[0, 0, config.past_sequence_length, :10])
# print(k_cache_ref[0, 0, config.past_sequence_length, :10])
# print(k_cache_ref.shape)
# print(present_k - k_cache_ref.detach().cpu().numpy())
# Make sure past-present buffer updating correctly
if past_format == Formats.BSNH:
assert numpy.allclose(
present_k,
k_cache_ref.detach().cpu().numpy(),
rtol=rtol,
atol=atol,
equal_nan=True,
)
assert numpy.allclose(
present_v,
v_cache_ref.detach().cpu().numpy(),
rtol=rtol,
atol=atol,
equal_nan=True,
)
else:
assert numpy.allclose(
present_k,
k_cache_ref.detach().cpu().numpy(),
rtol=rtol,
atol=atol,
equal_nan=True,
)
assert numpy.allclose(
present_v,
v_cache_ref.detach().cpu().numpy(),
rtol=rtol,
atol=atol,
equal_nan=True,
)
# Compare results
print(
"Unbuffered",
"past kv format:",
"BSNH" if past_format == Formats.BSNH else "BNSH",
" causal:",
causal,
" B:",
config.batch_size,
" S:",
config.sequence_length,
" kv S:",
config.kv_sequence_length,
" N:",
config.num_heads,
" kv N:",
config.kv_num_heads,
" h:",
config.head_size,
" Mean Error:",
@ -506,8 +1147,8 @@ if __name__ == "__main__":
for s in [97, 128, 200, 256, 257, 384, 512, 768, 1024, 1025, 2048]:
for n in [6]:
for h in [32, 40, 59, 64, 80, 96, 111, 128, 160, 192, 224, 256]:
config = Config(b, s, s, n, h)
parity_check(config, True)
config = Config(b, s, s, 0, n, n, h)
parity_check_mha(config, True)
print("-------- TEST MHA ---------")
for b in [5]:
for s, s2 in [
@ -524,5 +1165,60 @@ if __name__ == "__main__":
]:
for n in [6]:
for h in [32, 40, 59, 64, 80, 96, 111, 128, 160, 192, 224, 256]:
config = Config(b, s, s2, n, h)
parity_check(config, False)
config = Config(b, s, s2, 0, n, n, h)
parity_check_mha(config, False)
print("-------- TEST GQA ---------")
for b in [5]:
for s, s2 in [
(113, 203),
(128, 217),
(113, 211),
(108, 256),
(256, 512),
(512, 256),
(1024, 1024),
(1023, 1024),
(1024, 1023),
(2048, 2048),
]:
for n, n2 in [(6, 6), (6, 3), (9, 9), (9, 3)]:
for h in [32, 40, 64, 80, 96, 128, 160, 192, 224, 256]:
for causal in [True, False]:
config = Config(b, s, s2, 0, n, n2, h)
parity_check_gqa_no_past(config, causal=causal)
print("-------- TEST GQA PAST ---------")
random.seed(69)
for b in [2]:
for s, s2 in [
(1, 128),
(1, 339),
(3, 1024),
(64, 800),
(64, 256),
(3, 799),
(64, 2048),
(16, 20000),
(1, 128 * 512),
(16, 128 * 512),
(128, 128),
]:
for n, n2 in [(6, 6), (6, 3), (9, 9), (9, 3)]:
for h in [32, 40, 64, 80, 96, 128, 160, 192, 224, 256]:
for causal in [True]:
for past_kv_format in [Formats.BNSH, Formats.BSNH]:
sp = random.randint(1, s2 - s) if s2 - s > 0 else 0
config = Config(b, s, s2, sp, n, n2, h)
parity_check_gqa_past(
config,
causal=causal,
past_format=past_kv_format,
rtol=1e-3,
atol=1e-3,
)
parity_check_gqa_past_no_buff(
config,
causal=causal,
past_format=past_kv_format,
rtol=1e-3,
atol=1e-3,
)

View file

@ -898,7 +898,7 @@ def number_of_nvcc_threads(args):
# Standard_NC4as_T4_v3 has 4 CPUs and 28 GB memory. When parallel=4 and nvcc_threads=2,
# total nvcc threads is 4 * 2, which is barely able to build in 28 GB memory so we will use nvcc_threads=1.
memory_per_thread = 4 * 1024 * 1024 * 1024
fmha_cu_files = 4 if is_windows() else 8
fmha_cu_files = 4 if is_windows() else 16
fmha_parallel_jobs = min(fmha_cu_files, number_of_parallel_jobs(args))
nvcc_threads = max(1, int(available_memory / (memory_per_thread * fmha_parallel_jobs)))
print(
@ -2269,7 +2269,9 @@ def generate_documentation(source_dir, build_dir, configs, validate):
have_diff = False
def diff_file(path, regenerate_qualifiers=""):
diff = subprocess.check_output(["git", "diff", path], cwd=source_dir).decode("utf-8")
diff = subprocess.check_output(["git", "diff", "--ignore-blank-lines", path], cwd=source_dir).decode(
"utf-8"
)
if diff:
nonlocal have_diff
have_diff = True