"""Data utilities for Context Parallel training. Provides helpers for: - Broadcasting tensors across CP ranks. - Splitting temporal tensors by CP rank. - Handling non-divisible temporal lengths via right-padding. - Building frame-valid masks for padded temporal tails. - Reducing loss scalars across CP ranks. """ from __future__ import annotations import torch import torch.distributed as dist from torch import Tensor from torch.distributed import ProcessGroup def _cp_src_global_rank(group: ProcessGroup) -> int: """Global rank of CP-rank-0 in the given process group.""" return dist.get_global_rank(group, 0) def cp_broadcast_tensor( tensor: Tensor, group: ProcessGroup, ) -> Tensor: """In-place broadcast *tensor* from CP-rank-0 to all ranks in *group*.""" src = _cp_src_global_rank(group) dist.broadcast(tensor, src=src, group=group) return tensor def cp_split_temporal( tensor: Tensor, dim: int, group: ProcessGroup, ) -> Tensor: """Slice *tensor* along *dim* to keep only this rank's temporal chunk.""" cp_rank = dist.get_rank(group) cp_world = dist.get_world_size(group) T = tensor.shape[dim] assert T % cp_world == 0, f"Temporal size {T} (dim={dim}) must be divisible by cp_size={cp_world}" chunk = T // cp_world return tensor.narrow(dim, cp_rank * chunk, chunk).contiguous() def cp_right_pad_size(length: int, multiple: int) -> int: """Return right-pad size needed to make ``length`` divisible by ``multiple``.""" if multiple <= 0: raise ValueError(f"multiple must be > 0, got {multiple}") return (-length) % multiple def cp_right_pad_temporal( tensor: Tensor, dim: int, pad_size: int, value: float = 0.0, ) -> Tensor: """Right-pad ``tensor`` along temporal ``dim`` by ``pad_size``.""" if pad_size <= 0: return tensor if dim < 0: dim = tensor.ndim + dim if dim < 0 or dim >= tensor.ndim: raise ValueError(f"Invalid dim={dim} for tensor with ndim={tensor.ndim}") pad_shape = list(tensor.shape) pad_shape[dim] = pad_size pad_tensor = torch.full( pad_shape, fill_value=value, dtype=tensor.dtype, device=tensor.device, ) return torch.cat([tensor, pad_tensor], dim=dim) def cp_build_frame_valid_mask(clean_images: Tensor, pad_frames: int) -> Tensor: """Build ``(B, 1, T, 1, 1)`` frame-valid mask after temporal right-padding.""" if clean_images.ndim < 3: raise ValueError(f"clean_images must have at least 3 dims (B, C, T, ...), got shape={list(clean_images.shape)}") B = clean_images.shape[0] T = clean_images.shape[2] if pad_frames < 0 or pad_frames > T: raise ValueError(f"pad_frames must satisfy 0 <= pad_frames <= T, got pad_frames={pad_frames}, T={T}") mask = torch.ones((B, 1, T, 1, 1), device=clean_images.device, dtype=clean_images.dtype) if pad_frames > 0: mask[:, :, T - pad_frames :, :, :] = 0 return mask def cp_reduce_loss( loss: Tensor, group: ProcessGroup, num_valid_tokens: Tensor | int | float | None = None, ) -> Tensor: """Reduce CP-local loss to a global scalar with correct gradient scaling. This function is autograd-safe for CP: it returns a forward value equal to the CP-global reduced loss while preserving backward gradients scaled by the local contribution ratio. Args: loss: Local scalar loss. group: CP process group. num_valid_tokens: Optional local token count for weighted reduction. If omitted, all ranks are weighted equally. """ if num_valid_tokens is None: loss_avg_detached = loss.detach().clone() dist.all_reduce(loss_avg_detached, op=dist.ReduceOp.SUM, group=group) loss_avg_detached = loss_avg_detached / dist.get_world_size(group) # Keep local backward unchanged; only replace forward scalar for logging. return loss + (loss_avg_detached - loss.detach()) if torch.is_tensor(num_valid_tokens): local_tokens = num_valid_tokens.to(device=loss.device, dtype=loss.dtype) else: local_tokens = torch.tensor(float(num_valid_tokens), device=loss.device, dtype=loss.dtype) world = dist.get_world_size(group) total_tokens = local_tokens.detach().clone() dist.all_reduce(total_tokens, op=dist.ReduceOp.SUM, group=group) total_tokens = total_tokens.clamp_min(1.0) # FSDP2 already averages grads across the CP-enabled sharding mesh. # To obtain weighted global-token gradients, scale by n_i / mean(n). mean_tokens = (total_tokens / world).clamp_min(1.0) loss_for_backward = loss * (local_tokens / mean_tokens) weighted_loss_detached = loss.detach() * local_tokens.detach() dist.all_reduce(weighted_loss_detached, op=dist.ReduceOp.SUM, group=group) loss_avg_detached = weighted_loss_detached / total_tokens return loss_for_backward + (loss_avg_detached - loss_for_backward.detach())