Compare commits

...
Author SHA1 Message Date
Woosuk Kwon fb9f5790bb megamoe
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 23:59:53 +00:00
Woosuk Kwon 57b8526cfb Generalize
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 19:19:36 +00:00
Woosuk Kwon 25411f3138 minor
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 04:57:51 +00:00
Woosuk Kwon 261a8820c7 Bound DeepEPV2 num_max_tokens_per_rank
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 04:52:12 +00:00
Woosuk Kwon 416977534a fuse expert id gather
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 04:51:16 +00:00
Woosuk Kwon ca54c027c4 minor
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 01:51:39 +00:00
Woosuk Kwon 65e05df079 minor
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 01:34:59 +00:00
Woosuk Kwon 2a09d50034 Merge branch 'main' into woosuk/triton-fix 2026-06-22 00:29:09 +00:00
Woosuk Kwon 6f3d89d105 masked moe sum
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-22 00:26:33 +00:00
Woosuk Kwon a06a16ff0a minor
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-19 03:31:41 +00:00
Woosuk Kwon a1d80989d9 Plumb is_padding
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-19 03:23:23 +00:00
Woosuk Kwon 40a19bed77 Merge branch 'main' into woosuk/triton-fix 2026-06-18 16:06:08 +00:00
Woosuk Kwon 8e8f8c2e2c remove batched
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-18 01:16:13 +00:00
Woosuk Kwon 116d3e3149 Merge branch 'main' into woosuk/triton-fix 2026-06-18 01:14:50 +00:00
Woosuk Kwon f6fa9700e6 Merge branch 'main' into woosuk/triton-fix 2026-06-17 23:38:48 +00:00
Woosuk Kwon 0221ab433e wip
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
2026-06-17 22:57:43 +00:00
10 changed files with 555 additions and 18 deletions
+80 -1
View File
@@ -46,7 +46,8 @@ def test_deepseek_v4_mega_moe_ue8m0_uint8_to_float():
def test_deepseek_v4_mega_moe_weight_loader_uses_ep_expert_ownership():
vllm_config = SimpleNamespace(
scheduler_config=SimpleNamespace(max_num_batched_tokens=4)
scheduler_config=SimpleNamespace(max_num_batched_tokens=4),
compilation_config=SimpleNamespace(static_forward_context={}),
)
experts = DeepseekV4MegaMoEExperts(
vllm_config,
@@ -182,3 +183,81 @@ def test_deepseek_v4_mega_moe_fused_input_staging_is_bitwise_exact():
fused_topk_weights.view(torch.uint8),
ref_topk_weights.view(torch.uint8),
)
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="DeepSeek V4 MegaMoE fused input staging requires CUDA.",
)
def test_deepseek_v4_mega_moe_fused_input_staging_masks_padding():
from vllm.third_party.deep_gemm.utils import per_token_cast_to_fp8
device = torch.device("cuda")
num_tokens = 7
hidden_size = 256
top_k = 8
generator = torch.Generator(device=device)
generator.manual_seed(1)
hidden_states = torch.randn(
num_tokens,
hidden_size,
device=device,
dtype=torch.bfloat16,
generator=generator,
)
topk_ids = torch.randint(
0,
256,
(num_tokens, top_k),
device=device,
dtype=torch.int32,
generator=generator,
)
topk_weights = torch.randn(
num_tokens,
top_k,
device=device,
dtype=torch.float32,
generator=generator,
)
is_padding = torch.tensor(
[False, True, False, False, True, False, True],
device=device,
)
ref_x, ref_x_sf = per_token_cast_to_fp8(
hidden_states,
use_ue8m0=True,
gran_k=32,
use_packed_ue8m0=True,
)
ref_topk_idx = topk_ids.to(torch.int64)
ref_topk_idx[is_padding] = -1
ref_topk_weights = topk_weights.clone()
ref_topk_weights[is_padding] = 0.0
fused_x = torch.empty_like(ref_x)
fused_x_sf = torch.empty_like(ref_x_sf)
fused_topk_idx = torch.empty_like(ref_topk_idx)
fused_topk_weights = torch.empty_like(ref_topk_weights)
prepare_megamoe_inputs(
hidden_states,
topk_weights,
topk_ids,
fused_x,
fused_x_sf,
fused_topk_idx,
fused_topk_weights,
is_padding=is_padding,
)
torch.accelerator.synchronize()
assert torch.equal(fused_x.view(torch.uint8), ref_x.view(torch.uint8))
assert torch.equal(fused_x_sf, ref_x_sf)
assert torch.equal(fused_topk_idx, ref_topk_idx)
assert torch.equal(
fused_topk_weights.view(torch.uint8),
ref_topk_weights.view(torch.uint8),
)
+10
View File
@@ -147,6 +147,12 @@ class ForwardContext:
ubatch_slices: UBatchSlices | None = None
# Boolean mask over the (cudagraph-padded) token axis: True for trailing
# padding rows that are not real tokens. Consumers (e.g. DeepEP V2 dispatch)
# use it to skip a2a communication / MoE compute for padded tokens. None
# when the producer (currently the V2 model runner) does not set it.
is_padding: torch.Tensor | None = None
# If True, bypass the compiled model call, e.g. by using .forward() directly
skip_compiled: bool = False
@@ -211,6 +217,7 @@ def create_forward_context(
slot_mapping: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None = None,
additional_kwargs: dict[str, Any] | None = None,
skip_compiled: bool = False,
is_padding: torch.Tensor | None = None,
):
if vllm_config.compilation_config.fast_moe_cold_start:
all_moe_layers = vllm_config.compilation_config.static_all_moe_layers
@@ -228,6 +235,7 @@ def create_forward_context(
ubatch_slices=ubatch_slices,
skip_compiled=skip_compiled,
additional_kwargs=additional_kwargs or {},
is_padding=is_padding,
)
@@ -257,6 +265,7 @@ def set_forward_context(
ubatch_slices: UBatchSlices | None = None,
slot_mapping: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]] | None = None,
skip_compiled: bool = False,
is_padding: torch.Tensor | None = None,
):
"""A context manager that stores the current forward context,
can be attention metadata, etc.
@@ -316,6 +325,7 @@ def set_forward_context(
slot_mapping,
additional_kwargs,
skip_compiled,
is_padding=is_padding,
)
try:
@@ -4,7 +4,6 @@
import torch
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
from vllm import _custom_ops as ops
from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
from vllm.model_executor.layers.fused_moe.config import (
@@ -220,6 +219,241 @@ def _patch_make_bitmatrix_metadata() -> None:
_bm.make_bitmatrix_metadata = _make_bitmatrix_metadata_pow2_safe
def _patch_legacy_routing_for_nonpow2_topk() -> None:
"""Monkey-patch the legacy (v3.5.1) triton_kernels routing path to support
non-power-of-2 top_k (e.g. DeepSeek-V4 top_k=6).
The bundled ``_routing_compute_indx`` does ``tl.arange(0, N_EXPTS_ACT *
BLOCK_M)``, which fails to compile when ``N_EXPTS_ACT`` (top_k) is not a
power of 2 (6 * 32 = 192). This installs a pow2-safe variant that pads the
``tl.arange`` to the next power of 2, strides by the real per-block size,
and masks the padded tail so it neither loads the next block's gates nor
writes any output. For power-of-2 top_k it is identical to the original.
A matching ``sort_tokens`` is installed that threads the padded size into
the patched kernel. Only needed on the legacy path; the v3.6+ SparseMatrix
path is handled by ``_patch_make_bitmatrix_metadata``.
"""
import triton
import triton.language as tl
# Import via the `triton_kernels` alias (set up by has_triton_kernels) so
# we patch the SAME module object that `make_routing_data` consumes. The
# `vllm.third_party.triton_kernels.routing` path is a *different* module
# object under the import alias, so patching it would have no effect.
try:
import triton_kernels.routing as _routing
from triton_kernels.routing_details import _routing_compute as _rc
except ImportError:
return
_keyed_add = _rc._keyed_add
_expt_data_compute = _rc._expt_data_compute
@triton.jit
def _routing_compute_indx_pow2(
pid_m,
GatherIndx,
ScatterIndx,
GateScal,
ExptScal,
ExptIndx,
PartialOffs,
stride_pm,
stride_pn,
TokensStart,
n_tokens,
BLOCK_M: tl.constexpr,
N_EXPTS_ACT: tl.constexpr,
BLOCK_SIZE_PADDED: tl.constexpr,
):
if isinstance(n_tokens, tl.tensor) and n_tokens.dtype.is_ptr():
n_tokens = tl.load(n_tokens)
n_gates = n_tokens * N_EXPTS_ACT
BLOCK_SIZE: tl.constexpr = N_EXPTS_ACT * BLOCK_M
tl.static_assert(BLOCK_SIZE_PADDED <= 32768)
local_offs = tl.arange(0, BLOCK_SIZE_PADDED)
offs = pid_m * BLOCK_SIZE + local_offs
expert = tl.load(
ExptIndx + offs,
mask=(local_offs < BLOCK_SIZE) & (offs < n_gates),
other=-1,
).to(tl.uint32)
kv_pairs = ((expert << 16) | local_offs).to(tl.uint32)
kv_pairs = tl.sort(kv_pairs, 0)
expert = kv_pairs >> 16
offs = pid_m * BLOCK_SIZE + (kv_pairs & 0xFFFF)
mask = expert != 0xFFFF
gate_scal = tl.load(ExptScal + offs, mask=mask)
x = kv_pairs & 0xFFFF0000 | 0x00000001
run_lengths = tl.associative_scan(x, 0, _keyed_add)
exclusive_run_lengths = (run_lengths - 1) & 0xFFFF
gates = tl.load(PartialOffs + pid_m * stride_pm + expert * stride_pn, mask=mask)
gates += tl.load(TokensStart + expert, mask=mask)
gates += exclusive_run_lengths
tl.store(ScatterIndx + offs, gates, mask=mask)
tl.store(GatherIndx + gates, offs, mask=mask)
tl.store(GateScal + gates, gate_scal, mask=mask)
@triton.jit
def _combined_routing_compute_pow2(
GatherIndx,
ScatterIndx,
GateScal,
ExptScal,
ExptIndx,
PartialOffs,
stride_pm,
stride_pn,
TokensStart,
n_tokens,
BLOCK_M: tl.constexpr,
N_EXPTS_ACT: tl.constexpr,
Hist,
MDTileStarts,
tile_starts_stridem,
MDTileInfo,
tile_info_stridem,
first_tile_dim_log2,
SIZES: tl.constexpr,
BLOCK: tl.constexpr,
blocks2a,
BLOCK_SIZE_PADDED: tl.constexpr,
):
pid = tl.program_id(0)
if pid < blocks2a:
_expt_data_compute(
Hist,
MDTileStarts,
tile_starts_stridem,
MDTileInfo,
tile_info_stridem,
first_tile_dim_log2,
SIZES,
BLOCK,
)
else:
pid -= blocks2a
_routing_compute_indx_pow2(
pid,
GatherIndx,
ScatterIndx,
GateScal,
ExptScal,
ExptIndx,
PartialOffs,
stride_pm,
stride_pn,
TokensStart,
n_tokens,
BLOCK_M,
N_EXPTS_ACT,
BLOCK_SIZE_PADDED,
)
def _sort_tokens_pow2(expt_scal, expt_indx, n_expts_tot, bitmatrix):
import torch
HIST_BLOCK_M = 32
INDX_OFFS_BLOCK_M = 512
MEMSET_BLOCK = 1024
cdiv = triton.cdiv
device = expt_scal.device
dtype = expt_scal.dtype
n_tokens_raw, _ = bitmatrix.shape
n_tokens_pad, n_expts_act = expt_scal.shape
n_gates_pad = n_tokens_pad * n_expts_act
# pad per-block gate count (HIST_BLOCK_M * top_k) up to a pow2.
block_size_padded = triton.next_power_of_2(HIST_BLOCK_M * n_expts_act)
hist, partial_hist = bitmatrix.sum(partials_block_size=HIST_BLOCK_M)
hist = hist[:n_expts_tot]
expt_offs = torch.empty(n_expts_tot, dtype=torch.int32, device=device)
combined_indx = torch.empty(n_gates_pad * 2, dtype=torch.int32, device=device)
topk_indx = combined_indx[:n_gates_pad]
gate_indx = combined_indx[n_gates_pad:]
gate_scal = torch.empty(n_gates_pad, dtype=dtype, device=device)
(
token_offs_combined,
token_offs_raw,
token_offs_pad,
block_pid_map,
blocks1a,
blocks2a,
MEMSET_BLOCK_A,
HIST2_BLOCK_M,
block_m_log2_start,
block_m_num,
) = _routing._compute_expt_data_internal(hist, n_expts_tot, n_gates_pad)
blocks1b = cdiv(n_gates_pad * 2, MEMSET_BLOCK) + n_expts_tot + 1
blocks2b = cdiv(n_tokens_pad, HIST_BLOCK_M)
_rc._combined_routing_memset[(blocks1a + blocks1b,)](
combined_indx,
n_gates_pad * 2,
-1,
MEMSET_BLOCK,
hist,
expt_offs,
hist.shape[0],
n_expts_tot,
partial_hist,
partial_hist.shape[0],
partial_hist.stride(0),
partial_hist.stride(1),
token_offs_combined,
token_offs_combined.stride(0),
blocks1a,
block_pid_map,
block_m_log2_start,
SIZES=block_m_num,
BLOCK_A=MEMSET_BLOCK_A,
BLOCK_N=512,
BLOCK_M=INDX_OFFS_BLOCK_M,
)
indx_offs = partial_hist
_combined_routing_compute_pow2[(blocks2a + blocks2b,)](
topk_indx,
gate_indx,
gate_scal,
expt_scal,
expt_indx,
indx_offs,
indx_offs.stride(0),
indx_offs.stride(1),
expt_offs,
n_tokens_raw,
HIST_BLOCK_M,
n_expts_act,
hist,
token_offs_pad,
token_offs_pad.stride(0),
block_pid_map,
block_pid_map.stride(0),
block_m_log2_start,
block_m_num,
HIST2_BLOCK_M,
blocks2a,
block_size_padded,
)
return (
hist,
topk_indx,
gate_indx,
gate_scal,
token_offs_raw,
token_offs_pad,
block_pid_map,
)
# `routing_from_bitmatrix` looks up `sort_tokens` via the routing module
# global, so replacing it here redirects the legacy path to the pow2 kernel.
_routing.sort_tokens = _sort_tokens_pow2
# Two API generations of triton_kernels are supported:
# - v3.5.1 (the version bundled with vLLM): exposes `routing()` and
# `routing_from_bitmatrix()` in triton_kernels.routing; the `Bitmatrix`
@@ -260,6 +494,9 @@ if has_triton_kernels():
use_legacy_triton_kernels = True
if not use_legacy_triton_kernels:
_patch_make_bitmatrix_metadata()
else:
# Legacy routing fails to compile for non-pow2 top_k (DeepSeek-V4).
_patch_legacy_routing_for_nonpow2_topk()
except (AttributeError, ImportError) as e:
logger.error(
"Failed to import Triton kernels. Please make sure your triton "
@@ -577,6 +814,80 @@ def make_routing_data(
return routing_data, gather_indx, scatter_indx
@triton.jit
def _masked_topk_sum_kernel(
inp_ptr, # (M, topk, K) contiguous
topk_ids_ptr, # (M, topk) int: -1 marks an invalid / non-local slot
out_ptr, # (M, K), same dtype as inp
K,
topk: tl.constexpr,
BLOCK_K: tl.constexpr,
):
pid_m = tl.program_id(0).to(tl.int64)
k = tl.program_id(1) * BLOCK_K + tl.arange(0, BLOCK_K)
k_mask = k < K
base = pid_m * topk
acc = tl.zeros((BLOCK_K,), dtype=tl.float32)
for j in tl.static_range(topk):
eid = tl.load(topk_ids_ptr + base + j)
# NOTE: This is NaN-safe because the invalid slots are skipped.
if eid >= 0:
x = tl.load(inp_ptr + (base + j) * K + k, mask=k_mask)
acc += x.to(tl.float32)
tl.store(out_ptr + pid_m * K + k, acc.to(out_ptr.dtype.element_ty), mask=k_mask)
def masked_moe_sum(
intermediate: torch.Tensor, # (M, topk, K)
topk_ids: torch.Tensor, # (M, topk) int, -1 = invalid / non-local slot
output: torch.Tensor, # (M, K)
) -> None:
M, topk, K = intermediate.shape
BLOCK_K = 1024
grid = (M, triton.cdiv(K, BLOCK_K))
_masked_topk_sum_kernel[grid](
intermediate, topk_ids, output, K, topk=topk, BLOCK_K=BLOCK_K
)
@triton.jit
def _apply_expert_map_kernel(
topk_ids_ptr, # [n] global expert IDs (-1 = invalid)
expert_map_ptr, # [num_experts] global->local (-1 for non-local)
out_ptr, # [n] int64 local expert IDs (-1 for invalid/non-local)
n_elements,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n_elements
tid = tl.load(topk_ids_ptr + offs, mask=mask, other=-1)
# Gather expert_map[tid] for valid (tid >= 0); clamp the index so invalid
# rows don't read OOB, then select -1 for them. Matches
# torch.where(tid >= 0, expert_map[clamp(tid, 0)], -1) -- preserving -1 (a
# plain expert_map[-1] would wrap to a valid local id and misroute).
valid = tid >= 0
idx = tl.where(valid, tid, 0)
local = tl.load(expert_map_ptr + idx, mask=mask, other=-1)
out = tl.where(valid, local.to(tl.int64), -1)
tl.store(out_ptr + offs, out, mask=mask)
def apply_expert_map(topk_ids: torch.Tensor, expert_map: torch.Tensor) -> torch.Tensor:
"""Fused global->local expert-id mapping preserving -1.
Replaces ``torch.where(topk_ids >= 0, expert_map[topk_ids.clamp(min=0)], -1)``
with one kernel. Returns a NEW int64 tensor -- the caller keeps the original
``topk_ids`` as ``global_topk_ids``, so this must not write in place.
"""
out = torch.empty_like(topk_ids, dtype=torch.int64)
n = topk_ids.numel()
BLOCK = 1024
grid = (triton.cdiv(n, BLOCK),)
_apply_expert_map_kernel[grid](topk_ids, expert_map, out, n, BLOCK=BLOCK)
return out
class BaseOAITritonExperts(mk.FusedMoEExpertsModular):
@property
def expects_unquantized_inputs(self) -> bool:
@@ -708,7 +1019,9 @@ class OAITritonExperts(BaseOAITritonExperts):
self.quant_config: FusedMoEQuantConfig = FUSED_MOE_UNQUANTIZED_CONFIG
if expert_map is not None:
topk_ids = expert_map[topk_ids]
# Preserve -1 (invalid / non-local slots, e.g. from EP dispatch):
# make_routing_data treats -1 as the skip sentinel.
topk_ids = apply_expert_map(topk_ids, expert_map)
local_num_experts = w1.shape[0]
if global_num_experts == -1:
@@ -780,9 +1093,6 @@ class UnfusedOAITritonExperts(LoRAExpertsMixin, BaseOAITritonExperts):
output = (M, K)
return (workspace1, workspace2, output)
def moe_sum(self, input: torch.Tensor, output: torch.Tensor):
ops.moe_sum(input, output)
def activation(
self,
activation: MoEActivation,
@@ -853,7 +1163,9 @@ class UnfusedOAITritonExperts(LoRAExpertsMixin, BaseOAITritonExperts):
global_topk_ids = topk_ids
if expert_map is not None:
topk_ids = expert_map[topk_ids]
# Preserve -1 (invalid / non-local slots, e.g. from EP dispatch):
# make_routing_data treats -1 as the skip sentinel.
topk_ids = apply_expert_map(topk_ids, expert_map)
local_num_experts = w1.shape[0]
if global_num_experts == -1:
@@ -976,7 +1288,9 @@ class UnfusedOAITritonExperts(LoRAExpertsMixin, BaseOAITritonExperts):
top_k_num=topk,
)
self.moe_sum(intermediate_cache3.view(-1, topk, K), output)
# matmul_ogs leaves invalid (-1 / non-local EP) slots unwritten.
# Reduce over topk skipping those slots.
masked_moe_sum(intermediate_cache3.view(-1, topk, K), topk_ids, output)
class OAITritonMxfp4ExpertsMonolithic(mk.FusedMoEExpertsMonolithic):
@@ -10,6 +10,7 @@ from typing import final
import torch
import vllm.envs as envs
from vllm.forward_context import get_forward_context
from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe.activation import (
MoEActivation,
@@ -1132,6 +1133,17 @@ class FusedMoEKernelModularImpl:
The _prepare method is a wrapper around self.prepare_finalize.prepare
that handles DBO and async.
"""
# Skip cudagraph/DP padding tokens uniformly across all a2a backends:
# forcing padded rows' expert ids to -1 makes every prepare_finalize drop
# them (not dispatched / not computed by the experts). The V2 model runner
# marks them in forward_context.is_padding; it is None for runners that do
# not populate it, leaving topk_ids unchanged.
is_padding = get_forward_context().is_padding
if is_padding is not None:
n = topk_ids.shape[0]
# TODO: Properly support DBO (padding lives at the batch tail).
topk_ids = torch.where(is_padding[:n].unsqueeze(1), -1, topk_ids)
if not self.prepare_finalize.supports_async():
# We shouldn't be running an a2a kernel that doesn't
# support async prepare/finalize
@@ -6,12 +6,14 @@ import deep_ep
import torch
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
from vllm.forward_context import get_forward_context
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.triton_utils import tl, triton
from vllm.utils.math_utils import round_up
from vllm.v1.worker.ubatching import (
dbo_current_ubatch_id,
@@ -116,6 +118,28 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
do_expand = not self.use_cudagraph
do_cpu_sync = not self.use_cudagraph
# In do_expand=False mode, the recv buffer is the worst case
# R * num_max_tokens_per_rank. Defaulting to the buffer's init value
# (= max_num_batched_tokens) makes the experts process ~R*8192 rows even
# for a handful of decode tokens. Bound it to the actual DP-padded batch
# size (uniform across ranks): max(num_tokens_across_dp).
#
# DeepEP JIT-compiles a separate dispatch kernel per distinct
# num_max_tokens_per_rank, so feeding it the raw per-step size would make
# it recompile for every batch size (a cicc storm that starves the GPU at
# high concurrency). Round up to a power of 2 instead: this bounds the
# set to ~log2(max_num_batched_tokens) values (compiled once, then
# cached) while staying small for decode (e.g. 1 token -> 1) and capped
# at the buffer's init capacity for prefill.
num_max_tokens_per_rank = None
if not do_expand:
dp_meta = get_forward_context().dp_metadata
if dp_meta is not None:
n = int(dp_meta.num_tokens_across_dp_cpu.max())
else:
n = tokens.shape[0]
num_max_tokens_per_rank = 1 << max(n - 1, 0).bit_length()
(
recv_x,
recv_topk_idx,
@@ -127,6 +151,7 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
topk_idx=rank_topk_ids,
topk_weights=rank_topk_weights,
num_experts=num_experts,
num_max_tokens_per_rank=num_max_tokens_per_rank,
do_expand=do_expand,
do_cpu_sync=do_cpu_sync,
async_with_compute_stream=False,
@@ -196,17 +221,22 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
)
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 and padding rows).
# Convert valid local IDs to global. Rows with -1 are
# skipped by expert kernels (TrtLLM tile-level skipping,
# DeepGemm is_computation_valid), so no need to zero
# hidden states, scales, or weights for padding rows.
valid_mask = recv_topk_idx >= 0
recv_topk_idx = torch.where(
valid_mask,
recv_topk_idx + self.rank_expert_offset,
# do_expand=False (decode/cudagraph mode): the dispatch only writes
# rows [0, num_recv_tokens); the rest of the worst-case-allocated
# buffer is left UNINITIALIZED. For valid rows, recv_topk_idx holds
# LOCAL expert IDs (-1 for non-local slots). Convert valid local IDs
# to global and force everything else to -1:
# * non-local / out-of-range expert slots, and
# * every row >= num_recv_tokens (uninitialized padding): its
# stale contents can alias valid expert IDs and would otherwise
# 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.
recv_topk_idx = _globalize_recv_topk_idx(
recv_topk_idx,
psum_recv_per_rank,
self.rank_expert_offset,
self.num_experts,
)
# Reshape recv_topk_weights to match recv_topk_idx shape [N, 1]
@@ -392,3 +422,51 @@ class DeepEPV2PrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
weight_and_reduce_impl,
False,
)
@triton.jit
def _globalize_recv_topk_idx_kernel(
topk_idx_ptr, # [N*topk] local expert IDs (-1 = non-local), modified in place
psum_ptr, # [P] per-scaleup-rank recv prefix sum; num_recv = psum[P-1]
P,
rank_expert_offset,
num_experts,
n_elements, # N * topk
topk: tl.constexpr,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n_elements
# num_recv_tokens read on-device (no host sync) -> cudagraph-safe.
num_recv = tl.load(psum_ptr + P - 1)
val = tl.load(topk_idx_ptr + offs, mask=mask, other=-1)
g = val + rank_expert_offset
row = offs // topk
# Keep a slot iff: it is a local expert (val >= 0), its global id is in
# range, and its row is a real received token (< num_recv). Otherwise -1.
valid = (val >= 0) & (g < num_experts) & (row < num_recv)
tl.store(topk_idx_ptr + offs, tl.where(valid, g, -1), mask=mask)
def _globalize_recv_topk_idx(
recv_topk_idx: torch.Tensor, # [N, topk] local expert IDs, -1 = non-local
psum_recv_per_rank: torch.Tensor,
rank_expert_offset: int,
num_experts: int,
) -> torch.Tensor:
N, topk = recv_topk_idx.shape
n = N * topk
BLOCK = 1024
grid = (triton.cdiv(n, BLOCK),)
_globalize_recv_topk_idx_kernel[grid](
recv_topk_idx,
psum_recv_per_rank,
psum_recv_per_rank.shape[0],
rank_expert_offset,
num_experts,
n,
topk=topk,
BLOCK=BLOCK,
)
return recv_topk_idx
+9
View File
@@ -16,6 +16,7 @@ from vllm.distributed import (
get_tensor_model_parallel_world_size,
)
from vllm.distributed.eplb.eplb_state import EplbLayerState
from vllm.forward_context import get_forward_context, is_forward_context_available
from vllm.model_executor.kernels.mhc.tilelang import (
hc_head_fused_kernel_tilelang,
mhc_fused_post_pre_tilelang,
@@ -440,6 +441,11 @@ class DeepseekV4MegaMoEExperts(nn.Module):
symm_buffer = self.get_symm_buffer()
num_tokens = hidden_states.shape[0]
is_padding = None
if is_forward_context_available():
is_padding = get_forward_context().is_padding
if is_padding is not None:
is_padding = is_padding[:num_tokens]
# EPLB: map logical expert IDs to physical replicas and record load.
eplb_state = self.eplb_state
@@ -447,6 +453,8 @@ class DeepseekV4MegaMoEExperts(nn.Module):
assert eplb_state.expert_load_view is not None
assert eplb_state.logical_replica_count is not None
assert eplb_state.should_record_tensor is not None
if is_padding is not None:
topk_ids = torch.where(is_padding.unsqueeze(1), -1, topk_ids)
topk_ids = eplb_map_to_physical_and_record(
topk_ids=topk_ids,
expert_load_view=eplb_state.expert_load_view,
@@ -463,6 +471,7 @@ class DeepseekV4MegaMoEExperts(nn.Module):
symm_buffer.x_sf[:num_tokens],
symm_buffer.topk_idx[:num_tokens],
symm_buffer.topk_weights[:num_tokens],
is_padding=is_padding,
)
# This method must have been already called during the weight loading phase.
@@ -19,6 +19,7 @@ def _prepare_megamoe_inputs_kernel(
x_sf,
topk_ids,
topk_weights,
is_padding,
topk_idx_out,
topk_weights_out,
hidden_stride_m: tl.constexpr,
@@ -31,6 +32,7 @@ def _prepare_megamoe_inputs_kernel(
topk_ids_stride_k: tl.constexpr,
topk_weights_stride_m: tl.constexpr,
topk_weights_stride_k: tl.constexpr,
is_padding_stride_m: tl.constexpr,
topk_idx_stride_m: tl.constexpr,
topk_idx_stride_k: tl.constexpr,
topk_weights_out_stride_m: tl.constexpr,
@@ -85,12 +87,16 @@ def _prepare_megamoe_inputs_kernel(
if k_block_id == 0:
topk_offsets = tl.arange(0, BLOCK_TOPK)
topk_mask = topk_offsets < top_k
token_is_padding = False
if is_padding is not None:
token_is_padding = tl.load(is_padding + token_id * is_padding_stride_m)
ids = tl.load(
topk_ids + token_id * topk_ids_stride_m + topk_offsets * topk_ids_stride_k,
mask=topk_mask,
other=0,
).to(tl.int64)
ids = tl.where(token_is_padding, -1, ids)
tl.store(
topk_idx_out
+ token_id * topk_idx_stride_m
@@ -106,6 +112,7 @@ def _prepare_megamoe_inputs_kernel(
mask=topk_mask,
other=0.0,
)
weights = tl.where(token_is_padding, 0.0, weights)
tl.store(
topk_weights_out
+ token_id * topk_weights_out_stride_m
@@ -123,6 +130,7 @@ def prepare_megamoe_inputs(
x_sf: torch.Tensor,
topk_idx_out: torch.Tensor,
topk_weights_out: torch.Tensor,
is_padding: torch.Tensor | None = None,
) -> None:
num_tokens, hidden_size = hidden_states.shape
if num_tokens == 0:
@@ -142,12 +150,14 @@ def prepare_megamoe_inputs(
block_k = 128
grid = (num_tokens, triton.cdiv(hidden_size, block_k))
block_topk = triton.next_power_of_2(top_k)
padding_stride_m = is_padding.stride(0) if is_padding is not None else 0
_prepare_megamoe_inputs_kernel[grid](
hidden_states,
x_fp8,
x_sf,
topk_ids,
topk_weights,
is_padding,
topk_idx_out,
topk_weights_out,
hidden_states.stride(0),
@@ -160,6 +170,7 @@ def prepare_megamoe_inputs(
topk_ids.stride(1),
topk_weights.stride(0),
topk_weights.stride(1),
padding_stride_m,
topk_idx_out.stride(0),
topk_idx_out.stride(1),
topk_weights_out.stride(0),
+4
View File
@@ -471,6 +471,9 @@ class ModelCudaGraphManager(CudaGraphManager):
skip_attn=(desc.cg_mode == CUDAGraphMode.PIECEWISE),
)
# Capture with all-padding padding mask.
input_buffers.is_padding.fill_(True)
def forward_fn(cg_mode: CUDAGraphMode) -> None:
batch_descriptor = None
if cg_mode == CUDAGraphMode.PIECEWISE:
@@ -488,6 +491,7 @@ class ModelCudaGraphManager(CudaGraphManager):
num_tokens_across_dp=num_tokens_across_dp,
slot_mapping=slot_mappings,
batch_descriptor=batch_descriptor,
is_padding=input_buffers.is_padding[:num_tokens],
):
if cg_mode == CUDAGraphMode.PIECEWISE:
# PIECEWISE graph (compiled PW or breakable, chosen inside
+13
View File
@@ -22,6 +22,7 @@ class InputBuffers:
self.input_ids = torch.zeros(max_num_tokens, dtype=torch.int32, device=device)
self.positions = torch.zeros(max_num_tokens, dtype=torch.int64, device=device)
self.is_padding = torch.zeros(max_num_tokens, dtype=torch.bool, device=device)
self.query_start_loc = torch.zeros(
max_num_reqs + 1, dtype=torch.int32, device=device
)
@@ -83,6 +84,8 @@ class InputBatch:
input_ids: torch.Tensor
# [num_tokens_after_padding]
positions: torch.Tensor
# [num_tokens_after_padding]
is_padding: torch.Tensor
# [total_num_logits]
logits_indices: torch.Tensor
@@ -99,9 +102,14 @@ class InputBatch:
num_reqs: int,
num_tokens: int,
input_buffers: InputBuffers,
num_actual_tokens: int | None = None,
) -> "InputBatch":
assert 0 < num_reqs <= num_tokens
device = input_buffers.device
# Rows [num_actual_tokens, num_tokens) are treated as padding.
# Default (None) means all real.
if num_actual_tokens is None:
num_actual_tokens = num_tokens
req_ids = [f"req_{i}_{random_uuid()}" for i in range(num_reqs)]
idx_mapping_np = np.arange(num_reqs, dtype=np.int32)
@@ -134,6 +142,10 @@ class InputBatch:
input_ids = input_buffers.input_ids[:num_tokens].zero_()
positions = input_buffers.positions[:num_tokens].zero_()
input_buffers.is_padding[:num_actual_tokens].fill_(False)
input_buffers.is_padding[num_actual_tokens:num_tokens].fill_(True)
is_padding = input_buffers.is_padding[:num_tokens]
logits_indices = query_start_loc[1:] - 1
cu_num_logits = torch.arange(num_reqs + 1, device=device, dtype=torch.int32)
cu_num_logits_np = np.arange(num_reqs + 1, dtype=np.int32)
@@ -164,6 +176,7 @@ class InputBatch:
max_seq_len_np=None,
input_ids=input_ids,
positions=positions,
is_padding=is_padding,
logits_indices=logits_indices,
cu_num_logits=cu_num_logits,
cu_num_logits_np=cu_num_logits_np,
+7
View File
@@ -847,6 +847,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
num_tokens = scheduler_output.total_num_scheduled_tokens
num_tokens_after_padding = batch_desc.num_tokens
assert num_tokens > 0
# Mark trailing cudagraph-padding rows so the MoE can optionally
# skip a2a + compute for them.
self.input_buffers.is_padding[:num_tokens].fill_(False)
self.input_buffers.is_padding[num_tokens:num_tokens_after_padding].fill_(True)
num_tokens_per_req = scheduler_output.num_scheduled_tokens
num_reqs = len(num_tokens_per_req)
@@ -1001,6 +1005,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
max_seq_len_np=max_seq_len_np,
input_ids=self.input_buffers.input_ids[:num_tokens_after_padding],
positions=self.input_buffers.positions[:num_tokens_after_padding],
is_padding=self.input_buffers.is_padding[:num_tokens_after_padding],
logits_indices=logits_indices,
cu_num_logits=cu_num_logits,
cu_num_logits_np=cu_num_logits_np,
@@ -1175,6 +1180,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
batch_desc.num_reqs or num_reqs,
batch_desc.num_tokens,
self.input_buffers,
num_actual_tokens=0,
)
if not skip_attn_for_dummy_run:
block_tables, slot_mappings = self.prepare_dummy_attn(input_batch)
@@ -1277,6 +1283,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
batch_descriptor=batch_descriptor,
slot_mapping=slot_mappings_by_layer,
skip_compiled=skip_compiled,
is_padding=input_batch.is_padding,
):
self.kv_connector.pre_forward(scheduler_output)
if batch_desc.cg_mode == CUDAGraphMode.PIECEWISE: