| # SPDX-FileCopyrightText: Copyright 2020-2022 Arm Limited and/or its affiliates <open-source-office@arm.com> |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # Licensed 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 |
| # |
| # 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. |
| # |
| # Description: |
| # The TFLiteSupportedOperators class which is a collection of all TFLite supported operators and parameter checks. |
| from collections import defaultdict |
| |
| import numpy as np |
| |
| from .data_type import DataType |
| from .numeric_util import full_shape |
| from .operation import Op |
| from .operation import Padding |
| from .supported_operators_util import docstring_format_args |
| from .supported_operators_util import list_formatter |
| from .tensor import check_quantized_tens_scaling_equal |
| from .tflite_mapping import BUILTIN_OPERATOR_UNKNOWN |
| from .tflite_mapping import optype_to_builtintype |
| |
| |
| def _optype_formatter(op_list): |
| # Convert internal op types to external names |
| output = map(optype_to_builtintype, op_list) |
| # Remove UNKNOWNs |
| output = (x for x in output if x is not BUILTIN_OPERATOR_UNKNOWN) |
| return list_formatter(output) |
| |
| |
| class TFLiteSupportedOperators: |
| # Categorised lists of supported operators |
| npu_pre_ops = set( |
| ( |
| Op.SplitSliceRead, |
| Op.Shape, |
| ) |
| ) |
| convolution_ops = set( |
| ( |
| Op.Conv2DBias, |
| Op.Conv2D, |
| Op.QuantizedConv2D, |
| ) |
| ) |
| depthwise_convolution_ops = set((Op.DepthwiseConv2DBias,)) |
| transpose_convolution_ops = set((Op.Conv2DBackpropInput,)) |
| convolution_like_ops = convolution_ops | depthwise_convolution_ops | transpose_convolution_ops |
| max_pooling_ops = Op.op_set(Op.is_maxpool_op) |
| avg_pooling_ops = Op.op_set(Op.is_avgpool_op) |
| pooling_ops = set((Op.ReduceSum,)) | max_pooling_ops | avg_pooling_ops |
| resizing_ops = Op.op_set(Op.is_resize_op) |
| fc_vector_products = set( |
| ( |
| Op.QuantizedMatMul, |
| Op.MatMul, |
| Op.FullyConnected, |
| ) |
| ) |
| mac_main_ops = ( |
| # RNN/LSTM/GRU |
| set((Op.BlockLSTM,)) |
| # conv/depthwiseconv/transposeconv |
| | convolution_like_ops |
| # pooling |
| | pooling_ops |
| # resizing/upscaling |
| | resizing_ops |
| # FC layers |
| | fc_vector_products |
| # Mean (converts to depthwise conv) |
| | set((Op.Mean,)) |
| ) |
| unary_elem_wise_main_ops = Op.op_set(Op.is_unary_elementwise_op) |
| binary_elem_wise_min_max_ops = set( |
| ( |
| Op.Minimum, |
| Op.Maximum, |
| ) |
| ) |
| binary_elem_wise_shift_ops = set( |
| ( |
| Op.SHL, |
| Op.SHR, |
| ) |
| ) |
| binary_elem_wise_add_mul_sub = set( |
| ( |
| Op.Add, |
| Op.Mul, |
| Op.Sub, |
| ) |
| ) |
| binary_elem_wise_main_ops = binary_elem_wise_min_max_ops | binary_elem_wise_add_mul_sub | binary_elem_wise_shift_ops |
| elem_wise_main_ops = binary_elem_wise_main_ops | unary_elem_wise_main_ops |
| pad_ops = set((Op.Pad,)) |
| supported_int32_tensor_ops = ( |
| set( |
| ( |
| Op.ReduceSum, |
| Op.CLZ, |
| Op.Shape, |
| ) |
| ) |
| | binary_elem_wise_add_mul_sub |
| | binary_elem_wise_shift_ops |
| ) |
| |
| relu_ops = set( |
| ( |
| Op.Relu, |
| Op.Relu6, |
| Op.ReluN1To1, |
| Op.Clip, |
| ) |
| ) |
| activation_ops = relu_ops | set( |
| ( |
| Op.Tanh, |
| Op.Sigmoid, |
| Op.Softmax, |
| Op.HardSwish, |
| Op.LeakyRelu, |
| Op.Prelu, |
| ) |
| ) |
| npu_post_ops = ( |
| # activation functions |
| activation_ops |
| # concatenation write direction |
| | set((Op.ConcatSliceWrite,)) |
| # Quantization |
| | set((Op.Quantize,)) |
| ) |
| split_ops = set( |
| ( |
| Op.Split, |
| Op.SplitV, |
| Op.StridedSlice, |
| Op.Slice, |
| Op.UnpackReshaped, |
| Op.Unpack, |
| ) |
| ) |
| concat_ops = set( |
| ( |
| Op.Concat, |
| Op.ConcatTFLite, |
| Op.PackReshaped, |
| Op.Pack, |
| ) |
| ) |
| memory_only_ops = ( |
| set( |
| ( |
| Op.Reshape, |
| Op.QuantizedReshape, |
| Op.Squeeze, |
| Op.ExpandDims, |
| ) |
| ) |
| | concat_ops |
| | split_ops |
| ) |
| per_axis_quant_ops = convolution_like_ops # per-axis/channel quantization only currently supported for conv ops |
| supported_fused_activations = relu_ops | set( |
| ( |
| Op.Tanh, |
| Op.Sigmoid, |
| Op.LUT, |
| ) |
| ) |
| supported_operators = npu_pre_ops | mac_main_ops | elem_wise_main_ops | pad_ops | npu_post_ops | memory_only_ops |
| # Supported data types |
| supported_op_dtypes = set((DataType.uint8, DataType.int8, DataType.int16, DataType.int32)) |
| supported_faf_dtypes = set((DataType.uint8, DataType.int8, DataType.int16)) |
| supported_bias_dtypes = set((DataType.int32, DataType.int64)) |
| supported_pad_dtypes = set((DataType.int32, DataType.int64)) |
| # Defined ranges for allowed values: |
| tens_dim_range = (1, 65535) |
| stride_range = (1, 3) |
| dilated_height_range = (1, 64) |
| dilated_product_range = (1, 64 * 64) |
| weights_limit = 127 * 65536 |
| filter_range = (1, 8) |
| filter_height_range = (1, 256) |
| filter_product_range = (1, 256 * 256) |
| mean_kernel_product = 64 * 64 |
| mean_kernel_product_avgpool = 256 * 256 |
| |
| def __init__(self): |
| # Setup the generic constraints. Note: the order matters |
| self.generic_constraints = [] |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_tens_dtype) |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_tens_int32_ops) |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_tens_dimension) |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_tens_quant_per_axis) |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_batch_size) |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_faf) |
| self.generic_constraints.append(TFLiteSupportedOperators.constraint_faf_type) |
| |
| # Setup generic constraint exceptions |
| self.generic_constraints_exceptions = defaultdict(list) |
| self.generic_constraints_exceptions[Op.FullyConnected].append(TFLiteSupportedOperators.constraint_batch_size) |
| self.generic_constraints_exceptions[Op.Softmax].append(TFLiteSupportedOperators.constraint_batch_size) |
| self.generic_constraints_exceptions[Op.Reshape].append(TFLiteSupportedOperators.constraint_batch_size) |
| self.generic_constraints_exceptions[Op.Shape].append(TFLiteSupportedOperators.constraint_batch_size) |
| self.generic_constraints_exceptions[Op.Squeeze].append(TFLiteSupportedOperators.constraint_batch_size) |
| for op_type in TFLiteSupportedOperators.split_ops - set((Op.UnpackReshaped,)): |
| self.generic_constraints_exceptions[op_type].append(TFLiteSupportedOperators.constraint_batch_size) |
| |
| # Setup specific constraints. Note: the order matters |
| self.specific_constraints = defaultdict(list) |
| |
| # Conv-like checks: |
| for op_type in TFLiteSupportedOperators.convolution_like_ops: |
| if op_type not in TFLiteSupportedOperators.transpose_convolution_ops: |
| # Transpose Conv has a specific stride constraint (see constraint_tconv_stride below) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_stride_range) |
| |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_dilated_height_range) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_dilated_product_range) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_weights_type) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_weights_const) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_weights_limit) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_bias_shape) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_bias_type) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_bias_40bit) |
| # Transpose Conv specific checks: |
| for op_type in TFLiteSupportedOperators.transpose_convolution_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_tconv_stride) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_tconv_same) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_tconv_valid) |
| # Depthwise Conv specific checks: |
| for op_type in TFLiteSupportedOperators.depthwise_convolution_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_depth_multiplier) |
| |
| # Pooling checks: |
| for op_type in TFLiteSupportedOperators.pooling_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_stride_range) |
| # AVG pooling specific checks: |
| for op_type in TFLiteSupportedOperators.avg_pooling_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_filter_range) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_filter_height_range_valid_pad) |
| self.specific_constraints[op_type].append( |
| TFLiteSupportedOperators.constraint_filter_product_range_valid_pad |
| ) |
| # MAX pooling specific checks: |
| for op_type in TFLiteSupportedOperators.max_pooling_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_filter_height_range) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_filter_product_range) |
| |
| # Resizing specific checks: |
| for op_type in TFLiteSupportedOperators.resizing_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_resize) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_resize_size) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_resize_attrs) |
| |
| # Resize Bilinear specific checks: |
| self.specific_constraints[Op.ResizeBilinear].append( |
| TFLiteSupportedOperators.constraint_resizebi_half_pixel_centers_dims |
| ) |
| |
| # Vector Product specific checks: |
| for op_type in TFLiteSupportedOperators.fc_vector_products: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_weights_type) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_weights_const) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_bias_shape) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_bias_type) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_bias_40bit) |
| |
| # Element-wise checks |
| # Binary Min/Max specific checks: |
| for op_type in TFLiteSupportedOperators.binary_elem_wise_min_max_ops: |
| self.specific_constraints[op_type].append( |
| TFLiteSupportedOperators.constraint_matching_quantization_parameters |
| ) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_broadcast_shapes) |
| # Binary Add/Mul/Sub specific checks: |
| for op_type in TFLiteSupportedOperators.binary_elem_wise_add_mul_sub: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_broadcast_shapes) |
| # Binary Shift specific checks: |
| for op_type in TFLiteSupportedOperators.binary_elem_wise_shift_ops: |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_inputs_int32) |
| self.specific_constraints[op_type].append(TFLiteSupportedOperators.constraint_broadcast_shapes) |
| |
| # SHL specific checks: |
| self.specific_constraints[Op.SHL].append(TFLiteSupportedOperators.constraint_output_int32) |
| |
| # CLZ specific checks: |
| self.specific_constraints[Op.CLZ].append(TFLiteSupportedOperators.constraint_output_int32) |
| |
| # StridedSlice specific checks: |
| self.specific_constraints[Op.StridedSlice].append( |
| TFLiteSupportedOperators.constraint_stridedslice_stride_values |
| ) |
| |
| # Pad specific checks: |
| self.specific_constraints[Op.Pad].append(TFLiteSupportedOperators.constraint_pad_shape) |
| self.specific_constraints[Op.Pad].append(TFLiteSupportedOperators.constraint_padding_dimensions) |
| self.specific_constraints[Op.Pad].append(TFLiteSupportedOperators.constraint_pad_type) |
| |
| # Mean specific checks: |
| self.specific_constraints[Op.Mean].append(TFLiteSupportedOperators.constraint_mean_height_width_product_avgpool) |
| self.specific_constraints[Op.Mean].append(TFLiteSupportedOperators.constraint_mean_height_width_product) |
| self.specific_constraints[Op.Mean].append(TFLiteSupportedOperators.constraint_mean_height_single_axis) |
| |
| # Reshape specific checks: |
| self.specific_constraints[Op.Reshape].append(TFLiteSupportedOperators.constraint_reshape_shape_constant) |
| |
| def is_operator_supported(self, op): |
| ext_type = optype_to_builtintype(op.type) |
| if op.type not in TFLiteSupportedOperators.supported_operators: |
| if op.type not in (Op.Placeholder, Op.SubgraphInput, Op.Const): |
| print(f"Info: {ext_type} '{op.name}' is a CPU only op") |
| return False |
| |
| op_exceptions = self.generic_constraints_exceptions[op.type] |
| generic_constraints = [constraint for constraint in self.generic_constraints if constraint not in op_exceptions] |
| |
| for constraint in generic_constraints + self.specific_constraints[op.type]: |
| valid, extra = constraint(op) |
| if not valid: |
| print(f"Warning: {ext_type} '{op.name}' is not supported on the NPU. Placing on CPU instead") |
| print(f" - {constraint.__doc__}") |
| if extra: |
| print(f" {extra}") |
| return False |
| |
| return True |
| |
| @classmethod |
| @docstring_format_args([list_formatter(supported_op_dtypes)]) |
| def constraint_tens_dtype(cls, op): |
| "Tensors must be of type: {}" |
| valid = True |
| extra = [] |
| tensors = [tens for tens in op.get_ifm_ifm2_weights_ofm() if tens] |
| if not tensors: |
| tensors = [tens for tens in op.inputs if tens] |
| for tens in tensors: |
| if tens.dtype not in cls.supported_op_dtypes: |
| valid = False |
| extra.append(f"Tensor '{tens.name}' has data type: {tens.dtype}") |
| return valid, ", ".join(extra) |
| |
| @classmethod |
| @docstring_format_args([_optype_formatter(supported_int32_tensor_ops)]) |
| def constraint_tens_int32_ops(cls, op): |
| "Tensors which are int32 are only valid when op type is: {}" |
| valid = True |
| extra = [] |
| tensors = [tens for tens in op.get_ifm_ifm2_weights_ofm() if tens] |
| if not tensors: |
| tensors = [tens for tens in op.inputs if tens] |
| for tens in tensors: |
| if (tens.dtype == DataType.int32) and (op.type not in cls.supported_int32_tensor_ops): |
| valid = False |
| extra.append(tens.name) |
| extra = ", ".join(extra) |
| return valid, f"Op has int32 tensor(s): {extra}" |
| |
| @classmethod |
| @docstring_format_args(tens_dim_range) |
| def constraint_tens_dimension(cls, op): |
| "Tensor dimensions must be in the range [{}, {}]" |
| tens_min, tens_max = cls.tens_dim_range |
| valid = True |
| extra = [] |
| tensors = [tens for tens in op.get_ifm_ifm2_weights_ofm() if tens] |
| if not tensors: |
| tensors = [tens for tens in op.inputs if tens] |
| for tens in tensors: |
| if not all(tens_min <= dim <= tens_max for dim in tens.shape): |
| valid = False |
| extra.append(f"Tensor '{tens.name}' has shape: {tens.shape}") |
| return valid, ", ".join(extra) |
| |
| @classmethod |
| @docstring_format_args([_optype_formatter(per_axis_quant_ops)]) |
| def constraint_tens_quant_per_axis(cls, op): |
| "Per-axis quantization is only supported for the following op types: {}" |
| valid = True |
| extra = [] |
| if op.type not in cls.per_axis_quant_ops: |
| tensors = [tens for tens in op.get_ifm_ifm2_weights_ofm() if tens] |
| for tens in tensors: |
| if tens.quantization and tens.quantization.is_per_axis(): |
| valid = False |
| extra.append(tens.name) |
| return valid, "The following tensor(s) have per-axis quantization parameters: " + ", ".join(extra) |
| |
| @classmethod |
| @docstring_format_args([_optype_formatter(supported_fused_activations)]) |
| def constraint_faf(cls, op): |
| "The fused activation function (if present) must be one of type: {}" |
| if op.activation is None: |
| res = True, "Op has no fused activation function" |
| else: |
| faf = op.activation.op_type |
| valid = faf in cls.supported_fused_activations |
| res = valid, f"Op has its fused activation function as: {faf}" |
| return res |
| |
| @classmethod |
| @docstring_format_args([list_formatter(supported_faf_dtypes)]) |
| def constraint_faf_type(cls, op): |
| "If a fused activation function is present, the Output tensor must be one of type: {}" |
| if op.activation is None: |
| res = True, "Op has no fused activation function" |
| else: |
| valid = op.ofm.dtype in cls.supported_faf_dtypes |
| ext_type = optype_to_builtintype(op.activation.op_type) |
| res = valid, f"Op has fused activation function {ext_type}, and Output tensor data type: {op.ofm.dtype}" |
| return res |
| |
| @classmethod |
| @docstring_format_args(stride_range) |
| def constraint_stride_range(cls, op): |
| "Stride values for both width and height must be in the range [{}, {}]" |
| w, h = op.get_kernel_stride() |
| stride_min, stride_max = cls.stride_range |
| valid = (stride_min <= w <= stride_max) and (stride_min <= h <= stride_max) |
| return valid, f"Op has stride WxH as: {w}x{h}" |
| |
| @classmethod |
| @docstring_format_args(dilated_height_range) |
| def constraint_dilated_height_range(cls, op): |
| "Dilated kernel height must be in the range [{}, {}]" |
| h = op.kernel.area_height() |
| dilated_height_min, dilated_height_max = cls.dilated_height_range |
| valid = dilated_height_min <= h <= dilated_height_max |
| return valid, f"Op has dilated kernel height as: {h}" |
| |
| @classmethod |
| @docstring_format_args(dilated_product_range) |
| def constraint_dilated_product_range(cls, op): |
| "Product of dilated kernel width and height must be in the range [{}, {}]" |
| product = op.kernel.area_width() * op.kernel.area_height() |
| dilated_product_min, dilated_product_max = cls.dilated_product_range |
| valid = dilated_product_min <= product <= dilated_product_max |
| return valid, f"Op has product of dilated kernel width and height as: {product}" |
| |
| @staticmethod |
| def constraint_weights_type(op): |
| "Weight tensor must be 8-bit" |
| weights = op.weights |
| valid = weights.element_size() == 1 |
| return valid, f"Tensor '{weights.name}' is {int(weights.element_size() * 8)}-bit" |
| |
| @staticmethod |
| def constraint_weights_const(op): |
| "Weight tensor must be constant" |
| weights = op.weights |
| valid = weights.values is not None |
| return valid, f"Tensor '{weights.name}' has non-constant values" |
| |
| @classmethod |
| @docstring_format_args([weights_limit]) |
| def constraint_weights_limit(cls, op): |
| "The sum of the weights cannot exceed {}" |
| weights = op.weights |
| values = weights.values.astype(np.int64) - weights.quantization.zero_point |
| limit = np.amax(np.sum(np.absolute(values), axis=(0, 1, 2))) |
| valid = limit <= cls.weights_limit |
| return valid, f"Tensor '{weights.name}' has the sum of weights: {limit}" |
| |
| @staticmethod |
| def constraint_bias_shape(op): |
| "Optional Bias tensor must be of shape: 1D" |
| bias = op.bias |
| if bias: |
| valid = len(bias.shape) == 1 |
| return valid, f"Tensor '{bias.name}' has shape: {bias.shape}" |
| return True, "Op has no bias tensor" |
| |
| @classmethod |
| @docstring_format_args([list_formatter(supported_bias_dtypes)]) |
| def constraint_bias_type(cls, op): |
| "Optional Bias tensor must be of type: {}" |
| bias = op.bias |
| if bias: |
| valid = bias.dtype in cls.supported_bias_dtypes |
| return valid, f"Tensor '{bias.name}' has data type: {bias.dtype}" |
| return True, "Op has no bias tensor" |
| |
| @staticmethod |
| def constraint_bias_40bit(op): |
| "Optional Bias tensor values must fit within 40-bits" |
| bias = op.bias |
| if bias and bias.dtype == DataType.int64 and bias.values is not None: |
| valid = all(len(bin(value)[2:]) <= 40 for value in bias.values) |
| return valid, f"Tensor '{bias.name}' has values larger than 40-bits" |
| return True, "Op has no bias tensor, or it fits in 40-bit" |
| |
| @staticmethod |
| def constraint_batch_size(op): |
| "IFM Tensor batch size must be 1" |
| valid = True |
| extra = [] |
| for tens in (op.ifm, op.ifm2): |
| if tens is not None: |
| batch_size = full_shape(4, tens.shape, 1)[0] |
| if batch_size != 1: |
| valid = False |
| extra.append(f"Tensor '{tens.name}' has batch size: {batch_size}") |
| extra = "\n ".join(extra) |
| return valid, extra |
| |
| @staticmethod |
| def constraint_depth_multiplier(op): |
| "For depth multipliers > 1, IFM channels must be 1 and OFM channels must be equal to the depth multiplier" |
| depth_multiplier = op.attrs.get("depth_multiplier", 1) |
| if depth_multiplier > 1: |
| ifm_channels = op.ifm.shape[3] |
| ofm_channels = op.ofm.shape[3] |
| valid = (ifm_channels == 1) and (ofm_channels == depth_multiplier) |
| extra = ( |
| f"Op has ifm_channels={ifm_channels}, ofm_channels={ofm_channels}" |
| f" and depth_multiplier={depth_multiplier}" |
| ) |
| return valid, extra |
| return True, "Op has depth_multiplier=1" |
| |
| @staticmethod |
| def constraint_tconv_stride(op): |
| "Stride values for both width and height must be 2" |
| w = op.kernel.stride.x |
| h = op.kernel.stride.y |
| valid = (w == 2) and (h == 2) |
| return valid, f"Op has stride WxH as: {w}x{h}" |
| |
| @staticmethod |
| def constraint_tconv_same(op): |
| "SAME padding: OFM dimensions must equal IFM dimensions multiplied by stride" |
| if op.attrs["padding"] == Padding.SAME: |
| w = op.kernel.stride.x |
| h = op.kernel.stride.y |
| ifm_shape = op.ifm.shape |
| ofm_shape = op.ofm.shape |
| valid = (ofm_shape[1] == (ifm_shape[1] * h)) and (ofm_shape[2] == (ifm_shape[2] * w)) |
| return valid, f"Op has ifm_shape={ifm_shape}, ofm_shape={ofm_shape} and stride WxH as {w}x{h}" |
| return True, "Op has padding=VALID" |
| |
| @staticmethod |
| def constraint_tconv_valid(op): |
| """VALID padding: OFM dimensions must equal IFM dimensions multiplied by stride, |
| minus difference between kernel size and stride""" |
| if op.attrs["padding"] == Padding.VALID: |
| s_w = op.kernel.stride.x |
| s_h = op.kernel.stride.y |
| k_w = op.kernel.width |
| k_h = op.kernel.height |
| ifm_shape = op.ifm.shape |
| ofm_shape = op.ofm.shape |
| height_check = ofm_shape[1] == (ifm_shape[1] * s_h + max(k_h - s_h, 0)) |
| width_check = ofm_shape[2] == (ifm_shape[2] * s_w + max(k_w - s_w, 0)) |
| valid = height_check and width_check |
| extra = ( |
| f"Op has ifm_shape={ifm_shape}, ofm_shape={ofm_shape}," |
| f" stride WxH as {s_w}x{s_h} and kernel WxH as {k_w}x{k_h}" |
| ) |
| return valid, extra |
| return True, "Op has padding=SAME" |
| |
| @classmethod |
| @docstring_format_args(filter_range) |
| def constraint_filter_range(cls, op): |
| "Kernel filter values for both width and height must be in the range [{}, {}]" |
| if op.attrs["padding"] == Padding.SAME: |
| w = op.kernel.width |
| h = op.kernel.height |
| filter_min, filter_max = cls.filter_range |
| valid = (filter_min <= w <= filter_max) and (filter_min <= h <= filter_max) |
| return valid, f"Op has kernel filter WxH as: {w}x{h}" |
| return True, "Op has padding=VALID" |
| |
| @classmethod |
| @docstring_format_args(filter_height_range) |
| def constraint_filter_height_range(cls, op): |
| "Kernel filter height must be in the range [{}, {}]" |
| h = op.kernel.height |
| filter_height_min, filter_height_max = cls.filter_height_range |
| valid = filter_height_min <= h <= filter_height_max |
| return valid, f"Op has kernel filter height as: {h}" |
| |
| @classmethod |
| @docstring_format_args(filter_product_range) |
| def constraint_filter_product_range(cls, op): |
| "Product of kernel filter width and height must be in the range [{}, {}]" |
| product = op.kernel.elements_wh() |
| filter_product_min, filter_product_max = cls.filter_product_range |
| valid = filter_product_min <= product <= filter_product_max |
| return valid, f"Op has product of kernel filter width and height as: {product}" |
| |
| @staticmethod |
| @docstring_format_args(filter_height_range) |
| def constraint_filter_height_range_valid_pad(op): |
| "VALID padding: Kernel filter height must be in the range [{}, {}]" |
| if op.attrs["padding"] == Padding.VALID: |
| return TFLiteSupportedOperators.constraint_filter_height_range(op) |
| return True, "Op has padding=SAME" |
| |
| @staticmethod |
| @docstring_format_args(filter_product_range) |
| def constraint_filter_product_range_valid_pad(op): |
| "VALID padding: Product of kernel filter width and height must be in the range [{}, {}]" |
| if op.attrs["padding"] == Padding.VALID: |
| return TFLiteSupportedOperators.constraint_filter_product_range(op) |
| return True, "Op has padding=SAME" |
| |
| @staticmethod |
| def constraint_resize(op): |
| """The width and height of the IFM and OFM must match one of the following criteria: |
| IFM W and H must both be 1 |
| IFM must match OFM |
| W and H scaling must be equal and OFM W-1 and H-1 must be 2x/4x/8x IFM W-1 and H-1, if align_corners is True |
| W and H scaling must be equal and OFM W and H must be 2x/4x/8x IFM W and H, if align_corners is False""" |
| # Easier to start with False condition as very few cases result in a supported resize |
| valid = False |
| ifm_shape = op.ifm.shape |
| ifm_shape_h = ifm_shape[1] |
| ifm_shape_w = ifm_shape[2] |
| ofm_shape = op.ofm.shape |
| ofm_shape_h = ofm_shape[1] |
| ofm_shape_w = ofm_shape[2] |
| |
| align_corners = op.attrs.get("align_corners", False) |
| if len(ifm_shape) == 4: |
| # Valid if IFM W and H are both 1, or IFM and OFM shape are the same |
| if ((ifm_shape_h == 1) and (ifm_shape_w == 1)) or (ifm_shape == ofm_shape): |
| valid = True |
| else: |
| # Valid if OFM is 2/4/8x IFM (-1 for align corners) |
| if align_corners: |
| h_upscale_factor = (ofm_shape_h - 1) / (ifm_shape_h - 1) |
| w_upscale_factor = (ofm_shape_w - 1) / (ifm_shape_w - 1) |
| else: |
| h_upscale_factor = ofm_shape_h / ifm_shape_h |
| w_upscale_factor = ofm_shape_w / ifm_shape_w |
| |
| # could use either height or width. save as int because it is more usable later in graph optimiser |
| op.attrs["upscale_factor"] = int(h_upscale_factor) |
| valid = h_upscale_factor == w_upscale_factor and h_upscale_factor in (2.0, 4.0, 8.0) |
| |
| return valid, f"Op has ifm_shape={ifm_shape}, ofm_shape={ofm_shape} and align_corners={align_corners}" |
| |
| @staticmethod |
| def constraint_resize_size(op): |
| "The size tensor must match the output tensor shape" |
| valid = False |
| ofm_shape = op.ofm.shape |
| size_h, size_w = None, None |
| # check that the size tensor (the second input) exists, is not none, and has the correct values |
| if len(op.inputs) == 2 and op.inputs[1] is not None and len(op.inputs[1].values) == 2: |
| size_h, size_w = op.inputs[1].values |
| # check size and output size match |
| if size_h == ofm_shape[1] and size_w == ofm_shape[2]: |
| valid = True |
| |
| return valid, f"Op has size={size_h}x{size_w} and ofm_shape={ofm_shape}." |
| |
| @staticmethod |
| def constraint_resize_attrs(op): |
| "Both align_corners and half_pixel_centers can't be True" |
| valid = True |
| align_corners = op.attrs.get("align_corners", False) |
| half_pixel_centers = op.attrs.get("half_pixel_centers", False) |
| |
| if align_corners and half_pixel_centers: |
| valid = False |
| return valid, "Op has both align_corners and half_pixel_centers set to True." |
| |
| @staticmethod |
| def constraint_resizebi_half_pixel_centers_dims(op): |
| """Half_pixel_centers for resize bilinear requires that OFM W and H is 2x IFM W and H""" |
| half_pixel_centers = op.attrs.get("half_pixel_centers", False) |
| if not half_pixel_centers: |
| valid = True |
| elif len(op.ifm.shape) >= 3: |
| ifm_h, ifm_w = op.ifm.shape[-3:-1] |
| ofm_h, ofm_w = op.ofm.shape[-3:-1] |
| valid = ofm_h / ifm_h == 2 and ofm_w / ifm_w == 2 |
| else: |
| # Unexpected IFM shape |
| valid = False |
| return ( |
| valid, |
| f"Op has ifm_shape={op.ifm.shape}, ofm_shape={op.ofm.shape} and half_pixel_centers={half_pixel_centers}", |
| ) |
| |
| @staticmethod |
| def constraint_pad_shape(op): |
| "The padding tensor must have the shape [3,2] or [4,2]" |
| valid = op.inputs[1].shape in ([3, 2], [4, 2]) |
| return valid, f"The pad tensor has the shape: {op.inputs[1].shape}" |
| |
| @classmethod |
| @docstring_format_args([list_formatter(supported_pad_dtypes)]) |
| def constraint_pad_type(cls, op): |
| "Pad tensor must be of type: {}" |
| pad_tensor = op.inputs[1] |
| valid = pad_tensor.dtype in cls.supported_pad_dtypes |
| return valid, f"Tensor '{pad_tensor.name}' has data type: {pad_tensor.dtype}" |
| |
| @staticmethod |
| def constraint_padding_dimensions(op): |
| "The pad tensor can only pad width and height" |
| pad_tensor = op.inputs[1].values |
| |
| valid = sum(pad_tensor[-1, :]) == 0 |
| if valid and len(pad_tensor) > 3: |
| valid = sum(pad_tensor[0, :]) == 0 |
| return valid, f"First dimension padding: {pad_tensor[0,:]}, last dimension padding: {pad_tensor[-1,:]}" |
| |
| @staticmethod |
| def constraint_stridedslice_stride_values(op): |
| "All Strides values must be 1" |
| strides = op.inputs[3] |
| valid = all(stride == 1 for stride in strides.values) |
| return valid, f"Op has strides values {strides.values}" |
| |
| @staticmethod |
| def constraint_inputs_int32(op): |
| "Both Input data types must be int32" |
| ifm_dtype = op.ifm.dtype |
| ifm2_dtype = op.ifm2.dtype |
| valid = (ifm_dtype == DataType.int32) and (ifm2_dtype == DataType.int32) |
| return valid, f"Op has ifm_dtype={ifm_dtype} and ifm2_dtype={ifm2_dtype}" |
| |
| @staticmethod |
| def constraint_output_int32(op): |
| "OFM must be int32" |
| ofm_dtype = op.ofm.dtype |
| valid = ofm_dtype == DataType.int32 |
| return valid, f"Op has ofm_dtype={ofm_dtype}" |
| |
| @staticmethod |
| def constraint_matching_quantization_parameters(op): |
| "Both Input quantization parameters must match OFM quantization parameters" |
| valid = True |
| extra = [] |
| if not check_quantized_tens_scaling_equal(op.ofm, op.ifm): |
| valid = False |
| extra.append(op.ifm.name) |
| if op.ifm2 is not None and not check_quantized_tens_scaling_equal(op.ofm, op.ifm2): |
| valid = False |
| extra.append(op.ifm2.name) |
| extra = ", ".join(extra) |
| return valid, f"Op has tensors with different quantization parameters to the OFM '{op.ofm.name}': {extra}" |
| |
| @staticmethod |
| def constraint_broadcast_shapes(op): |
| "Broadcasting is only allowed for rank indices with dimension 1, from either IFM1 or IFM2" |
| ifm_shape = op.ifm.shape |
| ifm2_shape = op.ifm2.shape if op.ifm2 else None |
| ofm_shape = op.ofm.shape |
| valid = True |
| if ifm_shape is not None and ifm2_shape is not None: |
| # align trailing dimensions |
| size = min(len(ifm_shape), len(ifm2_shape)) |
| for i, i2, o in zip(ifm_shape[-size:], ifm2_shape[-size:], ofm_shape[-size:]): |
| mi = max(i, i2) |
| # Input dimensions should match or one should be of dimension 1 |
| # Output dimension should match the largest input dimension, together |
| # with constraint_match_either_shapes ensures broadcast from only one input |
| if not (i == i2 or i == 1 or i2 == 1) or o != mi: |
| valid = False |
| break |
| |
| return valid, f"Op has ifm_shape={ifm_shape} and ifm2_shape={ifm2_shape}" |
| |
| @classmethod |
| @docstring_format_args([mean_kernel_product_avgpool]) |
| def constraint_mean_height_width_product_avgpool(cls, op): |
| """Product of height and width must be no greater than {}""" |
| shape = op.inputs[0].shape |
| hi = 0 if len(shape) < 4 else 1 |
| h, w = shape[hi : hi + 2] |
| max_prod = cls.mean_kernel_product_avgpool |
| return h * w <= max_prod, f"Product of height and width is {h * w}" |
| |
| @classmethod |
| @docstring_format_args([mean_kernel_product]) |
| def constraint_mean_height_width_product(cls, op): |
| """Product of height and width must be no greater than {} when: |
| IFM and OFM have different scale or zero point; or |
| 'keep_dims' is True""" |
| ifmq, ofmq = op.ifm.quantization, op.ofm.quantization |
| keep_dims = op.attrs.get("keep_dims") |
| # doesn't apply, size is checked by constraint_mean_height_width_product_avgpool |
| if not keep_dims and ifmq.scale_f32 == ofmq.scale_f32 and ifmq.zero_point == ofmq.zero_point: |
| return True, "" |
| shape = op.inputs[0].shape |
| hi = 0 if len(shape) < 4 else 1 |
| h, w = shape[hi : hi + 2] |
| max_prod = cls.mean_kernel_product |
| return h * w <= max_prod, f"Product of height and width is {h * w}" |
| |
| @classmethod |
| @docstring_format_args([filter_height_range[1], dilated_height_range[1]]) |
| def constraint_mean_height_single_axis(cls, op): |
| """For single axis averages across the height dimension: |
| IFM height must be no greater than {} if the IFM and OFM scale and zero point match; otherwise |
| IFM height must be no greater than {} if the IFM and OFM scale or zero point do not match""" |
| inp, axis = op.inputs |
| if axis.shape == [] or axis.shape[0] == 1: # single axis |
| axis = int(axis.values) if len(axis.shape) == 0 else int(axis.values[0]) |
| else: |
| # Multiple axes |
| return True, "" |
| |
| shape = inp.shape |
| if len(shape) < 3: |
| # No height dimension present in IFM |
| return True, "" |
| if axis != len(shape) - 3: |
| # Not averaging across the height dimension |
| return True, "" |
| |
| h = shape[axis] |
| ifm, ofm = op.get_ifm_ofm() |
| |
| if check_quantized_tens_scaling_equal(ifm, ofm): |
| return h <= cls.filter_height_range[1], f"Height is {h}, IFM and OFM quantizations match" |
| else: |
| return h <= cls.dilated_height_range[1], f"Height is {h}, IFM and OFM quantizations do not match" |
| |
| @staticmethod |
| def constraint_reshape_shape_constant(op): |
| "Shape must be constant" |
| valid = True |
| extra = [] |
| |
| reshape_tens = op.inputs[1] |
| if reshape_tens is not None: |
| # constant inputs have either no driving operator or a const one |
| # create a list of non-constant inputs |
| if not (len(reshape_tens.ops) == 0 or reshape_tens.ops[0].type == Op.Const): |
| valid = False |
| extra.append(reshape_tens.name) |
| extra = ", ".join(extra) |
| |
| return valid, f"Op has non-const input(s): {extra}" |