Skip to content

Commit e07a6cb

Browse files
author
Manuel Laventure
committed
Add inference params support to MLFlow's custom invocation endpoint (#1374)
1 parent 0b4abe7 commit e07a6cb

3 files changed

Lines changed: 77 additions & 5 deletions

File tree

runtimes/mlflow/mlserver_mlflow/runtime.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
CONTENT_TYPE_CSV,
1111
CONTENT_TYPE_JSON,
1212
parse_csv_input,
13-
infer_and_parse_json_input,
13+
_split_data_and_params,
14+
infer_and_parse_data,
1415
predictions_to_json,
1516
)
1617

@@ -124,8 +125,10 @@ async def invocations(
124125
if mime_type == CONTENT_TYPE_CSV:
125126
csv_input = StringIO(raw_body)
126127
data = parse_csv_input(csv_input=csv_input, schema=self._input_schema)
128+
inference_params = None
127129
elif mime_type == CONTENT_TYPE_JSON:
128-
data = infer_and_parse_json_input(raw_body, self._input_schema)
130+
raw_data, inference_params = _split_data_and_params(raw_body)
131+
data = infer_and_parse_data(raw_data, self._input_schema)
129132
else:
130133
err_message = (
131134
"This predictor only supports the following content types, "
@@ -134,7 +137,7 @@ async def invocations(
134137
raise InferenceError(err_message)
135138

136139
try:
137-
raw_predictions = self._model.predict(data)
140+
raw_predictions = self._model.predict(data, params=inference_params)
138141
except MlflowException as e:
139142
raise InferenceError(e.message)
140143
except Exception:

runtimes/mlflow/tests/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ def dataset() -> tuple:
4141

4242

4343
@pytest.fixture
44-
def model_signature(dataset: tuple) -> ModelSignature:
44+
def default_inference_params() -> dict:
45+
return {"foo_param": "foo_value"}
46+
47+
48+
@pytest.fixture
49+
def model_signature(dataset: tuple, default_inference_params: dict) -> ModelSignature:
4550
X, y = dataset
46-
signature = infer_signature(X, y)
51+
signature = infer_signature(X, model_output=y, params=default_inference_params)
4752

4853
return signature
4954

runtimes/mlflow/tests/test_runtime.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from unittest import mock
23
import numpy as np
34
import pandas as pd
45

@@ -14,6 +15,7 @@
1415
)
1516
from mlflow.pyfunc import PyFuncModel
1617
from mlflow.models.signature import ModelSignature
18+
from mlflow.pyfunc.scoring_server import CONTENT_TYPE_CSV, CONTENT_TYPE_JSON
1719

1820
from mlserver_mlflow import MLflowRuntime
1921
from mlserver_mlflow.codecs import TensorDictCodec
@@ -188,3 +190,65 @@ async def test_metadata(runtime: MLflowRuntime, model_signature: ModelSignature)
188190

189191
assert metadata.parameters is not None
190192
assert metadata.parameters.content_type == PandasCodec.ContentType
193+
194+
195+
@pytest.mark.parametrize(
196+
"input, expected",
197+
[
198+
# works with params:
199+
(
200+
['{"instances": [1, 2, 3], "params": {"foo": "bar"}}', CONTENT_TYPE_JSON],
201+
{"data": {"foo": [1, 2, 3]}, "params": {"foo": "bar"}},
202+
),
203+
(
204+
[
205+
'{"inputs": [1, 2, 3], "params": {"foo": "bar"}}',
206+
CONTENT_TYPE_JSON,
207+
],
208+
{"data": {"foo": [1, 2, 3]}, "params": {"foo": "bar"}},
209+
),
210+
(
211+
[
212+
'{"inputs": {"foo": [1, 2, 3]}, "params": {"foo": "bar"}}',
213+
CONTENT_TYPE_JSON,
214+
],
215+
{"data": {"foo": [1, 2, 3]}, "params": {"foo": "bar"}},
216+
),
217+
(
218+
[
219+
'{"dataframe_split": {"columns": ["foo"], "data": [1, 2, 3]}, "params": {"foo": "bar"}}',
220+
CONTENT_TYPE_JSON,
221+
],
222+
{"data": {"foo": [1, 2, 3]}, "params": {"foo": "bar"}},
223+
),
224+
(
225+
[
226+
'{"dataframe_records": [{"foo": 1}, {"foo": 2}, {"foo": 3}], "params": {"foo": "bar"}}',
227+
CONTENT_TYPE_JSON,
228+
],
229+
{"data": {"foo": [1, 2, 3]}, "params": {"foo": "bar"}},
230+
),
231+
(
232+
["foo\n1\n2\n3\n", CONTENT_TYPE_CSV],
233+
{"data": {"foo": [1, 2, 3]}, "params": None},
234+
),
235+
# works without params:
236+
(
237+
['{"instances": [1, 2, 3]}', CONTENT_TYPE_JSON],
238+
{"data": {"foo": [1, 2, 3]}, "params": None},
239+
),
240+
],
241+
)
242+
async def test_invocation_with_params(
243+
runtime: MLflowRuntime,
244+
input: list,
245+
expected: dict,
246+
):
247+
with mock.patch.object(
248+
runtime._model, "predict", return_value=[1, 2, 3]
249+
) as predict_mock:
250+
await runtime.invocations(*input)
251+
np.testing.assert_array_equal(
252+
predict_mock.call_args[0][0].get("foo"), expected["data"]["foo"]
253+
)
254+
assert predict_mock.call_args.kwargs["params"] == expected["params"]

0 commit comments

Comments
 (0)