项目文件夹

文件
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1370 行
58 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
# Copyright (c) 2026 by FlashInfer team.
# Copyright 2026 SGLang Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
"""CuTe DSL TGV BF16 GEMM (low-latency Blackwell GEMM, SM100/SM103 only).
Computes ``out[M, N] = x[M, K] @ weight[N, K].T (+ bias[N])`` for bf16 inputs,
fp32 accumulation, bf16 output. The kernel writes M-contiguous output, so the
runner swaps A and B (``gemm_fn(b.t(), a.t(), ...)``) to land row-major (M, N).
Toggled by ``use_2cta`` in the constructor:
use_2cta=False -> 1x1 cluster, 1-CTA tcgen05.mma, cta_n in [8, 256] step 8
use_2cta=True -> 2x1 cluster, 2-CTA tcgen05.mma, cta_n in [16, 256] step 16
Warp specialization (8 warps, 256 threads/CTA; warp 3 idle):
Warp 0 DMA_A TMA-loads A tiles
Warp 1 DMA_B TMA-loads B tiles; PDL griddepcontrol.wait
Warp 2 MMA tcgen05.mma into TMEM; owns alloc/dealloc
Warps 4-7 EPILOG TMEM -> RMEM -> bf16 cast -> st.global
"""
from __future__ import annotations
from typing import List, NamedTuple, Optional, Tuple, Type
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
import torch
from cutlass.cute import experimental as cute_ext
from cutlass.cute.nvgpu import tcgen05
from cutlass.cute.runtime import from_dlpack, make_fake_stream
from sglang.kernel_api_logging import debug_kernel_api
from sglang.srt.utils import get_device_sm
from sglang.srt.utils.common import direct_register_custom_op
# Tuple format: (cta_m, cta_n, num_ab_stage, use_2cta); cta_k is fixed at
# 128. Sorted within each use_2cta block by (cta_m, cta_n, num_ab_stage).
# Tactic 1 (1-CTA 64×8 stages=8) is the default.
_TGV_CUTE_EXT_CTA_K: int = 128
_TGV_CUTE_EXT_DEFAULT_TACTIC: int = 1
_TGV_CUTE_EXT_TACTIC_CONFIGS: List[Tuple[int, int, int, bool]] = [
# 1-CTA configs
(64, 8, 6, False), # 0
(64, 8, 8, False), # 1 (default)
(64, 8, 10, False), # 2
(64, 8, 12, False), # 3
(64, 16, 6, False), # 4
(64, 16, 8, False), # 5
(64, 16, 11, False), # 6
(64, 32, 6, False), # 7
(64, 32, 9, False), # 8
(64, 64, 7, False), # 9
(64, 128, 4, False), # 10
(128, 8, 6, False), # 11
(128, 16, 6, False), # 12
(128, 32, 5, False), # 13
(128, 64, 4, False), # 14
(128, 128, 3, False), # 15
# 2-CTA configs
(64, 16, 6, True), # 16
(64, 16, 8, True), # 17
(64, 16, 12, True), # 18
(64, 32, 6, True), # 19
(64, 32, 8, True), # 20
(64, 32, 11, True), # 21
(64, 64, 6, True), # 22
(64, 64, 9, True), # 23
(64, 128, 7, True), # 24
(128, 16, 6, True), # 25
(128, 32, 6, True), # 26
(128, 64, 5, True), # 27
(128, 128, 4, True), # 28
]
def get_tgv_cute_ext_tactic_num() -> int:
return len(_TGV_CUTE_EXT_TACTIC_CONFIGS)
def get_tgv_cute_ext_default_tactic() -> int:
return _TGV_CUTE_EXT_DEFAULT_TACTIC
class WorkTileInfo(NamedTuple):
"""Which output tile this CTA processes. Mirrors the original WorkTileInfo."""
M_idx: cutlass.Int32
N_idx: cutlass.Int32
L_idx: cutlass.Int32
K_idx_start: cutlass.Int32
K_idx_end: cutlass.Int32
class TgvGemmCuteExtKernel:
"""
Low-latency Blackwell GEMM kernel rewritten with cute_ext primitives,
keeping the raw-mbarrier 7-warp specialization from the C++ kernel
in ``include/flashinfer/gemm/tgv_gemm.cuh``.
"""
def __init__(
self,
acc_dtype: Type[cutlass.Numeric] = cutlass.Float32,
cta_m: int = 64,
cta_n: int = 8,
cta_k: int = _TGV_CUTE_EXT_CTA_K,
num_ab_stage: int = 8,
use_2cta: bool = False,
use_pdl: bool = False,
pdl_launch: Optional[bool] = None,
pdl_count: int = -1,
has_bias: bool = False,
):
self.acc_dtype = acc_dtype
self.cta_m = cta_m
self.cta_n = cta_n
self.cta_k = cta_k
self.num_ab_stage = num_ab_stage
self.use_2cta = use_2cta
self.use_pdl = use_pdl
self.pdl_launch = pdl_launch if pdl_launch is not None else use_pdl
self.pdl_count = pdl_count
# has_bias: when True, kernel reads a (Gemm_M, Gemm_N, Gemm_L):(1,0,0)
# bias tensor (M-broadcast over N,L), converts it to fp32 in RMEM, and
# adds it to the accumulator before the bf16 cast. When False, all
# bias-related code is elided via cutlass.const_expr.
self.has_bias = has_bias
# 1-CTA: cta_n ∈ [8, 256] step 8 (bf16 tcgen05.mma atom limit).
# 2-CTA: cta_n ∈ [16, 256] step 16 (bf16 K-major cluster mma).
min_n, step_n = (16, 16) if use_2cta else (8, 8)
if cta_n < min_n or cta_n > 256 or cta_n % step_n != 0:
raise ValueError(
f"cta_n={cta_n} invalid for use_2cta={use_2cta}: "
f"bf16 K-major mma requires N ∈ [{min_n}, 256] step {step_n}"
)
# Fixed configuration matching the C++ / DSL kernels.
self.threads_per_cta = 256 # 8 warps (warp 3 unused)
if use_2cta:
# 2-CTA cluster along M; joint MMA tile = (cta_m*2, cta_n).
self.cluster_shape = (2, 1, 1)
self.mma_tiler_mn = (cta_m * 2, cta_n)
self.cta_group = tcgen05.CtaGroup.TWO
self.tma_op = cute_ext.OperationTypeEnum.SM100_TMA_LOAD_2SM
else:
# 1 SM mode, 1x1 cluster, no multicast.
self.cluster_shape = (1, 1, 1)
self.mma_tiler_mn = (cta_m, cta_n)
self.cta_group = tcgen05.CtaGroup.ONE
self.tma_op = cute_ext.OperationTypeEnum.SM90_TMA_LOAD
def __repr__(self) -> str:
return (
f"TgvGemmCuteExtKernel_cta{self.cta_m}x{self.cta_n}x{self.cta_k}"
f"_2cta{int(self.use_2cta)}_pdl{int(self.use_pdl)}"
f"_bias{int(self.has_bias)}"
)
@cute.experimental.jit
def __call__(
self,
a: cute.Tensor, # (Gemm_M, Gemm_K, Gemm_L), K-major
b: cute.Tensor, # (Gemm_N, Gemm_K, Gemm_L), K-major
c: cute.Tensor, # (Gemm_M, Gemm_N, Gemm_L), M-major
bias: cute.Tensor, # (Gemm_M, Gemm_N, Gemm_L):(1,0,0) — unused when has_bias=False
stream: cuda.CUstream,
):
# Each CTA processes one (CTA_M, CTA_N) output tile, no persistence.
# Round the (M, N) tile counts up to a full cluster so 2-CTA launches
# don't leave an odd M-CTA without its peer (e.g. M=2880, cta_m=64 →
# 45 M-tiles, invalid for cluster=(2,1,1)). cute_ext OOB-protects the
# extra CTA's GMEM stores.
grid = cute.round_up(
(
cute.ceil_div(c.layout.shape[0], self.cta_m),
cute.ceil_div(c.layout.shape[1], self.cta_n),
c.layout.shape[2],
),
self.cluster_shape,
)
self.kernel(a, b, c, bias).launch(
grid=grid,
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=self.pdl_launch,
)
@cute.experimental.kernel
def kernel(
self,
mA: cute.Tensor, # (Gemm_M, Gemm_K, Gemm_L), K-major
mB: cute.Tensor, # (Gemm_N, Gemm_K, Gemm_L), K-major
mC: cute.Tensor, # (Gemm_M, Gemm_N, Gemm_L), M-major
mBias: cute.Tensor, # (Gemm_M, Gemm_N, Gemm_L):(1,0,0) — unused when has_bias=False
):
"""
Device-side dispatcher: build MMA descriptor, allocate SMEM/TMEM/
mbarriers, init barriers, dispatch into dma_a/dma_b/mma/epilog warps.
"""
DMA_Stage = self.num_ab_stage
# ---- Tiled MMA ----
# make_trivial_tiled_mma picks the largest tcgen05.mma atom that fits
# mma_tiler_mn. 1-CTA bf16 (64,8): Mma_M=(16,4)=64, Mma_N=8, Mma_K=16.
# 2-CTA bf16 K-major (128,N): Mma_M=128 split across cluster, Mma_K=16.
a_major = utils.LayoutEnum.from_tensor(mA).mma_major_mode()
b_major = utils.LayoutEnum.from_tensor(mB).mma_major_mode()
ab_dtype = mA.element_type # bf16
c_dtype = mC.element_type # bf16
d_layout = utils.LayoutEnum.from_tensor(mC)
tiled_mma = sm100_utils.make_trivial_tiled_mma(
ab_dtype,
ab_dtype,
a_major,
b_major,
self.acc_dtype,
self.cta_group,
self.mma_tiler_mn,
)
num_mma_ctas = cute.size(tiled_mma.thr_id.shape) # 1 (1-CTA) or 2 (2-CTA)
# NumMma_K = CTA_K/Mma_K — inner-K trip count for the MMA warp.
# bf16 default: Mma_K=16, CTA_K=128 → mma_inst_tile_k = 8.
mma_inst_shape_k = cute.size(tiled_mma.shape_mnk, mode=[2])
mma_inst_tile_k = self.cta_k // mma_inst_shape_k
mnk_tiler = (self.mma_tiler_mn[0], self.mma_tiler_mn[1], self.cta_k)
a_tiler_mk = (self.cta_m, self.cta_k) # CTA tile of A
# 2-CTA splits B across cluster M (each CTA loads Mma_N/2 cols).
b_tiler_nk = (self.cta_n // num_mma_ctas, self.cta_k)
c_tiler_mn = (self.cta_m, self.cta_n) # CTA tile of C
# ---- WorkTileInfo (static 1-tile-per-CTA scheduler) ----
# blockIdx maps directly to (M_tile, N_tile, batch). K_idx_start/end
# carry over from C++ for future split-K; here every CTA reduces the
# full K range (= ceil(K/CTA_K) tiles, e.g. 12 for K=1536).
bidx, bidy, bidz = cute.arch.block_idx()
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
tidx, _, _ = cute.arch.thread_idx()
# In 2-CTA mode: identify which CTA within the (2,1,1) cluster owns
# the leader role (issues MMAs / multicasts commits). In 1-CTA mode
# every CTA is its own leader.
leader_rank = cutlass.Int32(0)
if cutlass.const_expr(self.use_2cta):
cta_rank_in_cluster = cute.arch.make_warp_uniform(
cute.arch.block_idx_in_cluster()
)
is_leader = cta_rank_in_cluster == 0
else:
cta_rank_in_cluster = leader_rank
is_leader = cutlass.Boolean(True)
# ---- SMEM A/B (DMA_Stage-staged, swizzled ComposedLayout) ----
# alignment=1024 covers TMA's natural alignment requirements. 2-CTA
# halves the M extent of sA and the N extent of sB internally (the SS
# atom splits A across cluster M and B across cluster M).
a_smem_layout_staged = sm100_utils.make_smem_layout_a(
tiled_mma,
mnk_tiler,
ab_dtype,
DMA_Stage,
) # ((Mma_M, Mma_K), NumMma_M, NumMma_K, DMA_Stage) — Sw<3,4,3>
b_smem_layout_staged = sm100_utils.make_smem_layout_b(
tiled_mma,
mnk_tiler,
ab_dtype,
DMA_Stage,
) # ((Mma_N, Mma_K), NumMma_N, NumMma_K, DMA_Stage) — Sw<3,4,3>
# sA per-CTA M extent = cta_m for both modes; sB per-CTA N extent =
# cta_n (1-CTA) or cta_n/2 (2-CTA, SS atom splits B across cluster M).
sA = cute_ext.allocate( # ((Mma_M_per_cta, Mma_K), NumMma_M, NumMma_K, DMA_Stage)
ab_dtype,
cute.AddressSpace.smem,
a_smem_layout_staged,
alignment=1024,
)
sB = cute_ext.allocate( # ((Mma_N_per_cta, Mma_K), NumMma_N, NumMma_K, DMA_Stage)
ab_dtype,
cute.AddressSpace.smem,
b_smem_layout_staged,
alignment=1024,
)
# ---- TMEM accumulator layout (manual alloc; see MMA warp) ----
# cute_ext.make_tmem_layout_acc accounts for cta_group when handed
# the cluster MMA tile, so the resulting layout is already per-CTA.
# acc_layout per-CTA: ((Mma_M_per_cta, Mma_N), NumMma_M, NumMma_N, AccStage)
# = (((16,4), cta_n), 1, 1, 1) for both 1-CTA and 2-CTA modes.
# We still allocate TMEM manually (via cute.arch.alloc_tmem below) for better performance.
acc_layout = cute_ext.make_tmem_layout_acc(
tiled_mma,
self.mma_tiler_mn,
acc_stage=1,
)
# ---- Raw mbarriers (Int64 SMEM arrays) + tmem_base_ptr Int32 slot ----
# Two flavors of barriers exist on Hopper/Blackwell:
# 1. Named barriers (bar.arv/bar.sync) — Ampere-era, 16 hardware
# barriers per SM, only handle intra-CTA thread sync.
# 2. Mbarriers (mbarrier.* PTX) — 64-bit in SMEM per barrier,
# software-programmable. Used here for (a) thread sync within a
# CTA, (b) TMA transaction-count tracking (TMA→SM signaling),
# and (c) 1-arrival "wake the consumer warp" patterns.
# Allocated as 1D tensors; .iterator gives a Pointer[Int64] supporting
# `bar + stage` arithmetic. Arrival counts are in the module docstring.
bar_full_arr = cute_ext.allocate(
cutlass.Int64,
cute.AddressSpace.smem,
cute.make_layout(DMA_Stage),
alignment=8,
)
bar_empty_arr = cute_ext.allocate(
cutlass.Int64,
cute.AddressSpace.smem,
cute.make_layout(DMA_Stage),
alignment=8,
)
bar_tma_epilog_arr = cute_ext.allocate(
cutlass.Int64, cute.AddressSpace.smem, cute.make_layout(1), alignment=8
)
bar_mma_epilog_arr = cute_ext.allocate(
cutlass.Int64, cute.AddressSpace.smem, cute.make_layout(1), alignment=8
)
bar_tmem_alloc_arr = cute_ext.allocate(
cutlass.Int64, cute.AddressSpace.smem, cute.make_layout(1), alignment=8
)
# alloc_tmem writes the TMEM base address to this slot; both MMA and
# EPILOG read it back via retrieve_tmem_ptr.
tmem_base_arr = cute_ext.allocate(
cutlass.Int32, cute.AddressSpace.smem, cute.make_layout(1), alignment=4
)
bar_full = bar_full_arr.iterator # Pointer[Int64], DMA_Stage
bar_empty = bar_empty_arr.iterator # Pointer[Int64], DMA_Stage
bar_tma_epilog = bar_tma_epilog_arr.iterator # Pointer[Int64]
bar_mma_epilog = bar_mma_epilog_arr.iterator # Pointer[Int64]
bar_tmem_alloc = bar_tmem_alloc_arr.iterator # Pointer[Int64]
tmem_base_ptr = tmem_base_arr.iterator # Pointer[Int32]
# ---- Barrier init (1 thread, PTX mbarrier.init is single-thread) ----
# bar_full arrival = 2 * num_mma_ctas:
# 1-CTA: 2 (own DMA_A + DMA_B)
# 2-CTA: 4 (own + peer DMA_A/DMA_B, routed via SM100_TMA_LOAD_2SM
# onto the leader's bar). In 2-CTA the peer's copy is unused.
if warp_idx == 0:
with cute.arch.elect_one():
for i in range(DMA_Stage):
cute.arch.mbarrier_init(bar_full + i, 2 * num_mma_ctas)
for i in range(DMA_Stage):
cute.arch.mbarrier_init(bar_empty + i, 1) # MMA tcgen05.commit
cute.arch.mbarrier_init(bar_tma_epilog, 32) # whole DMA_B warp
cute.arch.mbarrier_init(bar_mma_epilog, 1) # MMA tcgen05.commit
cute.arch.mbarrier_init(
bar_tmem_alloc, 32 + 128
) # MMA + 4 EPILOG warps
cute.arch.mbarrier_init_fence()
if cutlass.const_expr(self.use_2cta):
# Cluster-wide sync: ensure peer's barriers are visible before any
# cross-CTA arrives. Splitting arrive/wait lets the per-CTA tile
# setup below overlap with the cluster barrier. (Implicit
# intra-CTA sync — cluster.arrive requires all threads to reach.)
cute.arch.cluster_arrive_relaxed()
else:
cute.arch.barrier()
work_tile_info = WorkTileInfo(
M_idx=bidx,
N_idx=bidy,
L_idx=bidz,
K_idx_start=cutlass.Int32(0),
K_idx_end=cute.ceil_div(cute.size(mA, mode=[1]), self.cta_k),
)
k_tile_count = work_tile_info.K_idx_end - work_tile_info.K_idx_start
# ---- CTA-to-Value maps for TMA (constexpr) ----
# Encodes "which cluster CTA is responsible for which slice", i.e.
# the CTA-coord → logical-multicast-id layout. For our 1×1 cluster
# (no multicast) it's an identity layout; cute_ext.tma_load still
# needs this explicit layout to construct the TMA descriptor.
a_cta_v_map = cute_ext.get_cta_v_map_ab(mA, mnk_tiler, tiled_mma, "A")
b_cta_v_map = cute_ext.get_cta_v_map_ab(mB, mnk_tiler, tiled_mma, "B")
# ---- Per-CTA tile views ----
# local_tile(t, tiler, coord) = zipped_divide(t, tiler)[coord]; modes
# passed `None` stay free. Per-CTA M/N extents:
# gA M extent = cta_m (both modes; SS atom splits cluster M)
# gB N extent = cta_n (1-CTA) or cta_n//2 (2-CTA)
# gD M×N = cta_m × cta_n (cluster N is identical for both CTAs)
gA_tile = cute.local_tile( # (cta_m, cta_k, Tiles_K)
mA,
a_tiler_mk,
(work_tile_info.M_idx, None, work_tile_info.L_idx),
)
# gB n-index: 1-CTA just bidy; 2-CTA each cluster N_idx covers
# num_mma_ctas tiles of b_tiler, each CTA loads one half.
if cutlass.const_expr(self.use_2cta):
gB_n_idx = work_tile_info.N_idx * num_mma_ctas + cta_rank_in_cluster
else:
gB_n_idx = work_tile_info.N_idx
gB_tile = cute.local_tile( # (cta_n//num_mma_ctas, cta_k, Tiles_K)
mB,
b_tiler_nk,
(gB_n_idx, None, work_tile_info.L_idx),
)
gD_tile = cute.local_tile( # (cta_m, cta_n)
mC,
c_tiler_mn,
(work_tile_info.M_idx, work_tile_info.N_idx, work_tile_info.L_idx),
)
# gBias_tile: same (cta_m, cta_n) shape as gD_tile but stride (1, 0)
# — the same M element is repeated across N. local_tile preserves the
# (1, 0, 0) stride from mBias, so this works automatically.
gBias_tile = cute.local_tile( # (cta_m, cta_n) stride (1, 0)
mBias,
c_tiler_mn,
(work_tile_info.M_idx, work_tile_info.N_idx, work_tile_info.L_idx),
)
if cutlass.const_expr(self.use_2cta):
# Cluster sync deferred to here — tile-view setup above runs in
# parallel with the cluster barrier latency.
cute.arch.cluster_wait()
# ---- Warp dispatch (warp 3 idle, kept for 256-thread parity) ----
if warp_idx == 0:
self.dma_a_warp(
bar_full,
bar_empty,
leader_rank,
gA_tile,
sA,
a_cta_v_map,
k_tile_count,
)
elif warp_idx == 1:
self.dma_b_warp(
bar_full,
bar_empty,
bar_tma_epilog,
leader_rank,
gB_tile,
sB,
b_cta_v_map,
k_tile_count,
)
elif warp_idx == 2:
self.mma_warp(
is_leader,
bar_full,
bar_empty,
bar_mma_epilog,
bar_tmem_alloc,
tiled_mma,
sA,
sB,
tmem_base_ptr,
acc_layout,
mma_inst_tile_k,
k_tile_count,
)
elif warp_idx >= 4:
# Epilog tid is 128..255 in the CTA; offset to 0..127 for partition.
epi_tid = tidx - 128
self.epilog_warp(
bar_tma_epilog,
bar_mma_epilog,
bar_tmem_alloc,
tmem_base_ptr,
acc_layout,
gD_tile,
gBias_tile,
epi_tid,
c_dtype,
d_layout,
)
# ====================================================================
# DMA_A WARP — TMA-loads A tiles into sA[..., stage], one per K-iter.
# 1-CTA: SM90_TMA_LOAD into local sA, arrives on local bar_full.
# 2-CTA: SM100_TMA_LOAD_2SM (cluster-aware); peer's complete_tx routes
# onto the LEADER's bar_full. arrive_and_expect_tx is also
# peer-redirected to leader_rank.
# ====================================================================
@cute.experimental.jit
def dma_a_warp(
self,
bar_full, # Pointer[Int64], DMA_Stage entries
bar_empty, # Pointer[Int64], DMA_Stage entries
leader_rank: cutlass.Int32, # used for the 2-CTA arrive-peer redirect
gA_tile: cute.Tensor, # (CTA_M, CTA_K, Tiles_K) — this CTA's A strip
sA: cute.Tensor, # ((Mma_M, Mma_K), NumMma_M, NumMma_K, DMA_Stage)
a_cta_v_map: cute.Layout,
k_tile_count: cutlass.Int32,
):
DMA_Stage = self.num_ab_stage
# Phase-bit walkthrough for `bar_empty` (DMA_Stage=2 example).
# Init phase = 0; phase flips on each arrival-count round. We wait
# on the *old* phase about to flip away.
# k_tile stage old empty_phase → wait for flip
# 0 0 1 0→1 (init=0≠1, passes)
# 1 1 1 0→1
# 2 0 0 1→0 (slot reused)
# 3 1 0 1→0
# 4 0 1 0→1
# Flip empty_phase once per DMA_Stage iters.
empty_phase = cutlass.Int32(1)
pdl_count = self.pdl_count
for k_tile in cutlass.range(k_tile_count, unroll=1):
stage = k_tile % DMA_Stage
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
# cute_ext.tma_load only manages TX bytes, NEVER the arrival
# count (in the pipeline path producer_commit handles that).
# bar_full[stage] needs 2 (1-CTA) or 4 (2-CTA) arrivals; we
# arrive ourselves via the fused mbarrier.arrive.expect_tx PTX,
# paired with update_expect_tx=False so TX bytes aren't
# double-counted. peer_cta_rank_in_cluster=leader_rank in 2-CTA
# routes the arrive onto the leader's bar; None (1-CTA) lands
# on the local CTA's bar.
with cute.arch.elect_one():
cute.arch.mbarrier_arrive_and_expect_tx(
bar_full + stage,
# 1 stage of A SMEM = sizeof(bf16) × CTA_M × CTA_K
cute.size_in_bytes(
sA.element_type,
cute.slice_(sA.layout, (None, None, None, 0)),
),
peer_cta_rank_in_cluster=leader_rank if self.use_2cta else None,
)
cute_ext.tma_load(
gA_tile[None, None, k_tile], # (CTA_M, CTA_K) GMEM slice
sA[None, None, None, stage], # ((Mma_M,Mma_K),NumMma_M,NumMma_K)
(bar_full + stage).value, # Pointer→ir.Value bridge
cta_v_map=a_cta_v_map,
tma_operation_type=self.tma_op,
update_expect_tx=False,
)
if stage == (DMA_Stage - 1):
empty_phase = empty_phase ^ 1
# PDL: launch dependent grid at pdl_count (default -1 = end).
if cutlass.const_expr(self.use_pdl):
if k_tile == pdl_count:
cute.arch.griddepcontrol_launch_dependents()
if cutlass.const_expr(self.use_pdl):
cute.arch.griddepcontrol_launch_dependents()
# producer tail: drain the remaining bar_empty arrivals from MMA so the
# CTA stays alive until MMA's last tcgen05.commit lands — otherwise the
# arrive can hit freed SMEM (illegal memory access).
for k_tile in cutlass.range(DMA_Stage, unroll=1):
stage = (k_tile + k_tile_count) % DMA_Stage
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
if stage == (DMA_Stage - 1):
empty_phase = empty_phase ^ 1
# ====================================================================
# DMA_B WARP — same as DMA_A but for B; also drives the PDL
# griddepcontrol.wait and signals the epilog when all activation loads
# are issued.
# 2-CTA: SM100_TMA_LOAD_2SM + arrive redirected to leader_rank.
# ====================================================================
@cute.experimental.jit
def dma_b_warp(
self,
bar_full, # Pointer[Int64], DMA_Stage entries
bar_empty, # Pointer[Int64], DMA_Stage entries
bar_tma_epilog, # Pointer[Int64], 1 entry (32-arrival)
leader_rank: cutlass.Int32,
gB_tile: cute.Tensor, # (CTA_N, CTA_K, Tiles_K) — this CTA's B strip
sB: cute.Tensor, # ((Mma_N, Mma_K), NumMma_N, NumMma_K, DMA_Stage)
b_cta_v_map: cute.Layout,
k_tile_count: cutlass.Int32,
):
DMA_Stage = self.num_ab_stage
# PDL griddepcontrol.wait: B is the activation, produced by an
# upstream kernel. Block until that kernel signals completion. A
# is weights (already resident), so DMA_A doesn't need this.
if cutlass.const_expr(self.use_pdl):
cute.arch.griddepcontrol_wait()
# See dma_a_warp for empty_phase + fused arrive-and-expect-tx logic.
empty_phase = cutlass.Int32(1)
for k_tile in cutlass.range(k_tile_count, unroll=1):
stage = k_tile % DMA_Stage
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
with cute.arch.elect_one():
cute.arch.mbarrier_arrive_and_expect_tx(
bar_full + stage,
# 1 stage of B SMEM = sizeof(bf16) × CTA_N × CTA_K
cute.size_in_bytes(
sB.element_type,
cute.slice_(sB.layout, (None, None, None, 0)),
),
peer_cta_rank_in_cluster=leader_rank if self.use_2cta else None,
)
cute_ext.tma_load(
gB_tile[None, None, k_tile], # (CTA_N, CTA_K) GMEM slice
sB[None, None, None, stage], # ((Mma_N,Mma_K),NumMma_N,NumMma_K)
(bar_full + stage).value,
cta_v_map=b_cta_v_map,
tma_operation_type=self.tma_op,
update_expect_tx=False,
)
if stage == (DMA_Stage - 1):
empty_phase = empty_phase ^ 1
# 32-thread arrive on bar_tma_epilog: signals epilog "activations
# issued" so the bias-load can fire after all B tiles have landed.
# Only needed when has_bias=True; elided otherwise.
if cutlass.const_expr(self.has_bias):
cute.arch.mbarrier_arrive(bar_tma_epilog)
# producer tail: drain the remaining bar_empty arrivals from MMA so the
# CTA stays alive until MMA's last tcgen05.commit lands — otherwise the
# arrive can hit freed SMEM (illegal memory access).
for k_tile in cutlass.range(DMA_Stage, unroll=1):
stage = (k_tile + k_tile_count) % DMA_Stage
cute.arch.mbarrier_wait(bar_empty + stage, empty_phase)
if stage == (DMA_Stage - 1):
empty_phase = empty_phase ^ 1
# ====================================================================
# MMA WARP — owns the TMEM accumulator and issues every tcgen05.mma.
# 1-CTA: every CTA's MMA warp runs the K-loop on its own TMEM.
# 2-CTA: BOTH CTAs alloc/dealloc TMEM (cluster-coherent via
# is_two_cta=True); only the leader runs the K-loop; commits
# multicast to both CTAs (mask=0b11) so each one's bar_empty /
# bar_mma_epilog still gets signaled.
# ====================================================================
@cute.experimental.jit
def mma_warp(
self,
is_leader: cutlass.Boolean,
bar_full, # Pointer[Int64], DMA_Stage entries
bar_empty, # Pointer[Int64], DMA_Stage entries
bar_mma_epilog, # Pointer[Int64], 1 entry
bar_tmem_alloc, # Pointer[Int64], 1 entry, 160-arrival
tiled_mma: cute.TiledMma,
sA: cute.Tensor, # ((Mma_M, Mma_K), NumMma_M, NumMma_K, DMA_Stage)
sB: cute.Tensor, # ((Mma_N, Mma_K), NumMma_N, NumMma_K, DMA_Stage)
tmem_base_ptr, # Pointer[Int32] — SMEM slot for TMEM addr
acc_layout: cutlass.Constexpr, # TMEM accumulator layout
mma_inst_tile_k: cutlass.Constexpr, # NumMma_K — inner loop count
k_tile_count: cutlass.Int32, # Tiles_K — outer loop count
):
DMA_Stage = self.num_ab_stage
# ---- TMEM allocation (manual) ----
# We allocate TMEM explicitly here rather than via the convenience
# helper ``cute_ext.allocate(tmem)`` so that we can *hide the TMEM
# allocation latency*: the alloc must happen here in MMA's prolog
# (we need the TMEM ptr before the first tcgen05.mma), but it
# serializes against the next CTA's own alloc on the back-to-back
# PDL launch path. By manually emitting
# ``relinquish_tmem_alloc_permit`` right after our alloc completes,
# the next CTA can start ITS allocation in parallel with this
# CTA's MMA work rather than stalling at its own prolog waiting
# for us. cute_ext.allocate(tmem) does not expose this early-
# relinquish control — worth ~3% on the PDL hot path.
#
# TMEM on SM100 has 128 lanes × 512 columns × 4B = 256KB total. We
# allocate half (256 of 512 cols) so the next CTA can prefetch its
# alloc on the other half. For CTA_M=64 the accumulator only uses
# 64 lanes (16 lanes × 4 subpartitions), well within half-of-TMEM.
# 2-CTA: alloc/relinquish/dealloc are cluster-coherent (is_two_cta=True).
num_tmem_cols = 256
cute.arch.alloc_tmem(num_tmem_cols, tmem_base_ptr, is_two_cta=self.use_2cta)
cute.arch.mbarrier_arrive(bar_tmem_alloc) # phase 0: 32 of 160
cute.arch.relinquish_tmem_alloc_permit(is_two_cta=self.use_2cta)
# Bind acc_layout to the just-allocated TMEM ptr. Drop the trailing
# AccStage=1 mode so the MMA atom sees (MMA, Mma_M, Mma_N).
tmem_ptr = cute.arch.retrieve_tmem_ptr(self.acc_dtype, 16, tmem_base_ptr)
tAcc = cute.make_tensor(
tmem_ptr, acc_layout
) # ((Mma_M_per_cta, Mma_N), NumMma_M, NumMma_N, AccStage)
acc_view = tAcc[
None, None, None, 0
] # ((Mma_M_per_cta, Mma_N), NumMma_M, NumMma_N)
if is_leader:
# MMA atom: each cute_ext.dot call = 1 tcgen05.mma. ACCUMULATE:
# False → C = A*B (first inst of first k-tile only)
# True → C += A*B
mma_atom = cute.make_mma_atom(tiled_mma.op)
# 2-CTA: mask=0b11 multicasts the commit to both CTAs' bar_empty.
commit_mask = 0b11 if self.use_2cta else None
full_phase = cutlass.Int32(0)
for k_tile in cutlass.range(k_tile_count, unroll=1):
stage = k_tile % DMA_Stage
cute.arch.mbarrier_wait(bar_full + stage, full_phase)
# Inner-K loop: NumMma_K straight-line tcgen05.mma instructions.
# cute_ext.dot expects rank-3 operands in the MMA fragment
# profile (MMA_atom, REST_M, REST_K). Slicing
# `sA[None,None,k_block,stage]` leaves rank 2
# ((Mma_M, Mma_K), NumMma_M), so pad explicitly via
# cute.append_ones.
for k_block in range(mma_inst_tile_k):
if k_block == 0:
# First inst of the very first k_tile clears TMEM.
mma_atom.set(tcgen05.Field.ACCUMULATE, k_tile != 0)
else:
mma_atom.set(tcgen05.Field.ACCUMULATE, True)
a_frag = cute.append_ones(
sA[None, None, k_block, stage],
up_to_rank=3,
)
b_frag = cute.append_ones(
sB[None, None, k_block, stage],
up_to_rank=3,
)
cute_ext.dot(mma_atom, a_frag, b_frag, acc_view)
# CRITICAL: tcgen05.commit MUST be inside elect_one(). Unlike
# the C++ ``cutlass::arch::umma_arrive`` helper (which has an
# internal elect_one_sync), the DSL's tcgen05.commit has no
# guard — without this, all 32 lanes commit (32× redundant
# arrivals on bar_empty). This was a major perf bug encountered
# during development.
with cute.arch.elect_one():
tcgen05.commit(bar_empty + stage, commit_mask, self.cta_group)
if (k_tile % DMA_Stage) == (DMA_Stage - 1):
full_phase = full_phase ^ 1
# Wake the epilog (1-thread arrive on the 1-arrival barrier).
with cute.arch.elect_one():
tcgen05.commit(bar_mma_epilog, commit_mask, self.cta_group)
# ---- TMEM dealloc: wait for own EPILOG's tcgen05.ld to retire, free.
# bar_tmem_alloc phase 1 fires after EPILOG's tcgen05.ld is observable
# (post fence_view_async_tmem_load). 2-CTA dealloc is per-CTA (no
# cross-CTA handshake) because each CTA owns its own physical TMEM
# half; the cluster-shared accumulator is just a logical view.
cute.arch.mbarrier_arrive(bar_tmem_alloc) # phase 1: 32 of 160
cute.arch.mbarrier_wait(bar_tmem_alloc, 1)
cute.arch.dealloc_tmem(tmem_ptr, num_tmem_cols, is_two_cta=self.use_2cta)
# ====================================================================
# EPILOG WARPS — TMEM → RMEM → bf16 cast → direct st.global to GMEM.
# ====================================================================
@cute.experimental.jit
def epilog_warp(
self,
bar_tma_epilog, # Pointer[Int64], 1 entry (32-arrival)
bar_mma_epilog, # Pointer[Int64], 1 entry
bar_tmem_alloc, # Pointer[Int64], 1 entry, 160-arrival
tmem_base_ptr, # Pointer[Int32] — SMEM slot from MMA
acc_layout: cutlass.Constexpr,
gD_tile: cute.Tensor, # (CTA_M, CTA_N) — this CTA's output tile
gBias_tile: cute.Tensor, # (CTA_M, CTA_N) with stride (1,0) — bias broadcast
epi_tid: cutlass.Int32, # 0..127 within the 4 EPILOG warps
c_dtype: cutlass.Constexpr,
d_layout: cutlass.Constexpr,
):
# Sync with MMA's alloc_tmem (phase 0 of bar_tmem_alloc): 128 arrivals
# from this warp + 32 from MMA = 160. tmem_base_ptr is uninitialized
# before the wait clears.
cute.arch.mbarrier_arrive(bar_tmem_alloc)
cute.arch.mbarrier_wait(bar_tmem_alloc, 0)
# Bind acc_layout to the TMEM ptr written by alloc_tmem.
# acc_layout shape: ((Mma_M, Mma_N), NumMma_M, NumMma_N, AccStage)
# = (((16,4), Mma_N), 1, 1, 1).
# acc_view drops the 1×1×1 outer modes → (Mma_M, Mma_N).
#
# Concrete TMEM layout for 1-CTA bf16 M64 N8:
# tmem_[32b](0x0...) o (((16,4),8),1,1,1):(((65536,2097152),1),0,0,0)
# TMEM addr = [31:16=dp_lane, 15:0=column]:
# stride between dp lanes = 1<<16 = 65536 (Mma_M_per_subp)
# stride between subparts = 65536 × 32 = 2097152 (NumSubp=4)
# stride between cols = 1 (N is contiguous)
tmem_ptr = cute.arch.retrieve_tmem_ptr(self.acc_dtype, 16, tmem_base_ptr)
tCtAcc = cute.make_tensor(
tmem_ptr, acc_layout
) # ((Mma_M_per_cta, Mma_N), NumMma_M, NumMma_N, AccStage)
acc_view = tCtAcc[((None, None), 0, 0, 0)] # (Mma_M_per_cta, Mma_N)
# ---- t2r tiled-copy + per-thread RMEM layout ----
# ``sm100_utils.get_tmem_load_op`` picks the best tcgen05.ld atom
# for the (M, N, K) tile and output dtype. For CTA_M=64, M/N-major
# output, the chosen atom is SM100_TMEM_LOAD_16dp256b1x — the 16dp
# variant is required because each MMA subpartition only uses 16
# of the 32 lanes per subpartition (CTA_M=64 split across 4 subps =
# 16 lanes each). Other tcgen05.ld atoms would also be functional;
# 16dp is just optimal here.
# epi_tile = full CTA tile: N=8 fits in one tcgen05.ld instance
# (16dp × 8col → 4 regs per thread × 128 threads). Larger N would
# need a sub-tile loop here.
epi_tile = (self.cta_m, self.cta_n)
copy_atom_t2r = sm100_utils.get_tmem_load_op(
(self.cta_m, self.cta_n, self.cta_k),
d_layout,
c_dtype,
self.acc_dtype,
epi_tile,
self.use_2cta,
)
tiled_copy_t2r = cute.nvgpu.tcgen05.make_tmem_copy(copy_atom_t2r, acc_view)
# flat_divide → (CTA_M, CTA_N, Rest_M=1, Rest_N=1); the rank-4 shape
# is required because make_t2r_rmem_layout internally does [...,0,0].
gD_epi = cute.flat_divide(gD_tile, epi_tile)
# rmem_layout matches one t2r copy's destination. The TMEM-side
# partition (CpyS) per-subpartition view is shape ((CpyS_N=8,
# CpyS_M=16), NumCpy_M=1, NumCpy_N=Mma_N/CpyS_N) — i.e. each
# tcgen05.ld.16dp256bit.x1 instance moves 16dp×8col elements per
# subpartition. Per the PTX page for tcgen05.ld.16dp256bit, thread
# 0 of an SM100_TMEM_LOAD_16dp256b1x copy receives 4 registers at
# output coordinates (0,0), (0,1), (8,0), (8,1) — i.e. a (2,2)
# per-thread value-tile, hence CpyD = (2,2). The dst rmem layout
# mirrors this CpyD shape but with stride (1, 2) so the registers
# are stored contiguously.
rmem_layout = cute_ext.make_t2r_rmem_layout(tiled_copy_t2r, gD_epi, epi_tid)
rAcc = cute_ext.allocate( # fp32, per-thread
self.acc_dtype,
cute.AddressSpace.rmem,
rmem_layout,
alignment=32,
)
rD = cute_ext.allocate( # bf16, per-thread
c_dtype,
cute.AddressSpace.rmem,
rmem_layout,
alignment=32,
)
thr_t2r = tiled_copy_t2r.get_slice(epi_tid)
# ---- Bias setup (GMEM→RMEM via partition_and_copy, predicate auto) ----
# gBias_tile has the same (CTA_M, CTA_N) shape as gD_tile but with
# stride (1, 0) — the bias value depends only on the M coord. Each
# lane needs to read the same (m, n) coord it writes back to gD so
# bias and accumulator align register-for-register.
#
# partition_and_copy slices the GMEM src using
# `tiled_copy.layout_src_tv_tiled`. With t2r that's the TMEM (CpyS)
# side, which is 16dp×8col=128 elements/group — doesn't match rBias's
# CpyD=4 elements/thread. `make_tiled_copy_D` builds a new TiledCopy
# whose src/dst TV layouts both equal t2r's *dst* (CpyD) layout, so
# the GMEM src gets sliced to CpyD per thread — matching rBias. OOB
# is handled by the cute_ext lowering's auto-predBounds (same story
# as the st.global to mC).
if cutlass.const_expr(self.has_bias):
bias_dtype = gBias_tile.element_type
gBias_epi = cute.flat_divide(gBias_tile, epi_tile)
tiled_copy_g2r = cute.make_tiled_copy_D(
cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), bias_dtype),
tiled_copy_t2r,
)
thr_g2r = tiled_copy_g2r.get_slice(epi_tid)
rBias = cute_ext.allocate( # bias dtype (e.g. bf16)
bias_dtype,
cute.AddressSpace.rmem,
rmem_layout,
alignment=32,
)
rBiasAcc = cute_ext.allocate( # converted to fp32
self.acc_dtype,
cute.AddressSpace.rmem,
rmem_layout,
alignment=32,
)
# ---- Wait for data: B loads done + MMA done ----
# Only needed when has_bias=True (DMA_B signals bar_tma_epilog, epilog
# waits before issuing the bias load so it hides behind MMA). Without
# bias the bar is unused — neither arrive nor wait fires.
if cutlass.const_expr(self.has_bias):
cute.arch.mbarrier_wait(bar_tma_epilog, 0) # all activations issued
# ---- Bias load: GMEM → RMEM (predicated ld.global) and convert to fp32 ----
# Done before the MMA wait so the load latency hides behind MMA.
if cutlass.const_expr(self.has_bias):
cute_ext.partition_and_copy(thr_g2r, gBias_epi[None, None, 0, 0], rBias)
rBiasAcc.store(rBias.load().to(self.acc_dtype))
cute.arch.mbarrier_wait(bar_mma_epilog, 0) # accumulator ready
# TMEM → RMEM (one tcgen05.ld for the 64×8 tile).
cute_ext.partition_and_copy(thr_t2r, acc_view, rAcc)
# tcgen05.ld is async — fence makes the result visible to (a) the
# rAcc.load() below and (b) MMA's dealloc_tmem after we arrive
# on bar_tmem_alloc phase 1.
cute.arch.fence_view_async_tmem_load()
# Phase 1 of bar_tmem_alloc: 128 from this warp + 32 from MMA = 160.
# MMA's mbarrier_wait(bar_tmem_alloc, 1) clears and it dealloc's.
cute.arch.mbarrier_arrive(bar_tmem_alloc)
# Add bias in fp32 before the dtype cast.
if cutlass.const_expr(self.has_bias):
rAcc.store(rAcc.load() + rBiasAcc.load())
# fp32 → bf16 cast in RMEM.
rD.store(rAcc.load().to(c_dtype))
# RMEM → GMEM: cute_ext.partition_and_copy does two things —
# (a) PARTITION: splits non-RMEM operands by thr_copy's thread-value
# layout (RMEM operands skipped — already per-thread). Here only
# gD_epi (the dst) gets partitioned; rD stays as-is.
# (b) INSTRUCTION: picked from (src, dst) memspace pair —
# TMEM→RMEM = tcgen05.ld (atom-driven)
# RMEM→GMEM = st.global (simt_auto_vec_copy)
# GMEM→SMEM = cp.async (async_op=True)
# We REUSE thr_t2r so each lane writes to the same (m, n) GMEM coords
# where it read from TMEM. A different copy atom would scatter
# registers to the wrong addresses.
#
# OOB STORES are handled automatically by the cute_ext lowering —
# simt_auto_vec_copy infers predBounds from the destination tensor's
# MemRef shape (propagated from mC through local_tile / flat_divide).
# Lanes whose (m, n) coord is past mC's shape simply don't issue an
# st.global. Verified with a non-aligned M=63 test: writes to in-bounds rows
# 0..62 land correctly, the M=63 row + the rest of the (64,8) tile
# stay untouched. So the C++ / vanilla-DSL kernels' explicit predicate
# dance (make_identity_tensor + elem_less + basic_copy_if) isn't
# needed; the explicit `predicated_tensor_origin` marker isn't either.
cute_ext.partition_and_copy(thr_t2r, rD, gD_epi[None, None, 0, 0])
# =====================================================================
# BMM wrappers — feed (L, M, K) / (L, K, N) / (L, M, N) PyTorch-shaped
# cute tensors and select-permute them into the kernel's (M, K, L) /
# (N, K, L) / (M, N, L) view.
# =====================================================================
@cute.experimental.jit
def _bmm_no_bias(
gemm_op: cutlass.Constexpr,
a: cute.Tensor, # (L, M, K) from PyTorch
b: cute.Tensor, # (L, K, N) from PyTorch (permuted)
c: cute.Tensor, # (L, M, N) from PyTorch (permuted)
stream: cuda.CUstream,
):
a = cute.make_tensor(a.iterator, cute.select(a.layout, mode=[1, 2, 0]))
b = cute.make_tensor(b.iterator, cute.select(b.layout, mode=[2, 1, 0]))
c = cute.make_tensor(c.iterator, cute.select(c.layout, mode=[1, 2, 0]))
# Dummy bias aliasing C — never dereferenced when has_bias=False.
bias = cute.make_tensor(c.iterator, cute.select(c.layout, mode=[0, 1, 2]))
gemm_op(a, b, c, bias, stream)
@cute.experimental.jit
def _bmm_bias(
gemm_op: cutlass.Constexpr,
a: cute.Tensor, # (L, M, K) from PyTorch
b: cute.Tensor, # (L, K, N) from PyTorch (permuted)
c: cute.Tensor, # (L, M, N) from PyTorch (permuted)
bias: cute.Tensor, # (L, M, N):(0,1,0) — M-broadcast via as_strided
stream: cuda.CUstream,
):
a = cute.make_tensor(a.iterator, cute.select(a.layout, mode=[1, 2, 0]))
b = cute.make_tensor(b.iterator, cute.select(b.layout, mode=[2, 1, 0]))
c = cute.make_tensor(c.iterator, cute.select(c.layout, mode=[1, 2, 0]))
# After [1,2,0] permute: (M, N, L):(1, 0, 0) — matches C++ make_layout_Bias.
bias = cute.make_tensor(bias.iterator, cute.select(bias.layout, mode=[1, 2, 0]))
gemm_op(a, b, c, bias, stream)
# =====================================================================
# Compile + cache helpers (FlashInfer-side wrappers around the kernel).
# Caches by *config* only — the kernel uses ``mark_layout_dynamic`` so
# the same compiled binary handles any shape. Representative tensors used
# at compile time pin the dtype, rank, and leading-dim pattern; their
# concrete extents are erased.
# =====================================================================
_TORCH_TO_CUTLASS_DTYPE = {
torch.bfloat16: cutlass.BFloat16,
}
# Per-process cache mapping (dtype, config…) → compiled cute_ext callable.
# We need to construct concrete cute.Tensors once and reuse the resulting
# compiled function across all live calls; a fresh build per call would
# defeat the cache.
_TGV_CUTE_EXT_COMPILE_CACHE: dict = {}
def _detect_leading_dim(t: torch.Tensor) -> int:
"""Return the index of the stride-1 dim. Used by ``_to_cute_swap`` so the
integration layer auto-adapts to whichever of K-major / MN-major the
caller hands us. Falls back to the innermost dim if no stride-1 dim
exists (shouldn't happen for well-formed tensors)."""
for i, s in enumerate(t.stride()):
if s == 1:
return i
return t.dim() - 1
def _make_layout_tensor(
shape: Tuple[int, ...], dtype: torch.dtype, leading_dim: int
) -> torch.Tensor:
"""Allocate a torch tensor of ``shape`` where ``leading_dim`` is the
contiguous (stride-1) dim. Used to synthesize representative tensors at
compile time for arbitrary layout patterns."""
ndim = len(shape)
# Permute leading_dim to last, allocate contig, then permute back.
perm = [i for i in range(ndim) if i != leading_dim] + [leading_dim]
permuted_shape = tuple(shape[p] for p in perm)
t = torch.empty(permuted_shape, dtype=dtype, device="cuda")
inv_perm = [perm.index(i) for i in range(ndim)]
return t.permute(inv_perm)
def _make_compile_repr_tensors(
dtype: torch.dtype, has_bias: bool, a_leading: int, b_leading: int, c_leading: int
):
"""Build representative tensors with strides matching the requested
leading-dim pattern. After the A↔B swap, the cute_ext kernel sees:
A_ce: shape (L, N_pt, K) <- (L=1, dim 1, dim 2)
B_ce: shape (L, K, M_pt) <- (L=1, dim 1, dim 2)
C_ce: shape (L, N_pt, M_pt) <- (L=1, dim 1, dim 2)
Each of {a, b, c}_leading ∈ {1, 2} selects which dim is the stride-1
one (K vs kernel-M for A, K vs kernel-N for B, kernel-M vs kernel-N
for C). The compiled binary is specialized per ``(a, b, c)_leading``
tuple.
"""
# Pick aligned-but-small extents that satisfy the kernel's tile shape
# while leaving room for either dim to be the contig one.
M, N, K, L = 64, 8, 128, 1 # M_pt=M, N_pt=N
A_t = _make_layout_tensor(
(L, N, K), dtype, a_leading
) # kernel A shape (L, N_pt, K)
B_t = _make_layout_tensor(
(L, K, M), dtype, b_leading
) # kernel B shape (L, K, M_pt)
C_t = _make_layout_tensor(
(L, N, M), dtype, c_leading
) # kernel C shape (L, N_pt, M_pt)
a_ = from_dlpack(A_t, assumed_align=32).mark_layout_dynamic(leading_dim=a_leading)
b_ = from_dlpack(B_t, assumed_align=32).mark_layout_dynamic(leading_dim=b_leading)
c_ = from_dlpack(C_t, assumed_align=32).mark_layout_dynamic(leading_dim=c_leading)
if not has_bias:
return a_, b_, c_, None
# bias is per-output-feature in PyTorch terms; after the A↔B swap that
# maps to broadcast over the kernel's N axis and L (stride (0, 1, 0)).
bias_t = torch.empty((N,), dtype=dtype, device="cuda")
bias_3d = bias_t.as_strided(size=(L, N, M), stride=(0, 1, 0))
bias_ = from_dlpack(bias_3d, assumed_align=2).mark_layout_dynamic(leading_dim=1)
return a_, b_, c_, bias_
def _get_compiled_cute_ext_kernel(
dtype: torch.dtype,
cta_m: int,
cta_n: int,
cta_k: int,
num_ab_stage: int,
use_2cta: bool,
use_pdl: bool,
has_bias: bool,
a_leading: int,
b_leading: int,
c_leading: int,
):
"""Compile (or fetch from cache) a TgvGemmCuteExtKernel for the given
config + layout pattern. Returns a callable with signature
compiled(a_, b_, c_, [bias_,] stream)
accepting cute.Tensors. The same compiled binary handles any
(M, N, K, L) consistent with the (a, b, c)_leading layout pattern
thanks to ``mark_layout_dynamic``.
"""
key = (
dtype,
cta_m,
cta_n,
cta_k,
num_ab_stage,
bool(use_2cta),
bool(use_pdl),
bool(has_bias),
a_leading,
b_leading,
c_leading,
)
cached = _TGV_CUTE_EXT_COMPILE_CACHE.get(key)
if cached is not None:
return cached
if dtype not in _TORCH_TO_CUTLASS_DTYPE:
raise ValueError(
f"TGV cute_ext backend supports {list(_TORCH_TO_CUTLASS_DTYPE)}; got {dtype}."
)
gemm = TgvGemmCuteExtKernel(
acc_dtype=cutlass.Float32,
cta_m=cta_m,
cta_n=cta_n,
cta_k=cta_k,
num_ab_stage=num_ab_stage,
use_2cta=use_2cta,
use_pdl=use_pdl,
has_bias=has_bias,
)
a_, b_, c_, bias_ = _make_compile_repr_tensors(
dtype,
has_bias,
a_leading,
b_leading,
c_leading,
)
fake_stream = make_fake_stream()
if has_bias:
compiled = cute_ext.compile(_bmm_bias, gemm, a_, b_, c_, bias_, fake_stream)
else:
compiled = cute_ext.compile(_bmm_no_bias, gemm, a_, b_, c_, fake_stream)
_TGV_CUTE_EXT_COMPILE_CACHE[key] = compiled
return compiled
# =====================================================================
# Tensor adaptation: PyTorch (M, N) -> cute view via the A↔B swap. Now
# auto-detects each tensor's stride-1 dim so callers can pass either
# K-major or MN-major a/b (and either M-contig or N-contig out).
# =====================================================================
def _to_cute_swap(
a_pt: torch.Tensor, # (..., M, K)
b_pt: torch.Tensor, # (..., K, N)
out_pt: torch.Tensor, # (..., M, N)
bias_pt: Optional[torch.Tensor], # (N,) or None — bias is per-output-feature
):
"""Build the cute.Tensors fed to ``_bmm_no_bias`` / ``_bmm_bias`` AND
report the detected (a_leading, b_leading, c_leading) tuple so the
caller can fetch the matching compiled binary.
Internally swaps A↔B so the kernel writes its (kernel-M)-contiguous
output into the N-axis of a row-major PyTorch tensor — same trick as
the C++ runner's ``gemm_fn(b.t(), a.t(), …)``. After swap, the
kernel's M = PyTorch N and the kernel's N = PyTorch M; bias broadcast
over the kernel's M axis (= PyTorch N) correctly mirrors the
per-output-feature semantics.
The kernel itself (`make_trivial_tiled_mma` + `mma_major_mode()`)
adapts to whichever axis is the stride-1 one in each operand, so we
just need to make sure ``mark_layout_dynamic`` pins the right dim.
"""
# Lift to 3D batched shape with a leading L=1 axis when called from mm.
if a_pt.dim() == 2:
a_pt = a_pt.unsqueeze(0)
b_pt = b_pt.unsqueeze(0)
out_pt = out_pt.unsqueeze(0)
# Swap: feed b.transpose(-2,-1) as A_ce (shape (L, N_pt, K)),
# a.transpose(-2,-1) as B_ce (shape (L, K, M_pt)), and
# out.transpose(-2,-1) as C_ce (shape (L, N_pt, M_pt)).
a_swap = b_pt.transpose(-2, -1)
b_swap = a_pt.transpose(-2, -1)
c_swap = out_pt.transpose(-2, -1)
a_leading = _detect_leading_dim(a_swap)
b_leading = _detect_leading_dim(b_swap)
c_leading = _detect_leading_dim(c_swap)
a_ = from_dlpack(a_swap, assumed_align=32).mark_layout_dynamic(
leading_dim=a_leading
)
b_ = from_dlpack(b_swap, assumed_align=32).mark_layout_dynamic(
leading_dim=b_leading
)
c_ = from_dlpack(c_swap, assumed_align=32).mark_layout_dynamic(
leading_dim=c_leading
)
layout = (a_leading, b_leading, c_leading)
if bias_pt is None:
return a_, b_, c_, None, layout
# bias is per-output-feature in PyTorch terms. After the A↔B swap the
# kernel's M-axis maps to PyTorch's N-axis, which is exactly what the
# bias indexes — so this is a direct (0, 1, 0) broadcast.
L = c_swap.shape[0]
M_ce = c_swap.shape[1] # == PyTorch N
N_ce = c_swap.shape[2] # == PyTorch M
bias_3d = bias_pt.as_strided(size=(L, M_ce, N_ce), stride=(0, 1, 0))
bias_ = from_dlpack(bias_3d, assumed_align=2).mark_layout_dynamic(leading_dim=1)
return a_, b_, c_, bias_, layout
def _resolve_tactic(tactic: int) -> Tuple[int, int, int, bool]:
"""Return (cta_m, cta_n, num_ab_stage, use_2cta) for the given tactic id."""
if tactic < 0:
tactic = _TGV_CUTE_EXT_DEFAULT_TACTIC
if tactic >= len(_TGV_CUTE_EXT_TACTIC_CONFIGS):
raise ValueError(
f"TGV cute_ext tactic {tactic} out of range [0, {len(_TGV_CUTE_EXT_TACTIC_CONFIGS)})."
)
return _TGV_CUTE_EXT_TACTIC_CONFIGS[tactic]
def _run_tgv(
a: torch.Tensor,
b: torch.Tensor,
bias: Optional[torch.Tensor],
out: torch.Tensor,
pdl: bool,
tactic: int,
) -> torch.Tensor:
"""Dispatch one TGV GEMM: a (M, K) @ b (K, N) -> out (M, N), bias (N,)."""
cta_m, cta_n, num_ab_stage, use_2cta = _resolve_tactic(tactic)
has_bias = bias is not None
a_, b_, c_, bias_, (a_leading, b_leading, c_leading) = _to_cute_swap(
a,
b,
out,
bias,
)
compiled = _get_compiled_cute_ext_kernel(
dtype=a.dtype,
cta_m=cta_m,
cta_n=cta_n,
cta_k=_TGV_CUTE_EXT_CTA_K,
num_ab_stage=num_ab_stage,
use_2cta=use_2cta,
use_pdl=bool(pdl),
has_bias=has_bias,
a_leading=a_leading,
b_leading=b_leading,
c_leading=c_leading,
)
stream = cuda.CUstream(torch.cuda.current_stream(a.device).cuda_stream)
if has_bias:
compiled(a_, b_, c_, bias_, stream)
else:
compiled(a_, b_, c_, stream)
return out
_TGV_NUM_SMS = 148
# (tactic, cta_m, cta_n, num_mma_ctas), smallest cta_n first.
_TGV_TACTIC_LADDER = (
(18, 64, 16, 2),
(21, 64, 32, 2),
(23, 64, 64, 2),
(24, 64, 128, 2),
(12, 128, 16, 1),
(26, 128, 32, 2),
(27, 128, 64, 2),
)
def _grid_ctas(m: int, n: int, cta_m: int, cta_n: int, num_mma_ctas: int) -> int:
# Kernel axes are swapped: kernel-M = pytorch N, kernel-N = pytorch M.
cluster_m = cta_m * num_mma_ctas
return num_mma_ctas * -(n // -cluster_m) * -(m // -cta_n)
def _pick_tactic(m: int, n: int, k: int) -> int:
"""Pick the first ladder tactic whose grid fits one wave of the SMs."""
best, best_ctas = None, None
for tactic, cta_m, cta_n, num_mma_ctas in _TGV_TACTIC_LADDER:
ctas = _grid_ctas(m, n, cta_m, cta_n, num_mma_ctas)
if ctas <= _TGV_NUM_SMS:
return tactic
if best_ctas is None or ctas < best_ctas:
best, best_ctas = tactic, ctas
return best
def use_cutedsl_bf16_gemm(m: int, n: int, k: int) -> bool:
"""TGV-vs-cuBLAS (``F.linear``) decision, CUPTI-measured on B300 under CUDA
graph capture (cold L2). Conservative: ties and unmeasured regions fall
back to cuBLAS."""
if k % 8 != 0: # TMA requires 16B-aligned rows
return False
if n < 1024 or k < 2048 or k > 6144:
return False
ragged = m % 16 != 0
if n <= 1024:
return m <= 512 and (m >= 48 or m % 16 >= 9)
if n <= 2624:
return m <= 128 and k >= 4096
if n <= 4096:
return m <= 64
if k < 4096:
return k >= 3072 and 25 <= m <= 48 and ragged
if n <= 6144:
if m <= 64:
return m <= 32 or k >= 6144 or ragged
return m <= 72 and k >= 6144 and ragged
if n <= 8192:
return m <= 48 or (m <= 63 and ragged)
return n <= 12288 and k >= 6144 and (m <= 32 or (m <= 48 and ragged))
def _tgv_bf16_gemm_run(
x: torch.Tensor,
weight: torch.Tensor,
bias: Optional[torch.Tensor],
) -> torch.Tensor:
if get_device_sm() not in (100, 103):
raise RuntimeError("cutedsl_bf16_gemm requires SM100/SM103 (Blackwell)")
assert x.dtype == torch.bfloat16 and weight.dtype == torch.bfloat16
assert x.stride(-1) == 1, "x must be K-major [M, K]"
assert weight.stride(-1) == 1, "weight must be K-major [N, K]"
out = torch.empty(
(x.shape[0], weight.shape[0]), dtype=torch.bfloat16, device=x.device
)
return _run_tgv(
x,
weight.t(),
bias,
out,
pdl=True,
tactic=_pick_tactic(x.shape[0], weight.shape[0], weight.shape[1]),
)
def _tgv_bf16_gemm_fake(
x: torch.Tensor,
weight: torch.Tensor,
bias: Optional[torch.Tensor],
) -> torch.Tensor:
return x.new_empty((x.shape[0], weight.shape[0]))
direct_register_custom_op(
op_name="cutedsl_tgv_bf16_gemm",
op_func=_tgv_bf16_gemm_run,
mutates_args=[],
fake_impl=_tgv_bf16_gemm_fake,
)
@debug_kernel_api
def cutedsl_bf16_gemm(
x: torch.Tensor,
weight: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
"""out[M, N] = x[M, K] @ weight[N, K].T (+ bias[N]), all bf16, fp32 accum."""
return torch.ops.sglang.cutedsl_tgv_bf16_gemm(x, weight, bias)