Skip to content

Commit 6cf3fe3

Browse files
committed
test(mlserver): add model car (OCI image) deployment tests
- Add MLServer model car tests for sklearn, xgboost, and lightgbm using OCI images. - Add mlserver_model_car_inference_service fixture with env variable support. - Add Standard deployment mode support in constants and utils Signed-off-by: Jooho Lee <jlee@redhat.com>
1 parent 01788df commit 6cf3fe3

11 files changed

Lines changed: 426 additions & 25 deletions

tests/model_serving/model_runtime/mlserver/conftest.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,50 @@ def mlserver_model_service_account(admin_client: DynamicClient, kserve_s3_secret
146146
yield sa
147147

148148

149+
@pytest.fixture(scope="class")
150+
def mlserver_model_car_inference_service(
151+
request: pytest.FixtureRequest,
152+
admin_client: DynamicClient,
153+
model_namespace: Namespace,
154+
mlserver_serving_runtime: ServingRuntime,
155+
) -> Generator[InferenceService]:
156+
"""
157+
Create InferenceService for MLServer model car (OCI image) testing.
158+
159+
Args:
160+
request: Pytest fixture request with parameters.
161+
admin_client: Kubernetes dynamic client.
162+
model_namespace: Namespace for deployment.
163+
mlserver_serving_runtime: MLServer ServingRuntime instance.
164+
165+
Yields:
166+
InferenceService: Configured ISVC using OCI storage.
167+
"""
168+
params = request.param
169+
storage_uri = params.get("storage-uri")
170+
if not storage_uri:
171+
raise ValueError("storage-uri is required in params")
172+
173+
deployment_mode = params.get("deployment-mode", KServeDeploymentType.RAW_DEPLOYMENT)
174+
model_format = params.get("model-format")
175+
if not model_format:
176+
raise ValueError("model-format is required in params")
177+
178+
with create_isvc(
179+
client=admin_client,
180+
name=f"{model_format}-modelcar",
181+
namespace=model_namespace.name,
182+
runtime=mlserver_serving_runtime.name,
183+
storage_uri=storage_uri,
184+
model_format=model_format,
185+
deployment_mode=deployment_mode,
186+
external_route=params.get("enable_external_route"),
187+
wait_for_predictor_pods=params.get("wait_for_predictor_pods", False),
188+
model_env_variables=params.get("model_env_variables"),
189+
) as isvc:
190+
yield isvc
191+
192+
149193
@pytest.fixture
150194
def mlserver_response_snapshot(snapshot: Any) -> Any:
151195
"""
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# MLServer Model Car (OCI Image) Tests
2+
3+
End-to-end tests for MLServer inference using model car (OCI image-based) deployments.
4+
Models are packaged as OCI container images and deployed via KServe InferenceService with `storageUri: oci://...`.
5+
6+
## Supported Model Formats
7+
8+
- sklearn
9+
- xgboost
10+
- lightgbm
11+
- onnx
12+
13+
## OCI Model Images
14+
15+
The OCI model images used in these tests are built from:
16+
<https://github.com/Jooho/oci-model-images>
17+
18+
If the version of a supported framework (xgboost, lightgbm, sklearn, onnx) changes in MLServer,
19+
the model images must be rebuilt and pushed from that repository.
20+
21+
The framework versions used by MLServer can be found at:
22+
<https://github.com/red-hat-data-services/MLServer/blob/main/requirements/requirements-cpu.txt#L261>
23+
24+
For e2e testing, images should be tagged with the `-e2e` suffix to pin stable versions.
25+
26+
## Running Tests
27+
28+
Run all model car tests:
29+
30+
```bash
31+
OC_BINARY_PATH=/usr/local/bin/oc uv run pytest tests/model_serving/model_runtime/mlserver/model_car
32+
```
33+
34+
Run a specific model format (e.g., onnx only):
35+
36+
```bash
37+
OC_BINARY_PATH=/usr/local/bin/oc uv run pytest tests/model_serving/model_runtime/mlserver/model_car -k "onnx"
38+
```
39+
40+
## Updating Snapshots
41+
42+
If model responses change and snapshots need to be updated, add the `--snapshot-update` flag:
43+
44+
```bash
45+
OC_BINARY_PATH=/usr/local/bin/oc uv run pytest tests/model_serving/model_runtime/mlserver/model_car --snapshot-update
46+
```

tests/model_serving/model_runtime/mlserver/model_car/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"id": "lightgbm",
3+
"model_name": "lightgbm-modelcar",
4+
"outputs": [
5+
{
6+
"data": [
7+
0.0017564886970520908,
8+
0.008535872687680112,
9+
0.9897076386152678
10+
],
11+
"datatype": "FP64",
12+
"name": "predict",
13+
"parameters": {
14+
"content_type": "np"
15+
},
16+
"shape": [
17+
1,
18+
3
19+
]
20+
}
21+
],
22+
"parameters": {}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"id": "lightgbm",
3+
"model_name": "lightgbm-modelcar",
4+
"outputs": [
5+
{
6+
"data": [
7+
0.0017564886970520908,
8+
0.008535872687680112,
9+
0.9897076386152678
10+
],
11+
"datatype": "FP64",
12+
"name": "predict",
13+
"parameters": {
14+
"content_type": "np"
15+
},
16+
"shape": [
17+
1,
18+
3
19+
]
20+
}
21+
],
22+
"parameters": {}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"id": "onnx",
3+
"model_name": "onnx-modelcar",
4+
"outputs": [
5+
{
6+
"data": [
7+
4.1880645751953125,
8+
2.2079954147338867,
9+
-4.680917263031006
10+
],
11+
"datatype": "FP32",
12+
"name": "predict",
13+
"parameters": {
14+
"content_type": "np"
15+
},
16+
"shape": [
17+
1,
18+
3
19+
]
20+
}
21+
],
22+
"parameters": {}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"id": "sklearn",
3+
"model_name": "sklearn-modelcar",
4+
"outputs": [
5+
{
6+
"data": [
7+
1,
8+
1
9+
],
10+
"datatype": "INT64",
11+
"name": "predict",
12+
"parameters": {
13+
"content_type": "np"
14+
},
15+
"shape": [
16+
2,
17+
1
18+
]
19+
}
20+
],
21+
"parameters": {}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"id": "xgboost",
3+
"model_name": "xgboost-modelcar",
4+
"outputs": [
5+
{
6+
"data": [
7+
1.0,
8+
1.0
9+
],
10+
"datatype": "FP32",
11+
"name": "predict",
12+
"parameters": {
13+
"content_type": "np"
14+
},
15+
"shape": [
16+
2,
17+
1
18+
]
19+
}
20+
],
21+
"parameters": {}
22+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Test module for MLServer model car (OCI image) deployment.
3+
4+
This module validates MLServer inference using model car OCI images
5+
for sklearn, xgboost, and lightgbm formats.
6+
"""
7+
8+
from typing import Any
9+
10+
import pytest
11+
from ocp_resources.inference_service import InferenceService
12+
13+
from tests.model_serving.model_runtime.mlserver.constant import MODEL_CONFIGS
14+
from tests.model_serving.model_runtime.mlserver.utils import (
15+
get_deployment_config_dict,
16+
get_model_namespace_dict,
17+
get_model_storage_uri_dict,
18+
get_test_case_id,
19+
validate_inference_request,
20+
)
21+
from utilities.constants import ModelFormat, Protocols
22+
from utilities.infra import get_pods_by_isvc_label
23+
24+
25+
@pytest.mark.smoke
26+
@pytest.mark.parametrize(
27+
(
28+
"model_namespace",
29+
"mlserver_model_car_inference_service",
30+
"mlserver_serving_runtime",
31+
),
32+
[
33+
pytest.param(
34+
get_model_namespace_dict(model_format_name=ModelFormat.SKLEARN, modelcar=True),
35+
{
36+
**get_model_storage_uri_dict(model_format_name=ModelFormat.SKLEARN, modelcar=True),
37+
**get_deployment_config_dict(model_format_name=ModelFormat.SKLEARN),
38+
},
39+
get_deployment_config_dict(model_format_name=ModelFormat.SKLEARN),
40+
id=get_test_case_id(model_format_name=ModelFormat.SKLEARN, modelcar=True),
41+
),
42+
pytest.param(
43+
get_model_namespace_dict(model_format_name=ModelFormat.XGBOOST, modelcar=True),
44+
{
45+
**get_model_storage_uri_dict(model_format_name=ModelFormat.XGBOOST, modelcar=True),
46+
**get_deployment_config_dict(model_format_name=ModelFormat.XGBOOST),
47+
},
48+
get_deployment_config_dict(model_format_name=ModelFormat.XGBOOST),
49+
id=get_test_case_id(model_format_name=ModelFormat.XGBOOST, modelcar=True),
50+
),
51+
pytest.param(
52+
get_model_namespace_dict(model_format_name=ModelFormat.LIGHTGBM, modelcar=True),
53+
{
54+
**get_model_storage_uri_dict(model_format_name=ModelFormat.LIGHTGBM, modelcar=True),
55+
**get_deployment_config_dict(model_format_name=ModelFormat.LIGHTGBM),
56+
},
57+
get_deployment_config_dict(model_format_name=ModelFormat.LIGHTGBM),
58+
id=get_test_case_id(model_format_name=ModelFormat.LIGHTGBM, modelcar=True),
59+
),
60+
pytest.param(
61+
{"name": f"{ModelFormat.LIGHTGBM}-model-car-text-type"},
62+
{
63+
**get_model_storage_uri_dict(
64+
model_format_name=ModelFormat.LIGHTGBM,
65+
modelcar=True,
66+
env_variables=[{"name": "MLSERVER_MODEL_URI", "value": "/mnt/models/model.txt"}],
67+
),
68+
**get_deployment_config_dict(model_format_name=ModelFormat.LIGHTGBM),
69+
},
70+
get_deployment_config_dict(model_format_name=ModelFormat.LIGHTGBM),
71+
id=get_test_case_id(model_format_name=ModelFormat.LIGHTGBM, modelcar=True) + "_text_type",
72+
),
73+
pytest.param(
74+
get_model_namespace_dict(model_format_name=ModelFormat.ONNX, modelcar=True),
75+
{
76+
**get_model_storage_uri_dict(model_format_name=ModelFormat.ONNX, modelcar=True),
77+
**get_deployment_config_dict(model_format_name=ModelFormat.ONNX),
78+
},
79+
get_deployment_config_dict(model_format_name=ModelFormat.ONNX),
80+
id=get_test_case_id(model_format_name=ModelFormat.ONNX, modelcar=True),
81+
),
82+
],
83+
indirect=[
84+
"model_namespace",
85+
"mlserver_model_car_inference_service",
86+
"mlserver_serving_runtime",
87+
],
88+
)
89+
class TestMLServerModelCar:
90+
"""
91+
Test class for MLServer model car (OCI image) inference.
92+
93+
Validates inference functionality using OCI images for sklearn,
94+
xgboost, and lightgbm model formats.
95+
"""
96+
97+
def test_mlserver_model_car_inference(
98+
self,
99+
mlserver_model_car_inference_service: InferenceService,
100+
mlserver_response_snapshot: Any,
101+
) -> None:
102+
"""
103+
Test model inference using MLServer model car with OCI images.
104+
105+
Validates that MLServer can load models from OCI images and
106+
perform inference using REST protocol.
107+
108+
Args:
109+
mlserver_model_car_inference_service: Deployed inference service.
110+
mlserver_response_snapshot: Expected response for validation.
111+
"""
112+
# Extract model format from InferenceService spec
113+
model_format = mlserver_model_car_inference_service.instance.spec.predictor.model.modelFormat.name
114+
115+
if model_format not in MODEL_CONFIGS:
116+
raise ValueError(f"Unsupported model format: {model_format}")
117+
118+
model_format_config = MODEL_CONFIGS[model_format]
119+
120+
# Get pod directly from inference service (following kserve model_car pattern)
121+
pods = get_pods_by_isvc_label(
122+
client=mlserver_model_car_inference_service.client,
123+
isvc=mlserver_model_car_inference_service,
124+
)
125+
if not pods:
126+
raise RuntimeError(f"No pods found for InferenceService {mlserver_model_car_inference_service.name}")
127+
pod = pods[0]
128+
129+
validate_inference_request(
130+
pod_name=pod.name,
131+
isvc=mlserver_model_car_inference_service,
132+
response_snapshot=mlserver_response_snapshot,
133+
input_query=model_format_config["rest_query"],
134+
model_version="",
135+
model_output_type=model_format_config["output_type"],
136+
protocol=Protocols.REST,
137+
)

0 commit comments

Comments
 (0)