Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com> Signed-off-by: hsliuustc0106 <liuhongsheng4@huawei.com>
230 lines
7.9 KiB
Python
230 lines
7.9 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from contextlib import ExitStack, contextmanager
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.distributed as dist
|
|
import torch.multiprocessing as mp
|
|
|
|
import vllm_omni.diffusion.models.magi2.attention as attention_module
|
|
import vllm_omni.diffusion.models.magi2.layers as layers_module
|
|
import vllm_omni.diffusion.models.magi2.mh_moe as mh_moe_module
|
|
import vllm_omni.diffusion.models.magi2.parallel as parallel_module
|
|
from vllm_omni.diffusion.models.magi2.attention import VarlenHandler
|
|
from vllm_omni.diffusion.models.magi2.configuration_magi2 import (
|
|
Magi2MHCConfig,
|
|
Magi2MoEConfig,
|
|
Magi2PreviewConfig,
|
|
)
|
|
from vllm_omni.diffusion.models.magi2.modeling_magi2 import (
|
|
Magi2PreviewTransformer,
|
|
Modality,
|
|
)
|
|
from vllm_omni.diffusion.models.magi2.parallel import Magi2ParallelGroup
|
|
|
|
_WORLD_SIZE = 4
|
|
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
|
|
|
|
|
|
def _tiny_config() -> Magi2PreviewConfig:
|
|
# Four attention, KV, and MoE heads make every head axis divisible by each
|
|
# requested TP/SP layout. The MLP and shared-expert widths are likewise
|
|
# divisible by TP=4, so this exercises all native row/column collectives.
|
|
return Magi2PreviewConfig(
|
|
num_layers=1,
|
|
hidden_size=32,
|
|
head_dim=8,
|
|
num_query_groups=4,
|
|
video_in_channels=4,
|
|
audio_in_channels=4,
|
|
text_in_channels=4,
|
|
intermediate_factor=1.5,
|
|
multimodal_layers=(0,),
|
|
params_dtype=torch.float32,
|
|
mhc=Magi2MHCConfig(num_streams=2),
|
|
moe=Magi2MoEConfig(
|
|
num_heads=4,
|
|
num_experts=2,
|
|
top_k=2,
|
|
expert_intermediate_size=8,
|
|
shared_expert_intermediate_size=8,
|
|
modality_shared_expert_intermediate_size=8,
|
|
layers=(0,),
|
|
),
|
|
)
|
|
|
|
|
|
def _initialize_model(model: Magi2PreviewTransformer) -> None:
|
|
generator = torch.Generator(device="cpu").manual_seed(71)
|
|
with torch.no_grad():
|
|
for name, parameter in model.named_parameters():
|
|
if name == "pre_adapter.rope.bands":
|
|
continue
|
|
parameter.copy_(
|
|
torch.randn(
|
|
parameter.shape,
|
|
dtype=parameter.dtype,
|
|
generator=generator,
|
|
)
|
|
* 0.025
|
|
)
|
|
moe = model.block.layers[0].mlp.moe_mlp
|
|
moe.router.expert_bias.zero_()
|
|
moe.router.expert_bias_ema.zero_()
|
|
|
|
|
|
def _inputs() -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, VarlenHandler]:
|
|
generator = torch.Generator(device="cpu").manual_seed(113)
|
|
packed = torch.randn(8, 4, generator=generator) * 0.2
|
|
coordinates = torch.tensor(
|
|
[
|
|
[0, 0, 0, 2, 2, 2, 2, 2, 2],
|
|
[0, 1, 1, 2, 2, 2, 2, 2, 2],
|
|
[0, 0, 0, 1, 2, 2, 1, 2, 2],
|
|
[0, 1, 1, 1, 2, 2, 1, 2, 2],
|
|
[1, 0, 0, 2, 2, 2, 2, 2, 2],
|
|
[1, 1, 1, 2, 2, 2, 2, 2, 2],
|
|
[0, 0, 0, 1, 1, 1, 1, 1, 1],
|
|
[0, 0, 0, 1, 1, 1, 1, 1, 1],
|
|
],
|
|
dtype=torch.float32,
|
|
)
|
|
modalities = torch.tensor(
|
|
[
|
|
Modality.VIDEO,
|
|
Modality.AUDIO,
|
|
Modality.TEXT,
|
|
Modality.TIME,
|
|
Modality.VIDEO,
|
|
Modality.AUDIO,
|
|
Modality.TEXT,
|
|
Modality.TIME,
|
|
]
|
|
)
|
|
cumulative = torch.tensor([0, packed.shape[0]], dtype=torch.int32)
|
|
varlen = VarlenHandler(cumulative, cumulative, packed.shape[0], packed.shape[0])
|
|
return packed, coordinates, modalities, varlen
|
|
|
|
|
|
@contextmanager
|
|
def _patched_groups(
|
|
tp_group: Magi2ParallelGroup,
|
|
sp_group: Magi2ParallelGroup,
|
|
):
|
|
# TP owns native weight and MoE-head shards when enabled. SP owns Ulysses
|
|
# tokens and is also the MoE-head group in the released SP-only layout.
|
|
ep_group = tp_group if tp_group.world_size > 1 else sp_group
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch.object(layers_module, "get_magi2_tp_group", return_value=tp_group))
|
|
stack.enter_context(patch.object(mh_moe_module, "get_magi2_ep_group", return_value=ep_group))
|
|
stack.enter_context(patch.object(parallel_module, "get_magi2_ulysses_group", return_value=sp_group))
|
|
stack.enter_context(patch.object(attention_module, "get_magi2_ulysses_group", return_value=sp_group))
|
|
yield
|
|
|
|
|
|
def _current_group(
|
|
rank: int,
|
|
rank_groups: tuple[tuple[tuple[int, ...], dist.ProcessGroup], ...],
|
|
) -> Magi2ParallelGroup:
|
|
for ranks, process_group in rank_groups:
|
|
if rank in ranks:
|
|
return Magi2ParallelGroup(
|
|
process_group,
|
|
world_size=len(ranks),
|
|
rank=ranks.index(rank),
|
|
)
|
|
return Magi2ParallelGroup(None, world_size=1, rank=0)
|
|
|
|
|
|
def _new_groups(
|
|
rank_sets: tuple[tuple[int, ...], ...],
|
|
) -> tuple[tuple[tuple[int, ...], dist.ProcessGroup], ...]:
|
|
groups = []
|
|
for ranks in rank_sets:
|
|
process_group = dist.new_group(ranks=list(ranks), backend="gloo")
|
|
groups.append((ranks, process_group))
|
|
return tuple(groups)
|
|
|
|
|
|
def _distributed_worker(rank: int, rendezvous: str) -> None:
|
|
torch.set_num_threads(1)
|
|
dist.init_process_group(
|
|
"gloo",
|
|
init_method=rendezvous,
|
|
rank=rank,
|
|
world_size=_WORLD_SIZE,
|
|
)
|
|
try:
|
|
singleton = Magi2ParallelGroup(None, world_size=1, rank=0)
|
|
with _patched_groups(singleton, singleton):
|
|
oracle = Magi2PreviewTransformer(_tiny_config())
|
|
_initialize_model(oracle)
|
|
checkpoint = [(name, value.detach().clone()) for name, value in oracle.state_dict().items()]
|
|
inputs = _inputs()
|
|
with torch.no_grad():
|
|
expected = oracle(*inputs)
|
|
|
|
tp2_groups = _new_groups(((0, 1), (2, 3)))
|
|
sp2_groups = _new_groups(((0, 2), (1, 3)))
|
|
world_group = Magi2ParallelGroup(
|
|
dist.group.WORLD,
|
|
world_size=_WORLD_SIZE,
|
|
rank=rank,
|
|
)
|
|
layouts = (
|
|
("tp4", world_group, singleton),
|
|
("tp2sp2", _current_group(rank, tp2_groups), _current_group(rank, sp2_groups)),
|
|
("sp4", singleton, world_group),
|
|
)
|
|
|
|
for layout_name, tp_group, sp_group in layouts:
|
|
tp_group = Magi2ParallelGroup(
|
|
tp_group.group,
|
|
tp_group.world_size,
|
|
tp_group.rank,
|
|
replicated_sequence=True,
|
|
)
|
|
with _patched_groups(tp_group, sp_group):
|
|
model = Magi2PreviewTransformer(_tiny_config())
|
|
assert model.load_weights(checkpoint) == set(model.state_dict())
|
|
with torch.no_grad():
|
|
actual = model(*inputs)
|
|
|
|
max_abs_error = (actual - expected).abs().max()
|
|
dist.all_reduce(max_abs_error, op=dist.ReduceOp.MAX)
|
|
close = torch.tensor(
|
|
int(torch.allclose(actual, expected, atol=2e-5, rtol=2e-4)),
|
|
dtype=torch.int32,
|
|
)
|
|
dist.all_reduce(close, op=dist.ReduceOp.MIN)
|
|
if not close.item():
|
|
raise AssertionError(
|
|
f"{layout_name} differs from the single-rank oracle; "
|
|
f"global max absolute error={max_abs_error.item():.8g}"
|
|
)
|
|
dist.barrier()
|
|
finally:
|
|
dist.destroy_process_group()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not dist.is_available() or not dist.is_gloo_available(),
|
|
reason="requires torch.distributed gloo",
|
|
)
|
|
def test_four_rank_tp_sp_layouts_match_single_rank_oracle() -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
rendezvous = f"file://{os.path.join(temp_dir, 'gloo-rendezvous')}"
|
|
mp.spawn(
|
|
_distributed_worker,
|
|
args=(rendezvous,),
|
|
nprocs=_WORLD_SIZE,
|
|
join=True,
|
|
)
|