|
22 | 22 | import os |
23 | 23 | import random |
24 | 24 | from contextlib import contextmanager |
| 25 | +from dataclasses import dataclass |
25 | 26 | from typing import Any, Optional, Tuple |
26 | 27 |
|
27 | 28 | import torch |
|
40 | 41 | CP_SKIP_SAMPLING_SENTINEL = "__CP_SKIP_SAMPLING__" |
41 | 42 |
|
42 | 43 |
|
| 44 | +@dataclass(frozen=True) |
| 45 | +class DistributedBatchLayout: |
| 46 | + local_batch_size: int |
| 47 | + global_batch_size: int |
| 48 | + local_batch_offset: int |
| 49 | + data_rank: int |
| 50 | + data_parallel_size: int |
| 51 | + model_replica_size: int |
| 52 | + world_size: int |
| 53 | + data_replica_batch_sizes: tuple[int, ...] |
| 54 | + |
| 55 | + |
43 | 56 | def _normalize_parallel_size(value: Any, name: str) -> int: |
44 | 57 | if value is None: |
45 | 58 | return 1 |
@@ -219,6 +232,122 @@ def get_model_replica_data_info( |
219 | 232 | return True, data_rank, data_local_rank, data_group_size, dp_replicate_size |
220 | 233 |
|
221 | 234 |
|
| 235 | +def resolve_distributed_batch_layout(accelerator, local_batch_size: int) -> DistributedBatchLayout: |
| 236 | + """Resolve sample counts and this data replica's offset using fixed-shape collectives.""" |
| 237 | + local_batch_size = _normalize_parallel_size(local_batch_size, "local_batch_size") |
| 238 | + if local_batch_size < 1: |
| 239 | + raise ValueError("local_batch_size must be greater than 0.") |
| 240 | + |
| 241 | + if accelerator is None: |
| 242 | + return DistributedBatchLayout(local_batch_size, local_batch_size, 0, 0, 1, 1, 1, (local_batch_size,)) |
| 243 | + |
| 244 | + world_size = _normalize_parallel_size(getattr(accelerator, "num_processes", 1), "num_processes") |
| 245 | + process_index = _normalize_parallel_size(getattr(accelerator, "process_index", 0), "process_index") |
| 246 | + if world_size == 1: |
| 247 | + return DistributedBatchLayout(local_batch_size, local_batch_size, 0, 0, 1, 1, 1, (local_batch_size,)) |
| 248 | + |
| 249 | + count = torch.tensor([local_batch_size], device=accelerator.device, dtype=torch.long) |
| 250 | + gathered_counts = accelerator.gather(count).reshape(-1) |
| 251 | + if gathered_counts.numel() != world_size: |
| 252 | + raise RuntimeError( |
| 253 | + f"Distributed batch count gather returned {gathered_counts.numel()} values for world size {world_size}." |
| 254 | + ) |
| 255 | + world_batch_sizes = tuple(int(value) for value in gathered_counts.cpu().tolist()) |
| 256 | + |
| 257 | + data_enabled, data_rank, _data_local_rank, model_replica_size, data_parallel_size = get_model_replica_data_info( |
| 258 | + accelerator |
| 259 | + ) |
| 260 | + if data_enabled: |
| 261 | + replica_batch_sizes = [] |
| 262 | + for replica_rank in range(data_parallel_size): |
| 263 | + start = replica_rank * model_replica_size |
| 264 | + replica_counts = world_batch_sizes[start : start + model_replica_size] |
| 265 | + if len(set(replica_counts)) != 1: |
| 266 | + raise RuntimeError( |
| 267 | + "Model-parallel ranks received different local batch sizes: " |
| 268 | + f"data replica {replica_rank} reported {replica_counts}." |
| 269 | + ) |
| 270 | + replica_batch_sizes.append(replica_counts[0]) |
| 271 | + data_replica_batch_sizes = tuple(replica_batch_sizes) |
| 272 | + else: |
| 273 | + data_rank = process_index |
| 274 | + data_parallel_size = world_size |
| 275 | + model_replica_size = 1 |
| 276 | + data_replica_batch_sizes = world_batch_sizes |
| 277 | + |
| 278 | + return DistributedBatchLayout( |
| 279 | + local_batch_size=local_batch_size, |
| 280 | + global_batch_size=sum(data_replica_batch_sizes), |
| 281 | + local_batch_offset=sum(data_replica_batch_sizes[:data_rank]), |
| 282 | + data_rank=data_rank, |
| 283 | + data_parallel_size=data_parallel_size, |
| 284 | + model_replica_size=model_replica_size, |
| 285 | + world_size=world_size, |
| 286 | + data_replica_batch_sizes=data_replica_batch_sizes, |
| 287 | + ) |
| 288 | + |
| 289 | + |
| 290 | +def gather_variable_batch_tensor( |
| 291 | + tensor: torch.Tensor, |
| 292 | + accelerator, |
| 293 | + layout: Optional[DistributedBatchLayout] = None, |
| 294 | +) -> torch.Tensor: |
| 295 | + """Gather every rank's per-sample tensor when data replicas have different batch sizes.""" |
| 296 | + if tensor.ndim < 1: |
| 297 | + raise ValueError("Variable batch tensor gather requires a batch dimension.") |
| 298 | + if layout is None: |
| 299 | + layout = resolve_distributed_batch_layout(accelerator, tensor.shape[0]) |
| 300 | + if tensor.shape[0] != layout.local_batch_size: |
| 301 | + raise ValueError(f"Tensor batch dimension {tensor.shape[0]} does not match layout size {layout.local_batch_size}.") |
| 302 | + if layout.world_size == 1: |
| 303 | + return tensor.detach() |
| 304 | + |
| 305 | + maximum_batch_size = max(layout.data_replica_batch_sizes) |
| 306 | + padded = tensor.detach() |
| 307 | + if padded.shape[0] < maximum_batch_size: |
| 308 | + padding = padded.new_zeros((maximum_batch_size - padded.shape[0], *padded.shape[1:])) |
| 309 | + padded = torch.cat((padded, padding), dim=0) |
| 310 | + |
| 311 | + gathered = accelerator.gather(padded) |
| 312 | + expected_first_dimension = layout.world_size * maximum_batch_size |
| 313 | + if gathered.shape[0] != expected_first_dimension: |
| 314 | + raise RuntimeError( |
| 315 | + "Distributed tensor gather returned an unexpected first dimension: " |
| 316 | + f"expected {expected_first_dimension}, got {gathered.shape[0]}." |
| 317 | + ) |
| 318 | + gathered = gathered.reshape(layout.world_size, maximum_batch_size, *padded.shape[1:]) |
| 319 | + rank_tensors = [] |
| 320 | + for world_rank in range(layout.world_size): |
| 321 | + replica_rank = world_rank // layout.model_replica_size |
| 322 | + batch_size = layout.data_replica_batch_sizes[replica_rank] |
| 323 | + rank_tensors.append(gathered[world_rank, :batch_size]) |
| 324 | + return torch.cat(rank_tensors, dim=0) |
| 325 | + |
| 326 | + |
| 327 | +def gather_sample_weighted_scalar(value: torch.Tensor, local_batch_size: int, accelerator) -> torch.Tensor: |
| 328 | + """Return a sample-weighted world mean from one fixed-shape contribution per rank.""" |
| 329 | + local_batch_size = _normalize_parallel_size(local_batch_size, "local_batch_size") |
| 330 | + if local_batch_size < 1: |
| 331 | + raise ValueError("local_batch_size must be greater than 0.") |
| 332 | + if value.numel() != 1: |
| 333 | + raise ValueError("Sample-weighted scalar gather requires a scalar tensor.") |
| 334 | + |
| 335 | + value = value.detach().float().reshape(()) |
| 336 | + world_size = _normalize_parallel_size(getattr(accelerator, "num_processes", 1), "num_processes") |
| 337 | + if world_size == 1: |
| 338 | + return value |
| 339 | + |
| 340 | + local_count = value.new_tensor(float(local_batch_size)) |
| 341 | + contribution = torch.stack((value * local_count, local_count)) |
| 342 | + gathered = accelerator.gather(contribution).reshape(-1, 2) |
| 343 | + if gathered.shape[0] != world_size: |
| 344 | + raise RuntimeError( |
| 345 | + f"Distributed loss gather returned {gathered.shape[0]} contributions for world size {world_size}." |
| 346 | + ) |
| 347 | + totals = gathered.sum(dim=0) |
| 348 | + return totals[0] / totals[1] |
| 349 | + |
| 350 | + |
222 | 351 | class ContextParallelBatchSynchronizer: |
223 | 352 | """ |
224 | 353 | Caches context-parallel information for efficient batch synchronization. |
|
0 commit comments