Torchscript backend (#17132)

* Added torch script backend

* Added ts_backend to pytorch layer tests

* Added use_ts_backend fixture to the test suite to activate the
torchscript backend

* Fixed failing test_dict layer test

* Added USE_TS_BACKEND as an env variable

* Removed use_ts_backend fixture

* Added more tests for ts backend

* Added more information in the comments about usage

* Removed convolution3d test from precommit_ts_backend

* Added some torchscript backend tests to ci

* Removed tests from CI as torch.compile doesn't support 3.11 currently

* Fixed linter issues

* Addressed PR comments and linter issues
This commit is contained in:
Surya Siddharth Pemmaraju authored and GitHub committed 2023-05-17 14:58:38 +02:00
1 parent 293fccc4fe
commit e14d1b2a07
10 files changed
+266 -94

No files matched your search

+2 -2
View File
@@ -105,7 +105,7 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(OV_PYTHON_VERSION)' # Setting only major & minor version will download latest release from GH repo example 3.10 will be 3.10.10.
versionSpec: '$(OV_PYTHON_VERSION)' # Setting only major & minor version will download latest release from GH repo example 3.10 will be 3.10.10.
addToPath: true
disableDownloadFromRegistry: false
architecture: 'x64'
@@ -367,7 +367,7 @@ jobs:
displayName: 'Build cpp samples - gcc'
- script: $(SAMPLES_INSTALL_DIR)/cpp/build_samples.sh -b $(BUILD_DIR)/cpp_samples_clang
env:
env:
CC: clang
CXX: clang++
displayName: 'Build cpp samples - clang'
@@ -0,0 +1,98 @@
# Copyright (C) 2018-2023 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
# flake8: noqa
# mypy: ignore-errors
import logging
import os
import torch
from torch._dynamo.backends.common import fake_tensor_unsupported
from torch._dynamo.backends.registry import register_backend
from torch._inductor.compile_fx import compile_fx
from openvino.frontend import FrontEndManager
from openvino.runtime import Core, Type, PartialShape
from openvino.frontend.pytorch.decoder import TorchScriptPythonDecoder
log = logging.getLogger(__name__)
"""
This is a preview feature in OpenVINO. Torchscript backend
enables users to compile PyTorch models using torch.compile
with OpenVINO as a target backend in PyTorch applications
Sample usage:
This sample code loads resnet50 torchvision model and compiles it using torch dynamo.
We can then use this model for inference. We only need to add two lines of code to
the Pytorch applications which are marked in the code below
1) import openvino.frontend.pytorch.torchdynamo.backend
model = torchvision.models.resnet50()
2) model = torch.compile(model, backend="openvino")
"""
@register_backend
@fake_tensor_unsupported
def openvino(subgraph, example_inputs):
return ts_openvino(subgraph, example_inputs)
def ts_openvino(subgraph, example_inputs):
try:
model = torch.jit.script(subgraph)
model.eval()
fr_model = torch.jit.freeze(model)
core = Core()
fe_manager = FrontEndManager()
fe = fe_manager.load_by_framework('pytorch')
dtype_mapping = {
torch.float64: Type.f64,
torch.float32: Type.f32,
torch.float16: Type.f16,
torch.int64: Type.i64,
torch.int32: Type.i32,
torch.uint8: Type.u8,
torch.int8: Type.i8,
torch.bool: Type.boolean,
}
decoder = TorchScriptPythonDecoder(fr_model)
# TODO: Use convert_model instead when mo --convert_model api becomes a part of OV runtime
im = fe.load(decoder)
om = fe.convert(im)
for idx, input_data in enumerate(example_inputs):
om.inputs[idx].get_node().set_element_type(dtype_mapping[input_data.dtype])
om.inputs[idx].get_node().set_partial_shape(PartialShape(list(input_data.shape)))
om.validate_nodes_and_infer_types()
device = "CPU"
if (os.getenv("OPENVINO_TS_BACKEND_DEVICE") is not None):
device = os.getenv("OPENVINO_TS_BACKEND_DEVICE")
assert device in core.available_devices, "Specified device " + device + " is not in the list of OpenVINO Available Devices"
compiled_model = core.compile_model(om, device)
def _call(*args):
if not hasattr(_call, "execute_on_ov"):
_call.execute_on_ov = True
execute_on_ov = getattr(_call, "execute_on_ov")
if execute_on_ov:
ov_inputs = [a.detach().cpu().numpy() for a in args]
try:
res = compiled_model(ov_inputs)
except Exception as e:
log.debug(f"Failed in OpenVINO execution: {e}")
_call.execute_on_ov = False
return subgraph.forward(*args)
result = [torch.from_numpy(res[out]) for out in compiled_model.outputs]
return result
else:
return subgraph.forward(*args)
return _call
except Exception as e:
log.debug(f"Failed in compilation: {e}")
return compile_fx(subgraph, example_inputs)
+1
View File
@@ -2,4 +2,5 @@
markers =
nightly
precommit
precommit_ts_backend
timeout
@@ -4,6 +4,7 @@
import itertools
import warnings
from copy import deepcopy
import os
import numpy as np
from common.constants import test_device, test_precision
@@ -11,6 +12,8 @@ from openvino.frontend.pytorch.decoder import TorchScriptPythonDecoder
from openvino.frontend import FrontEndManager
from openvino.runtime import Core, Type, PartialShape
import torch
import openvino.frontend.pytorch.torchdynamo.backend
class PytorchLayerTest:
@@ -46,104 +49,89 @@ class PytorchLayerTest:
inputs = self._prepare_input(**kwargs['kwargs_to_prepare_input'])
else:
inputs = self._prepare_input()
with torch.no_grad():
model.eval()
torch_inputs = [torch.from_numpy(inp) if isinstance(
inp, np.ndarray) else inp for inp in inputs]
trace_model = kwargs.get('trace_model', False)
freeze_model = kwargs.get('freeze_model', True)
model, converted_model = self.convert_directly_via_frontend(model, torch_inputs, trace_model, dynamic_shapes, inputs, freeze_model)
graph = model.inlined_graph
if kind is not None and not isinstance(kind, (tuple, list)):
kind = [kind]
if kind is not None:
for op in kind:
assert self._check_kind_exist(
graph, op), f"Operation {op} type doesn't exist in provided graph"
# OV infer:
core = Core()
compiled = core.compile_model(converted_model, ie_device)
infer_res = compiled(deepcopy(inputs))
if hasattr(self, 'skip_framework') and self.skip_framework:
warnings.warn('Framework is skipped')
return
# Framework infer:
fw_res = model(*deepcopy(torch_inputs))
if not isinstance(fw_res, (tuple)):
fw_res = (fw_res,)
output_list = list(infer_res.values())
flatten_fw_res = []
def flattenize_dict_outputs(res):
if isinstance(res, dict):
return flattenize_outputs(res.values())
def flattenize_outputs(res):
results = []
for res_item in res:
# if None is at output we skip it
if res_item is None:
continue
# If input is list or tuple flattenize it
if isinstance(res_item, (list, tuple)):
decomposed_res = flattenize_outputs(res_item)
results.extend(decomposed_res)
continue
if isinstance(res_item, dict):
decomposed_res = flattenize_dict_outputs(res_item)
results.extend(decomposed_res)
continue
results.append(res_item)
return results
flatten_fw_res = flattenize_outputs(fw_res)
assert len(flatten_fw_res) == len(
output_list), f'number of outputs not equal, {len(flatten_fw_res)} != {len(output_list)}'
# check if results dtypes match
for fw_tensor, ov_tensor in zip(flatten_fw_res, output_list):
if not isinstance(fw_tensor, torch.Tensor):
fw_type = torch.tensor(fw_tensor).numpy().dtype
ov_type = ov_tensor.dtype
if fw_type in [np.int32, np.int64] and ov_type in [np.int32, np.int64]:
# do not differentiate between int32 and int64
continue
assert ov_type == fw_type, f"dtype validation failed: {ov_type} != {fw_type}"
continue
assert torch.tensor(np.array(
ov_tensor)).dtype == fw_tensor.dtype, f"dtype validation failed: {torch.tensor(np.array(ov_tensor)).dtype} != {fw_tensor.dtype}"
torch_inputs = [torch.from_numpy(inp) if isinstance(
inp, np.ndarray) else inp for inp in inputs]
if 'custom_eps' in kwargs and kwargs['custom_eps'] is not None:
custom_eps = kwargs['custom_eps']
else:
custom_eps = 1e-4
# Compare Ie results with Framework results
fw_eps = custom_eps if precision == 'FP32' else 5e-2
is_ok = True
for i in range(len(infer_res)):
cur_fw_res = flatten_fw_res[i].to(memory_format=torch.contiguous_format).numpy(
) if isinstance(flatten_fw_res[i], torch.Tensor) else flatten_fw_res[i]
cur_ov_res = infer_res[compiled.output(i)]
print(f"fw_re: {cur_fw_res};\n ov_res: {cur_ov_res}")
if not np.allclose(cur_ov_res, cur_fw_res,
atol=fw_eps,
rtol=fw_eps, equal_nan=True):
is_ok = False
print("Max diff is {}".format(
np.array(
abs(cur_ov_res - cur_fw_res)).max()))
else:
print("Accuracy validation successful!\n")
print("absolute eps: {}, relative eps: {}".format(fw_eps, fw_eps))
assert is_ok, "Accuracy validation failed"
def use_ts_backend():
return(os.environ.get('USE_TS_BACKEND', False))
if use_ts_backend():
self.ts_backend_test(model, torch_inputs, custom_eps)
else:
with torch.no_grad():
model.eval()
trace_model = kwargs.get('trace_model', False)
freeze_model = kwargs.get('freeze_model', True)
model, converted_model = self.convert_directly_via_frontend(model, torch_inputs, trace_model, dynamic_shapes, inputs, freeze_model)
graph = model.inlined_graph
if kind is not None and not isinstance(kind, (tuple, list)):
kind = [kind]
if kind is not None:
for op in kind:
assert self._check_kind_exist(
graph, op), f"Operation {op} type doesn't exist in provided graph"
# OV infer:
core = Core()
compiled = core.compile_model(converted_model, ie_device)
infer_res = compiled(deepcopy(inputs))
if hasattr(self, 'skip_framework') and self.skip_framework:
warnings.warn('Framework is skipped')
return
# Framework infer:
fw_res = model(*deepcopy(torch_inputs))
if not isinstance(fw_res, (tuple)):
fw_res = (fw_res,)
output_list = list(infer_res.values())
flatten_fw_res = []
flatten_fw_res = flattenize_outputs(fw_res)
assert len(flatten_fw_res) == len(
output_list), f'number of outputs not equal, {len(flatten_fw_res)} != {len(output_list)}'
# check if results dtypes match
for fw_tensor, ov_tensor in zip(flatten_fw_res, output_list):
if not isinstance(fw_tensor, torch.Tensor):
fw_type = torch.tensor(fw_tensor).numpy().dtype
ov_type = ov_tensor.dtype
if fw_type in [np.int32, np.int64] and ov_type in [np.int32, np.int64]:
# do not differentiate between int32 and int64
continue
assert ov_type == fw_type, f"dtype validation failed: {ov_type} != {fw_type}"
continue
assert torch.tensor(np.array(
ov_tensor)).dtype == fw_tensor.dtype, f"dtype validation failed: {torch.tensor(np.array(ov_tensor)).dtype} != {fw_tensor.dtype}"
# Compare Ie results with Framework results
fw_eps = custom_eps if precision == 'FP32' else 5e-2
is_ok = True
for i in range(len(infer_res)):
cur_fw_res = flatten_fw_res[i].to(memory_format=torch.contiguous_format).numpy(
) if isinstance(flatten_fw_res[i], torch.Tensor) else flatten_fw_res[i]
cur_ov_res = infer_res[compiled.output(i)]
print(f"fw_re: {cur_fw_res};\n ov_res: {cur_ov_res}")
if not np.allclose(cur_ov_res, cur_fw_res,
atol=fw_eps,
rtol=fw_eps, equal_nan=True):
is_ok = False
print("Max diff is {}".format(
np.array(
abs(cur_ov_res - cur_fw_res)).max()))
else:
print("Accuracy validation successful!\n")
print("absolute eps: {}, relative eps: {}".format(fw_eps, fw_eps))
assert is_ok, "Accuracy validation failed"
# Each model should specify inputs
def _prepare_input(self):
@@ -204,6 +192,58 @@ class PytorchLayerTest:
om.validate_nodes_and_infer_types()
return om
def ts_backend_test(self, model, inputs, custom_eps):
torch._dynamo.reset()
with torch.no_grad():
model.eval()
fw_model = torch.compile(model)
ov_model = torch.compile(model, backend="openvino")
ov_res = ov_model(*inputs)
fw_res = fw_model(*inputs)
if not isinstance(fw_res, (tuple)):
fw_res = (fw_res,)
if not isinstance(ov_res, (tuple)):
ov_res = (ov_res,)
flatten_fw_res, flatten_ov_res = [], []
flatten_fw_res = flattenize_outputs(fw_res)
flatten_ov_res = flattenize_outputs(ov_res)
assert len(flatten_fw_res) == len(
flatten_ov_res
), f'number of ouptuts are not equal, {len(flatten_fw_res)} != {len(flatten_ov_res)}'
# Check if output data types match
for fw_tensor, ov_tensor in zip(flatten_fw_res, flatten_ov_res):
if not isinstance(fw_tensor, torch.Tensor) and not isinstance(ov_tensor, torch.Tensor):
assert fw_tensor == ov_tensor
assert type(fw_tensor) == type(ov_tensor)
continue
assert fw_tensor.dtype == ov_tensor.dtype, f"dtype validation failed: {fw_tensor.dtype} != {ov_tensor.dtype}"
fw_eps = custom_eps
is_ok = True
for i in range(len(flatten_ov_res)):
cur_ov_res = flatten_ov_res[i]
cur_fw_res = flatten_fw_res[i]
if not torch.allclose(cur_fw_res, cur_ov_res,
atol=fw_eps, rtol=fw_eps,
equal_nan=True):
is_ok = False
print(
"Max diff is {}".format(
torch.max(torch.tensor(abs(cur_ov_res - cur_fw_res)))
)
)
else:
print("Accuracy validation successful!\n")
print("absolute eps: {}, relative eps: {}".format(fw_eps, fw_eps))
assert is_ok, "Accuracy validation failed"
def get_params(ie_device=None, precision=None):
"""
@@ -220,3 +260,27 @@ def get_params(ie_device=None, precision=None):
continue
test_args.append(element)
return test_args
def flattenize_dict_outputs(res):
if isinstance(res, dict):
return flattenize_outputs(res.values())
def flattenize_outputs(res):
results = []
for res_item in res:
# if None is at output we skip it
if res_item is None:
continue
# If input is list or tuple flattenize it
if isinstance(res_item, (list, tuple)):
decomposed_res = flattenize_outputs(res_item)
results.extend(decomposed_res)
continue
if isinstance(res_item, dict):
decomposed_res = flattenize_dict_outputs(res_item)
results.extend(decomposed_res)
continue
results.append(res_item)
return results
@@ -32,6 +32,7 @@ class TestAdaptiveAvgPool3D(PytorchLayerTest):
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_adaptive_avg_pool3d(self, ie_device, precision, ir_version, input_tensor, output_size):
self.input_tensor = input_tensor
self._test(*self.create_model(output_size), ie_device, precision, ir_version)
@@ -46,6 +46,7 @@ class TestAdaptiveMaxPool2D(PytorchLayerTest):
]))
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_adaptive_max_pool2d(self, ie_device, precision, ir_version, input_tensor, output_size, return_indices):
self.input_tensor = input_tensor
self._test(*self.create_model(output_size, return_indices), ie_device, precision, ir_version)
@@ -38,6 +38,7 @@ class TestAdd(PytorchLayerTest):
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
@pytest.mark.parametrize("op_type", ["add", "add_"])
def test_add(self, ie_device, precision, ir_version, alpha, input_rhs, op_type):
self.input_rhs = input_rhs
@@ -98,6 +99,7 @@ class TestAddTypes(PytorchLayerTest):
])
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_add_types(self, ie_device, precision, ir_version, lhs_type, lhs_shape, rhs_type, rhs_shape):
self.lhs_type = lhs_type
self.lhs_shape = lhs_shape
@@ -120,5 +122,6 @@ class TestAddLists(PytorchLayerTest):
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_add(self, ie_device, precision, ir_version):
self._test(*self.create_model(), ie_device, precision, ir_version)
@@ -34,7 +34,7 @@ class TestBatchNorm(PytorchLayerTest):
super(aten_batch_norm_train, self).__init__()
self.weight = torch.randn(6) if weights else None
self.bias = torch.randn(6) if bias else None
self.running_mean = torch.randn(6) if running_stats else None
self.running_mean = torch.randn(6) if running_stats else None
self.running_var = torch.randn(6) if running_stats else None
self.eps = eps
@@ -57,6 +57,7 @@ class TestBatchNorm(PytorchLayerTest):
])
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_batch_norm(self, weights, bias, eps, train, running_stats, ie_device, precision, ir_version, kwargs_to_prepare_input):
self._test(*self.create_model(weights, bias, eps, train, running_stats),
ie_device, precision, ir_version, kwargs_to_prepare_input=kwargs_to_prepare_input, dynamic_shapes=False, use_mo_convert=False)
@@ -207,6 +207,7 @@ class TestConvolution(PytorchLayerTest):
@pytest.mark.parametrize("underscore", [True, False])
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_convolution1d(self, params, bias, underscore, ie_device, precision, ir_version):
self._test(*self.create_model(**params, bias=bias, underscore=underscore),
ie_device, precision, ir_version, dynamic_shapes=params['groups'] == 1,
@@ -217,6 +218,7 @@ class TestConvolution(PytorchLayerTest):
@pytest.mark.parametrize("underscore", [True, False])
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_convolution2d(self, params, bias, underscore, ie_device, precision, ir_version):
self._test(*self.create_model(**params, bias=bias, underscore=underscore),
ie_device, precision, ir_version, dynamic_shapes=params['groups'] == 1)
@@ -30,5 +30,6 @@ class TestRelu(PytorchLayerTest):
@pytest.mark.parametrize("inplace", [False, True])
@pytest.mark.nightly
@pytest.mark.precommit
@pytest.mark.precommit_ts_backend
def test_relu(self, inplace, ie_device, precision, ir_version):
self._test(*self.create_model(inplace), ie_device, precision, ir_version)