[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>
This commit is contained in:
Yongye Zhu
2026-09-27 03:08:36 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 379e9a1ea8
commit 77871126f9
8 changed files with 1804 additions and 306 deletions
-2
View File
@@ -460,8 +460,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
DEFINED CMAKE_CUDA_COMPILER_VERSION AND
CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0)
list(APPEND VLLM_STABLE_EXT_SRC "csrc/libtorch_stable/all_reduce_mhc.cu")
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS
"9.0a;10.0f;10.1f;10.3f;10.7f;11.0f;12.0f;12.1f" "${CUDA_ARCHS}")
-212
View File
@@ -1,212 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#include "torch_utils.h"
#include "custom_all_gather_reduce_scatter.cuh"
#include <cooperative_groups.h>
#include <torch/csrc/stable/library.h>
namespace {
using Tensor = torch::stable::Tensor;
using Pack = vllm::packed_t<nv_bfloat16>::P;
constexpr int kHidden = 5120;
constexpr int kRanks = 4;
constexpr int kThreads = 128;
constexpr int kCluster = 5;
constexpr int kPacks = kHidden / Pack::size;
__global__ __launch_bounds__(kThreads)
__cluster_dims__(1, kCluster, 1) void all_reduce_mhc_kernel(
const Pack* input, const Pack* residual, const float* post,
const float* comb, const float* pre, const Pack* weight, Pack* output,
Pack* normalized, Pack* local, Pack* multicast, uint32_t* epochs,
int rank, int stage_size, float eps) {
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900
cudaGridDependencySynchronize();
const int token = blockIdx.x;
const int column = blockIdx.y * kThreads + threadIdx.x;
const int idx = token * kPacks + column;
const int size = gridDim.x * kPacks;
const int stage = epochs[0] % 3;
const int dirty = vllm::mnnvl_lamport_dirty_stage(stage);
const int dirty_size = epochs[2 + dirty];
Pack value = input[idx];
vllm::store_multimem_lamport_payload(
multicast + stage * stage_size + rank * size + idx,
vllm::sanitize_lamport_payload(value));
// Multi-token forwards benefit from launching dependents before mHC finishes.
if (gridDim.x > 1) cudaTriggerProgrammaticLaunchCompletion();
vllm::lamport_cta_arrive(epochs + 1);
for (int i = idx; i < dirty_size; i += size) {
local[dirty * stage_size + i] = vllm::lamport_sentinel<Pack>();
}
Pack peers[kRanks];
bool pending;
do {
vllm::LamportPack<Pack> packets[kRanks];
#pragma unroll
for (int peer = 0; peer < kRanks; ++peer) {
auto ptr = local + stage * stage_size + idx + peer * size;
asm volatile("ld.relaxed.sys.global.v4.u32 {%0,%1,%2,%3}, [%4];"
: "=r"(packets[peer].words[0]), "=r"(packets[peer].words[1]),
"=r"(packets[peer].words[2]), "=r"(packets[peer].words[3])
: "l"(ptr)
: "memory");
}
pending = false;
#pragma unroll
for (int peer = 0; peer < kRanks; ++peer) {
pending |= vllm::is_lamport_dirty(packets[peer]);
peers[peer] = packets[peer].packed;
}
} while (pending);
auto sum = vllm::upcast(peers[0]);
#pragma unroll
for (int r = 1; r < kRanks; ++r) {
vllm::packed_assign_add(sum, vllm::upcast(peers[r]));
}
// Keep the BF16 boundary between the collective and mHC post.
auto reduced = vllm::upcast(vllm::downcast<Pack>(sum));
decltype(sum) streams[4];
#pragma unroll
for (int r = 0; r < 4; ++r) {
streams[r] = vllm::upcast(residual[(token * 4 + r) * kPacks + column]);
}
decltype(sum) collapsed{};
#pragma unroll
for (int c = 0; c < 4; ++c) {
decltype(sum) mixed;
#pragma unroll
for (int e = 0; e < Pack::size; ++e) {
// Match post*x followed by the four residual FMAs.
float acc = __fmul_rn(post[token * 4 + c], reduced.data[e]);
#pragma unroll
for (int r = 0; r < 4; ++r) {
acc = fmaf(comb[token * 16 + r * 4 + c], streams[r].data[e], acc);
}
mixed.data[e] = acc;
}
Pack rounded = vllm::downcast<Pack>(mixed);
output[(token * 4 + c) * kPacks + column] = rounded;
auto as_float = vllm::upcast(rounded);
#pragma unroll
for (int e = 0; e < Pack::size; ++e) {
collapsed.data[e] =
fmaf(pre[token * 4 + c], as_float.data[e], collapsed.data[e]);
}
}
// RMSNorm consumes the rounded BF16 collapse in the unfused path.
collapsed = vllm::upcast(vllm::downcast<Pack>(collapsed));
float sqr = 0;
#pragma unroll
for (int e = 0; e < Pack::size; ++e) {
sqr = fmaf(collapsed.data[e], collapsed.data[e], sqr);
}
#pragma unroll
for (int delta = 16; delta; delta /= 2) {
sqr += __shfl_xor_sync(0xffffffff, sqr, delta);
}
__shared__ float warp_sums[kCluster][kThreads / 32];
auto cluster = cooperative_groups::this_cluster();
const int lane = threadIdx.x % 32;
if (lane < kCluster) {
*cluster.map_shared_rank(&warp_sums[blockIdx.y][threadIdx.x / 32], lane) =
sqr;
}
cluster.sync();
float total = 0;
#pragma unroll
for (int c = 0; c < kCluster; ++c) {
#pragma unroll
for (int w = 0; w < kThreads / 32; ++w) total += warp_sums[c][w];
}
const float scale = rsqrtf(__fadd_rn(total / kHidden, eps));
auto weights = vllm::upcast(weight[column]);
#pragma unroll
for (int e = 0; e < Pack::size; ++e) {
collapsed.data[e] = collapsed.data[e] * scale * weights.data[e];
}
normalized[idx] = vllm::downcast<Pack>(collapsed);
if (idx == 0) {
while (*reinterpret_cast<volatile uint32_t*>(epochs + 1) <
gridDim.x * gridDim.y) {
}
epochs[2 + stage] = kRanks * size;
epochs[0] = vllm::mnnvl_lamport_next_stage(stage);
epochs[1] = 0;
}
if (gridDim.x == 1) cudaTriggerProgrammaticLaunchCompletion();
#else
asm volatile("trap;");
#endif
}
void all_reduce_mhc(Tensor input, Tensor residual, Tensor post, Tensor comb,
Tensor pre, Tensor weight, Tensor output, Tensor normalized,
int64_t local, int64_t multicast, Tensor epochs,
int64_t rank, int64_t stage_bytes, double eps) {
using torch::headeronly::ScalarType;
const torch::stable::accelerator::DeviceGuard guard(input.get_device_index());
const int64_t n = input.size(0);
STD_TORCH_CHECK(input.dim() == 2 && input.size(1) == kHidden && n > 0 &&
n <= 16);
for (const auto& t : {input, residual, weight, output, normalized}) {
STD_TORCH_CHECK(t.is_contiguous() &&
t.scalar_type() == ScalarType::BFloat16);
STD_TORCH_CHECK(t.get_device_index() == input.get_device_index());
STD_TORCH_CHECK(reinterpret_cast<uintptr_t>(t.const_data_ptr()) % 16 == 0);
}
for (const auto& t : {post, comb, pre}) {
STD_TORCH_CHECK(t.is_contiguous() && t.scalar_type() == ScalarType::Float);
STD_TORCH_CHECK(t.get_device_index() == input.get_device_index());
}
STD_TORCH_CHECK(residual.numel() == n * 4 * kHidden &&
output.numel() == residual.numel() &&
normalized.numel() == input.numel() &&
weight.numel() == kHidden);
STD_TORCH_CHECK(post.numel() == n * 4 && pre.numel() == n * 4 &&
comb.numel() == n * 16);
STD_TORCH_CHECK(epochs.is_contiguous() &&
epochs.scalar_type() == ScalarType::Int &&
epochs.numel() >= 5 &&
epochs.get_device_index() == input.get_device_index());
STD_TORCH_CHECK(local && multicast && rank >= 0 && rank < kRanks &&
stage_bytes % 16 == 0 &&
stage_bytes >= n * kHidden * 2 * kRanks);
STD_TORCH_CHECK(get_device_prop()->major == 10);
cudaLaunchAttribute attr{};
attr.id = cudaLaunchAttributeProgrammaticStreamSerialization;
attr.val.programmaticStreamSerializationAllowed = 1;
cudaLaunchConfig_t config{
.gridDim = dim3(n, kCluster),
.blockDim = dim3(kThreads),
.dynamicSmemBytes = 0,
.stream = get_current_cuda_stream(input.get_device_index()),
.attrs = &attr,
.numAttrs = 1};
STD_CUDA_CHECK(cudaLaunchKernelEx(
&config, all_reduce_mhc_kernel,
static_cast<const Pack*>(input.const_data_ptr()),
static_cast<const Pack*>(residual.const_data_ptr()),
static_cast<const float*>(post.const_data_ptr()),
static_cast<const float*>(comb.const_data_ptr()),
static_cast<const float*>(pre.const_data_ptr()),
static_cast<const Pack*>(weight.const_data_ptr()),
static_cast<Pack*>(output.mutable_data_ptr()),
static_cast<Pack*>(normalized.mutable_data_ptr()),
reinterpret_cast<Pack*>(local), reinterpret_cast<Pack*>(multicast),
static_cast<uint32_t*>(epochs.mutable_data_ptr()), static_cast<int>(rank),
static_cast<int>(stage_bytes / sizeof(Pack)), static_cast<float>(eps)));
}
} // namespace
STABLE_TORCH_LIBRARY_FRAGMENT(_C_custom_ar, mhc) {
mhc.def(
"all_reduce_mhc(Tensor input, Tensor residual, Tensor post, Tensor comb, "
"Tensor pre, Tensor weight, Tensor! output, Tensor! normalized, "
"int local, int multicast, Tensor! epochs, int rank, int stage_bytes, "
"float eps) -> ()");
}
STABLE_TORCH_LIBRARY_IMPL(_C_custom_ar, CUDA, mhc) {
mhc.impl("all_reduce_mhc", TORCH_BOX(&all_reduce_mhc));
}
+117 -70
View File
@@ -25,12 +25,43 @@ for i, v in enumerate(test_sizes):
test_sizes[i] -= v % 8
def _bf16_ulps(a: torch.Tensor, b: torch.Tensor) -> int:
"""Largest distance between two BF16 tensors in units in the last place."""
def ordered(t: torch.Tensor) -> torch.Tensor:
bits = t.view(torch.int16).int() & 0xFFFF
return torch.where(bits >= 0x8000, 0x8000 - bits, bits)
return int((ordered(a) - ordered(b)).abs().max())
def _unfused_all_reduce_mhc(peers, residual, post, comb, pre, weight, eps):
"""All-reduce, mHC post, collapse and RMSNorm in the fused kernel's FP32
order, rounding to BF16 where the unfused path returns BF16."""
reduced = peers[0].float()
for peer in peers[1:]:
reduced = reduced + peer.float()
reduced = reduced.bfloat16().float()
output = torch.empty_like(residual)
collapse = torch.zeros_like(reduced)
for target in range(4):
mixed = reduced * post[:, target : target + 1]
for source in range(4):
mixed = torch.addcmul(
mixed, residual[:, source].float(), comb[:, source, target, None]
)
output[:, target] = mixed.bfloat16()
collapse = torch.addcmul(
collapse, output[:, target].float(), pre[:, target : target + 1]
)
prenorm = collapse.bfloat16().float()
inv_rms = torch.rsqrt(prenorm.square().mean(-1, keepdim=True) + eps)
return output, (prenorm * inv_rms * weight.float()).bfloat16()
@ray.remote(num_gpus=1, max_calls=1)
def _all_reduce_mhc(monkeypatch, tp_size, pp_size, rank, distributed_init_port):
from vllm.model_executor.kernels.mhc.tilelang import (
mhc_post_tilelang,
mhc_pre_delayed_tilelang,
)
from vllm.models.deepseek_v41.nvidia.ops.cute_dsl import AllReduceMHC
with monkeypatch.context() as m:
m.delenv("CUDA_VISIBLE_DEVICES", raising=False)
@@ -38,98 +69,114 @@ def _all_reduce_mhc(monkeypatch, tp_size, pp_size, rank, distributed_init_port):
torch.accelerator.set_device_index(device)
init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port)
ensure_model_parallel_initialized(tp_size, pp_size)
comm = get_tp_group().device_communicator.ca_comm
assert comm is not None and comm.mnnvl_lamport_ag_multicast_ptr
fn = torch.zeros(24, 20480, device=device)
scale = torch.ones(3, device=device)
base = torch.zeros(24, device=device)
op = AllReduceMHC(
hidden_size=5120, hc_mult=4, max_num_tokens=16, top_k=6, device=device
)
# Changing shapes and interleaving all-gather exercise the shared
# Lamport stages, including cleanup of a larger previous payload.
def run(n):
torch.manual_seed(42 + rank)
x = torch.randn(n, 5120, device=device, dtype=torch.bfloat16)
# Packed +0/-0 pairs collide with the Lamport sentinel.
x[:, :16] = 0
x[:, 9:16:2] = -0.0
def check_eager_and_replayed(fused, check, halved, doubled):
check(fused())
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
for _ in range(5):
fused()
captured = fused()
for _ in range(20):
graph.replay()
check(captured)
# Replays must pick up in-place input changes.
halved.mul_(0.5)
doubled.mul_(2)
graph.replay()
check(captured)
def mhc_inputs(n):
torch.manual_seed(123)
residual = torch.randn(n, 4, 5120, device=device, dtype=torch.bfloat16)
post = torch.rand(n, 4, device=device)
comb = torch.randn(n, 4, 4, device=device) * 0.1
pre = torch.rand(n, 4, device=device)
weight = torch.randn(5120, device=device, dtype=torch.bfloat16)
output = torch.empty_like(residual)
normalized = torch.empty_like(x)
return residual, post, comb, pre, weight
def run(n):
torch.manual_seed(42 + rank)
x = torch.randn(n, 5120, device=device, dtype=torch.bfloat16)
# Packed +0/-0 pairs collide with the Lamport sentinel.
x[:, :16] = 0
x[:, 9:16:2] = -0.0
residual, post, comb, pre, weight = mhc_inputs(n)
def fused():
torch.ops._C_custom_ar.all_reduce_mhc(
x,
return op(x, residual, post, comb, pre, weight, 1e-6)
def check(outputs):
peers = get_tp_group().all_gather(x, dim=0).view(tp_size, n, 5120)
output, normalized = _unfused_all_reduce_mhc(
peers, residual, post, comb, pre, weight, 1e-6
)
# The mixed hc streams match bit for bit. The RMSNorm sums the
# squares in another order and uses an approximate rsqrt.
assert torch.equal(outputs[0], output)
assert _bf16_ulps(outputs[1], normalized) <= 1
check_eager_and_replayed(fused, check, x, residual)
def run_finalize(n):
torch.manual_seed(7 + rank)
# A padded permuted GEMM2 buffer, like the MoE's.
rows = n * 6 + 5
gemm2 = torch.randn(rows, 5120, device=device, dtype=torch.bfloat16)
permuted = torch.randperm(rows, device=device)[: n * 6].view(n, 6).int()
# A route to an expert this rank does not hold.
permuted[0, -1] = -1
weights = torch.rand(n, 6, device=device)
shared = torch.randn(n, 5120, device=device, dtype=torch.bfloat16)
residual, post, comb, pre, weight = mhc_inputs(n)
def fused():
return op.finalize(
gemm2,
weights,
permuted,
shared,
residual,
post,
comb,
pre,
weight,
output,
normalized,
comm.mnnvl_lamport_ag_local_ptr,
comm.mnnvl_lamport_ag_multicast_ptr,
comm.mnnvl_lamport_epochs[0],
rank,
comm.mnnvl_buffer_size,
1e-6,
)
def check():
gathered = comm.custom_all_gather(x)
assert gathered is not None
peers = gathered.view(tp_size, n, 5120).float()
reduced = peers[0].clone()
for peer in peers[1:]:
reduced.add_(peer)
expected = mhc_post_tilelang(
reduced.bfloat16(), residual, post.unsqueeze(-1), comb
)
expected_norm = mhc_pre_delayed_tilelang(
expected,
fn,
scale,
base,
1e-6,
1e-6,
1e-6,
2.0,
20,
pre_mix=pre,
norm_weight=weight,
)[2]
torch.testing.assert_close(output, expected, rtol=0.008, atol=1e-6)
torch.testing.assert_close(
normalized, expected_norm, rtol=0.008, atol=0.008
)
def check(outputs):
# One FP32 FMA per route in route order, the shared add, one
# rounding, then the plain path: only the finalize differs.
acc = torch.zeros(n, 5120, device=device)
for k in range(6):
valid = (permuted[:, k] >= 0).unsqueeze(-1)
rows_k = gemm2[permuted[:, k].clamp_min(0).long()].float()
torch.addcmul(
acc,
torch.where(valid, rows_k, 0.0),
torch.where(valid, weights[:, k : k + 1], 0.0),
out=acc,
)
x = (acc + shared.float()).bfloat16()
expected = op(x, residual, post, comb, pre, weight, 1e-6)
assert torch.equal(outputs[0], expected[0])
assert torch.equal(outputs[1], expected[1])
fused()
check()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
for _ in range(6):
fused()
for _ in range(20):
graph.replay()
check()
x.mul_(0.5)
residual.mul_(2)
graph.replay()
check()
check_eager_and_replayed(fused, check, gemm2, shared)
# Cover fixed Q6 and shrinking/growing adaptive verification batches.
# Changing shapes cover shrinking and growing batches in one mailbox.
for n in (1, 6, 12, 8, 16, 3, 5, 2, 4, 1):
run(n)
run_finalize(n)
@pytest.mark.skipif(
not current_platform.is_device_capability_family(100), reason="Requires SM100"
)
def test_all_reduce_mhc_preserves_bf16_boundaries_and_graph_replay(monkeypatch):
def test_all_reduce_mhc_matches_unfused_path(monkeypatch):
if torch.accelerator.device_count() < 4:
pytest.skip("Requires four GPUs with NVLink multicast")
multi_process_parallel(monkeypatch, 4, 1, _all_reduce_mhc)
+76 -4
View File
@@ -33,6 +33,14 @@ from vllm.model_executor.kernels.mhc.triton import hc_collapse_triton
from vllm.model_executor.layers.fused_moe import (
fused_moe_make_expert_params_mapping,
)
from vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe import (
TrtLlmMxfp4ExpertsModular,
)
from vllm.model_executor.layers.fused_moe.moe_output import (
MoEOutput,
UnfinalizedMoEOutput,
)
from vllm.model_executor.layers.fused_moe.runner.moe_runner import _unpack
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -94,6 +102,7 @@ from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID, image_sentinel_mask
from .engram import Engram, can_share_engram_tables, gather_engram_hashes
from .ops.mhc import (
MHC_OVERLAP_MAX_TOKENS,
init_mhc_all_reduce,
mhc_pre_delayed_overlap,
mhc_shifted_post_pre,
supports_mhc_all_reduce,
@@ -135,6 +144,48 @@ class DeepseekV4MoE(DeepseekV4MoEBase):
image_sentinel_lo=IMAGE_SENTINEL_BASE_ID,
)
def defer_finalize(self) -> None:
"""Leave the routed top-k reduction to the next fused all-reduce + mHC.
Only the modular TRTLLM MXFP4 experts can stop after GEMM2; the fused
kernel takes as many tokens as the mHC overlap path.
"""
moe_config = self.experts.moe_config
experts_cls = getattr(
self.experts.routed_experts.quant_method, "experts_cls", None
)
if (
experts_cls is TrtLlmMxfp4ExpertsModular
and self.experts.routed_scaling_factor == 1.0
):
moe_config.defer_moe_finalize(MHC_OVERLAP_MAX_TOKENS)
if moe_config.use_deferred_moe_finalize:
logger.info_once(
"DSV4.1 mHC: MoE top-k finalize fused into the all-reduce for "
"up to %d tokens.",
MHC_OVERLAP_MAX_TOKENS,
)
def defers_finalize(self, num_tokens: int) -> bool:
return (
not self.use_mega_moe
and self.experts.moe_config.should_defer_moe_finalize(num_tokens)
)
def forward_unfinalized(
self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None
) -> MoEOutput:
"""``forward`` with the routed top-k reduction and all-reduce left open."""
# The runner's custom op returns tensors only, so run its body directly.
shared_output, routed = _unpack(
self.experts._forward_impl(
hidden_states, hidden_states, hidden_states, input_ids
)
)
assert shared_output is not None
assert isinstance(routed, UnfinalizedMoEOutput)
return MoEOutput(routed=routed, shared_output=shared_output)
def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
"""Pick the CUDA sparse-MLA attention class for the configured backend.
@@ -394,7 +445,7 @@ class DeepseekV4DecoderLayer(nn.Module):
def forward(
self,
x: torch.Tensor,
x: torch.Tensor | MoEOutput,
positions: torch.Tensor,
input_ids: torch.Tensor | None,
pre_mix: torch.Tensor | None = None,
@@ -407,7 +458,7 @@ class DeepseekV4DecoderLayer(nn.Module):
capture_previous_aux: bool = False,
mega_gate_metadata: MegaGateRoutingMetadata | None = None,
) -> tuple[
torch.Tensor,
torch.Tensor | MoEOutput,
torch.Tensor,
torch.Tensor,
torch.Tensor,
@@ -432,6 +483,7 @@ class DeepseekV4DecoderLayer(nn.Module):
# sublayer's pre-mix: attention uses the pre-mix carried in (identity
# for the first layer), the FFN uses this layer's attention pre-mix.
if residual is None:
assert isinstance(x, torch.Tensor)
if x.dim() == 2:
# First layer: the stream is the embedding broadcast to hc
# copies and the identity pre-mix selects copy 0.
@@ -471,7 +523,9 @@ class DeepseekV4DecoderLayer(nn.Module):
# Engram injection happens between the previous sublayer's post
# and this block's pre, on the full hc stream, so the mix
# coefficients see the injected stream. The injection also keeps
# the post out of the pre-norm GEMM's fused prologue.
# the post out of the pre-norm GEMM's fused prologue. The layer
# before an engram layer finalizes its own MoE.
assert isinstance(x, torch.Tensor)
if self.fuse_mhc_all_reduce:
x = tensor_model_parallel_all_reduce(x)
previous_post = mhc_post_tilelang(x, residual, post_mix, res_mix)
@@ -552,7 +606,11 @@ class DeepseekV4DecoderLayer(nn.Module):
stream=mhc_stream,
reduce_results=self.fuse_mhc_all_reduce,
)
x = self.ffn(x, input_ids, mega_gate_metadata)
if self.fuse_mhc_all_reduce and self.ffn.defers_finalize(x.shape[0]):
# The next layer's first boundary finalizes it with the all-reduce.
x = self.ffn.forward_unfinalized(x, input_ids)
else:
x = self.ffn(x, input_ids, mega_gate_metadata)
if mhc_stream is not None:
torch.cuda.current_stream().wait_stream(mhc_stream)
return x, residual, post_mix, res_mix, ffn_pre, previous_aux
@@ -591,6 +649,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
self.fuse_mhc_all_reduce = mhc_stream is not None and supports_mhc_all_reduce(
vllm_config
)
if self.fuse_mhc_all_reduce:
init_mhc_all_reduce(vllm_config)
# Reserved topk indices buffer for all Indexer layers to reuse.
self.topk_indices_buffer = torch.empty(
@@ -670,6 +730,16 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
),
prefix=f"{prefix}.layers",
)
if self.fuse_mhc_all_reduce:
# A MoE's top-k finalize folds into the next layer's first mHC
# boundary, which the last local layer lacks and an engram layer
# replaces with its own all-reduce.
local_layers = list(islice(self.layers, self.start_layer, self.end_layer))
for layer, successor in zip(local_layers, local_layers[1:]):
assert isinstance(layer, DeepseekV4DecoderLayer)
assert isinstance(successor, DeepseekV4DecoderLayer)
if successor.engram is None:
layer.ffn.defer_finalize()
# The n-gram hash needs a slot-keyed rolling store of compressed ids
# (chunked prefill / decode lookback); key it off the first local
@@ -874,6 +944,8 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
if self.use_sequence_parallel:
previous_aux = sp_all_gather(previous_aux)[:full_num_tokens]
aux_hidden_by_layer[idx] = previous_aux
# Without a successor boundary, the last layer finalized its own MoE.
assert isinstance(hidden_states, torch.Tensor)
if layer is not None:
# The last layer has no successor to fold its post into.
if self.fuse_mhc_all_reduce:
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CuTe DSL kernels for DSV4.1 mHC."""
from .all_reduce_mhc import AllReduceMHC
__all__ = ["AllReduceMHC"]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,396 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Adapted from FlashInfer's flashinfer/comm/mnnvl_cutedsl/cute_dsl_primitives.py
# and runtime.py (flashinfer-ai/flashinfer@139af6f6).
"""PTX building blocks and launch helpers for the DSV4.1 LL all-reduce."""
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
import torch
from cutlass import Float32, Int32, Int64, Uint16, Uint32
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm, vector
from cutlass.cute.runtime import from_dlpack, make_fake_compact_tensor
from cutlass.cutlass_dsl import T, dsl_user_op
WARP_SIZE = 32
VEC_BF16 = 8
QUAD_BF16 = 4
NEGATIVE_ZERO_BF16_BITS = 0x8000
NEGATIVE_ZERO_BF16_PAIR = 0x80008000
def _asm(result, operands, text, constraints, *, side_effects, loc, ip):
return llvm.inline_asm(
result,
operands,
text,
constraints,
has_side_effects=side_effects,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def load_global_u32x4(
pointer: cute.Pointer,
*,
volatile: cutlass.Constexpr[bool] = False,
loc=None,
ip=None,
):
address = pointer.toint(loc=loc, ip=ip)
opcode = "ld.volatile.global.v4.u32" if volatile else "ld.global.v4.u32"
loaded = _asm(
llvm.StructType.get_literal([T.i32()] * 4),
[address.ir_value(loc=loc, ip=ip)],
f"{opcode} {{$0, $1, $2, $3}}, [$4];",
"=r,=r,=r,=r,l",
side_effects=volatile,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(4)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def load_global_u32x2(pointer: cute.Pointer, *, loc=None, ip=None):
address = pointer.toint(loc=loc, ip=ip)
loaded = _asm(
llvm.StructType.get_literal([T.i32()] * 2),
[address.ir_value(loc=loc, ip=ip)],
"ld.global.v2.u32 {$0, $1}, [$2];",
"=r,=r,l",
side_effects=False,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([2], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(2)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 2, Uint32)
@dsl_user_op
def load_global_bf16_as_f32(address: Int64, *, loc=None, ip=None) -> Float32:
return Float32(
_asm(
T.f32(),
[address.ir_value(loc=loc, ip=ip)],
"{\n\t.reg .b16 bits;\n\tld.global.b16 bits, [$1];\n\t"
"cvt.f32.bf16 $0, bits;\n\t}",
"=f,l",
side_effects=False,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def store_global_u32x4(address: Int64, packed, *, loc=None, ip=None) -> None:
words = [packed[index].ir_value(loc=loc, ip=ip) for index in range(4)]
_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"st.global.v4.u32 [$0], {$1, $2, $3, $4};",
"l,r,r,r,r",
side_effects=True,
loc=loc,
ip=ip,
)
@dsl_user_op
def store_lamport_sentinel_u32x4(address: Int64, *, loc=None, ip=None) -> None:
sentinel = Uint32(NEGATIVE_ZERO_BF16_PAIR).ir_value(loc=loc, ip=ip)
_asm(
None,
[address.ir_value(loc=loc, ip=ip), sentinel, sentinel, sentinel, sentinel],
"st.global.v4.u32 [$0], {$1, $2, $3, $4};",
"l,r,r,r,r",
side_effects=True,
loc=loc,
ip=ip,
)
@dsl_user_op
def load_volatile_u32(pointer: cute.Pointer, *, loc=None, ip=None) -> Uint32:
address = pointer.toint(loc=loc, ip=ip)
return Uint32(
_asm(
T.i32(),
[address.ir_value(loc=loc, ip=ip)],
"ld.volatile.global.u32 $0, [$1];",
"=r,l",
side_effects=True,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def store_global_u32(
pointer: cute.Pointer, value: Uint32, *, loc=None, ip=None
) -> None:
address = pointer.toint(loc=loc, ip=ip)
_asm(
None,
[address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
"st.global.u32 [$0], $1;",
"l,r",
side_effects=True,
loc=loc,
ip=ip,
)
@dsl_user_op
def packed_u32x4_to_bf16x8(packed, *, loc=None, ip=None):
values = llvm.bitcast(
ir.VectorType.get([VEC_BF16], cutlass.BFloat16.mlir_type, loc=loc),
packed.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(values, VEC_BF16, cutlass.BFloat16)
@dsl_user_op
def packed_u32x2_to_bf16x4(packed, *, loc=None, ip=None):
values = llvm.bitcast(
ir.VectorType.get([QUAD_BF16], cutlass.BFloat16.mlir_type, loc=loc),
packed.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(values, QUAD_BF16, cutlass.BFloat16)
@dsl_user_op
def bf16x4_to_packed_u32x2(values, *, loc=None, ip=None):
packed = llvm.bitcast(
ir.VectorType.get([2], T.i32(), loc=loc),
values.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 2, Uint32)
@dsl_user_op
def bf16x8_to_packed_u32x4(values, *, loc=None, ip=None):
packed = llvm.bitcast(
ir.VectorType.get([4], T.i32(), loc=loc),
values.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@cute.jit
def sanitize_negative_zero_u32(word: Uint32) -> Uint32:
low = Uint16(word & Uint32(0xFFFF))
high = Uint16(word >> Uint32(16))
if low == Uint16(NEGATIVE_ZERO_BF16_BITS):
word = word & Uint32(0xFFFF0000)
if high == Uint16(NEGATIVE_ZERO_BF16_BITS):
word = word & Uint32(0x0000FFFF)
return word
@cute.jit
def sanitize_negative_zero_u32x4(packed):
sanitized = cute.make_rmem_tensor(cute.make_layout((4,)), Uint32)
for index in cutlass.range_constexpr(4):
sanitized[index] = sanitize_negative_zero_u32(packed[index])
return sanitized.load()
@cute.jit
def sanitize_negative_zero_u32x2(packed):
sanitized = cute.make_rmem_tensor(cute.make_layout((2,)), Uint32)
for index in cutlass.range_constexpr(2):
sanitized[index] = sanitize_negative_zero_u32(packed[index])
return sanitized.load()
@cute.jit
def fragment_has_negative_zero(packed):
dirty = False
for index in cutlass.range_constexpr(4):
word = packed[index]
dirty = (
dirty
| (Uint16(word & Uint32(0xFFFF)) == Uint16(NEGATIVE_ZERO_BF16_BITS))
| (Uint16(word >> Uint32(16)) == Uint16(NEGATIVE_ZERO_BF16_BITS))
)
return dirty
@dsl_user_op
def map_shared_to_peer(
smem_pointer: cute.Pointer, peer_rank: Int32, *, loc=None, ip=None
) -> Int32:
address = smem_pointer.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
return Int32(
_asm(
T.i32(),
[address, peer_rank.ir_value(loc=loc, ip=ip)],
"mapa.shared::cluster.u32 $0, $1, $2;",
"=r,r,r",
side_effects=False,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def store_shared_cluster_f32(
remote_address: Int32, value: Float32, *, loc=None, ip=None
) -> None:
_asm(
None,
[remote_address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
"st.shared::cluster.f32 [$0], $1;",
"r,f",
side_effects=True,
loc=loc,
ip=ip,
)
@dsl_user_op
def f32_to_bf16_bits(value: Float32, *, loc=None, ip=None) -> Uint32:
return Uint32(
_asm(
T.i32(),
[value.ir_value(loc=loc, ip=ip)],
"{\n\t.reg .b16 bits;\n\tcvt.rn.bf16.f32 bits, $1;\n\t"
"cvt.u32.u16 $0, bits;\n\t}",
"=r,f",
side_effects=False,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def shuffle_sync_idx_u32(
value: Uint32, source_lane: Int32, *, loc=None, ip=None
) -> Uint32:
return Uint32(
_asm(
T.i32(),
[value.ir_value(loc=loc, ip=ip), source_lane.ir_value(loc=loc, ip=ip)],
"shfl.sync.idx.b32 $0, $1, $2, 0x1f, 0xffffffff;",
"=r,r,r",
# Preserve full-warp execution across later divergent consumers.
side_effects=True,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def stmc_bf16x2(address: Int64, packed: Uint32, *, loc=None, ip=None) -> None:
_asm(
None,
[address.ir_value(loc=loc, ip=ip), packed.ir_value(loc=loc, ip=ip)],
"multimem.st.relaxed.sys.global.bf16x2 [$0], $1;",
"l,r",
side_effects=True,
loc=loc,
ip=ip,
)
@dsl_user_op
def stmc_bf16x4(address: Int64, values, *, loc=None, ip=None) -> None:
words = [values[index].ir_value(loc=loc, ip=ip) for index in range(2)]
_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"multimem.st.relaxed.sys.global.v2.bf16x2 [$0], {$1, $2};",
"l,r,r",
side_effects=True,
loc=loc,
ip=ip,
)
@dsl_user_op
def stmc_bf16x8(address: Int64, values, *, loc=None, ip=None) -> None:
words = [values[index].ir_value(loc=loc, ip=ip) for index in range(4)]
_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"multimem.st.relaxed.sys.global.v4.bf16x2 [$0], {$1, $2, $3, $4};",
"l,r,r,r,r",
side_effects=True,
loc=loc,
ip=ip,
)
class _GraphSafeDLPack:
__slots__ = ("tensor",)
def __init__(self, tensor: torch.Tensor) -> None:
self.tensor = tensor
def __dlpack__(self, stream=None):
# stream=-1 skips producer sync; CuTe launches on the current stream,
# which is the capturing one under CUDA graphs.
return self.tensor.__dlpack__(stream=-1)
def __dlpack_device__(self):
return self.tensor.__dlpack_device__()
def to_cute(tensor: torch.Tensor, alignment: int) -> cute.Tensor:
return from_dlpack(_GraphSafeDLPack(tensor.detach()), assumed_align=alignment)
def to_cute_dynamic(
tensor: torch.Tensor, alignment: int, *, divisibility: int
) -> cute.Tensor:
return to_cute(tensor, alignment).mark_compact_shape_dynamic(
mode=0, divisibility=divisibility
)
def make_fake_dynamic_compact_tensor(dtype, *, alignment: int, divisibility: int):
return make_fake_compact_tensor(
dtype, (cute.sym_int32(divisibility=divisibility),), assumed_align=alignment
)
def current_cu_stream() -> cuda.CUstream:
return cuda.CUstream(torch.cuda.current_stream().cuda_stream)
+73 -18
View File
@@ -9,10 +9,15 @@ import torch
from vllm.config import VllmConfig
from vllm.distributed import get_tp_group
from vllm.logger import init_logger
from vllm.model_executor.kernels.mhc.tilelang import (
mhc_fused_post_pre_delayed_tilelang,
mhc_post_tilelang,
)
from vllm.model_executor.layers.fused_moe.moe_output import (
MoEOutput,
UnfinalizedMoEOutput,
)
from vllm.platforms import current_platform
from vllm.utils.deep_gemm import is_deep_gemm_supported
@@ -24,9 +29,17 @@ from .mega_mhc import (
if TYPE_CHECKING:
from vllm.distributed.device_communicators.cuda_communicator import CudaCommunicator
from .cute_dsl import AllReduceMHC
logger = init_logger(__name__)
# GB200 TP4/FlashInfer improves through 16 tokens; larger screens tie or regress.
MHC_OVERLAP_MAX_TOKENS = 16
# The fused all-reduce + mHC kernel of the overlap path, built once by
# init_mhc_all_reduce.
_all_reduce_mhc: "AllReduceMHC | None" = None
def supports_mhc_overlap(vllm_config: VllmConfig) -> bool:
"""Check kernel requirements and safety of sharing the coefficient stream."""
@@ -53,10 +66,35 @@ def supports_mhc_all_reduce(vllm_config: VllmConfig) -> bool:
or config.hc_mult != 4
):
return False
# The custom all-reduce's MNNVL buffers succeed exactly when NVLink
# multicast does, which the fused kernel's own mailbox needs too.
comm = cast("CudaCommunicator", get_tp_group().device_communicator).ca_comm
return comm is not None and bool(comm.mnnvl_lamport_ag_multicast_ptr)
def init_mhc_all_reduce(vllm_config: VllmConfig) -> None:
"""Build the fused all-reduce + mHC kernel the overlap path reduces with.
Collective over the TP group, so every rank must call it, and only when
``supports_mhc_all_reduce`` holds.
"""
global _all_reduce_mhc
from .cute_dsl import AllReduceMHC
config = vllm_config.model_config.hf_config
_all_reduce_mhc = AllReduceMHC(
hidden_size=config.hidden_size,
hc_mult=config.hc_mult,
max_num_tokens=MHC_OVERLAP_MAX_TOKENS,
top_k=config.num_experts_per_tok,
device=current_platform.current_device(),
)
logger.info_once(
"DSV4.1 mHC: CuTe DSL Lamport all-reduce fused with mHC for up to %d tokens.",
MHC_OVERLAP_MAX_TOKENS,
)
def mhc_pre_delayed_overlap(
residual: torch.Tensor,
fn: torch.Tensor,
@@ -151,7 +189,7 @@ def mhc_pre_delayed_overlap(
def mhc_shifted_post_pre(
x: torch.Tensor,
x: torch.Tensor | MoEOutput,
residual: torch.Tensor,
post_layer_mix: torch.Tensor,
comb_res_mix: torch.Tensor,
@@ -175,39 +213,55 @@ def mhc_shifted_post_pre(
]:
"""Dispatch shifted post/pre to overlap, Mega-mHC, or fused TileLang.
A MoE output left unfinalized is finalized inside the fused all-reduce.
When stream is supplied, join it before consuming the returned coefficients.
"""
layer_input = None
if reduce_results:
tp = get_tp_group()
if isinstance(x, MoEOutput):
assert _all_reduce_mhc is not None, "init_mhc_all_reduce was not called"
assert pre_mix is not None and norm_weight is not None
routed = x.routed
assert isinstance(routed, UnfinalizedMoEOutput)
assert x.shared_output is not None
residual, layer_input = _all_reduce_mhc.finalize(
routed.gemm2_permuted,
routed.expert_weights,
routed.expanded_idx_to_permuted_idx,
x.shared_output,
residual,
post_layer_mix,
comb_res_mix,
pre_mix,
norm_weight,
norm_eps,
)
if stream is None:
# Outside FULL graphs, compute the next mixes inline.
stream = torch.cuda.current_stream()
elif reduce_results:
if stream is not None and 0 < x.shape[0] <= MHC_OVERLAP_MAX_TOKENS:
comm = cast("CudaCommunicator", tp.device_communicator).ca_comm
assert comm is not None and comm.mnnvl_lamport_epochs is not None
output = torch.empty_like(residual)
layer_input = torch.empty_like(x)
torch.ops._C_custom_ar.all_reduce_mhc(
assert _all_reduce_mhc is not None, "init_mhc_all_reduce was not called"
assert pre_mix is not None and norm_weight is not None
residual, layer_input = _all_reduce_mhc(
x,
residual,
post_layer_mix,
comb_res_mix,
pre_mix,
norm_weight,
output,
layer_input,
comm.mnnvl_lamport_ag_local_ptr,
comm.mnnvl_lamport_ag_multicast_ptr,
comm.mnnvl_lamport_epochs[0],
comm.rank,
comm.mnnvl_buffer_size,
norm_eps,
)
residual = output
else:
x = tp.all_reduce(x)
x = get_tp_group().all_reduce(x)
if stream is not None:
if layer_input is None:
assert isinstance(x, torch.Tensor)
residual = mhc_post_tilelang(x, residual, post_layer_mix, comb_res_mix)
aux = residual.mean(dim=1) if capture_aux else x.new_empty(0, x.shape[1])
aux = (
residual.mean(dim=1)
if capture_aux
else residual.new_empty(0, residual.shape[-1])
)
pre_outputs = mhc_pre_delayed_overlap(
residual,
fn,
@@ -226,6 +280,7 @@ def mhc_shifted_post_pre(
)
return residual, *pre_outputs, aux
assert isinstance(x, torch.Tensor)
if can_use_mega_mhc(x, residual, pre_mix, norm_weight, capture_aux):
assert pre_mix is not None and norm_weight is not None
outputs = mhc_shifted_post_pre_deep_gemm(