[Kernel][DSV4.1] Fuse MXFP8 wo_b GEMM with sequence-parallel reduce-scatter (#57428)

Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
Signed-off-by: Canlin <canlinguosdu@gmail.com>
Co-authored-by: Thien Tran <gau.nernst@yahoo.com.sg>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
This commit is contained in:
Canlin Guo
2026-09-22 01:07:17 -07:00
committed by GitHub
co-authored by Thien Tran OpenAI Codex Claude Fable 5.1 Jee Jee Li
parent a6c47fbbf4
commit f92b78f6ef
18 changed files with 1037 additions and 624 deletions
+6 -5
View File
@@ -387,17 +387,18 @@ steps:
- pytest -v -s tests/v1/distributed/test_dbo.py
- pytest -v -s tests/distributed/test_mnnvl_alltoall.py
- label: ":nvidia: (B200) Kimi-K3 GEMM-RS/AR"
key: kimi-k3-gemm-rs-ar-2xb200
- label: ":nvidia: (B200) GEMM-RS/AR"
key: gemm-rs-ar-2xb200
timeout_in_minutes: 20
device: b200-k8s
working_dir: "/vllm-workspace/"
num_devices: 2
source_file_dependencies:
- vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py
- tests/kernels/test_kimi_k3_gemm_rs_ar.py
- vllm/model_executor/kernels/linear/cute_dsl/gemm_rs_ar.py
- vllm/cute_utils/
- tests/kernels/test_gemm_rs_ar.py
commands:
- pytest -v -s tests/kernels/test_kimi_k3_gemm_rs_ar.py
- pytest -v -s tests/kernels/test_gemm_rs_ar.py
- label: ":nvidia: (L4) Distributed 2-Node"
key: 2-node-test-4-gpus
+407
View File
@@ -0,0 +1,407 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Benchmark the SM100 GEMM-RS/AR kernel against the model's unfused path.
The baseline is what a model runs without the fusion: the projection's own
linear kernel followed by vLLM's collective (``sp_reduce_scatter`` for RS,
``tensor_model_parallel_all_reduce`` for AR), with the collective backend
chosen by vLLM's dispatch exactly as in serving. The same two kernels are also
timed bare (the GEMM alone, the collective alone on a precomputed partial);
their max is the floor a perfectly overlapped fused kernel could reach.
All ranks must belong to one NVLink domain. Run a TP8 sweep over the Kimi-K3
projections with:
torchrun --nproc-per-node=8 -- benchmarks/kernels/benchmark_gemm_rs_ar.py
or the DeepSeek-V4.1 MXFP8 ``wo_b`` projection at TP4 with:
torchrun --nproc-per-node=4 -- benchmarks/kernels/benchmark_gemm_rs_ar.py \
--model deepseek_v41 --backend flashinfer_cutedsl
"""
import argparse
import json
import os
import statistics
from collections.abc import Callable
import torch
import torch.distributed as dist
from vllm.config import ModelConfig, VllmConfig, set_current_vllm_config
from vllm.distributed import (
cleanup_dist_env_and_memory,
tensor_model_parallel_all_reduce,
)
from vllm.distributed.parallel_state import (
get_tp_group,
init_distributed_environment,
initialize_model_parallel,
)
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
from vllm.model_executor.layers.linear import RowParallelLinear
from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter
# Per model: (hidden size N, global input widths K). Kimi-K3 lists the
# shared-expert down-proj and attention O-proj; DeepSeek-V4.1 lists the
# ``wo_b`` output projection (o_groups * o_lora_rank).
_MODEL_PROJECTIONS = {
"kimi_k3": (7168, (6144, 12288)),
"deepseek_v41": (5120, (8192,)),
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--mode",
choices=("rs", "ar"),
default="rs",
help="Collective mode to benchmark.",
)
parser.add_argument(
"--m",
type=int,
nargs="+",
default=[128, 512, 2048, 8192, 32768],
help="Global token counts to benchmark.",
)
parser.add_argument(
"--model",
choices=tuple(_MODEL_PROJECTIONS),
default="kimi_k3",
help="Model whose projection shapes set the default --n and --k.",
)
parser.add_argument(
"--backend",
choices=("bf16", "flashinfer_cutlass", "flashinfer_cutedsl"),
default="bf16",
help="Weight dtype/layout: BF16 or an online-MXFP8 FlashInfer kernel.",
)
parser.add_argument(
"--k",
type=int,
nargs="+",
help=(
"Per-rank input dimensions. By default, derive the selected "
"model's projection dimensions from the TP world size."
),
)
parser.add_argument(
"--n",
type=int,
help="Output width. Defaults to the selected model's hidden size.",
)
parser.add_argument(
"--num-workspaces",
type=int,
default=10,
help="Pointer-distinct inputs and CUDA graphs to rotate.",
)
parser.add_argument("--warmup-replays", type=int, default=5)
parser.add_argument("--samples", type=int, default=20)
parser.add_argument("--json", type=str, help="Also write the results here.")
return parser.parse_args()
def make_projection(
gemm_rs_ar: GemmRsAr,
weight: torch.Tensor,
world_size: int,
backend: str,
) -> RowParallelLinear:
"""Wrap a BF16 [N, K] weight the way the model would hand it to GEMM-RS.
For the MXFP8 backends this quantizes online through the FlashInfer
kernel's post-load step, so the fused kernel and the unfused baseline see
the production weight and scale layouts.
"""
N, K = weight.shape
quant_config = None
if backend != "bf16":
from vllm.config.quantization import QuantizationConfigArgs
from vllm.model_executor.layers.quantization.online.base import (
OnlineQuantizationConfig,
)
quant_config = OnlineQuantizationConfig(QuantizationConfigArgs(linear="mxfp8"))
with torch.device(weight.device):
linear = RowParallelLinear(
K * world_size,
N,
bias=False,
params_dtype=weight.dtype,
quant_config=quant_config,
# The baseline reduces the local partial itself, like the models.
reduce_results=False,
return_bias=False,
)
# Select the fused path before online quantization replaces the weights.
assert gemm_rs_ar.can_run(linear)
linear.weight = torch.nn.Parameter(weight, requires_grad=False)
linear.quant_method.process_weights_after_loading(linear)
return linear
def capture_graph(
op: Callable[[], torch.Tensor],
stream: torch.cuda.Stream,
cpu_group: dist.ProcessGroup,
) -> torch.cuda.CUDAGraph:
stream.wait_stream(torch.cuda.current_stream())
dist.barrier(group=cpu_group)
with torch.cuda.stream(stream):
for _ in range(3):
op()
stream.synchronize()
dist.barrier(group=cpu_group)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, stream=stream):
op()
torch.cuda.current_stream().wait_stream(stream)
dist.barrier(group=cpu_group)
return graph
def benchmark_graphs(
candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]],
warmup_replays: int,
samples: int,
device_group: dist.ProcessGroup,
device_barrier: Callable[[], None],
) -> dict[str, float]:
"""Median over samples of the slowest rank's replay time, in us."""
names = list(candidate_graphs)
for round_index in range(warmup_replays):
for offset in range(len(names)):
name = names[(round_index + offset) % len(names)]
graphs = candidate_graphs[name]
device_barrier()
graphs[round_index % len(graphs)].replay()
torch.accelerator.synchronize()
timings: dict[str, list[float]] = {name: [] for name in names}
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
for sample_index in range(samples):
for offset in range(len(names)):
name = names[(sample_index + offset) % len(names)]
graphs = candidate_graphs[name]
device_barrier()
start.record()
graphs[sample_index % len(graphs)].replay()
end.record()
end.synchronize()
elapsed = torch.tensor(
start.elapsed_time(end) * 1000,
dtype=torch.float64,
device=torch.accelerator.current_device_index(),
)
dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group)
timings[name].append(elapsed.item())
return {name: statistics.median(values) for name, values in timings.items()}
def benchmark_shape(
gemm_rs_ar: GemmRsAr,
mode: str,
backend: str,
M: int,
N: int,
K: int,
num_workspaces: int,
warmup_replays: int,
samples: int,
device_group: dist.ProcessGroup,
cpu_group: dist.ProcessGroup,
device_barrier: Callable[[], None],
) -> dict[str, float | int | str]:
all_reduce = mode == "ar"
world_size = dist.get_world_size(device_group)
rank = dist.get_rank(device_group)
device = torch.device("cuda", torch.accelerator.current_device_index())
local_M = (M + world_size - 1) // world_size
rng = torch.Generator(device=device)
rng.manual_seed(1000 + rank * 10 + M + K)
inputs = [
torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng)
for _ in range(num_workspaces)
]
projections = [
make_projection(
gemm_rs_ar,
torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng),
world_size,
backend,
)
for _ in range(num_workspaces)
]
def collective(partial: torch.Tensor) -> torch.Tensor:
if all_reduce:
return tensor_model_parallel_all_reduce(partial)
return sp_reduce_scatter(partial)
def gemm(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor:
return linear(x)
def unfused(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor:
return collective(linear(x))
def fused(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor:
return gemm_rs_ar.apply(x, linear)
# The bare collective runs on the GEMM's own output so it sees the same
# message size and layout the unfused path hands to the collective.
partials = [linear(x) for x, linear in zip(inputs, projections)]
expected = unfused(inputs[0], projections[0])
actual = fused(inputs[0], projections[0])
torch.accelerator.synchronize(device)
rows = M if all_reduce else min(max(M - rank * local_M, 0), local_M)
torch.testing.assert_close(
actual[:rows], expected[:rows], rtol=5e-2, atol=4.0, msg=f"M={M}, K={K}"
)
candidates: dict[str, list[Callable[[], torch.Tensor]]] = {
"gemm": [lambda x=x, w=w: gemm(x, w) for x, w in zip(inputs, projections)],
"collective": [lambda p=p: collective(p) for p in partials],
"vllm": [lambda x=x, w=w: unfused(x, w) for x, w in zip(inputs, projections)],
"gemm_rs_ar": [
lambda x=x, w=w: fused(x, w) for x, w in zip(inputs, projections)
],
}
candidate_graphs = {}
keepalive: list[object] = []
for name, ops in candidates.items():
stream = torch.cuda.Stream()
candidate_graphs[name] = [capture_graph(op, stream, cpu_group) for op in ops]
keepalive.append(stream)
times = benchmark_graphs(
candidate_graphs, warmup_replays, samples, device_group, device_barrier
)
floor = max(times["gemm"], times["collective"])
return {
"mode": mode.upper(),
"backend": backend,
"M": M,
"N": N,
"K": K,
"gemm_us": times["gemm"],
"collective_us": times["collective"],
"floor_us": floor,
"vllm_us": times["vllm"],
"gemm_rs_ar_us": times["gemm_rs_ar"],
"latency_change": (times["gemm_rs_ar"] - times["vllm"]) / times["vllm"],
"floor_gap": (times["gemm_rs_ar"] - floor) / floor,
}
def print_results(results: list[dict[str, float | int | str]]) -> None:
collective = results[0]["mode"]
backend = results[0]["backend"]
print(f"### GEMM-{collective} vs vLLM unfused path ({backend} weights)")
print(
f"| M | N | K | GEMM (us) | {collective} (us) | floor (us) | vLLM (us) | "
f"GEMM-{collective} (us) | vs vLLM | vs floor |"
)
print("|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|")
for r in results:
print(
f"| {r['M']} | {r['N']} | {r['K']} | {r['gemm_us']:.1f} | "
f"{r['collective_us']:.1f} | {r['floor_us']:.1f} | {r['vllm_us']:.1f} | "
f"{r['gemm_rs_ar_us']:.1f} | {r['latency_change']:+.1%} | "
f"{r['floor_gap']:+.1%} |"
)
def main() -> None:
args = parse_args()
default_n, default_k = _MODEL_PROJECTIONS[args.model]
if args.n is None:
args.n = default_n
assert args.m and min(args.m) > 0
assert args.n % 128 == 0
assert args.num_workspaces > 0
assert args.warmup_replays >= 0
assert args.samples > 0
local_rank = int(os.environ["LOCAL_RANK"])
torch.accelerator.set_device_index(local_rank)
init_distributed_environment()
world_size = dist.get_world_size()
if args.k is None:
assert all(K % world_size == 0 for K in default_k)
K_values = [K // world_size for K in default_k]
else:
K_values = args.k
k_alignment = 64 if args.backend == "bf16" else 128
assert all(K % k_alignment == 0 for K in K_values)
config = VllmConfig()
config.model_config = ModelConfig(dtype="bfloat16")
# Pin the online-MXFP8 kernel the projections quantize through.
config.kernel_config.linear_backend = (
"auto" if args.backend == "bf16" else args.backend
)
with set_current_vllm_config(config):
# Builds the TP communicator as in serving, so the baseline's
# collective is whatever vLLM's dispatch selects (FlashInfer all-reduce
# first, then NCCL symmetric memory, custom all-reduce, pynccl).
initialize_model_parallel(tensor_model_parallel_size=world_size)
tp_group = get_tp_group()
pynccl_comm = tp_group.device_communicator.pynccl_comm
assert pynccl_comm is not None
sync_input = torch.zeros(1, device=torch.accelerator.current_device_index())
sync_output = torch.empty_like(sync_input)
def device_barrier() -> None:
# Order the timed launch after a device-side rank rendezvous without
# including the rendezvous itself in the measured event interval.
pynccl_comm.all_reduce(sync_input, sync_output)
gemm_rs_ar = GemmRsAr(
max_M=max(args.m),
N=args.n,
all_reduce=args.mode == "ar",
)
with set_current_vllm_config(config):
results = [
benchmark_shape(
gemm_rs_ar,
args.mode,
args.backend,
M,
args.n,
K,
args.num_workspaces,
args.warmup_replays,
args.samples,
tp_group.device_group,
tp_group.cpu_group,
device_barrier,
)
for K in K_values
for M in args.m
]
del gemm_rs_ar
if tp_group.rank_in_group == 0:
print_results(results)
if args.json:
with open(args.json, "w") as f:
json.dump(results, f, indent=2)
dist.barrier(group=tp_group.cpu_group)
cleanup_dist_env_and_memory()
if __name__ == "__main__":
main()
@@ -1,469 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Benchmark the SM100 Kimi-K3 GEMM-RS/AR kernel.
All ranks must belong to one NVLink domain. For example, run a TP8 sweep with:
torchrun --nproc-per-node=8 \
benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py
"""
import argparse
import os
import statistics
from collections.abc import Callable
from dataclasses import dataclass
import pandas as pd
import torch
import torch.distributed as dist
import torch.distributed._symmetric_memory as symm_mem
from vllm.config import VllmConfig, set_current_vllm_config
from vllm.distributed import cleanup_dist_env_and_memory
from vllm.distributed.parallel_state import (
get_tp_group,
init_distributed_environment,
initialize_model_parallel,
)
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import GemmRsAr
# Shared-expert down-proj and attention O-proj.
_KIMI_K3_PROJECTION_K = (6144, 12288)
@dataclass
class Candidate:
name: str
runs: list[Callable[[], torch.Tensor]]
check_correctness: bool = True
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--mode",
choices=("rs", "ar"),
default="rs",
help="Collective mode to benchmark.",
)
parser.add_argument(
"--m",
type=int,
nargs="+",
default=[128, 512, 2048, 8192, 32768],
help="Global token counts to benchmark.",
)
parser.add_argument(
"--k",
type=int,
nargs="+",
help=(
"Per-rank input dimensions. By default, derive the Kimi-K3 "
"shared-expert down-proj and O-proj dimensions from the TP "
"world size."
),
)
parser.add_argument("--n", type=int, default=7168)
parser.add_argument(
"--num-workspaces",
type=int,
default=10,
help="Pointer-distinct inputs and CUDA graphs to rotate.",
)
parser.add_argument("--warmup-replays", type=int, default=5)
parser.add_argument("--samples", type=int, default=20)
return parser.parse_args()
def capture_graph(
op: Callable[[], torch.Tensor],
stream: torch.cuda.Stream,
cpu_group: dist.ProcessGroup,
) -> tuple[torch.cuda.CUDAGraph, list[torch.Tensor | None]]:
result: list[torch.Tensor | None] = [None]
stream.wait_stream(torch.cuda.current_stream())
dist.barrier(group=cpu_group)
with torch.cuda.stream(stream):
for _ in range(3):
result[0] = op()
stream.synchronize()
dist.barrier(group=cpu_group)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, stream=stream):
result[0] = op()
torch.cuda.current_stream().wait_stream(stream)
dist.barrier(group=cpu_group)
return graph, result
def benchmark_graphs(
candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]],
warmup_replays: int,
samples: int,
device_group: dist.ProcessGroup,
device_barrier: Callable[[], None],
) -> dict[str, float]:
candidate_names = list(candidate_graphs)
for round_index in range(warmup_replays):
for candidate_index in range(len(candidate_names)):
candidate_id = (round_index + candidate_index) % len(candidate_names)
name = candidate_names[candidate_id]
graphs = candidate_graphs[name]
device_barrier()
graphs[round_index % len(graphs)].replay()
torch.accelerator.synchronize()
timings: dict[str, list[float]] = {name: [] for name in candidate_names}
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
for sample_index in range(samples):
for candidate_index in range(len(candidate_names)):
candidate_id = (sample_index + candidate_index) % len(candidate_names)
name = candidate_names[candidate_id]
graphs = candidate_graphs[name]
device_barrier()
start.record()
graphs[sample_index % len(graphs)].replay()
end.record()
end.synchronize()
elapsed = torch.tensor(
start.elapsed_time(end) * 1000,
dtype=torch.float64,
device=torch.accelerator.current_device_index(),
)
dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group)
timings[name].append(elapsed.item())
return {name: statistics.median(values) for name, values in timings.items()}
def valid_rows(M: int, local_M: int, rank: int) -> int:
return min(max(M - rank * local_M, 0), local_M)
def benchmark_shape(
gemm_rs_ar: GemmRsAr,
mode: str,
M: int,
N: int,
K: int,
num_workspaces: int,
warmup_replays: int,
samples: int,
device_group: dist.ProcessGroup,
cpu_group: dist.ProcessGroup,
device_barrier: Callable[[], None],
) -> dict[str, float | int | str]:
all_reduce = mode == "ar"
world_size = dist.get_world_size(device_group)
rank = dist.get_rank(device_group)
device = torch.device("cuda", torch.accelerator.current_device_index())
padded_M = (M + world_size - 1) // world_size * world_size
local_M = padded_M // world_size
rng = torch.Generator(device=device)
rng.manual_seed(1000 + rank * 10 + M + K)
inputs = [
torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng)
for _ in range(num_workspaces)
]
weights = [
torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng)
for _ in range(num_workspaces)
]
partial = torch.empty((padded_M, N), dtype=torch.bfloat16, device=device)
symm_partial = symm_mem.empty((padded_M, N), dtype=torch.bfloat16, device=device)
symm_partial_handle = symm_mem.rendezvous(symm_partial, device_group)
collective_inputs = [torch.empty_like(partial) for _ in range(num_workspaces)]
symm_collective_inputs = []
symm_collective_handles = []
for _ in range(num_workspaces):
collective_input = symm_mem.empty(
(padded_M, N),
dtype=torch.bfloat16,
device=device,
)
symm_collective_inputs.append(collective_input)
symm_collective_handles.append(
symm_mem.rendezvous(collective_input, device_group)
)
torch_output = torch.empty((local_M, N), dtype=torch.bfloat16, device=device)
symm_output = torch.empty_like(torch_output)
gemm_output = torch.empty((M, N), dtype=torch.bfloat16, device=device)
if padded_M > M:
partial[M:].zero_()
symm_partial[M:].zero_()
def make_torch_ring_ll_gemm_collective(
x: torch.Tensor, weight: torch.Tensor
) -> Callable[[], torch.Tensor]:
def run() -> torch.Tensor:
torch.mm(x, weight.T, out=partial[:M])
if all_reduce:
dist.all_reduce(partial, group=device_group)
return partial[:M]
dist.reduce_scatter_single(torch_output, partial, group=device_group)
return torch_output
return run
def make_torch_ldmc_gemm_collective(
x: torch.Tensor, weight: torch.Tensor
) -> Callable[[], torch.Tensor]:
def run() -> torch.Tensor:
torch.mm(x, weight.T, out=symm_partial[:M])
if all_reduce:
dist.all_reduce(symm_partial, group=device_group)
return symm_partial[:M]
dist.reduce_scatter_single(
symm_output,
symm_partial,
group=device_group,
)
return symm_output
return run
def make_fused_gemm_collective(
x: torch.Tensor, weight: torch.Tensor
) -> Callable[[], torch.Tensor]:
def run() -> torch.Tensor:
return gemm_rs_ar(x, weight)
return run
def make_torch_gemm(
x: torch.Tensor,
weight: torch.Tensor,
) -> Callable[[], torch.Tensor]:
def run() -> torch.Tensor:
return torch.mm(x, weight.T, out=gemm_output)
return run
def make_ring_ll_collective(
collective_input: torch.Tensor,
) -> Callable[[], torch.Tensor]:
def run() -> torch.Tensor:
if all_reduce:
dist.all_reduce(collective_input, group=device_group)
return collective_input
dist.reduce_scatter_single(
torch_output, collective_input, group=device_group
)
return torch_output
return run
def make_ldmc_collective(
collective_input: torch.Tensor,
) -> Callable[[], torch.Tensor]:
def run() -> torch.Tensor:
if all_reduce:
dist.all_reduce(collective_input, group=device_group)
return collective_input
dist.reduce_scatter_single(
symm_output, collective_input, group=device_group
)
return symm_output
return run
candidates = (
Candidate(
"ring_ll_us",
[make_torch_ring_ll_gemm_collective(x, w) for x, w in zip(inputs, weights)],
),
Candidate(
"ldmc_us",
[make_torch_ldmc_gemm_collective(x, w) for x, w in zip(inputs, weights)],
),
Candidate(
"gemm_rs_ar_us",
[make_fused_gemm_collective(x, w) for x, w in zip(inputs, weights)],
),
Candidate(
"torch_gemm_us",
[make_torch_gemm(x, w) for x, w in zip(inputs, weights)],
check_correctness=False,
),
Candidate(
"ring_ll_collective_us",
[make_ring_ll_collective(x) for x in collective_inputs],
check_correctness=False,
),
Candidate(
"ldmc_collective_us",
[make_ldmc_collective(x) for x in symm_collective_inputs],
check_correctness=False,
),
)
expected = candidates[0].runs[0]()
rows = M if all_reduce else valid_rows(M, local_M, rank)
for candidate in candidates[1:]:
if not candidate.check_correctness:
continue
actual = candidate.runs[0]()
torch.accelerator.synchronize(device)
torch.testing.assert_close(
actual[:rows],
expected[:rows],
rtol=5e-2,
atol=4.0,
)
candidate_graphs = {}
graph_keepalive: list[object] = [symm_partial_handle, *symm_collective_handles]
for candidate in candidates:
stream = torch.cuda.Stream()
bundles = [capture_graph(run, stream, cpu_group) for run in candidate.runs]
candidate_graphs[candidate.name] = [graph for graph, _ in bundles]
graph_keepalive.extend(bundles)
graph_keepalive.append(stream)
times = benchmark_graphs(
candidate_graphs,
warmup_replays,
samples,
device_group,
device_barrier,
)
best_nccl_us = min(times["ring_ll_collective_us"], times["ldmc_collective_us"])
return {
"mode": mode.upper(),
"M": M,
"N": N,
"K": K,
**times,
"best_nccl_us": best_nccl_us,
"speedup_vs_ring_ll": times["ring_ll_us"] / times["gemm_rs_ar_us"],
"speedup_vs_ldmc": times["ldmc_us"] / times["gemm_rs_ar_us"],
}
def print_results(results: list[dict[str, float | int | str]]) -> None:
results_df = pd.DataFrame(results)
collective = str(results_df["mode"].iloc[0])
end_to_end = (
results_df[
[
"M",
"N",
"K",
"ring_ll_us",
"ldmc_us",
"gemm_rs_ar_us",
"speedup_vs_ring_ll",
"speedup_vs_ldmc",
]
]
.rename(
columns={
"ring_ll_us": f"Torch GEMM + NCCL {collective} (RING_LL) (us)",
"ldmc_us": f"Torch GEMM + NCCL {collective} (LDMC) (us)",
"gemm_rs_ar_us": f"GEMM-{collective} (us)",
"speedup_vs_ring_ll": "Speedup vs RING_LL",
"speedup_vs_ldmc": "Speedup vs LDMC",
}
)
.round(3)
)
components = (
results_df[["M", "N", "K", "torch_gemm_us", "best_nccl_us", "gemm_rs_ar_us"]]
.rename(
columns={
"torch_gemm_us": "Torch GEMM (us)",
"best_nccl_us": f"NCCL {collective} (best) (us)",
"gemm_rs_ar_us": f"GEMM-{collective} (us)",
}
)
.round(2)
)
print(f"### {collective} end-to-end latency")
print(end_to_end.to_markdown(index=False))
print(f"\n### {collective} component latency")
print(components.to_markdown(index=False))
print(
f"\nNCCL {collective} (best) is the faster of RING_LL and LDMC "
"for each shape.\n"
)
def main() -> None:
args = parse_args()
assert args.m and min(args.m) > 0
assert args.n % 256 == 0
assert args.num_workspaces > 0
assert args.warmup_replays >= 0
assert args.samples > 0
local_rank = int(os.environ["LOCAL_RANK"])
torch.accelerator.set_device_index(local_rank)
init_distributed_environment()
world_size = dist.get_world_size()
if args.k is None:
assert all(K % world_size == 0 for K in _KIMI_K3_PROJECTION_K)
K_values = [K // world_size for K in _KIMI_K3_PROJECTION_K]
else:
K_values = args.k
assert all(K % 64 == 0 for K in K_values)
# Reserve symmetric memory for the NCCL-managed benchmark allocations.
os.environ["VLLM_ALLREDUCE_USE_SYMM_MEM"] = "0"
# NCCL-managed symmetric allocations select the NVLS/LDMC collective path.
symm_mem.set_backend("NCCL")
with set_current_vllm_config(VllmConfig()):
initialize_model_parallel(tensor_model_parallel_size=world_size)
tp_group = get_tp_group()
group_warmup = torch.zeros(1, device=torch.accelerator.current_device_index())
dist.all_reduce(group_warmup, group=tp_group.device_group)
pynccl_comm = tp_group.device_communicator.pynccl_comm
assert pynccl_comm is not None
sync_input = torch.zeros(1, device=torch.accelerator.current_device_index())
sync_output = torch.empty_like(sync_input)
def device_barrier() -> None:
# Order the timed launch after a device-side rank rendezvous without
# including the rendezvous itself in the measured event interval.
pynccl_comm.all_reduce(sync_input, sync_output)
gemm_rs_ar = GemmRsAr(
max_M=max(args.m),
N=args.n,
all_reduce=args.mode == "ar",
)
results = [
benchmark_shape(
gemm_rs_ar,
args.mode,
M,
args.n,
K,
args.num_workspaces,
args.warmup_replays,
args.samples,
tp_group.device_group,
tp_group.cpu_group,
device_barrier,
)
for K in K_values
for M in args.m
]
del gemm_rs_ar
if tp_group.rank_in_group == 0:
print_results(results)
dist.barrier(group=tp_group.cpu_group)
cleanup_dist_env_and_memory()
if __name__ == "__main__":
main()
@@ -6,7 +6,7 @@ import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from tests.utils import ensure_current_vllm_config
from vllm.config import ModelConfig, VllmConfig, set_current_vllm_config
from vllm.distributed import cleanup_dist_env_and_memory
from vllm.distributed.parallel_state import (
init_distributed_environment,
@@ -17,6 +17,7 @@ from vllm.utils.network_utils import get_open_port
from vllm.utils.system_utils import update_environment_variables
_SHAPES = (
(129, 512),
(129, 768),
(257, 768),
(1023, 4224),
@@ -29,6 +30,7 @@ _N = 512
def _reference(
x: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor | None,
world_size: int,
group: dist.ProcessGroup,
all_reduce: bool,
@@ -40,7 +42,21 @@ def _reference(
dtype=x.dtype,
device=x.device,
)
torch.mm(x, weight.T, out=partial[:M])
if weight_scale is None:
torch.mm(x, weight.T, out=partial[:M])
else:
from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
mxfp8_e4m3_quantize,
)
x_q, x_sf = mxfp8_e4m3_quantize(x, is_sf_swizzled_layout=True)
partial[:M] = torch._scaled_mm(
x_q,
weight.T,
x_sf.view(torch.float8_e8m0fnu),
weight_scale.view(torch.float8_e8m0fnu),
out_dtype=x.dtype,
)
if padded_M > M:
partial[M:].zero_()
@@ -75,6 +91,10 @@ def _assert_valid_rows_close(
rtol=5e-2,
atol=4.0,
)
# Sequence-parallel callers may pass an unpadded M and read the padding
# rows back (they flow through per-token kernels before being dropped),
# so RS must leave them zero like ``sp_reduce_scatter`` does.
assert not actual[valid_rows:].any()
def _run_mode(
@@ -85,19 +105,52 @@ def _run_mode(
rank: int,
world_size: int,
weights: dict[int, torch.Tensor],
backend: str,
) -> None:
# cute_dsl is unavailable off CUDA, so import it only inside the GPU worker.
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import GemmRsAr
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
gemm_rs_ar = GemmRsAr(
max_M=max(M for M, _ in _SHAPES),
N=_N,
all_reduce=all_reduce,
)
from vllm.config.quantization import QuantizationConfigArgs
from vllm.model_executor.layers.linear import RowParallelLinear
from vllm.model_executor.layers.quantization.online.base import (
OnlineQuantizationConfig,
)
quant_config = (
None
if backend == "bf16"
else OnlineQuantizationConfig(QuantizationConfigArgs(linear="mxfp8"))
)
projections = {}
operands = {}
for K, w in weights.items():
with torch.device(device):
linear = RowParallelLinear(
K * world_size,
_N,
bias=False,
params_dtype=torch.bfloat16,
quant_config=quant_config,
)
# Select the fused path before online quantization replaces meta weights.
assert gemm_rs_ar.can_run(linear)
linear.weight = torch.nn.Parameter(w.clone(), requires_grad=False)
linear.quant_method.process_weights_after_loading(linear)
projections[K] = linear
weight = linear.weight
if backend == "flashinfer_cutedsl":
weight = weight.t()
operands[K] = (weight, getattr(linear, "weight_scale", None))
input_generator = torch.Generator(device=device)
# Alternating shapes exercise producer-flag reuse across different grids,
# CTA-group choices, and both BN=128 and BN=256 dispatches.
# Alternating shapes exercise producer-flag reuse across different grids
# and CTA-group choices.
for M, K in (*_SHAPES, *_SHAPES[::-1]):
input_generator.manual_seed(2000 + M + K)
x = torch.randn(
@@ -107,8 +160,8 @@ def _run_mode(
device=device,
generator=input_generator,
)
expected = _reference(x, weights[K], world_size, group, all_reduce)
actual = gemm_rs_ar(x, weights[K])
expected = _reference(x, *operands[K], world_size, group, all_reduce)
actual = gemm_rs_ar.apply(x, projections[K])
torch.accelerator.synchronize(device)
_assert_valid_rows_close(actual, expected, M, rank, all_reduce)
@@ -123,9 +176,9 @@ def _run_mode(
device=device,
generator=input_generator,
)
first_output = gemm_rs_ar(lifetime_x, weights[lifetime_K])
first_output = gemm_rs_ar.apply(lifetime_x, projections[lifetime_K])
first_snapshot = first_output.clone()
second_output = gemm_rs_ar(-lifetime_x, weights[lifetime_K])
second_output = gemm_rs_ar.apply(-lifetime_x, projections[lifetime_K])
torch.accelerator.synchronize(device)
assert first_output.data_ptr() != second_output.data_ptr()
torch.testing.assert_close(first_output, first_snapshot, rtol=0, atol=0)
@@ -141,7 +194,7 @@ def _run_mode(
)
graph_expected = _reference(
graph_x,
weights[graph_K],
*operands[graph_K],
world_size,
group,
all_reduce,
@@ -152,13 +205,13 @@ def _run_mode(
capture_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(capture_stream):
for _ in range(3):
graph_output.copy_(gemm_rs_ar(graph_x, weights[graph_K]))
graph_output.copy_(gemm_rs_ar.apply(graph_x, projections[graph_K]))
capture_stream.synchronize()
dist.barrier(group=group)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, stream=capture_stream):
graph_output.copy_(gemm_rs_ar(graph_x, weights[graph_K]))
graph_output.copy_(gemm_rs_ar.apply(graph_x, projections[graph_K]))
torch.cuda.current_stream().wait_stream(capture_stream)
dist.barrier(group=group)
@@ -177,7 +230,7 @@ def _run_mode(
del gemm_rs_ar, capture_stream, graph, graph_output
def _worker(local_rank: int, world_size: int, master_port: int) -> None:
def _worker(local_rank: int, world_size: int, master_port: int, backend: str) -> None:
# This module pulls in cute_dsl, which is unavailable off CUDA, so importing
# it at module level would fail collection. Import it where it is used, as
# `kda.py` does.
@@ -195,7 +248,10 @@ def _worker(local_rank: int, world_size: int, master_port: int) -> None:
)
init_distributed_environment()
with ensure_current_vllm_config():
config = VllmConfig()
config.model_config = ModelConfig(dtype="bfloat16")
config.kernel_config.linear_backend = "auto" if backend == "bf16" else backend
with set_current_vllm_config(config):
initialize_model_parallel(tensor_model_parallel_size=world_size)
group = dist.group.WORLD
@@ -215,24 +271,29 @@ def _worker(local_rank: int, world_size: int, master_port: int) -> None:
# Production binds one mode per worker; exercise both mode-bound instances
# sequentially in this test without implying that both are initialized.
for all_reduce in (False, True):
_run_mode(
all_reduce=all_reduce,
device=device,
group=group,
rank=rank,
world_size=world_size,
weights=weights,
)
with set_current_vllm_config(config):
for all_reduce in (False, True):
_run_mode(
all_reduce=all_reduce,
device=device,
group=group,
rank=rank,
world_size=world_size,
weights=weights,
backend=backend,
)
cleanup_dist_env_and_memory()
@pytest.mark.parametrize(
"backend", ["bf16", "flashinfer_cutlass", "flashinfer_cutedsl"]
)
@pytest.mark.distributed(num_gpus=2)
@pytest.mark.skipif(
not current_platform.is_device_capability_family(100),
reason="Kimi-K3 GEMM-RS/AR requires SM100",
reason="GEMM-RS/AR requires SM100",
)
def test_kimi_k3_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch) -> None:
def test_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch, backend: str) -> None:
world_size = 2
if torch.accelerator.device_count() < world_size:
pytest.skip("GEMM-RS/AR requires two GPUs")
@@ -242,7 +303,7 @@ def test_kimi_k3_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch) -> None:
try:
mp.spawn(
_worker,
args=(world_size, get_open_port()),
args=(world_size, get_open_port(), backend),
nprocs=world_size,
)
finally:
+71
View File
@@ -78,6 +78,10 @@ def make_sdesc_128B_swizzle(LBO: int):
return Uint64((LBO >> 4 << 16) | (SBO >> 4 << 32) | (1 << 46) | (2 << 61))
def make_mxfp8_idesc(MMA_M: int, MMA_N: int):
return Uint32((MMA_N >> 3 << 17) | (1 << 23) | (MMA_M >> 7 << 27))
@dsl_user_op
def mma_f16(
d_tmem,
@@ -130,6 +134,73 @@ def mma_ts_f16(
)
@dsl_user_op
def mma_mxfp8(
d_tmem,
a_desc,
b_desc,
idesc,
sfa_tmem,
sfb_tmem,
enable_input_d,
cta_group: int = 1,
*,
loc=None,
ip=None,
) -> None:
with cute.arch.elect_one():
nvvm.tcgen05_mma_block_scale(
nvvm.Tcgen05MMAKind.MXF8F6F4,
NVVM_CTA_GROUP_MAP[cta_group],
_make_tmem_llvm_ptr(d_tmem, loc=loc, ip=ip),
Uint64(a_desc).ir_value(loc=loc, ip=ip),
Uint64(b_desc).ir_value(loc=loc, ip=ip),
Int32(idesc).ir_value(loc=loc, ip=ip),
Boolean(enable_input_d).ir_value(loc=loc, ip=ip),
_make_tmem_llvm_ptr(sfa_tmem, loc=loc, ip=ip),
_make_tmem_llvm_ptr(sfb_tmem, loc=loc, ip=ip),
scale_vec_size=nvvm.Tcgen05MMAScaleVecSize.X1,
loc=loc,
ip=ip,
)
@dsl_user_op
def cp(
tmem,
sdesc,
shape: str,
mcast: str,
cta_group: int = 1,
*,
loc=None,
ip=None,
) -> None:
SHAPE_MAP = {
"128x256b": nvvm.Tcgen05CpShape.SHAPE_128x256b,
"4x256b": nvvm.Tcgen05CpShape.SHAPE_4x256b,
"128x128b": nvvm.Tcgen05CpShape.SHAPE_128x128b,
"64x128b": nvvm.Tcgen05CpShape.SHAPE_64x128b,
"32x128b": nvvm.Tcgen05CpShape.SHAPE_32x128b,
}
MCAST_MAP = {
"none": nvvm.Tcgen05CpMulticast.NONE,
"warpx2::02_13": nvvm.Tcgen05CpMulticast.WARPX2_02_13,
"warpx2::01_23": nvvm.Tcgen05CpMulticast.WARPX2_01_23,
"warpx4": nvvm.Tcgen05CpMulticast.WARPX4,
}
with cute.arch.elect_one():
nvvm.tcgen05_cp(
SHAPE_MAP[shape],
_make_tmem_llvm_ptr(tmem, loc=loc, ip=ip),
Uint64(sdesc).ir_value(loc=loc, ip=ip),
group=NVVM_CTA_GROUP_MAP[cta_group],
multicast=MCAST_MAP[mcast],
loc=loc,
ip=ip,
)
@dsl_user_op
def commit(mbar, cta_mask=None, cta_group: int = 1, *, loc=None, ip=None):
mbar_llvm = mbar.to_llvm_ptr(loc=loc, ip=ip)
+6 -4
View File
@@ -213,7 +213,7 @@ if TYPE_CHECKING:
VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT: bool = False
VLLM_KIMI_K3_AUX_ATTN_RES_STREAM: bool = False
VLLM_KIMI_K3_GEMM_AR: bool = True
VLLM_KIMI_K3_GEMM_RS: bool = False
VLLM_ENABLE_GEMM_RS: bool = False
VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER: bool = True
VLLM_USE_FLASHINFER_MOE_INT4: bool = False
VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR: str | None = None
@@ -1629,9 +1629,11 @@ environment_variables: dict[str, Callable[[], Any]] = {
# Use the SM100 BF16 GEMM-AR kernel for eligible Kimi-K3 row-parallel
# attention projections. All TP ranks must belong to one NVLink domain.
"VLLM_KIMI_K3_GEMM_AR": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_AR", "1"))),
# Use the SM100 BF16 GEMM-RS kernel for eligible Kimi-K3 sequence-parallel
# row-parallel projections. All TP ranks must belong to one NVLink domain.
"VLLM_KIMI_K3_GEMM_RS": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_RS", "0"))),
# Fuse eligible sequence-parallel row-parallel projections with their TP
# reduce-scatter using the SM100 BF16/MXFP8 GEMM-RS kernel (Kimi-K3
# attention/shared-expert projections, DeepSeek-V4.1 ``wo_b``). All TP
# ranks must belong to one NVLink domain.
"VLLM_ENABLE_GEMM_RS": lambda: bool(int(os.getenv("VLLM_ENABLE_GEMM_RS", "0"))),
# Allow use of FlashInfer FP8 block-scale GEMM for linear layers.
# This uses TensorRT-LLM kernels and requires SM90+ (Hopper).
"VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER": lambda: bool(
@@ -1,18 +1,36 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""SM100 BF16 GEMM with fused tensor-parallel reduce-scatter/all-reduce."""
"""SM100 BF16/MXFP8 GEMM with fused tensor-parallel reduce-scatter/all-reduce.
Model-neutral: any row-parallel projection whose output is reduced across TP
(Kimi-K3 ``o_proj``/``down_proj``, DeepSeek-V4.1 ``wo_b``) can bind to the
process-wide workspace through ``maybe_init_gemm_rs_ar`` + ``GemmRsAr.apply``.
"""
# Based on CUTLASS's Blackwell distributed GEMM-RS example at dcf215a.
# See https://github.com/NVIDIA/cutlass/issues/3117 for memory semantics.
# The MXFP8 path follows the block-scaled SM100 GEMM template from
# https://github.com/gau-nernst/gn-kernels (sm100_mm_mxfp8.py).
from functools import cache
from typing import TYPE_CHECKING
import cutlass
import torch
import torch.distributed._symmetric_memory as symm_mem
from cuda.bindings.driver import CUstream
from cutlass import BFloat16, Int32, Int64, Uint16, cute, utils
from cutlass import (
BFloat16,
Float8E4M3FN,
Float8E8M0FNU,
Int32,
Int64,
Uint16,
Uint64,
cute,
utils,
)
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm, nvvm, vector
from cutlass.cute.nvgpu import cpasync, tcgen05
@@ -24,6 +42,10 @@ from vllm.cute_utils import _tcgen05, mbarrier, simple_tma_copy, to_cta0_smem
from vllm.distributed import get_tp_group
from vllm.logger import init_logger
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
from vllm.platforms import current_platform
if TYPE_CHECKING:
from vllm.config import VllmConfig
logger = init_logger(__name__)
@@ -88,24 +110,31 @@ def multimem_st_16B(dst: cute.Tensor, value: cute.Tensor, *, loc=None, ip=None)
)
class Sm100GemmRsArBF16:
class Sm100GemmRsAr:
def __init__(
self,
rank: int,
num_ranks: int,
BN: int = 128,
cta_group: int = 1,
all_reduce: bool = False,
dtype: str = "bf16",
) -> None:
assert dtype in ("bf16", "mxfp8")
self.rank = rank
self.num_ranks = num_ranks
BM, BK = 128, 64
BM = 128
BN = 128
BK = {"bf16": 64, "mxfp8": 128}[dtype] # 128B
self.cta_tile = (BM, BN, BK)
self.cta_group = cta_group
self.all_reduce = all_reduce
self.dtype = dtype
smem_bytes = get_smem_capacity_in_bytes()
self.stage_size = (BM + (BN // cta_group)) * BK * 2
if dtype == "bf16":
self.stage_size = (BM + (BN // cta_group)) * BK * 2
elif dtype == "mxfp8":
self.stage_size = (BM + (BN // cta_group)) * BK + (BM + BN) * (BK // 32)
self.num_stages = smem_bytes // self.stage_size
@cute.jit
@@ -124,11 +153,36 @@ class Sm100GemmRsArBF16:
layout = cute.make_composed_layout(swizzle_128b, 0, layout)
return cpasync.make_tiled_tma_atom(tma_op, tensor, layout, (BM, BK))
@cute.jit
def prepare_sf_tma(
self,
SF: cute.Tensor,
R: Int32,
Ksf: Int32,
BR: cutlass.Constexpr,
BKsf: cutlass.Constexpr,
) -> cpasync.TmaInfo:
tma_group = (
tcgen05.CtaGroup.TWO if self.cta_group == 2 else tcgen05.CtaGroup.ONE
)
tma_op = cpasync.CopyBulkTensorTileG2SOp(cta_group=tma_group)
# NVIDIA SF gmem layout: an [R, Ksf] scale grid padded to R % 128 == 0
# and permuted as [R/128, Ksf/4, 32, 4, 4]. The trailing [32, 4, 4]
# atom is the opaque 512B block tcgen05 expects, so treat it as bytes.
# The Int64 recast keeps the TMA boxDim <= 256.
g_layout = cute.make_layout((Ksf * 128, cute.ceil_div(R, 128)))
SF_i64 = cute.recast_tensor(cute.make_tensor(SF.iterator, g_layout), Int64)
tiler = (128 * BKsf // 8, BR // 128)
layout = cute.make_layout((*tiler, self.num_stages))
return cpasync.make_tiled_tma_atom(tma_op, SF_i64, layout, tiler)
@cute.jit
def __call__(
self,
A: cute.Tensor,
B: cute.Tensor,
SFA: cute.Tensor,
SFB: cute.Tensor,
partial_uc: cute.Tensor,
partial_mc_ptr: cute.Pointer,
output: cute.Tensor,
@@ -138,10 +192,17 @@ class Sm100GemmRsArBF16:
grid_size: Int32,
stream: CUstream,
) -> None:
M, K = A.shape
N = B.shape[0]
BM, BN, BK = self.cta_tile
A_tma = self.prepare_tma(A, BM, BK)
B_tma = self.prepare_tma(B, BN // self.cta_group, BK)
if cutlass.const_expr(self.dtype == "mxfp8"):
SFA_tma = self.prepare_sf_tma(SFA, M, K // 32, BM, BK // 32)
SFB_tma = self.prepare_sf_tma(SFB, N, K // 32, BN, BK // 32)
else:
SFA_tma = None
SFB_tma = None
padded_M = partial_uc.shape[0]
partial_mc = cute.make_tensor(
partial_mc_ptr,
@@ -158,6 +219,8 @@ class Sm100GemmRsArBF16:
self.kernel(
A_tma,
B_tma,
SFA_tma,
SFB_tma,
partial_uc,
partial_mc,
output,
@@ -171,6 +234,8 @@ class Sm100GemmRsArBF16:
self,
A_tma: cpasync.TmaInfo,
B_tma: cpasync.TmaInfo,
SFA_tma: cpasync.TmaInfo,
SFB_tma: cpasync.TmaInfo,
partial_uc: cute.Tensor,
partial_mc: cute.Tensor,
output: cute.Tensor,
@@ -190,25 +255,33 @@ class Sm100GemmRsArBF16:
is_2cta = cta_group == 2
cta_rank = raw_bid % self.cta_group
num_tmem_stages = 512 // BN
# MXFP8 reserves the last 16 tmem columns (496-512) for the A/B scale
# factors.
num_tmem_stages = (496 if self.dtype == "mxfp8" else 512) // BN
smem = utils.SmemAllocator()
sA = smem.allocate_tensor(
BFloat16,
A_tma.atom.value_type,
A_tma.smem_layout.outer,
byte_alignment=128,
swizzle=A_tma.smem_layout.inner,
)
sB = smem.allocate_tensor(
BFloat16,
B_tma.atom.value_type,
B_tma.smem_layout.outer,
byte_alignment=128,
swizzle=B_tma.smem_layout.inner,
)
if cutlass.const_expr(self.dtype == "mxfp8"):
sSFA = smem.allocate_tensor(Int64, SFA_tma.smem_layout, byte_alignment=128)
sSFB = smem.allocate_tensor(Int64, SFB_tma.smem_layout, byte_alignment=128)
tma_full_mbar = smem.allocate_array(Int64, num_stages)
tma_empty_mbar = smem.allocate_array(Int64, num_stages)
tmem_full_mbar = smem.allocate_array(Int64, num_tmem_stages)
tmem_empty_mbar = smem.allocate_array(Int64, num_tmem_stages)
taddr = smem.allocate(Int32, 4)
# Named barriers
@@ -238,6 +311,9 @@ class Sm100GemmRsArBF16:
elif warp_id == 1:
cpasync.prefetch_descriptor(A_tma.atom)
cpasync.prefetch_descriptor(B_tma.atom)
if cutlass.const_expr(self.dtype == "mxfp8"):
cpasync.prefetch_descriptor(SFA_tma.atom)
cpasync.prefetch_descriptor(SFB_tma.atom)
if cutlass.const_expr(is_2cta):
cute.arch.cluster_arrive_relaxed()
@@ -261,12 +337,22 @@ class Sm100GemmRsArBF16:
# [(BM, BK), (M/BM, K/BK)]
gA_tiles = cute.zipped_divide(A_tma.tma_tensor, (BM, BK))
gB_tiles = cute.zipped_divide(B_tma.tma_tensor, (BN // cta_group, BK))
if cutlass.const_expr(self.dtype == "mxfp8"):
BKsf = BK // 32
gSFA_tiles = cute.zipped_divide(
SFA_tma.tma_tensor, (128 * BKsf // 8, BM // 128)
)
gSFB_tiles = cute.zipped_divide(
SFB_tma.tma_tensor, (128 * BKsf // 8, BN // 128)
)
for bid in range(raw_bid, total_tiles, num_bids):
bid_m = bid % grid_m
bid_n = bid // grid_m
if cutlass.const_expr(cta_group == 2):
bid_n = bid_n * cta_group + cta_rank
bid_n_b = bid_n * cta_group + cta_rank
else:
bid_n_b = bid_n
for iter_k in cutlass.range(cute.ceil_div(K, BK), unroll=1):
mbar = tma_full_mbar_ + tma_stage
@@ -282,10 +368,23 @@ class Sm100GemmRsArBF16:
)
simple_tma_copy(
B_tma.atom,
gB_tiles[None, (bid_n, iter_k)],
gB_tiles[None, (bid_n_b, iter_k)],
sB[None, None, tma_stage],
mbar,
)
if cutlass.const_expr(self.dtype == "mxfp8"):
simple_tma_copy(
SFA_tma.atom,
gSFA_tiles[None, (iter_k, bid_m)],
sSFA[None, None, tma_stage],
mbar,
)
simple_tma_copy(
SFB_tma.atom,
gSFB_tiles[None, (iter_k, bid_n)],
sSFB[None, None, tma_stage],
mbar,
)
tma_stage = (tma_stage + 1) % num_stages
if tma_stage == 0:
@@ -303,8 +402,15 @@ class Sm100GemmRsArBF16:
MMA_M = BM * cta_group
MMA_N = BN
idesc = _tcgen05.make_bf16_idesc(MMA_M, MMA_N)
sdesc = _tcgen05.make_sdesc_128B_swizzle(0)
if cutlass.const_expr(self.dtype == "mxfp8"):
# SF smem descriptor: no swizzle, SBO = 8 * 16 bytes.
sdesc_sf = Uint64((8 << 32) | (1 << 46))
sfa_tmem = 496
sfb_tmem = sfa_tmem + 4
idesc = _tcgen05.make_mxfp8_idesc(MMA_M, MMA_N)
else:
idesc = _tcgen05.make_bf16_idesc(MMA_M, MMA_N)
multicast_mask = Uint16((1 << self.cta_group) - 1)
for bid in range(raw_bid, total_tiles, num_bids):
@@ -319,17 +425,54 @@ class Sm100GemmRsArBF16:
b_addr = sB[None, None, tma_stage].iterator.toint()
a_desc = sdesc | (a_addr >> 4)
b_desc = sdesc | (b_addr >> 4)
if cutlass.const_expr(self.dtype == "mxfp8"):
sfa_desc = sdesc_sf | (
sSFA[None, None, tma_stage].iterator.toint() >> 4
)
sfb_desc = sdesc_sf | (
sSFB[None, None, tma_stage].iterator.toint() >> 4
)
cute.arch.mbarrier_wait(
tma_full_mbar + tma_stage, tma_full_parity
)
_tcgen05.fence_after_thread_sync()
for mma_k in cutlass.range_constexpr(BK // 16):
enable_d = iter_k > 0 or mma_k > 0
_tcgen05.mma_f16(
d_tmem, a_desc, b_desc, idesc, enable_d, cta_group
# load SF from smem->tmem
if cutlass.const_expr(self.dtype == "mxfp8"):
_tcgen05.cp(
sfa_tmem, sfa_desc, "32x128b", "warpx4", cta_group
)
for j in cutlass.range_constexpr(BN // 128):
_tcgen05.cp(
sfb_tmem + j * 4,
sfb_desc,
"32x128b",
"warpx4",
cta_group,
)
sfb_desc += (128 * 4) >> 4
# 4 = 128B / 32B
for mma_k in cutlass.range_constexpr(4):
enable_d = iter_k > 0 or mma_k > 0
if cutlass.const_expr(self.dtype == "mxfp8"):
# select SF IDs
idesc_k = idesc + ((mma_k << 4) | (mma_k << 29))
_tcgen05.mma_mxfp8(
d_tmem,
a_desc,
b_desc,
idesc_k,
sfa_tmem,
sfb_tmem,
enable_d,
cta_group,
)
else:
_tcgen05.mma_f16(
d_tmem, a_desc, b_desc, idesc, enable_d, cta_group
)
a_desc += 32 >> 4
b_desc += 32 >> 4
_tcgen05.commit(
@@ -638,9 +781,9 @@ class Sm100GemmRsArBF16:
def compile(
rank: int,
num_ranks: int,
BN: int,
cta_group: int,
all_reduce: bool = False,
dtype: str = "bf16",
):
M = cute.sym_int()
padded_M = cute.sym_int()
@@ -649,12 +792,37 @@ class Sm100GemmRsArBF16:
local_M = cute.sym_int()
num_flags = cute.sym_int()
ab_dtype = Float8E4M3FN if dtype == "mxfp8" else BFloat16
# Row stride must keep rows 16B-aligned: 8 BF16 or 16 FP8 elements.
row_div = 16 if dtype == "mxfp8" else 8
A = make_fake_tensor(
BFloat16, (M, K), (cute.sym_int64(divisibility=8), 1), assumed_align=16
ab_dtype,
(M, K),
(cute.sym_int64(divisibility=row_div), 1),
assumed_align=16,
)
B = make_fake_tensor(
BFloat16, (N, K), (cute.sym_int64(divisibility=8), 1), assumed_align=16
ab_dtype,
(N, K),
(cute.sym_int64(divisibility=row_div), 1),
assumed_align=16,
)
if dtype == "mxfp8":
SFA = make_fake_tensor(
Float8E8M0FNU,
(cute.sym_int(divisibility=512),),
(1,),
assumed_align=16,
)
SFB = make_fake_tensor(
Float8E8M0FNU,
(cute.sym_int(divisibility=512),),
(1,),
assumed_align=16,
)
else:
SFA = None
SFB = None
partial = make_fake_tensor(
BFloat16,
(padded_M, N),
@@ -677,13 +845,15 @@ class Sm100GemmRsArBF16:
peer_flag_ptr = nullptr(Int64, cute.AddressSpace.gmem, assumed_align=8)
stream = make_fake_stream(use_tvm_ffi_env_stream=True)
kernel = Sm100GemmRsArBF16(
rank, num_ranks, BN, cta_group, all_reduce=all_reduce
kernel = Sm100GemmRsAr(
rank, num_ranks, cta_group, all_reduce=all_reduce, dtype=dtype
)
return cute.compile(
kernel,
A,
B,
SFA,
SFB,
partial,
partial_mc_ptr,
output,
@@ -697,7 +867,7 @@ class Sm100GemmRsArBF16:
class GemmRsAr:
"""Own the symmetric workspace for Kimi-K3 GEMM-RS/AR launches.
"""Own the symmetric workspace for GEMM-RS/AR launches.
All TP ranks must belong to one NVLink domain for multimem instructions.
@@ -727,6 +897,7 @@ class GemmRsAr:
self.N = N
self.device = device
self.all_reduce = all_reduce
self.dtypes: set[str] = set()
self.partial = symm_mem.empty((max_M, N), dtype=torch.bfloat16, device=device)
self.partial_handle = symm_mem.rendezvous(self.partial, group)
@@ -766,22 +937,41 @@ class GemmRsAr:
tp_group.barrier()
def can_run(self, linear: LinearBase) -> bool:
# Validate projection-invariant requirements once during model init.
# only supports BF16 for now
if not isinstance(linear.quant_method, UnquantizedLinearMethod):
return False
w = linear.weight
if w.ndim != 2:
return False
K = w.shape[1]
return (
w.shape == (self.N, K)
and K % 64 == 0
and w.dtype == torch.bfloat16
and w.device == self.device
and w.is_contiguous()
from vllm.model_executor.kernels.linear.mxfp8.flashinfer import (
FlashInferCutedslMxfp8LinearKernel,
FlashInferCutlassMxfp8LinearKernel,
)
# Called before weight loading; online quantization starts on meta.
if isinstance(linear.quant_method, UnquantizedLinearMethod):
dtype, k_alignment = "bf16", 64
if linear.weight.dtype != torch.bfloat16:
return False
else:
method = getattr(linear, "scheme", linear.quant_method)
kernel = getattr(method, "kernel", None)
if not isinstance(
kernel,
(
FlashInferCutedslMxfp8LinearKernel,
FlashInferCutlassMxfp8LinearKernel,
),
):
return False
dtype, k_alignment = "mxfp8", 128
w = linear.weight
if (
w.ndim != 2
or w.shape[0] != self.N
or w.shape[1] % k_alignment != 0
or (w.device != self.device and not w.is_meta)
or not w.is_contiguous()
):
return False
self.dtypes.add(dtype)
return True
def warn_incompatible_projection(self) -> None:
logger.warning_once(
"Some projections are incompatible with GEMM-RS/AR; using the "
@@ -794,18 +984,51 @@ class GemmRsAr:
# supported but faster on the existing LL path.
return x.shape[0] >= 128
def __call__(self, x: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
def apply(self, x: torch.Tensor, linear: LinearBase) -> torch.Tensor:
from vllm.model_executor.kernels.linear.mxfp8.flashinfer import (
FlashInferCutedslMxfp8LinearKernel,
)
method = getattr(linear, "scheme", linear.quant_method)
w = linear.weight
if isinstance(
getattr(method, "kernel", None), FlashInferCutedslMxfp8LinearKernel
):
# This backend stores a column-major [K, N] view after loading.
w = w.t()
w_sf = getattr(linear, "weight_scale", None)
assert x.ndim == 2
M, K = x.shape
assert 0 < M <= self.max_M
assert w.shape == (self.N, K) and K % 64 == 0
assert w.dtype == torch.bfloat16
assert w.device == self.device
assert w.is_contiguous()
assert x.dtype == torch.bfloat16
assert x.device == self.device
assert x.is_contiguous()
N = w.shape[0]
assert w.device == self.device
N = self.N
dtype = "mxfp8" if w.dtype == torch.float8_e4m3fn else "bf16"
x_sf = None
if dtype == "mxfp8":
from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
mxfp8_e4m3_quantize,
)
assert w_sf is not None
assert w_sf.dtype == torch.uint8 and w_sf.device == self.device
assert w_sf.ndim == 1 and w_sf.is_contiguous()
assert w_sf.numel() == N * (K // 32)
assert w.shape == (N, K) and K % 128 == 0
assert w.is_contiguous()
x, x_sf = mxfp8_e4m3_quantize(x, is_sf_swizzled_layout=True)
# The kernel declares the scales as E8M0; the stored/quantized
# tensors carry the same bits as uint8.
x_sf = x_sf.view(torch.float8_e8m0fnu)
w_sf = w_sf.view(torch.float8_e8m0fnu)
else:
assert w_sf is None
assert w.shape == (N, K) and K % 64 == 0
assert w.dtype == torch.bfloat16
assert w.is_contiguous()
padded_M = (M + self.world_size - 1) // self.world_size
padded_M *= self.world_size
local_M = padded_M // self.world_size
@@ -814,10 +1037,8 @@ class GemmRsAr:
# Avoid padding small odd grids; 2-CTA wins consistently for M >= 1024.
cta_group = 2 if M >= 1024 or grid_m % 2 == 0 else 1
grid_m = (grid_m + cta_group - 1) // cta_group * cta_group
BN = 256 if M * K >= 24 * 1024 * 1024 else 128
assert N % BN == 0
num_tiles = grid_m * (N // BN)
num_tiles = grid_m * (N // 128)
num_ctas = min(num_tiles, self.num_sms)
num_ctas = num_ctas // cta_group * cta_group
assert self.flags.numel() >= num_tiles + num_ctas
@@ -825,16 +1046,26 @@ class GemmRsAr:
output = None
if not self.all_reduce:
output = torch.empty((local_M, N), dtype=torch.bfloat16, device=self.device)
compiled = Sm100GemmRsArBF16.compile(
# The kernel never touches rows at or beyond M. Callers that hand
# in an unpadded M (DeepSeek-V4.1 slices the SP all-gather back to
# the real token count) expect the same zero padding rows that
# ``sp_reduce_scatter`` produces, so clear them here. This is a
# no-op whenever M is already a multiple of the TP size.
valid_rows = min(max(M - self.rank * local_M, 0), local_M)
if valid_rows < local_M:
output[valid_rows:].zero_()
compiled = Sm100GemmRsAr.compile(
self.rank,
self.world_size,
BN,
cta_group,
self.all_reduce,
dtype=dtype,
)
compiled(
x,
w,
x_sf,
w_sf,
self.partial[:padded_M],
self.partial_mc_ptr,
output,
@@ -871,22 +1102,74 @@ def init_gemm_rs_ar(max_M: int, N: int, *, all_reduce: bool = False) -> None:
_gemm_rs_ar = GemmRsAr(max_M=max_M, N=N, all_reduce=all_reduce)
def maybe_init_gemm_rs_ar(
vllm_config: "VllmConfig", *, N: int, all_reduce: bool
) -> bool:
"""Collectively initialize the mode-bound GEMM-RS/AR state if supported.
Callers gate on their own feature flag first; this checks the worker
topology (SM100-family CUDA, BF16 model dtype, no ubatching, a TP size in
2-16 that divides 128) and the NVLink multicast rendezvous. Returns whether
projections may bind to the kernel through ``get_gemm_rs_ar().can_run``.
"""
mode = "GEMM-AR" if all_reduce else "GEMM-RS"
parallel_config = vllm_config.parallel_config
tp_size = parallel_config.tensor_parallel_size
if parallel_config.use_ubatching:
reason = "ubatching is enabled"
elif vllm_config.model_config.dtype != torch.bfloat16:
reason = "the model dtype is not BF16"
elif not current_platform.is_cuda():
reason = "the device is not CUDA"
elif not current_platform.is_device_capability_family(100):
reason = "the device is not SM100-family"
elif not 1 < tp_size <= 16:
reason = "TP size is not in the supported range 2-16"
elif 128 % tp_size != 0:
reason = "TP size does not divide 128"
elif N % 128 != 0:
reason = f"the output width {N} is not a multiple of 128"
else:
reason = None
if reason is not None:
logger.warning_once("%s is disabled because %s.", mode, reason)
return False
try:
init_gemm_rs_ar(
max_M=vllm_config.scheduler_config.max_num_batched_tokens,
N=N,
all_reduce=all_reduce,
)
except RuntimeError as e:
logger.warning_once(
"%s is disabled because initialization failed: %s. This may mean "
"the TP ranks do not share one NVLink domain.",
mode,
e,
)
return False
logger.info_once("%s is enabled.", mode)
return True
def warmup_gemm_rs_ar() -> int:
"""Compile every reachable dispatch for the initialized GEMM-RS/AR mode."""
# Initialization can be disabled or fail when multicast is unavailable.
if _gemm_rs_ar is None:
return 0
# Keep these profiles in sync with the dispatch in GemmRsAr.__call__.
profiles = ((128, 1), (128, 2), (256, 2))
for BN, cta_group in profiles:
Sm100GemmRsArBF16.compile(
_gemm_rs_ar.rank,
_gemm_rs_ar.world_size,
BN,
cta_group,
_gemm_rs_ar.all_reduce,
)
return len(profiles)
for dtype in sorted(_gemm_rs_ar.dtypes):
for cta_group in (1, 2):
Sm100GemmRsAr.compile(
_gemm_rs_ar.rank,
_gemm_rs_ar.world_size,
cta_group,
_gemm_rs_ar.all_reduce,
dtype=dtype,
)
return 2 * len(_gemm_rs_ar.dtypes)
def get_gemm_rs_ar() -> GemmRsAr:
+8 -7
View File
@@ -136,16 +136,17 @@ def _warmup_bf16x3_router_gemm(
logger.info_once("Warmed up BF16x3 router GEMM configs: %s.", configs)
def _warmup_kimi_k3_gemm_rs_ar() -> None:
# Kimi-K3 model construction imports this module only when GEMM-RS/AR is
# enabled and initializes its singleton before kernel_warmup runs. Avoid
# importing it here so other models do not compile the RS/AR variants.
module = sys.modules.get("vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar")
def _warmup_gemm_rs_ar() -> None:
# Model construction (Kimi-K3, DeepSeek-V4.1) imports this module only
# when GEMM-RS/AR is enabled and initializes its singleton before
# kernel_warmup runs. Avoid importing it here so other models do not
# compile the RS/AR variants.
module = sys.modules.get("vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar")
if module is None:
return
compiled = module.warmup_gemm_rs_ar()
if compiled:
logger.info_once("Warmed up %d Kimi-K3 GEMM-RS/AR variants.", compiled)
logger.info_once("Warmed up %d GEMM-RS/AR variants.", compiled)
def _autotune_kimi_k3_kda_qkvg(model: torch.nn.Module) -> None:
@@ -208,7 +209,7 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
if current_platform.has_device_capability(90):
_warmup_ll_bf16_router_gemm(worker.get_model())
_warmup_kimi_k3_gemm_rs_ar()
_warmup_gemm_rs_ar()
if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
# TODO(roberto): Remove after registered CuTeDSL warmups are migrated
+6 -2
View File
@@ -1,5 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Callable
import torch
import torch.nn as nn
@@ -31,7 +33,7 @@ def deep_gemm_fp8_o_proj(
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
wo_a: nn.Module,
wo_b: nn.Module,
wo_b: Callable[[torch.Tensor], torch.Tensor],
*,
n_groups: int,
heads_per_group: int,
@@ -44,7 +46,9 @@ def deep_gemm_fp8_o_proj(
"""O projection: inverse RoPE + grouped wo_a + wo_b.
Shared by the FlashMLA and FlashInfer CUDA backends. The attention
layer selects the recipe at initialization.
layer selects the recipe at initialization. ``wo_b`` is any callable over
the flattened ``z``: the projection module itself, or a wrapper that also
reduce-scatters its output (DeepSeek-V4.1 GEMM-RS).
"""
use_fp8 = wo_a.weight.dtype == torch.float8_e4m3fn
o_proj_input, o_scale = fused_inv_rope_fp8_quant(
+40
View File
@@ -26,6 +26,7 @@ from vllm.model_executor.layers.linear import (
from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
from vllm.model_executor.layers.sparse_mqa_indexer import SparseMQAIndexer
from vllm.models.common.ops import fused_q_kv_rmsnorm
from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter
from vllm.models.deepseek_v41.common.ops import (
MXFP4_BLOCK_SIZE,
fused_indexer_q_rope_quant,
@@ -33,6 +34,7 @@ from vllm.models.deepseek_v41.common.ops import (
)
if TYPE_CHECKING:
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
from vllm.v1.attention.backends.mla.sparse_swa import (
DeepseekSparseSWAMetadata,
)
@@ -389,6 +391,9 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
return_bias=False,
prefix=f"{prefix}.wo_b",
)
# Set by ``bind_gemm_rs`` when the decoder layer runs sequence
# parallel and the fused GEMM + reduce-scatter kernel accepts wo_b.
self.gemm_rs: GemmRsAr | None = None
# Initialize rotary embedding before the indexer/compressor consume it.
self.rotary_emb = build_deepseek_v4_rope(
@@ -672,6 +677,41 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
)
return self._o_proj(attn_out, positions)
def bind_gemm_rs(self) -> None:
"""Fuse ``wo_b`` with the sequence-parallel TP reduce-scatter.
The decoder layer calls this after the model initialized the
process-wide GEMM-RS workspace and before weights load: the
eligibility check inspects the projection's linear kernel and weight
shape, which online quantization may later re-layout.
"""
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
get_gemm_rs_ar,
)
# The layer owns the reduction under sequence parallel.
assert not self.wo_b.reduce_results
gemm_rs = get_gemm_rs_ar()
if gemm_rs.can_run(self.wo_b):
self.gemm_rs = gemm_rs
else:
gemm_rs.warn_incompatible_projection()
def _wo_b_proj(self, z: torch.Tensor) -> torch.Tensor:
"""Apply ``wo_b``; with GEMM-RS bound, also reduce-scatter the result.
Every ``_o_proj`` implementation projects through this so the decoder
layer sees one contract: when ``gemm_rs`` is bound the output is
already the local sequence-parallel shard, otherwise it is the
unreduced TP partial.
"""
if self.gemm_rs is None:
return self.wo_b(z)
if self.gemm_rs.should_run(z):
return self.gemm_rs.apply(z, self.wo_b)
# Small batches stay on the unfused path, which is faster there.
return sp_reduce_scatter(self.wo_b(z))
def _alloc_attn_out(
self, num_tokens: int, hidden_states: torch.Tensor
) -> "torch.Tensor | QuantizedActivation":
@@ -58,6 +58,7 @@ from .model import (
_linear_scale_param_name,
_use_sequence_parallel,
make_deepseek_v4_expert_params_mapping,
maybe_init_gemm_rs,
prepare_mega_gate_routing_metadata,
)
@@ -113,12 +114,18 @@ class DSparkDeepseekV4Model(nn.Module):
)
current_vllm_config = get_current_vllm_config()
# The target model already holds the GEMM-RS workspace (same hidden
# size, same TP group); this only re-checks and binds the draft layers.
run_gemm_rs = maybe_init_gemm_rs(
current_vllm_config, self.use_sequence_parallel
)
self.layers = nn.ModuleList(
[
DeepseekV4DecoderLayer(
current_vllm_config,
prefix=maybe_prefix(prefix, f"layers.{self.num_hidden_layers + i}"),
topk_indices_buffer=self.topk_indices_buffer,
run_gemm_rs=run_gemm_rs,
)
for i in range(self.num_dspark_layers)
]
@@ -225,7 +225,7 @@ class DeepseekV4MegaAttnAttention(DeepseekV4FlashMLAAttention):
z,
recipe=self._einsum_recipe,
)
return self.wo_b(z.flatten(1))
return self._wo_b_proj(z.flatten(1))
# ---- weights -----------------------------------------------------------
@@ -248,7 +248,7 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
positions,
self.rotary_emb.cos_sin_cache,
self.wo_a,
self.wo_b,
self._wo_b_proj,
n_groups=self.n_local_groups,
heads_per_group=self.n_local_heads // self.n_local_groups,
nope_dim=self.nope_head_dim,
@@ -627,7 +627,7 @@ class DeepseekV4FlashInferSM120Attention(DeepseekV4Attention):
positions,
self.rotary_emb.cos_sin_cache,
self.wo_a,
self.wo_b,
self._wo_b_proj,
n_groups=self.n_local_groups,
heads_per_group=self.n_local_heads // self.n_local_groups,
nope_dim=self.nope_head_dim,
+1 -1
View File
@@ -65,7 +65,7 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
positions,
self.rotary_emb.cos_sin_cache,
self.wo_a,
self.wo_b,
self._wo_b_proj,
n_groups=self.n_local_groups,
heads_per_group=self.n_local_heads // self.n_local_groups,
nope_dim=self.nope_head_dim,
+37 -1
View File
@@ -193,6 +193,30 @@ def _use_sequence_parallel(vllm_config: VllmConfig) -> bool:
)
def maybe_init_gemm_rs(vllm_config: VllmConfig, use_sequence_parallel: bool) -> bool:
"""Set up the fused ``wo_b`` GEMM + reduce-scatter when opted in.
Sequence parallel is the only topology where ``wo_b`` ends in a
reduce-scatter, so the kernel is bound to RS mode. The workspace is a
process-wide singleton shared with any other model code in this worker
(the DSpark drafter reuses it), and its NVLink multicast rendezvous is
collective, so every TP rank must take the same decision here.
"""
if not (use_sequence_parallel and envs.VLLM_ENABLE_GEMM_RS):
return False
# The kernel module pulls in cute_dsl, so import it only once opted in.
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
maybe_init_gemm_rs_ar,
)
hidden_size = vllm_config.model_config.hf_config.hidden_size
if not maybe_init_gemm_rs_ar(vllm_config, N=hidden_size, all_reduce=False):
return False
logger.info_once("To disable DeepSeek-V4.1 GEMM-RS, set VLLM_ENABLE_GEMM_RS=0.")
return True
class DeepseekV4DecoderLayer(nn.Module):
def __init__(
self,
@@ -202,6 +226,7 @@ class DeepseekV4DecoderLayer(nn.Module):
aux_stream_list: list[torch.cuda.Stream] | None = None,
candidate_block_buffer: torch.Tensor | None = None,
engram_layout: EngramLayout | None = None,
run_gemm_rs: bool = False,
mhc_stream: torch.cuda.Stream | None = None,
fuse_mhc_all_reduce: bool = False,
):
@@ -236,6 +261,10 @@ class DeepseekV4DecoderLayer(nn.Module):
)
if self.use_sequence_parallel or fuse_mhc_all_reduce:
self.attn.wo_b.reduce_results = False
if run_gemm_rs:
# Binds only when wo_b's kernel and shape qualify; otherwise
# forward keeps the separate reduce-scatter below.
self.attn.bind_gemm_rs()
self.ffn = DeepseekV4MoE(
vllm_config,
prefix=f"{prefix}.ffn",
@@ -492,7 +521,9 @@ class DeepseekV4DecoderLayer(nn.Module):
x = sp_all_gather(x)[: positions.shape[0]]
x = self.attn(positions, x, None)
if self.use_sequence_parallel:
# With GEMM-RS bound, the attention output is already this rank's
# sequence-parallel shard (see DeepseekV4Attention._wo_b_proj).
if self.use_sequence_parallel and self.attn.gemm_rs is None:
x = sp_reduce_scatter(x)
if mhc_stream is not None:
@@ -597,6 +628,10 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
):
engram_config.dp_shared_memory = can_share_engram_tables(self.engram_layout)
# GEMM-RS uses NCCL symmetric-memory multicast, which requires all TP
# ranks to belong to one NVLink domain. Collective: run before layers.
self.run_gemm_rs = maybe_init_gemm_rs(vllm_config, self.use_sequence_parallel)
self.start_layer, self.end_layer, self.layers = make_layers(
config.num_hidden_layers,
lambda prefix: DeepseekV4DecoderLayer(
@@ -606,6 +641,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
aux_stream_list=aux_stream_list,
candidate_block_buffer=self.candidate_block_buffer,
engram_layout=self.engram_layout,
run_gemm_rs=self.run_gemm_rs,
mhc_stream=mhc_stream,
fuse_mhc_all_reduce=self.fuse_mhc_all_reduce,
),
+2 -2
View File
@@ -724,7 +724,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
)
self.gemm_rs_ar = None
if run_gemm_rs_ar:
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
get_gemm_rs_ar,
)
@@ -809,7 +809,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
)
core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(core_attn_out):
return self.gemm_rs_ar(core_attn_out, self.o_proj.weight)
return self.gemm_rs_ar.apply(core_attn_out, self.o_proj)
return self.o_proj(core_attn_out)[0]
@eager_break_during_capture
+2 -2
View File
@@ -301,7 +301,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
)
self.gemm_rs_ar = None
if run_gemm_rs_ar:
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
get_gemm_rs_ar,
)
@@ -631,7 +631,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
attn_out = _gate_sigmoid_mul(attn_out, gate)
if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(attn_out):
return self.gemm_rs_ar(attn_out, self.o_proj.weight)
return self.gemm_rs_ar.apply(attn_out, self.o_proj)
return self.o_proj(attn_out)[0]
+13 -44
View File
@@ -160,54 +160,23 @@ def maybe_init_gemm_rs_ar(vllm_config: VllmConfig, use_sequence_parallel: bool)
# its singleton to exactly one mode.
all_reduce = not use_sequence_parallel
mode = "GEMM-AR" if all_reduce else "GEMM-RS"
enabled = envs.VLLM_KIMI_K3_GEMM_AR if all_reduce else envs.VLLM_KIMI_K3_GEMM_RS
enabled = envs.VLLM_KIMI_K3_GEMM_AR if all_reduce else envs.VLLM_ENABLE_GEMM_RS
if not enabled:
return False
parallel_config = vllm_config.parallel_config
tp_size = parallel_config.tensor_parallel_size
if parallel_config.use_ubatching:
reason = "ubatching is enabled"
elif vllm_config.model_config.dtype != torch.bfloat16:
reason = "the model dtype is not BF16"
elif not current_platform.is_cuda():
reason = "the device is not CUDA"
elif not current_platform.is_device_capability_family(100):
reason = "the device is not SM100-family"
elif not 1 < tp_size <= 16:
reason = "TP size is not in the supported range 2-16"
elif 128 % tp_size != 0:
reason = "TP size does not divide 128"
else:
reason = None
if reason is not None:
logger.warning_once("%s is disabled because %s.", mode, reason)
return False
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import init_gemm_rs_ar
# The kernel module pulls in cute_dsl, so import it only once the flag
# asks for it.
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
maybe_init_gemm_rs_ar as maybe_init_shared_gemm_rs_ar,
)
config = vllm_config.model_config.hf_text_config
try:
init_gemm_rs_ar(
max_M=vllm_config.scheduler_config.max_num_batched_tokens,
N=config.hidden_size,
all_reduce=all_reduce,
)
except RuntimeError as e:
logger.warning_once(
"%s is disabled because initialization failed: %s. This may mean "
"the TP ranks do not share one NVLink domain.",
mode,
e,
)
if not maybe_init_shared_gemm_rs_ar(
vllm_config, N=config.hidden_size, all_reduce=all_reduce
):
return False
if all_reduce:
logger.info_once(
"GEMM-AR is enabled. To disable it, set VLLM_KIMI_K3_GEMM_AR=0."
)
else:
logger.info_once("GEMM-RS is enabled.")
flag = "VLLM_KIMI_K3_GEMM_AR" if all_reduce else "VLLM_ENABLE_GEMM_RS"
logger.info_once("To disable %s, set %s=0.", mode, flag)
return True
@@ -276,7 +245,7 @@ class KimiMLP(nn.Module):
not use_sequence_parallel and reduce_results
)
if use_gemm_rs_ar and run_gemm_rs_ar:
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
get_gemm_rs_ar,
)
@@ -309,7 +278,7 @@ class KimiMLP(nn.Module):
x = self.act_fn(gate_up)
if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(x):
return self.gemm_rs_ar(x, self.down_proj.weight)
return self.gemm_rs_ar.apply(x, self.down_proj)
x, _ = self.down_proj(x)
if self.shard_sequence_parallel: