Files
sglang/python/sglang/jit_kernel/tests/test_custom_all_reduce.py
T

240 lines
7.5 KiB
Python

"""
Correctness test for the JIT custom all-reduce (v2) kernel.
The test compares the JIT custom all-reduce output against NCCL all-reduce
for various tensor sizes and dtypes, in both eager and CUDA-graph modes.
Usage:
python -m pytest test_jit_custom_all_reduce.py -v
This file doubles as the torchrun worker script. The test class launches
torchrun --nproc_per_node=N <this_file>
and asserts that all worker processes exit successfully.
"""
from __future__ import annotations
import itertools
import logging
import multiprocessing as mp
import os
from typing import Dict, Optional, Tuple
import pytest
import torch
import torch.distributed as dist
import sglang.srt.distributed.parallel_state as ps
from sglang.jit_kernel.all_reduce import (
AllReduceAlgo,
_jit_custom_all_reduce_pull_module,
_jit_custom_all_reduce_push_module,
)
from sglang.jit_kernel.tests.utils import multiprocess_main, multiprocess_test
from sglang.srt.distributed.device_communicators.custom_all_reduce_v2 import (
CustomAllReduceV2,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(
est_time=300,
suite="base-b-kernel-unit-8-gpu-h200",
)
register_cuda_ci(
est_time=300,
suite="nightly-kernel-8-gpu-h200",
nightly=True,
)
# ---------------------------------------------------------------------------
# Test parameters (shared between test class and worker)
# ---------------------------------------------------------------------------
TEST_SIZES = [
16,
32,
512,
1024,
1024 + 16, # weird case
4 * 1024,
32 * 1024,
256 * 1024,
2 * 1024 * 1024, # 2M elements
4 * 1024 * 1024, # 4M elements
]
TEST_DTYPES = [torch.float16, torch.bfloat16, torch.float32]
SHOTS = [
AllReduceAlgo.ONE_SHOT_PULL,
AllReduceAlgo.ONE_SHOT_PUSH,
AllReduceAlgo.TWO_SHOT_PULL,
]
USE_GRAPH_OPTIONS = [True, False]
TEST_CONFIG = itertools.product(TEST_SIZES, TEST_DTYPES, SHOTS, USE_GRAPH_OPTIONS)
TEST_LAYERS = 4
TEST_LOOP = 16
# ---------------------------------------------------------------------------
# Test class (runs via pytest, launches torchrun subprocesses)
# ---------------------------------------------------------------------------
def _compile_one(dtype: torch.dtype, world_size: int):
_jit_custom_all_reduce_push_module(dtype, world_size)
_jit_custom_all_reduce_pull_module(dtype, world_size)
def _precompile_kernels() -> None:
# NOTE: even when device count < 8, we should be able to compile all
process_map: Dict[Tuple[torch.dtype, int], mp.Process] = {}
COMPILE_SPACE = itertools.product(TEST_DTYPES, [2, 3, 4, 5, 6, 7, 8])
mp.set_start_method("spawn")
for config in COMPILE_SPACE:
process_map[config] = mp.Process(target=_compile_one, args=config)
for process in process_map.values():
process.start()
for (dtype, world_size), process in process_map.items():
process.join()
if process.exitcode != 0:
raise RuntimeError(f"Custom All Reduce {world_size=} {dtype=} failed")
@pytest.mark.parametrize("nproc", [1, 2, 3, 4, 5, 6, 7, 8])
def test_custom_allreduce(nproc: int) -> None:
if nproc == 1: # NOTE: special case to speed up tests
return _precompile_kernels()
device_count = torch.cuda.device_count()
if device_count < nproc:
pytest.skip(
f"Requires at least {nproc} GPUs, but only {device_count} available"
)
multiprocess_test(__file__, nproc)
# ---------------------------------------------------------------------------
# Worker logic (executed by each torchrun process)
# ---------------------------------------------------------------------------
def init_distributed():
"""Initialize distributed groups via torchrun env vars.
Returns (rank, device, cpu_group, nccl_group, comm).
"""
local_rank = int(os.environ["LOCAL_RANK"])
world_size = int(os.environ["WORLD_SIZE"])
rank = local_rank
device = torch.device(f"cuda:{rank}")
torch.cuda.set_device(device)
dist.init_process_group(backend="gloo")
ps._WORLD = coord = ps.init_world_group(
ranks=list(range(world_size)),
local_rank=local_rank,
backend="nccl",
)
cpu_group = coord.cpu_group
nccl_group = coord.device_group
assert nccl_group is not None
max_size = max(TEST_SIZES) * 4
comm = CustomAllReduceV2(cpu_group, device, max_size, max_size)
if comm.disabled:
raise RuntimeError("JIT CustomAllReduceV2 is disabled on this system")
return rank, device, cpu_group, nccl_group, comm
@torch.inference_mode()
def worker_test(
device: torch.device,
nccl_group: dist.ProcessGroup,
comm: CustomAllReduceV2,
size: int,
dtype: torch.dtype,
use_graph: bool,
algo: AllReduceAlgo,
) -> Optional[RuntimeError]:
comm.override_algo = algo
def get_run_graph_fn():
graph = torch.cuda.CUDAGraph()
graph_inp = torch.zeros((TEST_LAYERS, size), dtype=dtype, device=device)
out_jits = []
with comm.capture():
with torch.cuda.graph(graph):
for i in range(TEST_LAYERS):
out_jits.append(comm.custom_all_reduce(graph_inp[i]))
out_jit = torch.stack(out_jits)
torch.cuda.synchronize()
def run_graph(x: torch.Tensor) -> torch.Tensor:
graph_inp.copy_(x)
graph.replay()
return out_jit.clone()
return run_graph
def get_run_eager_fn():
def run_eager(x: torch.Tensor) -> torch.Tensor:
eager_inp = x.clone()
out_eagers = []
for i in range(TEST_LAYERS):
out_eagers.append(comm.custom_all_reduce(eager_inp[i]))
torch.cuda.synchronize()
return torch.stack(out_eagers)
return run_eager
run_fn = get_run_graph_fn() if use_graph else get_run_eager_fn()
num_errors = 0
for _ in range(TEST_LOOP):
# NOTE: 15 * 8 < 128, which is the precision limit for bf16
inp = torch.randint(0, 16, (TEST_LAYERS, size), dtype=dtype, device=device)
assert comm.should_custom_ar(inp[0])
out_ref = inp.clone()
dist.all_reduce(out_ref, group=nccl_group)
out_jit = run_fn(inp)
num_errors += not torch.all(out_jit == out_ref)
if num_errors > 0:
return RuntimeError(
f"Test failed for {size=}, {dtype=}, {algo=}, "
f"{use_graph=} with {num_errors} errors. "
)
return None
def worker_main() -> None:
"""Entry point for each torchrun worker process."""
rank, device, cpu_group, nccl_group, comm = init_distributed()
torch.cuda.set_stream(torch.cuda.Stream())
logging.disable(logging.INFO) # Suppress internal logging for cleaner test output
items = list(enumerate(TEST_CONFIG))
for i, (size, dtype, algo, use_graph) in items:
error = worker_test(device, nccl_group, comm, size, dtype, use_graph, algo)
if error is not None:
print(
f"Worker {rank} failed for {size=}, {dtype=}, "
f"{algo=}, {use_graph=}, iteration={i}\n"
f"Error: {error}"
)
# communicate the result to rank 0 for logging
result = torch.tensor([int(error is not None)])
dist.all_reduce(result, group=cpu_group)
failed = bool(result.item())
if failed:
raise RuntimeError(
f"Test failed on rank {rank} for config: "
f"{size=}, {dtype=}, {algo=}, {use_graph=}"
)
comm.close()
dist.destroy_process_group()
if __name__ == "__main__":
multiprocess_main(__file__, worker_main)