[AMD] Fuse topk padded-token masking into a single Triton kernel (#28084)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,8 @@ from typing import (
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
|
||||
@@ -130,6 +132,13 @@ _is_xpu = is_xpu()
|
||||
_use_aiter = get_bool_env_var("SGLANG_USE_AITER") and _is_hip
|
||||
_is_musa = is_musa()
|
||||
|
||||
# Experimental: skip the HIP padded-token routing-weight masking entirely.
|
||||
# Padded (CUDA-graph) rows are discarded downstream and the MoE combine is
|
||||
# per-token, so zeroing their weights is in principle unnecessary. Gated off by
|
||||
# default because it is a numerics-affecting change that must be validated with
|
||||
# an accuracy run before becoming the default.
|
||||
_skip_hip_pad_mask = get_bool_env_var("SGLANG_MORI_NO_PAD_MASK", "False")
|
||||
|
||||
if _is_cuda:
|
||||
from sgl_kernel import moe_fused_gate
|
||||
|
||||
@@ -1134,6 +1143,71 @@ def is_power_of_two(n):
|
||||
return n > 0 and math.log2(n).is_integer()
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fill_padded_rows_kernel(
|
||||
out_ptr,
|
||||
num_token_non_padded_ptr,
|
||||
n_cols,
|
||||
fill_value,
|
||||
stride_row,
|
||||
BLOCK_COLS: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
n_valid = tl.load(num_token_non_padded_ptr)
|
||||
if row >= n_valid:
|
||||
cols = tl.arange(0, BLOCK_COLS)
|
||||
mask = cols < n_cols
|
||||
ptrs = out_ptr + row * stride_row + cols
|
||||
fill = tl.full((BLOCK_COLS,), fill_value, dtype=out_ptr.dtype.element_ty)
|
||||
tl.store(ptrs, fill, mask=mask)
|
||||
|
||||
|
||||
def _can_fuse_padded_region(x: torch.Tensor) -> bool:
|
||||
# The fused kernel uses one program per row and assumes a row-major 2D
|
||||
# tensor (columns contiguous); fall back to eager for anything else.
|
||||
return x.dim() == 2 and x.stride(1) == 1
|
||||
|
||||
|
||||
def _fill_padded_rows(
|
||||
x: torch.Tensor,
|
||||
num_token_non_padded: torch.Tensor,
|
||||
fill_value,
|
||||
) -> None:
|
||||
"""Set ``x[row, :] = fill_value`` for every padded row (row index
|
||||
``>= num_token_non_padded``) using a single Triton launch.
|
||||
|
||||
Replaces the eager ``arange + (>=) + boolean index_put_`` sequence, which
|
||||
issues several launch-latency-bound kernels per call. The grid is static
|
||||
(one program per row) and the pad count is read from device memory inside
|
||||
the kernel, so this is safe to capture inside a CUDA/HIP graph.
|
||||
"""
|
||||
# Metadata-only checks (no device sync): the kernel reads a single scalar
|
||||
# routing count from device memory, so it must be a 1-element integer tensor
|
||||
# on the same device as ``x``.
|
||||
assert isinstance(
|
||||
num_token_non_padded, torch.Tensor
|
||||
), "num_token_non_padded must be a torch.Tensor"
|
||||
assert num_token_non_padded.numel() == 1, (
|
||||
"num_token_non_padded must be a single-element tensor, got shape "
|
||||
f"{tuple(num_token_non_padded.shape)}"
|
||||
)
|
||||
assert (
|
||||
not num_token_non_padded.dtype.is_floating_point
|
||||
), f"num_token_non_padded must be an integer tensor, got {num_token_non_padded.dtype}"
|
||||
assert (
|
||||
num_token_non_padded.device == x.device
|
||||
), "num_token_non_padded and x must be on the same device"
|
||||
n_rows, n_cols = x.shape
|
||||
_fill_padded_rows_kernel[(n_rows,)](
|
||||
x,
|
||||
num_token_non_padded,
|
||||
n_cols,
|
||||
fill_value,
|
||||
x.stride(0),
|
||||
BLOCK_COLS=triton.next_power_of_2(n_cols),
|
||||
)
|
||||
|
||||
|
||||
def _eplb_remap_enabled() -> bool:
|
||||
# A real logical->physical mapping only exists when EPLB is enabled, the
|
||||
# initial expert placement is non-trivial, or there are redundant physical
|
||||
@@ -1172,6 +1246,8 @@ def _mask_topk_ids_padded_region(
|
||||
indices = torch.arange(0, topk_ids.shape[0], device=topk_ids.device)
|
||||
mask = (indices >= num_token_non_padded).unsqueeze(-1)
|
||||
topk_ids = torch.where(mask, torch.full_like(topk_ids, -1), topk_ids)
|
||||
elif _can_fuse_padded_region(topk_ids):
|
||||
_fill_padded_rows(topk_ids, num_token_non_padded, fill_value)
|
||||
else:
|
||||
indices = torch.arange(0, topk_ids.shape[0], device=topk_ids.device)
|
||||
topk_ids[indices >= num_token_non_padded, :] = fill_value
|
||||
@@ -1183,6 +1259,9 @@ def _zero_topk_weights_padded_region(
|
||||
):
|
||||
if num_token_non_padded is None:
|
||||
return
|
||||
if _can_fuse_padded_region(topk_weights):
|
||||
_fill_padded_rows(topk_weights, num_token_non_padded, 0.0)
|
||||
return
|
||||
indices = torch.arange(0, topk_weights.shape[0], device=topk_weights.device)
|
||||
topk_weights[indices >= num_token_non_padded, :] = 0.0
|
||||
|
||||
@@ -1591,9 +1670,10 @@ def _post_process_topk_ids(
|
||||
topk_ids = topk_ids_logical_to_physical(
|
||||
topk_ids, expert_location_dispatch_info
|
||||
)
|
||||
# On AMD HIP the aiter MoE kernels do not handle topk_ids=-1 safely, so
|
||||
# padded tokens are neutralized by zeroing their routing weights.
|
||||
_zero_topk_weights_padded_region(topk_weights, num_token_non_padded)
|
||||
# NOTE (HIP): padded-token routing-weight zeroing is deferred to the
|
||||
# single pass at the end of this function (gated by SGLANG_MORI_NO_PAD_MASK).
|
||||
# That final pass re-zeros after any shared-expert append/remap, so a
|
||||
# second zeroing here would be redundant (zeroing is idempotent).
|
||||
|
||||
if recorder_topk_ids is None:
|
||||
recorder_topk_ids = topk_ids
|
||||
@@ -1635,7 +1715,7 @@ def _post_process_topk_ids(
|
||||
topk_config,
|
||||
)
|
||||
|
||||
if _is_hip:
|
||||
if _is_hip and not _skip_hip_pad_mask:
|
||||
# Shared-expert append/remap can introduce non-zero weights after the
|
||||
# initial HIP padding mask above. Ensure padded tokens leave this helper
|
||||
# with all expert weights zeroed.
|
||||
|
||||
Reference in New Issue
Block a user