Skip to content

Commit 6cf5187

Browse files
Siqi Huangfacebook-github-bot
authored andcommitted
Create Reduce-Scatter Primitive Benchmark
Summary: This diff introduces a new benchmark for evaluating the performance of the reduce-scatter collective in the `PooledEmbeddingsReduceScatter` module from `dist_data.py`. The benchmark is integrated with the existing `benchmark_primitive.py` file. The goal is to measure the reduce-scatter performance and analyze its impact on the overall efficiency of the `output_dist` process. ### Key Changes: * Updated `benchmark_primitive.py` to include the reduce-scatter benchmark. * Added comments to explain the newly added benchmark in the context of `PooledEmbeddingsReduceScatter` and its usage in `output_dist`. Differential Revision: D113960618
1 parent 1ecec90 commit 6cf5187

1 file changed

Lines changed: 147 additions & 2 deletions

File tree

torchrec/distributed/benchmark/benchmark_primitive.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
``dist_data.py``). The second is ``kt_a2a``, the All-to-All performance of
2525
``PooledEmbeddingsAllToAll`` -- the dense pooled-embedding (``KeyedTensor``) collective
2626
``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.
2831
2932
A follow-up launcher binary will call ``runner`` explicitly with options to run on
3033
MAST or locally.
@@ -36,7 +39,11 @@
3639

3740
import torch
3841
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+
)
4047
from torchrec.distributed.test_utils.process_runner import SingleProcessContext
4148
from torchrec.sparse.jagged_tensor import KeyedJaggedTensor
4249

@@ -358,12 +365,150 @@ def _benchmark_kt_a2a(
358365
return result
359366

360367

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+
361505
# Registry of available primitive benchmarks, keyed by the ``primitive`` flag.
362506
# Add new primitive benchmarks here -- each is called as
363507
# ``fn(ctx, rank, world_size, **kwargs)`` and returns a per-rank ``BenchmarkResult``.
364508
_BENCHMARKS: Dict[str, Callable[..., BenchmarkResult]] = {
365509
"kjt_a2a": _benchmark_kjt_a2a,
366510
"kt_a2a": _benchmark_kt_a2a,
511+
"reduce_scatter": _benchmark_reduce_scatter,
367512
}
368513

369514

0 commit comments

Comments
 (0)