Skip to content

Commit 989abc6

Browse files
Merge pull request #4364 from huytransformer:htn/load-balanced-cp-chunk-masks
PiperOrigin-RevId: 944548246
2 parents 3dc1efe + 90b0395 commit 989abc6

4 files changed

Lines changed: 239 additions & 39 deletions

File tree

src/maxtext/configs/pyconfig_deprecated.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,11 @@ def validate_keys(keys):
249249
keys["global_rampup_samples"],
250250
)
251251

252-
context_parallel_size = get_context_parallel_size(keys)
253252
# TODO remove after b/435512699 resolved
254-
if context_parallel_size > 1 and keys["context_parallel_load_balance"] and keys["attention_type"] == "chunk":
255-
raise ValueError("Currently load-balanced context parallelism is not supported for chunk attention.")
256253

257-
validate_context_parallel_strategy_ring(context_parallel_size, keys["context_parallel_strategy"], keys["hardware"])
254+
validate_context_parallel_strategy_ring(
255+
keys["context_parallel_size"], keys["context_parallel_strategy"], keys["hardware"]
256+
)
258257

259258
if keys["mtp_eval_target_module"] < 0:
260259
raise ValueError("mtp_eval_target_module cannot be negative. Set to 0 to disable evaluation.")
@@ -816,6 +815,7 @@ def user_init(raw_keys):
816815

817816
raw_keys["num_slices"] = max_utils.get_num_slices(raw_keys)
818817
raw_keys["quantization_local_shard_count"] = get_quantization_local_shard_count(raw_keys)
818+
raw_keys["context_parallel_size"] = get_context_parallel_size(raw_keys)
819819
raw_keys = create_parallelisms_list(raw_keys)
820820
raw_keys = set_and_validate_pipeline_config(raw_keys)
821821

