Prhub

#39291 [Diffusion] Make the SP sequence gather pass contiguous shards

原始 PR 作者 mickqian 合并时间 2026-09-13 20:30 文件变更 2 提交数 1 评论 2 代码增减 +99 / -2

执行摘要

修复 SP 序列收集批量时非连续分片崩溃

PR body 指出该崩溃可从默认配置触达:在 2 张 GPU 上非 CFG 并行模型自动选择 ulysses_degree=2 并批量处理两个 CFG 分支,服务在 warmup 阶段即失败。作者强调这不是冷僻路径,而是 Any SP / KV-gather path with a replicated prefix or suffix and batch > 1 都会命中。

值得精读。这是一个小而精准的 bugfix,根因分析透彻,修复方式仅用 .contiguous() 两处微调就消除了默认配置下的启动崩溃,且测试设计具有示范性:用 mock all-gather 在单进程内捕获 contiguity 问题,避免了多卡测试成本。适合作为「PyTorch 张量连续性陷阱 + 集合通信」相关问题的参考案例。

讨论亮点

该 PR 无 review 讨论,但 PR body 提供了详尽的根因分析与验证说明。核心结论:.contiguous() 在已是连续张量时是 no-op,因此 batch=1 既有路径不受影响;新测试在修复前对两个分支均失败,证明测试有效。

实现拆解

  1. 定位问题:python/sglang/multimodal_gen/runtime/layers/attention/layer.py 中的 USPAttention._gather_sharded_sequence 静态方法,在裁剪 replicated prefix/suffix 后直接对切片调用 sequence_model_parallel_all_gather。由于切片保留了原始行 stride,当 batch > 1 时切片不是连续张量,而底层 all_gather_into_tensor 要求连续输入。
  2. 修复:在 prefix 和 suffix 两个分支的切片操作后分别追加 .contiguous(),将切片物化为连续张量后再进入集合通信;不复制场景(无 replicated 部分)保持原样直接透传,避免不必要拷贝。
  3. 测试配套:新增 test/registered/unit/test_sp_gather_sharded_sequence_contiguous.py(位于 python/sglang/multimodal_gen/test/unit/ 下),通过 _CaptureAllGather mock 掉真正的 all-gather,单进程在 CPU 上验证:批量下 prefix/suffix 分片均连续、batch=1 旧路径不回归、数值保持不变、无复制时张量透传、prefix+suffix 同时使用被拒绝。
  4. CI 验证:PR Test 与 PR Test (Extra) 均通过;AMD ROCm 10 CI 失败,但与本改动无直接关联,作者未在本 PR 中处理。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/layers/attention/layer.py 注意力层 modified 5.67
python/sglang/multimodal_gen/test/unit/test_sp_gather_sharded_sequence_contiguous.py 单元测试 added 7.09

关键符号

_gather_sharded_sequence

关键源码片段

python/sglang/multimodal_gen/runtime/layers/attention/layer.py core-logic

核心修复文件,`USPAttention._gather_sharded_sequence` 的两个切片分支追加 `.contiguous()`,解决 batch>1 时非连续分片导致 all-gather 崩溃的问题。

@staticmethod
def _gather_sharded_sequence(
    tensor: torch.Tensor,
    num_replicated_prefix: int = 0,
    num_replicated_suffix: int = 0,
) -> torch.Tensor:
    # 前缀与后缀同时复制会被拒绝,二者互斥
    if num_replicated_prefix and num_replicated_suffix:
        raise ValueError(
            "Replicated prefix and suffix cannot be used at the same time."
        )
​
    # 注意:沿 dim 1 切片会保留原始行 stride,只有 batch 为 1 时 PyTorch
    # 才因 size-1 维度忽略 stride 而判定为连续;batch > 1 时必须调
    # .contiguous() 物化,否则序列并行 all_gather 会报
    # "Tensors must be contiguous" 错误。
    if num_replicated_prefix:
        replicated = 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].contiguous()
        gathered = sequence_model_parallel_all_gather(sharded, dim=1)
        return torch.cat([gathered, replicated], dim=1)
​
    # 无复制时原样透传,避免不必要的拷贝
    return sequence_model_parallel_all_gather(tensor, dim=1)
python/sglang/multimodal_gen/test/unit/test_sp_gather_sharded_sequence_contiguous.py test-coverage

新增回归测试,mock all-gather 后单进程验证批量下前缀 / 后缀分片连续、数值不变、batch=1 旧路径不回归、异常分支被拒绝。

class _CaptureAllGather:
    """模拟 all-gather 的替身:记录输入张量并返回拼接结果。"""
​
    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):
        # 回归:batch=2 时前缀分支的 all-gather 输入必须是连续张量
        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):
        # 回归:batch=2 时后缀分支的 all-gather 输入必须是连续张量
        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):
        # 旧路径 batch=1 恰好连续,必须保持不回退
        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())

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 性能风险:.contiguous() 在非连续张量上会触发一次显存拷贝。但 batch>1 的路径此前完全崩溃不可用,修复后才有此开销;batch=1 路径下连续张量调用 contiguous() 为 no-op,不影响现有性能。
  2. 回归风险:改动仅涉及 USPAttention._gather_sharded_sequence 内部,该函数被 _forward_with_kv_gather 调用,用于 K/V 的序列并行收集。新增测试覆盖了数值保持与形状,回归面小。
  3. 兼容性风险:无公共 API 或配置变更,不影响现有用户配置。
  4. 残余风险:仓库中可能存在其他类似的「先切片再集合通信」模式未覆盖,但本 PR 仅针对该单一入口修复;测试 mock 了 all-gather,未在真实多 GPU 环境端到端验证。

影响范围:仅影响 diffusion 多模态生成服务的序列并行(SP)路径,在 2 卡及以上、启用 Ulysses 序列并行且存在 replicated prefix/suffix(常见于 CFG 分组)的配置下,此前 warmup 即崩溃,修复后恢复正常。影响程度:高——修复了一个默认配置下的启动崩溃,对 FLUX.2 等模型在 2 卡部署有直接影响;对 batch=1 既有路径无影响。团队影响:新增 93 行测试代码,明确覆盖了关键回归场景,便于后续维护。

核心路径变更 默认配置崩溃修复 多 GPU 依赖未端到端验证 复制开销仅影响新增可用路径

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论