[AMD] Fuse topk padded-token masking into a single Triton kernel (#28084)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
karverma-amd
2026-06-23 11:15:12 -07:00
committed by GitHub
co-authored by Cursor
parent ed26a109ee
commit e0dc8b7137
2 changed files with 278 additions and 4 deletions
+84 -4
View File
@@ -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.