[Perf] Kimi K3 Specialized Decode: CuTeDSL Optimized GB300 (1.5-2x) (#61867)

Signed-off-by: Benjamin Chislett <bchislett@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vllm-agent <claw@inferact.ai>
This commit is contained in:
Benjamin Chislett
2026-10-15 22:50:41 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 vllm-agent
parent 3ddc6702a8
commit 642c18d64c
50 changed files with 12805 additions and 3 deletions
+48 -3
View File
@@ -4,7 +4,7 @@
import math
from collections.abc import Hashable, Iterable
from typing import Any, cast
from typing import TYPE_CHECKING, Any, cast
import torch
from torch import nn
@@ -121,6 +121,12 @@ from ..common.mm_preprocess import (
KimiK3MultiModalProcessor,
KimiK3ProcessingInfo,
)
from .specialized_decode.runtime import specialized_decode_config
if TYPE_CHECKING:
from vllm.v1.worker.gpu.model_states.interface import ModelState
from .specialized_decode.runtime.decode import SpecializedDecode
logger = init_logger(__name__)
@@ -1421,8 +1427,34 @@ class KimiLinearForCausalLM(
self.logits_processor = LogitsProcessor(
self.config.vocab_size, scale=logit_scale
)
# decode_backend=latency_specialized: a specialized decode executor over
# self.model, built by the model state once the weights are loaded.
self.specialized_decode_config = specialized_decode_config(vllm_config)
self.specialized_decode: SpecializedDecode | None = None
# The final norm is applied in compute_logits unless the specialized decode
# applies it in forward.
self.needs_norm_in_logits = True
def build_specialized_decode(self) -> None:
from .specialized_decode.runtime.decode import SpecializedDecode
self.specialized_decode = SpecializedDecode(
self.model, self.specialized_decode_config
)
# From here on forward() returns normalized hidden states on every step: the
# specialized decode's last AttnRes applies the norm, and forward() applies it
# to each step that falls back to the stock forward (prefills, larger batches,
# PIECEWISE steps).
self.needs_norm_in_logits = False
def get_model_state_cls(self) -> "type[ModelState]":
from .model_state import KimiK3ModelState
return KimiK3ModelState
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
if self.specialized_decode is not None:
return self.specialized_decode.embed_input_ids(input_ids)
return self.model.embed_input_ids(input_ids)
def make_empty_intermediate_tensors(
@@ -1441,9 +1473,18 @@ class KimiLinearForCausalLM(
inputs_embeds: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor | IntermediateTensors | tuple[torch.Tensor, list[torch.Tensor]]:
return self.model(
decode = self.specialized_decode
if decode is not None:
step = decode.plan(positions, intermediate_tensors)
if step is not None:
return decode.run(step, input_ids, inputs_embeds)
hidden_states = self.model(
input_ids, positions, intermediate_tensors, inputs_embeds, **kwargs
)
if not self.needs_norm_in_logits:
assert decode is not None
hidden_states = decode.normalize(hidden_states)
return hidden_states
@classmethod
def get_mamba_state_dtype_from_config(
@@ -1502,7 +1543,8 @@ class KimiLinearForCausalLM(
) -> torch.Tensor | None:
# The model's final norm is applied here (not at the end of forward) so
# that the pre-norm hidden states can be fed to the MTP draft model.
hidden_states = self.model.norm(hidden_states, None)
if self.needs_norm_in_logits:
hidden_states = self.model.norm(hidden_states, None)
return self.logits_processor(self.lm_head, hidden_states)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
@@ -1926,6 +1968,9 @@ class KimiK3ForConditionalGeneration(
def compute_logits(self, hidden_states: torch.Tensor, **kwargs) -> torch.Tensor:
return self.language_model.compute_logits(hidden_states)
def get_model_state_cls(self) -> "type[ModelState]":
return self.language_model.get_model_state_cls()
def copy_inputs_before_cuda_graphs(self, input_buffers, **kwargs):
return self.language_model.mamba_cache.copy_inputs_before_cuda_graphs(
input_buffers, **kwargs
+26
View File
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Kimi K3's model runner state: the hybrid (KDA + MLA) state, plus the lifecycle of
the specialized decode when ``decode_backend=latency_specialized`` selects it."""
from vllm.v1.worker.gpu.model_states.mamba_hybrid import MambaHybridModelState
class KimiK3ModelState(MambaHybridModelState):
"""Builds the specialized decode once the weights are loaded and the aux layers
set, and prepares it for every bound KV cache (the profiling cache, then the
real one)."""
def __init__(self, vllm_config, model, encoder_cache, device):
super().__init__(vllm_config, model, encoder_cache, device)
if hasattr(model, "get_language_model"):
model = model.get_language_model()
self.language_model = model
if model.specialized_decode_config is not None:
model.build_specialized_decode()
def initialize_kv_cache(self, kv_cache_config, block_tables):
super().initialize_kv_cache(kv_cache_config, block_tables)
decode = self.language_model.specialized_decode
if decode is not None:
decode.prepare_kv()
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,276 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Seven-CTA cluster AttnRes: one cluster per token, 1024 hidden columns per CTA.
The delta is a tensor (read after the PDL wait) or ``slots`` Lamport mailbox slots
summed in slot order, rounded to BF16 and re-armed. The final boundary rounds the
mixture to BF16 before its RMSNorm. Prefix and saved blocks must be complete at launch.
"""
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, Uint32, Uint64
from ..common.mailbox import poll, rearm
from ..common.primitives import (
alloc,
at,
bf16,
bf16x2,
dot,
fma,
group_barrier,
launch,
load_words,
push_float,
scatter_reduce,
store_words,
wait,
zeros,
)
@cute.jit
def round_pair(t, row: cutlass.Constexpr, j: cutlass.Constexpr):
word = bf16x2(t[row, 2 * j], t[row, 2 * j + 1])
t[row, 2 * j], t[row, 2 * j + 1] = bf16(word, 0), bf16(word, 1)
@cute.jit
def unpack(words, out, row: cutlass.Constexpr):
for i in cutlass.range_constexpr(8):
out[row, i] = bf16(words[i // 2], i % 2)
def block_ptr(blocks, token, block, col):
"""``&blocks[token, block, col]`` from the tensor's strides."""
return blocks.iterator + token * blocks.stride[0] + block * blocks.stride[1] + col
@cute.jit
def rstd(sum_sq, eps):
"""Reciprocal RMS of a 7168-wide row from its sum of squares."""
return cute.math.rsqrt(fma(sum_sq, Float32(1 / 7168), eps), fastmath=True)
@cute.jit
def dot8(a, ar: cutlass.Constexpr, b, br: cutlass.Constexpr):
return dot((a[ar, i] for i in range(8)), (b[br, i] for i in range(8)), Float32(0))
@cute.jit
def arm_exchanges(bars, counts):
"""Thread 0 arms barrier ``r`` for ``counts[r]`` FP32 pushes from each of 7 CTAs."""
if cute.arch.thread_idx()[0] == 0:
for r, count in enumerate(counts):
if cutlass.const_expr(count > 0):
cute.arch.mbarrier_init(bars.iterator + r, 1)
cute.arch.mbarrier_arrive_and_expect_tx(bars.iterator + r, 28 * count)
cute.arch.mbarrier_init_fence()
class ClusterAttnRes:
def __init__(self, nb, *, slots=1, has_delta=True, write=-1, final=False):
self.nb, self.slots, self.has_delta = nb, slots, has_delta
self.write, self.final = write, final
self.npre = 2 * nb
self.npost = (2 if final else 4) if nb else 1
self.kpre = 1 << max(self.npre - 1, 0).bit_length()
self.kpost = 1 << max(self.npost - 1, 0).bit_length()
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, (7, args[1].shape[0], 1), 128, 7)
@cute.jit
def exchange(
self, values, count: cutlass.Constexpr, kpad: cutlass.Constexpr, bufs, r, ids
):
"""Cluster-sum ``count`` per-thread partials into round ``r``'s total row."""
wb, peer_buf, total, bars = bufs
peer_buf, total, bar = peer_buf[r, None], total[r, None], bars.iterator + r
rank, _, tid = ids
lane, warp = tid % 32, tid // 32
reduced, index = scatter_reduce(values, lane, kpad)
if (lane & (32 // kpad - 1)) == 0 and index < count:
wb[warp * 16 + index] = reduced
group_barrier(0)
if tid < count:
value = (wb[tid] + wb[16 + tid]) + (wb[32 + tid] + wb[48 + tid])
for peer in cutlass.range_constexpr(7):
push_float(peer_buf.iterator + rank * 16 + tid, value, bar, Int32(peer))
wait(bar)
if tid < count:
sums = [Float32(0), Float32(0)]
for peer in cutlass.range_constexpr(7):
sums[peer % 2] += peer_buf[peer * 16 + tid]
total[tid] = sums[0] + sums[1]
group_barrier(0)
@cute.kernel
def kernel(self, args):
delta, prefix, blocks, norm, query, out_norm, output, eps, out_eps = args[0:9]
rank, token, _ = cute.arch.block_idx()
tid, _, _ = cute.arch.thread_idx()
bufs = self.setup(cutlass.utils.SmemAllocator())
cute.arch.cluster_arrive_relaxed()
cute.arch.griddepcontrol_launch_dependents()
state = self.state()
rows, ids = (prefix, blocks, norm, query, out_norm), (rank, token, tid)
self.pre(state, bufs, rows, eps, ids)
self.post(state, bufs, delta, rows, output, eps, out_eps, ids, args[9:])
cute.arch.griddepcontrol_wait()
def state(self):
"""Carried from pre() to post(); the rest reloads so a GEMM can run between."""
return cute.make_rmem_tensor((1, 8), Float32), cute.make_rmem_tensor(2, Float32)
@cute.jit
def setup(self, smem):
warp_buf = alloc(smem, Float32, 64)
peer_buf = alloc(smem, Float32, (3, 112))
total = alloc(smem, Float32, (3, 16))
bars = alloc(smem, Uint64, 3, 8)
arm_exchanges(bars, (self.npre, self.npost, int(self.final)))
return warp_buf, peer_buf, total, bars
@cute.jit
def pre(self, state, bufs, rows, eps: Float32, ids):
"""Inputs complete at launch, then the saved-block statistics exchange."""
prefix, blocks, norm, query, out_norm = rows
rank, token, tid = ids
warp_buf, peer_buf, total, bars = bufs
mixture, scal = state
# Rows: saved blocks, combined weight, gamma, old prefix, new prefix.
v = cute.make_rmem_tensor((self.nb + 4, 8), Float32)
col = (rank * 128 + tid) * 8
W = self.nb
for source in cutlass.range_constexpr(self.nb):
words = load_words(
block_ptr(blocks, token, source, col), count=4, mutable=True
)
unpack(words, v, source)
self.weights(v, norm, query, col)
cute.arch.cluster_wait()
maximum, exp_sum = Float32(0), Float32(0)
mixture.fill(0)
if cutlass.const_expr(self.nb > 0):
values = zeros(self.kpre, Float32)
for source in cutlass.range_constexpr(self.nb):
values[source] = dot8(v, source, v, source)
values[self.nb + source] = dot8(v, source, v, W)
self.exchange(values, self.npre, self.kpre, bufs, 0, ids)
logits = cute.make_rmem_tensor(self.nb, Float32)
for b in cutlass.range_constexpr(self.nb):
logits[b] = total[0, self.nb + b] * rstd(total[0, b], eps)
maximum = logits[0]
for source in cutlass.range_constexpr(1, self.nb):
maximum = cute.arch.fmax(maximum, logits[source])
for source in cutlass.range_constexpr(self.nb):
e = cute.math.exp(logits[source] - maximum, fastmath=True)
exp_sum += e
for i in cutlass.range_constexpr(8):
mixture[0, i] = fma(e, v[source, i], mixture[0, i])
scal[0], scal[1] = maximum, exp_sum
@cute.jit
def weights(self, v, norm, query, col):
if cutlass.const_expr(self.nb > 0):
nw = load_words(norm.iterator + col, count=4)
qw = load_words(query.iterator + col, count=4)
for i in cutlass.range_constexpr(8):
v[self.nb, i] = bf16(nw[i // 2], i % 2) * bf16(qw[i // 2], i % 2)
@cute.jit
def post(
self, state, bufs, delta, rows, output, eps: Float32, out_eps: Float32, ids, aux
):
"""Add the delta, update prefix (and its ``aux`` copy) and saved block, write
the normalized output."""
prefix, blocks, norm, query, out_norm = rows
rank, token, tid = ids
warp_buf, peer_buf, total, bars = bufs
mixture, scal = state
maximum, exp_sum = scal[0], scal[1]
v = cute.make_rmem_tensor((self.nb + 4, 8), Float32)
col = (rank * 128 + tid) * 8
W, G, OLD, NEW = self.nb, self.nb + 1, self.nb + 2, self.nb + 3
self.weights(v, norm, query, col)
unpack(load_words(out_norm.iterator + col, count=4), v, G)
if cutlass.const_expr(self.has_delta):
unpack(load_words(at(prefix, token, col), count=4, mutable=True), v, OLD)
cur = cute.make_rmem_tensor((1, 8), Float32)
if cutlass.const_expr(self.slots == 0):
cute.arch.griddepcontrol_wait()
unpack(load_words(at(delta, token, col), count=4, mutable=True), cur, 0)
else:
cur.fill(0)
base = delta.iterator + token * 7168 + col
got = poll(base, delta.shape[1] * 7168, self.slots)
for slot in cutlass.range_constexpr(self.slots):
rearm(base + slot * delta.shape[1] * 7168)
part = cute.make_rmem_tensor((1, 8), Float32)
unpack(tuple(got[slot, k] for k in range(4)), part, 0)
for i in cutlass.range_constexpr(8):
cur[0, i] = cur[0, i] + part[0, i]
if cutlass.const_expr(self.slots > 1):
for j in cutlass.range_constexpr(4): # all-reduce output boundary
round_pair(cur, 0, j)
words = cute.make_rmem_tensor(4, Uint32)
for j in cutlass.range_constexpr(4):
lo, hi = cur[0, 2 * j], cur[0, 2 * j + 1]
if cutlass.const_expr(self.has_delta):
lo, hi = v[OLD, 2 * j] + lo, v[OLD, 2 * j + 1] + hi
words[j] = bf16x2(lo, hi)
unpack(words, v, NEW)
values = zeros(self.kpost, Float32)
values[0] = dot8(v, NEW, v, NEW)
if cutlass.const_expr(self.nb > 0):
values[1] = dot8(v, NEW, v, W)
if cutlass.const_expr(not self.final):
values[2] = dot8(mixture, 0, v, NEW)
values[3] = dot8(mixture, 0, mixture, 0)
self.exchange(values, self.npost, self.kpost, bufs, 1, ids)
row = (words[0], words[1], words[2], words[3])
store_words(at(prefix, token, col), row)
for stream_out in aux:
store_words(at(stream_out, token, col), row)
if cutlass.const_expr(self.write >= 0):
store_words(block_ptr(blocks, token, self.write, col), row)
result = cute.make_rmem_tensor((1, 8), Float32)
if cutlass.const_expr(self.nb == 0):
scale = rstd(total[1, 0], out_eps)
for i in cutlass.range_constexpr(8):
result[0, i] = v[NEW, i] * scale * v[G, i]
else:
ss, dot = total[1, 0], total[1, 1]
logit = dot * rstd(ss, eps)
top = cute.arch.fmax(maximum, logit)
alpha = cute.math.exp(maximum - top, fastmath=True)
ep = cute.math.exp(logit - top, fastmath=True)
denominator = fma(alpha, exp_sum, ep)
if cutlass.const_expr(self.final):
inverse = Float32(1) / denominator
for j in cutlass.range_constexpr(4):
for i in (2 * j, 2 * j + 1):
result[0, i] = fma(
alpha * inverse, mixture[0, i], ep * inverse * v[NEW, i]
)
round_pair(result, 0, j)
fin = cute.make_rmem_tensor(1, Float32)
fin[0] = dot8(result, 0, result, 0)
self.exchange(fin, 1, 1, bufs, 2, ids)
scale = rstd(total[2, 0], out_eps)
for i in cutlass.range_constexpr(8):
result[0, i] = result[0, i] * scale * v[G, i]
else:
a2, t3 = alpha * alpha, total[1, 3]
ae2 = Float32(2) * alpha * ep
quad = fma(a2, t3, fma(ae2, total[1, 2], ep * ep * ss))
scale = rstd(quad, out_eps * denominator * denominator)
ca, cp = alpha * scale, ep * scale
for i in cutlass.range_constexpr(8):
result[0, i] = fma(ca, mixture[0, i], cp * v[NEW, i]) * v[G, i]
for j in cutlass.range_constexpr(4):
words[j] = bf16x2(result[0, 2 * j], result[0, 2 * j + 1])
store_words(at(output, token, col), tuple(words[j] for j in range(4)))
@@ -0,0 +1,88 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Prepared seven-CTA AttnRes consumers for ordinary or Lamport mailbox input."""
from functools import cache
from typing import Any
import cutlass
import cutlass.cute as cute
from cutlass.cute.runtime import make_fake_stream
from ..common.host import (
architecture,
dynamic_fake,
dynamic_view,
fake,
stream,
to_cute,
)
from .kernel import ClusterAttnRes
@cache
def compile_consumer(nb, delta, write, final, slots, cap, arch, aux=False):
row, weight, eps = dynamic_fake(2), fake((7168,), (1,)), cutlass.Float32(1e-6)
source = fake((slots, cap, 7168), (cap * 7168, 7168, 1)) if slots else row
kernel = ClusterAttnRes(nb, slots=slots, has_delta=delta, write=write, final=final)
args = (source, row, dynamic_fake(3), weight, weight, weight, row, eps, eps)
args += (row,) * aux # the updated prefix row, also stored to an aux stream
return cute.compile(kernel, *args, make_fake_stream(), options=f"--gpu-arch={arch}")
class PreparedAttnRes:
"""Ordinary-input code plus the Lamport variant of its bound mailbox ring."""
def __init__(self, weights, key, eps):
self.device = weights[0].device
self.views = tuple(to_cute(w.reshape(-1)) for w in weights)
self.key, self.eps = key, tuple(map(cutlass.Float32, eps))
self.code = self._compile(0, 0)
self.ring: Any = None
self.aux: Any = None
def _compile(self, slots, cap, aux=False):
arch = architecture(self.device)
return compile_consumer(*self.key, slots, cap, arch, aux)
@classmethod
def for_target(cls, target, *, final=False, has_delta=True):
if final:
norm, query = target.output_attn_res_norm, target.output_attn_res_proj
output, nb, write = target.norm, target.num_attn_res_blocks, -1
else:
norm = target.self_attention_res_norm
query, output = target.self_attention_res_proj, target.input_layernorm
nb = target.prev_valid_blocks
write = target.block_write_idx if target.is_block_write_layer else -1
weights = (norm.weight, query.weight, output.weight)
eps = (norm.variance_epsilon, output.variance_epsilon)
return cls(weights, (nb, has_delta, write, final), eps)
def bind_ring(self, mailbox):
"""Lamport input: sum the [slots, capacity, 7168] mailbox rows, then re-arm."""
shape = tuple(mailbox.shape[:2])
self.ring = self._compile(*shape), to_cute(mailbox), shape
def bind_aux(self):
"""Variants that also store the updated prefix rows (a stock aux stream)."""
ring = None
if self.ring is not None:
slots, cap = self.ring[2]
ring = self._compile(slots, cap, True)
self.aux = self._compile(0, 0, True), ring
def run_ring(self, prefix, blocks, out, aux=None):
code = self.ring[0] if aux is None else self.aux[1]
return self._launch(code, self.ring[1], prefix, blocks, out, aux)
def run(self, delta, prefix, blocks, out, aux=None):
code = self.code if aux is None else self.aux[0]
return self._launch(code, dynamic_view(delta), prefix, blocks, out, aux)
def _launch(self, code, source, prefix, blocks, out, aux=None):
rows = (prefix, blocks, out) + (() if aux is None else (aux,))
prefix, blocks, view, *aux = map(dynamic_view, rows)
args = *self.views, view, *self.eps, *aux
code(source, prefix, blocks, *args, stream(self.device))
return out
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,79 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Local CuTe vocabulary gather plus the ordinary framework TP all-reduce."""
from functools import cache
import cutlass as c
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream
from vllm.distributed import get_tp_group
from .host import architecture, fake
from .primitives import launch
class EmbeddingGather:
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, (7, args[0].shape[0], 1), 128, pdl=False)
@cute.kernel
def kernel(self, args):
ids, weight, out, start, vocab = args[0:5]
lane, _, _ = cute.arch.thread_idx()
tile, token, _ = cute.arch.block_idx()
token_id = c.Int64(ids[token])
row, column = token_id - start, tile * 1024 + lane * 8
values = cute.make_rmem_tensor((8,), c.BFloat16)
values.fill(c.BFloat16(0))
if (token_id >= 0) & (token_id < vocab) & (row >= 0) & (row < weight.shape[0]):
source = weight.iterator + row * 7168 + column
cute.autovec_copy(cute.make_tensor(source, cute.make_layout((8,))), values)
destination = out.iterator + token * 7168 + column
cute.autovec_copy(values, cute.make_tensor(destination, cute.make_layout((8,))))
@cache
def compile_gather(dtype, shard, arch):
batch = cute.sym_int32()
args: tuple = (fake((batch,), None, dtype, dtype.itemsize), fake((shard, 7168)))
args += (fake((batch, 7168)), c.Int64(0), c.Int64(0))
stream = make_fake_stream(use_tvm_ffi_env_stream=True)
options = f"--gpu-arch={arch} --enable-tvm-ffi"
return cute.compile(EmbeddingGather(), *args, stream, options=options)
class PreparedEmbedding:
def __init__(self, embedding, tp, rank):
weight = embedding.weight
# The gather moves 7 x 1024 BF16 columns of contiguous rows.
if weight.dim() != 2 or weight.shape[1] != 7168 or not weight.is_contiguous():
raise ValueError(
f"embedding shard must be a contiguous [V, 7168] tensor, got "
f"{tuple(weight.shape)}"
)
if weight.dtype != torch.bfloat16:
raise ValueError(f"embedding shard must be BF16, got {weight.dtype}")
self.group, self.weight = get_tp_group(), weight.detach()
self.start, self.vocab = rank * weight.shape[0], embedding.org_vocab_size
with torch.accelerator.device_index(weight.device.index):
arch, ids = architecture(weight.device), (torch.int32, torch.int64)
self.kernels = {t: compile_gather(t, weight.shape[0], arch) for t in ids}
def run(self, ids, out):
if ids.dim() != 1 or ids.dtype not in self.kernels:
raise ValueError(
f"token ids must be 1-D int32/int64, got {ids.dtype} {tuple(ids.shape)}"
)
if out.shape != (ids.shape[0], 7168) or not out.is_contiguous():
raise ValueError(
f"embedding output must be contiguous [{ids.shape[0]}, 7168]"
)
self.kernels[ids.dtype](ids, self.weight, out, self.start, self.vocab)
reduced = self.group.all_reduce(out)
if reduced is not out:
out.copy_(reduced)
return out
@@ -0,0 +1,126 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Host-side helpers for the prepared CuTe providers."""
import ctypes
import math
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_tensor
from vllm.models.kimi_k3.nvidia.ops.cute_dsl.latent_moe_tail.primitives import to_cute
_NAMES = ("BFloat16", "Float32", "Uint8", "Uint32", "Int32", "Int64")
_DTYPES = {getattr(torch, name.lower()): getattr(cutlass, name) for name in _NAMES}
def layout(tensor):
return tuple(tensor.shape), tuple(tensor.stride()), tensor.dtype
def stream(device):
return cuda.CUstream(torch.cuda.current_stream(device).cuda_stream)
def architecture(device):
major, minor = torch.cuda.get_device_capability(device)
return f"sm_{major}{minor}a"
def fake(shape, stride=None, dtype=torch.bfloat16, align=16):
"""Row-major unless ``stride`` is given."""
stride = stride or tuple(math.prod(shape[i + 1 :]) for i in range(len(shape)))
dtype = _DTYPES.get(dtype, dtype)
return make_fake_tensor(dtype, shape, stride, assumed_align=align)
def dynamic_fake(rank, dtype=torch.bfloat16):
shape = tuple(cute.sym_int() for _ in range(rank))
return fake(shape, (*(cute.sym_int(64) for _ in range(rank - 1)), 1), dtype)
def dynamic_view(tensor, align=16):
return to_cute(tensor, align).mark_layout_dynamic(leading_dim=tensor.ndim - 1)
def tensor_maps(specs, device):
"""TMA descriptors (128 B each) in one device tensor.
Each spec: (pointer, dtype, dims, strides, box, swizzle, l2 promotion).
The bytes are uploaded with a copy on the current stream: the new tensor may
reuse a block freed by a kernel still pending on that stream (e.g. a warmup
launch's output), which a copy outside the stream would race with."""
encoded = bytearray()
with torch.accelerator.device_index(torch.device(device).index):
for pointer, dtype, dims, strides, box, swizzle, l2 in specs:
u64 = ([cuda.cuuint64_t(v) for v in values] for values in (dims, strides))
u32 = ([cuda.cuuint32_t(v) for v in box], [cuda.cuuint32_t(1)] * len(dims))
interleave = cuda.CUtensorMapInterleave.CU_TENSOR_MAP_INTERLEAVE_NONE
fill = cuda.CUtensorMapFloatOOBfill.CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE
status, tensor_map = cuda.cuTensorMapEncodeTiled(
dtype, len(dims), pointer, *u64, *u32, interleave, swizzle, l2, fill
)
if status != cuda.CUresult.CUDA_SUCCESS:
raise RuntimeError(f"cuTensorMapEncodeTiled failed: {status}")
encoded += (ctypes.c_ubyte * 128).from_address(tensor_map.getPtr())
return torch.frombuffer(encoded, dtype=torch.uint8).to(device)
def _checked(result, operation="CUDA driver call"):
"""The values of a cuda-python driver result, raising on a failed status."""
status, *values = result
if status != cuda.CUresult.CUDA_SUCCESS:
raise RuntimeError(f"{operation} failed: {status}")
return tuple(values)
class PreparedLaunchAttributes:
def __init__(self, compiled, executor):
self.compiled, self.executor = compiled, executor
def __call__(self, *args, **kwargs):
return self.compiled(*args, **kwargs)
def resolve_kernel(compiled, device):
executor = compiled.to(device)
library = compiled.library
(count,) = _checked(cuda.cuLibraryGetKernelCount(library))
if count != 1: # every prepared module compiles one kernel
raise RuntimeError(
f"expected one kernel in the compiled library, found {count}"
)
(kernels,) = _checked(cuda.cuLibraryEnumerateKernels(count, library))
(function,) = _checked(cuda.cuKernelGetFunction(kernels[0]))
return executor, function
def check_residency(compiled, device_index: int, grid: int, threads: int, what: str):
"""Persistent grids whose CTAs wait on one another (not only within a cluster) must
all be resident at once: raise when the device cannot hold ``grid`` CTAs of
``compiled`` (``threads`` per CTA, its dynamic shared memory) at the same time."""
_, function = resolve_kernel(compiled, device_index)
attribute = cuda.CUfunction_attribute
max_dynamic = attribute.CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES
(smem,) = _checked(cuda.cuFuncGetAttribute(max_dynamic, function))
(per_sm,) = _checked(
cuda.cuOccupancyMaxActiveBlocksPerMultiprocessor(function, threads, smem)
)
sms = torch.cuda.get_device_properties(device_index).multi_processor_count
if grid > per_sm * sms:
raise RuntimeError(
f"{what}: {grid} CTAs cannot be co-resident ({per_sm} per SM x {sms} SMs)"
)
def prepare_shared_carveout(compiled, device_index: int, value: int):
"""Restore ``value`` (CuTe's launcher drops preferred_smem_carveout) as a driver
function attribute; call once before capture."""
executor, function = resolve_kernel(compiled, device_index)
attribute = cuda.CUfunction_attribute
attribute = attribute.CU_FUNC_ATTRIBUTE_PREFERRED_SHARED_MEMORY_CARVEOUT
_checked(cuda.cuFuncSetAttribute(function, attribute, value))
return PreparedLaunchAttributes(compiled, executor)
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Lamport mailboxes: BF16 rows published in 16-byte fragments; a reader polls until no
fragment holds the -0.0 sentinel, then re-arms."""
import cutlass
import cutlass.cute as cute
from cutlass import Uint32
from .primitives import load_words, published, store_relaxed
SENTINEL = 0x80000000
def rearm(pointer):
store_relaxed(pointer, (Uint32(SENTINEL),) * 4)
@cute.jit
def fetch(values, i: cutlass.Constexpr[int], pointer):
"""Load fragment ``i`` into ``values[i]``; returns its dirty flag at bit ``i``."""
got = load_words(pointer, count=4, volatile=True)
for k in cutlass.range_constexpr(4):
values[i, k] = got[k]
return Uint32(not published(got)) << i
@cute.jit
def poll(base, stride, count: cutlass.Constexpr[int]):
"""Lamport-poll ``count`` 16-byte fragments at base + i*stride (elements); all
loads issue before any check and only dirty fragments re-poll."""
values = cute.make_rmem_tensor((count, 4), Uint32)
pending = Uint32(0)
for i in cutlass.range_constexpr(count):
pending = pending | fetch(values, i, base + i * stride)
while pending != 0:
again = Uint32(0)
for i in cutlass.range_constexpr(count):
if (pending >> i) & 1:
again = again | fetch(values, i, base + i * stride)
pending = again
return values
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Per-batch policy tables."""
def batch_table(spec):
"""Per-batch table of integers ("1-2:96,3-8:120"), tuples ("16:64.16.4.8") or names
("16-64:trtllm"); later entries override earlier ones."""
table = {}
for item in filter(None, spec.split(",")):
span, value = item.split(":")
lo, _, hi = span.partition("-")
if "." in value:
value = tuple(map(int, value.split(".")))
elif value.lstrip("-").isdigit():
value = int(value)
table.update({b: value for b in range(int(lo), int(hi or lo) + 1)})
return table
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""L2 weight prefetch: each warp walks a persistent device table of (address, bytes),
spreading 16-KiB chunks over the grid in table order. The owner retains the weights
and table through graph replay; activations and mutable caches may not be ranges
before their producer completes."""
import functools
import cutlass
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream
from .host import fake
from .primitives import launch, prefetch_bulk, thread
class L2Prefetch:
def __init__(self, grid, carveout):
self.grid, self.carveout = grid, carveout
@cute.jit
def __call__(self, *args):
options = dict(pdl=False, preferred_smem_carveout=self.carveout)
launch(self.kernel, args, self.grid, 32, **options)
@cute.kernel
def kernel(self, args):
(ranges,) = args
cute.arch.griddepcontrol_launch_dependents()
tid, _, _, block = thread()
worker = cutlass.Int64(block * 32 + tid)
workers = cutlass.Int64(self.grid * 32)
phase, row = cutlass.Int64(0), cutlass.Int32(0)
while row < ranges.shape[0]:
base, nbytes = ranges[row, 0], ranges[row, 1]
if (base != 0) & (nbytes > 0):
chunks = (nbytes + 16384 - 1) // 16384
index = worker - phase % workers
if index < 0:
index += workers
while index < chunks:
offset = index * 16384
size = nbytes - offset
if size > 16384:
size = cutlass.Int64(16384)
size = size & cutlass.Int64(-16)
if size > 0:
address = (base + offset) & cutlass.Int64(-16)
prefetch_bulk(address, size.to(cutlass.Int32))
index += workers
phase += chunks
row += 1
# Early exit must not let a later grid bypass its producer.
cute.arch.griddepcontrol_wait()
@functools.cache
def compile_kernel(count, grid, carveout, device_index):
ranges = fake((count, 2), None, torch.int64)
with torch.accelerator.device_index(device_index):
return cute.compile(L2Prefetch(grid, carveout), ranges, make_fake_stream())
@@ -0,0 +1,928 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Inline-PTX, memory, synchronization and math helpers of the decode kernels."""
import math
from functools import partial, wraps
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Float64, Int32, Int64, Uint8, Uint16, Uint32, Uint64
from cutlass._mlir.dialects import llvm
from cutlass.cutlass_dsl import T, dsl_user_op
from cutlass.experimental import primitives as nvvm
def ptx(assembly, constraints, operands=(), result=None, *, effects, loc=None, ip=None):
"""One verbatim asm block; a tuple ``result`` (even a 1-tuple) returns a tuple."""
if isinstance(result, tuple) and len(result) == 1:
args = (assembly, constraints, operands, result[0])
return (ptx(*args, effects=effects, loc=loc, ip=ip),)
if result is None:
ir_type = None
elif isinstance(result, tuple):
ir_type = llvm.StructType.get_literal([dtype.mlir_type for dtype in result])
else:
ir_type = result.mlir_type
value = llvm.inline_asm(
ir_type,
[operand.ir_value(loc=loc, ip=ip) for operand in operands],
assembly,
constraints,
has_side_effects=effects,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
if result is None:
return None
if isinstance(result, tuple):
return tuple(
dtype(llvm.extractvalue(dtype.mlir_type, value, [i], loc=loc, ip=ip))
for i, dtype in enumerate(result)
)
return result(value)
def asm(result=None, *, pure=False):
"""``f(*args) -> (code, constraints, operands[, result, pure])`` as one inline-PTX
op. Pointer operands pass their address: 32-bit for "r" (shared), 64-bit for "l"."""
def wrap(fn):
@dsl_user_op
@wraps(fn)
def op(*args, loc=None, ip=None, **kwargs):
code, constraints, operands, *rest = fn(*args, **kwargs)
res, fx = (*rest, pure)[:2] if rest else (result, pure)
kinds = [c for c in constraints.split(",") if c[:1] not in ("=", "~", "")]
values = []
for kind, v in zip(kinds, operands):
if hasattr(v, "toint"):
v = v.toint(loc=loc, ip=ip)
v = Int32(v) if kind == "r" else v
values.append(v)
return ptx(code, constraints, values, res, effects=not fx, loc=loc, ip=ip)
return op
return wrap
def _vector(count, first=0):
"""(".vN", "{$first,...}") of an N-word access; ("", "$first") for one word."""
names = ",".join(f"${first + i}" for i in range(count))
return (f".v{count}", "{" + names + "}") if count > 1 else ("", names)
div_rn = partial(cute.math.div, rounding=cute.math.RoundingMode.NEAREST_EVEN)
@asm(Float32, pure=True)
def add_rn(a, b):
return "add.rn.f32 $0,$1,$2;", "=f,f,f", (Float32(a), Float32(b))
@asm(Float32, pure=True)
def routing_add(a: Float32, b: Float32):
return "add.ftz.f32 $0,$1,$2;", "=f,f,f", (a, b)
@asm(Float32, pure=True)
def fma(a, b, c, *, ftz=False):
return f"fma.rn{'.ftz' if ftz else ''}.f32 $0, $1, $2, $3;", "=f,f,f,f", (a, b, c)
def dot(a, b, acc=None, **kwargs):
"""``fma`` chain over ``zip(a, b)`` from ``acc``, or from ``a[0] * b[0]``."""
for x, y in zip(a, b):
acc = x * y if acc is None else fma(x, y, acc, **kwargs)
return acc
@asm(Float32, pure=True)
def fma_bf16(a, b, c, *, high=False):
half = "h" if high else "l"
code = "{ .reg .b16 al,ah,bl,bh; mov.b32 {al,ah},$1; mov.b32 {bl,bh},$2; "
code += f"fma.rn.f32.bf16 $0,a{half},b{half},$3; }}"
return code, "=f,r,r,f", (Uint32(a), Uint32(b), Float32(c))
@asm(Uint32, pure=True)
def round_pair_square(word):
return "mul.rn.bf16x2 $0,$1,$1;", "=r,r", (Uint32(word),)
@asm(Float32, pure=True)
def inv_rms(value, eps, *, m8=False):
"""rsqrt(value / 3584 + eps); ``m8``: the mean as a reciprocal product refined by
its FMA residual."""
mean = "div.rn.f32 r,$1,0f45600000;"
if m8:
mean = "mul.f32 q,$1,0f39924925; neg.f32 r,q; fma.rn.f32 r,r,0f45600000,$1; "
mean += "fma.rn.f32 r,r,0f39924925,q;"
code = f"{{ .reg .f32 {'q,' * m8}r; {mean} add.f32 r,r,$2; "
code += "rsqrt.approx.ftz.f32 $0,r; }"
return code, "=f,f,f", (Float32(value), Float32(eps))
@asm(Float32, pure=True)
def query_norm_rsqrt(total: Float32, eps: Float32):
code = "{ .reg .f32 mean; div.rn.f32 mean,$1,0f44c00000; "
code += "add.f32 mean,mean,$2; rsqrt.approx.f32 $0,mean; }"
return code, "=f,f,f", (total, eps)
@asm(Float64, pure=True)
def reciprocal64(value: Float64):
step = "fma.rn.f64 e,n,r,0d3ff0000000000000; fma.rn.f64 {},r,e,r; "
code = "{ .reg .f64 r,e,n; neg.f64 n,$1; rcp.approx.ftz.f64 r,$1; "
code += step.format("r") * 2 + step.format("$0") + "}"
return code, "=d,d", (value,)
@asm(Float32, pure=True)
def sigmoid_reciprocal(denominator: Float32, *, fast_math):
"""1 / (1 + exp(-x)) from its denominator: an approximate divide with ``fast_math``,
else the approximate reciprocal refined by two IEEE FMAs (out-of-range: div.rn)."""
if fast_math:
return "div.approx.ftz.f32 $0, 0f3f800000, $1;", "=f,f", (denominator,)
code = (
"{\n.reg .f32 r,error,neg_error; .reg .b32 bits; .reg .pred regular;\n"
"mov.b32 bits,$1; add.u32 bits,bits,0x01800000; and.b32 bits,bits,0x7f800000;\n"
"setp.gt.u32 regular,bits,0x01ffffff; rcp.approx.ftz.f32 r,$1;\n"
"fma.rn.f32 error,$1,r,0fbf800000; neg.f32 neg_error,error;\n"
"fma.rn.f32 $0,r,neg_error,r; @!regular div.rn.f32 $0,0f3f800000,$1;\n}"
)
return code, "=f,f", (denominator,)
@cute.jit
def sigmoid(value: Float32, fast_math: cutlass.Constexpr = None):
"""1 / (1 + exp(-x)); correctly rounded when ``fast_math`` is None."""
if cutlass.const_expr(fast_math is None):
result = div_rn(Float32(1), Float32(1) + cute.math.exp(-value, fastmath=False))
else:
denominator = Float32(1) + cute.math.exp(-value, fastmath=True)
result = sigmoid_reciprocal(denominator, fast_math=fast_math)
return result
@asm(Float32, pure=True)
def routing_sigmoid(value: Float32):
code = "{ .reg .f32 h,t; mul.ftz.f32 h,$1,0f3f000000; tanh.approx.f32 t,h; "
code += "fma.rn.ftz.f32 $0,t,0f3f000000,0f3f000000; }"
return code, "=f,f", (value,)
@asm(Float32, pure=True)
def routing_weight(score: Float32, total: Float32, scale: Float32, *, renorm=True):
code = "mul.ftz.f32 $0,$3,$1;"
if renorm:
code = (
"{ .reg .f32 n,d; mul.ftz.f32 n,$3,$1; "
"add.ftz.f32 d,0f1e3ce508,$2; div.approx.ftz.f32 $0,n,d; }"
)
return code, "=f,f,f,f", (score, total, scale)
@asm(Float32, pure=True)
def situ(g: Float32, u: Float32, beta_lb: Float32, inv_beta: Float32, inv_lb: Float32):
code = "{ .reg .f32 tg,tu,ex,rc,z; mul.f32 tg,$1,$4; tanh.approx.f32 tg,tg; "
code += "mul.f32 tu,$2,$5; tanh.approx.f32 tu,tu; mul.f32 z,$1,0fbfb8aa3b; "
code += "ex2.approx.ftz.f32 ex,z; add.f32 ex,ex,0f3f800000; "
code += "rcp.approx.ftz.f32 rc,ex; mul.f32 z,$3,tg; mul.f32 z,z,rc; "
code += "mul.f32 $0,z,tu; }"
return code, "=f,f,f,f,f,f", (g, u, beta_lb, inv_beta, inv_lb)
@cute.jit
def shared_situ(gate, up, beta, inverse_beta, linear_beta, inverse_linear_beta):
denominator = Float32(1) + cute.math.exp(-gate, fastmath=False)
sigmoid = Float32(0)
if denominator != Float32(float("inf")):
sigmoid = Float32(reciprocal64(Float64(denominator)))
g = (beta * cute.math.tanh(gate * inverse_beta, fastmath=False)) * sigmoid
u = up
if linear_beta > 0:
divided = Float32(Float64(up) * inverse_linear_beta)
u = linear_beta * cute.math.tanh(divided, fastmath=False)
return g * u
def bits_float(bits):
return Float32(llvm.bitcast(T.f32(), Uint32(bits).ir_value()))
def float_bits(value: Float32) -> Int32:
return Int32(llvm.bitcast(T.i32(), value.ir_value()))
def bf16(word, high=False):
"""FP32 of the low (or high) BF16 half of ``word``."""
return bits_float(word & 0xFFFF0000 if high else word << 16)
@asm((Float32, Float32), pure=True)
def unpack_bf16(word: Uint32):
return "shl.b32 $0,$2,16; and.b32 $1,$2,0xffff0000;", "=f,=f,r", (word,)
@asm((Float32, Float32), pure=True)
def fma2(a, b, c):
"""Lanewise a * b + c of FP32 pairs in one packed instruction (IEEE per lane)."""
code = "{ .reg .b64 a,b,c,d; mov.b64 a,{$2,$3}; mov.b64 b,{$4,$5}; "
code += "mov.b64 c,{$6,$7}; fma.rn.f32x2 d,a,b,c; mov.b64 {$0,$1},d; }"
return code, "=f,=f,f,f,f,f,f,f", (*a, *b, *c)
@asm((Float32, Float32), pure=True)
def mul2(a, b):
"""Lanewise a * b of FP32 pairs in one packed instruction (IEEE per lane)."""
code = "{ .reg .b64 a,b,d; mov.b64 a,{$2,$3}; mov.b64 b,{$4,$5}; "
code += "mul.rn.f32x2 d,a,b; mov.b64 {$0,$1},d; }"
return code, "=f,=f,f,f,f,f", (*a, *b)
@asm(Uint16, pure=True)
def gate_bf16_bits(value: Float32):
return "cvt.rn.bf16.f32 $0, $1;", "=h,f", (value,)
@asm(Uint32, pure=True)
def pack_pair(low, high, *, policy="plain"):
"""BF16x2 of (low, high); ``policy=None`` is the bare conversion."""
code = "cvt.rn.bf16x2.f32 $0,$2,$1; "
if policy == "stage":
# NaN halves become canonical, so no word is the 0xffffffff staging sentinel.
code += (
"and.b32 bits,$0,0x7fff; setp.gt.u32 p,bits,0x7f80; "
"@p and.b32 $0,$0,0xffff0000; @p or.b32 $0,$0,0x7fc0; "
"and.b32 bits,$0,0x7fff0000; setp.gt.u32 p,bits,0x7f800000; "
"@p and.b32 $0,$0,0xffff; @p or.b32 $0,$0,0x7fc00000; "
)
if policy == "latent":
# Negative BF16 zero is the downstream mailbox's unpublished sentinel.
code += (
"and.b32 bits,$0,0xffff; setp.eq.u32 p,bits,0x8000; "
"@p and.b32 $0,$0,0xffff0000; shr.u32 bits,$0,16; "
"setp.eq.u32 p,bits,0x8000; @p and.b32 $0,$0,0xffff; "
)
if policy is not None:
code = "{ .reg .b32 bits; .reg .pred p; " + code + "}"
return code, "=r,f,f", (Float32(low), Float32(high))
bf16x2 = partial(pack_pair, policy=None)
@cute.jit
def no_neg_zero(word):
"""``word`` with each negative-zero BF16 half made +0."""
result = Uint32(word)
if (result & 0xFFFF) == 0x8000:
result &= Uint32(0xFFFF0000)
if (result >> 16) == 0x8000:
result &= Uint32(0xFFFF)
return result
def published(words, ready=True, *, sentinel=0x80000000):
"""``ready`` and no word is the mailbox ``sentinel``."""
for word in words:
ready &= word != Uint32(sentinel)
return ready
@cute.jit
def front_dirty(word):
return ((word & 0xFFFF) == 0xFFFF) | ((word >> 16) == 0xFFFF)
def fp8_bits(value: Float32):
code = (
"{ .reg .b16 pair; cvt.rn.satfinite.e4m3x2.f32 pair, 0f00000000, $1; "
"cvt.u32.u16 $0, pair; }"
)
return Uint8(ptx(code, "=r,f", (value,), Uint32, effects=False))
@asm()
def store_fp8_pair(pointer, low: Float32, high: Float32):
"""FP8 E4M3 (satfinite) of ``low`` and ``high`` as the two bytes at ``pointer``."""
code = "{ .reg .b16 pair; cvt.rn.satfinite.e4m3x2.f32 pair,$2,$1; "
code += "st.global.b16 [$0],pair; }"
return code, "l,f,f,~{memory}", (pointer, low, high)
@asm(Uint32, pure=True)
def pack_fp8(values):
code = "{ .reg .b16 lo,hi; cvt.rn.satfinite.e4m3x2.f32 lo,$2,$1; "
code += "cvt.rn.satfinite.e4m3x2.f32 hi,$4,$3; mov.b32 $0,{lo,hi}; }"
return code, "=r,f,f,f,f", values
@cute.jit
def mx_exponent(amax: Float32, floor: cutlass.Constexpr[bool] = False):
bits = Uint32(float_bits(amax))
e = Int32(bits >> 23)
result = Int32(0)
if cutlass.const_expr(floor):
if e == 255:
result = 254
elif e > 8:
result = e - 8
else:
if e != 0:
result = min(max(e - 8 + Int32(bits & 8388607 > 6291456), 0), 254)
return result
@cute.jit
def mx_rescale(scale: Int32):
e = 254 - scale
return bits_float(Uint32(e) << 23) if e >= 1 else Float32(0)
@cute.jit
def warp_sum(value: Float32, lanes: cutlass.Constexpr[int] = 32):
for shift in cutlass.range_constexpr(lanes.bit_length() - 2, -1, -1):
value += cute.arch.shuffle_sync_bfly(value, 1 << shift)
return value
@asm(Uint32, pure=True)
def redux(value, op="min"):
return f"redux.sync.{op}.u32 $0,$1,0xffffffff;", "=r,r", (Uint32(value),)
def warp_max_abs(value):
return bits_float(redux(float_bits(abs(value)), "max"))
@asm((Float32,) * 4, pure=True)
def mma(a, b, accumulator, *, fp8=False, bf16=False):
"""m16n8k16 (FP8: k32) mma.sync."""
kind = "e4m3" if fp8 else "bf16" if bf16 else "f16"
code = f"mma.sync.aligned.m16n8k{32 if fp8 else 16}.row.col.f32.{kind}.{kind}.f32 "
code += "{$0,$1,$2,$3},{$4,$5,$6,$7},{$8,$9},{$10,$11,$12,$13};"
return code, "=f,=f,=f,=f,r,r,r,r,r,r,f,f,f,f", (*a, *b, *accumulator)
def mma_into(acc, a, b, **kwargs):
"""``acc += a @ b`` with one ``mma``; ``acc`` is a four-element register view."""
result = mma(a, b, tuple(acc[k] for k in range(4)), **kwargs)
for k in range(4):
acc[k] = result[k]
@asm()
def ldmatrix(pointer, *, trans=False, count=4):
"""``count`` 8x8 b16 shared matrices; lanes 8i..8i+7 address matrix i's rows."""
names = ",".join(f"${i}" for i in range(count))
code = f"ldmatrix.sync.aligned.m8n8.x{count}{'.trans' * trans}.shared.b16 "
code += f"{{{names}}},[${count}];"
constraints = ",".join(["=r"] * count + ["r"])
return code, constraints, (pointer,), (Uint32,) * count
def split(total, i, n):
"""(first, count) of part ``i`` of ``total`` items split ``n`` ways."""
first = total * i // n
return first, total * (i + 1) // n - first
def thread():
"""(thread, warp, lane, CTA) indices."""
tid, _, _ = cute.arch.thread_idx()
cta, _, _ = cute.arch.block_idx()
return tid, tid // 32, tid % 32, cta
def shared_address(pointer):
return Uint32(pointer.toint())
def at(tensor, *coord):
"""Pointer to ``tensor[coord]``."""
return tensor.iterator + cute.crd2idx(coord, tensor.layout)
def view(base, offset, dtype, shape, stride=None):
"""``dtype`` tensor at byte ``offset`` of pointer ``base``."""
pointer = cute.recast_ptr(base + offset, dtype=dtype)
return cute.make_tensor(pointer, cute.make_layout(shape, stride=stride))
def zeros(shape, dtype):
tensor = cute.make_rmem_tensor(shape, dtype)
tensor.fill(0)
return tensor
def alloc(allocator, dtype, shape, align=16):
"""Row-major ``shape`` tensor from a ``SmemAllocator``."""
stride = None
if isinstance(shape, tuple):
stride = tuple(math.prod(shape[i + 1 :]) for i in range(len(shape)))
layout = cute.make_layout(shape, stride=stride)
return allocator.allocate_tensor(dtype, layout, align)
@asm()
def load_words(
pointer,
*,
count=2,
shared=False,
evict_first=False,
mutable=False,
volatile=False,
scope=None,
cache_policy="cg",
word_type="b32",
result_type=Uint32,
):
"""``count`` words (a tuple); ``scope`` is a relaxed load at that scope.
``mutable`` (a side-effecting load) for data written during the step (activations,
tables, refills). Only static weights use ``evict_first``."""
space = "global" + (f".{cache_policy}" if cache_policy else "")
if shared:
space = "shared"
if volatile:
space = "volatile.global"
if scope:
space = f"relaxed.{scope}.global"
policy = hint = tail = ""
if evict_first:
assert not shared
space = "global.nc.L1::no_allocate"
policy = ".reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol, 1.0; "
hint, tail = ".L2::cache_hint", ", pol"
vector, registers = _vector(count)
address = f"[${count}]{tail}"
code = f"{{ {policy}ld.{space}{hint}{vector}.{word_type} {registers}, {address}; }}"
constraints = ["=f" if result_type is Float32 else "=r"] * count
constraints += ["r" if shared else "l"] + (["~{memory}"] if scope else [])
# Shared data is written by other threads between reads: never pure.
pure = not (shared or mutable or volatile or scope)
return code, ",".join(constraints), (pointer,), (result_type,) * count, pure
smem_words = partial(load_words, count=4, shared=True, mutable=True)
load_volatile = partial(load_words, count=1, volatile=True, word_type="u32")
mailbox_load = partial(load_words, count=1, scope="gpu", word_type="u32")
load_f32x4 = partial(load_words, count=4, result_type=Float32, word_type="f32")
early_weights = partial(
load_words, cache_policy="nc.L1::no_allocate", word_type="u32", mutable=True
)
@asm()
def load_bf16x8(pointer, *, shared=False, mutable=False):
opcode = "ld.shared.v4.b32" if shared else "ld.global.cg.v4.b32"
# Shifting BF16 bits into the high FP32 half preserves values exactly.
code = "{ .reg .b32 x<4>; " + opcode + " {x0,x1,x2,x3}, [$8]; "
for j in range(4):
code += f"shl.b32 ${2 * j}, x{j}, 16; and.b32 ${2 * j + 1}, x{j}, 0xffff0000; "
constraints = ",".join(["=f"] * 8 + ["r" if shared else "l"])
return code + "}", constraints, (pointer,), (Float32,) * 8, not (shared or mutable)
@asm()
def load_vector(pointer, *, cache="cg"):
"""``cache`` "weight": a streaming L2 evict_first load of static weights; else
``ld.global.<cache>``. Side-effecting in every mode."""
opcode, policy, tail = "ld.global." + cache, "", ""
if cache == "weight":
policy = ".reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol,1.0; "
opcode = "ld.global.L1::no_allocate.L2::cache_hint.L2::256B"
tail = ",pol"
if cache == "volatile":
opcode = "ld.volatile.global"
code = "{ " + policy + opcode + ".v4.b32 {$0,$1,$2,$3},[$4]" + tail + "; }"
clobber = ",~{memory}" if cache == "volatile" else ""
return code, "=r,=r,=r,=r,l" + clobber, (pointer,), (Uint32,) * 4
@asm(Float32, pure=True)
def table_load(pointer):
return "ld.global.nc.f32 $0,[$1];", "=f,l", (pointer,)
@asm()
def store_words(pointer, words, *, shared=False, scope=None):
"""``scope`` is a relaxed global store at that scope."""
vector, registers = _vector(len(words), 1)
if shared:
# Other threads read it: a memory side effect, like the global stores.
space, constraints = (
f"shared{vector}.b32",
"r" + ",r" * len(words) + ",~{memory}",
)
else:
space = (
f"relaxed.{scope}.global{vector}.u32" if scope else f"global{vector}.b32"
)
constraints = "l" + ",r" * len(words) + ",~{memory}"
words = tuple(Uint32(v) for v in words)
return f"st.{space} [$0], {registers};", constraints, (pointer, *words)
store_shared_words = partial(store_words, shared=True)
store_relaxed = partial(store_words, scope="sys")
mailbox_store = partial(store_words, scope="gpu")
@asm()
def store_f32x4(pointer, values, when=None):
"""``when``: a predicate, so callers need no branch."""
code = "st.global.v4.f32 [$0], {$1, $2, $3, $4};"
if when is None:
return code, "l,f,f,f,f", (pointer, *values)
code = "{ .reg .pred p; setp.ne.b32 p,$5,0; @p " + code + " }"
return code, "l,f,f,f,f,r", (pointer, *values, Int32(when))
@asm()
def store_gate_mailbox(pointer, bits: Uint16):
return "st.relaxed.gpu.global.u16 [$0], $1;", "l,h,~{memory}", (pointer, bits)
@asm()
def store_bf16_pair(pointer, a, b):
code = "{ .reg .b32 pair; cvt.rn.bf16x2.f32 pair,$2,$1; "
return code + "st.relaxed.gpu.global.b32 [$0],pair; }", "l,f,f", (pointer, a, b)
@asm()
def store_pair(pointer, a, b):
"""BF16x2 (a, b) with negative-zero halves made +0."""
code = "{ .reg .b32 pair,half; .reg .pred p; cvt.rn.bf16x2.f32 pair,$2,$1; "
code += "and.b32 half,pair,0xffff; setp.eq.b32 p,half,0x8000; "
code += "@p and.b32 pair,pair,0xffff0000; shr.u32 half,pair,16; "
code += "setp.eq.b32 p,half,0x8000; @p and.b32 pair,pair,0x0000ffff; "
return code + "st.global.b32 [$0],pair; }", "l,f,f", (pointer, a, b)
@asm()
def push_float(pointer, value, barrier, peer, when=None):
"""DSMEM store of one FP32 ``value`` (or a 4-tuple) completing on ``barrier``
(``when``: predicated on it)."""
values = tuple(map(Float32, value if isinstance(value, tuple) else (value,)))
vector, registers = _vector(len(values), 1)
b, p, w = (f"${len(values) + i}" for i in (1, 2, 3))
code = f"{{ .reg .b32 dst,bar; mapa.shared::cluster.u32 dst,$0,{p}; "
code += f"mapa.shared::cluster.u32 bar,{b},{p}; "
guard = "" if when is None else f".reg .pred w; setp.ne.b32 w,{w},0; @w "
code += (
guard + f"st.async.shared::cluster.mbarrier::complete_tx::bytes{vector}.b32 "
)
code += f"[dst],{registers},[bar]; }}"
constraints = "r" + ",f" * len(values) + ",r,r" + ",r" * (when is not None)
operands = (pointer, *values, barrier, Int32(peer))
operands += () if when is None else (Int32(when),)
return code, constraints + ",~{memory}", operands
@asm()
def multicast_words(address, words, *, kind="f32"):
"""Relaxed multimem store of FP32 ("f32") or integer ("b32", "bf16x2") words."""
vector, registers = _vector(len(words), 1)
f32 = kind == "f32"
values = (bits_float(v) if f32 else Uint32(v) for v in words)
constraints = "l" + (",f" if f32 else ",r") * len(words) + ",~{memory}"
code = f"multimem.st.relaxed.sys.global{vector}.{kind} [$0],{registers};"
return code, constraints, (Int64(address), *values)
@asm()
def publish_hint(pointer):
return "red.relaxed.gpu.global.add.u32 [$0],1;", "l,~{memory}", (pointer,)
@asm()
def copy_g2s(dst, src, size=16, barrier=None, *, evict_first=False):
"""16-byte cp.async, or a static/dynamic ``size`` bulk copy on ``barrier``."""
operands = [dst, src]
policy = hint = tail = ""
if evict_first:
policy = ".reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol, 1.0; "
hint, tail = ".L2::cache_hint", ", pol"
if barrier is None:
assert size == 16
code = f"cp.async.cg.shared.global{hint} [$0], [$1], 16{tail};"
else:
operands.append(barrier)
if not isinstance(size, int):
operands.append(size)
size = "$3"
code = (
"cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes"
f"{hint} [$0], [$1], {size}, [$2]{tail};"
)
constraints = ",".join(["r", "l"] + ["r"] * (len(operands) - 2))
return "{ " + policy + code + " }", constraints, operands
copy_evict = partial(copy_g2s, evict_first=True)
@asm()
def copy_bulk(dst, src, barrier, size):
code = "{ .reg .b64 p; createpolicy.fractional.L2::evict_first.b64 p,1.0; "
code += "cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes"
code += ".L2::cache_hint [$0],[$1],$3,[$2],p; }"
return code, "r,l,r,r,~{memory}", (dst, src, barrier, Int32(size))
@asm()
def copy_s2cluster(dst, src, barrier, peer, *, size):
code = "{ .reg .b32 dst,bar; mapa.shared::cluster.u32 dst,$0,$3; "
code += "mapa.shared::cluster.u32 bar,$2,$3; cp.async.bulk.shared::cluster"
code += f".shared::cta.mbarrier::complete_tx::bytes [dst],[$1],{size},[bar]; }}"
return code, "r,r,r,r", (dst, src, barrier, Int32(peer))
@asm()
def stage_weight(dst, src):
code = "{ .reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol,1.0; "
code += "cp.async.cg.shared.global.L2::cache_hint.L2::256B [$0],[$1],16,pol; }"
return code, "r,l,~{memory}", (dst, src)
@asm()
def load_tma2(dst, descriptor, barrier, column, row):
code = "{ .reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol,1.0; "
code += "cp.async.bulk.tensor.2d.shared::cluster.global.tile.mbarrier"
code += "::complete_tx::bytes.L2::cache_hint [$0],[$1,{$3,$4}],[$2],pol; }"
return code, "r,l,r,r,r", (dst, descriptor, barrier, Int32(column), Int32(row))
@asm()
def load_tma3(dst, descriptor, barrier, row, chunk):
code = "{ .reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol,1.0; "
code += "cp.async.bulk.tensor.3d.shared::cluster.global.tile.mbarrier"
code += "::complete_tx::bytes.L2::cache_hint [$0],[$1,{0,$3,$4}],[$2],pol; }"
return code, "r,l,r,r,r", (dst, descriptor, barrier, Int32(row), Int32(chunk))
@asm()
def prefetch_bytes(src, size):
return "cp.async.bulk.prefetch.L2.global [$0], $1;", "l,r", (src, Int32(size))
@dsl_user_op
def prefetch_bulk(address, size, *, loc=None, ip=None):
"""``prefetch_bytes`` of a 64-bit address through the NVVM op."""
pointer = cute.make_ptr(cutlass.Uint8, address, cute.AddressSpace.gmem)
nvvm.cp_async_bulk_prefetch(pointer, size, loc=loc, ip=ip)
@asm()
def prefetch_l2(pointer):
return "prefetch.global.L2 [$0];", "l", (pointer,)
@asm()
def prefetch_map(descriptor, row, chunk):
code = "cp.async.bulk.prefetch.tensor.3d.L2.global.tile [$0,{0,$1,$2}];"
return code, "l,r,r", (descriptor, Int32(row), Int32(chunk))
def acquire_tensormap(pointer):
generic = cute.make_ptr(cutlass.Uint8, pointer.toint(), cute.AddressSpace.generic)
nvvm.fence_proxy_acquire("sys", generic, 128)
nvvm.prefetch_tensormap(generic)
@asm()
def acquire_tensormap_asm(pointer):
"""``acquire_tensormap`` as one asm block on the unfolded address."""
code = "fence.proxy.tensormap::generic.acquire.sys [$0],128; "
return code + "prefetch.tensormap [$0];", "l", (pointer,)
@asm(Int32)
def try_wait(barrier, phase=0, *, test=False, scope="cluster"):
"""Acquire parity probe at ``scope`` ("cluster", or "cta" when every arrival is
CTA-local); ``test`` is the non-blocking probe of phase 0."""
op, parity = ("test_wait", "0") if test else ("try_wait", "$2")
code = f"{{ .reg .pred p; mbarrier.{op}.parity.acquire.{scope}.shared::cta.b64 "
code += f"p,[$1],{parity}; selp.u32 $0,1,0,p; }}"
if test:
return code, "=r,r", (barrier,)
return code, "=r,r,r,~{memory}", (barrier, Int32(phase))
@cute.jit
def wait(barrier, phase=0, test: cutlass.Constexpr[bool] = False, scope="cluster"):
done = try_wait(barrier, phase, test=test, scope=scope)
while done == 0:
done = try_wait(barrier, phase, test=test, scope=scope)
@asm(Int32)
def try_wait_phase_zero(barrier):
code = "{ .reg .pred ready; mbarrier.try_wait.parity.acquire.cluster"
code += ".shared::cta.b64 ready,[$1],0; selp.u32 $0,1,0,ready; }"
return code, "=r,r,~{memory}", (barrier,)
@cute.jit
def wait_phase_zero_cluster(barrier: cute.Pointer):
done = try_wait_phase_zero(barrier)
while done == 0:
done = try_wait_phase_zero(barrier)
def init_barriers(barriers, count, *, async_proxy=False):
for i in range(count):
cute.arch.mbarrier_init(barriers + i, 1)
cute.arch.mbarrier_init_fence()
if async_proxy:
cute.arch.fence_proxy("async.shared", space="cta")
@asm()
def named_barrier(barrier_id=1, count=128, *, arrive=False, all_groups=False):
if all_groups:
barrier_id, count = 2, 512
op = "arrive" if arrive else "sync"
return f"bar.{op} {barrier_id},{count};", "~{memory}", ()
def group_barrier(group):
cute.arch.barrier(barrier_id=1 + 3 * group, number_of_threads=128)
@asm()
def sleep_ns(ns):
"""``ns`` is an immediate if a Python int, else a register."""
if isinstance(ns, int):
return f"nanosleep.u32 {ns};", "", ()
return "nanosleep.u32 $0;", "r", (Uint32(ns),)
@cute.jit
def smem_desc(pointer: cute.Pointer, swizzle: cutlass.Constexpr[bool]):
address = shared_address(pointer)
value = Uint64((address & 262143) >> 4) | Uint64(1) << 46
if cutlass.const_expr(swizzle):
value |= Uint64(1) << 16 | Uint64(64) << 32 | Uint64(2) << 61
else:
value |= Uint64(8) << 32
return value
def fence_tmem(*, before=False):
nvvm.tcgen05_fence("before_thread_sync" if before else "after_thread_sync")
def wait_tmem(barrier, phase=0, scope="cluster"):
"""``wait``, then order later tcgen05 operations after it."""
wait(barrier, phase, scope=scope)
fence_tmem()
@asm()
def free_tmem(address, columns):
code = f"tcgen05.dealloc.cta_group::1.sync.aligned.b32 $0,{columns};"
return code, "r", (Uint32(address),)
@cute.jit
def release_tmem(tmem, columns: cutlass.Constexpr[int], warp, owner):
"""CTA teardown: warp ``owner`` frees the TMEM after every tcgen05 operation."""
fence_tmem(before=True)
cute.arch.sync_threads()
if warp == owner:
fence_tmem()
free_tmem(tmem, columns)
@asm()
def require_zero_tmem(address):
return "{ .reg .pred p; setp.ne.u32 p,$0,0; @p trap; }", "r", (Uint32(address),)
@asm()
def load_tmem(address, count=1):
"""``count`` FP32 TMEM columns of this lane, waiting for the load."""
registers = ",".join(f"${i}" for i in range(count))
code = f"tcgen05.ld.sync.aligned.32x32b.x{count}.b32 {{{registers}}},[${count}]; "
code += "tcgen05.wait::ld.sync.aligned;"
constraints = ",".join(["=f"] * count + ["r", "~{memory}"])
result = Float32 if count == 1 else (Float32,) * count
return code, constraints, (Uint32(address),), result
@asm()
def copy_scale(column, descriptor):
code = "tcgen05.cp.cta_group::1.32x128b.warpx4 [$0],$1;"
return code, "r,l", (Uint32(column), Uint64(descriptor))
commit = nvvm.tcgen05_commit
@asm()
def commit_asm(barrier):
"""``commit`` as an asm block on the unfolded address."""
code = "tcgen05.commit.cta_group::1.mbarrier::arrive::one.shared::cluster.b64 [$0];"
return code, "r", (barrier,)
@asm()
def mma_bf16_block(d, a, b, instruction, accumulate, empty, release):
"""Warp-uniform issue of one 64-K block (K-major 128B-swizzled A and B): the
elected lane runs the four K16 BF16 MMAs into ``d`` (the first accumulates iff
``accumulate`` is nonzero) and, if ``release``, commits ``empty``."""
mma = "tcgen05.mma.cta_group::1.kind::f16"
commit = "tcgen05.commit.cta_group::1.mbarrier::arrive::one.shared::cluster.b64"
code = "{ .reg .pred e,p0,pt,pc; .reg .b64 a1,a2,a3,b1,b2,b3; "
code += "elect.sync _|e,0xffffffff; setp.eq.b32 pt,0,0; setp.ne.b32 p0,$4,0; "
code += "setp.ne.b32 pc,$6,0; and.pred pc,pc,e; "
code += "add.s64 a1,$1,2; add.s64 a2,$1,4; add.s64 a3,$1,6; "
code += "add.s64 b1,$2,2; add.s64 b2,$2,4; add.s64 b3,$2,6; "
code += f"@e {mma} [$0],$1,$2,$3,p0; @e {mma} [$0],a1,b1,$3,pt; "
code += f"@e {mma} [$0],a2,b2,$3,pt; @e {mma} [$0],a3,b3,$3,pt; "
code += f"@pc {commit} [$5]; }}"
operands: tuple = (Uint32(d), Uint64(a), Uint64(b), Uint32(instruction))
operands += (Uint32(accumulate), empty, Uint32(release))
return code, "r,l,l,r,r,r,r", operands
@asm()
def mma_mx(d, a, b, instruction, sfa, sfb, accumulate):
"""One block-scaled MXF8 tcgen05 MMA; ``accumulate`` is a runtime flag."""
words = (Uint32(v) for v in (instruction, sfa, sfb, accumulate))
code = "{ .reg .pred p; setp.ne.b32 p,$6,0; tcgen05.mma.cta_group::1"
code += ".kind::mxf8f6f4.block_scale [$0],$1,$2,$3,[$4],[$5],p; }"
return code, "r,l,l,r,r,r,r", (Uint32(d), Uint64(a), Uint64(b), *words)
@cute.jit
def mma_steps(d, a, b, sfa, sfb, accumulate, fc2: cutlass.Constexpr[bool]):
for k in cutlass.range_constexpr(6 if fc2 else 8):
a_offset: cutlass.Constexpr = k // 4 * 1024 + k % 4 * 2 + 4 * (fc2 and k >= 4)
instruction: cutlass.Constexpr = 142738048 | (k % 4 << 29) | (k % 4 << 4)
ops: tuple = (d, a + a_offset, b + k // 4 * 64 + k % 4 * 2, instruction)
ops += (sfa + k // 4 * 4, sfb + (k * 4 if fc2 else k // 4 * 4))
mma_mx(*ops, accumulate if k == 0 else 1)
@asm()
def chunk_mma(*args):
"""(d, adesc, bdesc, sfa, sfa_src, sfb, sfb_src, copy_sfb, accum, empty_bar,
acc_bar): four K32 MMAs of one K128 chunk, with optional completion arrivals."""
constraints = "r,l,l,r,l,r,l,r,r,r,r"
types = [Uint32 if kind == "r" else Uint64 for kind in constraints.split(",")]
cp, mma = "tcgen05.cp.cta_group::1.32x128b.warpx4", "tcgen05.mma.cta_group::1"
commit = "tcgen05.commit.cta_group::1.mbarrier::arrive::one.shared::cluster.b64"
code = (
"{\n.reg .pred e,p0,pt,pc,pa,pe; .reg .b32 i<4>; .reg .b64 xa<4>,xb<4>;\n"
"elect.sync _|e,0xffffffff; setp.eq.b32 pt,0,0; setp.ne.b32 pc,$7,0;\n"
"and.pred pc,pc,e; setp.ne.b32 pa,$10,0; and.pred pa,pa,e;\n"
"setp.ne.b32 pe,$9,0; and.pred pe,pe,e; setp.ne.b32 p0,$8,0;\n"
"mov.b32 i0,142738048; or.b32 i1,i0,536870928; or.b32 i2,i0,1073741856;\n"
f"or.b32 i3,i0,1610612784; @pc {cp} [$5],$6; @e {cp} [$3],$4;\n"
"add.s64 xa0,$1,0; add.s64 xb0,$2,0; add.s64 xa1,$1,2; add.s64 xb1,$2,2;\n"
"add.s64 xa2,$1,4; add.s64 xb2,$2,4; add.s64 xa3,$1,6; add.s64 xb3,$2,6;\n"
)
for k in range(4):
code += f"@e {mma}.kind::mxf8f6f4.block_scale [$0],\n"
code += f"xa{k},xb{k},i{k},[$3],[$5],{'pt' if k else 'p0'};\n"
code += f"@pe {commit} [$9]; @pa {commit} [$10];\n}}"
return code, constraints, tuple(t(v) for t, v in zip(types, args))
def launch(kernel, args, grid, block, cluster=None, pdl=True, **options):
"""Launch kernel(args[:-1]) on stream args[-1]; an int dimension means (n, 1, 1)."""
dim = lambda d: d if isinstance(d, tuple) else (d, 1, 1)
if cluster is not None:
options["cluster"] = dim(cluster)
kernel(args[:-1]).launch(
grid=dim(grid), block=dim(block), stream=args[-1], use_pdl=pdl, **options
)
@cute.jit
def scatter_reduce(values, lane, count: cutlass.Constexpr[int]):
"""Warp-reduce ``count`` values; lane groups end up owning one value each."""
selected = Int32(0)
for stage in cutlass.range_constexpr(5):
offset = 16 >> stage
remaining = max(count >> stage, 1)
if cutlass.const_expr(remaining > 1):
half = remaining // 2
upper = (lane & offset) != 0
for i in cutlass.range_constexpr(half):
send = values[i] if upper else values[i + half]
keep = values[i + half] if upper else values[i]
values[i] = keep + cute.arch.shuffle_sync_bfly(send, offset)
if upper:
selected += half
else:
values[0] += cute.arch.shuffle_sync_bfly(values[0], offset)
return values[0], selected
@@ -0,0 +1,115 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Two-shot Lamport all-reduce of BF16 rows [B, width] over TP ranks.
Rank ``o`` owns columns [o W, (o + 1) W), W = width / owners. Every buffer is
Lamport-armed and only its local reader re-arms it:
- ``push``: a rank stores its 8-column chunk of row ``token`` into the owner's
receive slot [rank] (a peer store; the owner's ``recv`` [TP, cap, W]; callers with a
runtime parity pass the whole [parities x TP, cap, W] and slot parity x TP + rank).
- ``reduce``: the owner polls the TP slots of an owned chunk in its own memory, sums
them in FP32 in rank order, rounds once and multicasts the BF16 sum into every
rank's gather row [cap, width]; then re-arms the slots.
- every rank then polls its gather row (one slot) and re-arms it.
Each GPU moves ~2 x B x width x 2 bytes instead of TP x B x width x 2 (one-shot), and
every rank gets the same bits. ``owners`` = 1 is a one-GPU stand-in: rank 0 owns every
column and its TP local slots play the ranks. The o-projection reduces with ``reduce``
and re-arms at once (two parities); the MoE tail reduces in WaveMoeTail and re-arms the
previous MoE layer's rows (three parities).
"""
import cutlass
import cutlass.cute as cute
import torch
import torch.distributed._symmetric_memory as symm_mem
from cutlass import Float32, Int32, Int64, Uint32
from .mailbox import rearm
from .primitives import (
add_rn,
at,
load_vector,
multicast_words,
pack_pair,
published,
store_relaxed,
unpack_bf16,
zeros,
)
SENTINEL = -0x80000000
class TwoShot:
"""Symmetric buffers per layer parity: ``recv`` [TP, cap, W] (this rank's owned
columns, one slot per source rank), ``gather`` [1, cap, width] (multicast) and the
ranks' ``recv`` base addresses [parity, TP]."""
def __init__(self, group, device, tp, rank, width=7168, cap=64, parities=2):
self.tp, self.rank, self.width = tp, rank, width
shape = (parities, tp, cap, width // tp)
bf16 = dict(dtype=torch.bfloat16, device=device)
self.recv = symm_mem.empty(shape, **bf16)
self.gather = symm_mem.empty((parities, 1, cap, width), **bf16)
handles = [symm_mem.rendezvous(t, group) for t in (self.recv, self.gather)]
size = self.recv[0].numel() * 2
ptrs = handles[0].buffer_ptrs
bases = [[int(b) + p * size for b in ptrs] for p in range(parities)]
self.peers = torch.tensor(bases, dtype=torch.int64, device=device)
self.gather_mc = int(handles[1].multicast_ptr or 0)
if not self.gather_mc: # views() offsets this address
raise RuntimeError("Kimi K3 two-shot all-reduce needs multicast memory")
for t in (self.recv, self.gather):
t.view(torch.int32).fill_(SENTINEL)
torch.accelerator.synchronize(device)
torch.distributed.barrier(group=group)
def views(self, parity):
"""(recv, peers, gather, gather multicast address) of layer parity."""
gather_mc = self.gather_mc + parity * self.gather[0].numel() * 2
return self.recv[parity], self.peers[parity], self.gather[parity], gather_mc
@cute.jit
def push(recv, peers, slot, token, column, words):
"""Store this rank's BF16 chunk (token, column .. column + 7) in receive slot
``slot`` of its owner."""
width: cutlass.Constexpr = recv.shape[2]
owner = column // width
offset = cute.crd2idx((slot, token, column - owner * width), recv.layout)
store_relaxed(Int64(peers[owner]) + Int64(offset) * 2, words) # polled remotely
@cute.jit
def reduce(recv, gather, gather_mc, rank, token, column):
"""Owner of (token, column): the TP slots summed in FP32 in rank order, rounded
once (no negative zero, the sentinel), multicast to every gather row; slots
re-armed. Returns nothing; callers then poll ``gather`` like a one-slot mailbox."""
tp: cutlass.Constexpr = recv.shape[0]
local = column - rank * recv.shape[2]
raw = zeros((tp, 4), Uint32)
pending = Int32((1 << tp) - 1)
while pending != 0:
for slot in cutlass.range_constexpr(tp):
if (pending & (1 << slot)) != 0:
words = load_vector(at(recv, slot, token, local), cache="volatile")
for j in cutlass.range_constexpr(4):
raw[slot, j] = words[j]
if published(words):
pending &= ~(1 << slot)
values = cute.make_rmem_tensor(8, Float32)
for j in cutlass.range_constexpr(4):
low, high = unpack_bf16(raw[0, j])
for slot in cutlass.range_constexpr(1, tp):
a, b = unpack_bf16(raw[slot, j])
low, high = add_rn(low, a), add_rn(high, b)
values[2 * j], values[2 * j + 1] = low, high
words = tuple(
pack_pair(values[2 * j], values[2 * j + 1], policy="latent") for j in range(4)
)
offset = cute.crd2idx((0, token, column), gather.layout)
multicast_words(gather_mc + Int64(offset) * 2, words)
for slot in cutlass.range_constexpr(tp):
rearm(at(recv, slot, token, local))
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,106 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Split-K projection on the FlashInfer CuTe core vendored in ``splitk.py`` (with an
evict_first weight TMA; policies map padded rows to (M tile, N tile, split K,
stages); attention owns live slot masks)."""
from functools import cache
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream, make_fake_tensor
from ..common.host import architecture, tensor_maps, to_cute
from ..common.host import stream as current_stream
from .splitk import SplitKGemm, SplitKTactic
def matrix_view(tensor, *, output=False):
"""Keep packed row pitches and FlashInfer's singleton-safe dynamic major axis."""
rows, cols = tensor.shape
shape = (cols, rows, 1) if output else (rows, cols, 1)
stride = (1, tensor.stride(0), 0) if output else (tensor.stride(0), 1, 0)
result = to_cute(tensor.as_strided(shape, stride))
return result.mark_layout_dynamic(leading_dim=0 if output else 1)
@cache
def compile_projection(tactic, output_dtype, arch):
"""Code-only cache; weights, descriptors and activation pointers stay private."""
def fake(leading, dtype=cutlass.BFloat16):
shape = tuple(cute.sym_int() for _ in range(3))
stride = (1, cute.sym_int(64), 0) if leading == 0 else (cute.sym_int(64), 1, 0)
return make_fake_tensor(dtype, shape, stride, assumed_align=16)
y = fake(0, cutlass.Float32 if output_dtype == torch.float32 else cutlass.BFloat16)
split = SplitKTactic(*tactic)
kernel = SplitKGemm(tactic=split)
args = (fake(1), fake(1), y, y, cutlass.Int64(0), make_fake_stream())
return cute.experimental.compile(kernel, *args, options=f"--gpu-arch={arch}")
def weight_map(weight, rows):
"""Device TMA map of ``weight`` [N, K] (row pitch kept): box {64, rows}, SW128."""
types = cuda.CUtensorMapDataType
dtype = {
torch.bfloat16: types.CU_TENSOR_MAP_DATA_TYPE_BFLOAT16,
torch.float16: types.CU_TENSOR_MAP_DATA_TYPE_FLOAT16,
}[weight.dtype]
swizzle = cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_128B
l2 = cuda.CUtensorMapL2promotion.CU_TENSOR_MAP_L2_PROMOTION_L2_256B
n, k = weight.shape
pitch = weight.stride(0) * weight.element_size()
spec = (weight.data_ptr(), dtype, (k, n), (pitch,), (64, rows), swizzle, l2)
return tensor_maps([spec], weight.device)
class PreparedProjection:
"""Split-K projection with a per-batch tactic table and caller-owned outputs."""
def __init__(self, weight, policies, *, output_dtype=torch.bfloat16):
if weight.dim() != 2 or weight.dtype not in (torch.bfloat16, torch.float16):
raise ValueError(
f"projection weight must be a BF16/FP16 [N, K] matrix, got "
f"{tuple(weight.shape)} {weight.dtype}"
)
pitch = weight.stride(0) * weight.element_size()
if weight.stride(1) != 1 or pitch % 16 or weight.data_ptr() % 16:
raise ValueError(
"projection weight rows need unit stride and a 16 B TMA pitch"
)
if output_dtype not in (torch.bfloat16, torch.float32):
raise ValueError(f"unsupported projection output dtype {output_dtype}")
self.weight, self.device = weight, weight.device
self.output_dtype, self.policies = output_dtype, dict(policies)
self.weight_view = matrix_view(weight)
self.kernels, self.extra, self.maps = {}, {}, {}
def prepare(self):
arch, dtype = architecture(self.device), self.output_dtype
with torch.accelerator.device_index(self.device.index):
for batch, tactic in self.policies.items():
code = compile_projection(tactic, dtype, arch)
self.kernels[batch] = code
self.extra[batch] = self.weight_map(tactic[0])
return self
def weight_map(self, rows):
"""The kernel's extra argument: A's map for ``rows``-row tiles."""
if rows not in self.maps:
self.maps[rows] = weight_map(self.weight, rows)
return (cutlass.Int64(self.maps[rows].data_ptr()),)
def run(self, x, out):
y = matrix_view(out, output=True)
b, stream = x.shape[0], current_stream(self.device)
if b not in self.kernels or x.shape[1] != self.weight.shape[1]:
raise ValueError(
f"projection input {tuple(x.shape)}: prepared batches "
f"{sorted(self.kernels)}, K {self.weight.shape[1]}"
)
code, extra = self.kernels[b], self.extra[b]
code(self.weight_view, matrix_view(x), y, y, *extra, stream)
return out
@@ -0,0 +1,380 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Adapted from FlashInfer v0.7.0.post1 (Apache-2.0):
# flashinfer/gemm/kernels/dense_bf16_gemm_sm100_splitk.py
"""FlashInfer 0.7.0.post1 ``SplitKDenseGemmKernel`` (``dense_bf16_gemm_sm100_splitk``)
reduced to our use (PDL, no bias) with the weight (kernel-A) TMA issued through a
device tensor map with an L2 evict_first policy, and its synchronization fixed: the
owner waits for the peers' ``st.async`` partials at ``.acquire.cluster``; the MMA warp
waits for TMEM-pointer phase 0 before its release arrival (else its phase-1 wait can
pass before the epilogue has read TMEM); ``tcgen05.fence::*_thread_sync`` around
every mbarrier hand-off of tcgen05 work.
Each cluster rank accumulates an exact K slice in FP32; peers publish partials to
rank 0 through DSMEM, which reduces, casts and stores once. Kernel-M carries public N
and kernel-N public M."""
import cuda.bindings.driver as _cuda
import cutlass
import cutlass.cute as cute
import cutlass.utils as utils
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass import Int32
from cutlass._mlir.dialects import llvm
from cutlass.cute import experimental as cute_ext
from cutlass.cute.nvgpu import tcgen05
from cutlass.cutlass_dsl import dsl_user_op
from flashinfer.gemm.kernels.dense_bf16_gemm_sm100_splitk import (
OWNER_RANK,
SplitKTactic,
_store_shared_remote_v4,
)
CTA_K, ATOM_K = 128, 64 # K per tile; K per 128-byte swizzle atom (one A box)
__all__ = ["SplitKTactic", "SplitKGemm"]
def _asm(code, constraints, operands, loc, ip):
llvm.inline_asm(
None, operands, code, constraints, has_side_effects=True,
is_align_stack=False, asm_dialect=llvm.AsmDialect.AD_ATT, loc=loc, ip=ip,
) # fmt: skip
@dsl_user_op
def _acquire_tensor_map(tensor_map, *, loc=None, ip=None):
code = "fence.proxy.tensormap::generic.acquire.sys [$0], 128; "
_asm(code + "prefetch.tensormap [$0];", "l", [tensor_map.ir_value()], loc, ip)
@dsl_user_op
def _tma_load_evict_first(smem, mbar, tensor_map, k0, row0, *, loc=None, ip=None):
"""One {64, rows} box of A at (k0, row0), L2 evict_first."""
code = "{ .reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol, 1.0; "
code += "cp.async.bulk.tensor.2d.shared::cta.global.tile.mbarrier::complete_tx"
code += "::bytes.L2::cache_hint [$0], [$1, {$2, $3}], [$4], pol; }"
operands = [smem.toint().ir_value(), tensor_map.ir_value(), k0.ir_value()]
operands += [row0.ir_value(), mbar.toint().ir_value()]
_asm(code, "r,l,r,r,r", operands, loc, ip)
@dsl_user_op
def _wait_cluster(mbar, phase, *, loc=None, ip=None):
"""``cute.arch.mbarrier_wait`` at ``.acquire.cluster`` (remote ``st.async``)."""
code = "{ .reg .pred P1; LAB_WAIT: "
code += "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 P1, [$0], $1, "
code += "10000000; @P1 bra.uni DONE; bra.uni LAB_WAIT; DONE: }"
_asm(code, "r,r", [mbar.toint().ir_value(), phase.ir_value()], loc, ip)
@dsl_user_op
def _fence_tcgen05(*, before=False, loc=None, ip=None):
side = "before" if before else "after"
_asm(f"tcgen05.fence::{side}_thread_sync;", "", [], loc, ip)
def _smem(dtype, size, alignment, layout=None):
layout = cute.make_layout(size) if layout is None else layout
return cute_ext.allocate(dtype, cute.AddressSpace.smem, layout, alignment=alignment)
class SplitKGemm:
"""BF16 GEMM with a cluster-local split-K reduction; ``a_map`` is the device
address of A's tensor map (box {64, mma_m}, 128-byte swizzle)."""
def __init__(self, *, tactic: SplitKTactic):
self.acc_dtype = cutlass.Float32
self.cta_m, self.cta_n, self.cta_k = tactic.mma_m, tactic.mma_n, CTA_K
self.num_ab_stage, self.split_k = tactic.ab_stages, tactic.split_k
self.threads_per_cta, self.epilog_threads = 256, 128
self.mma_tiler_mn = (tactic.mma_m, tactic.mma_n)
self.cta_group = tcgen05.CtaGroup.ONE
self.cluster_shape = (1, tactic.split_k, 1)
values_per_thread = tactic.mma_m * tactic.mma_n // self.epilog_threads
assert values_per_thread % 4 == 0, "remote stores move 4 values"
self.mailbox_elements = (
(tactic.split_k - 1) * self.epilog_threads * values_per_thread
)
self.expected_transaction_bytes = self.mailbox_elements * 4
@cute.experimental.jit
def __call__(
self, a: cute.Tensor, b: cute.Tensor, c: cute.Tensor, bias: cute.Tensor,
a_map: cutlass.Int64, stream: _cuda.CUstream,
): # fmt: skip
# Grid-y packs output-N tile and cluster rank.
self.kernel(a, b, c, bias, a_map).launch(
grid=(
cute.ceil_div(c.layout.shape[0], self.cta_m),
cute.ceil_div(c.layout.shape[1], self.cta_n) * self.split_k,
c.layout.shape[2],
),
block=(self.threads_per_cta, 1, 1),
cluster=self.cluster_shape,
smem=cute.Int64(utils.get_smem_capacity_in_bytes("sm_100")),
stream=stream,
use_pdl=True,
)
@cute.experimental.kernel
def kernel(
self, mA: cute.Tensor, mB: cute.Tensor, mC: cute.Tensor, mBias: cute.Tensor,
a_map: cutlass.Int64,
): # fmt: skip
"""A (M, K, L) and B (N, K, L) K-major, C (M, N, L) M-major; bias unused."""
stages = self.num_ab_stage
ab_dtype = mA.element_type
majors = (utils.LayoutEnum.from_tensor(t).mma_major_mode() for t in (mA, mB))
tiled_mma = sm100_utils.make_trivial_tiled_mma(
ab_dtype, ab_dtype, *majors, self.acc_dtype, self.cta_group,
self.mma_tiler_mn,
) # fmt: skip
mnk_tiler = (self.mma_tiler_mn[0], self.mma_tiler_mn[1], self.cta_k)
block_idx = cute.arch.block_idx()
bidx = block_idx[0]
split_rank = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
n_idx = block_idx[1] // self.split_k
l_idx = block_idx[2]
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
shape = (tiled_mma, mnk_tiler, ab_dtype, stages)
sA = _smem(ab_dtype, None, 1024, sm100_utils.make_smem_layout_a(*shape))
sB = _smem(ab_dtype, None, 1024, sm100_utils.make_smem_layout_b(*shape))
acc_layout = cute_ext.make_tmem_layout_acc(
tiled_mma, self.mma_tiler_mn, acc_stage=1
)
bar_full = _smem(cutlass.Int64, stages, 8).iterator
bar_empty = _smem(cutlass.Int64, stages, 8).iterator
bar_tma_epilog = _smem(cutlass.Int64, 1, 8).iterator # FlashInfer's bias
bar_mma_epilog = _smem(cutlass.Int64, 1, 8).iterator
bar_tmem_alloc = _smem(cutlass.Int64, 1, 8).iterator
tmem_base_ptr = _smem(cutlass.Int32, 1, 4).iterator
mailbox, bar_reduce = sA, bar_mma_epilog # split 1: compile-time dead
if cutlass.const_expr(self.split_k > 1):
mailbox = _smem(cutlass.Float32, self.mailbox_elements, 128)
bar_reduce = _smem(cutlass.Int64, 1, 8).iterator
if warp_idx == 0:
with cute.arch.elect_one():
for i in range(stages):
cute.arch.mbarrier_init(bar_full + i, 2)
cute.arch.mbarrier_init(bar_empty + i, 1)
cute.arch.mbarrier_init(bar_tma_epilog, 32)
cute.arch.mbarrier_init(bar_mma_epilog, 1)
cute.arch.mbarrier_init(bar_tmem_alloc, 160)
if cutlass.const_expr(self.split_k > 1):
# Owner arrival plus peer transaction-byte credits.
cute.arch.mbarrier_init(bar_reduce, 1)
cute.arch.mbarrier_init_fence()
if cutlass.const_expr(self.split_k > 1):
cute.arch.cluster_arrive_relaxed() # peers' barriers before their stores
else:
cute.arch.barrier()
# K splits evenly over the cluster (tactics are validated for the shapes).
k_tile_count = cute.size(mA, mode=[1]) // self.cta_k // self.split_k
k_tile_start = split_rank * k_tile_count
if cutlass.const_expr(self.split_k > 1):
cute.arch.cluster_wait()
# Warp 3 is idle; warps 4-7 run the epilogue.
tiles = (k_tile_start, k_tile_count)
if warp_idx == 0:
self.weight_warp(bar_full, bar_empty, sA, a_map, bidx * self.cta_m, *tiles)
elif warp_idx == 1:
b_tile = cute.local_tile(mB, (self.cta_n, self.cta_k), (n_idx, None, l_idx))
b_map = cute_ext.get_cta_v_map_ab(mB, mnk_tiler, tiled_mma, "B")
self.activation_warp(bar_full, bar_empty, b_tile, sB, b_map, *tiles)
elif warp_idx == 2:
k_blocks = self.cta_k // cute.size(tiled_mma.shape_mnk, mode=[2])
self.mma_warp(
bar_full, bar_empty, bar_mma_epilog, bar_tmem_alloc, tiled_mma, sA, sB,
tmem_base_ptr, acc_layout, k_blocks, k_tile_count,
) # fmt: skip
elif warp_idx >= 4:
tile = cute.local_tile(mC, (self.cta_m, self.cta_n), (bidx, n_idx, l_idx))
self.epilog_warp(
bar_mma_epilog, bar_tmem_alloc, tmem_base_ptr, acc_layout, tile,
cute.arch.thread_idx()[0] - 128, mC.element_type,
utils.LayoutEnum.from_tensor(mC), mailbox, bar_reduce, split_rank,
) # fmt: skip
@cute.experimental.jit
def weight_warp(
self, bar_full, bar_empty, s_tile: cute.Tensor, a_map: cutlass.Int64,
row0: cutlass.Int32, k_tile_start: cutlass.Int32, k_tile_count: cutlass.Int32,
): # fmt: skip
"""A: two evict_first boxes per K tile, before the PDL wait (weights)."""
stages = self.num_ab_stage
stage_bytes = cute.size_in_bytes(
s_tile.element_type, cute.slice_(s_tile.layout, (None, None, None, 0))
)
_acquire_tensor_map(a_map)
empty_phase = cutlass.Int32(1)
for k_tile in cutlass.range(k_tile_count, unroll=1):
stage = k_tile % stages
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
with cute.arch.elect_one():
cute.arch.mbarrier_arrive_and_expect_tx(bar_full + stage, stage_bytes)
with cute.arch.elect_one():
k0 = (k_tile_start + k_tile) * self.cta_k
tile = s_tile[None, None, None, stage].iterator
for atom in cutlass.range_constexpr(self.cta_k // ATOM_K):
box, mbar = tile + atom * self.cta_m * ATOM_K, bar_full + stage
k = k0 + atom * ATOM_K
_tma_load_evict_first(box, mbar, a_map, k, row0)
if stage == stages - 1:
empty_phase = empty_phase ^ 1
cute.arch.griddepcontrol_launch_dependents()
self.drain(bar_empty, empty_phase, k_tile_count)
@cute.experimental.jit
def activation_warp(
self, bar_full, bar_empty, g_tile: cute.Tensor, s_tile: cute.Tensor,
cta_v_map: cute.Layout, k_tile_start: cutlass.Int32,
k_tile_count: cutlass.Int32,
): # fmt: skip
"""B (the activations): after the PDL wait."""
stages = self.num_ab_stage
stage_bytes = cute.size_in_bytes(
s_tile.element_type, cute.slice_(s_tile.layout, (None, None, None, 0))
)
cute.arch.griddepcontrol_wait()
empty_phase = cutlass.Int32(1)
for k_tile in cutlass.range(k_tile_count, unroll=1):
stage = k_tile % stages
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
with cute.arch.elect_one():
cute.arch.mbarrier_arrive_and_expect_tx(bar_full + stage, stage_bytes)
cute_ext.tma_load(
g_tile[None, None, k_tile_start + k_tile],
s_tile[None, None, None, stage],
(bar_full + stage).value,
cta_v_map=cta_v_map,
tma_operation_type=cute_ext.OperationTypeEnum.SM90_TMA_LOAD,
update_expect_tx=False,
)
if stage == stages - 1:
empty_phase = empty_phase ^ 1
self.drain(bar_empty, empty_phase, k_tile_count)
@cute.experimental.jit
def drain(self, bar_empty, empty_phase: cutlass.Int32, k_tile_count: cutlass.Int32):
stages = self.num_ab_stage
for tail in cutlass.range(stages, unroll=1):
stage = (tail + k_tile_count) % stages
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
if stage == stages - 1:
empty_phase = empty_phase ^ 1
@cute.experimental.jit
def mma_warp(
self, bar_full, bar_empty, bar_mma_epilog, bar_tmem_alloc,
tiled_mma: cute.TiledMma, sA: cute.Tensor, sB: cute.Tensor, tmem_base_ptr,
acc_layout: cutlass.Constexpr, mma_inst_tile_k: cutlass.Constexpr,
k_tile_count: cutlass.Int32,
): # fmt: skip
num_tmem_cols = 256
cute.arch.alloc_tmem(num_tmem_cols, tmem_base_ptr, is_two_cta=False)
_fence_tcgen05(before=True)
cute.arch.mbarrier_arrive(bar_tmem_alloc)
cute.arch.relinquish_tmem_alloc_permit(is_two_cta=False)
tmem_ptr = cute.arch.retrieve_tmem_ptr(self.acc_dtype, 16, tmem_base_ptr)
accumulator = cute.make_tensor(tmem_ptr, acc_layout)[None, None, None, 0]
mma_atom = cute.make_mma_atom(tiled_mma.op)
full_phase = cutlass.Int32(0)
for k_tile in cutlass.range(k_tile_count, unroll=1):
stage = k_tile % self.num_ab_stage
cute.arch.mbarrier_wait(bar_full + stage, full_phase)
_fence_tcgen05()
for k_block in range(mma_inst_tile_k):
accumulate = k_tile != 0 if k_block == 0 else True
mma_atom.set(tcgen05.Field.ACCUMULATE, accumulate)
cute_ext.dot(
mma_atom,
cute.append_ones(sA[None, None, k_block, stage], up_to_rank=3),
cute.append_ones(sB[None, None, k_block, stage], up_to_rank=3),
accumulator,
)
with cute.arch.elect_one():
tcgen05.commit(bar_empty + stage, None, self.cta_group)
if stage == self.num_ab_stage - 1:
full_phase = full_phase ^ 1
with cute.arch.elect_one():
tcgen05.commit(bar_mma_epilog, None, self.cta_group)
cute.arch.mbarrier_wait(bar_tmem_alloc, 0) # the release lands in phase 1
cute.arch.mbarrier_arrive(bar_tmem_alloc)
cute.arch.mbarrier_wait(bar_tmem_alloc, 1)
_fence_tcgen05()
cute.arch.dealloc_tmem(tmem_ptr, num_tmem_cols, is_two_cta=False)
@cute.experimental.jit
def epilog_warp(
self, bar_mma_epilog, bar_tmem_alloc, tmem_base_ptr,
acc_layout: cutlass.Constexpr, gD_tile: cute.Tensor, epi_tid: cutlass.Int32,
c_dtype: cutlass.Constexpr, d_layout: cutlass.Constexpr, mailbox, bar_reduce,
split_rank: cutlass.Int32,
): # fmt: skip
# Wait until MMA publishes the TMEM base pointer.
cute.arch.mbarrier_arrive(bar_tmem_alloc)
cute.arch.mbarrier_wait(bar_tmem_alloc, 0)
_fence_tcgen05()
acc_view = cute.make_tensor(
cute.arch.retrieve_tmem_ptr(self.acc_dtype, 16, tmem_base_ptr),
acc_layout,
)[((None, None), 0, 0, 0)]
epi_tile = (self.cta_m, self.cta_n)
load_op = sm100_utils.get_tmem_load_op(
(self.cta_m, self.cta_n, self.cta_k), d_layout, c_dtype, self.acc_dtype,
epi_tile, False,
) # fmt: skip
tiled_copy_t2r = cute.nvgpu.tcgen05.make_tmem_copy(load_op, acc_view)
gD_epi = cute.flat_divide(gD_tile, epi_tile)
# Match each epilogue thread's TMEM partition in RMEM.
rmem_layout = cute_ext.make_t2r_rmem_layout(tiled_copy_t2r, gD_epi, epi_tid)
rmem = cute.AddressSpace.rmem
rAcc = cute_ext.allocate(self.acc_dtype, rmem, rmem_layout, alignment=32)
rD = cute_ext.allocate(c_dtype, rmem, rmem_layout, alignment=32)
thr_t2r = tiled_copy_t2r.get_slice(epi_tid)
cute.arch.mbarrier_wait(bar_mma_epilog, 0)
_fence_tcgen05()
cute_ext.partition_and_copy(thr_t2r, acc_view, rAcc)
# Make tcgen05.ld visible before TMEM release and RMEM use.
cute.arch.fence_view_async_tmem_load()
_fence_tcgen05(before=True)
cute.arch.mbarrier_arrive(bar_tmem_alloc)
# Peers publish FP32 partials; only rank 0 reduces and stores.
if cutlass.const_expr(self.split_k > 1):
vpt = cutlass.const_expr(cute.size(rmem_layout)) # values per thread
vpp = cutlass.const_expr(self.epilog_threads * vpt) # values per peer
if split_rank != OWNER_RANK:
for i in cutlass.range_constexpr(0, vpt, 4):
words = (rAcc[i], rAcc[i + 1], rAcc[i + 2], rAcc[i + 3])
slot = mailbox.iterator + (split_rank - Int32(1)) * vpp
slot = slot + epi_tid * vpt + i
_store_shared_remote_v4(*words, slot, bar_reduce, Int32(OWNER_RANK))
else:
if epi_tid == 0:
expected = self.expected_transaction_bytes
cute.arch.mbarrier_arrive_and_expect_tx(bar_reduce, expected)
_wait_cluster(bar_reduce, Int32(0))
for peer in cutlass.range_constexpr(self.split_k - 1):
for i in cutlass.range_constexpr(vpt):
rAcc[i] = rAcc[i] + mailbox[peer * vpp + epi_tid * vpt + i]
if split_rank == OWNER_RANK:
rD.store(rAcc.load().to(c_dtype))
# Preserve TMEM coordinates; the copy predicates output tails.
cute_ext.partition_and_copy(thr_t2r, rD, gD_epi[None, None, 0, 0])
@@ -0,0 +1,293 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Weight-stationary skinny GEMM for the MoE front: y[M, N] = x[M, K] @ W[N, K]^T.
Each CTA owns ``rt`` 16-row N tiles; its ``rt * kq`` warps split K into ``kq`` ranges.
Weights enter registers and cp.async SMEM stages before the PDL wait. The epilogue
publishes the first ``valid`` of ``split`` rows (router shard) as FP32 words at BF16
column 3584 + 2 * (``slot`` + row) of a multicast [1, capacity, width] mailbox and the
rest (latent down shard) as sanitized BF16 at column ``base``.
``tg`` 0 is the single pass: warps load x from global and the cluster leader sums the
K partials over DSMEM. ``tg`` > 0 streams tokens in groups of ``tg``: warp 0 bulk-copies
the CTA's K slice of x once (one mbarrier per group) for all row tiles, and after each
group's MMAs every warp pushes its 8-token x 16-row partial blocks to their owner CTAs
(block % ks) with st.async; owners sum the ks * kq parts in K order and publish one
group behind, so only the last group's blocks follow the MMA loop.
"""
from typing import Any
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, Int64, Uint32, Uint64
from ..common.primitives import (
alloc,
asm,
at,
bf16x2,
bits_float,
copy_g2s,
float_bits,
launch,
load_words,
mma_into,
multicast_words,
push_float,
smem_words,
wait,
)
@asm(Uint32, pure=True)
def positive_zero(low, high):
"""BF16 (low, high) with -0.0 halves made +0.0, so it never equals the sentinel."""
code = "{ .reg .b32 t; .reg .pred p,q; and.b32 t,$1,0xFFFF; "
code += "setp.eq.u32 p,t,0x8000; shr.u32 t,$1,16; setp.eq.u32 q,t,0x8000; "
code += "mov.b32 $0,$1; @p and.b32 $0,$0,0xFFFF0000; @q and.b32 $0,$0,0x0000FFFF; }"
return code, "=r,r", (Uint32(bf16x2(low, high)),)
@asm(Float32)
def peer_load(pointer, peer):
"""FP32 at the same shared-memory offset in cluster CTA ``peer``."""
code = "{ .reg .b32 a; mapa.shared::cluster.u32 a,$1,$2; "
return code + "ld.shared::cluster.f32 $0,[a]; }", "=f,r,r", (pointer, Int32(peer))
@asm(Uint32, pure=True)
def positive_zero32(value):
"""FP32 bits of ``value`` with -0.0 made +0.0, so it never equals the sentinel."""
code = "{ .reg .pred p; setp.eq.u32 p,$1,0x80000000; selp.b32 $0,0,$1,p; }"
return code, "=r,r", (Uint32(float_bits(value)),)
class WsGemm:
def __init__(
self, n, k, m, rt, kq, staged, split, valid, ks=1, tg=0, ch=0
): # fmt: skip
self.n, self.k, self.m, self.rt, self.kq = n, k, m, rt, kq
# ks: a cluster of ks CTAs splits K, so each CTA preloads 1/ks of the tile's
# weights; ch: accumulator chains per 8-token tile (0: 4 / tiles per group).
self.split, self.valid, self.ks = split, valid, ks
self.staged, self.kb = staged, k // (32 * kq * ks)
self.rb, self.nt = self.kb - staged, (m + 7) // 8
self.grouped, self.ng = tg > 0, min(self.nt, tg // 8) if tg else self.nt
if self.grouped and ks <= 1: # token groups push partials to K-split peers
raise ValueError(f"WsGemm token groups (tg={tg}) need a K-split cluster")
self.groups, self.chains = -(-self.nt // self.ng), ch or max(1, 4 // self.ng)
self.grid, self.threads = n // (16 * rt) * ks, 32 * rt * kq
self.blocks, self.parts = self.nt * rt, ks * kq
self.slots = -(-self.blocks // ks) # owned blocks s * ks + kpart
# Bulk x rows: the CTA's K slice plus 64 B, so token pairs hit disjoint banks.
self.slice = k // ks
self.pitch = self.slice // 2 + 16
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, self.grid, self.threads, self.ks, min_blocks_per_mp=1)
@cute.kernel
def kernel(self, args):
x, w = args[0:2]
smem = cutlass.utils.SmemAllocator()
block, _, _ = cute.arch.block_idx()
cta, kpart = block // self.ks, block % self.ks
tid, _, _ = cute.arch.thread_idx()
lane, warp = tid % 32, tid // 32
group, part = lane // 4, lane % 4
tile, quarter = warp // self.kq, warp % self.kq
row = (cta * self.rt + tile) * 16 + group
kofs = (kpart * self.kq + quarter) * self.kb * 32 + part * 8
shape = (self.nt, self.rt, self.kq, 8, 17)
red = smem.allocate_tensor(Float32, cute.make_layout(shape), 16)
stage = alloc(smem, Uint32, (self.rt * self.kq, max(self.staged, 1), 2, 32, 4))
xs = recv = None
bars: Any = None
owned: Any = None
if cutlass.const_expr(self.grouped):
xs = alloc(smem, Uint32, (self.nt * 8, self.pitch))
bars = alloc(smem, Uint64, (self.groups,), 8)
recv = alloc(smem, Uint32, (self.slots, self.parts, 32, 4))
owned = alloc(smem, Uint64, (self.slots,), 8)
if tid == 0:
for g in cutlass.range_constexpr(self.groups):
cute.arch.mbarrier_init(bars.iterator + g, 1)
for s in cutlass.range_constexpr(self.slots):
if s * self.ks + kpart < self.blocks:
cute.arch.mbarrier_init(owned.iterator + s, 1)
nbytes = self.parts * 512
cute.arch.mbarrier_arrive_and_expect_tx(
owned.iterator + s, nbytes
)
cute.arch.mbarrier_init_fence()
cute.arch.sync_threads()
cute.arch.cluster_arrive_relaxed()
wp = w.iterator + row * self.k + kofs
for j in cutlass.range_constexpr(self.staged):
for h in cutlass.range_constexpr(2):
dst = at(stage, warp, j, h, lane, 0)
copy_g2s(dst, wp + h * 8 * self.k + (self.rb + j) * 32)
cute.arch.cp_async_commit_group()
wr = {}
for j in cutlass.range_constexpr(self.rb):
for h in cutlass.range_constexpr(2):
src = wp + h * 8 * self.k + j * 32
wr[j, h] = load_words(src, count=4, evict_first=True)
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
if cutlass.const_expr(self.grouped):
if warp == 0:
self.stage_x(x, xs, bars, kpart, lane)
cute.arch.cp_async_wait_group(0)
else:
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
for g in cutlass.range_constexpr(self.groups):
acc = cute.make_rmem_tensor((self.ng, self.chains, 4), Float32)
acc.fill(0)
if cutlass.const_expr(self.grouped):
wait(bars.iterator + g)
for j in cutlass.range_constexpr(self.kb):
if cutlass.const_expr(j < self.rb):
wa, wb = wr[j, 0], wr[j, 1]
else:
sa = at(stage, warp, j - self.rb, 0, lane, 0)
wa, wb = smem_words(sa), smem_words(sa + 128)
for t in cutlass.range_constexpr(self.tiles(g)):
token = (g * self.ng + t) * 8 + group
xv = (Uint32(0), Uint32(0), Uint32(0), Uint32(0))
if cutlass.const_expr(self.grouped):
col = (quarter * self.kb + j) * 16 + part * 4
xv = smem_words(at(xs, token, col))
else:
xg = x.iterator + token * x.stride[0] + kofs + j * 32
if (g * self.ng + t) * 8 + 7 < self.m or token < self.m:
xv = load_words(xg, count=4, mutable=True)
for h in cutlass.range_constexpr(2):
c = (j * 2 + h) % self.chains
a = (wa[2 * h], wb[2 * h], wa[2 * h + 1], wb[2 * h + 1])
b = (xv[2 * h], xv[2 * h + 1])
mma_into(acc[t, c, None], a, b, bf16=True)
if cutlass.const_expr(self.grouped and g == 0):
cute.arch.cluster_wait() # owners' barriers are armed
for t in cutlass.range_constexpr(self.tiles(g)):
values = []
for v in cutlass.range_constexpr(4):
value = acc[t, 0, v]
for c in cutlass.range_constexpr(1, self.chains):
value = value + acc[t, c, v]
tt, col = g * self.ng + t, group + (v // 2) * 8
if cutlass.const_expr(self.grouped):
values.append(value)
else:
red[tt, tile, quarter, 2 * part + v % 2, col] = value
if cutlass.const_expr(self.grouped):
blk = (g * self.ng + t) * self.rt + tile
s, peer = blk // self.ks, blk % self.ks
dst = at(recv, s, kpart * self.kq + quarter, lane, 0)
push_float(dst, tuple(values), owned.iterator + s, peer)
if cutlass.const_expr(self.grouped and g > 0):
self.settle(args, red, recv, owned, cta, kpart, warp, lane, g - 1)
if cutlass.const_expr(self.grouped):
self.settle(args, red, recv, owned, cta, kpart, warp, lane, self.groups - 1)
else:
cute.arch.sync_threads()
if cutlass.const_expr(self.ks > 1):
cute.arch.cluster_arrive()
cute.arch.cluster_wait()
self.gather(args, red, cta, kpart, tid, lane)
if cutlass.const_expr(self.ks > 1):
cute.arch.cluster_arrive()
cute.arch.cluster_wait()
def tiles(self, g):
"""8-token tiles in token group ``g``."""
return min(self.ng, self.nt - g * self.ng)
@cute.jit
def stage_x(self, x, xs, bars, kpart, lane):
"""Warp 0 bulk-copies every token's K slice; one barrier per token group."""
if lane == 0:
for g in cutlass.range_constexpr(self.groups):
tokens = min(self.m, (g + 1) * self.ng * 8) - g * self.ng * 8
nbytes = tokens * self.slice * 2
cute.arch.mbarrier_arrive_and_expect_tx(bars.iterator + g, nbytes)
cute.arch.sync_warp()
for i in cutlass.range_constexpr(-(-self.m // 32)):
token = i * 32 + lane
if i * 32 + 31 < self.m or token < self.m:
src = x.iterator + token * x.stride[0] + kpart * self.slice
bar = bars.iterator + token // (8 * self.ng)
copy_g2s(at(xs, token, 0), src, self.slice * 2, bar)
@cute.jit
def settle(
self, args, red, recv, owned, cta, kpart, warp, lane, g: cutlass.Constexpr
):
"""Owned blocks of token group ``g``: a warp per block waits for every K part,
sums them in K order, transposes through ``red`` and publishes."""
for t in cutlass.range_constexpr(self.tiles(g)):
for tile in cutlass.range_constexpr(self.rt):
blk = (g * self.ng + t) * self.rt + tile
s = blk // self.ks
if (kpart == blk % self.ks) & (warp == s % (self.rt * self.kq)):
wait(owned.iterator + s)
total = cute.make_rmem_tensor(4, Float32)
for p in cutlass.range_constexpr(self.parts):
words = smem_words(at(recv, s, p, lane, 0))
for v in cutlass.range_constexpr(4):
value = bits_float(words[v])
total[v] = value if p == 0 else total[v] + value
tt = g * self.ng + t
for v in cutlass.range_constexpr(4):
col = lane // 4 + v // 2 * 8
red[tt, tile, 0, 2 * (lane % 4) + v % 2, col] = total[v]
cute.arch.sync_warp()
token = tt * 8 + lane // 2
if (lane < 16) & (token < self.m):
half = lane % 2 * 8
values = [
red[tt, tile, 0, lane // 2, half + j] for j in range(8)
]
col = (cta * self.rt + tile) * 16 + half
self.epilogue(args, token, col, values)
@cute.jit
def gather(self, args, red, cta, kpart, tid, lane):
"""Each thread sums one column's K parts in order (the leader also its peers');
each eight-lane group then gathers the sums for its first lane's 16 B store."""
total = self.m * self.rt * 16
for start in range(0, total, self.threads):
item = start + tid
v8, half = item % 8, (item // 8) % 2
rtile, token = (item // 16) % self.rt, item // (16 * self.rt)
mine = Float32(0)
if item < total:
mine = red[token // 8, rtile, 0, token % 8, half * 8 + v8]
for q in cutlass.range_constexpr(1, self.kq):
mine = mine + red[token // 8, rtile, q, token % 8, half * 8 + v8]
if kpart == 0:
for peer in cutlass.range_constexpr(1, self.ks):
for q in cutlass.range_constexpr(self.kq):
idx = (token // 8, rtile, q, token % 8, half * 8 + v8)
mine = mine + peer_load(at(red, *idx), peer)
values = [cute.arch.shuffle_sync(mine, (lane & 24) + v) for v in range(8)]
if (item < total) & (v8 == 0) & (kpart == 0):
col = (cta * self.rt + rtile) * 16 + half * 8
self.epilogue(args, token, col, values)
@cute.jit
def epilogue(self, args, token, col, values):
out, mc, slot, base = args[2:6]
if col < self.valid:
for v in cutlass.range_constexpr(2):
words = tuple(positive_zero32(values[4 * v + i]) for i in range(4))
offset = token * out.shape[2] + 3584 + 2 * (slot + col + 4 * v)
multicast_words(mc + Int64(offset) * 2, words, kind="bf16x2")
elif col >= self.split:
column = base + col - self.split
words = tuple(positive_zero(*values[i : i + 2]) for i in range(0, 8, 2))
offset = token * out.shape[2] + column
multicast_words(mc + Int64(offset) * 2, words, kind="bf16x2")
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,715 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Cluster-split KDA recurrence; an optional fifth warp projects F_B and multicasts the
decays while the main warps run the convolution and query/key normalization."""
from functools import partial
import cutlass
import cutlass.cute as cute
from cutlass import BFloat16, Float32, Int32, Int64, Uint32
from cutlass.cute.arch import store_async_dsmem
from ..common.primitives import (
alloc,
asm,
at,
copy_g2s,
dot,
float_bits,
fma,
launch,
load_f32x4,
load_words,
mma_into,
push_float,
sigmoid,
smem_words,
store_f32x4,
unpack_bf16,
wait,
warp_sum,
zeros,
)
load_bf16x4 = partial(load_words, count=2, cache_policy="nc", word_type="u32")
load_weight_bf16x8 = partial(load_words, count=4, cache_policy="nc", word_type="u32")
load_activation_bf16x4 = partial(load_words, count=2, word_type="u32", mutable=True)
load_activation_bf16x8 = partial(load_words, count=4, word_type="u32", mutable=True)
load_staged = partial(
load_words, count=4, shared=True, mutable=True, result_type=Float32, word_type="f32"
)
@asm((Float32, Int32), pure=True)
def reciprocal_guess(denominator):
"""``sigmoid_reciprocal``'s refined fast path (IEEE mode) and whether it holds."""
code = "{ .reg .f32 r,error,neg_error; .reg .b32 bits; .reg .pred regular; "
code += "mov.b32 bits,$2; add.u32 bits,bits,0x01800000; "
code += "and.b32 bits,bits,0x7f800000; setp.gt.u32 regular,bits,0x01ffffff; "
code += "rcp.approx.ftz.f32 r,$2; fma.rn.f32 error,$2,r,0fbf800000; "
code += "neg.f32 neg_error,error; fma.rn.f32 $0,r,neg_error,r; "
return code + "selp.u32 $1,1,0,regular; }", "=f,=r,f", (denominator,)
@asm(Float32, pure=True)
def reciprocal_rn(value):
return "div.rn.f32 $0,0f3f800000,$1;", "=f,f", (value,)
@cute.jit
def sigmoids(values):
"""``sigmoid(v, False)`` of every value, bit for bit, with all fast paths ahead
of the division fallbacks' branches (verify tokens get ILP)."""
count: cutlass.Constexpr = len(values)
den = [Float32(1) + cute.math.exp(-Float32(v), fastmath=True) for v in values]
result, regular = zeros(count, Float32), zeros(count, Int32)
irregular = Int32(0)
for i in cutlass.range_constexpr(count):
result[i], regular[i] = reciprocal_guess(den[i])
irregular |= regular[i] ^ 1
if irregular != 0: # rare: one branch for the whole batch
for i in cutlass.range_constexpr(count):
if regular[i] == 0:
result[i] = reciprocal_rn(den[i])
return result
def advance(base, n):
"""``base + n``, or ``base`` itself for a static zero ``n`` (keeps q = 1 code)."""
return base + n if n else base
@asm()
def store_evict_f32x4(pointer, values):
code = "{ .reg .b64 pol; createpolicy.fractional.L2::evict_first.b64 pol,1.0; "
code += "st.global.L2::cache_hint.v4.f32 [$0],{$1,$2,$3,$4},pol; }"
return code, "l,f,f,f,f", (pointer, *values)
class KdaSplit:
"""128 main threads and C value partitions. Conv state [slot, 3 * heads * 128, 3]
and recurrent state [slot, head, value, key] keep their strides; slots <= 0 are
padding (never written) and active rows own distinct slots. Weights must be ready
at launch; each run waits for prior cache restoration and slot publication.
``schedule`` = (early, lanes per state row, minimum CTAs per SM, CTAs per head);
zeros keep the batch defaults. Two CTAs per head give each CTA 64 state rows and
each gate lane two decays. Early issues every load the predecessor does not
produce (slot, state and conv rows, conv / F_B / norm weights) before the PDL
wait, converts operands at first use, and syncs the cluster only before each
role's first DSMEM send. Safe because those loads read only weights and data a
graph or non-PDL boundary orders before this launch (states and conv rows of this
layer's previous call or a fresh prompt's non-PDL fill, the host's slot table),
never same-step producer output, which is read after the wait; PDL-chained calls
without such a boundary must not share state slots. Early 2 also bulk-copies
the CTA's (contiguous) state rows to shared memory and updates four passes at a
time, which frees the registers that held them across the input phase; early 3
(two CTAs per head) also stages the gate warp's F_B rows.
``tokens`` q > 1 (speculative verify; ``batch`` counts requests, rows b * q + t)
runs each request's q tokens in order with vLLM's spec cache semantics: slots
[B, >= q] and accepted [B] (an extra last tensor, read early like the slots); the
state comes from slot[b, accepted - 1] and token t's state goes to slot[b, t] if
> 0; conv slot[b, 0] holds [dim, q + 2] with taps at accepted - 1 ..., rewritten to
[c_accepted, c_accepted + 1, x_0, ..., x_q-1]. Token t's F_B row is MMA column t;
CTA t % C normalizes token t."""
def __init__(self, heads, batch, gate_mode, has_bias, schedule=(0, 0, 0), tokens=1):
# The gate stage sends decays for tokens 2 * quarter + column (at most 8), while
# the decay barrier expects 512 bytes per token: more tokens would never arrive.
if not 1 <= tokens <= 8:
raise ValueError(f"KdaSplit supports 1-8 tokens per request; got {tokens}")
self.heads, self.gate_mode, self.has_bias = heads, gate_mode, has_bias
early, lanes, self.occupancy, cluster, evict = (*schedule, 0, 0)[:5]
self.early, self.staged = bool(early), early >= 2
self.gate_staged, self.tokens = early == 3, tokens
assert gate_mode != 2 or not (early or cluster)
assert tokens == 1 or not (self.staged or gate_mode == 2 or cluster == 16)
# TP8 (12 heads) uses fast math; B1 releases dependents at entry.
self.fast_math, self.trigger_at_entry = heads == 12, batch == 1
self.cluster = cluster or (8 if batch == 1 else 4)
self.lanes_per_row = lanes or (8 if batch <= (4 if heads == 6 else 2) else 16)
self.rows, self.groups = 128 // self.cluster, 128 // self.lanes_per_row
self.passes = (self.rows + self.groups - 1) // self.groups
self.wave = min(4, self.passes) if self.staged else self.passes
self.chunks = 32 // self.lanes_per_row
assert not self.gate_staged or self.rows >= 64
self.evict = bool(evict) # staged state traffic marked L2 evict-first
assert self.staged or not self.evict
@cute.jit
def __call__(self, *args):
grid = (self.cluster, self.heads, args[0].shape[0] // self.tokens)
bound = dict(min_blocks_per_mp=self.occupancy) if self.occupancy else {}
launch(self.kernel, args, grid, 160, self.cluster, **bound)
@cute.kernel
def kernel(self, args):
x, weights, bias, conv_state, raw_gate, beta, a_log, dt_bias, slots = args[:9]
state, output, gate, norm_weight, lower_bound, scale, eps, f_b_weight = args[
9:17
]
T: cutlass.Constexpr = self.tokens
if cutlass.const_expr(not self.early):
cute.arch.griddepcontrol_wait()
thread, _, _ = cute.arch.thread_idx()
partition, head, token = cute.arch.block_idx()
if cutlass.const_expr(T == 1):
slot = slots[token]
conv_slot, offset, writes = slot, 0, [slot]
else:
request, token = token, token * T
offset = args[17][request] - 1
slot, conv_slot = slots[request, offset], slots[request, 0]
writes = [slots[request, t] for t in range(T)]
if slot <= 0:
cute.arch.griddepcontrol_wait()
if (partition == 0) & (thread < 128):
for t in cutlass.range_constexpr(T):
output[advance(token, t), head * 128 + thread] = BFloat16(0)
cute.arch.griddepcontrol_launch_dependents()
else:
lane, warp = thread % 32, thread // 32
group = thread // self.lanes_per_row
group_lane = thread % self.lanes_per_row
dim: cutlass.Constexpr = self.heads * 128
value_base, qk_channel = partition * self.rows, head * 128 + thread
value_channel = head * 128 + value_base + thread
smem = cutlass.utils.SmemAllocator()
shared_q, shared_k, shared_decay, shared_v, shared_o, reductions = (
alloc(smem, Float32, size * T)
for size in (128, 128, 128, self.rows, 128, 8)
)
barrier, gate_barrier = (smem.allocate(cutlass.Uint64, 8) for _ in range(2))
owned: cutlass.Constexpr = -(-T // self.cluster) # tokens a CTA normalizes
if cutlass.const_expr(owned > 1): # one output barrier per owned token
barrier = smem.allocate(8 * owned, 8)
barrier = cute.recast_ptr(barrier, dtype=cutlass.Uint64)
if cutlass.const_expr(self.staged):
staged = alloc(smem, Float32, (self.rows, 128), align=128)
state_barrier = smem.allocate(cutlass.Uint64, 8)
gate_rows = None
if cutlass.const_expr(self.gate_staged): # [copy, lane] 16-byte slots
gate_rows = alloc(smem, Uint32, (self.rows // 2, 32, 4), align=128)
if cutlass.const_expr(self.trigger_at_entry):
cute.arch.griddepcontrol_launch_dependents()
if cutlass.const_expr(not self.early):
self.setup(barrier, gate_barrier, partition, thread)
cute.arch.cluster_wait()
if thread >= 128:
if cutlass.const_expr(self.early):
cute.arch.cluster_arrive_relaxed()
coords = (head, token, value_base, lane)
self.gate_warp(args, shared_decay, gate_barrier, coords, gate_rows)
if thread < 128:
if cutlass.const_expr(self.early): # thread 0 arms; barrier 1 orders it
self.setup(barrier, gate_barrier, partition, thread)
# 64-bit page bases (slot x page stride passes Int32), 32-bit offsets.
page = lambda s: state[Int64(s), None, None, None]
state_page = page(slot)
conv_page = conv_state[Int64(conv_slot), None, None]
state_row = lambda r, c, s=state_page: at(s, head, value_base + r, c)
history = zeros((self.passes, self.chunks, 4), Float32)
if cutlass.const_expr(self.staged): # noqa: SIM102
if thread == 0:
cute.arch.mbarrier_init(state_barrier, 1)
cute.arch.mbarrier_init_fence()
size = self.rows * 512
cute.arch.mbarrier_arrive_and_expect_tx(state_barrier, size)
src, policy = state_row(0, 0), dict(evict_first=self.evict)
copy_g2s(staged.iterator, src, size, state_barrier, **policy)
for p in cutlass.range_constexpr(0 if self.staged else self.passes):
row = p * self.groups + group
if row < self.rows:
for j in cutlass.range_constexpr(self.chunks):
column = 4 * (group_lane + self.lanes_per_row * j)
values = load_f32x4(state_row(row, column))
for e in cutlass.range_constexpr(4):
history[p, j, e] = values[e]
taps = zeros((3, 3), BFloat16)
coeff = zeros((3, 4), weights.element_type if self.early else Float32)
accum = zeros(3 * T, Float32)
for s in cutlass.range_constexpr(3):
channel = value_channel if s == 2 else qk_channel
if (s != 2) | (thread < self.rows):
for t in cutlass.range_constexpr(3):
taps[s, t] = conv_page[s * dim + channel, offset + t]
for t in cutlass.range_constexpr(4):
coeff[s, t] = weights[s, t, channel]
if cutlass.const_expr(self.has_bias):
accum[s] = bias[s * dim + channel]
for t in cutlass.range_constexpr(1, T):
accum[3 * t + s] = accum[s]
nw = norm_weight[thread]
cute.arch.griddepcontrol_wait()
inputs = cute.make_rmem_tensor(3 * T, BFloat16)
betas, gates = [], []
for t in cutlass.range_constexpr(T):
row = advance(token, t)
inputs[3 * t], inputs[3 * t + 1] = (
x[row, qk_channel],
x[row, dim + qk_channel],
)
inputs[3 * t + 2] = BFloat16(0)
if cutlass.const_expr(t == T - 1): # noqa: SIM102 (all tokens)
if thread < self.rows:
for u in cutlass.range_constexpr(T):
channel = 2 * dim + value_channel
inputs[3 * u + 2] = x[advance(token, u), channel]
if cutlass.const_expr(self.early): # converted at first use
betas.append(beta[row, head])
gates.append(gate[row, qk_channel])
else:
betas.append(Float32(beta[row, head]))
gates.append(Float32(gate[row, qk_channel]))
cute.arch.griddepcontrol_launch_dependents()
vs, squares, qks = [], [], []
for t in cutlass.range_constexpr(T): # window: 3 taps, then the inputs
for s in cutlass.range_constexpr(3):
for i in cutlass.range_constexpr(4):
w = t + i
term = taps[s, w] if w < 3 else inputs[3 * (w - 3) + s]
term = Float32(term)
cf = Float32(coeff[s, i]) if self.early else coeff[s, i]
acc = accum[3 * t + s]
accum[3 * t + s] = fma(term, cf, acc, ftz=self.fast_math)
act = lambda i, value: sigmoid(value, self.fast_math)
if cutlass.const_expr(T > 1 and not self.fast_math):
raw = [accum[i] for i in range(3 * T)] + betas + gates
act = partial(lambda batch, i, value: batch[i], sigmoids(raw))
for t in cutlass.range_constexpr(T):
q = accum[3 * t] * act(3 * t, accum[3 * t])
k = accum[3 * t + 1] * act(3 * t + 1, accum[3 * t + 1])
v = accum[3 * t + 2] * act(3 * t + 2, accum[3 * t + 2])
betas[t] = act(3 * T + t, Float32(betas[t]))
gates[t] = act(4 * T + t, Float32(gates[t]))
shared_q[advance(thread, 128 * t)] = q
shared_k[advance(thread, 128 * t)] = k
vs.append(v)
qks += [q, k]
if cutlass.const_expr(t == T - 1): # noqa: SIM102
if thread < self.rows:
for u in cutlass.range_constexpr(T):
shared_v[advance(thread, self.rows * u)] = vs[u]
squares += [warp_sum(q * q), warp_sum(k * k)]
if cutlass.const_expr(t == T - 1): # noqa: SIM102
if lane == 0:
for u in cutlass.range_constexpr(T):
reductions[advance(warp, 8 * u)] = squares[2 * u]
reductions[advance(warp + 4, 8 * u)] = squares[
2 * u + 1
]
cute.arch.barrier(barrier_id=1, number_of_threads=128)
def factors(t):
r = lambda i: reductions[8 * t + i]
qn = (r(0) + r(2)) + (r(1) + r(3))
kn = (r(4) + r(6)) + (r(5) + r(7))
fq = cute.math.rsqrt(qn + 1.0e-6, fastmath=True) * scale
return fq, cute.math.rsqrt(kn + 1.0e-6, fastmath=True)
if cutlass.const_expr(T > 1): # normalize each token's q, k once
for t in cutlass.range_constexpr(T):
fq, fk = factors(t)
shared_q[128 * t + thread] = qks[2 * t] * fq
shared_k[128 * t + thread] = qks[2 * t + 1] * fk
cute.arch.barrier(barrier_id=1, number_of_threads=128)
def operands(t):
fq, fk = factors(t) if T == 1 else (None, None)
qr = cute.make_rmem_tensor((self.chunks, 4), Float32)
kr = cute.make_rmem_tensor((self.chunks, 4), Float32)
for j in cutlass.range_constexpr(self.chunks):
column = 4 * (group_lane + self.lanes_per_row * j)
q4 = load_f32x4(
advance(shared_q.iterator, 128 * t) + column, shared=True
)
k4 = load_f32x4(
advance(shared_k.iterator, 128 * t) + column, shared=True
)
for e in cutlass.range_constexpr(4):
qr[j, e] = q4[e] * fq if T == 1 else q4[e]
kr[j, e] = k4[e] * fk if T == 1 else k4[e]
if cutlass.const_expr(t == 0):
wait(gate_barrier)
decays = []
for j in cutlass.range_constexpr(self.chunks):
column = 4 * (group_lane + self.lanes_per_row * j)
decay_ptr = advance(shared_decay.iterator, 128 * t) + column
decays.append(load_f32x4(decay_ptr, shared=True))
values = shared_v
if cutlass.const_expr(T > 1):
pointer = shared_v.iterator + self.rows * t
values = cute.make_tensor(pointer, cute.make_layout(self.rows))
return kr, qr, decays, values, betas[t]
ready = operands(0)
for t in cutlass.range_constexpr(T):
current, ready = ready, operands(t + 1) if t + 1 < T else None
# Butterfly order: interleave rows per level, publish before state.
dots = cute.make_rmem_tensor(self.passes, Float32)
if cutlass.const_expr(self.staged):
wait(state_barrier)
source = staged if self.staged else None
owner, bar = t % self.cluster, advance(barrier, t // self.cluster)
for start in cutlass.range_constexpr(0, self.passes, self.wave):
stop = min(start + self.wave, self.passes)
lanes = (group, group_lane)
self.update(
history, dots, current, lanes, (start, stop), source
)
if cutlass.const_expr(self.early and start == 0 and t == 0):
cute.arch.cluster_wait()
for p in cutlass.range_constexpr(start, stop):
row = p * self.groups + group
send = (row < self.rows) & (group_lane == 0)
if cutlass.const_expr(T > 1): # predicated: no branches
position = self.output_position(value_base + row)
dst = advance(shared_o.iterator, 128 * t) + position
push_float(dst, dots[p], bar, owner, when=send)
elif send:
position = self.output_position(value_base + row)
dst = shared_o.iterator + position
store_async_dsmem(dst, float_bits(dots[p]), bar, owner)
rows = partial(state_row, s=page(writes[t]))
when = None if T == 1 else writes[t] > 0
self.store_rows(history, rows, (start, stop), lanes, when)
if thread < self.rows:
channel = 2 * dim + value_channel
window = (
taps[2, 1],
taps[2, 2],
*(inputs[3 * t + 2] for t in range(T)),
)
for i, value in enumerate(window):
conv_page[channel, i] = value
for t in cutlass.range_constexpr(T):
if partition == t % self.cluster:
wait(advance(barrier, t // self.cluster))
raw_o = shared_o[advance(self.output_position(thread), 128 * t)]
sums = cute.make_rmem_tensor(4, Float32)
for g in cutlass.range_constexpr(4):
base = lane % 8 * 16 + g * 4
o_row = advance(shared_o.iterator, 128 * t)
o4 = load_f32x4(o_row + base, shared=True)
s0, s1, s2, s3 = (o * o for o in o4)
sums[g] = warp_sum((s0 + s2) + (s1 + s3), 8)
sumsq = (sums[0] + sums[2]) + (sums[1] + sums[3])
rstd = cute.math.rsqrt(sumsq * (1.0 / 128) + eps, fastmath=True)
result = raw_o * rstd * nw * gates[t]
output[advance(token, t), qk_channel] = BFloat16(result)
if cutlass.const_expr(t == 0):
for section in cutlass.range_constexpr(2):
channel = section * dim + qk_channel
window = (taps[section, 1], taps[section, 2])
window += tuple(
inputs[3 * u + section] for u in range(T)
)
for i, value in enumerate(window):
conv_page[channel, i] = value
@cute.jit
def store_rows(self, history, state_row, span, lanes, when=None):
"""Store the passes in ``span`` of the CTA's state rows (``when``: predicated
on it, without branches)."""
(start, stop), (group, group_lane) = span, lanes
for p in cutlass.range_constexpr(start, stop):
row = p * self.groups + group
if cutlass.const_expr(when is not None):
for j in cutlass.range_constexpr(self.chunks):
column = 4 * (group_lane + self.lanes_per_row * j)
values = [history[p, j, e] for e in range(4)]
pointer = state_row(row % self.rows, column)
store_f32x4(pointer, values, when=when & (row < self.rows))
elif row < self.rows:
for j in cutlass.range_constexpr(self.chunks):
column = 4 * (group_lane + self.lanes_per_row * j)
pointer = state_row(row, column)
row_values = [history[p, j, e] for e in range(4)]
if cutlass.const_expr(self.evict):
store_evict_f32x4(pointer, row_values)
else:
store_f32x4(pointer, row_values)
@cute.jit
def update(self, history, dots, operands, lanes, span, staged):
"""Decay, delta-rule update and query dot of the passes in ``span``, whose rows
``staged`` (if given) first supplies from shared memory."""
kr, qr, decays, shared_v, beta = operands
(group, lane), (start, stop) = lanes, span
if cutlass.const_expr(staged is not None):
for p in cutlass.range_constexpr(start, stop):
row = (p * self.groups + group) % self.rows
for j in cutlass.range_constexpr(self.chunks):
column = 4 * (lane + self.lanes_per_row * j)
values = load_staged(staged.iterator + (row * 128 + column))
for e in cutlass.range_constexpr(4):
history[p, j, e] = values[e]
for p in cutlass.range_constexpr(start, stop):
for j in cutlass.range_constexpr(self.chunks):
for e in cutlass.range_constexpr(4):
history[p, j, e] *= decays[j][e]
dots[p] = self.row_dot(history, kr, p)
for shift in reversed(range(self.lanes_per_row.bit_length() - 1)):
for p in cutlass.range_constexpr(start, stop):
dots[p] += cute.arch.shuffle_sync_bfly(dots[p], 1 << shift)
for p in cutlass.range_constexpr(start, stop):
safe_row = (p * self.groups + group) % self.rows
v_new = (shared_v[safe_row] - dots[p]) * beta
for j in cutlass.range_constexpr(self.chunks):
for e in cutlass.range_constexpr(4):
history[p, j, e] = fma(
kr[j, e], v_new, history[p, j, e], ftz=self.fast_math
)
dots[p] = self.row_dot(history, qr, p)
for shift in reversed(range(self.lanes_per_row.bit_length() - 1)):
for p in cutlass.range_constexpr(start, stop):
dots[p] += cute.arch.shuffle_sync_bfly(dots[p], 1 << shift)
@cute.jit
def gate_warp(self, args, shared_decay, barrier, coords, staged_rows):
"""Project one value partition and multicast its decays to every CTA. Modes fix
the F_B reduction order: 0 = one chain of eight MMAs, 1 = four chains of two,
2 = sixteen 8-FMA partials summed in a butterfly. Projection math stays IEEE.
``staged_rows``: shared [copy, lane, 4] words; each lane cp.asyncs its own
16-byte F_B operands there before the PDL wait and reads them back after it."""
f_a, a_log, dt_bias, lower_bound, weight = (args[i] for i in (4, 6, 7, 13, 16))
head, token, value_base, lane = coords
rows, mode = self.rows, self.gate_mode
per: cutlass.Constexpr = max(1, rows // 32) # decays per lane
a = a_log[head]
if cutlass.const_expr(mode == 2):
feature = lane % rows
row = head * 128 + value_base + feature
# uint4 weights before the wait, two uint2 activation loads per 8-FMA
# partial after it; keep that granularity and the low-then-high FMA order.
wr = [load_weight_bf16x8(at(weight, row, 8 * i)) for i in range(16)]
dt = dt_bias[row]
cute.arch.griddepcontrol_wait()
partial = cute.make_rmem_tensor(16, Float32)
for i in cutlass.range_constexpr(16):
a0 = load_activation_bf16x4(at(f_a, token, 8 * i))
a1 = load_activation_bf16x4(at(f_a, token, 8 * i + 4))
av = (*a0, *a1)
acc = Float32(0)
for e in cutlass.range_constexpr(4):
x0, x1 = unpack_bf16(av[e])
w0, w1 = unpack_bf16(wr[i][e])
acc = fma(x0, w0, acc)
acc = fma(x1, w1, acc)
partial[i] = acc
for shift in cutlass.range_constexpr(3, -1, -1):
for i in cutlass.range_constexpr(1 << shift):
partial[i] += partial[i | (1 << shift)]
raw_g = [Float32(BFloat16(partial[0]))]
dt = [dt]
else:
tiles: cutlass.Constexpr = rows // 16
chains: cutlass.Constexpr = 4 if mode == 1 else 1
group, quarter = lane >> 2, lane & 3
feature = 16 * (quarter >> 1) + group + 8 * (quarter & 1)
dt = [Float32(0) for _ in range(per)]
if cutlass.const_expr(self.tokens > 1): # token column, its features
token += group % self.tokens
features = [
16 * (i // 2) + group + 8 * (i % 2) for i in range(rows // 8)
]
dt = [dt_bias[head * 128 + value_base + f] for f in features]
for r in cutlass.range_constexpr(per if self.tokens == 1 else 0):
if feature + 32 * r < rows:
dt[r] = dt_bias[head * 128 + value_base + 32 * r + feature]
# Two decays per lane load 16 bytes per row: chunk c then covers columns
# 32 (c // 2) + 8 quarter + 4 (c % 2) + [0, 4) instead of 16 c + 4 quarter.
wide: cutlass.Constexpr = per >= 2
wr = cute.make_rmem_tensor((tiles, 8, 4), Uint32)
if cutlass.const_expr(staged_rows is not None):
for tile in cutlass.range_constexpr(tiles):
row = head * 128 + value_base + 16 * tile + group
for chunk in cutlass.range_constexpr(4):
col = 32 * chunk + 8 * quarter
for half in cutlass.range_constexpr(2):
slot = staged_rows[
(tile * 4 + chunk) * 2 + half, lane, None
]
source = at(weight, row + 8 * half, col)
copy_g2s(slot.iterator, source)
cute.arch.cp_async_commit_group()
else:
for tile in cutlass.range_constexpr(tiles):
row = head * 128 + value_base + 16 * tile + group
for chunk in cutlass.range_constexpr(4 if wide else 8):
if cutlass.const_expr(wide):
col = 32 * chunk + 8 * quarter
low = load_weight_bf16x8(at(weight, row, col))
high = load_weight_bf16x8(at(weight, row + 8, col))
self.split_pairs(wr, tile, chunk, low, high)
else:
col = 16 * chunk + 4 * quarter
low = load_bf16x4(at(weight, row, col))
high = load_bf16x4(at(weight, row + 8, col))
wr[tile, chunk, 0], wr[tile, chunk, 2] = low
wr[tile, chunk, 1], wr[tile, chunk, 3] = high
cute.arch.griddepcontrol_wait()
if cutlass.const_expr(wide):
pairs = [
load_activation_bf16x8(at(f_a, token, 32 * c + 8 * quarter))
for c in range(4)
]
ar = [pairs[c // 2][2 * (c % 2) : 2 * (c % 2) + 2] for c in range(8)]
else:
ar = [
load_activation_bf16x4(at(f_a, token, 16 * c + 4 * quarter))
for c in range(8)
]
if cutlass.const_expr(staged_rows is not None):
cute.arch.cp_async_wait_group(0)
for tile in cutlass.range_constexpr(tiles):
for chunk in cutlass.range_constexpr(4):
copy = (tile * 4 + chunk) * 2
low = smem_words(staged_rows[copy, lane, None].iterator)
high = smem_words(staged_rows[copy + 1, lane, None].iterator)
self.split_pairs(wr, tile, chunk, low, high)
acc = zeros((chains, tiles, 4), Float32)
for chunk in cutlass.range_constexpr(8):
chain: cutlass.Constexpr = chunk // 2 if mode == 1 else 0
for tile in cutlass.range_constexpr(tiles):
wa = tuple(wr[tile, chunk, e] for e in range(4))
mma_into(acc[chain, tile, None], wa, ar[chunk], bf16=True)
if cutlass.const_expr(self.tokens > 1):
gates = (a, lower_bound, dt, features)
targets = (shared_decay, barrier, value_base)
return self.multicast(acc, gates, targets, lane)
raw_g = []
for r in cutlass.range_constexpr(per):
partial = cute.make_rmem_tensor(chains, Float32)
for chain in cutlass.range_constexpr(chains):
selected = acc[chain, 2 * r, 0]
if quarter & 1:
selected = acc[chain, 2 * r, 2]
if cutlass.const_expr(tiles > 2 * r + 1): # noqa: SIM102
if quarter >> 1:
selected = acc[chain, 2 * r + 1, 0]
if quarter & 1:
selected = acc[chain, 2 * r + 1, 2]
partial[chain] = selected
value = partial[0]
if cutlass.const_expr(mode == 1):
value = Float32(0) + (
((value + partial[1]) + partial[2]) + partial[3]
)
raw_g.append(Float32(BFloat16(value)))
decay = []
for r in cutlass.range_constexpr(per):
gate = cute.math.exp(a, fastmath=True) * (raw_g[r] + dt[r])
gate = sigmoid(gate, self.fast_math)
decay.append(cute.math.exp(lower_bound * gate, fastmath=True))
packets: cutlass.Constexpr = rows // 4
packet, destination = lane % packets, lane // packets
values = cute.make_rmem_tensor(4, Float32)
for e in cutlass.range_constexpr(4):
source = 4 * packet + e
if cutlass.const_expr(per == 2):
feature = source & 31
source = (feature & 7) << 2 | (feature >> 4) << 1 | feature >> 3 & 1
low = cute.arch.shuffle_sync(decay[0], source)
high = cute.arch.shuffle_sync(decay[1], source)
values[e] = low
if packet >= 8:
values[e] = high
elif cutlass.const_expr(per > 2): # one CTA per head: 128 features
feature = source & 31
source = (feature & 7) << 2 | (feature >> 4) << 1 | feature >> 3 & 1
shuffled = [cute.arch.shuffle_sync(d, source) for d in decay]
values[e] = shuffled[0]
for r in cutlass.range_constexpr(1, per):
if packet >> 3 == r:
values[e] = shuffled[r]
else:
if cutlass.const_expr(mode != 2):
source = (source & 7) << 2 | (source >> 4) << 1 | source >> 3 & 1
values[e] = cute.arch.shuffle_sync(decay[0], source)
offset = shared_decay.iterator + value_base + 4 * packet
pointer = cute.make_ptr(
Float32, offset.llvm_ptr, cute.AddressSpace.smem, assumed_align=16
)
words = tuple(float_bits(values[e]) for e in range(4))
if cutlass.const_expr(self.early):
cute.arch.cluster_wait()
store_async_dsmem(pointer, words, barrier, destination)
@cute.jit
def multicast(self, acc, gates, targets, lane):
"""Decays (q > 1) of the lane's D fragments (feature rows, token columns 2
quarter + {0, 1}) to every CTA's shared [token, 128] decays."""
(a, lower_bound, dt, features), (shared_decay, barrier, value_base) = (
gates,
targets,
)
quarter = lane & 3
if cutlass.const_expr(self.early):
cute.arch.cluster_wait()
for i in cutlass.range_constexpr(len(features)):
tile, e = i // 2, (i % 2) * 2
for column in cutlass.range_constexpr(2):
value = acc[0, tile, e + column]
if cutlass.const_expr(self.gate_mode == 1):
parts = [acc[c, tile, e + column] for c in range(4)]
value = Float32(0) + (((value + parts[1]) + parts[2]) + parts[3])
raw_g = Float32(BFloat16(value))
gate = cute.math.exp(a, fastmath=True) * (raw_g + dt[i])
gate = sigmoid(gate, self.fast_math)
decay = float_bits(cute.math.exp(lower_bound * gate, fastmath=True))
token = 2 * quarter + column
offset = token * 128 + value_base + features[i]
pointer = shared_decay.iterator + offset
if token < self.tokens:
for rank in cutlass.range_constexpr(self.cluster):
store_async_dsmem(pointer, decay, barrier, rank)
@staticmethod
def split_pairs(wr, tile, chunk, low, high):
"""16 bytes of rows r and r + 8 hold the A fragments of chunks 2c and 2c + 1."""
for h in range(2):
c = 2 * chunk + h
wr[tile, c, 0], wr[tile, c, 2] = low[2 * h : 2 * h + 2]
wr[tile, c, 1], wr[tile, c, 3] = high[2 * h : 2 * h + 2]
@cute.jit
def setup(self, barrier, gate_barrier, partition, thread):
"""Thread 0 arms the output (leader) and decay barriers; the CTA arrives on the
cluster barrier peers wait on before their sends."""
tokens, cluster = self.tokens, self.cluster
owned = [(barrier, partition == 0, 512)] # token t's outputs: CTA t % cluster
if cutlass.const_expr(tokens > 1):
owned = [
(barrier + k, partition + k * cluster < tokens, 512)
for k in range(-(-tokens // cluster))
]
for bar, cta, size in (*owned, (gate_barrier, True, 512 * tokens)):
if cta & (thread == 0):
cute.arch.mbarrier_init(bar, 1)
cute.arch.mbarrier_arrive_and_expect_tx(bar, size)
cute.arch.mbarrier_init_fence()
cute.arch.cluster_arrive_relaxed()
@cute.jit
def row_dot(self, history, vector, p):
partial = cute.make_rmem_tensor(self.chunks, Float32)
for j in cutlass.range_constexpr(self.chunks):
row = (history[p, j, e] for e in range(4))
partial[j] = dot(row, (vector[j, e] for e in range(4)), ftz=self.fast_math)
for shift in cutlass.range_constexpr(self.chunks.bit_length() - 2, -1, -1):
for j in cutlass.range_constexpr(1 << shift):
partial[j] += partial[j + (1 << shift)]
return partial[0]
@cute.jit
def output_position(self, row: Int32):
return ((row & 7) << 4) | ((row >> 5) << 2) | ((row & 31) >> 3)
@@ -0,0 +1,112 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Prepared KDA core. One packed projection supplies Q/K/V, the output gate, F_A and
beta; per-batch policies select the gate schedule and the projection row pitch."""
from functools import cache
import cutlass
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream
from ..common.host import fake, layout, stream, to_cute
from ..common.policy import batch_table
from .kernel import KdaSplit
# KdaSplit schedule "early.lanes.occupancy.cluster.evict" per batch by heads (TP8 12,
# TP16 6), from GB300 sweeps: early predecessor-independent loads (MMA gate modes only;
# 2 also stages the state rows in shared memory, 3 also the F_B operands), lanes per
# state row, minimum CTAs per SM, CTAs per head (0: defaults) and L2 evict-first state
# traffic (staged only).
SCHEDULE = {12: "16-32:3.16.3.2.1,64:3.16.4.2.1", 6: "16:1.0.0,32-64:3.16.3.2"}
# Verify (q > 1) schedules by request count B, from GB300
# sweeps over q 4-8: no staging; eight-CTA clusters, then fewer as B grows.
SPEC_SCHEDULE = {
12: "1:1.8.2.8,2-4:1.8.3.8,5-12:1.8.3.4,13-16:1.8.3.2",
6: "1:1.8.2.8,2:1.8.0.8,3-12:1.8.3.8,13-16:1.8.3.4",
}
@cache
def executable(specs, heads, gate_mode, has_bias, device, tokens=1):
args = tuple(fake(shape, stride, dtype) for shape, stride, dtype in specs)
batch = specs[0][0][0]
table = SCHEDULE if tokens == 1 else SPEC_SCHEDULE
schedule = batch_table(table.get(heads, ""))
schedule = schedule.get(batch // tokens, (0, 0, 0)) if gate_mode != 2 else (0,) * 3
if schedule[0] >= 2 and specs[9][1][-2:] != (128, 1): # staging needs whole rows
schedule = (1, *schedule[1:4]) # no evict hint without staging
requests = batch // tokens
kernel = KdaSplit(heads, requests, gate_mode, has_bias, schedule, tokens)
scalars = (cutlass.Float32(v) for v in (-5, 128**-0.5, 1e-6))
with torch.accelerator.device_index(device):
return cute.compile(
kernel, *args[:13], *scalars, *args[13:], make_fake_stream()
)
def decode_conv_weight(weight):
"""The [3, width, D] (Q, K, V) decode layout of the [3 * D, 1, width] conv
weight."""
width = weight.shape[-1]
return weight.detach().view(3, -1, width).transpose(1, 2).contiguous()
class PreparedKda:
def __init__(self, attention, policies, conv_dim_first):
bias, dt = attention.conv1d.bias, attention.dt_bias
self.heads = attention.A_log.numel()
self.dim, self.has_bias = self.heads * 128, bias is not None
conv_w = decode_conv_weight(attention.conv1d.weight)
bias, fb = dt if bias is None else bias, attention.f_b_proj.weight
norm = attention.o_norm.weight.detach().float() # BF16 slows the fused kernel
self.weights = (conv_w, bias, fb, attention.A_log, dt, norm)
self.device, self.policies = dt.device, policies
scalars = (attention.gate_lower_bound, 128**-0.5, attention.o_norm.eps)
self.scalars = tuple(map(cutlass.Float32, scalars))
self.conv_dim_first, self.kernels = conv_dim_first, {}
def _operands(self, caches, projected, slots, out, accepted=None):
"""Kernel tensors in launch order: 13 before the scalars, then F_B (and the
verify step's accepted counts)."""
conv, state = caches[:2]
conv = conv if self.conv_dim_first else conv.transpose(-1, -2)
w, bias, fb, a_log, dt, norm = self.weights
d, h = self.dim, self.heads
p = lambda start, width: projected[:, start : start + width]
ops = (p(0, 3 * d), w, bias, conv, p(4 * d, 128), p(4 * d + 128, h), a_log, dt)
tail = (fb,) if accepted is None else (fb, accepted)
return ops + (slots, state, out, p(3 * d, d), norm) + tail
def prepare_caches(self, caches):
"""Policy keys: a batch, or (requests, tokens) for verify steps."""
for key, (pitch, mode) in self.policies.items():
requests, tokens = key if isinstance(key, tuple) else (key, 1)
batch, meta = requests * tokens, dict(device="meta")
projected = torch.empty((batch, pitch), dtype=torch.bfloat16, **meta)
out = torch.empty(batch, self.dim, dtype=torch.bfloat16, **meta)
accepted = torch.empty(requests, dtype=torch.int32, **meta)
shape = (requests, tokens) if tokens > 1 else (batch,)
slots = torch.empty(shape, dtype=torch.int32, **meta)
accepted = accepted if tokens > 1 else None
operands = self._operands(caches, projected, slots, out, accepted)
self.kernels[key] = executable(
tuple(map(layout, operands)),
*(self.heads, mode, self.has_bias, self.device.index, tokens),
)
return self
def run(self, projected, out, *, caches, slots, accepted=None):
"""``accepted`` [>= B] (verify steps): ``slots`` is [>= B, q] contiguous."""
batch = projected.shape[0]
key, rows = batch, batch
if accepted is not None:
rows = batch // slots.shape[1]
key, accepted = (rows, slots.shape[1]), accepted[:rows]
if slots.stride() != (slots.shape[1], 1): # e.g. a block-table slice
slots = slots.contiguous()
tensors = self._operands(caches, projected, slots[:rows], out, accepted)
views = tuple(to_cute(t, 16) for t in tensors)
self.kernels[key](*views[:13], *self.scalars, *views[13:], stream(self.device))
return out
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,378 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""MLA decode under decode context parallelism (DCP = TP, rank d owns head block d)
without collectives, three kernels per layer. Each rank's KV cache holds 1 / DCP of
every context.
MlaQuery FP8 query rows [B, H, 576] (+ the KV insert where this rank owns the
slot); each CTA multicasts its bytes into every rank's Lamport q
mailbox [B, DCP*H, 576], then polls its share until all landed
tokenspeed all DCP*H heads over the local KV shard -> BF16 rows + LSE (log2)
DcpValue re-arms the q mailbox; rows pushed to their head owner's Lamport
mailbox (peer stores); the owner LSE-merges its H heads over the DCP
sources, then W_UV and the gate (padding from the global lengths)
"""
import math
from functools import cache
import cutlass
import cutlass.cute as cute
import torch
import torch.distributed._symmetric_memory as symm_mem
from cutlass import BFloat16, Float32, Int32, Int64, Uint32
from cutlass.cute.runtime import make_fake_stream
from ..common.host import fake
from ..common.primitives import (
alloc,
at,
bf16x2,
bits_float,
float_bits,
launch,
load_volatile,
load_words,
mma_into,
no_neg_zero,
pack_pair,
published,
sigmoid,
store_relaxed,
store_shared_words,
store_words,
unpack_bf16,
warp_sum,
zeros,
)
LSE_SENT = 0xFFFFFFFF # a NaN pattern the merge never sends
# The LSE of an empty or invalid source: finite, so the merge never forms inf - inf.
EMPTY_LSE = -3.0e38
Q_SENT = 0xFFFFFFFF # four FP8 0xFF bytes: never written (satfinite, canonical NaN)
PAIR_SENT = 0x80000000 # BF16 pair (+0, -0); sent pairs have no negative zero
class DcpBox:
"""Model-owned DCP state over ``group``, Lamport mailboxes (sentinel-filled,
re-armed by their reader): q [2, CAP, DCP*H, 576] FP8 bytes (multicast stores),
partials [2, DCP, CAP, H, 512] BF16 and LSE [2, DCP, CAP, H] FP32 (peer stores),
and the local tokenspeed rows / LSE. Parity alternates by layer. ``interleave``:
the KV cache's DCP interleave (global masking of spec verify rows)."""
def __init__(self, group, device, size, rank, heads, cap=64, interleave=1):
self.size, self.rank, self.heads, self.cap = size, rank, heads, cap
self.interleave = interleave
empty = symm_mem.empty
self.q = empty((2, cap, size * heads, 576), dtype=torch.uint8, device=device)
shape = (2, size, cap, heads)
self.partial = empty((*shape, 512), dtype=torch.bfloat16, device=device)
self.lse = empty(shape, dtype=torch.float32, device=device)
tensors = (self.q, self.partial, self.lse)
handles = [symm_mem.rendezvous(t, group) for t in tensors]
self.q_mc = int(handles[0].multicast_ptr or 0)
if not self.q_mc:
raise RuntimeError("Kimi K3 specialized MLA DCP needs multicast memory")
i64 = dict(dtype=torch.int64, device=device)
self.peers = torch.tensor(handles[1].buffer_ptrs, **i64)
self.lse_peers = torch.tensor(handles[2].buffer_ptrs, **i64)
self.q.fill_(0xFF)
self.partial.view(torch.int32).fill_(-0x80000000)
self.lse.view(torch.int32).fill_(-1)
rows = (cap, 1, size * heads)
self.rows = torch.empty((*rows, 512), dtype=torch.bfloat16, device=device)
self.rows_lse = torch.empty(rows, dtype=torch.float32, device=device)
torch.accelerator.synchronize(device)
torch.distributed.barrier(group=group)
def mc(self, parity):
"""The q mailbox multicast address of ``parity``."""
return Int64(self.q_mc + parity * self.q[0].numel())
class DcpValue:
"""The DCP tail of one MLA layer, one 512-thread CTA per (head, 16 requests).
Every rank re-arms this parity's q mailbox (tokenspeed has read it) and pushes all
its local tokenspeed rows (+ LSE) to their head owner's Lamport mailbox (empty
local shards: zeros, LSE -inf). Warp r then LSE-weights request t0 + r of this
head over the DCP sources (exp2(lse - max) / sum in FP32), the CTA merges the
polled rows in FP32
(one BF16 rounding, re-arming what it read) into shared memory, and W_UV (BF16
MMA, FP32 accumulation) and the sigmoid gate follow as in MlaValue: warp w owns
outputs 32 (w // 4) + 8 (w % 4) .. + 7. Rows are tokens, ``q`` per request."""
def __init__(self, heads, size, batch, sms=148, q=1):
self.heads, self.size, self.batch = heads, size, batch
self.q = q
# Push-only CTAs (z > 0) spread the NVLink stores over more SMs: up to one
# wave, at least 4 16 B chunks per thread.
merge = heads * ((batch + 15) // 16)
chunks = batch * size * heads * 64
pushers = min(max(1, sms // merge), -(-chunks // (merge * 512 * 4)))
self.grid = (heads, (batch + 15) // 16, pushers)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, self.grid, 512)
@cute.kernel
def kernel(self, args):
rows, rows_lse, lens, box, lbox, peers, lpeers, mailbox = args[0:8]
w_uv, gate, totals, output, parity, rank = args[8:14]
H, D, B, cap = self.heads, self.size, self.batch, box.shape[1]
smem = cutlass.utils.SmemAllocator()
latent = alloc(smem, BFloat16, (16, 544), 128)
scales = alloc(smem, Float32, (16, D))
tid, _, _ = cute.arch.thread_idx()
head, tile, pusher = cute.arch.block_idx()
lane, warp = tid % 32, tid // 32
group, quarter = lane // 4, lane % 4
v = 32 * (warp // 4) + 8 * (warp % 4) + group
# B fragments: 8 latent values per 32-wide chunk; A uses the same permutation.
wb = [
load_words(at(w_uv, head, 32 * c + 8 * quarter, v), count=4)
for c in range(16)
]
cute.arch.griddepcontrol_wait()
threads = H * self.grid[1] * self.grid[2] * 512
me = ((pusher * self.grid[1] + tile) * H + head) * 512 + tid
for c in range(me, B * D * H * 36, threads): # 16 B q mailbox chunks
store_words(mailbox.iterator + c * 16, (Uint32(Q_SENT),) * 4)
for i in range(me, B * D * H * 64, threads):
r, k = i // 64, i % 64 # r = b * D * H + global head
b, gh = r // (D * H), r % (D * H)
words = (Uint32(0),) * 4
if lens[b // self.q] > 0:
words = load_words(at(rows, b, 0, gh, 8 * k), count=4, mutable=True)
words = tuple(no_neg_zero(w) for w in words)
slot = ((parity * D + rank) * cap + b) * H + gh % H
store_relaxed(peers[gh // H] + (Int64(slot) * 512 + 8 * k) * 2, words)
for i in range(me, B * D * H, threads):
b, gh = i // (D * H), i % (D * H)
value, n = rows_lse[b, 0, gh], lens[b // self.q]
bad = (value != value) | (value <= Float32(EMPTY_LSE)) # NaN, -inf
if (n <= 0) | bad | (value >= Float32(-EMPTY_LSE)): # or +inf
value = Float32(EMPTY_LSE)
slot = ((parity * D + rank) * cap + b) * H + gh % H
store_relaxed(lpeers[gh // H] + Int64(slot) * 4, (float_bits(value),))
cute.arch.griddepcontrol_launch_dependents()
if pusher == 0:
self.merge(args, latent, scales, wb, tid)
@cute.jit
def merge(self, args, latent, scales, wb, tid):
"""The owner's merge of request rows t0.. of this head, then W_UV + gate."""
box, lbox, gate, totals, output = args[3], args[4], args[9], args[10], args[11]
D, B = self.size, self.batch
head, tile, _ = cute.arch.block_idx()
lane, warp = tid % 32, tid // 32
group, quarter = lane // 4, lane % 4
t0 = tile * 16
col = head * 128 + 32 * (warp // 4) + 8 * (warp % 4) + 2 * quarter
gates = []
for r in cutlass.range_constexpr(2):
word = Uint32(0)
if t0 + group + 8 * r < B:
word = load_words(
at(gate, t0 + group + 8 * r, col), count=1, mutable=True
)[0]
gates.append(unpack_bf16(word))
if t0 + warp < B: # warp r: the source weights of request t0 + r
lse = Float32(EMPTY_LSE)
if lane < D:
source = at(lbox, lane, t0 + warp, head)
word = load_volatile(source)[0]
while word == Uint32(LSE_SENT):
word = load_volatile(source)[0]
store_words(source, (Uint32(LSE_SENT),))
lse = bits_float(word)
top = lse
for shift in cutlass.range_constexpr(4, -1, -1):
top = max(top, cute.arch.shuffle_sync_bfly(top, 1 << shift))
weight = Float32(0)
if lse > Float32(EMPTY_LSE):
weight = cute.math.exp2(lse - top, fastmath=True)
total = warp_sum(weight)
scale = Float32(0)
if total > 0:
scale = weight / total
if lane < D:
scales[warp, lane] = scale
cute.arch.sync_threads()
chunk = tid % 64
for half in cutlass.range_constexpr(2): # rows r and r + 8, 16 B chunk
r = tid // 64 + 8 * half
merged = zeros(8, Float32)
if t0 + r < B:
for s0 in cutlass.range_constexpr(0, D, 8): # 8 polls in flight
sources = [
at(box, s, t0 + r, head, 8 * chunk)
for s in range(s0, min(D, s0 + 8))
]
loads = [load_volatile(source, count=4) for source in sources]
for i in cutlass.range_constexpr(len(sources)):
words = loads[i]
while not published(words):
words = load_volatile(sources[i], count=4)
store_words(sources[i], (Uint32(PAIR_SENT),) * 4)
w = scales[r, s0 + i]
for e in cutlass.range_constexpr(4):
lo, hi = unpack_bf16(words[e])
merged[2 * e] += w * lo
merged[2 * e + 1] += w * hi
pairs = tuple(
pack_pair(merged[2 * e], merged[2 * e + 1], policy=None)
for e in range(4)
)
store_shared_words(latent.iterator + r * 544 + 8 * chunk, pairs)
cute.arch.sync_threads()
parts = zeros((2, 4), Float32) # two accumulation chains
for c in cutlass.range_constexpr(16):
x, y = (
load_words(
latent.iterator + r * 544 + 32 * c + 8 * quarter,
count=4,
shared=True,
)
for r in (group, group + 8)
)
for s in cutlass.range_constexpr(2):
a = (x[2 * s], y[2 * s], x[2 * s + 1], y[2 * s + 1])
b = (wb[c][2 * s], wb[c][2 * s + 1])
mma_into(parts[s, None], a, b, bf16=True)
acc = [parts[0, k] + parts[1, k] for k in range(4)]
for r in cutlass.range_constexpr(2):
t = t0 + group + 8 * r
if t < B:
# Padded rows may hold a NaN gate; keep inactive rows zero.
lo, hi = Float32(0), Float32(0)
if totals[t // self.q] > 0:
lo, hi = (
Float32(BFloat16(acc[2 * r + e])) * sigmoid(gates[r][e], False)
for e in range(2)
)
store_words(at(output, t, col), (bf16x2(lo, hi),))
@cache
def compile_value(heads, size, batch, cap, uv, pitch, device, sms=148, q=1):
"""DcpValue of one batch (``q`` rows per request); ``uv``: the (shape, stride,
dtype) of W_UV, ``pitch``: the projected rows' (the gate follows the 2112 query /
KV columns)."""
f32, i32 = torch.float32, torch.int32
w_uv, gate = fake(*uv), fake((batch, heads * 128), (pitch, 1))
rows = fake((batch, 1, size * heads, 512))
rows_lse = fake((batch, 1, size * heads), None, f32, 4)
lens = fake((batch // q,), None, i32, 4)
box = fake((size, cap, heads, 512))
lbox = fake((size, cap, heads), None, f32, 4)
peers = fake((size,), None, torch.int64, 8)
mailbox = fake((cap, size * heads, 576), None, torch.uint8)
output = fake((batch, heads * 128), (heads * 128, 1))
args: tuple = (rows, rows_lse, lens, box, lbox, peers, peers, mailbox, w_uv)
args += (gate, lens, output, Int32(0), Int32(0), make_fake_stream())
with torch.accelerator.device_index(device):
return cute.compile(DcpValue(heads, size, batch, sms, q), *args)
_WORKSPACE: dict[tuple[torch.device, int], torch.Tensor] = {}
def stock_decode(
query,
cache,
block_table,
lengths,
max_seq,
scales,
out,
lse=None,
min_split=1,
pdl=True,
cp=(1, 0, 1),
totals=None,
pack=False,
):
"""tokenspeed_mla_decode's FP8 path with caller-owned ``out`` and ``lse`` (log2)
buffers and no per-call allocation: the split count follows from (B, max_seq,
min_split); ``lengths`` bound the work, empty splits are skipped by its reducer.
The q rows of a request mask causally; under DCP in global positions (``cp``:
world, rank, interleave; ``totals``: the global lengths). ``pack``: M128 tiles hold
consecutive (query row, head) rows across the q rows."""
import tvm_ffi
from tokenspeed_mla import mla_decode as md
batch, q_len, heads, _ = query.shape
fp8, device = torch.float8_e4m3fn, query.device
arch = torch.cuda.get_device_capability(device)
tiler, _ = md.select_mla_decode_tilers(
heads, q_len, is_fp8=True, compute_capability=arch
)
pack = pack and tiler[0] == 128
fold = 1 if pack else md.get_mla_decode_fold_sq_factor(heads, q_len, tiler[0])
rows = (q_len // fold, heads * fold)
if pack:
rows = (md.compute_q_tile_layout(heads, q_len, 128)[1], 128)
sms = md.get_num_sm(device)
split, size = md._get_split_kv_and_workspace_size(
batch, *rows, 512, sms, max_seq, fp8, tiler, min_split
)
# One split-KV workspace per (device, heads), shared by every MLA layer: the calls
# are stream ordered. PreparedMla.prepare_cache runs each prepared (batch, q) before
# any capture, so the buffer reaches its final size first; growing it during a
# capture would leave the graphs captured before with the freed buffer.
key = device, heads
if key not in _WORKSPACE or _WORKSPACE[key].numel() < size:
if torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"the MLA split-KV workspace must be sized before capture"
)
bound = max(size, sms * heads * 513 * 4)
_WORKSPACE[key] = torch.empty(bound, dtype=torch.int8, device=device)
kernel = md._get_compiled_mla_kernel(
torch_dtype=fp8,
page_size=cache.shape[1],
kv_lora_rank=512,
qk_rope_head_dim=64,
is_persistent=False,
is_var_seq=True,
is_var_split_kv=False,
compute_capability=arch,
has_local_visible_lens=False,
skip_correction_threshold=8.0,
is_workspace_size_zero=size == 0,
fold_sq_factor=fold,
causal_mask=True,
num_heads=heads,
seq_len_q=q_len,
window_left=-1,
cp_world=cp[0],
cp_rank=cp[1],
cp_interleave_size=cp[2],
use_pdl=pdl,
return_lse=lse is not None,
reducer_d_tiles=md._get_reducer_d_tiles(batch, q_len, heads, sms, split),
reducer_max_splits=md._get_reducer_max_splits(split),
pack_q=pack,
partial_fp16=False,
)
cache = cache.view(fp8)
softmax, output_scale = scales
args = [query[..., :512], query[..., 512:], cache[..., :512], cache[..., 512:]]
args += [block_table, out, lse, _WORKSPACE[key][:size] if size else None]
args += [Int32(split), lengths, lengths if totals is None else totals, None]
args += [Float32(softmax), Float32(output_scale)]
with tvm_ffi.use_torch_stream():
kernel(*args)
def fill_splits(batch, device):
"""Splits filling the SMs (one CTA pair each); tokenspeed caps itself at 32."""
from tokenspeed_mla import get_num_sm
return min(256, max(1, get_num_sm(device) // (2 * batch)))
def local_max(max_context, size):
"""Longest local shard of a ``max_context`` sequence (block-interleaved)."""
return math.ceil(max_context / (size * 64)) * 64
@@ -0,0 +1,255 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Prepared MLA core: the packed input projection feeds the query kernel, then either
tokenspeed_mla_decode (core "tokenspeed") or FlashInfer's trtllm-gen MLA decode (core
"trtllm"; FP8 Q and KV as tokenspeed, FP32 softmax and accumulation) into the BF16
latent, and MlaValue (W_UV / gate). Under DCP (``dcp``: a DcpBox) the tokenspeed core
runs over the local KV shard for every DCP rank's heads between the query's q
publication and ``DcpValue``'s merge (see mla.dcp). Spec verify: ``q`` query tokens
per request (rows grouped by request, causal among them)."""
from functools import cache
import cutlass
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream
from ..common.host import fake, layout, prepare_shared_carveout, stream, to_cute
from .dcp import (
compile_value,
fill_splits,
local_max,
stock_decode,
)
from .query import MlaQuery
from .value import MlaValue
# tokenspeed (SM-filling splits) streams long contexts fast; trtllm-gen wins from B16.
TRTLLM_CORE_BATCH, CORES = 16, ("tokenspeed", "trtllm")
_FLASHINFER: dict[tuple[torch.device, int], tuple[torch.Tensor, torch.Tensor]] = {}
@cache
def compile_core(batch, heads, pitch, specs, device, dcp=0, sms=148, q=1):
"""CuTe kernels (query, value) of one batch of rows (``q`` per request); under DCP
(``dcp``: its size) the query publishes and the value is DcpValue."""
qnorm, kvnorm, qb, uk, uv, qinv, kinv, kv = (
fake(shape, stride, dtype, 4 if dtype == torch.float32 else 16)
for shape, stride, dtype in specs
)
query = fake((batch, heads, 576), (heads * 576, 576, 1), torch.uint8)
projected = fake((batch, 2112), (pitch, 1))
slots = fake((batch,), (1,), torch.int64, 8)
lengths = fake((batch // q,), (1,), torch.int32, 4)
gate = fake((batch, heads * 128), (pitch, 1))
output = fake((batch, heads * 128), (heads * 128, 1))
latent = fake((batch, heads, 512))
f32, s = cutlass.Float32, make_fake_stream()
cp_async = heads == 12 and batch == 1
mla_query = MlaQuery(heads, batch, q_b_cp_async=cp_async, dcp=dcp)
q_args: tuple = (projected, qnorm, kvnorm, qb, uk, kv, slots, qinv, kinv)
q_args += (f32(1e-6), query)
if dcp: # this parity's q mailbox, its multicast address, rank
mailbox = fake((64, dcp * heads, 576), None, torch.uint8)
q_args += (mailbox, cutlass.Int64(0), cutlass.Int32(0))
with torch.accelerator.device_index(device):
kernels = [cute.compile(mla_query, *q_args, s)]
if batch > 1:
kernels = [prepare_shared_carveout(kernels[0], device, 100)]
if dcp:
uv_spec = specs[4]
kernels.append(
compile_value(heads, dcp, batch, 64, uv_spec, pitch, device, sms, q)
)
else:
v_args = (latent, uv, gate, lengths, output, s)
value = MlaValue(heads, batch, q)
kernels.append(cute.compile(value, *v_args))
return tuple(kernels)
def core_for(batch, core="auto", q=1):
"""``core`` unless "auto": trtllm-gen from B16, else tokenspeed (spec verify,
q > 1: tokenspeed, whose SM-filling splits stream long contexts far faster)."""
if core == "auto":
core = "trtllm" if batch >= TRTLLM_CORE_BATCH and q == 1 else "tokenspeed"
if core not in CORES:
raise ValueError(
f"Kimi K3 MLA core {core!r}: expected auto, tokenspeed or trtllm"
)
return core
def pad_requests(batch, block_table, lengths, totals=None):
"""Request metadata for ``batch`` rows: PIECEWISE graphs pad the tokens but not
the requests, so missing rows become empty requests (length 0 on page 0)."""
rows = min(lengths.shape[0], block_table.shape[0])
if rows >= batch:
return block_table, lengths, totals
pad = torch.nn.functional.pad
block_table = pad(block_table[:rows], (0, 0, 0, batch - rows))
lengths = pad(lengths[:rows], (0, batch - rows))
totals = None if totals is None else pad(totals[:rows], (0, batch - rows))
return block_table, lengths, totals
def trtllm_decode(query, cache, block_table, lengths, max_seq, scales, out):
"""FlashInfer trtllm-gen MLA decode of FP8 ``query`` [B, q, H, 576] (causal among
the q rows) over the FP8 paged cache into BF16 ``out`` [B, q, H, 512], PDL chained;
zeroed workspace and multi-CTA counter (self-resetting) owned here, one per (device,
heads)."""
from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
from flashinfer.utils import get_trtllm_gen_multi_ctas_kv_counter_bytes
device, heads = query.device, query.shape[2]
if (device, heads) not in _FLASHINFER:
sms = torch.cuda.get_device_properties(device).multi_processor_count
count = get_trtllm_gen_multi_ctas_kv_counter_bytes(64, heads, sms)
workspace = torch.zeros(128 << 20, dtype=torch.int8, device=device)
counter = torch.zeros(count, dtype=torch.uint8, device=device)
_FLASHINFER[device, heads] = workspace, counter
workspace, counter = _FLASHINFER[device, heads]
softmax, output_scale = scales
trtllm_batch_decode_with_kv_cache_mla(
query=query,
kv_cache=cache.view(torch.float8_e4m3fn).unsqueeze(1),
workspace_buffer=workspace,
qk_nope_head_dim=128,
kv_lora_rank=512,
qk_rope_head_dim=64,
block_tables=block_table,
seq_lens=lengths,
max_seq_len=max_seq,
out=out,
bmm1_scale=softmax,
bmm2_scale=output_scale,
enable_pdl=True,
backend="trtllm-gen",
multi_ctas_kv_counter_buffer=counter,
)
class PreparedMla:
"""``max_context`` (the deployment's longest sequence) bounds the split count;
``core`` forces "tokenspeed" or "trtllm". ``dcp``: the model's DcpBox, with this
layer's mailbox ``parity``. ``queries``: the query lengths q (spec verify: 1 + the
draft length) prepared for every batch of rows divisible by q."""
def __init__(
self,
attention,
pitches,
query_scratch,
*,
max_context=None,
core="auto",
dcp=None,
parity=0,
queries=(1,),
):
self.heads = attention.W_UK_T.shape[0]
norms = attention.q_a_layernorm.weight, attention.kv_a_layernorm.weight
self.weights: tuple = (*norms, attention.q_b_proj.weight, attention.W_UK_T)
self.weights += (attention.W_UV, attention._q_scale_inv, attention._k_scale_inv)
self.device, self.pitches = attention.q_b_proj.weight.device, pitches
k_scale = attention._k_scale_float
softmax = attention.scale * attention._q_scale_float * k_scale
self.eps = cutlass.Float32(attention.rms_norm_eps)
self.stock_scales = softmax, k_scale
self.views = tuple(
to_cute(w, 4 if w.dtype == torch.float32 else 16) for w in self.weights
)
self.query_scratch, self.kernels = query_scratch, {}
shapes = [(b, q) for b in pitches for q in queries if b % q == 0]
self.cores = {(b, q): core_for(b, core, q) for b, q in shapes}
# The split count only depends on this bound; seq_lens bound the work.
self.max_seq = max_context or 1 << 17
self.dcp, self.parity = dcp, parity
if dcp is not None:
self.cores = dict.fromkeys(shapes, "tokenspeed")
self.max_seq = local_max(self.max_seq, dcp.size)
shape, bf16 = (max(pitches, default=0), self.heads, 512), torch.bfloat16
self.latent = torch.empty(shape, dtype=bf16, device=self.device)
def prepare_cache(self, kv):
specs = tuple(layout(w) for w in (*self.weights, kv.view(torch.uint8)))
for batch, q in self.cores:
device = self.device.index
dcp = self.dcp.size if self.dcp is not None else 0
sms = torch.cuda.get_device_properties(self.device).multi_processor_count
pitch = self.pitches[batch]
args = batch, self.heads, pitch, specs, device, dcp, sms, q
query = to_cute(self.query_scratch[:batch])
latent = to_cute(self.latent[:batch])
self.kernels[batch, q] = (*compile_core(*args), query, latent)
if self.dcp is not None:
self.prepare_dcp(batch)
if kv.is_cuda: # compile before graph capture
table = torch.zeros(batch, 1, dtype=torch.int32, device=kv.device)
ones = torch.full((batch,), q, dtype=torch.int32, device=kv.device)
self.stock_core(kv, table, ones, batch, q, ones)
return self
def prepare_dcp(self, batch):
"""(MlaQuery's publish arguments, DcpValue's mailbox arguments, parity and
rank) of one batch."""
box, p = self.dcp, self.parity
mailbox, ids = to_cute(box.q[p]), (cutlass.Int32(p), cutlass.Int32(box.rank))
rows = to_cute(box.rows[:batch]), to_cute(box.rows_lse[:batch], 4)
boxes = to_cute(box.partial[p]), to_cute(box.lse[p], 4)
peers = to_cute(box.peers, 8), to_cute(box.lse_peers, 8)
self.dcp_args = getattr(self, "dcp_args", {})
self.dcp_args[batch] = (mailbox, box.mc(p), ids[1]), (rows, boxes, peers), ids
def stock_core(self, cache, block_table, lengths, batch, q=1, totals=None):
"""tokenspeed_mla_decode or trtllm-gen into the BF16 latent (DCP: tokenspeed
for every rank's heads over the local shard into the DcpBox rows + LSE; q > 1
masks causally in global positions, ``totals``). tokenspeed: a fixed split
count per batch (one CTA pair per split filling the SMs); both PDL chained
between our kernels."""
box, reqs = self.dcp, batch // q
query = self.query_scratch[:batch].view(torch.float8_e4m3fn)
out, lse, cp = self.latent[:batch], None, {}
if box is not None:
query, out = box.q[self.parity, :batch].view(query.dtype), box.rows[:batch]
lse = box.rows_lse[:batch].view(reqs, q, -1)
if q > 1: # D*H heads: packed (row, head) tiles stream the KV once per tile
cp = dict(cp=(box.size, box.rank, box.interleave), totals=totals[:reqs])
cp.update(pack=True)
query, out = query.view(reqs, q, *query.shape[1:]), out.view(reqs, q, -1, 512)
args = cache, block_table[:reqs], lengths[:reqs], self.max_seq
if self.cores[batch, q] == "trtllm":
trtllm_decode(query, *args, self.stock_scales, out)
return
split = fill_splits(batch, self.device)
stock_decode(query, *args, self.stock_scales, out, lse, split, **cp)
def run(
self, projected, out, *, cache, slots, block_table, lengths, totals=None, q=1
):
"""``lengths``/``block_table``: this rank's KV (the local shard under DCP);
``totals``: under DCP, the requests' full lengths. ``q`` rows per request (spec
verify; ``slots`` per row). Rows of ``projected`` beyond the requests run as
empty requests (zero output)."""
batch, s = projected.shape[0], stream(self.device)
reqs = batch // q
block_table, lengths, totals = pad_requests(reqs, block_table, lengths, totals)
kv = to_cute(cache.view(torch.uint8))
mq, v, query, latent = self.kernels[batch, q]
qnorm, kvnorm, qb, uk, uv, qinv, kinv = self.views
q_slots, x = to_cute(slots[:batch], 8), to_cute(projected[:, :2112])
q_args = (x, qnorm, kvnorm, qb, uk, kv, q_slots, qinv, kinv, self.eps, query)
out_view, gate = to_cute(out), to_cute(projected[:, 2112:])
lens = to_cute(lengths[:reqs], 4)
if self.dcp is not None: # DcpValue zeroes padded requests (global lengths)
publish, ((rows, rows_lse), boxes, peers), ids = self.dcp_args[batch]
mq(*q_args, *publish, s)
self.stock_core(cache, block_table, lengths, batch, q, totals)
v_args = (rows, rows_lse, lens, *boxes, *peers, publish[0], uv, gate)
v(*v_args, to_cute(totals[:reqs], 4), out_view, *ids, s)
return out
mq(*q_args, s)
self.stock_core(cache, block_table, lengths, batch, q)
v(latent, uv, gate, lens, out_view, s)
return out
@@ -0,0 +1,474 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Cluster MLA query RMSNorm, Q_B projection, W_UK absorption and paged FP8 KV insert.
Static weights load before the PDL wait; current-token data is consumed after it."""
import cutlass
import cutlass.cute as cute
from cutlass import BFloat16, Float32, Int64
from ..common.primitives import (
alloc,
at,
bf16x2,
copy_evict,
copy_g2s,
copy_s2cluster,
float_bits,
fma,
fma2,
fp8_bits,
launch,
ldmatrix,
load_bf16x8,
load_volatile,
load_words,
mma_into,
mul2,
multicast_words,
prefetch_l2,
published,
query_norm_rsqrt,
smem_words,
store_fp8_pair,
store_shared_words,
unpack_bf16,
view,
wait,
warp_sum,
zeros,
)
KP = 1536 + 32 # padded Q_B / x row (elements): 3136 B, conflict-free 16 B fragments
# The smallest batch on the tiled kernel (per head count):
# its 16-request tiles beat FP32 FMAs from B4 at 12 heads and from B6 at 6 heads.
TILED_FROM = {12: 4, 6: 6}
Q_SENT = 0xFFFFFFFF # q mailbox sentinel: four FP8 0xFF bytes, never written
class MlaQuery:
"""A cluster splits one head's 192 Q_B rows; each CTA then absorbs its share of the
512 latent columns. Small batches (below TILED_FROM) are served by one CTA per
cluster part with FP32 FMAs; wider batches tile 16-64 requests per CTA (one wave
of 96 CTAs at B64 for both head counts) and use BF16 MMAs (FP32 accumulation).
``dcp`` (decode context parallel size; args then end with this parity's local q
mailbox, its multicast address and the rank): each CTA also multicasts the query
bytes it wrote into head block ``rank`` of every rank's Lamport q mailbox
(mla.dcp.DcpBox), then polls its share of the gathered mailbox until no word is
the sentinel, so the next kernel reads the gathered query."""
def __init__(
self,
heads: int,
batch: int,
*,
q_b_cp_async: bool = False,
dcp: int = 0,
):
tiled = TILED_FROM[heads]
self.heads, self.batch, self.wide = heads, batch, batch >= tiled
self.dcp = dcp
self.cluster = 16 if heads == 6 and not self.wide else 8
self.rows, self.latent = 192 // self.cluster, 512 // self.cluster
self.groups, self.p_per_group = 256 // self.latent, self.latent // 2
self.tokens = 1 if batch == 2 else min(batch, 8)
if self.wide:
self.tokens = 16 if batch <= 16 else 32 if batch <= 32 or heads < 8 else 64
self.grid = (self.cluster, heads, (batch + self.tokens - 1) // self.tokens)
carveout = 100 if batch > 1 else None
self.options = dict(cluster=self.cluster, preferred_smem_carveout=carveout)
self.q_b_cp_async = q_b_cp_async
@cute.jit
def __call__(self, *args):
kernel = self.tiled if self.wide else self.kernel
launch(kernel, args, self.grid, 256, **self.options)
@cute.jit
def live(self, token):
return cutlass.const_expr(not self.wide) or token < self.batch
@cute.jit
def publish(self, args, tok0, tid):
"""DCP: this CTA's query bytes (latent columns L*part.. as 16 B, the rope rows
of Q_B rows R*part.. as 4 B multicast stores) -> every rank's q mailbox (FP8
bytes are never 0xFF: no word is the sentinel)."""
output, (box_mc, rank) = args[10], args[12:14]
part, head, _ = cute.arch.block_idx()
R, L, D = self.rows, self.latent, self.dcp
chunks: cutlass.Constexpr = L // 16
items: cutlass.Constexpr = chunks + R // 4
cute.arch.sync_threads()
for i in range(tid, self.tokens * items, 256):
t, w = i // items, i % items
p = R * part + 4 * (w - chunks) # Q_B row of a rope word
row = ((tok0 + t) * D + rank) * self.heads + head
if w < chunks:
byte = L * part + 16 * w
if self.live(tok0 + t):
words = load_words(
at(output, tok0 + t, head, byte), count=4, mutable=True
)
multicast_words(box_mc + Int64(row) * 576 + byte, words)
elif self.live(tok0 + t) & (p >= 128):
byte = 512 + p - 128
word = load_words(
at(output, tok0 + t, head, byte), count=1, mutable=True
)
multicast_words(box_mc + Int64(row) * 576 + byte, word, kind="b32")
@cute.jit
def gathered(self, args, tid):
"""DCP: poll this CTA's share of the gathered mailbox (16 B chunks of the
B x DCP*H x 576 bytes) until every rank's bytes have landed."""
mailbox = args[11]
part, head, tile = cute.arch.block_idx()
threads: cutlass.Constexpr = self.grid[0] * self.grid[1] * self.grid[2] * 256
chunks: cutlass.Constexpr = self.batch * self.dcp * self.heads * 36
first = ((tile * self.heads + head) * self.cluster + part) * 256 + tid
last = chunks - 1 # threads past the end re-poll it
for i0 in cutlass.range_constexpr(0, -(-chunks // threads), 8):
n: cutlass.Constexpr = min(8, -(-chunks // threads) - i0)
sources = [
mailbox.iterator + min(first + (i0 + i) * threads, last) * 16
for i in range(n)
]
loads = [load_volatile(source, count=4) for source in sources]
for i in cutlass.range_constexpr(n): # the loads above are in flight
words = loads[i]
while not published(words, sentinel=Q_SENT):
words = load_volatile(sources[i], count=4)
@cute.kernel
def kernel(self, args):
qkv, w_qa, w_kva, w_qb, w_uk, cache, slots, q_scale_inv = args[0:8]
k_scale_inv, eps, output = args[8:11]
tid, _, _ = cute.arch.thread_idx()
part, head, tok0 = cute.arch.block_idx()
lane, warp = tid % 32, tid // 32
B, R, L, G = self.tokens, self.rows, self.latent, self.groups
smem = cutlass.utils.SmemAllocator()
qb = alloc(smem, BFloat16, (R, 1536), 128)
uk = alloc(smem, BFloat16, (128, L))
qa = alloc(smem, BFloat16, 1536)
xq = alloc(smem, BFloat16, (B, 1536))
qrow = alloc(smem, Float32, (R, B))
qn = alloc(smem, Float32, (B, 128))
red = alloc(smem, Float32, (G, B, L))
ss = alloc(smem, Float32, (8, max(B, 2)))
if cutlass.const_expr(not self.q_b_cp_async):
barw = smem.allocate(cutlass.Uint64, 8)
barq = smem.allocate(cutlass.Uint64, 8)
if tid == 0:
if cutlass.const_expr(not self.q_b_cp_async):
cute.arch.mbarrier_init(barw, 1)
cute.arch.mbarrier_init(barq, 1)
cute.arch.mbarrier_init_fence()
if cutlass.const_expr(not self.q_b_cp_async):
cute.arch.mbarrier_arrive_and_expect_tx(barw, R * 1536 * 2)
copy_evict(
qb.iterator,
w_qb.iterator + (head * 192 + part * R) * 1536,
size=R * 1536 * 2,
barrier=barw,
)
cute.arch.mbarrier_arrive_and_expect_tx(barq, B * 128 * 4)
cute.arch.cluster_arrive_relaxed()
for c in range(tid, 128 * L // 8, 256):
p, j = c // (L // 8), c % (L // 8)
copy_evict(uk.iterator + p * L + j * 8, at(w_uk, head, p, part * L + j * 8))
if tid < 192:
copy_evict(qa.iterator + tid * 8, w_qa.iterator + tid * 8)
cute.arch.cp_async_commit_group()
if cutlass.const_expr(self.q_b_cp_async):
for c in range(tid, R * 1536 // 8, 256):
dst = qb.iterator + c * 8
src = w_qb.iterator + (head * 192 + part * R) * 1536 + c * 8
copy_evict(dst, src)
cute.arch.cp_async_commit_group()
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
qsi = q_scale_inv[0]
xv = cute.make_rmem_tensor((B, 8), Float32)
for b in cutlass.range_constexpr(B):
total = Float32(0)
if (tid < 192) & self.live(tok0 + b):
values = load_bf16x8(at(qkv, tok0 + b, tid * 8), mutable=True)
for i in cutlass.range_constexpr(8):
xv[b, i] = values[i]
total = fma(values[i], values[i], total)
total = warp_sum(total)
if lane == 0:
ss[warp, b] = total
cute.arch.cp_async_wait_group(1 if self.q_b_cp_async else 0)
cute.arch.sync_threads()
if tid < 192:
weights = load_bf16x8(qa.iterator + tid * 8, shared=True)
for b in cutlass.range_constexpr(B):
total = Float32(0)
for w in cutlass.range_constexpr(8):
total += ss[w, b]
rr = query_norm_rsqrt(total, eps)
for i in cutlass.range_constexpr(8):
xq[b, tid * 8 + i] = BFloat16(xv[b, i] * rr * weights[i])
cute.arch.sync_threads()
if cutlass.const_expr(self.q_b_cp_async):
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
else:
wait(barw)
for row in range(warp, R, 8):
acc = zeros((B, 2), Float32)
for chunk in cutlass.range_constexpr(6):
k = (chunk * 32 + lane) * 8
weights = load_bf16x8(qb.iterator + row * 1536 + k, shared=True)
for b in cutlass.range_constexpr(B):
values = load_bf16x8(xq.iterator + b * 1536 + k, shared=True)
for i in cutlass.range_constexpr(8):
acc[b, i % 2] = fma(weights[i], values[i], acc[b, i % 2])
for b in cutlass.range_constexpr(B):
value = warp_sum(acc[b, 0] + acc[b, 1])
if lane == 0:
qrow[row, b] = Float32(BFloat16(value))
cute.arch.sync_threads()
cute.arch.cluster_wait()
nrows = min(R, max(0, 128 - R * part))
for i in range(tid, nrows * B * self.cluster, 256):
dst, jb = i % self.cluster, i // self.cluster
j, b = jb % nrows, jb // nrows
cute.arch.store_async_dsmem(
qn.iterator + b * 128 + R * part + j, float_bits(qrow[j, b]), barq, dst
)
for i in range(tid, R * B, 256):
j, b = i % R, i // R
p = R * part + j
if (p >= 128) & self.live(tok0 + b):
output[tok0 + b, head, 512 + p - 128] = fp8_bits(qrow[j, b] * qsi)
wait(barq)
latent_col, group = tid % L, tid // L
acc = zeros((B, 2), Float32)
for i in cutlass.range_constexpr(self.p_per_group):
p = group * self.p_per_group + i
w = Float32(uk[p, latent_col])
for b in cutlass.range_constexpr(B):
acc[b, i % 2] = fma(qn[b, p], w, acc[b, i % 2])
for b in cutlass.range_constexpr(B):
red[group, b, latent_col] = acc[b, 0] + acc[b, 1]
cute.arch.sync_threads()
for i in range(tid, L * B, 256):
b, ll = i // L, i % L
value = Float32(0)
for g in cutlass.range_constexpr(G):
value += red[g, b, ll]
if self.live(tok0 + b):
rounded = Float32(BFloat16(value))
output[tok0 + b, head, L * part + ll] = fp8_bits(rounded * qsi)
if cutlass.const_expr(self.dcp):
self.publish(args, tok0, tid)
valid = (head == 0) & (part < B) & self.live(tok0 + part)
self.insert(args, ss, tok0, part, valid, tid)
if cutlass.const_expr(self.dcp):
self.gathered(args, tid)
@cute.jit
def insert(self, args, ss, tok0, j, valid, tid):
"""RMSNorm the current latent KV row of request tok0 + j and write it, with
k_pe, to its FP8 cache slot."""
qkv, w_kva, (cache, slots), (k_scale_inv, eps) = (
args[0],
args[2],
args[5:7],
args[8:10],
)
lane, warp = tid % 32, tid // 32
slot = cutlass.Int64(-1)
if valid:
slot = slots[tok0 + j]
if slot >= 0:
ksi = k_scale_inv[0]
xx = cute.make_rmem_tensor(8, Float32)
total = Float32(0)
if tid < 64:
values = load_bf16x8(at(qkv, tok0 + j, 1536 + tid * 8), mutable=True)
for i in cutlass.range_constexpr(8):
xx[i] = values[i]
total = fma(values[i], values[i], total)
total = warp_sum(total)
cute.arch.sync_threads()
if (lane == 0) & (warp < 2):
ss[warp, 0] = total
cute.arch.sync_threads()
page, offset = slot // cache.shape[1], slot % cache.shape[1]
if tid < 64:
mean = fma(ss[0, 0] + ss[1, 0], Float32(1 / 512), eps)
rr = cute.math.rsqrt(mean, fastmath=True)
weights = load_bf16x8(w_kva.iterator + tid * 8)
for i in cutlass.range_constexpr(8):
normed = Float32(BFloat16(xx[i] * rr * weights[i]))
cache[page, offset, tid * 8 + i] = fp8_bits(normed * ksi)
elif tid < 72:
c = tid - 64
values = load_bf16x8(at(qkv, tok0 + j, 2048 + c * 8), mutable=True)
for i in cutlass.range_constexpr(8):
cache[page, offset, 512 + c * 8 + i] = fp8_bits(values[i] * ksi)
@cute.kernel
def tiled(self, args):
"""B>8: T requests per CTA. Q_B fragments load to registers before the PDL
wait; in-place RMSNorm of the staged rows, split-K Q_B MMAs over the warps,
nope rows gathered from the six producing CTAs by bulk DSMEM copies (into the
dead x rows), then W_UK MMAs (warp w owns latent columns 8w..8w+7)."""
qkv, w_qa, w_qb, w_uk, q_scale_inv = (args[i] for i in (0, 1, 3, 4, 7))
eps, output = args[9], args[10]
tid, _, _ = cute.arch.thread_idx()
part, head, tok0 = cute.arch.block_idx()
tok0 *= self.tokens
smem = cutlass.utils.SmemAllocator()
T, R, L = self.tokens, self.rows, self.latent
MT, senders = T // 16, (128 + R - 1) // R
lane, warp = tid % 32, tid // 32
group, quarter = lane // 4, lane % 4
xq = alloc(smem, BFloat16, (T, KP), 128)
uk = alloc(smem, BFloat16, (128, L + 8), 128) # 144 B rows for ldmatrix
qa = alloc(smem, BFloat16, 1536)
ss = alloc(smem, Float32, (8, 2))
barq = smem.allocate(cutlass.Uint64, 8)
# After the Q_B MMAs the x rows hold the split-K partials, then our nope rows
# and the six producers' (peers write them only after our cluster arrival).
red = view(xq.iterator, 0, Float32, (8, T * R // 32, 32), (T * R, 32, 1))
qblk = view(xq.iterator, 8 * T * R * 4, BFloat16, (T, R), (R, 1))
qn = view(xq.iterator, 9 * T * R * 4, BFloat16, (senders, T, R), (T * R, R, 1))
if tid == 0:
cute.arch.mbarrier_init(barq, 1)
cute.arch.mbarrier_init_fence()
cute.arch.mbarrier_arrive_and_expect_tx(barq, senders * T * R * 2)
# Split-K Q_B: warp w owns k = 192w..192w+191 (8 values per lane and 32-wide
# chunk; the x fragments use the same permutation). L2 now, registers later.
rows = head * 192 + part * R + group
qb_at = [
[
at(w_qb, rows + 8 * n, (6 * warp + cc) * 32 + 8 * quarter)
for n in range(3)
]
for cc in range(6)
]
for chunk in qb_at:
for address in chunk:
prefetch_l2(address)
for c in range(tid, 128 * L // 8, 256):
p, j = c // (L // 8), c % (L // 8)
dst = uk.iterator + p * (L + 8) + j * 8
copy_evict(dst, at(w_uk, head, p, part * L + j * 8))
if tid < 192:
copy_evict(qa.iterator + tid * 8, w_qa.iterator + tid * 8)
cute.arch.cp_async_commit_group()
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
for c in range(tid, T * 192, 256):
t, j = c // 192, c % 192
if self.live(tok0 + t):
copy_g2s(xq.iterator + t * KP + j * 8, at(qkv, tok0 + t, j * 8))
cute.arch.cp_async_commit_group()
qsi = q_scale_inv[0]
# The KV insert overlaps the staging of x (CTA j of the tile owns request j).
j = head * self.cluster + part
self.insert(args, ss, tok0, j, (j < T) & self.live(tok0 + j), tid)
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
# Packed FP32 pairs: the same IEEE ops in the same order as the scalar form.
gamma = []
for i in cutlass.range_constexpr(6):
gamma += list(load_bf16x8(qa.iterator + 256 * i + 8 * lane, shared=True))
for pair in cutlass.range_constexpr(T // 16): # requests warp + 8k, two at once
xrows = [xq.iterator + (warp + 16 * pair + 8 * h) * KP for h in range(2)]
values: list = []
rr: list = []
for h in cutlass.range_constexpr(2):
values.append([])
for i in cutlass.range_constexpr(6):
for word in smem_words(xrows[h] + 256 * i + 8 * lane):
values[h] += list(unpack_bf16(word))
for h in cutlass.range_constexpr(2):
zero = Float32(0), Float32(0)
chains = [zero, zero] # (0, 1), (2, 3): element e feeds chain e % 4
for e in cutlass.range_constexpr(0, 48, 2):
x = values[h][e], values[h][e + 1]
chains[e % 4 // 2] = fma2(x, x, chains[e % 4 // 2])
(c0, c1), (c2, c3) = chains
rr.append(query_norm_rsqrt(warp_sum((c0 + c1) + (c2 + c3)), eps))
for h in cutlass.range_constexpr(2):
for i in cutlass.range_constexpr(6):
words = []
for e in cutlass.range_constexpr(8 * i, 8 * i + 8, 2):
x = values[h][e], values[h][e + 1]
y = mul2(mul2(x, (rr[h], rr[h])), (gamma[e], gamma[e + 1]))
words.append(bf16x2(*y))
store_shared_words(xrows[h] + 256 * i + 8 * lane, tuple(words))
qbr = [[load_words(address, count=4) for address in chunk] for chunk in qb_at]
cute.arch.sync_threads()
acc = zeros((MT * 3, 4), Float32)
for cc in cutlass.range_constexpr(6):
k = (6 * warp + cc) * 32 + 8 * quarter
z = qbr[cc]
for m in cutlass.range_constexpr(MT):
x, y = (
load_words(xq.iterator + r * KP + k, count=4, shared=True)
for r in (16 * m + group, 16 * m + group + 8)
)
for n in cutlass.range_constexpr(3):
for s in cutlass.range_constexpr(2):
a = (x[2 * s], y[2 * s], x[2 * s + 1], y[2 * s + 1])
b = (z[n][2 * s], z[n][2 * s + 1])
mma_into(acc[m * 3 + n, None], a, b, bf16=True)
cute.arch.sync_threads()
cute.arch.cluster_arrive_relaxed() # x is consumed: peers may fill qn
for i in cutlass.range_constexpr(MT * 12):
red[warp, i, lane] = acc[i // 4, i % 4]
cute.arch.sync_threads()
for e in range(tid, T * R, 256):
i, ln = e // 32, e % 32
value = Float32(0)
for w in cutlass.range_constexpr(8):
value += red[w, i, ln]
value = Float32(BFloat16(value))
tile, c = i // 4, i % 4
t = 16 * (tile // 3) + ln // 4 + 8 * (c // 2)
r = 8 * (tile % 3) + 2 * (ln % 4) + c % 2
p = R * part + r
if p < 128:
qblk[t, r] = BFloat16(value)
elif self.live(tok0 + t):
output[tok0 + t, head, 512 + p - 128] = fp8_bits(value * qsi)
cute.arch.fence_proxy("async.shared", space="cta")
cute.arch.sync_threads()
cute.arch.cluster_wait()
if (part < senders) & (tid < self.cluster):
copy_s2cluster(
qn.iterator + part * T * R, qblk.iterator, barq, tid, size=T * R * 2
)
wait(barq)
cute.arch.cluster_arrive_relaxed() # peers may exit once qblk is delivered
b_frags = [
ldmatrix(uk.iterator + (32 * kp + lane) * (L + 8) + 8 * warp, trans=True)
for kp in range(4)
]
for m in cutlass.range_constexpr(MT):
out = zeros(4, Float32)
t = 16 * m + lane % 8 + 8 * ((lane // 8) % 2)
for ks in cutlass.range_constexpr(8):
p = 16 * ks + 8 * (lane // 16)
a = ldmatrix(qn.iterator + ((p // R) * T + t) * R + p % R)
b = b_frags[ks // 2][2 * (ks % 2) : 2 * (ks % 2) + 2]
mma_into(out, a, b, bf16=True)
for h in cutlass.range_constexpr(2): # rows group, group + 8: 2 bytes
row = 16 * m + group + 8 * h
if self.live(tok0 + row):
lo, hi = (Float32(BFloat16(out[2 * h + e])) * qsi for e in range(2))
col = L * part + 8 * warp + 2 * quarter
store_fp8_pair(at(output, tok0 + row, head, col), lo, hi)
if cutlass.const_expr(self.dcp):
self.publish(args, tok0, tid)
cute.arch.cluster_wait()
if cutlass.const_expr(self.dcp):
self.gathered(args, tid)
@@ -0,0 +1,95 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""MLA value projection (W_UV) and sigmoid gate of the BF16 attention latent."""
import cutlass
import cutlass.cute as cute
from cutlass import BFloat16, Float32, Uint32
from ..common.primitives import (
alloc,
at,
bf16x2,
copy_g2s,
launch,
load_words,
mma_into,
sigmoid,
store_words,
unpack_bf16,
zeros,
)
class MlaValue:
"""W_UV and the sigmoid gate, [B, H, 512] BF16 latent -> [B, H*128]. One
128-thread CTA per (32 outputs, head, 16 rows); warp w owns outputs
8w..8w+7 over the whole latent (BF16 MMA, FP32 accumulation). W_UV must have a
contiguous latent axis (the absorbed kv_b_proj view). Rows are tokens, ``q`` per
request (``lengths`` per request)."""
def __init__(self, heads: int, batch: int, q: int = 1):
self.heads, self.batch, self.q = heads, batch, q
self.grid = (4, heads, (batch + 15) // 16)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, self.grid, 128)
@cute.kernel
def kernel(self, args):
latent, w_uv, gate, lengths, output = args
rows = alloc(cutlass.utils.SmemAllocator(), BFloat16, (16, 544), 128)
tid, _, _ = cute.arch.thread_idx()
chunk, head, tile = cute.arch.block_idx()
lane, warp = tid % 32, tid // 32
group, quarter = lane // 4, lane % 4
t0, v = tile * 16, chunk * 32 + warp * 8 + group
# B fragments: 8 latent values per 32-wide chunk; A uses the same permutation.
wb = [
load_words(at(w_uv, head, 32 * c + 8 * quarter, v), count=4)
for c in range(16)
]
cute.arch.griddepcontrol_wait()
for c in range(tid, 16 * 64, 128):
r, j = c // 64, c % 64
if t0 + r < self.batch:
src = at(latent, t0 + r, head, j * 8)
copy_g2s(rows.iterator + r * 544 + j * 8, src)
cute.arch.cp_async_commit_group()
col = head * 128 + chunk * 32 + warp * 8 + 2 * quarter
gates = []
for r in cutlass.range_constexpr(2):
word = Uint32(0)
if t0 + group + 8 * r < self.batch:
word = load_words(
at(gate, t0 + group + 8 * r, col), count=1, mutable=True
)[0]
gates.append(unpack_bf16(word))
cute.arch.griddepcontrol_launch_dependents()
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
parts = zeros((2, 4), Float32) # two accumulation chains
for c in cutlass.range_constexpr(16):
x, y = (
load_words(
rows.iterator + r * 544 + 32 * c + 8 * quarter, count=4, shared=True
)
for r in (group, group + 8)
)
for s in cutlass.range_constexpr(2):
a = (x[2 * s], y[2 * s], x[2 * s + 1], y[2 * s + 1])
b = (wb[c][2 * s], wb[c][2 * s + 1])
mma_into(parts[s, None], a, b, bf16=True)
acc = [parts[0, k] + parts[1, k] for k in range(4)]
for r in cutlass.range_constexpr(2):
t = t0 + group + 8 * r
if t < self.batch:
# Padded rows may hold a NaN gate; keep inactive rows zero.
lo, hi = Float32(0), Float32(0)
if lengths[t // self.q] > 0:
lo, hi = (
Float32(BFloat16(acc[2 * r + e])) * sigmoid(gates[r][e], False)
for e in range(2)
)
store_words(at(output, t, col), (bf16x2(lo, hi),))
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Prepared first dense MLP, sharing projection and SiTU decode arithmetic."""
from functools import cache
import cutlass
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream
from ..common.host import architecture, dynamic_fake, dynamic_view, stream
from ..common.primitives import launch
from ..gemm.projection import PreparedProjection
from .shared import situ
class DenseSitu:
def __init__(self, hidden, padded):
self.hidden, self.padded = hidden, padded
@cute.jit
def __call__(self, *args):
grid = (cute.ceil_div(self.padded, 256), args[0].shape[0], 1)
launch(self.kernel, args, grid, 256)
@cute.kernel
def kernel(self, args):
x, out, beta, linear = args[0:4]
tile, row, _ = cute.arch.block_idx()
tid, _, _ = cute.arch.thread_idx()
col = tile * 256 + tid
cute.arch.griddepcontrol_wait()
if col < self.padded:
value = cutlass.Float32(0)
if col < self.hidden:
gate = cutlass.Float32(x[row, col])
up = cutlass.Float32(x[row, self.hidden + col])
value = situ(gate, up, beta, linear, native=True)
out[row, col] = cutlass.BFloat16(value)
cute.arch.griddepcontrol_launch_dependents()
@cache
def _compile_activation(hidden, padded, arch):
args = (dynamic_fake(2), dynamic_fake(2), *map(cutlass.Float32, (4, 25)))
kernel = DenseSitu(hidden, padded)
return cute.compile(kernel, *args, make_fake_stream(), options=f"--gpu-arch={arch}")
class PreparedDenseMLP:
"""Dense MLP; TP16 pads down K 2112 -> 2176 and the activation zero-fills it."""
def __init__(self, mlp, batches, reduce=None):
gate_up, down = mlp.gate_up_proj.weight, mlp.down_proj.weight
self.device, self.reduce = gate_up.device, reduce
self.hidden = hidden = gate_up.shape[0] // 2
self.padded = padded = (hidden + 127) // 128 * 128
linear = -1 if mlp.act_fn.linear_beta is None else mlp.act_fn.linear_beta
self.scalars = tuple(map(cutlass.Float32, (mlp.act_fn.beta, linear)))
if padded != hidden:
down = torch.nn.functional.pad(down.detach(), (0, padded - hidden))
self.gates = gate_up.new_empty((64, 2 * hidden))
self.hidden_rows = gate_up.new_empty((64, padded))
tile = {b: next(n for n in (8, 16, 32, 64) if b <= n) for b in batches}
up_tiles = {8: (64, 8, 4, 8), 16: (64, 16, 2, 8), 32: (64, 32, 2, 8)}
up_tiles[64] = (64, 64, 2, 5)
down_tiles = {8: (128, 8, 1, 6), 16: (128, 16, 1, 6), 32: (128, 32, 1, 5)}
down_tiles[64] = (64, 64, 1, 5)
policy = lambda tiles: {b: tiles[n] for b, n in tile.items()}
self.gate = PreparedProjection(gate_up, policy(up_tiles))
self.down = PreparedProjection(down, policy(down_tiles))
self.gate.prepare(), self.down.prepare()
self.activation = _compile_activation(hidden, padded, architecture(self.device))
def run(self, x, out):
gate, hidden = self.gates[: x.shape[0]], self.hidden_rows[: x.shape[0]]
self.gate.run(x, gate)
views = (dynamic_view(gate), dynamic_view(hidden))
self.activation(*views, *self.scalars, stream(self.device))
self.down.run(hidden, out)
if self.reduce is not None:
reduced = self.reduce(out)
if reduced.data_ptr() != out.data_ptr():
out.copy_(reduced)
return out
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,207 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Persistent routed experts for B<=64 (B>8: grouped units of <=8 tokens per expert)
over one shared core and model-owned workspace. The entry wait completes the
predecessor (incl. workspace rearm/counters) before shared state is read; output rows
keep the caller's top16 order."""
import functools
import cuda.bindings.driver as cuda
import cutlass as c
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import make_fake_stream
from ..common.host import check_residency, fake, stream, tensor_maps, to_cute
from ..common.policy import batch_table
from .experts import (
PLAN_BYTES,
ExpertsGeometry,
ExpertsGroupKernel,
ExpertsKernel,
ExpertsPlanKernel,
fc2_pairs,
)
def tensor(value, align=16):
return to_cute(value.reshape(-1), align)
def streamed(batch):
"""B<=8 on the incremental producer: ~1-1.5% at B4-8, neutral at B1-2, which keep
the previous producer."""
return bool(batch_table("4-8:1").get(batch, 0))
def pregrouped(tp, batch):
"""B>8 experts read the unit table a one-CTA kernel builds after routing, beside
the fused shared expert (hidden at B9-32; at B64 its ~2.3 us after routing outlast
the shared expert)."""
table = batch_table(PREGROUP[tp])
return batch > 8 and bool(table.get(batch, 0))
PREGROUP = {8: "9-32:1", 16: "9-32:1"}
# With the shared join skipped (moe.prepared.JOIN), TP8 B16 releases at entry.
EARLY = {8: "9-16:1", 16: ""}
def early_group(tp, batch):
"""Pregrouped batches whose unit-table kernel releases the experts at its entry."""
table = batch_table(EARLY[tp])
return pregrouped(tp, batch) and bool(table.get(batch, 0))
def stealing(batch):
"""B>8 experts balance the FC2(B) tail by claims."""
return batch > 8 and bool(batch_table("9-64:1").get(batch, 0))
@functools.cache
def compile_experts(tp, experts, batch, grid, device, publish=False, *knobs):
"""Only code and geometry are cached; no owner tensors or descriptors. ``knobs``:
(B<=8 stream, pregrouped, steal)."""
defaults = (streamed(batch), False, False)
stream, grouped, steal = knobs + defaults[len(knobs) :]
capacity = 1 << (batch - 1).bit_length()
moe = ExpertsKernel(tp, capacity, experts, grid, publish, stream, grouped, steal)
flat = lambda n, dtype=c.Uint8, align=16: fake((n,), (1,), dtype, align)
box = (c.Int64(0), c.Int32(0), c.Int32(0)) if publish else ()
if publish == "twoshot": # receive slots, owners' bases, parity x TP + rank
box = (fake((3 * tp, 64, 3584 // tp)), fake((tp,), None, c.Int64), c.Int32(0))
x, ids = fake((batch, 3584), (3584, 1)), flat(batch * 16, c.Int32)
x = flat(PLAN_BYTES) if moe.grouped else x # grouped: the MXFP8 plan
scales = (flat(experts * n) for n in (moe.w13_scale_bytes, moe.w2_scale_bytes))
out = flat(batch * 16 * (1 if publish else 3584), c.BFloat16)
maps = flat(768 if moe.paired else 256, align=64)
tensors = (x, ids, maps, *scales, flat(moe.workspace_bytes), out)
scalars = (c.Int32(batch), *map(c.Float32, (100, 0.25, 0.04)), *box)
with torch.accelerator.device_index(device):
return cute.compile(moe, *tensors, *scalars, make_fake_stream())
@functools.cache
def compile_group(tp, experts, batch, grid, device, early=False):
"""The pregrouped experts' unit-table kernel: (ids, workspace, batch); ``early``:
it releases the experts at entry."""
capacity = 1 << (batch - 1).bit_length()
kernel = ExpertsGroupKernel(tp, capacity, experts, grid, early=early)
flat = lambda n, dtype=c.Uint8: fake((n,), (1,), dtype, 16)
args = (flat(batch * 16, c.Int32), flat(kernel.workspace_bytes), c.Int32(batch))
with torch.accelerator.device_index(device):
return cute.compile(kernel, *args, make_fake_stream())
@functools.cache
def compile_plan(batch, device):
"""Plan writer for callers without the routing kernel's copy role in front."""
args = (fake((batch, 3584)), fake((PLAN_BYTES,), (1,), c.Uint8), make_fake_stream())
with torch.accelerator.device_index(device):
return cute.compile(ExpertsPlanKernel(batch), *args)
def scratch(tp, device):
"""Model-owned expert buffers: the workspace and the grouped plan (MXFP8 x, written
by the routing kernel or ``compile_plan``)."""
size, u8 = (ExpertsGeometry(tp).workspace_bytes, torch.uint8)
workspace = torch.full((size,), 255, device=device, dtype=u8)
plan = torch.empty(PLAN_BYTES, device=device, dtype=u8)
return {"workspace": workspace, "plan": plan}
class PersistentExperts:
def __init__(self, w13, s13, w2, s2, *, beta=4.0, linear_beta=25.0):
experts, rows, _ = w13.shape
self.tp, self.grid = (8, 136) if rows == 768 else (16, 152)
self.device, self.experts = w13.device, experts
self.weights = (w13, s13, w2, s2)
# TP8: B3-4 on 148 CTAs; B5-8 publish on 128-136 CTAs, leaving SMs for the
# one-wave tail; grouped B9-64 on 152.
grids = "3-4:148,5-6:128,7:132,9-64:152" if self.tp == 8 else ""
self.grids = batch_table(grids)
scalars = (beta * linear_beta, 1 / beta, 1 / linear_beta)
self.scalars = tuple(map(c.Float32, scalars))
# The experts kernel uses maps 0-1 (L2 promotion on w13); the TP8 B1 cluster
# experts use maps 2-4 (w13, w2 two-chunk and one-chunk boxes, promoted); TP8
# FC2 pairs (B4-64) use map 5 (w2 chunk 2 of 256 rows).
w2_tile = (192, 3, 64) if self.tp == 8 else (128, 2, 32)
specs = [(w13, rows, 1792, 28, 64, 2, True), (w2, 3584, *w2_tile, 2, False)]
if self.tp == 8:
specs += [specs[0]] + [(w2, 3584, 192, 3, 64, box, True) for box in (2, 1)]
dtype = cuda.CUtensorMapDataType.CU_TENSOR_MAP_DATA_TYPE_16U4_ALIGN16B
swizzle = cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_128B
l2 = cuda.CUtensorMapL2promotion
l2 = (l2.CU_TENSOR_MAP_L2_PROMOTION_NONE, l2.CU_TENSOR_MAP_L2_PROMOTION_L2_256B)
maps = [
(weight.data_ptr(), dtype, (128, experts * n, chunks), (row_bytes, stride))
+ ((128, 128, box), swizzle, l2[promote])
for weight, n, row_bytes, chunks, stride, box, promote in specs
]
if self.tp == 8:
shape = (128, experts * 3584, 3), (192, 64), (128, 256, 1)
maps.append((w2.data_ptr(), dtype, *shape, swizzle, l2[False]))
self.maps = tensor_maps(maps, self.device)
scales = (tensor(s.view(torch.uint8)) for s in (s13, s2))
self.views = (to_cute(self.maps[:256], 64), *scales)
self.all_maps = to_cute(self.maps, 64) # TP8 FC2 pairs: maps 0-5
self.prepared = {}
def prepare(
self, batch, buffers, out, publish=(), planned=False, twoshot=False, early=False
):
"""``publish``: (multicast, parity, rank) of the WaveMoeTail latent slot to
publish (``twoshot``: the ``common.twoshot`` push box); ``planned``: B>8 calls
find the plan written (by the routing kernel), else run writes it from x first;
``early``: the caller runs ``group`` (pregrouped batches) before ``run``."""
mode = "twoshot" if twoshot else bool(publish)
grouped = pregrouped(self.tp, batch)
args: tuple = (batch, self.grid_for(batch), self.device.index, mode)
args += (streamed(batch), grouped, stealing(batch))
code = compile_experts(self.tp, self.experts, *args)
# CTAs wait on other clusters' partials: the whole grid must be resident.
check_residency(code, self.device.index, args[1], 384, "Kimi K3 MoE experts")
ws, maps = tensor(buffers["workspace"]), self.views[0]
plan: tuple | None = None
capacity = 1 << (batch - 1).bit_length()
if fc2_pairs(self.tp, capacity, batch > 8 or streamed(batch)):
maps = self.all_maps
group = None
if batch > 8:
plan = (tensor(buffers["plan"]), compile_plan(batch, self.device.index))
plan = plan[:1] if planned else plan
if grouped:
release = early_group(self.tp, batch)
grouping = compile_group(self.tp, self.experts, *args[:3], release)
group = grouping, ws, early
self.prepared[batch] = (out, tensor(out), ws, code, publish, plan, maps, group)
def group(self, ids):
"""Pregrouped batches prepared ``early``: the unit-table kernel (run it on the
stream after routing; ``run`` then skips it)."""
group = self.prepared[ids.shape[0]][7]
if group is not None and group[2]:
self.launch_group(ids, group)
def launch_group(self, ids, group):
code, ws, _ = group
code(tensor(ids), ws, c.Int32(ids.shape[0]), stream(self.device))
def grid_for(self, batch):
return self.grids.get(batch, self.grid)
def run(self, x, ids, weights=None):
batch = x.shape[0]
out, output, workspace, code, box, plan, maps, group = self.prepared[batch]
output = tensor(weights) if box else output
source = to_cute(x, 16)
if plan is not None:
if len(plan) > 1:
plan[1](source, plan[0], stream(self.device))
source = plan[0]
if group is not None and not group[2]:
self.launch_group(ids, group)
args = (source, tensor(ids), maps, *self.views[1:], workspace, output)
code(*args, c.Int32(batch), *self.scalars, *box, stream(self.device))
return out
@@ -0,0 +1,538 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""MoE front: the weight-stationary [router shard; down shard] GEMM publishes logits
and the latent shard into a multicast mailbox row; the routing kernel polls all 896
logits, ranks the top16 (sigmoid + bias, renormalized) and Lamport-copies the latent."""
import functools
import cutlass as c
import cutlass.cute as cute
import torch
from cutlass import Int32, Uint32, Uint64
from cutlass.cute.runtime import make_fake_stream
from ..common.host import fake, stream, to_cute
from ..common.mailbox import poll, rearm
from ..common.policy import batch_table
from ..common.primitives import (
alloc,
asm,
bits_float,
div_rn,
float_bits,
launch,
redux,
sigmoid,
store_words,
view,
)
from ..gemm.projection import PreparedProjection
from ..gemm.ws_gemm import WsGemm
from .experts import PLAN_BYTES, plan_chunk
from .shared import compile_down, compile_shared, situ, weight_maps
from .tail_runtime import CAP
@asm(Int32)
def shared_add(address, value):
code = "atom.shared.add.u32 $0,[$1],$2;"
return code, "=r,r,r,~{memory}", (address, Int32(value))
@cute.jit
def ordered(score):
"""Monotone uint32 key of a float; NaN maps to 0 (below every number)."""
bits = Uint32(float_bits(score))
key = ~bits if (bits & Uint32(0x80000000)) != 0 else bits | Uint32(0x80000000)
return key if score == score else Uint32(0)
class RouteSitu:
def __init__(
self, batch, shared, renorm, early, roles, polled, plan=False
): # fmt: skip
# B>8: few registers, so routing CTAs co-reside with the front and the fused
# shared expert, and polled logits skip the front's completion wait.
self.lean = batch > 8
self.batch, self.shared, self.experts, self.renorm = batch, shared, 896, renorm
# early: release the persistent experts at entry (safe once down drains first);
# polled: logits arrive as FP32 words in mailbox columns [3584, 5376) (Lamport);
# plan: the copy role also writes the latent as MXFP8 into the experts' plan.
self.early, self.polled, self.plan = early, polled, plan
self.roles = tuple(r for r, on in zip(("route", "copy", "situ"), roles) if on)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, len(self.roles) * self.batch, 256)
@cute.kernel
def kernel(self, args):
logits, bias, ids, weights, mailbox, latent, gate_up, hidden = args[0:8]
scale, beta, linear_beta = args[8:11]
plan = args[11] if self.plan else None
tid, _, _ = cute.arch.thread_idx()
cta, _, _ = cute.arch.block_idx()
token, role = cta % self.batch, cta // self.batch
if c.const_expr(self.early):
cute.arch.griddepcontrol_launch_dependents()
for r in c.range_constexpr(len(self.roles)):
if role == r:
if c.const_expr(self.roles[r] == "route"):
self.route(logits, bias, ids, weights, scale, token, tid, mailbox)
elif c.const_expr(self.roles[r] == "copy"):
# Lamport: no dependency wait; the sentinel poll is the barrier.
base = mailbox.iterator + token * mailbox.shape[2] + tid * 8
if tid < 192:
self.copy(base, latent, token, tid, 2, plan)
else:
self.copy(base, latent, token, tid, 1, plan)
else:
cute.arch.griddepcontrol_wait()
rounds = (self.shared + 255) // 256 # issue every load first
pair = cute.make_rmem_tensor((rounds, 2), c.Float32)
for i in c.range_constexpr(rounds):
if tid + 256 * i < self.shared:
pair[i, 0] = gate_up[token, tid + 256 * i]
pair[i, 1] = gate_up[token, self.shared + tid + 256 * i]
for i in c.range_constexpr(rounds):
if tid + 256 * i < self.shared:
gate = c.Float32(c.BFloat16(pair[i, 0]))
up = c.Float32(c.BFloat16(pair[i, 1]))
value = situ(gate, up, beta, linear_beta)
hidden[token, tid + 256 * i] = c.BFloat16(value)
# Late release: the persistent experts must not take SMs from the down GEMM.
if c.const_expr(not self.early):
cute.arch.griddepcontrol_launch_dependents()
@cute.jit
def select(self, key, threshold, buffers):
"""B>8 candidates (keys at or above ``threshold``) append to ``dense`` at a
per-warp offset from the shared count, then rank themselves: 8 threads per
candidate (4 past 32 candidates) each count their share of the candidates above
it (key, then lower expert). Ranks below 16 store the expert and its sigmoid."""
dense, sig, chosen, picked, found = buffers
tid, _, _ = cute.arch.thread_idx()
lane = tid % 32
packed = cute.make_rmem_tensor(4, Uint64)
ballots = cute.make_rmem_tensor(4, Uint32)
total = Int32(0)
for i in c.range_constexpr(4): # packed 0: not a candidate
expert = self.expert(tid, i)
hit = (expert < self.experts) & (key[i] >= threshold)
ballots[i] = cute.arch.vote_ballot_sync(hit)
packed[i] = Uint64(0)
if hit:
packed[i] = (Uint64(key[i]) << 32) | Uint64(~Uint32(expert))
total += cute.arch.popc(ballots[i])
offset = Int32(0)
if (lane == 0) & (total > 0):
offset = shared_add(found.iterator, total)
offset = cute.arch.shuffle_sync(offset, 0)
below = (Uint32(1) << lane) - Uint32(1)
for i in c.range_constexpr(4):
if packed[i] != Uint64(0):
dense[offset + cute.arch.popc(ballots[i] & below)] = packed[i]
offset += cute.arch.popc(ballots[i])
cute.arch.sync_threads()
n = found[0]
if n <= 32: # usually: 8 threads per candidate, 4 slots each
part, index = tid % 8, tid // 8
mine, rank = Uint64(0), Int32(0)
if index < n:
mine = Uint64(dense[index])
for k in c.range_constexpr(4): # slots past n do not count
j = part + 8 * k
rank += Int32((j < n) & (Uint64(dense[j]) > mine))
for shift in c.range_constexpr(3):
rank += cute.arch.shuffle_sync_bfly(rank, 1 << shift)
if (part == 0) & (index < n) & (rank < 16):
expert = Int32(~Uint32(mine))
chosen[rank], picked[rank] = expert, sig[expert]
else:
part = tid % 4
for first in c.range(0, n, 64, unroll=1):
index = first + tid // 4
mine, rank = Uint64(0), Int32(0)
if index < n:
mine = Uint64(dense[index])
if n <= 64: # every load issues at once; slots past n do not count
for k in c.range_constexpr(16):
j = part + 4 * k
rank += Int32((j < n) & (Uint64(dense[j]) > mine))
else:
for j in c.range(part, n, 4, unroll=2):
rank += Int32(Uint64(dense[j]) > mine)
rank += cute.arch.shuffle_sync_bfly(rank, 1)
rank += cute.arch.shuffle_sync_bfly(rank, 2)
if (part == 0) & (index < n) & (rank < 16):
expert = Int32(~Uint32(mine))
chosen[rank], picked[rank] = expert, sig[expert]
cute.arch.sync_threads()
@cute.jit
def publish(self, chosen, picked, ids, weights, scale, token):
"""Warp 0: the 16 ids by rank and their sigmoids renormalized (lane r holds
rank r; the butterfly sums in the same order for every lane)."""
tid, _, _ = cute.arch.thread_idx()
lane = tid % 32
if tid < 32:
value = c.Float32(0)
if lane < 16:
ids[token, lane] = chosen[lane]
value = picked[lane]
total = value
for shift in c.range_constexpr(4):
total += cute.arch.shuffle_sync_bfly(total, 1 << shift)
if lane < 16:
value = value * scale
if c.const_expr(self.renorm):
value = div_rn(value, total + c.Float32(1e-20))
weights[token, lane] = c.BFloat16(value)
def expert(self, tid, i):
return tid * 4 + i if self.polled else tid + i * 256
@cute.jit
def copy(self, base, latent, token, tid, count: c.Constexpr[int], plan=None):
got = poll(base, 2048, count)
for i in c.range_constexpr(count):
dst = latent.iterator + token * latent.stride[0] + (tid + 256 * i) * 8
words = [got[i, k] for k in range(4)]
store_words(dst, words)
rearm(base + i * 2048)
if c.const_expr(plan is not None):
plan_chunk(plan, token, tid + 256 * i, words)
@cute.jit
def route(self, logits, bias, ids, weights, scale, token, tid, mailbox):
"""Exact top16 by threads [0, 256): candidates at or above the 16th largest
lane-group maximum are compacted and ranked by an unrolled 64-way compare."""
warp, lane = tid // 32, tid % 32
smem = c.utils.SmemAllocator()
km = alloc(smem, Uint32, (8, 32))
cand, dense = (alloc(smem, Uint64, shape) for shape in ((8, 128), 64))
sig = alloc(smem, c.Float32, 1024)
counts = alloc(smem, Int32, 8)
cutoff = alloc(smem, Uint32, 1)
chosen = alloc(smem, Int32, 16)
if c.const_expr(self.lean): # candidate count; the chosen experts' sigmoids
found, picked = alloc(smem, Int32, 1), alloc(smem, c.Float32, 16)
# Thread t owns experts t + 256 i, or 4 t + i when polling 16-byte chunks.
offsets = cute.make_rmem_tensor(4, c.Float32)
for i in c.range_constexpr(4): # bias is a weight: load before the wait
offsets[i] = c.Float32(0)
if self.expert(tid, i) < self.experts:
offsets[i] = bias[self.expert(tid, i)]
raw = cute.make_rmem_tensor(4, c.Float32)
key, top = cute.make_rmem_tensor(4, Uint32), Uint32(0)
for i in c.range_constexpr(4):
key[i] = Uint32(0)
if c.const_expr(self.polled):
if tid < 224:
cell = mailbox.iterator + token * mailbox.shape[2] + 3584 + tid * 8
got = poll(cell, 0, 1)
rearm(cell)
for i in c.range_constexpr(4):
raw[i] = bits_float(got[0, i])
if c.const_expr(self.lean): # all four experts: one block, no branches
scores = [sigmoid(raw[i]) for i in range(4)]
for i in c.range_constexpr(4):
sig[4 * tid + i] = scores[i]
key[i] = ordered(scores[i] + offsets[i])
if c.const_expr(not self.lean):
cute.arch.griddepcontrol_wait()
else:
cute.arch.griddepcontrol_wait()
for i in c.range_constexpr(4):
if self.expert(tid, i) < self.experts:
raw[i] = logits[token, self.expert(tid, i)]
for i in c.range_constexpr(4):
expert = self.expert(tid, i)
if c.const_expr(not (self.lean and self.polled)): # noqa: SIM102
if expert < self.experts:
score = sigmoid(raw[i])
sig[expert] = score
key[i] = ordered(score + offsets[i])
top = max(top, key[i])
km[warp, lane] = top
if c.const_expr(self.lean): # noqa: SIM102
if tid == 0:
found[0] = Int32(0)
cute.arch.sync_threads()
if warp == 0:
group = km[0, lane]
for w in c.range_constexpr(1, 8):
group = max(group, km[w, lane])
km[0, lane] = group
cute.arch.sync_warp()
rank = Int32(0)
for j in c.range_constexpr(32):
rank += Int32(km[0, j] > group)
low = redux(group if rank < 16 else Uint32(0xFFFFFFFF))
if lane == 0:
cutoff[0] = low
cute.arch.sync_threads()
if c.const_expr(self.lean):
dense = view(cand.iterator, 0, Uint64, (1024,))
self.select(key, cutoff[0], (dense, sig, chosen, picked, found))
self.publish(chosen, picked, ids, weights, scale, token)
return
threshold, offset = cutoff[0], Int32(0)
for i in c.range_constexpr(4):
expert = self.expert(tid, i)
hit = (expert < self.experts) & (key[i] >= threshold)
ballot = cute.arch.vote_ballot_sync(hit)
if hit:
below = cute.arch.popc(ballot & ((Uint32(1) << lane) - Uint32(1)))
packed = (Uint64(key[i]) << 32) | Uint64(~Uint32(expert))
cand[warp, offset + below] = packed
offset += cute.arch.popc(ballot)
if lane == 0:
counts[warp] = offset
cute.arch.sync_threads()
if warp == 0:
n = Int32(0)
for v in c.range_constexpr(8):
n += counts[v]
for half in c.range_constexpr(2): # compact the regions densely
index = lane + 32 * half
rest, region = Int32(index), Int32(0)
for v in c.range_constexpr(8):
if (region == v) & (rest >= counts[v]):
rest -= counts[v]
region += 1
value = Uint64(0)
if index < n:
value = cand[region, rest]
dense[index] = value
cute.arch.sync_warp()
if n <= 64:
for half in c.range_constexpr(2):
mine, rank = dense[lane + 32 * half], Int32(0)
for j in c.range_constexpr(64):
rank += Int32(dense[j] > mine)
if (lane + 32 * half < n) & (rank < 16):
chosen[rank] = Int32(~Uint32(mine))
else: # rare: rank every candidate against all regions
for q in range(lane, n, 32):
rest, region = Int32(q), Int32(0)
for v in c.range_constexpr(8):
if (region == v) & (rest >= counts[v]):
rest -= counts[v]
region += 1
mine, rank = cand[region, rest], Int32(0)
for v in c.range_constexpr(8):
for j in range(counts[v]):
rank += Int32(cand[v, j] > mine)
if rank < 16:
chosen[rank] = Int32(~Uint32(mine))
cute.arch.sync_warp()
expert, value = chosen[lane % 16], c.Float32(0)
if lane < 16:
ids[token, lane] = expert
value = sig[expert]
total = value
for shift in c.range_constexpr(4):
total += cute.arch.shuffle_sync_bfly(total, 1 << shift)
if lane < 16:
value = value * scale
if c.const_expr(self.renorm):
value = div_rn(value, total + c.Float32(1e-20))
weights[token, lane] = c.BFloat16(value)
# Front GEMM geometry "ks.kb.tg.rt.ch" per batch, by packed rows (TP8 560, TP16 288),
# from GB300 sweeps. ks: cluster K-split; kb: 32-wide
# K blocks per warp, so a row tile takes 224 / (ks * kb) warps (at most 16); tg: tokens
# per group (0: the single-pass path); rt: 16-row tiles per CTA; ch: accumulator chains
# per 8-token tile (0: 4 / tiles per group).
FRONT = {
560: "1-4:4.14.0.1.0,5-32:16.7.16.5.2,33-64:16.14.16.5.2",
288: "1-4:4.14.0.1.0,5-64:16.7.16.3.2",
}
WIDTH = 3584 + 2 * 896 # front mailbox row: BF16 latent, then FP32 logit words
# Batches whose shared expert is the fused kernel (``SharedUmma``), not the split-K
# chain; of those, split batches run gate/up + SiTU beside the front and the down rows
# (``SharedDown``) on SPLIT SMs beside the routed experts (0: fused).
FUSED, SPLIT = "9-64:1", "9-12:6,13-24:4,25-64:2"
# Per-TP overrides of SPLIT entries, (q = 1, verify steps): on real prompts (GB300
# SPEED-Bench 8k mixed) SharedDown on 2 SMs (~123 us at M32) outlasts the experts (TP8
# q=1 B32: 3 SMs, -5.4% step). Verify steps (q > 1) share experts among a request's q
# tokens: TP8 DL7 M16/32/64 -5.9/-12.9/-1.1%, TP16 M32 -5.9%.
SPLITS = {8: ("25-32:3", "13-16:6,25-32:4,33-64:3"), 16: ("", "25-32:3")}
# Split batches whose front GEMM rows join the gate/up kernel as extra M-tiles (one
# kernel instead of two that slow each other ~2x side by side).
MERGED = {8: "41-64:1", 16: "33-64:1"}
# Shared-expert split-K tactics (M tile, N tile, split, stages) by width (TP8 768, TP16
# 384): gate_up, then down.
SHARED_TACTICS = {
768: (
"1-8:128.8.4.6,9-16:64.16.4.10,17-32:64.32.4.8,33-64:64.64.4.5",
"1-8:128.8.1.6,9-16:64.16.1.6,17-32:64.32.1.6,33-64:64.64.1.6",
),
384: (
"1-16:64.8.4.8,17-32:64.16.4.10,33-64:64.32.4.8",
"1-8:128.8.1.3,9-16:64.16.1.3,17-32:64.32.1.3,33-64:64.64.1.3",
),
}
@functools.cache
def front_table(n):
return batch_table(FRONT.get(n, ""))
def front_ks(batch, n):
return front_table(n).get(batch, (4,))[0]
def front_geometry(batch, n, ks, kb=None, tg=None, rt=None, ch=None):
"""(rt, kq, staged, tg, ch) of the front GEMM over ``n`` rows. Unset arguments come
from the table when ``ks`` is its K-split, else from the single pass; token groups
whose x slice does not fit in SMEM fall back to the single pass."""
entry = front_table(n).get(batch, ())
table = entry[1:] if entry[:1] == (ks,) else (14, 0, 1, 0)
kb, tg, rt, ch = (t if v is None else v for v, t in zip((kb, tg, rt, ch), table))
kq = min(16, 224 // (ks * kb))
kb, nt = 224 // (ks * kq), -(-batch // 8)
staged = max(kb - 8, 0) if kq == 16 else 0
used = (
rt * kq * (nt * 544 + max(staged, 1) * 1024) + -(-nt * rt // ks) * ks * kq * 512
)
if nt * 8 * (7168 // ks * 2 + 64) > 220 * 1024 - used:
tg = 0
return rt, kq, staged, tg, ch
@functools.cache
def compile_route(batch, shared, renorm, roles, device, polled=False, plan=False):
kernel = RouteSitu(batch, shared, renorm, batch <= 2, roles, polled, plan)
f32 = c.Float32
args: tuple = (fake((batch, 896), None, f32), fake((896,), None, f32))
args += (fake((batch, 16), None, c.Int32), fake((batch, 16)), fake((1, 64, WIDTH)))
args += (fake((batch, 3584)), fake((batch, 2 * shared), None, f32))
args += (fake((batch, shared)), *(f32(v) for v in (1, 4, 25)))
args += (fake((PLAN_BYTES,), None, c.Uint8),) * plan + (make_fake_stream(),)
with torch.accelerator.device_index(device):
return cute.compile(kernel, *args)
@functools.cache
def compile_publish(n, split, valid, batch, device, ks, geometry=None):
"""Front GEMM that only publishes; routing then runs in the polled kernel."""
rt, kq, staged, tg, ch = geometry or front_geometry(batch, n, ks)
kernel = WsGemm(n, 7168, batch, rt, kq, staged, split, valid, ks, tg, ch)
args: tuple = (fake((batch, 7168)), fake((n, 7168)), fake((1, 64, WIDTH)))
args += (c.Int64(0), c.Int32(0), c.Int32(0), fake((batch, 896), None, c.Float32))
with torch.accelerator.device_index(device):
return cute.compile(kernel, *args, make_fake_stream())
class SharedFront:
def __init__(
self, moe, packed, box, storage, batches, beta, linear_beta, plan=None,
counters=None, q=1,
): # fmt: skip
"""``plan``: the experts' plan buffer, written by the routing kernel at B>8;
``counters``: the fused shared expert's model-owned barrier counters; ``q``: the
deployment's verify length (its SPLITS entries)."""
experts = moe.shared_experts
gate_up, down = experts.gate_up_proj.weight, experts.down_proj.weight
self.shared = shared = gate_up.shape[0] // 2
tp = 6144 // shared
shard, valid = 3584 // tp, 896 // tp
self.device, self.packed, self.storage = packed.device, to_cute(packed), storage
gu, dn = map(batch_table, SHARED_TACTICS[shared])
self.gate_up = PreparedProjection(
gate_up, {b: gu[b] for b in batches}, output_dtype=torch.float32
).prepare()
self.down = PreparedProjection(down, {b: dn[b] for b in batches}).prepare()
scale, renorm = moe.routed_scaling_factor, moe.moe_renormalize
self.scalars = tuple(map(c.Float32, (scale, beta, linear_beta)))
self.box: tuple = (to_cute(box.buffer), c.Int64(box.mc))
self.box += (c.Int32(box.rank * valid), c.Int32(box.rank * shard))
bias = to_cute(moe.gate.e_score_correction_bias)
dev, n = packed.device.index, packed.shape[0]
fused = batch_table(FUSED)
fused = {b for b in batches if b > 8 and counters is not None and fused.get(b)}
verify = q > 1
split = batch_table(f"{SPLIT},{SPLITS[tp][verify]}")
self.split = {b: split[b] for b in fused if split.get(b)} # down SMs
if fused: # TMA maps and counters of the fused kernel
self.maps = weight_maps(gate_up, down)
self.fused = to_cute(self.maps, 64), to_cute(counters)
merged = batch_table(MERGED[tp])
self.merged = {b for b in self.split if merged.get(b)}
if self.merged: # gate/up, down and front maps; unused RS arguments
self.merged_maps = weight_maps(gate_up, down, front=packed)
# Kept alive here: a CuTe view does not hold its tensor.
self.unused = torch.zeros(tp, dtype=torch.int64, device=packed.device)
self.merged_args = to_cute(self.merged_maps, 64), to_cute(self.unused)
geometry = (n, n - shard, valid, box.buffer.shape[-1])
self.plans: dict[int, tuple] = {}
for b in batches:
views = tuple(t[:b] for t in storage)
logits, gate, ids, weights, h, _, latent = map(to_cute, views)
front = compile_publish(n, n - shard, valid, b, dev, front_ks(b, n))
planned = plan is not None and b > 8
roles = (True, True, False)
route = compile_route(b, shared, renorm, roles, dev, True, planned)
situ = compile_route(b, shared, renorm, (False, False, True), dev)
args = (logits, bias, ids, weights, self.box[0], latent, gate, h)
extra = (to_cute(plan),) if planned else ()
code = None
if b in self.merged:
gate = compile_shared(tp, b, dev, True, geometry)
code = gate, compile_down(tp, b, dev, self.split[b])
elif b in self.split:
gate = compile_shared(tp, b, dev, gate=True)
code = gate, compile_down(tp, b, dev, self.split[b])
elif b in fused:
code = compile_shared(tp, b, dev)
self.plans[b] = views, front, route, situ, args + self.scalars, extra, code
def route(self, x):
"""Router logits + published down shard, then top16 and the latent copy (and
at B>8 the experts' MXFP8 plan). Merged batches publish them from the gate/up
kernel, which also leaves h for ``shared_expert``'s down rows."""
views, front, route, _, args, extra, code = self.plans[x.shape[0]]
s = stream(self.device)
if x.shape[0] in self.merged:
maps, unused = self.merged_args
work = (to_cute(x), maps, args[7], self.fused[1], unused)
work += (Int32(0), Int32(0), Int32(CAP), *self.scalars[1:], *self.box[1:])
code[0](*work, s)
else:
front(to_cute(x), self.packed, *self.box, args[0], s)
route(*args, *extra, s)
return views[2], views[3], views[6]
def merged_batch(self, batch):
return batch in self.merged
def planned(self, batch):
return bool(self.plans[batch][5])
def shared_expert(self, x, tail):
"""The shared expert's rows into the tail's RS mailboxes (``tail.push``); the
fused kernel peer-stores them itself and leaves the returned rows unwritten.
Returns (rows, the split's down launch to run beside the experts, or None)."""
views, _, _, situ, args, _, code = self.plans[x.shape[0]]
s = stream(self.device)
if code is not None:
maps, counters = self.fused
mailbox = (tail.peers, *tail.ring, Int32(CAP))
front = (to_cute(x), maps, args[7], counters, *mailbox, *self.scalars[1:])
if x.shape[0] not in self.split:
code(*front, s)
return views[5], None
gate, down = code
if x.shape[0] not in self.merged: # merged: h comes from ``route``
gate(*front, s)
return views[5], lambda: down(args[7], maps, *mailbox, stream(self.device))
self.gate_up.run(x, views[1])
situ(*args, s)
shared = self.down.run(views[4], views[5])
tail.push(shared)
return shared, None
@@ -0,0 +1,103 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Prepared MoE provider: one model owner serializes its scratch, no tensor pointers
are rebound at execution, and the side stream joins before the tail."""
import torch
from ..common.policy import batch_table
from .experts_runtime import scratch as expert_scratch
from .shared import scratch as shared_scratch
class MoEScratch:
def __init__(self, tp, device, batches):
self.tp, self.device, self.batches = tp, torch.device(device), tuple(batches)
cap, h = max(batches), 6144 // tp
with torch.accelerator.device_index(self.device.index):
self.stream = torch.cuda.Stream(device=self.device)
self.begin, self.shared_done = torch.cuda.Event(), torch.cuda.Event()
self.gate_done = torch.cuda.Event() # the split shared expert's front half
f32, i32, bf16 = torch.float32, torch.int32, torch.bfloat16
empty = lambda s, t=bf16: torch.empty(s, dtype=t, device=self.device)
self.latent = empty((cap, 3584))
self.front: tuple = (empty((cap, 896), f32), empty((cap, 2 * h), f32))
self.front += (empty((cap, 16), i32), empty((cap, 16)), empty((cap, h)))
self.front += (empty((cap, 7168)), self.latent)
self.expanded = empty((cap * 16, 3584))
self.experts = expert_scratch(tp, self.device)
# The experts' plan (MXFP8 x), written by the routing kernel at B>8.
self.plan = self.experts["plan"]
self.shared = shared_scratch(self.device) # fused shared-expert counters
self.views = {}
for b in batches:
self.views[b] = (self.latent[:b], self.expanded[: b * 16], self.experts)
# TP8 B16: the experts start beside the split gate/up half (+1% E2E).
JOIN = {8: "9-16:0", 16: ""}
class PreparedMoE:
def __init__(self, front, experts, tail, state, *, prefetch=None, layer_idx=None):
self.front, self.experts, self.tail, self.state = front, experts, tail, state
self.prefetch, self.layer_idx = prefetch, layer_idx
self.small_batch = None # optional small-batch chain
# Split batches whose routed experts launch beside the shared expert's
# gate/up half instead of after it.
self.join = batch_table(JOIN[state.tp])
self.sms = torch.cuda.get_device_properties(state.device).multi_processor_count
for b, sms in front.split.items(): # the split's down CTAs take the SMs left
experts.grids[b] = min(experts.grid_for(b), self.sms - sms)
for b in state.batches:
_, expanded, buffers = state.views[b]
publish, planned = tail.publish.get(b, ()), front.planned(b)
twoshot = b in tail.twoshots
experts.prepare(b, buffers, expanded, publish, planned, twoshot, True)
def run(self, x, out, *, consume=None):
state, b = self.state, x.shape[0]
if self.small_batch is not None and b in self.small_batch.batches:
return self.small_batch.run(out, consume)
main = torch.cuda.current_stream(state.device)
# The shared expert runs on the side stream beside the routed experts, or, when
# the persistent expert grid spans every SM, beside the front only. Split: its
# gate/up half beside the front, its down rows beside the experts.
before = self.experts.grid_for(b) >= self.sms
if self.front.merged_batch(b):
# One kernel publishes the routing logits and leaves h; the down rows then
# run beside the experts.
ids, weights, latent = self.front.route(x)
state.begin.record(main)
with torch.cuda.stream(state.stream):
state.stream.wait_event(state.begin)
shared, beside = self.front.shared_expert(x, self.tail)
beside()
state.shared_done.record(state.stream)
self.experts.group(ids)
return self.finish(b, out, latent, ids, weights, shared, consume)
state.begin.record(main)
with torch.cuda.stream(state.stream):
state.stream.wait_event(state.begin)
shared, beside = self.front.shared_expert(x, self.tail)
if beside is not None:
state.gate_done.record(state.stream)
beside()
state.shared_done.record(state.stream)
ids, weights, latent = self.front.route(x)
self.experts.group(ids) # pregrouped unit table, beside the shared expert
if before or (beside is not None and self.join.get(b, 1)):
main.wait_event(state.shared_done if before else state.gate_done)
return self.finish(b, out, latent, ids, weights, shared, consume, before)
def finish(self, b, out, latent, ids, weights, shared, consume, before=False):
"""The routed experts, the next layer's prefetch fork, then the tail once the
shared expert's side stream is done."""
main = torch.cuda.current_stream(self.state.device)
expanded = self.experts.run(latent, ids, weights)
if self.prefetch is not None:
self.prefetch.fork("next_attention", self.layer_idx, b)
if not before:
main.wait_event(self.state.shared_done)
return self.tail.run(expanded, weights, shared, out, consume=consume)
@@ -0,0 +1,254 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Top16 routing (threshold, regional candidates and exact composite ranking) and the
helpers the TP8 and TP16 small-batch fronts share."""
from typing import Any
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, Uint32, Uint64
from ..common.primitives import (
bits_float,
copy_evict,
float_bits,
gate_bf16_bits,
mailbox_load,
mailbox_store,
no_neg_zero,
published,
redux,
store_shared_words,
view,
zeros,
)
@cute.jit
def bf16_word(lo, hi):
return no_neg_zero(Uint32(gate_bf16_bits(lo)) | (Uint32(gate_bf16_bits(hi)) << 16))
@cute.jit
def poll_words(words, pointer, valid):
"""Copy four mailbox words into ``words``; clear ``valid`` if any is unpublished."""
values = mailbox_load(pointer, count=4)
for j in cutlass.range_constexpr(4):
words[j] = values[j]
valid = valid & (values[j] != Uint32(0x80000000))
return valid
@cute.jit
def gather_x(xmoe, par, cta, dst, first_chunk, active, tokens=1, h2=None):
"""Poll this CTA's x_moe rows (and the TP16 H2 slices if ``h2 = (warp, lane,
pairs)``) until published, then stage the row chunks at ``dst``."""
words = zeros((tokens, 2, 4), Uint32)
h2_words: Any = None
if cutlass.const_expr(h2 is not None):
h2_words = zeros((tokens, 4, 4), Uint32)
ready = False
while not ready:
valid = True
for token in cutlass.range_constexpr(tokens):
rows = (par * xmoe.shape[1] + cta % xmoe.shape[1]) * xmoe.shape[2]
base = xmoe.iterator + rows * 7168 + token * 7168
if active:
for chunk in cutlass.range_constexpr(2):
pointer = base + (first_chunk + 28 * chunk) * 8
valid = poll_words(words[token, chunk, None], pointer, valid)
if cutlass.const_expr(h2 is not None):
warp, lane, pairs = h2
if (warp < 7) & (pairs > 0):
for chunk in cutlass.range_constexpr(4):
pointer = base + (32 * warp + lane + 224 * chunk) * 8
valid = poll_words(h2_words[token, chunk, None], pointer, valid)
ready = cute.arch.vote_all_sync(valid)
if active:
for token in cutlass.range_constexpr(tokens):
for chunk in cutlass.range_constexpr(2):
pointer = dst + token * 14336 + (first_chunk + 28 * chunk) * 16
values = tuple(words[token, chunk, j] for j in range(4))
store_shared_words(pointer, values)
return words, h2_words
@cute.jit
def score_record(score, biased):
"""Published (score, score+bias) bits; NaN or 0x80000000 bits become (0, -inf)."""
score_bits, biased_bits = Uint32(float_bits(score)), Uint32(float_bits(biased))
sentinel = (score_bits == Uint32(0x80000000)) | (biased_bits == Uint32(0x80000000))
if sentinel | (score != score) | (biased != biased):
score_bits, biased_bits = Uint32(0), Uint32(0xFF800000)
return score_bits, biased_bits
@cute.jit
def stage_rows(slots, pitch, slot, weights, first, rows, barrier, skip=0):
"""Bulk-copy weight rows ``first + skip + i`` into slots ``slot + i``."""
cute.arch.mbarrier_arrive_and_expect_tx(barrier, rows * 14336)
for row in range(rows):
dst = slots + (slot + row) * pitch
src = weights.iterator + (first + skip + row) * 7168
copy_evict(dst, src, size=14336, barrier=barrier)
class Top16Fast:
"""Eight-warp top16 of 896 experts, lower ids winning ties. ``scores`` holds FP32
(score, score+bias) bits per [parity, token, expert]; ``run`` polls ``token``'s
parity-``par`` records until published and writes ``ids[16*token:]``. Runs on the
first 256 threads of the CTA with named barrier 1 and 13,200 bytes of SMEM."""
def __init__(self, batch):
self.batch = batch
@cute.jit
def arrays(self, storage):
km = view(storage, 0, Uint32, (8, 32), (32, 1))
gc = view(storage, 1024, Uint32, 32)
wc = view(storage, 1152, Uint32, 8)
threshold = view(storage, 1184, Uint32, 1)
cc = view(storage, 1200, Uint64, 904)
cs = view(storage, 8432, Uint32, 904)
cc2 = view(storage, 12048, Uint64, 64)
selected_scores = view(storage, 12560, Float32, 80)
selected_ids = view(storage, 12880, Int32, 80)
return km, gc, wc, threshold, cc, cs, cc2, selected_scores, selected_ids
@cute.jit
def rank_small(self, arrays, counts, n, lane, ids, groups: cutlass.Constexpr[int]):
cc, cs, cc2, selected_scores, selected_ids = arrays[4:]
mine = cute.make_rmem_tensor(groups, Uint64)
score = cute.make_rmem_tensor(groups, Uint32)
for half in cutlass.range_constexpr(groups):
index = Uint32(lane + 32 * half)
total = Uint32(0)
slot = index
for warp in cutlass.range_constexpr(7):
total += counts[warp]
past = index >= total
gap = (128 if warp < 4 else 96) - counts[warp]
slot += gap if past else Uint32(0)
slot = min(slot, Uint32(895))
mine[half] = cc[slot]
score[half] = cs[slot]
if index >= n:
mine[half] = Uint64(0)
cc2[index] = mine[half]
cute.arch.sync_warp()
for half in cutlass.range_constexpr(groups):
rank = zeros(4, Uint32)
for q in cutlass.range_constexpr(32 * groups):
rank[q % 4] += Uint32(cc2[q] > mine[half])
r = (rank[0] + rank[1]) + (rank[2] + rank[3])
index = Uint32(lane + 32 * half)
selected = (index < n) & (r < 16)
slot = r if selected else 16 + index
expert = Int32(Uint32(0xFFFFFFFF) - Uint32(mine[half]))
selected_ids[slot] = expert
selected_scores[slot] = bits_float(score[half])
if selected:
mailbox_store(ids + r, (Uint32(expert),))
@cute.jit
def run(self, scores, ids, storage, par, token):
tid, _, _ = cute.arch.thread_idx()
warp, lane = tid // 32, tid % 32
records = zeros((4, 2), Uint32)
pending = Uint32(0)
for i in cutlass.range_constexpr(4):
if warp + 8 * i < 28:
pending |= Uint32(1 << i)
while pending != 0:
for i in cutlass.range_constexpr(4):
if (pending & Uint32(1 << i)) != 0:
row = (par * self.batch + token) * 896 + lane
pointer = scores.iterator + (row + 32 * (warp + 8 * i)) * 2
words = mailbox_load(pointer, count=2)
records[i, 0], records[i, 1] = words[0], words[1]
for i in cutlass.range_constexpr(4):
done = (pending & Uint32(1 << i)) != 0
if published((records[i, j] for j in range(2)), done):
pending &= Uint32(0xFFFFFFFF ^ (1 << i))
cute.arch.barrier(barrier_id=1, number_of_threads=256)
ids = ids.iterator + token * 16
arrays = self.arrays(storage)
km, gc, wc, threshold, cc, cs, _, selected_scores, selected_ids = arrays
key = cute.make_rmem_tensor(4, Uint32)
maximum = Uint32(0)
for i in cutlass.range_constexpr(4):
biased = bits_float(records[i, 1])
bits = records[i, 1]
negative = (bits & Uint32(0x80000000)) != 0
ordered = ~bits if negative else bits | Uint32(0x80000000)
key[i] = ordered if (warp + 8 * i < 28) & (biased == biased) else Uint32(0)
maximum = max(maximum, key[i])
km[warp, lane] = maximum
cute.arch.barrier(barrier_id=1, number_of_threads=256)
if warp == 0:
group_max = km[0, lane]
for w in cutlass.range_constexpr(1, 8):
group_max = max(group_max, km[w, lane])
composite = (group_max & Uint32(0xFFFFFFE0)) | Uint32(31 - lane)
gc[lane] = composite
cute.arch.sync_warp()
ranks = zeros(4, Int32)
for q in cutlass.range_constexpr(32):
ranks[q % 4] += Int32(gc[q] > composite)
r = (ranks[0] + ranks[1]) + (ranks[2] + ranks[3])
least = redux(composite if r < 16 else Uint32(0xFFFFFFFF))
threshold[0] = least & Uint32(0xFFFFFFE0)
cute.arch.barrier(barrier_id=1, number_of_threads=256)
cutoff = threshold[0]
lane_mask = (Uint32(1) << lane) - Uint32(1)
region = Uint32(128 * warp if warp < 4 else 512 + 96 * (warp - 4))
offset = Uint32(0)
for i in cutlass.range_constexpr(4):
live = warp + 8 * i < 28
ballot = cute.arch.vote_ballot_sync(live & (key[i] >= cutoff))
candidate = ((ballot >> lane) & 1) != 0
compact = region + offset + cute.arch.popc(ballot & lane_mask)
position = compact if candidate else Uint32(896 + warp)
expert = Uint32(lane + 32 * (warp + 8 * i))
cc[position] = (Uint64(key[i]) << 32) | Uint64(Uint32(0xFFFFFFFF) - expert)
cs[position] = records[i, 0]
offset += cute.arch.popc(ballot)
wc[warp] = offset
cute.arch.barrier(barrier_id=1, number_of_threads=256)
if warp == 0:
counts = cute.make_rmem_tensor(8, Uint32)
for w in cutlass.range_constexpr(8):
counts[w] = wc[w]
n = ((counts[0] + counts[1]) + (counts[2] + counts[3])) + (
(counts[4] + counts[5]) + (counts[6] + counts[7])
)
if n <= 32:
self.rank_small(arrays, counts, n, lane, ids, 1)
elif n <= 64:
self.rank_small(arrays, counts, n, lane, ids, 2)
else:
prefix = cute.make_rmem_tensor(8, Uint32)
total = Uint32(0)
for w in cutlass.range_constexpr(8):
prefix[w] = total
total += counts[w]
for index in range(lane, n, 32):
slot = Uint32(index)
for w in cutlass.range_constexpr(7):
past = index >= prefix[w + 1]
gap = (128 if w < 4 else 96) - counts[w]
slot += gap if past else Uint32(0)
mine = cc[slot]
rank = Uint32(0)
for w in cutlass.range_constexpr(8):
base = 128 * w if w < 4 else 512 + 96 * (w - 4)
for q in range(counts[w]):
rank += Uint32(cc[base + q] > mine)
if rank < 16:
selected_expert = Int32(Uint32(0xFFFFFFFF) - Uint32(mine))
selected_ids[rank] = selected_expert
selected_scores[rank] = bits_float(cs[slot])
mailbox_store(ids + rank, (Uint32(selected_expert),))
cute.arch.sync_warp()
return selected_scores
@@ -0,0 +1,684 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Fused shared expert for B>8: gate/up, SiTU, down and the routed tail's reduce-scatter
push in one persistent tcgen05 kernel, synchronized by DSMEM and one global-memory
barrier instead of kernel bounds.
120 CTAs of 160 threads in clusters of ks = 120 / tiles (TP8 5, TP16 10). Loader warps
0-3 feed a 128B-swizzled ring of W [64, 64] (TMA) and x [N, 64] (cp.async) K blocks to
warp 4, which issues each block's four M=64 kind::f16 MMAs warp-uniformly
(``mma_bf16_block``). Gate/up M-tile m (32 gate rows and the matching 32 up rows)
belongs to cluster m, whose CTAs split its K. Each CTA pushes its FP32 partial rows
over DSMEM to the owner of their h columns (``width`` columns per owner); an owner
thread per (column, token) sums the parts in K (rank) order, rounds gate and up to BF16
for SiTU, and the owner stores h rows as vectors. After the grid's h barrier, CTAs
below 112 run 64 down rows (the first ring's worth of weights already staged) and
peer-store them, -0 made +0, into the tail's RS mailboxes. The last CTA through the
barrier re-arms the counters."""
import functools
import cuda.bindings.driver as cuda
import cutlass as c
import cutlass.cute as cute
import torch
from cutlass import Constexpr, Float32, Int32, Int64, Uint32
from cutlass.cute.runtime import make_fake_stream
from ..common.host import fake, tensor_maps
from ..common.primitives import (
acquire_tensormap,
alloc,
asm,
at,
bits_float,
commit_asm,
copy_g2s,
copy_s2cluster,
div_rn,
fence_tmem,
float_bits,
launch,
load_tma2,
load_tmem,
mma_bf16_block,
multicast_words,
named_barrier,
pack_pair,
push_float,
release_tmem,
sigmoid,
smem_desc,
smem_words,
split,
store_relaxed,
store_words,
thread,
view,
wait,
)
from ..gemm.ws_gemm import positive_zero, positive_zero32
tanh = functools.partial(cute.math.tanh, fastmath=False)
GRID, ROWS = 120, 112 # CTAs; CTAs with 64 down rows
# With the MoE front rows: the K split by TP and a deeper ring that still fits beside
# three o-proj consumer CTAs (<= 30 KB each; the kernel launches at the consumer's
# entry and loads its first ring meanwhile).
FRONT_KS, FRONT_RING = {8: 4, 16: 6}, {8: 112 * 1024, 16: 96 * 1024}
COUNTERS = 4 # h ready, exit
@cute.jit
def situ(gate, up, beta, linear_beta, *, native: c.Constexpr = False):
if c.const_expr(native):
inv_beta, half = div_rn(c.Float32(1), beta), c.Float32(0.5)
g = half * beta * tanh(gate * inv_beta) * (c.Float32(1) + tanh(gate * half))
if linear_beta > 0:
up = linear_beta * tanh(up * div_rn(c.Float32(1), linear_beta))
return g * up
g = beta * tanh(div_rn(gate, beta)) * sigmoid(gate)
if linear_beta > 0:
up = linear_beta * tanh(div_rn(up, linear_beta))
return g * up
@asm()
def prefetch_tma2(descriptor, column, row):
"""L2 prefetch of the 2D TMA box at (``column``, ``row``)."""
code = "cp.async.bulk.prefetch.tensor.2d.L2.global.tile [$0,{$1,$2}];"
return code, "l,r,r", (descriptor, Int32(column), Int32(row))
@asm()
def release_count(address):
return "red.release.gpu.global.add.u32 [$0],1;", "l,~{memory}", (address,)
@asm(Uint32)
def acquire_count(address):
return "ld.acquire.gpu.global.u32 $0,[$1];", "=r,l,~{memory}", (address,)
@asm(Uint32)
def fetch_count(address):
return "atom.relaxed.gpu.global.add.u32 $0,[$1],1;", "=r,l,~{memory}", (address,)
@cute.jit
def wait_count(address, target):
seen = Int32(acquire_count(address))
while seen < target:
seen = Int32(acquire_count(address))
def weight_maps(gate_up, down, front=None):
"""TMA maps (W [64, 32] boxes, 128B swizzle) of gate/up, down, then (optional) the
MoE front GEMM's packed router / latent rows."""
dtype = cuda.CUtensorMapDataType.CU_TENSOR_MAP_DATA_TYPE_BFLOAT16
swizzle = cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_128B
l2 = cuda.CUtensorMapL2promotion.CU_TENSOR_MAP_L2_PROMOTION_L2_256B
weights = (gate_up, down) if front is None else (gate_up, down, front)
specs = [
(w.data_ptr(), dtype, (w.shape[1], w.shape[0]), (2 * w.stride(0),))
+ ((64, 32), swizzle, l2)
for w in weights
]
return tensor_maps(specs, gate_up.device)
class SharedBase:
"""What the fused kernel and the split down kernel share: geometry, TMA weight and
cp.async row tiles, the M=64 accumulator readout and the RS push of 64 rows."""
def __init__(self, tp, batch):
self.tp, self.batch, self.hidden = tp, batch, 6144 // tp
self.down = self.hidden // 64 # down K blocks
self.n = (batch + 7) // 8 * 8
self.columns = max(32, 1 << (self.n - 1).bit_length()) # one accumulator
@cute.jit
def load_weight(self, tile, descriptor, barrier, row0, row1, column):
"""W rows [row0, + 32) and [row1, + 32), 64 columns from ``column``, as a
K-major 128B-swizzled [64, 64] tile (two TMA boxes)."""
cute.arch.mbarrier_arrive_and_expect_tx(barrier, 8192)
load_tma2(tile, descriptor, barrier, column, row0)
load_tma2(tile + 4096, descriptor, barrier, column, row1)
@cute.jit
def load_rows(self, tile, source, column):
"""Rows [0, batch) of ``source``, 64 columns from ``column``, as the B tile
(threads [0, 128))."""
tid, _, _, _ = thread()
count: Constexpr = self.batch * 8
for i in c.range_constexpr((count + 127) // 128):
item = tid + 128 * i
row, chunk = item // 8, item % 8
dst = tile + row // 8 * 1024 + row % 8 * 128 + (chunk ^ row % 8) * 16
if c.const_expr((i + 1) * 128 <= count):
copy_g2s(dst, at(source, row, column + chunk * 8))
else:
if item < count: # noqa: SIM102
copy_g2s(dst, at(source, row, column + chunk * 8))
@cute.jit
def accumulated(self, tmem):
"""This lane's accumulator row (M=64: rows 16 warp + lane, lane < 16); later
MMAs may then reuse the columns."""
_, warp, _, _ = thread()
values = load_tmem(tmem + Uint32(32 * warp << 16), self.columns)
fence_tmem(before=True)
return values
@cute.jit
def elected(self, barrier):
with cute.arch.elect_one():
commit_asm(barrier)
@cute.jit
def push(self, mailbox, staging, values, row0):
"""Down rows [row0, + 64) (``values`` from ``accumulated``) -> FP32 [token,
row] tile at ``staging`` -> BF16 (-0 made +0) 16-byte peer stores into the RS
mailbox slot; ``mailbox`` is (peers, parity, rank, capacity). Warps 0-3."""
peers, parity, rank, capacity = mailbox
tid, warp, lane, _ = thread()
tile = view(staging, 0, Float32, (self.n, 68), (68, 1))
if lane < 16:
for t in c.range_constexpr(self.batch):
tile[t, 16 * warp + lane] = values[t]
named_barrier(1, 128)
shard: Constexpr = 7168 // self.tp
peer, column = row0 // shard, row0 % shard
count: Constexpr = self.batch * 8
for i in c.range_constexpr((count + 127) // 128):
item = tid + 128 * i
if item < count:
t, part = item // 8, item % 8
reduced = []
for half in c.range_constexpr(2):
words = smem_words(at(tile, t, part * 8 + half * 4))
reduced += [bits_float(w) for w in words]
packed = tuple(
pack_pair(reduced[2 * v], reduced[2 * v + 1], policy="latent")
for v in range(4)
)
row = (parity * self.tp + rank) * capacity + t
offset = Int64(row * shard + column + part * 8)
store_relaxed(peers[peer] + offset * 2, packed)
named_barrier(1, 128)
class SharedUmma(SharedBase):
def __init__(self, tp, batch, budget=80 * 1024, gate=False, front=None):
super().__init__(tp, batch)
# gate: the split variant's front-window half (gate/up and SiTU into h; the
# down rows run in ``SharedDown`` beside the routed experts). front: (rows,
# split, valid, mailbox row) of the MoE front GEMM, whose packed router /
# latent rows join as extra M-tiles (gate mode, on the main stream after the
# o-proj consumer: the first ring's weights load before the dependency wait).
self.gate, self.front = (gate or front is not None, front)
self.tiles = 2 * self.hidden // 64 # gate/up M-tiles, one per cluster
self.grid = GRID
if front is not None:
self.gtiles = self.tiles
self.tiles += -(-front[0] // 64)
self.grid = FRONT_KS[tp] * self.tiles
self.ks = self.grid // self.tiles # cluster size: the K split
self.blocks = -(-112 // self.ks) # gate/up K blocks per CTA, at most
self.stage = 8192 + self.n * 128
later = 0 if self.gate else self.down
self.stages = min(self.blocks + later, budget // self.stage)
# Owners 0 .. 32 / width - 1 take a tile's h columns ``width`` at a time.
self.width = 8 if self.ks < 8 else 4
self.owners = 32 // self.width
self.received = self.ks * 2 * self.width * self.n * 4 # an owner's bytes
@cute.jit
def __call__(self, *args):
options = dict(pdl=self.front is not None, min_blocks_per_mp=1)
launch(self.kernel, args, self.grid, 160, self.ks, **options)
@cute.kernel
def kernel(self, args):
maps = args[1]
tid, warp, lane, cta = thread()
rank = cta % self.ks
stages, stage = self.stages, self.stage
smem = c.utils.SmemAllocator()
ring = alloc(smem, c.Uint8, stages * stage, 1024).iterator
received = alloc(smem, Float32, self.received // 4, 16).iterator
staged = alloc(smem, c.BFloat16, self.n * self.width, 16) # owned h rows
if c.const_expr(self.front is not None): # owned front rows [token, half]
staged = (staged, alloc(smem, Float32, self.n * 2 * (self.width + 1), 16))
bars = alloc(smem, c.Uint64, 2 * stages + 3, 8).iterator
slot = alloc(smem, Uint32, 1)
if tid == 0:
for s in c.range_constexpr(stages):
cute.arch.mbarrier_init(bars + s, 129) # TMA + 128 cp.async arrivals
cute.arch.mbarrier_init(bars + stages + s, 1)
for i in c.range_constexpr(3):
cute.arch.mbarrier_init(bars + 2 * stages + i, 1)
cute.arch.mbarrier_init_fence()
# Every rank's gate and up partials of the owned columns.
nbytes = self.received if rank < self.owners else 0
cute.arch.mbarrier_arrive_and_expect_tx(bars + 2 * stages + 2, nbytes)
acquire_tensormap(maps.iterator)
acquire_tensormap(maps.iterator + 128)
if c.const_expr(self.front is not None):
acquire_tensormap(maps.iterator + 256)
if warp == 4:
cute.arch.alloc_tmem(self.columns, slot.iterator)
cute.arch.relinquish_tmem_alloc_permit()
fence_tmem(before=True)
cute.arch.sync_threads()
fence_tmem()
cute.arch.cluster_arrive_relaxed()
cute.arch.cluster_wait() # every owner's receive barrier is armed
tmem = slot[0]
if c.const_expr(self.front is not None):
# Weights do not depend on the predecessor: the first ring's worth.
if tid == 0:
m, k0, q = self.span(cta)
for kb in c.range_constexpr(stages):
if kb < q:
self.load_tile(ring + kb * stage, maps, bars + kb, m, k0 + kb)
# The rest of the front tiles' boxes (on the routing path) into L2.
if m >= self.gtiles:
for kb in c.range(stages, q, unroll=1):
self.prefetch_tile(maps, m, k0 + kb)
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
if warp < 4:
self.stream(args, ring, (received, staged), bars, tmem, cta)
else:
self.multiply(ring, bars, tmem, cta)
release_tmem(tmem, self.columns, warp, 4)
cute.arch.cluster_arrive_relaxed()
cute.arch.cluster_wait()
@cute.jit
def load_tile(self, tile, maps, barrier, m, k):
"""K block ``k`` of M-tile ``m``: 32 gate and the matching 32 up rows, or (front
tiles past the gate/up ones) 64 consecutive front rows."""
if m < self.gtiles:
rows = (32 * m, self.hidden + 32 * m)
self.load_weight(tile, maps.iterator, barrier, *rows, 64 * k)
else:
row = 64 * (m - self.gtiles)
self.load_weight(tile, maps.iterator + 256, barrier, row, row + 32, 64 * k)
@cute.jit
def prefetch_tile(self, maps, m, k):
"""L2 prefetch of ``load_tile``'s two boxes."""
if m < self.gtiles:
prefetch_tma2(maps.iterator, 64 * k, 32 * m)
prefetch_tma2(maps.iterator, 64 * k, self.hidden + 32 * m)
else:
row = 64 * (m - self.gtiles)
prefetch_tma2(maps.iterator + 256, 64 * k, row)
prefetch_tma2(maps.iterator + 256, 64 * k, row + 32)
@cute.jit
def span(self, cta):
"""(tile, first K block, K blocks) of the CTA's gate/up work."""
k0, count = split(112, cta % self.ks, self.ks)
return cta // self.ks, k0, count
@cute.jit
def multiply(self, ring, bars, tmem, cta):
"""Warp 4: every K block's four K16 MMAs and its stage release; gate/up
completes barrier 0 of three, down (the same columns) barrier 1."""
stages, q = self.stages, self.span(cta)[2]
total = q
if c.const_expr(not self.gate):
total = q + (self.down if cta < ROWS else 0)
instruction: Constexpr = 1 << 4 | 1 << 7 | 1 << 10 | self.n >> 3 << 17 | 4 << 24
done = bars + 2 * stages
for kb in c.range(total, unroll=1):
s = kb % stages
wait(bars + s, kb // stages % 2, scope="cta")
cute.arch.fence_proxy("async.shared", space="cta")
fence_tmem()
tile = ring + s * self.stage
a, b = smem_desc(tile, True), smem_desc(tile + 8192, True)
index = kb - q # block index within the product
if kb < q:
index = kb
release = Int32(kb + stages < total)
mma_bf16_block(tmem, a, b, instruction, index, bars + stages + s, release)
if kb == q - 1:
self.elected(done)
if c.const_expr(not self.gate): # noqa: SIM102
if cta < ROWS:
self.elected(done + 1)
@cute.jit
def stream(self, args, ring, buffers, bars, tmem, cta):
"""Loader warps: gate/up blocks, the first down weights, the partial push,
owned h columns, the h barrier, then the down blocks' h (and remaining
weights) and the push."""
x, maps, h, counters = args[0:4]
stages, stage, hidden = self.stages, self.stage, self.hidden
tid, _, _, _ = thread()
m, k0, q = self.span(cta)
gate_up, down = maps.iterator, maps.iterator + 128
for kb in c.range(q, unroll=1):
s = kb % stages
tile = ring + s * stage
if kb >= stages:
wait(bars + stages + s, (kb // stages - 1) % 2, scope="cta")
k = k0 + kb
if c.const_expr(self.front is not None):
if (tid == 0) & (kb >= stages): # the first ring went before the wait
self.load_tile(tile, maps, bars + s, m, k)
elif tid == 0:
rows = (32 * m, hidden + 32 * m)
self.load_weight(tile, gate_up, bars + s, *rows, 64 * k)
self.load_rows(tile + 8192, x, 64 * k)
cute.arch.cp_async_mbarrier_arrive_noinc(bars + s)
received, staged = buffers
if c.const_expr(self.gate):
self.partial(received, bars, tmem, cta, ring)
self.reduce(args, received, staged, bars, cta)
return
early = min(self.down, stages)
row0 = 64 * cta
if cta < ROWS:
for j in c.range(early, unroll=1):
s = (q + j) % stages
if q + j >= stages:
wait(bars + stages + s, ((q + j) // stages - 1) % 2, scope="cta")
if tid == 0:
tile = ring + s * stage
self.load_weight(tile, down, bars + s, row0, row0 + 32, 64 * j)
self.partial(received, bars, tmem, cta)
self.reduce(args, received, staged, bars, cta)
if tid == 0:
wait_count(counters.iterator, GRID)
if fetch_count(counters.iterator + 1) == GRID - 1:
counters[0], counters[1] = Int32(0), Int32(0)
named_barrier(1, 128)
if cta < ROWS:
for j in c.range(self.down, unroll=1):
s = (q + j) % stages
tile = ring + s * stage
if j >= early:
wait(bars + stages + s, ((q + j) // stages - 1) % 2, scope="cta")
if tid == 0:
self.load_weight(tile, down, bars + s, row0, row0 + 32, 64 * j)
self.load_rows(tile + 8192, h, 64 * j)
cute.arch.cp_async_mbarrier_arrive_noinc(bars + s)
wait(bars + 2 * self.stages + 1, 0, scope="cta")
fence_tmem()
self.push(args[4:8], ring, self.accumulated(tmem), row0)
@cute.jit
def partial(self, received, bars, tmem, cta, ring=None):
"""Push the CTA's FP32 gate/up rows to their h columns' owners: row r (gate
r, or up r - 32) of column g = r % 32 lands in owner g // width's slot
[rank, r // 32, g % width]. With the front rows, the slots are staged in the
drained ring and each owner's leaves in one DSMEM bulk copy."""
tid, warp, lane, _ = thread()
rank = cta % self.ks
wait(bars + 2 * self.stages, 0, scope="cta")
fence_tmem()
values = self.accumulated(tmem)
if c.const_expr(self.front is not None):
share: Constexpr = 2 * self.width * self.n # one owner's floats
if lane < 16:
row = 16 * warp + lane
g = row % 32
owner = g // self.width
cell = ((owner * 2 + row // 32) * self.width + g % self.width) * self.n
for v in c.range_constexpr(self.n // 4):
words = tuple(float_bits(values[4 * v + i]) for i in range(4))
at_v = cell + self.slot(4 * v, g % self.width)
store_words(ring + at_v * 4, words, shared=True)
cute.arch.fence_proxy("async.shared", space="cta")
named_barrier(1, 128)
if tid < self.owners:
arrived = bars + 2 * self.stages + 2
source = ring + tid * share * 4
dst = received + rank * share
copy_s2cluster(dst, source, arrived, tid, size=share * 4)
return
if lane < 16:
row = 16 * warp + lane
g = row % 32
cell = ((rank * 2 + row // 32) * self.width + g % self.width) * self.n
arrived = bars + 2 * self.stages + 2 # the owner's receive barrier
for v in c.range_constexpr(self.n // 4):
chunk = tuple(values[4 * v + i] for i in range(4))
push_float(received + cell + 4 * v, chunk, arrived, g // self.width)
@cute.jit
def slot(self, t, j):
"""Token ``t``'s place in a partial row of column ``j``: with the front rows
the row's 16-byte chunks rotate by ``j`` (conflict-free staging stores)."""
if c.const_expr(self.front is None):
return t
chunks: Constexpr = self.n // 4
return (t // 4 + j) % chunks * 4 + t % 4
@cute.jit
def reduce(self, args, received, staged, bars, cta):
"""Owner: a thread per (column, token) sums the parts in rank order and
applies SiTU; the rows leave in ``width``-column vectors. Then the CTA counts
into the h barrier (fused only)."""
h, counters = args[2:4]
beta, linear_beta = args[8:10]
tid, _, _, _ = thread()
m, rank = cta // self.ks, cta % self.ks
wait(bars + 2 * self.stages + 2, 0) # peers' DSMEM stores
parts = view(received, 0, Float32, self.received // 4)
if c.const_expr(self.front is None):
if rank < self.owners:
self.owned_h(h, parts, staged, m, rank, beta, linear_beta)
else:
staged, rows = staged
if rank < self.owners:
if m < self.gtiles:
self.owned_h(h, parts, staged, m, rank, beta, linear_beta)
else:
self.publish(args, parts, rows, m - self.gtiles, rank)
named_barrier(1, 128)
if c.const_expr(not self.gate): # noqa: SIM102
if tid == 0:
cute.arch.fence_acq_rel_gpu()
release_count(counters.iterator)
@cute.jit
def owned_h(self, h, parts, staged, m, rank, beta, linear_beta):
"""Owner of gate/up tile ``m``: SiTU of the summed parts into ``width``-column
h vectors."""
tid, _, _, _ = thread()
batch, width = self.batch, self.width
for i in c.range_constexpr((width * batch + 127) // 128):
item = tid + 128 * i
if item < width * batch:
j, t = item // batch, item % batch
sums = [Float32(0), Float32(0)]
for source in c.range_constexpr(self.ks):
for half in c.range_constexpr(2):
cell = (source * 2 + half) * width + j
sums[half] = sums[half] + parts[cell * self.n + self.slot(t, j)]
gate = Float32(c.BFloat16(sums[0]))
up = Float32(c.BFloat16(sums[1]))
value = c.BFloat16(situ(gate, up, beta, linear_beta))
staged[t * width + j] = value
named_barrier(1, 128)
if tid < batch:
row = staged.iterator + tid * width
words = smem_words(row) if width == 8 else smem_words(row, count=2)
store_words(at(h, tid, 32 * m + width * rank), words)
@cute.jit
def publish(self, args, parts, rows, f, rank):
"""Owner of front tile ``f``: sums each (row, token) in rank order, then
publishes ``width``-row vectors like the front GEMM: FP32 router logits (rows
< valid) and BF16 latent columns (rows >= split), -0 made +0, as multicast
stores into the front mailbox (``mc``; this rank's ``slot`` / ``base``)."""
mc, slot, base = args[10:13]
_, split, valid, pitch = self.front
tid, _, _, _ = thread()
batch, width = self.batch, self.width
shard: Constexpr = 3584 // self.tp
for i in c.range_constexpr((width * batch + 127) // 128):
item = tid + 128 * i
if item < width * batch:
j, t = item // batch, item % batch
for half in c.range_constexpr(2):
total = Float32(0)
for source in c.range_constexpr(self.ks):
cell = (source * 2 + half) * width + j
total = total + parts[cell * self.n + self.slot(t, j)]
rows[(t * 2 + half) * (width + 1) + j] = total
named_barrier(1, 128)
if tid < 2 * batch:
t, half = tid // 2, tid % 2
row = 64 * f + 32 * half + width * rank
values = [rows[tid * (width + 1) + v] for v in range(width)]
if row < valid:
for v in c.range_constexpr(width // 4):
words = tuple(positive_zero32(values[4 * v + i]) for i in range(4))
offset = t * pitch + 3584 + 2 * (slot + row + 4 * v)
multicast_words(mc + Int64(offset) * 2, words, kind="bf16x2")
elif (row >= split) & (row - split < shard):
pairs = range(0, width, 2)
words = tuple(positive_zero(values[i], values[i + 1]) for i in pairs)
offset = t * pitch + base + row - split
multicast_words(mc + Int64(offset) * 2, words, kind="bf16x2")
class SharedDown(SharedBase):
"""The split variant's down projection beside the routed experts: ``sms``
persistent CTAs (one per SM the experts' grid leaves) stage h [N, hidden] once,
take every ``sms``-th 64-row tile, stream its W blocks through a TMA ring (warp 5)
into alternating TMEM accumulators (warp 4) and peer-store each tile like the
fused kernel (warps 0-3). Exactly the fused kernel's down numerics."""
def __init__(self, tp, batch, sms, budget=200 * 1024):
super().__init__(tp, batch)
self.sms, self.image = sms, self.down * self.n * 128 # h as B tiles
ring = budget - self.image - self.n * 68 * 4
self.stages = min(24, ring // 8192) # W [64, 64] K blocks (8 KB)
@cute.jit
def __call__(self, *args):
options = dict(pdl=False, min_blocks_per_mp=1)
launch(self.kernel, args, self.sms, 192, **options)
@cute.kernel
def kernel(self, args):
h, maps = args[0:2]
tid, warp, lane, cta = thread()
stages, columns = self.stages, self.columns
smem = c.utils.SmemAllocator()
image = alloc(smem, c.Uint8, self.image, 1024).iterator
ring = alloc(smem, c.Uint8, stages * 8192, 1024).iterator
staging = alloc(smem, c.Uint8, self.n * 68 * 4, 16).iterator
# full [stages], empty [stages], accumulator full [2] / empty [2], h ready
bars = alloc(smem, c.Uint64, 2 * stages + 5, 8).iterator
slot = alloc(smem, Uint32, 1)
acc_full, acc_empty, ready = (bars + 2 * stages + i for i in (0, 2, 4))
if tid == 0:
for s in c.range_constexpr(stages):
cute.arch.mbarrier_init(bars + s, 1)
cute.arch.mbarrier_init(bars + stages + s, 1)
for i in c.range_constexpr(2):
cute.arch.mbarrier_init(acc_full + i, 1)
cute.arch.mbarrier_init(acc_empty + i, 128)
cute.arch.mbarrier_init(ready, 128)
cute.arch.mbarrier_init_fence()
if warp == 4:
cute.arch.alloc_tmem(2 * columns, slot.iterator)
cute.arch.relinquish_tmem_alloc_permit()
fence_tmem(before=True)
cute.arch.sync_threads()
fence_tmem()
tmem = slot[0]
tiles = (ROWS - cta + self.sms - 1) // self.sms
if warp < 4:
for j in c.range_constexpr(self.down):
self.load_rows(image + j * self.n * 128, h, 64 * j)
cute.arch.cp_async_mbarrier_arrive_noinc(ready)
for i in c.range(tiles, unroll=1):
wait(acc_full + i % 2, i // 2 % 2, scope="cta")
fence_tmem()
values = self.accumulated(tmem + Uint32(i % 2 * columns))
cute.arch.mbarrier_arrive(acc_empty + i % 2)
self.push(args[2:6], staging, values, 64 * (cta + i * self.sms))
elif warp == 4:
self.multiply(image, ring, bars, tmem, tiles)
elif lane == 0:
down = maps.iterator + 128
acquire_tensormap(down) # in the thread that issues its TMAs
for i in c.range(tiles, unroll=1):
row0 = 64 * (cta + i * self.sms)
for j in c.range(self.down, unroll=1):
g = i * self.down + j
s = g % stages
if g >= stages:
wait(bars + stages + s, (g // stages - 1) % 2, scope="cta")
self.load_weight(
ring + s * 8192, down, bars + s, row0, row0 + 32, 64 * j
)
release_tmem(tmem, 2 * columns, warp, 4)
@cute.jit
def multiply(self, image, ring, bars, tmem, tiles):
"""Warp 4: each tile's K blocks into accumulator half i % 2."""
stages, columns = self.stages, self.columns
instruction: Constexpr = 1 << 4 | 1 << 7 | 1 << 10 | self.n >> 3 << 17 | 4 << 24
acc_full, acc_empty, ready = (bars + 2 * stages + i for i in (0, 2, 4))
total = tiles * self.down
wait(ready, 0, scope="cta")
cute.arch.fence_proxy("async.shared", space="cta")
for i in c.range(tiles, unroll=1):
if i >= 2:
wait(acc_empty + i % 2, (i // 2 - 1) % 2, scope="cta")
fence_tmem()
d = tmem + Uint32(i % 2 * columns)
for j in c.range(self.down, unroll=1):
g = i * self.down + j
s = g % stages
wait(bars + s, g // stages % 2, scope="cta")
fence_tmem()
a = smem_desc(ring + s * 8192, True)
b = smem_desc(image + j * self.n * 128, True)
release = Int32(g + stages < total)
mma_bf16_block(d, a, b, instruction, j, bars + stages + s, release)
self.elected(acc_full + i % 2)
@functools.cache
def compile_shared(tp, batch, device, gate=False, front=None):
"""Arguments: x, ``weight_maps``, h, counters, RS peers, parity, rank, capacity,
beta, linear beta. ``gate``: the split variant's gate/up + SiTU half; ``front``
(rows, split, valid, mailbox row): with the MoE front GEMM's rows, whose results
go to the front mailbox (more arguments: multicast address, logit slot, latent
base; ``weight_maps`` with the front's)."""
maps = fake((256 if front is None else 384,), None, c.Uint8, 64)
args: tuple = (fake((batch, 7168)), maps, fake((batch, 6144 // tp)))
args += (fake((COUNTERS,), None, Int32), fake((tp,), None, Int64))
args += (Int32(0), Int32(0), Int32(0), Float32(4), Float32(25))
if front is not None:
args += (Int64(0), Int32(0), Int32(0))
ring = 80 * 1024 if front is None else FRONT_RING[tp]
kernel = SharedUmma(tp, batch, ring, gate, front)
with torch.accelerator.device_index(device):
return cute.compile(kernel, *args, make_fake_stream())
@functools.cache
def compile_down(tp, batch, device, sms):
"""Arguments: h, ``weight_maps``, RS peers, parity, rank, capacity."""
args: tuple = (fake((batch, 6144 // tp)), fake((256,), None, c.Uint8, 64))
args += (fake((tp,), None, Int64), Int32(0), Int32(0), Int32(0))
kernel = SharedDown(tp, batch, sms)
with torch.accelerator.device_index(device):
return cute.compile(kernel, *args, make_fake_stream())
def scratch(device):
"""Model-owned fused shared-expert counters (zeroed; re-armed by each launch)."""
return torch.zeros(COUNTERS, dtype=torch.int32, device=device)
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,168 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Small-batch MoE chains: front -> routed experts -> (shared expert) -> polling tail.
TP8 B1: ``MoeFrontTP8`` -> ``ClusterExpertsTP8`` -> ``MoeSharedTP8`` -> ``MoeTail``
(front_poll, trigger 3). TP16 B1/B2: ``MoeFrontTP16`` (shared expert inside, on
designated SMs) -> ``ClusterExpertsTP16`` -> ``MoeTail``. The o-projection consumer
publishes the post-attention row into the x_moe mailbox (parity = layer index % 2) and
bumps an epoch hint; the co-resident front has staged its weights by then. MoE tail
polls the G/W/S slots (three parities, 0xffff-armed, re-armed after use) and publishes
the next AttnRes input."""
import functools
import cutlass.cute as cute
import torch
from cutlass import Float32, Float64, Int32, Uint64
from cutlass.cute.runtime import make_fake_stream
from ...common.host import fake, layout, stream, to_cute
from ...oproj.prepared import OprojMailbox
from ..tail_runtime import POLL_CAP, compile_tail
from .experts import ClusterExpertsTP8, ClusterExpertsTP16
from .front_tp8 import MoeFrontTP8, MoeSharedTP8, SharedSituTables
from .front_tp16 import MoeFrontTP16, initialize_designated
# Kernel class and trailing scalar arguments of each small-batch launch.
_KERNELS = {
"front": (MoeFrontTP8, (Int32,) * 3 + (Uint64, Float32)),
"front16": (MoeFrontTP16, (Int32,) * 3 + (Uint64,) + (Float32,) * 4 + (Float64,)),
"experts_tp16": (ClusterExpertsTP16, (Float32,) * 3),
"experts_tp8": (ClusterExpertsTP8, (Float32,) * 3),
"shared": (MoeSharedTP8, (Int32,) * 2),
"tables": (SharedSituTables, (Float32,) * 2),
}
@functools.cache
def _compile(kind, specs, device, *args, **kwargs):
kernel, scalars = _KERNELS[kind]
operands = [fake(*spec) for spec in specs] + [t(0) for t in scalars]
with torch.accelerator.device_index(device):
return cute.compile(kernel(*args, **kwargs), *operands, make_fake_stream())
def _launch(kind, tensors, *args, **kwargs):
specs = tuple(map(layout, tensors))
code = _compile(kind, specs, tensors[0].device.index, *args, **kwargs)
return code, tuple(map(to_cute, tensors))
def u32(t):
return t.view(torch.uint32)
class SmallBatchBox:
"""Model-owned small-batch storage, shared serially by every MoE layer."""
def __init__(self, group, device, tp, rank):
def armed(shape, fill, dtype=torch.bfloat16):
value = torch.empty(shape, dtype=dtype, device=device)
value.view(torch.int32).fill_(fill)
return value
zeros = functools.partial(torch.zeros, dtype=torch.int32, device=device)
self.tp, self.rank, self.device = tp, rank, device
self.sms = torch.cuda.get_device_properties(device).multi_processor_count
self.xmoe = armed((2, 1, 4, 7168), -0x80000000)
self.epoch = zeros(256)
self.scores = armed((7168,), -0x80000000, torch.int32)
self.ids = armed((16,), -1, torch.int32)
self.w, self.s = armed((3, POLL_CAP, 16), -1), armed((3, POLL_CAP, 7168), -1)
self.g = armed((3, POLL_CAP * 16, 3584), -1)
self.h2 = OprojMailbox(group, device, tp, rank, (2, POLL_CAP, 3584))
self.latent = zeros(3584, dtype=torch.bfloat16)
self.hg, self.counter = zeros(6144), zeros(1)
self.tables = {}
self.batches = (1,) if tp == 8 else (1, 2)
if tp == 16: # resident-SM placement mask plus per-batch route/gate scratch
self.designated = zeros(self.sms + 16)
initialize_designated(self.designated)
self.scratch, i32 = {}, torch.int32
for b in self.batches:
scores = armed((2 * b * 896 * 2,), -0x80000000, i32)
self.scratch[b] = scores, armed((b * 16,), -1, i32), zeros(2 * b * 768)
self.front_views = tuple(self.xmoe[p] for p in range(2))
self.hint_views = tuple(self.epoch[p * 32 : p * 32 + 224] for p in range(2))
def situ_tables(self, beta, linear_beta):
key = float(beta), float(linear_beta)
if key not in self.tables:
table = torch.empty(131072, dtype=torch.float32, device=self.device)
code, args = _launch("tables", (table,))
code(*args, Float32(beta), Float32(linear_beta), stream(self.device))
self.tables[key] = table
return self.tables[key]
class SmallBatchChain:
"""One MoE layer's small-batch chain; MoE tail weights come from the layer's
prepared tail."""
def __init__(self, box, moe, experts, tail, layer_idx, beta, linear_beta, prefetch):
self.box, self.tail_weights, self.tail_box = (
box,
tail.tail_weights,
tail.tail_box,
)
self.prefetch, self.layer_idx = prefetch, layer_idx
self.parity, self.slot = layer_idx % 2, int(tail.tail_weights.parity)
self.batches, shared = box.batches, moe.shared_experts
router, bias = moe.gate.weight, moe.gate.e_score_correction_bias
gate_up, shared_down = shared.gate_up_proj.weight, shared.down_proj.weight
rows = 3584 // box.tp
down = moe.routed_expert_down_proj.weight.narrow(0, box.rank * rows, rows)
scales = tuple(experts.weights[i].view(torch.uint8).reshape(-1) for i in (1, 3))
scale, renorm = moe.routed_scaling_factor, moe.moe_renormalize
self.scalars: tuple = (Int32(box.rank), Uint64(box.h2.mc), Float32(scale))
head: tuple = (router, bias, box.xmoe, u32(box.epoch))
if box.tp == 8:
tensors = head + (u32(box.scores), box.ids, box.w, down, box.h2.buffer)
tensors += (box.latent, gate_up, u32(box.hg))
self.front = _launch("front", tensors, grid=box.sms - 8, renorm=renorm)
tables = box.situ_tables(beta, linear_beta)
tensors = (box.xmoe, shared_down, tables, u32(box.hg), box.s)
self.shared = _launch("shared", tensors, grid=box.sms - 96)
tensors = (box.latent, box.ids, experts.maps[256:640].view(3, 128), *scales)
routed, args = _launch(
"experts_tp8", tensors + (box.g[self.slot, :16], box.counter)
)
self.routed = routed, args + experts.scalars
self.tail = compile_tail(8, 1, box.device.index, "front_poll", 3)
return
maps, self.codes = experts.maps[:256].view(2, 128), {}
for b in self.batches:
scores, ids, hg = box.scratch[b]
tensors = head + (u32(scores), ids, box.w, down, box.h2.buffer, gate_up)
tensors += (u32(hg), shared_down, box.s, box.designated)
front = _launch("front16", tensors, b, grid=box.sms - 7 * b, renorm=renorm)
latent = box.h2.buffer[self.parity, :b].reshape(-1)
g = box.g[self.slot, : b * 16]
routed = _launch("experts_tp16", (latent, ids, maps, *scales, g), b)
tail = compile_tail(16, b, box.device.index, "front_poll", 2)
self.codes[b] = front, routed, tail
lb = linear_beta
self.scalars += (Float32(beta), Float32(1 / beta), Float32(lb), Float64(1 / lb))
self.experts = experts.scalars
def run(self, out, consume):
b, s = out.shape[0], stream(self.box.device)
par, slot = Int32(self.parity), Int32(self.slot)
if self.box.tp == 8:
self.front[0](*self.front[1], par, slot, *self.scalars, s)
self.routed[0](*self.routed[1], s)
self.shared[0](*self.shared[1], par, slot, s)
tail = self.tail
else:
(front, front_args), (routed, routed_args), tail = self.codes[b]
front(*front_args, par, slot, *self.scalars, s)
routed(*routed_args, *self.experts, s)
if self.prefetch is not None:
self.prefetch.fork("next_attention", self.layer_idx, b)
self.tail_weights.run(
self.tail_box, self.box.g, self.box.w, self.box.s, tail, b
)
if consume(out) is not out:
raise RuntimeError("MoE tail output must be consumed from its mailbox ring")
return out
@@ -0,0 +1,541 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Small-batch routed experts: ``ClusterExpertsTP16`` (TP16 B1/B2, six-CTA split-K
clusters exchanging quantized hidden rows) and ``ClusterExpertsTP8`` (TP8 B1, one
six-CTA cluster per routing slot)."""
from typing import Any
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, Int64, Uint8, Uint32, Uint64
from cutlass.cute.arch import store_async_dsmem
from ...common.primitives import (
acquire_tensormap_asm,
alloc,
chunk_mma,
commit_asm,
copy_evict,
copy_scale,
fence_tmem,
float_bits,
fp8_bits,
launch,
load_tma3,
load_tmem,
load_volatile,
load_words,
mma_steps,
mx_exponent,
mx_rescale,
pack_fp8,
prefetch_bytes,
prefetch_map,
published,
release_tmem,
require_zero_tmem,
shared_address,
situ,
smem_desc,
split,
store_bf16_pair,
store_pair,
store_shared_words,
thread,
unpack_bf16,
view,
wait,
wait_tmem,
warp_max_abs,
)
@cute.jit
def quantize_mx8(packed, row):
"""(E8M0 scale, FP8 words) of eight BF16 values; four lanes share a 32-block."""
values = cute.make_rmem_tensor(8, Float32)
for k in cutlass.range_constexpr(4):
values[2 * k], values[2 * k + 1] = unpack_bf16(packed[row, k])
amax = Float32(0)
for k in cutlass.range_constexpr(8):
amax = max(amax, abs(values[k]))
for k in (1, 2):
amax = max(amax, cute.arch.shuffle_sync_bfly(amax, k))
sb = mx_exponent(amax)
rs = mx_rescale(sb)
lo, hi = (pack_fp8([values[4 * h + k] * rs for k in range(4)]) for h in (0, 1))
return sb, (lo, hi)
class _ClusterExperts:
"""Shared launch of the six-CTA-cluster expert kernels."""
kernel: Any
threads: int
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, 96, self.threads, cluster=6, min_blocks_per_mp=1)
class ClusterExpertsTP16(_ClusterExperts):
"""Paired CTAs split FC1 and exchange quantized hidden rows; the TP16 intermediate
has 192 live values padded to 256. IDs [B, 16], x BF16 [B, 3584], output BF16
[B * 16, 3584]. Polled IDs are re-armed per cluster; the caller re-arms latent rows
after all consumers complete."""
threads = 288
def __init__(self, batch: int):
"""Polled IDs and latent rows; successors are released before the final wait."""
self.batch = batch
self.off_sfa = 6 * 32768
self.off_x = self.off_sfa + 6 * 1024
self.off_h = self.off_x + 7 * 2048
self.off_xsf = self.off_h + batch * 2048
self.off_hsf = self.off_xsf + 14 * 128 + 384
self.off_partial = self.off_hsf + batch * 1152
self.off_misc = self.off_partial + batch * 256
self.off_acc1 = self.off_misc + 104
self.off_acc2 = self.off_acc1 + batch * 8
self.off_precv = self.off_acc2 + batch * 40
self.off_hrecv = self.off_precv + batch * 8
self.off_tmem = self.off_hrecv + batch * 8
self.off_amax = self.off_tmem + 4
self.off_stage = (self.off_amax + batch * 16 + 15) // 16 * 16
# 512 B unused reserve keeps the tuned SMEM footprint.
self.smem_bytes = (self.off_stage + batch * 64 + 512 + 15) // 16 * 16
cols = (8, 64, 128, 184) if batch == 1 else (16, 96, 144, 200)
self.col_acc2, self.col_sfa, self.col_xsf, self.col_hsf = cols
@cute.jit
def get_id(self, ids, j, token):
expert = Int32(load_volatile(ids.iterator + token * 16 + j)[0])
while expert < 0:
expert = Int32(load_volatile(ids.iterator + token * 16 + j)[0])
return expert
@cute.jit
def issue(self, sm, maps, s13, s2, full, n, e0, e1, rank, tile0, tiles):
slot = n % 6
cute.arch.mbarrier_arrive_and_expect_tx(full.iterator + slot, 16384 + 1024)
if n < 7 * self.batch:
expert = e0 if n < 7 else e1
ks = 7 * (rank % 2) + n % 7
row = expert * 512 + rank // 2 * 128
dst = sm.iterator + slot * 32768
load_tma3(dst, maps.iterator, full.iterator + slot, row, ks * 2)
dst = sm.iterator + self.off_sfa + slot * 1024
src = s13.iterator + Int64(expert) * 57344 + rank // 2 * 14336 + ks * 1024
copy_evict(dst, src, size=1024, barrier=full.iterator + slot)
else:
u = n - 7 * self.batch
expert = e0 if u < tiles else e1
tile = tile0 + u % tiles
dst, desc = sm.iterator + slot * 32768, maps.iterator + 128
load_tma3(dst, desc, full.iterator + slot, expert * 3584 + tile * 128, 0)
dst = sm.iterator + self.off_sfa + slot * 1024
src = s2.iterator + Int64(expert) * 28672 + tile * 1024
copy_evict(dst, src, size=1024, barrier=full.iterator + slot)
@cute.kernel
def kernel(self, args):
x, ids, maps, s13, s2, output, *situ_args = args
tid, warp, lane, cta = thread()
rank, j = cta % 6, cta // 6
half, row_tile = rank % 2, rank // 2
tile0, tiles = split(28, rank, 6)
sm = alloc(cutlass.utils.SmemAllocator(), Uint8, self.smem_bytes, 1024)
full = view(sm.iterator, self.off_misc, Uint64, 6)
empty = view(sm.iterator, self.off_misc + 48, Uint64, 6)
xall = view(sm.iterator, self.off_misc + 96, Uint64, 1)
acc1 = view(sm.iterator, self.off_acc1, Uint64, self.batch)
acc2 = view(sm.iterator, self.off_acc2, Uint64, self.batch * 5)
precv = view(sm.iterator, self.off_precv, Uint64, self.batch)
hrecv = view(sm.iterator, self.off_hrecv, Uint64, self.batch)
tmem_base = view(sm.iterator, self.off_tmem, Uint32, 1)
if tid == 0:
for s in cutlass.range_constexpr(6):
cute.arch.mbarrier_init(full.iterator + s, 1)
cute.arch.mbarrier_init(empty.iterator + s, 1)
cute.arch.mbarrier_init(xall.iterator, 7)
for t in cutlass.range_constexpr(self.batch):
cute.arch.mbarrier_init(acc1.iterator + t, 1)
for i in cutlass.range_constexpr(5):
cute.arch.mbarrier_init(acc2.iterator + 5 * t + i, 1)
cute.arch.mbarrier_init(precv.iterator + t, 1)
cute.arch.mbarrier_init(hrecv.iterator + t, 1)
cute.arch.mbarrier_arrive_and_expect_tx(precv.iterator + t, 256)
cute.arch.mbarrier_arrive_and_expect_tx(hrecv.iterator + t, 288)
cute.arch.mbarrier_init_fence()
acquire_tensormap_asm(maps.iterator)
acquire_tensormap_asm(maps.iterator + 128)
if warp == 2:
cute.arch.alloc_tmem(256, tmem_base.iterator)
cute.arch.relinquish_tmem_alloc_permit()
cute.arch.cluster_arrive_relaxed()
fence_tmem(before=True)
cute.arch.sync_threads()
fence_tmem()
tmem = tmem_base[0]
cute.arch.griddepcontrol_launch_dependents()
if warp == 0:
if lane == 0:
e0 = e1 = self.get_id(ids, j, 0)
for n in cutlass.range_constexpr(6):
self.issue(sm, maps, s13, s2, full, n, e0, e1, rank, tile0, tiles)
ks = 7 * half + 6
prefetch_map(maps.iterator, e0 * 512 + row_tile * 128, 2 * ks)
src = s13.iterator + Int64(e0) * 57344 + row_tile * 14336 + ks * 1024
prefetch_bytes(src, 1024)
if cutlass.const_expr(self.batch == 2):
e1 = self.get_id(ids, j, 1)
for n in cutlass.range(6, self.batch * (7 + tiles), unroll=1):
slot = n % 6
wait(empty.iterator + slot, ((n // 6) - 1) % 2)
self.issue(sm, maps, s13, s2, full, n, e0, e1, rank, tile0, tiles)
elif warp == 1:
require_zero_tmem(tmem)
adesc = smem_desc(sm.iterator, True)
sfa = smem_desc(sm.iterator + self.off_sfa, False)
xdesc = smem_desc(sm.iterator + self.off_x, True)
xsf = smem_desc(sm.iterator + self.off_xsf, False)
wait_tmem(xall.iterator)
with cute.arch.elect_one():
for n in cutlass.range_constexpr(14):
copy_scale(self.col_xsf + n * 4, xsf + n * 8)
cute.arch.sync_warp()
for t in cutlass.range(self.batch, unroll=1):
for n in cutlass.range_constexpr(7):
g = 7 * t + n
slot = (t + n) % 6
wait_tmem(full.iterator + slot, (g // 6) % 2)
with cute.arch.elect_one():
col = self.col_sfa + 8 * slot
copy_scale(col, sfa + 64 * slot)
copy_scale(col + 4, sfa + 64 * slot + 32)
d, a, b = 8 * t, adesc + 2048 * slot, xdesc + 128 * n
mma_steps(d, a, b, col, self.col_xsf + 8 * n, n != 0, False)
commit_asm(empty.iterator + slot)
if cutlass.const_expr(n == 6):
commit_asm(acc1.iterator + t)
cute.arch.sync_warp()
for t in cutlass.range(self.batch, unroll=1):
wait(hrecv.iterator + t, test=True)
cute.arch.fence_proxy("async.shared", space="cta")
fence_tmem()
hdesc = smem_desc(sm.iterator + self.off_h + t * 2048, True)
hsf = smem_desc(sm.iterator + self.off_hsf + t * 1152, False)
with cute.arch.elect_one():
for k in cutlass.range_constexpr(6):
copy_scale(self.col_hsf + 24 * t + 4 * k, hsf + k * 8)
cute.arch.sync_warp()
for i in cutlass.range_constexpr(5):
if i < tiles:
g = self.batch * 7 + t * tiles + i
slot = g % 6
wait_tmem(full.iterator + slot, (g // 6) % 2)
with cute.arch.elect_one():
col = self.col_sfa + 8 * slot
copy_scale(col, sfa + 64 * slot)
copy_scale(col + 4, sfa + 64 * slot + 32)
d, a = self.col_acc2 + 8 * (5 * t + i), adesc + 2048 * slot
mma_steps(d, a, hdesc, col, self.col_hsf + 24 * t, 0, True)
commit_asm(empty.iterator + slot)
commit_asm(acc2.iterator + 5 * t + i)
cute.arch.sync_warp()
else:
n = warp - 2
packed = cute.make_rmem_tensor((self.batch, 4), Uint32)
for t in cutlass.range_constexpr(self.batch):
pointer = x.iterator + t * 3584 + 1792 * half + 256 * n + lane * 8
ready = False
while not ready:
words = load_volatile(pointer, count=4)
ready = published(words)
for k in cutlass.range_constexpr(4):
packed[t, k] = words[k]
block, k = lane // 4, lane % 4
kc, kb = 16 * n + 2 * block + k // 2, 8 * n + block
for t in cutlass.range_constexpr(self.batch):
sb, words = quantize_mx8(packed, t)
offset = self.off_x + kc // 8 * 1024 + t * 128 + ((kc % 8) ^ t) * 16
offset += (k % 2) * 8
store_shared_words(sm.iterator + offset, words)
if k == 0:
sm[self.off_xsf + kb // 4 * 128 + 16 * t + kb % 4] = Uint8(sb)
cute.arch.fence_proxy("async.shared", space="cta")
cute.arch.sync_warp()
if lane == 0:
cute.arch.mbarrier_arrive(xall.iterator)
if (warp >= 4) & (warp < 8):
ew = warp % 4
own = ew // 2 == half
tl = tmem + (Uint32(32 * ew) << 16)
cute.arch.cluster_wait()
partial = view(sm.iterator, self.off_partial, Float32, self.batch * 64)
maxima = view(sm.iterator, self.off_amax, Float32, self.batch * 4)
for t in cutlass.range(self.batch, unroll=1):
wait_tmem(acc1.iterator + t)
value = load_tmem(tl + 8 * t + t)
index = 64 * t + (ew % 2) * 32 + lane
if not own:
dst = cute.recast_ptr(partial.iterator + index, dtype=Uint32)
bits = Uint32(float_bits(value))
store_async_dsmem(dst, bits, precv.iterator + t, rank ^ 1)
else:
wait(precv.iterator + t, test=True)
value += partial[index]
other = cute.arch.shuffle_sync_bfly(value, 8)
up = (lane & 8) == 0
hidden = situ(other, value, *situ_args) if up else Float32(0)
amax = warp_max_abs(hidden)
if lane == 0:
maxima[4 * t + ew] = amax
cute.arch.barrier(barrier_id=1, number_of_threads=64)
sb = mx_exponent(max(amax, maxima[4 * t + (ew ^ 1)]))
if up:
byte = fp8_bits(hidden * mx_rescale(sb))
stage = self.off_stage + 64 * t + 16 * ew
sm[stage + 2 * (lane % 8) + lane // 16] = byte
cute.arch.sync_warp()
if lane < 6:
src = sm.iterator + self.off_stage + 64 * t + 16 * ew
h_words = load_words(src, count=4, shared=True)
kc = 4 * row_tile + ew
h_offset = self.off_h + 2048 * t + kc // 8 * 1024
h_offset += kc % 8 * 16
dst = sm.iterator + cute.assume(h_offset, divby=16)
store_async_dsmem(dst, h_words, hrecv.iterator + t, lane)
if ew % 2 == 1:
sw = (Uint32(sb) * Uint32(0x01010101),) * 4
scale_offset = self.off_hsf + 1152 * t + 128 * rank
dst = sm.iterator + cute.assume(scale_offset, divby=16)
store_async_dsmem(dst, sw, hrecv.iterator + t, lane)
for t in cutlass.range(self.batch, unroll=1):
for i in range(tiles):
wait_tmem(acc2.iterator + 5 * t + i)
value = load_tmem(tl + self.col_acc2 + 8 * (5 * t + i))
other = cute.arch.shuffle_sync_down(value, 8)
tile = tile0 + i
column = tile * 128 + 32 * ew + 4 * (lane % 8) + lane // 8
if (lane & 8) == 0:
dst = output.iterator + (t * 16 + j) * 3584 + column
store_pair(dst, value, other)
release_tmem(tmem, 256, warp, 2)
if (tid == 0) & (rank == 0):
for t in cutlass.range(self.batch, unroll=1):
ids[t * 16 + j] = Int32(-1)
if (cta == 0) & (tid == 0): # one-thread exit wait, as in the front
cute.arch.griddepcontrol_wait()
def _align_1024(value):
return (value + 1023) // 1024 * 1024
class ClusterExpertsTP8(_ClusterExperts):
"""FP4 x MXFP8 FC1 -> SiTU -> MXFP8 -> FC2 of x BF16 [3584] and IDs int32 [16] into
BF16 [16, 3584]. Weights stay in the loaded MoE weights' shuffled layout (three TMA
maps, two scale arrays); mailbox mode consumes/re-arms IDs and the latent row via a
grid ticket."""
slots = 6
slot_bytes = 32768
off_sfa = slots * slot_bytes
off_x = _align_1024(off_sfa + slots * 1024)
off_xsf = _align_1024(off_x + 27 * 128 + 1024)
off_h = _align_1024(off_xsf + 28 * 128 + 384)
off_hsf = off_h + 3 * 1024
off_hsc = off_hsf + 3 * 128 + 384
off_partial = off_hsc + 32
off_stage = off_partial + 1024
off_amax = off_stage + 64
off_bar = off_amax + 16
off_tmem = off_bar + (2 * slots + 8 + 3) * 8
smem_bytes = off_tmem + 16
threads = 256
@cute.jit
def issue(self, sm, maps, scales, index, row, chunk, offset, n, full, empty, count):
slot = n % 6
phase = (n // 6) % 2
if n >= 6:
wait(empty.iterator + slot, phase ^ 1)
cute.arch.mbarrier_arrive_and_expect_tx(full.iterator + slot, count * 8704)
dst, desc = sm.iterator + slot * 32768, maps.iterator + index * 128
load_tma3(dst, desc, full.iterator + slot, row, chunk)
dst, src = sm.iterator + self.off_sfa + slot * 1024, scales.iterator + offset
copy_evict(dst, src, size=count * 512, barrier=full.iterator + slot)
@cute.kernel
def kernel(self, args):
x, ids, maps, s13, s2, output, counter, *situ_args = args
tid, warp, lane, cta = thread()
rank, j = cta % 6, cta // 6
sm = alloc(cutlass.utils.SmemAllocator(), Uint8, self.smem_bytes, 1024)
full = view(sm.iterator, self.off_bar, Uint64, 6)
empty = view(sm.iterator, self.off_bar + 48, Uint64, 6)
accb = view(sm.iterator, self.off_bar + 96, Uint64, 8)
xbar, hbar = accb.iterator + 8, accb.iterator + 9
hsc = view(sm.iterator, self.off_hsc, Uint32, 6)
tmem_base = view(sm.iterator, self.off_tmem, Uint32, 1)
t2lo, t2hi = 28 * rank // 6, 28 * (rank + 1) // 6
if tid == 0:
for s in cutlass.range_constexpr(6):
cute.arch.mbarrier_init(full.iterator + s, 1)
cute.arch.mbarrier_init(empty.iterator + s, 1)
for a in cutlass.range_constexpr(8):
cute.arch.mbarrier_init(accb.iterator + a, 1)
cute.arch.mbarrier_init(xbar, 6)
cute.arch.mbarrier_init(hbar, 1)
cute.arch.mbarrier_arrive_and_expect_tx(hbar, 6 * 68)
cute.arch.mbarrier_init_fence()
for i in cutlass.range_constexpr(3):
acquire_tensormap_asm(maps.iterator + i * 128)
if warp == 1:
cute.arch.alloc_tmem(256, tmem_base.iterator)
cute.arch.relinquish_tmem_alloc_permit()
fence_tmem(before=True)
cute.arch.sync_threads()
cute.arch.cluster_arrive_relaxed()
fence_tmem()
tmem = tmem_base[0]
cute.arch.griddepcontrol_launch_dependents()
cute.arch.griddepcontrol_wait()
if warp == 0:
if lane == 0:
expert = ids[j]
for n in range(14):
row, chunk = expert * 768 + rank * 128, 2 * n
offset = Int64(expert) * 86016 + rank * 14336 + n * 1024
self.issue(sm, maps, s13, 0, row, chunk, offset, n, full, empty, 2)
n = Int32(14)
for tile in range(t2lo, t2hi):
offset = Int64(expert) * 43008 + tile * 1536
row = expert * 3584 + tile * 128
self.issue(sm, maps, s2, 1, row, 0, offset, n, full, empty, 2)
n += 1
offset += 1024
self.issue(sm, maps, s2, 2, row, 2, offset, n, full, empty, 1)
n += 1
elif warp == 1:
adesc = smem_desc(sm.iterator, True)
sfa = smem_desc(sm.iterator + self.off_sfa, False)
xdesc = smem_desc(sm.iterator + self.off_x, True)
xsf = smem_desc(sm.iterator + self.off_xsf, False)
hdesc = smem_desc(sm.iterator + self.off_h, True)
hsf = smem_desc(sm.iterator + self.off_hsf, False)
wait_tmem(xbar)
for n in range(14):
slot = n % 6
wait_tmem(full.iterator + slot, (n // 6) % 2)
for half in cutlass.range_constexpr(2):
kc = 2 * n + half
empty_address = acc_address = Uint32(0)
if cutlass.const_expr(half == 1):
empty_address = shared_address(empty.iterator + slot)
if n == 13:
acc_address = shared_address(accb.iterator)
a = (adesc + slot * 2048 + half * 1024, xdesc + kc * 8)
sf = (tmem + 64 + 8 * slot + 4 * half, sfa + slot * 64 + half * 32)
sfx = (tmem + 128 + 4 * kc, xsf + kc * 8, 1, Int32(kc != 0))
chunk_mma(tmem, *a, *sf, *sfx, empty_address, acc_address)
cute.arch.sync_warp()
wait(hbar)
if lane < 3:
word = (hsc[2 * lane] & 0xFFFF) | (hsc[2 * lane + 1] << 16)
dst = sm.iterator + self.off_hsf + lane * 128
store_shared_words(dst, (word,) * 4)
cute.arch.fence_proxy("async.shared", space="cta")
cute.arch.sync_warp()
n = Int32(14)
for tile in range(t2lo, t2hi):
a = 2 + tile - t2lo
for stage in cutlass.range_constexpr(2):
slot = n % 6
wait_tmem(full.iterator + slot, (n // 6) % 2)
for half in cutlass.range_constexpr(2 if stage == 0 else 1):
kh: cutlass.Constexpr = 2 * stage + half
empty_address = acc_address = Uint32(0)
if cutlass.const_expr(half == (1 if stage == 0 else 0)):
empty_address = shared_address(empty.iterator + slot)
if cutlass.const_expr(kh == 2):
acc_address = shared_address(accb.iterator + a)
ops: tuple = (tmem + 8 * a, adesc + slot * 2048 + half * 1024)
ops += (hdesc + kh * 64, tmem + 64 + 8 * slot + 4 * half)
ops += (sfa + slot * 64 + half * 32, tmem + 240 + 4 * kh)
ops += (hsf + kh * 8, Int32(tile == t2lo), int(kh != 0))
chunk_mma(*ops, empty_address, acc_address)
cute.arch.sync_warp()
n += 1
else:
xtid = tid - 64
packed = cute.make_rmem_tensor((3, 4), Uint32)
for i in cutlass.range_constexpr(3):
if 32 * (warp - 2) + 192 * i < 448:
words = load_words(
x.iterator + 8 * (xtid + 192 * i), count=4, mutable=True
)
for k in cutlass.range_constexpr(4):
packed[i, k] = words[k]
for i in cutlass.range_constexpr(3):
f = xtid + 192 * i
if 32 * (warp - 2) + 192 * i < 448:
sb, words = quantize_mx8(packed, i)
kc, chunk = f // 16, (f % 16) // 2
offset = self.off_x + kc * 128 + (chunk ^ kc % 8) * 16 + f % 2 * 8
store_shared_words(sm.iterator + offset, words)
kb = f // 4
if f % 4 == 0:
sm[self.off_xsf + (kb // 4) * 128 + kb % 4] = Uint8(sb)
cute.arch.fence_proxy("async.shared", space="cta")
cute.arch.sync_warp()
if lane == 0:
cute.arch.mbarrier_arrive(xbar)
if warp >= 4:
ew = warp % 4
address = tmem + (Uint32(32 * ew) << 16)
up = (lane & 8) == 0
local = 2 * (lane % 8) + lane // 16
cute.arch.cluster_wait()
wait_tmem(accb.iterator)
value = load_tmem(address)
other = cute.arch.shuffle_sync_bfly(value, 8)
hidden = situ(other, value, *situ_args) if up else Float32(0)
amax = abs(hidden)
for off in cutlass.range_constexpr(5):
amax = max(amax, cute.arch.shuffle_sync_bfly(amax, 1 << off))
maxima = view(sm.iterator, self.off_amax, Float32, 4)
if lane == 0:
maxima[ew] = amax
cute.arch.barrier(barrier_id=1, number_of_threads=128)
sb = mx_exponent(max(amax, maxima[ew ^ 1]), True)
if up:
byte = fp8_bits(hidden * mx_rescale(sb))
sm[self.off_stage + ew * 16 + local] = byte
cute.arch.sync_warp()
if lane == 0:
src = sm.iterator + self.off_stage + ew * 16
words = load_words(src, count=4, shared=True)
kc = 4 * rank + ew
offset = self.off_h + (kc // 8) * 1024 + (kc % 8) * 16
sw = Uint32(mx_exponent(max(maxima[0], maxima[1]), True))
sw |= Uint32(mx_exponent(max(maxima[2], maxima[3]), True)) << 8
for peer in range(6):
dst = sm.iterator + cute.assume(offset, divby=16)
store_async_dsmem(dst, words, hbar, peer)
if ew == 0:
store_async_dsmem(hsc.iterator + rank, sw, hbar, peer)
for tile in range(t2lo, t2hi):
a = 2 + tile - t2lo
wait_tmem(accb.iterator + a)
value = load_tmem(address + 8 * a)
other = cute.arch.shuffle_sync_down(value, 8)
if up:
column = tile * 128 + 32 * ew + 4 * (lane % 8) + lane // 8
dst = output.iterator + j * 3584 + column
store_bf16_pair(dst, value, other)
release_tmem(tmem, 256, warp, 1)
@@ -0,0 +1,580 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""TP16 B1/B2 MoE front: SM placement, early router/latent-down staging, MMA router
and shared gate/up, FP32 routing, latent multicast/gather and shared SiTU/down rows."""
import functools
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
import torch
from cutlass import BFloat16, Float32, Int32, Uint8, Uint32, Uint64
from cutlass.cute.runtime import make_fake_stream, make_fake_tensor
from .....ops.cute_dsl.latent_moe_tail.primitives import to_cute
from ...common.primitives import (
alloc,
asm,
bf16,
copy_bulk,
div_rn,
early_weights,
fma,
gate_bf16_bits,
init_barriers,
launch,
load_f32x4,
mailbox_load,
mailbox_store,
mma_into,
multicast_words,
named_barrier,
prefetch_bytes,
shared_situ,
sigmoid,
sleep_ns,
smem_words,
split,
thread,
unpack_bf16,
view,
wait,
warp_sum,
zeros,
)
from ..routing import Top16Fast, bf16_word, gather_x, score_record, stage_rows
@asm()
def sreg(name, dtype=Uint32, *, pure=False):
bits, kind = (64, "l") if dtype is Uint64 else (32, "r")
return f"mov.u{bits} $0,%{name};", f"={kind}", (), dtype, pure
@asm(Uint32)
def claim_add(pointer):
return "atom.relaxed.gpu.global.add.u32 $0,[$1],1;", "=r,l,~{memory}", (pointer,)
@asm()
def counter_reduce(pointer, value, *, op="add", release=False):
ordering = "release" if release else "relaxed"
code = f"red.{ordering}.gpu.global.{op}.{'b32' if op == 'or' else 'u32'} [$0],$1;"
return code, "l,r,~{memory}", (pointer, Uint32(value))
@asm(Uint32)
def load_arrivals(pointer):
return "ld.acquire.gpu.global.u32 $0,[$1];", "=r,l,~{memory}", (pointer,)
class FrontPlacement:
"""One claiming thread per CTA; every CTA publishes its arrival."""
def __init__(self, batch):
self.batch = batch
self.offset = 8 if batch == 2 else 0
@cute.jit
def claim(self, epoch, designated, par, cta):
pointer = epoch.iterator + (2 + par) * 32 + self.offset
sm = sreg("smid", pure=True)
claimed = Int32(-1)
if sm < designated.shape[0] - 16: # noqa: SIM102 - preserve guarded load
if designated[sm] != 0:
claimed = Int32(claim_add(pointer))
counter_reduce(pointer + 2, Uint32(0xFFFFFFFF) - Uint32(cta), op="max")
if cutlass.const_expr(self.batch == 2):
mask = pointer + 3 + (cta // 32)
counter_reduce(mask, Uint32(1) << (cta % 32), op="or")
counter_reduce(pointer + 1, Uint32(1), release=True)
return claimed
@cute.jit
def rearm(self, epoch, par):
pointer = epoch.iterator + (2 + (par ^ 1)) * 32 + self.offset
for i in cutlass.range_constexpr(8 if self.batch == 2 else 3):
mailbox_store(pointer + i, (Uint32(0),))
@cute.jit
def role(self, epoch, par, cta, claimed, grid):
pointer = epoch.iterator + (2 + par) * 32 + self.offset
count = Int32(0)
role = Int32(-1)
if claimed >= 0:
arrived = load_arrivals(pointer + 1)
while arrived < grid:
sleep_ns(64)
arrived = load_arrivals(pointer + 1)
count = Int32(mailbox_load(pointer)[0])
first = Uint32(0xFFFFFFFF) - mailbox_load(pointer + 2)[0]
if cutlass.const_expr(self.batch == 1):
role = claimed | (Int32(first == cta) << 16)
else:
order = Int32(0)
for word in cutlass.range_constexpr(5):
bits = mailbox_load(pointer + 3 + word)[0]
if word < cta // 32:
order += Int32(cute.arch.popc(bits))
elif word == cta // 32:
below = (Uint32(1) << (cta % 32)) - Uint32(1)
order += Int32(cute.arch.popc(bits & below))
role = order | (Int32(order < 2) << 16)
return role, count
class FrontPlacementProbe:
def __init__(self, cluster):
self.cluster = cluster
@cute.jit
def __call__(self, *args):
grid, smem = args[0].shape[0] // 2, 220 * 1024
launch(self.kernel, args, grid, 128, self.cluster, pdl=False, smem=smem)
@cute.kernel
def kernel(self, args):
output, spin = args
tid, _, _, cta = thread()
sm, cluster = sreg("smid", pure=True), sreg("clusterid.x")
start = sreg("globaltimer", Uint64)
if tid == 0:
output[2 * cta] = Int32(sm)
output[2 * cta + 1] = Int32(cluster)
while sreg("globaltimer", Uint64) - start < spin:
pass
@functools.cache
def _compile_probe(device, cluster, length):
output = make_fake_tensor(Int32, (length,), (1,), assumed_align=16)
with torch.accelerator.device_index(device):
probe = FrontPlacementProbe(cluster)
return cute.compile(probe, output, Uint64(20000), make_fake_stream())
def plan_designated(groups, *, engine_clusters=16, cluster_size=6):
"""Largest-capacity-first plan of the routed-expert clusters over the SM groups."""
capacities = [len(group) // cluster_size for group in groups]
if sum(capacities) < engine_clusters:
raise ValueError("SM probe found insufficient routed-engine cluster capacity")
order = sorted(range(len(groups)), key=lambda i: (-capacities[i], -len(groups[i])))
counts = [0] * len(groups)
remaining = engine_clusters
for i in order:
counts[i] = min(capacities[i], remaining)
remaining -= counts[i]
keep = set()
for group, count in zip(groups, counts):
quota = len(group) - cluster_size * count
chosen: list[int] = []
for sm in sorted(group):
if sm % 2 == 0 and sm + 1 in group and len(chosen) + 2 <= quota:
chosen.extend((sm, sm + 1))
for sm in sorted(group):
if len(chosen) < quota and sm not in chosen:
chosen.append(sm)
keep.update(chosen)
return keep, counts
def initialize_designated(table: torch.Tensor):
"""Probe SM groups and fill the model-owned int32[nsm+16] table; returns CPU audit
data. Probe storage is freed before capture. Probes: 1- and 3-wave grids of
2..16-CTA clusters, 220 KiB SMEM (one CTA per SM), a 20 us global-timer spin."""
device = table.device
nsm = torch.cuda.get_device_properties(device).multi_processor_count
parent = list(range(4096))
seen = set()
launches = []
def find(sm):
while parent[sm] != sm:
parent[sm] = parent[parent[sm]]
sm = parent[sm]
return sm
with torch.accelerator.device_index(device.index):
stream = cuda.CUstream(torch.cuda.current_stream(device).cuda_stream)
for cluster in range(2, 17):
for waves in (1, 3):
clusters = max(1, (nsm // cluster) * waves)
output = table.new_full((2 * cluster * clusters,), -1)
try:
compiled = _compile_probe(device.index, cluster, output.numel())
compiled(to_cute(output), Uint64(20000), stream)
except RuntimeError as e:
launches.append(dict(cluster=cluster, waves=waves, error=str(e)))
break
torch.accelerator.synchronize(device)
members: dict[int, list[int]] = {}
for sm, group in output.view(-1, 2).cpu().tolist():
if sm >= 0:
members.setdefault(group, []).append(sm)
seen.add(sm)
for group in members.values():
for sm in group[1:]:
parent[find(sm)] = find(group[0])
launches.append(dict(cluster=cluster, waves=waves, clusters=clusters))
if len(seen) != nsm:
raise RuntimeError(f"Placement probe saw {len(seen)} of {nsm} SMs")
group_map: dict[int, list[int]] = {}
for sm in sorted(seen):
group_map.setdefault(find(sm), []).append(sm)
groups = sorted(group_map.values(), key=lambda group: group[0])
keep, counts = plan_designated(groups)
values = [int(sm in keep) for sm in range(nsm + 16)]
table.copy_(torch.tensor(values, dtype=torch.int32, device=device))
return dict(
groups=groups,
engine_clusters=counts,
designated=sorted(keep),
launches=launches,
)
class MoeFrontTP16:
def __init__(self, batch: int, *, grid=None, renorm=True):
self.batch = batch
self.grid = grid if grid is not None else 141 if batch == 1 else 134
self.renorm = renorm
self.shared_bytes = 15 * 14336 + (512 * batch + 128) * 4 + 16 + 8
self.placement = FrontPlacement(batch)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, self.grid, 512, min_blocks_per_mp=1)
@cute.jit
def gemv(self, rows, nrows, activation, partials, warp, lane):
group, quarter = lane // 4, lane % 4
acc = zeros((4, 4), Float32)
for iteration in cutlass.range_constexpr(14):
offset = (warp * 448 + iteration * 32 + 8 * quarter) * 2
weight = x = (Uint32(0),) * 4
if group < nrows:
weight = smem_words(rows + group * 14336 + offset)
if group < 2 * self.batch:
x = smem_words(activation + (group // 2) * 14336 + offset)
b0 = x[2] if (group & 1) != 0 else x[0]
b1 = x[3] if (group & 1) != 0 else x[1]
chain: cutlass.Constexpr = iteration % 4
a = (weight[0], weight[2], weight[1], weight[3])
mma_into(acc[chain, None], a, (b0, b1), bf16=True)
if (quarter < self.batch) & (group < nrows):
chain_sum = lambda c: acc[c, 0] + acc[c, 3]
total = (chain_sum(0) + chain_sum(1)) + (chain_sum(2) + chain_sum(3))
partials[(warp * 16 + group) * self.batch + quarter] = total
@cute.jit
def gate_pass(self, slots, rows, start, bars, partials, hg, par, tid):
wait(bars + 1)
warp, lane = tid // 32, tid % 32
self.gemv(slots + 9 * 14336, rows, slots + 7 * 14336, partials, warp, lane)
cute.arch.sync_threads()
if tid < self.batch * rows:
token, row = tid // rows, tid % rows
total = Float32(0)
for w in cutlass.range_constexpr(16):
total += partials[(w * 16 + row) * self.batch + token]
bits = Uint32(gate_bf16_bits(total))
pointer = hg.iterator + (par * self.batch + token) * 768 + start + row
mailbox_store(pointer, ((bits << 16) | Uint32(1),))
@cute.jit
def poll_flag(self, pointer):
word = mailbox_load(pointer)[0]
while (word & 1) == 0:
if cutlass.const_expr(self.batch == 2):
sleep_ns(128)
word = mailbox_load(pointer)[0]
return word
@cute.jit
def shared_dot(self, packed, hq, token):
value = Float32(0)
for chunk in cutlass.range_constexpr(3):
w = (*unpack_bf16(packed[chunk, 0]), *unpack_bf16(packed[chunk, 1]))
for j in cutlass.range_constexpr(3, -1, -1):
value = fma(w[j], hq[token][chunk][j], value)
return warp_sum(value, 32)
@cute.jit
def load_down(self, down, packed, pair, end_pair, lane, fits=True):
for row in cutlass.range_constexpr(4):
for chunk in cutlass.range_constexpr(3):
words = (Uint32(0), Uint32(0))
if fits & (pair + row // 2 < end_pair):
words = early_weights(
down.iterator + (2 * pair + row) * 384 + 4 * (lane + 32 * chunk)
)
packed[row, chunk, 0], packed[row, chunk, 1] = words[0], words[1]
@cute.jit
def down_dots(self, packed, values, hq, token):
for row in cutlass.range_constexpr(4):
values[token, row] = self.shared_dot(packed[row, None, None], hq, token)
@cute.jit
def lane_word(self, values, token, lane):
lo = values[token, 0] if lane == 0 else values[token, 2]
hi = values[token, 1] if lane == 0 else values[token, 3]
return bf16_word(lo, hi)
@cute.jit
def store_word(self, output, par3, token, column, word):
row = par3 * output.shape[1] + token
mailbox_store(output.iterator + row * 7168 + 2 * column, (word,))
@cute.jit
def stream_pairs(self, down, hq, output, par3, begin_pair, end_pair, warp, lane):
packed = cute.make_rmem_tensor((4, 3, 2), Uint32)
values = cute.make_rmem_tensor((self.batch, 4), Float32)
for pair in range(begin_pair + 2 * warp, end_pair, 32):
self.load_down(down, packed, pair, end_pair, lane)
for token in cutlass.range_constexpr(self.batch):
self.down_dots(packed, values, hq, token)
if (lane < 2) & (pair + lane < end_pair):
word = self.lane_word(values, token, lane)
self.store_word(output, par3, token, pair + lane, word)
@cute.jit
def shared_outputs(self, down, hsm, output, par3, first, end, warp, lane):
begin_pair, end_pair = (first + 1) // 2, end // 2
hq = [
[
load_f32x4(hsm.iterator + t * 384 + 4 * (lane + 32 * c), shared=True)
for c in range(3)
]
for t in range(self.batch)
]
packed = cute.make_rmem_tensor((4, 3, 2), Uint32)
values = cute.make_rmem_tensor((self.batch, 4), Float32)
saved = cute.make_rmem_tensor((3, self.batch), Uint32)
fits = (end_pair - begin_pair) <= 96
if cutlass.const_expr(self.batch == 2):
for iteration in cutlass.range_constexpr(3):
pair = begin_pair + 2 * warp + 32 * iteration
self.load_down(down, packed, pair, end_pair, lane, fits)
for token in cutlass.range_constexpr(2):
self.down_dots(packed, values, hq, token)
saved[iteration, token] = self.lane_word(values, token, lane)
if cutlass.const_expr(self.batch == 1) or not fits:
self.stream_pairs(down, hq, output, par3, begin_pair, end_pair, warp, lane)
boundary = cute.make_rmem_tensor(self.batch, Uint32)
if ((end & 1) != 0) & (warp == 0):
edge = end // 2
for row in cutlass.range_constexpr(2):
for chunk in cutlass.range_constexpr(3):
words = early_weights(
down.iterator + (2 * edge + row) * 384 + 4 * (lane + 32 * chunk)
)
packed[row, chunk, 0], packed[row, chunk, 1] = words[0], words[1]
for token in cutlass.range_constexpr(self.batch):
lo = self.shared_dot(packed[0, None, None], hq, token)
hi = self.shared_dot(packed[1, None, None], hq, token)
boundary[token] = bf16_word(lo, hi)
if cutlass.const_expr(self.batch == 2):
for iteration in cutlass.range_constexpr(3):
pair = begin_pair + 2 * warp + 32 * iteration
if fits & (lane < 2) & (pair + lane < end_pair):
for token in cutlass.range_constexpr(2):
word = saved[iteration, token]
self.store_word(output, par3, token, pair + lane, word)
if ((end & 1) != 0) & (warp == 0) & (lane == 0):
for token in cutlass.range_constexpr(self.batch):
self.store_word(output, par3, token, end // 2, boundary[token])
@cute.kernel
def kernel(self, args):
router, bias, xmoe, epoch, scores, ids, weights, down, latent = args[0:9]
gate_up, hg, shared_down, output, designated, par, par3, rank = args[9:17]
multicast_base, scale, situ = args[17], args[18], args[19:23]
tid, warp, lane, cta = thread()
router_first, router_rows = split(896, cta, self.grid)
pair_first, pairs = split(112, cta, self.grid)
gate_first, gate_rows = Int32(0), Int32(0)
if cta >= self.batch:
part, parts = cta - self.batch, self.grid - self.batch
gate_first, gate_rows = split(768, part, parts)
slots = alloc(cutlass.utils.SmemAllocator(), Uint8, self.shared_bytes).iterator
top, size = 15 * 14336, 256 * self.batch
partials = view(slots, top, Float32, size)
latent_partial = view(slots, top + 4 * size, Float32, (16, 8), (8, 1))
gate_partial = view(slots, top + 4 * size + 512, Float32, size)
bars = cute.recast_ptr(slots + top + 8 * size + 512, dtype=Uint64)
roles = view(slots, top + 8 * size + 528, Int32, 2)
hsm = view(slots, 0, Float32, self.batch * 384)
gate_args = (slots, gate_rows, gate_first, bars, gate_partial, hg, par, tid)
cute.arch.griddepcontrol_launch_dependents()
active_lane = lane < 28
first_chunk = 56 * warp + (lane if active_lane else 0)
h2_weights = zeros((2, 4 if self.batch == 2 else 2, 4), Uint32)
for row in cutlass.range_constexpr(2):
for chunk in cutlass.range_constexpr(4 if self.batch == 2 else 2):
valid = active_lane & (pairs > 0)
index = first_chunk + 28 * chunk
if cutlass.const_expr(self.batch == 2):
valid = (warp < 7) & (pairs > 0)
index = 32 * warp + lane + 224 * chunk
if valid:
pointer = down.iterator + (2 * pair_first + row) * 7168 + index * 8
words = early_weights(pointer, count=4)
for j in cutlass.range_constexpr(4):
h2_weights[row, chunk, j] = words[j]
if tid == 0:
init_barriers(bars, 2, async_proxy=True)
claimed = self.placement.claim(epoch, designated, par, cta)
stage_rows(slots, 14336, 0, router, router_first, router_rows, bars)
if cta == 0:
mailbox_store(epoch.iterator + (par ^ 1) * 32, (Uint32(0),))
self.placement.rearm(epoch, par)
observed = mailbox_load(epoch.iterator + par * 32)[0]
while observed < 28 * self.batch:
sleep_ns(32)
observed = mailbox_load(epoch.iterator + par * 32)[0]
role, count = self.placement.role(epoch, par, cta, claimed, self.grid)
roles[0], roles[1] = role, count
else:
if (tid >= 32) & (tid < 32 + self.batch * router_rows):
offset = tid - 32
token, row = offset // router_rows, offset % router_rows
index = ((par ^ 1) * self.batch + token) * 896 + router_first + row
mailbox_store(scores.iterator + index * 2, (Uint32(0x80000000),) * 2)
if (tid >= 64) & (tid < 64 + self.batch * gate_rows):
offset = tid - 64
token, row = offset // max(gate_rows, 1), offset % max(gate_rows, 1)
index = ((par ^ 1) * self.batch + token) * 768 + gate_first + row
hg[index] = Uint32(0)
vectors: cutlass.Constexpr = xmoe.shape[1] * xmoe.shape[2] * 896
rearm = ((xmoe, vectors, vectors), (latent, 3584, self.batch * 448))
for tensor, stride, count in rearm:
for vector in range(cta * 511 + tid - 1, count, self.grid * 511):
pointer = tensor.iterator + (par ^ 1) * stride * 8 + vector * 8
mailbox_store(pointer, (Uint32(0x80000000),) * 4)
my_bias = Float32(0)
if tid < self.batch * router_rows:
my_bias = bias[router_first + tid % router_rows]
cute.arch.sync_threads()
role, count = roles[0], roles[1]
shared_resident = role >= 0
consumer = shared_resident & ((role & 0x10000) != 0)
dense = role & 0xFFFF
consumer_token = dense if self.batch == 2 else Int32(0)
shared_first, shared_end = Int32(0), Int32(0)
if shared_resident:
shared_first = 7168 * dense // count
shared_end = 7168 * (dense + 1) // count
h2 = (warp, lane, pairs) if self.batch == 2 else None
words_x, words_h2 = gather_x(
xmoe, par, cta, slots + 7 * 14336, first_chunk, active_lane, self.batch, h2
)
if cutlass.const_expr(self.batch == 1):
for row in cutlass.range_constexpr(2):
value = Float32(0)
if pairs > 0:
for chunk in cutlass.range_constexpr(2):
for j in cutlass.range_constexpr(4):
w0, w1 = unpack_bf16(h2_weights[row, chunk, j])
x0, x1 = unpack_bf16(words_x[0, chunk, j])
value = fma(w0, x0, fma(w1, x1, value))
value = warp_sum(value, 32)
if lane == 0:
latent_partial[warp, row] = value
elif (warp < 7) & (pairs > 0):
for token in cutlass.range_constexpr(2):
for row in cutlass.range_constexpr(2):
value = Float32(0)
for chunk in cutlass.range_constexpr(4):
for j in cutlass.range_constexpr(4):
w0, w1 = unpack_bf16(h2_weights[row, chunk, j])
x0, x1 = unpack_bf16(words_h2[token, chunk, j])
value = fma(w1, x1, fma(w0, x0, value))
value = warp_sum(value, 32)
if lane == 0:
latent_partial[warp, 2 * token + row] = value
cute.arch.sync_warp()
if cutlass.const_expr(self.batch == 1):
if warp == 1:
cute.arch.barrier(barrier_id=3, number_of_threads=512)
else:
named_barrier(3, 512, arrive=True)
elif (warp == 1) & (pairs > 0):
cute.arch.barrier(barrier_id=3, number_of_threads=224)
elif (warp < 7) & (pairs > 0):
named_barrier(3, 224, arrive=True)
if (warp == 1) & (pairs > 0) & (lane < self.batch):
lo, hi = Float32(0), Float32(0)
for w in cutlass.range_constexpr(16 if self.batch == 1 else 7):
lo += latent_partial[w, 2 * lane]
hi += latent_partial[w, 2 * lane + 1]
word = bf16_word(lo, hi)
offset = par * 8 * 3584 + lane * 3584 + rank * 224 + 2 * pair_first
multicast_words(multicast_base + Uint64(offset * 2), (word,), kind="b32")
if warp == 1:
cute.arch.sync_warp()
if tid == 0:
if gate_rows > 0:
cute.arch.mbarrier_arrive_and_expect_tx(bars + 1, gate_rows * 14336)
dst, src = slots + 9 * 14336, gate_up.iterator + gate_first * 7168
copy_bulk(dst, src, bars + 1, gate_rows * 14336)
if shared_resident & (shared_end > shared_first):
src = shared_down.iterator + shared_first * 384
prefetch_bytes(src, (shared_end - shared_first) * 768)
wait(bars)
self.gemv(slots, router_rows, slots + 7 * 14336, partials, warp, lane)
cute.arch.sync_threads()
if tid < self.batch * router_rows:
token, row = tid // router_rows, tid % router_rows
logit = Float32(0)
for w in cutlass.range_constexpr(16):
logit += partials[(w * 16 + row) * self.batch + token]
score = sigmoid(logit)
record = score_record(score, score + my_bias)
index = (par * self.batch + token) * 896 + router_first + row
mailbox_store(scores.iterator + index * 2, record)
if (gate_rows > 0) & (~consumer):
self.gate_pass(*gate_args)
cute.arch.sync_threads()
if consumer & (warp < 8):
top16 = Top16Fast(self.batch)
selected = top16.run(scores, ids, slots, par, consumer_token)
if warp == 0:
score = selected[lane] if lane < 16 else Float32(0)
total = warp_sum(score, 16)
if lane < 16:
normalized = score
if cutlass.const_expr(self.renorm):
# Like the TP8 front and vLLM's grouped_topk kernels.
normalized = div_rn(score, total + Float32(1e-20))
bits = Uint32(gate_bf16_bits(normalized * scale))
if bits == 0x8000:
bits = Uint32(0)
high = cute.arch.shuffle_sync_down(bits, 1, mask=0xFFFF)
if (lane & 1) == 0:
slot = par3 * weights.shape[1] + consumer_token
pointer = weights.iterator + slot * 16 + lane
mailbox_store(pointer, (bits | (high << 16),))
cute.arch.sync_warp()
if consumer & (gate_rows > 0):
cute.arch.sync_threads()
self.gate_pass(*gate_args)
if shared_resident:
if consumer:
cute.arch.sync_threads()
for element in range(tid, self.batch * 384, 512):
token, column = element // 384, element % 384
gp = hg.iterator + (par * self.batch + token) * 768 + column
gate, up = self.poll_flag(gp), self.poll_flag(gp + 384)
result = shared_situ(bf16(gate, True), bf16(up, True), *situ)
hsm[element] = Float32(BFloat16(result))
cute.arch.sync_threads()
self.shared_outputs(
shared_down, hsm, output, par3, shared_first, shared_end, warp, lane
)
cute.arch.sync_threads()
# One-thread exit wait: completion then covers the o-proj consumer (and its
# mailbox re-arm) for later waits.
if (cta == 0) & (tid == 0):
cute.arch.griddepcontrol_wait()
@@ -0,0 +1,361 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""TP8 B1 small-batch front (full router staged in SMEM, x_moe polling, threshold top16,
latent multicast and gather, shared gate/up) and the shared-expert finisher."""
import cutlass
import cutlass.cute as cute
from cutlass import BFloat16, Float32, Int32, Uint8, Uint32, Uint64
from ...common.primitives import (
alloc,
bf16,
copy_bulk,
copy_g2s,
div_rn,
early_weights,
fma,
gate_bf16_bits,
init_barriers,
launch,
load_f32x4,
mailbox_load,
mailbox_store,
mma_into,
multicast_words,
named_barrier,
published,
routing_add,
routing_sigmoid,
routing_weight,
sigmoid,
smem_words,
split,
store_gate_mailbox,
store_relaxed,
table_load,
thread,
unpack_bf16,
view,
wait,
warp_sum,
zeros,
)
from ..routing import Top16Fast, bf16_word, gather_x, score_record, stage_rows
class MoeSharedTP8:
ring_bytes = 57344
row_bytes = 14400
lsu_rows = 92
shared_bytes = ring_bytes + row_bytes + lsu_rows * 1536 + 4096 + 24
def __init__(self, grid=52):
self.grid = grid
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, self.grid, 512, min_blocks_per_mp=1)
@cute.jit
def stage(self, dst, down, low, high, barrier):
cute.arch.mbarrier_arrive_and_expect_tx(barrier, (high - low) * 1536)
if high > low:
copy_bulk(dst, down.iterator + low * 768, barrier, (high - low) * 1536)
@cute.jit
def rows_out(self, base, low, start, high, hsm, output, par3, lane):
packed = cute.make_rmem_tensor((6, 2), Uint32)
for row in range(start, high, 16):
for chunk in cutlass.range_constexpr(6):
pointer = base + (row - low) * 1536 + (lane + 32 * chunk) * 8
values = smem_words(pointer, count=2)
packed[chunk, 0], packed[chunk, 1] = values[0], values[1]
self.row_out(row, packed, hsm, output, par3, lane)
@cute.jit
def row_out(self, row, packed, hsm, output, par3, lane):
value = Float32(0)
for chunk in cutlass.range_constexpr(6):
h = load_f32x4(hsm.iterator + 4 * (lane + 32 * chunk), shared=True)
w0, w1 = unpack_bf16(packed[chunk, 0])
w2, w3 = unpack_bf16(packed[chunk, 1])
value = fma(w0, h[0], fma(w1, h[1], fma(w2, h[2], fma(w3, h[3], value))))
value = warp_sum(value, 32)
if lane == 0:
pointer = output.iterator + par3 * output.shape[1] * 7168 + row
store_gate_mailbox(pointer, gate_bf16_bits(value))
@cute.kernel
def kernel(self, args):
xmoe, down, tables, hg, output, par, par3 = args[0:7]
tid, warp, lane, cta = thread()
first = 7168 * cta // self.grid
end = 7168 * (cta + 1) // self.grid
first_end = min(end, first + 37)
second_end = min(end, first_end + 9)
lsu_count = min(92, end - second_end)
storage = alloc(cutlass.utils.SmemAllocator(), Uint8, self.shared_bytes, 1024)
ring = storage.iterator
xslot = ring + self.ring_bytes
lsu = xslot + self.row_bytes
partial = lsu + self.lsu_rows * 1536
hsm = view(partial, 1024, Float32, 768)
bars = cute.recast_ptr(partial + 4096, dtype=Uint64)
cute.arch.griddepcontrol_launch_dependents()
if tid == 0:
init_barriers(bars, 3, async_proxy=True)
self.stage(ring, down, first, first_end, bars + 1)
active = lane < 28
gather_x(xmoe, par, cta, xslot, 56 * warp + (lane if active else 0), active)
cute.arch.sync_warp()
if lsu_count > 0:
for vector in range(tid, lsu_count * 96, 512):
dst = lsu + vector * 16
copy_g2s(dst, down.iterator + second_end * 768 + vector * 8)
cute.arch.cp_async_commit_group()
cute.arch.fence_proxy("async.shared", space="cta")
cute.arch.sync_threads()
if tid == 0:
cute.arch.fence_proxy("async.shared", space="cta")
self.stage(xslot, down, first_end, second_end, bars + 2)
if tid < 384:
column = 2 * tid
gate = up = (Uint32(0), Uint32(0))
valid = False
while not valid:
gate = mailbox_load(hg.iterator + par * 2 * 1536 + column, count=2)
up = mailbox_load(hg.iterator + par * 2 * 1536 + 768 + column, count=2)
valid = ((gate[0] & gate[1] & up[0] & up[1]) & Uint32(1)) != 0
ga0 = table_load(tables.iterator + (gate[0] >> 16))
ua0 = table_load(tables.iterator + 65536 + (up[0] >> 16))
ga1 = table_load(tables.iterator + (gate[1] >> 16))
ua1 = table_load(tables.iterator + 65536 + (up[1] >> 16))
hsm[column] = Float32(BFloat16(ga0 * ua0))
hsm[column + 1] = Float32(BFloat16(ga1 * ua1))
cute.arch.sync_threads()
out = (hsm, output, par3, lane)
if lsu_count > 0:
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
self.rows_out(
lsu, second_end, second_end + warp, second_end + lsu_count, *out
)
parts = ((ring, first, first_end), (xslot, first_end, second_end))
for part, (base, low, high) in enumerate(parts):
if high > low:
if warp == 0:
wait(bars + 1 + part)
cute.arch.sync_threads()
self.rows_out(base, low, low + warp, high, *out)
if cta == 0:
cute.arch.griddepcontrol_wait()
class SharedSituTables:
"""Separate FP32 gate/up lookup tables over all BF16 patterns."""
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, 256, 256, pdl=False)
@cute.kernel
def kernel(self, args):
tables, beta, linear_beta = args[0:3]
tid, _, _, cta = thread()
index = 256 * cta + tid
value = bf16(Uint32(index))
gate = div_rn(value, beta)
probability = sigmoid(value)
tables[index] = (beta * cute.math.tanh(gate, fastmath=False)) * probability
up = value
if linear_beta > 0:
scaled = div_rn(value, linear_beta)
up = linear_beta * cute.math.tanh(scaled, fastmath=False)
tables[65536 + index] = up
class MoeFrontTP8:
row_bytes = 14400
shared_bytes = 15 * row_bytes + (16 * 16 + 16 * 4) * 4 + 24
def __init__(self, grid=140, *, renorm=True):
self.grid = grid
self.renorm = renorm
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, self.grid, 512, min_blocks_per_mp=1)
@cute.jit
def gemv(self, rows, nrows, activation, partials, warp, lane):
group, quarter = lane // 4, lane % 4
acc = zeros((8, 4), Float32)
for iteration in cutlass.range_constexpr(14):
offset = (warp * 448 + iteration * 32 + 8 * quarter) * 2
lo = hi = x = (Uint32(0),) * 4
if group < nrows:
lo = smem_words(rows + group * self.row_bytes + offset)
if group + 8 < nrows:
hi = smem_words(rows + (group + 8) * self.row_bytes + offset)
if group < 1:
x = smem_words(activation + offset)
for half in cutlass.range_constexpr(2):
chain: cutlass.Constexpr = (2 * iteration + half) % 8
a = (lo[2 * half], hi[2 * half], lo[2 * half + 1], hi[2 * half + 1])
mma_into(acc[chain, None], a, (x[2 * half], x[2 * half + 1]), bf16=True)
if quarter == 0:
for half in cutlass.range_constexpr(2):
comp: cutlass.Constexpr = 2 * half
partials[warp * 16 + group + 8 * half] = (
(acc[0, comp] + acc[1, comp]) + (acc[2, comp] + acc[3, comp])
) + ((acc[4, comp] + acc[5, comp]) + (acc[6, comp] + acc[7, comp]))
@cute.jit
def gate_pass(self, rows, count, start, barrier, ctx):
activation, partials, hg, par, tid, warp, lane = ctx
wait(barrier)
self.gemv(rows, count, activation, partials, warp, lane)
cute.arch.sync_threads()
if tid < count:
value = Float32(0)
for w in cutlass.range_constexpr(16):
value += partials[w * 16 + tid]
bits = Uint32(gate_bf16_bits(value))
pointer = hg.iterator + (par * 2) * 1536 + start + tid
mailbox_store(pointer, ((bits << 16) | Uint32(1),))
cute.arch.sync_threads()
@cute.kernel
def kernel(self, args):
router, bias, xmoe, epoch, scores, ids, weights, down = args[:8]
latent, latent_out, gate_up, hg, par, par3, rank, multicast_base = args[8:16]
scale = args[16]
tid, warp, lane, cta = thread()
first_router, router_rows = split(896, cta, self.grid)
first_pair, pairs = split(224, cta, self.grid)
gu_start, gu_rows = Int32(0), Int32(0)
if cta > 0:
gu_start, gu_rows = split(1536, cta - 1, self.grid - 1)
gu_first = min(gu_rows, 7)
gu_second = gu_rows - gu_first
storage = alloc(cutlass.utils.SmemAllocator(), Uint8, self.shared_bytes, 128)
slots = storage.iterator
activation = slots + 7 * self.row_bytes
dst = (slots, self.row_bytes)
top = 15 * self.row_bytes
partials = view(slots, top, Float32, 256)
latent_partial = view(slots, top + 1024, Float32, (16, 4), (4, 1))
bars = cute.recast_ptr(slots + top + 1280, dtype=Uint64)
cute.arch.griddepcontrol_launch_dependents()
active_lane = lane < 28
first_chunk = 56 * warp + (lane if active_lane else 0)
second_chunk = first_chunk + 28
latent_weights = zeros((4, 2, 4), Uint32)
for row in cutlass.range_constexpr(4):
if active_lane & (row < 2 * pairs):
for chunk in cutlass.range_constexpr(2):
col = first_chunk if chunk == 0 else second_chunk
pointer = down.iterator + (2 * first_pair + row) * 7168 + 8 * col
words = early_weights(pointer, count=4)
for j in cutlass.range_constexpr(4):
latent_weights[row, chunk, j] = words[j]
cute.arch.cp_async_commit_group()
if tid == 0:
init_barriers(bars, 3, async_proxy=True)
stage_rows(*dst, 0, router, first_router, router_rows, bars)
if cta == 0:
mailbox_store(epoch.iterator + (par ^ 1) * 32, (Uint32(0),))
observed = mailbox_load(epoch.iterator + par * 32)[0]
while observed < 28:
observed = mailbox_load(epoch.iterator + par * 32)[0]
else:
for i in range(tid - 1, 2 * router_rows, 511):
token, row = i // router_rows, i % router_rows
index = ((par ^ 1) * 2 + token) * 896 + first_router + row
mailbox_store(scores.iterator + index * 2, (Uint32(0x80000000),) * 2)
if cta > 0:
for i in range(tid - 1, 2 * gu_rows, 511):
token, row = i // gu_rows, i % gu_rows
hg[(par ^ 1) * 2 * 1536 + token * 1536 + gu_start + row] = Uint32(0)
slot_vectors: cutlass.Constexpr = xmoe.shape[1] * xmoe.shape[2] * 896
for i in range(cta * 511 + tid - 1, slot_vectors, self.grid * 511):
pointer = xmoe.iterator + (par ^ 1) * slot_vectors * 8 + i * 8
mailbox_store(pointer, (Uint32(0x80000000),) * 4)
my_bias = Float32(0)
if tid < router_rows:
my_bias = bias[first_router + tid]
cute.arch.sync_threads()
input_words, _ = gather_x(xmoe, par, cta, activation, first_chunk, active_lane)
for row in cutlass.range_constexpr(4):
if row < 2 * pairs:
total = Float32(0)
for chunk in cutlass.range_constexpr(2):
for j in cutlass.range_constexpr(4):
w0, w1 = unpack_bf16(latent_weights[row, chunk, j])
x0, x1 = unpack_bf16(input_words[0, chunk, j])
total = fma(w0, x0, fma(w1, x1, total))
total = warp_sum(total, 32)
if lane == 0:
latent_partial[warp, row] = total
cute.arch.sync_warp()
if warp == 1:
cute.arch.barrier(barrier_id=3, number_of_threads=512)
if lane < pairs:
lo, hi = Float32(0), Float32(0)
for w in cutlass.range_constexpr(16):
lo += latent_partial[w, 2 * lane]
hi += latent_partial[w, 2 * lane + 1]
packed = bf16_word(lo, hi)
offset = par * 8 * 3584 * 2 + rank * 896 + (first_pair + lane) * 4
multicast_words(multicast_base + Uint64(offset), (packed,), kind="b32")
else:
named_barrier(3, 512, arrive=True)
if tid == 0: # noqa: SIM102
if gu_first > 0:
stage_rows(*dst, 8, gate_up, gu_start, gu_first, bars + 1)
wait(bars)
self.gemv(slots, router_rows, activation, partials, warp, lane)
cute.arch.sync_threads()
if tid == 0: # noqa: SIM102
if gu_second > 0:
cute.arch.fence_proxy("async.shared", space="cta")
stage_rows(*dst, 0, gate_up, gu_start, gu_second, bars + 2, gu_first)
if tid < router_rows:
logit = Float32(0)
for w in cutlass.range_constexpr(16):
logit += partials[w * 16 + tid]
score = routing_sigmoid(logit)
record = score_record(score, routing_add(score, my_bias))
index = par * 2 * 896 + first_router + tid
mailbox_store(scores.iterator + index * 2, record)
if cta == 0:
if warp < 8:
selected = Top16Fast(2).run(scores, ids, slots, par, 0)
if warp == 0:
score = selected[lane] if lane < 16 else Float32(0)
total = warp_sum(score, 16)
if lane < 16:
value = routing_weight(score, total, scale, renorm=self.renorm)
pointer = weights.iterator + par3 * weights.shape[1] * 16 + lane
store_gate_mailbox(pointer, gate_bf16_bits(value))
cute.arch.sync_warp()
for vector in range(tid, 448, 256):
pointer = latent.iterator + par * 8 * 3584 + vector * 8
words = mailbox_load(pointer, count=4, scope="sys")
ready = published(words)
while not ready:
words = mailbox_load(pointer, count=4, scope="sys")
ready = published(words)
mailbox_store(latent_out.iterator + vector * 8, words)
store_relaxed(pointer, (Uint32(0x80000000),) * 4)
cute.arch.griddepcontrol_wait()
else:
cute.arch.sync_threads()
ctx = (activation, partials, hg, par, tid, warp, lane)
if gu_first > 0:
self.gate_pass(
slots + 8 * self.row_bytes, gu_first, gu_start, bars + 1, ctx
)
if gu_second > 0:
self.gate_pass(slots, gu_second, gu_start + gu_first, bars + 2, ctx)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,241 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Routed MoE tail: one fused kernel finalizes this rank's top-16 rows, multicasts the
latent partial and peer-stores the shared shard (three rotating parities), all-reduces
and RMS-normalizes per K-slice, runs the K-split up projection and multicasts the output
row into the next AttnRes mailbox. At TP8 B5+ and TP16 B9+ the experts publish the
latent partial, WaveMoeTail.push the shared shard and WaveMoeTail does the rest. At
``twoshot_batches`` the latent all-reduce is ``common.twoshot``: the experts push each
partial chunk to its column owner, whose WaveMoeTail CTAs sum and multicast it."""
from functools import cache
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
import torch
from cutlass import BFloat16, Float32, Int32, Int64, Uint8
from cutlass.cute.runtime import make_fake_stream
from ..common.host import fake, prepare_shared_carveout, stream, tensor_maps, to_cute
from ..common.policy import batch_table
from ..common.twoshot import TwoShot
from ..oproj.prepared import OprojMailbox
from .tail import MoeTail, WaveMoeTail
# MoE tail mailbox tokens (padded decode batches); small-batch front_poll G/W/S rows per
# parity.
CAP, POLL_CAP = 64, 8
# Two-shot latent all-reduce batches per TP (B9+): 2-/4-node GB300, us/layer of RS push
# + experts + WaveMoeTail, max over ranks, multicast vs two-shot: TP8 B16 41.74/42.80,
# B32 52.20/53.18, B64 79.27/81.50; TP16 B16 33.98/34.49, B32 42.19/42.52, B64
# 65.05/60.51.
TWOSHOT = {8: "", 16: "33-64:1"}
# WaveMoeTail batches that release the next AttnRes at entry.
EARLY = {8: "9-64:1", 16: "9-64:1"}
# WaveMoeTail batches whose up projection runs on tcgen05 (B9+; 1-GPU WaveMoeTail us
# mma.sync vs tcgen05, B16/32/64: TP8 4.53/5.80/9.05 vs 4.45/5.26/7.80, TP16 two-shot
# 4.50/5.39/7.75 vs 4.52/5.46/7.21).
UMMA = {8: "9-64:1", 16: "33-64:1"}
def flat(t):
return to_cute(t.reshape(-1), 16)
def published(tp, batch):
"""The experts publish the latent and WaveMoeTail runs the tail (else MoeTail
front)."""
return batch > (4 if tp == 8 else 8)
def twoshot_batches(tp):
table = batch_table(TWOSHOT.get(tp, ""))
return {batch for batch, on in table.items() if on and batch > 8}
def early_batches(tp):
table = batch_table(EARLY.get(tp, ""))
return {batch for batch, on in table.items() if on and published(tp, batch)}
def umma_batches(tp):
table = batch_table(UMMA.get(tp, ""))
return {batch for batch, on in table.items() if on and batch > 8}
def shares(tp, batch, twoshot=False):
"""WaveMoeTail clusters share reduced latent rows (all 112 CTAs must be
co-resident)."""
return published(tp, batch) and WaveMoeTail(batch, tp, twoshot).reduce
@cache
def resident_clusters(device):
"""Co-resident 16-CTA clusters at one CTA per SM."""
with torch.accelerator.device_index(device):
return cutlass.utils.HardwareInfo(device).get_max_active_clusters(16)
class MoeTailBox:
"""Model-owned MoE tail mailboxes: latent [3, TP, CAP, 3584] (multicast), shared
reduce-scatter [3, TP, CAP, 7168/TP] (peer stores), output [1, CAP, 7168], the
local all-reduced latent rows [3, CAP, 3584] that WaveMoeTail clusters share and the
two-shot latent buffers (``twoshot_args``/``twoshot_tail`` replace args/tail)."""
def __init__(self, group, device, tp, rank):
self.rank = rank
self.latent = OprojMailbox(group, device, tp, rank, (3, tp, CAP, 3584))
self.rs = OprojMailbox(group, device, tp, rank, (3, tp, CAP, 7168 // tp))
self.out = OprojMailbox(group, device, tp, rank, (1, CAP, 7168))
ptrs = self.rs.handle.buffer_ptrs
self.peers = torch.tensor(ptrs, dtype=torch.int64, device=device)
self.args = (flat(self.latent.buffer), flat(self.rs.buffer))
self.tail: tuple = (to_cute(self.peers, 16), flat(self.out.buffer))
self.tail += (Int64(self.latent.mc), Int64(self.out.mc))
self.reduced = torch.empty((3, CAP, 3584), dtype=torch.bfloat16, device=device)
self.reduced.view(torch.int32).fill_(-0x80000000)
self.twoshot = box = TwoShot(group, device, tp, rank, 3584, CAP, parities=3)
self.twoshot_args = (flat(box.recv), self.args[1])
self.twoshot_tail = (*self.tail[:2], Int64(box.gather_mc), self.tail[3])
# Experts' push: every parity's receive slots [3 x TP, CAP, 3584/TP] and the
# owners' buffer bases (slot parity x TP + rank).
self.push_box = (
to_cute(box.recv.view(3 * tp, CAP, -1)),
to_cute(box.peers[0], 16),
)
@cache
def compile_tail(tp, batch, device, mode="front", trigger=2, early=False, umma=False):
"""front: rows complete at the PDL wait; front_poll: three 0xffff-armed G/W/S parity
slots polled per row and re-armed after use; publish (``published``): WaveMoeTail
over the published mailboxes, reading the weight shard directly, not by TMA;
twoshot: publish over the two-shot receive slots and gather."""
m8, twoshot = mode in ("publish", "twoshot"), mode == "twoshot"
vec = lambda n, dtype=BFloat16, align=16: fake((n,), None, dtype, align)
slots = 3 * POLL_CAP if mode == "front_poll" else batch
latent = 3 * tp * CAP * (3584 // tp if twoshot else 3584)
tensors: tuple = (vec(latent), vec(3 * tp * CAP * (7168 // tp)))
weights = vec(7168 // tp * 3584) if m8 and not umma else vec(128, Uint8, 64)
tensors += (weights, vec(3584))
tensors += (vec(slots * 16 * 3584), vec(slots * 16), vec(slots * 7168))
tensors += (vec(tp, Int64), vec(CAP * 7168))
scalars: tuple = (Int64(0), Int64(0), Int32(0), Int32(0), Int32(CAP), Float32(1e-6))
scalars += (Int32(2), Int32(CAP))
kernel: MoeTail
if m8:
kernel = wave = WaveMoeTail(batch, tp, twoshot, early, umma)
scalars += (vec(3 * CAP * 3584),) if wave.reduce else ()
else:
kernel = MoeTail(tp, batch, mode=mode, trigger=trigger)
options = "" if m8 else "--ptxas-options=--maxrregcount=120"
with torch.accelerator.device_index(device):
args = (*tensors, *scalars, make_fake_stream())
code = cute.compile(kernel, *args, options=options)
# MoE tail requests 100% shared carveout so the next large PDL kernel can
# coexist.
return prepare_shared_carveout(code, device, 100)
@cache
def compile_push(batch, device, tp=8):
args = (fake((batch, 7168)), fake((tp,), None, Int64), Int32(0), Int32(0))
with torch.accelerator.device_index(device):
return cute.compile(
WaveMoeTail(batch, tp).push, *args, Int32(CAP), make_fake_stream()
)
class MoeTailWeights:
"""One layer's up-projection shard [7168/TP, 3584], RMS weight and TMA map."""
def __init__(self, tp, weight, gamma, eps, ordinal, count, batches, twoshots=()):
self.weight, self.gamma = weight, gamma
# Three parities rotate over the model's MoE layers; each launch re-arms the
# previous MoE layer's slots of its batch (wrapping across decode steps, where
# the first layer re-arms the largest batch the last layer may have published).
parity, rearm = ordinal % 3, (ordinal - 1) % count % 3
if parity == rearm: # a launch must never re-arm the slots it publishes to
raise ValueError(f"MoE tail parities collide for {count} MoE layers")
self.parity, rearm = Int32(parity), Int32(rearm)
self.scalars = (Int32(CAP), Float32(eps), rearm)
self.wrap = 0 if ordinal else max(batches)
cols = 512 if tp == 8 else 448
u16 = cuda.CUtensorMapDataType.CU_TENSOR_MAP_DATA_TYPE_UINT16
dims = (cols // 2, 3584 // (cols // 2), 7168 // tp)
spec: tuple = (weight.data_ptr(), u16, dims, (cols, 7168), (cols // 2, 2, 32))
spec += (cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_NONE,)
spec += (cuda.CUtensorMapL2promotion.CU_TENSOR_MAP_L2_PROMOTION_NONE,)
self.descriptor = tensor_maps([spec], weight.device)
self.views = (to_cute(self.descriptor, 64), to_cute(gamma, 16))
self.codes, device, early = {}, weight.device.index, early_batches(tp)
umma = umma_batches(tp)
if any(published(tp, b) and b in umma for b in batches):
# WaveMoeTail tcgen05: [64 x lines, 64] boxes of the shard, 128B swizzle
spec = (weight.data_ptr(), u16, (3584, 7168 // tp), (7168,))
spec += ((64, 128 if tp == 8 else 64),)
spec += (cuda.CUtensorMapSwizzle.CU_TENSOR_MAP_SWIZZLE_128B,)
spec += (cuda.CUtensorMapL2promotion.CU_TENSOR_MAP_L2_PROMOTION_L2_256B,)
self.umma_map = tensor_maps([spec], weight.device)
self.umma_views = (to_cute(self.umma_map, 64), self.views[1])
for b in batches: # WaveMoeTail reads the shard directly, not by TMA
twoshot = b in twoshots
mode = "twoshot" if twoshot else "publish" if published(tp, b) else "front"
views = self.views if mode == "front" else (flat(weight), self.views[1])
on = mode != "front" and b in umma
views = self.umma_views if on else views
code = compile_tail(tp, b, device, mode, early=b in early, umma=on)
self.codes[b] = code, views, shares(tp, b, twoshot), twoshot
shared = any(shares(tp, b, b in twoshots) for b in batches)
if shared and resident_clusters(device) < 7:
raise RuntimeError("WaveMoeTail needs seven co-resident 16-CTA clusters")
def run(self, box, expanded, weights, shared, code=None, batch=None):
batch = batch or weights.shape[0]
if code is None:
code, views, share, twoshot = self.codes[batch]
else:
views, share, twoshot = self.views, 0, False
args, tail = box.args, box.tail
if twoshot:
args, tail = box.twoshot_args, box.twoshot_tail
rows = (flat(expanded), flat(weights), flat(shared), *tail, self.parity)
rows += (Int32(box.rank), *self.scalars, Int32(self.wrap or batch))
if share:
rows += (flat(box.twoshot.gather if twoshot else box.reduced),)
code(*args, *views, *rows, stream(expanded.device))
class PreparedTail:
"""The layer's MoE tail; placement = (model MoeTailBox, MoE ordinal, MoE layer
count). At WaveMoeTail batches the experts publish into latent slot ``publish[b]``
(multicast, parity, rank; at ``twoshots`` the two-shot push box) and the side
stream pushes the shared-expert output (``push``)."""
def __init__(self, op, norm, up, batches, placement):
tp, rank = op.contract.tp_size, op.rank
local_up = up.narrow(0, rank * (7168 // tp), 7168 // tp)
self.tail_box, ordinal, count = placement
eps = op.contract.rms_eps
m8 = [b for b in batches if published(tp, b)]
self.twoshots = twoshot_batches(tp) & set(m8)
args = (tp, local_up, norm, eps, ordinal, count, batches, self.twoshots)
self.tail_weights = MoeTailWeights(*args)
self.peers, _, mc, _ = self.tail_box.tail
self.ring = (self.tail_weights.parity, Int32(rank))
self.pushers = {b: compile_push(b, norm.device.index, tp) for b in m8}
self.publish = {b: (mc, *self.ring) for b in m8}
slot = Int32(ordinal % 3 * tp + rank)
self.publish.update({b: (*self.tail_box.push_box, slot) for b in self.twoshots})
def push(self, shared):
if shared.shape[0] in self.pushers:
args = (to_cute(shared), self.peers, *self.ring, Int32(CAP))
self.pushers[shared.shape[0]](*args, stream(shared.device))
def run(self, expanded, weights, shared, out, *, consume):
self.tail_weights.run(self.tail_box, expanded, weights, shared)
if consume(out) is not out:
raise RuntimeError("The tail output must be consumed from its mailbox ring")
return out
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,423 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""O-projection all-reduce consumer (``groups`` 128-thread groups poll disjoint
rank slots, group 0 sums in rank order into the AttnRes) and TP16 fused producer +
consumer. With ``twoshot`` the mailbox is the one-slot gather row of
``common.twoshot``: the threads owning this rank's columns first reduce their
receive slots into it (args 11-13: receive view, gather multicast address, rank)."""
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, Uint32
from ..attn_res.kernel import arm_exchanges, rstd
from ..common.mailbox import rearm
from ..common.primitives import (
add_rn,
alloc,
asm,
at,
bits_float,
copy_g2s,
float_bits,
fma,
launch,
load_vector,
named_barrier,
pack_pair,
publish_hint,
published,
push_float,
smem_words,
store_relaxed,
store_shared_words,
store_words,
unpack_bf16,
zeros,
)
from ..common.primitives import wait_phase_zero_cluster as wait_phase_zero
from ..common.twoshot import reduce
from .producer import OprojProducer, pack8, tree_reduce
@asm((Float32,) * 2, pure=True)
def pair_op(a, b, c=(), *, op="add"):
values = (*a, *b, *c)
code = "{ .reg .b64 a,b,c,d; mov.b64 a,{$2,$3}; mov.b64 b,{$4,$5}; "
if op == "fma":
code += "mov.b64 c,{$6,$7}; fma.rn.f32x2 d,a,b,c; "
else:
code += f"{op}.rn.f32x2 d,a,b; "
constraints = ",".join(["=f", "=f"] + ["f"] * len(values))
return code + "mov.b64 {$0,$1},d; }", constraints, tuple(map(Float32, values))
@asm(Float32, pure=True)
def mul_rn(a, b):
return "mul.rn.f32 $0,$1,$2;", "=f,f,f", (*(Float32(v) for v in (a, b)),)
def two(values, j):
return values[2 * j], values[2 * j + 1]
@cute.jit
def dot8(a, b):
s0 = pair_op(two(a, 0), two(b, 0), op="mul")
s1 = pair_op(two(a, 1), two(b, 1), op="mul")
s0 = pair_op(two(a, 2), two(b, 2), s0, op="fma")
s1 = pair_op(two(a, 3), two(b, 3), s1, op="fma")
s = pair_op(s0, s1)
return add_rn(s[0], s[1])
@cute.jit
def reduce_scatter(values, size: cutlass.Constexpr[int], lane):
"""Warp-reduce ``size`` values; returns the index of the one this lane holds."""
for shift in cutlass.range_constexpr(4, -1, -1):
live: cutlass.Constexpr = max(1, size >> (4 - shift))
half: cutlass.Constexpr = max(1, live // 2)
if cutlass.const_expr(live == 1):
values[0] = add_rn(
values[0], cute.arch.shuffle_sync_bfly(values[0], 1 << shift)
)
else:
upper = (lane & (1 << shift)) != 0
for j in cutlass.range_constexpr(half):
send, keep = values[j + half], values[j]
if upper:
send, keep = values[j], values[j + half]
values[j] = add_rn(keep, cute.arch.shuffle_sync_bfly(send, 1 << shift))
idx = cutlass.Int32(0)
for shift in cutlass.range_constexpr(4, -1, -1):
width: cutlass.Constexpr = size >> (4 - shift)
if cutlass.const_expr(width > 1): # noqa: SIM102
if (lane & (1 << shift)) != 0:
idx += width // 2
return idx
@cute.jit
def fetch(raw, rank, pointer):
words = load_vector(pointer, cache="volatile")
for j in cutlass.range_constexpr(4):
raw[rank, j] = words[j]
class OprojConsumer:
def __init__(
self,
nb,
*,
has_delta=True,
groups=4,
twoshot=False,
):
if not 0 <= nb <= 8:
raise ValueError("Output-projection AttnRes supports zero to eight blocks")
self.nb, self.has_delta = nb, has_delta
self.groups, self.twoshot = groups, twoshot
self.kpre = 1 << max(0, (2 * nb - 1).bit_length())
self.npost = 4 if nb > 0 else 1
@cute.jit
def wait_exchange(self, barrier):
wait_phase_zero(barrier) # peers complete it by st.async
@cute.jit
def allocate(self):
smem = cutlass.utils.SmemAllocator()
wpre = alloc(smem, Float32, (4, 16))
pre = alloc(smem, Float32, (7, 16))
blk = alloc(smem, Float32, 16)
post = alloc(smem, Float32, (7, 4, 4))
total = alloc(smem, Float32, 4)
bars = alloc(smem, cutlass.Uint64, 2, 8)
parts = alloc(smem, Float32, (max(1, self.groups - 1), 2, 128, 4))
lean = self.groups == 1 # above B8: rows for this nb only (co-residency)
pf = alloc(smem, Uint32, (4 + self.nb if lean else 12, 128, 4))
return wpre, pre, blk, post, total, bars, parts, pf
@cute.jit
def setup(self, bars):
arm_exchanges(bars, (2 * self.nb, 4 * self.npost))
cute.arch.cluster_arrive_relaxed()
@cute.jit
def prefetch(self, args, pf, rank, token):
_, prefix, blocks, norm, query, out_norm = args[:6]
tid, _, _ = cute.arch.thread_idx()
h = (rank * 128 + tid) * 8
if cutlass.const_expr(self.nb > 0):
copy_g2s(pf.iterator + tid * 4, norm.iterator + h)
copy_g2s(pf.iterator + 512 + tid * 4, query.iterator + h)
copy_g2s(pf.iterator + 1024 + tid * 4, out_norm.iterator + h)
if cutlass.const_expr(self.has_delta):
copy_g2s(pf.iterator + 1536 + tid * 4, at(prefix, token, h))
for block in cutlass.range_constexpr(self.nb):
dst = pf.iterator + (4 + block) * 512 + tid * 4
copy_g2s(dst, at(blocks, token, block, h))
cute.arch.cp_async_commit_group()
@cute.jit
def fragment(self, pf, row, tid):
words = smem_words(pf.iterator + (row * 128 + tid) * 4)
values = cute.make_rmem_tensor(8, Float32)
for j in cutlass.range_constexpr(4):
values[2 * j], values[2 * j + 1] = unpack_bf16(words[j])
return values
@cute.jit
def combined_weight(self, pf, tid):
a, b = self.fragment(pf, 0, tid), self.fragment(pf, 1, tid)
result = cute.make_rmem_tensor(8, Float32)
for j in cutlass.range_constexpr(4):
result[2 * j], result[2 * j + 1] = pair_op(two(a, j), two(b, j), op="mul")
return result
@cute.jit
def pre(self, state, rank, eps, mix, wait_groups: cutlass.Constexpr[int] = 0):
wpre, pre, blk, _, _, bars, _, pf = state
tid, _, _ = cute.arch.thread_idx()
warp, lane = tid // 32, tid % 32
maximum, exponent_sum, qpart = Float32(0), Float32(0), Float32(0)
cute.arch.cp_async_wait_group(wait_groups)
if cutlass.const_expr(self.nb > 0):
weight = self.combined_weight(pf, tid)
partial = zeros(self.kpre, Float32)
for block in cutlass.range_constexpr(self.nb):
value = self.fragment(pf, 4 + block, tid)
partial[block] = dot8(value, value)
partial[self.nb + block] = dot8(value, weight)
idx = reduce_scatter(partial, self.kpre, lane)
if ((lane & (32 // self.kpre - 1)) == 0) & (idx < 2 * self.nb):
wpre[warp, idx] = partial[0]
named_barrier()
cute.arch.cluster_wait()
if tid < 2 * self.nb:
pre_sum = Float32(0)
for w in cutlass.range_constexpr(4):
pre_sum = add_rn(pre_sum, wpre[w, tid])
for peer in cutlass.range_constexpr(7):
push_float(at(pre, rank, tid), pre_sum, bars.iterator, peer)
self.wait_exchange(bars.iterator)
if tid < 2 * self.nb:
sums = [Float32(0), Float32(0)]
for peer in cutlass.range_constexpr(7):
sums[peer % 2] = add_rn(sums[peer % 2], pre[peer, tid])
blk[tid] = add_rn(*sums)
named_barrier()
logits = [
mul_rn(blk[self.nb + b], rstd(blk[b], eps)) for b in range(self.nb)
]
maximum = tree_reduce(logits, max)
exp = [cute.math.exp(x - maximum, fastmath=True) for x in logits]
exponent_sum = tree_reduce(exp, add_rn)
for block in cutlass.range_constexpr(self.nb):
value = self.fragment(pf, 4 + block, tid)
for j in cutlass.range_constexpr(4):
mix[2 * j], mix[2 * j + 1] = pair_op(
(exp[block], exp[block]), two(value, j), two(mix, j), op="fma"
)
qpart = dot8(mix, mix)
else:
cute.arch.cluster_wait()
return maximum, exponent_sum, qpart
def slots(self, tp):
"""Rank slots each group polls (at least four)."""
return max(4, (tp + self.groups - 1) // self.groups)
@cute.jit
def poll_sum(self, mailbox, token, h, first, count):
n: cutlass.Constexpr = self.slots(mailbox.shape[0])
raw = zeros((n, 4), Uint32)
pending = Int32(0)
for rank in cutlass.range_constexpr(n):
if rank < count:
fetch(raw, rank, at(mailbox, first + rank, token, h))
for rank in cutlass.range_constexpr(n):
if rank < count: # noqa: SIM102
if not published(raw[rank, j] for j in range(4)):
pending |= 1 << rank
while pending != 0:
for rank in cutlass.range_constexpr(n):
if (pending & (1 << rank)) != 0:
fetch(raw, rank, at(mailbox, first + rank, token, h))
for rank in cutlass.range_constexpr(n):
if (pending & (1 << rank)) != 0: # noqa: SIM102
if published(raw[rank, j] for j in range(4)):
pending &= ~(1 << rank)
values = cute.make_rmem_tensor(8, Float32)
for j in cutlass.range_constexpr(4):
pair = unpack_bf16(raw[0, j])
for rank in cutlass.range_constexpr(1, n):
if rank < count:
pair = pair_op(pair, unpack_bf16(raw[rank, j]))
values[2 * j], values[2 * j + 1] = pair[0], pair[1]
return values
@cute.jit
def publish(self, output, xmoe, hints, token, h, values):
tid, _, _ = cute.arch.thread_idx()
words = pack8(values)
store_words(at(output, token, h), words)
if cutlass.const_expr(xmoe is not None):
words = pack8(values, "latent")
for rep in cutlass.range_constexpr(xmoe.shape[0]):
store_relaxed(at(xmoe, rep, token, h), words, scope="gpu")
cute.arch.sync_warp()
if tid % 32 == 0:
publish_hint(hints.iterator)
@cute.jit
def post(self, args, state, block_mix, rank, token):
mailbox, prefix, _, _, _, _, output, xmoe, hints, eps, out_eps = args[:11]
_, _, _, post, total, bars, parts, pf = state
mix, maximum, exponent_sum, qpart = block_mix
thread, _, _ = cute.arch.thread_idx()
group, tid = thread // 128, thread % 128
warp, lane = tid // 32, tid % 32
h = (rank * 128 + tid) * 8
S: cutlass.Constexpr = (mailbox.shape[0] + self.groups - 1) // self.groups
everyone: cutlass.Constexpr = 128 * self.groups
if group > 0:
first = group * S
count = min(S, mailbox.shape[0] - first)
values = self.poll_sum(mailbox, token, h, first, count)
for half in cutlass.range_constexpr(2):
words = tuple(float_bits(values[half * 4 + i]) for i in range(4))
store_shared_words(at(parts, group - 1, half, tid, 0), words)
named_barrier(2, everyone, arrive=True)
for r in cutlass.range_constexpr(self.slots(mailbox.shape[0])):
if r < count:
rearm(at(mailbox, first + r, token, h))
else:
if cutlass.const_expr(self.twoshot):
recv, gather_mc, me = args[11:14]
if h // recv.shape[2] == me:
reduce(recv, mailbox, gather_mc, me, token, h)
if cutlass.const_expr(self.has_delta):
old = self.fragment(pf, 3, tid)
attn = self.poll_sum(mailbox, token, h, 0, min(S, mailbox.shape[0]))
if cutlass.const_expr(self.groups > 1):
named_barrier(2, everyone)
for other in cutlass.range_constexpr(self.groups - 1):
for half in cutlass.range_constexpr(2):
words = smem_words(at(parts, other, half, tid, 0))
for j in cutlass.range_constexpr(2):
k: cutlass.Constexpr = half * 2 + j
part = tuple(map(bits_float, two(words, j)))
attn[2 * k], attn[2 * k + 1] = pair_op(two(attn, k), part)
prefix_value = cute.make_rmem_tensor(8, Float32)
for j in cutlass.range_constexpr(4):
rounded = unpack_bf16(pack_pair(*two(attn, j)))
if cutlass.const_expr(self.has_delta):
rounded = pair_op(two(old, j), rounded)
rounded = unpack_bf16(pack_pair(rounded[0], rounded[1]))
prefix_value[2 * j], prefix_value[2 * j + 1] = rounded[0], rounded[1]
partial = cute.make_rmem_tensor(self.npost, Float32)
if cutlass.const_expr(self.nb > 0):
weight = self.combined_weight(pf, tid)
partial[0], partial[1] = dot8(mix, prefix_value), qpart
partial[2] = dot8(prefix_value, prefix_value)
partial[3] = dot8(prefix_value, weight)
else:
partial[0] = dot8(prefix_value, prefix_value)
idx = reduce_scatter(partial, self.npost, lane)
if idx < self.npost:
for peer in range(lane & (32 // self.npost - 1), 7, 32 // self.npost):
dst = at(post, rank, warp, idx)
push_float(dst, partial[0], bars.iterator + 1, peer)
words = pack8(prefix_value)
store_words(at(prefix, token, h), words)
for r in cutlass.range_constexpr(min(S, mailbox.shape[0])):
rearm(at(mailbox, r, token, h))
if cutlass.const_expr(xmoe is not None):
cute.arch.fence_acq_rel_gpu()
self.wait_exchange(bars.iterator + 1)
if tid < self.npost:
sums = [Float32(0)] * 4
for peer in cutlass.range_constexpr(7):
for w in cutlass.range_constexpr(4):
sums[w] = add_rn(sums[w], post[peer, w, tid])
total[tid] = tree_reduce(sums, add_rn)
named_barrier()
norm = self.fragment(pf, 2, tid)
final = cute.make_rmem_tensor(8, Float32)
if cutlass.const_expr(self.nb > 0):
lp = mul_rn(total[3], rstd(total[2], eps))
mx = max(maximum, lp)
alpha = cute.math.exp(maximum - mx, fastmath=True)
ep = cute.math.exp(lp - mx, fastmath=True)
den = fma(alpha, exponent_sum, ep)
a2, t1 = mul_rn(alpha, alpha), total[1]
ae2 = mul_rn(mul_rn(2.0, alpha), ep)
quad = fma(a2, t1, fma(ae2, total[0], mul_rn(mul_rn(ep, ep), total[2])))
scale = rstd(quad, mul_rn(mul_rn(out_eps, den), den))
ca, cp = mul_rn(alpha, scale), mul_rn(ep, scale)
else:
cp = rstd(total[0], out_eps)
for j in cutlass.range_constexpr(4):
value = pair_op((cp, cp), two(prefix_value, j), op="mul")
if cutlass.const_expr(self.nb > 0):
value = pair_op((ca, ca), two(mix, j), value, op="fma")
value = pair_op(value, two(norm, j), op="mul")
final[2 * j], final[2 * j + 1] = value[0], value[1]
self.publish(output, xmoe, hints, token, h, final)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, (7, args[1].shape[0], 1), 128 * self.groups, 7)
@cute.kernel
def kernel(self, args):
rank, token, _ = cute.arch.block_idx()
tid, _, _ = cute.arch.thread_idx()
state = self.allocate()
self.setup(state[5])
cute.arch.griddepcontrol_launch_dependents()
mix = zeros(8, Float32)
maximum, exponent_sum, qpart = Float32(0), Float32(0), Float32(0)
if tid < 128:
self.prefetch(args, state[7], rank, token)
maximum, exponent_sum, qpart = self.pre(state, rank, args[9], mix)
self.post(args, state, (mix, maximum, exponent_sum, qpart), rank, token)
cute.arch.griddepcontrol_wait()
class OprojFused:
def __init__(self, nb, *, has_delta=True):
self.producer = OprojProducer(768, staged_blocks=0)
# TP16 requires prefix/blocks to be ready at launch.
self.consumer = OprojConsumer(nb, has_delta=has_delta)
@cute.jit
def __call__(self, *args):
if cutlass.const_expr(args[0].shape[0] > 8):
raise ValueError("The selected fused schedule supports B1..8")
launch(self.kernel, args, (8, 14, 1), 512, 8, min_blocks_per_mp=1)
@cute.kernel
def kernel(self, args):
x, weight, rows = args[0], args[1], (args[2], *args[5:])
rank, cluster, _ = cute.arch.block_idx()
tid, _, _ = cute.arch.thread_idx()
cta = cluster * 8 + rank
active = (cluster < x.shape[0]) & (rank < 7)
state = self.consumer.allocate()
smem = self.producer.allocate(x.shape[0])
self.consumer.setup(state[5])
cute.arch.griddepcontrol_launch_dependents()
group0 = active & (tid < 128)
if group0:
self.consumer.prefetch(rows, state[7], rank, cluster)
weights = self.producer.load_weight(weight, smem[2], cta)
mix = zeros(8, Float32)
maximum, exponent_sum, qpart = Float32(0), Float32(0), Float32(0)
if group0:
maximum, exponent_sum, qpart = self.consumer.pre(state, rank, rows[9], mix)
self.producer.compute(args, smem, weights, cta, False)
if active:
block_mix = (mix, maximum, exponent_sum, qpart)
self.consumer.post(rows, state, block_mix, rank, cluster)
@@ -0,0 +1,162 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Output projection fused with its TP all-reduce and post-attention AttnRes: rank
partials go into Lamport slots of a multicast [TP, 64, 7168] mailbox (parity alternates
by layer) and the consumer sums and re-arms them. TP16 B<=8 fuses producer and consumer
(MLA layers below B5 keep them split); above B8 the producer runs 16-token groups.
At ``twoshot_batches``, given ``TwoShot`` buffers, the all-reduce is ``common.twoshot``
(chunks pushed to their column owner, owner sums + multicasts, all Lamport-polled), so
each GPU receives ~2 x 14 KB per token instead of TP x 14 KB."""
from functools import cache
from typing import Any
import cutlass
import cutlass.cute as cute
import torch
import torch.distributed._symmetric_memory as symm_mem
from cutlass.cute.runtime import make_fake_stream
from ..common.host import dynamic_fake, dynamic_view, fake, stream, to_cute
from ..common.policy import batch_table
from .consumer import OprojConsumer, OprojFused
from .producer import OprojProducer, OprojUmma
class OprojMailbox:
"""Sentinel-armed multicast mailbox; defaults to [parity, TP, 64, 7168] BF16."""
def __init__(self, group, device, tp, rank, shape=None):
self.tp, self.rank = tp, rank
shape = (2, tp, 64, 7168) if shape is None else shape
self.buffer = symm_mem.empty(shape, dtype=torch.bfloat16, device=device)
self.handle = handle = symm_mem.rendezvous(self.buffer, group)
self.mc = int(handle.multicast_ptr or 0)
if not self.mc: # slot() offsets this address
raise RuntimeError(
"Kimi K3 specialized o-projection needs multicast memory"
)
self.buffer.view(torch.int32).fill_(-0x80000000)
torch.accelerator.synchronize(device)
torch.distributed.barrier(group=group)
def slot(self, parity):
return self.buffer[parity], self.mc + parity * self.buffer[0].numel() * 2
# Two-shot all-reduce batches per TP (2-/4-node GB300, 32 MLA layers, us/layer, max over
# ranks, multicast vs two-shot: TP8 B16 8.85/11.40, B32 10.63/11.32, B64 18.06/13.18;
# TP16 B16 9.78/12.28, B32 14.96/13.05, B64 28.06/14.07).
TWOSHOT = {8: "33-64:1", 16: "17-64:1"}
def twoshot_batches(tp):
table = batch_table(TWOSHOT.get(tp, ""))
return {batch for batch, on in table.items() if on}
def engine(batch, tp):
"""Producer engine above B8 (GB300): grouped mma.sync for TP8 up to B16, else
tcgen05."""
default = "mma" if tp == 8 and batch <= 16 else "umma"
return default
def producer(k, batch, tp, mla, twoshot=False, kind=None):
"""B<=8: one mma.sync pass, TP8 stages 8 (MLA 4) of 12 K blocks per warp. Above
B8 (``kind`` overrides ``engine``): the tcgen05 engine, or 2-tile warps over 8 K
parts in 16-token groups, TP8 staging 2 of 6 K blocks per warp."""
if batch <= 8:
staged = 0 if tp == 16 else 4 if mla else 8
return OprojProducer(k, staged_blocks=staged, twoshot=twoshot)
if (kind or engine(batch, tp)) == "umma":
return OprojUmma(k, twoshot=twoshot)
grouped = dict(rows=2, group_tiles=2, twoshot=twoshot)
return OprojProducer(k, staged_blocks=0 if tp == 16 else 2, **grouped)
@cache
def compile_oproj(
kind, k, batch, tp, nb, has_delta, mla, front=False, cap=64, twoshot=False
):
"""Producer, consumer or fused kernel over a [TP, cap, 7168] parity view;
``front``: the consumer also publishes x_moe [1, 4, 7168] and the epoch hint.
``twoshot``: the two-shot pair (arguments as ``TwoShot.views``)."""
box = fake((1 if twoshot else tp, cap, 7168), (cap * 7168, 7168, 1))
x, w = fake((batch, k), (k, 1)), fake((7168, k), (k, 1))
mc, slot, eps = cutlass.Int64(0), cutlass.Int32(0), cutlass.Float32(1e-6)
recv, peers = fake((tp, cap, 7168 // tp)), fake((tp,), None, torch.int64)
if kind.startswith("producer"): # producer_mma / producer_umma: engine A/B
kernel = producer(k, batch, tp, mla, twoshot, kind[9:] or None)
tail = (recv, peers, slot) if twoshot else (box, mc, slot)
return cute.compile(kernel, x, w, *tail, make_fake_stream())
row, weight = dynamic_fake(2), fake((7168,), (1,))
xmoe = fake((1, 4, 7168), (4 * 7168, 7168, 1)) if front else None
hints = fake((224,), (1,), torch.int32) if front else None
rows = (row, dynamic_fake(3), weight, weight, weight, row, xmoe, hints, eps, eps)
if kind == "consumer": # B64: one 128-thread group per CTA polls every rank
options = dict(has_delta=has_delta)
groups = 4 if batch <= 32 and not twoshot else 1
kernel = OprojConsumer(nb, groups=groups, twoshot=twoshot, **options)
extra = (recv, mc, slot) if twoshot else ()
return cute.compile(kernel, box, *rows, *extra, make_fake_stream())
kernel = OprojFused(nb, has_delta=has_delta)
return cute.compile(kernel, x, w, box, mc, slot, *rows, make_fake_stream())
class PreparedOproj:
"""One decoder layer's o-projection + all-reduce + post-attention AttnRes."""
def __init__(self, decoder, mailbox, batches, mla, front=None, twoshot=None):
weight = decoder.self_attn.o_proj.weight
norm, out_norm = decoder.mlp_res_norm, decoder.post_attention_layernorm
weights = (norm.weight, decoder.mlp_res_proj.weight.squeeze(0), out_norm.weight)
self.views = tuple(to_cute(w.reshape(-1)) for w in weights)
self.eps = tuple(cutlass.Float32(n.variance_epsilon) for n in (norm, out_norm))
parity, rank = decoder.layer_idx % 2, cutlass.Int32(mailbox.rank)
local_box, mc = mailbox.slot(parity)
self.device, self.box = weight.device, to_cute(local_box)
self.head = (self.box, cutlass.Int64(mc), rank)
self.twoshot: Any = None
if twoshot is not None:
recv, peers, gather, gather_mc = twoshot.views(parity)
recv = to_cute(recv)
self.twoshot = (
(recv, to_cute(peers), rank),
to_cute(gather),
(recv, cutlass.Int64(gather_mc), rank),
)
self.wview, tp, k = to_cute(weight), mailbox.tp, weight.shape[1]
nb = decoder.prev_valid_blocks + decoder.is_block_write_layer
delta = not decoder.is_block_write_layer
self.front = None if front is None else (*map(to_cute, front[:2]), front[2])
self.codes, self.twoshots = {}, twoshot_batches(tp) if self.twoshot else set()
for b in batches:
publish = front is not None and b in front[2]
fused = tp == 16 and (not mla or b >= 5) and b <= 8
kinds = ("fused",) if fused else ("producer", "consumer")
twoshot = b in self.twoshots and not fused
shape = (k, b, tp, nb, delta, mla)
self.codes[b] = tuple(
compile_oproj(kind, *shape, publish and kind != "producer", 64, twoshot)
for kind in kinds
)
def run(self, local, prefix, blocks, out):
batch, s = local.shape[0], stream(self.device)
codes, x = self.codes[batch], to_cute(local)
front = (None, None)
if self.front is not None and batch in self.front[2]:
front = self.front[:2]
rows = (dynamic_view(prefix), dynamic_view(blocks), *self.views)
rows += (dynamic_view(out), *front, *self.eps, s)
if len(codes) == 1:
codes[0](x, self.wview, *self.head, *rows)
elif batch in self.twoshots:
head, gather, tail = self.twoshot
codes[0](x, self.wview, *head, s)
codes[1](gather, *rows[:-1], *tail, s)
else:
codes[0](x, self.wview, *self.head, s)
codes[1](self.box, *rows)
return out, prefix, blocks
@@ -0,0 +1,443 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""O-projection producer for the split and fused AttnRes paths: 112 CTAs of 64 output
rows. Each warp keeps ``rows`` 16-row weight tiles of one of ``4 * rows`` K parts in
registers (TP8 stages some K blocks in shared memory), runs BF16 MMA chains and the K
parts are reduced in order. Weights load before the PDL wait; the split producer
triggers its consumer right after it. With ``group_tiles`` the batch runs in groups of
that many 8-token tiles: each group's x arrives through a double-buffered cp.async ring
during the previous group's MMAs, and is reduced and published during the next one's.
With ``twoshot`` the partial chunks go to their owners' receive slots instead (the
first shot of ``common.twoshot``; args 2-4: receive view, owners' bases, rank).
``OprojUmma`` is the tcgen05 engine of
the same producer for B>8."""
import cutlass
import cutlass.cute as cute
from cutlass import Constexpr, Float32, Int64, Uint8, Uint32, Uint64
from ..common.primitives import (
add_rn,
alloc,
at,
bits_float,
copy_g2s,
fence_tmem,
launch,
load_tmem,
load_vector,
mma_bf16_block,
mma_into,
multicast_words,
named_barrier,
pack_pair,
prefetch_bytes,
release_tmem,
smem_desc,
smem_words,
stage_weight,
store_shared_words,
thread,
view,
wait,
)
from ..common.twoshot import push
def pack8(values, policy="plain"):
"""Four BF16x2 words of ``values[0:8]``."""
return tuple(
pack_pair(values[2 * j], values[2 * j + 1], policy=policy) for j in range(4)
)
def tree_reduce(values, op):
tree = list(values)
for stride in (1, 2, 4):
for block in range(0, len(tree) - stride, 2 * stride):
tree[block] = op(tree[block], tree[block + stride])
return tree[0]
class OprojProducer:
def __init__(self, k, *, staged_blocks, rows=1, group_tiles=0, twoshot=False):
self.k, self.staged, self.rows = k, staged_blocks, rows
self.twoshot = twoshot
self.parts = 4 * rows
self.kblocks = k // (32 * self.parts)
self.register_blocks = self.kblocks - self.staged
self.group_tiles = group_tiles
# Single pass: TP8 copies the batch to shared memory, TP16 loads registers.
self.shared_x = self.staged > 0 and not group_tiles
self.register_x = k == 768 and not group_tiles
self.x_stride = k // 8 + 4
self.chains = max(1, 4 // (rows * max(1, group_tiles)))
def schedule(self, batches):
"""(8-token tiles, tiles per group, groups) of ``batches`` tokens."""
tiles = (batches + 7) // 8
per_group = self.group_tiles or tiles
return tiles, per_group, (tiles + per_group - 1) // per_group
@cute.jit
def allocate(self, batches: cutlass.Constexpr[int]):
nt, gt, ng = self.schedule(batches)
smem = cutlass.utils.SmemAllocator()
red = alloc(smem, Float32, (gt, 4, self.parts, 8, 20))
rows = nt * 8 if self.shared_x else 0
if cutlass.const_expr(self.group_tiles):
rows = min(2, ng) * gt * 8
xs = alloc(smem, Uint32, rows * self.x_stride * 4 if rows else 1)
staged = alloc(smem, Uint32, max(1, 16 * self.staged * self.rows * 2 * 32 * 4))
return red, xs, staged
@cute.jit
def load_weight(self, weight, staged, cta):
tid, _, _ = cute.arch.thread_idx()
lane, warp = tid % 32, tid // 32
group, part = lane // 4, lane % 4
tile = cta * 4 + warp // self.parts * self.rows
kq = warp % self.parts
base = (tile * 16 + group) * self.k + kq * self.kblocks * 32 + part * 8
if cutlass.const_expr(self.staged > 0):
for j in cutlass.range_constexpr(self.staged):
for t in cutlass.range_constexpr(self.rows):
unit = (warp * self.staged + j) * self.rows + t
dst = staged.iterator + (unit * 2 * 32 + lane) * 4
src = weight.iterator + base + t * 16 * self.k
src += (self.register_blocks + j) * 32
stage_weight(dst, src)
stage_weight(dst + 128, src + 8 * self.k)
cute.arch.cp_async_commit_group()
wr = cute.make_rmem_tensor((self.register_blocks, self.rows, 2, 4), Uint32)
for j in cutlass.range_constexpr(self.register_blocks):
for t in cutlass.range_constexpr(self.rows):
for r in cutlass.range_constexpr(2):
src = weight.iterator + base + (t * 2 + r) * 8 * self.k + j * 32
words = load_vector(src, cache="weight")
for v in cutlass.range_constexpr(4):
wr[j, t, r, v] = words[v]
return wr
@cute.jit
def stage_x(self, x, xs, first: Constexpr[int], buffer: Constexpr[int]):
"""One cp.async group: x tokens [first, first + group) into ring ``buffer``."""
tid, _, _ = cute.arch.thread_idx()
chunks: Constexpr = self.k // 8
count: Constexpr = min(x.shape[0] - first, self.group_tiles * 8)
rows: Constexpr = buffer * self.group_tiles * 8
for i in cutlass.range_constexpr((count * chunks + 511) // 512):
chunk = tid + i * 512
token, col = chunk // chunks, chunk % chunks
dst = xs.iterator + ((rows + token) * self.x_stride + col) * 4
if cutlass.const_expr((i + 1) * 512 <= count * chunks):
copy_g2s(dst, at(x, first + token, col * 8))
else:
if chunk < count * chunks: # noqa: SIM102
copy_g2s(dst, at(x, first + token, col * 8))
cute.arch.cp_async_commit_group()
def x_words(self, xs, row, kofs, j):
return smem_words(xs.iterator + (row * self.x_stride + kofs // 8 + j * 4) * 4)
@cute.jit
def multiply(self, smem, wr, xr, first: Constexpr, buffer: Constexpr, M: Constexpr):
"""FP32 chains of tokens [first, first + group); x from ``xr`` (registers) or
shared-memory rows (ring ``buffer``)."""
red, xs, staged = smem
_, gt, _ = self.schedule(M)
tiles: Constexpr = min(gt, (M - first + 7) // 8)
tid, _, _ = cute.arch.thread_idx()
lane, warp = tid % 32, tid // 32
group, part = lane // 4, lane % 4
kofs = warp % self.parts * self.kblocks * 32 + part * 8
acc = cute.make_rmem_tensor((gt, self.rows, self.chains, 4), Float32)
acc.fill(0)
for j in cutlass.range_constexpr(self.kblocks):
wa, wb = [], []
for t in cutlass.range_constexpr(self.rows):
if cutlass.const_expr(j < self.register_blocks):
wa.append(tuple(wr[j, t, 0, v] for v in range(4)))
wb.append(tuple(wr[j, t, 1, v] for v in range(4)))
else:
sj: Constexpr = j - self.register_blocks
unit = (warp * self.staged + sj) * self.rows + t
ptr = staged.iterator + (unit * 2 * 32 + lane) * 4
wa.append(smem_words(ptr))
wb.append(smem_words(ptr + 128))
for nt in cutlass.range_constexpr(tiles):
if cutlass.const_expr(self.register_x):
xv = tuple(xr[nt, j, v] for v in range(4))
else:
row = buffer * gt * 8 + nt * 8
if cutlass.const_expr(self.group_tiles):
xv = self.x_words(xs, row + group, kofs, j)
else:
xv = (Uint32(0), Uint32(0), Uint32(0), Uint32(0))
if nt * 8 + group < M:
xv = self.x_words(xs, row + group, kofs, j)
for t in cutlass.range_constexpr(self.rows):
for half in cutlass.range_constexpr(2):
chain: Constexpr = (j * 2 + half) % self.chains
a: tuple = (wa[t][2 * half], wb[t][2 * half])
a += (wa[t][2 * half + 1], wb[t][2 * half + 1])
b = (xv[2 * half], xv[2 * half + 1])
mma_into(acc[nt, t, chain, None], a, b, bf16=True)
return acc
@cute.jit
def store_partials(self, red, acc, tiles: Constexpr):
tid, _, _ = cute.arch.thread_idx()
lane, warp = tid % 32, tid // 32
group, part = lane // 4, lane % 4
tile, kq = warp // self.parts * self.rows, warp % self.parts
for nt in cutlass.range_constexpr(tiles):
for t in cutlass.range_constexpr(self.rows):
for v in cutlass.range_constexpr(4):
chains = [acc[nt, t, c, v] for c in range(self.chains)]
token, row = 2 * part + v % 2, group + (v // 2) * 8
red[nt, tile + t, kq, token, row] = tree_reduce(chains, add_rn)
@cute.jit
def publish(self, args, red, cta, first: Constexpr[int]):
"""Sum the K parts of tokens [first, first + group) in order; multicast them."""
x, _, mailbox, mc, slot = args[:5]
_, gt, _ = self.schedule(x.shape[0])
count: Constexpr = min(x.shape[0] - first, gt * 8)
tid, _, _ = cute.arch.thread_idx()
if tid < 8 * count:
reduced = cute.make_rmem_tensor(8, Float32)
if cutlass.const_expr(self.group_tiles):
# Token-major lanes: conflict-free 16-byte partial loads.
token, out_half = tid % count, tid // count % 2
row_tile = tid // count // 2
tile, row = token // 8, token % 8
for i in cutlass.range_constexpr(2):
column = out_half * 8 + i * 4
words = smem_words(at(red, tile, row_tile, 0, row, column))
sums = [bits_float(w) for w in words]
for q in cutlass.range_constexpr(1, self.parts):
words = smem_words(at(red, tile, row_tile, q, row, column))
sums = [add_rn(s, bits_float(w)) for s, w in zip(sums, words)]
for v in cutlass.range_constexpr(4):
reduced[i * 4 + v] = sums[v]
else:
out_half, row_tile, token = tid % 2, (tid // 2) % 4, tid // 8
tile, row = token // 8, token % 8
for v in cutlass.range_constexpr(8):
value = red[tile, row_tile, 0, row, out_half * 8 + v]
for q in cutlass.range_constexpr(1, self.parts):
part = red[tile, row_tile, q, row, out_half * 8 + v]
value = add_rn(value, part)
reduced[v] = value
words = pack8(reduced, "latent")
out_col = (cta * 4 + row_tile) * 16 + out_half * 8
if cutlass.const_expr(self.twoshot):
push(mailbox, mc, slot, first + token, out_col, words)
else:
offset = cute.crd2idx((slot, first + token, out_col), mailbox.layout)
multicast_words(mc + Int64(offset) * 2, words)
@cute.jit
def compute(self, args, smem, wr, cta, trigger_after_wait: cutlass.Constexpr[bool]):
x = args[0]
red, xs, staged = smem
M: Constexpr = x.shape[0]
NT, GT, NG = self.schedule(M)
tid, _, _ = cute.arch.thread_idx()
lane, warp = tid % 32, tid // 32
group, part = lane // 4, lane % 4
kofs = warp % self.parts * self.kblocks * 32 + part * 8
cute.arch.griddepcontrol_wait()
if cutlass.const_expr(trigger_after_wait):
cute.arch.griddepcontrol_launch_dependents()
xr = cute.make_rmem_tensor((NT, self.kblocks, 4), Uint32)
if cutlass.const_expr(self.group_tiles):
self.stage_x(x, xs, 0, 0)
if cutlass.const_expr(self.shared_x):
for chunk in range(tid, M * (self.k // 8), 512):
token, col = chunk // (self.k // 8), chunk % (self.k // 8)
words = load_vector(at(x, token, col * 8))
store_shared_words(
xs.iterator + (token * self.x_stride + col) * 4, words
)
cute.arch.sync_threads()
if cutlass.const_expr(self.register_x):
xr.fill(0)
for nt in cutlass.range_constexpr(NT):
token = nt * 8 + group
if token < M:
for j in cutlass.range_constexpr(self.kblocks):
words = load_vector(at(x, token, kofs + j * 32))
for v in cutlass.range_constexpr(4):
xr[nt, j, v] = words[v]
if cutlass.const_expr(self.staged > 0 and not self.group_tiles):
cute.arch.cp_async_wait_group(0)
for g in cutlass.range_constexpr(NG):
if cutlass.const_expr(self.group_tiles):
# x(g) and group g - 1's partials are visible: stage x(g + 1), publish.
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
if cutlass.const_expr(g + 1 < NG):
self.stage_x(x, xs, (g + 1) * GT * 8, (g + 1) % 2)
if cutlass.const_expr(g > 0):
self.publish(args, red, cta, (g - 1) * GT * 8)
acc = self.multiply(smem, wr, xr, g * GT * 8, g % 2, M)
if cutlass.const_expr(self.group_tiles):
cute.arch.sync_threads()
self.store_partials(red, acc, min(GT, NT - g * GT))
cute.arch.sync_threads()
self.publish(args, red, cta, (NG - 1) * GT * 8)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, 112, 512, min_blocks_per_mp=1)
@cute.kernel
def kernel(self, args):
cta, _, _ = cute.arch.block_idx()
smem = self.allocate(args[0].shape[0])
wr = self.load_weight(args[1], smem[2], cta)
self.compute(args, smem, wr, cta, True)
class OprojUmma(OprojProducer):
"""B>8 producer on tcgen05: 112 CTAs x 64 rows, tokens on N. Four loader warps
stream 128B-swizzled W [64, 64] and x [N, 64] K blocks through a cp.async ring
(W of the first stages before the PDL wait, the rest prefetched to L2), the MMA
warp accumulates D [64, N] FP32 in TMEM with M=64 kind::f16 MMAs, and the loader
warps round D to BF16 and publish it like ``OprojProducer``."""
def __init__(self, k, *, twoshot=False, budget=184 * 1024):
super().__init__(k, staged_blocks=0, twoshot=twoshot)
self.blocks, self.budget = k // 64, budget
def geometry(self, batches):
"""(N, ring stage bytes, stages, TMEM columns)."""
n = (batches + 7) // 8 * 8
stage = 8192 + n * 128
stages = min(self.blocks, self.budget // stage)
return n, stage, stages, max(32, 1 << (n - 1).bit_length())
@cute.jit
def load(self, source, ring, rows: Constexpr, valid: Constexpr, row0, column):
"""cp.async ``rows`` x 64 BF16 of ``source`` from (row0, column) into a K-major
128B-swizzled tile; rows >= ``valid`` are left alone. ``valid`` < 0: weights
(evict-first, every row)."""
tid, _, _ = cute.arch.thread_idx()
for i in cutlass.range_constexpr((rows * 8 + 127) // 128):
c = tid + 128 * i
row, chunk = c // 8, c % 8
dst = ring + row // 8 * 1024 + row % 8 * 128 + (chunk ^ row % 8) * 16
src = at(source, row0 + row, column + chunk * 8)
if cutlass.const_expr(valid < 0):
stage_weight(dst, src)
elif cutlass.const_expr((i + 1) * 128 <= valid * 8):
copy_g2s(dst, src)
else:
if c < valid * 8: # noqa: SIM102
copy_g2s(dst, src)
@cute.jit
def multiply(self, ring, bars, tmem, n: Constexpr, stage: Constexpr):
"""The MMA warp: wait for each K block, issue its four K16 MMAs from the
elected lane and release its stage (the last block: the accumulator)."""
stages: Constexpr = self.geometry(n)[2]
# kind::f16 descriptor: FP32 D, BF16 A and B, both K-major, N, M = 64
instruction: Constexpr = 1 << 4 | 1 << 7 | 1 << 10 | n >> 3 << 17 | 4 << 24
for kb in cutlass.range_constexpr(self.blocks):
s: Constexpr = kb % stages
wait(bars.iterator + s, kb // stages % 2)
cute.arch.fence_proxy("async.shared", space="cta")
fence_tmem()
a = smem_desc(ring.iterator + s * stage, True)
b = smem_desc(ring.iterator + s * stage + 8192, True)
last: Constexpr = kb + 1 == self.blocks
release: Constexpr = last or kb + stages < self.blocks
barrier = bars.iterator + (2 * stages if last else stages + s)
mma_bf16_block(tmem, a, b, instruction, int(kb > 0), barrier, int(release))
@cute.jit
def epilogue(self, args, ring, tmem, cta, n: Constexpr):
"""D rows 16 * warp + lane (lane < 16: M=64 TMEM layout) -> FP32 [token, row]
tile in the drained ring -> 8-row BF16 chunks to the mailbox / partials."""
x, _, mailbox, mc, slot = args[:5]
M: Constexpr = x.shape[0]
tid, warp, lane, _ = thread()
values = load_tmem(tmem + (32 * warp << 16), self.geometry(M)[3])
tile = view(ring.iterator, 0, Float32, (n, 68), (68, 1)) # [token, row]
if lane < 16:
for t in cutlass.range_constexpr(M):
tile[t, 16 * warp + lane] = values[t]
named_barrier(1, 128)
for i in cutlass.range_constexpr((M * 8 + 127) // 128):
c = tid + 128 * i
if c < M * 8:
token, part = c // 8, c % 8
reduced = cute.make_rmem_tensor(8, Float32)
for h in cutlass.range_constexpr(2):
words = smem_words(at(tile, token, part * 8 + h * 4))
for v in cutlass.range_constexpr(4):
reduced[h * 4 + v] = bits_float(words[v])
words = pack8(reduced, "latent")
column = cta * 64 + part * 8
if cutlass.const_expr(self.twoshot):
push(mailbox, mc, slot, token, column, words)
else:
offset = cute.crd2idx((slot, token, column), mailbox.layout)
multicast_words(mc + Int64(offset) * 2, words)
@cute.jit
def __call__(self, *args):
launch(self.kernel, args, 112, 160, min_blocks_per_mp=1)
@cute.kernel
def kernel(self, args):
x, weight = args[0], args[1]
n, stage, stages, columns = self.geometry(x.shape[0])
tid, warp, _, cta = thread()
smem = cutlass.utils.SmemAllocator()
ring = alloc(smem, Uint8, stages * stage, 1024)
bars = alloc(smem, Uint64, 2 * stages + 1, 8)
tmem_slot = alloc(smem, Uint32, 1)
if tid == 0:
for s in cutlass.range_constexpr(stages):
cute.arch.mbarrier_init(bars.iterator + s, 256)
cute.arch.mbarrier_init(bars.iterator + stages + s, 1)
cute.arch.mbarrier_init(bars.iterator + 2 * stages, 1)
cute.arch.mbarrier_init_fence()
if warp == 4:
cute.arch.alloc_tmem(columns, tmem_slot.iterator)
cute.arch.relinquish_tmem_alloc_permit()
fence_tmem(before=True)
cute.arch.sync_threads()
fence_tmem()
tmem = tmem_slot[0]
row0 = cta * 64
if warp < 4: # weights do not depend on the previous kernel
if cutlass.const_expr(stages < self.blocks) and tid < 64:
rest = at(weight, row0 + tid, stages * 64)
prefetch_bytes(rest, (self.blocks - stages) * 128)
for s in cutlass.range_constexpr(stages):
self.load(weight, ring.iterator + s * stage, 64, -1, row0, s * 64)
cute.arch.cp_async_mbarrier_arrive_noinc(bars.iterator + s)
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
if warp < 4:
for kb in cutlass.range_constexpr(self.blocks):
slot: Constexpr = kb % stages
tile = ring.iterator + slot * stage
if cutlass.const_expr(kb >= stages):
wait(bars.iterator + stages + slot, (kb // stages - 1) % 2)
self.load(weight, tile, 64, -1, row0, kb * 64)
cute.arch.cp_async_mbarrier_arrive_noinc(bars.iterator + slot)
self.load(x, tile + 8192, n, x.shape[0], 0, kb * 64)
cute.arch.cp_async_mbarrier_arrive_noinc(bars.iterator + slot)
wait(bars.iterator + 2 * stages, 0)
fence_tmem()
# The MMAs (async proxy) read the ring the epilogue now overwrites.
cute.arch.fence_proxy("async.shared", space="cta")
self.epilogue(args, ring, tmem, cta, n)
else:
self.multiply(ring, bars, tmem, n, stage)
release_tmem(tmem, columns, warp, 4)
@@ -0,0 +1,135 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Kimi K3 specialized decode: a CuTe decode executor over the stock backbone, enabled
by ``additional_config={"decode_backend": "latency_specialized"}``."""
from dataclasses import dataclass
@dataclass(frozen=True)
class DecodeConfig:
max_context: int
mla_core: str
dcp: int
# Tokens per request in a verify step (1 + draft tokens; 1 without speculation).
q: int
capture_sizes: tuple[int, ...]
def specialized_decode_config(vllm_config) -> DecodeConfig | None:
"""The deployment's specialized decode settings, None for the stock decode.
Raises when the specialized decode is requested for an unsupported deployment."""
extra = vllm_config.additional_config
backend = extra.get("decode_backend", "auto") if isinstance(extra, dict) else "auto"
if backend == "auto":
return None
if backend != "latency_specialized":
raise ValueError("Supported decode_backend values are auto/latency_specialized")
reason = unsupported(vllm_config)
if reason is not None:
raise ValueError(f"Kimi K3 specialized decode does not support {reason}")
spec = vllm_config.speculative_config
return DecodeConfig(
max_context=vllm_config.model_config.max_model_len,
mla_core=extra.get("mla_core", "auto"),
dcp=vllm_config.parallel_config.decode_context_parallel_size,
q=1 + spec.num_speculative_tokens if spec is not None else 1,
capture_sizes=tuple(
vllm_config.compilation_config.cudagraph_capture_sizes or ()
),
)
def kda_dtypes(vllm_config):
"""(conv, recurrent) KDA state dtypes the deployment's cache config selects."""
from vllm.model_executor.layers.mamba.mamba_utils import MambaStateDtypeCalculator
cache = vllm_config.cache_config
return MambaStateDtypeCalculator.kda_state_dtype(
vllm_config.model_config.dtype,
cache.mamba_cache_dtype,
cache.mamba_ssm_cache_dtype,
)
def unsupported(vllm_config) -> str | None:
import torch
from vllm import envs
from vllm.config import CompilationMode, CUDAGraphMode
from vllm.platforms import current_platform
parallel, spec = vllm_config.parallel_config, vllm_config.speculative_config
text = vllm_config.model_config.hf_text_config
moe = tuple(
getattr(text, name, None)
for name in (
"hidden_act",
"activation_situ_beta",
"activation_situ_linear_beta",
"num_experts",
"num_experts_per_token",
"num_expert_group",
"topk_group",
"moe_router_activation_func",
)
)
compilation = vllm_config.compilation_config
graphs = compilation.cudagraph_mode
checks = (
(not current_platform.is_device_capability_family(100), "this GPU (SM100/103)"),
(
moe != ("situ", 4.0, 25.0, 896, 16, 1, 1, "sigmoid"),
"MoE configs but the one-group sigmoid top-16 SiTU MoE of Kimi K3",
),
(parallel.tensor_parallel_size not in (8, 16), "TP other than 8/16"),
(parallel.pipeline_parallel_size != 1, "pipeline parallelism"),
(parallel.enable_expert_parallel, "expert parallelism"),
(parallel.use_ubatching, "DBO"),
(
parallel.decode_context_parallel_size
not in (1, parallel.tensor_parallel_size),
"decode context parallel other than 1 or TP",
),
(vllm_config.model_config.enable_sleep_mode, "sleep mode"),
(vllm_config.lora_config is not None, "LoRA"),
# The runner's ModelState hooks build the executor and prepare the KV layouts.
(not vllm_config.use_v2_model_runner, "the V1 model runner"),
# The decode loop runs in forward; a compiled forward would trace it.
(compilation.mode == CompilationMode.STOCK_TORCH_COMPILE, "torch.compile"),
# The loop runs only in FULL decode graphs; eager and PIECEWISE steps run stock.
(
graphs is None or graphs.decode_mode() != CUDAGraphMode.FULL,
"deployments without FULL decode CUDA graphs (e.g. enforce-eager)",
),
(
spec is not None and spec.method != "dspark",
"speculative methods but DSpark",
),
(
spec is not None and spec.enable_adaptive_verification,
"adaptive verification",
),
# KdaSplit keeps a BF16 conv state and an FP32 recurrent state in place; it
# maintains neither other cache dtypes nor ReplaySSM / RecoverSSM records.
(
kda_dtypes(vllm_config) != (torch.bfloat16, torch.float32),
"KDA cache dtypes other than a BF16 conv state and an FP32 recurrent "
"state (mamba cache dtype)",
),
(
vllm_config.cache_config.use_replayssm
or vllm_config.cache_config.use_kda_recoverssm,
"ReplaySSM / RecoverSSM (--use-replayssm)",
),
# The verify kernels take at most 8 tokens per request (KdaSplit's barriers).
(
spec is not None and spec.num_speculative_tokens > 7,
"more than 7 speculative tokens",
),
(
spec is not None and envs.VLLM_KIMI_K3_AUX_ATTN_RES_STREAM,
"the AttnRes aux stream",
),
)
return next((reason for failed, reason in checks if failed), None)
@@ -0,0 +1,280 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""The specialized decode executor over the stock Kimi K3 backbone: one routing
decision per step, then every layer's prepared providers in one loop."""
from functools import partial
from typing import Any, NamedTuple
import torch
from vllm.config import CUDAGraphMode
from vllm.distributed import get_tp_group
from vllm.distributed import tensor_model_parallel_all_reduce as all_reduce
from vllm.forward_context import get_forward_context, is_forward_context_available
from vllm.platforms import current_platform
from vllm.utils.math_utils import round_up
from ..cutedsl.attn_res.prepared import PreparedAttnRes
from ..cutedsl.common.embedding import PreparedEmbedding
from ..cutedsl.common.twoshot import TwoShot
from ..cutedsl.moe.dense import PreparedDenseMLP
from ..cutedsl.moe.experts_runtime import PersistentExperts
from ..cutedsl.moe.front import SharedFront
from ..cutedsl.moe.prepared import MoEScratch, PreparedMoE
from ..cutedsl.moe.small_batch import chain as small_batch_chain
from ..cutedsl.moe.tail_runtime import MoeTailBox, PreparedTail
from ..cutedsl.oproj.prepared import OprojMailbox
from .prefetch import PrefetchController
from .providers import BATCHES, PREPARED_MAX, Attention, pack_front
class Step(NamedTuple):
q: int # tokens per request
metadata: list # per layer
slot_mapping: Any
def layer_name(module):
return getattr(module, "prefix", getattr(module, "layer_name", None))
def decode_metadata(attention, context, q, b):
"""One layer's metadata if this step is a pure decode (q = 1) or a uniform
verify step of b tokens, else None."""
md = context.attn_metadata
md = md.get(layer_name(attention)) if isinstance(md, dict) else None
if md is None:
return None
if q > 1:
return md if is_uniform_verify(md, q, b) else None
if md.num_prefills or getattr(md, "num_spec_decodes", 0):
return None
return md
def is_uniform_verify(md, q, b):
"""Whether every request of the b // q graph rows verifies q tokens (FULL graphs
pad whole requests: KDA stages NULL state rows, MLA zero-length decodes)."""
if md.num_prefills:
return False
if not hasattr(md, "num_spec_decodes"):
return md.num_decode_tokens == md.num_decodes * q == b
rows, tokens = md.spec_state_indices_tensor, md.num_spec_decode_tokens
full = rows is not None and rows.shape[0] * q == b
return full and not md.num_decodes and tokens == md.num_spec_decodes * q
def caches_of(attention):
cache = getattr(attention, "kv_cache", ())
return [cache] if isinstance(cache, torch.Tensor) else list(cache)
class SpecializedDecode:
"""Built once the weights are loaded; owns every prepared provider, the
mailboxes and the decode buffers. Reads the backbone, never modifies it."""
def __init__(self, backbone, config):
self.backbone, self.buffers = backbone, {}
q = config.q
# Verify steps decode q tokens per request at M = requests * q tokens; FULL
# verify graphs round each capture size up to whole requests.
rounded = {round_up(s, q) for s in config.capture_sizes}
verify = {m for m in rounded if m <= PREPARED_MAX}
self.batches, self.q = tuple(sorted({*BATCHES, *verify})), q
self.queries = (1, q) if q > 1 else (1,)
# The longest sequence the deployment serves bounds the MLA split count.
self.max_context, self.mla_core = config.max_context, config.mla_core
# Decode context parallel over the TP ranks: MLA KV is sequence-sharded.
self.dcp, self.dcp_box, self.mla_layers = config.dcp, None, 0
group = get_tp_group()
self.tp, self.rank = group.world_size, group.rank_in_group
self.group = group.device_group
self.device = torch.device(current_platform.current_device())
layers = backbone.layers[backbone.start_layer : backbone.end_layer]
self.layers = layers = tuple(layers)
self.build_providers()
self.embedding = PreparedEmbedding(backbone.embed_tokens, self.tp, self.rank)
self.before = {
d.layer_idx: PreparedAttnRes.for_target(d, has_delta=d is not layers[0])
for d in layers
}
# The final AttnRes also applies the backbone's final norm.
self.final = PreparedAttnRes.for_target(backbone, final=True)
# The next (or final) AttnRes consumes each MoE output from its MoE tail output
# row.
self.fused_before = {}
followers = [self.before[f.layer_idx] for f in layers[1:]] + [self.final]
for layer, consumer in zip(layers, followers):
moe = self.feed_forward[layer.layer_idx]
if isinstance(moe, PreparedMoE):
consumer.bind_ring(moe.tail.tail_box.out.buffer)
self.fused_before[layer.layer_idx] = consumer
# The AttnRes entering each aux layer (or the final one) also stores the
# updated prefix: the stock aux stream (prefix sum plus the last MLP output).
self.aux_layers = tuple(backbone.aux_hidden_state_layers)
end = layers[-1].layer_idx + 1
for index in self.aux_layers:
(self.final if index == end else self.before[index]).bind_aux()
empty = partial(torch.empty, device=self.device, dtype=torch.bfloat16)
self.hidden = (empty((64, 7168)), empty((64, 7168)))
self.prefix = empty((64, 7168))
self.blocks = empty((64, backbone.num_attn_res_blocks, 7168))
def build_providers(self):
"""Every layer's attention and feed-forward providers, the model's mailboxes
and the prefetch side stream. All collective allocations, weight packing and
kernel compiles finish here, before KV binding and CUDA graph capture."""
config, layers = self.backbone.config, self.layers
beta = config.activation_situ_beta
linear_beta = config.activation_situ_linear_beta
tp, rank, device, batches = self.tp, self.rank, self.device, self.batches
owner = self.group, device, tp, rank
packed = {
d.layer_idx: pack_front(d.mlp, rank, tp) for d in layers if d.is_moe_layer
}
prefetch = self.prefetch = PrefetchController(self, layers, packed)
state = MoEScratch(tp, device, batches)
mailbox, tail_box, moe_layers = (
OprojMailbox(*owner),
MoeTailBox(*owner),
list(packed),
)
twoshot = TwoShot(*owner)
front_box = OprojMailbox(*owner, (1, 64, 3584 + 2 * 896))
small_batch = small_batch_chain.SmallBatchBox(*owner)
self.attention: dict[int, Attention] = {}
self.feed_forward: dict[int, Any] = {}
for layer in layers:
index, module = layer.layer_idx, layer.mlp
boxes = (mailbox, small_batch, twoshot)
self.attention[index] = Attention(layer, self, prefetch, *boxes)
if not layer.is_moe_layer:
reduce = all_reduce if module.down_proj.reduce_results else None
self.feed_forward[index] = PreparedDenseMLP(module, batches, reduce)
continue
args = (module, packed[index], front_box, state.front, batches, beta)
shared = dict(plan=state.plan, counters=state.shared, q=self.q)
front = SharedFront(*args, linear_beta, **shared)
routed = module.experts.routed_experts
w13, w2 = (
w.view(torch.uint8) for w in (routed.w13_weight, routed.w2_weight)
)
weights = w13, routed.w13_weight_scale, w2, routed.w2_weight_scale
experts = PersistentExperts(*weights, beta=beta, linear_beta=linear_beta)
norm = module.routed_expert_norm.weight
up = module.routed_expert_up_proj.weight
op = module.experts._k3_latent_moe_tail_op
placement = tail_box, moe_layers.index(index), len(moe_layers)
tail = PreparedTail(op, norm, up, batches, placement)
moe = PreparedMoE(
front, experts, tail, state, prefetch=prefetch, layer_idx=index
)
moe.small_batch = small_batch_chain.SmallBatchChain(
small_batch, module, experts, tail, index, beta, linear_beta, prefetch
)
self.feed_forward[index] = moe
def prepare_kv(self):
"""Compile each layer's cores for its bound KV cache layout (the profiling
cache, then the real one)."""
for layer in self.layers:
caches = caches_of(layer.self_attn)
self.attention[layer.layer_idx].prepare_layout(caches)
def embed_input_ids(self, ids):
rows = ids.shape[0] if ids.ndim == 1 and ids.is_contiguous() else 0
if not 1 <= rows <= PREPARED_MAX:
return self.backbone.embed_input_ids(ids)
table = self.backbone.embed_tokens.weight
return self.embedding.run(ids, table.new_empty((ids.shape[0], table.shape[1])))
def plan(self, positions, intermediate_tensors):
"""This step's decode plan, or None to run the stock forward instead.
Steps that run the stock forward:
- pipeline-parallel stage inputs (``intermediate_tensors``);
- token counts without prepared providers (above 64 or not in ``batches``);
- steps outside a CUDA graph capture (profiling, warmup and the warmup run
before each capture), so every stock kernel runs before its capture;
- PIECEWISE steps, keyed by token count alone and so captured as stock;
- steps with prompt rows (prefills, chunked prefills, fresh prompts);
- speculative steps whose requests do not all verify q tokens (partial
drafts, verify rows mixed with plain decode rows).
Every predicate is the same on all TP ranks: the mailboxes of the specialized
decode need every rank to take the same path.
"""
b = positions.shape[0]
if intermediate_tensors is not None or b not in self.batches:
return None
if not torch.cuda.is_current_stream_capturing():
return None
if not is_forward_context_available():
return None
context = get_forward_context()
if context.cudagraph_runtime_mode == CUDAGraphMode.PIECEWISE:
return None
by_layer = context.attn_metadata
rows: list[Any] = list(by_layer.values()) if isinstance(by_layer, dict) else []
q = self.q if any(getattr(md, "num_spec_decodes", 0) for md in rows) else 1
if b % q:
return None
metadata = [decode_metadata(d.self_attn, context, q, b) for d in self.layers]
if any(md is None for md in metadata):
return None
return Step(q, metadata, context.slot_mapping)
def run(self, step, input_ids, inputs_embeds):
"""Normalized hidden states (and the aux streams) of a planned step."""
x = inputs_embeds
if x is None:
x = self.embed_input_ids(input_ids)
out = torch.empty_like(x)
# The stock aux streams: the AttnRes prefix entering each aux layer.
aux_out = [torch.empty_like(x) for _ in self.aux_layers]
aux = dict(zip(self.aux_layers, aux_out))
b, q, slots = x.shape[0], step.q, step.slot_mapping
prefix, blocks = self.prefix[:b], self.blocks[:b]
first, second = self.hidden[0][:b], self.hidden[1][:b]
hidden, pre_ready = x, False
for layer, md in zip(self.layers, step.metadata):
index, caches = layer.layer_idx, caches_of(layer.self_attn)
if not pre_ready:
self.before[index].run(hidden, prefix, blocks, first, aux.get(index))
self.attention[index].run(
first, second, prefix, blocks, caches, md, slots, q
)
consumer = self.fused_before.get(index)
pre_ready = consumer is not None
if consumer is not None:
target = out if consumer is self.final else first
stream = aux.get(index + 1)
consume = partial(consumer.run_ring, prefix, blocks, aux=stream)
hidden = self.feed_forward[index].run(second, target, consume=consume)
else:
hidden = self.feed_forward[index].run(second, first)
first, second = second, first
if not pre_ready:
self.final.run(hidden, prefix, blocks, out, aux.get(index + 1))
self.prefetch.join()
return (out, aux_out) if aux_out else out
def normalize(self, output):
"""A stock step's output with the backbone's final norm applied, as the
specialized decode's last AttnRes applies it; the aux streams stay pre-norm.
Runs in chunks of at most 255 rows: the stock RMSNorm kernel reduces with
1024-thread blocks below 256 rows and 256-thread blocks above, so chunking keeps
the reduction order ``compute_logits`` uses for the few sampled rows, and
prefill logits match stock.
"""
hidden, aux = output if isinstance(output, tuple) else (output, None)
norm = self.backbone.norm
if hidden.ndim == 2 and hidden.shape[0] >= 256:
chunks = range(0, hidden.shape[0], 255)
hidden = torch.cat([norm(hidden[i : i + 255], None) for i in chunks])
else:
hidden = norm(hidden, None)
return hidden if aux is None else (hidden, aux)
@@ -0,0 +1,180 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""L2 weight prefetch for the specialized decode.
Small-batch decode is bound by HBM bandwidth, and each kernel that starts on weights
not yet in L2 stalls on DRAM. Some kernels leave bandwidth idle, such as the attention
core and the all-reduces. At fixed points of each layer, this module launches a small
kernel (``cutedsl/common/prefetch.py``) on a side stream. It pulls the byte ranges of
the weights the next kernels read into L2, so that they start warm.
Prefetch points (``fork(point, layer, batch)``), each with its batches per TP:
- ``oproj_down``: after the o-projection (B3-8), the MoE front's packed
router + latent-down rows.
- ``attention_moe``: at TP8 only, after the KDA input projection (B5-8) or the MLA
core (B8), the same front rows plus the shared expert's weights.
- ``next_attention``: once the routed experts are launched, the next layer's
attention weights, capped in MiB per batch (``NEXT_ATTENTION``). Not above B8,
where the HBM-bound experts lose more bandwidth than the attention gains.
The front and shared-expert ranges are on the critical path. They launch with the
highest stream priority and the maximum shared-memory carveout, which needs a driver
launch because CuTe drops both attributes.
Each fork records an event on the main stream that the side stream waits on, so a
prefetch starts only after the work it overlaps. ``join()`` at the end of the step
makes the main stream wait for the side stream, so a captured graph always rejoins
it. This lives in ``runtime/`` rather than ``cutedsl/`` because it is host-side
orchestration across layers: a stream, events, and per-(point, layer, batch) tables
built from the model's layers.
"""
import ctypes
from functools import partial
import cuda.bindings.driver as cuda
import torch
from ...ops.cute_dsl.latent_moe_tail.primitives import to_cute
from ..cutedsl.common.host import _checked, resolve_kernel
from ..cutedsl.common.policy import batch_table
from ..cutedsl.common.prefetch import compile_kernel
# Per-TP batch -> MiB cap (or cap.carveout) of the next layer's attention weights,
# prefetched once the routed experts finish.
# Not at B>8: the HBM-bound experts lose more bandwidth than the attention gains.
NEXT_ATTENTION = {8: "1:96.100,2-3:96,4-7:64,8:32", 16: "1-8:1024"}
def attention_weights(attention):
"""Next-attention weights in their consumption order."""
names = (
*("in_proj_qkvgfab", "fused_qkv_a_g_proj", "fused_qkv_a_proj", "q_proj"),
*("kv_a_proj_with_mqa", "f_b_proj", "q_b_proj", "g_proj"),
)
weights = [getattr(getattr(attention, n, None), "weight", None) for n in names]
weights += [getattr(attention, "W_UK_T", None), getattr(attention, "W_UV", None)]
weights = [w for w in weights if isinstance(w, torch.Tensor)]
return (*weights, attention.o_proj.weight)
class PreparedWeightRanges:
"""One owner retains weights, immutable range table, executable and launch ABI."""
def __init__(self, weights, *, byte_cap=0, grid=32, carveout=None, priority=False):
self.weights = weights = tuple(weights)
device = weights[0].device
ranges, remaining = [], byte_cap
for weight in weights:
if weight.numel() == 0:
continue
if weight.data_ptr() % 16: # the kernel walks 16-byte aligned chunks
raise ValueError("L2 prefetch ranges must start 16-byte aligned")
span = 1 + sum((n - 1) * s for n, s in zip(weight.shape, weight.stride()))
size = span * weight.element_size()
if byte_cap:
size = min(size, remaining) // 16 * 16
remaining -= size
if size:
ranges.append((weight.data_ptr(), size))
if byte_cap and remaining < 16:
break
self.table = torch.tensor(ranges, dtype=torch.int64, device=device)
self.compiled = compile_kernel(len(ranges), grid, carveout, device.index)
self.config = None
if not priority and carveout is None:
self.views = to_cute(self.table, 16)
return
# Launch priority / carveout need a driver launch (CuTe drops both).
self.executor, self.function = resolve_kernel(self.compiled, device.index)
ids, self.attributes = cuda.CUlaunchAttributeID, []
if priority: # the greatest stream priority
top = cuda.CUlaunchAttribute()
top.id = ids.CU_LAUNCH_ATTRIBUTE_PRIORITY
top.value.priority = _checked(cuda.cuCtxGetStreamPriorityRange())[1]
self.attributes.append(top)
if carveout is not None:
smem = cuda.CUlaunchAttribute()
smem.id = ids.CU_LAUNCH_ATTRIBUTE_PREFERRED_SHARED_MEMORY_CARVEOUT
smem.value.sharedMemCarveout = carveout
self.attributes.append(smem)
config = self.config = cuda.CUlaunchConfig()
config.gridDimX, config.gridDimY, config.gridDimZ = grid, 1, 1
config.blockDimX, config.blockDimY, config.blockDimZ = 32, 1, 1
config.sharedMemBytes, config.attrs = 0, self.attributes
config.numAttrs = len(self.attributes)
self.pointer = ctypes.c_void_p(self.table.data_ptr())
self.arguments = (ctypes.c_void_p * 1)(ctypes.addressof(self.pointer))
self.address = ctypes.addressof(self.arguments)
def launch(self, stream):
if self.config is None:
self.compiled(self.views, stream)
else:
self.config.hStream = stream
_checked(cuda.cuLaunchKernelEx(self.config, self.function, self.address, 0))
class PrefetchController:
"""Prefetch launches per (point, layer, batch) on one side stream."""
def __init__(self, runtime, layers, packed):
self.device, tp = runtime.device, runtime.tp
self.stream = torch.cuda.Stream(device=self.device)
self.events, self.pending, self.tables = {}, False, {}
urgent = partial(PreparedWeightRanges, carveout=100, priority=True)
following = batch_table(NEXT_ATTENTION[tp])
for position, layer in enumerate(layers):
if not layer.is_moe_layer:
continue
index, moe, attention = layer.layer_idx, layer.mlp, layer.self_attn
# The front GEMM reads [router shard; down shard] from the packed copy.
ranges = urgent((packed[index],), grid=8)
self._add("oproj_down", index, range(3, 9), ranges)
if tp == 8:
shared = moe.shared_experts
shared = shared.gate_up_proj.weight, shared.down_proj.weight
ranges = urgent((packed[index], *shared), grid=32)
batches = range(5, 9) if hasattr(attention, "in_proj_qkvgfab") else (8,)
self._add("attention_moe", index, batches, ranges)
if position + 1 < len(layers):
weights = attention_weights(layers[position + 1].self_attn)
self._add_capped("next_attention", index, weights, following)
def _add_capped(self, point, layer, weights, table):
groups: dict[tuple, list[int]] = {}
for batch, spec in table.items():
cap, carveout = spec if isinstance(spec, tuple) else (spec, None)
if cap:
groups.setdefault((cap, carveout), []).append(batch)
for (cap, carveout), batches in groups.items():
ranges = PreparedWeightRanges(
weights, byte_cap=cap * 1024**2, grid=32, carveout=carveout
)
self._add(point, layer, batches, ranges)
def _add(self, point, layer, batches, ranges):
key = point, layer
if key not in self.events:
event = torch.cuda.Event()
event.record(torch.cuda.current_stream(self.device))
self.events[key] = event
for batch in batches:
self.tables[point, layer, batch] = ranges
def fork(self, point, layer, batch):
ranges = self.tables.get((point, layer, batch))
if ranges is None:
return False
event = self.events[point, layer]
event.record(torch.cuda.current_stream(self.device))
self.stream.wait_event(event)
self.pending = True
ranges.launch(cuda.CUstream(self.stream.cuda_stream))
return True
def join(self):
if self.pending:
torch.cuda.current_stream(self.device).wait_stream(self.stream)
self.pending = False
@@ -0,0 +1,172 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Per-layer attention providers (input projection, KDA/MLA core, o-projection) and
the helpers ``SpecializedDecode.build_providers`` uses."""
from typing import Any
import torch
from vllm.model_executor.kernels.linear.cute_dsl.skinny_gemm import (
SkinnyGemmConfig,
shape_dynamic_skinny_gemm,
)
from vllm.model_executor.layers.mamba.mamba_utils import is_conv_state_dim_first
from ..cutedsl.gemm.projection import PreparedProjection
from ..cutedsl.kda.prepared import PreparedKda
from ..cutedsl.mla.dcp import DcpBox
from ..cutedsl.mla.prepared import PreparedMla
from ..cutedsl.oproj.prepared import PreparedOproj
# Decode batches (tokens) served by the prepared providers: every batch up to 8, the
# padded CUDA-graph batches and each further captured size; others run stock.
BATCHES = (*range(1, 9), 16, 32, 64)
PREPARED_MAX = BATCHES[-1]
def projection_policies(tp, rows, batches):
"""Split-K (M tile, N tile, split, stages) per execution rows' next tested bin."""
policies = {}
for batch in batches:
n = next(b for b in (8, 16, 32, 64) if batch <= b)
if rows == 6288 and (tp == 8 or n > 8):
tactic = (64, 64, 1, 6) if n == 64 else (128, n, 2, 5 if n == 32 else 6)
else: # TP16 KDA B64: a sixth stage, -0.7 us per layer in the KDA chain
stages = {8: 8, 16: 10, 32: 8, 64: 6 if rows == 3216 else 5}[n]
tactic = (64, n, 4 if n == 8 else 2, stages)
policies[batch] = tactic
return policies
class SkinnyProjection:
"""B1/B2 skinny GEMM; static K loads two weight tiles before the PDL wait."""
def __init__(self, weight, fallback):
self.weight, self.fallback, k = weight, fallback, weight.shape[1]
tactics = {1: (128, 4, 1), 2: (64, 4, 2)}
self.configs = {m: SkinnyGemmConfig(m, *t, 8, k) for m, t in tactics.items()}
for m, cfg in self.configs.items(): # compile before capture
shape_dynamic_skinny_gemm(weight.new_zeros(m, k), weight, cfg, None)
def run(self, x, out):
cfg = self.configs.get(x.shape[0])
if cfg is None:
return self.fallback.run(x, out)
return shape_dynamic_skinny_gemm(x, self.weight, cfg, None)
def scratch(runtime, name, shape, dtype=torch.bfloat16):
if name not in runtime.buffers:
runtime.buffers[name] = torch.empty(shape, device=runtime.device, dtype=dtype)
return runtime.buffers[name]
class Attention:
"""Input projection -> KDA/MLA core -> o-projection with its fused AttnRes."""
def __init__(self, decoder, runtime, prefetch, mailbox, small_batch, twoshot=None):
attention, tp = decoder.self_attn, runtime.tp
self.kind = kind = "kda" if hasattr(attention, "in_proj_qkvgfab") else "mla"
name = "in_proj_qkvgfab" if kind == "kda" else "fused_qkv_a_g_proj"
weight = getattr(attention, name).weight
pitch = weight.shape[0]
policies = projection_policies(tp, pitch, runtime.batches)
split_k = PreparedProjection(weight, policies).prepare()
self.projection = SkinnyProjection(weight, split_k)
self.core: Any # PreparedKda or PreparedMla
if kind == "kda":
mode = lambda b: (1 if b == 1 else 2 if b <= 8 else 0) if tp == 8 else 0
policies = {b: (pitch, mode(b)) for b in runtime.batches}
q = runtime.q # verify steps: B requests x q tokens
verify = [b // q for b in runtime.batches if q > 1 and b % q == 0]
policies.update({(b, q): (pitch, int(tp == 8)) for b in verify})
self.core = PreparedKda(attention, policies, is_conv_state_dim_first())
else:
heads = attention.num_local_heads
query = scratch(runtime, "mla.query", (64, heads, 576), torch.uint8)
policy = dict(max_context=runtime.max_context, core=runtime.mla_core)
policy.update(queries=runtime.queries)
if runtime.dcp > 1:
policy.update(
dcp=dcp_box(runtime, heads), parity=runtime.mla_layers % 2
)
runtime.mla_layers += 1
pitches = {b: pitch for b in runtime.batches}
self.core = PreparedMla(attention, pitches, query, **policy)
parity = decoder.layer_idx % 2
front: tuple | None = None
if decoder.is_moe_layer:
front = small_batch.front_views[parity], small_batch.hint_views[parity]
front += (small_batch.batches,)
self.output = PreparedOproj(
decoder, mailbox, runtime.batches, kind == "mla", front, twoshot
)
projected = scratch(runtime, kind + ".projected", (64, pitch))
local = scratch(runtime, "attention.local", (64, 12288 // tp))
self.prefetch, self.layer = prefetch, decoder
self.storage = {b: (projected[:b], local[:b]) for b in range(1, 65)}
def prepare_layout(self, caches):
if self.kind == "kda":
self.core.prepare_caches(caches)
else:
self.core.prepare_cache(caches[0])
def run(self, x, out, prefix, blocks, caches, metadata, slot_mapping, q=1):
b = x.shape[0]
projected, local = self.storage[b]
projected = self.projection.run(x, projected)
if self.kind == "kda":
self.prefetch.fork("attention_moe", self.layer.layer_idx, b)
if q > 1: # uniform q-token verify rows
slots = metadata.spec_state_indices_tensor
accepted = metadata.num_accepted_tokens
self.core.run(
projected, local, caches=caches, slots=slots, accepted=accepted
)
else:
slots = metadata.non_spec_state_indices_tensor
self.core.run(projected, local, caches=caches, slots=slots)
else:
decode = metadata.decode
tables, lengths = decode.block_table, decode.seq_lens
# Request metadata shorter than b: the core runs the prepared batch b
# with the missing rows as empty requests.
rows = min(b, q * min(lengths.numel(), tables.shape[0]))
if rows < b:
local[rows:].zero_()
slots = slot_mapping[self.layer.self_attn.layer_name]
mla = dict(cache=caches[0], block_table=tables, lengths=lengths)
# DCP: lengths are the local shard's, totals the full sequences'.
mla.update(totals=decode.dcp_tot_seq_lens)
self.core.run(projected, local, slots=slots, q=q, **mla)
self.prefetch.fork("attention_moe", self.layer.layer_idx, b)
self.output.run(local, prefix, blocks, out)
self.prefetch.fork("oproj_down", self.layer.layer_idx, b)
return out
def dcp_box(runtime, heads):
"""The model's DCP mailboxes (one set for every MLA layer)."""
if runtime.dcp_box is None:
from vllm.distributed import get_dcp_group
group = get_dcp_group()
assert group.world_size == runtime.dcp == runtime.tp
from vllm.config import get_current_vllm_config
args = group.device_group, runtime.device, group.world_size, group.rank_in_group
parallel = get_current_vllm_config().parallel_config
interleave = parallel.cp_kv_cache_interleave_size
runtime.dcp_box = DcpBox(*args, heads, interleave=interleave)
return runtime.dcp_box
def pack_front(moe, rank, tp):
"""[this rank's router rows (padded to 16); this rank's latent down shard]."""
router, down = moe.gate.weight, moe.routed_expert_down_proj.weight
rows, shard = 896 // tp, 3584 // tp
router = router[rank * rows : (rank + 1) * rows]
down = down[rank * shard : (rank + 1) * shard]
return torch.cat((router, router.new_zeros((-rows % 16, 7168)), down)).detach()