Skip to content

Commit 021970c

Browse files
committed
Fix the seq-first Ulysses all2all output layout
_generate_layout_params builds the reshape target for every all2all in DistributedAttention. For batch_dim_idx=1 (s, b, n, h) with scatter_idx < 2 it returns [bs, seq_world_size * global_seq_len, num_local_head // seq_world_size, head_dim], which is the batch_dim_idx=0 / scatter_idx >= 2 shape: it puts the batch first, multiplies the sequence and divides the heads, when this direction scatters the sequence and gathers the heads. Before #6750 extracted this function, post_all2all computed [seq_len // seq_world_size, bs, seq_world_size * num_head, head_dim] for that case, so the refactor copied the wrong sibling branch. Restore that shape. The element count still matches whenever num_local_head is divisible by seq_world_size, so the reshape succeeds and silently returns a transposed, mis-strided tensor; when it is not divisible, the floor division makes a dimension 0 and the reshape raises. Both are reachable from DistributedAttention, whose default gather_idx is 0: the output projection all2all and the backward of the q/k/v all2alls both run scatter_idx < 2. The existing coverage misses it. TestUlyssesAll2All only runs batch_dim_idx=0, and TestUlyssesAll2All_odd sets num_kv_heads on its first call so every later call takes uneven_heads_all2all instead of _generate_layout_params. _generate_layout_params is pure, so add TestUlyssesAll2AllLayout, which drives it with an emulated all_to_all_single and checks that both directions land the right (sequence, head) shard of a known tensor. It needs no process group and no accelerator, so it runs in the CPU CI. Against the current code the two batch_dim_idx=1 head-to-sequence cases fail (2 failed, 6 passed: one shape assertion, one reshape RuntimeError) and all 8 pass with the fix.
1 parent 7154a00 commit 021970c

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

deepspeed/sequence/layer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ def _generate_layout_params(scatter_idx, batch_dim_idx, seq_world_size, input):
5050
pre_all2all_permute_idx = None
5151

5252
post_all2all_permute_idx = (1, 2, 0, 3, 4)
53-
post_all2all_res_shape = [bs, seq_world_size * global_seq_len, num_local_head // seq_world_size, head_dim]
53+
# seq-first layout: the all2all scatters the sequence and gathers the heads, so the
54+
# result keeps bs in dim 1 with a local sequence and every rank's heads.
55+
post_all2all_res_shape = [global_seq_len // seq_world_size, bs, seq_world_size * num_local_head, head_dim]
5456
else:
5557
local_seq_len, bs, num_total_head, head_dim = input.shape
5658
assert num_total_head % seq_world_size == 0, f"Number of heads ({num_total_head}) must be divisible by the sequence parallel size ({seq_world_size})!"

tests/unit/sequence_parallelism/test_ulysses.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import deepspeed.runtime.sequence_parallel.parallel_state_sp as sp_mpu
1212
from transformers import AutoModel
1313
from unit.common import DistributedTest
14-
from deepspeed.sequence.layer import _SeqAllToAll
14+
from deepspeed.sequence.layer import _SeqAllToAll, _generate_layout_params, post_all2all, pre_all2all_fun
1515
from deepspeed.sequence.fpdt_layer import _FPDTGPUOffloadingAttentionImpl_, FPDT_InputConstruct
1616
from unit.util import skip_on_arch
1717
from unit.simple_model import *
@@ -148,6 +148,63 @@ def test_alltoall_output_consistency(self, d0: int, d1: int, head_dim: int, num_
148148
assert torch.allclose(input_tensor, outputs[i]), f"Outputs differ for sequence dim {seq_dims[i]}"
149149

150150

151+
def _emulate_all_to_all(shards):
152+
"""CPU stand-in for dist.all_to_all_single: rank i sends chunk j of dim 0 to rank j."""
153+
seq_world_size = len(shards)
154+
return [
155+
torch.cat([shards[src][dst:dst + 1] for src in range(seq_world_size)], dim=0) for dst in range(seq_world_size)
156+
]
157+
158+
159+
def _run_layout_all_to_all(scatter_idx, batch_dim_idx, seq_world_size, shards):
160+
"""single_all_to_all's layout math, driven by the emulated all2all above."""
161+
pre_permute_idx, pre_inp_shape, post_permute_idx, post_res_shape = _generate_layout_params(
162+
scatter_idx, batch_dim_idx, seq_world_size, shards[0])
163+
sent = [pre_all2all_fun(pre_permute_idx, pre_inp_shape, shard) for shard in shards]
164+
post_fun = post_all2all(post_permute_idx, post_res_shape)
165+
return [post_fun(received) for received in _emulate_all_to_all(sent)]
166+
167+
168+
@pytest.mark.parametrize("batch_dim_idx", [0, 1])
169+
@pytest.mark.parametrize("seq_world_size", [2, 4])
170+
class TestUlyssesAll2AllLayout:
171+
"""_generate_layout_params is a pure function, so the shapes it hands to reshape can be
172+
checked on CPU without a process group. TestUlyssesAll2All above only runs batch_dim_idx=0
173+
and TestUlyssesAll2All_odd takes the uneven-head path, so the seq-first (s, b, n, h) layout
174+
is otherwise never exercised."""
175+
176+
def _shards(self, batch_dim_idx, seq_world_size):
177+
local_seq_len, bs, local_num_heads, head_dim = 3, 2, 2, 4
178+
seq_len = local_seq_len * seq_world_size
179+
num_heads = local_num_heads * seq_world_size
180+
seq_dim = 1 if batch_dim_idx == 0 else 0
181+
full = torch.arange(seq_len * bs * num_heads * head_dim, dtype=torch.float32)
182+
full = full.reshape(seq_len, bs, num_heads, head_dim)
183+
if batch_dim_idx == 0:
184+
full = full.transpose(0, 1).contiguous()
185+
# sequence parallel: every head, a slice of the sequence.
186+
seq_parallel = [
187+
full.narrow(seq_dim, r * local_seq_len, local_seq_len).contiguous() for r in range(seq_world_size)
188+
]
189+
# head parallel: every position, a slice of the heads.
190+
head_parallel = [
191+
full.narrow(2, r * local_num_heads, local_num_heads).contiguous() for r in range(seq_world_size)
192+
]
193+
return seq_dim, seq_parallel, head_parallel
194+
195+
def test_seq_to_head_parallel(self, batch_dim_idx, seq_world_size):
196+
_, seq_parallel, head_parallel = self._shards(batch_dim_idx, seq_world_size)
197+
got = _run_layout_all_to_all(2, batch_dim_idx, seq_world_size, seq_parallel)
198+
for rank, (actual, expected) in enumerate(zip(got, head_parallel)):
199+
assert torch.equal(actual, expected), f"rank {rank} got {actual.shape}, expected {expected.shape}"
200+
201+
def test_head_to_seq_parallel(self, batch_dim_idx, seq_world_size):
202+
seq_dim, seq_parallel, head_parallel = self._shards(batch_dim_idx, seq_world_size)
203+
got = _run_layout_all_to_all(seq_dim, batch_dim_idx, seq_world_size, head_parallel)
204+
for rank, (actual, expected) in enumerate(zip(got, seq_parallel)):
205+
assert torch.equal(actual, expected), f"rank {rank} got {actual.shape}, expected {expected.shape}"
206+
207+
151208
@pytest.mark.parametrize("d0", [2, 4]) #batch or sequence dimension
152209
@pytest.mark.parametrize("d1", [4, 8]) #batch or sequence dimension
153210
@pytest.mark.parametrize("num_heads", [3, 7])

0 commit comments

Comments
 (0)