[diffusion] fix: make the sp sequence gather pass contiguous shards (#39291)

Co-authored-by: Mick Qian <mickqian@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mick
2026-09-13 20:30:52 +08:00
committed by GitHub
co-authored by Mick Qian Claude Opus 5
parent 9ffe548738
commit f9fca05803
2 changed files with 99 additions and 2 deletions
@@ -1487,15 +1487,19 @@ class USPAttention(nn.Module):
"Replicated prefix and suffix cannot be used at the same time."
)
# slicing along dim 1 keeps the original row stride, so the shard is
# contiguous only while the batch dim is 1; all_gather_into_tensor
# requires a contiguous input, so materialize the shard before the
# collective
if num_replicated_prefix:
replicated = tensor[:, :num_replicated_prefix]
sharded = tensor[:, num_replicated_prefix:]
sharded = tensor[:, num_replicated_prefix:].contiguous()
gathered = sequence_model_parallel_all_gather(sharded, dim=1)
return torch.cat([replicated, gathered], dim=1)
if num_replicated_suffix:
replicated = tensor[:, -num_replicated_suffix:]
sharded = tensor[:, :-num_replicated_suffix]
sharded = tensor[:, :-num_replicated_suffix].contiguous()
gathered = sequence_model_parallel_all_gather(sharded, dim=1)
return torch.cat([gathered, replicated], dim=1)
@@ -0,0 +1,93 @@
"""Regression test for ``USPAttention._gather_sharded_sequence`` contiguity.
The helper slices a replicated prefix/suffix off dim 1 and all-gathers the rest.
Slicing along dim 1 keeps the original row stride, so the shard is contiguous
only while the batch dim is 1 -- PyTorch ignores strides for size-1 dims. With a
batched CFG pass (B=2) both branches hand a non-contiguous view to
``all_gather_into_tensor``, which rejects it with "Tensors must be contiguous".
That is reachable from a default configuration: FLUX.2 on 2 GPUs auto-selects
``ulysses_degree=2`` and batches its two CFG branches, so the server fails during
warmup.
Single-process test: the all-gather is mocked so the slicing runs on CPU.
"""
import unittest
from unittest.mock import patch
import torch
from sglang.multimodal_gen.runtime.layers.attention.layer import USPAttention
_LAYER = "sglang.multimodal_gen.runtime.layers.attention.layer"
_SP = 2
class _CaptureAllGather:
"""Stand-in all-gather that records whether its input was contiguous."""
def __init__(self):
self.inputs = []
def __call__(self, tensor, dim=-1):
self.inputs.append(tensor)
return torch.cat([tensor] * _SP, dim=dim)
class TestGatherShardedSequenceContiguous(unittest.TestCase):
def _run(self, tensor, **kwargs):
capture = _CaptureAllGather()
with patch(f"{_LAYER}.sequence_model_parallel_all_gather", capture):
out = USPAttention._gather_sharded_sequence(tensor, **kwargs)
self.assertEqual(len(capture.inputs), 1)
return out, capture.inputs[0]
def test_prefix_shard_is_contiguous_when_batched(self):
tensor = torch.randn(2, 16, 8)
_, gathered_input = self._run(tensor, num_replicated_prefix=4)
self.assertTrue(gathered_input.is_contiguous())
def test_suffix_shard_is_contiguous_when_batched(self):
tensor = torch.randn(2, 16, 8)
_, gathered_input = self._run(tensor, num_replicated_suffix=4)
self.assertTrue(gathered_input.is_contiguous())
def test_batch_one_still_contiguous(self):
# the case that happened to work before, and must keep working
tensor = torch.randn(1, 16, 8)
for kwargs in ({"num_replicated_prefix": 4}, {"num_replicated_suffix": 4}):
with self.subTest(**kwargs):
_, gathered_input = self._run(tensor, **kwargs)
self.assertTrue(gathered_input.is_contiguous())
def test_prefix_values_are_unchanged(self):
tensor = torch.randn(2, 16, 8)
out, gathered_input = self._run(tensor, num_replicated_prefix=4)
torch.testing.assert_close(gathered_input, tensor[:, 4:])
torch.testing.assert_close(out[:, :4], tensor[:, :4])
self.assertEqual(out.shape, (2, 4 + 12 * _SP, 8))
def test_suffix_values_are_unchanged(self):
tensor = torch.randn(2, 16, 8)
out, gathered_input = self._run(tensor, num_replicated_suffix=4)
torch.testing.assert_close(gathered_input, tensor[:, :-4])
torch.testing.assert_close(out[:, -4:], tensor[:, -4:])
self.assertEqual(out.shape, (2, 12 * _SP + 4, 8))
def test_no_replication_passes_the_tensor_through(self):
tensor = torch.randn(2, 16, 8)
_, gathered_input = self._run(tensor)
self.assertIs(gathered_input, tensor)
def test_prefix_and_suffix_together_are_rejected(self):
with self.assertRaises(ValueError):
USPAttention._gather_sharded_sequence(
torch.randn(2, 16, 8),
num_replicated_prefix=4,
num_replicated_suffix=4,
)
if __name__ == "__main__":
unittest.main()