mlrun 1.10.0rc7__py3-none-any.whl → 1.10.0rc8__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.

Files changed (34) hide show
  1. mlrun/__init__.py +3 -1
  2. mlrun/common/schemas/background_task.py +5 -0
  3. mlrun/common/schemas/model_monitoring/__init__.py +2 -0
  4. mlrun/common/schemas/model_monitoring/constants.py +16 -0
  5. mlrun/common/schemas/project.py +4 -0
  6. mlrun/common/schemas/serving.py +2 -0
  7. mlrun/config.py +11 -22
  8. mlrun/datastore/utils.py +3 -1
  9. mlrun/db/base.py +11 -10
  10. mlrun/db/httpdb.py +97 -25
  11. mlrun/db/nopdb.py +5 -4
  12. mlrun/frameworks/tf_keras/__init__.py +4 -4
  13. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +23 -20
  14. mlrun/frameworks/tf_keras/model_handler.py +69 -9
  15. mlrun/frameworks/tf_keras/utils.py +12 -1
  16. mlrun/launcher/base.py +6 -0
  17. mlrun/launcher/client.py +1 -21
  18. mlrun/projects/pipelines.py +33 -3
  19. mlrun/projects/project.py +13 -16
  20. mlrun/run.py +37 -5
  21. mlrun/runtimes/nuclio/serving.py +14 -5
  22. mlrun/serving/__init__.py +2 -0
  23. mlrun/serving/server.py +156 -26
  24. mlrun/serving/states.py +215 -18
  25. mlrun/serving/system_steps.py +391 -0
  26. mlrun/serving/v2_serving.py +9 -8
  27. mlrun/utils/helpers.py +18 -0
  28. mlrun/utils/version/version.json +2 -2
  29. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc8.dist-info}/METADATA +8 -8
  30. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc8.dist-info}/RECORD +34 -33
  31. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc8.dist-info}/WHEEL +0 -0
  32. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc8.dist-info}/entry_points.txt +0 -0
  33. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc8.dist-info}/licenses/LICENSE +0 -0
  34. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc8.dist-info}/top_level.txt +0 -0
@@ -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 an input sample:
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 = ModelFormats.SAVED_MODEL,
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'. Default: 'ModelFormats.SAVED_MODEL'.
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 its true we use it in the call:
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 = [input_layer.type_spec for input_layer in self._model.inputs]
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.type_spec for output_layer in self._model.outputs
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:
@@ -509,6 +556,17 @@ class TFKerasModelHandler(DLModelHandler):
509
556
  f"'{self._model_path}'"
510
557
  )
511
558
 
559
+ # ModelFormats.KERAS - Get the keras model file:
560
+ elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
561
+ self._model_file = os.path.join(
562
+ self._model_path, f"{self._model_name}.keras"
563
+ )
564
+ if not os.path.exists(self._model_file):
565
+ raise mlrun.errors.MLRunNotFoundError(
566
+ f"The model file '{self._model_name}.keras' was not found within the given 'model_path': "
567
+ f"'{self._model_path}'"
568
+ )
569
+
512
570
  # ModelFormats.SAVED_MODEL - Get the zip file and extract it, or simply locate the directory:
513
571
  elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
514
572
  self._model_file = os.path.join(self._model_path, f"{self._model_name}.zip")
@@ -559,7 +617,9 @@ class TFKerasModelHandler(DLModelHandler):
559
617
  # Supported types:
560
618
  if isinstance(sample, np.ndarray):
561
619
  return super()._read_sample(sample=sample)
562
- elif isinstance(sample, tf.TensorSpec):
620
+ elif isinstance(sample, tf.TensorSpec) or (
621
+ is_keras_3() and isinstance(sample, keras.KerasTensor)
622
+ ):
563
623
  return Feature(
564
624
  name=sample.name,
565
625
  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
@@ -148,6 +148,12 @@ class BaseLauncher(abc.ABC):
148
148
  self._validate_run_params(run.spec.parameters)
149
149
  self._validate_output_path(runtime, run)
150
150
 
151
+ for image in [
152
+ runtime.spec.image,
153
+ getattr(runtime.spec.build, "base_image", None),
154
+ ]:
155
+ mlrun.utils.helpers.warn_on_deprecated_image(image)
156
+
151
157
  @staticmethod
152
158
  def _validate_output_path(
153
159
  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
@@ -63,26 +62,7 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
63
62
  ):
64
63
  image = mlrun.mlconf.function_defaults.image_by_kind.to_dict()[runtime.kind]
65
64
 
66
- # Warn if user explicitly set the deprecated mlrun/ml-base image
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
- )
65
+ mlrun.utils.helpers.warn_on_deprecated_image(image)
86
66
 
