[ROCm][DSV4.1][Perf] Fold the mHC post step into the delayed pre projection (#56513)

Signed-off-by: Fangzhou Ai <fangzhou@semianalysis.ai>
Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Fangzhou Ai
2026-09-14 16:44:56 -05:00
committed by GitHub
co-authored by Cursor
parent bdad63c90a
commit 00972dfd72
5 changed files with 346 additions and 64 deletions
+76 -2
View File
@@ -983,8 +983,12 @@ def test_mhc_pre_delayed_rocm_aiter(num_tokens, carried):
)
expected = mhc_pre_delayed_torch(*args, pre_mix=pre_mix)
actual = object.__new__(MHCPreDelayedOp).forward_hip(*args, pre_mix=pre_mix)
residual_out, *actual = object.__new__(MHCPreDelayedOp).forward_hip(
*args, pre_mix=pre_mix
)
# No post was requested, so the residual comes straight back.
assert residual_out is residual
for i in (0, 1, 3):
torch.testing.assert_close(actual[i], expected[i], atol=1e-4, rtol=1e-3)
# The collapse is the same FP32 multiply-and-sum in both paths.
@@ -1026,7 +1030,12 @@ def test_mhc_pre_delayed_rocm_aiter_declines_unsupported(monkeypatch):
broadcast_fn = fn.view(-1, hc_mult, hidden_size).sum(1)
broadcast_residual = x.unsqueeze(1).expand(-1, hc_mult, -1).contiguous()
expected = mhc_pre_delayed_torch(broadcast_residual, broadcast_fn, *args[2:], x=x)
actual = op.forward_hip(broadcast_residual, broadcast_fn, *args[2:], x=x)
# forward_hip leads with the residual; no post was requested here, so it is
# the caller's own tensor and the reference's four outputs follow it.
residual_out, *actual = op.forward_hip(
broadcast_residual, broadcast_fn, *args[2:], x=x
)
assert residual_out is broadcast_residual
assert not took_aiter, "the broadcast seam must not reach AITER"
for i in range(4):
torch.testing.assert_close(actual[i], expected[i], atol=1e-4, rtol=1e-3)
@@ -1268,3 +1277,68 @@ def test_deepseek_v4_mhc_broadcast_refit_refreshes_in_place(monkeypatch):
assert layer.hc_attn_fn_broadcast is buffer
expected = layer.hc_attn_fn.detach().view(-1, 2, 8).sum(dim=1)
assert torch.equal(layer.hc_attn_fn_broadcast, expected)
@pytest.mark.skipif(
not (current_platform.is_rocm() and HAS_AITER_MHC),
reason="AITER mHC required",
)
@pytest.mark.parametrize("num_tokens", [1, 2, 7, 128])
@pytest.mark.parametrize("carried", [False, True])
def test_mhc_fused_post_pre_delayed_rocm_aiter(
num_tokens, carried, default_vllm_config
):
"""Folding the post into the pre projection must match doing them apart.
The residual is bfloat16, so the two accumulation orders are allowed to
land a rounding step apart; the tolerance here is one bfloat16 ULP.
"""
set_random_seed(0)
hc_mult, hidden_size = 4, 5120
residual, fn, hc_scale, hc_base, _ = _rocm_mhc_inputs(
num_tokens=num_tokens, hidden_size=hidden_size, hc_mult=hc_mult
)
x = torch.randn((num_tokens, hidden_size), dtype=torch.bfloat16, device=DEVICE)
post_layer_mix = (
torch.rand(num_tokens, hc_mult, 1, dtype=torch.float32, device=DEVICE) + 0.5
)
comb_res_mix = (
torch.rand(num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=DEVICE)
+ 0.2
)
pre_mix = (
torch.rand(num_tokens, hc_mult, dtype=torch.float32, device=DEVICE) + 0.5
if carried
else None
)
rms_eps = hc_pre_eps = hc_sinkhorn_eps = 1e-6
pre_args = (fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, 1.0, 20)
next_residual = mhc_post_torch(x, residual, post_layer_mix, comb_res_mix)
expected = (
next_residual,
*mhc_pre_delayed_torch(next_residual, *pre_args, pre_mix=pre_mix),
)
actual = MHCPreDelayedOp().forward_hip(
residual,
*pre_args,
pre_mix=pre_mix,
sublayer_out=x,
post_layer_mix=post_layer_mix,
comb_res_mix=comb_res_mix,
)
for got, want in zip(actual, expected, strict=True):
torch.testing.assert_close(got, want.reshape(got.shape), atol=2e-2, rtol=8e-3)
@pytest.mark.skipif(
not (current_platform.is_rocm() and HAS_AITER_MHC),
reason="AITER mHC required",
)
def test_mhc_fused_post_pre_delayed_falls_back_for_large_batches():
"""AITER's heuristic hands large batches back to the unfused path."""
from vllm._aiter_ops import rocm_aiter_ops
assert not rocm_aiter_ops.mhc_fused_post_pre_delayed_prefers_unfused(1)
assert rocm_aiter_ops.mhc_fused_post_pre_delayed_prefers_unfused(1 << 20)
+138 -37
View File
@@ -1676,6 +1676,75 @@ def _rocm_aiter_fp8_attn_fake(
)
def _mhc_delayed_pre_tail(
gemm_out: torch.Tensor,
sqrsum: torch.Tensor,
residual: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
pre_mix: torch.Tensor | None,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Finish a delayed mHC pre once AITER has produced the projection.
Shared by the plain and the post-fused entry points, which differ only in
which kernel computes ``gemm_out`` / ``sqrsum`` and the residual they
project. ``mhc_pre_big_fuse`` also writes a collapse against its own
pre-mix, which the delayed formulation cannot use and so discards.
Returns ``(post_mix, comb_mix, layer_input, next_pre_mix)``.
"""
from aiter.ops.mhc import mhc_pre_big_fuse
num_tokens, hc_mult, hidden_size = residual.shape
device = residual.device
with torch.device(device):
post_mix = torch.empty(
num_tokens, hc_mult, 1, dtype=torch.float32, device=device
)
comb_mix = torch.empty(
num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=device
)
unused_collapse = torch.empty(
num_tokens, hidden_size, dtype=torch.bfloat16, device=device
)
mhc_pre_big_fuse(
post_mix,
comb_mix,
unused_collapse,
gemm_out,
sqrsum,
hc_scale,
hc_base,
residual,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
)
next_pre_mix = torch.ops.vllm.mhc_pre_mix_triton(
gemm_out,
sqrsum,
hc_scale,
hc_base,
hc_mult,
hc_mult * hidden_size,
rms_eps,
hc_pre_eps,
)
if pre_mix is None:
# Model entry selects residual stream zero.
layer_input = residual[:, 0]
else:
layer_input = torch.ops.vllm.hc_collapse_triton(residual, pre_mix)
return post_mix, comb_mix, layer_input, next_pre_mix
# Global flag to ensure ops are registered only once
_OPS_REGISTERED = False
@@ -3611,6 +3680,10 @@ class rocm_aiter_ops:
hc_post_mult_value: float,
sinkhorn_repeat: int,
pre_mix: torch.Tensor | None = None,
sublayer_out: torch.Tensor | None = None,
post_layer_mix: torch.Tensor | None = None,
comb_res_mix: torch.Tensor | None = None,
residual_out: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""mHC pre using the pre-mix carried from the previous sublayer.
@@ -3625,6 +3698,15 @@ class rocm_aiter_ops:
that redundant store is the price of not having a native delayed
kernel, and is small next to the ~140 launches it replaces.
Passing ``sublayer_out`` / ``post_layer_mix`` / ``comb_res_mix``
also applies the preceding post block. AITER then projects the new
residual in the same kernel that computes it
(``mhc_fused_post_pre_gemm_sqrsum``), which saves one launch; every
later stage is identical either way. That new residual is written to
``residual_out``, which the caller must supply in that case; it is an
output buffer rather than a return value so the op never has to hand
back one of its own inputs on the unfused path.
Returns:
post_mix: shape (..., hc_mult, 1), dtype torch.float32
comb_mix: shape (..., hc_mult, hc_mult), dtype torch.float32
@@ -3632,8 +3714,9 @@ class rocm_aiter_ops:
next_pre_mix: shape (..., hc_mult), dtype torch.float32
"""
from aiter.ops.mhc import (
get_mhc_fused_post_pre_config,
get_mhc_pre_splitk,
mhc_pre_big_fuse,
mhc_fused_post_pre_gemm_sqrsum,
mhc_pre_gemm_sqrsum,
)
@@ -3666,9 +3749,16 @@ class rocm_aiter_ops:
pre_mix_flat = None if pre_mix is None else pre_mix.view(-1, hc_mult)
fuse_post = sublayer_out is not None
# AITER's Python wrappers allocate without explicit device arguments.
with torch.device(device):
splitk, tile_k = get_mhc_pre_splitk(num_tokens, hc_hidden_size)
if fuse_post:
# Keyed on the per-stream K, i.e. hidden_size, not hc_mult * it.
splitk, tile_m, tile_n, tile_k = get_mhc_fused_post_pre_config(
num_tokens, hidden_size
)
else:
splitk, tile_k = get_mhc_pre_splitk(num_tokens, hc_hidden_size)
# AITER pads the GEMM output to a multiple of 32 columns.
gemm_pad = torch.empty(
splitk,
@@ -3679,50 +3769,46 @@ class rocm_aiter_ops:
)
gemm_out = gemm_pad[:, :, :hc_mult3]
sqrsum = torch.empty(splitk, num_tokens, dtype=torch.float32, device=device)
mhc_pre_gemm_sqrsum(gemm_out, sqrsum, residual_flat, fn, tile_k, 0)
if fuse_post:
# Restated for the type checker, which cannot narrow the
# optional arguments through the fuse_post flag.
assert sublayer_out is not None
assert post_layer_mix is not None and comb_res_mix is not None
assert residual_out is not None
projected = residual_out.view(-1, hc_mult, hidden_size)
mhc_fused_post_pre_gemm_sqrsum(
gemm_out,
sqrsum,
projected,
sublayer_out.view(-1, hidden_size),
residual_flat,
# The kernel wants the post mix squeezed to (tokens, hc_mult).
post_layer_mix.view(-1, hc_mult, 1).squeeze(-1),
comb_res_mix.view(-1, hc_mult, hc_mult),
fn,
tile_m,
tile_n,
tile_k,
0,
)
else:
projected = residual_flat
mhc_pre_gemm_sqrsum(gemm_out, sqrsum, projected, fn, tile_k, 0)
post_mix = torch.empty(
num_tokens, hc_mult, 1, dtype=torch.float32, device=device
)
comb_mix = torch.empty(
num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=device
)
unused_collapse = torch.empty(
num_tokens, hidden_size, dtype=torch.bfloat16, device=device
)
mhc_pre_big_fuse(
post_mix,
comb_mix,
unused_collapse,
gemm_out,
sqrsum,
hc_scale,
hc_base,
residual_flat,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
)
next_pre_mix = torch.ops.vllm.mhc_pre_mix_triton(
post_mix, comb_mix, layer_input, next_pre_mix = _mhc_delayed_pre_tail(
gemm_out,
sqrsum,
projected,
hc_scale,
hc_base,
hc_mult,
hc_hidden_size,
pre_mix_flat,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
)
if pre_mix_flat is None:
# Model entry selects residual stream zero.
layer_input = residual_flat[:, 0]
else:
layer_input = torch.ops.vllm.hc_collapse_triton(residual_flat, pre_mix_flat)
return (
post_mix.view(*outer_shape, hc_mult, 1),
comb_mix.view(*outer_shape, hc_mult, hc_mult),
@@ -3730,6 +3816,21 @@ class rocm_aiter_ops:
next_pre_mix.view(*outer_shape, hc_mult),
)
@staticmethod
def mhc_fused_post_pre_delayed_prefers_unfused(num_tokens: int) -> bool:
"""True when AITER's own heuristic favours separate post and pre.
Mirrors the ``fused_m_upper_bound`` table in ``aiter.ops.mhc``: the
fused GEMM wins for decode-sized batches and loses for large ones.
"""
from aiter.jit.utils.chip_info import get_gfx_runtime
return num_tokens >= {
"gfx950": 1024,
"gfx942": 128,
"gfx1250": 1024,
}.get(get_gfx_runtime(), 1024)
@staticmethod
def hc_head(
hs_flat: torch.Tensor,
+20 -1
View File
@@ -115,6 +115,10 @@ def mhc_pre_delayed_aiter(
hc_post_mult_value: float,
sinkhorn_repeat: int,
pre_mix: torch.Tensor | None = None,
sublayer_out: torch.Tensor | None = None,
post_layer_mix: torch.Tensor | None = None,
comb_res_mix: torch.Tensor | None = None,
residual_out: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""mHC pre with the pre-mix carried in from the previous sublayer.
@@ -134,6 +138,13 @@ def mhc_pre_delayed_aiter(
sinkhorn_repeat: number of sinkhorn iterations
pre_mix: shape (..., hc_mult) from the previous sublayer, or None at
model entry to select residual stream zero.
sublayer_out: attention or FFN output, shape (..., hidden_size). When
given with the two mixes below, the preceding post block is
applied here so AITER can fold it into the pre projection.
post_layer_mix: shape (..., hc_mult, 1), post gate for that block.
comb_res_mix: shape (..., hc_mult, hc_mult), residual comb for it.
residual_out: shape (..., hc_mult, hidden_size), written with the post
block's new residual. Required exactly when the post is folded in.
Returns:
post_mix: shape (..., hc_mult, 1), dtype torch.float32
@@ -156,6 +167,10 @@ def mhc_pre_delayed_aiter(
hc_post_mult_value,
sinkhorn_repeat,
pre_mix,
sublayer_out,
post_layer_mix,
comb_res_mix,
residual_out,
)
@@ -170,6 +185,10 @@ def _mhc_pre_delayed_aiter_fake(
hc_post_mult_value: float,
sinkhorn_repeat: int,
pre_mix: torch.Tensor | None = None,
sublayer_out: torch.Tensor | None = None,
post_layer_mix: torch.Tensor | None = None,
comb_res_mix: torch.Tensor | None = None,
residual_out: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
hc_mult = residual.shape[-2]
hidden_size = residual.shape[-1]
@@ -313,7 +332,7 @@ direct_register_custom_op(
direct_register_custom_op(
op_name="mhc_pre_delayed_aiter",
op_func=mhc_pre_delayed_aiter,
mutates_args=[],
mutates_args=["residual_out"],
fake_impl=_mhc_pre_delayed_aiter_fake,
)
direct_register_custom_op(
+75 -7
View File
@@ -299,7 +299,13 @@ class MHCPreDelayedOp(CustomOp):
Same gates as :class:`MHCPreOp`, but the stream collapse applies the
caller's ``pre_mix`` and this sublayer's pre-mix is returned for the next
sublayer seam. Returns post_mix, comb_mix, layer_input, next_pre_mix.
sublayer seam.
Passing ``sublayer_out`` / ``post_layer_mix`` / ``comb_res_mix`` also
applies the preceding post block, which lets AITER fold it into the pre
projection and saves a launch. Returns residual, post_mix, comb_mix,
layer_input, next_pre_mix; ``residual`` is the caller's own tensor when
no post is requested.
"""
# --8<-- [end:mhc_pre_delayed]
@@ -307,6 +313,24 @@ class MHCPreDelayedOp(CustomOp):
def enabled(cls) -> bool:
return True
def __init__(self) -> None:
super().__init__()
# Built here, not lazily: CustomOp resolves dispatch against the
# current vLLM config, which is only set during model construction.
self._post = MHCPostOp()
def _maybe_post(
self,
residual: torch.Tensor,
sublayer_out: torch.Tensor | None,
post_layer_mix: torch.Tensor | None,
comb_res_mix: torch.Tensor | None,
) -> torch.Tensor:
if sublayer_out is None:
return residual
assert post_layer_mix is not None and comb_res_mix is not None
return self._post(sublayer_out, residual, post_layer_mix, comb_res_mix)
def forward_cuda(
self,
residual: torch.Tensor,
@@ -322,8 +346,14 @@ class MHCPreDelayedOp(CustomOp):
x: torch.Tensor | None = None,
norm_weight: torch.Tensor | None = None,
norm_eps: float = 1e-6,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
return torch.ops.vllm.mhc_pre_delayed_tilelang(
sublayer_out: torch.Tensor | None = None,
post_layer_mix: torch.Tensor | None = None,
comb_res_mix: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
residual = self._maybe_post(
residual, sublayer_out, post_layer_mix, comb_res_mix
)
return residual, *torch.ops.vllm.mhc_pre_delayed_tilelang(
residual,
fn,
hc_scale,
@@ -354,7 +384,10 @@ class MHCPreDelayedOp(CustomOp):
x: torch.Tensor | None = None,
norm_weight: torch.Tensor | None = None,
norm_eps: float = 1e-6,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
sublayer_out: torch.Tensor | None = None,
post_layer_mix: torch.Tensor | None = None,
comb_res_mix: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
# The AITER delayed path drives mhc_pre_gemm_sqrsum against `residual`
# and folds no RMSNorm, so it cannot serve the model-entry broadcast
# (which projects a narrower `x`) or a fused norm. Both are handled by
@@ -364,7 +397,26 @@ class MHCPreDelayedOp(CustomOp):
and norm_weight is None
and _aiter_mhc_supported(residual, None, supports_norm=False)
):
return torch.ops.vllm.mhc_pre_delayed_aiter(
from vllm._aiter_ops import rocm_aiter_ops
num_tokens = residual.numel() // (residual.shape[-1] * residual.shape[-2])
# Folding the post in only pays while the residual still fits in
# cache; past that AITER's heuristic wants the separate kernels,
# which can ask for a non-temporal store and a large-m split-k.
if sublayer_out is not None and (
rocm_aiter_ops.mhc_fused_post_pre_delayed_prefers_unfused(num_tokens)
):
residual = self._maybe_post(
residual, sublayer_out, post_layer_mix, comb_res_mix
)
sublayer_out = None
# The op writes the folded post's residual into this buffer rather
# than returning it, since on the unfused path it has none of its
# own and an op must not hand back one of its inputs.
residual_out = (
torch.empty_like(residual) if sublayer_out is not None else None
)
rest = torch.ops.vllm.mhc_pre_delayed_aiter(
residual,
fn,
hc_scale,
@@ -375,9 +427,17 @@ class MHCPreDelayedOp(CustomOp):
hc_post_mult_value,
sinkhorn_repeat,
pre_mix,
sublayer_out,
post_layer_mix,
comb_res_mix,
residual_out,
)
return (residual if residual_out is None else residual_out), *rest
residual = self._maybe_post(
residual, sublayer_out, post_layer_mix, comb_res_mix
)
if HAS_TILELANG_MHC:
return torch.ops.vllm.mhc_pre_delayed_tilelang(
return residual, *torch.ops.vllm.mhc_pre_delayed_tilelang(
residual,
fn,
hc_scale,
@@ -392,6 +452,7 @@ class MHCPreDelayedOp(CustomOp):
norm_weight,
norm_eps,
)
# The post is already applied, so it must not be requested again.
return self.forward_native(
residual,
fn,
@@ -423,7 +484,13 @@ class MHCPreDelayedOp(CustomOp):
x: torch.Tensor | None = None,
norm_weight: torch.Tensor | None = None,
norm_eps: float = 1e-6,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
sublayer_out: torch.Tensor | None = None,
post_layer_mix: torch.Tensor | None = None,
comb_res_mix: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
residual = self._maybe_post(
residual, sublayer_out, post_layer_mix, comb_res_mix
)
post_mix, comb_mix, layer_input, next_pre_mix = (
mhc_kernels.mhc_pre_delayed_torch(
residual,
@@ -440,6 +507,7 @@ class MHCPreDelayedOp(CustomOp):
)
)
return (
residual,
post_mix,
comb_mix,
_apply_mhc_norm(layer_input, norm_weight, norm_eps),
+37 -17
View File
@@ -274,7 +274,7 @@ class DeepseekV4DecoderLayer(nn.Module):
# copies and the identity pre-mix selects copy 0.
assert self.hc_attn_fn_broadcast is not None
residual = x.unsqueeze(1).expand(-1, self.hc_mult, -1).contiguous()
post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
residual, post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
residual,
self.hc_attn_fn_broadcast,
self.hc_attn_scale,
@@ -288,7 +288,7 @@ class DeepseekV4DecoderLayer(nn.Module):
)
else:
residual = x
post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
residual, post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
residual,
self.hc_attn_fn,
self.hc_attn_scale,
@@ -301,18 +301,7 @@ class DeepseekV4DecoderLayer(nn.Module):
pre_mix=pre_mix,
)
else:
residual = self.mhc_post(x, residual, post_mix, res_mix)
if self.engram is not None and engram_hashes is not None:
# Engram injection happens between the previous sublayer's
# post and this block's pre, on the full hc stream, so the
# mix coefficients see the injected stream.
residual = self.engram(
residual,
engram_hashes[:, self.engram.layer_hash_index],
engram_mask,
)
post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
residual,
pre_args = (
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
@@ -321,8 +310,37 @@ class DeepseekV4DecoderLayer(nn.Module):
self.hc_eps,
self.hc_post_alpha,
self.hc_sinkhorn_iters,
pre_mix=pre_mix,
)
if self.engram is not None and engram_hashes is not None:
# Engram injection happens between the previous sublayer's
# post and this block's pre, on the full hc stream, so the
# mix coefficients see the injected stream. That read of the
# residual is what stops the post from fusing into the pre
# here, unlike the attention seam below.
residual = self.mhc_post(x, residual, post_mix, res_mix)
residual = self.engram(
residual,
engram_hashes[:, self.engram.layer_hash_index],
engram_mask,
)
residual, post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
residual, *pre_args, pre_mix=pre_mix
)
else:
(
residual,
post_mix,
res_mix,
x,
attn_pre,
) = self.mhc_pre_delayed(
residual,
*pre_args,
pre_mix=pre_mix,
sublayer_out=x,
post_layer_mix=post_mix,
comb_res_mix=res_mix,
)
x = self.attn_norm(x)
if self.use_sequence_parallel:
@@ -332,8 +350,7 @@ class DeepseekV4DecoderLayer(nn.Module):
if self.use_sequence_parallel:
x = sp_reduce_scatter(x)
residual = self.mhc_post(x, residual, post_mix, res_mix)
post_mix, res_mix, x, ffn_pre = self.mhc_pre_delayed(
residual, post_mix, res_mix, x, ffn_pre = self.mhc_pre_delayed(
residual,
self.hc_ffn_fn,
self.hc_ffn_scale,
@@ -344,6 +361,9 @@ class DeepseekV4DecoderLayer(nn.Module):
self.hc_post_alpha,
self.hc_sinkhorn_iters,
pre_mix=attn_pre,
sublayer_out=x,
post_layer_mix=post_mix,
comb_res_mix=res_mix,
)
x = self.ffn_norm(x)
x = self.ffn(x, input_ids)