Use lower-right causal mask alignment consistently (#2967)

Co-authored-by: Awni Hannun <[email protected]>
This commit is contained in:
Anri Lombard
2026-01-28 17:15:14 -08:00
committed by GitHub
co-authored by Awni Hannun
parent c86a9bced1
commit 0c6a895ed7
5 changed files with 29 additions and 21 deletions
@@ -152,11 +152,7 @@ DnnGraph build_sdpa_graph(
.set_attn_scale(graph.scalar("Scale", SCALE, float32))
.set_generate_stats(output_logsumexp);
if (do_causal) {
if (q.shape(2) > k.shape(2)) {
options.set_causal_mask(do_causal);
} else {
options.set_causal_mask_bottom_right(do_causal);
}
options.set_causal_mask_bottom_right(do_causal);
}
if (mask_arr) {
options.set_bias(graph.tensor("BIAS", BIAS, *mask_arr));
@@ -201,11 +197,7 @@ DnnGraph build_sdpa_backward_graph(
.set_name("sdpa_backward_cudnn")
.set_attn_scale(graph.scalar("Scale", SCALE, float32));
if (do_causal) {
if (q.shape(2) > k.shape(2)) {
options.set_causal_mask(do_causal);
} else {
options.set_causal_mask_bottom_right(do_causal);
}
options.set_causal_mask_bottom_right(do_causal);
}
if (mask_arr) {
options.set_bias(graph.tensor("BIAS", BIAS, *mask_arr));
@@ -461,7 +461,7 @@ template <
metal::vec<AccumType, kRowsPT> rcp;
STEEL_PRAGMA_UNROLL
for (short i = 0; i < kRowsPT; ++i) {
rcp[i] = (1.f / sum_score[i]);
rcp[i] = 1.f / sum_score[i];
}
Otile.template row_bin_op<MulOp>(rcp);
+2 -2
View File
@@ -720,8 +720,8 @@ array scaled_dot_product_attention(
if (do_causal) {
int kL = k.shape(-2);
int qL = q.shape(-2);
int q_off = (kL - qL) < 0 ? 0 : (kL - qL);
auto q_idx = arange(q_off, q_off + qL, s);
int offset = kL - qL;
auto q_idx = arange(offset, qL + offset, s);
auto k_idx = arange(0, kL, s);
q_idx = expand_dims(q_idx, 1, s);
k_idx = expand_dims(k_idx, 0, s);
+2
View File
@@ -272,6 +272,8 @@ void init_fast(nb::module_& parent_module) {
can have at most 4 dimensions and must be broadcast-compatible with
the shape ``[B, N, T_q, T_kv]``. If an additive mask is given its
type must promote to the promoted type of ``q``, ``k``, and ``v``.
The ``"causal"`` mask uses lower-right alignment where the
last query aligns with the last key.
sinks (array, optional): An optional array of attention sinks.
Default: ``None``.
+22 -8
View File
@@ -23,11 +23,12 @@ def mlx_ref_attn(q, k, v, scale=1.0, mask=None, sinks=None):
v = mx.expand_dims(v, 2)
scores = q @ mx.swapaxes(k, -1, -2)
is_causal = mask == "causal"
if mask is not None:
if mask == "causal":
q_offset = max(0, kL - L)
q_indices = mx.arange(q_offset, q_offset + L)
if is_causal:
offset = kL - L
q_indices = mx.arange(L) + offset
k_indices = mx.arange(kL)
mask = q_indices[:, None] >= k_indices[None]
@@ -58,7 +59,6 @@ def mlx_ref_attn(q, k, v, scale=1.0, mask=None, sinks=None):
out = scores @ v
if n_repeats > 1:
out = mx.reshape(out, [B, n_q_heads, L, -1])
return out
@@ -104,11 +104,14 @@ def prepare_inputs(B, qL, kL, D, qH, kH, mask, transpose, dtype):
# SDPA for MHA (n_heads == n_kv_heads)
def mlx_primitives_sdpa(q, k, v, scale, mask=None):
p = (q * scale) @ k.transpose(0, 1, 3, 2)
qL = q.shape[2]
kL = k.shape[2]
is_causal = mask == "causal"
if mask is not None:
if mask == "causal":
q_offset = max(0, k.shape[2] - q.shape[2])
q_indices = mx.arange(q_offset, q_offset + q.shape[2])
k_indices = mx.arange(k.shape[2])
if is_causal:
offset = kL - qL
q_indices = mx.arange(qL) + offset
k_indices = mx.arange(kL)
mask = q_indices[:, None] >= k_indices[None]
p = mx.where(mask, p, mx.finfo(mx.float32).min)
elif mask.dtype == mx.bool_:
@@ -613,6 +616,17 @@ class TestSDPA(mlx_tests.MLXTestCase):
t,
)
# For causal mask when qL > kL, first qL-kL rows are undefined
# Compare only the valid portion
if mask_str == "causal" and qL > kL:
offset = qL - kL
if t: # transpose=True: shape is (B, qL, qH, D)
out_ref = out_ref[:, offset:, :, :]
out_fst = out_fst[:, offset:, :, :]
else: # transpose=False: shape is (B, qH, qL, D)
out_ref = out_ref[:, :, offset:, :]
out_fst = out_fst[:, :, offset:, :]
atol = 2e-5 if dtype == "float32" else 3e-4
self.assertListEqual(