chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
@@ -0,0 +1,49 @@
# 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=unused-import
"""CUDA HW intrinsic codegens, grouped by feature domain.
- ``mma`` / ``wgmma`` / ``tcgen05`` — matrix-multiply hardware (Volta+/Hopper/Blackwell).
- ``cp_async`` — cp.async + cp.async.bulk + cp.async.bulk.tensor (TMA), incl. TMA address helpers.
- ``sync`` — barriers, fences, mbarrier, cluster.barrier, warp vote, elect, sync helpers.
- ``math`` — packed-f32x2 arithmetic, exp2/rcp/reduce3, warp/CTA reductions.
- ``memory`` — typed copies, ldg, ld.global.acquire, atomics, type conversions, address casts.
- ``nvshmem`` — NVSHMEM RMA / signal / collective.
- ``misc`` — register-allocation control, profiler timer, debug helpers (printf / trap).
Plus the support modules:
- ``header`` — CUDA header generator and helper-tag table.
- ``registry`` — codegen registry.
- ``types`` — PTX dtype enum.
- ``utils`` — small parsing / validation helpers.
"""
# Import op modules to register their codegen functions.
from . import cp_async, math, memory, misc, mma, nvshmem, sync, tcgen05, wgmma
from .header import TAGS, header_generator
from .registry import CODEGEN_REGISTRY, get_codegen, register_codegen
from .types import PTXDataType
__all__ = [
"CODEGEN_REGISTRY",
"TAGS",
"PTXDataType",
"get_codegen",
"header_generator",
"register_codegen",
]
@@ -0,0 +1,144 @@
# 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=invalid-name
"""Thin device-helper registration for TIRx intrinsic codegens.
Exposes one entry point: :func:`device_intrinsic`. Given an op name plus
``(helper_name, c_signature, body)`` (each a string or a ``(*args) -> str``
callable), it:
* wraps the body in
``__forceinline__ __device__ <ret> <name><sig> { <body> }``,
* registers a codegen function under the op name so
``call_intrin("", "tirx.<op_name>", *args)`` resolves to a call to that
helper. TVM Op registration is static C++ only.
Args passed to the codegen are split into ``(forward_args, attr_args)``:
the trailing ``n_attrs`` are attrs (consumed by the ``helper_name`` /
``c_signature`` / ``body`` callables but **not** forwarded to the helper),
and the rest are operand args (forwarded). The default ``n_attrs=0`` means
every arg is forwarded — appropriate for fixed-arity ops with literal
``c_signature`` and ``helper_name``.
Coerce / validate attrs explicitly inside the callables — there is no
``Choice`` / ``Bool`` / ``IntAttr`` machinery; just call ``parse_str`` /
``int`` / ``bool`` on the raw arg as needed.
"""
from __future__ import annotations
from collections.abc import Callable
from tvm.backend.cuda.op import cuda_func_call
from tvm.backend.cuda.operator.intrinsics.registry import register_codegen
# C primitive type → TVM dtype string. Used when the caller specifies a
# non-void ``return_type`` but no explicit ``tvm_return_type`` — the helper
# knows the TVM-side dtype from the C return type.
_C_TO_TVM_DTYPE = {
"float": "float32",
"double": "float64",
"uint32_t": "uint32",
"int32_t": "int32",
"uint64_t": "uint64",
"int64_t": "int64",
"uint16_t": "uint16",
"int16_t": "int16",
"unsigned long long": "uint64",
"long long": "int64",
"unsigned short": "uint16",
"bool": "bool",
"unsigned int": "uint32",
"int": "int32",
}
def device_intrinsic(
op_name: str,
*,
helper_name: str | Callable | None = None,
c_signature: str | Callable = "()",
body: str | Callable,
n_attrs: int = 0,
return_type: str | Callable = "void",
tvm_return_type: str | Callable | None = None,
templated: bool = False,
extra_deps: tuple = (),
) -> None:
"""Register a CUDA device-helper intrinsic.
Parameters
----------
op_name :
Registry key — ``call_intrin("", "tirx.<op_name>", ...)`` resolves
here. Also used as the default helper name (``tvm_builtin_<op_name>``)
when ``helper_name`` is not provided.
helper_name :
Literal C function name, OR ``(*args) -> str`` to compute it from
attr values. Defaults to ``f"tvm_builtin_{op_name}"``.
c_signature :
Literal C parameter list including outer parens (``"(int x, int y)"``),
OR ``(*args) -> str`` to compute it from attr values. Defaults to
``"()"``.
body :
Literal C body string (already indented), OR ``(*args) -> str``.
n_attrs :
Number of trailing args that are attrs (consumed by ``helper_name``
/ ``c_signature`` / ``body`` callables, NOT forwarded to the helper
as call arguments). The first ``len(args) - n_attrs`` args are the
operand args forwarded to the helper.
return_type :
C return type. Default ``"void"``. Either a literal string or
``(*args) -> str`` when the helper return type depends on attrs.
tvm_return_type :
TVM dtype for the call result, when the helper has a non-void
return. Either a literal string (``"int32"``) or ``(*args) -> str``.
If omitted and ``return_type`` is non-void, it is auto-derived from
the ``_C_TO_TVM_DTYPE`` table.
templated :
Prefix the helper with ``template <typename T>``.
extra_deps :
Helper-tag list (e.g. ``("get_tmem_addr",)``) forwarded as the second
element of the codegen result so the header generator emits the
prerequisite snippets.
"""
if helper_name is None:
helper_name = f"tvm_builtin_{op_name}"
extra_deps = tuple(extra_deps)
def codegen(*args):
forward = args if n_attrs == 0 else args[:-n_attrs]
name = helper_name(*args) if callable(helper_name) else helper_name
sig = c_signature(*args) if callable(c_signature) else c_signature
body_str = body(*args) if callable(body) else body
ret_type = return_type(*args) if callable(return_type) else return_type
prefix = "template <typename T>\n" if templated else ""
source_code = (
f"\n{prefix}__forceinline__ __device__ {ret_type} {name}{sig} {{\n{body_str}\n}}\n"
)
kwargs = {"source_code": source_code}
if tvm_return_type is not None:
kwargs["return_type"] = (
tvm_return_type(*args) if callable(tvm_return_type) else tvm_return_type
)
elif ret_type != "void":
kwargs["return_type"] = _C_TO_TVM_DTYPE.get(ret_type, ret_type)
result = cuda_func_call(name, *forward, **kwargs)
return (result, list(extra_deps)) if extra_deps else result
codegen.__name__ = f"codegen_{op_name}"
register_codegen(op_name)(codegen)
@@ -0,0 +1,924 @@
# 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],
)
@@ -0,0 +1,809 @@
# 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=line-too-long
"""CUDA header generator for codegen.
The header generator is used to generate the header for the CUDA code.
It's controlled by the predefined tags.
The tags are used to identify the utility functions/classes necessary for the codegen.
"""
import tvm_ffi
TAGS = {
"cuda",
"cuda/barrier",
"cooperative_groups",
"fp16",
"bf16",
"fp8",
"fp6",
"fp4",
"int8",
"math_constants",
"mma",
"warp_shuffle",
"cast_smem_ptr_to_int",
"get_tmem_addr",
"gmma_descriptor",
"smem_descriptor",
"instr_descriptor",
"instr_descriptor_block_scaled",
"get_time_stamp",
"nvshmem",
"elect_one_sync",
}
@tvm_ffi.register_global_func("tirx.intrinsics.cuda.header_generator")
def header_generator(tags):
"""Generate the header for the CUDA code."""
for tag in tags:
if tag not in TAGS:
raise ValueError(f"Invalid tag: {tag}")
header = ""
if "nvshmem" in tags:
header += R"""
#include <nvshmem.h>
#include <nvshmemx.h>
"""
if "cuda/barrier" in tags or "cooperative_groups" in tags:
header += (
R"""
#include <cuda/barrier>
#include <cooperative_groups.h>
"""
+ "\n"
)
# NVRTC has no host C++ stdlib and no <cuda.h>. Branch on __CUDACC_RTC__ so
# the same emitted source compiles under both nvcc (offline) and NVRTC
# (runtime) without any post-processing in tvm.support.nvcc.
header += """
#ifdef __CUDACC_RTC__
#include <cuda/std/cstdint>
using cuda::std::uint8_t;
using cuda::std::uint16_t;
using cuda::std::uint32_t;
using cuda::std::uint64_t;
using cuda::std::int8_t;
using cuda::std::int16_t;
using cuda::std::int32_t;
using cuda::std::int64_t;
#include <cuda/std/type_traits>
namespace std {
using cuda::std::is_same;
using cuda::std::is_same_v;
using cuda::std::is_integral;
using cuda::std::is_signed;
using cuda::std::is_unsigned;
using cuda::std::is_floating_point;
using cuda::std::enable_if;
using cuda::std::conditional;
}
// NVRTC uses asm/volatile instead of __asm__/__volatile__ (gcc extension).
#ifndef __asm__
#define __asm__ asm
#endif
#ifndef __volatile__
#define __volatile__ volatile
#endif
#else
#include <cstdint>
#include <type_traits>
#include <cuda.h>
#endif
"""
if "fp16" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 530)
#include <cuda_fp16.h>
__device__ half max(half a, half b)
{
return __hgt(__half(a), __half(b)) ? a : b;
}
__device__ half min(half a, half b)
{
return __hlt(__half(a), __half(b)) ? a : b;
}
#endif // __CUDA_ARCH__ >= 530
// Pack two half values.
static inline __device__ __host__ unsigned
__pack_half2(const half x, const half y) {
unsigned v0 = *((unsigned short *)&x);
unsigned v1 = *((unsigned short *)&y);
return (v1 << 16) | v0;
}
#define CUDA_UNSUPPORTED_HALF_MATH_BINARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ half HALF_MATH_NAME(half x, half y) { \
float tmp_x = __half2float(x); \
float tmp_y = __half2float(y); \
float result = FP32_MATH_NAME(tmp_x, tmp_y); \
return __float2half(result); \
}
#define CUDA_UNSUPPORTED_HALF_MATH_UNARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ half HALF_MATH_NAME(half x) { \
float tmp_x = __half2float(x); \
float result = FP32_MATH_NAME(tmp_x); \
return __float2half(result); \
}
// Some fp16 math functions are not supported in cuda_fp16.h,
// so we define them here to make sure the generated CUDA code
// is valid.
#if defined(__CUDA_ARCH__)
#if (__CUDA_ARCH__ >= 530)
CUDA_UNSUPPORTED_HALF_MATH_BINARY(hpow, powf)
#if ((__CUDACC_VER_MAJOR__ < 12) || ((__CUDACC_VER_MAJOR__ == 12) && (__CUDACC_VER_MINOR__ < 8)))
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htanh, tanhf)
#endif
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htan, tanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(hatan, atanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(herf, erf)
#else
CUDA_UNSUPPORTED_HALF_MATH_UNARY(hexp, exp)
#endif
#endif
#undef CUDA_UNSUPPORTED_HALF_MATH_BINARY
#undef CUDA_UNSUPPORTED_HALF_MATH_UNARY
"""
if "bf16" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
#include <cuda_bf16.h>
__device__ nv_bfloat16 max(nv_bfloat16 a, nv_bfloat16 b)
{
return __hgt(a, b) ? a : b;
}
__device__ nv_bfloat16 min(nv_bfloat16 a, nv_bfloat16 b)
{
return __hlt(a, b) ? a : b;
}
#endif // __CUDA_ARCH__ >= 800
// Pack two bfloat16 values.
static inline __device__ __host__ unsigned
__pack_nv_bfloat162(const nv_bfloat16 x, const nv_bfloat16 y) {
unsigned v0 = *((unsigned short *)&x);
unsigned v1 = *((unsigned short *)&y);
return (v1 << 16) | v0;
}
// Some bfp16 math functions are not supported in cuda_bfp16.h,
// so we define them here to make sure the generated CUDA code
// is valid.
#define CUDA_UNSUPPORTED_HALF_MATH_BINARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ nv_bfloat16 HALF_MATH_NAME(nv_bfloat16 x, nv_bfloat16 y) { \
float tmp_x = __bfloat162float(x); \
float tmp_y = __bfloat162float(y); \
float result = FP32_MATH_NAME(tmp_x, tmp_y); \
return __float2bfloat16(result); \
}
#define CUDA_UNSUPPORTED_HALF_MATH_UNARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ nv_bfloat16 HALF_MATH_NAME(nv_bfloat16 x) { \
float tmp_x = __bfloat162float(x); \
float result = FP32_MATH_NAME(tmp_x); \
return __float2bfloat16(result); \
}
CUDA_UNSUPPORTED_HALF_MATH_BINARY(hpow, powf)
#if ((__CUDACC_VER_MAJOR__ < 12) || ((__CUDACC_VER_MAJOR__ == 12) && (__CUDACC_VER_MINOR__ < 8)))
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htanh, tanhf)
#endif
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htan, tanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(hatan, atanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(herf, erf)
#undef CUDA_UNSUPPORTED_HALF_MATH_BINARY
#undef CUDA_UNSUPPORTED_HALF_MATH_UNARY
"""
if "fp8" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 890)
#include <cuda_fp8.h>
using fp8_e4_t = __nv_fp8_e4m3;
using fp8_e4x2_t = __nv_fp8x2_e4m3;
using fp8_e4x4_t = __nv_fp8x4_e4m3;
struct fp8_e4x8_t {
fp8_e4_t data[8];
};
struct fp8_e4x16_t {
fp8_e4_t data[16];
};
using fp8_e5_t = __nv_fp8_e5m2;
using fp8_e5x2_t = __nv_fp8x2_e5m2;
using fp8_e5x4_t = __nv_fp8x4_e5m2;
struct fp8_e5x8_t {
fp8_e5_t data[8];
};
struct fp8_e5x16_t {
fp8_e5_t data[16];
};
using fp8_e8_t = __nv_fp8_e8m0;
using fp8_e8x2_t = __nv_fp8x2_e8m0;
using fp8_e8x4_t = __nv_fp8x4_e8m0;
struct fp8_e8x8_t {
fp8_e8_t data[8];
};
struct fp8_e8x16_t {
fp8_e8_t data[16];
};
#endif // __CUDA_ARCH__ >= 890
"""
if "fp6" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 1000)
#include <cuda_fp6.h>
using fp6_e2_t = __nv_fp6_e2m3;
using fp6_e2x2_t = __nv_fp6x2_e2m3;
using fp6_e2x4_t = __nv_fp6x4_e2m3;
struct fp6_e2x8_t {
fp6_e2_t data[8];
};
struct fp6_e2x16_t {
fp6_e2_t data[16];
};
using fp6_e3_t = __nv_fp6_e3m2;
using fp6_e3x2_t = __nv_fp6x2_e3m2;
using fp6_e3x4_t = __nv_fp6x4_e3m2;
struct fp6_e3x8_t {
fp6_e3_t data[8];
};
struct fp6_e3x16_t {
fp6_e3_t data[16];
};
#endif // __CUDA_ARCH__ >= 1000
"""
if "fp4" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
#include <cuda_fp4.h>
using fp4_e2_t = __nv_fp4_e2m1;
using fp4_e2x2_t = __nv_fp4x2_e2m1;
using fp4_e2x4_t = __nv_fp4x4_e2m1;
struct fp4_e2x8_t {
fp4_e2_t data[8];
};
struct fp4_e2x16_t {
fp4_e2_t data[16];
};
#endif // __CUDA_ARCH__ >= 800
"""
#########################################################
# Vector type extensions
#########################################################
if "fp16" in tags or "bf16" in tags:
header += R"""
template <typename T, typename TVec2>
struct __align__(8) half4_bfloat164 {
T x, y, z, w;
__host__ __device__ half4_bfloat164() : x(T(0)), y(T(0)), z(T(0)), w(T(0)) {}
__host__ __device__ half4_bfloat164(T x, T y, T z, T w) : x(x), y(y), z(z), w(w) {}
"""
if "fp8" in tags:
header += R"""
__host__ __device__ explicit half4_bfloat164(const __nv_fp8x4_e4m3& fp8x4) {
if constexpr (std::is_same_v<T, __half>) {
__nv_fp8x2_e4m3 lo_part, hi_part;
lo_part.__x = static_cast<__nv_fp8x2_storage_t>(fp8x4.__x & 0xFFFF);
hi_part.__x = static_cast<__nv_fp8x2_storage_t>((fp8x4.__x >> 16) & 0xFFFF);
TVec2 lo_half2 = static_cast<TVec2>(lo_part);
TVec2 hi_half2 = static_cast<TVec2>(hi_part);
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
} else {
__nv_fp8_storage_t elem0_raw = static_cast<__nv_fp8_storage_t>(fp8x4.__x & 0xFF);
__nv_fp8_storage_t elem1_raw = static_cast<__nv_fp8_storage_t>((fp8x4.__x >> 8) & 0xFF);
__nv_fp8_storage_t elem2_raw = static_cast<__nv_fp8_storage_t>((fp8x4.__x >> 16) & 0xFF);
__nv_fp8_storage_t elem3_raw = static_cast<__nv_fp8_storage_t>((fp8x4.__x >> 24) & 0xFF);
__nv_fp8_e4m3 elem0, elem1, elem2, elem3;
elem0.__x = elem0_raw;
elem1.__x = elem1_raw;
elem2.__x = elem2_raw;
elem3.__x = elem3_raw;
x = T(elem0);
y = T(elem1);
z = T(elem2);
w = T(elem3);
}
}
__host__ __device__ explicit operator __nv_fp8x4_e4m3() const {
__nv_fp8x4_e4m3 result;
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
__nv_fp8x2_e4m3 lo_part(lo_half2), hi_part(hi_half2);
result.__x =
(static_cast<uint32_t>(lo_part.__x) | (static_cast<uint32_t>(hi_part.__x) << 16));
return result;
}
__host__ __device__ explicit half4_bfloat164(const __nv_fp8x4_e5m2& fp8x4) {
__nv_fp8x2_e5m2 lo_part, hi_part;
lo_part.__x = static_cast<__nv_fp8x2_storage_t>(fp8x4.__x & 0xFFFF);
hi_part.__x = static_cast<__nv_fp8x2_storage_t>((fp8x4.__x >> 16) & 0xFFFF);
TVec2 lo_half2 = static_cast<TVec2>(lo_part);
TVec2 hi_half2 = static_cast<TVec2>(hi_part);
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
}
__host__ __device__ explicit operator __nv_fp8x4_e5m2() const {
__nv_fp8x4_e5m2 result;
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
__nv_fp8x2_e5m2 lo_part(lo_half2), hi_part(hi_half2);
result.__x =
(static_cast<uint32_t>(lo_part.__x) | (static_cast<uint32_t>(hi_part.__x) << 16));
return result;
}
__host__ __device__ explicit half4_bfloat164(const __nv_fp8x4_e8m0& fp8x4) {
__nv_fp8x2_e8m0 lo_part, hi_part;
lo_part.__x = static_cast<__nv_fp8x2_storage_t>(fp8x4.__x & 0xFFFF);
hi_part.__x = static_cast<__nv_fp8x2_storage_t>((fp8x4.__x >> 16) & 0xFFFF);
TVec2 lo_half2 = static_cast<TVec2>(lo_part);
TVec2 hi_half2 = static_cast<TVec2>(hi_part);
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
}
__host__ __device__ explicit operator __nv_fp8x4_e8m0() const {
__nv_fp8x4_e8m0 result;
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
__nv_fp8x2_e8m0 lo_part(lo_half2), hi_part(hi_half2);
result.__x =
(static_cast<uint32_t>(lo_part.__x) | (static_cast<uint32_t>(hi_part.__x) << 16));
return result;
}
"""
if "fp4" in tags:
header += R"""
__host__ __device__ explicit half4_bfloat164(const __nv_fp4x4_e2m1& fp4x4) {
if constexpr (std::is_same_v<T, __half>) {
__nv_fp4x2_storage_t lo_part = static_cast<__nv_fp4x2_storage_t>(fp4x4.__x & 0xFF);
__nv_fp4x2_storage_t hi_part = static_cast<__nv_fp4x2_storage_t>((fp4x4.__x >> 8) & 0xFF);
TVec2 lo_half2 = __half2(__nv_cvt_fp4x2_to_halfraw2(lo_part, __NV_E2M1));
TVec2 hi_half2 = __half2(__nv_cvt_fp4x2_to_halfraw2(hi_part, __NV_E2M1));
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
} else {
__nv_fp4_e2m1 elem0, elem1, elem2, elem3;
elem0.__x = static_cast<__nv_fp4_storage_t>(fp4x4.__x & 0xF);
elem1.__x = static_cast<__nv_fp4_storage_t>((fp4x4.__x >> 4) & 0xF);
elem2.__x = static_cast<__nv_fp4_storage_t>((fp4x4.__x >> 8) & 0xF);
elem3.__x = static_cast<__nv_fp4_storage_t>((fp4x4.__x >> 12) & 0xF);
x = T(elem0);
y = T(elem1);
z = T(elem2);
w = T(elem3);
}
}
__host__ __device__ explicit operator __nv_fp4x4_e2m1() const {
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
return __nv_fp4x4_e2m1(lo_half2, hi_half2);
}
"""
header += R"""
};
"""
if "fp16" in tags:
header += R"""
using half4 = half4_bfloat164<__half, __half2>;
__host__ __device__ half4 make_half4(__half x, __half y, __half z, __half w) {
return half4(x, y, z, w);
}
"""
if "bf16" in tags:
header += R"""
using nv_bfloat164 = half4_bfloat164<nv_bfloat16, nv_bfloat162>;
__host__ __device__ nv_bfloat164 make_nv_bfloat164(nv_bfloat16 x, nv_bfloat16 y, nv_bfloat16 z, nv_bfloat16 w) {
return nv_bfloat164(x, y, z, w);
}
__host__ __device__ nv_bfloat162 make_nv_bfloat162(nv_bfloat16 x, nv_bfloat16 y) {
return nv_bfloat162(x, y);
}
""" # noqa: E501
if "fp8" in tags:
header += R"""
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp8x2_e4m3& fp8x2) {
__nv_fp8_e4m3 elem0, elem1;
elem0.__x = static_cast<__nv_fp8_storage_t>(fp8x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp8_storage_t>((fp8x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp8x2_e5m2& fp8x2) {
__nv_fp8_e5m2 elem0, elem1;
elem0.__x = static_cast<__nv_fp8_storage_t>(fp8x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp8_storage_t>((fp8x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp8x2_e8m0& fp8x2) {
__nv_fp8_e8m0 elem0, elem1;
elem0.__x = static_cast<__nv_fp8_storage_t>(fp8x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp8_storage_t>((fp8x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
"""
if "fp8" in tags:
header += R"""
__device__ __nv_fp8x2_e5m2 make___nv_fp8x2_e5m2(__nv_fp8_e5m2 x, __nv_fp8_e5m2 y) {
__nv_fp8x2_e5m2 result;
result.__x = (x.__x) | (y.__x << 8);
return result;
}
__device__ __nv_fp8x4_e5m2 make___nv_fp8x4_e5m2(__nv_fp8_e5m2 a, __nv_fp8_e5m2 b, __nv_fp8_e5m2 c, __nv_fp8_e5m2 d) {
__nv_fp8x4_e5m2 result;
result.__x = (a.__x) | (b.__x << 8) | (c.__x << 16) | (d.__x << 24);
return result;
}
__device__ __nv_fp8x2_e4m3 make___nv_fp8x2_e4m3(__nv_fp8_e4m3 x, __nv_fp8_e4m3 y) {
__nv_fp8x2_e4m3 result;
result.__x = (x.__x) | (y.__x << 8);
return result;
}
__device__ __nv_fp8x4_e4m3 make___nv_fp8x4_e4m3(__nv_fp8_e4m3 a, __nv_fp8_e4m3 b, __nv_fp8_e4m3 c, __nv_fp8_e4m3 d) {
__nv_fp8x4_e4m3 result;
result.__x = (a.__x) | (b.__x << 8) | (c.__x << 16) | (d.__x << 24);
return result;
}
__device__ __nv_fp8x2_e8m0 make___nv_fp8x2_e8m0(__nv_fp8_e8m0 x, __nv_fp8_e8m0 y) {
__nv_fp8x2_e8m0 result;
result.__x = (x.__x) | (y.__x << 8);
return result;
}
__device__ __nv_fp8x4_e8m0 make___nv_fp8x4_e8m0(__nv_fp8_e8m0 a, __nv_fp8_e8m0 b, __nv_fp8_e8m0 c, __nv_fp8_e8m0 d) {
__nv_fp8x4_e8m0 result;
result.__x = (a.__x) | (b.__x << 8) | (c.__x << 16) | (d.__x << 24);
return result;
}
""" # noqa: E501
if "fp4" in tags:
header += R"""
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp4x2_e2m1& fp4x2) {
__nv_fp4_e2m1 elem0, elem1;
elem0.__x = static_cast<__nv_fp4_storage_t>(fp4x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp4_storage_t>((fp4x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
"""
if "int8" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 610)
#include <sm_61_intrinsics.h>
#if defined(__CUDACC_RTC__)
#define __SM_61_INTRINSICS_DECL__ __device__
#else /* !__CUDACC_RTC__ */
#define __SM_61_INTRINSICS_DECL__ static __device__ __inline__
#endif /* __CUDACC_RTC__ */
#ifndef __CUDA_ARCH__
#define __DEF_IF_HOST { }
#else /* !__CUDA_ARCH__ */
#define __DEF_IF_HOST ;
#endif /* __CUDA_ARCH__ */
__SM_61_INTRINSICS_DECL__ int __dp4a(unsigned int srcA, int srcB, int c) __DEF_IF_HOST
__SM_61_INTRINSICS_DECL__ int __dp4a(int srcA, unsigned int srcB, int c) __DEF_IF_HOST
#undef __DEF_IF_HOST
#if !defined(__CUDACC_RTC__) && defined(__CUDA_ARCH__)
__SM_61_INTRINSICS_DECL__ int __dp4a(unsigned int srcA, int srcB, int c) {
int ret;
asm volatile ("dp4a.u32.s32 %0, %1, %2, %3;" : "=r"(ret) : "r"(srcA), "r"(srcB), "r"(c));
return ret;
}
__SM_61_INTRINSICS_DECL__ int __dp4a(int srcA, unsigned int srcB, int c) {
int ret;
asm volatile ("dp4a.s32.u32 %0, %1, %2, %3;" : "=r"(ret) : "r"(srcA), "r"(srcB), "r"(c));
return ret;
}
#endif /* !__CUDACC_RTC__ && defined(__CUDA_ARCH__) */
#undef __SM_61_INTRINSICS_DECL__
#endif // __CUDA_ARCH__ >= 610
"""
if "math_constants" in tags:
header += R"""
#include <math_constants.h>
"""
if "mma" in tags:
header += R"""
#include <mma.h>
"""
if "warp_shuffle" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 700)
#define __shfl_sync(mask, var, lane, width) \
__shfl((var), (lane), (width))
#define __shfl_down_sync(mask, var, offset, width) \
__shfl_down((var), (offset), (width))
#define __shfl_up_sync(mask, var, offset, width) \
__shfl_up((var), (offset), (width))
#endif
"""
if "cast_smem_ptr_to_int" in tags:
header += R"""
__forceinline__ __device__ unsigned int cast_smem_ptr_to_int(const void* const smem_ptr) {
unsigned int smem_int;
asm volatile ("{ .reg .u64 smem_int; cvta.to.shared.u64 smem_int, %1; cvt.u32.u64 %0, smem_int; }"
: "=r"(smem_int) : "l"(smem_ptr));
return smem_int;
}
"""
header += R"""
#if (((__CUDACC_VER_MAJOR__ == 11) && (__CUDACC_VER_MINOR__ >= 4)) || \
(__CUDACC_VER_MAJOR__ > 11))
#define TVM_ENABLE_L2_PREFETCH 1
#else
#define TVM_ENABLE_L2_PREFETCH 0
#endif
#ifdef _WIN32
using uint = unsigned int;
using uchar = unsigned char;
using ushort = unsigned short;
using int64_t = long long;
using uint64_t = unsigned long long;
#else
#define uint unsigned int
#define uchar unsigned char
#define ushort unsigned short
#endif
"""
if "get_tmem_addr" in tags:
header += R"""
__forceinline__ __device__ uint32_t get_tmem_addr(uint32_t idx, int row_offset, int col_offset) {
int col_idx = idx & 0xFFFF;
int row_idx = (idx >> 16) & 0xFFFF;
col_idx += col_offset;
row_idx += row_offset;
col_idx = col_idx & 0xFFFF;
row_idx = row_idx & 0xFFFF;
uint32_t new_idx = (row_idx << 16) | col_idx;
return new_idx;
}
"""
if "get_time_stamp" in tags:
header += R"""
__forceinline__ __device__ uint32_t tvm_builtin_get_timestamp() {
volatile uint32_t ret;
asm volatile("mov.u32 %0, %globaltimer_lo;" : "=r"(ret));
return ret;
}
"""
if "gmma_descriptor" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union GmmaDescriptor
{
HOST_DEVICE constexpr
GmmaDescriptor() noexcept : desc_(0) {}
HOST_DEVICE constexpr
GmmaDescriptor(uint64_t desc) noexcept : desc_(desc) {}
HOST_DEVICE constexpr
GmmaDescriptor(GmmaDescriptor const& t) noexcept : desc_(t.desc_) {}
HOST_DEVICE constexpr
GmmaDescriptor(GmmaDescriptor && t) noexcept : desc_(t.desc_) {}
HOST_DEVICE constexpr
GmmaDescriptor& operator=(GmmaDescriptor const& t) noexcept {
desc_ = t.desc_;
return *this;
}
HOST_DEVICE constexpr
GmmaDescriptor& operator=(GmmaDescriptor && t) noexcept {
desc_ = t.desc_;
return *this;
}
uint64_t desc_;
uint32_t reg32_[2];
uint16_t reg16_[4];
// Bitfield implementation avoids the need for shifts in assignment
struct {
// start_address, bit [0,14), 4LSB not included
uint16_t start_address_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// leading dimension byte offset, bit [16,30), 4LSB not included
// For N: This is the stride from the first col to the second col of the 8x2 brick in INTERLEAVED
// Unused for all SWIZZLE_* layouts (and assumed to be 1)
// For T: This is the stride from the first 8 rows to the next 8 rows.
uint16_t leading_byte_offset_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// stride dimension byte offset, bit [32,46), 4LSB not included
// For N: This is the stride from the first 8 rows to the next 8 rows.
// For T: This is the stride fro mthe first 8 cols to the next 8 cols.
uint16_t stride_byte_offset_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// base_offset, bit [49,52)
// Valid only for SWIZZLE_128B and SWIZZLE_64B
uint8_t : 1, base_offset_ : 3, : 4; // 1 bit unused, 3 bits [1,4), 4 bits unused
// layout type, bit [62,64)
// SWIZZLE_NONE = 0, SWIZZLE_32B = 3, SWIZZLE_64B = 2, SWIZZLE_128B = 1
uint8_t : 6, layout_type_ : 2; // 6 bits unused, 2 bits [6,8)
} bitfield;
// Decay to a uint64_t
HOST_DEVICE constexpr
operator uint64_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "smem_descriptor" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union SmemDescriptor
{
uint64_t desc_ = 0;
// Bitfield implementation avoids the need for shifts in assignment
struct {
// start_address, bit [0,14), 4LSB not included
uint16_t start_address_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// leading dimension byte offset, bit [16,30), 4LSB not included
uint16_t leading_byte_offset_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// stride dimension byte offset, bit [32,46), 4LSB not included
uint16_t stride_byte_offset_ : 14, version_ : 2; // 14 bits [0,14), 2 bits [14,16)
// base_offset, bit [49,52). leading_byte_offset_mode, bit [52,53).
uint8_t : 1, base_offset_ : 3, lbo_mode_ : 1, : 3; // 1 bit unused, 3 bits [1,4), 1 bit [4,5), 3 bits unused
// layout type, bit [61,64), SWIZZLE_NONE matrix descriptor = 0, SWIZZLE_128B matrix descriptor = 2, SWIZZLE_64B descriptor = 4, SWIZZLE_32B descriptor = 6, SWIZZLE_128B_BASE32B = 1, N/A = 3, N/A = 5, N/A = 7
uint8_t : 5, layout_type_ : 3; // 6 bits unused, 3 bits [5,8)
};
// Seperate the field, as we may only update one part of desc
struct {
uint32_t lo;
uint32_t hi;
};
// Decay to a uint64_t
HOST_DEVICE constexpr
operator uint64_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "instr_descriptor" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union InstrDescriptor
{
uint32_t desc_;
struct {
// Bitfield implementation avoids the need for shifts in assignment
uint16_t sparse_id2_ : 2, // bit [ 0, 2) : Sparse meta data id2
sparse_flag_ : 1, // bit [ 2, 3) : 0 = dense. 1 = sparse. 1 value valid only for F32F16/S8/MXF8F6F4
saturate_ : 1, // bit [ 3, 4) : 0 = no saturate. 1 = saturate. 1 value valid only for S8
c_format_ : 2, // bit [ 4, 6) : 0 = F16. 1 = F32, 2 = S32
: 1, //
a_format_ : 3, // bit [ 7,10) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. Boolean MMA: 0 Boolean
b_format_ : 3, // bit [10,13) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. Boolean MMA: 0 Boolean
a_negate_ : 1, // bit [13,14) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
b_negate_ : 1, // bit [14,15) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
a_major_ : 1; // bit [15,16) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
uint16_t b_major_ : 1, // bit [16,17) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
n_dim_ : 6, // bit [17,23) : 3 LSBs not included. Valid values range from 1 (N=8) to 32 (N=256). All values are not valid for all instruction formats
: 1, //
m_dim_ : 5, // bit [24,29) : 4 LSBs not included. Valid values are: 4 (M=64), 8 (M=128), 16 (M=256)
: 1, //
max_shift_ : 2; // bit [30,32) : Maximum shift for WS instruction. Encoded as follows: 0 = no shift, 1 = maximum shift of 8, 2 = maximum shift of 16, 3 = maximum shift of 32.
};
// Decay to a uint32_t
HOST_DEVICE constexpr explicit
operator uint32_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "instr_descriptor_block_scaled" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union InstrDescriptorBlockScaled
{
uint32_t desc_;
struct {
// Bitfield implementation avoids the need for shifts in assignment
uint16_t sparse_id2_ : 2, // bit [ 0, 2) : Sparse meta data id2
sparse_flag_ : 1, // bit [ 2, 3) : 0 = dense. 1 = sparse. 1 value valid only for F32F16/S8/MXF8F6F4
: 1, //
b_sf_id_ : 2, // bit [ 4, 6) : Matrix B Scale Factor ID
: 1, //
a_format_ : 3, // bit [ 7, 9) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. BMMA: 0 Boolean
b_format_ : 3, // bit [10,12) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. BMMA: 0 Boolean
a_negate_ : 1, // bit [13,14) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
b_negate_ : 1, // bit [14,15) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
a_major_ : 1; // bit [15,16) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
uint16_t b_major_ : 1, // bit [16,17) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
n_dim_ : 6, // bit [17,23) : 3 LSBs not included. Valid values range from 1 (N=8) to 32 (N=256). All values are not valid for all instruction formats
scale_format_ : 1, // bit [23,24) : 0=E4M3, 1=E8M0
m_dim_ : 5, // bit [24,29) : 4 LSBs not included. Valid values are: 4 (M=64), 8 (M=128), 16 (M=256)
a_sf_id_ : 2, // bit [29,31) : Matrix A Scale Factor ID
: 1; //
};
// Decay to a uint32_t
HOST_DEVICE constexpr
operator uint32_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "elect_one_sync" in tags:
header += R"""
__forceinline__ __device__ uint32_t tvm_builtin_elect_one_sync() {{
uint32_t pred = 0;
uint32_t laneid = 0;
asm volatile(
"{\n"
".reg .b32 %%rx;\n"
".reg .pred %%px;\n"
" elect.sync %%rx|%%px, %2;\n"
"@%%px mov.s32 %1, 1;\n"
" mov.s32 %0, %%rx;\n"
"}\n"
: "+r"(laneid), "+r"(pred)
: "r"(0xFFFFFFFF));
return pred;
}}
"""
return header
@@ -0,0 +1,501 @@
# 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
"""Math intrinsics.
PTX side:
* ``add{.rnd}{.ftz}.f32x2`` / ``sub`` / ``mul`` / ``fma`` — packed f32x2.
* ``ex2.approx.ftz.f32`` / ``rcp.approx.ftz.f32`` — special functions.
* ``max.f32`` / ``min.f32`` — 3-operand reduction form.
CUDA side:
* warp / CTA reductions (templated butterfly shuffle-XOR).
"""
from tvm.backend.cuda.op import cuda_func_call
from ._schema import device_intrinsic
from .registry import register_codegen
from .utils import parse_str, validate_power_of_two_range
# =============================================================================
# Packed f32x2 arithmetic — `add{.rnd}{.ftz}.f32x2 d, a, b ;` and friends.
# Inputs are packed into a `.b64` register (low half = elem 0, high half =
# elem 1); the body packs/unpacks via ``make_float2`` + ``reinterpret_cast``.
# =============================================================================
# PTX add/sub/mul/fma over (f32 | f32x2 | f64), DPS form.
# add{.rnd}{.ftz}{.sat}.f32 [d], a, b
# add{.rnd}{.ftz}.f32x2 [d], a, b (a,b are packed-as-u64)
# add{.rnd}.f64 [d], a, b
# (sub / mul same shape; fma adds a `c` operand)
# Inputs a/b/c are register operands (scalar fp32 / packed u64 / scalar fp64).
# Result is written through `d` (a pointer).
_PACKED_ROUNDING = ("rz", "rn", "rm", "rp")
# Per-dtype operand types and asm constraints.
# - c_in: C type of input register operand (matches PTX register type)
# - out_cast: pointer cast applied at d_addr (callers may pass float*/double*/...)
# - in_cstr / out_cstr: GCC asm constraint letter
_DTYPE_INFO = {
"f32": {"c_in": "float", "out_cast": "float*", "in_cstr": "f", "out_cstr": "f"},
"f32x2": {
"c_in": "unsigned long long",
"out_cast": "uint64_t*",
"in_cstr": "l",
"out_cstr": "l",
},
"f64": {"c_in": "double", "out_cast": "double*", "in_cstr": "d", "out_cstr": "d"},
}
def _ptx_arith_modifier_string(dtype, rounding, ftz, sat):
"""Build the `.rnd.ftz.sat` modifier substring + name suffix."""
rnd = parse_str(rounding)
assert rnd in _PACKED_ROUNDING, f"invalid rounding {rnd!r}, expected one of {_PACKED_ROUNDING}"
ftz_b = bool(int(ftz)) if hasattr(ftz, "value") else bool(ftz)
sat_b = bool(int(sat)) if hasattr(sat, "value") else bool(sat)
if dtype == "f64" and (ftz_b or sat_b):
raise ValueError("PTX <op>.f64 does not accept .ftz or .sat")
if dtype == "f32x2" and sat_b:
raise ValueError("PTX <op>.f32x2 does not accept .sat")
mod = f".{rnd}"
if ftz_b:
mod += ".ftz"
if sat_b:
mod += ".sat"
name_suffix = f"_{rnd}"
if ftz_b:
name_suffix += "_ftz"
if sat_b:
name_suffix += "_sat"
return mod, name_suffix
def _ptx_binary_arith_parts(op, dtype):
"""Return (name_fn, sig, body_fn) for ptx_{op}_{dtype} binary form."""
info = _DTYPE_INFO[dtype]
# Destination is ``void*`` so callers can pass any element-type pointer
# (float* / double* / uint64_t*); body reinterpret-casts to the right type.
sig = f"(void* d, {info['c_in']} a, {info['c_in']} b)"
def _name(d, a, b, rounding, ftz, sat):
_, suf = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return f"tvm_builtin_ptx_{op}_{dtype}{suf}"
out_c = info["out_cstr"]
in_c = info["in_cstr"]
out_cast = info["out_cast"]
def _body(d, a, b, rounding, ftz, sat):
mod, _ = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return (
f' asm volatile("{op}{mod}.{dtype} %0, %1, %2;"\n'
f' : "={out_c}"(*reinterpret_cast<{out_cast}>(d))\n'
f' : "{in_c}"(a), "{in_c}"(b));'
)
return _name, sig, _body
def _ptx_fma_parts(dtype):
"""Return (name_fn, sig, body_fn) for ptx_fma_{dtype}."""
info = _DTYPE_INFO[dtype]
sig = f"(void* d, {info['c_in']} a, {info['c_in']} b, {info['c_in']} c)"
def _name(d, a, b, c, rounding, ftz, sat):
_, suf = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return f"tvm_builtin_ptx_fma_{dtype}{suf}"
out_c = info["out_cstr"]
in_c = info["in_cstr"]
out_cast = info["out_cast"]
def _body(d, a, b, c, rounding, ftz, sat):
mod, _ = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return (
f' asm volatile("fma{mod}.{dtype} %0, %1, %2, %3;"\n'
f' : "={out_c}"(*reinterpret_cast<{out_cast}>(d))\n'
f' : "{in_c}"(a), "{in_c}"(b), "{in_c}"(c));'
)
return _name, sig, _body
# Register 12 ops: {add, sub, mul, fma} x {f32, f32x2, f64}.
for _dtype in ("f32", "f32x2", "f64"):
for _op in ("add", "sub", "mul"):
_name_fn, _sig, _body_fn = _ptx_binary_arith_parts(_op, _dtype)
device_intrinsic(
f"ptx_{_op}_{_dtype}",
n_attrs=3, # rounding, ftz, sat
helper_name=_name_fn,
c_signature=_sig,
body=_body_fn,
)
_name_fn, _sig, _body_fn = _ptx_fma_parts(_dtype)
device_intrinsic(
f"ptx_fma_{_dtype}",
n_attrs=3,
helper_name=_name_fn,
c_signature=_sig,
body=_body_fn,
)
del _dtype, _op, _name_fn, _sig, _body_fn
# =============================================================================
# ex2.approx.ftz.f32 / rcp.approx.ftz.f32 — 1 form each.
# =============================================================================
device_intrinsic(
"ptx_exp2",
c_signature="(float x)",
return_type="float",
body=(
" float result;\n"
' asm volatile("ex2.approx.ftz.f32 %0, %1;" : "=f"(result) : "f"(x));\n'
" return result;"
),
)
device_intrinsic(
"ptx_rcp",
c_signature="(float x)",
return_type="float",
body=(
" float result;\n"
' asm volatile("rcp.approx.ftz.f32 %0, %1;" : "=f"(result) : "f"(x));\n'
" return result;"
),
)
# =============================================================================
# 3-operand max.f32 / min.f32 — the f32, 3-operand form-table entry of the
# redux/reduction-style fp32 max/min ops.
# =============================================================================
_ABC_SIG = "(float a, float b, float c)"
device_intrinsic(
"ptx_reduce3_max_f32",
c_signature=_ABC_SIG,
return_type="float",
body=(
" float result;\n"
' asm volatile("max.f32 %0, %1, %2, %3;"\n'
' : "=f"(result) : "f"(a), "f"(b), "f"(c));\n'
" return result;"
),
)
device_intrinsic(
"ptx_reduce3_min_f32",
c_signature=_ABC_SIG,
return_type="float",
body=(
" float result;\n"
' asm volatile("min.f32 %0, %1, %2, %3;"\n'
' : "=f"(result) : "f"(a), "f"(b), "f"(c));\n'
" return result;"
),
)
_BINARY_F32_SIG = "(float a, float b)"
def _ptx_max_f32_body(a, b, ftz, nan):
ftz_b = bool(int(ftz)) if hasattr(ftz, "value") else bool(ftz)
nan_b = bool(int(nan)) if hasattr(nan, "value") else bool(nan)
ftz_suffix = ".ftz" if ftz_b else ""
nan_suffix = ".NaN" if nan_b else ""
return (
" float result;\n"
f' asm volatile("max{ftz_suffix}{nan_suffix}.f32 %0, %1, %2;"\n'
' : "=f"(result) : "f"(a), "f"(b));\n'
" return result;"
)
def _ptx_max_f32_name(a, b, ftz, nan):
ftz_b = bool(int(ftz)) if hasattr(ftz, "value") else bool(ftz)
nan_b = bool(int(nan)) if hasattr(nan, "value") else bool(nan)
suffix = ""
if ftz_b:
suffix += "_ftz"
if nan_b:
suffix += "_nan"
return f"tvm_builtin_ptx_max_f32{suffix}"
device_intrinsic(
"ptx_max_f32",
n_attrs=2,
helper_name=_ptx_max_f32_name,
c_signature=_BINARY_F32_SIG,
return_type="float",
body=_ptx_max_f32_body,
)
# =============================================================================
# CUDA-side warp / CTA reductions (templated butterfly shuffle-XOR).
# Emitted directly via ``cuda_func_call`` — the helper signature uses a
# single template parameter ``T`` for both arg and return, which doesn't
# match the operand-driven C signature pattern.
# =============================================================================
# (accumulation expression, identity value for cross-warp padding)
_OP_TABLE = {
"sum": ("val += shuffled;", "T(0)"),
"max": ("val = max(val, shuffled);", "-INFINITY"),
"min": ("val = min(val, shuffled);", "INFINITY"),
}
def _validate_op(op_str, context):
if op_str not in _OP_TABLE:
raise ValueError(f"Unsupported {context} op '{op_str}', expected one of {list(_OP_TABLE)}")
return _OP_TABLE[op_str]
def _warp_reduce_source(func_name, width_int, step_expr):
return (
f"\ntemplate <typename T>\n"
f"__forceinline__ __device__ T {func_name}(T val) {{\n"
f" #pragma unroll\n"
f" for (int mask = {width_int} >> 1; mask > 0; mask >>= 1) {{\n"
" T shuffled = __shfl_xor_sync(0xFFFFFFFF, val, mask);\n"
f" {step_expr}\n"
" }\n"
" return val;\n"
"}\n"
)
@register_codegen("cuda_warp_reduce")
def codegen_cuda_warp_reduce(value, op, width):
op_str = parse_str(op)
width_int = validate_power_of_two_range(width, 2, 32, "warp_reduce width")
step_expr, _ = _validate_op(op_str, "warp_reduce")
func_name = f"tvm_builtin_cuda_warp_reduce_{op_str}_{width_int}"
source_code = _warp_reduce_source(func_name, width_int, step_expr)
return cuda_func_call(func_name, value, source_code=source_code, return_type=value.ty)
@register_codegen("cuda_cta_reduce")
def codegen_cuda_cta_reduce(value, op, num_warps, scratch):
op_str = parse_str(op)
nw = validate_power_of_two_range(num_warps, 1, 32, "cta_reduce num_warps")
step_expr, identity = _validate_op(op_str, "cta_reduce")
warp_reduce_name = f"tvm_builtin_cuda_warp_reduce_{op_str}_32"
func_name = f"tvm_builtin_cuda_cta_reduce_{op_str}_{nw}"
cta_body = (
f"{_warp_reduce_source(warp_reduce_name, 32, step_expr)}"
"template <typename T>\n"
f"__forceinline__ __device__ T {func_name}(T val, void* scratch_raw) {{\n"
" T* scratch = reinterpret_cast<T*>(scratch_raw);\n"
f" val = {warp_reduce_name}(val);\n"
" int tid = threadIdx.x + threadIdx.y * blockDim.x"
" + threadIdx.z * blockDim.x * blockDim.y;\n"
" int warp_id = tid / 32;\n"
" int lane_id = tid % 32;\n"
" if (lane_id == 0) scratch[warp_id] = val;\n"
" __syncthreads();\n"
" if (warp_id == 0) {\n"
f" T partial = (lane_id < {nw}) ? scratch[lane_id] : {identity};\n"
f" partial = {warp_reduce_name}(partial);\n"
" if (lane_id == 0) scratch[0] = partial;\n"
" }\n"
" __syncthreads();\n"
" return scratch[0];\n"
"}\n"
)
return cuda_func_call(func_name, value, scratch, source_code=cta_body, return_type=value.ty)
# =============================================================================
# Additional FP8/BF16 packing, integer, and activation helpers.
# =============================================================================
# PTX integer bit-search form:
# fns.b32 d, mask, base, offset;
device_intrinsic(
"ptx_fns_b32",
helper_name="tvm_builtin_ptx_fns_b32",
c_signature="(unsigned int mask, unsigned int base, int offset)",
return_type="unsigned int",
body=(
" unsigned int ret;\n"
' asm("fns.b32 %0, %1, %2, %3;" : "=r"(ret) : "r"(mask), "r"(base), "r"(offset));\n'
" return ret;"
),
)
device_intrinsic(
"cuda_ffs_u32",
helper_name="tvm_builtin_ffs_u32",
c_signature="(unsigned int value)",
return_type="int",
body=" return __ffs(value);",
)
device_intrinsic(
"ptx_add_rn_f32_bf16",
helper_name="tvm_builtin_ptx_add_rn_f32_bf16",
c_signature="(float acc, unsigned short x)",
return_type="float",
body=(' asm("add.rn.f32.bf16 %0, %1, %0;" : "+f"(acc) : "h"(x));\n return acc;'),
)
device_intrinsic(
"cuda_make_float2",
helper_name="tvm_builtin_make_float2",
c_signature="(float x, float y)",
return_type="unsigned long long",
body=(
" float2 value = make_float2(x, y);\n"
" return *reinterpret_cast<unsigned long long*>(&value);"
),
)
device_intrinsic(
"cuda_float2_x",
helper_name="tvm_builtin_float2_x",
c_signature="(unsigned long long packed)",
return_type="float",
body=(" float2 value = *reinterpret_cast<float2*>(&packed);\n return value.x;"),
)
device_intrinsic(
"cuda_float2_y",
helper_name="tvm_builtin_float2_y",
c_signature="(unsigned long long packed)",
return_type="float",
body=(" float2 value = *reinterpret_cast<float2*>(&packed);\n return value.y;"),
)
device_intrinsic(
"cuda_fmul2_rn",
helper_name="tvm_builtin_fmul2_rn",
c_signature="(unsigned long long a, unsigned long long b)",
return_type="unsigned long long",
body=(
" float2 lhs = *reinterpret_cast<float2*>(&a);\n"
" float2 rhs = *reinterpret_cast<float2*>(&b);\n"
" float2 result = __fmul2_rn(lhs, rhs);\n"
" return *reinterpret_cast<unsigned long long*>(&result);"
),
)
device_intrinsic(
"cuda_fadd2_rn",
helper_name="tvm_builtin_fadd2_rn",
c_signature="(unsigned long long a, unsigned long long b)",
return_type="unsigned long long",
body=(
" float2 lhs = *reinterpret_cast<float2*>(&a);\n"
" float2 rhs = *reinterpret_cast<float2*>(&b);\n"
" float2 result = __fadd2_rn(lhs, rhs);\n"
" return *reinterpret_cast<unsigned long long*>(&result);"
),
)
device_intrinsic(
"cuda_float22bfloat162_rn",
helper_name="tvm_builtin_float22bfloat162_rn",
c_signature="(float x, float y)",
return_type="unsigned int",
body=(
" __nv_bfloat162 value = __float22bfloat162_rn(make_float2(x, y));\n"
" return *reinterpret_cast<unsigned int*>(&value);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_float22bfloat162_rn_from_float2",
helper_name="tvm_builtin_float22bfloat162_rn_from_float2",
c_signature="(unsigned long long packed)",
return_type="unsigned int",
body=(
" float2 value = *reinterpret_cast<float2*>(&packed);\n"
" __nv_bfloat162 result = __float22bfloat162_rn(value);\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_bfloat1622float2",
helper_name="tvm_builtin_bfloat1622float2",
c_signature="(unsigned int packed)",
return_type="unsigned long long",
body=(
" __nv_bfloat162 value;\n"
" *reinterpret_cast<unsigned int*>(&value) = packed;\n"
" float2 result = __bfloat1622float2(value);\n"
" return *reinterpret_cast<unsigned long long*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_hmin2",
helper_name="tvm_builtin_hmin2",
c_signature="(unsigned int a, unsigned int b)",
return_type="unsigned int",
body=(
" __nv_bfloat162 lhs;\n"
" __nv_bfloat162 rhs;\n"
" *reinterpret_cast<unsigned int*>(&lhs) = a;\n"
" *reinterpret_cast<unsigned int*>(&rhs) = b;\n"
" __nv_bfloat162 result = __hmin2(lhs, rhs);\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_hmax2",
helper_name="tvm_builtin_hmax2",
c_signature="(unsigned int a, unsigned int b)",
return_type="unsigned int",
body=(
" __nv_bfloat162 lhs;\n"
" __nv_bfloat162 rhs;\n"
" *reinterpret_cast<unsigned int*>(&lhs) = a;\n"
" *reinterpret_cast<unsigned int*>(&rhs) = b;\n"
" __nv_bfloat162 result = __hmax2(lhs, rhs);\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_fp8x4_e4m3_from_float4",
helper_name="tvm_builtin_fp8x4_e4m3_from_float4",
c_signature="(float x, float y, float z, float w)",
return_type="unsigned int",
body=(
" __nv_fp8x4_e4m3 result = __nv_fp8x4_e4m3(make_float4(x, y, z, w));\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("fp8",),
)
@@ -0,0 +1,955 @@
# 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.
# ruff: noqa: E501
# pylint: disable=redefined-builtin, invalid-name, too-many-arguments
"""Memory ops (load / store / copy / atomic / address conversion / type punning).
PTX side:
* ``ld.acquire.scope{.ss}.type`` scalar load forms.
* ``ld.volatile{.ss}.type`` scalar load forms.
* Legacy ``ld.global.acquire.gpu`` / ``ld.global.cg`` result-argument helper.
* ``mapa.u64`` — map a SMEM ptr to a peer CTA's SMEM in the cluster.
CUDA side:
* ``__ldg`` (cache-as-read-only load).
* Templated ``atomicAdd`` / ``atomicCAS``.
* half↔float type-punned conversions (single, packed, batch-of-8).
* ``__cvta_generic_to_shared`` and ``cluster_addr → shared u32`` casts.
"""
from tvm import DataType
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
# =============================================================================
# __ldg — templated read-only cached load; ``T`` resolved at call time from
# the ``dtype`` argument. Hand-written because the helper signature uses a
# template parameter for both arg and return.
# =============================================================================
@register_codegen("cuda_ldg")
def codegen_cuda_ldg(addr, dtype):
dtype = DataType(parse_str(dtype))
func_name = "tvm_builtin_cuda_ldg"
source_code = f"""
template <typename T>
__forceinline__ __device__ T {func_name}(T* src) {{
return __ldg(src);
}}
"""
return cuda_func_call(func_name, addr, source_code=source_code, return_type=dtype)
# Shared PTX scalar type metadata (ld/st/red/atom).
_PTX_SCALAR_TYPE_INFO = {
"b8": ("unsigned int", "r", "uint32"),
"u8": ("unsigned int", "r", "uint32"),
"s8": ("int", "r", "int32"),
"b16": ("unsigned short", "h", "uint16"),
"u16": ("unsigned short", "h", "uint16"),
"s16": ("short", "h", "int16"),
"b32": ("unsigned int", "r", "uint32"),
"u32": ("unsigned int", "r", "uint32"),
"s32": ("int", "r", "int32"),
"b64": ("unsigned long long", "l", "uint64"),
"u64": ("unsigned long long", "l", "uint64"),
"s64": ("long long", "l", "int64"),
"f32": ("float", "f", "float32"),
"f64": ("double", "d", "float64"),
}
_PTX_LD_TYPE_RETURNS = {
"b32": {"uint32": "unsigned int", "int32": "int"},
"b64": {"uint64": "unsigned long long", "int64": "long long"},
}
_PTX_VEC_STORE_TYPE = {
16: "uint4",
8: "uint2",
4: "unsigned int",
2: "unsigned short",
1: "unsigned char",
}
def _safe_attr(value):
return parse_str(value).replace("::", "_").replace(".", "_")
def _dot(value):
value = parse_str(value)
return f".{value}" if value else ""
def _cache_suffix(cache):
return ".L2::cache_hint" if cache else ""
def _type_info(ptx_type):
ptx_type = parse_str(ptx_type)
if ptx_type not in _PTX_SCALAR_TYPE_INFO:
raise ValueError(
f"Unsupported PTX scalar type {ptx_type!r}; expected {sorted(_PTX_SCALAR_TYPE_INFO)}"
)
return (ptx_type, *_PTX_SCALAR_TYPE_INFO[ptx_type])
# =============================================================================
# PTX ld forms (ISA table entries registered via ``ptx_ld`` and siblings):
# ld{.weak}{.ss}{.cop}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld{.weak}{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld.volatile{.ss}{.level::prefetch_size}{.vec}.type d, [a];
# ld.relaxed.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld.acquire.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld.mmio.sem.sys{.global}.type d, [a];
# =============================================================================
_PTX_LD_SCOPES = {"cta", "cluster", "gpu", "sys"}
_PTX_LD_SPACES = {"global", "shared", "shared::cta", "shared::cluster", "local"}
_PTX_LD_VOLATILE_SPACES = _PTX_LD_SPACES | {"const"}
_PTX_LD_WEAK_SPACES = _PTX_LD_SPACES | {"const", "param::entry", "param::func"}
_PTX_LD_COPS = {"", "ca", "cg", "cs", "lu", "cv"}
_PTX_VEC = {"", "v2", "v4", "v8"}
_PTX_L1_EVICT = {
"",
"L1::evict_normal",
"L1::evict_unchanged",
"L1::evict_first",
"L1::evict_last",
"L1::no_allocate",
}
_PTX_L2_EVICT = {"", "L2::evict_normal", "L2::evict_first", "L2::evict_last"}
_PTX_PREFETCH = {"", "L2::64B", "L2::128B", "L2::256B"}
def _bool_attr(value):
return bool(int(value)) if hasattr(value, "value") else bool(value)
def _parse_ld_attrs(return_dtype, ptx_type, scope=None, space="global"):
return_dtype = parse_str(return_dtype)
ptx_type = parse_str(ptx_type)
scope = None if scope is None else parse_str(scope)
space = parse_str(space)
ptx_type, _ptx, constraint, default_tvm = _type_info(ptx_type)
if ptx_type in _PTX_LD_TYPE_RETURNS:
returns = _PTX_LD_TYPE_RETURNS[ptx_type]
if return_dtype not in returns:
raise ValueError(
f"PTX ld type {ptx_type!r} cannot return TVM dtype {return_dtype!r}; "
f"expected one of {sorted(returns)}"
)
c_type = returns[return_dtype]
else:
if return_dtype != default_tvm:
raise ValueError(
f"PTX ld type {ptx_type!r} cannot return TVM dtype {return_dtype!r}; "
f"expected {default_tvm!r}"
)
c_type = _ptx
if scope is not None and scope not in _PTX_LD_SCOPES:
raise ValueError(
f"Unsupported PTX ld scope {scope!r}; expected one of {sorted(_PTX_LD_SCOPES)}"
)
return return_dtype, ptx_type, scope, space, c_type, constraint
def _validate_ld_space(space: str, allowed: set[str]) -> None:
if space not in allowed:
raise ValueError(
f"Unsupported PTX ld state space {space!r}; expected one of {sorted(allowed)}"
)
def _ptx_level_suffix(has_cache, l1_evict, l2_evict, prefetch_size):
suffix = _cache_suffix("cache" if has_cache else "")
l1_evict = parse_str(l1_evict)
l2_evict = parse_str(l2_evict)
prefetch_size = parse_str(prefetch_size)
if l1_evict:
suffix += f".{l1_evict}"
if l2_evict:
suffix += f".{l2_evict}"
if prefetch_size:
suffix += f".{prefetch_size}"
return suffix
def _ptx_shared_addr(space, ptr_name="address"):
if parse_str(space).startswith("shared"):
return (
f" unsigned int addr = (unsigned int)__cvta_generic_to_shared({ptr_name});\n",
'"r"(addr)',
)
return "", f'"l"({ptr_name})'
def _ptx_ld_vec_store(num_bytes, vec_len, ptx_type):
if ptx_type == "u8" and vec_len == 1:
return " *reinterpret_cast<unsigned char*>(dst_ptr) = static_cast<unsigned char>(r0);"
store_type = _PTX_VEC_STORE_TYPE[num_bytes]
if vec_len > 1:
return (
f" *reinterpret_cast<{store_type}*>(dst_ptr) = "
+ "{"
+ ", ".join(f"r{i}" for i in range(vec_len))
+ "};"
)
return f" *reinterpret_cast<{store_type}*>(dst_ptr) = r0;"
def _ptx_ld_form_parts(form, attr_args):
if form == "weak":
(
return_dtype,
weak,
space,
cop,
vec,
ptx_type,
has_cache_hint,
to_dst,
l1_evict,
l2_evict,
prefetch_size,
) = attr_args
sem, scope = "", ""
elif form == "relaxed":
(
return_dtype,
scope,
space,
vec,
ptx_type,
has_cache_hint,
to_dst,
l1_evict,
l2_evict,
prefetch_size,
) = attr_args
sem, weak, cop = "", False, ""
elif form == "acquire":
(
return_dtype,
scope,
space,
vec,
ptx_type,
has_cache_hint,
to_dst,
l1_evict,
l2_evict,
prefetch_size,
) = attr_args
sem, weak, cop = "", False, ""
elif form == "volatile":
return_dtype, space, vec, ptx_type, to_dst, prefetch_size = attr_args
sem, scope, weak, cop = "", "", False, ""
has_cache_hint, l1_evict, l2_evict = False, "", ""
elif form == "mmio":
return_dtype, sem, scope, space, ptx_type, to_dst = attr_args
weak, cop, vec = False, "", ""
has_cache_hint, l1_evict, l2_evict, prefetch_size = False, "", "", ""
else:
raise ValueError(f"unknown ld form {form!r}")
return_dtype, ptx_type, scope, space, c_type, constraint = _parse_ld_attrs(
return_dtype, ptx_type, scope if form in ("relaxed", "acquire") else None, space
)
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
cop = parse_str(cop)
vec = parse_str(vec)
l1_evict = parse_str(l1_evict)
l2_evict = parse_str(l2_evict)
prefetch_size = parse_str(prefetch_size)
weak = _bool_attr(weak)
has_cache = _bool_attr(has_cache_hint)
to_dst = _bool_attr(to_dst)
if cop and cop not in _PTX_LD_COPS:
raise ValueError(f"Unsupported PTX ld cache operation {cop!r}")
if vec and vec not in _PTX_VEC:
raise ValueError(f"Unsupported PTX ld vector modifier {vec!r}")
if l1_evict and l1_evict not in _PTX_L1_EVICT:
raise ValueError(f"Unsupported PTX ld L1 eviction {l1_evict!r}")
if l2_evict and l2_evict not in _PTX_L2_EVICT:
raise ValueError(f"Unsupported PTX ld L2 eviction {l2_evict!r}")
if prefetch_size and prefetch_size not in _PTX_PREFETCH:
raise ValueError(f"Unsupported PTX ld prefetch size {prefetch_size!r}")
if form == "mmio":
if sem not in ("acquire", "relaxed") or scope != "sys" or space != "global":
raise ValueError("ld.mmio requires sem in {acquire, relaxed}, scope=sys, space=global")
prefix = f"ld.mmio.{sem}.{scope}"
elif form == "relaxed":
if not scope:
raise ValueError("ld.relaxed requires scope")
_validate_ld_space(space, _PTX_LD_SPACES)
prefix = f"ld.relaxed.{scope}{_dot(space)}"
elif form == "acquire":
if not scope:
raise ValueError("ld.acquire requires scope")
_validate_ld_space(space, _PTX_LD_SPACES)
prefix = f"ld.acquire.{scope}{_dot(space)}"
elif form == "volatile":
_validate_ld_space(space, _PTX_LD_VOLATILE_SPACES)
prefix = f"ld.volatile{_dot(space)}"
else:
_validate_ld_space(space, _PTX_LD_WEAK_SPACES)
prefix = f"ld{'.weak' if weak else ''}{_dot(space)}{_dot(cop)}"
level = _ptx_level_suffix(has_cache, l1_evict, l2_evict, prefetch_size)
vec_len = int(vec[1:]) if vec else 1
if vec and not to_dst:
raise ValueError("vector ld requires to_dst")
elem_bytes = (
8
if ptx_type.endswith("64")
else 2
if ptx_type in ("u16", "s16", "b16")
else 1
if ptx_type in ("u8", "s8", "b8")
else 4
)
num_bytes = vec_len * elem_bytes if vec else elem_bytes
name_parts = [
"tvm_builtin_ptx_ld",
form if form != "weak" else ("weak" if weak else "plain"),
]
if sem:
name_parts.append(_safe_attr(sem))
if scope:
name_parts.append(_safe_attr(scope))
name_parts.extend(
[
_safe_attr(space),
_safe_attr(cop) if cop else "",
_safe_attr(vec) if vec else "",
ptx_type,
return_dtype if not to_dst else "to_dst",
]
)
if has_cache:
name_parts.append("cache_hint")
if l1_evict:
name_parts.append(_safe_attr(l1_evict))
if l2_evict:
name_parts.append(_safe_attr(l2_evict))
if prefetch_size:
name_parts.append(_safe_attr(prefetch_size))
name = "_".join(p for p in name_parts if p)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl, addr_operand = _ptx_shared_addr(space, "src_ptr" if to_dst else "address")
if to_dst:
reg_decls = "".join(f" {c_type} r{i};\n" for i in range(vec_len))
if vec_len > 1:
out_slot = "{" + ", ".join(f"%{i}" for i in range(vec_len)) + "}"
out_constraints = ", ".join(f'"={constraint}"(r{i})' for i in range(vec_len))
addr_idx = vec_len
else:
out_slot = "%0"
out_constraints = f'"={constraint}"(r0)'
addr_idx = 1
cache_slot = f", %{addr_idx + 1}" if has_cache else ""
instr = f"{prefix}{level}{_dot(vec)}.{ptx_type}"
body = (
f"{addr_decl}{reg_decls}"
f' asm volatile("{instr} {out_slot}, [%{addr_idx}]{cache_slot};"\n'
f" : {out_constraints}\n"
f" : {addr_operand}{cache_operand});\n"
f"{_ptx_ld_vec_store(num_bytes, vec_len, ptx_type)}"
)
return (
name,
"(void* dst_ptr, void* src_ptr, unsigned long long cache_policy)",
"void",
"",
body,
)
cache_slot = ", %2" if has_cache else ""
instr = f"{prefix}{level}{_dot(vec)}.{ptx_type}"
body = (
f" {c_type} ret;\n"
f"{addr_decl}"
f' asm volatile("{instr} %0, [%1]{cache_slot};"\n'
f' : "={constraint}"(ret)\n'
f" : {addr_operand}{cache_operand});\n"
" return ret;"
)
sig = (
"(void* address, unsigned long long cache_policy)" if form == "weak" else "(void* address)"
)
return name, sig, c_type, return_dtype, body
def _register_ptx_ld(op_name, form, n_attrs):
def _parts(*args):
return _ptx_ld_form_parts(form, args[-n_attrs:])
device_intrinsic(
op_name,
n_attrs=n_attrs,
helper_name=lambda *a, _p=_parts: _p(*a)[0],
c_signature=lambda *a, _p=_parts: _p(*a)[1],
return_type=lambda *a, _p=_parts: _p(*a)[2],
tvm_return_type=lambda *a, _p=_parts: (
None if _p(*a)[2] == "void" else parse_str(a[-n_attrs])
),
body=lambda *a, _p=_parts: _p(*a)[4],
)
_register_ptx_ld("ptx_ld", "weak", 11)
_register_ptx_ld("ptx_ld_relaxed", "relaxed", 10)
_register_ptx_ld("ptx_ld_acquire", "acquire", 10)
_register_ptx_ld("ptx_ld_volatile", "volatile", 6)
_register_ptx_ld("ptx_ld_mmio", "mmio", 6)
# =============================================================================
# Legacy acquire-load lvalue API — compatibility wrapper over
# ``ld.acquire.gpu.global`` / ``ld.global.cg`` forms, dispatched on dtype.
# Wrapper picks .b32/.b64 + matching constraint by dtype.
#
# The body uses ``#if __CUDA_ARCH__ >= 700`` to select acquire on SM70+ and
# fall back to .cg on older arches. This is two PTX form table entries
# combined in one device helper for arch portability.
# =============================================================================
_LD_GLOBAL_ACQUIRE_DTYPES = {
"uint32": ("uint32_t", "b32", "r"),
"int32": ("int32_t", "b32", "r"),
"uint64": ("uint64_t", "b64", "l"),
"int64": ("int64_t", "b64", "l"),
}
def _ld_global_acquire_body(ptx_type: str, spec: str) -> str:
return (
" #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 700\n"
f' asm volatile ("ld.acquire.gpu.global.{ptx_type} %0, [%1];\\n"\n'
f' : "={spec}"(res) : "l"(addr));\n'
" #else\n"
f' asm volatile ("ld.global.cg.{ptx_type} %0, [%1];\\n"\n'
f' : "={spec}"(res) : "l"(addr));\n'
" #endif"
)
for _dtype, (_c_type, _ptx_type, _spec) in _LD_GLOBAL_ACQUIRE_DTYPES.items():
device_intrinsic(
f"ptx_ld_global_acquire_{_dtype}",
c_signature=f"({_c_type}& res, {_c_type}* addr)",
body=_ld_global_acquire_body(_ptx_type, _spec),
)
del _dtype, _c_type, _ptx_type, _spec
@register_codegen("ptx_ld_global_acquire")
def codegen_ptx_ld_global_acquire(res, addr):
"""Dispatch to the dtype-specific helper."""
dtype = str(res.ty)
if dtype not in _LD_GLOBAL_ACQUIRE_DTYPES:
raise ValueError(f"Unsupported data type for ld.global.acquire: {dtype}")
result = CODEGEN_REGISTRY[f"tirx.ptx_ld_global_acquire_{dtype}"]([res, addr])
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# Atomics — templated wrappers around CUDA's ``atomicAdd`` / ``atomicCAS``.
# =============================================================================
device_intrinsic(
"cuda_atomic_add",
helper_name="tvm_builtin_cuda_atomic_add",
c_signature="(T* addr, T value)",
body=" return atomicAdd(addr, value);",
return_type="T",
templated=True,
tvm_return_type=lambda _addr, value: value.ty,
)
device_intrinsic(
"cuda_atomic_cas",
helper_name="tvm_builtin_cuda_atomic_cas",
c_signature="(T* address, T compare, T val)",
body=" return atomicCAS(address, compare, val);",
return_type="T",
templated=True,
tvm_return_type=lambda _p, old, _n: old.ty,
)
# =============================================================================
# half / bfloat16 ↔ float type-punned conversions.
# =============================================================================
device_intrinsic(
"cuda_half2float",
c_signature="(half src)",
body=" return __half2float(src);",
return_type="float",
tvm_return_type="float32",
)
device_intrinsic(
"cuda_bfloat162float",
c_signature="(nv_bfloat16 src)",
body=" return __bfloat162float(src);",
return_type="float",
tvm_return_type="float32",
)
device_intrinsic(
"cuda_float22half2",
c_signature="(void* dst, void* src)",
body=(
" half2* dst_p = (half2*) dst;\n"
" float2* src_p = (float2*) src;\n"
" *dst_p = __float22half2_rn(*src_p);"
),
)
device_intrinsic(
"cuda_half8tofloat8",
c_signature="(void* src_addr, void* dst_addr)",
body=(
" half2* source = (half2*) src_addr;\n"
" float2* dest = (float2*) dst_addr;\n"
" for (int i = 0; i < 4; i++) {\n"
" dest[i] = __half22float2(source[i]);\n"
" }"
),
)
device_intrinsic(
"cuda_float8tohalf8",
c_signature="(void* src_addr, void* dst_addr)",
body=(
" float2* source = (float2*) src_addr;\n"
" half2* dest = (half2*) dst_addr;\n"
" for (int i = 0; i < 4; i++) {\n"
" dest[i] = __float22half2_rn(source[i]);\n"
" }"
),
)
# =============================================================================
# Address-conversion helpers used by op-wrapper-side dispatch in tvm.tirx.op.
# Each precomputes a value that the schema's specialized op then takes as a
# typed scalar input (instead of doing the conversion inside the asm helper).
# =============================================================================
device_intrinsic(
"cuda_cvta_generic_to_shared",
c_signature="(void* p)",
body=" return __cvta_generic_to_shared(p);",
return_type="unsigned int",
tvm_return_type="uint32",
)
device_intrinsic(
"cuda_smem_addr_from_uint64",
c_signature="(uint64_t cluster_addr)",
body=" return static_cast<unsigned int>(cluster_addr);",
return_type="unsigned int",
tvm_return_type="uint32",
)
# =============================================================================
# PTX mapa form:
# mapa{.space}.type d, a, b;
# .space = {.shared::cluster}; .type = {.u32, .u64}
# =============================================================================
def _ptx_mapa_parts(_addr, _rank, space, ptx_type, return_dtype):
space = parse_str(space)
ptx_type = parse_str(ptx_type)
return_dtype = parse_str(return_dtype)
if space not in ("", "shared::cluster"):
raise ValueError(f"Unsupported mapa space {space!r}")
if ptx_type not in ("u32", "u64"):
raise ValueError(f"Unsupported mapa type {ptx_type!r}")
c_type = "uint32_t" if ptx_type == "u32" else "uint64_t"
constraint = "r" if ptx_type == "u32" else "l"
name = f"tvm_builtin_ptx_mapa{('_' + _safe_attr(space)) if space else ''}_{ptx_type}"
body = (
f" {c_type} result;\n"
f' asm volatile("mapa{_dot(space)}.{ptx_type} %0, %1, %2;"\n'
f' : "={constraint}"(result) : "l"(addr), "r"(rank));\n'
" return result;"
)
return name, c_type, return_dtype, body
device_intrinsic(
"ptx_mapa",
n_attrs=3,
helper_name=lambda *a: _ptx_mapa_parts(*a)[0],
c_signature="(void* addr, uint32_t rank)",
return_type=lambda *a: _ptx_mapa_parts(*a)[1],
tvm_return_type=lambda *a: _ptx_mapa_parts(*a)[2],
body=lambda *a: _ptx_mapa_parts(*a)[3],
)
# =============================================================================
# Generic PTX memory forms. Compatibility wrappers in ``tvm.tirx.op`` bind
# concrete sem/scope/space/op/type parameters for existing call sites.
# =============================================================================
# PTX red scalar form:
# red{.sem}{.scope}{.space}.op{.level::cache_hint}.type [a], b{, cache-policy};
def _ptx_red_scalar_parts(*args):
sem, scope, space, op, ptx_type, has_cache_hint = args[-6:]
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
op = parse_str(op)
ptx_type, c_type, constraint, _tvm_dtype = _type_info(ptx_type)
has_cache = (
bool(int(has_cache_hint)) if hasattr(has_cache_hint, "value") else bool(has_cache_hint)
)
modifiers = f"{_dot(sem)}{_dot(scope)}{_dot(space)}"
instr = f"red{modifiers}.{op}{_cache_suffix('cache' if has_cache else '')}.{ptx_type}"
name = (
"tvm_builtin_ptx_red_scalar"
f"{_dot(sem).replace('.', '_')}{_dot(scope).replace('.', '_')}"
f"_{_safe_attr(space)}_{op}_{ptx_type}{'_cache_hint' if has_cache else ''}"
)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl = ""
addr_operand = '"l"(address)'
if space.startswith("shared"):
addr_decl = " unsigned int addr = (unsigned int)__cvta_generic_to_shared(address);\n"
addr_operand = '"r"(addr)'
body = (
f"{addr_decl}"
f' asm volatile("{instr} [%0], %1{", %2" if has_cache else ""};"\n'
" :\n"
f' : {addr_operand}, "{constraint}"(value)'
f"{cache_operand}\n"
' : "memory");'
)
return name, f"(void* address, {c_type} value, unsigned long long cache_policy)", body
device_intrinsic(
"ptx_red_scalar",
n_attrs=6,
helper_name=lambda *a: _ptx_red_scalar_parts(*a)[0],
c_signature=lambda *a: _ptx_red_scalar_parts(*a)[1],
body=lambda *a: _ptx_red_scalar_parts(*a)[2],
)
# PTX atom scalar one-source-operand form:
# atom{.sem}{.scope}{.space}.op{.level::cache_hint}.type d, [a], b{, cache-policy};
def _ptx_atom_scalar_parts(*args):
sem, scope, space, op, ptx_type, has_cache_hint = args[-6:]
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
op = parse_str(op)
ptx_type, c_type, constraint, tvm_dtype = _type_info(ptx_type)
has_cache = (
bool(int(has_cache_hint)) if hasattr(has_cache_hint, "value") else bool(has_cache_hint)
)
modifiers = f"{_dot(sem)}{_dot(scope)}{_dot(space)}"
instr = f"atom{modifiers}.{op}{_cache_suffix('cache' if has_cache else '')}.{ptx_type}"
name = (
"tvm_builtin_ptx_atom_scalar"
f"{_dot(sem).replace('.', '_')}{_dot(scope).replace('.', '_')}"
f"_{_safe_attr(space)}_{op}_{ptx_type}{'_cache_hint' if has_cache else ''}"
)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl = ""
addr_operand = '"l"(address)'
if space.startswith("shared"):
addr_decl = " unsigned int addr = (unsigned int)__cvta_generic_to_shared(address);\n"
addr_operand = '"r"(addr)'
body = (
f"{addr_decl}"
f" {c_type} ret;\n"
f' asm volatile("{instr} %0, [%1], %2{", %3" if has_cache else ""};"\n'
f' : "={constraint}"(ret)\n'
f' : {addr_operand}, "{constraint}"(value)'
f"{cache_operand}\n"
' : "memory");\n'
" return ret;"
)
return (
name,
f"(void* address, {c_type} value, unsigned long long cache_policy)",
c_type,
tvm_dtype,
body,
)
device_intrinsic(
"ptx_atom_scalar",
n_attrs=6,
helper_name=lambda *a: _ptx_atom_scalar_parts(*a)[0],
c_signature=lambda *a: _ptx_atom_scalar_parts(*a)[1],
return_type=lambda *a: _ptx_atom_scalar_parts(*a)[2],
tvm_return_type=lambda *a: _ptx_atom_scalar_parts(*a)[3],
body=lambda *a: _ptx_atom_scalar_parts(*a)[4],
)
# PTX prefetch tensormap form:
# prefetch{.tensormap_space}.tensormap [a];
def _prefetch_tensormap_parts(_tensor_map, tensormap_space):
space = parse_str(tensormap_space)
instr = f"prefetch{_dot(space)}.tensormap"
name = f"tvm_builtin_ptx_prefetch{('_' + _safe_attr(space)) if space else ''}_tensormap"
body = (
f' asm volatile("{instr} [%0];"\n'
" :\n"
' : "l"(tensor_map_addr)\n'
' : "memory");'
)
return name, body
device_intrinsic(
"ptx_prefetch_tensormap",
n_attrs=1,
helper_name=lambda *a: _prefetch_tensormap_parts(*a)[0],
c_signature="(unsigned long long tensor_map_addr)",
body=lambda *a: _prefetch_tensormap_parts(*a)[1],
)
# PTX st forms (ISA table entries registered via ``ptx_st`` and siblings):
# st{.weak}{.ss}{.cop}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st{.weak}{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st.volatile{.ss}{.vec}.type [a], b;
# st.relaxed.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st.release.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st.mmio.sem.sys{.global}.type [a], b;
_PTX_ST_COPS = {"", "wb", "cg", "cs", "wt"}
_PTX_ST_SPACES = {"global", "shared", "shared::cta", "shared::cluster", "local", "param::func"}
def _ptx_st_load_src(num_bytes, vec_len, ptx_type, c_type):
if ptx_type == "u8" and vec_len == 1:
return " unsigned int r0 = *reinterpret_cast<unsigned char*>(src_ptr);\n"
store_type = _PTX_VEC_STORE_TYPE[num_bytes]
if vec_len > 1:
return f" {store_type} src_ = *reinterpret_cast<{store_type}*>(src_ptr);\n" + "".join(
f" {c_type} r{i} = src_.{c};\n" for i, c in enumerate("xyzw"[:vec_len])
)
return f" {c_type} r0 = *reinterpret_cast<{c_type}*>(src_ptr);\n"
def _ptx_st_form_parts(form, attr_args, from_src):
if form == "weak":
weak, space, cop, vec, ptx_type, has_cache_hint, l1_evict, l2_evict = attr_args
sem, scope = "", ""
elif form == "relaxed":
scope, space, vec, ptx_type, has_cache_hint, l1_evict, l2_evict = attr_args
sem, weak, cop = "relaxed", False, ""
elif form == "release":
scope, space, vec, ptx_type, has_cache_hint, l1_evict, l2_evict = attr_args
sem, weak, cop = "release", False, ""
elif form == "volatile":
space, vec, ptx_type = attr_args
sem, scope, weak, cop = "", "", False, ""
has_cache_hint, l1_evict, l2_evict = False, "", ""
elif form == "mmio":
sem, scope, space, ptx_type = attr_args
weak, cop, vec = False, "", ""
has_cache_hint, l1_evict, l2_evict = False, "", ""
else:
raise ValueError(f"unknown st form {form!r}")
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
cop = parse_str(cop)
vec = parse_str(vec)
l1_evict = parse_str(l1_evict)
l2_evict = parse_str(l2_evict)
weak = _bool_attr(weak)
has_cache = _bool_attr(has_cache_hint)
ptx_type, c_type, constraint, _tvm_dtype = _type_info(ptx_type)
if cop and cop not in _PTX_ST_COPS:
raise ValueError(f"Unsupported PTX st cache operation {cop!r}")
if vec and vec not in _PTX_VEC:
raise ValueError(f"Unsupported PTX st vector modifier {vec!r}")
if space not in _PTX_ST_SPACES and not (form == "mmio" and space == "global"):
raise ValueError(f"Unsupported PTX st state space {space!r}")
vec_len = int(vec[1:]) if vec else 1
elem_bytes = (
8
if ptx_type.endswith("64")
else 2
if ptx_type in ("u16", "s16", "b16")
else 1
if ptx_type in ("u8", "s8", "b8")
else 4
)
num_bytes = vec_len * elem_bytes if vec else elem_bytes
use_cache_policy = form in ("weak", "relaxed", "release")
if form == "mmio":
if sem not in ("acquire", "relaxed", "release") or scope != "sys" or space != "global":
raise ValueError("st.mmio requires sem, scope=sys, space=global")
prefix = f"st.mmio.{sem}.{scope}"
elif form == "relaxed":
if not scope:
raise ValueError("st.relaxed requires scope")
prefix = f"st.relaxed.{scope}{_dot(space)}"
elif form == "release":
if not scope:
raise ValueError("st.release requires scope")
prefix = f"st.release.{scope}{_dot(space)}"
elif form == "volatile":
prefix = f"st.volatile{_dot(space)}"
else:
prefix = f"st{'.weak' if weak else ''}{_dot(space)}{_dot(cop)}"
level = _ptx_level_suffix(has_cache, l1_evict, l2_evict, "")
instr = f"{prefix}{level}{_dot(vec)}.{ptx_type}"
name_parts = ["tvm_builtin_ptx_st", form if form != "weak" else ("weak" if weak else "plain")]
if sem:
name_parts.append(_safe_attr(sem))
if scope:
name_parts.append(_safe_attr(scope))
name_parts.extend(
[
_safe_attr(space),
_safe_attr(cop) if cop else "",
_safe_attr(vec) if vec else "",
ptx_type,
"from_src" if from_src else "values",
]
)
if has_cache:
name_parts.append("cache_hint")
name = "_".join(p for p in name_parts if p)
values = f"{{{', '.join(f'%{i + 1}' for i in range(vec_len))}}}" if vec_len > 1 else "%1"
value_constraints = "".join(f', "{constraint}"(value{i})' for i in range(vec_len))
cache_slot = f", %{vec_len + 1}" if has_cache else ""
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl, addr_operand = _ptx_shared_addr(space, "address")
if from_src:
load_regs = _ptx_st_load_src(num_bytes, vec_len, ptx_type, c_type)
if vec_len > 1:
in_constraints = ", ".join(f'"{constraint}"(r{i})' for i in range(vec_len))
value_args = f", {in_constraints}"
else:
value_args = f', "{constraint}"(r0)'
body = (
f"{addr_decl}{load_regs}"
f' asm volatile("{instr} [%0], {values}{cache_slot};"\n'
" :\n"
f" : {addr_operand}{value_args}{cache_operand}\n"
' : "memory");'
)
if use_cache_policy:
sig = "(void* address, void* src_ptr, unsigned long long cache_policy)"
else:
sig = "(void* address, void* src_ptr)"
else:
body = (
f"{addr_decl}"
f' asm volatile("{instr} [%0], {values}{cache_slot};"\n'
" :\n"
f" : {addr_operand}{value_constraints}{cache_operand}\n"
' : "memory");'
)
if form == "mmio":
sig = f"(void* address, {c_type} value0)"
elif use_cache_policy:
value_params = ", ".join(f"{c_type} value{i}" for i in range(vec_len))
sig = f"(void* address, {value_params}, unsigned long long cache_policy)"
else:
value_params = ", ".join(f"{c_type} value{i}" for i in range(vec_len))
sig = f"(void* address, {value_params})"
return name, sig, body
def _register_ptx_st(op_name, form, n_attrs, *, with_cache_policy=True):
def codegen(*args):
from_src = _bool_attr(args[-1])
st_attrs = args[-n_attrs:-1]
parts = _ptx_st_form_parts(form, st_attrs, from_src)
forward = args[:-(n_attrs)]
name, sig, body_str = parts
source_code = f"\n__forceinline__ __device__ void {name}{sig} {{\n{body_str}\n}}\n"
return cuda_func_call(name, *forward, source_code=source_code)
codegen.__name__ = f"codegen_{op_name}"
register_codegen(op_name)(codegen)
def _register_ptx_st_mmio(op_name, form, n_attrs):
def codegen(*args):
parts = _ptx_st_form_parts(form, args[-n_attrs:], False)
forward = args[:-n_attrs]
name, sig, body_str = parts
source_code = f"\n__forceinline__ __device__ void {name}{sig} {{\n{body_str}\n}}\n"
return cuda_func_call(name, *forward, source_code=source_code)
codegen.__name__ = f"codegen_{op_name}"
register_codegen(op_name)(codegen)
_register_ptx_st("ptx_st", "weak", 9)
_register_ptx_st("ptx_st_relaxed", "relaxed", 8)
_register_ptx_st("ptx_st_release", "release", 8)
_register_ptx_st("ptx_st_volatile", "volatile", 4)
_register_ptx_st_mmio("ptx_st_mmio", "mmio", 4)
# PTX st.bulk form:
# st.bulk{.weak}{.shared::cta} [a], size, initval;
# ``initval`` is an immediate operand whose only legal value is 0.
def _ptx_st_bulk_parts(_ptr, _num_bytes, weak, space):
weak = bool(int(weak)) if hasattr(weak, "value") else bool(weak)
space = parse_str(space)
instr = f"st.bulk{'.weak' if weak else ''}{_dot(space)}"
name = f"tvm_builtin_ptx_st_bulk{'_weak' if weak else ''}{('_' + _safe_attr(space)) if space else ''}"
addr_arg = (
'"r"((unsigned int)__cvta_generic_to_shared(ptr))' if space == "shared::cta" else '"l"(ptr)'
)
body = (
f' asm volatile("{instr} [%0], %1, 0;"\n'
" :\n"
f" : {addr_arg}, "
'"l"(static_cast<uint64_t>(num_bytes))\n'
' : "memory");'
)
return name, body
device_intrinsic(
"ptx_st_bulk",
n_attrs=2,
helper_name=lambda *a: _ptx_st_bulk_parts(*a)[0],
c_signature="(void* ptr, unsigned int num_bytes)",
body=lambda *a: _ptx_st_bulk_parts(*a)[1],
)
device_intrinsic(
"cuda_uint_as_float",
helper_name="tvm_builtin_uint_as_float",
c_signature="(unsigned int bits)",
return_type="float",
body=" return __uint_as_float(bits);",
)
device_intrinsic(
"cuda_float_as_uint",
helper_name="tvm_builtin_float_as_uint",
c_signature="(float x)",
return_type="unsigned int",
body=" return __float_as_uint(x);",
)
@@ -0,0 +1,253 @@
# 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.
# ruff: noqa: E501
# pylint: disable=redefined-builtin, invalid-name
"""Miscellaneous device helpers.
Catch-all for ops that don't fit the (sync / mma / cp_async / memory / math /
nvshmem) feature buckets:
* PTX register-allocation control: ``setmaxnreg`` / ``mov`` from special reg.
* Per-thread queries / scheduling hints: ``thread_rank`` / ``nano_sleep``.
* Profiler timer hooks (``timer_init/start/end/finalize``).
* Debug helpers: ``printf`` / ``trap`` on assert failure.
"""
import hashlib
import json
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
# =============================================================================
# setmaxnreg.{inc,dec}.sync.aligned.u32 — 1 PTX form (.action picks inc/dec).
# =============================================================================
def _ptx_setmaxnreg(inc, nreg):
inc = bool(int(inc)) if hasattr(inc, "value") else bool(inc)
nreg = int(nreg)
action = "inc" if inc else "dec"
return (
f"tvm_builtin_ptx_setmaxnreg_{action}_{nreg}",
f' asm volatile("setmaxnreg.{action}.sync.aligned.u32 {nreg};");',
)
device_intrinsic(
"ptx_setmaxnreg",
n_attrs=2,
helper_name=lambda inc, nreg: _ptx_setmaxnreg(inc, nreg)[0],
body=lambda inc, nreg: _ptx_setmaxnreg(inc, nreg)[1],
)
# =============================================================================
# mov.u32/u64 from special register — 1 PTX form (Form 2 of mov.type d, sreg).
# Each (bits, reg) emits a distinct helper because the special reg name is
# baked into the PTX text.
# =============================================================================
def _ptx_fetch_register_body(bits):
spec = "l" if bits == 64 else "r"
def _body(reg):
reg = parse_str(reg)
return (
f" uint{bits}_t x;\n"
f' asm volatile("mov.u{bits} %0, %{reg};" : "={spec}"(x));\n'
f" return (int{bits}_t)x;"
)
return _body
for _bits in (32, 64):
device_intrinsic(
f"ptx_fetch_register_{_bits}",
n_attrs=1,
helper_name=(
lambda *a, bits=_bits: (
f"tvm_builtin_ptx_fetch_register_"
f"{parse_str(a[-1]).replace('::', '_').replace('.', '_')}"
)
),
return_type=f"int{_bits}_t",
body=_ptx_fetch_register_body(_bits),
)
del _bits
@register_codegen("ptx_fetch_register")
def codegen_ptx_fetch_register(bits, reg):
bits = int(bits)
reg = parse_str(reg)
if bits not in (32, 64):
raise ValueError(f"Only support 32/64 bits for ptx_fetch_register, but got {bits}.")
result = CODEGEN_REGISTRY[f"tirx.ptx_fetch_register_{bits}"]([reg])
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# Per-thread queries / scheduling hints.
# =============================================================================
device_intrinsic(
"cuda_thread_rank",
body=(
" namespace cg = cooperative_groups;\n return cg::this_thread_block().thread_rank();"
),
return_type="int",
tvm_return_type="int32",
extra_deps=("cooperative_groups",),
)
device_intrinsic("cuda_nano_sleep", c_signature="(uint64_t time)", body=" __nanosleep(time);")
# =============================================================================
# Profiler timer hooks.
# =============================================================================
_COMMON_PARAMS = (
"uint64_t* profiler_buffer, uint64_t* profiler_tag, "
"uint32_t* profiler_write_offset, int profiler_write_stride, bool leader_cond"
)
_EVENT_PARAMS = f"int event_type, {_COMMON_PARAMS}"
def _write_event(event_bits: str) -> str:
return (
"profiler_buffer[profiler_write_offset[0]] = "
"((uint64_t)tvm_builtin_get_timestamp() << 32) | "
f"(profiler_tag[0] | {event_bits});\n"
" profiler_write_offset[0] += profiler_write_stride;"
)
device_intrinsic(
"timer_init_cuda",
c_signature=(
"(uint64_t* profiler_buffer, uint64_t* profiler_tag, "
"uint32_t* profiler_write_offset, int num_groups, int group_id)"
),
body=(
" const uint32_t NBLOCKS = (uint32_t)(gridDim.x * gridDim.y * gridDim.z);\n"
" const uint32_t BLOCK_IDX = (uint32_t)("
"(blockIdx.z * gridDim.y + blockIdx.y) * gridDim.x + blockIdx.x);\n"
" const uint32_t NGROUPS = num_groups;\n"
" const uint32_t GROUP_ID = group_id;\n"
" const uint32_t BLOCK_GROUP_IDX = BLOCK_IDX * NGROUPS + GROUP_ID;\n"
" if ((blockIdx.x == 0) && (blockIdx.y == 0) && "
"(blockIdx.z == 0) && (threadIdx.x == 0)) {\n"
" profiler_buffer[0] = ((uint64_t)NGROUPS << 32) | NBLOCKS;\n"
" }\n"
" profiler_write_offset[0] = 1 + BLOCK_GROUP_IDX;\n"
" profiler_tag[0] = (uint64_t)BLOCK_GROUP_IDX << 12;"
),
)
device_intrinsic(
"timer_start_cuda",
c_signature=f"({_EVENT_PARAMS})",
body=(
f" if (leader_cond) {{\n {_write_event('(uint32_t)event_type << 2 | 0x0')}\n }}\n"
" __threadfence_block();"
),
extra_deps=("get_time_stamp",),
)
device_intrinsic(
"timer_end_cuda",
c_signature=f"({_EVENT_PARAMS})",
body=(
" __threadfence_block();\n"
f" if (leader_cond) {{\n {_write_event('(uint32_t)event_type << 2 | 0x1')}\n }}"
),
extra_deps=("get_time_stamp",),
)
device_intrinsic(
"timer_finalize_cuda",
c_signature=f"({_COMMON_PARAMS})",
body=(
f" __threadfence_block();\n if (leader_cond) {{\n {_write_event('0x3')}\n }}"
),
extra_deps=("get_time_stamp",),
)
# =============================================================================
# Debug helpers — ``printf`` (variadic templated) and ``trap`` on assert.
# =============================================================================
device_intrinsic(
"cuda_trap_when_assert_failed",
c_signature="(bool cond)",
body=' do {\n if (not (cond))\n asm("trap;");\n } while (0);',
)
@register_codegen("cuda_printf")
def codegen_cuda_printf(fmt, *args):
if isinstance(fmt, tvm.tirx.StringImm):
fmt = fmt.value
if not isinstance(fmt, str):
raise ValueError("T.cuda.printf format must be a string literal")
fmt_literal = json.dumps(fmt)
arg_dtypes = [str(arg.ty) for arg in args]
signature = "|".join([fmt, *arg_dtypes])
digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()
func_name = f"tvm_builtin_cuda_printf_{len(args)}_{digest}"
def c_type(dtype: str) -> str:
if dtype == "float32":
return "float"
if dtype == "float64":
return "double"
if dtype in {"int8", "int16", "int32"}:
return "int"
if dtype == "int64":
return "long long"
if dtype in {"uint8", "uint16", "uint32"}:
return "unsigned int"
if dtype == "uint64":
return "unsigned long long"
if dtype == "bool":
return "int"
if dtype == "handle":
return "void*"
raise ValueError(f"Unsupported T.cuda.printf argument dtype: {dtype}")
params = ", ".join(f"{c_type(dtype)} arg{i}" for i, dtype in enumerate(arg_dtypes))
call_args = ", ".join(f"arg{i}" for i in range(len(args)))
comma_call_args = f", {call_args}" if call_args else ""
source_code = f"""
__noinline__ __device__ void {func_name}({params}) {{
printf({fmt_literal}{comma_call_args});
}}
"""
return cuda_func_call(func_name, *args, source_code=source_code)
device_intrinsic(
"cuda_clock64",
helper_name="tvm_builtin_clock64",
return_type="unsigned long long",
body=" return clock64();",
)
@@ -0,0 +1,482 @@
# 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 MMA / ldmatrix / stmatrix intrinsics.
mma.sync.aligned has 7 form_kinds per the PTX docs (f16 / tf32 / bf16 / fp64
/ int8 / fp8 / subbyte). Each form_kind is one ``device_intrinsic`` registration;
the (shape, layouts, dtypes) modifier slots are attrs. Body computes the per-
fragment register counts at codegen time from M*N*bits/threads/frag_size and
hand-builds the asm constraint list.
ldmatrix / stmatrix each have a single PTX form (the .m8n8 .b16/.b8 variant
that TIRx uses); ``num`` and ``trans`` are modifier attrs.
"""
import re
from dataclasses import dataclass
from tvm import DataType
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .types import PTXDataType
from .utils import parse_str
@dataclass
class FragAttrs:
reg_type: str # asm constraint letter (r / f / d)
size: int # bit width per register slot (32 or 64)
ptr_type: str # C type for the cast
_FRAG_ATTRS_MAP = {
PTXDataType.BIT1: FragAttrs("r", 32, "uint32_t"),
PTXDataType.INT4: FragAttrs("r", 32, "uint32_t"),
PTXDataType.UINT4: FragAttrs("r", 32, "uint32_t"),
PTXDataType.INT8: FragAttrs("r", 32, "uint32_t"),
PTXDataType.UINT8: FragAttrs("r", 32, "uint32_t"),
PTXDataType.FLOAT8_E4M3FN: FragAttrs("r", 32, "uint32_t"),
PTXDataType.FLOAT8_E5M2: FragAttrs("r", 32, "uint32_t"),
PTXDataType.BIT16: FragAttrs("r", 32, "uint32_t"),
PTXDataType.FLOAT16: FragAttrs("r", 32, "uint32_t"),
PTXDataType.BFLOAT16: FragAttrs("r", 32, "uint32_t"),
PTXDataType.TENSOR_FLOAT32: FragAttrs("r", 32, "uint32_t"),
PTXDataType.INT32: FragAttrs("r", 32, "int32_t"),
PTXDataType.FLOAT32: FragAttrs("f", 32, "float"),
PTXDataType.FLOAT64: FragAttrs("d", 64, "double"),
}
def _parse_mma_shape(shape_str):
match = re.search(r"m(\d+)n(\d+)k(\d+)", shape_str)
if not match:
raise ValueError(f"Cannot parse MMA shape: {shape_str!r}")
return tuple(map(int, match.groups()))
def _classify_mma_form(d_type, a_type, b_type):
"""Map (d, a, b) dtype triple to one of the 7 PTX form_kind tags."""
fp16 = {"float16", "fp16"}
tf32 = {"tensor_float32", "tf32"}
bf16 = {"bfloat16", "bf16"}
fp64 = {"float64", "fp64"}
int_a = {"int8", "uint8", "s8", "u8"}
fp8 = {"e4m3", "e5m2", "float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2"}
subbyte = {"int4", "uint4", "bit1", "s4", "u4", "b1", "int1", "uint1"}
if a_type in fp16 and b_type in fp16:
return "f16"
if a_type in tf32 and b_type in tf32:
return "tf32"
if a_type in bf16 and b_type in bf16:
return "bf16"
if a_type in fp64 and b_type in fp64:
return "fp64"
if a_type in int_a and b_type in int_a:
return "int8"
if a_type in fp8 and b_type in fp8:
return "fp8"
if a_type in subbyte and b_type in subbyte:
return "subbyte"
raise ValueError(
f"Unknown ptx.mma form for d_type={d_type!r}, a_type={a_type!r}, b_type={b_type!r}"
)
def _frag(dtype_str):
return _FRAG_ATTRS_MAP[PTXDataType.from_string(dtype_str)]
def _mma_threads(shape, a_type):
"""Special case: m8n8k4 with f16 a/b uses 8 threads per fragment."""
m, n, k = _parse_mma_shape(shape)
if m == 8 and n == 8 and k == 4 and a_type == "float16":
return 8
return 32
# PTX dtype abbreviation -> element bit width. Used by _frag_count so that
# callers passing the PTX abbreviation (e.g. "fp32") don't blow up in
# ``DataType("fp32")``.
_PTX_BITS = {
"fp16": 16,
"fp32": 32,
"fp64": 64,
"bf16": 16,
"tf32": 32, # tensor-float32 packs 19 significant bits into a 32-bit slot
"s8": 8,
"u8": 8,
"s32": 32,
"s4": 4,
"u4": 4,
"b1": 1,
"b16": 16,
"e4m3": 8,
"e5m2": 8,
}
def _frag_count(dtype, dim_a, dim_b, threads):
if dtype in _PTX_BITS:
bits = _PTX_BITS[dtype]
else:
bits = DataType(dtype).bits
size = _frag(dtype).size
return dim_a * dim_b * bits // threads // size
# =============================================================================
# Shared helpers for the 7 mma form_kinds.
# Args layout for each form:
# (d_ptr_in, a_ptr_in, b_ptr_in [, c_ptr_in], shape, a_layout, b_layout,
# d_type, a_type, b_type, c_type, no_c_ptr [, saturate or bit_op])
# n_attrs = 8 for f16/tf32/bf16/fp64/fp8 (last 8 = shape, layouts, 4 dtypes, no_c_ptr)
# n_attrs = 9 for int8 (+ saturate) and subbyte (+ bit_op)
# =============================================================================
def _mma_form_parts(args, *, has_saturate=False, has_bit_op=False):
"""Compute (helper_name, c_signature, body) for one mma form invocation.
``args`` is the full positional arg tuple as received by codegen.
The trailing ``n_attrs`` (8 or 9) entries are attrs.
"""
n_extra = (1 if has_saturate else 0) + (1 if has_bit_op else 0)
n_attrs = 8 + n_extra
# Split off attr args from the tail (operand args are ahead).
attrs = args[-n_attrs:]
shape = parse_str(attrs[0])
a_layout = parse_str(attrs[1])
b_layout = parse_str(attrs[2])
d_type = parse_str(attrs[3])
a_type = parse_str(attrs[4])
b_type = parse_str(attrs[5])
c_type = parse_str(attrs[6])
no_c_ptr_raw = attrs[7]
no_c_ptr = bool(int(no_c_ptr_raw)) if hasattr(no_c_ptr_raw, "value") else bool(no_c_ptr_raw)
saturate = False
bit_op = ""
if has_saturate:
s = attrs[8]
saturate = bool(int(s)) if hasattr(s, "value") else bool(s)
if has_bit_op:
bit_op = parse_str(attrs[8])
# Fragment counts (same derivation as the contiguous form).
m, n, k = _parse_mma_shape(shape)
threads = _mma_threads(shape, a_type)
d_cnt = _frag_count(d_type, m, n, threads)
a_cnt = _frag_count(a_type, m, k, threads)
b_cnt = _frag_count(b_type, k, n, threads)
c_cnt = _frag_count(c_type, m, n, threads)
# C signature: one void* per register, ordered D regs, A regs, B regs,
# then C regs (only when the accumulator is used).
sig_parts = (
[f"void* d_ptr{i}" for i in range(d_cnt)]
+ [f"void* a_ptr{i}" for i in range(a_cnt)]
+ [f"void* b_ptr{i}" for i in range(b_cnt)]
)
if not no_c_ptr:
sig_parts += [f"void* c_ptr{i}" for i in range(c_cnt)]
sig = "(" + ", ".join(sig_parts) + ")"
def _safe(s):
return s.replace("::", "_").replace(".", "_")
name = (
f"ptx_mma_{shape}_{a_layout}_{b_layout}"
f"_{_safe(d_type)}_{_safe(a_type)}_{_safe(b_type)}_{_safe(c_type)}"
f"{'_no_c_ptr' if no_c_ptr else ''}"
f"{'_saturate' if saturate else ''}"
)
d_frag = _frag(d_type)
a_frag = _frag(a_type)
b_frag = _frag(b_type)
c_frag = _frag(c_type)
saturate_inst = ".satfinite" if saturate else ""
# PTX b1 mma requires a `.popc` suffix after the bit op (e.g. `.xor.popc`).
bit_op_inst = f".{bit_op}.popc" if bit_op else ""
d_type_inst = PTXDataType.from_string(d_type).to_string()
c_type_inst = PTXDataType.from_string(c_type).to_string()
a_type_inst = PTXDataType.from_string(a_type).to_string()
b_type_inst = PTXDataType.from_string(b_type).to_string()
def _slot_arr(start, cnt):
return "{" + ", ".join(f"%{start + i}" for i in range(cnt)) + "}"
args_template = (
f"{_slot_arr(0, d_cnt)}, {_slot_arr(d_cnt, a_cnt)}, "
f"{_slot_arr(d_cnt + a_cnt, b_cnt)}, {_slot_arr(d_cnt + a_cnt + b_cnt, c_cnt)}"
)
# Each register binds to its OWN pointer via *(T*)X_ptrN (scatter).
d_outs = ", ".join(
f'"=r"(*({d_frag.ptr_type}*)d_ptr{i})'
if d_frag.reg_type == "r"
else f'"={d_frag.reg_type}"(*({d_frag.ptr_type}*)d_ptr{i})'
for i in range(d_cnt)
)
a_inputs = ", ".join(
f'"{a_frag.reg_type}"(*({a_frag.ptr_type}*)a_ptr{i})' for i in range(a_cnt)
)
b_inputs = ", ".join(
f'"{b_frag.reg_type}"(*({b_frag.ptr_type}*)b_ptr{i})' for i in range(b_cnt)
)
if no_c_ptr:
c_value = "0.f" if c_frag.reg_type == "f" else "0"
c_inputs = ", ".join(f'"{c_frag.reg_type}"({c_value})' for _ in range(c_cnt))
else:
c_inputs = ", ".join(
f'"{c_frag.reg_type}"(*({c_frag.ptr_type}*)c_ptr{i})' for i in range(c_cnt)
)
body = (
" asm volatile(\n"
f' "mma.sync.aligned.{shape}.{a_layout}.{b_layout}{saturate_inst}'
f'{d_type_inst}{a_type_inst}{b_type_inst}{c_type_inst}{bit_op_inst} "\n'
f' "{args_template};\\n"\n'
f" : {d_outs}\n"
f" : {a_inputs}, {b_inputs}, {c_inputs}\n"
" );"
)
return name, sig, body
def _register_mma_form(form_kind, *, has_saturate=False, has_bit_op=False):
n_attrs = 8 + (1 if has_saturate else 0) + (1 if has_bit_op else 0)
def _parts(*args, hs=has_saturate, hb=has_bit_op):
return _mma_form_parts(args, has_saturate=hs, has_bit_op=hb)
device_intrinsic(
f"_ptx_mma_{form_kind}",
n_attrs=n_attrs,
helper_name=lambda *a: _parts(*a)[0],
c_signature=lambda *a: _parts(*a)[1],
body=lambda *a: _parts(*a)[2],
)
# Form 1 — f16. Form 2 — tf32. Form 3 — bf16. Form 4 — fp64. Form 6 — fp8.
# All share the same 8-attr layout (no saturate / bit_op).
for _kind in ("f16", "tf32", "bf16", "fp64", "fp8"):
_register_mma_form(_kind)
del _kind
# Form 5 — int8 (+ saturate).
_register_mma_form("int8", has_saturate=True)
# Form 7 — subbyte (+ bit_op for b1).
_register_mma_form("subbyte", has_bit_op=True)
@register_codegen("ptx_mma")
def codegen_ptx_mma(
shape,
a_layout,
b_layout,
d_type,
a_type,
b_type,
c_type,
d_cnt,
a_cnt,
b_cnt,
c_cnt,
no_c_ptr,
*rest,
):
"""Classify (d, a, b) dtype triple to one of 7 form_kinds and forward.
``rest`` = flattened per-register pointers (d_cnt + a_cnt + b_cnt + c_cnt of
them) followed by ``saturate`` and optionally ``bit_op``.
"""
shape = parse_str(shape)
a_layout = parse_str(a_layout)
b_layout = parse_str(b_layout)
d_type = parse_str(d_type)
a_type = parse_str(a_type)
b_type = parse_str(b_type)
c_type = parse_str(c_type)
d_cnt = int(d_cnt)
a_cnt = int(a_cnt)
b_cnt = int(b_cnt)
c_cnt = int(c_cnt)
no_c_ptr = bool(int(no_c_ptr)) if hasattr(no_c_ptr, "value") else bool(no_c_ptr)
n_ptrs = d_cnt + a_cnt + b_cnt + (0 if no_c_ptr else c_cnt)
ptrs = list(rest[:n_ptrs])
trailing = list(rest[n_ptrs:])
saturate = bool(trailing[0]) if trailing else False
bit_op_v = ""
if len(trailing) >= 2:
bo = trailing[1]
bit_op_v = parse_str(bo) if isinstance(bo, str) else (bo if bo is not None else "")
kind = _classify_mma_form(d_type, a_type, b_type)
# op_args are the flattened per-register pointers (already in PTX order:
# D regs, A regs, B regs, then C regs unless no_c_ptr).
op_args = ptrs
attr_args = [shape, a_layout, b_layout, d_type, a_type, b_type, c_type, no_c_ptr]
if kind == "int8":
attr_args.append(saturate)
elif kind == "subbyte":
attr_args.append(bit_op_v)
result = CODEGEN_REGISTRY[f"tirx._ptx_mma_{kind}"](op_args + attr_args)
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# ldmatrix / stmatrix — m8n8 fragment load/store. PTX docs lists 3 ldmatrix
# forms (m8n8 + m8n16 + m16n16); TIRx uses only the m8n8 form. 1
# device_intrinsic each. ``num`` (.x1/.x2/.x4) and ``trans`` are modifier
# attrs; the asm body loops over per-register constraints based on
# (num, dtype).
# =============================================================================
def _ldmatrix_parts(*args):
# args = (smem_ptr, dst0, dst1, ..., dst{N-1}, num, dtype, trans)
# The last 3 entries are the codegen attrs (n_attrs=3).
num = int(args[-3])
dtype = parse_str(args[-2])
trans_b = bool(int(args[-1])) if hasattr(args[-1], "value") else bool(args[-1])
if num not in (1, 2, 4):
raise ValueError(f"ldmatrix .num must be one of {{1, 2, 4}}, got {num}")
if dtype not in ("b16", "b8"):
raise ValueError(f"ldmatrix dtype must be 'b16' or 'b8', got {dtype!r}")
n_regs = num if dtype == "b16" else num // 2
trans_inst = ".trans" if trans_b else ""
slot_list = "{" + ", ".join(f"%{i}" for i in range(n_regs)) + "}"
reg_decls = ", ".join(f"r{i}" for i in range(n_regs))
out_constraints = ", ".join(f'"=r"(r{i})' for i in range(n_regs))
dst_assigns = "\n".join(f" *(uint32_t*)dst{i} = r{i};" for i in range(n_regs))
name = f"ptx_ldmatrix_{num}_{dtype.replace('::', '_').replace('.', '_')}_{1 if trans_b else 0}"
sig = "(void* smem_ptr, " + ", ".join(f"void* dst{i}" for i in range(n_regs)) + ")"
body = (
f" uint32_t {reg_decls};\n"
" unsigned int addr = __cvta_generic_to_shared(smem_ptr);\n"
" asm volatile(\n"
f' "ldmatrix.sync.aligned.m8n8.x{num}{trans_inst}.shared.{dtype} '
f'{slot_list}, [%{n_regs}];"\n'
f" : {out_constraints}\n"
f' : "r"(addr));\n'
f"{dst_assigns}"
)
return name, sig, body
device_intrinsic(
"_ptx_ldmatrix_impl",
n_attrs=3,
c_signature=lambda *a: _ldmatrix_parts(*a)[1],
helper_name=lambda *a: _ldmatrix_parts(*a)[0],
body=lambda *a: _ldmatrix_parts(*a)[2],
)
@register_codegen("ptx_ldmatrix")
def codegen_ptx_ldmatrix(trans, num, dtype, smem_ptr, *dst_handles):
trans = bool(trans)
num = int(num)
dtype = parse_str(dtype)
if dtype.startswith("."):
dtype = dtype[1:]
n_regs = num if dtype == "b16" else num // 2
if len(dst_handles) != n_regs:
raise ValueError(
f"ldmatrix .x{num}.{dtype} codegen expects {n_regs} dst handles, got {len(dst_handles)}"
)
result = CODEGEN_REGISTRY["tirx._ptx_ldmatrix_impl"](
[smem_ptr, *dst_handles, num, dtype, trans]
)
return result[0] if isinstance(result, tuple) else result
def _stmatrix_parts(*args):
# args = (smem_ptr, src0, src1, ..., src{N-1}, trans, num, dtype, shape, space)
# The last 5 entries are codegen attrs (n_attrs=5).
trans_arg, num_arg, dtype_arg, shape_arg, space_arg = args[-5:]
n_regs = int(num_arg)
trans_b = bool(int(trans_arg)) if hasattr(trans_arg, "value") else bool(trans_arg)
dtype = parse_str(dtype_arg)
shape = parse_str(shape_arg)
space = parse_str(space_arg)
if dtype.startswith("."):
dtype = dtype[1:]
if n_regs not in (1, 2, 4):
raise ValueError(f"stmatrix .num must be one of {{1, 2, 4}}, got {n_regs}")
if dtype not in ("b16", "b8"):
raise ValueError(f"stmatrix .type must be b16 or b8, got {dtype!r}")
if shape not in ("m8n8", "m16n8"):
raise ValueError(f"stmatrix .shape must be m8n8 or m16n8, got {shape!r}")
if space not in ("shared", "shared::cta"):
raise ValueError(f"stmatrix state space must be shared or shared::cta, got {space!r}")
if shape == "m16n8" and not trans_b:
raise ValueError("stmatrix .m16n8 requires .trans")
trans_inst = ".trans" if trans_b else ""
slot_list = "{" + ", ".join(f"%{i}" for i in range(n_regs)) + "}"
src_loads = "\n".join(f" uint32_t r{i} = *(uint32_t*)src{i};" for i in range(n_regs))
in_constraints = ", ".join(f'"r"(r{i})' for i in range(n_regs))
name = f"ptx_stmatrix_{shape}_{n_regs}_{1 if trans_b else 0}_{space.replace('::', '_')}_{dtype}"
sig = "(void* smem_ptr, " + ", ".join(f"void* src{i}" for i in range(n_regs)) + ")"
body = (
f"{src_loads}\n"
" unsigned int addr = __cvta_generic_to_shared(smem_ptr);\n"
" asm volatile(\n"
f' "stmatrix.sync.aligned.{shape}.x{n_regs}{trans_inst}.{space}.{dtype} '
f'[%{n_regs}], {slot_list};"\n'
" :\n"
f' : {in_constraints}, "r"(addr));'
)
return name, sig, body
device_intrinsic(
"_ptx_stmatrix_impl",
n_attrs=5,
c_signature=lambda *a: _stmatrix_parts(*a)[1],
helper_name=lambda *a: _stmatrix_parts(*a)[0],
body=lambda *a: _stmatrix_parts(*a)[2],
)
@register_codegen("ptx_stmatrix")
def codegen_ptx_stmatrix(trans, num, dtype, shape, space, smem_ptr, *src_handles):
trans = bool(trans)
num = int(num)
dtype_str = parse_str(dtype)
if dtype_str.startswith("."):
dtype_str = dtype_str[1:]
n_regs = num
if len(src_handles) != n_regs:
raise ValueError(
f"stmatrix .x{num}.{dtype_str} codegen expects {n_regs} src handles, "
f"got {len(src_handles)}"
)
result = CODEGEN_REGISTRY["tirx._ptx_stmatrix_impl"](
[smem_ptr, *src_handles, trans, num, dtype, shape, space]
)
return result[0] if isinstance(result, tuple) else result
@@ -0,0 +1,161 @@
# 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
"""NVSHMEM intrinsics. Each backend call is one ``device_intrinsic(...)``."""
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
_NVSHMEM = ("nvshmem",)
# =============================================================================
# No-arg helpers: PE queries, quiet, fence, barrier_all.
# =============================================================================
for _op, _call, _ret, _tvm_ret in [
("nvshmem_my_pe", "nvshmem_my_pe", "int32_t", "int32"),
("nvshmem_n_pes", "nvshmem_n_pes", "int32_t", "int32"),
("nvshmem_quiet", "nvshmem_quiet", "void", None),
("nvshmem_fence", "nvshmem_fence", "void", None),
("nvshmem_barrier_all", "nvshmem_barrier_all", "void", None),
]:
device_intrinsic(
_op,
body=(" " + (f"return {_call}();" if _ret != "void" else f"{_call}();")),
return_type=_ret,
tvm_return_type=_tvm_ret,
extra_deps=_NVSHMEM,
)
del _op, _call, _ret, _tvm_ret
# =============================================================================
# RMA get/put (thread/warp/block).
# =============================================================================
_RMA_SIG = "(void *dest, const void *source, size_t nelems, int pe)"
for _op, _backend_call in [
("nvshmem_getmem_nbi", "nvshmem_getmem_nbi"),
("nvshmem_putmem_nbi", "nvshmem_putmem_nbi"),
("nvshmem_getmem_nbi_warp", "nvshmemx_getmem_nbi_warp"),
("nvshmem_putmem_nbi_warp", "nvshmemx_putmem_nbi_warp"),
("nvshmem_getmem_nbi_block", "nvshmemx_getmem_nbi_block"),
("nvshmem_putmem_nbi_block", "nvshmemx_putmem_nbi_block"),
]:
device_intrinsic(
_op,
c_signature=_RMA_SIG,
body=f" {_backend_call}(dest, source, nelems, pe);",
extra_deps=_NVSHMEM,
)
del _op, _backend_call
# =============================================================================
# Signal / wait_until — each backend call is one device_intrinsic. String
# attrs (sig_op, cmp) are mapped to NVSHMEM integer constants in the
# user-facing dispatcher below.
# =============================================================================
_SIG_OP_VAL = {"set": 0, "add": 1}
_CMP_VAL = {"eq": 0, "ne": 1, "gt": 2, "ge": 3, "lt": 4, "le": 5}
def _resolve_attr(value, table, label):
s = value if isinstance(value, str) else value.value
if s not in table:
raise ValueError(f"Unsupported {label}: {s}")
return table[s]
device_intrinsic(
"_nvshmem_signal_op_impl",
helper_name="tvm_builtin_nvshmem_signal_op",
c_signature="(uint64_t* sig_addr, uint64_t signal, int sig_op, int pe)",
body=" nvshmemx_signal_op(sig_addr, signal, sig_op, pe);",
extra_deps=_NVSHMEM,
)
@register_codegen("nvshmem_signal_op")
def codegen_nvshmem_signal_op(sig_addr, signal, sig_op, pe):
"""Map ``sig_op`` (string) to its NVSHMEM int constant, then forward."""
sig_op_int = _resolve_attr(sig_op, _SIG_OP_VAL, "signal op")
result = CODEGEN_REGISTRY["tirx._nvshmem_signal_op_impl"]([sig_addr, signal, sig_op_int, pe])
return result
# nvshmem_<type>_wait_until — one device_intrinsic per supported type.
_WAIT_UNTIL_TYPES = {"uint64_t": "uint64", "uint64": "uint64"}
for _c_type, _suffix in [("uint64_t", "uint64")]:
device_intrinsic(
f"_nvshmem_{_suffix}_wait_until_impl",
helper_name=f"tvm_builtin_nvshmem_{_suffix}_wait_until",
c_signature=f"({_c_type}* ivar, int cmp, {_c_type} cmp_value)",
body=f" nvshmem_{_suffix}_wait_until(ivar, cmp, cmp_value);",
extra_deps=_NVSHMEM,
)
del _c_type, _suffix
@register_codegen("nvshmem_wait_until")
def codegen_nvshmem_wait_until(ivar, cmp, cmp_value, type):
"""Dispatch to the type-specific wait_until helper after mapping ``cmp``
(string) to its NVSHMEM int constant."""
type_str = type if isinstance(type, str) else type.value
if type_str not in _WAIT_UNTIL_TYPES:
raise ValueError(f"Unsupported type for nvshmem_wait_until: {type_str}")
suffix = _WAIT_UNTIL_TYPES[type_str]
cmp_int = _resolve_attr(cmp, _CMP_VAL, "cmp operation")
result = CODEGEN_REGISTRY[f"tirx._nvshmem_{suffix}_wait_until_impl"]([ivar, cmp_int, cmp_value])
return result
# putmem_signal_nbi (thread / warp / block) — three scope-specific helpers.
_PUTMEM_SIG_SIG = (
"(void* dest, const void* source, size_t nelems, "
"uint64_t* sig_addr, uint64_t signal, int sig_op, int pe)"
)
for _scope_suffix, _backend_call in [
("", "nvshmem_putmem_signal_nbi"),
("_warp", "nvshmemx_putmem_signal_nbi_warp"),
("_block", "nvshmemx_putmem_signal_nbi_block"),
]:
device_intrinsic(
f"_nvshmem_putmem_signal_nbi{_scope_suffix}_impl",
helper_name=f"tvm_builtin_nvshmem_putmem_signal_nbi{_scope_suffix}",
c_signature=_PUTMEM_SIG_SIG,
body=f" {_backend_call}(dest, source, nelems, sig_addr, signal, sig_op, pe);",
extra_deps=_NVSHMEM,
)
del _scope_suffix, _backend_call
def _make_putmem_signal_dispatcher(scope_suffix):
@register_codegen(f"nvshmem_putmem_signal_nbi{scope_suffix}")
def _codegen(dest, source, nelems, sig_addr, signal, sig_op, pe):
sig_op_int = _resolve_attr(sig_op, _SIG_OP_VAL, "signal op")
result = CODEGEN_REGISTRY[f"tirx._nvshmem_putmem_signal_nbi{scope_suffix}_impl"](
[dest, source, nelems, sig_addr, signal, sig_op_int, pe]
)
return result
return _codegen
for _suffix in ("", "_warp", "_block"):
_make_putmem_signal_dispatcher(_suffix)
del _suffix
@@ -0,0 +1,77 @@
# 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.
"""Codegen registry for CUDA HW ops.
User-facing Python wrappers are hand-written in :mod:`tvm.tirx.op` so that
editors / static analyzers (Cursor, Pyright) can see their signatures. This
module only handles the backend codegen side.
"""
import functools
import tvm_ffi
CODEGEN_REGISTRY = {}
def _canonical_device_intrin_name(op_name: str) -> str:
if not op_name.startswith("tirx."):
return op_name
basename = op_name[len("tirx.") :]
if "." in basename:
return op_name
for prefix, namespace in (
("cuda_", "cuda"),
("ptx_", "ptx"),
("nvshmem_", "nvshmem"),
("nki_", "nki"),
):
if basename.startswith(prefix):
return f"tirx.{namespace}.{basename[len(prefix) :]}"
return op_name
@tvm_ffi.register_global_func("tirx.intrinsics.cuda.get_codegen")
def get_codegen(op):
"""get the codegen function for a given op"""
return CODEGEN_REGISTRY.get(op, None)
def register_codegen(op, backend="cuda"):
"""Register a codegen function for a given op.
The codegen function should return a ``cuda_func_call`` statement, and
optionally a list of tags that the codegen function needs.
"""
def decorator(func):
full_op_name = "tirx." + op
canonical_op_name = _canonical_device_intrin_name(full_op_name)
op_names = {full_op_name, canonical_op_name}
@functools.wraps(func)
def wrapper(arg_list):
res = func(*arg_list) # pylint: disable=not-callable
if isinstance(res, tuple):
return res[0], res[1]
return res, list()
for op_name in op_names:
CODEGEN_REGISTRY[op_name] = wrapper
return wrapper
return decorator
@@ -0,0 +1,570 @@
# 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=invalid-name
"""Synchronization primitives.
PTX side:
* ``bar.arrive`` / ``bar.sync`` — named-barrier alias of ``barrier.arrive/sync``
* ``fence{.sem}.scope`` / ``fence.proxy.async`` / ``fence.mbarrier_init``
* ``barrier.cluster.arrive`` / ``barrier.cluster.wait``
* ``mbarrier.init`` / ``mbarrier.arrive[.expect_tx]`` (local + remote) / ``mbarrier.try_wait``
* ``elect.sync`` — warp leader election
* warp-vote ``__any_sync``
CUDA-side helpers:
* ``__threadfence`` / ``__syncwarp`` / ``__syncthreads`` / ``__syncthreads_and|or``
* cooperative-groups grid sync
* cluster sync (open-coded ``barrier.cluster.arrive/wait`` pair)
* warpgroup sync (``bar.sync``)
"""
from tvm.tirx.operator.intrinsics._common import (
CLUSTER_BARRIER_SEM,
FENCE_PROXY_ASYNC_SPACE,
FENCE_SCOPE,
FENCE_SEM,
)
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .utils import parse_str
# =============================================================================
# bar.arrive / bar.sync — alias of barrier.arrive/sync. 1 form each.
# bar.sync a, b ;
# bar.arrive a, b ;
# =============================================================================
device_intrinsic(
"ptx_bar_arrive",
c_signature="(int name_bar_id, int thread_count)",
body=(
' asm volatile("bar.arrive %0, %1;" : : "r"(name_bar_id), "r"(thread_count) : "memory");'
),
)
device_intrinsic(
"ptx_bar_sync",
c_signature="(int name_bar_id, int thread_count)",
body=(
' asm volatile("bar.sync %0, %1;" : : "r"(name_bar_id), "r"(thread_count) : "memory");'
),
)
# =============================================================================
# fence{.sem}.scope — 1 form (sem/scope are modifier values).
# =============================================================================
def _ptx_fence(sem, scope):
sem, scope = parse_str(sem), parse_str(scope)
assert sem in FENCE_SEM, f"invalid fence sem {sem!r}, expected one of {FENCE_SEM}"
assert scope in FENCE_SCOPE, f"invalid fence scope {scope!r}, expected one of {FENCE_SCOPE}"
return (
f"tvm_builtin_ptx_fence_{sem}_{scope}",
f' asm volatile("fence.{sem}.{scope};" ::: "memory");',
)
device_intrinsic(
"ptx_fence",
n_attrs=2,
helper_name=lambda sem, scope: _ptx_fence(sem, scope)[0],
body=lambda sem, scope: _ptx_fence(sem, scope)[1],
)
# =============================================================================
# fence.proxy.async{.<space>} — 1 form, optional .space modifier.
# =============================================================================
def _ptx_fence_proxy_async(space):
space = parse_str(space)
assert space in FENCE_PROXY_ASYNC_SPACE, (
f"invalid fence.proxy.async space {space!r}, expected one of {FENCE_PROXY_ASYNC_SPACE}"
)
suffix = f".{space}" if space else ""
name_safe = "_" + space.replace("::", "_").replace(".", "_") if space else ""
return (
f"tvm_builtin_ptx_fence_proxy_async{name_safe}",
f' asm volatile("fence.proxy.async{suffix};" ::: "memory");',
)
device_intrinsic(
"ptx_fence_proxy_async",
n_attrs=1,
helper_name=lambda space: _ptx_fence_proxy_async(space)[0],
body=lambda space: _ptx_fence_proxy_async(space)[1],
)
# =============================================================================
# fence.mbarrier_init.release.cluster — 1 form, no operands.
# =============================================================================
device_intrinsic(
"ptx_fence_mbarrier_init",
body=' asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory");',
)
# =============================================================================
# barrier.cluster.arrive{.sem}{.aligned} — 1 form.
# =============================================================================
def _ptx_barrier_cluster_arrive(sem, aligned):
sem = parse_str(sem)
aligned = bool(int(aligned)) if hasattr(aligned, "value") else bool(aligned)
assert sem in CLUSTER_BARRIER_SEM, (
f"invalid cluster.arrive sem {sem!r}, expected one of {CLUSTER_BARRIER_SEM}"
)
sem_suffix = f".{sem}" if sem else ""
aligned_suffix = ".aligned" if aligned else ""
name_sem = "_" + sem.replace("::", "_").replace(".", "_") if sem else ""
name_aligned = "_aligned" if aligned else ""
return (
f"tvm_builtin_ptx_barrier_cluster_arrive{name_sem}{name_aligned}",
f' asm volatile("barrier.cluster.arrive{sem_suffix}{aligned_suffix};" ::: "memory");',
)
device_intrinsic(
"ptx_barrier_cluster_arrive",
n_attrs=2,
helper_name=lambda sem, aligned: _ptx_barrier_cluster_arrive(sem, aligned)[0],
body=lambda sem, aligned: _ptx_barrier_cluster_arrive(sem, aligned)[1],
)
# =============================================================================
# barrier.cluster.wait{.acquire}{.aligned} — 1 form.
# =============================================================================
def _ptx_barrier_cluster_wait(acquire, aligned):
acquire = bool(int(acquire)) if hasattr(acquire, "value") else bool(acquire)
aligned = bool(int(aligned)) if hasattr(aligned, "value") else bool(aligned)
acq_suffix = ".acquire" if acquire else ""
aligned_suffix = ".aligned" if aligned else ""
return (
f"tvm_builtin_ptx_barrier_cluster_wait"
f"{'_acquire' if acquire else ''}{'_aligned' if aligned else ''}",
f' asm volatile("barrier.cluster.wait{acq_suffix}{aligned_suffix};" ::: "memory");',
)
device_intrinsic(
"ptx_barrier_cluster_wait",
n_attrs=2,
helper_name=lambda acquire, aligned: _ptx_barrier_cluster_wait(acquire, aligned)[0],
body=lambda acquire, aligned: _ptx_barrier_cluster_wait(acquire, aligned)[1],
)
# =============================================================================
# clusterlaunchcontrol.try_cancel / query_cancel — Blackwell Cluster Launch
# Control (CLC) work-stealing, written from the PTX ISA spec (section
# "clusterlaunchcontrol", PTX ISA 8.6). try_cancel async-requests cancelling the
# next cluster's launch, writing a 16B response to smem + signalling mbar. query
# decodes the response: on success it extracts the cancelled cluster's first
# ctaid.x (via the get_first_ctaid::x form); a single uint32 is returned, with
# 0xFFFFFFFF as the "no work stolen" sentinel (a device helper returns one scalar).
# =============================================================================
device_intrinsic(
"ptx_clc_try_cancel",
c_signature="(void* handle, void* mbar)",
body=(
" unsigned int addr = (unsigned int)__cvta_generic_to_shared(handle);\n"
" unsigned int bar = (unsigned int)__cvta_generic_to_shared(mbar);\n"
" asm volatile(\n"
' "clusterlaunchcontrol.try_cancel.async.shared::cta.mbarrier::complete_tx::bytes"\n'
' ".multicast::cluster::all.b128 [%0], [%1];\\n"\n'
' :: "r"(addr), "r"(bar) : "memory");'
),
)
device_intrinsic(
"ptx_clc_query_cancel",
c_signature="(void* handle)",
return_type="uint32_t",
tvm_return_type="uint32",
body=(
" unsigned int addr = (unsigned int)__cvta_generic_to_shared(handle);\n"
" unsigned int first_ctaid_x;\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred canceled;\\n"\n'
' ".reg .b128 response;\\n"\n'
' "ld.shared.b128 response, [%1];\\n"\n'
' "clusterlaunchcontrol.query_cancel.is_canceled.pred.b128 canceled, response;\\n"\n'
' "mov.u32 %0, 0xffffffff;\\n"\n'
' "@canceled clusterlaunchcontrol.query_cancel.get_first_ctaid::x.b32.b128"\n'
' " %0, response;\\n"\n'
' "}\\n"\n'
' : "=r"(first_ctaid_x) : "r"(addr) : "memory");\n'
' asm volatile("fence.proxy.async.shared::cta;\\n" ::: "memory");\n'
" return first_ctaid_x;"
),
)
# =============================================================================
# mbarrier.init.shared.b64 [addr], count ; — 1 form.
# =============================================================================
device_intrinsic(
"ptx_mbarrier_init",
c_signature="(void* barrier, int thread_count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
' asm volatile("mbarrier.init.shared.b64 [%0], %1;"'
' : : "r"(barrier_addr), "r"(thread_count) : "memory");'
),
)
# =============================================================================
# mbarrier.arrive — local + remote (cluster-mapped) forms. 2 PTX forms.
# Form local: mbarrier.arrive.shared.b64 _, [bar];
# Form remote: { setp+@p mapa.shared::cluster.u32 + @p mbarrier.arrive.shared::cluster.b64 }
# Dispatcher picks by arg count (1 vs 3).
# =============================================================================
device_intrinsic(
"_ptx_mbarrier_arrive_local",
helper_name="tvm_builtin_ptx_mbarrier_arrive",
c_signature="(void* barrier)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
' asm volatile("mbarrier.arrive.shared.b64 _, [%0];"\n'
' :: "r"(barrier_addr) : "memory");'
),
)
device_intrinsic(
"_ptx_mbarrier_arrive_remote",
helper_name="tvm_builtin_ptx_mbarrier_arrive_remote",
c_signature="(void* barrier, int cta_id, int pred)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred p;\\n"\n'
' ".reg .b32 remAddr32;\\n"\n'
' "setp.ne.s32 p, %2, 0;\\n"\n'
' "@p mapa.shared::cluster.u32 remAddr32, %0, %1;\\n"\n'
' "@p mbarrier.arrive.shared::cluster.b64 _, [remAddr32];\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr), "r"(cta_id), "r"(pred) : "memory");'
),
)
# Same cross-CTA arrive, but with an explicit arrival-count operand
# (``..., [remAddr32], count``). Matches the ``tma::cluster::arrive`` spelling.
device_intrinsic(
"_ptx_mbarrier_arrive_remote_count",
helper_name="tvm_builtin_ptx_mbarrier_arrive_remote_count",
c_signature="(void* barrier, int cta_id, int pred, int count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred p;\\n"\n'
' ".reg .b32 remAddr32;\\n"\n'
' "setp.ne.s32 p, %2, 0;\\n"\n'
' "@p mapa.shared::cluster.u32 remAddr32, %0, %1;\\n"\n'
' "@p mbarrier.arrive.shared::cluster.b64 _, [remAddr32], %3;\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr), "r"(cta_id), "r"(pred), "r"(count) : "memory");'
),
)
@register_codegen("ptx_mbarrier_arrive")
def _codegen_mbarrier_arrive(*args):
"""Dispatch by arg count: 1 -> local, 3 -> remote, 4 -> remote+count."""
if len(args) == 1:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_local"](list(args))
elif len(args) == 3:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_remote"](list(args))
elif len(args) == 4:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_remote_count"](list(args))
else:
raise ValueError(f"ptx_mbarrier_arrive expects 1, 3, or 4 args, got {len(args)}")
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# mbarrier.arrive.expect_tx — local + remote (cluster-mapped) forms.
# =============================================================================
device_intrinsic(
"_ptx_mbarrier_arrive_expect_tx_local",
helper_name="tvm_builtin_ptx_mbarrier_arrive_expect_tx",
c_signature="(void* barrier, int byte_count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
' asm volatile("mbarrier.arrive.expect_tx.shared.b64 _, [%0], %1;"\n'
' :: "r"(barrier_addr), "r"(byte_count) : "memory");'
),
)
device_intrinsic(
"_ptx_mbarrier_arrive_expect_tx_remote",
helper_name="tvm_builtin_ptx_mbarrier_arrive_expect_tx_remote",
c_signature="(void* barrier, int cta_id, int pred, int byte_count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred p;\\n"\n'
' ".reg .b32 remAddr32;\\n"\n'
' "setp.ne.s32 p, %2, 0;\\n"\n'
' "@p mapa.shared::cluster.u32 remAddr32, %0, %1;\\n"\n'
' "@p mbarrier.arrive.expect_tx.shared::cluster.b64 _, [remAddr32], %3;\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr), "r"(cta_id), "r"(pred), "r"(byte_count) : "memory");'
),
)
@register_codegen("ptx_mbarrier_arrive_expect_tx")
def _codegen_mbarrier_arrive_expect_tx(*args):
"""Dispatch by arg count: 2 -> local, 4 -> remote. Remote arg order from
the user is (bar, byte_count, cta_id, pred); reorder to match the helper
signature (bar, cta_id, pred, byte_count)."""
if len(args) == 2:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_expect_tx_local"](list(args))
elif len(args) == 4:
bar, byte_count, cta_id, pred = args
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_expect_tx_remote"](
[bar, cta_id, pred, byte_count]
)
else:
raise ValueError(f"ptx_mbarrier_arrive_expect_tx expects 2 or 4 args, got {len(args)}")
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# mbarrier.try_wait.parity.shared::cta.b64 — 1 form. Body wraps the asm in a
# label loop (TIRx convention; the magic ``ticks = 0x989680`` is the timeout
# hint in ns).
# =============================================================================
device_intrinsic(
"ptx_mbarrier_try_wait",
c_signature="(void* barrier, int phase)",
body=(
" unsigned int barrier_addr_int = __cvta_generic_to_shared(barrier);\n"
" unsigned int ticks = 0x989680;\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred P1;\\n"\n'
' "LAB_WAIT:\\n"\n'
' "mbarrier.try_wait.parity.shared::cta.b64 P1, [%0], %1, %2;\\n"\n'
' "@P1 bra.uni DONE;\\n"\n'
' "bra.uni LAB_WAIT;\\n"\n'
' "DONE:\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr_int), "r"(phase), "r"(ticks) : "memory");'
),
)
# mbarrier.try_wait.parity.acquire.cluster — cluster-scope acquire wait used for
# cross-CTA barrier handshakes (e.g. the tmem-finished handoff).
device_intrinsic(
"ptx_mbarrier_try_wait_acquire_cluster",
c_signature="(void* barrier, int phase)",
body=(
" unsigned int barrier_addr_int = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred P1;\\n"\n'
' "LAB_WAIT_AC:\\n"\n'
' "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 P1, [%0], %1;\\n"\n'
' "@P1 bra.uni DONE_AC;\\n"\n'
' "bra.uni LAB_WAIT_AC;\\n"\n'
' "DONE_AC:\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr_int), "r"(phase) : "memory");'
),
)
# =============================================================================
# mbarrier.try_wait.parity — ONE-SHOT non-blocking variant. Returns true
# if the requested parity has already been reached, false otherwise.
# The TIRx-standard ``ptx_mbarrier_try_wait`` above wraps this in a
# label loop that retries until success; this one-shot form is the
# building block for bounded-retry debug waits (Nymph's
# ``debug_bounded_wait`` lowering mode wraps it in a Python-counted
# loop so the kernel cannot hang forever at a mis-protocoled wait).
# =============================================================================
device_intrinsic(
"ptx_mbarrier_try_wait_once",
c_signature="(void* barrier, int phase, int ticks)",
return_type="uint32_t",
body=(
" unsigned int barrier_addr_int = __cvta_generic_to_shared(barrier);\n"
" unsigned int ticks_u = (unsigned int)ticks;\n"
" unsigned int result;\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred P1;\\n"\n'
' "mbarrier.try_wait.parity.shared::cta.b64 P1, [%1], %2, %3;\\n"\n'
' "selp.u32 %0, 1, 0, P1;\\n"\n'
' "}\\n"\n'
' : "=r"(result) : "r"(barrier_addr_int), "r"(phase), "r"(ticks_u) : "memory");\n'
" return result;"
),
)
# =============================================================================
# elect.sync — TIRx uses the CUDA builtin ``tvm_builtin_elect_one_sync()``
# helper (declared in the CUDA header tags), not direct PTX.
# =============================================================================
device_intrinsic(
"ptx_elect_sync",
helper_name="tvm_builtin_elect_one_sync_op",
return_type="uint32_t",
body=" return tvm_builtin_elect_one_sync();",
extra_deps=("elect_one_sync",),
)
# =============================================================================
# __any_sync — warp-vote (pure CUDA helper).
# =============================================================================
device_intrinsic(
"ptx_any_sync",
c_signature="(unsigned mask, int pred)",
body=" return __any_sync(mask, pred);",
return_type="int",
tvm_return_type="int32",
)
# =============================================================================
# CUDA-side sync helpers (zero-arg void unless noted).
# =============================================================================
device_intrinsic("cuda_thread_fence", body=" __threadfence();")
device_intrinsic("cuda_warp_sync", body=" __syncwarp();")
device_intrinsic("cuda_cta_sync", body=" __syncthreads();")
device_intrinsic(
"cuda_grid_sync",
body=" namespace cg = cooperative_groups;\n cg::this_grid().sync();",
extra_deps=("cooperative_groups",),
)
device_intrinsic(
"cuda_cluster_sync",
body=(' asm("barrier.cluster.arrive.aligned;");\n asm("barrier.cluster.wait.aligned;");'),
)
device_intrinsic(
"cuda_warpgroup_sync",
c_signature="(int name_bar_id)",
body=' asm volatile("bar.sync %0, 128;" : : "r"(name_bar_id));',
)
device_intrinsic(
"cuda_syncthreads_and",
c_signature="(int predicate)",
body=" return __syncthreads_and(predicate);",
return_type="int",
tvm_return_type="int32",
)
device_intrinsic(
"cuda_syncthreads_or",
c_signature="(int predicate)",
body=" return __syncthreads_or(predicate);",
return_type="int",
tvm_return_type="int32",
)
# =============================================================================
# Additional mbarrier, grid-sync, and warp collective helpers.
# =============================================================================
# PTX mbarrier parity wait form:
# mbarrier.test_wait.parity{.sem.scope}{.shared{::cta}}.b64 waitComplete, [addr], phaseParity;
def _mbarrier_test_wait_parity_parts(_barrier, _phase, sem, scope, space):
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
if sem and sem not in ("acquire", "relaxed"):
raise ValueError(f"Unsupported mbarrier.test_wait.parity sem {sem!r}")
if scope and scope not in ("cta", "cluster"):
raise ValueError(f"Unsupported mbarrier.test_wait.parity scope {scope!r}")
if space not in ("shared", "shared::cta"):
raise ValueError(f"Unsupported mbarrier.test_wait.parity space {space!r}")
sem_scope = f".{sem}.{scope}" if sem else ""
name = (
"tvm_builtin_ptx_mbarrier_test_wait_parity"
f"{('_' + sem + '_' + scope) if sem else ''}_{space.replace('::', '_')}_b64"
)
body = (
" unsigned int ready = 0;\n"
" asm volatile(\n"
' "{\\n\\t"\n'
' ".reg .pred P1; \\n\\t"\n'
f' "mbarrier.test_wait.parity{sem_scope}.{space}.b64 P1, [%1], %2; \\n\\t"\n'
' "selp.b32 %0, 1, 0, P1; \\n\\t"\n'
' "}" : "=r"(ready) : "r"((unsigned int)__cvta_generic_to_shared(barrier)), '
'"r"(phase) : "memory");\n'
" return ready;"
)
return name, body
device_intrinsic(
"ptx_mbarrier_test_wait_parity",
n_attrs=3,
helper_name=lambda *a: _mbarrier_test_wait_parity_parts(*a)[0],
c_signature="(void* barrier, int phase)",
return_type="unsigned int",
tvm_return_type="uint32",
body=lambda *a: _mbarrier_test_wait_parity_parts(*a)[1],
)
device_intrinsic(
"cuda_ballot_sync",
helper_name="tvm_builtin_ballot_sync",
c_signature="(unsigned int mask, int pred)",
return_type="unsigned int",
body=" return __ballot_sync(mask, pred);",
)
device_intrinsic(
"cuda_reduce_add_sync_u32",
helper_name="tvm_builtin_reduce_add_sync_u32",
c_signature="(unsigned int mask, unsigned int value)",
return_type="unsigned int",
body=" return __reduce_add_sync(mask, value);",
)
device_intrinsic(
"cuda_reduce_min_sync_u32",
helper_name="tvm_builtin_reduce_min_sync_u32",
c_signature="(unsigned int mask, unsigned int value)",
return_type="unsigned int",
body=" return __reduce_min_sync(mask, value);",
)
# =============================================================================
# griddepcontrol.wait / griddepcontrol.launch_dependents (sm_90+)
# Programmatic Dependent Launch (PDL) synchronization. Both carry memory
# clobber to prevent CSE / cross-barrier reordering.
# =============================================================================
device_intrinsic(
"ptx_griddepcontrol_wait",
body=' asm volatile("griddepcontrol.wait;" ::: "memory");',
)
device_intrinsic(
"ptx_griddepcontrol_launch_dependents",
body=' asm volatile("griddepcontrol.launch_dependents;" ::: "memory");',
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
# 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.
"""PTX data types for CUDA codegen."""
import enum
import tvm_ffi
from_string_func = tvm_ffi.get_global_func("tirx.intrinsics.cuda.PTXDTypeFromString")
to_string_func = tvm_ffi.get_global_func("tirx.intrinsics.cuda.PTXDTypeToString")
class PTXDataType(enum.Enum):
"""
A Python equivalent of the provided C++ DataType enum class.
Inherits from IntEnum so that members behave both as enum members
and as integers, mirroring the C++ behavior.
see also src/target/source/ptx.cc
"""
INT4 = 0
UINT4 = 1
INT8 = 2
UINT8 = 3
INT16 = 4
UINT16 = 5
INT32 = 6
UINT32 = 7
INT64 = 8
UINT64 = 9
FLOAT4_E2M1FN = 10
FLOAT6_E2M3FN = 11
FLOAT6_E3M2FN = 12
FLOAT8_E4M3FN = 13
FLOAT8_E4M3FNUZ = 14
FLOAT8_E5M2 = 15
FLOAT8_E8M0FNU = 16
FLOAT16 = 17
BFLOAT16 = 18
FLOAT16X2 = 19
FLOAT32 = 20
TENSOR_FLOAT32 = 21
FLOAT64 = 22
BIT1 = 23
BIT8 = 24
BIT16 = 25
BIT32 = 26
BIT64 = 27
@classmethod
def from_string(cls, s_type: str) -> "PTXDataType":
return PTXDataType(from_string_func(s_type))
def to_string(self) -> str:
return to_string_func(self.value)
@@ -0,0 +1,82 @@
# 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.
"""Common utility functions for CUDA op codegen."""
def parse_str(arg) -> str:
"""Parse TIR StringImm or Python str to a plain str.
TIR StringImm values stringify to quoted strings, e.g., ``'"float16"'``;
Python strs do not. Idempotent — passing an already-parsed str returns it
unchanged, so dispatchers that parse once before forwarding to inner
codegens won't double-strip the value.
"""
s = str(arg)
if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
return s[1:-1]
return s
def is_power_of_two(n: int) -> bool:
"""Check if n is a power of two."""
return n > 0 and (n & (n - 1)) == 0
def validate_cta_group(cta_group, context: str = "") -> int:
"""Validate that cta_group is 1 or 2 and return it as int.
Args:
cta_group: The cta_group value (can be int or TIR IntImm)
context: Optional context string for error message (e.g., "allocating Tensor Memory")
Returns:
The validated cta_group as int
Raises:
ValueError: If cta_group is not 1 or 2
"""
cta_group = int(cta_group)
if cta_group not in [1, 2]:
ctx = f" involved in {context}" if context else ""
raise ValueError(
f"The number of cta_group{ctx} is incorrect, expected 1 or 2, got {cta_group}"
)
return cta_group
def validate_power_of_two_range(value, min_val: int, max_val: int, name: str) -> int:
"""Validate that value is within range and is a power of two.
Args:
value: The value to validate
min_val: Minimum allowed value (inclusive)
max_val: Maximum allowed value (inclusive)
name: Name of the parameter for error messages
Returns:
The validated value as int
Raises:
ValueError: If value is out of range or not a power of two
"""
value = int(value)
if not (min_val <= value <= max_val and is_power_of_two(value)):
raise ValueError(
f"The {name} is invalid, expect a value within range [{min_val}, {max_val}] "
f"and be a power of 2, got {value}"
)
return value
@@ -0,0 +1,403 @@
# 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 WGMMA operations (Hopper warpgroup MMA).
One ``device_intrinsic`` registration per PTX form table entry. Bodies are
hand-written ``asm volatile(...)`` strings. Variable-arity register vectors
(``mma_async`` accumulators / A fragments) materialize via the same
device_intrinsic with an attr-driven ``c_signature`` callable.
"""
import tvm
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .types import PTXDataType
from .utils import parse_str
# =============================================================================
# wgmma.fence / commit_group / wait_group — one PTX form each.
# =============================================================================
device_intrinsic(
"ptx_wgmma_fence",
helper_name="ptx_wgmma_fence",
body=' asm volatile("wgmma.fence.sync.aligned;" ::: "memory");',
)
device_intrinsic(
"ptx_wgmma_commit_group",
helper_name="ptx_wgmma_commit_group",
body=' asm volatile("wgmma.commit_group.sync.aligned;" ::: "memory");',
)
device_intrinsic(
"ptx_wgmma_wait_group",
n_attrs=1,
helper_name=lambda n: f"ptx_wgmma_wait_group_{int(n)}",
body=lambda n: f' asm volatile("wgmma.wait_group.sync.aligned {int(n)};" ::: "memory");',
)
# =============================================================================
# wgmma_encode_matrix_descriptor — pure-C bitfield struct fill (no asm).
# =============================================================================
device_intrinsic(
"ptx_wgmma_encode_matrix_descriptor",
helper_name="ptx_wgmma_encode_matrix_descriptor",
c_signature="(uint64_t* desc, void* addr, int ldo, int sdo, int swizzle)",
body=(
" GmmaDescriptor _desc{}; // value-init: reading uncovered pad bits is UB\n"
"\n"
" switch (swizzle) {\n"
" case 0: _desc.bitfield.layout_type_ = uint8_t(0); break; // No swizzle\n"
" case 1: _desc.bitfield.layout_type_ = uint8_t(3); break; // 32B swizzle\n"
" case 2: _desc.bitfield.layout_type_ = uint8_t(2); break; // 64B swizzle\n"
" case 3: _desc.bitfield.layout_type_ = uint8_t(1); break; // 128B swizzle\n"
" }\n"
"\n"
" uint32_t start_address = __cvta_generic_to_shared(addr);\n"
" _desc.bitfield.start_address_ = static_cast<uint16_t>(start_address >> 4);\n"
"\n"
" constexpr uint8_t base_offset = 0;\n"
" _desc.bitfield.base_offset_ = base_offset;\n"
"\n"
" _desc.bitfield.stride_byte_offset_ = static_cast<uint32_t>(sdo);\n"
" _desc.bitfield.leading_byte_offset_ = static_cast<uint32_t>(ldo);\n"
"\n"
" *desc = (uint64_t)_desc;"
),
extra_deps=("gmma_descriptor",),
)
# =============================================================================
# wgmma_noop_barrier — empty asm with one inout register operand. Two
# device_intrinsic calls, one per supported dtype; dispatcher picks the form
# based on the operand's runtime dtype.
# =============================================================================
device_intrinsic(
"ptx_wgmma_noop_barrier_uint32",
helper_name="ptx_wgmma_fence_uint32_t",
c_signature="(uint32_t reg)",
body=' asm volatile("" : "+r"(reg) :: "memory");',
)
device_intrinsic(
"ptx_wgmma_noop_barrier_float32",
helper_name="ptx_wgmma_fence_float",
c_signature="(float reg)",
body=' asm volatile("" : "+f"(reg) :: "memory");',
)
@register_codegen("ptx_wgmma_noop_barrier")
def codegen_ptx_wgmma_noop_barrier(reg):
dtype = str(reg.dtype)
dtype_enum = PTXDataType.from_string(dtype)
if dtype_enum == PTXDataType.UINT32:
op_name = "tirx.ptx_wgmma_noop_barrier_uint32"
elif dtype_enum == PTXDataType.FLOAT32:
op_name = "tirx.ptx_wgmma_noop_barrier_float32"
else:
raise ValueError(f"Only support uint32/float32 for wgmma_fence, but got {dtype}.")
result = CODEGEN_REGISTRY[op_name]([reg])
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# wgmma.mma_async ss / rs — 2 PTX form table entries. Accumulator count and
# A-register count vary with (M, N, K, in_dtype) but are fully determined by
# attrs at codegen time.
#
# Args layout for ss form (forwarded operand args first, then 9 attr args):
# *p_acc[0..num_accums-1], p_descA, p_descB, p_scaleD,
# M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
#
# Args layout for rs form:
# *p_acc[0..num_accums-1], *p_A[0..num_A_regs-1], p_descB, p_scaleD,
# M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
# =============================================================================
def _coerce_wgmma_attrs(attrs):
"""Decode the trailing 9 attrs (M, N, K, in_dtype, out_dtype, transA,
transB, scaleA, scaleB) into native Python types."""
M, N, K = int(attrs[0]), int(attrs[1]), int(attrs[2])
in_dtype = parse_str(attrs[3])
out_dtype = parse_str(attrs[4])
transA = bool(int(attrs[5])) if hasattr(attrs[5], "value") else bool(attrs[5])
transB = bool(int(attrs[6])) if hasattr(attrs[6], "value") else bool(attrs[6])
scaleA = bool(int(float(attrs[7])))
scaleB = bool(int(float(attrs[8])))
if out_dtype != "float32":
raise ValueError("WGMMA codegen only supports float32 as output dtype.")
allow_transpose = in_dtype in {"float16", "bfloat16"}
if not allow_transpose and (transA or transB):
raise ValueError("Transpose is only supported for .f16/.bf16 types in WGMMA.")
return M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, allow_transpose
def _safe(s):
return s.replace("::", "_").replace(".", "_")
def _wgmma_helper_name(prefix, M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB):
return (
f"{prefix}_{M}x{N}x{K}_{_safe(in_dtype)}_{_safe(out_dtype)}"
f"_{1 if scaleA else 0}_{1 if scaleB else 0}"
f"_{1 if transA else 0}_{1 if transB else 0}"
)
def _wgmma_in_bits(in_dtype):
return tvm.runtime.DataType(in_dtype).bits
def _wgmma_ss_parts(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, allow_transpose = (
_coerce_wgmma_attrs(args[-9:])
)
num_accums = M * N // 128
name = _wgmma_helper_name(
"ptx_wgmma_mma_async_ss", M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
)
sig = (
"("
+ ", ".join(
[f"float& p_acc{i}" for i in range(num_accums)]
+ ["uint64_t p_descA", "uint64_t p_descB", "int p_scaleD"]
)
+ ")"
)
descA_idx = num_accums
descB_idx = num_accums + 1
scaleD_idx = num_accums + 2
scaleA_idx = num_accums + 3
scaleB_idx = num_accums + 4
transA_idx = num_accums + 5
transB_idx = num_accums + 6
accum_r_list = ", ".join(f"%{i}" for i in range(num_accums))
accum_constraints = ", ".join(f'"+f"(p_acc{i})' for i in range(num_accums))
itype = PTXDataType.from_string(in_dtype)
otype = PTXDataType.from_string(out_dtype)
if allow_transpose:
transpose_r_code = f", %{transA_idx}, %{transB_idx}"
transpose_constraints = f', "n"({1 if transA else 0}), "n"({1 if transB else 0})'
else:
transpose_r_code = ""
transpose_constraints = ""
instr = (
f"wgmma.mma_async.sync.aligned.m{M}n{N}k{K}"
f"{otype.to_string()}{itype.to_string()}{itype.to_string()}"
)
asm_inputs = (
f'"l"(p_descA), "l"(p_descB), "r"(p_scaleD),'
f' "n"({1 if scaleA else 0}), "n"({1 if scaleB else 0})'
f"{transpose_constraints}"
)
body = (
" asm volatile(\n"
' "{ \\n"\n'
' ".reg .pred p;\\n"\n'
f' "setp.ne.b32 p, %{scaleD_idx}, 0;\\n"\n'
f' "{instr} "\n'
f' "{{{accum_r_list}}},"\n'
f' "%{descA_idx}, %{descB_idx},"\n'
f' "p, %{scaleA_idx}, %{scaleB_idx}{transpose_r_code};\\n"\n'
' "}\\n"\n'
f" : {accum_constraints}\n"
f" : {asm_inputs}\n"
" );"
)
return name, sig, body
device_intrinsic(
"_ptx_wgmma_mma_async_ss_impl",
n_attrs=9,
helper_name=lambda *a: _wgmma_ss_parts(*a)[0],
c_signature=lambda *a: _wgmma_ss_parts(*a)[1],
body=lambda *a: _wgmma_ss_parts(*a)[2],
)
def _wgmma_rs_parts(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, allow_transpose = (
_coerce_wgmma_attrs(args[-9:])
)
num_accums = M * N // 128
in_bits = _wgmma_in_bits(in_dtype)
num_A_regs = M * K // 128 // (32 // in_bits)
name = _wgmma_helper_name(
"ptx_wgmma_mma_async_rs", M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
)
sig = (
"("
+ ", ".join(
[f"float& p_acc{i}" for i in range(num_accums)]
+ [f"uint32_t& p_A{i}" for i in range(num_A_regs)]
+ ["uint64_t p_descB", "int p_scaleD"]
)
+ ")"
)
accum_r_list = ", ".join(f"%{i}" for i in range(num_accums))
A_reg_r_list = ", ".join(f"%{num_accums + i}" for i in range(num_A_regs))
base_idx = num_accums + num_A_regs
descB_idx = base_idx
scaleD_idx = base_idx + 1
scaleA_idx = base_idx + 2
scaleB_idx = base_idx + 3
transB_idx = base_idx + 4
accum_constraints = ", ".join(f'"+f"(p_acc{i})' for i in range(num_accums))
A_reg_constraints = ", ".join(f'"r"(p_A{i})' for i in range(num_A_regs))
itype = PTXDataType.from_string(in_dtype)
otype = PTXDataType.from_string(out_dtype)
if allow_transpose:
transpose_r_code = f", %{transB_idx}"
transpose_constraints = f', "n"({1 if transB else 0})'
else:
transpose_r_code, transpose_constraints = "", ""
instr = (
f"wgmma.mma_async.sync.aligned.m{M}n{N}k{K}"
f"{otype.to_string()}{itype.to_string()}{itype.to_string()}"
)
asm_inputs = (
f'{A_reg_constraints}, "l"(p_descB), "r"(p_scaleD),'
f' "n"({1 if scaleA else 0}), "n"({1 if scaleB else 0})'
f"{transpose_constraints}"
)
body = (
" asm volatile(\n"
' "{ \\n"\n'
' ".reg .pred p;\\n"\n'
f' "setp.ne.b32 p, %{scaleD_idx}, 0;\\n"\n'
f' "{instr} "\n'
f' "{{{accum_r_list}}},"\n'
f' "{{{A_reg_r_list}}}, %{descB_idx},"\n'
f' "p, %{scaleA_idx}, %{scaleB_idx}{transpose_r_code};\\n"\n'
' "}\\n"\n'
f" : {accum_constraints}\n"
f" : {asm_inputs}\n"
" );"
)
return name, sig, body
device_intrinsic(
"_ptx_wgmma_mma_async_rs_impl",
n_attrs=9,
helper_name=lambda *a: _wgmma_rs_parts(*a)[0],
c_signature=lambda *a: _wgmma_rs_parts(*a)[1],
body=lambda *a: _wgmma_rs_parts(*a)[2],
)
# User-facing wrappers: just normalise types + reorder positional args to
# put operands first, then attrs, matching the schema convention.
def _wgmma_user_wrapper_ss(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, scaleD, descA, descB, *accums = (
args
)
M = int(M)
N = int(N)
K = int(K)
in_dtype = parse_str(in_dtype)
out_dtype = parse_str(out_dtype)
transA = bool(transA)
transB = bool(transB)
scaleA = bool(int(float(scaleA)))
scaleB = bool(int(float(scaleB)))
expected = M * N // 128
if len(accums) != expected:
raise ValueError(
"The number of arguments is incorrect. Expected "
f"{12 + expected} total args (meaning {expected} accumulator args), "
f"but got {len(accums)}."
)
return [
*accums,
descA,
descB,
scaleD,
M,
N,
K,
in_dtype,
out_dtype,
transA,
transB,
scaleA,
scaleB,
]
@register_codegen("ptx_wgmma_mma_async_ss")
def codegen_ptx_wgmma_mma_async_ss(*args):
forwarded = _wgmma_user_wrapper_ss(*args)
result = CODEGEN_REGISTRY["tirx._ptx_wgmma_mma_async_ss_impl"](forwarded)
return result[0] if isinstance(result, tuple) else result
def _wgmma_user_wrapper_rs(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, scaleD, descB, *reg_list = args
M = int(M)
N = int(N)
K = int(K)
in_dtype = parse_str(in_dtype)
out_dtype = parse_str(out_dtype)
transA = bool(transA)
transB = bool(transB)
scaleA = bool(int(float(scaleA)))
scaleB = bool(int(float(scaleB)))
if out_dtype != "float32":
raise ValueError("This generator only supports float32 as the output dtype for WGMMA.")
in_dtype_bits = tvm.runtime.DataType(in_dtype).bits
if in_dtype_bits is None:
raise ValueError(f"Bit width not defined for input dtype: {in_dtype}")
expected_A_cnt = M * K // 128 // (32 // in_dtype_bits)
expected_accm_cnt = M * N // 128
if len(reg_list) != expected_A_cnt + expected_accm_cnt:
raise ValueError(
f"Incorrect number of A registers. Expected {expected_A_cnt}, got {len(reg_list)}"
)
A_regs = reg_list[:expected_A_cnt]
accums = reg_list[expected_A_cnt:]
return [
*accums,
*A_regs,
descB,
scaleD,
M,
N,
K,
in_dtype,
out_dtype,
transA,
transB,
scaleA,
scaleB,
]
@register_codegen("ptx_wgmma_mma_async_rs")
def codegen_ptx_wgmma_mma_async_rs(*args):
forwarded = _wgmma_user_wrapper_rs(*args)
result = CODEGEN_REGISTRY["tirx._ptx_wgmma_mma_async_rs_impl"](forwarded)
return result[0] if isinstance(result, tuple) else result