mlrun 1.10.0rc19__py3-none-any.whl → 1.10.0rc20__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/common/schemas/function.py +10 -0
- mlrun/common/schemas/model_monitoring/constants.py +4 -11
- mlrun/common/schemas/model_monitoring/model_endpoints.py +2 -0
- mlrun/datastore/model_provider/huggingface_provider.py +109 -20
- mlrun/datastore/model_provider/model_provider.py +110 -32
- mlrun/datastore/model_provider/openai_provider.py +87 -31
- mlrun/db/base.py +0 -19
- mlrun/db/httpdb.py +10 -46
- mlrun/db/nopdb.py +0 -10
- mlrun/launcher/base.py +0 -6
- mlrun/model_monitoring/api.py +43 -22
- mlrun/model_monitoring/applications/base.py +1 -1
- mlrun/model_monitoring/controller.py +112 -38
- mlrun/model_monitoring/db/_schedules.py +13 -9
- mlrun/model_monitoring/stream_processing.py +16 -12
- mlrun/platforms/__init__.py +3 -2
- mlrun/projects/project.py +2 -2
- mlrun/serving/server.py +23 -0
- mlrun/serving/states.py +76 -29
- mlrun/serving/system_steps.py +60 -36
- mlrun/utils/helpers.py +27 -13
- mlrun/utils/notifications/notification_pusher.py +1 -1
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.10.0rc19.dist-info → mlrun-1.10.0rc20.dist-info}/METADATA +4 -4
- {mlrun-1.10.0rc19.dist-info → mlrun-1.10.0rc20.dist-info}/RECORD +29 -30
- mlrun/api/schemas/__init__.py +0 -259
- {mlrun-1.10.0rc19.dist-info → mlrun-1.10.0rc20.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc19.dist-info → mlrun-1.10.0rc20.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc19.dist-info → mlrun-1.10.0rc20.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc19.dist-info → mlrun-1.10.0rc20.dist-info}/top_level.txt +0 -0
|
@@ -162,19 +162,19 @@ class ModelMonitoringSchedulesFileEndpoint(ModelMonitoringSchedulesFileBase):
|
|
|
162
162
|
endpoint_id=model_endpoint.metadata.uid,
|
|
163
163
|
)
|
|
164
164
|
|
|
165
|
-
def get_application_time(self, application: str) -> Optional[
|
|
165
|
+
def get_application_time(self, application: str) -> Optional[float]:
|
|
166
166
|
self._check_open_schedules()
|
|
167
167
|
return self._schedules.get(application)
|
|
168
168
|
|
|
169
|
-
def update_application_time(self, application: str, timestamp:
|
|
169
|
+
def update_application_time(self, application: str, timestamp: float) -> None:
|
|
170
170
|
self._check_open_schedules()
|
|
171
|
-
self._schedules[application] = timestamp
|
|
171
|
+
self._schedules[application] = float(timestamp)
|
|
172
172
|
|
|
173
173
|
def get_application_list(self) -> set[str]:
|
|
174
174
|
self._check_open_schedules()
|
|
175
175
|
return set(self._schedules.keys())
|
|
176
176
|
|
|
177
|
-
def get_min_timestamp(self) -> Optional[
|
|
177
|
+
def get_min_timestamp(self) -> Optional[float]:
|
|
178
178
|
self._check_open_schedules()
|
|
179
179
|
return min(self._schedules.values(), default=None)
|
|
180
180
|
|
|
@@ -198,7 +198,7 @@ class ModelMonitoringSchedulesFileChief(ModelMonitoringSchedulesFileBase):
|
|
|
198
198
|
project=self._project
|
|
199
199
|
)
|
|
200
200
|
|
|
201
|
-
def get_endpoint_last_request(self, endpoint_uid: str) -> Optional[
|
|
201
|
+
def get_endpoint_last_request(self, endpoint_uid: str) -> Optional[float]:
|
|
202
202
|
self._check_open_schedules()
|
|
203
203
|
if endpoint_uid in self._schedules:
|
|
204
204
|
return self._schedules[endpoint_uid].get(
|
|
@@ -208,15 +208,19 @@ class ModelMonitoringSchedulesFileChief(ModelMonitoringSchedulesFileBase):
|
|
|
208
208
|
return None
|
|
209
209
|
|
|
210
210
|
def update_endpoint_timestamps(
|
|
211
|
-
self, endpoint_uid: str, last_request:
|
|
211
|
+
self, endpoint_uid: str, last_request: float, last_analyzed: float
|
|
212
212
|
) -> None:
|
|
213
213
|
self._check_open_schedules()
|
|
214
214
|
self._schedules[endpoint_uid] = {
|
|
215
|
-
schemas.model_monitoring.constants.ScheduleChiefFields.LAST_REQUEST:
|
|
216
|
-
|
|
215
|
+
schemas.model_monitoring.constants.ScheduleChiefFields.LAST_REQUEST: float(
|
|
216
|
+
last_request
|
|
217
|
+
),
|
|
218
|
+
schemas.model_monitoring.constants.ScheduleChiefFields.LAST_ANALYZED: float(
|
|
219
|
+
last_analyzed
|
|
220
|
+
),
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
def get_endpoint_last_analyzed(self, endpoint_uid: str) -> Optional[
|
|
223
|
+
def get_endpoint_last_analyzed(self, endpoint_uid: str) -> Optional[float]:
|
|
220
224
|
self._check_open_schedules()
|
|
221
225
|
if endpoint_uid in self._schedules:
|
|
222
226
|
return self._schedules[endpoint_uid].get(
|
|
@@ -396,6 +396,8 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
|
|
|
396
396
|
request_id = event.get("request", {}).get("id") or event.get("resp", {}).get(
|
|
397
397
|
"id"
|
|
398
398
|
)
|
|
399
|
+
feature_names = event.get("request", {}).get("input_schema")
|
|
400
|
+
labels_names = event.get("resp", {}).get("output_schema")
|
|
399
401
|
latency = event.get("microsec")
|
|
400
402
|
features = event.get("request", {}).get("inputs")
|
|
401
403
|
predictions = event.get("resp", {}).get("outputs")
|
|
@@ -496,6 +498,8 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
|
|
|
496
498
|
),
|
|
497
499
|
EventFieldType.EFFECTIVE_SAMPLE_COUNT: effective_sample_count,
|
|
498
500
|
EventFieldType.ESTIMATED_PREDICTION_COUNT: estimated_prediction_count,
|
|
501
|
+
EventFieldType.FEATURE_NAMES: feature_names,
|
|
502
|
+
EventFieldType.LABEL_NAMES: labels_names,
|
|
499
503
|
}
|
|
500
504
|
)
|
|
501
505
|
|
|
@@ -602,19 +606,19 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
|
|
|
602
606
|
self.endpoint_type = {}
|
|
603
607
|
|
|
604
608
|
def _infer_feature_names_from_data(self, event):
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
609
|
+
endpoint_id = event[EventFieldType.ENDPOINT_ID]
|
|
610
|
+
if endpoint_id in self.feature_names and len(
|
|
611
|
+
self.feature_names[endpoint_id]
|
|
612
|
+
) >= len(event[EventFieldType.FEATURES]):
|
|
613
|
+
return self.feature_names[endpoint_id]
|
|
610
614
|
return None
|
|
611
615
|
|
|
612
616
|
def _infer_label_columns_from_data(self, event):
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
endpoint_id = event[EventFieldType.ENDPOINT_ID]
|
|
618
|
+
if endpoint_id in self.label_columns and len(
|
|
619
|
+
self.label_columns[endpoint_id]
|
|
620
|
+
) >= len(event[EventFieldType.PREDICTION]):
|
|
621
|
+
return self.label_columns[endpoint_id]
|
|
618
622
|
return None
|
|
619
623
|
|
|
620
624
|
def do(self, event: dict):
|
|
@@ -659,7 +663,7 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
|
|
|
659
663
|
"Feature names are not initialized, they will be automatically generated",
|
|
660
664
|
endpoint_id=endpoint_id,
|
|
661
665
|
)
|
|
662
|
-
feature_names = [
|
|
666
|
+
feature_names = event.get(EventFieldType.FEATURE_NAMES) or [
|
|
663
667
|
f"f{i}" for i, _ in enumerate(event[EventFieldType.FEATURES])
|
|
664
668
|
]
|
|
665
669
|
|
|
@@ -682,7 +686,7 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
|
|
|
682
686
|
"label column names are not initialized, they will be automatically generated",
|
|
683
687
|
endpoint_id=endpoint_id,
|
|
684
688
|
)
|
|
685
|
-
label_columns = [
|
|
689
|
+
label_columns = event.get(EventFieldType.LABEL_NAMES) or [
|
|
686
690
|
f"p{i}" for i, _ in enumerate(event[EventFieldType.PREDICTION])
|
|
687
691
|
]
|
|
688
692
|
attributes_to_update[EventFieldType.LABEL_NAMES] = label_columns
|
mlrun/platforms/__init__.py
CHANGED
|
@@ -25,6 +25,7 @@ from .iguazio import (
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
# TODO: Remove in 1.11.0
|
|
28
29
|
class _DeprecationHelper:
|
|
29
30
|
"""A helper class to deprecate old schemas"""
|
|
30
31
|
|
|
@@ -48,12 +49,12 @@ class _DeprecationHelper:
|
|
|
48
49
|
def _warn(self):
|
|
49
50
|
warnings.warn(
|
|
50
51
|
f"mlrun.platforms.{self._new_target} is deprecated since version {self._version}, "
|
|
51
|
-
f"and will be removed in 1.
|
|
52
|
+
f"and will be removed in 1.11.0. Use mlrun.runtimes.mounts.{self._new_target} instead.",
|
|
52
53
|
FutureWarning,
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
# TODO: Remove in 1.
|
|
57
|
+
# TODO: Remove in 1.11.0
|
|
57
58
|
# For backwards compatibility
|
|
58
59
|
VolumeMount = _DeprecationHelper("VolumeMount")
|
|
59
60
|
auto_mount = _DeprecationHelper("auto_mount")
|
mlrun/projects/project.py
CHANGED
|
@@ -3939,8 +3939,8 @@ class MlrunProject(ModelObj):
|
|
|
3939
3939
|
:param start: The start time to filter by.Corresponding to the `created` field.
|
|
3940
3940
|
:param end: The end time to filter by. Corresponding to the `created` field.
|
|
3941
3941
|
:param top_level: If true will return only routers and endpoint that are NOT children of any router.
|
|
3942
|
-
:param mode: Specifies the mode of the model endpoint. Can be "real-time", "batch", or
|
|
3943
|
-
to None.
|
|
3942
|
+
:param mode: Specifies the mode of the model endpoint. Can be "real-time" (0), "batch" (1), or
|
|
3943
|
+
both if set to None.
|
|
3944
3944
|
:param uids: If passed will return a list `ModelEndpoint` object with uid in uids.
|
|
3945
3945
|
:param tsdb_metrics: When True, the time series metrics will be added to the output
|
|
3946
3946
|
of the resulting.
|
mlrun/serving/server.py
CHANGED
|
@@ -746,6 +746,26 @@ async def async_execute_graph(
|
|
|
746
746
|
return responses
|
|
747
747
|
|
|
748
748
|
|
|
749
|
+
def _is_inside_asyncio_loop():
|
|
750
|
+
try:
|
|
751
|
+
asyncio.get_running_loop()
|
|
752
|
+
return True
|
|
753
|
+
except RuntimeError:
|
|
754
|
+
return False
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
# Workaround for running with local=True in Jupyter (ML-10620)
|
|
758
|
+
def _workaround_asyncio_nesting():
|
|
759
|
+
try:
|
|
760
|
+
import nest_asyncio
|
|
761
|
+
except ImportError:
|
|
762
|
+
raise mlrun.errors.MLRunRuntimeError(
|
|
763
|
+
"Cannot execute graph from within an already running asyncio loop. "
|
|
764
|
+
"Attempt to import nest_asyncio as a workaround failed as well."
|
|
765
|
+
)
|
|
766
|
+
nest_asyncio.apply()
|
|
767
|
+
|
|
768
|
+
|
|
749
769
|
def execute_graph(
|
|
750
770
|
context: MLClientCtx,
|
|
751
771
|
data: DataItem,
|
|
@@ -771,6 +791,9 @@ def execute_graph(
|
|
|
771
791
|
|
|
772
792
|
:return: A list of responses.
|
|
773
793
|
"""
|
|
794
|
+
if _is_inside_asyncio_loop():
|
|
795
|
+
_workaround_asyncio_nesting()
|
|
796
|
+
|
|
774
797
|
return asyncio.run(
|
|
775
798
|
async_execute_graph(
|
|
776
799
|
context,
|
mlrun/serving/states.py
CHANGED
|
@@ -24,6 +24,7 @@ import inspect
|
|
|
24
24
|
import os
|
|
25
25
|
import pathlib
|
|
26
26
|
import traceback
|
|
27
|
+
import warnings
|
|
27
28
|
from abc import ABC
|
|
28
29
|
from copy import copy, deepcopy
|
|
29
30
|
from inspect import getfullargspec, signature
|
|
@@ -43,9 +44,13 @@ from mlrun.datastore.datastore_profile import (
|
|
|
43
44
|
DatastoreProfileV3io,
|
|
44
45
|
datastore_profile_read,
|
|
45
46
|
)
|
|
46
|
-
from mlrun.datastore.model_provider.model_provider import
|
|
47
|
+
from mlrun.datastore.model_provider.model_provider import (
|
|
48
|
+
InvokeResponseFormat,
|
|
49
|
+
ModelProvider,
|
|
50
|
+
UsageResponseKeys,
|
|
51
|
+
)
|
|
47
52
|
from mlrun.datastore.storeytargets import KafkaStoreyTarget, StreamStoreyTarget
|
|
48
|
-
from mlrun.utils import get_data_from_path, logger, split_path
|
|
53
|
+
from mlrun.utils import get_data_from_path, logger, set_data_by_path, split_path
|
|
49
54
|
|
|
50
55
|
from ..config import config
|
|
51
56
|
from ..datastore import _DummyStream, get_stream_pusher
|
|
@@ -1206,10 +1211,15 @@ class Model(storey.ParallelExecutionRunnable, ModelObj):
|
|
|
1206
1211
|
|
|
1207
1212
|
class LLModel(Model):
|
|
1208
1213
|
def __init__(
|
|
1209
|
-
self,
|
|
1214
|
+
self,
|
|
1215
|
+
name: str,
|
|
1216
|
+
input_path: Optional[Union[str, list[str]]] = None,
|
|
1217
|
+
result_path: Optional[Union[str, list[str]]] = None,
|
|
1218
|
+
**kwargs,
|
|
1210
1219
|
):
|
|
1211
1220
|
super().__init__(name, **kwargs)
|
|
1212
1221
|
self._input_path = split_path(input_path)
|
|
1222
|
+
self._result_path = split_path(result_path)
|
|
1213
1223
|
|
|
1214
1224
|
def predict(
|
|
1215
1225
|
self,
|
|
@@ -1221,11 +1231,14 @@ class LLModel(Model):
|
|
|
1221
1231
|
if isinstance(
|
|
1222
1232
|
self.invocation_artifact, mlrun.artifacts.LLMPromptArtifact
|
|
1223
1233
|
) and isinstance(self.model_provider, ModelProvider):
|
|
1224
|
-
|
|
1234
|
+
response_with_stats = self.model_provider.invoke(
|
|
1225
1235
|
messages=messages,
|
|
1226
|
-
|
|
1236
|
+
invoke_response_format=InvokeResponseFormat.USAGE,
|
|
1227
1237
|
**(model_configuration or {}),
|
|
1228
1238
|
)
|
|
1239
|
+
set_data_by_path(
|
|
1240
|
+
path=self._result_path, data=body, value=response_with_stats
|
|
1241
|
+
)
|
|
1229
1242
|
return body
|
|
1230
1243
|
|
|
1231
1244
|
async def predict_async(
|
|
@@ -1238,11 +1251,14 @@ class LLModel(Model):
|
|
|
1238
1251
|
if isinstance(
|
|
1239
1252
|
self.invocation_artifact, mlrun.artifacts.LLMPromptArtifact
|
|
1240
1253
|
) and isinstance(self.model_provider, ModelProvider):
|
|
1241
|
-
|
|
1254
|
+
response_with_stats = await self.model_provider.async_invoke(
|
|
1242
1255
|
messages=messages,
|
|
1243
|
-
|
|
1256
|
+
invoke_response_format=InvokeResponseFormat.USAGE,
|
|
1244
1257
|
**(model_configuration or {}),
|
|
1245
1258
|
)
|
|
1259
|
+
set_data_by_path(
|
|
1260
|
+
path=self._result_path, data=body, value=response_with_stats
|
|
1261
|
+
)
|
|
1246
1262
|
return body
|
|
1247
1263
|
|
|
1248
1264
|
def run(self, body: Any, path: str, origin_name: Optional[str] = None) -> Any:
|
|
@@ -1609,6 +1625,9 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1609
1625
|
:param outputs: list of the model outputs (e.g. labels) ,if provided will override the outputs
|
|
1610
1626
|
that been configured in the model artifact, please note that those outputs need to
|
|
1611
1627
|
be equal to the model_class predict method outputs (length, and order)
|
|
1628
|
+
|
|
1629
|
+
When using LLModel, the output will be overridden with UsageResponseKeys.fields().
|
|
1630
|
+
|
|
1612
1631
|
:param input_path: when specified selects the key/path in the event to use as model monitoring inputs
|
|
1613
1632
|
this require that the event body will behave like a dict, expects scopes to be
|
|
1614
1633
|
defined by dot notation (e.g "data.d").
|
|
@@ -1637,7 +1656,14 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1637
1656
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
1638
1657
|
"Cannot provide a model object as argument to `model_class` and also provide `model_parameters`."
|
|
1639
1658
|
)
|
|
1640
|
-
|
|
1659
|
+
if type(model_class) is LLModel or (
|
|
1660
|
+
isinstance(model_class, str) and model_class == LLModel.__name__
|
|
1661
|
+
):
|
|
1662
|
+
if outputs:
|
|
1663
|
+
warnings.warn(
|
|
1664
|
+
"LLModel with existing outputs detected, overriding to default"
|
|
1665
|
+
)
|
|
1666
|
+
outputs = UsageResponseKeys.fields()
|
|
1641
1667
|
model_parameters = model_parameters or (
|
|
1642
1668
|
model_class.to_dict() if isinstance(model_class, Model) else {}
|
|
1643
1669
|
)
|
|
@@ -1653,8 +1679,6 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1653
1679
|
except mlrun.errors.MLRunNotFoundError:
|
|
1654
1680
|
raise mlrun.errors.MLRunInvalidArgumentError("Artifact not found.")
|
|
1655
1681
|
|
|
1656
|
-
outputs = outputs or self._get_model_output_schema(model_artifact)
|
|
1657
|
-
|
|
1658
1682
|
model_artifact = (
|
|
1659
1683
|
model_artifact.uri
|
|
1660
1684
|
if isinstance(model_artifact, mlrun.artifacts.Artifact)
|
|
@@ -1720,28 +1744,13 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1720
1744
|
self.class_args[schemas.ModelRunnerStepData.MONITORING_DATA] = monitoring_data
|
|
1721
1745
|
|
|
1722
1746
|
@staticmethod
|
|
1723
|
-
def
|
|
1724
|
-
model_artifact: Union[ModelArtifact, LLMPromptArtifact],
|
|
1725
|
-
) -> Optional[list[str]]:
|
|
1726
|
-
if isinstance(
|
|
1727
|
-
model_artifact,
|
|
1728
|
-
ModelArtifact,
|
|
1729
|
-
):
|
|
1730
|
-
return [feature.name for feature in model_artifact.spec.outputs]
|
|
1731
|
-
elif isinstance(
|
|
1732
|
-
model_artifact,
|
|
1733
|
-
LLMPromptArtifact,
|
|
1734
|
-
):
|
|
1735
|
-
_model_artifact = model_artifact.model_artifact
|
|
1736
|
-
return [feature.name for feature in _model_artifact.spec.outputs]
|
|
1737
|
-
|
|
1738
|
-
@staticmethod
|
|
1739
|
-
def _get_model_endpoint_output_schema(
|
|
1747
|
+
def _get_model_endpoint_schema(
|
|
1740
1748
|
name: str,
|
|
1741
1749
|
project: str,
|
|
1742
1750
|
uid: str,
|
|
1743
|
-
) -> list[str]:
|
|
1751
|
+
) -> tuple[list[str], list[str]]:
|
|
1744
1752
|
output_schema = None
|
|
1753
|
+
input_schema = None
|
|
1745
1754
|
try:
|
|
1746
1755
|
model_endpoint: mlrun.common.schemas.model_monitoring.ModelEndpoint = (
|
|
1747
1756
|
mlrun.db.get_run_db().get_model_endpoint(
|
|
@@ -1752,6 +1761,7 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1752
1761
|
)
|
|
1753
1762
|
)
|
|
1754
1763
|
output_schema = model_endpoint.spec.label_names
|
|
1764
|
+
input_schema = model_endpoint.spec.feature_names
|
|
1755
1765
|
except (
|
|
1756
1766
|
mlrun.errors.MLRunNotFoundError,
|
|
1757
1767
|
mlrun.errors.MLRunInvalidArgumentError,
|
|
@@ -1760,7 +1770,7 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1760
1770
|
f"Model endpoint not found, using default output schema for model {name}",
|
|
1761
1771
|
error=f"{type(ex).__name__}: {ex}",
|
|
1762
1772
|
)
|
|
1763
|
-
return output_schema
|
|
1773
|
+
return input_schema, output_schema
|
|
1764
1774
|
|
|
1765
1775
|
def _calculate_monitoring_data(self) -> dict[str, dict[str, str]]:
|
|
1766
1776
|
monitoring_data = deepcopy(
|
|
@@ -1776,6 +1786,36 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1776
1786
|
monitoring_data[model][schemas.MonitoringData.RESULT_PATH] = split_path(
|
|
1777
1787
|
monitoring_data[model][schemas.MonitoringData.RESULT_PATH]
|
|
1778
1788
|
)
|
|
1789
|
+
|
|
1790
|
+
mep_output_schema, mep_input_schema = None, None
|
|
1791
|
+
|
|
1792
|
+
output_schema = self.class_args[
|
|
1793
|
+
mlrun.common.schemas.ModelRunnerStepData.MONITORING_DATA
|
|
1794
|
+
][model][schemas.MonitoringData.OUTPUTS]
|
|
1795
|
+
input_schema = self.class_args[
|
|
1796
|
+
mlrun.common.schemas.ModelRunnerStepData.MONITORING_DATA
|
|
1797
|
+
][model][schemas.MonitoringData.INPUTS]
|
|
1798
|
+
if not output_schema or not input_schema:
|
|
1799
|
+
# if output or input schema is not provided, try to get it from the model endpoint
|
|
1800
|
+
mep_input_schema, mep_output_schema = (
|
|
1801
|
+
self._get_model_endpoint_schema(
|
|
1802
|
+
model,
|
|
1803
|
+
self.context.project,
|
|
1804
|
+
monitoring_data[model].get(
|
|
1805
|
+
schemas.MonitoringData.MODEL_ENDPOINT_UID, ""
|
|
1806
|
+
),
|
|
1807
|
+
)
|
|
1808
|
+
)
|
|
1809
|
+
self.class_args[
|
|
1810
|
+
mlrun.common.schemas.ModelRunnerStepData.MONITORING_DATA
|
|
1811
|
+
][model][schemas.MonitoringData.OUTPUTS] = (
|
|
1812
|
+
output_schema or mep_output_schema
|
|
1813
|
+
)
|
|
1814
|
+
self.class_args[
|
|
1815
|
+
mlrun.common.schemas.ModelRunnerStepData.MONITORING_DATA
|
|
1816
|
+
][model][schemas.MonitoringData.INPUTS] = (
|
|
1817
|
+
input_schema or mep_input_schema
|
|
1818
|
+
)
|
|
1779
1819
|
return monitoring_data
|
|
1780
1820
|
else:
|
|
1781
1821
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
@@ -1803,6 +1843,13 @@ class ModelRunnerStep(MonitoredStep):
|
|
|
1803
1843
|
.get(model_params.get("name"), {})
|
|
1804
1844
|
.get(schemas.MonitoringData.INPUT_PATH)
|
|
1805
1845
|
)
|
|
1846
|
+
model_params[schemas.MonitoringData.RESULT_PATH] = (
|
|
1847
|
+
self.class_args.get(
|
|
1848
|
+
mlrun.common.schemas.ModelRunnerStepData.MONITORING_DATA, {}
|
|
1849
|
+
)
|
|
1850
|
+
.get(model_params.get("name"), {})
|
|
1851
|
+
.get(schemas.MonitoringData.RESULT_PATH)
|
|
1852
|
+
)
|
|
1806
1853
|
model = get_class(model, namespace).from_dict(
|
|
1807
1854
|
model_params, init_with_params=True
|
|
1808
1855
|
)
|
mlrun/serving/system_steps.py
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import random
|
|
16
|
+
from copy import copy
|
|
16
17
|
from datetime import timedelta
|
|
17
18
|
from typing import Any, Optional, Union
|
|
18
19
|
|
|
@@ -22,6 +23,7 @@ import storey
|
|
|
22
23
|
import mlrun
|
|
23
24
|
import mlrun.artifacts
|
|
24
25
|
import mlrun.common.schemas.model_monitoring as mm_schemas
|
|
26
|
+
import mlrun.feature_store
|
|
25
27
|
import mlrun.serving
|
|
26
28
|
from mlrun.common.schemas import MonitoringData
|
|
27
29
|
from mlrun.utils import get_data_from_path, logger
|
|
@@ -45,33 +47,20 @@ class MonitoringPreProcessor(storey.MapClass):
|
|
|
45
47
|
result_path = model_monitoring_data.get(MonitoringData.RESULT_PATH)
|
|
46
48
|
input_path = model_monitoring_data.get(MonitoringData.INPUT_PATH)
|
|
47
49
|
|
|
48
|
-
result = get_data_from_path(result_path, event.body.get(model, event.body))
|
|
49
50
|
output_schema = model_monitoring_data.get(MonitoringData.OUTPUTS)
|
|
50
51
|
input_schema = model_monitoring_data.get(MonitoringData.INPUTS)
|
|
51
|
-
logger.debug(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
logger.warn(
|
|
57
|
-
"Output schema was not provided using Project:log_model or by ModelRunnerStep:add_model order "
|
|
58
|
-
"may not preserved"
|
|
59
|
-
)
|
|
60
|
-
else:
|
|
61
|
-
outputs = result
|
|
52
|
+
logger.debug(
|
|
53
|
+
"output and input schema retrieved",
|
|
54
|
+
output_schema=output_schema,
|
|
55
|
+
input_schema=input_schema,
|
|
56
|
+
)
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
inputs
|
|
68
|
-
|
|
69
|
-
logger.warn(
|
|
70
|
-
"Input schema was not provided using by ModelRunnerStep:add_model, order "
|
|
71
|
-
"may not preserved"
|
|
72
|
-
)
|
|
73
|
-
else:
|
|
74
|
-
inputs = event_inputs
|
|
58
|
+
outputs, new_output_schema = self.get_listed_data(
|
|
59
|
+
event.body.get(model, event.body), result_path, output_schema
|
|
60
|
+
)
|
|
61
|
+
inputs, new_input_schema = self.get_listed_data(
|
|
62
|
+
event._metadata.get("inputs", {}), input_path, input_schema
|
|
63
|
+
)
|
|
75
64
|
|
|
76
65
|
if outputs and isinstance(outputs[0], list):
|
|
77
66
|
if output_schema and len(output_schema) != len(outputs[0]):
|
|
@@ -96,15 +85,43 @@ class MonitoringPreProcessor(storey.MapClass):
|
|
|
96
85
|
"outputs and inputs are not in the same length check 'input_path' and "
|
|
97
86
|
"'output_path' was specified if needed"
|
|
98
87
|
)
|
|
99
|
-
request = {
|
|
100
|
-
|
|
88
|
+
request = {
|
|
89
|
+
"inputs": inputs,
|
|
90
|
+
"id": getattr(event, "id", None),
|
|
91
|
+
"input_schema": new_input_schema,
|
|
92
|
+
}
|
|
93
|
+
resp = {"outputs": outputs, "output_schema": new_output_schema}
|
|
101
94
|
|
|
102
95
|
return request, resp
|
|
103
96
|
|
|
97
|
+
def get_listed_data(
|
|
98
|
+
self,
|
|
99
|
+
raw_data: dict,
|
|
100
|
+
data_path: Optional[Union[list[str], str]] = None,
|
|
101
|
+
schema: Optional[list[str]] = None,
|
|
102
|
+
):
|
|
103
|
+
"""Get data from a path and transpose it by keys if dict is provided."""
|
|
104
|
+
new_schema = None
|
|
105
|
+
data_from_path = get_data_from_path(data_path, raw_data)
|
|
106
|
+
if isinstance(data_from_path, dict):
|
|
107
|
+
# transpose by key the inputs:
|
|
108
|
+
listed_data, new_schema = self.transpose_by_key(data_from_path, schema)
|
|
109
|
+
new_schema = new_schema or schema
|
|
110
|
+
if not schema:
|
|
111
|
+
logger.warn(
|
|
112
|
+
f"No schema provided through add_model(); the order of {data_from_path} "
|
|
113
|
+
"may not be preserved."
|
|
114
|
+
)
|
|
115
|
+
elif not isinstance(data_from_path, list):
|
|
116
|
+
listed_data = [data_from_path]
|
|
117
|
+
else:
|
|
118
|
+
listed_data = data_from_path
|
|
119
|
+
return listed_data, new_schema
|
|
120
|
+
|
|
104
121
|
@staticmethod
|
|
105
122
|
def transpose_by_key(
|
|
106
123
|
data: dict, schema: Optional[Union[str, list[str]]] = None
|
|
107
|
-
) -> Union[list[Any], list[list[Any]]]:
|
|
124
|
+
) -> tuple[Union[list[Any], list[list[Any]]], list[str]]:
|
|
108
125
|
"""
|
|
109
126
|
Transpose values from a dictionary by keys.
|
|
110
127
|
|
|
@@ -136,20 +153,27 @@ class MonitoringPreProcessor(storey.MapClass):
|
|
|
136
153
|
* If result is a matrix, returns a list of lists.
|
|
137
154
|
|
|
138
155
|
:raises ValueError: If the values include a mix of scalars and lists, or if the list lengths do not match.
|
|
156
|
+
mlrun.MLRunInvalidArgumentError if the schema keys are not contained in the data keys.
|
|
139
157
|
"""
|
|
140
|
-
|
|
158
|
+
new_schema = None
|
|
159
|
+
# Normalize keys in data:
|
|
160
|
+
normalize_data = {
|
|
161
|
+
mlrun.feature_store.api.norm_column_name(k): copy(v)
|
|
162
|
+
for k, v in data.items()
|
|
163
|
+
}
|
|
141
164
|
# Normalize schema to list
|
|
142
165
|
if not schema:
|
|
143
|
-
keys = list(
|
|
166
|
+
keys = list(normalize_data.keys())
|
|
167
|
+
new_schema = keys
|
|
144
168
|
elif isinstance(schema, str):
|
|
145
|
-
keys = [schema]
|
|
169
|
+
keys = [mlrun.feature_store.api.norm_column_name(schema)]
|
|
146
170
|
else:
|
|
147
|
-
keys = schema
|
|
171
|
+
keys = [mlrun.feature_store.api.norm_column_name(key) for key in schema]
|
|
148
172
|
|
|
149
|
-
values = [
|
|
173
|
+
values = [normalize_data[key] for key in keys if key in normalize_data]
|
|
150
174
|
if len(values) != len(keys):
|
|
151
175
|
raise mlrun.MLRunInvalidArgumentError(
|
|
152
|
-
f"Schema keys {keys}
|
|
176
|
+
f"Schema keys {keys} are not contained in the data keys {list(data.keys())}."
|
|
153
177
|
)
|
|
154
178
|
|
|
155
179
|
# Detect if all are scalars ie: int,float,str
|
|
@@ -168,12 +192,12 @@ class MonitoringPreProcessor(storey.MapClass):
|
|
|
168
192
|
mat = np.stack(arrays, axis=0)
|
|
169
193
|
transposed = mat.T
|
|
170
194
|
else:
|
|
171
|
-
return values[0]
|
|
195
|
+
return values[0], new_schema
|
|
172
196
|
|
|
173
197
|
if transposed.shape[1] == 1 and transposed.shape[0] == 1:
|
|
174
198
|
# Transform [[0]] -> [0]:
|
|
175
|
-
return transposed[:, 0].tolist()
|
|
176
|
-
return transposed.tolist()
|
|
199
|
+
return transposed[:, 0].tolist(), new_schema
|
|
200
|
+
return transposed.tolist(), new_schema
|
|
177
201
|
|
|
178
202
|
def do(self, event):
|
|
179
203
|
monitoring_event_list = []
|
mlrun/utils/helpers.py
CHANGED
|
@@ -464,17 +464,11 @@ def to_date_str(d):
|
|
|
464
464
|
return ""
|
|
465
465
|
|
|
466
466
|
|
|
467
|
-
def normalize_name(name: str
|
|
467
|
+
def normalize_name(name: str):
|
|
468
468
|
# TODO: Must match
|
|
469
469
|
# [a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?
|
|
470
470
|
name = re.sub(r"\s+", "-", name)
|
|
471
471
|
if "_" in name:
|
|
472
|
-
if verbose:
|
|
473
|
-
warnings.warn(
|
|
474
|
-
"Names with underscore '_' are about to be deprecated, use dashes '-' instead. "
|
|
475
|
-
f"Replacing '{name}' underscores with dashes.",
|
|
476
|
-
FutureWarning,
|
|
477
|
-
)
|
|
478
472
|
name = name.replace("_", "-")
|
|
479
473
|
return name.lower()
|
|
480
474
|
|
|
@@ -835,7 +829,7 @@ def extend_hub_uri_if_needed(uri) -> tuple[str, bool]:
|
|
|
835
829
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
836
830
|
"Invalid character '/' in function name or source name"
|
|
837
831
|
) from exc
|
|
838
|
-
name = normalize_name(name=name
|
|
832
|
+
name = normalize_name(name=name)
|
|
839
833
|
if not source_name:
|
|
840
834
|
# Searching item in all sources
|
|
841
835
|
sources = db.list_hub_sources(item_name=name, tag=tag)
|
|
@@ -2409,9 +2403,7 @@ def split_path(path: str) -> typing.Union[str, list[str], None]:
|
|
|
2409
2403
|
return path
|
|
2410
2404
|
|
|
2411
2405
|
|
|
2412
|
-
def get_data_from_path(
|
|
2413
|
-
path: typing.Union[str, list[str], None], data: dict
|
|
2414
|
-
) -> dict[str, Any]:
|
|
2406
|
+
def get_data_from_path(path: typing.Union[str, list[str], None], data: dict) -> Any:
|
|
2415
2407
|
if isinstance(path, str):
|
|
2416
2408
|
output_data = data.get(path)
|
|
2417
2409
|
elif isinstance(path, list):
|
|
@@ -2424,8 +2416,6 @@ def get_data_from_path(
|
|
|
2424
2416
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
2425
2417
|
"Expected path be of type str or list of str or None"
|
|
2426
2418
|
)
|
|
2427
|
-
if isinstance(output_data, (int, float)):
|
|
2428
|
-
output_data = [output_data]
|
|
2429
2419
|
return output_data
|
|
2430
2420
|
|
|
2431
2421
|
|
|
@@ -2437,3 +2427,27 @@ def is_valid_port(port: int, raise_on_error: bool = False) -> bool:
|
|
|
2437
2427
|
if raise_on_error:
|
|
2438
2428
|
raise ValueError("Port must be in the range 0–65535")
|
|
2439
2429
|
return False
|
|
2430
|
+
|
|
2431
|
+
|
|
2432
|
+
def set_data_by_path(
|
|
2433
|
+
path: typing.Union[str, list[str], None], data: dict, value
|
|
2434
|
+
) -> None:
|
|
2435
|
+
if path is None:
|
|
2436
|
+
if not isinstance(value, dict):
|
|
2437
|
+
raise ValueError("When path is None, value must be a dictionary.")
|
|
2438
|
+
data.update(value)
|
|
2439
|
+
|
|
2440
|
+
elif isinstance(path, str):
|
|
2441
|
+
data[path] = value
|
|
2442
|
+
|
|
2443
|
+
elif isinstance(path, list):
|
|
2444
|
+
current = data
|
|
2445
|
+
for key in path[:-1]:
|
|
2446
|
+
if key not in current or not isinstance(current[key], dict):
|
|
2447
|
+
current[key] = {}
|
|
2448
|
+
current = current[key]
|
|
2449
|
+
current[path[-1]] = value
|
|
2450
|
+
else:
|
|
2451
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
2452
|
+
"Expected path to be of type str or list of str"
|
|
2453
|
+
)
|
|
@@ -308,7 +308,7 @@ class NotificationPusher(_NotificationPusherBase):
|
|
|
308
308
|
and retry_count >= max_retries
|
|
309
309
|
):
|
|
310
310
|
message += (
|
|
311
|
-
"\nRetry limit reached
|
|
311
|
+
"\nRetry limit reached - run has failed after all retry attempts."
|
|
312
312
|
)
|
|
313
313
|
|
|
314
314
|
severity = (
|
mlrun/utils/version/version.json
CHANGED