[Kernel] Fuse DeepEP-v2 decode globalize+align into one launch (#53911)

Signed-off-by: Elvir Crncevic <elvircrn@gmail.com>
This commit is contained in:
Elvir Crnčević
2026-10-13 11:10:38 -04:00
committed by GitHub
parent 669f85e686
commit b9b401b2ef
10 changed files with 686 additions and 16 deletions
+2 -1
View File
@@ -1287,7 +1287,8 @@ set(VLLM_MOE_EXT_SRC
if(VLLM_GPU_LANG STREQUAL "CUDA")
list(APPEND VLLM_MOE_EXT_SRC
"csrc/moe/moe_wna16.cu"
"csrc/moe/grouped_topk_kernels.cu")
"csrc/moe/grouped_topk_kernels.cu"
"csrc/moe/fused_globalize_align_block_size.cu")
endif()
if(VLLM_GPU_LANG STREQUAL "CUDA")
@@ -0,0 +1,266 @@
// Single-launch fusion of the DeepEP-v2 decode metadata chain
// (_globalize_recv_topk_idx + moe_align_block_size + count_and_sort) for the
// humming INDEXED / CUDA-graph decode path. recv values are LOCAL expert ids;
// the kernel bins by local id while storing back the global id, sized by local
// experts. Hybrid launch: one non-cooperative block for small decode, else a
// cooperative multi-SM grid (launched via cudaLaunchKernelEx so grid.sync() is
// CUDA-graph capturable). Barriers: zero+sentinel -> shared histogram
// (distributed reduce) -> block-0 prefix + expert_ids fill ->
// scatter+globalize.
#include <cooperative_groups.h>
#include <cuda_runtime.h>
#include <torch/csrc/stable/macros.h>
#include <torch/csrc/stable/accelerator.h>
#include <torch/csrc/stable/ops.h>
#include <torch/csrc/stable/tensor.h>
#include <torch/headeronly/core/ScalarType.h>
#include "torch_utils.h"
namespace cg = cooperative_groups;
namespace vllm {
namespace moe {
static constexpr int FGAS_THREADS = 256;
static constexpr long FGAS_SB_MAX_EFFECTIVE_WORK = 4096;
// COOP selects the phase barrier: grid.sync() (multi-block) vs __syncthreads().
// topk / block_size are runtime args: any (topk, block_size) is supported, and
// the kernel is barrier/memory-bound so the divisions are not on the hot path.
template <bool COOP>
__device__ __forceinline__ void fused_gas_body(
long* __restrict__ topk_idx, int* __restrict__ sorted_ids,
int* __restrict__ expert_ids, int* __restrict__ num_tokens_post_pad,
int* __restrict__ counts, int* __restrict__ scatter_idx, int num_recv,
int rank_expert_offset, int global_num_experts, int numel,
int max_num_tokens_padded, int max_num_m_blocks, int local_num_experts,
int topk, int block_size) {
extern __shared__ int sh[]; // [local_num_experts] per-block histogram
const int tid = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
auto BAR = [&]() {
if constexpr (COOP)
cg::this_grid().sync();
else
__syncthreads();
};
// Phase 0: zero the histogram + sentinel-fill sorted_ids.
for (int e = tid; e < local_num_experts; e += stride) counts[e] = 0;
for (int i = tid; i < max_num_tokens_padded; i += stride)
sorted_ids[i] = numel;
BAR();
// Phase 1: per-block shared histogram, one global atomic per (block, expert).
// Reduce stays distributed across blocks (centralizing it in block 0 is
// slower).
for (int e = threadIdx.x; e < local_num_experts; e += blockDim.x) sh[e] = 0;
__syncthreads();
for (int i = tid; i < numel; i += stride) {
int val = (int)topk_idx[i];
int row = i / topk;
bool valid = (val >= 0) && (val < local_num_experts) &&
(val + rank_expert_offset < global_num_experts) &&
(row < num_recv);
if (valid) atomicAdd(&sh[val], 1);
}
__syncthreads();
for (int e = threadIdx.x; e < local_num_experts; e += blockDim.x)
if (sh[e]) atomicAdd(&counts[e], sh[e]);
BAR();
// Phase 2 (block 0). Thread 0 runs the tiny serial prefix scan; then all of
// block 0's threads split the expert_ids fill grid-stride.
if (blockIdx.x == 0) {
int* sh_start = sh; // [local_num_experts] padded start offsets
int& sh_run = sh[local_num_experts];
if (threadIdx.x == 0) {
int run = 0;
for (int e = 0; e < local_num_experts; e++) {
int c = counts[e];
int nb = (c + block_size - 1) / block_size;
sh_start[e] = run;
counts[e] = run; // exclusive prefix; empty experts skip
run += nb * block_size;
}
sh_run = run;
*num_tokens_post_pad = run;
}
__syncthreads();
int run = sh_run;
// owner(j) = highest expert whose padded start <= j*block_size (skips empty
// experts); blocks past the last token get -1.
for (int j = threadIdx.x; j < max_num_m_blocks; j += blockDim.x) {
int off = j * block_size;
if (off >= run) {
expert_ids[j] = -1;
continue;
}
int owner = 0;
for (int e = 1; e < local_num_experts; e++)
if (sh_start[e] <= off) owner = e;
expert_ids[j] = owner;
}
}
BAR();
// Phase 3: scatter token indices at cursor[val]++ and globalize in place.
for (int i = tid; i < numel; i += stride) {
int val = (int)topk_idx[i];
int g = val + rank_expert_offset;
int row = i / topk;
bool valid = (val >= 0) && (val < local_num_experts) &&
(g < global_num_experts) && (row < num_recv);
if (scatter_idx != nullptr) scatter_idx[i] = valid ? i : -1;
if (valid) {
int pos = atomicAdd(&counts[val], 1);
sorted_ids[pos] = i;
}
topk_idx[i] = valid ? g : -1;
}
}
template <bool COOP>
__global__ void fused_gas_kernel(
long* __restrict__ topk_idx, const int* __restrict__ psum,
int* __restrict__ sorted_ids, int* __restrict__ expert_ids,
int* __restrict__ num_tokens_post_pad, int* __restrict__ counts,
int* __restrict__ scatter_idx, int P, int rank_expert_offset,
int global_num_experts, int numel, int max_num_tokens_padded,
int max_num_m_blocks, int local_num_experts, int topk, int block_size) {
// num_recv read on-device (psum[P-1]) -> baked into replay, cudagraph-safe.
fused_gas_body<COOP>(topk_idx, sorted_ids, expert_ids, num_tokens_post_pad,
counts, scatter_idx, psum[P - 1], rank_expert_offset,
global_num_experts, numel, max_num_tokens_padded,
max_num_m_blocks, local_num_experts, topk, block_size);
}
static int fgas_sm_count() {
// Device-constant: query the driver once, never on the capture/replay path.
static int sm_count = -1;
if (sm_count < 0) {
int dev = 0;
cudaGetDevice(&dev);
cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, dev);
if (sm_count < 1) sm_count = 1;
}
return sm_count;
}
static int fgas_cooperative_blocks(int bps, long work) {
int max_blocks = bps * fgas_sm_count();
if (max_blocks < 1) max_blocks = 1;
long need = (work + FGAS_THREADS - 1) / FGAS_THREADS; // no over-launch
if (need < 1) need = 1;
return need < (long)max_blocks ? (int)need : max_blocks;
}
static void fgas_launch(long* p_topk, const int* p_psum, int* p_sorted,
int* p_expert, int* p_num, int* p_counts,
int* p_scatter, int P, int reo, int gne, int numel,
int mntp, int mnmb, int local_e, int topk,
int block_size, cudaStream_t stream) {
long work = numel > mntp ? numel : mntp;
long effective_work = (long)mntp + 2L * numel + (long)mnmb * local_e;
// Histogram [local_e] + one scalar (phase-2 padded-run total).
size_t smem = (size_t)(local_e + 1) * sizeof(int);
// Small decode: one non-cooperative block, __syncthreads barriers. Account
// for both numel passes and the expert_ids owner scan in the crossover.
if (effective_work <= FGAS_SB_MAX_EFFECTIVE_WORK) {
fused_gas_kernel<false><<<1, FGAS_THREADS, smem, stream>>>(
p_topk, p_psum, p_sorted, p_expert, p_num, p_counts, p_scatter, P, reo,
gne, numel, mntp, mnmb, local_e, topk, block_size);
STD_CUDA_CHECK(cudaGetLastError());
return;
}
// Large decode: cooperative multi-SM. Occupancy (fixed per rank) queried
// once.
static int bps = -1;
if (bps < 0)
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&bps, (const void*)fused_gas_kernel<true>, FGAS_THREADS, smem);
int blocks = fgas_cooperative_blocks(bps, work);
// Cooperative launch via cudaLaunchKernelEx + the cooperative launch
// attribute: unlike the legacy cudaLaunchCooperativeKernel, this form IS
// stream-capturable, so grid.sync() works inside a captured CUDA graph.
cudaLaunchConfig_t config = {};
config.gridDim = dim3(blocks);
config.blockDim = dim3(FGAS_THREADS);
config.dynamicSmemBytes = smem;
config.stream = stream;
cudaLaunchAttribute attr = {};
attr.id = cudaLaunchAttributeCooperative;
attr.val.cooperative = 1;
config.attrs = &attr;
config.numAttrs = 1;
STD_CUDA_CHECK(cudaLaunchKernelEx(&config, fused_gas_kernel<true>, p_topk,
p_psum, p_sorted, p_expert, p_num, p_counts,
p_scatter, P, reo, gne, numel, mntp, mnmb,
local_e, topk, block_size));
}
} // namespace moe
} // namespace vllm
// recv_topk_idx (int64, [N, topk]) is globalized in place and the align/sort
// outputs (int32) are filled. counts is an internal write-cursor allocated
// here.
void fused_globalize_align_block_size(
torch::stable::Tensor topk_idx, torch::stable::Tensor psum,
int64_t rank_expert_offset, int64_t global_num_experts,
int64_t local_num_experts, int64_t block_size,
torch::stable::Tensor sorted_ids, torch::stable::Tensor expert_ids,
torch::stable::Tensor num_tokens_post_pad,
std::optional<torch::stable::Tensor> scatter_idx) {
STD_TORCH_CHECK(topk_idx.scalar_type() == torch::headeronly::ScalarType::Long,
"fused_globalize_align_block_size: topk_idx must be int64");
STD_TORCH_CHECK(
local_num_experts <= 1024,
"fused_globalize_align_block_size: local_num_experts <= 1024");
const torch::stable::accelerator::DeviceGuard device_guard(
topk_idx.get_device_index());
const cudaStream_t stream =
get_current_cuda_stream(topk_idx.get_device_index());
int numel = (int)topk_idx.numel();
int topk = (int)topk_idx.size(1);
int mntp = (int)sorted_ids.size(0);
int mnmb = (int)expert_ids.size(0);
int local_e = (int)local_num_experts;
int reo = (int)rank_expert_offset;
int gne = (int)global_num_experts;
int P = (int)psum.size(0);
// Internal per-local-expert histogram / write-cursor.
torch::stable::Tensor counts = torch::stable::new_empty(
topk_idx, {local_num_experts}, torch::headeronly::ScalarType::Int);
long* p_topk = reinterpret_cast<long*>(topk_idx.mutable_data_ptr());
const int* p_psum = reinterpret_cast<const int*>(psum.const_data_ptr());
int* p_sorted = reinterpret_cast<int*>(sorted_ids.mutable_data_ptr());
int* p_expert = reinterpret_cast<int*>(expert_ids.mutable_data_ptr());
int* p_num = reinterpret_cast<int*>(num_tokens_post_pad.mutable_data_ptr());
int* p_counts = reinterpret_cast<int*>(counts.mutable_data_ptr());
int* p_scatter = nullptr;
if (scatter_idx.has_value()) {
STD_TORCH_CHECK(
scatter_idx->scalar_type() == torch::headeronly::ScalarType::Int &&
scatter_idx->device() == topk_idx.device() &&
scatter_idx->is_contiguous() &&
scatter_idx->numel() == topk_idx.numel(),
"scatter_idx must be contiguous int32 on the same device and match "
"topk_idx.numel()");
p_scatter = reinterpret_cast<int*>(scatter_idx->mutable_data_ptr());
}
vllm::moe::fgas_launch(p_topk, p_psum, p_sorted, p_expert, p_num, p_counts,
p_scatter, P, reo, gne, numel, mntp, mnmb, local_e,
topk, (int)block_size, stream);
}
+10
View File
@@ -44,6 +44,16 @@ void moe_align_block_size(torch::stable::Tensor topk_ids, int64_t num_experts,
std::optional<torch::stable::Tensor> maybe_expert_map,
std::optional<torch::stable::Tensor> scatter_idx);
#ifdef USE_CUDA
void fused_globalize_align_block_size(
torch::stable::Tensor topk_idx, torch::stable::Tensor psum,
int64_t rank_expert_offset, int64_t global_num_experts,
int64_t local_num_experts, int64_t block_size,
torch::stable::Tensor sorted_ids, torch::stable::Tensor expert_ids,
torch::stable::Tensor num_tokens_post_pad,
std::optional<torch::stable::Tensor> scatter_idx);
#endif
void batched_moe_align_block_size(
int64_t max_tokens_per_batch, int64_t block_size,
const torch::stable::Tensor& expert_num_tokens,
+16
View File
@@ -42,6 +42,18 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) {
" Tensor? maybe_expert_map,"
" Tensor(a!)? scatter_idx=None) -> ()");
#ifdef USE_CUDA
// Fused globalize + align + sort for the DeepEP-v2 humming INDEXED decode
// path (one launch; num_recv read on device from psum[-1], cudagraph-safe).
m.def(
"fused_globalize_align_block_size(Tensor! topk_idx, Tensor psum,"
" int rank_expert_offset, int global_num_experts,"
" int local_num_experts, int block_size,"
" Tensor! sorted_ids, Tensor! expert_ids,"
" Tensor! num_tokens_post_pad,"
" Tensor(a!)? scatter_idx=None) -> ()");
#endif
// Aligning the number of tokens to be processed by each expert such
// that it is divisible by the block size, but for the batched case.
m.def(
@@ -143,6 +155,10 @@ STABLE_TORCH_LIBRARY_IMPL(_moe_C, CUDA, m) {
m.impl("topk_softplus_sqrt", TORCH_BOX(&topk_softplus_sqrt));
m.impl("moe_sum", TORCH_BOX(&moe_sum));
m.impl("moe_align_block_size", TORCH_BOX(&moe_align_block_size));
#ifdef USE_CUDA
m.impl("fused_globalize_align_block_size",
TORCH_BOX(&fused_globalize_align_block_size));
#endif
m.impl("batched_moe_align_block_size",
TORCH_BOX(&batched_moe_align_block_size));
m.impl("moe_lora_align_block_size", TORCH_BOX(&moe_lora_align_block_size));
@@ -0,0 +1,229 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Correctness of the fused globalize + align + sort decode metadata kernel.
The fused op replaces the DeepEP-v2 INDEXED cudagraph-decode chain
(_globalize_recv_topk_idx + moe_align_block_size + count_and_sort). It is
checked against that exact reference chain over a grid of shapes / EP ranks /
fill distributions.
"""
import pytest
import torch
from vllm.model_executor.layers.fused_moe.fused_globalize_align_block_size import (
fused_globalize_align_block_size,
)
from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
moe_align_block_size,
)
from vllm.platforms import current_platform
from vllm.utils.import_utils import has_deep_ep_v2
if current_platform.is_cuda() and has_deep_ep_v2():
from vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_v2 import (
_globalize_recv_topk_idx,
)
else:
pytest.skip("Requires CUDA and DeepEP v2", allow_module_level=True)
def _expert_map(global_num_experts, rank_expert_offset, local_num_experts, device):
expert_map = torch.full((global_num_experts,), -1, device=device, dtype=torch.int32)
rng = torch.arange(local_num_experts, device=device, dtype=torch.int32)
expert_map[rank_expert_offset : rank_expert_offset + local_num_experts] = rng
return expert_map
def _make_case(n, topk, ep_size, local_e, block_size, seed, frac_recv, rank, fill):
torch.manual_seed(seed)
device = "cuda"
global_num_experts = local_e * ep_size
rank_idx = {"first": 0, "mid": ep_size // 2, "last": ep_size - 1}[rank]
rank_expert_offset = rank_idx * local_e
if fill == "all_nonlocal":
local_idx = torch.full((n, topk), -1, device=device, dtype=torch.int64)
elif fill == "single":
local_idx = torch.zeros((n, topk), device=device, dtype=torch.int64)
elif fill == "all_local":
local_idx = torch.randint(
0, local_e, (n, topk), device=device, dtype=torch.int64
)
else:
local_idx = torch.randint(
-1, local_e, (n, topk), device=device, dtype=torch.int64
)
num_recv = max(0, min(n, int(round(n * frac_recv))))
if num_recv < n:
local_idx[num_recv:] = torch.randint(
-1, local_e, (n - num_recv, topk), device=device, dtype=torch.int64
)
psum = torch.zeros(4, device=device, dtype=torch.int32)
psum[-1] = num_recv
return local_idx, psum, rank_expert_offset, global_num_experts
def _group_by_expert(sorted_ids, expert_ids, block_size, valid_len, total):
groups: dict[int, list[int]] = {}
for b in range(valid_len // block_size):
eid = int(expert_ids[b].item())
blk = sorted_ids[b * block_size : (b + 1) * block_size]
groups.setdefault(eid, []).extend(blk[blk < total].tolist())
return groups
@pytest.mark.parametrize("n", [1, 7, 64, 257])
# topk=16 with local_num_experts=28 is the Kimi K3 decode config. topk=6 and
# block_size=16, 128 are outside the old template dispatch table: they guard
# against a regression to fixed (topk, block_size) specialization.
@pytest.mark.parametrize("topk", [1, 6, 8, 16])
@pytest.mark.parametrize("block_size", [16, 32, 64, 128])
@pytest.mark.parametrize("local_e", [28, 32])
@pytest.mark.parametrize("rank", ["first", "mid", "last"])
@pytest.mark.parametrize("fill", ["rand", "all_local", "all_nonlocal", "single"])
def test_fused_matches_reference(n, topk, block_size, local_e, rank, fill):
ep_size = 4
local_idx, psum, reo, gne = _make_case(
n,
topk,
ep_size,
local_e,
block_size,
seed=n * 131 + topk,
frac_recv=0.75,
rank=rank,
fill=fill,
)
ref_global = _globalize_recv_topk_idx(local_idx.clone(), psum, reo, gne)
ref_sorted, ref_expert_ids, ref_num = moe_align_block_size(
topk_ids=ref_global,
block_size=block_size,
num_experts=gne,
expert_map=_expert_map(gne, reo, local_e, local_idx.device),
ignore_invalid_experts=True,
)
fused_global, fused_sorted, fused_expert_ids, fused_num = (
fused_globalize_align_block_size(
local_idx.clone(), psum, reo, gne, local_e, block_size
)
)
total = n * topk
assert torch.equal(fused_global, ref_global)
assert torch.equal(fused_num, ref_num)
valid = int(fused_num.item())
nb = valid // block_size
assert torch.equal(fused_expert_ids[:nb], ref_expert_ids[:nb])
assert bool((fused_expert_ids[nb:] == -1).all())
assert bool((fused_sorted[valid:] == total).all())
ref_groups = _group_by_expert(
ref_sorted, ref_expert_ids, block_size, int(ref_num.item()), total
)
fused_groups = _group_by_expert(
fused_sorted, fused_expert_ids, block_size, valid, total
)
assert set(ref_groups) == set(fused_groups)
for eid in ref_groups:
assert sorted(ref_groups[eid]) == sorted(fused_groups[eid])
@pytest.mark.parametrize("n", [7, 257])
def test_fused_scatter_idx_matches_reference(n):
"""The optional scatter output masks invalid and padded routes."""
local_e = 28
local_idx, psum, reo, gne = _make_case(
n, 16, 4, local_e, 64, seed=n, frac_recv=0.75, rank="mid", fill="rand"
)
ref_global = _globalize_recv_topk_idx(local_idx.clone(), psum, reo, gne)
*_, ref_scatter = moe_align_block_size(
topk_ids=ref_global,
block_size=64,
num_experts=gne,
expert_map=_expert_map(gne, reo, local_e, local_idx.device),
ignore_invalid_experts=True,
return_scatter_idx=True,
)
fused_global, *_, fused_scatter = fused_globalize_align_block_size(
local_idx.clone(), psum, reo, gne, local_e, 64, return_scatter_idx=True
)
assert torch.equal(fused_global, ref_global)
assert torch.equal(fused_scatter, ref_scatter)
# These have effective_work 1504 (single block) and 6068 (cooperative).
@pytest.mark.parametrize("n", [1, 64])
def test_fused_cuda_graph_replay(n):
"""Both launch paths remain capturable and consume replay-time inputs."""
topk = 16
local_e = 28
block_size = 64
ep_size = 4
local_idx, psum, reo, gne = _make_case(
n,
topk,
ep_size,
local_e,
block_size,
seed=n,
frac_recv=0.75,
rank="mid",
fill="rand",
)
graph_idx = torch.empty_like(local_idx)
graph_psum = torch.empty_like(psum)
# Initialize lazy CUDA state before capture.
fused_globalize_align_block_size(
local_idx.clone(), psum, reo, gne, local_e, block_size
)
torch.accelerator.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
graph_idx.copy_(local_idx)
graph_psum.copy_(psum)
graph_outputs = fused_globalize_align_block_size(
graph_idx, graph_psum, reo, gne, local_e, block_size
)
for frac_recv in (0.75, 0.5):
psum[-1] = int(round(n * frac_recv))
ref_idx = _globalize_recv_topk_idx(local_idx.clone(), psum, reo, gne)
ref_sorted, ref_expert_ids, ref_num = moe_align_block_size(
topk_ids=ref_idx,
block_size=block_size,
num_experts=gne,
expert_map=_expert_map(gne, reo, local_e, local_idx.device),
ignore_invalid_experts=True,
)
graph.replay()
torch.accelerator.synchronize()
fused_idx, fused_sorted, fused_expert_ids, fused_num = graph_outputs
assert torch.equal(fused_idx, ref_idx)
assert torch.equal(fused_num, ref_num)
valid = int(fused_num.item())
num_blocks = valid // block_size
assert torch.equal(fused_expert_ids[:num_blocks], ref_expert_ids[:num_blocks])
fused_groups = _group_by_expert(
fused_sorted, fused_expert_ids, block_size, valid, n * topk
)
ref_groups = _group_by_expert(
ref_sorted,
ref_expert_ids,
block_size,
int(ref_num.item()),
n * topk,
)
assert set(fused_groups) == set(ref_groups)
for eid in ref_groups:
assert sorted(fused_groups[eid]) == sorted(ref_groups[eid])
+26
View File
@@ -2195,6 +2195,32 @@ def moe_align_block_size(
)
def fused_globalize_align_block_size(
topk_idx: torch.Tensor,
psum: torch.Tensor,
rank_expert_offset: int,
global_num_experts: int,
local_num_experts: int,
block_size: int,
sorted_ids: torch.Tensor,
expert_ids: torch.Tensor,
num_tokens_post_pad: torch.Tensor,
scatter_idx: torch.Tensor | None = None,
) -> None:
torch.ops._moe_C.fused_globalize_align_block_size(
topk_idx,
psum,
rank_expert_offset,
global_num_experts,
local_num_experts,
block_size,
sorted_ids,
expert_ids,
num_tokens_post_pad,
scatter_idx,
)
def batched_moe_align_block_size(
max_tokens_per_batch: int,
block_size: int,
@@ -22,6 +22,9 @@ from vllm.model_executor.layers.fused_moe.config import (
FusedMoEParallelConfig,
FusedMoEQuantConfig,
)
from vllm.model_executor.layers.fused_moe.fused_globalize_align_block_size import (
fused_globalize_align_block_size,
)
from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
moe_align_block_size,
)
@@ -710,6 +713,35 @@ class HummingIndexedExperts(HummingExpertsBase):
num_sms = num_compute_units(topk_ids.get_device())
use_scatter = topk_ids.numel() > 2 * num_sms
# When DeepEP-v2 deferred globalization, fuse globalize + align + sort
# into one launch (topk_ids globalized in place); else align only.
rank_expert_offset = (
expert_tokens_meta.rank_expert_offset
if expert_tokens_meta is not None
else None
)
psum = (
expert_tokens_meta.psum_recv_per_rank
if expert_tokens_meta is not None
else None
)
if (
current_platform.is_cuda()
and rank_expert_offset is not None
and psum is not None
):
alignment = fused_globalize_align_block_size(
recv_topk_idx=topk_ids,
psum_recv_per_rank=psum,
rank_expert_offset=rank_expert_offset,
global_num_experts=self.global_num_experts,
local_num_experts=self.num_experts,
block_size=moe_block_size,
return_scatter_idx=use_scatter,
)
_, sorted_ids, expert_ids, num_tokens_padded = alignment[:4]
scatter_idx = alignment[4] if use_scatter else None
else:
alignment = moe_align_block_size(
topk_ids=topk_ids,
block_size=moe_block_size,
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import torch
from vllm import _custom_ops as ops
from vllm.triton_utils import triton
def fused_globalize_align_block_size(
recv_topk_idx: torch.Tensor,
psum_recv_per_rank: torch.Tensor,
rank_expert_offset: int,
global_num_experts: int,
local_num_experts: int,
block_size: int,
return_scatter_idx: bool = False,
) -> tuple[torch.Tensor, ...]:
"""Fuse the DeepEP-v2 INDEXED decode metadata chain into one launch.
Replaces ``_globalize_recv_topk_idx`` + ``moe_align_block_size`` +
``count_and_sort_expert_tokens``. ``recv_topk_idx`` holds LOCAL expert ids
and is globalized in place; valid-token count is read on-device from
``psum_recv_per_rank[-1]`` (CUDA-graph safe).
Returns the globalized ``recv_topk_idx`` and the ``moe_align_block_size``
outputs ``(sorted_ids, expert_ids, num_tokens_post_pad)``. When requested,
the final output is a masked identity scatter index for valid routes.
"""
assert recv_topk_idx.dtype == torch.int64
_, topk = recv_topk_idx.shape
numel = recv_topk_idx.numel()
device = recv_topk_idx.device
max_num_tokens_padded = numel + local_num_experts * (block_size - 1)
if numel < local_num_experts:
max_num_tokens_padded = min(numel * block_size, max_num_tokens_padded)
max_num_m_blocks = triton.cdiv(max_num_tokens_padded, block_size)
sorted_ids = torch.empty((max_num_tokens_padded,), dtype=torch.int32, device=device)
expert_ids = torch.empty((max_num_m_blocks,), dtype=torch.int32, device=device)
num_tokens_post_pad = torch.empty((1,), dtype=torch.int32, device=device)
scatter_idx = (
torch.empty((numel, 1), dtype=torch.int32, device=device)
if return_scatter_idx
else None
)
ops.fused_globalize_align_block_size(
recv_topk_idx,
psum_recv_per_rank,
rank_expert_offset,
global_num_experts,
local_num_experts,
block_size,
sorted_ids,
expert_ids,
num_tokens_post_pad,
scatter_idx,
)
outputs = (recv_topk_idx, sorted_ids, expert_ids, num_tokens_post_pad)
return (*outputs, scatter_idx) if scatter_idx is not None else outputs
@@ -103,6 +103,9 @@ class ExpertTokensMetadata:
expert_num_tokens: torch.Tensor | None
expert_num_tokens_cpu: torch.Tensor | None
psum_recv_per_rank: torch.Tensor | None = None
# local->global expert-id offset, set when globalization is deferred to the
# experts' fused align kernel.
rank_expert_offset: int | None = None
@staticmethod
def make_from_list(
@@ -1048,6 +1051,18 @@ class FusedMoEKernelModularImpl:
):
self.prepare_finalize = prepare_finalize
self.fused_experts = fused_experts
# Experts that globalize recv_topk_idx themselves let a capable prepare
# stage skip its standalone launch.
from vllm.model_executor.layers.fused_moe.experts.fused_humming_moe import (
HummingIndexedExperts,
)
if (
current_platform.is_cuda()
and isinstance(fused_experts, HummingIndexedExperts)
and hasattr(prepare_finalize, "defer_globalize")
):
prepare_finalize.defer_globalize = True
self.shared_experts: SharedExperts | None = None
moe_parallel_config = fused_experts.moe_config.moe_parallel_config
self.moe_parallel_config = moe_parallel_config
@@ -127,6 +127,11 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
self.use_cudagraph = use_cudagraph
self.sp_size = sp_size
# Set by FusedMoEKernelModularImpl when the experts globalize
# recv_topk_idx themselves, letting the decode path skip the standalone
# _globalize_recv_topk_idx launch.
self.defer_globalize = False
# DBO microbatching: one handle slot per micro-batch.
self.handles: list[deep_ep.EPHandle | None] = [None, None]
@@ -275,6 +280,7 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
# during profile_run when CUDA graphs are enabled.
expert_tokens_meta = None
globalize_deferred = False
if recv_topk_idx is None:
# do_expand=True (prefill mode): build topk_ids from
# per-expert token counts.
@@ -307,6 +313,9 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
# be treated as real routed tokens by experts that build routing
# over *all* rows (e.g. triton MoE backend's make_routing_data),
# polluting the per-expert token lists and corrupting real tokens.
if self.defer_globalize and self.use_cudagraph:
globalize_deferred = True
else:
recv_topk_idx = _globalize_recv_topk_idx(
recv_topk_idx,
psum_recv_per_rank,
@@ -329,6 +338,9 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
expert_num_tokens_cpu=None,
)
expert_tokens_meta.psum_recv_per_rank = psum_recv_per_rank
if globalize_deferred:
# recv_topk_idx still holds LOCAL ids; experts globalize it.
expert_tokens_meta.rank_expert_offset = self.rank_expert_offset
if _quantize_before_dispatch(quant_config, defer_input_quant):
if quant_config.quant_dtype == "mxfp8" and expert_x_scale is not None: