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

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


The following commit(s) were added to refs/heads/unity by this push:
     new c3aa71a53e [Unity][Analysis] Add utility for collecting compile-time 
bindings (#16312)
c3aa71a53e is described below

commit c3aa71a53e507ebe5caa283c0c2aee643d69e561
Author: Eric Lunderberg <[email protected]>
AuthorDate: Thu Jan 4 12:06:48 2024 -0600

    [Unity][Analysis] Add utility for collecting compile-time bindings (#16312)
    
    Whether an optimizations should be performed may depend on when the
    variables in an expression are known.
    
    For example, consider a LoRA-adjusted model, with base weights `W` of
    shape `[m,n]`, LoRA components `A` and `B` with shapes `[r,n]` and
    `[m,r]` respectively, and activations `x` with shape `[n,1]`.  The
    LoRA-adjusted matmul could be computed either as `(W + B*A)*x` or as
    `(W*x + B*(A*x))`.
    
    If `A` and `B` are provided at run-time, then computing `(W +
    B*(A*x))` requires significantly fewer computations.
    
    * `(W + B*A)*x`: `m*n*(2*r + 3)` operations
      1. `B*A`: `2*m*n*r` operations using a naive matmul
      2. Adding `W` to (1): `m*n` operations
      3. Multiplying `x` by (2): `2*m*n` operations
    
    * `(W*x + B*(A*x))`: (2*m*n + r*(2*n + 2*m + 1))
      1. `W*x`: `2*m*n` operations
      2. `A*x`: `2*r*n` operations
      3. Multiplying `B` by (2): `2*m*r` operations
      4. Adding (1) and (3)`: `m` operations
    
    However, if `A` and `B` are known at compile-time, then computing `(W
    + B*A)*x` groups all compile-time values together, allowing them to be
    computed earlier (i.e. using `LiftTransformParams`)
    
    * `(W + B*A)*x`: `2*m*n` operations
      1. `B*A`: 0 operations, computed at compile-time
      2. Adding `W` to (1): 0 operations, computed at compile-time
      3. Multiplying `x` by (2): `2*m*n` operations
    
    Since the choice of optimized expression depends on which parameters
    can be computed at compile-time, it is useful to have a utility that
    identifies values that can be computed at compile-time.
---
 include/tvm/relax/analysis.h                       |  15 ++
 python/tvm/relax/analysis/__init__.py              |   1 +
 python/tvm/relax/analysis/analysis.py              |  25 +++
 src/relax/analysis/computable_at_compile_time.cc   |  99 +++++++++
 .../test_analysis_computable_at_compile_time.py    | 243 +++++++++++++++++++++
 5 files changed, 383 insertions(+)

diff --git a/include/tvm/relax/analysis.h b/include/tvm/relax/analysis.h
index 6e2209d519..291b79ea55 100644
--- a/include/tvm/relax/analysis.h
+++ b/include/tvm/relax/analysis.h
@@ -533,6 +533,21 @@ TVM_DLL bool WellFormed(IRModule m, bool check_struct_info 
= true);
 TVM_DLL Map<tir::Block, Map<ObjectRef, tir::IndexMap>> SuggestLayoutTransforms(
     const Function& fn, Array<tir::IndexMap> write_buffer_transformations);
 
+/* \brief Collect variables whose value can be computed at compile-time
+ *
+ * If a function has the `kNumInput` attribute, then the first
+ * `kNumInput` parameters are provided at run-time, while all
+ * remaining parameters may be known at compile-time.  This utility
+ * collects all variable bindings that only depend, directly or
+ * indirectly, on the parameters known at compile-time.
+ *
+ * \param func The relax::Function to analyze
+ *
+ * \return The set of variables that can be computed at compile-time,
+ * in order of their occurrence within the function.
+ */
+TVM_DLL Array<Var> ComputableAtCompileTime(const Function& func);
+
 }  // namespace relax
 }  // namespace tvm
 
diff --git a/python/tvm/relax/analysis/__init__.py 
b/python/tvm/relax/analysis/__init__.py
index d8454a02cc..06b4f64326 100644
--- a/python/tvm/relax/analysis/__init__.py
+++ b/python/tvm/relax/analysis/__init__.py
@@ -21,6 +21,7 @@ from .analysis import (
     all_global_vars,
     all_vars,
     bound_vars,
+    computable_at_compile_time,
     contains_impure_call,
     definable_tir_vars_in_struct_info,
     defined_symbolic_vars,
diff --git a/python/tvm/relax/analysis/analysis.py 
b/python/tvm/relax/analysis/analysis.py
index 38f5ea2fea..83286c0980 100644
--- a/python/tvm/relax/analysis/analysis.py
+++ b/python/tvm/relax/analysis/analysis.py
@@ -528,3 +528,28 @@ def detect_recursion(mod: tvm.IRModule) -> 
List[List[GlobalVar]]:
         with any other, it will be a singleton in this list.
     """
     return _ffi_api.detect_recursion(mod)  # type: ignore
+
+
+def computable_at_compile_time(func: Function) -> List[Var]:
+    """Collect variables whose value can be computed at compile-time
+
+    If a function has the `kNumInput` attribute, then the first
+    `kNumInput` parameters are provided at run-time, while all
+    remaining parameters may be known at compile-time.  This utility
+    collects all variable bindings that only depend, directly or
+    indirectly, on the parameters known at compile-time.
+
+    Parameters
+    ----------
+    func: Function
+
+        The `relax.Function` to analyze
+
+    Returns
+    -------
+    ret: List[Var]
+
+        The set of variables that can be computed at compile-time, in
+        order of their occurrence within the function.
+    """
+    return _ffi_api.computable_at_compile_time(func)  # type: ignore
diff --git a/src/relax/analysis/computable_at_compile_time.cc 
b/src/relax/analysis/computable_at_compile_time.cc
new file mode 100644
index 0000000000..5ee336ff00
--- /dev/null
+++ b/src/relax/analysis/computable_at_compile_time.cc
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+/*!
+ * \file computable_at_compile_time.cc
+ *
+ * \brief Utilities for identifying potentially compile-time variables
+ */
+
+#include <tvm/relax/analysis.h>
+#include <tvm/relax/expr_functor.h>
+
+#include "../../support/ordered_set.h"
+
+namespace tvm {
+namespace relax {
+
+namespace {
+class CompileTimeCollector : ExprVisitor {
+ public:
+  static Array<Var> Collect(const Function& func) {
+    CompileTimeCollector visitor;
+    visitor(func);
+    return Array<Var>(visitor.known_relax_vars_.begin(), 
visitor.known_relax_vars_.end());
+  }
+
+ private:
+  void VisitExpr_(const FunctionNode* func) override {
+    if (auto opt_num_input = func->attrs.GetAttr<Integer>(attr::kNumInput)) {
+      size_t num_input = opt_num_input.value()->value;
+      for (size_t i = num_input; i < func->params.size(); i++) {
+        MarkAsKnown(func->params[i]);
+      }
+    }
+
+    ExprVisitor::VisitExpr_(func);
+  }
+
+  void VisitBinding(const Binding& binding) override {
+    Expr value = GetBoundValue(binding);
+    bool can_compute_at_compile_time = [&]() {
+      for (const auto& relax_var : FreeVars(value)) {
+        if (!known_relax_vars_.count(relax_var)) {
+          return false;
+        }
+      }
+      for (const auto& tir_var : FreeSymbolicVars(value)) {
+        if (!known_tir_vars_.count(tir_var)) {
+          return false;
+        }
+      }
+
+      return true;
+    }();
+
+    if (can_compute_at_compile_time) {
+      MarkAsKnown(binding->var);
+    }
+
+    ExprVisitor::VisitBinding(binding);
+  }
+
+  void MarkAsKnown(const Var& var) {
+    known_relax_vars_.insert(var);
+    for (const auto& tir_var : 
DefinableTIRVarsInStructInfo(GetStructInfo(var))) {
+      known_tir_vars_.insert(tir_var);
+    }
+  }
+
+  support::OrderedSet<Var> known_relax_vars_;
+  std::unordered_set<tir::Var, ObjectPtrHash, ObjectPtrEqual> known_tir_vars_;
+};
+}  // namespace
+
+Array<Var> ComputableAtCompileTime(const Function& func) {
+  return CompileTimeCollector::Collect(func);
+}
+
+TVM_REGISTER_GLOBAL("relax.analysis.computable_at_compile_time")
+    .set_body_typed(ComputableAtCompileTime);
+
+}  // namespace relax
+}  // namespace tvm
diff --git a/tests/python/relax/test_analysis_computable_at_compile_time.py 
b/tests/python/relax/test_analysis_computable_at_compile_time.py
new file mode 100644
index 0000000000..be78648e96
--- /dev/null
+++ b/tests/python/relax/test_analysis_computable_at_compile_time.py
@@ -0,0 +1,243 @@
+# 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.
+
+from typing import List
+
+import tvm
+import tvm.testing
+from tvm.script import relax as R, tir as T
+
+
+def _analyze_func(func: tvm.relax.Function) -> List[str]:
+    return [var.name_hint for var in 
tvm.relax.analysis.computable_at_compile_time(func)]
+
+
+def test_no_num_input_attribute():
+    """Without the "num_input" attribute, all params are runtime"""
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32")):
+        C = R.add(A, B)
+        return C
+
+    assert _analyze_func(func) == []
+
+
+def test_compile_time_param():
+    """Parameters after "num_input" are known at compile-time"""
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32")):
+        R.func_attr({"num_input": 1})
+        return ()
+
+    assert _analyze_func(func) == ["B"]
+
+
+def test_binding_using_one_param():
+    """Bindings may be computable at compile-time"""
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32")):
+        R.func_attr({"num_input": 1})
+        C = R.add(B, B)
+        D = R.add(A, C)
+        return D
+
+    assert _analyze_func(func) == ["B", "C"]
+
+
+def test_binding_using_multiple_params():
+    """Compile-time bindings may use multiple parameters"""
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32"), C: 
R.Tensor([16], "int32")):
+        R.func_attr({"num_input": 1})
+        D = R.add(B, C)
+        E = R.add(A, D)
+        return E
+
+    assert _analyze_func(func) == ["B", "C", "D"]
+
+
+def test_compile_time_binding_after_run_time():
+    """Compile-time bindings may occur after run-time
+
+    A binding being computable at compile-time only depends on the
+    arguments used for it.  A value that is computable at compile-time
+    may occur after a value that is only computable at run-time.
+    """
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32")):
+        R.func_attr({"num_input": 1})
+        C = R.add(A, A)
+        D = R.add(B, B)
+        E = R.add(C, D)
+        return E
+
+    assert _analyze_func(func) == ["B", "D"]
+
+
+def test_sequential_compile_time_bindings():
+    """Compile-time bindings may occur after run-time
+
+    A compile-time value may depend on variables defined within the
+    function, so long as those variables are themselves computable at
+    compile-time.
+    """
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32")):
+        R.func_attr({"num_input": 1})
+        C = R.add(B, B)
+        D = R.add(C, C)
+        E = R.add(D, D)
+        F = R.add(E, A)
+        return F
+
+    assert _analyze_func(func) == ["B", "C", "D", "E"]
+
+
+def test_dataflow_vars():
+    """Compile-time bindings may occur in dataflow blocks"""
+
+    @R.function
+    def func(A: R.Tensor([16], "int32"), B: R.Tensor([16], "int32")):
+        R.func_attr({"num_input": 1})
+        with R.dataflow():
+            C = R.add(B, B)
+            D = R.add(C, C)
+            E = R.add(D, D)
+            F = R.add(E, A)
+            R.output(F)
+        return F
+
+    assert _analyze_func(func) == ["B", "C", "D", "E"]
+
+
+def test_compile_time_symbolic_shape():
+    """Compile-time bindings may contain symbolic shapes"""
+
+    @R.function
+    def func(A: R.Tensor([1], "int32"), B: R.Tensor(["n"], "int32")):
+        R.func_attr({"num_input": 1})
+        n = T.int64()
+
+        C: R.Tensor([n], "int32") = R.add(B, B)
+        D: R.Tensor([], "int32") = R.max(C, axis=0)
+        E: R.Tensor([1], "int32") = R.add(A, D)
+        return E
+
+    assert _analyze_func(func) == ["B", "C", "D"]
+
+
+def test_symbolic_variables_from_match_binding():
+    """Symbolic vars may be inferred from compile-time bindings"""
+
+    @R.function
+    def func(A: R.Tensor(ndim=1, dtype="int32"), B: R.Tensor(ndim=1, 
dtype="int32")):
+        R.func_attr({"num_input": 1})
+        n = T.int64()
+        m = T.int64()
+
+        A2 = R.match_cast(A, R.Tensor([n], "int32"))
+        B2 = R.match_cast(B, R.Tensor([m], "int32"))
+
+        C = R.add(B2, B2)
+        D = R.max(C, axis=0)
+        E = R.max(A2, axis=0)
+        F = R.add(D, E)
+        return F
+
+    assert _analyze_func(func) == ["B", "B2", "C", "D"]
+
+
+def test_compile_time_expressions_may_not_use_runtime_symbolic_variables():
+    """Symbolic vars may be inferred from compile-time bindings
+
+    Here, `C` uses the symbolic variable `m`, which can be inferred
+    from the shape of `B` and is known at compile-time.  However, `D`
+    uses the symbolic variable `n`, which cannot be inferred without
+    first knowing `A`, and is therefore unknown at compile-time.
+    """
+
+    @R.function
+    def func(A: R.Tensor(["n"], "int32"), B: R.Tensor(["m"], "int32")):
+        R.func_attr({"num_input": 1})
+        n = T.int64()
+        m = T.int64()
+
+        C = R.ones([m], "int32")
+        D = R.ones([n], "int32")
+
+        E = (C, D)
+        return E
+
+    assert _analyze_func(func) == ["B", "C"]
+
+
+def test_compile_time_expressions_may_infer_same_variable_as_run_time():
+    """Symbolic vars may be inferred from compile-time bindings
+
+    A symbolic variable may be inferrable from multiple sources.
+    Here, while `n` can be inferred from the runtime parameter `A`, it
+    can also be inferred from the compile-time parameter `B`.
+    """
+
+    @R.function
+    def func(A: R.Tensor(["n"], "int32"), B: R.Tensor(["n"], "int32")):
+        R.func_attr({"num_input": 1})
+        n = T.int64()
+
+        C = R.ones([n], "int32")
+        D = R.ones([n], "int32")
+
+        E = (C, D)
+        return E
+
+    assert _analyze_func(func) == ["B", "C", "D", "E"]
+
+
+def test_compile_time_expressions_may_use_variables_from_match_cast():
+    """Symbolic vars may be inferred from compile-time bindings
+
+    Here, `C` uses the symbolic variable `m`, which can be inferred
+    from the shape of `B` and is known at compile-time.  However, `D`
+    uses the symbolic variable `n`, which cannot be inferred without
+    first knowing `A`, and is therefore unknown at compile-time.
+    """
+
+    @R.function
+    def func(A: R.Tensor(["n"], "int32"), B: R.Tensor(ndim=1, dtype="int32")):
+        R.func_attr({"num_input": 1})
+        n = T.int64()
+        m = T.int64()
+
+        B2 = R.match_cast(B, R.Tensor([m], "int32"))
+
+        C = R.ones([m], "int32")
+        D = R.ones([n], "int32")
+
+        E = (C, D)
+        return E
+
+    assert _analyze_func(func) == ["B", "B2", "C"]
+
+
+if __name__ == "__main__":
+    tvm.testing.main()

Reply via email to