blob: 7d7b74bf5961566881081aa2979b4b000baa6688 [file]
# 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.
import tvm
from tvm.relax.transform.transform import CanonicalizeBindings
import tvm.script
import tvm.testing
import pytest
from tvm import relax
from tvm.ir.base import assert_structural_equal
from tvm.script import ir as I, relax as R, tir as T
def verify(input, expected):
tvm.ir.assert_structural_equal(CanonicalizeBindings()(input), expected)
def test_simple_assignments():
@I.ir_module
class TestChainAssignments:
@R.function
def main(x: R.Tensor):
y = x
z = y
q = z
p = q
o = p
return o
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
return x
verify(TestChainAssignments, Expected)
def test_dataflow_block():
@I.ir_module
class TestDataflowAssignments:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.const(1)
z = y
o = z
p = o
m = p
n = m
R.output(n)
return n
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
n = R.const(1)
R.output(n)
return n
verify(TestDataflowAssignments, Expected)
def test_assign_to_output_in_dataflow_block():
@I.ir_module
class TestDataflowAssignments:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = x # is not a dataflow var
z = y
o = z
p = o
m = p
n = m
R.output(n)
return n
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
# we get a dataflow block where the
# only assignment is n = x, which we can eliminate,
# resulting in an empty block that is normalized away
return x
verify(TestDataflowAssignments, Expected)
def test_ops():
@I.ir_module
class TestOps:
@R.function
def main(x: R.Tensor, y: R.Tensor):
w = y
q = x
z = R.add(w, q)
return R.add(q, z)
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor, y: R.Tensor):
z = R.add(y, x)
return R.add(x, z)
verify(TestOps, Expected)
@pytest.mark.xfail(reason="The lhs and rhs of an assignment should have the same struct info.")
def test_casting():
@I.ir_module
class TestCasting:
@R.function
def main(x: R.Tensor) -> R.Object:
y = x
# z will be treated as object type even though it's a tensor
z: R.Object = y
return z
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor) -> R.Object:
# Cannot unify because the cast indicates user intent
z: R.Object = x
return z
verify(TestCasting, Expected)
def test_match_cast():
@I.ir_module
class TestMatchCast:
@R.function
def main(x: R.Tensor):
q = x
m, n = T.int64(), T.int64()
z = R.match_cast(q, R.Tensor((m, n)))
w = z
return w
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
# can't get rid of z because its struct_info is different from x's
m, n = T.int64(), T.int64()
z = R.match_cast(x, R.Tensor((m, n)))
return z
verify(TestMatchCast, Expected)
def test_same_shape():
@I.ir_module
class TestSameShape:
@R.function
def main(x: R.Tensor(("m", "n"), "float32")):
m, n = T.int64(), T.int64()
y = x
# trivial check
z = R.match_cast(x, R.Tensor((m, n), "float32"))
w = z
q = R.add(w, y)
return R.add(q, w)
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor(("m", "n"), "float32")):
# the trivial check is canonicalized into a var binding
# and then eliminated
q = R.add(x, x)
return R.add(q, x)
verify(TestSameShape, Expected)
def test_change_shape():
@I.ir_module
class TestChangeShape:
@R.function
def main(x: R.Tensor(("m", "n"))):
y = x
# not trivial: introduces new shape vars
o, p = T.int64(), T.int64()
z = R.match_cast(x, R.Tensor((o, p)))
w = z
q = R.add(w, y)
return R.add(q, w)
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor(("m", "n"))):
o, p = T.int64(), T.int64()
z = R.match_cast(x, R.Tensor((o, p)))
# the struct_info field on q will need to be updated
q = R.add(z, x)
return R.add(q, z)
verify(TestChangeShape, Expected)
def test_unwrap_tuple():
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor, y: R.Tensor):
tuple_var = (x, y)
w = tuple_var[0]
q = tuple_var[1]
z = R.add(w, q)
return R.add(q, z)
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor, y: R.Tensor):
tuple_var = (x, y)
z = R.add(x, y)
return R.add(y, z)
verify(Before, Expected)
def test_basic_folding_example():
@I.ir_module
class Input:
@R.function
def main() -> R.Tensor((), "int32"):
with R.dataflow():
y = R.const(1)
n = y
R.output(n)
return n
@I.ir_module
class Expected:
@R.function
def main() -> R.Tensor((), "int32"):
with R.dataflow():
n = R.const(1)
R.output(n)
return n
verify(Input, Expected)
def test_fold_match_cast():
@I.ir_module
class Input:
@R.function
def main() -> R.Tensor((), "int32"):
with R.dataflow():
y = R.const(1)
n = R.match_cast(y, R.Tensor((), "int32"))
R.output(n)
return n
@I.ir_module
class Expected:
@R.function
def main() -> R.Tensor((), "int32"):
with R.dataflow():
# the cast is trivial, so it is removed
n = R.const(1)
R.output(n)
return n
verify(Input, Expected)
def test_multiple_outputs():
@I.ir_module
class Input:
@R.function
def main():
with R.dataflow():
x = R.const(1)
y = R.const(1)
z = R.const(1)
l = x
m = y
n = z
R.output(l, m, n)
return (l, m, n)
@I.ir_module
class Expected:
@R.function
def main():
with R.dataflow():
l = R.const(1)
m = R.const(1)
n = R.const(1)
R.output(l, m, n)
return (l, m, n)
verify(Input, Expected)
def test_single_output_multiple_nondataflow():
"""Non-dataflow vars being updated may also be part trivial bindings
Like `test_multiple_outputs`, but only `n` is used in the return
statement.
"""
@I.ir_module
class Input:
@R.function
def main():
with R.dataflow():
x = R.const(1)
y = R.const(1)
z = R.const(1)
l = x
m = y
n = z
R.output(l, m, n)
return n
@I.ir_module
class Expected:
@R.function
def main():
with R.dataflow():
l = R.const(1)
m = R.const(1)
n = R.const(1)
R.output(n)
return n
verify(Input, Expected)
def test_fold_const_to_output():
@I.ir_module
class Before:
@R.function
def main() -> R.Tensor((), "int32"):
with R.dataflow():
n = R.const(1)
R.output(n)
return n
@I.ir_module
class Expected:
@R.function
def main() -> R.Tensor((), "int32"):
with R.dataflow():
n = R.const(1)
R.output(n)
return R.const(1)
verify(Before, Expected)
def test_canonicalize_var_to_dataflow_var_if_legal():
"""Canonicalize Var to DataflowVar inside DataflowBlock
DataflowVar instances may only be used inside a DataflowBlock. If
a trivial binding `y = x` occurs, where `x` is a `DataflowVar` and
`y` is a `Var`, replacing `y` with `x` may result in usage of a
`DataflowVar` outside of a `DataflowBlock`.
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
z = R.add(y, R.const(1))
R.output(y, z)
return z
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
z = R.add(y, R.const(1))
R.output(z)
return z
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_update_dataflow_computations_if_var_replacement_occurs():
"""Canonicalize Var to DataflowVar inside DataflowBlock
DataflowBlocks may produce additional outputs after the first
output Var, and these additional outputs may be in terms of the
first output. Computations that depend on a replaced var must be
updated to remain well-formed.
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
lv1 = R.add(x, R.const(1))
gv1 = lv1
gv2 = R.add(lv1, R.const(1))
R.output(gv1, gv2)
return (gv1, gv2)
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
# lv1 has been replaced with gv1
gv1 = R.add(x, R.const(1))
# So gv1 must be used in the computation of gv2
gv2 = R.add(gv1, R.const(1))
R.output(gv1, gv2)
return (gv1, gv2)
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_update_dataflow_computations_if_var_replacement_occurs_after_usage():
"""Canonicalize Var to DataflowVar inside DataflowBlock
Like test_update_dataflow_computations_if_var_replacement_occurs,
but the usage of a DataflowVar occurs before the trivial binding
that causes it to be replaced.
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
lv1 = R.add(x, R.const(1))
gv2 = R.add(lv1, R.const(1))
gv1 = lv1
R.output(gv1, gv2)
return (gv1, gv2)
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
# lv1 has been replaced with gv1
gv1 = R.add(x, R.const(1))
# So gv1 must be used in the computation of gv2
gv2 = R.add(gv1, R.const(1))
# Even though the trivial binding of "gv1 = lv1"
# occurred in this position.
R.output(gv1, gv2)
return (gv1, gv2)
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_replace_var_with_dataflow_if_all_usage_within_dataflow_block():
"""Canonicalize Var to DataflowVar inside DataflowBlock
Like `test_update_dataflow_computations_if_var_replacement_occurs`,
except that `gv1` is not part of the function's return value. When
deciding which variable to replace, the following logic is applied:
1. Normally, when encountering `x = y`, replace usage of `x` with `y`.
2. Unless the trivial binding is a `var_x = dataflow_y`, in which case
replace `dataflow_y` with `var_x` at the point of definition. This
prevents usage of `dataflow_y` from escaping the dataflow block.
3. Unless `var_x` has no usage outside the dataflow block, in which
case we replace usage of `var_x` with `dataflow_y`.
The third rule ensures that canonicalization can occur in a single
step. Otherwise, the output of this test case would contain a
non-dataflow var defined within a dataflow block, and only used within
that dataflow block. (Equivalent to the input for the test case
`test_canonicalize_var_to_dataflow_var_if_legal`.)
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
lv1 = R.add(x, R.const(1))
gv1 = lv1
gv2 = R.add(lv1, R.const(1))
R.output(gv1, gv2)
return gv2
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
gv1 = R.add(x, R.const(1))
gv2 = R.add(gv1, R.const(1))
R.output(gv2)
return gv2
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_var_to_dataflow_with_trivial_binding():
"""Canonicalize Var to DataflowVar inside DataflowBlock
Like
`test_replace_var_with_dataflow_if_all_usage_within_dataflow_block`,
except the non-DataflowVar is on the right-hand side of the trivial
binding.
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
gv1 = R.add(x, R.const(1))
lv1 = gv1
gv2 = R.add(lv1, R.const(1))
R.output(gv1, gv2)
return gv2
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
gv1 = R.add(x, R.const(1))
gv2 = R.add(gv1, R.const(1))
R.output(gv2)
return gv2
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_with_updated_struct_info():
"""CanonicalizeBindings and Normalizer may both replace a Var
If the CanonicalizeBindings pass has no replacements to make for a
variable, it must still delegate to the ExprMutator. This is because
a variable replacement may have occurred as part of the IRNormalizer,
in order to provide better struct info.
"""
@I.ir_module
class Before:
@R.function(private=True)
def main(A: R.Tensor(("n", 16), dtype="int32")) -> R.Tensor(("n", 16), dtype="int32"):
# CanonicalizeBindings recognizes this trivial binding, and
# replaces `B` with `A`.
B = A
# The value is updated from `R.add(B,B)` to `R.add(A,A)`.
# Changing the value triggers struct inference, allowing the
# shape to be updated to `[n,16]`. This requires a variable
# replacement, which is tracked by the `ExprMutator`.
C: R.Tensor(dtype="int32", ndim=2) = R.add(B, B)
# Replacement of `C` is not explicitly tracked by
# CanonicalizeBindings. However, if CanonicalizeBindings just
# returns `GetRef<Var>(var)`, `ExprMutator` cannot apply the
# replacement, and this will try to return the old
# version of `C` with `ndim=2`.
return C
@I.ir_module
class Expected:
@R.function(private=True)
def main(A: R.Tensor(("n", 16), dtype="int32")) -> R.Tensor(("n", 16), dtype="int32"):
n = T.int64()
C: R.Tensor([n, 16], "int32") = R.add(A, A)
return C
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_trivial_binding_to_dataflow_var():
"""Canonicalize Var to DataflowVar inside DataflowBlock
DataflowVar instances may only be used inside a DataflowBlock. If
a trivial binding `y = x` occurs, where `x` is a `DataflowVar` and
`y` is a `Var`, replacing `y` with `x` may result in usage of a
`DataflowVar` outside of a `DataflowBlock`.
If a binding exists solely to convert from DataflowVar into Var,
then canonicalization replaces the earlier DataflowVar with a Var.
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
z = y
R.output(z)
return z
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
R.output(y)
return y
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_multiple_trivial_binding_to_dataflow_var():
"""Canonicalize Var to DataflowVar inside DataflowBlock
Like test_canonicalize_trivial_binding_to_dataflow_var, but there
exist multiple trivial bindings to the DataflowVar.
"""
@I.ir_module
class Before:
@R.function
def main(w: R.Tensor):
with R.dataflow():
x = R.add(w, R.const(1))
y = x
z = x
R.output(y, z)
return (y, z)
@I.ir_module
class Expected:
@R.function
def main(w: R.Tensor):
with R.dataflow():
x = R.add(w, R.const(1))
R.output(x)
return (x, x)
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_trivial_var_binding_inside_dataflow_block():
"""Canonicalize Var to DataflowVar inside DataflowBlock
Canonicalization handles cases where a Var could be replaced by a
DataflowVar, and where a Var is a trivial binding. If these two
cases both occur, should produce reasonable results.
"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
z = y
R.output(y, z)
return z
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
R.output(y)
return y
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_across_non_dataflow_tuple():
"""Canonicalize Var to DataflowVar inside DataflowBlock"""
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
z = (y,)
gv = R.add(z[0], R.const(1))
R.output(z, gv)
return gv
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor):
with R.dataflow():
y = R.add(x, R.const(1))
z = (y,)
gv = R.add(y, R.const(1))
R.output(gv)
return gv
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_var_used_in_distinct_df_blocks():
"""If a var is used only in dataflow blocks,
but outside of the one where it was originally defined,
it should be exposed as an output."""
@I.ir_module
class Before:
@R.function(pure=False)
def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
z = R.add(x, y)
w = R.multiply(z, y)
v = R.add(w, x)
# v must remain exposed!
R.output(v)
_ = R.print(format="Hi mom!")
with R.dataflow():
a = R.multiply(v, v)
b = R.add(a, a)
c = R.subtract(b, a)
d = R.add(c, c)
R.output(d)
return d
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Before, after)
def test_inner_function():
@I.ir_module
class Before:
@R.function(pure=False)
def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
@R.function(pure=False)
def inner_func(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
z = R.add(x, y)
w = R.multiply(x, z)
v = R.add(y, w)
R.output(z, w, v)
_ = R.print(format="oops")
with R.dataflow():
a = R.multiply(v, v)
b = R.add(a, a)
c = R.multiply(a, b)
R.output(a, b, c)
return c
z = R.add(x, y)
w = R.multiply(z, z)
v = R.divide(w, z)
R.output(inner_func, z, v, w)
q = inner_func(w, v)
with R.dataflow():
a = R.multiply(q, q)
b = R.add(a, a)
c = R.multiply(b, a)
R.output(a, b, c)
return c
# expected: we do not need to expose all the outputs
@I.ir_module
class Expected:
@R.function(pure=False)
def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
@R.function(pure=False)
def inner_func(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
z = R.add(x, y)
w = R.multiply(x, z)
v = R.add(y, w)
R.output(v)
_ = R.print(format="oops")
with R.dataflow():
a = R.multiply(v, v)
b = R.add(a, a)
c = R.multiply(a, b)
R.output(c)
return c
z = R.add(x, y)
w = R.multiply(z, z)
v = R.divide(w, z)
R.output(inner_func, v, w)
q = inner_func(w, v)
with R.dataflow():
a = R.multiply(q, q)
b = R.add(a, a)
c = R.multiply(b, a)
R.output(c)
return c
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalize_inside_branches():
@I.ir_module
class Before:
@R.function
def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
z = R.add(x, y)
R.output(z)
if R.const(True):
with R.dataflow():
w = R.add(z, z)
v = R.multiply(w, w)
# w does not need to be output
R.output(w, v)
q = v
else:
with R.dataflow():
w = R.multiply(z, z)
v = R.add(w, w)
R.output(w, v)
q = v
return q
@I.ir_module
class Expected:
@R.function
def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
with R.dataflow():
z = R.add(x, y)
R.output(z)
if R.const(True):
with R.dataflow():
w = R.add(z, z)
v = R.multiply(w, w)
R.output(v)
q = v
else:
with R.dataflow():
w = R.multiply(z, z)
v = R.add(w, w)
R.output(v)
q = v
return q
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_canonicalization_causes_struct_info_update():
"""Regression test for failure mode causing undefined variable
The ExprMutator is only allowed to update a variable's struct info
if the value bound to it has new struct info. When
CanonicalizeBindings replaces a trivial binding, this may provide
better struct info as a result. If this happens, the
In previous implementations, ExprMutator::ReEmitBinding defined a
remap for `binding->var->vid`, even if the derived class defined a
replacement by overriding `VisitVarDef`. If the derived class
defines a new variable binding by overriding `VisitVarDef`, and
also causes a variable replacement by overriding `VisitExpr` and
returning a type with different struct info, then `ExprMutator`
must check for both `binding->var->vid` *AND* `new_var->vid`. The
former may be present in the unmodified graph, and the latter may
be produced by the derived class before delegating to the base
class.
"""
@I.ir_module
class Before:
@R.function
def transform_params(
A: R.Tensor(("vocab_size", 4096), dtype="float16"),
B: R.Tensor((6144, 4096), dtype="float16"),
):
with R.dataflow():
# Trivial binding of `DataFlow = NonDataFlow`.
# Wherever `C` is used, Canonicalization will attempt
# to replace it with `B`.
C = B
# RHS contains `(A,C)`, which CanonicalizeBindings
# replaces with `(A,B)`. Because this changes the
# RHS, a new LHS (and new struct info!) will be
# generated.
D: R.Tuple(
R.Tensor(dtype="float16", ndim=2),
R.Tensor((6144, 4096), dtype="float16"),
) = (A, C)
# Trivial binding of `NonDataFlow = DataFlow`. The
# definition of `D` will be replaced with a definition
# of `E`. This definition of `E` will then be updated
# to have a known shape.
E = D
R.output(E)
# By the time `E` is encountered at a usage site, the
# `ExprMutator` must have a replacement for the old
# version of `E` with `ndim=2` to the new versions of `E`
# with `shape=[vocab_size,4096]`.
return E
@I.ir_module
class Expected:
@R.function
def transform_params(
A: R.Tensor(("vocab_size", 4096), dtype="float16"),
B: R.Tensor((6144, 4096), dtype="float16"),
):
vocab_size = T.int64()
with R.dataflow():
E: R.Tuple(
R.Tensor((vocab_size, 4096), dtype="float16"),
R.Tensor((6144, 4096), dtype="float16"),
) = (A, B)
R.output(E)
return E
after = relax.transform.CanonicalizeBindings()(Before)
assert_structural_equal(Expected, after)
def test_unwrap_tuple_of_constant():
@I.ir_module
class TestChainAssignments:
@R.function
def main():
tup = (R.const(0, "int64"), R.const(1, "int64"))
x = tup[0]
y = tup[1]
z = R.add(x, y)
return z
@I.ir_module
class Expected:
@R.function
def main():
tup = (R.const(0, "int64"), R.const(1, "int64"))
x = tup[0]
y = tup[1]
z = R.add(R.const(0, "int64"), R.const(1, "int64"))
return z
verify(TestChainAssignments, Expected)
if __name__ == "__main__":
tvm.testing.main()