-
Notifications
You must be signed in to change notification settings - Fork 854
[OpenVINO] Enabling OpenVINO backend in test harness #17730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5ede955
6921a1c
dfe13b4
2f09cce
a27a5e2
dbd9d2d
e282f84
e29ffca
6eb2233
f7a600d
1f9673b
f30ab4c
6a860ba
6c8b7be
a8f43cb
9df844b
31e7310
8d722b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||
| name: Test OpenVINO Backend | ||||||
|
|
||||||
| on: | ||||||
| schedule: | ||||||
| - cron: 0 2 * * * | ||||||
| push: | ||||||
| branches: | ||||||
| - release/* | ||||||
| tags: | ||||||
| - ciflow/nightly/* | ||||||
| pull_request: | ||||||
| paths: | ||||||
| - .github/workflows/test-backend-openvino.yml | ||||||
| - .github/workflows/_test_backend.yml | ||||||
| workflow_dispatch: | ||||||
|
|
||||||
| concurrency: | ||||||
| group: ${{ github.workflow }}--${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }} | ||||||
| cancel-in-progress: true | ||||||
|
|
||||||
| jobs: | ||||||
| test-openvino: | ||||||
| uses: ./.github/workflows/_test_backend.yml | ||||||
| with: | ||||||
| backend: openvino | ||||||
| flows: '["openvino"]' | ||||||
|
||||||
| flows: '["openvino"]' | |
| flows: '["openvino", "openvino_int8"]' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Copyright (c) Intel Corporation | ||
| # | ||
| # Licensed under the BSD License (the "License"); you may not use this file | ||
| # except in compliance with the License. See the license file found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
|
|
||
| from .decompose_floor_divide_pass import DecomposeFloorDividePass | ||
|
|
||
| __all__ = ["DecomposeFloorDividePass"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| # Copyright (c) Intel Corporation | ||
| # | ||
| # Licensed under the BSD License (the "License"); you may not use this file | ||
| # except in compliance with the License. See the license file found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
|
|
||
| import torch | ||
| from executorch.exir.dialects._ops import ops as exir_ops | ||
| from executorch.exir.pass_base import ExportPass, PassResult | ||
|
|
||
| # Ops to match | ||
| DIV_TENSOR_MODE_OPS = { | ||
| exir_ops.edge.aten.div.Tensor_mode, | ||
| torch.ops.aten.div.Tensor_mode, | ||
| } | ||
|
|
||
| # Replacement op sets per dialect | ||
| EDGE_OPS = { | ||
| "div": exir_ops.edge.aten.div.Tensor, | ||
| "floor": exir_ops.edge.aten.floor.default, | ||
| "to_copy": exir_ops.edge.aten._to_copy.default, | ||
| } | ||
|
|
||
| ATEN_OPS = { | ||
| "div": torch.ops.aten.div.Tensor, | ||
| "floor": torch.ops.aten.floor.default, | ||
| "to_copy": torch.ops.aten._to_copy.default, | ||
| } | ||
|
|
||
|
|
||
| def _get_opset(op): | ||
| if op is exir_ops.edge.aten.div.Tensor_mode: | ||
| return EDGE_OPS | ||
| if op is torch.ops.aten.div.Tensor_mode: | ||
| return ATEN_OPS | ||
| raise RuntimeError(f"Unexpected op: {op}") | ||
|
|
||
|
|
||
| def _node_dtype(node): | ||
| """Return the dtype of a graph node's output, or None if unknown.""" | ||
| if isinstance(node, torch.fx.Node): | ||
| val = node.meta.get("val") | ||
| if val is not None: | ||
| return val.dtype | ||
| return None | ||
|
|
||
|
|
||
| class DecomposeFloorDividePass(ExportPass): | ||
| """Decompose div with rounding_mode='floor' for correct semantics. | ||
|
|
||
| ExecuTorch decomposes floor_divide into aten.div.Tensor_mode with | ||
| rounding_mode='floor'. OpenVINO implements this with truncation-toward-zero | ||
| semantics instead of PyTorch's floor-toward-negative-infinity. | ||
|
|
||
| For float inputs, replaces div(x, y, rounding_mode='floor') with | ||
| floor(div(x, y)). | ||
|
|
||
| For integer inputs, OpenVINO's integer division truncates toward zero, so | ||
| floor(int_div(x, y)) still gives truncation semantics. Instead we cast to | ||
| float32, divide, floor, then cast back: | ||
| _to_copy(floor(div(_to_copy(x, float32), _to_copy(y, float32))), int_dtype) | ||
| """ | ||
suryasidd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def call(self, graph_module: torch.fx.GraphModule) -> PassResult: | ||
| graph = graph_module.graph | ||
|
|
||
| for node in list(graph.nodes): | ||
| if node.op != "call_function": | ||
| continue | ||
| if node.target not in DIV_TENSOR_MODE_OPS: | ||
| continue | ||
|
|
||
| rounding_mode = node.kwargs.get("rounding_mode") | ||
| if rounding_mode != "floor": | ||
| continue | ||
|
|
||
| opset = _get_opset(node.target) | ||
| a, b = node.args[0], node.args[1] | ||
|
|
||
| a_dtype = _node_dtype(a) | ||
| is_integer = a_dtype is not None and not a_dtype.is_floating_point | ||
|
|
||
| with graph.inserting_before(node): | ||
| if is_integer: | ||
| a_f = graph.call_function( | ||
| opset["to_copy"], (a,), {"dtype": torch.float32} | ||
| ) | ||
| b_f = graph.call_function( | ||
| opset["to_copy"], (b,), {"dtype": torch.float32} | ||
| ) | ||
| div_node = graph.call_function(opset["div"], (a_f, b_f)) | ||
| floored = graph.call_function(opset["floor"], (div_node,)) | ||
| result = graph.call_function( | ||
| opset["to_copy"], (floored,), {"dtype": a_dtype} | ||
| ) | ||
|
Comment on lines
+77
to
+95
|
||
| else: | ||
| div_node = graph.call_function(opset["div"], (a, b)) | ||
| result = graph.call_function(opset["floor"], (div_node,)) | ||
|
|
||
| node.replace_all_uses_with(result) | ||
| graph.erase_node(node) | ||
|
|
||
| graph.eliminate_dead_code() | ||
| graph_module.recompile() | ||
| graph_module = super().call(graph_module).graph_module | ||
| return PassResult(graph_module, True) | ||
|
Comment on lines
+103
to
+106
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -8,13 +8,14 @@ | |||||
|
|
||||||
| from typing import final, List | ||||||
|
|
||||||
| from executorch.backends.openvino._passes import DecomposeFloorDividePass | ||||||
|
|
||||||
| from executorch.exir.backend.backend_details import ( | ||||||
| BackendDetails, | ||||||
| ExportedProgram, | ||||||
| PreprocessResult, | ||||||
| ) | ||||||
| from executorch.exir.backend.compile_spec_schema import CompileSpec | ||||||
|
|
||||||
| from executorch.exir.passes.memory_format_ops_pass import DimOrderOpsRevertPass | ||||||
| from openvino.frontend.pytorch.torchdynamo.compile import ( # type: ignore[import-untyped] | ||||||
| openvino_compile, | ||||||
|
|
@@ -38,11 +39,10 @@ def preprocess( | |||||
| Returns: | ||||||
| PreprocessResult: The result of preprocessing, including the compiled model bytes. | ||||||
| """ | ||||||
| transformed_ep = DimOrderOpsRevertPass()(edge_program.graph_module) | ||||||
|
|
||||||
| # Update the edge_program with the transformed graph | ||||||
| if transformed_ep and transformed_ep.graph_module: | ||||||
| edge_program._graph_module = transformed_ep.graph_module | ||||||
| for pass_cls in [DimOrderOpsRevertPass, DecomposeFloorDividePass]: | ||||||
| result = pass_cls()(edge_program.graph_module) | ||||||
| if result and result.graph_module: | ||||||
| edge_program._graph_module = result.graph_module | ||||||
|
||||||
| edge_program._graph_module = result.graph_module | |
| edge_program.graph_module = result.graph_module |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,7 +64,7 @@ build_python_enabled() { | |
| export CMAKE_BUILD_ARGS="--target openvino_backend" | ||
|
|
||
| # Build the package | ||
| ./install_executorch.sh --use-pt-pinned-commit | ||
| ./install_executorch.sh --minimal | ||
|
||
| } | ||
|
|
||
| main() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Copyright (c) Intel Corporation | ||
| # | ||
| # Licensed under the BSD License (the "License"); you may not use this file | ||
| # except in compliance with the License. See the license file found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
|
|
||
| from executorch.backends.openvino.test.tester.tester import ( | ||
| OpenVINOTester, | ||
| Partition, | ||
| Quantize, | ||
| ToEdgeTransformAndLower, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "OpenVINOTester", | ||
| "Partition", | ||
| "Quantize", | ||
| "ToEdgeTransformAndLower", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # Copyright (c) Intel Corporation | ||
| # | ||
| # Licensed under the BSD License (the "License"); you may not use this file | ||
| # except in compliance with the License. See the license file found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
|
|
||
| import functools | ||
| from typing import Any, List, Optional, Sequence, Tuple | ||
|
|
||
| import executorch | ||
| import executorch.backends.test.harness.stages as BaseStages | ||
| import torch | ||
|
|
||
| from executorch.backends.openvino.partitioner import OpenvinoPartitioner | ||
| from executorch.backends.openvino.quantizer.quantizer import OpenVINOQuantizer | ||
| from executorch.backends.test.harness import Tester as TesterBase | ||
| from executorch.backends.test.harness.stages import StageType | ||
| from executorch.exir import EdgeCompileConfig | ||
| from executorch.exir.backend.backend_details import CompileSpec | ||
| from executorch.exir.backend.partitioner import Partitioner | ||
|
|
||
|
|
||
| class Quantize(BaseStages.Quantize): | ||
| def __init__( | ||
| self, | ||
| calibrate: bool = True, | ||
| calibration_samples: Optional[Sequence[Any]] = None, | ||
| is_qat=False, | ||
| ): | ||
| super().__init__( | ||
| quantizer=OpenVINOQuantizer(), | ||
| calibrate=calibrate, | ||
| calibration_samples=calibration_samples, | ||
| is_qat=is_qat, | ||
| fold_quantize=False, | ||
| ) | ||
|
|
||
|
|
||
| class ToEdgeTransformAndLower(BaseStages.ToEdgeTransformAndLower): | ||
| def __init__( | ||
| self, | ||
| partitioners: Optional[List[Partitioner]] = None, | ||
| edge_compile_config: Optional[EdgeCompileConfig] = None, | ||
| compile_specs: Optional[List[CompileSpec]] = None, | ||
| ): | ||
| compile_specs = compile_specs or [CompileSpec("device", b"CPU")] | ||
| super().__init__( | ||
| default_partitioner_cls=lambda: OpenvinoPartitioner(compile_specs), # type: ignore[arg-type] | ||
| partitioners=partitioners, | ||
| edge_compile_config=edge_compile_config | ||
| or EdgeCompileConfig(_check_ir_validity=False), | ||
| ) | ||
|
|
||
|
|
||
| class Partition(BaseStages.Partition): | ||
| def __init__( | ||
| self, | ||
| partitioner: Optional[Partitioner] = None, | ||
| compile_specs: Optional[List[CompileSpec]] = None, | ||
| ): | ||
| super().__init__( | ||
| partitioner=partitioner or OpenvinoPartitioner(compile_specs or []), | ||
| ) | ||
|
|
||
|
|
||
| class OpenVINOTester(TesterBase): | ||
| def __init__( | ||
| self, | ||
| module: torch.nn.Module, | ||
| example_inputs: Tuple[torch.Tensor], | ||
| dynamic_shapes: Optional[Tuple[Any]] = None, | ||
| compile_specs: Optional[List[CompileSpec]] = None, | ||
| ): | ||
| compile_specs = compile_specs or [CompileSpec("device", b"CPU")] | ||
| stage_classes = ( | ||
| executorch.backends.test.harness.Tester.default_stage_classes() | ||
| | { | ||
| StageType.PARTITION: functools.partial( | ||
| Partition, compile_specs=compile_specs | ||
| ), | ||
| StageType.QUANTIZE: Quantize, | ||
| StageType.TO_EDGE_TRANSFORM_AND_LOWER: functools.partial( | ||
| ToEdgeTransformAndLower, | ||
| compile_specs=compile_specs, | ||
| ), | ||
| } | ||
| ) | ||
|
|
||
| super().__init__( | ||
| module=module, | ||
| stage_classes=stage_classes, | ||
| example_inputs=example_inputs, | ||
| dynamic_shapes=dynamic_shapes, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull_request.pathsonly watches workflow files, so changes to the OpenVINO backend/test harness (e.g.backends/openvino/**,.ci/scripts/**,backends/test/**) won’t trigger this workflow on PRs. This makes it easy for OpenVINO regressions to merge without any PR signal. Consider expandingpathssimilarly to other backend workflows (e.g. xnnpack) to include the relevant backend + harness directories.