[AMD] DeepSeek-V4: add aiter fused mHC post+pre with cross-layer boundary dispatch (#32577)

Co-authored-by: 1am9trash <1am9trash@gmail.com>
Co-authored-by: HAI <hixiao@gmail.com>
This commit is contained in:
karverma-amd
2026-08-22 07:34:57 -07:00
committed by GitHub
co-authored by 1am9trash HAI
parent db570fe619
commit d315eb7250
4 changed files with 801 additions and 128 deletions
@@ -4,13 +4,55 @@ from typing import Optional, Tuple
import torch
import triton
from sglang.srt.environ import envs
from sglang.srt.utils import get_bool_env_var, is_gfx95_supported, is_hip
from sglang.srt.utils.common import is_sm120_supported
logger = logging.getLogger(__name__)
_is_hip = is_hip()
_is_gfx95_supported = is_gfx95_supported()
_FUSED_HC_POST_PRE_M_THRESHOLD = 64
_FUSED_HC_POST_PRE_CACHE: dict[tuple, dict[str, torch.Tensor]] = {}
_TRITON_MHC_POST_PRE_OPS = None
_TRITON_MHC_POST_PRE_RUNTIME_DISABLED = False
# aiter fused mHC state. The kernel self-gates the fused-vs-unfused decision per
# arch internally (see aiter.ops.mhc.mhc_fused_post_pre), so this wrapper only
# tracks import/runtime availability and never re-implements the token threshold.
_AITER_MHC_FUSED_POST_PRE_RUNTIME_DISABLED = False
_AITER_MHC_IMPORT_WARNED = False
def _is_fused_mhc_post_pre_enabled() -> bool:
# SM120 disables the standalone TileLang pre path. mhc_fused_post_pre does
# not read that flag and dispatches independently for both small and large
# token batches, so the standalone pre flag must not veto the fused opt-in.
return (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.get()
and envs.SGLANG_OPT_USE_TILELANG_MHC_POST.get()
and (envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.get() or is_sm120_supported())
)
def _is_aiter_gfx95_mhc_available() -> bool:
return _is_hip and get_bool_env_var("SGLANG_USE_AITER") and is_gfx95_supported()
def _is_production_mhc_enabled() -> bool:
return _is_fused_mhc_post_pre_enabled() or _is_aiter_gfx95_mhc_available()
def is_cross_layer_mhc_fusion_enabled() -> bool:
"""Whether DeepSeek-V4 may defer mHC post across the attn/MoE boundary.
Cross-layer fusion requires a fused post+pre kernel to be available: either
the TileLang path (``SGLANG_OPT_FUSE_MHC_POST_PRE`` + TileLang pre/post) or
the aiter HIP path on a supported gfx95 device.
"""
return _is_production_mhc_enabled()
def _get_triton_mhc_post_pre_ops():
global _TRITON_MHC_POST_PRE_OPS
@@ -153,3 +195,220 @@ def try_fused_hc_post_pre(
return None
return new_residual, layer_input_out, bufs["h_post"], bufs["h_res"], False
def try_aiter_fused_mhc_post_pre(
layer_input: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_eps: float,
hc_post_mult: float,
sinkhorn_iters: int,
norm_weight: Optional[torch.Tensor],
norm_eps: Optional[float],
) -> Optional[Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, bool]]:
"""Fused mhc_post + next-layer mhc_pre via the aiter HIP kernel.
Returns ``(next_residual, layer_input, post_mix, comb_mix, norm_applied)`` or
``None`` to let the caller fall back. The aiter kernel internally chooses
between its fused and unfused (mhc_post + mhc_pre) implementations based on the
token count and detected arch, so no token threshold is applied here.
"""
global _AITER_MHC_FUSED_POST_PRE_RUNTIME_DISABLED, _AITER_MHC_IMPORT_WARNED
if (
_AITER_MHC_FUSED_POST_PRE_RUNTIME_DISABLED
or not _is_aiter_gfx95_mhc_available()
or layer_input.shape[0] == 0
or layer_input.dim() != 2
or residual.dim() != 3
):
return None
try:
from aiter.ops.mhc import mhc_fused_post_pre
except Exception as err:
if not _AITER_MHC_IMPORT_WARNED:
logger.warning("aiter fused mHC is unavailable, falling back: %s", err)
_AITER_MHC_IMPORT_WARNED = True
_AITER_MHC_FUSED_POST_PRE_RUNTIME_DISABLED = True
return None
norm_kwargs = {}
if norm_weight is not None:
norm_kwargs["norm_weight"] = norm_weight
norm_kwargs["norm_eps"] = norm_eps if norm_eps is not None else rms_eps
try:
post_mix, comb_mix, layer_input_out, next_residual = mhc_fused_post_pre(
layer_input=layer_input,
residual_in=residual,
post_layer_mix=post,
comb_res_mix=comb,
fn=hc_fn,
hc_scale=hc_scale,
hc_base=hc_base,
rms_eps=rms_eps,
hc_pre_eps=hc_eps,
hc_sinkhorn_eps=hc_eps,
hc_post_mult_value=hc_post_mult,
sinkhorn_repeat=sinkhorn_iters,
**norm_kwargs,
)
except Exception as err:
logger.warning(
"aiter fused mHC kernel failed, disabling fallback path: %s", err
)
_AITER_MHC_FUSED_POST_PRE_RUNTIME_DISABLED = True
return None
post_out = post_mix.squeeze(-1) if post_mix.ndim == 3 else post_mix
return (
next_residual,
layer_input_out,
post_out,
comb_mix,
norm_weight is not None,
)
def try_mhc_fused_post_pre_boundary(
layer_input: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_mult: int,
rms_eps: float,
hc_eps: float,
hc_post_mult: float,
sinkhorn_iters: int,
norm_weight: Optional[torch.Tensor],
norm_eps: Optional[float],
fn_transpose: bool,
is_gfx95_supported_flag: bool,
) -> Optional[Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, bool]]:
"""Dispatch the fused mHC post+pre across the attn/MoE boundary.
Preference order (first available wins): aiter HIP kernel, then the Triton
kernel. Returns ``None`` when neither fires so the caller can fall back to the
TileLang path or the unfused ``hc_post`` + ``hc_pre`` sequence.
The aiter and Triton kernels expect opposite ``fn`` orientations: the Triton
kernel takes ``hc_fn`` transposed (``fn_transpose``), while aiter consumes it
in the native ``mhc_pre`` layout.
"""
aiter_result = try_aiter_fused_mhc_post_pre(
layer_input,
residual,
post,
comb,
hc_fn,
hc_scale,
hc_base,
rms_eps,
hc_eps,
hc_post_mult,
sinkhorn_iters,
norm_weight,
norm_eps,
)
if aiter_result is not None:
return aiter_result
triton_fn = hc_fn.T if fn_transpose else hc_fn
return try_fused_hc_post_pre(
layer_input,
residual,
post,
comb,
triton_fn,
hc_scale,
hc_base,
hc_mult,
rms_eps,
hc_eps,
hc_post_mult,
sinkhorn_iters,
is_gfx95_supported_flag,
)
def apply_mhc_post_pre_boundary(
layer_input: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_mult: int,
rms_eps: float,
hc_eps: float,
hc_post_mult: float,
sinkhorn_iters: int,
norm_weight: Optional[torch.Tensor],
norm_eps: Optional[float],
*,
fn_transpose: bool,
) -> Optional[Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, bool]]:
# Try the aiter/Triton fused post+pre kernels first; if neither fires,
# fall back to the TileLang fused kernel, else return None so the caller
# runs the unfused hc_post + hc_pre sequence.
fused = try_mhc_fused_post_pre_boundary(
layer_input,
residual,
post,
comb,
hc_fn,
hc_scale,
hc_base,
hc_mult,
rms_eps,
hc_eps,
hc_post_mult,
sinkhorn_iters,
norm_weight,
norm_eps,
fn_transpose,
_is_gfx95_supported,
)
if fused is not None:
return fused
if not _is_fused_mhc_post_pre_enabled():
return None
from sglang.srt.models.deepseek_v4 import _get_mhc_ops
post_in = post.unsqueeze(-1) if post.ndim == 2 else post
(
residual,
post_out,
comb_out,
layer_input_out,
) = _get_mhc_ops().mhc_fused_post_pre(
layer_input,
residual,
post_in,
comb,
hc_fn,
hc_scale,
hc_base,
rms_eps,
hc_eps,
hc_eps,
hc_post_mult,
sinkhorn_iters,
norm_weight=norm_weight,
norm_eps=norm_eps,
)
post_out = post_out.squeeze(-1) if post_out.ndim == 3 else post_out
return residual, layer_input_out, post_out, comb_out, True
+142 -49
View File
@@ -138,7 +138,8 @@ from sglang.srt.model_loader.weight_utils import (
)
from sglang.srt.models.dbrx import ReplicatedLinear
from sglang.srt.models.deepseek_common.amd.deepseek_v4_fused_mhc import (
try_fused_hc_post_pre,
apply_mhc_post_pre_boundary,
is_cross_layer_mhc_fusion_enabled,
)
from sglang.srt.models.deepseek_common.utils import (
_use_aiter_bpreshuffle_gfx95,
@@ -168,7 +169,6 @@ from sglang.srt.utils import (
log_info_on_rank0,
make_layers,
)
from sglang.srt.utils.common import is_sm120_supported
from sglang.srt.utils.custom_op import register_custom_op
from sglang.srt.utils.hf_transformers_utils import get_rope_config
@@ -219,17 +219,6 @@ DEEPSEEK_V4_STACKED_PARAMS_MAPPING: List[Tuple[str, str, int]] = [
]
def _is_fused_mhc_post_pre_enabled() -> bool:
# SM120 disables the standalone TileLang pre path. mhc_fused_post_pre does
# not read that flag and dispatches independently for both small and large
# token batches, so the standalone pre flag must not veto the fused opt-in.
return (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.get()
and envs.SGLANG_OPT_USE_TILELANG_MHC_POST.get()
and (envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.get() or is_sm120_supported())
)
# FlashInfer's mhc_pre_big_fuse only accepts these split-K counts.
_FLASHINFER_MHC_PRE_SPLITS = (1, 2, 4, 8, 16)
@@ -1792,7 +1781,7 @@ class DeepseekV4DecoderLayer(nn.Module):
) = make_hc_mixing_params(hc_mult, config.hidden_size)
self.rms_norm_eps = config.rms_norm_eps
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.use_fused_mhc_post_pre = _is_fused_mhc_post_pre_enabled()
self.use_fused_mhc_post_pre = is_cross_layer_mhc_fusion_enabled()
self._input_layernorm_weight_bf16 = None
self._post_attention_layernorm_weight_bf16 = None
@@ -2022,7 +2011,12 @@ class DeepseekV4DecoderLayer(nn.Module):
use_fused = self.use_fused_mhc_post_pre
if prev_residual is not None and use_fused:
residual, post, comb, hidden_states = _get_mhc_ops().mhc_fused_post_pre(
input_norm_weight = (
self._input_layernorm_weight_bf16
if self._input_layernorm_weight_bf16 is not None
else self.input_layernorm.weight.data
)
fused = apply_mhc_post_pre_boundary(
hidden_states,
prev_residual,
prev_post,
@@ -2030,19 +2024,63 @@ class DeepseekV4DecoderLayer(nn.Module):
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
self.hc_mult,
self.rms_norm_eps,
self.hc_eps,
self.hc_eps,
_MHC_POST_MULT_VALUE,
self.hc_sinkhorn_iters,
norm_weight=(
self._input_layernorm_weight_bf16
if self._input_layernorm_weight_bf16 is not None
else self.input_layernorm.weight.data
),
norm_eps=self.input_layernorm.variance_epsilon,
input_norm_weight,
self.input_layernorm.variance_epsilon,
fn_transpose=False,
)
x_quant = None
if fused is not None:
residual, hidden_states, post, comb, norm_fused = fused
if not norm_fused:
# The Triton fused post+pre returns the layer input WITHOUT
# the input layernorm applied (norm_fused=False). Apply it
# (fp8-quant on aiter gfx95) before attention, exactly as the
# unfused hc_pre path below does; otherwise unnormalized
# activations reach self_attn.
if _use_aiter and _is_gfx95_supported:
x_quant, hidden_states = _fused_rmsnorm_fp8_quant(
hidden_states,
self.input_layernorm.weight,
self.rms_norm_eps,
)
else:
hidden_states = self.input_layernorm(hidden_states)
x_quant = None
else:
x_quant = None
else:
# Fused dispatch declined: close the previous layer's deferred
# mHC post (prev_residual/prev_post/prev_comb) before opening this
# layer's pre. Skipping hc_post here would drop the previous-layer
# post state and corrupt all subsequent layers.
hidden_states = self.hc_post(
hidden_states, prev_residual, prev_post, prev_comb
)
residual = hidden_states
hidden_states, post, comb, norm_fused = self.hc_pre(
hidden_states,
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
norm=self.input_layernorm,
forward_batch=forward_batch,
)
if not norm_fused:
if _use_aiter and _is_gfx95_supported:
x_quant, hidden_states = _fused_rmsnorm_fp8_quant(
hidden_states,
self.input_layernorm.weight,
self.rms_norm_eps,
)
else:
hidden_states = self.input_layernorm(hidden_states)
x_quant = None
else:
x_quant = None
else:
residual = hidden_states
hidden_states, post, comb, norm_fused = self.hc_pre(
@@ -2075,12 +2113,17 @@ class DeepseekV4DecoderLayer(nn.Module):
)
if use_fused:
fused_mhc = try_fused_hc_post_pre(
post_attn_norm_weight = (
self._post_attention_layernorm_weight_bf16
if self._post_attention_layernorm_weight_bf16 is not None
else self.post_attention_layernorm.weight.data
)
fused = apply_mhc_post_pre_boundary(
hidden_states,
residual,
post,
comb,
self.hc_ffn_fn.T,
self.hc_ffn_fn,
self.hc_ffn_scale,
self.hc_ffn_base,
self.hc_mult,
@@ -2088,32 +2131,30 @@ class DeepseekV4DecoderLayer(nn.Module):
self.hc_eps,
_MHC_POST_MULT_VALUE,
self.hc_sinkhorn_iters,
_is_gfx95_supported,
post_attn_norm_weight,
self.post_attention_layernorm.variance_epsilon,
fn_transpose=True,
)
if fused_mhc is not None:
residual, hidden_states, post, comb, norm_fused = fused_mhc
if fused is not None:
residual, hidden_states, post, comb, norm_fused = fused
if not norm_fused:
# The Triton fused post+pre skips the post-attention
# layernorm (norm_fused=False); apply it before the MoE,
# matching the unfused hc_pre path below.
hidden_states = self.post_attention_layernorm(hidden_states)
else:
residual, post, comb, hidden_states = _get_mhc_ops().mhc_fused_post_pre(
hidden_states = self.hc_post(hidden_states, residual, post, comb)
residual = hidden_states
hidden_states, post, comb, norm_fused = self.hc_pre(
hidden_states,
residual,
post.unsqueeze(-1) if post.ndim == 2 else post,
comb,
self.hc_ffn_fn,
self.hc_ffn_scale,
self.hc_ffn_base,
self.rms_norm_eps,
self.hc_eps,
self.hc_eps,
_MHC_POST_MULT_VALUE,
self.hc_sinkhorn_iters,
norm_weight=(
self._post_attention_layernorm_weight_bf16
if self._post_attention_layernorm_weight_bf16 is not None
else self.post_attention_layernorm.weight.data
),
norm_eps=self.post_attention_layernorm.variance_epsilon,
norm=self.post_attention_layernorm,
forward_batch=forward_batch,
)
norm_fused = True
if not norm_fused:
hidden_states = self.post_attention_layernorm(hidden_states)
else:
hidden_states = self.hc_post(hidden_states, residual, post, comb)
residual = hidden_states
@@ -2352,11 +2393,63 @@ class DeepseekV4DecoderLayer(nn.Module):
def op_mhc_post_attn_pre_mlp(self, state):
# Close the attention mHC (hc_post), then open the FFN-side mHC pre +
# post-attention layernorm. Produces the 2D MoE input.
#
# Pop each boundary tensor from the state EXACTLY ONCE, up front, and
# reuse the locals for both the fused attempt and the non-fused
# fallback. apply_mhc_post_pre_boundary() returns None when it declines
# to fuse -- most importantly for the 0-token DP two-batch-overlap idle
# ubatch -- in which case control must fall through to the unfused
# hc_post. Popping in the fused call's arguments and again in the
# fallback would double-pop -> KeyError: 'hidden_states_after_attn' on
# every idle DP rank. use_fused_mhc_post_pre is on whenever the aiter
# gfx95 mHC path is available (is_cross_layer_mhc_fusion_enabled), so
# this fallback is reached under DP regardless of the TileLang env.
hidden_states_after_attn = state.pop("hidden_states_after_attn")
attn_residual = state.pop("attn_residual")
attn_post = state.pop("attn_post")
attn_comb = state.pop("attn_comb")
if self.use_fused_mhc_post_pre:
post_attn_norm_weight = (
self._post_attention_layernorm_weight_bf16
if self._post_attention_layernorm_weight_bf16 is not None
else self.post_attention_layernorm.weight.data
)
fused = apply_mhc_post_pre_boundary(
hidden_states_after_attn,
attn_residual,
attn_post,
attn_comb,
self.hc_ffn_fn,
self.hc_ffn_scale,
self.hc_ffn_base,
self.hc_mult,
self.rms_norm_eps,
self.hc_eps,
_MHC_POST_MULT_VALUE,
self.hc_sinkhorn_iters,
post_attn_norm_weight,
self.post_attention_layernorm.variance_epsilon,
fn_transpose=True,
)
if fused is not None:
ffn_residual, hidden_states, post, comb, norm_fused = fused
if not norm_fused:
# The Triton fused post+pre skips the post-attention
# layernorm (norm_fused=False); apply it before the MoE,
# matching the unfused hc_pre path below.
hidden_states = self.post_attention_layernorm(hidden_states)
state.ffn_residual = ffn_residual
state.ffn_post = post
state.ffn_comb = comb
state.hidden_states_mlp_input = hidden_states
return
hidden_states = self.hc_post(
state.pop("hidden_states_after_attn"),
state.pop("attn_residual"),
state.pop("attn_post"),
state.pop("attn_comb"),
hidden_states_after_attn,
attn_residual,
attn_post,
attn_comb,
)
ffn_residual = hidden_states
hidden_states, post, comb, norm_fused = self.hc_pre(
@@ -2630,7 +2723,7 @@ class DeepseekV4Model(nn.Module):
) = make_hc_head_params(hc_mult, config.hidden_size)
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.use_fused_mhc_post_pre = _is_fused_mhc_post_pre_enabled()
self.use_fused_mhc_post_pre = is_cross_layer_mhc_fusion_enabled()
if self.dsa_enable_prefill_cp:
self.cp_size = get_parallel().attn_cp_size
@@ -0,0 +1,400 @@
import unittest
from unittest import mock
from sglang.srt.environ import envs
from sglang.srt.models.deepseek_common.amd import deepseek_v4_fused_mhc
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=4, suite="base-a-test-cpu")
class TestAmdFusedMhcCrossLayerGating(unittest.TestCase):
"""Gating and dispatch-preference tests (CPU, no kernels required)."""
def test_tilelang_fuse_flag_enables_cross_layer_fusion(self):
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(True),
):
self.assertTrue(deepseek_v4_fused_mhc.is_cross_layer_mhc_fusion_enabled())
@mock.patch.object(deepseek_v4_fused_mhc, "is_sm120_supported", return_value=True)
def test_sm120_enables_fusion_with_tilelang_pre_disabled(self, _mock_sm120):
# Regression (PR review): consolidating _is_fused_mhc_post_pre_enabled into
# this module must preserve the SM120 special case. SM120 disables the
# standalone TileLang pre path, but mhc_fused_post_pre dispatches
# independently, so fuse+post enabled with the pre flag OFF must still
# enable fusion when SM120 is supported. The pre-fix consolidation
# required the pre flag unconditionally and silently disabled fusion on
# SM120.
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(False),
):
self.assertTrue(deepseek_v4_fused_mhc._is_fused_mhc_post_pre_enabled())
@mock.patch.object(deepseek_v4_fused_mhc, "is_sm120_supported", return_value=False)
def test_no_sm120_still_requires_tilelang_pre(self, _mock_sm120):
# Negative branch: the (pre OR sm120) clause must not degrade to
# always-true. With SM120 unsupported and the pre flag off, fuse+post
# alone must not enable the standalone TileLang fused path.
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(True),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(False),
):
self.assertFalse(deepseek_v4_fused_mhc._is_fused_mhc_post_pre_enabled())
def test_is_fused_mhc_post_pre_enabled_policy(self):
# Full gating table for _is_fused_mhc_post_pre_enabled, migrated from the
# removed test_deepseek_v4_fused_mhc_policy.py now that the helper lives
# in this module (it used to patch deepseek_v4.is_sm120_supported /
# deepseek_v4._is_fused_mhc_post_pre_enabled, both gone after the
# consolidation -> the registered CPU test AttributeError'd). Fusion
# requires the opt-in flag AND TileLang post AND (TileLang pre OR SM120).
cases = [
# (fuse, pre, post, sm120, expected)
(True, False, True, True, True), # SM120 waives the standalone pre flag
(True, False, True, False, False), # non-SM120 still needs the pre flag
(True, True, True, False, True), # non-SM120 with the pre flag on
(False, False, True, True, False), # fusion opt-in is required
(True, False, False, True, False), # TileLang post is required
]
for fuse, pre, post, sm120, expected in cases:
with self.subTest(fuse=fuse, pre=pre, post=post, sm120=sm120):
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(fuse),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(pre),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(post),
mock.patch.object(
deepseek_v4_fused_mhc,
"is_sm120_supported",
return_value=sm120,
),
):
self.assertEqual(
deepseek_v4_fused_mhc._is_fused_mhc_post_pre_enabled(),
expected,
)
@mock.patch.object(deepseek_v4_fused_mhc, "is_gfx95_supported", return_value=True)
@mock.patch.object(deepseek_v4_fused_mhc, "get_bool_env_var", return_value=True)
@mock.patch.object(deepseek_v4_fused_mhc, "_is_hip", True)
def test_aiter_gfx95_enables_cross_layer_fusion(self, _mock_aiter, _mock_gfx95):
# TileLang flags off: fusion must still enable via the aiter gfx95 path.
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(False),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(False),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(False),
):
self.assertTrue(deepseek_v4_fused_mhc.is_cross_layer_mhc_fusion_enabled())
@mock.patch.object(deepseek_v4_fused_mhc, "is_gfx95_supported", return_value=False)
@mock.patch.object(deepseek_v4_fused_mhc, "get_bool_env_var", return_value=True)
@mock.patch.object(deepseek_v4_fused_mhc, "_is_hip", True)
def test_aiter_cross_layer_disabled_without_gfx95(self, _mock_aiter, _mock_gfx95):
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(False),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(False),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(False),
):
self.assertFalse(deepseek_v4_fused_mhc.is_cross_layer_mhc_fusion_enabled())
@mock.patch.object(deepseek_v4_fused_mhc, "is_gfx95_supported", return_value=True)
@mock.patch.object(deepseek_v4_fused_mhc, "get_bool_env_var", return_value=False)
@mock.patch.object(deepseek_v4_fused_mhc, "_is_hip", True)
def test_aiter_path_skips_without_sglang_use_aiter(self, _mock_aiter, _mock_gfx95):
result = deepseek_v4_fused_mhc.try_aiter_fused_mhc_post_pre(
layer_input=mock.Mock(shape=(32, 7168), dim=2, device="cpu"),
residual=mock.Mock(dim=3),
post=mock.Mock(),
comb=mock.Mock(),
hc_fn=mock.Mock(),
hc_scale=mock.Mock(),
hc_base=mock.Mock(),
rms_eps=1e-6,
hc_eps=1e-6,
hc_post_mult=2.0,
sinkhorn_iters=20,
norm_weight=mock.Mock(),
norm_eps=1e-6,
)
self.assertIsNone(result)
@mock.patch.object(
deepseek_v4_fused_mhc,
"try_aiter_fused_mhc_post_pre",
return_value=("res", "hs", "post", "comb", True),
)
@mock.patch.object(deepseek_v4_fused_mhc, "try_fused_hc_post_pre")
def test_boundary_prefers_aiter_over_triton(self, mock_triton, mock_aiter):
result = deepseek_v4_fused_mhc.try_mhc_fused_post_pre_boundary(
layer_input=mock.Mock(shape=(32, 7168), dim=2),
residual=mock.Mock(dim=3),
post=mock.Mock(),
comb=mock.Mock(),
hc_fn=mock.Mock(),
hc_scale=mock.Mock(),
hc_base=mock.Mock(),
hc_mult=4,
rms_eps=1e-6,
hc_eps=1e-6,
hc_post_mult=2.0,
sinkhorn_iters=20,
norm_weight=mock.Mock(),
norm_eps=1e-6,
fn_transpose=True,
is_gfx95_supported_flag=True,
)
self.assertEqual(result, ("res", "hs", "post", "comb", True))
mock_triton.assert_not_called()
mock_aiter.assert_called_once()
@mock.patch.object(
deepseek_v4_fused_mhc, "try_aiter_fused_mhc_post_pre", return_value=None
)
@mock.patch.object(
deepseek_v4_fused_mhc,
"try_fused_hc_post_pre",
return_value=("res", "hs", "post", "comb", False),
)
def test_boundary_falls_back_to_triton(self, mock_triton, mock_aiter):
hc_fn = mock.Mock()
hc_fn.T = "transposed_fn"
result = deepseek_v4_fused_mhc.try_mhc_fused_post_pre_boundary(
layer_input=mock.Mock(shape=(32, 7168), dim=2),
residual=mock.Mock(dim=3),
post=mock.Mock(),
comb=mock.Mock(),
hc_fn=hc_fn,
hc_scale=mock.Mock(),
hc_base=mock.Mock(),
hc_mult=4,
rms_eps=1e-6,
hc_eps=1e-6,
hc_post_mult=2.0,
sinkhorn_iters=20,
norm_weight=mock.Mock(),
norm_eps=1e-6,
fn_transpose=True,
is_gfx95_supported_flag=True,
)
self.assertEqual(result, ("res", "hs", "post", "comb", False))
mock_aiter.assert_called_once()
# fn_transpose=True must hand the Triton kernel the transposed fn.
self.assertEqual(mock_triton.call_args.args[4], "transposed_fn")
class TestAmdFusedMhcAttnBoundaryFallback(unittest.TestCase):
"""Regression: the attn-side boundary fallback must close the previous
layer's deferred mHC post before opening the current layer's pre.
When ``apply_mhc_post_pre_boundary`` declines to fuse (returns ``None``) --
reachable after an aiter import/kernel failure permanently disables the fused
path -- the fallback must call
``hc_post(hidden_states, prev_residual, prev_post, prev_comb)`` before
``hc_pre``. The pre-fix code ran ``hc_pre`` directly on the raw input and
dropped ``prev_residual``/``prev_post``/``prev_comb``, so the previous
layer's deferred post was never applied and every subsequent layer computed
on corrupted activations.
Drives the real ``DeepseekV4DecoderLayer.forward`` on a mocked layer with the
fused dispatcher forced to ``None`` and halts at ``self_attn`` via a sentinel,
so only the boundary fallback executes.
"""
def test_fallback_closes_previous_post_before_pre(self):
try:
from sglang.srt.models.deepseek_v4 import DeepseekV4DecoderLayer
except Exception as e: # pragma: no cover - env without full model deps
self.skipTest(f"deepseek_v4 import unavailable: {e}")
class _StopForward(Exception):
pass
layer = mock.Mock()
layer.use_fused_mhc_post_pre = True
layer._input_layernorm_weight_bf16 = None
closed_post = object()
layer.hc_post.return_value = closed_post
# norm_fused=True keeps the fallback off the fp8-quant / layernorm branch.
layer.hc_pre.return_value = (object(), object(), object(), True)
layer.self_attn.maybe_use_decode_attn_tp.side_effect = _StopForward
hs_in = object()
prev_residual, prev_post, prev_comb = object(), object(), object()
with (
mock.patch(
"sglang.srt.models.deepseek_v4.apply_mhc_post_pre_boundary",
return_value=None,
),
self.assertRaises(_StopForward),
):
DeepseekV4DecoderLayer.forward(
layer,
positions=object(),
hidden_states=hs_in,
input_ids=object(),
forward_batch=object(),
input_ids_global=object(),
prev_residual=prev_residual,
prev_post=prev_post,
prev_comb=prev_comb,
)
# The deferred previous-layer post must be closed with exactly the
# prev_* tensors, and hc_pre must then run on the closed result.
layer.hc_post.assert_called_once_with(
hs_in, prev_residual, prev_post, prev_comb
)
layer.hc_pre.assert_called_once()
self.assertIs(layer.hc_pre.call_args.args[0], closed_post)
class TestAmdFusedMhcNormFusedHandling(unittest.TestCase):
"""Regression: a fused-success result with ``norm_fused=False`` must have its
layernorm applied at the call site before the activation reaches attention.
``try_fused_hc_post_pre`` (the Triton fused post+pre) always returns
``norm_fused=False`` -- it does not apply the input/post-attention layernorm.
The boundary dispatcher reaches it whenever the aiter kernel declines
(notably after an aiter import/kernel failure permanently disables the aiter
path). The pre-fix fused-success branch unpacked the tuple and fed the raw
(unnormalized) hidden_states straight into ``self_attn`` with ``x_quant=None``,
silently corrupting every subsequent layer. The fix mirrors the unfused
``hc_pre`` branch: apply the input layernorm when ``norm_fused`` is False.
Drives the real ``DeepseekV4DecoderLayer.forward`` with the boundary forced to
return ``norm_fused=False`` and halts at ``self_attn`` via a sentinel.
"""
def test_fused_success_applies_input_layernorm_when_not_norm_fused(self):
try:
import sglang.srt.models.deepseek_v4 as deepseek_v4
from sglang.srt.models.deepseek_v4 import DeepseekV4DecoderLayer
except Exception as e: # pragma: no cover - env without full model deps
self.skipTest(f"deepseek_v4 import unavailable: {e}")
class _StopForward(Exception):
pass
layer = mock.Mock()
layer.use_fused_mhc_post_pre = True
layer._input_layernorm_weight_bf16 = None
fused_hs = object()
residual, post, comb = object(), object(), object()
# Fused dispatch SUCCEEDS but reports the input layernorm was NOT applied
# (norm_fused=False) -- the Triton fused post+pre contract.
normed = object()
layer.input_layernorm.return_value = normed
layer.self_attn.maybe_use_decode_attn_tp.side_effect = _StopForward
# Force the non-aiter (torch layernorm) branch deterministically so the
# test does not depend on the runner arch and needs no real tensors.
with (
mock.patch.object(deepseek_v4, "_use_aiter", False),
mock.patch.object(deepseek_v4, "_is_gfx95_supported", False),
mock.patch(
"sglang.srt.models.deepseek_v4.apply_mhc_post_pre_boundary",
return_value=(residual, fused_hs, post, comb, False),
),
self.assertRaises(_StopForward),
):
DeepseekV4DecoderLayer.forward(
layer,
positions=object(),
hidden_states=object(),
input_ids=object(),
forward_batch=object(),
input_ids_global=object(),
prev_residual=object(),
prev_post=object(),
prev_comb=object(),
)
# The fused (unnormalized) layer input must be run through the input
# layernorm before attention. Pre-fix this was never called on the
# fused-success path.
layer.input_layernorm.assert_called_once_with(fused_hs)
def _hardware_available() -> bool:
try:
import torch
if not (
torch.cuda.is_available() and deepseek_v4_fused_mhc.is_gfx95_supported()
):
return False
from aiter.ops.mhc import mhc_fused_post_pre # noqa: F401
return True
except Exception:
return False
@unittest.skipUnless(
_hardware_available(), "requires a gfx95 device with aiter mHC kernels"
)
class TestAmdFusedMhcNumerical(unittest.TestCase):
"""On-device equivalence of the aiter fused kernel vs unfused mhc_post+mhc_pre.
Asserts the proven invariants: ``next_residual`` is bit-exact and
``layer_input``/``post_mix`` match within bf16 tolerance. ``comb_mix`` is
intentionally not asserted here -- its raw-tensor value differs between the
fused and unfused kernels at the production Sinkhorn setting, and correctness
is established end-to-end (fused-on vs fused-off token match). See the PR
description; extend this test once the end-to-end sign-off pins the expected
comb_mix convention.
"""
def _run(self, m, hc_mult=4, hidden=7168, sinkhorn_iters=20):
import torch
from aiter.ops import mhc
dev = "cuda:0"
torch.manual_seed(0)
hc_mult3 = hc_mult * 2 + hc_mult * hc_mult
li = (torch.randn(m, hidden, device=dev) * 0.02).bfloat16()
res = (torch.randn(m, hc_mult, hidden, device=dev) * 0.02).bfloat16()
post = torch.randn(m, hc_mult, device=dev) * 0.02
comb = torch.randn(m, hc_mult, hc_mult, device=dev) * 0.02
fn = (torch.randn(hc_mult3, hc_mult * hidden, device=dev) * 0.02).bfloat16()
scl = torch.ones(hc_mult3, device=dev)
base = torch.zeros(hc_mult3, device=dev)
nw = torch.ones(hidden, device=dev).bfloat16()
kw = dict(
rms_eps=1e-6,
hc_pre_eps=1e-6,
hc_sinkhorn_eps=1e-6,
hc_post_mult_value=2.0,
sinkhorn_repeat=sinkhorn_iters,
norm_weight=nw,
norm_eps=1e-6,
)
post_mix, _comb_mix, li_out, next_res = mhc.mhc_fused_post_pre(
li, res, post, comb, fn, scl, base, force_fused=True, **kw
)
ref_next = torch.empty_like(res)
mhc.mhc_post(ref_next, li, res, post, comb)
ref_post, _ref_comb, ref_li = mhc.mhc_pre(ref_next, fn, scl, base, **kw)
torch.testing.assert_close(next_res, ref_next, rtol=0, atol=0)
torch.testing.assert_close(li_out.float(), ref_li.float(), rtol=3e-2, atol=3e-2)
torch.testing.assert_close(
post_mix.float(), ref_post.float(), rtol=3e-2, atol=3e-2
)
def test_equivalence_decode(self):
self._run(m=32)
def test_equivalence_prefill(self):
self._run(m=96)
if __name__ == "__main__":
unittest.main()
@@ -1,79 +0,0 @@
"""Unit tests for the DeepSeek-V4 fused-MHC enable policy."""
import unittest
from unittest.mock import patch
import sglang.srt.models.deepseek_v4 as deepseek_v4
from sglang.srt.environ import envs
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
class TestDeepseekV4FusedMHCPolicy(CustomTestCase):
def _is_enabled(
self,
*,
fuse: bool,
tilelang_pre: bool,
tilelang_post: bool,
sm120: bool,
) -> bool:
with (
envs.SGLANG_OPT_FUSE_MHC_POST_PRE.override(fuse),
envs.SGLANG_OPT_USE_TILELANG_MHC_PRE.override(tilelang_pre),
envs.SGLANG_OPT_USE_TILELANG_MHC_POST.override(tilelang_post),
patch.object(deepseek_v4, "is_sm120_supported", return_value=sm120),
):
return deepseek_v4._is_fused_mhc_post_pre_enabled()
def test_sm120_allows_fused_opt_in_with_standalone_pre_disabled(self):
self.assertTrue(
self._is_enabled(
fuse=True,
tilelang_pre=False,
tilelang_post=True,
sm120=True,
)
)
def test_other_platform_still_requires_tilelang_pre(self):
self.assertFalse(
self._is_enabled(
fuse=True,
tilelang_pre=False,
tilelang_post=True,
sm120=False,
)
)
self.assertTrue(
self._is_enabled(
fuse=True,
tilelang_pre=True,
tilelang_post=True,
sm120=False,
)
)
def test_fusion_opt_in_and_tilelang_post_remain_required(self):
self.assertFalse(
self._is_enabled(
fuse=False,
tilelang_pre=False,
tilelang_post=True,
sm120=True,
)
)
self.assertFalse(
self._is_enabled(
fuse=True,
tilelang_pre=False,
tilelang_post=False,
sm120=True,
)
)
if __name__ == "__main__":
unittest.main()