408 lines
12 KiB
Python
408 lines
12 KiB
Python
# 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()
|