blob: b4fa49ffa288602692d459980aba5568a61d819e [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Arm Compute Library integration conv2d tests."""
import numpy as np
import pytest
import tvm
from tvm import relay
from test_arm_compute_lib.infrastructure import (
QNN_DTYPES,
get_low_high_atol_rtol,
skip_runtime_test,
skip_codegen_test,
build_and_run,
verify,
verify_codegen,
)
from test_arm_compute_lib.infrastructure import Device
def _get_model(
shape,
kernel_h,
kernel_w,
padding,
strides,
dilation,
groups,
dtype,
channels,
var_names,
has_bias=False,
has_activation=False,
has_pad=False,
):
"""Return a model and any parameters it may have"""
a = relay.var(next(var_names), shape=shape, dtype=dtype)
if has_pad:
p = ((0, 0), (padding[0], padding[0]), (padding[1], padding[1]), (0, 0))
a = relay.nn.pad(a, pad_width=p)
padding = (0, 0, 0, 0)
else:
if len(padding) == 2:
padding = (padding[0], padding[1], padding[0], padding[1])
shape = (shape[0], shape[1] + padding[0] * 2, shape[2] + padding[1] * 2, shape[3])
is_depthwise = shape[3] == channels == groups
weight_format = "HWOI" if is_depthwise else "HWIO"
if weight_format == "HWIO":
weight_shape = (kernel_h, kernel_w, shape[3] // groups, channels)
else:
weight_shape = (kernel_h, kernel_w, channels, shape[3] // groups)
w = tvm.nd.array(np.random.uniform(-128, 127, weight_shape).astype(dtype))
weights = relay.const(w, dtype)
out = relay.nn.conv2d(
a,
weights,
kernel_size=(kernel_h, kernel_w),
data_layout="NHWC",
kernel_layout=weight_format,
dilation=dilation,
strides=strides,
padding=padding,
groups=groups,
channels=channels,
out_dtype=dtype,
)
params = {"w": w}
if has_bias:
bias_shape = weight_shape[2] if is_depthwise else weight_shape[3]
b = tvm.nd.array(np.random.uniform(-128, 127, bias_shape).astype(dtype))
biasc = relay.const(b, dtype)
out = relay.nn.bias_add(out, biasc, axis=3)
params["b"] = b
if has_activation:
out = relay.nn.relu(out)
return out, params
def _get_qnn_params(input_zp, input_sc, kernel_zp, kernel_sc, kernel_h, kernel_w, channels):
"""Get output qnn parameters given input and kernel parameters."""
input_max = input_sc * (255 - input_zp)
input_min = -input_sc * input_zp
kernel_max = kernel_sc * (255 - kernel_zp)
kernel_min = -kernel_sc * kernel_zp
output_limits = [
kernel_max * kernel_h * kernel_w * channels * input_max,
kernel_min * kernel_h * kernel_w * channels * input_max,
kernel_min * kernel_h * kernel_w * channels * input_min,
kernel_max * kernel_h * kernel_w * channels * input_min,
]
output_max = max(output_limits)
output_min = min(output_limits)
output_sc = (output_max - output_min) / 255
output_zp = -int(output_min / output_sc)
return output_zp, output_sc
def _get_qnn_model(
shape,
kernel_h,
kernel_w,
padding,
strides,
dilation,
groups,
dtype,
channels,
input_zp,
input_sc,
kernel_zp,
kernel_sc,
output_zp,
output_sc,
var_names,
has_bias=False,
has_activation=False,
has_pad=False,
):
"""Return a model and any parameters it may have."""
low, high, _, _ = get_low_high_atol_rtol(dtype)
a = relay.var(next(var_names), shape=shape, dtype=dtype)
if has_pad:
p = ((0, 0), (padding[0], padding[0]), (padding[1], padding[1]), (0, 0))
a = relay.nn.pad(a, pad_width=p, pad_value=input_zp, pad_mode="constant")
padding = (0, 0, 0, 0)
else:
if len(padding) == 2:
padding = (padding[0], padding[1], padding[0], padding[1])
shape = (shape[0], shape[1] + padding[0] * 2, shape[2] + padding[1] * 2, shape[3])
is_depthwise = shape[3] == channels == groups
weight_format = "HWOI" if is_depthwise else "HWIO"
if weight_format == "HWIO":
weight_shape = (kernel_h, kernel_w, shape[3] // groups, channels)
else:
weight_shape = (kernel_h, kernel_w, channels, shape[3] // groups)
w = tvm.nd.array(np.random.uniform(low, high, weight_shape).astype(dtype))
weights = relay.const(w, dtype)
out = relay.qnn.op.conv2d(
a,
weights,
input_zero_point=relay.const(input_zp, "int32"),
kernel_zero_point=relay.const(kernel_zp, "int32"),
input_scale=relay.const(input_sc, "float32"),
kernel_scale=relay.const(kernel_sc, "float32"),
kernel_size=(kernel_h, kernel_w),
data_layout="NHWC",
kernel_layout=weight_format,
dilation=dilation,
strides=strides,
padding=padding,
groups=groups,
channels=channels,
out_dtype="int32",
)
params = {"w": w}
if has_bias:
bias_shape = weight_shape[2] if is_depthwise else weight_shape[3]
b = tvm.nd.array(np.random.uniform(-128, 127, bias_shape).astype("int32"))
biasc = relay.const(b, "int32")
out = relay.nn.bias_add(out, biasc, axis=3)
params["b"] = b
if has_activation:
out = relay.nn.relu(out)
req = relay.qnn.op.requantize(
out,
relay.const(input_sc * kernel_sc, "float32"), # input scale
relay.const(0, "int32"), # input zero point
relay.const(output_sc, "float32"), # output scale
relay.const(output_zp, "int32"), # output zero point
out_dtype=dtype,
)
return req, params
def _get_expected_codegen(
shape,
kernel_h,
kernel_w,
padding,
strides,
dilation,
groups,
dtype,
channels,
has_bias=False,
has_activation=False,
):
if len(padding) == 2:
padding = (padding[0], padding[1], padding[0], padding[1])
output_height = ((shape[1] - kernel_h + padding[0] + padding[2]) / strides[0]) + 1
output_width = ((shape[2] - kernel_w + padding[1] + padding[3]) / strides[1]) + 1
output_shape = (1, int(output_height), int(output_width), channels)
out_dtype = "int32" if dtype in QNN_DTYPES else "float32"
is_depthwise = shape[3] == channels == groups
weight_format = "IHWO" if is_depthwise else "OHWI"
if weight_format == "IHWO":
weight_shape = (shape[3] // groups, kernel_h, kernel_w, channels)
else:
weight_shape = (channels, kernel_h, kernel_w, shape[3] // groups)
if is_depthwise:
name = "nn.depthwise_conv2d"
else:
name = "nn.conv2d"
node = {
"op": "kernel",
"name": name,
"inputs": [],
"attrs": {
"groups": [[str(groups)]],
"num_outputs": "1",
"data_layout": [["NHWC"]],
"kernel_layout": [[weight_format]],
"channels": [[str(channels)]],
"dilation": [[str(dilation[0]), str(dilation[1])]],
"out_layout": [[""]],
"out_dtype": [[out_dtype]],
"kernel_size": [[str(kernel_h), str(kernel_w)]],
"shape": [[list(output_shape)]],
"dtype": [[dtype]],
"padding": [[str(p) for p in padding]],
"strides": [[str(s) for s in strides]],
},
}
if has_activation:
node["attrs"]["activation_type"] = [["relu"]]
inputs = [
{"op": "input", "name": "", "attrs": {"shape": [[list(shape)]], "dtype": [[str(dtype)]]}},
{
"op": "const",
"name": "",
"attrs": {"shape": [[list(weight_shape)]], "dtype": [[str(dtype)]]},
},
]
# qnn.conv2d params, input and kernel
if dtype in QNN_DTYPES:
node["name"] = "qnn." + node["name"].split(".")[1]
for param_dtype in ["int32", "float32"]:
for _ in range(2):
inputs.append(
{
"op": "const",
"name": "",
"attrs": {"shape": [[[]]], "dtype": [[param_dtype]]},
}
)
if has_bias:
bias_dtype = "int32" if dtype in QNN_DTYPES else "float32"
inputs.append(
{
"op": "const",
"name": "",
"attrs": {
"shape": [[[1, 1, 1, weight_shape[3] if is_depthwise else weight_shape[0]]]],
"dtype": [[bias_dtype]],
},
}
)
# qnn.conv2d params, output
if dtype in QNN_DTYPES:
for param_dtype in ["float32", "int32"]:
inputs.append(
{"op": "const", "name": "", "attrs": {"shape": [[[]]], "dtype": [[param_dtype]]}}
)
input_idx = 0
for _ in range(len(inputs)):
node["inputs"].append([input_idx, 0, 0])
input_idx += 1
node["attrs"]["num_inputs"] = str(len(inputs))
inputs.append(node)
return inputs
@pytest.mark.parametrize(
"trial",
[
# Normal convolution
[2, 2, (1, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, False, False), False],
[2, 1, (2, 2), (1, 1), (1, 1), 7, (12, 15, 16), (False, False, True), False],
[3, 3, (2, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, True, False), False],
[3, 3, (1, 1), (1, 1), (1, 1), 16, (12, 15, 16), (False, False, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[1, 3, (1, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, True), False],
[2, 2, (2, 2), (1, 1), (1, 1), 4, (20, 20, 20), (False, True, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[3, 3, (2, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, False), False],
[3, 3, (1, 1), (2, 2), (1, 1), 16, (10, 10, 14), (False, True, True), False],
# Depth-wise convolution
[3, 3, (1, 1), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, True), True],
[5, 5, (2, 2), (1, 1), (1, 1), 20, (20, 20, 20), (False, True, False), True],
[3, 3, (2, 2), (2, 2), (1, 1), 14, (10, 10, 14), (True, False, False), True],
[5, 5, (0, 0), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, False), True],
[3, 3, (1, 1), (2, 2), (1, 1), 14, (10, 10, 14), (False, True, True), True],
],
)
def test_conv2d(trial):
Device.load("test_config.json")
if skip_runtime_test():
return
device = Device()
np.random.seed(0)
dtype = "float32"
(
kernel_h,
kernel_w,
pad,
stride,
dilation,
out_channels,
shape,
composite,
is_depthwise,
) = trial
shape = (1, *shape)
if is_depthwise:
groups = shape[3]
else:
groups = 1
outputs = []
inputs = {
"a": tvm.nd.array(np.random.uniform(-128, 127, shape).astype(dtype)),
}
func, params = _get_model(
shape,
kernel_h,
kernel_w,
pad,
stride,
dilation,
groups,
dtype,
out_channels,
iter(inputs),
has_pad=composite[0],
has_bias=composite[1],
has_activation=composite[2],
)
# Generate results for ACL conv2d and TVM native conv2d for comparison
for acl in [False, True]:
outputs.append(build_and_run(func, inputs, 1, params, device, enable_acl=acl)[0])
config = {
"shape": shape,
"groups": groups,
"kernel size": (kernel_h, kernel_w),
"padding": pad,
"stride": stride,
"dilation": dilation,
"out channels": out_channels,
"composite operators (pad, bias, activation)": composite,
}
verify(outputs, atol=0.002, rtol=0.01, config=config)
@pytest.mark.parametrize(
"trial",
[
# Normal convolution
[2, 2, (1, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, False, False), False],
[2, 1, (2, 2), (1, 1), (1, 1), 7, (12, 15, 16), (False, False, True), False],
[3, 3, (2, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, True, False), False],
[3, 3, (1, 1), (1, 1), (1, 1), 16, (12, 15, 16), (False, False, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[1, 3, (1, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, True), False],
[2, 2, (2, 2), (1, 1), (1, 1), 4, (20, 20, 20), (False, True, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[3, 3, (2, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, False), False],
[3, 3, (1, 1), (2, 2), (1, 1), 16, (10, 10, 14), (False, True, True), False],
# Depth-wise convolution
[3, 3, (1, 1), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, True), True],
[5, 5, (2, 2), (1, 1), (1, 1), 20, (20, 20, 20), (False, True, False), True],
[3, 3, (2, 2), (2, 2), (1, 1), 14, (10, 10, 14), (True, False, False), True],
[5, 5, (0, 0), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, False), True],
[3, 3, (1, 1), (2, 2), (1, 1), 14, (10, 10, 14), (False, True, True), True],
],
)
def test_codegen_conv2d(trial):
if skip_codegen_test():
return
dtype = "float32"
(
kernel_h,
kernel_w,
pad,
stride,
dilation,
out_channels,
shape,
composite,
is_depthwise,
) = trial
shape = (1, *shape)
if is_depthwise:
groups = shape[3]
else:
groups = 1
inputs = {"a"}
args = (shape, kernel_h, kernel_w, pad, stride, dilation, groups, dtype, out_channels)
func, params = _get_model(
*args,
var_names=iter(inputs),
has_pad=composite[0],
has_bias=composite[1],
has_activation=composite[2],
)
exp_codegen = _get_expected_codegen(*args, has_bias=composite[1], has_activation=composite[2])
verify_codegen(func, exp_codegen, 1)
@pytest.mark.parametrize(
"trial",
[
# Normal convolution
[2, 2, (1, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, False, False), False],
[2, 1, (2, 2), (1, 1), (1, 1), 7, (12, 15, 16), (False, False, True), False],
[3, 3, (2, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, True, False), False],
[3, 3, (1, 1), (1, 1), (1, 1), 16, (12, 15, 16), (False, False, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[1, 3, (1, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, True), False],
[2, 2, (2, 2), (1, 1), (1, 1), 4, (20, 20, 20), (False, True, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[3, 3, (2, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, False), False],
[3, 3, (1, 1), (2, 2), (1, 1), 16, (10, 10, 14), (False, True, True), False],
# Depth-wise convolution
[3, 3, (1, 1), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, True), True],
[5, 5, (2, 2), (1, 1), (1, 1), 20, (20, 20, 20), (False, True, False), True],
[3, 3, (2, 2), (2, 2), (1, 1), 14, (10, 10, 14), (True, False, False), True],
[5, 5, (0, 0), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, False), True],
[3, 3, (1, 1), (2, 2), (1, 1), 14, (10, 10, 14), (False, True, True), True],
],
)
@pytest.mark.parametrize("dtype", QNN_DTYPES)
def test_qnn_conv2d(trial, dtype):
Device.load("test_config.json")
if skip_runtime_test():
return
device = Device()
np.random.seed(0)
(
kernel_h,
kernel_w,
pad,
stride,
dilation,
out_channels,
shape,
composite,
is_depthwise,
) = trial
shape = (1, *shape)
if is_depthwise:
groups = shape[3]
else:
groups = 1
outputs = []
inputs = {"a": tvm.nd.array(np.random.uniform(0, 255, shape).astype(dtype))}
input_zp = 100
input_sc = 0.5
kernel_zp = 25
kernel_sc = 0.03
output_zp, output_sc = _get_qnn_params(
input_zp, input_sc, kernel_zp, kernel_sc, kernel_h, kernel_w, shape[3]
)
func, params = _get_qnn_model(
shape,
kernel_h,
kernel_w,
pad,
stride,
dilation,
groups,
dtype,
out_channels,
input_zp,
input_sc,
kernel_zp,
kernel_sc,
output_zp,
output_sc,
iter(inputs),
has_pad=composite[0],
has_bias=composite[1],
has_activation=composite[2],
)
for acl in [False, True]:
outputs.append(build_and_run(func, inputs, 1, params, device, enable_acl=acl)[0])
config = {
"shape": shape,
"groups": groups,
"kernel size": (kernel_h, kernel_w),
"padding": pad,
"stride": stride,
"dilation": dilation,
"out channels": out_channels,
"composite operators (pad, bias, activation)": composite,
"input scale": input_sc,
"input zero point": input_zp,
"kernel scale": kernel_sc,
"kernel zero point": kernel_zp,
"output scale": output_sc,
"output zero point": output_zp,
}
atol = 2 if is_depthwise else 1
verify(outputs, atol=atol, rtol=0, config=config, verify_saturation=True)
@pytest.mark.parametrize("dtype", QNN_DTYPES)
@pytest.mark.parametrize(
"trial",
[
# Normal convolution
[2, 2, (1, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, False, False), False],
[2, 1, (2, 2), (1, 1), (1, 1), 7, (12, 15, 16), (False, False, True), False],
[3, 3, (2, 1), (1, 1), (1, 1), 4, (10, 10, 14), (False, True, False), False],
[3, 3, (1, 1), (1, 1), (1, 1), 16, (12, 15, 16), (False, False, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[1, 3, (1, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, True), False],
[2, 2, (2, 2), (1, 1), (1, 1), 4, (20, 20, 20), (False, True, False), False],
[5, 5, (1, 1), (2, 2), (1, 1), 4, (10, 10, 14), (True, False, False), False],
[3, 3, (2, 1), (1, 1), (1, 1), 7, (20, 20, 20), (False, False, False), False],
[3, 3, (1, 1), (2, 2), (1, 1), 16, (10, 10, 14), (False, True, True), False],
# Depth-wise convolution
[3, 3, (1, 1), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, True), True],
[5, 5, (2, 2), (1, 1), (1, 1), 20, (20, 20, 20), (False, True, False), True],
[3, 3, (2, 2), (2, 2), (1, 1), 14, (10, 10, 14), (True, False, False), True],
[5, 5, (0, 0), (1, 1), (1, 1), 20, (20, 20, 20), (False, False, False), True],
[3, 3, (1, 1), (2, 2), (1, 1), 14, (10, 10, 14), (False, True, True), True],
],
)
def test_codegen_qnn_conv2d(trial, dtype):
if skip_codegen_test():
return
(
kernel_h,
kernel_w,
pad,
stride,
dilation,
out_channels,
shape,
composite,
is_depthwise,
) = trial
shape = (1, *shape)
if is_depthwise:
groups = shape[3]
else:
groups = 1
inputs = {"a"}
input_zp = 100
input_sc = 0.5
kernel_zp = 25
kernel_sc = 0.03
output_zp, output_sc = _get_qnn_params(
input_zp, input_sc, kernel_zp, kernel_sc, kernel_h, kernel_w, shape[3]
)
args = (shape, kernel_h, kernel_w, pad, stride, dilation, groups, dtype, out_channels)
func, params = _get_qnn_model(
*args,
input_zp=input_zp,
input_sc=input_sc,
kernel_zp=kernel_zp,
kernel_sc=kernel_sc,
output_zp=output_zp,
output_sc=output_sc,
var_names=iter(inputs),
has_pad=composite[0],
has_bias=composite[1],
has_activation=composite[2],
)
exp_codegen = _get_expected_codegen(*args, has_bias=composite[1], has_activation=composite[2])
verify_codegen(func, exp_codegen, 1)
@pytest.mark.parametrize(
"param",
["kernel_sc", "kernel_zp"],
)
def test_codegen_qnn_conv2d_per_channel_quantization(param):
if skip_codegen_test():
return
dtype = "int8"
kernel_h = 2
kernel_w = 2
pad = (1, 1)
stride = (1, 1)
dilation = (1, 1)
out_channels = 4
shape = (1, 10, 10, 14)
composite = (False, False, False)
groups = 1
inputs = {"a"}
qnn_params = {
"input_zp": 1,
"input_sc": 1,
"kernel_zp": 1,
"kernel_sc": 1,
"output_zp": 1,
"output_sc": 1,
}
qnn_params[param] = [1, 1, 1, 1]
args = (shape, kernel_h, kernel_w, pad, stride, dilation, groups, dtype, out_channels)
func, params = _get_qnn_model(
*args,
input_zp=qnn_params["input_zp"],
input_sc=qnn_params["input_sc"],
kernel_zp=qnn_params["kernel_zp"],
kernel_sc=qnn_params["kernel_sc"],
output_zp=qnn_params["output_zp"],
output_sc=qnn_params["output_sc"],
var_names=iter(inputs),
has_pad=composite[0],
has_bias=composite[1],
has_activation=composite[2],
)
exp_codegen = _get_expected_codegen(*args, has_bias=composite[1], has_activation=composite[2])
verify_codegen(func, exp_codegen, num_acl_modules=0, tvm_ops=2)
if __name__ == "__main__":
test_conv2d()
test_qnn_conv2d()
test_codegen_conv2d()
test_codegen_qnn_conv2d()
test_codegen_qnn_conv2d_per_channel_quantization()