Skip to content

Commit b46d2af

Browse files
Merge pull request #4469 from AI-Hypercomputer:zjiahao/DSA3.2-optimize-reorder-sequence
PiperOrigin-RevId: 949268432
2 parents 11085e1 + 6ef78e9 commit b46d2af

2 files changed

Lines changed: 138 additions & 32 deletions

File tree

src/maxtext/utils/max_utils.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -871,46 +871,43 @@ def reorder_sequence(tensor, cp_size: int, seq_dim: int = 1, to_contiguous: bool
871871
if seq_len % (cp_size * 2) != 0:
872872
raise ValueError(f"{tensor.shape=} is not a multiple of {cp_size*2=}")
873873

874-
# [B, S, H, D]: [B, 2*cp_size, S/2*cp_size, H, D] -> [B, 2, S/2*cp_size, H, D]
875-
# [S, B, H, D]: [2*cp_size, S/2*cp_size, B, H, D] -> [2, S/2*cp_size, B, H, D]
874+
seq_dim = seq_dim % tensor.ndim
876875
ori_tensor_shape = tensor.shape
876+
877+
# Generic transformation: Isolates the target sequence dimension into `2 * cp_size` discrete chunks.
878+
# Note: The shape walkthrough below uses [b, s, h, d] with seq_dim=1 as an illustrative example,
879+
# but actual dimensions depend on the input tensor (e.g., [s, b, h, d], [b, t, d], etc.):
880+
# [b, s, h, d] -> [b, 2*cp_size, group_size, h, d]
877881
reshaped = tensor.reshape(
878882
*ori_tensor_shape[:seq_dim],
879883
2 * cp_size,
880884
group_size,
881885
*ori_tensor_shape[seq_dim + 1 :],
882886
)
883887

884-
if not to_contiguous:
885-
# Create first and second halves
886-
first_half = jnp.arange(cp_size)
887-
second_half = jnp.arange(2 * cp_size - 1, cp_size - 1, -1)
888-
889-
# Stack and reshape to interleave
890-
src_indices = jnp.stack([first_half, second_half], axis=1).reshape(-1)
888+
# Swap target seq_dim with axis 0 to perform slicing/concat easily:
889+
# e.g., [b, 2*cp_size, group_size, h, d] -> [2*cp_size, b, group_size, h, d]
890+
swapped = jnp.swapaxes(reshaped, 0, seq_dim)
891891

892+
if not to_contiguous:
893+
# Split along axis 0 into halves: each [cp_size, b, group_size, h, d]
894+
first_half, second_half = jnp.split(swapped, 2, axis=0)
895+
second_half_reversed = second_half[::-1, ...]
896+
# Stack along axis 1 to interleave: [cp_size, 2, b, group_size, h, d]
897+
stacked = jnp.stack([first_half, second_half_reversed], axis=1)
898+
# Reshape squashes the [cp_size, 2] pair dimensions back into [2*cp_size]: [2*cp_size, b, group_size, h, d]
899+
permuted = stacked.reshape(2 * cp_size, *swapped.shape[1:])
892900
else:
893-
894-
half = cp_size // 2
895-
896-
# Build the 1st and 2nd groups of contiguous‑pair indices:
897-
first_pair = [4 * r for r in range(half)] # [0, 4, 8, …]
898-
second_pair = [4 * r + 2 for r in range(half)] # [2, 6, 10, …]
899-
third_pair = [2 * cp_size - 1 - 4 * r for r in range(half)] # [2*cp_size-1, 2*cp_size-5, …]
900-
fourth_pair = [i - 2 for i in third_pair] # [2*cp_size-3, 2*cp_size-7, …]
901-
902-
# Concatenate so each rank’s two indices sit next to each other:
903-
# e.g. [0,2, 4,6, …, (2cp‑1),(2cp‑3), …]
904-
first_block = first_pair + third_pair
905-
second_block = second_pair + fourth_pair
906-
907-
# Stack into shape (2*cp_size//2, 2) → then flatten → length=2*cp_size
908-
src_indices = jnp.stack([jnp.array(first_block), jnp.array(second_block)], axis=1).reshape(-1)
909-
910-
# One gather and one reshape
911-
reordered = jnp.take(reshaped, src_indices, axis=seq_dim)
912-
913-
# Reshape back to original dimensions
901+
# Strided slice extracts every other chunk natively: each [cp_size, b, group_size, h, d]
902+
first_half = swapped[0::2, ...]
903+
second_half_reversed = swapped[1::2, ...]
904+
second_half = second_half_reversed[::-1, ...]
905+
# Concatenate along axis 0: [2*cp_size, b, group_size, h, d]
906+
permuted = jnp.concatenate([first_half, second_half], axis=0)
907+
908+
# Swap axis 0 back to seq_dim: e.g., [b, 2*cp_size, group_size, h, d]
909+
reordered = jnp.swapaxes(permuted, 0, seq_dim)
910+
# Restore original tensor shape: e.g., [b, s, h, d]
914911
return reordered.reshape(ori_tensor_shape)
915912

916913

@@ -1245,5 +1242,9 @@ def maybe_pad(inputs, tile_size):
12451242
padding_amount = 0
12461243
if inputs_dim % tile_size:
12471244
padding_amount = tile_size - inputs_dim % tile_size
1248-
inputs = jax.lax.pad(inputs, jnp.array(0.0, dtype=inputs.dtype), [(0, padding_amount, 0), (0, 0, 0)])
1245+
inputs = jax.lax.pad(
1246+
inputs,
1247+
jnp.array(0.0, dtype=inputs.dtype),
1248+
[(0, padding_amount, 0), (0, 0, 0)],
1249+
)
12491250
return inputs, padding_amount

tests/unit/max_utils_test.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,9 @@ def test_tpu_checkpointing_no_emergency_calls_jax_init(self, mock_init):
539539
@mock.patch("maxtext.utils.max_utils.initialize_jax_for_tpu_with_emergency_checkpointing")
540540
def test_tpu_checkpointing_with_emergency(self, mock_tpu_emergency):
541541
raw_keys = self._base_keys(
542-
enable_checkpointing=True, compile_topology_num_slices=-1, enable_emergency_checkpoint=True
542+
enable_checkpointing=True,
543+
compile_topology_num_slices=-1,
544+
enable_emergency_checkpoint=True,
543545
)
544546
max_utils.maybe_initialize_jax_distributed_system(raw_keys)
545547
mock_tpu_emergency.assert_called_once_with(raw_keys)
@@ -551,5 +553,108 @@ def test_tpu_no_checkpointing_does_not_call_jax_init(self, mock_init):
551553
mock_init.assert_not_called()
552554

553555

556+
class TestReorderSequence(unittest.TestCase):
557+
"""Tests for reorder_sequence optimization in max_utils.py"""
558+
559+
def _old_reorder_sequence(self, tensor, cp_size: int, seq_dim: int = 1, to_contiguous: bool = False):
560+
"""Original gather-based reorder_sequence implementation used as a reference."""
561+
if tensor is None:
562+
return tensor
563+
564+
seq_len = tensor.shape[seq_dim]
565+
group_size = seq_len // (2 * cp_size)
566+
567+
if cp_size % 2 != 0:
568+
raise ValueError(f"{cp_size=} must be a multiple of 2.")
569+
570+
if seq_len % (cp_size * 2) != 0:
571+
raise ValueError(f"{tensor.shape=} is not a multiple of {cp_size*2=}")
572+
573+
seq_dim = seq_dim % tensor.ndim
574+
ori_tensor_shape = tensor.shape
575+
reshaped = tensor.reshape(
576+
*ori_tensor_shape[:seq_dim],
577+
2 * cp_size,
578+
group_size,
579+
*ori_tensor_shape[seq_dim + 1 :],
580+
)
581+
582+
if not to_contiguous:
583+
first_half = jnp.arange(cp_size)
584+
second_half = jnp.arange(2 * cp_size - 1, cp_size - 1, -1)
585+
src_indices = jnp.stack([first_half, second_half], axis=1).reshape(-1)
586+
else:
587+
half = cp_size // 2
588+
first_pair = [4 * r for r in range(half)]
589+
second_pair = [4 * r + 2 for r in range(half)]
590+
third_pair = [2 * cp_size - 1 - 4 * r for r in range(half)]
591+
fourth_pair = [i - 2 for i in third_pair]
592+
first_block = first_pair + third_pair
593+
second_block = second_pair + fourth_pair
594+
src_indices = jnp.stack([jnp.array(first_block), jnp.array(second_block)], axis=1).reshape(-1)
595+
596+
reordered = jnp.take(reshaped, src_indices, axis=seq_dim)
597+
return reordered.reshape(ori_tensor_shape)
598+
599+
def test_reorder_equivalence_sweep(self):
600+
key = random.key(42)
601+
602+
# Sweep configurations
603+
cp_sizes = [2, 4, 8]
604+
to_contiguous_options = [False, True]
605+
606+
# Test cases: (shape, seq_dim)
607+
test_cases = [
608+
((64,), 0), # 1D
609+
((2, 128, 4, 8), 1), # 4D (standard B, S, H, D)
610+
((256, 2, 4, 8), 0), # 4D (S, B, H, D)
611+
((2, 4, 8, 128), -1), # Negative seq_dim (last dimension)
612+
((2, 128, 4, 8), -3), # Negative seq_dim (-3 corresponding to S)
613+
]
614+
615+
for cp_size in cp_sizes:
616+
for to_contiguous in to_contiguous_options:
617+
for shape, seq_dim in test_cases:
618+
with self.subTest(
619+
cp_size=cp_size,
620+
to_contiguous=to_contiguous,
621+
shape=shape,
622+
seq_dim=seq_dim,
623+
):
624+
# Generate random input
625+
x = random.normal(key, shape)
626+
627+
# Run old (reference) implementation
628+
ref_out = self._old_reorder_sequence(x, cp_size, seq_dim, to_contiguous)
629+
630+
# Run new (optimized) implementation
631+
opt_out = max_utils.reorder_sequence(x, cp_size, seq_dim, to_contiguous)
632+
633+
# Assert mathematical equivalence
634+
self.assertTrue(
635+
jnp.allclose(ref_out, opt_out, rtol=1e-5, atol=1e-6),
636+
msg=f"Failed for cp_size={cp_size}, to_contiguous={to_contiguous}, shape={shape}, seq_dim={seq_dim}",
637+
)
638+
639+
def test_reorder_roundtrip(self):
640+
# If we reorder to load-balanced (to_contiguous=False) and then restore (to_contiguous=True),
641+
# we should get back the exact original tensor. Also tests negative seq_dim.
642+
key = random.key(123)
643+
shape = (2, 128, 4, 8)
644+
seq_dim = -3
645+
cp_size = 4
646+
647+
x = random.normal(key, shape)
648+
649+
# 1. Reorder to load-balanced
650+
balanced = max_utils.reorder_sequence(x, cp_size, seq_dim, to_contiguous=False)
651+
652+
# 2. Restore to contiguous
653+
restored = max_utils.reorder_sequence(balanced, cp_size, seq_dim, to_contiguous=True)
654+
655+
# 3. Assert roundtrip is lossless
656+
self.assertTrue(jnp.allclose(x, restored, rtol=1e-5, atol=1e-6))
657+
658+
554659
if __name__ == "__main__":
555660
unittest.main()

0 commit comments

Comments
 (0)