mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-27 21:33:02 +03:00
* draft:add neuron as a legit backend
* feat: neuron-specific changes in the pipeline
* tests: eager tests
* draft: start with tp for flux2
* fix: style
* fix:apr_02 beta
* feat:add wan
* fix:pixart
* fix: rewrite flux swiglu activation to avoid gather op in neuron IR
* test: pixart compile mode on neuron
* cleanup & fix style
* merge: another change
* review: cleanup+suggestions
* fix: CIs style
* tests: add test units for tp
* fix: in case of text-encoder(s) on CPU
* review:cleanup+add test
* fix: style
* doc: remove it for now
* Add from_single_file support for SkyReelsV2 and ChronoEdit transformers (#13946)
SkyReels-V2 and ChronoEdit are both built on Wan, and their transformers have
the same keys as WanTransformer3DModel, so they reuse
convert_wan_transformer_to_diffusers (like WanVACE / WanAnimate). This lets the
community GGUF builds load directly.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* multi-GPU VAE Fix for Cosmos 3 (#13924)
fix(cosmos3): pin VAE latent norm buffers to encode output device
Under sharded placement (device_map="balanced"), vae.encode() runs on the
VAE's own device while the mean/inv_std buffers were pinned to x.device,
causing a cross-device RuntimeError. Compute raw_mu first, then pin the
normalization buffers to its device so all tensors share one device.
Co-authored-by: Atharva Joshi <atjoshi@smc521ge-0036.ipp2a2.colossus.nvidia.com>
Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
* docs: fix repeated word typo in set_timesteps docstring (#13876)
* docs: fix repeated word typo in set_timesteps docstring
Removed the duplicate word "schedule" from the docstring for the sigmas argument in EulerDiscreteScheduler.set_timesteps.
* Update scheduling_euler_discrete.py
* Apply style fixes
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* clean some stuff to simplify code.
* clean more to remove permutation related shenanigans.
* revert: put torch.chunk back
* Update docs/source/en/training/distributed_inference.md
Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
* Address Sayak's TP review nits (docstrings + stale comments)
- _modeling_parallel: fix stale comment claiming attention processors read
tp_degree at runtime (no longer true after the PackedColwise/Rowwise
refactor; processors are TP-agnostic via head_dim).
- tensor_parallel: single backticks instead of double in docstrings, add
input/output examples to _blocks_to_block_sizes and _resolve_tp_plan, and
drop the stale "applies the Flux2 fused-weight permutations" line from
apply_tensor_parallel (permuters were removed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix stale permuter reference in Flux2ParallelSelfAttnProcessor comment
The proportional QKV/MLP slice is now guaranteed by PackedColwiseParallel
block-by-block sharding, not the removed fused-weight permuter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Derive TP backend inside apply_tensor_parallel from the mesh device type
Drops the `backend` parameter: apply_tensor_parallel already holds the TP
mesh, so it derives neuron-vs-default from `config._mesh.device_type` itself.
The caller no longer computes it. Documents why torch_device can't be used
(Neuron reports as "cpu" via torch's accelerator API). Removes the now-unused
is_torch_neuronx_available import from modeling_utils.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Simplify TP backend derivation to the mesh device type alone
Drop the redundant is_torch_neuronx_available() guard: a DeviceMesh("neuron", ...)
only exists when running on Neuron, so tp_mesh.device_type is the single source
of truth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Remove incorrect "Neuron reports as cpu" rationale from TP backend docstring
Verified on the current native torch_neuronx drop that Neuron does surface as
the torch accelerator (torch._C._get_accelerator().type == "neuron",
get_device() == "neuron"); the old "reports as cpu" claim was stale. The
mesh-device-type derivation is unchanged — it's the device being sharded onto.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Drop stale "XLA" labels from Neuron device/runtime comments
Neuron is a native torch backend now (not XLA-routed): remove the "(XLA)"
parenthetical from the randn-on-CPU note in torch_utils and the "XLA runtime"
mention from the Neuron TP test docstring. Behavior unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* make style: reflow tensor_parallel docstrings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Revert "revert: put torch.chunk back"
This reverts commit 7ea75f771a.
* fix: sharding for neuron + validate flux 1
* fix: change for check_repository_consistency
* feat: support qwen image as well
* Update docs/source/en/training/distributed_inference.md
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
* review: keep bria and nucleusmoe out
* review: keep bria and nucleusmoe out
* review: keep bria and nucleusmoe out
* review: address agent review
* review: apply suggestions
* Update docs/source/en/training/distributed_inference.md
Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
* review: change example tp size to 4
* review: add flux tp test
* review: address comments
* review: improve the doc
* review:apply qwen rotary_emb per device
* review:remove redundant device def after PR #14383
* review: doc title suggestion
* review: restore copy comment
* review: remove cp divisibility explanation from doc on parallelism guide
* fix style
---------
Co-authored-by: Hz_Zhang <47402297+HaozheZhang6@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Atharva Joshi <atjoshi@nvidia.com>
Co-authored-by: Atharva Joshi <atjoshi@smc521ge-0036.ipp2a2.colossus.nvidia.com>
Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
Co-authored-by: Ramkumar R <ramkumarashvanth09@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
613 lines
23 KiB
Python
613 lines
23 KiB
Python
# coding=utf-8
|
|
# Copyright 2026 HuggingFace Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import os
|
|
import socket
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.distributed as dist
|
|
import torch.multiprocessing as mp
|
|
|
|
from diffusers.models._modeling_parallel import ContextParallelConfig, TensorParallelConfig
|
|
from diffusers.models.attention_dispatch import AttentionBackendName, _AttentionBackendRegistry
|
|
|
|
from ...testing_utils import (
|
|
is_attention,
|
|
is_context_parallel,
|
|
is_kernels_available,
|
|
is_tensor_parallel,
|
|
require_torch_multi_accelerator,
|
|
torch_device,
|
|
)
|
|
from .utils import _maybe_cast_to_bf16
|
|
|
|
|
|
# Device configuration mapping
|
|
DEVICE_CONFIG = {
|
|
"cuda": {"backend": "nccl", "module": torch.cuda},
|
|
"xpu": {"backend": "xccl", "module": torch.xpu},
|
|
}
|
|
|
|
|
|
def _find_free_port():
|
|
"""Find a free port on localhost."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("", 0))
|
|
s.listen(1)
|
|
port = s.getsockname()[1]
|
|
return port
|
|
|
|
|
|
def _context_parallel_worker(
|
|
rank,
|
|
world_size,
|
|
master_port,
|
|
model_class,
|
|
init_dict,
|
|
cp_dict,
|
|
inputs_dict,
|
|
return_dict,
|
|
attention_backend=None,
|
|
state_dict=None,
|
|
):
|
|
"""Worker function for context parallel testing."""
|
|
try:
|
|
# Set up distributed environment
|
|
os.environ["MASTER_ADDR"] = "localhost"
|
|
os.environ["MASTER_PORT"] = str(master_port)
|
|
os.environ["RANK"] = str(rank)
|
|
os.environ["WORLD_SIZE"] = str(world_size)
|
|
|
|
# Get device configuration
|
|
device_config = DEVICE_CONFIG.get(torch_device, DEVICE_CONFIG["cuda"])
|
|
backend = device_config["backend"]
|
|
device_module = device_config["module"]
|
|
|
|
# Initialize process group
|
|
dist.init_process_group(backend=backend, rank=rank, world_size=world_size)
|
|
|
|
# Set device for this process
|
|
device_module.set_device(rank)
|
|
device = torch.device(f"{torch_device}:{rank}")
|
|
|
|
# Create model
|
|
model = model_class(**init_dict)
|
|
if state_dict is not None:
|
|
model.load_state_dict(state_dict)
|
|
model.to(device)
|
|
model.eval()
|
|
|
|
# Cast as needed.
|
|
model, inputs_dict = _maybe_cast_to_bf16(attention_backend, model, inputs_dict)
|
|
|
|
# Move inputs to device
|
|
inputs_on_device = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
|
|
# Enable attention backend
|
|
if attention_backend:
|
|
model.set_attention_backend(attention_backend)
|
|
|
|
# Enable context parallelism
|
|
cp_config = ContextParallelConfig(**cp_dict)
|
|
model.enable_parallelism(config=cp_config)
|
|
|
|
# Run forward pass
|
|
with torch.no_grad():
|
|
output = model(**inputs_on_device, return_dict=False)[0]
|
|
|
|
# Only rank 0 reports results
|
|
if rank == 0:
|
|
return_dict["status"] = "success"
|
|
return_dict["output_shape"] = list(output.shape)
|
|
if state_dict is not None:
|
|
# Serialise via nested list so the manager dict can transport it across processes.
|
|
return_dict["output"] = output.cpu().tolist()
|
|
|
|
except Exception as e:
|
|
if rank == 0:
|
|
return_dict["status"] = "error"
|
|
return_dict["error"] = str(e)
|
|
finally:
|
|
if dist.is_initialized():
|
|
dist.destroy_process_group()
|
|
|
|
|
|
def _context_parallel_backward_worker(
|
|
rank, world_size, master_port, model_class, init_dict, cp_dict, inputs_dict, return_dict
|
|
):
|
|
"""Worker function for context parallel backward pass testing."""
|
|
try:
|
|
# Set up distributed environment
|
|
os.environ["MASTER_ADDR"] = "localhost"
|
|
os.environ["MASTER_PORT"] = str(master_port)
|
|
os.environ["RANK"] = str(rank)
|
|
os.environ["WORLD_SIZE"] = str(world_size)
|
|
|
|
# Get device configuration
|
|
device_config = DEVICE_CONFIG.get(torch_device, DEVICE_CONFIG["cuda"])
|
|
backend = device_config["backend"]
|
|
device_module = device_config["module"]
|
|
|
|
# Initialize process group
|
|
dist.init_process_group(backend=backend, rank=rank, world_size=world_size)
|
|
|
|
# Set device for this process
|
|
device_module.set_device(rank)
|
|
device = torch.device(f"{torch_device}:{rank}")
|
|
|
|
# Create model in training mode
|
|
model = model_class(**init_dict)
|
|
model.to(device)
|
|
model.train()
|
|
|
|
# Move inputs to device
|
|
inputs_on_device = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
|
|
# Enable context parallelism
|
|
cp_config = ContextParallelConfig(**cp_dict)
|
|
model.enable_parallelism(config=cp_config)
|
|
|
|
# Run forward and backward pass
|
|
output = model(**inputs_on_device, return_dict=False)[0]
|
|
loss = output.sum()
|
|
loss.backward()
|
|
|
|
# Check that backward actually produced at least one valid gradient
|
|
grads = [p.grad for p in model.parameters() if p.requires_grad and p.grad is not None]
|
|
has_valid_grads = len(grads) > 0 and all(torch.isfinite(g).all() for g in grads)
|
|
|
|
# Only rank 0 reports results
|
|
if rank == 0:
|
|
return_dict["status"] = "success"
|
|
return_dict["has_valid_grads"] = bool(has_valid_grads)
|
|
|
|
except Exception as e:
|
|
if rank == 0:
|
|
return_dict["status"] = "error"
|
|
return_dict["error"] = str(e)
|
|
finally:
|
|
if dist.is_initialized():
|
|
dist.destroy_process_group()
|
|
|
|
|
|
def _custom_mesh_worker(
|
|
rank,
|
|
world_size,
|
|
master_port,
|
|
model_class,
|
|
init_dict,
|
|
cp_dict,
|
|
mesh_shape,
|
|
mesh_dim_names,
|
|
inputs_dict,
|
|
return_dict,
|
|
):
|
|
"""Worker function for context parallel testing with a user-provided custom DeviceMesh."""
|
|
try:
|
|
os.environ["MASTER_ADDR"] = "localhost"
|
|
os.environ["MASTER_PORT"] = str(master_port)
|
|
os.environ["RANK"] = str(rank)
|
|
os.environ["WORLD_SIZE"] = str(world_size)
|
|
|
|
# Get device configuration
|
|
device_config = DEVICE_CONFIG.get(torch_device, DEVICE_CONFIG["cuda"])
|
|
backend = device_config["backend"]
|
|
device_module = device_config["module"]
|
|
|
|
dist.init_process_group(backend=backend, rank=rank, world_size=world_size)
|
|
|
|
# Set device for this process
|
|
device_module.set_device(rank)
|
|
device = torch.device(f"{torch_device}:{rank}")
|
|
|
|
model = model_class(**init_dict)
|
|
model.to(device)
|
|
model.eval()
|
|
|
|
inputs_on_device = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
|
|
# DeviceMesh must be created after init_process_group, inside each worker process.
|
|
mesh = torch.distributed.device_mesh.init_device_mesh(
|
|
torch_device, mesh_shape=mesh_shape, mesh_dim_names=mesh_dim_names
|
|
)
|
|
cp_config = ContextParallelConfig(**cp_dict, mesh=mesh)
|
|
model.enable_parallelism(config=cp_config)
|
|
|
|
with torch.no_grad():
|
|
output = model(**inputs_on_device, return_dict=False)[0]
|
|
|
|
if rank == 0:
|
|
return_dict["status"] = "success"
|
|
return_dict["output_shape"] = list(output.shape)
|
|
|
|
except Exception as e:
|
|
if rank == 0:
|
|
return_dict["status"] = "error"
|
|
return_dict["error"] = str(e)
|
|
finally:
|
|
if dist.is_initialized():
|
|
dist.destroy_process_group()
|
|
|
|
|
|
def _tensor_parallel_worker(
|
|
rank, world_size, master_port, model_class, init_dict, inputs_dict, return_dict, state_dict
|
|
):
|
|
"""Worker function for tensor parallel inference testing.
|
|
|
|
Each rank builds the (identical, `state_dict`-loaded) model, sets up the accelerator device, shards the model
|
|
with `enable_parallelism(config=TensorParallelConfig(tp_degree=world_size))` and runs a forward pass. Rank 0
|
|
reports its output so the caller can compare it against a single-device reference (TP is mathematically equivalent
|
|
to the unsharded model up to floating-point reduction order).
|
|
"""
|
|
try:
|
|
os.environ["MASTER_ADDR"] = "localhost"
|
|
os.environ["MASTER_PORT"] = str(master_port)
|
|
os.environ["RANK"] = str(rank)
|
|
os.environ["WORLD_SIZE"] = str(world_size)
|
|
|
|
device_config = DEVICE_CONFIG.get(torch_device, DEVICE_CONFIG["cuda"])
|
|
backend = device_config["backend"]
|
|
device_module = device_config["module"]
|
|
|
|
dist.init_process_group(backend=backend, rank=rank, world_size=world_size)
|
|
|
|
device_module.set_device(rank)
|
|
device = torch.device(f"{torch_device}:{rank}")
|
|
|
|
model = model_class(**init_dict)
|
|
model.load_state_dict(state_dict)
|
|
model.to(device)
|
|
model.eval()
|
|
|
|
inputs_on_device = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
|
|
# Shard the model across all ranks; the device mesh is built from `tp_degree` on the active accelerator.
|
|
model.enable_parallelism(config=TensorParallelConfig(tp_degree=world_size))
|
|
|
|
with torch.no_grad():
|
|
output = model(**inputs_on_device, return_dict=False)[0]
|
|
|
|
if rank == 0:
|
|
return_dict["status"] = "success"
|
|
return_dict["output_shape"] = list(output.shape)
|
|
# Serialise via nested list so the manager dict can transport it across processes.
|
|
return_dict["output"] = output.float().cpu().tolist()
|
|
|
|
except Exception as e:
|
|
if rank == 0:
|
|
return_dict["status"] = "error"
|
|
return_dict["error"] = str(e)
|
|
finally:
|
|
if dist.is_initialized():
|
|
dist.destroy_process_group()
|
|
|
|
|
|
@is_tensor_parallel
|
|
@require_torch_multi_accelerator
|
|
class TensorParallelTesterMixin:
|
|
def test_tensor_parallel_inference(self, batch_size: int = 1):
|
|
if not torch.distributed.is_available():
|
|
pytest.skip("torch.distributed is not available.")
|
|
|
|
if getattr(self.model_class, "_tp_plan", None) is None:
|
|
pytest.skip("Model does not define a `_tp_plan` for tensor parallel inference.")
|
|
|
|
world_size = 2
|
|
init_dict = self.get_init_dict()
|
|
num_heads = init_dict.get("num_attention_heads")
|
|
if num_heads is not None and num_heads % world_size != 0:
|
|
pytest.skip(f"`num_attention_heads` ({num_heads}) is not divisible by tp_degree ({world_size}).")
|
|
|
|
inputs_dict = self.get_dummy_inputs(batch_size=batch_size)
|
|
|
|
# Single-device reference
|
|
model = self.model_class(**init_dict).eval().to(torch_device)
|
|
state_dict = {k: v.cpu() for k, v in model.state_dict().items()}
|
|
with torch.no_grad():
|
|
ref_output = model(**inputs_dict, return_dict=False)[0].float().cpu()
|
|
|
|
# Move all tensors to CPU for multiprocessing
|
|
inputs_dict = {k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
|
|
master_port = _find_free_port()
|
|
manager = mp.Manager()
|
|
return_dict = manager.dict()
|
|
|
|
mp.spawn(
|
|
_tensor_parallel_worker,
|
|
args=(world_size, master_port, self.model_class, init_dict, inputs_dict, return_dict, state_dict),
|
|
nprocs=world_size,
|
|
join=True,
|
|
)
|
|
|
|
assert return_dict.get("status") == "success", (
|
|
f"Tensor parallel inference failed: {return_dict.get('error', 'Unknown error')}"
|
|
)
|
|
|
|
tp_output = torch.tensor(return_dict["output"])
|
|
# Sharded matmuls + all-reduce reorder the summation, so allow a small tolerance over the reference.
|
|
torch.testing.assert_close(ref_output, tp_output, atol=1e-3, rtol=1e-3)
|
|
|
|
def test_tensor_parallel_batch_inputs(self):
|
|
self.test_tensor_parallel_inference(batch_size=2)
|
|
|
|
|
|
@is_context_parallel
|
|
@require_torch_multi_accelerator
|
|
class ContextParallelTesterMixin:
|
|
@pytest.mark.parametrize("cp_type", ["ulysses_degree", "ring_degree"], ids=["ulysses", "ring"])
|
|
def test_context_parallel_inference(self, cp_type, batch_size: int = 1):
|
|
if not torch.distributed.is_available():
|
|
pytest.skip("torch.distributed is not available.")
|
|
|
|
if not hasattr(self.model_class, "_cp_plan") or self.model_class._cp_plan is None:
|
|
pytest.skip("Model does not have a _cp_plan defined for context parallel inference.")
|
|
|
|
if cp_type == "ring_degree":
|
|
active_backend, _ = _AttentionBackendRegistry.get_active_backend()
|
|
if active_backend == AttentionBackendName.NATIVE:
|
|
pytest.skip("Ring attention is not supported with the native attention backend.")
|
|
|
|
world_size = 2
|
|
init_dict = self.get_init_dict()
|
|
inputs_dict = self.get_dummy_inputs(batch_size=batch_size)
|
|
|
|
# Single-GPU reference
|
|
model = self.model_class(**init_dict).eval().to(torch_device)
|
|
state_dict = {k: v.cpu() for k, v in model.state_dict().items()}
|
|
with torch.no_grad():
|
|
ref_output = model(**inputs_dict, return_dict=False)[0].cpu()
|
|
|
|
# Move all tensors to CPU for multiprocessing
|
|
inputs_dict = {k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
cp_dict = {cp_type: world_size}
|
|
|
|
# Find a free port for distributed communication
|
|
master_port = _find_free_port()
|
|
|
|
# Use multiprocessing manager for cross-process communication
|
|
manager = mp.Manager()
|
|
return_dict = manager.dict()
|
|
|
|
# Spawn worker processes
|
|
mp.spawn(
|
|
_context_parallel_worker,
|
|
args=(
|
|
world_size,
|
|
master_port,
|
|
self.model_class,
|
|
init_dict,
|
|
cp_dict,
|
|
inputs_dict,
|
|
return_dict,
|
|
None,
|
|
state_dict,
|
|
),
|
|
nprocs=world_size,
|
|
join=True,
|
|
)
|
|
|
|
assert return_dict.get("status") == "success", (
|
|
f"Context parallel inference failed: {return_dict.get('error', 'Unknown error')}"
|
|
)
|
|
|
|
cp_output = torch.tensor(return_dict["output"])
|
|
torch.testing.assert_close(ref_output, cp_output, atol=1e-4, rtol=1e-4)
|
|
|
|
@pytest.mark.parametrize("cp_type", ["ulysses_degree", "ring_degree"], ids=["ulysses", "ring"])
|
|
def test_context_parallel_batch_inputs(self, cp_type):
|
|
self.test_context_parallel_inference(cp_type, batch_size=2)
|
|
|
|
@pytest.mark.parametrize("cp_type", ["ulysses_degree", "ring_degree"], ids=["ulysses", "ring"])
|
|
def test_context_parallel_backward(self, cp_type, batch_size: int = 1):
|
|
if not torch.distributed.is_available():
|
|
pytest.skip("torch.distributed is not available.")
|
|
|
|
if not hasattr(self.model_class, "_cp_plan") or self.model_class._cp_plan is None:
|
|
pytest.skip("Model does not have a _cp_plan defined for context parallel inference.")
|
|
|
|
if cp_type == "ring_degree":
|
|
active_backend, _ = _AttentionBackendRegistry.get_active_backend()
|
|
if active_backend == AttentionBackendName.NATIVE:
|
|
pytest.skip("Ring attention is not supported with the native attention backend.")
|
|
|
|
world_size = 2
|
|
init_dict = self.get_init_dict()
|
|
inputs_dict = self.get_dummy_inputs(batch_size=batch_size)
|
|
|
|
# Move all tensors to CPU for multiprocessing
|
|
inputs_dict = {k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
cp_dict = {cp_type: world_size}
|
|
|
|
# Find a free port for distributed communication
|
|
master_port = _find_free_port()
|
|
|
|
# Use multiprocessing manager for cross-process communication
|
|
manager = mp.Manager()
|
|
return_dict = manager.dict()
|
|
|
|
# Spawn worker processes
|
|
mp.spawn(
|
|
_context_parallel_backward_worker,
|
|
args=(world_size, master_port, self.model_class, init_dict, cp_dict, inputs_dict, return_dict),
|
|
nprocs=world_size,
|
|
join=True,
|
|
)
|
|
|
|
assert return_dict.get("status") == "success", (
|
|
f"Context parallel backward pass failed: {return_dict.get('error', 'Unknown error')}"
|
|
)
|
|
assert return_dict.get("has_valid_grads"), "Context parallel backward pass did not produce valid gradients."
|
|
|
|
@pytest.mark.parametrize("cp_type", ["ulysses_degree", "ring_degree"], ids=["ulysses", "ring"])
|
|
def test_context_parallel_backward_batch_inputs(self, cp_type):
|
|
self.test_context_parallel_backward(cp_type, batch_size=2)
|
|
|
|
@pytest.mark.parametrize(
|
|
"cp_type,mesh_shape,mesh_dim_names",
|
|
[
|
|
("ring_degree", (2, 1, 1), ("ring", "ulysses", "fsdp")),
|
|
("ulysses_degree", (1, 2, 1), ("ring", "ulysses", "fsdp")),
|
|
],
|
|
ids=["ring-3d-fsdp", "ulysses-3d-fsdp"],
|
|
)
|
|
def test_context_parallel_custom_mesh(self, cp_type, mesh_shape, mesh_dim_names):
|
|
if not torch.distributed.is_available():
|
|
pytest.skip("torch.distributed is not available.")
|
|
|
|
if not hasattr(self.model_class, "_cp_plan") or self.model_class._cp_plan is None:
|
|
pytest.skip("Model does not have a _cp_plan defined for context parallel inference.")
|
|
|
|
if cp_type == "ring_degree":
|
|
active_backend, _ = _AttentionBackendRegistry.get_active_backend()
|
|
if active_backend == AttentionBackendName.NATIVE:
|
|
pytest.skip("Ring attention is not supported with the native attention backend.")
|
|
|
|
world_size = 2
|
|
init_dict = self.get_init_dict()
|
|
inputs_dict = {k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in self.get_dummy_inputs().items()}
|
|
cp_dict = {cp_type: world_size}
|
|
|
|
master_port = _find_free_port()
|
|
manager = mp.Manager()
|
|
return_dict = manager.dict()
|
|
|
|
mp.spawn(
|
|
_custom_mesh_worker,
|
|
args=(
|
|
world_size,
|
|
master_port,
|
|
self.model_class,
|
|
init_dict,
|
|
cp_dict,
|
|
mesh_shape,
|
|
mesh_dim_names,
|
|
inputs_dict,
|
|
return_dict,
|
|
),
|
|
nprocs=world_size,
|
|
join=True,
|
|
)
|
|
|
|
assert return_dict.get("status") == "success", (
|
|
f"Custom mesh context parallel inference failed: {return_dict.get('error', 'Unknown error')}"
|
|
)
|
|
|
|
|
|
@is_attention
|
|
@is_context_parallel
|
|
@require_torch_multi_accelerator
|
|
class ContextParallelAttentionBackendsTesterMixin:
|
|
unsupported_attn_backends: list[str] = []
|
|
|
|
@pytest.mark.parametrize("cp_type", ["ulysses_degree", "ring_degree"])
|
|
@pytest.mark.parametrize(
|
|
"attention_backend",
|
|
[
|
|
"native",
|
|
pytest.param(
|
|
"flash_hub",
|
|
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
|
|
),
|
|
pytest.param(
|
|
"flash_varlen_hub",
|
|
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
|
|
),
|
|
pytest.param(
|
|
"_flash_3_hub",
|
|
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
|
|
),
|
|
pytest.param(
|
|
"_flash_3_varlen_hub",
|
|
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("ulysses_anything", [True, False])
|
|
@torch.no_grad()
|
|
def test_context_parallel_attn_backend_inference(self, cp_type, attention_backend, ulysses_anything):
|
|
if not torch.distributed.is_available():
|
|
pytest.skip("torch.distributed is not available.")
|
|
|
|
if getattr(self.model_class, "_cp_plan", None) is None:
|
|
pytest.skip("Model does not have a _cp_plan defined for context parallel inference.")
|
|
|
|
if attention_backend in self.unsupported_attn_backends:
|
|
pytest.skip(f"{attention_backend} is not supported for this model.")
|
|
|
|
if cp_type == "ring_degree":
|
|
if attention_backend == AttentionBackendName.NATIVE:
|
|
pytest.skip("Skipping test because ring isn't supported with native attention backend.")
|
|
elif attention_backend in ("flash_varlen_hub", "_flash_3_varlen_hub"):
|
|
pytest.skip("`ring_degree` is not yet supported for varlen attention hub kernels.")
|
|
|
|
if ulysses_anything and "ulysses" not in cp_type:
|
|
pytest.skip("Skipping test as ulysses anything needs the ulysses degree set.")
|
|
|
|
world_size = 2
|
|
init_dict = self.get_init_dict()
|
|
inputs_dict = self.get_dummy_inputs()
|
|
|
|
# Single-GPU reference with the same attention backend (no context parallel)
|
|
model = self.model_class(**init_dict).eval().to(torch_device)
|
|
if attention_backend:
|
|
model.set_attention_backend(attention_backend)
|
|
|
|
# Copy inputs and cast model + inputs as needed
|
|
ref_inputs = inputs_dict.copy()
|
|
model, ref_inputs = _maybe_cast_to_bf16(attention_backend, model, ref_inputs)
|
|
state_dict = {k: v.cpu() for k, v in model.state_dict().items()}
|
|
with torch.no_grad():
|
|
ref_output = model(**ref_inputs, return_dict=False)[0].cpu()
|
|
|
|
# Move all tensors to CPU for multiprocessing
|
|
inputs_dict = {k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in inputs_dict.items()}
|
|
cp_dict = {cp_type: world_size}
|
|
if ulysses_anything:
|
|
cp_dict.update({"ulysses_anything": ulysses_anything})
|
|
|
|
# Find a free port for distributed communication
|
|
master_port = _find_free_port()
|
|
|
|
# Use multiprocessing manager for cross-process communication
|
|
manager = mp.Manager()
|
|
return_dict = manager.dict()
|
|
|
|
# Spawn worker processes
|
|
mp.spawn(
|
|
_context_parallel_worker,
|
|
args=(
|
|
world_size,
|
|
master_port,
|
|
self.model_class,
|
|
init_dict,
|
|
cp_dict,
|
|
inputs_dict,
|
|
return_dict,
|
|
attention_backend,
|
|
state_dict,
|
|
),
|
|
nprocs=world_size,
|
|
join=True,
|
|
)
|
|
|
|
assert return_dict.get("status") == "success", (
|
|
f"Context parallel inference failed: {return_dict.get('error', 'Unknown error')}"
|
|
)
|
|
|
|
cp_output = torch.tensor(return_dict["output"], dtype=ref_output.dtype)
|
|
torch.testing.assert_close(ref_output, cp_output, atol=1e-2, rtol=1e-2)
|