File size: 9,822 Bytes
5c93746 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | """
Utility functions and constants for communication operations.
This module provides utility functions and constants used across the communication module.
"""
import torch
import torch.distributed as dist
from typing import List, Tuple, Optional
import time
import logging
# Communication tags for different types of data
class CommunicationTags:
"""Constants for communication tags."""
LATENT_HDR = 11001
LATENT_PAY = 11002
START_END_STEP = 11003
PATCHED_X_SHAPE = 11004
LATENT_ORIGIN_HDR = 11005
LATENT_ORIGIN_PAY = 11006
KV_CACHE_K = 11007
KV_CACHE_V = 11008
KV_CACHE_GLOBAL_END = 11009
KV_CACHE_LOCAL_END = 11010
BLOCK_INTERVALS = 11011
PERFORMANCE_METRICS = 11012
UPDATED_PROMPT_LENGTH = 11013
UPDATED_PROMPT = 11014
def init_distributed():
"""
Initialize distributed communication.
This function initializes the distributed process group if not already initialized.
"""
if not dist.is_initialized():
backend = "nccl"
dist.init_process_group(backend=backend)
def get_rank_info() -> Tuple[int, int]:
"""
Get current rank and world size.
Returns:
Tuple of (rank, world_size)
"""
if not dist.is_initialized():
raise RuntimeError("Distributed not initialized")
return dist.get_rank(), dist.get_world_size()
def get_next_rank(rank: int, world_size: int) -> int:
"""
Get the next rank in the ring topology.
Args:
rank: Current rank
world_size: Total number of ranks
Returns:
Next rank in the ring
"""
return (rank + 1) % world_size
def get_prev_rank(rank: int, world_size: int) -> int:
"""
Get the previous rank in the ring topology.
Args:
rank: Current rank
world_size: Total number of ranks
Returns:
Previous rank in the ring
"""
return (rank - 1) % world_size
def create_tensor_header(shape: Tuple[int, ...], dtype: torch.dtype,
chunk_idx: int, device: torch.device) -> torch.Tensor:
"""
Create a header tensor for communication.
Args:
shape: Shape of the tensor to be sent
dtype: Data type of the tensor
chunk_idx: Chunk index
device: Device where the header will be created
Returns:
Header tensor containing metadata
"""
header_data = [chunk_idx] + list(shape)
return torch.tensor(header_data, dtype=torch.int64, device=device)
def parse_tensor_header(header: torch.Tensor) -> Tuple[int, Tuple[int, ...]]:
"""
Parse a header tensor to extract metadata.
Args:
header: Header tensor
Returns:
Tuple of (chunk_idx, shape)
"""
header_list = header.tolist()
chunk_idx = int(header_list[0])
shape = tuple(int(x) for x in header_list[1:])
return chunk_idx, shape
def validate_tensor_for_communication(tensor: torch.Tensor,
expected_device: torch.device,
expected_dtype: torch.dtype) -> None:
"""
Validate tensor properties for communication.
Args:
tensor: Tensor to validate
expected_device: Expected device
expected_dtype: Expected data type
Raises:
ValueError: If tensor properties don't match expectations
"""
if not isinstance(tensor, torch.Tensor):
raise ValueError("Input must be a torch.Tensor")
if tensor.device != expected_device:
raise ValueError(f"Tensor device {tensor.device} doesn't match expected {expected_device}")
if tensor.dtype != expected_dtype:
raise ValueError(f"Tensor dtype {tensor.dtype} doesn't match expected {expected_dtype}")
def compute_balanced_split(total_blocks: int, rank_times: List[float],
dit_times: List[float],
current_block_nums: List[List[int]]) -> List[List[int]]:
"""
Compute new block splits for all ranks to balance total rank times.
This function is moved from the original file to provide better organization.
Args:
total_blocks: Total number of DiT blocks
rank_times: List of total iteration times for each rank [t_rank0, t_rank1, ..., t_rankN] (DiT + VAE time)
dit_times: List of pure DiT inference times for each rank [dit_rank0, dit_rank1, ..., dit_rankN] (DiT time only)
current_block_nums: List of current block_num format for each rank [[rank0_blocks], [rank1_blocks], ...]
Returns:
List of new block_num format for each rank, matching the original format:
- For world_size == 2: [[end_idx_rank0], [start_idx_rank1]]
- For world_size > 2: [[end_idx_rank0], [start1, end1], [start2, end2], ..., [start_idx_last]]
Note: Numbers are shared across ranks (rank0_end = rank1_start, rank1_end = rank2_start, etc.)
"""
num_ranks = len(rank_times)
if num_ranks == 0 or num_ranks != len(current_block_nums) or num_ranks != len(dit_times):
return current_block_nums
# Edge case: if we have more ranks than blocks, we can't guarantee 1 block per rank
if num_ranks > total_blocks:
# Fall back to original behavior for this edge case
return current_block_nums
# Step 1: Calculate total DiT time and per-block DiT time
total_dit_time = sum(dit_times)
dit_time_per_block = total_dit_time / total_blocks
# Step 2: Calculate average rank time
avg_rank_time = sum(rank_times) / num_ranks
# Step 3: Extract current block counts from current_block_nums (all ranks use [start, end) now)
current_block_counts = []
for block_num in current_block_nums:
# block_num: [start, end) exclusive end
start_idx, end_idx = int(block_num[0]), int(block_num[1])
current_block_counts.append(max(0, end_idx - start_idx))
# Step 4: Calculate target block counts based on time differences
target_blocks = []
for i in range(num_ranks):
time_diff = avg_rank_time - rank_times[i] # positive = needs more time, negative = needs less time
block_adjustment = time_diff / dit_time_per_block # convert time difference to block count
target_count = current_block_counts[i] + block_adjustment
# Ensure each rank gets at least 1 block (minimum allocation)
target_count = max(1, int(round(target_count)))
target_blocks.append(target_count)
# Step 5: Adjust to ensure total blocks sum to total_blocks while maintaining minimum 1 block per rank
current_total = sum(target_blocks)
if current_total != total_blocks:
diff = total_blocks - current_total
# When adding, give to ranks with smallest counts first; when removing, take from largest counts first
if diff > 0:
order = sorted(range(num_ranks), key=lambda i: (target_blocks[i], i))
else:
order = sorted(range(num_ranks), key=lambda i: (target_blocks[i], i), reverse=True)
i = 0
while diff != 0 and num_ranks > 0:
idx = order[i % num_ranks]
if diff > 0:
target_blocks[idx] += 1
diff -= 1
else:
# Only remove blocks if rank has more than 1 block (maintain minimum allocation)
if target_blocks[idx] > 1:
target_blocks[idx] -= 1
diff += 1
i += 1
# Step 6: Convert target block counts to contiguous [start, end) intervals from 0 to total_blocks
new_block_nums = []
running_start = 0
for i in range(num_ranks):
block_count = int(target_blocks[i])
start_idx = running_start
end_idx = start_idx + block_count
# Guard (should not trigger if sums are correct)
if end_idx > total_blocks:
end_idx = total_blocks
new_block_nums.append([start_idx, end_idx])
running_start = end_idx
return new_block_nums
def setup_logging(rank: int, log_level: int = logging.INFO) -> logging.Logger:
"""
Setup logging for the current rank.
Args:
rank: Current rank
log_level: Logging level
Returns:
Configured logger
"""
logger = logging.getLogger(f"rank_{rank}")
logger.setLevel(log_level)
# Prevent messages from propagating to the root logger (avoid double prints)
logger.propagate = False
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
f'[Rank {rank}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
class CommunicationTimer:
"""
Timer for measuring communication performance.
This class provides context manager functionality for timing communication operations.
"""
def __init__(self, operation_name: str, logger: Optional[logging.Logger] = None):
self.operation_name = operation_name
self.logger = logger
self.start_time = None
self.end_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.end_time = time.time()
duration = self.end_time - self.start_time
if self.logger:
self.logger.info(f"{self.operation_name} took {duration:.4f} seconds")
@property
def duration(self) -> float:
"""Get the duration of the timed operation."""
if self.start_time is None or self.end_time is None:
return 0.0
return self.end_time - self.start_time
|