This is an automated email from the ASF dual-hosted git repository. mboehm7 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/systemml.git
The following commit(s) were added to refs/heads/master by this push: new 0ac0c25 [SYSTEMDS-263] ONNX graph importer (Python API, docs, tests) 0ac0c25 is described below commit 0ac0c2571b39e96f7a117fd317d73443632f6f26 Author: Lukas Timpl <lukas.ti...@student.tugraz.at> AuthorDate: Thu May 14 23:39:04 2020 +0200 [SYSTEMDS-263] ONNX graph importer (Python API, docs, tests) This PR implements a first poc-implementation for an ONNX importer. It adds support for the following operators: Add, Sub, MatMul, Neg, Xor, Or, And, Relu, Tanh, Sigmoid, Softmax, Dropout, MaxPool, Conv, If; as well as the logic for nested sub-graphs. AMLS project SS 2020 Closes #904. --- .github/workflows/python.yml | 17 +- .gitignore | 3 + docs/Tasks.txt | 3 +- docs/onnx-systemds-design.md | 46 -- pom.xml | 1 + .../python/docs/source/assets/sample_graph.png | Bin 0 -> 35508 bytes src/main/python/docs/source/index.rst | 8 + src/main/python/docs/source/onnx_systemds.rst | 59 +++ .../python/docs/source/onnx_systemds_design.rst | 217 ++++++++++ src/main/python/systemds/__init__.py | 2 +- src/main/python/systemds/onnx_systemds/README.md | 22 + src/main/python/systemds/onnx_systemds/__init__.py | 14 + src/main/python/systemds/onnx_systemds/convert.py | 53 +++ .../python/systemds/onnx_systemds/onnx_helper.py | 218 ++++++++++ .../python/systemds/onnx_systemds/operator_gen.py | 465 +++++++++++++++++++++ src/main/python/systemds/onnx_systemds/render.py | 215 ++++++++++ .../templates/graph_function.dml.jinja | 54 +++ .../onnx_systemds/templates/graph_header.dml.jinja | 22 + .../onnx_systemds/templates/main.dml.jinja | 26 ++ .../templates/matrix_initialize.dml.jinja | 24 ++ .../onnx_systemds/templates/model_header.dml.jinja | 36 ++ .../templates/module_import.dml.jinja | 17 + .../operators/2input_1output_operator.dml.jinja | 18 + .../templates/operators/function_call.dml.jinja | 31 ++ .../templates/operators/if_operator.dml.jinja | 19 + .../templates/operators/neg.dml.jinja | 18 + .../onnx_systemds/templates/util.dml.jinja | 42 ++ src/main/python/systemds/onnx_systemds/util.py | 40 ++ src/main/python/{systemds => tests}/__init__.py | 5 - .../python/{systemds => tests/onnx}/__init__.py | 4 - .../dml_wrapper/simple_conv_layer_2_wrapper.dml | 27 ++ .../onnx/dml_wrapper/simple_conv_layer_wrapper.dml | 25 ++ .../dml_wrapper/simple_dropout_layer_wrapper.dml | 22 + .../onnx/dml_wrapper/simple_if_graph_wrapper.dml | 27 ++ .../dml_wrapper/simple_mat_add_mul_sub_wrapper.dml | 24 ++ .../onnx/dml_wrapper/simple_mat_add_wrapper.dml | 24 ++ .../dml_wrapper/simple_mat_initialized_wrapper.dml | 21 + .../dml_wrapper/simple_maxpool_layer_wrapper.dml | 22 + .../simple_relu_tanh_sigmoid_softmax_wrapper.dml | 27 ++ .../simple_conv_layer_2_reference.out | 5 + .../simple_conv_layer_reference.out | 25 ++ .../output_reference/simple_if_graph_reference.out | 5 + .../simple_mat_add_mul_sub_reference.out | 4 + .../output_reference/simple_mat_add_reference.out | 4 + .../simple_mat_initialized_reference.out | 9 + .../simple_maxpool_layer_reference.out | 25 ++ .../simple_relu_tanh_sigmoid_softmax_reference.out | 11 + .../tests/onnx/test_models/model_generate.py | 388 +++++++++++++++++ src/main/python/tests/onnx/test_simple.py | 65 +++ src/main/python/tests/onnx/util.py | 84 ++++ 50 files changed, 2485 insertions(+), 58 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 27c12ec..156d843 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -72,9 +72,12 @@ jobs: key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('src/main/python/setup.py') }} restore-keys: | ${{ runner.os }}-pip-${{ matrix.python-version }}- + + - name: Install protobuf + run: sudo apt-get install protobuf-compiler libprotoc-dev - name: Install pip Dependencies - run: pip install numpy py4j wheel scipy sklearn + run: pip install numpy py4j wheel scipy sklearn jinja2 onnx - name: Build Python Package run: | @@ -97,3 +100,15 @@ jobs: cd src/main/python python -m unittest tests/lineage/*.py echo "Exit Status: " $? + + - name: Run onnx-systemds python tests + run: | + export SYSTEMDS_ROOT=$(pwd) + export PATH=$SYSTEMDS_ROOT/bin:$PATH + cd src/main/python + echo "Creating models" + python tests/onnx/test_models/model_generate.py + ls tests/onnx/test_models/*.onnx + echo "Beginning tests" + python -m unittest tests/onnx/*.py + echo "Exit Status: " $? diff --git a/.gitignore b/.gitignore index 87a0fcd..e986cea 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,9 @@ src/main/python/NOTICE src/main/python/dist src/main/python/docs/build src/main/python/docs/source/_build +src/main/python/tests/onnx/output_test +src/main/python/tests/onnx/dml_output +src/main/python/tests/onnx/test_models/*.onnx # User configuration files conf/SystemDS-config.xml diff --git a/docs/Tasks.txt b/docs/Tasks.txt index e83d0ee..8163e9f 100644 --- a/docs/Tasks.txt +++ b/docs/Tasks.txt @@ -213,7 +213,8 @@ SYSTEMDS-250 Extended Slice Finding SYSTEMDS-260 Misc Tools * 261 Stable marriage algorithm OK * 262 Data augmentation tool for data cleaning OK - * 263 ONNX graph importer/exporter + * 263 ONNX graph importer (Python API, docs, tests) OK + * 264 ONNX graph exporter SYSTEMDS-270 Compressed Matrix Blocks * 271 Reintroduce compressed matrix blocks from SystemML OK diff --git a/docs/onnx-systemds-design.md b/docs/onnx-systemds-design.md deleted file mode 100644 index 9650f9c..0000000 --- a/docs/onnx-systemds-design.md +++ /dev/null @@ -1,46 +0,0 @@ -# onnx-systemds - -A tool for importing/exporting [ONNX](https://github.com/onnx/onnx/blob/master/docs/IR.md) graphs into/from SystemDS DML scripts. - - -## Goals - -* Support for importing [operators of the ONNX base definition](https://github.com/onnx/onnx/blob/master/docs/Operators.md) - -* Support for importing [operators defined by ONNX-ML](https://github.com/onnx/onnx/blob/master/docs/Operators-ml.md) - -* Support for exporting DML script to ONNX graphs - -## Limitations - -* Not able to support all data types / operators as they are not currently supported by SystemDS - - - -## Suggested Implementation - -Since the ONNX specification includes the conditional operators [loop](https://github.com/onnx/onnx/blob/master/docs/Operators.md#Loop) and [if](https://github.com/onnx/onnx/blob/master/docs/Operators.md#If), a direct conversion from ONNX to the internal HOP might not be ideal. - -Hence my suggested implementation is a dedicated tool invoked from command line which generates DML scripts. This also enables optimizations performed by the compiler at both graph and program level. - -### Example Call - -```bash -onnx-systemds model.onx --out model_script.dml -``` - - -### Tooling - -* Due to the availability of a [Python API](https://github.com/onnx/onnx/blob/master/docs/PythonAPIOverview.md) for ONNX, I would suggest implementing the tool in Python -* Another advantage of Python is good support for template engines e.g. [Jinja](https://jinja.palletsprojects.com/en/2.11.x/) -* An implementation could use templates for various operators which are then combined into a script - -### Implementation Details - -ONNX is a [serialized graph](https://github.com/onnx/onnx/blob/master/docs/IR.md#graphs) structured as a sorted list of nodes that form a DAG (directed acyclic graph). - -1. Loading in the serialized structure -2. [Checking](https://github.com/onnx/onnx/blob/master/docs/PythonAPIOverview.md#checking-an-onnx-model) model and [converting](https://github.com/onnx/onnx/blob/master/docs/PythonAPIOverview.md#converting-version-of-an-onnx-model-within-default-domain-aionnx) models to a common version -3. Building a simple internal graph structure (for arbitrary operators) -4. Generating the DML script while traversing this graph (provided information in doc_strings and other description variables are added as comments to improve human-readability of the generated script) diff --git a/pom.xml b/pom.xml index 4c25f07..6d6ab8d 100644 --- a/pom.xml +++ b/pom.xml @@ -508,6 +508,7 @@ <exclude>**/*.libsvm</exclude> <exclude>**/*.mtx</exclude> <exclude>**/*.mtd</exclude> + <exclude>**/*.out</exclude> <exclude>**/part-*</exclude> <exclude>**/*.keep</exclude> <exclude>**/target/**</exclude> diff --git a/src/main/python/docs/source/assets/sample_graph.png b/src/main/python/docs/source/assets/sample_graph.png new file mode 100644 index 0000000..630a98d Binary files /dev/null and b/src/main/python/docs/source/assets/sample_graph.png differ diff --git a/src/main/python/docs/source/index.rst b/src/main/python/docs/source/index.rst index cdcb0a2..5b6cf53 100644 --- a/src/main/python/docs/source/index.rst +++ b/src/main/python/docs/source/index.rst @@ -59,3 +59,11 @@ tensors (multi-dimensional arrays) whose first dimension may have a heterogeneou :caption: Central Classes matrix.rst + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: onnx-systemds + + onnx_systemds.rst + onnx_systemds_design.rst diff --git a/src/main/python/docs/source/onnx_systemds.rst b/src/main/python/docs/source/onnx_systemds.rst new file mode 100644 index 0000000..4d9a4f4 --- /dev/null +++ b/src/main/python/docs/source/onnx_systemds.rst @@ -0,0 +1,59 @@ +.. ------------------------------------------------------------- +.. +.. 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. +.. +.. ------------------------------------------------------------- + +QuickStart +============= +onnx-systemds is a tool for importing/exporting onnx graphs into/from SystemDS DML scripts. + +Prerequisites +--------------- +to run onnx-systemds you need to: + +- install `onnx <https://github.com/onnx/onnx>`_: `Installation instructions <https://github.com/onnx/onnx#installation>`_ +- `set up the environment <https://github.com/apache/systemml/blob/master/bin/README.md>`_ + +Usage +------ +An example call from the ``src/main/python`` directory of systemds:: + + python -m systemds.onnx_systemds.convert tests/onnx/test_models/simple_mat_add.onnx + + +This will generate the dml script ``simple_mat_add.dml`` in the current directory. + +Run Tests +--------- +Form the ``src/main/python`` directory of systemds: + +At first generate the test models:: + + python tests/onnx/test_models/model_generate.py + +Then you can run the tests:: + + python -m unittest tests/onnx/test_simple.py + + +Converter +--------- +It is also possible to invoke the converter from within python. + +.. autofunction:: systemds.onnx_systemds.convert.onnx2systemds \ No newline at end of file diff --git a/src/main/python/docs/source/onnx_systemds_design.rst b/src/main/python/docs/source/onnx_systemds_design.rst new file mode 100644 index 0000000..5ff05a0 --- /dev/null +++ b/src/main/python/docs/source/onnx_systemds_design.rst @@ -0,0 +1,217 @@ +.. ------------------------------------------------------------- +.. +.. 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. +.. +.. ------------------------------------------------------------- + +Design +====== + +This document describes the initial design of `onnx-systemds` + +For dealing with different operator-set versions of onnx the current strategy is to use the +`converter provided by onnx <https://github.com/onnx/onnx/blob/master/docs/PythonAPIOverview.md#converting-version-of-an-onnx-model-within-default-domain-aionnx>`_ to convert to a common version. + +However, the converter does not support adapters for all op-sets/operators so this conversion will fail for many models. +On the onnx repository you can find a list of +`currently supported adapters <https://github.com/onnx/onnx/blob/master/onnx/version_converter.py#L21>`_ + + +Goals +----- + + - Support for importing `operators of the ONNX base definition <https://github.com/onnx/onnx/blob/master/docs/Operators.md>`_ + - Support for importing `operators defined by ONNX-ML <https://github.com/onnx/onnx/blob/master/docs/Operators-ml.md>`_ + - Support for exporting DML script to ONNX graphs + + +Limitations +------------ + + - Not able to support all data types / operators as they are not currently supported by SystemDS + +Onnx - Operators +----------------- + +Onnx includes several very simple and also more complex operators. +When implementing an operator it's best to have a look at the +`operator schemas <https://github.com/onnx/onnx/blob/master/docs/Operators.md>`_, +which precisely define the inputs, outputs and attributes of the operation. + +Besides the standard onnx definition, there also exists onnx-ML the operator schemas for which are defined in a +`separate document <https://github.com/onnx/onnx/blob/master/docs/Operators-ml.md>`_. +It is an extension of the standard onnx format, however currently only onnx standard operators are supported. + +Onnx - Files +------------- + +Onnx uses the `ProtoBuf format <https://developers.google.com/protocol-buffers/>`_. +It specifies this representation in several ``.proto``/``.proto3`` +`files <https://github.com/onnx/onnx/tree/master/onnx>`_ again with dedicated files for onnx-ML. +These files are helpful to understand the underlying structure and values that are possible. + +Protobuf creates the underlying structure such that you can access elements of the onnx graph as if they were +class members. For more information take a look at +`Google's protocol-buffer documentation +<https://developers.google.com/protocol-buffers/docs/pythontutorial#the-protocol-buffer-api>`_. + +This is also why in its current form, this converter does not convert the protobuf-structure into an internal format, +as the provided protobuf structure can already be conveniently used. Instead, +there exist a number of onnx-helper functions/classes (see ``onnx_helper.py``). + +Traversing the Graph +--------------------- + +For creating the script, it is essential to insert computations in the right order into the dml-script. +To do this, the converter builds a tree-structure (DAG) from the protobuf-nodes +(see `render.gen_graph_functions`). + + - For traversing the graph, we start from the bottom. + - The converter starts with the graph-outputs as available outputs. + - It generates the dml snippets in reverse-order + +Graph traversal +^^^^^^^^^^^^^^^^ + +1. Find a node for which all outputs are available. + +2. Process the node: + + - Generate the dml parts for this node + - add its inputs to the list of available outputs + - remove the node from the graph + +3. if there are nodes left restart at 1. + +Example +^^^^^^^ + +In the example below with the nodes ``Add``, ``MatMul`` and ``Sub``, we would start with ``F`` as available output. +Therefore the first node to insert would be ``Sub``. After inserting ``Sub`` its inputs become available outputs, +therefore all outputs of ``MatMul`` become available. Finally, after removing ``MatMul`` from the graph all outputs +to ``Add`` are available, and it can be removed from the graph as well. + +.. image:: assets/sample_graph.png + :width: 200px + :align: center + :alt: sample graph + + +Rendering DML scripts +--------------------- + +The main idea of this converter is, that the logic for generating the actual dml-syntax is handled by +`Jinja templates <https://jinja.palletsprojects.com/en/2.11.x/>`_ (located in ``/templates``). +Therefore the python code stays uncluttered, because it does not have to merge strings together to produce valid +dml-syntax and instead simply provides the elements that are needed to render the script. + +The template-engine then takes these inputs and renders a human readable script with valid dml syntax. +To improve readability the generator also automatically ads the doc-strings which are part of the onnx-definitions as +comments to the script. + +When traversing the graph, a script part is generated for each node consisting of three elements: + + - `dml_script` The actual script snipped for the node + - `imports` Imports required for the node + - `sub_graphs` Any sub_graphs of the node that need to be handled + +The function that is called for rendering a specific operator is defined in the dictionary +``operator_generators`` in ``render.py`` + +1. `dml_script` +^^^^^^^^^^^^^^^^^^ + +Depending on the operator this can be a function call or a more complex dml-snippet. +This part is generated by the template-engine when the corresponding template is rendered. + +Many onnx-operators can be handled by a single template file. There exists a ``function_call.dml.jinja`` +template which should be able to handle a large number of operators. + +2. `imports` +^^^^^^^^^^^^^ + +Some operators are handled by calling scripts provided by systemds located in ``$SYSTEMDS_ROOT/scripts``. +To enable these imports, the converter automatically resolves the ``$SYSTEMDS_ROOT`` +environment variable and adds a ``setw($SYSTEMDS_ROOT/scripts)`` to the script. + +3. `sub_graphs` +^^^^^^^^^^^^^^^^^ + +Since sub-graphs have their own variable scope and are independent, they are handled as separate functions. +The converter generates a function for each graph in the model. +In the main-graph, the sub-graph is replaced by a function call to the sub-graph function. +To handle this the function ``render.gen_graph_functions`` recursively calls itself to render sub-graph functions +(and also the sub-graph functions of sub-graphs and so on...). + +Final Script +------------ + +In the final render all required imports, the sub-functions and the main-function are combined in a single dml-file. + +Implementing new operators +---------------------------- + +When implementing an operator it's best to have a look at the +`operator schemas <https://github.com/onnx/onnx/blob/master/docs/Operators.md>`_ +which exactly define the inputs, outputs and attributes of the operation + +It is also nice to have a test-model to work with, to generate one refer to +``tests/onnx/test_models/model_generate.py``. + +To implement a new operator, the function that handles the operator needs to be defined in the ``operator_generators`` +located in ``render.py``. +All functions listed in this dictionary need to have the same call structure. + +If there exists a dml-script (in ``$SYSTEMDS_ROOT/scripts``) that provides the functionality the operator +can be implemented by translating the arguments/inputs, adding the import-render and function-call-render to this script. + +Testing models +--------------- + +onnx provides a convenient way for +`creating models <https://github.com/onnx/onnx/blob/master/docs/PythonAPIOverview.md#checking-an-onnx-model>`_ +using helper functions in python. All current test-models are produced like this (see ``tests/onnx/test_models``). + +Creating a Testcase +^^^^^^^^^^^^^^^^^^^^^ + +The current test-system takes a model, converts it to dml using the converter and then runs a +``dml_wrapper`` which calls the model-function using the script ``$SYSTEMDS_ROOT/bin/systemds``. +Finally, the output (stored by the dml-wrapper) is compared to a reference output. + +When creating files stick to the naming conventions of other files in the same folder. + +Steps: +"""""""" + +1. Create a model in ``tests/onnx/test_models``, e.g. ``sample_model.onnx`` + +2. Create a dml wrapper that calls the model-function in ``tests/onnx/dml_wrapper/sample_model_wrapper.dml`` + + - The wrapper needs to call the model-function and store the output to ``output_test/sample_model.out`` + - The name of the model-function is generated from the model-name (see ``util.generate_function_name`` ) + +3. Provide a reference output in ``tests/onnx/output_reference/sample_model_reference.out`` + +4. Create the unit test function. + +Tools +------ + + - `Pycharm <https://www.jetbrains.com/pycharm/>`_ in the professional version allows you to `debug template files <https://www.jetbrains.com/help/pycharm/templates.html#debug>`_ which can be handy + - `Neutron <https://github.com/lutzroeder/netron>`_ is a nice free tool for viewing onnx-graphs \ No newline at end of file diff --git a/src/main/python/systemds/__init__.py b/src/main/python/systemds/__init__.py index e51fbf8..f4e5a1d 100644 --- a/src/main/python/systemds/__init__.py +++ b/src/main/python/systemds/__init__.py @@ -19,4 +19,4 @@ # #------------------------------------------------------------- -__all__ = ['context', 'matrix'] +__all__ = ['context', 'matrix', 'onnx_systemds'] diff --git a/src/main/python/systemds/onnx_systemds/README.md b/src/main/python/systemds/onnx_systemds/README.md new file mode 100644 index 0000000..5b855de --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/README.md @@ -0,0 +1,22 @@ +<!-- +{% comment %} +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. +{% end comment %} +--> + +# onnx-systemds + +A tool for importing/exporting [onnx](https://github.com/onnx/onnx/blob/master/docs/IR.md) graphs into/from SystemDS DML scripts. diff --git a/src/main/python/systemds/onnx_systemds/__init__.py b/src/main/python/systemds/onnx_systemds/__init__.py new file mode 100644 index 0000000..27bcb2e --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/__init__.py @@ -0,0 +1,14 @@ +# 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. diff --git a/src/main/python/systemds/onnx_systemds/convert.py b/src/main/python/systemds/onnx_systemds/convert.py new file mode 100644 index 0000000..5ee062b --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/convert.py @@ -0,0 +1,53 @@ +# 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 argparse +import os.path +import systemds.onnx_systemds.onnx_helper as onnx_helper +from systemds.onnx_systemds import render + + +def init_argparse() -> argparse.ArgumentParser: + arg_parser = argparse.ArgumentParser(description="Convert onnx models into dml scripts") + arg_parser.add_argument("input", type=str) + arg_parser.add_argument("-o", "--output", type=str, + help="output file", required=False) + return arg_parser + + +def onnx2systemds(input_onnx_file: str, output_dml_file: str = None) -> None: + """ + Loads the model from the input file and generates a dml file. + + :param input_onnx_file: the onnx input file + :param output_dml_file: (optional) the dml output file, + if this parameter is not given the output file will have the same name as the input file + """ + if not os.path.isfile(input_onnx_file): + raise Exception("Invalid input-file: " + str(input_onnx_file)) + + if not output_dml_file: + output_dml_file = os.path.splitext(os.path.basename(input_onnx_file))[0] + ".dml" + + model = onnx_helper.load_model(input_onnx_file) + render.gen_script(model, output_dml_file) + + +if __name__ == '__main__': + parser = init_argparse() + args = parser.parse_args() + input_file = args.input + output_file = args.output + onnx2systemds(input_file, output_file) diff --git a/src/main/python/systemds/onnx_systemds/onnx_helper.py b/src/main/python/systemds/onnx_systemds/onnx_helper.py new file mode 100644 index 0000000..0425ea4 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/onnx_helper.py @@ -0,0 +1,218 @@ +# 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 functools + +import onnx +import onnx.version_converter + + +class TreeNode: + def __init__(self, node: onnx.NodeProto): + self.node = node + self.parent_nodes = list() + self.child_nodes = list() + + +class NodeTree: + """ A simple class for representing a tree structure of nodes """ + + def __init__(self, nodes: [onnx.NodeProto]): + self.nodes = [TreeNode(node) for node in nodes] + self.root_nodes = [] # nodes that have no parents + self.end_nodes = [] # nodes that have no children + + # find parents and children for each node + for tree_node in self.nodes: + for compare_tree_node in self.nodes: + if tree_node != compare_tree_node: + for node_output in tree_node.node.output: + if node_output in compare_tree_node.node.input: + tree_node.child_nodes.append(compare_tree_node) + compare_tree_node.parent_nodes.append(tree_node) + + for node in self.nodes: + if len(node.child_nodes) == 0: + self.end_nodes.append(node) + if len(node.parent_nodes) == 0: + self.root_nodes.append(node) + + def remove_end_node(self, node: TreeNode): + """ + Removes the given end-node from the tree. + Removing a non-existing or non end-node raises an exception. + :param node: The node that shall be removed + """ + if node not in self.end_nodes: + raise Exception("Can only remove end nodes") + self.end_nodes.remove(node) + self.nodes.remove(node) + + for parent_node in node.parent_nodes: + parent_node.child_nodes.remove(node) + self.end_nodes += node.parent_nodes + node.parent_nodes = [] + + +def load_model(onnx_file: str) -> onnx.ModelProto: + """ + Loads the onnx file, checks the model and converts it to a common version if necessary. + + :param onnx_file: + :return: the loaded onnx-model + """ + TARGET_VERSION = 12 + model = onnx.load(onnx_file) + onnx.checker.check_model(model) + if len(list(model.opset_import)) == 1 and list(model.opset_import)[0].version == TARGET_VERSION: + return model + else: + return onnx.version_converter.convert_version(model, TARGET_VERSION) + + +def get_value_info(graph: onnx.GraphProto, name: str) -> onnx.ValueInfoProto: + """ + Searches the `graph` for the given `name` and returns the associated ValueInfo, + if the name is not found None is returned. + + :param graph: the onnx-graph that shall be searched + :param name: the name of the value + :return: the value-info or None if it is not found + """ + for info in graph.input: + if info.name == name: + return info + + for info in graph.value_info: + if info.name == name: + return info + + for info in graph.output: + if info.name == name: + return info + + return None + + +def get_graph_inputs_without_initializers(graph: onnx.GraphProto) -> [onnx.ValueInfoProto]: + """ + Returns all inputs of the `graph` that have no associated initializer values. + + :param graph: the onnx-graph + :return: list of uninitialized inputs + """ + inputs_without_initializers = [] + for input in graph.input: + has_initializer = False + for initializer in graph.initializer: + if initializer.name == input.name: + has_initializer = True + break + + if not has_initializer: + inputs_without_initializers.append(input) + + return inputs_without_initializers + + +def get_graph_inputs_with_initializers(graph: onnx.GraphProto) -> [(onnx.ValueInfoProto, onnx.TensorProto)]: + """ + Returns all initialized inputs of the `graph` with their corresponding initializer. + + :param graph: the onnx-graph + :return: list of tuples of (input, initializer) + """ + inputs_with_initializers = [] + + for input in graph.input: + for initializer in graph.initializer: + if initializer.name == input.name: + inputs_with_initializers.append((input, initializer)) + + return inputs_with_initializers + + +class PreparedValue: + """ Class for preparing onnx value structures for writing them to the dml script """ + def __init__(self, value_info: onnx.ValueInfoProto, initializer: onnx.TensorProto = None): + + systemds_supported_types = ["integer", "boolean", "double", "string"] + + # TODO: these type translations are not correct double -> float + # Translating onnx types to systemds types + type_translation = { + 1: "double", # float + 2: "unsigned integer", # uint8_t + 3: "integer", # int8_t + 4: "unsigned integer", # uint16_t + 5: "integer", # int16_t + 6: "integer", # int32_t + 7: "long", # int64_t + 8: "string", + 9: "boolean", # bool + + 10: "double", # float16, + 11: "double", + 12: "unsigned integer", # uint32 + 13: "unsigned long", # uint64 + + 14: "COMPLEX64", + 15: "COMPLEX128", + 16: "BFLOAT16" + } + + if value_info.type.tensor_type.elem_type not in type_translation.keys(): + raise NotImplementedError("Only support Tensor Types") + + # TODO: add support for other data types + + self.value_type = type_translation[value_info.type.tensor_type.elem_type] + if self.value_type not in systemds_supported_types: + raise NotImplementedError("The type " + self.value_type + " is currently not supported") + + self.shape = [] + dims = get_valueinfo_dimensions(value_info) + + if len(dims) == 1 and dims[0] == 1: + self.data_type = "scalar" + self.shape = [1] + else: + self.data_type = "matrix" + if self.value_type != "double": + raise NotImplementedError("A matrix can only have the type double") + shape_dimensions = value_info.type.tensor_type.shape.dim + for dim in shape_dimensions: + # TODO: shapes with no value but instead name -> support? + if len(dim.dim_param) != 0: + raise NotImplementedError("Only support dim_value") + self.shape.append(dim.dim_value) + + if len(self.shape) > 2: + # TODO: not sure this is the solution for every instance of this problem + # Multiply all shapes right + rows = self.shape[0] + cols = functools.reduce(lambda s0, s1: s0 * s1, self.shape[1:]) + self.shape = [rows, cols] + + self.identifier_name = value_info.name + self.description = value_info.doc_string + self.initializer = None + + if initializer: + self.initializer_values = list(initializer.float_data) + + +def get_valueinfo_dimensions(value_info: onnx.ValueInfoProto) -> [int]: + return [dim.dim_value for dim in value_info.type.tensor_type.shape.dim] + diff --git a/src/main/python/systemds/onnx_systemds/operator_gen.py b/src/main/python/systemds/onnx_systemds/operator_gen.py new file mode 100644 index 0000000..b8021ac --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/operator_gen.py @@ -0,0 +1,465 @@ +# 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. +from random import randint + +import jinja2 +import onnx +import systemds.onnx_systemds.onnx_helper as onnx_helper +from systemds.onnx_systemds import util + + +class GeneratedScriptPart: + def __init__(self, dml_script: str, imports: [str] = None, sub_graphs: [onnx.GraphProto] = None): + if sub_graphs is None: + sub_graphs = [] + if imports is None: + imports = [] + self.dml_script = dml_script + self.imports = imports + self.sub_graphs = sub_graphs + + +def gen_simple_function_call(env: jinja2.environment.Environment, graph: onnx.GraphProto, + node: onnx.NodeProto) -> GeneratedScriptPart: + """ + Generates a simple function call by directly providing the node inputs as arguments + and node outputs as outputs to a function call. Additionally adds the required imports. + + :param env: Jinja environment to load the template files + :param graph: the onnx-graph for which the script shall be generated + :param node: the onnx-node for which the script shall be generated + :return: The generated script part + """ + operator_template = env.get_template("operators/" + "function_call.dml.jinja") + import_template = env.get_template("module_import.dml.jinja") + + if len(list(node.output)) != 1: + raise Exception("Function call needs output") + + if len(node.attribute) != 0: + raise Exception("Attributes not supported for this generator") + + required_import = { + "Relu": {"path": "/nn/layers/relu.dml", "import_name": "relu_layer", "function_name": "forward"}, + "Tanh": {"path": "/nn/layers/tanh.dml", "import_name": "tanh_layer", "function_name": "forward"}, + "Sigmoid": {"path": "/nn/layers/sigmoid.dml", "import_name": "sigmoid_layer", "function_name": "forward"}, + "Softmax": {"path": "/nn/layers/softmax.dml", "import_name": "softmax_layer", "function_name": "forward"} + } + + import_render = "" + function_name = node.op_type + function_namespace = "" + if node.op_type in required_import.keys(): + module_import = required_import[node.op_type] + import_render = import_template.render( + path=module_import["path"], + name=module_import["import_name"] + ) + function_name = module_import["function_name"] + function_namespace = module_import["import_name"] + + node_render = operator_template.render( + function_namespace=function_namespace, + function=function_name, + arguments=list(node.input), + outputs=list(node.output), + doc_string=node.doc_string + ) + return GeneratedScriptPart(imports=[import_render], dml_script=node_render) + + +def gen_2input_1output_operator(env: jinja2.environment.Environment, graph: onnx.GraphProto, + node: onnx.NodeProto) -> GeneratedScriptPart: + """ + Generates simple operator calls like 'z = x + y' which have two inputs (left and right) and one output. + :param env: Jinja environment to load the template files + :param graph: the onnx-graph for which the script shall be generated + :param node: the onnx-node for which the script shall be generated + :return: The generated script part + """ + operator = { + "Add": "+", + "Sub": "-", + "MatMul": "%*%", + "And": "&", + "Or": "|" + } + operator_template = env.get_template("operators/2input_1output_operator.dml.jinja") + + if len(node.attribute) != 0: + raise Exception("attributes not supported for operator") + + if len(list(node.input)) > 2 or len(list(node.output)) > 1: + raise Exception("Operator needs 2 inputs and 1 output") + + node_render = operator_template.render( + input_0=list(node.input)[0], + input_1=list(node.input)[1], + output=list(node.output)[0], + operator=operator[node.op_type], + doc_string=node.doc_string + ) + return GeneratedScriptPart(node_render) + + +def gen_1input_1output_mat_operator(env: jinja2.environment.Environment, graph: onnx.GraphProto, + node: onnx.NodeProto) -> GeneratedScriptPart: + """ + Generates simple operators like 'y = -x' which have one input and one output. + :param env: Jinja environment to load the template files + :param graph: the onnx-graph for which the script shall be generated + :param node: the onnx-node for which the script shall be generated + :return: The generated script part + """ + template_for_operator = { + "Neg": "neg.dml.jinja", + } + + operator_template = env.get_template("operators/" + template_for_operator[node.op_type]) + + if len(node.attribute) != 0: + raise Exception("attributes not supported for operator") + + if len(list(node.input)) != 1 or len(list(node.output)) != 1: + raise Exception("Operator needs 1 input and 1 output") + + node_render = operator_template.render( + input=list(node.input)[0], + output=list(node.output)[0], + doc_string=node.doc_string + ) + return GeneratedScriptPart(dml_script=node_render) + + +def gen_dropout_call(env: jinja2.environment.Environment, graph: onnx.GraphProto, + node: onnx.NodeProto) -> GeneratedScriptPart: + operator_template = env.get_template("operators/" + "function_call.dml.jinja") + import_template = env.get_template("module_import.dml.jinja") + + function_namespace = "dropout_layer" + function_name = "forward" + path = "/nn/layers/dropout.dml" + + # * Inputs: + # * - X: Inputs, of shape (any, any). + # * - p: Probability of keeping a neuron output. + # * - seed: [Optional: -1] Random number generator seed to allow for + # * deterministic evaluation. Set to -1 for a random seed. + # * Outputs: + # * - out: Outputs, of same shape as `X`. + # * - mask: Dropout mask used to compute the output. + + X = list(node.input)[0] + p = 0.5 + seed = -1 + if len(list(node.attribute)) > 0: + attributes = list(node.attribute) + if attributes[0].name != "ratio" or len(attributes) > 1: + raise Exception("Error in generating dropout call invalid attributes" + str(attributes)) + p = attributes[0].f + + import_render = import_template.render( + path=path, + name=function_namespace + ) + + node_render = operator_template.render( + function_namespace=function_namespace, + function=function_name, + arguments=[X, p, seed], + outputs=list(node.output), + doc_string=node.doc_string + ) + return GeneratedScriptPart(imports=[import_render], dml_script=node_render) + + +def __compute_pad(auto_pad: str, Hf: int, Wf: int, strides: [int], pads: [int], Hin: int, Win: int): + strideh = strides[0] + stridew = strides[1] + + if auto_pad == "NOTSET": + padh = pads[0] + padw = pads[1] + if pads[0] != pads[2] or pads[1] != pads[3]: + raise Exception("Only support symmetric pads") + elif auto_pad == "SAME_UPPER" or "SAME_LOWER": + # pad such that output size matches input + padh = (Hin * (strideh - 1) + Hf - strideh) / 2 + padw = (Win * (stridew - 1) + Wf - stridew) / 2 + elif auto_pad == "VALID": + # no padding + padh = 0 + padw = 0 + else: + raise Exception("Invalid auto_pad value") + + return padh, padw + + +def gen_maxpool_call(env: jinja2.environment.Environment, graph: onnx.GraphProto, + node: onnx.NodeProto) -> GeneratedScriptPart: + operator_template = env.get_template("operators/" + "function_call.dml.jinja") + import_template = env.get_template("module_import.dml.jinja") + + function_namespace = "maxpool_layer" + function_name = "forward" + path = "/nn/layers/max_pool2d.dml" + + # * Inputs: + # * - X: Inputs, of shape (N, C*Hin*Win). + # * - C: Number of input channels (dimensionality of input depth). + # * - Hin: Input height. + # * - Win: Input width. + # * - Hf: Filter height. + # * - Wf: Filter width. + # * - strideh: Stride over height. + # * - stridew: Stride over width. + # * - padh: Padding for top and bottom sides. + # * A typical value is 0. + # * - padw: Padding for left and right sides. + # * A typical value is 0. + # * + # * Outputs: + # * - out: Outputs, of shape (N, C*Hout*Wout). + # * - Hout: Output height. + # * - Wout: Output width. + + if len(node.input) != 1: + raise Exception("Invalid number of inputs") + if len(node.output) < 1 or len(node.output) > 2: + raise Exception("Invalid number of outputs") + + # Inputs + x = onnx_helper.get_value_info(graph, node.input[0]) + # dimensions are (N x C x H x W), where N is the batch size, C is the number of channels, + # and H and W are the height and the width + x_shape = onnx_helper.get_valueinfo_dimensions(x) + if len(x_shape) > 4: + raise NotImplementedError("Currently only MaxPool-2D supported") + + batch_size = x_shape[0] # TODO: currently not used + C = x_shape[1] + Hin = x_shape[2] + Win = x_shape[3] + + # Attributes + auto_pad = "NOTSET" + ceil_mode = 0 + dilations = [1, 1] + kernel_shape = None + pads = [0, 0, 0, 0] + storage_order = 0 + strides = [1, 1] + for attribute in node.attribute: + if attribute.name == "auto_pad": + auto_pad = attribute.strings[0] + elif attribute.name == "ceil_mode": + ceil_mode = attribute.ints[0] + raise NotImplementedError("Currently no support for ceil_mode") + elif attribute.name == "dilations": + raise NotImplementedError + elif attribute.name == "kernel_shape": + kernel_shape = attribute.ints + elif attribute.name == "pads": + pads = attribute.ints + elif attribute.name == "storage_order": + raise NotImplementedError("Currently no support for storage_order") + elif attribute.name == "strides": + strides = attribute.ints + else: + raise Exception("Invalid Attribute") + + if kernel_shape is None: + raise Exception("kernel_shape attribute is required") + + Hf = kernel_shape[0] + Wf = kernel_shape[1] + strideh = strides[0] + stridew = strides[1] + padh, padw = __compute_pad(auto_pad, Hf, Wf, strides, pads, Hin, Win) + + # Create render + node_render = operator_template.render( + function_namespace=function_namespace, + function=function_name, + arguments=[x.name, C, Hin, Win, Hf, Wf, strideh, stridew, padh, padw], + outputs=list(node.output), + doc_string=node.doc_string + ) + + import_render = import_template.render( + path=path, + name=function_namespace + ) + + return GeneratedScriptPart(imports=[import_render], dml_script=node_render) + + +def gen_conv_call(env: jinja2.environment.Environment, graph: onnx.GraphProto, node: onnx.NodeProto) \ + -> GeneratedScriptPart: + operator_template = env.get_template("operators/" + "function_call.dml.jinja") + import_template = env.get_template("module_import.dml.jinja") + + function_namespace = "conv_layer" + function_name = "forward" + path = "/nn/layers/conv2d.dml" + + # * Inputs: + # * - X: Inputs, of shape (N, C*Hin*Win). + # * - W: Weights, of shape (F, C*Hf*Wf). + # * - b: Biases, of shape (F, 1). + # * - C: Number of input channels (dimensionality of input depth). + # * - Hin: Input height. + # * - Win: Input width. + # * - Hf: Filter height. + # * - Wf: Filter width. + # * - strideh: Stride over height. + # * - stridew: Stride over width. + # * - padh: Padding for top and bottom sides. + # * - padw: Padding for left and right sides. + # * + # * Outputs: + # * - out: Outputs, of shape (N, F*Hout*Wout). + # * - Hout: Output height. + # * - Wout: Output width. + + if len(node.input) < 2 or len(node.input) > 3: + raise Exception("Invalid number of inputs") + + if len(node.output) > 1: + raise Exception("Invalid number of outputs") + + # Inputs + x = onnx_helper.get_value_info(graph, node.input[0]) + # size (N x C x H x W), where N is the batch size, C is the number of channels, and H and W are the height and width + x_shape = onnx_helper.get_valueinfo_dimensions(x) + if len(x_shape) > 4: + raise NotImplementedError("Currently only Conv-2D supported") + batch_size = x_shape[0] # TODO: Batch size unused? + C = x_shape[1] + Hin = x_shape[2] + Win = x_shape[3] + + w = onnx_helper.get_value_info(graph, node.input[1]) + W_shape = onnx_helper.get_valueinfo_dimensions(w) + M = W_shape[0] + C_group = W_shape[1] # TODO Channels/group unused? + Hf = W_shape[2] + Wf = W_shape[3] + + bias = None + bias_initializer_render = "" + if len(node.input) == 2: + # Generate 0-bias if no bias given + generated_bias_identifier = "gen_bias" + while onnx_helper.get_value_info(graph, generated_bias_identifier) is not None: + # add random number to create unique identifier if already exists + generated_bias_identifier += str(randint()) + + bias_init_template = env.get_template("matrix_initialize.dml.jinja") + bias_initializer_render = bias_init_template.render( + identifier_name=generated_bias_identifier, + initializer_values=[0] * M, + rows=M, + cols=1 + ) + bias = generated_bias_identifier + elif len(node.input) == 3: + bias = node.input[3] + + # Attributes + auto_pad = "NOTSET" + dilations = [1, 1] + group = 1 + pads = [0, 0, 0, 0] + strides = [1, 1] + for attribute in node.attribute: + if attribute.name == "auto_pad": + auto_pad = attribute.strings[0] + elif attribute.name == "dilations": + raise NotImplementedError + elif attribute.name == "group": + group = attribute.ints[0] + elif attribute.name == "kernel_shape": + kernel_shape = attribute.ints + if kernel_shape[0] != Hf or kernel_shape[1] != Wf: + raise Exception("Invalid kernel shape") + elif attribute.name == "pads": + pads = attribute.ints + elif attribute.name == "strides": + strides = attribute.ints + else: + raise Exception("Invalid Attribute") + + strideh = strides[0] + stridew = strides[1] + padh, padw = __compute_pad(auto_pad, Hf, Wf, strides, pads, Hin, Win) + + node_render = operator_template.render( + function_namespace=function_namespace, + function=function_name, + arguments=[x.name, w.name, bias, C, Hin, Win, Hf, Wf, strideh, stridew, padh, padw], + outputs=list(node.output), + doc_string=node.doc_string + ) + + import_render = import_template.render( + path=path, + name=function_namespace + ) + + return GeneratedScriptPart(imports=[import_render], dml_script=bias_initializer_render + "\n" + node_render) + + +def gen_if_call(env: jinja2.environment.Environment, graph: onnx.GraphProto, node: onnx.NodeProto) \ + -> GeneratedScriptPart: + operator_template = env.get_template("operators/if_operator.dml.jinja") + function_call_template = env.get_template("operators/function_call.dml.jinja") + + if len(node.input) != 1: + raise Exception("Wrong number of inputs") + if len(node.attribute) != 2: + raise Exception("Wrong number of attributes") + if node.attribute[0].name != "else_branch" or node.attribute[1].name != "then_branch": + raise Exception("Wrong attributes") + + else_graph = node.attribute[0].g + then_graph = node.attribute[1].g + + else_call = function_call_template.render( + doc_string="", + function_namespace="", + function=util.generate_function_name(else_graph.name), + arguments=[i.name for i in list(else_graph.input)], + outputs=[o.name for o in list(else_graph.output)], + ) + + then_call = function_call_template.render( + doc_string="", + function_namespace="", + function=util.generate_function_name(then_graph.name), + arguments=[i.name for i in list(then_graph.input)], + outputs=[o.name for o in list(then_graph.output)], + ) + + sub_graphs = [else_graph, then_graph] + + node_render = operator_template.render( + cond=node.input[0], + then_function_call=then_call, + else_function_call=else_call + ) + + return GeneratedScriptPart(dml_script=node_render, sub_graphs=sub_graphs) diff --git a/src/main/python/systemds/onnx_systemds/render.py b/src/main/python/systemds/onnx_systemds/render.py new file mode 100644 index 0000000..8140f95 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/render.py @@ -0,0 +1,215 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from systemds.onnx_systemds import util, operator_gen +import onnx +import systemds.onnx_systemds.onnx_helper as onnx_helper +import jinja2 + +# Each operator listed shall be supported by this converter +operator_generators = { + "Add": operator_gen.gen_2input_1output_operator, + "Sub": operator_gen.gen_2input_1output_operator, + "MatMul": operator_gen.gen_2input_1output_operator, + "Neg": operator_gen.gen_1input_1output_mat_operator, + "Xor": operator_gen.gen_simple_function_call, + "Or": operator_gen.gen_2input_1output_operator, + "And": operator_gen.gen_2input_1output_operator, + "Relu": operator_gen.gen_simple_function_call, + "Tanh": operator_gen.gen_simple_function_call, + "Sigmoid": operator_gen.gen_simple_function_call, + "Softmax": operator_gen.gen_simple_function_call, + "Dropout": operator_gen.gen_dropout_call, + "MaxPool": operator_gen.gen_maxpool_call, + "Conv": operator_gen.gen_conv_call, + "If": operator_gen.gen_if_call, +} + + +def gen_node_script(env: jinja2.environment.Environment, graph: onnx.GraphProto, node: onnx.NodeProto) \ + -> operator_gen.GeneratedScriptPart: + """ + Generates a dml script snippet, the required imports and sub-graphs for the given `node` + + :param env: Jinja environment to load the template files + :param graph: the onnx graph for which the script shall be generated + :param node: the node for which the dml snippet shall be generated + :return: The generated script-part + """ + try: + return operator_generators[node.op_type](env, graph, node) + except KeyError as error: + print("Operator " + str(node.op_type) + " not supported") + raise error + + +def gen_graph_functions(env: jinja2.environment.Environment, main_graph: onnx.GraphProto) -> ([str], str, [str]): + """ + Traverses the node tree of the onnx-graph structure and generates a script string for each node, + as well as a string for the required imports together with all functions of sub-graphs. + The resulting lists are correctly ordered for inserting them in the dml script. + + :param env: Jinja environment to load the template files + :param main_graph: the onnx graph for which the script shall be generated + :return: Tuple (imports, main function, sub-graph functions) + """ + + main_function_node_scripts = [] + sub_graph_functions = [] + generated_imports = set() # set to avoid duplicate imports + + node_tree = onnx_helper.NodeTree(main_graph.node) + available_outputs = [o.name for o in list(main_graph.output)] + + while len(node_tree.nodes) != 0: + current_lowest_nodes = node_tree.end_nodes + + # Find next operation to insert -> check if all outputs are available + next_tree_node = None + for tree_node in current_lowest_nodes: + if all(output in available_outputs for output in list(tree_node.node.output)): + next_tree_node = tree_node + break + if not next_tree_node: + raise Exception("Error in parsing nodes, did not find a next node to compute") + + # Insert generated parts + generated_node = gen_node_script(env, main_graph, next_tree_node.node) + generated_imports.update(generated_node.imports) + main_function_node_scripts.append(generated_node.dml_script) + # handle sub-graphs + for sub_graph in generated_node.sub_graphs: + sub_graph_imports, sub_graph_main_function, sub_graph_sub_graph_functions = \ + gen_graph_functions(env, sub_graph) + # Inherit imports + generated_imports.update(sub_graph_imports) + # Inherit sub-graph functions of sub-graph + sub_graph_functions += sub_graph_sub_graph_functions + # Sub-graph main-function becomes sub-graph function + sub_graph_functions.append(sub_graph_main_function) + + # After insertion the inputs to the node become available and the node is removed + available_outputs += list(next_tree_node.node.input) + node_tree.remove_end_node(next_tree_node) + + main_function_node_scripts.reverse() + main_graph_function = render_function(env, main_graph, main_function_node_scripts) + return list(generated_imports), main_graph_function, sub_graph_functions + + +def render_function(env: jinja2.environment.Environment, graph: onnx.GraphProto, + generated_node_scripts: [str]) -> str: + """ + Generates the dml function for the given `graph` and inserts the 'generated_node_scripts' in + the function-body. + + :param env: Jinja environment to load the template files + :param graph: the graph for which the function shall be generated + :param generated_node_scripts: the node scripts in correct order for the function-body + :return: the generated function + """ + function_template = env.get_template("graph_function.dml.jinja") + + inputs_with_initializers = onnx_helper.get_graph_inputs_with_initializers(graph) + inputs_without_initializers = onnx_helper.get_graph_inputs_without_initializers(graph) + outputs = list(graph.output) + + # prepare inputs/outputs + function_inputs = [onnx_helper.PreparedValue(i) for i in inputs_without_initializers] + function_outputs = [onnx_helper.PreparedValue(o) for o in outputs] + function_initializers = [onnx_helper.PreparedValue(info, init) for info, init in inputs_with_initializers] + + # render function + graph_function_render = function_template.render( + function_inputs=function_inputs, + function_outputs=function_outputs, + function_start_initializers=function_initializers, + graph_function_name=util.generate_function_name(graph.name), + graph_function_description=graph.doc_string, + node_scripts=generated_node_scripts + ) + return graph_function_render + + +def gen_model_header(env: jinja2.environment.Environment, model: onnx.ModelProto) -> str: + """ + Generates the header of the script for the given `model` + + :param env: Jinja environment to load the template files + :param model: the onnx model for which the header shall be generated + :return: the generated header + """ + header_template = env.get_template("model_header.dml.jinja") + header_infos = dict() + + header_infos["ir_version"] = model.ir_version + opset_import = list() + for opset in model.opset_import: + if len(opset.domain) == 0: + opset.domain = "ONNX" + opset_import.append(opset.domain + "/" + str(opset.version)) + header_infos["producer_name"] = model.producer_name + header_infos["producer_version"] = model.producer_version + header_infos["domain"] = model.domain + header_infos["model_version"] = model.model_version + header_infos["doc_string"] = model.doc_string + metadata_props = [[prop.key, prop.vale] for prop in model.metadata_props] + + model_header_render = header_template.render( + header_components=header_infos, + opset_import=opset_import, + metadata_props=metadata_props + ) + return model_header_render + + +def gen_script(model: onnx.ModelProto, output_file: str = None) -> str: + """ + Generate the dml script for the given `model` and return it. + If an `output_file` is given, the script is also written to a file. + + :param model: the model for which the dml script shall be generated + :param output_file: (optional) the file to which the script shall be written + :return: the generated dml-script + """ + current_dir = os.path.dirname(os.path.realpath(__file__)) + env = jinja2.Environment(loader=jinja2.FileSystemLoader(current_dir + '/templates/')) + model_header_render = gen_model_header(env, model) + imports, main_function, sub_functions = gen_graph_functions(env, model.graph) + + wdir = "" + if len(imports) > 0: + # need to set wdir to enable imports + wdir = util.resolve_systemds_root() + "/scripts" + + main_template = env.get_template("main.dml.jinja") + result_render = main_template.render( + title="This file was generated by onnx-systemds", + model_header_render=model_header_render, + wdir=wdir, + imports=imports, + main_function=main_function, + sub_functions=sub_functions + ) + if output_file: + directory = os.path.dirname(output_file) + if len(directory) > 0: + os.makedirs(directory, exist_ok=True) + with open(output_file, 'w') as f: + f.write(result_render) + + return result_render diff --git a/src/main/python/systemds/onnx_systemds/templates/graph_function.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/graph_function.dml.jinja new file mode 100644 index 0000000..d35d70a --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/graph_function.dml.jinja @@ -0,0 +1,54 @@ +{# + 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. + #} +{%- macro parameter_description() -%} +# {{ "%-15.15s" | format("NAME") }} {{ "%-8.8s" | format("TYPE") }} {{ "%-12.12s" | format("VALUE_TYPE") }} {{ "%-8.8s" | format("SHAPE") }} {{ "%-10s" | format("MEANING") }} +{%- endmacro -%} + +{% import 'util.dml.jinja' as util %} +{%- if graph_function_description != "" -%} # {{ graph_function_description + "\n" }} {%- endif -%} +{%- if function_inputs | length > 0 -%} +# -------------------------------------------------------------------------------------------- +# INPUT PARAMETERS: +# -------------------------------------------------------------------------------------------- +{{ parameter_description() }} +{% for input in function_inputs -%} +# {{ "%-15.15s" | format(input.identifier_name) }} {{ "%-8.8s" | format(input.data_type,) }} {{ "%-12.12s" | format(input.value_type,) }} {{ "%-8.8s" | format(input.shape,) }} {{ "%-10s" | format(input.description,) }} +{% endfor -%} +{%- endif %} +{%- if function_outputs | length > 0 -%} +# --------------------------------------------------------------------------------------------- +# OUTPUTS: +# --------------------------------------------------------------------------------------------- +{{ parameter_description() }} +{% for output in function_outputs -%} +# {{ "%-15.15s" | format(output.identifier_name) }} {{ "%-8.8s" | format(output.data_type,) }} {{ "%-12.12s" | format(output.value_type,) }} {{ "%-8.8s" | format(output.shape,) }} {{ "%-10s" | format(output.description,) }} +{% endfor -%} +# --------------------------------------------------------------------------------------------- +{%- endif %} +{{ graph_function_name }} = function( +{{ util.generate_call_list(function_inputs) }} +) +return ( +{{ util.generate_call_list(function_outputs) }} +) { +{%- for initializer in function_start_initializers %} + {{ util.initialize_variable(initializer) }} +{% endfor %} +{% for node_script in node_scripts -%} +{{ node_script | indent(4, first=true) }} +{% endfor -%} +} \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/templates/graph_header.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/graph_header.dml.jinja new file mode 100644 index 0000000..7e831fe --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/graph_header.dml.jinja @@ -0,0 +1,22 @@ +{# + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + #} +# ------------------------------------------------------------- +# Graph +{%- for key, value in header_components.items() %} +# {{ key }}: {{ value }} +{%- endfor %} +# ------------------------------------------------------------- diff --git a/src/main/python/systemds/onnx_systemds/templates/main.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/main.dml.jinja new file mode 100644 index 0000000..db77074 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/main.dml.jinja @@ -0,0 +1,26 @@ +{# + 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. + #} +### {{ title }} +{{ model_header_render }} +{% if wdir != "" -%} setwd("{{ wdir }}") {%- endif %} +{% for import in imports -%} +{{ import }} +{% endfor %} +{% for sub_function in sub_functions %} +{{ sub_function }} +{% endfor %} +{{ main_function }} diff --git a/src/main/python/systemds/onnx_systemds/templates/matrix_initialize.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/matrix_initialize.dml.jinja new file mode 100644 index 0000000..a0d930c --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/matrix_initialize.dml.jinja @@ -0,0 +1,24 @@ +{# + 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. + #} +{{ identifier_name }} = matrix(" + {%- for value in initializer_values -%} + {{ value }} + {%- if not loop.last %} {% endif -%} + {%- endfor %}", + rows={{ rows }}, + cols={{ cols -}} +) \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/templates/model_header.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/model_header.dml.jinja new file mode 100644 index 0000000..e1ba18d --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/model_header.dml.jinja @@ -0,0 +1,36 @@ +{# + 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. + #} +# ------------------------------------------------------------- +# Model +{% for key, value in header_components.items() -%} +{%- if value != "" -%} +# {{ key }}: {{ value }} {%- if not loop.last -%} {{ "\n" }} {%- endif -%} +{%- endif %} +{%- endfor -%} +# opset_import: +{%- for opset_domain in opset_import %} +# - domain/opset = {{ opset_domain }} +{%- endfor %} +{%- if metadata_props | length > 0 -%} +{%- for key, value in metadata_props %} +{% if loop.first %} +# metadata_props: +{% endif %} +# - {{ key }}: {{ value }} +{%- endfor %} +{% endif %} +# ------------------------------------------------------------- diff --git a/src/main/python/systemds/onnx_systemds/templates/module_import.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/module_import.dml.jinja new file mode 100644 index 0000000..a2a3b92 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/module_import.dml.jinja @@ -0,0 +1,17 @@ +{# + 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. + #} +source("{{ path }}") as {{ name }} \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/templates/operators/2input_1output_operator.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/operators/2input_1output_operator.dml.jinja new file mode 100644 index 0000000..5d3d2fa --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/operators/2input_1output_operator.dml.jinja @@ -0,0 +1,18 @@ +{# + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + #} +{% if doc_string != "" %}# {{ doc_string }} {% endif %} +{{ output }} = ({{ input_0 }} {{ operator }} {{ input_1 }}) \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/templates/operators/function_call.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/operators/function_call.dml.jinja new file mode 100644 index 0000000..96aecfa --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/operators/function_call.dml.jinja @@ -0,0 +1,31 @@ +{# + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + #} +{% if doc_string != "" %}# {{ doc_string + "\n" }} {% endif %} + {%- if outputs | length == 1 -%} +{%- for output in outputs -%}{{ output }} = {% endfor -%} +{% elif outputs | length > 1 -%} +[ +{%- for output in outputs -%} +{{ output }} +{%- if loop.last %}] = {% else -%}, {% endif -%} +{%- endfor -%} +{%- endif -%} +{% if function_namespace != "" -%}{{ function_namespace }}::{%- endif -%}{{ function }}( + {%- for argument in arguments -%} + {{ argument }} + {%- if not loop.last -%}, {%- endif -%} +{%- endfor -%}) \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/templates/operators/if_operator.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/operators/if_operator.dml.jinja new file mode 100644 index 0000000..816987c --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/operators/if_operator.dml.jinja @@ -0,0 +1,19 @@ +{# + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + #} +if ({{ cond }}) { {{ then_function_call | indent(4) }} +} else { {{ else_function_call | indent(4) }} +} \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/templates/operators/neg.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/operators/neg.dml.jinja new file mode 100644 index 0000000..4a733f0 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/operators/neg.dml.jinja @@ -0,0 +1,18 @@ +{# + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to you under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + #} +{% if doc_string != "" %}# {{ doc_string }} {% endif %} +{{ output }} = (-{{ input }}) diff --git a/src/main/python/systemds/onnx_systemds/templates/util.dml.jinja b/src/main/python/systemds/onnx_systemds/templates/util.dml.jinja new file mode 100644 index 0000000..b3f4084 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/templates/util.dml.jinja @@ -0,0 +1,42 @@ +{# + 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. + #} +{% macro generate_call_list(variables) -%} +{% for input in variables -%} + {%- if input.data_type == "matrix" -%} + {{ ' ' + "matrix" }} [{{ input.value_type }}] {{ input.identifier_name }} + {%- elif input.data_type == "scalar" -%} + {{ ' ' + input.value_type }} {{ input.identifier_name }} + {%- endif -%} + {%- if not loop.last -%}, +{% endif %} +{%- endfor %} +{%- endmacro %} + +{% macro initialize_variable(variable) -%} +{%- if variable.data_type == "matrix" -%} +{{ variable.identifier_name }} = matrix(" + {%- for value in variable.initializer_values -%} + {{ value }} + {%- if not loop.last %} {% endif -%} + {%- endfor %}", + rows={{ variable.shape[0] }}, + cols={{ variable.shape[1] -}} +) +{%- elif variable.data_type == "scalar" -%} + {{ input.value_type }} {{ input.identifier_name }} = {{ input.initializer_values[0] }} + {%- endif -%} +{%- endmacro %} \ No newline at end of file diff --git a/src/main/python/systemds/onnx_systemds/util.py b/src/main/python/systemds/onnx_systemds/util.py new file mode 100644 index 0000000..4ba9638 --- /dev/null +++ b/src/main/python/systemds/onnx_systemds/util.py @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + + +def generate_function_name(graph_name: str) -> str: + """ + Takes the given graph name and constructs a valid function name from it. + :param graph_name: The name of the graph. + :return: the constructed function name. + """ + function_name = "gen_" + re.sub(r"[-| ]", "_", graph_name.lower()) + return re.sub(r"[^0-9a-z_]", "", function_name) + + +def resolve_systemds_root() -> str: + """ + Searches for SYSTEMDS_ROOT in the environment variables. + :return: The SYSTEMDS_ROOT path + """ + try: + systemds_root_path = os.environ['SYSTEMDS_ROOT'] + return systemds_root_path + except KeyError as error: + print("ERROR environment variable SYSTEMDS_ROOT_PATH not set could not resolve path to module") + exit(-1) diff --git a/src/main/python/systemds/__init__.py b/src/main/python/tests/__init__.py similarity index 83% copy from src/main/python/systemds/__init__.py copy to src/main/python/tests/__init__.py index e51fbf8..217e5db 100644 --- a/src/main/python/systemds/__init__.py +++ b/src/main/python/tests/__init__.py @@ -1,4 +1,3 @@ -#------------------------------------------------------------- # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -16,7 +15,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# -#------------------------------------------------------------- - -__all__ = ['context', 'matrix'] diff --git a/src/main/python/systemds/__init__.py b/src/main/python/tests/onnx/__init__.py similarity index 83% copy from src/main/python/systemds/__init__.py copy to src/main/python/tests/onnx/__init__.py index e51fbf8..fe95886 100644 --- a/src/main/python/systemds/__init__.py +++ b/src/main/python/tests/onnx/__init__.py @@ -1,4 +1,3 @@ -#------------------------------------------------------------- # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -17,6 +16,3 @@ # specific language governing permissions and limitations # under the License. # -#------------------------------------------------------------- - -__all__ = ['context', 'matrix'] diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_conv_layer_2_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_conv_layer_2_wrapper.dml new file mode 100644 index 0000000..d1cfcec --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_conv_layer_2_wrapper.dml @@ -0,0 +1,27 @@ +# 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. + +source("tests/onnx/dml_output/simple_conv_layer_2.dml") as simple_conv + + +x = matrix("0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34.", rows=1, cols=35) +W = matrix("1. 1. 1. 1. 1. 1. 1. 1. 1.", rows=1, cols=9) + +[o0, o1, o2] = simple_conv::gen_a_simple_convolution_graph(x, W) +out = append(toString(o0), toString(o1)) +out = append(out, toString(o2)) + +write(out, "tests/onnx/output_test/simple_conv_layer_2.out") + diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_conv_layer_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_conv_layer_wrapper.dml new file mode 100644 index 0000000..0dd1daa --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_conv_layer_wrapper.dml @@ -0,0 +1,25 @@ +# 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. + +source("tests/onnx/dml_output/simple_conv_layer.dml") as simple_conv + + +x = matrix("0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24.", rows=1, cols=25) +W = matrix("1. 1. 1. 1. 1. 1. 1. 1. 1.", rows=1, cols=9) + +out = simple_conv::gen_a_simple_convolution_graph(x, W) + +write(out, "tests/onnx/output_test/simple_conv_layer.out") + diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_dropout_layer_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_dropout_layer_wrapper.dml new file mode 100644 index 0000000..25ff9cf --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_dropout_layer_wrapper.dml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source("tests/onnx/dml_output/simple_dropout_layer.dml") as simple_dropout + +A = matrix("3 8 9 10", rows=2, cols=2) +[O, mask] = simple_dropout::gen_simple_dropout_graph(A) +out = append(toString(O), toString(mask)) + +write(out, "tests/onnx/output_test/simple_dropout_layer.out") diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_if_graph_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_if_graph_wrapper.dml new file mode 100644 index 0000000..755ec52 --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_if_graph_wrapper.dml @@ -0,0 +1,27 @@ +# 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. + +source("tests/onnx/dml_output/simple_if_graph.dml") as simple_if_graph + +A = matrix("3 8 9 10", rows=2, cols=2) +cond = TRUE +O_true = simple_if_graph::gen_a_simple_if_graph(A, cond) +cond = FALSE +O_false = simple_if_graph::gen_a_simple_if_graph(A, cond) + +out = append(toString(O_true), toString(O_false)) +write(out, "tests/onnx/output_test/simple_if_graph.out") + + diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_mat_add_mul_sub_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_mat_add_mul_sub_wrapper.dml new file mode 100644 index 0000000..efcdb87 --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_mat_add_mul_sub_wrapper.dml @@ -0,0 +1,24 @@ +# 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. + +source("tests/onnx/dml_output/simple_mat_add_mul_sub.dml") as simple_mat_add_mul_sub + +A = matrix("3 8 9 10", rows=2, cols=2) +B = matrix("2 5 7 8", rows=2, cols=2) +C = matrix("1 2 3 4", rows=2, cols=2) +O = matrix(0, rows=2, cols=2) +O = simple_mat_add_mul_sub::gen_a_simple_matrix_addition_multiplication_and_substraction_test_graph(A, B, C) + +write(O, "tests/onnx/output_test/simple_mat_add_mul_sub.out") diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_mat_add_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_mat_add_wrapper.dml new file mode 100644 index 0000000..943e93d --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_mat_add_wrapper.dml @@ -0,0 +1,24 @@ +# 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. + +source("tests/onnx/dml_output/simple_mat_add.dml") as simple_mat_add + +A = matrix("3 8 9 10", rows=2, cols=2) +B = matrix("2 5 7 8", rows=2, cols=2) +C = matrix("1 2 3 4", rows=2, cols=2) +O = matrix(0, rows=2, cols=2) +O = simple_mat_add::gen_a_simple_matrix_addition_test_graph(A, B, C) + +write(O, "tests/onnx/output_test/simple_mat_add.out") diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_mat_initialized_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_mat_initialized_wrapper.dml new file mode 100644 index 0000000..f99a168 --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_mat_initialized_wrapper.dml @@ -0,0 +1,21 @@ +# 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. + +source("tests/onnx/dml_output/simple_mat_initialized.dml") as simple_mat_initialized + +O = simple_mat_initialized::gen_simple_mat_initialized_graph() + +write(O, "tests/onnx/output_test/simple_mat_initialized.out") + diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_maxpool_layer_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_maxpool_layer_wrapper.dml new file mode 100644 index 0000000..c6604af --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_maxpool_layer_wrapper.dml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +source("tests/onnx/dml_output/simple_maxpool_layer.dml") as simple_maxpool + +x = matrix("1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25", rows=1, cols=25) +out = simple_maxpool::gen_a_simple_maxpool_graph(x) + +write(out, "tests/onnx/output_test/simple_maxpool_layer.out") + diff --git a/src/main/python/tests/onnx/dml_wrapper/simple_relu_tanh_sigmoid_softmax_wrapper.dml b/src/main/python/tests/onnx/dml_wrapper/simple_relu_tanh_sigmoid_softmax_wrapper.dml new file mode 100644 index 0000000..f306094 --- /dev/null +++ b/src/main/python/tests/onnx/dml_wrapper/simple_relu_tanh_sigmoid_softmax_wrapper.dml @@ -0,0 +1,27 @@ +# 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. + +source("tests/onnx/dml_output/simple_relu_tanh_sigmoid_softmax.dml") as simple_relu_tanh + +A = matrix("0.2 2 12 20", rows=2, cols=2) +B = matrix("0.2 2 12 20", rows=2, cols=2) +C = matrix("0.2 2 12 20", rows=2, cols=2) +D = matrix("0.2 2 12 20", rows=2, cols=2) +[O, U, I, J] = simple_relu_tanh::gen_simple_relu_tanh_sigmoid_softmax_graph(A, B, C, D) +out = append(toString(O), toString(U)) +out = append(out, toString(I)) +out = append(out, toString(J)) + +write(out, "tests/onnx/output_test/simple_relu_tanh_sigmoid_softmax.out") \ No newline at end of file diff --git a/src/main/python/tests/onnx/output_reference/simple_conv_layer_2_reference.out b/src/main/python/tests/onnx/output_reference/simple_conv_layer_2_reference.out new file mode 100644 index 0000000..b7283d2 --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_conv_layer_2_reference.out @@ -0,0 +1,5 @@ +12.000 27.000 24.000 63.000 108.000 81.000 123.000 198.000 141.000 112.000 177.000 124.000 + +54.000 72.000 144.000 162.000 234.000 252.000 + +21.000 33.000 99.000 117.000 189.000 207.000 171.000 183.000 diff --git a/src/main/python/tests/onnx/output_reference/simple_conv_layer_reference.out b/src/main/python/tests/onnx/output_reference/simple_conv_layer_reference.out new file mode 100644 index 0000000..a0631f6 --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_conv_layer_reference.out @@ -0,0 +1,25 @@ +1 1 12.0 +1 2 21.0 +1 3 27.0 +1 4 33.0 +1 5 24.0 +1 6 33.0 +1 7 54.0 +1 8 63.0 +1 9 72.0 +1 10 51.0 +1 11 63.0 +1 12 99.0 +1 13 108.0 +1 14 117.0 +1 15 81.0 +1 16 93.0 +1 17 144.0 +1 18 153.0 +1 19 162.0 +1 20 111.0 +1 21 72.0 +1 22 111.0 +1 23 117.0 +1 24 123.0 +1 25 84.0 diff --git a/src/main/python/tests/onnx/output_reference/simple_if_graph_reference.out b/src/main/python/tests/onnx/output_reference/simple_if_graph_reference.out new file mode 100644 index 0000000..4c5603d --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_if_graph_reference.out @@ -0,0 +1,5 @@ +0.995 1.000 +1.000 1.000 + +3.000 8.000 +9.000 10.000 diff --git a/src/main/python/tests/onnx/output_reference/simple_mat_add_mul_sub_reference.out b/src/main/python/tests/onnx/output_reference/simple_mat_add_mul_sub_reference.out new file mode 100644 index 0000000..6f41b8a --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_mat_add_mul_sub_reference.out @@ -0,0 +1,4 @@ +1 1 -41.0 +1 2 -54.0 +2 1 -61.0 +2 2 -94.0 diff --git a/src/main/python/tests/onnx/output_reference/simple_mat_add_reference.out b/src/main/python/tests/onnx/output_reference/simple_mat_add_reference.out new file mode 100644 index 0000000..0b12bb0 --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_mat_add_reference.out @@ -0,0 +1,4 @@ +1 1 9.0 +1 2 23.0 +2 1 28.0 +2 2 32.0 diff --git a/src/main/python/tests/onnx/output_reference/simple_mat_initialized_reference.out b/src/main/python/tests/onnx/output_reference/simple_mat_initialized_reference.out new file mode 100644 index 0000000..54c1684 --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_mat_initialized_reference.out @@ -0,0 +1,9 @@ +1 1 1.0 +1 2 2.0 +1 3 3.0 +2 1 4.0 +2 2 5.0 +2 3 6.0 +3 1 7.0 +3 2 8.0 +3 3 9.0 diff --git a/src/main/python/tests/onnx/output_reference/simple_maxpool_layer_reference.out b/src/main/python/tests/onnx/output_reference/simple_maxpool_layer_reference.out new file mode 100644 index 0000000..a594c76 --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_maxpool_layer_reference.out @@ -0,0 +1,25 @@ +1 1 13.0 +1 2 14.0 +1 3 15.0 +1 4 15.0 +1 5 15.0 +1 6 18.0 +1 7 19.0 +1 8 20.0 +1 9 20.0 +1 10 20.0 +1 11 23.0 +1 12 24.0 +1 13 25.0 +1 14 25.0 +1 15 25.0 +1 16 23.0 +1 17 24.0 +1 18 25.0 +1 19 25.0 +1 20 25.0 +1 21 23.0 +1 22 24.0 +1 23 25.0 +1 24 25.0 +1 25 25.0 diff --git a/src/main/python/tests/onnx/output_reference/simple_relu_tanh_sigmoid_softmax_reference.out b/src/main/python/tests/onnx/output_reference/simple_relu_tanh_sigmoid_softmax_reference.out new file mode 100644 index 0000000..d92ee57 --- /dev/null +++ b/src/main/python/tests/onnx/output_reference/simple_relu_tanh_sigmoid_softmax_reference.out @@ -0,0 +1,11 @@ +0.200 2.000 +12.000 20.000 + +0.197 0.964 +1.000 1.000 + +0.550 0.881 +1.000 1.000 + +0.142 0.858 +0.000 1.000 diff --git a/src/main/python/tests/onnx/test_models/model_generate.py b/src/main/python/tests/onnx/test_models/model_generate.py new file mode 100644 index 0000000..3d64eb6 --- /dev/null +++ b/src/main/python/tests/onnx/test_models/model_generate.py @@ -0,0 +1,388 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import onnx +from onnx import helper + + +def save_graph(graph_def, name): + model_def = helper.make_model(graph_def, producer_name="onnx-systemds test-graph generator") + onnx.save_model(model_def, os.path.dirname(os.path.realpath(__file__)) + "/" + name) + + +def generate_simple_add_graph(): + A = helper.make_tensor_value_info('A', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable A") + B = helper.make_tensor_value_info('B', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable B") + C = helper.make_tensor_value_info('C', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable C") + D = helper.make_tensor_value_info('D', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable D") + E = helper.make_tensor_value_info('E', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable E") + F = helper.make_tensor_value_info('F', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable F") + + nodes = [ + helper.make_node("Add", ['A', 'B'], ['C'], name="AddNodeName", + doc_string="This is a description of this Add operation"), + helper.make_node("Add", ['C', 'D'], ['E'], name="MulNodeName", + doc_string="This is a description of this mul operation"), + helper.make_node("Add", ['A', 'E'], ['F'], name="MulNodeName") + ] + + graph = helper.make_graph( + nodes=nodes, + name="A simple matrix addition test graph", + inputs=[A, B, D], + outputs=[F], + initializer=None, + doc_string="Doc string of a simple matrix addition test graph", + value_info=[C, E] + ) + + save_graph(graph, "simple_mat_add.onnx") + + +def generate_simple_mat_add_mul_sub_graph(): + A = helper.make_tensor_value_info('A', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable A") + B = helper.make_tensor_value_info('B', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable B") + C = helper.make_tensor_value_info('C', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable C") + D = helper.make_tensor_value_info('D', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable D") + E = helper.make_tensor_value_info('E', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable E") + F = helper.make_tensor_value_info('F', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable F") + + nodes = [ + helper.make_node("Add", ['A', 'B'], ['C'], name="AddNodeName", + doc_string="This is a description of this Add operation"), + helper.make_node("MatMul", ['C', 'D'], ['E'], name="MulNodeName", + doc_string="This is a description of this mul operation"), + helper.make_node("Sub", ['A', 'E'], ['F'], name="MulNodeName") + ] + + graph = helper.make_graph( + nodes=nodes, + name="A simple matrix addition, multiplication and substraction test graph", + inputs=[A, B, D], + outputs=[F], + initializer=None, + doc_string="Doc string with additional information", + value_info=[C, E] + ) + + save_graph(graph, "simple_mat_add_mul_sub.onnx") + + +def generate_simple_initialized_graph(): + A_init = helper.make_tensor("A_init", onnx.TensorProto.FLOAT, [3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]) + B_init = helper.make_tensor("B_init", onnx.TensorProto.FLOAT, [3, 3], [2, 4, 6, 8, 10, 12, 14, 16, 18]) + + B_init_valinfo = helper.make_tensor_value_info(B_init.name, onnx.TensorProto.FLOAT, B_init.dims, + doc_string="A single value tensor") + A_init_valinfo = helper.make_tensor_value_info(A_init.name, onnx.TensorProto.FLOAT, A_init.dims, + doc_string="A 3x3 matrix") + C = helper.make_tensor_value_info("C", onnx.TensorProto.FLOAT, [3, 3], doc_string="This is the output C") + D = helper.make_tensor_value_info("D", onnx.TensorProto.FLOAT, [3, 3], doc_string="This is the output D") + + nodes = [ + helper.make_node("Neg", ["A_init"], ["C"]), + helper.make_node("Add", ["B_init", "C"], ["D"]) + ] + + graph = helper.make_graph( + nodes=nodes, + name="Simple mat initialized graph", + inputs=[A_init_valinfo, B_init_valinfo], + outputs=[D], + initializer=[A_init, B_init], + value_info=[A_init_valinfo, B_init_valinfo, C, D] + ) + + save_graph(graph, "simple_mat_initialized.onnx") + + +def generate_simple_boolean_noshape(): + A = helper.make_tensor_value_info('A', onnx.TensorProto.BOOL, + doc_string="This is a description of variable A", shape=[]) + B = helper.make_tensor_value_info('B', onnx.TensorProto.BOOL, + doc_string="This is a description of variable B", shape=[]) + C = helper.make_tensor_value_info('C', onnx.TensorProto.BOOL, + doc_string="This is a description of variable C", shape=[]) + D = helper.make_tensor_value_info('D', onnx.TensorProto.BOOL, + doc_string="This is a description of variable D", shape=[]) + E = helper.make_tensor_value_info('E', onnx.TensorProto.BOOL, + doc_string="This is a description of variable E", shape=[]) + + nodes = [ + helper.make_node("Or", ["A", "B"], ["C"]), + helper.make_node("And", ["C", "A"], ["D"]), + helper.make_node("Xor", ["B", "D"], ["E"]) + ] + + graph = helper.make_graph( + nodes=nodes, + name="Simple bool and or xor noshape graph", + inputs=[A, B], + outputs=[E], + value_info=[A, B, C, D, E] + ) + + save_graph(graph, "simple_bool_and_or_xor_noshape.onnx") + + +def generate_simple_relu_tanh_sigmoid_softmax(): + A = helper.make_tensor_value_info('A', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable A") + B = helper.make_tensor_value_info('B', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable B") + C = helper.make_tensor_value_info('C', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable C") + D = helper.make_tensor_value_info('D', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable D") + E = helper.make_tensor_value_info('E', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable E") + F = helper.make_tensor_value_info('F', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable F") + G = helper.make_tensor_value_info('G', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable G") + H = helper.make_tensor_value_info('H', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable H") + + nodes = [ + helper.make_node("Relu", ["A"], ["E"], doc_string="Call of Relu function"), + helper.make_node("Tanh", ["B"], ["F"], doc_string="Call of Tanh function"), + helper.make_node("Sigmoid", ["C"], ["G"], doc_string="Call of Sigmoid function"), + helper.make_node("Softmax", ["D"], ["H"], doc_string="Call of Softmax function") + ] + + graph = helper.make_graph( + nodes=nodes, + name="Simple relu tanh sigmoid softmax graph", + inputs=[A, B, C, D], + outputs=[E, F, G, H], + value_info=[A, B, D, E, F, G, H], + doc_string="This graph tests simple nn layer calls" + ) + + save_graph(graph, "simple_relu_tanh_sigmoid_softmax.onnx") + + +def generate_simple_dropout_layer(): + x = helper.make_tensor_value_info('x', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable x") + y = helper.make_tensor_value_info('y', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable y") + z = helper.make_tensor_value_info('z', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable z") + mask = helper.make_tensor_value_info('mask', onnx.TensorProto.BOOL, [2, 2], + doc_string="This is a description of variable mask") + + nodes = [ + onnx.helper.make_node( + op_type='Dropout', + inputs=['x'], + outputs=['y'], + ratio=.1 + ), + onnx.helper.make_node( + op_type='Dropout', + inputs=['y'], + outputs=['z', 'mask'], + ratio=.1 + ) + ] + + graph = helper.make_graph( + nodes=nodes, + name="Simple dropout graph", + inputs=[x], + outputs=[z, mask], + value_info=[x, y, z], + doc_string="This graph tests a simple dropout layer call" + ) + + save_graph(graph, "simple_dropout_layer.onnx") + + +def generate_simple_conv(): + x_init_valinfo = helper.make_tensor_value_info("x", onnx.TensorProto.FLOAT, [1, 1, 5, 5], + doc_string="A multi dimensional input value") + W_init_valinfo = helper.make_tensor_value_info("W", onnx.TensorProto.FLOAT, [1, 1, 3, 3], + doc_string="A multi dimensional input value") + y_valinfo = helper.make_tensor_value_info("y", onnx.TensorProto.FLOAT, [1, 1, 5, 5], doc_string="Y output") + + # Convolution with padding + node = onnx.helper.make_node( + op_type='Conv', + inputs=['x', 'W'], + outputs=['y'], + kernel_shape=[3, 3], + # Default values for other attributes: strides=[1, 1], dilations=[1, 1], groups=1 + pads=[1, 1, 1, 1], + ) + + nodes = [node] + graph = helper.make_graph( + nodes=nodes, + name="A simple convolution graph", + inputs=[x_init_valinfo, W_init_valinfo], + outputs=[y_valinfo], + value_info=[x_init_valinfo, W_init_valinfo] + ) + + save_graph(graph, "simple_conv_layer.onnx") + + +def generate_simple_conv_2(): + x_init_valinfo = helper.make_tensor_value_info("x", onnx.TensorProto.FLOAT, [1, 1, 7, 5], + doc_string="A multi dimensional input value") + W_init_valinfo = helper.make_tensor_value_info("W", onnx.TensorProto.FLOAT, [1, 1, 3, 3], + doc_string="A multi dimensional input value") + + y_0_valinfo = helper.make_tensor_value_info("y_0", onnx.TensorProto.FLOAT, [1, 1, 4, 3], doc_string="Y0 output") + + # Convolution with strides=2 and padding + node_with_padding = onnx.helper.make_node( + op_type='Conv', + inputs=['x', 'W'], + outputs=['y_0'], + kernel_shape=[3, 3], + pads=[1, 1, 1, 1], + strides=[2, 2], # Default values for other attributes: dilations=[1, 1], groups=1 + ) + + y_1_valinfo = helper.make_tensor_value_info("y_1", onnx.TensorProto.FLOAT, [1, 1, 3, 2], doc_string="Y1 output") + + # Convolution with strides=2 and no padding + node_without_padding = onnx.helper.make_node( + op_type='Conv', + inputs=['x', 'W'], + outputs=['y_1'], + kernel_shape=[3, 3], + pads=[0, 0, 0, 0], + strides=[2, 2], # Default values for other attributes: dilations=[1, 1], groups=1 + ) + + y_2_valinfo = helper.make_tensor_value_info("y_2", onnx.TensorProto.FLOAT, [1, 1, 4, 2], doc_string="Y2 output") + # Convolution with strides=2 and padding only along one dimension (the H dimension in NxCxHxW tensor) + node_with_asymmetric_padding = onnx.helper.make_node( + op_type='Conv', + inputs=['x', 'W'], + outputs=['y_2'], + kernel_shape=[3, 3], + pads=[1, 0, 1, 0], + strides=[2, 2], # Default values for other attributes: dilations=[1, 1], groups=1 + ) + + nodes = [node_with_padding, node_without_padding, node_with_asymmetric_padding] + graph = helper.make_graph( + nodes=nodes, + name="A simple convolution graph", + inputs=[x_init_valinfo, W_init_valinfo], + outputs=[y_0_valinfo, y_1_valinfo, y_2_valinfo], + value_info=[x_init_valinfo, W_init_valinfo] + ) + + save_graph(graph, "simple_conv_layer_2.onnx") + + +def generate_simple_maxpool(): + x_init_valinfo = helper.make_tensor_value_info("x", onnx.TensorProto.FLOAT, [1, 1, 5, 5], + doc_string="A multi dimensional input value") + + y_valinfo = helper.make_tensor_value_info("y", onnx.TensorProto.FLOAT, [1, 1, 5, 5], doc_string="Y output") + + node = onnx.helper.make_node( + op_type='MaxPool', + inputs=['x'], + outputs=['y'], + kernel_shape=[5, 5], + pads=[2, 2, 2, 2] + ) + nodes = [node] + graph = helper.make_graph( + nodes=nodes, + name="A simple maxpool graph", + inputs=[x_init_valinfo], + outputs=[y_valinfo], + value_info=[x_init_valinfo] + ) + + save_graph(graph, "simple_maxpool_layer.onnx") + + +def generate_simple_if(): + A = helper.make_tensor_value_info('A', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable A") + E = helper.make_tensor_value_info('E', onnx.TensorProto.FLOAT, [2, 2], + doc_string="This is a description of variable E") + condition = helper.make_tensor_value_info('cond', onnx.TensorProto.BOOL, [1], doc_string="Condition for the if") + + else_node = helper.make_node("Relu", ["A"], ["E"], doc_string="Call of Relu function in else branch") + else_graph = helper.make_graph( + nodes=[else_node], + name="The Else branch graph", + inputs=[A], + outputs=[E], + value_info=[A] + ) + + then_node = helper.make_node("Tanh", ["A"], ["E"], doc_string="Call of Tanh function in then branch") + then_graph = helper.make_graph( + nodes=[then_node], + name="The Then branch graph", + inputs=[A], + outputs=[E], + value_info=[A] + ) + + node = helper.make_node( + op_type="If", + name="A simple if graph", + inputs=["cond"], + outputs=["E"], + else_branch=else_graph, + then_branch=then_graph + ) + + graph = helper.make_graph( + nodes=[node], + name="A simple if graph", + inputs=[A, condition], + outputs=[E] + ) + + save_graph(graph, "simple_if_graph.onnx") + + +if __name__ == '__main__': + generate_simple_add_graph() + # generate_simple_boolean_noshape() + generate_simple_mat_add_mul_sub_graph() + generate_simple_initialized_graph() + generate_simple_relu_tanh_sigmoid_softmax() + generate_simple_dropout_layer() + generate_simple_conv() + generate_simple_conv_2() + generate_simple_maxpool() + generate_simple_if() diff --git a/src/main/python/tests/onnx/test_simple.py b/src/main/python/tests/onnx/test_simple.py new file mode 100644 index 0000000..bb7d822 --- /dev/null +++ b/src/main/python/tests/onnx/test_simple.py @@ -0,0 +1,65 @@ +# 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 unittest +import tests.onnx.util as util + + +class TestSimpleOperators(unittest.TestCase): + def test_simple_mat_add(self): + name = "simple_mat_add" + util.run_and_compare_output(name, self) + + def test_simple_mat_add_mul_sub(self): + name = "simple_mat_add_mul_sub" + util.run_and_compare_output(name, self) + + def test_simple_mat_initialized(self): + name = "simple_mat_initialized" + util.run_and_compare_output(name, self) + + def test_simple_relu_tanh_sigmoid_softmax(self): + name = "simple_relu_tanh_sigmoid_softmax" + util.run_and_compare_output(name, self) + + def test_simple_conv2d_layer(self): + name = "simple_conv_layer" + util.run_and_compare_output(name, self) + + def test_simple_conv2d_layer_2(self): + name = "simple_conv_layer_2" + util.run_and_compare_output(name, self) + + def test_simple_maxpool_layer(self): + name = "simple_maxpool_layer" + util.run_and_compare_output(name, self) + + def test_simple_if_graph(self): + name = "simple_if_graph" + util.run_and_compare_output(name, self) + + # TODO: dml implementation of dropout does not work + # def test_simple_dropout_layer(self): + # name = "simple_dropout_layer" + # test_util.run_and_compare_output(name, self) + + # TODO: dml does not support boolean matrices? + # def test_simple_bool_and_or_xor_noshape(self): + # name = "simple_bool_and_or_xor_noshape" + # test_util.run_and_compare_output(name, self) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/main/python/tests/onnx/util.py b/src/main/python/tests/onnx/util.py new file mode 100644 index 0000000..1ee82e6 --- /dev/null +++ b/src/main/python/tests/onnx/util.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess +import unittest + +from systemds.onnx_systemds.convert import onnx2systemds +from systemds.onnx_systemds.util import resolve_systemds_root + + +def invoke_systemds(input_file: str, args: [str] = None) -> int: + """ + Runs systemds by running the script in $SYSTEMDS_ROOT_PATH/bin/systemds with the provided input_file, + will fail if environment variable SYSTEMDS_ROOT_PATH is not set. + + :param input_file: the dml script to run + :param args: additional arguments if needed + :return: the return-code of systemds + """ + if args is None: + args = [] + + systemds_root_path = resolve_systemds_root() + + try: + realpath_input = os.path.relpath(input_file, os.getcwd()) + res = subprocess.run([systemds_root_path + "/bin/systemds", realpath_input] + args, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=10000) + except subprocess.CalledProcessError as systemds_error: + print("SYSTEMDS FAILED!") + print("error code: " + str(systemds_error.returncode)) + print("Stdout:") + print(systemds_error.output.decode("utf-8")) + print("Stderr:") + print(systemds_error.stderr.decode("utf-8")) + return systemds_error.returncode + + stderr = res.stderr.decode("utf-8") + if len(stderr) != 0: + print("No exception but stderr was not empty:") + print(stderr) + + return res.returncode + + +def run_and_compare_output(name: str, test_case: unittest.TestCase) -> None: + """ + Converts the onnx-model to dml, runs systemds with the dml-wrapper and compares the resulting output + with the reference output. + :param name: The name of the test-case (also used for finding onnx-model, dml-wrapper and reference output) + :param test_case: The testcase + """ + onnx2systemds("tests/onnx/test_models/" + name + ".onnx", "tests/onnx/dml_output/" + name + ".dml") + ret = invoke_systemds("tests/onnx/dml_wrapper/" + name + "_wrapper.dml") + test_case.assertEqual(ret, 0, "systemds failed") + + # We read the file content such that pytest can present the actual difference between the files + with open("tests/onnx/output_reference/" + name + "_reference.out") as reference_file: + reference_content = reference_file.read() + + with open("tests/onnx/output_test/" + name + ".out") as output_file: + test_content = output_file.read() + + test_case.assertEqual( + test_content, + reference_content, + "generated output differed from reference output" + )