From d16af94c8504a2f8a9ab9ebfca4d1d569a05998c Mon Sep 17 00:00:00 2001 From: Logan Weber <36520469+weberlo@users.noreply.github.com> Date: Mon, 10 Jun 2019 18:19:43 -0700 Subject: [PATCH] Add MicroTVM tutorial patch (#1) --- CMakeLists.txt | 2 + cmake/config.cmake | 3 + cmake/modules/Micro.cmake | 22 ++ include/tvm/runtime/c_runtime_api.h | 1 + include/tvm/runtime/device_api.h | 1 + include/tvm/runtime/micro/utvm_device_lib.h | 71 ++++ python/tvm/__init__.py | 2 +- python/tvm/_ffi/runtime_ctypes.py | 2 + python/tvm/contrib/binutil.py | 233 ++++++++++++ python/tvm/micro/__init__.py | 9 + python/tvm/micro/base.py | 178 +++++++++ python/tvm/micro/cross_compile.py | 62 +++ python/tvm/ndarray.py | 16 + src/api/api_pass.cc | 4 +- src/codegen/codegen_c.cc | 36 +- src/codegen/codegen_c_host.cc | 9 +- src/codegen/codegen_c_host.h | 3 +- src/runtime/micro/device/utvm_runtime.c | 80 ++++ src/runtime/micro/device/utvm_runtime.h | 98 +++++ src/runtime/micro/host_low_level_device.cc | 101 +++++ src/runtime/micro/low_level_device.h | 95 +++++ src/runtime/micro/micro_common.cc | 124 ++++++ src/runtime/micro/micro_common.h | 316 ++++++++++++++++ src/runtime/micro/micro_device_api.cc | 135 +++++++ src/runtime/micro/micro_module.cc | 145 +++++++ src/runtime/micro/micro_session.cc | 354 ++++++++++++++++++ src/runtime/micro/micro_session.h | 257 +++++++++++++ src/runtime/micro/openocd_low_level_device.cc | 76 ++++ .../micro/target_data_layout_encoder.h | 188 ++++++++++ src/runtime/module.cc | 2 + tests/python/contrib/test_binutil.py | 130 +++++++ tests/python/unittest/test_runtime_micro.py | 193 ++++++++++ topi/python/topi/generic/nn.py | 2 +- 33 files changed, 2932 insertions(+), 18 deletions(-) create mode 100644 cmake/modules/Micro.cmake create mode 100644 include/tvm/runtime/micro/utvm_device_lib.h create mode 100644 python/tvm/contrib/binutil.py create mode 100644 python/tvm/micro/__init__.py create mode 100644 python/tvm/micro/base.py create mode 100644 python/tvm/micro/cross_compile.py create mode 100644 src/runtime/micro/device/utvm_runtime.c create mode 100644 src/runtime/micro/device/utvm_runtime.h create mode 100644 src/runtime/micro/host_low_level_device.cc create mode 100644 src/runtime/micro/low_level_device.h create mode 100644 src/runtime/micro/micro_common.cc create mode 100644 src/runtime/micro/micro_common.h create mode 100644 src/runtime/micro/micro_device_api.cc create mode 100644 src/runtime/micro/micro_module.cc create mode 100644 src/runtime/micro/micro_session.cc create mode 100644 src/runtime/micro/micro_session.h create mode 100644 src/runtime/micro/openocd_low_level_device.cc create mode 100644 src/runtime/micro/target_data_layout_encoder.h create mode 100644 tests/python/contrib/test_binutil.py create mode 100644 tests/python/unittest/test_runtime_micro.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 6500ba013e28f..53df11dcd8a24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ tvm_option(USE_RELAY_DEBUG "Building Relay in debug mode..." OFF) tvm_option(USE_SGX "Build with SGX" OFF) tvm_option(USE_RTTI "Build with RTTI" ON) tvm_option(USE_MSVC_MT "Build with MT" OFF) +tvm_option(USE_MICRO "Build with Micro" OFF) tvm_option(INSTALL_DEV "Install compiler infrastructure" OFF) tvm_option(HIDE_PRIVATE_SYMBOLS "Compile with -fvisibility=hidden." OFF) @@ -208,6 +209,7 @@ include(cmake/modules/Metal.cmake) include(cmake/modules/ROCM.cmake) include(cmake/modules/SGX.cmake) include(cmake/modules/LLVM.cmake) +include(cmake/modules/Micro.cmake) include(cmake/modules/ANTLR.cmake) include(cmake/modules/contrib/BLAS.cmake) include(cmake/modules/contrib/Random.cmake) diff --git a/cmake/config.cmake b/cmake/config.cmake index 6239bc4e6dce3..97173eec04b78 100644 --- a/cmake/config.cmake +++ b/cmake/config.cmake @@ -62,6 +62,9 @@ set(USE_VULKAN OFF) # Whether enable OpenGL runtime set(USE_OPENGL OFF) +# Whether enable Micro runtime +set(USE_MICRO OFF) + # Whether to enable SGX runtime # # Possible values for USE_SGX: diff --git a/cmake/modules/Micro.cmake b/cmake/modules/Micro.cmake new file mode 100644 index 0000000000000..edb5063fe68cc --- /dev/null +++ b/cmake/modules/Micro.cmake @@ -0,0 +1,22 @@ +# 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. + +if(USE_MICRO) + message(STATUS "Build with Micro support") + file(GLOB RUNTIME_MICRO_SRCS src/runtime/micro/*.cc) + list(APPEND RUNTIME_SRCS ${RUNTIME_MICRO_SRCS}) +endif(USE_MICRO) diff --git a/include/tvm/runtime/c_runtime_api.h b/include/tvm/runtime/c_runtime_api.h index ba2c0d2291b68..0bf032d31ce31 100644 --- a/include/tvm/runtime/c_runtime_api.h +++ b/include/tvm/runtime/c_runtime_api.h @@ -81,6 +81,7 @@ typedef enum { kDLAOCL = 5, kDLSDAccel = 6, kOpenGL = 11, + kDLMicroDev = 13, // AddExtraTVMType which is not in DLPack here } TVMDeviceExtType; diff --git a/include/tvm/runtime/device_api.h b/include/tvm/runtime/device_api.h index 6986e62475fd2..68029c13cb935 100644 --- a/include/tvm/runtime/device_api.h +++ b/include/tvm/runtime/device_api.h @@ -215,6 +215,7 @@ inline const char* DeviceName(int type) { case kDLROCM: return "rocm"; case kOpenGL: return "opengl"; case kDLExtDev: return "ext_dev"; + case kDLMicroDev: return "micro_dev"; default: LOG(FATAL) << "unknown type =" << type; return "Unknown"; } } diff --git a/include/tvm/runtime/micro/utvm_device_lib.h b/include/tvm/runtime/micro/utvm_device_lib.h new file mode 100644 index 0000000000000..45ea3b559bdc8 --- /dev/null +++ b/include/tvm/runtime/micro/utvm_device_lib.h @@ -0,0 +1,71 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file utvm_device_lib.h + * \brief utvm device library definitions + */ +#ifndef TVM_RUNTIME_MICRO_UTVM_DEVICE_LIB_H_ +#define TVM_RUNTIME_MICRO_UTVM_DEVICE_LIB_H_ + +#ifdef __cplusplus +extern "C" { +#endif +#include + +void *(*TVMBackendAllocWorkspace_)(int, int, uint64_t, int, int) = + (void *(*)(int, int, uint64_t, int, int)) 1; +int (*TVMBackendFreeWorkspace_)(int, int, void*) = (int (*)(int, int, void*)) 1; +void (*TVMAPISetLastError_)(const char*) = (void (*)(const char*)) 1; + +void* TVMBackendAllocWorkspace(int device_type, int device_id, uint64_t size, + int dtype_code_hint, int dtype_bits_hint) { + return (*TVMBackendAllocWorkspace_)(device_type, device_id, size, dtype_code_hint, + dtype_bits_hint); +} + +int TVMBackendFreeWorkspace(int device_type, int device_id, void* ptr) { + return (*TVMBackendFreeWorkspace_)(device_type, device_id, ptr); +} + +void TVMAPISetLastError(const char* msg) { + (*TVMAPISetLastError_)(msg); +} + +float min(float a, float b) { + if (a < b) { + return a; + } else { + return b; + } +} + +float max(float a, float b) { // NOLINT(*) + if (a > b) { + return a; + } else { + return b; + } +} + +#ifdef __cplusplus +} // TVM_EXTERN_C +#endif +#endif // TVM_RUNTIME_MICRO_UTVM_DEVICE_LIB_H_ diff --git a/python/tvm/__init__.py b/python/tvm/__init__.py index 5765eed0ad8ba..56b8b3d9d2989 100644 --- a/python/tvm/__init__.py +++ b/python/tvm/__init__.py @@ -42,7 +42,7 @@ from . import ndarray as nd from .ndarray import context, cpu, gpu, opencl, cl, vulkan, metal, mtl -from .ndarray import vpi, rocm, opengl, ext_dev +from .ndarray import vpi, rocm, opengl, ext_dev, micro_dev from ._ffi.runtime_ctypes import TypeCode, TVMType from ._ffi.ndarray import TVMContext diff --git a/python/tvm/_ffi/runtime_ctypes.py b/python/tvm/_ffi/runtime_ctypes.py index 72cff1a10eadb..a418ea7c523ba 100644 --- a/python/tvm/_ffi/runtime_ctypes.py +++ b/python/tvm/_ffi/runtime_ctypes.py @@ -143,6 +143,7 @@ class TVMContext(ctypes.Structure): 10: 'rocm', 11: 'opengl', 12: 'ext_dev', + 13: 'micro_dev', } STR2MASK = { 'llvm': 1, @@ -163,6 +164,7 @@ class TVMContext(ctypes.Structure): 'rocm': 10, 'opengl': 11, 'ext_dev': 12, + 'micro_dev': 13, } def __init__(self, device_type, device_id): super(TVMContext, self).__init__() diff --git a/python/tvm/contrib/binutil.py b/python/tvm/contrib/binutil.py new file mode 100644 index 0000000000000..e2d6456c256f4 --- /dev/null +++ b/python/tvm/contrib/binutil.py @@ -0,0 +1,233 @@ +# 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. + +"""Utilities for binary file manipulation""" +import os +import subprocess +from . import util +from .._ffi.base import py_str +from ..api import register_func + + +@register_func("tvm_callback_get_section_size") +def tvm_callback_get_section_size(binary_path, section_name): + """Finds size of the section in the binary. + Assumes `size` shell command exists (typically works only on Linux machines) + + Parameters + ---------- + binary_path : str + path of the binary file + + section_name : str + name of section + + Return + ------ + size : integer + size of the section in bytes + """ + if not os.path.isfile(binary_path): + raise RuntimeError("no such file \"{}\"".format(binary_path)) + # We use the "-A" flag here to get the ".rodata" section's size, which is + # not included by default. + size_proc = subprocess.Popen(["size", "-A", binary_path], stdout=subprocess.PIPE) + (size_output, _) = size_proc.communicate() + if size_proc.returncode != 0: + msg = "error in finding section size:\n" + msg += py_str(out) + raise RuntimeError(msg) + + size_output = size_output.decode("utf-8") + section_size = 0 + # Skip the first two header lines in the `size` output. + for line in size_output.split("\n")[2:]: + tokens = list(filter(lambda s: len(s) != 0, line.split(" "))) + if len(tokens) != 3: + continue + entry_name = tokens[0] + entry_size = int(tokens[1]) + if entry_name.startswith("." + section_name): + # The `.rodata` section should be the only section for which we + # need to collect the size from *multiple* entries in the command + # output. + if section_size != 0 and not entry_name.startswith(".rodata"): + raise RuntimeError( + "multiple entries in `size` output for section {}".format(section_name)) + section_size += entry_size + return section_size + + +@register_func("tvm_callback_relocate_binary") +def tvm_callback_relocate_binary(binary_path, text_addr, rodata_addr, data_addr, bss_addr): + """Relocates sections in the binary to new addresses + + Parameters + ---------- + binary_path : str + path of the binary file + + text_addr : str + text section address + + rodata_addr : str + rodata section address + + data_addr : str + data section address + + bss_addr : str + bss section address + + Return + ------ + rel_bin : bytearray + the relocated binary + """ + tmp_dir = util.tempdir() + rel_obj = tmp_dir.relpath("relocated.o") + ld_script_contents = """ +SECTIONS +{ + . = %s; + . = ALIGN(8); + .text : + { + *(.text) + . = ALIGN(8); + *(.text*) + } + . = %s; + . = ALIGN(8); + .rodata : + { + *(.rodata) + . = ALIGN(8); + *(.rodata*) + } + . = %s; + . = ALIGN(8); + .data : + { + *(.data) + . = ALIGN(8); + *(.data*) + } + . = %s; + . = ALIGN(8); + .bss : + { + *(.bss) + . = ALIGN(8); + *(.bss*) + } +} + """ % (text_addr, rodata_addr, data_addr, bss_addr) + rel_ld_script = tmp_dir.relpath("relocated.lds") + with open(rel_ld_script, "w") as f: + f.write(ld_script_contents) + ld_proc = subprocess.Popen(["ld", binary_path, + "-T", rel_ld_script, + "-o", rel_obj], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + (out, _) = ld_proc.communicate() + if ld_proc.returncode != 0: + msg = "linking error using ld:\n" + msg += py_str(out) + raise RuntimeError(msg) + with open(rel_obj, "rb") as f: + rel_bin = bytearray(f.read()) + return rel_bin + + +@register_func("tvm_callback_read_binary_section") +def tvm_callback_read_binary_section(binary, section): + """Returns the contents of the specified section in the binary byte array + + Parameters + ---------- + binary : bytearray + contents of the binary + + section : str + type of section + + Return + ------ + section_bin : bytearray + contents of the read section + """ + tmp_dir = util.tempdir() + tmp_bin = tmp_dir.relpath("temp.bin") + tmp_section = tmp_dir.relpath("tmp_section.bin") + with open(tmp_bin, "wb") as out_file: + out_file.write(bytes(binary)) + objcopy_proc = subprocess.Popen(["objcopy", "--dump-section", + ".{}={}".format(section, tmp_section), + tmp_bin], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + (out, _) = objcopy_proc.communicate() + if objcopy_proc.returncode != 0: + msg = "error in using objcopy:\n" + msg += py_str(out) + raise RuntimeError(msg) + if os.path.isfile(tmp_section): + # Get section content if it exists. + with open(tmp_section, "rb") as f: + section_bin = bytearray(f.read()) + else: + # Return empty bytearray if the section does not exist. + section_bin = bytearray("", "utf-8") + return section_bin + + +@register_func("tvm_callback_get_symbol_map") +def tvm_callback_get_symbol_map(binary): + """Obtains a map of symbols to addresses in the passed binary + + Parameters + ---------- + binary : bytearray + contents of the binary + + Return + ------ + map_str : str + map of defined symbols to addresses, encoded as a series of + alternating newline-separated keys and values + """ + tmp_dir = util.tempdir() + tmp_obj = tmp_dir.relpath("tmp_obj.bin") + with open(tmp_obj, "wb") as out_file: + out_file.write(bytes(binary)) + nm_proc = subprocess.Popen(["nm", "-C", "--defined-only", tmp_obj], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + (out, _) = nm_proc.communicate() + if nm_proc.returncode != 0: + msg = "error in using nm:\n" + msg += py_str(out) + raise RuntimeError(msg) + out = out.decode("utf8").splitlines() + map_str = "" + for line in out: + line = line.split() + map_str += line[2] + "\n" + map_str += line[0] + "\n" + return map_str diff --git a/python/tvm/micro/__init__.py b/python/tvm/micro/__init__.py new file mode 100644 index 0000000000000..6e2d8154a77b2 --- /dev/null +++ b/python/tvm/micro/__init__.py @@ -0,0 +1,9 @@ +"""uTVM module for bare-metal backends. + +uTVM (or the micro backend) enables provides support for bare-metal devices. +Its targets currently include a host-emulated device which is used for testing, +and JTAG-based openocd device which allows actual interfacing with microdevices. +""" + +from ..contrib import binutil +from .base import Session diff --git a/python/tvm/micro/base.py b/python/tvm/micro/base.py new file mode 100644 index 0000000000000..f1b79fb63c31e --- /dev/null +++ b/python/tvm/micro/base.py @@ -0,0 +1,178 @@ +# 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. + +"""Base definitions for micro.""" + +from __future__ import absolute_import + +import logging +import os + +import tvm.module +from tvm.contrib import graph_runtime, util +from tvm import relay + +from .._ffi.function import _init_api +from .._ffi.libinfo import find_include_path +from .cross_compile import create_lib + +SUPPORTED_DEVICE_TYPES = ["host", "openocd"] + +class Session: + """MicroTVM Session + + Example + -------- + .. code-block:: python + + c_mod = ... # some module generated with "c" as the target + device_type = "host" + with tvm.micro.Session(device_type) as sess: + sess.create_micro_mod(c_mod) + """ + + def __init__(self, device_type, binutil_prefix, port=0): + """Stores parameters for initializing a micro device session. + + The session is not initialized until the constructed object is used + in a `with` block. + + Parameters + ---------- + device_type : str + type of low-level device + + binutil_prefix : str + binutil prefix to be used. For example, a prefix of + "riscv64-unknown-elf-" means "riscv64-unknown-elf-gcc" is used as + the compiler and "riscv64-unknown-elf-ld" is used as the linker, + etc. + + port : integer, optional + port number of OpenOCD server + """ + if device_type not in SUPPORTED_DEVICE_TYPES: + raise RuntimeError("unknown micro device type \"{}\"".format(device_type)) + + self.device_type = device_type + self.binutil_prefix = binutil_prefix + self.port = port + + def micro_build(self, func: relay.Function, params={}): + """Create a graph runtime module with a micro device context.""" + with tvm.build_config(disable_vectorize=True): + with relay.build_config(opt_level=3): + graph, c_mod, params = relay.build(func, target="c", params=params) + + micro_mod = self.create_micro_mod(c_mod) + ctx = tvm.micro_dev(0) + mod = graph_runtime.create(graph, micro_mod, ctx) + return mod, params + + def create_micro_mod(self, c_mod): + """Produces a micro module from a given module. + + Parameters + ---------- + c_mod : tvm.module.Module + module with "c" as its target backend + + device_type : str + type of low-level device to target + + Return + ------ + micro_mod : tvm.module.Module + micro module for the target device + """ + temp_dir = util.tempdir() + # Save module source to temp file. + lib_src_path = temp_dir.relpath("dev_lib.c") + mod_src = c_mod.get_source() + with open(lib_src_path, "w") as f: + f.write(mod_src) + # Compile to object file. + lib_obj_path = self.create_micro_lib(lib_src_path) + micro_mod = tvm.module.load(lib_obj_path, "micro_dev") + return micro_mod + + def create_micro_lib(self, src_path, obj_path=None): + """Compiles code into a binary for the target micro device. + + Parameters + ---------- + src_path : str + path to source file + + obj_path : str, optional + path to generated object file (defaults to same directory as + `src_path`) + + Return + ------ + obj_path : bytearray + compiled binary file path (will match input `obj_path`, if it was specified) + """ + def replace_suffix(s, new_suffix): + if "." in os.path.basename(s): + # There already exists an extension. + return os.path.join( + os.path.dirname(s), + ".".join(os.path.basename(s).split(".")[:-1] + [new_suffix])) + # No existing extension; we can just append. + return s + "." + new_suffix + + if obj_path is None: + obj_name = replace_suffix(src_path, "obj") + obj_path = os.path.join(os.path.dirname(src_path), obj_name) + # uTVM object files cannot have an ".o" suffix, because it triggers the + # code path for creating shared objects in `tvm.module.load`. So we replace + # ".o" suffixes with ".obj". + if obj_path.endswith(".o"): + logging.warning( + "\".o\" suffix in \"%s\" has been replaced with \".obj\"", obj_path) + obj_path = replace_suffix(obj_path, "obj") + + options = ["-I" + path for path in find_include_path()] + ["-fno-stack-protector"] + # TODO(weberlo): Consolidate `create_lib` and `contrib.cc.cross_compiler` + create_lib(obj_path, src_path, options, self._compile_cmd()) + return obj_path + + def _compile_cmd(self): + return "{}gcc".format(self.binutil_prefix) + + def __enter__(self): + # First, find and compile runtime library. + micro_dir = os.path.dirname(os.path.realpath(os.path.expanduser(__file__))) + micro_device_dir = os.path.join(micro_dir, "..", "..", "..", + "src", "runtime", "micro", "device") + runtime_src_path = os.path.join(micro_device_dir, "utvm_runtime.c") + tmp_dir = util.tempdir() + runtime_lib_path = tmp_dir.relpath("utvm_runtime.obj") + runtime_lib_path = self.create_micro_lib(runtime_src_path, obj_path=runtime_lib_path) + + # Then, initialize the session (includes loading the compiled runtime lib). + _InitSession(self.device_type, runtime_lib_path, self.port) + + # Return `self` to bind the session as a variable in the `with` block. + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + _EndSession() + + +_init_api("tvm.micro", "tvm.micro.base") diff --git a/python/tvm/micro/cross_compile.py b/python/tvm/micro/cross_compile.py new file mode 100644 index 0000000000000..b863646c5bd7a --- /dev/null +++ b/python/tvm/micro/cross_compile.py @@ -0,0 +1,62 @@ +# 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. + +"""Cross compilation for MicroTVM""" + +from __future__ import absolute_import + +import subprocess + +from .._ffi.function import _init_api +from .._ffi.base import py_str + + +def create_lib(output, sources, options=None, compile_cmd="gcc"): + """Compiles source code into a binary object file + + Parameters + ---------- + output : str + target library path + + sources : list + list of source files to be compiled + + options: list + list of additional option strings + + compile_cmd : str, optional + compiler string + """ + cmd = [compile_cmd] + cmd += ["-c"] + cmd += ["-o", output] + if isinstance(sources, str): + cmd += [sources] + else: + cmd += sources + if options: + cmd += options + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (out, _) = proc.communicate() + if proc.returncode != 0: + msg = "Error in compilation:\n" + msg += py_str(out) + raise RuntimeError(msg) + + +_init_api("tvm.micro.cross_compile") diff --git a/python/tvm/ndarray.py b/python/tvm/ndarray.py index e6c911576e64a..9a00f78eb77fa 100644 --- a/python/tvm/ndarray.py +++ b/python/tvm/ndarray.py @@ -189,6 +189,22 @@ def ext_dev(dev_id=0): return TVMContext(12, dev_id) +def micro_dev(dev_id=0): + """Construct a micro device + + Parameters + ---------- + dev_id : int, optional + The integer device id + + Returns + ------- + ctx : TVMContext + The created context + """ + return TVMContext(13, dev_id) + + cl = opencl mtl = metal diff --git a/src/api/api_pass.cc b/src/api/api_pass.cc index e5b003cafb870..5a81d6fb5e10c 100644 --- a/src/api/api_pass.cc +++ b/src/api/api_pass.cc @@ -6,9 +6,9 @@ * 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 diff --git a/src/codegen/codegen_c.cc b/src/codegen/codegen_c.cc index 19f7a270b8652..b762ca898adc1 100644 --- a/src/codegen/codegen_c.cc +++ b/src/codegen/codegen_c.cc @@ -6,9 +6,9 @@ * 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 @@ -443,7 +443,23 @@ inline void PrintBinaryExpr(const T* op, } } -inline void PrintBinaryIntrinsitc(const Call* op, +template +inline void PrintTernaryCondExpr(const T* op, + const char* compare, + std::ostream& os, // NOLINT(*) + CodeGenC* p) { + os << "("; + p->PrintExpr(op->a, os); + os << ") " << compare << " ("; + p->PrintExpr(op->b, os); + os << ") ? ("; + p->PrintExpr(op->a, os); + os << ") : ("; + p->PrintExpr(op->b, os); + os << ")"; +} + +inline void PrintBinaryIntrinsic(const Call* op, const char *opstr, std::ostream& os, // NOLINT(*) CodeGenC* p) { @@ -482,10 +498,10 @@ void CodeGenC::VisitExpr_(const Mod *op, std::ostream& os) { // NOLINT(*) PrintBinaryExpr(op, "%", os, this); } void CodeGenC::VisitExpr_(const Min *op, std::ostream& os) { // NOLINT(*) - PrintBinaryExpr(op, "min", os, this); + PrintTernaryCondExpr(op, "<", os, this); } void CodeGenC::VisitExpr_(const Max *op, std::ostream& os) { // NOLINT(*) - PrintBinaryExpr(op, "max", os, this); + PrintTernaryCondExpr(op, ">", os, this); } void CodeGenC::VisitExpr_(const EQ *op, std::ostream& os) { // NOLINT(*) PrintBinaryExpr(op, "==", os, this); @@ -528,20 +544,20 @@ void CodeGenC::VisitExpr_(const Call *op, std::ostream& os) { // NOLINT(*) } os << ")"; } else if (op->is_intrinsic(Call::bitwise_and)) { - PrintBinaryIntrinsitc(op, " & ", os, this); + PrintBinaryIntrinsic(op, " & ", os, this); } else if (op->is_intrinsic(Call::bitwise_xor)) { - PrintBinaryIntrinsitc(op, " ^ ", os, this); + PrintBinaryIntrinsic(op, " ^ ", os, this); } else if (op->is_intrinsic(Call::bitwise_or)) { - PrintBinaryIntrinsitc(op, " | ", os, this); + PrintBinaryIntrinsic(op, " | ", os, this); } else if (op->is_intrinsic(Call::bitwise_not)) { CHECK_EQ(op->args.size(), 1U); os << "(~"; this->PrintExpr(op->args[0], os); os << ')'; } else if (op->is_intrinsic(Call::shift_left)) { - PrintBinaryIntrinsitc(op, " << ", os, this); + PrintBinaryIntrinsic(op, " << ", os, this); } else if (op->is_intrinsic(Call::shift_right)) { - PrintBinaryIntrinsitc(op, " >> ", os, this); + PrintBinaryIntrinsic(op, " >> ", os, this); } else if (op->is_intrinsic(intrinsic::tvm_if_then_else)) { os << "("; PrintExpr(op->args[0], os); diff --git a/src/codegen/codegen_c_host.cc b/src/codegen/codegen_c_host.cc index ca7b070a97c70..3c869d2b5ca5b 100644 --- a/src/codegen/codegen_c_host.cc +++ b/src/codegen/codegen_c_host.cc @@ -31,13 +31,16 @@ namespace tvm { namespace codegen { CodeGenCHost::CodeGenCHost() { - module_name = GetUniqueName("__tvm_module_ctx"); + module_name_ = GetUniqueName("__tvm_module_ctx"); } void CodeGenCHost::Init(bool output_ssa) { decl_stream << "#include \"tvm/runtime/c_runtime_api.h\"\n"; decl_stream << "#include \"tvm/runtime/c_backend_api.h\"\n"; - decl_stream << "extern void* " << module_name << " = NULL;\n"; + // TODO(weberlo): Make this line conditioned on whether or not we're + // generating this for uTVM purposes. + decl_stream << "#include \"tvm/runtime/micro/utvm_device_lib.h\"\n"; + decl_stream << "extern void* " << module_name_ << " = NULL;\n"; CodeGenC::Init(output_ssa); } @@ -159,7 +162,7 @@ void CodeGenCHost::PrintGetFuncFromBackend(std::string func_name, std::string pa this->stream << "if (" << packed_func_name << " == NULL) {\n"; int packed_func_if_scope = this->BeginScope(); this->PrintIndent(); - this->stream << "if (TVMBackendGetFuncFromEnv(" << module_name + this->stream << "if (TVMBackendGetFuncFromEnv(" << module_name_ << ", \"" << func_name << "\"" << ", &" << packed_func_name << ") != 0) {\n"; int get_func_env_scope = this->BeginScope(); diff --git a/src/codegen/codegen_c_host.h b/src/codegen/codegen_c_host.h index 23ae185512e1f..a4eedb050c393 100644 --- a/src/codegen/codegen_c_host.h +++ b/src/codegen/codegen_c_host.h @@ -48,7 +48,8 @@ class CodeGenCHost final : public CodeGenC { void VisitStmt_(const AssertStmt *op) final; // NOLINT(*) private: - std::string module_name; + std::string module_name_; + void PrintGetFuncFromBackend(std::string func_name, std::string packed_func_name); void PrintFuncCall(std::string packed_func_name, int num_args); }; diff --git a/src/runtime/micro/device/utvm_runtime.c b/src/runtime/micro/device/utvm_runtime.c new file mode 100644 index 0000000000000..81f6ce4de9d79 --- /dev/null +++ b/src/runtime/micro/device/utvm_runtime.c @@ -0,0 +1,80 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file utvm_runtime.cc + * \brief micro device init stub + */ +#include "utvm_runtime.h" + +// Task pointers must be patched before calling a function. +UTVMTask task; + +// We use a dummy function to signal execution is finished for device +// backends which require breakpoints. +void UTVMDone() {} + +void UTVMMain() { + task.func((void*) task.args->values, (void*) task.args->type_codes, // NOLINT(*) + task.args->num_args); + UTVMDone(); +} + +// TODO(weberlo): Writes fail to pointer variables if they're initialized to +// `NULL`. Why? + +// These pointers are patched at load time to point to the workspace section. +char *utvm_workspace_begin = (char*) 1; // NOLINT(*) +char *utvm_workspace_curr = (char*) 1; // NOLINT(*) +// Keep track of how many active allocations there are on the workspace. +size_t num_active_allocs = 0; + +const char *last_error = (char*) 1; // NOLINT(*) + +void* TVMBackendAllocWorkspace(int device_type, int device_id, uint64_t size, + int dtype_code_hint, int dtype_bits_hint) { + // Align up to 8 bytes. + utvm_workspace_curr += (8 - ((uintptr_t) utvm_workspace_curr % 8)) % 8; // NOLINT(*) + void* ret_ptr = (void*) utvm_workspace_curr; // NOLINT(*) + utvm_workspace_curr += size; + num_active_allocs++; + return ret_ptr; +} + +int TVMBackendFreeWorkspace(int device_type, int device_id, void* ptr) { + num_active_allocs--; + if (num_active_allocs < 0) { + TVMAPISetLastError("free called with no active workspace allocations"); + // Reset allocations and workspace (for future task executions). + num_active_allocs = 0; + utvm_workspace_curr = utvm_workspace_begin; + return -1; + } else if (num_active_allocs == 0) { + // No more allocations. Reset workspace. + utvm_workspace_curr = utvm_workspace_begin; + return 0; + } else { + return 0; + } +} + +void TVMAPISetLastError(const char* msg) { + last_error = msg; +} diff --git a/src/runtime/micro/device/utvm_runtime.h b/src/runtime/micro/device/utvm_runtime.h new file mode 100644 index 0000000000000..cc941b8d7a323 --- /dev/null +++ b/src/runtime/micro/device/utvm_runtime.h @@ -0,0 +1,98 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file utvm_runtime.h + * \brief utvm runtime headers + */ +#ifndef TVM_RUNTIME_MICRO_DEVICE_UTVM_RUNTIME_H_ +#define TVM_RUNTIME_MICRO_DEVICE_UTVM_RUNTIME_H_ + +#ifdef __cplusplus +extern "C" { +#endif +#include +#include + +/*! + * \brief POD variant of TVMArgs + */ +typedef struct { + /*! \brief Array of values */ + TVMValue* values; + /*! \brief Array of type codes for each value */ + int* type_codes; + /*! \brief Number of arguments */ + int32_t num_args; +} UTVMArgs; + +/*! + * \brief Task structure for uTVM + */ +typedef struct { + /*! \brief Pointer to function to call for this task */ + void (*func)(void*, void*, int32_t); + /*! \brief Arguments for this task's function call */ + UTVMArgs* args; +} UTVMTask; + +/*! + * \brief Backend function to allocate temporal workspace. + * + * \note The result allocate spaced is ensured to be aligned to kTempAllocaAlignment. + * + * \param nbytes The size of the space requested. + * \param device_type The device type which the space will be allocated. + * \param device_id The device id which the space will be allocated. + * \param dtype_code_hint The type code of the array elements. Only used in + * certain backends such as OpenGL. + * \param dtype_bits_hint The type bits of the array elements. Only used in + * certain backends such as OpenGL. + * \return nullptr when error is thrown, a valid ptr if success + */ +void* TVMBackendAllocWorkspace(int device_type, + int device_id, + uint64_t size, + int dtype_code_hint, + int dtype_bits_hint); + +/*! + * \brief Backend function to free temporal workspace. + * + * \param ptr The result allocated space pointer. + * \param device_type The device type which the space will be allocated. + * \param device_id The device id which the space will be allocated. + * \return 0 when no error is thrown, -1 when failure happens + * + * \sa TVMBackendAllocWorkspace + */ +int TVMBackendFreeWorkspace(int device_type, int device_id, void* ptr); + +/*! + * \brief Used for implementing C API function. + * Set last error message before return. + * \param msg The error message to be set. + */ +void TVMAPISetLastError(const char* msg); + +#ifdef __cplusplus +} // TVM_EXTERN_C +#endif +#endif // TVM_RUNTIME_MICRO_DEVICE_UTVM_RUNTIME_H_ diff --git a/src/runtime/micro/host_low_level_device.cc b/src/runtime/micro/host_low_level_device.cc new file mode 100644 index 0000000000000..230db53024fd4 --- /dev/null +++ b/src/runtime/micro/host_low_level_device.cc @@ -0,0 +1,101 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file host_low_level_device.cc + * \brief emulated low-level micro device implementation on host machine + */ + +#include +#include +#include "low_level_device.h" +#include "micro_common.h" + +namespace tvm { +namespace runtime { +/*! + * \brief emulated low-level device on host machine + */ +class HostLowLevelDevice final : public LowLevelDevice { + public: + /*! + * \brief constructor to initialize on-host memory region to act as device + * \param num_bytes size of the emulated on-device memory region + */ + explicit HostLowLevelDevice(size_t num_bytes) + : size_(num_bytes) { + size_t size_in_pages = (num_bytes + kPageSize - 1) / kPageSize; + // TODO(weberlo): Set permissions per section (e.g., read-write perms for + // the heap, execute perms for text, etc.). + int mmap_prot = PROT_READ | PROT_WRITE | PROT_EXEC; + int mmap_flags = MAP_ANONYMOUS | MAP_PRIVATE; + base_addr_ = DevBaseAddr( + (reinterpret_cast( + mmap(nullptr, size_in_pages * kPageSize, mmap_prot, mmap_flags, -1, 0)))); + } + + /*! + * \brief destructor to deallocate on-host device region + */ + ~HostLowLevelDevice() { + munmap(base_addr_.cast_to(), size_); + } + + void Write(DevBaseOffset offset, + void* buf, + size_t num_bytes) final { + void* addr = (offset + base_addr_).cast_to(); + std::memcpy(addr, buf, num_bytes); + } + + void Read(DevBaseOffset offset, + void* buf, + size_t num_bytes) final { + void* addr = (offset + base_addr_).cast_to(); + std::memcpy(buf, addr, num_bytes); + } + + void Execute(DevBaseOffset func_offset, DevBaseOffset breakpoint) final { + DevAddr func_addr = func_offset + base_addr_; + reinterpret_cast(func_addr.value())(); + } + + DevBaseAddr base_addr() const final { + return base_addr_; + } + + const char* device_type() const final { + return "host"; + } + + private: + /*! \brief base address of the micro device memory region */ + DevBaseAddr base_addr_; + /*! \brief size of memory region */ + size_t size_; +}; + +const std::shared_ptr HostLowLevelDeviceCreate(size_t num_bytes) { + std::shared_ptr lld = + std::make_shared(num_bytes); + return lld; +} +} // namespace runtime +} // namespace tvm diff --git a/src/runtime/micro/low_level_device.h b/src/runtime/micro/low_level_device.h new file mode 100644 index 0000000000000..9b5591ecc46c9 --- /dev/null +++ b/src/runtime/micro/low_level_device.h @@ -0,0 +1,95 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file low_level_device.h + * \brief Abstract low-level micro device management + */ +#ifndef TVM_RUNTIME_MICRO_LOW_LEVEL_DEVICE_H_ +#define TVM_RUNTIME_MICRO_LOW_LEVEL_DEVICE_H_ + +#include + +#include "micro_common.h" + +namespace tvm { +namespace runtime { +/*! + * \brief virtual interface for low-level micro device management + */ +class LowLevelDevice { + public: + /*! \brief virtual destructor */ + virtual ~LowLevelDevice() {} + + /*! + * \brief writes num_bytes from buffer to device memory at base_addr + offset + * \param offset on-device memory offset pointer to be written to + * \param buffer on-host buffer to be written + * \param num_bytes number of bytes to be written + */ + virtual void Write(DevBaseOffset offset, + void* buffer, + size_t num_bytes) = 0; + + /*! + * \brief reads num_bytes from device memory at base_addr + offset into buffer + * \param offset on-device memory offset pointer to be read from + * \param buffer on-host buffer to be read into + * \param num_bytes number of bytes to be read + */ + virtual void Read(DevBaseOffset offset, + void* buffer, + size_t num_bytes) = 0; + + /*! + * \brief starts execution of device at offset + * \param func_addr offset of the init stub function + * \param breakpoint breakpoint at which to stop function execution + */ + virtual void Execute(DevBaseOffset func_offset, DevBaseOffset breakpoint) = 0; + + /*! + * \brief getter function for base_addr + * \return the base address of the device memory region + */ + virtual DevBaseAddr base_addr() const = 0; + + /*! + * \brief getter function for low-level device type + * \return string containing device type + */ + virtual const char* device_type() const = 0; +}; + +/*! + * \brief create a host low-level device + * \param num_bytes size of the memory region + */ +const std::shared_ptr HostLowLevelDeviceCreate(size_t num_bytes); + +/*! + * \brief connect to OpenOCD and create an OpenOCD low-level device + * \param port port of the OpenOCD server to connect to + */ +const std::shared_ptr OpenOCDLowLevelDeviceCreate(int port); +} // namespace runtime +} // namespace tvm +#endif // TVM_RUNTIME_MICRO_LOW_LEVEL_DEVICE_H_ diff --git a/src/runtime/micro/micro_common.cc b/src/runtime/micro/micro_common.cc new file mode 100644 index 0000000000000..2d03b76968ba3 --- /dev/null +++ b/src/runtime/micro/micro_common.cc @@ -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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file micro_common.cc + * \brief common utilties for uTVM + */ + +#include +#include +#include +#include +#include +#include +#include "micro_session.h" +#include "micro_common.h" + +namespace tvm { +namespace runtime { + +DevBaseOffset DevAddr::operator-(DevBaseAddr base) { + return DevBaseOffset(value_ - base.value()); +} + +DevAddr DevAddr::operator+(size_t n) { + return DevAddr(value_ + n); +} + +DevAddr DevBaseAddr::operator+(DevBaseOffset offset) { + return DevAddr(value_ + offset.value()); +} + +DevAddr DevBaseOffset::operator+(DevBaseAddr base) { + return DevAddr(value_ + base.value()); +} + +DevBaseOffset DevBaseOffset::operator+(size_t n) { + return DevBaseOffset(value_ + n); +} + +const char* SectionToString(SectionKind section) { + switch (section) { + case SectionKind::kText: return "text"; + case SectionKind::kRodata: return "rodata"; + case SectionKind::kData: return "data"; + case SectionKind::kBss: return "bss"; + case SectionKind::kArgs: return "args"; + case SectionKind::kStack: return "stack"; + case SectionKind::kHeap: return "heap"; + case SectionKind::kWorkspace: return "workspace"; + default: return ""; + } +} + +static std::string AddrToString(void* addr) { + std::stringstream stream; + if (addr != nullptr) + stream << addr; + else + stream << "0x0"; + std::string string_addr = stream.str(); + return string_addr; +} + +std::string RelocateBinarySections(const std::string& binary_path, + DevAddr text, + DevAddr rodata, + DevAddr data, + DevAddr bss) { + const auto* f = Registry::Get("tvm_callback_relocate_binary"); + CHECK(f != nullptr) + << "Require tvm_callback_relocate_binary to exist in registry"; + std::string relocated_bin = (*f)(binary_path, + AddrToString(text.cast_to()), + AddrToString(rodata.cast_to()), + AddrToString(data.cast_to()), + AddrToString(bss.cast_to())); + return relocated_bin; +} + +std::string ReadSection(const std::string& binary, SectionKind section) { + CHECK(section == SectionKind::kText || section == SectionKind::kRodata || + section == SectionKind::kData || section == SectionKind::kBss) + << "ReadSection requires section to be one of text, rodata, data, or bss."; + const auto* f = Registry::Get("tvm_callback_read_binary_section"); + CHECK(f != nullptr) + << "Require tvm_callback_read_binary_section to exist in registry"; + TVMByteArray arr; + arr.data = &binary[0]; + arr.size = binary.length(); + std::string section_contents = (*f)(arr, SectionToString(section)); + return section_contents; +} + +size_t GetSectionSize(const std::string& binary_path, SectionKind section, size_t align) { + CHECK(section == SectionKind::kText || section == SectionKind::kRodata || + section == SectionKind::kData || section == SectionKind::kBss) + << "GetSectionSize requires section to be one of text, rodata, data, or bss."; + const auto* f = Registry::Get("tvm_callback_get_section_size"); + CHECK(f != nullptr) + << "Require tvm_callback_get_section_size to exist in registry"; + size_t size = (*f)(binary_path, SectionToString(section)); + size = UpperAlignValue(size, align); + return size; +} +} // namespace runtime +} // namespace tvm diff --git a/src/runtime/micro/micro_common.h b/src/runtime/micro/micro_common.h new file mode 100644 index 0000000000000..f7e6c67ca337f --- /dev/null +++ b/src/runtime/micro/micro_common.h @@ -0,0 +1,316 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file micro_common.h + */ +#ifndef TVM_RUNTIME_MICRO_MICRO_COMMON_H_ +#define TVM_RUNTIME_MICRO_MICRO_COMMON_H_ + +#include + +#include + +#include +#include +#include + +namespace tvm { +namespace runtime { +/*! + * \brief enum of device memory region sections + */ +enum class SectionKind : int { + kText = 0, + kRodata = 1, + kData = 2, + kBss = 3, + kArgs = 4, + kStack = 5, + kHeap = 6, + kWorkspace = 7, +}; + +// TODO(weberlo): Do we only need a device location class? Think about pros/cons. +// It seems that offsets don't semantically fit in the class of device pointers. +// But the type safety guarantees from having all three subclasses is very +// helpful. `DevBaseOffset` is the weirdest to have as a subclass, because it's +// not an address. + +/*! \brief Base class for interfacing with device locations (pointers/offsets) */ +class DeviceLocation { + public: + /*! \brief construct a location with value `value` */ + explicit DeviceLocation(std::uintptr_t value) : value_(value) {} + + /*! \brief default constructor */ + DeviceLocation() : value_(0) {} + + /*! \brief construct a null location */ + explicit DeviceLocation(std::nullptr_t value) : value_(0) {} + + /*! \brief destructor */ + virtual ~DeviceLocation() {} + + /*! + * \brief get value of location + * \return value of location + */ + std::uintptr_t value() const { return value_; } + + /*! + * \brief cast location to type `T` + * \return casted result + */ + template + T cast_to() const { return reinterpret_cast(value_); } + + /*! \brief check if location is null */ + bool operator==(std::nullptr_t) const { return value_ == 0; } + /*! \brief check if location is not null */ + bool operator!=(std::nullptr_t) const { return value_ != 0; } + + protected: + /*! \brief raw value storing the location */ + std::uintptr_t value_; +}; + +class DevAddr; +class DevBaseAddr; +class DevBaseOffset; + +/*! \brief absolute device address */ +class DevAddr : public DeviceLocation { + public: + /*! \brief construct an absolute address with value `value` */ + explicit DevAddr(std::uintptr_t val) : DeviceLocation(val) {} + + /*! \brief default constructor */ + DevAddr() : DeviceLocation() {} + + /*! \brief construct a null absolute address */ + explicit DevAddr(std::nullptr_t val) : DeviceLocation(val) {} + + /*! \brief subtract a base address from an absolute address to get a base offset */ + DevBaseOffset operator-(DevBaseAddr base); + + /*! \brief add an integer to an absolute address to get an absolute address */ + DevAddr operator+(size_t n); +}; + +/*! \brief base address of the device */ +class DevBaseAddr : public DeviceLocation { + public: + /*! \brief construct a base address with value `value` */ + explicit DevBaseAddr(std::uintptr_t value) : DeviceLocation(value) {} + + /*! \brief default constructor */ + DevBaseAddr() : DeviceLocation() {} + + /*! \brief construct a null base address */ + explicit DevBaseAddr(std::nullptr_t value) : DeviceLocation(value) {} + + /*! \brief add a base address with a base offset to get an absolute address */ + DevAddr operator+(DevBaseOffset offset); +}; + +/*! \brief offset from device base address */ +class DevBaseOffset : public DeviceLocation { + public: + /*! \brief construct a base offset with value `value` */ + explicit DevBaseOffset(std::uintptr_t value) : DeviceLocation(value) {} + + /*! \brief default constructor */ + DevBaseOffset() : DeviceLocation() {} + + /*! \brief construct a null base offset */ + explicit DevBaseOffset(std::nullptr_t value) : DeviceLocation(value) {} + + /*! \brief add a base offset to a base address to get an absolute address */ + DevAddr operator+(DevBaseAddr base); + + /*! \brief add an integer to a base offset to increase the offset */ + DevBaseOffset operator+(size_t n); +}; + +/*! + * \brief map from symbols to their on-device offsets + */ +class SymbolMap { + public: + /*! + * \brief default constructor + */ + SymbolMap() {} + + /*! + * \brief constructor that builds the mapping + * \param binary contents of binary object file + * \param base_addr base address of the target device + */ + SymbolMap(const std::string& binary, DevBaseAddr base_addr) { + const auto* f = Registry::Get("tvm_callback_get_symbol_map"); + CHECK(f != nullptr) << "require tvm_callback_get_symbol_map to exist in registry"; + TVMByteArray arr; + arr.data = &binary[0]; + arr.size = binary.length(); + std::string map_str = (*f)(arr); + // Parse symbols and addresses from returned string. + std::stringstream stream; + stream << map_str; + std::string name; + std::uintptr_t addr; + stream >> name; + stream >> std::hex >> addr; + while (stream) { + map_[name] = DevAddr(addr) - base_addr; + stream >> name; + stream >> std::hex >> addr; + } + } + + /*! + * \brief retrieve on-device offset for a symbol name + * \param name name of the symbol + * \return on-device offset of the symbol + */ + DevBaseOffset operator[](const std::string& name) { + auto result = map_.find(name); + CHECK(result != map_.end()) << "\"" << name << "\" not in symbol map"; + return result->second; + } + + private: + /*! \brief backing map */ + std::unordered_map map_; +}; + +/*! \brief struct containing section location info */ +struct SectionLocation { + /*! \brief section start offset */ + DevBaseOffset start; + /*! \brief size of section */ + size_t size; +}; + +/*! \brief struct containing section locations and symbol mappings */ +struct BinaryInfo { + /*! \brief text section location */ + SectionLocation text; + /*! \brief rodata section location */ + SectionLocation rodata; + /*! \brief data section location */ + SectionLocation data; + /*! \brief bss section location */ + SectionLocation bss; + /*! \brief symbol map to offsets */ + SymbolMap symbol_map; +}; + +// TODO(weberlo): should this be here? +/*! \brief number of bytes in each page */ +constexpr int kPageSize = 4096; + +// TODO(weberlo): We need to allow configurable memory layouts by the user, and +// the constants below should be made into defaults. + +/*! \brief memory offset at which text section starts */ +const DevBaseOffset kTextStart = DevBaseOffset(64); + +/*! \brief memory offset at which rodata section starts */ +const DevBaseOffset kRodataStart = DevBaseOffset(500000000); + +/*! \brief memory offset at which data section starts */ +const DevBaseOffset kDataStart = DevBaseOffset(1000000000); + +/*! \brief memory offset at which bss section starts */ +const DevBaseOffset kBssStart = DevBaseOffset(1500000000); + +/*! \brief memory offset at which args section starts */ +const DevBaseOffset kArgsStart = DevBaseOffset(2000000000); + +/*! \brief memory offset at which stack section starts */ +const DevBaseOffset kStackStart = DevBaseOffset(3000000000); + +/*! \brief memory offset at which heap section starts */ +const DevBaseOffset kHeapStart = DevBaseOffset(3500000000); + +/*! \brief memory offset at which workspace section starts */ +const DevBaseOffset kWorkspaceStart = DevBaseOffset(4000000000); + +/*! \brief total memory size */ +constexpr uint64_t kMemorySize = 45000000000; + +/*! \brief default size alignment */ +constexpr int kDefaultSizeAlignment = 8; + +/*! + * \brief upper-aligns value according to specified alignment + * \param value value to be aligned + * \param align alignment + * \return upper-aligned value + */ +inline size_t UpperAlignValue(size_t value, size_t align) { + return value + (align - (value % align)) % align; +} + +/*! + * \brief maps section enums to text + * \param section section type + * \return text form of the specified section + */ +const char* SectionToString(SectionKind section); + +/*! + * \brief links binary by repositioning section addresses + * \param binary_name input binary filename + * \param text new text section address + * \param rodata new rodata section address + * \param data new data section address + * \param bss new bss section address + * \return relocated binary file contents + */ +std::string RelocateBinarySections(const std::string& binary_name, + DevAddr text, + DevAddr rodata, + DevAddr data, + DevAddr bss); + +/*! + * \brief reads section from binary + * \param binary input binary contents + * \param section section type to be read + * \return contents of the section + */ +std::string ReadSection(const std::string& binary, SectionKind section); + +/*! + * \brief finds size of the section in the binary + * \param binary input binary contents + * \param section section type + * \param align alignment of the returned size (default: 8) + * \return size of the section if it exists, 0 otherwise + */ +size_t GetSectionSize(const std::string& binary_name, + SectionKind section, + size_t align = kDefaultSizeAlignment); +} // namespace runtime +} // namespace tvm +#endif // TVM_RUNTIME_MICRO_MICRO_COMMON_H_ diff --git a/src/runtime/micro/micro_device_api.cc b/src/runtime/micro/micro_device_api.cc new file mode 100644 index 0000000000000..b5f2ed40cfaf9 --- /dev/null +++ b/src/runtime/micro/micro_device_api.cc @@ -0,0 +1,135 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file micro_device_api.cc + */ + +#include +#include +#include +#include "../workspace_pool.h" +#include "micro_session.h" + +namespace tvm { +namespace runtime { +/*! + * \brief device API for uTVM micro devices + */ +class MicroDeviceAPI final : public DeviceAPI { + public: + /*! \brief constructor */ + MicroDeviceAPI() + : session_(MicroSession::Global()) { + } + + void SetDevice(TVMContext ctx) final {} + + void GetAttr(TVMContext ctx, DeviceAttrKind kind, TVMRetValue* rv) final { + if (kind == kExist) { + *rv = 1; + } + } + + void* AllocDataSpace(TVMContext ctx, + size_t nbytes, + size_t alignment, + TVMType type_hint) final { + return session_->AllocateInSection(SectionKind::kHeap, nbytes).cast_to(); + } + + void FreeDataSpace(TVMContext ctx, void* ptr) final { + session_->FreeInSection(SectionKind::kHeap, + DevBaseOffset(reinterpret_cast(ptr))); + } + + void CopyDataFromTo(const void* from, + size_t from_offset, + void* to, + size_t to_offset, + size_t size, + TVMContext ctx_from, + TVMContext ctx_to, + TVMType type_hint, + TVMStreamHandle stream) final { + constexpr int micro_devtype = kDLMicroDev; + std::tuple type_from_to(ctx_from.device_type, ctx_to.device_type); + DevBaseOffset from_base_offset = + DevBaseOffset(reinterpret_cast(const_cast(from)) + from_offset); + DevBaseOffset to_base_offset = + DevBaseOffset(reinterpret_cast(const_cast(to)) + to_offset); + const std::shared_ptr& lld = session_->low_level_device(); + + if (type_from_to == std::make_tuple(micro_devtype, micro_devtype)) { + // Copying from the device to the device. + CHECK(ctx_from.device_id == ctx_to.device_id) + << "can only copy between the same micro device"; + std::vector buffer(size); + lld->Read(from_base_offset, reinterpret_cast(buffer.data()), size); + lld->Write(to_base_offset, reinterpret_cast(buffer.data()), size); + } else if (type_from_to == std::make_tuple(micro_devtype, kDLCPU)) { + // Reading from the device. + const std::shared_ptr& from_lld = session_->low_level_device(); + lld->Read(from_base_offset, to_base_offset.cast_to(), size); + } else if (type_from_to == std::make_tuple(kDLCPU, micro_devtype)) { + // Writing to the device. + const std::shared_ptr& to_lld = session_->low_level_device(); + lld->Write(to_base_offset, from_base_offset.cast_to(), size); + + } else { + LOG(FATAL) << "Expect copy from/to micro_dev or between micro_dev\n"; + } + } + + void StreamSync(TVMContext ctx, TVMStreamHandle stream) final { + } + + void* AllocWorkspace(TVMContext ctx, size_t size, TVMType type_hint) final { + return session_->AllocateInSection(SectionKind::kWorkspace, size).cast_to(); + } + + void FreeWorkspace(TVMContext ctx, void* data) final { + session_->FreeInSection(SectionKind::kWorkspace, + DevBaseOffset(reinterpret_cast(data))); + } + + /*! + * \brief obtain a global singleton of MicroDeviceAPI + * \return global shared pointer to MicroDeviceAPI + */ + static const std::shared_ptr& Global() { + static std::shared_ptr inst = + std::make_shared(); + return inst; + } + + private: + /*! \brief pointer to global session */ + std::shared_ptr session_; +}; + +// register device that can be obtained from Python frontend +TVM_REGISTER_GLOBAL("device_api.micro_dev") +.set_body([](TVMArgs args, TVMRetValue* rv) { + DeviceAPI* ptr = MicroDeviceAPI::Global().get(); + *rv = static_cast(ptr); + }); +} // namespace runtime +} // namespace tvm diff --git a/src/runtime/micro/micro_module.cc b/src/runtime/micro/micro_module.cc new file mode 100644 index 0000000000000..bf8a81d7540ae --- /dev/null +++ b/src/runtime/micro/micro_module.cc @@ -0,0 +1,145 @@ +/* + * 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. + */ + +/*! +* Copyright (c) 2019 by Contributors +* \file micro_module.cc +*/ + +#include +#include +#include +#include +#include +#include "micro_session.h" +#include "low_level_device.h" +#include "micro_common.h" +#include "../pack_args.h" + +namespace tvm { +namespace runtime { +/*! + * \brief module for uTVM micro devices + */ +class MicroModuleNode final : public ModuleNode { + public: + MicroModuleNode() {} + + ~MicroModuleNode() {} + + const char* type_key() const final { + return "micro"; + } + + PackedFunc GetFunction(const std::string& name, + const std::shared_ptr& sptr_to_self) final; + + /*! + * \brief initializes module by establishing device connection and loads binary + * \param binary_path path of the binary to be loaded + */ + void InitMicroModule(const std::string& binary_path) { + session_ = MicroSession::Global(); + low_level_device_ = session_->low_level_device(); + binary_path_ = binary_path; + binary_info_ = session_->LoadBinary(binary_path_); + // Patch device lib pointers. + PatchImplHole("TVMBackendAllocWorkspace"); + PatchImplHole("TVMBackendFreeWorkspace"); + PatchImplHole("TVMAPISetLastError"); + } + + /*! + * \brief runs selected function on the micro device + * \param func_name name of the function to be run + * \param func_offset offset of the function to be run + * \param args type-erased arguments passed to the function + */ + void RunFunction(const std::string& func_name, DevBaseOffset func_offset, const TVMArgs& args) { + session_->PushToExecQueue(func_offset, args); + } + + private: + /*! \brief module binary info */ + BinaryInfo binary_info_; + /*! \brief path to module binary */ + std::string binary_path_; + /*! \brief global session pointer */ + std::shared_ptr session_; + /*! \brief low-level device pointer */ + std::shared_ptr low_level_device_; + + SymbolMap& symbol_map() { + return binary_info_.symbol_map; + } + + /*! + * \brief patches a function pointer in this module to an implementation + * \param func_name name of the function pointer being patched + */ + void PatchImplHole(const std::string& func_name) { + const DevBaseOffset init_impl_offset = session_->init_symbol_map()[func_name]; + void* init_impl_addr = (low_level_device_->base_addr() + init_impl_offset).cast_to(); + std::stringstream func_name_underscore; + func_name_underscore << func_name << "_"; + const DevBaseOffset lib_hole_offset = symbol_map()[func_name_underscore.str()]; + session_->low_level_device()->Write(lib_hole_offset, &init_impl_addr, sizeof(void*)); + } +}; + +class MicroWrappedFunc { + public: + MicroWrappedFunc(MicroModuleNode* m, + const std::string& func_name, + DevBaseOffset func_offset) { + m_ = m; + func_name_ = func_name; + func_offset_ = func_offset; + } + + void operator()(TVMArgs args, TVMRetValue* rv, void** void_args) const { + m_->RunFunction(func_name_, func_offset_, args); + } + + private: + // internal module + MicroModuleNode* m_; + // name of the function + std::string func_name_; + // address of the function to be called + DevBaseOffset func_offset_; +}; + +PackedFunc MicroModuleNode::GetFunction( + const std::string& name, + const std::shared_ptr& sptr_to_self) { + DevBaseOffset func_offset = symbol_map()[name]; + MicroWrappedFunc f(this, name, func_offset); + return PackFuncVoidAddr(f, std::vector()); +} + +// register loadfile function to load module from Python frontend +TVM_REGISTER_GLOBAL("module.loadfile_micro_dev") +.set_body([](TVMArgs args, TVMRetValue* rv) { + std::shared_ptr n = std::make_shared(); + n->InitMicroModule(args[0]); + *rv = runtime::Module(n); + }); +} // namespace runtime +} // namespace tvm diff --git a/src/runtime/micro/micro_session.cc b/src/runtime/micro/micro_session.cc new file mode 100644 index 0000000000000..720023f65d6df --- /dev/null +++ b/src/runtime/micro/micro_session.cc @@ -0,0 +1,354 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file micro_session.cc + * \brief session to manage multiple micro modules + */ + +#include +#include +#include +#include "micro_session.h" +#include "low_level_device.h" +#include "target_data_layout_encoder.h" + +namespace tvm { +namespace runtime { + +MicroSession::MicroSession() { } + +MicroSession::~MicroSession() { } + +void MicroSession::InitSession(const TVMArgs& args) { + text_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kTextStart, + kRodataStart)); + rodata_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kRodataStart, + kDataStart)); + data_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kDataStart, + kBssStart)); + bss_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kBssStart, + kArgsStart)); + args_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kArgsStart, + kStackStart)); + stack_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kStackStart, + kHeapStart)); + heap_allocator_ = std::unique_ptr( + new MicroSectionAllocator(kHeapStart, + kWorkspaceStart)); + + const std::string& device_type = args[0]; + const std::string& binary_path = args[1]; + SetInitBinaryPath(binary_path); + if (device_type == "host") { + low_level_device_ = HostLowLevelDeviceCreate(kMemorySize); + } else if (device_type == "openocd") { + int port = args[2]; + low_level_device_ = OpenOCDLowLevelDeviceCreate(port); + } else { + LOG(FATAL) << "Unsupported micro low-level device"; + } + CHECK(!init_binary_path_.empty()) << "init library not initialized"; + init_stub_info_ = LoadBinary(init_binary_path_); + utvm_main_symbol_addr_ = init_stub_info_.symbol_map["UTVMMain"]; + utvm_done_symbol_addr_ = init_stub_info_.symbol_map["UTVMDone"]; + + // Patch workspace pointers to the start of the workspace section. + DevBaseOffset workspace_start_hole_offset = init_symbol_map()["utvm_workspace_begin"]; + DevBaseOffset workspace_curr_hole_offset = init_symbol_map()["utvm_workspace_curr"]; + DevBaseOffset workspace_start(kWorkspaceStart.value()); + void* workspace_hole_fill = + (workspace_start + low_level_device_->base_addr().value()).cast_to(); + low_level_device()->Write(workspace_start_hole_offset, &workspace_hole_fill, sizeof(void*)); + low_level_device()->Write(workspace_curr_hole_offset, &workspace_hole_fill, sizeof(void*)); +} + +void MicroSession::EndSession() { + // text_allocator_ = nullptr; + // rodata_allocator_ = nullptr; + // data_allocator_ = nullptr; + // bss_allocator_ = nullptr; + // args_allocator_ = nullptr; + // stack_allocator_ = nullptr; + // heap_allocator_ = nullptr; + + low_level_device_ = nullptr; +} + +DevBaseOffset MicroSession::AllocateInSection(SectionKind type, size_t size) { + switch (type) { + case SectionKind::kText: + return text_allocator_->Allocate(size); + case SectionKind::kRodata: + return rodata_allocator_->Allocate(size); + case SectionKind::kData: + return data_allocator_->Allocate(size); + case SectionKind::kBss: + return bss_allocator_->Allocate(size); + case SectionKind::kArgs: + return args_allocator_->Allocate(size); + case SectionKind::kStack: + return stack_allocator_->Allocate(size); + case SectionKind::kHeap: + return heap_allocator_->Allocate(size); + default: + LOG(FATAL) << "Unsupported section type during allocation"; + return DevBaseOffset(nullptr); + } +} + +void MicroSession::FreeInSection(SectionKind type, DevBaseOffset ptr) { + switch (type) { + case SectionKind::kText: + text_allocator_->Free(ptr); + return; + case SectionKind::kRodata: + rodata_allocator_->Free(ptr); + return; + case SectionKind::kData: + data_allocator_->Free(ptr); + return; + case SectionKind::kBss: + bss_allocator_->Free(ptr); + return; + case SectionKind::kArgs: + args_allocator_->Free(ptr); + return; + case SectionKind::kStack: + stack_allocator_->Free(ptr); + return; + case SectionKind::kHeap: + heap_allocator_->Free(ptr); + return; + default: + LOG(FATAL) << "Unsupported section type during free"; + } +} + +std::string MicroSession::ReadString(DevBaseOffset str_offset) { + std::stringstream result; + static char buf[256]; + size_t i = 256; + while (i == 256) { + low_level_device()->Read(str_offset, reinterpret_cast(buf), 256); + i = 0; + while (i < 256) { + if (buf[i] == 0) break; + result << buf[i]; + i++; + } + str_offset = str_offset + i; + } + return result.str(); +} + +void MicroSession::PushToExecQueue(DevBaseOffset func, const TVMArgs& args) { + void (*func_dev_addr)(void*, void*, int32_t) = + reinterpret_cast( + (func + low_level_device()->base_addr()).value()); + + // Create an allocator stream for the memory region after the most recent + // allocation in the args section. + DevAddr args_addr = args_allocator_->section_max() + low_level_device()->base_addr(); + TargetDataLayoutEncoder encoder(args_addr); + + EncoderAppend(&encoder, args); + // Flush `stream` to device memory. + DevBaseOffset stream_dev_offset = args_allocator_->Allocate(encoder.buf_size()); + low_level_device()->Write(stream_dev_offset, + reinterpret_cast(encoder.data()), + encoder.buf_size()); + + UTVMTask task = { + .func = func_dev_addr, + .args = args_addr.cast_to(), + }; + // TODO(mutinifni): handle bits / endianness + // Write the task. + low_level_device()->Write(init_symbol_map()["task"], &task, sizeof(task)); + // Zero out the last error. + std::uintptr_t last_error = 0; + low_level_device()->Write(init_symbol_map()["last_error"], &last_error, sizeof(std::uintptr_t)); + + low_level_device()->Execute(utvm_main_symbol_addr_, utvm_done_symbol_addr_); + + // Check if there was an error during execution. If so, log it. + CheckDeviceError(); +} + +BinaryInfo MicroSession::LoadBinary(std::string binary_path) { + SectionLocation text; + SectionLocation rodata; + SectionLocation data; + SectionLocation bss; + + text.size = GetSectionSize(binary_path, SectionKind::kText); + rodata.size = GetSectionSize(binary_path, SectionKind::kRodata); + data.size = GetSectionSize(binary_path, SectionKind::kData); + bss.size = GetSectionSize(binary_path, SectionKind::kBss); + + text.start = AllocateInSection(SectionKind::kText, text.size); + rodata.start = AllocateInSection(SectionKind::kRodata, rodata.size); + data.start = AllocateInSection(SectionKind::kData, data.size); + bss.start = AllocateInSection(SectionKind::kBss, bss.size); + CHECK(text.start != nullptr && rodata.start != nullptr && data.start != nullptr && + bss.start != nullptr) << "not enough space to load module on device"; + const DevBaseAddr base_addr = low_level_device_->base_addr(); + std::string relocated_bin = RelocateBinarySections( + binary_path, + text.start + base_addr, + rodata.start + base_addr, + data.start + base_addr, + bss.start + base_addr); + std::string text_contents = ReadSection(relocated_bin, SectionKind::kText); + std::string rodata_contents = ReadSection(relocated_bin, SectionKind::kRodata); + std::string data_contents = ReadSection(relocated_bin, SectionKind::kData); + std::string bss_contents = ReadSection(relocated_bin, SectionKind::kBss); + low_level_device_->Write(text.start, &text_contents[0], text.size); + low_level_device_->Write(rodata.start, &rodata_contents[0], rodata.size); + low_level_device_->Write(data.start, &data_contents[0], data.size); + low_level_device_->Write(bss.start, &bss_contents[0], bss.size); + SymbolMap symbol_map {relocated_bin, base_addr}; + return BinaryInfo{ + .text = text, + .rodata = rodata, + .data = data, + .bss = bss, + .symbol_map = symbol_map, + }; +} + +void MicroSession::SetInitBinaryPath(std::string path) { + init_binary_path_ = path; +} + +DevAddr MicroSession::EncoderAppend(TargetDataLayoutEncoder* encoder, const TVMArgs& args) { + auto utvm_args_slot = encoder->Alloc(); + + const int* type_codes = args.type_codes; + int num_args = args.num_args; + + auto tvm_vals_slot = encoder->Alloc(num_args); + auto type_codes_slot = encoder->Alloc(num_args); + + for (int i = 0; i < num_args; i++) { + switch (type_codes[i]) { + case kNDArrayContainer: + case kArrayHandle: { + TVMArray* arr_handle = args[i]; + void* arr_ptr = EncoderAppend(encoder, *arr_handle).cast_to(); + TVMValue val; + val.v_handle = arr_ptr; + tvm_vals_slot.WriteValue(val); + break; + } + // TODO(weberlo): Implement `double` and `int64` case. + case kDLFloat: + case kDLInt: + case kDLUInt: + default: + LOG(FATAL) << "Unsupported type code for writing args: " << type_codes[i]; + break; + } + } + type_codes_slot.WriteArray(type_codes, num_args); + + UTVMArgs dev_args = { + .values = tvm_vals_slot.start_addr().cast_to(), + .type_codes = type_codes_slot.start_addr().cast_to(), + .num_args = num_args, + }; + utvm_args_slot.WriteValue(dev_args); + return utvm_args_slot.start_addr(); +} + +DevAddr MicroSession::EncoderAppend(TargetDataLayoutEncoder* encoder, const TVMArray& arr) { + auto tvm_arr_slot = encoder->Alloc(); + auto shape_slot = encoder->Alloc(arr.ndim); + + // `shape` and `strides` are stored on the host, so we need to write them to + // the device first. The `data` field is already allocated on the device and + // is a device pointer, so we don't need to write it. + shape_slot.WriteArray(arr.shape, arr.ndim); + DevAddr shape_addr = shape_slot.start_addr(); + DevAddr strides_addr = DevAddr(nullptr); + if (arr.strides != nullptr) { + auto stride_slot = encoder->Alloc(arr.ndim); + stride_slot.WriteArray(arr.strides, arr.ndim); + strides_addr = stride_slot.start_addr(); + } + + // Copy `arr`, update the copy's pointers to be device pointers, then + // write the copy to `tvm_arr_slot`. + TVMArray dev_arr = arr; + // Update the device type to look like a host, because codegen generates + // checks that it is a host array. + CHECK(dev_arr.ctx.device_type == static_cast(kDLMicroDev)) + << "attempt to write TVMArray with non-micro device type"; + dev_arr.ctx.device_type = DLDeviceType::kDLCPU; + // Add the base address of the device to the array's data's device offset to + // get a device address. + DevBaseOffset arr_offset(reinterpret_cast(arr.data)); + dev_arr.data = (low_level_device()->base_addr() + arr_offset).cast_to(); + dev_arr.shape = shape_addr.cast_to(); + dev_arr.strides = strides_addr.cast_to(); + tvm_arr_slot.WriteValue(dev_arr); + return tvm_arr_slot.start_addr(); +} + +void MicroSession::CheckDeviceError() { + DevBaseOffset last_err_offset = init_symbol_map()["last_error"]; + std::uintptr_t last_error; + low_level_device()->Read(last_err_offset, &last_error, sizeof(std::uintptr_t)); + if (last_error) { + // First, retrieve the string `last_error` points to. + std::uintptr_t last_err_data_addr; + low_level_device()->Read(last_err_offset, &last_err_data_addr, sizeof(std::uintptr_t)); + DevBaseOffset last_err_data_offset = + DevAddr(last_err_data_addr) - low_level_device()->base_addr(); + // Then read the string from device to host and log it. + std::string last_error_str = ReadString(last_err_data_offset); + LOG(FATAL) << "error during micro function execution:\n" + << " dev str addr: 0x" << std::hex << last_err_data_addr << "\n" + << " dev str data: " << last_error_str; + } +} + +// initializes micro session and low-level device from Python frontend +TVM_REGISTER_GLOBAL("micro._InitSession") +.set_body([](TVMArgs args, TVMRetValue* rv) { + std::shared_ptr session = MicroSession::Global(); + session->InitSession(args); + }); + +// ends micro session and destructs low-level device from Python frontend +TVM_REGISTER_GLOBAL("micro._EndSession") +.set_body([](TVMArgs args, TVMRetValue* rv) { + std::shared_ptr session = MicroSession::Global(); + session->EndSession(); + }); +} // namespace runtime +} // namespace tvm diff --git a/src/runtime/micro/micro_session.h b/src/runtime/micro/micro_session.h new file mode 100644 index 0000000000000..01164cc914934 --- /dev/null +++ b/src/runtime/micro/micro_session.h @@ -0,0 +1,257 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file micro_session.h + */ +#ifndef TVM_RUNTIME_MICRO_MICRO_SESSION_H_ +#define TVM_RUNTIME_MICRO_MICRO_SESSION_H_ + +#include "micro_common.h" + +#include +#include + +#include +#include +#include + +#include "low_level_device.h" +#include "device/utvm_runtime.h" +#include "target_data_layout_encoder.h" + +namespace tvm { +namespace runtime { +/*! + * \brief allocator for a on-device memory section + */ +class MicroSectionAllocator { + public: + /*! + * \brief constructor that specifies section boundaries + * \param section_start start address of the section + * \param section_end end address of the section (non inclusive) + */ + MicroSectionAllocator(DevBaseOffset section_start, DevBaseOffset section_end) + : section_start_(section_start), section_end_(section_end), + section_max_(section_start) { + } + + /*! + * \brief destructor + */ + ~MicroSectionAllocator() { + } + + /*! + * \brief memory allocator + * \param size size of allocated memory in bytes + * \return pointer to allocated memory region in section, nullptr if out of space + */ + DevBaseOffset Allocate(size_t size) { + CHECK(section_max_.value() + size < section_end_.value()) + << "out of space in section with start_addr=" << section_start_.value(); + DevBaseOffset alloc_ptr = section_max_; + section_max_ = section_max_ + size; + alloc_map_[alloc_ptr.value()] = size; + return alloc_ptr; + } + + /*! + * \brief free prior allocation from section + * \param type type of section to allocate in + * \param ptr pointer to allocated memory + * \note simple allocator scheme, more complex versions will be implemented later + */ + void Free(DevBaseOffset offs) { + std::uintptr_t ptr = offs.value(); + CHECK(alloc_map_.find(ptr) != alloc_map_.end()) << "freed pointer was never allocated"; + alloc_map_.erase(ptr); + if (alloc_map_.empty()) { + section_max_ = section_start_; + } + } + + /*! + * \brief obtain the end address of the last allocation + * \return pointer immediately following the last allocation + */ + DevBaseOffset section_max() { + return section_max_; + } + + private: + /*! \brief start address of the section */ + DevBaseOffset section_start_; + /*! \brief end address of the section */ + DevBaseOffset section_end_; + /*! \brief end address of last allocation */ + DevBaseOffset section_max_; + /*! \brief allocation map for allocation sizes */ + std::unordered_map alloc_map_; +}; + +/*! + * \brief session for facilitating micro device interaction + */ +class MicroSession { + public: + /*! + * \brief constructor + */ + MicroSession(); + + /*! + * \brief destructor + */ + ~MicroSession(); + + /*! + * \brief get MicroSession global singleton + * \return pointer to the micro session global singleton + */ + static std::shared_ptr& Global() { + static std::shared_ptr inst = std::make_shared(); + return inst; + } + + /*! + * \brief initializes session by setting up a low-level device and initting allocators for it + * \param args TVMArgs passed into the micro.init packedfunc + * \note must be called upon first call to Global() + */ + void InitSession(const TVMArgs& args); + + /*! + * \brief ends the session by destructing the low-level device and its allocators + */ + void EndSession(); + + /*! + * \brief allocate memory in section + * \param type type of section to allocate in + * \param size size of allocated memory in bytes + * \return pointer to allocated memory region in section, nullptr if out of space + */ + DevBaseOffset AllocateInSection(SectionKind type, size_t size); + + /*! + * \brief free prior allocation from section + * \param type type of section to allocate in + * \param ptr pointer to allocated memory + */ + void FreeInSection(SectionKind type, DevBaseOffset ptr); + + /*! + * \brief read string from device to host + * \param str_offset device offset of first character of string + * \return host copy of device string that was read + */ + std::string ReadString(DevBaseOffset str_offset); + + /*! + * \brief sets up init stub pointers and copies arguments for on-device execution + * \param func address of the function to be executed + * \param args args to the packed function + */ + void PushToExecQueue(DevBaseOffset func, const TVMArgs& args); + + /*! + * \brief loads binary onto device + * \param binary_path path to binary object file + * \return info about loaded binary + */ + BinaryInfo LoadBinary(std::string binary_path); + + /*! + * \brief returns low-level device pointer + * \note assumes low-level device has been initialized + */ + const std::shared_ptr low_level_device() const { + CHECK(low_level_device_ != nullptr) << "attempt to get uninitialized low-level device"; + return low_level_device_; + } + + SymbolMap& init_symbol_map() { + return init_stub_info_.symbol_map; + } + + private: + /*! \brief low-level device pointer */ + std::shared_ptr low_level_device_; + /*! \brief text section allocator */ + std::unique_ptr text_allocator_; + /*! \brief rodata section allocator */ + std::unique_ptr rodata_allocator_; + /*! \brief data section allocator */ + std::unique_ptr data_allocator_; + /*! \brief bss section allocator */ + std::unique_ptr bss_allocator_; + /*! \brief args section allocator */ + std::unique_ptr args_allocator_; + /*! \brief stack section allocator */ + std::unique_ptr stack_allocator_; + /*! \brief heap section allocator */ + std::unique_ptr heap_allocator_; + /*! \brief init stub binary info */ + BinaryInfo init_stub_info_; + /*! \brief path to init stub source code */ + std::string init_binary_path_; + /*! \brief offset of the init stub entry function */ + DevBaseOffset utvm_main_symbol_addr_; + /*! \brief offset of the init stub exit breakpoint */ + DevBaseOffset utvm_done_symbol_addr_; + + /*! + * \brief sets up and loads init stub into the low-level device memory + */ + void LoadInitStub(); + + /*! + * \brief sets the init stub binary path + * \param path to init stub binary + */ + void SetInitBinaryPath(std::string path); + + /*! + * \brief appends arguments to the host-side buffer of `encoder` + * \param encoder encoder being used to append `args` + * \param args args to be appended + * \return device address of the allocated args + */ + DevAddr EncoderAppend(TargetDataLayoutEncoder* encoder, const TVMArgs& args); + + /*! + * \brief appends a `TVMArray` to the host-side buffer of `encoder` + * \param encoder encoder being used to append `arr` + * \param arr TVMArray to be appended + * \return device address of the allocated `TVMArray` + */ + DevAddr EncoderAppend(TargetDataLayoutEncoder* encoder, const TVMArray& arr); + + // TODO(weberlo): should there be both a check and log method? + /*! + * \brief checks and logs if there was an error during the device's most recent execution + */ + void CheckDeviceError(); +}; +} // namespace runtime +} // namespace tvm +#endif // TVM_RUNTIME_MICRO_MICRO_SESSION_H_ diff --git a/src/runtime/micro/openocd_low_level_device.cc b/src/runtime/micro/openocd_low_level_device.cc new file mode 100644 index 0000000000000..789d866288d0f --- /dev/null +++ b/src/runtime/micro/openocd_low_level_device.cc @@ -0,0 +1,76 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file openocd_low_level_device.cc + * \brief openocd low-level device to interface with micro devices over JTAG + */ + +#include "low_level_device.h" + +namespace tvm { +namespace runtime { + +// TODO(weberlo): Add implementation for this device. + +/*! + * \brief openocd low-level device for uTVM micro devices connected over JTAG + */ +class OpenOCDLowLevelDevice final : public LowLevelDevice { + public: + /*! + * \brief constructor to initialize connection to openocd device + * \param port port of the OpenOCD server to connect to + */ + explicit OpenOCDLowLevelDevice(int port); + + /*! + * \brief destructor to close openocd device connection + */ + ~OpenOCDLowLevelDevice(); + + void Write(DevBaseOffset offset, + void* buf, + size_t num_bytes) final; + + void Read(DevBaseOffset offset, + void* buf, + size_t num_bytes) final; + + void Execute(DevBaseOffset func_addr, DevBaseOffset breakpoint) final; + + DevBaseAddr base_addr() const final; + + const char* device_type() const final { + return "openocd"; + } + + private: + /*! \brief base address of the micro device memory region */ + DevBaseAddr base_addr_; + /*! \brief size of memory region */ + size_t size_; +}; + +const std::shared_ptr OpenOCDLowLevelDeviceCreate(int port) { + return nullptr; +} +} // namespace runtime +} // namespace tvm diff --git a/src/runtime/micro/target_data_layout_encoder.h b/src/runtime/micro/target_data_layout_encoder.h new file mode 100644 index 0000000000000..b591c042a202c --- /dev/null +++ b/src/runtime/micro/target_data_layout_encoder.h @@ -0,0 +1,188 @@ +/* + * 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. + */ + +/*! + * Copyright (c) 2019 by Contributors + * \file target_data_layout_encoder.h + * \brief uTVM data layout encoder + */ +#ifndef TVM_RUNTIME_MICRO_TARGET_DATA_LAYOUT_ENCODER_H_ +#define TVM_RUNTIME_MICRO_TARGET_DATA_LAYOUT_ENCODER_H_ + +#include +#include "device/utvm_runtime.h" + +namespace tvm { +namespace runtime { + +// TODO(weberlo): Handle endianness. + +/*! + * \brief data encoder for uTVM that builds a host-side buffer + */ +class TargetDataLayoutEncoder { + public: + /*! + * \brief helper class for writing into `TargetDataLayoutEncoder` + */ + template + class Slot { + public: + /*! + * \brief constructor + * \param parent pointer to parent encoder + * \param start_offset start byte offset of the slot in the backing buffer + * \param size size (in bytes) of the memory region allocated for this slot + * \param start_addr start address of the slot in the device's memory + */ + Slot(TargetDataLayoutEncoder* parent, size_t start_offset, size_t size, DevAddr start_addr); + + ~Slot(); + + /*! + * \brief writes `sizeof(T) * num_elems` bytes of data from `arr` + * \param arr array to be read from + * \param num_elems number of elements in array + */ + void WriteArray(const T* arr, size_t num_elems); + + /*! + * \brief writes `val` + * \param val value to be written + */ + void WriteValue(const T& val); + + /*! + * \brief returns start address of the slot in device memory + * \return device start address + */ + DevAddr start_addr(); + + /*! + * \brief returns number of bytes allocated for this slot + * \return size of this slot + */ + size_t size(); + + private: + /*! \brief pointer to parent encoder */ + TargetDataLayoutEncoder* parent_; + /*! \brief start offset of the slot in the parent's backing parent_buffer */ + size_t start_offset_; + /*! \brief current offset relative to the start offset of this slot */ + size_t curr_offset_; + /*! \brief size (in bytes) of the memory region allocated for this slot */ + size_t size_; + /*! \brief start address of the slot in the device's memory */ + DevAddr start_addr_; + }; + + /*! + * \brief constructor + * \param start_addr start address of the encoder in device memory + */ + explicit TargetDataLayoutEncoder(DevAddr start_addr) + : buf_(std::vector()), + curr_offset_(0), + start_addr_(start_addr) {} + + /*! + * \brief allocates a slot for `sizeof(T) * num_elems` bytes of data + * \param num_elems number of elements of type `T` being allocated (defaults to 1) + * \return slot of size `sizeof(T) * num_elems` bytes + */ + template + Slot Alloc(size_t num_elems = 1) { + size_t size = sizeof(T) * num_elems; + if (curr_offset_ + size > buf_.size()) { + buf_.resize(curr_offset_ + size); + } + size_t slot_start_offset = curr_offset_; + curr_offset_ += size; + return Slot(this, slot_start_offset, size, start_addr_ + slot_start_offset); + } + + /*! + * \brief returns the array backing the encoder's buffer + * \return array backing the encoder's buffer + */ + uint8_t* data() { + return buf_.data(); + } + + /*! + * \brief returns current size of the encoder's buffer + * \return buffer size + */ + size_t buf_size() { + return buf_.size(); + } + + private: + /*! \brief in-memory backing buffer */ + std::vector buf_; + /*! \brief current offset */ + size_t curr_offset_; + /*! \brief start address of the encoder in device memory */ + DevAddr start_addr_; +}; + +template +TargetDataLayoutEncoder::Slot::Slot(TargetDataLayoutEncoder* parent, + size_t start_offset, + size_t size, DevAddr start_addr) + : parent_(parent), + start_offset_(start_offset), + curr_offset_(0), + size_(size), + start_addr_(start_addr) {} + +template +TargetDataLayoutEncoder::Slot::~Slot() { + CHECK(curr_offset_ == size_) << "unwritten space in slot"; +} + +template +void TargetDataLayoutEncoder::Slot::WriteArray(const T* arr, size_t num_elems) { + if (num_elems == 0) return; + size_t size = sizeof(T) * num_elems; + CHECK(curr_offset_ + size <= size_) << "not enough space in slot"; + uint8_t* curr_ptr = &(parent_->data())[start_offset_ + curr_offset_]; + std::memcpy(curr_ptr, arr, size); + curr_offset_ += size; +} + +template +void TargetDataLayoutEncoder::Slot::WriteValue(const T& val) { + WriteArray(&val, 1); +} + +template +DevAddr TargetDataLayoutEncoder::Slot::start_addr() { + return start_addr_; +} + +template +size_t TargetDataLayoutEncoder::Slot::size() { + return size_; +} + +} // namespace runtime +} // namespace tvm +#endif // TVM_RUNTIME_MICRO_TARGET_DATA_LAYOUT_ENCODER_H_ diff --git a/src/runtime/module.cc b/src/runtime/module.cc index cc0fd0922f93d..6e26553c14aaf 100644 --- a/src/runtime/module.cc +++ b/src/runtime/module.cc @@ -139,6 +139,8 @@ bool RuntimeEnabled(const std::string& target) { f_name = "device_api.rpc"; } else if (target == "vpi" || target == "verilog") { f_name = "device_api.vpi"; + } else if (target == "micro_dev") { + f_name = "device_api.micro_dev"; } else if (target.length() >= 5 && target.substr(0, 5) == "nvptx") { f_name = "device_api.gpu"; } else if (target.length() >= 4 && target.substr(0, 4) == "rocm") { diff --git a/tests/python/contrib/test_binutil.py b/tests/python/contrib/test_binutil.py new file mode 100644 index 0000000000000..855234e6a690a --- /dev/null +++ b/tests/python/contrib/test_binutil.py @@ -0,0 +1,130 @@ +# 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 subprocess +from tvm.contrib import util +from tvm.contrib import cc +from tvm.contrib.binutil import * + + +def make_binary(): + prog = "int a = 7; \ + int main() { \ + int b = 5; \ + return 0; \ + }" + tmp_dir = util.tempdir() + tmp_source = tmp_dir.relpath("source.c") + tmp_obj = tmp_dir.relpath("obj.o") + with open(tmp_source, "w") as f: + f.write(prog) + p1 = subprocess.Popen(["gcc", "-c", tmp_source, "-o", tmp_obj], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + p1.communicate() + prog_bin = bytearray(open(tmp_obj, "rb").read()) + return prog_bin + + +def test_tvm_callback_get_section_size(binary): + tmp_dir = util.tempdir() + tmp_bin = tmp_dir.relpath("obj.bin") + with open(tmp_bin, "wb") as f: + f.write(binary) + def verify(): + print("Text section size: %d" % tvm_callback_get_section_size(tmp_bin, "text")) + print("Data section size: %d" % tvm_callback_get_section_size(tmp_bin, "data")) + print("Bss section size: %d" % tvm_callback_get_section_size(tmp_bin, "bss")) + print + verify() + + +def test_tvm_callback_relocate_binary(binary): + tmp_dir = util.tempdir() + tmp_bin = tmp_dir.relpath("obj.bin") + with open(tmp_bin, "wb") as f: + f.write(binary) + def verify(): + text_loc_str = "0x0" + data_loc_str = "0x10000" + bss_loc_str = "0x20000" + rel_bin = tvm_callback_relocate_binary(tmp_bin, text_loc_str, data_loc_str, bss_loc_str) + print("Relocated binary section sizes") + test_tvm_callback_get_section_size(rel_bin) + relf = tmp_dir.relpath("rel.bin") + with open(relf, "wb") as f: + f.write(rel_bin) + nm_proc = subprocess.Popen(["nm", "-C", "--defined-only", relf], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + (out, _) = nm_proc.communicate() + # Ensure the relocated symbols are within the ranges we specified. + text_loc = int(text_loc_str, 16) + data_loc = int(data_loc_str, 16) + bss_loc = int(bss_loc_str, 16) + symbol_entries = out.decode("utf-8").split("\n") + for entry in symbol_entries: + if len(entry) == 0: + continue + sym_loc, section, sym_name = entry.split(' ') + sym_loc = int(sym_loc, 16) + if section == 'T': # text + assert sym_loc >= text_loc and sym_loc < data_loc + elif section == 'D': # data + assert sym_loc >= data_loc and sym_loc < bss_loc + elif section == 'B': # bss + assert sym_loc >= bss_loc + verify() + + +def test_tvm_callback_read_binary_section(binary): + def verify(): + text_bin = tvm_callback_read_binary_section(binary, "text") + data_bin = tvm_callback_read_binary_section(binary, "data") + bss_bin = tvm_callback_read_binary_section(binary, "bss") + print("Read text section part of binary? %r" % (text_bin in binary)) + print("Read data section part of binary? %r" % (data_bin in binary)) + print("Read bss section part of binary? %r" % (bss_bin in binary)) + print + verify() + + +def test_tvm_callback_get_symbol_map(binary): + tmp_dir = util.tempdir() + tmp_bin = tmp_dir.relpath("obj.bin") + with open(tmp_bin, "wb") as f: + f.write(binary) + def verify(): + rel_bin = tvm_callback_relocate_binary(tmp_bin, "0x0", "0x10000", "0x20000") + symbol_map = tvm_callback_get_symbol_map(rel_bin) + symbols = set() + for i, line in enumerate(symbol_map.split('\n')): + # Every other line is the value the symbol maps to. + if i % 2 == 0: + symbols.add(line) + assert "a" in symbols + assert "main" in symbols + verify() + + +if __name__ == "__main__": + prog_bin = make_binary() + test_tvm_callback_get_section_size(prog_bin) + test_tvm_callback_relocate_binary(prog_bin) + test_tvm_callback_read_binary_section(prog_bin) + test_tvm_callback_get_symbol_map(prog_bin) diff --git a/tests/python/unittest/test_runtime_micro.py b/tests/python/unittest/test_runtime_micro.py new file mode 100644 index 0000000000000..7b53f9af570ca --- /dev/null +++ b/tests/python/unittest/test_runtime_micro.py @@ -0,0 +1,193 @@ +# 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 os + +from nose.tools import nottest +import numpy as np +import tvm +from tvm.contrib import graph_runtime, util +from tvm import relay +import tvm.micro as micro +from tvm.relay.testing import resnet + +# Use the host emulated micro device, because it's simpler and faster to test. +DEVICE_TYPE = "host" +BINUTIL_PREFIX = "" +HOST_SESSION = micro.Session(DEVICE_TYPE, BINUTIL_PREFIX) + +# TODO(weberlo): Add example program to test scalar double/int TVMValue serialization. + +def test_add(): + """Test a program which performs addition.""" + shape = (1024,) + dtype = "float32" + + # Construct TVM expression. + tvm_shape = tvm.convert(shape) + A = tvm.placeholder(tvm_shape, name="A", dtype=dtype) + B = tvm.placeholder(tvm_shape, name="B", dtype=dtype) + C = tvm.compute(A.shape, lambda *i: A(*i) + B(*i), name="C") + s = tvm.create_schedule(C.op) + + func_name = "fadd" + c_mod = tvm.build(s, [A, B, C], target="c", name=func_name) + + with HOST_SESSION as sess: + micro_mod = sess.create_micro_mod(c_mod) + micro_func = micro_mod[func_name] + ctx = tvm.micro_dev(0) + a = tvm.nd.array(np.random.uniform(size=shape).astype(dtype), ctx) + b = tvm.nd.array(np.random.uniform(size=shape).astype(dtype), ctx) + c = tvm.nd.array(np.zeros(shape, dtype=dtype), ctx) + micro_func(a, b, c) + + tvm.testing.assert_allclose( + c.asnumpy(), a.asnumpy() + b.asnumpy()) + + +def test_workspace_add(): + """Test a program which uses a workspace.""" + shape = (1024,) + dtype = "float32" + + # Construct TVM expression. + tvm_shape = tvm.convert(shape) + A = tvm.placeholder(tvm_shape, name="A", dtype=dtype) + B = tvm.placeholder(tvm_shape, name="B", dtype=dtype) + B = tvm.compute(A.shape, lambda *i: A(*i) + 1, name="B") + C = tvm.compute(A.shape, lambda *i: B(*i) + 1, name="C") + s = tvm.create_schedule(C.op) + + func_name = "fadd_two_workspace" + c_mod = tvm.build(s, [A, C], target="c", name=func_name) + + with HOST_SESSION as sess: + micro_mod = sess.create_micro_mod(c_mod) + micro_func = micro_mod[func_name] + ctx = tvm.micro_dev(0) + a = tvm.nd.array(np.random.uniform(size=shape).astype(dtype), ctx) + c = tvm.nd.array(np.zeros(shape, dtype=dtype), ctx) + micro_func(a, c) + + tvm.testing.assert_allclose( + c.asnumpy(), a.asnumpy() + 2.0) + + +def test_graph_runtime(): + """Test a program which uses the graph runtime.""" + shape = (1024,) + dtype = "float32" + + # Construct Relay program. + x = relay.var("x", relay.TensorType(shape=shape, dtype=dtype)) + xx = relay.multiply(x, x) + z = relay.add(xx, relay.const(1.0)) + func = relay.Function([x], z) + + with HOST_SESSION as sess: + mod, params = sess.micro_build(func) + + mod.set_input(**params) + x_in = np.random.uniform(size=shape[0]).astype(dtype) + mod.run(x=x_in) + result = mod.get_output(0).asnumpy() + + tvm.testing.assert_allclose( + result, x_in * x_in + 1.0) + + +def test_resnet_random(): + """Test ResNet18 inference with random weights and inputs.""" + resnet_func, params = resnet.get_workload(num_classes=10, + num_layers=18, + image_shape=(3, 32, 32)) + # Remove the final softmax layer, because uTVM does not currently support it. + resnet_func_no_sm = relay.Function(resnet_func.params, + resnet_func.body.args[0], + resnet_func.ret_type) + + with HOST_SESSION as sess: + # TODO(weberlo): Use `resnet_func` once we have libc support. + mod, params = sess.micro_build(resnet_func_no_sm, params=params) + mod.set_input(**params) + # Generate random input. + data = np.random.uniform(size=mod.get_input(0).shape) + mod.run(data=data) + result = mod.get_output(0).asnumpy() + # We gave a random input, so all we want is a result with some nonzero + # entries. + assert result.sum() != 0.0 + + +# TODO(weberlo): Enable this test or move the code somewhere else. +@nottest +def test_resnet_pretrained(): + """Test classification with a pretrained ResNet18 model.""" + import mxnet as mx + from mxnet.gluon.model_zoo.vision import get_model + from mxnet.gluon.utils import download + from PIL import Image + + # TODO(weberlo) there's a significant amount of overlap between here and + # `tutorials/frontend/from_mxnet.py`. Should refactor. + dtype = "float32" + + # Fetch a mapping from class IDs to human-readable labels. + synset_url = "".join(["https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt"]) + synset_name = "synset.txt" + download(synset_url, synset_name) + with open(synset_name) as f: + synset = eval(f.read()) + + # Read raw image and preprocess into the format ResNet can work on. + img_name = "cat.png" + download("https://github.com/dmlc/mxnet.js/blob/master/data/cat.png?raw=true", + img_name) + image = Image.open(img_name).resize((224, 224)) + image = np.array(image) - np.array([123., 117., 104.]) + image /= np.array([58.395, 57.12, 57.375]) + image = image.transpose((2, 0, 1)) + image = image[np.newaxis, :] + image = tvm.nd.array(image.astype(dtype)) + + block = get_model("resnet18_v1", pretrained=True) + func, params = relay.frontend.from_mxnet(block, + shape={"data": image.shape}) + + with HOST_SESSION as sess: + mod, params = sess.micro_build(func, params=params) + # Set model weights. + mod.set_input(**params) + # Execute with `image` as the input. + mod.run(data=image) + # Get outputs. + tvm_output = mod.get_output(0) + + prediction_idx = np.argmax(tvm_output.asnumpy()[0]) + prediction = synset[prediction_idx] + assert prediction == "tiger cat" + + +if __name__ == "__main__": + test_add() + test_workspace_add() + test_graph_runtime() + test_resnet_random() diff --git a/topi/python/topi/generic/nn.py b/topi/python/topi/generic/nn.py index 60a2d55486e51..b40b63aea2d2e 100644 --- a/topi/python/topi/generic/nn.py +++ b/topi/python/topi/generic/nn.py @@ -24,7 +24,7 @@ def _default_schedule(outs, auto_inline): """Default schedule for llvm.""" target = tvm.target.current_target(allow_none=False) outs = [outs] if isinstance(outs, tvm.tensor.Tensor) else outs - if target.target_name != "llvm": + if target.target_name not in ("llvm", "c"): raise RuntimeError("schedule not registered for '%s'" % target) s = tvm.create_schedule([x.op for x in outs]) if auto_inline: