Skip to content

Commit

Permalink
Introduce --interface-api={c,packed} parameter
Browse files Browse the repository at this point in the history
This introduces structures generated to provide a documented and stable user
friendly interface to a TVM generated model, as can be seen in the AOT
demo application:
```
struct tvmgen_default_inputs inputs = {
  .input_1 = input_data,
};
struct tvmgen_default_outputs outputs = {
  .output = output_data,
};
int ret_val = tvmgen_default_run(&inputs, &outputs, NULL, NULL);
```

To facilitate this, some other changes are included:
* Removed dependency on `aot_executor.{c,h}` in tests, pending the
discussion in the interface RFC as to whether we keep them.
* Moved creation of test DLTensor's into the AOT test utils, in future this
can be replaced by loading via the Python API or otherwise
* Introduce `parametrize_aot_options` which can be used to test
permutations of AOT which work together - for now this filters C
interface and packed operators
* Updated demo application to generate the header for demonstration
purposes, we should consider porting the demo application to Model
Library Format and using the toolchain in the Zephyr App via CMake
instead?

This patch builds upon the improvements @giuseros made to AOT testing
and name mangling from apache#8014
  • Loading branch information
Mousius committed Jul 2, 2021
1 parent ab01abc commit d1df9c8
Show file tree
Hide file tree
Showing 13 changed files with 611 additions and 245 deletions.
11 changes: 6 additions & 5 deletions apps/microtvm/zephyr/aot_demo/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#include "input_data.h"
#include "output_data.h"
#include "tvmgen_default.h"
#include "zephyr_uart.h"

#ifdef CONFIG_ARCH_POSIX
Expand Down Expand Up @@ -194,18 +195,18 @@ void main(void) {
}
TVMLogf("Zephyr AOT Runtime\n");

