Files
wehub-resource-sync eec33d25b2
pre-commit / pre-commit (push) Failing after 1s
Build Wheel / build (3.11) (push) Failing after 1s
Build Wheel / build (3.12) (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:29:08 +08:00

776 lines
27 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for CFG (Classifier-Free Guidance) parallel functionality.
This test verifies that predict_noise_maybe_with_cfg and
predict_noise_with_multi_branch_cfg produce numerically equivalent results
with and without CFG parallel using fixed random inputs.
"""
import os
import pytest
import torch
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
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
def update_environment_variables(envs_dict: dict[str, str]):
"""Update multiple environment variables."""
for k, v in envs_dict.items():
os.environ[k] = v
class SimpleTransformer(torch.nn.Module):
"""Simple transformer model for testing with random initialization.
Contains:
- Input projection (conv to hidden_dim)
- QKV projection layers
- Self-attention layer
- Output projection
"""
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
assert hidden_dim % num_heads == 0, "hidden_dim must be divisible by num_heads"
# Input projection: (B, C, H, W) -> (B, hidden_dim, H, W)
self.input_proj = torch.nn.Conv2d(in_channels, hidden_dim, 1)
# QKV projection layers
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)
# Output projection after attention
self.out_proj = torch.nn.Linear(hidden_dim, hidden_dim)
# Final output projection: (B, hidden_dim, H, W) -> (B, C, H, W)
self.final_proj = torch.nn.Conv2d(hidden_dim, in_channels, 1)
# Layer norm
self.norm1 = torch.nn.LayerNorm(hidden_dim)
self.norm2 = torch.nn.LayerNorm(hidden_dim)
def forward(self, x: torch.Tensor, **kwargs) -> tuple[torch.Tensor]:
"""Forward pass with self-attention.
Args:
x: Input tensor of shape (B, C, H, W)
Returns:
Output tensor of shape (B, C, H, W)
"""
B, C, H, W = x.shape
# Input projection
x = self.input_proj(x) # (B, hidden_dim, H, W)
# Reshape to sequence: (B, hidden_dim, H, W) -> (B, H*W, hidden_dim)
x = x.flatten(2).transpose(1, 2) # (B, H*W, hidden_dim)
# Self-attention with residual connection
residual = x
x = self.norm1(x)
# QKV projection
q = self.q_proj(x) # (B, H*W, hidden_dim)
k = self.k_proj(x) # (B, H*W, hidden_dim)
v = self.v_proj(x) # (B, H*W, hidden_dim)
# Reshape for multi-head attention: (B, H*W, hidden_dim) -> (B, num_heads, H*W, head_dim)
seq_len = H * W
q = q.view(B, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(B, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(B, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# Scaled dot-product attention
scale = self.head_dim**-0.5
attn_scores = torch.matmul(q, k.transpose(-2, -1)) * scale # (B, num_heads, H*W, H*W)
attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1)
attn_output = torch.matmul(attn_weights, v)
attn_output = attn_output.transpose(1, 2).contiguous().view(B, seq_len, self.hidden_dim)
attn_output = self.out_proj(attn_output)
x = residual + attn_output
residual = x
x = self.norm2(x)
x = residual + x
x = x.transpose(1, 2).view(B, self.hidden_dim, H, W)
out = self.final_proj(x)
return (out,)
class TestCFGPipeline(CFGParallelMixin):
"""Test pipeline using CFGParallelMixin."""
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, seed: int = 42):
# Set seed BEFORE creating transformer to ensure consistent layer initialization
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
self.transformer = SimpleTransformer(in_channels, hidden_dim)
# Re-initialize all parameters with fixed seed for full reproducibility
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
for param in self.transformer.parameters():
torch.nn.init.normal_(param, mean=0.0, std=0.02)
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,
):
"""Worker function for CFG parallel test."""
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)
cfg_rank = get_classifier_free_guidance_rank()
cfg_world_size = get_classifier_free_guidance_world_size()
assert cfg_world_size == cfg_parallel_size
# Create pipeline with same seed to ensure identical model weights across all ranks
# Note: model_seed is set inside TestCFGPipeline.__init__
pipeline = TestCFGPipeline(
in_channels=test_config["channels"],
hidden_dim=test_config["hidden_dim"],
seed=test_config["model_seed"],
)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval() # Set to eval mode for deterministic behavior
# Create fixed inputs with explicit seed setting for reproducibility
# Set both CPU and CUDA seeds to ensure identical inputs across all ranks
torch.manual_seed(test_config["input_seed"])
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"])
batch_size = test_config["batch_size"]
channels = test_config["channels"]
height = test_config["height"]
width = test_config["width"]
# Positive input
positive_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
# Negative input with different seed
torch.manual_seed(test_config["input_seed"] + 1)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"] + 1)
negative_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
# Prepare kwargs for predict_noise_maybe_with_cfg
positive_kwargs = {"x": positive_input}
negative_kwargs = {"x": negative_input}
with torch.no_grad():
# Call predict_noise_maybe_with_cfg
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"],
)
# CFG parallel returns the combined prediction on every rank.
assert noise_pred is not None
result_queue.put((cfg_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,
):
"""Worker function for sequential CFG test (baseline)."""
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) # No CFG parallel
cfg_world_size = get_classifier_free_guidance_world_size()
assert cfg_world_size == 1
# Create pipeline with same seed to ensure identical model weights as CFG parallel
# Note: model_seed is set inside TestCFGPipeline.__init__
pipeline = TestCFGPipeline(
in_channels=test_config["channels"],
hidden_dim=test_config["hidden_dim"],
seed=test_config["model_seed"],
)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
# Create fixed inputs (same seed as CFG parallel to ensure identical inputs)
# Set both CPU and CUDA seeds for full reproducibility
torch.manual_seed(test_config["input_seed"])
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"])
batch_size = test_config["batch_size"]
channels = test_config["channels"]
height = test_config["height"]
width = test_config["width"]
# Positive input
positive_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
# Negative input with different seed
torch.manual_seed(test_config["input_seed"] + 1)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"] + 1)
negative_input = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
positive_kwargs = {"x": positive_input}
negative_kwargs = {"x": negative_input}
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"],
)
# Sequential CFG always returns output
assert noise_pred is not None
result_queue.put(noise_pred.cpu())
destroy_distributed_env()
@pytest.mark.parametrize("cfg_parallel_size", [2])
@pytest.mark.parametrize("dtype", [torch.bfloat16])
@pytest.mark.parametrize("batch_size", [2])
@pytest.mark.parametrize("cfg_normalize", [False, True])
@pytest.mark.parametrize("kwargs", [None, {"step_i": 1}])
def test_predict_noise_maybe_with_cfg(
cfg_parallel_size: int, dtype: torch.dtype, batch_size: int, cfg_normalize: bool, kwargs: dict | None
):
"""
Test that predict_noise_maybe_with_cfg produces identical results
with and without CFG parallel.
Args:
cfg_parallel_size: Number of GPUs for CFG parallel
dtype: Data type for computation
batch_size: Batch size for testing
cfg_normalize: Whether to normalize CFG output
"""
available_gpus = current_omni_platform.get_device_count()
if available_gpus < cfg_parallel_size:
pytest.skip(f"Test requires {cfg_parallel_size} GPUs but only {available_gpus} available")
test_config = {
"batch_size": batch_size,
"channels": 4,
"height": 16,
"width": 16,
"hidden_dim": 128,
"cfg_scale": 7.5,
"cfg_normalize": cfg_normalize,
"model_seed": 42, # Fixed seed for model initialization
"input_seed": 123, # Fixed seed for input generation
"kwargs": kwargs, # Additional kwargs to test passing through CFG parallel
}
mp_context = torch.multiprocessing.get_context("spawn")
manager = mp_context.Manager()
baseline_queue = manager.Queue()
cfg_parallel_queue = manager.Queue()
# Run baseline (sequential CFG) on single GPU
torch.multiprocessing.spawn(
_test_cfg_sequential_worker,
args=(1, dtype, test_config, baseline_queue),
nprocs=1,
)
# Run CFG parallel on multiple GPUs
torch.multiprocessing.spawn(
_test_cfg_parallel_worker,
args=(cfg_parallel_size, cfg_parallel_size, dtype, test_config, cfg_parallel_queue),
nprocs=cfg_parallel_size,
)
# Get results from queues
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 produced different outputs (rank 0 vs rank {cfg_rank})",
)
# Verify shapes match
assert baseline_output.shape == cfg_parallel_output.shape, (
f"Shape mismatch: baseline {baseline_output.shape} vs CFG parallel {cfg_parallel_output.shape}"
)
# Verify numerical equivalence with appropriate tolerances
if dtype == torch.float32:
rtol, atol = 1e-5, 1e-5
elif dtype == torch.bfloat16:
rtol, atol = 1e-2, 1e-2
else:
rtol, atol = 1e-3, 1e-3
torch.testing.assert_close(
cfg_parallel_output,
baseline_output,
rtol=rtol,
atol=atol,
msg=(
f"CFG parallel output differs from sequential CFG\n"
f" dtype={dtype}, batch_size={batch_size}, cfg_normalize={cfg_normalize}\n"
f" Max diff: {(cfg_parallel_output - baseline_output).abs().max().item():.6e}"
),
)
print(
f"✓ Test passed: cfg_size={cfg_parallel_size}, dtype={dtype}, "
f"batch_size={batch_size}, cfg_normalize={cfg_normalize}"
)
@pytest.mark.parametrize("dtype", [torch.bfloat16])
def test_predict_noise_without_cfg(dtype: torch.dtype):
"""
Test predict_noise_maybe_with_cfg when do_true_cfg=False.
When CFG is disabled, only the positive branch should be computed.
This test runs on a single GPU without distributed environment.
"""
available_gpus = current_omni_platform.get_device_count()
if available_gpus < 1:
pytest.skip("Test requires at least 1 GPU")
device = torch.device(f"{current_omni_platform.device_type}:0")
current_omni_platform.set_device(device)
# Create pipeline without distributed environment
pipeline = TestCFGPipeline(in_channels=4, hidden_dim=128, seed=42)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
# Set seed for input generation
torch.manual_seed(123)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(123)
positive_input = torch.randn(1, 4, 16, 16, dtype=dtype, device=device)
with torch.no_grad():
noise_pred = pipeline.predict_noise_maybe_with_cfg(
do_true_cfg=False, # No CFG
true_cfg_scale=7.5,
positive_kwargs={"x": positive_input},
negative_kwargs=None,
cfg_normalize=False,
)
# Should always return output when do_true_cfg=False
assert noise_pred is not None
assert noise_pred.shape == (1, 4, 16, 16)
print(f"✓ Test passed: predict_noise without CFG (dtype={dtype})")
class MultiBranchTestPipeline(CFGParallelMixin):
"""Test pipeline with custom 3-branch combine logic (like OmniGen2)."""
def __init__(self, in_channels: int = 4, hidden_dim: int = 128, seed: int = 42):
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
self.transformer = SimpleTransformer(in_channels, hidden_dim)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
for param in self.transformer.parameters():
torch.nn.init.normal_(param, mean=0.0, std=0.02)
def combine_multi_branch_cfg_noise(self, predictions, true_cfg_scale, cfg_normalize=False):
"""N-branch combine with weighted sum for testing.
- 2-branch: standard CFG formula (true_cfg_scale is float)
- 3-branch: OmniGen2-style dual guidance scale (true_cfg_scale is dict)
- 4-branch: DreamID-style weighted sum (true_cfg_scale is dict)
"""
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
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,
):
"""Worker function for multi-branch CFG parallel test."""
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": "29504",
}
)
init_distributed_environment()
initialize_model_parallel(cfg_parallel_size=cfg_parallel_size)
cfg_rank = get_classifier_free_guidance_rank()
cfg_world_size = get_classifier_free_guidance_world_size()
assert cfg_world_size == cfg_parallel_size
pipeline = MultiBranchTestPipeline(
in_channels=test_config["channels"],
hidden_dim=test_config["hidden_dim"],
seed=test_config["model_seed"],
)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
n_branches = test_config["n_branches"]
batch_size = test_config["batch_size"]
channels = test_config["channels"]
height = test_config["height"]
width = test_config["width"]
# Create N branch inputs with distinct seeds
branches_kwargs = []
for b in range(n_branches):
torch.manual_seed(test_config["input_seed"] + b)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"] + b)
x = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
branches_kwargs.append({"x": x})
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((cfg_rank, noise_pred.cpu()))
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,
):
"""Worker function for sequential multi-branch CFG test (baseline)."""
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": "29505",
}
)
init_distributed_environment()
initialize_model_parallel(cfg_parallel_size=1)
cfg_world_size = get_classifier_free_guidance_world_size()
assert cfg_world_size == 1
pipeline = MultiBranchTestPipeline(
in_channels=test_config["channels"],
hidden_dim=test_config["hidden_dim"],
seed=test_config["model_seed"],
)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
n_branches = test_config["n_branches"]
batch_size = test_config["batch_size"]
channels = test_config["channels"]
height = test_config["height"]
width = test_config["width"]
branches_kwargs = []
for b in range(n_branches):
torch.manual_seed(test_config["input_seed"] + b)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(test_config["input_seed"] + b)
x = torch.randn(batch_size, channels, height, width, dtype=dtype, device=device)
branches_kwargs.append({"x": x})
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()
@pytest.mark.parametrize(
"cfg_parallel_size,n_branches",
[
(2, 2), # 2 branches on 2 GPUs: [[0],[1]]
(2, 3), # 3 branches on 2 GPUs: [[0,2],[1]]
(3, 3), # 3 branches on 3 GPUs: [[0],[1],[2]]
(2, 4), # 4 branches on 2 GPUs: [[0,2],[1,3]]
],
)
@pytest.mark.parametrize("dtype", [torch.bfloat16])
@pytest.mark.parametrize("batch_size", [2])
@pytest.mark.parametrize("cfg_normalize", [False, True])
def test_predict_noise_with_multi_branch_cfg(
cfg_parallel_size: int,
n_branches: int,
dtype: torch.dtype,
batch_size: int,
cfg_normalize: bool,
):
"""
Test that predict_noise_with_multi_branch_cfg produces identical results
with and without CFG parallel for N-branch models.
Args:
cfg_parallel_size: Number of GPUs for CFG parallel
n_branches: Number of CFG branches
dtype: Data type for computation
batch_size: Batch size for testing
cfg_normalize: Whether to normalize CFG output
"""
available_gpus = current_omni_platform.get_device_count()
if available_gpus < cfg_parallel_size:
pytest.skip(f"Test requires {cfg_parallel_size} GPUs but only {available_gpus} available")
if n_branches == 2:
cfg_scale = 5.0
elif n_branches == 3:
cfg_scale = {"text": 5.0, "image": 2.0}
else:
cfg_scale = {"text": 5.0, "image": 2.0, "vid_ref": 1.5}
test_config = {
"batch_size": batch_size,
"channels": 4,
"height": 16,
"width": 16,
"hidden_dim": 128,
"cfg_scale": cfg_scale,
"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()
# Run baseline (sequential, cfgp=1)
torch.multiprocessing.spawn(
_test_multi_branch_sequential_worker,
args=(1, dtype, test_config, baseline_queue),
nprocs=1,
)
# Run CFG parallel
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]
# All ranks should produce identical output
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})",
)
assert baseline_output.shape == cfg_parallel_output.shape, (
f"Shape mismatch: baseline {baseline_output.shape} vs CFG parallel {cfg_parallel_output.shape}"
)
if dtype == torch.float32:
rtol, atol = 1e-5, 1e-5
elif dtype == torch.bfloat16:
rtol, atol = 1e-2, 1e-2
else:
rtol, atol = 1e-3, 1e-3
torch.testing.assert_close(
cfg_parallel_output,
baseline_output,
rtol=rtol,
atol=atol,
msg=(
f"Multi-branch CFG parallel output differs from sequential\n"
f" n_branches={n_branches}, cfg_parallel_size={cfg_parallel_size}\n"
f" dtype={dtype}, cfg_normalize={cfg_normalize}\n"
f" Max diff: {(cfg_parallel_output - baseline_output).abs().max().item():.6e}"
),
)
print(
f"✓ Test passed: multi_branch n_branches={n_branches}, "
f"cfg_size={cfg_parallel_size}, dtype={dtype}, cfg_normalize={cfg_normalize}"
)
@pytest.mark.parametrize("dtype", [torch.bfloat16])
def test_multi_branch_without_cfg(dtype: torch.dtype):
"""
Test predict_noise_with_multi_branch_cfg when do_true_cfg=False.
When CFG is disabled, only the first branch (positive) should be computed.
This test runs on a single GPU without distributed environment.
"""
available_gpus = current_omni_platform.get_device_count()
if available_gpus < 1:
pytest.skip("Test requires at least 1 GPU")
device = torch.device(f"{current_omni_platform.device_type}:0")
current_omni_platform.set_device(device)
pipeline = MultiBranchTestPipeline(in_channels=4, hidden_dim=128, seed=42)
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
pipeline.transformer.eval()
# Create 3 branch inputs (only first should be used)
branches_kwargs = []
for b in range(3):
torch.manual_seed(123 + b)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(123 + b)
x = torch.randn(1, 4, 16, 16, dtype=dtype, device=device)
branches_kwargs.append({"x": x})
with torch.no_grad():
noise_pred = pipeline.predict_noise_with_multi_branch_cfg(
do_true_cfg=False, # No CFG
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)
print(f"✓ Test passed: multi_branch predict_noise without CFG (dtype={dtype})")