Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 146 additions & 8 deletions onnxscript/version_converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,157 @@
from __future__ import annotations

__all__ = [
# Functions
"ConvertVersionPass",
"convert_version",
]

import onnxscript.optimizer
import logging

import onnx

from onnxscript import ir
from onnxscript.ir.passes.common import _c_api_utils
from onnxscript.ir.passes.common import inliner as _inliner
from onnxscript.ir.passes.common import unused_removal as _unused_removal
from onnxscript.version_converter import _version_converter

logger = logging.getLogger(__name__)


class ConvertVersionPass(ir.passes.InPlacePass):
"""Convert the model to the specified ONNX opset version.

This pass leverages the onnxscript version converter to convert the model. If
the conversion is not supported, it falls back to the onnx C API to convert
the model. This pass is in-place.

The pass is an no-op if the c-api fails.

Attributes:
target_version: The target ONNX opset version to convert the model to.
fallback: Whether to fallback to the onnx version converter if the
target version is not supported. Default is True.
Comment thread
justinchuby marked this conversation as resolved.
Outdated
"""

def __init__(self, target_version: int, fallback: bool = False) -> None:
super().__init__()
self.target_version = target_version
self.fallback = fallback
self.convert_pass = ir.passes.Sequential(
_inliner.InlinePass(),
_ConvertVersionPassRequiresInline(
target_version=target_version,
fallback=fallback,
),
_unused_removal.RemoveUnusedNodesPass(),
_unused_removal.RemoveUnusedFunctionsPass(),
_unused_removal.RemoveUnusedOpsetsPass(),
)

def call(self, model: ir.Model) -> ir.passes.PassResult:
return self.convert_pass(model)


class _ConvertVersionPassRequiresInline(ir.passes.InPlacePass):
"""Convert the model to the specified ONNX opset version.

This pass leverages the onnxscript version converter to convert the model. If
the conversion is not supported, it falls back to the onnx C API to convert
the model. This pass is in-place.

The pass is an no-op if the c-api fails.

Attributes:
target_version: The target ONNX opset version to convert the model to.
fallback: Whether to fallback to the onnx version converter if the
target version is not supported. Default is False.
"""

def __init__(self, target_version: int, fallback: bool = False) -> None:
super().__init__()
self.target_version = target_version
self.fallback = fallback

def call(self, model: ir.Model) -> ir.passes.PassResult:
if model.functions:
raise ValueError(
"The model contains functions. The version conversion pass does not support "
"functions. Please use `onnxscript.ir.passes.common.inliner.InlinePass` to inline the "
f"functions before applying this pass ({self.__class__.__name__})."
)
if "" in model.graph.opset_imports:
onnx_opset_version = model.graph.opset_imports[""]
if onnx_opset_version == self.target_version:
# No need to convert the version
return ir.passes.PassResult(model, False)

# When fallback is disabled, always use the onnxscript version converter;
# When fallback is enabled, use the onnxscript version converter
# if the target version is supported. Otherwise, use the onnx C API
# to convert the model.
if not self.fallback or _version_converter.version_supported(
model, self.target_version
):
_version_converter.convert_version(
model,
target_version=self.target_version,
)
return ir.passes.PassResult(model, True)

if not self.fallback:
logger.warning(
"The model version conversion is not supported by the onnxscript version converter "
"and fallback is disabled. The model was not modified"
" (target version: %d). "
"Set fallback=True to enable fallback to the onnx c-api version converter.",
self.target_version,
)
return ir.passes.PassResult(model, False)

# If the onnxscript version converter does not support the conversion,
# we can use the onnx C API to convert the model
def _partial_convert_version(proto: onnx.ModelProto) -> onnx.ModelProto:
"""Partial function to check the model."""
return onnx.version_converter.convert_version(
proto, target_version=self.target_version
)

try:
converted_proto = _c_api_utils.call_onnx_api(
func=_partial_convert_version, model=model
)
except Exception as e: # pylint: disable=broad-exception-caught
logger.warning(
"Failed to convert the model to the target version %d using the ONNX C API. "
"The model was not modified",
self.target_version,
exc_info=e,
)
return ir.passes.PassResult(model, False)

