[ROCm][Perf] Fuse DeepSeek-V4 mHC post/pre and RMSNorm with AITER (#52737)
Signed-off-by: shen-shanshan <467638484@qq.com> Signed-off-by: Shanshan Shen <87969357+shen-shanshan@users.noreply.github.com>
Shanshan Shen committed
Aug 20, 2026 at 12:51 UTC
d626108b1841888ec90aced33367149a6bbc7e4b
5 files changed
+272
-10
vllm/_aiter_ops.py
+96
@@ -3299,6 +3299,8 @@ class rocm_aiter_ops:
3299
hc_sinkhorn_eps: float,
3300
hc_post_mult_value: float,
3301
sinkhorn_repeat: int,
3302
+ norm_weight: torch.Tensor | None = None,
3303
+ norm_eps: float = 0.0,
3304
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
3305
"""
3306
Forward pass for mHC pre block.
@@ -3313,6 +3315,8 @@ class rocm_aiter_ops:
3315
hc_sinkhorn_eps: sinkhorn epsilon
3316
hc_post_mult_value: post-mix multiplier value
3317
sinkhorn_repeat: number of sinkhorn iterations
3318
+ norm_weight: optional RMSNorm weight fused into the pre kernel
3319
+ norm_eps: epsilon for the fused RMSNorm when norm_weight is set
3320
3321
Returns:
3322
post_mix: shape (..., hc_mult), dtype torch.float32
@@ -3380,6 +3384,8 @@ class rocm_aiter_ops:
3384
hc_sinkhorn_eps,
3385
hc_post_mult_value,
3386
sinkhorn_repeat,
3387
+ norm_weight,
3388
+ norm_eps,
3389
)
3390
return (
3391
post_mix.view(*outer_shape, hc_mult, 1),
@@ -3465,5 +3471,95 @@ class rocm_aiter_ops:
3471
)
3472
return out.view_as(residual)
3473
3474
+ @staticmethod
3475
+ def mhc_fused_post_pre(
3476
+ x: torch.Tensor,
3477
+ residual: torch.Tensor,
3478
+ post_layer_mix: torch.Tensor,
3479
+ comb_res_mix: torch.Tensor,
3480
+ fn: torch.Tensor,
3481
+ hc_scale: torch.Tensor,
3482
+ hc_base: torch.Tensor,
3483
+ rms_eps: float,
3484
+ hc_pre_eps: float,
3485
+ hc_sinkhorn_eps: float,
3486
+ hc_post_mult_value: float,
3487
+ sinkhorn_repeat: int,
3488
+ norm_weight: torch.Tensor | None = None,
3489
+ norm_eps: float = 0.0,
3490
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
3491
+ """Fused mHC post + next mHC pre via AITER.
3492
+
3493
+ Returns residual_cur, post_mix, comb_mix, layer_input in vLLM order
3494
+ (AITER returns post_mix, comb_mix, layer_input, next_residual).
3495
+ """
3496
+ from aiter.ops.mhc import mhc_fused_post_pre
3497
+
3498
+ assert x.dtype == torch.bfloat16
3499
+ assert residual.dtype == torch.bfloat16
3500
+ assert fn.dtype == torch.float32
3501
+ assert hc_scale.dtype == torch.float32
3502
+ assert hc_base.dtype == torch.float32
3503
+
3504
+ hc_mult = residual.shape[-2]
3505
+ hidden_size = residual.shape[-1]
3506
+ outer_shape = residual.shape[:-2]
3507
+
3508
+ residual_flat = residual.view(-1, hc_mult, hidden_size)
3509
+ num_tokens = residual_flat.shape[0]
3510
+ x_flat = x.view(num_tokens, hidden_size)
3511
+ post_flat = post_layer_mix.view(num_tokens, hc_mult, 1)
3512
+ comb_flat = comb_res_mix.view(num_tokens, hc_mult, hc_mult)
3513
+
3514
+ if num_tokens == 0:
3515
+ return (
3516
+ torch.empty_like(residual_flat).view_as(residual),
3517
+ torch.empty(
3518
+ *outer_shape,
3519
+ hc_mult,
3520
+ 1,
3521
+ dtype=torch.float32,
3522
+ device=residual.device,
3523
+ ),
3524
+ torch.empty(
3525
+ *outer_shape,
3526
+ hc_mult,
3527
+ hc_mult,
3528
+ dtype=torch.float32,
3529
+ device=residual.device,
3530
+ ),
3531
+ torch.empty(
3532
+ *outer_shape,
3533
+ hidden_size,
3534
+ dtype=torch.bfloat16,
3535
+ device=residual.device,
3536
+ ),
3537
+ )
3538
+
3539
+ with torch.device(residual_flat.device):
3540
+ post_mix, comb_mix, layer_input, next_residual = mhc_fused_post_pre(
3541
+ x_flat,
3542
+ residual_flat,
3543
+ post_flat,
3544
+ comb_flat,
3545
+ fn,
3546
+ hc_scale,
3547
+ hc_base,
3548
+ rms_eps,
3549
+ hc_pre_eps,
3550
+ hc_sinkhorn_eps,
3551
+ hc_post_mult_value,
3552
+ sinkhorn_repeat,
3553
+ norm_weight,
3554
+ norm_eps,
3555
+ )
3556
+
3557
+ return (
3558
+ next_residual.view_as(residual),
3559
+ post_mix.view(*outer_shape, hc_mult, 1),
3560
+ comb_mix.view(*outer_shape, hc_mult, hc_mult),
3561
+ layer_input.view(*outer_shape, hidden_size),
3562
+ )
3563
+
3564
3565
rocm_aiter_ops.register_ops_once()
vllm/model_executor/kernels/mhc/aiter.py
+102
@@ -16,6 +16,8 @@ def mhc_pre_aiter(
16
hc_post_mult_value: float,
17
sinkhorn_repeat: int,
18
n_splits: int = 1,
19
+ norm_weight: torch.Tensor | None = None,
20
+ norm_eps: float = 0.0,
21
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
22
"""
23
Forward pass for mHC pre block.
@@ -31,6 +33,8 @@ def mhc_pre_aiter(
33
hc_post_mult_value: post-mix multiplier value
34
sinkhorn_repeat: number of sinkhorn iterations
35
n_splits: split-k factor;
36
+ norm_weight: optional RMSNorm weight fused into the pre kernel
37
+ norm_eps: epsilon for the fused RMSNorm when norm_weight is set
38
39
Returns:
40
post_mix: shape (..., hc_mult), dtype torch.float32
@@ -52,6 +56,8 @@ def mhc_pre_aiter(
56
hc_sinkhorn_eps,
57
hc_post_mult_value,
58
sinkhorn_repeat,
59
+ norm_weight,
60
+ norm_eps,
61
)
62
63
@@ -66,6 +72,8 @@ def _mhc_pre_aiter_fake(
72
hc_post_mult_value: float,
73
sinkhorn_repeat: int,
74
n_splits: int = 1,
75
+ norm_weight: torch.Tensor | None = None,
76
+ norm_eps: float = 0.0,
77
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
78
hc_mult = residual.shape[-2]
79
hidden_size = residual.shape[-1]
@@ -124,6 +132,94 @@ def _mhc_post_aiter_fake(
132
return torch.empty_like(residual)
133
134
135
+def mhc_fused_post_pre_aiter(
136
+ x: torch.Tensor,
137
+ residual: torch.Tensor,
138
+ post_layer_mix: torch.Tensor,
139
+ comb_res_mix: torch.Tensor,
140
+ fn: torch.Tensor,
141
+ hc_scale: torch.Tensor,
142
+ hc_base: torch.Tensor,
143
+ rms_eps: float,
144
+ hc_pre_eps: float,
145
+ hc_sinkhorn_eps: float,
146
+ hc_post_mult_value: float,
147
+ sinkhorn_repeat: int,
148
+ n_splits: int = 1,
149
+ tile_n: int = 1,
150
+ norm_weight: torch.Tensor | None = None,
151
+ norm_eps: float = 0.0,
152
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
153
+ """Fused mHC post + next mHC pre on ROCm via AITER.
154
+
155
+ Returns residual_cur, post_mix_cur, comb_mix_cur, layer_input_cur.
156
+ """
157
+ hidden_size = residual.shape[-1]
158
+ assert hidden_size % 256 == 0
159
+ from vllm._aiter_ops import rocm_aiter_ops
160
+
161
+ return rocm_aiter_ops.mhc_fused_post_pre(
162
+ x,
163
+ residual,
164
+ post_layer_mix,
165
+ comb_res_mix,
166
+ fn,
167
+ hc_scale,
168
+ hc_base,
169
+ rms_eps,
170
+ hc_pre_eps,
171
+ hc_sinkhorn_eps,
172
+ hc_post_mult_value,
173
+ sinkhorn_repeat,
174
+ norm_weight,
175
+ norm_eps,
176
+ )
177
+
178
+
179
+def _mhc_fused_post_pre_aiter_fake(
180
+ x: torch.Tensor,
181
+ residual: torch.Tensor,
182
+ post_layer_mix: torch.Tensor,
183
+ comb_res_mix: torch.Tensor,
184
+ fn: torch.Tensor,
185
+ hc_scale: torch.Tensor,
186
+ hc_base: torch.Tensor,
187
+ rms_eps: float,
188
+ hc_pre_eps: float,
189
+ hc_sinkhorn_eps: float,
190
+ hc_post_mult_value: float,
191
+ sinkhorn_repeat: int,
192
+ n_splits: int = 1,
193
+ tile_n: int = 1,
194
+ norm_weight: torch.Tensor | None = None,
195
+ norm_eps: float = 0.0,
196
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
197
+ hc_mult = residual.shape[-2]
198
+ hidden_size = residual.shape[-1]
199
+ outer_shape = residual.shape[:-2]
200
+ post_mix = torch.empty(
201
+ *outer_shape,
202
+ hc_mult,
203
+ 1,
204
+ dtype=torch.float32,
205
+ device=residual.device,
206
+ )
207
+ comb_mix = torch.empty(
208
+ *outer_shape,
209
+ hc_mult,
210
+ hc_mult,
211
+ dtype=torch.float32,
212
+ device=residual.device,
213
+ )
214
+ layer_input = torch.empty(
215
+ *outer_shape,
216
+ hidden_size,
217
+ dtype=torch.bfloat16,
218
+ device=residual.device,
219
+ )
220
+ return torch.empty_like(residual), post_mix, comb_mix, layer_input
221
+
222
+
223
direct_register_custom_op(
224
op_name="mhc_pre_aiter",
225
op_func=mhc_pre_aiter,
@@ -136,3 +232,9 @@ direct_register_custom_op(
232
mutates_args=[],
233
fake_impl=_mhc_post_aiter_fake,
234
)
235
+direct_register_custom_op(
236
+ op_name="mhc_fused_post_pre_aiter",
237
+ op_func=mhc_fused_post_pre_aiter,
238
+ mutates_args=[],
239
+ fake_impl=_mhc_fused_post_pre_aiter_fake,
240
+)
vllm/model_executor/layers/mhc.py
+28
-2
@@ -90,7 +90,8 @@ class MHCPreOp(CustomOp):
90
norm_eps: float = 0.0,
91
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
92
hidden_size = residual.shape[-1]
93
- if HAS_AITER_MHC and hidden_size % 256 == 0:
93
+ hc_mult = residual.shape[-2]
94
+ if HAS_AITER_MHC and hidden_size % 256 == 0 and hc_mult == 4:
95
return torch.ops.vllm.mhc_pre_aiter(
96
residual,
97
fn,
@@ -101,6 +102,9 @@ class MHCPreOp(CustomOp):
102
hc_sinkhorn_eps,
103
hc_post_mult_value,
104
sinkhorn_repeat,
105
+ n_splits,
106
+ norm_weight,
107
+ norm_eps,
108
)
109
elif HAS_TILELANG_MHC:
110
return torch.ops.vllm.mhc_pre_tilelang(
@@ -222,7 +226,8 @@ class MHCPostOp(CustomOp):
226
comb_res_mix: torch.Tensor,
227
) -> torch.Tensor:
228
hidden_size = residual.shape[-1]
225
- if HAS_AITER_MHC and hidden_size % 256 == 0:
229
+ hc_mult = residual.shape[-2]
230
+ if HAS_AITER_MHC and hidden_size % 256 == 0 and hc_mult == 4:
231
return torch.ops.vllm.mhc_post_aiter(
232
x,
233
residual,
@@ -444,6 +449,27 @@ class MHCFusedPostPreOp(CustomOp):
449
norm_weight: torch.Tensor | None = None,
450
norm_eps: float = 0.0,
451
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
452
+ hidden_size = residual.shape[-1]
453
+ hc_mult = residual.shape[-2]
454
+ if HAS_AITER_MHC and hidden_size % 256 == 0 and hc_mult == 4:
455
+ return torch.ops.vllm.mhc_fused_post_pre_aiter(
456
+ x,
457
+ residual,
458
+ post_layer_mix,
459
+ comb_res_mix,
460
+ fn,
461
+ hc_scale,
462
+ hc_base,
463
+ rms_eps,
464
+ hc_pre_eps,
465
+ hc_sinkhorn_eps,
466
+ hc_post_mult_value,
467
+ sinkhorn_repeat,
468
+ n_splits,
469
+ tile_n,
470
+ norm_weight,
471
+ norm_eps,
472
+ )
473
if HAS_TILELANG_MHC:
474
return torch.ops.vllm.mhc_fused_post_pre_tilelang(
475
x,
vllm/models/deepseek_v4/amd/dspark.py
+2
-2
@@ -9,8 +9,8 @@ ROCm port of ``nvidia/dspark.py``. Follows the same nvidia->amd recipe used for
9
attention + MHC CustomOp path) instead of the nvidia one;
10
* route the MHC head through the ``HCHeadOp`` CustomOp dispatcher (aiter /
11
tilelang / triton / torch) instead of calling the tilelang kernels directly,
12
- and gate the trailing ``mhc_post`` on ``use_fused_mhc`` (False on the aiter
13
- path, where the decoder layer already applies hc_post in-layer);
12
+ and gate the trailing ``mhc_post`` on ``use_fused_mhc`` (True when AITER
13
+ or TileLang fused MHC is available; False only on the torch fallback);
14
* drop the mega-MoE weight path (``make_deepseek_v4_expert_params_mapping`` /
15
``use_mega_moe`` / ``finalize_mega_moe_weights`` do not exist in amd/model.py).
16
vllm/models/deepseek_v4/amd/model.py
+44
-6
@@ -305,6 +305,10 @@ class DeepseekV4MoE(nn.Module):
305
return final_hidden_states.view(org_shape)
306
307
308
+# Hidden sizes supported by AITER mhc_pre_big_fuse_rmsnorm.
309
+_AITER_MHC_FUSED_RMSNORM_SIZES = frozenset({1280, 2560, 4096, 7168})
310
+
311
+
312
class DeepseekV4DecoderLayer(nn.Module):
313
def __init__(
314
self,
@@ -384,9 +388,18 @@ class DeepseekV4DecoderLayer(nn.Module):
388
self.mhc_pre = MHCPreOp()
389
self.mhc_post = MHCPostOp()
390
self.mhc_fused_post_pre = MHCFusedPostPreOp()
387
- self.use_fused_mhc = HAS_TILELANG_MHC and not (
388
- HAS_AITER_MHC and self.hidden_size % 256 == 0
391
+ # AITER mhc kernels (pre/post/fused) require hc_mult == 4.
392
+ use_aiter_mhc = (
393
+ HAS_AITER_MHC and self.hidden_size % 256 == 0 and self.hc_mult == 4
394
)
395
+ # Prefer AITER fused post+pre when eligible; otherwise TileLang.
396
+ self.use_fused_mhc = use_aiter_mhc or HAS_TILELANG_MHC
397
+ # Fold attn/ffn RMSNorm into MHC only when the active backend's
398
+ # fused-rmsnorm path supports this hidden size.
399
+ if use_aiter_mhc:
400
+ self.fuse_mhc_rmsnorm = self.hidden_size in _AITER_MHC_FUSED_RMSNORM_SIZES
401
+ else:
402
+ self.fuse_mhc_rmsnorm = HAS_TILELANG_MHC and self.use_fused_mhc
403
404
def hc_pre(
405
self,
@@ -394,7 +407,13 @@ class DeepseekV4DecoderLayer(nn.Module):
407
hc_fn: torch.Tensor,
408
hc_scale: torch.Tensor,
409
hc_base: torch.Tensor,
410
+ norm_weight: torch.Tensor | None = None,
411
+ norm_eps: float = 0.0,
412
):
413
+ """Reduce HC residual streams to the next sub-layer input.
414
+
415
+ When ``norm_weight`` is set, RMSNorm is fused into the pre kernel.
416
+ """
417
post_mix, res_mix, layer_input = self.mhc_pre(
418
residual=x,
419
fn=hc_fn,
@@ -405,6 +424,8 @@ class DeepseekV4DecoderLayer(nn.Module):
424
hc_sinkhorn_eps=self.hc_eps,
425
hc_post_mult_value=self.hc_post_alpha,
426
sinkhorn_repeat=self.hc_sinkhorn_iters,
427
+ norm_weight=norm_weight,
428
+ norm_eps=norm_eps,
429
)
430
return layer_input, post_mix, res_mix
431
@@ -426,11 +447,20 @@ class DeepseekV4DecoderLayer(nn.Module):
447
res_mix: torch.Tensor | None = None,
448
residual: torch.Tensor | None = None,
449
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
450
+ attn_norm_weight = self.attn_norm.weight if self.fuse_mhc_rmsnorm else None
451
+ attn_norm_eps = (
452
+ self.attn_norm.variance_epsilon if self.fuse_mhc_rmsnorm else 0.0
453
+ )
454
if residual is None:
455
# Run standalone hc_pre on first layer
456
residual = x
457
x, post_mix, res_mix = self.hc_pre(
433
- x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
458
+ x,
459
+ self.hc_attn_fn,
460
+ self.hc_attn_scale,
461
+ self.hc_attn_base,
462
+ norm_weight=attn_norm_weight,
463
+ norm_eps=attn_norm_eps,
464
)
465
else:
466
residual, post_mix, res_mix, x = self.mhc_fused_post_pre(
@@ -446,11 +476,16 @@ class DeepseekV4DecoderLayer(nn.Module):
476
self.hc_eps,
477
self.hc_post_alpha,
478
self.hc_sinkhorn_iters,
479
+ norm_weight=attn_norm_weight,
480
+ norm_eps=attn_norm_eps,
481
)
482
451
- x = self.attn_norm(x)
483
+ if not self.fuse_mhc_rmsnorm:
484
+ x = self.attn_norm(x)
485
x = self.attn(positions, x, None)
486
487
+ ffn_norm_weight = self.ffn_norm.weight if self.fuse_mhc_rmsnorm else None
488
+ ffn_norm_eps = self.ffn_norm.variance_epsilon if self.fuse_mhc_rmsnorm else 0.0
489
residual, post_mix, res_mix, x = self.mhc_fused_post_pre(
490
x,
491
residual,
@@ -464,8 +499,11 @@ class DeepseekV4DecoderLayer(nn.Module):
499
self.hc_eps,
500
self.hc_post_alpha,
501
self.hc_sinkhorn_iters,
502
+ norm_weight=ffn_norm_weight,
503
+ norm_eps=ffn_norm_eps,
504
)
468
- x = self.ffn_norm(x)
505
+ if not self.fuse_mhc_rmsnorm:
506
+ x = self.ffn_norm(x)
507
x = self.ffn(x, input_ids)
508
return x, residual, post_mix, res_mix
509
@@ -678,7 +716,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
716
residual,
717
)
718
if (idx + 1) in self.aux_hidden_state_layers:
681
- # On the unfused (aiter) path the layer already applied hc_post,
719
+ # On the unfused path the layer already applied hc_post,
720
# so hidden_states is the reconstructed stream; on the fused
721
# path reconstruct it via hc_post before averaging.
722
if layer.use_fused_mhc: