[Kernel][DSV4.1] Fuse MoE finalize into the TP all-reduce + mHC boundary (#58586)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Yongye Zhu committed
Sep 26, 2026 at 12:08 UTC
77871126f9b69cff9feffff390d1171a60dbd7e2
8 files changed
+1807
-309
CMakeLists.txt
-2
@@ -460,8 +460,6 @@ 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
-
463
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
464
cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS
465
"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
deleted
-212
@@ -1,212 +0,0 @@
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
+120
-73
@@ -25,12 +25,43 @@ for i, v in enumerate(test_sizes):
25
test_sizes[i] -= v % 8
26
27
28
+def _bf16_ulps(a: torch.Tensor, b: torch.Tensor) -> int:
29
+ """Largest distance between two BF16 tensors in units in the last place."""
30
+
31
+ def ordered(t: torch.Tensor) -> torch.Tensor:
32
+ bits = t.view(torch.int16).int() & 0xFFFF
33
+ return torch.where(bits >= 0x8000, 0x8000 - bits, bits)
34
+
35
+ return int((ordered(a) - ordered(b)).abs().max())
36
+
37
+
38
+def _unfused_all_reduce_mhc(peers, residual, post, comb, pre, weight, eps):
39
+ """All-reduce, mHC post, collapse and RMSNorm in the fused kernel's FP32
40
+ order, rounding to BF16 where the unfused path returns BF16."""
41
+ reduced = peers[0].float()
42
+ for peer in peers[1:]:
43
+ reduced = reduced + peer.float()
44
+ reduced = reduced.bfloat16().float()
45
+ output = torch.empty_like(residual)
46
+ collapse = torch.zeros_like(reduced)
47
+ for target in range(4):
48
+ mixed = reduced * post[:, target : target + 1]
49
+ for source in range(4):
50
+ mixed = torch.addcmul(
51
+ mixed, residual[:, source].float(), comb[:, source, target, None]
52
+ )
53
+ output[:, target] = mixed.bfloat16()
54
+ collapse = torch.addcmul(
55
+ collapse, output[:, target].float(), pre[:, target : target + 1]
56
+ )
57
+ prenorm = collapse.bfloat16().float()
58
+ inv_rms = torch.rsqrt(prenorm.square().mean(-1, keepdim=True) + eps)
59
+ return output, (prenorm * inv_rms * weight.float()).bfloat16()
60
+
61
+
62
@ray.remote(num_gpus=1, max_calls=1)
63
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
- )
64
+ from vllm.models.deepseek_v41.nvidia.ops.cute_dsl import AllReduceMHC
65
66
with monkeypatch.context() as m:
67
m.delenv("CUDA_VISIBLE_DEVICES", raising=False)
@@ -38,98 +69,114 @@ def _all_reduce_mhc(monkeypatch, tp_size, pp_size, rank, distributed_init_port):
69
torch.accelerator.set_device_index(device)
70
init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port)
71
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
72
+ op = AllReduceMHC(
73
+ hidden_size=5120, hc_mult=4, max_num_tokens=16, top_k=6, device=device
74
+ )
75
+
76
+ def check_eager_and_replayed(fused, check, halved, doubled):
77
+ check(fused())
78
+ graph = torch.cuda.CUDAGraph()
79
+ with torch.cuda.graph(graph):
80
+ for _ in range(5):
81
+ fused()
82
+ captured = fused()
83
+ for _ in range(20):
84
+ graph.replay()
85
+ check(captured)
86
+ # Replays must pick up in-place input changes.
87
+ halved.mul_(0.5)
88
+ doubled.mul_(2)
89
+ graph.replay()
90
+ check(captured)
91
+
92
+ def mhc_inputs(n):
93
torch.manual_seed(123)
94
residual = torch.randn(n, 4, 5120, device=device, dtype=torch.bfloat16)
95
post = torch.rand(n, 4, device=device)
96
comb = torch.randn(n, 4, 4, device=device) * 0.1
97
pre = torch.rand(n, 4, device=device)
98
weight = torch.randn(5120, device=device, dtype=torch.bfloat16)
61
- output = torch.empty_like(residual)
62
- normalized = torch.empty_like(x)
99
+ return residual, post, comb, pre, weight
100
+
101
+ def run(n):
102
+ torch.manual_seed(42 + rank)
103
+ x = torch.randn(n, 5120, device=device, dtype=torch.bfloat16)
104
+ # Packed +0/-0 pairs collide with the Lamport sentinel.
105
+ x[:, :16] = 0
106
+ x[:, 9:16:2] = -0.0
107
+ residual, post, comb, pre, weight = mhc_inputs(n)
108
+
109
+ def fused():
110
+ return op(x, residual, post, comb, pre, weight, 1e-6)
111
+
112
+ def check(outputs):
113
+ peers = get_tp_group().all_gather(x, dim=0).view(tp_size, n, 5120)
114
+ output, normalized = _unfused_all_reduce_mhc(
115
+ peers, residual, post, comb, pre, weight, 1e-6
116
+ )
117
+ # The mixed hc streams match bit for bit. The RMSNorm sums the
118
+ # squares in another order and uses an approximate rsqrt.
119
+ assert torch.equal(outputs[0], output)
120
+ assert _bf16_ulps(outputs[1], normalized) <= 1
121
+
122
+ check_eager_and_replayed(fused, check, x, residual)
123
+
124
+ def run_finalize(n):
125
+ torch.manual_seed(7 + rank)
126
+ # A padded permuted GEMM2 buffer, like the MoE's.
127
+ rows = n * 6 + 5
128
+ gemm2 = torch.randn(rows, 5120, device=device, dtype=torch.bfloat16)
129
+ permuted = torch.randperm(rows, device=device)[: n * 6].view(n, 6).int()
130
+ # A route to an expert this rank does not hold.
131
+ permuted[0, -1] = -1
132
+ weights = torch.rand(n, 6, device=device)
133
+ shared = torch.randn(n, 5120, device=device, dtype=torch.bfloat16)
134
+ residual, post, comb, pre, weight = mhc_inputs(n)
135
136
def fused():
65
- torch.ops._C_custom_ar.all_reduce_mhc(
66
- x,
137
+ return op.finalize(
138
+ gemm2,
139
+ weights,
140
+ permuted,
141
+ shared,
142
residual,
143
post,
144
comb,
145
pre,
146
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,
147
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
148
)
149
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.
150
+ def check(outputs):
151
+ # One FP32 FMA per route in route order, the shared add, one
152
+ # rounding, then the plain path: only the finalize differs.
153
+ acc = torch.zeros(n, 5120, device=device)
154
+ for k in range(6):
155
+ valid = (permuted[:, k] >= 0).unsqueeze(-1)
156
+ rows_k = gemm2[permuted[:, k].clamp_min(0).long()].float()
157
+ torch.addcmul(
158
+ acc,
159
+ torch.where(valid, rows_k, 0.0),
160
+ torch.where(valid, weights[:, k : k + 1], 0.0),
161
+ out=acc,
162
+ )
163
+ x = (acc + shared.float()).bfloat16()
164
+ expected = op(x, residual, post, comb, pre, weight, 1e-6)
165
+ assert torch.equal(outputs[0], expected[0])
166
+ assert torch.equal(outputs[1], expected[1])
167
+
168
+ check_eager_and_replayed(fused, check, gemm2, shared)
169
+
170
+ # Changing shapes cover shrinking and growing batches in one mailbox.
171
for n in (1, 6, 12, 8, 16, 3, 5, 2, 4, 1):
172
run(n)
173
+ run_finalize(n)
174
175
176
@pytest.mark.skipif(
177
not current_platform.is_device_capability_family(100), reason="Requires SM100"
178
)
132
-def test_all_reduce_mhc_preserves_bf16_boundaries_and_graph_replay(monkeypatch):
179
+def test_all_reduce_mhc_matches_unfused_path(monkeypatch):
180
if torch.accelerator.device_count() < 4:
181
pytest.skip("Requires four GPUs with NVLink multicast")
182
multi_process_parallel(monkeypatch, 4, 1, _all_reduce_mhc)
vllm/models/deepseek_v41/nvidia/model.py
+76
-4
@@ -33,6 +33,14 @@ from vllm.model_executor.kernels.mhc.triton import hc_collapse_triton
33
from vllm.model_executor.layers.fused_moe import (
34
fused_moe_make_expert_params_mapping,
35
)
36
+from vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe import (
37
+ TrtLlmMxfp4ExpertsModular,
38
+)
39
+from vllm.model_executor.layers.fused_moe.moe_output import (
40
+ MoEOutput,
41
+ UnfinalizedMoEOutput,
42
+)
43
+from vllm.model_executor.layers.fused_moe.runner.moe_runner import _unpack
44
from vllm.model_executor.layers.layernorm import RMSNorm
45
from vllm.model_executor.layers.logits_processor import LogitsProcessor
46
from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -94,6 +102,7 @@ from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID, image_sentinel_mask
102
from .engram import Engram, can_share_engram_tables, gather_engram_hashes
103
from .ops.mhc import (
104
MHC_OVERLAP_MAX_TOKENS,
105
+ init_mhc_all_reduce,
106
mhc_pre_delayed_overlap,
107
mhc_shifted_post_pre,
108
supports_mhc_all_reduce,
@@ -135,6 +144,48 @@ class DeepseekV4MoE(DeepseekV4MoEBase):
144
image_sentinel_lo=IMAGE_SENTINEL_BASE_ID,
145
)
146
147
+ def defer_finalize(self) -> None:
148
+ """Leave the routed top-k reduction to the next fused all-reduce + mHC.
149
+
150
+ Only the modular TRTLLM MXFP4 experts can stop after GEMM2; the fused
151
+ kernel takes as many tokens as the mHC overlap path.
152
+ """
153
+ moe_config = self.experts.moe_config
154
+ experts_cls = getattr(
155
+ self.experts.routed_experts.quant_method, "experts_cls", None
156
+ )
157
+ if (
158
+ experts_cls is TrtLlmMxfp4ExpertsModular
159
+ and self.experts.routed_scaling_factor == 1.0
160
+ ):
161
+ moe_config.defer_moe_finalize(MHC_OVERLAP_MAX_TOKENS)
162
+ if moe_config.use_deferred_moe_finalize:
163
+ logger.info_once(
164
+ "DSV4.1 mHC: MoE top-k finalize fused into the all-reduce for "
165
+ "up to %d tokens.",
166
+ MHC_OVERLAP_MAX_TOKENS,
167
+ )
168
+
169
+ def defers_finalize(self, num_tokens: int) -> bool:
170
+ return (
171
+ not self.use_mega_moe
172
+ and self.experts.moe_config.should_defer_moe_finalize(num_tokens)
173
+ )
174
+
175
+ def forward_unfinalized(
176
+ self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None
177
+ ) -> MoEOutput:
178
+ """``forward`` with the routed top-k reduction and all-reduce left open."""
179
+ # The runner's custom op returns tensors only, so run its body directly.
180
+ shared_output, routed = _unpack(
181
+ self.experts._forward_impl(
182
+ hidden_states, hidden_states, hidden_states, input_ids
183
+ )
184
+ )
185
+ assert shared_output is not None
186
+ assert isinstance(routed, UnfinalizedMoEOutput)
187
+ return MoEOutput(routed=routed, shared_output=shared_output)
188
+
189
190
def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
191
"""Pick the CUDA sparse-MLA attention class for the configured backend.
@@ -394,7 +445,7 @@ class DeepseekV4DecoderLayer(nn.Module):
445
446
def forward(
447
self,
397
- x: torch.Tensor,
448
+ x: torch.Tensor | MoEOutput,
449
positions: torch.Tensor,
450
input_ids: torch.Tensor | None,
451
pre_mix: torch.Tensor | None = None,
@@ -407,7 +458,7 @@ class DeepseekV4DecoderLayer(nn.Module):
458
capture_previous_aux: bool = False,
459
mega_gate_metadata: MegaGateRoutingMetadata | None = None,
460
) -> tuple[
410
- torch.Tensor,
461
+ torch.Tensor | MoEOutput,
462
torch.Tensor,
463
torch.Tensor,
464
torch.Tensor,
@@ -432,6 +483,7 @@ class DeepseekV4DecoderLayer(nn.Module):
483
# sublayer's pre-mix: attention uses the pre-mix carried in (identity
484
# for the first layer), the FFN uses this layer's attention pre-mix.
485
if residual is None:
486
+ assert isinstance(x, torch.Tensor)
487
if x.dim() == 2:
488
# First layer: the stream is the embedding broadcast to hc
489
# copies and the identity pre-mix selects copy 0.
@@ -471,7 +523,9 @@ class DeepseekV4DecoderLayer(nn.Module):
523
# Engram injection happens between the previous sublayer's post
524
# and this block's pre, on the full hc stream, so the mix
525
# coefficients see the injected stream. The injection also keeps
474
- # the post out of the pre-norm GEMM's fused prologue.
526
+ # the post out of the pre-norm GEMM's fused prologue. The layer
527
+ # before an engram layer finalizes its own MoE.
528
+ assert isinstance(x, torch.Tensor)
529
if self.fuse_mhc_all_reduce:
530
x = tensor_model_parallel_all_reduce(x)
531
previous_post = mhc_post_tilelang(x, residual, post_mix, res_mix)
@@ -552,7 +606,11 @@ class DeepseekV4DecoderLayer(nn.Module):
606
stream=mhc_stream,
607
reduce_results=self.fuse_mhc_all_reduce,
608
)
555
- x = self.ffn(x, input_ids, mega_gate_metadata)
609
+ if self.fuse_mhc_all_reduce and self.ffn.defers_finalize(x.shape[0]):
610
+ # The next layer's first boundary finalizes it with the all-reduce.
611
+ x = self.ffn.forward_unfinalized(x, input_ids)
612
+ else:
613
+ x = self.ffn(x, input_ids, mega_gate_metadata)
614
if mhc_stream is not None:
615
torch.cuda.current_stream().wait_stream(mhc_stream)
616
return x, residual, post_mix, res_mix, ffn_pre, previous_aux
@@ -591,6 +649,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
649
self.fuse_mhc_all_reduce = mhc_stream is not None and supports_mhc_all_reduce(
650
vllm_config
651
)
652
+ if self.fuse_mhc_all_reduce:
653
+ init_mhc_all_reduce(vllm_config)
654
655
# Reserved topk indices buffer for all Indexer layers to reuse.
656
self.topk_indices_buffer = torch.empty(
@@ -670,6 +730,16 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
730
),
731
prefix=f"{prefix}.layers",
732
)
733
+ if self.fuse_mhc_all_reduce:
734
+ # A MoE's top-k finalize folds into the next layer's first mHC
735
+ # boundary, which the last local layer lacks and an engram layer
736
+ # replaces with its own all-reduce.
737
+ local_layers = list(islice(self.layers, self.start_layer, self.end_layer))
738
+ for layer, successor in zip(local_layers, local_layers[1:]):
739
+ assert isinstance(layer, DeepseekV4DecoderLayer)
740
+ assert isinstance(successor, DeepseekV4DecoderLayer)
741
+ if successor.engram is None:
742
+ layer.ffn.defer_finalize()
743
744
# The n-gram hash needs a slot-keyed rolling store of compressed ids
745
# (chunked prefill / decode lookback); key it off the first local
@@ -874,6 +944,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
944
if self.use_sequence_parallel:
945
previous_aux = sp_all_gather(previous_aux)[:full_num_tokens]
946
aux_hidden_by_layer[idx] = previous_aux
947
+ # Without a successor boundary, the last layer finalized its own MoE.
948
+ assert isinstance(hidden_states, torch.Tensor)
949
if layer is not None:
950
# The last layer has no successor to fold its post into.
951
if self.fuse_mhc_all_reduce:
vllm/models/deepseek_v41/nvidia/ops/cute_dsl/__init__.py
new
+8
@@ -0,0 +1,8 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+"""CuTe DSL kernels for DSV4.1 mHC."""
5
+
6
+from .all_reduce_mhc import AllReduceMHC
7
+
8
+__all__ = ["AllReduceMHC"]
vllm/models/deepseek_v41/nvidia/ops/cute_dsl/all_reduce_mhc.py
new
+1134
@@ -0,0 +1,1134 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# Adapted from FlashInfer's low-latency MNNVL CuTe DSL all-reduce
4
+# (flashinfer/comm/mnnvl_cutedsl/kernel_ll, flashinfer-ai/flashinfer@139af6f6).
5
+
6
+"""Lamport all-reduce fused with DSV4.1's mHC post, collapse and RMSNorm.
7
+
8
+FlashInfer's LL kernels, copied: a publish kernel (or its MoE finalize
9
+variant) multicasts each rank's contribution into a three-generation Lamport
10
+mailbox, and a PDL-chained consumer spins until every rank's fragment lands,
11
+reduces it and normalizes. The consumer's residual add and copy are replaced by
12
+the mHC post-mix of the hc residual streams and their pre-mix collapse, which
13
+the RMSNorm then normalizes. The finalize variant also reads FP32 routing
14
+weights.
15
+"""
16
+
17
+from __future__ import annotations
18
+
19
+import math
20
+
21
+import cuda.bindings.driver as cuda
22
+import cutlass
23
+import cutlass.cute as cute
24
+import torch
25
+import torch.distributed as dist
26
+import torch.distributed._symmetric_memory as symm_mem
27
+from cutlass import BFloat16, Float32, Int32, Int64, Uint32
28
+from cutlass.cute.runtime import make_fake_compact_tensor
29
+
30
+from vllm.distributed import get_tp_group
31
+
32
+from .primitives import (
33
+ NEGATIVE_ZERO_BF16_BITS,
34
+ QUAD_BF16,
35
+ VEC_BF16,
36
+ WARP_SIZE,
37
+ bf16x4_to_packed_u32x2,
38
+ bf16x8_to_packed_u32x4,
39
+ current_cu_stream,
40
+ f32_to_bf16_bits,
41
+ fragment_has_negative_zero,
42
+ load_global_bf16_as_f32,
43
+ load_global_u32x2,
44
+ load_global_u32x4,
45
+ load_volatile_u32,
46
+ make_fake_dynamic_compact_tensor,
47
+ map_shared_to_peer,
48
+ packed_u32x2_to_bf16x4,
49
+ packed_u32x4_to_bf16x8,
50
+ sanitize_negative_zero_u32x2,
51
+ sanitize_negative_zero_u32x4,
52
+ shuffle_sync_idx_u32,
53
+ stmc_bf16x2,
54
+ stmc_bf16x4,
55
+ stmc_bf16x8,
56
+ store_global_u32,
57
+ store_global_u32x4,
58
+ store_lamport_sentinel_u32x4,
59
+ store_shared_cluster_f32,
60
+ to_cute,
61
+ to_cute_dynamic,
62
+)
63
+
64
+LAMPORT_GENERATIONS = 3
65
+NEXT_STAGE = 0
66
+ACTIVE_STAGE = 1
67
+
68
+
69
+@cute.jit
70
+def _group_leader_block_sum(
71
+ value: Float32,
72
+ warp_sums: cute.Tensor,
73
+ warps: cutlass.Constexpr[int],
74
+ leader_stride: cutlass.Constexpr[int],
75
+) -> Float32:
76
+ lane = cute.arch.lane_idx()
77
+ warp = cute.arch.warp_idx()
78
+ for offset in cutlass.range_constexpr(1, WARP_SIZE):
79
+ if cutlass.const_expr(offset >= leader_stride and (offset & (offset - 1)) == 0):
80
+ value = value + cute.arch.shuffle_sync_bfly(
81
+ value,
82
+ offset=offset,
83
+ mask=-1,
84
+ mask_and_clamp=31,
85
+ )
86
+ if lane == 0:
87
+ cute.arch.store((warp_sums + warp).llvm_ptr, value)
88
+ cute.arch.barrier()
89
+
90
+ result = Float32(0.0)
91
+ if warp == 0:
92
+ if lane < Int32(warps):
93
+ result = cute.arch.load(
94
+ (warp_sums + lane).llvm_ptr,
95
+ Float32,
96
+ )
97
+ result = cute.arch.warp_reduction_sum(result)
98
+ if lane == 0:
99
+ cute.arch.store(warp_sums.llvm_ptr, result)
100
+ cute.arch.barrier()
101
+ return cute.arch.load(warp_sums.llvm_ptr, Float32)
102
+
103
+
104
+class _SharedOnlyPublishDeviceKernel:
105
+ def __init__(
106
+ self,
107
+ *,
108
+ hidden: int,
109
+ tp: int,
110
+ rank: int,
111
+ capacity_m: int,
112
+ elements_per_thread: int,
113
+ threads: int,
114
+ release_before_store: bool,
115
+ enable_pdl: bool,
116
+ ) -> None:
117
+ if elements_per_thread not in (1, QUAD_BF16, VEC_BF16):
118
+ raise ValueError("elements_per_thread must be 1, 4, or 8")
119
+ if hidden <= 0 or hidden % elements_per_thread:
120
+ raise ValueError("hidden must divide evenly across thread fragments")
121
+ self.hidden = hidden
122
+ self.tp = tp
123
+ self.rank = rank
124
+ self.capacity_m = capacity_m
125
+ self.elements_per_thread = elements_per_thread
126
+ self.threads = threads
127
+ self.fragments = hidden // elements_per_thread
128
+ self.ctas_per_token = math.ceil(self.fragments / threads)
129
+ self.release_before_store = release_before_store
130
+ self.enable_pdl = enable_pdl
131
+
132
+ @cute.jit
133
+ def __call__(
134
+ self,
135
+ shared_output: cute.Tensor,
136
+ stage_state: cute.Tensor,
137
+ contribution_mailbox_multicast_address: Int64,
138
+ m: Int32,
139
+ stream: cuda.CUstream,
140
+ ) -> None:
141
+ self.kernel(
142
+ shared_output,
143
+ stage_state,
144
+ contribution_mailbox_multicast_address,
145
+ ).launch(
146
+ grid=(m * self.ctas_per_token, 1, 1),
147
+ block=(self.threads, 1, 1),
148
+ stream=stream,
149
+ use_pdl=self.enable_pdl,
150
+ )
151
+
152
+ @cute.kernel
153
+ def kernel(
154
+ self,
155
+ shared_output: cute.Tensor,
156
+ stage_state: cute.Tensor,
157
+ contribution_mailbox_multicast_address: Int64,
158
+ ) -> None:
159
+ block, _, _ = cute.arch.block_idx()
160
+ tidx, _, _ = cute.arch.thread_idx()
161
+ token = block // self.ctas_per_token
162
+ cta_in_token = block % self.ctas_per_token
163
+ fragment = cta_in_token * self.threads + tidx
164
+
165
+ if cutlass.const_expr(self.enable_pdl):
166
+ cute.arch.griddepcontrol_wait()
167
+
168
+ if cutlass.const_expr(self.elements_per_thread == 1):
169
+ bits = Uint32(0)
170
+ if fragment < self.fragments:
171
+ element = Int64(token) * self.hidden + Int64(fragment)
172
+ bits = f32_to_bf16_bits(
173
+ load_global_bf16_as_f32(
174
+ Int64((shared_output.iterator + element).toint())
175
+ )
176
+ )
177
+ if bits == Uint32(NEGATIVE_ZERO_BF16_BITS):
178
+ bits = Uint32(0)
179
+ partner_bits = shuffle_sync_idx_u32(
180
+ bits,
181
+ Int32(cute.arch.lane_idx() | Int32(1)),
182
+ )
183
+ packed_word = bits | (partner_bits << Uint32(16))
184
+ elif cutlass.const_expr(self.elements_per_thread == QUAD_BF16):
185
+ packed_words = cute.make_rmem_tensor(cute.make_layout((2,)), Uint32)
186
+ packed_words.fill(Uint32(0))
187
+ if fragment < self.fragments:
188
+ element = Int64(token) * self.hidden + Int64(fragment) * QUAD_BF16
189
+ pointer = cute.make_ptr(
190
+ BFloat16,
191
+ (shared_output.iterator + element).llvm_ptr,
192
+ cute.AddressSpace.gmem,
193
+ assumed_align=8,
194
+ )
195
+ packed_words.store(
196
+ sanitize_negative_zero_u32x2(load_global_u32x2(pointer))
197
+ )
198
+ else:
199
+ packed_words = cute.make_rmem_tensor(cute.make_layout((4,)), Uint32)
200
+ packed_words.fill(Uint32(0))
201
+ if fragment < self.fragments:
202
+ element = Int64(token) * self.hidden + Int64(fragment) * VEC_BF16
203
+ pointer = cute.make_ptr(
204
+ BFloat16,
205
+ (shared_output.iterator + element).llvm_ptr,
206
+ cute.AddressSpace.gmem,
207
+ assumed_align=16,
208
+ )
209
+ packed_words.store(
210
+ sanitize_negative_zero_u32x4(load_global_u32x4(pointer))
211
+ )
212
+
213
+ stage = load_volatile_u32(stage_state.iterator + NEXT_STAGE)
214
+ if cutlass.const_expr(self.release_before_store):
215
+ if block == 0 and tidx == 0:
216
+ store_global_u32(
217
+ stage_state.iterator + ACTIVE_STAGE,
218
+ stage,
219
+ )
220
+ cute.arch.barrier()
221
+ if cutlass.const_expr(self.enable_pdl):
222
+ cute.arch.griddepcontrol_launch_dependents()
223
+
224
+ if cutlass.const_expr(self.elements_per_thread == 1):
225
+ if (cute.arch.lane_idx() & Int32(1)) == Int32(
226
+ 0
227
+ ) and fragment < self.fragments:
228
+ mailbox_element = (
229
+ (Int64(stage) * self.tp + self.rank) * self.capacity_m
230
+ + Int64(token)
231
+ ) * self.hidden + Int64(fragment)
232
+ stmc_bf16x2(
233
+ contribution_mailbox_multicast_address + mailbox_element * 2,
234
+ packed_word,
235
+ )
236
+ elif cutlass.const_expr(self.elements_per_thread == QUAD_BF16):
237
+ if fragment < self.fragments:
238
+ mailbox_element = (
239
+ (Int64(stage) * self.tp + self.rank) * self.capacity_m
240
+ + Int64(token)
241
+ ) * self.hidden + Int64(fragment) * QUAD_BF16
242
+ stmc_bf16x4(
243
+ contribution_mailbox_multicast_address + mailbox_element * 2,
244
+ packed_words,
245
+ )
246
+ else:
247
+ if fragment < self.fragments:
248
+ mailbox_element = (
249
+ (Int64(stage) * self.tp + self.rank) * self.capacity_m
250
+ + Int64(token)
251
+ ) * self.hidden + Int64(fragment) * VEC_BF16
252
+ stmc_bf16x8(
253
+ contribution_mailbox_multicast_address + mailbox_element * 2,
254
+ packed_words,
255
+ )
256
+
257
+ if cutlass.const_expr(not self.release_before_store):
258
+ if block == 0 and tidx == 0:
259
+ store_global_u32(
260
+ stage_state.iterator + ACTIVE_STAGE,
261
+ stage,
262
+ )
263
+ cute.arch.barrier()
264
+ if cutlass.const_expr(self.enable_pdl):
265
+ cute.arch.griddepcontrol_launch_dependents()
266
+
267
+
268
+class _QuadFinalizePublishDeviceKernel:
269
+ def __init__(
270
+ self,
271
+ *,
272
+ hidden: int,
273
+ top_k: int,
274
+ tp: int,
275
+ rank: int,
276
+ capacity_m: int,
277
+ threads: int,
278
+ routed_scaling_factor: float,
279
+ include_shared_expert: bool,
280
+ load_shared_expert_before_pdl: bool,
281
+ enable_pdl: bool,
282
+ prefetch_group: int,
283
+ fp32_weights: bool = False,
284
+ ) -> None:
285
+ if hidden <= 0 or hidden % QUAD_BF16:
286
+ raise ValueError("hidden must be a positive multiple of 4")
287
+ self.hidden = hidden
288
+ self.top_k = top_k
289
+ self.tp = tp
290
+ self.rank = rank
291
+ self.capacity_m = capacity_m
292
+ self.threads = threads
293
+ self.routed_scaling_factor = routed_scaling_factor
294
+ self.fragments = hidden // QUAD_BF16
295
+ self.ctas_per_token = math.ceil(self.fragments / threads)
296
+ self.include_shared_expert = include_shared_expert
297
+ self.load_shared_expert_before_pdl = load_shared_expert_before_pdl
298
+ self.enable_pdl = enable_pdl
299
+ self.prefetch_group = prefetch_group
300
+ self.prefetch_groups = (top_k + prefetch_group - 1) // prefetch_group
301
+ self.fp32_weights = fp32_weights
302
+
303
+ def smem_size_in_bytes(self) -> int:
304
+ return self.top_k * 8
305
+
306
+ @cute.jit
307
+ def __call__(
308
+ self,
309
+ routed_output: cute.Tensor,
310
+ expert_weights: cute.Tensor,
311
+ permuted_indices: cute.Tensor,
312
+ shared_output: cute.Tensor,
313
+ stage_state: cute.Tensor,
314
+ contribution_mailbox_multicast_address: Int64,
315
+ m: Int32,
316
+ stream: cuda.CUstream,
317
+ ) -> None:
318
+ self.kernel(
319
+ routed_output,
320
+ expert_weights,
321
+ permuted_indices,
322
+ shared_output,
323
+ stage_state,
324
+ contribution_mailbox_multicast_address,
325
+ ).launch(
326
+ grid=(m * self.ctas_per_token, 1, 1),
327
+ block=(self.threads, 1, 1),
328
+ smem=self.smem_size_in_bytes(),
329
+ stream=stream,
330
+ use_pdl=self.enable_pdl,
331
+ )
332
+
333
+ @cute.kernel
334
+ def kernel(
335
+ self,
336
+ routed_output: cute.Tensor,
337
+ expert_weights: cute.Tensor,
338
+ permuted_indices: cute.Tensor,
339
+ shared_output: cute.Tensor,
340
+ stage_state: cute.Tensor,
341
+ contribution_mailbox_multicast_address: Int64,
342
+ ) -> None:
343
+ block, _, _ = cute.arch.block_idx()
344
+ tidx, _, _ = cute.arch.thread_idx()
345
+ token = block // self.ctas_per_token
346
+ cta_in_token = block % self.ctas_per_token
347
+ fragment = cta_in_token * self.threads + tidx
348
+
349
+ smem = cutlass.utils.SmemAllocator()
350
+ staged_indices = smem.allocate_array(Int32, self.top_k)
351
+ staged_weights = smem.allocate_array(Float32, self.top_k)
352
+ metadata_index = Int32(tidx)
353
+ while metadata_index < Int32(self.top_k):
354
+ element = Int64(token) * self.top_k + Int64(metadata_index)
355
+ row = cute.arch.load(
356
+ (permuted_indices.iterator + element).llvm_ptr,
357
+ Int32,
358
+ )
359
+ if cutlass.const_expr(self.fp32_weights):
360
+ weight = cute.arch.load(
361
+ (expert_weights.iterator + element).llvm_ptr,
362
+ Float32,
363
+ )
364
+ else:
365
+ weight = load_global_bf16_as_f32(
366
+ Int64((expert_weights.iterator + element).toint())
367
+ )
368
+ if cutlass.const_expr(self.routed_scaling_factor != 1.0):
369
+ weight = weight * Float32(self.routed_scaling_factor)
370
+ if row == Int32(-1):
371
+ weight = Float32(0.0)
372
+ cute.arch.store(
373
+ (staged_indices + metadata_index).llvm_ptr,
374
+ row,
375
+ )
376
+ cute.arch.store(
377
+ (staged_weights + metadata_index).llvm_ptr,
378
+ weight,
379
+ )
380
+ metadata_index = metadata_index + self.threads
381
+ cute.arch.barrier()
382
+
383
+ if cutlass.const_expr(self.include_shared_expert):
384
+ shared_values = cute.make_rmem_tensor(
385
+ cute.make_layout((QUAD_BF16,)), BFloat16
386
+ )
387
+ shared_values.fill(BFloat16(0.0))
388
+ if cutlass.const_expr(self.load_shared_expert_before_pdl): # noqa: SIM102
389
+ if fragment < self.fragments:
390
+ shared_element = (
391
+ Int64(token) * self.hidden + Int64(fragment) * QUAD_BF16
392
+ )
393
+ shared_pointer = cute.make_ptr(
394
+ BFloat16,
395
+ (shared_output.iterator + shared_element).llvm_ptr,
396
+ cute.AddressSpace.gmem,
397
+ assumed_align=8,
398
+ )
399
+ shared_values.store(
400
+ packed_u32x2_to_bf16x4(load_global_u32x2(shared_pointer))
401
+ )
402
+
403
+ if cutlass.const_expr(self.enable_pdl):
404
+ cute.arch.griddepcontrol_wait()
405
+
406
+ if cutlass.const_expr( # noqa: SIM102
407
+ self.include_shared_expert and not self.load_shared_expert_before_pdl
408
+ ):
409
+ if fragment < self.fragments:
410
+ shared_element = (
411
+ Int64(token) * self.hidden + Int64(fragment) * QUAD_BF16
412
+ )
413
+ shared_pointer = cute.make_ptr(
414
+ BFloat16,
415
+ (shared_output.iterator + shared_element).llvm_ptr,
416
+ cute.AddressSpace.gmem,
417
+ assumed_align=8,
418
+ )
419
+ shared_values.store(
420
+ packed_u32x2_to_bf16x4(load_global_u32x2(shared_pointer))
421
+ )
422
+
423
+ stage = load_volatile_u32(stage_state.iterator + NEXT_STAGE)
424
+ if fragment < self.fragments:
425
+ accumulator = cute.make_rmem_tensor(cute.make_layout((QUAD_BF16,)), Float32)
426
+ accumulator.fill(Float32(0.0))
427
+ if cutlass.const_expr(self.prefetch_group == 1):
428
+ for k in cutlass.range_constexpr(self.top_k):
429
+ row = cute.arch.load((staged_indices + k).llvm_ptr, Int32)
430
+ weight = cute.arch.load(
431
+ (staged_weights + k).llvm_ptr,
432
+ Float32,
433
+ )
434
+ if row != Int32(-1):
435
+ source_element = (
436
+ Int64(row) * self.hidden + Int64(fragment) * QUAD_BF16
437
+ )
438
+ source_pointer = cute.make_ptr(
439
+ BFloat16,
440
+ (routed_output.iterator + source_element).llvm_ptr,
441
+ cute.AddressSpace.gmem,
442
+ assumed_align=8,
443
+ )
444
+ accumulator.store(
445
+ accumulator.load()
446
+ + packed_u32x2_to_bf16x4(
447
+ load_global_u32x2(source_pointer)
448
+ ).to(Float32)
449
+ * weight
450
+ )
451
+ else:
452
+ inputs = cute.make_rmem_tensor(
453
+ cute.make_layout((self.prefetch_group, 2)),
454
+ Uint32,
455
+ )
456
+ inputs.fill(Uint32(0))
457
+ for group in cutlass.range_constexpr(self.prefetch_groups):
458
+ for item in cutlass.range_constexpr(self.prefetch_group):
459
+ k = group * self.prefetch_group + item
460
+ if cutlass.const_expr(k < self.top_k):
461
+ row = cute.arch.load(
462
+ (staged_indices + k).llvm_ptr,
463
+ Int32,
464
+ )
465
+ for word in cutlass.range_constexpr(2):
466
+ inputs[item, word] = Uint32(0)
467
+ if row != Int32(-1):
468
+ source_element = (
469
+ Int64(row) * self.hidden
470
+ + Int64(fragment) * QUAD_BF16
471
+ )
472
+ source_pointer = cute.make_ptr(
473
+ BFloat16,
474
+ (routed_output.iterator + source_element).llvm_ptr,
475
+ cute.AddressSpace.gmem,
476
+ assumed_align=8,
477
+ )
478
+ source = load_global_u32x2(source_pointer)
479
+ for word in cutlass.range_constexpr(2):
480
+ inputs[item, word] = source[word]
481
+ for item in cutlass.range_constexpr(self.prefetch_group):
482
+ k = group * self.prefetch_group + item
483
+ if cutlass.const_expr(k < self.top_k):
484
+ source = cute.make_rmem_tensor(
485
+ cute.make_layout((2,)),
486
+ Uint32,
487
+ )
488
+ for word in cutlass.range_constexpr(2):
489
+ source[word] = inputs[item, word]
490
+ accumulator.store(
491
+ accumulator.load()
492
+ + packed_u32x2_to_bf16x4(source.load()).to(Float32)
493
+ * cute.arch.load(
494
+ (staged_weights + k).llvm_ptr,
495
+ Float32,
496
+ )
497
+ )
498
+ result = accumulator.load()
499
+ if cutlass.const_expr(self.include_shared_expert):
500
+ result = result + shared_values.load().to(Float32)
501
+ result_packed = bf16x4_to_packed_u32x2(result.to(BFloat16))
502
+ packed = sanitize_negative_zero_u32x2(result_packed)
503
+ mailbox_element = (
504
+ (Int64(stage) * self.tp + self.rank) * self.capacity_m + Int64(token)
505
+ ) * self.hidden + Int64(fragment) * QUAD_BF16
506
+ stmc_bf16x4(
507
+ contribution_mailbox_multicast_address + mailbox_element * 2,
508
+ packed,
509
+ )
510
+
511
+ if block == 0 and tidx == 0:
512
+ store_global_u32(
513
+ stage_state.iterator + ACTIVE_STAGE,
514
+ stage,
515
+ )
516
+ cute.arch.barrier()
517
+ if cutlass.const_expr(self.enable_pdl):
518
+ cute.arch.griddepcontrol_launch_dependents()
519
+
520
+
521
+class _LamportMHCDeviceKernel:
522
+ """FlashInfer's ``_LamportResidualRMSNormDeviceKernel`` with mHC.
523
+
524
+ The residual add and residual copy become the mHC post-mix of the hc
525
+ streams, stored as the residual output, and their pre-mix collapse, which
526
+ the unchanged RMSNorm normalizes.
527
+ """
528
+
529
+ def __init__(
530
+ self,
531
+ *,
532
+ hidden: int,
533
+ hc: int,
534
+ tp: int,
535
+ capacity_m: int,
536
+ cluster_size: int,
537
+ rank_lanes: int,
538
+ threads: int,
539
+ enable_pdl: bool,
540
+ ) -> None:
541
+ if rank_lanes not in (1, 2, 4, 8):
542
+ raise ValueError("rank_lanes must be 1, 2, 4, or 8")
543
+ if tp % rank_lanes:
544
+ raise ValueError("tp must be divisible by rank_lanes")
545
+ if threads <= 0 or threads % WARP_SIZE or threads % rank_lanes:
546
+ raise ValueError("threads must be a positive warp and rank-lane multiple")
547
+ if hidden <= 0 or hidden % VEC_BF16:
548
+ raise ValueError("hidden must be a positive multiple of 8")
549
+ self.hidden = hidden
550
+ self.hc = hc
551
+ self.tp = tp
552
+ self.capacity_m = capacity_m
553
+ self.cluster_size = cluster_size
554
+ self.rank_lanes = rank_lanes
555
+ self.threads = threads
556
+ self.enable_pdl = enable_pdl
557
+ self.fragments = hidden // VEC_BF16
558
+ self.groups_per_cta = threads // rank_lanes
559
+ self.fragment_stride = cluster_size * self.groups_per_cta
560
+ self.trips = math.ceil(self.fragments / self.fragment_stride)
561
+ self.warps = threads // WARP_SIZE
562
+ self.rank_waves = tp // rank_lanes
563
+
564
+ def smem_size_in_bytes(self) -> int:
565
+ return (self.warps + self.cluster_size) * 4
566
+
567
+ @cute.jit
568
+ def __call__(
569
+ self,
570
+ contribution_mailbox: cute.Tensor,
571
+ residual_source: cute.Tensor,
572
+ post: cute.Tensor,
573
+ comb: cute.Tensor,
574
+ pre: cute.Tensor,
575
+ gamma: cute.Tensor,
576
+ residual_output: cute.Tensor,
577
+ norm_output: cute.Tensor,
578
+ stage_state: cute.Tensor,
579
+ eps: Float32,
580
+ m: Int32,
581
+ stream: cuda.CUstream,
582
+ ) -> None:
583
+ self.kernel(
584
+ contribution_mailbox,
585
+ residual_source,
586
+ post,
587
+ comb,
588
+ pre,
589
+ gamma,
590
+ residual_output,
591
+ norm_output,
592
+ stage_state,
593
+ eps,
594
+ ).launch(
595
+ grid=(m, self.cluster_size, 1),
596
+ block=(self.threads, 1, 1),
597
+ cluster=(1, self.cluster_size, 1),
598
+ smem=self.smem_size_in_bytes(),
599
+ stream=stream,
600
+ use_pdl=self.enable_pdl,
601
+ )
602
+
603
+ @cute.kernel
604
+ def kernel(
605
+ self,
606
+ contribution_mailbox: cute.Tensor,
607
+ residual_source: cute.Tensor,
608
+ post: cute.Tensor,
609
+ comb: cute.Tensor,
610
+ pre: cute.Tensor,
611
+ gamma: cute.Tensor,
612
+ residual_output: cute.Tensor,
613
+ norm_output: cute.Tensor,
614
+ stage_state: cute.Tensor,
615
+ eps: Float32,
616
+ ) -> None:
617
+ hc = self.hc
618
+ tidx, _, _ = cute.arch.thread_idx()
619
+ token, _, _ = cute.arch.block_idx()
620
+ cluster_rank = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
621
+ rank_lane = tidx % self.rank_lanes
622
+ group = tidx // self.rank_lanes
623
+ base_fragment = cluster_rank * self.groups_per_cta + group
624
+
625
+ prenorm_fragments = cute.make_rmem_tensor(
626
+ cute.make_layout(
627
+ (self.trips, VEC_BF16),
628
+ stride=(VEC_BF16, 1),
629
+ ),
630
+ BFloat16,
631
+ )
632
+ prenorm_fragments.fill(BFloat16(0.0))
633
+ gamma_fragments = cute.make_rmem_tensor(
634
+ cute.make_layout(
635
+ (self.trips, VEC_BF16),
636
+ stride=(VEC_BF16, 1),
637
+ ),
638
+ BFloat16,
639
+ )
640
+ gamma_fragments.fill(BFloat16(0.0))
641
+ residual_fragments = cute.make_rmem_tensor(
642
+ cute.make_layout(
643
+ (self.trips, hc, VEC_BF16),
644
+ stride=(hc * VEC_BF16, VEC_BF16, 1),
645
+ ),
646
+ BFloat16,
647
+ )
648
+ residual_fragments.fill(BFloat16(0.0))
649
+
650
+ for trip in cutlass.range_constexpr(self.trips):
651
+ fragment = base_fragment + trip * self.fragment_stride
652
+ if fragment < self.fragments and rank_lane == 0:
653
+ gamma_element = Int64(fragment) * VEC_BF16
654
+ gamma_pointer = cute.make_ptr(
655
+ BFloat16,
656
+ (gamma.iterator + gamma_element).llvm_ptr,
657
+ cute.AddressSpace.gmem,
658
+ assumed_align=16,
659
+ )
660
+ gamma_fragments[trip, None].store(
661
+ packed_u32x4_to_bf16x8(load_global_u32x4(gamma_pointer))
662
+ )
663
+ for source in cutlass.range_constexpr(hc):
664
+ residual_element = (
665
+ Int64(token) * hc + source
666
+ ) * self.hidden + gamma_element
667
+ residual_pointer = cute.make_ptr(
668
+ BFloat16,
669
+ (residual_source.iterator + residual_element).llvm_ptr,
670
+ cute.AddressSpace.gmem,
671
+ assumed_align=16,
672
+ )
673
+ residual_fragments[trip, source, None].store(
674
+ packed_u32x4_to_bf16x8(load_global_u32x4(residual_pointer))
675
+ )
676
+ post_mix = cute.make_rmem_tensor(cute.make_layout((hc,)), Float32)
677
+ pre_mix = cute.make_rmem_tensor(cute.make_layout((hc,)), Float32)
678
+ comb_mix = cute.make_rmem_tensor(
679
+ cute.make_layout((hc, hc), stride=(hc, 1)), Float32
680
+ )
681
+ for target in cutlass.range_constexpr(hc):
682
+ mix_index = Int64(token) * hc + target
683
+ post_mix[target] = cute.arch.load(
684
+ (post.iterator + mix_index).llvm_ptr, Float32
685
+ )
686
+ pre_mix[target] = cute.arch.load(
687
+ (pre.iterator + mix_index).llvm_ptr, Float32
688
+ )
689
+ for source in cutlass.range_constexpr(hc):
690
+ comb_index = (Int64(token) * hc + source) * hc + target
691
+ comb_mix[source, target] = cute.arch.load(
692
+ (comb.iterator + comb_index).llvm_ptr, Float32
693
+ )
694
+
695
+ if cutlass.const_expr(self.enable_pdl):
696
+ cute.arch.griddepcontrol_wait()
697
+
698
+ active_stage = load_volatile_u32(stage_state.iterator + ACTIVE_STAGE)
699
+ for trip in cutlass.range_constexpr(self.trips):
700
+ fragment = base_fragment + trip * self.fragment_stride
701
+ lane_packed = cute.make_rmem_tensor(
702
+ cute.make_layout((self.rank_waves, 4)),
703
+ Uint32,
704
+ )
705
+ lane_packed.fill(Uint32(0))
706
+ dirty = fragment < self.fragments
707
+ while dirty:
708
+ dirty = False
709
+ for wave in cutlass.range_constexpr(self.rank_waves):
710
+ source_rank = wave * self.rank_lanes + rank_lane
711
+ if fragment < self.fragments:
712
+ source_element = (
713
+ (Int64(active_stage) * self.tp + Int64(source_rank))
714
+ * self.capacity_m
715
+ + Int64(token)
716
+ ) * self.hidden + Int64(fragment) * VEC_BF16
717
+ source_pointer = cute.make_ptr(
718
+ BFloat16,
719
+ (contribution_mailbox.iterator + source_element).llvm_ptr,
720
+ cute.AddressSpace.gmem,
721
+ assumed_align=16,
722
+ )
723
+ packed = load_global_u32x4(
724
+ source_pointer,
725
+ volatile=True,
726
+ )
727
+ dirty = dirty | fragment_has_negative_zero(packed)
728
+ for word in cutlass.range_constexpr(4):
729
+ lane_packed[wave, word] = packed[word]
730
+
731
+ lane_sum = cute.make_rmem_tensor(cute.make_layout((VEC_BF16,)), Float32)
732
+ lane_sum.fill(Float32(0.0))
733
+ for wave in cutlass.range_constexpr(self.rank_waves):
734
+ packed = cute.make_rmem_tensor(cute.make_layout((4,)), Uint32)
735
+ for word in cutlass.range_constexpr(4):
736
+ packed[word] = lane_packed[wave, word]
737
+ lane_sum.store(
738
+ lane_sum.load() + packed_u32x4_to_bf16x8(packed.load()).to(Float32)
739
+ )
740
+ for offset in cutlass.range_constexpr(1, 5):
741
+ if cutlass.const_expr(offset < self.rank_lanes and offset in (1, 2, 4)):
742
+ for element in cutlass.range_constexpr(VEC_BF16):
743
+ lane_sum[element] = lane_sum[
744
+ element
745
+ ] + cute.arch.shuffle_sync_bfly(
746
+ lane_sum[element],
747
+ offset=offset,
748
+ mask=-1,
749
+ mask_and_clamp=31,
750
+ )
751
+
752
+ if fragment < self.fragments and rank_lane == 0:
753
+ # The all-reduced sublayer output is BF16, as the unfused
754
+ # collective returns it, before mHC post mixes it in.
755
+ reduced = lane_sum.load().to(BFloat16).to(Float32)
756
+ collapse = cute.make_rmem_tensor(cute.make_layout((VEC_BF16,)), Float32)
757
+ collapse.fill(Float32(0.0))
758
+ for target in cutlass.range_constexpr(hc):
759
+ mixed = reduced * post_mix[target]
760
+ for source in cutlass.range_constexpr(hc):
761
+ mixed = (
762
+ mixed
763
+ + residual_fragments[trip, source, None].load().to(Float32)
764
+ * comb_mix[source, target]
765
+ )
766
+ mixed_bf16 = mixed.to(BFloat16)
767
+ output_element = (Int64(token) * hc + target) * self.hidden + Int64(
768
+ fragment
769
+ ) * VEC_BF16
770
+ store_global_u32x4(
771
+ Int64((residual_output.iterator + output_element).toint()),
772
+ bf16x8_to_packed_u32x4(mixed_bf16),
773
+ )
774
+ collapse.store(
775
+ collapse.load() + mixed_bf16.to(Float32) * pre_mix[target]
776
+ )
777
+ prenorm_fragments[trip, None].store(collapse.load().to(BFloat16))
778
+
779
+ if token == 0 and cluster_rank == 0 and tidx == 0:
780
+ store_global_u32(
781
+ stage_state.iterator + NEXT_STAGE,
782
+ (active_stage + Uint32(1)) % Uint32(LAMPORT_GENERATIONS),
783
+ )
784
+ if cutlass.const_expr(self.enable_pdl):
785
+ cute.arch.griddepcontrol_launch_dependents()
786
+
787
+ for trip in cutlass.range_constexpr(self.trips):
788
+ fragment = base_fragment + trip * self.fragment_stride
789
+ for wave in cutlass.range_constexpr(self.rank_waves):
790
+ source_rank = wave * self.rank_lanes + rank_lane
791
+ if fragment < self.fragments:
792
+ source_element = (
793
+ (Int64(active_stage) * self.tp + Int64(source_rank))
794
+ * self.capacity_m
795
+ + Int64(token)
796
+ ) * self.hidden + Int64(fragment) * VEC_BF16
797
+ store_lamport_sentinel_u32x4(
798
+ Int64((contribution_mailbox.iterator + source_element).toint())
799
+ )
800
+
801
+ thread_sum = Float32(0.0)
802
+ for trip in cutlass.range_constexpr(self.trips):
803
+ fragment = base_fragment + trip * self.fragment_stride
804
+ if fragment < self.fragments and rank_lane == 0:
805
+ values = prenorm_fragments[trip, None].load().to(Float32)
806
+ thread_sum = thread_sum + (values * values).reduce(
807
+ cute.ReductionOp.ADD,
808
+ init_val=Float32(0.0),
809
+ reduction_profile=0,
810
+ )
811
+
812
+ smem = cutlass.utils.SmemAllocator()
813
+ warp_sums = smem.allocate_array(Float32, self.warps)
814
+ cluster_sums = smem.allocate_array(Float32, self.cluster_size)
815
+ cta_sum = _group_leader_block_sum(
816
+ thread_sum,
817
+ warp_sums,
818
+ self.warps,
819
+ self.rank_lanes,
820
+ )
821
+ if tidx < self.cluster_size:
822
+ local_slot = cluster_sums + cluster_rank
823
+ remote_slot = map_shared_to_peer(local_slot, Int32(tidx))
824
+ store_shared_cluster_f32(remote_slot, cta_sum)
825
+ cute.arch.cluster_arrive()
826
+ cute.arch.cluster_wait()
827
+ full_sum = Float32(0.0)
828
+ for peer in cutlass.range_constexpr(self.cluster_size):
829
+ full_sum = full_sum + cute.arch.load(
830
+ (cluster_sums + peer).llvm_ptr,
831
+ Float32,
832
+ )
833
+ inv_rms = cute.math.rsqrt(
834
+ full_sum / Float32(self.hidden) + eps,
835
+ fastmath=True,
836
+ )
837
+ for trip in cutlass.range_constexpr(self.trips):
838
+ fragment = base_fragment + trip * self.fragment_stride
839
+ if fragment < self.fragments and rank_lane == 0:
840
+ gamma_values = gamma_fragments[trip, None].load().to(Float32)
841
+ result = (
842
+ prenorm_fragments[trip, None].load().to(Float32)
843
+ * inv_rms
844
+ * gamma_values
845
+ ).to(BFloat16)
846
+ output_element = Int64(token) * self.hidden + Int64(fragment) * VEC_BF16
847
+ store_global_u32x4(
848
+ Int64((norm_output.iterator + output_element).toint()),
849
+ bf16x8_to_packed_u32x4(result),
850
+ )
851
+
852
+
853
+class AllReduceMHC:
854
+ """Lamport TP all-reduce fused with mHC post, collapse and RMSNorm.
855
+
856
+ ``__call__`` reduces ``x`` across the TP group, writes the post-mixed hc
857
+ streams and the normalized collapse, and leaves the next mixes to the
858
+ caller. ``finalize`` does the same for an unfinalized MoE output, folding
859
+ the top-k reduction and the shared-expert add into the publish. Each
860
+ instance owns a Lamport mailbox; construction is collective over the TP
861
+ group.
862
+ """
863
+
864
+ def __init__(
865
+ self,
866
+ *,
867
+ hidden_size: int,
868
+ hc_mult: int,
869
+ max_num_tokens: int,
870
+ top_k: int,
871
+ device: torch.device,
872
+ ) -> None:
873
+ group = get_tp_group().device_group
874
+ self.hidden_size = hidden = hidden_size
875
+ self.hc_mult = hc = hc_mult
876
+ self.capacity = capacity = max_num_tokens
877
+ self.top_k = top_k
878
+ tp = dist.get_world_size(group)
879
+ rank = dist.get_rank(group)
880
+ device = torch.device(device)
881
+
882
+ # FlashInfer's LL_ALL_REDUCE_GB300_TP{4,8}_H5120 and
883
+ # LL_FINALIZE_..._H5120_K* presets: a cluster of 5 x 128 threads covers
884
+ # a 640-fragment token in one fully used trip, and the finalize stages
885
+ # all top_k rows at once.
886
+ with torch.accelerator.device_index(device.index):
887
+ self._publish = cute.compile(
888
+ _SharedOnlyPublishDeviceKernel(
889
+ hidden=hidden,
890
+ tp=tp,
891
+ rank=rank,
892
+ capacity_m=capacity,
893
+ elements_per_thread=VEC_BF16,
894
+ threads=128,
895
+ release_before_store=False,
896
+ enable_pdl=True,
897
+ ),
898
+ make_fake_dynamic_compact_tensor(
899
+ BFloat16, alignment=16, divisibility=hidden
900
+ ),
901
+ make_fake_compact_tensor(Int32, (2,), assumed_align=4),
902
+ Int64(0),
903
+ Int32(capacity),
904
+ current_cu_stream(),
905
+ )
906
+ self._finalize_publish = cute.compile(
907
+ _QuadFinalizePublishDeviceKernel(
908
+ hidden=hidden,
909
+ top_k=top_k,
910
+ tp=tp,
911
+ rank=rank,
912
+ capacity_m=capacity,
913
+ threads=128,
914
+ # DSV4.1's router folds the scale into the weights.
915
+ routed_scaling_factor=1.0,
916
+ include_shared_expert=True,
917
+ load_shared_expert_before_pdl=False,
918
+ enable_pdl=True,
919
+ prefetch_group=top_k,
920
+ fp32_weights=True,
921
+ ),
922
+ make_fake_dynamic_compact_tensor(
923
+ BFloat16, alignment=16, divisibility=hidden
924
+ ),
925
+ make_fake_dynamic_compact_tensor(
926
+ Float32, alignment=4, divisibility=top_k
927
+ ),
928
+ make_fake_dynamic_compact_tensor(
929
+ Int32, alignment=4, divisibility=top_k
930
+ ),
931
+ make_fake_dynamic_compact_tensor(
932
+ BFloat16, alignment=16, divisibility=hidden
933
+ ),
934
+ make_fake_compact_tensor(Int32, (2,), assumed_align=4),
935
+ Int64(0),
936
+ Int32(capacity),
937
+ current_cu_stream(),
938
+ )
939
+ self._collective = cute.compile(
940
+ _LamportMHCDeviceKernel(
941
+ hidden=hidden,
942
+ hc=hc,
943
+ tp=tp,
944
+ capacity_m=capacity,
945
+ cluster_size=5,
946
+ rank_lanes=1,
947
+ threads=128,
948
+ enable_pdl=True,
949
+ ),
950
+ make_fake_compact_tensor(
951
+ BFloat16,
952
+ (LAMPORT_GENERATIONS * tp * capacity * hidden,),
953
+ assumed_align=16,
954
+ ),
955
+ make_fake_dynamic_compact_tensor(
956
+ BFloat16, alignment=16, divisibility=hc * hidden
957
+ ),
958
+ make_fake_dynamic_compact_tensor(Float32, alignment=4, divisibility=hc),
959
+ make_fake_dynamic_compact_tensor(
960
+ Float32, alignment=4, divisibility=hc * hc
961
+ ),
962
+ make_fake_dynamic_compact_tensor(Float32, alignment=4, divisibility=hc),
963
+ make_fake_compact_tensor(BFloat16, (hidden,), assumed_align=16),
964
+ make_fake_dynamic_compact_tensor(
965
+ BFloat16, alignment=16, divisibility=hc * hidden
966
+ ),
967
+ make_fake_dynamic_compact_tensor(
968
+ BFloat16, alignment=16, divisibility=hidden
969
+ ),
970
+ make_fake_compact_tensor(Int32, (2,), assumed_align=4),
971
+ Float32(0.0),
972
+ Int32(capacity),
973
+ current_cu_stream(),
974
+ )
975
+
976
+ self._mailbox = symm_mem.empty(
977
+ (LAMPORT_GENERATIONS, tp, capacity, hidden),
978
+ dtype=torch.bfloat16,
979
+ device=device,
980
+ )
981
+ self._mailbox_handle = symm_mem.rendezvous(self._mailbox, group)
982
+ multicast = int(self._mailbox_handle.multicast_ptr or 0)
983
+ if not multicast or multicast % 16:
984
+ raise RuntimeError("NVLink multicast mapping is unavailable")
985
+ self._multicast = multicast
986
+ # Every slot starts as the Lamport sentinel, BF16 negative zero.
987
+ self._mailbox.view(torch.int16).fill_(-32768)
988
+ self._stage_state = torch.zeros(2, dtype=torch.int32, device=device)
989
+ torch.accelerator.synchronize()
990
+ dist.barrier(group=group)
991
+
992
+ def __call__(
993
+ self,
994
+ x: torch.Tensor,
995
+ residual: torch.Tensor,
996
+ post: torch.Tensor,
997
+ comb: torch.Tensor,
998
+ pre: torch.Tensor,
999
+ norm_weight: torch.Tensor,
1000
+ norm_eps: float,
1001
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1002
+ """Return (post-mixed residual streams, normalized layer input).
1003
+
1004
+ Args:
1005
+ x: [M, hidden] BF16 TP-partial sublayer output.
1006
+ residual: [M, hc, hidden] BF16 residual streams.
1007
+ post: [M, hc(, 1)] FP32 post-mix.
1008
+ comb: [M, hc, hc] FP32 residual mix, indexed [source, target].
1009
+ pre: [M, hc] FP32 carried pre-mix for the collapse.
1010
+ norm_weight: [hidden] BF16 RMSNorm weight.
1011
+ norm_eps: RMSNorm epsilon.
1012
+
1013
+ """
1014
+ m = x.shape[0]
1015
+ self._check(m, residual, post, comb, pre)
1016
+ if x.shape != (m, self.hidden_size):
1017
+ raise ValueError("unexpected x shape")
1018
+ self._publish(
1019
+ to_cute_dynamic(x.flatten(), 16, divisibility=self.hidden_size),
1020
+ to_cute(self._stage_state, 4),
1021
+ Int64(self._multicast),
1022
+ Int32(m),
1023
+ current_cu_stream(),
1024
+ )
1025
+ return self._collect(residual, post, comb, pre, norm_weight, norm_eps, m)
1026
+
1027
+ def finalize(
1028
+ self,
1029
+ gemm2_permuted: torch.Tensor,
1030
+ expert_weights: torch.Tensor,
1031
+ expanded_idx_to_permuted_idx: torch.Tensor,
1032
+ shared_output: torch.Tensor,
1033
+ residual: torch.Tensor,
1034
+ post: torch.Tensor,
1035
+ comb: torch.Tensor,
1036
+ pre: torch.Tensor,
1037
+ norm_weight: torch.Tensor,
1038
+ norm_eps: float,
1039
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1040
+ """``__call__`` for a MoE output left unfinalized.
1041
+
1042
+ Args:
1043
+ gemm2_permuted: [rows, hidden] BF16 unweighted GEMM2 output in the
1044
+ MoE's permuted order.
1045
+ expert_weights: [M, top_k] FP32 routing weights, already scaled.
1046
+ expanded_idx_to_permuted_idx: [M, top_k] int32 row of each route,
1047
+ -1 for an expert this rank does not hold.
1048
+ shared_output: [M, hidden] BF16 TP-partial shared-expert output.
1049
+ residual: [M, hc, hidden] BF16 residual streams.
1050
+ post: [M, hc(, 1)] FP32 post-mix.
1051
+ comb: [M, hc, hc] FP32 residual mix, indexed [source, target].
1052
+ pre: [M, hc] FP32 carried pre-mix for the collapse.
1053
+ norm_weight: [hidden] BF16 RMSNorm weight.
1054
+ norm_eps: RMSNorm epsilon.
1055
+
1056
+ """
1057
+ m = shared_output.shape[0]
1058
+ hidden, top_k = self.hidden_size, self.top_k
1059
+ self._check(m, residual, post, comb, pre)
1060
+ if gemm2_permuted.dim() != 2 or gemm2_permuted.shape[1] != hidden:
1061
+ raise ValueError("gemm2_permuted must be [rows, hidden]")
1062
+ if shared_output.shape != (m, hidden):
1063
+ raise ValueError("unexpected shared_output shape")
1064
+ if expert_weights.dtype != torch.float32 or expert_weights.numel() != m * top_k:
1065
+ raise ValueError("expert_weights must be FP32 [M, top_k]")
1066
+ if (
1067
+ expanded_idx_to_permuted_idx.dtype != torch.int32
1068
+ or expanded_idx_to_permuted_idx.numel() != m * top_k
1069
+ ):
1070
+ raise ValueError("expanded_idx_to_permuted_idx must be int32 [M, top_k]")
1071
+ self._finalize_publish(
1072
+ to_cute_dynamic(gemm2_permuted.flatten(), 16, divisibility=hidden),
1073
+ to_cute_dynamic(expert_weights.flatten(), 4, divisibility=top_k),
1074
+ to_cute_dynamic(
1075
+ expanded_idx_to_permuted_idx.flatten(), 4, divisibility=top_k
1076
+ ),
1077
+ to_cute_dynamic(shared_output.flatten(), 16, divisibility=hidden),
1078
+ to_cute(self._stage_state, 4),
1079
+ Int64(self._multicast),
1080
+ Int32(m),
1081
+ current_cu_stream(),
1082
+ )
1083
+ return self._collect(residual, post, comb, pre, norm_weight, norm_eps, m)
1084
+
1085
+ def _check(
1086
+ self,
1087
+ m: int,
1088
+ residual: torch.Tensor,
1089
+ post: torch.Tensor,
1090
+ comb: torch.Tensor,
1091
+ pre: torch.Tensor,
1092
+ ) -> None:
1093
+ hc = self.hc_mult
1094
+ if not 1 <= m <= self.capacity:
1095
+ raise ValueError(f"M={m} is outside [1, {self.capacity}]")
1096
+ if residual.shape != (m, hc, self.hidden_size):
1097
+ raise ValueError("unexpected residual shape")
1098
+ if (
1099
+ post.numel() != m * hc
1100
+ or pre.numel() != m * hc
1101
+ or comb.numel() != m * hc * hc
1102
+ ):
1103
+ raise ValueError("unexpected mix shapes")
1104
+
1105
+ def _collect(
1106
+ self,
1107
+ residual: torch.Tensor,
1108
+ post: torch.Tensor,
1109
+ comb: torch.Tensor,
1110
+ pre: torch.Tensor,
1111
+ norm_weight: torch.Tensor,
1112
+ norm_eps: float,
1113
+ m: int,
1114
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1115
+ hidden, hc = self.hidden_size, self.hc_mult
1116
+ residual_output = torch.empty_like(residual)
1117
+ layer_input = torch.empty(
1118
+ (m, hidden), dtype=torch.bfloat16, device=residual.device
1119
+ )
1120
+ self._collective(
1121
+ to_cute(self._mailbox.flatten(), 16),
1122
+ to_cute_dynamic(residual.flatten(), 16, divisibility=hc * hidden),
1123
+ to_cute_dynamic(post.flatten(), 4, divisibility=hc),
1124
+ to_cute_dynamic(comb.flatten(), 4, divisibility=hc * hc),
1125
+ to_cute_dynamic(pre.flatten(), 4, divisibility=hc),
1126
+ to_cute(norm_weight, 16),
1127
+ to_cute_dynamic(residual_output.flatten(), 16, divisibility=hc * hidden),
1128
+ to_cute_dynamic(layer_input.flatten(), 16, divisibility=hidden),
1129
+ to_cute(self._stage_state, 4),
1130
+ Float32(norm_eps),
1131
+ Int32(m),
1132
+ current_cu_stream(),
1133
+ )
1134
+ return residual_output, layer_input
vllm/models/deepseek_v41/nvidia/ops/cute_dsl/primitives.py
new
+396
@@ -0,0 +1,396 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# Adapted from FlashInfer's flashinfer/comm/mnnvl_cutedsl/cute_dsl_primitives.py
4
+# and runtime.py (flashinfer-ai/flashinfer@139af6f6).
5
+
6
+"""PTX building blocks and launch helpers for the DSV4.1 LL all-reduce."""
7
+
8
+import cuda.bindings.driver as cuda
9
+import cutlass
10
+import cutlass.cute as cute
11
+import torch
12
+from cutlass import Float32, Int32, Int64, Uint16, Uint32
13
+from cutlass._mlir import ir
14
+from cutlass._mlir.dialects import llvm, vector
15
+from cutlass.cute.runtime import from_dlpack, make_fake_compact_tensor
16
+from cutlass.cutlass_dsl import T, dsl_user_op
17
+
18
+WARP_SIZE = 32
19
+VEC_BF16 = 8
20
+QUAD_BF16 = 4
21
+NEGATIVE_ZERO_BF16_BITS = 0x8000
22
+NEGATIVE_ZERO_BF16_PAIR = 0x80008000
23
+
24
+
25
+def _asm(result, operands, text, constraints, *, side_effects, loc, ip):
26
+ return llvm.inline_asm(
27
+ result,
28
+ operands,
29
+ text,
30
+ constraints,
31
+ has_side_effects=side_effects,
32
+ is_align_stack=False,
33
+ asm_dialect=llvm.AsmDialect.AD_ATT,
34
+ loc=loc,
35
+ ip=ip,
36
+ )
37
+
38
+
39
+@dsl_user_op
40
+def load_global_u32x4(
41
+ pointer: cute.Pointer,
42
+ *,
43
+ volatile: cutlass.Constexpr[bool] = False,
44
+ loc=None,
45
+ ip=None,
46
+):
47
+ address = pointer.toint(loc=loc, ip=ip)
48
+ opcode = "ld.volatile.global.v4.u32" if volatile else "ld.global.v4.u32"
49
+ loaded = _asm(
50
+ llvm.StructType.get_literal([T.i32()] * 4),
51
+ [address.ir_value(loc=loc, ip=ip)],
52
+ f"{opcode} {{$0, $1, $2, $3}}, [$4];",
53
+ "=r,=r,=r,=r,l",
54
+ side_effects=volatile,
55
+ loc=loc,
56
+ ip=ip,
57
+ )
58
+ packed = vector.from_elements(
59
+ ir.VectorType.get([4], T.i32(), loc=loc),
60
+ [
61
+ llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
62
+ for index in range(4)
63
+ ],
64
+ loc=loc,
65
+ ip=ip,
66
+ )
67
+ return cute.TensorSSA(packed, 4, Uint32)
68
+
69
+
70
+@dsl_user_op
71
+def load_global_u32x2(pointer: cute.Pointer, *, loc=None, ip=None):
72
+ address = pointer.toint(loc=loc, ip=ip)
73
+ loaded = _asm(
74
+ llvm.StructType.get_literal([T.i32()] * 2),
75
+ [address.ir_value(loc=loc, ip=ip)],
76
+ "ld.global.v2.u32 {$0, $1}, [$2];",
77
+ "=r,=r,l",
78
+ side_effects=False,
79
+ loc=loc,
80
+ ip=ip,
81
+ )
82
+ packed = vector.from_elements(
83
+ ir.VectorType.get([2], T.i32(), loc=loc),
84
+ [
85
+ llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
86
+ for index in range(2)
87
+ ],
88
+ loc=loc,
89
+ ip=ip,
90
+ )
91
+ return cute.TensorSSA(packed, 2, Uint32)
92
+
93
+
94
+@dsl_user_op
95
+def load_global_bf16_as_f32(address: Int64, *, loc=None, ip=None) -> Float32:
96
+ return Float32(
97
+ _asm(
98
+ T.f32(),
99
+ [address.ir_value(loc=loc, ip=ip)],
100
+ "{\n\t.reg .b16 bits;\n\tld.global.b16 bits, [$1];\n\t"
101
+ "cvt.f32.bf16 $0, bits;\n\t}",
102
+ "=f,l",
103
+ side_effects=False,
104
+ loc=loc,
105
+ ip=ip,
106
+ )
107
+ )
108
+
109
+
110
+@dsl_user_op
111
+def store_global_u32x4(address: Int64, packed, *, loc=None, ip=None) -> None:
112
+ words = [packed[index].ir_value(loc=loc, ip=ip) for index in range(4)]
113
+ _asm(
114
+ None,
115
+ [address.ir_value(loc=loc, ip=ip), *words],
116
+ "st.global.v4.u32 [$0], {$1, $2, $3, $4};",
117
+ "l,r,r,r,r",
118
+ side_effects=True,
119
+ loc=loc,
120
+ ip=ip,
121
+ )
122
+
123
+
124
+@dsl_user_op
125
+def store_lamport_sentinel_u32x4(address: Int64, *, loc=None, ip=None) -> None:
126
+ sentinel = Uint32(NEGATIVE_ZERO_BF16_PAIR).ir_value(loc=loc, ip=ip)
127
+ _asm(
128
+ None,
129
+ [address.ir_value(loc=loc, ip=ip), sentinel, sentinel, sentinel, sentinel],
130
+ "st.global.v4.u32 [$0], {$1, $2, $3, $4};",
131
+ "l,r,r,r,r",
132
+ side_effects=True,
133
+ loc=loc,
134
+ ip=ip,
135
+ )
136
+
137
+
138
+@dsl_user_op
139
+def load_volatile_u32(pointer: cute.Pointer, *, loc=None, ip=None) -> Uint32:
140
+ address = pointer.toint(loc=loc, ip=ip)
141
+ return Uint32(
142
+ _asm(
143
+ T.i32(),
144
+ [address.ir_value(loc=loc, ip=ip)],
145
+ "ld.volatile.global.u32 $0, [$1];",
146
+ "=r,l",
147
+ side_effects=True,
148
+ loc=loc,
149
+ ip=ip,
150
+ )
151
+ )
152
+
153
+
154
+@dsl_user_op
155
+def store_global_u32(
156
+ pointer: cute.Pointer, value: Uint32, *, loc=None, ip=None
157
+) -> None:
158
+ address = pointer.toint(loc=loc, ip=ip)
159
+ _asm(
160
+ None,
161
+ [address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
162
+ "st.global.u32 [$0], $1;",
163
+ "l,r",
164
+ side_effects=True,
165
+ loc=loc,
166
+ ip=ip,
167
+ )
168
+
169
+
170
+@dsl_user_op
171
+def packed_u32x4_to_bf16x8(packed, *, loc=None, ip=None):
172
+ values = llvm.bitcast(
173
+ ir.VectorType.get([VEC_BF16], cutlass.BFloat16.mlir_type, loc=loc),
174
+ packed.ir_value(loc=loc, ip=ip),
175
+ loc=loc,
176
+ ip=ip,
177
+ )
178
+ return cute.TensorSSA(values, VEC_BF16, cutlass.BFloat16)
179
+
180
+
181
+@dsl_user_op
182
+def packed_u32x2_to_bf16x4(packed, *, loc=None, ip=None):
183
+ values = llvm.bitcast(
184
+ ir.VectorType.get([QUAD_BF16], cutlass.BFloat16.mlir_type, loc=loc),
185
+ packed.ir_value(loc=loc, ip=ip),
186
+ loc=loc,
187
+ ip=ip,
188
+ )
189
+ return cute.TensorSSA(values, QUAD_BF16, cutlass.BFloat16)
190
+
191
+
192
+@dsl_user_op
193
+def bf16x4_to_packed_u32x2(values, *, loc=None, ip=None):
194
+ packed = llvm.bitcast(
195
+ ir.VectorType.get([2], T.i32(), loc=loc),
196
+ values.ir_value(loc=loc, ip=ip),
197
+ loc=loc,
198
+ ip=ip,
199
+ )
200
+ return cute.TensorSSA(packed, 2, Uint32)
201
+
202
+
203
+@dsl_user_op
204
+def bf16x8_to_packed_u32x4(values, *, loc=None, ip=None):
205
+ packed = llvm.bitcast(
206
+ ir.VectorType.get([4], T.i32(), loc=loc),
207
+ values.ir_value(loc=loc, ip=ip),
208
+ loc=loc,
209
+ ip=ip,
210
+ )
211
+ return cute.TensorSSA(packed, 4, Uint32)
212
+
213
+
214
+@cute.jit
215
+def sanitize_negative_zero_u32(word: Uint32) -> Uint32:
216
+ low = Uint16(word & Uint32(0xFFFF))
217
+ high = Uint16(word >> Uint32(16))
218
+ if low == Uint16(NEGATIVE_ZERO_BF16_BITS):
219
+ word = word & Uint32(0xFFFF0000)
220
+ if high == Uint16(NEGATIVE_ZERO_BF16_BITS):
221
+ word = word & Uint32(0x0000FFFF)
222
+ return word
223
+
224
+
225
+@cute.jit
226
+def sanitize_negative_zero_u32x4(packed):
227
+ sanitized = cute.make_rmem_tensor(cute.make_layout((4,)), Uint32)
228
+ for index in cutlass.range_constexpr(4):
229
+ sanitized[index] = sanitize_negative_zero_u32(packed[index])
230
+ return sanitized.load()
231
+
232
+
233
+@cute.jit
234
+def sanitize_negative_zero_u32x2(packed):
235
+ sanitized = cute.make_rmem_tensor(cute.make_layout((2,)), Uint32)
236
+ for index in cutlass.range_constexpr(2):
237
+ sanitized[index] = sanitize_negative_zero_u32(packed[index])
238
+ return sanitized.load()
239
+
240
+
241
+@cute.jit
242
+def fragment_has_negative_zero(packed):
243
+ dirty = False
244
+ for index in cutlass.range_constexpr(4):
245
+ word = packed[index]
246
+ dirty = (
247
+ dirty
248
+ | (Uint16(word & Uint32(0xFFFF)) == Uint16(NEGATIVE_ZERO_BF16_BITS))
249
+ | (Uint16(word >> Uint32(16)) == Uint16(NEGATIVE_ZERO_BF16_BITS))
250
+ )
251
+ return dirty
252
+
253
+
254
+@dsl_user_op
255
+def map_shared_to_peer(
256
+ smem_pointer: cute.Pointer, peer_rank: Int32, *, loc=None, ip=None
257
+) -> Int32:
258
+ address = smem_pointer.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
259
+ return Int32(
260
+ _asm(
261
+ T.i32(),
262
+ [address, peer_rank.ir_value(loc=loc, ip=ip)],
263
+ "mapa.shared::cluster.u32 $0, $1, $2;",
264
+ "=r,r,r",
265
+ side_effects=False,
266
+ loc=loc,
267
+ ip=ip,
268
+ )
269
+ )
270
+
271
+
272
+@dsl_user_op
273
+def store_shared_cluster_f32(
274
+ remote_address: Int32, value: Float32, *, loc=None, ip=None
275
+) -> None:
276
+ _asm(
277
+ None,
278
+ [remote_address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
279
+ "st.shared::cluster.f32 [$0], $1;",
280
+ "r,f",
281
+ side_effects=True,
282
+ loc=loc,
283
+ ip=ip,
284
+ )
285
+
286
+
287
+@dsl_user_op
288
+def f32_to_bf16_bits(value: Float32, *, loc=None, ip=None) -> Uint32:
289
+ return Uint32(
290
+ _asm(
291
+ T.i32(),
292
+ [value.ir_value(loc=loc, ip=ip)],
293
+ "{\n\t.reg .b16 bits;\n\tcvt.rn.bf16.f32 bits, $1;\n\t"
294
+ "cvt.u32.u16 $0, bits;\n\t}",
295
+ "=r,f",
296
+ side_effects=False,
297
+ loc=loc,
298
+ ip=ip,
299
+ )
300
+ )
301
+
302
+
303
+@dsl_user_op
304
+def shuffle_sync_idx_u32(
305
+ value: Uint32, source_lane: Int32, *, loc=None, ip=None
306
+) -> Uint32:
307
+ return Uint32(
308
+ _asm(
309
+ T.i32(),
310
+ [value.ir_value(loc=loc, ip=ip), source_lane.ir_value(loc=loc, ip=ip)],
311
+ "shfl.sync.idx.b32 $0, $1, $2, 0x1f, 0xffffffff;",
312
+ "=r,r,r",
313
+ # Preserve full-warp execution across later divergent consumers.
314
+ side_effects=True,
315
+ loc=loc,
316
+ ip=ip,
317
+ )
318
+ )
319
+
320
+
321
+@dsl_user_op
322
+def stmc_bf16x2(address: Int64, packed: Uint32, *, loc=None, ip=None) -> None:
323
+ _asm(
324
+ None,
325
+ [address.ir_value(loc=loc, ip=ip), packed.ir_value(loc=loc, ip=ip)],
326
+ "multimem.st.relaxed.sys.global.bf16x2 [$0], $1;",
327
+ "l,r",
328
+ side_effects=True,
329
+ loc=loc,
330
+ ip=ip,
331
+ )
332
+
333
+
334
+@dsl_user_op
335
+def stmc_bf16x4(address: Int64, values, *, loc=None, ip=None) -> None:
336
+ words = [values[index].ir_value(loc=loc, ip=ip) for index in range(2)]
337
+ _asm(
338
+ None,
339
+ [address.ir_value(loc=loc, ip=ip), *words],
340
+ "multimem.st.relaxed.sys.global.v2.bf16x2 [$0], {$1, $2};",
341
+ "l,r,r",
342
+ side_effects=True,
343
+ loc=loc,
344
+ ip=ip,
345
+ )
346
+
347
+
348
+@dsl_user_op
349
+def stmc_bf16x8(address: Int64, values, *, loc=None, ip=None) -> None:
350
+ words = [values[index].ir_value(loc=loc, ip=ip) for index in range(4)]
351
+ _asm(
352
+ None,
353
+ [address.ir_value(loc=loc, ip=ip), *words],
354
+ "multimem.st.relaxed.sys.global.v4.bf16x2 [$0], {$1, $2, $3, $4};",
355
+ "l,r,r,r,r",
356
+ side_effects=True,
357
+ loc=loc,
358
+ ip=ip,
359
+ )
360
+
361
+
362
+class _GraphSafeDLPack:
363
+ __slots__ = ("tensor",)
364
+
365
+ def __init__(self, tensor: torch.Tensor) -> None:
366
+ self.tensor = tensor
367
+
368
+ def __dlpack__(self, stream=None):
369
+ # stream=-1 skips producer sync; CuTe launches on the current stream,
370
+ # which is the capturing one under CUDA graphs.
371
+ return self.tensor.__dlpack__(stream=-1)
372
+
373
+ def __dlpack_device__(self):
374
+ return self.tensor.__dlpack_device__()
375
+
376
+
377
+def to_cute(tensor: torch.Tensor, alignment: int) -> cute.Tensor:
378
+ return from_dlpack(_GraphSafeDLPack(tensor.detach()), assumed_align=alignment)
379
+
380
+
381
+def to_cute_dynamic(
382
+ tensor: torch.Tensor, alignment: int, *, divisibility: int
383
+) -> cute.Tensor:
384
+ return to_cute(tensor, alignment).mark_compact_shape_dynamic(
385
+ mode=0, divisibility=divisibility
386
+ )
387
+
388
+
389
+def make_fake_dynamic_compact_tensor(dtype, *, alignment: int, divisibility: int):
390
+ return make_fake_compact_tensor(
391
+ dtype, (cute.sym_int32(divisibility=divisibility),), assumed_align=alignment
392
+ )
393
+
394
+
395
+def current_cu_stream() -> cuda.CUstream:
396
+ return cuda.CUstream(torch.cuda.current_stream().cuda_stream)
vllm/models/deepseek_v41/nvidia/ops/mhc.py
+73
-18
@@ -9,10 +9,15 @@ import torch
9
10
from vllm.config import VllmConfig
11
from vllm.distributed import get_tp_group
12
+from vllm.logger import init_logger
13
from vllm.model_executor.kernels.mhc.tilelang import (
14
mhc_fused_post_pre_delayed_tilelang,
15
mhc_post_tilelang,
16
)
17
+from vllm.model_executor.layers.fused_moe.moe_output import (
18
+ MoEOutput,
19
+ UnfinalizedMoEOutput,
20
+)
21
from vllm.platforms import current_platform
22
from vllm.utils.deep_gemm import is_deep_gemm_supported
23
@@ -24,9 +29,17 @@ from .mega_mhc import (
29
if TYPE_CHECKING:
30
from vllm.distributed.device_communicators.cuda_communicator import CudaCommunicator
31
32
+ from .cute_dsl import AllReduceMHC
33
+
34
+logger = init_logger(__name__)
35
+
36
# GB200 TP4/FlashInfer improves through 16 tokens; larger screens tie or regress.
37
MHC_OVERLAP_MAX_TOKENS = 16
38
39
+# The fused all-reduce + mHC kernel of the overlap path, built once by
40
+# init_mhc_all_reduce.
41
+_all_reduce_mhc: "AllReduceMHC | None" = None
42
+
43
44
def supports_mhc_overlap(vllm_config: VllmConfig) -> bool:
45
"""Check kernel requirements and safety of sharing the coefficient stream."""
@@ -53,10 +66,35 @@ def supports_mhc_all_reduce(vllm_config: VllmConfig) -> bool:
66
or config.hc_mult != 4
67
):
68
return False
69
+ # The custom all-reduce's MNNVL buffers succeed exactly when NVLink
70
+ # multicast does, which the fused kernel's own mailbox needs too.
71
comm = cast("CudaCommunicator", get_tp_group().device_communicator).ca_comm
72
return comm is not None and bool(comm.mnnvl_lamport_ag_multicast_ptr)
73
74
75
+def init_mhc_all_reduce(vllm_config: VllmConfig) -> None:
76
+ """Build the fused all-reduce + mHC kernel the overlap path reduces with.
77
+
78
+ Collective over the TP group, so every rank must call it, and only when
79
+ ``supports_mhc_all_reduce`` holds.
80
+ """
81
+ global _all_reduce_mhc
82
+ from .cute_dsl import AllReduceMHC
83
+
84
+ config = vllm_config.model_config.hf_config
85
+ _all_reduce_mhc = AllReduceMHC(
86
+ hidden_size=config.hidden_size,
87
+ hc_mult=config.hc_mult,
88
+ max_num_tokens=MHC_OVERLAP_MAX_TOKENS,
89
+ top_k=config.num_experts_per_tok,
90
+ device=current_platform.current_device(),
91
+ )
92
+ logger.info_once(
93
+ "DSV4.1 mHC: CuTe DSL Lamport all-reduce fused with mHC for up to %d tokens.",
94
+ MHC_OVERLAP_MAX_TOKENS,
95
+ )
96
+
97
+
98
def mhc_pre_delayed_overlap(
99
residual: torch.Tensor,
100
fn: torch.Tensor,
@@ -151,7 +189,7 @@ def mhc_pre_delayed_overlap(
189
190
191
def mhc_shifted_post_pre(
154
- x: torch.Tensor,
192
+ x: torch.Tensor | MoEOutput,
193
residual: torch.Tensor,
194
post_layer_mix: torch.Tensor,
195
comb_res_mix: torch.Tensor,
@@ -175,39 +213,55 @@ def mhc_shifted_post_pre(
213
]:
214
"""Dispatch shifted post/pre to overlap, Mega-mHC, or fused TileLang.
215
216
+ A MoE output left unfinalized is finalized inside the fused all-reduce.
217
When stream is supplied, join it before consuming the returned coefficients.
218
"""
219
layer_input = None
181
- if reduce_results:
182
- tp = get_tp_group()
220
+ if isinstance(x, MoEOutput):
221
+ assert _all_reduce_mhc is not None, "init_mhc_all_reduce was not called"
222
+ assert pre_mix is not None and norm_weight is not None
223
+ routed = x.routed
224
+ assert isinstance(routed, UnfinalizedMoEOutput)
225
+ assert x.shared_output is not None
226
+ residual, layer_input = _all_reduce_mhc.finalize(
227
+ routed.gemm2_permuted,
228
+ routed.expert_weights,
229
+ routed.expanded_idx_to_permuted_idx,
230
+ x.shared_output,
231
+ residual,
232
+ post_layer_mix,
233
+ comb_res_mix,
234
+ pre_mix,
235
+ norm_weight,
236
+ norm_eps,
237
+ )
238
+ if stream is None:
239
+ # Outside FULL graphs, compute the next mixes inline.
240
+ stream = torch.cuda.current_stream()
241
+ elif reduce_results:
242
if stream is not None and 0 < x.shape[0] <= MHC_OVERLAP_MAX_TOKENS:
184
- comm = cast("CudaCommunicator", tp.device_communicator).ca_comm
185
- assert comm is not None and comm.mnnvl_lamport_epochs is not None
186
- output = torch.empty_like(residual)
187
- layer_input = torch.empty_like(x)
188
- torch.ops._C_custom_ar.all_reduce_mhc(
243
+ assert _all_reduce_mhc is not None, "init_mhc_all_reduce was not called"
244
+ assert pre_mix is not None and norm_weight is not None
245
+ residual, layer_input = _all_reduce_mhc(
246
x,
247
residual,
248
post_layer_mix,
249
comb_res_mix,
250
pre_mix,
251
norm_weight,
195
- output,
196
- layer_input,
197
- comm.mnnvl_lamport_ag_local_ptr,
198
- comm.mnnvl_lamport_ag_multicast_ptr,
199
- comm.mnnvl_lamport_epochs[0],
200
- comm.rank,
201
- comm.mnnvl_buffer_size,
252
norm_eps,
253
)
204
- residual = output
254
else:
206
- x = tp.all_reduce(x)
255
+ x = get_tp_group().all_reduce(x)
256
if stream is not None:
257
if layer_input is None:
258
+ assert isinstance(x, torch.Tensor)
259
residual = mhc_post_tilelang(x, residual, post_layer_mix, comb_res_mix)
210
- aux = residual.mean(dim=1) if capture_aux else x.new_empty(0, x.shape[1])
260
+ aux = (
261
+ residual.mean(dim=1)
262
+ if capture_aux
263
+ else residual.new_empty(0, residual.shape[-1])
264
+ )
265
pre_outputs = mhc_pre_delayed_overlap(
266
residual,
267
fn,
@@ -226,6 +280,7 @@ def mhc_shifted_post_pre(
280
)
281
return residual, *pre_outputs, aux
282
283
+ assert isinstance(x, torch.Tensor)
284
if can_use_mega_mhc(x, residual, pre_mix, norm_weight, capture_aux):
285
assert pre_mix is not None and norm_weight is not None
286
outputs = mhc_shifted_post_pre_deep_gemm(