Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

1099 lines
40 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""CuTe DSL based sampling kernels.
Wraps the upstream CuTe DSL ``ArgmaxKernel`` (derived from the Quack library and
ported through TensorRT-LLM) so the sampling public API can register it
without touching the third-party module directly.
Exports two entry points:
* :func:`argmax`: drop-in replacement for ``torch.argmax(logits, dim=-1)``.
Returns int64 indices written by the kernel directly — no post-kernel cast
on the hot path. Transparently falls back to ``torch.argmax`` when the CuTe
DSL kernel is unavailable or its preconditions are not met
(dtype/N/alignment/SM-version).
* :func:`argmax_pair`: row-wise ``(max_value, argmax_index)`` packed as a
single ``(M, 2)`` float32 tensor. The kernel writes the max value and index
into two separate tensors; this entry point assembles them back into the
legacy ``(M, 2)`` layout (one extra elementwise copy off the hot path). The
runtime no longer uses this layout — kept for tests / future logprob users.
Platform support:
The CuTe DSL kernel ships only for NVIDIA Hopper/Blackwell (sm_90..<sm_120).
The common ``tokenspeed_kernel.ops.sampling.argmax`` API selects this
registered solution on NVIDIA.
"""
from dataclasses import dataclass
import torch
import torch.distributed as _dist
import torch.distributed._symmetric_memory as _symm_mem
from tokenspeed_kernel.platform import (
ArchVersion,
CapabilityRequirement,
current_platform,
)
from tokenspeed_kernel.registry import Priority, error_fn, register_kernel
from tokenspeed_kernel.signature import format_signatures
__all__ = [
"argmax",
"argmax_pair",
"create_dist_argmax_state",
"cute_dsl_argmax",
"distributed_argmax",
"is_available",
]
_argmax_kernel_impl = error_fn
_compile_cache: dict[tuple, object] = {}
# Minimum vocab size for the CuTe tiled kernel.
#
# The kernel hangs on B200 (sm_100) when ``_calculate_threads_per_row()``
# returns 32 AND ``tiler_mn[1] == N`` (i.e. ``is_even_N`` skips ``fill_oob``).
# Empirically that happens for N ∈ {256, 512, 1024, 2048, 3072} — every clean
# multiple in the upstream ``128 < N <= 3072`` band. Bumping the floor above
# 3072 sidesteps the bad band entirely; every real LLM vocab (≥ 30K) is far
# above this, so we never lose the kernel in practice.
_MIN_VOCAB_SIZE = 4096
# The async copy requires 128-byte alignment.
_VOCAB_SIZE_ALIGNMENT = 32
def _ts_supported_arch() -> bool:
"""Gate: only NVIDIA Hopper/Blackwell run the CuTe DSL kernel.
* Vendor must be NVIDIA — AMD ROCm and any future vendor get the torch
fallback (CuTe DSL has no ROCm backend).
* SM range ``[9.0, 12.0)``: ``redux.sync.max.f32`` exists from Blackwell
(sm_100/sm_103); we run on Hopper too via the shuffle path. ``sm_120+``
is excluded — upstream TRT-LLM reports CUTLASS DSL JIT instability there.
* If platform detection itself raises (e.g. CPU-only host with no GPU),
treat it as unsupported and let callers fall back transparently.
"""
try:
p = current_platform()
except Exception:
return False
if not p.is_nvidia:
return False
sm = p.arch_version.major * 10 + p.arch_version.minor
return 90 <= sm < 120
def _has_cluster_launch_support() -> bool:
"""Check if the GPU supports TMA cluster launches required by the kernel.
The CUTLASS DSL ArgmaxKernel uses cluster dimensions > 1 via TMA, which
requires hardware cluster launch support. NVIDIA H20 GPUs report sm_90
(Hopper architecture) but lack the cluster launch capability, causing
CUDA_ERROR_INVALID_CLUSTER_SIZE (error 912) at kernel launch time.
This function uses a device-name heuristic to detect H20 SKUs and route
them through the torch.argmax fallback instead.
"""
try:
p = current_platform()
except Exception:
return False
if not p.is_nvidia:
return False
# Only Hopper (sm_90) SKUs are affected -- Blackwell always supports
# cluster launches.
if p.arch_version.major > 9:
return True
# Device-name heuristic: H20 is the only sm_90 SKU known to lack cluster
# launch support. Other Hopper SKUs (H100, H200, H800) all support it.
import re
return not re.search(r"\bH20\b", p.device_name, re.IGNORECASE)
_ARCH_SUPPORTED = _ts_supported_arch()
# Only import the third-party CuTe DSL module on supported NVIDIA hardware
# with cluster launch capability. H20 GPUs (sm_90 without TMA cluster support)
# hit CUDA_ERROR_INVALID_CLUSTER_SIZE; route them through torch.argmax instead.
_CUTE_AVAILABLE = False
if _ARCH_SUPPORTED and _has_cluster_launch_support():
try:
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
from cutlass._mlir.dialects import llvm
from cutlass.cute.runtime import from_dlpack
from cutlass.cute.typing import Float32, Int32
from cutlass.cutlass_dsl import T, dsl_user_op
from tokenspeed_kernel.thirdparty.cute_dsl.argmax import (
ArgmaxKernel,
CUDAGraphCompatibleWrapper,
domain_offset_i64,
elem_pointer,
fill_oob,
predicate_k,
store_shared_remote,
torch2cute_dtype_map,
warp_argmax_redux,
warp_reduce_argmax,
)
_CUTE_AVAILABLE = True
except ImportError:
_CUTE_AVAILABLE = False
def is_available() -> bool:
"""Whether the CuTe DSL argmax kernel can run on this platform."""
return _CUTE_AVAILABLE
def _supports_cute(N: int, dtype: torch.dtype) -> bool:
if not _CUTE_AVAILABLE:
return False
if dtype not in (torch.float16, torch.bfloat16, torch.float32):
return False
if N < _MIN_VOCAB_SIZE:
return False
if N % _VOCAB_SIZE_ALIGNMENT != 0:
return False
return True
def _convert_to_cute(t: torch.Tensor):
"""Wrap a torch tensor as a CuTe DSL tensor with a CUDA-graph-safe view."""
return from_dlpack(
CUDAGraphCompatibleWrapper(t.detach()), assumed_align=16
).mark_compact_shape_dynamic(mode=0, stride_order=(0, 1))
def _convert_to_cute_1d(t: torch.Tensor):
"""1D-tensor variant of :func:`_convert_to_cute`."""
return from_dlpack(
CUDAGraphCompatibleWrapper(t.detach()), assumed_align=16
).mark_compact_shape_dynamic(mode=0, stride_order=(0,))
def _invoke_kernel(
logits: torch.Tensor, out_max: torch.Tensor, out_idx: torch.Tensor
) -> None:
"""Launch ArgmaxKernel with separate ``(M,)`` max and idx output tensors.
Caller is responsible for shape/dtype checks; this helper assumes inputs
are already validated by :func:`_supports_cute`.
"""
dtype = torch2cute_dtype_map[logits.dtype]
x_tensor = _convert_to_cute(logits)
max_tensor = _convert_to_cute_1d(out_max)
idx_tensor = _convert_to_cute_1d(out_idx)
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
# Blackwell (sm_100/103) supports redux.sync.max.f32; Hopper falls back to
# warp shuffles.
p = current_platform()
sm = p.arch_version.major * 10 + p.arch_version.minor
use_redux = 100 <= sm < 120
N = logits.shape[1]
# Cache by index dtype too: the kernel writes the index with the output
# tensor's element type, so int64 vs int32 produce distinct compiled units.
compile_key = (dtype, N, use_redux, out_idx.dtype)
compiled = _compile_cache.get(compile_key)
if compiled is None:
kernel = ArgmaxKernel(dtype, N, use_redux=use_redux)
compiled = cute.compile(kernel, x_tensor, max_tensor, idx_tensor, stream)
_compile_cache[compile_key] = compiled
compiled(x_tensor, max_tensor, idx_tensor, stream)
_SUPPORTED_OUT_DTYPES = (torch.int32, torch.int64)
def _validate_argmax_out(logits: torch.Tensor, out: torch.Tensor) -> None:
if out.shape != (logits.shape[0],):
raise ValueError(
f"out must have shape (M,)={(logits.shape[0],)}, got {tuple(out.shape)}"
)
if out.dtype not in _SUPPORTED_OUT_DTYPES:
raise ValueError(f"out must be int32 or int64; got {out.dtype}")
if out.device != logits.device:
raise ValueError("out must be on the same device as logits")
def _validate_argmax_pair_out(logits: torch.Tensor, out: torch.Tensor) -> None:
M = logits.shape[0]
if out.shape != (M, 2):
raise ValueError(f"out must have shape (M, 2)={M, 2}, got {tuple(out.shape)}")
if out.dtype != torch.float32 or out.device != logits.device:
raise ValueError("out must be float32 on the same device as logits")
def _argmax_torch_fallback(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
"""Pure-torch implementation of :func:`argmax`.
Selected at import time on non-NVIDIA / unsupported-SM hosts (AMD ROCm,
CPU-only, sm_80, sm_120+, missing ``nvidia-cutlass-dsl``). Also reached
per-call from the cute path when the input fails the kernel's
preconditions (1D / non-CUDA / fp16 / bf16 / small N / unaligned N).
"""
if out is not None:
_validate_argmax_out(logits, out)
result = torch.argmax(logits, dim=-1)
if out is not None:
out.copy_(result)
return out
return result
def _argmax_pair_torch_fallback(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
"""Pure-torch implementation of :func:`argmax_pair`.
Selected at import time on non-NVIDIA / unsupported-SM hosts, and reached
per-call from the cute path when the input fails the kernel's
preconditions.
"""
if logits.dim() != 2:
raise ValueError(f"argmax_pair expects 2D input, got {logits.dim()}D")
M = logits.shape[0]
device = logits.device
if out is None:
out = torch.empty((M, 2), dtype=torch.float32, device=device)
else:
_validate_argmax_pair_out(logits, out)
max_vals, max_indices = torch.max(logits, dim=-1, keepdim=True)
out[:, 0:1].copy_(max_vals.to(torch.float32))
out[:, 1:2].copy_(max_indices.to(torch.float32))
return out
def _register_cute_argmax(fn):
if not _CUTE_AVAILABLE:
return fn
return register_kernel(
"sampling",
"argmax",
name="cute_dsl_argmax",
solution="cute_dsl",
capability=CapabilityRequirement(
min_arch_version=ArchVersion(9, 0),
max_arch_version=ArchVersion(11, 9),
vendors=frozenset({"nvidia"}),
),
signatures=format_signatures(
"logits", "dense", {torch.float16, torch.bfloat16, torch.float32}
),
priority=Priority.SPECIALIZED,
tags={"latency", "determinism"},
)(fn)
@_register_cute_argmax
def _argmax_cute(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
"""CuTe DSL fast path for argmax.
Falls back per-call to :func:`_argmax_torch_fallback` when the input
isn't kernel-eligible (1D / non-CUDA / empty batch / unsupported dtype /
small N / unaligned N). Only ever bound to the public ``argmax`` name on
NVIDIA hosts with the cute DSL Python packages available — see the
module-level dispatch below.
"""
if out is not None:
_validate_argmax_out(logits, out)
if (
logits.dim() != 2
or not logits.is_cuda
or logits.shape[0] == 0
or not _supports_cute(logits.shape[1], logits.dtype)
):
return _argmax_torch_fallback(logits, out=out)
M = logits.shape[0]
device = logits.device
out_idx = (
out if out is not None else torch.empty((M,), dtype=torch.int64, device=device)
)
# The max value is needed only inside the kernel reduction; the caller
# never sees it. Allocate a scratch buffer so the kernel has somewhere to
# write it.
scratch_max = torch.empty((M,), dtype=torch.float32, device=device)
_invoke_kernel(logits, scratch_max, out_idx)
return out_idx
def _argmax_pair_cute(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
"""CuTe DSL fast path for argmax_pair. Falls back per-call when needed."""
if logits.dim() != 2:
raise ValueError(f"argmax_pair expects 2D input, got {logits.dim()}D")
M, N = logits.shape
device = logits.device
if out is None:
out = torch.empty((M, 2), dtype=torch.float32, device=device)
else:
_validate_argmax_pair_out(logits, out)
if not logits.is_cuda or M == 0 or not _supports_cute(N, logits.dtype):
# Reuse the pure-torch packing path; pass our pre-allocated buffer so
# the caller-supplied ``out`` is honored.
return _argmax_pair_torch_fallback(logits, out=out)
# Kernel writes into separate (M,) tensors; assemble into the legacy
# (M, 2) layout for backward compatibility. This is off the runtime hot
# path (callers use :func:`argmax` instead), so the extra copy/cast is OK.
tmp_max = torch.empty((M,), dtype=torch.float32, device=device)
tmp_idx = torch.empty((M,), dtype=torch.int64, device=device)
_invoke_kernel(logits, tmp_max, tmp_idx)
out[:, 0].copy_(tmp_max)
out[:, 1].copy_(tmp_idx.to(torch.float32))
return out
cute_dsl_argmax = _argmax_cute
# Direct CuTe DSL module API. The common runtime-facing API lives in
# tokenspeed_kernel.ops.sampling and selects among registered solutions.
if _CUTE_AVAILABLE:
argmax = _argmax_cute
argmax_pair = _argmax_pair_cute
_argmax_kernel_impl = _invoke_kernel
else:
argmax = _argmax_torch_fallback
argmax_pair = _argmax_pair_torch_fallback
# Distributed (cross-rank) argmax — kernel + operator with symm-mem workspace.
if _CUTE_AVAILABLE:
@dsl_user_op
def ptx_multimem_st_release_u64(
mc_ptr: cutlass.Int64, value: cutlass.Int64, *, loc=None, ip=None
) -> None:
llvm.inline_asm(
None,
[
cutlass.Int64(mc_ptr).ir_value(loc=loc, ip=ip),
cutlass.Int64(value).ir_value(loc=loc, ip=ip),
],
"""multimem.st.release.sys.global.b64 [$0], $1;""",
"l,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def ptx_ld_acquire_sys_u64(
addr: cutlass.Int64, *, loc=None, ip=None
) -> cutlass.Int64:
return cutlass.Int64(
llvm.inline_asm(
T.i64(),
[cutlass.Int64(addr).ir_value(loc=loc, ip=ip)],
"""ld.acquire.sys.global.u64 $0, [$1];""",
"=l,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
@dsl_user_op
def ptx_atomic_and_relaxed_sys_u64(
addr: cutlass.Int64, mask: cutlass.Int64, *, loc=None, ip=None
) -> cutlass.Int64:
return cutlass.Int64(
llvm.inline_asm(
T.i64(),
[
cutlass.Int64(addr).ir_value(loc=loc, ip=ip),
cutlass.Int64(mask).ir_value(loc=loc, ip=ip),
],
"""atom.and.relaxed.sys.global.b64 $0, [$1], $2;""",
"=l,l,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
# u64 slot layout: [flag:63 | idx:32..62 | f32 value:0..31].
@cute.jit
def pack_argmax_payload_u64(
val_f32: cutlass.Float32, global_idx: cutlass.Int32
) -> cutlass.Int64:
val_bits = val_f32.bitcast(cutlass.Int32).to(cutlass.Int64) & cutlass.Int64(
0xFFFFFFFF
)
idx_bits = cutlass.Int64(global_idx) & cutlass.Int64(0x7FFFFFFF)
flag_bit = cutlass.Int64(1) << cutlass.Int64(63)
return flag_bit | (idx_bits << cutlass.Int64(32)) | val_bits
@cute.jit
def unpack_argmax_payload_u64(packed: cutlass.Int64):
val_bits32 = (packed & cutlass.Int64(0xFFFFFFFF)).to(Int32)
val_f32 = val_bits32.bitcast(cutlass.Float32)
idx_i32 = ((packed >> cutlass.Int64(32)) & cutlass.Int64(0x7FFFFFFF)).to(Int32)
return val_f32, idx_i32
@dsl_user_op
def ptx_ld_global_u32(addr: cutlass.Int64, *, loc=None, ip=None) -> cutlass.Int32:
return cutlass.Int32(
llvm.inline_asm(
T.i32(),
[cutlass.Int64(addr).ir_value(loc=loc, ip=ip)],
"""ld.global.u32 $0, [$1];""",
"=r,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
@dsl_user_op
def ptx_atomic_add_acq_rel_sys_u32(
addr: cutlass.Int64, val: cutlass.Int32, *, loc=None, ip=None
) -> cutlass.Int32:
return cutlass.Int32(
llvm.inline_asm(
T.i32(),
[
cutlass.Int64(addr).ir_value(loc=loc, ip=ip),
cutlass.Int32(val).ir_value(loc=loc, ip=ip),
],
"""atom.add.acq_rel.sys.global.u32 $0, [$1], $2;""",
"=r,l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
@dsl_user_op
def ptx_st_relaxed_sys_u32(
addr: cutlass.Int64, val: cutlass.Int32, *, loc=None, ip=None
) -> None:
llvm.inline_asm(
None,
[
cutlass.Int64(addr).ir_value(loc=loc, ip=ip),
cutlass.Int32(val).ir_value(loc=loc, ip=ip),
],
"""st.relaxed.sys.global.u32 [$0], $1;""",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
class DistArgmaxKernel(ArgmaxKernel):
"""Cross-rank argmax: per-rank local argmax + warp-level peer reduce.
``skip_ping_pong=False`` (default): kernel reads ``round_id`` and
alternates between two slot bands so back-to-back calls are
race-free on their own. ``skip_ping_pong=True``: hardcodes band
0; caller must guarantee an external sync between consecutive
calls (same contract as ``all_gather_inner``'s
``SKIP_ENTRY_SYNC=True``).
"""
def __init__(
self,
dtype,
N: int,
use_redux: bool = False,
world_size: int = 1,
rank: int = 0,
skip_ping_pong: bool = False,
):
super().__init__(dtype, N, use_redux=use_redux)
self.world_size = world_size
self.rank = rank
self.dist_tp_enabled = world_size > 1
self.skip_ping_pong = skip_ping_pong
@cute.jit
def __call__(
self,
mX: cute.Tensor,
mO_max: cute.Tensor,
mO_idx: cute.Tensor,
stream: cuda.CUstream,
slot_ptrs: cutlass.Int64,
slot_multicast_ptr: cutlass.Int64,
round_id_ptr: cutlass.Int64,
warps_done_ptr: cutlass.Int64,
):
self._set_cluster_n()
tiler_mn, tv_layout = self._get_tv_layout()
num_threads = cute.size(tv_layout, mode=[0])
num_warps = num_threads // cute.arch.WARP_SIZE
self.kernel(
mX,
mO_max,
mO_idx,
tv_layout,
tiler_mn,
slot_ptrs,
slot_multicast_ptr,
round_id_ptr,
warps_done_ptr,
).launch(
grid=[cute.ceil_div(mX.shape[0], tiler_mn[0]), self.cluster_n, 1],
block=[num_threads, 1, 1],
cluster=(
[1, self.cluster_n, 1]
if cutlass.const_expr(self.cluster_n > 1)
else None
),
smem=self._smem_size_in_bytes(tiler_mn, num_warps),
stream=stream,
)
@cute.kernel
def kernel(
self,
mX: cute.Tensor,
mO_max: cute.Tensor,
mO_idx: cute.Tensor,
tv_layout: cute.Layout,
tiler_mn: cute.Shape,
slot_ptrs: cutlass.Int64,
slot_multicast_ptr: cutlass.Int64,
round_id_ptr: cutlass.Int64,
warps_done_ptr: cutlass.Int64,
):
tidx, _, _ = cute.arch.thread_idx()
bidx, bidy, bidz = cute.arch.block_idx()
if cutlass.const_expr(self.cluster_n > 1):
cluster_y = cute.arch.block_idx()[1]
else:
cluster_y = cutlass.const_expr(0)
shape = mX.shape
idX = cute.make_identity_tensor(shape)
mX = domain_offset_i64((bidx * tiler_mn[0], 0), mX)
gX = cute.local_tile(mX, tiler_mn, (0, cluster_y))
mO_max = domain_offset_i64((bidx * tiler_mn[0],), mO_max)
mO_idx = domain_offset_i64((bidx * tiler_mn[0],), mO_idx)
cX = cute.local_tile(idX, tiler_mn, (bidx, cluster_y))
smem = cutlass.utils.SmemAllocator()
sX = smem.allocate_tensor(
mX.element_type,
cute.make_ordered_layout(tiler_mn, order=(1, 0)),
byte_alignment=16,
)
reduction_buffer, mbar_ptr = self._allocate_reduction_buffer_and_mbar(
smem, tv_layout
)
copy_atom_load_X = cute.make_copy_atom(
cute.nvgpu.cpasync.CopyG2SOp(),
mX.element_type,
num_bits_per_copy=128,
)
thr_copy_X = cute.make_tiled_copy(
copy_atom_load_X, tv_layout, tiler_mn
).get_slice(tidx)
tXgX = thr_copy_X.partition_S(gX)
tXsX = thr_copy_X.partition_D(sX)
tXcX = thr_copy_X.partition_S(cX)[(0, None), None, None]
tvlayout_cX = cute.composition(cX, tv_layout)
thr_coord = (tidx, (None, None))
thr_cX = tvlayout_cX[thr_coord]
tXrX = cute.make_fragment_like(tXgX)
num_warps = cute.size(tv_layout, mode=[0]) // cute.arch.WARP_SIZE
self._initialize_cluster(tidx, mbar_ptr, num_warps)
is_even_N = cutlass.const_expr(shape[1] == tiler_mn[1] * self.cluster_n)
tXpX = (
predicate_k(thr_copy_X.partition_S(cX), limit=shape[1])
if not is_even_N
else None
)
if tXcX[0][0] < shape[0]:
cute.copy(copy_atom_load_X, tXgX, tXsX, pred=tXpX)
cute.arch.cp_async_commit_group()
cute.arch.cp_async_wait_group(0)
if cutlass.const_expr(not is_even_N):
fill_oob(tXsX, tXpX, -tXsX.element_type.inf)
cute.autovec_copy(tXsX, tXrX)
x = tXrX.load().to(cute.Float32)
current_max = -tXsX.element_type.inf
current_argmax = Int32(0xFFFFFFFF)
for i in cutlass.range_constexpr(thr_cX.shape[0]):
for j in cutlass.range_constexpr(thr_cX.shape[1]):
col_idx = thr_cX[i, j][1]
linear_idx = i + j * thr_cX.shape[0]
element_value1 = x[linear_idx]
if element_value1 > current_max:
current_max = element_value1
current_argmax = Int32(col_idx)
lane_idx, warp_idx = cute.arch.lane_idx(), cute.arch.warp_idx()
if cutlass.const_expr(self.use_redux):
warp_max, warp_argmax = warp_argmax_redux(current_max, current_argmax)
else:
warp_max, warp_argmax = warp_reduce_argmax(current_max, current_argmax)
if cutlass.const_expr(self.cluster_n == 1):
warps_per_row = cute.size(reduction_buffer.shape[1])
row_idx_buf, col_idx_buf = (
warp_idx // warps_per_row,
warp_idx % warps_per_row,
)
if lane_idx == 0:
reduction_buffer[row_idx_buf, col_idx_buf, 0, 0] = warp_max
reduction_buffer[row_idx_buf, col_idx_buf, 0, 1] = warp_argmax.to(
cutlass.Float32
)
cute.arch.barrier()
block_reduce_max = -tXsX.element_type.inf
block_reduce_argmax = Int32(0xFFFFFFFF)
if lane_idx < warps_per_row:
block_reduce_max = reduction_buffer[row_idx_buf, lane_idx, 0, 0]
block_reduce_argmax = reduction_buffer[
row_idx_buf, lane_idx, 0, 1
].to(cutlass.Int32)
if cutlass.const_expr(self.use_redux):
warp_max, warp_argmax = warp_argmax_redux(
block_reduce_max, block_reduce_argmax
)
else:
warp_max, warp_argmax = warp_reduce_argmax(
block_reduce_max, block_reduce_argmax
)
else:
cute.arch.cluster_wait()
warps_per_row, cluster_n = reduction_buffer.shape[1]
cta_rank_in_cluster = cute.arch.block_idx_in_cluster()
rows_per_block, (warps_per_row, cluster_n), _, _ = (
reduction_buffer.shape
)
row_idx_buf, col_idx_buf = (
warp_idx // warps_per_row,
warp_idx % warps_per_row,
)
if warp_idx == 0:
with cute.arch.elect_one():
num_warps_total = rows_per_block * warps_per_row
cute.arch.mbarrier_arrive_and_expect_tx(
mbar_ptr,
num_warps_total
* cluster_n
* 2
* reduction_buffer.element_type.width
// 8,
)
if lane_idx < cluster_n:
store_shared_remote(
warp_max,
elem_pointer(
reduction_buffer,
(row_idx_buf, (col_idx_buf, cta_rank_in_cluster), 0, 0),
),
mbar_ptr,
peer_cta_rank_in_cluster=lane_idx,
)
store_shared_remote(
warp_argmax.to(cutlass.Float32),
elem_pointer(
reduction_buffer,
(row_idx_buf, (col_idx_buf, cta_rank_in_cluster), 0, 1),
),
mbar_ptr,
peer_cta_rank_in_cluster=lane_idx,
)
cute.arch.mbarrier_wait(mbar_ptr, phase=0)
block_reduce_val = -tXsX.element_type.inf
block_reduce_argmax = Int32(0xFFFFFFFF)
num_iter = cute.ceil_div(warps_per_row * cluster_n, cute.arch.WARP_SIZE)
for i in cutlass.range_constexpr(num_iter):
idx = lane_idx + i * cute.arch.WARP_SIZE
if idx < cute.size(reduction_buffer, mode=[1]):
element_max = reduction_buffer[row_idx_buf, idx, 0, 0]
element_argmax = reduction_buffer[row_idx_buf, idx, 0, 1].to(
cutlass.Int32
)
if element_max > block_reduce_val:
block_reduce_val = element_max
block_reduce_argmax = element_argmax
elif element_max == block_reduce_val:
if element_argmax < block_reduce_argmax:
block_reduce_argmax = element_argmax
if cutlass.const_expr(self.use_redux):
warp_max, warp_argmax = warp_argmax_redux(
block_reduce_val, block_reduce_argmax
)
else:
warp_max, warp_argmax = warp_reduce_argmax(
block_reduce_val, block_reduce_argmax
)
row_idx = tXcX[0][0]
warps_per_row = tv_layout.shape[0][0] // cute.arch.WARP_SIZE
local_row_idx = row_idx - (bidx * tiler_mn[0])
first_warp_for_row = local_row_idx * warps_per_row
first_thread_for_row = first_warp_for_row * cute.arch.WARP_SIZE
if cutlass.const_expr(self.dist_tp_enabled):
row_is_valid = (
row_idx < shape[0]
and local_row_idx >= 0
and local_row_idx < tiler_mn[0]
and (self.cluster_n == 1 or bidy == 0)
)
is_leader_warp = (warp_idx == first_warp_for_row) and row_is_valid
if is_leader_warp:
if cutlass.const_expr(self.skip_ping_pong):
round_bit = Int32(0)
else:
round_id_val = ptx_ld_global_u32(round_id_ptr)
round_bit = round_id_val & Int32(1)
band_off_u64 = (
cutlass.Int64(row_idx) * cutlass.Int64(2)
+ cutlass.Int64(round_bit)
) * cutlass.Int64(self.world_size)
if lane_idx == 0:
global_idx = Int32(self.rank * self.N) + warp_argmax
if warp_argmax == Int32(0xFFFFFFFF):
global_idx = Int32(0x7FFFFFFF)
packed = pack_argmax_payload_u64(warp_max, global_idx)
mc_addr = slot_multicast_ptr + (
band_off_u64 + cutlass.Int64(self.rank)
) * cutlass.Int64(8)
ptx_multimem_st_release_u64(mc_addr, packed)
local_pad = ptx_ld_acquire_sys_u64(
slot_ptrs + cutlass.Int64(self.rank) * cutlass.Int64(8)
)
flag_bit_check = cutlass.Int64(1) << cutlass.Int64(63)
clear_mask = cutlass.Int64(0x7FFFFFFFFFFFFFFF)
peer_val = -tXsX.element_type.inf
peer_idx = Int32(0x7FFFFFFF)
if lane_idx < self.world_size:
slot_addr = local_pad + (
band_off_u64 + cutlass.Int64(lane_idx)
) * cutlass.Int64(8)
v = cutlass.Int64(0)
while (v & flag_bit_check) == cutlass.Int64(0):
v = ptx_ld_acquire_sys_u64(slot_addr)
peer_val, peer_idx = unpack_argmax_payload_u64(v)
ptx_atomic_and_relaxed_sys_u64(slot_addr, clear_mask)
if cutlass.const_expr(self.use_redux):
warp_max, warp_argmax = warp_argmax_redux(peer_val, peer_idx)
else:
warp_max, warp_argmax = warp_reduce_argmax(peer_val, peer_idx)
# A row whose elements never beat -inf (all-NaN / all -inf) leaves
# the argmax at its 0xFFFFFFFF sentinel; emit the in-range index 0.
if warp_argmax == Int32(0x7FFFFFFF):
warp_argmax = Int32(0)
if cutlass.const_expr(not self.skip_ping_pong):
if lane_idx == 0:
old = ptx_atomic_add_acq_rel_sys_u32(
warps_done_ptr, cutlass.Int32(1)
)
if old == cutlass.Int32(shape[0]) - cutlass.Int32(1):
ptx_atomic_add_acq_rel_sys_u32(
round_id_ptr, cutlass.Int32(1)
)
ptx_st_relaxed_sys_u32(warps_done_ptr, cutlass.Int32(0))
if (
tidx == first_thread_for_row
and row_idx < shape[0]
and local_row_idx >= 0
and local_row_idx < tiler_mn[0]
and (self.cluster_n == 1 or bidy == 0)
):
mO_max[local_row_idx] = warp_max.to(mO_max.element_type)
mO_idx[local_row_idx] = warp_argmax.to(mO_idx.element_type)
_dist_argmax_compile_cache: dict[tuple, object] = {}
@dataclass
class DistArgmaxState:
group: _dist.ProcessGroup
rank_in_group: int
world_size: int
max_M: int
dtype: torch.dtype
device: torch.device
slot_buffer: torch.Tensor
slot_handle: object
round_id_gpu: torch.Tensor
warps_done_gpu: torch.Tensor
use_redux: bool
skip_ping_pong: bool = False
def create_dist_argmax_state(
group: _dist.ProcessGroup,
rank_in_group: int,
max_M: int,
dtype: torch.dtype = torch.bfloat16,
device: torch.device | None = None,
skip_ping_pong: bool = False,
) -> DistArgmaxState:
assert dtype in (
torch.bfloat16,
torch.float16,
torch.float32,
), f"distributed argmax supports bf16/fp16/fp32 value dtype; got {dtype}"
assert _ARCH_SUPPORTED and _CUTE_AVAILABLE, (
"distributed_argmax requires CuTe DSL on NVIDIA Hopper+/Blackwell; "
"current platform doesn't qualify."
)
device = device or torch.device(f"cuda:{torch.cuda.current_device()}")
world_size = group.size()
assert 1 <= world_size <= 32, (
f"world_size={world_size} unsupported; must be 1..32 (cross-rank "
f"reduce uses a single warp shuffle)"
)
p = current_platform()
sm = p.arch_version.major * 10 + p.arch_version.minor
use_redux = 100 <= sm < 120
slots = _symm_mem.empty(
(2 * max_M * world_size,),
dtype=torch.int64,
device=device,
)
slots.zero_()
round_id_gpu = torch.zeros(1, dtype=torch.int32, device=device)
warps_done_gpu = torch.zeros(1, dtype=torch.int32, device=device)
torch.cuda.current_stream().synchronize()
hdl = _symm_mem.rendezvous(slots, group=group)
assert hdl.rank == rank_in_group and hdl.world_size == world_size, (
f"symm-mem handle reports rank={hdl.rank}, world_size={hdl.world_size}, "
f"but state was constructed with rank_in_group={rank_in_group}, "
f"world_size={world_size}"
)
if not hdl.multicast_ptr:
raise RuntimeError(
f"distributed_argmax requires CUDA multicast / NVLS, but the "
f"symm-mem handle on device {device} reports multicast_ptr="
f"{hdl.multicast_ptr}. The kernel uses multimem.st.release.sys "
f"which needs NVSwitch + sm_90+ multicast support; non-NVLS "
f"hardware (PCIe-only, passthrough, etc.) cannot run this op."
)
_dist.barrier(group=group, device_ids=[device.index])
return DistArgmaxState(
group=group,
rank_in_group=rank_in_group,
world_size=world_size,
max_M=max_M,
dtype=dtype,
device=device,
slot_buffer=slots,
slot_handle=hdl,
round_id_gpu=round_id_gpu,
warps_done_gpu=warps_done_gpu,
use_redux=use_redux,
skip_ping_pong=skip_ping_pong,
)
def _dist_argmax_validate_inputs(
state: DistArgmaxState,
logits: torch.Tensor,
out_max: torch.Tensor | None,
out_idx: torch.Tensor | None,
) -> tuple[int, int]:
assert (
logits.dim() == 2
), f"logits must be 2D (M, N); got shape {tuple(logits.shape)}"
M, N = logits.shape
assert (
logits.device == state.device
), f"logits.device={logits.device} != state.device={state.device}"
assert (
logits.dtype == state.dtype
), f"logits.dtype={logits.dtype} != state.dtype={state.dtype}"
assert (
N >= _MIN_VOCAB_SIZE
), f"per-rank vocab N={N} below kernel floor {_MIN_VOCAB_SIZE}"
assert (
N % _VOCAB_SIZE_ALIGNMENT == 0
), f"per-rank vocab N={N} not aligned to {_VOCAB_SIZE_ALIGNMENT}"
if out_max is not None:
assert out_max.shape == (M,) and out_max.device == logits.device, (
f"out_max must be shape ({M},) on logits.device; got "
f"shape={tuple(out_max.shape)} device={out_max.device}"
)
if out_idx is not None:
_validate_argmax_out(logits, out_idx)
return M, N
def _dist_argmax_invoke_kernel(
state: DistArgmaxState,
logits: torch.Tensor,
out_max: torch.Tensor,
out_idx: torch.Tensor,
) -> None:
dtype = torch2cute_dtype_map[logits.dtype]
x_tensor = _convert_to_cute(logits)
max_tensor = _convert_to_cute_1d(out_max)
idx_tensor = _convert_to_cute_1d(out_idx)
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
N = logits.shape[1]
compile_key = (
dtype,
N,
state.use_redux,
out_max.dtype,
out_idx.dtype,
state.world_size,
state.rank_in_group,
state.skip_ping_pong,
)
round_id_ptr = state.round_id_gpu.data_ptr()
warps_done_ptr = state.warps_done_gpu.data_ptr()
compiled = _dist_argmax_compile_cache.get(compile_key)
if compiled is None:
kernel = DistArgmaxKernel(
dtype,
N,
use_redux=state.use_redux,
world_size=state.world_size,
rank=state.rank_in_group,
skip_ping_pong=state.skip_ping_pong,
)
compiled = cute.compile(
kernel,
x_tensor,
max_tensor,
idx_tensor,
stream,
state.slot_handle.buffer_ptrs_dev,
state.slot_handle.multicast_ptr,
round_id_ptr,
warps_done_ptr,
)
_dist_argmax_compile_cache[compile_key] = compiled
compiled(
x_tensor,
max_tensor,
idx_tensor,
stream,
state.slot_handle.buffer_ptrs_dev,
state.slot_handle.multicast_ptr,
round_id_ptr,
warps_done_ptr,
)
def distributed_argmax(
state: DistArgmaxState,
logits: torch.Tensor,
*,
out_max: torch.Tensor | None = None,
out_idx: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Distributed argmax over the vocab dim, across ``state.group``.
Contract: every rank in ``state.group`` calls this in lockstep with
matching ``M, N``, same ``state``, and the same call ordering — the
cross-rank exchange is collective and slot bands are reused across
calls. Do not share one ``DistArgmaxState`` across concurrent streams.
"""
M, N = _dist_argmax_validate_inputs(state, logits, out_max, out_idx)
device = logits.device
if state.world_size == 1:
max_vals, idx_vals = logits.max(dim=-1)
if out_max is not None:
out_max.copy_(max_vals.to(out_max.dtype))
max_vals = out_max
if out_idx is not None:
out_idx.copy_(idx_vals)
idx_vals = out_idx
return max_vals, idx_vals
assert M <= state.max_M, f"batch size {M} exceeds state.max_M={state.max_M}"
assert (
state.world_size * N < 0x7FFFFFFF
), f"world_size*N = {state.world_size * N} overflows the 31-bit slot idx"
if out_max is None:
out_max = torch.empty((M,), dtype=logits.dtype, device=device)
if out_idx is None:
out_idx = torch.empty((M,), dtype=torch.int64, device=device)
_dist_argmax_invoke_kernel(state, logits, out_max, out_idx)
return out_max, out_idx