dstack 0.19.25__py3-none-any.whl → 0.19.26__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 dstack might be problematic. Click here for more details.
- dstack/_internal/cli/commands/__init__.py +2 -2
- dstack/_internal/cli/commands/apply.py +3 -61
- dstack/_internal/cli/commands/attach.py +1 -1
- dstack/_internal/cli/commands/completion.py +1 -1
- dstack/_internal/cli/commands/delete.py +2 -2
- dstack/_internal/cli/commands/fleet.py +1 -1
- dstack/_internal/cli/commands/gateway.py +2 -2
- dstack/_internal/cli/commands/init.py +56 -24
- dstack/_internal/cli/commands/logs.py +1 -1
- dstack/_internal/cli/commands/metrics.py +1 -1
- dstack/_internal/cli/commands/offer.py +45 -7
- dstack/_internal/cli/commands/project.py +2 -2
- dstack/_internal/cli/commands/secrets.py +2 -2
- dstack/_internal/cli/commands/server.py +1 -1
- dstack/_internal/cli/commands/stop.py +1 -1
- dstack/_internal/cli/commands/volume.py +1 -1
- dstack/_internal/cli/main.py +2 -2
- dstack/_internal/cli/services/completion.py +2 -2
- dstack/_internal/cli/services/configurators/__init__.py +6 -2
- dstack/_internal/cli/services/configurators/base.py +6 -7
- dstack/_internal/cli/services/configurators/fleet.py +1 -3
- dstack/_internal/cli/services/configurators/gateway.py +2 -4
- dstack/_internal/cli/services/configurators/run.py +195 -55
- dstack/_internal/cli/services/configurators/volume.py +2 -4
- dstack/_internal/cli/services/profile.py +1 -1
- dstack/_internal/cli/services/repos.py +51 -47
- dstack/_internal/core/backends/aws/configurator.py +11 -7
- dstack/_internal/core/backends/azure/configurator.py +11 -7
- dstack/_internal/core/backends/base/configurator.py +25 -13
- dstack/_internal/core/backends/cloudrift/configurator.py +13 -7
- dstack/_internal/core/backends/cudo/configurator.py +11 -7
- dstack/_internal/core/backends/datacrunch/compute.py +5 -1
- dstack/_internal/core/backends/datacrunch/configurator.py +13 -7
- dstack/_internal/core/backends/gcp/configurator.py +11 -7
- dstack/_internal/core/backends/hotaisle/configurator.py +13 -7
- dstack/_internal/core/backends/kubernetes/configurator.py +13 -7
- dstack/_internal/core/backends/lambdalabs/configurator.py +11 -7
- dstack/_internal/core/backends/nebius/compute.py +1 -1
- dstack/_internal/core/backends/nebius/configurator.py +11 -7
- dstack/_internal/core/backends/nebius/resources.py +21 -11
- dstack/_internal/core/backends/oci/configurator.py +11 -7
- dstack/_internal/core/backends/runpod/configurator.py +11 -7
- dstack/_internal/core/backends/template/configurator.py.jinja +11 -7
- dstack/_internal/core/backends/tensordock/configurator.py +13 -7
- dstack/_internal/core/backends/vastai/configurator.py +11 -7
- dstack/_internal/core/backends/vultr/configurator.py +11 -4
- dstack/_internal/core/compatibility/gpus.py +13 -0
- dstack/_internal/core/compatibility/runs.py +1 -0
- dstack/_internal/core/models/common.py +3 -3
- dstack/_internal/core/models/configurations.py +172 -27
- dstack/_internal/core/models/files.py +1 -1
- dstack/_internal/core/models/fleets.py +5 -1
- dstack/_internal/core/models/profiles.py +41 -11
- dstack/_internal/core/models/resources.py +46 -42
- dstack/_internal/core/models/runs.py +4 -0
- dstack/_internal/core/services/configs/__init__.py +2 -2
- dstack/_internal/core/services/profiles.py +2 -2
- dstack/_internal/core/services/repos.py +5 -3
- dstack/_internal/core/services/ssh/ports.py +1 -1
- dstack/_internal/proxy/lib/deps.py +6 -2
- dstack/_internal/server/app.py +22 -17
- dstack/_internal/server/background/tasks/process_gateways.py +4 -1
- dstack/_internal/server/background/tasks/process_instances.py +10 -2
- dstack/_internal/server/background/tasks/process_probes.py +1 -1
- dstack/_internal/server/background/tasks/process_running_jobs.py +10 -4
- dstack/_internal/server/background/tasks/process_runs.py +1 -1
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +54 -43
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +2 -2
- dstack/_internal/server/background/tasks/process_volumes.py +1 -1
- dstack/_internal/server/db.py +8 -4
- dstack/_internal/server/models.py +1 -0
- dstack/_internal/server/routers/gpus.py +1 -6
- dstack/_internal/server/schemas/runner.py +10 -0
- dstack/_internal/server/services/backends/__init__.py +14 -8
- dstack/_internal/server/services/backends/handlers.py +6 -1
- dstack/_internal/server/services/docker.py +5 -5
- dstack/_internal/server/services/fleets.py +14 -13
- dstack/_internal/server/services/gateways/__init__.py +2 -0
- dstack/_internal/server/services/gateways/client.py +5 -2
- dstack/_internal/server/services/gateways/connection.py +1 -1
- dstack/_internal/server/services/gpus.py +50 -49
- dstack/_internal/server/services/instances.py +41 -1
- dstack/_internal/server/services/jobs/__init__.py +15 -4
- dstack/_internal/server/services/jobs/configurators/base.py +7 -11
- dstack/_internal/server/services/jobs/configurators/dev.py +5 -0
- dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +3 -3
- dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +3 -3
- dstack/_internal/server/services/jobs/configurators/service.py +1 -0
- dstack/_internal/server/services/jobs/configurators/task.py +3 -0
- dstack/_internal/server/services/locking.py +5 -5
- dstack/_internal/server/services/logging.py +10 -2
- dstack/_internal/server/services/logs/__init__.py +8 -6
- dstack/_internal/server/services/logs/aws.py +330 -327
- dstack/_internal/server/services/logs/filelog.py +7 -6
- dstack/_internal/server/services/logs/gcp.py +141 -139
- dstack/_internal/server/services/plugins.py +1 -1
- dstack/_internal/server/services/projects.py +2 -5
- dstack/_internal/server/services/proxy/repo.py +5 -1
- dstack/_internal/server/services/requirements/__init__.py +0 -0
- dstack/_internal/server/services/requirements/combine.py +259 -0
- dstack/_internal/server/services/runner/client.py +7 -0
- dstack/_internal/server/services/runs.py +1 -1
- dstack/_internal/server/services/services/__init__.py +8 -2
- dstack/_internal/server/services/services/autoscalers.py +2 -0
- dstack/_internal/server/services/ssh.py +2 -1
- dstack/_internal/server/services/storage/__init__.py +5 -6
- dstack/_internal/server/services/storage/gcs.py +49 -49
- dstack/_internal/server/services/storage/s3.py +52 -52
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/testing/common.py +1 -1
- dstack/_internal/server/utils/logging.py +3 -3
- dstack/_internal/server/utils/provisioning.py +3 -3
- dstack/_internal/utils/json_schema.py +3 -1
- dstack/_internal/utils/typing.py +14 -0
- dstack/api/_public/repos.py +21 -2
- dstack/api/_public/runs.py +5 -7
- dstack/api/server/__init__.py +17 -19
- dstack/api/server/_gpus.py +2 -1
- dstack/api/server/_group.py +4 -3
- dstack/api/server/_repos.py +20 -3
- dstack/plugins/builtin/rest_plugin/_plugin.py +1 -0
- dstack/version.py +1 -1
- {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/METADATA +1 -1
- {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/RECORD +127 -124
- dstack/api/huggingface/__init__.py +0 -73
- {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/WHEEL +0 -0
- {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.25.dist-info → dstack-0.19.26.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -8,7 +8,6 @@ from dstack._internal.core.backends.base.configurator import (
|
|
|
8
8
|
from dstack._internal.core.backends.tensordock import api_client
|
|
9
9
|
from dstack._internal.core.backends.tensordock.backend import TensorDockBackend
|
|
10
10
|
from dstack._internal.core.backends.tensordock.models import (
|
|
11
|
-
AnyTensorDockBackendConfig,
|
|
12
11
|
TensorDockBackendConfig,
|
|
13
12
|
TensorDockBackendConfigWithCreds,
|
|
14
13
|
TensorDockConfig,
|
|
@@ -23,7 +22,12 @@ from dstack._internal.core.models.backends.base import (
|
|
|
23
22
|
REGIONS = []
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
class TensorDockConfigurator(
|
|
25
|
+
class TensorDockConfigurator(
|
|
26
|
+
Configurator[
|
|
27
|
+
TensorDockBackendConfig,
|
|
28
|
+
TensorDockBackendConfigWithCreds,
|
|
29
|
+
]
|
|
30
|
+
):
|
|
27
31
|
TYPE = BackendType.TENSORDOCK
|
|
28
32
|
BACKEND_CLASS = TensorDockBackend
|
|
29
33
|
|
|
@@ -44,12 +48,14 @@ class TensorDockConfigurator(Configurator):
|
|
|
44
48
|
auth=TensorDockCreds.parse_obj(config.creds).json(),
|
|
45
49
|
)
|
|
46
50
|
|
|
47
|
-
def
|
|
48
|
-
self, record: BackendRecord
|
|
49
|
-
) ->
|
|
51
|
+
def get_backend_config_with_creds(
|
|
52
|
+
self, record: BackendRecord
|
|
53
|
+
) -> TensorDockBackendConfigWithCreds:
|
|
54
|
+
config = self._get_config(record)
|
|
55
|
+
return TensorDockBackendConfigWithCreds.__response__.parse_obj(config)
|
|
56
|
+
|
|
57
|
+
def get_backend_config_without_creds(self, record: BackendRecord) -> TensorDockBackendConfig:
|
|
50
58
|
config = self._get_config(record)
|
|
51
|
-
if include_creds:
|
|
52
|
-
return TensorDockBackendConfigWithCreds.__response__.parse_obj(config)
|
|
53
59
|
return TensorDockBackendConfig.__response__.parse_obj(config)
|
|
54
60
|
|
|
55
61
|
def get_backend(self, record: BackendRecord) -> TensorDockBackend:
|
|
@@ -8,7 +8,6 @@ from dstack._internal.core.backends.base.configurator import (
|
|
|
8
8
|
from dstack._internal.core.backends.vastai import api_client
|
|
9
9
|
from dstack._internal.core.backends.vastai.backend import VastAIBackend
|
|
10
10
|
from dstack._internal.core.backends.vastai.models import (
|
|
11
|
-
AnyVastAIBackendConfig,
|
|
12
11
|
VastAIBackendConfig,
|
|
13
12
|
VastAIBackendConfigWithCreds,
|
|
14
13
|
VastAIConfig,
|
|
@@ -23,7 +22,12 @@ from dstack._internal.core.models.backends.base import (
|
|
|
23
22
|
REGIONS = []
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
class VastAIConfigurator(
|
|
25
|
+
class VastAIConfigurator(
|
|
26
|
+
Configurator[
|
|
27
|
+
VastAIBackendConfig,
|
|
28
|
+
VastAIBackendConfigWithCreds,
|
|
29
|
+
]
|
|
30
|
+
):
|
|
27
31
|
TYPE = BackendType.VASTAI
|
|
28
32
|
BACKEND_CLASS = VastAIBackend
|
|
29
33
|
|
|
@@ -42,12 +46,12 @@ class VastAIConfigurator(Configurator):
|
|
|
42
46
|
auth=VastAICreds.parse_obj(config.creds).json(),
|
|
43
47
|
)
|
|
44
48
|
|
|
45
|
-
def
|
|
46
|
-
self
|
|
47
|
-
|
|
49
|
+
def get_backend_config_with_creds(self, record: BackendRecord) -> VastAIBackendConfigWithCreds:
|
|
50
|
+
config = self._get_config(record)
|
|
51
|
+
return VastAIBackendConfigWithCreds.__response__.parse_obj(config)
|
|
52
|
+
|
|
53
|
+
def get_backend_config_without_creds(self, record: BackendRecord) -> VastAIBackendConfig:
|
|
48
54
|
config = self._get_config(record)
|
|
49
|
-
if include_creds:
|
|
50
|
-
return VastAIBackendConfigWithCreds.__response__.parse_obj(config)
|
|
51
55
|
return VastAIBackendConfig.__response__.parse_obj(config)
|
|
52
56
|
|
|
53
57
|
def get_backend(self, record: BackendRecord) -> VastAIBackend:
|
|
@@ -23,7 +23,12 @@ from dstack._internal.core.models.backends.base import (
|
|
|
23
23
|
REGIONS = []
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class VultrConfigurator(
|
|
26
|
+
class VultrConfigurator(
|
|
27
|
+
Configurator[
|
|
28
|
+
VultrBackendConfig,
|
|
29
|
+
VultrBackendConfigWithCreds,
|
|
30
|
+
]
|
|
31
|
+
):
|
|
27
32
|
TYPE = BackendType.VULTR
|
|
28
33
|
BACKEND_CLASS = VultrBackend
|
|
29
34
|
|
|
@@ -42,10 +47,12 @@ class VultrConfigurator(Configurator):
|
|
|
42
47
|
auth=VultrCreds.parse_obj(config.creds).json(),
|
|
43
48
|
)
|
|
44
49
|
|
|
45
|
-
def
|
|
50
|
+
def get_backend_config_with_creds(self, record: BackendRecord) -> VultrBackendConfigWithCreds:
|
|
51
|
+
config = self._get_config(record)
|
|
52
|
+
return VultrBackendConfigWithCreds.__response__.parse_obj(config)
|
|
53
|
+
|
|
54
|
+
def get_backend_config_without_creds(self, record: BackendRecord) -> VultrBackendConfig:
|
|
46
55
|
config = self._get_config(record)
|
|
47
|
-
if include_creds:
|
|
48
|
-
return VultrBackendConfigWithCreds.__response__.parse_obj(config)
|
|
49
56
|
return VultrBackendConfig.__response__.parse_obj(config)
|
|
50
57
|
|
|
51
58
|
def get_backend(self, record: BackendRecord) -> VultrBackend:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from dstack._internal.core.compatibility.runs import get_run_spec_excludes
|
|
4
|
+
from dstack._internal.core.models.common import IncludeExcludeDictType
|
|
5
|
+
from dstack._internal.server.schemas.gpus import ListGpusRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_list_gpus_excludes(request: ListGpusRequest) -> Optional[IncludeExcludeDictType]:
|
|
9
|
+
list_gpus_excludes: IncludeExcludeDictType = {}
|
|
10
|
+
run_spec_excludes = get_run_spec_excludes(request.run_spec)
|
|
11
|
+
if run_spec_excludes is not None:
|
|
12
|
+
list_gpus_excludes["run_spec"] = run_spec_excludes
|
|
13
|
+
return list_gpus_excludes
|
|
@@ -136,6 +136,7 @@ def get_run_spec_excludes(run_spec: RunSpec) -> IncludeExcludeDictType:
|
|
|
136
136
|
configuration_excludes["schedule"] = True
|
|
137
137
|
if profile is not None and profile.schedule is None:
|
|
138
138
|
profile_excludes.add("schedule")
|
|
139
|
+
configuration_excludes["repos"] = True
|
|
139
140
|
|
|
140
141
|
if configuration_excludes:
|
|
141
142
|
spec_excludes["configuration"] = configuration_excludes
|
|
@@ -102,12 +102,12 @@ class RegistryAuth(CoreModel):
|
|
|
102
102
|
password (str): The password or access token
|
|
103
103
|
"""
|
|
104
104
|
|
|
105
|
-
class Config(CoreModel.Config):
|
|
106
|
-
frozen = True
|
|
107
|
-
|
|
108
105
|
username: Annotated[str, Field(description="The username")]
|
|
109
106
|
password: Annotated[str, Field(description="The password or access token")]
|
|
110
107
|
|
|
108
|
+
class Config(CoreModel.Config):
|
|
109
|
+
frozen = True
|
|
110
|
+
|
|
111
111
|
|
|
112
112
|
class ApplyAction(str, Enum):
|
|
113
113
|
CREATE = "create" # resource is to be created or overridden
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import string
|
|
2
3
|
from collections import Counter
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
|
-
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|
6
7
|
|
|
7
8
|
import orjson
|
|
8
9
|
from pydantic import Field, ValidationError, conint, constr, root_validator, validator
|
|
9
|
-
from typing_extensions import
|
|
10
|
+
from typing_extensions import Self
|
|
10
11
|
|
|
11
12
|
from dstack._internal.core.errors import ConfigurationError
|
|
12
13
|
from dstack._internal.core.models.common import CoreModel, Duration, RegistryAuth
|
|
@@ -83,6 +84,72 @@ class PortMapping(CoreModel):
|
|
|
83
84
|
return PortMapping(local_port=local_port, container_port=int(container_port))
|
|
84
85
|
|
|
85
86
|
|
|
87
|
+
class RepoSpec(CoreModel):
|
|
88
|
+
local_path: Annotated[
|
|
89
|
+
Optional[str],
|
|
90
|
+
Field(
|
|
91
|
+
description=(
|
|
92
|
+
"The path to the Git repo on the user's machine. Relative paths are resolved"
|
|
93
|
+
" relative to the parent directory of the the configuration file."
|
|
94
|
+
" Mutually exclusive with `url`"
|
|
95
|
+
)
|
|
96
|
+
),
|
|
97
|
+
] = None
|
|
98
|
+
url: Annotated[
|
|
99
|
+
Optional[str],
|
|
100
|
+
Field(description="The Git repo URL. Mutually exclusive with `local_path`"),
|
|
101
|
+
] = None
|
|
102
|
+
branch: Annotated[
|
|
103
|
+
Optional[str],
|
|
104
|
+
Field(
|
|
105
|
+
description=(
|
|
106
|
+
"The repo branch. Defaults to the active branch for local paths"
|
|
107
|
+
" and the default branch for URLs"
|
|
108
|
+
)
|
|
109
|
+
),
|
|
110
|
+
] = None
|
|
111
|
+
hash: Annotated[
|
|
112
|
+
Optional[str],
|
|
113
|
+
Field(description="The commit hash"),
|
|
114
|
+
] = None
|
|
115
|
+
# Not implemented, has no effect, hidden in the docs
|
|
116
|
+
path: str = DEFAULT_REPO_DIR
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def parse(cls, v: str) -> Self:
|
|
120
|
+
is_url = False
|
|
121
|
+
parts = v.split(":")
|
|
122
|
+
if len(parts) > 1:
|
|
123
|
+
# Git repo, git@github.com:dstackai/dstack.git or https://github.com/dstackai/dstack
|
|
124
|
+
if "@" in parts[0] or parts[1].startswith("//"):
|
|
125
|
+
parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
|
|
126
|
+
is_url = True
|
|
127
|
+
# Windows path, e.g., `C:\path\to`, 'c:/path/to'
|
|
128
|
+
elif (
|
|
129
|
+
len(parts[0]) == 1
|
|
130
|
+
and parts[0] in string.ascii_letters
|
|
131
|
+
and parts[1][:1] in ["\\", "/"]
|
|
132
|
+
):
|
|
133
|
+
parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
|
|
134
|
+
if len(parts) == 1:
|
|
135
|
+
if is_url:
|
|
136
|
+
return cls(url=parts[0])
|
|
137
|
+
return cls(local_path=parts[0])
|
|
138
|
+
if len(parts) == 2:
|
|
139
|
+
if is_url:
|
|
140
|
+
return cls(url=parts[0], path=parts[1])
|
|
141
|
+
return cls(local_path=parts[0], path=parts[1])
|
|
142
|
+
raise ValueError(f"Invalid repo: {v}")
|
|
143
|
+
|
|
144
|
+
@root_validator
|
|
145
|
+
def validate_local_path_or_url(cls, values):
|
|
146
|
+
if values["local_path"] and values["url"]:
|
|
147
|
+
raise ValueError("`local_path` and `url` are mutually exclusive")
|
|
148
|
+
if not values["local_path"] and not values["url"]:
|
|
149
|
+
raise ValueError("Either `local_path` or `url` must be specified")
|
|
150
|
+
return values
|
|
151
|
+
|
|
152
|
+
|
|
86
153
|
class ScalingSpec(CoreModel):
|
|
87
154
|
metric: Annotated[
|
|
88
155
|
Literal["rps"],
|
|
@@ -221,7 +288,7 @@ class ProbeConfig(CoreModel):
|
|
|
221
288
|
),
|
|
222
289
|
] = None
|
|
223
290
|
timeout: Annotated[
|
|
224
|
-
Optional[
|
|
291
|
+
Optional[int],
|
|
225
292
|
Field(
|
|
226
293
|
description=(
|
|
227
294
|
f"Maximum amount of time the HTTP request is allowed to take. Defaults to `{DEFAULT_PROBE_TIMEOUT}s`"
|
|
@@ -229,7 +296,7 @@ class ProbeConfig(CoreModel):
|
|
|
229
296
|
),
|
|
230
297
|
] = None
|
|
231
298
|
interval: Annotated[
|
|
232
|
-
Optional[
|
|
299
|
+
Optional[int],
|
|
233
300
|
Field(
|
|
234
301
|
description=(
|
|
235
302
|
"Minimum amount of time between the end of one probe execution"
|
|
@@ -249,7 +316,19 @@ class ProbeConfig(CoreModel):
|
|
|
249
316
|
),
|
|
250
317
|
] = None
|
|
251
318
|
|
|
252
|
-
|
|
319
|
+
class Config(CoreModel.Config):
|
|
320
|
+
@staticmethod
|
|
321
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
322
|
+
add_extra_schema_types(
|
|
323
|
+
schema["properties"]["timeout"],
|
|
324
|
+
extra_types=[{"type": "string"}],
|
|
325
|
+
)
|
|
326
|
+
add_extra_schema_types(
|
|
327
|
+
schema["properties"]["interval"],
|
|
328
|
+
extra_types=[{"type": "string"}],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
@validator("timeout", pre=True)
|
|
253
332
|
def parse_timeout(cls, v: Optional[Union[int, str]]) -> Optional[int]:
|
|
254
333
|
if v is None:
|
|
255
334
|
return v
|
|
@@ -258,7 +337,7 @@ class ProbeConfig(CoreModel):
|
|
|
258
337
|
raise ValueError(f"Probe timeout cannot be shorter than {MIN_PROBE_TIMEOUT}s")
|
|
259
338
|
return parsed
|
|
260
339
|
|
|
261
|
-
@validator("interval")
|
|
340
|
+
@validator("interval", pre=True)
|
|
262
341
|
def parse_interval(cls, v: Optional[Union[int, str]]) -> Optional[int]:
|
|
263
342
|
if v is None:
|
|
264
343
|
return v
|
|
@@ -373,22 +452,36 @@ class BaseRunConfiguration(CoreModel):
|
|
|
373
452
|
),
|
|
374
453
|
),
|
|
375
454
|
] = None
|
|
376
|
-
volumes: Annotated[
|
|
377
|
-
List[Union[MountPoint, str]], Field(description="The volumes mount points")
|
|
378
|
-
] = []
|
|
455
|
+
volumes: Annotated[List[MountPoint], Field(description="The volumes mount points")] = []
|
|
379
456
|
docker: Annotated[
|
|
380
457
|
Optional[bool],
|
|
381
458
|
Field(
|
|
382
459
|
description="Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"
|
|
383
460
|
),
|
|
384
461
|
] = None
|
|
462
|
+
repos: Annotated[
|
|
463
|
+
list[RepoSpec],
|
|
464
|
+
Field(description="The list of Git repos"),
|
|
465
|
+
] = []
|
|
385
466
|
files: Annotated[
|
|
386
|
-
list[
|
|
467
|
+
list[FilePathMapping],
|
|
387
468
|
Field(description="The local to container file path mappings"),
|
|
388
469
|
] = []
|
|
389
470
|
# deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
|
|
390
471
|
setup: CommandsList = []
|
|
391
472
|
|
|
473
|
+
class Config(CoreModel.Config):
|
|
474
|
+
@staticmethod
|
|
475
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
476
|
+
add_extra_schema_types(
|
|
477
|
+
schema["properties"]["volumes"]["items"],
|
|
478
|
+
extra_types=[{"type": "string"}],
|
|
479
|
+
)
|
|
480
|
+
add_extra_schema_types(
|
|
481
|
+
schema["properties"]["files"]["items"],
|
|
482
|
+
extra_types=[{"type": "string"}],
|
|
483
|
+
)
|
|
484
|
+
|
|
392
485
|
@validator("python", pre=True, always=True)
|
|
393
486
|
def convert_python(cls, v, values) -> Optional[PythonVersion]:
|
|
394
487
|
if v is not None and values.get("image"):
|
|
@@ -413,18 +506,30 @@ class BaseRunConfiguration(CoreModel):
|
|
|
413
506
|
# but it's not possible to do so without breaking backwards compatibility.
|
|
414
507
|
return v
|
|
415
508
|
|
|
416
|
-
@validator("volumes", each_item=True)
|
|
417
|
-
def convert_volumes(cls, v) -> MountPoint:
|
|
509
|
+
@validator("volumes", each_item=True, pre=True)
|
|
510
|
+
def convert_volumes(cls, v: Union[MountPoint, str]) -> MountPoint:
|
|
418
511
|
if isinstance(v, str):
|
|
419
512
|
return parse_mount_point(v)
|
|
420
513
|
return v
|
|
421
514
|
|
|
422
|
-
@validator("files", each_item=True)
|
|
423
|
-
def convert_files(cls, v) -> FilePathMapping:
|
|
515
|
+
@validator("files", each_item=True, pre=True)
|
|
516
|
+
def convert_files(cls, v: Union[FilePathMapping, str]) -> FilePathMapping:
|
|
424
517
|
if isinstance(v, str):
|
|
425
518
|
return FilePathMapping.parse(v)
|
|
426
519
|
return v
|
|
427
520
|
|
|
521
|
+
@validator("repos", pre=True, each_item=True)
|
|
522
|
+
def convert_repos(cls, v: Union[RepoSpec, str]) -> RepoSpec:
|
|
523
|
+
if isinstance(v, str):
|
|
524
|
+
return RepoSpec.parse(v)
|
|
525
|
+
return v
|
|
526
|
+
|
|
527
|
+
@validator("repos")
|
|
528
|
+
def validate_repos(cls, v) -> RepoSpec:
|
|
529
|
+
if len(v) > 1:
|
|
530
|
+
raise ValueError("A maximum of one repo is currently supported")
|
|
531
|
+
return v
|
|
532
|
+
|
|
428
533
|
@validator("user")
|
|
429
534
|
def validate_user(cls, v) -> Optional[str]:
|
|
430
535
|
if v is None:
|
|
@@ -444,7 +549,7 @@ class BaseRunConfiguration(CoreModel):
|
|
|
444
549
|
raise ValueError("The value must be `sh`, `bash`, or an absolute path")
|
|
445
550
|
|
|
446
551
|
|
|
447
|
-
class
|
|
552
|
+
class ConfigurationWithPortsParams(CoreModel):
|
|
448
553
|
ports: Annotated[
|
|
449
554
|
List[Union[ValidPort, constr(regex=r"^(?:[0-9]+|\*):[0-9]+$"), PortMapping]],
|
|
450
555
|
Field(description="Port numbers/mapping to expose"),
|
|
@@ -459,7 +564,7 @@ class BaseRunConfigurationWithPorts(BaseRunConfiguration):
|
|
|
459
564
|
return v
|
|
460
565
|
|
|
461
566
|
|
|
462
|
-
class
|
|
567
|
+
class ConfigurationWithCommandsParams(CoreModel):
|
|
463
568
|
commands: Annotated[CommandsList, Field(description="The shell commands to run")] = []
|
|
464
569
|
|
|
465
570
|
@root_validator
|
|
@@ -503,10 +608,25 @@ class DevEnvironmentConfigurationParams(CoreModel):
|
|
|
503
608
|
|
|
504
609
|
|
|
505
610
|
class DevEnvironmentConfiguration(
|
|
506
|
-
ProfileParams,
|
|
611
|
+
ProfileParams,
|
|
612
|
+
BaseRunConfiguration,
|
|
613
|
+
ConfigurationWithPortsParams,
|
|
614
|
+
DevEnvironmentConfigurationParams,
|
|
507
615
|
):
|
|
508
616
|
type: Literal["dev-environment"] = "dev-environment"
|
|
509
617
|
|
|
618
|
+
class Config(ProfileParams.Config, BaseRunConfiguration.Config):
|
|
619
|
+
@staticmethod
|
|
620
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
621
|
+
ProfileParams.Config.schema_extra(schema)
|
|
622
|
+
BaseRunConfiguration.Config.schema_extra(schema)
|
|
623
|
+
|
|
624
|
+
@validator("entrypoint")
|
|
625
|
+
def validate_entrypoint(cls, v: Optional[str]) -> Optional[str]:
|
|
626
|
+
if v is not None:
|
|
627
|
+
raise ValueError("entrypoint is not supported for dev-environment")
|
|
628
|
+
return v
|
|
629
|
+
|
|
510
630
|
|
|
511
631
|
class TaskConfigurationParams(CoreModel):
|
|
512
632
|
nodes: Annotated[int, Field(description="Number of nodes", ge=1)] = 1
|
|
@@ -514,12 +634,19 @@ class TaskConfigurationParams(CoreModel):
|
|
|
514
634
|
|
|
515
635
|
class TaskConfiguration(
|
|
516
636
|
ProfileParams,
|
|
517
|
-
|
|
518
|
-
|
|
637
|
+
BaseRunConfiguration,
|
|
638
|
+
ConfigurationWithCommandsParams,
|
|
639
|
+
ConfigurationWithPortsParams,
|
|
519
640
|
TaskConfigurationParams,
|
|
520
641
|
):
|
|
521
642
|
type: Literal["task"] = "task"
|
|
522
643
|
|
|
644
|
+
class Config(ProfileParams.Config, BaseRunConfiguration.Config):
|
|
645
|
+
@staticmethod
|
|
646
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
647
|
+
ProfileParams.Config.schema_extra(schema)
|
|
648
|
+
BaseRunConfiguration.Config.schema_extra(schema)
|
|
649
|
+
|
|
523
650
|
|
|
524
651
|
class ServiceConfigurationParams(CoreModel):
|
|
525
652
|
port: Annotated[
|
|
@@ -547,7 +674,7 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
547
674
|
),
|
|
548
675
|
] = STRIP_PREFIX_DEFAULT
|
|
549
676
|
model: Annotated[
|
|
550
|
-
Optional[
|
|
677
|
+
Optional[AnyModel],
|
|
551
678
|
Field(
|
|
552
679
|
description=(
|
|
553
680
|
"Mapping of the model for the OpenAI-compatible endpoint provided by `dstack`."
|
|
@@ -578,6 +705,18 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
578
705
|
Field(description="List of probes used to determine job health"),
|
|
579
706
|
] = []
|
|
580
707
|
|
|
708
|
+
class Config(CoreModel.Config):
|
|
709
|
+
@staticmethod
|
|
710
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
711
|
+
add_extra_schema_types(
|
|
712
|
+
schema["properties"]["replicas"],
|
|
713
|
+
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
714
|
+
)
|
|
715
|
+
add_extra_schema_types(
|
|
716
|
+
schema["properties"]["model"],
|
|
717
|
+
extra_types=[{"type": "string"}],
|
|
718
|
+
)
|
|
719
|
+
|
|
581
720
|
@validator("port")
|
|
582
721
|
def convert_port(cls, v) -> PortMapping:
|
|
583
722
|
if isinstance(v, int):
|
|
@@ -586,7 +725,7 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
586
725
|
return PortMapping.parse(v)
|
|
587
726
|
return v
|
|
588
727
|
|
|
589
|
-
@validator("model")
|
|
728
|
+
@validator("model", pre=True)
|
|
590
729
|
def convert_model(cls, v: Optional[Union[AnyModel, str]]) -> Optional[AnyModel]:
|
|
591
730
|
if isinstance(v, str):
|
|
592
731
|
return OpenAIChatModel(type="chat", name=v, format="openai")
|
|
@@ -645,17 +784,23 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
645
784
|
|
|
646
785
|
|
|
647
786
|
class ServiceConfiguration(
|
|
648
|
-
ProfileParams,
|
|
787
|
+
ProfileParams,
|
|
788
|
+
BaseRunConfiguration,
|
|
789
|
+
ConfigurationWithCommandsParams,
|
|
790
|
+
ServiceConfigurationParams,
|
|
649
791
|
):
|
|
650
792
|
type: Literal["service"] = "service"
|
|
651
793
|
|
|
652
|
-
class Config(
|
|
794
|
+
class Config(
|
|
795
|
+
ProfileParams.Config,
|
|
796
|
+
BaseRunConfiguration.Config,
|
|
797
|
+
ServiceConfigurationParams.Config,
|
|
798
|
+
):
|
|
653
799
|
@staticmethod
|
|
654
800
|
def schema_extra(schema: Dict[str, Any]):
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
)
|
|
801
|
+
ProfileParams.Config.schema_extra(schema)
|
|
802
|
+
BaseRunConfiguration.Config.schema_extra(schema)
|
|
803
|
+
ServiceConfigurationParams.Config.schema_extra(schema)
|
|
659
804
|
|
|
660
805
|
|
|
661
806
|
AnyRunConfiguration = Union[DevEnvironmentConfiguration, TaskConfiguration, ServiceConfiguration]
|
|
@@ -224,7 +224,7 @@ class InstanceGroupParams(CoreModel):
|
|
|
224
224
|
Field(description="The maximum instance price per hour, in dollars", gt=0.0),
|
|
225
225
|
] = None
|
|
226
226
|
idle_duration: Annotated[
|
|
227
|
-
Optional[
|
|
227
|
+
Optional[int],
|
|
228
228
|
Field(
|
|
229
229
|
description="Time to wait before terminating idle instances. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration"
|
|
230
230
|
),
|
|
@@ -243,6 +243,10 @@ class InstanceGroupParams(CoreModel):
|
|
|
243
243
|
schema["properties"]["nodes"],
|
|
244
244
|
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
245
245
|
)
|
|
246
|
+
add_extra_schema_types(
|
|
247
|
+
schema["properties"]["idle_duration"],
|
|
248
|
+
extra_types=[{"type": "string"}],
|
|
249
|
+
)
|
|
246
250
|
|
|
247
251
|
_validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
|
|
248
252
|
parse_idle_duration
|
|
@@ -9,6 +9,7 @@ from dstack._internal.core.models.backends.base import BackendType
|
|
|
9
9
|
from dstack._internal.core.models.common import CoreModel, Duration
|
|
10
10
|
from dstack._internal.utils.common import list_enum_values_for_annotation
|
|
11
11
|
from dstack._internal.utils.cron import validate_cron
|
|
12
|
+
from dstack._internal.utils.json_schema import add_extra_schema_types
|
|
12
13
|
from dstack._internal.utils.json_utils import pydantic_orjson_dumps_with_indent
|
|
13
14
|
from dstack._internal.utils.tags import tags_validator
|
|
14
15
|
|
|
@@ -61,15 +62,17 @@ def parse_duration(v: Optional[Union[int, str]]) -> Optional[int]:
|
|
|
61
62
|
return Duration.parse(v)
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
def parse_max_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[
|
|
65
|
+
def parse_max_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[Literal["off"], int]]:
|
|
65
66
|
return parse_off_duration(v)
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def parse_stop_duration(
|
|
69
|
+
def parse_stop_duration(
|
|
70
|
+
v: Optional[Union[int, str, bool]],
|
|
71
|
+
) -> Optional[Union[Literal["off"], int]]:
|
|
69
72
|
return parse_off_duration(v)
|
|
70
73
|
|
|
71
74
|
|
|
72
|
-
def parse_off_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[
|
|
75
|
+
def parse_off_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[Literal["off"], int]]:
|
|
73
76
|
if v == "off" or v is False:
|
|
74
77
|
return "off"
|
|
75
78
|
if v is True:
|
|
@@ -77,7 +80,7 @@ def parse_off_duration(v: Optional[Union[int, str, bool]]) -> Optional[Union[str
|
|
|
77
80
|
return parse_duration(v)
|
|
78
81
|
|
|
79
82
|
|
|
80
|
-
def parse_idle_duration(v: Optional[Union[int, str]]) -> Optional[
|
|
83
|
+
def parse_idle_duration(v: Optional[Union[int, str]]) -> Optional[int]:
|
|
81
84
|
if v == "off" or v == -1:
|
|
82
85
|
return -1
|
|
83
86
|
return parse_duration(v)
|
|
@@ -121,10 +124,18 @@ class ProfileRetry(CoreModel):
|
|
|
121
124
|
),
|
|
122
125
|
] = None
|
|
123
126
|
duration: Annotated[
|
|
124
|
-
Optional[
|
|
127
|
+
Optional[int],
|
|
125
128
|
Field(description="The maximum period of retrying the run, e.g., `4h` or `1d`"),
|
|
126
129
|
] = None
|
|
127
130
|
|
|
131
|
+
class Config(CoreModel.Config):
|
|
132
|
+
@staticmethod
|
|
133
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
134
|
+
add_extra_schema_types(
|
|
135
|
+
schema["properties"]["duration"],
|
|
136
|
+
extra_types=[{"type": "string"}],
|
|
137
|
+
)
|
|
138
|
+
|
|
128
139
|
_validate_duration = validator("duration", pre=True, allow_reuse=True)(parse_duration)
|
|
129
140
|
|
|
130
141
|
@root_validator
|
|
@@ -151,7 +162,7 @@ class UtilizationPolicy(CoreModel):
|
|
|
151
162
|
),
|
|
152
163
|
]
|
|
153
164
|
time_window: Annotated[
|
|
154
|
-
|
|
165
|
+
int,
|
|
155
166
|
Field(
|
|
156
167
|
description=(
|
|
157
168
|
"The time window of metric samples taking into account to measure utilization"
|
|
@@ -160,6 +171,14 @@ class UtilizationPolicy(CoreModel):
|
|
|
160
171
|
),
|
|
161
172
|
]
|
|
162
173
|
|
|
174
|
+
class Config(CoreModel.Config):
|
|
175
|
+
@staticmethod
|
|
176
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
177
|
+
add_extra_schema_types(
|
|
178
|
+
schema["properties"]["time_window"],
|
|
179
|
+
extra_types=[{"type": "string"}],
|
|
180
|
+
)
|
|
181
|
+
|
|
163
182
|
@validator("time_window", pre=True)
|
|
164
183
|
def validate_time_window(cls, v: Union[int, str]) -> int:
|
|
165
184
|
v = parse_duration(v)
|
|
@@ -247,7 +266,7 @@ class ProfileParams(CoreModel):
|
|
|
247
266
|
Field(description="The policy for resubmitting the run. Defaults to `false`"),
|
|
248
267
|
] = None
|
|
249
268
|
max_duration: Annotated[
|
|
250
|
-
Optional[Union[Literal["off"],
|
|
269
|
+
Optional[Union[Literal["off"], int]],
|
|
251
270
|
Field(
|
|
252
271
|
description=(
|
|
253
272
|
"The maximum duration of a run (e.g., `2h`, `1d`, etc)."
|
|
@@ -257,7 +276,7 @@ class ProfileParams(CoreModel):
|
|
|
257
276
|
),
|
|
258
277
|
] = None
|
|
259
278
|
stop_duration: Annotated[
|
|
260
|
-
Optional[Union[Literal["off"],
|
|
279
|
+
Optional[Union[Literal["off"], int]],
|
|
261
280
|
Field(
|
|
262
281
|
description=(
|
|
263
282
|
"The maximum duration of a run graceful stopping."
|
|
@@ -282,7 +301,7 @@ class ProfileParams(CoreModel):
|
|
|
282
301
|
),
|
|
283
302
|
] = None
|
|
284
303
|
idle_duration: Annotated[
|
|
285
|
-
Optional[
|
|
304
|
+
Optional[int],
|
|
286
305
|
Field(
|
|
287
306
|
description=(
|
|
288
307
|
"Time to wait before terminating idle instances."
|
|
@@ -347,6 +366,18 @@ class ProfileParams(CoreModel):
|
|
|
347
366
|
del schema["properties"]["retry_policy"]
|
|
348
367
|
del schema["properties"]["termination_policy"]
|
|
349
368
|
del schema["properties"]["termination_idle_time"]
|
|
369
|
+
add_extra_schema_types(
|
|
370
|
+
schema["properties"]["max_duration"],
|
|
371
|
+
extra_types=[{"type": "boolean"}, {"type": "string"}],
|
|
372
|
+
)
|
|
373
|
+
add_extra_schema_types(
|
|
374
|
+
schema["properties"]["stop_duration"],
|
|
375
|
+
extra_types=[{"type": "boolean"}, {"type": "string"}],
|
|
376
|
+
)
|
|
377
|
+
add_extra_schema_types(
|
|
378
|
+
schema["properties"]["idle_duration"],
|
|
379
|
+
extra_types=[{"type": "string"}],
|
|
380
|
+
)
|
|
350
381
|
|
|
351
382
|
_validate_max_duration = validator("max_duration", pre=True, allow_reuse=True)(
|
|
352
383
|
parse_max_duration
|
|
@@ -366,7 +397,7 @@ class ProfileProps(CoreModel):
|
|
|
366
397
|
Field(
|
|
367
398
|
description="The name of the profile that can be passed as `--profile` to `dstack apply`"
|
|
368
399
|
),
|
|
369
|
-
]
|
|
400
|
+
] = ""
|
|
370
401
|
default: Annotated[
|
|
371
402
|
bool, Field(description="If set to true, `dstack apply` will use this profile by default.")
|
|
372
403
|
] = False
|
|
@@ -382,7 +413,6 @@ class ProfilesConfig(CoreModel):
|
|
|
382
413
|
class Config(CoreModel.Config):
|
|
383
414
|
json_loads = orjson.loads
|
|
384
415
|
json_dumps = pydantic_orjson_dumps_with_indent
|
|
385
|
-
|
|
386
416
|
schema_extra = {"$schema": "http://json-schema.org/draft-07/schema#"}
|
|
387
417
|
|
|
388
418
|
def default(self) -> Optional[Profile]:
|