blob: 0e4737292f52228f6fabc505c4adccd35208c4a4 [file]
# 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.
"""Tests for the table-driven PTX dialect (``T.ptx``)."""
import itertools
import os
import re
import shutil
import numpy as np
import pytest
import tvm
from tvm.ir import Op
from tvm.script import tirx as T
from tvm.testing import env
TARGET = tvm.target.Target("cuda")
requires_nvcc = pytest.mark.skipif(shutil.which("nvcc") is None, reason="nvcc not available")
def _cuda_source(func) -> str:
with TARGET:
mod = tvm.compile(tvm.IRModule({"main": func}), target=TARGET, tir_pipeline="tirx")
return mod.mod.imports[0].inspect_source("cuda")
# Architecture the certification assembles at. Families whose ISA floor is
# higher (tcgen05, clusterlaunchcontrol, several cp.async.bulk forms) MUST be
# certified at their own floor: assembling them below it makes ptxas report
# legal variants as illegal, which would then get baked into a check().
PTX_ARCH = os.environ.get("PTX_ARCH", "sm_90")
def _assert_ptxas_ok(src: str, rdc: bool = False, arch: str = PTX_ARCH) -> None:
"""Assemble through ptxas (cubin) — `-ptx` alone never validates inline asm."""
from tvm.support import nvcc
options = ["-rdc=true"] if rdc else None
nvcc.compile_cuda(src, target_format="cubin", arch=arch, options=options, compiler="nvcc")
def test_ptx_registration():
from tvm.backend.cuda.codegen.registry import CODEGEN_REGISTRY
from tvm.backend.cuda.ptx.table import TABLE, escape_token
assert hasattr(T, "ptx")
for entry in TABLE.values():
op = Op.get(entry.op_name) # raises if unregistered
assert op.get_attr("TCallEffectKind") is not None, entry.name
# The printer name is the attribute path a program types, so it is the
# *escaped* family: `and`/`or`/`not` (ISA 9.7.9) are Python keywords and
# print as `T.ptx.and_`. Identity for every other family.
family = escape_token(entry.family) # several entries may share a mnemonic
assert op.get_attr("TScriptPrinterName") == f"ptx.{family}", entry.name
assert entry.op_name in CODEGEN_REGISTRY, entry.name
def test_ptx_prefetch_codegen():
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
T.ptx.prefetch.global_.L2(A.ptr_to([0]))
A[tx] = T.float32(0) # keep-alive store so the buffer is not elided
src = _cuda_source(kernel)
assert "prefetch.global.L2 [%0];" in src
assert "tvm_builtin_ptx_prefetch_global_L2" in src
def test_ptx_ld_st_codegen():
@T.prim_func
def kernel(a_ptr: T.handle, b_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
B = T.match_buffer(b_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
# Declare the register, then name it as an operand — the PTX model.
val = T.local_scalar("uint32")
T.ptx.ld.global_.acquire.gpu.b32(val, A.ptr_to([0]))
T.ptx.st.release.gpu.global_.b32(B.ptr_to([0]), val)
B[tx] = B[tx]
src = _cuda_source(kernel)
# Modifier tokens render in table slot order (sem, scope, space, type)
# regardless of the order they were written in the chain.
assert "ld.acquire.gpu.global.b32 %0, [%1];" in src
assert "st.release.gpu.global.b32 [%0], %1;" in src
assert "tvm_builtin_ptx_ld_acquire_gpu_global_b32" in src
assert "tvm_builtin_ptx_st_release_gpu_global_b32" in src
@requires_nvcc
def test_ptx_ld_s32_wide_destination_codegen():
"""A signed scalar load may sign-extend into a wider destination register."""
@T.prim_func
def kernel(a_ptr: T.handle, out_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "int32")
Out = T.match_buffer(out_ptr, (32,), "int64")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
wide = T.local_scalar("int64")
T.ptx.ld.global_.s32(wide, A.ptr_to([tx]))
Out[tx] = wide
src = _cuda_source(kernel)
assert "tvm_builtin_ptx_ld_global_s32_s64(int64_t& __d" in src
assert 'asm volatile("ld.global.s32 %0, [%1];" : "=l"(__d)' in src
assert "cvt." not in src
_assert_ptxas_ok(src)
def test_ptx_st_shared_coercion():
@T.prim_func
def kernel(out_ptr: T.handle):
out = T.match_buffer(out_ptr, (1,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
smem = T.alloc_buffer((4,), "uint32", scope="shared")
if tx == 0:
# Shared-space slot fed a shared-scope pointer: engine must
# auto-wrap with cvta_generic_to_shared.
T.ptx.st.shared__cta.b32(smem.ptr_to([0]), T.uint32(7))
T.cuda.cta_sync()
out[0] = smem[0]
src = _cuda_source(kernel)
assert "st.shared::cta.b32 [%0], %1;" in src
assert "cvta_generic_to_shared" in src
def test_ptx_explicit_cvta():
@T.prim_func
def kernel(out_ptr: T.handle):
out = T.match_buffer(out_ptr, (1,), "uint64")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
smem = T.alloc_buffer((4,), "uint32", scope="shared")
smem[tx % 4] = T.uint32(0)
if tx == 0:
T.ptx.cvta.to.shared.u64(out[0], smem.data)
src = _cuda_source(kernel)
assert "cvta.to.shared.u64 %0, %1;" in src
assert "tvm_builtin_ptx_cvta_to_shared_u64" in src
def test_ptx_red_codegen():
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
T.ptx.red.relaxed.gpu.global_.add.u32(A.ptr_to([0]), T.uint32(1))
A[0] = A[0]
src = _cuda_source(kernel)
assert "red.relaxed.gpu.global.add.u32 [%0], %1;" in src
def test_ptx_predication_codegen():
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
flag: T.uint32 = T.uint32(0)
if tx == 0:
flag = T.uint32(1)
T.ptx.red.relaxed.gpu.global_.add.u32(A.ptr_to([0]), T.uint32(1), pred=flag)
A[tx] = A[tx]
src = _cuda_source(kernel)
assert "tvm_builtin_ptx_red_relaxed_gpu_global_add_u32_pred" in src
assert "setp.ne.b32 p, %2, 0; @p red.relaxed.gpu.global.add.u32 [%0], %1;" in src
def test_ptx_red_vector_codegen_and_roundtrip():
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (16,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
half = T.local_scalar("uint16")
packed = T.local_scalar("uint32")
value = T.local_scalar("float32")
policy = T.local_scalar("uint64")
T.ptx.red.relaxed.gpu.global_.add.noftz.v8.f16(
A.ptr_to([0]), half, half, half, half, half, half, half, half
)
T.ptx.red.release.sys.global_.max.noftz.L2__cache_hint.v4.f16x2(
A.ptr_to([1]), packed, packed, packed, packed, policy
)
T.ptx.red.global_.add.v4.f32(A.ptr_to([2]), value, value, value, value)
A[tx % 16] = A[tx % 16]
src = _cuda_source(kernel)
assert (
"red.relaxed.gpu.global.add.noftz.v8.f16 [%0], {%1, %2, %3, %4, %5, %6, %7, %8};"
) in src
assert (
"red.release.sys.global.max.noftz.L2::cache_hint.v4.f16x2 [%0], {%1, %2, %3, %4}, %5;"
) in src
assert "red.global.add.v4.f32 [%0], {%1, %2, %3, %4};" in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="already a 32-bit pair"):
@T.prim_func
def packed_v8(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
v = T.local_scalar("uint32")
T.ptx.red.global_.add.noftz.v8.f16x2(A.ptr_to([0]), v, v, v, v, v, v, v, v)
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def f32_max(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "float32")
T.device_entry()
v = T.local_scalar("float32")
T.ptx.red.global_.max.v2.f32(A.ptr_to([0]), v, v)
def test_ptx_atom_bitbucket_codegen_and_roundtrip():
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (16,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
word = T.local_scalar("uint32")
half = T.local_scalar("uint16")
value = T.local_scalar("float32")
T.ptx.atom.global_.add.u32(A.ptr_to([0]), word)
T.ptx.atom.global_.cas.b32(A.ptr_to([1]), word, word)
T.ptx.atom.global_.exch.b32(A.ptr_to([2]), word)
T.ptx.atom.global_.add.noftz.f16(A.ptr_to([3]), half)
T.ptx.atom.global_.add.noftz.v2.f16(A.ptr_to([4]), half, half)
T.ptx.atom.global_.add.v2.f32(A.ptr_to([5]), value, value, pred=tx)
A[tx % 16] = A[tx % 16]
src = _cuda_source(kernel)
for text in (
"atom.global.add.u32 _, [%0], %1;",
"atom.global.cas.b32 _, [%0], %1, %2;",
"atom.global.exch.b32 _, [%0], %1;",
"atom.global.add.noftz.f16 _, [%0], %1;",
"atom.global.add.noftz.v2.f16 _, [%0], {%1, %2};",
"@p atom.global.add.v2.f32 _, [%0], {%1, %2};",
):
assert text in src, text
assert "atom.global.add.v2.f32 {_," not in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
@requires_nvcc
def test_ptx_predicated_destination_preserves_old_value():
@T.prim_func
def kernel(a_ptr: T.handle, out_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "float32")
Out = T.match_buffer(out_ptr, (32,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
value: T.float32 = T.float32(0)
pred: T.uint32 = T.cast(tx == 0, "uint32")
T.ptx.ld.global_.f32(value, A.ptr_to([0]), pred=pred, preserve_dst=True)
T.ptx.ex2.approx.ftz.f32(value, value, pred=pred, preserve_dst=True)
Out[tx] = value
src = _cuda_source(kernel)
assert "tvm_builtin_ptx_ld_global_f32_pred_keep" in src
assert "tvm_builtin_ptx_ex2_approx_ftz_f32_pred_keep" in src
assert '"+f"(__d)' in src
_assert_ptxas_ok(src)
@requires_nvcc
def test_ptx_predicated_destination_is_undefined_by_default():
@T.prim_func
def kernel(a_ptr: T.handle, out_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "float32")
Out = T.match_buffer(out_ptr, (32,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
value = T.local_scalar("float32")
pred: T.uint32 = T.cast(tx == 0, "uint32")
T.ptx.ld.global_.f32(value, A.ptr_to([0]), pred=pred)
Out[tx] = T.if_then_else(pred != 0, value, T.float32(0))
src = _cuda_source(kernel)
assert "tvm_builtin_ptx_ld_global_f32_pred_undef" in src
assert '"=f"(__d)' in src
assert '"+f"(__d)' not in src
_assert_ptxas_ok(src)
def test_ptx_string_form_matches_chain():
chain_call = T.ptx.ld.global_.acquire.gpu.b32
string_call = T.ptx["ld.acquire.gpu.global.b32"]
def make(fn):
@T.prim_func
def kernel(a_ptr: T.handle, b_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
B = T.match_buffer(b_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
fn(B[0], A.ptr_to([0]))
B[tx] = B[tx]
return kernel
tvm.ir.assert_structural_equal(make(chain_call), make(string_call))
def test_ptx_92_cp_bulk_exact_renderings():
"""PTX 9.2 bulk-copy and non-tensor reduction forms render exactly."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
cases = (
(
"cp_async_bulk_g2s_cta",
dict(
api="async",
kind="bulk",
dst="shared::cta",
src="global",
completion="mbarrier::complete_tx::bytes",
cache="L2::cache_hint",
ignore_oob="ignore_oob",
),
"cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes."
"L2::cache_hint.ignore_oob [%0], [%1], %2, %3, %4, [%5], %6;",
),
(
"cp_reduce_async_bulk_s2c",
dict(
op="reduce",
api="async",
kind="bulk",
dst="shared::cluster",
src="shared::cta",
completion="mbarrier::complete_tx::bytes",
redop="xor",
type="b32",
),
"cp.reduce.async.bulk.shared::cluster.shared::cta."
"mbarrier::complete_tx::bytes.xor.b32 [%0], [%1], %2, [%3];",
),
(
"cp_reduce_async_bulk_s2g",
dict(
op="reduce",
api="async",
kind="bulk",
dst="global",
src="shared::cta",
completion="bulk_group",
cache="L2::cache_hint",
redop="add",
noftz="noftz",
type="f16",
),
"cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint."
"add.noftz.f16 [%0], [%1], %2, %3;",
),
)
for name, modifiers, expected in cases:
entry = TABLE[name]
_, _, source = render_variant(entry, tokens_for(entry, **modifiers))
assert f'asm volatile("{expected}"' in source, source
def test_ptx_93_sm103a_exact_renderings():
"""Representative PTX 9.3 forms keep their documented operand shapes."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
cases = (
(
"clmad",
dict(mode="hi", type="u64"),
"clmad.hi.u64 %0, %1, %2, %3;",
),
(
"ld",
dict(mmio="mmio", sem="acquire", scope="sys", space="global", type="u32"),
"ld.mmio.acquire.sys.global.u32 %0, [%1];",
),
(
"st",
dict(mmio="mmio", sem="release", scope="sys", space="global", type="u32"),
"st.mmio.release.sys.global.u32 [%0], %1;",
),
(
"multimem_st_async",
dict(sem="release", scope="sys", space="global", type="u32"),
"multimem.st.async.release.sys.global.u32 [%0], %1;",
),
(
"cp_async_bulk_s2c",
dict(
api="async",
kind="bulk",
sem="relaxed",
scope="cluster",
dst="shared::cluster",
src="shared::cta",
completion="mbarrier::complete_tx::bytes",
type="b128",
),
"cp.async.bulk.relaxed.cluster.shared::cluster.shared::cta."
"mbarrier::complete_tx::bytes.b128 [%0], [%1], %2, [%3];",
),
(
"fabric_try_put_counted",
dict(
action="try_put",
api="async",
src="shared::cta",
completion=("mbarrier::complete_tx::16B.mbarrier::report::fabric.counted::bytes"),
sem="relaxed",
scope="sys",
type="b128",
),
"fabric.try_put.async.shared::cta.mbarrier::complete_tx::16B."
"mbarrier::report::fabric.counted::bytes.relaxed.sys.b128 "
"[%0, %1, %2], [%3], %4, [%5];",
),
(
"fence_proxy_fabric",
dict(
proxy="proxy",
direction="generic::fabric",
proxykind="alias",
sem="release",
scope="sys",
),
"fence.proxy.generic::fabric.alias.release.sys;",
),
(
"mbarrier_check_layout",
dict(action="check_layout", layout="layout::v1", space="shared::cta", type="b64"),
"mbarrier.check_layout.layout::v1.shared::cta.b64 pd0, [%1];",
),
(
"tcgen05_ld_red",
dict(
action="ld",
red="red",
sync="sync",
aligned="aligned",
shape="32x32b",
num="x2",
type="f32",
redop="max",
abs="abs",
nan="NaN",
),
"tcgen05.ld.red.sync.aligned.32x32b.x2.max.abs.NaN.f32 {%0, %1}, %2, [%3];",
),
(
"tcgen05_ld_red",
dict(
action="ld",
red="red",
sync="sync",
aligned="aligned",
shape="32x32b",
num="x2",
redop="min",
type="u32",
),
"tcgen05.ld.red.sync.aligned.32x32b.x2.min.u32 {%0, %1}, %2, [%3];",
),
)
for name, modifiers, expected in cases:
entry = TABLE[name]
_, _, source = render_variant(entry, tokens_for(entry, **modifiers))
assert expected in source, source
# The split shape carries the immHalfSplitoff immediate after the address.
split = TABLE["tcgen05_ld_red_split"]
_, _, source = render_variant(
split,
tokens_for(
split,
action="ld",
red="red",
sync="sync",
aligned="aligned",
shape="16x32bx2",
num="x2",
redop="min",
type="s32",
),
imms=("0",),
)
assert "tcgen05.ld.red.sync.aligned.16x32bx2.x2.min.s32 {%0, %1}, %2, [%3], 0;" in source, (
source
)
report = TABLE["mbarrier_test_wait_report_value"]
_, _, source = render_variant(
report,
tokens_for(
report,
action="test_wait",
phase_type="phase_type::primary",
space="shared::cta",
type="b64",
),
)
assert ".reg .b8 raw_report_value;" in source
assert "pd0|pd1, raw_report_value, [%3], %4;" in source
def test_ptx_92_cp_reduce_negative_grids():
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
from tvm.ir.type import PointerType, PrimType
shared_reduce = TABLE["cp_reduce_async_bulk_s2c"]
with pytest.raises(ValueError, match=r"\.add to \.shared::cluster takes"):
tokens_for(
shared_reduce,
op="reduce",
api="async",
kind="bulk",
dst="shared::cluster",
src="shared::cta",
completion="mbarrier::complete_tx::bytes",
redop="add",
type="b32",
)
global_reduce = TABLE["cp_reduce_async_bulk_s2g"]
with pytest.raises(ValueError, match=r"add\.f16 requires \.noftz"):
tokens_for(
global_reduce,
op="reduce",
api="async",
kind="bulk",
dst="global",
src="shared::cta",
completion="bulk_group",
redop="add",
type="f16",
)
shared_ptr = tvm.tirx.Var("shared_ptr", PointerType(PrimType("uint32"), "shared"))
global_ptr = tvm.tirx.Var("global_ptr", PointerType(PrimType("uint32"), "global"))
size = tvm.tirx.Var("size", "uint32")
with pytest.raises(ValueError, match=r"\.add to \.shared::cluster takes"):
T.ptx[
"cp.reduce.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes.add.b32"
](shared_ptr, shared_ptr, size, shared_ptr)
with pytest.raises(ValueError, match=r"add\.f16 requires \.noftz"):
T.ptx["cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f16"](
global_ptr, shared_ptr, size
)
# `.add.noftz.f32` is PTX ISA 9.4: the pre-9.4 entry refuses it and names
# the sibling, which renders the documented spelling and is the single
# dispatch hit for the string form.
with pytest.raises(ValueError, match=r"PTX 9\.4 sibling"):
tokens_for(
global_reduce,
op="reduce",
api="async",
kind="bulk",
dst="global",
src="shared::cta",
completion="bulk_group",
redop="add",
noftz="noftz",
type="f32",
)
sibling = TABLE["cp_reduce_async_bulk_s2g_f32_noftz"]
_, _, source = render_variant(
sibling,
tokens_for(
sibling,
op="reduce",
api="async",
kind="bulk",
dst="global",
src="shared::cta",
completion="bulk_group",
redop="add",
noftz="noftz",
type="f32",
),
)
assert (
"cp.reduce.async.bulk.global.shared::cta.bulk_group.add.noftz.f32 [%0], [%1], %2;" in source
)
T.ptx["cp.reduce.async.bulk.global.shared::cta.bulk_group.add.noftz.f32"](
global_ptr, shared_ptr, size
)
T.ptx["multimem_cp.reduce.async.bulk.relaxed.gpu.global.shared::cta.bulk_group.add.noftz.f32"](
global_ptr, shared_ptr, size
)
def test_ptx_92_cp_bulk_roundtrip():
@T.prim_func
def kernel(src: T.Buffer((64,), "uint32"), dst: T.Buffer((64,), "uint32")):
T.device_entry()
smem = T.alloc_buffer((64,), "uint32", scope="shared")
mbar = T.alloc_buffer((2,), "uint64", scope="shared")
T.ptx[
"cp.async.bulk.shared::cta.global."
"mbarrier::complete_tx::bytes.L2::cache_hint.ignore_oob"
](
smem.ptr_to([0]),
src.ptr_to([0]),
T.uint32(16),
T.uint32(0),
T.uint32(0),
mbar.ptr_to([0]),
T.uint64(0),
)
T.ptx[
"cp.reduce.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes.xor.b32"
](smem.ptr_to([16]), smem.ptr_to([0]), T.uint32(16), mbar.ptr_to([0]))
T.ptx["cp.reduce.async.bulk.global.shared::cta.bulk_group.add.noftz.f16"](
dst.ptr_to([0]), smem.ptr_to([0]), T.uint32(16)
)
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_trace_time_errors():
# Global ld fed a raw uint32 address.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="shared state space"):
@T.prim_func
def bad_global_addr(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.ld.global_.b32(out[0], T.uint32(0))
# Bogus modifier token.
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def bad_modifier(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.ld.global_.bogus.b32(out[0], T.uint32(0))
# A floating register is not compatible with an integer instruction type,
# even when it is wider (the relaxed ld/st rule admits a bit carrier, not a
# differently typed floating register).
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="must have dtype"):
@T.prim_func
def bad_value_dtype(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.ptx.st.global_.u32(A.ptr_to([0]), T.float64(1.0))
# Missing required modifier (no type token).
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="missing required modifier"):
@T.prim_func
def missing_type(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.ptx.st.global_(A.ptr_to([0]), T.uint32(0))
# Per-slot legal tokens whose combination is illegal PTX: acquire
# requires a scope — rejected by the entry's check function.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="requires a scope"):
@T.prim_func
def acquire_without_scope(out: T.Buffer((1,), "uint32"), a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.ptx.ld.global_.acquire.b32(out[0], A.ptr_to([0]))
# A float is not an address in any state space.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="pointer or uint64 handle"):
@T.prim_func
def bad_addr_dtype(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.ld.global_.b32(out[0], T.float32(0))
def test_ptx_destination_errors():
"""A destination is a register the caller declared: it must be a writable lvalue."""
# A floating destination is incompatible with an integer instruction type;
# relaxed widening does not numerically convert between the two.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="must have dtype"):
@T.prim_func
def wrong_dst_dtype(out: T.Buffer((1,), "float64"), a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.ptx.ld.global_.u32(out[0], A.ptr_to([0]))
# A T.let binding is immutable, so it cannot be written into. This is the
# gate that keeps the analyzer from re-expanding one call into N.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="writable scalar"):
@T.prim_func
def let_destination(out: T.Buffer((1,), "uint32"), a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
bound: T.let = out[0] + T.uint32(1)
T.ptx.ld.global_.b32(bound, A.ptr_to([0]))
# An rvalue is not a destination either.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="writable scalar"):
@T.prim_func
def rvalue_destination(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.ptx.ld.global_.b32(T.uint32(0), A.ptr_to([0]))
def test_ptx_register_group_codegen():
"""A `.lanes > 1` operand renders as braces in the asm, flat params in C.
`{%1, %2}` is ONE PTX operand occupying two registers (ISA 9.7.10.4), which
is why the lane count appears nowhere in the instruction text -- `mov.b64`
names the aggregate width, and the operand shape carries the rest.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
packed = T.local_scalar("uint64")
lo = T.local_scalar("float32")
hi = T.local_scalar("float32")
T.ptx.mov.b64(packed, A[0], A[1]) # pack: one dst, a 2-register source
T.ptx.mov.b64(lo, hi, packed) # unpack: a 2-register dst, one source
A[tx % 4] = lo + hi
src = _cuda_source(kernel)
assert "mov.b64 %0, {%1, %2};" in src
assert "mov.b64 {%0, %1}, %2;" in src
# Both shapes share the mnemonic; the operand shape picks the entry, which
# is the same information ptxas resolves them by.
assert "tvm_builtin_ptx_mov_pack_b32x2_b64_u64_f32(" in src
assert "tvm_builtin_ptx_mov_unpack_b32x2_b64_f32_u64(" in src
def test_ptx_register_group_errors():
"""A register group is one operand: its arity is fixed and its lanes agree."""
# No `mov` shape takes two operands, so nothing in the family matches.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"expects \d+ operand"):
@T.prim_func
def wrong_arity(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
packed = T.local_scalar("uint64")
T.ptx.mov.b64(packed, A[0])
A[0] = T.uint32(0)
# Each lane of a destination group is its own register the caller declared,
# so each has to be an lvalue -- not just the first.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="writable scalar"):
@T.prim_func
def non_lvalue_lane(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
packed = T.local_scalar("uint64")
lo = T.local_scalar("uint32")
T.ptx.mov.b64(lo, A[0] + T.uint32(1), packed)
A[0] = T.uint32(0)
# Lanes disagreeing on dtype: legal for each lane alone (both are 32-bit),
# but the group is one operand with one C parameter type, so binding the
# odd lane to it would be a numeric conversion.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="must have one dtype"):
@T.prim_func
def mixed_lane_dtypes(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
packed = T.local_scalar("uint64")
f = T.local_scalar("float32")
u = T.local_scalar("uint32")
T.ptx.mov.b64(packed, f, u)
A[0] = T.uint32(0)
# A bare float literal names no dtype: on a .b32 lane it could be the
# float's bits or the number, and it used to silently become T.uint32(1).
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="is ambiguous"):
@T.prim_func
def bare_float_literal(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
packed = T.local_scalar("uint64")
T.ptx.mov.b64(packed, 1.5, 2.5)
A[0] = T.uint32(0)
# An explicit constant is accepted and picks the float32 helper.
@T.prim_func
def typed_literal(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
packed = T.local_scalar("uint64")
T.ptx.mov.b64(packed, T.float32(1.5), T.float32(2.5))
A[tx % 4] = A[tx % 4]
assert "mov_pack_b32x2_b64_u64_f32" in _cuda_source(typed_literal)
def test_ptx_optional_operand_arity_dispatch():
"""A no-count and a counted syntax line share a mnemonic, split by arity.
This only works because `pred` is keyword-only: the old positional-pred
fallback let every entry also accept arity+1 calls, so both lines matched a
two-operand call and dispatch was ambiguous. The pred marker in the Call
layout keeps the printed form exact (a predicated no-count arrive must not
re-parse as a counted one).
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
bar = T.alloc_buffer((2,), "uint64", scope="shared")
T.ptx.bar.sync(T.uint32(0))
T.ptx.bar.sync(T.uint32(0), T.uint32(64))
T.ptx.mbarrier.arrive.shared.b64(bar.ptr_to([0]))
T.ptx.mbarrier.arrive.shared.b64(bar.ptr_to([0]), T.uint32(2))
T.ptx.mbarrier.arrive.shared.b64(bar.ptr_to([1]), pred=T.uint32(1))
A[tx % 4] = A[tx % 4]
src = _cuda_source(kernel)
assert "bar.sync %0;" in src
assert "bar.sync %0, %1;" in src
assert "mbarrier.arrive.shared.b64 _, [%0];" in src
assert "mbarrier.arrive.shared.b64 _, [%0], %1;" in src
assert "@p mbarrier.arrive.shared.b64 _, [%0];" in src
# The predicated no-count arrive survives a print/parse round trip as
# itself -- the pred marker is what stops the count entry from absorbing
# the predicate as a count.
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_mbarrier_92_shapes_render_and_roundtrip():
"""PTX 9.2 noComplete sink/state and wait shapes render exactly."""
@T.prim_func
def kernel(out_ptr: T.handle):
out = T.match_buffer(out_ptr, (4,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
bar = T.alloc_buffer((2,), "uint64", scope="shared")
state = T.local_scalar("uint64")
pending = T.local_scalar("uint32")
wait_complete = T.local_scalar("uint32")
T.ptx.mbarrier.init.shared.b64(bar.ptr_to([0]), T.uint32(1))
T.ptx.mbarrier.arrive.noComplete.shared.b64(bar.ptr_to([0]), T.uint32(1))
T.ptx.mbarrier.arrive_drop.noComplete.shared__cta.b64(state, bar.ptr_to([1]), T.uint32(1))
T.ptx.mbarrier.pending_count.b64(pending, state)
T.ptx.mbarrier.test_wait.shared__cta.b64(wait_complete, bar.ptr_to([0]), state)
T.ptx.mbarrier.try_wait.parity.relaxed.cluster.shared__cta.b64(
wait_complete, bar.ptr_to([1]), T.uint32(0), T.uint32(20)
)
out[tx % 4] = pending + wait_complete
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE
selected = (
("mbarrier_init", ("init", "", "shared", "b64"), False),
(
"mbarrier_arrive_no_complete_sink",
("arrive", "noComplete", "", "", "shared", "b64"),
False,
),
(
"mbarrier_arrive_drop_no_complete",
("arrive_drop", "noComplete", "", "", "shared::cta", "b64"),
False,
),
("mbarrier_pending_count", ("pending_count", "", "b64"), False),
("mbarrier_test_wait", ("test_wait", "", "", "", "shared::cta", "b64"), False),
(
"mbarrier_try_wait_parity",
("try_wait", "parity", "", "relaxed", "cluster", "shared::cta", "b64"),
False,
),
)
src = "\n".join(
render_variant(TABLE[name], tokens, predicated)[2] for name, tokens, predicated in selected
)
for text in (
"mbarrier.init.shared.b64 [%0], %1;",
"mbarrier.arrive.noComplete.shared.b64 _, [%0], %1;",
"mbarrier.arrive_drop.noComplete.shared::cta.b64 %0, [%1], %2;",
"mbarrier.pending_count.b64 %0, %1;",
"mbarrier.test_wait.shared::cta.b64 pd0, [%1], %2;",
"mbarrier.try_wait.parity.relaxed.cluster.shared::cta.b64 pd0, [%1], %2, %3;",
):
assert text in src, text
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_mbarrier_92_state_carriers():
from tvm.backend.cuda.ptx.table import TABLE, mods, operand_dtypes, variants
pending = TABLE["mbarrier_pending_count"]
for tokens in variants(pending):
mod_map = mods(pending, tokens)
assert operand_dtypes(pending.operands[1], mod_map) == ("uint64", "int64")
for name in ("mbarrier_arrive_no_complete", "mbarrier_arrive_drop_no_complete"):
entry = TABLE[name]
for tokens in variants(entry):
mod_map = mods(entry, tokens)
assert operand_dtypes(entry.operands[0], mod_map) == ("uint64", "int64")
def test_ptx_bit_width_axis():
"""A `.bN` operand takes any dtype of that width, each with its own helper.
PTX ISA 5.2: "The bit-size type is compatible with any fundamental type
having the same size." The helper follows the dtype the caller actually
holds, so the value binds its own register class and no conversion is
emitted -- handing a float to a uint32_t parameter would instead be a
numeric conversion and emit `cvt.rzi.u32.f32`.
"""
@T.prim_func
def kernel(a_ptr: T.handle, o_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "float32")
Out = T.match_buffer(o_ptr, (4,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
fv = T.local_scalar("float32")
sv = T.local_scalar("int32")
T.ptx.ld.global_.b32(fv, A.ptr_to([0])) # .b32 destination, float32
T.ptx.ld.global_.b32(sv, A.ptr_to([1])) # .b32 destination, int32
T.ptx.st.global_.b32(Out.ptr_to([0]), fv) # .b32 source, float32
Out[tx % 4] = A[tx % 4]
src = _cuda_source(kernel)
# One instruction text, three signatures, named by the dtypes they carry.
assert "tvm_builtin_ptx_ld_global_b32_f32(float& __d" in src
assert "tvm_builtin_ptx_ld_global_b32_s32(int32_t& __d" in src
assert "tvm_builtin_ptx_st_global_b32_f32(const void* __addr, float __value" in src
assert '"=f"(__d)' in src and '"=r"(__d)' in src
assert src.count("ld.global.b32 %0, [%1];") >= 1
# The float binds "f" directly. Routing it through the canonical uint32_t
# parameter would have been a numeric conversion, not a bit pun.
assert "cvt." not in src
# The canonical dtype keeps the unsuffixed name it had before the axis.
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
ld = TABLE["ld"]
tokens = tokens_for(ld, space="global", type="b32")
assert render_variant(ld, tokens)[1] == "tvm_builtin_ptx_ld_global_b32"
def test_ptx_relaxed_load_store_typing():
"""Scalar/vector ld, st and ldu accept ISA section 9.4.1's wider register carriers."""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (8,), "uint64")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
wide = T.local_scalar("uint64")
signed0 = T.local_scalar("int64")
signed1 = T.local_scalar("int64")
T.ptx.ld.global_.b8(wide, A.ptr_to([0]))
T.ptx.ld.global_.v2.s8(signed0, signed1, A.ptr_to([1]))
T.ptx.st.global_.b8(A.ptr_to([2]), wide)
T.ptx.st.global_.v2.u16(A.ptr_to([3]), wide, wide)
T.ptx.ldu.global_.u16(wide, A.ptr_to([4]))
T.ptx.ldu.global_.v2.s8(signed0, signed1, A.ptr_to([5]))
A[tx % 8] = wide + T.uint64(signed0 + signed1)
src = _cuda_source(kernel)
for text in (
"ld.global.b8 %0, [%1];",
"ld.global.v2.s8 {%0, %1}, [%2];",
"st.global.b8 [%0], %1;",
"st.global.v2.u16 [%0], {%1, %2};",
"ldu.global.u16 %0, [%1];",
"ldu.global.v2.s8 {%0, %1}, [%2];",
):
assert text in src, text
# All data operands above use 64-bit registers; widening is expressed by
# the inline-asm constraint and does not insert a numeric conversion.
assert '"=l"(__d)' in src or '"=l"(__d0)' in src
assert '"l"(__value)' in src or '"l"(__value0)' in src
assert "cvt." not in src
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="must have dtype"):
@T.prim_func
def floating_source_for_integer_type(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint16")
T.device_entry()
T.ptx.st.global_.u16(A.ptr_to([0]), T.float32(1))
def test_ptx_st_vec_f64_cuda_13_4_carrier_gap():
"""Keep CUDA 13.4's st.v2.f64 carrier gap (ptxas C7907) local to that source-operand shape."""
from tvm.backend.cuda.ptx.table import TABLE, dtype_combos, tokens_for
st_vec_tokens = tokens_for(TABLE["st_vec"], vec="v2", type="f64")
assert dtype_combos(TABLE["st_vec"], st_vec_tokens) == (
("float64", "uint64"),
("uint64", "uint64"),
("int64", "uint64"),
)
for name, tokens in (
("st", tokens_for(TABLE["st"], type="f64")),
("ld_vec", tokens_for(TABLE["ld_vec"], vec="v2", type="f64")),
("ldu_vec", tokens_for(TABLE["ldu_vec"], vec="v2", type="f64")),
):
combos = dtype_combos(TABLE[name], tokens)
assert any(combo[0] == "uint128" for combo in combos), name
assert any(combo[0] == "int128" for combo in combos), name
def test_ptx_vec256_cache_policy():
"""Pin the PTX 9.2 256-bit cache-policy arity and exact-width carriers."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, mods, operand_dtypes, tokens_for
ld256 = TABLE["ld_vec256"]
ld_tokens = tokens_for(ld256, space="global", cache="L2::cache_hint", vec="v8", type="b32")
ld_src = render_variant(ld256, ld_tokens)[2]
assert "uint64_t __cache_policy" in ld_src
assert "ld.global.L2::cache_hint.v8.b32 {%0, %1, %2, %3, %4, %5, %6, %7}, [%8], %9;" in ld_src
st256 = TABLE["st_vec256"]
st_tokens = tokens_for(st256, space="global", cache="L2::cache_hint", vec="v8", type="b32")
st_src = render_variant(st256, st_tokens)[2]
assert "uint64_t __cache_policy" in st_src
assert "st.global.L2::cache_hint.v8.b32 [%0], {%1, %2, %3, %4, %5, %6, %7, %8}, %9;" in st_src
assert operand_dtypes(st256.operands[1], mods(st256, st_tokens)) == (
"uint32",
"int32",
"float32",
)
@T.prim_func
def vec256_calls(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (8,), "uint32")
T.device_entry()
policy = T.local_scalar("uint64")
x0 = T.local_scalar("uint32")
x1 = T.local_scalar("uint32")
x2 = T.local_scalar("uint32")
x3 = T.local_scalar("uint32")
x4 = T.local_scalar("uint32")
x5 = T.local_scalar("uint32")
x6 = T.local_scalar("uint32")
x7 = T.local_scalar("uint32")
T.ptx.ld.global_.L2__cache_hint.v8.b32(
x0, x1, x2, x3, x4, x5, x6, x7, A.ptr_to([0]), policy
)
T.ptx.st.global_.L2__cache_hint.v8.b32(
A.ptr_to([0]), x0, x1, x2, x3, x4, x5, x6, x7, policy
)
reparsed = tvm.script.from_source(vec256_calls.script())
tvm.ir.assert_structural_equal(vec256_calls, reparsed)
@requires_nvcc
def test_ptx_st_bulk_size_carriers_and_st_async_byte_bridge():
"""st.bulk takes 32/64-bit sizes; st.async stages one private .b8 register."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, dtype_combos, tokens_for
bulk = TABLE["st_bulk"]
bulk_tokens = tokens_for(bulk, space="shared::cta")
assert dtype_combos(bulk, bulk_tokens) == (
("uint64",),
("int64",),
("uint32",),
("int32",),
)
assert "uint32_t __size" in render_variant(bulk, bulk_tokens, dtypes=("uint32",))[2]
release = TABLE["st_async_release"]
for ty, dtype in (
("b8", "uint8"),
("u8", "uint8"),
("s8", "int8"),
):
tokens = tokens_for(release, sem="release", scope="sys", space="global", type=ty)
source = render_variant(release, tokens, dtypes=(dtype,))[2]
assert ".reg .b8 raw_b;" in source
assert "cvt.u8.u16 raw_b, %1;" in source
assert f"st.async.release.sys.global.{ty} [%0], raw_b;" in source
asm_text = _ASM_RE.findall(source)[0]
assert _sole_instruction(asm_text) == f"st.async.release.sys.global.{ty} [%0], raw_b;"
# The C-boundary casts preserve all byte patterns; the bridge conversion
# consumes their low 8 bits in the exact register class named above.
for bits in (0x00, 0x7F, 0x80, 0xFF):
signed_value = bits if bits < 0x80 else bits - 0x100
assert (bits & 0xFF) == bits
assert (signed_value & 0xFF) == bits
@T.prim_func
def carrier_calls(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (8,), "uint8")
T.device_entry()
T.cta_id([1])
T.thread_id([1])
T.ptx.st_async.release.sys.global_.b8(A.ptr_to([0]), T.uint8(0x00))
T.ptx.st_async.release.sys.global_.b8(A.ptr_to([1]), T.uint8(0x7F))
T.ptx.st_async.release.sys.global_.b8(A.ptr_to([2]), T.uint8(0x80))
T.ptx.st_async.release.sys.global_.b8(A.ptr_to([3]), T.uint8(0xFF))
T.ptx.st_async.release.sys.global_.u8(A.ptr_to([4]), T.uint8(0xFF))
T.ptx.st_async.release.sys.global_.s8(A.ptr_to([5]), T.int8(-1))
reparsed = tvm.script.from_source(carrier_calls.script())
tvm.ir.assert_structural_equal(carrier_calls, reparsed)
_assert_ptxas_ok(_cuda_source(carrier_calls), arch="sm_100")
def test_ptx_u64_address_handle_is_reinterpreted():
"""A 64-bit address handle is accepted via an explicit, visible cast.
Some PTX address operands reach us as u64 handles rather than typed
pointers (``T.address_of(tensormap)`` is the motivating case). PTX binds
both to the same "l" register, so the conversion must exist in the IR
rather than being punned inside the helper.
"""
handle = tvm.tirx.Var("h", "uint64")
call = T.ptx.prefetch.tensormap(handle)
addr = call.args[0]
assert addr.op.name == "tirx.reinterpret", addr
assert addr.args[0].same_as(handle)
def test_ptx_integer_arithmetic_dispatch():
"""The 9.7.1 integer lines share five mnemonics with the floating-point ones.
Nothing in the call names the entry: `T.ptx.add` reaches three candidates
(integer, single/double, half) and the written type token is what resolves
them, exactly as ptxas resolves them. `.wide` goes further -- it is a
separate entry per mnemonic because its result is "twice as wide as a and
b", a dtype no token in the instruction names.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "int32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
si = T.local_scalar("int32")
sw = T.local_scalar("int64")
fv = T.local_scalar("float32")
cnt = T.local_scalar("uint32")
T.ptx.add.s32(si, A[0], A[1]) # integer line
T.ptx.add.rn.f32(fv, fv, fv) # floating-point line, same mnemonic
T.ptx.mul.lo.s32(si, si, A[2]) # .lo keeps the operand width
T.ptx.mul.wide.s32(sw, si, A[3]) # .wide doubles the destination
T.ptx.mad.wide.s32(sw, si, A[0], sw) # ... and mad's accumulator too
T.ptx.popc.b32(cnt, si) # .b32 source, .u32 destination
T.ptx.dp4a.s32.s32(si, si, si, si)
A[tx % 4] = si + T.int32(sw) + T.int32(cnt)
src = _cuda_source(kernel)
# Same mnemonic, different entries, each emitting its own ISA line.
assert "add.s32 %0, %1, %2;" in src
assert "add.rn.f32 %0, %1, %2;" in src
assert "mul.lo.s32 %0, %1, %2;" in src
# The derived destination is a 64-bit "l" register while the sources stay
# 32-bit "r" -- the whole reason `.wide` is its own entry.
assert '"=l"(__d) : "r"(__a), "r"(__b)' in src
assert "mul.wide.s32 %0, %1, %2;" in src
assert "mad.wide.s32 %0, %1, %2, %3;" in src
assert "popc.b32 %0, %1;" in src
assert "dp4a.s32.s32 %0, %1, %2, %3;" in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# A derived dtype is enforced at trace time like any other: `.wide.s32`
# writes 64 bits, so a 32-bit destination is rejected before codegen.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="must have dtype int64"):
@T.prim_func
def narrow_wide_dst(out: T.Buffer((1,), "int32")):
T.device_entry()
T.ptx.mul.wide.s32(out[0], T.int32(2), T.int32(3))
# `.sat` is a syntax line of its own, not a free qualifier: the check
# rejects it where the ISA does not spell it.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="hi.s32"):
@T.prim_func
def sat_on_lo(out: T.Buffer((1,), "int32")):
T.device_entry()
T.ptx.mad.lo.sat.s32(out[0], T.int32(2), T.int32(3), T.int32(4))
# The unavailable add.sat forms added in PTX 9.2 are distinct from the
# integer lines on which `.sat` has never been legal. Keep both reasons
# visible so an invalid spelling is not misreported as an architecture
# gate. In particular, sub.sat.u32 is not the counterpart of add.sat.u32.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="sm_120f-only"):
@T.prim_func
def add_sat_sm120(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.add.sat.u32(out[0], T.uint32(2), T.uint32(3))
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"not on the add\.u64"):
@T.prim_func
def add_sat_no_syntax_line(out: T.Buffer((1,), "uint64")):
T.device_entry()
T.ptx.add.sat.u64(out[0], T.uint64(2), T.uint64(3))
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"not on the sub\.u32"):
@T.prim_func
def sub_sat_no_syntax_line(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.sub.sat.u32(out[0], T.uint32(2), T.uint32(3))
def test_ptx_floating_point_dispatch():
"""ISA 9.7.3's lines, including three mnemonics shared with the integer group.
`mad`, `div` and `abs` each name an integer entry and a floating-point one;
as with `add` above, only the written tokens choose between them. The rest
of the section is here for its shapes: a `.pred` destination (testp), the
f64 approximations that exist only with a mandatory `.ftz` (9.7.3.14 and
9.7.3.17), and the lone approximation with no `.ftz` at all (tanh).
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
f = T.local_scalar("float32")
d = T.local_scalar("float64")
si = T.local_scalar("int32")
isnan = T.local_scalar("uint32")
T.ptx.mad.rn.f32(f, A[0], A[1], A[2]) # fp line
T.ptx.mad.lo.s32(si, si, si, si) # integer line, same mnemonic
T.ptx.div.approx.ftz.f32(f, f, A[3]) # fp divide
T.ptx.div.s32(si, si, si) # integer divide, same mnemonic
T.ptx.abs.ftz.f32(f, f) # fp abs
T.ptx.abs.s32(si, si) # integer abs, same mnemonic
T.ptx.copysign.f32(f, A[0], f)
T.ptx.sqrt.rn.f64(d, d)
T.ptx.rsqrt.approx.ftz.f64(d, d) # ISA 9.7.3.17
T.ptx.rcp.approx.ftz.f64(d, d) # ISA 9.7.3.14
T.ptx.sin.approx.f32(f, f)
T.ptx.cos.approx.ftz.f32(f, f)
T.ptx.lg2.approx.f32(f, f)
T.ptx.tanh.approx.f32(f, f)
T.ptx.testp.notanumber.f32(isnan, f)
A[tx % 4] = f + T.float32(d) + T.float32(si) + T.float32(isnan)
src = _cuda_source(kernel)
for text in (
"mad.rn.f32 %0, %1, %2, %3;",
"mad.lo.s32 %0, %1, %2, %3;",
"div.approx.ftz.f32 %0, %1, %2;",
"div.s32 %0, %1, %2;",
"abs.ftz.f32 %0, %1;",
"abs.s32 %0, %1;",
"copysign.f32 %0, %1, %2;",
"sqrt.rn.f64 %0, %1;",
"rsqrt.approx.ftz.f64 %0, %1;",
"rcp.approx.ftz.f64 %0, %1;",
"sin.approx.f32 %0, %1;",
"cos.approx.ftz.f32 %0, %1;",
"lg2.approx.f32 %0, %1;",
"tanh.approx.f32 %0, %1;",
):
assert text in src, text
# The predicate result crosses the C boundary as a uint32 materialized by
# selp, with the real .pred register living inside the asm block.
assert ".reg .pred pd0; testp.notanumber.f32 pd0, %1; selp.b32 %0, 1, 0, pd0;" in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# sqrt has no f64 approximation at any spelling, unlike rcp (9.7.3.14).
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="only on the .f32 line"):
@T.prim_func
def sqrt_approx_f64(out: T.Buffer((1,), "float64")):
T.device_entry()
T.ptx.sqrt.approx.f64(out[0], T.float64(2.0))
# ... and rcp's f64 approximation is unreachable without its mandatory .ftz.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="rcp.approx.ftz.f64"):
@T.prim_func
def rcp_approx_f64(out: T.Buffer((1,), "float64")):
T.device_entry()
T.ptx.rcp.approx.f64(out[0], T.float64(2.0))
# One of .approx/.full/.rnd is required: the bare mnemonic names no line.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="missing required modifier"):
@T.prim_func
def div_without_mode(out: T.Buffer((1,), "float32")):
T.device_entry()
T.ptx.div.f32(out[0], T.float32(1.0), T.float32(2.0))
def test_ptx_half_precision_dispatch():
"""ISA 9.7.4, whose every mnemonic is shared with a wider-precision entry.
`abs` is the extreme case: three entries answer to it (integer, f32/f64,
half), and only the type token says which. The section also has the two
qualifiers no same-precision line carries -- fma's `.relu` and `.oob` --
and ex2's split over whether `.ftz` is mandatory or unspellable.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
h = T.local_scalar("uint16") # .f16 / .bf16 carrier
p = T.local_scalar("uint32") # .f16x2 / .bf16x2 carrier
f = T.local_scalar("float32")
si = T.local_scalar("int32")
T.ptx.add.rn.sat.f16(h, h, h) # existing half entry, beside the new ones
T.ptx.fma.rn.relu.f16x2(p, p, p, p) # .relu: no same-precision line has it
T.ptx.fma.rn.oob.bf16x2(p, p, p, p) # .oob likewise
T.ptx.fma.rn.f32(f, f, f, f) # same mnemonic, single precision
T.ptx.abs.ftz.f16(h, h) # three-way family: half ...
T.ptx.abs.ftz.f32(f, f) # ... single ...
T.ptx.abs.s32(si, si) # ... and integer
T.ptx.neg.bf16x2(p, p)
T.ptx.tanh.approx.f16x2(p, p)
T.ptx.ex2.approx.f16(h, h)
T.ptx.ex2.approx.ftz.bf16(h, h) # .ftz mandatory on this line
A[tx % 4] = T.uint32(p) + T.uint32(h) + T.uint32(si) + T.uint32(f)
src = _cuda_source(kernel)
for text in (
"add.rn.sat.f16 %0, %1, %2;",
"fma.rn.relu.f16x2 %0, %1, %2, %3;",
"fma.rn.oob.bf16x2 %0, %1, %2, %3;",
"fma.rn.f32 %0, %1, %2, %3;",
"abs.ftz.f16 %0, %1;",
"abs.ftz.f32 %0, %1;",
"abs.s32 %0, %1;",
"neg.bf16x2 %0, %1;",
"tanh.approx.f16x2 %0, %1;",
"ex2.approx.f16 %0, %1;",
"ex2.approx.ftz.bf16 %0, %1;",
):
assert text in src, text
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# The `.oob` line spells neither .ftz nor .sat beside it.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="oob line spells no"):
@T.prim_func
def oob_with_ftz(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.fma.rn.oob.ftz.f16(out[0], T.uint16(0), T.uint16(0), T.uint16(0))
# .sat and .relu are two clampings on two different syntax lines.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="separate syntax lines"):
@T.prim_func
def sat_and_relu(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.fma.rn.sat.relu.f16(out[0], T.uint16(0), T.uint16(0), T.uint16(0))
# ex2's bf16 line without its mandatory .ftz names no syntax line.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="mandatorily"):
@T.prim_func
def ex2_bf16_no_ftz(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.ex2.approx.bf16(out[0], T.uint16(0))
# ... and the bf16 abs/neg lines spell no .ftz at all.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="takes no .ftz"):
@T.prim_func
def abs_bf16_ftz(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.abs.ftz.bf16(out[0], T.uint16(0))
def test_ptx_mixed_precision_dispatch():
"""ISA 9.7.5, which adds no instruction of its own.
Its three subsections are a fourth syntax line of add/sub/fma:
`op{.rnd}{.sat}.f32.atype`, where a second type token names a 16-bit source
converted to .f32 before the operation. That is the `srctype` slot, so
these forms are reached through the same entries as the same-precision
ones, and what selects a line is only whether that second token is written.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "float32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
f = T.local_scalar("float32")
h = T.local_scalar("uint16") # the .f16/.bf16 source carrier
T.ptx.fma.rn.f32.f16(f, h, h, A[0]) # both sources converted
T.ptx.add.f32.bf16(f, h, f) # `{.rnd}` omitted: the bare mixed line
T.ptx.sub.rn.sat.f32.f16(f, h, f)
T.ptx.add.rn.f32(f, f, f) # same entry, same-precision line
A[tx % 4] = f
src = _cuda_source(kernel)
for text in (
"fma.rn.f32.f16 %0, %1, %2, %3;",
"add.f32.bf16 %0, %1, %2;",
"sub.rn.sat.f32.f16 %0, %1, %2;",
"add.rn.f32 %0, %1, %2;",
):
assert text in src, text
# Two carriers in one helper: the converted source is 16-bit, the rest f32.
assert '"=f"(__d) : "h"(__a), "f"(__b)' in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# The mixed line spells no .ftz, unlike the .f32 line it shares an entry with.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="takes no .ftz"):
@T.prim_func
def mixed_with_ftz(out: T.Buffer((1,), "float32")):
T.device_entry()
T.ptx.add.rn.ftz.f32.f16(out[0], T.uint16(0), T.float32(0))
# A converted source exists only on the .f32 line -- there is no f64 form.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="only exists on the .f32"):
@T.prim_func
def mixed_f64(out: T.Buffer((1,), "float64")):
T.device_entry()
T.ptx.add.rn.f64.f16(out[0], T.uint16(0), T.float64(0))
# And `mul` is the one mnemonic of the four with no mixed line at all, so
# its entry declares no srctype slot and the token does not resolve.
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def mul_mixed(out: T.Buffer((1,), "float32")):
T.device_entry()
T.ptx.mul.rn.f32.f16(out[0], T.uint16(0), T.float32(0))
def test_ptx_comparison_selection_dispatch():
"""ISA 9.7.7, the section whose results and selectors are predicates.
Four mnemonics, eight entries: `setp` alone is four, because `{.BoolOp}`
adds an operand and `[|q]` adds a destination, and those are two
independent shape choices rather than optional tokens. Nothing in a call
names the entry -- the arity and the operand classes pick it, exactly as
ptxas picks the syntax line.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "int32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
p = T.local_scalar("uint32")
q = T.local_scalar("uint32")
d = T.local_scalar("uint32")
f = T.local_scalar("float32")
p_buffer = T.alloc_local((1,), "uint32")
T.ptx.setp.lt.s32(p_buffer[0], A[0], A[1]) # one destination
T.ptx.setp.lt.s32(p, q, A[0], A[1]) # ... two: `p|q`, chosen by arity
T.ptx.setp.lt.and_.s32(p, A[0], A[1], T.ptx.pred(q)) # ... plus a BoolOp
T.ptx.setp.gt.or_.s32(p, q, A[2], A[3], T.ptx.pred(d)) # ... and both
T.ptx.set.lt.u32.f32(d, f, f) # writes a value, not a predicate
T.ptx.selp.b32(d, d, p, T.ptx.pred(p_buffer[0])) # predicate selects
T.ptx.slct.ftz.b32.f32(d, d, p, f) # a sign selects
# slct treats d/a/b independently as bit-size values. This mixes all
# three 32-bit carrier classes while c remains exactly .s32.
T.ptx.slct.f32.s32(d, f, A[0], A[1])
A[tx % 4] = T.int32(p + q + d)
src = _cuda_source(kernel)
for text in (
"setp.lt.s32 pd0, %1, %2;",
"setp.lt.s32 pd0|pd1, %2, %3;",
"setp.lt.and.s32 pd0, %1, %2, ps0;",
"setp.gt.or.s32 pd0|pd1, %2, %3, ps0;",
"set.lt.u32.f32 %0, %1, %2;",
"selp.b32 %0, %1, %2, ps0;",
"slct.ftz.b32.f32 %0, %1, %2, %3;",
"slct.f32.s32 %0, %1, %2, %3;",
):
assert text in src, text
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# Each source type takes its own operator set, and ptxas agrees with every
# rejection below (probed at sm_90 before they were written into the check).
# lo/ls/hi/hs are unsigned-only alternates for lt/le/gt/ge.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="signed"):
@T.prim_func
def unsigned_op_on_signed(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.setp.lo.s32(out[0], T.int32(1), T.int32(2))
# The unordered comparisons and the NaN predicates are floating point only.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="floating-point comparison"):
@T.prim_func
def float_op_on_integer(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.setp.nan.u32(out[0], T.uint32(1), T.uint32(2))
# A bit-size type has no ordering, only equality.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="only with eq/ne"):
@T.prim_func
def ordered_on_bitsize(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.setp.lt.b32(out[0], T.uint32(1), T.uint32(2))
# .ftz applies only to .f32 comparisons ...
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="only to .f32 comparisons"):
@T.prim_func
def ftz_off_f32(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.setp.eq.ftz.s32(out[0], T.int32(1), T.int32(2))
# ... and on slct, only to the .f32 selector line.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="f32 selector line"):
@T.prim_func
def ftz_on_s32_selector(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.slct.ftz.b32.s32(out[0], T.uint32(1), T.uint32(2), T.int32(3))
# Only d/a/b receive slct's relaxed bit-size typing. The selector must
# still match the second instruction type exactly, even when another
# same-constraint integer carrier would fit through inline asm.
with pytest.raises(
(ValueError, tvm.error.DiagnosticError), match=r"operand 'c'.*int32.*uint32"
):
@T.prim_func
def relaxed_selector(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.slct.f32.s32(out[0], T.float32(1), T.int32(2), T.uint32(3))
def test_ptx_slct_relaxed_value_dtype_domain():
"""The ptxas-proven d/a/b carrier product is complete and native-first."""
from tvm.backend.cuda.ptx.table import (
PTX_TYPE_DTYPES,
TABLE,
dtype_combos,
mods,
operand_dtypes,
tokens_for,
variants,
)
expected = {
"b16": ("uint16", "int16", "float16", "bfloat16"),
"u16": ("uint16", "int16", "float16", "bfloat16"),
"s16": ("int16", "uint16", "float16", "bfloat16"),
"b32": ("uint32", "int32", "float32"),
"u32": ("uint32", "int32"),
"s32": ("int32", "uint32"),
"f32": ("float32", "uint32", "int32"),
"b64": ("uint64", "int64", "float64"),
"u64": ("uint64", "int64"),
"s64": ("int64", "uint64"),
"f64": ("float64", "uint64", "int64"),
}
entry = TABLE["slct"]
for dtype, value_dtypes in expected.items():
for ctype in ("s32", "f32"):
tokens = tokens_for(entry, dtype=dtype, ctype=ctype)
mod_map = mods(entry, tokens)
domains = tuple(operand_dtypes(slot, mod_map) for slot in entry.typed_operands)
assert domains == (value_dtypes, value_dtypes, value_dtypes, PTX_TYPE_DTYPES[ctype])
# The three value operands form an independent product. Across the legal
# (dtype, ctype, ftz) variants this is the exhaustively certified 996.
assert sum(len(dtype_combos(entry, tokens)) for tokens in variants(entry)) == 996
def test_ptx_half_comparison_dispatch():
"""ISA 9.7.8, the half twin of 9.7.7 -- same two mnemonics, different grids.
Two things separate it from the section it shares names with. `setp`'s
destination shape is decided by the type rather than chosen: the scalar
lines spell one predicate, the packed lines only `p|q`, and there the pair
is the two lanes rather than a result and its complement. And `set` pairs
its two type tokens on a grid, so which (dtype, stype) combinations exist
is a fact of the section, not a product of two slots.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
h = T.local_scalar("uint16") # f16 / bf16 carrier
x2 = T.local_scalar("uint32") # f16x2 / bf16x2 carrier
p = T.local_scalar("uint32")
q = T.local_scalar("uint32")
d32 = T.local_scalar("uint32")
T.ptx.setp.eq.f16(p, h, h) # scalar type: one destination
T.ptx.setp.lt.ftz.f16x2(p, q, x2, x2) # packed type: the pair, per lane
T.ptx.setp.eq.and_.bf16(p, h, h, T.ptx.pred(q))
T.ptx.setp.gt.or_.bf16x2(p, q, x2, x2, T.ptx.pred(d32))
T.ptx.set.lt.u32.f16(d32, h, h) # integer answer to a half compare
T.ptx.set.lt.u32.bf16(d32, h, h) # bf16 is floating, not a .b* bit type
T.ptx.set.nan.u32.bf16x2(d32, x2, x2) # nor is packed bf16x2
T.ptx.set.gt.f16.f32(h, T.float32(1.0), T.float32(2.0)) # ... and back
T.ptx.set.eq.f16x2.f16x2(x2, x2, x2) # packed both sides
A[tx % 4] = p + q + d32 + T.uint32(h) + x2
src = _cuda_source(kernel)
for text in (
"setp.eq.f16 pd0, %1, %2;",
"setp.lt.ftz.f16x2 pd0|pd1, %2, %3;",
"setp.eq.and.bf16 pd0, %1, %2, ps0;",
"setp.gt.or.bf16x2 pd0|pd1, %2, %3, ps0;",
"set.lt.u32.f16 %0, %1, %2;",
"set.lt.u32.bf16 %0, %1, %2;",
"set.nan.u32.bf16x2 %0, %1, %2;",
"set.gt.f16.f32 %0, %1, %2;",
"set.eq.f16x2.f16x2 %0, %1, %2;",
):
assert text in src, text
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# Every rejection below was probed against ptxas before being written into
# the check. The (dtype, stype) grid: a packed source needs a 32-bit answer.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="no half-precision syntax"):
@T.prim_func
def bad_pair(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.set.eq.u16.f16x2(out[0], T.uint32(0), T.uint32(0))
# `.ftz` follows the source precision: a bf16 source has none to flush.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="does not take it"):
@T.prim_func
def ftz_bf16_source(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.set.eq.ftz.u32.bf16(out[0], T.uint16(0), T.uint16(0))
# ... and no bf16 destination takes it either.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="destination takes no"):
@T.prim_func
def ftz_bf16_dest(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.set.eq.ftz.bf16.f32(out[0], T.float32(0), T.float32(0))
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="spells no .ftz"):
@T.prim_func
def setp_ftz_bf16(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.setp.eq.ftz.bf16(out[0], T.uint16(0), T.uint16(0))
# The unordered comparisons need a floating-point source.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="integer source"):
@T.prim_func
def unordered_on_integer(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.set.equ.f16.s32(out[0], T.int32(0), T.int32(0))
# The classification fix is narrow: a real .b16 source remains a bit-size
# value and therefore still permits equality comparisons only.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="bit-size source"):
@T.prim_func
def ordered_on_bitsize(out: T.Buffer((1,), "uint16")):
T.device_entry()
T.ptx.set.lt.f16.b16(out[0], T.uint16(0), T.uint16(0))
# 9.7.8 spells no unsigned alternates at all, so `lo` never resolves here.
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def unsigned_alternate(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.setp.lo.f16(out[0], T.uint16(0), T.uint16(0))
def test_ptx_logic_shift_dispatch():
"""ISA 9.7.9, including the three mnemonics that are Python keywords.
`and`, `or` and `not` cannot be attributes as they stand, so the surface
spells them `and_`/`or_`/`not_` and the escape is carried through both
directions -- typing, and the name the printer emits. The round-trip
assertion below is what proves the second half: a script that printed
`T.ptx.and(...)` would not re-parse at all.
The section also puts `.pred` in the *type* slot rather than on a single
operand, so a helper can hold three bridges around one instruction.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (4,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
d = T.local_scalar("uint32")
p = T.local_scalar("uint32")
q = T.local_scalar("uint32")
s = T.local_scalar("int32")
T.ptx.and_.b32(d, A[0], A[1])
T.ptx.or_.b32(d, d, A[2])
T.ptx.xor.b32(d, d, A[3])
T.ptx.and_.pred(p, T.ptx.pred(q), T.ptx.pred(d)) # .pred as the type
T.ptx.not_.pred(p, T.ptx.pred(p))
T.ptx.not_.b32(d, d)
T.ptx.cnot.b32(d, d)
T.ptx.lop3.b32(d, d, d, d, 0x80) # a & b & c, by look-up table
T.ptx.lop3.or_.b32(d, p, d, d, d, 0xFE, T.ptx.pred(q)) # d|p pair
T.ptx.lop3.and_.b32(p, d, d, d, 0x80, T.ptx.pred(q)) # fixed _|p sibling
T.ptx.shf.l.clamp.b32(d, d, d, T.uint32(4)) # funnel shift
T.ptx.shl.b32(d, d, T.uint32(2))
T.ptx.shr.s32(s, s, T.uint32(1)) # signed: fills with the sign bit
A[tx % 4] = d + p + q + T.uint32(s)
src = _cuda_source(kernel)
for text in (
"and.b32 %0, %1, %2;",
"or.b32 %0, %1, %2;",
"xor.b32 %0, %1, %2;",
"and.pred pd0, ps0, ps1;",
"not.pred pd0, ps0;",
"not.b32 %0, %1;",
"cnot.b32 %0, %1;",
"lop3.b32 %0, %1, %2, %3, 128;",
"lop3.or.b32 %0|pd0, %2, %3, %4, 254, ps0;",
"lop3.and.b32 _|pd0, %1, %2, %3, 128, ps0;",
"shf.l.clamp.b32 %0, %1, %2, %3;",
"shl.b32 %0, %1, %2;",
"shr.s32 %0, %1, %2;",
):
assert text in src, text
# The load-bearing one: `and`/`or`/`not` have to survive being printed.
script = kernel.script()
assert "T.ptx.and_(" in script and "T.ptx.not_(" in script
reparsed = tvm.script.from_source(script)
tvm.ir.assert_structural_equal(kernel, reparsed)
# cnot has no predicate line -- ptxas: "Unexpected instruction types
# specified for 'cnot'" -- so the token is not in its slot at all.
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def cnot_pred(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.cnot.pred(out[0], T.uint32(0))
# lop3's BoolOp line stops at .or/.and; ptxas rejects .xor outright.
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def lop3_xor(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.lop3.xor.b32(out[0], T.uint32(0), T.uint32(0), T.uint32(0), 0x80)
# shl is untyped -- a left shift zero-fills whatever the bits mean, so
# there is no signed line (unlike shr, which needs to know how to fill).
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def shl_signed(out: T.Buffer((1,), "int32")):
T.device_entry()
T.ptx.shl.s32(out[0], T.int32(1), T.uint32(2))
# Open immediates may survive tracing so explicitly-unrolled expressions
# can specialize, but a runtime LUT byte still has no register form and is
# rejected at CUDA codegen.
@T.prim_func
def lut_runtime(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
tx = T.thread_id([32])
if tx == 0:
d = T.local_scalar("uint32")
T.ptx.lop3.b32(d, A[0], A[0], A[0], A[0])
with pytest.raises((ValueError, tvm.error.InternalError), match="compile-time constants"):
_cuda_source(lut_runtime)
@T.prim_func
def lut_unrolled(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
tx = T.thread_id([32])
if tx == 0:
d = T.local_scalar("uint32")
for i in T.unroll(2):
T.ptx.lop3.b32(d, A[0], A[0], A[0], i * 128)
unrolled_src = _cuda_source(lut_unrolled)
assert "lop3.b32 %0, %1, %2, %3, 0;" in unrolled_src
assert "lop3.b32 %0, %1, %2, %3, 128;" in unrolled_src
@T.prim_func
def lut_boundaries(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.cta_id([1])
T.thread_id([1])
d = T.local_scalar("uint32")
T.ptx.lop3.b32(d, A[0], A[0], A[0], 0)
T.ptx.lop3.b32(d, A[0], A[0], A[0], 255)
boundary_src = _cuda_source(lut_boundaries)
assert "lop3.b32 %0, %1, %2, %3, 0;" in boundary_src
assert "lop3.b32 %0, %1, %2, %3, 255;" in boundary_src
with pytest.raises(
(ValueError, tvm.error.DiagnosticError), match=r"inclusive range 0\.\.255, got -1"
):
@T.prim_func
def lut_below_range(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
d = T.local_scalar("uint32")
T.ptx.lop3.b32(d, A[0], A[0], A[0], -1)
with pytest.raises(
(ValueError, tvm.error.DiagnosticError), match=r"inclusive range 0\.\.255, got 256"
):
@T.prim_func
def lut_above_range(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
d = T.local_scalar("uint32")
T.ptx.lop3.b32(d, A[0], A[0], A[0], 256)
@T.prim_func
def lut_unrolled_out_of_range(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.cta_id([1])
T.thread_id([1])
d = T.local_scalar("uint32")
for i in T.unroll(2):
T.ptx.lop3.b32(d, A[0], A[0], A[0], i * 256)
with pytest.raises(ValueError, match=r"inclusive range 0\.\.255, got 256"):
_cuda_source(lut_unrolled_out_of_range)
def test_ptx_data_movement_dispatch():
"""ISA 9.7.10's newly registered instructions, end to end.
The section's own difficulty is that its shapes vary more than its
qualifiers: `mov` shares a mnemonic with ten vector pack/unpack entries and
is told apart by arity alone, `shfl.sync` and `multimem` each split on
whether a bracketed part of the syntax line is written, and several
operands are immediates that live in the instruction text.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (8,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
smem = T.alloc_buffer((4,), "uint32", scope="shared")
d = T.local_scalar("uint32")
p = T.local_scalar("uint32")
v = T.local_scalar("uint32")
pol = T.local_scalar("uint64")
gen = T.local_scalar("uint64")
T.ptx.mov.b32(d, A[0]) # scalar mov: two operands
T.ptx.mov.pred(p, T.ptx.pred(d)) # ... and its predicate line
T.ptx.prmt.b32(d, d, d, A[1]) # generic byte permute
T.ptx.prmt.b32.f4e(d, d, d, A[2]) # ... and a mode-driven one
T.ptx.ldu.global_.b32(v, A.ptr_to([3]))
T.ptx.shfl_sync.idx.b32(d, d, T.uint32(0), T.uint32(31), T.uint32(0xFFFFFFFF))
T.ptx.shfl_sync.up.b32(d, p, d, T.uint32(1), T.uint32(0), T.uint32(0xFFFFFFFF))
T.ptx.isspacep.global_(p, A.ptr_to([4]))
T.ptx.cvta.to.global_.u64(gen, A.ptr_to([5]))
T.ptx.cvt_pack.sat.u16.s32(d, T.int32(1), T.int32(2))
T.ptx.cvt_pack.sat.u8.s32.b32(d, T.int32(1), T.int32(2), d)
T.ptx.createpolicy.fractional.L2__evict_last.b64(pol)
T.ptx.createpolicy.cvt.L2.b64(pol, pol)
T.ptx.applypriority.global_.L2__evict_normal(A.ptr_to([6]))
T.ptx.discard.global_.L2(A.ptr_to([7]))
T.ptx.prefetchu.L1(A.ptr_to([0]))
T.ptx.cp.async_.wait_group(255)
T.ptx.cp.async_.bulk.wait_group(255)
T.ptx.cp.async_.bulk.wait_group.read(8)
T.ptx.cp.async_.bulk.wait_group.read(-1)
T.ptx.multimem_ld_reduce.add.u32(v, A.ptr_to([0]))
T.ptx.multimem_red.relaxed.gpu.add.u32(A.ptr_to([0]), v)
smem[tx % 4] = d + p + v
A[tx % 8] = smem[tx % 4] + T.uint32(gen) + T.uint32(pol)
src = _cuda_source(kernel)
for text in (
"mov.b32 %0, %1;",
"mov.pred pd0, ps0;",
"prmt.b32 %0, %1, %2, %3;",
"prmt.b32.f4e %0, %1, %2, %3;",
"ldu.global.b32 %0, [%1];",
"shfl.sync.idx.b32 %0, %1, %2, %3, %4;",
"shfl.sync.up.b32 %0|pd0, %2, %3, %4, %5;",
"isspacep.global pd0, %1;",
"cvta.to.global.u64 %0, %1;",
"cvt.pack.sat.u16.s32 %0, %1, %2;",
"cvt.pack.sat.u8.s32.b32 %0, %1, %2, %3;",
"createpolicy.fractional.L2::evict_last.b64 %0;",
"createpolicy.cvt.L2.b64 %0, %1;",
"applypriority.global.L2::evict_normal [%0], 128;",
"discard.global.L2 [%0], 128;",
"prefetchu.L1 [%0];",
"cp.async.wait_group 255;",
"cp.async.bulk.wait_group 255;",
"cp.async.bulk.wait_group.read 8;",
"cp.async.bulk.wait_group.read -1;",
"multimem.ld_reduce.add.u32 %0, [%1];",
"multimem.red.relaxed.gpu.add.u32 [%0], %1;",
):
assert text in src, text
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# Every rejection below was probed against ptxas before it was written into
# a check. multimem pairs an ordering semantic with a scope ...
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="go together"):
@T.prim_func
def sem_without_scope(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
v = T.local_scalar("uint32")
T.ptx.multimem_ld_reduce.relaxed.add.u32(v, A.ptr_to([0]))
# ... and `.weak` is the line that has neither.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="takes no scope"):
@T.prim_func
def weak_with_scope(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
v = T.local_scalar("uint32")
T.ptx.multimem_ld_reduce.weak.gpu.add.u32(v, A.ptr_to([0]))
# The op x type table: `.add` is the row that takes .s32 but not .s64.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"\.add takes"):
@T.prim_func
def add_s64(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "int64")
T.device_entry()
v = T.local_scalar("int64")
T.ptx.multimem_ld_reduce.add.s64(v, A.ptr_to([0]))
# The scalar float line has no lone half: a width has to reach 32 bits.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="the scalar line takes"):
@T.prim_func
def scalar_f16(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint16")
T.device_entry()
v = T.local_scalar("uint16")
T.ptx.multimem_ld_reduce.add.f16(v, A.ptr_to([0]))
# `.acc::f32` raises an accumulation, so it needs something to accumulate.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="applies to .add"):
@T.prim_func
def acc_on_min(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
v = T.local_scalar("uint32")
T.ptx.multimem_ld_reduce.min.acc__f32.f16x2(v, A.ptr_to([0]))
# st.async's mmio line is system-scoped only.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="requires .sys"):
@T.prim_func
def mmio_gpu(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.ptx.st_async.mmio.release.gpu.global_.u32(A.ptr_to([0]), T.uint32(1))
def test_ptx_parallel_sync_dispatch():
"""ISA 9.7.15's warp-level primitives and the atom shapes beside `.op`.
The section is where predicates are most load-bearing: they are sources
(bar.red's `c`), destinations (vote, elect), and both at once. It is also
where one mnemonic carries the most shapes -- `atom` has the `.op` line,
`.cas`, `.exch`, the half-precision adds and three vector syntax lines
represented by two entries, all resolved by tokens and arity alone.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (8,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
d = T.local_scalar("uint32")
p = T.local_scalar("uint32")
m = T.local_scalar("uint32")
h = T.local_scalar("uint16")
FULL = T.uint32(0xFFFFFFFF)
T.ptx.vote_sync.all.pred(p, T.ptx.pred(d), FULL)
T.ptx.vote_sync.ballot.b32(m, T.ptx.pred(d), FULL)
T.ptx.match.any.sync.b32(m, m, FULL)
T.ptx.match.all.sync.b32(m, p, m, FULL) # the |p twin
T.ptx.activemask.b32(m)
T.ptx.elect_sync(d, p, FULL)
T.ptx.redux_sync.add.u32(d, d, FULL)
T.ptx.redux_sync.and_.b32(d, d, FULL)
T.ptx.atom.global_.add.u32(d, A.ptr_to([0]), T.uint32(1)) # the .op line
T.ptx.atom.global_.cas.b32(d, A.ptr_to([1]), d, d) # ... and .cas
T.ptx.atom.global_.exch.b32(d, A.ptr_to([2]), d) # ... and .exch
T.ptx.atom.global_.add.noftz.f16(h, A.ptr_to([3]), h)
T.ptx.bar.red.popc.u32(d, T.uint32(0), T.ptx.pred(p))
T.ptx.bar.red.and_.pred(p, T.uint32(0), T.ptx.pred(p))
A[tx % 8] = d + p + m + T.uint32(h)
src = _cuda_source(kernel)
for text in (
"vote.sync.all.pred pd0, ps0, %2;",
"vote.sync.ballot.b32 %0, ps0, %2;",
"match.any.sync.b32 %0, %1, %2;",
"match.all.sync.b32 %0|pd0, %2, %3;",
"activemask.b32 %0;",
"elect.sync %0|pd0, %2;",
"redux.sync.add.u32 %0, %1, %2;",
"redux.sync.and.b32 %0, %1, %2;",
"atom.global.add.u32 %0, [%1], %2;",
"atom.global.cas.b32 %0, [%1], %2, %3;",
"atom.global.exch.b32 %0, [%1], %2;",
"atom.global.add.noftz.f16 %0, [%1], %2;",
"bar.red.popc.u32 %0, %1, ps0;",
"bar.red.and.pred pd0, %1, ps0;",
):
assert text in src, text
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# Every rejection below was probed against ptxas first. redux pairs the
# arithmetic ops with a signed type and the bitwise ones with the untyped
# word -- they are two syntax lines, so the token does not even resolve.
with pytest.raises((AttributeError, tvm.error.DiagnosticError), match="not a valid modifier"):
@T.prim_func
def redux_add_b32(out: T.Buffer((1,), "uint32")):
T.device_entry()
T.ptx.redux_sync.add.b32(out[0], T.uint32(1), T.uint32(0xFFFFFFFF))
# atom's vector lines bound the width by the element: a packed pair stops
# at .v4, and only a lone half reaches .v8.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="already a 32-bit pair"):
@T.prim_func
def packed_v8(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
v = T.local_scalar("uint32")
T.ptx.atom.global_.add.noftz.v8.f16x2(
v, v, v, v, v, v, v, v, A.ptr_to([0]), v, v, v, v, v, v, v, v
)
# red.async's op groups: `.add` reaches 64 bits, the bitwise ops do not.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"\.and takes"):
@T.prim_func
def red_async_and_u64(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint64")
T.device_entry()
v = T.local_scalar("uint64")
T.ptx.red_async.relaxed.cluster.shared__cluster.mbarrier__complete_tx__bytes.and_.u64(
A.ptr_to([0]), v, A.ptr_to([0])
)
def test_ptx_lazy_subscript_operands_realize():
"""Raw buffer elements realize before PTX predicate operand validation."""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (1,), "uint32")
T.device_entry()
T.cta_id([1])
T.thread_id([32])
regs = T.alloc_buffer((2,), "uint32", scope="local")
full = T.uint32(0xFFFFFFFF)
T.ptx.elect_sync(regs[0], regs[1], full)
T.ptx.vote_sync.all.pred(regs[0], T.ptx.pred(regs[1]), full)
T.ptx.activemask.b32(regs[1], pred=regs[0])
A[0] = regs[0] + regs[1]
src = _cuda_source(kernel)
assert "elect.sync" in src
assert "vote.sync.all.pred" in src
assert "@p activemask.b32" in src
def test_ptx_parser_roundtrip():
"""script() output re-parses to a structurally equal PrimFunc."""
@T.prim_func
def kernel(a_ptr: T.handle, b_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
B = T.match_buffer(b_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
smem = T.alloc_buffer((4,), "uint32", scope="shared")
if tx == 0:
val = T.local_scalar("uint32")
smem_addr = T.local_scalar("uint64")
lo = T.local_scalar("float32")
hi = T.local_scalar("float32")
packed = T.local_scalar("uint64")
# Several `mov` entries share one mnemonic; the printed form names
# the family and reparsing re-dispatches on the operand shape.
T.ptx.mov.b64(packed, lo, hi)
T.ptx.mov.b64(lo, hi, packed)
T.ptx.ld.global_.acquire.gpu.b32(val, A.ptr_to([0]))
T.ptx.st.shared__cta.b32(smem.ptr_to([0]), val)
T.ptx.red.relaxed.gpu.global_.add.u32(B.ptr_to([0]), T.uint32(1), pred=val)
T.ptx.prefetch.global_.L2(A.ptr_to([16]))
T.ptx.cvta.to.shared.u64(smem_addr, smem.data)
T.cuda.cta_sync()
B[tx] = smem[tx % 4]
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_pred_operand_roundtrip():
"""A `.pred` operand survives print/parse, tag and all.
The printed text carries neither the `T.ptx.pred(...)` wrapper nor the
register class -- a predicate and an integer share the same uint32 carrier
-- so the marker has to name the position. Without that, reparsing would
re-dispatch on a bare uint32 and could pick a *different* entry of the same
family (which is exactly what the src-size / ignore-src pair does).
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
flag = T.local_scalar("uint32")
T.ptx["tcgen05.mma.cta_group::1.kind::f16"](
tmem, desc, desc, idesc, 0, 0, 0, 0, T.ptx.pred(flag)
)
A[tx] = A[tx]
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_wgmma_scale_d_runtime_predicate_roundtrip():
"""WGMMA scale-d is a runtime predicate, not a 0/1 text immediate."""
@T.prim_func
def kernel(out_ptr: T.handle):
Out = T.match_buffer(out_ptr, (128,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([128])
scale_d: T.uint32 = T.cast(tx < 128, "uint32")
d0: T.uint32 = T.uint32(0)
d1: T.uint32 = T.uint32(0)
d2: T.uint32 = T.uint32(0)
d3: T.uint32 = T.uint32(0)
T.ptx.wgmma.mma_async.sync.aligned.m64n8k32.s32.s8.s8(
d0, d1, d2, d3, T.uint64(0), T.uint64(0), T.ptx.pred(scale_d)
)
Out[tx] = d0 + d1 + d2 + d3
target = tvm.target.Target({"kind": "cuda", "arch": "sm_90a"})
with target:
mod = tvm.compile(tvm.IRModule({"main": kernel}), target=target, tir_pipeline="tirx")
src = mod.mod.imports[0].inspect_source("cuda")
assert "wgmma.mma_async.sync.aligned.m64n8k32.s32.s8.s8" in src
assert ".reg .pred ps0;" in src
assert "setp.ne.b32 ps0," in src
assert "}, %4, %5, ps0;" in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
# There is deliberately no compatibility overload for the old integer
# immediate spelling. A bool already identifies a predicate; a runtime
# integer must carry the explicit T.ptx.pred(...) register-class marker.
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"T\.ptx\.pred"):
@T.prim_func
def bare_integer_scale_d():
T.device_entry()
d0: T.uint32 = T.uint32(0)
d1: T.uint32 = T.uint32(0)
d2: T.uint32 = T.uint32(0)
d3: T.uint32 = T.uint32(0)
T.ptx.wgmma.mma_async.sync.aligned.m64n8k32.s32.s8.s8(
d0, d1, d2, d3, T.uint64(0), T.uint64(0), 0
)
def test_ptx_wgmma_integer_shape_domains_follow_concrete_syntax():
"""s8/u8 stop at N=224, while the b1 syntax includes N=240/256."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, lanes_of, mods, tokens_for
for form in ("ss", "rs"):
entry = TABLE[f"wgmma_int_{form}"]
shape_slot = next(slot for slot in entry.slots if slot.name == "shape")
assert "m64n224k32" in shape_slot.choices
assert "m64n240k32" not in shape_slot.choices
assert "m64n256k32" not in shape_slot.choices
assert entry.operands[-1].dtype == "pred"
tokens = tokens_for(
entry,
action="mma_async",
sync="sync",
aligned="aligned",
shape="m64n224k32",
dtype="s32",
atype="s8",
btype="s8",
)
assert lanes_of(entry.operands[0], mods(entry, tokens)) == 112
opcode, _, source = render_variant(entry, tokens)
assert opcode == "wgmma.mma_async.sync.aligned.m64n224k32.s32.s8.s8"
assert f"{opcode} {{" in source
assert ", ps0;" in source
b1_entry = TABLE[f"wgmma_b1_{form}"]
b1_shapes = next(slot for slot in b1_entry.slots if slot.name == "shape").choices
assert "m64n240k256" in b1_shapes
assert "m64n256k256" in b1_shapes
@requires_nvcc
def test_ptx_tcgen05_mma_block_size_form():
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
flag = T.local_scalar("uint32")
T.ptx["tcgen05.mma.cta_group::1.kind::mxf4.block_scale.block32"](
tmem, desc, desc, idesc, tmem, tmem, T.ptx.pred(flag)
)
A[tx] = A[tx]
src = _cuda_source(kernel)
assert "tcgen05.mma.cta_group::1.kind::mxf4.block_scale.block32" in src
_assert_ptxas_ok(src, arch="sm_100a")
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="mxf4.*block32"):
@T.prim_func
def invalid_mxf4_block16():
T.device_entry()
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
flag = T.local_scalar("uint32")
T.ptx["tcgen05.mma.cta_group::1.kind::mxf4.block_scale.block16"](
tmem, desc, desc, idesc, tmem, tmem, T.ptx.pred(flag)
)
@pytest.mark.skipif(
not env.has_nvcc_version(13, 4),
reason="collector-qualified block_scale MMA is a PTX 9.4 form; need nvcc >= 13.4",
)
@requires_nvcc
def test_ptx_tcgen05_mma_block_size_collector_form():
"""PTX 9.4 collector qualifiers on block-scaled MMA certify at their sm_107f floor."""
@T.prim_func
def sm107_collector_kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
flag = T.local_scalar("uint32")
T.ptx[
"tcgen05.mma.cta_group::1.kind::mxf4nvf4.block_scale.block16"
".collector::a::discard.collector::b::fill"
](tmem, desc, desc, idesc, tmem, tmem, T.ptx.pred(flag))
T.ptx[
"tcgen05.mma.cta_group::1.kind::mxf4.block_scale.block32"
".collector::a::fill.collector::b::lastuse"
](tmem, tmem, desc, idesc, tmem, tmem, T.ptx.pred(flag))
A[tx] = A[tx]
collector_src = _cuda_source(sm107_collector_kernel)
ss_collector_opcode = (
"tcgen05.mma.cta_group::1.kind::mxf4nvf4.block_scale.block16"
".collector::a::discard.collector::b::fill"
)
ts_collector_opcode = (
"tcgen05.mma.cta_group::1.kind::mxf4.block_scale.block32"
".collector::a::fill.collector::b::lastuse"
)
assert ss_collector_opcode in collector_src
assert ts_collector_opcode in collector_src
_assert_ptxas_ok(collector_src, arch="sm_107f")
@pytest.mark.skipif(
not env.has_nvcc_version(13, 4),
reason="collector-qualified block_scale MMA is a PTX 9.4 form; need nvcc >= 13.4",
)
@requires_nvcc
def test_ptx_tcgen05_mma_block_scale_collector_a_without_block_size():
"""SM107 activation-stationary FP8 accepts collector A without `.block*`."""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
flag = T.local_scalar("uint32")
T.ptx["tcgen05.mma.cta_group::1.kind::mxf8f6f4.block_scale.collector::a::discard"](
tmem, desc, desc, idesc, tmem, tmem, T.ptx.pred(flag)
)
A[tx] = A[tx]
src = _cuda_source(kernel)
opcode = "tcgen05.mma.cta_group::1.kind::mxf8f6f4.block_scale.collector::a::discard"
assert opcode in src
_assert_ptxas_ok(src, arch="sm_107a")
def test_ptx_tcgen05_mma_block_size_collector_legality():
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
kind_blocks = (
("kind::mxf8f6f4", "block32"),
("kind::mxf4", "block32"),
("kind::mxf4nvf4", "block16"),
("kind::mxf4nvf4", "block32"),
)
for form in ("ss", "ts"):
entry = TABLE[f"tcgen05_mma_block_scale_block_{form}"]
for kind, block_size in kind_blocks:
required = {
"action": "mma",
"cta_group": "cta_group::1",
"kind": kind,
"block_scale": "block_scale",
"block_size": block_size,
}
tokens_for(entry, **required, collector_a="collector::a::fill")
tokens_for(
entry,
**required,
collector_a="collector::a::fill",
collector_b="collector::b::lastuse",
)
with pytest.raises(ValueError, match="collector B requires collector A"):
tokens_for(
entry,
action="mma",
cta_group="cta_group::1",
kind="kind::mxf4",
block_scale="block_scale",
block_size="block32",
collector_b="collector::b::fill",
)
@requires_nvcc
def test_ptx_tcgen05_mma_block_size_no_b_certifies_at_sm100f():
"""No-collector and collector-A-only forms retain their documented lower floor."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
kind_blocks = (
("kind::mxf8f6f4", "block32"),
("kind::mxf4", "block32"),
("kind::mxf4nvf4", "block16"),
("kind::mxf4nvf4", "block32"),
)
sources = []
for form in ("ss", "ts"):
entry = TABLE[f"tcgen05_mma_block_scale_block_{form}"]
for kind, block_size in kind_blocks:
for collector_a in ("", "collector::a::fill"):
kwargs = {
"action": "mma",
"cta_group": "cta_group::1",
"kind": kind,
"block_scale": "block_scale",
"block_size": block_size,
}
if collector_a:
kwargs["collector_a"] = collector_a
tokens = tokens_for(entry, **kwargs)
_, helper, helper_source = render_variant(entry, tokens)
sources.append(_certification_kernel(helper, helper_source, len(sources)))
_assert_ptxas_ok("\n".join((_CERT_PRELUDE, *sources)), arch="sm_100f")
def test_ptx_pred_operand_rejects_untagged_integer():
"""An untagged integer at a `.pred` position is refused, by name.
The carrier is shared, so accepting it would erase the only thing that
tells the two `cp.async` optional-operand syntax lines apart. A bool needs
no tag -- it already says what it is.
"""
with pytest.raises((ValueError, tvm.error.DiagnosticError), match=r"T\.ptx\.pred"):
@T.prim_func
def untagged_integer():
T.device_entry()
T.cta_id([1])
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
flag = T.local_scalar("uint32")
T.ptx["tcgen05.mma.cta_group::1.kind::f16"](tmem, desc, desc, idesc, 0, 0, 0, 0, flag)
# A bool expression carries the class in its own dtype, so it needs no tag.
@T.prim_func
def bool_needs_no_tag(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
tmem = T.local_scalar("uint32")
desc = T.local_scalar("uint64")
idesc = T.local_scalar("uint32")
T.ptx["tcgen05.mma.cta_group::1.kind::f16"](
tmem, desc, desc, idesc, 0, 0, 0, 0, tx == 0
)
A[tx] = A[tx]
assert bool_needs_no_tag is not None
def test_ptx_sink_lane_codegen_and_roundtrip():
"""`T.ptx.SINK` renders the ISA's `_` and survives print/parse.
A sunk lane has no C parameter, so the printed call is *shorter* than the
one that was written -- the marker is the only thing that can say a lane
was there at all, and without it reparsing would land on a different
arity.
"""
@T.prim_func
def kernel(a_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
hi = T.local_scalar("uint32")
packed = T.local_scalar("uint64")
T.ptx.mov.b64(T.ptx.SINK, hi, packed)
A[tx] = A[tx]
src = _cuda_source(kernel)
assert "mov.b64 {_, %0}, %1;" in src
assert "_sink_d0" in src
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_sink_rejected_where_the_isa_has_no_underscore():
"""Which operands take `_` is read off each syntax line, never derived.
Not from the direction: `ld` sinks a destination it does not write and
`st` sinks a source it does not store, so "is it written" answers nothing.
Not from the family either: mov's *pack* shape has a scalar destination
and a vector source, and the ISA gives neither of them a sink.
"""
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="not sinkable here"):
@T.prim_func
def sink_a_source():
T.device_entry()
packed = T.local_scalar("uint64")
lo = T.local_scalar("uint32")
hi = T.local_scalar("uint32")
T.ptx.mov.b64(packed, lo, T.ptx.SINK)
T.evaluate(hi)
# ISA 9.7.10.4: "provided that at least one element is a scalar register".
with pytest.raises((ValueError, tvm.error.DiagnosticError), match="must be a real register"):
@T.prim_func
def sink_every_lane():
T.device_entry()
packed = T.local_scalar("uint64")
T.ptx.mov.b64(T.ptx.SINK, T.ptx.SINK, packed)
def test_ptx_printer_form():
@T.prim_func
def kernel(a_ptr: T.handle, b_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
B = T.match_buffer(b_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
if tx == 0:
T.ptx.ld.global_.acquire.gpu.b32(B[0], A.ptr_to([0]))
B[tx] = B[tx]
script = kernel.script()
assert "T.ptx.ld(" in script
assert '"acquire"' in script
# ---------------------------------------------------------------------------
# Registered-instruction unit tests: pin down the engine's generated helpers
# and its trace-time coercion so the behavior is readable here, not implicit.
# ---------------------------------------------------------------------------
def test_ptx_94_sm107_helpers_render():
"""Representative SM107 forms preserve PTX 9.4 modifier order and shape."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, escape_token, tokens_for, unescape_token
cases = (
(
"mul_mixed_vec_bf16_f16",
dict(dtype="bf16x2", atype="bf16x2", ctype="f16x2"),
"mul.bf16x2.bf16x2.f16x2",
),
(
"mul_mixed_vec_f16_bf16",
dict(dtype="f16x2", atype="f16x2", ctype="bf16x2"),
"mul.f16x2.f16x2.bf16x2",
),
("set_packed", dict(cmp="eq", type="u8x4"), "set.eq.u8x4"),
(
"ld_proxy_readonly",
dict(space="global", type="u32", proxy="proxy::readonly"),
"ld.global.u32.proxy::readonly",
),
(
"prefetch_valid_addr",
dict(space="global", level="L1::32B", valid_addr="valid_addr"),
"prefetch.global.L1::32B.valid_addr",
),
(
"spcompress",
dict(elemsize="b8", idxsize="b2", spfactor="sp::2:4", num="x4"),
"spcompress.b8.b2.sp::2:4.x4",
),
(
"tcgen05_mma_ti16_ss_collector_b",
dict(
action="mma",
cta_group="cta_group::1",
kind="kind::ti16",
collector_b="collector::b::fill",
),
"tcgen05.mma.cta_group::1.kind::ti16.collector::b::fill",
),
(
"tcgen05_mma_ss",
dict(
action="mma",
cta_group="cta_group::2",
kind="kind::f8f6f4",
collector_a="collector::a::discard",
),
"tcgen05.mma.cta_group::2.kind::f8f6f4.collector::a::discard",
),
(
"tcgen05_mma_lut_b_ts",
dict(
action="mma",
cta_group="cta_group::1",
kind="kind::f8f6f4",
decompress="decompress::lut::b",
collector_b="collector::b::use",
),
"tcgen05.mma.cta_group::1.kind::f8f6f4.decompress::lut::b.collector::b::use",
),
(
"cp_async_bulk_tensor_g2s_cta_override_address_im2col",
dict(
api="async",
kind="bulk",
unit="tensor",
dim="3d",
dst="shared::cta",
src="global",
load_mode="im2col",
completion="mbarrier::complete_tx::bytes",
override_address="override::global_address",
),
(
"cp.async.bulk.tensor.3d.shared::cta.global.im2col."
"mbarrier::complete_tx::bytes.override::global_address"
),
),
(
"cp_async_bulk_tensor_g2s_cta_override_address",
dict(
api="async",
kind="bulk",
unit="tensor",
dim="2d",
dst="shared::cta",
src="global",
load_mode="tile",
completion="mbarrier::complete_tx::bytes",
report="mbarrier::report::disabled",
override_address="override::global_address",
),
(
"cp.async.bulk.tensor.2d.shared::cta.global.tile."
"mbarrier::complete_tx::bytes.mbarrier::report::disabled."
"override::global_address"
),
),
(
"cp_async_bulk_tensor_s2g_im2col_no_offs_w",
dict(
api="async",
kind="bulk",
unit="tensor",
dim="3d",
dst="global",
src="shared::cta",
load_mode="im2col_no_offs",
completion="bulk_group",
),
"cp.async.bulk.tensor.3d.global.shared::cta.im2col_no_offs.bulk_group",
),
(
"atom_f32_noftz_bitbucket",
dict(space="global", op="add", noftz="noftz", type="f32"),
"atom.global.add.noftz.f32",
),
(
"ldmatrix_s8_s4",
dict(
sync="sync",
aligned="aligned",
shape="m8n16",
num="x1",
space="shared",
dtype="s8",
ctype="s4",
),
"ldmatrix.sync.aligned.m8n16.x1.shared.s8.s4",
),
)
for name, slots, expected in cases:
entry = TABLE[name]
opcode, _, source = render_variant(entry, tokens_for(entry, **slots))
assert opcode == expected
assert f"{expected} " in source
cluster_override_im2col = TABLE["cp_async_bulk_tensor_g2s_cluster_override_address_im2col"]
opcode, _, source = render_variant(
cluster_override_im2col,
tokens_for(
cluster_override_im2col,
api="async",
kind="bulk",
unit="tensor",
dim="3d",
dst="shared::cluster",
src="global",
load_mode="im2col",
completion="mbarrier::complete_tx::bytes",
multicast="multicast::cluster::16b",
override_address="override::global_address",
),
)
assert (
opcode == "cp.async.bulk.tensor.3d.shared::cluster.global.im2col."
"mbarrier::complete_tx::bytes.multicast::cluster::16b.override::global_address"
)
assert "uint16_t __cta_mask" in source
tma = TABLE["cp_async_bulk_tensor_g2s_cta_override_address_im2col"]
_, _, source = render_variant(
tma,
tokens_for(
tma,
api="async",
kind="bulk",
unit="tensor",
dim="3d",
dst="shared::cta",
src="global",
load_mode="im2col",
completion="mbarrier::complete_tx::bytes",
override_address="override::global_address",
),
)
assert "[%1, %2, {%3, %4, %5}], [%6], {%7};" in source
escaped = escape_token("sp::2:4")
assert escaped == "sp__2__colon__4"
assert unescape_token(escaped) == "sp::2:4"
def test_ptx_94_cp_bulk_semantic_renderings():
"""Report and mask-width siblings retain every non-tensor semantic line."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
cases = (
(
"cp_async_bulk_g2s_cta_report",
dict(
api="async",
kind="bulk",
sem="weak",
dst="shared::cta",
src="global",
completion="mbarrier::complete_tx::bytes",
report="mbarrier::report::disabled",
ignore_oob="ignore_oob",
),
"cp.async.bulk.weak.shared::cta.global.mbarrier::complete_tx::bytes."
"mbarrier::report::disabled.ignore_oob "
"[%0], [%1], %2, %3, %4, [%5];",
),
(
"cp_async_bulk_g2s_cta_report",
dict(
api="async",
kind="bulk",
sem="relaxed",
scope="sys",
dst="shared::cta",
src="global",
completion="mbarrier::complete_tx::bytes",
report="mbarrier::report::validity::per_16bytes::8",
cache="L2::cache_hint",
type="b128",
),
"cp.async.bulk.relaxed.sys.shared::cta.global."
"mbarrier::complete_tx::bytes."
"mbarrier::report::validity::per_16bytes::8.L2::cache_hint.b128 "
"[%0], [%1], %2, [%3], %4;",
),
(
"cp_async_bulk_g2s_cluster_multicast16",
dict(
api="async",
kind="bulk",
sem="weak",
dst="shared::cluster",
src="global",
completion="mbarrier::complete_tx::bytes",
multicast="multicast::cluster::16b",
),
"cp.async.bulk.weak.shared::cluster.global.mbarrier::complete_tx::bytes."
"multicast::cluster::16b [%0], [%1], %2, [%3], %4;",
),
(
"cp_async_bulk_g2s_cluster_multicast32",
dict(
api="async",
kind="bulk",
sem="relaxed",
scope="cluster",
dst="shared::cluster",
src="global",
completion="mbarrier::complete_tx::bytes",
multicast="multicast::cluster::32b",
type="b128",
),
"cp.async.bulk.relaxed.cluster.shared::cluster.global."
"mbarrier::complete_tx::bytes.multicast::cluster::32b.b128 "
"[%0], [%1], %2, [%3], %4;",
),
(
"cp_async_bulk_g2s_cluster_report",
dict(
api="async",
kind="bulk",
sem="relaxed",
scope="gpu",
dst="shared::cluster",
src="global",
completion="mbarrier::complete_tx::bytes",
report="mbarrier::report::validity::per_element::ff",
multicast="multicast::cluster::32b",
type="b128",
),
"cp.async.bulk.relaxed.gpu.shared::cluster.global."
"mbarrier::complete_tx::bytes."
"mbarrier::report::validity::per_element::ff."
"multicast::cluster::32b.b128 [%0], [%1], %2, [%3], %4;",
),
)
for name, modifiers, expected in cases:
entry = TABLE[name]
_, _, source = render_variant(entry, tokens_for(entry, **modifiers))
assert f'asm volatile("{expected}"' in source, source
def test_ptx_94_cp_bulk_semantics_roundtrip():
"""The string namespace dispatches every widened PTX 9.4 sibling."""
@T.prim_func
def kernel(src: T.Buffer((64,), "uint32")):
T.device_entry()
smem = T.alloc_buffer((64,), "uint32", scope="shared")
mbar = T.alloc_buffer((2,), "uint64", scope="shared")
T.ptx[
"cp.async.bulk.relaxed.sys.shared::cta.global."
"mbarrier::complete_tx::bytes."
"mbarrier::report::validity::per_16bytes::8.L2::cache_hint.b128"
](
smem.ptr_to([0]),
src.ptr_to([0]),
T.uint32(16),
mbar.ptr_to([0]),
T.uint64(0),
)
T.ptx[
"cp.async.bulk.weak.shared::cluster.global."
"mbarrier::complete_tx::bytes.multicast::cluster::16b"
](
T.uint32(0),
src.ptr_to([0]),
T.uint32(16),
mbar.ptr_to([0]),
T.uint16(1),
)
T.ptx[
"cp.async.bulk.relaxed.cluster.shared::cluster.global."
"mbarrier::complete_tx::bytes.multicast::cluster::32b.b128"
](
T.uint32(0),
src.ptr_to([0]),
T.uint32(16),
mbar.ptr_to([0]),
T.uint32(1),
)
T.ptx[
"cp.async.bulk.relaxed.gpu.shared::cluster.global."
"mbarrier::complete_tx::bytes."
"mbarrier::report::validity::per_element::ff."
"multicast::cluster::32b.b128"
](
T.uint32(0),
src.ptr_to([0]),
T.uint32(16),
mbar.ptr_to([0]),
T.uint32(1),
)
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_94_cp_bulk_semantic_negative_grids():
"""The widened siblings reject malformed strong semantics and report/OOB pairs."""
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
multicast = TABLE["cp_async_bulk_g2s_cluster_multicast32"]
required = dict(
api="async",
kind="bulk",
dst="shared::cluster",
src="global",
completion="mbarrier::complete_tx::bytes",
multicast="multicast::cluster::32b",
)
with pytest.raises(ValueError, match=r"requires both \.scope and \.b128"):
tokens_for(multicast, **required, sem="relaxed")
with pytest.raises(ValueError, match=r"belong only to the \.relaxed"):
tokens_for(multicast, **required, sem="weak", scope="cta", type="b128")
report = TABLE["cp_async_bulk_g2s_cta_report"]
with pytest.raises(ValueError, match=r"\.ignore_oob requires .*report::disabled"):
tokens_for(
report,
api="async",
kind="bulk",
dst="shared::cta",
src="global",
completion="mbarrier::complete_tx::bytes",
report="mbarrier::report::validity::per_element::ff",
ignore_oob="ignore_oob",
)
def test_ptx_94_sm107_arch_floors_and_delta():
"""Entries owning SM107 variants certify at 107f and remain in the 9.4 delta."""
from tvm.backend.cuda.ptx.table import _PTX_94_ENTRIES, TABLE
sm107_entries = {
"add_mixed_vec_up",
"sub_mixed_vec_up",
"add_mixed_vec_down_f16",
"sub_mixed_vec_down_f16",
"add_mixed_vec_down_bf16",
"sub_mixed_vec_down_bf16",
"fma_mixed_vec",
"mul_mixed_vec_down_f16",
"mul_mixed_vec_down_bf16",
"mul_mixed_vec_bf16_f16",
"mul_mixed_vec_f16_bf16",
"set_packed",
"tcgen05_mma_block_scale_block_ss",
"tcgen05_mma_block_scale_block_ts",
}
assert {TABLE[name].cert_arch for name in sm107_entries} == {"sm_107f"}
delta_names = {entry.name for entry in _PTX_94_ENTRIES}
noftz_siblings = {
"atom_f32_noftz",
"red_f32_noftz",
"atom_vec_f32_noftz",
"red_vec_f32_noftz",
"cp_reduce_async_bulk_s2g_f32_noftz",
"multimem_cp_reduce_async_bulk_f32_noftz",
}
assert {"ldmatrix_s8_s4", *noftz_siblings} <= delta_names
# `.noftz` with `.f32` is a PTX ISA 9.4 line that requires sm_90, not a
# family-specific one; the siblings certify at that floor.
assert {TABLE[name].cert_arch for name in noftz_siblings} == {"sm_90"}
def test_ptx_tcgen05_mapa_address_rendering():
"""Optional state spaces select generic versus shared address carriers."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for, variants
def render(name, **by_name):
entry = TABLE[name]
return render_variant(entry, tokens_for(entry, **by_name))[2]
bare_alloc = render(
"tcgen05_alloc",
action="alloc",
cta_group="cta_group::1",
sync="sync",
aligned="aligned",
type="b32",
)
shared_alloc = render(
"tcgen05_alloc",
action="alloc",
cta_group="cta_group::1",
sync="sync",
aligned="aligned",
space="shared::cta",
type="b32",
)
assert "(const void* __dst, uint32_t __ncols)" in bare_alloc
assert '"l"(__dst), "r"(__ncols)' in bare_alloc
assert "tcgen05.alloc.cta_group::1.sync.aligned.b32 [%0], %1;" in bare_alloc
assert "(uint32_t __dst, uint32_t __ncols)" in shared_alloc
assert '"r"(__dst), "r"(__ncols)' in shared_alloc
assert "tcgen05.alloc.cta_group::1.sync.aligned.shared::cta.b32 [%0], %1;" in shared_alloc
for name, extra in (
("tcgen05_commit", {}),
("tcgen05_commit_multicast", {"multicast": "multicast::cluster"}),
):
required = {
"action": "commit",
"cta_group": "cta_group::2",
"completion": "mbarrier::arrive::one",
"type": "b64",
**extra,
}
bare = render(name, **required)
shared = render(name, space="shared::cluster", **required)
assert "(const void* __mbar" in bare
assert '"l"(__mbar)' in bare
assert "(uint32_t __mbar" in shared
assert '"r"(__mbar)' in shared
generic_mapa = render("mapa", type="u64")
shared_mapa_ptr = render("mapa", space="shared::cluster", type="u64")
raw_mapa = render("mapa_u64_raw", type="u64")
shared_mapa_u64 = render("mapa_u64_shared", space="shared::cluster", type="u64")
shared_mapa_u32 = render("mapa_u32", space="shared::cluster", type="u32")
assert "(uint64_t& __d, const void* __a, uint32_t __b)" in generic_mapa
assert '"=l"(__d) : "l"(__a), "r"(__b)' in generic_mapa
assert "mapa.u64 %0, %1, %2;" in generic_mapa
assert "(uint64_t& __d, const void* __a, uint32_t __b)" in shared_mapa_ptr
assert "mapa.shared::cluster.u64 %0, %1, %2;" in shared_mapa_ptr
assert "(uint64_t& __d, uint64_t __a, uint32_t __b)" in raw_mapa
assert '"=l"(__d) : "l"(__a), "r"(__b)' in raw_mapa
assert "mapa.u64 %0, %1, %2;" in raw_mapa
assert "(uint64_t& __d, uint64_t __a, uint32_t __b)" in shared_mapa_u64
assert '"=l"(__d) : "l"(__a), "r"(__b)' in shared_mapa_u64
assert "mapa.shared::cluster.u64 %0, %1, %2;" in shared_mapa_u64
assert "(uint32_t& __d, uint32_t __a, uint32_t __b)" in shared_mapa_u32
# Two entries share bare mapa.u64's ISA spelling but accept disjoint
# pointer/register call shapes; explicit shared has both carrier shapes.
assert (
sum(
len(variants(TABLE[name]))
for name in ("mapa", "mapa_u64_raw", "mapa_u64_shared", "mapa_u32")
)
== 5
)
def test_ptx_tcgen05_mapa_address_coercion():
"""Bare forms preserve generic pointers; explicit shared forms coerce only addresses."""
from tvm.ir.type import PointerType, PrimType
generic_ptr = tvm.tirx.Var("g", PointerType(PrimType("uint64"), "global"))
shared_ptr = tvm.tirx.Var("s", PointerType(PrimType("uint64"), "shared"))
raw_u32 = tvm.tirx.Var("a32", "uint32")
raw_u64 = tvm.tirx.Var("a64", "uint64")
ncols = tvm.tirx.Var("n", "uint32")
mask = tvm.tirx.Var("mask", "uint16")
out32 = tvm.tirx.decl_buffer((1,), "uint32", name="out32", scope="local")
out64 = tvm.tirx.decl_buffer((1,), "uint64", name="out64", scope="local")
bare_alloc = T.ptx.tcgen05.alloc.cta_group__1.sync.aligned.b32(generic_ptr, ncols)
assert bare_alloc.args[0].same_as(generic_ptr)
shared_alloc = T.ptx.tcgen05.alloc.cta_group__1.sync.aligned.shared__cta.b32(shared_ptr, ncols)
assert shared_alloc.args[0].op.name == "tirx.cuda.cvta_generic_to_shared"
assert shared_alloc.args[0].args[0].same_as(shared_ptr)
bare_commit = T.ptx.tcgen05.commit.cta_group__1.mbarrier__arrive__one.b64(generic_ptr)
assert bare_commit.args[0].same_as(generic_ptr)
shared_commit = T.ptx[
"tcgen05.commit.cta_group::2.mbarrier::arrive::one.shared::cluster.multicast::cluster.b64"
](raw_u32, mask)
assert shared_commit.args[0].same_as(raw_u32)
generic_mapa = T.ptx.mapa.u64(out64[0], generic_ptr, ncols)
assert generic_mapa.args[1].same_as(generic_ptr)
raw_mapa = T.ptx.mapa.u64(out64[0], raw_u64, ncols)
assert raw_mapa.args[1].same_as(raw_u64)
shared_mapa_u64 = T.ptx.mapa.shared__cluster.u64(out64[0], raw_u64, ncols)
assert shared_mapa_u64.args[1].same_as(raw_u64)
shared_mapa_u32 = T.ptx.mapa.shared__cluster.u32(out32[0], raw_u32, ncols)
assert shared_mapa_u32.args[1].same_as(raw_u32)
shared_mapa_ptr = T.ptx.mapa.shared__cluster.u64(out64[0], shared_ptr, ncols)
assert shared_mapa_ptr.args[1].same_as(shared_ptr)
def test_ptx_tcgen05_mapa_address_roundtrip():
"""The generic/shared split remains exact through TVMScript print and parse."""
@T.prim_func
def kernel(ptr: T.handle):
generic = T.match_buffer(ptr, (4,), "uint64")
T.device_entry()
mapped32 = T.local_scalar("uint32")
mapped64 = T.local_scalar("uint64")
window64 = T.local_scalar("uint64")
T.ptx.tcgen05.alloc.cta_group__1.sync.aligned.b32(generic.ptr_to([0]), T.uint32(32))
T.ptx.tcgen05.alloc.cta_group__1.sync.aligned.shared__cta.b32(
generic.ptr_to([0]), T.uint32(32)
)
T.ptx.tcgen05.commit.cta_group__1.mbarrier__arrive__one.b64(generic.ptr_to([0]))
T.ptx.tcgen05.commit.cta_group__1.mbarrier__arrive__one.shared__cluster.b64(
generic.ptr_to([0])
)
T.ptx.tcgen05.commit.cta_group__2.mbarrier__arrive__one.multicast__cluster.b64(
generic.ptr_to([0]), T.uint16(3)
)
T.ptx[
"tcgen05.commit.cta_group::2.mbarrier::arrive::one"
".shared::cluster.multicast::cluster.b64"
](generic.ptr_to([0]), T.uint16(3))
T.ptx.mapa.u64(mapped64, generic.ptr_to([0]), T.uint32(1))
T.ptx.cvta.to.shared__cluster.u64(window64, generic.ptr_to([0]))
T.ptx.mapa.u64(mapped64, window64, T.uint32(1))
T.ptx.mapa.shared__cluster.u64(mapped64, window64, T.uint32(1))
T.ptx.mapa.shared__cluster.u32(mapped32, T.uint32(0), T.uint32(1))
reparsed = tvm.script.from_source(kernel.script())
tvm.ir.assert_structural_equal(kernel, reparsed)
def test_ptx_helper_source_golden():
"""Exact generated helper source, one per family (executable documentation)."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
def render(name, predicated=False, dtypes=None, imms=None, **by_name):
entry = TABLE[name]
return render_variant(entry, tokens_for(entry, **by_name), predicated, dtypes, imms)[2]
# tokens_for is what lets the goldens below name their modifiers. Positional
# tuples silently shift when a slot is inserted, which is the one edit this
# table invites; naming makes that a loud error instead.
assert tokens_for(TABLE["ld"], space="global", type="b32") == (
("", "", "", "global", "", "", "", "", "", "b32")
)
with pytest.raises(ValueError, match="no modifier slot named"):
tokens_for(TABLE["ld"], storage="global", type="b32")
with pytest.raises(ValueError, match="not in"):
tokens_for(TABLE["ld"], space="tmem", type="b32")
with pytest.raises(ValueError, match="is required"):
tokens_for(TABLE["ld"], space="global")
assert render("prefetch", space="global", level="L2") == (
"__forceinline__ __device__ void tvm_builtin_ptx_prefetch_global_L2"
"(const void* __addr) {\n"
' asm volatile("prefetch.global.L2 [%0];" : : "l"(__addr) : "memory");\n'
"}\n"
)
# A destination is an ordinary operand taken by reference, so the C
# parameter list is the PTX operand list in order and the helper is void.
assert render("ld", sem="acquire", scope="gpu", space="global", type="b32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_ld_acquire_gpu_global_b32"
"(uint32_t& __d, const void* __addr) {\n"
' asm volatile("ld.acquire.gpu.global.b32 %0, [%1];" : "=r"(__d) : "l"(__addr)'
' : "memory");\n'
"}\n"
)
assert render("ld", dtypes=("int64", "uint64"), space="global", type="s32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_ld_global_s32_s64"
"(int64_t& __d, const void* __addr) {\n"
' asm volatile("ld.global.s32 %0, [%1];" : "=l"(__d) : "l"(__addr) : "memory");\n'
"}\n"
)
# 8-bit destination: no asm constraint of its own, so it rides a 16-bit
# carrier register and is narrowed into the reference afterwards. The asm
# block still holds exactly one instruction.
assert render("ld", space="global", type="b8") == (
"__forceinline__ __device__ void tvm_builtin_ptx_ld_global_b8"
"(uint8_t& __d, const void* __addr) {\n"
" uint16_t __d_reg;\n"
' asm volatile("ld.global.b8 %0, [%1];" : "=h"(__d_reg) : "l"(__addr) : "memory");\n'
" __d = (uint8_t)__d_reg;\n"
"}\n"
)
# Shared-space addr slot: helper takes uint32_t (post-coercion form), not void*.
assert render("st", space="shared::cta", type="b32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_st_shared__cta_b32"
"(uint32_t __addr, uint32_t __value) {\n"
' asm volatile("st.shared::cta.b32 [%0], %1;" : : "r"(__addr), "r"(__value)'
' : "memory");\n'
"}\n"
)
# Register-only op: plain asm (asm_volatile=False), no memory clobber.
assert render("cvta", dir="to", space="shared", type="u64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_cvta_to_shared_u64"
"(uint64_t& __d, const void* __ptr) {\n"
' asm("cvta.to.shared.u64 %0, %1;" : "=l"(__d) : "l"(__ptr));\n'
"}\n"
)
# Predicated twin (framework-level @p): extra uint32 pred param,
# setp + @p guard wrapping the same instruction.
assert render(
"red", predicated=True, sem="relaxed", scope="gpu", space="global", op="add", type="u32"
) == (
"__forceinline__ __device__ void tvm_builtin_ptx_red_relaxed_gpu_global_add_u32_pred"
"(const void* __addr, uint32_t __value, uint32_t __pred) {\n"
' asm volatile("{ .reg .pred p; setp.ne.b32 p, %2, 0; '
'@p red.relaxed.gpu.global.add.u32 [%0], %1; }"'
' : : "l"(__addr), "r"(__value), "r"(__pred) : "memory");\n'
"}\n"
)
# Mixed-space operands: per-operand space/dtype pick each carrier —
# shared addrs are uint32, the global addr is a pointer.
assert render(
"cp_async_bulk_g2s_cta",
api="async",
kind="bulk",
dst="shared::cta",
src="global",
completion="mbarrier::complete_tx::bytes",
) == (
"__forceinline__ __device__ void tvm_builtin_ptx_cp_async_bulk_g2s_cta_async_bulk"
"_shared__cta_global_mbarrier__complete_tx__bytes"
"(uint32_t __dst_mem, const void* __src_mem, uint32_t __size, uint32_t __mbar) {\n"
' asm volatile("cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes '
'[%0], [%1], %2, [%3];" : : "r"(__dst_mem), "l"(__src_mem), "r"(__size), "r"(__mbar)'
' : "memory");\n'
"}\n"
)
# Integer arithmetic (ISA 9.7.1). The plain shape first: an integer line
# sharing the `add` mnemonic with the floating-point entry, resolved apart
# by its type token alone.
assert render("add_int", sat="sat", type="s32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_add_int_sat_s32"
"(int32_t& __d, int32_t __a, int32_t __b) {\n"
' asm volatile("add.sat.s32 %0, %1, %2;" : "=r"(__d) : "r"(__a), "r"(__b));\n'
"}\n"
)
# A dtype the instruction text does not name: `.wide`'s destination is
# "twice as wide as a and b", so one written token (.s16) types the sources
# while the result is the derived .s32 -- 16-bit "h" inputs, a 32-bit "r"
# output. mad.wide derives c the same way, which is why its accumulator is
# the wide type and its multiplicands are not.
assert render("mul_wide", mode="wide", type="s16") == (
"__forceinline__ __device__ void tvm_builtin_ptx_mul_wide_wide_s16"
"(int32_t& __d, int16_t __a, int16_t __b) {\n"
' asm volatile("mul.wide.s16 %0, %1, %2;" : "=r"(__d) : "h"(__a), "h"(__b));\n'
"}\n"
)
assert render("mad_wide", mode="wide", type="u32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_mad_wide_wide_u32"
"(uint64_t& __d, uint32_t __a, uint32_t __b, uint64_t __c) {\n"
' asm volatile("mad.wide.u32 %0, %1, %2, %3;" : "=l"(__d) : "r"(__a), '
'"r"(__b), "l"(__c));\n'
"}\n"
)
# A destination the ISA types outright: popc counts a 64-bit source into a
# .u32 result, so the two operands have unrelated widths.
assert render("popc", type="b64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_popc_b64"
"(uint32_t& __d, uint64_t __a) {\n"
' asm volatile("popc.b64 %0, %1;" : "=r"(__d) : "l"(__a));\n'
"}\n"
)
# Two written type tokens, and an accumulator derived from both: c and d
# are .u32 only when atype and btype both are, so this mixed pair makes
# them .s32 (ISA 9.7.1.24).
assert render("dp4a", atype="u32", btype="s32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_dp4a_u32_s32"
"(int32_t& __d, uint32_t __a, int32_t __b, int32_t __c) {\n"
' asm volatile("dp4a.u32.s32 %0, %1, %2, %3;" : "=r"(__d) : "r"(__a), '
'"r"(__b), "r"(__c));\n'
"}\n"
)
# Five operands under the ISA's own names: the destination is `f`, and `d`
# is the field-length input -- the one family where `d` is not the result.
assert render("bfi", type="b64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_bfi_b64"
"(uint64_t& __f, uint64_t __a, uint64_t __b, uint32_t __c, uint32_t __d) {\n"
' asm volatile("bfi.b64 %0, %1, %2, %3, %4;" : "=l"(__f) : "l"(__a), "l"(__b), '
'"r"(__c), "r"(__d));\n'
"}\n"
)
# bmsk's destination is written .b32 but declared u32: ptxas takes no float
# register anywhere in this instruction, unlike every other .bN family (see
# the entry's note). So there is no f32 twin of this helper to render.
assert render("bmsk", mode="clamp", type="b32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_bmsk_clamp_b32"
"(uint32_t& __d, uint32_t __a, uint32_t __b) {\n"
' asm volatile("bmsk.clamp.b32 %0, %1, %2;" : "=r"(__d) : "r"(__a), "r"(__b));\n'
"}\n"
)
# Floating point (ISA 9.7.3). A `.pred` destination: the instruction writes
# a predicate register, which no asm constraint can bind, so the helper
# declares one inside the block and materializes 0/1 through selp on the
# way out. Still exactly one instruction -- the rest is the boundary.
assert render("testp", op="notanumber", type="f32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_testp_notanumber_f32"
"(uint32_t& __p, float __a) {\n"
' asm volatile("{ .reg .pred pd0; testp.notanumber.f32 pd0, %1; '
'selp.b32 %0, 1, 0, pd0; }" : "=r"(__p) : "f"(__a));\n'
"}\n"
)
# `.full` is a divide mode beside the rounding modes, not a qualifier on
# top of one -- the ISA requires exactly one of .approx/.full/.rnd, so the
# entry fuses them into a single mandatory slot.
assert render("div_f", mode="approx", ftz="ftz", type="f32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_div_f_approx_ftz_f32"
"(float& __d, float __a, float __b) {\n"
' asm volatile("div.approx.ftz.f32 %0, %1, %2;" : "=f"(__d) : "f"(__a), "f"(__b));\n'
"}\n"
)
# mad shares fma's grid and `_check_farith`, and shares the `mad` mnemonic
# with the two integer entries; `.rn` is what tells them apart.
assert render("mad_f", rnd="rn", sat="sat", type="f32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_mad_f_rn_sat_f32"
"(float& __d, float __a, float __b, float __c) {\n"
' asm volatile("mad.rn.sat.f32 %0, %1, %2, %3;" : "=f"(__d) : "f"(__a), '
'"f"(__b), "f"(__c));\n'
"}\n"
)
# ISA 9.7.3.14 is its own subsection because it is a different computation,
# but syntactically it is one cell of rcp's grid -- reachable only with the
# mandatory .ftz its syntax line spells (see `_check_rcp`).
assert render("rcp", mode="approx", ftz="ftz", type="f64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_rcp_approx_ftz_f64"
"(double& __d, double __value) {\n"
' asm volatile("rcp.approx.ftz.f64 %0, %1;" : "=d"(__d) : "d"(__value));\n'
"}\n"
)
# Half precision (ISA 9.7.4). fma's two extra clampings, on the packed
# type: `.relu` and `.oob` exist on no same-precision line, which is what
# keeps the half fma out of the f32/f64 entry. A packed pair rides one
# 32-bit register, so every operand is uint32.
assert render("fma_half", rnd="rn", oob="oob", relu="relu", type="f16x2") == (
"__forceinline__ __device__ void tvm_builtin_ptx_fma_half_rn_oob_relu_f16x2"
"(uint32_t& __d, uint32_t __a, uint32_t __b, uint32_t __c) {\n"
' asm volatile("fma.rn.oob.relu.f16x2 %0, %1, %2, %3;" : "=r"(__d) : "r"(__a), '
'"r"(__b), "r"(__c));\n'
"}\n"
)
# ex2's bf16 line spells `.ftz` mandatorily while its f16 line does not
# offer it -- the reason these are a separate entry from the .f32 ex2.
assert render("ex2_half", mode="approx", ftz="ftz", type="bf16x2") == (
"__forceinline__ __device__ void tvm_builtin_ptx_ex2_half_approx_ftz_bf16x2"
"(uint32_t& __d, uint32_t __value) {\n"
' asm volatile("ex2.approx.ftz.bf16x2 %0, %1;" : "=r"(__d) : "r"(__value));\n'
"}\n"
)
# The scalar half type is the other carrier: a 16-bit register on "h".
assert render("abs_half", ftz="ftz", type="f16") == (
"__forceinline__ __device__ void tvm_builtin_ptx_abs_half_ftz_f16"
"(uint16_t& __d, uint16_t __a) {\n"
' asm volatile("abs.ftz.f16 %0, %1;" : "=h"(__d) : "h"(__a));\n'
"}\n"
)
# Mixed precision (ISA 9.7.5) is not a separate instruction but a fourth
# syntax line of add/sub/fma: a second type token names a 16-bit source
# that is converted to .f32 before the operation. So one helper carries two
# carriers at once -- "h" for the converted sources, "f" for the rest.
# fma converts both a and b (`.abtype`); add/sub convert only a.
assert render("fma", rnd="rn", sat="sat", type="f32", srctype="bf16") == (
"__forceinline__ __device__ void tvm_builtin_ptx_fma_rn_sat_f32_bf16"
"(float& __d, uint16_t __a, uint16_t __b, float __c) {\n"
' asm volatile("fma.rn.sat.f32.bf16 %0, %1, %2, %3;" : "=f"(__d) : "h"(__a), '
'"h"(__b), "f"(__c));\n'
"}\n"
)
# Comparison and selection (ISA 9.7.7). setp's `p|q`: ONE operand position
# holding two predicate destinations, joined by the ISA's own separator
# rather than a comma. Each half is a real register with its own selp on
# the way out -- q carries the Boolean applied to the complement of the
# compare, so it is a second result, not a restatement of p.
assert render("setp_pq", cmp="lt", type="s32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_setp_pq_lt_s32"
"(uint32_t& __p, uint32_t& __q, int32_t __a, int32_t __b) {\n"
' asm volatile("{ .reg .pred pd0; .reg .pred pd1; setp.lt.s32 pd0|pd1, %2, %3; '
'selp.b32 %0, 1, 0, pd0; selp.b32 %1, 1, 0, pd1; }" : "=r"(__p), "=r"(__q) '
': "r"(__a), "r"(__b));\n'
"}\n"
)
# A predicate *source* rides the same carrier in the other direction: setp
# in, then the instruction. selp is the plainest case of it.
assert render("selp", type="f64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_selp_f64"
"(double& __d, double __a, double __b, uint32_t __c) {\n"
' asm volatile("{ .reg .pred ps0; setp.ne.b32 ps0, %3, 0; '
'selp.f64 %0, %1, %2, ps0; }" : "=d"(__d) : "d"(__a), "d"(__b), "r"(__c));\n'
"}\n"
)
# `set` writes a value rather than a predicate, so its two type tokens are
# independent: an f32 comparison landing 0xffffffff in a u32 destination.
assert render("set", cmp="lt", dtype="u32", stype="f32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_set_lt_u32_f32"
"(uint32_t& __d, float __a, float __b) {\n"
' asm volatile("set.lt.u32.f32 %0, %1, %2;" : "=r"(__d) : "f"(__a), "f"(__b));\n'
"}\n"
)
# slct likewise: the selected value and the number whose sign selects it
# are separately typed.
assert render("slct", ftz="ftz", dtype="b32", ctype="f32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_slct_ftz_b32_f32"
"(uint32_t& __d, uint32_t __a, uint32_t __b, float __c) {\n"
' asm volatile("slct.ftz.b32.f32 %0, %1, %2, %3;" : "=r"(__d) : "r"(__a), '
'"r"(__b), "f"(__c));\n'
"}\n"
)
# The first type controls the bit width, but does not force d/a/b to share
# an interpretation or a carrier class. A noncanonical helper names all
# four operand dtypes so independently mixed products cannot collide.
assert render(
"slct",
dtypes=("uint32", "float32", "int32", "int32"),
dtype="f32",
ctype="s32",
) == (
"__forceinline__ __device__ void tvm_builtin_ptx_slct_f32_s32_u32_f32_s32_s32"
"(uint32_t& __d, float __a, int32_t __b, int32_t __c) {\n"
' asm volatile("slct.f32.s32 %0, %1, %2, %3;" : "=r"(__d) : "f"(__a), '
'"r"(__b), "r"(__c));\n'
"}\n"
)
# Half-precision comparison (ISA 9.7.8). The packed setp reuses the pipe
# pair, but its two halves mean something else than 9.7.7's: p and q are
# the two lanes' comparisons, not a result and its complement.
assert render("setp_half_pq", cmp="lt", ftz="ftz", type="f16x2") == (
"__forceinline__ __device__ void tvm_builtin_ptx_setp_half_pq_lt_ftz_f16x2"
"(uint32_t& __p, uint32_t& __q, uint32_t __a, uint32_t __b) {\n"
' asm volatile("{ .reg .pred pd0; .reg .pred pd1; '
"setp.lt.ftz.f16x2 pd0|pd1, %2, %3; selp.b32 %0, 1, 0, pd0; "
'selp.b32 %1, 1, 0, pd1; }" : "=r"(__p), "=r"(__q) : "r"(__a), "r"(__b));\n'
"}\n"
)
# set's two type tokens run in either direction: a half-valued answer to an
# integer comparison ...
assert render("set_half", cmp="lt", dtype="f16", stype="s32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_set_half_lt_f16_s32"
"(uint16_t& __d, int32_t __a, int32_t __b) {\n"
' asm volatile("set.lt.f16.s32 %0, %1, %2;" : "=h"(__d) : "r"(__a), "r"(__b));\n'
"}\n"
)
# ... and an integer-valued answer to a half one, where `.ftz` is legal
# because the *source* is the f16 (see `_FTZ_SET_STYPES`).
assert render("set_half", cmp="gt", ftz="ftz", dtype="u32", stype="f16") == (
"__forceinline__ __device__ void tvm_builtin_ptx_set_half_gt_ftz_u32_f16"
"(uint32_t& __d, uint16_t __a, uint16_t __b) {\n"
' asm volatile("set.gt.ftz.u32.f16 %0, %1, %2;" : "=r"(__d) : "h"(__a), "h"(__b));\n'
"}\n"
)
# Logic and shift (ISA 9.7.9). `.pred` as an instruction type, rather than
# as one operand's class: three bridges -- two setp in, one selp out --
# wrapped around a single instruction that never touches the carriers.
assert render("and", type="pred") == (
"__forceinline__ __device__ void tvm_builtin_ptx_and_pred"
"(uint32_t& __d, uint32_t __a, uint32_t __b) {\n"
' asm volatile("{ .reg .pred pd0; .reg .pred ps0; .reg .pred ps1; '
"setp.ne.b32 ps0, %1, 0; setp.ne.b32 ps1, %2, 0; and.pred pd0, ps0, ps1; "
'selp.b32 %0, 1, 0, pd0; }" : "=r"(__d) : "r"(__a), "r"(__b));\n'
"}\n"
)
# lop3's BoolOp form: the pipe pair holding two DIFFERENT register classes.
# `d` is an ordinary b32 output bound straight to %0 while `p` rides the
# predicate bridge, so the pair renders `%0|pd0` -- the mechanism groups
# the text and leaves each half its own constraint. The LUT byte is an open
# immediate, baked into the text and into the helper name.
assert render("lop3_bool", boolop="and", type="b32", imms=("128",)) == (
"__forceinline__ __device__ void tvm_builtin_ptx_lop3_bool_and_b32_128"
"(uint32_t& __d, uint32_t& __p, uint32_t __a, uint32_t __b, uint32_t __c, "
"uint32_t __q) {\n"
' asm volatile("{ .reg .pred pd0; .reg .pred ps0; setp.ne.b32 ps0, %5, 0; '
'lop3.and.b32 %0|pd0, %2, %3, %4, 128, ps0; selp.b32 %1, 1, 0, pd0; }" '
': "=r"(__d), "=r"(__p) : "r"(__a), "r"(__b), "r"(__c), "r"(__q));\n'
"}\n"
)
# Discarding d is a fixed syntax sibling, so `_` is table-owned text and
# neither a C parameter nor a use of the caller-selectable sink mechanism.
assert render("lop3_bool_sink", boolop="and", type="b32", imms=("128",)) == (
"__forceinline__ __device__ void tvm_builtin_ptx_lop3_bool_sink_and_b32_128"
"(uint32_t& __p, uint32_t __a, uint32_t __b, uint32_t __c, uint32_t __q) {\n"
' asm volatile("{ .reg .pred pd0; .reg .pred ps0; setp.ne.b32 ps0, %4, 0; '
'lop3.and.b32 _|pd0, %1, %2, %3, 128, ps0; selp.b32 %0, 1, 0, pd0; }" '
': "=r"(__p) : "r"(__a), "r"(__b), "r"(__c), "r"(__q));\n'
"}\n"
)
# The shift amount is a 32-bit value "regardless of the instruction type",
# so a 16-bit shl still takes a uint32 there.
assert render("shl", type="b16") == (
"__forceinline__ __device__ void tvm_builtin_ptx_shl_b16"
"(uint16_t& __d, uint16_t __a, uint32_t __b) {\n"
' asm volatile("shl.b16 %0, %1, %2;" : "=h"(__d) : "h"(__a), "r"(__b));\n'
"}\n"
)
# Data movement (ISA 9.7.10). shfl.sync's `d|p`: a pipe pair whose halves
# are DIFFERENT register classes -- an ordinary b32 result bound straight
# to %0, and an in-range predicate that rides the bridge.
assert render("shfl_sync_p", mode="up", type="b32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_shfl_sync_p_up_b32"
"(uint32_t& __d, uint32_t& __p, uint32_t __a, uint32_t __b, uint32_t __c, "
"uint32_t __membermask) {\n"
' asm volatile("{ .reg .pred pd0; shfl.sync.up.b32 %0|pd0, %2, %3, %4, %5; '
'selp.b32 %1, 1, 0, pd0; }" : "=r"(__d), "=r"(__p) : "r"(__a), "r"(__b), '
'"r"(__c), "r"(__membermask));\n'
"}\n"
)
# multimem's vector line: `.v4` of `.f16` is one 64-bit access spread over
# four half registers, and `.acc::f32` widens only the accumulation.
assert render("multimem_ld_reduce_f_vec", op="add", acc="acc::f32", vec="v4", type="f16") == (
"__forceinline__ __device__ void "
"tvm_builtin_ptx_multimem_ld_reduce_f_vec_add_acc__f32_v4_f16"
"(uint16_t& __d0, uint16_t& __d1, uint16_t& __d2, uint16_t& __d3, "
"const void* __addr) {\n"
' asm volatile("multimem.ld_reduce.add.acc::f32.v4.f16 {%0, %1, %2, %3}, [%4];" '
': "=h"(__d0), "=h"(__d1), "=h"(__d2), "=h"(__d3) : "l"(__addr) : "memory");\n'
"}\n"
)
# A table-owned immediate: the ISA fixes applypriority's size at 128, so it
# is in the text and the helper has no parameter for it at all.
assert render("applypriority", space="global", level="L2::evict_normal") == (
"__forceinline__ __device__ void tvm_builtin_ptx_applypriority_global_L2__evict_normal"
"(const void* __addr) {\n"
' asm volatile("applypriority.global.L2::evict_normal [%0], 128;" : : "l"(__addr)'
' : "memory");\n'
"}\n"
)
# A caller-chosen immediate: tensormap.replace's field3 new_val must be a
# constant, so each legal value is its own helper, named for it.
assert render(
"tensormap_replace_elemtype",
mode="tile",
field="elemtype",
space="global",
width="b1024",
type="b32",
imms=("7",),
) == (
"__forceinline__ __device__ void "
"tvm_builtin_ptx_tensormap_replace_elemtype_tile_elemtype_global_b1024_b32_7"
"(const void* __addr) {\n"
' asm volatile("tensormap.replace.tile.elemtype.global.b1024.b32 [%0], 7;" : : '
'"l"(__addr) : "memory");\n'
"}\n"
)
# Parallel synchronization (ISA 9.7.15). elect.sync's `d|p` is the one
# pipe pair the ISA makes mandatory -- ptxas rejects a bare destination --
# and its halves are two different register classes.
assert render("elect_sync") == (
"__forceinline__ __device__ void tvm_builtin_ptx_elect_sync"
"(uint32_t& __d, uint32_t& __p, uint32_t __membermask) {\n"
' asm volatile("{ .reg .pred pd0; elect.sync %0|pd0, %2; '
'selp.b32 %1, 1, 0, pd0; }" : "=r"(__d), "=r"(__p) : "r"(__membermask));\n'
"}\n"
)
# atom.cas is the four-value shape, and the only atom line reaching 128
# bits in every position -- the "q" constraint the C boundary needs.
assert render("atom_cas", space="global", op="cas", type="b128") == (
"__forceinline__ __device__ void tvm_builtin_ptx_atom_cas_global_cas_b128"
"(__uint128_t& __d, const void* __addr, __uint128_t __compare, "
"__uint128_t __value) {\n"
' asm volatile("atom.global.cas.b128 %0, [%1], %2, %3;" : "=q"(__d) : '
'"l"(__addr), "q"(__compare), "q"(__value) : "memory");\n'
"}\n"
)
# The bit-bucket overload is a fixed `_` operand, selected by omitting the
# returned-value destination from the call. It has no output constraint.
assert render("atom_bitbucket", space="global", op="add", type="u32") == (
"__forceinline__ __device__ void tvm_builtin_ptx_atom_bitbucket_global_add_u32"
"(const void* __addr, uint32_t __value) {\n"
' asm volatile("atom.global.add.u32 _, [%0], %1;" : : "l"(__addr), '
'"r"(__value) : "memory");\n'
"}\n"
)
# Vector atom also uses one whole bit bucket, not a brace group of lane
# sinks. The value remains a normal register group.
assert render("atom_vec_f32_bitbucket", space="global", op="add", vec="v2", type="f32") == (
"__forceinline__ __device__ void "
"tvm_builtin_ptx_atom_vec_f32_bitbucket_global_add_v2_f32"
"(const void* __addr, float __value0, float __value1) {\n"
' asm volatile("atom.global.add.v2.f32 _, [%0], {%1, %2};" : : '
'"l"(__addr), "f"(__value0), "f"(__value1) : "memory");\n'
"}\n"
)
# The ISA documents the same bit-bucket spelling for bf16 atom; the table
# withholds it pending certification on CUDA 13.4 (see
# `_check_atom_half_bitbucket`). Returned-value bf16 atom and f16 bit
# buckets remain registered.
for entry_name, by_name in (
("atom_half_bitbucket", {"op": "add", "noftz": "noftz", "type": "bf16"}),
(
"atom_vec_half_bitbucket",
{"op": "add", "noftz": "noftz", "vec": "v2", "type": "bf16"},
),
):
with pytest.raises(ValueError, match="bf16 atom bit-bucket destination.*withholds"):
tokens_for(TABLE[entry_name], **by_name)
# red's half-word and packed vector syntax lines share this entry; the
# conditional policy operand follows the value group.
assert render(
"red_vec_half",
sem="release",
scope="sys",
space="global",
op="max",
noftz="noftz",
cache="L2::cache_hint",
vec="v4",
type="f16x2",
) == (
"__forceinline__ __device__ void "
"tvm_builtin_ptx_red_vec_half_release_sys_global_max_noftz_"
"L2__cache_hint_v4_f16x2"
"(const void* __addr, uint32_t __value0, uint32_t __value1, "
"uint32_t __value2, uint32_t __value3, uint64_t __cache_policy) {\n"
' asm volatile("red.release.sys.global.max.noftz.L2::cache_hint.v4.f16x2 '
'[%0], {%1, %2, %3, %4}, %5;" : : "l"(__addr), "r"(__value0), '
'"r"(__value1), "r"(__value2), "r"(__value3), "l"(__cache_policy) '
': "memory");\n'
"}\n"
)
# bar.red reduces a predicate across a barrier, so a predicate crosses the
# boundary in BOTH directions around one instruction: setp on the way in,
# selp on the way out.
assert render("bar_red_pred", action="red", op="and", type="pred") == (
"__forceinline__ __device__ void tvm_builtin_ptx_bar_red_pred_red_and_pred"
"(uint32_t& __d, uint32_t __a, uint32_t __c) {\n"
' asm volatile("{ .reg .pred pd0; .reg .pred ps0; setp.ne.b32 ps0, %2, 0; '
'bar.red.and.pred pd0, %1, ps0; selp.b32 %0, 1, 0, pd0; }" : "=r"(__d) : '
'"r"(__a), "r"(__c) : "memory");\n'
"}\n"
)
# Register group: `{%1, %2}` is ONE PTX operand spanning two registers, so
# the group is braces in the asm text and flat C parameters around it. An
# unpack turns that into two "=" outputs.
assert render("mov_pack_b32x2", type="b64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_mov_pack_b32x2_b64"
"(uint64_t& __d, uint32_t __a0, uint32_t __a1) {\n"
' asm("mov.b64 %0, {%1, %2};" : "=l"(__d) : "r"(__a0), "r"(__a1));\n'
"}\n"
)
assert render("mov_unpack_b32x2", type="b64") == (
"__forceinline__ __device__ void tvm_builtin_ptx_mov_unpack_b32x2_b64"
"(uint32_t& __d0, uint32_t& __d1, uint64_t __a) {\n"
' asm("mov.b64 {%0, %1}, %2;" : "=r"(__d0), "=r"(__d1) : "l"(__a));\n'
"}\n"
)
# 128-bit destination on the "q" constraint, and asm_volatile=False: a
# register shuffle nvcc is free to common up.
assert render("mov_pack_b64x2", type="b128") == (
"__forceinline__ __device__ void tvm_builtin_ptx_mov_pack_b64x2_b128"
"(__uint128_t& __d, uint64_t __a0, uint64_t __a1) {\n"
' asm("mov.b128 %0, {%1, %2};" : "=q"(__d) : "l"(__a0), "l"(__a1));\n'
"}\n"
)
# Same instruction text, f32 lanes: the dtype axis renames the helper and
# moves the lanes to "f", and nothing else moves. This is the shape the
# packed-f32x2 call sites use.
assert render("mov_pack_b32x2", type="b64", dtypes=("uint64", "float32")) == (
"__forceinline__ __device__ void tvm_builtin_ptx_mov_pack_b32x2_b64_u64_f32"
"(uint64_t& __d, float __a0, float __a1) {\n"
' asm("mov.b64 %0, {%1, %2};" : "=l"(__d) : "f"(__a0), "f"(__a1));\n'
"}\n"
)
def test_ptx_coercion_ir_forms():
"""The trace-time addr coercion, written down as IR-level assertions."""
from tvm.ir.type import PointerType, PrimType
shared_ptr = tvm.tirx.Var("p", PointerType(PrimType("uint32"), "shared"))
global_ptr = tvm.tirx.Var("g", PointerType(PrimType("uint32"), "global"))
raw_u32 = tvm.tirx.Var("a", "uint32")
val = tvm.tirx.Var("v", "uint32")
# Shared slot + shared pointer: engine wraps with an explicit cvta call node.
call = T.ptx.st.shared__cta.b32(shared_ptr, val)
addr = call.args[0]
assert addr.op.name == "tirx.cuda.cvta_generic_to_shared"
assert addr.args[0].same_as(shared_ptr)
# Shared slot + raw uint32: passthrough, no conversion inserted.
call = T.ptx.st.shared__cta.b32(raw_u32, val)
assert call.args[0].same_as(raw_u32)
# Global slot + global pointer: passthrough.
call = T.ptx.st.release.gpu.global_.b32(global_ptr, val)
assert call.args[0].same_as(global_ptr)
# Modifiers ride as trailing positional string args in slot order, then
# the pred marker ("pred" or "") that makes the printed form re-parse
# exactly instead of guessing from the argument count.
assert [str(a).strip('"') for a in call.args[2:]] == [
"",
"release",
"gpu",
"global",
"",
"",
"",
"b32",
"",
]
# A shared-space slot converts whatever pointer it is given: TIRx pointer
# scopes are not a reliable discriminator (a shared buffer's ptr_to()
# reports 'global'), and the legacy helpers converted unconditionally too.
call = T.ptx.st.shared__cta.b32(global_ptr, val)
assert call.args[0].op.name == "tirx.cuda.cvta_generic_to_shared"
# Predication: pred rides after the operands (codegen derives the
# predicated form from the arg count).
flag = tvm.tirx.Var("f", "uint32")
call = T.ptx.st.release.gpu.global_.b32(global_ptr, val, pred=flag)
assert call.args[2].same_as(flag)
assert len(call.args) == 2 + 1 + 8 + 1 # operands + pred + slot tokens + marker
out = tvm.tirx.decl_buffer((1,), "uint32", name="out", scope="local")
call = T.ptx.ld.global_.b32(out[0], global_ptr, pred=flag)
assert str(call.args[-1]).strip('"') == "pred"
call = T.ptx.ld.global_.b32(out[0], global_ptr, pred=flag, preserve_dst=True)
assert str(call.args[-1]).strip('"') == "pred,keep"
with pytest.raises(ValueError, match="requires pred"):
T.ptx.ld.global_.b32(out[0], global_ptr, preserve_dst=True)
with pytest.raises(ValueError, match="requires a written destination"):
T.ptx.st.release.gpu.global_.b32(global_ptr, val, pred=flag, preserve_dst=True)
# fp16/bf16 dtypes bring in __half / __nv_bfloat16 and their bit-cast helpers.
_CERT_PRELUDE = "#include <cstdint>\n#include <cuda_fp16.h>\n#include <cuda_bf16.h>"
# A certification unit must be small enough that ptxas is checking helpers,
# rather than being stress-tested by one translation unit containing every
# sampled table variant. The limit is on generated source (not helper count),
# because helper sizes vary substantially across instruction families.
_CERT_MAX_SOURCE_CHARS = 256 * 1024
_CERT_HELPER_SIGNATURE_RE = re.compile(
r"\A__forceinline__ __device__ void (?P<helper>[A-Za-z_]\w*)\((?P<params>[^)]*)\) \{"
)
_CERT_PARAM_RE = re.compile(r"(?P<type>.+?)(?P<reference>&)?\s+(?P<name>__[A-Za-z_]\w*)\Z")
def _certification_kernel(helper, helper_source, kernel_index):
"""Keep a helper inline and call it from a retained, production-shaped kernel.
Merely placing an unreferenced ``__forceinline__`` helper in a translation
unit does not certify it: nvcc may discard it before ptxas sees its body.
The old workaround stripped ``__forceinline__`` and retained the resulting
device function with RDC. That changes the device ABI and is not an
equivalent compilation shape for wide inline-asm operands.
A global caller is retained without RDC and forces the original helper to
inline, which is the shape emitted code uses. Pointer arguments come from
the runtime sink so address instructions are not specialized to null.
Register inputs are locals, avoiding an artificial device ABI boundary;
ordinary values and raw 32-bit addresses are loaded through the sink so
they keep the runtime data flow of generated calls. Predicate and b128
inputs remain local because crossing either through the certification ABI
creates a tool-only compilation shape. Written values are loaded and
stored through the sink so non-volatile output instructions cannot be
optimized away before ptxas.
"""
match = _CERT_HELPER_SIGNATURE_RE.match(helper_source)
assert match is not None, f"cannot parse certification signature for {helper}"
assert match.group("helper") == helper
declarations = []
arguments = []
stores = []
params = match.group("params")
parsed_params = params.split(", ") if params else ()
for param_index, param in enumerate(parsed_params):
param_match = _CERT_PARAM_RE.fullmatch(param)
assert param_match is not None, f"cannot parse {helper} parameter: {param}"
c_type = param_match.group("type")
param_name = param_match.group("name")
local = f"__cert_arg{param_index}"
input_offset = 16 * param_index
if c_type.endswith("*"):
declarations.append(f" {c_type} {local} = __cert_sink + {input_offset};")
elif param_match.group("reference"):
declarations.append(
f" {c_type} {local} = "
f"*reinterpret_cast<const {c_type}*>(__cert_sink + {input_offset});"
)
elif c_type in ("__int128_t", "__uint128_t") or param_name == "__pred":
declarations.append(f" {c_type} {local}{{}};")
else:
declarations.append(
f" {c_type} {local} = "
f"*reinterpret_cast<const {c_type}*>(__cert_sink + {input_offset});"
)
arguments.append(local)
if param_match.group("reference"):
# Every C binding is at most 16 bytes. Distinct, aligned locations
# keep all outputs observable without imposing a constraint letter
# of our own on the value being certified.
stores.append(
f" *reinterpret_cast<{c_type}*>(__cert_sink + "
f"{16 * (len(parsed_params) + len(stores))}) = {local};"
)
kernel = [
f'extern "C" __global__ void __ptx_cert_{kernel_index}(char* __cert_sink) {{',
*declarations,
f" {helper}({', '.join(arguments)});",
*stores,
"}",
]
return "\n".join((helper_source, *kernel))
def _append_certification(by_arch, arch, helper, helper_source):
items = by_arch.setdefault(arch, [])
items.append((helper, _certification_kernel(helper, helper_source, len(items))))
def _certification_batches(items):
"""Yield deterministic, source-size-bounded groups of certification kernels."""
batch = []
size = len(_CERT_PRELUDE) + 1
for item in items:
item_size = len(item[1]) + 1
assert item_size + len(_CERT_PRELUDE) <= _CERT_MAX_SOURCE_CHARS, (
f"single certification helper exceeds source limit: {item[0]}"
)
if batch and size + item_size > _CERT_MAX_SOURCE_CHARS:
yield batch
batch = []
size = len(_CERT_PRELUDE) + 1
batch.append(item)
size += item_size
if batch:
yield batch
def _assert_certifications_ok(by_arch):
for arch, items in by_arch.items():
for batch_index, batch in enumerate(_certification_batches(items)):
names, sources = zip(*batch, strict=True)
try:
_assert_ptxas_ok("\n".join((_CERT_PRELUDE, *sources)), arch=arch)
except Exception as err:
message = str(err)
marker = "Compilation error:"
diagnostic = (
message[message.rfind(marker) :] if marker in message else message[-4000:]
)
raise AssertionError(
f"PTX certification failed for {arch} batch {batch_index} "
f"({len(names)} helpers, {names[0]} .. {names[-1]}):\n{diagnostic}"
) from None
_ASM_RE = re.compile(r'asm(?: volatile)?\("(.*?)"\s*:', re.S)
_BLOCK_RE = re.compile(r"^\{ (?P<body>.*) \}$")
# The asm block's sanctioned non-instructions: `render.BRIDGE`'s register
# declarations and the conversions that move a value between the class the ISA
# names and the carrier inline asm can bind, plus `@p`'s own guard. Never
# semantics -- see BRIDGE.
#
# Matched in FULL, not by opcode prefix, because an opcode is no longer a
# discriminator: `setp` and `selp` are registered instructions (ISA 9.7.7), so
# a helper's one real instruction can carry the same mnemonic as the bridge
# statements around it. What separates them is the shape the bridge always
# has -- it names a bridge-local register (`p`, `ps<n>`, `pd<n>`, `raw_<slot>`)
# and compares or selects against the literals the conversion is made of, while
# a registered instruction's operands are `%<n>` throughout. Matching the whole
# statement also makes this stricter than the prefix form it replaces: a stray
# conversion in some other shape is now a failure instead of being peeled.
_BOUNDARY_RE = re.compile(
r"|".join(
(
r"\.reg \.pred (?:p|ps\d+|pd\d+);", # @p guard / pred bridge declarations
r"\.reg \.b8 raw_\w+;", # st.async byte-register bridge declaration
r"setp\.ne\.b32 (?:p|ps\d+), %\d+, 0;", # @p guard, pred_src conversion in
r"selp\.b32 %\d+, 1, 0, pd\d+;", # pred_dst materialization out
r"cvt\.u8\.u16 raw_\w+, %\d+;", # st.async byte conversion in
r"cvt\.u16\.u8 %\d+, raw_\w+;", # st.async byte conversion out
)
)
)
def _as_render_args(rendering):
"""Reorder a five-field ``renderings`` item for ``render_variant``."""
tokens, dtypes, predicated, imms, sinks = rendering
return tokens, predicated, dtypes, imms, sinks
def _addr_offset_samples(entry):
"""Small certification axis for address immediates, separate from modifier products."""
from tvm.backend.cuda.ptx.table import renderings
enabled = [
logical_slot
for logical_slot, slot in enumerate(s for s in entry.operands if s.kind == "addr")
if slot.allow_imm_offset
]
if not enabled:
return ()
representative = _as_render_args(next(iter(renderings(entry))))
samples = [
(representative, ((logical_slot, offset),))
for logical_slot in enabled
for offset in (16, -16)
]
if len(enabled) > 1:
samples.append(
(
representative,
tuple(
(logical_slot, 16 if index % 2 == 0 else -16)
for index, logical_slot in enumerate(enabled)
),
)
)
return tuple(samples)
def _sole_instruction(asm_text):
"""The single PTX statement in ``asm_text``, or None if it is not exactly one.
The sanctioned boundary conversions are peeled first: ``@p``'s guard,
pred_src's setp, pred_dst's selp. They convert values at the block
boundary; they never add a second instruction.
"""
m = _BLOCK_RE.match(asm_text)
if m:
stmts = [f"{part.strip()};" for part in m.group("body").split(";") if part.strip()]
core = [st for st in stmts if not _BOUNDARY_RE.fullmatch(st)]
if len(core) != 1:
return None
body = core[0].removeprefix("@p ")
else:
body = asm_text
if body.count(";") != 1 or not body.endswith(";"):
return None
return body
def test_ptx_single_instruction_invariant():
"""Every ptx variant emits exactly ONE native PTX instruction.
This is the dialect's defining constraint, enforced mechanically so it
cannot erode as the table grows: the only multi-statement form allowed is
the framework-level ``@p`` wrapper, which guards a single instruction
rather than adding one. cvta coercion is a separate IR node and must never
appear inside a helper body.
Framework boundary conversions such as predicate materialization and the
byte-register bridges are peeled before counting. ``RAW_ENTRIES`` is the
separate closed exemption list for a genuinely hand-written helper body;
asserting that it equals the table's ``raw_render`` entries means a new one
cannot be added without editing this test.
"""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, renderings
# Empty, and it should stay that way: the one shape that used to need a
# hand-written body (`.e2m1x2`'s .b8 operand) turned out to be a dtype with
# a bridge, not an irregular family.
RAW_ENTRIES = set()
assert RAW_ENTRIES == {name for name, e in TABLE.items() if e.raw_render}, (
"the set of hand-written (raw_render) entries changed; each one is a "
"permanent exemption from the single-instruction invariant, so it has "
"to be added here deliberately"
)
asm_re, sole_instruction = _ASM_RE, _sole_instruction
checked = 0
for entry in TABLE.values():
raw = entry.raw_render is not None
for tokens, dtypes, predicated, imms, sinks in renderings(entry):
opcode, helper, source = render_variant(entry, tokens, predicated, dtypes, imms, sinks)
asm_blocks = asm_re.findall(source)
assert len(asm_blocks) == 1, f"{opcode}: {len(asm_blocks)} asm blocks, expected 1"
if raw:
# A raw helper may contain several statements, which is why it
# is exempt. The instruction must still be in there as its own
# statement. The helper name needs no assertion:
# `render_variant` passes the name it derived into
# `raw_render`, so a raw body cannot declare a different one.
assert f"; {opcode} " in asm_blocks[0], (
f"{opcode}: raw body does not emit the opcode: {asm_blocks[0]!r}"
)
else:
instr = sole_instruction(asm_blocks[0])
assert instr is not None, f"{opcode}: not a single statement: {asm_blocks[0]!r}"
assert instr.startswith(opcode + " ") or instr == opcode + ";", (
f"{opcode}: emitted instruction does not match the opcode: {instr!r}"
)
# The address coercion `cvta` is a separate IR node, so no OTHER
# instruction's helper may smuggle one in. The cvta entries
# themselves are the exception, being that instruction: the guard
# keys off the mnemonic rather than the table name so that every
# syntax line of it (`cvta.to.space` and `cvta.space`) is covered.
assert "cvta" not in source or entry.ptx_name == "cvta", (
f"{opcode}: cvta must be a separate IR node, never inside a helper"
)
# Every helper is void: a PTX destination is an operand, never a C
# return value.
assert source.startswith("__forceinline__ __device__ void "), (
f"{opcode}: helper must be void, got {source.splitlines()[0]!r}"
)
assert "return" not in source, f"{opcode}: helper must not return a value"
checked += 1
assert checked > 0
def test_ptx_single_instruction_invariant_detects_violations():
"""Falsify the probe: it must reject every shape the invariant forbids.
A guard that has never been shown to fail is worth nothing, so exercise
the real helper the invariant test uses.
"""
forbidden = {
# two chained instructions
"bundle": 'asm volatile("mov.u32 %0, 1; add.u32 %0, %0, 2;" : "=r"(x));',
# spin loop with a label and a branch (the mbarrier.try_wait shape)
"spin": 'asm volatile("{ LAB: mbarrier.try_wait.b64 p, [%0]; @!p bra LAB; }" :: "r"(a));',
# a prologue that computes rather than converts: `shl` is arithmetic,
# so it is a second instruction no matter that it feeds the first.
# (Contrast the b8 staging below, which is a sanctioned conversion.)
"prologue": 'asm volatile("{ .reg .b32 t; shl.b32 t, %1, 4; cvt.f32 %0, t; }" : "=r"(d));',
# a bridge conversion on a register class that has no bridge: the
# prefixes are a closed set, not "anything that looks like a cvt".
"unsanctioned": 'asm volatile("{ cvt.u32.u16 %0, %1; st.b32 [%2], %0; }" : "=r"(d));',
}
for shape, src in forbidden.items():
assert _sole_instruction(_ASM_RE.findall(src)[0]) is None, f"{shape} slipped through"
# The sanctioned wrappers are NOT violations -- each carries exactly one
# instruction, everything else being a `render.BRIDGE` boundary conversion.
guarded = "{ .reg .pred p; setp.ne.b32 p, %2, 0; @p red.relaxed.gpu.global.add.u32 [%0], %1; }"
assert _sole_instruction(guarded) == "red.relaxed.gpu.global.add.u32 [%0], %1;"
staged = "{ .reg .b8 raw_a; cvt.u8.u16 raw_a, %1; cvt.rn.f16x2.e2m1x2 %0, raw_a; }"
assert _sole_instruction(staged) == "cvt.rn.f16x2.e2m1x2 %0, raw_a;"
def test_ptx_all_variants_render_unique():
"""Every legal variant renders; helper names (incl. @p twins) are unique."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, renderings, variants
names = set()
total = 0
for entry in TABLE.values():
assert variants(entry), f"{entry.name}: check() filtered out every combination"
for tokens, dtypes, predicated, imms, sinks in renderings(entry):
opcode, helper, source = render_variant(entry, tokens, predicated, dtypes, imms, sinks)
assert helper not in names, f"helper name collision: {helper}"
names.add(helper)
if predicated: # framework-level @p twin, guarded inside the block
assert f"@p {opcode} " in source or f"@p {opcode};" in source
else:
# The instruction may open the asm text or sit inside a
# boundary-conversion block after a setp.
assert (
f'"{opcode} ' in source
or f'"{opcode};"' in source
or f"; {opcode} " in source
or f"; {opcode};" in source
)
total += not predicated # a @p twin is not a separate variant
for args, addr_offsets in _addr_offset_samples(entry):
_, helper, _ = render_variant(entry, *args, addr_offsets=addr_offsets)
assert helper not in names, f"address-offset helper name collision: {helper}"
names.add(helper)
assert total == 762050 # update when the table grows or a ptxas gap narrows it
def test_ptx_no_instruction_registered_twice():
"""No two entries register the same PTX instruction and C-call shape.
Sibling entries may intentionally share an opcode when their public call
shapes are disjoint (bare mapa.u64 accepts a pointer or a raw uint64).
Strip only helper and parameter *names*; the remaining C types, asm text,
and constraints form the identity whose duplication would be ambiguous.
"""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, renderings
owners = {}
for entry in TABLE.values():
for tokens, dtypes, predicated, imms, sinks in renderings(entry):
_, _, source = render_variant(entry, tokens, predicated, dtypes, imms, sinks)
instruction = re.sub(r"__[A-Za-z_][A-Za-z0-9_]*", "ARG", source)
instruction = re.sub(r"tvm_builtin_\w+", "FN", instruction)
first = owners.setdefault(instruction, entry.name)
assert first == entry.name, f"{first} and {entry.name} both register\n{instruction}"
def test_ptx_dispatch_unambiguous():
"""No two entries may accept the same call.
Models what the engine resolves by, and only that: the written token
multiset (slot names are invisible to `_fill`, but repeated tokens remain
repeated), the operand count, and each position's acceptance class.
Declared spaces that `_coerce_address` treats
alike collapse into one class, which is what makes this stricter than the
rendering check above — two entries can emit different assembly and still
leave a call with nothing to choose between them.
"""
from tvm.backend.cuda.ptx.table import (
TABLE,
mods,
operand_dtypes,
operand_layout,
operand_space,
operand_type,
variants,
)
def accepts(slot, mod_map, pred_is_distinct=True):
if slot.kind == "addr":
space = operand_space(slot, mod_map)
if space == "tmem":
return ("addr", "tmem")
return ("addr", "shared*" if space.startswith("shared") else "generic")
if slot.kind == "imm":
return ("imm", slot.literal, slot.choices)
if slot.kind != "reg":
return (slot.kind,)
if pred_is_distinct and operand_type(slot, mod_map) == "pred":
# A `.pred` operand shares its uint32 carrier with every integer
# one, so it is only a class of its own because `T.ptx.pred(...)`
# evidences it at the call. `pred_is_distinct=False` models the
# engine as it would be WITHOUT that tag -- see the falsification
# twin below, which is what proves this key can see the collision
# it exists to prevent.
return (slot.rw, "pred")
return (slot.rw, tuple(sorted(operand_dtypes(slot, mod_map))))
owners = {}
for entry in TABLE.values():
for tokens in variants(entry):
mod_map = mods(entry, tokens)
layout = operand_layout(entry, mod_map)
shape = tuple(accepts(s, mod_map) for s, _, n in layout for _ in range(n))
key = (entry.family, tuple(sorted(t for t in tokens if t)), shape)
first = owners.setdefault(key, entry.name)
assert first == entry.name, (
f"{first} and {entry.name} both accept "
f"T.ptx.{entry.family} with {sorted(key[1])} and {len(shape)} operand(s)"
)
def test_ptx_dispatch_model_detects_a_collapsed_class():
"""Falsify the discriminator: erasing a class must make it go red.
`.pred` is only an acceptance class of its own because `T.ptx.pred(...)`
evidences it -- the carrier it rides is the same uint32 every integer
operand uses. Collapse that distinction and the two `cp.async` lines whose
only difference is `{, src-size}` vs `{, ignore-src}` become
indistinguishable, which is the defect this guard exists to catch. A guard
that has only ever been seen to pass proves nothing.
"""
from tvm.backend.cuda.ptx.table import (
TABLE,
mods,
operand_dtypes,
operand_layout,
operand_space,
variants,
)
def accepts(slot, mod_map):
# The same key as test_ptx_dispatch_unambiguous, minus the pred class.
if slot.kind == "addr":
space = operand_space(slot, mod_map)
if space == "tmem":
return ("addr", "tmem")
return ("addr", "shared*" if space.startswith("shared") else "generic")
if slot.kind == "imm":
return ("imm", slot.literal, slot.choices)
if slot.kind != "reg":
return (slot.kind,)
return (slot.rw, tuple(sorted(operand_dtypes(slot, mod_map))))
owners, collisions = {}, set()
for entry in TABLE.values():
for tokens in variants(entry):
mod_map = mods(entry, tokens)
layout = operand_layout(entry, mod_map)
shape = tuple(accepts(s, mod_map) for s, _, n in layout for _ in range(n))
key = (entry.family, tuple(sorted(t for t in tokens if t)), shape)
first = owners.setdefault(key, entry.name)
if first != entry.name:
collisions.add(tuple(sorted((first, entry.name))))
assert collisions == {
("cp_async_ca_ignore_src", "cp_async_ca_src_size"),
("cp_async_cg_ignore_src", "cp_async_cg_src_size"),
}, f"expected exactly the src-size/ignore-src pairs to collapse, got {sorted(collisions)}"
def test_ptx_stub_up_to_date():
"""The checked-in tvm.script.tirx stub must match the generator."""
from tvm.backend.cuda.ptx import gen_stubs
stub = gen_stubs.STUB_PATH
assert stub.read_text(encoding="utf-8") == gen_stubs.generate(), (
"python/tvm/script/tirx.pyi is stale; regenerate with "
"`python -m tvm.backend.cuda.ptx.gen_stubs -o python/tvm/script/tirx.pyi`"
)
@requires_nvcc
def test_ptxas_gate_rejects_invalid():
"""Honesty check: the gate path must actually reject bad instructions."""
bogus = (
"__forceinline__ __device__ void f(uint32_t __x) {\n"
' asm volatile("totally.bogus.instr %0;" : : "r"(__x));\n'
"}\n"
)
source = "\n".join((_CERT_PRELUDE, _certification_kernel("f", bogus, 0)))
with pytest.raises(Exception, match="bogus|error"):
_assert_ptxas_ok(source)
def test_ptx_vec256_wide_carriers_not_registered():
"""PTX documents wider carriers; CUDA 13.4 ptxas does not compile the axis uniformly (C7907)."""
from tvm.backend.cuda.ptx.table import TABLE, dtype_combos, tokens_for
expected = (
("uint32", "uint64"),
("int32", "uint64"),
("float32", "uint64"),
)
for name in ("ld_vec256", "st_vec256"):
entry = TABLE[name]
tokens = tokens_for(entry, vec="v8", type="b32")
assert dtype_combos(entry, tokens) == expected
_PTX_93_SM103A_FULL_ENTRIES = frozenset(
{
"clmad",
"multimem_st_async",
"multimem_red_async",
"tensormap_replace_swizzle_mode_sm103a",
"multimem_cp_async_bulk",
"multimem_cp_reduce_async_bulk",
"mbarrier_check_layout",
"tcgen05_ld_red",
"tcgen05_ld_red_split",
"fabric_try_get",
"fabric_try_put",
"fabric_try_put_cp_mask",
"fabric_try_put_counted",
"fabric_try_red",
"fabric_try_red_counted",
"fabric_try_pullred",
"fabric_submit",
"fabric_wait",
"fence_proxy_fabric",
}
)
_PTX_93_SM103A_BULK_ENTRIES = frozenset(
{
"cp_async_bulk_g2s_cta",
"cp_async_bulk_g2s_cluster",
"cp_async_bulk_s2c",
"cp_async_bulk_s2g",
"cp_reduce_async_bulk_s2c",
"cp_reduce_async_bulk_s2g",
}
)
_PTX_93_SM103A_PHASE_ENTRIES = frozenset(
{
"mbarrier_test_wait_parity",
"mbarrier_try_wait_parity",
"mbarrier_try_wait_parity_no_hint",
"mbarrier_test_wait",
"mbarrier_try_wait",
"mbarrier_try_wait_hint",
}
)
def _ptx_93_sm103a_manifest():
"""Yield every rendering introduced or extended by PTX 9.3 for SM103a."""
from tvm.backend.cuda.ptx.table import _PTX_94_ENTRIES, TABLE, mods, renderings
ptx94 = {entry.name for entry in _PTX_94_ENTRIES}
for name, entry in TABLE.items():
if name in ptx94:
# The 9.4 delta (sm_107f floors, including the cp.async.bulk
# `*_report` entries) is certified at its own arch by
# test_ptx_all_helpers_certify, not at sm_103a here.
continue
for rendering in renderings(entry):
tokens = rendering[0]
mod_map = mods(entry, tokens)
if (
name in _PTX_93_SM103A_FULL_ENTRIES
or "_report" in name
or (name == "ld" and mod_map["mmio"] and mod_map["sem"] == "acquire")
or (name == "st" and mod_map["mmio"] and mod_map["sem"] == "release")
or (name in _PTX_93_SM103A_BULK_ENTRIES and bool(mod_map.get("sem")))
or (name == "mbarrier_init" and bool(mod_map["layout"]))
or (name == "mbarrier_pending_count" and bool(mod_map["layout"]))
or (name in _PTX_93_SM103A_PHASE_ENTRIES and bool(mod_map["phase_type"]))
):
yield entry, rendering
@pytest.mark.skipif(
not os.environ.get("PTX_CERT"),
reason="exhaustive PTX 9.3 helpers; run with PTX_CERT=1 after changing the table",
)
@requires_nvcc
def test_ptx_93_sm103a_helpers_certify():
"""Every PTX 9.3 addition assembles as a production-shaped SM103a caller."""
from tvm.backend.cuda.ptx.render import render_variant
by_arch = {}
covered = 0
for entry, rendering in _ptx_93_sm103a_manifest():
tokens, dtypes, predicated, imms, sinks = rendering
_, helper, source = render_variant(entry, tokens, predicated, dtypes, imms, sinks)
_append_certification(by_arch, "sm_103a", helper, source)
covered += 1
assert covered == 2668
_assert_certifications_ok(by_arch)
@pytest.mark.skipif(
not env.has_nvcc_version(13, 4),
reason="whole-table certification includes PTX 9.4 forms; need nvcc >= 13.4",
)
@requires_nvcc
def test_ptx_sampled_helpers_assemble():
"""Fast tier: production-shaped calls to a seeded sample assemble."""
import random
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, renderings
rng = random.Random(20260802)
by_arch = {}
for entry in TABLE.values():
arch = entry.cert_arch or PTX_ARCH
# Some dtype domains originate in sets/frozensets. Sort the complete
# rendering tuple so the seeded certification sample is reproducible
# across Python hash seeds and can be isolated from a reported helper
# name alone.
rendered = sorted(renderings(entry), key=repr)
for i in rng.sample(range(len(rendered)), min(48, len(rendered))):
_, helper, source = render_variant(entry, *_as_render_args(rendered[i]))
_append_certification(by_arch, arch, helper, source)
for args, addr_offsets in _addr_offset_samples(entry):
_, helper, source = render_variant(entry, *args, addr_offsets=addr_offsets)
_append_certification(by_arch, arch, helper, source)
_assert_certifications_ok(by_arch)
_CERT_SHARDS = 32
@pytest.mark.skipif(
not os.environ.get("PTX_CERT"),
reason="full-table ptxas certification; run with PTX_CERT=1 after changing the table",
)
@pytest.mark.skipif(
not env.has_nvcc_version(13, 4),
reason="whole-table certification includes PTX 9.4 forms; need nvcc >= 13.4",
)
@requires_nvcc
@pytest.mark.parametrize("shard", range(_CERT_SHARDS))
def test_ptx_all_helpers_certify(shard):
"""Certification tier: EVERY legal variant assembles under ptxas.
One scoped exception: an OPEN immediate operand (role="imm" with neither
literal nor choices) has no domain to enumerate, so its entries are
certified at the enumeration's sample values (imm_combos' open_samples,
default "0"). For those entries this proves the instruction SHAPE
assembles, not the caller's particular constant -- a facility limit of
sampling an open domain, not a property of the table.
Sharded so pytest-xdist can spread the nvcc work::
PTX_CERT=1 pytest -n 16 -k certify tests/python/tirx/codegen/test_ptx_dialect.py
Stride slicing keeps shards balanced (ld dominates the variant count).
Variants are grouped by their family's arch floor and each group is
assembled at that arch: below an instruction's floor ptxas rejects legal
variants, and believing it would delete real coverage. Every helper keeps
``__forceinline__`` and is called by a retained certification kernel, so
ptxas sees the same inline-asm shape as a real generated caller. Each
pytest shard is further split by source size to avoid oversized compiler
translation units.
"""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, renderings
by_arch = {}
covered = 0
baseline = (
(TABLE[name], _as_render_args(rendering), ())
for name in sorted(TABLE)
for rendering in renderings(TABLE[name])
)
address_samples = (
(TABLE[name], args, addr_offsets)
for name in sorted(TABLE)
for args, addr_offsets in _addr_offset_samples(TABLE[name])
)
for index, (entry, args, addr_offsets) in enumerate(itertools.chain(baseline, address_samples)):
if index % _CERT_SHARDS == shard:
covered += 1
arch = entry.cert_arch or PTX_ARCH
_, helper, src = render_variant(entry, *args, addr_offsets=addr_offsets)
_append_certification(by_arch, arch, helper, src)
assert covered, "empty shard: lower _CERT_SHARDS"
_assert_certifications_ok(by_arch)
@requires_nvcc
def test_ptx_nvcc_smoke():
@T.prim_func
def kernel(a_ptr: T.handle, b_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
B = T.match_buffer(b_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
smem = T.alloc_buffer((4,), "uint32", scope="shared")
if tx == 0:
val = T.local_scalar("uint32")
T.ptx.ld.global_.acquire.gpu.b32(val, A.ptr_to([0]))
T.ptx.st.shared__cta.b32(smem.ptr_to([0]), val)
T.ptx.red.relaxed.gpu.global_.add.u32(B.ptr_to([0]), T.uint32(1))
T.ptx.prefetch.global_.L2(A.ptr_to([16]))
T.cuda.cta_sync()
B[tx] = smem[tx % 4]
_assert_ptxas_ok(_cuda_source(kernel))
@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="CUDA GPU not available")
def test_ptx_ld_st_gpu_roundtrip():
@T.prim_func
def kernel(a_ptr: T.handle, b_ptr: T.handle):
A = T.match_buffer(a_ptr, (32,), "uint32")
B = T.match_buffer(b_ptr, (32,), "uint32")
T.device_entry()
T.cta_id([1])
tx = T.thread_id([32])
val = T.local_scalar("uint32")
T.ptx.ld.global_.acquire.gpu.b32(val, A.ptr_to([tx]))
T.ptx.st.release.gpu.global_.b32(B.ptr_to([tx]), val)
with TARGET:
mod = tvm.compile(tvm.IRModule({"main": kernel}), target=TARGET, tir_pipeline="tirx")
def run_and_check():
dev = tvm.cuda(0)
a_np = np.arange(32, dtype=np.uint32) + 100
b_np = np.zeros(32, dtype=np.uint32)
a = tvm.runtime.tensor(a_np, device=dev)
b = tvm.runtime.tensor(b_np, device=dev)
mod(a, b)
np.testing.assert_array_equal(b.numpy(), a_np)
tvm.testing.run_with_gpu_lock(run_and_check)
def test_ptx_tcgen05_ld_red_binds_redval_as_output():
"""ISA 9.7.18.8.3: `tcgen05.ld.red... r, redval, [taddr]` writes the reduction result into
`redval`. The helper must bind it with an output constraint; an input binding compiles but
the kernel never observes the hardware max (measured on GB300: the probe's redval stayed at
its initial value until the binding was fixed)."""
from tvm.backend.cuda.ptx.render import render_variant
from tvm.backend.cuda.ptx.table import TABLE, tokens_for
for name, modifiers, binding in (
(
"tcgen05_ld_red",
dict(
action="ld",
red="red",
sync="sync",
aligned="aligned",
shape="32x32b",
num="x2",
redop="max",
type="f32",
),
'"=f"(__redval)',
),
(
"tcgen05_ld_red_split",
dict(
action="ld",
red="red",
sync="sync",
aligned="aligned",
shape="16x32bx2",
num="x2",
redop="min",
type="s32",
),
'"=r"(__redval)',
),
):
entry = TABLE[name]
assert next(s for s in entry.operands if s.name == "redval").rw == "w"
imms = ("0",) if name.endswith("_split") else ()
_, _, source = render_variant(entry, tokens_for(entry, **modifiers), imms=imms)
assert binding in source, source
assert '"r"(__redval)' not in source and '"f"(__redval)' not in source
if __name__ == "__main__":
tvm.testing.main()