# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ruff: noqa: E501, F841 import sys import tempfile import numpy as np import pytest import tvm_ffi import tvm import tvm.testing from tvm import relax from tvm.script import ir as I from tvm.script import relax as R from tvm.script import tirx as T exec_mode = tvm.testing.parameter("bytecode", "compiled") @tvm.script.ir_module class InputModule: @R.function def foo(x: R.Tensor(("m", "n"), "int64")): y = R.unique(x, sorted=False) y_sorted = R.unique(x) return y, y_sorted def run_cpu(mod, func_name, *args, exec_mode): if isinstance(mod, relax.Function): func = mod args = [func_name, *args] func_name = func.attrs["global_symbol"] mod = tvm.IRModule.from_expr(func) target = tvm.target.Target("llvm") ex = relax.build(mod, target, exec_mode=exec_mode) vm = relax.VirtualMachine(ex, tvm.cpu()) return vm[func_name](*args) def test_unique(exec_mode): # TODO(prakalp): also add test for compiling and running on CUDA device. data_numpy = np.random.randint(0, 16, (16, 16)) data = tvm.runtime.tensor(data_numpy) result, result_sorted = run_cpu(InputModule, "foo", data, exec_mode=exec_mode) expected_output_sorted, indices = np.unique(data_numpy, return_index=True) expected_output = [data_numpy.flatten()[index] for index in sorted(indices)] np.testing.assert_array_equal(expected_output_sorted, result_sorted.numpy()) np.testing.assert_array_equal(expected_output, result.numpy()) @tvm.script.ir_module class PrintTest: @R.function(pure=False) def foo(x: R.Tensor((), "int32")): # results have to be bound, but we don't use them # TODO: We should allow calls whose results are not bound for side effects; # it would be easy syntactic sugar to add. p1 = R.print(x) p2 = R.print(x, format="Number: {}") t = (x, x) p3 = R.print(t, format="Tuple: {}") p4 = R.print(x, t) p5 = R.print(x, x, format="Custom print: {} {}") p6 = R.print(x, t, format="Another print: {} {}") return x def test_print(exec_mode): try: stdout = sys.stdout with tempfile.TemporaryFile(mode="w+") as test_out: sys.stdout = test_out run_cpu( PrintTest, "foo", tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode, ) test_out.seek(0) printed_text = str(test_out.read()) expected = "1\nNumber: 1\nTuple: (1, 1)\n1 (1, 1)\nCustom print: 1 1\nAnother print: 1 (1, 1)\n" assert printed_text in expected, ("printed_text is ", printed_text) finally: sys.stdout = stdout def test_assert_passes(exec_mode): @R.function(pure=False) def func(x: R.Tensor((), "int32")): _ = R.assert_op(relax.const(True)) return x run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode) def test_assert_passes_with_format_args(exec_mode): @R.function(pure=False) def func(x: R.Tensor((), "int32")): _ = R.assert_op(relax.const(True), x, format="You won't see me") return x run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode) def test_assert_fails(exec_mode): @R.function(pure=False) def func(x: R.Tensor((), "int32")): _ = R.assert_op(relax.const(False)) return x with pytest.raises(AssertionError, match="Assertion Failed"): run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode) def test_assert_fails_with_message(exec_mode): @R.function(pure=False) def func(x: R.Tensor((), "int32")): _ = R.assert_op(relax.const(False), format="I failed...") return x with pytest.raises(AssertionError, match="I failed..."): run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode) def test_assert_fails_with_args(exec_mode): @R.function(pure=False) def func(x: R.Tensor((), "int32")): _ = R.assert_op(relax.const(False), [x, x]) return x with pytest.raises(AssertionError, match="5, 5"): run_cpu(func, tvm.runtime.tensor(np.array(5).astype("int32")), exec_mode=exec_mode) def test_assert_fails_with_formatted_args(exec_mode): @R.function(pure=False) def func(x: R.Tensor((), "int32")): _ = R.assert_op(relax.const(False), x, format="Number: {}") return x with pytest.raises(AssertionError, match="Number: 6"): run_cpu(func, tvm.runtime.tensor(np.array(6).astype("int32")), exec_mode=exec_mode) def test_assert_on_argument_passes(exec_mode): @R.function(pure=False) def func(condition: R.Tensor((), "bool"), x: R.Tensor((), "int32")): _ = R.assert_op(condition) return x condition = tvm.runtime.tensor(np.array(True)) x = tvm.runtime.tensor(np.array(5).astype("int32")) run_cpu(func, condition, x, exec_mode=exec_mode) def test_assert_on_argument_fails(exec_mode): @R.function(pure=False) def func(condition: R.Tensor((), "bool"), x: R.Tensor((), "int32")): _ = R.assert_op(condition) return x condition = tvm.runtime.tensor(np.array(False)) x = tvm.runtime.tensor(np.array(5).astype("int32")) with pytest.raises(AssertionError): run_cpu(func, condition, x, exec_mode=exec_mode) def test_assert_on_symbolic_var_passes(exec_mode): @R.function(pure=False) def func(x: R.Tensor(["N"], "int32")): N = T.int64() _ = R.assert_op(R.prim_value(N % 8 == 0)) return x x = tvm.runtime.tensor(np.arange(8, dtype="int32")) run_cpu(func, x, exec_mode=exec_mode) def test_assert_on_symbolic_var_fails(exec_mode): @R.function(pure=False) def func(x: R.Tensor(["N"], "int32")): N = T.int64() _ = R.assert_op(R.prim_value(N % 8 == 0)) return x x = tvm.runtime.tensor(np.arange(10, dtype="int32")) with pytest.raises(AssertionError): run_cpu(func, x, exec_mode=exec_mode) @tvm.script.ir_module class ShapeOfTest: @R.function def get_shape(t: R.Tensor(ndim=-1, dtype="int32")) -> R.Shape(ndim=-1): return R.shape_of(t) @R.function def get_constrained_shape(t: R.Tensor(ndim=1, dtype="int32")) -> R.Shape(ndim=1): # require the input tensor to have rank 1 return R.shape_of(t) @R.function def get_scalar_shape() -> R.Shape(()): x: R.Tensor((), "int32") = R.const(1, dtype="int32") return R.shape_of(x) @R.function def get_constant_shape() -> R.Shape((2, 2)): x: R.Tensor((2, 2), "int32") = R.const( np.array([[1, 2], [3, 4]], dtype="int32"), dtype="int32" ) return R.shape_of(x) def test_op_shape_of(exec_mode): unit_shape = run_cpu(ShapeOfTest, "get_scalar_shape", exec_mode=exec_mode) assert unit_shape == tvm_ffi.Shape([]) const_shape = run_cpu(ShapeOfTest, "get_constant_shape", exec_mode=exec_mode) assert const_shape == tvm_ffi.Shape([2, 2]) scalar_shape = run_cpu( ShapeOfTest, "get_shape", tvm.runtime.tensor(np.array(1, dtype="int32")), exec_mode=exec_mode, ) assert scalar_shape == tvm_ffi.Shape([]) tensor_shape = run_cpu( ShapeOfTest, "get_shape", tvm.runtime.tensor(np.zeros((1, 2, 3)).astype("int32")), exec_mode=exec_mode, ) assert tensor_shape == tvm_ffi.Shape([1, 2, 3]) constrained_shape = run_cpu( ShapeOfTest, "get_constrained_shape", tvm.runtime.tensor(np.zeros((1,)).astype("int32")), exec_mode=exec_mode, ) assert constrained_shape == tvm_ffi.Shape([1]) @tvm.script.ir_module class ShapeToTensorTest: @R.function def const_shape(shape: R.Shape(ndim=-1)) -> R.Tensor(ndim=-1): return R.shape_to_tensor(shape) @R.function def symbolic_shape(shape: R.Shape(("m", "n"))) -> R.Tensor(ndim=-1): m = T.int64() n = T.int64() return R.shape_to_tensor(shape) def test_op_shape_to_tensor(exec_mode): # Check type isinstance(ShapeToTensorTest["const_shape"].body.ty, tvm.relax.TensorType) assert ShapeToTensorTest["const_shape"].body.ty.ndim == 1 isinstance(ShapeToTensorTest["symbolic_shape"].body.ty, tvm.relax.TensorType) assert ShapeToTensorTest["symbolic_shape"].body.ty.ndim == 1 # Check its functionality out2d = run_cpu(ShapeToTensorTest, "const_shape", tvm_ffi.Shape([3, 2]), exec_mode=exec_mode) assert isinstance(out2d, tvm.runtime.Tensor) assert np.array_equal(out2d.numpy(), np.array([3, 2])) out3d = run_cpu(ShapeToTensorTest, "const_shape", tvm_ffi.Shape([3, 3, 2]), exec_mode=exec_mode) assert isinstance(out3d, tvm.runtime.Tensor) assert np.array_equal(out3d.numpy(), np.array([3, 3, 2])) out4d = run_cpu( ShapeToTensorTest, "const_shape", tvm_ffi.Shape([3, 3, 2, 2]), exec_mode=exec_mode ) assert isinstance(out4d, tvm.runtime.Tensor) assert np.array_equal(out4d.numpy(), np.array([3, 3, 2, 2])) outs = run_cpu(ShapeToTensorTest, "symbolic_shape", tvm_ffi.Shape([3, 2]), exec_mode=exec_mode) assert isinstance(outs, tvm.runtime.Tensor) assert np.array_equal(outs.numpy(), np.array([3, 2])) def test_op_call_pure_packed(exec_mode): @tvm.script.ir_module class CallPureTest: @R.function def pure_copy(x: R.Tensor((3, 4), "float32")): z = R.call_pure_packed( "vm.builtin.copy", x, ty_args=(R.Tensor((3, 4), dtype="float32")) ) return z np.random.seed(0) # to avoid flakiness arr = np.random.rand(3, 4).astype("float32") copy_found = run_cpu(CallPureTest, "pure_copy", tvm.runtime.tensor(arr), exec_mode=exec_mode) assert (copy_found.numpy() == arr).all() def test_op_call_inplace_packed(exec_mode): # in this case we can use the same test as above @tvm.script.ir_module class CallInplaceTest: @R.function def pure_copy(x: R.Tensor((3, 4), "float32")): z = R.call_inplace_packed( "vm.builtin.copy", x, inplace_indices=0, ty_args=(R.Tensor((3, 4), dtype="float32")), ) return z @tvm.register_global_func("test.inplace.add", override=True) def inplace_add(a, b): arr_a = a.numpy() arr_b = b.numpy() for i in range(len(arr_a)): for j in range(len(arr_a[i])): arr_a[i][j] = arr_a[i][j] + arr_b[i][j] a.copyfrom(arr_a) return a @tvm.script.ir_module class CallInplaceAddTest: @R.function def inplace_add(x: R.Tensor((3, 4), "float32"), y: R.Tensor((3, 4), "float32")): z = R.call_inplace_packed( "test.inplace.add", x, y, inplace_indices=0, ty_args=(R.Tensor((3, 4), dtype="float32")), ) return z np.random.seed(1) # to avoid flakiness arr_a = np.random.rand(3, 4).astype("float32") arr_b = np.random.rand(3, 4).astype("float32") sum = arr_a + arr_b tvm_arr_a = tvm.runtime.tensor(arr_a) result = run_cpu( CallInplaceAddTest, "inplace_add", tvm_arr_a, tvm.runtime.tensor(arr_b), exec_mode=exec_mode ) assert result == tvm_arr_a assert (result.numpy() == sum).all() @tvm.register_global_func("test.inplace.tuple_add", override=True) def inplace_tuple_add(a, b): arr_a = a.numpy() arr_b = b.numpy() c = tvm.runtime.tensor(arr_a + arr_b) for i in range(len(arr_a)): for j in range(len(arr_a[i])): arr_a[i][j] = arr_a[i][j] + arr_b[i][j] a.copyfrom(arr_a) return tvm.runtime.convert([a, c]) @tvm.script.ir_module class CallInplaceTuple: @R.function def inplace_tuple(x: R.Tensor((3, 4), "float32"), y: R.Tensor((3, 4), "float32")): z = R.call_inplace_packed( "test.inplace.tuple_add", x, y, inplace_indices=[0, -1], ty_args=(R.Tensor((3, 4), dtype="float32"), R.Tensor((3, 4), dtype="float32")), ) return z np.random.seed(2) # to avoid flakiness arr_a = np.random.rand(3, 4).astype("float32") arr_b = np.random.rand(3, 4).astype("float32") sum = arr_a + arr_b tvm_arr_a = tvm.runtime.tensor(arr_a) tvm_arr_b = tvm.runtime.tensor(arr_b) result = run_cpu(CallInplaceTuple, "inplace_tuple", tvm_arr_a, tvm_arr_b, exec_mode=exec_mode) assert result[0] == tvm_arr_a assert (result[0].numpy() == sum).all() assert result[1] != tvm_arr_a and result[1] != tvm_arr_b assert (result[1].numpy() == sum).all() def test_op_call_py_func(exec_mode): """Test R.call_py_func operator functionality.""" import torch def torch_relu(x): if isinstance(x, tvm.runtime.Tensor): x_torch = torch.from_numpy(x.numpy()) elif hasattr(x, "asnumpy"): x_torch = torch.from_numpy(x.asnumpy()) else: x_np = np.array(x) if isinstance(x_np, tvm.runtime.Tensor): x_torch = torch.from_numpy(x_np.numpy()) elif len(x_np) > 0 and isinstance(x_np[0], tvm.runtime.Tensor): x_torch = torch.from_numpy(np.array([t.numpy() for t in x_np])) if x_torch.ndim > 1: x_torch = x_torch.flatten() else: x_torch = torch.from_numpy(x_np) result = torch.relu(x_torch) return tvm.runtime.tensor(result.numpy()) def torch_sigmoid(x): if isinstance(x, tvm.runtime.Tensor): x_torch = torch.from_numpy(x.numpy()) elif hasattr(x, "asnumpy"): x_torch = torch.from_numpy(x.asnumpy()) else: x_np = np.array(x) if isinstance(x_np, tvm.runtime.Tensor): x_torch = torch.from_numpy(x_np.numpy()) elif len(x_np) > 0 and isinstance(x_np[0], tvm.runtime.Tensor): x_torch = torch.from_numpy(np.array([t.numpy() for t in x_np])) if x_torch.ndim > 1: x_torch = x_torch.flatten() else: x_torch = torch.from_numpy(x_np) result = torch.sigmoid(x_torch) return tvm.runtime.tensor(result.numpy()) register_func = tvm.get_global_func("vm.builtin.register_py_func") register_func("torch_relu", torch_relu) register_func("torch_sigmoid", torch_sigmoid) @tvm.script.ir_module class CallPyFuncTest: @R.function def simple_call(x: R.Tensor((3,), "float32")): result = R.call_py_func(R.str("torch_relu"), (x,), out_ty=R.Tensor((3,), "float32")) return result @R.function def multiple_calls(x: R.Tensor((2,), "float32")): y = R.call_py_func(R.str("torch_relu"), (x,), out_ty=R.Tensor((2,), "float32")) z = R.call_py_func(R.str("torch_sigmoid"), (y,), out_ty=R.Tensor((2,), "float32")) return z np.random.seed(0) x_data = np.array([-1.0, 0.0, 1.0], dtype=np.float32) x_tvm = tvm.runtime.tensor(x_data) result = run_cpu(CallPyFuncTest, "simple_call", x_tvm, exec_mode=exec_mode) expected = np.maximum(x_data, 0.0) assert (result.numpy() == expected).all() y_data = np.array([-0.5, 0.5], dtype=np.float32) y_tvm = tvm.runtime.tensor(y_data) result2 = run_cpu(CallPyFuncTest, "multiple_calls", y_tvm, exec_mode=exec_mode) expected2 = 1.0 / (1.0 + np.exp(-np.maximum(y_data, 0.0))) assert (result2.numpy() == expected2).all() clear_func = tvm.get_global_func("vm.builtin.clear_py_func_registry") clear_func() def test_op_to_device(exec_mode): @tvm.script.ir_module class CallToDevice: @R.function def to_dev(x: R.Tensor((3, 4), "float32")): z = R.call_pure_packed( "vm.builtin.to_device", x, 1, 0, ty_args=(R.Tensor((3, 4), dtype="float32")), ) return z np.random.seed(0) # to avoid flakiness arr = np.random.rand(3, 4).astype("float32") copy_found = run_cpu(CallToDevice, "to_dev", tvm.runtime.tensor(arr), exec_mode=exec_mode) assert (copy_found.numpy() == arr).all() def test_op_to_vdevice(exec_mode): @tvm.script.ir_module class ToVDevice: I.module_global_infos({"vdevice": [I.vdevice("llvm")]}) @R.function def to_vdev(x: R.Tensor((3, 4), "float32")): dst_vdev = tvm.ir.VDevice("llvm", 0, "global") ret = R.to_vdevice(x, "llvm") return ret np.random.seed(0) arr = np.random.rand(3, 4).astype("float32") copy_found = run_cpu(ToVDevice, "to_vdev", tvm.runtime.tensor(arr), exec_mode=exec_mode) assert (copy_found.numpy() == arr).all() def test_scalar_tensor_as_branch_condition(exec_mode): """The condition of a branch may be a scalar tensor""" @R.function def func(condition: R.Tensor((), "bool")): if condition: out = R.prim_value(5) else: out = R.prim_value(10) return out res = run_cpu(func, tvm.runtime.tensor(np.array(True)), exec_mode=exec_mode) assert res == 5 res = run_cpu(func, tvm.runtime.tensor(np.array(False)), exec_mode=exec_mode) assert res == 10 def test_prim_value_as_branch_condition(exec_mode): """The condition may be a Expr""" @R.function def func(condition: R.Prim("bool")): if condition: out = R.prim_value(5) else: out = R.prim_value(10) return out res = run_cpu(func, True, exec_mode=exec_mode) assert res == 5 res = run_cpu(func, False, exec_mode=exec_mode) assert res == 10 def test_computed_prim_value_as_branch_condition(exec_mode): """The R.Prim condition may be computed within the function""" @R.function def func(x: R.Tensor(["N"], "int64")): N = T.int64() if R.prim_value(N % 16 == 0): out = R.prim_value(5) else: out = R.prim_value(10) return out res = run_cpu(func, tvm.runtime.tensor(np.arange(16)), exec_mode=exec_mode) assert res == 5 res = run_cpu(func, tvm.runtime.tensor(np.arange(20)), exec_mode=exec_mode) assert res == 10 if __name__ == "__main__": tvm.testing.main()