mlrun 1.10.0rc7__py3-none-any.whl → 1.10.0rc9__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 +3 -1
- mlrun/common/db/dialects.py +25 -0
- mlrun/common/schemas/background_task.py +5 -0
- mlrun/common/schemas/function.py +1 -0
- mlrun/common/schemas/model_monitoring/__init__.py +2 -0
- mlrun/common/schemas/model_monitoring/constants.py +16 -0
- mlrun/common/schemas/model_monitoring/model_endpoints.py +8 -0
- mlrun/common/schemas/partition.py +13 -3
- mlrun/common/schemas/project.py +4 -0
- mlrun/common/schemas/serving.py +2 -0
- mlrun/config.py +11 -22
- mlrun/datastore/utils.py +3 -2
- mlrun/db/__init__.py +1 -0
- mlrun/db/base.py +11 -10
- mlrun/db/httpdb.py +97 -25
- mlrun/db/nopdb.py +5 -4
- mlrun/db/sql_types.py +160 -0
- mlrun/frameworks/tf_keras/__init__.py +4 -4
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +23 -20
- mlrun/frameworks/tf_keras/mlrun_interface.py +4 -1
- mlrun/frameworks/tf_keras/model_handler.py +80 -9
- mlrun/frameworks/tf_keras/utils.py +12 -1
- mlrun/launcher/base.py +6 -1
- mlrun/launcher/client.py +1 -22
- mlrun/launcher/local.py +0 -4
- mlrun/model_monitoring/applications/base.py +21 -1
- mlrun/model_monitoring/applications/context.py +2 -1
- mlrun/projects/pipelines.py +35 -3
- mlrun/projects/project.py +13 -29
- mlrun/run.py +37 -5
- mlrun/runtimes/daskjob.py +0 -2
- mlrun/runtimes/kubejob.py +0 -4
- mlrun/runtimes/mpijob/abstract.py +0 -2
- mlrun/runtimes/mpijob/v1.py +0 -2
- mlrun/runtimes/nuclio/function.py +0 -2
- mlrun/runtimes/nuclio/serving.py +14 -51
- mlrun/runtimes/pod.py +0 -3
- mlrun/runtimes/remotesparkjob.py +0 -2
- mlrun/runtimes/sparkjob/spark3job.py +0 -2
- mlrun/serving/__init__.py +2 -0
- mlrun/serving/server.py +159 -123
- mlrun/serving/states.py +215 -18
- mlrun/serving/system_steps.py +391 -0
- mlrun/serving/v2_serving.py +9 -8
- mlrun/utils/helpers.py +19 -1
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/METADATA +22 -18
- {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/RECORD +52 -50
- mlrun/common/db/sql_session.py +0 -79
- {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/top_level.txt +0 -0
mlrun/db/sql_types.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
This module provides SQLAlchemy TypeDecorator subclasses that are aware of
|
|
16
|
+
database dialects (MySQL, PostgreSQL, SQLite) and automatically select
|
|
17
|
+
appropriate native types (e.g., UUID, BLOB, TIMESTAMP with precision) or
|
|
18
|
+
fallbacks (e.g., hex-string storage) to ensure consistent behavior across
|
|
19
|
+
different database backends.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import uuid
|
|
23
|
+
from typing import Any, Optional, Union
|
|
24
|
+
|
|
25
|
+
import sqlalchemy.types
|
|
26
|
+
from sqlalchemy import CHAR, Text
|
|
27
|
+
from sqlalchemy.dialects.mysql import DATETIME as MYSQL_DATETIME
|
|
28
|
+
from sqlalchemy.dialects.mysql import MEDIUMBLOB
|
|
29
|
+
from sqlalchemy.dialects.postgresql import BYTEA
|
|
30
|
+
from sqlalchemy.dialects.postgresql import TIMESTAMP as PG_TIMESTAMP
|
|
31
|
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
32
|
+
from sqlalchemy.engine.interfaces import Dialect
|
|
33
|
+
from sqlalchemy.types import TypeDecorator
|
|
34
|
+
|
|
35
|
+
import mlrun.common.db.dialects
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DateTime(TypeDecorator):
|
|
39
|
+
impl = sqlalchemy.types.DateTime
|
|
40
|
+
cache_ok = True
|
|
41
|
+
precision: int = 3
|
|
42
|
+
|
|
43
|
+
def load_dialect_impl(
|
|
44
|
+
self,
|
|
45
|
+
dialect: Dialect,
|
|
46
|
+
) -> sqlalchemy.types.TypeEngine:
|
|
47
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.MYSQL:
|
|
48
|
+
return dialect.type_descriptor(
|
|
49
|
+
MYSQL_DATETIME(
|
|
50
|
+
fsp=self.precision,
|
|
51
|
+
timezone=True,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
|
|
55
|
+
return dialect.type_descriptor(
|
|
56
|
+
PG_TIMESTAMP(
|
|
57
|
+
precision=self.precision,
|
|
58
|
+
timezone=True,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return dialect.type_descriptor(sqlalchemy.types.DateTime)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MicroSecondDateTime(DateTime):
|
|
65
|
+
cache_ok = True
|
|
66
|
+
precision: int = 6
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Blob(TypeDecorator):
|
|
70
|
+
impl = sqlalchemy.types.LargeBinary
|
|
71
|
+
cache_ok = True
|
|
72
|
+
|
|
73
|
+
def load_dialect_impl(
|
|
74
|
+
self,
|
|
75
|
+
dialect: Dialect,
|
|
76
|
+
) -> sqlalchemy.types.TypeEngine:
|
|
77
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.MYSQL:
|
|
78
|
+
return dialect.type_descriptor(MEDIUMBLOB)
|
|
79
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
|
|
80
|
+
return dialect.type_descriptor(BYTEA)
|
|
81
|
+
return dialect.type_descriptor(self.impl)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Utf8BinText(TypeDecorator):
|
|
85
|
+
impl = Text
|
|
86
|
+
cache_ok = True
|
|
87
|
+
|
|
88
|
+
def load_dialect_impl(
|
|
89
|
+
self,
|
|
90
|
+
dialect: Dialect,
|
|
91
|
+
) -> sqlalchemy.types.TypeEngine:
|
|
92
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.MYSQL:
|
|
93
|
+
return dialect.type_descriptor(
|
|
94
|
+
sqlalchemy.dialects.mysql.VARCHAR(
|
|
95
|
+
collation="utf8_bin",
|
|
96
|
+
length=255,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
|
|
100
|
+
# This collation is created as part of the database creation
|
|
101
|
+
return dialect.type_descriptor(
|
|
102
|
+
Text(
|
|
103
|
+
collation="utf8_bin",
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.SQLITE:
|
|
107
|
+
return dialect.type_descriptor(
|
|
108
|
+
Text(
|
|
109
|
+
collation="BINARY",
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return dialect.type_descriptor(self.impl)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class UuidType(TypeDecorator):
|
|
116
|
+
"""
|
|
117
|
+
A UUID type which stores as native UUID on Postgres (as_uuid=True)
|
|
118
|
+
and as 32-char hex strings on other dialects.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
impl = CHAR(32)
|
|
122
|
+
cache_ok = True
|
|
123
|
+
|
|
124
|
+
def load_dialect_impl(self, dialect: Dialect) -> sqlalchemy.types.TypeEngine:
|
|
125
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
|
|
126
|
+
return dialect.type_descriptor(PG_UUID(as_uuid=True))
|
|
127
|
+
return dialect.type_descriptor(CHAR(32))
|
|
128
|
+
|
|
129
|
+
def process_bind_param(
|
|
130
|
+
self,
|
|
131
|
+
value: Optional[Union[uuid.UUID, str]],
|
|
132
|
+
dialect: Dialect,
|
|
133
|
+
) -> Optional[Union[uuid.UUID, str]]:
|
|
134
|
+
if value is None:
|
|
135
|
+
return None
|
|
136
|
+
if isinstance(value, uuid.UUID):
|
|
137
|
+
return (
|
|
138
|
+
value
|
|
139
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL
|
|
140
|
+
else value.hex
|
|
141
|
+
)
|
|
142
|
+
if isinstance(value, str):
|
|
143
|
+
u = uuid.UUID(value)
|
|
144
|
+
return (
|
|
145
|
+
u
|
|
146
|
+
if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL
|
|
147
|
+
else u.hex
|
|
148
|
+
)
|
|
149
|
+
raise ValueError(f"Cannot bind UUID value {value!r}")
|
|
150
|
+
|
|
151
|
+
def process_result_value(
|
|
152
|
+
self, value: Optional[Union[uuid.UUID, bytes, str]], dialect: Dialect
|
|
153
|
+
) -> Optional[uuid.UUID]:
|
|
154
|
+
if value is None:
|
|
155
|
+
return None
|
|
156
|
+
return value if isinstance(value, uuid.UUID) else uuid.UUID(value)
|
|
157
|
+
|
|
158
|
+
def coerce_compared_value(self, op: Any, value: Any) -> TypeDecorator:
|
|
159
|
+
# ensure STR comparisons are coerced through this type
|
|
160
|
+
return self
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
from typing import Any, Optional, Union
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import tensorflow as tf
|
|
18
18
|
|
|
19
19
|
import mlrun
|
|
20
20
|
import mlrun.common.constants as mlrun_constants
|
|
@@ -27,11 +27,11 @@ from .utils import TFKerasTypes, TFKerasUtils
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def apply_mlrun(
|
|
30
|
-
model: keras.Model = None,
|
|
30
|
+
model: tf.keras.Model = None,
|
|
31
31
|
model_name: Optional[str] = None,
|
|
32
32
|
tag: str = "",
|
|
33
33
|
model_path: Optional[str] = None,
|
|
34
|
-
model_format: str =
|
|
34
|
+
model_format: Optional[str] = None,
|
|
35
35
|
save_traces: bool = False,
|
|
36
36
|
modules_map: Optional[Union[dict[str, Union[None, str, list[str]]], str]] = None,
|
|
37
37
|
custom_objects_map: Optional[Union[dict[str, Union[str, list[str]]], str]] = None,
|
|
@@ -54,7 +54,7 @@ def apply_mlrun(
|
|
|
54
54
|
:param model_path: The model's store object path. Mandatory for evaluation (to know which model to
|
|
55
55
|
update). If model is not provided, it will be loaded from this path.
|
|
56
56
|
:param model_format: The format to use for saving and loading the model. Should be passed as a
|
|
57
|
-
member of the class 'ModelFormats'.
|
|
57
|
+
member of the class 'ModelFormats'.
|
|
58
58
|
:param save_traces: Whether or not to use functions saving (only available for the 'SavedModel'
|
|
59
59
|
format) for loading the model later without the custom objects dictionary. Only
|
|
60
60
|
from tensorflow version >= 2.4.0. Using this setting will increase the model
|
|
@@ -16,14 +16,14 @@ from typing import Callable, Optional, Union
|
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
import tensorflow as tf
|
|
19
|
-
from tensorflow import
|
|
19
|
+
from tensorflow import keras
|
|
20
20
|
from tensorflow.python.keras.callbacks import Callback
|
|
21
21
|
|
|
22
22
|
import mlrun
|
|
23
23
|
|
|
24
24
|
from ..._common import LoggingMode
|
|
25
25
|
from ..._dl_common.loggers import Logger
|
|
26
|
-
from ..utils import TFKerasTypes
|
|
26
|
+
from ..utils import TFKerasTypes, is_keras_3
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class LoggingCallback(Callback):
|
|
@@ -70,7 +70,7 @@ class LoggingCallback(Callback):
|
|
|
70
70
|
{
|
|
71
71
|
"epochs": 7
|
|
72
72
|
}
|
|
73
|
-
:param auto_log: Whether
|
|
73
|
+
:param auto_log: Whether to enable auto logging, trying to track common static and dynamic
|
|
74
74
|
hyperparameters.
|
|
75
75
|
"""
|
|
76
76
|
super().__init__()
|
|
@@ -385,18 +385,24 @@ class LoggingCallback(Callback):
|
|
|
385
385
|
self._logger.log_context_parameters()
|
|
386
386
|
|
|
387
387
|
# Add learning rate:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
388
|
+
learning_rate_keys = [
|
|
389
|
+
"learning_rate",
|
|
390
|
+
"lr",
|
|
391
|
+
] # "lr" is for backward compatibility in older keras versions.
|
|
392
|
+
if all(
|
|
393
|
+
learning_rate_key not in self._dynamic_hyperparameters_keys
|
|
394
|
+
for learning_rate_key in learning_rate_keys
|
|
395
|
+
) and hasattr(self.model, "optimizer"):
|
|
396
|
+
for learning_rate_key in learning_rate_keys:
|
|
397
|
+
learning_rate_key_chain = ["optimizer", learning_rate_key]
|
|
398
|
+
try:
|
|
399
|
+
self._get_hyperparameter(key_chain=learning_rate_key_chain)
|
|
400
|
+
except (KeyError, IndexError, AttributeError, ValueError):
|
|
401
|
+
continue
|
|
395
402
|
self._dynamic_hyperparameters_keys[learning_rate_key] = (
|
|
396
403
|
learning_rate_key_chain
|
|
397
404
|
)
|
|
398
|
-
|
|
399
|
-
pass
|
|
405
|
+
break
|
|
400
406
|
|
|
401
407
|
def _get_hyperparameter(
|
|
402
408
|
self,
|
|
@@ -427,7 +433,7 @@ class LoggingCallback(Callback):
|
|
|
427
433
|
value = value[key]
|
|
428
434
|
else:
|
|
429
435
|
value = getattr(value, key)
|
|
430
|
-
except KeyError or IndexError as KeyChainError:
|
|
436
|
+
except KeyError or IndexError or AttributeError as KeyChainError:
|
|
431
437
|
raise KeyChainError(
|
|
432
438
|
f"Error during getting a hyperparameter value with the key chain {key_chain}. "
|
|
433
439
|
f"The {value.__class__} in it does not have the following key/index from the key provided: "
|
|
@@ -435,7 +441,9 @@ class LoggingCallback(Callback):
|
|
|
435
441
|
)
|
|
436
442
|
|
|
437
443
|
# Parse the value:
|
|
438
|
-
if isinstance(value, Tensor) or
|
|
444
|
+
if isinstance(value, (tf.Tensor, tf.Variable)) or (
|
|
445
|
+
is_keras_3() and isinstance(value, (keras.KerasTensor, keras.Variable))
|
|
446
|
+
):
|
|
439
447
|
if int(tf.size(value)) == 1:
|
|
440
448
|
value = float(value)
|
|
441
449
|
else:
|
|
@@ -451,12 +459,7 @@ class LoggingCallback(Callback):
|
|
|
451
459
|
f"The parameter with the following key chain: {key_chain} is a numpy.ndarray with {value.size} "
|
|
452
460
|
f"elements. numpy arrays are trackable only if they have 1 element."
|
|
453
461
|
)
|
|
454
|
-
elif not (
|
|
455
|
-
isinstance(value, float)
|
|
456
|
-
or isinstance(value, int)
|
|
457
|
-
or isinstance(value, str)
|
|
458
|
-
or isinstance(value, bool)
|
|
459
|
-
):
|
|
462
|
+
elif not (isinstance(value, (float, int, str, bool))):
|
|
460
463
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
461
464
|
f"The parameter with the following key chain: {key_chain} is of type '{type(value)}'. The only "
|
|
462
465
|
f"trackable types are: float, int, str and bool."
|
|
@@ -280,7 +280,10 @@ class TFKerasMLRunInterface(MLRunInterface, ABC):
|
|
|
280
280
|
print(f"Horovod worker #{self._hvd.rank()} is using CPU")
|
|
281
281
|
|
|
282
282
|
# Adjust learning rate based on the number of GPUs:
|
|
283
|
-
optimizer
|
|
283
|
+
if hasattr(self.optimizer, "lr"):
|
|
284
|
+
optimizer.lr *= self._hvd.size()
|
|
285
|
+
else:
|
|
286
|
+
optimizer.learning_rate *= self._hvd.size()
|
|
284
287
|
|
|
285
288
|
# Wrap the optimizer in horovod's distributed optimizer: 'hvd.DistributedOptimizer'.
|
|
286
289
|
optimizer = self._hvd.DistributedOptimizer(optimizer)
|
|
@@ -29,7 +29,7 @@ from mlrun.features import Feature
|
|
|
29
29
|
from .._common import without_mlrun_interface
|
|
30
30
|
from .._dl_common import DLModelHandler
|
|
31
31
|
from .mlrun_interface import TFKerasMLRunInterface
|
|
32
|
-
from .utils import TFKerasUtils
|
|
32
|
+
from .utils import TFKerasUtils, is_keras_3
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class TFKerasModelHandler(DLModelHandler):
|
|
@@ -40,8 +40,8 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
40
40
|
# Framework name:
|
|
41
41
|
FRAMEWORK_NAME = "tensorflow.keras"
|
|
42
42
|
|
|
43
|
-
# Declare a type of
|
|
44
|
-
IOSample = Union[tf.Tensor, tf.TensorSpec, np.ndarray]
|
|
43
|
+
# Declare a type of input sample (only from keras v3 there is a KerasTensor type):
|
|
44
|
+
IOSample = Union[tf.Tensor, tf.TensorSpec, "keras.KerasTensor", np.ndarray]
|
|
45
45
|
|
|
46
46
|
class ModelFormats:
|
|
47
47
|
"""
|
|
@@ -49,9 +49,19 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
51
|
SAVED_MODEL = "SavedModel"
|
|
52
|
+
KERAS = "keras"
|
|
52
53
|
H5 = "h5"
|
|
53
54
|
JSON_ARCHITECTURE_H5_WEIGHTS = "json_h5"
|
|
54
55
|
|
|
56
|
+
@classmethod
|
|
57
|
+
def default(cls) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Get the default model format to use for saving and loading the model based on the keras version.
|
|
60
|
+
|
|
61
|
+
:return: The default model format to use.
|
|
62
|
+
"""
|
|
63
|
+
return cls.KERAS if is_keras_3() else cls.SAVED_MODEL
|
|
64
|
+
|
|
55
65
|
class _LabelKeys:
|
|
56
66
|
"""
|
|
57
67
|
Required labels keys to log with the model.
|
|
@@ -65,7 +75,7 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
65
75
|
model: keras.Model = None,
|
|
66
76
|
model_path: Optional[str] = None,
|
|
67
77
|
model_name: Optional[str] = None,
|
|
68
|
-
model_format: str =
|
|
78
|
+
model_format: Optional[str] = None,
|
|
69
79
|
context: mlrun.MLClientCtx = None,
|
|
70
80
|
modules_map: Optional[
|
|
71
81
|
Union[dict[str, Union[None, str, list[str]]], str]
|
|
@@ -98,7 +108,7 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
98
108
|
* If given a loaded model object and the model name is None, the name will be
|
|
99
109
|
set to the model's object name / class.
|
|
100
110
|
:param model_format: The format to use for saving and loading the model. Should be passed as a
|
|
101
|
-
member of the class 'ModelFormats'.
|
|
111
|
+
member of the class 'ModelFormats'.
|
|
102
112
|
:param context: MLRun context to work with for logging the model.
|
|
103
113
|
:param modules_map: A dictionary of all the modules required for loading the model. Each key
|
|
104
114
|
is a path to a module and its value is the object name to import from it. All
|
|
@@ -144,8 +154,11 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
144
154
|
* 'save_traces' parameter was miss-used.
|
|
145
155
|
"""
|
|
146
156
|
# Validate given format:
|
|
157
|
+
if not model_format:
|
|
158
|
+
model_format = TFKerasModelHandler.ModelFormats.default()
|
|
147
159
|
if model_format not in [
|
|
148
160
|
TFKerasModelHandler.ModelFormats.SAVED_MODEL,
|
|
161
|
+
TFKerasModelHandler.ModelFormats.KERAS,
|
|
149
162
|
TFKerasModelHandler.ModelFormats.H5,
|
|
150
163
|
TFKerasModelHandler.ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS,
|
|
151
164
|
]:
|
|
@@ -153,6 +166,22 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
153
166
|
f"Unrecognized model format: '{model_format}'. Please use one of the class members of "
|
|
154
167
|
"'TFKerasModelHandler.ModelFormats'"
|
|
155
168
|
)
|
|
169
|
+
if not is_keras_3():
|
|
170
|
+
if model_format == TFKerasModelHandler.ModelFormats.KERAS:
|
|
171
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
172
|
+
"The 'keras' model format is only supported in Keras 3.0.0 and above. "
|
|
173
|
+
f"Current version is {keras.__version__}."
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
if (
|
|
177
|
+
model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL
|
|
178
|
+
or model_format
|
|
179
|
+
== TFKerasModelHandler.ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS
|
|
180
|
+
):
|
|
181
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
182
|
+
f"The '{model_format}' model format is not supported in Keras 3.0.0 and above. "
|
|
183
|
+
f"Current version is {keras.__version__}."
|
|
184
|
+
)
|
|
156
185
|
|
|
157
186
|
# Validate 'save_traces':
|
|
158
187
|
if save_traces:
|
|
@@ -239,11 +268,19 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
239
268
|
self._model_file = f"{self._model_name}.h5"
|
|
240
269
|
self._model.save(self._model_file)
|
|
241
270
|
|
|
271
|
+
# ModelFormats.keras - Save as a keras file:
|
|
272
|
+
elif self._model_format == self.ModelFormats.KERAS:
|
|
273
|
+
self._model_file = f"{self._model_name}.keras"
|
|
274
|
+
self._model.save(self._model_file)
|
|
275
|
+
|
|
242
276
|
# ModelFormats.SAVED_MODEL - Save as a SavedModel directory and zip its file:
|
|
243
277
|
elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
|
|
244
278
|
# Save it in a SavedModel format directory:
|
|
279
|
+
# Note: Using keras>=3.0.0 can save in this format via `model.export` but then it won't be able to load it
|
|
280
|
+
# back, only for inference. So, we use the `save` method instead for keras 2 and validate the user won't use
|
|
281
|
+
# keras 3 and this model format.
|
|
245
282
|
if self._save_traces is True:
|
|
246
|
-
# Save traces can only be used in versions >= 2.4, so only if
|
|
283
|
+
# Save traces can only be used in versions >= 2.4, so only if it's true, we use it in the call:
|
|
247
284
|
self._model.save(self._model_name, save_traces=self._save_traces)
|
|
248
285
|
else:
|
|
249
286
|
self._model.save(self._model_name)
|
|
@@ -303,6 +340,12 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
303
340
|
self._model_file, custom_objects=self._custom_objects
|
|
304
341
|
)
|
|
305
342
|
|
|
343
|
+
# ModelFormats.KERAS - Load from a keras file:
|
|
344
|
+
elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
|
|
345
|
+
self._model = keras.models.load_model(
|
|
346
|
+
self._model_file, custom_objects=self._custom_objects
|
|
347
|
+
)
|
|
348
|
+
|
|
306
349
|
# ModelFormats.SAVED_MODEL - Load from a SavedModel directory:
|
|
307
350
|
elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
|
|
308
351
|
self._model = keras.models.load_model(
|
|
@@ -434,7 +477,10 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
434
477
|
)
|
|
435
478
|
|
|
436
479
|
# Read the inputs:
|
|
437
|
-
input_signature = [
|
|
480
|
+
input_signature = [
|
|
481
|
+
getattr(input_layer, "type_spec", input_layer)
|
|
482
|
+
for input_layer in self._model.inputs
|
|
483
|
+
]
|
|
438
484
|
|
|
439
485
|
# Set the inputs:
|
|
440
486
|
self.set_inputs(from_sample=input_signature)
|
|
@@ -453,7 +499,8 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
453
499
|
|
|
454
500
|
# Read the outputs:
|
|
455
501
|
output_signature = [
|
|
456
|
-
output_layer
|
|
502
|
+
getattr(output_layer, "type_spec", output_layer)
|
|
503
|
+
for output_layer in self._model.outputs
|
|
457
504
|
]
|
|
458
505
|
|
|
459
506
|
# Set the outputs:
|
|
@@ -480,11 +527,22 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
480
527
|
self._model_file = os.path.join(
|
|
481
528
|
os.path.dirname(self._model_file), self._model_name
|
|
482
529
|
)
|
|
530
|
+
elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
|
|
531
|
+
# When keras tried to load it, it validates the suffix. The `artifacts.model.get_model` function is
|
|
532
|
+
# downloading the keras file to a temp file with a `pkl` suffix, so it needs to be replaced:
|
|
533
|
+
self._model_file = self._model_file.rsplit(".pkl", 1)[0] + ".keras"
|
|
534
|
+
elif self._model_format == TFKerasModelHandler.ModelFormats.H5:
|
|
535
|
+
# When keras tried to load it, it validates the suffix. The `artifacts.model.get_model` function is
|
|
536
|
+
# downloading the keras file to a temp file with a `pkl` suffix, so it needs to be replaced:
|
|
537
|
+
self._model_file = self._model_file.rsplit(".pkl", 1)[0] + ".h5"
|
|
483
538
|
# # ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS - Get the weights file:
|
|
484
539
|
elif (
|
|
485
540
|
self._model_format
|
|
486
541
|
== TFKerasModelHandler.ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS
|
|
487
542
|
):
|
|
543
|
+
# When keras tried to load it, it validates the suffix. The `artifacts.model.get_model` function is
|
|
544
|
+
# downloading the keras file to a temp file with a `pkl` suffix, so it needs to be replaced:
|
|
545
|
+
self._model_file = self._model_file.rsplit(".pkl", 1)[0] + ".json"
|
|
488
546
|
# Get the weights file:
|
|
489
547
|
self._weights_file = self._extra_data[
|
|
490
548
|
self._get_weights_file_artifact_name()
|
|
@@ -509,6 +567,17 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
509
567
|
f"'{self._model_path}'"
|
|
510
568
|
)
|
|
511
569
|
|
|
570
|
+
# ModelFormats.KERAS - Get the keras model file:
|
|
571
|
+
elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
|
|
572
|
+
self._model_file = os.path.join(
|
|
573
|
+
self._model_path, f"{self._model_name}.keras"
|
|
574
|
+
)
|
|
575
|
+
if not os.path.exists(self._model_file):
|
|
576
|
+
raise mlrun.errors.MLRunNotFoundError(
|
|
577
|
+
f"The model file '{self._model_name}.keras' was not found within the given 'model_path': "
|
|
578
|
+
f"'{self._model_path}'"
|
|
579
|
+
)
|
|
580
|
+
|
|
512
581
|
# ModelFormats.SAVED_MODEL - Get the zip file and extract it, or simply locate the directory:
|
|
513
582
|
elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
|
|
514
583
|
self._model_file = os.path.join(self._model_path, f"{self._model_name}.zip")
|
|
@@ -559,7 +628,9 @@ class TFKerasModelHandler(DLModelHandler):
|
|
|
559
628
|
# Supported types:
|
|
560
629
|
if isinstance(sample, np.ndarray):
|
|
561
630
|
return super()._read_sample(sample=sample)
|
|
562
|
-
elif isinstance(sample, tf.TensorSpec)
|
|
631
|
+
elif isinstance(sample, tf.TensorSpec) or (
|
|
632
|
+
is_keras_3() and isinstance(sample, keras.KerasTensor)
|
|
633
|
+
):
|
|
563
634
|
return Feature(
|
|
564
635
|
name=sample.name,
|
|
565
636
|
value_type=TFKerasUtils.convert_tf_dtype_to_value_type(
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
|
|
15
14
|
import tensorflow as tf
|
|
15
|
+
from packaging import version
|
|
16
16
|
from tensorflow import keras
|
|
17
17
|
|
|
18
18
|
import mlrun
|
|
@@ -117,3 +117,14 @@ class TFKerasUtils(DLUtils):
|
|
|
117
117
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
118
118
|
f"MLRun value type is not supporting the given tensorflow data type: '{tf_dtype}'."
|
|
119
119
|
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_keras_3() -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Check if the current Keras version is 3.x.
|
|
125
|
+
|
|
126
|
+
:return: True if Keras version is 3.x, False otherwise.
|
|
127
|
+
"""
|
|
128
|
+
return hasattr(keras, "__version__") and version.parse(
|
|
129
|
+
keras.__version__
|
|
130
|
+
) >= version.parse("3.0.0")
|
mlrun/launcher/base.py
CHANGED
|
@@ -82,7 +82,6 @@ class BaseLauncher(abc.ABC):
|
|
|
82
82
|
runtime: "mlrun.runtimes.base.BaseRuntime",
|
|
83
83
|
project_name: Optional[str] = "",
|
|
84
84
|
full: bool = True,
|
|
85
|
-
client_version: str = "",
|
|
86
85
|
):
|
|
87
86
|
pass
|
|
88
87
|
|
|
@@ -148,6 +147,12 @@ class BaseLauncher(abc.ABC):
|
|
|
148
147
|
self._validate_run_params(run.spec.parameters)
|
|
149
148
|
self._validate_output_path(runtime, run)
|
|
150
149
|
|
|
150
|
+
for image in [
|
|
151
|
+
runtime.spec.image,
|
|
152
|
+
getattr(runtime.spec.build, "base_image", None),
|
|
153
|
+
]:
|
|
154
|
+
mlrun.utils.helpers.warn_on_deprecated_image(image)
|
|
155
|
+
|
|
151
156
|
@staticmethod
|
|
152
157
|
def _validate_output_path(
|
|
153
158
|
runtime: "mlrun.runtimes.BaseRuntime",
|
mlrun/launcher/client.py
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import abc
|
|
15
|
-
import warnings
|
|
16
15
|
from typing import Optional
|
|
17
16
|
|
|
18
17
|
import IPython.display
|
|
@@ -37,7 +36,6 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
|
|
|
37
36
|
runtime: "mlrun.runtimes.base.BaseRuntime",
|
|
38
37
|
project_name: Optional[str] = "",
|
|
39
38
|
full: bool = True,
|
|
40
|
-
client_version: str = "",
|
|
41
39
|
):
|
|
42
40
|
runtime.try_auto_mount_based_on_config()
|
|
43
41
|
runtime._fill_credentials()
|
|
@@ -63,26 +61,7 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
|
|
|
63
61
|
):
|
|
64
62
|
image = mlrun.mlconf.function_defaults.image_by_kind.to_dict()[runtime.kind]
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
if image and "mlrun/ml-base" in image:
|
|
68
|
-
client_version = mlrun.utils.version.Version().get()["version"]
|
|
69
|
-
auto_replaced = mlrun.utils.validate_component_version_compatibility(
|
|
70
|
-
"mlrun-client", "1.10.0", mlrun_client_version=client_version
|
|
71
|
-
)
|
|
72
|
-
message = (
|
|
73
|
-
"'mlrun/ml-base' image is deprecated in 1.10.0 and will be removed in 1.12.0, "
|
|
74
|
-
"use 'mlrun/mlrun' instead."
|
|
75
|
-
)
|
|
76
|
-
if auto_replaced:
|
|
77
|
-
message += (
|
|
78
|
-
" Since your client version is >= 1.10.0, the image will be automatically "
|
|
79
|
-
"replaced with mlrun/mlrun."
|
|
80
|
-
)
|
|
81
|
-
warnings.warn(
|
|
82
|
-
message,
|
|
83
|
-
# TODO: Remove this in 1.12.0
|
|
84
|
-
FutureWarning,
|
|
85
|
-
)
|
|
64
|
+
mlrun.utils.helpers.warn_on_deprecated_image(image)
|
|
86
65
|
|
|
87
66
|
# TODO: need a better way to decide whether a function requires a build
|
|
88
67
|
if require_build and image and not runtime.spec.build.base_image:
|
mlrun/launcher/local.py
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import os
|
|
15
15
|
import pathlib
|
|
16
|
-
from os import environ
|
|
17
16
|
from typing import Callable, Optional, Union
|
|
18
17
|
|
|
19
18
|
import mlrun.common.constants as mlrun_constants
|
|
@@ -252,9 +251,6 @@ class ClientLocalLauncher(launcher.ClientBaseLauncher):
|
|
|
252
251
|
# copy the code/base-spec to the local function (for the UI and code logging)
|
|
253
252
|
fn.spec.description = runtime.spec.description
|
|
254
253
|
fn.spec.build = runtime.spec.build
|
|
255
|
-
serving_spec = getattr(runtime.spec, "serving_spec", None)
|
|
256
|
-
if serving_spec:
|
|
257
|
-
environ["SERVING_SPEC_ENV"] = serving_spec
|
|
258
254
|
|
|
259
255
|
run.spec.handler = handler
|
|
260
256
|
run.spec.reset_on_run = reset_on_run
|
|
@@ -166,13 +166,29 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
166
166
|
return result
|
|
167
167
|
|
|
168
168
|
@staticmethod
|
|
169
|
+
def _check_writer_is_up(project: "mlrun.MlrunProject") -> None:
|
|
170
|
+
try:
|
|
171
|
+
project.get_function(
|
|
172
|
+
mm_constants.MonitoringFunctionNames.WRITER, ignore_cache=True
|
|
173
|
+
)
|
|
174
|
+
except mlrun.errors.MLRunNotFoundError:
|
|
175
|
+
raise mlrun.errors.MLRunValueError(
|
|
176
|
+
"Writing outputs to the databases is blocked as the model monitoring infrastructure is disabled.\n"
|
|
177
|
+
"To unblock, enable model monitoring with `project.enable_model_monitoring()`."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
169
181
|
@contextmanager
|
|
170
182
|
def _push_to_writer(
|
|
183
|
+
cls,
|
|
171
184
|
*,
|
|
172
185
|
write_output: bool,
|
|
173
186
|
stream_profile: Optional[ds_profile.DatastoreProfile],
|
|
187
|
+
project: "mlrun.MlrunProject",
|
|
174
188
|
) -> Iterator[dict[str, list[tuple]]]:
|
|
175
189
|
endpoints_output: dict[str, list[tuple]] = defaultdict(list)
|
|
190
|
+
if write_output:
|
|
191
|
+
cls._check_writer_is_up(project)
|
|
176
192
|
try:
|
|
177
193
|
yield endpoints_output
|
|
178
194
|
finally:
|
|
@@ -220,6 +236,9 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
220
236
|
for an MLRun job.
|
|
221
237
|
This method should not be called directly.
|
|
222
238
|
"""
|
|
239
|
+
project = context.get_project_object()
|
|
240
|
+
if not project:
|
|
241
|
+
raise mlrun.errors.MLRunValueError("Could not load project from context")
|
|
223
242
|
|
|
224
243
|
if write_output and (
|
|
225
244
|
not endpoints or sample_data is not None or reference_data is not None
|
|
@@ -236,7 +255,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
236
255
|
)
|
|
237
256
|
|
|
238
257
|
with self._push_to_writer(
|
|
239
|
-
write_output=write_output, stream_profile=stream_profile
|
|
258
|
+
write_output=write_output, stream_profile=stream_profile, project=project
|
|
240
259
|
) as endpoints_output:
|
|
241
260
|
|
|
242
261
|
def call_do_tracking(event: Optional[dict] = None):
|
|
@@ -249,6 +268,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
249
268
|
event=event,
|
|
250
269
|
application_name=self.__class__.__name__,
|
|
251
270
|
context=context,
|
|
271
|
+
project=project,
|
|
252
272
|
sample_df=sample_data,
|
|
253
273
|
feature_stats=feature_stats,
|
|
254
274
|
)
|
|
@@ -137,13 +137,14 @@ class MonitoringApplicationContext:
|
|
|
137
137
|
cls,
|
|
138
138
|
context: "mlrun.MLClientCtx",
|
|
139
139
|
*,
|
|
140
|
+
project: Optional["mlrun.MlrunProject"] = None,
|
|
140
141
|
application_name: str,
|
|
141
142
|
event: dict[str, Any],
|
|
142
143
|
model_endpoint_dict: Optional[dict[str, ModelEndpoint]] = None,
|
|
143
144
|
sample_df: Optional[pd.DataFrame] = None,
|
|
144
145
|
feature_stats: Optional[FeatureStats] = None,
|
|
145
146
|
) -> "MonitoringApplicationContext":
|
|
146
|
-
project = context.get_project_object()
|
|
147
|
+
project = project or context.get_project_object()
|
|
147
148
|
if not project:
|
|
148
149
|
raise mlrun.errors.MLRunValueError("Could not load project from context")
|
|
149
150
|
logger = context.logger
|