2026-01-31 00:23:44 +08:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
2026-09-14 14:27:10 +08:00
|
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
2026-07-29 23:53:03 +08:00
|
|
|
"""Tests for CFG (Classifier-Free Guidance) parallel functionality.
|
2026-01-31 00:23:44 +08:00
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
CPU tests mock the CFG process group and validate mixin logic without GPUs.
|
|
|
|
|
Nightly GPU parity tests compare sequential CFG (cfg_parallel_size=1) against
|
|
|
|
|
real multi-GPU CFG parallel under fixed seeds.
|
2026-01-31 00:23:44 +08:00
|
|
|
"""
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-01-31 00:23:44 +08:00
|
|
|
import os
|
2026-07-29 23:53:03 +08:00
|
|
|
from typing import Any
|
2026-01-31 00:23:44 +08:00
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
import torch
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
from tests.helpers.mark import hardware_marks, hardware_test
|
|
|
|
|
from vllm_omni.diffusion.distributed import parallel_state
|
2026-09-03 10:37:21 +08:00
|
|
|
from vllm_omni.diffusion.distributed.cfg_parallel import (
|
|
|
|
|
CFGParallelMixin,
|
|
|
|
|
_dispatch_branches,
|
|
|
|
|
_get_cfg_world_size_or_one,
|
|
|
|
|
_wrap,
|
|
|
|
|
)
|
2026-01-31 00:23:44 +08:00
|
|
|
from vllm_omni.diffusion.distributed.parallel_state import (
|
|
|
|
|
destroy_distributed_env,
|
|
|
|
|
get_classifier_free_guidance_rank,
|
|
|
|
|
get_classifier_free_guidance_world_size,
|
|
|
|
|
init_distributed_environment,
|
|
|
|
|
initialize_model_parallel,
|
|
|
|
|
)
|
|
|
|
|
from vllm_omni.platforms import current_omni_platform
|
|
|
|
|
|
2026-09-14 14:27:10 +08:00
|
|
|
_L4_TWO_GPU = hardware_marks(res={"cuda": ["L4", "B200"]}, num_cards=2)
|
|
|
|
|
_L4_THREE_GPU = hardware_marks(res={"cuda": ["L4", "B200"]}, num_cards=3)
|
2026-01-31 00:23:44 +08:00
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
|
|
|
|
|
def _set_random_seeds(seed: int) -> None:
|
|
|
|
|
torch.manual_seed(seed)
|
|
|
|
|
if torch.cuda.is_available():
|
|
|
|
|
torch.cuda.manual_seed_all(seed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _init_model_weights(module: torch.nn.Module, seed: int) -> None:
|
|
|
|
|
_set_random_seeds(seed)
|
|
|
|
|
for param in module.parameters():
|
|
|
|
|
torch.nn.init.normal_(param, mean=0.0, std=0.02)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cfg_close_tolerance(dtype: torch.dtype) -> tuple[float, float]:
|
|
|
|
|
if dtype == torch.float32:
|
|
|
|
|
return 1e-5, 1e-5
|
|
|
|
|
if dtype == torch.bfloat16:
|
|
|
|
|
return 1e-2, 1e-2
|
|
|
|
|
return 1e-3, 1e-3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assert_cfg_outputs_close(
|
|
|
|
|
actual: torch.Tensor,
|
|
|
|
|
expected: torch.Tensor,
|
|
|
|
|
*,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
msg: str,
|
|
|
|
|
) -> None:
|
|
|
|
|
rtol, atol = _cfg_close_tolerance(dtype)
|
|
|
|
|
torch.testing.assert_close(actual, expected, rtol=rtol, atol=atol, msg=msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_environment_variables(envs_dict: dict[str, str]) -> None:
|
|
|
|
|
for key, value in envs_dict.items():
|
|
|
|
|
os.environ[key] = value
|
2026-01-31 00:23:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleTransformer(torch.nn.Module):
|
|
|
|
|
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, num_heads: int = 8):
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.hidden_dim = hidden_dim
|
|
|
|
|
self.num_heads = num_heads
|
|
|
|
|
self.head_dim = hidden_dim // num_heads
|
2026-07-29 23:53:03 +08:00
|
|
|
assert hidden_dim % num_heads == 0
|
2026-01-31 00:23:44 +08:00
|
|
|
|
|
|
|
|
self.input_proj = torch.nn.Conv2d(in_channels, hidden_dim, 1)
|
|
|
|
|
self.q_proj = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
|
self.k_proj = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
|
self.v_proj = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
|
self.out_proj = torch.nn.Linear(hidden_dim, hidden_dim)
|
|
|
|
|
self.final_proj = torch.nn.Conv2d(hidden_dim, in_channels, 1)
|
|
|
|
|
self.norm1 = torch.nn.LayerNorm(hidden_dim)
|
|
|
|
|
self.norm2 = torch.nn.LayerNorm(hidden_dim)
|
|
|
|
|
|
|
|
|
|
def forward(self, x: torch.Tensor, **kwargs) -> tuple[torch.Tensor]:
|
2026-07-29 23:53:03 +08:00
|
|
|
del kwargs
|
|
|
|
|
batch_size, _channels, height, width = x.shape
|
|
|
|
|
x = self.input_proj(x)
|
|
|
|
|
x = x.flatten(2).transpose(1, 2)
|
2026-01-31 00:23:44 +08:00
|
|
|
residual = x
|
|
|
|
|
x = self.norm1(x)
|
2026-07-29 23:53:03 +08:00
|
|
|
q = self.q_proj(x)
|
|
|
|
|
k = self.k_proj(x)
|
|
|
|
|
v = self.v_proj(x)
|
|
|
|
|
seq_len = height * width
|
|
|
|
|
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
|
|
|
|
|
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
|
|
|
|
|
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
|
2026-01-31 00:23:44 +08:00
|
|
|
scale = self.head_dim**-0.5
|
2026-07-29 23:53:03 +08:00
|
|
|
attn_scores = torch.matmul(q, k.transpose(-2, -1)) * scale
|
2026-01-31 00:23:44 +08:00
|
|
|
attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1)
|
|
|
|
|
attn_output = torch.matmul(attn_weights, v)
|
2026-07-29 23:53:03 +08:00
|
|
|
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.hidden_dim)
|
2026-01-31 00:23:44 +08:00
|
|
|
attn_output = self.out_proj(attn_output)
|
|
|
|
|
x = residual + attn_output
|
|
|
|
|
residual = x
|
|
|
|
|
x = self.norm2(x)
|
|
|
|
|
x = residual + x
|
2026-07-29 23:53:03 +08:00
|
|
|
x = x.transpose(1, 2).view(batch_size, self.hidden_dim, height, width)
|
|
|
|
|
return (self.final_proj(x),)
|
2026-01-31 00:23:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCFGPipeline(CFGParallelMixin):
|
|
|
|
|
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, seed: int = 42):
|
2026-07-29 23:53:03 +08:00
|
|
|
_set_random_seeds(seed)
|
2026-01-31 00:23:44 +08:00
|
|
|
self.transformer = SimpleTransformer(in_channels, hidden_dim)
|
2026-07-29 23:53:03 +08:00
|
|
|
_init_model_weights(self.transformer, seed)
|
2026-04-12 22:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MultiBranchTestPipeline(CFGParallelMixin):
|
|
|
|
|
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, seed: int = 42):
|
2026-07-29 23:53:03 +08:00
|
|
|
_set_random_seeds(seed)
|
2026-04-12 22:06:49 +08:00
|
|
|
self.transformer = SimpleTransformer(in_channels, hidden_dim)
|
2026-07-29 23:53:03 +08:00
|
|
|
_init_model_weights(self.transformer, seed)
|
2026-04-12 22:06:49 +08:00
|
|
|
|
|
|
|
|
def combine_multi_branch_cfg_noise(self, predictions, true_cfg_scale, cfg_normalize=False):
|
|
|
|
|
if len(predictions) == 4:
|
|
|
|
|
text_scale = true_cfg_scale["text"]
|
|
|
|
|
image_scale = true_cfg_scale["image"]
|
|
|
|
|
vid_ref_scale = true_cfg_scale["vid_ref"]
|
|
|
|
|
pos, neg, vid_neg, audio_neg = predictions
|
|
|
|
|
combined = (
|
|
|
|
|
audio_neg
|
|
|
|
|
+ vid_ref_scale * (vid_neg - audio_neg)
|
|
|
|
|
+ image_scale * (neg - vid_neg)
|
|
|
|
|
+ text_scale * (pos - neg)
|
|
|
|
|
)
|
|
|
|
|
elif len(predictions) == 3:
|
|
|
|
|
text_scale = true_cfg_scale["text"]
|
|
|
|
|
image_scale = true_cfg_scale["image"]
|
|
|
|
|
pos, ref, uncond = predictions
|
|
|
|
|
combined = uncond + image_scale * (ref - uncond) + text_scale * (pos - ref)
|
|
|
|
|
else:
|
|
|
|
|
pos, neg = predictions[0], predictions[1]
|
|
|
|
|
combined = neg + true_cfg_scale * (pos - neg)
|
|
|
|
|
|
|
|
|
|
if cfg_normalize:
|
|
|
|
|
combined = self.cfg_normalize_function(pos, combined)
|
|
|
|
|
return combined
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
class FakeCfgGroup:
|
|
|
|
|
def __init__(self, *, world_size: int, rank_in_group: int, rank_tensors: list[torch.Tensor]):
|
|
|
|
|
self.world_size = world_size
|
|
|
|
|
self.rank_in_group = rank_in_group
|
|
|
|
|
self._rank_tensors = rank_tensors
|
|
|
|
|
|
|
|
|
|
def all_gather(
|
|
|
|
|
self,
|
|
|
|
|
input_: torch.Tensor,
|
|
|
|
|
dim: int = 0,
|
|
|
|
|
separate_tensors: bool = False,
|
|
|
|
|
) -> torch.Tensor | list[torch.Tensor]:
|
|
|
|
|
del dim, input_
|
|
|
|
|
assert separate_tensors
|
|
|
|
|
return [tensor.clone() for tensor in self._rank_tensors]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakeMultiBranchCfgGroup:
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
world_size: int,
|
|
|
|
|
rank_in_group: int,
|
|
|
|
|
slots_per_rank: list[list[tuple[torch.Tensor, ...]]],
|
|
|
|
|
):
|
|
|
|
|
self.world_size = world_size
|
|
|
|
|
self.rank_in_group = rank_in_group
|
|
|
|
|
self._slots_per_rank = slots_per_rank
|
|
|
|
|
# Production path re-runs predict_noise, so gathered tensors are value-equal
|
|
|
|
|
# but not identity-equal to the precomputed slot table. Index by call order
|
|
|
|
|
# instead: slot-major then element-major, matching _predict_multi_branch_parallel.
|
|
|
|
|
self._call_idx = 0
|
|
|
|
|
|
|
|
|
|
def all_gather(
|
|
|
|
|
self,
|
|
|
|
|
input_: torch.Tensor,
|
|
|
|
|
dim: int = 0,
|
|
|
|
|
separate_tensors: bool = False,
|
|
|
|
|
) -> torch.Tensor | list[torch.Tensor]:
|
|
|
|
|
del dim, input_
|
|
|
|
|
assert separate_tensors
|
|
|
|
|
n_elems = len(self._slots_per_rank[0][0])
|
|
|
|
|
slot_idx, elem_idx = divmod(self._call_idx, n_elems)
|
|
|
|
|
self._call_idx += 1
|
|
|
|
|
assert slot_idx < len(self._slots_per_rank[self.rank_in_group]), (
|
|
|
|
|
f"all_gather call {self._call_idx} exceeds slot table "
|
|
|
|
|
f"(slot_idx={slot_idx}, n_slots={len(self._slots_per_rank[self.rank_in_group])})"
|
|
|
|
|
)
|
|
|
|
|
return [self._slots_per_rank[rank][slot_idx][elem_idx].clone() for rank in range(self.world_size)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _patch_cfg_state(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
*,
|
|
|
|
|
world_size: int,
|
|
|
|
|
rank: int,
|
|
|
|
|
fake_group: FakeCfgGroup | FakeMultiBranchCfgGroup,
|
|
|
|
|
) -> None:
|
|
|
|
|
# Patch both the source module and cfg_parallel's bound imports.
|
|
|
|
|
# cfg_parallel does `from ...parallel_state import get_cfg_group`, so
|
|
|
|
|
# patching parallel_state alone does not affect CFGParallelMixin.
|
|
|
|
|
from vllm_omni.diffusion.distributed import cfg_parallel as cfg_parallel_mod
|
|
|
|
|
|
|
|
|
|
for module in (parallel_state, cfg_parallel_mod):
|
|
|
|
|
monkeypatch.setattr(module, "get_classifier_free_guidance_world_size", lambda: world_size)
|
|
|
|
|
monkeypatch.setattr(module, "get_classifier_free_guidance_rank", lambda: rank)
|
|
|
|
|
monkeypatch.setattr(module, "get_cfg_group", lambda: fake_group)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_two_branch_inputs(
|
|
|
|
|
*,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
channels: int,
|
|
|
|
|
height: int,
|
|
|
|
|
width: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
device: torch.device,
|
|
|
|
|
input_seed: int,
|
|
|
|
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
|
|
|
_set_random_seeds(input_seed)
|
|
|
|
|
positive = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
|
|
|
|
|
_set_random_seeds(input_seed + 1)
|
|
|
|
|
negative = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
|
|
|
|
|
return {"x": positive}, {"x": negative}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_multi_branch_inputs(
|
|
|
|
|
*,
|
|
|
|
|
n_branches: int,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
channels: int,
|
|
|
|
|
height: int,
|
|
|
|
|
width: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
device: torch.device,
|
|
|
|
|
input_seed: int,
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
branches_kwargs: list[dict[str, Any]] = []
|
|
|
|
|
for branch_idx in range(n_branches):
|
|
|
|
|
_set_random_seeds(input_seed + branch_idx)
|
|
|
|
|
x = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
|
|
|
|
|
branches_kwargs.append({"x": x})
|
|
|
|
|
return branches_kwargs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _multi_branch_cfg_scale(n_branches: int) -> float | dict[str, float]:
|
|
|
|
|
if n_branches == 2:
|
|
|
|
|
return 5.0
|
|
|
|
|
if n_branches == 3:
|
|
|
|
|
return {"text": 5.0, "image": 2.0}
|
|
|
|
|
return {"text": 5.0, "image": 2.0, "vid_ref": 1.5}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_multi_branch_slot_table(
|
|
|
|
|
*,
|
|
|
|
|
n_branches: int,
|
|
|
|
|
cfg_world_size: int,
|
|
|
|
|
branch_preds: list[torch.Tensor | tuple[torch.Tensor, ...]],
|
|
|
|
|
) -> list[list[tuple[torch.Tensor, ...]]]:
|
|
|
|
|
assignments = _dispatch_branches(n_branches, cfg_world_size)
|
|
|
|
|
max_per_rank = max(len(branch_ids) for branch_ids in assignments)
|
|
|
|
|
ref_pred = _wrap(branch_preds[0])
|
|
|
|
|
slots_per_rank: list[list[tuple[torch.Tensor, ...]]] = []
|
|
|
|
|
for branch_ids in assignments:
|
|
|
|
|
slots = [_wrap(branch_preds[branch_id]) for branch_id in branch_ids]
|
|
|
|
|
while len(slots) < max_per_rank:
|
|
|
|
|
slots.append(tuple(torch.zeros_like(tensor) for tensor in ref_pred))
|
|
|
|
|
slots_per_rank.append(slots)
|
|
|
|
|
return slots_per_rank
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_two_branch_cfg_parallel_mock(
|
|
|
|
|
pipeline: TestCFGPipeline,
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
*,
|
|
|
|
|
cfg_world_size: int,
|
|
|
|
|
positive_kwargs: dict[str, Any],
|
|
|
|
|
negative_kwargs: dict[str, Any],
|
|
|
|
|
true_cfg_scale: float,
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
extra_kwargs: dict[str, Any] | None,
|
|
|
|
|
) -> torch.Tensor:
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
pos_tensor = _wrap(pipeline.predict_noise(**positive_kwargs))[0]
|
|
|
|
|
neg_tensor = _wrap(pipeline.predict_noise(**negative_kwargs))[0]
|
|
|
|
|
|
|
|
|
|
outputs: list[torch.Tensor] = []
|
|
|
|
|
for cfg_rank in range(cfg_world_size):
|
|
|
|
|
fake_group = FakeCfgGroup(
|
|
|
|
|
world_size=cfg_world_size,
|
|
|
|
|
rank_in_group=cfg_rank,
|
|
|
|
|
rank_tensors=[pos_tensor, neg_tensor],
|
|
|
|
|
)
|
|
|
|
|
_patch_cfg_state(monkeypatch, world_size=cfg_world_size, rank=cfg_rank, fake_group=fake_group)
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
output = pipeline.predict_noise_maybe_with_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=true_cfg_scale,
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=negative_kwargs,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
kwargs=extra_kwargs,
|
|
|
|
|
)
|
|
|
|
|
assert output is not None
|
|
|
|
|
outputs.append(output)
|
|
|
|
|
|
|
|
|
|
for rank_output in outputs[1:]:
|
|
|
|
|
torch.testing.assert_close(
|
|
|
|
|
rank_output,
|
|
|
|
|
outputs[0],
|
|
|
|
|
rtol=0,
|
|
|
|
|
atol=0,
|
|
|
|
|
msg="CFG parallel ranks produced different outputs in mock path",
|
|
|
|
|
)
|
|
|
|
|
return outputs[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_multi_branch_cfg_parallel_mock(
|
|
|
|
|
pipeline: MultiBranchTestPipeline,
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
*,
|
|
|
|
|
cfg_world_size: int,
|
|
|
|
|
branches_kwargs: list[dict[str, Any]],
|
|
|
|
|
true_cfg_scale: float | dict[str, float],
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
) -> torch.Tensor:
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
branch_preds = [pipeline.predict_noise(**kwargs) for kwargs in branches_kwargs]
|
|
|
|
|
slots_per_rank = _build_multi_branch_slot_table(
|
|
|
|
|
n_branches=len(branches_kwargs),
|
|
|
|
|
cfg_world_size=cfg_world_size,
|
|
|
|
|
branch_preds=branch_preds,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
outputs: list[torch.Tensor] = []
|
|
|
|
|
for cfg_rank in range(cfg_world_size):
|
|
|
|
|
fake_group = FakeMultiBranchCfgGroup(
|
|
|
|
|
world_size=cfg_world_size,
|
|
|
|
|
rank_in_group=cfg_rank,
|
|
|
|
|
slots_per_rank=slots_per_rank,
|
|
|
|
|
)
|
|
|
|
|
_patch_cfg_state(monkeypatch, world_size=cfg_world_size, rank=cfg_rank, fake_group=fake_group)
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
output = pipeline.predict_noise_with_multi_branch_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=true_cfg_scale,
|
|
|
|
|
branches_kwargs=branches_kwargs,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
)
|
|
|
|
|
assert output is not None
|
|
|
|
|
outputs.append(output)
|
|
|
|
|
|
|
|
|
|
for rank_output in outputs[1:]:
|
|
|
|
|
torch.testing.assert_close(
|
|
|
|
|
rank_output,
|
|
|
|
|
outputs[0],
|
|
|
|
|
rtol=0,
|
|
|
|
|
atol=0,
|
|
|
|
|
msg="Multi-branch CFG parallel ranks produced different outputs in mock path",
|
|
|
|
|
)
|
|
|
|
|
return outputs[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_pipeline_on_device(
|
|
|
|
|
pipeline_cls: type,
|
|
|
|
|
*,
|
|
|
|
|
hidden_dim: int,
|
|
|
|
|
model_seed: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
device: torch.device,
|
|
|
|
|
):
|
|
|
|
|
pipeline = pipeline_cls(in_channels=4, hidden_dim=hidden_dim, seed=model_seed)
|
|
|
|
|
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
|
|
|
|
|
pipeline.transformer.eval()
|
|
|
|
|
return pipeline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# CPU: mixin logic via mocked CFG groups (no GPU / no torch.distributed spawn)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 10:37:21 +08:00
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
def test_cfg_world_size_defaults_only_when_group_is_uninitialized(monkeypatch: pytest.MonkeyPatch):
|
|
|
|
|
monkeypatch.setattr(parallel_state, "_CFG", None)
|
|
|
|
|
|
|
|
|
|
assert not parallel_state.is_cfg_group_initialized()
|
|
|
|
|
assert _get_cfg_world_size_or_one() == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
def test_cfg_world_size_does_not_swallow_initialized_group_errors(monkeypatch: pytest.MonkeyPatch):
|
|
|
|
|
class BrokenCfgGroup:
|
|
|
|
|
@property
|
|
|
|
|
def world_size(self) -> int:
|
|
|
|
|
raise AssertionError("CFG group is broken")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(parallel_state, "_CFG", BrokenCfgGroup())
|
|
|
|
|
|
|
|
|
|
assert parallel_state.is_cfg_group_initialized()
|
|
|
|
|
with pytest.raises(AssertionError, match="CFG group is broken"):
|
|
|
|
|
_get_cfg_world_size_or_one()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
def test_cfg_defaults_to_sequential_without_initialized_group():
|
|
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
TestCFGPipeline,
|
|
|
|
|
hidden_dim=32,
|
|
|
|
|
model_seed=42,
|
|
|
|
|
dtype=torch.float32,
|
|
|
|
|
device=torch.device("cpu"),
|
|
|
|
|
)
|
|
|
|
|
positive_kwargs, negative_kwargs = _make_two_branch_inputs(
|
|
|
|
|
batch_size=1,
|
|
|
|
|
channels=4,
|
|
|
|
|
height=4,
|
|
|
|
|
width=4,
|
|
|
|
|
dtype=torch.float32,
|
|
|
|
|
device=torch.device("cpu"),
|
|
|
|
|
input_seed=123,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
positive = pipeline.predict_noise(**positive_kwargs)
|
|
|
|
|
negative = pipeline.predict_noise(**negative_kwargs)
|
|
|
|
|
actual = pipeline.predict_noise_maybe_with_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=5.0,
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=negative_kwargs,
|
|
|
|
|
cfg_normalize=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expected = negative + 5.0 * (positive - negative)
|
|
|
|
|
torch.testing.assert_close(actual, expected)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
@pytest.mark.parametrize("batch_size", [2])
|
|
|
|
|
@pytest.mark.parametrize("cfg_normalize", [False, True])
|
|
|
|
|
@pytest.mark.parametrize("extra_kwargs", [None, {"step_i": 1}])
|
|
|
|
|
def test_predict_noise_with_cfg(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
extra_kwargs: dict[str, Any] | None,
|
|
|
|
|
):
|
|
|
|
|
dtype = torch.float32
|
|
|
|
|
device = torch.device("cpu")
|
|
|
|
|
pipeline = _build_pipeline_on_device(TestCFGPipeline, hidden_dim=128, model_seed=42, dtype=dtype, device=device)
|
|
|
|
|
positive_kwargs, negative_kwargs = _make_two_branch_inputs(
|
|
|
|
|
batch_size=batch_size,
|
|
|
|
|
channels=4,
|
|
|
|
|
height=16,
|
|
|
|
|
width=16,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=123,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_patch_cfg_state(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
world_size=1,
|
|
|
|
|
rank=0,
|
|
|
|
|
fake_group=FakeCfgGroup(world_size=1, rank_in_group=0, rank_tensors=[]),
|
|
|
|
|
)
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
baseline = pipeline.predict_noise_maybe_with_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=7.5,
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=negative_kwargs,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
kwargs=extra_kwargs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
parallel = _run_two_branch_cfg_parallel_mock(
|
|
|
|
|
pipeline,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
cfg_world_size=2,
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=negative_kwargs,
|
|
|
|
|
true_cfg_scale=7.5,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
extra_kwargs=extra_kwargs,
|
|
|
|
|
)
|
|
|
|
|
assert baseline is not None
|
|
|
|
|
_assert_cfg_outputs_close(
|
|
|
|
|
parallel,
|
|
|
|
|
baseline,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
msg=f"mock CFG parallel differs from sequential (cfg_normalize={cfg_normalize})",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
def test_predict_noise_without_cfg():
|
|
|
|
|
dtype = torch.float32
|
|
|
|
|
device = torch.device("cpu")
|
|
|
|
|
pipeline = _build_pipeline_on_device(TestCFGPipeline, hidden_dim=128, model_seed=42, dtype=dtype, device=device)
|
|
|
|
|
positive_kwargs, _ = _make_two_branch_inputs(
|
|
|
|
|
batch_size=1,
|
|
|
|
|
channels=4,
|
|
|
|
|
height=16,
|
|
|
|
|
width=16,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=123,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
noise_pred = pipeline.predict_noise_maybe_with_cfg(
|
|
|
|
|
do_true_cfg=False,
|
|
|
|
|
true_cfg_scale=7.5,
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=None,
|
|
|
|
|
cfg_normalize=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert noise_pred is not None
|
|
|
|
|
assert noise_pred.shape == (1, 4, 16, 16)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"cfg_parallel_size,n_branches",
|
|
|
|
|
[(2, 2), (2, 3), (3, 3), (2, 4)],
|
|
|
|
|
)
|
|
|
|
|
@pytest.mark.parametrize("batch_size", [2])
|
|
|
|
|
@pytest.mark.parametrize("cfg_normalize", [False, True])
|
|
|
|
|
def test_predict_noise_with_multi_branch_cfg(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
cfg_parallel_size: int,
|
|
|
|
|
n_branches: int,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
):
|
|
|
|
|
dtype = torch.float32
|
|
|
|
|
device = torch.device("cpu")
|
|
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
MultiBranchTestPipeline,
|
|
|
|
|
hidden_dim=128,
|
|
|
|
|
model_seed=42,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
branches_kwargs = _make_multi_branch_inputs(
|
|
|
|
|
n_branches=n_branches,
|
|
|
|
|
batch_size=batch_size,
|
|
|
|
|
channels=4,
|
|
|
|
|
height=16,
|
|
|
|
|
width=16,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=123,
|
|
|
|
|
)
|
|
|
|
|
cfg_scale = _multi_branch_cfg_scale(n_branches)
|
|
|
|
|
|
|
|
|
|
_patch_cfg_state(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
world_size=1,
|
|
|
|
|
rank=0,
|
|
|
|
|
fake_group=FakeCfgGroup(world_size=1, rank_in_group=0, rank_tensors=[]),
|
|
|
|
|
)
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
baseline = pipeline.predict_noise_with_multi_branch_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=cfg_scale,
|
|
|
|
|
branches_kwargs=branches_kwargs,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
parallel = _run_multi_branch_cfg_parallel_mock(
|
|
|
|
|
pipeline,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
cfg_world_size=cfg_parallel_size,
|
|
|
|
|
branches_kwargs=branches_kwargs,
|
|
|
|
|
true_cfg_scale=cfg_scale,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
)
|
|
|
|
|
assert baseline is not None
|
|
|
|
|
_assert_cfg_outputs_close(
|
|
|
|
|
parallel,
|
|
|
|
|
baseline,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
msg=(
|
|
|
|
|
f"mock multi-branch CFG parallel differs from sequential "
|
|
|
|
|
f"(n_branches={n_branches}, cfg_parallel_size={cfg_parallel_size})"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
def test_multi_branch_without_cfg():
|
|
|
|
|
dtype = torch.float32
|
|
|
|
|
device = torch.device("cpu")
|
|
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
MultiBranchTestPipeline,
|
|
|
|
|
hidden_dim=128,
|
|
|
|
|
model_seed=42,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
branches_kwargs = _make_multi_branch_inputs(
|
|
|
|
|
n_branches=3,
|
|
|
|
|
batch_size=1,
|
|
|
|
|
channels=4,
|
|
|
|
|
height=16,
|
|
|
|
|
width=16,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=123,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
noise_pred = pipeline.predict_noise_with_multi_branch_cfg(
|
|
|
|
|
do_true_cfg=False,
|
|
|
|
|
true_cfg_scale=5.0,
|
|
|
|
|
branches_kwargs=branches_kwargs,
|
|
|
|
|
cfg_normalize=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert noise_pred is not None
|
|
|
|
|
assert noise_pred.shape == (1, 4, 16, 16)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# GPU nightly: same-seed sequential vs real multi-GPU CFG parallel parity
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _test_cfg_parallel_worker(
|
|
|
|
|
local_rank: int,
|
|
|
|
|
world_size: int,
|
|
|
|
|
cfg_parallel_size: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
test_config: dict,
|
|
|
|
|
result_queue: torch.multiprocessing.Queue,
|
|
|
|
|
):
|
|
|
|
|
device = torch.device(f"{current_omni_platform.device_type}:{local_rank}")
|
|
|
|
|
current_omni_platform.set_device(device)
|
|
|
|
|
_update_environment_variables(
|
|
|
|
|
{
|
|
|
|
|
"RANK": str(local_rank),
|
|
|
|
|
"LOCAL_RANK": str(local_rank),
|
|
|
|
|
"WORLD_SIZE": str(world_size),
|
|
|
|
|
"MASTER_ADDR": "localhost",
|
|
|
|
|
"MASTER_PORT": "29502",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
init_distributed_environment()
|
|
|
|
|
initialize_model_parallel(cfg_parallel_size=cfg_parallel_size)
|
|
|
|
|
assert get_classifier_free_guidance_world_size() == cfg_parallel_size
|
|
|
|
|
|
|
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
TestCFGPipeline,
|
|
|
|
|
hidden_dim=test_config["hidden_dim"],
|
|
|
|
|
model_seed=test_config["model_seed"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
positive_kwargs, negative_kwargs = _make_two_branch_inputs(
|
|
|
|
|
batch_size=test_config["batch_size"],
|
|
|
|
|
channels=test_config["channels"],
|
|
|
|
|
height=test_config["height"],
|
|
|
|
|
width=test_config["width"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=test_config["input_seed"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
noise_pred = pipeline.predict_noise_maybe_with_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=test_config["cfg_scale"],
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=negative_kwargs,
|
|
|
|
|
cfg_normalize=test_config["cfg_normalize"],
|
|
|
|
|
kwargs=test_config["kwargs"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert noise_pred is not None
|
|
|
|
|
result_queue.put((get_classifier_free_guidance_rank(), noise_pred.cpu()))
|
|
|
|
|
destroy_distributed_env()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _test_cfg_sequential_worker(
|
|
|
|
|
local_rank: int,
|
|
|
|
|
world_size: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
test_config: dict,
|
|
|
|
|
result_queue: torch.multiprocessing.Queue,
|
|
|
|
|
):
|
|
|
|
|
device = torch.device(f"{current_omni_platform.device_type}:{local_rank}")
|
|
|
|
|
current_omni_platform.set_device(device)
|
|
|
|
|
_update_environment_variables(
|
|
|
|
|
{
|
|
|
|
|
"RANK": str(local_rank),
|
|
|
|
|
"LOCAL_RANK": str(local_rank),
|
|
|
|
|
"WORLD_SIZE": str(world_size),
|
|
|
|
|
"MASTER_ADDR": "localhost",
|
|
|
|
|
"MASTER_PORT": "29503",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
init_distributed_environment()
|
|
|
|
|
initialize_model_parallel(cfg_parallel_size=1)
|
|
|
|
|
assert get_classifier_free_guidance_world_size() == 1
|
|
|
|
|
|
|
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
TestCFGPipeline,
|
|
|
|
|
hidden_dim=test_config["hidden_dim"],
|
|
|
|
|
model_seed=test_config["model_seed"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
positive_kwargs, negative_kwargs = _make_two_branch_inputs(
|
|
|
|
|
batch_size=test_config["batch_size"],
|
|
|
|
|
channels=test_config["channels"],
|
|
|
|
|
height=test_config["height"],
|
|
|
|
|
width=test_config["width"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=test_config["input_seed"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
noise_pred = pipeline.predict_noise_maybe_with_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=test_config["cfg_scale"],
|
|
|
|
|
positive_kwargs=positive_kwargs,
|
|
|
|
|
negative_kwargs=negative_kwargs,
|
|
|
|
|
cfg_normalize=test_config["cfg_normalize"],
|
|
|
|
|
kwargs=test_config["kwargs"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert noise_pred is not None
|
|
|
|
|
result_queue.put(noise_pred.cpu())
|
|
|
|
|
destroy_distributed_env()
|
|
|
|
|
|
|
|
|
|
|
2026-04-12 22:06:49 +08:00
|
|
|
def _test_multi_branch_parallel_worker(
|
|
|
|
|
local_rank: int,
|
|
|
|
|
world_size: int,
|
|
|
|
|
cfg_parallel_size: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
test_config: dict,
|
|
|
|
|
result_queue: torch.multiprocessing.Queue,
|
|
|
|
|
):
|
|
|
|
|
device = torch.device(f"{current_omni_platform.device_type}:{local_rank}")
|
|
|
|
|
current_omni_platform.set_device(device)
|
2026-07-29 23:53:03 +08:00
|
|
|
_update_environment_variables(
|
2026-04-12 22:06:49 +08:00
|
|
|
{
|
|
|
|
|
"RANK": str(local_rank),
|
|
|
|
|
"LOCAL_RANK": str(local_rank),
|
|
|
|
|
"WORLD_SIZE": str(world_size),
|
|
|
|
|
"MASTER_ADDR": "localhost",
|
|
|
|
|
"MASTER_PORT": "29504",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
init_distributed_environment()
|
|
|
|
|
initialize_model_parallel(cfg_parallel_size=cfg_parallel_size)
|
2026-07-29 23:53:03 +08:00
|
|
|
assert get_classifier_free_guidance_world_size() == cfg_parallel_size
|
2026-04-12 22:06:49 +08:00
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
MultiBranchTestPipeline,
|
2026-04-12 22:06:49 +08:00
|
|
|
hidden_dim=test_config["hidden_dim"],
|
2026-07-29 23:53:03 +08:00
|
|
|
model_seed=test_config["model_seed"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
branches_kwargs = _make_multi_branch_inputs(
|
|
|
|
|
n_branches=test_config["n_branches"],
|
|
|
|
|
batch_size=test_config["batch_size"],
|
|
|
|
|
channels=test_config["channels"],
|
|
|
|
|
height=test_config["height"],
|
|
|
|
|
width=test_config["width"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=test_config["input_seed"],
|
2026-04-12 22:06:49 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
noise_pred = pipeline.predict_noise_with_multi_branch_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=test_config["cfg_scale"],
|
|
|
|
|
branches_kwargs=branches_kwargs,
|
|
|
|
|
cfg_normalize=test_config["cfg_normalize"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert noise_pred is not None
|
2026-07-29 23:53:03 +08:00
|
|
|
result_queue.put((get_classifier_free_guidance_rank(), noise_pred.cpu()))
|
2026-04-12 22:06:49 +08:00
|
|
|
destroy_distributed_env()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _test_multi_branch_sequential_worker(
|
|
|
|
|
local_rank: int,
|
|
|
|
|
world_size: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
test_config: dict,
|
|
|
|
|
result_queue: torch.multiprocessing.Queue,
|
|
|
|
|
):
|
|
|
|
|
device = torch.device(f"{current_omni_platform.device_type}:{local_rank}")
|
|
|
|
|
current_omni_platform.set_device(device)
|
2026-07-29 23:53:03 +08:00
|
|
|
_update_environment_variables(
|
2026-04-12 22:06:49 +08:00
|
|
|
{
|
|
|
|
|
"RANK": str(local_rank),
|
|
|
|
|
"LOCAL_RANK": str(local_rank),
|
|
|
|
|
"WORLD_SIZE": str(world_size),
|
|
|
|
|
"MASTER_ADDR": "localhost",
|
|
|
|
|
"MASTER_PORT": "29505",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
init_distributed_environment()
|
|
|
|
|
initialize_model_parallel(cfg_parallel_size=1)
|
2026-07-29 23:53:03 +08:00
|
|
|
assert get_classifier_free_guidance_world_size() == 1
|
2026-04-12 22:06:49 +08:00
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
pipeline = _build_pipeline_on_device(
|
|
|
|
|
MultiBranchTestPipeline,
|
2026-04-12 22:06:49 +08:00
|
|
|
hidden_dim=test_config["hidden_dim"],
|
2026-07-29 23:53:03 +08:00
|
|
|
model_seed=test_config["model_seed"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
branches_kwargs = _make_multi_branch_inputs(
|
|
|
|
|
n_branches=test_config["n_branches"],
|
|
|
|
|
batch_size=test_config["batch_size"],
|
|
|
|
|
channels=test_config["channels"],
|
|
|
|
|
height=test_config["height"],
|
|
|
|
|
width=test_config["width"],
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
input_seed=test_config["input_seed"],
|
2026-04-12 22:06:49 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
noise_pred = pipeline.predict_noise_with_multi_branch_cfg(
|
|
|
|
|
do_true_cfg=True,
|
|
|
|
|
true_cfg_scale=test_config["cfg_scale"],
|
|
|
|
|
branches_kwargs=branches_kwargs,
|
|
|
|
|
cfg_normalize=test_config["cfg_normalize"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert noise_pred is not None
|
|
|
|
|
result_queue.put(noise_pred.cpu())
|
|
|
|
|
destroy_distributed_env()
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
def _run_two_branch_gpu_parity(
|
|
|
|
|
*,
|
2026-04-12 22:06:49 +08:00
|
|
|
cfg_parallel_size: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
cfg_normalize: bool,
|
2026-07-29 23:53:03 +08:00
|
|
|
extra_kwargs: dict[str, Any] | None,
|
|
|
|
|
) -> None:
|
2026-04-12 22:06:49 +08:00
|
|
|
test_config = {
|
|
|
|
|
"batch_size": batch_size,
|
|
|
|
|
"channels": 4,
|
|
|
|
|
"height": 16,
|
|
|
|
|
"width": 16,
|
|
|
|
|
"hidden_dim": 128,
|
2026-07-29 23:53:03 +08:00
|
|
|
"cfg_scale": 7.5,
|
|
|
|
|
"cfg_normalize": cfg_normalize,
|
|
|
|
|
"model_seed": 42,
|
|
|
|
|
"input_seed": 123,
|
|
|
|
|
"kwargs": extra_kwargs,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mp_context = torch.multiprocessing.get_context("spawn")
|
|
|
|
|
manager = mp_context.Manager()
|
|
|
|
|
baseline_queue = manager.Queue()
|
|
|
|
|
cfg_parallel_queue = manager.Queue()
|
|
|
|
|
|
|
|
|
|
torch.multiprocessing.spawn(
|
|
|
|
|
_test_cfg_sequential_worker,
|
|
|
|
|
args=(1, dtype, test_config, baseline_queue),
|
|
|
|
|
nprocs=1,
|
|
|
|
|
)
|
|
|
|
|
torch.multiprocessing.spawn(
|
|
|
|
|
_test_cfg_parallel_worker,
|
|
|
|
|
args=(cfg_parallel_size, cfg_parallel_size, dtype, test_config, cfg_parallel_queue),
|
|
|
|
|
nprocs=cfg_parallel_size,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
baseline_output = baseline_queue.get()
|
|
|
|
|
cfg_parallel_outputs = [cfg_parallel_queue.get() for _ in range(cfg_parallel_size)]
|
|
|
|
|
cfg_parallel_outputs.sort(key=lambda item: item[0])
|
|
|
|
|
cfg_parallel_output = cfg_parallel_outputs[0][1]
|
|
|
|
|
|
|
|
|
|
for cfg_rank, rank_output in cfg_parallel_outputs[1:]:
|
|
|
|
|
torch.testing.assert_close(
|
|
|
|
|
rank_output,
|
|
|
|
|
cfg_parallel_output,
|
|
|
|
|
rtol=0,
|
|
|
|
|
atol=0,
|
|
|
|
|
msg=f"CFG parallel ranks differ (rank 0 vs rank {cfg_rank})",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_assert_cfg_outputs_close(
|
|
|
|
|
cfg_parallel_output,
|
|
|
|
|
baseline_output,
|
|
|
|
|
dtype=dtype,
|
|
|
|
|
msg=(
|
|
|
|
|
f"GPU CFG parallel output differs from sequential CFG "
|
|
|
|
|
f"(cfg_parallel_size={cfg_parallel_size}, cfg_normalize={cfg_normalize})"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_multi_branch_gpu_parity(
|
|
|
|
|
*,
|
|
|
|
|
cfg_parallel_size: int,
|
|
|
|
|
n_branches: int,
|
|
|
|
|
dtype: torch.dtype,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
) -> None:
|
|
|
|
|
test_config = {
|
|
|
|
|
"batch_size": batch_size,
|
|
|
|
|
"channels": 4,
|
|
|
|
|
"height": 16,
|
|
|
|
|
"width": 16,
|
|
|
|
|
"hidden_dim": 128,
|
|
|
|
|
"cfg_scale": _multi_branch_cfg_scale(n_branches),
|
2026-04-12 22:06:49 +08:00
|
|
|
"cfg_normalize": cfg_normalize,
|
|
|
|
|
"model_seed": 42,
|
|
|
|
|
"input_seed": 123,
|
|
|
|
|
"n_branches": n_branches,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mp_context = torch.multiprocessing.get_context("spawn")
|
|
|
|
|
manager = mp_context.Manager()
|
|
|
|
|
baseline_queue = manager.Queue()
|
|
|
|
|
cfg_parallel_queue = manager.Queue()
|
|
|
|
|
|
|
|
|
|
torch.multiprocessing.spawn(
|
|
|
|
|
_test_multi_branch_sequential_worker,
|
|
|
|
|
args=(1, dtype, test_config, baseline_queue),
|
|
|
|
|
nprocs=1,
|
|
|
|
|
)
|
|
|
|
|
torch.multiprocessing.spawn(
|
|
|
|
|
_test_multi_branch_parallel_worker,
|
|
|
|
|
args=(cfg_parallel_size, cfg_parallel_size, dtype, test_config, cfg_parallel_queue),
|
|
|
|
|
nprocs=cfg_parallel_size,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
baseline_output = baseline_queue.get()
|
|
|
|
|
cfg_parallel_outputs = [cfg_parallel_queue.get() for _ in range(cfg_parallel_size)]
|
|
|
|
|
cfg_parallel_outputs.sort(key=lambda item: item[0])
|
|
|
|
|
cfg_parallel_output = cfg_parallel_outputs[0][1]
|
|
|
|
|
|
|
|
|
|
for cfg_rank, rank_output in cfg_parallel_outputs[1:]:
|
|
|
|
|
torch.testing.assert_close(
|
|
|
|
|
rank_output,
|
|
|
|
|
cfg_parallel_output,
|
|
|
|
|
rtol=0,
|
|
|
|
|
atol=0,
|
|
|
|
|
msg=f"Multi-branch CFG parallel ranks differ (rank 0 vs rank {cfg_rank})",
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
_assert_cfg_outputs_close(
|
2026-04-12 22:06:49 +08:00
|
|
|
cfg_parallel_output,
|
|
|
|
|
baseline_output,
|
2026-07-29 23:53:03 +08:00
|
|
|
dtype=dtype,
|
2026-04-12 22:06:49 +08:00
|
|
|
msg=(
|
2026-07-29 23:53:03 +08:00
|
|
|
f"GPU multi-branch CFG parallel differs from sequential "
|
|
|
|
|
f"(n_branches={n_branches}, cfg_parallel_size={cfg_parallel_size})"
|
2026-04-12 22:06:49 +08:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.full_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.parallel
|
2026-09-14 14:27:10 +08:00
|
|
|
@hardware_test(res={"cuda": ["L4", "B200"]}, num_cards=2)
|
2026-07-29 23:53:03 +08:00
|
|
|
@pytest.mark.parametrize("batch_size", [2])
|
|
|
|
|
@pytest.mark.parametrize("cfg_normalize", [False, True])
|
|
|
|
|
@pytest.mark.parametrize("extra_kwargs", [None, {"step_i": 1}])
|
|
|
|
|
def test_predict_noise_with_cfg_parity(
|
|
|
|
|
batch_size: int,
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
extra_kwargs: dict[str, Any] | None,
|
|
|
|
|
):
|
|
|
|
|
_run_two_branch_gpu_parity(
|
|
|
|
|
cfg_parallel_size=2,
|
|
|
|
|
dtype=torch.bfloat16,
|
|
|
|
|
batch_size=batch_size,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
extra_kwargs=extra_kwargs,
|
2026-04-12 22:06:49 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 23:53:03 +08:00
|
|
|
@pytest.mark.full_model
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.parallel
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"cfg_parallel_size,n_branches",
|
|
|
|
|
[
|
|
|
|
|
pytest.param(2, 2, marks=_L4_TWO_GPU),
|
|
|
|
|
pytest.param(2, 3, marks=_L4_TWO_GPU),
|
|
|
|
|
pytest.param(3, 3, marks=_L4_THREE_GPU),
|
|
|
|
|
pytest.param(2, 4, marks=_L4_TWO_GPU),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
@pytest.mark.parametrize("batch_size", [2])
|
|
|
|
|
@pytest.mark.parametrize("cfg_normalize", [False, True])
|
|
|
|
|
def test_predict_noise_with_multi_branch_cfg_parity(
|
|
|
|
|
cfg_parallel_size: int,
|
|
|
|
|
n_branches: int,
|
|
|
|
|
batch_size: int,
|
|
|
|
|
cfg_normalize: bool,
|
|
|
|
|
):
|
|
|
|
|
_run_multi_branch_gpu_parity(
|
|
|
|
|
cfg_parallel_size=cfg_parallel_size,
|
|
|
|
|
n_branches=n_branches,
|
|
|
|
|
dtype=torch.bfloat16,
|
|
|
|
|
batch_size=batch_size,
|
|
|
|
|
cfg_normalize=cfg_normalize,
|
|
|
|
|
)
|