161 lines
5.0 KiB
Python
161 lines
5.0 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
# Adapted from https://github.com/vllm-project/vllm/blob/2c58742dff8613a3bd7496f2008ce927e18d38d1/tests/kernels/mamba/test_mamba_mixer2.py
|
|
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from sglang.srt.distributed.device_communicators.custom_all_reduce_utils import (
|
|
update_environment_variables,
|
|
)
|
|
from sglang.srt.distributed.parallel_state import (
|
|
get_default_distributed_backend,
|
|
init_distributed_environment,
|
|
initialize_model_parallel,
|
|
)
|
|
from sglang.srt.runtime_context import get_parallel
|
|
from sglang.srt.utils import get_device, get_device_count
|
|
from sglang.test.ci.ci_register import register_cuda_ci, register_xpu_ci
|
|
from sglang.test.test_utils import publish_build_topology
|
|
|
|
register_cuda_ci(est_time=30, stage="base-b", runner_config="2-gpu-large")
|
|
register_xpu_ci(est_time=60, suite="nightly-xpu-2-gpu", nightly=True)
|
|
|
|
NUM_GPUS = 2
|
|
|
|
|
|
@pytest.mark.parametrize("batch_size", [8])
|
|
@pytest.mark.parametrize("seq_len", [128])
|
|
@pytest.mark.parametrize(
|
|
"hidden_size_n_groups",
|
|
[
|
|
(64, 1), # hidden_size be divisible by num_gpus
|
|
(100, 4), # and n_groups must divide hidden_size
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("dtype", [torch.float16])
|
|
def test_mixer2_gated_norm_multi_gpu(
|
|
batch_size: int,
|
|
seq_len: int,
|
|
hidden_size_n_groups: tuple[int, int],
|
|
dtype: torch.dtype,
|
|
device: str = get_device(),
|
|
):
|
|
if device not in ["cuda", "xpu"]:
|
|
pytest.skip("Test only supports CUDA and XPU devices")
|
|
|
|
assert get_device_count() >= NUM_GPUS, (
|
|
f"This test requires at least {NUM_GPUS} GPUs, but only {get_device_count()} available"
|
|
)
|
|
|
|
hidden_size, n_groups = hidden_size_n_groups
|
|
num_processes = NUM_GPUS
|
|
|
|
def run_torch_spawn(fn, nprocs):
|
|
# need to use torch.mp.spawn otherwise will have problems with
|
|
# torch.distributed and cuda
|
|
torch.multiprocessing.spawn(
|
|
fn,
|
|
args=(
|
|
num_processes,
|
|
batch_size,
|
|
seq_len,
|
|
hidden_size,
|
|
n_groups,
|
|
dtype,
|
|
device,
|
|
),
|
|
nprocs=nprocs,
|
|
)
|
|
|
|
run_torch_spawn(mixer2_gated_norm_tensor_parallel, NUM_GPUS)
|
|
|
|
|
|
def mixer2_gated_norm_tensor_parallel(
|
|
local_rank: int,
|
|
world_size: int,
|
|
batch_size: int,
|
|
seq_len: int,
|
|
hidden_size: int,
|
|
n_groups: int,
|
|
dtype: torch.dtype,
|
|
device: str,
|
|
):
|
|
torch.manual_seed(0)
|
|
|
|
device = torch.device(get_device(local_rank))
|
|
torch.get_device_module(device).set_device(device)
|
|
torch.set_default_device(device)
|
|
torch.set_default_dtype(dtype)
|
|
|
|
update_environment_variables(
|
|
{
|
|
"RANK": str(local_rank),
|
|
"LOCAL_RANK": str(local_rank),
|
|
"WORLD_SIZE": str(world_size),
|
|
"MASTER_ADDR": "localhost",
|
|
"MASTER_PORT": "12345",
|
|
}
|
|
)
|
|
|
|
# nccl on CUDA, xccl on XPU, ...; the parameter default is always "nccl".
|
|
init_distributed_environment(
|
|
world_size=world_size,
|
|
rank=local_rank,
|
|
local_rank=local_rank,
|
|
backend=get_default_distributed_backend(device.type),
|
|
)
|
|
publish_build_topology(tp_size=world_size, world_rank=local_rank)
|
|
initialize_model_parallel()
|
|
|
|
# create random weights an inputs
|
|
weight = torch.rand((hidden_size,), dtype=dtype, device=device)
|
|
hidden_states = torch.randn(batch_size, seq_len, hidden_size)
|
|
gate_states = torch.randn(batch_size, seq_len, hidden_size)
|
|
|
|
import sglang.srt.layers.attention.mamba.mixer2_rms_norm_gated as m2
|
|
|
|
# Force the TP topology through the context (the weight loader reads
|
|
# get_parallel().attn_tp_rank, Mixer2RMSNormGated reads tp_size / tp_rank);
|
|
# avoids calling initialize_dp_attention.
|
|
with get_parallel().override(
|
|
attn_tp_rank=local_rank, tp_size=world_size, tp_rank=local_rank
|
|
):
|
|
# create gated-norm with TP
|
|
mixer = m2.Mixer2RMSNormGated(
|
|
full_hidden_size=hidden_size,
|
|
full_n_groups=n_groups,
|
|
)
|
|
mixer.weight.weight_loader(mixer.weight, weight)
|
|
|
|
# m2 reads tp via get_parallel().tp_size/rank — force it through the context.
|
|
with get_parallel().override(tp_size=1, tp_rank=0):
|
|
# create gated-norm without TP to compute reference
|
|
mixer_single_gpu = m2.Mixer2RMSNormGated(
|
|
full_hidden_size=hidden_size,
|
|
full_n_groups=n_groups,
|
|
)
|
|
# assign weight to single-gpu mixer
|
|
mixer_single_gpu.weight.data = weight
|
|
|
|
# generate and compare
|
|
N = hidden_size // world_size
|
|
output = mixer(
|
|
hidden_states[..., local_rank * N : (local_rank + 1) * N],
|
|
gate_states[..., local_rank * N : (local_rank + 1) * N],
|
|
)
|
|
ref_output = mixer_single_gpu(hidden_states, gate_states)
|
|
torch.testing.assert_close(
|
|
output,
|
|
ref_output[..., local_rank * N : (local_rank + 1) * N],
|
|
atol=5e-3,
|
|
rtol=1e-3,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main([__file__]))
|