diff --git a/.github/workflows/check-urls.yml b/.github/workflows/check-urls.yml index 67d7731..d56adba 100644 --- a/.github/workflows/check-urls.yml +++ b/.github/workflows/check-urls.yml @@ -42,6 +42,6 @@ jobs: print_all: false timeout: 2 retry_count# : 2 - exclude_urls: https://hal.archives-ouvertes.fr/hal-00990252/document - exclude_patterns: https://www.data.gouv.fr/fr/datasets/r/e3d83ab3-dc52-4c99-abaf-8a38050cc68c,https://dev.azure.com/ + exclude_urls: https://hal.archives-ouvertes.fr/hal-00990252/document,https://github.com/onnx/tensorflow-onnx + exclude_patterns: https://www.data.gouv.fr/fr/datasets/r/e3d83ab3-dc52-4c99-abaf-8a38050cc68c,https://dev.azure.com/,https://github.com/onnx/tensorflow-onnx # force_pass : true diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ba80296..3ad7c7c 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.12' - uses: tlylt/install-graphviz@v1 @@ -35,7 +35,7 @@ jobs: run: python -m pip install -r requirements-dev.txt - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} @@ -57,7 +57,7 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Install - run: python setup.py install + run: python -m pip install -e . -v - name: Copy license, changelogs run: | @@ -83,6 +83,6 @@ jobs: exit 1 fi - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: path: ./dist/html/** diff --git a/.github/workflows/wheels-any.yml b/.github/workflows/wheels-any.yml index c20a15d..4bf89c7 100644 --- a/.github/workflows/wheels-any.yml +++ b/.github/workflows/wheels-any.yml @@ -19,11 +19,11 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.12' - name: build wheel run: python -m pip wheel . - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: path: ./onnx_array_api*.whl diff --git a/CHANGELOGS.rst b/CHANGELOGS.rst index f6feee7..8a91bbe 100644 --- a/CHANGELOGS.rst +++ b/CHANGELOGS.rst @@ -1,10 +1,25 @@ Change Logs =========== -0.2.0 +0.3.1 ++++++ + +* :pr:`100`: updates requirements, add 3.12 +* :pr:`96`: supports local functions in translator +* :pr:`95`: improves translation to GraphBuilder + +0.3.0 +++++ +* :pr:`93`: fixes evaluator type in ``compare_onnx_execution`` +* :pr:`92`: avoids recursion errors in profiling +* :pr:`87`: adds command line to replace contant by ConstantOfShape +* :pr:`79`: first draft to export to GraphBuilder * :pr:`77`: supports ConcatOfShape and Slice with the light API + +0.2.0 ++++++ + * :pr:`76`, :pr:`79`: add a mode to compare models without execution * :pr:`75`: add QuickGelu to ExtendedReferenceEvaluator * :pr:`71`: adds tools to compare two onnx graphs diff --git a/LICENSE.txt b/LICENSE.txt index e027853..1a46a8e 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2023-2024, Xavier Dupré +Copyright (c) 2023-2025, Xavier Dupré Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/_doc/api/tools.rst b/_doc/api/tools.rst index ef161e0..e0450dc 100644 --- a/_doc/api/tools.rst +++ b/_doc/api/tools.rst @@ -6,6 +6,11 @@ Benchmark .. autofunction:: onnx_array_api.ext_test_case.measure_time +Manipulations ++++++++++++++ + +.. autofunction:: onnx_array_api.tools.replace_constants.replace_initializer_by_constant_of_shape + Examples ++++++++ diff --git a/_doc/api/translate_api.rst b/_doc/api/translate_api.rst index b554538..f2d90df 100644 --- a/_doc/api/translate_api.rst +++ b/_doc/api/translate_api.rst @@ -39,6 +39,12 @@ InnerEmitter .. autoclass:: onnx_array_api.translate_api.inner_emitter.InnerEmitter :members: +InnerEmitterShortInitializer +++++++++++++++++++++++++++++ + +.. autoclass:: onnx_array_api.translate_api.inner_emitter.InnerEmitterShortInitializer + :members: + LightEmitter ++++++++++++ diff --git a/_doc/conf.py b/_doc/conf.py index 3c7a1ad..eaf8eb1 100644 --- a/_doc/conf.py +++ b/_doc/conf.py @@ -121,7 +121,7 @@ "inner API": "https://onnx.ai/onnx/intro/python.html", "JIT": "https://en.wikipedia.org/wiki/Just-in-time_compilation", "onnx": "https://onnx.ai/onnx/", - "onnx-graphsurgeon": "https://docs.nvidia.com/deeplearning/tensorrt/onnx-graphsurgeon/docs/index.html", + "onnx-graphsurgeon": "https://github.com/NVIDIA/TensorRT/tree/main/tools/onnx-graphsurgeon", "onnx.helper": "https://onnx.ai/onnx/api/helper.html", "ONNX": "https://onnx.ai/", "ONNX Operators": "https://onnx.ai/onnx/operators/", @@ -146,11 +146,9 @@ "torch.onnx": "https://pytorch.org/docs/stable/onnx.html", # "C_OrtValue": ( - "http://www.xavierdupre.fr/app/onnxcustom/helpsphinx/" - "api/onnxruntime_python/ortvalue.html#c-class-ortvalue-or-c-ortvalue" + "https://onnxruntime.ai/docs/api/csharp/api/Microsoft.ML.OnnxRuntime.OrtValue.html" ), "OrtValue": ( - "http://www.xavierdupre.fr/app/onnxcustom/helpsphinx/" - "api/onnxruntime_python/ortvalue.html#onnxruntime.OrtValue" + "https://onnxruntime.ai/docs/api/python/api_summary.html#onnxruntime.OrtValue" ), } diff --git a/_doc/examples/plot_benchmark_rf.py b/_doc/examples/plot_benchmark_rf.py index 423669c..c1ce486 100644 --- a/_doc/examples/plot_benchmark_rf.py +++ b/_doc/examples/plot_benchmark_rf.py @@ -22,8 +22,6 @@ import numpy import pandas from lightgbm import LGBMRegressor -from onnxmltools.convert.lightgbm.operator_converters.LightGbm import convert_lightgbm -from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost from onnxruntime import InferenceSession, SessionOptions from psutil import cpu_count from sphinx_runpython.runpython import run_cmd @@ -33,14 +31,16 @@ from sklearn.ensemble import RandomForestRegressor from tqdm import tqdm from xgboost import XGBRegressor +from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost def skl2onnx_convert_lightgbm(scope, operator, container): + from onnxmltools.convert.lightgbm.operator_converters.LightGbm import ( + convert_lightgbm, + ) + options = scope.get_options(operator.raw_operator) - if "split" in options: - operator.split = options["split"] - else: - operator.split = None + operator.split = options.get("split", None) convert_lightgbm(scope, operator, container) @@ -100,7 +100,7 @@ def measure_inference(fct, X, repeat, max_time=5, quantile=1): :return: number of runs, sum of the time, average, median """ times = [] - for n in range(repeat): + for _n in range(repeat): perf = time.perf_counter() fct(X) delta = time.perf_counter() - perf @@ -238,7 +238,10 @@ def measure_inference(fct, X, repeat, max_time=5, quantile=1): # onnxruntime bar.set_description(f"J={n_j} E={n_estimators} D={max_depth} predictO") r, t, mean, med = measure_inference( - lambda x: sess.run(None, {"X": x}), X, repeat=repeat, max_time=max_time + lambda x, sess=sess: sess.run(None, {"X": x}), + X, + repeat=repeat, + max_time=max_time, ) o2 = obs.copy() o2.update(dict(avg=mean, med=med, n_runs=r, ttime=t, name="ort_")) diff --git a/_doc/examples/plot_onnxruntime.py b/_doc/examples/plot_onnxruntime.py index fcace3e..0aba6ac 100644 --- a/_doc/examples/plot_onnxruntime.py +++ b/_doc/examples/plot_onnxruntime.py @@ -87,14 +87,14 @@ def loop(n=1000): x = np.random.randn(n, 2).astype(np.float32) y = np.random.randn(n, 2).astype(np.float32) - obs = measure_time(lambda: myloss(x, y)) + obs = measure_time(lambda x=x, y=y: myloss(x, y)) obs["name"] = "numpy" obs["n"] = n data.append(obs) xort = OrtTensor.from_array(x) yort = OrtTensor.from_array(y) - obs = measure_time(lambda: ort_myloss(xort, yort)) + obs = measure_time(lambda xort=xort, yort=yort: ort_myloss(xort, yort)) obs["name"] = "ort" obs["n"] = n data.append(obs) diff --git a/_doc/index.rst b/_doc/index.rst index f9a07e5..9bdc4e2 100644 --- a/_doc/index.rst +++ b/_doc/index.rst @@ -187,5 +187,7 @@ to know onnx for that. See :ref:`l-numpy-api-onnx`. Older versions ++++++++++++++ +* `0.3.0 <../v0.3.0/index.html>`_ +* `0.2.0 <../v0.2.0/index.html>`_ * `0.1.3 <../v0.1.3/index.html>`_ * `0.1.2 <../v0.1.2/index.html>`_ diff --git a/_doc/long_outputs.rst b/_doc/long_outputs.rst index 64c0b84..745382b 100644 --- a/_doc/long_outputs.rst +++ b/_doc/long_outputs.rst @@ -4,9 +4,6 @@ Long outputs uneasy to see ========================== -.. contents:: - :local: - onnx ==== diff --git a/_unittests/onnx-numpy-skips.txt b/_unittests/onnx-numpy-skips.txt index 1d46bbb..5deb50e 100644 --- a/_unittests/onnx-numpy-skips.txt +++ b/_unittests/onnx-numpy-skips.txt @@ -6,6 +6,12 @@ array_api_tests/test_creation_functions.py::test_asarray_arrays array_api_tests/test_creation_functions.py::test_empty array_api_tests/test_creation_functions.py::test_empty_like array_api_tests/test_creation_functions.py::test_eye +array_api_tests/test_creation_functions.py::test_full +array_api_tests/test_creation_functions.py::test_full_like +array_api_tests/test_creation_functions.py::test_ones +array_api_tests/test_creation_functions.py::test_ones_like +array_api_tests/test_creation_functions.py::test_zeros +array_api_tests/test_creation_functions.py::test_zeros_like # fails to precision issue array_api_tests/test_creation_functions.py::test_linspace array_api_tests/test_creation_functions.py::test_meshgrid diff --git a/_unittests/ut_array_api/test_hypothesis_array_api.py b/_unittests/ut_array_api/test_hypothesis_array_api.py index 95b1447..f55d230 100644 --- a/_unittests/ut_array_api/test_hypothesis_array_api.py +++ b/_unittests/ut_array_api/test_hypothesis_array_api.py @@ -1,7 +1,7 @@ import unittest -import warnings from os import getenv from functools import reduce +import packaging.version as pv import numpy as np from operator import mul from hypothesis import given @@ -44,9 +44,7 @@ class TestHypothesisArraysApis(ExtTestCase): @classmethod def setUpClass(cls): - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from numpy import array_api as xp + import array_api_strict as xp api_version = getenv( "ARRAY_API_TESTS_VERSION", @@ -63,6 +61,9 @@ def test_strategies(self): self.assertNotEmpty(self.xps) self.assertNotEmpty(self.onxps) + @unittest.skipIf( + pv.Version(np.__version__) >= pv.Version("2.0"), reason="abandonned" + ) def test_scalar_strategies(self): dtypes = dict( integer_dtypes=self.xps.integer_dtypes(), @@ -139,6 +140,9 @@ def fctonx(x, kw): fctonx() self.assertEqual(len(args_onxp), len(args_np)) + @unittest.skipIf( + pv.Version(np.__version__) >= pv.Version("2.0"), reason="abandonned" + ) def test_square_sizes_strategies(self): dtypes = dict( integer_dtypes=self.xps.integer_dtypes(), diff --git a/_unittests/ut_graph_api/test_graph_builder.py b/_unittests/ut_graph_api/test_graph_builder.py index 33c3155..9e6229b 100644 --- a/_unittests/ut_graph_api/test_graph_builder.py +++ b/_unittests/ut_graph_api/test_graph_builder.py @@ -3,7 +3,7 @@ import unittest import numpy as np import onnx -from onnx_array_api.ext_test_case import ExtTestCase +from onnx_array_api.ext_test_case import ExtTestCase, skipif_ci_apple from onnx_array_api.graph_api.graph_builder import GraphBuilder, OptimizationOptions from onnx_array_api.reference import ( from_array_extended, @@ -107,6 +107,7 @@ def test_simple_big(self): got = ref.run(None, feeds) self.assertEqualArray(expected, got[0]) + @skipif_ci_apple("libomp is missing") def test_constant_folding(self): with contextlib.redirect_stdout(io.StringIO()): g = GraphBuilder(verbose=10) @@ -133,6 +134,7 @@ def test_constant_folding(self): got = ref.run(None, feeds) self.assertEqualArray(expected, got[0]) + @skipif_ci_apple("libomp is missing") def test_constant_folding2(self): g = GraphBuilder( optimization_options=OptimizationOptions(constant_folding=True) @@ -270,6 +272,7 @@ def test_remove_unused_nodes_simple(self): got = ref.run(None, feeds) self.assertEqualArray(expected, got[0]) + @skipif_ci_apple("libomp is missing") def test_constant_array(self): with contextlib.redirect_stdout(io.StringIO()): g = GraphBuilder(verbose=10) @@ -290,6 +293,7 @@ def test_constant_array(self): got = ref.run(None, feeds) self.assertEqualArray(expected, got[0]) + @skipif_ci_apple("libomp is missing") def test_constant_array_2(self): with contextlib.redirect_stdout(io.StringIO()): g = GraphBuilder(verbose=10) diff --git a/_unittests/ut_light_api/test_backend_export.py b/_unittests/ut_light_api/test_backend_export.py index 42ac7f5..91f4dd4 100644 --- a/_unittests/ut_light_api/test_backend_export.py +++ b/_unittests/ut_light_api/test_backend_export.py @@ -5,6 +5,7 @@ import packaging.version as pv import numpy from numpy.testing import assert_allclose +from onnx.defs import onnx_opset_version import onnx.backend.base import onnx.backend.test import onnx.shape_inference @@ -31,7 +32,6 @@ class ReferenceImplementationError(RuntimeError): "Fails, export cannot be compared." - pass class ExportWrapper: @@ -64,7 +64,8 @@ def run( expected = self.expected_sess.run(names, feeds) except (RuntimeError, AssertionError, TypeError, KeyError) as e: raise ReferenceImplementationError( - f"ReferenceImplementation fails with {onnx_simple_text_plot(self.model)}" + f"ReferenceImplementation fails with " + f"{onnx_simple_text_plot(self.model)}" f"\n--RAW--\n{self.model}" ) from e @@ -85,7 +86,7 @@ def run( new_code = "\n".join( [f"{i+1:04} {line}" for i, line in enumerate(code.split("\n"))] ) - raise AssertionError(f"ERROR {e}\n{new_code}") + raise AssertionError(f"ERROR {e}\n{new_code}") # noqa: B904 locs = { "np": numpy, @@ -154,7 +155,8 @@ def run( ): if a.tolist() != b.tolist(): raise AssertionError( - f"Text discrepancies for api {api!r} with a.dtype={a.dtype} " + f"Text discrepancies for api {api!r} " + f"with a.dtype={a.dtype} " f"and b.dtype={b.dtype}" f"\n--BASE--\n{onnx_simple_text_plot(self.model)}" f"\n--EXP[{api}]--\n{onnx_simple_text_plot(export_model)}" @@ -242,7 +244,7 @@ def run_node(cls, node, inputs, device=None, outputs_info=None, **kwargs): # The following tests are too slow with the reference implementation (Conv). backend_test.exclude( - "(FLOAT8|BFLOAT16|_opt_|_3d_|_momentum_|_4d_" + "(FLOAT8|BFLOAT16|INT4|_opt_|_3d_|_momentum_|_4d_|int4" "|test_adagrad" "|test_adam" "|test_ai_onnx_ml_" @@ -270,9 +272,27 @@ def run_node(cls, node, inputs, device=None, outputs_info=None, **kwargs): "|test_squeezenet" "|test_vgg19" "|test_zfnet512" + "|test_range_float_type_positive_delta_expanded" + "|test_range_int32_type_negative_delta_expanded" ")" ) +if onnx_opset_version() < 22: + backend_test.exclude( + "(" + "test_dft_inverse_cpu" + "|test_dft_inverse_opset19_cpu" + "|test_lppool_1d_default_cpu" + "|test_lppool_2d_default_cpu" + "|test_lppool_2d_dilations_cpu" + "|test_lppool_2d_pads_cpu" + "|test_lppool_2d_same_lower_cpu" + "|test_lppool_2d_same_upper_cpu" + "|test_lppool_2d_strides_cpu" + "|test_lppool_3d_default_cpu" + ")" + ) + if pv.Version(onnx_version) < pv.Version("1.16.0"): backend_test.exclude("(test_strnorm|test_range_)") diff --git a/_unittests/ut_light_api/test_light_api.py b/_unittests/ut_light_api/test_light_api.py index e14896a..f936cc1 100644 --- a/_unittests/ut_light_api/test_light_api.py +++ b/_unittests/ut_light_api/test_light_api.py @@ -484,7 +484,7 @@ def g(self): def ah(self): return True - setattr(A, "h", ah) + setattr(A, "h", ah) # noqa: B010 self.assertTrue(A().h()) self.assertIn("(self)", str(inspect.signature(A.h))) diff --git a/_unittests/ut_npx/test_npx.py b/_unittests/ut_npx/test_npx.py index 50e319a..873665d 100644 --- a/_unittests/ut_npx/test_npx.py +++ b/_unittests/ut_npx/test_npx.py @@ -208,7 +208,7 @@ def local1( return x def local2( - x: TensorType[ElemType.floats, "T"] + x: TensorType[ElemType.floats, "T"], ) -> TensorType[ElemType.floats, "T"]: return x diff --git a/_unittests/ut_npx/test_sklearn_array_api.py b/_unittests/ut_npx/test_sklearn_array_api.py index 083c009..9c0d56f 100644 --- a/_unittests/ut_npx/test_sklearn_array_api.py +++ b/_unittests/ut_npx/test_sklearn_array_api.py @@ -17,6 +17,7 @@ class TestSklearnArrayAPI(ExtTestCase): reason="reshape ArrayAPI not followed", ) @ignore_warnings(DeprecationWarning) + @unittest.skip("not maintained") def test_sklearn_array_api_linear_discriminant(self): X = np.array( [[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]], dtype=np.float64 @@ -39,6 +40,7 @@ def test_sklearn_array_api_linear_discriminant(self): reason="reshape ArrayAPI not followed", ) @ignore_warnings(DeprecationWarning) + @unittest.skip("not maintained") def test_sklearn_array_api_linear_discriminant_float32(self): X = np.array( [[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]], dtype=np.float32 diff --git a/_unittests/ut_ort/test_ort_profile.py b/_unittests/ut_ort/test_ort_profile.py index e868860..6e139cb 100644 --- a/_unittests/ut_ort/test_ort_profile.py +++ b/_unittests/ut_ort/test_ort_profile.py @@ -57,8 +57,6 @@ def myloss(x, y): prof = ort_profile(optimized, feeds) events = { "kernel_time", - "fence_before", - "fence_after", "SequentialExecutor::Execute", "model_run", "model_loading_array", diff --git a/_unittests/ut_ort/test_sklearn_array_api_ort.py b/_unittests/ut_ort/test_sklearn_array_api_ort.py index 296a9b0..f50fce1 100644 --- a/_unittests/ut_ort/test_sklearn_array_api_ort.py +++ b/_unittests/ut_ort/test_sklearn_array_api_ort.py @@ -17,6 +17,7 @@ class TestSklearnArrayAPIOrt(ExtTestCase): reason="reshape ArrayAPI not followed", ) @skipif_ci_windows("Unstable on Windows.") + @unittest.skip("discontinued") def test_sklearn_array_api_linear_discriminant_ort(self): X = np.array( [[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]], dtype=np.float64 @@ -40,6 +41,7 @@ def test_sklearn_array_api_linear_discriminant_ort(self): reason="reshape ArrayAPI not followed", ) @skipif_ci_windows("Unstable on Windows.") + @unittest.skip("discontinued") def test_sklearn_array_api_linear_discriminant_ort_float32(self): X = np.array( [[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]], dtype=np.float32 diff --git a/_unittests/ut_plotting/test_dot_plot.py b/_unittests/ut_plotting/test_dot_plot.py index 5c03746..4c8c4dd 100644 --- a/_unittests/ut_plotting/test_dot_plot.py +++ b/_unittests/ut_plotting/test_dot_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import unittest diff --git a/_unittests/ut_plotting/test_text_plot.py b/_unittests/ut_plotting/test_text_plot.py index 963b5cb..5844ff0 100644 --- a/_unittests/ut_plotting/test_text_plot.py +++ b/_unittests/ut_plotting/test_text_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import textwrap import unittest @@ -95,6 +94,7 @@ def test_onnx_text_plot_tree_cls_2(self): +f 0:1 1:0 2:0 """ ).strip(" \n\r") + res = res.replace("np.float32(", "").replace(")", "") self.assertEqual(expected, res.strip(" \n\r")) @ignore_warnings((UserWarning, FutureWarning)) diff --git a/_unittests/ut_reference/test_backend_extended_reference_evaluator.py b/_unittests/ut_reference/test_backend_extended_reference_evaluator.py index b35fb3c..fbf12b7 100644 --- a/_unittests/ut_reference/test_backend_extended_reference_evaluator.py +++ b/_unittests/ut_reference/test_backend_extended_reference_evaluator.py @@ -149,7 +149,7 @@ def run_node(cls, node, inputs, device=None, outputs_info=None, **kwargs): "|test_scan_sum)" ) -if onnx_opset_version() < 21: +if onnx_opset_version() < 200: # The following tests are using types not supported by NumPy. # They could be if method to_array is extended to support custom # types the same as the reference implementation does @@ -164,8 +164,10 @@ def run_node(cls, node, inputs, device=None, outputs_info=None, **kwargs): "|test_cast_no_saturate_" "|_to_FLOAT8" "|_FLOAT8" + "|INT4" "|test_quantizelinear_e4m3fn" "|test_quantizelinear_e5m2" + "|test_scatter_with" ")" ) @@ -215,6 +217,25 @@ def run_node(cls, node, inputs, device=None, outputs_info=None, **kwargs): # The following tests fail due to a type mismatch. backend_test.exclude("(test_eyelike_without_dtype)") +if onnx_opset_version() < 22: + backend_test.exclude( + "(" + "test_adagrad_cpu" + "|test_adagrad_multiple_cpu" + "|test_dft_inverse_cpu" + "|test_dft_inverse_opset19_cpu" + "|test_lppool_1d_default_cpu" + "|test_lppool_2d_default_cpu" + "|test_lppool_2d_dilations_cpu" + "|test_lppool_2d_pads_cpu" + "|test_lppool_2d_same_lower_cpu" + "|test_lppool_2d_same_upper_cpu" + "|test_lppool_2d_strides_cpu" + "|test_lppool_3d_default_cpu" + ")" + ) + + # The following tests fail due to discrepancies (small but still higher than 1e-7). backend_test.exclude("test_adam_multiple") # 1e-2 diff --git a/_unittests/ut_tools/test_replace_constants.py b/_unittests/ut_tools/test_replace_constants.py new file mode 100644 index 0000000..5cad1c2 --- /dev/null +++ b/_unittests/ut_tools/test_replace_constants.py @@ -0,0 +1,160 @@ +import unittest +import numpy as np +import onnx +import onnx.helper as oh +import onnx.numpy_helper as onh +from onnx import TensorProto +from onnx_array_api.ext_test_case import ExtTestCase +from onnx_array_api.reference import ( + ExtendedReferenceEvaluator as ReferenceEvaluator, +) +from onnx_array_api.tools.replace_constants import ( + replace_initializer_by_constant_of_shape, +) + + +class TestReplaceConstants(ExtTestCase): + + def test_replace_initializer(self): + dtype = np.float32 + value = np.random.randn(2, 100).astype(dtype) + A = onh.from_array(value, name="A") + value = np.array([1], dtype=dtype) + C = onh.from_array(value, name="C") + + X = oh.make_tensor_value_info("X", TensorProto.FLOAT, [None, None]) + Y = oh.make_tensor_value_info("Y", TensorProto.FLOAT, [None]) + node1 = oh.make_node("MatMul", ["X", "A"], ["AX"]) + node2 = oh.make_node("Sub", ["AX", "C"], ["Y"]) + graph = oh.make_graph([node1, node2], "lr", [X], [Y], [A, C]) + model_def = oh.make_model(graph) + + x = np.array([1, 2, 4, 5, 5, 4]).astype(np.float32).reshape((3, 2)) + oinf1 = ReferenceEvaluator(model_def) + y1 = oinf1.run(None, {"X": x})[0] # type: ignore[index] + repl = replace_initializer_by_constant_of_shape(model_def) + node_types = {n.op_type for n in repl.graph.node} + self.assertIn("ConstantOfShape", node_types) + oinf2 = ReferenceEvaluator(repl) + y1[:, :] = 3.5 + y1[0, :] = 0.5 + y2 = oinf2.run(None, {"X": x})[0] # type: ignore[index] + self.assertEqualArray(y1, y2) + + def test_replace_constant(self): + dtype = np.float32 + value = np.random.randn(2, 10).astype(dtype) + A = onh.from_array(value, name="A") + value = np.array([1], dtype=dtype) + C = onh.from_array(value, name="C") + + X = oh.make_tensor_value_info("X", TensorProto.FLOAT, [None, None]) + Y = oh.make_tensor_value_info("Y", TensorProto.FLOAT, [None]) + node0 = oh.make_node("Constant", [], ["A"], value=A) + node1 = oh.make_node("MatMul", ["X", "A"], ["AX"]) + node2 = oh.make_node("Sub", ["AX", "C"], ["Y"]) + graph = oh.make_graph([node0, node1, node2], "lr", [X], [Y], [C]) + model_def = oh.make_model(graph) + + x = np.array([1, 2, 4, 5, 5, 4]).astype(np.float32).reshape((3, 2)) + oinf1 = ReferenceEvaluator(model_def) + y1 = oinf1.run(None, {"X": x})[0] # type: ignore[index] + repl = replace_initializer_by_constant_of_shape(model_def, threshold=0) + node_types = {n.op_type for n in repl.graph.node} + self.assertIn("ConstantOfShape", node_types) + oinf2 = ReferenceEvaluator(repl) + y1[:, :] = 4 + y1[0, :] = 1 + y2 = oinf2.run(None, {"X": x})[0] # type: ignore[index] + self.assertEqualArray(y1, y2) + + def test_replace_constant_function(self): + dtype = np.float32 + value = np.random.randn(2, 100).astype(dtype) + A = onh.from_array(value, name="A") + value = np.array([1], dtype=dtype) + C = onh.from_array(value, name="C") + + X = oh.make_tensor_value_info("X", TensorProto.FLOAT, [None, None]) + Y = oh.make_tensor_value_info("Y", TensorProto.FLOAT, [None]) + nodeC = oh.make_node("Constant", [], ["C"], value=C) + node0 = oh.make_node("Constant", [], ["A"], value=A) + node1 = oh.make_node("MatMul", ["X", "A"], ["AX"]) + node2 = oh.make_node("Sub", ["AX", "C"], ["Y"]) + opset_imports = [ + oh.make_opsetid("", onnx.defs.onnx_opset_version()), + oh.make_opsetid("custom", 1), + ] + fct = oh.make_function( + "custom", + "unittest", + ["X"], + ["Y"], + [nodeC, node0, node1, node2], + opset_imports, + ) + + node = oh.make_node("unittest", ["X"], ["Y"], domain="custom") + graph = oh.make_graph([node], "lr", [X], [Y], [C]) + model_def = oh.make_model(graph, functions=[fct], opset_imports=opset_imports) + + x = np.array([1, 2, 4, 5, 5, 4]).astype(np.float32).reshape((3, 2)) + oinf1 = ReferenceEvaluator(model_def) + y1 = oinf1.run(None, {"X": x})[0] # type: ignore[index] + repl = replace_initializer_by_constant_of_shape(model_def) + node_types = {n.op_type for n in repl.functions[0].node} + self.assertIn("ConstantOfShape", node_types) + oinf2 = ReferenceEvaluator(repl) + y1[:, :] = 3.5 + y1[0, :] = 0.5 + y2 = oinf2.run(None, {"X": x})[0] # type: ignore[index] + self.assertEqualArray(y1, y2) + + def test_replace_constant_graph(self): + value = np.array([0], dtype=np.float32) + zero = onh.from_array(value, name="zero") + + X = oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [None, None]) + Y = oh.make_tensor_value_info("Y", onnx.TensorProto.FLOAT, [None]) + + rsum = oh.make_node("ReduceSum", ["X"], ["rsum"]) + cond = oh.make_node("Greater", ["rsum", "zero"], ["cond"]) + + then_out = oh.make_tensor_value_info("then_out", onnx.TensorProto.FLOAT, None) + then_cst = onh.from_array(np.array([1] * 129).astype(np.float32)) + + then_const_node = oh.make_node( + "Constant", inputs=[], outputs=["then_out"], value=then_cst, name="cst1" + ) + then_body = oh.make_graph([then_const_node], "then_body", [], [then_out]) + + else_out = oh.make_tensor_value_info("else_out", onnx.TensorProto.FLOAT, None) + else_cst = onh.from_array(np.array([-1] * 129).astype(np.float32)) + else_const_node = oh.make_node( + "Constant", inputs=[], outputs=["else_out"], value=else_cst, name="cst2" + ) + else_body = oh.make_graph([else_const_node], "else_body", [], [else_out]) + + if_node = oh.make_node( + "If", ["cond"], ["Y"], then_branch=then_body, else_branch=else_body + ) + graph = oh.make_graph([rsum, cond, if_node], "if", [X], [Y], [zero]) + onnx_model = oh.make_model( + graph, opset_imports=[oh.make_opsetid("", onnx.defs.onnx_opset_version())] + ) + self.assertNotIn("ConstantOfShape", str(onnx_model)) + + x = np.ones((3, 2), dtype=np.float32) + oinf1 = ReferenceEvaluator(onnx_model) + y1 = oinf1.run(None, {"X": x})[0] # type: ignore[index] + repl = replace_initializer_by_constant_of_shape(onnx_model) + self.assertIn("ConstantOfShape", str(repl)) + oinf2 = ReferenceEvaluator(repl) + y2 = oinf2.run(None, {"X": x})[0] # type: ignore[index] + y1 = y1.copy() + y1[:] = 0.5 + self.assertEqualArray(y1, y2) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut_translate_api/test_translate.py b/_unittests/ut_translate_api/test_translate.py index d505135..98629d8 100644 --- a/_unittests/ut_translate_api/test_translate.py +++ b/_unittests/ut_translate_api/test_translate.py @@ -160,8 +160,14 @@ def test_export_if(self): self.assertEqualArray(np.array([1], dtype=np.int64), got[0]) code = translate(onx) - selse = "g().cst(np.array([0], dtype=np.int64)).rename('Z').bring('Z').vout(elem_type=TensorProto.FLOAT)" - sthen = "g().cst(np.array([1], dtype=np.int64)).rename('Z').bring('Z').vout(elem_type=TensorProto.FLOAT)" + selse = ( + "g().cst(np.array([0], dtype=np.int64)).rename('Z')." + "bring('Z').vout(elem_type=TensorProto.FLOAT)" + ) + sthen = ( + "g().cst(np.array([1], dtype=np.int64)).rename('Z')." + "bring('Z').vout(elem_type=TensorProto.FLOAT)" + ) expected = dedent( f""" ( @@ -221,5 +227,4 @@ def test_aionnxml(self): if __name__ == "__main__": - TestTranslate().test_export_if() unittest.main(verbosity=2) diff --git a/_unittests/ut_translate_api/test_translate_builder.py b/_unittests/ut_translate_api/test_translate_builder.py new file mode 100644 index 0000000..b1ad394 --- /dev/null +++ b/_unittests/ut_translate_api/test_translate_builder.py @@ -0,0 +1,285 @@ +import unittest +from textwrap import dedent +import numpy as np +import onnx.helper as oh +from onnx import ModelProto, TensorProto +from onnx.checker import check_model +from onnx.defs import onnx_opset_version +from onnx.reference import ReferenceEvaluator +from onnx_array_api.ext_test_case import ExtTestCase +from onnx_array_api.light_api import start +from onnx_array_api.graph_api import GraphBuilder +from onnx_array_api.translate_api import translate, Translater +from onnx_array_api.translate_api.builder_emitter import BuilderEmitter + + +OPSET_API = min(19, onnx_opset_version() - 1) + + +class TestTranslateBuilder(ExtTestCase): + def setUp(self): + self.maxDiff = None + + def test_exp(self): + onx = start(opset=19, ir_version=10).vin("X").Exp().rename("Y").vout().to_onnx() + self.assertIsInstance(onx, ModelProto) + self.assertIn("Exp", str(onx)) + ref = ReferenceEvaluator(onx) + a = np.arange(10).astype(np.float32) + got = ref.run(None, {"X": a})[0] + self.assertEqualArray(np.exp(a), got) + + code = translate(onx, api="builder") + expected = ( + dedent( + """ + def light_api( + op: "GraphBuilder", + X: "FLOAT[]", + ): + Y = op.Exp(X, outputs=['Y']) + op.Identity(Y, outputs=["Y"]) + return Y + + g = GraphBuilder({'': 19}, ir_version=10) + g.make_tensor_input("X", TensorProto.FLOAT, ()) + light_api(g.op, "X") + g.make_tensor_output("Y", TensorProto.FLOAT, ()__SUFFIX__) + model = g.to_onnx() + """ + ) + .strip("\n") + .replace("__SUFFIX__", ", is_dimension=False, indexed=False") + ) + self.assertEqual(expected, code.strip("\n")) + + def light_api( + op: "GraphBuilder", + X: "FLOAT[]", # noqa: F722 + ): + Y = op.Exp(X, outputs=["Y"]) + op.Identity(Y, outputs=["Y"]) + return Y + + g2 = GraphBuilder({"": 19}) + g2.make_tensor_input("X", TensorProto.FLOAT, ("A",)) + light_api(g2.op, "X") + g2.make_tensor_output( + "Y", TensorProto.FLOAT, ("A",), is_dimension=False, indexed=False + ) + onx2 = g2.to_onnx() + + ref = ReferenceEvaluator(onx2) + a = np.arange(10).astype(np.float32) + got = ref.run(None, {"X": a})[0] + self.assertEqualArray(np.exp(a), got) + + def test_zdoc(self): + onx = ( + start(opset=19, ir_version=10) + .vin("X") + .reshape((-1, 1)) + .Transpose(perm=[1, 0]) + .rename("Y") + .vout() + .to_onnx() + ) + code = translate(onx, api="builder") + expected = ( + dedent( + """ + def light_api( + op: "GraphBuilder", + X: "FLOAT[]", + ): + r = np.array([-1, 1], dtype=np.int64) + r0_0 = op.Reshape(X, r, outputs=['r0_0']) + Y = op.Transpose(r0_0, perm=[1, 0], outputs=['Y']) + op.Identity(Y, outputs=["Y"]) + return Y + + g = GraphBuilder({'': 19}, ir_version=10) + g.make_tensor_input("X", TensorProto.FLOAT, ()) + light_api(g.op, "X") + g.make_tensor_output("Y", TensorProto.FLOAT, ()__SUFFIX__) + model = g.to_onnx() + """ + ) + .strip("\n") + .replace("__SUFFIX__", ", is_dimension=False, indexed=False") + ) + self.maxDiff = None + self.assertEqual(expected, code.strip("\n")) + + def light_api( + op: "GraphBuilder", + X: "FLOAT[]", # noqa: F722 + ): + r = np.array([-1, 1], dtype=np.int64) + r0_0 = op.Reshape(X, r) + Y = op.Transpose(r0_0, perm=[1, 0]) + op.Identity(Y, outputs=["Y"]) + return Y + + g = GraphBuilder({"": 21}) + X = g.make_tensor_input("X", TensorProto.FLOAT, ()) + light_api(g.op, X) + g.make_tensor_output("Y", TensorProto.FLOAT, ()) + model = g.to_onnx() + self.assertNotEmpty(model) + check_model(model) + + def test_exp_f(self): + onx = start(opset=19, ir_version=10).vin("X").Exp().rename("Y").vout().to_onnx() + self.assertIsInstance(onx, ModelProto) + self.assertIn("Exp", str(onx)) + ref = ReferenceEvaluator(onx) + a = np.arange(10).astype(np.float32) + got = ref.run(None, {"X": a})[0] + self.assertEqualArray(np.exp(a), got) + + tr = Translater(onx, emitter=BuilderEmitter("mm")) + code = tr.export(as_str=True) + + expected = ( + dedent( + """ + def light_api( + op: "GraphBuilder", + X: "FLOAT[]", + ): + Y = op.Exp(X, outputs=['Y']) + op.Identity(Y, outputs=["Y"]) + return Y + + + def mm() -> "ModelProto": + g = GraphBuilder({'': 19}, ir_version=10) + g.make_tensor_input("X", TensorProto.FLOAT, ()) + light_api(g.op, "X") + g.make_tensor_output("Y", TensorProto.FLOAT, ()__SUFFIX__) + model = g.to_onnx() + return model + + + model = mm() + """ + ) + .strip("\n") + .replace("__SUFFIX__", ", is_dimension=False, indexed=False") + ) + self.assertEqual(expected, code.strip("\n")) + + def light_api( + op: "GraphBuilder", + X: "FLOAT[]", # noqa: F722 + ): + Y = op.Exp(X) + op.Identity(Y, outputs=["Y"]) + return Y + + g2 = GraphBuilder({"": 19}) + g2.make_tensor_input("X", TensorProto.FLOAT, ("A",)) + light_api(g2.op, "X") + g2.make_tensor_output( + "Y", TensorProto.FLOAT, ("A",), is_dimension=False, indexed=False + ) + onx2 = g2.to_onnx() + + ref = ReferenceEvaluator(onx2) + a = np.arange(10).astype(np.float32) + got = ref.run(None, {"X": a})[0] + self.assertEqualArray(np.exp(a), got) + + def test_local_function(self): + new_domain = "custom" + + linear_regression = oh.make_function( + new_domain, + "LinearRegression", + ["x", "a", "b"], + ["y"], + [ + oh.make_node("MatMul", ["x", "a"], ["xa"]), + oh.make_node("Add", ["xa", "b"], ["y"]), + ], + [oh.make_opsetid("", 14)], + [], + ) + + graph = oh.make_graph( + [ + oh.make_node( + "LinearRegression", ["X", "A", "B"], ["Y1"], domain=new_domain + ), + oh.make_node("Abs", ["Y1"], ["Y"]), + ], + "example", + [ + oh.make_tensor_value_info("X", TensorProto.FLOAT, [None, None]), + oh.make_tensor_value_info("A", TensorProto.FLOAT, [None, None]), + oh.make_tensor_value_info("B", TensorProto.FLOAT, [None, None]), + ], + [oh.make_tensor_value_info("Y", TensorProto.FLOAT, None)], + ) + + onnx_model = oh.make_model( + graph, + opset_imports=[oh.make_opsetid("", 14), oh.make_opsetid(new_domain, 1)], + functions=[linear_regression], + ir_version=10, + ) + tr = Translater(onnx_model, emitter=BuilderEmitter("mm")) + code = tr.export(as_str=True) + + expected = ( + dedent( + """ + def example( + op: "GraphBuilder", + X: "FLOAT[, ]", + A: "FLOAT[, ]", + B: "FLOAT[, ]", + ): + Y1 = op.LinearRegression(X, A, B, domain='custom', outputs=['Y1']) + Y = op.Abs(Y1, outputs=['Y']) + op.Identity(Y, outputs=["Y"]) + return Y + + + def make_custom_LinearRegression(g: "GraphBuilder"): + gr = GraphBuilder({'': 14}, as_function=True) + x = gr.make_tensor_input('x') + a = gr.make_tensor_input('a') + b = gr.make_tensor_input('b') + op = gr.op + xa = op.MatMul(x, a, outputs=['xa']) + y = op.Add(xa, b, outputs=['y']) + gr.make_tensor_output(y) + g.add_function(builder=gr) + return gr + + + def mm() -> "ModelProto": + g = GraphBuilder({'': 14, 'custom': 1}, ir_version=10) + g.make_tensor_input("X", TensorProto.FLOAT, ('', '')) + g.make_tensor_input("A", TensorProto.FLOAT, ('', '')) + g.make_tensor_input("B", TensorProto.FLOAT, ('', '')) + example(g.op, "X", "A", "B") + g.make_tensor_output("Y", TensorProto.FLOAT, ()__SUFFIX__) + make_custom_LinearRegression(g) + model = g.to_onnx() + return model + + + model = mm() + """ + ) + .strip("\n") + .replace("__SUFFIX__", ", is_dimension=False, indexed=False") + ) + self.assertEqual(expected, code.strip("\n")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/_unittests/ut_translate_api/test_translate_classic.py b/_unittests/ut_translate_api/test_translate_classic.py index c6cb412..4f65b99 100644 --- a/_unittests/ut_translate_api/test_translate_classic.py +++ b/_unittests/ut_translate_api/test_translate_classic.py @@ -178,6 +178,75 @@ def test_transpose(self): self.maxDiff = None self.assertEqual(expected, code) + def test_transpose_short(self): + onx = ( + start(opset=19) + .vin("X") + .reshape((-1, 1)) + .Transpose(perm=[1, 0]) + .rename("Y") + .vout() + .to_onnx() + ) + self.assertIsInstance(onx, ModelProto) + self.assertIn("Transpose", str(onx)) + ref = ReferenceEvaluator(onx) + a = np.arange(10).astype(np.float32) + got = ref.run(None, {"X": a})[0] + self.assertEqualArray(a.reshape((-1, 1)).T, got) + + code = translate(onx, api="onnx-short") + expected = dedent( + """ + opset_imports = [ + make_opsetid('', 19), + ] + inputs = [] + outputs = [] + nodes = [] + initializers = [] + sparse_initializers = [] + functions = [] + initializers.append( + from_array( + np.array([-1, 1], dtype=np.int64), + name='r' + ) + ) + inputs.append(make_tensor_value_info('X', TensorProto.FLOAT, shape=[])) + nodes.append( + make_node_extended( + 'Reshape', + ['X', 'r'], + ['r0_0'] + ) + ) + nodes.append( + make_node_extended( + 'Transpose', + ['r0_0'], + ['Y'], + perm=[1, 0] + ) + ) + outputs.append(make_tensor_value_info('Y', TensorProto.FLOAT, shape=[])) + graph = make_graph( + nodes, + 'light_api', + inputs, + outputs, + initializers, + sparse_initializer=sparse_initializers, + ) + model = make_model( + graph, + functions=functions, + opset_imports=opset_imports + )""" + ).strip("\n") + self.maxDiff = None + self.assertEqual(expected, code) + def test_topk_reverse(self): onx = ( start(opset=19) @@ -252,7 +321,7 @@ def test_fft(self): new_code = "\n".join( [f"{i+1:04} {line}" for i, line in enumerate(code.split("\n"))] ) - raise AssertionError(f"ERROR {e}\n{new_code}") + raise AssertionError(f"ERROR {e}\n{new_code}") # noqa: B904 def test_aionnxml(self): onx = ( diff --git a/_unittests/ut_validation/test_f8.py b/_unittests/ut_validation/test_f8.py index 80611b5..4c6517f 100644 --- a/_unittests/ut_validation/test_f8.py +++ b/_unittests/ut_validation/test_f8.py @@ -88,7 +88,7 @@ def test_fe5m2_to_float32_paper(self): self.assertEqual(fe5m2_to_float32(int("11111100", 2)), -numpy.inf) def test_fe4m3fn_to_float32_all(self): - for i in range(0, 256): + for i in range(256): a = fe4m3_to_float32_float(i) b = fe4m3_to_float32(i) if numpy.isnan(a): @@ -97,7 +97,7 @@ def test_fe4m3fn_to_float32_all(self): self.assertEqual(a, b) def test_fe4m3fn_to_float32_all_ml_types(self): - for i in range(0, 256): + for i in range(256): a = fe4m3_to_float32_float(i) b = fe4m3_to_float32(i) c = new_cvt_float32_to_e4m3fn(b) @@ -188,7 +188,7 @@ def test_search_float32_into_fe5m2_simple(self): self.assertEqual(b1, b2) def test_search_float32_into_fe4m3fn_equal(self): - values = [(fe4m3_to_float32_float(i), i) for i in range(0, 256)] + values = [(fe4m3_to_float32_float(i), i) for i in range(256)] values.sort() for value, expected in values: @@ -208,7 +208,7 @@ def test_search_float32_into_fe4m3fn_equal(self): self.assertIn(nf, (0, 128)) def test_search_float32_into_fe5m2_equal(self): - values = [(fe5m2_to_float32_float(i), i) for i in range(0, 256)] + values = [(fe5m2_to_float32_float(i), i) for i in range(256)] values.sort() for value, expected in values: @@ -233,7 +233,7 @@ def test_search_float32_into_fe5m2_equal(self): self.assertEqual(fe5m2_to_float32(nf), float(cf)) def test_search_float32_into_fe4m3fn(self): - values = [(fe4m3_to_float32_float(i), i) for i in range(0, 256)] + values = [(fe4m3_to_float32_float(i), i) for i in range(256)] values.sort() obs = [] @@ -308,7 +308,7 @@ def test_search_float32_into_fe4m3fn(self): ) def test_search_float32_into_fe5m2(self): - values = [(fe5m2_to_float32_float(i), i) for i in range(0, 256)] + values = [(fe5m2_to_float32_float(i), i) for i in range(256)] values.sort() obs = [] @@ -651,7 +651,7 @@ def test_search_float32_into_fe5m2fnuz_simple(self): self.assertEqual(expected, got) def test_fe4m3fnuz_to_float32_all(self): - for i in range(0, 256): + for i in range(256): a = fe4m3_to_float32_float(i, uz=True) b = fe4m3_to_float32(i, uz=True) if numpy.isnan(a): @@ -660,7 +660,7 @@ def test_fe4m3fnuz_to_float32_all(self): self.assertEqual(a, b) def test_fe5m2fnuz_to_float32_all(self): - for i in range(0, 256): + for i in range(256): a = fe5m2_to_float32_float(i, fn=True, uz=True) b = fe5m2_to_float32(i, fn=True, uz=True) if numpy.isnan(a): @@ -669,7 +669,7 @@ def test_fe5m2fnuz_to_float32_all(self): self.assertEqual(a, b) def test_search_float32_into_fe4m3fnuz(self): - values = [(fe4m3_to_float32_float(i, uz=True), i) for i in range(0, 256)] + values = [(fe4m3_to_float32_float(i, uz=True), i) for i in range(256)] values.sort() obs = [] @@ -715,9 +715,7 @@ def test_search_float32_into_fe4m3fnuz(self): ) def test_search_float32_into_fe5m2fnuz(self): - values = [ - (fe5m2_to_float32_float(i, fn=True, uz=True), i) for i in range(0, 256) - ] + values = [(fe5m2_to_float32_float(i, fn=True, uz=True), i) for i in range(256)] values.sort() obs = [] @@ -1235,7 +1233,7 @@ def test_nan(self): expected, ) ] - for i in range(0, 23): + for i in range(23): v = 0x7F800000 | (1 << i) f = numpy.uint32(v).view(numpy.float32) values.append((i, v, f, expected)) diff --git a/_unittests/ut_xrun_doc/test_command_lines1.py b/_unittests/ut_xrun_doc/test_command_lines1.py index 02f84bd..0503f55 100644 --- a/_unittests/ut_xrun_doc/test_command_lines1.py +++ b/_unittests/ut_xrun_doc/test_command_lines1.py @@ -16,6 +16,7 @@ get_main_parser, get_parser_compare, get_parser_translate, + get_parser_replace, main, ) @@ -35,6 +36,13 @@ def test_parser_translate(self): text = st.getvalue() self.assertIn("model", text) + def test_parser_replace(self): + st = StringIO() + with redirect_stdout(st): + get_parser_replace().print_help() + text = st.getvalue() + self.assertIn("model", text) + def test_command_translate(self): X = make_tensor_value_info("X", TensorProto.FLOAT, [None, None]) Y = make_tensor_value_info("Y", TensorProto.FLOAT, [5, 6]) diff --git a/_unittests/ut_xrun_doc/test_documentation_examples.py b/_unittests/ut_xrun_doc/test_documentation_examples.py index 12a36ba..6f6a5d1 100644 --- a/_unittests/ut_xrun_doc/test_documentation_examples.py +++ b/_unittests/ut_xrun_doc/test_documentation_examples.py @@ -49,7 +49,7 @@ def run_test(self, fold: str, name: str, verbose=0) -> int: if verbose: print(f"failed: {name!r} due to missing dot.") return 0 - raise AssertionError( + raise AssertionError( # noqa: B904 "Example '{}' (cmd: {} - exec_prefix='{}') " "failed due to\n{}" "".format(name, cmds, sys.exec_prefix, st) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 61587f4..e9b3859 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,7 +24,7 @@ jobs: - script: pip install -r requirements-dev.txt displayName: 'Install Requirements dev' - script: | - ruff . + ruff check . displayName: 'Ruff' - script: | black --diff . @@ -35,6 +35,9 @@ jobs: - script: | python -m pip install . -v -v -v displayName: 'install wheel' + - script: | + python -m pip freeze + displayName: 'pip freeze' - script: | python -m pytest displayName: 'Runs Unit Tests' @@ -48,8 +51,8 @@ jobs: vmImage: 'ubuntu-latest' strategy: matrix: - Python311-Linux: - python.version: '3.11' + Python312-Linux: + python.version: '3.12' maxParallel: 3 steps: @@ -78,11 +81,14 @@ jobs: - script: pip install onnxmltools --no-deps displayName: 'Install onnxmltools' - script: | - ruff . + ruff check . displayName: 'Ruff' - script: | black --diff . displayName: 'Black' + - script: | + python -m pip freeze + displayName: 'pip freeze' - script: | python -m pytest displayName: 'Runs Unit Tests' @@ -125,16 +131,19 @@ jobs: export ARRAY_API_TESTS_MODULE=onnx_array_api.array_api.onnx_numpy cd array-api-tests displayName: 'Set API' + - script: | + python -m pip freeze + displayName: 'pip freeze' - script: | export ARRAY_API_TESTS_MODULE=onnx_array_api.array_api.onnx_numpy cd array-api-tests python -m pytest -x array_api_tests/test_creation_functions.py --skips-file=../_unittests/onnx-numpy-skips.txt --hypothesis-explain displayName: "numpy test_creation_functions.py" - - script: | - export ARRAY_API_TESTS_MODULE=onnx_array_api.array_api.onnx_ort - cd array-api-tests - python -m pytest -x array_api_tests/test_creation_functions.py --skips-file=../_unittests/onnx-ort-skips.txt --hypothesis-explain - displayName: "ort test_creation_functions.py" + # - script: | + # export ARRAY_API_TESTS_MODULE=onnx_array_api.array_api.onnx_ort + # cd array-api-tests + # python -m pytest -x array_api_tests/test_creation_functions.py --skips-file=../_unittests/onnx-ort-skips.txt --hypothesis-explain + # displayName: "ort test_creation_functions.py" #- script: | # export ARRAY_API_TESTS_MODULE=onnx_array_api.array_api.onnx_numpy # cd array-api-tests @@ -146,8 +155,8 @@ jobs: vmImage: 'ubuntu-latest' strategy: matrix: - Python311-Linux: - python.version: '3.11' + Python312-Linux: + python.version: '3.12' maxParallel: 3 steps: @@ -172,11 +181,14 @@ jobs: - script: pip install onnxmltools --no-deps displayName: 'Install onnxmltools' - script: | - ruff . + ruff check . displayName: 'Ruff' - script: | black --diff . displayName: 'Black' + - script: | + python -m pip freeze + displayName: 'pip freeze' - script: | python -m pytest --cov displayName: 'Runs Unit Tests' @@ -196,8 +208,8 @@ jobs: vmImage: 'windows-latest' strategy: matrix: - Python311-Windows: - python.version: '3.11' + Python312-Windows: + python.version: '3.12' maxParallel: 3 steps: @@ -213,6 +225,9 @@ jobs: displayName: 'Install Requirements dev' - script: pip install onnxmltools --no-deps displayName: 'Install onnxmltools' + - script: | + python -m pip freeze + displayName: 'pip freeze' - script: | python -m pytest -v displayName: 'Runs Unit Tests' @@ -223,47 +238,3 @@ jobs: inputs: artifactName: 'wheel-windows-$(python.version)' targetPath: 'dist' - -- job: 'TestMac' - pool: - vmImage: 'macOS-latest' - strategy: - matrix: - Python311-Mac: - python.version: '3.11' - maxParallel: 3 - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' - - script: gcc --version - displayName: 'gcc version' - #- script: brew upgrade - # displayName: 'brew upgrade' - #- script: brew update - # displayName: 'brew update' - - script: export - displayName: 'export' - - script: gcc --version - displayName: 'gcc version' - - script: python -m pip install --upgrade pip setuptools wheel - displayName: 'Install tools' - - script: pip install -r requirements.txt - displayName: 'Install Requirements' - - script: pip install -r requirements-dev.txt - displayName: 'Install Requirements dev' - - script: pip install onnxmltools --no-deps - displayName: 'Install onnxmltools' - - script: | - python -m pytest - displayName: 'Runs Unit Tests' - - script: | - python -u setup.py bdist_wheel - displayName: 'Build Package' - - task: PublishPipelineArtifact@0 - inputs: - artifactName: 'wheel-mac-$(python.version)' - targetPath: 'dist' - diff --git a/onnx_array_api/__init__.py b/onnx_array_api/__init__.py index c4bc456..98371ac 100644 --- a/onnx_array_api/__init__.py +++ b/onnx_array_api/__init__.py @@ -1,7 +1,6 @@ -# coding: utf-8 """ APIs to create ONNX Graphs. """ -__version__ = "0.2.0" +__version__ = "0.3.1" __author__ = "Xavier Dupré" diff --git a/onnx_array_api/_command_lines_parser.py b/onnx_array_api/_command_lines_parser.py index 15ee153..d1eac62 100644 --- a/onnx_array_api/_command_lines_parser.py +++ b/onnx_array_api/_command_lines_parser.py @@ -14,13 +14,15 @@ def get_main_parser() -> ArgumentParser: ) parser.add_argument( "cmd", - choices=["translate", "compare"], + choices=["translate", "compare", "replace"], help=dedent( """ Selects a command. - + 'translate' exports an onnx graph into a piece of code replicating it, - 'compare' compares the execution of two onnx models + 'compare' compares the execution of two onnx models, + 'replace' replaces constant and initliazers by ConstantOfShape + to make the model lighter """ ), ) @@ -49,7 +51,7 @@ def get_parser_translate() -> ArgumentParser: parser.add_argument( "-a", "--api", - choices=["onnx", "light"], + choices=["onnx", "light", "onnx-short", "builder"], default="onnx", help="API to choose, API from onnx package or light API.", ) @@ -74,7 +76,8 @@ def get_parser_compare() -> ArgumentParser: Compares the execution of two onnx models. """ ), - epilog="This is used when two models are different but should produce the same results.", + epilog="This is used when two models are different but " + "should produce the same results.", ) parser.add_argument( "-m1", @@ -123,8 +126,14 @@ def _cmd_compare(argv: List[Any]): parser = get_parser_compare() args = parser.parse_args(argv[1:]) + if args.verbose in ("1", 1, "True", True): + print(f"[compare] first model {args.model1!r}") + print(f"[compare] second model {args.model2!r}") onx1 = onnx.load(args.model1) onx2 = onnx.load(args.model2) + if args.verbose in ("1", 1, "True", True): + print(f"[compare] first model has {len(onx1.graph.node)} nodes") + print(f"[compare] second model has {len(onx2.graph.node)} nodes") res1, res2, align, dc = compare_onnx_execution( onx1, onx2, @@ -136,8 +145,75 @@ def _cmd_compare(argv: List[Any]): print(text) +def get_parser_replace() -> ArgumentParser: + parser = ArgumentParser( + prog="translate", + description=dedent( + """ + Replaces constants and initializes by ConstOfShape or any other nodes + to make the model smaller. + """ + ), + epilog="This is mostly used to write unit tests without adding " + "a big file to the repository.", + ) + parser.add_argument( + "-m", + "--model", + type=str, + required=True, + help="onnx model to translate", + ) + parser.add_argument( + "-o", + "--out", + type=str, + required=True, + help="output file", + ) + parser.add_argument( + "-t", + "--threshold", + default=128, + help="Threshold above which every constant is replaced", + ) + parser.add_argument( + "--type", + default="ConstontOfShape", + help="Inserts this operator type", + ) + parser.add_argument( + "--domain", + default="", + help="Inserts this domain", + ) + parser.add_argument( + "-v", + "--verbose", + default=0, + help="verbosity", + ) + return parser + + +def _cmd_replace(argv: List[Any]): + from .tools.replace_constants import replace_initializer_by_constant_of_shape + + parser = get_parser_replace() + args = parser.parse_args(argv[1:]) + if args.verbose in ("1", 1, "True", True): + print(f"[compare] load model {args.model!r}") + onx = onnx.load(args.model) + new_onx = replace_initializer_by_constant_of_shape( + onx, threshold=args.threshold, op_type=args.type, domain=args.domain + ) + if args.verbose in ("1", 1, "True", True): + print(f"[compare] save model {args.out!r}") + onnx.save(new_onx, args.out) + + def main(argv: Optional[List[Any]] = None): - fcts = dict(translate=_cmd_translate, compare=_cmd_compare) + fcts = dict(translate=_cmd_translate, compare=_cmd_compare, replace=_cmd_replace) if argv is None: argv = sys.argv[1:] @@ -146,7 +222,11 @@ def main(argv: Optional[List[Any]] = None): parser = get_main_parser() parser.parse_args(argv) else: - parsers = dict(translate=get_parser_translate, compare=get_parser_compare) + parsers = dict( + translate=get_parser_translate, + compare=get_parser_compare, + replace=get_parser_replace, + ) cmd = argv[0] if cmd not in parsers: raise ValueError( diff --git a/onnx_array_api/_helpers.py b/onnx_array_api/_helpers.py index f9808ca..9331098 100644 --- a/onnx_array_api/_helpers.py +++ b/onnx_array_api/_helpers.py @@ -9,7 +9,7 @@ def np_dtype_to_tensor_dtype(dtype: Any): """ try: dt = helper.np_dtype_to_tensor_dtype(dtype) - except KeyError: + except (KeyError, ValueError): if dtype == np.float32: dt = TensorProto.FLOAT elif dtype == np.float64: @@ -40,6 +40,10 @@ def np_dtype_to_tensor_dtype(dtype: Any): dt = TensorProto.INT64 elif dtype is float: dt = TensorProto.DOUBLE + elif dtype == np.complex64: + dt = TensorProto.COMPLEX64 + elif dtype == np.complex128: + dt = TensorProto.COMPLEX128 else: - raise KeyError(f"Unable to guess type for dtype={dtype}.") + raise KeyError(f"Unable to guess type for dtype={dtype}.") # noqa: B904 return dt diff --git a/onnx_array_api/annotations.py b/onnx_array_api/annotations.py index 9941f95..c29102c 100644 --- a/onnx_array_api/annotations.py +++ b/onnx_array_api/annotations.py @@ -64,6 +64,8 @@ def wrapper(self, *args: List[Any], **kwargs: Dict[str, Any]) -> Any: np.uint64: TensorProto.UINT64, np.bool_: TensorProto.BOOL, np.str_: TensorProto.STRING, + np.complex64: TensorProto.COMPLEX64, + np.complex128: TensorProto.COMPLEX128, } diff --git a/onnx_array_api/array_api/__init__.py b/onnx_array_api/array_api/__init__.py index f4b3c4d..9b67b4b 100644 --- a/onnx_array_api/array_api/__init__.py +++ b/onnx_array_api/array_api/__init__.py @@ -47,12 +47,14 @@ def _finfo(dtype): continue if isinstance(v, (np.float32, np.float64, np.float16)): d[k] = float(v) + elif isinstance(v, (np.complex128, np.complex64)): + d[k] = complex(v) else: d[k] = v d["dtype"] = DType(np_dtype_to_tensor_dtype(dt)) nres = type("finfo", (res.__class__,), d) - setattr(nres, "smallest_normal", float(res.smallest_normal)) - setattr(nres, "tiny", float(res.tiny)) + setattr(nres, "smallest_normal", float(res.smallest_normal)) # noqa: B010 + setattr(nres, "tiny", float(res.tiny)) # noqa: B010 return nres @@ -84,8 +86,8 @@ def _iinfo(dtype): d[k] = v d["dtype"] = DType(np_dtype_to_tensor_dtype(dt)) nres = type("iinfo", (res.__class__,), d) - setattr(nres, "min", int(res.min)) - setattr(nres, "max", int(res.max)) + setattr(nres, "min", int(res.min)) # noqa: B010 + setattr(nres, "max", int(res.max)) # noqa: B010 return nres @@ -124,6 +126,8 @@ def _finalize_array_api(module, function_names, TEagerTensor): module.float16 = DType(TensorProto.FLOAT16) module.float32 = DType(TensorProto.FLOAT) module.float64 = DType(TensorProto.DOUBLE) + module.complex64 = DType(TensorProto.COMPLEX64) + module.complex128 = DType(TensorProto.COMPLEX128) module.int8 = DType(TensorProto.INT8) module.int16 = DType(TensorProto.INT16) module.int32 = DType(TensorProto.INT32) @@ -133,10 +137,10 @@ def _finalize_array_api(module, function_names, TEagerTensor): module.uint32 = DType(TensorProto.UINT32) module.uint64 = DType(TensorProto.UINT64) module.bfloat16 = DType(TensorProto.BFLOAT16) - setattr(module, "bool", DType(TensorProto.BOOL)) - setattr(module, "str", DType(TensorProto.STRING)) - setattr(module, "finfo", _finfo) - setattr(module, "iinfo", _iinfo) + setattr(module, "bool", DType(TensorProto.BOOL)) # noqa: B010 + setattr(module, "str", DType(TensorProto.STRING)) # noqa: B010 + setattr(module, "finfo", _finfo) # noqa: B010 + setattr(module, "iinfo", _iinfo) # noqa: B010 if function_names is None: function_names = supported_functions @@ -146,7 +150,10 @@ def _finalize_array_api(module, function_names, TEagerTensor): if f is None: f2 = getattr(npx_functions, name, None) if f2 is None: - warnings.warn(f"Function {name!r} is not available in {module!r}.") + warnings.warn( + f"Function {name!r} is not available in {module!r}.", + stacklevel=0, + ) continue f = lambda TEagerTensor, *args, _f=f2, **kwargs: _f( # noqa: E731 *args, **kwargs diff --git a/onnx_array_api/array_api/_onnx_common.py b/onnx_array_api/array_api/_onnx_common.py index 6e8ee6d..7c486ce 100644 --- a/onnx_array_api/array_api/_onnx_common.py +++ b/onnx_array_api/array_api/_onnx_common.py @@ -1,11 +1,8 @@ from typing import Any, Optional -import warnings import numpy as np from onnx import TensorProto +import array_api_strict -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from numpy.array_api._array_object import Array from ..npx.npx_types import ( DType, ElemType, @@ -30,6 +27,9 @@ ) +Array = type(array_api_strict.ones((1,))) + + # These functions with no specific code do not have to be # implemented. They are automatically added in # :mod:`onnx_array_api.array_api`. It needs @@ -46,14 +46,13 @@ def asarray( dtype: Optional[DType] = None, order: Optional[str] = None, like: Any = None, + device: Optional[str] = None, copy: bool = False, ) -> EagerTensor: """ Converts anything into an array. """ - """ - Converts anything into an array. - """ + assert device is None, f"asarray not implemented yet for device={device!r}" if order not in ("C", None): raise NotImplementedError(f"asarray is not implemented for order={order!r}.") if like is not None: @@ -88,18 +87,20 @@ def asarray( v = TEagerTensor(va) elif isinstance(a, float): v = TEagerTensor(np.array(a, dtype=np.float64)) + elif isinstance(a, complex): + v = TEagerTensor(np.array(a, dtype=np.complex128)) elif isinstance(a, bool): v = TEagerTensor(np.array(a, dtype=np.bool_)) elif isinstance(a, str): v = TEagerTensor(np.array(a, dtype=np.str_)) elif isinstance(a, list): - if all(map(lambda x: isinstance(x, bool), a)): + if all(isinstance(x, bool) for x in a): v = TEagerTensor(np.array(a, dtype=np.bool_)) - elif all(map(lambda x: isinstance(x, int), a)): + elif all(isinstance(x, int) for x in a): try: cvt = np.array(a, dtype=np.int64) except OverflowError as e: - if all(map(lambda x: x >= 0, a)): + if all(x >= 0 for x in a): cvt = np.array(a, dtype=np.uint64) else: raise e @@ -108,7 +109,7 @@ def asarray( v = TEagerTensor(np.array(a)) elif isinstance(a, np.ndarray): v = TEagerTensor(a) - elif isinstance(a, Array): + elif Array and isinstance(a, Array): v = TEagerTensor(np.asarray(a)) else: raise RuntimeError(f"Unexpected type {type(a)} for the first input.") @@ -128,9 +129,7 @@ def arange( step: EagerTensor[OptTensorType[ElemType.int64, "I", (1,)]] = None, dtype: OptParType[DType] = None, ) -> EagerTensor[TensorType[ElemType.numerics, "T"]]: - use_float = any( - map(lambda x: isinstance(x, float), [start_or_stop, stop_or_step, step]) - ) + use_float = any(isinstance(x, float) for x in [start_or_stop, stop_or_step, step]) if isinstance(start_or_stop, int): start_or_stop = TEagerTensor( np.array([start_or_stop], dtype=np.float64 if use_float else np.int64) @@ -208,7 +207,7 @@ def eye( /, *, k: ParType[int] = 0, - dtype: ParType[DType] = DType(TensorProto.DOUBLE), + dtype: ParType[DType] = DType(TensorProto.DOUBLE), # noqa: B008 ): if isinstance(n_rows, int): n_rows = TEagerTensor(np.array(n_rows, dtype=np.int64)) @@ -246,7 +245,7 @@ def linspace( dtype: OptParType[DType] = None, endpoint: ParType[int] = 1, ) -> EagerTensor[TensorType[ElemType.numerics, "T"]]: - use_float = any(map(lambda x: isinstance(x, float), [start, stop])) + use_float = any(isinstance(x, float) for x in [start, stop]) if isinstance(start, int): start = TEagerTensor( np.array(start, dtype=np.float64 if use_float else np.int64) diff --git a/onnx_array_api/ext_test_case.py b/onnx_array_api/ext_test_case.py index 3c12e65..d91ba1a 100644 --- a/onnx_array_api/ext_test_case.py +++ b/onnx_array_api/ext_test_case.py @@ -235,7 +235,7 @@ def assertRaise(self, fct: Callable, exc_type: Exception): fct() except exc_type as e: if not isinstance(e, exc_type): - raise AssertionError(f"Unexpected exception {type(e)!r}.") + raise AssertionError(f"Unexpected exception {type(e)!r}.") # noqa: B904 return raise AssertionError("No exception was raised.") @@ -266,7 +266,7 @@ def assertStartsWith(self, prefix: str, full: str): @classmethod def tearDownClass(cls): for name, line, w in cls._warns: - warnings.warn(f"\n{name}:{line}: {type(w)}\n {str(w)}") + warnings.warn(f"\n{name}:{line}: {type(w)}\n {str(w)}", stacklevel=0) def capture(self, fct: Callable): """ @@ -277,9 +277,8 @@ def capture(self, fct: Callable): """ sout = StringIO() serr = StringIO() - with redirect_stdout(sout): - with redirect_stderr(serr): - res = fct() + with redirect_stdout(sout), redirect_stderr(serr): + res = fct() return res, sout.getvalue(), serr.getvalue() def relative_path(self, filename: str, *names: List[str]) -> str: diff --git a/onnx_array_api/graph_api/graph_builder.py b/onnx_array_api/graph_api/graph_builder.py index c9c2059..5e414ed 100644 --- a/onnx_array_api/graph_api/graph_builder.py +++ b/onnx_array_api/graph_api/graph_builder.py @@ -119,6 +119,18 @@ def __getattr__(self, name): except AttributeError as e: raise AttributeError(f"Unable to access attribute {name!r}.") from e + def Initializer( + self, init: Union[TensorProto, np.ndarray], name: Optional[str] = None + ) -> str: + """ + Creates an initializer. + + :param init: value + :param name: name if value is not a TensorProto + :return: its name + """ + return self.builder.make_initializer(init, name=name, exists=True) + def make_node( self, op_type: str, @@ -156,6 +168,7 @@ def __init__( optimization_options: Optional[OptimizationOptions] = None, args: Optional[List[Any]] = None, verbose: int = 0, + ir_version: Optional[int] = None, ): self.optimization_options = optimization_options or OptimizationOptions() self.as_function = as_function @@ -170,6 +183,7 @@ def __init__( if isinstance(target_opset_or_existing_proto, int) else target_opset_or_existing_proto ) + self.ir_version = ir_version self.nodes = [] self.initializers_dict = {} self.inputs = [] @@ -180,12 +194,14 @@ def __init__( self._known_shapes = {} self._known_types = {} self.constants_ = {} + self.functions_ = {} elif isinstance(target_opset_or_existing_proto, ModelProto): assert ( not input_names ), "input_names must be empty if the input is an existing model." proto = target_opset_or_existing_proto self.opsets = {d.domain: d.version for d in proto.opset_import} + self.ir_version = ir_version or target_opset_or_existing_proto.ir_version self.nodes = list(proto.graph.node) self.initializers_dict = {i.name: i for i in proto.graph.initializer} self.initializers_dict.update( @@ -208,6 +224,8 @@ def __init__( self.constants_[node.output[0]] = node self.set_shape(node.output[0], self._get_tensor_shape(node)) self.set_type(node.output[0], self._get_tensor_type(node)) + for f in proto.functions: + self.add_function(f) else: raise NotImplementedError( f"{type(target_opset_or_existing_proto)} is not supported." @@ -216,6 +234,14 @@ def __init__( self.op = Opset(self, self.opsets[""]) if "" in self.opsets else None self._cache_array = [] + def add_local_function(self, domain: str, name: str, gr: "GraphBuilder"): + "Adds a local function." + assert ( + domain, + name, + ) not in self.functions_, f"Function {(domain, name)} was already added." + self.functions_[domain, name] = gr + def _get_tensor_shape( self, proto: Union[NodeProto, TensorProto] ) -> Tuple[int, ...]: @@ -313,9 +339,9 @@ def has_type(self, name: str) -> bool: return name in self._known_types def get_type(self, name: str) -> int: - assert name in self._known_types, ( - f"Type is unknown for result {name!r}, " f"known_types={self._known_types}." - ) + assert ( + name in self._known_types + ), f"Type is unknown for result {name!r}, known_types={self._known_types}." return self._known_types[name] def unique_name(self, prefix: str) -> str: @@ -402,6 +428,8 @@ def make_tensor_output( name: Union[str, List[str]], elem_type: Optional[int] = None, shape: Optional[Tuple[int, ...]] = None, + is_dimension: bool = False, + indexed: bool = False, ) -> Union[str, List[str]]: if isinstance(name, list): res = [] @@ -457,7 +485,7 @@ def make_node( f"A node {op_type!r} cannot be created with " f"inputs={inputs} (types={[type(i) for i in inputs]}), " f"outputs={outputs} " - f"(types={[type(o) for o in outputs] if isinstance(outputs, (tuple, list)) else outputs}), " + f"(types={[type(o) for o in outputs] if isinstance(outputs, (tuple, list)) else outputs}), " # noqa: E501 f"domain={domain!r}, kwargs={kwargs}." ) from e if attributes: @@ -521,7 +549,6 @@ def make_nodes( if isinstance(value, TensorProto): value.name = name self.initializers_dict[name] = value - self.constants_[name] = None self.set_shape(name, builder._known_shapes[init]) self.set_type(name, builder._known_types[init]) @@ -579,14 +606,16 @@ def make_nodes( return output_names[0] return output_names - def from_array(self, arr: T, name: str = None) -> TensorProto: # noqa: F821 + def from_array( + self, arr: T, name: Optional[str] = None + ) -> TensorProto: # noqa: F821 if isinstance(arr, np.ndarray): return self.from_np_array(arr, name) raise NotImplementedError( f"{type(arr)} is not supported yet but initializer {name or ''!r} is." ) - def from_np_array(self, arr: np.ndarray, name: str = None) -> TensorProto: + def from_np_array(self, arr: np.ndarray, name: Optional[str] = None) -> TensorProto: arr_cpu = np.ascontiguousarray(arr) if not arr.flags["C_CONTIGUOUS"] else arr if arr_cpu.ctypes.data == arr.ctypes.data: if sys.byteorder == "big": @@ -674,6 +703,8 @@ def to_onnx( if self.verbose: print("[GraphBuilder] onh.make_model") model = oh.make_model(graph, opset_imports=opsets) + if self.ir_version: + model.ir_version = self.ir_version return model def _check_order_node(self, ind: int, node: NodeProto, existing: Set[str]): @@ -799,7 +830,7 @@ def constant_folding(self): """ updates = {} node_to_remove = set() - for k, v in self.constants_.items(): + for _k, v in self.constants_.items(): if v is None: # this is an initiliazer continue @@ -820,7 +851,8 @@ def constant_folding(self): self.initializers_dict[name] = value if self.verbose: print( - f"[GraphBuilder] fold_constant:{v.op_type}:{name}[{value.dtype}:" + f"[GraphBuilder] fold_constant:" + f"{v.op_type}:{name}[{value.dtype}:" f"{value.shape}]:from:{','.join(sorted(feeds))}" ) @@ -863,7 +895,8 @@ def remove_identity_nodes(self): if new_name in replacements: new_name = replacements[new_name] assert new_name not in replacements, ( - f"Name {old_name!r} still in {replacements}, node.op_type={node.op_type!r}, " + f"Name {old_name!r} still in {replacements}, " + f"node.op_type={node.op_type!r}, " f"node.input={node.input}, node.output={node.output}, " f"input_names={input_names}, output_names={output_names}" ) @@ -874,7 +907,8 @@ def remove_identity_nodes(self): if old_name in replacements: replacements[replacements[old_name]] = new_name assert new_name not in replacements, ( - f"Name {old_name!r} still in {replacements}, node.op_type={node.op_type!r}, " + f"Name {old_name!r} still in {replacements}, " + f"node.op_type={node.op_type!r}, " f"node.input={node.input}, node.output={node.output}, " f"input_names={input_names}, output_names={output_names}" ) @@ -885,7 +919,8 @@ def remove_identity_nodes(self): for k, v in replacements.items(): assert v not in replacements, ( f"replacement {k}->{v} is not possible because of " - f"{v}->{replacements[v]}, old_name={old_name!r}, new_name={new_name!r}" + f"{v}->{replacements[v]}, old_name={old_name!r}, " + f"new_name={new_name!r}" ) # second pass: replacements in initializer diff --git a/onnx_array_api/light_api/model.py b/onnx_array_api/light_api/model.py index 6478c4d..f6770eb 100644 --- a/onnx_array_api/light_api/model.py +++ b/onnx_array_api/light_api/model.py @@ -319,7 +319,8 @@ def rename(self, old_name: str, new_name: str): value = self.unique_names_[old_name] if isinstance(value, int): raise TypeError( - f"Unexpected type {type(value)} for value {old_name!r} renamed into {new_name!r}." + f"Unexpected type {type(value)} for value {old_name!r} " + f"renamed into {new_name!r}." ) self.unique_names_[new_name] = value self.renames_[old_name] = new_name diff --git a/onnx_array_api/light_api/var.py b/onnx_array_api/light_api/var.py index 2d7eac8..72a9533 100644 --- a/onnx_array_api/light_api/var.py +++ b/onnx_array_api/light_api/var.py @@ -193,7 +193,7 @@ def make_node( ) if len(names) == 1: return Var(self.parent, names[0]) - return Vars(self.parent, *list(map(lambda v: Var(self.parent, v), names))) + return Vars(self.parent, *[Var(self.parent, v) for v in names]) def vin( self, @@ -445,7 +445,8 @@ def rename(self, *new_names: List[str]) -> "Vars": "Renames variables." if len(new_names) != len(self): raise ValueError( - f"Vars has {len(self)} elements but the method received {len(new_names)} names." + f"Vars has {len(self)} elements but the method " + f"received {len(new_names)} names." ) new_vars = [] for var, name in zip(self.vars_, new_names): diff --git a/onnx_array_api/npx/npx_array_api.py b/onnx_array_api/npx/npx_array_api.py index 142a892..a9fb3d6 100644 --- a/onnx_array_api/npx/npx_array_api.py +++ b/onnx_array_api/npx/npx_array_api.py @@ -10,8 +10,6 @@ class ArrayApiError(RuntimeError): Raised when a function is not supported by the :epkg:`Array API`. """ - pass - class BaseArrayApi: """ diff --git a/onnx_array_api/npx/npx_core_api.py b/onnx_array_api/npx/npx_core_api.py index d6688cf..a09280a 100644 --- a/onnx_array_api/npx/npx_core_api.py +++ b/onnx_array_api/npx/npx_core_api.py @@ -15,7 +15,7 @@ class args_tuple(tuple): """Overwrites a tuple to make the distinction later in the code.""" - pass + __slots__ = () def cst(*args, **kwargs): @@ -140,7 +140,7 @@ def _xapi(fn: Callable, inline: bool): # It has the same signature def wrapper(*inputs, **kwargs): - if any(map(lambda x: isinstance(x, EagerTensor), inputs)): + if any(isinstance(x, EagerTensor) for x in inputs): tensor_class = None for x in inputs: if isinstance(x, EagerTensor): diff --git a/onnx_array_api/npx/npx_functions.py b/onnx_array_api/npx/npx_functions.py index 2f547d6..c6319f2 100644 --- a/onnx_array_api/npx/npx_functions.py +++ b/onnx_array_api/npx/npx_functions.py @@ -281,7 +281,8 @@ def astype( to = DType(TensorProto.STRING) else: raise TypeError(f"dtype must of type DType, not {type(dtype)}-{dtype}.") - return var(a, op="Cast", to=to.code) + return var(a, op="Cast", to=to.code) + return var(a, op="Cast", to=dtype.code) @npxapi_inline @@ -479,7 +480,7 @@ def eye( /, *, k: ParType[int] = 0, - dtype: ParType[DType] = DType(TensorProto.DOUBLE), + dtype: ParType[DType] = DType(TensorProto.DOUBLE), # noqa: B008 ): "See :func:`numpy.eye`." shape = cst(np.array([-1], dtype=np.int64)) diff --git a/onnx_array_api/npx/npx_functions_test.py b/onnx_array_api/npx/npx_functions_test.py index 4d442dd..3d03def 100644 --- a/onnx_array_api/npx/npx_functions_test.py +++ b/onnx_array_api/npx/npx_functions_test.py @@ -22,21 +22,21 @@ @npxapi_function def _min_max( - x: TensorType[ElemType.numerics, "T"] + x: TensorType[ElemType.numerics, "T"], ) -> TupleType[TensorType[ElemType.numerics, "T"], TensorType[ElemType.numerics, "T"]]: return tuple_var(var(x, op="ReduceMin"), var(x, op="ReduceMax")) @npxapi_inline def _min_max_inline( - x: TensorType[ElemType.numerics, "T"] + x: TensorType[ElemType.numerics, "T"], ) -> TupleType[TensorType[ElemType.numerics, "T"], TensorType[ElemType.numerics, "T"]]: return tuple_var(var(x, op="ReduceMin"), var(x, op="ReduceMax")) @npxapi_function def absolute( - x: TensorType[ElemType.numerics, "T"] + x: TensorType[ElemType.numerics, "T"], ) -> TensorType[ElemType.numerics, "T"]: "See :func:`numpy.absolute`." return var(x, op="Abs") @@ -90,7 +90,7 @@ def log1p(x: TensorType[ElemType.floats, "T"]) -> TensorType[ElemType.floats, "T @npxapi_function def negative( - x: TensorType[ElemType.numerics, "T"] + x: TensorType[ElemType.numerics, "T"], ) -> TensorType[ElemType.numerics, "T"]: "See :func:`numpy.negative`." return var(x, op="Neg") diff --git a/onnx_array_api/npx/npx_graph_builder.py b/onnx_array_api/npx/npx_graph_builder.py index 4496d79..91034f7 100644 --- a/onnx_array_api/npx/npx_graph_builder.py +++ b/onnx_array_api/npx/npx_graph_builder.py @@ -450,7 +450,7 @@ def _make_onnx(self): name = inp.name if name is None: raise RuntimeError( - f"Input {i} is None for function " f"{self.function_name!r}." + f"Input {i} is None for function {self.function_name!r}." ) inputs.append(name) @@ -473,7 +473,7 @@ def _make_onnx(self): model = make_model( graph, opset_imports=opset_imports, - functions=list(f[0] for f in self.functions_.values()), + functions=[f[0] for f in self.functions_.values()], ir_version=self.ir_version, ) if not is_windows() or not is_azure(): @@ -512,12 +512,7 @@ def _function_to_onnx(self, fct: Callable, n_inputs: int, n_outputs: int): there is an undefined number of inputs """ sig = signature(fct) - if any( - map( - lambda t: issubclass(t.annotation, SequenceType), - sig.parameters.values(), - ) - ): + if any(issubclass(t.annotation, SequenceType) for t in sig.parameters.values()): # onnx does not allow undefined number of inputs key = fct.__module__, fct.__name__, n_inputs else: @@ -852,7 +847,7 @@ def to_onnx( node_inputs.append(input_name) continue - if isinstance(i, tuple) and all(map(lambda x: isinstance(x, int), i)): + if isinstance(i, tuple) and all(isinstance(x, int) for x in i): ai = np.array(list(i), dtype=np.int64) c = Cst(ai) input_name = self._unique(var._prefix) diff --git a/onnx_array_api/npx/npx_helper.py b/onnx_array_api/npx/npx_helper.py index 34d9af3..b2c6b48 100644 --- a/onnx_array_api/npx/npx_helper.py +++ b/onnx_array_api/npx/npx_helper.py @@ -130,8 +130,7 @@ def iter_nodes(nodes: Sequence[NodeProto]) -> Iterator[NodeProto]: and hasattr(att, "g") and att.g is not None ): - for n in iter_nodes(att.g.node): - yield n + yield from iter_nodes(att.g.node) def onnx_model_to_function( diff --git a/onnx_array_api/npx/npx_jit_eager.py b/onnx_array_api/npx/npx_jit_eager.py index 20becbd..267eda5 100644 --- a/onnx_array_api/npx/npx_jit_eager.py +++ b/onnx_array_api/npx/npx_jit_eager.py @@ -167,7 +167,7 @@ def make_key(self, *values: List[Any], **kwargs: Dict[str, Any]) -> Tuple[Any, . f"to the attribute list, v={v}." ) res.append(v.key) - elif isinstance(v, (int, float, bool, DType)): + elif isinstance(v, (int, float, bool, complex, DType)): if iv in self.kwargs_to_input_: res.append(self.kwargs_to_input_[iv]) res.append(type(v)) @@ -204,7 +204,7 @@ def make_key(self, *values: List[Any], **kwargs: Dict[str, Any]) -> Tuple[Any, . if k in self.kwargs_to_input_: res.append(type(v)) res.append(v) - elif isinstance(v, (int, float, str, type, bool, DType)): + elif isinstance(v, (int, float, str, type, bool, complex, DType)): res.append(k) res.append(type(v)) res.append(v) @@ -563,7 +563,7 @@ class JitOnnx(JitEager): def __init__( self, f: Callable, - tensor_class: type = None, + tensor_class: Optional[type] = None, target_opsets: Optional[Dict[str, int]] = None, output_types: Optional[Dict[Any, TensorType]] = None, ir_version: Optional[int] = None, @@ -636,7 +636,7 @@ class EagerOnnx(JitEager): def __init__( self, f: Callable, - tensor_class: type = None, + tensor_class: Optional[type] = None, target_opsets: Optional[Dict[str, int]] = None, output_types: Optional[Dict[Any, TensorType]] = None, ir_version: Optional[int] = None, @@ -671,12 +671,12 @@ def _preprocess_constants(self, *args): new_args.append(self.tensor_class(n.inputs[0])) modified = True elif isinstance(n, tuple): - if all(map(lambda x: isinstance(x, int), n)): + if all(isinstance(x, int) for x in n): new_args.append( self.tensor_class(np.array(list(n), dtype=np.int64)) ) modified = True - elif any(map(lambda t: isinstance(t, Var), n)): + elif any(isinstance(t, Var) for t in n): raise TypeError( f"Unexpected types in tuple " f"({[type(t) for t in n]}) for input {i}, " @@ -727,14 +727,14 @@ def __call__(self, *args, already_eager=False, **kwargs): ) if already_eager: if any( - map( - lambda t: t is not None + ( + t is not None and not isinstance( t, EagerOnnx.allowed_input_types, - ), - args, + ) ) + for t in args ): raise TypeError( f"One of the input is not an EagerTensor or a constant, " @@ -759,8 +759,8 @@ def __call__(self, *args, already_eager=False, **kwargs): try: res = self.f(*values, **kwargs) except (AttributeError, TypeError) as e: - inp1 = ", ".join(map(str, map(lambda a: type(a).__name__, args))) - inp2 = ", ".join(map(str, map(lambda a: type(a).__name__, values))) + inp1 = ", ".join(map(str, [type(a).__name__ for a in args])) + inp2 = ", ".join(map(str, [type(a).__name__ for a in values])) raise TypeError( f"Unexpected types, input types are args=[{inp1}], " f"values=[{inp2}], kwargs={kwargs}. " @@ -778,7 +778,7 @@ def __call__(self, *args, already_eager=False, **kwargs): f"from module {self.f.__module__!r}, " f"type of first input is {type(args[0])}." ) - elif isinstance(res, Var) or any(map(lambda x: isinstance(x, Var), res)): + elif isinstance(res, Var) or any(isinstance(x, Var) for x in res): # The function returns instance of type Var. # It does not support eager mode and needs # to be converted into onnx. diff --git a/onnx_array_api/npx/npx_numpy_tensors.py b/onnx_array_api/npx/npx_numpy_tensors.py index 68a4da7..9579455 100644 --- a/onnx_array_api/npx/npx_numpy_tensors.py +++ b/onnx_array_api/npx/npx_numpy_tensors.py @@ -223,7 +223,8 @@ def __bool__(self): if self.shape: warnings.warn( f"Conversion to bool only works for scalar, not for {self!r}, " - f"bool(...)={bool(self._tensor)}." + f"bool(...)={bool(self._tensor)}.", + stacklevel=0, ) try: return bool(self._tensor) @@ -264,6 +265,8 @@ def __float__(self): DType(TensorProto.DOUBLE), DType(TensorProto.FLOAT16), DType(TensorProto.BFLOAT16), + DType(TensorProto.COMPLEX64), + DType(TensorProto.COMPLEX128), }: raise TypeError( f"Conversion to float only works for float scalar, " @@ -271,6 +274,26 @@ def __float__(self): ) return float(self._tensor) + def __complex__(self): + "Implicit conversion to complex." + if self.shape: + raise ValueError( + f"Conversion to bool only works for scalar, not for {self!r}." + ) + if self.dtype not in { + DType(TensorProto.FLOAT), + DType(TensorProto.DOUBLE), + DType(TensorProto.FLOAT16), + DType(TensorProto.BFLOAT16), + DType(TensorProto.COMPLEX64), + DType(TensorProto.COMPLEX128), + }: + raise TypeError( + f"Conversion to float only works for float scalar, " + f"not for dtype={self.dtype}." + ) + return complex(self._tensor) + def __iter__(self): """ The :epkg:`Array API` does not define this function (2022/12). @@ -279,7 +302,8 @@ def __iter__(self): warnings.warn( f"Iterators are not implemented in the generic case. " f"Every function using them cannot be converted into ONNX " - f"(tensors - {type(self)})." + f"(tensors - {type(self)}).", + stacklevel=0, ) for row in self._tensor: yield self.__class__(row) @@ -289,5 +313,3 @@ class JitNumpyTensor(NumpyTensor, JitTensor): """ Defines a value for a specific backend. """ - - pass diff --git a/onnx_array_api/npx/npx_tensors.py b/onnx_array_api/npx/npx_tensors.py index 3e4faa7..40ebc12 100644 --- a/onnx_array_api/npx/npx_tensors.py +++ b/onnx_array_api/npx/npx_tensors.py @@ -10,8 +10,6 @@ class JitTensor: Defines a value for a specific jit mode """ - pass - class EagerTensor(BaseArrayApi): """ @@ -93,7 +91,7 @@ def _astype_impl( if not isinstance(x, Var): raise TypeError(f"Input 0 must be a Var not {type(x)}.") - meth = getattr(Var, "astype") + meth = getattr(Var, "astype") # noqa: B009 return meth(x, dtype) @staticmethod diff --git a/onnx_array_api/npx/npx_types.py b/onnx_array_api/npx/npx_types.py index 8284765..2f2a6a6 100644 --- a/onnx_array_api/npx/npx_types.py +++ b/onnx_array_api/npx/npx_types.py @@ -11,7 +11,7 @@ class WrapperType: WrapperType. """ - pass + __slots__ = () class DType(WrapperType): @@ -78,8 +78,8 @@ def __eq__(self, dt: "DType") -> bool: return self.code_ == dt.dtype.code_ try: dti = np_dtype_to_tensor_dtype(dt) - except KeyError: - raise TypeError(f"dt must be DType not {type(dt)} - {dt!r}.") + except KeyError as e: + raise TypeError(f"dt must be DType not {type(dt)} - {dt!r}.") from e return self.code_ == dti def __lt__(self, dt: "DType") -> bool: @@ -90,8 +90,8 @@ def __lt__(self, dt: "DType") -> bool: raise TypeError(f"dt must be DType not {type(dt)}.") try: dti = np_dtype_to_tensor_dtype(dt) - except KeyError: - raise TypeError(f"dt must be DType not {type(dt)} - {dt}.") + except KeyError as e: + raise TypeError(f"dt must be DType not {type(dt)} - {dt}.") from e return self.code_ < dti @classmethod @@ -102,12 +102,10 @@ def type_name(cls) -> str: class _DType2(DType): "Wraps a type into a different type." - pass class _DTypes(DType): "Wraps a type into a different type." - pass class ElemTypeCstInner(WrapperType): @@ -367,7 +365,7 @@ def onnx_type(cls): if cls.dtype == str: return AttributeProto.STRING raise RuntimeError( - f"Unsupported attribute type {cls.dtype!r} " f"for parameter {cls!r}." + f"Unsupported attribute type {cls.dtype!r} for parameter {cls!r}." ) @@ -403,9 +401,11 @@ class ShapeType(Tuple[int, ...]): Defines a shape type. """ + __slots__ = () + @classmethod def __class_getitem__(cls, *args): - if any(map(lambda t: t is not None and not isinstance(t, (int, str)), args)): + if any((t is not None and not isinstance(t, (int, str))) for t in args): raise TypeError( f"Unexpected value for args={args}, every element should int or str." ) @@ -504,7 +504,7 @@ def __class_getitem__(cls, *args): if name: msg.append(name) if dtypes is not None: - msg.append("_".join(map(lambda t: str(t.dtype), dtypes))) + msg.append("_".join(str(t.dtype) for t in dtypes)) if shape is not None: msg.append("_".join(map(str, shape))) final = "__".join(msg) @@ -561,11 +561,11 @@ def _name_set(self): s += 1 << dt.dtype try: return ElemType.set_names[s] - except KeyError: + except KeyError as e: raise RuntimeError( f"Unable to guess element type name for {s}: " f"{repr(self)} in {ElemType.set_names}." - ) + ) from e @classmethod def issuperset(cls, tensor_type: type) -> bool: @@ -686,7 +686,7 @@ def len(cls): @classmethod def type_name(cls) -> str: "Returns its full name." - dts = ", ".join(map(lambda s: s.type_name(), cls.elem_types)) + dts = ", ".join(s.type_name() for s in cls.elem_types) if cls.name: newt = f"TupleType[{dts}, {cls.name!r}]" else: diff --git a/onnx_array_api/npx/npx_var.py b/onnx_array_api/npx/npx_var.py index ca8af0d..0e71070 100644 --- a/onnx_array_api/npx/npx_var.py +++ b/onnx_array_api/npx/npx_var.py @@ -33,7 +33,7 @@ def __init__( ): if not issubclass(dtype, ParType): raise TypeError( - f"dtype for parameter {name!r} must be of " f"ParType not {dtype}." + f"dtype for parameter {name!r} must be of ParType not {dtype}." ) if parent_op is None: raise ValueError(f"parent_op must be filled for paramenter {name!r}.") @@ -453,7 +453,7 @@ def _get_vars(self): deleted.append(var) continue raise TypeError( - f"Unexpected type {type(applied)} as output of " f"function {fct}." + f"Unexpected type {type(applied)} as output of function {fct}." ) vs.append(var) for i in reversed(var.inputs): @@ -469,11 +469,11 @@ def _get_vars(self): replacement_cst[id(i)] = cst(np.array(i)) continue if isinstance(i, tuple): - if all(map(lambda x: isinstance(x, int), i)): + if all(isinstance(x, int) for x in i): cst = Var.get_cst_var()[0] replacement_cst[id(i)] = cst(np.array(list(i), dtype=np.int64)) continue - if any(map(lambda t: isinstance(t, Var), i)): + if any(isinstance(t, Var) for t in i): raise TypeError( f"Unexpected types in tuple " f"({[type(t) for t in i]}), " @@ -1138,7 +1138,7 @@ class Input(Var): :param annotation: annotation if any is available """ - def __init__(self, name: str = None, annotation: Optional[type] = None): + def __init__(self, name: Optional[str] = None, annotation: Optional[type] = None): Var.__init__(self) self.name = name self._prefix = name or "I" @@ -1171,16 +1171,20 @@ def __init__(self, cst: Any): Var.__init__(self, np.array(cst, dtype=np.int64), op="Identity") elif isinstance(cst, float): Var.__init__(self, np.array(cst, dtype=np.float64), op="Identity") + elif isinstance(cst, complex): + Var.__init__(self, np.array(cst, dtype=np.complex128), op="Identity") elif isinstance(cst, list): - if all(map(lambda t: isinstance(t, bool), cst)): + if all(isinstance(t, bool) for t in cst): Var.__init__(self, np.array(cst, dtype=np.bool_), op="Identity") - elif all(map(lambda t: isinstance(t, (int, bool)), cst)): + elif all(isinstance(t, (int, bool)) for t in cst): Var.__init__(self, np.array(cst, dtype=np.int64), op="Identity") - elif all(map(lambda t: isinstance(t, (float, int, bool)), cst)): + elif all(isinstance(t, (float, int, bool)) for t in cst): Var.__init__(self, np.array(cst, dtype=np.float64), op="Identity") + elif all(isinstance(t, (float, int, bool, complex)) for t in cst): + Var.__init__(self, np.array(cst, dtype=np.complex128), op="Identity") else: raise ValueError( - f"Unable to convert cst (type={type(cst)}), " f"value={cst}." + f"Unable to convert cst (type={type(cst)}), value={cst}." ) else: raise NotImplementedError( diff --git a/onnx_array_api/ort/ort_profile.py b/onnx_array_api/ort/ort_profile.py index b61df67..ebccaba 100644 --- a/onnx_array_api/ort/ort_profile.py +++ b/onnx_array_api/ort/ort_profile.py @@ -52,7 +52,7 @@ def sep_event(s): for c in agg_cols: df[c] = df[c].fillna("") df["dur"] = df["dur"].fillna(0) - agg = df[agg_cols + ["dur"]].groupby(agg_cols).sum() + agg = df[[*agg_cols, "dur"]].groupby(agg_cols).sum() return agg @@ -101,14 +101,16 @@ def ort_profile( if providers is None: providers = ["CPUExecutionProvider"] sess = InferenceSession(obj, sess_options, providers=providers, **kwargs) - first = list(feeds.values())[0] + for v in feeds.values(): + first = v + break if isinstance(first, numpy.ndarray): - for i in range(repeat): + for _i in range(repeat): sess.run(None, feeds) else: out_names = [o.name for o in sess.get_outputs()] - for i in range(repeat): + for _i in range(repeat): sess._sess.run_with_ort_values(feeds, out_names, None) prof = sess.end_profiling() @@ -177,7 +179,7 @@ def _idx(row): df[c] = df[c].apply(str) df = df.copy() df["count"] = 1 - gr = df[groupkey + ["dur", "count"]].groupby(groupkey) + gr = df[[*groupkey, "dur", "count"]].groupby(groupkey) return gr.sum() @@ -187,7 +189,9 @@ def _process_shape(s: Tuple[int, ...], keys: Dict[str, str]) -> str: for v in value: if len(v) != 1: raise NotImplementedError(f"Unexpected value {v} in {s!r}.") - k, v = list(v.items())[0] + for _k, _v in v.items(): + k, v = _k, _v + break n = "-".join([keys[k], "x".join(map(str, v))]) ns.append(n) return ",".join(ns) diff --git a/onnx_array_api/ort/ort_tensors.py b/onnx_array_api/ort/ort_tensors.py index 2117e3f..4f53e6e 100644 --- a/onnx_array_api/ort/ort_tensors.py +++ b/onnx_array_api/ort/ort_tensors.py @@ -86,7 +86,7 @@ def __init__( tensor_class: type, input_names: List[str], onx: ModelProto, - f: Callable = None, + f: Optional[Callable] = None, ): try: self.ref = InferenceSession( @@ -282,5 +282,3 @@ class JitOrtTensor(OrtTensor, OrtCommon, JitTensor): """ Defines a value for :epkg:`onnxruntime` as a backend. """ - - pass diff --git a/onnx_array_api/plotting/_helper.py b/onnx_array_api/plotting/_helper.py index 3131177..5c5d881 100644 --- a/onnx_array_api/plotting/_helper.py +++ b/onnx_array_api/plotting/_helper.py @@ -94,7 +94,7 @@ def _extract_attribute_value( f"Unable to convert attribute {att.name!r} type {att.type!r}." ) raise AttributeError( # pragma: no cover - f"Unable to convert default value for {ref_att.name!r} " f"type {att.type!r}." + f"Unable to convert default value for {ref_att.name!r} type {att.type!r}." ) @@ -120,7 +120,7 @@ def get_tensor_shape(obj): for d in obj.tensor_type.shape.dim: v = d.dim_value if d.dim_value > 0 else d.dim_param shape.append(v) - shape = None if not shape else list(None if s == 0 else s for s in shape) + shape = None if not shape else [None if s == 0 else s for s in shape] return shape @@ -183,7 +183,7 @@ def _get_shape(obj): arr = to_array(obj) return arr.shape raise RuntimeError( # pragma: no cover - f"Unable to guess type from {obj0!r}, " f"data_type is {obj.data_type!r}." + f"Unable to guess type from {obj0!r}, data_type is {obj.data_type!r}." ) if hasattr(obj, "type"): obj = obj.type diff --git a/onnx_array_api/plotting/dot_plot.py b/onnx_array_api/plotting/dot_plot.py index 5bfba5d..af8ad22 100644 --- a/onnx_array_api/plotting/dot_plot.py +++ b/onnx_array_api/plotting/dot_plot.py @@ -310,7 +310,7 @@ def dot_label(text): exp.append(f' label="{node.op_type}\\n({dot_name(field)}){satts}";') exp.append(f" fontsize={fontsize};") exp.append(" color=black;") - exp.append("\n".join(map(lambda s: " " + s, subgraph.split("\n")))) + exp.append("\n".join(f" {s}" for s in subgraph.split("\n"))) node0 = body.node[0] connects.append( diff --git a/onnx_array_api/plotting/text_plot.py b/onnx_array_api/plotting/text_plot.py index 9449acb..0b4d30a 100644 --- a/onnx_array_api/plotting/text_plot.py +++ b/onnx_array_api/plotting/text_plot.py @@ -85,10 +85,8 @@ def process_node(self): ) else: ts = " ".join( - map( - lambda t: f"{t['target_id']}:{_number2str(t['weight'])}", - self.targets, - ) + f"{t['target_id']}:{_number2str(t['weight'])}" + for t in self.targets ) text = f"{self.true_false}f {ts}" else: @@ -351,7 +349,7 @@ def __init__(self, nodes): def _find_sequence(node_name, known, done): inputs = dnodes[node_name].input - if any(map(lambda i: i not in known, inputs)): + if any((i not in known) for i in inputs): return [] res = [node_name] @@ -362,7 +360,7 @@ def _find_sequence(node_name, known, done): if len(next_names) == 1: next_name = next_names.pop() inputs = dnodes[next_name].input - if any(map(lambda i: i not in known, inputs)): + if any((i not in known) for i in inputs): break res.extend(next_name) else: @@ -390,7 +388,7 @@ def _find_sequence(node_name, known, done): possibles[k] = v sequences = OrderedDict() - for k, v in possibles.items(): + for k, _v in possibles.items(): if k in done: continue sequences[k] = _find_sequence(k, known, done) @@ -826,7 +824,10 @@ def str_node(indent, node): rows.append(f"opset: domain={opset.domain!r} version={opset.version!r}") if hasattr(model, "graph"): if model.doc_string: - rows.append(f"doc_string: {model.doc_string}") + if len(model.doc_string) < 55: + rows.append(f"doc_string: {model.doc_string}") + else: + rows.append(f"doc_string: {model.doc_string[:55]}...") main_model = model model = model.graph else: @@ -863,9 +864,16 @@ def str_node(indent, node): else: content = "" line_name_new[init.name] = len(rows) + if init.doc_string: + t = ( + f"init: name={init.name!r} type={_get_type(init)} " + f"shape={_get_shape(init)}{content}" + ) + rows.append(f"{t}{' ' * max(0, 70 - len(t))}-- {init.doc_string}") + continue rows.append( - "init: name=%r type=%r shape=%r%s" - % (init.name, _get_type(init), _get_shape(init), content) + f"init: name={init.name!r} type={_get_type(init)} " + f"shape={_get_shape(init)}{content}" ) if level == 0: rows.append("----- main graph ----") @@ -941,7 +949,7 @@ def str_node(indent, node): rows.append(str_node(indent if use_indentation else 0, node)) indents[name] = indent - for i, o in enumerate(node.output): + for _i, o in enumerate(node.output): indents[o] = indent + 1 previous_indent = indents[name] @@ -1046,7 +1054,10 @@ def _mark_link(rows, lengths, r1, r2, d): for fct in main_model.functions: rows.append(f"----- function name={fct.name} domain={fct.domain}") if fct.doc_string: - rows.append(f"----- doc_string: {fct.doc_string}") + if len(fct.doc_string) < 55: + rows.append(f"----- doc_string: {fct.doc_string}") + else: + rows.append(f"----- doc_string: {fct.doc_string[:55]}...") res = onnx_simple_text_plot( fct, verbose=verbose, @@ -1105,10 +1116,19 @@ def onnx_text_plot_io(model, verbose=False, att_display=None): ) # initializer for init in model.initializer: + + if init.doc_string: + t = ( + f"init: name={init.name!r} type={_get_type(init)} " + f"shape={_get_shape(init)}" + ) + rows.append(f"{t}{' ' * max(0, 70 - len(t))}-- {init.doc_string}") + continue rows.append( - "init: name=%r type=%r shape=%r" - % (init.name, _get_type(init), _get_shape(init)) + f"init: name={init.name!r} type={_get_type(init)} " + f"shape={_get_shape(init)}" ) + # outputs for out in model.output: rows.append( diff --git a/onnx_array_api/profiling.py b/onnx_array_api/profiling.py index 52c464a..ab2cc6b 100644 --- a/onnx_array_api/profiling.py +++ b/onnx_array_api/profiling.py @@ -73,8 +73,8 @@ def _get_root(node, stor=None): stor.append(node) if not node.called_by: return node - if len(node.called_by) == 1: - return _get_root(node.called_by[0], stor=stor) + if len(node.called_by) == 0: + return None res = None for ct in node.called_by: k = id(node), id(ct) @@ -247,8 +247,7 @@ def depth_first(node, roots_keys, indent=0): else: if filter_node is not None and not filter_node(n): continue - for t in depth_first(n, roots_keys, indent + 1): - yield t + yield from depth_first(n, roots_keys, indent + 1) if filter_node is None: filter_node = ProfileNode.filter_node_ @@ -472,7 +471,7 @@ def add_rows(rows, d): def profile2df( ps: Stats, as_df: bool = True, - clean_text: bool = None, + clean_text: Optional[bool] = None, verbose: bool = False, fLOG=None, ): @@ -740,7 +739,7 @@ def fct4(): node.add_called_by(child) child.add_calls_to(node, vv) - for k, v in nodes.items(): + for _k, v in nodes.items(): root = v.get_root() break diff --git a/onnx_array_api/reference/evaluator_yield.py b/onnx_array_api/reference/evaluator_yield.py index 88c8a1f..b53c27d 100644 --- a/onnx_array_api/reference/evaluator_yield.py +++ b/onnx_array_api/reference/evaluator_yield.py @@ -3,6 +3,7 @@ from enum import IntEnum import numpy as np from onnx import ModelProto, TensorProto, ValueInfoProto, load +from onnx.reference import ReferenceEvaluator from onnx.helper import tensor_dtype_to_np_dtype from onnx.shape_inference import infer_shapes from . import to_array_extended @@ -138,17 +139,23 @@ class YieldEvaluator: :param onnx_model: model to run :param recursive: dig into subgraph and functions as well + :param cls: evaluator to use, default value is :class:`ExtendedReferenceEvaluator + ` """ def __init__( self, onnx_model: ModelProto, recursive: bool = False, - cls=ExtendedReferenceEvaluator, + cls: Optional[type[ExtendedReferenceEvaluator]] = None, ): assert not recursive, "recursive=True is not yet implemented" self.onnx_model = onnx_model - self.evaluator = cls(onnx_model) if cls is not None else None + self.evaluator = ( + cls(onnx_model) + if cls is not None + else ExtendedReferenceEvaluator(onnx_model) + ) def enumerate_results( self, @@ -166,9 +173,9 @@ def enumerate_results( Returns: iterator on tuple(result kind, name, value, node.op_type or None) """ - assert isinstance(self.evaluator, ExtendedReferenceEvaluator), ( + assert isinstance(self.evaluator, ReferenceEvaluator), ( f"This implementation only works with " - f"ExtendedReferenceEvaluator not {type(self.evaluator)}" + f"ReferenceEvaluator not {type(self.evaluator)}" ) attributes = {} if output_names is None: @@ -190,7 +197,8 @@ def enumerate_results( for i in node.input: if i not in results: raise RuntimeError( - f"Unable to find input {i!r} in known results {sorted(results)}, " + f"Unable to find input {i!r} " + f"in known results {sorted(results)}, " f"self.rt_inits_ has {sorted(self.evaluator.rt_inits_)}, " f"feed_inputs has {sorted(feed_inputs)}." ) @@ -222,7 +230,8 @@ def enumerate_results( for name in output_names: if name not in results: raise RuntimeError( - f"Unable to find output name {name!r} in {sorted(results)}, proto is\n{self.proto_}" + f"Unable to find output name {name!r} in {sorted(results)}, " + f"proto is\n{self.proto_}" ) yield ResultType.OUTPUT, name, results[name], None @@ -325,9 +334,7 @@ def _cost_type(self, t1: "np.dtype", t2: "np.dtype") -> float: def _cost_shape(self, s1: Tuple[int, ...], s2: Tuple[int, ...]) -> float: if s1 is None or s2 is None: return self.rank_cost - if any(map(lambda s: isinstance(s, str), s1)) or any( - map(lambda s: isinstance(s, str), s2) - ): + if any(isinstance(s, str) for s in s1) or any(isinstance(s, str) for s in s2): # dynamic shapes if len(s1) != len(s2): return self.rank_cost @@ -428,7 +435,10 @@ def to_str( d2 = s2[j] d = self.distance_pair(d1, d2) symbol = "=" if d == 0 else "~" - line = f"{symbol} | {_align(str(d1), column_size)} | {_align(str(d2), column_size)}" + line = ( + f"{symbol} | {_align(str(d1), column_size)} | " + f"{_align(str(d2), column_size)}" + ) if ( d1.value is not None and d2.value is not None @@ -436,7 +446,7 @@ def to_str( ): disc = discrepancies(d1.value, d2.value) a, r = disc["aerr"], disc["rerr"] - line += f" | a={a:.3f} r={r:.3f}" + line += f" | a={a:.5g} r={r:.5g}" elif i == last[0]: d2 = s2[j] line = ( @@ -457,7 +467,7 @@ def generate_input(info: ValueInfoProto) -> np.ndarray: """ elem_type = info.type.tensor_type.elem_type shape = [ - (getattr(d, "dim_value", None) or getattr(d, "dim_param")) + (getattr(d, "dim_value", None) or getattr(d, "dim_param")) # noqa: B009 for d in info.type.tensor_type.shape.dim ] new_shape = [] @@ -482,6 +492,12 @@ def generate_input(info: ValueInfoProto) -> np.ndarray: return (value.astype(np.float16) / p).astype(np.float16).reshape(new_shape) if elem_type == TensorProto.DOUBLE: return (value.astype(np.float64) / p).astype(np.float64).reshape(new_shape) + if elem_type == TensorProto.COMPLEX64: + return (value.astype(np.complex64) / p).astype(np.complex64).reshape(new_shape) + if elem_type == TensorProto.COMPLEX128: + return ( + (value.astype(np.complex128) / p).astype(np.complex128).reshape(new_shape) + ) raise RuntimeError(f"Unexpected element_type {elem_type} for info={info}") @@ -586,6 +602,7 @@ def compare_onnx_execution( raise_exc: bool = True, mode: str = "execute", keep_tensor: bool = False, + cls: Optional[type[ReferenceEvaluator]] = None, ) -> Tuple[List[ResultExecution], List[ResultExecution], List[Tuple[int, int]]]: """ Compares the execution of two onnx models. @@ -602,7 +619,9 @@ def compare_onnx_execution( :param mode: the model should be executed but the function can be executed but the comparison may append on nodes only :param keep_tensor: keeps the tensor in order to compute a precise distance - :return: four results, a sequence of results for the first model and the second model, + :param cls: evaluator class to use + :return: four results, a sequence of results + for the first model and the second model, the alignment between the two, DistanceExecution """ assert mode in {"execute", "nodes"}, f"Unexpected value for mode={mode!r}." @@ -624,7 +643,7 @@ def compare_onnx_execution( print(f"[compare_onnx_execution] execute with {len(inputs)} inputs") print("[compare_onnx_execution] execute first model") res1 = list( - YieldEvaluator(model1).enumerate_summarized( + YieldEvaluator(model1, cls=cls).enumerate_summarized( None, feeds1, raise_exc=raise_exc, keep_tensor=keep_tensor ) ) @@ -632,7 +651,7 @@ def compare_onnx_execution( print(f"[compare_onnx_execution] got {len(res1)} results") print("[compare_onnx_execution] execute second model") res2 = list( - YieldEvaluator(model2).enumerate_summarized( + YieldEvaluator(model2, cls=cls).enumerate_summarized( None, feeds2, raise_exc=raise_exc, keep_tensor=keep_tensor ) ) @@ -642,7 +661,7 @@ def compare_onnx_execution( print("[compare_onnx_execution] loading first model") proto1 = load(model1) if isinstance(model1, str) else model1 if verbose: - print("[compare_onnx_execution] loading first model") + print("[compare_onnx_execution] loading second model") proto2 = load(model2) if isinstance(model2, str) else model2 res1 = list(_enumerate_result_no_execution(proto1)) res2 = list(_enumerate_result_no_execution(proto2)) @@ -650,7 +669,8 @@ def compare_onnx_execution( return if verbose: - print(f"[compare_onnx_execution] got {len(res2)} results") + print(f"[compare_onnx_execution] got {len(res1)} results (first model)") + print(f"[compare_onnx_execution] got {len(res2)} results (second model)") print("[compare_onnx_execution] compute edit distance") dc = DistanceExecution() _, align = dc.distance_sequence(res1, res2) diff --git a/onnx_array_api/reference/ops/op_constant_of_shape.py b/onnx_array_api/reference/ops/op_constant_of_shape.py index 00c6989..a54bb5a 100644 --- a/onnx_array_api/reference/ops/op_constant_of_shape.py +++ b/onnx_array_api/reference/ops/op_constant_of_shape.py @@ -19,6 +19,8 @@ def _process(value): cst = np.int64(cst) elif isinstance(cst, float): cst = np.float64(cst) + elif isinstance(cst, complex): + cst = np.complex128(cst) elif cst is None: cst = np.float32(0) if not isinstance( @@ -27,6 +29,8 @@ def _process(value): np.float16, np.float32, np.float64, + np.complex64, + np.complex128, np.int64, np.int32, np.int16, diff --git a/onnx_array_api/tools/__init__.py b/onnx_array_api/tools/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/onnx_array_api/tools/__init__.py @@ -0,0 +1 @@ + diff --git a/onnx_array_api/tools/replace_constants.py b/onnx_array_api/tools/replace_constants.py new file mode 100644 index 0000000..daa4ca8 --- /dev/null +++ b/onnx_array_api/tools/replace_constants.py @@ -0,0 +1,227 @@ +import numpy as np +from onnx import FunctionProto, ModelProto, GraphProto, AttributeProto +from onnx.helper import ( + make_model, + set_model_props, + make_graph, + make_node, + make_attribute, + make_function, + tensor_dtype_to_np_dtype, +) +from onnx.numpy_helper import from_array + + +def replace_initializer_by_constant_of_shape( + onx, threshold=128, op_type="ConstantOfShape", domain="" +): + """ + Replaces initializers by nodes *ConstantOfShape* to reduce + the size and still write a unit test. + + :param onx: ModelProto + :param threshold: every initializer under this threshold is not impacted + :param op_type: replace by this node + :param domain: replace by this domain + :return: onx, modified ModelProto + """ + if isinstance(onx, FunctionProto): + modified = False + new_nodes = [] + for node in onx.node: + if node.op_type == "Constant": + from onnx_array_api.reference import ExtendedReferenceEvaluator + + ref = ExtendedReferenceEvaluator(node) + cst = ref.run(None, {})[0] + + size = np.prod(cst.shape) + if size <= threshold: + new_nodes.append(node) + continue + + new_name = f"{node.output[0]}__SHAPE" + new_nodes.append( + make_node( + "Constant", + [], + [new_name], + value=from_array( + np.array(cst.shape, dtype=np.int64), name=new_name + ), + ) + ) + dtype = cst.dtype + assert op_type != "Constant" + new_nodes.append( + make_node( + op_type, + [new_name], + node.output, + value=from_array(np.array([0.5], dtype=dtype)), + domain=domain, + ) + ) + modified = True + continue + + new_nodes.append(node) + + if not modified: + return onx + + onxf = make_function( + domain=onx.domain, + fname=onx.name, + inputs=onx.input, + outputs=onx.output, + nodes=new_nodes, + doc_string=onx.doc_string, + overload=onx.overload, + opset_imports=[], + ) + if onx.opset_import: + onxf.opset_import.extend(onx.opset_import) + if onx.value_info: + onxf.value_info.extend(onx.value_info) + if onx.attribute: + onxf.attribute.extend(onx.attribute) + if onx.attribute_proto: + onxf.attribute_proto.extend(onx.attribute_proto) + return onxf + + if isinstance(onx, ModelProto): + new_graph = replace_initializer_by_constant_of_shape( + onx.graph, threshold=threshold, op_type=op_type, domain=domain + ) + new_functions = [ + replace_initializer_by_constant_of_shape( + f, threshold=threshold, op_type=op_type, domain=domain + ) + for f in onx.functions + ] + model = make_model( + new_graph, + functions=new_functions, + producer_name=onx.producer_name, + producer_version=onx.producer_version, + ir_version=onx.ir_version, + doc_string=onx.doc_string, + domain=onx.domain, + model_version=onx.model_version, + ) + if len(onx.metadata_props) > 0: # pragma: no cover + values = {p.key: p.value for p in onx.metadata_props} + set_model_props(model, values) + + del model.opset_import[:] # pylint: disable=E1101 + for oimp in onx.opset_import: + op_set = model.opset_import.add() # pylint: disable=E1101 + if oimp.domain == "" and oimp.version < 9: + raise RuntimeError( + f"ConstantOfShape was introduced in " + f"opset 9 but opset is {oimp.version}." + ) + op_set.domain = oimp.domain + op_set.version = oimp.version + return model + + if not isinstance(onx, GraphProto): + raise TypeError(f"onx should be a GraphProto as this stage not {type(onx)}.") + + new_nodes = [] + removed = set() + additional_inputs = [] + + new_inits = [] + for init in onx.initializer: + dims = tuple(init.dims) + size = np.prod(dims) + if size <= threshold: + new_inits.append(init) + continue + new_name = f"{init.name}__SHAPE" + new_inits.append( + from_array(np.array(list(dims), dtype=np.int64), name=new_name) + ) + dtype = tensor_dtype_to_np_dtype(init.data_type) + node = make_node( + op_type, + [new_name], + [init.name], + value=from_array(np.array([0.5], dtype=dtype)), + domain=domain, + ) + new_nodes.append(node) + removed.add(init.name) + + new_sparse_inits = [] + for init in onx.sparse_initializer: + dims = tuple(init.dims) + size = np.prod(dims) + if size <= threshold: + new_sparse_inits.append(init) + continue + raise NotImplementedError( + f"This feature is not yet implemented for sparse initializer" + f"(name={init.name!r})." + ) + + for node in onx.node: + if node.op_type == "Constant": + from onnx_array_api.reference import ExtendedReferenceEvaluator + + ref = ExtendedReferenceEvaluator(node) + cst = ref.run(None, {})[0] + + size = np.prod(cst.shape) + if size <= threshold: + new_nodes.append(node) + continue + + new_name = f"{node.output[0]}__SHAPE" + new_inits.append( + from_array(np.array(cst.shape, dtype=np.int64), name=new_name) + ) + dtype = cst.dtype + new_nodes.append( + make_node( + op_type, + [new_name], + node.output, + value=from_array(np.array([0.5], dtype=dtype)), + domain=domain, + ) + ) + continue + + modified = False + atts = [] + for att in node.attribute: + if ( + att.type == AttributeProto.GRAPH + and hasattr(att, "g") + and att.g is not None + ): + modified = True + g = replace_initializer_by_constant_of_shape( + att.g, threshold=threshold, op_type=op_type, domain=domain + ) + att = make_attribute(att.name, g) + atts.append(att) + if modified: + new_node = make_node(node.op_type, node.input, node.output) + new_node.attribute.extend(atts) + new_nodes.append(new_node) + else: + new_nodes.append(node) + + graph = make_graph( + new_nodes, + onx.name, + [i for i in onx.input if i.name not in removed] + additional_inputs, + onx.output, + initializer=new_inits, + sparse_initializer=new_sparse_inits, + ) + return graph diff --git a/onnx_array_api/translate_api/__init__.py b/onnx_array_api/translate_api/__init__.py index 25daef6..a9a8932 100644 --- a/onnx_array_api/translate_api/__init__.py +++ b/onnx_array_api/translate_api/__init__.py @@ -1,6 +1,7 @@ from onnx import ModelProto from .translate import Translater -from .inner_emitter import InnerEmitter +from .inner_emitter import InnerEmitter, InnerEmitterShortInitializer +from .builder_emitter import BuilderEmitter def translate(proto: ModelProto, single_line: bool = False, api: str = "light") -> str: @@ -14,7 +15,9 @@ def translate(proto: ModelProto, single_line: bool = False, api: str = "light") default is `"light"` and this is handle by class :class:`onnx_array_api.translate_api.light_emitter.LightEmitter`, another value is `"onnx"` which is the inner API implemented - in onnx package. + in onnx package, `"builder"` follows the syntax for the + class :class:`onnx_array_api.graph_api.GraphBuilder`, + `"onnx-short"` replaces long initializer with random values :return: code .. runpython:: @@ -35,7 +38,7 @@ def translate(proto: ModelProto, single_line: bool = False, api: str = "light") code = translate(onx) print(code) - The inner API from onnx packahe is also available. + The inner API from onnx package is also available. .. runpython:: :showcode: @@ -54,6 +57,27 @@ def translate(proto: ModelProto, single_line: bool = False, api: str = "light") ) code = translate(onx, api="onnx") print(code) + + The :class:`GraphBuilder + ` API returns this: + + .. runpython:: + :showcode: + + from onnx_array_api.light_api import start + from onnx_array_api.translate_api import translate + + onx = ( + start() + .vin("X") + .reshape((-1, 1)) + .Transpose(perm=[1, 0]) + .rename("Y") + .vout() + .to_onnx() + ) + code = translate(onx, api="builder") + print(code) """ if api == "light": tr = Translater(proto) @@ -61,4 +85,10 @@ def translate(proto: ModelProto, single_line: bool = False, api: str = "light") if api == "onnx": tr = Translater(proto, emitter=InnerEmitter()) return tr.export(as_str=True) + if api == "onnx-short": + tr = Translater(proto, emitter=InnerEmitterShortInitializer()) + return tr.export(as_str=True) + if api == "builder": + tr = Translater(proto, emitter=BuilderEmitter()) + return tr.export(as_str=True) raise ValueError(f"Unexpected value {api!r} for api.") diff --git a/onnx_array_api/translate_api/base_emitter.py b/onnx_array_api/translate_api/base_emitter.py index 3a0dfb6..e8d3811 100644 --- a/onnx_array_api/translate_api/base_emitter.py +++ b/onnx_array_api/translate_api/base_emitter.py @@ -21,6 +21,14 @@ class EventType(IntEnum): FUNCTION_OUTPUT = 12 FUNCTION_ATTRIBUTES = 13 TO_ONNX_FUNCTION = 14 + BEGIN_SIGNATURE = 15 + END_SIGNATURE = 16 + BEGIN_RETURN = 17 + END_RETURN = 18 + BEGIN_FUNCTION_SIGNATURE = 19 + END_FUNCTION_SIGNATURE = 20 + BEGIN_FUNCTION_RETURN = 21 + END_FUNCTION_RETURN = 22 @classmethod def to_str(cls, self) -> str: @@ -72,6 +80,12 @@ def __call__(self, event: EventType, **kwargs: Dict[str, Any]) -> List[str]: if event == EventType.BEGIN_FUNCTION: return self._emit_begin_function(**kwargs) + if event == EventType.BEGIN_FUNCTION_SIGNATURE: + return self._emit_begin_function_signature(**kwargs) + + if event == EventType.END_FUNCTION_SIGNATURE: + return self._emit_end_function_signature(**kwargs) + if event == EventType.END_FUNCTION: return self._emit_end_function(**kwargs) @@ -84,6 +98,24 @@ def __call__(self, event: EventType, **kwargs: Dict[str, Any]) -> List[str]: if event == EventType.FUNCTION_ATTRIBUTES: return self._emit_function_attributes(**kwargs) + if event == EventType.BEGIN_SIGNATURE: + return self._emit_begin_signature(**kwargs) + + if event == EventType.END_SIGNATURE: + return self._emit_end_signature(**kwargs) + + if event == EventType.BEGIN_RETURN: + return self._emit_begin_return(**kwargs) + + if event == EventType.END_RETURN: + return self._emit_end_return(**kwargs) + + if event == EventType.BEGIN_FUNCTION_RETURN: + return self._emit_begin_function_return(**kwargs) + + if event == EventType.END_FUNCTION_RETURN: + return self._emit_end_function_return(**kwargs) + raise ValueError(f"Unexpected event {EventType.to_str(event)}.") def render_attribute_value(self, value: Any) -> Tuple[List[str], str]: @@ -208,6 +240,12 @@ def _emit_begin_function(self, **kwargs: Dict[str, Any]) -> List[str]: f"Method {inspect.currentframe().f_code.co_name!r} was not overloaded." ) + def _emit_begin_function_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_function_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + def _emit_function_input(self, **kwargs: Dict[str, Any]) -> List[str]: raise NotImplementedError( f"Method {inspect.currentframe().f_code.co_name!r} was not overloaded." @@ -222,3 +260,21 @@ def _emit_function_attributes(self, **kwargs: Dict[str, Any]) -> List[str]: raise NotImplementedError( f"Method {inspect.currentframe().f_code.co_name!r} was not overloaded." ) + + def _emit_begin_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_begin_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_begin_function_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_function_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] diff --git a/onnx_array_api/translate_api/builder_emitter.py b/onnx_array_api/translate_api/builder_emitter.py new file mode 100644 index 0000000..19dd7f9 --- /dev/null +++ b/onnx_array_api/translate_api/builder_emitter.py @@ -0,0 +1,242 @@ +from typing import Any, Dict, List +from onnx import TensorProto +from onnx.numpy_helper import to_array +from .base_emitter import BaseEmitter + +_types = { + TensorProto.DOUBLE: "DOUBLE", + TensorProto.FLOAT: "FLOAT", + TensorProto.FLOAT16: "FLOAT16", + TensorProto.INT64: "INT64", + TensorProto.INT32: "INT32", + TensorProto.INT16: "INT16", + TensorProto.UINT64: "UINT64", + TensorProto.UINT32: "UINT32", + TensorProto.UINT16: "UINT16", + TensorProto.STRING: "STRING", + TensorProto.BOOL: "BOOL", +} + + +def _itype_to_string(itype: int) -> str: + return _types[itype] + + +class BuilderEmitter(BaseEmitter): + """ + Converts event into proper code. + """ + + def __init__(self, make_model_function: str = ""): + super().__init__() + self.make_model_function = make_model_function + + def join(self, rows: List[str], single_line: bool = False) -> str: + "Join the rows" + assert ( + not single_line + ), f"The emitter {type(self)} does not work with single_line=True." + return "\n".join(rows) + + def _emit_start(self, **kwargs: Dict[str, Any]) -> List[str]: + self.opsets = kwargs.get("opsets", {}) + self.ir_version = kwargs.get("ir_version", None) + self.function_calls = [] + return [] + + def _emit_to_onnx_model(self, **kwargs: Dict[str, Any]) -> List[str]: + inps = ", ".join(["g.op", *[f'"{i}"' for i in self.inputs]]) + inputs = [] + for inp, stype, shape in self.inputs_full_: + inputs.append(f'g.make_tensor_input("{inp}", TensorProto.{stype}, {shape})') + outputs = [] + for inp, stype, shape in self.outputs_full_: + outputs.append( + f'g.make_tensor_output("{inp}", TensorProto.{stype}, ' + f"{shape}, is_dimension=False, indexed=False)" + ) + rows = [ + "", + ( + f"g = GraphBuilder({self.opsets}, ir_version={self.ir_version})" + if self.ir_version + else f"GraphBuilder({self.opsets})" + ), + *inputs, + f"{self.name}({inps})", + *outputs, + *self.function_calls, + "model = g.to_onnx()", + ] + if self.make_model_function: + rows = [ + "", + "", + f'def {self.make_model_function}() -> "ModelProto":', + *[" " + _ for _ in rows[1:]], + " return model", + "", + "", + f"model = {self.make_model_function}()", + ] + return rows + + def _emit_begin_graph(self, **kwargs: Dict[str, Any]) -> List[str]: + self.inputs = [] + self.inputs_full = [] + self.outputs = [] + self.inits = [] + self.inputs_full_ = [] + self.outputs_full_ = [] + self.name = kwargs.get("name", "make_graph") + return [] + + def _emit_end_graph(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_initializer(self, **kwargs: Dict[str, Any]) -> List[str]: + init = kwargs["init"] + if isinstance(init, TensorProto): + assert ( + kwargs["name"] == init.name + ), f"Name mismatch init.name={init.name!r}, name={kwargs['name']!r}" + self.inits.append(init) + return [] + raise AssertionError(f"Unsupported type for an initializer {type(init)}") + + def _emit_input(self, **kwargs: Dict[str, Any]) -> List[str]: + name = kwargs["name"] + itype = kwargs.get("elem_type", 0) + shape = kwargs.get("shape", None) + name = self._clean_result_name(name) + if itype == 0: + inp = name or "X" + else: + if shape is None: + inp = f'{name}: "{_itype_to_string(itype)}"' + else: + inp = ( + f'{name}: "{_itype_to_string(itype)}[{", ".join(map(str, shape))}]"' + ) + self.inputs_full.append(inp) + self.inputs.append(name) + self.inputs_full_.append((name, _itype_to_string(itype), shape)) + return [] + + def _emit_begin_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + rows = ["", f"def {self.name}(", ' op: "GraphBuilder",'] + for i in self.inputs_full: + rows.append(f" {i},") + rows.append("):") + for init in self.inits: + val = to_array(init) + stype = str(val.dtype).split(".")[-1] + name = self._clean_result_name(init.name) + rows.append(f" {name} = np.array({val.tolist()}, dtype=np.{stype})") + return rows + + def _emit_begin_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_return(self, **kwargs: Dict[str, Any]) -> List[str]: + outs = ", ".join(self.outputs) + return [f" return {outs}"] + + def _emit_output(self, **kwargs: Dict[str, Any]) -> List[str]: + name = kwargs["name"] + name = self._clean_result_name(name) + itype = kwargs.get("elem_type", 0) + shape = kwargs.get("shape", None) + self.outputs.append(name) + self.outputs_full_.append((name, _itype_to_string(itype), shape)) + return [f' op.Identity({name}, outputs=["{name}"])'] + + def _emit_node(self, **kwargs: Dict[str, Any]) -> List[str]: + op_type = kwargs["op_type"] + inputs = kwargs["inputs"] + outputs = kwargs["outputs"] + domain = kwargs.get("domain", "") + atts = kwargs.get("atts", {}) + args = [] + for k, v in atts.items(): + before, vatt = self.render_attribute_value(v) + if before: + raise NotImplementedError("Graph attribute not supported yet.") + args.append(f"{k}={vatt}") + + cleaned_outputs = list(map(self._clean_result_name, outputs)) + outs = ", ".join(cleaned_outputs) + inps = ", ".join(map(self._clean_result_name, inputs)) + op_type = self._emit_node_type(op_type, domain) + # Let's add output names to make it easier to debug. + soutputs = f", outputs={cleaned_outputs}" + sdomain = soutputs if not domain else f", domain={domain!r}{soutputs}" + if args: + sargs = ", ".join(args) + if inps: + row = f" {outs} = op.{op_type}({inps}, {sargs}{sdomain})" + else: + row = f" {outs} = op.{op_type}({sargs}{sdomain})" + else: + row = f" {outs} = op.{op_type}({inps}{sdomain})" + return [row] + + def _clean_result_name(self, name): + return name + + def _emit_node_type(self, op_type, domain): + return op_type + + def _emit_begin_function(self, **kwargs: Dict[str, Any]) -> List[str]: + self.f_inputs = [] + self.f_outputs = [] + self.f_inits = [] + self.f_name = kwargs["name"] + self.f_domain = kwargs["domain"] + self.f_attributes = [] + self.f_opsets = kwargs["opsets"] + return [] + + def _emit_begin_function_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_function_signature(self, **kwargs: Dict[str, Any]) -> List[str]: + self.f_call_name = f"make_{self.f_domain}_{self.f_name}" + return [ + "", + "", + f'def {self.f_call_name}(g: "GraphBuilder"):', + f" gr = GraphBuilder({self.f_opsets}, as_function=True)", + *[f" {name} = gr.make_tensor_input({name!r})" for name in self.f_inputs], + " op = gr.op", + ] + + def _emit_to_onnx_function(self, **kwargs: Dict[str, Any]) -> List[str]: + return [" return gr"] + + def _emit_function_input(self, **kwargs: Dict[str, Any]) -> List[str]: + self.f_inputs.append(kwargs["name"]) + return [] + + def _emit_function_output(self, **kwargs: Dict[str, Any]) -> List[str]: + self.f_outputs.append(kwargs["name"]) + return [] + + def _emit_function_attributes(self, **kwargs: Dict[str, Any]) -> List[str]: + raise NotImplementedError("Function attribute are not implemented yet.") + + def _emit_end_function(self, **kwargs: Dict[str, Any]) -> List[str]: + self.function_calls.append(f"{self.f_call_name}(g)") + return [ + *[f" gr.make_tensor_output({name})" for name in self.f_outputs], + " g.add_function(builder=gr)", + ] + + def _emit_begin_function_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] + + def _emit_end_function_return(self, **kwargs: Dict[str, Any]) -> List[str]: + return [] diff --git a/onnx_array_api/translate_api/inner_emitter.py b/onnx_array_api/translate_api/inner_emitter.py index 50d4f5e..de63dcc 100644 --- a/onnx_array_api/translate_api/inner_emitter.py +++ b/onnx_array_api/translate_api/inner_emitter.py @@ -38,7 +38,10 @@ def _make_attribute( raise NotImplementedError( f"Cannot create attribute with name={name!r}, attr_type={attr_type}." ) - return f"make_ref_attribute(key={name!r}, attr_type={attr_type}, ref_attr_name={ref_attr_name!r})" + return ( + f"make_ref_attribute(key={name!r}, attr_type={attr_type}, " + f"ref_attr_name={ref_attr_name!r})" + ) def join(self, rows: List[str], single_line: bool = False) -> str: "Returns the separators. `single_line` is unused." @@ -103,6 +106,7 @@ def _emit_initializer(self, **kwargs: Dict[str, Any]) -> List[str]: raise NotImplementedError(f"Unexpected dtype={sdtype}.") else: sdtype = f"np.{sdtype}" + return [ "initializers.append(", f" {fra}(", @@ -118,14 +122,17 @@ def _emit_io(self, container: str, **kwargs: Dict[str, Any]) -> List[str]: shape = kwargs.get("shape", None) if elem_type and shape: return [ - f"{container}.append(make_tensor_value_info({name!r}, TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, shape={shape!r}))" + f"{container}.append(make_tensor_value_info({name!r}, " + f"TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, shape={shape!r}))" ] if elem_type: return [ - f"{container}.append(make_tensor_value_info({name!r}, TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, shape=[]))" + f"{container}.append(make_tensor_value_info({name!r}, " + f"TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, shape=[]))" ] return [ - f"{container}.append(make_tensor_value_info({name!r}, TensorProto.UNDEFINED, []))" + f"{container}.append(make_tensor_value_info({name!r}, " + f"TensorProto.UNDEFINED, []))" ] def _emit_input(self, **kwargs: Dict[str, Any]) -> List[str]: @@ -184,7 +191,7 @@ def _emit_function_output(self, **kwargs: Dict[str, Any]) -> List[str]: def _emit_function_attributes(self, **kwargs: Dict[str, Any]) -> List[str]: atts = kwargs["attributes"] - if isinstance(atts, list) and all(map(lambda t: isinstance(t, str), atts)): + if isinstance(atts, list) and all(isinstance(t, str) for t in atts): return [f"atts.extend({atts!r})"] raise NotImplementedError(f"Unable to process function attributes {atts!r}.") @@ -203,3 +210,57 @@ def _emit_end_function(self, **kwargs: Dict[str, Any]) -> List[str]: ")", ] return lines + + +class InnerEmitterShortInitializer(InnerEmitter): + """ + Converts event into proper code. + Initializer are replaced by random values if too big. + """ + + def _emit_initializer(self, **kwargs: Dict[str, Any]) -> List[str]: + name = kwargs["name"] + value = kwargs["value"] + repl = {"bool": "bool_", "object": "object_", "str": "str_"} + fra = "from_array" + sdtype = repl.get(str(value.dtype), str(value.dtype)) + if sdtype.startswith("("): + from onnx.reference.custom_element_types import float8e4m3fn + + if sdtype == str(float8e4m3fn): + sdtype = "float8e4m3fn" + fra = "from_array_extended" + else: + raise NotImplementedError(f"Unexpected dtype={sdtype}.") + else: + sdtype = f"np.{sdtype}" + if value.size <= 16: + return [ + "initializers.append(", + f" {fra}(", + f" np.array({value.tolist()}, dtype={sdtype}),", + f" name={name!r}", + " )", + ")", + ] + if "int" in sdtype: + return [ + f"value = np.random.randint(0, 10, size={value.shape})" + f".astype({sdtype})", + "initializers.append(", + f" {fra}(", + f" np.array(value, dtype={sdtype}),", + f" name={name!r}", + " )", + ")", + ] + return [ + f"value = np.random.randn({', '.join(map(str,value.shape))})" + f".astype({sdtype})", + "initializers.append(", + f" {fra}(", + f" np.array(value, dtype={sdtype}),", + f" name={name!r}", + " )", + ")", + ] diff --git a/onnx_array_api/translate_api/light_emitter.py b/onnx_array_api/translate_api/light_emitter.py index 7a7aef9..9c58830 100644 --- a/onnx_array_api/translate_api/light_emitter.py +++ b/onnx_array_api/translate_api/light_emitter.py @@ -54,7 +54,8 @@ def _emit_input(self, **kwargs: Dict[str, Any]) -> List[str]: shape = kwargs.get("shape", None) if elem_type and shape: return [ - f"vin({name!r}, elem_type=TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, shape={shape!r})" + f"vin({name!r}, elem_type=TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, " + f"shape={shape!r})" ] if elem_type: return [ @@ -71,7 +72,8 @@ def _emit_output(self, **kwargs: Dict[str, Any]) -> List[str]: shape = kwargs.get("shape", None) if elem_type and shape: inst.append( - f"vout(elem_type=TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, shape={shape!r})" + f"vout(elem_type=TensorProto.{ELEMENT_TYPE_NAME[elem_type]}, " + f"shape={shape!r})" ) elif elem_type: inst.append(f"vout(elem_type=TensorProto.{ELEMENT_TYPE_NAME[elem_type]})") diff --git a/onnx_array_api/translate_api/translate.py b/onnx_array_api/translate_api/translate.py index 31c1bce..81d515a 100644 --- a/onnx_array_api/translate_api/translate.py +++ b/onnx_array_api/translate_api/translate.py @@ -35,7 +35,11 @@ def export(self, as_str, single_line: bool = False) -> Union[str, List[str]]: last_event = None if isinstance(self.proto_, ModelProto): opsets = {d.domain: d.version for d in self.proto_.opset_import} - rows.extend(self.emitter(EventType.START, opsets=opsets)) + rows.extend( + self.emitter( + EventType.START, opsets=opsets, ir_version=self.proto_.ir_version + ) + ) inputs = self.proto_.graph.input outputs = self.proto_.graph.output nodes = self.proto_.graph.node @@ -73,10 +77,15 @@ def export(self, as_str, single_line: bool = False) -> Union[str, List[str]]: EventType.BEGIN_FUNCTION, name=self.proto_.name, domain=self.proto_.domain, + opsets={d.domain: d.version for d in self.proto_.opset_import}, ) ) + elif isinstance(self.proto_, GraphProto): + rows.extend(self.emitter(EventType.BEGIN_GRAPH, name=self.proto_.name)) else: - rows.extend(self.emitter(EventType.BEGIN_GRAPH)) + rows.extend( + self.emitter(EventType.BEGIN_GRAPH, name=self.proto_.graph.name) + ) for i in initializers: rows.extend( @@ -88,6 +97,14 @@ def export(self, as_str, single_line: bool = False) -> Union[str, List[str]]: ) ) + rows.extend( + self.emitter( + EventType.BEGIN_FUNCTION_SIGNATURE + if is_function + else EventType.BEGIN_SIGNATURE + ) + ) + for i in inputs: if is_function: rows.extend(self.emitter(EventType.FUNCTION_INPUT, name=i)) @@ -109,6 +126,14 @@ def export(self, as_str, single_line: bool = False) -> Union[str, List[str]]: self.emitter(EventType.FUNCTION_ATTRIBUTES, attributes=list(attributes)) ) + rows.extend( + self.emitter( + EventType.END_FUNCTION_SIGNATURE + if is_function + else EventType.END_SIGNATURE + ) + ) + for node in nodes: atts = self.extract_attributes(node) rows.extend( @@ -122,6 +147,14 @@ def export(self, as_str, single_line: bool = False) -> Union[str, List[str]]: ) ) + rows.extend( + self.emitter( + EventType.BEGIN_FUNCTION_RETURN + if is_function + else EventType.BEGIN_RETURN + ) + ) + for o in outputs: if is_function: rows.extend(self.emitter(EventType.FUNCTION_OUTPUT, name=o)) @@ -137,6 +170,13 @@ def export(self, as_str, single_line: bool = False) -> Union[str, List[str]]: ), ) ) + + rows.extend( + self.emitter( + EventType.END_FUNCTION_RETURN if is_function else EventType.END_RETURN + ) + ) + if isinstance(self.proto_, (GraphProto, FunctionProto)): name = self.proto_.name else: diff --git a/onnx_array_api/validation/f8.py b/onnx_array_api/validation/f8.py index ecd68f8..13b778d 100644 --- a/onnx_array_api/validation/f8.py +++ b/onnx_array_api/validation/f8.py @@ -9,8 +9,6 @@ class UndefinedCastError(FloatingPointError): Unable to case a number. """ - pass - def display_int(ival, sign=1, exponent=8, mantissa=23): """ @@ -317,25 +315,23 @@ def fe5m2_to_float32(ival: int, fn: bool = False, uz: bool = False) -> float: class CastFloat8Sets: values_e4m3fn = list( sorted( - (fe4m3_to_float32_float(i), i) for i in range(0, 256) if i not in (255, 127) + (fe4m3_to_float32_float(i), i) for i in range(256) if i not in (255, 127) ) ) values_e4m3fnuz = list( - sorted( - (fe4m3_to_float32_float(i, uz=True), i) for i in range(0, 256) if i != 0x80 - ) + sorted((fe4m3_to_float32_float(i, uz=True), i) for i in range(256) if i != 0x80) ) values_e5m2 = list( sorted( (fe5m2_to_float32_float(i), i) - for i in range(0, 256) + for i in range(256) if i not in {253, 254, 255, 125, 126, 127} ) ) values_e5m2fnuz = list( sorted( (fe5m2_to_float32_float(i, fn=True, uz=True), i) - for i in range(0, 256) + for i in range(256) if i != 0x80 ) ) diff --git a/onnx_array_api/validation/tools.py b/onnx_array_api/validation/tools.py index 6cd1da3..cbb02c1 100644 --- a/onnx_array_api/validation/tools.py +++ b/onnx_array_api/validation/tools.py @@ -20,7 +20,7 @@ def randomize_proto( - onx: Union[ModelProto, GraphProto, FunctionProto, NodeProto, TensorProto] + onx: Union[ModelProto, GraphProto, FunctionProto, NodeProto, TensorProto], ) -> Union[ModelProto, GraphProto, FunctionProto, NodeProto, TensorProto]: """ Randomizes float initializers or constant nodes. diff --git a/pyproject.toml b/pyproject.toml index 525b648..a465006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,36 @@ exclude = [ # Same as Black. line-length = 88 -[tool.ruff.lint.mccabe] -# Unlike Flake8, default to a complexity level of 10. -max-complexity = 10 +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + #"D", # pydocstyle + "E", # pycodestyle + "F", # Pyflakes + "G", # flake8-logging-format + #"I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + #"N", # pep8-naming + #"NPY", # modern numpy + #"PERF", # Perflint + "PIE", # flake8-pie + "PYI", # flake8-pyi + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLOT", # flake8-slot + "T10", # flake8-debugger + #"TID", # Disallow relative imports + #"TRY", # flake8-try-except-raise + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] [tool.ruff.lint.per-file-ignores] +"**" = ["B905", "C401", "C408", "C413", "PYI041", "RUF012", "RUF100", "RUF010", "SIM108", "SIM910", "SIM110", "SIM102", "SIM114", "SIM103", "UP015", "UP027", "UP031", "UP034", "UP032", "UP006", "UP035", "UP007", "UP038"] +"**/plot*.py" = ["B018"] "_doc/examples/plot_first_example.py" = ["E402", "F811"] "_doc/examples/plot_onnxruntime.py" = ["E402", "F811"] "onnx_array_api/array_api/_onnx_common.py" = ["F821"] @@ -34,4 +59,5 @@ max-complexity = 10 "onnx_array_api/profiling.py" = ["E731"] "onnx_array_api/reference/__init__.py" = ["F401"] "_unittests/ut_npx/test_npx.py" = ["F821"] +"_unittests/ut_translate_api/test_translate_classic.py" = ["E501"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 5e262e3..de339f5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ +array_api_compat +array_api_strict autopep8 black coverage diff --git a/requirements.txt b/requirements.txt index 4680cfc..4396e32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -array_api_compat numpy onnx>=1.15.0 scipy diff --git a/setup.py b/setup.py index bc4e87e..b4cced8 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os from setuptools import setup @@ -63,9 +62,10 @@ "Operating System :: Unix", "Operating System :: MacOS", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], )