[Perf][DSV4.1] Overlap mHC coefficients for small TP batches (#57603)
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Codex <noreply@openai.com>
Woosuk Kwon committed
Sep 20, 2026 at 17:33 UTC
9b49f92344312c41ad61e05282c8e6a2d9bafb7f
6 files changed
+357
-46
tests/kernels/test_mhc_kernels.py
+129
-12
@@ -266,6 +266,77 @@ def test_deepseek_v41_mhc_pre_delayed(
266
torch.testing.assert_close(actual[2], expected[2], atol=1.6e-2, rtol=1e-2)
267
268
269
+@pytest.mark.skipif(
270
+ not HAS_TILELANG_MHC or not current_platform.is_device_capability_family(100),
271
+ reason="SM100 TileLang mHC required",
272
+)
273
+@pytest.mark.parametrize("num_tokens", [0, 1, 8, 9, 32, 128, 256])
274
+@pytest.mark.parametrize("entry", ["broadcast", "identity", "carried"])
275
+@pytest.mark.parametrize("hidden_size,hc_mult", [(4096, 2), (5120, 4), (7168, 4)])
276
+def test_deepseek_v41_mhc_overlap_preserves_outputs_on_replay(
277
+ num_tokens, entry, hidden_size, hc_mult
278
+):
279
+ """The input is usable before joining; coefficients stay exact on graph replay."""
280
+ from vllm.models.deepseek_v41.nvidia.ops.mhc import mhc_pre_delayed_overlap
281
+ from vllm.utils.deep_gemm import is_deep_gemm_supported
282
+
283
+ if not is_deep_gemm_supported():
284
+ pytest.skip("DeepGEMM required")
285
+ set_random_seed(42)
286
+ mix_size = hc_mult * (hc_mult + 2)
287
+ residual = torch.randn(
288
+ num_tokens, hc_mult, hidden_size, dtype=torch.bfloat16, device=DEVICE
289
+ )
290
+ fn = torch.randn(mix_size, hc_mult, hidden_size, device=DEVICE) * 0.02
291
+ x = None
292
+ pre_mix = None
293
+ if entry == "broadcast":
294
+ x = residual[:, 0].contiguous()
295
+ residual = x.unsqueeze(1).expand(-1, hc_mult, -1).contiguous()
296
+ fn = fn.sum(1)
297
+ else:
298
+ fn = fn.flatten(1)
299
+ if entry == "carried":
300
+ pre_mix = torch.rand(num_tokens, hc_mult, device=DEVICE)
301
+ scale = torch.tensor([0.5, 0.25, 1.0], device=DEVICE)
302
+ base = torch.randn(mix_size, device=DEVICE)
303
+ weight = torch.empty(hidden_size, dtype=torch.bfloat16, device=DEVICE).uniform_(
304
+ 0.5, 1.5
305
+ )
306
+ args = (residual, fn, scale, base, 1e-20, 1e-6, 1e-6, 2.0, 20)
307
+ kwargs = dict(pre_mix=pre_mix, x=x, norm_weight=weight, norm_eps=1e-20)
308
+ side = torch.cuda.Stream()
309
+
310
+ def run():
311
+ outputs = mhc_pre_delayed_overlap(*args, **kwargs, stream=side)
312
+ consumed_input = outputs[2].clone()
313
+ torch.cuda.current_stream().wait_stream(side)
314
+ return (*outputs, consumed_input)
315
+
316
+ def check(actual):
317
+ expected = mhc_pre_delayed_tilelang(*args, **kwargs)
318
+ for a, b in zip(actual[:4], expected, strict=True):
319
+ torch.testing.assert_close(a, b, atol=0, rtol=0)
320
+ torch.testing.assert_close(actual[4], expected[2], atol=0, rtol=0)
321
+
322
+ check(run())
323
+ if num_tokens == 0:
324
+ return
325
+ graph = torch.cuda.CUDAGraph()
326
+ with torch.cuda.graph(graph):
327
+ captured = run()
328
+ for _ in range(5):
329
+ if x is not None:
330
+ x.normal_()
331
+ residual.copy_(x.unsqueeze(1).expand_as(residual))
332
+ else:
333
+ residual.normal_()
334
+ if pre_mix is not None:
335
+ pre_mix.uniform_()
336
+ graph.replay()
337
+ check(captured)
338
+
339
+
340
def mhc_fused_post_pre_delayed_ref(
341
x: torch.Tensor,
342
residual: torch.Tensor,
@@ -433,10 +504,16 @@ def test_mhc_fused_post_pre_delayed_custom_op_supports_compile(carried, capture_
504
505
@pytest.mark.skipif(not HAS_TILELANG_MHC, reason="TileLang MHC support required")
506
@pytest.mark.parametrize("entry", ["broadcast", "pipeline", "residual", "engram"])
507
+@pytest.mark.parametrize(
508
+ "mhc_mode", ["disabled", "overlap", "piecewise", "large_batch"]
509
+)
510
def test_deepseek_v41_decoder_mixes_match_torch(
437
- entry, monkeypatch, default_vllm_config
511
+ entry, mhc_mode, monkeypatch, default_vllm_config
512
):
513
"""Preserve carried pre-mixes and Engram ordering at every decoder entry."""
514
+ from vllm.models.deepseek_v41.nvidia.ops.mhc import MHC_OVERLAP_MAX_TOKENS
515
+
516
+ num_tokens = MHC_OVERLAP_MAX_TOKENS + 1 if mhc_mode == "large_batch" else 3
517
set_random_seed(0)
518
decoder = DeepseekV41DecoderLayer.__new__(DeepseekV41DecoderLayer)
519
nn.Module.__init__(decoder)
@@ -446,6 +523,29 @@ def test_deepseek_v41_decoder_mixes_match_torch(
523
decoder.rms_norm_eps = 1e-20
524
decoder.hc_post_alpha = 2.0
525
decoder.use_sequence_parallel = False
526
+ decoder.mhc_stream = None
527
+ if mhc_mode != "disabled":
528
+ from vllm.utils.deep_gemm import is_deep_gemm_supported
529
+
530
+ if (
531
+ not current_platform.is_device_capability_family(100)
532
+ or not is_deep_gemm_supported()
533
+ ):
534
+ pytest.skip("SM100 DeepGEMM required for overlap")
535
+ decoder.mhc_stream = torch.cuda.Stream()
536
+ if mhc_mode in ("piecewise", "large_batch"):
537
+
538
+ def unexpected_overlap(*args, **kwargs):
539
+ pytest.fail("unsupported execution must retain native mHC")
540
+
541
+ monkeypatch.setattr(
542
+ "vllm.models.deepseek_v41.nvidia.model.mhc_pre_delayed_overlap",
543
+ unexpected_overlap,
544
+ )
545
+ monkeypatch.setattr(
546
+ "vllm.models.deepseek_v41.nvidia.ops.mhc.mhc_pre_delayed_overlap",
547
+ unexpected_overlap,
548
+ )
549
decoder.engram = None
550
from vllm.model_executor.layers.layernorm import RMSNorm
551
@@ -461,18 +561,18 @@ def test_deepseek_v41_decoder_mixes_match_torch(
561
decoder.hc_attn_scale = decoder.hc_ffn_scale = torch.ones(3)
562
decoder.hc_attn_base = torch.randn(24)
563
decoder.hc_ffn_base = torch.randn(24)
464
- x = torch.randn(3, 5120, dtype=torch.bfloat16)
465
- positions = torch.arange(3)
564
+ x = torch.randn(num_tokens, 5120, dtype=torch.bfloat16)
565
+ positions = torch.arange(num_tokens)
566
kwargs = {}
567
if entry != "broadcast":
468
- kwargs["pre_mix"] = torch.rand(3, 4)
568
+ kwargs["pre_mix"] = torch.rand(num_tokens, 4)
569
if entry == "pipeline":
470
- x = torch.randn(3, 4, 5120, dtype=torch.bfloat16)
570
+ x = torch.randn(num_tokens, 4, 5120, dtype=torch.bfloat16)
571
if entry in ("residual", "engram"):
572
kwargs.update(
473
- residual=torch.randn(3, 4, 5120, dtype=torch.bfloat16),
474
- post_mix=torch.rand(3, 4, 1),
475
- res_mix=torch.rand(3, 4, 4),
573
+ residual=torch.randn(num_tokens, 4, 5120, dtype=torch.bfloat16),
574
+ post_mix=torch.rand(num_tokens, 4, 1),
575
+ res_mix=torch.rand(num_tokens, 4, 4),
576
)
577
if entry == "engram":
578
@@ -483,15 +583,22 @@ def test_deepseek_v41_decoder_mixes_match_torch(
583
return residual + 0.125
584
585
decoder.engram = FakeEngram()
486
- kwargs["engram_hashes"] = torch.zeros(3, 1, 1, dtype=torch.int32)
586
+ kwargs["engram_hashes"] = torch.zeros(num_tokens, 1, 1, dtype=torch.int32)
587
488
- actual = decoder(x, positions, None, **kwargs)
588
+ from vllm.config import CUDAGraphMode
589
+ from vllm.forward_context import set_forward_context
590
+
591
+ mode = CUDAGraphMode.PIECEWISE if mhc_mode == "piecewise" else CUDAGraphMode.NONE
592
+ with set_forward_context(None, default_vllm_config, cudagraph_runtime_mode=mode):
593
+ actual = decoder(x, positions, None, **kwargs)
594
595
def reference(*args, norm_weight, norm_eps, **kwargs):
596
post, res, collapsed, pre = mhc_pre_delayed_torch(*args, **kwargs)
597
return post, res, decoder.attn_norm(collapsed), pre
598
494
- def fused_reference(x, residual, post_mix, res_mix, *args, capture_aux=False, **kw):
599
+ def fused_reference(
600
+ x, residual, post_mix, res_mix, *args, capture_aux=False, stream=None, **kw
601
+ ):
602
residual = mhc_post_torch(x, residual, post_mix, res_mix)
603
aux = residual.mean(dim=1) if capture_aux else residual.new_empty(0)
604
return residual, *reference(residual, *args, **kw), aux
@@ -500,6 +607,7 @@ def test_deepseek_v41_decoder_mixes_match_torch(
607
"vllm.models.deepseek_v41.nvidia.model.mhc_pre_delayed_tilelang",
608
reference,
609
)
610
+ decoder.mhc_stream = None
611
monkeypatch.setattr(
612
"vllm.models.deepseek_v41.nvidia.model.mhc_shifted_post_pre",
613
fused_reference,
@@ -530,6 +638,7 @@ def test_deepseek_v41_capture_previous_aux(entry, monkeypatch, default_vllm_conf
638
decoder.rms_norm_eps = 1e-6
639
decoder.hc_post_alpha = 2.0
640
decoder.use_sequence_parallel = False
641
+ decoder.mhc_stream = None
642
decoder.engram = None
643
from vllm.model_executor.layers.layernorm import RMSNorm
644
@@ -772,7 +881,8 @@ def hc_head_ref(
881
@pytest.mark.parametrize("num_tokens", [1, 4, 8, 128])
882
@pytest.mark.parametrize("hidden_size", [4096, 7168])
883
@pytest.mark.parametrize("hc_mult", [4])
775
-def test_mhc_pre_tilelang(num_tokens, hidden_size, hc_mult):
884
+@pytest.mark.parametrize("fused_norm", [False, True])
885
+def test_mhc_pre_tilelang(num_tokens, hidden_size, hc_mult, fused_norm):
886
torch.set_default_device(DEVICE)
887
set_random_seed(0)
888
@@ -802,6 +912,12 @@ def test_mhc_pre_tilelang(num_tokens, hidden_size, hc_mult):
912
hc_post_alpha,
913
sinkhorn_repeat,
914
)
915
+ norm_kwargs = {}
916
+ if fused_norm:
917
+ # Non-shifted mHC must still use its freshly computed pre-mix.
918
+ weight = torch.ones(hidden_size, dtype=torch.bfloat16)
919
+ norm_kwargs = dict(norm_weight=weight, norm_eps=rms_eps)
920
+ ref = (*ref[:2], F.rms_norm(ref[2], (hidden_size,), weight, rms_eps))
921
out = torch.ops.vllm.mhc_pre_tilelang(
922
residual,
923
fn,
@@ -812,6 +928,7 @@ def test_mhc_pre_tilelang(num_tokens, hidden_size, hc_mult):
928
hc_sinkhorn_eps,
929
hc_post_alpha,
930
sinkhorn_repeat,
931
+ **norm_kwargs,
932
)
933
934
for actual, expected in zip(out, ref, strict=True):
vllm/model_executor/kernels/mhc/tilelang_kernels.py
+21
-17
@@ -305,8 +305,12 @@ 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
+ split_mode: str = "fused",
309
write_aux: bool = False,
310
):
311
+ # Split modes require shifted mHC: input collapse uses a carried pre-mix.
312
+ assert split_mode in ("fused", "stats", "input")
313
+ assert split_mode == "fused" or save_pre_mix
314
num_tokens = T.dynamic("num_tokens")
315
hc_mult3 = hc_mult * (2 + hc_mult)
316
if gemm_last_dim < 0:
@@ -330,26 +334,26 @@ def mhc_pre_big_fuse_with_norm_tilelang(
334
aux_out: T.Tensor[[num_tokens, hidden_size], T.bfloat16] # type: ignore[no-redef, valid-type]
335
336
with T.Kernel(num_tokens, threads=96) as i:
333
- rms = T.alloc_fragment(1, T.float32)
334
- mixes = T.alloc_fragment(hc_mult3, T.float32)
335
- T.clear(mixes)
336
- rms[0] = 0
337
-
337
if ENABLE_PDL:
338
T.pdl_sync()
340
-
341
- for i_split in T.serial(n_splits):
342
- rms[0] += gemm_out_sqrsum[i_split, i]
343
- rms[0] = T.rsqrt(rms[0] / rms_numel + rms_eps)
344
- for j in T.Parallel(hc_mult3):
345
- mixes[j] = 0
346
- for i_split in T.serial(n_splits):
347
- mixes[j] += gemm_out_mul[i_split, i, j]
348
- mixes[j] *= rms[0]
339
mixes_shared = T.alloc_shared(hc_mult3, T.float32)
350
- T.copy(mixes, mixes_shared)
340
+ if split_mode != "input":
341
+ rms = T.alloc_fragment(1, T.float32)
342
+ mixes = T.alloc_fragment(hc_mult3, T.float32)
343
+ T.clear(mixes)
344
+ rms[0] = 0
345
352
- if T.get_thread_binding() < 32:
346
+ for i_split in T.serial(n_splits):
347
+ rms[0] += gemm_out_sqrsum[i_split, i]
348
+ rms[0] = T.rsqrt(rms[0] / rms_numel + rms_eps)
349
+ for j in T.Parallel(hc_mult3):
350
+ mixes[j] = 0
351
+ for i_split in T.serial(n_splits):
352
+ mixes[j] += gemm_out_mul[i_split, i, j]
353
+ mixes[j] *= rms[0]
354
+ T.copy(mixes, mixes_shared)
355
+
356
+ if split_mode != "input" and T.get_thread_binding() < 32:
357
cm = T.alloc_fragment((hc_mult, hc_mult), T.float32)
358
for j in T.Parallel(hc_mult):
359
if save_pre_mix:
@@ -395,7 +399,7 @@ def mhc_pre_big_fuse_with_norm_tilelang(
399
400
for j, k in T.Parallel(hc_mult, hc_mult):
401
comb_mix[i, j * hc_mult + k] = cm[j, k]
398
- else:
402
+ elif split_mode != "stats" and T.get_thread_binding() >= 32:
403
pre_mix_shared = T.alloc_shared(hc_mult, T.float32)
404
for j in T.Parallel(hc_mult):
405
if use_pre_mix_in:
vllm/model_executor/kernels/mhc/warmup.py
+1
@@ -36,6 +36,7 @@ class MHCPreNormKernel(VllmJitKernel["MHCPreNormKernel.CompileKey"]):
36
use_pre_mix_in: bool
37
rms_numel: int
38
save_pre_mix: bool = True
39
+ split_mode: str = "fused"
40
write_aux: bool = False
41
42
kernel: Any = staticmethod(mhc_pre_big_fuse_with_norm_tilelang)
vllm/models/deepseek_v41/nvidia/model.py
+60
-16
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import typing
4
from collections.abc import Callable, Iterable
5
+from functools import partial
6
from itertools import islice
7
8
import regex as re
@@ -17,7 +18,11 @@ from vllm.distributed import (
18
get_tensor_model_parallel_rank,
19
get_tensor_model_parallel_world_size,
20
)
20
-from vllm.forward_context import get_forward_context, is_forward_context_available
21
+from vllm.forward_context import (
22
+ get_forward_context,
23
+ in_piecewise_cudagraph,
24
+ is_forward_context_available,
25
+)
26
from vllm.logger import init_logger
27
from vllm.model_executor.kernels.mhc.tilelang import (
28
mhc_post_tilelang,
@@ -84,6 +89,11 @@ from ..common.engram import EngramLayout, NgramHashState
89
from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID, image_sentinel_mask
90
from .engram import Engram, gather_engram_hashes
91
from .ops.mega_mhc import mhc_shifted_post_pre
92
+from .ops.mhc import (
93
+ MHC_OVERLAP_MAX_TOKENS,
94
+ mhc_pre_delayed_overlap,
95
+ supports_mhc_overlap,
96
+)
97
98
if typing.TYPE_CHECKING:
99
from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
@@ -188,11 +198,13 @@ class DeepseekV4DecoderLayer(nn.Module):
198
aux_stream_list: list[torch.cuda.Stream] | None = None,
199
candidate_block_buffer: torch.Tensor | None = None,
200
engram_layout: EngramLayout | None = None,
201
+ mhc_stream: torch.cuda.Stream | None = None,
202
):
203
super().__init__()
204
205
config = vllm_config.model_config.hf_config
206
self.hidden_size = config.hidden_size
207
+ self.mhc_stream = mhc_stream
208
self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
209
210
self.engram: Engram | None = None
@@ -269,19 +281,30 @@ class DeepseekV4DecoderLayer(nn.Module):
281
variants.append(
282
{**hc_stream, "use_pre_mix_in": True, "write_aux": True}
283
)
284
+ # Shifted mHC splits the epilogue in two: the input collapse on the
285
+ # caller stream and coefficient generation on a side stream.
286
+ split_modes = (
287
+ ("fused", "stats", "input") if mhc_stream is not None else ("fused",)
288
+ )
289
for variant in variants:
273
- MHC_PRE_NORM_KERNEL.register_warmup(
274
- max_tokens=max_tokens,
275
- hidden_size=self.hidden_size,
276
- rms_eps=self.rms_norm_eps,
277
- hc_pre_eps=self.hc_eps,
278
- hc_sinkhorn_eps=self.hc_eps,
279
- hc_post_mult_value=self.hc_post_alpha,
280
- sinkhorn_repeat=self.hc_sinkhorn_iters,
281
- norm_eps=self.rms_norm_eps,
282
- hc_mult=self.hc_mult,
283
- **variant,
284
- )
290
+ for split_mode in split_modes:
291
+ MHC_PRE_NORM_KERNEL.register_warmup(
292
+ split_mode=split_mode,
293
+ max_tokens=(
294
+ max_tokens
295
+ if split_mode == "fused"
296
+ else min(max_tokens, MHC_OVERLAP_MAX_TOKENS)
297
+ ),
298
+ hidden_size=self.hidden_size,
299
+ rms_eps=self.rms_norm_eps,
300
+ hc_pre_eps=self.hc_eps,
301
+ hc_sinkhorn_eps=self.hc_eps,
302
+ hc_post_mult_value=self.hc_post_alpha,
303
+ sinkhorn_repeat=self.hc_sinkhorn_iters,
304
+ norm_eps=self.rms_norm_eps,
305
+ hc_mult=self.hc_mult,
306
+ **variant,
307
+ )
308
mix_hc = (2 + self.hc_mult) * self.hc_mult
309
hc_dim = self.hc_mult * self.hidden_size
310
self.hc_attn_fn = nn.Parameter(
@@ -351,6 +374,18 @@ class DeepseekV4DecoderLayer(nn.Module):
374
torch.Tensor | None,
375
]:
376
previous_aux: torch.Tensor | None = None
377
+ mhc_stream = self.mhc_stream
378
+ if mhc_stream is not None and (
379
+ in_piecewise_cudagraph()
380
+ or not 0 < positions.shape[0] <= MHC_OVERLAP_MAX_TOKENS
381
+ ):
382
+ # A side stream cannot remain unjoined across breakable graph segments.
383
+ mhc_stream = None
384
+ mhc_pre = (
385
+ partial(mhc_pre_delayed_overlap, stream=mhc_stream)
386
+ if mhc_stream is not None
387
+ else mhc_pre_delayed_tilelang
388
+ )
389
# The reference collapses each sublayer's input with the *previous*
390
# sublayer's pre-mix: attention uses the pre-mix carried in (identity
391
# for the first layer), the FFN uses this layer's attention pre-mix.
@@ -360,7 +395,7 @@ class DeepseekV4DecoderLayer(nn.Module):
395
# copies and the identity pre-mix selects copy 0.
396
assert self.hc_attn_fn_broadcast is not None
397
residual = x.unsqueeze(1).expand(-1, self.hc_mult, -1).contiguous()
363
- post_mix, res_mix, x, attn_pre = mhc_pre_delayed_tilelang(
398
+ post_mix, res_mix, x, attn_pre = mhc_pre(
399
residual,
400
self.hc_attn_fn_broadcast,
401
self.hc_attn_scale,
@@ -376,7 +411,7 @@ class DeepseekV4DecoderLayer(nn.Module):
411
)
412
else:
413
residual = x
379
- post_mix, res_mix, x, attn_pre = mhc_pre_delayed_tilelang(
414
+ post_mix, res_mix, x, attn_pre = mhc_pre(
415
residual,
416
self.hc_attn_fn,
417
self.hc_attn_scale,
@@ -403,7 +438,7 @@ class DeepseekV4DecoderLayer(nn.Module):
438
engram_hashes[:, self.engram.layer_hash_index],
439
engram_mask,
440
)
406
- post_mix, res_mix, x, attn_pre = mhc_pre_delayed_tilelang(
441
+ post_mix, res_mix, x, attn_pre = mhc_pre(
442
residual,
443
self.hc_attn_fn,
444
self.hc_attn_scale,
@@ -437,6 +472,7 @@ class DeepseekV4DecoderLayer(nn.Module):
472
norm_weight=self.attn_norm.weight,
473
norm_eps=self.attn_norm.variance_epsilon,
474
capture_aux=capture_previous_aux,
475
+ stream=mhc_stream,
476
)
477
if capture_previous_aux:
478
previous_aux = aux
@@ -448,6 +484,8 @@ class DeepseekV4DecoderLayer(nn.Module):
484
if self.use_sequence_parallel:
485
x = sp_reduce_scatter(x)
486
487
+ if mhc_stream is not None:
488
+ torch.cuda.current_stream().wait_stream(mhc_stream)
489
residual, post_mix, res_mix, x, ffn_pre, _ = mhc_shifted_post_pre(
490
x,
491
residual,
@@ -464,8 +502,11 @@ class DeepseekV4DecoderLayer(nn.Module):
502
pre_mix=attn_pre,
503
norm_weight=self.ffn_norm.weight,
504
norm_eps=self.ffn_norm.variance_epsilon,
505
+ stream=mhc_stream,
506
)
507
x = self.ffn(x, input_ids, mega_gate_metadata)
508
+ if mhc_stream is not None:
509
+ torch.cuda.current_stream().wait_stream(mhc_stream)
510
return x, residual, post_mix, res_mix, ffn_pre, previous_aux
511
512
@@ -497,6 +538,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
538
# (compressor kv_score, indexer.weights_proj). fused_wqa_wkv stays on
539
# the default stream.
540
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
541
+ # Keep mHC independent of the streams used inside attention.
542
+ mhc_stream = torch.cuda.Stream() if supports_mhc_overlap(vllm_config) else None
543
544
# Reserved topk indices buffer for all Indexer layers to reuse.
545
self.topk_indices_buffer = torch.empty(
@@ -541,6 +584,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
584
aux_stream_list=aux_stream_list,
585
candidate_block_buffer=self.candidate_block_buffer,
586
engram_layout=self.engram_layout,
587
+ mhc_stream=mhc_stream,
588
),
589
prefix=f"{prefix}.layers",
590
)
vllm/models/deepseek_v41/nvidia/ops/mega_mhc.py
+29
-1
@@ -7,6 +7,7 @@ import torch
7
8
from vllm.model_executor.kernels.mhc.tilelang import (
9
mhc_fused_post_pre_delayed_tilelang,
10
+ mhc_post_tilelang,
11
)
12
from vllm.platforms import current_platform
13
from vllm.utils.deep_gemm import (
@@ -106,10 +107,37 @@ def mhc_shifted_post_pre(
107
norm_weight: torch.Tensor | None = None,
108
norm_eps: float = 1e-6,
109
capture_aux: bool = False,
110
+ *,
111
+ stream: torch.cuda.Stream | None = None,
112
) -> tuple[
113
torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
114
]:
112
- """Use Mega-mHC for carried mixing, retaining TileLang for aux capture."""
115
+ """Dispatch shifted post/pre to overlap, Mega-mHC, or fused TileLang.
116
+
117
+ When stream is supplied, join it before consuming the returned coefficients.
118
+ """
119
+ if stream is not None:
120
+ from .mhc import mhc_pre_delayed_overlap
121
+
122
+ residual = mhc_post_tilelang(x, residual, post_layer_mix, comb_res_mix)
123
+ aux = residual.mean(dim=1) if capture_aux else x.new_empty(0, x.shape[1])
124
+ pre_outputs = mhc_pre_delayed_overlap(
125
+ residual,
126
+ fn,
127
+ hc_scale,
128
+ hc_base,
129
+ rms_eps,
130
+ hc_pre_eps,
131
+ hc_sinkhorn_eps,
132
+ hc_post_mult_value,
133
+ sinkhorn_repeat,
134
+ pre_mix=pre_mix,
135
+ norm_weight=norm_weight,
136
+ norm_eps=norm_eps,
137
+ stream=stream,
138
+ )
139
+ return residual, *pre_outputs, aux
140
+
141
if (
142
pre_mix is not None
143
and norm_weight is not None
vllm/models/deepseek_v41/nvidia/ops/mhc.py
new
+117
@@ -0,0 +1,117 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Split shifted mHC pre so coefficient generation can overlap the sublayer."""
4
+
5
+from functools import partial
6
+
7
+import torch
8
+
9
+from vllm.config import VllmConfig
10
+from vllm.platforms import current_platform
11
+from vllm.utils.deep_gemm import is_deep_gemm_supported
12
+
13
+# GB200 TP4/FlashInfer improves through 16 tokens; larger screens tie or regress.
14
+MHC_OVERLAP_MAX_TOKENS = 16
15
+
16
+
17
+def supports_mhc_overlap(vllm_config: VllmConfig) -> bool:
18
+ """Check kernel requirements and safety of sharing the coefficient stream."""
19
+ config = vllm_config.model_config.hf_config
20
+ # DeepGEMM's prenorm kernel requires K % 64 == 0, N % 8 == 0, and N <= 32.
21
+ mix_size = config.hc_mult * (config.hc_mult + 2)
22
+ return (
23
+ current_platform.is_device_capability_family(100)
24
+ and is_deep_gemm_supported()
25
+ and config.hidden_size % 64 == 0
26
+ and 0 < mix_size <= 32
27
+ and mix_size % 8 == 0
28
+ and not vllm_config.parallel_config.use_ubatching
29
+ )
30
+
31
+
32
+def mhc_pre_delayed_overlap(
33
+ residual: torch.Tensor,
34
+ fn: torch.Tensor,
35
+ hc_scale: torch.Tensor,
36
+ hc_base: torch.Tensor,
37
+ rms_eps: float,
38
+ hc_pre_eps: float,
39
+ hc_sinkhorn_eps: float,
40
+ hc_post_mult_value: float,
41
+ sinkhorn_repeat: int,
42
+ pre_mix: torch.Tensor | None = None,
43
+ x: torch.Tensor | None = None,
44
+ norm_weight: torch.Tensor | None = None,
45
+ norm_eps: float = 1e-6,
46
+ *,
47
+ stream: torch.cuda.Stream,
48
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
49
+ """Prepare the input on the caller stream and coefficients on another stream.
50
+
51
+ Returns post mix, residual mix, normalized input, and next pre-mix. Only the
52
+ input is ready on the caller stream; join stream before using coefficients.
53
+ The caller must retain the weights and coefficient outputs until the join.
54
+ """
55
+ from vllm.model_executor.kernels.mhc.warmup import (
56
+ MHC_PRE_NORM_KERNEL,
57
+ compute_mhc_pre_num_splits,
58
+ )
59
+ from vllm.utils.deep_gemm import tf32_hc_prenorm_gemm
60
+
61
+ n, hc, hidden = residual.shape
62
+ assert residual.is_contiguous() and residual.dtype == torch.bfloat16
63
+ assert norm_weight is not None
64
+ if x is None:
65
+ x = residual.view(n, hc * hidden)
66
+ assert x.is_contiguous()
67
+ post = torch.empty((n, hc), device=residual.device, dtype=torch.float32)
68
+ comb = torch.empty((n, hc * hc), device=residual.device, dtype=torch.float32)
69
+ next_pre = torch.empty_like(post)
70
+ layer_input = torch.empty((n, hidden), device=residual.device, dtype=torch.bfloat16)
71
+ outputs = post.unsqueeze(-1), comb.view(n, hc, hc), layer_input, next_pre
72
+ if n == 0:
73
+ return outputs
74
+ splits = compute_mhc_pre_num_splits(x.shape[1], n)
75
+ mix = torch.empty(
76
+ (splits, n, hc * (hc + 2)), device=residual.device, dtype=torch.float32
77
+ )
78
+ sqr = torch.empty((splits, n), device=residual.device, dtype=torch.float32)
79
+ epilogue = partial(
80
+ MHC_PRE_NORM_KERNEL,
81
+ mix,
82
+ sqr,
83
+ hc_scale,
84
+ hc_base,
85
+ residual,
86
+ post,
87
+ comb,
88
+ layer_input,
89
+ norm_weight,
90
+ pre_mix if pre_mix is not None else post,
91
+ next_pre,
92
+ layer_input, # Unused aux output in split modes.
93
+ hidden_size=hidden,
94
+ rms_eps=rms_eps,
95
+ hc_pre_eps=hc_pre_eps,
96
+ hc_sinkhorn_eps=hc_sinkhorn_eps,
97
+ hc_post_mult_value=hc_post_mult_value,
98
+ sinkhorn_repeat=sinkhorn_repeat,
99
+ norm_eps=norm_eps,
100
+ hc_mult=hc,
101
+ use_pre_mix_in=pre_mix is not None,
102
+ save_pre_mix=True,
103
+ rms_numel=x.shape[1],
104
+ )
105
+ main = torch.cuda.current_stream()
106
+ # Prioritize input readiness for small batches before releasing statistics.
107
+ if n <= 8:
108
+ epilogue(split_mode="input")
109
+ stream.wait_stream(main)
110
+ with torch.cuda.stream(stream):
111
+ tf32_hc_prenorm_gemm(x, fn, mix, sqr, splits)
112
+ epilogue(split_mode="stats")
113
+ for tensor in (x, mix, sqr):
114
+ tensor.record_stream(stream)
115
+ if n > 8:
116
+ epilogue(split_mode="input")
117
+ return outputs