void* inputs[1] = {
input_data,
struct tvmgen_default_inputs inputs = {
.input_1 = input_data,
};
void* outputs[1] = {
output_data,
struct tvmgen_default_outputs outputs = {
.output = output_data,
};

StackMemoryManager_Init(&app_workspace, g_aot_memory, WORKSPACE_SIZE);

double elapsed_time = 0;
TVMPlatformTimerStart();
int ret_val = tvm_runtime_run(&tvmgen_default_network, inputs, outputs);
int ret_val = tvmgen_default_run(&inputs, &outputs, NULL, NULL);
TVMPlatformTimerStop(&elapsed_time);

if (ret_val != 0) {
Expand Down
2 changes: 2 additions & 0 deletions include/tvm/runtime/module.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ constexpr const char* tvm_param_prefix = "__tvm_param__";
constexpr const char* tvm_lookup_linked_param = "_lookup_linked_param";
/*! \brief The main AOT executor function */
constexpr const char* tvm_run_func_suffix = "run_model";
/*! \brief The models entrypoint function which calls the executor */
constexpr const char* tvm_entrypoint_suffix = "run";
} // namespace symbol

// implementations of inline functions.
Expand Down
81 changes: 81 additions & 0 deletions python/tvm/micro/interface_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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.

"""Defines functions for generating a C interface header"""

import os

from tvm.relay.backend.utils import mangle_module_name


def _emit_brief(header_file, model_name, description):
header_file.write("/*!\n")
header_file.write(f" * \\brief TVM {model_name} model {description} \n")
header_file.write(" */\n")


def generate_c_interface_header(model_name, inputs, outputs, output_path):
"""Generates a C interface header for a given models inputs and outputs
Parameters
----------
model_name : str
Name of the model to be used in defining structs and naming the header
inputs : list[str]
List of model input names to be placed in generated structs
outputs : list[str]
List of model output names to be placed in generated structs
output_path : str
Path to the output folder to generate the header into
"""

mangled_name = mangle_module_name(model_name)
metadata_header = os.path.join(output_path, f"{mangled_name}.h")
with open(metadata_header, "w") as header_file:
_emit_brief(header_file, model_name, "input tensors")
header_file.write(f"struct {mangled_name}_inputs {{\n")
for input_name in inputs:
header_file.write(f" void* {input_name};\n")
header_file.write("};\n\n")

_emit_brief(header_file, model_name, "output tensors")
header_file.write(f"struct {mangled_name}_outputs {{\n")
for output_name in outputs:
header_file.write(f" void* {output_name};\n")
header_file.write("};\n\n")

_emit_brief(header_file, model_name, "memory blocks")
header_file.write(f"struct {mangled_name}_memory {{\n")
header_file.write("};\n\n")

_emit_brief(header_file, model_name, "device configurations")
header_file.write(f"struct {mangled_name}_devices {{\n")
header_file.write("};\n\n")

header_file.write("/*!\n")
header_file.write(f" * \\brief TVM {model_name} model run function \n")
header_file.write(" * \\param inputs Input tensors for the model \n")
header_file.write(" * \\param outputs Output tensors for the model \n")
header_file.write(" * \\param memory Memory blocks for the model to use \n")
header_file.write(" * \\param devices Devices for the model to use \n")
header_file.write(" */\n")
header_file.write(f"int {mangled_name}_run(\n")
header_file.write(f" struct {mangled_name}_inputs* inputs,\n")
header_file.write(f" struct {mangled_name}_outputs* outputs,\n")
header_file.write(f" struct {mangled_name}_memory* memory,\n")
header_file.write(f" struct {mangled_name}_devices* devices\n")
header_file.write(");\n")
45 changes: 44 additions & 1 deletion python/tvm/micro/model_library_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import re
import tarfile

from tvm.ir.type import TupleType
from .interface_api import generate_c_interface_header
from ..contrib import utils
from ..relay.backend import executor_factory
from ..relay import param_dict
Expand All @@ -49,7 +51,6 @@ def _populate_codegen_dir(mod, codegen_dir: str, module_name: str = None):
"""
dso_modules = mod._collect_dso_modules()
dso_module_handles = [m.handle.value for m in dso_modules]
non_dso_modules = mod._collect_from_import_tree(lambda m: m not in dso_modules)
if non_dso_modules:
raise UnsupportedInModelLibraryFormatError(
Expand Down Expand Up @@ -207,6 +208,42 @@ def _build_function_memory_map(function_metadata):
return ret


def _get_main_relay_func(mod: executor_factory.ExecutorFactoryModule):
main_func = mod.function_metadata[MAIN_FUNC_NAME_STR]
target = list(main_func.relay_primfuncs.keys())[0]
return main_func.relay_primfuncs[target]


def _convert_tuple_to_outputs(ret_type, offset=0):
outputs = []
added_fields = len(ret_type.fields)
for output_index in range(added_fields):
next_output = offset + len(outputs)
if isinstance(ret_type.fields[output_index], TupleType):
outputs.extend(_convert_tuple_to_outputs(ret_type.fields[output_index], next_output))
else:
outputs.append(f"output{next_output}")
return outputs


def _get_inputs_and_outputs_from_module(mod):
main_func = _get_main_relay_func(mod)
inputs = [argument.name_hint for argument in main_func.params]

outputs = ["output"]
if isinstance(main_func.ret_type, TupleType):
outputs = _convert_tuple_to_outputs(main_func.ret_type)

return inputs, outputs


def _should_generate_interface_header(mod):
for _, target in mod.target.items():
if "interface-api" in target.attrs and target.attrs["interface-api"] == "c":
return True
return False


def export_model_library_format(mod: executor_factory.ExecutorFactoryModule, file_name):
"""Export the build artifact in Model Library Format.
Expand Down Expand Up @@ -246,6 +283,12 @@ def export_model_library_format(mod: executor_factory.ExecutorFactoryModule, fil
os.mkdir(codegen_dir_path)
_populate_codegen_dir(mod.lib, codegen_dir_path, mod.libmod_name)

if _should_generate_interface_header(mod):
include_path = os.path.join(codegen_dir_path, "host/include")
os.mkdir(include_path)
inputs, outputs = _get_inputs_and_outputs_from_module(mod)
generate_c_interface_header(mod.libmod_name, inputs, outputs, include_path)

parameters_dir_path = tempdir.relpath("parameters")
os.mkdir(parameters_dir_path)
param_filename = os.path.join(parameters_dir_path, f"{mod.libmod_name}.params")
Expand Down
6 changes: 3 additions & 3 deletions src/relay/backend/aot_executor_codegen.cc
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ class AOTExecutorCodegen : public ExprVisitor {
/*! \brief mod */
runtime::Module* mod_;
/*! \brief list of input expressions (i.e., variable passed by the user) */
std::vector<Expr> input_vars_;
std::vector<Var> input_vars_;
/*! \brief input and output variables belonging to the main function signature */
Array<tir::Var> main_signature_;
/*! \brief target device */
Expand Down Expand Up @@ -783,8 +783,8 @@ class AOTExecutorCodegen : public ExprVisitor {
ret.lowered_funcs.Set(target_host_str, mod_run);
}
ret.function_metadata = std::move(function_metadata_);
ret.metadata = runtime::Metadata(input_vars_.size(), return_sid_.size(),
runtime::kTvmExecutorAot, mod_name);

ret.metadata = runtime::Metadata(input_vars_, return_sid_.size(), runtime::kTvmExecutorAot, mod_name);
return ret;
}
};
Expand Down
9 changes: 5 additions & 4 deletions src/runtime/meta_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include <dmlc/io.h>
#include <dmlc/json.h>
#include <tvm/relay/expr.h>
#include <tvm/runtime/executor_info.h>
#include <tvm/runtime/module.h>
#include <tvm/runtime/ndarray.h>
Expand Down Expand Up @@ -54,8 +55,8 @@ inline String get_name_mangled(const String& module_name, const String& name) {
*/
class MetadataNode : public Object {
public:
/*! \brief number of inputs of the main function */
int num_inputs = 1;
/*! \brief input information for the main function */
Array<tvm::relay::Var> inputs;
/*! \brief number of outputs of the main function */
int num_outputs = 1;
/*! \brief the executor to be used to run the model */
Expand All @@ -73,9 +74,9 @@ class MetadataNode : public Object {
*/
class Metadata : public ObjectRef {
public:
TVM_DLL Metadata(int num_inputs, int num_outputs, String executor, String mod_name) {
TVM_DLL Metadata(Array<tvm::relay::Var> inputs, int num_outputs, String executor, String mod_name) {
auto n = make_object<MetadataNode>();
n->num_inputs = num_inputs;
n->inputs = inputs;
n->num_outputs = num_outputs;
n->executor = executor;
n->mod_name = mod_name;
Expand Down
91 changes: 70 additions & 21 deletions src/target/source/source_module.cc
Original file line number Diff line number Diff line change
Expand Up @@ -192,25 +192,26 @@ class CSourceCrtMetadataModuleNode : public runtime::ModuleNode {
<< "}\n";
}

void GenerateEntrypointForUnpackedAPI(const std::string& run_func) {
void GenerateEntrypointForUnpackedAPI(const std::string& entrypoint_name,
const std::string& run_func) {
code_ << "TVM_DLL int32_t " << run_func << "(";
int total_args = (metadata_->num_inputs + metadata_->num_outputs);
for (int i = 0; i < total_args; ++i) {
code_ << "arg" << i;
unsigned int total_args = (metadata_->inputs.size() + metadata_->num_outputs);
for (unsigned int i = 0; i < total_args; ++i) {
code_ << "void* arg" << i;
if (i + 1 != total_args) {
code_ << ",";
}
}
code_ << ");\n";
code_ << "static int32_t " << ::tvm::runtime::symbol::tvm_module_main;
code_ << "int32_t " << entrypoint_name;
code_ << "(void* args, void* type_code, int num_args, void* out_value, void* "
"out_type_code, void* resource_handle) {\n";
code_ << "return " << run_func << "(";
for (int i = 0; i < metadata_->num_inputs; ++i) {
for (unsigned int i = 0; i < metadata_->inputs.size(); ++i) {
code_ << "((DLTensor*)(((TVMValue*)args)[" << i << "].v_handle))[0].data,";
}
for (int i = 0; i < metadata_->num_outputs; ++i) {
int j = metadata_->num_inputs + i;
int j = metadata_->inputs.size() + i;
code_ << "((DLTensor*)(((TVMValue*)args)[" << j << "].v_handle))[0].data";
if (i + 1 != metadata_->num_outputs) {
code_ << ",";
Expand All @@ -220,37 +221,85 @@ class CSourceCrtMetadataModuleNode : public runtime::ModuleNode {
code_ << "}\n";
}

void GenerateEntrypointForPackedAPI(const std::string& run_func) {
void GenerateEntrypointForPackedAPI(const std::string& entrypoint_name,
const std::string& run_func) {
code_ << "TVM_DLL int32_t " << run_func;
code_ << "(void* args, void* type_code, int num_args, void* out_value, void* "
"out_type_code, void* resource_handle);\n";
code_ << "static int32_t " << ::tvm::runtime::symbol::tvm_module_main;
code_ << "int32_t " << entrypoint_name;
code_ << "(void* args, void* type_code, int num_args, void* out_value, void* "
"out_type_code, void* resource_handle) {\n";
code_ << "return " << run_func;
code_ << "(args, type_code, num_args, out_value, out_type_code, resource_handle);\n";
code_ << "}\n";
}

void GenerateCInterfaceEntrypoint(const std::string& entrypoint_name, const std::string& run_func,
const std::string& mod_name) {
code_ << "#include <" << mod_name << ".h>\n";
code_ << "TVM_DLL int32_t " << run_func << "(";
unsigned int total_args = (metadata_->inputs.size() + metadata_->num_outputs);
for (unsigned int i = 0; i < total_args; ++i) {
code_ << "void* arg" << i;
if (i + 1 != total_args) {
code_ << ",";
}
}
code_ << ");\n";
code_ << "int32_t " << entrypoint_name << "(";
code_ << "struct " << runtime::get_name_mangled(mod_name, "inputs") << "* inputs,"
<< "struct " << runtime::get_name_mangled(mod_name, "outputs") << "* outputs,"
<< "struct " << runtime::get_name_mangled(mod_name, "memory") << "* memory,"
<< "struct " << runtime::get_name_mangled(mod_name, "devices") << "* devices"
<< ") {";
code_ << "return " << run_func << "(";
for (const auto& input : metadata_->inputs) {
code_ << "inputs->" << input->name_hint() << ",";
}
if (metadata_->num_outputs == 1) {
code_ << "outputs->output";
} else {
for (int i = 0; i < metadata_->num_outputs; ++i) {
code_ << "outputs->output" << i;
if (i + 1 != metadata_->num_outputs) {
code_ << ",";
}
}
}
code_ << ");\n";
code_ << "}\n";
}

void GenerateAOTDescriptor() {
const std::string run_func = ::tvm::runtime::symbol::tvm_run_func_suffix;
const std::string run_func_mangled = runtime::get_name_mangled(metadata_->mod_name, run_func);
const std::string run_func_suffix = ::tvm::runtime::symbol::tvm_run_func_suffix;
const std::string tvm_entrypoint_suffix = ::tvm::runtime::symbol::tvm_entrypoint_suffix;
const std::string run_func_mangled =
runtime::get_name_mangled(metadata_->mod_name, run_func_suffix);
const std::string entrypoint_mangled =
runtime::get_name_mangled(metadata_->mod_name, tvm_entrypoint_suffix);
const std::string network_mangled = runtime::get_name_mangled(metadata_->mod_name, "network");
code_ << "#include \"tvm/runtime/crt/internal/aot_executor/aot_executor.h\"\n";
auto unpacked_api = target_->GetAttr<Bool>("unpacked-api").value_or(Bool(false));
auto interface_api = target_->GetAttr<String>("interface-api").value_or(String("packed"));

code_ << "#include \"tvm/runtime/c_runtime_api.h\"\n";
code_ << "#ifdef __cplusplus\n";
code_ << "extern \"C\"\n";
code_ << "extern \"C\" {\n";
code_ << "#endif\n";
if (target_->GetAttr<Bool>("unpacked-api").value_or(Bool(false))) {
GenerateEntrypointForUnpackedAPI(run_func_mangled);

if (unpacked_api) {
if (interface_api == "c") {
GenerateCInterfaceEntrypoint(entrypoint_mangled, run_func_mangled, metadata_->mod_name);
} else {
GenerateEntrypointForUnpackedAPI(entrypoint_mangled, run_func_mangled);
}
} else {
GenerateEntrypointForPackedAPI(run_func_mangled);
ICHECK_EQ(interface_api, "packed") << "Packed interface required for packed operators";
GenerateEntrypointForPackedAPI(entrypoint_mangled, run_func_mangled);
}
code_ << "const tvm_model_t " << network_mangled << " = {\n"
<< " .run_func = &" << ::tvm::runtime::symbol::tvm_module_main << ",\n"
<< " .num_input_tensors = " << metadata_->num_inputs << ",\n"
<< " .num_output_tensors = " << metadata_->num_outputs << ", \n"
<< "};\n";

code_ << "#ifdef __cplusplus\n";
code_ << "}\n";
code_ << "#endif\n";
}

void CreateSource() {
Expand Down
2 changes: 2 additions & 0 deletions src/target/target_kind.cc
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ TVM_REGISTER_TARGET_KIND("llvm", kDLCPU)
.add_attr_option<String>("runtime")
.add_attr_option<Bool>("link-params", Bool(false))
.add_attr_option<Bool>("unpacked-api")
.add_attr_option<String>("interface-api")
.set_default_keys({"cpu"});

TVM_REGISTER_TARGET_KIND("c", kDLCPU)
Expand All @@ -310,6 +311,7 @@ TVM_REGISTER_TARGET_KIND("c", kDLCPU)
.add_attr_option<String>("executor")
.add_attr_option<Integer>("workspace-byte-alignment")
.add_attr_option<Bool>("unpacked-api")
.add_attr_option<String>("interface-api")
.set_default_keys({"cpu"});

TVM_REGISTER_TARGET_KIND("cuda", kDLCUDA)
Expand Down
Loading

0 comments on commit d1df9c8

Please sign in to comment.