Add local_bound attribute support to conv ops

This includes both respecting the local_bound attribute in the reference
model and generating tests with local_bound=True and local_bound=False
for floating point types. For non-floating point types, we always set it
to False.

Signed-off-by: IanTayler <ian.taylerlessa@arm.com>
Change-Id: Ifa47b6df31b9142835f48da00b595b89c7082734
diff --git a/reference_model/src/ops/tensor_ops.cc b/reference_model/src/ops/tensor_ops.cc
index f38f486..28d54fd 100644
--- a/reference_model/src/ops/tensor_ops.cc
+++ b/reference_model/src/ops/tensor_ops.cc
@@ -744,18 +744,24 @@
         weight_val = weight_val - (WeightEigenType)attribute->weight_zp();
     }
 
-    ETensor4<InEigenType> input_padded = input_val.pad(pad);
-
     TBias bias_val = this->bias->getTensor();
 
     if (g_func_config.abs_mode)
     {
         // in abs_mode: take abs values of conv operands
-        input_padded = input_padded.abs();
-        weight_val   = weight_val.abs();
-        bias_val     = bias_val.abs();
+        input_val  = input_val.abs();
+        weight_val = weight_val.abs();
+        bias_val   = bias_val.abs();
+
+        if (!this->attribute->local_bound())
+        {
+            Eigen::Tensor<InEigenType, 0> input_abs_max = input_val.maximum();
+            input_val.setConstant(input_abs_max(0));
+        }
     }
 
+    ETensor4<InEigenType> input_padded = input_val.pad(pad);
+
     // extract_image_patches() output [N, KH, KW, H * W, C]
     // need to transpose to [N, H * W, KH, KW, C]
     ETensor5<InEigenType> input_extract_patches =
@@ -938,18 +944,24 @@
         weight_val = weight_val - (WeightEigenType)attribute->weight_zp();
     }
 
-    ETensor5<InEigenType> input_padded = input_val.pad(pad);
-
     TBias bias_val = this->bias->getTensor();
 
     if (g_func_config.abs_mode)
     {
         // in abs_mode: take abs values of conv operands
-        input_padded = input_padded.abs();
-        weight_val   = weight_val.abs();
-        bias_val     = bias_val.abs();
+        input_val  = input_val.abs();
+        weight_val = weight_val.abs();
+        bias_val   = bias_val.abs();
+
+        if (!this->attribute->local_bound())
+        {
+            Eigen::Tensor<InEigenType, 0> input_abs_max = input_val.maximum();
+            input_val.setConstant(input_abs_max(0));
+        }
     }
 
+    ETensor5<InEigenType> input_padded = input_val.pad(pad);
+
     // 1. initialize with bias
     Eigen::array<Eigen::Index, 5> reshape_dim;
     reshape_dim.fill(1);
@@ -1140,18 +1152,24 @@
         weight_val = weight_val - (WeightEigenType)attribute->weight_zp();
     }
 
-    ETensor4<InEigenType> input_padded = input_val.pad(pad);
-
     TBias bias_val = this->bias->getTensor();
 
     if (g_func_config.abs_mode)
     {
         // in abs_mode: take abs values of conv operands
-        input_padded = input_padded.abs();
-        weight_val   = weight_val.abs();
-        bias_val     = bias_val.abs();
+        input_val  = input_val.abs();
+        weight_val = weight_val.abs();
+        bias_val   = bias_val.abs();
+
+        if (!this->attribute->local_bound())
+        {
+            Eigen::Tensor<InEigenType, 0> input_abs_max = input_val.maximum();
+            input_val.setConstant(input_abs_max(0));
+        }
     }
 
+    ETensor4<InEigenType> input_padded = input_val.pad(pad);
+
     // GEMM doesn't fit well with DepthwiseConv2d
     // 1. use extract_image_patches() to handle stride/dilation/pad
     // 2. perform direct convolution
@@ -2078,6 +2096,12 @@
         input_val  = input_val.abs();
         weight_val = weight_val.abs();
         bias_val   = bias_val.abs();
+
+        if (!this->attribute->local_bound())
+        {
+            Eigen::Tensor<InEigenType, 0> input_abs_max = input_val.maximum();
+            input_val.setConstant(input_abs_max(0));
+        }
     }
 
     Eigen::array<Eigen::Index, 4> reshape_dim;
diff --git a/verif/conformance/test_select.py b/verif/conformance/test_select.py
index e3f1738..76f0160 100644
--- a/verif/conformance/test_select.py
+++ b/verif/conformance/test_select.py
@@ -555,21 +555,48 @@
     """Test selector for the CONV2D operator."""
 
     name = "conv2d"
-    param_names = ["kernel", "shape", "type", "accum_type", "stride", "pad", "dilation"]
+    param_names = [
+        "kernel",
+        "shape",
+        "type",
+        "accum_type",
+        "stride",
+        "pad",
+        "dilation",
+        "local_bound",
+    ]
 
 
 class Conv3dOperator(Operator):
     """Test selector for the CONV3D operator."""
 
     name = "conv3d"
-    param_names = ["kernel", "shape", "type", "accum_type", "stride", "pad", "dilation"]
+    param_names = [
+        "kernel",
+        "shape",
+        "type",
+        "accum_type",
+        "stride",
+        "pad",
+        "dilation",
+        "local_bound",
+    ]
 
 
 class DepthwiseConv2dOperator(Operator):
     """Test selector for the DEPTHWISE_CONV2D operator."""
 
     name = "depthwise_conv2d"
-    param_names = ["kernel", "shape", "type", "accum_type", "stride", "pad", "dilation"]
+    param_names = [
+        "kernel",
+        "shape",
+        "type",
+        "accum_type",
+        "stride",
+        "pad",
+        "dilation",
+        "local_bound",
+    ]
 
 
 class DimOeprator(Operator):
