# 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: F841 import tvm import tvm.testing from tvm import relax from tvm.script import ir as I from tvm.script import relax as R # functions that will not change def test_trivial(): @I.ir_module class Before: # already a DF block @R.function def main(A: R.Tensor, B: R.Tensor): with R.dataflow(): x = R.add(A, B) y = R.multiply(x, A) z = R.add(x, y) q = R.multiply(y, z) p = R.add(z, q) R.output(p) return p # too small @R.function def func(A: R.Tensor, B: R.Tensor) -> R.Tensor: x = R.add(A, B) return x # too few pure ops between non-dataflow ops @R.function(pure=False) def func2(A: R.Tensor, B: R.Tensor) -> R.Tensor: _ = R.print(format="Hi there!") y = R.add(A, B) _ = R.print(y, format="Sum: {}") x = R.multiply(y, y) if R.const(False): _ = R.print(format="True branch") q = R.add(x, y) _ = R.print(q, format="Value of q: {}") w = q else: _ = R.print(format="False branch") q = R.subtract(x, y) _ = R.print(q, format="Value of q: {}") w = q p = R.multiply(w, w) return p Expected = Before After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_basic(): @I.ir_module class Before: @R.function def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(z, y) v = R.add(w, x) return v @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) w = R.multiply(z, y) v = R.add(w, x) R.output(v) return v After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_multiple_blocks(): @I.ir_module class Before: @R.function(pure=False) def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(z, y) v = R.add(w, x) _ = R.print(format="Hi mom!") a = R.multiply(v, v) b = R.add(a, a) c = R.subtract(b, a) d = R.add(c, c) return d @I.ir_module class Expected: @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) 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.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_extract_inside_branches(): @I.ir_module class Before: @R.function(pure=False) def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(z, y) v = R.add(w, x) if R.const(True): q = R.multiply(v, v) a = R.add(q, q) b = R.multiply(a, a) else: q = R.add(v, v) a = R.multiply(q, q) b = R.add(a, a) c = R.multiply(b, b) d = R.add(c, c) e = R.multiply(d, d) return e @I.ir_module class Expected: @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) R.output(v) if R.const(True): with R.dataflow(): q = R.multiply(v, v) a = R.add(q, q) b = R.multiply(a, a) R.output(b) # weird but the parser requires this construct c = b else: with R.dataflow(): q = R.add(v, v) a = R.multiply(q, q) b = R.add(a, a) R.output(b) c = b with R.dataflow(): d = R.multiply(c, c) e = R.add(d, d) f = R.multiply(e, e) R.output(f) return f After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_treat_non_call_as_pure(): @I.ir_module class Before: @R.function def tuples_and_const(x: R.Tensor, y: R.Tensor) -> R.Tensor: t1 = (x, y, x) t2 = (y, y, x) c = R.const([1, 2, 3], dtype="int32") return c @R.function def shapes() -> R.Shape: s1 = R.shape((1, 2, 3)) s2 = R.shape((4, 5, 6)) s3 = R.shape((7, 8, 9)) return s3 @R.function def prim_values(): x = R.prim_value(1) y = R.prim_value(2) z = R.prim_value(3) return z @R.function def main(t: R.Tuple(R.Tensor, R.Tensor)) -> R.Tensor: x = t[0] y = t[1] z = R.add(x, y) w = R.multiply(z, z) return w @I.ir_module class Expected: @R.function def tuples_and_const(x: R.Tensor, y: R.Tensor) -> R.Tensor: with R.dataflow(): t1 = (x, y, x) t2 = (y, y, x) c = R.const([1, 2, 3], dtype="int32") R.output(c) return R.const([1, 2, 3], dtype="int32") @R.function def shapes() -> R.Shape: with R.dataflow(): s1 = R.shape((1, 2, 3)) s2 = R.shape((4, 5, 6)) s3 = R.shape((7, 8, 9)) R.output(s3) return s3 @R.function def prim_values(): with R.dataflow(): x = R.prim_value(1) y = R.prim_value(2) z = R.prim_value(3) R.output(z) return z @R.function def main(t: R.Tuple(R.Tensor, R.Tensor)) -> R.Tensor: with R.dataflow(): x = t[0] y = t[1] z = R.add(x, y) w = R.multiply(z, z) R.output(w) return w After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_impure_inner_function(): @I.ir_module class Before: @R.function(pure=False) def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: @R.function(pure=False) def inner_func(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(x, z) v = R.add(y, w) _ = R.print(format="oops") a = R.multiply(v, v) b = R.add(a, a) c = R.multiply(a, b) return c z = R.add(x, y) w = R.multiply(z, z) v = R.divide(w, z) q = inner_func(w, v) a = R.multiply(q, q) b = R.add(a, a) c = R.multiply(b, a) return c @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.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_pure_inner_function(): @I.ir_module class Before: @R.function def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: @R.function def inner_func(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(x, z) v = R.add(y, w) return v z = R.add(x, y) w = R.multiply(z, z) v = R.divide(w, z) q = inner_func(w, v) return q @I.ir_module class Expected: @R.function def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: with R.dataflow(): @R.function 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) return v z = R.add(x, y) w = R.multiply(z, z) v = R.divide(w, z) q = inner_func(w, v) R.output(q) return q After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_impure_external_function(): @I.ir_module class Before: @R.function(pure=False) def extra(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) q = R.matmul(z, x) w = R.nn.relu(q) _ = R.print(format="Whoa") return w @R.function(pure=False) def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(z, z) q = Before.extra(z, w) return q @I.ir_module class Expected: @R.function(pure=False) def extra(x: R.Tensor, y: R.Tensor) -> R.Tensor: with R.dataflow(): z = R.add(x, y) q = R.matmul(z, x) w = R.nn.relu(q) R.output(w) _ = R.print(format="Whoa") return w @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, z) R.output(z, w) q = Expected.extra(z, w) return q After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_pure_external_function(): @I.ir_module class Before: @R.function def extra(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) q = R.matmul(z, x) w = R.nn.relu(q) return w @R.function def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: z = R.add(x, y) w = R.multiply(z, z) q = Before.extra(z, w) return q @I.ir_module class Expected: @R.function def extra(x: R.Tensor, y: R.Tensor) -> R.Tensor: with R.dataflow(): z = R.add(x, y) q = R.matmul(z, x) w = R.nn.relu(q) R.output(w) return w @R.function def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: with R.dataflow(): z = R.add(x, y) w = R.multiply(z, z) q = Expected.extra(z, w) R.output(q) return q After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_merge_with_preceding_dataflow_block(): @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) w = R.multiply(z, y) R.output(w) # The single binding of `v = R.add` would normally not be # enough to make a dataflow block, as `1 < min_size == 2`. v = R.add(w, x) return v @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) w = R.multiply(z, y) # However, it occurs just after an existing dataflow # block, and can be merged into it. v = R.add(w, x) R.output(v) return v After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_merge_with_next_dataflow_block(): @I.ir_module class Before: @R.function def main(x: R.Tensor, y: R.Tensor) -> R.Tensor: # The single binding of `z = R.add` would normally not be # enough to make a dataflow block, as `1 < min_size == 2`. z = R.add(x, y) # However, it occurs just before an existing dataflow # block, and can be merged into it. with R.dataflow(): w = R.multiply(z, y) v = R.add(w, x) R.output(v) return v @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) w = R.multiply(z, y) v = R.add(w, x) R.output(v) return v After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_preserve_existing_dataflow_blocks_at_beginning(): """Preserve existing DataflowBlocks This is a regression test. In previous implementations, a DataflowBlock in the input, without enough bindings to become a new dataflow block, could be accidentally ommitted. This test is identical to `TestPreserveExistingDataflowBlocksAtEnd`, except that the existing dataflow block is at the beginning of the function. """ @I.ir_module class Before: @R.function(pure=False) def main(A0: R.Tensor, B0: R.Tensor): # This DataflowBlock is below the minimum size for a new # block, but already exists in the input IRModule. with R.dataflow(): A1 = R.add(A0, A0) R.output(A1) R.print(format="impure_function") # This sequence is large enough that it may be converted # to a DataflowBlock. B1 = R.add(B0, B0) B2 = R.add(B1, B1) B3 = R.add(B2, B2) return (A1, B3) @I.ir_module class Expected: @R.function(pure=False) def main(A0: R.Tensor, B0: R.Tensor): # This dataflow block should be preserved in the output. with R.dataflow(): A1 = R.add(A0, A0) R.output(A1) R.print(format="impure_function") with R.dataflow(): B1 = R.add(B0, B0) B2 = R.add(B1, B1) B3 = R.add(B2, B2) R.output(B3) return (A1, B3) After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) def test_preserve_existing_dataflow_blocks_at_end(): """Preserve existing DataflowBlocks This is a regression test. In previous implementations, a DataflowBlock in the input, without enough bindings to become a new dataflow block, could be accidentally ommitted. This test is identical to `TestPreserveExistingDataflowBlocksAtBeginning`, except that the existing dataflow block is at the end of the function. """ @I.ir_module class Before: @R.function(pure=False) def main(A0: R.Tensor, B0: R.Tensor): # This sequence is large enough that it may be converted # to a DataflowBlock. B1 = R.add(B0, B0) B2 = R.add(B1, B1) B3 = R.add(B2, B2) R.print(format="impure_function") # This DataflowBlock is below the minimum size for a new # block, but already exists in the input IRModule. with R.dataflow(): A1 = R.add(A0, A0) R.output(A1) return (A1, B3) @I.ir_module class Expected: @R.function(pure=False) def main(A0: R.Tensor, B0: R.Tensor): with R.dataflow(): B1 = R.add(B0, B0) B2 = R.add(B1, B1) B3 = R.add(B2, B2) R.output(B3) R.print(format="impure_function") # This dataflow block should be preserved in the output. with R.dataflow(): A1 = R.add(A0, A0) R.output(A1) return (A1, B3) After = relax.transform.ConvertToDataflow()(Before) tvm.ir.assert_structural_equal(After, Expected) if __name__ == "__main__": tvm.testing.main()