[Perf][DSv4.1] Fold the mHC post block into the delayed pre projection (#56633)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Yongye Zhu committed
Sep 14, 2026 at 11:57 UTC
5372e72a9884c141874465f0eb8054a4a81da6bb
8 files changed
+837
-92
tests/distributed/test_engram_dp_shard.py
+5
-2
@@ -491,7 +491,9 @@ def _check_dummy_hash_model_forward(
491
state.forward = lambda *args: ids.unsqueeze(1)
492
493
class Decoder(SimpleNamespace):
494
- def __call__(self, hidden, positions, input_ids, *args):
494
+ # Engram hashes and the mask stay the last positional arguments;
495
+ # **kwargs absorbs the decoder's keyword-only flags.
496
+ def __call__(self, hidden, positions, input_ids, *args, **kwargs):
497
hashes, keep = args[-2:]
498
if dp_shared_memory and dp_rank == 1:
499
assert hashes is None and keep is None
@@ -504,7 +506,8 @@ def _check_dummy_hash_model_forward(
506
if dp_rank == 1:
507
assert torch.all(hashes == engram_ops.DEAD_ID)
508
output = engram.embed(hashes[:, 0])
507
- return output, None, None, None, None
509
+ # Trailing None is previous_aux; this stub captures no aux states.
510
+ return output, None, None, None, None, None
511
512
model = SimpleNamespace(
513
use_mega_moe=False,
tests/kernels/test_mhc_jit_warmup.py
+38
-9
@@ -17,6 +17,8 @@ from vllm.model_executor.kernels.mhc.tilelang_kernels import (
17
MhcFusedTileLangKernel,
18
MhcPostTileLangKernel,
19
MhcPreBigFuseTileLangKernel,
20
+ mhc_fused_post_pre_split_config,
21
+ require_fused_post_pre_config,
22
)
23
from vllm.model_executor.warmup import jit_warmup_tilelang_helper
24
@@ -113,17 +115,32 @@ def test_mhc_pre_big_fuse_dispatch_matches_legacy_runtime_config(
115
116
117
@pytest.mark.parametrize(
116
- ("num_tokens", "hidden_size", "expected_n_splits", "expected_tile_n"),
117
- [(4, 4096, 8, 2), (4, 8192, 4, 2), (8, 4096, 4, 3)],
118
+ ("num_tokens", "hidden_size", "expected_tile_n", "expected_n_splits"),
119
+ [
120
+ (1, 4096, 2, 8),
121
+ (4, 8192, 2, 8),
122
+ (8, 5120, 2, 8),
123
+ (16, 5120, 6, 8),
124
+ (32, 7168, 6, 8),
125
+ ],
126
)
119
-def test_mhc_fused_dispatch_matches_legacy_runtime_config(
127
+def test_mhc_fused_dispatch_matches_the_launch_config(
128
num_tokens: int,
129
hidden_size: int,
122
- expected_n_splits: int,
130
expected_tile_n: int,
131
+ expected_n_splits: int,
132
) -> None:
133
+ """The compile key must be the config the launch will actually use.
134
+
135
+ Both read mhc_fused_post_pre_split_config, so this pins the tuned bands
136
+ and guards against the two drifting apart again.
137
+ """
138
kernel = MhcFusedTileLangKernel()
139
+ tile_n, n_splits, n_thr = require_fused_post_pre_config(
140
+ num_tokens, hidden_size, hc_mult=4
141
+ )
142
143
+ assert (tile_n, n_splits) == (expected_tile_n, expected_n_splits)
144
assert kernel.dispatch(
145
num_tokens=num_tokens,
146
hidden_size=hidden_size,
@@ -131,11 +148,20 @@ def test_mhc_fused_dispatch_matches_legacy_runtime_config(
148
) == kernel.CompileKey(
149
hidden_size=hidden_size,
150
hc_mult=4,
134
- n_splits=expected_n_splits,
135
- tile_n=expected_tile_n,
151
+ n_splits=n_splits,
152
+ tile_n=tile_n,
153
+ n_thr=n_thr,
154
)
155
156
157
+def test_mhc_fused_declines_shapes_it_cannot_tile() -> None:
158
+ """Above the token cutoff, and for a hidden size the block cannot split."""
159
+ assert mhc_fused_post_pre_split_config(33, 5120, 4) is None
160
+ assert mhc_fused_post_pre_split_config(1, 5137, 4) is None
161
+ with pytest.raises(ValueError, match="does not cover num_tokens"):
162
+ require_fused_post_pre_config(33, 5120, 4)
163
+
164
+
165
@pytest.mark.parametrize(
166
("kernel", "compile_key"),
167
[
@@ -235,8 +261,9 @@ def test_mhc_fused_dispatch_matches_legacy_runtime_config(
261
MhcFusedTileLangKernel.CompileKey(
262
hidden_size=4096,
263
hc_mult=4,
238
- n_splits=4,
239
- tile_n=3,
264
+ n_splits=8,
265
+ tile_n=6,
266
+ n_thr=128,
267
),
268
),
269
(
@@ -246,6 +273,7 @@ def test_mhc_fused_dispatch_matches_legacy_runtime_config(
273
hc_mult=4,
274
n_splits=8,
275
tile_n=2,
276
+ n_thr=128,
277
),
278
),
279
(
@@ -253,8 +281,9 @@ def test_mhc_fused_dispatch_matches_legacy_runtime_config(
281
MhcFusedTileLangKernel.CompileKey(
282
hidden_size=8192,
283
hc_mult=4,
256
- n_splits=4,
284
+ n_splits=8,
285
tile_n=2,
286
+ n_thr=128,
287
),
288
),
289
(
tests/kernels/test_mhc_kernels.py
+244
-4
@@ -15,8 +15,12 @@ from vllm.model_executor.kernels.mhc.tilelang import (
15
)
16
from vllm.model_executor.kernels.mhc.tilelang_kernels import (
17
_HC_PRENORM_GEMM_TILELANG_KERNEL,
18
+ mhc_fused_post_pre_split_config,
19
+)
20
+from vllm.model_executor.kernels.mhc.torch import (
21
+ mhc_post_torch,
22
+ mhc_pre_delayed_torch,
23
)
19
-from vllm.model_executor.kernels.mhc.torch import mhc_pre_delayed_torch
24
from vllm.model_executor.kernels.mhc.triton import hc_collapse_triton
25
from vllm.model_executor.layers.mhc import (
26
HAS_AITER_MHC,
@@ -115,9 +119,9 @@ def test_v41_dspark_head_collapses_with_last_ffn_mix(num_tokens, monkeypatch):
119
carried_mixes = []
120
121
def make_layer(mix):
118
- def forward(hidden, positions, ids, pre, post, res, residual):
122
+ def forward(hidden, positions, ids, pre, post, res, residual, **kwargs):
123
carried_mixes.append(pre)
120
- return hidden, None, None, None, mix
124
+ return hidden, None, None, None, mix, None
125
126
return forward
127
@@ -235,6 +239,171 @@ def test_deepseek_v41_mhc_pre_delayed(
239
torch.testing.assert_close(actual[2], expected[2], atol=1.6e-2, rtol=1e-2)
240
241
242
+def mhc_fused_post_pre_delayed_ref(
243
+ x: torch.Tensor,
244
+ residual: torch.Tensor,
245
+ post_layer_mix: torch.Tensor,
246
+ comb_res_mix: torch.Tensor,
247
+ *mix_args,
248
+ pre_mix: torch.Tensor | None,
249
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
250
+ """Project the post result before it is rounded to BF16.
251
+
252
+ The fused kernel keeps the updated residual streams in registers, so its
253
+ projection sees the FP32 post result while the streams it stores are the
254
+ BF16 rounding of it.
255
+ """
256
+ post = torch.einsum("tij,tih->tjh", comb_res_mix, residual.float())
257
+ post = post + post_layer_mix * x.float().unsqueeze(-2)
258
+ return mhc_pre_delayed_torch(
259
+ post.bfloat16(), *mix_args, pre_mix=pre_mix, x=post.flatten(1)
260
+ )
261
+
262
+
263
+@pytest.mark.skipif(not HAS_TILELANG_MHC, reason="TileLang MHC support required")
264
+@pytest.mark.parametrize("num_tokens", [0, 1, 8, 128])
265
+@pytest.mark.parametrize("hidden_size", [4096, 7168])
266
+@pytest.mark.parametrize("carried", [False, True])
267
+def test_deepseek_v41_mhc_fused_post_pre_delayed(num_tokens, hidden_size, carried):
268
+ """Fold the post block into the delayed pre without changing its outputs."""
269
+ set_random_seed(0)
270
+ hc_mult = 4
271
+ mix_size = hc_mult * (hc_mult + 2)
272
+ x = torch.randn(num_tokens, hidden_size, dtype=torch.bfloat16, device=DEVICE)
273
+ residual = torch.randn(
274
+ num_tokens, hc_mult, hidden_size, dtype=torch.bfloat16, device=DEVICE
275
+ )
276
+ post_layer_mix = torch.rand(num_tokens, hc_mult, 1, device=DEVICE)
277
+ comb_res_mix = torch.rand(num_tokens, hc_mult, hc_mult, device=DEVICE)
278
+ fn = torch.randn(mix_size, hc_mult * hidden_size, device=DEVICE) * 0.02
279
+ scale = torch.tensor([0.5, 0.25, 1.0], device=DEVICE)
280
+ base = torch.randn(mix_size, device=DEVICE)
281
+ pre_mix = torch.rand(num_tokens, hc_mult, device=DEVICE) if carried else None
282
+ weight = torch.empty(hidden_size, dtype=torch.bfloat16, device=DEVICE)
283
+ weight.uniform_(0.5, 1.5)
284
+ mix_args = (fn, scale, base, 1e-20, 1e-6, 1e-6, 2.0, 20)
285
+
286
+ residual_cur, post, comb, layer_input, next_pre, aux = (
287
+ torch.ops.vllm.mhc_fused_post_pre_delayed_tilelang(
288
+ x, residual, post_layer_mix, comb_res_mix, *mix_args, pre_mix, weight, 1e-6
289
+ )
290
+ )
291
+ assert aux.shape == (0, hidden_size)
292
+ assert [
293
+ residual_cur.shape,
294
+ post.shape,
295
+ comb.shape,
296
+ layer_input.shape,
297
+ next_pre.shape,
298
+ ] == [
299
+ (num_tokens, hc_mult, hidden_size),
300
+ (num_tokens, hc_mult, 1),
301
+ (num_tokens, hc_mult, hc_mult),
302
+ (num_tokens, hidden_size),
303
+ (num_tokens, hc_mult),
304
+ ]
305
+ if num_tokens == 0:
306
+ return
307
+
308
+ # The post mapping and the collapse are what the unfused kernels produce.
309
+ residual_ref = torch.ops.vllm.mhc_post_tilelang(
310
+ x, residual, post_layer_mix, comb_res_mix
311
+ )
312
+ layer_input_ref = mhc_pre_delayed_tilelang(
313
+ residual_ref, *mix_args, pre_mix=pre_mix, norm_weight=weight, norm_eps=1e-6
314
+ )[2]
315
+ torch.testing.assert_close(residual_cur, residual_ref, atol=0, rtol=0)
316
+ torch.testing.assert_close(layer_input, layer_input_ref, atol=0, rtol=0)
317
+
318
+ post_ref, comb_ref, _, next_pre_ref = mhc_fused_post_pre_delayed_ref(
319
+ x, residual, post_layer_mix, comb_res_mix, *mix_args, pre_mix=pre_mix
320
+ )
321
+ # Above the fused kernel's token count the projection falls back to a
322
+ # split-k TF32 GEMM over the stored BF16 streams.
323
+ tol = (
324
+ 1e-5
325
+ if mhc_fused_post_pre_split_config(num_tokens, hidden_size, hc_mult) is not None
326
+ else 5e-3
327
+ )
328
+ for actual, expected in (
329
+ (post, post_ref),
330
+ (comb, comb_ref),
331
+ (next_pre, next_pre_ref),
332
+ ):
333
+ torch.testing.assert_close(actual, expected, atol=tol, rtol=tol)
334
+
335
+ # Folding the stream mean into the collapse must not change it, or anything
336
+ # else the op returns.
337
+ captured = torch.ops.vllm.mhc_fused_post_pre_delayed_tilelang(
338
+ x,
339
+ residual,
340
+ post_layer_mix,
341
+ comb_res_mix,
342
+ *mix_args,
343
+ pre_mix,
344
+ weight,
345
+ 1e-6,
346
+ True,
347
+ )
348
+ torch.testing.assert_close(captured[5], residual_cur.mean(dim=1), atol=0, rtol=0)
349
+ unchanged = (residual_cur, post, comb, layer_input, next_pre)
350
+ for with_aux, without in zip(captured[:5], unchanged, strict=True):
351
+ torch.testing.assert_close(with_aux, without, atol=0, rtol=0)
352
+
353
+ # The unnormalized epilogue has its own fused aux path.
354
+ bare = torch.ops.vllm.mhc_fused_post_pre_delayed_tilelang(
355
+ x, residual, post_layer_mix, comb_res_mix, *mix_args, pre_mix, None, 1e-6, True
356
+ )
357
+ torch.testing.assert_close(bare[5], bare[0].mean(dim=1), atol=0, rtol=0)
358
+
359
+ # Decode replays this op from a captured graph.
360
+ graph = torch.cuda.CUDAGraph()
361
+ with torch.cuda.graph(graph):
362
+ captured = torch.ops.vllm.mhc_fused_post_pre_delayed_tilelang(
363
+ x, residual, post_layer_mix, comb_res_mix, *mix_args, pre_mix, weight, 1e-6
364
+ )
365
+ graph.replay()
366
+ eager = (residual_cur, post, comb, layer_input, next_pre, aux)
367
+ for replayed, expected in zip(captured, eager, strict=True):
368
+ torch.testing.assert_close(replayed, expected, atol=0, rtol=0)
369
+
370
+
371
+@pytest.mark.skipif(not HAS_TILELANG_MHC, reason="TileLang MHC support required")
372
+@pytest.mark.parametrize("carried", [False, True])
373
+@pytest.mark.parametrize("capture_aux", [False, True])
374
+def test_mhc_fused_post_pre_delayed_custom_op_supports_compile(carried, capture_aux):
375
+ set_random_seed(0)
376
+ x = torch.randn(2, 5120, dtype=torch.bfloat16, device=DEVICE)
377
+ residual = torch.randn(2, 4, 5120, dtype=torch.bfloat16, device=DEVICE)
378
+ post_layer_mix = torch.rand(2, 4, 1, device=DEVICE)
379
+ comb_res_mix = torch.rand(2, 4, 4, device=DEVICE)
380
+ fn = torch.randn(24, 20480, device=DEVICE) * 0.02
381
+ pre_mix = torch.rand(2, 4, device=DEVICE) if carried else None
382
+ scale = torch.ones(3, device=DEVICE)
383
+ base = torch.zeros(24, device=DEVICE)
384
+ torch.library.opcheck(
385
+ torch.ops.vllm.mhc_fused_post_pre_delayed_tilelang.default,
386
+ (
387
+ x,
388
+ residual,
389
+ post_layer_mix,
390
+ comb_res_mix,
391
+ fn,
392
+ scale,
393
+ base,
394
+ 1e-20,
395
+ 1e-6,
396
+ 1e-6,
397
+ 2.0,
398
+ 20,
399
+ pre_mix,
400
+ None,
401
+ 1e-6,
402
+ capture_aux,
403
+ ),
404
+ )
405
+
406
+
407
@pytest.mark.skipif(not HAS_TILELANG_MHC, reason="TileLang MHC support required")
408
@pytest.mark.parametrize("entry", ["broadcast", "pipeline", "residual", "engram"])
409
def test_deepseek_v41_decoder_mixes_match_torch(
@@ -295,15 +464,86 @@ def test_deepseek_v41_decoder_mixes_match_torch(
464
post, res, collapsed, pre = mhc_pre_delayed_torch(*args, **kwargs)
465
return post, res, decoder.attn_norm(collapsed), pre
466
467
+ def fused_reference(x, residual, post_mix, res_mix, *args, capture_aux=False, **kw):
468
+ residual = mhc_post_torch(x, residual, post_mix, res_mix)
469
+ aux = residual.mean(dim=1) if capture_aux else residual.new_empty(0)
470
+ return residual, *reference(residual, *args, **kw), aux
471
+
472
monkeypatch.setattr(
473
"vllm.models.deepseek_v41.nvidia.model.mhc_pre_delayed_tilelang",
474
reference,
475
)
476
+ monkeypatch.setattr(
477
+ "vllm.models.deepseek_v41.nvidia.model.mhc_fused_post_pre_delayed_tilelang",
478
+ fused_reference,
479
+ )
480
expected = decoder(x, positions, None, **kwargs)
303
- for result, ref in zip(actual, expected, strict=True):
481
+ assert actual[-1] is None and expected[-1] is None
482
+ for result, ref in zip(actual[:-1], expected[:-1], strict=True):
483
torch.testing.assert_close(result, ref, atol=2e-2, rtol=1e-2)
484
485
486
+@pytest.mark.skipif(not HAS_TILELANG_MHC, reason="TileLang MHC support required")
487
+@pytest.mark.parametrize("entry", ["fused", "engram"])
488
+def test_deepseek_v41_capture_previous_aux(entry, monkeypatch, default_vllm_config):
489
+ """Read the aux hidden state back out of the seam that already computed it.
490
+
491
+ The draft model's input is the mean over hc streams of the previous
492
+ layer's post. That post now runs inside this layer's fused pre, so the
493
+ captured value must equal a standalone ``mhc_post_tilelang`` on the same
494
+ inputs -- and Engram must not be folded into it, since the injection
495
+ happens after the aux consumers read the stream.
496
+ """
497
+ set_random_seed(0)
498
+ decoder = DeepseekV41DecoderLayer.__new__(DeepseekV41DecoderLayer)
499
+ nn.Module.__init__(decoder)
500
+ decoder.hc_mult = 4
501
+ decoder.hc_sinkhorn_iters = 20
502
+ decoder.hc_eps = 1e-6
503
+ decoder.rms_norm_eps = 1e-6
504
+ decoder.hc_post_alpha = 2.0
505
+ decoder.use_sequence_parallel = False
506
+ decoder.engram = None
507
+ from vllm.model_executor.layers.layernorm import RMSNorm
508
+
509
+ decoder.attn_norm = decoder.ffn_norm = RMSNorm(5120, 1e-6).to(
510
+ device=DEVICE, dtype=torch.bfloat16
511
+ )
512
+ decoder.attn = lambda positions, x, _: x * 0.5
513
+ decoder.ffn = lambda x, input_ids: x * 0.25
514
+ with torch.device(DEVICE):
515
+ decoder.hc_attn_fn = torch.randn(24, 20480) * 0.02
516
+ decoder.hc_ffn_fn = torch.randn(24, 20480) * 0.02
517
+ decoder.hc_attn_scale = decoder.hc_ffn_scale = torch.ones(3)
518
+ decoder.hc_attn_base = torch.randn(24)
519
+ decoder.hc_ffn_base = torch.randn(24)
520
+ x = torch.randn(3, 5120, dtype=torch.bfloat16)
521
+ kwargs = dict(
522
+ pre_mix=torch.rand(3, 4),
523
+ residual=torch.randn(3, 4, 5120, dtype=torch.bfloat16),
524
+ post_mix=torch.rand(3, 4, 1),
525
+ res_mix=torch.rand(3, 4, 4),
526
+ )
527
+ if entry == "engram":
528
+
529
+ class FakeEngram(nn.Module):
530
+ layer_hash_index = 0
531
+
532
+ def forward(self, residual, hashes, mask):
533
+ return residual + 0.125
534
+
535
+ decoder.engram = FakeEngram()
536
+ kwargs["engram_hashes"] = torch.zeros(3, 1, 1, dtype=torch.int32)
537
+ positions = torch.arange(3)
538
+
539
+ expected = torch.ops.vllm.mhc_post_tilelang(
540
+ x, kwargs["residual"], kwargs["post_mix"], kwargs["res_mix"]
541
+ ).mean(dim=1)
542
+ previous_aux = decoder(x, positions, None, **kwargs, capture_previous_aux=True)[-1]
543
+ torch.testing.assert_close(previous_aux, expected, atol=0, rtol=0)
544
+ assert decoder(x, positions, None, **kwargs)[-1] is None
545
+
546
+
547
@pytest.mark.skipif(not HAS_TILELANG_MHC, reason="TileLang MHC support required")
548
@pytest.mark.parametrize("carried", [False, True])
549
def test_mhc_pre_delayed_custom_op_supports_compile(carried):
vllm/model_executor/kernels/mhc/tilelang.py
+283
-2
@@ -194,6 +194,7 @@ def mhc_pre_delayed_tilelang(
194
norm_weight,
195
pre_mix if pre_mix is not None else post,
196
next_pre_mix,
197
+ layer_input,
198
hidden_size=hidden_size,
199
rms_eps=rms_eps,
200
hc_pre_eps=hc_pre_eps,
@@ -218,6 +219,7 @@ def mhc_pre_delayed_tilelang(
219
layer_input,
220
pre_mix if pre_mix is not None else post,
221
next_pre_mix,
222
+ layer_input,
223
hidden_size,
224
rms_eps,
225
hc_pre_eps,
@@ -263,6 +265,278 @@ def _mhc_pre_delayed_tilelang_fake(
265
)
266
267
268
+def mhc_fused_post_pre_delayed_tilelang(
269
+ x: torch.Tensor,
270
+ residual: torch.Tensor,
271
+ post_layer_mix: torch.Tensor,
272
+ comb_res_mix: torch.Tensor,
273
+ fn: torch.Tensor,
274
+ hc_scale: torch.Tensor,
275
+ hc_base: torch.Tensor,
276
+ rms_eps: float,
277
+ hc_pre_eps: float,
278
+ hc_sinkhorn_eps: float,
279
+ hc_post_mult_value: float,
280
+ sinkhorn_repeat: int,
281
+ pre_mix: torch.Tensor | None = None,
282
+ norm_weight: torch.Tensor | None = None,
283
+ norm_eps: float = 1e-6,
284
+ capture_aux: bool = False,
285
+) -> tuple[
286
+ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
287
+]:
288
+ """Run one mHC post block followed by the next delayed mHC pre block.
289
+
290
+ Within the fused kernel's token range the post mapping is folded into the
291
+ pre-norm GEMM, so the updated residual streams feed the projection from
292
+ registers instead of a second pass over global memory. Above it this runs
293
+ the same post kernel and split-k GEMM as the unfused pair.
294
+
295
+ Args:
296
+ x: BF16 sublayer output of shape (tokens, hidden_size).
297
+ residual: BF16 residual streams of shape (tokens, hc_mult, hidden_size).
298
+ post_layer_mix: FP32 post coefficients of shape (tokens, hc_mult, 1).
299
+ comb_res_mix: FP32 residual coefficients, (tokens, hc_mult, hc_mult).
300
+ fn: FP32 projection of shape (hc_mult * (hc_mult + 2), input_size).
301
+ hc_scale: FP32 scales of shape (3,).
302
+ hc_base: FP32 bias of shape (hc_mult * (hc_mult + 2),).
303
+ rms_eps: RMS normalization epsilon.
304
+ hc_pre_eps: Pre-mix epsilon.
305
+ hc_sinkhorn_eps: Sinkhorn epsilon.
306
+ hc_post_mult_value: Post-mix multiplier.
307
+ sinkhorn_repeat: Number of Sinkhorn iterations.
308
+ pre_mix: FP32 coefficients from the previous sublayer, or None to
309
+ select residual stream zero.
310
+ norm_weight: Optional BF16 RMSNorm weight for the collapsed input.
311
+ norm_eps: RMSNorm epsilon for the collapsed input.
312
+ capture_aux: Also return the mean over the post-mapped streams, which
313
+ draft models consume as the target's hidden state. It is folded
314
+ into the collapse, which already reads those streams.
315
+
316
+ Returns:
317
+ The post-mapped residual streams, the post and residual coefficients,
318
+ the optionally normalized BF16 layer input, the next FP32 pre-mix, and
319
+ the BF16 stream mean (empty unless capture_aux), with shapes
320
+ (tokens, hc_mult, hidden_size), (tokens, hc_mult, 1),
321
+ (tokens, hc_mult, hc_mult), (tokens, hidden_size), (tokens, hc_mult),
322
+ and (tokens, hidden_size).
323
+ """
324
+ from vllm.model_executor.kernels.mhc.tilelang_kernels import (
325
+ _HC_PRENORM_GEMM_TILELANG_KERNEL,
326
+ _MHC_FUSED_TILELANG_KERNEL,
327
+ _MHC_POST_TILELANG_KERNEL,
328
+ mhc_fused_post_pre_split_config,
329
+ mhc_pre_big_fuse_tilelang,
330
+ )
331
+ from vllm.model_executor.kernels.mhc.warmup import (
332
+ MHC_PRE_NORM_KERNEL,
333
+ compute_mhc_pre_num_splits,
334
+ )
335
+ from vllm.utils.deep_gemm import (
336
+ is_deep_gemm_supported,
337
+ tf32_hc_prenorm_gemm,
338
+ )
339
+
340
+ assert residual.ndim == 3 and residual.dtype == torch.bfloat16
341
+ assert residual.is_contiguous()
342
+ num_tokens, hc_mult, hidden_size = residual.shape
343
+ input_size = hc_mult * hidden_size
344
+ mix_size = hc_mult * (hc_mult + 2)
345
+ assert x.shape == (num_tokens, hidden_size) and x.dtype == torch.bfloat16
346
+ assert x.is_contiguous()
347
+ assert post_layer_mix.shape[:2] == (num_tokens, hc_mult)
348
+ assert post_layer_mix.dtype == torch.float32 and post_layer_mix.is_contiguous()
349
+ assert comb_res_mix.shape == (num_tokens, hc_mult, hc_mult)
350
+ assert comb_res_mix.dtype == torch.float32 and comb_res_mix.is_contiguous()
351
+ assert fn.shape == (mix_size, input_size) and fn.dtype == torch.float32
352
+ assert hc_scale.shape == (3,) and hc_scale.dtype == torch.float32
353
+ assert hc_base.shape == (mix_size,) and hc_base.dtype == torch.float32
354
+ if pre_mix is not None:
355
+ assert pre_mix.shape == (num_tokens, hc_mult)
356
+ assert pre_mix.dtype == torch.float32 and pre_mix.is_contiguous()
357
+
358
+ next_pre_mix = torch.empty(
359
+ num_tokens, hc_mult, dtype=torch.float32, device=residual.device
360
+ )
361
+ post = torch.empty_like(next_pre_mix)
362
+ comb = torch.empty(
363
+ num_tokens, hc_mult * hc_mult, dtype=torch.float32, device=residual.device
364
+ )
365
+ layer_input = torch.empty(
366
+ num_tokens, hidden_size, dtype=torch.bfloat16, device=residual.device
367
+ )
368
+ aux = torch.empty(
369
+ num_tokens if capture_aux else 0,
370
+ hidden_size,
371
+ dtype=torch.bfloat16,
372
+ device=residual.device,
373
+ )
374
+ if num_tokens == 0:
375
+ return (
376
+ torch.empty_like(residual),
377
+ post.unsqueeze(-1),
378
+ comb.view(num_tokens, hc_mult, hc_mult),
379
+ layer_input,
380
+ next_pre_mix,
381
+ aux,
382
+ )
383
+
384
+ fused_config = mhc_fused_post_pre_split_config(num_tokens, hidden_size, hc_mult)
385
+ if fused_config is not None:
386
+ mixes, sqrsum, residual_cur = _MHC_FUSED_TILELANG_KERNEL(
387
+ comb_res_mix,
388
+ residual,
389
+ post_layer_mix.view(num_tokens, hc_mult),
390
+ x,
391
+ fn.view(mix_size, hc_mult, hidden_size),
392
+ hc_mult,
393
+ hidden_size,
394
+ mix_size,
395
+ )
396
+ else:
397
+ residual_cur = torch.empty_like(residual)
398
+ _MHC_POST_TILELANG_KERNEL(
399
+ comb_res_mix,
400
+ residual,
401
+ post_layer_mix.view(num_tokens, hc_mult),
402
+ x,
403
+ residual_cur,
404
+ hc_mult,
405
+ hidden_size,
406
+ )
407
+ # The delayed epilogue is compiled per bucketed split count, so the
408
+ # projection has to use the same bucket rather than the raw estimate.
409
+ use_deep_gemm = is_deep_gemm_supported()
410
+ n_splits = (
411
+ compute_mhc_pre_num_splits(input_size, num_tokens) if use_deep_gemm else 1
412
+ )
413
+ mixes = torch.empty(
414
+ n_splits, num_tokens, mix_size, dtype=torch.float32, device=residual.device
415
+ )
416
+ sqrsum = torch.empty(
417
+ n_splits, num_tokens, dtype=torch.float32, device=residual.device
418
+ )
419
+ residual_cur_2d = residual_cur.view(num_tokens, input_size)
420
+ if use_deep_gemm:
421
+ tf32_hc_prenorm_gemm(residual_cur_2d, fn, mixes, sqrsum, n_splits)
422
+ else:
423
+ _HC_PRENORM_GEMM_TILELANG_KERNEL(
424
+ residual_cur_2d,
425
+ fn,
426
+ mixes,
427
+ sqrsum,
428
+ input_size,
429
+ 1,
430
+ )
431
+
432
+ outputs = (
433
+ residual_cur,
434
+ post.unsqueeze(-1),
435
+ comb.view(num_tokens, hc_mult, hc_mult),
436
+ layer_input,
437
+ next_pre_mix,
438
+ aux,
439
+ )
440
+ if norm_weight is not None:
441
+ assert norm_weight.shape == (hidden_size,)
442
+ assert norm_weight.dtype == torch.bfloat16 and norm_weight.is_contiguous()
443
+ MHC_PRE_NORM_KERNEL(
444
+ mixes,
445
+ sqrsum,
446
+ hc_scale,
447
+ hc_base,
448
+ residual_cur,
449
+ post,
450
+ comb,
451
+ layer_input,
452
+ norm_weight,
453
+ pre_mix if pre_mix is not None else post,
454
+ next_pre_mix,
455
+ aux if capture_aux else layer_input,
456
+ hidden_size=hidden_size,
457
+ rms_eps=rms_eps,
458
+ hc_pre_eps=hc_pre_eps,
459
+ hc_sinkhorn_eps=hc_sinkhorn_eps,
460
+ hc_post_mult_value=hc_post_mult_value,
461
+ sinkhorn_repeat=sinkhorn_repeat,
462
+ norm_eps=norm_eps,
463
+ hc_mult=hc_mult,
464
+ use_pre_mix_in=pre_mix is not None,
465
+ save_pre_mix=True,
466
+ rms_numel=input_size,
467
+ write_aux=capture_aux,
468
+ )
469
+ return outputs
470
+ mhc_pre_big_fuse_tilelang(
471
+ mixes,
472
+ sqrsum,
473
+ hc_scale,
474
+ hc_base,
475
+ residual_cur,
476
+ post,
477
+ comb,
478
+ layer_input,
479
+ pre_mix if pre_mix is not None else post,
480
+ next_pre_mix,
481
+ aux if capture_aux else layer_input,
482
+ hidden_size,
483
+ rms_eps,
484
+ hc_pre_eps,
485
+ hc_sinkhorn_eps,
486
+ hc_post_mult_value,
487
+ sinkhorn_repeat,
488
+ mixes.shape[0],
489
+ hc_mult,
490
+ use_pre_mix_in=pre_mix is not None,
491
+ save_pre_mix=True,
492
+ rms_numel=input_size,
493
+ write_aux=capture_aux,
494
+ )
495
+ return outputs
496
+
497
+
498
+def _mhc_fused_post_pre_delayed_tilelang_fake(
499
+ x: torch.Tensor,
500
+ residual: torch.Tensor,
501
+ post_layer_mix: torch.Tensor,
502
+ comb_res_mix: torch.Tensor,
503
+ fn: torch.Tensor,
504
+ hc_scale: torch.Tensor,
505
+ hc_base: torch.Tensor,
506
+ rms_eps: float,
507
+ hc_pre_eps: float,
508
+ hc_sinkhorn_eps: float,
509
+ hc_post_mult_value: float,
510
+ sinkhorn_repeat: int,
511
+ pre_mix: torch.Tensor | None = None,
512
+ norm_weight: torch.Tensor | None = None,
513
+ norm_eps: float = 1e-6,
514
+ capture_aux: bool = False,
515
+) -> tuple[
516
+ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
517
+]:
518
+ num_tokens, hc_mult, hidden_size = residual.shape
519
+ return (
520
+ torch.empty_like(residual),
521
+ torch.empty(
522
+ num_tokens, hc_mult, 1, dtype=torch.float32, device=residual.device
523
+ ),
524
+ torch.empty(
525
+ num_tokens, hc_mult, hc_mult, dtype=torch.float32, device=residual.device
526
+ ),
527
+ torch.empty(
528
+ num_tokens, hidden_size, dtype=torch.bfloat16, device=residual.device
529
+ ),
530
+ torch.empty(num_tokens, hc_mult, dtype=torch.float32, device=residual.device),
531
+ torch.empty(
532
+ num_tokens if capture_aux else 0,
533
+ hidden_size,
534
+ dtype=torch.bfloat16,
535
+ device=residual.device,
536
+ ),
537
+ )
538
+
539
+
540
def mhc_pre_tilelang(
541
residual: torch.Tensor,
542
fn: torch.Tensor,
@@ -574,6 +848,7 @@ def mhc_fused_post_pre_tilelang(
848
_MHC_FUSED_TILELANG_KERNEL,
849
_MHC_POST_TILELANG_KERNEL,
850
_MHC_PRE_BIG_FUSE_TILELANG_KERNEL,
851
+ mhc_fused_post_pre_split_config,
852
)
853
854
assert residual.dtype == torch.bfloat16
@@ -614,7 +889,7 @@ def mhc_fused_post_pre_tilelang(
889
post_layer_mix_flat = post_layer_mix.view(num_tokens, hc_mult)
890
comb_res_mix_flat = comb_res_mix.view(num_tokens, hc_mult, hc_mult)
891
617
- use_small_fma = num_tokens <= 16
892
+ fused_config = mhc_fused_post_pre_split_config(num_tokens, hidden_size, hc_mult)
893
894
post_mix_cur = torch.empty(
895
num_tokens,
@@ -635,7 +910,7 @@ def mhc_fused_post_pre_tilelang(
910
device=residual.device,
911
)
912
638
- if use_small_fma:
913
+ if fused_config is not None:
914
gemm_out_mul, gemm_out_sqrsum, residual_cur = _MHC_FUSED_TILELANG_KERNEL(
915
comb_res_mix_flat,
916
residual_flat,
@@ -815,6 +1090,12 @@ direct_register_custom_op(
1090
mutates_args=[],
1091
fake_impl=_mhc_pre_delayed_tilelang_fake,
1092
)
1093
+direct_register_custom_op(
1094
+ op_name="mhc_fused_post_pre_delayed_tilelang",
1095
+ op_func=mhc_fused_post_pre_delayed_tilelang,
1096
+ mutates_args=[],
1097
+ fake_impl=_mhc_fused_post_pre_delayed_tilelang_fake,
1098
+)
1099
direct_register_custom_op(
1100
op_name="mhc_pre_tilelang",
1101
op_func=mhc_pre_tilelang,
vllm/model_executor/kernels/mhc/tilelang_kernels.py
+124
-16
@@ -37,8 +37,67 @@ def compute_num_split(block_k: int, k: int | None, grid_size: int) -> int:
37
return split_k
38
39
40
-def _mhc_fused_n_splits(num_tokens: int, hidden_size: int) -> int:
41
- return 8 if num_tokens < 8 and hidden_size <= 4096 else 4
40
+# ``mhc_fused_tilelang`` splits the hidden size across a fixed thread block, so
41
+# every (tile_n, n_thr) pair is one more kernel to compile. Tuned on GB300
42
+# (hc_mult 4, hidden_size 5120 and 7168) against the separate post + split-k
43
+# GEMM it replaces; the bands are coarse because the loss against a per-shape
44
+# optimum is ~1-2%, while one fixed config gives up to 18% at the top of the
45
+# range.
46
+_FUSED_POST_PRE_N_THR = 128
47
+_FUSED_POST_PRE_MAX_TOKENS = 32
48
+
49
+
50
+def mhc_fused_post_pre_split_config(
51
+ num_tokens: int, hidden_size: int, hc_mult: int
52
+) -> tuple[int, int, int] | None:
53
+ """Pick ``(tile_n, n_splits, n_thr)`` for the fused post + pre-norm GEMM.
54
+
55
+ Returns None when a separate post kernel followed by a split-k GEMM is the
56
+ better choice, or when the hidden size does not divide evenly across the
57
+ fused kernel's block. One source of truth for the dispatch decision, the
58
+ compile key and the launch, which must agree.
59
+ """
60
+ if num_tokens > _FUSED_POST_PRE_MAX_TOKENS:
61
+ return None
62
+ n_thr = _FUSED_POST_PRE_N_THR
63
+ if hidden_size % n_thr:
64
+ return None
65
+ # The projection tiles supply the parallelism a small token count cannot,
66
+ # so the tile grows with the batch: 1.3x at one token and 1.03x at 32
67
+ # against a split-k GEMM given the split its own estimator computes.
68
+ if num_tokens < 16:
69
+ tile_n, n_splits = 2, 8
70
+ else:
71
+ tile_n, n_splits = 6, 8
72
+ # The kernel drops projection tiles and k-slices it cannot fill evenly.
73
+ mix_size = hc_mult * (hc_mult + 2)
74
+ while tile_n > 1 and mix_size % tile_n:
75
+ tile_n -= 1
76
+ while n_splits > 1 and hidden_size % (n_splits * n_thr):
77
+ n_splits //= 2
78
+ return tile_n, n_splits, n_thr
79
+
80
+
81
+def require_fused_post_pre_config(
82
+ num_tokens: int, hidden_size: int, hc_mult: int
83
+) -> tuple[int, int, int]:
84
+ """The config for a shape the caller has already committed to fusing."""
85
+ config = mhc_fused_post_pre_split_config(num_tokens, hidden_size, hc_mult)
86
+ if config is None:
87
+ raise ValueError(
88
+ "the fused mHC post + pre-norm GEMM does not cover num_tokens="
89
+ f"{num_tokens} at hidden_size={hidden_size}"
90
+ )
91
+ return config
92
+
93
+
94
+def mhc_fused_post_pre_splits(hidden_size: int, hc_mult: int) -> tuple[int, ...]:
95
+ """Every split-k factor the fused post + pre-norm GEMM path can pick."""
96
+ configs = (
97
+ mhc_fused_post_pre_split_config(num_tokens, hidden_size, hc_mult)
98
+ for num_tokens in range(1, _FUSED_POST_PRE_MAX_TOKENS + 1)
99
+ )
100
+ return tuple(sorted({config[1] for config in configs if config is not None}))
101
102
103
@tilelang_jit
@@ -53,6 +112,7 @@ def mhc_pre_big_fuse_tilelang(
112
layer_input,
113
pre_mix_in,
114
pre_mix_out,
115
+ aux_out,
116
hidden_size: int,
117
rms_eps: float,
118
hc_pre_eps: float,
@@ -64,6 +124,7 @@ def mhc_pre_big_fuse_tilelang(
124
use_pre_mix_in: bool = False,
125
save_pre_mix: bool = False,
126
rms_numel: int = 0,
127
+ write_aux: bool = False,
128
):
129
"""Fuse coefficient generation and residual collapse after the projection.
130
@@ -88,6 +149,7 @@ def mhc_pre_big_fuse_tilelang(
149
150
pre_mix_in: T.Tensor[[num_tokens, hc_mult], T.float32] # type: ignore[no-redef, valid-type]
151
pre_mix_out: T.Tensor[[num_tokens, hc_mult], T.float32] # type: ignore[no-redef, valid-type]
152
+ aux_out: T.Tensor[[num_tokens, hidden_size], T.bfloat16] # type: ignore[no-redef, valid-type]
153
154
with T.Kernel(num_tokens, threads=96) as i:
155
if ENABLE_PDL:
@@ -188,11 +250,24 @@ def mhc_pre_big_fuse_tilelang(
250
251
ol = T.alloc_fragment(hidden_block, T.float32)
252
T.clear(ol)
253
+ if write_aux:
254
+ # Aux consumers (draft models) take the plain stream mean
255
+ # of the same residual this collapse reads.
256
+ aux = T.alloc_fragment(hidden_block, T.float32)
257
+ T.clear(aux)
258
259
for i_hc in T.serial(hc_mult):
260
pre = pre_mix_shared[i_hc]
261
for i1_h in T.Parallel(hidden_block):
262
ol[i1_h] += pre * xl[i_hc, i1_h]
263
+ if write_aux:
264
+ aux[i1_h] += xl[i_hc, i1_h]
265
+
266
+ if write_aux:
267
+ for i1_h in T.Parallel(hidden_block):
268
+ aux_out[i, i0_h * hidden_block + i1_h] = T.bfloat16(
269
+ aux[i1_h] / hc_mult
270
+ )
271
272
T.copy(ol, layer_input[i, i0_h * hidden_block])
273
@@ -216,6 +291,7 @@ def mhc_pre_big_fuse_with_norm_tilelang(
291
norm_weight,
292
pre_mix_in,
293
pre_mix_out,
294
+ aux_out,
295
hidden_size: int,
296
rms_eps: float,
297
hc_pre_eps: float,
@@ -229,6 +305,7 @@ def mhc_pre_big_fuse_with_norm_tilelang(
305
use_pre_mix_in: bool = False,
306
save_pre_mix: bool = False,
307
rms_numel: int = 0,
308
+ write_aux: bool = False,
309
):
310
num_tokens = T.dynamic("num_tokens")
311
hc_mult3 = hc_mult * (2 + hc_mult)
@@ -250,6 +327,7 @@ def mhc_pre_big_fuse_with_norm_tilelang(
327
328
pre_mix_in: T.Tensor[[num_tokens, hc_mult], T.float32] # type: ignore[no-redef, valid-type]
329
pre_mix_out: T.Tensor[[num_tokens, hc_mult], T.float32] # type: ignore[no-redef, valid-type]
330
+ aux_out: T.Tensor[[num_tokens, hidden_size], T.bfloat16] # type: ignore[no-redef, valid-type]
331
332
with T.Kernel(num_tokens, threads=96) as i:
333
rms = T.alloc_fragment(1, T.float32)
@@ -345,11 +423,24 @@ def mhc_pre_big_fuse_with_norm_tilelang(
423
424
ol = T.alloc_fragment(hidden_block, T.float32)
425
T.clear(ol)
426
+ if write_aux:
427
+ # Aux consumers (draft models) take the plain stream mean
428
+ # of the same residual this collapse reads.
429
+ aux = T.alloc_fragment(hidden_block, T.float32)
430
+ T.clear(aux)
431
432
for i_hc in T.serial(hc_mult):
433
pre = pre_mix_shared[i_hc]
434
for i1_h in T.Parallel(hidden_block):
435
ol[i1_h] += pre * xl[i_hc, i1_h]
436
+ if write_aux:
437
+ aux[i1_h] += xl[i_hc, i1_h]
438
+
439
+ if write_aux:
440
+ for i1_h in T.Parallel(hidden_block):
441
+ aux_out[i, i0_h * hidden_block + i1_h] = T.bfloat16(
442
+ aux[i1_h] / hc_mult
443
+ )
444
445
if save_pre_mix:
446
# Keep the BF16 boundary before the delayed input RMSNorm.
@@ -1211,9 +1302,17 @@ class MhcPreBigFuseTileLangKernel(
1302
middle_args = (residual_out, *output_args, norm_weight)
1303
elif compile_key.use_norm_weight:
1304
assert norm_weight is not None
1214
- middle_args = (*output_args, norm_weight, post_mix, post_mix)
1305
+ # layer_input stands in for the aux buffer on both epilogues: same
1306
+ # shape and dtype, and write_aux is off here so it is never written.
1307
+ middle_args = (
1308
+ *output_args,
1309
+ norm_weight,
1310
+ post_mix,
1311
+ post_mix,
1312
+ layer_input,
1313
+ )
1314
else:
1216
- middle_args = (*output_args, post_mix, post_mix)
1315
+ middle_args = (*output_args, post_mix, post_mix, layer_input)
1316
norm_eps_args = (compile_key.norm_eps,) if compile_key.use_norm_weight else ()
1317
return (
1318
*common_args,
@@ -1247,8 +1346,14 @@ class MhcPreBigFuseTileLangKernel(
1346
if use_pre_gemm_splits
1347
else n_splits
1348
)
1250
- fused_n_splits = _mhc_fused_n_splits(num_tokens, hidden_size)
1251
- actual_n_splits = fused_n_splits if use_fused_tilelang else pre_gemm_n_splits
1349
+ # The epilogue reduces over whatever the fused kernel split the GEMM
1350
+ # into, so both have to read the same config.
1351
+ fused_config = mhc_fused_post_pre_split_config(num_tokens, hidden_size, hc_mult)
1352
+ actual_n_splits = (
1353
+ fused_config[1]
1354
+ if use_fused_tilelang and fused_config is not None
1355
+ else pre_gemm_n_splits
1356
+ )
1357
actual_norm_eps = broadcast_norm_eps if is_broadcast else norm_eps
1358
actual_use_norm_weight = use_norm_weight or is_broadcast
1359
return self.CompileKey(
@@ -1540,6 +1645,7 @@ class MhcFusedTileLangKernel(
1645
hc_mult: int
1646
n_splits: int
1647
tile_n: int
1648
+ n_thr: int
1649
1650
@staticmethod
1651
def kernel() -> Any:
@@ -1552,14 +1658,15 @@ class MhcFusedTileLangKernel(
1658
hidden_size: int,
1659
hc_mult: int,
1660
) -> CompileKey:
1555
- # TODO(gnovack): investigate autotuning these heuristics
1556
- tile_n = 2 if num_tokens < 8 else 3
1557
- n_splits = _mhc_fused_n_splits(num_tokens, hidden_size)
1661
+ # Subscripts rather than an unpack: the warmup tracer parses this body
1662
+ # and allows only plain assignments before the return.
1663
+ config = require_fused_post_pre_config(num_tokens, hidden_size, hc_mult)
1664
return self.CompileKey(
1665
hidden_size=hidden_size,
1666
hc_mult=hc_mult,
1561
- n_splits=n_splits,
1562
- tile_n=tile_n,
1667
+ n_splits=config[1],
1668
+ tile_n=config[0],
1669
+ n_thr=config[2],
1670
)
1671
1672
def get_warmup_keys(
@@ -1574,11 +1681,11 @@ class MhcFusedTileLangKernel(
1681
num_tokens=WarmupIntRange(1, max_tokens + 1),
1682
hidden_size=hidden_size,
1683
hc_mult=hc_mult,
1577
- _when=lambda *, num_tokens: num_tokens <= 16,
1684
+ _when=lambda *, num_tokens: num_tokens <= _FUSED_POST_PRE_MAX_TOKENS,
1685
)
1686
1687
def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
1581
- num_tokens = 1 if compile_key.tile_n == 2 else 8
1688
+ num_tokens = 1 if compile_key.tile_n == 2 else 16
1689
hidden_size = compile_key.hidden_size
1690
hc_mult = compile_key.hc_mult
1691
hc_mult3 = hc_mult * (2 + hc_mult)
@@ -1617,8 +1724,9 @@ class MhcFusedTileLangKernel(
1724
hc_mult3: int,
1725
) -> TileLangLaunchSpec:
1726
num_tokens = residual_in.shape[0]
1620
- tile_n = 2 if num_tokens < 8 else 3
1621
- n_splits = _mhc_fused_n_splits(num_tokens, hidden_size)
1727
+ tile_n, n_splits, n_thr = require_fused_post_pre_config(
1728
+ num_tokens, hidden_size, hc_mult
1729
+ )
1730
yp_out = residual_in.new_empty(
1731
(n_splits, num_tokens, hc_mult3), dtype=torch.float32
1732
)
@@ -1639,7 +1747,7 @@ class MhcFusedTileLangKernel(
1747
hidden_size,
1748
hc_mult3,
1749
),
1642
- dict(tile_n=tile_n, split_k=n_splits),
1750
+ dict(n_thr=n_thr, tile_n=tile_n),
1751
(yp_out, rp_out, residual_out),
1752
)
1753
vllm/model_executor/kernels/mhc/warmup.py
+16
-6
@@ -36,23 +36,33 @@ class MHCPreNormKernel(VllmJitKernel["MHCPreNormKernel.CompileKey"]):
36
use_pre_mix_in: bool
37
rms_numel: int
38
save_pre_mix: bool = True
39
+ write_aux: bool = False
40
41
kernel: Any = staticmethod(mhc_pre_big_fuse_with_norm_tilelang)
42
43
def dispatch(self, *, n_splits, **fields) -> CompileKey: # type: ignore[override]
44
return self.CompileKey(n_splits=n_splits, **fields)
45
45
- def get_warmup_keys(self, *, max_tokens: int, **fields) -> list[CompileKey]:
46
- # The split heuristic changes only at 64-token boundaries.
47
- splits = (
48
- sorted(
46
+ def get_warmup_keys(
47
+ self,
48
+ *,
49
+ max_tokens: int,
50
+ extra_splits: Iterable[int] = (),
51
+ **fields,
52
+ ) -> list[CompileKey]:
53
+ # The split heuristic changes only at 64-token boundaries. Callers that
54
+ # reach this kernel through another GEMM (the fused post + pre-norm
55
+ # GEMM) pass the splits that GEMM picks in extra_splits.
56
+ splits = sorted(
57
+ set(extra_splits)
58
+ | (
59
{
60
compute_mhc_pre_num_splits(fields["rms_numel"], num_tokens)
61
for num_tokens in range(1, max_tokens + 1, 64)
62
}
63
+ if is_deep_gemm_supported()
64
+ else {1}
65
)
54
- if is_deep_gemm_supported()
55
- else [1]
66
)
67
return self._trace_dispatch(self.dispatch)(n_splits=splits, **fields)
68
vllm/models/deepseek_v41/nvidia/dspark.py
+1
-1
@@ -207,7 +207,7 @@ class DSparkDeepseekV4Model(nn.Module):
207
208
residual = post_mix = res_mix = pre_mix = None
209
for layer in self.layers:
210
- hidden_states, residual, post_mix, res_mix, pre_mix = layer(
210
+ hidden_states, residual, post_mix, res_mix, pre_mix, _ = layer(
211
hidden_states,
212
positions,
213
input_ids,
vllm/models/deepseek_v41/nvidia/model.py
+126
-52
@@ -20,6 +20,7 @@ from vllm.distributed import (
20
from vllm.forward_context import get_forward_context, is_forward_context_available
21
from vllm.logger import init_logger
22
from vllm.model_executor.kernels.mhc.tilelang import (
23
+ mhc_fused_post_pre_delayed_tilelang,
24
mhc_post_tilelang,
25
mhc_pre_delayed_tilelang,
26
)
@@ -214,17 +215,45 @@ class DeepseekV4DecoderLayer(nn.Module):
215
self.hc_eps = config.hc_eps
216
self.hc_post_alpha = 2.0
217
if vllm_config.kernel_config.enable_jit_warmup and current_platform.is_cuda():
218
+ from vllm.model_executor.kernels.mhc.tilelang_kernels import (
219
+ mhc_fused_post_pre_splits,
220
+ )
221
from vllm.model_executor.kernels.mhc.warmup import MHC_PRE_NORM_KERNEL
222
223
max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
224
if self.use_sequence_parallel:
225
tp_size = vllm_config.parallel_config.tensor_parallel_size
226
max_tokens = (max_tokens + tp_size - 1) // tp_size
223
- for input_size, use_pre_mix in (
224
- (self.hidden_size, False),
225
- (self.hc_mult * self.hidden_size, False),
226
- (self.hc_mult * self.hidden_size, True),
227
- ):
227
+ # The epilogue compiles per projection width and per pre-mix mode.
228
+ # The first layer projects the broadcast embedding, so it reads one
229
+ # hidden_size-wide row and selects stream zero; every later sublayer
230
+ # projects the full hc stream, and collapses it with the pre-mix the
231
+ # previous sublayer carried in. Those later shapes also arrive
232
+ # through the fused post + pre-norm GEMM, which picks split-k
233
+ # factors the token sweep below never produces.
234
+ broadcast_embedding = {
235
+ "rms_numel": self.hidden_size,
236
+ "use_pre_mix_in": False,
237
+ "extra_splits": (),
238
+ }
239
+ hc_stream = {
240
+ "rms_numel": self.hc_mult * self.hidden_size,
241
+ "extra_splits": mhc_fused_post_pre_splits(
242
+ self.hidden_size, self.hc_mult
243
+ ),
244
+ }
245
+ variants = [
246
+ broadcast_embedding,
247
+ {**hc_stream, "use_pre_mix_in": False},
248
+ {**hc_stream, "use_pre_mix_in": True},
249
+ ]
250
+ if vllm_config.speculative_config is not None:
251
+ # Draft setups read aux hidden states out of the same collapse,
252
+ # which is its own specialization of the kernel.
253
+ variants.append(
254
+ {**hc_stream, "use_pre_mix_in": True, "write_aux": True}
255
+ )
256
+ for variant in variants:
257
MHC_PRE_NORM_KERNEL.register_warmup(
258
max_tokens=max_tokens,
259
hidden_size=self.hidden_size,
@@ -235,8 +264,7 @@ class DeepseekV4DecoderLayer(nn.Module):
264
sinkhorn_repeat=self.hc_sinkhorn_iters,
265
norm_eps=self.rms_norm_eps,
266
hc_mult=self.hc_mult,
238
- use_pre_mix_in=use_pre_mix,
239
- rms_numel=input_size,
267
+ **variant,
268
)
269
mix_hc = (2 + self.hc_mult) * self.hc_mult
270
hc_dim = self.hc_mult * self.hidden_size
@@ -295,7 +323,17 @@ class DeepseekV4DecoderLayer(nn.Module):
323
residual: torch.Tensor | None = None,
324
engram_hashes: torch.Tensor | None = None,
325
engram_mask: torch.Tensor | None = None,
298
- ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
326
+ *,
327
+ capture_previous_aux: bool = False,
328
+ ) -> tuple[
329
+ torch.Tensor,
330
+ torch.Tensor,
331
+ torch.Tensor,
332
+ torch.Tensor,
333
+ torch.Tensor,
334
+ torch.Tensor | None,
335
+ ]:
336
+ previous_aux: torch.Tensor | None = None
337
# The reference collapses each sublayer's input with the *previous*
338
# sublayer's pre-mix: attention uses the pre-mix carried in (identity
339
# for the first layer), the FFN uses this layer's attention pre-mix.
@@ -335,17 +373,19 @@ class DeepseekV4DecoderLayer(nn.Module):
373
norm_weight=self.attn_norm.weight,
374
norm_eps=self.attn_norm.variance_epsilon,
375
)
338
- else:
339
- residual = mhc_post_tilelang(x, residual, post_mix, res_mix)
340
- if self.engram is not None and engram_hashes is not None:
341
- # Engram injection happens between the previous sublayer's
342
- # post and this block's pre, on the full hc stream, so the
343
- # mix coefficients see the injected stream.
344
- residual = self.engram(
345
- residual,
346
- engram_hashes[:, self.engram.layer_hash_index],
347
- engram_mask,
348
- )
376
+ elif self.engram is not None and engram_hashes is not None:
377
+ # Engram injection happens between the previous sublayer's post
378
+ # and this block's pre, on the full hc stream, so the mix
379
+ # coefficients see the injected stream. The injection also keeps
380
+ # the post out of the pre-norm GEMM's fused prologue.
381
+ previous_post = mhc_post_tilelang(x, residual, post_mix, res_mix)
382
+ if capture_previous_aux:
383
+ previous_aux = previous_post.mean(dim=1)
384
+ residual = self.engram(
385
+ previous_post,
386
+ engram_hashes[:, self.engram.layer_hash_index],
387
+ engram_mask,
388
+ )
389
post_mix, res_mix, x, attn_pre = mhc_pre_delayed_tilelang(
390
residual,
391
self.hc_attn_fn,
@@ -360,6 +400,31 @@ class DeepseekV4DecoderLayer(nn.Module):
400
norm_weight=self.attn_norm.weight,
401
norm_eps=self.attn_norm.variance_epsilon,
402
)
403
+ else:
404
+ # The collapse already reads the post-mapped streams, so the mean
405
+ # aux consumers want comes out of the same kernel.
406
+ residual, post_mix, res_mix, x, attn_pre, aux = (
407
+ mhc_fused_post_pre_delayed_tilelang(
408
+ x,
409
+ residual,
410
+ post_mix,
411
+ res_mix,
412
+ self.hc_attn_fn,
413
+ self.hc_attn_scale,
414
+ self.hc_attn_base,
415
+ self.rms_norm_eps,
416
+ self.hc_eps,
417
+ self.hc_eps,
418
+ self.hc_post_alpha,
419
+ self.hc_sinkhorn_iters,
420
+ pre_mix=pre_mix,
421
+ norm_weight=self.attn_norm.weight,
422
+ norm_eps=self.attn_norm.variance_epsilon,
423
+ capture_aux=capture_previous_aux,
424
+ )
425
+ )
426
+ if capture_previous_aux:
427
+ previous_aux = aux
428
429
if self.use_sequence_parallel:
430
x = sp_all_gather(x)[: positions.shape[0]]
@@ -368,23 +433,27 @@ class DeepseekV4DecoderLayer(nn.Module):
433
if self.use_sequence_parallel:
434
x = sp_reduce_scatter(x)
435
371
- residual = mhc_post_tilelang(x, residual, post_mix, res_mix)
372
- post_mix, res_mix, x, ffn_pre = mhc_pre_delayed_tilelang(
373
- residual,
374
- self.hc_ffn_fn,
375
- self.hc_ffn_scale,
376
- self.hc_ffn_base,
377
- self.rms_norm_eps,
378
- self.hc_eps,
379
- self.hc_eps,
380
- self.hc_post_alpha,
381
- self.hc_sinkhorn_iters,
382
- pre_mix=attn_pre,
383
- norm_weight=self.ffn_norm.weight,
384
- norm_eps=self.ffn_norm.variance_epsilon,
436
+ residual, post_mix, res_mix, x, ffn_pre, _ = (
437
+ mhc_fused_post_pre_delayed_tilelang(
438
+ x,
439
+ residual,
440
+ post_mix,
441
+ res_mix,
442
+ self.hc_ffn_fn,
443
+ self.hc_ffn_scale,
444
+ self.hc_ffn_base,
445
+ self.rms_norm_eps,
446
+ self.hc_eps,
447
+ self.hc_eps,
448
+ self.hc_post_alpha,
449
+ self.hc_sinkhorn_iters,
450
+ pre_mix=attn_pre,
451
+ norm_weight=self.ffn_norm.weight,
452
+ norm_eps=self.ffn_norm.variance_epsilon,
453
+ )
454
)
455
x = self.ffn(x, input_ids)
387
- return x, residual, post_mix, res_mix, ffn_pre
456
+ return x, residual, post_mix, res_mix, ffn_pre, previous_aux
457
458
459
class DeepseekV4Model(nn.Module, EagleModelMixin):
@@ -631,13 +700,14 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
700
if not get_pp_group().is_first_rank:
701
assert intermediate_tensors is not None
702
pre_mix = intermediate_tensors["pre_mix"]
634
- aux_hidden_states: list[torch.Tensor] = []
635
- final_aux_recon: torch.Tensor | None = None # avoid duplicate mhc_post call
703
+ # Every layer's post runs inside the next layer's fused pre, so aux
704
+ # hidden states are read back from there instead of recomputed.
705
+ aux_hidden_by_layer: dict[int, torch.Tensor] = {}
706
for idx, layer in enumerate(
707
islice(self.layers, self.start_layer, self.end_layer),
708
start=self.start_layer,
709
):
640
- hidden_states, residual, post_mix, res_mix, pre_mix = layer(
710
+ hidden_states, residual, post_mix, res_mix, pre_mix, previous_aux = layer(
711
hidden_states,
712
positions,
713
input_ids,
@@ -647,25 +717,29 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
717
residual,
718
engram_hashes,
719
engram_mask,
720
+ capture_previous_aux=idx in self.aux_hidden_state_layers,
721
)
651
- if idx + 1 in self.aux_hidden_state_layers:
652
- # Reconstruct the aux hidden state for draft models
653
- aux_recon = mhc_post_tilelang(
654
- hidden_states, residual, post_mix, res_mix
655
- )
656
- aux_hidden_state = aux_recon.mean(dim=1)
722
+ if previous_aux is not None:
723
+ # idx is the one-based id of the layer whose post this is.
724
if self.use_sequence_parallel:
658
- aux_hidden_state = sp_all_gather(aux_hidden_state)[:full_num_tokens]
659
- aux_hidden_states.append(aux_hidden_state)
660
- final_aux_recon = aux_recon
725
+ previous_aux = sp_all_gather(previous_aux)[:full_num_tokens]
726
+ aux_hidden_by_layer[idx] = previous_aux
727
if layer is not None:
662
- # Reuse if the last layer was captured as an aux hidden state
728
+ # The last layer has no successor to fold its post into.
729
+ hidden_states = mhc_post_tilelang(
730
+ hidden_states, residual, post_mix, res_mix
731
+ )
732
if self.end_layer in self.aux_hidden_state_layers:
664
- hidden_states = final_aux_recon
665
- else:
666
- hidden_states = mhc_post_tilelang(
667
- hidden_states, residual, post_mix, res_mix
668
- )
733
+ final_aux = hidden_states.mean(dim=1)
734
+ if self.use_sequence_parallel:
735
+ final_aux = sp_all_gather(final_aux)[:full_num_tokens]
736
+ aux_hidden_by_layer[self.end_layer] = final_aux
737
+
738
+ aux_hidden_states = [
739
+ aux_hidden_by_layer[layer_id]
740
+ for layer_id in self.aux_hidden_state_layers
741
+ if layer_id in aux_hidden_by_layer
742
+ ]
743
744
if not get_pp_group().is_last_rank:
745
return IntermediateTensors(