mlrun 1.8.0rc45__py3-none-any.whl → 1.8.0rc47__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/alerts/alert.py +1 -1
- mlrun/common/schemas/model_monitoring/constants.py +5 -0
- mlrun/config.py +2 -0
- mlrun/data_types/__init__.py +5 -1
- mlrun/datastore/targets.py +7 -5
- mlrun/model_monitoring/api.py +31 -18
- mlrun/model_monitoring/applications/context.py +14 -1
- mlrun/model_monitoring/applications/evidently/base.py +38 -0
- mlrun/model_monitoring/controller.py +208 -84
- mlrun/model_monitoring/db/_schedules.py +110 -32
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +6 -1
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +11 -5
- mlrun/model_monitoring/helpers.py +46 -53
- mlrun/projects/project.py +29 -24
- mlrun/runtimes/function_reference.py +3 -0
- mlrun/runtimes/nuclio/function.py +48 -0
- mlrun/runtimes/nuclio/serving.py +16 -1
- mlrun/serving/states.py +48 -27
- mlrun/serving/v2_serving.py +51 -1
- mlrun/utils/helpers.py +5 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.8.0rc45.dist-info → mlrun-1.8.0rc47.dist-info}/METADATA +5 -5
- {mlrun-1.8.0rc45.dist-info → mlrun-1.8.0rc47.dist-info}/RECORD +27 -27
- {mlrun-1.8.0rc45.dist-info → mlrun-1.8.0rc47.dist-info}/WHEEL +1 -1
- {mlrun-1.8.0rc45.dist-info → mlrun-1.8.0rc47.dist-info}/entry_points.txt +0 -0
- {mlrun-1.8.0rc45.dist-info → mlrun-1.8.0rc47.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.8.0rc45.dist-info → mlrun-1.8.0rc47.dist-info}/top_level.txt +0 -0
mlrun/alerts/alert.py
CHANGED
|
@@ -112,7 +112,7 @@ class AlertConfig(ModelObj):
|
|
|
112
112
|
complex trigger which is based on a prometheus alert
|
|
113
113
|
:param criteria: When the alert will be triggered based on the specified number of events within the
|
|
114
114
|
defined time period.
|
|
115
|
-
:param reset_policy: When to clear the alert.
|
|
115
|
+
:param reset_policy: When to clear the alert. Either "manual" for manual reset of the alert, or
|
|
116
116
|
"auto" if the criteria contains a time period
|
|
117
117
|
:param notifications: List of notifications to invoke once the alert is triggered
|
|
118
118
|
:param entities: Entities that the event relates to. The entity object will contain fields that
|
|
@@ -289,6 +289,11 @@ class ModelMonitoringMode(StrEnum):
|
|
|
289
289
|
disabled = "disabled"
|
|
290
290
|
|
|
291
291
|
|
|
292
|
+
class ScheduleChiefFields(StrEnum):
|
|
293
|
+
LAST_REQUEST = "last_request"
|
|
294
|
+
LAST_ANALYZED = "last_analyzed"
|
|
295
|
+
|
|
296
|
+
|
|
292
297
|
class EndpointType(IntEnum):
|
|
293
298
|
NODE_EP = 1 # end point that is not a child of a router
|
|
294
299
|
ROUTER = 2 # endpoint that is router
|
mlrun/config.py
CHANGED
|
@@ -631,6 +631,8 @@ default_config = {
|
|
|
631
631
|
"parquet_batching_max_events": 10_000,
|
|
632
632
|
"parquet_batching_timeout_secs": timedelta(minutes=1).total_seconds(),
|
|
633
633
|
"tdengine": {
|
|
634
|
+
"run_directly": True,
|
|
635
|
+
# timeout and retry are ignored when run_directly is set to True
|
|
634
636
|
"timeout": 10,
|
|
635
637
|
"retries": 1,
|
|
636
638
|
},
|
mlrun/data_types/__init__.py
CHANGED
|
@@ -27,8 +27,12 @@ class BaseDataInfer:
|
|
|
27
27
|
get_stats = None
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def is_spark_dataframe(df) -> bool:
|
|
31
|
+
return "rdd" in dir(df)
|
|
32
|
+
|
|
33
|
+
|
|
30
34
|
def get_infer_interface(df) -> BaseDataInfer:
|
|
31
|
-
if
|
|
35
|
+
if is_spark_dataframe(df):
|
|
32
36
|
from .spark import SparkDataInfer
|
|
33
37
|
|
|
34
38
|
return SparkDataInfer
|
mlrun/datastore/targets.py
CHANGED
|
@@ -40,7 +40,7 @@ from mlrun.utils.helpers import to_parquet
|
|
|
40
40
|
from mlrun.utils.v3io_clients import get_frames_client
|
|
41
41
|
|
|
42
42
|
from .. import errors
|
|
43
|
-
from ..data_types import ValueType
|
|
43
|
+
from ..data_types import ValueType, is_spark_dataframe
|
|
44
44
|
from ..platforms.iguazio import parse_path, split_path
|
|
45
45
|
from .datastore_profile import datastore_profile_read
|
|
46
46
|
from .spark_utils import spark_session_update_hadoop_options
|
|
@@ -86,8 +86,10 @@ def generate_target_run_id():
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def write_spark_dataframe_with_options(spark_options, df, mode, write_format=None):
|
|
89
|
+
# TODO: Replace with just df.sparkSession when Spark 3.2 support is dropped
|
|
90
|
+
spark_session = getattr(df, "sparkSession", None) or df.sql_ctx.sparkSession
|
|
89
91
|
non_hadoop_spark_options = spark_session_update_hadoop_options(
|
|
90
|
-
|
|
92
|
+
spark_session, spark_options
|
|
91
93
|
)
|
|
92
94
|
if write_format:
|
|
93
95
|
df.write.format(write_format).mode(mode).save(**non_hadoop_spark_options)
|
|
@@ -510,7 +512,7 @@ class BaseStoreTarget(DataTargetBase):
|
|
|
510
512
|
chunk_id=0,
|
|
511
513
|
**kwargs,
|
|
512
514
|
) -> Optional[int]:
|
|
513
|
-
if
|
|
515
|
+
if is_spark_dataframe(df):
|
|
514
516
|
options = self.get_spark_options(key_column, timestamp_key)
|
|
515
517
|
options.update(kwargs)
|
|
516
518
|
df = self.prepare_spark_df(df, key_column, timestamp_key, options)
|
|
@@ -1376,7 +1378,7 @@ class NoSqlBaseTarget(BaseStoreTarget):
|
|
|
1376
1378
|
def write_dataframe(
|
|
1377
1379
|
self, df, key_column=None, timestamp_key=None, chunk_id=0, **kwargs
|
|
1378
1380
|
):
|
|
1379
|
-
if
|
|
1381
|
+
if is_spark_dataframe(df):
|
|
1380
1382
|
options = self.get_spark_options(key_column, timestamp_key)
|
|
1381
1383
|
options.update(kwargs)
|
|
1382
1384
|
df = self.prepare_spark_df(df)
|
|
@@ -2108,7 +2110,7 @@ class SQLTarget(BaseStoreTarget):
|
|
|
2108
2110
|
|
|
2109
2111
|
self._create_sql_table()
|
|
2110
2112
|
|
|
2111
|
-
if
|
|
2113
|
+
if is_spark_dataframe(df):
|
|
2112
2114
|
raise ValueError("Spark is not supported")
|
|
2113
2115
|
else:
|
|
2114
2116
|
(
|
mlrun/model_monitoring/api.py
CHANGED
|
@@ -50,8 +50,8 @@ DatasetType = typing.Union[
|
|
|
50
50
|
|
|
51
51
|
def get_or_create_model_endpoint(
|
|
52
52
|
project: str,
|
|
53
|
+
model_endpoint_name: str,
|
|
53
54
|
model_path: str = "",
|
|
54
|
-
model_endpoint_name: str = "",
|
|
55
55
|
endpoint_id: str = "",
|
|
56
56
|
function_name: str = "",
|
|
57
57
|
function_tag: str = "latest",
|
|
@@ -59,6 +59,7 @@ def get_or_create_model_endpoint(
|
|
|
59
59
|
sample_set_statistics: typing.Optional[dict[str, typing.Any]] = None,
|
|
60
60
|
monitoring_mode: mm_constants.ModelMonitoringMode = mm_constants.ModelMonitoringMode.enabled,
|
|
61
61
|
db_session=None,
|
|
62
|
+
feature_analysis: bool = False,
|
|
62
63
|
) -> ModelEndpoint:
|
|
63
64
|
"""
|
|
64
65
|
Get a single model endpoint object. If not exist, generate a new model endpoint with the provided parameters. Note
|
|
@@ -66,9 +67,9 @@ def get_or_create_model_endpoint(
|
|
|
66
67
|
features, set `monitoring_mode=enabled`.
|
|
67
68
|
|
|
68
69
|
:param project: Project name.
|
|
69
|
-
:param model_path: The model store path (applicable only to new endpoint_id).
|
|
70
70
|
:param model_endpoint_name: If a new model endpoint is created, the model endpoint name will be presented
|
|
71
71
|
under this endpoint (applicable only to new endpoint_id).
|
|
72
|
+
:param model_path: The model store path (applicable only to new endpoint_id).
|
|
72
73
|
:param endpoint_id: Model endpoint unique ID. If not exist in DB, will generate a new record based
|
|
73
74
|
on the provided `endpoint_id`.
|
|
74
75
|
:param function_name: If a new model endpoint is created, use this function name.
|
|
@@ -80,6 +81,7 @@ def get_or_create_model_endpoint(
|
|
|
80
81
|
:param monitoring_mode: If enabled, apply model monitoring features on the provided endpoint id
|
|
81
82
|
(applicable only to new endpoint_id).
|
|
82
83
|
:param db_session: A runtime session that manages the current dialog with the database.
|
|
84
|
+
:param feature_analysis: If True, the model endpoint will be retrieved with the feature analysis mode.
|
|
83
85
|
|
|
84
86
|
:return: A ModelEndpoint object
|
|
85
87
|
"""
|
|
@@ -99,6 +101,7 @@ def get_or_create_model_endpoint(
|
|
|
99
101
|
endpoint_id=endpoint_id,
|
|
100
102
|
function_name=function_name,
|
|
101
103
|
function_tag=function_tag or "latest",
|
|
104
|
+
feature_analysis=feature_analysis,
|
|
102
105
|
)
|
|
103
106
|
# If other fields provided, validate that they are correspond to the existing model endpoint data
|
|
104
107
|
_model_endpoint_validations(
|
|
@@ -157,7 +160,8 @@ def record_results(
|
|
|
157
160
|
:param context: MLRun context. Note that the context is required generating the model endpoint.
|
|
158
161
|
:param infer_results_df: DataFrame that will be stored under the model endpoint parquet target. Will be
|
|
159
162
|
used for doing the drift analysis. Please make sure that the dataframe includes
|
|
160
|
-
both feature names and label columns.
|
|
163
|
+
both feature names and label columns. If you are recording results for existing
|
|
164
|
+
model endpoint, the endpoint should be a batch endpoint.
|
|
161
165
|
:param sample_set_statistics: Dictionary of sample set statistics that will be used as a reference data for
|
|
162
166
|
the current model endpoint.
|
|
163
167
|
:param monitoring_mode: If enabled, apply model monitoring features on the provided endpoint id. Enabled
|
|
@@ -218,23 +222,32 @@ def record_results(
|
|
|
218
222
|
)
|
|
219
223
|
logger.debug("Model endpoint", endpoint=model_endpoint)
|
|
220
224
|
|
|
221
|
-
timestamp = datetime_now()
|
|
222
225
|
if infer_results_df is not None:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
if (
|
|
227
|
+
model_endpoint.metadata.endpoint_type
|
|
228
|
+
!= mlrun.common.schemas.model_monitoring.EndpointType.BATCH_EP
|
|
229
|
+
):
|
|
230
|
+
logger.warning(
|
|
231
|
+
"Inference results can be recorded only for batch endpoints. "
|
|
232
|
+
"Therefore the current results won't be monitored."
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
timestamp = datetime_now()
|
|
236
|
+
# Write the monitoring parquet to the relevant model endpoint context
|
|
237
|
+
write_monitoring_df(
|
|
238
|
+
feature_set_uri=model_endpoint.spec.monitoring_feature_set_uri,
|
|
239
|
+
infer_datetime=timestamp,
|
|
240
|
+
endpoint_id=model_endpoint.metadata.uid,
|
|
241
|
+
infer_results_df=infer_results_df,
|
|
242
|
+
)
|
|
230
243
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
# Update the last request time
|
|
245
|
+
update_model_endpoint_last_request(
|
|
246
|
+
project=project,
|
|
247
|
+
model_endpoint=model_endpoint,
|
|
248
|
+
current_request=timestamp,
|
|
249
|
+
db=db,
|
|
250
|
+
)
|
|
238
251
|
|
|
239
252
|
return model_endpoint
|
|
240
253
|
|
|
@@ -76,7 +76,6 @@ class MonitoringApplicationContext:
|
|
|
76
76
|
:param sample_df: (pd.DataFrame) The new sample DataFrame.
|
|
77
77
|
:param start_infer_time: (pd.Timestamp) Start time of the monitoring schedule.
|
|
78
78
|
:param end_infer_time: (pd.Timestamp) End time of the monitoring schedule.
|
|
79
|
-
:param latest_request: (pd.Timestamp) Timestamp of the latest request on this endpoint_id.
|
|
80
79
|
:param endpoint_id: (str) ID of the monitored model endpoint
|
|
81
80
|
:param feature_set: (FeatureSet) the model endpoint feature set
|
|
82
81
|
:param endpoint_name: (str) Name of the monitored model endpoint
|
|
@@ -208,6 +207,20 @@ class MonitoringApplicationContext:
|
|
|
208
207
|
@property
|
|
209
208
|
def sample_df(self) -> pd.DataFrame:
|
|
210
209
|
if self._sample_df is None:
|
|
210
|
+
if (
|
|
211
|
+
self.endpoint_name is None
|
|
212
|
+
or self.endpoint_id is None
|
|
213
|
+
or pd.isnull(self.start_infer_time)
|
|
214
|
+
or pd.isnull(self.end_infer_time)
|
|
215
|
+
):
|
|
216
|
+
raise mlrun.errors.MLRunValueError(
|
|
217
|
+
"You have tried to access `monitoring_context.sample_df`, but have not provided it directly "
|
|
218
|
+
"through `sample_data`, nor have you provided the model endpoint's name, ID, and the start and "
|
|
219
|
+
f"end times: `endpoint_name`={self.endpoint_name}, `endpoint_uid`={self.endpoint_id}, "
|
|
220
|
+
f"`start`={self.start_infer_time}, and `end`={self.end_infer_time}. "
|
|
221
|
+
"You can either provide the sample dataframe directly, the model endpoint's details and times, "
|
|
222
|
+
"or adapt the application's logic to not access the sample dataframe."
|
|
223
|
+
)
|
|
211
224
|
feature_set = self.feature_set
|
|
212
225
|
features = [f"{feature_set.metadata.name}.*"]
|
|
213
226
|
vector = fstore.FeatureVector(
|
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import json
|
|
16
|
+
import posixpath
|
|
15
17
|
import uuid
|
|
16
18
|
import warnings
|
|
17
19
|
from abc import ABC
|
|
18
20
|
|
|
19
21
|
import pandas as pd
|
|
20
22
|
import semver
|
|
23
|
+
from evidently.ui.storage.local.base import METADATA_PATH, FSLocation
|
|
21
24
|
|
|
22
25
|
import mlrun.model_monitoring.applications.base as mm_base
|
|
23
26
|
import mlrun.model_monitoring.applications.context as mm_context
|
|
@@ -81,12 +84,47 @@ class EvidentlyModelMonitoringApplicationBase(
|
|
|
81
84
|
# TODO : more then one project (mep -> project)
|
|
82
85
|
if not _HAS_EVIDENTLY:
|
|
83
86
|
raise ModuleNotFoundError("Evidently is not installed - the app cannot run")
|
|
87
|
+
self._log_location(evidently_workspace_path)
|
|
84
88
|
self.evidently_workspace = Workspace.create(evidently_workspace_path)
|
|
85
89
|
self.evidently_project_id = evidently_project_id
|
|
86
90
|
self.evidently_project = self.evidently_workspace.get_project(
|
|
87
91
|
evidently_project_id
|
|
88
92
|
)
|
|
89
93
|
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _log_location(evidently_workspace_path):
|
|
96
|
+
# TODO remove function + usage after solving issue ML-9530
|
|
97
|
+
location = FSLocation(base_path=evidently_workspace_path)
|
|
98
|
+
location.invalidate_cache("")
|
|
99
|
+
paths = [p for p in location.listdir("") if location.isdir(p)]
|
|
100
|
+
|
|
101
|
+
for path in paths:
|
|
102
|
+
metadata_path = posixpath.join(path, METADATA_PATH)
|
|
103
|
+
full_path = posixpath.join(location.path, metadata_path)
|
|
104
|
+
print(f"evidently json issue, working on path: {full_path}")
|
|
105
|
+
try:
|
|
106
|
+
with location.open(metadata_path) as f:
|
|
107
|
+
content = json.load(f)
|
|
108
|
+
print(
|
|
109
|
+
f"evidently json issue, successful load path: {full_path}, content: {content}"
|
|
110
|
+
)
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
print(f"evidently json issue, path not found: {full_path}")
|
|
113
|
+
continue
|
|
114
|
+
except json.decoder.JSONDecodeError as json_error:
|
|
115
|
+
print(
|
|
116
|
+
f"evidently json issue, path got json error, path:{full_path}, error: {json_error}"
|
|
117
|
+
)
|
|
118
|
+
print("evidently json issue, file content:")
|
|
119
|
+
with location.open(metadata_path) as f:
|
|
120
|
+
print(f.read())
|
|
121
|
+
continue
|
|
122
|
+
except Exception as error:
|
|
123
|
+
print(
|
|
124
|
+
f"evidently json issue, path got general error, path:{full_path}, error: {error}"
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
127
|
+
|
|
90
128
|
@staticmethod
|
|
91
129
|
def log_evidently_object(
|
|
92
130
|
monitoring_context: mm_context.MonitoringApplicationContext,
|