Signed-off-by: Nick Cao <ncao@redhat.com> Co-authored-by: Codex <noreply@openai.com>
180 lines
6.4 KiB
Python
180 lines
6.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""Unit tests for SP subgroup construction for diffusion parallel state."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import torch
|
|
import vllm.distributed.parallel_state as vllm_parallel_state
|
|
from vllm.utils.network_utils import get_file_store_init_method
|
|
|
|
from vllm_omni.diffusion.distributed import parallel_state as omni_parallel_state
|
|
from vllm_omni.diffusion.distributed.parallel_state import RankGenerator, set_seq_parallel_pg
|
|
|
|
pytestmark = [pytest.mark.diffusion, pytest.mark.parallel, pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
|
|
def ensure_parallel_state_initialized():
|
|
"""Ensure torch distributed is initialized and that all world / count vars are set."""
|
|
assert torch.distributed.is_initialized()
|
|
assert omni_parallel_state._WORLD is not None
|
|
assert vllm_parallel_state._WORLD is not None
|
|
assert vllm_parallel_state._NODE_COUNT is not None
|
|
|
|
|
|
def ensure_parallel_state_not_initialized():
|
|
"""Ensure torch distributed is not initialized and that all world / count vars are unset."""
|
|
assert not torch.distributed.is_initialized()
|
|
assert omni_parallel_state._WORLD is None
|
|
assert vllm_parallel_state._WORLD is None
|
|
assert vllm_parallel_state._NODE_COUNT is None
|
|
|
|
|
|
def test_omni_manages_vllm_distributed_state(monkeypatch):
|
|
"""Verify Omni initializes and tears down vLLM's distributed state properly.
|
|
|
|
This is a regression test for ensuring vLLM Omni also sets up vLLM's vars correctly
|
|
to avoid potential misalignment in cases where Omni uses vLLM's native coordinator,
|
|
e.g., MoE + diffusion.
|
|
"""
|
|
ensure_parallel_state_not_initialized()
|
|
monkeypatch.setattr(omni_parallel_state.current_omni_platform, "get_device_count", lambda: 1)
|
|
monkeypatch.setattr(omni_parallel_state.current_omni_platform, "set_device", lambda _device: None)
|
|
omni_parallel_state.init_distributed_environment(
|
|
world_size=1,
|
|
rank=0,
|
|
local_rank=0,
|
|
distributed_init_method=get_file_store_init_method(),
|
|
backend="gloo",
|
|
)
|
|
try:
|
|
ensure_parallel_state_initialized()
|
|
finally:
|
|
omni_parallel_state.destroy_distributed_env()
|
|
ensure_parallel_state_not_initialized()
|
|
|
|
|
|
def _fake_new_group_factory(created_groups: list[SimpleNamespace]):
|
|
def _fake_new_group(ranks, *args, **kwargs):
|
|
group = SimpleNamespace(ranks=list(ranks))
|
|
created_groups.append(group)
|
|
return group
|
|
|
|
return _fake_new_group
|
|
|
|
|
|
@pytest.mark.cpu
|
|
@pytest.mark.parametrize(
|
|
"rank, expected_ulysses, expected_ring",
|
|
[
|
|
(0, [0, 2], [0]),
|
|
(1, [1, 3], [1]),
|
|
(2, [0, 2], [2]),
|
|
(3, [1, 3], [3]),
|
|
],
|
|
)
|
|
def test_set_seq_parallel_pg_uses_explicit_sp_groups(rank, expected_ulysses, expected_ring, monkeypatch):
|
|
created_groups: list[SimpleNamespace] = []
|
|
monkeypatch.setattr(torch.distributed, "new_group", _fake_new_group_factory(created_groups))
|
|
|
|
# tp=2, sp=2 -> SP groups are non-contiguous: [0,2] and [1,3]
|
|
sp_group_ranks = RankGenerator(2, 2, 1, 1, 1, "tp-sp-pp-cfg-dp").get_ranks("sp")
|
|
|
|
ulysses_pg, ring_pg, allgather_pg = set_seq_parallel_pg(
|
|
sp_ulysses_degree=2,
|
|
sp_ring_degree=1,
|
|
rank=rank,
|
|
world_size=4,
|
|
sp_group_ranks=sp_group_ranks,
|
|
)
|
|
|
|
assert ulysses_pg.ranks == expected_ulysses
|
|
assert ring_pg.ranks == expected_ring
|
|
assert allgather_pg.ranks == [rank]
|
|
|
|
|
|
@pytest.mark.cpu
|
|
@pytest.mark.parametrize("rank", [0, 1, 2, 3])
|
|
def test_set_seq_parallel_pg_builds_independent_allgather_group(rank, monkeypatch):
|
|
created_groups: list[SimpleNamespace] = []
|
|
monkeypatch.setattr(torch.distributed, "new_group", _fake_new_group_factory(created_groups))
|
|
sp_group_ranks = [[0, 2], [1, 3]]
|
|
|
|
ulysses_pg, ring_pg, allgather_pg = set_seq_parallel_pg(
|
|
sp_ulysses_degree=1,
|
|
sp_ring_degree=1,
|
|
sp_allgather_degree=2,
|
|
rank=rank,
|
|
world_size=4,
|
|
sp_group_ranks=sp_group_ranks,
|
|
)
|
|
|
|
assert ulysses_pg.ranks == [rank]
|
|
assert ring_pg.ranks == [rank]
|
|
assert allgather_pg.ranks == ([0, 2] if rank % 2 == 0 else [1, 3])
|
|
|
|
|
|
@pytest.mark.cpu
|
|
def test_set_seq_parallel_pg_rejects_allgather_with_ulysses_or_ring(monkeypatch):
|
|
created_groups: list[SimpleNamespace] = []
|
|
monkeypatch.setattr(torch.distributed, "new_group", _fake_new_group_factory(created_groups))
|
|
|
|
with pytest.raises(ValueError, match="mutually exclusive"):
|
|
set_seq_parallel_pg(
|
|
sp_ulysses_degree=2,
|
|
sp_ring_degree=1,
|
|
sp_allgather_degree=2,
|
|
rank=0,
|
|
world_size=4,
|
|
)
|
|
|
|
|
|
@pytest.mark.cpu
|
|
def test_set_seq_parallel_pg_validates_sp_group_ranks(monkeypatch):
|
|
created_groups: list[SimpleNamespace] = []
|
|
monkeypatch.setattr(torch.distributed, "new_group", _fake_new_group_factory(created_groups))
|
|
|
|
# world_size=4, sp_size=2 -> expect 2 groups, provide 1 to trigger validation
|
|
with pytest.raises(ValueError, match="Invalid sp_group_ranks"):
|
|
set_seq_parallel_pg(
|
|
sp_ulysses_degree=2,
|
|
sp_ring_degree=1,
|
|
rank=0,
|
|
world_size=4,
|
|
sp_group_ranks=[[0, 2]],
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("rank", [1, 3])
|
|
def test_tensor_dict_broadcast_preserves_subgroup_source(rank, monkeypatch):
|
|
from vllm_omni.diffusion.distributed.group_coordinator import GroupCoordinator, TensorMetadata
|
|
|
|
coordinator = object.__new__(GroupCoordinator)
|
|
coordinator.rank = rank
|
|
coordinator.ranks = [1, 3]
|
|
coordinator.rank_in_group = [1, 3].index(rank)
|
|
coordinator.world_size = 2
|
|
coordinator.device_group = object()
|
|
coordinator.cpu_group = object()
|
|
metadata = [("payload", TensorMetadata("cpu", torch.float32, (2,)))]
|
|
monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True)
|
|
|
|
def broadcast_object(value, src):
|
|
assert src == 0
|
|
return metadata
|
|
|
|
def broadcast(tensor, src, group, async_op):
|
|
assert src == 1
|
|
assert group is coordinator.cpu_group
|
|
assert async_op
|
|
tensor.fill_(7)
|
|
return SimpleNamespace(wait=lambda: None)
|
|
|
|
monkeypatch.setattr(coordinator, "broadcast_object", broadcast_object)
|
|
monkeypatch.setattr(torch.distributed, "broadcast", broadcast)
|
|
result = coordinator.broadcast_tensor_dict({"payload": torch.full((2,), 7.0)} if rank == 1 else None)
|
|
assert torch.equal(result["payload"], torch.full((2,), 7.0))
|