# 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: F401 import pytest import tvm import tvm.testing from tvm.script import ir as I from tvm.script import relax as R from tvm.script import tirx as T @pytest.mark.parametrize("key_type", [tvm.ir.GlobalVar, str]) def test_inline_simple(key_type): """Simple case of inlining Inlining can be done either by providing a string name or a GlobalVar. """ @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): B = A * A C = Before.subroutine(B) D = C + C return D @R.function(private=True) def subroutine(B: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): C = R.concat([B, B], axis=1) return C @R.function(private=True) def expected(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): B = A * A C = R.concat([B, B], axis=1) D = C + C return D gvar = Before.get_global_var("subroutine") if key_type is tvm.ir.GlobalVar: key = gvar elif key_type is str: key = gvar.name_hint else: raise TypeError(f"Unknown key_type: {key_type}") after = Before["main"].inline_functions({key: Before[gvar]}) tvm.ir.assert_structural_equal(expected, after) def test_ambiguous_function_name(): """Raise an error on ambiguous inputs For convenience, the function being replaced can be specified either as a string, or as a GlobalVar. However, all replacements must be unambiguous. """ @R.function def func(): return R.tuple() gvar = tvm.ir.GlobalVar("name") with pytest.raises(ValueError): func.inline_functions({gvar: func, "name": func}) def test_inline_dataflow_block(): """Functions may be inlined within a dataflow block""" @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): with R.dataflow(): B = A * A C = Before.subroutine(B) D = C + C R.output(D) return D @R.function(private=True) def subroutine(B: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): with R.dataflow(): C = R.concat([B, B], axis=1) R.output(C) return C @R.function(private=True) def expected(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): with R.dataflow(): B = A * A C = R.concat([B, B], axis=1) D = C + C R.output(D) return D after = Before["main"].inline_functions({"subroutine": Before["subroutine"]}) tvm.ir.assert_structural_equal(expected, after) def test_inline_non_dataflow_block_into_dataflow_block(): """Function inlining may not produce invalid Relax IR A subroutine call may appear within a DataflowBlock, even if the subroutine does not itself use a DataflowBlock. In this case, to avoid inserting a non-dataflow block in the middle of a set of dataflow bindings, the DataflowBlock in the caller must be split up. """ @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): with R.dataflow(): B = A * A C = Before.subroutine(B) D = C + C R.output(D) return D @R.function(private=True) def subroutine(B: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): C = R.concat([B, B], axis=1) return C @R.function(private=True) def expected(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): # DataflowBlock before subroutine with R.dataflow(): B = A * A R.output(B) # BindingBlock from the inlined subroutine. Because B is used # here, outside of a DataflowBlock, this requires it to be # updated from a DataflowVar to a normal Var. C = R.concat([B, B], axis=1) # Resuming the DataflowBlock after the inlined subroutine with R.dataflow(): D = C + C R.output(D) return D after = Before["main"].inline_functions({"subroutine": Before["subroutine"]}) tvm.ir.assert_structural_equal(expected, after) def test_subroutine_with_symbolic_vars(): """Inlined subroutines should use the caller's symbolic variables Before inlining, the subroutine and the caller have distinct `tirx::Var` for each symbolic variables. After inlining, only the caller's `tirx::Var` symbolic variables should remain. """ @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor(["n", 16], "int32")) -> R.Tensor(["n", 32], "int32"): B = A * A C = Before.subroutine(B) D = C + C return D @R.function(private=True) def subroutine(B: R.Tensor(["n", 16], "int32")) -> R.Tensor(["n", 32], "int32"): C = R.concat([B, B], axis=1) return C @R.function(private=True) def expected(A: R.Tensor(["n", 16], "int32")) -> R.Tensor(["n", 32], "int32"): B = A * A C = R.concat([B, B], axis=1) D = C + C return D after = Before["main"].inline_functions({"subroutine": Before["subroutine"]}) tvm.ir.assert_structural_equal(expected, after) def test_subroutine_with_symbolic_vars_and_static_argument(): """Inlined subroutines should use the caller's static shape Before inlining, the subroutine has symbolic variables, and the caller have static shape. After inlining, no symbolic variables should remain. """ @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): B = A * A C = Before.subroutine(B) D = C + C return D @R.function(private=True) def subroutine(B: R.Tensor(["n", 16], "int32")) -> R.Tensor(["n", 32], "int32"): C = R.concat([B, B], axis=1) return C @R.function(private=True) def expected(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): B = A * A C = R.concat([B, B], axis=1) D = C + C return D after = Before["main"].inline_functions({"subroutine": Before["subroutine"]}) tvm.ir.assert_structural_equal(expected, after) def test_inline_multiple_instances(): """A subroutine may be inlined multiple times When inlining, SSA should still be respected. """ @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor): B = Before.subroutine(A) C = Before.subroutine(B) return C @R.function(private=True) def subroutine(A0: R.Tensor) -> R.Tensor: A1 = A0 * A0 A2 = A1 + A1 return A2 @R.function(private=True) def expected(A: R.Tensor): # First call B = A * A C = B + B # Second call D = C * C E = D + D return E after = Before["main"].inline_functions({"subroutine": Before["subroutine"]}) tvm.ir.assert_structural_equal(expected, after) def test_inline_multiple_instances_with_distinct_static_shapes(): """A subroutine may be inlined multiple times When inlining, each instance of the inlined function may have a different value for the symbolic variables it uses. """ @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor([16, 16]), B: R.Tensor([32, 32])): A_out: R.Tensor([16, 16]) = Before.subroutine(A) B_out: R.Tensor([32, 32]) = Before.subroutine(B) return (A_out, B_out) @R.function(private=True) def subroutine(Input: R.Tensor(["n", "m"])) -> R.Tensor(["n", "m"]): Output = Input + Input return Output @R.function(private=True) def expected(A: R.Tensor([16, 16]), B: R.Tensor([32, 32])): A_out: R.Tensor([16, 16]) = A + A B_out: R.Tensor([32, 32]) = B + B return (A_out, B_out) after = Before["main"].inline_functions({"subroutine": Before["subroutine"]}) tvm.ir.assert_structural_equal(expected, after) def test_inline_nested_subroutine_calls(): """A private function may itself require inlining""" @I.ir_module class Before: @R.function(private=True) def main(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): B = A * A D = Before.subroutine(B) E = D + D return E @R.function(private=True) def subroutine(B: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): C = R.concat([B, B], axis=1) D = Before.subsubroutine(C) return D @R.function(private=True) def subsubroutine(C: R.Tensor([16, 32], "int32")) -> R.Tensor([16, 32], "int32"): D = C * C * C return D @R.function(private=True) def expected(A: R.Tensor([16, 16], "int32")) -> R.Tensor([16, 32], "int32"): B = A * A C = R.concat([B, B], axis=1) D = C * C * C E = D + D return E after = Before["main"].inline_functions( { "subroutine": Before["subroutine"], "subsubroutine": Before["subsubroutine"], } ) tvm.ir.assert_structural_equal(expected, after) def test_error_when_inlining_recursive_function(): """Inlining a recursive function call should raise an error""" @I.ir_module class Before: @R.function(private=True) def main(): B = Before.subroutine() return B @R.function(private=True) def subroutine() -> R.Tensor([], "int64"): R.func_attr({"relax.force_pure": True}) cond = R.call_packed("dummy_function", ty_args=R.Tensor([], "bool")) if cond: Out = Before.subroutine() else: Out = R.const(0, "int64") return Out with pytest.raises(Exception): Before["main"].inline_functions({"subroutine": Before["subroutine"]}) def test_error_when_inlining_mutually_recursive_functions(): """Inlining a recursive function call should raise an error""" @I.ir_module class Before: @R.function(private=True) def main(): B = Before.subroutine_a() return B @R.function(private=True) def subroutine_a() -> R.Tensor([], "int64"): R.func_attr({"relax.force_pure": True}) cond = R.call_packed("dummy_function", ty_args=R.Tensor([], "bool")) if cond: Out = Before.subroutine_b() else: Out = R.const(0, "int64") return Out @R.function(private=True) def subroutine_b() -> R.Tensor([], "int64"): R.func_attr({"relax.force_pure": True}) cond = R.call_packed("dummy_function", ty_args=R.Tensor([], "bool")) if cond: Out = Before.subroutine_a() else: Out = R.const(0, "int64") return Out with pytest.raises(Exception): Before["main"].inline_functions( { "subroutine_a": Before["subroutine_a"], "subroutine_b": Before["subroutine_b"], } ) if __name__ == "__main__": tvm.testing.main()