mlrun 1.7.0rc14__py3-none-any.whl → 1.7.0rc16__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/__init__.py +10 -1
- mlrun/__main__.py +18 -109
- mlrun/{runtimes/mpijob/v1alpha1.py → alerts/__init__.py} +2 -16
- mlrun/alerts/alert.py +141 -0
- mlrun/artifacts/__init__.py +8 -3
- mlrun/artifacts/base.py +36 -253
- mlrun/artifacts/dataset.py +9 -190
- mlrun/artifacts/manager.py +20 -41
- mlrun/artifacts/model.py +8 -140
- mlrun/artifacts/plots.py +14 -375
- mlrun/common/schemas/__init__.py +4 -2
- mlrun/common/schemas/alert.py +46 -4
- mlrun/common/schemas/api_gateway.py +4 -0
- mlrun/common/schemas/artifact.py +15 -0
- mlrun/common/schemas/auth.py +2 -0
- mlrun/common/schemas/model_monitoring/__init__.py +8 -1
- mlrun/common/schemas/model_monitoring/constants.py +40 -4
- mlrun/common/schemas/model_monitoring/model_endpoints.py +73 -2
- mlrun/common/schemas/project.py +2 -0
- mlrun/config.py +7 -4
- mlrun/data_types/to_pandas.py +4 -4
- mlrun/datastore/base.py +41 -9
- mlrun/datastore/datastore_profile.py +54 -4
- mlrun/datastore/inmem.py +2 -2
- mlrun/datastore/sources.py +43 -2
- mlrun/datastore/store_resources.py +2 -6
- mlrun/datastore/targets.py +106 -39
- mlrun/db/base.py +23 -3
- mlrun/db/httpdb.py +101 -47
- mlrun/db/nopdb.py +20 -2
- mlrun/errors.py +5 -0
- mlrun/feature_store/__init__.py +0 -2
- mlrun/feature_store/api.py +12 -47
- mlrun/feature_store/feature_set.py +9 -0
- mlrun/feature_store/retrieval/base.py +9 -4
- mlrun/feature_store/retrieval/conversion.py +4 -4
- mlrun/feature_store/retrieval/dask_merger.py +2 -0
- mlrun/feature_store/retrieval/job.py +2 -0
- mlrun/feature_store/retrieval/local_merger.py +2 -0
- mlrun/feature_store/retrieval/spark_merger.py +5 -0
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +5 -10
- mlrun/launcher/base.py +4 -3
- mlrun/launcher/client.py +1 -1
- mlrun/lists.py +4 -2
- mlrun/model.py +25 -11
- mlrun/model_monitoring/__init__.py +1 -1
- mlrun/model_monitoring/api.py +41 -18
- mlrun/model_monitoring/application.py +5 -305
- mlrun/model_monitoring/applications/__init__.py +11 -0
- mlrun/model_monitoring/applications/_application_steps.py +157 -0
- mlrun/model_monitoring/applications/base.py +282 -0
- mlrun/model_monitoring/applications/context.py +214 -0
- mlrun/model_monitoring/applications/evidently_base.py +211 -0
- mlrun/model_monitoring/applications/histogram_data_drift.py +132 -91
- mlrun/model_monitoring/applications/results.py +99 -0
- mlrun/model_monitoring/controller.py +3 -1
- mlrun/model_monitoring/db/__init__.py +2 -0
- mlrun/model_monitoring/db/stores/base/store.py +9 -36
- mlrun/model_monitoring/db/stores/sqldb/models/base.py +7 -6
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +63 -110
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +104 -187
- mlrun/model_monitoring/db/tsdb/__init__.py +71 -0
- mlrun/model_monitoring/db/tsdb/base.py +135 -0
- mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +117 -0
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +404 -0
- mlrun/model_monitoring/db/v3io_tsdb_reader.py +134 -0
- mlrun/model_monitoring/evidently_application.py +6 -118
- mlrun/model_monitoring/helpers.py +1 -1
- mlrun/model_monitoring/model_endpoint.py +3 -2
- mlrun/model_monitoring/stream_processing.py +48 -213
- mlrun/model_monitoring/writer.py +101 -121
- mlrun/platforms/__init__.py +10 -9
- mlrun/platforms/iguazio.py +21 -202
- mlrun/projects/operations.py +11 -7
- mlrun/projects/pipelines.py +13 -76
- mlrun/projects/project.py +73 -45
- mlrun/render.py +11 -13
- mlrun/run.py +6 -41
- mlrun/runtimes/__init__.py +3 -3
- mlrun/runtimes/base.py +6 -6
- mlrun/runtimes/funcdoc.py +0 -28
- mlrun/runtimes/kubejob.py +2 -1
- mlrun/runtimes/local.py +1 -1
- mlrun/runtimes/mpijob/__init__.py +0 -20
- mlrun/runtimes/mpijob/v1.py +1 -1
- mlrun/runtimes/nuclio/api_gateway.py +75 -9
- mlrun/runtimes/nuclio/function.py +9 -35
- mlrun/runtimes/pod.py +16 -36
- mlrun/runtimes/remotesparkjob.py +1 -1
- mlrun/runtimes/sparkjob/spark3job.py +1 -1
- mlrun/runtimes/utils.py +1 -39
- mlrun/utils/helpers.py +72 -71
- mlrun/utils/notifications/notification/base.py +1 -1
- mlrun/utils/notifications/notification/slack.py +12 -5
- mlrun/utils/notifications/notification/webhook.py +1 -1
- mlrun/utils/notifications/notification_pusher.py +134 -14
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc16.dist-info}/METADATA +4 -3
- {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc16.dist-info}/RECORD +105 -95
- mlrun/kfpops.py +0 -865
- mlrun/platforms/other.py +0 -305
- /mlrun/{runtimes → common/runtimes}/constants.py +0 -0
- {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc16.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc16.dist-info}/WHEEL +0 -0
- {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc16.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc16.dist-info}/top_level.txt +0 -0
mlrun/model_monitoring/writer.py
CHANGED
|
@@ -12,25 +12,21 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
import datetime
|
|
16
15
|
import json
|
|
17
16
|
from typing import Any, NewType
|
|
18
17
|
|
|
19
|
-
import pandas as pd
|
|
20
|
-
from v3io.dataplane import Client as V3IOClient
|
|
21
|
-
from v3io_frames.client import ClientBase as V3IOFramesClient
|
|
22
|
-
from v3io_frames.errors import Error as V3IOFramesError
|
|
23
|
-
from v3io_frames.frames_pb2 import IGNORE
|
|
24
|
-
|
|
25
18
|
import mlrun.common.model_monitoring
|
|
19
|
+
import mlrun.common.schemas
|
|
26
20
|
import mlrun.common.schemas.alert as alert_constants
|
|
27
21
|
import mlrun.model_monitoring
|
|
28
|
-
import mlrun.model_monitoring.db.stores
|
|
29
|
-
import mlrun.utils.v3io_clients
|
|
30
22
|
from mlrun.common.schemas.model_monitoring.constants import (
|
|
31
23
|
EventFieldType,
|
|
24
|
+
HistogramDataDriftApplicationConstants,
|
|
25
|
+
MetricData,
|
|
26
|
+
ResultData,
|
|
32
27
|
ResultStatusApp,
|
|
33
28
|
WriterEvent,
|
|
29
|
+
WriterEventKind,
|
|
34
30
|
)
|
|
35
31
|
from mlrun.common.schemas.notification import NotificationKind, NotificationSeverity
|
|
36
32
|
from mlrun.model_monitoring.helpers import get_endpoint_record
|
|
@@ -38,9 +34,6 @@ from mlrun.serving.utils import StepToDict
|
|
|
38
34
|
from mlrun.utils import logger
|
|
39
35
|
from mlrun.utils.notifications.notification_pusher import CustomNotificationPusher
|
|
40
36
|
|
|
41
|
-
_TSDB_BE = "tsdb"
|
|
42
|
-
_TSDB_RATE = "1/s"
|
|
43
|
-
_TSDB_TABLE = "app-results"
|
|
44
37
|
_RawEvent = dict[str, Any]
|
|
45
38
|
_AppResultEvent = NewType("_AppResultEvent", _RawEvent)
|
|
46
39
|
|
|
@@ -75,20 +68,20 @@ class _Notifier:
|
|
|
75
68
|
self._severity = severity
|
|
76
69
|
|
|
77
70
|
def _should_send_event(self) -> bool:
|
|
78
|
-
return self._event[
|
|
71
|
+
return self._event[ResultData.RESULT_STATUS] >= ResultStatusApp.detected.value
|
|
79
72
|
|
|
80
73
|
def _generate_message(self) -> str:
|
|
81
74
|
return f"""\
|
|
82
75
|
The monitoring app `{self._event[WriterEvent.APPLICATION_NAME]}` \
|
|
83
|
-
of kind `{self._event[
|
|
76
|
+
of kind `{self._event[ResultData.RESULT_KIND]}` \
|
|
84
77
|
detected a problem in model endpoint ID `{self._event[WriterEvent.ENDPOINT_ID]}` \
|
|
85
78
|
at time `{self._event[WriterEvent.START_INFER_TIME]}`.
|
|
86
79
|
|
|
87
80
|
Result data:
|
|
88
|
-
Name: `{self._event[
|
|
89
|
-
Value: `{self._event[
|
|
90
|
-
Status: `{self._event[
|
|
91
|
-
Extra data: `{self._event[
|
|
81
|
+
Name: `{self._event[ResultData.RESULT_NAME]}`
|
|
82
|
+
Value: `{self._event[ResultData.RESULT_VALUE]}`
|
|
83
|
+
Status: `{self._event[ResultData.RESULT_STATUS]}`
|
|
84
|
+
Extra data: `{self._event[ResultData.RESULT_EXTRA_DATA]}`\
|
|
92
85
|
"""
|
|
93
86
|
|
|
94
87
|
def notify(self) -> None:
|
|
@@ -103,7 +96,7 @@ Extra data: `{self._event[WriterEvent.RESULT_EXTRA_DATA]}`\
|
|
|
103
96
|
|
|
104
97
|
class ModelMonitoringWriter(StepToDict):
|
|
105
98
|
"""
|
|
106
|
-
Write monitoring
|
|
99
|
+
Write monitoring application results to the target databases
|
|
107
100
|
"""
|
|
108
101
|
|
|
109
102
|
kind = "monitoring_application_stream_pusher"
|
|
@@ -111,128 +104,97 @@ class ModelMonitoringWriter(StepToDict):
|
|
|
111
104
|
def __init__(self, project: str) -> None:
|
|
112
105
|
self.project = project
|
|
113
106
|
self.name = project # required for the deployment process
|
|
114
|
-
|
|
115
|
-
self._tsdb_client = self._get_v3io_frames_client(self._v3io_container)
|
|
107
|
+
|
|
116
108
|
self._custom_notifier = CustomNotificationPusher(
|
|
117
109
|
notification_types=[NotificationKind.slack]
|
|
118
110
|
)
|
|
119
|
-
self._create_tsdb_table()
|
|
120
|
-
self._endpoints_records = {}
|
|
121
|
-
|
|
122
|
-
@staticmethod
|
|
123
|
-
def get_v3io_container(project_name: str) -> str:
|
|
124
|
-
return f"users/pipelines/{project_name}/monitoring-apps"
|
|
125
|
-
|
|
126
|
-
@staticmethod
|
|
127
|
-
def _get_v3io_client() -> V3IOClient:
|
|
128
|
-
return mlrun.utils.v3io_clients.get_v3io_client(
|
|
129
|
-
endpoint=mlrun.mlconf.v3io_api,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
@staticmethod
|
|
133
|
-
def _get_v3io_frames_client(v3io_container: str) -> V3IOFramesClient:
|
|
134
|
-
return mlrun.utils.v3io_clients.get_frames_client(
|
|
135
|
-
address=mlrun.mlconf.v3io_framesd,
|
|
136
|
-
container=v3io_container,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
def _create_tsdb_table(self) -> None:
|
|
140
|
-
self._tsdb_client.create(
|
|
141
|
-
backend=_TSDB_BE,
|
|
142
|
-
table=_TSDB_TABLE,
|
|
143
|
-
if_exists=IGNORE,
|
|
144
|
-
rate=_TSDB_RATE,
|
|
145
|
-
)
|
|
146
111
|
|
|
147
|
-
|
|
148
|
-
event = _AppResultEvent(event.copy())
|
|
149
|
-
application_result_store = mlrun.model_monitoring.get_store_object(
|
|
112
|
+
self._app_result_store = mlrun.model_monitoring.get_store_object(
|
|
150
113
|
project=self.project
|
|
151
114
|
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _update_tsdb(self, event: _AppResultEvent) -> None:
|
|
155
|
-
event = _AppResultEvent(event.copy())
|
|
156
|
-
event[WriterEvent.END_INFER_TIME] = datetime.datetime.fromisoformat(
|
|
157
|
-
event[WriterEvent.END_INFER_TIME]
|
|
115
|
+
self._tsdb_connector = mlrun.model_monitoring.get_tsdb_connector(
|
|
116
|
+
project=self.project,
|
|
158
117
|
)
|
|
159
|
-
|
|
160
|
-
try:
|
|
161
|
-
self._tsdb_client.write(
|
|
162
|
-
backend=_TSDB_BE,
|
|
163
|
-
table=_TSDB_TABLE,
|
|
164
|
-
dfs=pd.DataFrame.from_records([event]),
|
|
165
|
-
index_cols=[
|
|
166
|
-
WriterEvent.END_INFER_TIME,
|
|
167
|
-
WriterEvent.ENDPOINT_ID,
|
|
168
|
-
WriterEvent.APPLICATION_NAME,
|
|
169
|
-
WriterEvent.RESULT_NAME,
|
|
170
|
-
],
|
|
171
|
-
)
|
|
172
|
-
logger.info("Updated V3IO TSDB successfully", table=_TSDB_TABLE)
|
|
173
|
-
except V3IOFramesError as err:
|
|
174
|
-
logger.warn(
|
|
175
|
-
"Could not write drift measures to TSDB",
|
|
176
|
-
err=err,
|
|
177
|
-
table=_TSDB_TABLE,
|
|
178
|
-
event=event,
|
|
179
|
-
)
|
|
118
|
+
self._endpoints_records = {}
|
|
180
119
|
|
|
181
120
|
@staticmethod
|
|
182
121
|
def _generate_event_on_drift(
|
|
183
|
-
|
|
184
|
-
):
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
kind=event_kind, entity=entity, value_dict=event_value
|
|
201
|
-
)
|
|
202
|
-
mlrun.get_run_db().generate_event(event_kind, event_data)
|
|
122
|
+
model_endpoint: str, drift_status: str, event_value: dict, project_name: str
|
|
123
|
+
) -> None:
|
|
124
|
+
logger.info("Sending an alert")
|
|
125
|
+
entity = mlrun.common.schemas.alert.EventEntities(
|
|
126
|
+
kind=alert_constants.EventEntityKind.MODEL,
|
|
127
|
+
project=project_name,
|
|
128
|
+
ids=[model_endpoint],
|
|
129
|
+
)
|
|
130
|
+
event_kind = (
|
|
131
|
+
alert_constants.EventKind.DRIFT_DETECTED
|
|
132
|
+
if drift_status == ResultStatusApp.detected.value
|
|
133
|
+
else alert_constants.EventKind.DRIFT_SUSPECTED
|
|
134
|
+
)
|
|
135
|
+
event_data = mlrun.common.schemas.Event(
|
|
136
|
+
kind=event_kind, entity=entity, value_dict=event_value
|
|
137
|
+
)
|
|
138
|
+
mlrun.get_run_db().generate_event(event_kind, event_data)
|
|
203
139
|
|
|
204
140
|
@staticmethod
|
|
205
|
-
def _reconstruct_event(event: _RawEvent) -> _AppResultEvent:
|
|
141
|
+
def _reconstruct_event(event: _RawEvent) -> tuple[_AppResultEvent, str]:
|
|
206
142
|
"""
|
|
207
143
|
Modify the raw event into the expected monitoring application event
|
|
208
144
|
schema as defined in `mlrun.common.schemas.model_monitoring.constants.WriterEvent`
|
|
209
145
|
"""
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
146
|
+
if not isinstance(event, dict):
|
|
147
|
+
raise _WriterEventTypeError(
|
|
148
|
+
f"The event is of type: {type(event)}, expected a dictionary"
|
|
213
149
|
)
|
|
214
|
-
|
|
215
|
-
|
|
150
|
+
kind = event.pop(WriterEvent.EVENT_KIND, WriterEventKind.RESULT)
|
|
151
|
+
result_event = _AppResultEvent(json.loads(event.pop(WriterEvent.DATA, "{}")))
|
|
152
|
+
if not result_event: # BC for < 1.7.0, can be removed in 1.9.0
|
|
153
|
+
result_event = _AppResultEvent(event)
|
|
154
|
+
else:
|
|
155
|
+
result_event.update(_AppResultEvent(event))
|
|
156
|
+
|
|
157
|
+
expected_keys = list(
|
|
158
|
+
set(WriterEvent.list()).difference(
|
|
159
|
+
[WriterEvent.EVENT_KIND, WriterEvent.DATA]
|
|
216
160
|
)
|
|
217
|
-
|
|
218
|
-
|
|
161
|
+
)
|
|
162
|
+
if kind == WriterEventKind.METRIC:
|
|
163
|
+
expected_keys.extend(MetricData.list())
|
|
164
|
+
elif kind == WriterEventKind.RESULT:
|
|
165
|
+
expected_keys.extend(ResultData.list())
|
|
166
|
+
else:
|
|
219
167
|
raise _WriterEventValueError(
|
|
220
|
-
"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
raise
|
|
225
|
-
f"The event
|
|
226
|
-
|
|
168
|
+
f"Unknown event kind: {kind}, expected one of: {WriterEventKind.list()}"
|
|
169
|
+
)
|
|
170
|
+
missing_keys = [key for key in expected_keys if key not in result_event]
|
|
171
|
+
if missing_keys:
|
|
172
|
+
raise _WriterEventValueError(
|
|
173
|
+
f"The received event misses some keys compared to the expected "
|
|
174
|
+
f"monitoring application event schema: {missing_keys}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return result_event, kind
|
|
227
178
|
|
|
228
179
|
def do(self, event: _RawEvent) -> None:
|
|
229
|
-
event = self._reconstruct_event(event)
|
|
180
|
+
event, kind = self._reconstruct_event(event)
|
|
230
181
|
logger.info("Starting to write event", event=event)
|
|
231
|
-
|
|
232
|
-
self.
|
|
182
|
+
|
|
183
|
+
self._tsdb_connector.write_application_event(event=event.copy(), kind=kind)
|
|
184
|
+
self._app_result_store.write_application_event(event=event.copy(), kind=kind)
|
|
185
|
+
logger.info("Completed event DB writes")
|
|
186
|
+
|
|
233
187
|
_Notifier(event=event, notification_pusher=self._custom_notifier).notify()
|
|
234
188
|
|
|
235
|
-
if
|
|
189
|
+
if (
|
|
190
|
+
mlrun.mlconf.alerts.mode == mlrun.common.schemas.alert.AlertsModes.enabled
|
|
191
|
+
and kind == WriterEventKind.RESULT
|
|
192
|
+
and (
|
|
193
|
+
event[ResultData.RESULT_STATUS] == ResultStatusApp.detected.value
|
|
194
|
+
or event[ResultData.RESULT_STATUS]
|
|
195
|
+
== ResultStatusApp.potential_detection.value
|
|
196
|
+
)
|
|
197
|
+
):
|
|
236
198
|
endpoint_id = event[WriterEvent.ENDPOINT_ID]
|
|
237
199
|
endpoint_record = self._endpoints_records.setdefault(
|
|
238
200
|
endpoint_id,
|
|
@@ -242,13 +204,31 @@ class ModelMonitoringWriter(StepToDict):
|
|
|
242
204
|
"app_name": event[WriterEvent.APPLICATION_NAME],
|
|
243
205
|
"model": endpoint_record.get(EventFieldType.MODEL),
|
|
244
206
|
"model_endpoint_id": event[WriterEvent.ENDPOINT_ID],
|
|
245
|
-
"result_name": event[
|
|
246
|
-
"result_value": event[
|
|
207
|
+
"result_name": event[ResultData.RESULT_NAME],
|
|
208
|
+
"result_value": event[ResultData.RESULT_VALUE],
|
|
247
209
|
}
|
|
248
210
|
self._generate_event_on_drift(
|
|
249
211
|
event[WriterEvent.ENDPOINT_ID],
|
|
250
|
-
event[
|
|
212
|
+
event[ResultData.RESULT_STATUS],
|
|
251
213
|
event_value,
|
|
252
214
|
self.project,
|
|
253
215
|
)
|
|
254
|
-
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
kind == WriterEventKind.RESULT
|
|
219
|
+
and event[WriterEvent.APPLICATION_NAME]
|
|
220
|
+
== HistogramDataDriftApplicationConstants.NAME
|
|
221
|
+
and event[ResultData.RESULT_NAME]
|
|
222
|
+
== HistogramDataDriftApplicationConstants.GENERAL_RESULT_NAME
|
|
223
|
+
):
|
|
224
|
+
endpoint_id = event[WriterEvent.ENDPOINT_ID]
|
|
225
|
+
logger.info(
|
|
226
|
+
"Updating the model endpoint with metadata specific to the histogram "
|
|
227
|
+
"data drift app",
|
|
228
|
+
endpoint_id=endpoint_id,
|
|
229
|
+
)
|
|
230
|
+
store = mlrun.model_monitoring.get_store_object(project=self.project)
|
|
231
|
+
store.update_model_endpoint(
|
|
232
|
+
endpoint_id=endpoint_id,
|
|
233
|
+
attributes=json.loads(event[ResultData.RESULT_EXTRA_DATA]),
|
|
234
|
+
)
|
mlrun/platforms/__init__.py
CHANGED
|
@@ -17,22 +17,23 @@ import json
|
|
|
17
17
|
from pprint import pprint
|
|
18
18
|
from time import sleep
|
|
19
19
|
|
|
20
|
-
from .
|
|
21
|
-
|
|
22
|
-
VolumeMount,
|
|
23
|
-
add_or_refresh_credentials,
|
|
24
|
-
is_iguazio_session_cookie,
|
|
25
|
-
mount_v3io,
|
|
26
|
-
v3io_cred,
|
|
27
|
-
)
|
|
28
|
-
from .other import (
|
|
20
|
+
from mlrun_pipelines.common.mounts import VolumeMount
|
|
21
|
+
from mlrun_pipelines.mounts import (
|
|
29
22
|
auto_mount,
|
|
30
23
|
mount_configmap,
|
|
31
24
|
mount_hostpath,
|
|
32
25
|
mount_pvc,
|
|
33
26
|
mount_s3,
|
|
34
27
|
mount_secret,
|
|
28
|
+
mount_v3io,
|
|
35
29
|
set_env_variables,
|
|
30
|
+
v3io_cred,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .iguazio import (
|
|
34
|
+
V3ioStreamClient,
|
|
35
|
+
add_or_refresh_credentials,
|
|
36
|
+
is_iguazio_session_cookie,
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
|
mlrun/platforms/iguazio.py
CHANGED
|
@@ -15,12 +15,9 @@
|
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
17
|
import urllib
|
|
18
|
-
from collections import namedtuple
|
|
19
18
|
from urllib.parse import urlparse
|
|
20
19
|
|
|
21
|
-
import kfp.dsl
|
|
22
20
|
import requests
|
|
23
|
-
import semver
|
|
24
21
|
import v3io
|
|
25
22
|
|
|
26
23
|
import mlrun.errors
|
|
@@ -29,203 +26,6 @@ from mlrun.utils import dict_to_json
|
|
|
29
26
|
|
|
30
27
|
_cached_control_session = None
|
|
31
28
|
|
|
32
|
-
VolumeMount = namedtuple("Mount", ["path", "sub_path"])
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def mount_v3io(
|
|
36
|
-
name="v3io",
|
|
37
|
-
remote="",
|
|
38
|
-
access_key="",
|
|
39
|
-
user="",
|
|
40
|
-
secret=None,
|
|
41
|
-
volume_mounts=None,
|
|
42
|
-
):
|
|
43
|
-
"""Modifier function to apply to a Container Op to volume mount a v3io path
|
|
44
|
-
|
|
45
|
-
:param name: the volume name
|
|
46
|
-
:param remote: the v3io path to use for the volume. ~/ prefix will be replaced with /users/<username>/
|
|
47
|
-
:param access_key: the access key used to auth against v3io. if not given V3IO_ACCESS_KEY env var will be used
|
|
48
|
-
:param user: the username used to auth against v3io. if not given V3IO_USERNAME env var will be used
|
|
49
|
-
:param secret: k8s secret name which would be used to get the username and access key to auth against v3io.
|
|
50
|
-
:param volume_mounts: list of VolumeMount. empty volume mounts & remote will default to mount /v3io & /User.
|
|
51
|
-
"""
|
|
52
|
-
volume_mounts, user = _enrich_and_validate_v3io_mounts(
|
|
53
|
-
remote=remote,
|
|
54
|
-
volume_mounts=volume_mounts,
|
|
55
|
-
user=user,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
def _attach_volume_mounts_and_creds(container_op: kfp.dsl.ContainerOp):
|
|
59
|
-
from kubernetes import client as k8s_client
|
|
60
|
-
|
|
61
|
-
vol = v3io_to_vol(name, remote, access_key, user, secret=secret)
|
|
62
|
-
container_op.add_volume(vol)
|
|
63
|
-
for volume_mount in volume_mounts:
|
|
64
|
-
container_op.container.add_volume_mount(
|
|
65
|
-
k8s_client.V1VolumeMount(
|
|
66
|
-
mount_path=volume_mount.path,
|
|
67
|
-
sub_path=volume_mount.sub_path,
|
|
68
|
-
name=name,
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
if not secret:
|
|
73
|
-
container_op = v3io_cred(access_key=access_key, user=user)(container_op)
|
|
74
|
-
return container_op
|
|
75
|
-
|
|
76
|
-
return _attach_volume_mounts_and_creds
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _enrich_and_validate_v3io_mounts(remote="", volume_mounts=None, user=""):
|
|
80
|
-
if remote and not volume_mounts:
|
|
81
|
-
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
82
|
-
"volume_mounts must be specified when remote is given"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
# Empty remote & volume_mounts defaults are volume mounts of /v3io and /User
|
|
86
|
-
if not remote and not volume_mounts:
|
|
87
|
-
user = _resolve_mount_user(user)
|
|
88
|
-
if not user:
|
|
89
|
-
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
90
|
-
"user name/env must be specified when using empty remote and volume_mounts"
|
|
91
|
-
)
|
|
92
|
-
volume_mounts = [
|
|
93
|
-
VolumeMount(path="/v3io", sub_path=""),
|
|
94
|
-
VolumeMount(path="/User", sub_path="users/" + user),
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
if not isinstance(volume_mounts, list) and any(
|
|
98
|
-
[not isinstance(x, VolumeMount) for x in volume_mounts]
|
|
99
|
-
):
|
|
100
|
-
raise TypeError("mounts should be a list of Mount")
|
|
101
|
-
|
|
102
|
-
return volume_mounts, user
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _resolve_mount_user(user=None):
|
|
106
|
-
return user or os.environ.get("V3IO_USERNAME")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def mount_spark_conf():
|
|
110
|
-
def _mount_spark(container_op: kfp.dsl.ContainerOp):
|
|
111
|
-
from kubernetes import client as k8s_client
|
|
112
|
-
|
|
113
|
-
container_op.container.add_volume_mount(
|
|
114
|
-
k8s_client.V1VolumeMount(
|
|
115
|
-
name="spark-master-config", mount_path="/etc/config/spark"
|
|
116
|
-
)
|
|
117
|
-
)
|
|
118
|
-
return container_op
|
|
119
|
-
|
|
120
|
-
return _mount_spark
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def mount_v3iod(namespace, v3io_config_configmap):
|
|
124
|
-
def _mount_v3iod(container_op: kfp.dsl.ContainerOp):
|
|
125
|
-
from kubernetes import client as k8s_client
|
|
126
|
-
|
|
127
|
-
def add_vol(name, mount_path, host_path):
|
|
128
|
-
vol = k8s_client.V1Volume(
|
|
129
|
-
name=name,
|
|
130
|
-
host_path=k8s_client.V1HostPathVolumeSource(path=host_path, type=""),
|
|
131
|
-
)
|
|
132
|
-
container_op.add_volume(vol)
|
|
133
|
-
container_op.container.add_volume_mount(
|
|
134
|
-
k8s_client.V1VolumeMount(mount_path=mount_path, name=name)
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# this is a legacy path for the daemon shared memory
|
|
138
|
-
host_path = "/dev/shm/"
|
|
139
|
-
|
|
140
|
-
# path to shared memory for daemon was changed in Iguazio 3.2.3-b1
|
|
141
|
-
igz_version = mlrun.mlconf.get_parsed_igz_version()
|
|
142
|
-
if igz_version and igz_version >= semver.VersionInfo.parse("3.2.3-b1"):
|
|
143
|
-
host_path = "/var/run/iguazio/dayman-shm/"
|
|
144
|
-
add_vol(name="shm", mount_path="/dev/shm", host_path=host_path + namespace)
|
|
145
|
-
|
|
146
|
-
add_vol(
|
|
147
|
-
name="v3iod-comm",
|
|
148
|
-
mount_path="/var/run/iguazio/dayman",
|
|
149
|
-
host_path="/var/run/iguazio/dayman/" + namespace,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
vol = k8s_client.V1Volume(
|
|
153
|
-
name="daemon-health", empty_dir=k8s_client.V1EmptyDirVolumeSource()
|
|
154
|
-
)
|
|
155
|
-
container_op.add_volume(vol)
|
|
156
|
-
container_op.container.add_volume_mount(
|
|
157
|
-
k8s_client.V1VolumeMount(
|
|
158
|
-
mount_path="/var/run/iguazio/daemon_health", name="daemon-health"
|
|
159
|
-
)
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
vol = k8s_client.V1Volume(
|
|
163
|
-
name="v3io-config",
|
|
164
|
-
config_map=k8s_client.V1ConfigMapVolumeSource(
|
|
165
|
-
name=v3io_config_configmap, default_mode=420
|
|
166
|
-
),
|
|
167
|
-
)
|
|
168
|
-
container_op.add_volume(vol)
|
|
169
|
-
container_op.container.add_volume_mount(
|
|
170
|
-
k8s_client.V1VolumeMount(mount_path="/etc/config/v3io", name="v3io-config")
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
container_op.container.add_env_variable(
|
|
174
|
-
k8s_client.V1EnvVar(
|
|
175
|
-
name="CURRENT_NODE_IP",
|
|
176
|
-
value_from=k8s_client.V1EnvVarSource(
|
|
177
|
-
field_ref=k8s_client.V1ObjectFieldSelector(
|
|
178
|
-
api_version="v1", field_path="status.hostIP"
|
|
179
|
-
)
|
|
180
|
-
),
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
|
-
container_op.container.add_env_variable(
|
|
184
|
-
k8s_client.V1EnvVar(
|
|
185
|
-
name="IGZ_DATA_CONFIG_FILE", value="/igz/java/conf/v3io.conf"
|
|
186
|
-
)
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
return container_op
|
|
190
|
-
|
|
191
|
-
return _mount_v3iod
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def v3io_cred(api="", user="", access_key=""):
|
|
195
|
-
"""
|
|
196
|
-
Modifier function to copy local v3io env vars to container
|
|
197
|
-
|
|
198
|
-
Usage::
|
|
199
|
-
|
|
200
|
-
train = train_op(...)
|
|
201
|
-
train.apply(use_v3io_cred())
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
def _use_v3io_cred(container_op: kfp.dsl.ContainerOp):
|
|
205
|
-
from os import environ
|
|
206
|
-
|
|
207
|
-
from kubernetes import client as k8s_client
|
|
208
|
-
|
|
209
|
-
web_api = api or environ.get("V3IO_API") or mlconf.v3io_api
|
|
210
|
-
_user = user or environ.get("V3IO_USERNAME")
|
|
211
|
-
_access_key = access_key or environ.get("V3IO_ACCESS_KEY")
|
|
212
|
-
v3io_framesd = mlconf.v3io_framesd or environ.get("V3IO_FRAMESD")
|
|
213
|
-
|
|
214
|
-
return (
|
|
215
|
-
container_op.container.add_env_variable(
|
|
216
|
-
k8s_client.V1EnvVar(name="V3IO_API", value=web_api)
|
|
217
|
-
)
|
|
218
|
-
.add_env_variable(k8s_client.V1EnvVar(name="V3IO_USERNAME", value=_user))
|
|
219
|
-
.add_env_variable(
|
|
220
|
-
k8s_client.V1EnvVar(name="V3IO_ACCESS_KEY", value=_access_key)
|
|
221
|
-
)
|
|
222
|
-
.add_env_variable(
|
|
223
|
-
k8s_client.V1EnvVar(name="V3IO_FRAMESD", value=v3io_framesd)
|
|
224
|
-
)
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
return _use_v3io_cred
|
|
228
|
-
|
|
229
29
|
|
|
230
30
|
def split_path(mntpath=""):
|
|
231
31
|
if mntpath[0] == "/":
|
|
@@ -525,8 +325,8 @@ def add_or_refresh_credentials(
|
|
|
525
325
|
# different access keys for the 2 usages
|
|
526
326
|
token = (
|
|
527
327
|
token
|
|
528
|
-
# can't use mlrun.runtimes.constants.FunctionEnvironmentVariables.auth_session cause this is running
|
|
529
|
-
# import execution path (when we're initializing the run db) and therefore we can't import mlrun.runtimes
|
|
328
|
+
# can't use mlrun.common.runtimes.constants.FunctionEnvironmentVariables.auth_session cause this is running
|
|
329
|
+
# in the import execution path (when we're initializing the run db) and therefore we can't import mlrun.runtimes
|
|
530
330
|
or os.environ.get("MLRUN_AUTH_SESSION")
|
|
531
331
|
or os.environ.get("V3IO_ACCESS_KEY")
|
|
532
332
|
)
|
|
@@ -582,3 +382,22 @@ def sanitize_username(username: str):
|
|
|
582
382
|
So simply replace it with dash
|
|
583
383
|
"""
|
|
584
384
|
return username.replace("_", "-")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def min_iguazio_versions(*versions):
|
|
388
|
+
def decorator(function):
|
|
389
|
+
def wrapper(*args, **kwargs):
|
|
390
|
+
if mlrun.utils.helpers.validate_component_version_compatibility(
|
|
391
|
+
"iguazio", *versions
|
|
392
|
+
):
|
|
393
|
+
return function(*args, **kwargs)
|
|
394
|
+
|
|
395
|
+
message = (
|
|
396
|
+
f"{function.__name__} is supported since Iguazio {' or '.join(versions)}, currently using "
|
|
397
|
+
f"Iguazio {mlconf.igz_version}."
|
|
398
|
+
)
|
|
399
|
+
raise mlrun.errors.MLRunIncompatibleVersionError(message)
|
|
400
|
+
|
|
401
|
+
return wrapper
|
|
402
|
+
|
|
403
|
+
return decorator
|
mlrun/projects/operations.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import warnings
|
|
16
16
|
from typing import Optional, Union
|
|
17
17
|
|
|
18
|
-
import
|
|
18
|
+
from mlrun_pipelines.models import PipelineNodeWrapper
|
|
19
19
|
|
|
20
20
|
import mlrun
|
|
21
21
|
from mlrun.utils import hub_prefix
|
|
@@ -76,7 +76,7 @@ def run_function(
|
|
|
76
76
|
notifications: list[mlrun.model.Notification] = None,
|
|
77
77
|
returns: Optional[list[Union[str, dict[str, str]]]] = None,
|
|
78
78
|
builder_env: Optional[list] = None,
|
|
79
|
-
) -> Union[mlrun.model.RunObject,
|
|
79
|
+
) -> Union[mlrun.model.RunObject, PipelineNodeWrapper]:
|
|
80
80
|
"""Run a local or remote task as part of a local/kubeflow pipeline
|
|
81
81
|
|
|
82
82
|
run_function() allow you to execute a function locally, on a remote cluster, or as part of an automated workflow
|
|
@@ -86,7 +86,7 @@ def run_function(
|
|
|
86
86
|
when functions run as part of a workflow/pipeline (project.run()) some attributes can be set at the run level,
|
|
87
87
|
e.g. local=True will run all the functions locally, setting artifact_path will direct all outputs to the same path.
|
|
88
88
|
project runs provide additional notifications/reporting and exception handling.
|
|
89
|
-
inside a Kubeflow pipeline (KFP) run_function() generates KFP
|
|
89
|
+
inside a Kubeflow pipeline (KFP) run_function() generates KFP node (see PipelineNodeWrapper) which forms a DAG
|
|
90
90
|
some behavior may differ between regular runs and deferred KFP runs.
|
|
91
91
|
|
|
92
92
|
example (use with function object)::
|
|
@@ -166,7 +166,7 @@ def run_function(
|
|
|
166
166
|
artifact type can be given there. The artifact key must appear in the dictionary as
|
|
167
167
|
"key": "the_key".
|
|
168
168
|
:param builder_env: env vars dict for source archive config/credentials e.g. builder_env={"GIT_TOKEN": token}
|
|
169
|
-
:return: MLRun RunObject or
|
|
169
|
+
:return: MLRun RunObject or PipelineNodeWrapper
|
|
170
170
|
"""
|
|
171
171
|
engine, function = _get_engine_and_function(function, project_object)
|
|
172
172
|
task = mlrun.new_task(
|
|
@@ -254,7 +254,7 @@ def build_function(
|
|
|
254
254
|
overwrite_build_params: bool = False,
|
|
255
255
|
extra_args: str = None,
|
|
256
256
|
force_build: bool = False,
|
|
257
|
-
) -> Union[BuildStatus,
|
|
257
|
+
) -> Union[BuildStatus, PipelineNodeWrapper]:
|
|
258
258
|
"""deploy ML function, build container with its dependencies
|
|
259
259
|
|
|
260
260
|
:param function: Name of the function (in the project) or function object
|
|
@@ -294,7 +294,11 @@ def build_function(
|
|
|
294
294
|
if overwrite_build_params:
|
|
295
295
|
function.spec.build.commands = None
|
|
296
296
|
if requirements or requirements_file:
|
|
297
|
-
function.with_requirements(
|
|
297
|
+
function.with_requirements(
|
|
298
|
+
requirements=requirements,
|
|
299
|
+
requirements_file=requirements_file,
|
|
300
|
+
overwrite=True,
|
|
301
|
+
)
|
|
298
302
|
if commands:
|
|
299
303
|
function.with_commands(commands)
|
|
300
304
|
return function.deploy_step(
|
|
@@ -358,7 +362,7 @@ def deploy_function(
|
|
|
358
362
|
builder_env: dict = None,
|
|
359
363
|
project_object=None,
|
|
360
364
|
mock: bool = None,
|
|
361
|
-
) -> Union[DeployStatus,
|
|
365
|
+
) -> Union[DeployStatus, PipelineNodeWrapper]:
|
|
362
366
|
"""deploy real-time (nuclio based) functions
|
|
363
367
|
|
|
364
368
|
:param function: name of the function (in the project) or function object
|