5e29dddd02
* Added conv2d relay test for each schedule * Enable relay tests in qemu * split aot test utils
924 lines
33 KiB
Python
924 lines
33 KiB
Python
# 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.
|
|
"""Common functions for AOT test cases"""
|
|
import sys
|
|
import datetime
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import logging
|
|
from typing import Any, NamedTuple, Union, Optional, List, Dict
|
|
import numpy as np
|
|
|
|
import tvm
|
|
from tvm import relay
|
|
from tvm import autotvm
|
|
from tvm.contrib import utils, graph_executor
|
|
from tvm.relay.backend import Executor, Runtime
|
|
from tvm.relay.backend.utils import mangle_module_name
|
|
from tvm.micro import export_model_library_format
|
|
from tvm.micro.testing.utils import mlf_extract_workspace_size_bytes
|
|
|
|
_LOG = logging.getLogger(__name__)
|
|
|
|
NP_TYPE_TO_C = {
|
|
"int8": "int8_t",
|
|
"uint8": "uint8_t",
|
|
"int16": "int16_t",
|
|
"uint16": "uint16_t",
|
|
"int32": "int32_t",
|
|
"uint32": "uint32_t",
|
|
"float32": "float",
|
|
}
|
|
|
|
AOT_SUCCESS_TOKEN = "AOT_TEST_SUCCESS"
|
|
AOT_FAILURE_TOKEN = "AOT_TEST_FAILURE"
|
|
|
|
|
|
class AOTTestModel(NamedTuple):
|
|
"""Class to describe a model under test
|
|
|
|
Parameters
|
|
----------
|
|
module: tvm.IRModule
|
|
IRModule to generate AOT executor for
|
|
inputs: Dict[str, np.array]
|
|
Dict of input names to value arrays
|
|
outputs: List[np.array]
|
|
Dict of output names to value arrays
|
|
output_tolerance: Optional[Union[int, float]]
|
|
Allowed tolerance of the output
|
|
name: str
|
|
Name to use for this model
|
|
params: Optional[Dict[str, np.array]]
|
|
Dict of parameter names to value arrays
|
|
extra_memory_in_bytes: int
|
|
Extra memory to allocate after planned memory
|
|
"""
|
|
|
|
module: tvm.IRModule
|
|
inputs: Dict[str, np.array]
|
|
outputs: Dict[str, np.array]
|
|
output_tolerance: Optional[Union[int, float]] = None
|
|
name: str = "default"
|
|
params: Optional[Dict[str, np.array]] = None
|
|
extra_memory_in_bytes: int = 0
|
|
|
|
|
|
class AOTCompiledTestModel(NamedTuple):
|
|
"""A compiled AOTTestModel with associated module
|
|
|
|
Parameters
|
|
----------
|
|
model: AOTTestModel
|
|
Input model to be compiled
|
|
module: tvm.runtime.Module
|
|
The compiled Module for the associated AOTTestModel
|
|
"""
|
|
|
|
model: AOTTestModel
|
|
executor_factory: tvm.relay.backend.executor_factory.AOTExecutorFactoryModule
|
|
|
|
|
|
class AOTDataLinkage(NamedTuple):
|
|
"""A compiled AOTTestModel with associated module
|
|
|
|
Parameters
|
|
----------
|
|
section: str
|
|
Named section to place data into
|
|
alignment: int
|
|
Section alignment
|
|
"""
|
|
|
|
section: str
|
|
alignment: int
|
|
|
|
|
|
class AOTTestRunner(NamedTuple):
|
|
"""Class to describe a test runner for AOT code
|
|
|
|
Parameters
|
|
----------
|
|
makefile: str
|
|
Premade Makefile to use from the AOT test folder
|
|
prologue: str
|
|
Code to prepend to the main function
|
|
epilogue: str
|
|
Code to append to the main function
|
|
includes: List[str]
|
|
Additional includes required to run the AOT test runner
|
|
parameters: Dict[str, str]
|
|
Additional parameters to pass to the make command
|
|
pass_config: Dict[str, Any]
|
|
Additional pass configuration when building the model
|
|
"""
|
|
|
|
makefile: str = "default"
|
|
prologue: str = ""
|
|
epilogue: str = ""
|
|
includes: List[str] = []
|
|
parameters: Dict[str, str] = {}
|
|
pass_config: Dict[str, Any] = {}
|
|
|
|
|
|
def _subprocess_check_log_output(cmd, cwd, logfile):
|
|
"""
|
|
This method runs a process and logs the output to both a log file and stdout
|
|
"""
|
|
_LOG.info("Execute (%s): %s", cwd, cmd)
|
|
cmd_base = cmd[0] if isinstance(cmd, (list, tuple)) else cmd.split(" ", 1)[0]
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
cwd=cwd,
|
|
shell=True,
|
|
bufsize=0,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
encoding="utf-8",
|
|
)
|
|
stdout = ""
|
|
with open(logfile, "a") as f:
|
|
msg = (
|
|
"\n"
|
|
+ "-" * 80
|
|
+ f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Execute ({cwd}): {cmd}\n"
|
|
+ "-" * 80
|
|
)
|
|
f.write(msg)
|
|
stdout += msg + "\n"
|
|
while True:
|
|
data = proc.stdout.readline()
|
|
stdout += data
|
|
_LOG.debug("%s: %s", cmd_base, data.rstrip("\n"))
|
|
f.write(data)
|
|
|
|
# process is done if there is no data and the result is valid
|
|
if not data: # EOF
|
|
break
|
|
|
|
proc.wait()
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f"Subprocess failed: {cmd}\nstdout:\n{stdout}")
|
|
|
|
|
|
def _mangle_name(mod_name, name):
|
|
mod_name = mangle_module_name(mod_name)
|
|
return mod_name + "_" + name
|
|
|
|
|
|
# TODO: Move to linker script with list of symbols rather than coding into source
|
|
def _emit_data_linkage(output_file, data_linkage):
|
|
if data_linkage is not None:
|
|
output_file.write(
|
|
f'__attribute__((section("{data_linkage.section}"), '
|
|
f"aligned({data_linkage.alignment}))) "
|
|
)
|
|
|
|
|
|
def _emit_main_prologue(
|
|
main_file,
|
|
custom_prologue,
|
|
workspace_bytes,
|
|
data_linkage,
|
|
compiled_models,
|
|
interface_api,
|
|
use_stack_allocator=True,
|
|
):
|
|
if use_stack_allocator:
|
|
workspace_define = f"#define WORKSPACE_SIZE ({workspace_bytes}"
|
|
if interface_api == "c":
|
|
for compiled_model in compiled_models:
|
|
model = compiled_model.model
|
|
workspace_define += f" + TVMGEN_{model.name.upper()}_WORKSPACE_SIZE"
|
|
# Add TVM_RUNTIME_ALLOC_ALIGNMENT_BYTES because of memory alignment.
|
|
workspace_define += " + TVM_RUNTIME_ALLOC_ALIGNMENT_BYTES)\n"
|
|
main_file.write(workspace_define)
|
|
_emit_data_linkage(main_file, data_linkage)
|
|
main_file.write("static uint8_t g_aot_memory[WORKSPACE_SIZE];\n")
|
|
main_file.write("tvm_workspace_t app_workspace;\n")
|
|
main_file.write(
|
|
"""\n
|
|
tvm_crt_error_t TVMPlatformMemoryAllocate(size_t num_bytes, DLDevice dev, void** out_ptr) {
|
|
return StackMemoryManager_Allocate(&app_workspace, num_bytes, out_ptr);
|
|
}
|
|
tvm_crt_error_t TVMPlatformMemoryFree(void* ptr, DLDevice dev) {
|
|
return StackMemoryManager_Free(&app_workspace,ptr);
|
|
}
|
|
"""
|
|
)
|
|
else:
|
|
# An implementation is not needed for these if the stack allocator is not used
|
|
main_file.write(
|
|
"""\n
|
|
tvm_crt_error_t TVMPlatformMemoryAllocate(size_t num_bytes, DLDevice dev, void** out_ptr) {
|
|
return kTvmErrorFunctionCallNotImplemented;
|
|
}
|
|
tvm_crt_error_t TVMPlatformMemoryFree(void* ptr, DLDevice dev) {
|
|
return kTvmErrorFunctionCallNotImplemented;
|
|
}
|
|
"""
|
|
)
|
|
main_file.write(
|
|
"""\n
|
|
void TVMPlatformAbort(tvm_crt_error_t code) { exit(-1); }
|
|
void TVMLogf(const char* msg, ...) {
|
|
va_list args;
|
|
va_start(args, msg);
|
|
vfprintf(stdout, msg, args);
|
|
va_end(args);
|
|
}\n
|
|
TVM_DLL int TVMFuncRegisterGlobal(const char* name, TVMFunctionHandle f, int override) {}
|
|
int main(){\n
|
|
"""
|
|
)
|
|
main_file.write(custom_prologue)
|
|
|
|
|
|
def _emit_main_data(main_file, input_map, output_map, mod_name):
|
|
for key in input_map:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
main_file.write(
|
|
f'#include "{_mangle_name(mod_name,"input_data")}_{sanitized_tensor_name}.h"\n'
|
|
)
|
|
|
|
for key in output_map:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
main_file.write(
|
|
f'#include "{_mangle_name(mod_name,"expected_output_data")}_'
|
|
f'{sanitized_tensor_name}.h"\n'
|
|
f'#include "{_mangle_name(mod_name,"output_data")}_'
|
|
f'{sanitized_tensor_name}.h"\n'
|
|
)
|
|
|
|
|
|
def _emit_main_device_structs(main_file, devices, mod_name):
|
|
if devices:
|
|
main_file.write(
|
|
f"struct {_mangle_name(mod_name, 'devices')} {_mangle_name(mod_name, 'devices')} = {{"
|
|
)
|
|
for device in devices:
|
|
main_file.write(f"\t.{device} = {device},\n")
|
|
main_file.write("};\n")
|
|
|
|
|
|
def _emit_main_workspace_pool_structs(main_file, workspace_pool_names, mod_name):
|
|
if workspace_pool_names and len(workspace_pool_names) > 0:
|
|
main_file.write(
|
|
f"struct {_mangle_name(mod_name, 'workspace_pools')} "
|
|
f"{_mangle_name(mod_name, 'workspace_pools')} = {{"
|
|
)
|
|
for workspace_pool_name in workspace_pool_names:
|
|
main_file.write(f"\t.{workspace_pool_name} = {workspace_pool_name},\n")
|
|
main_file.write("};\n")
|
|
|
|
|
|
def _emit_main_data_structs(main_file, input_map, output_map, mod_name):
|
|
main_file.write(
|
|
f"struct {_mangle_name(mod_name, 'inputs')} {_mangle_name(mod_name, 'inputs')} = {{"
|
|
)
|
|
for key in input_map:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
main_file.write(
|
|
f"\t.{sanitized_tensor_name} = "
|
|
f"{_mangle_name(mod_name, 'input_data')}_{sanitized_tensor_name},\n"
|
|
)
|
|
main_file.write("};\n")
|
|
|
|
main_file.write(
|
|
f"struct {_mangle_name(mod_name, 'outputs')} {_mangle_name(mod_name, 'outputs')} = {{"
|
|
)
|
|
for key in output_map:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
main_file.write(
|
|
f"\t.{sanitized_tensor_name} = {_mangle_name(mod_name, 'output_data')}_"
|
|
f"{sanitized_tensor_name},\n"
|
|
)
|
|
main_file.write("};\n")
|
|
|
|
|
|
def _emit_main_data_setup(main_file, input_map, output_map, mod_name):
|
|
num_outputs = len(output_map)
|
|
num_inputs = len(input_map)
|
|
main_file.write(f'void* {_mangle_name(mod_name,"inputs")}[{num_inputs}] = {{ ')
|
|
for key in input_map:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
main_file.write(f'{_mangle_name(mod_name,"input_data")}_{sanitized_tensor_name}, ')
|
|
main_file.write("};\n")
|
|
main_file.write(f'void* {_mangle_name(mod_name,"outputs")}[{num_outputs}] = {{ ')
|
|
for key in output_map:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
main_file.write(f'{_mangle_name(mod_name, "output_data")}_{sanitized_tensor_name}, ')
|
|
main_file.write("};\n")
|
|
|
|
|
|
def _emit_main_c_interface_call(
|
|
main_file, devices, workspace_pool_names, mod_name, use_workspace_io
|
|
):
|
|
sub_strings = list()
|
|
sub_strings.append(f'{_mangle_name(mod_name,"run")}(')
|
|
if not use_workspace_io:
|
|
sub_strings.append(f'&{_mangle_name(mod_name,"inputs")}, ')
|
|
sub_strings.append(f'&{_mangle_name(mod_name,"outputs")}, ')
|
|
if workspace_pool_names:
|
|
sub_strings.append(f'&{_mangle_name(mod_name,"workspace_pools")}, ')
|
|
if devices:
|
|
sub_strings.append(f'&{_mangle_name(mod_name,"devices")}, ')
|
|
# Removing the last two characters that is a comma and a space
|
|
sub_strings[-1] = sub_strings[-1][:-2]
|
|
# Adding brackets and newline instead
|
|
sub_strings[-1] = sub_strings[-1] + ");\n"
|
|
|
|
main_file_string = "".join(sub_strings)
|
|
main_file.write(main_file_string)
|
|
|
|
|
|
def _emit_main_fake_packed_values(main_file):
|
|
main_file.write(
|
|
"""
|
|
static DLDevice fake_device = {kDLCPU, 0};
|
|
static int64_t fake_dims = 0;
|
|
static int64_t fake_shape = {0};
|
|
"""
|
|
)
|
|
|
|
|
|
def _emit_main_packed_call(main_file, input_map, output_list, mod_name):
|
|
tensors_name = _mangle_name(mod_name, "tensors")
|
|
values_name = _mangle_name(mod_name, "values")
|
|
typeids_name = _mangle_name(mod_name, "typeids")
|
|
|
|
def fake_tensor(source, source_index, packed_index):
|
|
main_file.write(
|
|
f"""
|
|
{tensors_name}[{packed_index}].device = fake_device;
|
|
{tensors_name}[{packed_index}].data = {source}[{source_index}];
|
|
{tensors_name}[{packed_index}].shape = &fake_shape;
|
|
{tensors_name}[{packed_index}].ndim = fake_dims;
|
|
{tensors_name}[{packed_index}].byte_offset = 0;
|
|
{tensors_name}[{packed_index}].strides = NULL;
|
|
{values_name}[{packed_index}].v_handle = &{tensors_name}[{packed_index}];
|
|
"""
|
|
)
|
|
|
|
num_outputs = len(output_list)
|
|
num_inputs = len(input_map)
|
|
num_tensors = num_inputs + num_outputs
|
|
main_file.write(
|
|
f"""
|
|
DLTensor {tensors_name}[{num_tensors}];
|
|
TVMValue {values_name}[{num_tensors}];
|
|
int32_t {typeids_name}[{num_tensors}];
|
|
"""
|
|
)
|
|
|
|
for i in range(0, num_inputs):
|
|
fake_tensor(_mangle_name(mod_name, "inputs"), i, i)
|
|
for i in range(0, num_outputs):
|
|
fake_tensor(_mangle_name(mod_name, "outputs"), i, i + num_inputs)
|
|
|
|
main_file.write(
|
|
f'{_mangle_name(mod_name, "run")}({values_name}, {typeids_name}, 0, NULL, 0, NULL);\n'
|
|
)
|
|
main_file.write("\n")
|
|
|
|
|
|
def _emit_main_compare(main_file, outputs, output_tolerance, mod_name, use_interface_c=False):
|
|
for key in outputs:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
expected_data_name = _mangle_name(mod_name, f"expected_output_data_{sanitized_tensor_name}")
|
|
is_float_dtype = outputs[key].dtype == "float32"
|
|
|
|
comparison_function = "abs"
|
|
tolerance = output_tolerance or 0
|
|
if is_float_dtype:
|
|
comparison_function = "fabs"
|
|
tolerance = output_tolerance or 0.001
|
|
|
|
data_length_var_name = (
|
|
_mangle_name(mod_name, f"output_data_{sanitized_tensor_name}") + "_len"
|
|
)
|
|
if use_interface_c:
|
|
c_type = NP_TYPE_TO_C[str(outputs[key].dtype)]
|
|
actual_data_name = f"(({c_type}*)" + _mangle_name(
|
|
mod_name, f"outputs.{sanitized_tensor_name})"
|
|
)
|
|
else:
|
|
actual_data_name = _mangle_name(mod_name, f"output_data_{sanitized_tensor_name}")
|
|
main_file.write(
|
|
f"for (int i = 0; i<{data_length_var_name}; i++) {{\n"
|
|
f"\tif ({comparison_function}({actual_data_name}[i]-"
|
|
f"{expected_data_name}[i]) > {tolerance}) {{\n"
|
|
f'\t\tprintf("{AOT_FAILURE_TOKEN}\\n");\n'
|
|
f"\t\treturn -1;\n"
|
|
f"\t}}\n"
|
|
f"}}"
|
|
)
|
|
|
|
|
|
def _emit_main_init_memory_manager(main_file):
|
|
main_file.write("StackMemoryManager_Init(&app_workspace, g_aot_memory, WORKSPACE_SIZE);")
|
|
main_file.write("\n")
|
|
|
|
|
|
def _emit_main_epilogue(main_file, custom_epilogue):
|
|
main_file.write(custom_epilogue)
|
|
main_file.write(f'printf("{AOT_SUCCESS_TOKEN}\\n");')
|
|
main_file.write("return 0;")
|
|
main_file.write("}\n")
|
|
|
|
|
|
def _emit_main_common_includes(main_file, custom_includes):
|
|
main_file.write("#include <stdio.h>\n")
|
|
main_file.write("#include <stdarg.h>\n")
|
|
main_file.write("#include <stdlib.h>\n")
|
|
main_file.write("#include <math.h>\n")
|
|
main_file.write('#include "tvm/runtime/c_runtime_api.h"\n')
|
|
main_file.write('#include "tvm/runtime/crt/stack_allocator.h"\n')
|
|
for include in custom_includes:
|
|
main_file.write(f'#include "{include}"\n')
|
|
|
|
|
|
def _emit_main_micro_include(main_file, mod_name):
|
|
main_file.write(f"#include <{mangle_module_name(mod_name)}.h>\n")
|
|
|
|
|
|
def _create_main(
|
|
test_name,
|
|
compiled_models,
|
|
output_path,
|
|
custom_includes,
|
|
custom_prologue,
|
|
custom_epilogue,
|
|
data_linkage,
|
|
interface_api,
|
|
workspace_bytes,
|
|
use_stack_allocator=True,
|
|
use_workspace_io=False,
|
|
):
|
|
file_path = pathlib.Path(f"{output_path}/" + test_name).resolve()
|
|
# create header file
|
|
raw_path = file_path.with_suffix(".c").resolve()
|
|
with open(raw_path, "w") as main_file:
|
|
_emit_main_common_includes(main_file, custom_includes)
|
|
|
|
if interface_api == "c":
|
|
for compiled_model in compiled_models:
|
|
model = compiled_model.model
|
|
_emit_main_micro_include(main_file, model.name)
|
|
for compiled_model in compiled_models:
|
|
model = compiled_model.model
|
|
_emit_main_data(main_file, model.inputs, model.outputs, model.name)
|
|
|
|
_emit_main_prologue(
|
|
main_file,
|
|
custom_prologue,
|
|
workspace_bytes,
|
|
data_linkage,
|
|
compiled_models,
|
|
interface_api,
|
|
use_stack_allocator,
|
|
)
|
|
if use_stack_allocator:
|
|
_emit_main_init_memory_manager(main_file)
|
|
|
|
if interface_api == "c":
|
|
for compiled_model in compiled_models:
|
|
model = compiled_model.model
|
|
executor_codegen_metadata = (
|
|
compiled_model.executor_factory.executor_codegen_metadata
|
|
)
|
|
devices = compiled_model.executor_factory.get_devices()
|
|
workspace_pool_names = None
|
|
if executor_codegen_metadata.pool_inputs:
|
|
workspace_pool_names = [
|
|
allocated_pool.pool_info.pool_name
|
|
for allocated_pool in dict(executor_codegen_metadata.pool_inputs).values()
|
|
if not allocated_pool.pool_info.is_internal
|
|
]
|
|
_emit_main_device_structs(main_file, devices, model.name)
|
|
if not use_workspace_io:
|
|
_emit_main_workspace_pool_structs(main_file, workspace_pool_names, model.name)
|
|
_emit_main_data_structs(main_file, model.inputs, model.outputs, model.name)
|
|
_emit_main_c_interface_call(
|
|
main_file, devices, workspace_pool_names, model.name, use_workspace_io
|
|
)
|
|
else:
|
|
_emit_main_fake_packed_values(main_file)
|
|
for compiled_model in compiled_models:
|
|
model = compiled_model.model
|
|
_emit_main_data_setup(main_file, model.inputs, model.outputs, model.name)
|
|
_emit_main_packed_call(main_file, model.inputs, model.outputs, model.name)
|
|
|
|
for compiled_model in compiled_models:
|
|
model = compiled_model.model
|
|
_emit_main_compare(
|
|
main_file, model.outputs, model.output_tolerance, model.name, interface_api == "c"
|
|
)
|
|
_emit_main_epilogue(main_file, custom_epilogue)
|
|
|
|
|
|
def _create_header_file(tensor_name, npy_data, output_path, data_linkage):
|
|
"""
|
|
This method generates a header file containing the data contained in the numpy array provided.
|
|
It is used to capture the tensor data (for both inputs and expected outputs)
|
|
to be bundled into the standalone application.
|
|
"""
|
|
file_path = pathlib.Path(f"{output_path}/" + tensor_name).resolve()
|
|
# create header file
|
|
raw_path = file_path.with_suffix(".h").resolve()
|
|
with open(raw_path, "w") as header_file:
|
|
header_file.write("#include <stddef.h>\n")
|
|
header_file.write("#include <stdint.h>\n")
|
|
header_file.write("#include <dlpack/dlpack.h>\n")
|
|
header_file.write(f"const size_t {tensor_name}_len = {npy_data.size};\n")
|
|
|
|
_emit_data_linkage(header_file, data_linkage)
|
|
|
|
header_file.write(f"{NP_TYPE_TO_C[str(npy_data.dtype)]} {tensor_name}[] =")
|
|
|
|
header_file.write("{")
|
|
for i in np.ndindex(npy_data.shape):
|
|
header_file.write(f"{npy_data[i]}, ")
|
|
header_file.write("};\n\n")
|
|
|
|
|
|
def convert_to_relay(
|
|
tflite_model_buf,
|
|
):
|
|
"""Convert a tflite model buffer in a Relay module"""
|
|
# TFLite.Model.Model has changed to TFLite.Model from 1.14 to 2.1
|
|
try:
|
|
import tflite.Model # pylint: disable=import-outside-toplevel
|
|
|
|
tflite_model = tflite.Model.Model.GetRootAsModel(tflite_model_buf, 0)
|
|
except AttributeError:
|
|
import tflite # pylint: disable=import-outside-toplevel
|
|
|
|
tflite_model = tflite.Model.GetRootAsModel(tflite_model_buf, 0)
|
|
except ImportError:
|
|
raise ImportError("The tflite package must be installed")
|
|
|
|
mod, params = relay.frontend.from_tflite(tflite_model)
|
|
mod["main"] = relay.build_module.bind_params_by_name(mod["main"], params)
|
|
return mod, params
|
|
|
|
|
|
def compile_models(
|
|
models: Union[List[AOTTestModel], AOTTestModel],
|
|
interface_api: str,
|
|
use_unpacked_api: bool,
|
|
workspace_byte_alignment: int = 8,
|
|
enable_op_fusion: bool = True,
|
|
pass_config: Dict[str, Any] = None,
|
|
use_runtime_executor: bool = True,
|
|
target: tvm.target.Target = tvm.target.Target("c"),
|
|
workspace_memory_pools=None,
|
|
schedule_name: str = None,
|
|
) -> List[AOTCompiledTestModel]:
|
|
"""
|
|
This method generates runtime.Modules for the tests
|
|
"""
|
|
if not isinstance(models, list):
|
|
models = [models]
|
|
|
|
runtime = Runtime("crt")
|
|
executor = Executor(
|
|
"aot",
|
|
{
|
|
"workspace-byte-alignment": workspace_byte_alignment,
|
|
"interface-api": interface_api,
|
|
"unpacked-api": use_unpacked_api,
|
|
},
|
|
)
|
|
|
|
config = {"tir.disable_vectorize": True}
|
|
if pass_config:
|
|
config = {**config, **pass_config}
|
|
if not enable_op_fusion:
|
|
config["relay.FuseOps.max_depth"] = 1
|
|
|
|
compiled_mods = list()
|
|
for model in models:
|
|
if schedule_name:
|
|
# Testing with deterministic schedule
|
|
task_list = autotvm.task.extract_from_program(
|
|
model.module, target=target, params=model.params
|
|
)
|
|
with tvm.autotvm.apply_fixed_config(task_list, schedule_name):
|
|
with tvm.transform.PassContext(opt_level=3, config=config):
|
|
if use_runtime_executor:
|
|
executor_factory = tvm.relay.build(
|
|
model.module,
|
|
target,
|
|
executor=executor,
|
|
runtime=runtime,
|
|
workspace_memory_pools=workspace_memory_pools,
|
|
params=model.params,
|
|
mod_name=model.name,
|
|
)
|
|
compiled_mods.append(
|
|
AOTCompiledTestModel(model=model, executor_factory=executor_factory)
|
|
)
|
|
else:
|
|
executor_factory = tvm.relay.build(
|
|
model.module,
|
|
tvm.target.Target(target, host=target),
|
|
params=model.params,
|
|
mod_name=model.name,
|
|
)
|
|
compiled_mods.append(
|
|
AOTCompiledTestModel(model=model, executor_factory=executor_factory)
|
|
)
|
|
else:
|
|
with tvm.transform.PassContext(opt_level=3, config=config):
|
|
# TODO(Mousius) - Remove once executor/runtime are fully removed from Target
|
|
if use_runtime_executor:
|
|
executor_factory = tvm.relay.build(
|
|
model.module,
|
|
target,
|
|
executor=executor,
|
|
runtime=runtime,
|
|
workspace_memory_pools=workspace_memory_pools,
|
|
params=model.params,
|
|
mod_name=model.name,
|
|
)
|
|
compiled_mods.append(
|
|
AOTCompiledTestModel(model=model, executor_factory=executor_factory)
|
|
)
|
|
else:
|
|
executor_factory = tvm.relay.build(
|
|
model.module,
|
|
tvm.target.Target(target, host=target),
|
|
params=model.params,
|
|
mod_name=model.name,
|
|
)
|
|
compiled_mods.append(
|
|
AOTCompiledTestModel(model=model, executor_factory=executor_factory)
|
|
)
|
|
return compiled_mods
|
|
|
|
|
|
def run_and_check(
|
|
models: List[AOTCompiledTestModel],
|
|
runner: AOTTestRunner,
|
|
interface_api: str,
|
|
debug_calculated_workspaces=False,
|
|
workspace_byte_alignment=8,
|
|
data_linkage: AOTDataLinkage = None,
|
|
test_dir: str = None,
|
|
verbose: bool = False,
|
|
use_workspace_io: bool = False,
|
|
):
|
|
"""
|
|
This method uses the original test data and compiled runtime.Modules
|
|
to run in the test runner to verify the results.
|
|
"""
|
|
|
|
def run_and_check_body(base_path):
|
|
cflags = f"-DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES={workspace_byte_alignment} "
|
|
# The calculated workspaces will not account for stack allocator tags used for debugging
|
|
if debug_calculated_workspaces:
|
|
cflags += "-DTVM_CRT_STACK_ALLOCATOR_ENABLE_LIFO_CHECK "
|
|
|
|
base_path = os.path.abspath(base_path)
|
|
build_path = os.path.join(base_path, "build")
|
|
os.makedirs(build_path, exist_ok=True)
|
|
|
|
include_path = os.path.join(base_path, "include")
|
|
os.mkdir(include_path)
|
|
crt_root = tvm.micro.get_standalone_crt_dir()
|
|
shutil.copy2(
|
|
os.path.join(crt_root, "template", "crt_config-template.h"),
|
|
os.path.join(include_path, "crt_config.h"),
|
|
)
|
|
|
|
workspace_bytes = 0
|
|
for compiled_model in models:
|
|
model = compiled_model.model
|
|
tar_file = os.path.join(base_path, f"{model.name}.tar")
|
|
export_model_library_format(compiled_model.executor_factory, tar_file)
|
|
t = tarfile.open(tar_file)
|
|
t.extractall(base_path)
|
|
|
|
# Interface C APIs does not need compiler generated
|
|
# workspace to generate the test application, because
|
|
# workspace size is codegen'd as a macro to
|
|
# tvmgen_<model_name>.h.
|
|
if interface_api != "c":
|
|
workspace_bytes += mlf_extract_workspace_size_bytes(tar_file)
|
|
|
|
workspace_bytes += model.extra_memory_in_bytes
|
|
for key in model.inputs:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
_create_header_file(
|
|
f'{_mangle_name(model.name, "input_data")}_{sanitized_tensor_name}',
|
|
model.inputs[key],
|
|
include_path,
|
|
data_linkage,
|
|
)
|
|
|
|
for key in model.outputs:
|
|
sanitized_tensor_name = re.sub(r"\W", "_", key)
|
|
_create_header_file(
|
|
f'{_mangle_name(model.name, "output_data")}_{sanitized_tensor_name}',
|
|
np.zeros(model.outputs[key].shape, model.outputs[key].dtype),
|
|
include_path,
|
|
data_linkage,
|
|
)
|
|
_create_header_file(
|
|
f'{_mangle_name(model.name, "expected_output_data")}_{sanitized_tensor_name}',
|
|
model.outputs[key],
|
|
include_path,
|
|
data_linkage,
|
|
)
|
|
|
|
use_usmp = runner.pass_config.get("tir.usmp.enable", False)
|
|
# We only need the stack allocator if USMP is not used
|
|
use_stack_allocator = not use_usmp
|
|
|
|
_create_main(
|
|
"test.c",
|
|
models,
|
|
build_path,
|
|
runner.includes,
|
|
runner.prologue,
|
|
runner.epilogue,
|
|
data_linkage,
|
|
interface_api,
|
|
workspace_bytes,
|
|
use_stack_allocator,
|
|
use_workspace_io,
|
|
)
|
|
|
|
# Verify that compiles fine
|
|
file_dir = os.path.dirname(os.path.abspath(__file__))
|
|
makefile_dir = os.path.join(file_dir, "../../../tests/python/relay/aot")
|
|
codegen_path = os.path.join(base_path, "codegen")
|
|
makefile = os.path.join(makefile_dir, f"{runner.makefile}.mk")
|
|
fvp_dir = "/opt/arm/FVP_Corstone_SSE-300/models/Linux64_GCC-6.4/"
|
|
# TODO(@grant-arm): Remove once ci_cpu docker image has been updated to FVP_Corstone_SSE
|
|
if not os.path.isdir(fvp_dir):
|
|
fvp_dir = "/opt/arm/FVP_Corstone_SSE-300_Ethos-U55/models/Linux64_GCC-6.4/"
|
|
custom_params = " ".join(
|
|
[f" {param}='{value}'" for param, value in runner.parameters.items()]
|
|
)
|
|
make_command = (
|
|
f"make -f {makefile} build_dir={build_path}"
|
|
+ f" CFLAGS='{cflags}'"
|
|
+ f" TVM_ROOT={file_dir}/../../.."
|
|
+ f" AOT_TEST_ROOT={makefile_dir}"
|
|
+ f" CODEGEN_ROOT={codegen_path}"
|
|
+ f" STANDALONE_CRT_DIR={tvm.micro.get_standalone_crt_dir()}"
|
|
+ f" FVP_DIR={fvp_dir}"
|
|
+ custom_params
|
|
)
|
|
|
|
compile_log_path = os.path.join(build_path, "test_compile.log")
|
|
compile_command = f"{make_command} aot_test_runner"
|
|
if verbose:
|
|
print("Compile command:\n", compile_command)
|
|
_subprocess_check_log_output(compile_command, ".", compile_log_path)
|
|
|
|
# Verify that runs fine
|
|
run_log_path = os.path.join(build_path, "test_run.log")
|
|
run_command = f"{make_command} run"
|
|
if verbose:
|
|
print("Run command:\n", run_command)
|
|
|
|
# TODO(lhutton1) This is a quick and dirty work around to help temporarily reduce
|
|
# the flakyness of the tests. Will remove once #10300 and #10314 are resolved.
|
|
try:
|
|
_subprocess_check_log_output(run_command, build_path, run_log_path)
|
|
except RuntimeError as err:
|
|
print("Failed to run the module, having a second attempt...", file=sys.stderr)
|
|
print(err, file=sys.stderr)
|
|
_subprocess_check_log_output(run_command, build_path, run_log_path)
|
|
|
|
with open(run_log_path) as run_log:
|
|
assert AOT_SUCCESS_TOKEN in run_log.read()
|
|
|
|
if test_dir is None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
run_and_check_body(os.path.join(tmpdir, "test"))
|
|
else:
|
|
run_and_check_body(test_dir)
|
|
|
|
|
|
def compile_and_run(
|
|
models: Union[List[AOTTestModel], AOTTestModel],
|
|
runner: AOTTestRunner,
|
|
interface_api: str,
|
|
use_unpacked_api: bool,
|
|
debug_calculated_workspaces: bool = False,
|
|
workspace_byte_alignment: int = 8,
|
|
enable_op_fusion: bool = True,
|
|
data_linkage: AOTDataLinkage = None,
|
|
use_runtime_executor: bool = True,
|
|
target: str = "c",
|
|
target_opts: Dict = None,
|
|
test_dir: str = None,
|
|
verbose: bool = False,
|
|
schedule_name: str = None,
|
|
):
|
|
"""This is a wrapper API to compile and run models as test for AoT
|
|
|
|
Parameters
|
|
----------
|
|
test_dir : str
|
|
This path will contain build, codegen, include directories
|
|
verbose: bool
|
|
Prints commands to build and run AOT test runner
|
|
"""
|
|
|
|
if target_opts:
|
|
for key, val in target_opts.items():
|
|
target += f" {key}={val}"
|
|
|
|
compiled_test_mods = compile_models(
|
|
models=models,
|
|
interface_api=interface_api,
|
|
use_unpacked_api=use_unpacked_api,
|
|
workspace_byte_alignment=workspace_byte_alignment,
|
|
enable_op_fusion=enable_op_fusion,
|
|
pass_config=runner.pass_config,
|
|
use_runtime_executor=use_runtime_executor,
|
|
target=tvm.target.Target(target),
|
|
schedule_name=schedule_name,
|
|
)
|
|
|
|
run_and_check(
|
|
models=compiled_test_mods,
|
|
runner=runner,
|
|
interface_api=interface_api,
|
|
debug_calculated_workspaces=debug_calculated_workspaces,
|
|
workspace_byte_alignment=workspace_byte_alignment,
|
|
data_linkage=data_linkage,
|
|
test_dir=test_dir,
|
|
verbose=verbose,
|
|
)
|
|
|
|
|
|
def generate_ref_data(mod, input_data, params=None, target="llvm"):
|
|
"""Generate reference data through executing the relay module"""
|
|
with tvm.transform.PassContext(opt_level=3, config={"tir.disable_vectorize": True}):
|
|
lib = relay.build(mod, target=target, params=params)
|
|
|
|
lib_name = "mod.so"
|
|
temp = utils.tempdir()
|
|
lib_path = temp.relpath(lib_name)
|
|
lib.export_library(lib_path)
|
|
lib = tvm.runtime.load_module(lib_path)
|
|
grt_mod = graph_executor.GraphModule(lib["default"](tvm.cpu()))
|
|
grt_mod.set_input(**input_data)
|
|
grt_mod.run()
|
|
output_count = grt_mod.get_num_outputs()
|
|
out = [grt_mod.get_output(i).numpy() for i in range(output_count)]
|
|
if isinstance(mod, tvm.relay.Function):
|
|
main = mod
|
|
else:
|
|
main = mod["main"]
|
|
if main.attrs is None or main.attrs["output_tensor_names"] is None:
|
|
output_tensor_names = ["output" if i == 0 else f"output{i+1}" for i in range(output_count)]
|
|
else:
|
|
output_tensor_names = main.attrs["output_tensor_names"]
|
|
|
|
return dict(zip(output_tensor_names, out))
|
|
|
|
|
|
def create_relay_module_and_inputs_from_tflite_file(tflite_model_file):
|
|
"""A helper function to create a Relay IRModule with inputs
|
|
and params from a tflite file"""
|
|
with open(tflite_model_file, "rb") as f:
|
|
tflite_model_buf = f.read()
|
|
mod, params = convert_to_relay(tflite_model_buf)
|
|
|
|
inputs = dict()
|
|
for param in mod["main"].params:
|
|
name = str(param.name_hint)
|
|
data_shape = [int(i) for i in param.type_annotation.shape]
|
|
dtype = str(param.type_annotation.dtype)
|
|
in_min, in_max = (np.iinfo(dtype).min, np.iinfo(dtype).max)
|
|
data = np.random.randint(in_min, high=in_max, size=data_shape, dtype=dtype)
|
|
inputs[name] = data
|
|
|
|
return mod, inputs, params
|