Files
apache--tvm/python/tvm/backend/cuda/operator/intrinsics/cp_async.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

925 lines
36 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=redefined-builtin, invalid-name, too-many-arguments, too-many-locals, too-many-positional-arguments
"""PTX cp.async / cp.async.bulk / cp.async.bulk.tensor intrinsics.
Each PTX form table entry is registered as one ``device_intrinsic``.
User-facing wrappers in ``tvm.tirx.op`` keep their v1 signatures;
``register_codegen`` dispatchers below decode the (cp_size, fill_mode,
predicate) / (dim, cta_mask, tile_mode) arguments to pick the right form.
Bodies are hand-written ``asm volatile(...)`` strings. The file is grouped
as cp.async, cp.async.bulk.tensor, cp.async.bulk non-TMA, and CUDA
compatibility helpers.
"""
import tvm
from tvm.backend.cuda.op import cuda_func_call
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .utils import parse_str
_PREFETCH_CHOICES = ("", "64", "128", "256")
_DIM_CHOICES = (1, 2, 3, 4, 5)
_TILE_MODE_CHOICES = ("tile", "tile_gather4")
def _safe(s):
return s.replace("::", "_").replace(".", "_")
# =============================================================================
# cp.async forms from the PTX Syntax block.
#
# Includes commit/wait plus the non-bulk shared/global copy forms.
# =============================================================================
device_intrinsic(
"ptx_cp_async_commit_group",
helper_name="tvm_builtin_ptx_cp_async_commit_group",
body=' asm volatile("cp.async.commit_group;");',
)
device_intrinsic(
"ptx_cp_async_wait_group",
n_attrs=1,
helper_name=lambda n: f"tvm_builtin_ptx_cp_async_wait_group_{int(n)}",
body=lambda n: f' asm volatile("cp.async.wait_group {int(n)};");',
)
# cp.async non-bulk copy forms:
# Form 1: cp.async.ca.shared.global ... [dst], [src], cp-size{, src-size}{, cache-policy}
# Form 2: cp.async.cg.shared.global ... [dst], [src], 16{, src-size}{, cache-policy}
# Form 3: cp.async.ca.shared.global ... [dst], [src], cp-size{, ignore-src}{, cache-policy}
# Form 4: cp.async.cg.shared.global ... [dst], [src], 16{, ignore-src}{, cache-policy}
def _cp_async_modifier_str(has_cache_hint, prefetch_size):
s = ""
if has_cache_hint:
s += ".L2::cache_hint"
if prefetch_size:
s += f".L2::{prefetch_size}B"
return s
def _make_form_parts(ca_or_cg, fixed_cp_size, extra):
"""Build a parts callable for one of the cp.async PTX forms.
Args layout: (dst, src [, extra_int], cache_policy, has_cache, prefetch_size [, cp_size_attr])
Forwarded operands: dst, src [, extra_int], cache_policy.
Trailing attrs: has_cache, prefetch_size [, cp_size if .ca].
"""
n_op = 3 if extra is not None else 2
n_attrs = 2 if fixed_cp_size is not None else 3
extra_in_name = f"_with_{extra}" if extra is not None else ""
def _parts(*args):
# Operand args (forwarded) come first, then attr args.
attr_args = args[-n_attrs:]
has_cache = _bool_attr(attr_args[0])
prefetch_size = parse_str(attr_args[1])
cp_size = fixed_cp_size if fixed_cp_size is not None else int(attr_args[2])
modifier = _cp_async_modifier_str(has_cache, prefetch_size)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
# name parts
name_cache = "_cache_hint" if has_cache else ""
name_prefetch = f"_prefetch_{prefetch_size}" if prefetch_size else ""
name = (
f"tvm_builtin_ptx_cp_async_{ca_or_cg}_{cp_size}"
f"{name_cache}{name_prefetch}{extra_in_name}"
)
sig = (
"(void* dst, void* src"
+ (f", int {extra}" if extra else "")
+ ", unsigned long long cache_policy)"
)
instr_base = f"cp.async.{ca_or_cg}.shared.global{modifier}"
if extra is None:
cache_arg = ", %2" if has_cache else ""
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
f' asm volatile("{instr_base} [%0], [%1], {cp_size}{cache_arg};\\n"\n'
f' :: "r"(dst_addr), "l"(src){cache_operand} : "memory");'
)
else:
cache_arg = ", %3" if has_cache else ""
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
f' asm volatile("{instr_base} [%0], [%1], {cp_size}, %2{cache_arg};\\n"\n'
f' :: "r"(dst_addr), "l"(src), "r"({extra})'
f'{cache_operand} : "memory");'
)
return name, sig, body
return _parts, n_op + n_attrs - n_op # n_attrs
def _register_nb_form(op_name, ca_or_cg, fixed_cp_size, extra):
parts_fn, n_attrs = _make_form_parts(ca_or_cg, fixed_cp_size, extra)
n_op = 3 if extra is not None else 2
sig_static = (
"(void* dst, void* src"
+ (f", int {extra}" if extra else "")
+ ", unsigned long long cache_policy)"
)
device_intrinsic(
f"ptx_cp_async_{op_name}",
n_attrs=n_attrs,
c_signature=sig_static, # static — depends on `extra` not on attrs
helper_name=lambda *a, fn=parts_fn: fn(*a)[0],
body=lambda *a, fn=parts_fn: fn(*a)[2],
)
return n_op
# Form 1: .ca + src-size (cp-size ∈ {4, 8}). src-size is required when present.
_register_nb_form("ca_src_size", "ca", fixed_cp_size=None, extra="src_size")
# Form 2: .cg + src-size (cp-size = 16).
_register_nb_form("cg_src_size", "cg", fixed_cp_size=16, extra="src_size")
# Form 3: .ca + ignore-src.
_register_nb_form("ca_ignore_src", "ca", fixed_cp_size=None, extra="ignore_src")
# Form 4: .cg + ignore-src.
_register_nb_form("cg_ignore_src", "cg", fixed_cp_size=16, extra="ignore_src")
# Plain degenerate of forms 1+2 with optional src-size omitted.
_register_nb_form("ca", "ca", fixed_cp_size=None, extra=None)
_register_nb_form("cg", "cg", fixed_cp_size=16, extra=None)
def _make_setp_at_p_helper(ca_or_cg, cp_size, has_cache, prefetch):
"""Wrapper convenience: ``setp+@p`` around a form 1/2 cp.async (predicate-
gated skip with dst untouched on false). Not a PTX form — emitted directly
here as a one-off helper rather than a separate device_intrinsic."""
modifier = _cp_async_modifier_str(has_cache, prefetch)
cache_arg = ", %4" if has_cache else ""
cache_operand = ', "l"(cache_policy)' if has_cache else ""
func_name = (
f"tvm_builtin_ptx_cp_async_{cp_size}"
+ ("_cache_hint" if has_cache else "")
+ (f"_prefetch_{prefetch}" if prefetch else "")
+ "_predicate"
)
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
" __asm__ __volatile__(\n"
' "{\\n"\n'
' " .reg .pred p;\\n"\n'
' " setp.eq.u32 p, %3, 1;\\n"\n'
f' " @p cp.async.{ca_or_cg}.shared.global{modifier}'
f' [%0], [%1], %2{cache_arg};\\n"\n'
' "}\\n"\n'
f' :: "r"(dst_addr), "l"(src), "n"({cp_size}), "r"(predicate){cache_operand}\n'
" );"
)
source_code = (
f"\n__forceinline__ __device__ void {func_name}"
"(void* dst, void* src, int predicate, unsigned long long cache_policy) {\n"
f"{body}\n"
"}\n"
)
return func_name, source_code
@register_codegen("ptx_cp_async")
def codegen_ptx_cp_async(*args):
"""Map the wrapper API to the 4 PTX form table entries.
Accepts three call shapes (sorted by arity):
* 5 args ``(dst_ptr, dst_offset, src_ptr, src_offset, cp_size)`` —
the legacy form emitted by
``tvm.backend.cuda.transform.InjectPTXAsyncCopy``.
Offsets are folded into the pointers via ``tvm_access_ptr`` (in
bytes; offsets are pre-scaled by the pass) and the call is
forwarded with default cache / predicate / fill_mode.
* 6 args ``(dst_ptr, dst_offset, src_ptr, src_offset, cp_size,
predicate)`` — same as 5-arg form with an explicit predicate,
zero-filling the destination when the predicate is false.
* 8 args ``(dst_ptr, src_ptr, cp_size, cache_policy, has_cache_hint,
prefetch_size, predicate, fill_mode)`` — the fork-native wrapper
API.
The three resulting form_kinds:
* ``fill_mode == "zero"`` -> form 1/2 (src-size = predicate ? cp_size : 0)
* ``predicate != -1`` and no fill_mode -> form 1/2 wrapped in setp+@p
(wrapper convenience; not a PTX form)
* else -> form 1/2 with src-size omitted (the "plain" degenerate)
"""
from tvm.tirx.op import if_then_else
if len(args) in (5, 6):
# Legacy InjectPTXAsyncCopy emission: (dst_ptr, dst_off, src_ptr,
# src_off, cp_size [, predicate]). Offsets are element indices into
# the typed buffers (the pass uses index_factor=1 except for the
# shared.dyn-merged byte-buffer path). Emit a C helper that scales
# the offset by the buffer element size, then runs cp.async.
#
# PTX plain form for both .ca and .cg is just
# ``cp.async.<v>.shared.global [dst], [src], cp_size;`` — three
# operands, no trailing src-size / cache-policy.
from tvm import DataType
dst_ptr_in, dst_offset, src_ptr_in, src_offset, cp_size = args[:5]
predicate = args[5] if len(args) == 6 else -1
cp_size_v = int(cp_size)
ca_or_cg = "cg" if cp_size_v == 16 else "ca"
# Recover the per-side element dtype from each pointer's type
# type (Var has ty = PointerType(PrimType(dtype))).
# InjectPTXAsyncCopy emits offsets in element-units of each side's
# buffer dtype (dst gets dst_offset * src_elem_size only when dst is a
# merged shared.dyn byte buffer, in which case dst_elem_dtype is uint8
# and the resulting scale-by-1 is a no-op).
def _elem_bytes(ptr):
ta = getattr(ptr, "ty", None)
if ta is None or getattr(ta, "element_type", None) is None:
return 1
et = ta.element_type
if not hasattr(et, "dtype"):
return 1
bits = DataType(str(et.dtype)).bits
assert bits % 8 == 0, f"non-byte element dtype: {et.dtype}"
return bits // 8
dst_elem_bytes = _elem_bytes(dst_ptr_in)
src_elem_bytes = _elem_bytes(src_ptr_in)
has_predicate = not (
(isinstance(predicate, int) and predicate == -1)
or (hasattr(predicate, "value") and int(predicate.value) == -1)
)
def _scale(n):
return "" if n == 1 else f" * {n}"
dst_scale = _scale(dst_elem_bytes)
src_scale = _scale(src_elem_bytes)
if has_predicate:
func_name = (
f"ptx_cp_async_legacy_pred_{ca_or_cg}_{cp_size_v}_{dst_elem_bytes}_{src_elem_bytes}"
)
if cp_size_v == 4:
zero_fill = ' " @!p st.shared.u32 [%0], {%4};\\n"\n'
elif cp_size_v == 8:
zero_fill = ' " @!p st.shared.v2.u32 [%0], {%4, %4};\\n"\n'
elif cp_size_v == 16:
zero_fill = ' " @!p st.shared.v4.u32 [%0], {%4, %4, %4, %4};\\n"\n'
else:
raise ValueError(f"unsupported legacy predicated cp.async size: {cp_size_v}")
body = (
f" uint8_t* dst_p = (uint8_t*)dst + dst_off{dst_scale};\n"
f" uint8_t* src_p = (uint8_t*)src + src_off{src_scale};\n"
" unsigned int dst_addr = __cvta_generic_to_shared(dst_p);\n"
" __asm__ __volatile__(\n"
' "{\\n"\n'
' " .reg .pred p;\\n"\n'
' " setp.eq.u32 p, %3, 1;\\n"\n'
f' " @p cp.async.{ca_or_cg}.shared.global'
' [%0], [%1], %2;\\n"\n'
f"{zero_fill}"
' "}\\n"\n'
f' :: "r"(dst_addr), "l"(src_p), "n"({cp_size_v}), "r"(predicate), "r"(0)\n'
" );"
)
source_code = (
f"\n__forceinline__ __device__ void {func_name}"
"(void* dst, int dst_off, void* src, int src_off, int predicate) {\n"
f"{body}\n"
"}\n"
)
return cuda_func_call(
func_name,
dst_ptr_in,
dst_offset,
src_ptr_in,
src_offset,
predicate,
source_code=source_code,
)
# No predicate — plain cp.async.
func_name = f"ptx_cp_async_legacy_{ca_or_cg}_{cp_size_v}_{dst_elem_bytes}_{src_elem_bytes}"
body = (
f" uint8_t* dst_p = (uint8_t*)dst + dst_off{dst_scale};\n"
f" uint8_t* src_p = (uint8_t*)src + src_off{src_scale};\n"
" unsigned int dst_addr = __cvta_generic_to_shared(dst_p);\n"
f' asm volatile("cp.async.{ca_or_cg}.shared.global'
' [%0], [%1], %2;"\n'
f' :: "r"(dst_addr), "l"(src_p), "n"({cp_size_v}));'
)
source_code = (
f"\n__forceinline__ __device__ void {func_name}"
"(void* dst, int dst_off, void* src, int src_off) {\n"
f"{body}\n"
"}\n"
)
return cuda_func_call(
func_name,
dst_ptr_in,
dst_offset,
src_ptr_in,
src_offset,
source_code=source_code,
)
elif len(args) == 8:
(
dst_ptr,
src_ptr,
cp_size,
cache_policy,
has_cache_hint,
prefetch_size,
predicate,
fill_mode,
) = args
else:
raise ValueError(f"ptx_cp_async codegen expects 5/6/8 args, got {len(args)}")
cp_size_v = int(cp_size)
ca_or_cg = "cg" if cp_size_v == 16 else "ca"
pref = "" if int(prefetch_size) == -1 else str(int(prefetch_size))
fill = parse_str(fill_mode)
has_cache = _bool_attr(has_cache_hint)
has_predicate = not (
(isinstance(predicate, int) and predicate == -1)
or (hasattr(predicate, "value") and int(predicate.value) == -1)
)
if fill == "zero":
src_size = if_then_else(predicate != 0, cp_size_v, 0)
op = f"tirx.ptx_cp_async_{ca_or_cg}_src_size"
if cp_size_v == 16:
args = [dst_ptr, src_ptr, src_size, cache_policy, has_cache, pref]
else:
args = [dst_ptr, src_ptr, src_size, cache_policy, has_cache, pref, cp_size_v]
result = CODEGEN_REGISTRY[op](args)
return result[0] if isinstance(result, tuple) else result
if has_predicate:
func_name, source_code = _make_setp_at_p_helper(ca_or_cg, cp_size_v, has_cache, pref)
return cuda_func_call(
func_name, dst_ptr, src_ptr, predicate, cache_policy, source_code=source_code
)
# Plain — form 1/2 with src-size omitted.
op = f"tirx.ptx_cp_async_{ca_or_cg}"
if cp_size_v == 16:
args = [dst_ptr, src_ptr, cache_policy, has_cache, pref]
else:
args = [dst_ptr, src_ptr, cache_policy, has_cache, pref, cp_size_v]
result = CODEGEN_REGISTRY[op](args)
return result[0] if isinstance(result, tuple) else result
CODEGEN_REGISTRY["tirx.ptx.cp_async_raw"] = CODEGEN_REGISTRY["tirx.ptx.cp_async"]
# =============================================================================
# cp.async.bulk.tensor (TMA) — one device_intrinsic per arity variant of each
# PTX form. Per-dim coord operands materialise via the ``c_signature`` callable.
# =============================================================================
def _is_sm100_or_higher():
target = tvm.target.Target.current()
if target is None:
return False
arch = target.arch[3:]
if not arch[-1].isdigit():
arch = arch[:-1]
return int(arch) >= 100
def _resolve_cta_group_str(cta_group):
if cta_group == 2 or (cta_group != -1 and _is_sm100_or_higher()):
return f".cta_group::{cta_group}"
return ""
def _coord_template(coord_count, start_slot):
inner = ", ".join(f"%{start_slot + i}" for i in range(coord_count))
return f"{{{inner}}}"
def _coord_constraints(coord_count):
return ", ".join(f'"r"(coord{i})' for i in range(coord_count))
def _coord_sig(n):
return ", ".join(f"int coord{i}" for i in range(n))
# PTX cp.async.bulk.tensor global -> shared::cluster form:
# cp.async.bulk.tensor.dim.dst.src{.load_mode}.completion_mechanism
# {.multicast}{.cta_group}{.level::cache_hint}
# [dstMem], [tensorMap, tensorCoords], [mbar]{, im2colInfo}
# {, ctaMask} {, cache-policy}
# .dst = {.shared::cluster}; .src = {.global}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
# .multicast = {.multicast::cluster}
# .cta_group = {.cta_group::1, .cta_group::2}
# .load_mode = {.tile, .tile::gather4, .im2col, .im2col::w, .im2col::w::128}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile/tile::gather4 modes; ctaMask is only used
# when the optional ``.multicast::cluster`` modifier is enabled.
def _g2cluster_parts(*args):
attrs = args[-6:]
dim = int(attrs[0])
cta_group = int(attrs[1])
has_cache = _bool_attr(attrs[2])
tile_mode = parse_str(attrs[3])
bar_is_addr = _bool_attr(attrs[4])
multicast = _bool_attr(attrs[5])
coord_count = 5 if tile_mode == "tile_gather4" else dim
bar_type = "unsigned int bar_addr" if bar_is_addr else "void* bar"
sig = (
f"(void* dst, {bar_type}, unsigned long long tensormap_addr, "
"uint16_t cta_mask, unsigned long long cache_policy"
+ (", " + _coord_sig(coord_count) if coord_count else "")
+ ")"
)
name = (
f"ptx_cp_async_bulk_tensor_g2cluster_{tile_mode}_{dim}d"
f"{'_multicast' if multicast else ''}"
f"{'_cache_hint' if has_cache else ''}{'_bar_addr' if bar_is_addr else ''}"
)
tile_modifier = ".tile::gather4" if tile_mode == "tile_gather4" else ""
cta_group_str = _resolve_cta_group_str(cta_group)
multicast_inst = ".multicast::cluster" if multicast else ""
cache_inst = ".L2::cache_hint" if has_cache else ""
mask_arg = ',\n "h"(cta_mask)' if multicast else ""
cache_arg = ',\n "l"(cache_policy)' if has_cache else ""
mask_slot = ", %3" if multicast else ""
cache_slot = ", %4" if multicast and has_cache else ", %3" if has_cache else ""
coord_start = 5 if multicast and has_cache else 4 if multicast or has_cache else 3
coord_tpl = _coord_template(coord_count, coord_start)
instr = (
f"cp.async.bulk.tensor.{dim}d.shared::cluster.global{tile_modifier}"
f".mbarrier::complete_tx::bytes{multicast_inst}"
f"{cta_group_str}{cache_inst}"
)
bar_addr_decl = (
"" if bar_is_addr else " unsigned int bar_addr = __cvta_generic_to_shared(bar);\n"
)
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
f"{bar_addr_decl}"
" asm volatile(\n"
f' "{instr} [%0], [%1, {coord_tpl}], [%2]{mask_slot}{cache_slot};"\n'
" :\n"
f' : "r"(dst_addr), "l"(tensormap_addr), "r"(bar_addr){mask_arg}{cache_arg},\n'
f" {_coord_constraints(coord_count)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_g2cluster",
n_attrs=6,
helper_name=lambda *a: _g2cluster_parts(*a)[0],
c_signature=lambda *a: _g2cluster_parts(*a)[1],
body=lambda *a: _g2cluster_parts(*a)[2],
)
# PTX cp.async.bulk.tensor shared::cta -> global form:
# cp.async.bulk.tensor.dim.dst.src{.load_mode}.completion_mechanism
# {.level::cache_hint}
# [tensorMap, tensorCoords], [srcMem] {, cache-policy}
# .dst = {.global}; .src = {.shared::cta}
# .completion_mechanism = {.bulk_group}
# .load_mode = {.tile, .tile::scatter4, .im2col_no_offs}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile mode; cache-policy is a real operand.
def _s2g_parts(*args):
attrs = args[-2:]
dim = int(attrs[0])
has_cache = _bool_attr(attrs[1])
sig = (
"(void* src, unsigned long long tensormap_addr, unsigned long long cache_policy"
+ (", " + _coord_sig(dim) if dim else "")
+ ")"
)
name = f"ptx_cp_async_bulk_tensor_shared_to_global_{dim}d{'_cache_hint' if has_cache else ''}"
cache_inst = ".L2::cache_hint" if has_cache else ""
cache_arg = ', "l"(cache_policy)' if has_cache else ""
cache_slot = ", %2" if has_cache else ""
coord_start = 3 if has_cache else 2
coord_tpl = _coord_template(dim, coord_start)
instr = f"cp.async.bulk.tensor.{dim}d.global.shared::cta.tile.bulk_group{cache_inst}"
body = (
" unsigned int src_addr = __cvta_generic_to_shared(src);\n"
" asm volatile(\n"
f' "{instr} [%0, {coord_tpl}], [%1]{cache_slot};"\n'
" :\n"
f' : "l"(tensormap_addr), "r"(src_addr){cache_arg},\n'
f" {_coord_constraints(dim)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_s2g",
n_attrs=2,
helper_name=lambda *a: _s2g_parts(*a)[0],
c_signature=lambda *a: _s2g_parts(*a)[1],
body=lambda *a: _s2g_parts(*a)[2],
)
# PTX cp.async.bulk.prefetch.tensor form:
# cp.async.bulk.prefetch.tensor.dim.L2.src{.load_mode}{.level::cache_hint}
# [tensorMap, tensorCoords] {, im2colInfo} {, cache-policy}
# .src = {.global}
# .load_mode = {.tile, .tile::gather4, .im2col, .im2col::w, .im2col::w::128}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile mode; cache-policy is a real operand.
def _prefetch_parts(*args):
attrs = args[-2:]
dim = int(attrs[0])
has_cache = _bool_attr(attrs[1])
sig = (
"(unsigned long long tensormap_addr, unsigned long long cache_policy"
+ (", " + _coord_sig(dim) if dim else "")
+ ")"
)
name = (
f"ptx_cp_async_bulk_tensor_global_to_cluster_prefetch_{dim}d"
f"{'_cache_hint' if has_cache else ''}"
)
cache_inst = ".L2::cache_hint" if has_cache else ""
cache_arg = ', "l"(cache_policy)' if has_cache else ""
cache_slot = ", %1" if has_cache else ""
coord_start = 2 if has_cache else 1
coord_tpl = _coord_template(dim, coord_start)
instr = f"cp.async.bulk.prefetch.tensor.{dim}d.L2.global.tile{cache_inst}"
body = (
" asm volatile(\n"
f' "{instr} [%0, {coord_tpl}]{cache_slot};"\n'
" :\n"
f' : "l"(tensormap_addr){cache_arg},\n'
f" {_coord_constraints(dim)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_prefetch",
n_attrs=2,
helper_name=lambda *a: _prefetch_parts(*a)[0],
c_signature=lambda *a: _prefetch_parts(*a)[1],
body=lambda *a: _prefetch_parts(*a)[2],
)
# PTX cp.reduce.async.bulk.tensor shared::cta -> global form:
# cp.reduce.async.bulk.tensor.dim.dst.src.redOp{.load_mode}.completion_mechanism
# {.level::cache_hint}
# [tensorMap, tensorCoords], [srcMem] {, cache-policy}
# .dst = {.global}; .src = {.shared::cta}
# .completion_mechanism = {.bulk_group}
# .redOp = {.add, .min, .max, .inc, .dec, .and, .or, .xor}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile mode; redOp is syntax, cache-policy is an operand.
def _reduce_parts(*args):
attrs = args[-3:]
dim = int(attrs[0])
has_cache = _bool_attr(attrs[1])
red_op = parse_str(attrs[2])
sig = (
"(void* src, unsigned long long tensormap_addr, unsigned long long cache_policy"
+ (", " + _coord_sig(dim) if dim else "")
+ ")"
)
name = (
f"ptx_cp_async_bulk_tensor_shared_to_global_reduce_{dim}d"
f"{'_cache_hint' if has_cache else ''}"
)
cache_inst = ".L2::cache_hint" if has_cache else ""
cache_arg = ', "l"(cache_policy)' if has_cache else ""
cache_slot = ", %2" if has_cache else ""
coord_start = 3 if has_cache else 2
coord_tpl = _coord_template(dim, coord_start)
instr = (
f"cp.reduce.async.bulk.tensor.{dim}d.global.shared::cta"
f".{red_op}.tile.bulk_group{cache_inst}"
)
body = (
" unsigned int src_addr = __cvta_generic_to_shared(src);\n"
" asm volatile(\n"
f' "{instr} [%0, {coord_tpl}], [%1]{cache_slot};"\n'
" :\n"
f' : "l"(tensormap_addr), "r"(src_addr){cache_arg},\n'
f" {_coord_constraints(dim)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_reduce",
n_attrs=3,
helper_name=lambda *a: _reduce_parts(*a)[0],
c_signature=lambda *a: _reduce_parts(*a)[1],
body=lambda *a: _reduce_parts(*a)[2],
)
# User-facing dispatchers for tensor global -> shared::cluster. The same
# backend root handles the optional ``.multicast::cluster`` modifier.
def _g2c_dispatch(dim, dst_ptr, bar, tensormap, *args, tile_mode):
cta_mask, cta_group, cache_policy, has_cache, *rest = args
coord_count = 5 if tile_mode == "tile_gather4" else int(dim)
if len(rest) == coord_count + 1:
bar_is_addr = _bool_attr(rest[0])
coords = rest[1:]
else:
bar_is_addr = False
coords = rest
is_unicast = isinstance(cta_mask, tvm.tirx.IntImm) and bin(int(cta_mask)).count("1") <= 1
cg = int(cta_group)
op = "tirx.ptx_cp_async_bulk_tensor_g2cluster"
call_args = [
dst_ptr,
bar,
tensormap,
cta_mask,
cache_policy,
*coords,
int(dim),
cg,
has_cache,
tile_mode,
bar_is_addr,
int(not is_unicast),
]
result = CODEGEN_REGISTRY[op](call_args)
return result[0] if isinstance(result, tuple) else result
@register_codegen("ptx_cp_async_bulk_tensor_global_to_cluster")
def codegen_g2c(dim, dst_ptr, bar, tensormap, *args):
return _g2c_dispatch(dim, dst_ptr, bar, tensormap, *args, tile_mode="tile")
@register_codegen("ptx_cp_async_bulk_tensor_tile_gather4_global_to_cluster")
def codegen_g2c_gather4(dim, dst_ptr, bar, tensormap, *args):
return _g2c_dispatch(dim, dst_ptr, bar, tensormap, *args, tile_mode="tile_gather4")
@register_codegen("ptx_cp_async_bulk_tensor_shared_to_global")
def codegen_s2g(dim, src_ptr, tensormap, *args):
cache_policy, has_cache, *coords = args
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_tensor_s2g"](
[src_ptr, tensormap, cache_policy, *coords, int(dim), has_cache]
)
return result[0] if isinstance(result, tuple) else result
@register_codegen("ptx_cp_async_bulk_tensor_global_to_cluster_prefetch")
def codegen_prefetch(dim, tensormap, *args):
cache_policy, has_cache, *coords = args
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_tensor_prefetch"](
[tensormap, cache_policy, *coords, int(dim), has_cache]
)
return result[0] if isinstance(result, tuple) else result
@register_codegen("ptx_cp_async_bulk_tensor_shared_to_global_reduce")
def codegen_reduce(dim, src_ptr, tensormap, *args):
cache_policy, has_cache, red_op, *coords = args
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_tensor_reduce"](
[src_ptr, tensormap, cache_policy, *coords, int(dim), has_cache, red_op]
)
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# cp.async.bulk non-TMA forms from the PTX Syntax block. Each form is one
# device_intrinsic; optional PTX modifiers are attrs, not separate fixed ops.
# =============================================================================
device_intrinsic(
"ptx_cp_async_bulk_commit_group",
helper_name="ptx_cp_async_bulk_tensor_commit_group",
body=' asm volatile("cp.async.bulk.commit_group;");',
)
def _ptx_cp_async_bulk_wait_group_parts(n, read):
n = int(n)
read_b = bool(int(read)) if hasattr(read, "value") else bool(read)
return (
f"ptx_cp_async_bulk_wait_group{'_read' if read_b else ''}_{n}",
f' asm volatile("cp.async.bulk.wait_group{".read" if read_b else ""} {n};");',
)
device_intrinsic(
"ptx_cp_async_bulk_wait_group",
n_attrs=2,
helper_name=lambda n, read: _ptx_cp_async_bulk_wait_group_parts(n, read)[0],
body=lambda n, read: _ptx_cp_async_bulk_wait_group_parts(n, read)[1],
)
def _bool_attr(value):
return bool(int(value)) if hasattr(value, "value") else bool(value)
def _bulk_cache_operand_constraint(has_cache):
return ', "l"(cache_policy)' if has_cache else ""
def _bulk_cache_operand_suffix(has_cache):
return ".L2::cache_hint" if has_cache else ""
# PTX cp.async.bulk global -> shared::cta form:
# cp.async.bulk.dst.src.completion_mechanism{.level::cache_hint}{.ignore_oob}
# [dstMem], [srcMem], size{, ignoreBytesLeft, ignoreBytesRight}, [mbar] {, cache-policy}
# .dst = {.shared::cta}; .src = {.global}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
# .level::cache_hint = {.L2::cache_hint}
def _bulk_g2s_cta_parts(*args):
has_cache = _bool_attr(args[-2])
ignore_oob = _bool_attr(args[-1])
instr = (
"cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes"
f"{_bulk_cache_operand_suffix(has_cache)}{'.ignore_oob' if ignore_oob else ''}"
)
if ignore_oob:
asm_args = (
'"r"(dst), "l"(src_ptr), "r"(num_bytes), "r"(ignore_bytes_left), '
'"r"(ignore_bytes_right), "r"(mbarrier)'
)
operands = "%2, %3, %4, [%5]"
cache_slot = ", %6" if has_cache else ""
else:
asm_args = '"r"(dst), "l"(src_ptr), "r"(num_bytes), "r"(mbarrier)'
operands = "%2, [%3]"
cache_slot = ", %4" if has_cache else ""
body = (
" unsigned int dst = (unsigned int)__cvta_generic_to_shared(dst_ptr);\n"
" unsigned int mbarrier = (unsigned int)__cvta_generic_to_shared(mbarrier_ptr);\n"
f' asm volatile("{instr} [%0], [%1], {operands}{cache_slot};"\n'
" :\n"
f" : {asm_args}{_bulk_cache_operand_constraint(has_cache)}\n"
' : "memory");'
)
name = (
"tvm_builtin_ptx_cp_async_bulk_g2s_cta"
f"{'_cache_hint' if has_cache else ''}{'_ignore_oob' if ignore_oob else ''}"
)
return name, body
device_intrinsic(
"ptx_cp_async_bulk_g2s_cta",
n_attrs=2,
helper_name=lambda *a: _bulk_g2s_cta_parts(*a)[0],
c_signature=(
"(void* dst_ptr, void* src_ptr, unsigned int num_bytes, "
"unsigned int ignore_bytes_left, unsigned int ignore_bytes_right, "
"void* mbarrier_ptr, unsigned long long cache_policy)"
),
body=lambda *a: _bulk_g2s_cta_parts(*a)[1],
)
# PTX cp.async.bulk global -> shared::cluster form:
# cp.async.bulk.dst.src.completion_mechanism{.multicast}{.level::cache_hint}
# [dstMem], [srcMem], size, [mbar] {, ctaMask} {, cache-policy}
# .dst = {.shared::cluster}; .src = {.global}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
# .level::cache_hint = {.L2::cache_hint}
# .multicast = {.multicast::cluster}
def _bulk_g2s_cluster_parts(*args):
has_cache = _bool_attr(args[-2])
multicast = _bool_attr(args[-1])
instr = (
"cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes"
f"{'.multicast::cluster' if multicast else ''}{_bulk_cache_operand_suffix(has_cache)}"
)
cta_constraint = ', "h"(cta_mask)' if multicast else ""
mask_slot = ", %4" if multicast else ""
cache_slot = ", %5" if multicast and has_cache else ", %4" if has_cache else ""
body = (
" unsigned int dst = (unsigned int)__cvta_generic_to_shared(dst_ptr);\n"
" unsigned int mbarrier = (unsigned int)__cvta_generic_to_shared(mbarrier_ptr);\n"
f' asm volatile("{instr} [%0], [%1], %2, [%3]'
f'{mask_slot}{cache_slot};"\n'
" :\n"
' : "r"(dst), "l"(src_ptr), "r"(num_bytes), "r"(mbarrier)'
f"{cta_constraint}{_bulk_cache_operand_constraint(has_cache)}\n"
' : "memory");'
)
name = (
"tvm_builtin_ptx_cp_async_bulk_g2s_cluster"
f"{'_multicast' if multicast else ''}{'_cache_hint' if has_cache else ''}"
)
return name, body
device_intrinsic(
"ptx_cp_async_bulk_g2s_cluster",
n_attrs=2,
helper_name=lambda *a: _bulk_g2s_cluster_parts(*a)[0],
c_signature=(
"(void* dst_ptr, void* src_ptr, unsigned int num_bytes, "
"void* mbarrier_ptr, unsigned short cta_mask, unsigned long long cache_policy)"
),
body=lambda *a: _bulk_g2s_cluster_parts(*a)[1],
)
# PTX cp.async.bulk shared::cta -> shared::cluster form:
# cp.async.bulk.dst.src.completion_mechanism [dstMem], [srcMem], size, [mbar]
# .dst = {.shared::cluster}; .src = {.shared::cta}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
device_intrinsic(
"ptx_cp_async_bulk_s2s_cluster",
helper_name="tvm_builtin_ptx_cp_async_bulk_s2s_cluster",
c_signature="(uint64_t dst, void* src, int size, uint64_t mbar)",
body=r""" unsigned int dst_addr = static_cast<unsigned int>(dst);
unsigned int src_addr = __cvta_generic_to_shared(src);
unsigned int mbar_addr = static_cast<unsigned int>(mbar);
asm volatile(
"cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes"
" [%0], [%1], %2, [%3];"
:
: "r"(dst_addr), "r"(src_addr), "r"(size), "r"(mbar_addr)
: "memory");""",
)
@register_codegen("ptx_cp_async_bulk_shared_to_cluster")
def codegen_ptx_cp_async_bulk_shared_to_cluster(dst_ptr, src_ptr, size, mbar):
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_s2s_cluster"]([dst_ptr, src_ptr, size, mbar])
return result[0] if isinstance(result, tuple) else result
# PTX cp.async.bulk shared::cta -> global form:
# cp.async.bulk.dst.src.completion_mechanism{.level::cache_hint}{.cp_mask}
# [dstMem], [srcMem], size {, cache-policy} {, byteMask}
# .dst = {.global}; .src = {.shared::cta}
# .completion_mechanism = {.bulk_group}
# .level::cache_hint = {.L2::cache_hint}
def _bulk_s2g_parts(*args):
has_cache = _bool_attr(args[-2])
cp_mask = _bool_attr(args[-1])
if cp_mask and not has_cache:
raise ValueError("cp.async.bulk shared::cta -> global .cp_mask requires .L2::cache_hint")
instr = f"cp.async.bulk.global.shared::cta.bulk_group{_bulk_cache_operand_suffix(has_cache)}"
if cp_mask:
instr += ".cp_mask"
cache_slot = ", %3" if has_cache else ""
mask_slot = ", %4" if cp_mask else ""
mask_constraint = ', "r"(byte_mask)' if cp_mask else ""
body = (
" unsigned int src = (unsigned int)__cvta_generic_to_shared(src_ptr);\n"
f' asm volatile("{instr} [%0], [%1], %2'
f'{cache_slot}{mask_slot};"\n'
" :\n"
' : "l"(dst_ptr), "r"(src), "r"(num_bytes)'
f"{_bulk_cache_operand_constraint(has_cache)}{mask_constraint}\n"
' : "memory");'
)
name = (
"tvm_builtin_ptx_cp_async_bulk_s2g"
f"{'_cache_hint' if has_cache else ''}{'_cp_mask' if cp_mask else ''}"
)
return name, body
device_intrinsic(
"ptx_cp_async_bulk_s2g",
n_attrs=2,
helper_name=lambda *a: _bulk_s2g_parts(*a)[0],
c_signature=(
"(void* dst_ptr, void* src_ptr, unsigned int num_bytes, "
"unsigned int byte_mask, unsigned long long cache_policy)"
),
body=lambda *a: _bulk_s2g_parts(*a)[1],
)