mlrun 1.10.0rc16__py3-none-any.whl → 1.10.1rc4__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 +22 -2
- mlrun/artifacts/document.py +6 -1
- mlrun/artifacts/llm_prompt.py +21 -15
- mlrun/artifacts/model.py +3 -3
- mlrun/common/constants.py +9 -0
- mlrun/common/formatters/artifact.py +1 -0
- mlrun/common/model_monitoring/helpers.py +86 -0
- mlrun/common/schemas/__init__.py +2 -0
- mlrun/common/schemas/auth.py +2 -0
- mlrun/common/schemas/function.py +10 -0
- mlrun/common/schemas/hub.py +30 -18
- mlrun/common/schemas/model_monitoring/__init__.py +2 -0
- mlrun/common/schemas/model_monitoring/constants.py +30 -6
- mlrun/common/schemas/model_monitoring/functions.py +13 -4
- mlrun/common/schemas/model_monitoring/model_endpoints.py +11 -0
- mlrun/common/schemas/pipeline.py +1 -1
- mlrun/common/schemas/serving.py +3 -0
- mlrun/common/schemas/workflow.py +1 -0
- mlrun/common/secrets.py +22 -1
- mlrun/config.py +34 -21
- mlrun/datastore/__init__.py +11 -3
- mlrun/datastore/azure_blob.py +162 -47
- mlrun/datastore/base.py +265 -7
- mlrun/datastore/datastore.py +10 -5
- mlrun/datastore/datastore_profile.py +61 -5
- mlrun/datastore/model_provider/huggingface_provider.py +367 -0
- mlrun/datastore/model_provider/mock_model_provider.py +87 -0
- mlrun/datastore/model_provider/model_provider.py +211 -74
- mlrun/datastore/model_provider/openai_provider.py +243 -71
- mlrun/datastore/s3.py +24 -2
- mlrun/datastore/store_resources.py +4 -4
- mlrun/datastore/storeytargets.py +2 -3
- mlrun/datastore/utils.py +15 -3
- mlrun/db/base.py +27 -19
- mlrun/db/httpdb.py +57 -48
- mlrun/db/nopdb.py +25 -10
- mlrun/execution.py +55 -13
- mlrun/hub/__init__.py +15 -0
- mlrun/hub/module.py +181 -0
- mlrun/k8s_utils.py +105 -16
- mlrun/launcher/base.py +13 -6
- mlrun/launcher/local.py +2 -0
- mlrun/model.py +9 -3
- mlrun/model_monitoring/api.py +66 -27
- mlrun/model_monitoring/applications/__init__.py +1 -1
- mlrun/model_monitoring/applications/base.py +388 -138
- mlrun/model_monitoring/applications/context.py +2 -4
- mlrun/model_monitoring/applications/results.py +4 -7
- mlrun/model_monitoring/controller.py +239 -101
- mlrun/model_monitoring/db/_schedules.py +36 -13
- mlrun/model_monitoring/db/_stats.py +4 -3
- mlrun/model_monitoring/db/tsdb/base.py +29 -9
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +4 -5
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +154 -50
- mlrun/model_monitoring/db/tsdb/tdengine/writer_graph_steps.py +51 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +17 -4
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +245 -51
- mlrun/model_monitoring/helpers.py +28 -5
- mlrun/model_monitoring/stream_processing.py +45 -14
- mlrun/model_monitoring/writer.py +220 -1
- mlrun/platforms/__init__.py +3 -2
- mlrun/platforms/iguazio.py +7 -3
- mlrun/projects/operations.py +16 -11
- mlrun/projects/pipelines.py +2 -2
- mlrun/projects/project.py +157 -69
- mlrun/run.py +97 -20
- mlrun/runtimes/__init__.py +18 -0
- mlrun/runtimes/base.py +14 -6
- mlrun/runtimes/daskjob.py +1 -0
- mlrun/runtimes/local.py +5 -2
- mlrun/runtimes/mounts.py +20 -2
- mlrun/runtimes/nuclio/__init__.py +1 -0
- mlrun/runtimes/nuclio/application/application.py +147 -17
- mlrun/runtimes/nuclio/function.py +72 -27
- mlrun/runtimes/nuclio/serving.py +102 -20
- mlrun/runtimes/pod.py +213 -21
- mlrun/runtimes/utils.py +49 -9
- mlrun/secrets.py +54 -13
- mlrun/serving/remote.py +79 -6
- mlrun/serving/routers.py +23 -41
- mlrun/serving/server.py +230 -40
- mlrun/serving/states.py +605 -232
- mlrun/serving/steps.py +62 -0
- mlrun/serving/system_steps.py +136 -81
- mlrun/serving/v2_serving.py +9 -10
- mlrun/utils/helpers.py +215 -83
- mlrun/utils/logger.py +3 -1
- mlrun/utils/notifications/notification/base.py +18 -0
- mlrun/utils/notifications/notification/git.py +2 -4
- mlrun/utils/notifications/notification/mail.py +38 -15
- mlrun/utils/notifications/notification/slack.py +2 -4
- mlrun/utils/notifications/notification/webhook.py +2 -5
- mlrun/utils/notifications/notification_pusher.py +1 -1
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.10.0rc16.dist-info → mlrun-1.10.1rc4.dist-info}/METADATA +51 -50
- {mlrun-1.10.0rc16.dist-info → mlrun-1.10.1rc4.dist-info}/RECORD +100 -95
- mlrun/api/schemas/__init__.py +0 -259
- {mlrun-1.10.0rc16.dist-info → mlrun-1.10.1rc4.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc16.dist-info → mlrun-1.10.1rc4.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc16.dist-info → mlrun-1.10.1rc4.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc16.dist-info → mlrun-1.10.1rc4.dist-info}/top_level.txt +0 -0
mlrun/runtimes/nuclio/serving.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
16
|
import warnings
|
|
17
|
+
from base64 import b64decode
|
|
17
18
|
from copy import deepcopy
|
|
18
19
|
from typing import Optional, Union
|
|
19
20
|
|
|
@@ -22,6 +23,8 @@ from nuclio import KafkaTrigger
|
|
|
22
23
|
|
|
23
24
|
import mlrun
|
|
24
25
|
import mlrun.common.schemas as schemas
|
|
26
|
+
import mlrun.common.secrets
|
|
27
|
+
import mlrun.datastore.datastore_profile as ds_profile
|
|
25
28
|
from mlrun.datastore import get_kafka_brokers_from_dict, parse_kafka_url
|
|
26
29
|
from mlrun.model import ObjectList
|
|
27
30
|
from mlrun.runtimes.function_reference import FunctionReference
|
|
@@ -280,7 +283,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
280
283
|
:param exist_ok: - allow overriding existing topology
|
|
281
284
|
:param class_args: - optional, router/flow class init args
|
|
282
285
|
|
|
283
|
-
:return graph object (fn.spec.graph)
|
|
286
|
+
:return: graph object (fn.spec.graph)
|
|
284
287
|
"""
|
|
285
288
|
topology = topology or StepKinds.router
|
|
286
289
|
if self.spec.graph and not exist_ok:
|
|
@@ -393,7 +396,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
393
396
|
outputs: Optional[list[str]] = None,
|
|
394
397
|
**class_args,
|
|
395
398
|
):
|
|
396
|
-
"""
|
|
399
|
+
"""Add ml model and/or route to the function.
|
|
397
400
|
|
|
398
401
|
Example, create a function (from the notebook), add a model class, and deploy::
|
|
399
402
|
|
|
@@ -401,7 +404,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
401
404
|
fn.add_model("boost", model_path, model_class="MyClass", my_arg=5)
|
|
402
405
|
fn.deploy()
|
|
403
406
|
|
|
404
|
-
|
|
407
|
+
Only works with router topology. For nested topologies (model under router under flow)
|
|
405
408
|
need to add router to flow and use router.add_route()
|
|
406
409
|
|
|
407
410
|
:param key: model api key (or name:version), will determine the relative url/path
|
|
@@ -414,18 +417,19 @@ class ServingRuntime(RemoteRuntime):
|
|
|
414
417
|
with multiple router steps)
|
|
415
418
|
:param child_function: child function name, when the model runs in a child function
|
|
416
419
|
:param creation_strategy: Strategy for creating or updating the model endpoint:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
model
|
|
420
|
+
|
|
421
|
+
* **overwrite**: If model endpoints with the same name exist, delete the `latest`
|
|
422
|
+
one. Create a new model endpoint entry and set it as `latest`.
|
|
423
|
+
|
|
424
|
+
* **inplace** (default): If model endpoints with the same name exist, update the
|
|
425
|
+
`latest` entry. Otherwise, create a new entry.
|
|
426
|
+
|
|
427
|
+
* **archive**: If model endpoints with the same name exist, preserve them.
|
|
428
|
+
Create a new model endpoint with the same name and set it to `latest`.
|
|
429
|
+
|
|
430
|
+
:param outputs: list of the model outputs (e.g. labels), if provided will override the outputs that were
|
|
431
|
+
configured in the model artifact. Note that those outputs need to be equal to the
|
|
432
|
+
model serving function outputs (length, and order).
|
|
429
433
|
:param class_args: extra kwargs to pass to the model serving class __init__
|
|
430
434
|
(can be read in the model using .get_param(key) method)
|
|
431
435
|
"""
|
|
@@ -518,7 +522,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
518
522
|
:param requirements: py package requirements file path OR list of packages
|
|
519
523
|
:param kind: mlrun function/runtime kind
|
|
520
524
|
|
|
521
|
-
:return function object
|
|
525
|
+
:return: function object
|
|
522
526
|
"""
|
|
523
527
|
function_reference = FunctionReference(
|
|
524
528
|
url,
|
|
@@ -633,7 +637,12 @@ class ServingRuntime(RemoteRuntime):
|
|
|
633
637
|
|
|
634
638
|
:returns: The Runtime (function) object
|
|
635
639
|
"""
|
|
636
|
-
|
|
640
|
+
if kind == "azure_vault" and isinstance(source, dict):
|
|
641
|
+
candidate_secret_name = (source.get("k8s_secret") or "").strip()
|
|
642
|
+
if candidate_secret_name:
|
|
643
|
+
mlrun.common.secrets.validate_not_forbidden_secret(
|
|
644
|
+
candidate_secret_name
|
|
645
|
+
)
|
|
637
646
|
if kind == "vault" and isinstance(source, list):
|
|
638
647
|
source = {"project": self.metadata.project, "secrets": source}
|
|
639
648
|
|
|
@@ -657,6 +666,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
657
666
|
:param builder_env: env vars dict for source archive config/credentials e.g. builder_env={"GIT_TOKEN": token}
|
|
658
667
|
:param force_build: set True for force building the image
|
|
659
668
|
"""
|
|
669
|
+
|
|
660
670
|
load_mode = self.spec.load_mode
|
|
661
671
|
if load_mode and load_mode not in ["sync", "async"]:
|
|
662
672
|
raise ValueError(f"illegal model loading mode {load_mode}")
|
|
@@ -677,6 +687,21 @@ class ServingRuntime(RemoteRuntime):
|
|
|
677
687
|
f"function {function} is used in steps and is not defined, "
|
|
678
688
|
"use the .add_child_function() to specify child function attributes"
|
|
679
689
|
)
|
|
690
|
+
if (
|
|
691
|
+
isinstance(self.spec.graph, RootFlowStep)
|
|
692
|
+
and any(
|
|
693
|
+
isinstance(step_type, mlrun.serving.states.ModelRunnerStep)
|
|
694
|
+
for step_type in self.spec.graph.steps.values()
|
|
695
|
+
)
|
|
696
|
+
and self.spec.build.functionSourceCode
|
|
697
|
+
):
|
|
698
|
+
# Add import for LLModel
|
|
699
|
+
decoded_code = b64decode(self.spec.build.functionSourceCode).decode("utf-8")
|
|
700
|
+
import_llmodel_code = "\nfrom mlrun.serving.states import LLModel\n"
|
|
701
|
+
if import_llmodel_code not in decoded_code:
|
|
702
|
+
decoded_code += import_llmodel_code
|
|
703
|
+
encoded_code = mlrun.utils.helpers.encode_user_code(decoded_code)
|
|
704
|
+
self.spec.build.functionSourceCode = encoded_code
|
|
680
705
|
|
|
681
706
|
# Handle secret processing before handling child functions, since secrets are transferred to them
|
|
682
707
|
if self.spec.secret_sources:
|
|
@@ -740,6 +765,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
740
765
|
current_function="*",
|
|
741
766
|
track_models=False,
|
|
742
767
|
workdir=None,
|
|
768
|
+
stream_profile: Optional[ds_profile.DatastoreProfile] = None,
|
|
743
769
|
**kwargs,
|
|
744
770
|
) -> GraphServer:
|
|
745
771
|
"""create mock server object for local testing/emulation
|
|
@@ -748,6 +774,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
748
774
|
:param current_function: specify if you want to simulate a child function, * for all functions
|
|
749
775
|
:param track_models: allow model tracking (disabled by default in the mock server)
|
|
750
776
|
:param workdir: working directory to locate the source code (if not the current one)
|
|
777
|
+
:param stream_profile: stream profile to use for the mock server output stream.
|
|
751
778
|
"""
|
|
752
779
|
|
|
753
780
|
# set the namespaces/modules to look for the steps code in
|
|
@@ -787,6 +814,7 @@ class ServingRuntime(RemoteRuntime):
|
|
|
787
814
|
logger=logger,
|
|
788
815
|
is_mock=True,
|
|
789
816
|
monitoring_mock=self.spec.track_models,
|
|
817
|
+
stream_profile=stream_profile,
|
|
790
818
|
)
|
|
791
819
|
|
|
792
820
|
server.graph = add_system_steps_to_graph(
|
|
@@ -835,8 +863,20 @@ class ServingRuntime(RemoteRuntime):
|
|
|
835
863
|
)
|
|
836
864
|
self._mock_server = self.to_mock_server()
|
|
837
865
|
|
|
838
|
-
def to_job(self) -> KubejobRuntime:
|
|
839
|
-
"""Convert this ServingRuntime to a KubejobRuntime, so that the graph can be run as a standalone job.
|
|
866
|
+
def to_job(self, func_name: Optional[str] = None) -> KubejobRuntime:
|
|
867
|
+
"""Convert this ServingRuntime to a KubejobRuntime, so that the graph can be run as a standalone job.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
func_name: Optional custom name for the job function. If not provided, automatically
|
|
871
|
+
appends '-batch' suffix to the serving function name to prevent database collision.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
KubejobRuntime configured to execute the serving graph as a batch job.
|
|
875
|
+
|
|
876
|
+
Note:
|
|
877
|
+
The job will have a different name than the serving function to prevent database collision.
|
|
878
|
+
The original serving function remains unchanged and can still be invoked after running the job.
|
|
879
|
+
"""
|
|
840
880
|
if self.spec.function_refs:
|
|
841
881
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
842
882
|
f"Cannot convert function '{self.metadata.name}' to a job because it has child functions"
|
|
@@ -870,8 +910,50 @@ class ServingRuntime(RemoteRuntime):
|
|
|
870
910
|
parameters=self.spec.parameters,
|
|
871
911
|
graph=self.spec.graph,
|
|
872
912
|
)
|
|
913
|
+
|
|
914
|
+
job_metadata = deepcopy(self.metadata)
|
|
915
|
+
original_name = job_metadata.name
|
|
916
|
+
|
|
917
|
+
if func_name:
|
|
918
|
+
# User provided explicit job name
|
|
919
|
+
job_metadata.name = func_name
|
|
920
|
+
logger.debug(
|
|
921
|
+
"Creating job from serving function with custom name",
|
|
922
|
+
new_name=func_name,
|
|
923
|
+
)
|
|
924
|
+
else:
|
|
925
|
+
job_metadata.name, was_renamed, suffix = (
|
|
926
|
+
mlrun.utils.helpers.ensure_batch_job_suffix(job_metadata.name)
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
# Check if the resulting name exceeds Kubernetes length limit
|
|
930
|
+
if (
|
|
931
|
+
len(job_metadata.name)
|
|
932
|
+
> mlrun.common.constants.K8S_DNS_1123_LABEL_MAX_LENGTH
|
|
933
|
+
):
|
|
934
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
935
|
+
f"Cannot convert serving function '{original_name}' to batch job: "
|
|
936
|
+
f"the resulting name '{job_metadata.name}' ({len(job_metadata.name)} characters) "
|
|
937
|
+
f"exceeds Kubernetes limit of {mlrun.common.constants.K8S_DNS_1123_LABEL_MAX_LENGTH} characters. "
|
|
938
|
+
f"Please provide a custom name via the func_name parameter, "
|
|
939
|
+
f"with at most {mlrun.common.constants.K8S_DNS_1123_LABEL_MAX_LENGTH} characters."
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
if was_renamed:
|
|
943
|
+
logger.info(
|
|
944
|
+
"Creating job from serving function (auto-appended suffix to prevent collision)",
|
|
945
|
+
new_name=job_metadata.name,
|
|
946
|
+
suffix=suffix,
|
|
947
|
+
)
|
|
948
|
+
else:
|
|
949
|
+
logger.debug(
|
|
950
|
+
"Creating job from serving function (name already has suffix)",
|
|
951
|
+
name=original_name,
|
|
952
|
+
suffix=suffix,
|
|
953
|
+
)
|
|
954
|
+
|
|
873
955
|
job = KubejobRuntime(
|
|
874
956
|
spec=spec,
|
|
875
|
-
metadata=
|
|
957
|
+
metadata=job_metadata,
|
|
876
958
|
)
|
|
877
959
|
return job
|
mlrun/runtimes/pod.py
CHANGED
|
@@ -17,14 +17,17 @@ import os
|
|
|
17
17
|
import re
|
|
18
18
|
import time
|
|
19
19
|
import typing
|
|
20
|
+
import warnings
|
|
20
21
|
from collections.abc import Iterable
|
|
21
22
|
from enum import Enum
|
|
23
|
+
from typing import Optional
|
|
22
24
|
|
|
23
25
|
import dotenv
|
|
24
26
|
import kubernetes.client as k8s_client
|
|
25
27
|
from kubernetes.client import V1Volume, V1VolumeMount
|
|
26
28
|
|
|
27
29
|
import mlrun.common.constants
|
|
30
|
+
import mlrun.common.secrets
|
|
28
31
|
import mlrun.errors
|
|
29
32
|
import mlrun.runtimes.mounts
|
|
30
33
|
import mlrun.utils.regex
|
|
@@ -35,6 +38,7 @@ from mlrun.common.schemas import (
|
|
|
35
38
|
|
|
36
39
|
from ..config import config as mlconf
|
|
37
40
|
from ..k8s_utils import (
|
|
41
|
+
generate_preemptible_nodes_affinity_terms,
|
|
38
42
|
validate_node_selectors,
|
|
39
43
|
)
|
|
40
44
|
from ..utils import logger, update_in
|
|
@@ -107,6 +111,7 @@ class KubeResourceSpec(FunctionSpec):
|
|
|
107
111
|
"track_models",
|
|
108
112
|
"parameters",
|
|
109
113
|
"graph",
|
|
114
|
+
"filename",
|
|
110
115
|
]
|
|
111
116
|
_default_fields_to_strip = FunctionSpec._default_fields_to_strip + [
|
|
112
117
|
"volumes",
|
|
@@ -705,19 +710,45 @@ class KubeResource(BaseRuntime):
|
|
|
705
710
|
def spec(self, spec):
|
|
706
711
|
self._spec = self._verify_dict(spec, "spec", KubeResourceSpec)
|
|
707
712
|
|
|
708
|
-
def set_env_from_secret(
|
|
709
|
-
|
|
710
|
-
|
|
713
|
+
def set_env_from_secret(
|
|
714
|
+
self,
|
|
715
|
+
name: str,
|
|
716
|
+
secret: Optional[str] = None,
|
|
717
|
+
secret_key: Optional[str] = None,
|
|
718
|
+
):
|
|
719
|
+
"""
|
|
720
|
+
Set an environment variable from a Kubernetes Secret.
|
|
721
|
+
Client-side guard forbids MLRun internal auth/project secrets; no-op on API.
|
|
722
|
+
"""
|
|
723
|
+
mlrun.common.secrets.validate_not_forbidden_secret(secret)
|
|
724
|
+
key = secret_key or name
|
|
711
725
|
value_from = k8s_client.V1EnvVarSource(
|
|
712
|
-
secret_key_ref=k8s_client.V1SecretKeySelector(name=secret, key=
|
|
726
|
+
secret_key_ref=k8s_client.V1SecretKeySelector(name=secret, key=key)
|
|
713
727
|
)
|
|
714
|
-
return self._set_env(name, value_from=value_from)
|
|
728
|
+
return self._set_env(name=name, value_from=value_from)
|
|
729
|
+
|
|
730
|
+
def set_env(
|
|
731
|
+
self,
|
|
732
|
+
name: str,
|
|
733
|
+
value: Optional[str] = None,
|
|
734
|
+
value_from: Optional[typing.Any] = None,
|
|
735
|
+
):
|
|
736
|
+
"""
|
|
737
|
+
Set an environment variable.
|
|
738
|
+
If value comes from a Secret, validate on client-side only.
|
|
739
|
+
"""
|
|
740
|
+
if value_from is not None:
|
|
741
|
+
secret_name = self._extract_secret_name_from_value_from(
|
|
742
|
+
value_from=value_from
|
|
743
|
+
)
|
|
744
|
+
if secret_name:
|
|
745
|
+
mlrun.common.secrets.validate_not_forbidden_secret(secret_name)
|
|
746
|
+
return self._set_env(name=name, value_from=value_from)
|
|
715
747
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
return self._set_env(name, value_from=value_from)
|
|
748
|
+
# Plain literal value path
|
|
749
|
+
return self._set_env(
|
|
750
|
+
name=name, value=(str(value) if value is not None else None)
|
|
751
|
+
)
|
|
721
752
|
|
|
722
753
|
def with_annotations(self, annotations: dict):
|
|
723
754
|
"""set a key/value annotations in the metadata of the pod"""
|
|
@@ -874,6 +905,133 @@ class KubeResource(BaseRuntime):
|
|
|
874
905
|
"""
|
|
875
906
|
self.spec.with_requests(mem, cpu, patch=patch)
|
|
876
907
|
|
|
908
|
+
@staticmethod
|
|
909
|
+
def detect_preemptible_node_selector(node_selector: dict[str, str]) -> list[str]:
|
|
910
|
+
"""
|
|
911
|
+
Check whether any provided node selector matches preemptible selectors.
|
|
912
|
+
|
|
913
|
+
:param node_selector: User-provided node selector mapping.
|
|
914
|
+
:return: List of `"key='value'"` strings that match a preemptible selector.
|
|
915
|
+
"""
|
|
916
|
+
preemptible_node_selector = mlconf.get_preemptible_node_selector()
|
|
917
|
+
|
|
918
|
+
return [
|
|
919
|
+
f"'{key}': '{val}'"
|
|
920
|
+
for key, val in node_selector.items()
|
|
921
|
+
if preemptible_node_selector.get(key) == val
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
def detect_preemptible_tolerations(
|
|
925
|
+
self, tolerations: list[k8s_client.V1Toleration]
|
|
926
|
+
) -> list[str]:
|
|
927
|
+
"""
|
|
928
|
+
Check whether any provided toleration matches preemptible tolerations.
|
|
929
|
+
|
|
930
|
+
:param tolerations: User-provided tolerations.
|
|
931
|
+
:return: List of formatted toleration strings that are considered preemptible.
|
|
932
|
+
"""
|
|
933
|
+
preemptible_tolerations = [
|
|
934
|
+
k8s_client.V1Toleration(
|
|
935
|
+
key=toleration.get("key"),
|
|
936
|
+
value=toleration.get("value"),
|
|
937
|
+
effect=toleration.get("effect"),
|
|
938
|
+
)
|
|
939
|
+
for toleration in mlconf.get_preemptible_tolerations()
|
|
940
|
+
]
|
|
941
|
+
|
|
942
|
+
def _format_toleration(toleration):
|
|
943
|
+
return f"'{toleration.key}'='{toleration.value}' (effect: '{toleration.effect}')"
|
|
944
|
+
|
|
945
|
+
return [
|
|
946
|
+
_format_toleration(toleration)
|
|
947
|
+
for toleration in tolerations
|
|
948
|
+
if toleration in preemptible_tolerations
|
|
949
|
+
]
|
|
950
|
+
|
|
951
|
+
def detect_preemptible_affinity(self, affinity: k8s_client.V1Affinity) -> list[str]:
|
|
952
|
+
"""
|
|
953
|
+
Check whether any provided affinity rules match preemptible affinity configs.
|
|
954
|
+
|
|
955
|
+
:param affinity: User-provided affinity object.
|
|
956
|
+
:return: List of formatted expressions that overlap with preemptible terms.
|
|
957
|
+
"""
|
|
958
|
+
preemptible_affinity_terms = generate_preemptible_nodes_affinity_terms()
|
|
959
|
+
conflicting_affinities = []
|
|
960
|
+
|
|
961
|
+
if (
|
|
962
|
+
affinity
|
|
963
|
+
and affinity.node_affinity
|
|
964
|
+
and affinity.node_affinity.required_during_scheduling_ignored_during_execution
|
|
965
|
+
):
|
|
966
|
+
user_terms = affinity.node_affinity.required_during_scheduling_ignored_during_execution.node_selector_terms
|
|
967
|
+
for user_term in user_terms:
|
|
968
|
+
user_expressions = {
|
|
969
|
+
(expr.key, expr.operator, tuple(expr.values or []))
|
|
970
|
+
for expr in user_term.match_expressions or []
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
for preemptible_term in preemptible_affinity_terms:
|
|
974
|
+
preemptible_expressions = {
|
|
975
|
+
(expr.key, expr.operator, tuple(expr.values or []))
|
|
976
|
+
for expr in preemptible_term.match_expressions or []
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
# Ensure operators match and preemptible expressions are present
|
|
980
|
+
common_exprs = user_expressions & preemptible_expressions
|
|
981
|
+
if common_exprs:
|
|
982
|
+
formatted = ", ".join(
|
|
983
|
+
f"'{key} {operator} {list(values)}'"
|
|
984
|
+
for key, operator, values in common_exprs
|
|
985
|
+
)
|
|
986
|
+
conflicting_affinities.append(formatted)
|
|
987
|
+
return conflicting_affinities
|
|
988
|
+
|
|
989
|
+
def raise_preemptible_warning(
|
|
990
|
+
self,
|
|
991
|
+
node_selector: typing.Optional[dict[str, str]],
|
|
992
|
+
tolerations: typing.Optional[list[k8s_client.V1Toleration]],
|
|
993
|
+
affinity: typing.Optional[k8s_client.V1Affinity],
|
|
994
|
+
) -> None:
|
|
995
|
+
"""
|
|
996
|
+
Detect conflicts and emit a single consolidated warning if needed.
|
|
997
|
+
|
|
998
|
+
:param node_selector: User-provided node selector.
|
|
999
|
+
:param tolerations: User-provided tolerations.
|
|
1000
|
+
:param affinity: User-provided affinity.
|
|
1001
|
+
:warns: PreemptionWarning - Emitted when any of the provided selectors,
|
|
1002
|
+
tolerations, or affinity terms match the configured preemptible
|
|
1003
|
+
settings. The message lists the conflicting items.
|
|
1004
|
+
"""
|
|
1005
|
+
conflict_messages = []
|
|
1006
|
+
|
|
1007
|
+
if node_selector:
|
|
1008
|
+
ns_conflicts = ", ".join(
|
|
1009
|
+
self.detect_preemptible_node_selector(node_selector)
|
|
1010
|
+
)
|
|
1011
|
+
if ns_conflicts:
|
|
1012
|
+
conflict_messages.append(f"Node selectors: {ns_conflicts}")
|
|
1013
|
+
|
|
1014
|
+
if tolerations:
|
|
1015
|
+
tol_conflicts = ", ".join(self.detect_preemptible_tolerations(tolerations))
|
|
1016
|
+
if tol_conflicts:
|
|
1017
|
+
conflict_messages.append(f"Tolerations: {tol_conflicts}")
|
|
1018
|
+
|
|
1019
|
+
if affinity:
|
|
1020
|
+
affinity_conflicts = ", ".join(self.detect_preemptible_affinity(affinity))
|
|
1021
|
+
if affinity_conflicts:
|
|
1022
|
+
conflict_messages.append(f"Affinity: {affinity_conflicts}")
|
|
1023
|
+
|
|
1024
|
+
if conflict_messages:
|
|
1025
|
+
warning_componentes = "; \n".join(conflict_messages)
|
|
1026
|
+
warnings.warn(
|
|
1027
|
+
f"Warning: based on MLRun's preemptible node configuration, the following components \n"
|
|
1028
|
+
f"may be removed or adjusted at runtime:\n"
|
|
1029
|
+
f"{warning_componentes}.\n"
|
|
1030
|
+
"This adjustment depends on the function's preemption mode. \n"
|
|
1031
|
+
"The list of potential adjusted preemptible selectors can be viewed here: "
|
|
1032
|
+
"mlrun.mlconf.get_preemptible_node_selector() and mlrun.mlconf.get_preemptible_tolerations()."
|
|
1033
|
+
)
|
|
1034
|
+
|
|
877
1035
|
def with_node_selection(
|
|
878
1036
|
self,
|
|
879
1037
|
node_name: typing.Optional[str] = None,
|
|
@@ -882,18 +1040,26 @@ class KubeResource(BaseRuntime):
|
|
|
882
1040
|
tolerations: typing.Optional[list[k8s_client.V1Toleration]] = None,
|
|
883
1041
|
):
|
|
884
1042
|
"""
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
:param
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1043
|
+
Configure Kubernetes node scheduling for this function.
|
|
1044
|
+
|
|
1045
|
+
Updates one or more scheduling hints: exact node pinning, label-based selection,
|
|
1046
|
+
affinity/anti-affinity rules, and taint tolerations. Passing ``None`` leaves the
|
|
1047
|
+
current value unchanged; pass an empty dict/list (e.g., ``{}``, ``[]``) to clear.
|
|
1048
|
+
|
|
1049
|
+
:param node_name: Exact Kubernetes node name to pin the pod to.
|
|
1050
|
+
:param node_selector: Mapping of label selectors. Use ``{}`` to clear.
|
|
1051
|
+
:param affinity: :class:`kubernetes.client.V1Affinity` constraints.
|
|
1052
|
+
:param tolerations: List of :class:`kubernetes.client.V1Toleration`. Use ``[]`` to clear.
|
|
1053
|
+
:warns: PreemptionWarning - Emitted if provided selectors/tolerations/affinity
|
|
1054
|
+
conflict with the function's preemption mode.
|
|
1055
|
+
|
|
1056
|
+
Example usage:
|
|
1057
|
+
Prefer a GPU pool and allow scheduling on spot nodes::
|
|
896
1058
|
|
|
1059
|
+
job.with_node_selection(
|
|
1060
|
+
node_selector={"nodepool": "gpu"},
|
|
1061
|
+
tolerations=[k8s_client.V1Toleration(key="spot", operator="Exists")],
|
|
1062
|
+
)
|
|
897
1063
|
"""
|
|
898
1064
|
if node_name:
|
|
899
1065
|
self.spec.node_name = node_name
|
|
@@ -904,6 +1070,11 @@ class KubeResource(BaseRuntime):
|
|
|
904
1070
|
self.spec.affinity = affinity
|
|
905
1071
|
if tolerations is not None:
|
|
906
1072
|
self.spec.tolerations = tolerations
|
|
1073
|
+
self.raise_preemptible_warning(
|
|
1074
|
+
node_selector=self.spec.node_selector,
|
|
1075
|
+
tolerations=self.spec.tolerations,
|
|
1076
|
+
affinity=self.spec.affinity,
|
|
1077
|
+
)
|
|
907
1078
|
|
|
908
1079
|
def with_priority_class(self, name: typing.Optional[str] = None):
|
|
909
1080
|
"""
|
|
@@ -1223,6 +1394,27 @@ class KubeResource(BaseRuntime):
|
|
|
1223
1394
|
|
|
1224
1395
|
return self.status.state
|
|
1225
1396
|
|
|
1397
|
+
@staticmethod
|
|
1398
|
+
def _extract_secret_name_from_value_from(
|
|
1399
|
+
value_from: typing.Any,
|
|
1400
|
+
) -> Optional[str]:
|
|
1401
|
+
"""Extract secret name from a V1EnvVarSource or dict representation."""
|
|
1402
|
+
if isinstance(value_from, k8s_client.V1EnvVarSource):
|
|
1403
|
+
if value_from.secret_key_ref:
|
|
1404
|
+
return value_from.secret_key_ref.name
|
|
1405
|
+
elif isinstance(value_from, dict):
|
|
1406
|
+
value_from = (
|
|
1407
|
+
value_from.get("valueFrom")
|
|
1408
|
+
or value_from.get("value_from")
|
|
1409
|
+
or value_from
|
|
1410
|
+
)
|
|
1411
|
+
secret_key_ref = (value_from or {}).get("secretKeyRef") or (
|
|
1412
|
+
value_from or {}
|
|
1413
|
+
).get("secret_key_ref")
|
|
1414
|
+
if isinstance(secret_key_ref, dict):
|
|
1415
|
+
return secret_key_ref.get("name")
|
|
1416
|
+
return None
|
|
1417
|
+
|
|
1226
1418
|
|
|
1227
1419
|
def _resolve_if_type_sanitized(attribute_name, attribute):
|
|
1228
1420
|
attribute_config = sanitized_attributes[attribute_name]
|
mlrun/runtimes/utils.py
CHANGED
|
@@ -26,6 +26,7 @@ import pandas as pd
|
|
|
26
26
|
import mlrun
|
|
27
27
|
import mlrun.common.constants
|
|
28
28
|
import mlrun.common.constants as mlrun_constants
|
|
29
|
+
import mlrun.common.runtimes.constants
|
|
29
30
|
import mlrun.common.schemas
|
|
30
31
|
import mlrun.utils.regex
|
|
31
32
|
from mlrun.artifacts import TableArtifact
|
|
@@ -153,6 +154,7 @@ def results_to_iter(results, runspec, execution):
|
|
|
153
154
|
|
|
154
155
|
iter = []
|
|
155
156
|
failed = 0
|
|
157
|
+
pending_retry = 0
|
|
156
158
|
running = 0
|
|
157
159
|
for task in results:
|
|
158
160
|
if task:
|
|
@@ -164,17 +166,26 @@ def results_to_iter(results, runspec, execution):
|
|
|
164
166
|
"state": state,
|
|
165
167
|
"iter": id,
|
|
166
168
|
}
|
|
167
|
-
if state ==
|
|
169
|
+
if state == mlrun.common.runtimes.constants.RunStates.error:
|
|
168
170
|
failed += 1
|
|
169
171
|
err = get_in(task, ["status", "error"], "")
|
|
170
|
-
logger.error(f"error in task
|
|
171
|
-
elif state
|
|
172
|
+
logger.error(f"error in task {execution.uid}:{id} - {err_to_str(err)}")
|
|
173
|
+
elif state == mlrun.common.runtimes.constants.RunStates.pending_retry:
|
|
174
|
+
pending_retry += 1
|
|
175
|
+
err = get_in(task, ["status", "error"], "")
|
|
176
|
+
retry_count = get_in(task, ["status", "retry_count"], 0)
|
|
177
|
+
logger.warning(
|
|
178
|
+
f"pending retry in task {execution.uid}:{id} - {err_to_str(err)}. Retry count: {retry_count}"
|
|
179
|
+
)
|
|
180
|
+
elif state != mlrun.common.runtimes.constants.RunStates.completed:
|
|
172
181
|
running += 1
|
|
173
182
|
|
|
174
183
|
iter.append(struct)
|
|
175
184
|
|
|
176
185
|
if not iter:
|
|
177
|
-
execution.set_state(
|
|
186
|
+
execution.set_state(
|
|
187
|
+
mlrun.common.runtimes.constants.RunStates.completed, commit=True
|
|
188
|
+
)
|
|
178
189
|
logger.warning("Warning!, zero iteration results")
|
|
179
190
|
return
|
|
180
191
|
if hasattr(pd, "json_normalize"):
|
|
@@ -204,8 +215,14 @@ def results_to_iter(results, runspec, execution):
|
|
|
204
215
|
error=f"{failed} of {len(results)} tasks failed, check logs in db for details",
|
|
205
216
|
commit=False,
|
|
206
217
|
)
|
|
218
|
+
elif pending_retry:
|
|
219
|
+
execution.set_state(
|
|
220
|
+
mlrun.common.runtimes.constants.RunStates.pending_retry, commit=False
|
|
221
|
+
)
|
|
207
222
|
elif running == 0:
|
|
208
|
-
execution.set_state(
|
|
223
|
+
execution.set_state(
|
|
224
|
+
mlrun.common.runtimes.constants.RunStates.completed, commit=False
|
|
225
|
+
)
|
|
209
226
|
execution.commit()
|
|
210
227
|
|
|
211
228
|
|
|
@@ -431,22 +448,45 @@ def enrich_function_from_dict(function, function_dict):
|
|
|
431
448
|
return function
|
|
432
449
|
|
|
433
450
|
|
|
451
|
+
def resolve_owner(
|
|
452
|
+
labels: dict,
|
|
453
|
+
owner_to_enrich: Optional[str] = None,
|
|
454
|
+
):
|
|
455
|
+
"""
|
|
456
|
+
Resolve the owner label value
|
|
457
|
+
:param labels: The run labels dict
|
|
458
|
+
:param auth_username: The authenticated username
|
|
459
|
+
:return: The resolved owner label value
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
if owner_to_enrich and (
|
|
463
|
+
labels.get("job-type") == mlrun.common.constants.JOB_TYPE_WORKFLOW_RUNNER
|
|
464
|
+
or labels.get("job-type")
|
|
465
|
+
== mlrun.common.constants.JOB_TYPE_RERUN_WORKFLOW_RUNNER
|
|
466
|
+
):
|
|
467
|
+
return owner_to_enrich
|
|
468
|
+
else:
|
|
469
|
+
return os.environ.get("V3IO_USERNAME") or getpass.getuser()
|
|
470
|
+
|
|
471
|
+
|
|
434
472
|
def enrich_run_labels(
|
|
435
473
|
labels: dict,
|
|
436
474
|
labels_to_enrich: Optional[list[mlrun_constants.MLRunInternalLabels]] = None,
|
|
475
|
+
owner_to_enrich: Optional[str] = None,
|
|
437
476
|
):
|
|
438
477
|
"""
|
|
439
|
-
Enrich the run labels with the internal labels and the labels enrichment extension
|
|
478
|
+
Enrich the run labels with the internal labels and the labels enrichment extension.
|
|
440
479
|
:param labels: The run labels dict
|
|
441
480
|
:param labels_to_enrich: The label keys to enrich from MLRunInternalLabels.default_run_labels_to_enrich
|
|
481
|
+
:param owner_to_enrich: Optional owner to enrich the labels with, if not provided will try to resolve it.
|
|
442
482
|
:return: The enriched labels dict
|
|
443
483
|
"""
|
|
444
484
|
# Merge the labels with the labels enrichment extension
|
|
445
485
|
labels_enrichment = {
|
|
446
|
-
mlrun_constants.MLRunInternalLabels.owner:
|
|
447
|
-
|
|
486
|
+
mlrun_constants.MLRunInternalLabels.owner: resolve_owner(
|
|
487
|
+
labels, owner_to_enrich
|
|
488
|
+
),
|
|
448
489
|
}
|
|
449
|
-
|
|
450
490
|
# Resolve which label keys to enrich
|
|
451
491
|
if labels_to_enrich is None:
|
|
452
492
|
labels_to_enrich = (
|