Files
mscclpp/test/python/ep/ep_bench_ll.py
Binyang Li ad0b63d21b Update EP benchmarks for current low-latency API
Refresh the Python and C++ benchmark paths for BF16 and FP8 dispatch, current MoERuntime signatures, active kernel sources, portable CUPTI discovery, realistic routing, and safe unified reporting. Remove the merged change to the inactive legacy implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: efbacae6-f679-430b-bc16-b45ae162fc76
2026-07-13 21:45:58 +00:00

676 lines
30 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
"""Unified low-latency EP benchmark for MSCCL++ EP — an apples-to-apples port of
NCCL-EP's ``contrib/nccl_ep/ep_bench.cu`` low-latency (LL) flow, with the NCCL-EP
API (``ncclEpDispatch`` / ``ncclEpCombine``) replaced by the MSCCL++ EP high-level
``MoECommunicator.dispatch`` / ``MoECommunicator.combine`` (feature/ep API).
Why this exists
---------------
``ep_bench`` is the reference NCCL-EP micro-benchmark. To compare MSCCL++ EP
against it fairly we must measure *the same thing the same way*. This script is a
line-for-line reimplementation of ``ep_bench``'s LL measurement methodology, only
swapping the collective API underneath:
* **Paired** dispatch→sync→combine→sync→barrier per iteration (``runPairedBenchmark``).
* **Per-iteration CUDA events** recorded on the stream *around each kernel launch*;
the ``cudaStreamSynchronize`` and ``MPI_Barrier`` (here ``dist.barrier``) happen
**outside** the timed region, exactly as in ``ep_bench``.
* **Skip the first timed iteration** (warmup outlier) — matches ``ep_bench``'s
``calc_stats`` which trims ``times[0]`` when ``num_iters > 1``.
* **Byte accounting** identical to ``calculateLowLatencyBytes``:
``bytes = num_valid_selections * hidden * 2`` (BF16) for *both* dispatch and
combine, where ``num_valid_selections = count(topk_idx >= 0)``.
* **Cross-rank reduction** identical to ``printLowLatencyResults``: latency
``avg = mean``, ``min = MIN``, ``max = MAX``; per-rank throughput min/max are
tagged with the owning rank (``MPI_MINLOC`` / ``MPI_MAXLOC`` analog).
* **Output** mirrors ``ep_bench``'s ``=== Summary (Low Latency, across N ranks) ===``
block so the two runs can be diffed directly.
CLI mirrors ``ep_bench``'s LL-relevant flags (long + short):
-t/--num-tokens tokens per rank (ep_bench LL default 128)
-d/--hidden hidden dim (7168)
-k/--num-topk top-k experts per token (8)
-e/--num-experts global experts (256)
-w/--num-warmup warmup iterations (10)
-i/--num-iters timed iterations (50)
Fidelity note
-------------
``ep_bench`` is C++/MPI; MSCCL++ EP's LL API is Python/torch, so this harness is
Python. The *measurement* is identical: both bracket the same dispatch/combine
kernels with CUDA events and report GPU-side host-observed time. The only
difference is host-side launch latency, which sits *outside* the recorded events
for the async kernels and is the same definitional gap ``ep_bench`` has (larger
in Python, but not counted in the kernel elapsed time). For a pure kernel number,
run under ``nsys``/CUPTI as with ``ep_bench``'s ``--- Kernel-only ---`` section.
Launch
------
Manual per-rank env (DSM hostnames break torchrun rendezvous on these nodes):
RANK=.. LOCAL_RANK=.. WORLD_SIZE=.. MASTER_ADDR=.. MASTER_PORT=.. \
python ep_bench_ll.py -t 128 -d 7168 -k 8 -e 256 -w 10 -i 50
Single node (4/8 GPU):
torchrun --standalone --nproc_per_node=4 ep_bench_ll.py -e 128
"""
from __future__ import annotations
import argparse
import os
import random
# Quiet ProcessGroupNCCL's heartbeat monitor before importing torch.distributed
# (same rationale as test_low_latency_multirank.py).
os.environ.setdefault("TORCH_NCCL_ENABLE_MONITORING", "0")
import torch
import torch.distributed as dist
# ----------------------------------------------------------------------------
# CLI — mirrors ep_bench.cu's getopt flags for the LL path.
# ----------------------------------------------------------------------------
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="MSCCL++ EP low-latency benchmark (ep_bench parity)",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
# Env fallbacks keep the existing MSCCLPP_EP_BENCH_* launchers working.
p.add_argument(
"-a",
"--algorithm",
default="ll",
choices=["ll", "low-latency"],
help="algorithm mode (only LL is implemented here)",
)
p.add_argument(
"-t",
"--num-tokens",
type=int,
default=int(os.environ.get("MSCCLPP_EP_BENCH_TOKENS", "128")),
help="tokens per rank (ep_bench LL max_tokens_per_rank)",
)
p.add_argument(
"-d",
"--hidden",
type=int,
default=int(os.environ.get("MSCCLPP_EP_BENCH_HIDDEN", "7168")),
choices=(4096, 7168, 8192, 9216),
help="hidden dimension",
)
p.add_argument(
"-k",
"--num-topk",
type=int,
default=int(os.environ.get("MSCCLPP_EP_BENCH_TOPK", "8")),
choices=range(1, 10),
help="top-k experts per token",
)
p.add_argument(
"-e",
"--num-experts",
type=int,
default=int(os.environ.get("MSCCLPP_EP_BENCH_EXPERTS", "256")),
help="global number of experts",
)
p.add_argument(
"-w",
"--num-warmup",
type=int,
default=int(os.environ.get("MSCCLPP_EP_BENCH_WARMUP", "10")),
help="warmup iterations",
)
p.add_argument(
"-i",
"--num-iters",
type=int,
default=int(os.environ.get("MSCCLPP_EP_BENCH_ITERS", "50")),
help="timed iterations",
)
p.add_argument(
"--dispatch-dtype",
choices=("bf16", "fp8_e4m3"),
default="bf16",
help="low-latency dispatch payload format",
)
p.add_argument(
"--combine-mode",
choices=("rank_local_reduce", "direct_send"),
default="rank_local_reduce",
help="low-latency combine algorithm",
)
p.add_argument("--num-blocks", type=int, default=130, help="total low-latency dispatch blocks")
p.add_argument(
"--no-kernel-timing",
dest="kernel_timing",
action="store_false",
help="disable the CUPTI/torch.profiler kernel-only measurement pass "
"(on by default, mirrors ep_bench's CUPTI KernelTimer)",
)
p.add_argument(
"--cupti-region",
action="store_true",
help="bracket ONLY the timed loop with cudaProfilerStart/Stop (for nsys "
"--capture-range=cudaProfilerApi) so an external CUPTI collector times "
"exactly the post-warmup dispatch/combine kernels, like ep_bench's "
"KernelTimer.start()-after-warmup. Skips the in-process torch.profiler "
"pass; kernel numbers come from nsys.",
)
p.add_argument(
"--cupti-inproc",
action="store_true",
help="use the in-process CUPTI collector (libcupti_kernel_timer.so, a faithful "
"port of ep_bench's KernelTimer): CUPTI Activity API records per-kernel GPU "
"time over the post-warmup timed loop, near-zero host perturbation, and works "
"multinode without nsys. Matches mangled dispatch/combine kernel names and "
"replaces the torch.profiler pass.",
)
p.add_argument("--seed", type=int, default=0xB3C4, help="per-rank RNG seed base")
args = p.parse_args()
if args.hidden not in (4096, 7168, 8192, 9216):
p.error("--hidden must be one of 4096, 7168, 8192, 9216")
if not 1 <= args.num_topk <= 9:
p.error("--num-topk must be in [1, 9]")
if args.num_tokens <= 0 or args.num_experts <= 0:
p.error("--num-tokens and --num-experts must be positive")
if args.num_topk > args.num_experts:
p.error("--num-topk must not exceed --num-experts")
if args.num_warmup < 0 or args.num_iters <= 0:
p.error("--num-warmup must be non-negative and --num-iters must be positive")
return args
def init_dist():
rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
local_rank = int(os.environ.get("LOCAL_RANK", rank))
torch.cuda.set_device(local_rank)
dist.init_process_group(
backend="nccl",
init_method=f"tcp://{os.environ.get('MASTER_ADDR', '127.0.0.1')}:{os.environ.get('MASTER_PORT', '29500')}",
world_size=world_size,
rank=rank,
)
return rank, world_size, local_rank, dist.new_group(list(range(world_size)))
def _reduce_scalar(value: float, op, group) -> float:
t = torch.tensor([value], dtype=torch.float64, device="cuda")
dist.all_reduce(t, op=op, group=group)
return t.item()
def _gather_scalars(value: float, num_ranks: int, group) -> list:
t = torch.tensor([value], dtype=torch.float64, device="cuda")
out = [torch.zeros_like(t) for _ in range(num_ranks)]
dist.all_gather(out, t, group=group)
return [float(x.item()) for x in out]
def _profile_paired_kernels(dispatch_fn, combine_fn, iters: int, stream, group, rank: int):
"""Kernel-only dispatch/combine device time (us/iter) via torch.profiler.
Mirrors ep_bench's CUPTI ``KernelTimer``: it profiles the SAME paired
``dispatch -> sync -> combine -> sync -> barrier`` loop used for the
host-observed measurement. Profiling the *paired* loop (rather than isolated
dispatch-only / combine-only loops) is essential: the LL dispatch kernel
ends with a cross-rank receive spin-wait, and without the per-iter barrier
the ranks drift out of lockstep so that spin balloons to milliseconds on the
laggards. The barrier keeps every rank aligned at each iteration boundary, so
the recv-wait stays bounded -- exactly why ep_bench times the paired loop.
Kernels are bucketed by name substring ``dispatch`` / ``combine`` (the mscclpp
LL kernels demangle to ``mscclpp::ep::low_latency::dispatch<...>`` /
``::combine<...>``), matching ep_bench's ``get_avg_us("dispatch"/"combine")``.
All other device activity (the pacing barrier's NCCL kernel, memcpy/memset)
is ignored.
"""
from torch.profiler import profile, ProfilerActivity
torch.cuda.synchronize()
with profile(activities=[ProfilerActivity.CUDA]) as prof:
for _ in range(iters):
dout = dispatch_fn()
stream.synchronize()
combine_fn(dout)
stream.synchronize()
dist.barrier(group=group)
torch.cuda.synchronize()
disp_us = 0.0
comb_us = 0.0
dbg = []
for e in prof.key_averages():
dev_us = getattr(e, "self_device_time_total", None)
if dev_us is None:
dev_us = getattr(e, "self_cuda_time_total", 0.0)
if not dev_us or dev_us <= 0:
continue
low = str(e.key).lower()
if "memcpy" in low or "memset" in low:
continue # CUPTI KernelTimer counts KERNEL activities only
if "dispatch" in low:
disp_us += dev_us
elif "combine" in low:
comb_us += dev_us
dbg.append((dev_us, str(e.key)))
if os.environ.get("MSCCLPP_EP_KDEBUG", "0") == "1" and rank == 0:
dbg.sort(reverse=True)
print(f"[kdebug] top device activities (self device us/iter over {iters} iters):", flush=True)
for us, name in dbg[:10]:
print(f" {us / iters:8.2f} us/iter {name[:90]}", flush=True)
return disp_us / iters, comb_us / iters
class _InProcCupti:
"""In-process CUPTI kernel timer, a faithful analog of ep_bench's KernelTimer.
Loads ``libcupti_kernel_timer.so`` (built from cupti_kernel_timer.cpp, sitting
next to this file) via ctypes and drives the CUPTI Activity API directly:
``start()`` after warmup, ``stop()`` after the timed loop, then
``avg_us("dispatch"/"combine")`` buckets recorded kernels by mangled-name
substring -- exactly ep_bench's methodology, with near-zero host perturbation
(out-of-band buffer callbacks), so the LL dispatch recv-spin is measured
cleanly rather than being serialized by an in-process tracer.
"""
def __init__(self):
import ctypes
import os as _os
so = _os.environ.get(
"MSCCLPP_EP_CUPTI_TIMER_LIB",
_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "build", "libcupti_kernel_timer.so"),
)
self.lib = ctypes.CDLL(so)
self.lib.kt_start.restype = ctypes.c_int
self.lib.kt_stop.restype = ctypes.c_int
self.lib.kt_get_avg_us.restype = ctypes.c_double
self.lib.kt_get_avg_us.argtypes = [ctypes.c_char_p]
self.lib.kt_get_count.restype = ctypes.c_long
self.lib.kt_get_count.argtypes = [ctypes.c_char_p]
def start(self) -> int:
return int(self.lib.kt_start())
def stop(self) -> int:
return int(self.lib.kt_stop())
def avg_us(self, substr: str) -> float:
return float(self.lib.kt_get_avg_us(substr.encode()))
def count(self, substr: str) -> int:
return int(self.lib.kt_get_count(substr.encode()))
def main() -> None:
args = parse_args()
rank, num_ranks, local_rank, group = init_dist()
from mscclpp import CommGroup
import mscclpp.ep as ep
from mscclpp.ep._cpp import get_low_latency_rdma_size_hint
ep_group = CommGroup(torch_group=group)
num_tokens = args.num_tokens
hidden = args.hidden
num_topk = args.num_topk
num_experts = args.num_experts
warmup = args.num_warmup
iters = args.num_iters
assert num_experts % num_ranks == 0, "num_experts must be divisible by num_ranks"
num_local_experts = num_experts // num_ranks
dispatch_data_type = {
"bf16": ep.DispatchDataType.BF16,
"fp8_e4m3": ep.DispatchDataType.FP8_E4M3,
}[args.dispatch_dtype]
combine_mode = {
"rank_local_reduce": ep.CombineMode.RANK_LOCAL_REDUCE,
"direct_send": ep.CombineMode.DIRECT_SEND,
}[args.combine_mode]
dispatch_quant = (
None if dispatch_data_type == ep.DispatchDataType.BF16 else ep.QuantConfig(format=dispatch_data_type)
)
dispatch_dtype = torch.bfloat16 if dispatch_quant is None else torch.float8_e4m3fn
dispatch_label = "BF16" if dispatch_quant is None else "FP8_E4M3"
# bf16 precision anchor (same convention as test_low_latency_multirank.py).
rank_offset = 128
assert num_ranks - rank_offset < 257, "too many ranks for bf16 precision anchor"
torch.manual_seed(args.seed + rank)
random.seed(args.seed + rank)
# ---- Inputs (mirror ep_bench setupLowLatencyTensors: BF16 tokens + routing).
x = torch.ones((num_tokens, hidden), dtype=torch.bfloat16, device="cuda") * (rank - rank_offset)
x[:, -128:] = torch.arange(num_tokens, device="cuda").to(torch.bfloat16).view(-1, 1)
scores = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda").abs() + 1
topk_idx = torch.topk(scores, num_topk, dim=-1, largest=True, sorted=True)[1].to(torch.int64)
topk_weights = torch.randn((num_tokens, num_topk), dtype=torch.float32, device="cuda").abs()
# ep_bench byte accounting: num_valid_selections = count(topk_idx >= 0). We
# keep every selection valid (a full LL load), so this equals num_tokens*top_k.
num_valid_selections = int((topk_idx >= 0).sum().item())
dispatch_bytes_per_token = hidden * 2 if dispatch_quant is None else hidden + hidden // 128 * 4
disp_bytes = num_valid_selections * dispatch_bytes_per_token
comb_bytes = num_valid_selections * hidden * 2 # BF16 (symmetric, per ep_bench)
num_rdma_bytes = get_low_latency_rdma_size_hint(num_tokens, hidden, num_ranks, num_experts, num_topk)
if rank == 0:
print(
f"[cfg] algorithm=LOW_LATENCY num_ranks={num_ranks} tokens/rank={num_tokens} hidden={hidden} "
f"num_experts={num_experts} top_k={num_topk} warmup={warmup} iters={iters} "
f"dispatch_dtype={args.dispatch_dtype} combine_mode={args.combine_mode} "
f"num_rdma_bytes={num_rdma_bytes}",
flush=True,
)
# High-level MoE communicator (feature/ep). LOW_LATENCY mode selects the LL
# backend; dispatch/combine run the full (send+recv) op inline on the stream.
moe_comm = ep.MoECommunicator(
comm=ep_group,
num_experts=num_experts,
num_local_experts=num_local_experts,
hidden_size=hidden,
topk=num_topk,
max_tokens_per_rank=num_tokens,
mode=ep.MoEMode.LOW_LATENCY,
num_rdma_qps_per_rank=max(1, num_experts // num_ranks),
low_latency_num_blocks=args.num_blocks,
low_latency_combine_mode=combine_mode,
quant=dispatch_quant,
)
assert moe_comm.is_available()
if rank == 0:
print(f"[cfg] MoECommunicator is_internode={moe_comm.is_internode()}", flush=True)
# ---- Hoist dispatch/combine output tensors out of the timed loop (ep_bench
# preallocates all EP tensors before benchmarking; matching that keeps the
# timed region kernel-bound rather than allocator-bound). The communicator
# owns its src_info/layout_range/count buffers internally; we only supply the
# dispatch output buffer and the combine output tensor.
output_buffer = torch.empty(
(num_local_experts, num_ranks * num_tokens, hidden), dtype=dispatch_dtype, device="cuda"
)
expert_output = (
None
if dispatch_quant is None
else torch.zeros((num_local_experts, num_ranks * num_tokens, hidden), dtype=torch.bfloat16, device="cuda")
)
out = torch.empty((num_tokens, hidden), dtype=torch.bfloat16, device="cuda")
def dispatch_fn():
# MoECommunicator.dispatch runs the full (send+recv) LL dispatch inline on
# the stream and returns (DispatchOutput, DispatchHandle) -- the analog of
# ncclEpDispatch + ncclEpComplete.
return moe_comm.dispatch(x, topk_idx, topk_weights, output_buffer=output_buffer)
def combine_fn(dout):
dispatch_out, handle = dout
moe_comm.combine(dispatch_out.tokens if expert_output is None else expert_output, handle, out=out)
stream = torch.cuda.current_stream()
# ---- runPairedBenchmark: warmup (paired), then per-iter timed (paired). ----
for _ in range(warmup):
dout = dispatch_fn()
stream.synchronize()
combine_fn(dout)
stream.synchronize()
dist.barrier(group=group)
# CUPTI/nsys region: capture ONLY the post-warmup timed kernels, matching
# ep_bench's KernelTimer.start() (called after warmup). An external nsys run
# with --capture-range=cudaProfilerApi records exactly the dispatch/combine
# kernels between these two calls.
_cupti = bool(getattr(args, "cupti_region", False))
if _cupti:
torch.cuda.synchronize()
dist.barrier(group=group)
torch.cuda.cudart().cudaProfilerStart()
# In-process CUPTI collector (ep_bench KernelTimer analog). start() after
# warmup, stop() after the timed loop -- same window as the CUDA events.
_inproc = None
inproc_requested = bool(getattr(args, "cupti_inproc", False))
local_inproc_ready = False
if inproc_requested:
try:
_inproc = _InProcCupti()
local_inproc_ready = True
except Exception as exc:
if rank == 0:
print(f"[warn] in-proc CUPTI unavailable ({exc}); host-observed only", flush=True)
_inproc = None
ready = torch.tensor(int(local_inproc_ready), dtype=torch.int32, device="cuda")
dist.all_reduce(ready, op=dist.ReduceOp.MIN, group=group)
if ready.item() == 0:
if rank == 0:
print("[warn] in-proc CUPTI unavailable on at least one rank; disabling globally", flush=True)
_inproc = None
else:
torch.cuda.synchronize()
dist.barrier(group=group)
try:
_rc = _inproc.start()
except Exception:
_rc = -1
started = torch.tensor(int(_rc == 0), dtype=torch.int32, device="cuda")
dist.all_reduce(started, op=dist.ReduceOp.MIN, group=group)
if started.item() == 0:
if _rc == 0:
_inproc.stop()
if rank == 0:
print("[warn] in-proc CUPTI failed to start on at least one rank; disabling globally", flush=True)
_inproc = None
dist.barrier(group=group)
d_start = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
d_end = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
c_start = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
c_end = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
for i in range(iters):
d_start[i].record(stream)
dout = dispatch_fn()
d_end[i].record(stream) # record before sync
stream.synchronize() # sync outside timing
c_start[i].record(stream) # record after sync, before combine
combine_fn(dout)
c_end[i].record(stream) # record before sync
stream.synchronize() # sync outside timing
dist.barrier(group=group) # keep ranks in lockstep, outside timing
torch.cuda.synchronize()
if _cupti:
torch.cuda.cudart().cudaProfilerStop()
ck_disp_us = ck_comb_us = 0.0
inproc_ok = False
if _inproc is not None:
_inproc.stop()
dist.barrier(group=group)
ck_disp_us = _inproc.avg_us("dispatch")
ck_comb_us = _inproc.avg_us("combine")
n_disp = _inproc.count("dispatch")
n_comb = _inproc.count("combine")
inproc_ok = ck_disp_us > 0 and ck_comb_us > 0
if os.environ.get("MSCCLPP_EP_KDEBUG", "0") == "1" and rank == 0:
print(
f"[kdebug inproc] dispatch: {ck_disp_us:.1f}us x{n_disp} " f"combine: {ck_comb_us:.1f}us x{n_comb}",
flush=True,
)
# ---- Collect per-iter times (ms->us) and trim the first (warmup outlier). --
disp_us = [d_start[i].elapsed_time(d_end[i]) * 1e3 for i in range(iters)]
comb_us = [c_start[i].elapsed_time(c_end[i]) * 1e3 for i in range(iters)]
tot_us = [d_start[i].elapsed_time(c_end[i]) * 1e3 for i in range(iters)]
if iters > 1:
disp_us, comb_us, tot_us = disp_us[1:], comb_us[1:], tot_us[1:]
def stats(times):
return sum(times) / len(times), min(times), max(times)
d_avg, d_min, d_max = stats(disp_us)
c_avg, c_min, c_max = stats(comb_us)
t_avg, t_min, t_max = stats(tot_us)
# per-rank throughput (GB/s) uses this rank's own byte count / its avg time.
d_tp = (disp_bytes / 1e9) / (d_avg * 1e-6)
c_tp = (comb_bytes / 1e9) / (c_avg * 1e-6)
t_tp = ((disp_bytes + comb_bytes) / 1e9) / (t_avg * 1e-6)
# ---- Cross-rank reduction (mirror printLowLatencyResults). ----
g_d_avg = _reduce_scalar(d_avg, dist.ReduceOp.SUM, group) / num_ranks
g_d_min = _reduce_scalar(d_min, dist.ReduceOp.MIN, group)
g_d_max = _reduce_scalar(d_max, dist.ReduceOp.MAX, group)
g_c_avg = _reduce_scalar(c_avg, dist.ReduceOp.SUM, group) / num_ranks
g_c_min = _reduce_scalar(c_min, dist.ReduceOp.MIN, group)
g_c_max = _reduce_scalar(c_max, dist.ReduceOp.MAX, group)
g_t_avg = _reduce_scalar(t_avg, dist.ReduceOp.SUM, group) / num_ranks
g_t_min = _reduce_scalar(t_min, dist.ReduceOp.MIN, group)
g_t_max = _reduce_scalar(t_max, dist.ReduceOp.MAX, group)
# ---- Kernel-only pass (torch.profiler / Kineto-CUPTI) — ep_bench parity. ----
# Measures device-side kernel time (strips host launch latency). Dispatch and
# combine are profiled in isolation so no kernel-name matching is required.
kernel_ok = False
g_dk_avg = g_dk_min = g_dk_max = 0.0
g_ck_avg = g_ck_min = g_ck_max = 0.0
if args.kernel_timing and not _cupti and not bool(getattr(args, "cupti_inproc", False)):
try:
dk_us, ck_us = _profile_paired_kernels(dispatch_fn, combine_fn, iters, stream, group, rank)
torch.cuda.synchronize()
dist.barrier(group=group)
g_dk_avg = _reduce_scalar(dk_us, dist.ReduceOp.SUM, group) / num_ranks
g_dk_min = _reduce_scalar(dk_us, dist.ReduceOp.MIN, group)
g_dk_max = _reduce_scalar(dk_us, dist.ReduceOp.MAX, group)
g_ck_avg = _reduce_scalar(ck_us, dist.ReduceOp.SUM, group) / num_ranks
g_ck_min = _reduce_scalar(ck_us, dist.ReduceOp.MIN, group)
g_ck_max = _reduce_scalar(ck_us, dist.ReduceOp.MAX, group)
kernel_ok = g_dk_avg > 0 and g_ck_avg > 0
except Exception as exc: # profiler unavailable / hiccup: keep host numbers valid
if rank == 0:
print(f"[warn] kernel-only pass failed ({exc}); reporting host-observed only", flush=True)
# ---- In-process CUPTI reduction (ep_bench KernelTimer analog). ----
g_ik_d_avg = g_ik_d_min = g_ik_d_max = 0.0
g_ik_c_avg = g_ik_c_min = g_ik_c_max = 0.0
g_inproc_ok = 0
if bool(getattr(args, "cupti_inproc", False)):
g_ik_d_avg = _reduce_scalar(ck_disp_us, dist.ReduceOp.SUM, group) / num_ranks
g_ik_d_min = _reduce_scalar(ck_disp_us if inproc_ok else 1e18, dist.ReduceOp.MIN, group)
g_ik_d_max = _reduce_scalar(ck_disp_us, dist.ReduceOp.MAX, group)
g_ik_c_avg = _reduce_scalar(ck_comb_us, dist.ReduceOp.SUM, group) / num_ranks
g_ik_c_min = _reduce_scalar(ck_comb_us if inproc_ok else 1e18, dist.ReduceOp.MIN, group)
g_ik_c_max = _reduce_scalar(ck_comb_us, dist.ReduceOp.MAX, group)
g_inproc_ok = int(_reduce_scalar(1.0 if inproc_ok else 0.0, dist.ReduceOp.MIN, group))
d_tp_all = _gather_scalars(d_tp, num_ranks, group)
c_tp_all = _gather_scalars(c_tp, num_ranks, group)
t_tp_all = _gather_scalars(t_tp, num_ranks, group)
if rank == 0:
# avg throughput uses rank-0 byte count / global avg time (as ep_bench does).
avg_d_tp = (disp_bytes / 1e9) / (g_d_avg * 1e-6)
avg_c_tp = (comb_bytes / 1e9) / (g_c_avg * 1e-6)
avg_t_tp = ((disp_bytes + comb_bytes) / 1e9) / (g_t_avg * 1e-6)
def minmax_rank(vals):
lo = min(range(num_ranks), key=lambda r: vals[r])
hi = max(range(num_ranks), key=lambda r: vals[r])
return vals[lo], lo, vals[hi], hi
d_lo, d_lo_r, d_hi, d_hi_r = minmax_rank(d_tp_all)
c_lo, c_lo_r, c_hi, c_hi_r = minmax_rank(c_tp_all)
t_lo, t_lo_r, t_hi, t_hi_r = minmax_rank(t_tp_all)
print(f"\n=== Summary (Low Latency, across {num_ranks} ranks) ===")
print("\n--- Host-observed performance ---")
print(f"Dispatch ({dispatch_label}): avg={g_d_avg:.2f} us, min={g_d_min:.2f} us, max={g_d_max:.2f} us")
print(
f" throughput: avg={avg_d_tp:.2f} GB/s, "
f"min={d_lo:.2f} GB/s (rank {d_lo_r}), max={d_hi:.2f} GB/s (rank {d_hi_r})"
)
print(f"Combine (BF16): avg={g_c_avg:.2f} us, min={g_c_min:.2f} us, max={g_c_max:.2f} us")
print(
f" throughput: avg={avg_c_tp:.2f} GB/s, "
f"min={c_lo:.2f} GB/s (rank {c_lo_r}), max={c_hi:.2f} GB/s (rank {c_hi_r})"
)
print(f"Total (D+C): avg={g_t_avg:.2f} us, min={g_t_min:.2f} us, max={g_t_max:.2f} us")
print(
f" throughput: avg={avg_t_tp:.2f} GB/s, "
f"min={t_lo:.2f} GB/s (rank {t_lo_r}), max={t_hi:.2f} GB/s (rank {t_hi_r})"
)
print("\n--- Kernel-only performance (device kernel time via torch.profiler/CUPTI) ---")
if kernel_ok:
# The LL dispatch kernel ends with a cross-rank receive spin-wait, so
# its device time includes wait skew. torch.profiler's host tracing
# overhead makes one rank lag, inflating that rank's dispatch device
# time into the ms range; the cross-rank MIN (the rank that did not
# wait) is the representative kernel floor and matches ep_bench's
# low-perturbation CUPTI number. Combine has little recv-spin and is
# stable across ranks. throughput uses the representative (min) time.
print(
f"Dispatch: min={g_dk_min:.2f} us (representative) "
f"[avg={g_dk_avg:.2f}, max={g_dk_max:.2f} us -- inflated by profiler recv-spin skew]"
)
print(f" throughput @min: {(disp_bytes / 1e9) / (g_dk_min * 1e-6):.2f} GB/s")
print(
f"Combine: min={g_ck_min:.2f} us (representative) "
f"[avg={g_ck_avg:.2f}, max={g_ck_max:.2f} us -- inflated by profiler rank skew]"
)
print(f" throughput @min: {(comb_bytes / 1e9) / (g_ck_min * 1e-6):.2f} GB/s")
print(
" NOTE: for an authoritative low-perturbation kernel-only number, run under "
"nsys (as ep_bench's CUPTI path does); torch.profiler perturbs the LL recv-spin."
)
else:
print(" NOTE: kernel-only pass disabled or unavailable.")
if bool(getattr(args, "cupti_inproc", False)):
print("\n--- Kernel-only performance (in-process CUPTI Activity API, ep_bench KernelTimer analog) ---")
if g_inproc_ok:
# The LL dispatch kernel ends with a cross-rank receive spin-wait,
# so a lagging rank's device time includes wait skew (same effect
# as nsys's max outlier). The cross-rank MIN (the rank that did not
# wait) is the representative kernel floor; it matches the nsys
# CUPTI number and ep_bench's low-perturbation figure. Combine has
# little recv-spin and is stable across ranks.
print(
f"Dispatch: min={g_ik_d_min:.2f} us (representative) "
f"[avg={g_ik_d_avg:.2f}, max={g_ik_d_max:.2f} us -- recv-spin skew on lagging ranks]"
)
print(f" throughput @min: {(disp_bytes / 1e9) / (g_ik_d_min * 1e-6):.2f} GB/s")
print(
f"Combine: min={g_ik_c_min:.2f} us (representative) "
f"[avg={g_ik_c_avg:.2f}, max={g_ik_c_max:.2f} us -- rank skew on lagging ranks]"
)
print(f" throughput @min: {(comb_bytes / 1e9) / (g_ik_c_min * 1e-6):.2f} GB/s")
else:
print(" NOTE: in-process CUPTI collector unavailable (see [warn] above).")
print(
f"\nByte counts: dispatch={disp_bytes / 1e6:.2f} MB ({dispatch_label}), "
f"combine={comb_bytes / 1e6:.2f} MB (BF16), selections={num_valid_selections}"
)
if __name__ == "__main__":
try:
main()
finally:
if dist.is_initialized():
try:
dist.barrier()
except Exception:
pass
try:
dist.destroy_process_group()
except Exception:
pass