|
24 | 24 | ``dist_data.py``). The second is ``kt_a2a``, the All-to-All performance of |
25 | 25 | ``PooledEmbeddingsAllToAll`` -- the dense pooled-embedding (``KeyedTensor``) collective |
26 | 26 | ``output_dist`` uses to redistribute real embedding outputs. Unlike ``kjt_a2a`` it |
27 | | -exchanges float tensors rather than sparse indices. |
| 27 | +exchanges float tensors rather than sparse indices. The third is ``reduce_scatter``, the |
| 28 | +reduce-scatter performance of ``PooledEmbeddingsReduceScatter`` (from ``dist_data.py``) -- |
| 29 | +the collective ``output_dist`` uses instead of the A2A for row-wise / table-row-wise |
| 30 | +sharding, summing each rank's partial pooled embeddings and scattering the batch dimension. |
28 | 31 |
|
29 | 32 | A follow-up launcher binary will call ``runner`` explicitly with options to run on |
30 | 33 | MAST or locally. |
|
36 | 39 |
|
37 | 40 | import torch |
38 | 41 | from torchrec.distributed.benchmark.base import benchmark_func, BenchmarkResult |
39 | | -from torchrec.distributed.dist_data import KJTAllToAll, PooledEmbeddingsAllToAll |
| 42 | +from torchrec.distributed.dist_data import ( |
| 43 | + KJTAllToAll, |
| 44 | + PooledEmbeddingsAllToAll, |
| 45 | + PooledEmbeddingsReduceScatter, |
| 46 | +) |
40 | 47 | from torchrec.distributed.test_utils.process_runner import SingleProcessContext |
41 | 48 | from torchrec.sparse.jagged_tensor import KeyedJaggedTensor |
42 | 49 |
|
@@ -358,12 +365,150 @@ def _benchmark_kt_a2a( |
358 | 365 | return result |
359 | 366 |
|
360 | 367 |
|
| 368 | +def _make_reduce_scatter_input( |
| 369 | + batch_size: int, |
| 370 | + dim: int, |
| 371 | + values_dtype: torch.dtype, |
| 372 | + device: torch.device, |
| 373 | +) -> torch.Tensor: |
| 374 | + """Build this rank's input for ``PooledEmbeddingsReduceScatter``. |
| 375 | +
|
| 376 | + Returns a ``[batch_size, dim]`` tensor standing in for this rank's *partial* pooled |
| 377 | + embeddings over the full global batch. reduce-scatter sums these partials across ranks |
| 378 | + and scatters the batch dimension, so each rank ends up with |
| 379 | + ``[batch_size // world_size, dim]`` -- its slice of the reduced result. Content is |
| 380 | + random -- only the transport size matters for this benchmark, not the values. |
| 381 | + """ |
| 382 | + return torch.rand((batch_size, dim), dtype=values_dtype, device=device) |
| 383 | + |
| 384 | + |
| 385 | +def _run_reduce_scatter( |
| 386 | + _batch_inputs: List[Any], |
| 387 | + *, |
| 388 | + rs: PooledEmbeddingsReduceScatter, |
| 389 | + local_embs: torch.Tensor, |
| 390 | +) -> None: |
| 391 | + """One measured iteration: full reduce-scatter, then touch the output. |
| 392 | +
|
| 393 | + Rank alignment against the straggler effect is handled by ``PerfWrapper`` (it barriers |
| 394 | + before each iteration, outside the timing window); this function only runs the |
| 395 | + collective. ``PooledEmbeddingsReduceScatter`` returns a single-stage awaitable -- one |
| 396 | + ``wait()`` sums each rank's ``local_embs`` across the group and scatters the batch |
| 397 | + dimension, yielding this rank's ``[batch_size // world_size, dim]`` slice. ``numel()`` |
| 398 | + only reads the output's shape metadata: no data read, no kernel, and -- like ``wait()`` |
| 399 | + on CUDA -- no host sync (the collective runs async on the stream). The input tensor is |
| 400 | + reused across iterations. |
| 401 | +
|
| 402 | + So we ``torch.cuda.synchronize()`` at the end to actually block the host on the |
| 403 | + collective inside the measured region; that is what makes the wall-clock timer reflect |
| 404 | + end-to-end collective latency (GPU-event timing is unaffected either way). |
| 405 | + """ |
| 406 | + out = rs(local_embs).wait() |
| 407 | + out.numel() |
| 408 | + if local_embs.is_cuda: |
| 409 | + torch.cuda.synchronize(local_embs.device) |
| 410 | + |
| 411 | + |
| 412 | +def _benchmark_reduce_scatter( |
| 413 | + ctx: SingleProcessContext, |
| 414 | + rank: int, |
| 415 | + world_size: int, |
| 416 | + **kwargs: Any, |
| 417 | +) -> BenchmarkResult: |
| 418 | + """``PooledEmbeddingsReduceScatter`` (dense pooled-embedding reduce-scatter) benchmark. |
| 419 | +
|
| 420 | + Builds a dense pooled-embedding input tensor of a configurable size, then measures the |
| 421 | + latency of reducing-and-scattering it through ``PooledEmbeddingsReduceScatter`` (the |
| 422 | + ``dist_data.py`` module, the same one row-wise / table-row-wise / grid sharding |
| 423 | + instantiate for ``output_dist``) over ``ctx.pg`` -- each rank holds partial pooled sums |
| 424 | + for the global batch that must be summed across ranks and scattered back to each rank's |
| 425 | + local batch slice. Correctness of the reduced output is intentionally not verified. |
| 426 | +
|
| 427 | + Args: |
| 428 | + ctx: live single-process context (device + process group) injected by the |
| 429 | + process runner; use ``ctx.device`` / ``ctx.pg`` directly. |
| 430 | + rank: this process' global rank. |
| 431 | + world_size: total number of ranks. |
| 432 | + **kwargs: benchmark options: |
| 433 | + batch_size (int): global batch size (rows of the input) -- must be divisible by |
| 434 | + ``world_size`` (reduce-scatter splits the batch evenly across ranks). |
| 435 | + Default 32 * 1024. |
| 436 | + dim (int): embedding width. The headline transport size is ``batch_size * dim`` |
| 437 | + (with the defaults, ``32768 * 3072 * 4B ~= 400 MB`` of float32 per rank, |
| 438 | + comparable to ``kt_a2a``). Default 3072. |
| 439 | + values_dtype (torch.dtype): dtype of the embedding tensor; must be a floating |
| 440 | + dtype. Default float32. |
| 441 | + num_benchmarks (int): number of measured iterations. Default 20. |
| 442 | + num_profiles (int): number of profiled iterations (requires profile_dir). |
| 443 | + Default 0. |
| 444 | + profile_dir (str): directory for chrome traces; empty disables profiling. |
| 445 | + name (str): human-readable benchmark name. Default "reduce_scatter". |
| 446 | +
|
| 447 | + Returns: |
| 448 | + This rank's ``BenchmarkResult``. |
| 449 | + """ |
| 450 | + batch_size: int = int(kwargs.get("batch_size", 32 * 1024)) |
| 451 | + dim: int = int(kwargs.get("dim", 3072)) |
| 452 | + values_dtype: torch.dtype = kwargs.get("values_dtype", torch.float32) |
| 453 | + num_benchmarks: int = int(kwargs.get("num_benchmarks", 20)) |
| 454 | + num_profiles: int = int(kwargs.get("num_profiles", 0)) |
| 455 | + profile_dir: str = str(kwargs.get("profile_dir", "")) |
| 456 | + name: str = str(kwargs.get("name", "reduce_scatter")) |
| 457 | + |
| 458 | + pg: Optional[torch.distributed.ProcessGroup] = ctx.pg |
| 459 | + assert pg is not None, "ctx.pg must be initialized by the process runner" |
| 460 | + assert batch_size % world_size == 0, ( |
| 461 | + f"batch_size ({batch_size}) must be divisible by world_size ({world_size}): " |
| 462 | + "PooledEmbeddingsReduceScatter scatters the global batch evenly across ranks." |
| 463 | + ) |
| 464 | + |
| 465 | + local_embs = _make_reduce_scatter_input( |
| 466 | + batch_size=batch_size, |
| 467 | + dim=dim, |
| 468 | + values_dtype=values_dtype, |
| 469 | + device=ctx.device, |
| 470 | + ) |
| 471 | + rs = PooledEmbeddingsReduceScatter(pg) |
| 472 | + |
| 473 | + logger.info( |
| 474 | + "rank=%d local_rank=%d host=%s running reduce-scatter benchmark: batch_size=%d " |
| 475 | + "dim=%d device=%s", |
| 476 | + rank, |
| 477 | + ctx.local_rank, |
| 478 | + socket.gethostname(), |
| 479 | + batch_size, |
| 480 | + dim, |
| 481 | + ctx.device, |
| 482 | + ) |
| 483 | + |
| 484 | + result = benchmark_func( |
| 485 | + name=name, |
| 486 | + rank=rank, |
| 487 | + world_size=world_size, |
| 488 | + func_to_benchmark=_run_reduce_scatter, |
| 489 | + bench_inputs=[], |
| 490 | + prof_inputs=[], |
| 491 | + benchmark_func_kwargs={"rs": rs, "local_embs": local_embs}, |
| 492 | + num_profiles=num_profiles, |
| 493 | + num_benchmarks=num_benchmarks, |
| 494 | + profile_dir=profile_dir, |
| 495 | + device_type=ctx.device.type, |
| 496 | + pg=pg, |
| 497 | + ) |
| 498 | + |
| 499 | + if rank == 0: |
| 500 | + logger.info("reduce-scatter benchmark result:\n%s", result) |
| 501 | + |
| 502 | + return result |
| 503 | + |
| 504 | + |
361 | 505 | # Registry of available primitive benchmarks, keyed by the ``primitive`` flag. |
362 | 506 | # Add new primitive benchmarks here -- each is called as |
363 | 507 | # ``fn(ctx, rank, world_size, **kwargs)`` and returns a per-rank ``BenchmarkResult``. |
364 | 508 | _BENCHMARKS: Dict[str, Callable[..., BenchmarkResult]] = { |
365 | 509 | "kjt_a2a": _benchmark_kjt_a2a, |
366 | 510 | "kt_a2a": _benchmark_kt_a2a, |
| 511 | + "reduce_scatter": _benchmark_reduce_scatter, |
367 | 512 | } |
368 | 513 |
|
369 | 514 |
|
|
0 commit comments