[PT FE] Add quantized::linear (#18570)

* Support GetAttr with packed params

* Apply suggestions from code review

* [PT FE] Add quantized types as normal types to decoder

* [PT FE] Add decoder dequantize, add dtypes to quantize

* [PT FE] Add dequantize example

* [PT FE] Implement replacer for quantized nodes

* [PT FE] Register replacer for quantize/dequantize

* [PT FE] Remove unwanted junk from previous version

* [PT FE] Fix building mistakes for frontend

* [PT FE] Clang fix

* [PT FE] Ease of use upgrade to quantize funcs

* [PT FE] Clang format

* [PT FE] Introduce new version of quantize/dequantize

* [PT FE] Remove unwanted files from new version

* [PT FE] Fix style

* [PT FE] Add QuantizedPtNode replacer, fix accuracy error

* [PT FE] Add improved version of quantize/dequantize with shared_ptrs

* [PT FE] Fix utils shared ptr reference error

* [PT FE] Quantize now takes correct input for operations

* [PT FE] Upgrade quantize method

* [PT FE] Add BFS for dequantize, add quantize_per_channel

* [PT FE] Add missing replacer to frontend, improve tests

* [PT FE] Rename replacer -> remover, remove unwanted header files

* [PT FE] Change function declarations to return ov::Output instead of shared ptr

* [PT FE] Add missing context mark node

* [PT FE] Remove unknown modifications to ie_c_api

* [PT FE] Remove fp16 support, turn off int32 tests

* [PT FE] Clang format

* [PT FE] Fix quantize_per_tensor

* [PT FE] Minor fixes from review

* [PT FE] Remove dequantize, remove helpers, replacer now removes nodes instead

* [PT FE] Rename Replacer to Remover for dequantize nodes

* [PT FE] Clang format

* [PT FE] Move comments to header files, minor import fixes

* [PT FE] Fix clang format

* [PT FE] Fix dtype issue

* [PT FE] Fix quantize_per_channel tests

* Add quantized::linear

* Update quantized_linear.cpp

* add contructors

* Improve tests

* Add requested checks

---------

Co-authored-by: Maxim Vafin <maxim.vafin@intel.com>
Co-authored-by: Roboreptile <piotr.krzeminski@intel.com>
This commit is contained in:
Mateusz Mikolajczyk
2023-07-20 11:25:56 +02:00
committed by GitHub
co-authored by Maxim Vafin Roboreptile
parent 6d8dcb059d
commit b315880cc3
7 changed files with 218 additions and 5 deletions
@@ -7,6 +7,7 @@
from openvino.frontend.pytorch.py_pytorch_frontend import _FrontEndPytorchDecoder as Decoder
from openvino.frontend.pytorch.py_pytorch_frontend import _Type as DecoderType
from openvino.runtime import op, PartialShape, Type as OVType, OVAny, Shape, Tensor
from openvino.runtime import opset11 as ops
import typing
from packaging.version import parse
@@ -404,10 +405,59 @@ class TorchScriptPythonDecoder (Decoder):
node.set_friendly_name(name)
return node
@staticmethod
def convert_quantized_tensor(qtensor: torch.Tensor):
# need to represent as Constant(u8) -> Convert(f32) -> Subtract(zero_point) -> Multiply (scale)
qscheme = qtensor.qscheme() # torch.per_channel_affine (per_tensor)
if qscheme == torch.per_channel_affine:
int8_tensor = qtensor.int_repr()
scale = qtensor.q_per_channel_scales().numpy().astype(np.float32) # (weight.q_scale() for per_tensor)
zero_point = qtensor.q_per_channel_zero_points().numpy().astype(np.float32) # (weight.q_zero_point() for per_tensor)
axis = np.int32(qtensor.q_per_channel_axis())
new_shape = np.ones(len(int8_tensor.shape), dtype=np.int32)
new_shape[axis] = -1
zero_point_bc = np.reshape(zero_point, new_shape)
scale_bc = np.reshape(scale, new_shape)
int8_const = op.Constant(int8_tensor.numpy())
convert = ops.convert(int8_const, np.float32)
sub = ops.subtract(convert, zero_point_bc)
return ops.multiply(sub, scale_bc).outputs()
elif qscheme == torch.per_tensor_affine:
int8_tensor = qtensor.int_repr()
scale = np.float32(qtensor.q_scale())
zero_point = np.float32(qtensor.q_zero_point())
int8_const = op.Constant(int8_tensor.numpy())
convert = ops.convert(int8_const, np.float32)
sub = ops.subtract(convert, zero_point)
return ops.multiply(sub, scale).outputs()
assert False, "Unsupported qscheme"
def try_decode_get_attr(self):
pt_value = get_value_from_getattr(self.graph_element, self.pt_module)
assert pt_value is not None, "Couldn't retrieve value from prim::GetAttr"
if not isinstance(pt_value, (torch.jit.ScriptModule, torch.jit.TracedModule)):
if isinstance(pt_value, torch.ScriptObject):
# We assume this is __torch__.torch.classes.quantized.Conv2dPackedParamsBase or __torch__.torch.classes.quantized.LinearPackedParamsBase
# TODO: but can be anything. Figure a better way to distinguish
weight, bias = pt_value.unpack()
res = self.convert_quantized_tensor(weight)
if isinstance(bias, torch.Tensor):
res += ivalue_to_constant(bias)
else:
res += ops.convert_like(ivalue_to_constant(torch.zeros(1))[0], res[0]).outputs()
try:
# these params exist only for conv params
stride = pt_value.stride()
padding = pt_value.padding()
dilation = pt_value.dilation()
groups = pt_value.groups()
res += ivalue_to_constant(stride) + ivalue_to_constant(padding) + ivalue_to_constant(dilation) + ivalue_to_constant(groups)
except:
pass
return res
elif not isinstance(pt_value, (torch.jit.ScriptModule, torch.jit.TracedModule)):
return ivalue_to_constant(pt_value)
else:
return []
+15 -4
View File
@@ -3,7 +3,6 @@
//
#include "openvino/frontend/pytorch/node_context.hpp"
#include "openvino/opsets/opset10.hpp"
#include "pt_framework_node.hpp"
#include "utils.hpp"
@@ -14,11 +13,23 @@ namespace op {
OutputVector translate_get_attr(const NodeContext& context) {
auto res = context.get_decoder()->try_decode_get_attr();
FRONT_END_OP_CONVERSION_CHECK(res.size() > 0, "GetAttr must have at least one output.");
return res;
FRONT_END_OP_CONVERSION_CHECK(res.size() > 0,
"Failed to obtain data from GetAttr with output tensor name: ",
context.get_decoder()->get_output_debug_name(0));
if (res.size() == 1) {
return res;
} else {
// Packed params case
std::shared_ptr<Node> fw_node = std::make_shared<PtFrameworkNode>(context.get_decoder(), res, 1);
add_exception_to_fw_node(
fw_node,
"PackedParams represented as FrameworkNode, all contained params represented as inputs to this "
"node.");
return {context.mark_node(fw_node)};
}
};
} // namespace op
} // namespace pytorch
} // namespace frontend
} // namespace ov
} // namespace ov
@@ -0,0 +1,46 @@
// Copyright (C) 2018-2023 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
#include "openvino/frontend/pytorch/node_context.hpp"
#include "openvino/op/add.hpp"
#include "openvino/op/matmul.hpp"
#include "utils.hpp"
#include "utils_quantize.hpp"
namespace ov {
namespace frontend {
namespace pytorch {
namespace op {
OutputVector translate_quantized_linear(const NodeContext& context) {
// "quantized::linear(Tensor X, __torch__.torch.classes.quantized.LinearPackedParamsBase W_prepack, float Y_scale_i,
// int Y_zero_point_i) -> Tensor Y"
num_inputs_check(context, 4, 4);
auto x = context.get_input(0);
auto packed_params_node =
std::dynamic_pointer_cast<ov::op::util::FrameworkNode>(context.get_input(1).get_node_shared_ptr());
FRONT_END_OP_CONVERSION_CHECK(packed_params_node, "Packed params input node type is required to be FrameworkNode.");
const auto& attrs = packed_params_node->get_attrs();
FRONT_END_OP_CONVERSION_CHECK((attrs.find(PtFrameworkNode::op_type_key) != attrs.end()),
"Packed params input node does not contain information about op type.");
FRONT_END_OP_CONVERSION_CHECK((attrs.at(PtFrameworkNode::op_type_key) == "prim::GetAttr"),
"Incorrect packed params input node operator type, expected prim::GetAttr.");
auto packed_params = packed_params_node->inputs();
FRONT_END_OP_CONVERSION_CHECK(packed_params.size() == 2,
"Packed parameters for quantized linear should contain 2 items.");
auto weights = packed_params[0].get_source_output();
auto bias = packed_params[1].get_source_output();
auto linear = context.mark_node(std::make_shared<ov::op::v0::MatMul>(x, weights, false, true));
linear = context.mark_node(std::make_shared<ov::op::v1::Add>(linear, bias));
auto scale = context.get_input(2);
auto zero_point = context.get_input(3);
return {context.mark_output(quantize(context, linear, scale, zero_point, x))};
};
} // namespace op
} // namespace pytorch
} // namespace frontend
} // namespace ov
+2
View File
@@ -158,6 +158,7 @@ OP_CONVERTER(translate_var_mean);
OP_CONVERTER(translate_where);
OP_CONVERTER(translate_zeros);
OP_CONVERTER(translate_zeros_like);
OP_CONVERTER(translate_quantized_linear);
} // namespace op
@@ -418,6 +419,7 @@ const std::map<std::string, CreatorFunction> get_supported_ops() {
{"prim::requires_grad", op::return_false_scalar},
{"prim::PythonOp", op::translate_pythonop},
{"prim::type", op::skip_node}, // Used with prim::device, pass PtFrameworkNode.
{"quantized::linear", op::translate_quantized_linear},
{"torchvision::deform_conv2d", op::translate_deform_conv},
{"torchvision::nms", op::translate_nms},
{"torchvision::roi_align", op::translate_roi_align},
@@ -152,6 +152,40 @@ ov::Output<ov::Node> quantize(const NodeContext& context,
quantization_type);
}
ov::Output<ov::Node> quantize(const NodeContext& context,
ov::Output<ov::Node> input,
ov::Output<ov::Node> quantized_node) {
std::shared_ptr<QuantizedPtNode> quantized_pt_node;
if ((quantized_pt_node = cast_quantized_fw_node(quantized_node.get_node_shared_ptr()))) {
return quantize(context,
input.get_node_shared_ptr(),
quantized_pt_node->get_scale(),
quantized_pt_node->get_zero_point(),
quantized_pt_node->get_axis(),
quantized_pt_node->get_dtype(),
quantized_pt_node->get_type());
}
FRONT_END_OP_CONVERSION_CHECK(false, "Failed to convert a node to QuantizedPtNode");
}
ov::Output<ov::Node> quantize(const NodeContext& context,
ov::Output<ov::Node> input,
ov::Output<ov::Node> scale,
ov::Output<ov::Node> zero_point,
ov::Output<ov::Node> quantized_node) {
std::shared_ptr<QuantizedPtNode> quantized_pt_node;
if ((quantized_pt_node = cast_quantized_fw_node(quantized_node.get_node_shared_ptr()))) {
return quantize(context,
input.get_node_shared_ptr(),
scale.get_node_shared_ptr(),
zero_point.get_node_shared_ptr(),
quantized_pt_node->get_axis(),
quantized_pt_node->get_dtype(),
quantized_pt_node->get_type());
}
FRONT_END_OP_CONVERSION_CHECK(false, "Failed to convert a node to QuantizedPtNode");
}
std::shared_ptr<QuantizedPtNode> cast_quantized_fw_node(ov::Output<Node> node) {
auto quant_node = std::dynamic_pointer_cast<QuantizedPtNode>(node.get_node_shared_ptr());
if (!quant_node) {
@@ -108,6 +108,23 @@ ov::Output<ov::Node> quantize(const NodeContext& context,
ov::element::Type dtype,
QuantizedPtNodeType quantization_type);
/**
* Quantizes input node like the quantized node. Returns a shared pointer to the new QuantizedPtNode.
*/
ov::Output<ov::Node> quantize(const NodeContext& context,
ov::Output<ov::Node> input,
ov::Output<ov::Node> quantized_node);
/**
* Quantizes input node like the quantized node, with new scale and zero_point parameters. Returns a shared pointer to
* the new QuantizedPtNode.
*/
ov::Output<ov::Node> quantize(const NodeContext& context,
ov::Output<ov::Node> input,
ov::Output<ov::Node> scale,
ov::Output<ov::Node> zero_point,
ov::Output<ov::Node> quantized_node);
std::shared_ptr<QuantizedPtNode> cast_quantized_fw_node(ov::Output<ov::Node> node);
std::shared_ptr<QuantizedPtNode> cast_quantized_fw_node(ov::Output<ov::Node> node, const std::string& type);
} // namespace pytorch
@@ -0,0 +1,53 @@
# Copyright (C) 2018-2023 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import pytest
import torch
import numpy as np
from pytorch_layer_test_class import PytorchLayerTest
class TestQuantizedLinear(PytorchLayerTest):
def _prepare_input(self, input_shape=(2, 2)):
return (np.random.randn(*input_shape).astype(np.float32),)
def create_model(self, weight_shape, is_bias, scale, zero_point):
class aten_quantized_linear(torch.nn.Module):
def __init__(self, weight_shape, is_bias, scale, zero_point):
super(aten_quantized_linear, self).__init__()
if is_bias:
self.linear = torch.ao.nn.quantized.Linear(weight_shape[-1], weight_shape[0], True)
torch.nn.init.normal_(self.linear.bias())
else:
self.linear = torch.ao.nn.quantized.Linear(weight_shape[-1], weight_shape[0], False)
self.linear.scale = float(scale)
self.linear.zero_point = int(zero_point)
def forward(self, inp):
inp_q = torch.quantize_per_tensor(inp, 1., 0, torch.quint8)
return torch.dequantize(self.linear(inp_q))
ref_net = None
return aten_quantized_linear(weight_shape, is_bias, scale, zero_point), ref_net, "quantized::linear"
@pytest.mark.parametrize("params", [
{'input_shape': [3, 9], 'weight_shape': [10, 9]},
{'input_shape': [3, 9], 'weight_shape': [9]},
{'input_shape': [2, 3, 9], 'weight_shape': [10, 9]},
{'input_shape': [2, 3, 9], 'weight_shape': [9]},
{'input_shape': [3, 9], 'weight_shape': [9], "bias": True},
{'input_shape': [3, 9], 'weight_shape': [10, 9], "bias": True},
{'input_shape': [2, 3, 9], 'weight_shape': [10, 9], "bias": True},
])
@pytest.mark.parametrize("scale", [1., 0.3, 1.3])
@pytest.mark.parametrize("zero_point", [0, 1])
@pytest.mark.parametrize("trace", [True, False])
@pytest.mark.nightly
@pytest.mark.precommit
def test_quantized_linear(self, params, scale, zero_point, trace, ie_device, precision, ir_version):
input_shape = params.get("input_shape")
weight_shape = params.get("weight_shape")
bias = params.get("bias", False)
self._test(*self.create_model(weight_shape, bias, scale, zero_point), ie_device, precision, ir_version,
kwargs_to_prepare_input={"input_shape": input_shape}, trace_model=trace, freeze_model=False)