[Perf][DSV4.1] Fuse TP all-reduce with mHC input preparation (#57643)
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Codex <noreply@openai.com>
Woosuk Kwon committed
Sep 20, 2026 at 20:28 UTC
9a70c233cd5d3ef3b14d79c582a6f9ddfa03cf80
9 files changed
+549
-105
CMakeLists.txt
+2
@@ -460,6 +460,8 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
460
DEFINED CMAKE_CUDA_COMPILER_VERSION AND
461
CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0)
462
463
+ list(APPEND VLLM_STABLE_EXT_SRC "csrc/libtorch_stable/all_reduce_mhc.cu")
464
+
465
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
466
cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS
467
"9.0a;10.0f;10.1f;10.3f;10.7f;11.0f;12.0f;12.1f" "${CUDA_ARCHS}")
csrc/libtorch_stable/all_reduce_mhc.cu
new
+212
@@ -0,0 +1,212 @@
1
+// SPDX-License-Identifier: Apache-2.0
2
+// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+#include "torch_utils.h"
5
+#include "custom_all_gather_reduce_scatter.cuh"
6
+#include <cooperative_groups.h>
7
+#include <torch/csrc/stable/library.h>
8
+
9
+namespace {
10
+using Tensor = torch::stable::Tensor;
11
+using Pack = vllm::packed_t<nv_bfloat16>::P;
12
+constexpr int kHidden = 5120;
13
+constexpr int kRanks = 4;
14
+constexpr int kThreads = 128;
15
+constexpr int kCluster = 5;
16
+constexpr int kPacks = kHidden / Pack::size;
17
+
18
+__global__ __launch_bounds__(kThreads)
19
+ __cluster_dims__(1, kCluster, 1) void all_reduce_mhc_kernel(
20
+ const Pack* input, const Pack* residual, const float* post,
21
+ const float* comb, const float* pre, const Pack* weight, Pack* output,
22
+ Pack* normalized, Pack* local, Pack* multicast, uint32_t* epochs,
23
+ int rank, int stage_size, float eps) {
24
+#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900
25
+ cudaGridDependencySynchronize();
26
+ const int token = blockIdx.x;
27
+ const int column = blockIdx.y * kThreads + threadIdx.x;
28
+ const int idx = token * kPacks + column;
29
+ const int size = gridDim.x * kPacks;
30
+ const int stage = epochs[0] % 3;
31
+ const int dirty = vllm::mnnvl_lamport_dirty_stage(stage);
32
+ const int dirty_size = epochs[2 + dirty];
33
+ Pack value = input[idx];
34
+ vllm::store_multimem_lamport_payload(
35
+ multicast + stage * stage_size + rank * size + idx,
36
+ vllm::sanitize_lamport_payload(value));
37
+ // Multi-token forwards benefit from launching dependents before mHC finishes.
38
+ if (gridDim.x > 1) cudaTriggerProgrammaticLaunchCompletion();
39
+ vllm::lamport_cta_arrive(epochs + 1);
40
+ for (int i = idx; i < dirty_size; i += size) {
41
+ local[dirty * stage_size + i] = vllm::lamport_sentinel<Pack>();
42
+ }
43
+ Pack peers[kRanks];
44
+ bool pending;
45
+ do {
46
+ vllm::LamportPack<Pack> packets[kRanks];
47
+ #pragma unroll
48
+ for (int peer = 0; peer < kRanks; ++peer) {
49
+ auto ptr = local + stage * stage_size + idx + peer * size;
50
+ asm volatile("ld.relaxed.sys.global.v4.u32 {%0,%1,%2,%3}, [%4];"
51
+ : "=r"(packets[peer].words[0]), "=r"(packets[peer].words[1]),
52
+ "=r"(packets[peer].words[2]), "=r"(packets[peer].words[3])
53
+ : "l"(ptr)
54
+ : "memory");
55
+ }
56
+ pending = false;
57
+ #pragma unroll
58
+ for (int peer = 0; peer < kRanks; ++peer) {
59
+ pending |= vllm::is_lamport_dirty(packets[peer]);
60
+ peers[peer] = packets[peer].packed;
61
+ }
62
+ } while (pending);
63
+ auto sum = vllm::upcast(peers[0]);
64
+ #pragma unroll
65
+ for (int r = 1; r < kRanks; ++r) {
66
+ vllm::packed_assign_add(sum, vllm::upcast(peers[r]));
67
+ }
68
+ // Keep the BF16 boundary between the collective and mHC post.
69
+ auto reduced = vllm::upcast(vllm::downcast<Pack>(sum));
70
+ decltype(sum) streams[4];
71
+ #pragma unroll
72
+ for (int r = 0; r < 4; ++r) {
73
+ streams[r] = vllm::upcast(residual[(token * 4 + r) * kPacks + column]);
74
+ }
75
+ decltype(sum) collapsed{};
76
+ #pragma unroll
77
+ for (int c = 0; c < 4; ++c) {
78
+ decltype(sum) mixed;
79
+ #pragma unroll
80
+ for (int e = 0; e < Pack::size; ++e) {
81
+ // Match post*x followed by the four residual FMAs.
82
+ float acc = __fmul_rn(post[token * 4 + c], reduced.data[e]);
83
+ #pragma unroll
84
+ for (int r = 0; r < 4; ++r) {
85
+ acc = fmaf(comb[token * 16 + r * 4 + c], streams[r].data[e], acc);
86
+ }
87
+ mixed.data[e] = acc;
88
+ }
89
+ Pack rounded = vllm::downcast<Pack>(mixed);
90
+ output[(token * 4 + c) * kPacks + column] = rounded;
91
+ auto as_float = vllm::upcast(rounded);
92
+ #pragma unroll
93
+ for (int e = 0; e < Pack::size; ++e) {
94
+ collapsed.data[e] =
95
+ fmaf(pre[token * 4 + c], as_float.data[e], collapsed.data[e]);
96
+ }
97
+ }
98
+ // RMSNorm consumes the rounded BF16 collapse in the unfused path.
99
+ collapsed = vllm::upcast(vllm::downcast<Pack>(collapsed));
100
+ float sqr = 0;
101
+ #pragma unroll
102
+ for (int e = 0; e < Pack::size; ++e) {
103
+ sqr = fmaf(collapsed.data[e], collapsed.data[e], sqr);
104
+ }
105
+ #pragma unroll
106
+ for (int delta = 16; delta; delta /= 2) {
107
+ sqr += __shfl_xor_sync(0xffffffff, sqr, delta);
108
+ }
109
+ __shared__ float warp_sums[kCluster][kThreads / 32];
110
+ auto cluster = cooperative_groups::this_cluster();
111
+ const int lane = threadIdx.x % 32;
112
+ if (lane < kCluster) {
113
+ *cluster.map_shared_rank(&warp_sums[blockIdx.y][threadIdx.x / 32], lane) =
114
+ sqr;
115
+ }
116
+ cluster.sync();
117
+ float total = 0;
118
+ #pragma unroll
119
+ for (int c = 0; c < kCluster; ++c) {
120
+ #pragma unroll
121
+ for (int w = 0; w < kThreads / 32; ++w) total += warp_sums[c][w];
122
+ }
123
+ const float scale = rsqrtf(__fadd_rn(total / kHidden, eps));
124
+ auto weights = vllm::upcast(weight[column]);
125
+ #pragma unroll
126
+ for (int e = 0; e < Pack::size; ++e) {
127
+ collapsed.data[e] = collapsed.data[e] * scale * weights.data[e];
128
+ }
129
+ normalized[idx] = vllm::downcast<Pack>(collapsed);
130
+ if (idx == 0) {
131
+ while (*reinterpret_cast<volatile uint32_t*>(epochs + 1) <
132
+ gridDim.x * gridDim.y) {
133
+ }
134
+ epochs[2 + stage] = kRanks * size;
135
+ epochs[0] = vllm::mnnvl_lamport_next_stage(stage);
136
+ epochs[1] = 0;
137
+ }
138
+ if (gridDim.x == 1) cudaTriggerProgrammaticLaunchCompletion();
139
+#else
140
+ asm volatile("trap;");
141
+#endif
142
+}
143
+
144
+void all_reduce_mhc(Tensor input, Tensor residual, Tensor post, Tensor comb,
145
+ Tensor pre, Tensor weight, Tensor output, Tensor normalized,
146
+ int64_t local, int64_t multicast, Tensor epochs,
147
+ int64_t rank, int64_t stage_bytes, double eps) {
148
+ using torch::headeronly::ScalarType;
149
+ const torch::stable::accelerator::DeviceGuard guard(input.get_device_index());
150
+ const int64_t n = input.size(0);
151
+ STD_TORCH_CHECK(input.dim() == 2 && input.size(1) == kHidden && n > 0 &&
152
+ n <= 16);
153
+ for (const auto& t : {input, residual, weight, output, normalized}) {
154
+ STD_TORCH_CHECK(t.is_contiguous() &&
155
+ t.scalar_type() == ScalarType::BFloat16);
156
+ STD_TORCH_CHECK(t.get_device_index() == input.get_device_index());
157
+ STD_TORCH_CHECK(reinterpret_cast<uintptr_t>(t.const_data_ptr()) % 16 == 0);
158
+ }
159
+ for (const auto& t : {post, comb, pre}) {
160
+ STD_TORCH_CHECK(t.is_contiguous() && t.scalar_type() == ScalarType::Float);
161
+ STD_TORCH_CHECK(t.get_device_index() == input.get_device_index());
162
+ }
163
+ STD_TORCH_CHECK(residual.numel() == n * 4 * kHidden &&
164
+ output.numel() == residual.numel() &&
165
+ normalized.numel() == input.numel() &&
166
+ weight.numel() == kHidden);
167
+ STD_TORCH_CHECK(post.numel() == n * 4 && pre.numel() == n * 4 &&
168
+ comb.numel() == n * 16);
169
+ STD_TORCH_CHECK(epochs.is_contiguous() &&
170
+ epochs.scalar_type() == ScalarType::Int &&
171
+ epochs.numel() >= 5 &&
172
+ epochs.get_device_index() == input.get_device_index());
173
+ STD_TORCH_CHECK(local && multicast && rank >= 0 && rank < kRanks &&
174
+ stage_bytes % 16 == 0 &&
175
+ stage_bytes >= n * kHidden * 2 * kRanks);
176
+ STD_TORCH_CHECK(get_device_prop()->major == 10);
177
+ cudaLaunchAttribute attr{};
178
+ attr.id = cudaLaunchAttributeProgrammaticStreamSerialization;
179
+ attr.val.programmaticStreamSerializationAllowed = 1;
180
+ cudaLaunchConfig_t config{
181
+ .gridDim = dim3(n, kCluster),
182
+ .blockDim = dim3(kThreads),
183
+ .dynamicSmemBytes = 0,
184
+ .stream = get_current_cuda_stream(input.get_device_index()),
185
+ .attrs = &attr,
186
+ .numAttrs = 1};
187
+ STD_CUDA_CHECK(cudaLaunchKernelEx(
188
+ &config, all_reduce_mhc_kernel,
189
+ static_cast<const Pack*>(input.const_data_ptr()),
190
+ static_cast<const Pack*>(residual.const_data_ptr()),
191
+ static_cast<const float*>(post.const_data_ptr()),
192
+ static_cast<const float*>(comb.const_data_ptr()),
193
+ static_cast<const float*>(pre.const_data_ptr()),
194
+ static_cast<const Pack*>(weight.const_data_ptr()),
195
+ static_cast<Pack*>(output.mutable_data_ptr()),
196
+ static_cast<Pack*>(normalized.mutable_data_ptr()),
197
+ reinterpret_cast<Pack*>(local), reinterpret_cast<Pack*>(multicast),
198
+ static_cast<uint32_t*>(epochs.mutable_data_ptr()), static_cast<int>(rank),
199
+ static_cast<int>(stage_bytes / sizeof(Pack)), static_cast<float>(eps)));
200
+}
201
+} // namespace
202
+
203
+STABLE_TORCH_LIBRARY_FRAGMENT(_C_custom_ar, mhc) {
204
+ mhc.def(
205
+ "all_reduce_mhc(Tensor input, Tensor residual, Tensor post, Tensor comb, "
206
+ "Tensor pre, Tensor weight, Tensor! output, Tensor! normalized, "
207
+ "int local, int multicast, Tensor! epochs, int rank, int stage_bytes, "
208
+ "float eps) -> ()");
209
+}
210
+STABLE_TORCH_LIBRARY_IMPL(_C_custom_ar, CUDA, mhc) {
211
+ mhc.impl("all_reduce_mhc", TORCH_BOX(&all_reduce_mhc));
212
+}
tests/distributed/test_custom_all_reduce.py
+111
@@ -11,6 +11,7 @@ import torch.distributed as dist
11
from vllm.distributed.communication_op import tensor_model_parallel_all_reduce # noqa
12
from vllm.distributed.device_communicators import custom_all_reduce as car
13
from vllm.distributed.parallel_state import get_tp_group, graph_capture
14
+from vllm.platforms import current_platform
15
16
from ..utils import (
17
ensure_model_parallel_initialized,
@@ -24,6 +25,116 @@ for i, v in enumerate(test_sizes):
25
test_sizes[i] -= v % 8
26
27
28
+@ray.remote(num_gpus=1, max_calls=1)
29
+def _all_reduce_mhc(monkeypatch, tp_size, pp_size, rank, distributed_init_port):
30
+ from vllm.model_executor.kernels.mhc.tilelang import (
31
+ mhc_post_tilelang,
32
+ mhc_pre_delayed_tilelang,
33
+ )
34
+
35
+ with monkeypatch.context() as m:
36
+ m.delenv("CUDA_VISIBLE_DEVICES", raising=False)
37
+ device = torch.device(f"cuda:{rank}")
38
+ torch.accelerator.set_device_index(device)
39
+ init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port)
40
+ ensure_model_parallel_initialized(tp_size, pp_size)
41
+ comm = get_tp_group().device_communicator.ca_comm
42
+ assert comm is not None and comm.mnnvl_lamport_ag_multicast_ptr
43
+ fn = torch.zeros(24, 20480, device=device)
44
+ scale = torch.ones(3, device=device)
45
+ base = torch.zeros(24, device=device)
46
+
47
+ # Changing shapes and interleaving all-gather exercise the shared
48
+ # Lamport stages, including cleanup of a larger previous payload.
49
+ def run(n):
50
+ torch.manual_seed(42 + rank)
51
+ x = torch.randn(n, 5120, device=device, dtype=torch.bfloat16)
52
+ # Packed +0/-0 pairs collide with the Lamport sentinel.
53
+ x[:, :16] = 0
54
+ x[:, 9:16:2] = -0.0
55
+ torch.manual_seed(123)
56
+ residual = torch.randn(n, 4, 5120, device=device, dtype=torch.bfloat16)
57
+ post = torch.rand(n, 4, device=device)
58
+ comb = torch.randn(n, 4, 4, device=device) * 0.1
59
+ pre = torch.rand(n, 4, device=device)
60
+ weight = torch.randn(5120, device=device, dtype=torch.bfloat16)
61
+ output = torch.empty_like(residual)
62
+ normalized = torch.empty_like(x)
63
+
64
+ def fused():
65
+ torch.ops._C_custom_ar.all_reduce_mhc(
66
+ x,
67
+ residual,
68
+ post,
69
+ comb,
70
+ pre,
71
+ weight,
72
+ output,
73
+ normalized,
74
+ comm.mnnvl_lamport_ag_local_ptr,
75
+ comm.mnnvl_lamport_ag_multicast_ptr,
76
+ comm.mnnvl_lamport_epochs[0],
77
+ rank,
78
+ comm.mnnvl_buffer_size,
79
+ 1e-6,
80
+ )
81
+
82
+ def check():
83
+ gathered = comm.custom_all_gather(x)
84
+ assert gathered is not None
85
+ peers = gathered.view(tp_size, n, 5120).float()
86
+ reduced = peers[0].clone()
87
+ for peer in peers[1:]:
88
+ reduced.add_(peer)
89
+ expected = mhc_post_tilelang(
90
+ reduced.bfloat16(), residual, post.unsqueeze(-1), comb
91
+ )
92
+ expected_norm = mhc_pre_delayed_tilelang(
93
+ expected,
94
+ fn,
95
+ scale,
96
+ base,
97
+ 1e-6,
98
+ 1e-6,
99
+ 1e-6,
100
+ 2.0,
101
+ 20,
102
+ pre_mix=pre,
103
+ norm_weight=weight,
104
+ )[2]
105
+ torch.testing.assert_close(output, expected, rtol=0.008, atol=1e-6)
106
+ torch.testing.assert_close(
107
+ normalized, expected_norm, rtol=0.008, atol=0.008
108
+ )
109
+
110
+ fused()
111
+ check()
112
+ graph = torch.cuda.CUDAGraph()
113
+ with torch.cuda.graph(graph):
114
+ for _ in range(6):
115
+ fused()
116
+ for _ in range(20):
117
+ graph.replay()
118
+ check()
119
+ x.mul_(0.5)
120
+ residual.mul_(2)
121
+ graph.replay()
122
+ check()
123
+
124
+ # Cover fixed Q6 and shrinking/growing adaptive verification batches.
125
+ for n in (1, 6, 12, 8, 16, 3, 5, 2, 4, 1):
126
+ run(n)
127
+
128
+
129
+@pytest.mark.skipif(
130
+ not current_platform.is_device_capability_family(100), reason="Requires SM100"
131
+)
132
+def test_all_reduce_mhc_preserves_bf16_boundaries_and_graph_replay(monkeypatch):
133
+ if torch.accelerator.device_count() < 4:
134
+ pytest.skip("Requires four GPUs with NVLink multicast")
135
+ multi_process_parallel(monkeypatch, 4, 1, _all_reduce_mhc)
136
+
137
+
138
@pytest.mark.parametrize(
139
("dtype", "expected"),
140
[
tests/distributed/test_engram_dp_shard.py
+1
@@ -512,6 +512,7 @@ def _check_dummy_hash_model_forward(
512
model = SimpleNamespace(
513
use_mega_moe=False,
514
use_sequence_parallel=False,
515
+ fuse_mhc_all_reduce=False,
516
engram_hash=state,
517
engram_swa_prefix="swa",
518
engram_dp_shared_memory=dp_shared_memory,
tests/kernels/test_mhc_kernels.py
+45
-1
@@ -50,6 +50,40 @@ from vllm.utils.torch_utils import set_random_seed
50
DEVICE = current_platform.device_type
51
52
53
+@pytest.mark.parametrize(
54
+ "tp,ep,hidden,hc,multicast,expected",
55
+ [
56
+ (4, False, 5120, 4, 1, True),
57
+ (2, False, 5120, 4, 1, False),
58
+ (4, True, 5120, 4, 1, False),
59
+ (4, False, 4096, 4, 1, False),
60
+ (4, False, 5120, 2, 1, False),
61
+ (4, False, 5120, 4, 0, False),
62
+ ],
63
+)
64
+def test_deepseek_v41_all_reduce_fusion_requires_kernel_support(
65
+ monkeypatch, tp, ep, hidden, hc, multicast, expected
66
+):
67
+ """Eligibility depends on kernel inputs, not serving or attention settings."""
68
+ from vllm.models.deepseek_v41.nvidia.ops import mhc
69
+
70
+ config = SimpleNamespace(
71
+ parallel_config=SimpleNamespace(
72
+ tensor_parallel_size=tp, enable_expert_parallel=ep
73
+ ),
74
+ model_config=SimpleNamespace(
75
+ hf_config=SimpleNamespace(hidden_size=hidden, hc_mult=hc)
76
+ ),
77
+ )
78
+ group = SimpleNamespace(
79
+ device_communicator=SimpleNamespace(
80
+ ca_comm=SimpleNamespace(mnnvl_lamport_ag_multicast_ptr=multicast)
81
+ )
82
+ )
83
+ monkeypatch.setattr(mhc, "get_tp_group", lambda: group)
84
+ assert mhc.supports_mhc_all_reduce(config) is expected
85
+
86
+
87
@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
88
@pytest.mark.parametrize(
89
"num_tokens,hc_mult,hidden_size",
@@ -523,6 +557,7 @@ def test_deepseek_v41_decoder_mixes_match_torch(
557
decoder.rms_norm_eps = 1e-20
558
decoder.hc_post_alpha = 2.0
559
decoder.use_sequence_parallel = False
560
+ decoder.fuse_mhc_all_reduce = False
561
decoder.mhc_stream = None
562
if mhc_mode != "disabled":
563
from vllm.utils.deep_gemm import is_deep_gemm_supported
@@ -597,7 +632,15 @@ def test_deepseek_v41_decoder_mixes_match_torch(
632
return post, res, decoder.attn_norm(collapsed), pre
633
634
def fused_reference(
600
- x, residual, post_mix, res_mix, *args, capture_aux=False, stream=None, **kw
635
+ x,
636
+ residual,
637
+ post_mix,
638
+ res_mix,
639
+ *args,
640
+ capture_aux=False,
641
+ stream=None,
642
+ reduce_results=False,
643
+ **kw,
644
):
645
residual = mhc_post_torch(x, residual, post_mix, res_mix)
646
aux = residual.mean(dim=1) if capture_aux else residual.new_empty(0)
@@ -638,6 +681,7 @@ def test_deepseek_v41_capture_previous_aux(entry, monkeypatch, default_vllm_conf
681
decoder.rms_norm_eps = 1e-6
682
decoder.hc_post_alpha = 2.0
683
decoder.use_sequence_parallel = False
684
+ decoder.fuse_mhc_all_reduce = False
685
decoder.mhc_stream = None
686
decoder.engram = None
687
from vllm.model_executor.layers.layernorm import RMSNorm
vllm/models/deepseek_v4/nvidia/model.py
+3
@@ -793,6 +793,7 @@ class DeepseekV4MoE(nn.Module):
793
use_sequence_parallel: bool = False,
794
*,
795
num_hash_layers: int,
796
+ reduce_results: bool = True,
797
n_routed_experts: int | None = None,
798
n_activated_experts: int | None = None,
799
image_sentinel_lo: int = IMAGE_SENTINEL_BASE_ID,
@@ -804,6 +805,7 @@ class DeepseekV4MoE(nn.Module):
805
quant_config = vllm_config.quant_config
806
self.prefix = prefix
807
self.use_sequence_parallel = use_sequence_parallel
808
+ self.reduce_results = reduce_results
809
moe_backend = vllm_config.kernel_config.moe_backend
810
validate_fi_moe_ep_config(vllm_config)
811
self.use_mega_moe = moe_backend in MEGA_MOE_BACKENDS
@@ -1010,6 +1012,7 @@ class DeepseekV4MoE(nn.Module):
1012
self.physical_expert_end = self.experts_end_idx
1013
1014
self.experts = FusedMoEFactory(
1015
+ reduce_results=self.reduce_results,
1016
shared_experts=self.shared_experts,
1017
gate=self.gate,
1018
num_experts=self.n_routed_experts,
vllm/models/deepseek_v41/nvidia/model.py
+19
-2
@@ -17,6 +17,7 @@ from vllm.distributed import (
17
get_pp_group,
18
get_tensor_model_parallel_rank,
19
get_tensor_model_parallel_world_size,
20
+ tensor_model_parallel_all_reduce,
21
)
22
from vllm.forward_context import (
23
get_forward_context,
@@ -88,10 +89,11 @@ from vllm.v1.worker.ubatching import dbo_current_ubatch_id
89
from ..common.engram import EngramLayout, NgramHashState
90
from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID, image_sentinel_mask
91
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
+ mhc_shifted_post_pre,
96
+ supports_mhc_all_reduce,
97
supports_mhc_overlap,
98
)
99
@@ -107,6 +109,7 @@ class DeepseekV4MoE(DeepseekV4MoEBase):
109
vllm_config: VllmConfig,
110
prefix: str = "",
111
use_sequence_parallel: bool = False,
112
+ reduce_results: bool = True,
113
):
114
config = vllm_config.model_config.hf_config
115
n_routed_experts = config.n_routed_experts
@@ -125,6 +128,7 @@ class DeepseekV4MoE(DeepseekV4MoEBase):
128
n_routed_experts=n_routed_experts,
129
n_activated_experts=n_activated_experts,
130
num_hash_layers=0,
131
+ reduce_results=reduce_results,
132
image_sentinel_lo=IMAGE_SENTINEL_BASE_ID,
133
)
134
@@ -199,12 +203,14 @@ class DeepseekV4DecoderLayer(nn.Module):
203
candidate_block_buffer: torch.Tensor | None = None,
204
engram_layout: EngramLayout | None = None,
205
mhc_stream: torch.cuda.Stream | None = None,
206
+ fuse_mhc_all_reduce: bool = False,
207
):
208
super().__init__()
209
210
config = vllm_config.model_config.hf_config
211
self.hidden_size = config.hidden_size
212
self.mhc_stream = mhc_stream
213
+ self.fuse_mhc_all_reduce = fuse_mhc_all_reduce
214
self.use_sequence_parallel = _use_sequence_parallel(vllm_config)
215
216
self.engram: Engram | None = None
@@ -228,12 +234,13 @@ class DeepseekV4DecoderLayer(nn.Module):
234
aux_stream_list=aux_stream_list,
235
candidate_block_buffer=candidate_block_buffer,
236
)
231
- if self.use_sequence_parallel:
237
+ if self.use_sequence_parallel or fuse_mhc_all_reduce:
238
self.attn.wo_b.reduce_results = False
239
self.ffn = DeepseekV4MoE(
240
vllm_config,
241
prefix=f"{prefix}.ffn",
242
use_sequence_parallel=self.use_sequence_parallel,
243
+ reduce_results=not fuse_mhc_all_reduce,
244
)
245
246
self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
@@ -430,6 +437,8 @@ class DeepseekV4DecoderLayer(nn.Module):
437
# and this block's pre, on the full hc stream, so the mix
438
# coefficients see the injected stream. The injection also keeps
439
# the post out of the pre-norm GEMM's fused prologue.
440
+ if self.fuse_mhc_all_reduce:
441
+ x = tensor_model_parallel_all_reduce(x)
442
previous_post = mhc_post_tilelang(x, residual, post_mix, res_mix)
443
if capture_previous_aux:
444
previous_aux = previous_post.mean(dim=1)
@@ -473,6 +482,7 @@ class DeepseekV4DecoderLayer(nn.Module):
482
norm_eps=self.attn_norm.variance_epsilon,
483
capture_aux=capture_previous_aux,
484
stream=mhc_stream,
485
+ reduce_results=self.fuse_mhc_all_reduce,
486
)
487
if capture_previous_aux:
488
previous_aux = aux
@@ -503,6 +513,7 @@ class DeepseekV4DecoderLayer(nn.Module):
513
norm_weight=self.ffn_norm.weight,
514
norm_eps=self.ffn_norm.variance_epsilon,
515
stream=mhc_stream,
516
+ reduce_results=self.fuse_mhc_all_reduce,
517
)
518
x = self.ffn(x, input_ids, mega_gate_metadata)
519
if mhc_stream is not None:
@@ -540,6 +551,9 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
551
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
552
# Keep mHC independent of the streams used inside attention.
553
mhc_stream = torch.cuda.Stream() if supports_mhc_overlap(vllm_config) else None
554
+ self.fuse_mhc_all_reduce = mhc_stream is not None and supports_mhc_all_reduce(
555
+ vllm_config
556
+ )
557
558
# Reserved topk indices buffer for all Indexer layers to reuse.
559
self.topk_indices_buffer = torch.empty(
@@ -585,6 +599,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
599
candidate_block_buffer=self.candidate_block_buffer,
600
engram_layout=self.engram_layout,
601
mhc_stream=mhc_stream,
602
+ fuse_mhc_all_reduce=self.fuse_mhc_all_reduce,
603
),
604
prefix=f"{prefix}.layers",
605
)
@@ -794,6 +809,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
809
aux_hidden_by_layer[idx] = previous_aux
810
if layer is not None:
811
# The last layer has no successor to fold its post into.
812
+ if self.fuse_mhc_all_reduce:
813
+ hidden_states = tensor_model_parallel_all_reduce(hidden_states)
814
hidden_states = mhc_post_tilelang(
815
hidden_states, residual, post_mix, res_mix
816
)
vllm/models/deepseek_v41/nvidia/ops/mega_mhc.py
-98
@@ -5,10 +5,6 @@ import functools
5
6
import torch
7
8
-from vllm.model_executor.kernels.mhc.tilelang import (
9
- mhc_fused_post_pre_delayed_tilelang,
10
- mhc_post_tilelang,
11
-)
8
from vllm.platforms import current_platform
9
from vllm.utils.deep_gemm import (
10
_import_deep_gemm,
@@ -88,97 +84,3 @@ def mhc_shifted_post_pre_deep_gemm(
84
y_bf16,
85
new_prev_mix.squeeze(-1),
86
)
91
-
92
-
93
-def mhc_shifted_post_pre(
94
- x: torch.Tensor,
95
- residual: torch.Tensor,
96
- post_layer_mix: torch.Tensor,
97
- comb_res_mix: torch.Tensor,
98
- fn: torch.Tensor,
99
- hc_scale: torch.Tensor,
100
- hc_base: torch.Tensor,
101
- rms_eps: float,
102
- hc_pre_eps: float,
103
- hc_sinkhorn_eps: float,
104
- hc_post_mult_value: float,
105
- sinkhorn_repeat: int,
106
- pre_mix: torch.Tensor | None = None,
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
-]:
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
144
- and not capture_aux
145
- and x.shape[0] <= 1 << 20
146
- and is_mega_mhc_supported(x.shape[1], residual.shape[1])
147
- ):
148
- outputs = mhc_shifted_post_pre_deep_gemm(
149
- x,
150
- residual,
151
- pre_mix,
152
- post_layer_mix,
153
- comb_res_mix,
154
- fn,
155
- hc_scale,
156
- hc_base,
157
- rms_eps,
158
- hc_pre_eps,
159
- hc_post_mult_value,
160
- hc_sinkhorn_eps,
161
- sinkhorn_repeat,
162
- norm_weight,
163
- norm_eps,
164
- )
165
- return *outputs, x.new_empty(0, x.shape[1])
166
-
167
- return mhc_fused_post_pre_delayed_tilelang(
168
- x,
169
- residual,
170
- post_layer_mix,
171
- comb_res_mix,
172
- fn,
173
- hc_scale,
174
- hc_base,
175
- rms_eps,
176
- hc_pre_eps,
177
- hc_sinkhorn_eps,
178
- hc_post_mult_value,
179
- sinkhorn_repeat,
180
- pre_mix=pre_mix,
181
- norm_weight=norm_weight,
182
- norm_eps=norm_eps,
183
- capture_aux=capture_aux,
184
- )
vllm/models/deepseek_v41/nvidia/ops/mhc.py
+156
-4
@@ -1,15 +1,26 @@
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."""
3
+"""Dispatch DSV4.1 mHC operations and overlap coefficient generation."""
4
5
from functools import partial
6
+from typing import TYPE_CHECKING, cast
7
8
import torch
9
10
from vllm.config import VllmConfig
11
+from vllm.distributed import get_tp_group
12
+from vllm.model_executor.kernels.mhc.tilelang import (
13
+ mhc_fused_post_pre_delayed_tilelang,
14
+ mhc_post_tilelang,
15
+)
16
from vllm.platforms import current_platform
17
from vllm.utils.deep_gemm import is_deep_gemm_supported
18
19
+from .mega_mhc import is_mega_mhc_supported, mhc_shifted_post_pre_deep_gemm
20
+
21
+if TYPE_CHECKING:
22
+ from vllm.distributed.device_communicators.cuda_communicator import CudaCommunicator
23
+
24
# GB200 TP4/FlashInfer improves through 16 tokens; larger screens tie or regress.
25
MHC_OVERLAP_MAX_TOKENS = 16
26
@@ -29,6 +40,20 @@ def supports_mhc_overlap(vllm_config: VllmConfig) -> bool:
40
)
41
42
43
+def supports_mhc_all_reduce(vllm_config: VllmConfig) -> bool:
44
+ parallel = vllm_config.parallel_config
45
+ config = vllm_config.model_config.hf_config
46
+ if (
47
+ parallel.tensor_parallel_size != 4
48
+ or parallel.enable_expert_parallel
49
+ or config.hidden_size != 5120
50
+ or config.hc_mult != 4
51
+ ):
52
+ return False
53
+ comm = cast("CudaCommunicator", get_tp_group().device_communicator).ca_comm
54
+ return comm is not None and bool(comm.mnnvl_lamport_ag_multicast_ptr)
55
+
56
+
57
def mhc_pre_delayed_overlap(
58
residual: torch.Tensor,
59
fn: torch.Tensor,
@@ -45,6 +70,7 @@ def mhc_pre_delayed_overlap(
70
norm_eps: float = 1e-6,
71
*,
72
stream: torch.cuda.Stream,
73
+ layer_input: torch.Tensor | None = None,
74
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
75
"""Prepare the input on the caller stream and coefficients on another stream.
76
@@ -67,7 +93,11 @@ def mhc_pre_delayed_overlap(
93
post = torch.empty((n, hc), device=residual.device, dtype=torch.float32)
94
comb = torch.empty((n, hc * hc), device=residual.device, dtype=torch.float32)
95
next_pre = torch.empty_like(post)
70
- layer_input = torch.empty((n, hidden), device=residual.device, dtype=torch.bfloat16)
96
+ compute_input = layer_input is None
97
+ if layer_input is None:
98
+ layer_input = torch.empty(
99
+ (n, hidden), device=residual.device, dtype=torch.bfloat16
100
+ )
101
outputs = post.unsqueeze(-1), comb.view(n, hc, hc), layer_input, next_pre
102
if n == 0:
103
return outputs
@@ -104,7 +134,7 @@ def mhc_pre_delayed_overlap(
134
)
135
main = torch.cuda.current_stream()
136
# Prioritize input readiness for small batches before releasing statistics.
107
- if n <= 8:
137
+ if compute_input and n <= 8:
138
epilogue(split_mode="input")
139
stream.wait_stream(main)
140
with torch.cuda.stream(stream):
@@ -112,6 +142,128 @@ def mhc_pre_delayed_overlap(
142
epilogue(split_mode="stats")
143
for tensor in (x, mix, sqr):
144
tensor.record_stream(stream)
115
- if n > 8:
145
+ if compute_input and n > 8:
146
epilogue(split_mode="input")
147
return outputs
148
+
149
+
150
+def mhc_shifted_post_pre(
151
+ x: torch.Tensor,
152
+ residual: torch.Tensor,
153
+ post_layer_mix: torch.Tensor,
154
+ comb_res_mix: torch.Tensor,
155
+ fn: torch.Tensor,
156
+ hc_scale: torch.Tensor,
157
+ hc_base: torch.Tensor,
158
+ rms_eps: float,
159
+ hc_pre_eps: float,
160
+ hc_sinkhorn_eps: float,
161
+ hc_post_mult_value: float,
162
+ sinkhorn_repeat: int,
163
+ pre_mix: torch.Tensor | None = None,
164
+ norm_weight: torch.Tensor | None = None,
165
+ norm_eps: float = 1e-6,
166
+ capture_aux: bool = False,
167
+ *,
168
+ stream: torch.cuda.Stream | None = None,
169
+ reduce_results: bool = False,
170
+) -> tuple[
171
+ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
172
+]:
173
+ """Dispatch shifted post/pre to overlap, Mega-mHC, or fused TileLang.
174
+
175
+ When stream is supplied, join it before consuming the returned coefficients.
176
+ """
177
+ layer_input = None
178
+ if reduce_results:
179
+ tp = get_tp_group()
180
+ if stream is not None and 0 < x.shape[0] <= MHC_OVERLAP_MAX_TOKENS:
181
+ comm = cast("CudaCommunicator", tp.device_communicator).ca_comm
182
+ assert comm is not None and comm.mnnvl_lamport_epochs is not None
183
+ output = torch.empty_like(residual)
184
+ layer_input = torch.empty_like(x)
185
+ torch.ops._C_custom_ar.all_reduce_mhc(
186
+ x,
187
+ residual,
188
+ post_layer_mix,
189
+ comb_res_mix,
190
+ pre_mix,
191
+ norm_weight,
192
+ output,
193
+ layer_input,
194
+ comm.mnnvl_lamport_ag_local_ptr,
195
+ comm.mnnvl_lamport_ag_multicast_ptr,
196
+ comm.mnnvl_lamport_epochs[0],
197
+ comm.rank,
198
+ comm.mnnvl_buffer_size,
199
+ norm_eps,
200
+ )
201
+ residual = output
202
+ else:
203
+ x = tp.all_reduce(x)
204
+ if stream is not None:
205
+ if layer_input is None:
206
+ residual = mhc_post_tilelang(x, residual, post_layer_mix, comb_res_mix)
207
+ aux = residual.mean(dim=1) if capture_aux else x.new_empty(0, x.shape[1])
208
+ pre_outputs = mhc_pre_delayed_overlap(
209
+ residual,
210
+ fn,
211
+ hc_scale,
212
+ hc_base,
213
+ rms_eps,
214
+ hc_pre_eps,
215
+ hc_sinkhorn_eps,
216
+ hc_post_mult_value,
217
+ sinkhorn_repeat,
218
+ pre_mix=pre_mix,
219
+ norm_weight=norm_weight,
220
+ norm_eps=norm_eps,
221
+ stream=stream,
222
+ layer_input=layer_input,
223
+ )
224
+ return residual, *pre_outputs, aux
225
+
226
+ if (
227
+ pre_mix is not None
228
+ and norm_weight is not None
229
+ and not capture_aux
230
+ and x.shape[0] <= 1 << 20
231
+ and is_mega_mhc_supported(x.shape[1], residual.shape[1])
232
+ ):
233
+ outputs = mhc_shifted_post_pre_deep_gemm(
234
+ x,
235
+ residual,
236
+ pre_mix,
237
+ post_layer_mix,
238
+ comb_res_mix,
239
+ fn,
240
+ hc_scale,
241
+ hc_base,
242
+ rms_eps,
243
+ hc_pre_eps,
244
+ hc_post_mult_value,
245
+ hc_sinkhorn_eps,
246
+ sinkhorn_repeat,
247
+ norm_weight,
248
+ norm_eps,
249
+ )
250
+ return *outputs, x.new_empty(0, x.shape[1])
251
+
252
+ return mhc_fused_post_pre_delayed_tilelang(
253
+ x,
254
+ residual,
255
+ post_layer_mix,
256
+ comb_res_mix,
257
+ fn,
258
+ hc_scale,
259
+ hc_base,
260
+ rms_eps,
261
+ hc_pre_eps,
262
+ hc_sinkhorn_eps,
263
+ hc_post_mult_value,
264
+ sinkhorn_repeat,
265
+ pre_mix=pre_mix,
266
+ norm_weight=norm_weight,
267
+ norm_eps=norm_eps,
268
+ capture_aux=capture_aux,
269
+ )