Skip to content

Commit eb706ed

Browse files
titaiwangmsCopilot
andauthored
Fix CUDA ONNX Attention: min_bias_align crash on SM<80 and MEA NaN for fully-masked batches (microsoft#27831)
Description: ### Summary Fixes three issues in the CUDA ONNX Attention operator and improves spec compliance: 1. min_bias_align crash on SM<80: The alignment check for Memory Efficient Attention (MEA) bias used 4*sizeof(T) (bytes), but the check is against element counts. Fixed to 4 elements, matching CUTLASS kMinimumAlignment. This prevented valid MEA dispatch on SM<80. 2. MEA NaN for fully-masked batches: When nonpad_kv_seqlen=0, CUTLASS MEA computes 1/s_prime where s_prime=0, producing NaN. Added ZeroOutputForFullyMaskedBatches kernel (MEA path only) to zero output for these batches. Uses int64_t for element count to prevent overflow at large context lengths. 3. Flash rejects attn_mask for spec compliance: Flash Attention's paged KV cache produces spec-divergent present_key/present_value layout when used with attn_mask + past_key. Flash now requires attn_mask == nullptr — cases with bool mask + past_key fall to the unfused runner which handles them spec-correctly. Removed ~137 lines of dead code (ConvertMaskToSeqlensKernel, LaunchConvertMaskToFlashSeqlensK) no longer needed after this change. ### Known limitation - GQA + bool attn_mask + past_key currently has no runner (Flash rejected, unfused doesn't support GQA, MEA blocked by past_key). Tracked via TODO — PR microsoft#27851 (MEA with past_key support) will close this gap. ### Related - Issue microsoft#27885: Flash Attention bool attn_mask semantic divergence (root cause documented) - PR microsoft#27851: MEA with past_key support (will close GQA gap) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aaa4944 commit eb706ed

7 files changed

Lines changed: 231 additions & 447 deletions

File tree

onnxruntime/core/providers/cuda/llm/attention.cc

Lines changed: 58 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ static Status TransposeBSNHtoBNSH(int batch_size, int sequence_length,
134134

135135
// ============================================================================
136136
// ConvertAttnMaskToBias: shared helper for mask→additive bias conversion.
137-
// Used by both Flash (nonpad+mask) and MEA paths to avoid code duplication.
137+
// Used by the MEA path to convert masks before the CUTLASS kernel call.
138138
// Converts bool masks to additive bias (true→0, false→mask_filter_value),
139139
// passes float masks through directly, and sets broadcast flags from mask shape.
140140
// ============================================================================
@@ -186,15 +186,12 @@ Status Attention<T>::ConvertAttnMaskToBias(
186186
// Flash Attention dispatch paths:
187187
// Path 1: nonpad_kv_seqlen (opset 24 external cache) -> mha_fwd_kvcache
188188
// Path 2: past_key + past_value (internal cache decode) -> mha_fwd_kvcache
189-
// - Supports: bool mask -> seqlens_k, no mask -> fill past_seq_len
189+
// - No mask support (attn_mask rejected at eligibility)
190190
// - 4D BNSH: transposes Q/K/V to BSNH before kernel
191191
// Path 3: no past, no mask (prompt) -> mha_fwd
192-
// Eligibility: fp16/bf16, head_size==v_head_size, no output_qk,
193-
// (no mask OR bool mask + past OR nonpad_kv_seqlen without mask)
192+
// Eligibility: fp16/bf16, head_size==v_head_size, no output_qk, attn_mask==nullptr
194193
// Note: softcap is passed to the Flash kernel natively. softmax_precision is
195194
// inherently satisfied (Flash accumulates softmax in FP32).
196-
// Note: nonpad_kv_seqlen + attn_mask routes to MEA/unfused, not Flash
197-
// (Flash has no bias parameter for this combination).
198195
//
199196
// PERFORMANCE NOTE: ONNX Attention's internal-cache decode path (past_key/past_value)
200197
// is ~15-30% slower than contrib GQA's decode path for grouped-query attention workloads.
@@ -227,7 +224,7 @@ template <typename T>
227224
Status Attention<T>::RunFlashAttention(
228225
OpKernelContext* context,
229226
const Tensor* Q, const Tensor* K, const Tensor* V,
230-
const Tensor* attn_mask, const Tensor* past_key, const Tensor* past_value,
227+
const Tensor* past_key, const Tensor* past_value,
231228
const Tensor* nonpad_kv_seqlen,
232229
Tensor* Y, Tensor* present_key, Tensor* present_value,
233230
const attention_helper::AttentionParameters& parameters) const {
@@ -294,6 +291,8 @@ Status Attention<T>::RunFlashAttention(
294291
"(past_sequence_length must be 0, got ",
295292
parameters.past_sequence_length, ").");
296293

294+
// seqlens_k_buffer lifetime: allocated via BFC arena, remains valid for all kernel
295+
// launches on the same CUDA stream until the IAllocatorUniquePtr goes out of scope.
297296
auto seqlens_k_buffer = GetScratchBuffer<int>(parameters.batch_size, GetComputeStream(context));
298297
ORT_RETURN_IF_ERROR(LaunchConvertNonpadKvSeqlenToFlashSeqlensK(
299298
nonpad_kv_seqlen->Data<int64_t>(),
@@ -348,25 +347,11 @@ Status Attention<T>::RunFlashAttention(
348347

349348
// Step 1: Compute per-batch past sequence lengths for the concat kernel.
350349
// The concat kernel needs past_seq_lens to know where past data ends and new begins.
350+
// attn_mask is always nullptr here (Flash rejects attn_mask), so use uniform past_seq.
351351
auto past_seqlens_buffer = GetScratchBuffer<int>(parameters.batch_size, GetComputeStream(context));
352-
if (attn_mask != nullptr && attn_mask->IsDataType<bool>()) {
353-
size_t mask_dims = attn_mask->Shape().NumDimensions();
354-
auto dims = attn_mask->Shape().GetDims();
355-
int64_t mask_dim0 = dims[0];
356-
int64_t mask_dim1 = mask_dims >= 3 ? dims[1] : 0;
357-
int64_t mask_dim2 = mask_dims >= 4 ? dims[2] : 0;
358-
// Offset -kv_seq: mask encodes total valid count; subtract to get past-only count.
359-
int seqlen_offset = -parameters.kv_sequence_length;
360-
ORT_RETURN_IF_ERROR(LaunchConvertMaskToFlashSeqlensK(
361-
attn_mask->Data<bool>(), past_seqlens_buffer.get(),
362-
parameters.batch_size, parameters.total_sequence_length,
363-
static_cast<int>(mask_dims), mask_dim0, mask_dim1, mask_dim2,
364-
cuda_stream, device_prop.maxThreadsPerBlock, seqlen_offset));
365-
} else {
366-
ORT_RETURN_IF_ERROR(LaunchFillInt32(past_seqlens_buffer.get(), parameters.past_sequence_length,
367-
parameters.batch_size, cuda_stream,
368-
device_prop.maxThreadsPerBlock));
369-
}
352+
ORT_RETURN_IF_ERROR(LaunchFillInt32(past_seqlens_buffer.get(), parameters.past_sequence_length,
353+
parameters.batch_size, cuda_stream,
354+
device_prop.maxThreadsPerBlock));
370355

371356
// Step 2: Transpose K/V to BSNH if input is 4D BNSH (concat kernel reads new as BSNH).
372357
const T* k_new_bsnh = K->Data<T>();
@@ -399,11 +384,7 @@ Status Attention<T>::RunFlashAttention(
399384
// into present buffer at [past_seq, past_seq + kv_seq), all in BNSH.
400385
// Note: is_bsnh=false means past/present cache layout is BNSH. New tokens
401386
// (k_new_bsnh/v_new_bsnh) are always read as BSNH by the kernel (hardcoded strides).
402-
// NOTE: When bool masks produce variable per-batch past_seq_lens, positions in the range
403-
// [past_seq_lens[b] + kv_sequence_length, total_sequence_length) for each batch b are left
404-
// uninitialized by the concat kernel. Flash Attention reads only up to seqlens_k[b] positions
405-
// per batch, so these values are never accessed. In the no-mask case (uniform past_seq_lens),
406-
// every position in the present buffer is written.
387+
// past_seqlens is uniform (no mask) so every position in the present buffer is written.
407388
ORT_RETURN_IF_ERROR(onnxruntime::contrib::cuda::LaunchConcatNewToPastKV<NativeCudaT>(
408389
parameters.batch_size,
409390
parameters.kv_num_heads,
@@ -427,27 +408,13 @@ Status Attention<T>::RunFlashAttention(
427408
// Step 4: Compute total seqlens for mha_fwd_kvcache.
428409
// With k_new=nullptr, the kernel treats seqlens_k as the total valid token count
429410
// (not pre-append count), so we need past + new.
411+
// attn_mask is always nullptr here (Flash rejects attn_mask), so use uniform seqlens.
430412
auto seqlens_k_buffer = GetScratchBuffer<int>(parameters.batch_size, GetComputeStream(context));
431-
if (attn_mask != nullptr && attn_mask->IsDataType<bool>()) {
432-
size_t mask_dims = attn_mask->Shape().NumDimensions();
433-
auto dims = attn_mask->Shape().GetDims();
434-
int64_t mask_dim0 = dims[0];
435-
int64_t mask_dim1 = mask_dims >= 3 ? dims[1] : 0;
436-
int64_t mask_dim2 = mask_dims >= 4 ? dims[2] : 0;
437-
// Offset 0: mask encodes total valid count, which is exactly what we need.
438-
int seqlen_offset = 0;
439-
ORT_RETURN_IF_ERROR(LaunchConvertMaskToFlashSeqlensK(
440-
attn_mask->Data<bool>(), seqlens_k_buffer.get(),
441-
parameters.batch_size, parameters.total_sequence_length,
442-
static_cast<int>(mask_dims), mask_dim0, mask_dim1, mask_dim2,
443-
cuda_stream, device_prop.maxThreadsPerBlock, seqlen_offset));
444-
} else {
445-
ORT_RETURN_IF_ERROR(LaunchFillInt32(
446-
seqlens_k_buffer.get(),
447-
parameters.past_sequence_length + parameters.kv_sequence_length,
448-
parameters.batch_size, cuda_stream,
449-
device_prop.maxThreadsPerBlock));
450-
}
413+
ORT_RETURN_IF_ERROR(LaunchFillInt32(
414+
seqlens_k_buffer.get(),
415+
parameters.past_sequence_length + parameters.kv_sequence_length,
416+
parameters.batch_size, cuda_stream,
417+
device_prop.maxThreadsPerBlock));
451418

452419
// Step 5: Flash attention on pre-populated cache.
453420
// k_new=nullptr tells mha_fwd_kvcache to skip its internal Append_KV — the cache
@@ -542,7 +509,6 @@ Status Attention<T>::RunFlashAttention(
542509
ORT_UNUSED_PARAMETER(Q);
543510
ORT_UNUSED_PARAMETER(K);
544511
ORT_UNUSED_PARAMETER(V);
545-
ORT_UNUSED_PARAMETER(attn_mask);
546512
ORT_UNUSED_PARAMETER(past_key);
547513
ORT_UNUSED_PARAMETER(past_value);
548514
ORT_UNUSED_PARAMETER(nonpad_kv_seqlen);
@@ -730,6 +696,22 @@ Status Attention<T>::RunMemoryEfficientAttention(
730696
p.workspace = nullptr;
731697
}
732698
onnxruntime::contrib::cuda::run_memory_efficient_attention(p);
699+
700+
// On the MEA (CUTLASS) path (used for both MHA and GQA when nonpad_kv_seqlen is provided),
701+
// zero out output for fully-masked batches to produce zeros (matching Flash behavior).
702+
// CUTLASS epilogue computes 1/s_prime where s_prime=0 for seqlens_k=0, producing NaN.
703+
{
704+
using CudaT = typename onnxruntime::cuda::OrtToCudaType<T>::type;
705+
int64_t elements_per_batch = static_cast<int64_t>(parameters.q_sequence_length) *
706+
parameters.q_num_heads * parameters.v_head_size;
707+
ORT_RETURN_IF_ERROR(LaunchZeroOutputForFullyMaskedBatches<CudaT>(
708+
reinterpret_cast<CudaT*>(out_data),
709+
seqlens_k_buffer.get(),
710+
parameters.batch_size,
711+
elements_per_batch,
712+
cuda_stream,
713+
device_prop.maxThreadsPerBlock));
714+
}
733715
}
734716
// Standard MEA path: float attention bias, bool mask (converted to bias), or no mask.
735717
// Bool masks are converted to additive attention bias (true→0, false→mask_filter_value)
@@ -858,6 +840,8 @@ Status Attention<T>::RunUnfusedAttention(
858840
Tensor* output_qk,
859841
const attention_helper::AttentionParameters& parameters) const {
860842
using CudaT = typename ToCudaType<T>::MappedType;
843+
// OrtToCudaType maps BFloat16 → __nv_bfloat16 (native HW type), matching kernel instantiations.
844+
using NativeCudaT = typename onnxruntime::cuda::OrtToCudaType<T>::type;
861845
auto& device_prop = GetDeviceProp();
862846
auto cuda_stream = Stream(context);
863847
auto ort_stream = GetOrtStream(context);
@@ -938,7 +922,6 @@ Status Attention<T>::RunUnfusedAttention(
938922
IAllocatorUniquePtr<void> mask_bias_buffer; // temp buffer for mask→bias when composing
939923
if (nonpad_kv_seqlen != nullptr) {
940924
// Convert nonpad_kv_seqlen to additive attention bias: [B, q_seq, total_seq]
941-
using NativeCudaT = typename onnxruntime::cuda::OrtToCudaType<T>::type;
942925
int64_t bias_elements = static_cast<int64_t>(parameters.batch_size) *
943926
parameters.q_sequence_length *
944927
parameters.total_sequence_length;
@@ -1004,7 +987,6 @@ Status Attention<T>::RunUnfusedAttention(
1004987
contribop_parameters.broadcast_attn_bias_dim_1 = true;
1005988
} else if (attn_mask != nullptr) {
1006989
if (attn_mask->IsDataType<bool>()) {
1007-
using NativeCudaT = typename onnxruntime::cuda::OrtToCudaType<T>::type;
1008990
int64_t num_elements = attn_mask->Shape().Size();
1009991
converted_mask_buffer = GetScratchBuffer<void>(num_elements * sizeof(NativeCudaT), GetComputeStream(context));
1010992
ORT_RETURN_IF_ERROR(LaunchConvertBoolMaskToAttentionBias<NativeCudaT>(
@@ -1049,6 +1031,9 @@ Status Attention<T>::RunUnfusedAttention(
10491031
cublasHandle_t cublas = GetCublasHandle(context);
10501032
cudnnHandle_t cudnn = GetCudnnHandle(context);
10511033

1034+
// Note: unfused attention produces valid finite output (mean-of-V via uniform softmax)
1035+
// for fully-masked batches, so ZeroOutput is not needed here. Only MEA requires
1036+
// ZeroOutput to prevent NaN from the CUTLASS epilogue's 1/s_prime division.
10521037
return onnxruntime::contrib::cuda::QkvToContext<CudaT, CudaT>(
10531038
device_prop, cublas, cudnn, ort_stream.get(), contribop_parameters, data);
10541039
}
@@ -1134,20 +1119,17 @@ Status Attention<T>::ComputeInternal(OpKernelContext* context) const {
11341119
parameters.q_num_heads, parameters.kv_num_heads) &&
11351120
parameters.head_size == parameters.v_head_size &&
11361121
!has_output_qk &&
1137-
// Bool masks without past_key (prompt) can't use flash because mha_fwd_kvcache's
1138-
// causal semantics are decode-oriented (window offset by seqlens_k). For causal
1139-
// prompt with padding, MEA handles it correctly via attention bias conversion.
1140-
// Flash handles: no mask, decode with past (±mask), nonpad_kv_seqlen.
1141-
// Note: contrib MHA similarly excludes flash when attention_bias is present
1142-
// (no mask support in mha_fwd). Float masks and bool prompt masks route to MEA
1143-
// which supports additive bias natively.
1144-
(attn_mask == nullptr || (attn_mask->IsDataType<bool>() && past_key != nullptr)) &&
1145-
// Flash cannot handle nonpad_kv_seqlen + attn_mask simultaneously (no bias parameter
1146-
// in mha_fwd/mha_fwd_kvcache when seqlens_k is used). Route to MEA instead.
1147-
!(nonpad_kv_seqlen != nullptr && attn_mask != nullptr);
1122+
// Flash does not support attention masks (no bias parameter in mha_fwd/mha_fwd_kvcache).
1123+
// Bool attn_mask + past_key is rejected because Flash uses paged KV cache semantics
1124+
// that produce spec-divergent present_kv layout for partial masks (e.g. [T,T,T,F]).
1125+
// Unfused handles bool+past_key spec-correctly via standard ConcatPastToPresent.
1126+
// TODO(titaiwang): GQA + bool attn_mask + past_key currently has no runner (Flash
1127+
// rejected here, unfused doesn't support GQA, MEA blocked by past_key != nullptr).
1128+
// Once PR #27851 merges (MEA supports past_key), this gap will be covered.
1129+
attn_mask == nullptr;
11481130

11491131
if (flash_eligible) {
1150-
return RunFlashAttention(context, Q, K, V, attn_mask, past_key, past_value,
1132+
return RunFlashAttention(context, Q, K, V, past_key, past_value,
11511133
nonpad_kv_seqlen, Y, present_key, present_value, parameters);
11521134
}
11531135
}
@@ -1171,13 +1153,14 @@ Status Attention<T>::ComputeInternal(OpKernelContext* context) const {
11711153
// total_sequence_length. Skip MEA if this stride can't satisfy the kernel's
11721154
// minimum alignment requirement.
11731155
if (mea_eligible && attn_mask != nullptr) {
1174-
int min_bias_align = 1;
1175-
if ((std::is_same<T, float>::value && sm >= 80) ||
1176-
(!std::is_same<T, float>::value && sm >= 75)) {
1177-
min_bias_align = 4; // TensorOp on Sm80+ (float) or Sm75+ (fp16/bf16)
1178-
} else if (!std::is_same<T, float>::value && sm >= 70) {
1179-
min_bias_align = 2; // TensorOp on Volta (fp16)
1180-
}
1156+
// NOTE: CUTLASS uses kMinimumAlignment = 4 (elements, not bytes) for the bias
1157+
// pointer in its epilogue. total_sequence_length is the bias row stride in elements,
1158+
// so we check alignment in element count. The contrib_ops convention (4 * sizeof(T))
1159+
// conflates bytes with elements; we use the correct value of 4 elements here.
1160+
// Note: on SM50/53 (Maxwell), CUTLASS kMinimumAlignment=1, so this is stricter than
1161+
// necessary — cases with odd total_sequence_length that previously used MEA on those
1162+
// GPUs will now fall to unfused. This is acceptable for these very old architectures.
1163+
constexpr int min_bias_align = 4;
11811164
if (parameters.total_sequence_length % min_bias_align != 0) {
11821165
mea_eligible = false;
11831166
}
@@ -1215,8 +1198,9 @@ Status Attention<T>::ComputeInternal(OpKernelContext* context) const {
12151198
// to replicate kv_num_heads -> q_num_heads before unfused can process.
12161199
// Requires ~160 lines. See issue #27516.
12171200
return ORT_MAKE_STATUS(ONNXRUNTIME, NOT_IMPLEMENTED,
1218-
"GQA (q_num_heads != kv_num_heads) requires flash or memory efficient attention, "
1219-
"but neither is eligible. Ensure fp16/bf16 on Ampere+ GPU, or check head_size constraints.");
1201+
"ONNX Attention with GQA (q_num_heads != kv_num_heads) is not supported by the "
1202+
"unfused runner. Flash requires fp16/bf16, SM>=80, and attn_mask==nullptr; MEA "
1203+
"requires past_key==nullptr. See PR #27851 for MEA past_key support.");
12201204
}
12211205

12221206
return RunUnfusedAttention(context, Q, K, V, attn_mask, past_key, past_value,

onnxruntime/core/providers/cuda/llm/attention.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Attention final : public CudaKernel {
1818
Status RunFlashAttention(
1919
OpKernelContext* context,
2020
const Tensor* Q, const Tensor* K, const Tensor* V,
21-
const Tensor* attn_mask, const Tensor* past_key, const Tensor* past_value,
21+
const Tensor* past_key, const Tensor* past_value,
2222
const Tensor* nonpad_kv_seqlen,
2323
Tensor* Y, Tensor* present_key, Tensor* present_value,
2424
const attention_helper::AttentionParameters& parameters) const;

0 commit comments

Comments
 (0)