@@ -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>
227224Status 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,
0 commit comments