# 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 import pytest import tvm import tvm.testing from tvm import relax from tvm.relax.transform import LegalizeOps from tvm.relax.transform.legalize_ops.common import register_legalize from tvm.script import ir as I from tvm.script import relax as R from tvm.script import tirx as T def test_customize_legalize(): # fmt: off @tvm.script.ir_module class Add: @R.function def main(x: R.Tensor((1, 2, 3), "float32"), y: R.Tensor((4, 3, 2, 1), "float32")) -> R.Tensor((4, 3, 2, 3), "float32"): gv: R.Tensor((4, 3, 2, 3), "float32") = R.add(x, y) return gv @tvm.script.ir_module class Expected: @R.function def main(x: R.Tensor((1, 2, 3), "float32"), y: R.Tensor((4, 3, 2, 1), "float32")) -> R.Tensor((4, 3, 2, 3), "float32"): cls = Expected gv = R.call_tir(cls.add, (y, x), R.Tensor((4, 3, 2, 3), dtype="float32")) return gv @T.prim_func(private=True, s_tir=True) def add(rxplaceholder_1: T.Buffer((T.int64(4), T.int64(3), T.int64(2), T.int64(1)), "float32"), rxplaceholder: T.Buffer((T.int64(1), T.int64(2), T.int64(3)), "float32"), T_add: T.Buffer((T.int64(4), T.int64(3), T.int64(2), T.int64(3)), "float32")): T.func_attr({"tirx.noalias": True}) for i0, i1, i2, i3 in T.grid(T.int64(4), T.int64(3), T.int64(2), T.int64(3)): with T.sblock("T_add"): ax0, ax1, ax2, ax3 = T.axis.remap("SSSS", [i0, i1, i2, i3]) T.reads(rxplaceholder_1[ax0, ax1, ax2, T.int64(0)], rxplaceholder[T.int64(0), ax2, ax3]) T.writes(T_add[ax0, ax1, ax2, ax3]) T_add[ax0, ax1, ax2, ax3] = rxplaceholder_1[ax0, ax1, ax2, T.int64(0)] + rxplaceholder[T.int64(0), ax2, ax3] # fmt: on def customize_legalize_add(bb: relax.BlockBuilder, call: relax.Call): from tvm import topi # pylint: disable=import-outside-toplevel return bb.call_te(topi.add, call.args[1], call.args[0]) mod = LegalizeOps({"relax.add": customize_legalize_add})(Add) tvm.ir.assert_structural_equal(mod, Expected) def test_legalize_multiple_types_of_call(): # fmt: off @tvm.script.ir_module class Before: @R.function def mul2(x: R.Tensor((3, 3), "float32")): gv = R.multiply(x, R.const(2.0, "float32")) return gv @T.prim_func(private=True, s_tir=True) def identity(rxplaceholder: T.Buffer((T.int64(3), T.int64(3)), "float32"), T_id: T.Buffer((T.int64(3), T.int64(3)), "float32")): for ax0, ax1 in T.grid(T.int64(3), T.int64(3)): with T.sblock("T_add"): v_ax0, v_ax1 = T.axis.remap("SS", [ax0, ax1]) T.reads(rxplaceholder[v_ax0, v_ax1]) T.writes(T_id[v_ax0, v_ax1]) T_id[v_ax0, v_ax1] = rxplaceholder[v_ax0, v_ax1] @R.function def main(x: R.Tensor((3, 3), "float32")): cls = Before gv: R.Tensor((3, 3), "float32") = cls.mul2(x) gv1 = R.call_tir(cls.identity, gv, R.Tensor((3, 3), dtype="float32")) gv2 = R.multiply(gv1, R.const(2.0, "float32")) return gv2 @tvm.script.ir_module class Expected: @R.function def mul2(x: R.Tensor((3, 3), dtype="float32")) -> R.Tensor((3, 3), dtype="float32"): cls = Expected gv = R.call_tir(cls.multiply, (x,), R.Tensor((3, 3), dtype="float32")) return gv @T.prim_func(private=True, s_tir=True) def identity(rxplaceholder: T.Buffer((T.int64(3), T.int64(3)), "float32"), T_id: T.Buffer((T.int64(3), T.int64(3)), "float32")): for ax0, ax1 in T.grid(T.int64(3), T.int64(3)): with T.sblock("T_add"): v_ax0, v_ax1 = T.axis.remap("SS", [ax0, ax1]) T.reads(rxplaceholder[v_ax0, v_ax1]) T.writes(T_id[v_ax0, v_ax1]) T_id[v_ax0, v_ax1] = rxplaceholder[v_ax0, v_ax1] @T.prim_func(private=True, s_tir=True) def multiply(rxplaceholder: T.Buffer((T.int64(3), T.int64(3)), "float32"), T_multiply: T.Buffer((T.int64(3), T.int64(3)), "float32")): T.func_attr({"tirx.noalias": True}) for ax0, ax1 in T.grid(T.int64(3), T.int64(3)): with T.sblock("T_multiply"): v_ax0, v_ax1 = T.axis.remap("SS", [ax0, ax1]) T.reads(rxplaceholder[v_ax0, v_ax1]) T.writes(T_multiply[v_ax0, v_ax1]) T_multiply[v_ax0, v_ax1] = rxplaceholder[v_ax0, v_ax1] * T.float32(2) @R.function def main(x1: R.Tensor((3, 3), dtype="float32")) -> R.Tensor((3, 3), dtype="float32"): cls = Expected gv1: R.Tensor((3, 3), dtype="float32") = cls.mul2(x1) gv11 = R.call_tir(cls.identity, gv1, R.Tensor((3, 3), dtype="float32")) gv2 = R.call_tir(cls.multiply, (gv11,), R.Tensor((3, 3), dtype="float32")) return gv2 # fmt: on After = LegalizeOps()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_can_not_legalize(): # case 1: does't have legalization add_legalize = tvm.ir.Op.get("relax.add").get_attr("FLegalize") # reset it for test tvm.ir.Op.get("relax.add").reset_attr("FLegalize") # fmt: off @tvm.script.ir_module class Before0: @R.function def main(x: R.Tensor((3, 3), "float32")): gv: R.Tensor((3, 3), "float32") = R.add(x, x) return gv # fmt: on After0 = LegalizeOps()(Before0) tvm.ir.assert_structural_equal(After0, Before0) register_legalize("relax.add", add_legalize) # case 2: don't know all shape s = relax.Var("s", relax.ShapeType((3, 3))) x = relax.Var("x", relax.TensorType((3, 3), "float32")) y = relax.Var("y", relax.TensorType(s, "float32")) bb = relax.BlockBuilder() with bb.function("main", [x, y]): with bb.dataflow(): gv = bb.emit_output(R.add(x, y)) bb.emit_func_output(gv) Before1 = bb.get() After1 = LegalizeOps()(Before1) tvm.ir.assert_structural_equal(After1, Before1) def test_legalize_scalar_data_type_preserve(): # fmt: off @tvm.script.ir_module class Before0: @R.function def main(x: R.Tensor((3, 3), "float16")): gv: R.Tensor((3, 3), "float16") = R.multiply(x, R.const(1.14514, "float16")) return gv @tvm.script.ir_module class Before1: @R.function def main(x: R.Tensor((3, 3), "uint8")): gv: R.Tensor((3, 3), "uint8") = R.multiply(x, R.const(2, "uint8")) return gv @tvm.script.ir_module class Before2: @R.function def main(x: R.Tensor((3, 3), "bool")): gv: R.Tensor((3, 3), "bool") = R.equal(x, R.const(True, "bool")) return gv @tvm.script.ir_module class Expected0: @T.prim_func(private=True, s_tir=True) def multiply( rxplaceholder: T.Buffer((T.int64(3), T.int64(3)), "float16"), T_multiply: T.Buffer((T.int64(3), T.int64(3)), "float16"), ): T.func_attr({"tirx.noalias": True}) # with T.sblock("root"): for ax0, ax1 in T.grid(T.int64(3), T.int64(3)): with T.sblock("T_multiply"): v_ax0, v_ax1 = T.axis.remap("SS", [ax0, ax1]) T.reads(rxplaceholder[v_ax0, v_ax1]) T.writes(T_multiply[v_ax0, v_ax1]) T_multiply[v_ax0, v_ax1] = rxplaceholder[v_ax0, v_ax1] * T.float16( 1.1455078125 ) @R.function def main(x: R.Tensor((3, 3), dtype="float16")) -> R.Tensor((3, 3), dtype="float16"): cls = Expected0 gv = R.call_tir(cls.multiply, (x,), out_ty=R.Tensor((3, 3), dtype="float16")) return gv @tvm.script.ir_module class Expected1: @T.prim_func(private=True, s_tir=True) def multiply( rxplaceholder: T.Buffer((T.int64(3), T.int64(3)), "uint8"), T_multiply: T.Buffer((T.int64(3), T.int64(3)), "uint8"), ): T.func_attr({"tirx.noalias": True}) # with T.sblock("root"): for ax0, ax1 in T.grid(T.int64(3), T.int64(3)): with T.sblock("T_multiply"): v_ax0, v_ax1 = T.axis.remap("SS", [ax0, ax1]) T.reads(rxplaceholder[v_ax0, v_ax1]) T.writes(T_multiply[v_ax0, v_ax1]) T_multiply[v_ax0, v_ax1] = rxplaceholder[v_ax0, v_ax1] * T.uint8(2) @R.function def main(x: R.Tensor((3, 3), dtype="uint8")) -> R.Tensor((3, 3), dtype="uint8"): cls = Expected1 gv = R.call_tir(cls.multiply, (x,), out_ty=R.Tensor((3, 3), dtype="uint8")) return gv @tvm.script.ir_module class Expected2: @T.prim_func(private=True, s_tir=True) def equal( rxplaceholder: T.Buffer((T.int64(3), T.int64(3)), "bool"), T_equal: T.Buffer((T.int64(3), T.int64(3)), "bool"), ): T.func_attr({"tirx.noalias": True}) # with T.sblock("root"): for ax0, ax1 in T.grid(T.int64(3), T.int64(3)): with T.sblock("T_equal"): v_ax0, v_ax1 = T.axis.remap("SS", [ax0, ax1]) T.reads(rxplaceholder[v_ax0, v_ax1]) T.writes(T_equal[v_ax0, v_ax1]) T_equal[v_ax0, v_ax1] = rxplaceholder[v_ax0, v_ax1] == tvm.tirx.const(True, "bool") @R.function def main(x: R.Tensor((3, 3), dtype="bool")) -> R.Tensor((3, 3), dtype="bool"): cls = Expected2 gv = R.call_tir(cls.equal, (x,), out_ty=R.Tensor((3, 3), dtype="bool")) return gv # fmt: on After0 = LegalizeOps()(Before0) tvm.ir.assert_structural_equal(After0, Expected0) After1 = LegalizeOps()(Before1) tvm.ir.assert_structural_equal(After1, Expected1) After2 = LegalizeOps()(Before2) tvm.ir.assert_structural_equal(After2, Expected2) def test_matmul_legalization_requires_known_dtype(): @I.ir_module(s_tir=True) class ArbitraryDtype: @R.function def main(A: R.Tensor([16, 32]), B: R.Tensor([32, 8])) -> R.Tensor([16, 8]): return R.matmul(A, B) with pytest.raises(AssertionError) as err: LegalizeOps()(ArbitraryDtype) # This error should be caught while attempting to legalize the # R.matmul, where we can present a user-friendly error. # Otherwise, the error isn't caught until the implementation of # `BlockBuilder.call_te`, when attempting to create a numeric # constant of type kHandle, which produces a much less # user-friendly error. err_message = err.value.args[0] assert err_message.startswith("To legalize R.matmul") emit_legalization_through_builder = tvm.testing.parameter( by_dict={ "return_relax_expr": False, "return_relax_var": True, } ) @pytest.fixture def custom_op(emit_legalization_through_builder): op_name = "custom_op.matmul_bias_add" def infer_ty(call: relax.Call, context): activations, weight, bias = call.args matmul_call = relax.op.matmul(activations, weight) matmul_ty = tvm.ir.Op.get("relax.matmul").get_attr("FInferType")(matmul_call, context) matmul_var = relax.Var("dummy_var", matmul_ty) add_call = matmul_var + bias add_ty = tvm.ir.Op.get("relax.add").get_attr("FInferType")(add_call, context) return add_ty def legalize(bb: relax.BlockBuilder, call: relax.Call): activations, weight, bias = call.args legalized = relax.op.matmul(activations, weight) + bias if emit_legalization_through_builder: legalized = bb.emit(legalized) return legalized op_attrs = { "FInferType": infer_ty, "FLegalize": legalize, "FPurity": True, } for key, value in op_attrs.items(): tvm.ir.register_op_attr(op_name, key, value) op = tvm.ir.Op.get(op_name) yield op for key in op_attrs: op.reset_attr(key) def test_recursive_legalization(custom_op): """Legalization of an operator may produce new operators requiring legalization""" @I.ir_module(s_tir=True) class Before: @R.function def main( A: R.Tensor([16, 32, 64], "float32"), Weight: R.Tensor([64, 128], "float32"), Bias: R.Tensor([16, 32, 128], "float32"), ): return relax.Call(custom_op, [A, Weight, Bias]) AfterFirstIter = LegalizeOps()(Before) AfterSecondIter = LegalizeOps()(AfterFirstIter) # After LegalizeOps, the custom operation should be replaced by # `R.matmul` and `R.add`, which should in turn be replaced with # TIR implementations. Therefore, the second application of # LegalizeOps() should be a no-op. tvm.ir.assert_structural_equal(AfterFirstIter, AfterSecondIter) def test_legalize_with_vdevice(): """Legalization may generate kernels for multiple targets This is a regression test. In previous implementations, Relax expressions whose argument types differed only by their `vdevice` would be legalized to use the same `PrimFunc`. """ @I.ir_module(s_tir=True) class Before: I.module_global_infos({"vdevice": [I.vdevice("llvm")]}) @R.function def func_cuda(A: R.Tensor([32, 32], "float32"), B: R.Tensor([32, 32], "float32")): C = R.add(A, B) return C @R.function def func_llvm( A: R.Tensor([32, 32], "float32", "llvm"), B: R.Tensor([32, 32], "float32", "llvm") ): C = R.add(A, B) return C @I.ir_module(s_tir=True) class Expected: I.module_global_infos({"vdevice": [I.vdevice("llvm")]}) @R.function def func_cuda( A: R.Tensor((32, 32), dtype="float32"), B: R.Tensor((32, 32), dtype="float32"), ): cls = Expected C = R.call_tir(cls.add, (A, B), out_ty=R.Tensor((32, 32), dtype="float32")) return C @T.prim_func(private=True, s_tir=True) def add( A: T.Buffer((T.int64(32), T.int64(32)), "float32"), B: T.Buffer((T.int64(32), T.int64(32)), "float32"), C: T.Buffer((T.int64(32), T.int64(32)), "float32"), ): T.func_attr({"tirx.noalias": True}) for iters in T.grid(T.int64(32), T.int64(32)): with T.sblock("T_add"): ax0, ax1 = T.axis.remap("SS", iters) C[ax0, ax1] = A[ax0, ax1] + B[ax0, ax1] @R.function def func_llvm( A: R.Tensor((32, 32), dtype="float32", vdevice="llvm"), B: R.Tensor((32, 32), dtype="float32", vdevice="llvm"), ): cls = Expected C = R.call_tir( cls.add_llvm, (A, B), out_ty=R.Tensor((32, 32), dtype="float32", vdevice="llvm"), ) return C @T.prim_func(private=True, s_tir=True) def add_llvm( A: T.Buffer((T.int64(32), T.int64(32)), "float32"), B: T.Buffer((T.int64(32), T.int64(32)), "float32"), C: T.Buffer((T.int64(32), T.int64(32)), "float32"), ): T.func_attr({"target": T.target("llvm"), "tirx.noalias": True}) for iters in T.grid(T.int64(32), T.int64(32)): with T.sblock("T_add"): ax0, ax1 = T.axis.remap("SS", iters) C[ax0, ax1] = A[ax0, ax1] + B[ax0, ax1] with tvm.target.Target("cuda"): After = tvm.relax.transform.LegalizeOps()(Before) tvm.ir.assert_structural_equal(Expected, After) if __name__ == "__main__": tvm.testing.main()