Repository: SeldonIO/MLServer

Issue:
Issue in aggregating batched requests parameters
When sending batched requests with custom parameters (using extra field in [here](https://github.com/SeldonIO/MLServer/blob/749cd02bc5f3ee0b915151eedeff50075ac946ad/mlserver/types/dataplane.py#L27)) like the following:

```python
# request 1
payload = types.InferenceRequest(
    inputs=[
        types.RequestInput(
            name="parameters-np",
            shape=[1],
            datatype="BYTES",
            data=[],
            parameters=types.Parameters(
                custom-param='value-1,
        )
    ]
)
# request 2
payload = types.InferenceRequest(
    inputs=[
        types.RequestInput(
            name="parameters-np",
            shape=[1],
            datatype="BYTES",
            data=[],
            parameters=types.Parameters(
                custom-param='value-2',
        )
    ]
)
```
the results param field for the batched inference request will only contain a single `custom-param` field with the last request value for that, this is because a [dictionary is used](https://github.com/SeldonIO/MLServer/blob/749cd02bc5f3ee0b915151eedeff50075ac946ad/mlserver/batching/requests.py#L112) to aggregate parameters and for the case of having a same parameter with different values in the same batch only one value will be stored for them. E.g. in the case above we will only have `custom-param` equals to `value-2`. A better solution would be able to store all the values of common request parameter separately in a list e.g. `{custom-param=['value-1', 'value-2']`



Discussion:

Resolving patch:
diff --git a/mlserver/batching/requests.py b/mlserver/batching/requests.py
--- a/mlserver/batching/requests.py
+++ b/mlserver/batching/requests.py
@@ -29,6 +29,40 @@ def _merge_parameters(
     return {**all_params, **obj_params}
 
 
+def _merge_input_parameters(
+    all_params: dict,
+    parametrised_obj: Union[
+        InferenceRequest, InferenceResponse, RequestInput, RequestOutput
+    ],
+) -> dict:
+    if not parametrised_obj.parameters:
+        return all_params
+    obj_params = parametrised_obj.parameters.dict()
+    if all_params == {}:
+        return obj_params
+    else:
+        common_keys = set(all_params).intersection(set(obj_params)) - {
+            "content_type",
+            "headers",
+        }
+        uncommon_keys = set(all_params).union(set(obj_params)) - common_keys
+        new_all_params = {}
+        for key in common_keys:
+            if type(all_params[key]) == list:
+                new_value = all_params[key] + [obj_params[key]]
+                new_all_params[key] = new_value
+            else:
+                new_value = [all_params[key]]
+                new_value.append(obj_params[key])
+                new_all_params[key] = new_value
+        for key in uncommon_keys:
+            if key in all_params.keys():
+                new_all_params[key] = all_params[key]
+            if key in obj_params.keys():
+                new_all_params[key] = obj_params[key]
+    return new_all_params
+
+
 def _merge_data(
     all_data: Union[list, List[str], List[bytes]]
 ) -> Union[list, str, bytes]:
@@ -109,7 +143,7 @@ def _merge_request_inputs(
         all_data = []
         all_params: dict = {}
         for internal_id, request_input in request_inputs.items():
-            all_params = _merge_parameters(all_params, request_input)
+            all_params = _merge_input_parameters(all_params, request_input)
             all_data.append(_get_data(request_input))
             minibatch_shape = Shape(request_input.shape)
             self._minibatch_sizes[internal_id] = minibatch_shape.batch_size


Test patch:
diff --git a/tests/batching/test_requests.py b/tests/batching/test_requests.py
--- a/tests/batching/test_requests.py
+++ b/tests/batching/test_requests.py
@@ -47,6 +47,40 @@
             ),
             {"req-1": 1, "req-2": 1, "req-3": 1},
         ),
+        (
+            {
+                "req-1": RequestInput(
+                    name="foo",
+                    datatype="INT32",
+                    shape=[1, 3],
+                    data=[1, 2, 3],
+                    parameters=Parameters(content_type="np", foo="bar"),
+                ),
+                "req-2": RequestInput(
+                    name="foo",
+                    datatype="INT32",
+                    shape=[1, 3],
+                    data=[4, 5, 6],
+                ),
+                "req-3": RequestInput(
+                    name="foo",
+                    datatype="INT32",
+                    shape=[1, 3],
+                    data=[7, 8, 9],
+                    parameters=Parameters(foo="bar", time="13:00"),
+                ),
+            },
+            RequestInput(
+                name="foo",
+                datatype="INT32",
+                shape=[3, 3],
+                data=[1, 2, 3, 4, 5, 6, 7, 8, 9],
+                parameters=Parameters(
+                    content_type="np", foo=["bar", "bar"], time="13:00"
+                ),
+            ),
+            {"req-1": 1, "req-2": 1, "req-3": 1},
+        ),
         (
             {
                 "req-1": RequestInput(