Skip to content

Commit b85d536

Browse files
Updates to JAX flash attention kernel.
- Sink jnp.dynamic-slice instructions fetching blocks of q, k and v into the inner loop. - Forced final @v layout for better utilization - Forcing fusions with must_fuse_call - Removed calculations related to mask blocking when the mask is causal FUTURE_COPYBARA_INTEGRATE_REVIEW=#4504 from AI-Hypercomputer:fix/nnx_layers 90f5201 PiperOrigin-RevId: 952800018
1 parent 3a792f1 commit b85d536

2 files changed

Lines changed: 116 additions & 29 deletions

File tree

src/maxtext/kernels/attention/jax_flash_attention.py

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616
from typing import Optional, Tuple, Union
1717

1818
import jax
19+
from jax.experimental import layout
20+
from jax.experimental.pallas.ops.tpu.splash_attention import splash_attention_mask as mask_lib
21+
from jax.experimental.xla_metadata import must_fuse_call
1922
import jax.numpy as jnp
2023
from maxtext.kernels.attention import splash_attention_kernel
2124

25+
26+
DLL = layout.Layout
27+
Layout = layout.Format
28+
2229
SegmentIds = splash_attention_kernel.SegmentIds
2330

2431

@@ -35,7 +42,7 @@ def flash_attention_block_masked(
3542
segment_ids: SegmentIds | None,
3643
block_kv: int,
3744
block_q: int,
38-
mask: jnp.ndarray,
45+
mask: mask_lib.Mask,
3946
mask_value: float,
4047
cap: Optional[float] = None,
4148
save_residuals: bool = False,
@@ -91,15 +98,39 @@ def flash_attention_block_masked(
9198
num_kv_blocks = kv_seq_len // block_kv
9299
num_q_blocks = q_seq_len // block_q
93100

101+
mask_array = mask[:, :]
94102
# Before applying the segment mask, we need to broadcast the mask in batch
95103
# dimension since we have same logic for all batches.
96-
mask_full = jnp.broadcast_to(mask[None, :, :], (batch_size, q_seq_len, kv_seq_len))
104+
mask_full = jnp.broadcast_to(mask_array[None, :, :], (batch_size, q_seq_len, kv_seq_len))
97105

98106
if segment_ids is not None:
99107
segment_ids_q = segment_ids.q[:, :, None]
100108
segment_ids_kv = segment_ids.kv[:, None, :]
101109
mask_full = jnp.logical_and(mask_full, segment_ids_q == segment_ids_kv)
102-
mask_blocked = jax.jit(mask_blocker, static_argnums=[1, 2])(mask_full, block_q, block_kv)
110+
111+
if isinstance(mask, mask_lib.CausalMask):
112+
# In the case of a causal mask, the compute_attention_block should be executed
113+
# if the current block (i, j) falls within the lower triangle. This means that
114+
# the maximum query index in block i must be greater than or equal to the
115+
# minimum key/value index in block j.
116+
# Max q_idx in block i: (i + 1) * block_q - 1
117+
# Min kv_idx in block j: j * block_kv
118+
# Condition: (i + 1) * block_q - 1 >= j * block_kv
119+
# Which simplifies to: (i + 1) * block_q > j * block_kv
120+
should_compute_block = lambda i, j: (i + 1) * block_q > j * block_kv
121+
else:
122+
mask_blocked = jax.jit(mask_blocker, static_argnums=[1, 2])(
123+
mask_full, block_q, block_kv
124+
)
125+
126+
def should_compute_block(i, j):
127+
mask_i_j_slice = jax.lax.dynamic_slice(
128+
mask_blocked, (0, i, j), (batch_size, 1, 1)
129+
)
130+
# The compute_attention_block should be executed if at least one element
131+
# in the slice is non-zero, meaning at least one batch requires work for
132+
# this block.
133+
return jnp.any(jnp.not_equal(mask_i_j_slice, 0))
103134

104135
# Initialize `l` (logsumexp) and `m` (max_logits) for the online softmax.
105136
# `l` is initialized to 0 since no blocks have been processed yet and the sum
@@ -127,28 +158,17 @@ def flash_attention_block_masked(
127158
# Outer loop over the key/value blocks.
128159
def outer_loop_body(j, carried):
129160
output, l, m = carried
130-
k_j_slice = jax.lax.dynamic_slice_in_dim(k, j * block_kv, block_kv, axis=-2)
131-
v_j_slice = jax.lax.dynamic_slice_in_dim(v, j * block_kv, block_kv, axis=-2)
132161

133162
# Inner loop over the query blocks.
134163
def inner_loop_body(i, carried_inner):
135164
output, l, m = carried_inner
136165

137-
# let's get the slice of Q in N dimension
138-
q_slice = jax.lax.dynamic_slice_in_dim(q, i * block_q, block_q, axis=-2)
139-
140166
# Calculates the attention computation (Q@K.T)@V with online softmax for
141167
# the current query and key/value blocks.
142168
def compute_attention_block(output, l, m):
143169
output_i_slice = jax.lax.dynamic_slice_in_dim(output, i * block_q, block_q, axis=-2)
144170
l_i_slice = jax.lax.dynamic_slice_in_dim(l, i * block_q, block_q, axis=-1)
145171
m_i_slice = jax.lax.dynamic_slice_in_dim(m, i * block_q, block_q, axis=-1)
146-
s_i_j = jnp.einsum(
147-
"bxhqc,bxkc->bxhqk",
148-
q_slice,
149-
k_j_slice,
150-
preferred_element_type=data_type,
151-
)
152172
full_mask_i_j_slice = jax.lax.dynamic_slice(
153173
mask_full,
154174
(0, i * block_q, j * block_kv),
@@ -159,12 +179,41 @@ def compute_attention_block(output, l, m):
159179
(batch_size, num_kv_heads, q_groups, block_q, block_kv),
160180
)
161181

182+
k_j_slice = jax.lax.dynamic_slice_in_dim(k, j * block_kv, block_kv, axis=-2)
183+
v_j_slice = jax.lax.dynamic_slice_in_dim(v, j * block_kv, block_kv, axis=-2)
184+
185+
# let's get the slice of Q in N dimension
186+
q_slice = jax.lax.dynamic_slice_in_dim(q, i * block_q, block_q, axis=-2)
187+
188+
s_i_j_dup = jnp.einsum(
189+
"bxhqc,bxkc->bxhqk",
190+
q_slice,
191+
k_j_slice,
192+
preferred_element_type=data_type,
193+
)
194+
s_i_j_dup = jnp.where(broadcasted_mask, s_i_j_dup, mask_value)
162195
if cap is not None:
163-
s_i_j = jnp.tanh(s_i_j / cap)
164-
s_i_j = s_i_j * cap
165-
s_i_j = jnp.where(broadcasted_mask, s_i_j, mask_value)
166-
m_i_j = s_i_j.max(axis=-1)
167-
p_i_j = jnp.exp(s_i_j - m_i_j[..., None])
196+
s_i_j_dup = jnp.tanh(s_i_j_dup / cap)
197+
s_i_j_dup = s_i_j_dup * cap
198+
m_i_j = s_i_j_dup.max(axis=-1)
199+
200+
def fuse_this(q_slice, k_j_slice, mask_value, broadcasted_mask, m_i_j):
201+
s_i_j = jnp.einsum(
202+
"bxhqc,bxkc->bxhqk",
203+
q_slice,
204+
k_j_slice,
205+
preferred_element_type=jnp.bfloat16,
206+
)
207+
s_i_j = jnp.where(broadcasted_mask, s_i_j, mask_value)
208+
if cap is not None:
209+
s_i_j = jnp.tanh(s_i_j / cap)
210+
s_i_j = s_i_j * cap
211+
p_i_j = jnp.exp(s_i_j - m_i_j[..., None])
212+
return p_i_j
213+
214+
p_i_j = must_fuse_call("1")(fuse_this)(
215+
q_slice, k_j_slice, mask_value, broadcasted_mask, m_i_j
216+
)
168217
l_i_j = p_i_j.sum(axis=-1)
169218
assert m_i_j.shape == m_i_slice.shape
170219
m_i_new = jnp.maximum(m_i_slice, m_i_j)
@@ -173,14 +222,24 @@ def compute_attention_block(output, l, m):
173222
l_i_new = m_i_difference * l_i_slice + m_i_j_difference * l_i_j
174223

175224
divider = l_i_new[..., None]
176-
numerator = l_i_slice[..., None] * m_i_difference[..., None] * output_i_slice + m_i_j_difference[
177-
..., None
178-
] * jnp.einsum(
225+
pv = jnp.einsum(
179226
"bxhqk,bxkc->bxhqc",
180227
p_i_j,
181228
v_j_slice,
182229
preferred_element_type=data_type,
183230
)
231+
# This forces the layout of the final @V to have better utilization by
232+
# avoiding using XLU:
233+
# Changes conv emitter type from
234+
# EmitAllInputFeatureInSublanesOutputBatchInSublanesXposeReuse to
235+
# EmitInputBatchInLanes
236+
pv = layout.with_layout_constraint(
237+
pv, DLL(major_to_minor=(0, 1, 2, 4, 3))
238+
)
239+
numerator = (
240+
l_i_slice[..., None] * m_i_difference[..., None] * output_i_slice
241+
+ m_i_j_difference[..., None] * pv
242+
)
184243

185244
output_i_slice_new = numerator / divider
186245
output = jax.lax.dynamic_update_index_in_dim(output, output_i_slice_new, i * block_q, axis=-2)
@@ -193,13 +252,8 @@ def identity(output, l, m):
193252

194253
return output, l, m
195254

196-
batch_size = mask_blocked.shape[0]
197-
mask_i_j_slice = jax.lax.dynamic_slice(mask_blocked, (0, i, j), (batch_size, 1, 1))
198-
# The compute_attention_block should be executed if at least one element
199-
# in the slice is non-zero, meaning at least one batch requires work for
200-
# this block.
201255
output, l, m = jax.lax.cond(
202-
jnp.any(jnp.not_equal(mask_i_j_slice, 0)),
256+
should_compute_block(i, j),
203257
compute_attention_block,
204258
identity,
205259
output,

tests/unit/attention_test.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def test_flash_attention_block_masked_soft_cap(self):
7171
key = jnp.array([[[[10.0], [0.0]]]], dtype=jnp.float32)
7272
value = jnp.array([[[[1.0], [3.0]]]], dtype=jnp.float32)
7373
mask = jnp.array([[True, True], [True, False]])
74+
mask_obj = splash_attention_mask.NumpyMask(mask)
7475

7576
output = jax_flash_attention.flash_attention_block_masked(
7677
query,
@@ -79,7 +80,7 @@ def test_flash_attention_block_masked_soft_cap(self):
7980
segment_ids=None,
8081
block_kv=2,
8182
block_q=1,
82-
mask=mask,
83+
mask=mask_obj,
8384
mask_value=mask_value,
8485
cap=cap,
8586
)
@@ -95,6 +96,38 @@ def test_flash_attention_block_masked_soft_cap(self):
9596
atol=1e-2,
9697
)
9798

99+
def test_flash_attention_block_masked_causal_mask(self):
100+
cap = 1.0
101+
mask_value = -1.0e9
102+
query = jnp.array([[[[10.0], [10.0]]]], dtype=jnp.float32)
103+
key = jnp.array([[[[10.0], [0.0]]]], dtype=jnp.float32)
104+
value = jnp.array([[[[1.0], [3.0]]]], dtype=jnp.float32)
105+
mask_obj = splash_attention_mask.CausalMask((2, 2))
106+
107+
output = jax_flash_attention.flash_attention_block_masked(
108+
query,
109+
key,
110+
value,
111+
segment_ids=None,
112+
block_kv=2,
113+
block_q=1,
114+
mask=mask_obj,
115+
mask_value=mask_value,
116+
cap=cap,
117+
)
118+
119+
mask_arr = mask_obj[:, :]
120+
logits = jnp.einsum("bhqd,bhkd->bhqk", query, key)
121+
logits = jnp.tanh(logits / cap) * cap
122+
logits = jnp.where(mask_arr[None, None, :, :], logits, mask_value)
123+
expected = jnp.einsum("bhqk,bhkd->bhqd", jax.nn.softmax(logits, axis=-1), value)
124+
np.testing.assert_allclose(
125+
np.asarray(output),
126+
np.asarray(expected),
127+
rtol=1e-2,
128+
atol=1e-2,
129+
)
130+
98131

99132
class SplashLocalMaskTest(unittest.TestCase):
100133
"""Tests for Splash local masks."""

0 commit comments

Comments
 (0)