Signed-off-by: Maciej Bala <mbala@nvidia.com> Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com>
201 lines
8.8 KiB
Python
201 lines
8.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""Compare full and pre-sharded Cosmos loading with real two-rank FSDP."""
|
|
|
|
from contextlib import ExitStack
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.distributed as dist
|
|
import torch.nn.functional as F
|
|
from torch import nn
|
|
|
|
from tests.helpers.mark import hardware_test
|
|
|
|
pytestmark = [pytest.mark.diffusion, pytest.mark.parallel, pytest.mark.core_model, pytest.mark.gpu]
|
|
|
|
|
|
class _SDPAAttention(nn.Module):
|
|
"""Use deterministic native attention while testing loading and FSDP."""
|
|
|
|
def __init__(self, *, causal, softmax_scale, **kwargs):
|
|
super().__init__()
|
|
self.causal = causal
|
|
self.scale = softmax_scale
|
|
|
|
def forward(self, query, key, value):
|
|
return F.scaled_dot_product_attention(
|
|
query.transpose(1, 2),
|
|
key.transpose(1, 2),
|
|
value.transpose(1, 2),
|
|
is_causal=self.causal,
|
|
scale=self.scale,
|
|
).transpose(1, 2)
|
|
|
|
|
|
def _load_cosmos_worker(rank, rendezvous, checkpoint_dir, edge):
|
|
from tests.model_executor.helpers import bootstrap_vllm_layer_custom_op_modules
|
|
|
|
bootstrap_vllm_layer_custom_op_modules()
|
|
|
|
from safetensors.torch import save_file
|
|
from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config
|
|
from vllm.config.load import LoadConfig
|
|
from vllm.model_executor import parameter
|
|
from vllm.model_executor.layers import linear
|
|
from vllm.utils.torch_utils import set_default_torch_dtype
|
|
|
|
from vllm_omni.diffusion.distributed import hsdp
|
|
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
|
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3, transformer_cosmos3_edge
|
|
from vllm_omni.platforms import current_omni_platform
|
|
|
|
device = torch.device("cuda", rank)
|
|
current_omni_platform.set_device(device)
|
|
dist.init_process_group("nccl", init_method=rendezvous, rank=rank, world_size=2, timeout=timedelta(seconds=120))
|
|
try:
|
|
with ExitStack() as stack:
|
|
stack.enter_context(set_current_vllm_config(VllmConfig(device_config=DeviceConfig(device="cuda"))))
|
|
stack.enter_context(set_default_torch_dtype(torch.bfloat16))
|
|
# HSDP spans both workers, while every vLLM linear uses TP=1.
|
|
for module in (linear, parameter):
|
|
stack.enter_context(patch.object(module, "get_tensor_model_parallel_rank", return_value=0))
|
|
stack.enter_context(patch.object(module, "get_tensor_model_parallel_world_size", return_value=1))
|
|
for module in (transformer_cosmos3, transformer_cosmos3_edge):
|
|
stack.enter_context(patch.object(module, "get_tensor_model_parallel_world_size", return_value=1))
|
|
stack.enter_context(patch.object(module, "FrameworkAttention", _SDPAAttention))
|
|
stack.enter_context(patch.object(transformer_cosmos3, "_get_ulysses_state", return_value=(1, 0, None)))
|
|
stack.enter_context(
|
|
patch.object(hsdp, "get_world_group", return_value=SimpleNamespace(world_size=2, rank_in_group=rank))
|
|
)
|
|
|
|
model_cls = (
|
|
transformer_cosmos3_edge.Cosmos3EdgeVFMTransformer
|
|
if edge
|
|
else transformer_cosmos3.Cosmos3VFMTransformer
|
|
)
|
|
tf_config = dict(
|
|
hidden_size=16,
|
|
num_hidden_layers=1,
|
|
num_attention_heads=2,
|
|
num_key_value_heads=2,
|
|
head_dim=8,
|
|
intermediate_size=32,
|
|
vocab_size=32,
|
|
latent_patch_size=2,
|
|
latent_channel=48,
|
|
temporal_compression_factor=4,
|
|
rope_scaling={"mrope_section": [2, 1, 1]},
|
|
)
|
|
if edge:
|
|
tf_config.update(
|
|
backbone_type=transformer_cosmos3_edge.COSMOS3_EDGE_BACKBONE_TYPE,
|
|
qk_norm_for_text=False,
|
|
use_und_k_norm_for_gen=True,
|
|
)
|
|
config = SimpleNamespace(
|
|
tf_model_config=tf_config,
|
|
dtype=torch.bfloat16,
|
|
quantization_config=None,
|
|
lora_path=None,
|
|
num_weight_load_threads=1,
|
|
hsdp_weight_load_strategy="full",
|
|
parallel_config=SimpleNamespace(
|
|
use_hsdp=True, hsdp_replicate_size=1, hsdp_shard_size=2, tensor_parallel_size=1
|
|
),
|
|
)
|
|
|
|
class Pipeline(nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.transformer = model_cls(config)
|
|
self.transformer.register_buffer("scratch", torch.tensor([7.0]), persistent=False)
|
|
self.vae = nn.Linear(2, 2)
|
|
self.weights_sources = [
|
|
DiffusersPipelineLoader.ComponentSource(checkpoint_dir, None, None, "transformer.", False)
|
|
]
|
|
|
|
def load_weights(self, weights):
|
|
state = dict(weights)
|
|
self.transformer.load_state_dict(
|
|
{name.removeprefix("transformer."): t for name, t in state.items()}
|
|
)
|
|
self.transformer.post_load_weights()
|
|
self.transformer.eval()
|
|
self.transformer.validate_loaded_weights(set(state))
|
|
return set(state)
|
|
|
|
full = Pipeline()
|
|
if rank == 0:
|
|
torch.manual_seed(42)
|
|
with torch.no_grad():
|
|
for name, tensor in full.transformer.named_parameters():
|
|
tensor.copy_(torch.randn_like(tensor) * 0.02)
|
|
if "norm" in name:
|
|
tensor.fill_(1)
|
|
save_file(full.transformer.state_dict(), str(Path(checkpoint_dir) / "model.safetensors"))
|
|
dist.barrier()
|
|
|
|
def load(model):
|
|
loader = DiffusersPipelineLoader(LoadConfig(), config)
|
|
with patch.object(loader, "_init_from_load_format", return_value=model):
|
|
return loader._load_model_with_hsdp(device)
|
|
|
|
full = load(full)
|
|
config.hsdp_weight_load_strategy = "pre_sharded"
|
|
pre_sharded = load(Pipeline())
|
|
full_parameters = dict(full.transformer.named_parameters())
|
|
from torch.distributed.tensor import DTensor
|
|
|
|
for name, actual in pre_sharded.transformer.named_parameters():
|
|
expected = full_parameters[name]
|
|
if name.startswith("time_embedder."):
|
|
assert not isinstance(actual, DTensor)
|
|
assert actual.dtype == torch.float32
|
|
else:
|
|
assert isinstance(actual, DTensor)
|
|
actual = actual.full_tensor()
|
|
expected = expected.full_tensor()
|
|
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
|
assert pre_sharded.vae.weight.device == device
|
|
torch.testing.assert_close(pre_sharded.transformer.scratch, full.transformer.scratch)
|
|
assert all(t.device.type != "meta" for t in pre_sharded.transformer.buffers())
|
|
|
|
inputs = dict(
|
|
hidden_states=torch.linspace(-1, 1, 48 * 4, device=device).reshape(1, 48, 1, 2, 2),
|
|
timestep=torch.tensor([1.0], device=device),
|
|
text_ids=torch.tensor([[1, 2]], device=device),
|
|
text_mask=torch.ones(1, 2, dtype=torch.long, device=device),
|
|
video_shape=(1, 2, 2),
|
|
fps=24.0,
|
|
)
|
|
with torch.no_grad():
|
|
# Also exercise the cached UND path on the second denoising step.
|
|
for step in (1.0, 0.5):
|
|
inputs["timestep"].fill_(step)
|
|
expected = full.transformer(**inputs)
|
|
actual = pre_sharded.transformer(**inputs)
|
|
assert torch.isfinite(actual).all()
|
|
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
|
finally:
|
|
dist.destroy_process_group()
|
|
|
|
|
|
@pytest.mark.parametrize("edge", [False, True], ids=["cosmos3", "cosmos3_edge"])
|
|
@hardware_test(res={"cuda": "L4"}, num_cards=2)
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available() or torch.accelerator.device_count() < 2 or not dist.is_nccl_available(),
|
|
reason="requires two CUDA GPUs and NCCL",
|
|
)
|
|
def test_cosmos3_pre_sharded_matches_full_hsdp(tmp_path, edge):
|
|
torch.multiprocessing.spawn(
|
|
_load_cosmos_worker,
|
|
args=(f"file://{tmp_path / 'rendezvous'}", str(tmp_path), edge),
|
|
nprocs=2,
|
|
join=True,
|
|
)
|