converted_model = ir.from_proto(converted_proto)

# Recover the initializers in the converted model
for input in converted_model.graph.inputs:
if input.name in model.graph.initializers:
input.const_value = model.graph.initializers[input.name].const_value
converted_model.graph.register_initializer(input)
user_inputs = converted_model.graph.inputs[: len(model.graph.inputs)]
converted_model.graph.inputs.clear()
converted_model.graph.inputs.extend(user_inputs)

# Return the converted graph to the original model to keep the pass in-place
model.graph = converted_model.graph
return ir.passes.PassResult(model, True)


def convert_version(model: ir.Model, target_version: int) -> None:
"""Convert the model to the specified ONNX opset version."""
def convert_version(model: ir.Model, target_version: int, fallback=False) -> None:
Comment thread
justinchuby marked this conversation as resolved.
"""Convert the model to the specified ONNX opset version.

# In functions, we can have attribute-parameters, which means we don't know the value of the attribute.
# Hence, we inline all the functions.
onnxscript.optimizer.inline(model)
_version_converter.convert_version(model, target_version)
Args:
model: The model to convert.
target_version: The target ONNX opset version.
fallback: Whether to fallback to the onnx version converter if the
target version is not supported.
Comment thread
justinchuby marked this conversation as resolved.
Outdated
Comment thread
justinchuby marked this conversation as resolved.
Outdated
"""
ConvertVersionPass(target_version=target_version, fallback=fallback)(model)
19 changes: 17 additions & 2 deletions onnxscript/version_converter/_version_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
logger = logging.getLogger(__name__)


CURRENT_MAX_ONNX_OPSET = 23
SUPPORTED_MAX_ONNX_OPSET = 23
SUPPORTED_MIN_ONNX_OPSET = 18


class VersionConverterError(RuntimeError):
Expand All @@ -38,6 +39,20 @@ class Replacement:
AdapterFunction = Callable[[ir.Node, orp.RewriterContext], ReturnValue]


def version_supported(model: ir.Model, target_version: int) -> bool:
"""Check if the target version is supported by the current version."""
if "" in model.graph.opset_imports:
current_version = model.graph.opset_imports[""]
else:
return True
return (
SUPPORTED_MIN_ONNX_OPSET
<= current_version
<= target_version
<= SUPPORTED_MAX_ONNX_OPSET
Comment thread
justinchuby marked this conversation as resolved.
)


class AdapterRegistry:
"""A class that maintains a registry of adapters for ops."""

Expand Down Expand Up @@ -262,7 +277,7 @@ def visit_node(
return None

def visit_graph(self, graph: ir.Graph) -> None:
if self.target_version > CURRENT_MAX_ONNX_OPSET:
if self.target_version > SUPPORTED_MAX_ONNX_OPSET:
logger.warning(
"Conversion to target opset: %s not currently supported.",
self.target_version,
Expand Down
4 changes: 1 addition & 3 deletions onnxscript/version_converter/_version_converter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@

import unittest

import onnx.checker
import onnx.defs
import onnx.parser
import onnx.shape_inference

from onnxscript import ir, version_converter


class ApapterCoverageTest(unittest.TestCase):
class AdapterCoverageTest(unittest.TestCase):
def get_all_unique_schema_versions(self) -> dict[str, list]:
"""Collect all unique versions of ONNX standard domain ops"""
op_version_dict = {}
Expand Down
24 changes: 24 additions & 0 deletions tests/version_converter/version_conversion_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from __future__ import annotations

import pathlib
import unittest

from onnxscript import ir, version_converter

model_folder_path = pathlib.Path(__file__).resolve().parent.parent.parent / "testdata"


class ModelTest(unittest.TestCase):
def test_model_runs_and_matches_accuracy_after_conversion_fallback_true(self):
model_path = model_folder_path / "e2e_models/torchscript_model/torchscript_model.onnx"
model = ir.load(model_path)

# Down convert the model with the onnx version converter
version_converter.convert_version(model, target_version=16, fallback=True)
self.assertEqual(model.opset_imports[""], 16)


if __name__ == "__main__":
unittest.main()
Loading