# 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 TIRx op namespace split between T, T.tile, and device namespaces.""" import importlib import sys import types import pytest import tvm from tvm.ir import Op, assert_structural_equal from tvm.script import tirx as T from tvm.script.tirx import tile as Tx from tvm.tirx.stmt import TilePrimitiveCall def _tile_calls(func): calls = [] def visit(stmt): if isinstance(stmt, TilePrimitiveCall): calls.append(stmt) tvm.tirx.stmt_functor.post_order_visit(func.body, visit) return calls def _expr_calls(func): calls = [] def visit(node): if isinstance(node, tvm.ir.Call): calls.append(node) tvm.tirx.stmt_functor.post_order_visit(func.body, visit) return calls def _op_attr(op_name, attr_name): return Op.get(op_name).get_attr(attr_name) def _has_path(root, path): cur = root for part in path.split("."): if not hasattr(cur, part): return False cur = getattr(cur, part) return True def test_tx_is_tile_shorthand_only(): assert T.tile is Tx assert T.tile.copy is Tx.copy assert not hasattr(T, "copy") assert not hasattr(Tx, "SMEMPool") assert not hasattr(Tx, "ScopedOp") assert not hasattr(Tx, "meta_class") assert T.cast is not Tx.cast assert T.sqrt is not Tx.sqrt def test_tx_rejects_expression_overloads(): x = tvm.tirx.Var("x", "float32") y = tvm.tirx.Var("y", "int32") with pytest.raises(TypeError, match="tile-only"): Tx.sqrt(x) with pytest.raises(TypeError, match="tile-only"): T.tile.sqrt(x) with pytest.raises(TypeError, match="tile-only"): Tx.cast(y, "float32") with pytest.raises(TypeError, match="tile-only"): T.tile.cast(y, "float32") def test_builtin_expression_ops_are_not_tile_primitives(): x = tvm.tirx.Var("x", "int32") y = tvm.tirx.Var("y", "float32") cast = T.cast(x, "float32") assert isinstance(cast, tvm.tirx.Cast) assert cast.ty.dtype == "float32" sqrt = T.sqrt(y) assert sqrt.op.name == "tirx.sqrt" fma = T.fma(y, y, y) assert fma.op.name == "tirx.fma" def test_kernel_replace_point_is_builtin_marker_not_tile_primitive(): assert _op_attr("tirx.tvm_kernel_replace_point", "TIRxOpCategory") == "builtin" assert "tirx.tile.tvm_kernel_replace_point" not in Op.list_op_names() assert hasattr(T, "tvm_kernel_replace_point") assert not hasattr(Tx, "tvm_kernel_replace_point") @T.prim_func(check_well_formed=False) def marker(): T.tvm_kernel_replace_point() calls = _expr_calls(marker) assert [call.op.name for call in calls] == ["tirx.tvm_kernel_replace_point"] assert _tile_calls(marker) == [] code = marker.script() assert "T.tvm_kernel_replace_point()" in code assert "tvm_kernel_replace_point" in code assert "T.tile.tvm_kernel_replace_point" not in code assert "Tx.tvm_kernel_replace_point" not in code reparsed = tvm.script.from_source(code) assert_structural_equal(marker, reparsed) def test_tile_shorthand_and_scoped_aliases_use_tile_ops(): @T.prim_func(check_well_formed=False) def tile_aliases(a: T.handle, b: T.handle): A = T.match_buffer(a, (16,), "float32") B = T.match_buffer(b, (16,), "float32") T.tile.copy(A[0:16], B[0:16]) Tx.cast(A[0:16], B[0:16]) T.cta.cast(A[0:16], B[0:16]) Tx.cta.sqrt(A[0:16], B[0:16]) calls = _tile_calls(tile_aliases) assert [call.op.name for call in calls] == [ "tirx.tile.copy", "tirx.tile.cast", "tirx.tile.cast", "tirx.tile.sqrt", ] assert [call.scope.name for call in calls] == ["thread", "thread", "cta", "cta"] def test_device_intrinsic_namespaces_are_canonical_and_classified(): from tvm.backend.cuda.script import ( CUDANamespace as BackendCUDANamespace, ) from tvm.backend.cuda.script import ( NVSHMEMNamespace as BackendNVSHMEMNamespace, ) from tvm.backend.cuda.script import ( PTXNamespace as BackendPTXNamespace, ) from tvm.backend.metal.script import MetalNamespace as BackendMetalNamespace from tvm.backend.trn.script import NKINamespace as BackendNKINamespace from tvm.tirx.script.builder import ir as builder_ir assert isinstance(builder_ir.cuda, BackendCUDANamespace) assert isinstance(builder_ir.ptx, BackendPTXNamespace) assert isinstance(builder_ir.nvshmem, BackendNVSHMEMNamespace) assert isinstance(builder_ir.metal, BackendMetalNamespace) assert isinstance(builder_ir.nki, BackendNKINamespace) assert T.cuda is builder_ir.cuda assert T.ptx is builder_ir.ptx assert T.nvshmem is builder_ir.nvshmem assert T.metal is builder_ir.metal assert T.nki is builder_ir.nki buffer = tvm.tirx.decl_buffer((1,), "float32") calls = [ T.ptx.elect_sync(), T.cuda.thread_fence(), T.nvshmem.fence(), T.nki.identity(buffer[0:1], 1), ] expected = [ ("tirx.ptx.elect_sync", "ptx"), ("tirx.cuda.thread_fence", "cuda"), ("tirx.nvshmem.fence", "nvshmem"), ("tirx.nki.identity", "nki"), ] assert [ (call.op.name, _op_attr(call.op.name, "TDeviceIntrinsicNamespace")) for call in calls ] == expected for op_name, namespace in expected: assert _op_attr(op_name, "TIRxOpCategory") == "device_intrin" assert _op_attr(op_name, "TDeviceIntrinsicNamespace") == namespace def test_backend_specific_wrappers_are_not_root_exports(): from tvm.tirx.cuda import op as cuda_op from tvm.tirx.metal import op as metal_op from tvm.tirx.trn import op as trn_op backend_only_names = [ "ptx_mma", "mma_store", "cuda_thread_fence", "nvshmem_fence", "make_filled_simdgroup_matrix", "simdgroup_load", "nki_load", ] for name in backend_only_names: assert not hasattr(tvm.tirx.op, name) assert not hasattr(tvm.tirx, name) assert not hasattr(T, name) assert cuda_op.ptx_mma assert cuda_op.mma_store assert cuda_op.cuda_thread_fence assert cuda_op.nvshmem_fence assert metal_op.make_filled_simdgroup_matrix assert metal_op.simdgroup_load assert trn_op.nki_load assert hasattr(T, "cuda") assert hasattr(T, "ptx") assert hasattr(T, "nvshmem") assert hasattr(T, "metal") assert hasattr(T, "nki") def test_backend_load_updates_tirx_alias_and_script_facades(monkeypatch): from tvm.tirx.script import builder, parser from tvm.tirx.script.builder import ir as builder_ir backend_name = "unit_test_backend" backend_module_name = f"tvm.backend.{backend_name}" public_module_name = f"tvm.tirx.{backend_name}" public_op_module_name = f"{public_module_name}.op" namespace_name = "unit_test_backend_ns" register_calls = [] class UnitTestNamespace: pass module = types.ModuleType(backend_module_name) module.__path__ = [] module.__package__ = backend_module_name op_module = types.ModuleType(f"{backend_module_name}.op") op_module.marker = object() def register_backend(): register_calls.append(True) builder_ir.register_script_namespace(namespace_name, UnitTestNamespace()) module.register_backend = register_backend monkeypatch.setitem(sys.modules, backend_module_name, module) monkeypatch.setitem(sys.modules, op_module.__name__, op_module) sys.modules.pop(public_module_name, None) sys.modules.pop(public_op_module_name, None) tvm.backend.loader._LOADED_BACKENDS.pop(backend_name, None) if hasattr(tvm.tirx, backend_name): delattr(tvm.tirx, backend_name) with pytest.raises(ModuleNotFoundError): importlib.import_module(public_op_module_name) try: assert tvm.backend.load(backend_name) is None assert tvm.backend.load(backend_name) is None assert register_calls == [True] assert tvm.backend.is_loaded(backend_name) assert getattr(tvm.tirx, backend_name) is module assert sys.modules[public_module_name] is module public_op_module = importlib.import_module(public_op_module_name) assert public_op_module.__tvm_backend_module__ is op_module assert public_op_module.marker is op_module.marker namespace = getattr(builder_ir, namespace_name) assert isinstance(namespace, UnitTestNamespace) assert getattr(builder, namespace_name) is namespace assert getattr(parser, namespace_name) is namespace assert getattr(T, namespace_name) is namespace finally: tvm.backend.loader._LOADED_BACKENDS.pop(backend_name, None) if hasattr(tvm.tirx, backend_name): delattr(tvm.tirx, backend_name) sys.modules.pop(public_module_name, None) sys.modules.pop(public_op_module_name, None) def test_device_intrinsic_printer_roundtrips_canonical_namespaces(): @T.prim_func def device_namespaces(dst: T.handle, src: T.handle): A = T.match_buffer(src, (1,), "float32") R = T.alloc_buffer((1,), "float32", scope="local") T.cuda.cta_sync() T.ptx.ldg32(R[0], 1, A[0], 0) T.metal.simd_shuffle(A[0], 0) T.metal.simd_shuffle_up(A[0], 1) T.metal.simd_shuffle_down(A[0], 1) calls = _expr_calls(device_namespaces) assert [call.op.name for call in calls] == [ "tirx.cuda.cta_sync", "tirx.ptx.ldg32", "tirx.metal.simd_shuffle", "tirx.metal.simd_shuffle_up", "tirx.metal.simd_shuffle_down", ] for op_name, namespace in [ ("tirx.cuda.cta_sync", "cuda"), ("tirx.ptx.ldg32", "ptx"), ("tirx.metal.simd_shuffle", "metal"), ("tirx.metal.simd_shuffle_up", "metal"), ("tirx.metal.simd_shuffle_down", "metal"), ]: assert _op_attr(op_name, "TIRxOpCategory") == "device_intrin" assert _op_attr(op_name, "TDeviceIntrinsicNamespace") == namespace assert _op_attr(op_name, "TCallEffectKind") in (1, 3) code = device_namespaces.script() assert "T.cuda.cta_sync(" in code assert "T.ptx.ldg32(" in code assert "T.metal.simd_shuffle(" in code assert "T.metal.simd_shuffle_up(" in code assert "T.metal.simd_shuffle_down(" in code assert "T.tirx." not in code reparsed = tvm.script.from_source(code) assert reparsed.script() == code assert_structural_equal(device_namespaces, reparsed) def test_registered_tirx_ops_have_exactly_one_category(): if _op_attr("tirx.sqrt", "TIRxOpCategory") is None: pytest.skip("TIRx op categories require a rebuilt C++ runtime") categories = {"builtin", "tile_primitive", "device_intrin"} device_namespaces = {"cuda", "ptx", "nvshmem", "nki", "metal", "webgpu"} flat_tile_only_names = { "tirx.add", "tirx.binary_chain", "tirx.binary_reduce", "tirx.compose_op", "tirx.copy", "tirx.copy_async", "tirx.fdiv", "tirx.fill", "tirx.gemm", "tirx.gemm_async", "tirx.maximum", "tirx.memset", "tirx.minimum", "tirx.mul", "tirx.permute_layout", "tirx.reduce_negate", "tirx.select", "tirx.sub", "tirx.sum", "tirx.unary_reduce", "tirx.zero", } missing = [] invalid = [] lingering_flat_tile = [] for op_name in sorted(name for name in Op.list_op_names() if name.startswith("tirx.")): category = _op_attr(op_name, "TIRxOpCategory") device_namespace = _op_attr(op_name, "TDeviceIntrinsicNamespace") if category is None: missing.append(op_name) continue if category not in categories: invalid.append((op_name, category)) continue if op_name in flat_tile_only_names: lingering_flat_tile.append(op_name) if category == "tile_primitive": if not op_name.startswith("tirx.tile."): lingering_flat_tile.append(op_name) assert device_namespace is None, op_name elif category == "device_intrin": assert device_namespace in device_namespaces, op_name printer_name = _op_attr(op_name, "TScriptPrinterName") assert printer_name is not None, op_name assert printer_name.startswith(device_namespace + "."), op_name assert _has_path(T, printer_name), op_name else: assert category == "builtin" assert device_namespace is None, op_name assert not missing assert not invalid assert not lingering_flat_tile