mlrun 1.10.0rc21__py3-none-any.whl → 1.10.0rc23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mlrun might be problematic. Click here for more details.
- mlrun/artifacts/llm_prompt.py +11 -10
- mlrun/artifacts/model.py +3 -3
- mlrun/common/schemas/auth.py +2 -0
- mlrun/common/schemas/model_monitoring/functions.py +13 -4
- mlrun/datastore/datastore.py +6 -1
- mlrun/datastore/model_provider/mock_model_provider.py +87 -0
- mlrun/db/base.py +9 -0
- mlrun/db/httpdb.py +21 -1
- mlrun/db/nopdb.py +8 -0
- mlrun/execution.py +52 -10
- mlrun/k8s_utils.py +105 -2
- mlrun/model_monitoring/applications/__init__.py +1 -1
- mlrun/model_monitoring/applications/base.py +86 -33
- mlrun/model_monitoring/controller.py +1 -1
- mlrun/model_monitoring/db/_schedules.py +21 -0
- mlrun/model_monitoring/db/tsdb/base.py +14 -5
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +4 -5
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +53 -20
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +39 -1
- mlrun/projects/project.py +50 -7
- mlrun/serving/server.py +24 -7
- mlrun/serving/states.py +358 -75
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.10.0rc21.dist-info → mlrun-1.10.0rc23.dist-info}/METADATA +3 -3
- {mlrun-1.10.0rc21.dist-info → mlrun-1.10.0rc23.dist-info}/RECORD +29 -28
- {mlrun-1.10.0rc21.dist-info → mlrun-1.10.0rc23.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc21.dist-info → mlrun-1.10.0rc23.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc21.dist-info → mlrun-1.10.0rc23.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc21.dist-info → mlrun-1.10.0rc23.dist-info}/top_level.txt +0 -0
mlrun/artifacts/llm_prompt.py
CHANGED
|
@@ -83,19 +83,20 @@ class LLMPromptArtifactSpec(ArtifactSpec):
|
|
|
83
83
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
84
84
|
"Expected prompt_template to be a list of dicts"
|
|
85
85
|
)
|
|
86
|
-
keys_to_pop = []
|
|
87
86
|
for message in prompt_template:
|
|
87
|
+
if set(key.lower() for key in message.keys()) != set(
|
|
88
|
+
self.PROMPT_TEMPLATE_KEYS
|
|
89
|
+
):
|
|
90
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
91
|
+
f"Expected prompt_template to contain dicts with keys "
|
|
92
|
+
f"{self.PROMPT_TEMPLATE_KEYS}, got {message.keys()}"
|
|
93
|
+
)
|
|
94
|
+
keys_to_pop = []
|
|
88
95
|
for key in message.keys():
|
|
89
96
|
if isinstance(key, str):
|
|
90
|
-
if key.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
f"only has keys from {self.PROMPT_TEMPLATE_KEYS}"
|
|
94
|
-
)
|
|
95
|
-
else:
|
|
96
|
-
if not key.islower():
|
|
97
|
-
message[key.lower()] = message[key]
|
|
98
|
-
keys_to_pop.append(key)
|
|
97
|
+
if not key.islower():
|
|
98
|
+
message[key.lower()] = message[key]
|
|
99
|
+
keys_to_pop.append(key)
|
|
99
100
|
else:
|
|
100
101
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
101
102
|
f"Expected prompt_template to contain dict that only"
|
mlrun/artifacts/model.py
CHANGED
|
@@ -190,10 +190,10 @@ class ModelArtifact(Artifact):
|
|
|
190
190
|
"""
|
|
191
191
|
super().__init__(key, body, format=format, target_path=target_path, **kwargs)
|
|
192
192
|
model_file = str(model_file or "")
|
|
193
|
-
if model_file and model_url:
|
|
193
|
+
if (model_file or model_dir or body) and model_url:
|
|
194
194
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
195
|
-
"Arguments 'model_file' and '
|
|
196
|
-
" used together with '
|
|
195
|
+
"Arguments 'model_file' and 'model_url' cannot be"
|
|
196
|
+
" used together with 'model_file', 'model_dir' or 'body'."
|
|
197
197
|
)
|
|
198
198
|
if model_file and "/" in model_file:
|
|
199
199
|
if model_dir:
|
mlrun/common/schemas/auth.py
CHANGED
|
@@ -55,6 +55,7 @@ class AuthorizationResourceTypes(mlrun.common.types.StrEnum):
|
|
|
55
55
|
secret = "secret"
|
|
56
56
|
run = "run"
|
|
57
57
|
model_endpoint = "model-endpoint"
|
|
58
|
+
model_monitoring = "model-monitoring"
|
|
58
59
|
pipeline = "pipeline"
|
|
59
60
|
hub_source = "hub-source"
|
|
60
61
|
workflow = "workflow"
|
|
@@ -96,6 +97,7 @@ class AuthorizationResourceTypes(mlrun.common.types.StrEnum):
|
|
|
96
97
|
# runtime resource doesn't have an identifier, we don't need any auth granularity behind project level
|
|
97
98
|
AuthorizationResourceTypes.runtime_resource: "/projects/{project_name}/runtime-resources",
|
|
98
99
|
AuthorizationResourceTypes.model_endpoint: "/projects/{project_name}/model-endpoints/{resource_name}",
|
|
100
|
+
AuthorizationResourceTypes.model_monitoring: "/projects/{project_name}/model-monitoring/{resource_name}",
|
|
99
101
|
AuthorizationResourceTypes.pipeline: "/projects/{project_name}/pipelines/{resource_name}",
|
|
100
102
|
AuthorizationResourceTypes.datastore_profile: "/projects/{project_name}/datastore_profiles",
|
|
101
103
|
# Hub sources are not project-scoped, and auth is globally on the sources endpoint.
|
|
@@ -54,12 +54,21 @@ class FunctionSummary(BaseModel):
|
|
|
54
54
|
|
|
55
55
|
return cls(
|
|
56
56
|
type=func_type,
|
|
57
|
-
name=func_dict["metadata"]["name"]
|
|
57
|
+
name=func_dict["metadata"]["name"]
|
|
58
|
+
if func_type != FunctionsType.APPLICATION
|
|
59
|
+
else func_dict["spec"]
|
|
60
|
+
.get("graph", {})
|
|
61
|
+
.get("steps", {})
|
|
62
|
+
.get("PrepareMonitoringEvent", {})
|
|
63
|
+
.get("class_args", {})
|
|
64
|
+
.get("application_name"),
|
|
58
65
|
application_class=""
|
|
59
66
|
if func_type != FunctionsType.APPLICATION
|
|
60
|
-
else func_dict["spec"]
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
else func_dict["spec"]
|
|
68
|
+
.get("graph", {})
|
|
69
|
+
.get("steps", {})
|
|
70
|
+
.get("PushToMonitoringWriter", {})
|
|
71
|
+
.get("after", [None])[0],
|
|
63
72
|
project_name=func_dict["metadata"]["project"],
|
|
64
73
|
updated_time=func_dict["metadata"].get("updated"),
|
|
65
74
|
status=func_dict["status"].get("state"),
|
mlrun/datastore/datastore.py
CHANGED
|
@@ -39,6 +39,7 @@ from .base import DataItem, DataStore, HttpStore
|
|
|
39
39
|
from .filestore import FileStore
|
|
40
40
|
from .inmem import InMemoryStore
|
|
41
41
|
from .model_provider.huggingface_provider import HuggingFaceProvider
|
|
42
|
+
from .model_provider.mock_model_provider import MockModelProvider
|
|
42
43
|
from .model_provider.openai_provider import OpenAIProvider
|
|
43
44
|
from .store_resources import get_store_resource, is_store_uri
|
|
44
45
|
from .v3io import V3ioStore
|
|
@@ -103,7 +104,11 @@ def schema_to_store(schema) -> DataStore.__subclasses__():
|
|
|
103
104
|
def schema_to_model_provider(
|
|
104
105
|
schema: str, raise_missing_schema_exception=True
|
|
105
106
|
) -> type[ModelProvider]:
|
|
106
|
-
schema_dict = {
|
|
107
|
+
schema_dict = {
|
|
108
|
+
"openai": OpenAIProvider,
|
|
109
|
+
"huggingface": HuggingFaceProvider,
|
|
110
|
+
"mock": MockModelProvider,
|
|
111
|
+
}
|
|
107
112
|
provider_class = schema_dict.get(schema, None)
|
|
108
113
|
if not provider_class:
|
|
109
114
|
if raise_missing_schema_exception:
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright 2023 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Any, Optional, Union
|
|
16
|
+
|
|
17
|
+
import mlrun
|
|
18
|
+
from mlrun.datastore.model_provider.model_provider import (
|
|
19
|
+
InvokeResponseFormat,
|
|
20
|
+
ModelProvider,
|
|
21
|
+
UsageResponseKeys,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MockModelProvider(ModelProvider):
|
|
26
|
+
support_async = False
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
parent,
|
|
31
|
+
kind,
|
|
32
|
+
name,
|
|
33
|
+
endpoint="",
|
|
34
|
+
secrets: Optional[dict] = None,
|
|
35
|
+
default_invoke_kwargs: Optional[dict] = None,
|
|
36
|
+
):
|
|
37
|
+
super().__init__(
|
|
38
|
+
parent=parent, name=name, kind=kind, endpoint=endpoint, secrets=secrets
|
|
39
|
+
)
|
|
40
|
+
self.default_invoke_kwargs = default_invoke_kwargs or {}
|
|
41
|
+
self._client = None
|
|
42
|
+
self._async_client = None
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _extract_string_output(response: Any) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Extracts string response from response object
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def load_client(self) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Initializes the SDK client for the model provider with the given keyword arguments
|
|
54
|
+
and assigns it to an instance attribute (e.g., self._client).
|
|
55
|
+
|
|
56
|
+
Subclasses should override this method to:
|
|
57
|
+
- Create and configure the provider-specific client instance.
|
|
58
|
+
- Assign the client instance to self._client.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def invoke(
|
|
64
|
+
self,
|
|
65
|
+
messages: Union[list[dict], Any],
|
|
66
|
+
invoke_response_format: InvokeResponseFormat = InvokeResponseFormat.FULL,
|
|
67
|
+
**invoke_kwargs,
|
|
68
|
+
) -> Union[str, dict[str, Any], Any]:
|
|
69
|
+
if invoke_response_format == InvokeResponseFormat.STRING:
|
|
70
|
+
return (
|
|
71
|
+
"You are using a mock model provider, no actual inference is performed."
|
|
72
|
+
)
|
|
73
|
+
elif invoke_response_format == InvokeResponseFormat.FULL:
|
|
74
|
+
return {
|
|
75
|
+
UsageResponseKeys.USAGE: {"prompt_tokens": 0, "completion_tokens": 0},
|
|
76
|
+
UsageResponseKeys.ANSWER: "You are using a mock model provider, no actual inference is performed.",
|
|
77
|
+
"extra": {},
|
|
78
|
+
}
|
|
79
|
+
elif invoke_response_format == InvokeResponseFormat.USAGE:
|
|
80
|
+
return {
|
|
81
|
+
UsageResponseKeys.ANSWER: "You are using a mock model provider, no actual inference is performed.",
|
|
82
|
+
UsageResponseKeys.USAGE: {"prompt_tokens": 0, "completion_tokens": 0},
|
|
83
|
+
}
|
|
84
|
+
else:
|
|
85
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
86
|
+
f"Unsupported invoke response format: {invoke_response_format}"
|
|
87
|
+
)
|
mlrun/db/base.py
CHANGED
|
@@ -1111,6 +1111,15 @@ class RunDBInterface(ABC):
|
|
|
1111
1111
|
) -> None:
|
|
1112
1112
|
pass
|
|
1113
1113
|
|
|
1114
|
+
@abstractmethod
|
|
1115
|
+
def delete_model_monitoring_metrics(
|
|
1116
|
+
self,
|
|
1117
|
+
project: str,
|
|
1118
|
+
application_name: str,
|
|
1119
|
+
endpoint_ids: Optional[list[str]] = None,
|
|
1120
|
+
) -> None:
|
|
1121
|
+
pass
|
|
1122
|
+
|
|
1114
1123
|
@abstractmethod
|
|
1115
1124
|
def get_monitoring_function_summaries(
|
|
1116
1125
|
self,
|
mlrun/db/httpdb.py
CHANGED
|
@@ -3580,7 +3580,7 @@ class HTTPRunDB(RunDBInterface):
|
|
|
3580
3580
|
intersection {"intersect_metrics":[], "intersect_results":[]}
|
|
3581
3581
|
:return: A dictionary of application metrics and/or results for the model endpoints formatted by events_format.
|
|
3582
3582
|
"""
|
|
3583
|
-
path = f"projects/{project}/model-
|
|
3583
|
+
path = f"projects/{project}/model-monitoring/metrics"
|
|
3584
3584
|
params = {
|
|
3585
3585
|
"type": type,
|
|
3586
3586
|
"endpoint-id": endpoint_ids,
|
|
@@ -4121,6 +4121,26 @@ class HTTPRunDB(RunDBInterface):
|
|
|
4121
4121
|
params={**credentials, "replace_creds": replace_creds},
|
|
4122
4122
|
)
|
|
4123
4123
|
|
|
4124
|
+
def delete_model_monitoring_metrics(
|
|
4125
|
+
self,
|
|
4126
|
+
project: str,
|
|
4127
|
+
application_name: str,
|
|
4128
|
+
endpoint_ids: Optional[list[str]] = None,
|
|
4129
|
+
) -> None:
|
|
4130
|
+
"""
|
|
4131
|
+
Delete model endpoints metrics values.
|
|
4132
|
+
|
|
4133
|
+
:param project: The name of the project.
|
|
4134
|
+
:param application_name: The name of the application.
|
|
4135
|
+
:param endpoint_ids: The unique IDs of the model endpoints to delete metrics values from. If none is
|
|
4136
|
+
provided, the metrics values will be deleted from all project's model endpoints.
|
|
4137
|
+
"""
|
|
4138
|
+
self.api_call(
|
|
4139
|
+
method=mlrun.common.types.HTTPMethod.DELETE,
|
|
4140
|
+
path=f"projects/{project}/model-monitoring/metrics",
|
|
4141
|
+
params={"endpoint-id": endpoint_ids, "application-name": application_name},
|
|
4142
|
+
)
|
|
4143
|
+
|
|
4124
4144
|
def get_monitoring_function_summaries(
|
|
4125
4145
|
self,
|
|
4126
4146
|
project: str,
|
mlrun/db/nopdb.py
CHANGED
|
@@ -885,6 +885,14 @@ class NopDB(RunDBInterface):
|
|
|
885
885
|
) -> None:
|
|
886
886
|
pass
|
|
887
887
|
|
|
888
|
+
def delete_model_monitoring_metrics(
|
|
889
|
+
self,
|
|
890
|
+
project: str,
|
|
891
|
+
application_name: str,
|
|
892
|
+
endpoint_ids: Optional[list[str]] = None,
|
|
893
|
+
) -> None:
|
|
894
|
+
pass
|
|
895
|
+
|
|
888
896
|
def get_monitoring_function_summaries(
|
|
889
897
|
self,
|
|
890
898
|
project: str,
|
mlrun/execution.py
CHANGED
|
@@ -934,14 +934,51 @@ class MLClientCtx:
|
|
|
934
934
|
|
|
935
935
|
Examples::
|
|
936
936
|
|
|
937
|
-
# Log an inline prompt
|
|
937
|
+
# Log directly with an inline prompt template
|
|
938
938
|
context.log_llm_prompt(
|
|
939
|
-
key="
|
|
940
|
-
prompt_template=[
|
|
939
|
+
key="customer_support_prompt",
|
|
940
|
+
prompt_template=[
|
|
941
|
+
{
|
|
942
|
+
"role": "system",
|
|
943
|
+
"content": "You are a helpful customer support assistant.",
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
"role": "user",
|
|
947
|
+
"content": "The customer reports: {issue_description}",
|
|
948
|
+
},
|
|
949
|
+
],
|
|
950
|
+
prompt_legend={
|
|
951
|
+
"issue_description": {
|
|
952
|
+
"field": "user_issue",
|
|
953
|
+
"description": "Detailed description of the customer's issue",
|
|
954
|
+
},
|
|
955
|
+
"solution": {
|
|
956
|
+
"field": "proposed_solution",
|
|
957
|
+
"description": "Suggested fix for the customer's issue",
|
|
958
|
+
},
|
|
959
|
+
},
|
|
941
960
|
model_artifact=model,
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
tag="
|
|
961
|
+
model_configuration={"temperature": 0.5, "max_tokens": 200},
|
|
962
|
+
description="Prompt for handling customer support queries",
|
|
963
|
+
tag="support-v1",
|
|
964
|
+
labels={"domain": "support"},
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Log a prompt from file
|
|
968
|
+
context.log_llm_prompt(
|
|
969
|
+
key="qa_prompt",
|
|
970
|
+
prompt_path="prompts/template.json",
|
|
971
|
+
prompt_legend={
|
|
972
|
+
"question": {
|
|
973
|
+
"field": "user_question",
|
|
974
|
+
"description": "The actual question asked by the user",
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
model_artifact=model,
|
|
978
|
+
model_configuration={"temperature": 0.7, "max_tokens": 256},
|
|
979
|
+
description="Q&A prompt template with user-provided question",
|
|
980
|
+
tag="v2",
|
|
981
|
+
labels={"task": "qa", "stage": "experiment"},
|
|
945
982
|
)
|
|
946
983
|
|
|
947
984
|
:param key: Unique name of the artifact.
|
|
@@ -950,7 +987,10 @@ class MLClientCtx:
|
|
|
950
987
|
"role": "user", "content": "I need your help with {profession}"]. only "role" and "content" keys allow in any
|
|
951
988
|
str format (upper/lower case), keys will be modified to lower case.
|
|
952
989
|
Cannot be used with `prompt_path`.
|
|
953
|
-
:param prompt_path: Path to a file containing the prompt
|
|
990
|
+
:param prompt_path: Path to a JSON file containing the prompt template.
|
|
991
|
+
Cannot be used together with `prompt_template`.
|
|
992
|
+
The file should define a list of dictionaries in the same format
|
|
993
|
+
supported by `prompt_template`.
|
|
954
994
|
:param prompt_legend: A dictionary where each key is a placeholder in the prompt (e.g., ``{user_name}``)
|
|
955
995
|
and the value is a dictionary holding two keys, "field", "description". "field" points to the field in
|
|
956
996
|
the event where the value of the place-holder inside the event, if None or not exist will be replaced
|
|
@@ -958,9 +998,11 @@ class MLClientCtx:
|
|
|
958
998
|
Useful for documenting and clarifying dynamic parts of the prompt.
|
|
959
999
|
:param model_artifact: Reference to the parent model (either `ModelArtifact` or model URI string).
|
|
960
1000
|
:param model_configuration: Dictionary of generation parameters (e.g., temperature, max_tokens).
|
|
961
|
-
:param description:
|
|
962
|
-
:param target_path:
|
|
963
|
-
:param artifact_path:
|
|
1001
|
+
:param description: Optional description of the prompt.
|
|
1002
|
+
:param target_path: Absolute target path (instead of using artifact_path + local_path)
|
|
1003
|
+
:param artifact_path: Target artifact path (when not using the default)
|
|
1004
|
+
To define a subpath under the default location use:
|
|
1005
|
+
`artifact_path=context.artifact_subpath('data')`
|
|
964
1006
|
:param tag: Tag/version to assign to the prompt artifact.
|
|
965
1007
|
:param labels: Labels to tag the artifact (e.g., list or dict of key-value pairs).
|
|
966
1008
|
:param upload: Whether to upload the artifact to the store (defaults to True).
|
mlrun/k8s_utils.py
CHANGED
|
@@ -26,6 +26,10 @@ from .config import config as mlconfig
|
|
|
26
26
|
|
|
27
27
|
_running_inside_kubernetes_cluster = None
|
|
28
28
|
|
|
29
|
+
K8sObj = typing.Union[kubernetes.client.V1Affinity, kubernetes.client.V1Toleration]
|
|
30
|
+
SanitizedK8sObj = dict[str, typing.Any]
|
|
31
|
+
K8sObjList = typing.Union[list[K8sObj], list[SanitizedK8sObj]]
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
def is_running_inside_kubernetes_cluster():
|
|
31
35
|
global _running_inside_kubernetes_cluster
|
|
@@ -232,6 +236,54 @@ def validate_node_selectors(
|
|
|
232
236
|
return True
|
|
233
237
|
|
|
234
238
|
|
|
239
|
+
def sanitize_k8s_objects(
|
|
240
|
+
k8s_objects: typing.Union[None, K8sObjList, SanitizedK8sObj, K8sObj],
|
|
241
|
+
) -> typing.Union[list[SanitizedK8sObj], SanitizedK8sObj]:
|
|
242
|
+
"""Convert K8s objects to dicts. Handles single objects or lists."""
|
|
243
|
+
api_client = kubernetes.client.ApiClient()
|
|
244
|
+
if not k8s_objects:
|
|
245
|
+
return k8s_objects
|
|
246
|
+
|
|
247
|
+
def _sanitize_k8s_object(k8s_obj):
|
|
248
|
+
return (
|
|
249
|
+
api_client.sanitize_for_serialization(k8s_obj)
|
|
250
|
+
if hasattr(k8s_obj, "to_dict")
|
|
251
|
+
else k8s_obj
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
[_sanitize_k8s_object(k8s_obj) for k8s_obj in k8s_objects]
|
|
256
|
+
if isinstance(k8s_objects, list)
|
|
257
|
+
else _sanitize_k8s_object(k8s_objects)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def sanitize_scheduling_configuration(
|
|
262
|
+
tolerations: typing.Optional[list[kubernetes.client.V1Toleration]] = None,
|
|
263
|
+
affinity: typing.Optional[kubernetes.client.V1Affinity] = None,
|
|
264
|
+
) -> tuple[
|
|
265
|
+
typing.Optional[list[dict]],
|
|
266
|
+
typing.Optional[dict],
|
|
267
|
+
]:
|
|
268
|
+
"""
|
|
269
|
+
Sanitizes pod scheduling configuration for serialization.
|
|
270
|
+
|
|
271
|
+
Takes affinity and tolerations and converts them to
|
|
272
|
+
JSON-serializable dictionaries using the Kubernetes API client's
|
|
273
|
+
sanitization method.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
affinity: Pod affinity/anti-affinity rules
|
|
277
|
+
tolerations: List of toleration rules
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (sanitized_affinity, sanitized_tolerations)
|
|
281
|
+
- affinity: Sanitized dict representation or None
|
|
282
|
+
- tolerations: List of sanitized dict representations or None
|
|
283
|
+
"""
|
|
284
|
+
return sanitize_k8s_objects(tolerations), sanitize_k8s_objects(affinity)
|
|
285
|
+
|
|
286
|
+
|
|
235
287
|
def enrich_preemption_mode(
|
|
236
288
|
preemption_mode: typing.Optional[str],
|
|
237
289
|
node_selector: dict[str, str],
|
|
@@ -269,8 +321,8 @@ def enrich_preemption_mode(
|
|
|
269
321
|
)
|
|
270
322
|
|
|
271
323
|
enriched_node_selector = copy.deepcopy(node_selector or {})
|
|
272
|
-
enriched_tolerations =
|
|
273
|
-
enriched_affinity =
|
|
324
|
+
enriched_tolerations = _safe_copy_tolerations(tolerations or [])
|
|
325
|
+
enriched_affinity = _safe_copy_affinity(affinity)
|
|
274
326
|
preemptible_tolerations = generate_preemptible_tolerations()
|
|
275
327
|
|
|
276
328
|
if handler := _get_mode_handler(preemption_mode):
|
|
@@ -288,6 +340,57 @@ def enrich_preemption_mode(
|
|
|
288
340
|
)
|
|
289
341
|
|
|
290
342
|
|
|
343
|
+
def _safe_copy_tolerations(
|
|
344
|
+
tolerations: list[kubernetes.client.V1Toleration],
|
|
345
|
+
) -> list[kubernetes.client.V1Toleration]:
|
|
346
|
+
"""
|
|
347
|
+
Safely copy a list of V1Toleration objects without mutating the originals.
|
|
348
|
+
|
|
349
|
+
Explicitly reconstructs V1Toleration objects instead of using deepcopy() to avoid
|
|
350
|
+
serialization errors with K8s client objects that contain threading primitives
|
|
351
|
+
and non-copyable elements like RLock objects.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
tolerations: List of V1Toleration objects to copy
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
New list containing copied V1Toleration objects with identical field values"""
|
|
358
|
+
return [
|
|
359
|
+
kubernetes.client.V1Toleration(
|
|
360
|
+
effect=toleration.effect,
|
|
361
|
+
key=toleration.key,
|
|
362
|
+
value=toleration.value,
|
|
363
|
+
operator=toleration.operator,
|
|
364
|
+
toleration_seconds=toleration.toleration_seconds,
|
|
365
|
+
)
|
|
366
|
+
for toleration in tolerations
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _safe_copy_affinity(
|
|
371
|
+
affinity: kubernetes.client.V1Affinity,
|
|
372
|
+
) -> kubernetes.client.V1Affinity:
|
|
373
|
+
"""
|
|
374
|
+
Safely create a deep copy of a V1Affinity object.
|
|
375
|
+
|
|
376
|
+
Uses K8s API client serialization/deserialization instead of deepcopy() to avoid
|
|
377
|
+
errors with threading primitives and complex internal structures in K8s objects.
|
|
378
|
+
Serializes to dict then deserializes back to a clean V1Affinity object.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
affinity: V1Affinity object to copy, or None
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
New V1Affinity object with identical field values, or None if input was None
|
|
385
|
+
"""
|
|
386
|
+
if not affinity:
|
|
387
|
+
return None
|
|
388
|
+
api_client = kubernetes.client.ApiClient()
|
|
389
|
+
# Convert to dict then back to object properly
|
|
390
|
+
affinity_dict = api_client.sanitize_for_serialization(affinity)
|
|
391
|
+
return api_client._ApiClient__deserialize(affinity_dict, "V1Affinity")
|
|
392
|
+
|
|
393
|
+
|
|
291
394
|
def _get_mode_handler(mode: str):
|
|
292
395
|
return {
|
|
293
396
|
mlrun.common.schemas.PreemptionModes.prevent: _handle_prevent_mode,
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from .base import ModelMonitoringApplicationBase
|
|
15
|
+
from .base import ExistingDataHandling, ModelMonitoringApplicationBase
|
|
16
16
|
from .context import MonitoringApplicationContext
|
|
17
17
|
from .results import ModelMonitoringApplicationMetric, ModelMonitoringApplicationResult
|