381 lines
13 KiB
Python
381 lines
13 KiB
Python
#
|
|
# 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
|