Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae8c445789 | ||
|
|
45a0b6d72d | ||
|
|
0f94cbef14 | ||
|
|
ed29278a33 | ||
|
|
56a8767d94 | ||
|
|
5a766bf3de | ||
|
|
75149ae1b9 | ||
|
|
67d26f78d2 | ||
|
|
019a41f00a | ||
|
|
515e36da11 | ||
|
|
a2a4b00f83 |
@@ -19,6 +19,7 @@ from vllm.utils.flashinfer import (
|
||||
has_flashinfer_nvlink_one_sided,
|
||||
has_flashinfer_nvlink_two_sided,
|
||||
)
|
||||
from vllm.utils.import_utils import has_deep_ep_v2
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from ..utils import init_test_distributed_environment
|
||||
@@ -194,6 +195,10 @@ requires_ptrace = pytest.mark.skipif(
|
||||
not _has_sys_ptrace(),
|
||||
reason="SYS_PTRACE required (docker run --cap-add=SYS_PTRACE)",
|
||||
)
|
||||
requires_deep_ep_v2 = pytest.mark.skipif(
|
||||
not has_deep_ep_v2(),
|
||||
reason="DeepEP v2 (ElasticBuffer) not available or NCCL < 2.30.4",
|
||||
)
|
||||
|
||||
# NOTE: No module-level pytestmark here. The FlashInfer lifecycle tests have
|
||||
# their own @requires_two_sided / @requires_one_sided decorators, and
|
||||
@@ -772,3 +777,76 @@ def _one_sided_data_worker(rank, world_size):
|
||||
def test_one_sided_dispatch_combine(world_size):
|
||||
"""Test FlashInfer one-sided dispatch/combine with actual data flow."""
|
||||
_spawn_workers(_one_sided_data_worker, world_size, dp_size=world_size)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 6: DeepEP v2 (ElasticBuffer) manager lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Tests DeepEPV2All2AllManager which wraps DeepEP's ElasticBuffer API using
|
||||
# the NCCL GIN backend. Requires DeepEP >= 2.0 and NCCL >= 2.30.4.
|
||||
#
|
||||
# Uses EP group because the DeepEP v2 manager is constructed with an
|
||||
# EP-scoped communicator in production. With tp=world_size the EP group
|
||||
# spans all ranks.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _deepep_v2_lifecycle_worker(rank, world_size):
|
||||
from vllm.distributed.device_communicators.all2all import (
|
||||
DeepEPV2All2AllManager,
|
||||
)
|
||||
|
||||
cpu_group = get_ep_group().cpu_group
|
||||
manager = DeepEPV2All2AllManager(cpu_group)
|
||||
|
||||
assert manager.rank == rank
|
||||
assert manager.world_size == world_size
|
||||
assert manager._num_sms is None
|
||||
|
||||
hidden_size = 7168
|
||||
num_experts = world_size * 32
|
||||
num_topk = 8
|
||||
max_tokens = 256
|
||||
|
||||
handle_kwargs = dict(
|
||||
num_max_tokens_per_rank=max_tokens,
|
||||
hidden=hidden_size,
|
||||
num_topk=num_topk,
|
||||
num_experts=num_experts,
|
||||
use_fp8_dispatch=False,
|
||||
)
|
||||
|
||||
handle = manager.get_handle(handle_kwargs)
|
||||
assert handle is not None
|
||||
assert manager._num_sms is not None
|
||||
assert manager._num_sms > 0
|
||||
|
||||
torch.distributed.barrier()
|
||||
|
||||
# get_handle again with same args should return cached handle
|
||||
handle2 = manager.get_handle(dict(handle_kwargs))
|
||||
assert handle2 is handle
|
||||
|
||||
torch.distributed.barrier()
|
||||
|
||||
# Destroy clears the cache
|
||||
manager.destroy()
|
||||
assert len(manager.handle_cache._cache) == 0
|
||||
|
||||
torch.distributed.barrier()
|
||||
|
||||
# Re-create after destroy
|
||||
handle3 = manager.get_handle(dict(handle_kwargs))
|
||||
assert handle3 is not None
|
||||
|
||||
torch.distributed.barrier()
|
||||
manager.destroy()
|
||||
|
||||
|
||||
@requires_multi_gpu
|
||||
@requires_deep_ep_v2
|
||||
@pytest.mark.parametrize("world_size", [2])
|
||||
def test_deepep_v2_manager_lifecycle(world_size):
|
||||
"""Test DeepEP v2 ElasticBuffer manager init, caching, and destroy."""
|
||||
_spawn_workers(_deepep_v2_lifecycle_worker, world_size)
|
||||
|
||||
@@ -15,7 +15,7 @@ from torch.distributed import ProcessGroup
|
||||
from torch.multiprocessing import spawn # pyright: ignore[reportPrivateImportUsage]
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from vllm.utils.import_utils import has_deep_ep
|
||||
from vllm.utils.import_utils import has_deep_ep, has_deep_ep_v2
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
if has_deep_ep():
|
||||
@@ -26,6 +26,11 @@ if has_deep_ep():
|
||||
DeepEPLLPrepareAndFinalize,
|
||||
)
|
||||
|
||||
if has_deep_ep_v2():
|
||||
from vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_v2 import (
|
||||
DeepEPV2PrepareAndFinalize,
|
||||
)
|
||||
|
||||
## Parallel Processes Utils
|
||||
|
||||
P = ParamSpec("P")
|
||||
@@ -55,11 +60,10 @@ def _worker_parallel_launch(
|
||||
torch.accelerator.set_device_index(local_rank)
|
||||
device = torch.device("cuda", local_rank)
|
||||
torch.distributed.init_process_group(
|
||||
backend="cpu:gloo,cuda:nccl",
|
||||
backend="nccl",
|
||||
init_method=init_method,
|
||||
rank=rank,
|
||||
world_size=world_size,
|
||||
device_id=device,
|
||||
)
|
||||
barrier = torch.tensor([rank], device=device)
|
||||
torch.distributed.all_reduce(barrier)
|
||||
@@ -200,3 +204,40 @@ def make_deepep_a2a(
|
||||
|
||||
assert deepep_ll_args is not None
|
||||
return make_deepep_ll_a2a(pg, pgi, deepep_ll_args, q_dtype, block_shape)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DeepEPV2Args:
|
||||
num_local_experts: int
|
||||
num_experts: int
|
||||
num_topk: int
|
||||
hidden_size: int
|
||||
max_tokens_per_rank: int
|
||||
use_fp8_dispatch: bool
|
||||
|
||||
|
||||
def make_deepep_v2_a2a(
|
||||
pg: ProcessGroup,
|
||||
pgi: ProcessGroupInfo,
|
||||
dp_size: int,
|
||||
v2_args: DeepEPV2Args,
|
||||
):
|
||||
import deep_ep
|
||||
|
||||
buffer = deep_ep.ElasticBuffer(
|
||||
group=pg,
|
||||
num_max_tokens_per_rank=v2_args.max_tokens_per_rank,
|
||||
hidden=v2_args.hidden_size,
|
||||
num_topk=v2_args.num_topk,
|
||||
use_fp8_dispatch=v2_args.use_fp8_dispatch,
|
||||
explicitly_destroy=True,
|
||||
)
|
||||
return DeepEPV2PrepareAndFinalize(
|
||||
buffer=buffer,
|
||||
num_dispatchers=pgi.world_size,
|
||||
dp_size=dp_size,
|
||||
rank_expert_offset=pgi.rank * v2_args.num_local_experts,
|
||||
num_experts=v2_args.num_experts,
|
||||
num_topk=v2_args.num_topk,
|
||||
use_fp8_dispatch=v2_args.use_fp8_dispatch,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Test DeepEP v2 (ElasticBuffer) dispatch-combine logic.
|
||||
Compares against a pure-PyTorch reference MoE implementation.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
import torch.distributed
|
||||
from torch.distributed import ProcessGroup
|
||||
|
||||
from tests.kernels.moe.utils import make_dummy_moe_config
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.config import VllmConfig, set_current_vllm_config
|
||||
from vllm.model_executor.layers.activation import SiluAndMul
|
||||
from vllm.model_executor.layers.fused_moe import TritonExperts
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEQuantConfig,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.modular_kernel import FusedMoEKernel
|
||||
from vllm.utils.import_utils import has_deep_ep_v2
|
||||
from vllm.utils.torch_utils import set_random_seed
|
||||
from vllm.v1.worker.workspace import init_workspace_manager
|
||||
|
||||
from ...utils import multi_gpu_test
|
||||
from .parallel_utils import ProcessGroupInfo, parallel_launch
|
||||
|
||||
if has_deep_ep_v2():
|
||||
from .parallel_utils import DeepEPV2Args, make_deepep_v2_a2a
|
||||
|
||||
requires_deep_ep_v2 = pytest.mark.skipif(
|
||||
not has_deep_ep_v2(),
|
||||
reason="Requires DeepEP v2 (ElasticBuffer)",
|
||||
)
|
||||
|
||||
|
||||
def make_weights(
|
||||
e, n, k, dtype
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
if dtype in [torch.float16, torch.bfloat16]:
|
||||
w1 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) / 10
|
||||
w2 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 10
|
||||
return w1, w2, None, None
|
||||
|
||||
assert dtype == torch.float8_e4m3fn
|
||||
w1 = torch.empty((e, 2 * n, k), device="cuda", dtype=torch.float16)
|
||||
w2 = torch.empty((e, k, n), device="cuda", dtype=torch.float16)
|
||||
|
||||
n_b_scales = 2 * n
|
||||
k_b_scales = k
|
||||
w1_q = torch.empty_like(w1, dtype=dtype)
|
||||
w2_q = torch.empty_like(w2, dtype=dtype)
|
||||
w1_scale = torch.empty((e, n_b_scales, 1), device="cuda", dtype=torch.float32)
|
||||
w2_scale = torch.empty((e, k_b_scales, 1), device="cuda", dtype=torch.float32)
|
||||
for expert in range(e):
|
||||
w1_q[expert], w1_scale[expert] = ops.scaled_fp8_quant(
|
||||
w1[expert], use_per_token_if_dynamic=True
|
||||
)
|
||||
w2_q[expert], w2_scale[expert] = ops.scaled_fp8_quant(
|
||||
w2[expert], use_per_token_if_dynamic=True
|
||||
)
|
||||
return w1_q, w2_q, w1_scale, w2_scale
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TestConfig:
|
||||
dtype: torch.dtype
|
||||
topk: int
|
||||
m: int
|
||||
k: int
|
||||
n: int
|
||||
num_experts: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TestTensors:
|
||||
rank_tokens: torch.Tensor
|
||||
rank_token_scales: torch.Tensor | None
|
||||
topk: torch.Tensor
|
||||
topk_weights: torch.Tensor
|
||||
config: TestConfig
|
||||
|
||||
@staticmethod
|
||||
def make(config: TestConfig) -> "TestTensors":
|
||||
assert config.dtype in [torch.bfloat16, torch.float8_e4m3fn]
|
||||
token_dtype = (
|
||||
torch.bfloat16 if config.dtype == torch.float8_e4m3fn else config.dtype
|
||||
)
|
||||
rank_tokens = (
|
||||
torch.randn((config.m, config.k), device="cuda", dtype=token_dtype) / 10
|
||||
)
|
||||
|
||||
topk = torch.stack(
|
||||
[
|
||||
torch.randperm(config.num_experts, device="cuda")[: config.topk]
|
||||
for _ in range(config.m)
|
||||
]
|
||||
).to(dtype=torch.int64)
|
||||
topk_weights = torch.randn(topk.shape, dtype=torch.float32, device="cuda")
|
||||
return TestTensors(
|
||||
rank_tokens=rank_tokens,
|
||||
rank_token_scales=None,
|
||||
topk=topk,
|
||||
topk_weights=topk_weights,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
def make_modular_kernel(
|
||||
pg: ProcessGroup,
|
||||
pgi: ProcessGroupInfo,
|
||||
dp_size: int,
|
||||
hidden_size: int,
|
||||
num_experts: int,
|
||||
num_local_experts: int,
|
||||
topk: int,
|
||||
q_dtype: torch.dtype | None,
|
||||
use_fp8_dispatch: bool,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
) -> FusedMoEKernel:
|
||||
v2_args = DeepEPV2Args(
|
||||
num_local_experts=num_local_experts,
|
||||
num_experts=num_experts,
|
||||
num_topk=topk,
|
||||
hidden_size=hidden_size,
|
||||
max_tokens_per_rank=8192,
|
||||
use_fp8_dispatch=use_fp8_dispatch,
|
||||
)
|
||||
|
||||
a2a = make_deepep_v2_a2a(
|
||||
pg=pg,
|
||||
pgi=pgi,
|
||||
dp_size=dp_size,
|
||||
v2_args=v2_args,
|
||||
)
|
||||
|
||||
moe_config = make_dummy_moe_config()
|
||||
|
||||
fused_experts = TritonExperts(
|
||||
moe_config=moe_config,
|
||||
quant_config=quant_config,
|
||||
)
|
||||
|
||||
mk = FusedMoEKernel(
|
||||
prepare_finalize=a2a,
|
||||
fused_experts=fused_experts,
|
||||
inplace=False,
|
||||
)
|
||||
return mk
|
||||
|
||||
|
||||
def deepep_v2_moe_impl(
|
||||
pg: ProcessGroup,
|
||||
pgi: ProcessGroupInfo,
|
||||
dp_size: int,
|
||||
test_tensors: TestTensors,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w1_scale: torch.Tensor | None,
|
||||
w2_scale: torch.Tensor | None,
|
||||
num_experts: int,
|
||||
topk: int,
|
||||
use_fp8_dispatch: bool,
|
||||
per_act_token_quant: bool,
|
||||
) -> torch.Tensor:
|
||||
num_local_experts = w1.size(0)
|
||||
|
||||
def build_expert_map():
|
||||
expert_map = torch.full((num_experts,), fill_value=-1, dtype=torch.int32)
|
||||
s = pgi.rank * num_local_experts
|
||||
e = s + num_local_experts
|
||||
expert_map[s:e] = torch.tensor(list(range(num_local_experts)))
|
||||
device = torch.accelerator.current_device_index()
|
||||
return expert_map.to(device=device, dtype=torch.int32)
|
||||
|
||||
is_quantized = w1.dtype == torch.float8_e4m3fn
|
||||
q_dtype = torch.float8_e4m3fn if is_quantized else None
|
||||
|
||||
quant_config = FusedMoEQuantConfig.make(
|
||||
q_dtype,
|
||||
w1_scale=w1_scale,
|
||||
w2_scale=w2_scale,
|
||||
per_act_token_quant=per_act_token_quant,
|
||||
a1_scale=test_tensors.rank_token_scales,
|
||||
)
|
||||
|
||||
hidden_size = test_tensors.rank_tokens.size(1)
|
||||
|
||||
mk: FusedMoEKernel = make_modular_kernel(
|
||||
pg,
|
||||
pgi,
|
||||
dp_size,
|
||||
hidden_size,
|
||||
num_experts,
|
||||
num_local_experts,
|
||||
topk,
|
||||
q_dtype,
|
||||
use_fp8_dispatch,
|
||||
quant_config,
|
||||
)
|
||||
|
||||
out = mk.apply(
|
||||
hidden_states=test_tensors.rank_tokens,
|
||||
w1=w1,
|
||||
w2=w2,
|
||||
topk_weights=test_tensors.topk_weights,
|
||||
topk_ids=test_tensors.topk,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=build_expert_map(),
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def torch_moe_impl(
|
||||
test_tensors: TestTensors,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w1_scale: torch.Tensor | None,
|
||||
w2_scale: torch.Tensor | None,
|
||||
per_act_token_quant: bool,
|
||||
):
|
||||
a, topk_ids, topk_weights = (
|
||||
test_tensors.rank_tokens,
|
||||
test_tensors.topk,
|
||||
test_tensors.topk_weights,
|
||||
)
|
||||
|
||||
is_quantized = w1.dtype == torch.float8_e4m3fn
|
||||
a_dtype = a.dtype
|
||||
if is_quantized:
|
||||
w1 = w1.to(dtype=torch.float32) * w1_scale
|
||||
w2 = w2.to(dtype=torch.float32) * w2_scale
|
||||
a = a.to(dtype=torch.float32)
|
||||
|
||||
m, _ = a.shape
|
||||
topk = topk_ids.size(1)
|
||||
out = torch.zeros_like(a)
|
||||
|
||||
for i in range(m):
|
||||
a_i = a[i]
|
||||
o_i = out[i]
|
||||
for j in range(topk):
|
||||
e = topk_ids[i][j]
|
||||
e_w = topk_weights[i][j]
|
||||
w1_e = w1[e]
|
||||
w2_e = w2[e]
|
||||
o_i += (
|
||||
SiluAndMul()(a_i @ w1_e.transpose(0, 1)) @ w2_e.transpose(0, 1)
|
||||
) * e_w
|
||||
|
||||
if is_quantized:
|
||||
out = out.to(dtype=a_dtype)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _deep_ep_v2_moe(
|
||||
pgi: ProcessGroupInfo,
|
||||
dp_size: int,
|
||||
config: TestConfig,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w1_scale: torch.Tensor | None,
|
||||
w2_scale: torch.Tensor | None,
|
||||
use_fp8_dispatch: bool,
|
||||
per_act_token_quant: bool,
|
||||
):
|
||||
device = torch.device(f"cuda:{pgi.local_rank}")
|
||||
init_workspace_manager(device)
|
||||
|
||||
is_quantized = w1.dtype == torch.float8_e4m3fn
|
||||
device_idx = torch.accelerator.current_device_index()
|
||||
w1 = w1.to(device=device_idx)
|
||||
w2 = w2.to(device=device_idx)
|
||||
if is_quantized:
|
||||
assert w1_scale is not None and w2_scale is not None
|
||||
w1_scale = w1_scale.to(device=device_idx)
|
||||
w2_scale = w2_scale.to(device=device_idx)
|
||||
|
||||
pg = torch.distributed.new_group(list(range(pgi.world_size)))
|
||||
test_tensors = TestTensors.make(config)
|
||||
|
||||
with set_current_vllm_config(VllmConfig()):
|
||||
# Reference
|
||||
torch_combined = torch_moe_impl(
|
||||
test_tensors,
|
||||
w1,
|
||||
w2,
|
||||
w1_scale,
|
||||
w2_scale,
|
||||
per_act_token_quant,
|
||||
)
|
||||
|
||||
# Splice experts for this rank
|
||||
num_local_experts = config.num_experts // pgi.world_size
|
||||
e_start = num_local_experts * pgi.rank
|
||||
e_end = e_start + num_local_experts
|
||||
w1_ep = w1[e_start:e_end]
|
||||
w2_ep = w2[e_start:e_end]
|
||||
|
||||
w1_scale_ep, w2_scale_ep = None, None
|
||||
if is_quantized:
|
||||
w1_scale_ep = w1_scale[e_start:e_end] # type: ignore
|
||||
w2_scale_ep = w2_scale[e_start:e_end] # type: ignore
|
||||
|
||||
deepep_combined = deepep_v2_moe_impl(
|
||||
pg,
|
||||
pgi,
|
||||
dp_size,
|
||||
test_tensors,
|
||||
w1_ep,
|
||||
w2_ep,
|
||||
w1_scale_ep,
|
||||
w2_scale_ep,
|
||||
config.num_experts,
|
||||
config.topk,
|
||||
use_fp8_dispatch,
|
||||
per_act_token_quant,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(
|
||||
torch_combined,
|
||||
deepep_combined,
|
||||
atol=6e-2,
|
||||
rtol=6e-2,
|
||||
)
|
||||
|
||||
|
||||
MNKs = [
|
||||
(1, 256, 256),
|
||||
(2, 256, 512),
|
||||
(3, 1024, 2048),
|
||||
(32, 256, 1024),
|
||||
(45, 512, 2048),
|
||||
(64, 1024, 1024),
|
||||
(222, 1024, 2048),
|
||||
]
|
||||
|
||||
DTYPES = [torch.bfloat16, torch.float8_e4m3fn]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("m,n,k", MNKs)
|
||||
@pytest.mark.parametrize("num_experts", [32])
|
||||
@pytest.mark.parametrize("topk", [6])
|
||||
@pytest.mark.parametrize("world_dp_size", [(2, 1)])
|
||||
@multi_gpu_test(num_gpus=2)
|
||||
@requires_deep_ep_v2
|
||||
def test_deep_ep_v2_moe(
|
||||
dtype: torch.dtype,
|
||||
m: int,
|
||||
n: int,
|
||||
k: int,
|
||||
num_experts: int,
|
||||
topk: int,
|
||||
world_dp_size: tuple[int, int],
|
||||
workspace_init,
|
||||
):
|
||||
per_act_token_quant = False
|
||||
use_fp8_dispatch = False
|
||||
|
||||
set_random_seed(7)
|
||||
world_size, dp_size = world_dp_size
|
||||
config = TestConfig(dtype=dtype, topk=topk, m=m, k=k, n=n, num_experts=num_experts)
|
||||
|
||||
w1, w2, w1_scale, w2_scale = make_weights(num_experts, n, k, dtype)
|
||||
|
||||
parallel_launch(
|
||||
world_size,
|
||||
_deep_ep_v2_moe,
|
||||
dp_size,
|
||||
config,
|
||||
w1,
|
||||
w2,
|
||||
w1_scale,
|
||||
w2_scale,
|
||||
use_fp8_dispatch,
|
||||
per_act_token_quant,
|
||||
)
|
||||
|
||||
|
||||
def _deep_ep_v2_moe_cudagraph(
|
||||
pgi: ProcessGroupInfo,
|
||||
dp_size: int,
|
||||
config: TestConfig,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w1_scale: torch.Tensor | None,
|
||||
w2_scale: torch.Tensor | None,
|
||||
):
|
||||
"""Worker function: verify DeepEP v2 MoE works under cudagraph capture."""
|
||||
device = torch.device(f"cuda:{pgi.local_rank}")
|
||||
init_workspace_manager(device)
|
||||
|
||||
device_idx = torch.accelerator.current_device_index()
|
||||
w1 = w1.to(device=device_idx)
|
||||
w2 = w2.to(device=device_idx)
|
||||
|
||||
pg = torch.distributed.new_group(list(range(pgi.world_size)))
|
||||
test_tensors = TestTensors.make(config)
|
||||
|
||||
num_local_experts = config.num_experts // pgi.world_size
|
||||
e_start = num_local_experts * pgi.rank
|
||||
e_end = e_start + num_local_experts
|
||||
w1_ep = w1[e_start:e_end]
|
||||
w2_ep = w2[e_start:e_end]
|
||||
|
||||
with set_current_vllm_config(VllmConfig()):
|
||||
# Reference
|
||||
torch_combined = torch_moe_impl(
|
||||
test_tensors, w1, w2, None, None, False,
|
||||
)
|
||||
|
||||
# Build kernel
|
||||
num_local_experts = w1_ep.size(0)
|
||||
hidden_size = test_tensors.rank_tokens.size(1)
|
||||
|
||||
quant_config = FusedMoEQuantConfig.make(None)
|
||||
|
||||
mk_kernel = make_modular_kernel(
|
||||
pg, pgi, dp_size, hidden_size,
|
||||
config.num_experts, num_local_experts,
|
||||
config.topk, None, False, quant_config,
|
||||
)
|
||||
|
||||
def build_expert_map():
|
||||
expert_map = torch.full(
|
||||
(config.num_experts,), fill_value=-1, dtype=torch.int32,
|
||||
)
|
||||
s = pgi.rank * num_local_experts
|
||||
expert_map[s:s + num_local_experts] = torch.tensor(
|
||||
list(range(num_local_experts)),
|
||||
)
|
||||
return expert_map.to(device=device_idx, dtype=torch.int32)
|
||||
|
||||
expert_map = build_expert_map()
|
||||
|
||||
# Warmup (non-captured)
|
||||
out = mk_kernel.apply(
|
||||
hidden_states=test_tensors.rank_tokens,
|
||||
w1=w1_ep, w2=w2_ep,
|
||||
topk_weights=test_tensors.topk_weights,
|
||||
topk_ids=test_tensors.topk,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=config.num_experts,
|
||||
expert_map=expert_map,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(
|
||||
torch_combined, out, atol=6e-2, rtol=6e-2,
|
||||
)
|
||||
|
||||
# Cudagraph capture
|
||||
torch.cuda.synchronize()
|
||||
s = torch.cuda.Stream()
|
||||
s.wait_stream(torch.cuda.current_stream())
|
||||
|
||||
# Warmup on capture stream
|
||||
with torch.cuda.stream(s):
|
||||
for _ in range(3):
|
||||
mk_kernel.apply(
|
||||
hidden_states=test_tensors.rank_tokens,
|
||||
w1=w1_ep, w2=w2_ep,
|
||||
topk_weights=test_tensors.topk_weights,
|
||||
topk_ids=test_tensors.topk,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=config.num_experts,
|
||||
expert_map=expert_map,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
torch.cuda.current_stream().wait_stream(s)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# Capture
|
||||
g = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(g, stream=s):
|
||||
graph_out = mk_kernel.apply(
|
||||
hidden_states=test_tensors.rank_tokens,
|
||||
w1=w1_ep, w2=w2_ep,
|
||||
topk_weights=test_tensors.topk_weights,
|
||||
topk_ids=test_tensors.topk,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=config.num_experts,
|
||||
expert_map=expert_map,
|
||||
apply_router_weight_on_input=False,
|
||||
)
|
||||
|
||||
# Replay
|
||||
g.replay()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
torch.testing.assert_close(
|
||||
torch_combined, graph_out, atol=6e-2, rtol=6e-2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m,n,k", [(32, 256, 1024)])
|
||||
@pytest.mark.parametrize("num_experts", [32])
|
||||
@pytest.mark.parametrize("topk", [6])
|
||||
@pytest.mark.parametrize("world_dp_size", [(2, 1)])
|
||||
@multi_gpu_test(num_gpus=2)
|
||||
@requires_deep_ep_v2
|
||||
def test_deep_ep_v2_moe_cudagraph(
|
||||
m: int,
|
||||
n: int,
|
||||
k: int,
|
||||
num_experts: int,
|
||||
topk: int,
|
||||
world_dp_size: tuple[int, int],
|
||||
workspace_init,
|
||||
):
|
||||
set_random_seed(7)
|
||||
world_size, dp_size = world_dp_size
|
||||
config = TestConfig(
|
||||
dtype=torch.bfloat16, topk=topk, m=m, k=k, n=n,
|
||||
num_experts=num_experts,
|
||||
)
|
||||
|
||||
w1, w2, _, _ = make_weights(num_experts, n, k, torch.bfloat16)
|
||||
|
||||
parallel_launch(
|
||||
world_size,
|
||||
_deep_ep_v2_moe_cudagraph,
|
||||
dp_size,
|
||||
config,
|
||||
w1,
|
||||
w2,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -8,7 +8,7 @@ set -ex
|
||||
# --nvshmem-ver <ver> NVSHMEM version
|
||||
|
||||
CUDA_HOME=${CUDA_HOME:-/usr/local/cuda}
|
||||
DEEPEP_COMMIT_HASH=${DEEPEP_COMMIT_HASH:-"73b6ea4"}
|
||||
DEEPEP_COMMIT_HASH=${DEEPEP_COMMIT_HASH:-"b306af06af"}
|
||||
NVSHMEM_VER=${NVSHMEM_VER:-"3.3.24"} # Default supports both CUDA 12 and 13
|
||||
WORKSPACE=${WORKSPACE:-$(pwd)/ep_kernels_workspace}
|
||||
MODE=${MODE:-install}
|
||||
|
||||
@@ -42,6 +42,7 @@ All2AllBackend = Literal[
|
||||
"pplx",
|
||||
"deepep_high_throughput",
|
||||
"deepep_low_latency",
|
||||
"deepep_v2",
|
||||
"mori",
|
||||
"nixl_ep",
|
||||
"allgather_reducescatter",
|
||||
|
||||
+7
-5
@@ -1227,12 +1227,14 @@ class VllmConfig:
|
||||
assert a2a_backend in [
|
||||
"deepep_low_latency",
|
||||
"deepep_high_throughput",
|
||||
"deepep_v2",
|
||||
], (
|
||||
"Microbatching currently only supports the deepep_low_latency and "
|
||||
f"deepep_high_throughput all2all backend. {a2a_backend} is not "
|
||||
"supported. To fix use --all2all-backend=deepep_low_latency or "
|
||||
"--all2all-backend=deepep_high_throughput and install the DeepEP"
|
||||
" kernels."
|
||||
"Microbatching currently only supports the deepep_low_latency, "
|
||||
"deepep_high_throughput, and deepep_v2 all2all backends. "
|
||||
f"{a2a_backend} is not supported. To fix use "
|
||||
"--all2all-backend=deepep_low_latency, "
|
||||
"--all2all-backend=deepep_high_throughput, or "
|
||||
"--all2all-backend=deepep_v2 and install the DeepEP kernels."
|
||||
)
|
||||
|
||||
if not self.model_config.disable_cascade_attn:
|
||||
|
||||
@@ -15,7 +15,7 @@ from vllm.utils.flashinfer import (
|
||||
has_flashinfer_nvlink_one_sided,
|
||||
has_flashinfer_nvlink_two_sided,
|
||||
)
|
||||
from vllm.utils.import_utils import has_deep_ep, has_mori
|
||||
from vllm.utils.import_utils import has_deep_ep, has_deep_ep_v2, has_mori
|
||||
|
||||
from .base_device_communicator import All2AllManagerBase, Cache
|
||||
|
||||
@@ -760,3 +760,63 @@ class MoriAll2AllManager(All2AllManagerBase):
|
||||
mori_kwargs, self._make_handle
|
||||
)
|
||||
return handle
|
||||
|
||||
|
||||
class DeepEPV2All2AllManager(All2AllManagerBase):
|
||||
"""
|
||||
All2All communication based on DeepEP v2 ElasticBuffer (unified API).
|
||||
Uses NCCL Gin backend with analytical SM calculation.
|
||||
"""
|
||||
|
||||
def __init__(self, cpu_group, tcp_store_group=None):
|
||||
assert has_deep_ep_v2(), (
|
||||
"DeepEP v2 (ElasticBuffer) not available. Requires DeepEP >= 2.0 "
|
||||
"(https://github.com/deepseek-ai/DeepEP) and NCCL >= 2.30.4."
|
||||
)
|
||||
super().__init__(cpu_group, tcp_store_group)
|
||||
self.handle_cache = Cache()
|
||||
self._num_sms: int | None = None
|
||||
|
||||
def _make_all2all_kwargs(
|
||||
self,
|
||||
num_max_tokens_per_rank: int,
|
||||
hidden: int,
|
||||
num_topk: int,
|
||||
use_fp8_dispatch: bool,
|
||||
) -> dict:
|
||||
return dict(
|
||||
group=self.cpu_group,
|
||||
num_max_tokens_per_rank=num_max_tokens_per_rank,
|
||||
hidden=hidden,
|
||||
num_topk=num_topk,
|
||||
use_fp8_dispatch=use_fp8_dispatch,
|
||||
allow_hybrid_mode=envs.VLLM_DEEPEP_V2_ALLOW_HYBRID_MODE,
|
||||
prefer_overlap_with_compute=envs.VLLM_DEEPEP_V2_PREFER_OVERLAP,
|
||||
allow_multiple_reduction=(envs.VLLM_DEEPEP_V2_ALLOW_MULTIPLE_REDUCTION),
|
||||
explicitly_destroy=True,
|
||||
)
|
||||
|
||||
def get_handle(self, kwargs):
|
||||
import deep_ep # type: ignore[import-not-found]
|
||||
|
||||
num_experts = kwargs.pop("num_experts", 256)
|
||||
buffer_kwargs = self._make_all2all_kwargs(**kwargs)
|
||||
logger.debug("DeepEP v2 all2all args %s", buffer_kwargs)
|
||||
handle: deep_ep.ElasticBuffer = self.handle_cache.get_or_create(
|
||||
buffer_kwargs, deep_ep.ElasticBuffer
|
||||
)
|
||||
if self._num_sms is None:
|
||||
self._num_sms = handle.get_theoretical_num_sms(
|
||||
num_experts=num_experts,
|
||||
num_topk=kwargs["num_topk"],
|
||||
)
|
||||
return handle
|
||||
|
||||
def max_sms_used(self) -> int | None:
|
||||
return self._num_sms
|
||||
|
||||
def destroy(self):
|
||||
with self.handle_cache._lock:
|
||||
for _, handle in self.handle_cache._cache.items():
|
||||
handle.destroy()
|
||||
self.handle_cache._cache.clear()
|
||||
|
||||
@@ -137,6 +137,12 @@ class CudaCommunicator(DeviceCommunicatorBase):
|
||||
from .all2all import MoriAll2AllManager
|
||||
|
||||
self.all2all_manager = MoriAll2AllManager(self.cpu_group)
|
||||
elif self.all2all_backend == "deepep_v2":
|
||||
from .all2all import DeepEPV2All2AllManager
|
||||
|
||||
self.all2all_manager = DeepEPV2All2AllManager(
|
||||
self.cpu_group, tcp_store_group
|
||||
)
|
||||
elif self.all2all_backend == "nixl_ep":
|
||||
from .all2all import NixlEPAll2AllManager
|
||||
|
||||
|
||||
@@ -233,6 +233,9 @@ if TYPE_CHECKING:
|
||||
VLLM_DEEPEP_BUFFER_SIZE_MB: int = 1024
|
||||
VLLM_DEEPEP_HIGH_THROUGHPUT_FORCE_INTRA_NODE: bool = False
|
||||
VLLM_DEEPEP_LOW_LATENCY_USE_MNNVL: bool = False
|
||||
VLLM_DEEPEP_V2_ALLOW_HYBRID_MODE: bool = True
|
||||
VLLM_DEEPEP_V2_PREFER_OVERLAP: bool = False
|
||||
VLLM_DEEPEP_V2_ALLOW_MULTIPLE_REDUCTION: bool = False
|
||||
VLLM_DBO_COMM_SMS: int = 20
|
||||
VLLM_PATTERN_MATCH_DEBUG: str | None = None
|
||||
VLLM_DEBUG_DUMP_PATH: str | None = None
|
||||
@@ -1623,6 +1626,18 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
"VLLM_DEEPEP_LOW_LATENCY_USE_MNNVL": lambda: bool(
|
||||
int(os.getenv("VLLM_DEEPEP_LOW_LATENCY_USE_MNNVL", "0"))
|
||||
),
|
||||
# DeepEP v2 ElasticBuffer: enable two-tier NVLink+RDMA hybrid mode
|
||||
"VLLM_DEEPEP_V2_ALLOW_HYBRID_MODE": lambda: bool(
|
||||
int(os.getenv("VLLM_DEEPEP_V2_ALLOW_HYBRID_MODE", "1"))
|
||||
),
|
||||
# DeepEP v2 ElasticBuffer: use fewer SMs at slight throughput cost
|
||||
"VLLM_DEEPEP_V2_PREFER_OVERLAP": lambda: bool(
|
||||
int(os.getenv("VLLM_DEEPEP_V2_PREFER_OVERLAP", "0"))
|
||||
),
|
||||
# DeepEP v2 ElasticBuffer: trade precision for transfer size in combine
|
||||
"VLLM_DEEPEP_V2_ALLOW_MULTIPLE_REDUCTION": lambda: bool(
|
||||
int(os.getenv("VLLM_DEEPEP_V2_ALLOW_MULTIPLE_REDUCTION", "0"))
|
||||
),
|
||||
# The number of SMs to allocate for communication kernels when running DBO
|
||||
# the rest of the SMs on the device will be allocated to compute
|
||||
"VLLM_DBO_COMM_SMS": lambda: int(os.getenv("VLLM_DBO_COMM_SMS", "20")),
|
||||
|
||||
@@ -29,7 +29,12 @@ from vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_two
|
||||
FlashInferNVLinkTwoSidedPrepareAndFinalize,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.import_utils import has_deep_ep, has_mori, has_nixl_ep
|
||||
from vllm.utils.import_utils import (
|
||||
has_deep_ep,
|
||||
has_deep_ep_v2,
|
||||
has_mori,
|
||||
has_nixl_ep,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -40,6 +45,8 @@ if current_platform.is_cuda_alike():
|
||||
DEEPEP_QUANT_BLOCK_SHAPE,
|
||||
DeepEPLLPrepareAndFinalize,
|
||||
)
|
||||
if has_deep_ep_v2():
|
||||
from .prepare_finalize.deepep_v2 import DeepEPV2PrepareAndFinalize
|
||||
if has_mori():
|
||||
from .prepare_finalize.mori import MoriPrepareAndFinalize
|
||||
if has_nixl_ep():
|
||||
@@ -78,6 +85,11 @@ def maybe_roundup_layer_hidden_size(
|
||||
hidden_size
|
||||
)
|
||||
|
||||
if moe_parallel_config.use_deepep_v2_kernels:
|
||||
hidden_size = DeepEPV2PrepareAndFinalize.maybe_roundup_layer_hidden_size(
|
||||
hidden_size, act_dtype
|
||||
)
|
||||
|
||||
if moe_parallel_config.use_nixl_ep_kernels:
|
||||
hidden_size = NixlEPPrepareAndFinalize.maybe_roundup_layer_hidden_size(
|
||||
hidden_size
|
||||
@@ -181,6 +193,36 @@ def maybe_make_prepare_finalize(
|
||||
physical_to_global=physical_to_global,
|
||||
local_expert_global_ids=local_expert_global_ids,
|
||||
)
|
||||
elif moe.use_deepep_v2_kernels:
|
||||
assert moe.dp_size == all2all_manager.dp_world_size
|
||||
|
||||
use_fp8_dispatch = (
|
||||
quant_config is not None
|
||||
and quant_config.quant_dtype == current_platform.fp8_dtype()
|
||||
and quant_config.is_block_quantized
|
||||
)
|
||||
all_to_all_args = dict(
|
||||
num_max_tokens_per_rank=moe.max_num_tokens,
|
||||
hidden=moe.hidden_dim,
|
||||
num_topk=moe.experts_per_token,
|
||||
num_experts=moe.num_experts,
|
||||
use_fp8_dispatch=use_fp8_dispatch,
|
||||
)
|
||||
handle = all2all_manager.get_handle(all_to_all_args)
|
||||
vllm_config = get_current_vllm_config()
|
||||
use_cudagraph = not vllm_config.model_config.enforce_eager
|
||||
|
||||
prepare_finalize = DeepEPV2PrepareAndFinalize(
|
||||
buffer=handle,
|
||||
num_dispatchers=all2all_manager.world_size,
|
||||
dp_size=all2all_manager.dp_world_size,
|
||||
rank_expert_offset=all2all_manager.rank * moe.num_local_experts,
|
||||
num_experts=moe.num_experts,
|
||||
num_topk=moe.experts_per_token,
|
||||
use_fp8_dispatch=use_fp8_dispatch,
|
||||
use_cudagraph=use_cudagraph,
|
||||
)
|
||||
|
||||
elif moe.use_mori_kernels:
|
||||
assert quant_config is not None
|
||||
|
||||
|
||||
@@ -1063,6 +1063,10 @@ class FusedMoEParallelConfig:
|
||||
def use_nixl_ep_kernels(self):
|
||||
return self.use_all2all_kernels and self.all2all_backend == "nixl_ep"
|
||||
|
||||
@property
|
||||
def use_deepep_v2_kernels(self):
|
||||
return self.use_all2all_kernels and self.all2all_backend == "deepep_v2"
|
||||
|
||||
@staticmethod
|
||||
def flatten_tp_across_dp_and_pcp(
|
||||
tp_size: int, dp_size: int, dp_rank: int, pcp_size: int, pcp_rank: int
|
||||
@@ -1350,6 +1354,10 @@ class FusedMoEConfig:
|
||||
def use_nixl_ep_kernels(self):
|
||||
return self.moe_parallel_config.use_nixl_ep_kernels
|
||||
|
||||
@property
|
||||
def use_deepep_v2_kernels(self):
|
||||
return self.moe_parallel_config.use_deepep_v2_kernels
|
||||
|
||||
@property
|
||||
def needs_round_robin_routing_tables(self):
|
||||
return self.moe_parallel_config.needs_round_robin_routing_tables
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from collections.abc import Callable
|
||||
|
||||
import deep_ep
|
||||
import torch
|
||||
|
||||
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
|
||||
from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
|
||||
from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
|
||||
TopKWeightAndReduceContiguous,
|
||||
TopKWeightAndReduceDelegate,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.utils import moe_kernel_quantize_input
|
||||
from vllm.utils.math_utils import round_up
|
||||
from vllm.v1.worker.ubatching import (
|
||||
dbo_current_ubatch_id,
|
||||
)
|
||||
|
||||
|
||||
class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
|
||||
"""
|
||||
Prepare/Finalize using DeepEP v2 ElasticBuffer (unified API).
|
||||
|
||||
Supports two modes controlled by the `use_cudagraph` constructor arg:
|
||||
|
||||
**Decode mode (use_cudagraph=True):**
|
||||
- do_expand=False, do_cpu_sync=False
|
||||
- Tokens returned in original order with recv_topk_idx (global IDs)
|
||||
- Worst-case tensor allocation; padding rows zeroed via
|
||||
handle.psum_num_recv_tokens_per_scaleup_rank
|
||||
- Fully cudagraph-capturable
|
||||
- Expert kernel sorts internally (expert_tokens_meta=None)
|
||||
|
||||
**Prefill mode (use_cudagraph=False):**
|
||||
- do_expand=True, do_cpu_sync=True
|
||||
- Per-expert-contiguous layout; exact memory allocation
|
||||
- Saves GPU memory (no worst-case allocation)
|
||||
- Not cudagraph-capturable (CPU polling), but prefill doesn't
|
||||
use cudagraphs anyway
|
||||
- Provides expert_tokens_meta for efficient batched expert kernels
|
||||
|
||||
Both modes use async_with_compute_stream=False (synchronous from
|
||||
caller's perspective). The ElasticBuffer handles comm internally.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def maybe_roundup_layer_hidden_size(hidden_size: int, dtype: torch.dtype) -> int:
|
||||
hidden_size_bytes = hidden_size * dtype.itemsize
|
||||
xfer_atom_size = 512 # 32 * 16 (size(int4))
|
||||
if hidden_size_bytes % xfer_atom_size == 0:
|
||||
return hidden_size
|
||||
|
||||
hidden_size_bytes = round_up(hidden_size_bytes, xfer_atom_size)
|
||||
return hidden_size_bytes // dtype.itemsize
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
buffer: deep_ep.ElasticBuffer,
|
||||
num_dispatchers: int,
|
||||
dp_size: int,
|
||||
rank_expert_offset: int,
|
||||
num_experts: int,
|
||||
num_topk: int,
|
||||
use_fp8_dispatch: bool = False,
|
||||
use_cudagraph: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.buffer = buffer
|
||||
self.num_dispatchers_ = num_dispatchers
|
||||
self.dp_size = dp_size
|
||||
self.rank_expert_offset = rank_expert_offset
|
||||
self.num_experts = num_experts
|
||||
self.num_topk = num_topk
|
||||
self.use_fp8_dispatch = use_fp8_dispatch
|
||||
self.use_cudagraph = use_cudagraph
|
||||
|
||||
# DBO microbatching: one handle slot per micro-batch.
|
||||
self.handles: list[deep_ep.EPHandle | None] = [None, None]
|
||||
|
||||
def num_dispatchers(self) -> int:
|
||||
return self.num_dispatchers_
|
||||
|
||||
def output_is_reduced(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def activation_format(self) -> mk.FusedMoEActivationFormat:
|
||||
return mk.FusedMoEActivationFormat.Standard
|
||||
|
||||
def max_num_tokens_per_rank(self) -> int | None:
|
||||
return None
|
||||
|
||||
def topk_indices_dtype(self) -> torch.dtype | None:
|
||||
return torch.int64
|
||||
|
||||
def _do_dispatch(
|
||||
self,
|
||||
tokens: torch.Tensor,
|
||||
token_scales: torch.Tensor | None,
|
||||
rank_topk_ids: torch.Tensor,
|
||||
rank_topk_weights: torch.Tensor,
|
||||
num_experts: int,
|
||||
a1_scale: torch.Tensor | None,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
defer_input_quant: bool,
|
||||
) -> Callable:
|
||||
has_scales = token_scales is not None
|
||||
|
||||
token_data = tokens
|
||||
if has_scales:
|
||||
token_data = (tokens, token_scales)
|
||||
|
||||
# Decode: do_expand=False + do_cpu_sync=False (cudagraph-safe)
|
||||
# Prefill: do_expand=True + do_cpu_sync=True (memory-efficient)
|
||||
do_expand = not self.use_cudagraph
|
||||
do_cpu_sync = not self.use_cudagraph
|
||||
|
||||
(
|
||||
recv_x,
|
||||
recv_topk_idx,
|
||||
recv_topk_weights,
|
||||
handle,
|
||||
event,
|
||||
) = self.buffer.dispatch(
|
||||
x=token_data,
|
||||
topk_idx=rank_topk_ids,
|
||||
topk_weights=rank_topk_weights,
|
||||
num_experts=num_experts,
|
||||
do_expand=do_expand,
|
||||
do_cpu_sync=do_cpu_sync,
|
||||
async_with_compute_stream=False,
|
||||
)
|
||||
|
||||
a2a_idx = dbo_current_ubatch_id()
|
||||
self.handles[a2a_idx] = handle
|
||||
|
||||
return lambda: self._receiver(
|
||||
event,
|
||||
has_scales,
|
||||
recv_x,
|
||||
recv_topk_idx,
|
||||
num_experts,
|
||||
handle.num_recv_tokens_per_expert_list,
|
||||
recv_topk_weights,
|
||||
handle.psum_num_recv_tokens_per_scaleup_rank,
|
||||
a1_scale,
|
||||
quant_config,
|
||||
defer_input_quant=defer_input_quant,
|
||||
)
|
||||
|
||||
def _receiver(
|
||||
self,
|
||||
event: deep_ep.EventOverlap,
|
||||
has_scales: bool,
|
||||
recv_x: tuple[torch.Tensor, torch.Tensor] | torch.Tensor,
|
||||
recv_topk_idx: torch.Tensor | None,
|
||||
num_experts: int,
|
||||
recv_expert_num_tokens: list[int],
|
||||
recv_topk_weights: torch.Tensor | None,
|
||||
psum_recv_per_rank: torch.Tensor,
|
||||
a1_scale: torch.Tensor | None,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
defer_input_quant: bool,
|
||||
) -> mk.PrepareResultType:
|
||||
if event.event is not None:
|
||||
event.current_stream_wait()
|
||||
|
||||
if isinstance(recv_x, tuple):
|
||||
expert_x, expert_x_scale = recv_x
|
||||
else:
|
||||
expert_x, expert_x_scale = recv_x, None
|
||||
|
||||
if recv_topk_idx is None:
|
||||
# do_expand=True (prefill mode): build topk_ids from
|
||||
# per-expert token counts.
|
||||
parts = [
|
||||
torch.full(
|
||||
(count,),
|
||||
i + self.rank_expert_offset,
|
||||
dtype=torch.int64,
|
||||
device=expert_x.device,
|
||||
)
|
||||
for i, count in enumerate(recv_expert_num_tokens)
|
||||
if count > 0
|
||||
]
|
||||
recv_topk_idx = (
|
||||
torch.cat(parts)
|
||||
if parts
|
||||
else torch.empty(
|
||||
0, dtype=torch.int64, device=expert_x.device,
|
||||
)
|
||||
)
|
||||
recv_topk_idx = recv_topk_idx.unsqueeze(1)
|
||||
else:
|
||||
# do_expand=False (decode/cudagraph mode): recv_topk_idx has
|
||||
# LOCAL expert IDs (-1 for non-local). Sanitize padding rows
|
||||
# from worst-case allocation, then convert to global IDs.
|
||||
total_rows = expert_x.shape[0]
|
||||
num_real = psum_recv_per_rank[-1] # GPU scalar tensor
|
||||
row_indices = torch.arange(
|
||||
total_rows, device=expert_x.device, dtype=num_real.dtype,
|
||||
)
|
||||
is_padding = (row_indices >= num_real).unsqueeze(1)
|
||||
|
||||
expert_x = torch.where(
|
||||
is_padding, torch.zeros_like(expert_x), expert_x)
|
||||
if expert_x_scale is not None:
|
||||
expert_x_scale = torch.where(
|
||||
is_padding, torch.ones_like(expert_x_scale),
|
||||
expert_x_scale)
|
||||
if recv_topk_weights is not None:
|
||||
recv_topk_weights = torch.where(
|
||||
is_padding, torch.zeros_like(recv_topk_weights),
|
||||
recv_topk_weights)
|
||||
|
||||
# dispatch(do_expand=False) returns LOCAL expert IDs (-1 for
|
||||
# non-local, -1 for padding after the where above).
|
||||
# Convert valid local IDs to global. Keep -1 as -1 so
|
||||
# DeepGemm's is_computation_valid skips those rows.
|
||||
is_invalid = (recv_topk_idx < 0) | is_padding
|
||||
recv_topk_idx = torch.where(
|
||||
is_invalid,
|
||||
-1,
|
||||
recv_topk_idx + self.rank_expert_offset,
|
||||
)
|
||||
if recv_topk_weights is not None:
|
||||
recv_topk_weights = torch.where(
|
||||
is_invalid,
|
||||
torch.zeros_like(recv_topk_weights),
|
||||
recv_topk_weights,
|
||||
)
|
||||
|
||||
# Reshape recv_topk_weights to match recv_topk_idx shape [N, 1]
|
||||
if recv_topk_weights is not None and recv_topk_weights.ndim == 1:
|
||||
recv_topk_weights = recv_topk_weights.unsqueeze(1)
|
||||
|
||||
if recv_topk_idx is not None and not self.use_cudagraph:
|
||||
# do_expand=True: we have exact per-expert counts
|
||||
expert_tokens_meta = mk.ExpertTokensMetadata.make_from_list(
|
||||
recv_expert_num_tokens, device=expert_x.device,
|
||||
)
|
||||
else:
|
||||
expert_tokens_meta = None
|
||||
|
||||
if not quant_config.is_block_quantized and not defer_input_quant:
|
||||
expert_x_scale = None
|
||||
if expert_x.numel() != 0:
|
||||
expert_x, expert_x_scale = moe_kernel_quantize_input(
|
||||
expert_x,
|
||||
a1_scale,
|
||||
quant_dtype=quant_config.quant_dtype,
|
||||
per_act_token_quant=False,
|
||||
block_shape=quant_config.block_shape,
|
||||
is_fp4_scale_swizzled=quant_config.is_nvfp4_scale_swizzled,
|
||||
)
|
||||
|
||||
return (
|
||||
expert_x,
|
||||
expert_x_scale,
|
||||
expert_tokens_meta,
|
||||
recv_topk_idx,
|
||||
recv_topk_weights,
|
||||
)
|
||||
|
||||
def supports_async(self) -> bool:
|
||||
return True
|
||||
|
||||
def prepare_async(
|
||||
self,
|
||||
a1: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
num_experts: int,
|
||||
expert_map: torch.Tensor | None,
|
||||
apply_router_weight_on_input: bool,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
defer_input_quant: bool = False,
|
||||
) -> mk.ReceiverType:
|
||||
if apply_router_weight_on_input:
|
||||
topk = topk_ids.size(1)
|
||||
assert topk == 1, (
|
||||
"apply_router_weight_on_input is only implemented for topk=1"
|
||||
)
|
||||
a1 = a1 * topk_weights.to(a1.dtype)
|
||||
|
||||
if quant_config.is_block_quantized and not defer_input_quant:
|
||||
a1q, a1q_scale = moe_kernel_quantize_input(
|
||||
a1,
|
||||
quant_config.a1_scale,
|
||||
quant_dtype=quant_config.quant_dtype,
|
||||
per_act_token_quant=quant_config.per_act_token_quant,
|
||||
block_shape=quant_config.block_shape,
|
||||
)
|
||||
if a1q_scale is not None and a1q_scale.numel() == 1:
|
||||
a1q_scale = a1q_scale.view(1, 1)
|
||||
a1_post_scale = None
|
||||
else:
|
||||
a1q = a1
|
||||
a1q_scale = None
|
||||
a1_post_scale = (
|
||||
quant_config.a1_gscale
|
||||
if quant_config.quant_dtype == "nvfp4"
|
||||
else quant_config.a1_scale
|
||||
)
|
||||
|
||||
return self._do_dispatch(
|
||||
tokens=a1q,
|
||||
token_scales=a1q_scale,
|
||||
rank_topk_ids=topk_ids,
|
||||
rank_topk_weights=topk_weights,
|
||||
num_experts=num_experts,
|
||||
a1_scale=a1_post_scale,
|
||||
quant_config=quant_config,
|
||||
defer_input_quant=defer_input_quant,
|
||||
)
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
a1: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
num_experts: int,
|
||||
expert_map: torch.Tensor | None,
|
||||
apply_router_weight_on_input: bool,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
defer_input_quant: bool = False,
|
||||
) -> mk.PrepareResultType:
|
||||
receiver = self.prepare_async(
|
||||
a1,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
num_experts,
|
||||
expert_map,
|
||||
apply_router_weight_on_input,
|
||||
quant_config,
|
||||
defer_input_quant,
|
||||
)
|
||||
return receiver()
|
||||
|
||||
def _finalize(
|
||||
self,
|
||||
output: torch.Tensor,
|
||||
fused_expert_output: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
apply_router_weight_on_input: bool,
|
||||
weight_and_reduce_impl: mk.TopKWeightAndReduce,
|
||||
do_async: bool,
|
||||
) -> Callable | None:
|
||||
a2a_idx = dbo_current_ubatch_id()
|
||||
handle = self.handles[a2a_idx]
|
||||
assert handle is not None
|
||||
|
||||
if fused_expert_output.numel() != 0:
|
||||
if isinstance(weight_and_reduce_impl, TopKWeightAndReduceDelegate):
|
||||
weight_and_reduce_impl = TopKWeightAndReduceContiguous()
|
||||
fused_expert_output = weight_and_reduce_impl.apply(
|
||||
output=None,
|
||||
fused_expert_output=fused_expert_output,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
apply_router_weight_on_input=apply_router_weight_on_input,
|
||||
)
|
||||
|
||||
if fused_expert_output.dtype != torch.bfloat16:
|
||||
raise ValueError(
|
||||
f"DeepEP v2 combine requires bfloat16 input, "
|
||||
f"got {fused_expert_output.dtype}"
|
||||
)
|
||||
|
||||
combined_x, _, event = self.buffer.combine(
|
||||
x=fused_expert_output,
|
||||
handle=handle,
|
||||
topk_weights=None,
|
||||
async_with_compute_stream=False,
|
||||
)
|
||||
|
||||
output.copy_(combined_x, non_blocking=True)
|
||||
return None
|
||||
|
||||
def finalize_async(
|
||||
self,
|
||||
output: torch.Tensor,
|
||||
fused_expert_output: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
apply_router_weight_on_input: bool,
|
||||
weight_and_reduce_impl: mk.TopKWeightAndReduce,
|
||||
) -> Callable:
|
||||
self._finalize(
|
||||
output,
|
||||
fused_expert_output,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
apply_router_weight_on_input,
|
||||
weight_and_reduce_impl,
|
||||
False,
|
||||
)
|
||||
return lambda: None
|
||||
|
||||
def finalize(
|
||||
self,
|
||||
output: torch.Tensor,
|
||||
fused_expert_output: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
apply_router_weight_on_input: bool,
|
||||
weight_and_reduce_impl: mk.TopKWeightAndReduce,
|
||||
) -> None:
|
||||
self._finalize(
|
||||
output,
|
||||
fused_expert_output,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
apply_router_weight_on_input,
|
||||
weight_and_reduce_impl,
|
||||
False,
|
||||
)
|
||||
@@ -405,6 +405,61 @@ def has_deep_ep() -> bool:
|
||||
return _has_module("deep_ep")
|
||||
|
||||
|
||||
DEEPEP_V2_MIN_NCCL_VERSION_RAW = 23004 # 2.30.4
|
||||
|
||||
|
||||
def _get_runtime_nccl_version() -> int | None:
|
||||
"""Get the runtime NCCL version by loading the actual library.
|
||||
|
||||
Returns the raw version int (e.g. 23004 for 2.30.4), or None on failure.
|
||||
torch.cuda.nccl.version() is a compile-time constant from the PyTorch
|
||||
wheel and does not reflect a separately installed NCCL.
|
||||
"""
|
||||
import ctypes
|
||||
|
||||
try:
|
||||
from vllm.utils.nccl import find_nccl_library
|
||||
|
||||
lib = ctypes.CDLL(find_nccl_library())
|
||||
version = ctypes.c_int()
|
||||
lib.ncclGetVersion(ctypes.byref(version))
|
||||
return version.value
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _format_nccl_raw_version(raw: int) -> str:
|
||||
s = str(raw)
|
||||
return f"{s[0]}.{s[1:3].lstrip('0') or '0'}.{s[3:].lstrip('0') or '0'}"
|
||||
|
||||
|
||||
def has_deep_ep_v2() -> bool:
|
||||
"""Whether deep_ep with ElasticBuffer (v2 API) is available.
|
||||
|
||||
Requires both the ElasticBuffer class in the deep_ep module and
|
||||
NCCL >= 2.30.4 (GIN backend), checked against the runtime library.
|
||||
"""
|
||||
if not _has_module("deep_ep"):
|
||||
return False
|
||||
import deep_ep # type: ignore[import-not-found]
|
||||
|
||||
if not hasattr(deep_ep, "ElasticBuffer"):
|
||||
return False
|
||||
try:
|
||||
nccl_ver = _get_runtime_nccl_version()
|
||||
if nccl_ver is None or nccl_ver < DEEPEP_V2_MIN_NCCL_VERSION_RAW:
|
||||
logger.info_once(
|
||||
"DeepEP v2 requires NCCL >= %s but found %s. "
|
||||
"deepep_v2 backend will not be available.",
|
||||
_format_nccl_raw_version(DEEPEP_V2_MIN_NCCL_VERSION_RAW),
|
||||
_format_nccl_raw_version(nccl_ver) if nccl_ver else "unknown",
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def has_deep_gemm() -> bool:
|
||||
"""Whether the optional `deep_gemm` package is available.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user