@@ -953,6 +980,7 @@
         "stride",
         "pad",
         "out_shape",
+        "local_bound",
     ]
 
     def path_params(self, path):
diff --git a/verif/generator/tosa_arg_gen.py b/verif/generator/tosa_arg_gen.py
index 6f130a8..c2407ab 100644
--- a/verif/generator/tosa_arg_gen.py
+++ b/verif/generator/tosa_arg_gen.py
@@ -2364,6 +2364,10 @@
                                     dots = gtu.product(
                                         (ifm_shape[0], *outputs, filter_shape[0])
                                     )
+                                if gtu.dtypeIsFloat(dtypes[0]):
+                                    local_bound = rng.choice((False, True))
+                                else:
+                                    local_bound = False
                                 args_dict = {
                                     "acc_type": a,
                                     "stride": s,
@@ -2373,17 +2377,19 @@
                                     "ks": k_size,
                                     "dot_products": dots,
                                     "shape": ifm_shape,
+                                    "local_bound": local_bound,
                                 }
 
                                 # Support for larger values than 9 needs different delimiter
                                 delim = "" if max(s + p + d) <= 9 else "x"
                                 arg_list.append(
                                     (
-                                        "acc{}_st{}_pad{}_dilat{}".format(
+                                        "acc{}_st{}_pad{}_dilat{}_lclbnd{}".format(
                                             testGen.typeStr(a),
                                             delim.join([str(x) for x in s]),
                                             delim.join([str(x) for x in p]),
                                             delim.join([str(x) for x in d]),
+                                            "1" if local_bound else "0",
                                         ),
                                         args_dict,
                                     )
@@ -2620,6 +2626,11 @@
                         ow = (ifm_shape[2] - 1) * s[1] + p[2] + p[3] + k_shape[1]
                         os = [ifm_shape[0], oh, ow, filter_shape[0]]
 
+                        if gtu.dtypeIsFloat(dtypes[0]):
+                            local_bound = rng.choice((False, True))
+                        else:
+                            local_bound = False
+
                         # N*OH*OW*OC
                         dots = gtu.product((ifm_shape[0], oh, ow, filter_shape[0]))
                         args_dict = {
@@ -2631,17 +2642,19 @@
                             "dot_products": dots,
                             "shape": ifm_shape,
                             "out_shape": os,
+                            "local_bound": local_bound,
                         }
 
                         # Support for larger values than 9 needs different delimiter
                         delim = "" if max(s + p) <= 9 else "x"
                         arg_list.append(
                             (
-                                "acc{}_st{}_pad{}_os{}".format(
+                                "acc{}_st{}_pad{}_os{}_lclbnd{}".format(
                                     testGen.typeStr(a),
                                     delim.join([str(x) for x in s]),
                                     delim.join([str(x) for x in p]),
                                     "x".join([str(x) for x in os]),
+                                    "1" if local_bound else "0",
                                 ),
                                 args_dict,
                             )
diff --git a/verif/generator/tosa_test_gen.py b/verif/generator/tosa_test_gen.py
index 2db094a..482e8f8 100644
--- a/verif/generator/tosa_test_gen.py
+++ b/verif/generator/tosa_test_gen.py
@@ -860,6 +860,7 @@
         strides = args_dict["stride"]
         padding = args_dict["pad"]
         dilations = args_dict["dilation"]
+        local_bound = args_dict["local_bound"]
 
         assert len(padding) == 4
         result_tensor = OutputShaper.conv2dOp(
@@ -916,9 +917,6 @@
         ):
             return None
 
-        # TODO - Test local_bound, for now set local bound attribute to False
-        local_bound = False
-
         attr = ts.TosaSerializerAttribute()
         attr.ConvAttribute(
             padding, strides, dilations, qinfo[0], qinfo[1], local_bound, accum_dtype
@@ -948,6 +946,7 @@
         strides = args_dict["stride"]
         padding = args_dict["pad"]
         dilations = args_dict["dilation"]
+        local_bound = args_dict["local_bound"]
 
         assert len(padding) == 6
         result_tensor = OutputShaper.conv3dOp(
@@ -1004,9 +1003,6 @@
         ):
             return None
 
-        # TODO - Test local_bound, for now set local bound attribute to False
-        local_bound = False
-
         attr = ts.TosaSerializerAttribute()
         attr.ConvAttribute(
             padding, strides, dilations, qinfo[0], qinfo[1], local_bound, accum_dtype
@@ -1035,6 +1031,7 @@
         accum_dtype = args_dict["acc_type"]
         strides = args_dict["stride"]
         out_pad = args_dict["pad"]
+        local_bound = args_dict["local_bound"]
 
         assert len(out_pad) == 4
         result_tensor = OutputShaper.transposeConv2DOp(
@@ -1082,9 +1079,6 @@
         ):
             return None
 
-        # TODO - Test local_bound, for now set local bound attribute to False
-        local_bound = False
-
         attr = ts.TosaSerializerAttribute()
         attr.TransposeConvAttribute(
             out_pad, strides, qinfo[0], qinfo[1], local_bound, accum_dtype
@@ -1114,6 +1108,7 @@
         strides = args_dict["stride"]
         padding = args_dict["pad"]
         dilations = args_dict["dilation"]
+        local_bound = args_dict["local_bound"]
 
         result_tensor = OutputShaper.depthwiseConv2dOp(
             self.ser,
@@ -1169,9 +1164,6 @@
         ):
             return None
 
-        # TODO - Test local_bound, for now set local bound attribute to False
-        local_bound = False
-
         attr = ts.TosaSerializerAttribute()
         attr.ConvAttribute(
             padding, strides, dilations, qinfo[0], qinfo[1], local_bound, accum_dtype