Skip to content

Commit a74ef13

Browse files
[ONNX] QLinearAveragePool and QLinearGlobalAveragePool contrib op (#9017)
* [ONNX] QLinearAveragePool and QLinearGlobalAveragePool contrib op * Fix linter error for variable name and else after return * Separate quantized avg_pool impl and add TODO for global_avg_pool * Fix comment typo
1 parent db78d96 commit a74ef13

File tree

2 files changed

+225
-10
lines changed

2 files changed

+225
-10
lines changed

python/tvm/relay/frontend/onnx.py

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ class Pool(OnnxOpConverter):
276276

277277
@classmethod
278278
def _impl_v1(cls, inputs, attr, params):
279+
attr_cvt, data = cls._run_calculation(inputs, attr, params)
280+
return attr_cvt([data], attr, params)
281+
282+
@classmethod
283+
def _run_calculation(cls, inputs, attr, params):
284+
"""Helper method to return the processed input data and AttrCvt object"""
285+
279286
data = inputs[0]
280287
input_shape = infer_shape(data)
281288
input_dtype = infer_type(data).checked_type.dtype
@@ -325,16 +332,19 @@ def _impl_v1(cls, inputs, attr, params):
325332
else:
326333
attr["layout"] = onnx_default_layout(dims=(len(input_shape) - 2), op_name=cls.name)
327334

328-
return AttrCvt(
329-
op_name=dimension_picker(cls.name),
330-
transforms={
331-
"kernel_shape": "pool_size",
332-
"pads": ("padding", 0),
333-
"dilations": ("dilation", 1),
334-
},
335-
ignores=["storage_order"],
336-
custom_check=dimension_constraint(),
337-
)([data], attr, params)
335+
return (
336+
AttrCvt(
337+
op_name=dimension_picker(cls.name),
338+
transforms={
339+
"kernel_shape": "pool_size",
340+
"pads": ("padding", 0),
341+
"dilations": ("dilation", 1),
342+
},
343+
ignores=["storage_order"],
344+
custom_check=dimension_constraint(),
345+
),
346+
data,
347+
)
338348

339349

340350
class Absolute(Unary):
@@ -355,6 +365,29 @@ class AveragePool(Pool):
355365
name = "avg_pool"
356366

357367

368+
class QLinearAveragePool(Pool):
369+
"""Operator converter for QLinearAveragePool from Microsoft onnxruntime contrib opset."""
370+
371+
name = "avg_pool"
372+
373+
@classmethod
374+
def _impl_v1(cls, inputs, attr, params):
375+
x_scale = get_scalar(inputs[1], params)
376+
x_zero_point = get_scalar(inputs[2], params, dtype="int32")
377+
y_scale = fold_constant(get_scalar(inputs[3], params))
378+
y_zero_point = get_scalar(inputs[4], params, dtype="int32")
379+
380+
attr_cvt, data = cls._run_calculation(inputs, attr, params)
381+
382+
input_dtype = infer_type(data).checked_type.dtype
383+
# Onnxruntime doesn't actually do this op in integer, they dequantize to fp32
384+
# and then requantize afer (according to documentation below)
385+
# https://github.com/microsoft/onnxruntime/blob/master/docs/ContribOperators.md#com.microsoft.QLinearAveragePool
386+
float_node = _qnn.op.dequantize(data, x_scale, x_zero_point)
387+
out = attr_cvt([float_node], attr, params)
388+
return _qnn.op.quantize(out, y_scale, y_zero_point, out_dtype=input_dtype)
389+
390+
358391
class BatchNorm(OnnxOpConverter):
359392
"""Operator converter for BatchNorm."""
360393

@@ -658,6 +691,40 @@ def _impl_v1(cls, inputs, attr, params):
658691
)
659692

660693

