# 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. """Unittests for tvm.script.parser.tirx""" import pytest import tvm_ffi import tvm.testing from tvm import ir, tirx from tvm.script.parser import tirx as T def test_tir_buffer_proxy(): buffer_0 = T.Buffer((128, 128), "float32") assert ( isinstance(buffer_0, tirx.Buffer) and list(buffer_0.shape) == [128, 128] and buffer_0.dtype == ir.PrimType("float32") ) buffer_1 = T.Buffer((64, 64, 64), "int32") assert ( isinstance(buffer_1, tirx.Buffer) and list(buffer_1.shape) == [64, 64, 64] and buffer_1.dtype == ir.PrimType("int32") ) def test_tir_ptr_proxy(): ptr_0 = T.handle("int32", "global") assert ( isinstance(ptr_0, tirx.Var) and isinstance(ptr_0.ty, ir.PointerType) and ptr_0.ty.element_type == ir.PrimType("int32") and ptr_0.ty.storage_scope == "global" ) ptr_1 = T.handle("float32", "shared") assert ( isinstance(ptr_1, tirx.Var) and isinstance(ptr_1.ty, ir.PointerType) and ptr_1.ty.element_type == ir.PrimType("float32") and ptr_1.ty.storage_scope == "shared" ) def test_tir_func_name(): @T.prim_func(s_tir=True) def matmul(a: T.handle, b: T.handle, c: T.handle) -> None: A = T.match_buffer(a, [128, 128]) B = T.match_buffer(b, [128, 128]) C = T.match_buffer(c, [128, 128]) for i, j, k in T.grid(128, 128, 128): with T.sblock("update"): vi, vj, vk = T.axis.remap("SSR", [i, j, k]) C[vi, vj] = C[vi, vj] + A[vi, vk] * B[vj, vk] assert matmul.__name__ == "matmul" assert matmul.attrs["global_symbol"] == "matmul" def test_tir_func_private_attrs(): @T.prim_func(private=True, s_tir=True) def matmul(a: T.handle, b: T.handle, c: T.handle) -> None: T.func_attr({"attr": "value"}) A = T.match_buffer(a, [128, 128]) B = T.match_buffer(b, [128, 128]) C = T.match_buffer(c, [128, 128]) for i, j, k in T.grid(128, 128, 128): with T.sblock("update"): vi, vj, vk = T.axis.remap("SSR", [i, j, k]) C[vi, vj] = C[vi, vj] + A[vi, vk] * B[vj, vk] assert "global_symbol" not in matmul.attrs def test_tir_func_private_manual_global_symbol_fail(): with pytest.raises(tvm.error.DiagnosticError): @T.prim_func(private=True, s_tir=True) def matmul(a: T.handle, b: T.handle, c: T.handle) -> None: T.func_attr({"global_symbol": "matmul"}) A = T.match_buffer(a, [128, 128]) B = T.match_buffer(b, [128, 128]) C = T.match_buffer(c, [128, 128]) for i, j, k in T.grid(128, 128, 128): with T.sblock("update"): vi, vj, vk = T.axis.remap("SSR", [i, j, k]) C[vi, vj] = C[vi, vj] + A[vi, vk] * B[vj, vk] # should not execute assert matmul.__name__ == "matmul" def test_tir_macro_decorator_signature(): @T.prim_func(private=True, s_tir=True) def evaluate0(): T.evaluate(0) # Ok, no parentheses @T.inline def func1(): T.evaluate(0) @T.prim_func(private=True, s_tir=True) def use1(): func1() tvm.ir.assert_structural_equal(use1, evaluate0) # Ok, empty parentheses @T.inline() def func2(): T.evaluate(0) @T.prim_func(private=True, s_tir=True) def use2(): func2() tvm.ir.assert_structural_equal(use1, evaluate0) with pytest.raises(ValueError): # Wrong: non-keyword argument @T.inline(True) def func3(): T.evaluate() def test_tir_macro_signature(): @T.inline def assign(i, *args, t1, **kwargs): vi, vj, vk = T.axis.remap("SSR", [i, args[0], args[1]]) kwargs["t3"][vi, vj] = kwargs["t3"][vi, vj] + t1[vi, vk] * kwargs["t2"][vj, vk] @T.prim_func(private=True, s_tir=True) def matmul_w_macro(a: T.handle, b: T.handle, c: T.handle) -> None: A = T.match_buffer(a, [128, 128]) B = T.match_buffer(b, [128, 128]) C = T.match_buffer(c, [128, 128]) for i, j, k in T.grid(128, 128, 128): with T.sblock("update"): assign(i, j, k, t1=A, t2=B, t3=C) @T.prim_func(private=True, s_tir=True) def matmul_no_macro(a: T.handle, b: T.handle, c: T.handle) -> None: A = T.match_buffer(a, [128, 128]) B = T.match_buffer(b, [128, 128]) C = T.match_buffer(c, [128, 128]) for i, j, k in T.grid(128, 128, 128): with T.sblock("update"): vi, vj, vk = T.axis.remap("SSR", [i, j, k]) C[vi, vj] = C[vi, vj] + A[vi, vk] * B[vj, vk] tvm.ir.assert_structural_equal(matmul_no_macro, matmul_w_macro) def test_tir_macro_hygienic(): x_value = 128 @T.inline def static_capture(A, B): B[()] = A[x_value] @T.prim_func(private=True, s_tir=True) def use_hygienic(A: T.Buffer((1024,), "int32"), B: T.Buffer((), "int32")) -> None: for x_value in T.serial(10): static_capture(A, B) @T.prim_func(private=True, s_tir=True) def expected_hygienic(A: T.Buffer((1024,), "int32"), B: T.Buffer((), "int32")) -> None: for x_value in range(10): B[()] = A[128] tvm.ir.assert_structural_equal(use_hygienic, expected_hygienic) def test_tir_inline_late_binding(): """Inline defined inside prim_func uses LEGB late binding: it sees the current value of variables from its enclosing scope at call time.""" @T.prim_func(private=True, s_tir=True) def use_late_binding(A: T.Buffer((1024,), "int32"), B: T.Buffer((), "int32")) -> None: for x_value in T.serial(10): @T.inline def capture(A, B): B[()] = A[x_value] capture(A, B) @T.prim_func(private=True, s_tir=True) def expected(A: T.Buffer((1024,), "int32"), B: T.Buffer((), "int32")) -> None: for x_value in range(10): B[()] = A[x_value] tvm.ir.assert_structural_equal(use_late_binding, expected) def test_tir_macro_in_class(): class Object: def __init__(self, x: T.Buffer): self.local_x = T.sblock_alloc_buffer(x.shape, x.dtype) @T.inline def load(self, x: T.Buffer): N, M = T.meta_var(self.local_x.shape) for i, j in T.grid(N, M): with T.sblock("update"): vi, vj = T.axis.remap("SS", [i, j]) self.local_x[vi, vj] = x[vi, vj] @T.prim_func(private=True, s_tir=True) def func_w_macro(a: T.handle): A = T.match_buffer(a, [128, 128]) o1 = T.meta_var(Object(A)) o1.load(A) o2 = T.meta_var(Object(A)) o2.load(o1.local_x) @T.prim_func(private=True, s_tir=True) def func_no_macro(a: T.handle): A = T.match_buffer(a, [128, 128]) local_a = T.sblock_alloc_buffer([128, 128]) for i, j in T.grid(128, 128): with T.sblock("update"): vi, vj = T.axis.remap("SS", [i, j]) local_a[vi, vj] = A[vi, vj] local_b = T.sblock_alloc_buffer([128, 128]) for i, j in T.grid(128, 128): with T.sblock("update"): vi, vj = T.axis.remap("SS", [i, j]) local_b[vi, vj] = local_a[vi, vj] tvm.ir.assert_structural_equal(func_no_macro, func_w_macro) def test_tir_starred_expression(): dims = (128, 128) @T.prim_func(private=True, s_tir=True) def starred(a: T.handle) -> None: A = T.match_buffer(a, [128, *dims], "int32") for i, j, k in T.grid(128, *dims): A[i, j, k] = T.int32(1) @T.prim_func(private=True, s_tir=True) def non_starred(a: T.handle) -> None: A = T.match_buffer(a, [128, 128, 128], "int32") for i, j, k in T.grid(128, 128, 128): A[i, j, k] = T.int32(1) tvm.ir.assert_structural_equal(starred, non_starred) def test_tir_starred_shape_expression(): dims = (128, 128) @T.prim_func(private=True, s_tir=True) def starred(a: T.handle) -> None: A = T.match_buffer(a, [128, *dims], "int32") for i, j, k in T.grid(*A.shape): A[i, j, k] = T.int32(1) @T.prim_func(private=True, s_tir=True) def non_starred(a: T.handle) -> None: A = T.match_buffer(a, [128, 128, 128], "int32") for i, j, k in T.grid(128, 128, 128): A[i, j, k] = T.int32(1) tvm.ir.assert_structural_equal(starred, non_starred) def test_tir_dynamic_for_loop(): dims = (128, 128) @T.prim_func(private=True, s_tir=True) def starred(a: T.handle) -> None: A = T.match_buffer(a, [128, *dims], "int32") for iters in T.grid(*A.shape): A[iters] = T.int32(1) @T.prim_func(private=True, s_tir=True) def non_starred(a: T.handle) -> None: A = T.match_buffer(a, [128, 128, 128], "int32") for i, j, k in T.grid(128, 128, 128): A[i, j, k] = T.int32(1) tvm.ir.assert_structural_equal(starred, non_starred) def test_tir_starred_for_loop(): dims = (128, 128) @T.prim_func(private=True, s_tir=True) def starred(a: T.handle, b: T.handle): A = T.match_buffer(a, [*dims, 128], "int32") B = T.match_buffer(b, dims, "int32") for *spatial, reduction in T.grid(*A.shape): with T.sblock("reduce"): with T.init(): B[spatial] = T.int32(0) B[spatial] = B[spatial] + A[(*spatial, reduction)] @T.prim_func(private=True, s_tir=True) def non_starred(a: T.handle, b: T.handle): A = T.match_buffer(a, [128, 128, 128], "int32") B = T.match_buffer(b, [128, 128], "int32") for i, j, k in T.grid(128, 128, 128): with T.sblock("reduce"): with T.init(): B[i, j] = T.int32(0) B[i, j] = B[i, j] + A[i, j, k] tvm.ir.assert_structural_equal(starred, non_starred) def test_tir_loop_steps(): N = T.Var("N", "int32") @T.prim_func(private=True, s_tir=True) def loop_with_steps( A: T.Buffer((N,)), B: T.Buffer((N,)), C: T.Buffer((N,)), tid: T.int32, v: T.int32 ): for i in T.serial(tid, N, step=2): C[i] = A[i] + B[i] for i in T.unroll(tid, N, step=3): C[i] = A[i] + B[i] for i in T.vectorized(tid, N, step=4): C[i] = A[i] + B[i] for i in T.parallel(tid, N, step=5): C[i] = A[i] + B[i] for i in T.serial(tid, N, step=v): C[i] = A[i] + B[i] stmts = loop_with_steps.body.seq assert stmts[0].step == 2 assert stmts[1].step == 3 assert stmts[2].step == 4 assert stmts[3].step == 5 assert stmts[4].step.name == "v" def test_tir_empty_tuple_index(): @T.inline def bar(val): T.evaluate(val) @T.prim_func(private=True, s_tir=True) def func_with_empty_tuple(A: T.Buffer((), "int32"), B: T.Buffer((), "int32")): bar(val=A[()]) @T.prim_func(private=True, s_tir=True) def expected(A: T.Buffer((), "int32"), B: T.Buffer((), "int32")): T.evaluate(A[()]) tvm.ir.assert_structural_equal(func_with_empty_tuple, expected) def test_tir_builtin_expression(): dims = (128, 128) @T.prim_func(private=True, s_tir=True) def with_builtin(a: T.handle) -> None: A = T.match_buffer(a, [len(dims), *dims], "int32") for i, j, k in T.grid(*A.shape): A[i, j, k] = T.int32(1 + len(A.shape)) @T.prim_func(private=True, s_tir=True) def evaluated(A: T.Buffer((2, 128, 128), "int32")): for i, j, k in T.grid(2, 128, 128): A[i, j, k] = 4 tvm.ir.assert_structural_equal(with_builtin, evaluated) def test_thread_binding_dtype(): @T.prim_func(private=True, s_tir=True) def func(A: T.Buffer((128, 128)), B: T.Buffer((128, 128))): for i in T.thread_binding(T.int64(128), "threadIdx.x"): for j in T.thread_binding(128, "threadIdx.y"): B[i, j] = A[i, j] loop_i = func.body loop_j = loop_i.body assert loop_i.loop_var.ty.dtype == "int64" assert loop_i.thread_binding.var.ty.dtype == "int64" assert loop_j.loop_var.ty.dtype == "int32" assert loop_j.thread_binding.var.ty.dtype == "int32" def test_inferred_ty_with_prim_args(): """A PrimFunc may have inferred Type""" @T.prim_func(s_tir=True) def func(M: T.int32, N: T.int32) -> T.int32: T.ret(M * N) expected = tvm.relax.FuncType( [ tvm.ir.PrimType("int32"), tvm.ir.PrimType("int32"), ], tvm.ir.PrimType("int32"), purity=True, ) tvm.ir.assert_structural_equal(func.ty, expected) def test_inferred_ty_with_buffer_args(): """PrimFunc buffer arguments are inferred as R.Tensor""" @T.prim_func(s_tir=True) def func(A: T.Buffer([16, 16], "float32"), B: T.Buffer([256], "int32")) -> T.float32: T.ret(T.float32(42.0)) expected = tvm.relax.FuncType( [ tvm.relax.TensorType([16, 16], "float32"), tvm.relax.TensorType([256], "int32"), ], tvm.ir.PrimType("float32"), purity=True, ) tvm.ir.assert_structural_equal(func.ty, expected) def test_inferred_ty_with_internal_allocation(): """A pure function may still write to internal allocations. Whether a function writes to internal allocations is not a visible effect, and does not impact the purity of a function. """ @T.prim_func(s_tir=True) def func(A: T.Buffer([16, 16], "float32")) -> T.float32: Sum = T.decl_buffer([], "float32") Sum[()] = 0.0 for i, j in T.grid(16, 16): Sum[()] = Sum[()] + A[i, j] T.ret(Sum[()]) expected = tvm.relax.FuncType( [ tvm.relax.TensorType([16, 16], "float32"), ], tvm.ir.PrimType("float32"), purity=True, ) tvm.ir.assert_structural_equal(func.ty, expected) def test_inferred_ty_with_output_buffer(): """A pure function may not write to an argument buffer If an argument buffer is written to, the function must be impure. """ @T.prim_func(s_tir=True) def func(A: T.Buffer(16, "float32"), B: T.Buffer(16, "float32")): for i in range(16): B[i] = A[i] expected = tvm.relax.FuncType( [ tvm.relax.TensorType([16], "float32"), tvm.relax.TensorType([16], "float32"), ], tvm.relax.TupleType([]), purity=False, ) tvm.ir.assert_structural_equal(func.ty, expected) def test_inferred_ty_with_dynamic_buffer(): """The inferred Type may contain dynamic shapes""" @T.prim_func(s_tir=True) def func(a_handle: T.handle, b_handle: T.handle): M = T.int64() N = T.int64() A = T.match_buffer(a_handle, [M, N], "float32") B = T.match_buffer(b_handle, [M * N], "float32") for i, j in T.grid(M, N): B[i * N + j] = A[i, j] M = tvm.tirx.Var("M", "int64") N = tvm.tirx.Var("N", "int64") expected = tvm.relax.FuncType( [ tvm.relax.TensorType([M, N], "float32"), tvm.relax.TensorType([M * N], "float32"), ], tvm.relax.TupleType([]), purity=False, ) tvm.ir.assert_structural_equal(func.ty, expected) def test_reinterpret_nop(): """Test builtin reinterpret op""" @T.prim_func(s_tir=True) def func(A: T.Buffer((32,), "float32"), B: T.Buffer((32,), "float32")) -> None: T.func_attr({"global_symbol": "main"}) for i in T.serial(0, 32): with T.sblock(): vi = T.axis.remap("S", [i]) B[vi] = T.reinterpret("float32", A[vi]) @T.prim_func(s_tir=True) def expected(A: T.Buffer((32,), "float32"), B: T.Buffer((32,), "float32")) -> None: T.func_attr({"global_symbol": "main"}) for i in T.serial(0, 32): with T.sblock(): vi = T.axis.remap("S", [i]) B[vi] = A[vi] tvm.ir.assert_structural_equal(func, expected) def test_launch_thread_i64(): """Test launching thread with int64""" @T.prim_func(s_tir=True) def func() -> None: blockIdx_x = T.launch_thread("blockIdx.x", T.int64(1)) if blockIdx_x == T.int64(0): T.evaluate(T.int64(0)) else: T.evaluate(T.int64(1)) assert func.body.node.dom.min.ty.dtype == "int64" assert func.body.node.dom.extent.ty.dtype == "int64" def test_deterministic_branch(): """Test deterministic branch""" def create_func(predicate: bool): @T.prim_func(private=True, s_tir=True) def func() -> None: if predicate: T.evaluate(0) else: T.evaluate(1) return func def create_expected(value): @T.prim_func(private=True, s_tir=True) def expected() -> None: T.evaluate(value) return expected tvm.ir.assert_structural_equal(create_func(True), create_expected(0)) tvm.ir.assert_structural_equal(create_func(False), create_expected(1)) def test_block_annotation_merge(): def _to_dict(anno: tvm_ffi.container.Map): result = {} for k, v in anno.items(): result[k] = _to_dict(v) if isinstance(v, tvm_ffi.container.Map) else v return result @T.prim_func(s_tir=True) def func0(): with T.sblock(): T.sblock_attr({"key1": "block1"}) T.sblock_attr({"key2": "block2"}) T.evaluate(0) assert _to_dict(func0.body.block.annotations) == {"key1": "block1", "key2": "block2"} @T.prim_func(s_tir=True) def func1(): with T.sblock(): T.sblock_attr({"key": {"key1": "block1"}}) T.sblock_attr({"key": {"key2": "block2"}}) T.evaluate(0) assert _to_dict(func1.body.block.annotations) == {"key": {"key1": "block1", "key2": "block2"}} @T.prim_func(s_tir=True) def func2(): with T.sblock(): T.sblock_attr({"key1": "block1"}) T.sblock_attr({"key1": "block1"}) T.evaluate(0) assert _to_dict(func2.body.block.annotations) == {"key1": "block1"} with pytest.raises(RuntimeError): @T.prim_func(s_tir=True) def func3(): with T.sblock(): T.sblock_attr({"key1": "block1"}) T.sblock_attr({"key1": "block2"}) T.evaluate(0) def test_alloc_inside_block(): @T.prim_func(private=True, s_tir=True) def func() -> None: with T.sblock(): A = T.sblock_alloc_buffer([10], "float32") for i in T.serial(0, 10): B = T.sblock_alloc_buffer([10], "float32") for j in T.serial(0, 10): B[j] = T.float32(j) A[i] += B[j] @T.prim_func(private=True, s_tir=True) def expected() -> None: with T.sblock(): A = T.sblock_alloc_buffer([10], "float32") B = T.sblock_alloc_buffer([10], "float32") for i, j in T.grid(10, 10): B[j] = T.float32(j) A[i] += B[j] tvm.ir.assert_structural_equal(func, expected) def test_tir_macro_block_name_suffix(): @T.inline def operation(A, idx): with T.sblock("op"): v = T.axis.remap("S", [idx]) A[v] = A[v] * T.float32(2) @T.prim_func(private=True, s_tir=True) def func_w_macro(a: T.handle) -> None: A = T.match_buffer(a, [10]) for i in T.serial(0, 10): operation(A, i) operation(A, i) operation(A, i) @T.prim_func(private=True, s_tir=True) def expected(a: T.handle) -> None: A = T.match_buffer(a, [10]) for i in T.serial(0, 10): with T.sblock("op"): v = T.axis.remap("S", [i]) A[v] = A[v] * T.float32(2) with T.sblock("op_1"): v = T.axis.remap("S", [i]) A[v] = A[v] * T.float32(2) with T.sblock("op_2"): v = T.axis.remap("S", [i]) A[v] = A[v] * T.float32(2) tvm.ir.assert_structural_equal(func_w_macro, expected) def test_ifexp(): @T.prim_func(private=True, s_tir=True) def func(A: T.buffer((128, 128), "float32")): for i, j in T.grid(128, 128): A[i, j] = i if i < j else j @T.prim_func(private=True, s_tir=True) def expected(A: T.buffer((128, 128), "float32")): for i, j in T.grid(128, 128): A[i, j] = T.if_then_else(i < j, i, j) tvm.ir.assert_structural_equal(func, expected) def test_sequence_compare(): @T.prim_func(private=True, s_tir=True) def tir_func(A: T.Buffer((128, 128), "float32")): for i, j in T.grid(128, 128): if 0 < i < 128 and 0 < j < 128: A[i, j] = 1 else: A[i, j] = 0 @T.prim_func(private=True, s_tir=True) def expected(A: T.buffer((128, 128), "float32")): for i, j in T.grid(128, 128): if (0 < i and i < 128) and (0 < j and j < 128): A[i, j] = 1 else: A[i, j] = 0 tvm.ir.assert_structural_equal(tir_func, expected) if __name__ == "__main__": tvm.testing.main()