Files
2026-07-13 13:18:33 +08:00

199 lines
7.6 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""
Unit test for the transparent SDMA allgather path in deepspeed.comm.
After ``deepspeed.init_distributed()`` returns, ``dist.all_gather_into_tensor``
on the WORLD process group is transparently routed through
``mori_cpp.AllGatherIntoTensor`` on AMD MI300 when mori is available, with
RCCL/NCCL as a fallback. This test exercises that path the same way
ZeRO-3's ``_all_gather_dtype`` does (flat output / per-rank shard input
with ``async_op=True``) and verifies correctness and algorithm bandwidth
for the common dtypes.
Usage:
cd examples/sdma_allgather
deepspeed --num_gpus 8 test_sdma_allgather_zero3.py
deepspeed --num_gpus 8 test_sdma_allgather_zero3.py --partition_sz 4194304 --iterations 50
"""
import argparse
import os
import numpy as np
import torch
import deepspeed
from deepspeed import comm as dist
from deepspeed.accelerator import get_accelerator
from deepspeed.comm import mori as _mori
def verify_allgather(partitions, world_size, partition_sz, rank, dtype):
"""Verify that each rank's partition contains the expected fill pattern."""
passed = True
for r in range(world_size):
chunk = partitions[r].narrow(0, 0, partition_sz).float().cpu()
expected_val = float(r + 1)
if not torch.allclose(chunk, torch.full_like(chunk, expected_val)):
unique_vals = chunk.unique()
print(f" [rank {rank}] FAIL: partition[{r}] expected all {expected_val}, "
f"got unique values: {unique_vals[:10]}")
passed = False
return passed
def run_single_allgather(rank, world_size, dtype, partition_sz, ag_stream):
"""Execute one allgather call following the ZeRO-3 ``_all_gather_dtype`` path."""
device = get_accelerator().current_device_name()
flat_tensor = torch.empty(partition_sz * world_size, dtype=dtype, device=device, requires_grad=False)
partitions = [flat_tensor.narrow(0, partition_sz * i, partition_sz) for i in range(world_size)]
partitions[rank].fill_(float(rank + 1))
with get_accelerator().stream(ag_stream):
handle = dist.allgather_fn(flat_tensor, partitions[rank], async_op=True)
with get_accelerator().stream(ag_stream):
handle.wait()
get_accelerator().current_stream().wait_stream(ag_stream)
return partitions
def run_bandwidth_test(rank, world_size, dtype, partition_sz, ag_stream, iterations, warmup):
"""Measure allgather bandwidth following the ZeRO-3 overlap pattern."""
device = get_accelerator().current_device_name()
elem_size = torch.tensor([], dtype=dtype).element_size()
total_bytes = partition_sz * elem_size * world_size
ev_start = get_accelerator().Event(enable_timing=True)
ev_end = get_accelerator().Event(enable_timing=True)
times_ms = []
for i in range(warmup + iterations):
flat_tensor = torch.empty(partition_sz * world_size, dtype=dtype, device=device, requires_grad=False)
partitions = [flat_tensor.narrow(0, partition_sz * r, partition_sz) for r in range(world_size)]
partitions[rank].fill_(float(rank + 1))
dist.barrier()
ev_start.record(ag_stream)
with get_accelerator().stream(ag_stream):
handle = dist.allgather_fn(flat_tensor, partitions[rank], async_op=True)
with get_accelerator().stream(ag_stream):
handle.wait()
ev_end.record(ag_stream)
ag_stream.synchronize()
elapsed_ms = ev_start.elapsed_time(ev_end)
if i >= warmup:
times_ms.append(elapsed_ms)
return times_ms, total_bytes
def main():
parser = argparse.ArgumentParser(description="Transparent SDMA allgather unit test")
parser.add_argument("--partition_sz", type=int, default=1024 * 1024, help="Elements per rank per allgather call")
parser.add_argument("--iterations", type=int, default=20, help="Number of measurement iterations")
parser.add_argument("--warmup", type=int, default=5, help="Number of warmup iterations")
parser.add_argument("--local_rank", type=int, default=int(os.environ.get("LOCAL_RANK", 0)))
parser = deepspeed.add_config_arguments(parser)
args = parser.parse_args()
deepspeed.init_distributed(dist_backend="cpu:gloo,cuda:nccl")
rank = dist.get_rank()
world_size = dist.get_world_size()
get_accelerator().set_device(args.local_rank)
if rank == 0:
backend = "SDMA (mori)" if _mori.is_enabled() else "RCCL/NCCL (mori unavailable or disabled)"
print(f"\n{'=' * 65}")
print(f" Transparent SDMA Allgather Unit Test")
print(f" world_size : {world_size}")
print(f" partition_sz : {args.partition_sz:,} elements")
print(f" iterations : {args.iterations} (warmup {args.warmup})")
print(f" backend : {backend}")
print(f"{'=' * 65}\n")
ag_stream = get_accelerator().Stream()
test_dtypes = [
("bfloat16", torch.bfloat16),
("float16", torch.float16),
("float32", torch.float32),
]
if rank == 0:
print("--- Correctness ---")
all_correct = True
for dtype_name, dtype in test_dtypes:
dist.barrier()
partitions = run_single_allgather(rank, world_size, dtype, args.partition_sz, ag_stream)
passed = verify_allgather(partitions, world_size, args.partition_sz, rank, dtype)
passed_t = torch.tensor([1 if passed else 0], dtype=torch.int32)
dist.all_reduce(passed_t, op=dist.ReduceOp.MIN)
ok = passed_t.item() == 1
if rank == 0:
elem_bytes = torch.tensor([], dtype=dtype).element_size()
data_mb = args.partition_sz * elem_bytes * world_size / (1024**2)
status = "PASSED" if ok else "FAILED"
print(f" {dtype_name:10s} data={data_mb:8.2f} MB {status}")
if not ok:
all_correct = False
if rank == 0:
print(f"\n--- Bandwidth (iterations={args.iterations}, warmup={args.warmup}) ---")
print(f" {'dtype':10s} {'data_MB':>10s} {'avg_ms':>9s} "
f"{'min_ms':>9s} {'max_ms':>9s} {'algo_BW':>12s}")
print(f" {'-'*10} {'-'*10} {'-'*9} {'-'*9} {'-'*9} {'-'*12}")
for dtype_name, dtype in test_dtypes:
dist.barrier()
times_ms, total_bytes = run_bandwidth_test(rank, world_size, dtype, args.partition_sz, ag_stream,
args.iterations, args.warmup)
avg_ms = np.mean(times_ms)
min_ms = np.min(times_ms)
max_ms = np.max(times_ms)
avg_t = torch.tensor([avg_ms], dtype=torch.float64)
min_t = torch.tensor([min_ms], dtype=torch.float64)
max_t = torch.tensor([max_ms], dtype=torch.float64)
dist.all_reduce(avg_t, op=dist.ReduceOp.SUM)
dist.all_reduce(min_t, op=dist.ReduceOp.MIN)
dist.all_reduce(max_t, op=dist.ReduceOp.MAX)
if rank == 0:
g_avg_ms = avg_t.item() / world_size
g_min_ms = min_t.item()
g_max_ms = max_t.item()
data_mb = total_bytes / (1024**2)
algo_bw_gbs = total_bytes / (g_avg_ms / 1000) / (1024**3)
print(f" {dtype_name:10s} {data_mb:10.2f} {g_avg_ms:9.3f} "
f"{g_min_ms:9.3f} {g_max_ms:9.3f} {algo_bw_gbs:9.2f} GB/s")
dist.barrier()
if rank == 0:
print()
print(f"Result: {'All correctness tests PASSED' if all_correct else 'Some correctness tests FAILED'}")
print(f"{'=' * 65}\n")
get_accelerator().synchronize()
dist.barrier()
if _mori.is_enabled():
import mori.shmem as shmem
shmem.shmem_finalize()
if __name__ == "__main__":
main()