Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

259 lines
8.0 KiB
Python

"""
Multi-GPU test for CommManager.
Spawns real distributed workers, initializes torch.distributed + NCCL,
and runs CommManager's communication cycle with real GPU tensors:
pre_attn(AG) → attn → post_attn(RS) → pre_dense(AG) → dense → post_dense(RS)
Verifies that with attn_tp ≠ dense_tp and uneven token counts, each rank
recovers its original hidden states after the full cycle.
"""
import socket
from typing import List, Optional
import pytest
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
def get_open_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
return s.getsockname()[1]
def build_scattered(dp_tokens: List[int], tp_size: int) -> List[int]:
"""Build per-rank scattered token counts from per-DP-rank token counts.
For each DP rank, divides its tokens across tp_size ranks:
e.g. dp_tokens=[1, 100], tp_size=2 → [1, 0, 50, 50]
"""
scattered = []
for tokens in dp_tokens:
base, rem = divmod(tokens, tp_size)
scattered.extend([base + 1] * rem)
scattered.extend([base] * (tp_size - rem))
return scattered
class FakeBatch:
def __init__(self, attn_tl, dense_tl, moe_tl):
self.attn_tp_group_scattered_num_tokens = attn_tl
self.dense_tp_group_scattered_num_tokens = dense_tl
self.moe_tp_ep_group_scattered_num_tokens = moe_tl
def make_batch(mapping, scattered):
a_s = mapping.attn.tp_size * mapping.attn.dp_rank
d_s = mapping.dense.tp_size * mapping.dense.dp_rank
m_s = mapping.moe.tp_ep_size * mapping.moe.dp_rank
return FakeBatch(
scattered[a_s : a_s + mapping.attn.tp_size],
scattered[d_s : d_s + mapping.dense.tp_size],
scattered[m_s : m_s + mapping.moe.tp_ep_size],
)
# ---------------------------------------------------------------------------
# Worker: runs on each GPU
# ---------------------------------------------------------------------------
def worker_fn(
rank, world_size, port, attn_tp, dense_tp, dp_tokens, hidden_size, error_dict
):
try:
_worker_main(rank, world_size, port, attn_tp, dense_tp, dp_tokens, hidden_size)
except Exception as e:
import traceback
error_dict[rank] = traceback.format_exc()
def _worker_main(rank, world_size, port, attn_tp, dense_tp, dp_tokens, hidden_size):
import sys
def dbg(msg):
print(f"[Rank {rank}] {msg}", flush=True, file=sys.stderr)
device = torch.device(f"cuda:{rank}")
torch.cuda.set_device(device)
from tokenspeed.runtime.distributed.mapping import Mapping
from tokenspeed.runtime.distributed.process_group_manager import (
process_group_manager as pg_manager,
)
mapping = Mapping(
rank=rank,
world_size=world_size,
attn_tp_size=attn_tp,
attn_cp_size=1,
dense_tp_size=dense_tp,
)
# --- Initialize distributed via ProcessGroupManager ---
pg_manager.init_distributed(
mapping=mapping,
distributed_init_method=f"tcp://localhost:{port}",
backend="nccl",
)
# Pre-create all process groups needed by CommManager.
# Order is fixed across all ranks; _make_all_groups ensures identical new_group calls.
for group in [
mapping.attn.tp_group,
mapping.dense.tp_group,
mapping.moe.tp_ep_group,
]:
if len(group) > 1:
pg_manager.init_process_group(group)
# --- Set up global state that CommManager depends on ---
from tokenspeed.runtime.utils.env import global_server_args_dict
max_tokens = max(sum(dp_tokens), 1)
global_server_args_dict["chunked_prefill_size"] = max_tokens * 2
global_server_args_dict["max_prefill_tokens"] = max_tokens * 2
global_server_args_dict["max_model_len"] = 4096
global_server_args_dict["enable_allreduce_fusion"] = False
global_server_args_dict["force_deterministic_rsag"] = True
global_server_args_dict["mapping"] = mapping
from tokenspeed.runtime.distributed.comm_manager import CommManager
cm = CommManager(
mapping=mapping,
layer_id=1,
is_moe=False,
prev_is_moe=False,
)
# --- Token distribution ---
scattered = build_scattered(dp_tokens, attn_tp)
batch = make_batch(mapping, scattered)
# In all-reduce mode, all ranks in a TP group hold the same replicated tokens,
# so each rank has dp_tokens[dp_rank] tokens (not the scattered per-rank count).
is_all_reduce = cm.use_all_reduce(cm.is_moe)
if is_all_reduce:
my_num_tokens = dp_tokens[mapping.attn.dp_rank]
else:
my_num_tokens = scattered[rank]
dist.barrier()
# --- Create input tensor on GPU ---
torch.manual_seed(42 if is_all_reduce else 42 + rank)
original = torch.randn(
my_num_tokens, hidden_size, dtype=torch.bfloat16, device=device
)
hidden = original.clone()
residual = original.clone()
# === Full communication cycle ===
dbg(f"tokens={my_num_tokens}, pre_attn_comm")
hidden = cm.pre_attn_comm(hidden, batch)
dbg(f"pre_attn_comm done, shape={hidden.shape}")
hidden = hidden / attn_tp
dbg("post_attn_comm")
hidden, residual = cm.post_attn_comm(hidden, residual, batch)
dbg(f"post_attn_comm done, shape={hidden.shape}")
dbg("pre_dense_comm")
hidden = cm.pre_dense_comm(hidden, batch)
dbg(f"pre_dense_comm done, shape={hidden.shape}")
hidden = hidden / dense_tp
dbg("post_dense_comm")
hidden, residual = cm.post_dense_comm(hidden, residual, batch)
dbg(f"post_dense_comm done, shape={hidden.shape}")
# === Verify ===
assert (
hidden.shape == original.shape
), f"Rank {rank}: shape {hidden.shape} != {original.shape}"
torch.testing.assert_close(hidden, original, atol=0.01, rtol=0.01)
dist.destroy_process_group()
def _run(world_size, attn_tp, dense_tp, dp_tokens, hidden_size=256):
if world_size > torch.cuda.device_count():
pytest.skip(f"Need {world_size} GPUs, have {torch.cuda.device_count()}")
attn_dp = world_size // attn_tp
assert (
len(dp_tokens) == attn_dp
), f"dp_tokens length {len(dp_tokens)} != attn_dp {attn_dp}"
port = get_open_port()
error_dict = mp.Manager().dict()
mp.spawn(
worker_fn,
args=(world_size, port, attn_tp, dense_tp, dp_tokens, hidden_size, error_dict),
nprocs=world_size,
join=True,
)
if error_dict:
raise RuntimeError("\n".join(f"Rank {r}: {e}" for r, e in error_dict.items()))
# ---------------------------------------------------------------------------
# Test configs
# ---------------------------------------------------------------------------
PARALLELISM_CONFIGS = [
pytest.param(4, 2, 2, id="ws4_atp2_dtp2"),
pytest.param(8, 2, 2, id="ws8_atp2_dtp2"),
pytest.param(8, 2, 4, id="ws8_atp2_dtp4"),
pytest.param(8, 4, 2, id="ws8_atp4_dtp2"),
pytest.param(8, 4, 4, id="ws8_atp4_dtp4"),
]
# Token distributions keyed by attn_dp count.
# Each entry: (name, dp_tokens)
TOKEN_DISTS = {
2: [
pytest.param([100, 100], id="even"),
pytest.param([1, 131], id="uneven"),
pytest.param([0, 200], id="extreme_skew"),
pytest.param([0, 500], id="all_on_single_dp"),
],
4: [
pytest.param([100, 100, 100, 100], id="even"),
pytest.param([1, 100, 2, 50], id="uneven"),
pytest.param([0, 200, 0, 1], id="extreme_skew"),
pytest.param([0, 0, 0, 500], id="all_on_single_dp"),
],
}
def _make_test_params():
params = []
for pc in PARALLELISM_CONFIGS:
ws, atp, dtp = pc.values
attn_dp = ws // atp
for td in TOKEN_DISTS[attn_dp]:
dp_tokens = td.values[0]
test_id = f"{pc.id}-{td.id}"
params.append(pytest.param(ws, atp, dtp, dp_tokens, id=test_id))
return params
class TestCommManager:
@pytest.mark.parametrize(
"world_size,attn_tp,dense_tp,dp_tokens", _make_test_params()
)
def test_comm_cycle(self, world_size, attn_tp, dense_tp, dp_tokens):
_run(world_size, attn_tp, dense_tp, dp_tokens)