diff --git a/apps/microtvm/zephyr/template_project/boards.json b/apps/microtvm/zephyr/template_project/boards.json new file mode 100644 index 000000000000..bdfa51109ff7 --- /dev/null +++ b/apps/microtvm/zephyr/template_project/boards.json @@ -0,0 +1,56 @@ +{ + "mps2_an521": { + "board": "mps2_an521", + "model": "mps2_an521", + "is_qemu": true, + "fpu": false + }, + "nrf5340dk_nrf5340_cpuapp": { + "board": "nrf5340dk_nrf5340_cpuapp", + "model": "nrf5340dk", + "is_qemu": false, + "fpu": true + }, + "nucleo_f746zg": { + "board": "nucleo_f746zg", + "model": "stm32f746xx", + "is_qemu": false, + "fpu": true + }, + "nucleo_l4r5zi": { + "board": "nucleo_l4r5zi", + "model": "stm32l4r5zi", + "is_qemu": false, + "fpu": true + }, + "qemu_cortex_r5": { + "board": "qemu_cortex_r5", + "model": "zynq_mp_r5", + "is_qemu": true, + "fpu": true + }, + "qemu_riscv32": { + "board": "qemu_riscv32", + "model": "host", + "is_qemu": true, + "fpu": true + }, + "qemu_riscv64": { + "board": "qemu_riscv64", + "model": "host", + "is_qemu": true, + "fpu": true + }, + "qemu_x86": { + "board": "qemu_x86", + "model": "host", + "is_qemu": true, + "fpu": true + }, + "stm32f746g_disco": { + "board": "stm32f746g_disco", + "model": "stm32f746xx", + "is_qemu": false, + "fpu": true + } +} diff --git a/apps/microtvm/zephyr/template_project/microtvm_api_server.py b/apps/microtvm/zephyr/template_project/microtvm_api_server.py index f2e091b2f5b5..ed275e610912 100644 --- a/apps/microtvm/zephyr/template_project/microtvm_api_server.py +++ b/apps/microtvm/zephyr/template_project/microtvm_api_server.py @@ -35,6 +35,7 @@ import tempfile import threading import time +import json import serial import serial.tools.list_ports @@ -57,46 +58,16 @@ IS_TEMPLATE = not (API_SERVER_DIR / MODEL_LIBRARY_FORMAT_RELPATH).exists() + +BOARDS = API_SERVER_DIR / "boards.json" + # Data structure to hold the information microtvm_api_server.py needs # to communicate with each of these boards. -BOARD_PROPERTIES = { - "qemu_x86": { - "board": "qemu_x86", - "model": "host", - }, - "qemu_riscv32": { - "board": "qemu_riscv32", - "model": "host", - }, - "qemu_riscv64": { - "board": "qemu_riscv64", - "model": "host", - }, - "mps2_an521": { - "board": "mps2_an521", - "model": "mps2_an521", - }, - "nrf5340dk_nrf5340_cpuapp": { - "board": "nrf5340dk_nrf5340_cpuapp", - "model": "nrf5340dk", - }, - "stm32f746g_disco": { - "board": "stm32f746g_disco", - "model": "stm32f746xx", - }, - "nucleo_f746zg": { - "board": "nucleo_f746zg", - "model": "stm32f746xx", - }, - "nucleo_l4r5zi": { - "board": "nucleo_l4r5zi", - "model": "stm32l4r5zi", - }, - "qemu_cortex_r5": { - "board": "qemu_cortex_r5", - "model": "zynq_mp_r5", - }, -} +try: + with open(BOARDS) as boards: + BOARD_PROPERTIES = json.load(boards) +except FileNotFoundError: + raise FileNotFoundError(f"Board file {{{BOARDS}}} does not exist.") def check_call(cmd_args, *args, **kwargs): @@ -290,9 +261,8 @@ def _get_nrf_device_args(options): help="Name of the Zephyr board to build for.", ), server.ProjectOption( - "zephyr_model", - choices=[board["model"] for _, board in BOARD_PROPERTIES.items()], - help="Name of the model for each Zephyr board.", + "config_main_stack_size", + help="Sets CONFIG_MAIN_STACK_SIZE for Zephyr board.", ), ] @@ -351,13 +321,9 @@ def _create_prj_conf(self, project_dir, options): if self._has_fpu(options["zephyr_board"]): f.write("# For models with floating point.\n" "CONFIG_FPU=y\n" "\n") - main_stack_size = None - if self._is_qemu(options) and options["project_type"] == "host_driven": - main_stack_size = 1536 - # Set main stack size, if needed. - if main_stack_size is not None: - f.write(f"CONFIG_MAIN_STACK_SIZE={main_stack_size}\n") + if options.get("config_main_stack_size") is not None: + f.write(f"CONFIG_MAIN_STACK_SIZE={options['config_main_stack_size']}\n") f.write("# For random number generation.\n" "CONFIG_TEST_RANDOM_GENERATOR=y\n") @@ -384,6 +350,9 @@ def generate_project(self, model_library_format_path, standalone_crt_dir, projec # by launching the copy. shutil.copy2(__file__, project_dir / os.path.basename(__file__)) + # Copy boards.json file to generated project. + shutil.copy2(BOARDS, project_dir / BOARDS.name) + # Place Model Library Format tarball in the special location, which this script uses to decide # whether it's being invoked in a template or generated project. project_model_library_format_tar_path = project_dir / MODEL_LIBRARY_FORMAT_RELPATH @@ -471,20 +440,10 @@ def _is_qemu(cls, options): or options["zephyr_board"] in cls._KNOWN_QEMU_ZEPHYR_BOARDS ) - _KNOWN_FPU_ZEPHYR_BOARDS = ( - "nucleo_f746zg", - "nucleo_l4r5zi", - "nrf5340dk_nrf5340_cpuapp", - "qemu_cortex_r5", - "qemu_riscv32", - "qemu_riscv64", - "qemu_x86", - "stm32f746g_disco", - ) - @classmethod def _has_fpu(cls, zephyr_board): - return zephyr_board in cls._KNOWN_FPU_ZEPHYR_BOARDS + fpu_boards = [name for name, board in BOARD_PROPERTIES.items() if board["fpu"]] + return zephyr_board in fpu_boards def flash(self, options): if self._is_qemu(options): diff --git a/python/tvm/micro/project.py b/python/tvm/micro/project.py index 8a62c9b5f9ba..5393096b5df3 100644 --- a/python/tvm/micro/project.py +++ b/python/tvm/micro/project.py @@ -18,7 +18,7 @@ """Defines glue wrappers around the Project API which mate to TVM interfaces.""" import pathlib -import typing +from typing import Union from .. import __version__ from ..contrib import utils @@ -64,7 +64,7 @@ class GeneratedProject: """Defines a glue interface to interact with a generated project through the API server.""" @classmethod - def from_directory(cls, project_dir: typing.Union[pathlib.Path, str], options: dict): + def from_directory(cls, project_dir: Union[pathlib.Path, str], options: dict): return cls(client.instantiate_from_dir(project_dir), options) def __init__(self, api_client, options): @@ -101,7 +101,17 @@ def __init__(self, api_client): if not self._info["is_template"]: raise NotATemplateProjectError() + def _check_project_options(self, options: dict): + """Check if options are valid ProjectOptions""" + available_options = [option["name"] for option in self.info()["project_options"]] + if options and not set(options.keys()).issubset(available_options): + raise ValueError( + f"""options:{list(options)} include non valid ProjectOptions. + Here is a list of available options:{list(available_options)}.""" + ) + def generate_project_from_mlf(self, model_library_format_path, project_dir, options): + self._check_project_options(options) self._api_client.generate_project( model_library_format_path=str(model_library_format_path), standalone_crt_dir=get_standalone_crt_dir(), @@ -124,9 +134,9 @@ def generate_project(self, graph_executor_factory, project_dir, options): def generate_project( - template_project_dir: typing.Union[pathlib.Path, str], + template_project_dir: Union[pathlib.Path, str], module: ExportableModule, - generated_project_dir: typing.Union[pathlib.Path, str], + generated_project_dir: Union[pathlib.Path, str], options: dict = None, ): """Generate a project for an embedded platform that contains the given model. diff --git a/python/tvm/micro/project_api/client.py b/python/tvm/micro/project_api/client.py index ac8ff629a718..f1eb115cfbbe 100644 --- a/python/tvm/micro/project_api/client.py +++ b/python/tvm/micro/project_api/client.py @@ -205,7 +205,6 @@ def instantiate_from_dir(project_dir: typing.Union[pathlib.Path, str], debug: bo """Launch server located in project_dir, and instantiate a Project API Client connected to it.""" args = None - project_dir = pathlib.Path(project_dir) python_script = project_dir / SERVER_PYTHON_FILENAME diff --git a/tests/micro/zephyr/conftest.py b/tests/micro/zephyr/conftest.py index 7c19b62ac63d..177ca8aa269e 100644 --- a/tests/micro/zephyr/conftest.py +++ b/tests/micro/zephyr/conftest.py @@ -20,44 +20,16 @@ import pytest -from tvm.micro import project -import tvm.contrib.utils -import tvm.target.target +import test_utils -TEMPLATE_PROJECT_DIR = ( - pathlib.Path(__file__).parent - / ".." - / ".." - / ".." - / "apps" - / "microtvm" - / "zephyr" - / "template_project" -).resolve() - - -def zephyr_boards() -> dict: - """Returns a dict mapping board to target model""" - template = project.TemplateProject.from_directory(TEMPLATE_PROJECT_DIR) - project_options = template.info()["project_options"] - for option in project_options: - if option["name"] == "zephyr_board": - boards = option["choices"] - if option["name"] == "zephyr_model": - models = option["choices"] - - arduino_boards = {boards[i]: models[i] for i in range(len(boards))} - return arduino_boards - - -ZEPHYR_BOARDS = zephyr_boards() +from tvm.contrib.utils import tempdir def pytest_addoption(parser): parser.addoption( "--zephyr-board", required=True, - choices=ZEPHYR_BOARDS.keys(), + choices=test_utils.ZEPHYR_BOARDS.keys(), help=("Zephyr board for test."), ) parser.addoption( @@ -104,4 +76,4 @@ def temp_dir(board): if not os.path.exists(board_workspace.parent): os.makedirs(board_workspace.parent) - return tvm.contrib.utils.tempdir(board_workspace) + return tempdir(board_workspace) diff --git a/tests/micro/zephyr/test_utils.py b/tests/micro/zephyr/test_utils.py new file mode 100644 index 000000000000..54c3de252f8a --- /dev/null +++ b/tests/micro/zephyr/test_utils.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. +import json +import pathlib + + +TEMPLATE_PROJECT_DIR = ( + pathlib.Path(__file__).parent + / ".." + / ".." + / ".." + / "apps" + / "microtvm" + / "zephyr" + / "template_project" +).resolve() + +BOARDS = TEMPLATE_PROJECT_DIR / "boards.json" + + +def zephyr_boards() -> dict: + """Returns a dict mapping board to target model""" + with open(BOARDS) as f: + board_properties = json.load(f) + + boards_model = {board: info["model"] for board, info in board_properties.items()} + return boards_model + + +ZEPHYR_BOARDS = zephyr_boards() + + +def qemu_boards(board: str): + """Returns True if board is QEMU.""" + with open(BOARDS) as f: + board_properties = json.load(f) + + qemu_boards = [name for name, board in board_properties.items() if board["is_qemu"]] + return board in qemu_boards + + +def has_fpu(board: str): + """Returns True if board has FPU.""" + with open(BOARDS) as f: + board_properties = json.load(f) + + fpu_boards = [name for name, board in board_properties.items() if board["fpu"]] + return board in fpu_boards diff --git a/tests/micro/zephyr/test_zephyr.py b/tests/micro/zephyr/test_zephyr.py index b6396ce53315..be1f231156ad 100644 --- a/tests/micro/zephyr/test_zephyr.py +++ b/tests/micro/zephyr/test_zephyr.py @@ -14,14 +14,12 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - import logging import os import pathlib import subprocess import sys import logging -import json import pytest import numpy as np @@ -29,19 +27,12 @@ from PIL import Image import tvm -import tvm.rpc -import tvm.micro -import tvm.testing import tvm.relay as relay from tvm.relay.testing import byoc - from tvm.contrib import utils -from tvm.relay.expr_functor import ExprMutator -from tvm.relay.op.annotation import compiler_begin, compiler_end - from tvm.micro.testing import check_tune_log -import conftest +import test_utils _LOG = logging.getLogger(__name__) @@ -58,16 +49,24 @@ def _make_sess_from_op( def _make_session(temp_dir, zephyr_board, west_cmd, mod, build_config): + config_main_stack_size = None + if test_utils.qemu_boards(zephyr_board): + config_main_stack_size = 1536 + + project_options = { + "project_type": "host_driven", + "west_cmd": west_cmd, + "verbose": bool(build_config.get("debug")), + "zephyr_board": zephyr_board, + } + if config_main_stack_size is not None: + project_options["config_main_stack_size"] = config_main_stack_size + project = tvm.micro.generate_project( - str(conftest.TEMPLATE_PROJECT_DIR), + str(test_utils.TEMPLATE_PROJECT_DIR), mod, temp_dir / "project", - { - "project_type": "host_driven", - "west_cmd": west_cmd, - "verbose": bool(build_config.get("debug")), - "zephyr_board": zephyr_board, - }, + project_options, ) project.build() project.flash() @@ -89,7 +88,7 @@ def _make_add_sess(temp_dir, model, zephyr_board, west_cmd, build_config, dtype= def test_add_uint(temp_dir, board, west_cmd, tvm_debug): """Test compiling the on-device runtime.""" - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} # NOTE: run test in a nested function so cPython will delete arrays before closing the session. @@ -109,22 +108,12 @@ def test_basic_add(sess): test_basic_add(sess) -def has_fpu(zephyr_board): - sys.path.insert(0, str(conftest.TEMPLATE_PROJECT_DIR)) - try: - import microtvm_api_server - finally: - sys.path.pop(0) - - return microtvm_api_server.Handler._has_fpu(zephyr_board) - - # The same test code can be executed on both the QEMU simulation and on real hardware. @tvm.testing.requires_micro def test_add_float(temp_dir, board, west_cmd, tvm_debug): """Test compiling the on-device runtime.""" - model = conftest.ZEPHYR_BOARDS[board] - if not has_fpu(board): + model = test_utils.ZEPHYR_BOARDS[board] + if not test_utils.has_fpu(board): pytest.skip(f"FPU not enabled for {board}") build_config = {"debug": tvm_debug} @@ -150,7 +139,7 @@ def test_basic_add(sess): def test_platform_timer(temp_dir, board, west_cmd, tvm_debug): """Test compiling the on-device runtime.""" - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} # NOTE: run test in a nested function so cPython will delete arrays before closing the session. @@ -178,7 +167,7 @@ def test_basic_add(sess): @tvm.testing.requires_micro def test_relay(temp_dir, board, west_cmd, tvm_debug): """Testing a simple relay graph""" - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} shape = (10,) dtype = "int8" @@ -209,7 +198,7 @@ def test_relay(temp_dir, board, west_cmd, tvm_debug): @tvm.testing.requires_micro def test_onnx(temp_dir, board, west_cmd, tvm_debug): """Testing a simple ONNX model.""" - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} this_dir = pathlib.Path(os.path.dirname(__file__)) @@ -286,7 +275,7 @@ def check_result( @tvm.testing.requires_micro def test_byoc_microtvm(temp_dir, board, west_cmd, tvm_debug): """This is a simple test case to check BYOC capabilities of microTVM""" - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} x = relay.var("x", shape=(10, 10)) w0 = relay.var("w0", shape=(10, 10)) @@ -366,7 +355,7 @@ def _make_add_sess_with_shape(temp_dir, model, zephyr_board, west_cmd, shape, bu @tvm.testing.requires_micro def test_rpc_large_array(temp_dir, board, west_cmd, tvm_debug, shape): """Test large RPC array transfer.""" - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} # NOTE: run test in a nested function so cPython will delete arrays before closing the session. @@ -385,9 +374,8 @@ def test_tensors(sess): @tvm.testing.requires_micro def test_autotune_conv2d(temp_dir, board, west_cmd, tvm_debug): """Test AutoTune for microTVM Zephyr""" - import tvm.relay as relay - - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] + build_config = {"debug": tvm_debug} # Create a Relay model data_shape = (1, 3, 16, 16) @@ -420,18 +408,22 @@ def test_autotune_conv2d(temp_dir, board, west_cmd, tvm_debug): tasks = tvm.autotvm.task.extract_from_program(mod["main"], {}, target) assert len(tasks) > 0 - repo_root = pathlib.Path( - subprocess.check_output(["git", "rev-parse", "--show-toplevel"], encoding="utf-8").strip() - ) - template_project_dir = repo_root / "apps" / "microtvm" / "zephyr" / "template_project" + config_main_stack_size = None + if test_utils.qemu_boards(board): + config_main_stack_size = 1536 + + project_options = { + "zephyr_board": board, + "west_cmd": west_cmd, + "verbose": 1, + "project_type": "host_driven", + } + if config_main_stack_size is not None: + project_options["config_main_stack_size"] = config_main_stack_size + module_loader = tvm.micro.AutoTvmModuleLoader( - template_project_dir=template_project_dir, - project_options={ - "zephyr_board": board, - "west_cmd": west_cmd, - "verbose": 1, - "project_type": "host_driven", - }, + template_project_dir=test_utils.TEMPLATE_PROJECT_DIR, + project_options=project_options, ) timeout = 200 @@ -473,21 +465,7 @@ def test_autotune_conv2d(temp_dir, board, west_cmd, tvm_debug): lowered = tvm.relay.build(mod, target=target, params=params) temp_dir = utils.tempdir() - project = tvm.micro.generate_project( - str(template_project_dir), - lowered, - temp_dir / "project", - { - "zephyr_board": board, - "west_cmd": west_cmd, - "verbose": 1, - "project_type": "host_driven", - }, - ) - project.build() - project.flash() - - with tvm.micro.Session(project.transport()) as session: + with _make_session(temp_dir, board, west_cmd, lowered, build_config) as session: graph_mod = tvm.micro.create_local_graph_executor( lowered.get_graph_json(), session.get_system_lib(), session.device ) @@ -502,21 +480,7 @@ def test_autotune_conv2d(temp_dir, board, west_cmd, tvm_debug): lowered_tuned = tvm.relay.build(mod, target=target, params=params) temp_dir = utils.tempdir() - project = tvm.micro.generate_project( - str(template_project_dir), - lowered_tuned, - temp_dir / "project", - { - "zephyr_board": board, - "west_cmd": west_cmd, - "verbose": 1, - "project_type": "host_driven", - }, - ) - project.build() - project.flash() - - with tvm.micro.Session(project.transport()) as session: + with _make_session(temp_dir, board, west_cmd, lowered_tuned, build_config) as session: graph_mod = tvm.micro.create_local_graph_executor( lowered_tuned.get_graph_json(), session.get_system_lib(), session.device ) diff --git a/tests/micro/zephyr/test_zephyr_aot.py b/tests/micro/zephyr/test_zephyr_aot.py index 6c72d3d7becf..f03b8ecce6d0 100644 --- a/tests/micro/zephyr/test_zephyr_aot.py +++ b/tests/micro/zephyr/test_zephyr_aot.py @@ -14,7 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - import io import logging import os @@ -28,35 +27,19 @@ import numpy as np import tvm -import tvm.rpc -import tvm.micro from tvm.micro.project_api import server -import tvm.testing import tvm.relay as relay -from tvm.contrib import utils from tvm.contrib.download import download_testdata from tvm.micro.interface_api import generate_c_interface_header -import conftest - -_LOG = logging.getLogger(__name__) +import test_utils def _build_project(temp_dir, zephyr_board, west_cmd, mod, build_config, extra_files_tar=None): - template_project_dir = ( - pathlib.Path(__file__).parent - / ".." - / ".." - / ".." - / "apps" - / "microtvm" - / "zephyr" - / "template_project" - ).resolve() project_dir = temp_dir / "project" project = tvm.micro.generate_project( - str(template_project_dir), + str(test_utils.TEMPLATE_PROJECT_DIR), mod, project_dir, { @@ -145,7 +128,7 @@ def test_tflite(temp_dir, board, west_cmd, tvm_debug): ]: pytest.skip(msg="Model does not fit.") - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] input_shape = (1, 32, 32, 3) output_shape = (1, 10) build_config = {"debug": tvm_debug} @@ -227,7 +210,7 @@ def test_qemu_make_fail(temp_dir, board, west_cmd, tvm_debug): if board not in ["qemu_x86", "mps2_an521"]: pytest.skip(msg="Only for QEMU targets.") - model = conftest.ZEPHYR_BOARDS[board] + model = test_utils.ZEPHYR_BOARDS[board] build_config = {"debug": tvm_debug} shape = (10,) dtype = "float32"