Files
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 lines
58 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)