Fix symbolic shape inference script on precision loss issue (#15215)

### Description
When calculating symbolic shape like `mul(get_int_val(values=[1024,
0.5]))`,
the current script calls `get_int_val()` to get values, which values
becomes `[1024, 0]`.
Thus, the result of `mul(values)`->`mul([1024,0])`=0, but the expected
shape size is 512

Fix: for math binary operations like `mul()` and `div()`, 
don't convert input shapes into integers if any possible precision loss
happen;
keep the input shape as float, finish the operation, and cast final
result into integer and output the shape.

Test cases are added:
1. mul(1024, 0.5)=>512 (before this fix, the output would be 0, as float
0.5 would be converted to int 0)
2. div(768, 1.5)=>512 (before this fix, the output would be 768, as
float 1.5 would be converted to int 0)

### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->
This commit is contained in:
yf711 2023-03-30 12:15:27 -07:00 committed by GitHub
parent c2dad6893b
commit dc61d3b5b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 80 additions and 12 deletions

View file

@ -548,7 +548,13 @@ class SymbolicShapeInference:
self.symbolic_dims_.update(new_dims)
return symbolic_shape_inference
def _get_int_values(self, node, broadcast=False):
def _get_int_or_float_values(self, node, broadcast=False, allow_float_values=False):
def int_or_float(value, allow_float_values):
# If casting into int has precision loss: keep float output
if allow_float_values and value % 1 != 0:
return value
return int(value)
values = [self._try_get_value(node, i) for i in range(len(node.input))]
if all([v is not None for v in values]):
# some shape compute is in floating point, cast to int for sympy
@ -558,10 +564,10 @@ class SymbolicShapeInference:
if len(v.shape) > 1:
new_v = None # ignore value for rank > 1
elif len(v.shape) == 0:
new_v = int(v.item())
new_v = int_or_float(v.item(), allow_float_values)
else:
assert len(v.shape) == 1
new_v = [int(vv) for vv in v]
new_v = [int_or_float(vv, allow_float_values) for vv in v]
values[i] = new_v
values_len = [len(v) if type(v) == list else 0 for v in values]
max_len = max(values_len)
@ -581,7 +587,15 @@ class SymbolicShapeInference:
def _compute_on_sympy_data(self, node, op_func):
assert len(node.output) == 1
values = self._get_int_values(node, broadcast=True)
# Before mul & div operations
# cast inputs into interger might lose decimal part and reduce precision
# keep them as float, finish the operation, then cast the result into integer
if node.op_type in ["Mul", "Div"]:
values = self._get_int_or_float_values(node, broadcast=True, allow_float_values=True)
else:
values = self._get_int_or_float_values(node, broadcast=True)
if all([v is not None for v in values]):
is_list = [type(v) == list for v in values]
as_list = any(is_list)
@ -781,7 +795,9 @@ class SymbolicShapeInference:
def _infer_symbolic_compute_ops(self, node):
funcs = {
"Add": lambda l: l[0] + l[1], # noqa: E741
"Div": lambda l: l[0] // l[1], # integer div in sympy # noqa: E741
"Div": lambda l: int(l[0] // l[1]) # noqa: E741
if isinstance(l[0] // l[1], float)
else l[0] // l[1], # integer div in sympy
"Equal": lambda l: l[0] == l[1], # noqa: E741
"Floor": lambda l: sympy.floor(l[0]), # noqa: E741
"Max": lambda l: l[1] # noqa: E741
@ -790,7 +806,7 @@ class SymbolicShapeInference:
"Min": lambda l: l[1] # noqa: E741
if is_literal(l[0]) and int(l[0]) > self.int_max_
else (l[0] if is_literal(l[1]) and int(l[1]) > self.int_max_ else sympy.Min(l[0], l[1])),
"Mul": lambda l: l[0] * l[1], # noqa: E741
"Mul": lambda l: int(l[0] * l[1]) if isinstance(l[0] * l[1], float) else l[0] * l[1], # noqa: E741
"Sub": lambda l: l[0] - l[1], # noqa: E741
"Where": lambda l: l[1] if l[0] else l[2], # noqa: E741
"Neg": lambda l: -l[0], # noqa: E741
@ -832,7 +848,7 @@ class SymbolicShapeInference:
def _infer_Concat(self, node): # noqa: N802
if any([i in self.sympy_data_ or i in self.initializers_ for i in node.input]):
values = self._get_int_values(node)
values = self._get_int_or_float_values(node)
if all([v is not None for v in values]):
assert get_attribute(node, "axis") == 0
self.sympy_data_[node.output[0]] = []
@ -895,7 +911,7 @@ class SymbolicShapeInference:
self.sympy_data_[node.output[0]] = numpy_helper.to_array(t)
def _infer_ConstantOfShape(self, node): # noqa: N802
sympy_shape = self._get_int_values(node)[0]
sympy_shape = self._get_int_or_float_values(node)[0]
vi = self.known_vi_[node.output[0]]
if sympy_shape is not None:
if type(sympy_shape) != list:
@ -1442,7 +1458,7 @@ class SymbolicShapeInference:
def _infer_Range(self, node): # noqa: N802
vi = self.known_vi_[node.output[0]]
input_data = self._get_int_values(node)
input_data = self._get_int_or_float_values(node)
if all([i is not None for i in input_data]):
start = as_scalar(input_data[0])
limit = as_scalar(input_data[1])
@ -1496,7 +1512,7 @@ class SymbolicShapeInference:
axes = get_attribute(node, "axes")
keep_dims = get_attribute(node, "keepdims", 1)
if keep_dims == 0 and axes == [0]:
data = self._get_int_values(node)[0]
data = self._get_int_or_float_values(node)[0]
if data is not None:
self.sympy_data_[node.output[0]] = sympy_reduce_product(data)
@ -1964,7 +1980,7 @@ class SymbolicShapeInference:
if get_opset(self.out_mp_) <= 9:
k = get_attribute(node, "k")
else:
k = self._get_int_values(node)[1]
k = self._get_int_or_float_values(node)[1]
if k is None:
k = self._new_symbolic_dim_from_output(node)

View file

@ -3,9 +3,11 @@
import os
import numpy
# -*- coding: UTF-8 -*-
import onnx
from onnx import AttributeProto, GraphProto, TensorProto, helper # noqa: F401
from onnx import AttributeProto, GraphProto, TensorProto, helper, numpy_helper # noqa: F401
if os.path.exists(
os.path.join(
@ -340,6 +342,56 @@ class TestSymbolicShapeInferenceForOperators(unittest.TestCase):
def test_einsum_transpose(self):
self._test_einsum_one_input_impl(["a", "b"], ["b", "a"], "ij -> ji")
def test_mul_precision(self):
graph_input = onnx.helper.make_tensor_value_info("input", TensorProto.FLOAT, [1024])
graph_output = onnx.helper.make_tensor_value_info("output", TensorProto.FLOAT, None)
# initializers
value = numpy.array([0.5], dtype=numpy.float32)
constant = numpy_helper.from_array(value, name="constant")
nodes = [
# Get the shape of the input tensor: `input_tensor_shape = [1024]`.
onnx.helper.make_node("Shape", ["input"], ["input_shape"]),
# mul(1024, 0.5) => 512
onnx.helper.make_node("Mul", ["input_shape", "constant"], ["output_shape"]),
# Resize input
onnx.helper.make_node(
"Resize", inputs=["input", "", "", "output_shape"], outputs=["output"], mode="nearest"
),
]
graph_def = onnx.helper.make_graph(nodes, "TestMulPrecision", [graph_input], [graph_output], [constant])
model = SymbolicShapeInference.infer_shapes(onnx.helper.make_model(graph_def))
output_dims = unique_element(model.graph.output).type.tensor_type.shape.dim
self.assertEqual(len(output_dims), 1)
self.assertEqual(output_dims[0].dim_value, 512)
def test_div_precision(self):
graph_input = onnx.helper.make_tensor_value_info("input", TensorProto.FLOAT, [768])
graph_output = onnx.helper.make_tensor_value_info("output", TensorProto.FLOAT, None)
# initializers
value = numpy.array([1.5], dtype=numpy.float32)
constant = numpy_helper.from_array(value, name="constant")
nodes = [
# Get the shape of the input tensor: `input_tensor_shape = [768]`.
onnx.helper.make_node("Shape", ["input"], ["input_shape"]),
# div(768, 1.5) => 512
onnx.helper.make_node("Div", ["input_shape", "constant"], ["output_shape"]),
# Resize input
onnx.helper.make_node(
"Resize", inputs=["input", "", "", "output_shape"], outputs=["output"], mode="nearest"
),
]
graph_def = onnx.helper.make_graph(nodes, "TestDivPrecision", [graph_input], [graph_output], [constant])
model = SymbolicShapeInference.infer_shapes(onnx.helper.make_model(graph_def))
output_dims = unique_element(model.graph.output).type.tensor_type.shape.dim
self.assertEqual(len(output_dims), 1)
self.assertEqual(output_dims[0].dim_value, 512)
class TestSymbolicShapeInferenceForSlice(unittest.TestCase):
def check_slice_of_concat(self, input_dims, start, end, step, expected_output_dim):