Files
mscclpp/test/python/ep/test_internode_multirank.py
Binyang Li 8e34326d7a Binyli/ep revise (#828)
This pull request makes significant improvements to the MoE (Mixture of
Experts) Python API and documentation, focusing on clarifying and
expanding the Expert Parallel (EP) interface, especially around
quantization, dispatch/combine handles, and overlap configuration. The
changes introduce new data structures, update function signatures, and
improve documentation to better reflect the current and planned
capabilities of the system. Additionally, the base development container
is updated to CUDA 13.0, and minor corrections are made to extension
naming.
2026-07-06 21:14:29 -07:00

477 lines
22 KiB
Python

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
"""Multi-rank internode (HT) functional validation for mscclpp_ep.
Launch on each node with (example: 2 nodes x 8 GPUs = 16 ranks):
# on master (NODE_RANK=0):
MASTER_ADDR=<master_ip> MASTER_PORT=29600 NODE_RANK=0 \
torchrun --nnodes=2 --nproc_per_node=8 \
--rdzv-backend=c10d --rdzv-endpoint=<master_ip>:29600 \
test/python/ep/test_internode_multirank.py
# on worker (NODE_RANK=1):
MASTER_ADDR=<master_ip> MASTER_PORT=29600 NODE_RANK=1 \
torchrun --nnodes=2 --nproc_per_node=8 \
--rdzv-backend=c10d --rdzv-endpoint=<master_ip>:29600 \
test/python/ep/test_internode_multirank.py
Round-trip dispatch + combine using internode HT kernels across nodes.
Set ``MSCCLPP_EP_BENCH=1`` to also run a post-correctness benchmark pass
that times dispatch and combine **separately** with CUDA events. Reports
per-phase latency (max across ranks) plus aggregate effective bandwidth
(sum across ranks). Override iteration counts with
``MSCCLPP_EP_BENCH_WARMUP`` / ``MSCCLPP_EP_BENCH_ITERS`` and the bench
problem size with ``MSCCLPP_EP_BENCH_TOKENS`` / ``_HIDDEN``.
"""
from __future__ import annotations
import os
import sys
# Disable ProcessGroupNCCL's HeartbeatMonitor before importing torch.distributed.
# It runs in a background thread polling the TCPStore; under mpirun, rank 0
# (the store server) can exit before non-zero ranks finish teardown, producing
# noisy 'recvValue failed / Connection was likely closed' stack traces.
os.environ.setdefault("TORCH_NCCL_ENABLE_MONITORING", "0")
import torch
import torch.distributed as dist
def _detect_local_world_size():
"""Number of GPUs per node (4 on GB200, 8 on H100/A100, etc.).
Resolution order:
1. `MSCCLPP_EP_LOCAL_WORLD_SIZE` env var (matches the C++ side).
2. `LOCAL_WORLD_SIZE` (torchrun) or `OMPI_COMM_WORLD_LOCAL_SIZE` (mpirun).
3. `torch.cuda.device_count()` on the current host.
"""
for var in ("MSCCLPP_EP_LOCAL_WORLD_SIZE", "LOCAL_WORLD_SIZE", "OMPI_COMM_WORLD_LOCAL_SIZE"):
v = os.environ.get(var)
if v and int(v) > 0:
return int(v)
return max(1, torch.cuda.device_count())
def init_dist():
rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
local_world_size = _detect_local_world_size()
local_rank = int(os.environ.get("LOCAL_RANK", rank % local_world_size))
torch.cuda.set_device(local_rank)
dist.init_process_group(
backend="nccl", world_size=world_size, rank=rank, device_id=torch.device(f"cuda:{local_rank}")
)
return rank, world_size, local_rank, dist.new_group(list(range(world_size)))
def inplace_unique(x: torch.Tensor, num_slots: int):
assert x.dim() == 2
mask = x < 0
x_padded = x.masked_fill(mask, num_slots)
bin_count = torch.zeros((x.size(0), num_slots + 1), dtype=x.dtype, device=x.device)
bin_count.scatter_add_(1, x_padded, torch.ones_like(x_padded))
bin_count = bin_count[:, :num_slots]
sorted_bin_count, sorted_bin_idx = torch.sort(bin_count, dim=-1, descending=True)
sorted_bin_idx.masked_fill_(sorted_bin_count == 0, -1)
sorted_bin_idx = torch.sort(sorted_bin_idx, descending=True, dim=-1).values
x[:, :].fill_(-1)
valid_len = min(num_slots, x.size(1))
x[:, :valid_len] = sorted_bin_idx[:, :valid_len]
def main():
rank, num_ranks, local_rank, group = init_dist()
from mscclpp import CommGroup
import mscclpp.ep as ep
ep_group = CommGroup(torch_group=group)
NUM_MAX_NVL_PEERS = _detect_local_world_size()
assert (
num_ranks % NUM_MAX_NVL_PEERS == 0 and num_ranks > NUM_MAX_NVL_PEERS
), f"expected >1 node with {NUM_MAX_NVL_PEERS} GPUs each, got num_ranks={num_ranks}"
num_nodes = num_ranks // NUM_MAX_NVL_PEERS
num_local_ranks = NUM_MAX_NVL_PEERS
# Small settings for functional check
import os as _os
num_tokens = int(_os.environ.get("MSCCLPP_EP_BENCH_TOKENS", "128"))
hidden = int(_os.environ.get("MSCCLPP_EP_BENCH_HIDDEN", "1024"))
num_topk = int(_os.environ.get("MSCCLPP_EP_BENCH_TOPK", str(min(4, num_ranks))))
_experts_env = _os.environ.get("MSCCLPP_EP_BENCH_EXPERTS", "")
num_experts = int(_experts_env) if _experts_env else num_ranks * 4
assert num_experts % num_ranks == 0
torch.manual_seed(0xA1B2 + rank)
scores = torch.randn((num_tokens, num_experts), device="cuda", dtype=torch.float32).abs() + 1
topk_idx = torch.topk(scores, num_topk, dim=-1, sorted=False).indices
topk_weights = torch.ones((num_tokens, num_topk), dtype=torch.float32, device="cuda")
rank_idx = topk_idx // (num_experts // num_ranks)
rank_idx.masked_fill_(topk_idx == -1, -1)
inplace_unique(rank_idx, num_ranks)
rdma_rank_idx = rank_idx // num_local_ranks
rdma_rank_idx.masked_fill_(rank_idx == -1, -1)
inplace_unique(rdma_rank_idx, num_nodes)
num_tokens_per_expert = torch.zeros((num_experts,), dtype=torch.int, device="cuda")
for i in range(num_experts):
num_tokens_per_expert[i] = (topk_idx == i).sum()
num_tokens_per_rank = torch.empty((num_ranks,), dtype=torch.int, device="cuda")
num_tokens_per_rdma_rank = torch.empty((num_nodes,), dtype=torch.int, device="cuda")
token_idx_in_rank = torch.full((num_ranks, num_tokens), -1, dtype=torch.long, device="cuda")
for i in range(num_ranks):
num_tokens_per_rank[i] = (rank_idx == i).sum()
token_sel = (rank_idx == i).max(dim=-1).values
cnt = token_sel.sum().item()
tokens = torch.sort(token_sel.to(torch.int), descending=True).indices
tokens[:cnt] = torch.sort(tokens[:cnt]).values
token_idx_in_rank[i][tokens[:cnt]] = torch.arange(cnt, dtype=torch.long, device="cuda")
for i in range(num_nodes):
num_tokens_per_rdma_rank[i] = (rdma_rank_idx == i).sum()
token_idx_in_rank = token_idx_in_rank.T.contiguous().to(torch.int)
is_token_in_rank = token_idx_in_rank >= 0
x = torch.ones((num_tokens, hidden), dtype=torch.bfloat16, device="cuda") * float(rank)
moe = ep.MoECommunicator(
comm=ep_group,
num_experts=num_experts,
hidden_size=hidden,
topk=num_topk,
max_tokens_per_rank=num_tokens,
mode=ep.MoEMode.HIGH_THROUGHPUT,
num_sms=int(os.environ.get("MSCCLPP_EP_NSM", "152")),
nvl_chunked_send=int(os.environ.get("MSCCLPP_EP_NVL_SEND", "8")),
nvl_chunked_recv=int(os.environ.get("MSCCLPP_EP_NVL_RECV", "256")),
rdma_chunked_send=int(os.environ.get("MSCCLPP_EP_RDMA_SEND", "16")),
rdma_chunked_recv=int(os.environ.get("MSCCLPP_EP_RDMA_RECV", "128")),
)
if rank == 0:
print(
f"[cfg] num_nodes={num_nodes} num_ranks={num_ranks} num_tokens={num_tokens} "
f"hidden={hidden} num_experts={num_experts} num_topk={num_topk}",
flush=True,
)
print(
f"[rank {rank}] MoECommunicator created is_available={moe.is_available()} "
f"is_internode={moe.is_internode_available()}",
flush=True,
)
assert moe.is_available() and moe.is_internode_available()
assert moe.is_internode(), "expected the communicator to select the internode HT transport"
dispatch_out, handle = moe.dispatch(
x,
topk_idx,
topk_weights,
)
recv_x = dispatch_out.tokens
dist.barrier(group=group)
assert recv_x.dim() == 2 and recv_x.size(1) == hidden
local_experts = num_experts // num_ranks
all_expert_counts = torch.empty((num_ranks, num_experts), dtype=num_tokens_per_expert.dtype, device="cuda")
dist.all_gather_into_tensor(all_expert_counts, num_tokens_per_expert, group=group)
expected_counts = all_expert_counts[:, rank * local_experts : (rank + 1) * local_experts].sum(dim=0).cpu().tolist()
assert dispatch_out.layout.num_tokens_per_expert is not None
actual_counts = [int(count) for count in dispatch_out.layout.num_tokens_per_expert]
assert actual_counts == [int(count) for count in expected_counts]
if rank == 0:
print(f"[dispatch] OK (recv {recv_x.size(0)} tokens)", flush=True)
# Keep the existing dispatch/combine phase guard for internode HT until the
# backend wires a proper stream-dependency hand-off.
torch.cuda.synchronize()
dist.barrier(group=group)
combined_x = moe.combine(recv_x, handle)
num_dst = is_token_in_rank.sum(dim=1).to(torch.float32)
expected = num_dst * float(rank)
got = combined_x.float().mean(dim=1)
diff = (got - expected).abs().max().item()
max_exp = expected.abs().max().item()
print(f"[combine r{rank}] max|got-expected|={diff:.4e} max|expected|={max_exp:.4e}", flush=True)
# bf16 accumulator has 7-bit mantissa; intermediate partial sums can
# round at ulp = max_exp * 2**-7. Use a tolerance that scales with magnitude.
tol = max(1e-2, max_exp * (1.0 / 64))
assert diff <= tol, f"rank{rank}: combine mismatch max diff {diff} > tol {tol} (max_exp={max_exp})"
dist.barrier(group=group)
if rank == 0:
print("PASS", flush=True)
# ------------------------------------------------------------------
# Optional benchmark (enable with MSCCLPP_EP_BENCH=1).
# ------------------------------------------------------------------
if os.environ.get("MSCCLPP_EP_BENCH", "0") != "1":
return
warmup = int(os.environ.get("MSCCLPP_EP_BENCH_WARMUP", "5"))
iters = int(os.environ.get("MSCCLPP_EP_BENCH_ITERS", "20"))
bench_tokens = int(os.environ.get("MSCCLPP_EP_BENCH_TOKENS", "4096"))
bench_hidden = int(os.environ.get("MSCCLPP_EP_BENCH_HIDDEN", "7168"))
# Allow overriding num_experts / num_topk for the bench phase to match
# NCCL-EP's `ep_bench -a ht` defaults (256 experts, top-8). The functional
# check above still uses the smaller (num_experts=num_ranks*4, topk=4)
# configuration.
bench_num_experts = int(os.environ.get("MSCCLPP_EP_BENCH_EXPERTS", str(num_experts)))
bench_num_topk = int(os.environ.get("MSCCLPP_EP_BENCH_TOPK", str(num_topk)))
if bench_num_experts % num_ranks != 0:
if rank == 0:
print(
f"[bench] skip: num_experts={bench_num_experts} not divisible " f"by num_ranks={num_ranks}", flush=True
)
return
if bench_num_topk > bench_num_experts:
if rank == 0:
print(f"[bench] skip: topk={bench_num_topk} > experts={bench_num_experts}", flush=True)
return
scores_b = torch.randn((bench_tokens, bench_num_experts), device="cuda", dtype=torch.float32).abs() + 1
topk_idx_b = torch.topk(scores_b, bench_num_topk, dim=-1, sorted=False).indices
topk_weights_b = torch.ones((bench_tokens, bench_num_topk), dtype=torch.float32, device="cuda")
rank_idx_b = topk_idx_b // (bench_num_experts // num_ranks)
rank_idx_b.masked_fill_(topk_idx_b == -1, -1)
inplace_unique(rank_idx_b, num_ranks)
rdma_rank_idx_b = rank_idx_b // num_local_ranks
rdma_rank_idx_b.masked_fill_(rank_idx_b == -1, -1)
inplace_unique(rdma_rank_idx_b, num_nodes)
num_tokens_per_expert_b = torch.zeros((bench_num_experts,), dtype=torch.int, device="cuda")
for i in range(bench_num_experts):
num_tokens_per_expert_b[i] = (topk_idx_b == i).sum()
num_tokens_per_rank_b = torch.empty((num_ranks,), dtype=torch.int, device="cuda")
num_tokens_per_rdma_rank_b = torch.empty((num_nodes,), dtype=torch.int, device="cuda")
token_idx_in_rank_b = torch.full((num_ranks, bench_tokens), -1, dtype=torch.long, device="cuda")
for i in range(num_ranks):
num_tokens_per_rank_b[i] = (rank_idx_b == i).sum()
token_sel = (rank_idx_b == i).max(dim=-1).values
cnt = token_sel.sum().item()
tokens = torch.sort(token_sel.to(torch.int), descending=True).indices
tokens[:cnt] = torch.sort(tokens[:cnt]).values
token_idx_in_rank_b[i][tokens[:cnt]] = torch.arange(cnt, dtype=torch.long, device="cuda")
for i in range(num_nodes):
num_tokens_per_rdma_rank_b[i] = (rdma_rank_idx_b == i).sum()
token_idx_in_rank_b = token_idx_in_rank_b.T.contiguous().to(torch.int)
is_token_in_rank_b = token_idx_in_rank_b >= 0
x_b = torch.ones((bench_tokens, bench_hidden), dtype=torch.bfloat16, device="cuda") * float(rank)
# Drive the benchmark through the public high-level API. The communicator
# auto-selects internode HT when the RDMA size hint is non-zero. The first
# (uncached) dispatch records routing layout on the returned handle;
# subsequent dispatches reuse it via previous_handle, skipping host-side
# layout computation. This isolates the on-GPU dispatch-kernel cost
# (NCCL-EP ep_bench convention).
moe = ep.MoECommunicator(
comm=ep_group,
num_experts=bench_num_experts,
hidden_size=bench_hidden,
topk=bench_num_topk,
max_tokens_per_rank=bench_tokens,
mode=ep.MoEMode.HIGH_THROUGHPUT,
num_sms=int(os.environ.get("MSCCLPP_EP_NSM", "152")),
nvl_chunked_send=int(os.environ.get("MSCCLPP_EP_NVL_SEND", "8")),
nvl_chunked_recv=int(os.environ.get("MSCCLPP_EP_NVL_RECV", "256")),
rdma_chunked_send=int(os.environ.get("MSCCLPP_EP_RDMA_SEND", "16")),
rdma_chunked_recv=int(os.environ.get("MSCCLPP_EP_RDMA_RECV", "128")),
)
assert moe.is_available() and moe.is_internode_available()
assert moe.is_internode(), "expected the communicator to select the internode HT transport"
# One uncached dispatch to build the cached routing layout on the handle.
_handle0 = moe.dispatch(x_b, topk_idx_b, topk_weights_b)[1]
def _dispatch_cached():
return moe.dispatch(x_b, topk_idx_b, topk_weights_b, previous_handle=_handle0)
def _combine(dout):
dispatch_out_, handle_ = dout
moe.combine(dispatch_out_.tokens, handle_)
# Warmup (full round-trip with the sync/barrier guard between phases,
# matching the correctness-path invariant: internode combine must observe
# the completed dispatch outputs before it launches).
for _ in range(warmup):
dout = _dispatch_cached()
torch.cuda.synchronize()
dist.barrier(group=group)
_combine(dout)
torch.cuda.synchronize()
dist.barrier(group=group)
# Time dispatch alone (cached mode -- skips the host-side layout computation).
start_ev = torch.cuda.Event(enable_timing=True)
end_ev = torch.cuda.Event(enable_timing=True)
start_ev.record()
dout = None
for _ in range(iters):
dout = _dispatch_cached()
end_ev.record()
torch.cuda.synchronize()
disp_us = start_ev.elapsed_time(end_ev) * 1e3 / iters
# Required guard before combine sees the dispatch outputs (see correctness
# path's XXX note). Not included in either phase's timing.
torch.cuda.synchronize()
dist.barrier(group=group)
# Time combine alone (reusing the same dispatch output each iter).
start_ev.record()
for _ in range(iters):
_combine(dout)
end_ev.record()
torch.cuda.synchronize()
comb_us = start_ev.elapsed_time(end_ev) * 1e3 / iters
# Per-rank "send bytes" matches NCCL-EP's `ep_bench` accounting (`RDMA_send`):
# bench_tokens * hidden * sizeof(bf16). Each rank ships its `bench_tokens`
# input rows out (some replicated to multiple peers); NCCL-EP normalizes by
# the input footprint, not by the recv-side fan-out. We use the same
# convention here so `per_rank_bw` is directly comparable across stacks.
bytes_one_way = bench_tokens * bench_hidden * x_b.element_size()
# NCCL-EP `ep_bench` six-metric breakdown.
# Send-side accounting follows NCCL-EP: count unique (token, dst_node) pairs.
# `num_tokens_per_rdma_rank_b[n]` is exactly that count for node `n`.
# Recv-side accounting: each rank reports `num_tokens_per_rank_b[r]`
# (tokens it sends to dst rank `r`); an `all_to_all_single` lets every
# rank read how many tokens each source rank sent to it.
bytes_per_token = bench_hidden * x_b.element_size()
local_node = rank // num_local_ranks
nodes_unique = num_tokens_per_rdma_rank_b.to(torch.int64)
total_send_tokens_local = int(nodes_unique.sum().item())
nvl_send_tokens_local = int(nodes_unique[local_node].item())
rdma_send_tokens_local = total_send_tokens_local - nvl_send_tokens_local
# Replaced dist.all_to_all_single (NCCL socket transport fails with
# NCCL_IB_DISABLE=1 internode) with all_gather_into_tensor + transpose,
# which works on the same socket-NCCL setup the LL test uses.
_send_row = num_tokens_per_rank_b.to(torch.int64).contiguous()
_gathered = torch.empty(num_ranks * num_ranks, dtype=torch.int64, device="cuda")
dist.all_gather_into_tensor(_gathered, _send_row, group=group)
recv_from_src = _gathered.view(num_ranks, num_ranks)[:, rank].contiguous()
src_node = torch.arange(num_ranks, device="cuda") // num_local_ranks
remote_mask = (src_node != local_node).to(torch.int64)
total_recv_tokens_local = int(recv_from_src.sum().item())
rdma_recv_tokens_local = int((recv_from_src * remote_mask).sum().item())
# Average per-rank token counts across ranks (matches NCCL-EP `Byte counts (per rank avg)`).
counts_t = torch.tensor(
[total_send_tokens_local, rdma_send_tokens_local, total_recv_tokens_local, rdma_recv_tokens_local],
dtype=torch.float64,
device="cuda",
)
dist.all_reduce(counts_t, op=dist.ReduceOp.SUM, group=group)
counts_avg = (counts_t / num_ranks).tolist()
total_send_avg, rdma_send_avg, total_recv_avg, rdma_recv_avg = counts_avg
total_send_bytes = total_send_avg * bytes_per_token
rdma_send_bytes = rdma_send_avg * bytes_per_token
total_recv_bytes = total_recv_avg * bytes_per_token
rdma_recv_bytes = rdma_recv_avg * bytes_per_token
nvl_send_bytes = total_send_bytes - rdma_send_bytes
nvl_recv_bytes = total_recv_bytes - rdma_recv_bytes
# Reduce timings: report min/avg/max and base BW on AVG to match NCCL-EP's
# `ep_bench.cu` convention.
disp_min_t = torch.tensor([disp_us], dtype=torch.float64, device="cuda")
disp_avg_t = torch.tensor([disp_us], dtype=torch.float64, device="cuda")
disp_max_t = torch.tensor([disp_us], dtype=torch.float64, device="cuda")
comb_min_t = torch.tensor([comb_us], dtype=torch.float64, device="cuda")
comb_avg_t = torch.tensor([comb_us], dtype=torch.float64, device="cuda")
comb_max_t = torch.tensor([comb_us], dtype=torch.float64, device="cuda")
dist.all_reduce(disp_min_t, op=dist.ReduceOp.MIN, group=group)
dist.all_reduce(disp_avg_t, op=dist.ReduceOp.SUM, group=group)
dist.all_reduce(disp_max_t, op=dist.ReduceOp.MAX, group=group)
dist.all_reduce(comb_min_t, op=dist.ReduceOp.MIN, group=group)
dist.all_reduce(comb_avg_t, op=dist.ReduceOp.SUM, group=group)
dist.all_reduce(comb_max_t, op=dist.ReduceOp.MAX, group=group)
disp_avg_us = disp_avg_t.item() / num_ranks
comb_avg_us = comb_avg_t.item() / num_ranks
disp_bw_per_rank = bytes_one_way / (disp_avg_us * 1e-6) / 1e9
comb_bw_per_rank = bytes_one_way / (comb_avg_us * 1e-6) / 1e9
# Six-metric BW (NCCL-EP convention). Combine reverses send<->recv:
# in combine, this rank pushes back what it received in dispatch.
disp_t_s = disp_avg_us * 1e-6
comb_t_s = comb_avg_us * 1e-6
d_send_total_bw = total_send_bytes / disp_t_s / 1e9
d_send_nvl_bw = nvl_send_bytes / disp_t_s / 1e9
d_send_rdma_bw = rdma_send_bytes / disp_t_s / 1e9
d_recv_total_bw = total_recv_bytes / disp_t_s / 1e9
d_recv_nvl_bw = nvl_recv_bytes / disp_t_s / 1e9
d_recv_rdma_bw = rdma_recv_bytes / disp_t_s / 1e9
c_send_total_bw = total_recv_bytes / comb_t_s / 1e9
c_send_nvl_bw = nvl_recv_bytes / comb_t_s / 1e9
c_send_rdma_bw = rdma_recv_bytes / comb_t_s / 1e9
c_recv_total_bw = total_send_bytes / comb_t_s / 1e9
c_recv_nvl_bw = nvl_send_bytes / comb_t_s / 1e9
c_recv_rdma_bw = rdma_send_bytes / comb_t_s / 1e9
if rank == 0:
print(
f"[bench internode HT] nodes={num_nodes} num_ranks={num_ranks} "
f"tokens={bench_tokens} hidden={bench_hidden} "
f"experts={bench_num_experts} topk={bench_num_topk} "
f"warmup={warmup} iters={iters}",
flush=True,
)
print(
f" dispatch: avg={disp_avg_us:.1f}us min={disp_min_t.item():.1f}us max={disp_max_t.item():.1f}us "
f"per_rank_bw={disp_bw_per_rank:.2f} GB/s "
f"agg_bw={disp_bw_per_rank * num_ranks:.2f} GB/s (BW @ avg time)",
flush=True,
)
print(
f" send: total={d_send_total_bw:.2f} nvl={d_send_nvl_bw:.2f} rdma={d_send_rdma_bw:.2f} GB/s "
f"recv: total={d_recv_total_bw:.2f} nvl={d_recv_nvl_bw:.2f} rdma={d_recv_rdma_bw:.2f} GB/s",
flush=True,
)
print(
f" combine : avg={comb_avg_us:.1f}us min={comb_min_t.item():.1f}us max={comb_max_t.item():.1f}us "
f"per_rank_bw={comb_bw_per_rank:.2f} GB/s "
f"agg_bw={comb_bw_per_rank * num_ranks:.2f} GB/s (BW @ avg time)",
flush=True,
)
print(
f" send: total={c_send_total_bw:.2f} nvl={c_send_nvl_bw:.2f} rdma={c_send_rdma_bw:.2f} GB/s "
f"recv: total={c_recv_total_bw:.2f} nvl={c_recv_nvl_bw:.2f} rdma={c_recv_rdma_bw:.2f} GB/s",
flush=True,
)
print(
f" byte counts (per rank avg): "
f"total_send={total_send_bytes/1e6:.2f} MB ({total_send_avg:.0f} tok) "
f"rdma_send={rdma_send_bytes/1e6:.2f} MB ({rdma_send_avg:.0f} tok) "
f"total_recv={total_recv_bytes/1e6:.2f} MB ({total_recv_avg:.0f} tok) "
f"rdma_recv={rdma_recv_bytes/1e6:.2f} MB ({rdma_recv_avg:.0f} tok)",
flush=True,
)
if __name__ == "__main__":
try:
main()
except Exception:
import traceback
traceback.print_exc()
sys.exit(1)
finally:
# Ordered shutdown: barrier so every rank reaches teardown before the
# TCPStore server (rank 0) exits, then destroy the PG. Without this,
# ProcessGroupNCCL's HeartbeatMonitor on non-zero ranks logs noisy
# "recvValue failed / Connection was likely closed" stack traces.
if dist.is_initialized():
try:
dist.barrier()
except Exception:
pass
dist.destroy_process_group()