Skip to content

Commit 80646f7

Browse files
Merge pull request #4537 from huytransformer:htn/tokamax-ring-load-balance
PiperOrigin-RevId: 952535031
2 parents a8e7fae + bb0f40a commit 80646f7

12 files changed

Lines changed: 229 additions & 30 deletions

src/maxtext/configs/types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3421,7 +3421,10 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
34213421
if self.packing:
34223422
raise ValueError("TPU Tokamax ring attention does not support packing yet.")
34233423
if self.context_parallel_load_balance:
3424-
raise ValueError("TPU Tokamax ring attention does not support context_parallel_load_balance yet.")
3424+
if context_parallel_size % 2 != 0:
3425+
raise ValueError("TPU Tokamax ring load balancing requires an even context_parallel_size.")
3426+
if self.mtp_num_layers > 0:
3427+
raise ValueError("TPU Tokamax ring attention with context_parallel_load_balance=True does not support MTP.")
34253428
if self.use_ragged_attention:
34263429
raise ValueError("TPU Tokamax ring attention does not support ragged attention.")
34273430
if self.attention_sink:

src/maxtext/kernels/attention/tokamax_ring_attention.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525

2626
import jax
2727
from jax.experimental import pallas as pl
28+
import numpy as np
2829

2930
from maxtext.common.common_types import MODEL_MODE_TRAIN
3031
from maxtext.kernels.tokamax_splash_attention import ring_attention_kernel
3132
from maxtext.kernels.tokamax_splash_attention import splash_attention_kernel as tokamax_splash_kernel
3233
from maxtext.kernels.tokamax_splash_attention import splash_attention_mask as tokamax_splash_mask
34+
from maxtext.utils import max_utils
3335

3436

