MLBEDSW-3302: Reject per-channel scaling for unsupported ops

Vela only supports per-channel scaling for
convolution ops. This commit adds a check that
puts ops with per-channel scaling on the CPU.
A caveat worth mentioning is that neither
TensorFlow Lite or TensorFlow Lite Micro support
per-channel scaling for the CPU placed op,
however the problem is moved away from Vela.
This commit also changes a small utility function
in supported_operators.py used for docstring
formatting.

Signed-off-by: Dwight Lidman <dwight.lidman@arm.com>
Change-Id: I9ed090592f1d05dd4566d3e54dba1ef405299383
diff --git a/ethosu/vela/supported_operators.py b/ethosu/vela/supported_operators.py
index 91fcb5a..6dcb27d 100644
--- a/ethosu/vela/supported_operators.py
+++ b/ethosu/vela/supported_operators.py
@@ -43,8 +43,8 @@
     output = map(optype_to_builtintype, op_list)
     # Remove UNKNOWNs
     output = (x for x in output if x is not BUILTIN_OPERATOR_UNKNOWN)
-    # Order alphabetically
-    return sorted(output)
+    # Order alphabetically and join into a string representation
+    return ", ".join(str(op) for op in sorted(output))
 
 
 class SupportedOperators:
@@ -94,6 +94,7 @@
     concat_ops = set((Op.Concat, Op.ConcatTFLite, Op.PackReshaped, Op.Pack,))
     memory_only_ops = set((Op.Squeeze, Op.Reshape, Op.QuantizedReshape,)) | concat_ops | split_ops
     shapeless_input_ops = binary_elem_wise_main_ops | set((Op.Split, Op.SplitV,))
+    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 | npu_post_ops | memory_only_ops
     # Supported data types
@@ -113,6 +114,7 @@
     docstring_shapeless_input_ops = _optype_formatter(shapeless_input_ops)
     docstring_supported_int32_tensor_ops = _optype_formatter(supported_int32_tensor_ops)
     docstring_supported_fused_activations = _optype_formatter(supported_fused_activations)
+    docstring_per_axis_quant_ops = _optype_formatter(per_axis_quant_ops)
 
     def __init__(self):
         # Setup the generic constraints. Note: the order matters
@@ -127,6 +129,7 @@
         self.generic_constraints.append(SupportedOperators.constraint_tens_dimension)
         self.generic_constraints.append(SupportedOperators.constraint_tens_quant_none_check)
         self.generic_constraints.append(SupportedOperators.constraint_tens_quant_scale)
+        self.generic_constraints.append(SupportedOperators.constraint_tens_quant_per_axis)
         self.generic_constraints.append(SupportedOperators.constraint_faf)
 
         # Setup specific constraints. Note: the order matters
@@ -391,6 +394,20 @@
         return valid, ", ".join(extra)
 
     @classmethod
+    @docstring_format_args([docstring_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.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([docstring_supported_fused_activations])
     def constraint_faf(cls, op):
         "The fused activation function (if present) must be one of type: {}"