Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion onnxruntime/core/providers/cpu/llm/attention.cc
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,13 @@ void AttentionBase<T>::ComputeAttentionProbs(T* attention_probs,

T* mask_data = nullptr;
bool delete_mask_data = false;
bool causal = parameters.is_causal && parameters.q_sequence_length > 1;
// For the nonpad_kv_seqlen path, keep the existing q_len=1 behavior: the
// TensorScatter decode tests expect bottom-right alignment where the single
// query can see all valid keys.
bool causal = parameters.is_causal &&
(parameters.has_nonpad_kv_seqlen
? parameters.q_sequence_length > 1
: !(parameters.q_sequence_length == 1 && parameters.past_sequence_length > 0));
Comment thread
titaiwangms marked this conversation as resolved.
// When nonpad_kv_seqlen is present the causal frontier is offset-aware
// (bottom-right) and per-batch, so it cannot be baked into the batch-shared mask
// buffer here; it is applied per-batch in the main loop below. Skip the top-left
Expand Down
34 changes: 33 additions & 1 deletion onnxruntime/test/providers/cpu/llm/attention_op_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3087,9 +3087,41 @@ TEST(AttentionTest, Attention4DSoftCapOutputQkRawLogits) {

// ============================================================================
// Causal alignment tests: verify upper-left (no past) vs lower-right (with past)
// These are CUDA-only tests that validate the causal masking fix.
// These tests validate causal mask alignment across CPU and CUDA.
// ============================================================================

// Test: Causal + cross-attention (S_q=1, S_kv=6, no past)
// ONNX spec mandates upper-left alignment: q0 attends only to kv[0].
// This covers GitHub issue #29020, where CPU skipped causal masking for S_q=1.
TEST(AttentionTest, Attention4DCausalSingleQueryCrossAttentionUpperLeft) {
int batch_size = 1;
int q_num_heads = 1;
int q_sequence_length = 1;
int head_size = 8;
int kv_sequence_length = 6;
int kv_num_heads = 1;
int v_head_size = 8;
int past_sequence_length = 0;

std::vector<float> q(batch_size * q_num_heads * q_sequence_length * head_size, 0.0f);
std::vector<float> k(batch_size * kv_num_heads * kv_sequence_length * head_size, 0.0f);
std::vector<float> v(batch_size * kv_num_heads * kv_sequence_length * v_head_size, 0.0f);
for (int i = 0; i < v_head_size; ++i) {
v[i] = 1.0f;
}
std::vector<float> y(batch_size * q_num_heads * q_sequence_length * v_head_size, 1.0f);

RunTest4D(batch_size, q_num_heads, q_sequence_length, head_size, kv_sequence_length, kv_num_heads,
v_head_size, past_sequence_length,
q, k, v, std::vector<float>(), std::initializer_list<bool>(),
std::vector<float>(), std::vector<float>(),
1, -1, std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::quiet_NaN(), -1,
TensorType::kFloat,
y, std::vector<float>(), std::vector<float>(), std::vector<float>(),
false, false, true // disable_cpu, disable_cuda, disable_dml
);
}

// Test: Causal + cross-attention (S_q=3, S_kv=5, no past)
// ONNX spec mandates upper-left alignment: q_i attends to kv[0..i].
// V is identity-like so output directly reveals which KV positions were attended.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,12 +666,11 @@ def attention_past_func(
# #################################################################################################


def construct_causal_mask(seqlen_q, seqlen_k, device):
"""Construct a causal mask for attention."""
def construct_causal_mask(seqlen_q, seqlen_k, device, past_seqlen=0):
"""Construct a causal mask for ONNX Attention upper-left alignment."""
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)
# Causal: positions can only attend to earlier positions
return col_idx > row_idx + seqlen_k - seqlen_q
return col_idx > row_idx + past_seqlen


def attention_ref(
Expand All @@ -682,6 +681,7 @@ def attention_ref(
attn_bias=None,
causal=False,
softcap=0.0,
past_seqlen=0,
):
"""
Reference implementation of scaled dot-product attention with GQA support.
Expand All @@ -694,6 +694,7 @@ def attention_ref(
attn_bias: Additive attention bias [broadcastable to batch, num_heads, seq_q, seq_k]
causal: Whether to apply causal masking
softcap: Softcap value for attention scores (0.0 = disabled)
past_seqlen: Number of past K/V tokens before q[0] for causal masking.

Returns:
output: Attention output [batch, seq_q, num_heads, head_size]
Expand Down Expand Up @@ -724,7 +725,7 @@ def attention_ref(
scores.masked_fill_(rearrange(~key_padding_mask, "b s -> b 1 1 s"), float("-inf"))

if causal:
causal_mask = construct_causal_mask(seqlen_q, seqlen_k, q.device)
causal_mask = construct_causal_mask(seqlen_q, seqlen_k, q.device, past_seqlen=past_seqlen)
scores.masked_fill_(causal_mask, float("-inf"))

attention = torch.softmax(scores, dim=-1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def parity_check_gqa_past(
key_padding_mask=key_padding_mask,
causal=causal,
softcap=config.softcap,
past_seqlen=config.past_kv_sequence_length,
)
out_ref_np = out_ref.to(torch.float32).detach().cpu().numpy()

Expand Down Expand Up @@ -548,6 +549,7 @@ def parity_check_gqa_past_with_padding(
key_padding_mask=key_padding_mask,
causal=config.is_causal == 1,
softcap=config.softcap,
past_seqlen=config.past_kv_sequence_length,
)

# --- ONNX Runtime Path ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def parity_check_mha_past(
attn_bias=attn_bias_ref,
causal=causal,
softcap=config.softcap,
past_seqlen=config.past_kv_sequence_length,
)
out_ref_np = out_ref.to(torch.float32).detach().cpu().numpy()

Expand Down Expand Up @@ -2393,7 +2394,13 @@ def test_mha_past_asymmetric_v_head_size(self):
full_k_bsnh = full_k_bnsh.transpose(1, 2)
full_v_bsnh = full_v_bnsh.transpose(1, 2)

out_ref, _ = attention_ref(q=q, k=full_k_bsnh, v=full_v_bsnh, causal=True)
out_ref, _ = attention_ref(
q=q,
k=full_k_bsnh,
v=full_v_bsnh,
causal=True,
past_seqlen=config.past_kv_sequence_length,
)

# ORT path — should fall back to unfused (not crash in MEA)
out_ort, present_k, present_v = attention_past_func(
Expand Down
Loading