87
67
  # TODO: need a better way to decide whether a function requires a build
88
68
  if require_build and image and not runtime.spec.build.base_image:
@@ -39,7 +39,12 @@ from mlrun.utils import (
39
39
 
40
40
  from ..common.helpers import parse_versioned_object_uri
41
41
  from ..config import config
42
- from ..run import _run_pipeline, retry_pipeline, wait_for_pipeline_completion
42
+ from ..run import (
43
+ _run_pipeline,
44
+ retry_pipeline,
45
+ terminate_pipeline,
46
+ wait_for_pipeline_completion,
47
+ )
43
48
  from ..runtimes.pod import AutoMountType
44
49
 
45
50
 
@@ -696,6 +701,24 @@ class _KFPRunner(_PipelineRunner):
696
701
  )
697
702
  return run_id
698
703
 
704
+ @classmethod
705
+ def terminate(
706
+ cls,
707
+ run: "_PipelineRunStatus",
708
+ project: typing.Optional["mlrun.projects.MlrunProject"] = None,
709
+ ) -> str:
710
+ project_name = project.metadata.name if project else ""
711
+ logger.info(
712
+ "Terminating pipeline",
713
+ run_id=run.run_id,
714
+ project=project_name,
715
+ )
716
+ run_id = terminate_pipeline(
717
+ run.run_id,
718
+ project=project_name,
719
+ )
720
+ return run_id
721
+
699
722
  @staticmethod
