# Copyright (C) 2021 Arm Limited or its affiliates. All rights reserved. # # 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 TosaSupportedOperators class which is a collection of all supported operators and parameter checks. from collections import defaultdict from .data_type import DataType from .operation import Op from .supported_operators_util import docstring_format_args from .supported_operators_util import list_formatter from .tosa_mapping import optype_to_tosa_op_type class TosaSupportedOperators: # TODO currently sparsely populated # Categorised lists of supported operators convolution_ops = set((Op.Conv2DBias,)) depthwise_convolution_ops = set((Op.DepthwiseConv2DBias,)) convolution_like_ops = convolution_ops | depthwise_convolution_ops # TODO depending on what will be committed max_pooling_ops = Op.op_set(Op.is_maxpool_op) avg_pooling_ops = Op.op_set(Op.is_avgpool_op) pooling_ops = max_pooling_ops | avg_pooling_ops fc_vector_products = set((Op.FullyConnected,)) mac_main_ops = convolution_like_ops | pooling_ops | fc_vector_products memory_only_ops = set( ( Op.Reshape, Op.Transpose, Op.Concat, Op.SplitSliceRead, ) ) binary_elem_wise_add_mul_sub = set( ( Op.Add, Op.Mul, Op.RescaleMul, Op.Sub, ) ) elem_wise_ops = binary_elem_wise_add_mul_sub type_conversion_ops = set((Op.Rescale,)) relu_ops = set( ( Op.Clamp, Op.ReluN, ) ) activation_ops = relu_ops | set((Op.Table,)) pad_ops = set((Op.Pad,)) rank_unlimited_ops = set((Op.Concat, Op.Reshape, Op.Identity, Op.Pad)) rank6_limited_ops = elem_wise_ops batch_enabled_ops = rank6_limited_ops | rank_unlimited_ops large_tens_dims_enabled_ops = batch_enabled_ops | set((Op.SplitSliceRead,)) npu_post_ops = activation_ops supported_operators = ( mac_main_ops | type_conversion_ops | npu_post_ops | memory_only_ops | elem_wise_ops | pad_ops | set((Op.Identity,)) ) # Supported data types # TODO will differ compared to TensorFlow Lite, currently set to the same supported_op_dtypes = set((DataType.uint8, DataType.int8, DataType.int16, DataType.int32)) # TODO add bool tens_dim_range = (1, 65535) # TODO HW limitation, that is to be resolved in SW def __init__(self): # Setup the generic constraints. Note: the order matters self.generic_constraints = [] self.generic_constraints.append(TosaSupportedOperators.constraint_tens_dtype) self.generic_constraints.append(TosaSupportedOperators.constraint_tens_dimension) # TODO not supported yet self.generic_constraints.append(TosaSupportedOperators.constraint_rank) # TODO not supported for all ops yet self.generic_constraints.append(TosaSupportedOperators.constraint_batch) # TODO not supported for all ops yet # Setup specific constraints. Note: the order matters self.specific_constraints = defaultdict(list) self.specific_constraints[Op.Transpose].append(TosaSupportedOperators.constraint_ifm_producer) self.specific_constraints[Op.Pad].append(TosaSupportedOperators.constraint_padding_producer) self.specific_constraints[Op.Table].append(TosaSupportedOperators.constraint_table_dtype) self.specific_constraints[Op.Table].append(TosaSupportedOperators.constraint_table_producer) # Depthwise Conv specific checks: for op_type in TosaSupportedOperators.depthwise_convolution_ops: self.specific_constraints[op_type].append(TosaSupportedOperators.constraint_depth_multiplier) # Avgpool specific checks for op_type in TosaSupportedOperators.avg_pooling_ops: self.specific_constraints[op_type].append(TosaSupportedOperators.constraint_padding) def is_operator_supported(self, op): ext_type = optype_to_tosa_op_type(op.type) if op.type not in TosaSupportedOperators.supported_operators: if op.type not in (Op.Placeholder, Op.SubgraphInput, Op.Const): print(f"Info: {ext_type} '{op.name}' is not a NPU op") return False for constraint in self.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") print(f" - {constraint.__doc__}") if extra: print(f" {extra}") return False return True # TODO this function is the same for TensorFlow Lite, but input might differ @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) # TODO Duplicates check present for TFLite. But it is only temporarily added # This is for a HW limitation, that is to be resolved in SW later on @classmethod @docstring_format_args(tens_dim_range) def constraint_tens_dimension(self, op): "Tensor dimensions must be in the range [{}, {}]" tens_min, tens_max = self.tens_dim_range valid = True extra = [] if op.type not in self.large_tens_dims_enabled_ops: 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) # TODO This is for a HW limitation, that is to be resolved in SW later on @classmethod def constraint_rank(self, op): "Tensor rank must be <= 6 or <= 4 depending on operator" valid = True extra = [] if op.type not in self.rank_unlimited_ops: if op.type in self.rank6_limited_ops: rank_limit = 6 else: rank_limit = 4 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: rank = len(tens.shape) if not rank <= rank_limit: valid = False extra.append( f"Tensor '{tens.name}' has rank: {rank}, rank limit is currently {rank_limit}" f" for op of type {op.type}" ) return valid, ", ".join(extra) # TODO This is for a HW limitation, that is to be resolved in SW later on @classmethod def constraint_batch(self, op): "If Tensor rank is 4 batch of ifms/ofm must be 1" valid = True extra = [] if op.type not in self.batch_enabled_ops: tensors = [tens for tens in op.get_ifm_ifm2_ofm() if tens] if not tensors: tensors = [tens for tens in op.inputs if tens] for tens in tensors: rank = len(tens.shape) if rank == 4 and tens.shape[0] != 1: valid = False extra.append(f"Tensor '{tens.name}' has rank: 4 and N: {tens.shape[0]}") return valid, ", ".join(extra) @staticmethod def constraint_ifm_producer(cls, op): "Input must be constant data" valid = op.ifm.ops and op.ifm.ops[0].type == Op.Const return valid, "Op has ifm with non-constant data" @staticmethod def constraint_padding(op): # TODO Only support for when global scaling can be used. # That is when there is padding no padding "Avgpool only supported for no padding" top, left, _, _ = op.attrs["explicit_padding"] valid = top == 0 and left == 0 return valid, "Avgpool with pad_top {top} and pad_left {left}" # TODO limit padding to be const data for now. # For TFLite it is assumed to be constant. @staticmethod def constraint_padding_producer(op): "Input must be constant data" valid = op.inputs[1].ops and op.inputs[1].ops[0].type == Op.Const return valid, "PAD Op with non-constant data padding" # TODO duplicates tflite_supported operators, but support for depth multiplier should be added at a later stage @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" # TODO Table operator support limited to int8 for now. # For TFLite it is assumed to be constant. @staticmethod def constraint_table_dtype(op): "Only supported is int8" valid = True tensors = [op.ifm, op.ofm, op.inputs[1]] for tens in tensors: if tens.dtype != DataType.int8: valid = False return valid, "Table operator with non int8 tensor" # TODO limit table to be constant data for now. # Can it be non-constant? @staticmethod def constraint_table_producer(op): "Input must be constant data" valid = op.inputs[1].ops and op.inputs[1].ops[0].type == Op.Const return valid, "Table Op with non-constant table input"