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

lunderberg 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 3c5ee30630 [Relax] Support callback as argument (#16542)
3c5ee30630 is described below

commit 3c5ee30630760b3e42202e622517a92c18f11889
Author: Eric Lunderberg <[email protected]>
AuthorDate: Tue Feb 13 12:07:09 2024 -0600

    [Relax] Support callback as argument (#16542)
    
    Prior to this commit, calls from Relax to external PackedFuncs could
    only be done through the TVM global registry.  While Relax functions
    accepting a callback could be written as `callback_arg:
    R.Callable(arg_struct_info, ret_struct_info)`, attempting to compile
    these functions would raise an error during the `CodeGenVM` step of
    `relax.build`.  In addition, the global registry is only queried when
    initializing the `relax.VirtualMachine`, and so later changes requires
    restarting the VM.
    
    This commit updates both the `CodeGenVM` lowering pass and the relax
    VM to support callbacks.  The is primarily intended for use with the
    `LazyTransformParams` pass, to improve flexibility by avoiding use of
    the global registry.
---
 include/tvm/runtime/relax_vm/bytecode.h         |  28 +++++-
 src/relax/backend/vm/exec_builder.cc            |  20 +++-
 src/runtime/library_module.cc                   |  12 ++-
 src/runtime/relax_vm/bytecode.cc                |  11 +++
 src/runtime/relax_vm/executable.cc              |   8 ++
 src/runtime/relax_vm/vm.cc                      |  39 +++++---
 tests/python/relax/test_vm_callback_function.py | 124 ++++++++++++++++++++++++
 7 files changed, 219 insertions(+), 23 deletions(-)

diff --git a/include/tvm/runtime/relax_vm/bytecode.h 
b/include/tvm/runtime/relax_vm/bytecode.h
index 4526c6fffa..0db610ff42 100644
--- a/include/tvm/runtime/relax_vm/bytecode.h
+++ b/include/tvm/runtime/relax_vm/bytecode.h
@@ -58,6 +58,7 @@ enum class Opcode {
   Ret = 2U,
   Goto = 3U,
   If = 4U,
+  CallFromRegister = 5U,
 };
 
 /*! \brief A single virtual machine instruction.
@@ -183,10 +184,15 @@ struct Instruction {
   /*! \brief The instruction opcode. */
   Opcode op;
   union {
-    struct /* Call */ {
+    struct /* Call, CallFromRegister */ {
       /*! \brief The destination register. */
       RegName dst;
-      /*! \brief The index into the packed function table. */
+      /*! \brief The index of the function.
+       *
+       * For `OpCode::Call`, this is an index into the table of static
+       * functions.  For `OpCode::CallFromRegister`, this is an index
+       * of a register.
+       */
       Index func_idx;
       /*! \brief The number of arguments to the packed function. */
       Index num_args;
@@ -208,27 +214,43 @@ struct Instruction {
       Index false_offset;
     };
   };
+
   /*!
    * \brief Construct a Call instruction.
-   * \param func_idx The index of the function to call.
+   * \param func_idx The index of the function to call within the
+   *                 static function table
    * \param num_args The number of arguments.
    * \param args The input arguments.
    * \param dst The destination register.
    * \return The call instruction.
    */
   static Instruction Call(Index func_idx, Index num_args, Arg* args, RegName 
dst);
+
+  /*!
+   * \brief Construct a Call instruction.
+   * \param func_idx The index of the function to call within the
+   *                 current stack frame's registers.
+   * \param num_args The number of arguments.
+   * \param args The input arguments.
+   * \param dst The destination register.
+   * \return The call instruction.
+   */
+  static Instruction CallFromRegister(Index func_idx, Index num_args, Arg* 
args, RegName dst);
+
   /*!
    * \brief Construct a return instruction.
    * \param result The register containing the return value.
    * \return The return instruction.
    */
   static Instruction Ret(RegName result);
+
   /*!
    * \brief Construct a goto instruction.
    * \param pc_offset The register containing the jump offset.
    * \return The goto instruction.
    */
   static Instruction Goto(RegName pc_offset);
+
   /*!
    * \brief Construct an If instruction.
    * \param cond The register containing the cond value.
diff --git a/src/relax/backend/vm/exec_builder.cc 
b/src/relax/backend/vm/exec_builder.cc
index b5d932137b..aa47812235 100644
--- a/src/relax/backend/vm/exec_builder.cc
+++ b/src/relax/backend/vm/exec_builder.cc
@@ -138,10 +138,20 @@ void ExecBuilderNode::EndFunction(const std::string& 
func_name) {
 
 void ExecBuilderNode::EmitCall(vm::Instruction::Arg func, 
std::vector<vm::Instruction::Arg> args,
                                vm::RegName dst) {
-  ICHECK(func.kind() == vm::Instruction::ArgKind::kFuncIdx);
+  Opcode op_code;
+  if (func.kind() == vm::Instruction::ArgKind::kFuncIdx) {
+    op_code = Opcode::Call;
+  } else if (func.kind() == vm::Instruction::ArgKind::kRegister) {
+    op_code = Opcode::CallFromRegister;
+  } else {
+    LOG(FATAL) << "VM instruction for a function must be either "
+               << "kFuncIdx (static function ) "
+               << "or kRegister (function passed as parameter), "
+               << "but instead found " << func.kind();
+  }
   // store instruction
   exec_->instr_offset.push_back(exec_->instr_data.size());
-  exec_->instr_data.push_back(static_cast<ExecWord>(Opcode::Call));
+  exec_->instr_data.push_back(static_cast<ExecWord>(op_code));
   exec_->instr_data.push_back(dst);
   exec_->instr_data.push_back(func.value());
   exec_->instr_data.push_back(args.size());
@@ -228,7 +238,8 @@ void ExecBuilderNode::CheckExecutable() {
     for (size_t idx = start_instr; idx < end_instr; ++idx) {
       Instruction instr = exec_->GetInstruction(idx);
       switch (instr.op) {
-        case Opcode::Call: {
+        case Opcode::Call:
+        case Opcode::CallFromRegister: {
           check_func_defined(Instruction::Arg::FuncIdx(instr.func_idx));
           for (int i = 0; i < instr.num_args; ++i) {
             check_reg_defined(instr.args[i]);
@@ -280,7 +291,8 @@ void ExecBuilderNode::Formalize() {
     for (size_t idx = start_instr; idx < end_instr; ++idx) {
       Instruction instr = this->exec_->GetInstruction(idx);
       switch (instr.op) {
-        case Opcode::Call: {
+        case Opcode::Call:
+        case Opcode::CallFromRegister: {
           // rewrite args
           for (int i = 0; i < instr.num_args; ++i) {
             if (instr.args[i].kind() == Instruction::ArgKind::kRegister &&
diff --git a/src/runtime/library_module.cc b/src/runtime/library_module.cc
index bb5733ce01..7b39bcd8da 100644
--- a/src/runtime/library_module.cc
+++ b/src/runtime/library_module.cc
@@ -71,11 +71,15 @@ PackedFunc WrapPackedFunc(TVMBackendPackedCFunc faddr, 
const ObjectPtr<Object>&
   return PackedFunc([faddr, sptr_to_self](TVMArgs args, TVMRetValue* rv) {
     TVMValue ret_value;
     int ret_type_code = kTVMNullptr;
-    int ret = (*faddr)(const_cast<TVMValue*>(args.values), 
const_cast<int*>(args.type_codes),
-                       args.num_args, &ret_value, &ret_type_code, nullptr);
-    // NOTE: important to keep the original error message.
+    auto arg_values = const_cast<TVMValue*>(args.values);
+    auto arg_type_codes = const_cast<int*>(args.type_codes);
+    int ret =
+        (*faddr)(arg_values, arg_type_codes, args.num_args, &ret_value, 
&ret_type_code, nullptr);
+    // NOTE: It is important to keep the original error message.
+    // Using the `TVMThrowLastError()` function will also preserve the
+    // full stack trace for debugging in pdb.
     if (ret != 0) {
-      LOG(FATAL) << TVMGetLastError();
+      TVMThrowLastError();
     }
     if (ret_type_code != kTVMNullptr) {
       *rv = TVMRetValue::MoveFromCHost(ret_value, ret_type_code);
diff --git a/src/runtime/relax_vm/bytecode.cc b/src/runtime/relax_vm/bytecode.cc
index 9084207848..30d3bebd5f 100644
--- a/src/runtime/relax_vm/bytecode.cc
+++ b/src/runtime/relax_vm/bytecode.cc
@@ -42,6 +42,17 @@ Instruction Instruction::Call(Index func_idx, Index 
num_args, Instruction::Arg*
   return instr;
 }
 
+Instruction Instruction::CallFromRegister(Index func_idx, Index num_args, 
Instruction::Arg* args,
+                                          RegName dst) {
+  Instruction instr;
+  instr.op = Opcode::CallFromRegister;
+  instr.dst = dst;
+  instr.func_idx = func_idx;
+  instr.num_args = num_args;
+  instr.args = args;
+  return instr;
+}
+
 Instruction Instruction::Ret(RegName result) {
   Instruction instr;
   instr.op = Opcode::Ret;
diff --git a/src/runtime/relax_vm/executable.cc 
b/src/runtime/relax_vm/executable.cc
index f45786c3da..9de708f49a 100644
--- a/src/runtime/relax_vm/executable.cc
+++ b/src/runtime/relax_vm/executable.cc
@@ -134,6 +134,14 @@ Instruction Executable::GetInstruction(Index i) const {
       ExecWord* args = const_cast<ExecWord*>(&instr_data[offset + 4]);
       return Instruction::Call(func_idx, num_args, 
reinterpret_cast<Instruction::Arg*>(args), dst);
     }
+    case Opcode::CallFromRegister: {
+      RegName dst = instr_data[offset + 1];
+      Index func_idx = instr_data[offset + 2];
+      Index num_args = instr_data[offset + 3];
+      ExecWord* args = const_cast<ExecWord*>(&instr_data[offset + 4]);
+      return Instruction::CallFromRegister(func_idx, num_args,
+                                           
reinterpret_cast<Instruction::Arg*>(args), dst);
+    }
     case Opcode::Ret: {
       RegName result = instr_data[offset + 1];
       return Instruction::Ret(result);
diff --git a/src/runtime/relax_vm/vm.cc b/src/runtime/relax_vm/vm.cc
index d7f943d5f4..14a42df5f1 100644
--- a/src/runtime/relax_vm/vm.cc
+++ b/src/runtime/relax_vm/vm.cc
@@ -372,9 +372,10 @@ class VirtualMachineImpl : public VirtualMachine {
   /*!
    * \brief Run call instruction.
    * \param curr_frame The current frame.
+   * \param callable The callable object, either PackedFunc or closure
    * \param inst The call instruction.
    */
-  virtual void RunInstrCall(VMFrame* curr_frame, Instruction inst);
+  virtual void RunInstrCall(VMFrame* curr_frame, const ObjectRef& callable, 
Instruction inst);
 
   /*! \brief Run VM dispatch loop. */
   void RunLoop();
@@ -506,6 +507,9 @@ void VirtualMachineImpl::SetInput(std::string func_name, 
bool with_param_module,
 //------------------------------------------
 void VirtualMachineImpl::InvokeClosurePacked(const ObjectRef& 
closure_or_packedfunc, TVMArgs args,
                                              TVMRetValue* rv) {
+  ICHECK(closure_or_packedfunc.defined())
+      << "InvokeClosurePacked requires the callable object to be defined";
+
   // run packed call if it is a packed func.
   if (auto* packed = closure_or_packedfunc.as<PackedFunc::ContainerType>()) {
     packed->CallPacked(args, rv);
@@ -513,7 +517,8 @@ void VirtualMachineImpl::InvokeClosurePacked(const 
ObjectRef& closure_or_packedf
   }
   // run closure call.
   auto* clo = closure_or_packedfunc.as<VMClosureObj>();
-  ICHECK(clo != nullptr) << "Function expects a closure or PackedFunc ";
+  ICHECK(clo != nullptr) << "Function expects a closure or PackedFunc, "
+                         << "but received " << 
closure_or_packedfunc->GetTypeKey();
 
   std::vector<TVMValue> values(args.size() + 1);
   std::vector<int> tcodes(args.size() + 1);
@@ -595,6 +600,8 @@ Optional<VMClosure> 
VirtualMachineImpl::GetClosureInternal(const String& func_na
     auto impl = PackedFunc([gf_idx](TVMArgs args, TVMRetValue* rv) {
       // Per convention, ctx ptr is a VirtualMachine*
       VirtualMachine* ctx_ptr = static_cast<VirtualMachine*>(args[0].operator 
void*());
+      ICHECK(ctx_ptr) << "Context pointer for relax VM closure should be a 
VirtualMachine*, "
+                      << "but was NULL";
 
       std::vector<RegType> inputs(args.size() - 1);
       for (size_t i = 0; i < inputs.size(); ++i) {
@@ -644,7 +651,7 @@ RegType VirtualMachineImpl::InvokeBytecode(Index gf_idx, 
const std::vector<RegTy
   auto guard = PushFrame(this->pc_, gfunc);
   // Get new frame and set the caller info.
   VMFrame* curr_frame = frames_.back().get();
-  if (curr_instr.op == Opcode::Call) {
+  if (curr_instr.op == Opcode::Call || curr_instr.op == 
Opcode::CallFromRegister) {
     curr_frame->caller_return_register = curr_instr.dst;
   }
 
@@ -688,8 +695,12 @@ void VirtualMachineImpl::InitFuncPool() {
   }
 }
 
-void VirtualMachineImpl::RunInstrCall(VMFrame* curr_frame, Instruction instr) {
-  DLOG(INFO) << "\n  pc = " << pc_ << ", execute: " << 
GetFuncName(instr.func_idx);
+void VirtualMachineImpl::RunInstrCall(VMFrame* curr_frame, const ObjectRef& 
callable,
+                                      Instruction instr) {
+  ICHECK(callable.defined()) << "RunInstrCall requires the callable object to 
be defined";
+  auto func_name = instr.op == Opcode::Call ? GetFuncName(instr.func_idx) : 
"<dynamic>";
+
+  DLOG(INFO) << "\n  pc = " << pc_ << ", execute: " << func_name;
   int args_begin_offset = instrument_ != nullptr ? 4 : 0;
   // Use the call arg stack from the current frame to increase reuse
   // and avoid re-allocation
@@ -735,11 +746,11 @@ void VirtualMachineImpl::RunInstrCall(VMFrame* 
curr_frame, Instruction instr) {
   ICHECK_LT(static_cast<size_t>(instr.func_idx), this->func_pool_.size());
 
   if (instrument_ == nullptr) {
-    this->InvokeClosurePacked(func_pool_[instr.func_idx], args, &ret);
+    this->InvokeClosurePacked(callable, args, &ret);
   } else {
     // insert light-weight instrument callback
-    setter(0, func_pool_[instr.func_idx]);
-    setter(1, GetFuncName(instr.func_idx));
+    setter(0, callable);
+    setter(1, func_name);
     setter(2, true);
     setter(3, nullptr);
     TVMRetValue rv;
@@ -758,7 +769,7 @@ void VirtualMachineImpl::RunInstrCall(VMFrame* curr_frame, 
Instruction instr) {
       ret_kind = rv;
     }
     if (ret_kind != static_cast<int>(VMInstrumentReturnKind::kSkipRun)) {
-      this->InvokeClosurePacked(func_pool_[instr.func_idx], args, &ret);
+      this->InvokeClosurePacked(callable, args, &ret);
       setter(2, false);
       setter(3, ret);
       instrument_.CallPacked(TVMArgs(values.data(), tcodes.data(), 
values.size()), &rv);
@@ -782,7 +793,11 @@ void VirtualMachineImpl::RunLoop() {
     Instruction instr = exec_->GetInstruction(pc_);
     switch (instr.op) {
       case Opcode::Call: {
-        this->RunInstrCall(curr_frame, instr);
+        this->RunInstrCall(curr_frame, func_pool_[instr.func_idx], instr);
+        break;
+      }
+      case Opcode::CallFromRegister: {
+        this->RunInstrCall(curr_frame, ReadRegister(curr_frame, 
instr.func_idx), instr);
         break;
       }
       case Opcode::Ret: {
@@ -1000,7 +1015,7 @@ class VirtualMachineProfiler : public VirtualMachineImpl {
   }
 
  protected:
-  void RunInstrCall(VMFrame* curr_frame, Instruction inst) override {
+  void RunInstrCall(VMFrame* curr_frame, const ObjectRef& callable, 
Instruction inst) override {
     bool profiling = false;
     if (prof_ && prof_->IsRunning()) {
       auto f_name = GetFuncName(inst.func_idx);
@@ -1036,7 +1051,7 @@ class VirtualMachineProfiler : public VirtualMachineImpl {
       }
     }
 
-    VirtualMachineImpl::RunInstrCall(curr_frame, inst);
+    VirtualMachineImpl::RunInstrCall(curr_frame, callable, inst);
 
     if (profiling) {
       prof_->StopCall();
diff --git a/tests/python/relax/test_vm_callback_function.py 
b/tests/python/relax/test_vm_callback_function.py
new file mode 100644
index 0000000000..29a502ad7f
--- /dev/null
+++ b/tests/python/relax/test_vm_callback_function.py
@@ -0,0 +1,124 @@
+# 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
+import tvm.testing
+
+from tvm.script import relax as R
+
+import numpy as np
+
+exec_mode = tvm.testing.parameter("bytecode", "compiled")
+
+pytestmark = tvm.testing.parametrize_targets("llvm")
+
+
+def test_pass_tensor_to_function(exec_mode, target, dev):
+    @R.function
+    def relax_func(
+        A: R.Tensor([16], "int32"),
+        callback: R.Callable([R.Tensor([16], "int32")], R.Tuple([])),
+    ):
+        B = R.multiply(A, R.const(2))
+        _ = callback(B)
+        return R.tuple()
+
+    ex = tvm.relax.build(tvm.IRModule.from_expr(relax_func), target=target, 
exec_mode=exec_mode)
+    vm = tvm.relax.VirtualMachine(ex, dev)
+
+    from_callback = None
+
+    def custom_callback(arr):
+        nonlocal from_callback
+        from_callback = arr
+
+    np_A = np.arange(16, dtype="int32")
+    tvm_A = tvm.nd.array(np_A)
+
+    vm["relax_func"](tvm_A, custom_callback)
+
+    assert from_callback is not None
+    np.testing.assert_array_equal(np_A * 2, from_callback.numpy())
+
+
+def test_generate_tensor_in_function(exec_mode, target, dev):
+    @R.function
+    def relax_func(
+        callback: R.Callable([], R.Tensor([16], "int32")),
+    ):
+        A = callback()
+        B = R.multiply(A, R.const(2))
+        return B
+
+    ex = tvm.relax.build(
+        tvm.IRModule.from_expr(relax_func),
+        target=target,
+        exec_mode=exec_mode,
+    )
+    vm = tvm.relax.VirtualMachine(ex, dev)
+
+    np_A = np.arange(16, dtype="int32")
+
+    def custom_callback():
+        return tvm.nd.array(np_A)
+
+    output = vm["relax_func"](custom_callback)
+
+    np.testing.assert_array_equal(np_A * 2, output.numpy())
+
+
+def test_catch_exception_with_full_stack_trace(exec_mode, target, dev):
+    @R.function
+    def relax_func(
+        callback: R.Callable([], R.Tensor([16], "int32")),
+    ):
+        A = callback()
+        return A
+
+    ex = tvm.relax.build(
+        tvm.IRModule.from_expr(relax_func),
+        target=target,
+        exec_mode=exec_mode,
+    )
+    vm = tvm.relax.VirtualMachine(ex, dev)
+
+    def custom_callback():
+        local_var = 42
+        raise RuntimeError("Error thrown from callback")
+
+    try:
+        vm["relax_func"](custom_callback)
+    except RuntimeError as err:
+        stack = err.__traceback__
+        while stack.tb_next is not None:
+            stack = stack.tb_next
+        frame = stack.tb_frame
+
+        assert frame.f_code is custom_callback.__code__, (
+            "Inner-most stack frame should be from Python callback, "
+            "even though that crosses an FFI boundary"
+        )
+        assert frame.f_locals.get("local_var") == 42, (
+            "Python __traceback__ should include local variables, "
+            "even though that crosses an FFI boundary"
+        )
+    else:
+        raise RuntimeError("Exception thrown in callback was not propagated to 
calling scope")
+
+
+if __name__ == "__main__":
+    tvm.testing.main()

Reply via email to