3537
def is_context_parallel_ring_requested(config: Any) -> bool:
@@ -245,6 +247,11 @@ def build_splash_config(
245247
block_q_dkv = min(config.sa_block_q_dkv, q_seq_len_per_shard)
246248
block_kv_dkv = min(config.sa_block_kv_dkv, kv_seq_len_per_shard)
247249
block_kv_dkv_compute = min(config.sa_block_kv_dkv_compute, kv_seq_len_per_shard)
250+
if config.context_parallel_load_balance and block_q_dkv % tokamax_splash_kernel.NUM_LANES != 0:
251+
raise ValueError(
252+
"TPU Tokamax ring attention with context_parallel_load_balance=True requires "
253+
f"sa_block_q_dkv ({block_q_dkv}) to be a multiple of {tokamax_splash_kernel.NUM_LANES} after clamping."
254+
)
248255
return tokamax_splash_kernel.SplashConfig(
249256
block_q=block_q,
250257
block_kv=block_kv,
@@ -270,11 +277,18 @@ def build_splash_config(
270277
)
271278

272279

273-
def _make_causal_mask(shape: tuple[int, int], context_parallel_size: int):
280+
def _make_causal_mask(shape: tuple[int, int], context_parallel_size: int, *, load_balanced: bool = False):
274281
"""Builds a lazy causal mask for ring attention."""
275282
if context_parallel_size <= 1:
276283
raise ValueError("context_parallel_size must be > 1 for ring attention.")
277-
return tokamax_splash_mask.CausalMask(shape=shape, shard_count=context_parallel_size)
284+
mask = tokamax_splash_mask.CausalMask(shape=shape, shard_count=context_parallel_size)
285+
if load_balanced:
286+
sequence_indices = max_utils.reorder_mask_load_balancing(
287+
np.arange(shape[0], dtype=np.int32), context_parallel_size, 0
288+
)
289+
mask.q_sequence = sequence_indices
290+
mask.kv_sequence = sequence_indices
291+
return mask
278292

279293

280294
def make_sharded_ring_attention_kernel(
@@ -301,6 +315,7 @@ def make_sharded_ring_attention_kernel(
301315
mask = _make_causal_mask(
302316
(query.shape[2], key.shape[2]),
303317
context_parallel_size,
318+
load_balanced=config.context_parallel_load_balance,
304319
)
305320

306321
@functools.partial(jax.jit, static_argnames=["single_head_mask"])

src/maxtext/kernels/tokamax_splash_attention/ring_attention_kernel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ def mask_info_spec(mask_info):
604604
if mask_info.partial_mask_blocks is not None
605605
else None,
606606
q_sequence=_resolve_spec(mask_info.q_sequence),
607+
kv_sequence=jax.sharding.PartitionSpec() if mask_info.kv_sequence is not None else None,
607608
)
608609

609610
return RingSplashAttentionKernel(

src/maxtext/kernels/tokamax_splash_attention/ring_attention_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ def slice_if_exists(arr: jax.Array | None):
4141
block_mask=slice_if_exists(mask_info.block_mask),
4242
partial_mask_blocks=mask_info.partial_mask_blocks, # partial mask blocks are global
4343
q_sequence=mask_info.q_sequence, # Q sequence stays stationary
44+
kv_sequence=slice_if_exists(mask_info.kv_sequence),
4445
)
4546

4647

4748
def offset_q_sequence_for_kv_shard(mask_info: MaskInfo, kv_shard_idx: jax.Array, kv_seq_len: int) -> MaskInfo:
4849
"""Converts lazy mask Q ids to the current local KV coordinate frame."""
4950
if mask_info.q_sequence is None:
5051
return mask_info
52+
if mask_info.kv_sequence is not None:
53+
return mask_info
5154

5255
kv_shard_offset = jnp.asarray(kv_shard_idx, dtype=mask_info.q_sequence.dtype) * kv_seq_len
5356
return mask_info._replace(q_sequence=mask_info.q_sequence - kv_shard_offset)

src/maxtext/kernels/tokamax_splash_attention/splash_attention_kernel.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def _apply_mask_and_soft_cap(
194194
mask_value: float,
195195
mask_ref,
196196
q_sequence_ref,
197+
kv_sequence_ref,
197198
q_segment_ids_ref,
198199
kv_segment_ids_ref,
199200
*,
@@ -207,30 +208,38 @@ def _apply_mask_and_soft_cap(
207208
) -> tuple[jax.Array, jax.Array | None]:
208209
assert mask_ref is None or q_sequence_ref is None
209210
assert (q_sequence_ref is None) == (mask_function is None)
211+
assert kv_sequence_ref is None or mask_function is not None
210212

211213
masks = []
212214
if has_partial_mask:
213215
if mask_ref is not None:
214216
mask = mask_ref[:, k_slice] if k_in_lanes else mask_ref[k_slice, :]
215217
masks.append(mask)
216218
elif mask_function is not None:
217-
# Compute the mask using the given q_sequence indices.
218-
# KV indices are computed on the fly. This works because we only support Q
219-
# sequence sharding. If we wanted to compute Q indices too, then we would
220-
# need to keep into account the current shard along Q sequence.
219+
# Compute the mask using original Q positions. K/V positions are computed
220+
# on the fly unless explicit original K/V positions are provided.
221221

222222
if k_in_lanes:
223223
assert q_sequence_ref.shape == (bq, NUM_LANES)
224224

225-
k_sequence = k_offset + jax.lax.broadcasted_iota(jnp.int32, (bq, k_slice.size), 1)
225+
if kv_sequence_ref is None:
226+
k_sequence = k_offset + jax.lax.broadcasted_iota(jnp.int32, (bq, k_slice.size), 1)
227+
else:
228+
k_sequence = jnp.broadcast_to(kv_sequence_ref[:1, k_slice], (bq, k_slice.size))
226229

227230
repeats, rem = divmod(k_slice.size, NUM_LANES)
228231
assert rem == 0
229232
q_sequence = jnp.tile(q_sequence_ref[...], (1, repeats)) # [bq, k_slice.size]
230233
else:
231234
assert q_sequence_ref.shape == (NUM_SUBLANES, bq)
232235

233-
k_sequence = k_offset + jax.lax.broadcasted_iota(jnp.int32, (k_slice.size, bq), 0)
236+
if kv_sequence_ref is None:
237+
k_sequence = k_offset + jax.lax.broadcasted_iota(jnp.int32, (k_slice.size, bq), 0)
238+
else:
239+
repeats, rem = divmod(bq, NUM_LANES)
240+
if rem:
241+
raise NotImplementedError(f"block_q must be a multiple of {NUM_LANES}")
242+
k_sequence = jnp.tile(kv_sequence_ref[k_slice, :], (1, repeats))
234243
q_sequence = q_sequence_ref[:1, :] # [1, bq]
235244
q_sequence = jnp.broadcast_to(q_sequence, (k_slice.size, bq))
236245

@@ -290,6 +299,7 @@ def flash_attention_kernel(
290299
sinks_ref,
291300
mask_ref,
292301
q_sequence_ref,
302+
kv_sequence_ref,
293303
max_logit_value_ref,
294304
# Outputs
295305
o_ref,
@@ -394,6 +404,7 @@ def body(kv_compute_index, _, has_partial_mask=False):
394404
mask_value,
395405
mask_ref,
396406
q_sequence_ref,
407+
kv_sequence_ref,
397408
q_segment_ids_ref,
398409
kv_segment_ids_ref,
399410
attn_logits_soft_cap=attn_logits_soft_cap,
@@ -669,6 +680,13 @@ def mask_index_map(h, grid_idx, rows_ref, cols_ref, mask_next_ref=None, *_):
669680
q_sequence = None
670681
in_specs.append(None)
671682

683+
if mask_info.kv_sequence is not None:
684+
kv_sequence = jax.lax.broadcast_in_dim(mask_info.kv_sequence, (NUM_SUBLANES, kv_seq_len), (1,))
685+
in_specs.append(pl.BlockSpec((NUM_SUBLANES, bkv), kv_segment_ids_index_map))
686+
else:
687+
kv_sequence = None
688+
in_specs.append(None)
689+
672690
if max_logit_value is not None:
673691
# reshape to allow sublane selection for vmap-ping and shard_map-ping
674692
max_logit_value = jnp.broadcast_to(
@@ -819,6 +837,7 @@ def _fwd_cost_estimate(
819837
sinks,
820838
mask_info.partial_mask_blocks,
821839
q_sequence,
840+
kv_sequence,
822841
max_logit_value,
823842
)
824843
out, logsumexp, l_linear, max_logits = all_out
@@ -996,6 +1015,7 @@ def _flash_attention_dq_kernel(
9961015
di_ref,
9971016
mask_ref,
9981017
q_sequence_ref,
1018+
kv_sequence_ref,
9991019
# Outputs
10001020
dq_scratch_ref,
10011021
dq_ref,
@@ -1049,6 +1069,7 @@ def body(has_partial_mask: bool = False):
10491069
mask_value,
10501070
mask_ref,
10511071
q_sequence_ref,
1072+
kv_sequence_ref,
10521073
q_segment_ids_ref,
10531074
kv_segment_ids_ref,
10541075
attn_logits_soft_cap=attn_logits_soft_cap,
@@ -1115,6 +1136,7 @@ def _flash_attention_dkv_kernel(
11151136
di_ref,
11161137
mask_ref,
11171138
q_sequence_ref,
1139+
kv_sequence_ref,
11181140
# aliases
11191141
dq_alias,
11201142
dk_alias,
@@ -1212,6 +1234,7 @@ def _load_kv(ref, layout):
12121234
mask_value,
12131235
mask_ref,
12141236
q_sequence_ref,
1237+
kv_sequence_ref,
12151238
q_segment_ids_ref,
12161239
kv_segment_ids_ref,
12171240
attn_logits_soft_cap=attn_logits_soft_cap,
@@ -1436,9 +1459,8 @@ def create_dkv_index_map(h, i, j, *_):
14361459
mask_spec = pl.BlockSpec((None, bkv, bq), mask_index_map)
14371460

14381461
q_segment_ids_index_map = unravel(lambda h, i, j: (0, i))
1462+
kv_segment_ids_index_map = unravel(lambda h, i, j: (j, 0))
14391463
if segment_ids is not None:
1440-
kv_segment_ids_index_map = unravel(lambda h, i, j: (j, 0))
1441-
14421464
q_segment_spec = pl.BlockSpec((NUM_SUBLANES, bq), q_segment_ids_index_map)
14431465
kv_segment_spec = pl.BlockSpec((bkv, NUM_LANES), kv_segment_ids_index_map)
14441466
q_segment_ids = jax.lax.broadcast_in_dim(segment_ids.q, (NUM_SUBLANES, q_seq_len), (1,))
@@ -1485,6 +1507,13 @@ def create_dkv_index_map(h, i, j, *_):
14851507
q_sequence = None
14861508
in_specs.append(None)
14871509

1510+
if mask_info.kv_sequence is not None:
1511+
in_specs.append(pl.BlockSpec((bkv, NUM_LANES), kv_segment_ids_index_map))
1512+
kv_sequence = jax.lax.broadcast_in_dim(mask_info.kv_sequence, (kv_seq_len, NUM_LANES), (0,))
1513+
else:
1514+
kv_sequence = None
1515+
in_specs.append(None)
1516+
14881517
dq_reduction_steps = config.dq_reduction_steps
14891518
if not dynamic_grid and kv_steps <= 3 and dq_reduction_steps == 3:
14901519
dq_reduction_steps = None
@@ -1581,6 +1610,7 @@ def create_dkv_index_map(h, i, j, *_):
15811610
di,
15821611
mask_info.partial_mask_blocks,
15831612
q_sequence,
1613+
kv_sequence,
15841614
]
15851615
num_args = sum(1 for x in args if x is not None)
15861616
input_output_aliases = {}
@@ -1609,6 +1639,7 @@ def _bwd_cost_estimate(
16091639
di: jax.Array,
16101640
partial_mask_blocks: jax.Array | None,
16111641
q_sequence: jax.Array | None,
1642+
kv_sequence: jax.Array | None,
16121643
out_shapes: list[jax.ShapeDtypeStruct],
16131644
mask_sparsity_factor: float,
16141645
) -> pl.CostEstimate:
@@ -1643,6 +1674,7 @@ def _bwd_cost_estimate(
16431674
di,
16441675
partial_mask_blocks,
16451676
q_sequence,
1677+
kv_sequence,
16461678
]
16471679
input_bytes = sum(map(_bytes, inputs_))
16481680
output_bytes = sum(map(_bytes, out_shapes))
@@ -1666,6 +1698,7 @@ def _bwd_cost_estimate(
16661698
di,
16671699
mask_info.partial_mask_blocks,
16681700
q_sequence,
1701+
kv_sequence,
16691702
out_shapes,
16701703
dkv_mask_sparsity,
16711704
)
@@ -1880,6 +1913,7 @@ def mask_info_spec(mask_info):
18801913
if mask_info.partial_mask_blocks is not None
18811914
else None,
18821915
q_sequence=_resolve_spec(mask_info.q_sequence),
1916+
kv_sequence=(jax.sharding.PartitionSpec() if mask_info.kv_sequence is not None else None),
18831917
)
18841918

18851919
return SplashAttentionKernel(
@@ -2029,6 +2063,7 @@ def process_mask_shard(mask):
20292063
block_mask=mask_spec,
20302064
partial_mask_blocks=mask_spec,
20312065
q_sequence=None,
2066+
kv_sequence=None,
20322067
)
20332068
out_specs = (
20342069
mask_info_specs,

src/maxtext/kernels/tokamax_splash_attention/splash_attention_mask.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,15 @@ class _ComputableMask(Mask):
183183
to undefined softmax.
184184
q_sequence: Indices of Q sequence. q_sequence is reused across __getitem__
185185
calls which is important for compile-time performance.
186+
kv_sequence: Optional indices of KV sequence. When unset, KV positions are
187+
the physical column indices.
186188
mask_function: Function used by the SplashAttention kernel to compute the
187189
mask rather than loading it.
188190
"""
189191

190192
_shape: tuple[int, int]
191193
q_sequence: np.ndarray
194+
kv_sequence: np.ndarray | None
192195
mask_function: Callable[..., Any]
193196

194197
def __init__(
@@ -207,6 +210,7 @@ def __init__(
207210
)
208211

209212
self.q_sequence = np.arange(q_seq_len, dtype=np.int32)
213+
self.kv_sequence = None
210214

211215
@property
212216
def shape(self) -> tuple[int, ...]:
@@ -224,7 +228,10 @@ def __getitem__(self, idx) -> np.ndarray:
224228
kv_slice = _fill_slice(kv_slice, self.shape[1])
225229

226230
rows = self.q_sequence[q_slice]
227-
cols = np.arange(kv_slice.start, kv_slice.stop)
231+
if self.kv_sequence is None:
232+
cols = np.arange(kv_slice.start, kv_slice.stop)
233+
else:
234+
cols = self.kv_sequence[kv_slice]
228235

229236
return self.mask_function(rows[:, None], cols[None, :])
230237

@@ -276,7 +283,12 @@ def __eq__(self, other: object):
276283
if not isinstance(other, type(self)):
277284
return NotImplemented
278285

279-
return self.shape == other.shape and self.offset == other.offset and np.array_equal(self.q_sequence, other.q_sequence)
286+
return (
287+
self.shape == other.shape
288+
and self.offset == other.offset
289+
and np.array_equal(self.q_sequence, other.q_sequence)
290+
and np.array_equal(self.kv_sequence, other.kv_sequence)
291+
)
280292

281293
def __hash__(self):
282294
return hash(
@@ -285,6 +297,7 @@ def __hash__(self):
285297
self.shape,
286298
self.offset,
287299
self.q_sequence.tobytes() if self.q_sequence is not None else None,
300+
self.kv_sequence.tobytes() if self.kv_sequence is not None else None,
288301
)
289302
)
290303

0 commit comments

Comments
 (0)