src/maxtext/layers/attention_op.py

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,7 @@ def generate_attention_mask(
615615
previous_chunk: Any = None,
616616
bidirectional_mask: Any = None,
617617
compressed_mask: Optional[Array] = None,
618+
segment_positions: Array | None = None,
618619
) -> Array | None:
619620
"""Generates a combined attention mask for Transformer models.
620621
@@ -675,6 +676,9 @@ def generate_attention_mask(
675676
compressed_mask: Optional `Array`. A pre-computed attention mask for
676677
compressed kv blocks (e.g., DeepSeek-V4 compressed attention). If provided, it is
677678
concatenated with the dynamically generated uncompressed mask.
679+
segment_positions: Optional `Array` of shape `[batch_size,
680+
q_sequence_length]`. Identifies original positions for load-balanced
681+
context-parallel inputs.
678682
679683
Returns:
680684
An `Array` representing the attention mask, with shape
@@ -710,20 +714,33 @@ def generate_attention_mask(
710714
elif model_mode == MODEL_MODE_AUTOREGRESSIVE and q_seq_len == 1:
711715
# In autoregression, the query position is the last position in the KV sequence.
712716
next_pos = kv_seq_len - 1
717+
use_segment_positions = (
718+
segment_positions is not None
719+
and self.config.context_parallel_load_balance
720+
and self.mesh.shape.get(self.config.context_sharding, 1) > 1
721+
and previous_chunk is None
722+
and model_mode != MODEL_MODE_AUTOREGRESSIVE
723+
)
724+
if use_segment_positions:
725+
position_row_ids = segment_positions[:, :, None]
726+
position_col_ids = segment_positions[:, None, :]
713727

714728
causal_mask = None
715729
if model_mode != MODEL_MODE_AUTOREGRESSIVE and self.attention_type not in (
716730
AttentionType.FULL,
717731
AttentionType.COMPRESSED,
718732
):
719-
mask_shape = (q_seq_len, kv_seq_len)
720-
# row_ids indicates the position of query
721-
# col_ids indicates the position of kv
722-
row_ids = jax.lax.broadcasted_iota(jnp.int32, mask_shape, 0)
723-
col_ids = jax.lax.broadcasted_iota(jnp.int32, mask_shape, 1)
724-
# Attention mask for chunked prefill is generated in the same way
725-
# as mentioned in SARATHI - https://arxiv.org/abs/2308.16369
726-
causal_mask = (col_ids <= row_ids + next_pos)[None, None, None, :, :]
733+
if use_segment_positions:
734+
causal_mask = (position_col_ids <= position_row_ids)[:, None, None, :, :]
735+
else:
736+
mask_shape = (q_seq_len, kv_seq_len)
737+
# row_ids indicates the position of query
738+
# col_ids indicates the position of kv
739+
row_ids = jax.lax.broadcasted_iota(jnp.int32, mask_shape, 0)
740+
col_ids = jax.lax.broadcasted_iota(jnp.int32, mask_shape, 1)
741+
# Attention mask for chunked prefill is generated in the same way
742+
# as mentioned in SARATHI - https://arxiv.org/abs/2308.16369
743+
causal_mask = (col_ids <= row_ids + next_pos)[None, None, None, :, :]
727744

728745
output_mask = None
729746
if (mask is not None) and (causal_mask is not None):
@@ -737,11 +754,17 @@ def generate_attention_mask(
737754
if self.sliding_window_size is None:
738755
raise ValueError("Sliding_window_size must be set if Local Sliding attention type")
739756

740-
row_ids_sliding = jax.lax.broadcasted_iota(jnp.int32, (q_seq_len, 1), 0) + next_pos
741-
col_ids_sliding = jax.lax.broadcasted_iota(jnp.int32, (1, kv_seq_len), 1)
757+
if use_segment_positions:
758+
row_ids_sliding = position_row_ids
759+
col_ids_sliding = position_col_ids
760+
else:
761+
row_ids_sliding = jax.lax.broadcasted_iota(jnp.int32, (q_seq_len, 1), 0) + next_pos
762+
col_ids_sliding = jax.lax.broadcasted_iota(jnp.int32, (1, kv_seq_len), 1)
742763
sliding_mask = (col_ids_sliding > (row_ids_sliding - self.sliding_window_size)) & (
743764
col_ids_sliding <= row_ids_sliding
744765
)
766+
if use_segment_positions:
767+
sliding_mask = sliding_mask[:, None, None, :, :]
745768
output_mask = sliding_mask * output_mask
746769
elif self.attention_type == AttentionType.COMPRESSED:
747770
if compressed_mask is None:
@@ -772,12 +795,17 @@ def generate_attention_mask(
772795
return jnp.concatenate([uncompressed_mask, compressed_mask], axis=-1)
773796

774797
elif self.attention_type == AttentionType.CHUNK and output_mask is not None:
775-
mask_shape = (q_seq_len, kv_seq_len)
776-
chunk_mask = _generate_chunk_attention_mask(
777-
mask_shape=(q_seq_len, kv_seq_len),
778-
chunk_size=self.chunk_attn_window_size,
779-
q_offset=next_pos,
780-
)
798+
if use_segment_positions:
799+
same_chunk = (position_row_ids // self.chunk_attn_window_size) == (
800+
position_col_ids // self.chunk_attn_window_size
801+
)
802+
chunk_mask = (same_chunk & (position_row_ids >= position_col_ids))[:, None, None, :, :]
803+
else:
804+
chunk_mask = _generate_chunk_attention_mask(
805+
mask_shape=(q_seq_len, kv_seq_len),
806+
chunk_size=self.chunk_attn_window_size,
807+
q_offset=next_pos,
808+
)
781809
output_mask = chunk_mask * output_mask
782810

783811
if bidirectional_mask is not None:
@@ -976,6 +1004,7 @@ def apply_attention(
9761004
decoder_segment_ids,
9771005
model_mode,
9781006
previous_chunk,
1007+
segment_positions=segment_positions,
9791008
bidirectional_mask=bidirectional_mask,
9801009
sinks=sinks,
9811010
indexer_mask=indexer_mask,
@@ -1301,10 +1330,17 @@ def create_sa_config(config, query, key, attn_logits_soft_cap):
13011330
if self.chunk_attn_window_size is None:
13021331
raise ValueError("chunk_attn_window_size must be set for chunk attention type")
13031332

1304-
mask &= ChunkedCausalMask(
1305-
shape=(query.shape[2], key.shape[2]),
1306-
chunk_size=self.chunk_attn_window_size,
1307-
)
1333+
if use_load_balanced_cp:
1334+
mask &= LoadBalancedChunkedCausalMask(
1335+
shape=(query.shape[2], key.shape[2]),
1336+
chunk_size=self.chunk_attn_window_size,
1337+
cp_size=cp_size,
1338+
)
1339+
else:
1340+
mask &= ChunkedCausalMask(
1341+
shape=(query.shape[2], key.shape[2]),
1342+
chunk_size=self.chunk_attn_window_size,
1343+
)
13081344

13091345
max_logit_value = None
13101346
if self.config.use_tokamax_splash:
@@ -1588,6 +1624,8 @@ def cudnn_flash_attention(
15881624
and self.config.context_parallel_strategy == "ring"
15891625
and self.config.context_parallel_load_balance
15901626
)
1627+
if using_context_parallelism and self.attention_type == AttentionType.CHUNK:
1628+
raise ValueError("Chunk attention is not supported with CUDNN context parallelism.")
15911629

15921630
# Initialize default attention configuration
15931631
sliding_window_size = None
@@ -1815,6 +1853,7 @@ def apply_attention_dot(
18151853
decoder_segment_ids: Array | None,
18161854
model_mode: str = MODEL_MODE_TRAIN,
18171855
previous_chunk: Any = None,
1856+
segment_positions: Array | None = None,
18181857
bidirectional_mask: Any = None,
18191858
sinks: Array | None = None,
18201859
indexer_mask: Array | None = None,
@@ -1880,6 +1919,7 @@ def apply_attention_dot(
18801919
previous_chunk,
18811920
bidirectional_mask,
18821921
compressed_mask=compressed_mask,
1922+
segment_positions=segment_positions,
18831923
)
18841924

18851925
if self.config.moba:
@@ -2254,3 +2294,21 @@ def __init__(
22542294
# LocalMask uses shard_count for mask-shard validation. cp_size is the
22552295
# context-parallel size used for the load-balanced query order.
22562296
self.q_sequence = _load_balanced_q_sequence(shape, cp_size)
2297+
2298+
2299+
class LoadBalancedChunkedCausalMask(ChunkedCausalMask):
2300+
"""Lazy chunked mask with load-balanced query positions."""
2301+
2302+
def __init__(
2303+
self,
2304+
shape: tuple[int, int],
2305+
chunk_size: int,
2306+
cp_size: int,
2307+
shard_count: int = 1,
2308+
):
2309+
super().__init__(
2310+
shape=shape,
2311+
chunk_size=chunk_size,
2312+
shard_count=shard_count,
2313+
)
2314+
self.q_sequence = _load_balanced_q_sequence(shape, cp_size)

0 commit comments

Comments
 (0)