694+
class QLinearGlobalAveragePool(OnnxOpConverter):
695+
"Operator converter for QLinearGlobalAveragePool from Microsoft onnxruntime contrib opset."
696+
697+
@classmethod
698+
def _impl_v1(cls, inputs, attr, params):
699+
rank = len(infer_shape(inputs[0]))
700+
701+
x_scale = get_scalar(inputs[1], params)
702+
x_zero_point = get_scalar(inputs[2], params, dtype="int32")
703+
y_scale = fold_constant(get_scalar(inputs[3], params))
704+
y_zero_point = get_scalar(inputs[4], params, dtype="int32")
705+
706+
input_dtype = infer_type(inputs[0]).checked_type.dtype
707+
708+
# Onnxruntime documentation does not mention that this global avg_pool should follow the
709+
# sequence dequantize -> float op -> quantize, but that is how QLinearAveragePool is done.
710+
#
711+
# This op also follows the same pattern since qnn op is not available right now.
712+
# TODO: Generate QNN op to perform quantized operation instead of dequant -> op -> quant
713+
x = _qnn.op.dequantize(inputs[0], x_scale, x_zero_point)
714+
if rank == 3:
715+
out = _op.nn.global_avg_pool1d(x)
716+
elif rank == 4:
717+
out = _op.nn.global_avg_pool2d(x)
718+
elif rank == 5:
719+
out = _op.nn.global_avg_pool3d(x)
720+
else:
721+
raise NotImplementedError(
722+
"Global average pooling is only implemented for 1D, 2D, and 3D kernels, got %dD."
723+
% (rank - 2),
724+
)
725+
return _qnn.op.quantize(out, y_scale, y_zero_point, out_dtype=input_dtype)
726+
727+
661728
class GlobalMaxPool(OnnxOpConverter):
662729
"""Operator converter for GlobalMaxPool"""
663730

@@ -3964,6 +4031,8 @@ def _get_convert_map(opset):
39644031
"QLinearAdd": QLinearAdd.get_converter(opset),
39654032
"QLinearMul": QLinearMul.get_converter(opset),
39664033
"ConvInteger": ConvInteger.get_converter(opset),
4034+
"QLinearAveragePool": QLinearAveragePool.get_converter(opset),
4035+
"QLinearGlobalAveragePool": QLinearGlobalAveragePool.get_converter(opset),
39674036
# Random number generation.
39684037
"RandomUniform": RandomUniform.get_converter(opset),
39694038
# Loss functions / training

tests/python/frontend/onnx/test_forward.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3056,6 +3056,152 @@ def verify_global_pooling(x_shape, mode):
30563056
verify_global_pooling([4, 1, 2, 6, 4], mode)
30573057

30583058

