[Refactor] Extend CFG Parallel to support 3 or 4 branch dispatch across M GPUs (#2423)

This commit is contained in:
zhou zhuoxin authored and GitHub committed 2026-04-12 22:06:49 +08:00
1 parent ef230ac720
commit 16041ab550
8 files changed
+670 -122

No files matched your search

@@ -9,6 +9,7 @@ from abc import ABCMeta
from typing import Any
import torch
from vllm.logger import init_logger
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
@@ -16,6 +17,8 @@ from vllm_omni.diffusion.distributed.parallel_state import (
get_classifier_free_guidance_world_size,
)
logger = init_logger(__name__)
def _wrap(pred: torch.Tensor | tuple[torch.Tensor, ...]) -> tuple[torch.Tensor, ...]:
"""Normalize prediction to tuple form."""
@@ -32,6 +35,24 @@ def _slice_pred(pred: tuple[torch.Tensor, ...], output_slice: int) -> tuple[torc
return tuple(p[:, :output_slice] for p in pred)
def _dispatch_branches(n_branches: int, n_ranks: int) -> list[list[int]]:
"""
Round-robin dispatch N branches to M ranks.
Rule: branch i → rank (i % n_ranks).
Examples:
_dispatch_branches(3, 2) -> [[0, 2], [1]]
_dispatch_branches(3, 3) -> [[0], [1], [2]]
_dispatch_branches(4, 2) -> [[0, 2], [1, 3]]
_dispatch_branches(4, 4) -> [[0], [1], [2], [3]]
"""
assignments: list[list[int]] = [[] for _ in range(n_ranks)]
for i in range(n_branches):
assignments[i % n_ranks].append(i)
return assignments
class CFGParallelMixin(metaclass=ABCMeta):
"""
Base Mixin class for Diffusion pipelines providing shared CFG methods.
@@ -189,6 +210,165 @@ class CFGParallelMixin(metaclass=ABCMeta):
results.append(comb)
return _unwrap(tuple(results))
# ── N-branch CFG interface (for 3+ branch models) ──
def predict_noise_with_multi_branch_cfg(
self,
do_true_cfg: bool,
true_cfg_scale: float | dict[str, float],
branches_kwargs: list[dict[str, Any]],
cfg_normalize: bool = False,
output_slice: int | None = None,
) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""
Predict noise with N-branch CFG dispatch across M GPUs.
This is the multi-branch counterpart of predict_noise_maybe_with_cfg().
Use this for models with 3 or more CFG branches (e.g., OmniGen2, Bagel,
DreamID). Existing 2-branch models should continue using
predict_noise_maybe_with_cfg().
Args:
do_true_cfg: Whether to apply CFG.
true_cfg_scale: CFG scale factor (passed to combine_multi_branch_cfg_noise).
branches_kwargs: List of N dicts, each containing kwargs for one
predict_noise() call. branches_kwargs[0] is always the
positive/conditional branch.
cfg_normalize: Whether to normalize (passed to combine_multi_branch_cfg_noise).
output_slice: If set, slice each output to [:, :output_slice].
Returns:
Combined noise prediction, identical on all ranks in CFG parallel.
"""
if do_true_cfg:
n_branches = len(branches_kwargs)
cfg_world_size = get_classifier_free_guidance_world_size()
cfg_parallel_ready = cfg_world_size > 1
if cfg_parallel_ready:
return self._predict_multi_branch_parallel(
branches_kwargs,
n_branches,
cfg_world_size,
true_cfg_scale,
cfg_normalize,
output_slice,
)
else:
# Sequential: run all N branches on single device
preds: list[torch.Tensor | tuple[torch.Tensor, ...]] = []
for kw in branches_kwargs:
pred = _wrap(self.predict_noise(**kw))
if output_slice is not None:
pred = _slice_pred(pred, output_slice)
preds.append(_unwrap(pred))
return self.combine_multi_branch_cfg_noise(preds, true_cfg_scale, cfg_normalize)
else:
# No CFG: only compute positive/conditional prediction
pred = self.predict_noise(**branches_kwargs[0])
if output_slice is not None:
pred = _unwrap(_slice_pred(_wrap(pred), output_slice))
return pred
def _predict_multi_branch_parallel(
self,
branches_kwargs: list[dict[str, Any]],
n_branches: int,
cfg_world_size: int,
true_cfg_scale: float,
cfg_normalize: bool,
output_slice: int | None,
) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""Dispatch N branches across M ranks, all_gather, then combine."""
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_world_size > n_branches:
logger.warning_once(
"cfg_parallel_size=%d > n_branches=%d, %d GPU(s) will be idle for CFG",
cfg_world_size,
n_branches,
cfg_world_size - n_branches,
)
# Assign branches to ranks via round-robin
assignments = _dispatch_branches(n_branches, cfg_world_size)
my_branch_ids = assignments[cfg_rank]
max_per_rank = max(len(a) for a in assignments)
# Run assigned branches
my_preds: list[tuple[torch.Tensor, ...]] = []
for bid in my_branch_ids:
pred = _wrap(self.predict_noise(**branches_kwargs[bid]))
if output_slice is not None:
pred = _slice_pred(pred, output_slice)
my_preds.append(pred)
# Idle ranks (cfg_world_size > n_branches) run a forward pass to get the output shape for all_gather.
# Output shape cannot be inferred from kwargs — may be tuple, sliced, etc.
if not my_preds:
pred = _wrap(self.predict_noise(**branches_kwargs[0]))
if output_slice is not None:
pred = _slice_pred(pred, output_slice)
my_preds.append(pred)
# Pad to max_per_rank with zeros so all ranks have same size
ref_pred = my_preds[0]
while len(my_preds) < max_per_rank:
my_preds.append(tuple(torch.zeros_like(t) for t in ref_pred))
# All-gather each output element separately (like predict_noise_maybe_with_cfg)
# For each slot, gather across ranks; then pick valid results by owner_rank
# all_slots[slot][elem_idx] = [rank0_tensor, rank1_tensor, ...]
all_slots: list[list[list[torch.Tensor]]] = []
for slot in range(max_per_rank):
slot_results: list[list[torch.Tensor]] = []
for p in my_preds[slot]:
gathered = cfg_group.all_gather(p, separate_tensors=True)
slot_results.append(gathered)
all_slots.append(slot_results)
# Reconstruct final_preds in branch order
final_preds: list[torch.Tensor | tuple[torch.Tensor, ...]] = []
for bid in range(n_branches):
owner_rank = bid % cfg_world_size
slot_idx = bid // cfg_world_size
elements = tuple(all_slots[slot_idx][elem_idx][owner_rank] for elem_idx in range(len(ref_pred)))
final_preds.append(_unwrap(elements))
return self.combine_multi_branch_cfg_noise(final_preds, true_cfg_scale, cfg_normalize)
def combine_multi_branch_cfg_noise(
self,
predictions: list[torch.Tensor | tuple[torch.Tensor, ...]],
true_cfg_scale: float | dict[str, float],
cfg_normalize: bool = False,
) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""
Combine N branch predictions. Default: standard 2-branch CFG formula.
Override this method for custom multi-branch combine logic.
Args:
predictions: List of N predictions, where predictions[0] is always
the positive/conditional branch.
true_cfg_scale: CFG scale factor (float for 2-branch, dict for multi-branch).
cfg_normalize: Whether to normalize the combined prediction.
Returns:
Combined noise prediction.
"""
positive = _wrap(predictions[0])
negative = _wrap(predictions[1])
results = []
for p, n in zip(positive, negative):
comb = n + true_cfg_scale * (p - n)
if cfg_normalize:
comb = self.cfg_normalize_function(p, comb)
results.append(comb)
return _unwrap(tuple(results))
def predict_noise(self, *args: Any, **kwargs: Any) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""
Forward pass through transformer to predict noise.