[ROCm][DSV4.1][Perf] Use AITER mHC for the delayed pre block (#56503)

Signed-off-by: Fangzhou Ai <fangzhou@semianalysis.ai> Co-authored-by: Fangzhou Ai <fangzhou@semianalysis.ai>

Fangzhou Ai committed Sep 11, 2026 at 20:35 UTC 46d2b23ac5047a813ebb082122166e4ae09b5f39
7 files changed +620 -19
tests/kernels/test_mhc_kernels.py
+95
@@ -23,6 +23,7 @@ from vllm.model_executor.layers.mhc import (
23 HAS_AITER_MHC_PRE_NORM,
24 HAS_TILELANG_MHC,
25 MHCFusedPostPreOp,
26 + MHCPreDelayedOp,
27 MHCPreOp,
28 )
29 from vllm.models.deepseek_v4.nvidia.model import (
@@ -697,6 +698,100 @@ def test_mhc_fused_rocm_fallback_applies_norm(monkeypatch):
698 torch.testing.assert_close(out[3], expected_layer_input)
699
700
701 +@pytest.mark.skipif(
702 + not (current_platform.is_rocm() and HAS_AITER_MHC),
703 + reason="AITER mHC required",
704 +)
705 +@pytest.mark.parametrize("num_tokens", [1, 2, 7, 128, 1024])
706 +@pytest.mark.parametrize("carried", [False, True])
707 +def test_mhc_pre_delayed_rocm_aiter(num_tokens, carried):
708 + """AITER must reproduce the delayed reference on both seam variants.
709 +
710 + ``num_tokens`` spans the split-k choices AITER makes for the projection,
711 + since the pre-mix is recovered from that unreduced output.
712 + """
713 + set_random_seed(0)
714 + hc_mult, hidden_size = 4, 5120
715 + residual, fn, hc_scale, hc_base, _ = _rocm_mhc_inputs(
716 + num_tokens=num_tokens, hidden_size=hidden_size, hc_mult=hc_mult
717 + )
718 + pre_mix = (
719 + torch.rand(num_tokens, hc_mult, dtype=torch.float32, device=DEVICE) + 0.5
720 + if carried
721 + else None
722 + )
723 + rms_eps = hc_pre_eps = hc_sinkhorn_eps = 1e-6
724 + args = (
725 + residual,
726 + fn,
727 + hc_scale,
728 + hc_base,
729 + rms_eps,
730 + hc_pre_eps,
731 + hc_sinkhorn_eps,
732 + 1.0,
733 + 20,
734 + )
735 +
736 + expected = mhc_pre_delayed_torch(*args, pre_mix=pre_mix)
737 + actual = object.__new__(MHCPreDelayedOp).forward_hip(*args, pre_mix=pre_mix)
738 +
739 + for i in (0, 1, 3):
740 + torch.testing.assert_close(actual[i], expected[i], atol=1e-4, rtol=1e-3)
741 + # The collapse is the same FP32 multiply-and-sum in both paths.
742 + torch.testing.assert_close(actual[2], expected[2], atol=0, rtol=0)
743 +
744 +
745 +@pytest.mark.skipif(
746 + not (current_platform.is_rocm() and HAS_AITER_MHC),
747 + reason="AITER mHC required",
748 +)
749 +def test_mhc_pre_delayed_rocm_aiter_declines_unsupported(monkeypatch):
750 + """The broadcast seam and a fused norm must not take the AITER path.
751 +
752 + Neither is expressible with AITER's pre kernels: the broadcast projects a
753 + narrower ``x``, and ``mhc_pre_gemm_sqrsum`` folds no RMSNorm. What is
754 + asserted here is the routing decision, which is what this gate owns; the
755 + numerics of whichever fallback it lands on are covered by
756 + ``test_deepseek_v41_mhc_pre_delayed``.
757 + """
758 + set_random_seed(0)
759 + hc_mult, hidden_size = 4, 5120
760 + residual, fn, hc_scale, hc_base, norm_weight = _rocm_mhc_inputs(
761 + num_tokens=4, hidden_size=hidden_size, hc_mult=hc_mult
762 + )
763 + args = (residual, fn, hc_scale, hc_base, 1e-6, 1e-6, 1e-6, 1.0, 20)
764 + op = object.__new__(MHCPreDelayedOp)
765 +
766 + aiter_op = torch.ops.vllm.mhc_pre_delayed_aiter
767 + took_aiter = False
768 +
769 + def spy(*spy_args, **spy_kwargs):
770 + nonlocal took_aiter
771 + took_aiter = True
772 + return aiter_op(*spy_args, **spy_kwargs)
773 +
774 + monkeypatch.setattr(torch.ops.vllm, "mhc_pre_delayed_aiter", spy)
775 +
776 + x = residual[:, 0].contiguous()
777 + broadcast_fn = fn.view(-1, hc_mult, hidden_size).sum(1)
778 + broadcast_residual = x.unsqueeze(1).expand(-1, hc_mult, -1).contiguous()
779 + expected = mhc_pre_delayed_torch(broadcast_residual, broadcast_fn, *args[2:], x=x)
780 + actual = op.forward_hip(broadcast_residual, broadcast_fn, *args[2:], x=x)
781 + assert not took_aiter, "the broadcast seam must not reach AITER"
782 + for i in range(4):
783 + torch.testing.assert_close(actual[i], expected[i], atol=1e-4, rtol=1e-3)
784 +
785 + op.forward_hip(*args, norm_weight=norm_weight, norm_eps=1e-6)
786 + assert not took_aiter, "a fused norm must not reach AITER"
787 +
788 + # Positive control: the same inputs without a norm do take the AITER path,
789 + # so the two declines above are the gate discriminating rather than the
790 + # path being unavailable in this environment.
791 + op.forward_hip(*args)
792 + assert took_aiter
793 +
794 +
795 @pytest.mark.skipif(
796 not (current_platform.is_rocm() and HAS_AITER_MHC and HAS_AITER_MHC_PRE_NORM),
797 reason="AITER mHC with fused RMSNorm required",
vllm/_aiter_ops.py
+131
@@ -3599,6 +3599,137 @@ class rocm_aiter_ops:
3599 layer_input.view(*outer_shape, hidden_size),
3600 )
3601
3602 + @staticmethod
3603 + def mhc_pre_delayed(
3604 + residual: torch.Tensor,
3605 + fn: torch.Tensor,
3606 + hc_scale: torch.Tensor,
3607 + hc_base: torch.Tensor,
3608 + rms_eps: float,
3609 + hc_pre_eps: float,
3610 + hc_sinkhorn_eps: float,
3611 + hc_post_mult_value: float,
3612 + sinkhorn_repeat: int,
3613 + pre_mix: torch.Tensor | None = None,
3614 + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
3615 + """mHC pre using the pre-mix carried from the previous sublayer.
3616 +
3617 + Same gates as :meth:`mhc_pre`, but the stream collapse applies the
3618 + caller's ``pre_mix`` instead of the one computed here, and the one
3619 + computed here is returned for the next sublayer seam. AITER has no
3620 + single kernel for that shape, so drive its two stages directly:
3621 + ``mhc_pre_big_fuse`` still produces the post and comb gates (including
3622 + every sinkhorn iteration), while the pre gate is recovered from the
3623 + same split-k GEMM output and the collapse is done by the Triton
3624 + kernel. ``mhc_pre_big_fuse`` also writes a collapse we do not use;
3625 + that redundant store is the price of not having a native delayed
3626 + kernel, and is small next to the ~140 launches it replaces.
3627 +
3628 + Returns:
3629 + post_mix: shape (..., hc_mult, 1), dtype torch.float32
3630 + comb_mix: shape (..., hc_mult, hc_mult), dtype torch.float32
3631 + layer_input: shape (..., hidden_size), dtype torch.bfloat16
3632 + next_pre_mix: shape (..., hc_mult), dtype torch.float32
3633 + """
3634 + from aiter.ops.mhc import (
3635 + get_mhc_pre_splitk,
3636 + mhc_pre_big_fuse,
3637 + mhc_pre_gemm_sqrsum,
3638 + )
3639 +
3640 + assert residual.dtype == torch.bfloat16
3641 + assert fn.dtype == torch.float32
3642 + assert hc_scale.dtype == torch.float32
3643 + assert hc_base.dtype == torch.float32
3644 +
3645 + hc_mult = residual.shape[-2]
3646 + hidden_size = residual.shape[-1]
3647 + hc_mult3 = hc_mult * 2 + hc_mult * hc_mult
3648 + hc_hidden_size = hc_mult * hidden_size
3649 +
3650 + assert fn.shape == (hc_mult3, hc_hidden_size)
3651 + assert hc_scale.shape == (3,)
3652 + assert hc_base.shape == (hc_mult3,)
3653 +
3654 + outer_shape = residual.shape[:-2]
3655 + residual_flat = residual.view(-1, hc_mult, hidden_size)
3656 + num_tokens = residual_flat.shape[0]
3657 + device = residual_flat.device
3658 +
3659 + if num_tokens == 0:
3660 + return (
3661 + torch.empty(0, hc_mult, 1, dtype=torch.float32, device=device),
3662 + torch.empty(0, hc_mult, hc_mult, dtype=torch.float32, device=device),
3663 + torch.empty(0, hidden_size, dtype=torch.bfloat16, device=device),
3664 + torch.empty(0, hc_mult, dtype=torch.float32, device=device),
3665 + )
3666 +
3667 + pre_mix_flat = None if pre_mix is None else pre_mix.view(-1, hc_mult)
3668 +
3669 + # AITER's Python wrappers allocate without explicit device arguments.
3670 + with torch.device(device):
3671 + splitk, tile_k = get_mhc_pre_splitk(num_tokens, hc_hidden_size)
3672 + # AITER pads the GEMM output to a multiple of 32 columns.
3673 + gemm_pad = torch.empty(
3674 + splitk,
3675 + num_tokens,
3676 + (hc_mult3 + 31) // 32 * 32,
3677 + dtype=torch.float32,
3678 + device=device,
3679 + )
3680 + gemm_out = gemm_pad[:, :, :hc_mult3]
3681 + sqrsum = torch.empty(splitk, num_tokens, dtype=torch.float32, device=device)
3682 + mhc_pre_gemm_sqrsum(gemm_out, sqrsum, residual_flat, fn, tile_k, 0)
3683 +
3684 + post_mix = torch.empty(
3685 + num_tokens, hc_mult, 1, dtype=torch.float32, device=device
3686 + )
3687 + comb_mix = torch.empty(
3688 + num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=device
3689 + )
3690 + unused_collapse = torch.empty(
3691 + num_tokens, hidden_size, dtype=torch.bfloat16, device=device
3692 + )
3693 + mhc_pre_big_fuse(
3694 + post_mix,
3695 + comb_mix,
3696 + unused_collapse,
3697 + gemm_out,
3698 + sqrsum,
3699 + hc_scale,
3700 + hc_base,
3701 + residual_flat,
3702 + rms_eps,
3703 + hc_pre_eps,
3704 + hc_sinkhorn_eps,
3705 + hc_post_mult_value,
3706 + sinkhorn_repeat,
3707 + )
3708 +
3709 + next_pre_mix = torch.ops.vllm.mhc_pre_mix_triton(
3710 + gemm_out,
3711 + sqrsum,
3712 + hc_scale,
3713 + hc_base,
3714 + hc_mult,
3715 + hc_hidden_size,
3716 + rms_eps,
3717 + hc_pre_eps,
3718 + )
3719 +
3720 + if pre_mix_flat is None:
3721 + # Model entry selects residual stream zero.
3722 + layer_input = residual_flat[:, 0]
3723 + else:
3724 + layer_input = torch.ops.vllm.hc_collapse_triton(residual_flat, pre_mix_flat)
3725 +
3726 + return (
3727 + post_mix.view(*outer_shape, hc_mult, 1),
3728 + comb_mix.view(*outer_shape, hc_mult, hc_mult),
3729 + layer_input.view(*outer_shape, hidden_size),
3730 + next_pre_mix.view(*outer_shape, hc_mult),
3731 + )
3732 +
3733 @staticmethod
3734 def hc_head(
3735 hs_flat: torch.Tensor,
vllm/model_executor/kernels/mhc/__init__.py
+2
@@ -12,6 +12,7 @@ __all__ = [
12 "mhc_fused_post_pre_cuda",
13 "hc_head_fused_kernel_cuda",
14 "mhc_pre_aiter",
15 + "mhc_pre_delayed_aiter",
16 "mhc_post_aiter",
17 "mhc_fused_post_pre_aiter",
18 "hc_head_fused_aiter",
@@ -26,6 +27,7 @@ __all__ = [
27 "mhc_post_torch",
28 "mhc_fused_post_pre_torch",
29 "hc_head_fused_torch",
30 + "mhc_pre_mix_triton",
31 "mhc_pre_triton",
32 "mhc_post_triton",
33 "mhc_fused_post_pre_triton",
vllm/model_executor/kernels/mhc/aiter.py
+90
@@ -104,6 +104,90 @@ def _mhc_pre_aiter_fake(
104 return post_mix, comb_mix, layer_input
105
106
107 +def mhc_pre_delayed_aiter(
108 + residual: torch.Tensor,
109 + fn: torch.Tensor,
110 + hc_scale: torch.Tensor,
111 + hc_base: torch.Tensor,
112 + rms_eps: float,
113 + hc_pre_eps: float,
114 + hc_sinkhorn_eps: float,
115 + hc_post_mult_value: float,
116 + sinkhorn_repeat: int,
117 + pre_mix: torch.Tensor | None = None,
118 +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
119 + """mHC pre with the pre-mix carried in from the previous sublayer.
120 +
121 + Matches ``mhc_pre_delayed_torch``: the stream collapse uses *pre_mix*
122 + rather than the gate computed here, and that gate is returned as the
123 + pre-mix for the next sublayer seam.
124 +
125 + Args:
126 + residual: shape (..., hc_mult, hidden_size), dtype torch.bfloat16
127 + fn: shape (hc_mult3, hc_mult * hidden_size), dtype torch.float32
128 + hc_scale: shape (3,), dtype torch.float32
129 + hc_base: shape (hc_mult3,), dtype torch.float32
130 + rms_eps: RMS normalization epsilon
131 + hc_pre_eps: pre-mix epsilon
132 + hc_sinkhorn_eps: sinkhorn epsilon
133 + hc_post_mult_value: post-mix multiplier value
134 + sinkhorn_repeat: number of sinkhorn iterations
135 + pre_mix: shape (..., hc_mult) from the previous sublayer, or None at
136 + model entry to select residual stream zero.
137 +
138 + Returns:
139 + post_mix: shape (..., hc_mult, 1), dtype torch.float32
140 + comb_mix: shape (..., hc_mult, hc_mult), dtype torch.float32
141 + layer_input: shape (..., hidden_size), dtype torch.bfloat16
142 + next_pre_mix: shape (..., hc_mult), dtype torch.float32
143 + """
144 + hidden_size = residual.shape[-1]
145 + assert hidden_size % 256 == 0
146 + from vllm._aiter_ops import rocm_aiter_ops
147 +
148 + return rocm_aiter_ops.mhc_pre_delayed(
149 + residual,
150 + fn,
151 + hc_scale,
152 + hc_base,
153 + rms_eps,
154 + hc_pre_eps,
155 + hc_sinkhorn_eps,
156 + hc_post_mult_value,
157 + sinkhorn_repeat,
158 + pre_mix,
159 + )
160 +
161 +
162 +def _mhc_pre_delayed_aiter_fake(
163 + residual: torch.Tensor,
164 + fn: torch.Tensor,
165 + hc_scale: torch.Tensor,
166 + hc_base: torch.Tensor,
167 + rms_eps: float,
168 + hc_pre_eps: float,
169 + hc_sinkhorn_eps: float,
170 + hc_post_mult_value: float,
171 + sinkhorn_repeat: int,
172 + pre_mix: torch.Tensor | None = None,
173 +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
174 + hc_mult = residual.shape[-2]
175 + hidden_size = residual.shape[-1]
176 + outer_shape = residual.shape[:-2]
177 + return (
178 + torch.empty(
179 + *outer_shape, hc_mult, 1, dtype=torch.float32, device=residual.device
180 + ),
181 + torch.empty(
182 + *outer_shape, hc_mult, hc_mult, dtype=torch.float32, device=residual.device
183 + ),
184 + torch.empty(
185 + *outer_shape, hidden_size, dtype=torch.bfloat16, device=residual.device
186 + ),
187 + torch.empty(*outer_shape, hc_mult, dtype=torch.float32, device=residual.device),
188 + )
189 +
190 +
191 def mhc_post_aiter(
192 x: torch.Tensor,
193 residual: torch.Tensor,
@@ -226,6 +310,12 @@ direct_register_custom_op(
310 mutates_args=[],
311 fake_impl=_mhc_pre_aiter_fake,
312 )
313 +direct_register_custom_op(
314 + op_name="mhc_pre_delayed_aiter",
315 + op_func=mhc_pre_delayed_aiter,
316 + mutates_args=[],
317 + fake_impl=_mhc_pre_delayed_aiter_fake,
318 +)
319 direct_register_custom_op(
320 op_name="mhc_post_aiter",
321 op_func=mhc_post_aiter,
vllm/model_executor/kernels/mhc/triton.py
+131
@@ -141,6 +141,129 @@ def _hc_collapse_triton_fake(x: Tensor, pre_mix: Tensor) -> Tensor:
141 return torch.empty(x.shape[0], x.shape[2], dtype=x.dtype, device=x.device)
142
143
144 +@triton.jit
145 +def _mhc_pre_mix_kernel(
146 + gemm_ptr,
147 + sqrsum_ptr,
148 + hc_scale_ptr,
149 + hc_base_ptr,
150 + out_ptr,
151 + splitk,
152 + hc_mult: tl.constexpr,
153 + gemm_stride_k,
154 + gemm_stride_t,
155 + gemm_stride_j,
156 + sqrsum_stride_k,
157 + sqrsum_stride_t,
158 + out_stride_t,
159 + out_stride_j,
160 + inv_hc_hidden,
161 + rms_eps,
162 + hc_pre_eps,
163 + SPLITK_BLOCK: tl.constexpr,
164 + HC_BLOCK: tl.constexpr,
165 +):
166 + """Recover the pre-mix gate from a split-k mHC pre GEMM output."""
167 + token_idx = tl.program_id(0).to(tl.int64)
168 + ks = tl.arange(0, SPLITK_BLOCK)
169 + js = tl.arange(0, HC_BLOCK)
170 + kmask = ks < splitk
171 + jmask = js < hc_mult
172 +
173 + # Only the first hc_mult GEMM columns feed the pre gate.
174 + gemm = tl.load(
175 + gemm_ptr
176 + + ks[:, None] * gemm_stride_k
177 + + token_idx * gemm_stride_t
178 + + js[None, :] * gemm_stride_j,
179 + mask=kmask[:, None] & jmask[None, :],
180 + other=0.0,
181 + ).to(tl.float32)
182 + mixes = tl.sum(gemm, 0)
183 +
184 + sqrsum = tl.load(
185 + sqrsum_ptr + ks * sqrsum_stride_k + token_idx * sqrsum_stride_t,
186 + mask=kmask,
187 + other=0.0,
188 + ).to(tl.float32)
189 + rstd = tl.rsqrt(tl.sum(sqrsum, 0) * inv_hc_hidden + rms_eps)
190 +
191 + scale = tl.load(hc_scale_ptr).to(tl.float32)
192 + base = tl.load(hc_base_ptr + js, mask=jmask, other=0.0).to(tl.float32)
193 +
194 + pre = tl.sigmoid(mixes * rstd * scale + base) + hc_pre_eps
195 + tl.store(out_ptr + token_idx * out_stride_t + js * out_stride_j, pre, mask=jmask)
196 +
197 +
198 +def mhc_pre_mix_triton(
199 + gemm_out: Tensor,
200 + sqrsum: Tensor,
201 + hc_scale: Tensor,
202 + hc_base: Tensor,
203 + hc_mult: int,
204 + hc_hidden_size: int,
205 + rms_eps: float,
206 + hc_pre_eps: float,
207 +) -> Tensor:
208 + """Pre-mix gate for the delayed mHC pre, from AITER's split-k GEMM output.
209 +
210 + AITER's ``mhc_pre_big_fuse`` consumes the unreduced ``[splitk, tokens,
211 + hc_mult3]`` GEMM output and the matching row square-sums, but only returns
212 + the post and comb gates. The delayed formulation also needs the pre gate,
213 + to carry into the next sublayer seam. It is the same slice of the same
214 + numbers, so recover it here rather than repeating the projection.
215 + """
216 + assert gemm_out.ndim == 3 and gemm_out.dtype == torch.float32
217 + assert sqrsum.ndim == 2 and sqrsum.dtype == torch.float32
218 + splitk, num_tokens = gemm_out.shape[0], gemm_out.shape[1]
219 + assert sqrsum.shape == (splitk, num_tokens)
220 +
221 + out = torch.empty(num_tokens, hc_mult, dtype=torch.float32, device=gemm_out.device)
222 + if num_tokens == 0:
223 + return out
224 +
225 + _mhc_pre_mix_kernel[(num_tokens,)](
226 + gemm_out,
227 + sqrsum,
228 + hc_scale,
229 + hc_base,
230 + out,
231 + splitk,
232 + hc_mult,
233 + gemm_out.stride(0),
234 + gemm_out.stride(1),
235 + gemm_out.stride(2),
236 + sqrsum.stride(0),
237 + sqrsum.stride(1),
238 + out.stride(0),
239 + out.stride(1),
240 + 1.0 / hc_hidden_size,
241 + rms_eps,
242 + hc_pre_eps,
243 + SPLITK_BLOCK=triton.next_power_of_2(splitk),
244 + HC_BLOCK=triton.next_power_of_2(hc_mult),
245 + num_warps=1,
246 + # Match the separate FP32 multiply and add in the Torch reference.
247 + enable_fp_fusion=False,
248 + )
249 + return out
250 +
251 +
252 +def _mhc_pre_mix_triton_fake(
253 + gemm_out: Tensor,
254 + sqrsum: Tensor,
255 + hc_scale: Tensor,
256 + hc_base: Tensor,
257 + hc_mult: int,
258 + hc_hidden_size: int,
259 + rms_eps: float,
260 + hc_pre_eps: float,
261 +) -> Tensor:
262 + return torch.empty(
263 + gemm_out.shape[1], hc_mult, dtype=torch.float32, device=gemm_out.device
264 + )
265 +
266 +
267 def hc_head_reduce_triton_kernel(
268 x: torch.Tensor,
269 hc_fn: torch.Tensor,
@@ -216,3 +339,11 @@ direct_register_custom_op(
339 mutates_args=[],
340 fake_impl=_hc_collapse_triton_fake,
341 )
342 +
343 +
344 +direct_register_custom_op(
345 + op_name="mhc_pre_mix_triton",
346 + op_func=mhc_pre_mix_triton,
347 + mutates_args=[],
348 + fake_impl=_mhc_pre_mix_triton_fake,
349 +)
vllm/model_executor/layers/mhc.py
+155
@@ -292,6 +292,161 @@ class MHCPreOp(CustomOp):
292 )
293
294
295 +# --8<-- [start:mhc_pre_delayed]
296 +@CustomOp.register("mhc_pre_delayed")
297 +class MHCPreDelayedOp(CustomOp):
298 + """MHC pre block using the pre-mix carried from the previous sublayer.
299 +
300 + Same gates as :class:`MHCPreOp`, but the stream collapse applies the
301 + caller's ``pre_mix`` and this sublayer's pre-mix is returned for the next
302 + sublayer seam. Returns post_mix, comb_mix, layer_input, next_pre_mix.
303 + """
304 +
305 + # --8<-- [end:mhc_pre_delayed]
306 + @classmethod
307 + def enabled(cls) -> bool:
308 + return True
309 +
310 + def forward_cuda(
311 + self,
312 + residual: torch.Tensor,
313 + fn: torch.Tensor,
314 + hc_scale: torch.Tensor,
315 + hc_base: torch.Tensor,
316 + rms_eps: float,
317 + hc_pre_eps: float,
318 + hc_sinkhorn_eps: float,
319 + hc_post_mult_value: float,
320 + sinkhorn_repeat: int,
321 + pre_mix: torch.Tensor | None = None,
322 + x: torch.Tensor | None = None,
323 + norm_weight: torch.Tensor | None = None,
324 + norm_eps: float = 1e-6,
325 + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
326 + return torch.ops.vllm.mhc_pre_delayed_tilelang(
327 + residual,
328 + fn,
329 + hc_scale,
330 + hc_base,
331 + rms_eps,
332 + hc_pre_eps,
333 + hc_sinkhorn_eps,
334 + hc_post_mult_value,
335 + sinkhorn_repeat,
336 + pre_mix,
337 + x,
338 + norm_weight,
339 + norm_eps,
340 + )
341 +
342 + def forward_hip(
343 + self,
344 + residual: torch.Tensor,
345 + fn: torch.Tensor,
346 + hc_scale: torch.Tensor,
347 + hc_base: torch.Tensor,
348 + rms_eps: float,
349 + hc_pre_eps: float,
350 + hc_sinkhorn_eps: float,
351 + hc_post_mult_value: float,
352 + sinkhorn_repeat: int,
353 + pre_mix: torch.Tensor | None = None,
354 + x: torch.Tensor | None = None,
355 + norm_weight: torch.Tensor | None = None,
356 + norm_eps: float = 1e-6,
357 + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
358 + # The AITER delayed path drives mhc_pre_gemm_sqrsum against `residual`
359 + # and folds no RMSNorm, so it cannot serve the model-entry broadcast
360 + # (which projects a narrower `x`) or a fused norm. Both are handled by
361 + # TileLang, or by the reference when TileLang is unavailable.
362 + if (
363 + x is None
364 + and norm_weight is None
365 + and _aiter_mhc_supported(residual, None, supports_norm=False)
366 + ):
367 + return torch.ops.vllm.mhc_pre_delayed_aiter(
368 + residual,
369 + fn,
370 + hc_scale,
371 + hc_base,
372 + rms_eps,
373 + hc_pre_eps,
374 + hc_sinkhorn_eps,
375 + hc_post_mult_value,
376 + sinkhorn_repeat,
377 + pre_mix,
378 + )
379 + if HAS_TILELANG_MHC:
380 + return torch.ops.vllm.mhc_pre_delayed_tilelang(
381 + residual,
382 + fn,
383 + hc_scale,
384 + hc_base,
385 + rms_eps,
386 + hc_pre_eps,
387 + hc_sinkhorn_eps,
388 + hc_post_mult_value,
389 + sinkhorn_repeat,
390 + pre_mix,
391 + x,
392 + norm_weight,
393 + norm_eps,
394 + )
395 + return self.forward_native(
396 + residual,
397 + fn,
398 + hc_scale,
399 + hc_base,
400 + rms_eps,
401 + hc_pre_eps,
402 + hc_sinkhorn_eps,
403 + hc_post_mult_value,
404 + sinkhorn_repeat,
405 + pre_mix,
406 + x,
407 + norm_weight,
408 + norm_eps,
409 + )
410 +
411 + def forward_native(
412 + self,
413 + residual: torch.Tensor,
414 + fn: torch.Tensor,
415 + hc_scale: torch.Tensor,
416 + hc_base: torch.Tensor,
417 + rms_eps: float,
418 + hc_pre_eps: float,
419 + hc_sinkhorn_eps: float,
420 + hc_post_mult_value: float,
421 + sinkhorn_repeat: int,
422 + pre_mix: torch.Tensor | None = None,
423 + x: torch.Tensor | None = None,
424 + norm_weight: torch.Tensor | None = None,
425 + norm_eps: float = 1e-6,
426 + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
427 + post_mix, comb_mix, layer_input, next_pre_mix = (
428 + mhc_kernels.mhc_pre_delayed_torch(
429 + residual,
430 + fn,
431 + hc_scale,
432 + hc_base,
433 + rms_eps,
434 + hc_pre_eps,
435 + hc_sinkhorn_eps,
436 + hc_post_mult_value,
437 + sinkhorn_repeat,
438 + pre_mix=pre_mix,
439 + x=x,
440 + )
441 + )
442 + return (
443 + post_mix,
444 + comb_mix,
445 + _apply_mhc_norm(layer_input, norm_weight, norm_eps),
446 + next_pre_mix,
447 + )
448 +
449 +
450 # --8<-- [start:mhc_post]
451 @CustomOp.register("mhc_post")
452 class MHCPostOp(CustomOp):
vllm/models/deepseek_v4_1/amd/model.py
+16 -19
@@ -17,15 +17,12 @@ from vllm.distributed import (
17 )
18 from vllm.forward_context import get_forward_context, is_forward_context_available
19 from vllm.logger import init_logger
20 -from vllm.model_executor.kernels.mhc.torch import (
21 - mhc_post_torch,
22 - mhc_pre_delayed_torch,
23 -)
20 from vllm.model_executor.layers.fused_moe import (
21 fused_moe_make_expert_params_mapping,
22 )
23 from vllm.model_executor.layers.layernorm import RMSNorm
24 from vllm.model_executor.layers.logits_processor import LogitsProcessor
25 +from vllm.model_executor.layers.mhc import MHCPostOp, MHCPreDelayedOp
26 from vllm.model_executor.layers.quantization import QuantizationConfig
27 from vllm.model_executor.layers.vocab_parallel_embedding import (
28 ParallelLMHead,
@@ -248,11 +245,13 @@ class DeepseekV4DecoderLayer(nn.Module):
245 ),
246 requires_grad=False,
247 )
248 + self.mhc_pre_delayed = MHCPreDelayedOp()
249 + self.mhc_post = MHCPostOp()
250
251 @staticmethod
252 def _hc_collapse(x: torch.Tensor, pre_mix: torch.Tensor) -> torch.Tensor:
254 - """Reference hyper-connection collapse used by DSpark on ROCm."""
255 - return (pre_mix.unsqueeze(-1) * x.float()).sum(dim=1).to(x.dtype)
253 + """Hyper-connection collapse used by DSpark on ROCm."""
254 + return torch.ops.vllm.hc_collapse_triton(x, pre_mix)
255
256 def forward(
257 self,
@@ -275,7 +274,7 @@ class DeepseekV4DecoderLayer(nn.Module):
274 # copies and the identity pre-mix selects copy 0.
275 assert self.hc_attn_fn_broadcast is not None
276 residual = x.unsqueeze(1).expand(-1, self.hc_mult, -1).contiguous()
278 - post_mix, res_mix, x, attn_pre = mhc_pre_delayed_torch(
277 + post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
278 residual,
279 self.hc_attn_fn_broadcast,
280 self.hc_attn_scale,
@@ -289,7 +288,7 @@ class DeepseekV4DecoderLayer(nn.Module):
288 )
289 else:
290 residual = x
292 - post_mix, res_mix, x, attn_pre = mhc_pre_delayed_torch(
291 + post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
292 residual,
293 self.hc_attn_fn,
294 self.hc_attn_scale,
@@ -302,7 +301,7 @@ class DeepseekV4DecoderLayer(nn.Module):
301 pre_mix=pre_mix,
302 )
303 else:
305 - residual = mhc_post_torch(x, residual, post_mix, res_mix)
304 + residual = self.mhc_post(x, residual, post_mix, res_mix)
305 if self.engram is not None and engram_hashes is not None:
306 # Engram injection happens between the previous sublayer's
307 # post and this block's pre, on the full hc stream, so the
@@ -312,7 +311,7 @@ class DeepseekV4DecoderLayer(nn.Module):
311 engram_hashes[:, self.engram.layer_hash_index],
312 engram_mask,
313 )
315 - post_mix, res_mix, x, attn_pre = mhc_pre_delayed_torch(
314 + post_mix, res_mix, x, attn_pre = self.mhc_pre_delayed(
315 residual,
316 self.hc_attn_fn,
317 self.hc_attn_scale,
@@ -333,8 +332,8 @@ class DeepseekV4DecoderLayer(nn.Module):
332 if self.use_sequence_parallel:
333 x = sp_reduce_scatter(x)
334
336 - residual = mhc_post_torch(x, residual, post_mix, res_mix)
337 - post_mix, res_mix, x, ffn_pre = mhc_pre_delayed_torch(
335 + residual = self.mhc_post(x, residual, post_mix, res_mix)
336 + post_mix, res_mix, x, ffn_pre = self.mhc_pre_delayed(
337 residual,
338 self.hc_ffn_fn,
339 self.hc_ffn_scale,
@@ -453,6 +452,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
452 else:
453 self.norm = PPMissingLayer()
454
455 + self.mhc_post = MHCPostOp()
456 +
457 spec_config = vllm_config.speculative_config
458 needs_mtp_hidden_states = spec_config is not None and (
459 spec_config.use_eagle() or spec_config.uses_draft_model()
@@ -603,7 +604,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
604 )
605 if idx + 1 in self.aux_hidden_state_layers:
606 # Reconstruct the aux hidden state for draft models
606 - aux_recon = mhc_post_torch(hidden_states, residual, post_mix, res_mix)
607 + aux_recon = self.mhc_post(hidden_states, residual, post_mix, res_mix)
608 aux_hidden_state = aux_recon.mean(dim=1)
609 if self.use_sequence_parallel:
610 aux_hidden_state = sp_all_gather(aux_hidden_state)[:full_num_tokens]
@@ -614,7 +615,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
615 if self.end_layer in self.aux_hidden_state_layers:
616 hidden_states = final_aux_recon
617 else:
617 - hidden_states = mhc_post_torch(
618 + hidden_states = self.mhc_post(
619 hidden_states, residual, post_mix, res_mix
620 )
621
@@ -635,11 +636,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
636 # mixes — the mix the reference applies via
637 # ``last_layer.hc_pre(h, pre_mix)`` (v4.1 has no learned hc_head).
638 assert pre_mix is not None
638 - hidden_states = (
639 - (pre_mix.unsqueeze(-1) * hidden_states.float())
640 - .sum(dim=1)
641 - .to(hidden_states.dtype)
642 - )
639 + hidden_states = torch.ops.vllm.hc_collapse_triton(hidden_states, pre_mix)
640 hidden_states = self.norm(hidden_states)
641 if len(aux_hidden_states) > 0:
642 return hidden_states, aux_hidden_states