700
723
  def wait_for_completion(
701
724
  run: "_PipelineRunStatus",
@@ -1145,7 +1168,9 @@ def load_and_run_workflow(
1145
1168
  notification.when = ["running"]
1146
1169
 
1147
1170
  workflow_log_message = workflow_name or workflow_path
1148
- context.logger.info(f"Running workflow {workflow_log_message} from remote")
1171
+ context.logger.info(
1172
+ "Running workflow from remote", workflow_log_message=workflow_log_message
1173
+ )
1149
1174
  run = project.run(
1150
1175
  name=workflow_name,
1151
1176
  workflow_path=workflow_path,
@@ -1162,6 +1187,11 @@ def load_and_run_workflow(
1162
1187
  notifications=start_notifications,
1163
1188
  context=context,
1164
1189
  )
1190
+ # Patch the current run object (the workflow-runner) with the workflow-id label
1191
+ context.logger.info(
1192
+ "Associating workflow-runner with workflow ID", run_id=run.run_id
1193
+ )
1194
+ context.set_label("workflow-id", run.run_id)
1165
1195
  context.log_result(key="workflow_id", value=run.run_id)
1166
1196
  context.log_result(key="engine", value=run._engine.engine, commit=True)
1167
1197
 
@@ -1321,4 +1351,4 @@ def import_remote_project(
1321
1351
  sync_functions=True,
1322
1352
  )
1323
1353
 
1324
- context.logger.info(f"Loaded project {project.name} successfully")
1354
+ context.logger.info("Loaded project successfully", project_name=project.name)
mlrun/projects/project.py CHANGED
@@ -2518,7 +2518,6 @@ class MlrunProject(ModelObj):
2518
2518
 
2519
2519
  def enable_model_monitoring(
2520
2520
  self,
2521
- default_controller_image: str = "mlrun/mlrun",
2522
2521
  base_period: int = 10,
2523
2522
  image: str = "mlrun/mlrun",
2524
2523
  *,
@@ -2534,7 +2533,6 @@ class MlrunProject(ModelObj):
2534
2533
  The stream function goal is to monitor the log of the data stream. It is triggered when a new log entry
2535
2534
  is detected. It processes the new events into statistics that are then written to statistics databases.
2536
2535
 
2537
- :param default_controller_image: Deprecated.
2538
2536
  :param base_period: The time period in minutes in which the model monitoring controller
2539
2537
  function is triggered. By default, the base period is 10 minutes
2540
2538
  (which is also the minimum value for production environments).
@@ -2562,14 +2560,6 @@ class MlrunProject(ModelObj):
2562
2560
  background, including the histogram data drift app if selected.
2563
2561
  :param fetch_credentials_from_sys_config: If true, fetch the credentials from the system configuration.
2564
2562
  """
2565
- if default_controller_image != "mlrun/mlrun":
2566
- # TODO: Remove this in 1.10.0
2567
- warnings.warn(
2568
- "'default_controller_image' is deprecated in 1.7.0 and will be removed in 1.10.0, "
2569
- "use 'image' instead",
2570
- FutureWarning,
2571
- )
2572
- image = default_controller_image
2573
2563
  if base_period < 10:
2574
2564
  logger.warn(
2575
2565
  "enable_model_monitoring: 'base_period' < 10 minutes is not supported in production environments",
@@ -3848,7 +3838,8 @@ class MlrunProject(ModelObj):
3848
3838
  )
3849
3839
 
3850
3840
  The replication factor and timeout configuration might need to be adjusted according to your Confluent cluster
3851
- type and settings.
3841
+ type and settings. Nuclio annotations for the model monitoring infrastructure and application functions are
3842
+ supported through ``kwargs_public={"nuclio_annotations": {...}, ...}``.
3852
3843
 
3853
3844
  :param tsdb_profile_name: The datastore profile name of the time-series database to be used in model
3854
3845
  monitoring. The supported profiles are:
@@ -4278,11 +4269,17 @@ class MlrunProject(ModelObj):
4278
4269
  function = mlrun.new_function("mlrun--project--image--builder", kind="job")
4279
4270
 
4280
4271
  if self.spec.source and not self.spec.load_source_on_run:
4281
- function.with_source_archive(
4282
- source=self.spec.source,
4283
- target_dir=target_dir,
4284
- pull_at_runtime=False,
4285
- )
4272
+ if self.spec.source.startswith("db://"):
4273
+ logger.debug(
4274
+ "Project source is 'db://', which refers to metadata stored in the MLRun DB."
4275
+ " Skipping source archive setup for image build"
4276
+ )
4277
+ else:
4278
+ function.with_source_archive(
4279
+ source=self.spec.source,
4280
+ target_dir=target_dir,
4281
+ pull_at_runtime=False,
4282
+ )
4286
4283
 
4287
4284
  build = self.spec.build
4288
4285
  result = self.build_function(
mlrun/run.py CHANGED
@@ -894,7 +894,6 @@ def _run_pipeline(
894
894
  def retry_pipeline(
895
895
  run_id: str,
896
896
  project: str,
897
- namespace: Optional[str] = None,
898
897
  ) -> str:
899
898
  """Retry a pipeline run.
900
899
 
@@ -903,7 +902,6 @@ def retry_pipeline(
903
902
 
904
903
  :param run_id: ID of the pipeline run to retry.
905
904
  :param project: name of the project associated with the pipeline run.
906
- :param namespace: Optional; Kubernetes namespace to use if not the default.
907
905
 
908
906
  :returns: ID of the retried pipeline run or the ID of a cloned run if the original run is not retryable.
909
907
  :raises ValueError: If access to the remote API service is not available.
@@ -918,7 +916,6 @@ def retry_pipeline(
918
916
  pipeline_run_id = mldb.retry_pipeline(
919
917
  run_id=run_id,
920
918
  project=project,
921
- namespace=namespace,
922
919
  )
923
920
  if pipeline_run_id == run_id:
924
921
  logger.info(
@@ -931,6 +928,35 @@ def retry_pipeline(
931
928
  return pipeline_run_id
932
929
 
933
930
 
931
+ def terminate_pipeline(
932
+ run_id: str,
933
+ project: str,
934
+ ) -> str:
935
+ """Terminate a pipeline run.
936
+
937
+ This function terminates a running pipeline with the specified run ID. If the run is not in a
938
+ terminable state, an error is raised.
939
+
940
+ :param run_id: ID of the pipeline run to terminate.
941
+ :param project: name of the project associated with the pipeline run.
942
+
943
+ :returns: ID of the terminate pipeline run background task.
944
+ :raises ValueError: If access to the remote API service is not available.
945
+ """
946
+ mldb = mlrun.db.get_run_db()
947
+ if mldb.kind != "http":
948
+ raise ValueError(
949
+ "Terminating a pipeline requires access to remote API service. "
950
+ "Please set the dbpath URL."
951
+ )
952
+
953
+ pipeline_run_task = mldb.terminate_pipeline(
954
+ run_id=run_id,
955
+ project=project,
956
+ )
957
+ return pipeline_run_task["metadata"]["id"]
958
+
959
+
934
960
  def wait_for_pipeline_completion(
935
961
  run_id,
936
962
  timeout=60 * 60,
@@ -997,7 +1023,10 @@ def wait_for_pipeline_completion(
997
1023
  _wait_for_pipeline_completion,
998
1024
  )
999
1025
  else:
1000
- client = mlrun_pipelines.utils.get_client(namespace=namespace)
1026
+ client = mlrun_pipelines.utils.get_client(
1027
+ logger=logger,
1028
+ namespace=namespace,
1029
+ )
1001
1030
  resp = client.wait_for_run_completion(run_id, timeout)
1002
1031
  if resp:
1003
1032
  resp = resp.to_dict()
@@ -1058,7 +1087,10 @@ def get_pipeline(
1058
1087
  )
1059
1088
 
1060
1089
  else:
1061
- client = mlrun_pipelines.utils.get_client(namespace=namespace)
1090
+ client = mlrun_pipelines.utils.get_client(
1091
+ logger=logger,
1092
+ namespace=namespace,
1093
+ )
1062
1094
  resp = client.get_run(run_id)
1063
1095
  if resp:
1064
1096
  resp = resp.to_dict()
@@ -11,7 +11,6 @@
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
- import copy
15
14
  import json
16
15
  import os
17
16
  import warnings
@@ -482,6 +481,7 @@ class ServingRuntime(RemoteRuntime):
482
481
  state = TaskStep(
483
482
  class_name,
484
483
  class_args,
484
+ name=key,
485
485
  handler=handler,
486
486
  function=child_function,
487
487
  model_endpoint_creation_strategy=creation_strategy,
@@ -751,13 +751,10 @@ class ServingRuntime(RemoteRuntime):
751
751
  set_paths(workdir)
752
752
  os.chdir(workdir)
753
753
 
754
- system_graph = None
755
- if isinstance(self.spec.graph, RootFlowStep):
756
- system_graph = add_system_steps_to_graph(copy.deepcopy(self.spec.graph))
757
754
  server = create_graph_server(
758
755
  parameters=self.spec.parameters,
759
756
  load_mode=self.spec.load_mode,
760
- graph=system_graph or self.spec.graph,
757
+ graph=self.spec.graph,
761
758
  verbose=self.verbose,
762
759
  current_function=current_function,
763
760
  graph_initializer=self.spec.graph_initializer,
@@ -778,6 +775,18 @@ class ServingRuntime(RemoteRuntime):
778
775
  monitoring_mock=self.spec.track_models,
779
776
  )
780
777
 
778
+ if (
779
+ isinstance(self.spec.graph, RootFlowStep)
780
+ and self.spec.graph.include_monitored_step()
781
+ ):
782
+ server.graph = add_system_steps_to_graph(
783
+ server.project,
784
+ server.graph,
785
+ self.spec.track_models,
786
+ server.context,
787
+ self.spec,
788
+ )
789
+
781
790
  if workdir:
782
791
  os.chdir(old_workdir)
783
792
 
mlrun/serving/__init__.py CHANGED
@@ -27,6 +27,7 @@ __all__ = [
27
27
  "ModelRunner",
28
28
  "Model",
29
29
  "ModelSelector",
30
+ "MonitoredStep",
30
31
  ]
31
32
 
32
33
  from .routers import ModelRouter, VotingEnsemble # noqa
@@ -45,6 +46,7 @@ from .states import (
45
46
  ModelRunner,
46
47
  Model,
47
48
  ModelSelector,
49
+ MonitoredStep,
48
50
  ) # noqa
49
51
  from .v1_serving import MLModelServer, new_v1_model_server # noqa
50
52
  from .v2_serving import V2ModelServer # noqa