From f9efe0ddf865c55d28bcaa203fefffa94bf09b42 Mon Sep 17 00:00:00 2001 From: Kshitij Sisodia Date: Fri, 30 Sep 2022 16:42:50 +0100 Subject: Added Python interface for Arm Ethos-U NPU driver library. Python `ethosu_driver` could be built as part of Arm Ethos-U Linux driver library CMake flow. See driver_library/python/README.md for more details. Change-Id: I177a890add5c13df9a839f4f43621f972afe5ab1 Signed-off-by: Kshitij Sisodia --- driver_library/CMakeLists.txt | 11 +- driver_library/include/ethosu.hpp | 2 +- driver_library/python/CMakeLists.txt | 75 +++ driver_library/python/MANIFEST.in | 1 + driver_library/python/README.md | 336 +++++++++++++ driver_library/python/init_devenv.sh | 10 + driver_library/python/pylintconfig | 427 ++++++++++++++++ driver_library/python/setup.py | 204 ++++++++ .../python/src/ethosu_driver/__init__.py | 6 + .../src/ethosu_driver/_generated/__init__.py | 2 + .../src/ethosu_driver/_utilities/__init__.py | 5 + .../ethosu_driver/_utilities/driver_utilities.py | 186 +++++++ .../python/src/ethosu_driver/inference_runner.py | 100 ++++ .../python/src/ethosu_driver/swig/driver.i | 554 +++++++++++++++++++++ .../src/ethosu_driver/swig/standard_header.i | 50 ++ .../src/ethosu_driver/swig/typemaps/buffer.i | 42 ++ driver_library/python/swig_generate.py | 27 + driver_library/python/test/conftest.py | 34 ++ driver_library/python/test/test_capabilities.py | 73 +++ driver_library/python/test/test_driver.py | 179 +++++++ .../python/test/test_driver_utilities.py | 77 +++ driver_library/python/test/test_inference.py | 50 ++ driver_library/python/test/test_shadow_classes.py | 20 + driver_library/python/test/testdata/download.py | 46 ++ driver_library/src/ethosu.cpp | 2 +- driver_library/src/ethosu_stub.cpp | 2 +- 26 files changed, 2517 insertions(+), 4 deletions(-) create mode 100644 driver_library/python/CMakeLists.txt create mode 100644 driver_library/python/MANIFEST.in create mode 100644 driver_library/python/README.md create mode 100755 driver_library/python/init_devenv.sh create mode 100644 driver_library/python/pylintconfig create mode 100644 driver_library/python/setup.py create mode 100644 driver_library/python/src/ethosu_driver/__init__.py create mode 100644 driver_library/python/src/ethosu_driver/_generated/__init__.py create mode 100644 driver_library/python/src/ethosu_driver/_utilities/__init__.py create mode 100644 driver_library/python/src/ethosu_driver/_utilities/driver_utilities.py create mode 100644 driver_library/python/src/ethosu_driver/inference_runner.py create mode 100644 driver_library/python/src/ethosu_driver/swig/driver.i create mode 100644 driver_library/python/src/ethosu_driver/swig/standard_header.i create mode 100644 driver_library/python/src/ethosu_driver/swig/typemaps/buffer.i create mode 100755 driver_library/python/swig_generate.py create mode 100644 driver_library/python/test/conftest.py create mode 100644 driver_library/python/test/test_capabilities.py create mode 100644 driver_library/python/test/test_driver.py create mode 100644 driver_library/python/test/test_driver_utilities.py create mode 100644 driver_library/python/test/test_inference.py create mode 100644 driver_library/python/test/test_shadow_classes.py create mode 100644 driver_library/python/test/testdata/download.py diff --git a/driver_library/CMakeLists.txt b/driver_library/CMakeLists.txt index 375192f..4c247ae 100644 --- a/driver_library/CMakeLists.txt +++ b/driver_library/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2020,2022 Arm Limited. +# SPDX-FileCopyrightText: Copyright 2020, 2022 Arm Limited and/or its affiliates # # SPDX-License-Identifier: Apache-2.0 # @@ -18,6 +18,9 @@ cmake_minimum_required(VERSION 3.0.2) +option(BUILD_PYTHON_WHL "Build Python wheel package" OFF) +option(BUILD_PYTHON_SRC "Build Python source package" OFF) + # set the project name and version project("driver_library" VERSION 1.0.0 LANGUAGES C CXX) @@ -28,9 +31,15 @@ add_library(ethosu STATIC "src/ethosu.cpp") target_include_directories(ethosu PUBLIC "include") set_target_properties(ethosu PROPERTIES PUBLIC_HEADER "include/ethosu.hpp") set_target_properties(ethosu PROPERTIES VERSION ${PROJECT_VERSION}) +set_target_properties(ethosu PROPERTIES POSITION_INDEPENDENT_CODE ON) # Install library and public headers install(TARGETS ethosu LIBRARY DESTINATION "lib" ARCHIVE DESTINATION "lib" PUBLIC_HEADER DESTINATION "include") + +## Build Python bindings +if (BUILD_PYTHON_WHL OR BUILD_PYTHON_SRC) + add_subdirectory(python) +endif() diff --git a/driver_library/include/ethosu.hpp b/driver_library/include/ethosu.hpp index 74f8abb..15957f4 100644 --- a/driver_library/include/ethosu.hpp +++ b/driver_library/include/ethosu.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 Arm Limited. + * SPDX-FileCopyrightText: Copyright 2020-2022 Arm Limited and/or its affiliates * * SPDX-License-Identifier: Apache-2.0 * diff --git a/driver_library/python/CMakeLists.txt b/driver_library/python/CMakeLists.txt new file mode 100644 index 0000000..5603273 --- /dev/null +++ b/driver_library/python/CMakeLists.txt @@ -0,0 +1,75 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 +# +# 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. +# + +find_package(PythonInterp 3.5 REQUIRED) +if(NOT ${PYTHONINTERP_FOUND}) + message(FATAL_ERROR "Python 3.5 or greater is required to build python driver, but was not found") +endif() + +find_package(SWIG 3.0.12 REQUIRED) +if(NOT ${SWIG_FOUND}) + message(FATAL_ERROR "SWIG 3.0.12 or greater is required to build python driver, but was not found") +endif() + +set(SETUP_PY "${CMAKE_CURRENT_BINARY_DIR}/setup.py") +set(SWIG_GENERATE "${CMAKE_CURRENT_BINARY_DIR}/swig_generate.py") +set(OUT_WRAP "${CMAKE_CURRENT_BINARY_DIR}/pydriver.wrap.timestamp") + +# local env variables passed down to the python scripts +# scripts can thus be used standalone +set(DRIVER_ENV ETHOS_U_DRIVER_INCLUDE="${PROJECT_SOURCE_DIR}/include" + ETHOS_U_DRIVER_LIB=${PROJECT_BINARY_DIR}/lib) + +# common step - generates swig wrappers +add_custom_command(OUTPUT ${OUT_WRAP} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/README.md ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/swig_generate.py ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR}/src + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Clearing Python build ..." + COMMAND ${PYTHON_EXECUTABLE} ${SETUP_PY} --quiet clean --all + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Generating SWIG wrappers ..." + COMMAND ${PYTHON_EXECUTABLE} ${SWIG_GENERATE} + DEPENDS ethosu) + +# source package +if(BUILD_PYTHON_SRC) + set(OUT_SRC "${CMAKE_CURRENT_BINARY_DIR}/pydriver.src.timestamp") + add_custom_command(OUTPUT ${OUT_SRC} + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Building Python source package ..." + COMMAND ${PYTHON_EXECUTABLE} ${SETUP_PY} sdist + COMMAND ${CMAKE_COMMAND} -E touch ${OUT_SRC} + DEPENDS ${OUT_WRAP}) +endif() +# wheel package +if(BUILD_PYTHON_WHL) + + find_package(PythonLibs 3.5 REQUIRED) + if(NOT ${PYTHONLIBS_FOUND}) + message(FATAL_ERROR "Python 3.5 or greater development libraries were not found.") + endif() + + set(OUT_WHL "${CMAKE_CURRENT_BINARY_DIR}/pydriver.whl.timestamp") + add_custom_command(OUTPUT ${OUT_WHL} + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Building Python binary package ..." + COMMAND ${CMAKE_COMMAND} -E env ${DRIVER_ENV} CXX=${CMAKE_CXX_COMPILER} CC=${CMAKE_C_COMPILER} ${PYTHON_EXECUTABLE} ${SETUP_PY} bdist_wheel + COMMAND ${CMAKE_COMMAND} -E touch ${OUT_WHL} + DEPENDS ${OUT_WRAP}) +endif() +add_custom_target(pydriver ALL DEPENDS ${OUT_WRAP} ${OUT_SRC} ${OUT_WHL}) diff --git a/driver_library/python/MANIFEST.in b/driver_library/python/MANIFEST.in new file mode 100644 index 0000000..6356387 --- /dev/null +++ b/driver_library/python/MANIFEST.in @@ -0,0 +1 @@ +prune test diff --git a/driver_library/python/README.md b/driver_library/python/README.md new file mode 100644 index 0000000..7fae749 --- /dev/null +++ b/driver_library/python/README.md @@ -0,0 +1,336 @@ +# About Python ethosu_driver + +Python ethosu_driver is an extension for +[Arm Ethos-U driver library](https://review.mlplatform.org/plugins/gitiles/ml/ethos-u/ethos-u-linux-driver-stack/). +Python ethosu_driver provides interface similar to Ethos-u Linux driver C++ +Api. + +The Python package is built with public headers from the +[driver_library/include](https://review.mlplatform.org/plugins/gitiles/ml/ethos-u/ethos-u-linux-driver-stack/+/refs/heads/master/driver_library/include/) +folder. + +The [SWIG](http://www.swig.org/) tool is used to generate the Ethos-U driver +library Python shadow classes and C wrapper. + +## Python ethosu_driver library installation + +Python ethosu_driver library can be packaged as a source package or a binary +package (wheel). Binary package is platform dependent, the name of the +package will indicate the platform it was built for, e.g.: + +* Linux Aarch 64 bit machine: ethosu_driver-1.0.0-cp37-cp37m-linux_aarch64.whl + +The source package is platform independent but installation will involve +Ethos-U driver library C wrapper compilation on a target machine. +You will need to have g++ compatible with C++ 14 standard and a Python +development library installed on the target machine. + +Python driver binary package is linked statically with C++ Ethos-U driver +library and can operate independently from it when built. +Python driver source package requires static Ethos-U driver library - +libethosu.a - and public header during installation, thus they must be +present on the target machine. + +### Installing from wheel + +Install ethosu_driver from binary by pointing to the wheel file: + +1) If corresponding wheel is available for your platform architecture in the +public repository. + ``` + pip install ethosu_driver + ``` +2) If you have local wheel file. Example: + ``` + pip install /path/to/ethosu_driver-X.X.X-cp37-cp37m-linux_aarch64.whl + ``` + +### Installing from source package + +While installing from sources, you can choose Ethos-U driver library to +be used. By default, library will be searched in standard for your system +locations. You can check them by running: +``` +gcc --print-search-dirs +``` +Headers will be searched in standard include directories for your system. +If Ethos-U driver library has custom location, set environment variables +*ETHOS_U_DRIVER_LIB* and *ETHOS_U_DRIVER_INCLUDE* to point to Ethos-U driver +library (libethosu.a) and header (ethosu.hpp): +``` +export ETHOS_U_DRIVER_LIB=/path/to/lib +export ETHOS_U_DRIVER_INCLUDE=/path/to/headers +``` + +Installing from the public repository. +``` +pip install ethosu_driver +``` +Installing from local file. +``` +pip install /path/to/ethosu_driver-X.X.X.tar.gz +``` + +If ethosu_driver installation script fails to find Ethos-U driver libraries it +will raise an error like this + +`RuntimeError: Ethos-U driver library was not found in +('/usr/lib/gcc/aarch64-linux-gnu/8/', <...> ,'/lib/', '/usr/lib/'). +Please install driver to one of the standard locations or set correct +ETHOS_U_DRIVER_INCLUDE and ETHOS_U_DRIVER_LIB env variables.` + +You can now verify that ethosu_driver library is installed and check +ethosu_driver version using: + +``` +pip show ethosu_driver +``` + +## Building Python ethosu_driver library locally + +### Install SWIG + +We suggest to use SWIG version 3.0.12 or newer. You can check available swig +version for you system here: https://pkgs.org/download/swig. + +For example, install the tool with Ubuntu package manager as follows: +``` +sudo apt install swig +``` +If your system has swig version less than 3.0.12, please, build and install +from sources: + +1. Download SWIG: + ``` + wget https://github.com/swig/swig/archive/refs/tags/v4.0.2.zip + unzip v4.0.2.zip + ``` +2. Build and install SWIG: + ``` + cd swig-4.0.2 + ./autogen.sh + ./configure --prefix= + make + make install + ``` + +### Building as part of cmake flow + +To build Python ethosu_driver as part of Ethos-U NPU Linux driver stack provide +the following cmake flags: +1) For source distribution + ``` + -DBUILD_PYTHON_SRC=1 + ``` +2) For wheel distribution + ``` + -DBUILD_PYTHON_WHL=1 + ``` +Note: you will need to have a Python instance for your target platform to build +wheel. For example, if you are building for an AArch64 platform, you will need +Python installation for aarch64-linux-gnu tool-chain. + +Build result can be found in `/python/dist`. + +### Building standalone + +Navigate to `driver_libarary/python` and execute: +``` +python3 setup.py clean --all +python3 ./swig_generate.py +python3 setup.py sdist +``` +Build result can be found in `driver_libarary/python/dist`. + +## Python ethosu_driver API overview + +### Getting started + +After the Python driver library is installed with pip and can be accessed +within your work environment, import it in your script: + +```python +import ethosu_driver as driver +``` + +Create a device. You can ping Ethos-U device with `ping` method: + +```python +device = driver.Device("/dev/ethosu0") +device.ping() +``` + +You can create memory buffer with data from a binary file or Python buffer +object: + +```python +# from file: +network_file = "/path/to/model.tflite" +network_buffer = driver.Buffer(device, network_file) + +# from numpy: +ifm_zeros = numpy.zeros(ifm_size, dtype=np.uint8) +ifm_buffer = driver.Buffer(device, ifm_size) +ifm_buffer.from_buffer(ifm_zeros.data) +``` + +To create a network object, provide memory buffer for the model file and +created device: + +```python +network = driver.Network(device, network_buffer) +``` + +Inference object is instantiated with a network object and lists of input +memory buffers and output memory buffers. +For example: + +```python +ifms = [ifm_buffer] + +ofms = [] +for ofm_size in network.getOfmDims(): + ofm_buffer = driver.Buffer(device, ofm_size) + ofms.append(ofm_buffer) + +inference = driver.Inference(network, ifms, ofms) +``` + +To execute the inference and wait for the callback: + +```python +# wait infinitely +inference.wait() + +# wait with a timeout in nano seconds: +inference.wait(timeoutNanos=60e9) +``` + +To read results of the inference, iterate through available outputs and convert +them to numpy array: + +```python +for buffer_out in inference.getOfmBuffers(): +array = np.frombuffer(buffer_out.data(), dtype=np.uint8) +``` + +See inline py docs for more info on driver public API. + +## Inference runner + +Python ethosu_driver library comes with a script `inference_runner` that is +installed to the Python environment `bin` directory and could be invoked +from a command line by the file name: + +```cmd +inference_runner +``` + +Arguments: + +* `--device` : Npu device name. Default: ethosu0. +* `--model` : Tflite model file path. +* `--timeout` : inference timeout in seconds, Default: infinite. +* `--inputs` : list of files containing input feature maps. +* `--output_dir` : directory to store inference results, output feature maps. +Default: current directory. +* `--npy` : Use npy input/output. Default: 0. +* `--profile_counters`: profile counters to measure during inference, accepts +four integers chosen from +[HW Supported Ethos-U PMU +Events](https://review.mlplatform.org/plugins/gitiles/ml/ethos-u/ethos-u-core-driver/+/refs/heads/master/include/pmu_ethosu.h). + +Example: + +```cmd +inference_runner --device ethosu0 --model ./mobilenet_v2_vela.tflite --timeout +60 --npy 1 --inputs ./input1.npy --output_dir ./ofms +``` + +## Using inference runner with numpy + +Python ethosu_driver libarary could be installed with numpy support. +Numpy will be automatically downloaded and installed alongside ethosu_driver. +If your machine does not have access to pypi repositories you might need to +install NumPy in advance by following public instructions: +, or to have it installed from wheel package +built for your platform. + +Now, if you provide inference runner command line parameter `--npy 1` the +script will interpret input files as numpy array exports and will save output +feature maps as numpy arrays. + +## Setup development environment + +Before, proceeding to the next steps, make sure that: + +1. You have Python 3.6+ installed system-side. The package is not compatible +with older Python versions. +2. You have python3.6-dev installed system-side. This contains header files +needed to build ethosu_driver extension module. +3. In case you build Python from sources manually, make sure that the following +libraries are installed and available in you system: +``python3.6-dev build-essential checkinstall libreadline-gplv2-dev +libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev +libbz2-dev`` +4. install SWIG, swig must be version 4.* + +## Setup virtual environment + +Now you can proceed with setting up workspace: + +1. Set environment variables ETHOS_U_DRIVER_LIB (pointing to Ethos-U driver +library) and ETHOS_U_DRIVER_INCLUDE (pointing to Ethos-U driver library +headers) +2. Create development env using script ``init_devenv.sh`` + +## Generating SWIG wrappers + +Before building package or running tests you need to generate SWIG wrappers +based on the interface [files](src/ethosu_driver/swig). +```commandline +python setup.py clean --all +python ./swig_generate.py +``` + +## Running unit-tests + +Tests could be executed only on a system with Ethos-U NPU device. +Pytest is used as unit-test framework, before running the test you need to +install pytest and numpy or include into your rootfs image: + +```commandline +pip install pytest +pip install numpy +``` + +Execute command from the project root dir: +``` +pytest -v +``` + +## Regenerate SWIG stubs inplace + +If you want to check that swig wrappers are compiling correctly, you can issue +extension compilation inplace: + +1) clean old generated files: + + ```bash + python setup.py clean --all + ``` + +2) Run swig wrapper source generation as described in [Generating SWIG +wrappers](#generating-swig-wrappers) +3) Run `build_ext` command: + + ```bash + export ETHOS_U_DRIVER_LIB=/path/to/driver/lib + export ETHOS_U_DRIVER_INCLUDE=/path/to/driver/include + python setup.py build_ext --inplace + ``` + +It will put all generated files under ./src/ethosu_driver/_generated folder. +Command will fail on x86 machine during linkage phase because Ethos-U driver +is built for Arm platform, but compilation stage should pass successfully +(or give you indication of compilation problems). diff --git a/driver_library/python/init_devenv.sh b/driver_library/python/init_devenv.sh new file mode 100755 index 0000000..53544e7 --- /dev/null +++ b/driver_library/python/init_devenv.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 + +set -e +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +python3 -m venv devenv +source devenv/bin/activate +python "${SCRIPT_DIR}"/test/testdata/download.py diff --git a/driver_library/python/pylintconfig b/driver_library/python/pylintconfig new file mode 100644 index 0000000..df550a2 --- /dev/null +++ b/driver_library/python/pylintconfig @@ -0,0 +1,427 @@ +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,generated,_generated + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=_version.py + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=colorized + +# Tells whether to display a full report or only the messages +reports=yes + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/driver_library/python/setup.py b/driver_library/python/setup.py new file mode 100644 index 0000000..f90cd26 --- /dev/null +++ b/driver_library/python/setup.py @@ -0,0 +1,204 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import logging +import os +from functools import lru_cache +from itertools import chain +from pathlib import Path + +from setuptools import setup +from distutils.core import Extension +from setuptools.command.build_py import build_py + +logger = logging.Logger(__name__) + + +def linux_gcc_lib_search(): + """ + Calls the `gcc` to get linker default system paths. + Returns: + list of paths + """ + cmd = 'gcc --print-search-dirs | grep libraries' + cmd_res = os.popen(cmd).read() + cmd_res = cmd_res.split('=') + if len(cmd_res) > 1: + return tuple(cmd_res[1].split(':')) + return None + + +def find_includes(include_env: str = 'ETHOS_U_DRIVER_INCLUDE'): + include_path = os.getenv(include_env, '') + return [include_path] if include_path else ['/usr/local/include', '/usr/include'] + + +@lru_cache(maxsize=1) +def find_driver(lib_name: str, + optional: bool = False, + libs_env: str = 'ETHOS_U_DRIVER_LIB', + default_lib_search: tuple = linux_gcc_lib_search()): + """ + Searches for library installation on the local machine. + + Args: + lib_name: library name to find + optional: Do not fail if optional. Default is False - fail if library was not found. + libs_env: custom environment variable pointing to libraries location, default is 'ETHOS_U_DRIVER_LIB' + default_lib_search: list of paths to search for a library if not found within path provided by + 'ETHOS_U_DRIVER_LIB' env variable + + Returns: + tuple containing name of the driver libs, paths to the libs + """ + + lib_path = os.getenv(libs_env, "") + + lib_search = [lib_path] if lib_path else default_lib_search + + libs = dict(map(lambda path: (':{}'.format(path.name), path), + chain.from_iterable(map(lambda lib_path: Path(lib_path).glob(lib_name), + lib_search)))) + if not optional and len(libs) == 0: + raise RuntimeError("""Ethos-U driver library {} was not found in {}. Please install driver to one of the standard + locations or set correct ETHOS_U_DRIVER_INCLUDE and ETHOS_U_DRIVER_LIB env variables.""" + .format(lib_name, lib_search)) + + # gives back tuple of names of the libs, set of unique libs locations and includes. + return list(libs.keys()), list(set( + map(lambda path: str(path.absolute().parent), libs.values()))) + + +class LibFinderExtension(Extension): + """ + Derived from `Extension` this class adds libraries search on the user's machine. + SWIG options and compilation flags are updated with relevant libraries files locations (-L) and headers (-I). + + Search for the library is executed only when attributes include_dirs, library_dirs, runtime_library_dirs, libraries or + swig_opts are queried. + + """ + + def __init__(self, name, sources, libs, include_dirs=None, define_macros=None, undef_macros=None, + library_dirs=None, + libraries=None, runtime_library_dirs=None, extra_objects=None, extra_compile_args=None, + extra_link_args=None, export_symbols=None, language=None, **kw): + self._include_dirs = None + self._library_dirs = None + self._runtime_library_dirs = None + self._libs = libs + super().__init__(name, sources, include_dirs, define_macros, undef_macros, library_dirs, libraries, + runtime_library_dirs, extra_objects, extra_compile_args, extra_link_args, export_symbols, + language, **kw) + + @property + def include_dirs(self): + return self._include_dirs + find_includes() + + @include_dirs.setter + def include_dirs(self, include_dirs): + self._include_dirs = include_dirs + + @property + def library_dirs(self): + library_dirs = self._library_dirs + for lib in self._libs: + _, lib_path = find_driver(lib) + library_dirs = library_dirs + lib_path + + return library_dirs + + @library_dirs.setter + def library_dirs(self, library_dirs): + self._library_dirs = library_dirs + + @property + def runtime_library_dirs(self): + library_dirs = self._runtime_library_dirs + for lib in self._libs: + _, lib_path = find_driver(lib) + library_dirs = library_dirs + lib_path + + return library_dirs + + @runtime_library_dirs.setter + def runtime_library_dirs(self, runtime_library_dirs): + self._runtime_library_dirs = runtime_library_dirs + + @property + def libraries(self): + libraries = self._libraries + for lib in self._libs: + lib_names, _ = find_driver(lib) + libraries = libraries + lib_names + + return libraries + + @libraries.setter + def libraries(self, libraries): + self._libraries = libraries + + def __eq__(self, other): + return self.__class__ == other.__class__ and self.name == other.name + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return self.name.__hash__() + + +class ExtensionPriorityBuilder(build_py): + """ + Runs extension builder before other stages. Otherwise generated files are not included to the distribution. + """ + + def run(self): + self.run_command('build_ext') + return super().run() + + +if __name__ == '__main__': + # mandatory extensions + driver_module = LibFinderExtension('ethosu_driver._generated._driver', + sources=['src/ethosu_driver/_generated/driver_wrap.cpp'], + extra_compile_args=['-std=gnu++14'], + language='c++', + libs=['libethosu.a'] + ) + + extensions_to_build = [driver_module] + + setup( + name='ethosu_driver', + version='1.0.0', + author='Arm ltd', + author_email='support@arm.com', + description='Arm Ethos-U NPU Linux Driver Stack Python wrapper', + url='https://git.mlplatform.org/ml/ethos-u/ethos-u-linux-driver-stack.git/', + license='Apache License 2.0', + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", + "Programming Language :: C", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Topic :: Scientific/Engineering :: Artificial Intelligence" + ], + keywords=["ethos-u", "driver", "npu"], + package_dir={'': 'src'}, + packages=[ + 'ethosu_driver', + 'ethosu_driver._generated', + 'ethosu_driver._utilities' + ], + data_files=[('', ['LICENSE'])], + entry_points={"console_scripts": ["inference_runner = ethosu_driver.inference_runner:main"]}, + python_requires='>=3.5', + extras_require={"numpy": ["numpy"]}, + cmdclass={'build_py': ExtensionPriorityBuilder}, + ext_modules=extensions_to_build + ) diff --git a/driver_library/python/src/ethosu_driver/__init__.py b/driver_library/python/src/ethosu_driver/__init__.py new file mode 100644 index 0000000..ee6ea1f --- /dev/null +++ b/driver_library/python/src/ethosu_driver/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 + +from ._generated.driver import Device, Inference, Network, Buffer +from ._utilities import open_device, load_model, populate_buffers, \ + allocate_buffers, get_results, InferenceRunner diff --git a/driver_library/python/src/ethosu_driver/_generated/__init__.py b/driver_library/python/src/ethosu_driver/_generated/__init__.py new file mode 100644 index 0000000..6a36657 --- /dev/null +++ b/driver_library/python/src/ethosu_driver/_generated/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 diff --git a/driver_library/python/src/ethosu_driver/_utilities/__init__.py b/driver_library/python/src/ethosu_driver/_utilities/__init__.py new file mode 100644 index 0000000..c0dc1eb --- /dev/null +++ b/driver_library/python/src/ethosu_driver/_utilities/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 + +from .driver_utilities import open_device, load_model, populate_buffers, \ + allocate_buffers, get_results, InferenceRunner diff --git a/driver_library/python/src/ethosu_driver/_utilities/driver_utilities.py b/driver_library/python/src/ethosu_driver/_utilities/driver_utilities.py new file mode 100644 index 0000000..216895a --- /dev/null +++ b/driver_library/python/src/ethosu_driver/_utilities/driver_utilities.py @@ -0,0 +1,186 @@ +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +import logging +import time +from typing import List +from .._generated.driver import Device, Inference, Network, Buffer, InferenceStatus_OK + + +def open_device(device: str) -> Device: + """Opens the Ethos-U device file descriptor. + + Args: + device: device name. + + Returns: + `Device`: Return the object that represents Ethos-U device file descriptor and manages Ethos-U device lifecycle. + """ + device = Device("/dev/{}".format(device)) + return device + + +def load_model(device: Device, model: str) -> Network: + """Create a `Network` when providing `Device` object and a string containing tflite file path. + + Args: + device: `Device` object that Ethos-U device file descriptor. + model: tflite model file path . + + Returns: + `Network`: Return the object that represent the neural __network file descriptor received from the Ethos-U device. + """ + logging.info("Creating network") + network_buffer = Buffer(device, model) + return Network(device, network_buffer) + + +def populate_buffers(input_data: List[bytearray], buffers: List[Buffer]): + """Set the feature maps associated memory buffer with the given data. + + Args: + input_data: list of input feature maps data. + buffers: list of already initialized ifm buffers. + Raises: + RuntimeError: if input data size is incorrect. + """ + number_of_buffers = len(buffers) + + if number_of_buffers != len(input_data): + raise RuntimeError("Incorrect number of inputs, expected {}, got {}.".format(number_of_buffers, len(input_data))) + + for index, (buffer, data_chunk) in enumerate(zip(buffers, input_data)): + cap = buffer.capacity() + logging.info("Copying data to a buffer {} of {} with size = {}".format(index + 1, number_of_buffers, cap)) + + if len(data_chunk) > cap: + raise RuntimeError("Buffer expects {} bytes, got {} bytes.".format(cap, len(data_chunk))) + buffer.resize(len(data_chunk)) + buffer.from_buffer(data_chunk) + + +def allocate_buffers(device: Device, dimensions: List) -> List[Buffer]: + """Returns output feature maps associated with memory buffers. + + Args: + device: `Device` object that Ethos-U device file descriptor. + dimensions: `Network` object that represent the neural __network file descriptor. + + Returns: + list: output feature map buffers. + """ + buffers = [] + total = len(dimensions) + for index, size in enumerate(dimensions): + logging.info("Allocating {} of {} buffer with size = {}".format(index + 1, total, size)) + buffer = Buffer(device, size) + buffers.append(buffer) + + return buffers + + +def get_results(inference: Inference) -> List[Buffer]: + """Retrieves output inference buffers + + Args: + inference: `Inference` object that represents the inference file descriptor. + + Returns: + list: list of buffer objects + Raises: + RuntimeError: in case of inference returned failure status. + + """ + if InferenceStatus_OK != inference.status(): + raise RuntimeError("Inference failed!") + else: + logging.info("Inference succeeded!") + return inference.getOfmBuffers() + + +class InferenceRunner: + """Helper class to execute inference.""" + + def __init__(self, device_name: str, model: str): + """Initialises instance to execute inferences on the given model with given device + + Device is opened with the name '/dev/'. + Input/Output feature maps memory is allocated. + + Args: + device_name: npu device name + model: Tflite model file path + """ + self.__device = open_device(device_name) + if not InferenceRunner.wait_for_ping(self.__device, 3): + raise RuntimeError("Failed to communicate with device {}".format(device_name)) + + self.__network = load_model(self.__device, model) + # it is important to have a reference to current inference object to have access to OFMs. + self.__inf = None + self.__enabled_counters = () + + @staticmethod + def wait_for_ping(device: Device, count: int) -> bool: + if count == 0: + return False + try: + device.ping() + return True + except: + logging.info("Waiting for device: {}".format(count)) + time.sleep(0.5) + return InferenceRunner.wait_for_ping(device, count-1) + + def set_enabled_counters(self, enabled_counters: List[int] = ()): + """Set the enabled performance counter to use during inference. + + Args: + enabled_counters: list of integer counter to enable. + Raises: + ValueError: in case of inference returned failure status or the Pmu counter requests exceed the maximum supported. + """ + max_pmu_events = Inference.getMaxPmuEventCounters() + if len(enabled_counters) > max_pmu_events: + raise ValueError("Number of PMU counters requested exceed the maximum supported ({}).".format(max_pmu_events)) + self.__enabled_counters = enabled_counters + + def run(self, input_data: List[bytearray], timeout: int) -> List[Buffer]: + """Run a inference with the given input feature maps data. + + Args: + input_data: data list containing input data as binary arrays + timeout: inference timout in nano seconds + + Returns: + list: list of buffer objects + """ + ofms = allocate_buffers(self.__device, self.__network.getOfmDims()) + ifms = allocate_buffers(self.__device, self.__network.getIfmDims()) + populate_buffers(input_data, ifms) + + self.__inf = Inference( + self.__network, + ifms, + ofms, + self.__enabled_counters, + True) + + self.__inf.wait(int(timeout)) + return get_results(self.__inf) + + def get_pmu_counters(self) -> List: + """Return the PMU data for the inference run. + + Returns: + list: pairs of PMU type and cycle count value + """ + return list(zip(self.__enabled_counters, self.__inf.getPmuCounters())) + + def get_pmu_total_cycles(self) -> int: + """ + Returns the total cycle count, including idle cycles, as reported by + the PMU + + Returns: total cycle count + """ + return self.__inf.getCycleCounter() diff --git a/driver_library/python/src/ethosu_driver/inference_runner.py b/driver_library/python/src/ethosu_driver/inference_runner.py new file mode 100644 index 0000000..1998465 --- /dev/null +++ b/driver_library/python/src/ethosu_driver/inference_runner.py @@ -0,0 +1,100 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +from argparse import ArgumentParser +import os +import logging +from pathlib import Path +from typing import List + +import ethosu_driver as driver +try: + import numpy as np + with_numpy = True +except ImportError: + with_numpy = False + + +def read_bin_file_to_buf(file_path: str) -> bytearray: + with open(file_path, 'rb') as f: + return bytearray(f.read()) + + +def read_npy_file_to_buf(file_path: str) -> bytearray: + ifm_arr = np.load(file_path).astype(dtype=np.int8, order='C') + return ifm_arr.flatten().data + + +def read_ifms(ifm_files: List[str], use_npy: bool = False): + read_file_to_buf = read_npy_file_to_buf if use_npy else read_bin_file_to_buf + for ifm_file in ifm_files: + yield read_file_to_buf(ifm_file) + + +def write_npy(dir: str, file_name: str, data: memoryview): + ar = np.frombuffer(data, dtype=np.int8) + file_path = os.path.join(dir, "{}.npy".format(file_name)) + if os.path.isfile(file_path): + os.remove(file_path) + np.save(file_path, ar) + logging.info("File saved to {}".format(file_path)) + + +def write_bin_file(dir: str, file_name: str, data: memoryview): + file_path = os.path.join(dir, "{}.bin".format(file_name)) + if os.path.isfile(file_path): + os.remove(file_path) + with open(file_path, "wb") as f: + f.write(data) + logging.info("File saved to {}".format(file_path)) + + +def write_ofm(buf: memoryview, ofm_index: int, model_path: str, output_dir: str, use_npy: bool = False): + write_buf_to_file = write_npy if use_npy else write_bin_file + model_file_name = Path(model_path).name + ofm_name = "{}_ofm_{}".format(model_file_name, ofm_index) + write_buf_to_file(output_dir, ofm_name, buf) + + +def main(): + format = "%(asctime)s %(levelname)s - %(message)s" + logging.basicConfig(format=format, level=logging.INFO) + + parser = ArgumentParser() + parser.add_argument("--device", help="Npu device name. Default: ethosu0", default="ethosu0") + parser.add_argument("--model", help="Tflite model file path", required=True) + parser.add_argument("--timeout", help="Inference timout in seconds, Default: infinite", default=-1, type=int) + parser.add_argument("--inputs", nargs='+', help="list of files containing input feature maps", required=True) + parser.add_argument("--output_dir", help="directory to store inference results, output feature maps. " + "Default: current directory", default=os.getcwd()) + parser.add_argument("--npy", help="Use npy input/output", default=0, type=int) + parser.add_argument("--profile_counters", help="Performance counters to profile", nargs=4, type=int, required=True) + args = parser.parse_args() + + use_numpy = with_numpy & bool(int(args.npy)) + if use_numpy: + logging.info("Running with numpy inputs/outputs") + else: + logging.info("Running with byte array inputs/outputs") + + # @TODO: Discuss if this is needed anymore. Remove this commented line, if not. + # driver.reset() + + ifms_data = read_ifms(args.inputs, use_numpy) + + runner = driver.InferenceRunner(args.device, args.model) + runner.set_enabled_counters(args.profile_counters) + ofm_buffers = runner.run(list(ifms_data), int(args.timeout)) + + for index, buffer_out in enumerate(ofm_buffers): + logging.info("Output buffer size: {}".format(buffer_out.size())) + write_ofm(buffer_out.data(), index, args.model, args.output_dir, use_numpy) + + inference_pmu_counters = runner.get_pmu_counters() + + # Profiling + total_cycles = runner.get_pmu_total_cycles() + for pmu, value in inference_pmu_counters: + logging.info("\tNPU %d counter: %d", pmu, value) + logging.info("\tNPU TOTAL cycles: %d", total_cycles) diff --git a/driver_library/python/src/ethosu_driver/swig/driver.i b/driver_library/python/src/ethosu_driver/swig/driver.i new file mode 100644 index 0000000..4cd8bdf --- /dev/null +++ b/driver_library/python/src/ethosu_driver/swig/driver.i @@ -0,0 +1,554 @@ +// +// SPDX-FileCopyrightText: Copyright 2020, 2022 Arm Limited and/or its affiliates +// SPDX-License-Identifier: Apache-2.0 +// +%module driver +%{ +#define SWIG_FILE_WITH_INIT +%} + +//typemap definitions and other common stuff +%include "standard_header.i" + +%{ +#include "ethosu.hpp" +#include +#include +#include +#include +#include +#include + +#define ETHOSU_IOCTL_BASE 0x01 +#define ETHOSU_IO(nr) _IO(ETHOSU_IOCTL_BASE, nr) +#define ETHOSU_IOCTL_PING ETHOSU_IO(0x00) + +%} +%include + +%shared_ptr(EthosU::Buffer); +%shared_ptr(EthosU::Network); + + +namespace std { + %template(UintVector) vector; + %template(SizeTVector) vector; + %template(SharedBufferVector) vector>; +} + +namespace EthosU +{ + +%feature("docstring", +" +Semantic Version : major.minor.patch +") SemanticVersion; +%nodefaultctor SemanticVersion; +class SemanticVersion { +public: + SemanticVersion(uint32_t major = 0, uint32_t minor = 0, uint32_t patch = 0); + + uint32_t major; + uint32_t minor; + uint32_t patch; +}; + +%extend SemanticVersion { + std::string __str__() const { + std::ostringstream out; + out << *$self; + return out.str(); + } +} + +%feature("docstring", +" +Hardware Identifier which consists of version status, version revision, product revision and architecture revision. +") HardwareId; +class HardwareId { +public: + HardwareId(uint32_t versionStatus, SemanticVersion& version, SemanticVersion& product, SemanticVersion& arch); + + uint32_t versionStatus{0}; + SemanticVersion version{}; + SemanticVersion product{}; + SemanticVersion architecture{}; +}; + +%extend HardwareId { + std::string __str__() const { + std::ostringstream out; + out << "{versionStatus=" << $self->versionStatus << + ", version=" << EthosU_SemanticVersion___str__(&$self->version) << + ", product=" << EthosU_SemanticVersion___str__(&$self->product) << + ", architecture=" << EthosU_SemanticVersion___str__(&$self->architecture) << "}"; + return out.str(); + } +} + +%feature("docstring", +" +Hardware Configuration object defines specific configuration including MACs per clock cycle and NPU command stream +version. This also specifies is custom DMA is enabled or not. +") HardwareConfiguration; +%nodefaultctor HardwareConfiguration; +class HardwareConfiguration { + public: + HardwareConfiguration(uint32_t macs = 0, uint32_t cmdStreamVersion = 0, bool customDma = false); + + uint32_t macsPerClockCycle; + uint32_t cmdStreamVersion; + bool customDma; +}; + +%extend HardwareConfiguration { + std::string __str__() const { + std::ostringstream out; + out << "{macsPerClockCycle=" << $self->macsPerClockCycle << + ", cmdStreamVersion=" << $self->cmdStreamVersion << + ", customDma=" << ($self->customDma? "True": "False") << "}"; + return out.str(); + } +} + +%feature("docstring", +" +Device capabilities object which specifies capabilities based on hardware ID, configuration and semantic version. +") Capabilities; +class Capabilities { + public: + Capabilities() {} + Capabilities(const HardwareId& hwId, const HardwareConfiguration& hwCfg, const SemanticVersion& driverVersion); + + HardwareId hwId; + HardwareConfiguration hwCfg; + SemanticVersion driver; +}; + +%extend Capabilities { + std::string __str__() const { + std::ostringstream out; + out << "{hwId=" << EthosU_HardwareId___str__(&$self->hwId) << + ", hwCfg=" << EthosU_HardwareConfiguration___str__(&$self->hwCfg) << + ", driver=" << EthosU_SemanticVersion___str__(&$self->driver) << "}"; + return out.str(); + } +} + +%feature("docstring", +" +Device object represents Ethos-U device file descriptor and manages Ethos-U device lifecycle. +Constructor accepts device name and opens file descriptor with O_RDWR | O_NONBLOCK flags. +When the object is destroyed - device file descriptor is closed. +") Device; +%nodefaultctor Device; +class Device { +public: + Device(const char *device); + + %feature("docstring", + " + Performs the I/O control operation on the Ethos-U device. + + Args: + cmd: Command code + data: Command data + Returns: + int: Return value depends on command. Usually -1 indicates error. + ") ioctl; + int ioctl(unsigned long cmd, void *data = nullptr) const; + + %feature("docstring", + " + Returns the capabilities of the Ethos-U device. + + Returns: + Capabilities: Return capabilities of device. + ") capabilities; + Capabilities capabilities() const; +}; + +%extend Device { + + %feature("docstring", + " + Sends ping command to the Ethos-U device. + + See ETHOSU_IOCTL_PING from kernel module uapi/ethosu.h + ") ping; + void ping() { + $self->ioctl(ETHOSU_IOCTL_PING); + } +} + +%feature("docstring", + " + Buffer object represents a RW mapping in the virtual address space of the caller. + + Created mapping is shareable, updates to the mapping are visible to other processes mapping the same region. + Issues ETHOSU_IOCTL_BUFFER_CREATE I/O request to the device with given Maximum capacity. + + Buffer could be created for a device with given maximum capacity or instantiated directly from + a file containing binary data. + + Examples: + >>> import ethosu_driver as driver + >>> # from file: + >>> buf = driver.Buffer(device, '/path/to/file') + >>> # Empty, with maximum capacity: + >>> buf = driver.Buffer(device, 1024) + ") Buffer; +%nodefaultctor Buffer; +class Buffer { +public: + Buffer(const Device &device, const size_t capacity); + + %feature("docstring", + " + Returns maximum buffer capacity set during initialisation. + + Returns: + int: maximum buffer capacity. + ") capacity; + size_t capacity() const; + + %feature("docstring", + " + Sets the size of the device buffer to 0. + ") clear; + void clear() const; + + %feature("docstring", + " + Returns a readonly view to the mapped memory. + + Returns: + memoryview: readonly memory data. + ") data; + %driver_buffer_out; + char* data() const; + %clear_driver_buffer_out; + + %feature("docstring", + " + Sets a size of the memory buffer for the device. + + 'offset + size' must not exceed the capacity of the buffer. + Does not change the size of the mapped region. + + Issues ETHOSU_IOCTL_BUFFER_SET I/O request with a given size and offset. + + Args: + size (int): Device buffer size. + offset (int): Offset to where the data starts. + ") resize; + void resize(size_t size, size_t offset = 0) const; + + %feature("docstring", + " + Queries device and returns buffer data offset. + + Issues ETHOSU_IOCTL_BUFFER_GET I/O request. + + Returns: + int: data offset + ") offset; + size_t offset() const; + + %feature("docstring", + " + Queries device and returns buffer data size. + + Issues ETHOSU_IOCTL_BUFFER_GET I/O request. + + Returns: + int: current device buffer size. + ") size; + size_t size() const; + + %feature("docstring", + " + Returns buffer file descriptor id. + + Returns: + int: file descriptor id. + ") getFd; + int getFd() const; +}; + +%extend Buffer { + + Buffer(const Device& device, const std::string& filename) { + std::ifstream stream(filename, std::ios::binary); + if (!stream.is_open()) { + throw EthosU::Exception(std::string("Failed to open file: ").append(filename).c_str()); + } + + stream.seekg(0, std::ios_base::end); + size_t size = stream.tellg(); + stream.seekg(0, std::ios_base::beg); + + auto buffer = new EthosU::Buffer(device, size); + buffer->resize(size); + stream.read(buffer->data(), size); + + return buffer; + } + + %feature("docstring", + " + Fills the buffer from python buffer. + + Copies python buffer data to the mapped memory region. + Input buffer size must be within `Buffer` maximum capacity. + + Args: + buffer: data to be copied to the mapped memory. + + ") from_buffer; + %mutable_buffer(char* buffer, size_t size); + void from_buffer(char* buffer, size_t size) { + self->resize(size); + char* data = $self->data(); + std::memcpy(data, buffer, size); + } + %clear_mutable_buffer(char* buffer, size_t size); +} + +%feature("docstring", + " + Represents the neural network file descriptor received from the Ethos-U device. + + `Network` is created providing `Device` object and a `Buffer` containing tflite file data. + Network creation issues ETHOSU_IOCTL_NETWORK_CREATE I/O request with buffer file descriptor id. + Provided `Buffer` data is parsed into tflite Model object and input/output feature maps sizes are saved. + + Destruction of the object closes network file descriptor. + ") Network; +%nodefaultctor Network; +class Network { +public: + + %feature("docstring", + " + Performs the I/O control operation with network buffer device. + + Args: + cmd: Command code + data: Command data + Returns: + int: Return value depends on command. Usually -1 indicates error. + ") ioctl; + int ioctl(unsigned long cmd, void *data); + + %feature("docstring", + " + Returns associated memory buffer. + + Returns: + `Buffer`: buffer object used during initialisation. + ") getBuffer; + std::shared_ptr getBuffer(); + + %feature("docstring", + " + Returns saved sizes of the neural network model input feature maps. + + Returns: + list: sizes of all input feature maps + ") getIfmDims; + const std::vector &getIfmDims() const; + + %feature("docstring", + " + Returns total size of all input feature maps. + + Returns: + int: total size of all input feature maps + ") getIfmSize; + size_t getIfmSize() const; + + %feature("docstring", + " + Returns saved sizes of the neural network model output feature maps. + + Returns: + list: sizes of all output feature maps + ") getOfmDims; + const std::vector &getOfmDims() const; + + %feature("docstring", + " + Returns total size of all output feature maps. + + Returns: + int: total size of all output feature maps + ") getOfmSize; + size_t getOfmSize() const; +}; + +%extend Network { + Network(const Device &device, std::shared_ptr &buffer) + { + if(buffer == nullptr){ + throw EthosU::Exception(std::string("Failed to create the network, buffer is nullptr.").c_str()); + } + auto network = new EthosU::Network(device, buffer); + return network; + } +} + +%extend Network { + Network(const Device &device, const unsigned int index) + { + auto network = new EthosU::Network(device, index); + return network; + } +} + +%feature("docstring", + " + InferenceStatus enumeration + ") InferenceStatus; +enum class InferenceStatus { + OK, + ERROR, + RUNNING, + REJECTED, + ABORTED, + ABORTING + }; + +%feature("docstring", + " + Represents the inference file descriptor received from the Ethos-U device. + + `Inference` is created providing `Network` object and lists of input and output feature maps buffers. + Feature map buffers are copied. + + Inference creation issues ETHOSU_IOCTL_INFERENCE_CREATE I/O request with + file descriptor ids for all input and output buffers. + + The number of input/output buffers must not exceed ETHOSU_FD_MAX value defined in the kernel module + uapi/ethosu.h. + + Destruction of the object closes inference file descriptor. + ") Inference; +%nodefaultctor Inference; +class Inference { +public: + + %feature("docstring", + " + Polls inference file descriptor for events. + + Args: + timeoutNanos (int64_t): polling timeout in nanoseconds. + + Returns: + bool: True for success, False otherwise. + ") wait; + void wait(int64_t timeoutNanos = -1) const; + + %feature("docstring", + " + Aborts the current inference job. + + Returns: + bool: True if gracefully stopped, False otherwise. + ") cancel; + bool cancel() const; + + %feature("docstring", + " + Gets the current inference job status. + + Returns: + InferenceStatus. + ") status; + EthosU::InferenceStatus status() const; + + %feature("docstring", + " + Returns inference file descriptor. + + Returns: + int: file descriptor id + ") getFd; + int getFd() const; + + %feature("docstring", + " + Returns associated `Network` object. + + Returns: + `Network`: network used during initialisation + ") getNetwork; + std::shared_ptr getNetwork() const; + + %feature("docstring", + " + Returns copied input feature maps buffers. + + Returns: + list: input feature map buffers + ") getIfmBuffers; + std::vector> &getIfmBuffers(); + + %feature("docstring", + " + Returns copied output feature maps buffers. + + Returns: + list: output feature map buffers + ") getOfmBuffers; + std::vector> &getOfmBuffers(); + + %feature("docstring", + " + Returns PMU event data. + + Returns: + list: PMU event data + ") getPmuCounters; + const std::vector getPmuCounters(); + + %feature("docstring", + " + Returns the total cycle count, including idle cycles, as reported by the PMU. + + Returns: + int: total cycle count + ") getCycleCounter; + uint64_t getCycleCounter(); + + %feature("docstring", + " + Returns maximum supported number of PMU events. + + Returns: + int: PMU event max + ") getMaxPmuEventCounters; + static uint32_t getMaxPmuEventCounters(); +}; + +%extend Inference { + Inference(const std::shared_ptr &network, + const std::vector> &ifm, + const std::vector> &ofm) + { + return new EthosU::Inference(network, ifm.begin(), ifm.end(), ofm.begin(), ofm.end()); + } + Inference(const std::shared_ptr & network, + const std::vector> &ifm, + const std::vector> &ofm, + const std::vector &enabledCounters, + bool enableCycleCounter) + { + return new EthosU::Inference(network, ifm.begin(), ifm.end(), ofm.begin(), ofm.end(), enabledCounters, enableCycleCounter); + } +} + +} +// Clear exception typemap. +%exception; diff --git a/driver_library/python/src/ethosu_driver/swig/standard_header.i b/driver_library/python/src/ethosu_driver/swig/standard_header.i new file mode 100644 index 0000000..03cf015 --- /dev/null +++ b/driver_library/python/src/ethosu_driver/swig/standard_header.i @@ -0,0 +1,50 @@ +// +// SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +// SPDX-License-Identifier: Apache-2.0 +// +%include "stl.i" +%include "cstring.i" +%include "std_string.i" +%include "std_vector.i" +%include "std_unordered_set.i" +%include "std_pair.i" +%include "stdint.i" +%include "carrays.i" +%include "exception.i" +%include "typemaps.i" +%include "std_iostream.i" +%include "std_shared_ptr.i" + +%ignore *::operator=; +%ignore *::operator[]; + + +// Define exception typemap to wrap exception into python exception. + +%exception{ + try { + $action + } catch (const EthosU::Exception& e) { + SWIG_exception(SWIG_RuntimeError, const_cast(e.what())); + } +}; + +%exception __getitem__ { + try { + $action + } catch (const std::out_of_range &e) { + SWIG_exception(SWIG_IndexError, const_cast(e.what())); + } catch (const std::exception &e) { + SWIG_exception(SWIG_RuntimeError, const_cast(e.what())); + } +}; + +%exception __setitem__ { + try { + $action + } catch (const std::out_of_range &e) { + SWIG_exception(SWIG_IndexError, const_cast(e.what())); + } catch (const std::exception &e) { + SWIG_exception(SWIG_RuntimeError, const_cast(e.what())); + } +}; diff --git a/driver_library/python/src/ethosu_driver/swig/typemaps/buffer.i b/driver_library/python/src/ethosu_driver/swig/typemaps/buffer.i new file mode 100644 index 0000000..13b7909 --- /dev/null +++ b/driver_library/python/src/ethosu_driver/swig/typemaps/buffer.i @@ -0,0 +1,42 @@ +// +// SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +// SPDX-License-Identifier: Apache-2.0 +// +%define %mutable_buffer(TYPEMAP, SIZE) + %typemap(in) (TYPEMAP, SIZE) { + int res; void *buf = 0; size_t size = 0; + Py_buffer view; + res = PyObject_GetBuffer($input, &view, PyBUF_WRITABLE); + buf = view.buf; + size = view.len; + PyBuffer_Release(&view); + if (res < 0) { + PyErr_Clear(); + %argument_fail(res, "(TYPEMAP, SIZE)", $symname, $argnum); + } + $1 = ($1_ltype) buf; + $2 = ($2_ltype) size; + } + + %typemap(typecheck) (TYPEMAP, SIZE) { + $1 = PyObject_CheckBuffer($input) || PyTuple_Check($input) ? 1 : 0; + } +%enddef + +%define %clear_mutable_buffer(TYPEMAP, SIZE) + %typemap(in) (TYPEMAP, SIZE); + %typemap(typecheck) (TYPEMAP, SIZE); +%enddef + + +%define %driver_buffer_out + %typemap(out) (char*) { + auto size = arg1->size(); + int readOnly = 0; + $result = PyMemoryView_FromMemory($1, size, readOnly); + } +%enddef + +%define %clear_driver_buffer_out + %typemap(out) (char*); +%enddef diff --git a/driver_library/python/swig_generate.py b/driver_library/python/swig_generate.py new file mode 100755 index 0000000..bdd43a3 --- /dev/null +++ b/driver_library/python/swig_generate.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +""" +This script executes SWIG commands to generate C++ library wrappers. +""" +import subprocess +from pathlib import Path + +__current_dir = Path(__file__).parent.absolute() + + +def generate_wrap(name, extr_includes): + print('Generating wrappers for {}'.format(name)) + subprocess.check_output("swig -v -c++ -python" + + " -Wall" + + " -o {}/src/ethosu_driver/_generated/{}_wrap.cpp ".format(__current_dir, name) + + "-outdir {}/src/ethosu_driver/_generated ".format(__current_dir) + + "{} ".format(extr_includes) + + "-I{}/src/ethosu_driver/swig ".format(__current_dir) + + "{}/src/ethosu_driver/swig/{}.i".format(__current_dir, name), + shell=True, + stderr=subprocess.STDOUT) + + +if __name__ == "__main__": + includes = ["{}/../../driver_library/include".format(__current_dir)] + generate_wrap('driver', "-I{} ".format(' -I'.join(includes))) diff --git a/driver_library/python/test/conftest.py b/driver_library/python/test/conftest.py new file mode 100644 index 0000000..8eef1f8 --- /dev/null +++ b/driver_library/python/test/conftest.py @@ -0,0 +1,34 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import os +import pytest + + +@pytest.fixture(scope="module") +def data_folder_per_test(request): + """ + This fixture returns path to folder with test resources (one per test module) + """ + + basedir, script = request.fspath.dirname, request.fspath.basename + return str(os.path.join(basedir, "testdata", os.path.splitext(script)[0])) + + +@pytest.fixture(scope="module") +def shared_data_folder(request): + """ + This fixture returns path to folder with shared test resources among all tests + """ + + return str(os.path.join(request.fspath.dirname, "testdata", "shared")) + + +@pytest.fixture(scope="function") +def tmpdir(tmpdir): + """ + This fixture returns path to temp folder. Fixture was added for py35 compatibility + """ + + return str(tmpdir) diff --git a/driver_library/python/test/test_capabilities.py b/driver_library/python/test/test_capabilities.py new file mode 100644 index 0000000..ffb201c --- /dev/null +++ b/driver_library/python/test/test_capabilities.py @@ -0,0 +1,73 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +from ethosu_driver._generated.driver import SemanticVersion +from ethosu_driver._generated.driver import HardwareId +from ethosu_driver._generated.driver import HardwareConfiguration +from ethosu_driver._generated.driver import Capabilities + + +def check_semantic_version(ma, mi, pa, sv): + assert ma == sv.major + assert mi == sv.minor + assert pa == sv.patch + + +def test_semantic_version(): + sv = SemanticVersion(1, 2, 3) + assert '{ major=1, minor=2, patch=3 }' == sv.__str__() + check_semantic_version(1, 2, 3, sv) + + +def test_hardware_id(): + version = SemanticVersion(1, 2, 3) + product = SemanticVersion(4, 5, 6) + architecture = SemanticVersion(7, 8, 9) + hw_id = HardwareId(1, version, product, architecture) + + assert 1 == hw_id.versionStatus + + check_semantic_version(1, 2, 3, hw_id.version) + check_semantic_version(4, 5, 6, hw_id.product) + check_semantic_version(7, 8, 9, hw_id.architecture) + + assert '{versionStatus=1, version={ major=1, minor=2, patch=3 }, product={ major=4, minor=5, patch=6 }, ' \ + 'architecture={ major=7, minor=8, patch=9 }}' == hw_id.__str__() + + +def test_hw_configuration(): + hw_cfg = HardwareConfiguration(128, 1, True) + + assert 1 == hw_cfg.cmdStreamVersion + assert 128 == hw_cfg.macsPerClockCycle + assert hw_cfg.customDma + + assert "{macsPerClockCycle=128, cmdStreamVersion=1, customDma=True}" == hw_cfg.__str__() + + +def test_capabilities(): + version = SemanticVersion(100, 200, 300) + product = SemanticVersion(400, 500, 600) + architecture = SemanticVersion(700, 800, 900) + hw_id = HardwareId(1, version, product, architecture) + hw_cfg = HardwareConfiguration(256, 1000, False) + driver_v = SemanticVersion(10, 20, 30) + + cap = Capabilities(hw_id, hw_cfg, driver_v) + + check_semantic_version(10, 20, 30, cap.driver) + + check_semantic_version(100, 200, 300, cap.hwId.version) + check_semantic_version(400, 500, 600, cap.hwId.product) + check_semantic_version(700, 800, 900, cap.hwId.architecture) + + assert 1000 == cap.hwCfg.cmdStreamVersion + assert 256 == cap.hwCfg.macsPerClockCycle + assert not cap.hwCfg.customDma + + assert '{hwId={versionStatus=1, version={ major=100, minor=200, patch=300 }, ' \ + 'product={ major=400, minor=500, patch=600 }, ' \ + 'architecture={ major=700, minor=800, patch=900 }}, ' \ + 'hwCfg={macsPerClockCycle=256, cmdStreamVersion=1000, customDma=False}, ' \ + 'driver={ major=10, minor=20, patch=30 }}' == cap.__str__() diff --git a/driver_library/python/test/test_driver.py b/driver_library/python/test/test_driver.py new file mode 100644 index 0000000..5496aed --- /dev/null +++ b/driver_library/python/test/test_driver.py @@ -0,0 +1,179 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +import os +import ethosu_driver as driver +from ethosu_driver.inference_runner import read_npy_file_to_buf + + +@pytest.fixture() +def device(device_name): + device = driver.Device("/dev/{}".format(device_name)) + yield device + + +@pytest.fixture() +def network_buffer(device, model_name, shared_data_folder): + network_file = os.path.join(shared_data_folder, model_name) + network_buffer = driver.Buffer(device, network_file) + yield network_buffer + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +def test_check_device_swig_ownership(device): + # Check to see that SWIG has ownership for parser. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + assert device.thisown + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +def test_device_ping(device): + device.ping() + + +@pytest.mark.parametrize('device_name', ['blabla']) +def test_device_wrong_name(device_name): + with pytest.raises(RuntimeError) as err: + driver.Device("/dev/{}".format(device_name)) + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Failed to open device' in str(err.value) + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +def test_driver_network_filenotfound_exception(device, shared_data_folder): + + network_file = os.path.join(shared_data_folder, "some_unknown_model.tflite") + + with pytest.raises(RuntimeError) as err: + network_buffer = driver.Buffer(device, network_file) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Failed to open file:' in str(err.value) + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_buffer_swig_ownership(network_buffer): + # Check to see that SWIG has ownership for parser. This instructs SWIG to take + # ownership of the return value. This allows the value to be automatically + # garbage-collected when it is no longer in use + assert network_buffer.thisown + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_buffer_capacity(network_buffer): + assert network_buffer.capacity() > 0 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_buffer_size(network_buffer): + assert network_buffer.size() > 0 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_buffer_clear(network_buffer): + network_buffer.clear() + assert network_buffer.size() == 0 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_buffer_resize(network_buffer): + offset = 1 + new_size = network_buffer.capacity() - offset + network_buffer.resize(new_size, offset) + assert network_buffer.size() == new_size + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_buffer_getFd(network_buffer): + assert network_buffer.getFd() >= 0 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_network_ifm_size(device, network_buffer): + network = driver.Network(device, network_buffer) + assert network.getIfmSize() > 0 + assert network_buffer.thisown + + +@pytest.mark.parametrize('device_name', [('ethosu0')]) +def test_check_network_buffer_none(device): + + with pytest.raises(RuntimeError) as err: + driver.Network(device, None) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Failed to create the network' in str(err.value) + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_network_ofm_size(device, network_buffer): + network = driver.Network(device, network_buffer) + assert network.getOfmSize() > 0 + + +def test_getMaxPmuEventCounters(): + assert driver.Inference.getMaxPmuEventCounters() > 0 + + +@pytest.fixture() +def inf(device_name, model_name, input_files, timeout, shared_data_folder): + # Prepate full path of model and inputs + full_path_model_file = os.path.join(shared_data_folder, model_name) + full_path_input_files = [] + for input_file in input_files: + full_path_input_files.append(os.path.join(shared_data_folder, input_file)) + + ifms_data = [] + for ifm_file in full_path_input_files: + ifms_data.append(read_npy_file_to_buf(ifm_file)) + + device = driver.open_device(device_name) + device.ping() + network = driver.load_model(device, full_path_model_file) + ofms = driver.allocate_buffers(device, network.getOfmDims()) + ifms = driver.allocate_buffers(device, network.getIfmDims()) + + # ofm_buffers = runner.run(ifms_data,timeout, ethos_pmu_counters) + driver.populate_buffers(ifms_data, ifms) + ethos_pmu_counters = [1] + enable_cycle_counter = True + inf_inst = driver.Inference(network, ifms, ofms, ethos_pmu_counters, enable_cycle_counter) + inf_inst.wait(int(timeout)) + + yield inf_inst + + +@pytest.mark.parametrize('device_name, model_name, timeout, input_files', + [('ethosu0', 'model.tflite', 5000000000, ['model_ifm.npy'])]) +def test_inf_get_cycle_counter(inf): + total_cycles = inf.getCycleCounter() + assert total_cycles >= 0 + + +@pytest.mark.parametrize('device_name, model_name, timeout, input_files', + [('ethosu0', 'model.tflite', 5000000000, ['model_ifm.npy'])]) +def test_inf_get_pmu_counters(inf): + inf_pmu_counter = inf.getPmuCounters() + assert len(inf_pmu_counter) > 0 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +def test_capabilities(device): + cap = device.capabilities() + assert cap.hwId + assert cap.hwCfg + assert cap.driver diff --git a/driver_library/python/test/test_driver_utilities.py b/driver_library/python/test/test_driver_utilities.py new file mode 100644 index 0000000..fc8e921 --- /dev/null +++ b/driver_library/python/test/test_driver_utilities.py @@ -0,0 +1,77 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +import os +import ethosu_driver as driver +from ethosu_driver.inference_runner import read_npy_file_to_buf + + +@pytest.fixture() +def device(device_name): + device = driver.open_device(device_name) + yield device + + +@pytest.fixture() +def network(device, model_name, shared_data_folder): + network_file = os.path.join(shared_data_folder, model_name) + network = driver.load_model(device, network_file) + yield network + + +@pytest.mark.parametrize('device_name', ['blabla']) +def test_open_device_wrong_name(device_name): + with pytest.raises(RuntimeError) as err: + device = driver.open_device(device_name) + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Failed to open device' in str(err.value) + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +def test_network_filenotfound_exception(device, shared_data_folder): + + network_file = os.path.join(shared_data_folder, "some_unknown_model.tflite") + + with pytest.raises(RuntimeError) as err: + driver.load_model(device, network_file) + + # Only check for part of the exception since the exception returns + # absolute path which will change on different machines. + assert 'Failed to open file:' in str(err.value) + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +def test_check_network_ifm_size(network): + assert network.getIfmSize() > 0 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +def test_allocate_buffers(device): + buffers = driver.allocate_buffers(device, [128, 256]) + assert len(buffers) == 2 + assert buffers[0].size() == 0 + assert buffers[0].capacity() == 128 + assert buffers[1].size() == 0 + assert buffers[1].capacity() == 256 + + +@pytest.mark.parametrize('device_name', ['ethosu0']) +@pytest.mark.parametrize('model_name', ['model.tflite']) +@pytest.mark.parametrize('ifms_file_list', [['model_ifm.npy']]) +def test_set_ifm_buffers(device, network, ifms_file_list, shared_data_folder): + full_path_input_files = [] + for input_file in ifms_file_list: + full_path_input_files.append(os.path.join(shared_data_folder, input_file)) + + ifms_data = [] + for ifm_file in full_path_input_files: + ifms_data.append(read_npy_file_to_buf(ifm_file)) + + ifms = driver.allocate_buffers(device, network.getIfmDims()) + driver.populate_buffers(ifms_data, ifms) + assert len(ifms) > 0 + diff --git a/driver_library/python/test/test_inference.py b/driver_library/python/test/test_inference.py new file mode 100644 index 0000000..bfb4068 --- /dev/null +++ b/driver_library/python/test/test_inference.py @@ -0,0 +1,50 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import pytest +import os +import ethosu_driver as driver +from ethosu_driver.inference_runner import read_npy_file_to_buf + + +def run_inference_test(runner, timeout, input_files, golden_outputs, shared_data_folder): + + full_path_input_files = [] + for input_file in input_files: + full_path_input_files.append(os.path.join(shared_data_folder, input_file)) + + ifms_data = [] + for ifm_file in full_path_input_files: + ifms_data.append(read_npy_file_to_buf(ifm_file)) + + ofm_buffers = runner.run(ifms_data, timeout) + + for index, buffer_out in enumerate(ofm_buffers): + golden_output = read_npy_file_to_buf(os.path.join(shared_data_folder, golden_outputs[index])) + assert buffer_out.data().nbytes == golden_output.nbytes + for index, golden_value in enumerate(golden_output): + assert golden_value == buffer_out.data()[index] + + +@pytest.mark.parametrize('device_name, model_name, timeout, input_files, golden_outputs', + [('ethosu0', 'model.tflite', 5000000000, ['model_ifm.npy'], ['model_ofm.npy'])]) +def test_inference(device_name, model_name, input_files, timeout, golden_outputs, shared_data_folder): + # Prepate full path of model and inputs + full_path_model_file = os.path.join(shared_data_folder, model_name) + + runner = driver.InferenceRunner(device_name, full_path_model_file) + run_inference_test(runner, timeout, input_files, golden_outputs, shared_data_folder) + + +@pytest.mark.parametrize('device_name, model_name, timeout, input_files, golden_outputs', + [('ethosu0', 'model.tflite', 5000000000, + [['model_ifm.npy'], ['model_ifm.npy']], + [['model_ofm.npy'], ['model_ofm.npy']])]) +def test_inference_loop(device_name, model_name, input_files, timeout, golden_outputs, shared_data_folder): + # Prepare full path of model and inputs + full_path_model_file = os.path.join(shared_data_folder, model_name) + + runner = driver.InferenceRunner(device_name, full_path_model_file) + for input_file, golden_output in zip(input_files, golden_outputs): + run_inference_test(runner, timeout, input_file, golden_output, shared_data_folder) diff --git a/driver_library/python/test/test_shadow_classes.py b/driver_library/python/test/test_shadow_classes.py new file mode 100644 index 0000000..055c10b --- /dev/null +++ b/driver_library/python/test/test_shadow_classes.py @@ -0,0 +1,20 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import inspect +import pytest +import ethosu_driver._generated.driver as driver_shadow + + +def get_classes(): + ignored_class_names = ('_SwigNonDynamicMeta', '_object', '_swig_property') + return list(filter(lambda x: x[0] not in ignored_class_names, + inspect.getmembers(driver_shadow, inspect.isclass))) + + +@pytest.mark.parametrize("class_instance", get_classes(), ids=lambda x: 'class={}'.format(x[0])) +class TestOwnership: + + def test_destructors_exist_per_class(self, class_instance): + assert getattr(class_instance[1], '__swig_destroy__', None) diff --git a/driver_library/python/test/testdata/download.py b/driver_library/python/test/testdata/download.py new file mode 100644 index 0000000..18aa9af --- /dev/null +++ b/driver_library/python/test/testdata/download.py @@ -0,0 +1,46 @@ +# +# SPDX-FileCopyrightText: Copyright 2021-2022 Arm Limited and/or its affiliates +# SPDX-License-Identifier: Apache-2.0 +# +import os +from pathlib import Path +from typing import List +from urllib.request import urlopen +""" +Downloads resources for tests from Arm public model zoo. +Run this script before executing tests. +""" + + +PMZ_URL = 'https://github.com/ARM-software/ML-zoo/raw/9f506fe52b39df545f0e6c5ff9223f671bc5ae00/models' +test_resources = [ + {'model': '{}/visual_wake_words/micronet_vww2/tflite_int8/vww2_50_50_INT8.tflite'.format(PMZ_URL), + 'ifm': '{}/visual_wake_words/micronet_vww2/tflite_int8/testing_input/input/0.npy'.format(PMZ_URL), + 'ofm': '{}/visual_wake_words/micronet_vww2/tflite_int8/testing_output/Identity/0.npy'.format(PMZ_URL)} +] + + +def download(path: str, url: str): + with urlopen(url) as response, open(path, 'wb') as file: + print("Downloading {} ...".format(url)) + file.write(response.read()) + file.seek(0) + print("Finished downloading {}.".format(url)) + + +def download_test_resources(test_res_entries: List[dict], where_to: str): + os.makedirs(where_to, exist_ok=True) + + for resources in test_res_entries: + download(os.path.join(where_to, 'model.tflite'), resources['model']) + download(os.path.join(where_to, 'model_ifm.npy'), resources['ifm']) + download(os.path.join(where_to, 'model_ofm.npy'), resources['ofm']) + + +def main(): + current_dir = str(Path(__file__).parent.absolute()) + download_test_resources(test_resources, os.path.join(current_dir, 'shared')) + + +if __name__ == '__main__': + main() diff --git a/driver_library/src/ethosu.cpp b/driver_library/src/ethosu.cpp index 16b9654..e425e52 100644 --- a/driver_library/src/ethosu.cpp +++ b/driver_library/src/ethosu.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 Arm Limited. + * SPDX-FileCopyrightText: Copyright 2020-2022 Arm Limited and/or its affiliates * * SPDX-License-Identifier: Apache-2.0 * diff --git a/driver_library/src/ethosu_stub.cpp b/driver_library/src/ethosu_stub.cpp index d85f653..e223be3 100644 --- a/driver_library/src/ethosu_stub.cpp +++ b/driver_library/src/ethosu_stub.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 Arm Limited. + * SPDX-FileCopyrightText: Copyright 2020-2022 Arm Limited and/or its affiliates * * SPDX-License-Identifier: Apache-2.0 * -- cgit v1.2.1