# # SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # from collections import OrderedDict import numpy as np import onnx import onnx.numpy_helper import onnx.shape_inference import pytest from onnx_graphsurgeon.importers.onnx_importer import OnnxImporter from onnx_graphsurgeon.ir.tensor import Constant, Tensor, Variable, SparseValues from onnx_graphsurgeon.ir.function import Function from onnx_graphsurgeon.ir.node import Node from onnx_graphsurgeon.logger import G_LOGGER from onnx_models import ( dim_param_model, ext_weights, identity_model, initializer_is_output_model, lstm_model, nested_dup_names, scan_model, sparse_nnz_model, sparse_nnz_rank_model, ) G_LOGGER.severity = G_LOGGER.ULTRA_VERBOSE class TestOnnxImporter(object): @pytest.mark.parametrize( "onnx_type, expected_type", [ (onnx.TensorProto.FLOAT, np.float32), (onnx.TensorProto.BFLOAT16, onnx.TensorProto.BFLOAT16), (onnx.TensorProto.FLOAT8E4M3FN, onnx.TensorProto.FLOAT8E4M3FN), (onnx.TensorProto.FLOAT8E4M3FNUZ, onnx.TensorProto.FLOAT8E4M3FNUZ), (onnx.TensorProto.FLOAT8E5M2, onnx.TensorProto.FLOAT8E5M2), (onnx.TensorProto.FLOAT8E5M2FNUZ, onnx.TensorProto.FLOAT8E5M2FNUZ), (onnx.TensorProto.FLOAT4E2M1, onnx.TensorProto.FLOAT4E2M1), ], ) def test_import_variable_tensor(self, onnx_type, expected_type): name = "test0" shape = (1, 2, 3, 4) onnx_tensor = onnx.helper.make_tensor_value_info(name, onnx_type, shape) tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Variable assert tensor.name == name assert tensor.dtype == expected_type assert tuple(tensor.shape) == shape def test_import_constant_tensor(self): shape = (3, 3, 3) dtype = np.float32 onnx_tensor = onnx.numpy_helper.from_array(np.ones(shape=shape, dtype=dtype)) tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Constant assert tensor.dtype == dtype assert tuple(tensor.shape) == shape def test_import_tensor_unknown_metadata(self): name = "test0" onnx_tensor = onnx.helper.make_empty_tensor_value_info(name) tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Variable assert tensor.name == name # An empty string in `dim_param` should be treated like a dynamic dimension def test_import_empty_dim_param_tensor(self): shape = (1, 2, "non-empty", "") onnx_tensor = onnx.helper.make_tensor_value_info( "test0", onnx.TensorProto.FLOAT, shape ) tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Variable assert tuple(tensor.shape) == shape # Sometimes, tensor shape is not known, in which case we shouldn't import it def test_import_unknown_shape_tensor(self): shape = None onnx_tensor = onnx.helper.make_tensor_value_info( "test0", onnx.TensorProto.FLOAT, shape ) tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Variable assert tensor.shape is None # Scalars can be represented in ONNX with a dim that includes neither a dim_param nor dim_value def test_import_empty_dim_tensor(self): shape = (None,) onnx_tensor = onnx.helper.make_tensor_value_info( "test0", onnx.TensorProto.FLOAT, shape ) onnx_tensor.type.tensor_type.shape.dim[0].ClearField("dim_value") onnx_tensor.type.tensor_type.shape.dim[0].ClearField("dim_param") tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Variable assert tuple(tensor.shape) == shape # TODO: Test all attribute types - missing graph def test_import_node(self): op = "Test" inputs = ["x"] outputs = ["y"] float_attr = 4.0 int_attr = 10 str_attr = "constant" tensor_vals = np.ones(shape=(1, 2, 3, 4), dtype=np.float32) tensor_attr = onnx.numpy_helper.from_array(tensor_vals) floats_attr = [1.0, 2.0, 3.0, 4.0] ints_attr = [4, 3, 2, 1] strings_attr = ["constant", "and", "variable"] onnx_node = onnx.helper.make_node( op, inputs, outputs, float_attr=float_attr, int_attr=int_attr, str_attr=str_attr, tensor_attr=tensor_attr, floats_attr=floats_attr, ints_attr=ints_attr, strings_attr=strings_attr, ) node = OnnxImporter.import_node( onnx_node, OrderedDict(), OrderedDict(), opset=11, import_domains=None ) assert node.op == op assert node.attrs["float_attr"] == float_attr assert node.attrs["int_attr"] == int_attr assert node.attrs["str_attr"] == str_attr # Tensor should turn into a Constant assert np.all(node.attrs["tensor_attr"].values == tensor_vals) assert node.attrs["floats_attr"] == floats_attr assert node.attrs["ints_attr"] == ints_attr assert node.attrs["strings_attr"] == strings_attr def test_import_node_ref_attrs(self): op = "Test" inputs = ["x"] outputs = ["y"] attrs = {"attr1": 1, "attr2": 2.0} referencing_attr = "attr3" referenced_attr = "attr4" onnx_node = onnx.helper.make_node(op, inputs, outputs, **attrs) onnx_attr_ref = onnx.helper.make_attribute_ref( referencing_attr, onnx.AttributeProto.FLOAT ) onnx_attr_ref.ref_attr_name = referenced_attr onnx_node.attribute.append(onnx_attr_ref) node = OnnxImporter.import_node( onnx_node, OrderedDict(), OrderedDict(), opset=11, import_domains=None ) assert node.op == op assert node.attrs["attr1"] == 1 assert node.attrs["attr2"] == 2.0 assert node.attrs["attr3"] == Node.AttributeRef(referenced_attr, float) def test_import_function(self): name = "Test" domain = "com.test" inputs = ["X", "Y"] outputs = ["Z"] nodes = [ onnx.helper.make_node("Add", ["X", "Y"], ["V"], attr1=1), onnx.helper.make_node("Add", ["X", "V"], ["W"], attr2=2), onnx.helper.make_node("Mul", ["V", "W"], ["Z"], attr3=3), ] opset = 18 opset_imports = [onnx.helper.make_operatorsetid("ai.onnx", opset)] attributes = ["attr1", "attr2"] attribute_protos = [onnx.helper.make_attribute("attr3", 3)] doc_string = "docstring" onnx_function = onnx.helper.make_function( domain, name, inputs, outputs, nodes, opset_imports, attributes=attributes, attribute_protos=attribute_protos, doc_string=doc_string, ) func = OnnxImporter.import_function(onnx_function) assert type(func) == Function assert func.name == name assert func.domain == domain assert func.doc_string == doc_string assert list(func.import_domains) == list(opset_imports) assert set(func.attrs.keys()) == set(attributes) | { a.name for a in attribute_protos } assert func.opset == opset assert all([isinstance(t, Tensor) for t in func.inputs + func.outputs]) assert sorted(inputs) == sorted([t.name for t in func.inputs]) assert sorted(outputs) == sorted([t.name for t in func.outputs]) assert sorted([n.op_type for n in nodes]) == sorted([n.op for n in func.nodes]) assert attribute_protos[0].i == func.attrs[attribute_protos[0].name] @pytest.mark.parametrize( "model", [ identity_model(), lstm_model(), scan_model(), dim_param_model(), initializer_is_output_model(), nested_dup_names(), ext_weights(), ], ids=lambda model: str(model), ) def test_import_graph(self, model): graph = OnnxImporter.import_graph(model.load().graph) model.assert_equal(graph) def test_import_graph_value_info(self): model = onnx.shape_inference.infer_shapes(identity_model().load()) graph = OnnxImporter.import_graph(model.graph) tensors = graph.tensors() assert all( [ type(tensor) == Variable and tensor.dtype is not None and tensor.shape for tensor in tensors.values() ] ) def test_import_graph_tensor_map_preserved(self): model = identity_model() tensor_map = OrderedDict() graph = OnnxImporter.import_graph(model.load().graph, tensor_map=tensor_map) assert len(tensor_map) == 0 model.assert_equal(graph) def test_import_graph_with_initializer(self): model = lstm_model() graph = OnnxImporter.import_graph(model.load().graph) model.assert_equal(graph) def test_import_graph_with_dim_param(self): model = dim_param_model() graph = OnnxImporter.import_graph(model.load().graph) model.assert_equal(graph) def test_import_graph_with_sparse_nnz_rank(self): model = sparse_nnz_rank_model() graph = OnnxImporter.import_graph(model.load().graph) tensors = graph.tensors() assert "w_sparse" in tensors sparse_tensor = tensors["w_sparse"] ref_value = np.array( [ 1.0, 2.0, 3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ] ).reshape(sparse_tensor._values.shape) assert ( type(sparse_tensor) == Constant and type(sparse_tensor._values) == SparseValues ) assert (tensors["w_sparse"]._values.load() == ref_value).all() def test_import_graph_with_sparse_nnz(self): model = sparse_nnz_model() graph = OnnxImporter.import_graph(model.load().graph) tensors = graph.tensors() assert "w_sparse" in tensors sparse_tensor = tensors["w_sparse"] ref_value = np.array( [ 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ] ).reshape(sparse_tensor._values.shape) assert ( type(sparse_tensor) == Constant and type(sparse_tensor._values) == SparseValues ) assert (tensors["w_sparse"]._values.load() == ref_value).all() def test_import_fp4_tensor(self): name = "test_fp4" shape = (2, 2) dtype = onnx.TensorProto.FLOAT4E2M1 # Four 4-bit values = 16 bits = 2 bytes # [0x01, 0x02] should give us the values [0.5, 0, 1, 0] once unpacked tensor_bytes = bytes([0x01, 0x02]) onnx_tensor = onnx.helper.make_tensor(name, dtype, shape, tensor_bytes , raw=True) tensor = OnnxImporter.import_tensor(onnx_tensor) assert type(tensor) == Constant assert tensor.name == name assert tuple(tensor.shape) == shape # Before accessing values we expect the tensor to have an ONNX type assert tensor.dtype == dtype # After loading LazyValues, numpy represents this as four separate 8-bit values # while retaining the shape assert np.array_equal(tensor.values.ravel(), [0.5, 0, 1, 0]) # Post loading the values, we should see the ml_dtypes conversion import ml_dtypes assert tensor.dtype == ml_dtypes.float4_e2m1fn