dstack 0.19.25rc1__py3-none-any.whl → 0.19.27__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 +293 -58
- dstack/_internal/cli/services/configurators/volume.py +2 -4
- dstack/_internal/cli/services/profile.py +1 -1
- dstack/_internal/cli/services/repos.py +35 -48
- dstack/_internal/core/backends/amddevcloud/__init__.py +1 -0
- dstack/_internal/core/backends/amddevcloud/backend.py +16 -0
- dstack/_internal/core/backends/amddevcloud/compute.py +5 -0
- dstack/_internal/core/backends/amddevcloud/configurator.py +29 -0
- dstack/_internal/core/backends/aws/compute.py +6 -1
- dstack/_internal/core/backends/aws/configurator.py +11 -7
- dstack/_internal/core/backends/azure/configurator.py +11 -7
- dstack/_internal/core/backends/base/compute.py +33 -5
- dstack/_internal/core/backends/base/configurator.py +25 -13
- dstack/_internal/core/backends/base/offers.py +2 -0
- dstack/_internal/core/backends/cloudrift/configurator.py +13 -7
- dstack/_internal/core/backends/configurators.py +15 -0
- 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/digitalocean/__init__.py +1 -0
- dstack/_internal/core/backends/digitalocean/backend.py +16 -0
- dstack/_internal/core/backends/digitalocean/compute.py +5 -0
- dstack/_internal/core/backends/digitalocean/configurator.py +31 -0
- dstack/_internal/core/backends/digitalocean_base/__init__.py +1 -0
- dstack/_internal/core/backends/digitalocean_base/api_client.py +104 -0
- dstack/_internal/core/backends/digitalocean_base/backend.py +5 -0
- dstack/_internal/core/backends/digitalocean_base/compute.py +173 -0
- dstack/_internal/core/backends/digitalocean_base/configurator.py +57 -0
- dstack/_internal/core/backends/digitalocean_base/models.py +43 -0
- dstack/_internal/core/backends/gcp/compute.py +32 -8
- dstack/_internal/core/backends/gcp/configurator.py +11 -7
- dstack/_internal/core/backends/hotaisle/api_client.py +25 -33
- dstack/_internal/core/backends/hotaisle/compute.py +1 -6
- 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/models.py +7 -0
- dstack/_internal/core/backends/nebius/compute.py +1 -8
- dstack/_internal/core/backends/nebius/configurator.py +11 -7
- dstack/_internal/core/backends/nebius/resources.py +21 -11
- dstack/_internal/core/backends/oci/compute.py +4 -5
- 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/compute.py +1 -5
- dstack/_internal/core/backends/vultr/configurator.py +11 -4
- dstack/_internal/core/compatibility/fleets.py +5 -0
- dstack/_internal/core/compatibility/gpus.py +13 -0
- dstack/_internal/core/compatibility/runs.py +9 -1
- dstack/_internal/core/models/backends/base.py +5 -1
- dstack/_internal/core/models/common.py +3 -3
- dstack/_internal/core/models/configurations.py +191 -32
- dstack/_internal/core/models/files.py +1 -1
- dstack/_internal/core/models/fleets.py +80 -3
- dstack/_internal/core/models/profiles.py +41 -11
- dstack/_internal/core/models/resources.py +46 -42
- dstack/_internal/core/models/runs.py +28 -5
- dstack/_internal/core/services/configs/__init__.py +6 -3
- dstack/_internal/core/services/profiles.py +2 -2
- dstack/_internal/core/services/repos.py +86 -79
- 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_fleets.py +109 -13
- dstack/_internal/server/background/tasks/process_gateways.py +4 -1
- dstack/_internal/server/background/tasks/process_instances.py +22 -73
- dstack/_internal/server/background/tasks/process_probes.py +1 -1
- dstack/_internal/server/background/tasks/process_running_jobs.py +12 -4
- dstack/_internal/server/background/tasks/process_runs.py +3 -1
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +67 -44
- 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/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
- dstack/_internal/server/models.py +6 -2
- dstack/_internal/server/routers/gpus.py +1 -6
- dstack/_internal/server/schemas/runner.py +11 -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 +37 -38
- 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 +44 -4
- dstack/_internal/server/services/jobs/__init__.py +15 -4
- dstack/_internal/server/services/jobs/configurators/base.py +53 -17
- dstack/_internal/server/services/jobs/configurators/dev.py +9 -4
- dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +6 -8
- dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +7 -9
- dstack/_internal/server/services/jobs/configurators/service.py +1 -3
- dstack/_internal/server/services/jobs/configurators/task.py +3 -3
- 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 +17 -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/statics/{main-d151b300fcac3933213d.js → main-4eecc75fbe64067eb1bc.js} +1146 -899
- dstack/_internal/server/statics/{main-d151b300fcac3933213d.js.map → main-4eecc75fbe64067eb1bc.js.map} +1 -1
- dstack/_internal/server/statics/{main-aec4762350e34d6fbff9.css → main-56191c63d516fd0041c4.css} +1 -1
- dstack/_internal/server/testing/common.py +7 -4
- 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/path.py +8 -1
- dstack/_internal/utils/ssh.py +7 -0
- dstack/_internal/utils/typing.py +14 -0
- dstack/api/_public/repos.py +62 -8
- dstack/api/_public/runs.py +19 -8
- 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.25rc1.dist-info → dstack-0.19.27.dist-info}/METADATA +2 -2
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/RECORD +160 -142
- dstack/api/huggingface/__init__.py +0 -73
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/WHEEL +0 -0
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -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
|
|
@@ -33,7 +34,7 @@ STRIP_PREFIX_DEFAULT = True
|
|
|
33
34
|
RUN_PRIOTIRY_MIN = 0
|
|
34
35
|
RUN_PRIOTIRY_MAX = 100
|
|
35
36
|
RUN_PRIORITY_DEFAULT = 0
|
|
36
|
-
|
|
37
|
+
LEGACY_REPO_DIR = "/workflow"
|
|
37
38
|
MIN_PROBE_TIMEOUT = 1
|
|
38
39
|
MIN_PROBE_INTERVAL = 1
|
|
39
40
|
DEFAULT_PROBE_URL = "/"
|
|
@@ -83,6 +84,87 @@ 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
|
+
path: Annotated[
|
|
116
|
+
Optional[str],
|
|
117
|
+
Field(
|
|
118
|
+
description=(
|
|
119
|
+
"The repo path inside the run container. Relative paths are resolved"
|
|
120
|
+
f" relative to the working directory. Defaults to `{LEGACY_REPO_DIR}`"
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
] = None
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def parse(cls, v: str) -> Self:
|
|
127
|
+
is_url = False
|
|
128
|
+
parts = v.split(":")
|
|
129
|
+
if len(parts) > 1:
|
|
130
|
+
# Git repo, git@github.com:dstackai/dstack.git or https://github.com/dstackai/dstack
|
|
131
|
+
if "@" in parts[0] or parts[1].startswith("//"):
|
|
132
|
+
parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
|
|
133
|
+
is_url = True
|
|
134
|
+
# Windows path, e.g., `C:\path\to`, 'c:/path/to'
|
|
135
|
+
elif (
|
|
136
|
+
len(parts[0]) == 1
|
|
137
|
+
and parts[0] in string.ascii_letters
|
|
138
|
+
and parts[1][:1] in ["\\", "/"]
|
|
139
|
+
):
|
|
140
|
+
parts = [f"{parts[0]}:{parts[1]}", *parts[2:]]
|
|
141
|
+
if len(parts) == 1:
|
|
142
|
+
if is_url:
|
|
143
|
+
return cls(url=parts[0])
|
|
144
|
+
return cls(local_path=parts[0])
|
|
145
|
+
if len(parts) == 2:
|
|
146
|
+
if is_url:
|
|
147
|
+
return cls(url=parts[0], path=parts[1])
|
|
148
|
+
return cls(local_path=parts[0], path=parts[1])
|
|
149
|
+
raise ValueError(f"Invalid repo: {v}")
|
|
150
|
+
|
|
151
|
+
@root_validator
|
|
152
|
+
def validate_local_path_or_url(cls, values):
|
|
153
|
+
if values["local_path"] and values["url"]:
|
|
154
|
+
raise ValueError("`local_path` and `url` are mutually exclusive")
|
|
155
|
+
if not values["local_path"] and not values["url"]:
|
|
156
|
+
raise ValueError("Either `local_path` or `url` must be specified")
|
|
157
|
+
return values
|
|
158
|
+
|
|
159
|
+
@validator("path")
|
|
160
|
+
def validate_path(cls, v: Optional[str]) -> Optional[str]:
|
|
161
|
+
if v is None:
|
|
162
|
+
return v
|
|
163
|
+
if v.startswith("~") and PurePosixPath(v).parts[0] != "~":
|
|
164
|
+
raise ValueError("`~username` syntax is not supported")
|
|
165
|
+
return v
|
|
166
|
+
|
|
167
|
+
|
|
86
168
|
class ScalingSpec(CoreModel):
|
|
87
169
|
metric: Annotated[
|
|
88
170
|
Literal["rps"],
|
|
@@ -221,7 +303,7 @@ class ProbeConfig(CoreModel):
|
|
|
221
303
|
),
|
|
222
304
|
] = None
|
|
223
305
|
timeout: Annotated[
|
|
224
|
-
Optional[
|
|
306
|
+
Optional[int],
|
|
225
307
|
Field(
|
|
226
308
|
description=(
|
|
227
309
|
f"Maximum amount of time the HTTP request is allowed to take. Defaults to `{DEFAULT_PROBE_TIMEOUT}s`"
|
|
@@ -229,7 +311,7 @@ class ProbeConfig(CoreModel):
|
|
|
229
311
|
),
|
|
230
312
|
] = None
|
|
231
313
|
interval: Annotated[
|
|
232
|
-
Optional[
|
|
314
|
+
Optional[int],
|
|
233
315
|
Field(
|
|
234
316
|
description=(
|
|
235
317
|
"Minimum amount of time between the end of one probe execution"
|
|
@@ -249,7 +331,19 @@ class ProbeConfig(CoreModel):
|
|
|
249
331
|
),
|
|
250
332
|
] = None
|
|
251
333
|
|
|
252
|
-
|
|
334
|
+
class Config(CoreModel.Config):
|
|
335
|
+
@staticmethod
|
|
336
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
337
|
+
add_extra_schema_types(
|
|
338
|
+
schema["properties"]["timeout"],
|
|
339
|
+
extra_types=[{"type": "string"}],
|
|
340
|
+
)
|
|
341
|
+
add_extra_schema_types(
|
|
342
|
+
schema["properties"]["interval"],
|
|
343
|
+
extra_types=[{"type": "string"}],
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@validator("timeout", pre=True)
|
|
253
347
|
def parse_timeout(cls, v: Optional[Union[int, str]]) -> Optional[int]:
|
|
254
348
|
if v is None:
|
|
255
349
|
return v
|
|
@@ -258,7 +352,7 @@ class ProbeConfig(CoreModel):
|
|
|
258
352
|
raise ValueError(f"Probe timeout cannot be shorter than {MIN_PROBE_TIMEOUT}s")
|
|
259
353
|
return parsed
|
|
260
354
|
|
|
261
|
-
@validator("interval")
|
|
355
|
+
@validator("interval", pre=True)
|
|
262
356
|
def parse_interval(cls, v: Optional[Union[int, str]]) -> Optional[int]:
|
|
263
357
|
if v is None:
|
|
264
358
|
return v
|
|
@@ -301,7 +395,7 @@ class BaseRunConfiguration(CoreModel):
|
|
|
301
395
|
Field(
|
|
302
396
|
description=(
|
|
303
397
|
"The user inside the container, `user_name_or_id[:group_name_or_id]`"
|
|
304
|
-
" (e.g., `ubuntu`, `1000:1000`). Defaults to the default `image`
|
|
398
|
+
" (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"
|
|
305
399
|
)
|
|
306
400
|
),
|
|
307
401
|
] = None
|
|
@@ -311,9 +405,8 @@ class BaseRunConfiguration(CoreModel):
|
|
|
311
405
|
Optional[str],
|
|
312
406
|
Field(
|
|
313
407
|
description=(
|
|
314
|
-
"The path to the working directory inside the container."
|
|
315
|
-
f"
|
|
316
|
-
' Defaults to `"."` '
|
|
408
|
+
"The absolute path to the working directory inside the container."
|
|
409
|
+
f" Defaults to `{LEGACY_REPO_DIR}`"
|
|
317
410
|
)
|
|
318
411
|
),
|
|
319
412
|
] = None
|
|
@@ -373,22 +466,36 @@ class BaseRunConfiguration(CoreModel):
|
|
|
373
466
|
),
|
|
374
467
|
),
|
|
375
468
|
] = None
|
|
376
|
-
volumes: Annotated[
|
|
377
|
-
List[Union[MountPoint, str]], Field(description="The volumes mount points")
|
|
378
|
-
] = []
|
|
469
|
+
volumes: Annotated[List[MountPoint], Field(description="The volumes mount points")] = []
|
|
379
470
|
docker: Annotated[
|
|
380
471
|
Optional[bool],
|
|
381
472
|
Field(
|
|
382
473
|
description="Use Docker inside the container. Mutually exclusive with `image`, `python`, and `nvcc`. Overrides `privileged`"
|
|
383
474
|
),
|
|
384
475
|
] = None
|
|
476
|
+
repos: Annotated[
|
|
477
|
+
list[RepoSpec],
|
|
478
|
+
Field(description="The list of Git repos"),
|
|
479
|
+
] = []
|
|
385
480
|
files: Annotated[
|
|
386
|
-
list[
|
|
481
|
+
list[FilePathMapping],
|
|
387
482
|
Field(description="The local to container file path mappings"),
|
|
388
483
|
] = []
|
|
389
484
|
# deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
|
|
390
485
|
setup: CommandsList = []
|
|
391
486
|
|
|
487
|
+
class Config(CoreModel.Config):
|
|
488
|
+
@staticmethod
|
|
489
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
490
|
+
add_extra_schema_types(
|
|
491
|
+
schema["properties"]["volumes"]["items"],
|
|
492
|
+
extra_types=[{"type": "string"}],
|
|
493
|
+
)
|
|
494
|
+
add_extra_schema_types(
|
|
495
|
+
schema["properties"]["files"]["items"],
|
|
496
|
+
extra_types=[{"type": "string"}],
|
|
497
|
+
)
|
|
498
|
+
|
|
392
499
|
@validator("python", pre=True, always=True)
|
|
393
500
|
def convert_python(cls, v, values) -> Optional[PythonVersion]:
|
|
394
501
|
if v is not None and values.get("image"):
|
|
@@ -413,18 +520,30 @@ class BaseRunConfiguration(CoreModel):
|
|
|
413
520
|
# but it's not possible to do so without breaking backwards compatibility.
|
|
414
521
|
return v
|
|
415
522
|
|
|
416
|
-
@validator("volumes", each_item=True)
|
|
417
|
-
def convert_volumes(cls, v) -> MountPoint:
|
|
523
|
+
@validator("volumes", each_item=True, pre=True)
|
|
524
|
+
def convert_volumes(cls, v: Union[MountPoint, str]) -> MountPoint:
|
|
418
525
|
if isinstance(v, str):
|
|
419
526
|
return parse_mount_point(v)
|
|
420
527
|
return v
|
|
421
528
|
|
|
422
|
-
@validator("files", each_item=True)
|
|
423
|
-
def convert_files(cls, v) -> FilePathMapping:
|
|
529
|
+
@validator("files", each_item=True, pre=True)
|
|
530
|
+
def convert_files(cls, v: Union[FilePathMapping, str]) -> FilePathMapping:
|
|
424
531
|
if isinstance(v, str):
|
|
425
532
|
return FilePathMapping.parse(v)
|
|
426
533
|
return v
|
|
427
534
|
|
|
535
|
+
@validator("repos", pre=True, each_item=True)
|
|
536
|
+
def convert_repos(cls, v: Union[RepoSpec, str]) -> RepoSpec:
|
|
537
|
+
if isinstance(v, str):
|
|
538
|
+
return RepoSpec.parse(v)
|
|
539
|
+
return v
|
|
540
|
+
|
|
541
|
+
@validator("repos")
|
|
542
|
+
def validate_repos(cls, v) -> RepoSpec:
|
|
543
|
+
if len(v) > 1:
|
|
544
|
+
raise ValueError("A maximum of one repo is currently supported")
|
|
545
|
+
return v
|
|
546
|
+
|
|
428
547
|
@validator("user")
|
|
429
548
|
def validate_user(cls, v) -> Optional[str]:
|
|
430
549
|
if v is None:
|
|
@@ -444,7 +563,7 @@ class BaseRunConfiguration(CoreModel):
|
|
|
444
563
|
raise ValueError("The value must be `sh`, `bash`, or an absolute path")
|
|
445
564
|
|
|
446
565
|
|
|
447
|
-
class
|
|
566
|
+
class ConfigurationWithPortsParams(CoreModel):
|
|
448
567
|
ports: Annotated[
|
|
449
568
|
List[Union[ValidPort, constr(regex=r"^(?:[0-9]+|\*):[0-9]+$"), PortMapping]],
|
|
450
569
|
Field(description="Port numbers/mapping to expose"),
|
|
@@ -459,7 +578,7 @@ class BaseRunConfigurationWithPorts(BaseRunConfiguration):
|
|
|
459
578
|
return v
|
|
460
579
|
|
|
461
580
|
|
|
462
|
-
class
|
|
581
|
+
class ConfigurationWithCommandsParams(CoreModel):
|
|
463
582
|
commands: Annotated[CommandsList, Field(description="The shell commands to run")] = []
|
|
464
583
|
|
|
465
584
|
@root_validator
|
|
@@ -503,10 +622,25 @@ class DevEnvironmentConfigurationParams(CoreModel):
|
|
|
503
622
|
|
|
504
623
|
|
|
505
624
|
class DevEnvironmentConfiguration(
|
|
506
|
-
ProfileParams,
|
|
625
|
+
ProfileParams,
|
|
626
|
+
BaseRunConfiguration,
|
|
627
|
+
ConfigurationWithPortsParams,
|
|
628
|
+
DevEnvironmentConfigurationParams,
|
|
507
629
|
):
|
|
508
630
|
type: Literal["dev-environment"] = "dev-environment"
|
|
509
631
|
|
|
632
|
+
class Config(ProfileParams.Config, BaseRunConfiguration.Config):
|
|
633
|
+
@staticmethod
|
|
634
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
635
|
+
ProfileParams.Config.schema_extra(schema)
|
|
636
|
+
BaseRunConfiguration.Config.schema_extra(schema)
|
|
637
|
+
|
|
638
|
+
@validator("entrypoint")
|
|
639
|
+
def validate_entrypoint(cls, v: Optional[str]) -> Optional[str]:
|
|
640
|
+
if v is not None:
|
|
641
|
+
raise ValueError("entrypoint is not supported for dev-environment")
|
|
642
|
+
return v
|
|
643
|
+
|
|
510
644
|
|
|
511
645
|
class TaskConfigurationParams(CoreModel):
|
|
512
646
|
nodes: Annotated[int, Field(description="Number of nodes", ge=1)] = 1
|
|
@@ -514,12 +648,19 @@ class TaskConfigurationParams(CoreModel):
|
|
|
514
648
|
|
|
515
649
|
class TaskConfiguration(
|
|
516
650
|
ProfileParams,
|
|
517
|
-
|
|
518
|
-
|
|
651
|
+
BaseRunConfiguration,
|
|
652
|
+
ConfigurationWithCommandsParams,
|
|
653
|
+
ConfigurationWithPortsParams,
|
|
519
654
|
TaskConfigurationParams,
|
|
520
655
|
):
|
|
521
656
|
type: Literal["task"] = "task"
|
|
522
657
|
|
|
658
|
+
class Config(ProfileParams.Config, BaseRunConfiguration.Config):
|
|
659
|
+
@staticmethod
|
|
660
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
661
|
+
ProfileParams.Config.schema_extra(schema)
|
|
662
|
+
BaseRunConfiguration.Config.schema_extra(schema)
|
|
663
|
+
|
|
523
664
|
|
|
524
665
|
class ServiceConfigurationParams(CoreModel):
|
|
525
666
|
port: Annotated[
|
|
@@ -547,7 +688,7 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
547
688
|
),
|
|
548
689
|
] = STRIP_PREFIX_DEFAULT
|
|
549
690
|
model: Annotated[
|
|
550
|
-
Optional[
|
|
691
|
+
Optional[AnyModel],
|
|
551
692
|
Field(
|
|
552
693
|
description=(
|
|
553
694
|
"Mapping of the model for the OpenAI-compatible endpoint provided by `dstack`."
|
|
@@ -578,6 +719,18 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
578
719
|
Field(description="List of probes used to determine job health"),
|
|
579
720
|
] = []
|
|
580
721
|
|
|
722
|
+
class Config(CoreModel.Config):
|
|
723
|
+
@staticmethod
|
|
724
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
725
|
+
add_extra_schema_types(
|
|
726
|
+
schema["properties"]["replicas"],
|
|
727
|
+
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
728
|
+
)
|
|
729
|
+
add_extra_schema_types(
|
|
730
|
+
schema["properties"]["model"],
|
|
731
|
+
extra_types=[{"type": "string"}],
|
|
732
|
+
)
|
|
733
|
+
|
|
581
734
|
@validator("port")
|
|
582
735
|
def convert_port(cls, v) -> PortMapping:
|
|
583
736
|
if isinstance(v, int):
|
|
@@ -586,7 +739,7 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
586
739
|
return PortMapping.parse(v)
|
|
587
740
|
return v
|
|
588
741
|
|
|
589
|
-
@validator("model")
|
|
742
|
+
@validator("model", pre=True)
|
|
590
743
|
def convert_model(cls, v: Optional[Union[AnyModel, str]]) -> Optional[AnyModel]:
|
|
591
744
|
if isinstance(v, str):
|
|
592
745
|
return OpenAIChatModel(type="chat", name=v, format="openai")
|
|
@@ -645,17 +798,23 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
645
798
|
|
|
646
799
|
|
|
647
800
|
class ServiceConfiguration(
|
|
648
|
-
ProfileParams,
|
|
801
|
+
ProfileParams,
|
|
802
|
+
BaseRunConfiguration,
|
|
803
|
+
ConfigurationWithCommandsParams,
|
|
804
|
+
ServiceConfigurationParams,
|
|
649
805
|
):
|
|
650
806
|
type: Literal["service"] = "service"
|
|
651
807
|
|
|
652
|
-
class Config(
|
|
808
|
+
class Config(
|
|
809
|
+
ProfileParams.Config,
|
|
810
|
+
BaseRunConfiguration.Config,
|
|
811
|
+
ServiceConfigurationParams.Config,
|
|
812
|
+
):
|
|
653
813
|
@staticmethod
|
|
654
814
|
def schema_extra(schema: Dict[str, Any]):
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
)
|
|
815
|
+
ProfileParams.Config.schema_extra(schema)
|
|
816
|
+
BaseRunConfiguration.Config.schema_extra(schema)
|
|
817
|
+
ServiceConfigurationParams.Config.schema_extra(schema)
|
|
659
818
|
|
|
660
819
|
|
|
661
820
|
AnyRunConfiguration = Union[DevEnvironmentConfiguration, TaskConfiguration, ServiceConfiguration]
|
|
@@ -19,7 +19,7 @@ from dstack._internal.core.models.profiles import (
|
|
|
19
19
|
TerminationPolicy,
|
|
20
20
|
parse_idle_duration,
|
|
21
21
|
)
|
|
22
|
-
from dstack._internal.core.models.resources import
|
|
22
|
+
from dstack._internal.core.models.resources import ResourcesSpec
|
|
23
23
|
from dstack._internal.utils.common import list_enum_values_for_annotation
|
|
24
24
|
from dstack._internal.utils.json_schema import add_extra_schema_types
|
|
25
25
|
from dstack._internal.utils.tags import tags_validator
|
|
@@ -141,6 +141,67 @@ class SSHParams(CoreModel):
|
|
|
141
141
|
return value
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
class FleetNodesSpec(CoreModel):
|
|
145
|
+
min: Annotated[
|
|
146
|
+
int, Field(description=("The minimum number of instances to maintain in the fleet"))
|
|
147
|
+
]
|
|
148
|
+
target: Annotated[
|
|
149
|
+
int,
|
|
150
|
+
Field(
|
|
151
|
+
description=(
|
|
152
|
+
"The number of instances to provision on fleet apply. `min` <= `target` <= `max`"
|
|
153
|
+
" Defaults to `min`"
|
|
154
|
+
)
|
|
155
|
+
),
|
|
156
|
+
]
|
|
157
|
+
max: Annotated[
|
|
158
|
+
Optional[int],
|
|
159
|
+
Field(
|
|
160
|
+
description=(
|
|
161
|
+
"The maximum number of instances allowed in the fleet. Unlimited if not specified"
|
|
162
|
+
)
|
|
163
|
+
),
|
|
164
|
+
] = None
|
|
165
|
+
|
|
166
|
+
def dict(self, *args, **kwargs) -> Dict:
|
|
167
|
+
# super() does not work with pydantic-duality
|
|
168
|
+
res = CoreModel.dict(self, *args, **kwargs)
|
|
169
|
+
# For backward compatibility with old clients
|
|
170
|
+
# that do not ignore extra fields due to https://github.com/dstackai/dstack/issues/3066
|
|
171
|
+
if "target" in res and res["target"] == res["min"]:
|
|
172
|
+
del res["target"]
|
|
173
|
+
return res
|
|
174
|
+
|
|
175
|
+
@root_validator(pre=True)
|
|
176
|
+
def set_min_and_target_defaults(cls, values):
|
|
177
|
+
min_ = values.get("min")
|
|
178
|
+
target = values.get("target")
|
|
179
|
+
if min_ is None:
|
|
180
|
+
values["min"] = 0
|
|
181
|
+
if target is None:
|
|
182
|
+
values["target"] = values["min"]
|
|
183
|
+
return values
|
|
184
|
+
|
|
185
|
+
@validator("min")
|
|
186
|
+
def validate_min(cls, v: int) -> int:
|
|
187
|
+
if v < 0:
|
|
188
|
+
raise ValueError("min cannot be negative")
|
|
189
|
+
return v
|
|
190
|
+
|
|
191
|
+
@root_validator(skip_on_failure=True)
|
|
192
|
+
def _post_validate_ranges(cls, values):
|
|
193
|
+
min_ = values["min"]
|
|
194
|
+
target = values["target"]
|
|
195
|
+
max_ = values.get("max")
|
|
196
|
+
if target < min_:
|
|
197
|
+
raise ValueError("target must not be be less than min")
|
|
198
|
+
if max_ is not None and max_ < min_:
|
|
199
|
+
raise ValueError("max must not be less than min")
|
|
200
|
+
if max_ is not None and max_ < target:
|
|
201
|
+
raise ValueError("max must not be less than target")
|
|
202
|
+
return values
|
|
203
|
+
|
|
204
|
+
|
|
144
205
|
class InstanceGroupParams(CoreModel):
|
|
145
206
|
env: Annotated[
|
|
146
207
|
Env,
|
|
@@ -151,7 +212,9 @@ class InstanceGroupParams(CoreModel):
|
|
|
151
212
|
Field(description="The parameters for adding instances via SSH"),
|
|
152
213
|
] = None
|
|
153
214
|
|
|
154
|
-
nodes: Annotated[
|
|
215
|
+
nodes: Annotated[
|
|
216
|
+
Optional[FleetNodesSpec], Field(description="The number of instances in cloud fleet")
|
|
217
|
+
] = None
|
|
155
218
|
placement: Annotated[
|
|
156
219
|
Optional[InstanceGroupPlacement],
|
|
157
220
|
Field(description="The placement of instances: `any` or `cluster`"),
|
|
@@ -224,7 +287,7 @@ class InstanceGroupParams(CoreModel):
|
|
|
224
287
|
Field(description="The maximum instance price per hour, in dollars", gt=0.0),
|
|
225
288
|
] = None
|
|
226
289
|
idle_duration: Annotated[
|
|
227
|
-
Optional[
|
|
290
|
+
Optional[int],
|
|
228
291
|
Field(
|
|
229
292
|
description="Time to wait before terminating idle instances. Defaults to `5m` for runs and `3d` for fleets. Use `off` for unlimited duration"
|
|
230
293
|
),
|
|
@@ -243,6 +306,20 @@ class InstanceGroupParams(CoreModel):
|
|
|
243
306
|
schema["properties"]["nodes"],
|
|
244
307
|
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
245
308
|
)
|
|
309
|
+
add_extra_schema_types(
|
|
310
|
+
schema["properties"]["idle_duration"],
|
|
311
|
+
extra_types=[{"type": "string"}],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@validator("nodes", pre=True)
|
|
315
|
+
def parse_nodes(cls, v: Optional[Union[dict, str]]) -> Optional[dict]:
|
|
316
|
+
if isinstance(v, str) and ".." in v:
|
|
317
|
+
v = v.replace(" ", "")
|
|
318
|
+
min, max = v.split("..")
|
|
319
|
+
return dict(min=min or None, max=max or None)
|
|
320
|
+
elif isinstance(v, str) or isinstance(v, int):
|
|
321
|
+
return dict(min=v, max=v)
|
|
322
|
+
return v
|
|
246
323
|
|
|
247
324
|
_validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
|
|
248
325
|
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]:
|