forked from Karylab-cklius/vllm
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb9f5790bb | ||
|
|
57b8526cfb | ||
|
|
25411f3138 | ||
|
|
261a8820c7 | ||
|
|
416977534a | ||
|
|
ca54c027c4 | ||
|
|
65e05df079 | ||
|
|
2a09d50034 | ||
|
|
6f3d89d105 | ||
|
|
a06a16ff0a | ||
|
|
a1d80989d9 | ||
|
|
40a19bed77 | ||
|
|
8e8f8c2e2c | ||
|
|
116d3e3149 | ||
|
|
f6fa9700e6 | ||
|
|
0221ab433e |
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user