3059+
@tvm.testing.parametrize_targets
3060+
def test_qlinear_average_pool(target, dev):
3061+
def verify_qlinear_average_pool(
3062+
x_shape, kernel_shape, strides, pads, out_shape, auto_pad="NOTSET"
3063+
):
3064+
input_nodes = [
3065+
helper.make_tensor_value_info("X", TensorProto.FLOAT, list(x_shape)),
3066+
]
3067+
3068+
output_nodes = [
3069+
helper.make_tensor_value_info("Y", TensorProto.FLOAT, list(out_shape)),
3070+
]
3071+
3072+
input_names = ["X"]
3073+
3074+
node = helper.make_node(
3075+
"AveragePool",
3076+
inputs=input_names,
3077+
outputs=["Y"],
3078+
kernel_shape=kernel_shape,
3079+
strides=strides,
3080+
)
3081+
3082+
if pads is None:
3083+
pad_attr = helper.make_attribute("auto_pad", auto_pad)
3084+
else:
3085+
pad_attr = helper.make_attribute("pads", pads)
3086+
node.attribute.append(pad_attr)
3087+
3088+
graph = helper.make_graph(
3089+
[node],
3090+
"qlinear_average_pool_test",
3091+
inputs=input_nodes,
3092+
outputs=output_nodes,
3093+
)
3094+
3095+
model = helper.make_model(graph, producer_name="qlinear_average_pool_Test")
3096+
quantize_and_verify_with_ort(model, input_names, [x_shape], target, dev)
3097+
3098+
# Pool1D
3099+
verify_qlinear_average_pool(
3100+
x_shape=[1, 1, 32],
3101+
kernel_shape=[3],
3102+
strides=[1],
3103+
pads=[1, 1],
3104+
out_shape=[1, 1, 32],
3105+
)
3106+
# Pool2D
3107+
verify_qlinear_average_pool(
3108+
x_shape=[1, 1, 32, 32],
3109+
kernel_shape=[3, 3],
3110+
strides=[1, 1],
3111+
pads=[1, 1, 1, 1],
3112+
out_shape=[1, 1, 32, 32],
3113+
)
3114+
3115+
# Pool1D with stride
3116+
verify_qlinear_average_pool(
3117+
x_shape=[1, 1, 32],
3118+
kernel_shape=[3],
3119+
strides=[2],
3120+
pads=[1, 1],
3121+
out_shape=[1, 1, 16],
3122+
)
3123+
# Pool2D with stride
3124+
verify_qlinear_average_pool(
3125+
x_shape=[1, 1, 32, 32],
3126+
kernel_shape=[3, 3],
3127+
strides=[2, 2],
3128+
pads=[1, 1, 1, 1],
3129+
out_shape=[1, 1, 16, 16],
3130+
)
3131+
3132+
# Pool1D with stride and autopadding
3133+
verify_qlinear_average_pool(
3134+
x_shape=[1, 1, 32],
3135+
kernel_shape=[3],
3136+
strides=[2],
3137+
pads=None,
3138+
out_shape=[1, 1, 16],
3139+
auto_pad="SAME_UPPER",
3140+
)
3141+
# Pool2D with stride and autopadding
3142+
verify_qlinear_average_pool(
3143+
x_shape=[1, 1, 32, 32],
3144+
kernel_shape=[3, 3],
3145+
strides=[2, 2],
3146+
pads=None,
3147+
out_shape=[1, 1, 16, 16],
3148+
auto_pad="SAME_UPPER",
3149+
)
3150+
3151+
# Pool3D with stride
3152+
verify_qlinear_average_pool(
3153+
x_shape=[1, 1, 32, 32, 32],
3154+
kernel_shape=[3, 3, 3],
3155+
strides=[2, 2, 2],
3156+
pads=[1, 1, 1, 1, 1, 1],
3157+
out_shape=[1, 1, 16, 16, 16],
3158+
)
3159+
3160+
# Pool3D with stride and autopadding
3161+
verify_qlinear_average_pool(
3162+
x_shape=[1, 1, 32, 32, 32],
3163+
kernel_shape=[3, 3, 3],
3164+
strides=[2, 2, 2],
3165+
pads=None,
3166+
out_shape=[1, 1, 16, 16, 16],
3167+
auto_pad="SAME_UPPER",
3168+
)
3169+
3170+
3171+
@tvm.testing.parametrize_targets
3172+
def test_qlinear_global_average_pool(target, dev):
3173+
def verify_qlinear_global_average_pool(x_shape):
3174+
out_shape = x_shape[:2] + [1] * (len(x_shape) - 2)
3175+
3176+
node_type = "GlobalAveragePool"
3177+
3178+
input_names = ["X"]
3179+
3180+
pool_node = helper.make_node(node_type, inputs=input_names, outputs=["Y"])
3181+
3182+
graph = helper.make_graph(
3183+
[pool_node],
3184+
"qlinear_global_average_pool_test",
3185+
inputs=[helper.make_tensor_value_info("X", TensorProto.FLOAT, list(x_shape))],
3186+
outputs=[helper.make_tensor_value_info("Y", TensorProto.FLOAT, list(out_shape))],
3187+
)
3188+
3189+
model = helper.make_model(graph, producer_name="qlinear_global_average_pool_test")
3190+
quantize_and_verify_with_ort(model, input_names, [x_shape], target, dev)
3191+
3192+
# 1D Pooling (NCW)
3193+
verify_qlinear_global_average_pool([1, 8, 8])
3194+
verify_qlinear_global_average_pool([4, 1, 4])
3195+
3196+
# 2D Pooling (NCHW)
3197+
verify_qlinear_global_average_pool([1, 8, 8, 8])
3198+
verify_qlinear_global_average_pool([4, 1, 6, 4])
3199+
3200+
# 3D Pooling (NCDHW)
3201+
verify_qlinear_global_average_pool([1, 8, 6, 8, 8])
3202+
verify_qlinear_global_average_pool([4, 1, 2, 6, 4])
3203+
3204+
30593205
@tvm.testing.parametrize_targets
30603206
def test_mod(target, dev):
30613207
def verify_mod(x_shape, y_shape, fmod, out_shape, dtype="float32"):

0 commit comments

Comments
 (0)