dstack 0.18.44__py3-none-any.whl → 0.19.0rc1__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.
- dstack/_internal/cli/commands/gateway.py +15 -3
- dstack/_internal/cli/commands/logs.py +0 -22
- dstack/_internal/cli/commands/stats.py +8 -17
- dstack/_internal/cli/main.py +1 -5
- dstack/_internal/cli/services/configurators/fleet.py +4 -39
- dstack/_internal/cli/services/configurators/run.py +22 -21
- dstack/_internal/cli/services/profile.py +34 -83
- dstack/_internal/cli/utils/gateway.py +1 -1
- dstack/_internal/core/backends/__init__.py +56 -39
- dstack/_internal/core/backends/aws/__init__.py +0 -25
- dstack/_internal/core/backends/aws/auth.py +1 -10
- dstack/_internal/core/backends/aws/backend.py +26 -0
- dstack/_internal/core/backends/aws/compute.py +20 -45
- dstack/_internal/{server/services/backends/configurators/aws.py → core/backends/aws/configurator.py} +46 -85
- dstack/_internal/core/backends/aws/models.py +135 -0
- dstack/_internal/core/backends/aws/resources.py +1 -1
- dstack/_internal/core/backends/azure/__init__.py +0 -20
- dstack/_internal/core/backends/azure/auth.py +2 -11
- dstack/_internal/core/backends/azure/backend.py +21 -0
- dstack/_internal/core/backends/azure/compute.py +13 -27
- dstack/_internal/{server/services/backends/configurators/azure.py → core/backends/azure/configurator.py} +141 -210
- dstack/_internal/core/backends/azure/models.py +89 -0
- dstack/_internal/core/backends/base/__init__.py +0 -12
- dstack/_internal/core/backends/base/backend.py +18 -0
- dstack/_internal/core/backends/base/compute.py +153 -33
- dstack/_internal/core/backends/base/configurator.py +105 -0
- dstack/_internal/core/backends/base/models.py +14 -0
- dstack/_internal/core/backends/configurators.py +138 -0
- dstack/_internal/core/backends/cudo/__init__.py +0 -15
- dstack/_internal/core/backends/cudo/backend.py +16 -0
- dstack/_internal/core/backends/cudo/compute.py +8 -26
- dstack/_internal/core/backends/cudo/configurator.py +72 -0
- dstack/_internal/core/backends/cudo/models.py +37 -0
- dstack/_internal/core/backends/datacrunch/__init__.py +0 -15
- dstack/_internal/core/backends/datacrunch/backend.py +16 -0
- dstack/_internal/core/backends/datacrunch/compute.py +8 -25
- dstack/_internal/core/backends/datacrunch/configurator.py +66 -0
- dstack/_internal/core/backends/datacrunch/models.py +38 -0
- dstack/_internal/core/{models/backends/dstack.py → backends/dstack/models.py} +7 -7
- dstack/_internal/core/backends/gcp/__init__.py +0 -16
- dstack/_internal/core/backends/gcp/auth.py +2 -11
- dstack/_internal/core/backends/gcp/backend.py +17 -0
- dstack/_internal/core/backends/gcp/compute.py +13 -43
- dstack/_internal/{server/services/backends/configurators/gcp.py → core/backends/gcp/configurator.py} +46 -103
- dstack/_internal/core/backends/gcp/models.py +125 -0
- dstack/_internal/core/backends/kubernetes/__init__.py +0 -15
- dstack/_internal/core/backends/kubernetes/backend.py +16 -0
- dstack/_internal/core/backends/kubernetes/compute.py +16 -5
- dstack/_internal/core/backends/kubernetes/configurator.py +55 -0
- dstack/_internal/core/backends/kubernetes/models.py +72 -0
- dstack/_internal/core/backends/lambdalabs/__init__.py +0 -16
- dstack/_internal/core/backends/lambdalabs/backend.py +17 -0
- dstack/_internal/core/backends/lambdalabs/compute.py +7 -28
- dstack/_internal/core/backends/lambdalabs/configurator.py +82 -0
- dstack/_internal/core/backends/lambdalabs/models.py +37 -0
- dstack/_internal/core/backends/local/__init__.py +0 -13
- dstack/_internal/core/backends/local/backend.py +14 -0
- dstack/_internal/core/backends/local/compute.py +16 -2
- dstack/_internal/core/backends/models.py +128 -0
- dstack/_internal/core/backends/oci/__init__.py +0 -15
- dstack/_internal/core/backends/oci/auth.py +1 -5
- dstack/_internal/core/backends/oci/backend.py +16 -0
- dstack/_internal/core/backends/oci/compute.py +9 -23
- dstack/_internal/{server/services/backends/configurators/oci.py → core/backends/oci/configurator.py} +40 -85
- dstack/_internal/core/{models/backends/oci.py → backends/oci/models.py} +24 -25
- dstack/_internal/core/backends/oci/region.py +1 -1
- dstack/_internal/core/backends/runpod/__init__.py +0 -15
- dstack/_internal/core/backends/runpod/backend.py +16 -0
- dstack/_internal/core/backends/runpod/compute.py +7 -3
- dstack/_internal/core/backends/runpod/configurator.py +59 -0
- dstack/_internal/core/backends/runpod/models.py +54 -0
- dstack/_internal/core/backends/template/__init__.py +0 -0
- dstack/_internal/core/backends/tensordock/__init__.py +0 -15
- dstack/_internal/core/backends/tensordock/backend.py +16 -0
- dstack/_internal/core/backends/tensordock/compute.py +8 -27
- dstack/_internal/core/backends/tensordock/configurator.py +68 -0
- dstack/_internal/core/backends/tensordock/models.py +38 -0
- dstack/_internal/core/backends/vastai/__init__.py +0 -15
- dstack/_internal/core/backends/vastai/backend.py +16 -0
- dstack/_internal/core/backends/vastai/compute.py +2 -2
- dstack/_internal/core/backends/vastai/configurator.py +66 -0
- dstack/_internal/core/backends/vastai/models.py +37 -0
- dstack/_internal/core/backends/vultr/__init__.py +0 -15
- dstack/_internal/core/backends/vultr/backend.py +16 -0
- dstack/_internal/core/backends/vultr/compute.py +10 -24
- dstack/_internal/core/backends/vultr/configurator.py +64 -0
- dstack/_internal/core/backends/vultr/models.py +34 -0
- dstack/_internal/core/models/backends/__init__.py +0 -184
- dstack/_internal/core/models/backends/base.py +0 -19
- dstack/_internal/core/models/configurations.py +20 -15
- dstack/_internal/core/models/envs.py +4 -3
- dstack/_internal/core/models/fleets.py +17 -22
- dstack/_internal/core/models/gateways.py +3 -3
- dstack/_internal/core/models/instances.py +24 -0
- dstack/_internal/core/models/profiles.py +41 -46
- dstack/_internal/core/models/projects.py +1 -1
- dstack/_internal/core/models/repos/base.py +0 -5
- dstack/_internal/core/models/repos/local.py +3 -3
- dstack/_internal/core/models/repos/remote.py +26 -12
- dstack/_internal/core/models/repos/virtual.py +1 -1
- dstack/_internal/core/models/resources.py +45 -76
- dstack/_internal/core/models/runs.py +17 -19
- dstack/_internal/core/models/volumes.py +1 -3
- dstack/_internal/core/services/profiles.py +7 -16
- dstack/_internal/core/services/repos.py +0 -4
- dstack/_internal/server/app.py +0 -3
- dstack/_internal/server/background/tasks/process_gateways.py +4 -8
- dstack/_internal/server/background/tasks/process_instances.py +14 -9
- dstack/_internal/server/background/tasks/process_metrics.py +1 -1
- dstack/_internal/server/background/tasks/process_placement_groups.py +4 -1
- dstack/_internal/server/background/tasks/process_prometheus_metrics.py +1 -1
- dstack/_internal/server/background/tasks/process_running_jobs.py +14 -5
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +16 -37
- dstack/_internal/server/background/tasks/process_volumes.py +5 -2
- dstack/_internal/server/migrations/versions/7bc2586e8b9e_make_instancemodel_pool_id_optional.py +36 -0
- dstack/_internal/server/migrations/versions/bc8ca4a505c6_store_backendtype_as_string.py +171 -0
- dstack/_internal/server/models.py +48 -9
- dstack/_internal/server/routers/backends.py +14 -23
- dstack/_internal/server/routers/instances.py +3 -4
- dstack/_internal/server/routers/metrics.py +10 -8
- dstack/_internal/server/routers/prometheus.py +1 -1
- dstack/_internal/server/routers/repos.py +1 -2
- dstack/_internal/server/routers/runs.py +13 -59
- dstack/_internal/server/schemas/gateways.py +14 -23
- dstack/_internal/server/schemas/projects.py +7 -2
- dstack/_internal/server/schemas/repos.py +2 -38
- dstack/_internal/server/schemas/runner.py +1 -0
- dstack/_internal/server/schemas/runs.py +1 -24
- dstack/_internal/server/services/backends/__init__.py +85 -158
- dstack/_internal/server/services/config.py +52 -576
- dstack/_internal/server/services/fleets.py +8 -103
- dstack/_internal/server/services/gateways/__init__.py +12 -4
- dstack/_internal/server/services/{pools.py → instances.py} +22 -329
- dstack/_internal/server/services/jobs/__init__.py +9 -6
- dstack/_internal/server/services/jobs/configurators/base.py +16 -0
- dstack/_internal/server/services/jobs/configurators/dev.py +9 -1
- dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +42 -0
- dstack/_internal/server/services/metrics.py +39 -13
- dstack/_internal/server/services/offers.py +1 -1
- dstack/_internal/server/services/projects.py +23 -14
- dstack/_internal/server/services/prometheus.py +176 -18
- dstack/_internal/server/services/runs.py +24 -16
- dstack/_internal/server/services/volumes.py +8 -4
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-4eb116b97819badd1e2c.js → main-4fd5a4770eff59325ee3.js} +7 -7
- dstack/_internal/server/statics/{main-4eb116b97819badd1e2c.js.map → main-4fd5a4770eff59325ee3.js.map} +1 -1
- dstack/_internal/server/testing/common.py +58 -32
- dstack/_internal/utils/json_schema.py +6 -0
- dstack/_internal/utils/ssh.py +2 -1
- dstack/api/__init__.py +4 -0
- dstack/api/_public/__init__.py +16 -20
- dstack/api/_public/backends.py +1 -1
- dstack/api/_public/repos.py +36 -36
- dstack/api/_public/runs.py +167 -83
- dstack/api/server/__init__.py +11 -13
- dstack/api/server/_backends.py +12 -16
- dstack/api/server/_fleets.py +15 -57
- dstack/api/server/_gateways.py +3 -14
- dstack/api/server/_repos.py +1 -4
- dstack/api/server/_runs.py +21 -100
- dstack/api/server/_volumes.py +10 -5
- dstack/version.py +1 -1
- {dstack-0.18.44.dist-info → dstack-0.19.0rc1.dist-info}/METADATA +1 -1
- {dstack-0.18.44.dist-info → dstack-0.19.0rc1.dist-info}/RECORD +218 -204
- tests/_internal/cli/services/configurators/test_profile.py +6 -6
- tests/_internal/core/backends/aws/test_configurator.py +35 -0
- tests/_internal/core/backends/aws/test_resources.py +1 -1
- tests/_internal/core/backends/azure/test_configurator.py +61 -0
- tests/_internal/core/backends/cudo/__init__.py +0 -0
- tests/_internal/core/backends/cudo/test_configurator.py +37 -0
- tests/_internal/core/backends/datacrunch/__init__.py +0 -0
- tests/_internal/core/backends/datacrunch/test_configurator.py +17 -0
- tests/_internal/core/backends/gcp/test_configurator.py +42 -0
- tests/_internal/core/backends/kubernetes/test_configurator.py +43 -0
- tests/_internal/core/backends/lambdalabs/__init__.py +0 -0
- tests/_internal/core/backends/lambdalabs/test_configurator.py +38 -0
- tests/_internal/core/backends/oci/test_configurator.py +55 -0
- tests/_internal/core/backends/runpod/__init__.py +0 -0
- tests/_internal/core/backends/runpod/test_configurator.py +33 -0
- tests/_internal/core/backends/tensordock/__init__.py +0 -0
- tests/_internal/core/backends/tensordock/test_configurator.py +38 -0
- tests/_internal/core/backends/vastai/__init__.py +0 -0
- tests/_internal/core/backends/vastai/test_configurator.py +33 -0
- tests/_internal/core/backends/vultr/__init__.py +0 -0
- tests/_internal/core/backends/vultr/test_configurator.py +33 -0
- tests/_internal/server/background/tasks/test_process_gateways.py +4 -0
- tests/_internal/server/background/tasks/test_process_instances.py +49 -48
- tests/_internal/server/background/tasks/test_process_metrics.py +0 -3
- tests/_internal/server/background/tasks/test_process_placement_groups.py +2 -0
- tests/_internal/server/background/tasks/test_process_prometheus_metrics.py +0 -3
- tests/_internal/server/background/tasks/test_process_running_jobs.py +0 -21
- tests/_internal/server/background/tasks/test_process_runs.py +8 -22
- tests/_internal/server/background/tasks/test_process_submitted_jobs.py +3 -40
- tests/_internal/server/background/tasks/test_process_submitted_volumes.py +2 -0
- tests/_internal/server/background/tasks/test_process_terminating_jobs.py +10 -15
- tests/_internal/server/routers/test_backends.py +6 -764
- tests/_internal/server/routers/test_fleets.py +0 -26
- tests/_internal/server/routers/test_gateways.py +27 -3
- tests/_internal/server/routers/test_instances.py +0 -10
- tests/_internal/server/routers/test_metrics.py +27 -0
- tests/_internal/server/routers/test_projects.py +56 -0
- tests/_internal/server/routers/test_prometheus.py +116 -27
- tests/_internal/server/routers/test_repos.py +0 -15
- tests/_internal/server/routers/test_runs.py +4 -219
- tests/_internal/server/routers/test_volumes.py +2 -3
- tests/_internal/server/services/backends/__init__.py +0 -0
- tests/_internal/server/services/jobs/configurators/test_task.py +35 -0
- tests/_internal/server/services/test_config.py +7 -4
- tests/_internal/server/services/test_fleets.py +1 -4
- tests/_internal/server/services/{test_pools.py → test_instances.py} +11 -49
- tests/_internal/server/services/test_metrics.py +9 -5
- tests/_internal/server/services/test_repos.py +1 -14
- tests/_internal/server/services/test_runs.py +0 -4
- dstack/_internal/cli/commands/pool.py +0 -581
- dstack/_internal/cli/commands/run.py +0 -75
- dstack/_internal/core/backends/aws/config.py +0 -18
- dstack/_internal/core/backends/azure/config.py +0 -12
- dstack/_internal/core/backends/base/config.py +0 -5
- dstack/_internal/core/backends/cudo/config.py +0 -9
- dstack/_internal/core/backends/datacrunch/config.py +0 -9
- dstack/_internal/core/backends/gcp/config.py +0 -22
- dstack/_internal/core/backends/kubernetes/config.py +0 -6
- dstack/_internal/core/backends/lambdalabs/config.py +0 -9
- dstack/_internal/core/backends/nebius/__init__.py +0 -15
- dstack/_internal/core/backends/nebius/api_client.py +0 -319
- dstack/_internal/core/backends/nebius/compute.py +0 -220
- dstack/_internal/core/backends/nebius/config.py +0 -6
- dstack/_internal/core/backends/nebius/types.py +0 -37
- dstack/_internal/core/backends/oci/config.py +0 -6
- dstack/_internal/core/backends/runpod/config.py +0 -17
- dstack/_internal/core/backends/tensordock/config.py +0 -9
- dstack/_internal/core/backends/vastai/config.py +0 -6
- dstack/_internal/core/backends/vultr/config.py +0 -9
- dstack/_internal/core/models/backends/aws.py +0 -86
- dstack/_internal/core/models/backends/azure.py +0 -68
- dstack/_internal/core/models/backends/cudo.py +0 -43
- dstack/_internal/core/models/backends/datacrunch.py +0 -44
- dstack/_internal/core/models/backends/gcp.py +0 -67
- dstack/_internal/core/models/backends/kubernetes.py +0 -40
- dstack/_internal/core/models/backends/lambdalabs.py +0 -43
- dstack/_internal/core/models/backends/nebius.py +0 -54
- dstack/_internal/core/models/backends/runpod.py +0 -42
- dstack/_internal/core/models/backends/tensordock.py +0 -44
- dstack/_internal/core/models/backends/vastai.py +0 -43
- dstack/_internal/core/models/backends/vultr.py +0 -40
- dstack/_internal/core/models/pools.py +0 -43
- dstack/_internal/server/routers/pools.py +0 -142
- dstack/_internal/server/schemas/pools.py +0 -38
- dstack/_internal/server/services/backends/configurators/base.py +0 -72
- dstack/_internal/server/services/backends/configurators/cudo.py +0 -87
- dstack/_internal/server/services/backends/configurators/datacrunch.py +0 -79
- dstack/_internal/server/services/backends/configurators/kubernetes.py +0 -63
- dstack/_internal/server/services/backends/configurators/lambdalabs.py +0 -98
- dstack/_internal/server/services/backends/configurators/nebius.py +0 -85
- dstack/_internal/server/services/backends/configurators/runpod.py +0 -67
- dstack/_internal/server/services/backends/configurators/tensordock.py +0 -82
- dstack/_internal/server/services/backends/configurators/vastai.py +0 -80
- dstack/_internal/server/services/backends/configurators/vultr.py +0 -80
- dstack/api/_public/pools.py +0 -41
- dstack/api/_public/resources.py +0 -105
- dstack/api/server/_pools.py +0 -63
- tests/_internal/server/routers/test_pools.py +0 -612
- /dstack/_internal/{server/services/backends/configurators → core/backends/dstack}/__init__.py +0 -0
- {dstack-0.18.44.dist-info → dstack-0.19.0rc1.dist-info}/LICENSE.md +0 -0
- {dstack-0.18.44.dist-info → dstack-0.19.0rc1.dist-info}/WHEEL +0 -0
- {dstack-0.18.44.dist-info → dstack-0.19.0rc1.dist-info}/entry_points.txt +0 -0
- {dstack-0.18.44.dist-info → dstack-0.19.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -7,8 +7,11 @@ from typing import Optional
|
|
|
7
7
|
from sqlalchemy import select
|
|
8
8
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
9
|
|
|
10
|
+
from dstack._internal.core.models.instances import Resources
|
|
10
11
|
from dstack._internal.core.models.metrics import JobMetrics, Metric
|
|
11
12
|
from dstack._internal.server.models import JobMetricsPoint, JobModel
|
|
13
|
+
from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
|
|
14
|
+
from dstack._internal.utils.common import get_or_error
|
|
12
15
|
from dstack._internal.utils.logging import get_logger
|
|
13
16
|
|
|
14
17
|
logger = get_logger(__name__)
|
|
@@ -47,10 +50,10 @@ async def get_job_metrics(
|
|
|
47
50
|
# we need at least 2 points to calculate cpu_usage_percent
|
|
48
51
|
if len(points) < 2:
|
|
49
52
|
return JobMetrics(metrics=[])
|
|
50
|
-
return _calculate_job_metrics(points)
|
|
53
|
+
return _calculate_job_metrics(job_model, points)
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
|
|
56
|
+
def _calculate_job_metrics(job_model: JobModel, points: Sequence[JobMetricsPoint]) -> JobMetrics:
|
|
54
57
|
timestamps: list[datetime] = []
|
|
55
58
|
cpu_usage_points: list[int] = []
|
|
56
59
|
memory_usage_points: list[int] = []
|
|
@@ -58,6 +61,23 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
|
|
|
58
61
|
gpus_memory_usage_points: defaultdict[int, list[int]] = defaultdict(list)
|
|
59
62
|
gpus_util_points: defaultdict[int, list[int]] = defaultdict(list)
|
|
60
63
|
|
|
64
|
+
cpus_detected_num: Optional[int] = None
|
|
65
|
+
memory_total: Optional[int] = None
|
|
66
|
+
gpu_memory_total: Optional[int] = None
|
|
67
|
+
resources: Optional[Resources] = None
|
|
68
|
+
jrd = get_job_runtime_data(job_model)
|
|
69
|
+
if jrd is not None and jrd.offer is not None:
|
|
70
|
+
resources = jrd.offer.instance.resources
|
|
71
|
+
else:
|
|
72
|
+
jpd = get_job_provisioning_data(job_model)
|
|
73
|
+
if jpd is not None:
|
|
74
|
+
resources = jpd.instance_type.resources
|
|
75
|
+
if resources is not None:
|
|
76
|
+
cpus_detected_num = resources.cpus
|
|
77
|
+
memory_total = resources.memory_mib * 1024 * 1024
|
|
78
|
+
if len(resources.gpus) > 0:
|
|
79
|
+
gpu_memory_total = resources.gpus[0].memory_mib * 1024 * 1024
|
|
80
|
+
|
|
61
81
|
gpus_detected_num: Optional[int] = None
|
|
62
82
|
gpus_detected_num_mismatch: bool = False
|
|
63
83
|
for point, prev_point in zip(points, points[1:]):
|
|
@@ -93,6 +113,10 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
|
|
|
93
113
|
values=memory_working_set_points,
|
|
94
114
|
),
|
|
95
115
|
]
|
|
116
|
+
if cpus_detected_num is not None:
|
|
117
|
+
metrics.append(_make_constant_metric("cpus_detected_num", timestamps, cpus_detected_num))
|
|
118
|
+
if memory_total is not None:
|
|
119
|
+
metrics.append(_make_constant_metric("memory_total_bytes", timestamps, memory_total))
|
|
96
120
|
if gpus_detected_num_mismatch:
|
|
97
121
|
# If number of GPUs changed in the time window, skip GPU metrics altogether, otherwise
|
|
98
122
|
# results can be unpredictable (e.g, one GPU takes place of another, as they are
|
|
@@ -100,18 +124,12 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
|
|
|
100
124
|
logger.warning("gpus_detected_num mismatch, skipping GPU metrics")
|
|
101
125
|
else:
|
|
102
126
|
metrics.append(
|
|
103
|
-
|
|
104
|
-
# and the earliest in the batch
|
|
105
|
-
Metric(
|
|
106
|
-
name="gpus_detected_num",
|
|
107
|
-
timestamps=[timestamps[0], timestamps[-1]]
|
|
108
|
-
if len(timestamps) > 1
|
|
109
|
-
else [timestamps[0]],
|
|
110
|
-
values=[gpus_detected_num, gpus_detected_num]
|
|
111
|
-
if len(timestamps) > 1
|
|
112
|
-
else [gpus_detected_num],
|
|
113
|
-
)
|
|
127
|
+
_make_constant_metric("gpus_detected_num", timestamps, get_or_error(gpus_detected_num))
|
|
114
128
|
)
|
|
129
|
+
if gpu_memory_total is not None:
|
|
130
|
+
metrics.append(
|
|
131
|
+
_make_constant_metric("gpu_memory_total_bytes", timestamps, gpu_memory_total)
|
|
132
|
+
)
|
|
115
133
|
for index, gpu_memory_usage_points in gpus_memory_usage_points.items():
|
|
116
134
|
metrics.append(
|
|
117
135
|
Metric(
|
|
@@ -131,6 +149,14 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
|
|
|
131
149
|
return JobMetrics(metrics=metrics)
|
|
132
150
|
|
|
133
151
|
|
|
152
|
+
def _make_constant_metric(name: str, timestamps: list[datetime], value: float) -> Metric:
|
|
153
|
+
return Metric(
|
|
154
|
+
name=name,
|
|
155
|
+
timestamps=timestamps,
|
|
156
|
+
values=[value] * len(timestamps),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
134
160
|
def _get_cpu_usage(last_point: JobMetricsPoint, prev_point: JobMetricsPoint) -> int:
|
|
135
161
|
window = last_point.timestamp_micro - prev_point.timestamp_micro
|
|
136
162
|
if window == 0:
|
|
@@ -7,7 +7,7 @@ from dstack._internal.core.backends import (
|
|
|
7
7
|
BACKENDS_WITH_MULTINODE_SUPPORT,
|
|
8
8
|
BACKENDS_WITH_RESERVATION_SUPPORT,
|
|
9
9
|
)
|
|
10
|
-
from dstack._internal.core.backends.base import Backend
|
|
10
|
+
from dstack._internal.core.backends.base.backend import Backend
|
|
11
11
|
from dstack._internal.core.models.backends.base import BackendType
|
|
12
12
|
from dstack._internal.core.models.instances import (
|
|
13
13
|
InstanceOfferWithAvailability,
|
|
@@ -7,19 +7,22 @@ from sqlalchemy import func as safunc
|
|
|
7
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
8
|
from sqlalchemy.orm import joinedload
|
|
9
9
|
|
|
10
|
-
from dstack._internal.core.
|
|
11
|
-
from dstack._internal.core.models
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
DstackConfigInfo,
|
|
10
|
+
from dstack._internal.core.backends.configurators import get_configurator
|
|
11
|
+
from dstack._internal.core.backends.dstack.models import (
|
|
12
|
+
DstackBackendConfig,
|
|
13
|
+
DstackBaseBackendConfig,
|
|
15
14
|
)
|
|
15
|
+
from dstack._internal.core.backends.models import BackendInfo
|
|
16
|
+
from dstack._internal.core.errors import ForbiddenError, ResourceExistsError, ServerClientError
|
|
16
17
|
from dstack._internal.core.models.common import is_core_model_instance
|
|
17
18
|
from dstack._internal.core.models.projects import Member, MemberPermissions, Project
|
|
18
19
|
from dstack._internal.core.models.users import GlobalRole, ProjectRole
|
|
19
20
|
from dstack._internal.server.models import MemberModel, ProjectModel, UserModel
|
|
20
21
|
from dstack._internal.server.schemas.projects import MemberSetting
|
|
21
22
|
from dstack._internal.server.services import users
|
|
22
|
-
from dstack._internal.server.services.backends import
|
|
23
|
+
from dstack._internal.server.services.backends import (
|
|
24
|
+
get_backend_config_from_backend_model,
|
|
25
|
+
)
|
|
23
26
|
from dstack._internal.server.services.permissions import get_default_permissions
|
|
24
27
|
from dstack._internal.server.settings import DEFAULT_PROJECT_NAME
|
|
25
28
|
from dstack._internal.utils.common import get_current_datetime, run_async
|
|
@@ -176,12 +179,16 @@ async def set_project_members(
|
|
|
176
179
|
# FIXME: potentially long write transaction
|
|
177
180
|
# clear_project_members() issues DELETE without commit
|
|
178
181
|
await clear_project_members(session=session, project=project)
|
|
179
|
-
|
|
180
|
-
res = await session.execute(
|
|
182
|
+
names = [m.username for m in members]
|
|
183
|
+
res = await session.execute(
|
|
184
|
+
select(UserModel).where((UserModel.name.in_(names)) | (UserModel.email.in_(names)))
|
|
185
|
+
)
|
|
181
186
|
users = res.scalars().all()
|
|
187
|
+
# Create lookup maps for both username and email
|
|
182
188
|
username_to_user = {user.name: user for user in users}
|
|
189
|
+
email_to_user = {user.email: user for user in users if user.email}
|
|
183
190
|
for i, member in enumerate(members):
|
|
184
|
-
user_to_add = username_to_user.get(member.username)
|
|
191
|
+
user_to_add = username_to_user.get(member.username) or email_to_user.get(member.username)
|
|
185
192
|
if user_to_add is None:
|
|
186
193
|
continue
|
|
187
194
|
await add_project_member(
|
|
@@ -376,20 +383,22 @@ def project_model_to_project(
|
|
|
376
383
|
b.type.value,
|
|
377
384
|
)
|
|
378
385
|
continue
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
386
|
+
backend_config = get_backend_config_from_backend_model(
|
|
387
|
+
configurator, b, include_creds=False
|
|
388
|
+
)
|
|
389
|
+
if is_core_model_instance(backend_config, DstackBackendConfig):
|
|
390
|
+
for backend_type in backend_config.base_backends:
|
|
382
391
|
backends.append(
|
|
383
392
|
BackendInfo(
|
|
384
393
|
name=backend_type,
|
|
385
|
-
config=
|
|
394
|
+
config=DstackBaseBackendConfig(type=backend_type),
|
|
386
395
|
)
|
|
387
396
|
)
|
|
388
397
|
else:
|
|
389
398
|
backends.append(
|
|
390
399
|
BackendInfo(
|
|
391
400
|
name=b.type,
|
|
392
|
-
config=
|
|
401
|
+
config=backend_config,
|
|
393
402
|
)
|
|
394
403
|
)
|
|
395
404
|
return Project(
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import itertools
|
|
1
2
|
from collections.abc import Generator, Iterable
|
|
3
|
+
from datetime import timezone
|
|
2
4
|
|
|
3
5
|
from prometheus_client import Metric
|
|
4
6
|
from prometheus_client.parser import text_string_to_metric_families
|
|
@@ -7,21 +9,172 @@ from sqlalchemy import select
|
|
|
7
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
10
|
from sqlalchemy.orm import joinedload
|
|
9
11
|
|
|
10
|
-
from dstack._internal.core.models.
|
|
11
|
-
from dstack._internal.
|
|
12
|
+
from dstack._internal.core.models.instances import InstanceStatus
|
|
13
|
+
from dstack._internal.core.models.runs import JobStatus, RunSpec
|
|
14
|
+
from dstack._internal.server.models import (
|
|
15
|
+
InstanceModel,
|
|
16
|
+
JobModel,
|
|
17
|
+
JobPrometheusMetrics,
|
|
18
|
+
ProjectModel,
|
|
19
|
+
RunModel,
|
|
20
|
+
)
|
|
21
|
+
from dstack._internal.server.services.instances import get_instance_offer
|
|
22
|
+
from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
|
|
23
|
+
from dstack._internal.utils.common import get_current_datetime
|
|
24
|
+
|
|
25
|
+
_INSTANCE_DURATION = "dstack_instance_duration_seconds_total"
|
|
26
|
+
_INSTANCE_PRICE = "dstack_instance_price_dollars_per_hour"
|
|
27
|
+
_INSTANCE_GPU_COUNT = "dstack_instance_gpu_count"
|
|
28
|
+
_JOB_DURATION = "dstack_job_duration_seconds_total"
|
|
29
|
+
_JOB_PRICE = "dstack_job_price_dollars_per_hour"
|
|
30
|
+
_JOB_GPU_COUNT = "dstack_job_gpu_count"
|
|
12
31
|
|
|
13
32
|
|
|
14
33
|
async def get_metrics(session: AsyncSession) -> str:
|
|
34
|
+
metrics_iter = itertools.chain(
|
|
35
|
+
await get_instance_metrics(session),
|
|
36
|
+
await get_job_metrics(session),
|
|
37
|
+
await get_job_gpu_metrics(session),
|
|
38
|
+
)
|
|
39
|
+
return "\n".join(_render_metrics(metrics_iter)) + "\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def get_instance_metrics(session: AsyncSession) -> Iterable[Metric]:
|
|
43
|
+
res = await session.execute(
|
|
44
|
+
select(InstanceModel)
|
|
45
|
+
.join(ProjectModel)
|
|
46
|
+
.where(
|
|
47
|
+
InstanceModel.deleted == False,
|
|
48
|
+
InstanceModel.status.in_(
|
|
49
|
+
[
|
|
50
|
+
InstanceStatus.PROVISIONING,
|
|
51
|
+
InstanceStatus.IDLE,
|
|
52
|
+
InstanceStatus.BUSY,
|
|
53
|
+
InstanceStatus.TERMINATING,
|
|
54
|
+
]
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
.order_by(ProjectModel.name, InstanceModel.name)
|
|
58
|
+
.options(
|
|
59
|
+
joinedload(InstanceModel.project),
|
|
60
|
+
joinedload(InstanceModel.fleet),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
instances = res.unique().scalars().all()
|
|
64
|
+
metrics: dict[str, Metric] = {
|
|
65
|
+
_INSTANCE_DURATION: Metric(
|
|
66
|
+
name=_INSTANCE_DURATION,
|
|
67
|
+
documentation="Total seconds the instance is running",
|
|
68
|
+
typ="counter",
|
|
69
|
+
),
|
|
70
|
+
_INSTANCE_PRICE: Metric(
|
|
71
|
+
name=_INSTANCE_PRICE, documentation="Instance price, USD/hour", typ="gauge"
|
|
72
|
+
),
|
|
73
|
+
_INSTANCE_GPU_COUNT: Metric(
|
|
74
|
+
name=_INSTANCE_GPU_COUNT, documentation="Instance GPU count", typ="gauge"
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
now = get_current_datetime()
|
|
78
|
+
for instance in instances:
|
|
79
|
+
fleet = instance.fleet
|
|
80
|
+
offer = get_instance_offer(instance)
|
|
81
|
+
gpu = ""
|
|
82
|
+
gpu_count = 0
|
|
83
|
+
if offer is not None and len(offer.instance.resources.gpus) > 0:
|
|
84
|
+
gpu = offer.instance.resources.gpus[0].name
|
|
85
|
+
gpu_count = len(offer.instance.resources.gpus)
|
|
86
|
+
labels: dict[str, str] = {
|
|
87
|
+
"dstack_project_name": instance.project.name,
|
|
88
|
+
"dstack_fleet_name": fleet.name if fleet is not None else "",
|
|
89
|
+
"dstack_fleet_id": str(fleet.id) if fleet is not None else "",
|
|
90
|
+
"dstack_instance_name": str(instance.name),
|
|
91
|
+
"dstack_instance_id": str(instance.id),
|
|
92
|
+
"dstack_instance_type": offer.instance.name if offer is not None else "",
|
|
93
|
+
"dstack_backend": instance.backend.value if instance.backend is not None else "",
|
|
94
|
+
"dstack_gpu": gpu,
|
|
95
|
+
}
|
|
96
|
+
duration = (now - instance.created_at.replace(tzinfo=timezone.utc)).total_seconds()
|
|
97
|
+
metrics[_INSTANCE_DURATION].add_sample(
|
|
98
|
+
name=_INSTANCE_DURATION, labels=labels, value=duration
|
|
99
|
+
)
|
|
100
|
+
metrics[_INSTANCE_PRICE].add_sample(
|
|
101
|
+
name=_INSTANCE_PRICE, labels=labels, value=instance.price or 0.0
|
|
102
|
+
)
|
|
103
|
+
metrics[_INSTANCE_GPU_COUNT].add_sample(
|
|
104
|
+
name=_INSTANCE_GPU_COUNT, labels=labels, value=gpu_count
|
|
105
|
+
)
|
|
106
|
+
return metrics.values()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
|
|
110
|
+
res = await session.execute(
|
|
111
|
+
select(JobModel)
|
|
112
|
+
.join(ProjectModel)
|
|
113
|
+
.where(
|
|
114
|
+
JobModel.status.in_(
|
|
115
|
+
[
|
|
116
|
+
JobStatus.PROVISIONING,
|
|
117
|
+
JobStatus.PULLING,
|
|
118
|
+
JobStatus.RUNNING,
|
|
119
|
+
JobStatus.TERMINATING,
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
.order_by(ProjectModel.name, JobModel.job_name)
|
|
124
|
+
.options(
|
|
125
|
+
joinedload(JobModel.project),
|
|
126
|
+
joinedload(JobModel.run).joinedload(RunModel.user),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
jobs = res.scalars().all()
|
|
130
|
+
metrics: dict[str, Metric] = {
|
|
131
|
+
_JOB_DURATION: Metric(
|
|
132
|
+
name=_JOB_DURATION, documentation="Total seconds the job is running", typ="counter"
|
|
133
|
+
),
|
|
134
|
+
_JOB_PRICE: Metric(
|
|
135
|
+
name=_JOB_PRICE, documentation="Job instance price, USD/hour", typ="gauge"
|
|
136
|
+
),
|
|
137
|
+
_JOB_GPU_COUNT: Metric(name=_JOB_GPU_COUNT, documentation="Job GPU count", typ="gauge"),
|
|
138
|
+
}
|
|
139
|
+
now = get_current_datetime()
|
|
140
|
+
for job in jobs:
|
|
141
|
+
jpd = get_job_provisioning_data(job)
|
|
142
|
+
if jpd is None:
|
|
143
|
+
continue
|
|
144
|
+
jrd = get_job_runtime_data(job)
|
|
145
|
+
gpus = jpd.instance_type.resources.gpus
|
|
146
|
+
price = jpd.price
|
|
147
|
+
if jrd is not None and jrd.offer is not None:
|
|
148
|
+
gpus = jrd.offer.instance.resources.gpus
|
|
149
|
+
price = jrd.offer.price
|
|
150
|
+
run_spec = RunSpec.__response__.parse_raw(job.run.run_spec)
|
|
151
|
+
labels = _get_job_labels(job)
|
|
152
|
+
labels["dstack_run_type"] = run_spec.configuration.type
|
|
153
|
+
labels["dstack_backend"] = jpd.get_base_backend().value
|
|
154
|
+
labels["dstack_gpu"] = gpus[0].name if gpus else ""
|
|
155
|
+
duration = (now - job.submitted_at.replace(tzinfo=timezone.utc)).total_seconds()
|
|
156
|
+
metrics[_JOB_DURATION].add_sample(name=_JOB_DURATION, labels=labels, value=duration)
|
|
157
|
+
metrics[_JOB_PRICE].add_sample(name=_JOB_PRICE, labels=labels, value=price)
|
|
158
|
+
metrics[_JOB_GPU_COUNT].add_sample(name=_JOB_GPU_COUNT, labels=labels, value=len(gpus))
|
|
159
|
+
return metrics.values()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def get_job_gpu_metrics(session: AsyncSession) -> Iterable[Metric]:
|
|
15
163
|
res = await session.execute(
|
|
16
164
|
select(JobPrometheusMetrics)
|
|
17
165
|
.join(JobModel)
|
|
18
166
|
.join(ProjectModel)
|
|
19
167
|
.where(JobModel.status.in_([JobStatus.RUNNING]))
|
|
20
168
|
.order_by(ProjectModel.name, JobModel.job_name)
|
|
21
|
-
.options(
|
|
169
|
+
.options(
|
|
170
|
+
joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project),
|
|
171
|
+
joinedload(JobPrometheusMetrics.job)
|
|
172
|
+
.joinedload(JobModel.run)
|
|
173
|
+
.joinedload(RunModel.user),
|
|
174
|
+
)
|
|
22
175
|
)
|
|
23
176
|
metrics_models = res.scalars().all()
|
|
24
|
-
return
|
|
177
|
+
return _parse_and_enrich_job_gpu_metrics(metrics_models)
|
|
25
178
|
|
|
26
179
|
|
|
27
180
|
async def get_project_metrics(session: AsyncSession, project: ProjectModel) -> str:
|
|
@@ -33,20 +186,20 @@ async def get_project_metrics(session: AsyncSession, project: ProjectModel) -> s
|
|
|
33
186
|
JobModel.status.in_([JobStatus.RUNNING]),
|
|
34
187
|
)
|
|
35
188
|
.order_by(JobModel.job_name)
|
|
36
|
-
.options(
|
|
189
|
+
.options(
|
|
190
|
+
joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project),
|
|
191
|
+
joinedload(JobPrometheusMetrics.job)
|
|
192
|
+
.joinedload(JobModel.run)
|
|
193
|
+
.joinedload(RunModel.user),
|
|
194
|
+
)
|
|
37
195
|
)
|
|
38
196
|
metrics_models = res.scalars().all()
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _process_metrics(metrics_models: Iterable[JobPrometheusMetrics]) -> str:
|
|
43
|
-
metrics = _parse_and_enrich_metrics(metrics_models)
|
|
44
|
-
if not metrics:
|
|
45
|
-
return ""
|
|
46
|
-
return "\n".join(_render_metrics(metrics)) + "\n"
|
|
197
|
+
return "\n".join(_render_metrics(_parse_and_enrich_job_gpu_metrics(metrics_models))) + "\n"
|
|
47
198
|
|
|
48
199
|
|
|
49
|
-
def
|
|
200
|
+
def _parse_and_enrich_job_gpu_metrics(
|
|
201
|
+
metrics_models: Iterable[JobPrometheusMetrics],
|
|
202
|
+
) -> Iterable[Metric]:
|
|
50
203
|
metrics: dict[str, Metric] = {}
|
|
51
204
|
for metrics_model in metrics_models:
|
|
52
205
|
for metric in text_string_to_metric_families(metrics_model.text):
|
|
@@ -56,18 +209,21 @@ def _parse_and_enrich_metrics(metrics_models: Iterable[JobPrometheusMetrics]) ->
|
|
|
56
209
|
metric = metrics.setdefault(name, metric)
|
|
57
210
|
for sample in samples:
|
|
58
211
|
labels = sample.labels
|
|
59
|
-
labels.update(
|
|
212
|
+
labels.update(_get_job_labels(metrics_model.job))
|
|
60
213
|
# text_string_to_metric_families "fixes" counter names appending _total,
|
|
61
214
|
# we rebuild Sample to revert this
|
|
62
215
|
metric.samples.append(Sample(name, labels, *sample[2:]))
|
|
63
|
-
return
|
|
216
|
+
return metrics.values()
|
|
64
217
|
|
|
65
218
|
|
|
66
|
-
def
|
|
219
|
+
def _get_job_labels(job: JobModel) -> dict[str, str]:
|
|
67
220
|
return {
|
|
68
221
|
"dstack_project_name": job.project.name,
|
|
222
|
+
"dstack_user_name": job.run.user.name,
|
|
69
223
|
"dstack_run_name": job.run_name,
|
|
224
|
+
"dstack_run_id": str(job.run_id),
|
|
70
225
|
"dstack_job_name": job.job_name,
|
|
226
|
+
"dstack_job_id": str(job.id),
|
|
71
227
|
"dstack_job_num": str(job.job_num),
|
|
72
228
|
"dstack_replica_num": str(job.replica_num),
|
|
73
229
|
}
|
|
@@ -75,12 +231,14 @@ def _get_dstack_labels(job: JobModel) -> dict[str, str]:
|
|
|
75
231
|
|
|
76
232
|
def _render_metrics(metrics: Iterable[Metric]) -> Generator[str, None, None]:
|
|
77
233
|
for metric in metrics:
|
|
234
|
+
if not metric.samples:
|
|
235
|
+
continue
|
|
78
236
|
yield f"# HELP {metric.name} {metric.documentation}"
|
|
79
237
|
yield f"# TYPE {metric.name} {metric.type}"
|
|
80
238
|
for sample in metric.samples:
|
|
81
239
|
parts: list[str] = [f"{sample.name}{{"]
|
|
82
240
|
parts.extend(",".join(f'{name}="{value}"' for name, value in sample.labels.items()))
|
|
83
|
-
parts.append(f"}} {sample.value}")
|
|
241
|
+
parts.append(f"}} {float(sample.value)}")
|
|
84
242
|
# text_string_to_metric_families converts milliseconds to float seconds
|
|
85
243
|
if isinstance(sample.timestamp, float):
|
|
86
244
|
parts.append(f" {int(sample.timestamp * 1000)}")
|
|
@@ -52,7 +52,6 @@ from dstack._internal.server import settings
|
|
|
52
52
|
from dstack._internal.server.db import get_db
|
|
53
53
|
from dstack._internal.server.models import (
|
|
54
54
|
JobModel,
|
|
55
|
-
PoolModel,
|
|
56
55
|
ProjectModel,
|
|
57
56
|
RepoModel,
|
|
58
57
|
RunModel,
|
|
@@ -61,6 +60,12 @@ from dstack._internal.server.models import (
|
|
|
61
60
|
from dstack._internal.server.services import repos as repos_services
|
|
62
61
|
from dstack._internal.server.services import services
|
|
63
62
|
from dstack._internal.server.services.docker import is_valid_docker_volume_target
|
|
63
|
+
from dstack._internal.server.services.instances import (
|
|
64
|
+
filter_pool_instances,
|
|
65
|
+
get_instance_offer,
|
|
66
|
+
get_pool_instances,
|
|
67
|
+
get_shared_pool_instances_with_offers,
|
|
68
|
+
)
|
|
64
69
|
from dstack._internal.server.services.jobs import (
|
|
65
70
|
check_can_attach_job_volumes,
|
|
66
71
|
delay_job_instance_termination,
|
|
@@ -74,13 +79,6 @@ from dstack._internal.server.services.jobs import (
|
|
|
74
79
|
from dstack._internal.server.services.locking import get_locker, string_to_lock_id
|
|
75
80
|
from dstack._internal.server.services.logging import fmt
|
|
76
81
|
from dstack._internal.server.services.offers import get_offers_by_requirements
|
|
77
|
-
from dstack._internal.server.services.pools import (
|
|
78
|
-
filter_pool_instances,
|
|
79
|
-
get_instance_offer,
|
|
80
|
-
get_or_create_pool_by_name,
|
|
81
|
-
get_pool_instances,
|
|
82
|
-
get_shared_pool_instances_with_offers,
|
|
83
|
-
)
|
|
84
82
|
from dstack._internal.server.services.projects import list_project_models, list_user_project_models
|
|
85
83
|
from dstack._internal.server.services.users import get_user_model_by_name
|
|
86
84
|
from dstack._internal.utils.logging import get_logger
|
|
@@ -308,12 +306,9 @@ async def get_plan(
|
|
|
308
306
|
job_num=0,
|
|
309
307
|
)
|
|
310
308
|
|
|
311
|
-
pool = await get_or_create_pool_by_name(
|
|
312
|
-
session=session, project=project, pool_name=profile.pool_name
|
|
313
|
-
)
|
|
314
309
|
pool_offers = await _get_pool_offers(
|
|
315
310
|
session=session,
|
|
316
|
-
|
|
311
|
+
project=project,
|
|
317
312
|
run_spec=run_spec,
|
|
318
313
|
job=jobs[0],
|
|
319
314
|
volumes=volumes,
|
|
@@ -342,8 +337,11 @@ async def get_plan(
|
|
|
342
337
|
job_offers.extend(offer for _, offer in offers)
|
|
343
338
|
job_offers.sort(key=lambda offer: not offer.availability.is_available())
|
|
344
339
|
|
|
340
|
+
job_spec = job.job_spec
|
|
341
|
+
_remove_job_spec_sensitive_info(job_spec)
|
|
342
|
+
|
|
345
343
|
job_plan = JobPlan(
|
|
346
|
-
job_spec=
|
|
344
|
+
job_spec=job_spec,
|
|
347
345
|
offers=job_offers[:50],
|
|
348
346
|
total_offers=len(job_offers),
|
|
349
347
|
max_price=max((offer.price for offer in job_offers), default=None),
|
|
@@ -619,7 +617,10 @@ async def delete_runs(
|
|
|
619
617
|
|
|
620
618
|
|
|
621
619
|
def run_model_to_run(
|
|
622
|
-
run_model: RunModel,
|
|
620
|
+
run_model: RunModel,
|
|
621
|
+
include_job_submissions: bool = True,
|
|
622
|
+
return_in_api: bool = False,
|
|
623
|
+
include_sensitive: bool = False,
|
|
623
624
|
) -> Run:
|
|
624
625
|
jobs: List[Job] = []
|
|
625
626
|
run_jobs = sorted(run_model.jobs, key=lambda j: (j.replica_num, j.job_num, j.submission_num))
|
|
@@ -634,6 +635,8 @@ def run_model_to_run(
|
|
|
634
635
|
for job_model in job_submissions:
|
|
635
636
|
if job_spec is None:
|
|
636
637
|
job_spec = JobSpec.__response__.parse_raw(job_model.job_spec_data)
|
|
638
|
+
if not include_sensitive:
|
|
639
|
+
_remove_job_spec_sensitive_info(job_spec)
|
|
637
640
|
if include_job_submissions:
|
|
638
641
|
job_submission = job_model_to_job_submission(job_model)
|
|
639
642
|
if return_in_api:
|
|
@@ -680,7 +683,7 @@ def run_model_to_run(
|
|
|
680
683
|
|
|
681
684
|
async def _get_pool_offers(
|
|
682
685
|
session: AsyncSession,
|
|
683
|
-
|
|
686
|
+
project: ProjectModel,
|
|
684
687
|
run_spec: RunSpec,
|
|
685
688
|
job: Job,
|
|
686
689
|
volumes: List[List[Volume]],
|
|
@@ -688,7 +691,8 @@ async def _get_pool_offers(
|
|
|
688
691
|
pool_offers: list[InstanceOfferWithAvailability] = []
|
|
689
692
|
|
|
690
693
|
detaching_instances_ids = await get_instances_ids_with_detaching_volumes(session)
|
|
691
|
-
pool_instances =
|
|
694
|
+
pool_instances = await get_pool_instances(session, project)
|
|
695
|
+
pool_instances = [i for i in pool_instances if i.id not in detaching_instances_ids]
|
|
692
696
|
multinode = job.job_spec.jobs_per_replica > 1
|
|
693
697
|
|
|
694
698
|
if not multinode:
|
|
@@ -1046,3 +1050,7 @@ async def retry_run_replica_jobs(
|
|
|
1046
1050
|
# dirty hack to avoid passing all job submissions
|
|
1047
1051
|
new_job_model.submission_num = job_model.submission_num + 1
|
|
1048
1052
|
session.add(new_job_model)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _remove_job_spec_sensitive_info(spec: JobSpec):
|
|
1056
|
+
spec.ssh_key = None
|
|
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
7
7
|
from sqlalchemy.orm import joinedload, selectinload
|
|
8
8
|
|
|
9
9
|
from dstack._internal.core.backends import BACKENDS_WITH_VOLUMES_SUPPORT
|
|
10
|
+
from dstack._internal.core.backends.base.compute import ComputeWithVolumeSupport
|
|
10
11
|
from dstack._internal.core.errors import (
|
|
11
12
|
BackendNotAvailable,
|
|
12
13
|
ResourceExistsError,
|
|
@@ -32,11 +33,11 @@ from dstack._internal.server.models import (
|
|
|
32
33
|
VolumeModel,
|
|
33
34
|
)
|
|
34
35
|
from dstack._internal.server.services import backends as backends_services
|
|
36
|
+
from dstack._internal.server.services.instances import get_instance_provisioning_data
|
|
35
37
|
from dstack._internal.server.services.locking import (
|
|
36
38
|
get_locker,
|
|
37
39
|
string_to_lock_id,
|
|
38
40
|
)
|
|
39
|
-
from dstack._internal.server.services.pools import get_instance_provisioning_data
|
|
40
41
|
from dstack._internal.server.services.projects import list_project_models, list_user_project_models
|
|
41
42
|
from dstack._internal.utils import common, random_names
|
|
42
43
|
from dstack._internal.utils.logging import get_logger
|
|
@@ -375,10 +376,11 @@ async def generate_volume_name(session: AsyncSession, project: ProjectModel) ->
|
|
|
375
376
|
def _validate_volume_configuration(configuration: VolumeConfiguration):
|
|
376
377
|
if configuration.volume_id is None and configuration.size is None:
|
|
377
378
|
raise ServerClientError("Volume must specify either volume_id or size")
|
|
379
|
+
backends_services.check_backend_type_available(configuration.backend)
|
|
378
380
|
if configuration.backend not in BACKENDS_WITH_VOLUMES_SUPPORT:
|
|
379
381
|
raise ServerClientError(
|
|
380
|
-
f"Volumes are not supported for {configuration.backend.value} backend.
|
|
381
|
-
f"
|
|
382
|
+
f"Volumes are not supported for {configuration.backend.value} backend."
|
|
383
|
+
f" Available backends with volumes support: {[b.value for b in BACKENDS_WITH_VOLUMES_SUPPORT]}."
|
|
382
384
|
)
|
|
383
385
|
if configuration.name is not None:
|
|
384
386
|
validate_dstack_resource_name(configuration.name)
|
|
@@ -409,7 +411,9 @@ async def _delete_volume(session: AsyncSession, project: ProjectModel, volume_mo
|
|
|
409
411
|
)
|
|
410
412
|
return
|
|
411
413
|
|
|
414
|
+
compute = backend.compute()
|
|
415
|
+
assert isinstance(compute, ComputeWithVolumeSupport)
|
|
412
416
|
await common.run_async(
|
|
413
|
-
|
|
417
|
+
compute.delete_volume,
|
|
414
418
|
volume=volume,
|
|
415
419
|
)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>dstack</title><meta name="description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
2
2
|
"/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"><meta name="og:title" content="dstack"><meta name="og:type" content="article"><meta name="og:image" content="/splash_thumbnail.png"><meta name="og:description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
3
|
-
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-
|
|
3
|
+
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-4fd5a4770eff59325ee3.js"></script><link href="/main-da9f8c06a69c20dac23e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>
|