This is an automated email from the ASF dual-hosted git repository.

sanirudh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new d8c06767e3 [Hexagon] Simplify Mul->Sub->Conv to Conv->Add when 
possible (#15367)
d8c06767e3 is described below

commit d8c06767e3e99d888c892fa31a61d02ab21ce90b
Author: arangasa <[email protected]>
AuthorDate: Fri Jul 21 18:35:22 2023 +0530

    [Hexagon] Simplify Mul->Sub->Conv to Conv->Add when possible (#15367)
    
    * [Relay] Simplify Mul->Sub->Conv to Conv->Add when possible
    
    * Remove unused import: has_shape
---
 python/tvm/contrib/hexagon/transform.py            |  94 ++++++++-
 .../test_hexagon/test_relay_simplify_conv_pat.py   | 224 +++++++++++++++++++++
 2 files changed, 317 insertions(+), 1 deletion(-)

diff --git a/python/tvm/contrib/hexagon/transform.py 
b/python/tvm/contrib/hexagon/transform.py
index 664739dea5..6800f4cea0 100644
--- a/python/tvm/contrib/hexagon/transform.py
+++ b/python/tvm/contrib/hexagon/transform.py
@@ -18,6 +18,7 @@
 """Hexagon-specific IR transformations"""
 
 import functools as ft
+import numpy as np
 
 import tvm
 from tvm import relay
@@ -29,8 +30,9 @@ from tvm.relay.dataflow_pattern import (
     rewrite,
     wildcard,
 )
+from tvm.topi.utils import get_const_tuple
 from tvm.relay.expr import Call
-
+from tvm.runtime import ndarray as nd
 from ..._ffi.registry import register_func
 
 ### VTCM
@@ -410,3 +412,93 @@ def simplify_qnn_concat(mod, _=None):
     for global_var in mod.functions.keys():
         mod[global_var] = rewrite(simplify_qnn_concat_in_func(), 
mod[global_var])
     return mod
+
+
+class simplify_conv_pat_in_func(DFPatternCallback):
+
+    """
+    Simplify Mul->Sub->Conv->bias_add to Conv->bias_add->add sequence if
+    one of the inputs to Mul and Sub are constant scalars.
+
+    Replace
+    def @main(%q1: Tensor[(1, 128, 128, 3), float16])
+        %0 = multiply(%q1, c1_const_scalar)  /* ty=Tensor[(1, 128, 128, 3), 
float16] */;
+        %1 = subtract(%0, c2_const_scalar) /* ty=Tensor[(1, 128, 128, 3), 
float16] */
+        %2 = transpose(%1, axes=[0,3,1,2])
+            /* ty=Tensor[(1, 3, 128, 128), float16] */
+        %3 = nn.conv2d(%2, weights, ...) .
+        %4 = nn.bias_add(%3, bias)
+    }
+
+    with
+
+    def @main(%q1: Tensor[(1, 128, 128, 3), float16])
+        %0 = transpose(%q1, axes=[0, 3, 1, 2])
+            /* ty=Tensor[(1, 3, 128, 128), float16] */;
+        %1 = multiply(c1, weights) /* ty=Tensor[(64, 3, 3, 3), float16] */;
+        %2 = nn.conv2d(%0, %1, padding=[1, 1, 1, 1],
+            channels=64, kernel_size=[3, 3])
+            /* ty=Tensor[(1, 64, 128, 128), float16] */;
+        %3 = subtract(%0 shaped zero_tensor, c2)
+            /* ty=Tensor[(1, 3, 128, 128), float16] */;
+        %4 = nn.bias_add(%2, bias) /* ty=Tensor[(1, 64, 128, 128), float16] */;
+        %5 = nn.conv2d(%3, weights, padding=[1, 1, 1, 1],
+            channels=64, kernel_size=[3, 3])
+            /* ty=Tensor[(1, 64, 128, 128), float16] */;
+        add(%4, %5) /* ty=Tensor[(1, 64, 128, 128), float16] */
+
+    Why is it legal? Ignore the transpose in the above pattern.
+    res[p,q,r,s] = Conv(a*c1 - c2, W)
+                 = SUM{i=[0,c-1], j=[0,kh-1], k=[0,kw-1]}
+                    {(a[p,i,r+j,s+k] * c1 - c2) * W[q,i,j,k]}
+                 = SUM{i=[0,c-1], j=[0,kh-1], k=[0,kw-1]}
+                    {a[p,i,r+j,s+k] * c1 * W[q,i,j,k]} - c2 * W[q,i,j,k]}
+                 = Conv(a, W*c1) + Conv(0-c2, W)
+
+
+    }
+
+    In the above, %1, %3, %5 are constants and can be folded, so we're
+    left with 4 ops, as opposed to the original 5 ops
+    """
+
+    def __init__(self):
+        super().__init__()
+        self.inp = wildcard()
+        self.mul = is_op("multiply")(self.inp, is_constant().has_shape(()))
+        self.sub = is_op("subtract")(self.mul, is_constant().has_shape(()))
+        self.act = is_op("transpose")(self.sub)
+        self.weights = is_constant()
+        self.conv2d_op = is_op("nn.conv2d")(self.act, self.weights)
+        self.pattern = is_op("nn.bias_add")(self.conv2d_op, is_constant())
+
+    def callback(self, pre, post, node_map):
+        new_transpose = relay.transpose((node_map[self.inp][0]), 
**((node_map[self.act][0]).attrs))
+        new_weights = relay.multiply((node_map[self.mul][0].args[1]), 
(node_map[self.weights][0]))
+        new_conv2d = relay.nn.conv2d(
+            new_transpose, new_weights, **((node_map[self.conv2d_op][0]).attrs)
+        )
+        new_bias_add = relay.nn.bias_add(new_conv2d, 
(node_map[self.pattern][0].args[1]))
+
+        zero_tensor = relay.Constant(
+            nd.array(
+                np.zeros(
+                    
get_const_tuple((node_map[self.act][0]).checked_type.shape),
+                    dtype=(node_map[self.act][0]).checked_type.dtype,
+                )
+            )
+        )
+        negated = relay.subtract(zero_tensor, (node_map[self.sub][0].args[1]))
+        const_conv2d = relay.nn.conv2d(
+            negated, (node_map[self.weights][0]), 
**((node_map[self.conv2d_op][0]).attrs)
+        )
+        return relay.add(new_bias_add, const_conv2d)
+
+
+# Right now context is ignored
[email protected]_pass(opt_level=1)
+def simplify_conv_pat(mod, _=None):
+    """top level function for conv pattern simplification"""
+    for global_var in mod.functions.keys():
+        mod[global_var] = rewrite(simplify_conv_pat_in_func(), mod[global_var])
+    return mod
diff --git a/tests/python/contrib/test_hexagon/test_relay_simplify_conv_pat.py 
b/tests/python/contrib/test_hexagon/test_relay_simplify_conv_pat.py
new file mode 100644
index 0000000000..a85762cc24
--- /dev/null
+++ b/tests/python/contrib/test_hexagon/test_relay_simplify_conv_pat.py
@@ -0,0 +1,224 @@
+# 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.
+# pylint: disable=unused-wildcard-import, invalid-name
+
+"""
+Test hexagon relay transform - qnn.concat optimization
+"""
+import numpy as np
+import tvm
+from tvm.runtime import ndarray as nd
+from tvm.relay.backend import Executor
+from tvm import relay, testing
+from tvm.contrib.hexagon.transform import simplify_conv_pat
+from tvm.topi.utils import get_const_tuple
+from tvm.contrib.hexagon.session import Session
+from tvm.contrib.hexagon.pytest_plugin import HEXAGON_AOT_LLVM_TARGET
+
+
+def get_test_module_relay_exprs(isConstScalarMultiplier=True):
+    """
+    Creates relay expressions that can be used both by
+    test module and expected output module
+    """
+
+    act_shape = (1, 32, 32, 3)
+    data_in = np.random.rand(*get_const_tuple(act_shape))
+    data_in_float32 = np.full(data_in.shape, data_in, dtype="float32")
+    kernel_shape = (16, 3, 3, 3)
+    weights = np.random.rand(*get_const_tuple(kernel_shape))
+
+    bias = np.random.rand(get_const_tuple(kernel_shape)[0])
+    relay_act = relay.var("q1", shape=act_shape, dtype="float32")
+    if isConstScalarMultiplier:
+        relay_mul_factor = relay.const(0.00392151, dtype="float32")
+    else:
+        relay_mul_factor = np.random.rand(*get_const_tuple(act_shape))
+        relay_mul_factor = relay.Constant(
+            nd.array(np.full(relay_mul_factor.shape, relay_mul_factor, 
dtype="float32"))
+        )
+    relay_sub_term = relay.const(0.5, dtype="float32")
+    relay_weights = relay.Constant(nd.array(np.full(weights.shape, weights, 
dtype="float32")))
+    relay_bias = relay.Constant(nd.array(np.full(bias.shape, bias, 
dtype="float32")))
+    return (relay_act, relay_mul_factor, relay_sub_term, relay_weights, 
relay_bias, data_in_float32)
+
+
+def get_test_module_graph(relay_act, relay_mul_factor, relay_sub_term, 
relay_weights, relay_bias):
+    """Creates a test relay graph with the specified relay expressions"""
+    v1 = relay.multiply(relay_act, relay_mul_factor)
+    v2 = relay.subtract(v1, relay_sub_term)
+    v3 = relay.transpose(v2, axes=[0, 3, 1, 2])
+    weights_type_info = tvm.relay.transform.InferTypeLocal(relay_weights)
+    v4 = relay.nn.conv2d(
+        v3,
+        relay_weights,
+        padding=[1, 1, 1, 1],
+        channels=weights_type_info.shape[0],
+        kernel_size=[3, 3],
+    )
+    graph = relay.nn.bias_add(v4, relay_bias)
+    return graph
+
+
+def get_test_module(relay_act, relay_mul_factor, relay_sub_term, 
relay_weights, relay_bias):
+    """Creates a test relay module and returns it."""
+    graph = get_test_module_graph(
+        relay_act, relay_mul_factor, relay_sub_term, relay_weights, relay_bias
+    )
+
+    func = relay.Function(relay.analysis.free_vars(graph), graph)
+    mod = tvm.IRModule.from_expr(func)
+    return mod
+
+
+def get_expected_output_module_graph(
+    relay_act, relay_mul_factor, relay_sub_term, relay_weights, relay_bias
+):
+    """Creates the relay graph for expected output"""
+    v1 = relay.transpose(relay_act, axes=[0, 3, 1, 2])
+    v2 = relay.multiply(relay_mul_factor, relay_weights)
+    weights_type_info = tvm.relay.transform.InferTypeLocal(relay_weights)
+    v3 = relay.nn.conv2d(
+        v1, v2, padding=[1, 1, 1, 1], channels=weights_type_info.shape[0], 
kernel_size=[3, 3]
+    )
+    type_info = tvm.relay.transform.InferTypeLocal(v1)
+    relay_zero_act = relay.Constant(
+        nd.array(np.zeros(get_const_tuple(type_info.shape), dtype="float32"))
+    )
+    v4 = relay.subtract(relay_zero_act, relay_sub_term)
+    v5 = relay.nn.bias_add(v3, relay_bias)
+    v6 = relay.nn.conv2d(
+        v4,
+        relay_weights,
+        padding=[1, 1, 1, 1],
+        channels=weights_type_info.shape[0],
+        kernel_size=[3, 3],
+    )
+    return relay.add(v5, v6)
+
+
+def get_expected_output_module(
+    relay_act, relay_mul_factor, relay_sub_term, relay_weights, relay_bias
+):
+    """Returns manually created expected output module."""
+    graph = get_expected_output_module_graph(
+        relay_act, relay_mul_factor, relay_sub_term, relay_weights, relay_bias
+    )
+
+    out_func = relay.Function(relay.analysis.free_vars(graph), graph)
+    return tvm.IRModule.from_expr(out_func)
+
+
+def build_module(relay_mod, target):
+    """builds a relay module for a specified target"""
+    params = {}
+    executor = Executor("aot", {"link-params": True})
+    lowered = tvm.relay.build(
+        relay_mod,
+        tvm.target.Target(target, host=target),
+        executor=executor,
+        params=params,
+    )
+    return lowered
+
+
+def run_module(mod, inputs):
+    """invokes run function of specified module with inputs provided"""
+    mod.set_input(**inputs)
+    mod.run()
+    output = mod.get_output(0).numpy()
+    return output
+
+
+def get_test_modules():
+    """generates test, expected modules and their inputs"""
+    (
+        relay_act,
+        relay_mul_factor,
+        relay_sub_term,
+        relay_weights,
+        relay_bias,
+        data_in_float32,
+    ) = get_test_module_relay_exprs()
+    mod = get_test_module(relay_act, relay_mul_factor, relay_sub_term, 
relay_weights, relay_bias)
+    exp_relay_mod = get_expected_output_module(
+        relay_act, relay_mul_factor, relay_sub_term, relay_weights, relay_bias
+    )
+
+    return mod, exp_relay_mod, {"q1": data_in_float32}
+
+
[email protected]_hexagon
+def test_simplify_conv_pat(hexagon_session: Session):
+    """A positive test case"""
+
+    (mod, exp_relay_mod, inputs) = get_test_modules()
+
+    with tvm.transform.PassContext(opt_level=3):
+        mod = tvm.relay.transform.InferType()(mod)
+        hexagon_lowered = build_module(
+            mod, tvm.target.Target(HEXAGON_AOT_LLVM_TARGET, 
host=HEXAGON_AOT_LLVM_TARGET)
+        )
+
+    with tvm.transform.PassContext(opt_level=3):
+        mod = simplify_conv_pat(mod)
+        mod = tvm.relay.transform.InferType()(mod)
+        exp_relay_mod = tvm.relay.transform.InferType()(exp_relay_mod)
+        assert tvm.ir.structural_equal(mod["main"], exp_relay_mod["main"], 
map_free_vars=True)
+        mod = tvm.relay.transform.FoldConstant()(mod)
+        hexagon_lowered_opt = build_module(
+            mod, tvm.target.Target(HEXAGON_AOT_LLVM_TARGET, 
host=HEXAGON_AOT_LLVM_TARGET)
+        )
+
+    # Run unoptimized llvm module
+    hexagon_mod = hexagon_session.get_executor_from_factory(hexagon_lowered)
+    expected_output = run_module(hexagon_mod, inputs)
+
+    # Run optimized llvm module
+    hexagon_mod_opt = 
hexagon_session.get_executor_from_factory(hexagon_lowered_opt)
+    actual_output = run_module(hexagon_mod_opt, inputs)
+
+    tvm.testing.assert_allclose(actual_output, expected_output, rtol=0.00001)
+
+
+def get_negative_test_module():
+    """generates a negative test module with non-const multiplier"""
+    (
+        relay_act,
+        relay_mul_factor,
+        relay_sub_term,
+        relay_weights,
+        relay_bias,
+        _,
+    ) = get_test_module_relay_exprs(False)
+    mod = get_test_module(relay_act, relay_mul_factor, relay_sub_term, 
relay_weights, relay_bias)
+
+    return mod
+
+
+def test_negative():
+    """A negative test case"""
+    orig_mod = get_negative_test_module()
+    with tvm.transform.PassContext(opt_level=3):
+        orig_mod = tvm.relay.transform.InferType()(orig_mod)
+        opt_mod = simplify_conv_pat(orig_mod)
+        opt_mod = tvm.relay.transform.InferType()(opt_mod)
+        assert tvm.ir.structural_equal(orig_mod["main"], opt_mod["main"], 
map_free_vars=True)
+
+
+if __name__ == "__main__":
+    testing.main()

Reply via email to