213 lines
6.2 KiB
Python
213 lines
6.2 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# pylint: disable=invalid-name
|
|
"""
|
|
ONNX Backend Tests
|
|
===================
|
|
Systematically verify the Relax ONNX importer using the official ONNX
|
|
Backend Test Suite (node-level tests only). Each test loads a small
|
|
ONNX model with protobuf reference inputs/outputs and checks that the
|
|
Relax-imported model produces numerically correct results.
|
|
|
|
Only ``onnx.backend.test.data.node`` tests are registered here; real,
|
|
simple, and PyTorch model tests are out of scope for importer-level
|
|
semantic verification.
|
|
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
pytest.importorskip("onnx")
|
|
|
|
import onnx
|
|
import onnx.backend.test
|
|
from onnx.backend.base import Backend, BackendRep
|
|
|
|
import tvm
|
|
from tvm import relax
|
|
from tvm.relax.frontend.onnx import from_onnx
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backend adapter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TVMRelaxBackendRep(BackendRep):
|
|
"""Compiled Relax VM representation for running an ONNX model."""
|
|
|
|
def __init__(self, mod, params, func_param_names, graph_input_names):
|
|
super().__init__()
|
|
self._params = params
|
|
self._func_param_names = func_param_names
|
|
self._graph_input_names = graph_input_names
|
|
|
|
with tvm.transform.PassContext(opt_level=3):
|
|
ex = tvm.compile(mod, target="llvm")
|
|
self._vm = relax.VirtualMachine(ex, tvm.cpu())
|
|
|
|
def run(self, inputs, **kwargs):
|
|
# Map positional inputs to names. The runner loads one .pb per
|
|
# non-initializer input, aligned with model.graph.input order.
|
|
input_map = {}
|
|
for i, arr in enumerate(inputs):
|
|
if i < len(self._graph_input_names):
|
|
input_map[self._graph_input_names[i]] = arr
|
|
|
|
# Build the argument list matching the Relax function's param order:
|
|
# user inputs first, then weight params from self._params.
|
|
input_list = []
|
|
for name in self._func_param_names:
|
|
if name in input_map:
|
|
input_list.append(input_map[name])
|
|
if self._params and "main" in self._params:
|
|
input_list += self._params["main"]
|
|
|
|
self._vm.set_input("main", *input_list)
|
|
self._vm.invoke_stateful("main")
|
|
output = self._vm.get_outputs("main")
|
|
|
|
if isinstance(output, (tvm.runtime.Tensor, np.ndarray)): # noqa: UP038
|
|
return (output.numpy() if hasattr(output, "numpy") else output,)
|
|
if isinstance(output, (tuple, list)): # noqa: UP038
|
|
return tuple(o.numpy() if hasattr(o, "numpy") else np.array(o) for o in output)
|
|
return (np.array(output),)
|
|
|
|
|
|
class TVMRelaxBackend(Backend):
|
|
"""ONNX backend that imports models through Relax's ONNX frontend."""
|
|
|
|
@classmethod
|
|
def is_compatible(cls, model, device="CPU", **kwargs):
|
|
return True
|
|
|
|
@classmethod
|
|
def prepare(cls, model, device="CPU", **kwargs):
|
|
opset = None
|
|
for opset_import in model.opset_import:
|
|
if opset_import.domain in ("", "ai.onnx"):
|
|
opset = opset_import.version
|
|
break
|
|
|
|
tvm_model = from_onnx(model, opset=opset, keep_params_in_input=True)
|
|
tvm_model = relax.transform.DecomposeOpsForInference()(tvm_model)
|
|
tvm_model = relax.transform.LegalizeOps()(tvm_model)
|
|
tvm_model, params = relax.frontend.detach_params(tvm_model)
|
|
|
|
func = tvm_model["main"]
|
|
func_param_names = [p.name_hint for p in func.params]
|
|
graph_input_names = [inp.name for inp in model.graph.input]
|
|
|
|
return TVMRelaxBackendRep(tvm_model, params, func_param_names, graph_input_names)
|
|
|
|
@classmethod
|
|
def supports_device(cls, device: str) -> bool:
|
|
return device == "CPU"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test registration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
backend_test = onnx.backend.test.BackendTest(TVMRelaxBackend, __name__)
|
|
|
|
# Operators where ALL ONNX node tests pass on the Relax importer.
|
|
# Each prefix covers the base test and all its variants
|
|
# (e.g. test_add, test_add_bcast, test_add_uint8).
|
|
#
|
|
# Operators not listed here have known importer gaps or have not yet been
|
|
# validated against the ONNX Backend Test Suite. They can be added
|
|
# incrementally as the importer improves.
|
|
_INCLUDE_OPS = [
|
|
"abs",
|
|
"acos",
|
|
"acosh",
|
|
"add",
|
|
"and",
|
|
"argmax",
|
|
"argmin",
|
|
"averagepool",
|
|
"bitshift",
|
|
"bitwise_and",
|
|
"bitwise_not",
|
|
"bitwise_or",
|
|
"bitwise_xor",
|
|
"ceil",
|
|
"clip",
|
|
"compress",
|
|
"concat",
|
|
"conv",
|
|
"cos",
|
|
"cosh",
|
|
"depthtospace",
|
|
"div",
|
|
"einsum",
|
|
"erf",
|
|
"exp",
|
|
"flatten",
|
|
"floor",
|
|
"gathernd",
|
|
"gemm",
|
|
"globalaveragepool",
|
|
"globalmaxpool",
|
|
"greater",
|
|
"greater_equal",
|
|
"hardmax",
|
|
"hardswish",
|
|
"isnan",
|
|
"less",
|
|
"less_equal",
|
|
"lrn",
|
|
"matmul",
|
|
"matmulinteger",
|
|
"mean",
|
|
"min",
|
|
"mod",
|
|
"mul",
|
|
"neg",
|
|
"nonzero",
|
|
"not",
|
|
"or",
|
|
"reciprocal",
|
|
"round",
|
|
"scatternd",
|
|
"sigmoid",
|
|
"sign",
|
|
"sin",
|
|
"sinh",
|
|
"size",
|
|
"slice",
|
|
"spacetodepth",
|
|
"sqrt",
|
|
"squeeze",
|
|
"sub",
|
|
"sum",
|
|
"tan",
|
|
"tanh",
|
|
"tile",
|
|
"transpose",
|
|
"unique",
|
|
"unsqueeze",
|
|
"where",
|
|
"xor",
|
|
]
|
|
|
|
for _op in _INCLUDE_OPS:
|
|
backend_test.include(rf"^test_{_op}(?:_.*)?(?:_cpu|_cuda)$")
|
|
|
|
globals().update(backend_test.test_cases)
|