Files
hao-ai-lab-FastVideo/fastvideo/distributed/communication_op.py

159 lines
5.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Adapted from https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/distributed/communication_op.py
import torch
from fastvideo.distributed.parallel_state import (get_sp_group, get_sp_world_size, get_tp_group,
model_parallel_is_initialized)
from fastvideo.distributed.utils import (unpad_sequence_tensor, compute_padding_for_sp, pad_sequence_tensor)
from fastvideo.logger import init_logger
logger = init_logger(__name__)
# Track if SP communication has been warmed up
_sp_warmup_done = False
def tensor_model_parallel_all_reduce(input_: torch.Tensor) -> torch.Tensor:
"""All-reduce the input tensor across model parallel group."""
return get_tp_group().all_reduce(input_)
def tensor_model_parallel_all_gather(input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
"""All-gather the input tensor across model parallel group."""
return get_tp_group().all_gather(input_, dim)
# TODO: remove model, make it sequence_parallel
def sequence_model_parallel_all_to_all_4D(input_: torch.Tensor,
scatter_dim: int = 2,
gather_dim: int = 1) -> torch.Tensor:
"""All-to-all communication of 4D tensors (e.g. QKV matrices) across sequence parallel group."""
return get_sp_group().all_to_all_4D(input_, scatter_dim, gather_dim)
def sequence_model_parallel_all_gather(input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
"""All-gather the input tensor across model parallel group."""
return get_sp_group().all_gather(input_, dim)
def sequence_model_parallel_all_gather_with_unpad(input_: torch.Tensor,
original_seq_len: int,
dim: int = -1) -> torch.Tensor:
"""All-gather the input tensor and remove padding.
Args:
input_: Sharded (and possibly padded) tensor to gather
original_seq_len: Original sequence length before padding
dim: Dimension to gather along (default: -1)
Returns:
Tensor: Gathered and unpadded tensor
"""
# First gather across all ranks
gathered = get_sp_group().all_gather(input_, dim)
current_seq_len = gathered.shape[dim]
if current_seq_len > original_seq_len:
gathered = unpad_sequence_tensor(gathered, original_seq_len, seq_dim=dim)
return gathered
def sequence_model_parallel_shard(input_: torch.Tensor, dim: int = 1) -> tuple[torch.Tensor, int]:
"""Shard the input tensor across model parallel group with optional padding.
Args:
input_: Input tensor to shard
dim: Dimension to shard along (default: 1)
Returns:
tuple: (sharded_tensor, original_seq_len)
- sharded_tensor: The sharded (and possibly padded) tensor
- original_seq_len: Original sequence length before padding
"""
sp_world_size = get_sp_world_size()
original_seq_len = input_.shape[dim]
# Compute padding if needed
padded_seq_len, padding_amount = compute_padding_for_sp(original_seq_len, sp_world_size)
# Pad if necessary
if padding_amount > 0:
input_ = pad_sequence_tensor(input_, padded_seq_len, seq_dim=dim)
# Sharding with autograd-aware backward (all-gather in backward).
input_ = get_sp_group().shard(input_, dim=dim, scale_grad=True)
return input_, original_seq_len
def warmup_sequence_parallel_communication(device: torch.device | None = None) -> None:
"""Warmup NCCL communicators for sequence parallel all-to-all operations.
The first NCCL collective operation is slow due to lazy communicator
initialization. This function runs dummy all-to-all operations to
trigger the initialization upfront, before the first real forward pass.
Args:
device: Device to use for warmup tensors. If None, uses CUDA device 0.
"""
global _sp_warmup_done
if _sp_warmup_done:
return
if not model_parallel_is_initialized():
return
sp_world_size = get_sp_world_size()
if sp_world_size <= 1:
_sp_warmup_done = True
return
if device is None:
device = torch.device("cuda")
logger.info("Warming up sequence parallel communication (SP=%d)...", sp_world_size)
# Use small but representative tensor shapes for warmup
# Shape: [batch, seq_len, num_heads, head_dim]
# The all-to-all patterns used in attention:
# 1. scatter_dim=2 (heads), gather_dim=1 (seq) - before attention
# 2. scatter_dim=1 (seq), gather_dim=2 (heads) - after attention
batch_size = 1
seq_len_per_rank = 16 # Small sequence per rank
num_heads = sp_world_size * 4 # Must be divisible by sp_world_size
head_dim = 64
# Create dummy tensor for warmup
dummy = torch.zeros(batch_size, seq_len_per_rank, num_heads, head_dim, device=device, dtype=torch.bfloat16)
# Warmup pattern 1: scatter heads, gather sequence (before attention)
_ = sequence_model_parallel_all_to_all_4D(dummy, scatter_dim=2, gather_dim=1)
# Warmup pattern 2: scatter sequence, gather heads (after attention)
dummy2 = torch.zeros(batch_size,
seq_len_per_rank * sp_world_size,
num_heads // sp_world_size,
head_dim,
device=device,
dtype=torch.bfloat16)
_ = sequence_model_parallel_all_to_all_4D(dummy2, scatter_dim=1, gather_dim=2)
# Warmup all-gather (used for replicated tokens)
dummy3 = torch.zeros(batch_size, 8, num_heads // sp_world_size, head_dim, device=device, dtype=torch.bfloat16)
_ = sequence_model_parallel_all_gather(dummy3, dim=2)
# Synchronize to ensure warmup completes
torch.cuda.synchronize(device)
# Clean up
del dummy, dummy2, dummy3
_sp_warmup_done = True
logger.info("Sequence parallel communication warmup complete.")