Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 14 additions & 3 deletions pytensor_ml/dispatch/jax/attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ def jax_funcify_AttentionLayer(op, node=None, **kwargs):
scale = op.scale

def attention(q, k, v, mask=None):
# jax's is_causal aligns the triangle top-left, so a query block shorter than its keys, as when
# decoding from a cache, would see only the first keys. The layer aligns it bottom-right, so
# every query sees the whole prefix before it; build that triangle here when the lengths differ.
q_len, kv_len = q.shape[-2], k.shape[-2]
causal, causal_mask = is_causal, None
if is_causal and q_len != kv_len:
rows = jnp.arange(q_len)[:, None]
cols = jnp.arange(kv_len)[None, :]
causal, causal_mask = False, (cols <= rows + (kv_len - q_len))[None, None]
# jax expects (batch, seq, head, dim); our convention is (batch, head, seq, dim). The additive
# mask is (batch, head, seq, seq) in both, so only q/k/v are transposed. is_causal composes with
# bias, so a combined causal + padding mask needs no manual construction here.
# mask is (batch, head, seq, seq) in both, so only q/k/v are transposed. jax combines the
# additive bias with whichever triangle applies, its own or the boolean one built above.
q = jnp.swapaxes(q, -3, -2)
k = jnp.swapaxes(k, -3, -2)
v = jnp.swapaxes(v, -3, -2)
out = jax.nn.dot_product_attention(q, k, v, bias=mask, scale=scale, is_causal=is_causal)
out = jax.nn.dot_product_attention(
q, k, v, bias=mask, mask=causal_mask, scale=scale, is_causal=causal
)
return jnp.swapaxes(out, -3, -2)

return attention
15 changes: 15 additions & 0 deletions tests/dispatch/jax/test_attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ def test_additive_mask(is_causal, rng):
compare_jax_and_py([q_sym, k_sym, v_sym], out, [q, k, v], assert_fn=assert_close)


@pytest.mark.parametrize("with_mask", [False, True], ids=["causal_only", "mask_and_causal"])
def test_a_query_shorter_than_its_keys_sees_the_whole_prefix(with_mask, rng):
"""Decoding from a key-value cache: two queries over four keys. The triangle aligns bottom-right,
so the first query sees keys 0 to 2 and the second all four."""
q = rng.normal(size=(2, 3, 2, 4)).astype(floatX)
k, v = (rng.normal(size=(2, 3, 4, 4)).astype(floatX) for _ in range(2))
mask = np.where(rng.normal(size=(2, 3, 2, 4)) > 0, 0.0, -np.inf).astype(floatX)
mask[:, :, :, 0] = 0.0
q_sym, k_sym, v_sym = symbolic_like(q, k, v)
out = scaled_dot_product_attention(
q_sym, k_sym, v_sym, mask=pt.as_tensor(mask) if with_mask else None, is_causal=True
)
compare_jax_and_py([q_sym, k_sym, v_sym], out, [q, k, v], assert_fn=assert_close)


def test_dispatch_auto_registers():
"""Loading JAX's dispatch must auto-register ours -- no `import pytensor_ml.dispatch.jax` needed."""
from pytensor.link.jax.dispatch import jax_funcify
Expand Down
12 changes: 12 additions & 0 deletions tests/test_attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def test_sdpa_matches_reference(is_causal, scale, rng):
np.testing.assert_allclose(out, sdpa_np(q, k, v, is_causal=is_causal, scale=scale), atol=1e-5)


def test_sdpa_causal_query_shorter_than_its_keys_sees_the_whole_prefix(rng):
"""Decoding from a key-value cache: the triangle aligns bottom-right, so each of the two queries
attends to every key up to its own position at the end of the five."""
q = rng.normal(size=(2, 3, 2, 4)).astype(floatX)
k = rng.normal(size=(2, 3, 5, 4)).astype(floatX)
v = rng.normal(size=(2, 3, 5, 6)).astype(floatX)

out = scaled_dot_product_attention(q, k, v, is_causal=True).eval()

np.testing.assert_allclose(out, sdpa_np(q, k, v, is_causal=True), atol=1e-5)


def test_sdpa_grouped_query(rng):
b, n_head, n_kv_head, seq, dim = 2, 6, 2, 4, 5
q = rng.normal(size=(b, n_head, seq, dim)).astype(floatX)
Expand Down
Loading