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
259 lines
8.0 KiB
Python
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)
|