dstack 0.19.20__py3-none-any.whl → 0.19.22__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/apply.py +8 -3
- dstack/_internal/cli/services/configurators/__init__.py +8 -0
- dstack/_internal/cli/services/configurators/fleet.py +1 -1
- dstack/_internal/cli/services/configurators/gateway.py +1 -1
- dstack/_internal/cli/services/configurators/run.py +11 -1
- dstack/_internal/cli/services/configurators/volume.py +1 -1
- dstack/_internal/cli/utils/common.py +48 -5
- dstack/_internal/cli/utils/fleet.py +5 -5
- dstack/_internal/cli/utils/run.py +32 -0
- dstack/_internal/core/backends/__init__.py +0 -65
- dstack/_internal/core/backends/configurators.py +9 -0
- dstack/_internal/core/backends/features.py +64 -0
- dstack/_internal/core/backends/hotaisle/__init__.py +1 -0
- dstack/_internal/core/backends/hotaisle/api_client.py +109 -0
- dstack/_internal/core/backends/hotaisle/backend.py +16 -0
- dstack/_internal/core/backends/hotaisle/compute.py +225 -0
- dstack/_internal/core/backends/hotaisle/configurator.py +60 -0
- dstack/_internal/core/backends/hotaisle/models.py +45 -0
- dstack/_internal/core/backends/lambdalabs/compute.py +2 -1
- dstack/_internal/core/backends/models.py +8 -0
- dstack/_internal/core/compatibility/fleets.py +2 -0
- dstack/_internal/core/compatibility/runs.py +12 -0
- dstack/_internal/core/models/backends/base.py +2 -0
- dstack/_internal/core/models/configurations.py +139 -1
- dstack/_internal/core/models/health.py +28 -0
- dstack/_internal/core/models/instances.py +2 -0
- dstack/_internal/core/models/logs.py +2 -1
- dstack/_internal/core/models/profiles.py +37 -0
- dstack/_internal/core/models/runs.py +21 -1
- dstack/_internal/core/services/ssh/tunnel.py +7 -0
- dstack/_internal/server/app.py +26 -10
- dstack/_internal/server/background/__init__.py +9 -6
- dstack/_internal/server/background/tasks/process_fleets.py +52 -38
- dstack/_internal/server/background/tasks/process_gateways.py +2 -2
- dstack/_internal/server/background/tasks/process_idle_volumes.py +5 -4
- dstack/_internal/server/background/tasks/process_instances.py +168 -103
- dstack/_internal/server/background/tasks/process_metrics.py +9 -2
- dstack/_internal/server/background/tasks/process_placement_groups.py +2 -0
- dstack/_internal/server/background/tasks/process_probes.py +164 -0
- dstack/_internal/server/background/tasks/process_prometheus_metrics.py +14 -2
- dstack/_internal/server/background/tasks/process_running_jobs.py +142 -124
- dstack/_internal/server/background/tasks/process_runs.py +84 -34
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +12 -10
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +12 -4
- dstack/_internal/server/background/tasks/process_volumes.py +4 -1
- dstack/_internal/server/migrations/versions/25479f540245_add_probes.py +43 -0
- dstack/_internal/server/migrations/versions/50dd7ea98639_index_status_columns.py +55 -0
- dstack/_internal/server/migrations/versions/728b1488b1b4_add_instance_health.py +50 -0
- dstack/_internal/server/migrations/versions/ec02a26a256c_add_runmodel_next_triggered_at.py +38 -0
- dstack/_internal/server/models.py +57 -16
- dstack/_internal/server/routers/instances.py +33 -5
- dstack/_internal/server/schemas/health/dcgm.py +56 -0
- dstack/_internal/server/schemas/instances.py +32 -0
- dstack/_internal/server/schemas/runner.py +5 -0
- dstack/_internal/server/services/fleets.py +19 -10
- dstack/_internal/server/services/gateways/__init__.py +17 -17
- dstack/_internal/server/services/instances.py +113 -15
- dstack/_internal/server/services/jobs/__init__.py +18 -13
- dstack/_internal/server/services/jobs/configurators/base.py +26 -0
- dstack/_internal/server/services/logging.py +4 -2
- dstack/_internal/server/services/logs/aws.py +13 -1
- dstack/_internal/server/services/logs/gcp.py +16 -1
- dstack/_internal/server/services/offers.py +3 -3
- dstack/_internal/server/services/probes.py +6 -0
- dstack/_internal/server/services/projects.py +51 -19
- dstack/_internal/server/services/prometheus/client_metrics.py +3 -0
- dstack/_internal/server/services/prometheus/custom_metrics.py +2 -3
- dstack/_internal/server/services/runner/client.py +52 -20
- dstack/_internal/server/services/runner/ssh.py +4 -4
- dstack/_internal/server/services/runs.py +115 -39
- dstack/_internal/server/services/services/__init__.py +4 -1
- dstack/_internal/server/services/ssh.py +66 -0
- dstack/_internal/server/services/users.py +2 -3
- dstack/_internal/server/services/volumes.py +11 -11
- dstack/_internal/server/settings.py +16 -0
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-8f9ee218d3eb45989682.css → main-03e818b110e1d5705378.css} +1 -1
- dstack/_internal/server/statics/{main-39a767528976f8078166.js → main-cc067b7fd1a8f33f97da.js} +26 -15
- dstack/_internal/server/statics/{main-39a767528976f8078166.js.map → main-cc067b7fd1a8f33f97da.js.map} +1 -1
- dstack/_internal/server/testing/common.py +51 -0
- dstack/_internal/{core/backends/remote → server/utils}/provisioning.py +22 -17
- dstack/_internal/server/utils/sentry_utils.py +12 -0
- dstack/_internal/settings.py +3 -0
- dstack/_internal/utils/common.py +15 -0
- dstack/_internal/utils/cron.py +5 -0
- dstack/api/server/__init__.py +1 -1
- dstack/version.py +1 -1
- {dstack-0.19.20.dist-info → dstack-0.19.22.dist-info}/METADATA +13 -22
- {dstack-0.19.20.dist-info → dstack-0.19.22.dist-info}/RECORD +93 -75
- /dstack/_internal/{core/backends/remote → server/schemas/health}/__init__.py +0 -0
- {dstack-0.19.20.dist-info → dstack-0.19.22.dist-info}/WHEEL +0 -0
- {dstack-0.19.20.dist-info → dstack-0.19.22.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.20.dist-info → dstack-0.19.22.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import math
|
|
3
3
|
import uuid
|
|
4
|
+
from collections.abc import Iterable
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from typing import List, Optional
|
|
6
7
|
|
|
7
8
|
import pydantic
|
|
9
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
8
10
|
from sqlalchemy import and_, func, or_, select, update
|
|
9
11
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
12
|
from sqlalchemy.orm import joinedload, selectinload
|
|
@@ -16,7 +18,11 @@ from dstack._internal.core.errors import (
|
|
|
16
18
|
ServerClientError,
|
|
17
19
|
)
|
|
18
20
|
from dstack._internal.core.models.common import ApplyAction
|
|
19
|
-
from dstack._internal.core.models.configurations import
|
|
21
|
+
from dstack._internal.core.models.configurations import (
|
|
22
|
+
RUN_PRIORITY_DEFAULT,
|
|
23
|
+
AnyRunConfiguration,
|
|
24
|
+
ServiceConfiguration,
|
|
25
|
+
)
|
|
20
26
|
from dstack._internal.core.models.instances import (
|
|
21
27
|
InstanceAvailability,
|
|
22
28
|
InstanceOfferWithAvailability,
|
|
@@ -42,7 +48,6 @@ from dstack._internal.core.models.runs import (
|
|
|
42
48
|
RunTerminationReason,
|
|
43
49
|
ServiceSpec,
|
|
44
50
|
)
|
|
45
|
-
from dstack._internal.core.models.users import GlobalRole
|
|
46
51
|
from dstack._internal.core.models.volumes import (
|
|
47
52
|
InstanceMountPoint,
|
|
48
53
|
Volume,
|
|
@@ -81,7 +86,7 @@ from dstack._internal.server.services.locking import get_locker, string_to_lock_
|
|
|
81
86
|
from dstack._internal.server.services.logging import fmt
|
|
82
87
|
from dstack._internal.server.services.offers import get_offers_by_requirements
|
|
83
88
|
from dstack._internal.server.services.plugins import apply_plugin_policies
|
|
84
|
-
from dstack._internal.server.services.projects import
|
|
89
|
+
from dstack._internal.server.services.projects import list_user_project_models
|
|
85
90
|
from dstack._internal.server.services.resources import set_resources_defaults
|
|
86
91
|
from dstack._internal.server.services.secrets import get_project_secrets_mapping
|
|
87
92
|
from dstack._internal.server.services.users import get_user_model_by_name
|
|
@@ -115,10 +120,11 @@ async def list_user_runs(
|
|
|
115
120
|
) -> List[Run]:
|
|
116
121
|
if project_name is None and repo_id is not None:
|
|
117
122
|
return []
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
projects = await list_user_project_models(
|
|
124
|
+
session=session,
|
|
125
|
+
user=user,
|
|
126
|
+
only_names=True,
|
|
127
|
+
)
|
|
122
128
|
runs_user = None
|
|
123
129
|
if username is not None:
|
|
124
130
|
runs_user = await get_user_model_by_name(session=session, username=username)
|
|
@@ -217,9 +223,10 @@ async def list_projects_run_models(
|
|
|
217
223
|
res = await session.execute(
|
|
218
224
|
select(RunModel)
|
|
219
225
|
.where(*filters)
|
|
226
|
+
.options(joinedload(RunModel.user).load_only(UserModel.name))
|
|
227
|
+
.options(selectinload(RunModel.jobs).joinedload(JobModel.probes))
|
|
220
228
|
.order_by(*order_by)
|
|
221
229
|
.limit(limit)
|
|
222
|
-
.options(selectinload(RunModel.user))
|
|
223
230
|
)
|
|
224
231
|
run_models = list(res.scalars().all())
|
|
225
232
|
return run_models
|
|
@@ -259,6 +266,7 @@ async def get_run_by_name(
|
|
|
259
266
|
RunModel.deleted == False,
|
|
260
267
|
)
|
|
261
268
|
.options(joinedload(RunModel.user))
|
|
269
|
+
.options(selectinload(RunModel.jobs).joinedload(JobModel.probes))
|
|
262
270
|
)
|
|
263
271
|
run_model = res.scalar()
|
|
264
272
|
if run_model is None:
|
|
@@ -278,6 +286,7 @@ async def get_run_by_id(
|
|
|
278
286
|
RunModel.id == run_id,
|
|
279
287
|
)
|
|
280
288
|
.options(joinedload(RunModel.user))
|
|
289
|
+
.options(selectinload(RunModel.jobs).joinedload(JobModel.probes))
|
|
281
290
|
)
|
|
282
291
|
run_model = res.scalar()
|
|
283
292
|
if run_model is None:
|
|
@@ -511,6 +520,14 @@ async def submit_run(
|
|
|
511
520
|
)
|
|
512
521
|
|
|
513
522
|
submitted_at = common_utils.get_current_datetime()
|
|
523
|
+
initial_status = RunStatus.SUBMITTED
|
|
524
|
+
initial_replicas = 1
|
|
525
|
+
if run_spec.merged_profile.schedule is not None:
|
|
526
|
+
initial_status = RunStatus.PENDING
|
|
527
|
+
initial_replicas = 0
|
|
528
|
+
elif run_spec.configuration.type == "service":
|
|
529
|
+
initial_replicas = run_spec.configuration.replicas.min
|
|
530
|
+
|
|
514
531
|
run_model = RunModel(
|
|
515
532
|
id=uuid.uuid4(),
|
|
516
533
|
project_id=project.id,
|
|
@@ -519,21 +536,20 @@ async def submit_run(
|
|
|
519
536
|
user_id=user.id,
|
|
520
537
|
run_name=run_spec.run_name,
|
|
521
538
|
submitted_at=submitted_at,
|
|
522
|
-
status=
|
|
539
|
+
status=initial_status,
|
|
523
540
|
run_spec=run_spec.json(),
|
|
524
541
|
last_processed_at=submitted_at,
|
|
525
542
|
priority=run_spec.configuration.priority,
|
|
526
543
|
deployment_num=0,
|
|
527
544
|
desired_replica_count=1, # a relevant value will be set in process_runs.py
|
|
545
|
+
next_triggered_at=_get_next_triggered_at(run_spec),
|
|
528
546
|
)
|
|
529
547
|
session.add(run_model)
|
|
530
548
|
|
|
531
|
-
replicas = 1
|
|
532
549
|
if run_spec.configuration.type == "service":
|
|
533
|
-
replicas = run_spec.configuration.replicas.min
|
|
534
550
|
await services.register_service(session, run_model, run_spec)
|
|
535
551
|
|
|
536
|
-
for replica_num in range(
|
|
552
|
+
for replica_num in range(initial_replicas):
|
|
537
553
|
jobs = await get_jobs_from_run_spec(
|
|
538
554
|
run_spec=run_spec,
|
|
539
555
|
secrets=secrets,
|
|
@@ -549,8 +565,8 @@ async def submit_run(
|
|
|
549
565
|
await session.commit()
|
|
550
566
|
await session.refresh(run_model)
|
|
551
567
|
|
|
552
|
-
run =
|
|
553
|
-
return run
|
|
568
|
+
run = await get_run_by_id(session, project, run_model.id)
|
|
569
|
+
return common_utils.get_or_error(run)
|
|
554
570
|
|
|
555
571
|
|
|
556
572
|
def create_job_model_for_new_submission(
|
|
@@ -575,6 +591,7 @@ def create_job_model_for_new_submission(
|
|
|
575
591
|
termination_reason=None,
|
|
576
592
|
job_spec_data=job.job_spec.json(),
|
|
577
593
|
job_provisioning_data=None,
|
|
594
|
+
probes=[],
|
|
578
595
|
)
|
|
579
596
|
|
|
580
597
|
|
|
@@ -693,8 +710,8 @@ def run_model_to_run(
|
|
|
693
710
|
id=run_model.id,
|
|
694
711
|
project_name=run_model.project.name,
|
|
695
712
|
user=run_model.user.name,
|
|
696
|
-
submitted_at=run_model.submitted_at
|
|
697
|
-
last_processed_at=run_model.last_processed_at
|
|
713
|
+
submitted_at=run_model.submitted_at,
|
|
714
|
+
last_processed_at=run_model.last_processed_at,
|
|
698
715
|
status=run_model.status,
|
|
699
716
|
status_message=status_message,
|
|
700
717
|
termination_reason=run_model.termination_reason,
|
|
@@ -732,7 +749,9 @@ def _get_run_jobs_with_submissions(
|
|
|
732
749
|
job_models = list(job_models)[-job_submissions_limit:]
|
|
733
750
|
for job_model in job_models:
|
|
734
751
|
if job_submissions_limit != 0:
|
|
735
|
-
job_submission = job_model_to_job_submission(
|
|
752
|
+
job_submission = job_model_to_job_submission(
|
|
753
|
+
job_model, include_probes=return_in_api
|
|
754
|
+
)
|
|
736
755
|
if return_in_api:
|
|
737
756
|
# Set default non-None values for 0.18 backward-compatibility
|
|
738
757
|
# Remove in 0.19
|
|
@@ -972,6 +991,22 @@ def _validate_run_spec_and_set_defaults(run_spec: RunSpec):
|
|
|
972
991
|
raise ServerClientError(
|
|
973
992
|
f"Maximum utilization_policy.time_window is {settings.SERVER_METRICS_RUNNING_TTL_SECONDS}s"
|
|
974
993
|
)
|
|
994
|
+
if isinstance(run_spec.configuration, ServiceConfiguration):
|
|
995
|
+
if run_spec.merged_profile.schedule and run_spec.configuration.replicas.min == 0:
|
|
996
|
+
raise ServerClientError(
|
|
997
|
+
"Scheduled services with autoscaling to zero are not supported"
|
|
998
|
+
)
|
|
999
|
+
if len(run_spec.configuration.probes) > settings.MAX_PROBES_PER_JOB:
|
|
1000
|
+
raise ServerClientError(
|
|
1001
|
+
f"Cannot configure more than {settings.MAX_PROBES_PER_JOB} probes"
|
|
1002
|
+
)
|
|
1003
|
+
if any(
|
|
1004
|
+
p.timeout is not None and p.timeout > settings.MAX_PROBE_TIMEOUT
|
|
1005
|
+
for p in run_spec.configuration.probes
|
|
1006
|
+
):
|
|
1007
|
+
raise ServerClientError(
|
|
1008
|
+
f"Probe timeout cannot be longer than {settings.MAX_PROBE_TIMEOUT}s"
|
|
1009
|
+
)
|
|
975
1010
|
if run_spec.configuration.priority is None:
|
|
976
1011
|
run_spec.configuration.priority = RUN_PRIORITY_DEFAULT
|
|
977
1012
|
set_resources_defaults(run_spec.configuration.resources)
|
|
@@ -997,6 +1032,7 @@ _TYPE_SPECIFIC_CONF_UPDATABLE_FIELDS = {
|
|
|
997
1032
|
# rolling deployment
|
|
998
1033
|
# NOTE: keep this list in sync with the "Rolling deployment" section in services.md
|
|
999
1034
|
"port",
|
|
1035
|
+
"probes",
|
|
1000
1036
|
"resources",
|
|
1001
1037
|
"volumes",
|
|
1002
1038
|
"docker",
|
|
@@ -1059,7 +1095,7 @@ def _check_can_update_configuration(
|
|
|
1059
1095
|
)
|
|
1060
1096
|
|
|
1061
1097
|
|
|
1062
|
-
async def process_terminating_run(session: AsyncSession,
|
|
1098
|
+
async def process_terminating_run(session: AsyncSession, run_model: RunModel):
|
|
1063
1099
|
"""
|
|
1064
1100
|
Used by both `process_runs` and `stop_run` to process a TERMINATING run.
|
|
1065
1101
|
Stops the jobs gracefully and marks them as TERMINATING.
|
|
@@ -1067,44 +1103,54 @@ async def process_terminating_run(session: AsyncSession, run: RunModel):
|
|
|
1067
1103
|
When all jobs are terminated, assigns a finished status to the run.
|
|
1068
1104
|
Caller must acquire the lock on run.
|
|
1069
1105
|
"""
|
|
1070
|
-
assert
|
|
1071
|
-
|
|
1106
|
+
assert run_model.termination_reason is not None
|
|
1107
|
+
run = run_model_to_run(run_model, include_jobs=False)
|
|
1108
|
+
job_termination_reason = run_model.termination_reason.to_job_termination_reason()
|
|
1072
1109
|
|
|
1073
1110
|
unfinished_jobs_count = 0
|
|
1074
|
-
for
|
|
1075
|
-
if
|
|
1111
|
+
for job_model in run_model.jobs:
|
|
1112
|
+
if job_model.status.is_finished():
|
|
1076
1113
|
continue
|
|
1077
1114
|
unfinished_jobs_count += 1
|
|
1078
|
-
if
|
|
1115
|
+
if job_model.status == JobStatus.TERMINATING:
|
|
1079
1116
|
if job_termination_reason == JobTerminationReason.ABORTED_BY_USER:
|
|
1080
1117
|
# Override termination reason so that
|
|
1081
1118
|
# abort actions such as volume force detach are triggered
|
|
1082
|
-
|
|
1119
|
+
job_model.termination_reason = job_termination_reason
|
|
1083
1120
|
continue
|
|
1084
1121
|
|
|
1085
|
-
if
|
|
1122
|
+
if job_model.status == JobStatus.RUNNING and job_termination_reason not in {
|
|
1086
1123
|
JobTerminationReason.ABORTED_BY_USER,
|
|
1087
1124
|
JobTerminationReason.DONE_BY_RUNNER,
|
|
1088
1125
|
}:
|
|
1089
1126
|
# Send a signal to stop the job gracefully
|
|
1090
|
-
await stop_runner(session,
|
|
1091
|
-
delay_job_instance_termination(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1127
|
+
await stop_runner(session, job_model)
|
|
1128
|
+
delay_job_instance_termination(job_model)
|
|
1129
|
+
job_model.status = JobStatus.TERMINATING
|
|
1130
|
+
job_model.termination_reason = job_termination_reason
|
|
1131
|
+
job_model.last_processed_at = common_utils.get_current_datetime()
|
|
1095
1132
|
|
|
1096
1133
|
if unfinished_jobs_count == 0:
|
|
1097
|
-
if
|
|
1134
|
+
if run_model.service_spec is not None:
|
|
1098
1135
|
try:
|
|
1099
|
-
await services.unregister_service(session,
|
|
1136
|
+
await services.unregister_service(session, run_model)
|
|
1100
1137
|
except Exception as e:
|
|
1101
|
-
logger.warning("%s: failed to unregister service: %s", fmt(
|
|
1102
|
-
|
|
1138
|
+
logger.warning("%s: failed to unregister service: %s", fmt(run_model), repr(e))
|
|
1139
|
+
if (
|
|
1140
|
+
run.run_spec.merged_profile.schedule is not None
|
|
1141
|
+
and run_model.termination_reason
|
|
1142
|
+
not in [RunTerminationReason.ABORTED_BY_USER, RunTerminationReason.STOPPED_BY_USER]
|
|
1143
|
+
):
|
|
1144
|
+
run_model.next_triggered_at = _get_next_triggered_at(run.run_spec)
|
|
1145
|
+
run_model.status = RunStatus.PENDING
|
|
1146
|
+
else:
|
|
1147
|
+
run_model.status = run_model.termination_reason.to_status()
|
|
1148
|
+
|
|
1103
1149
|
logger.info(
|
|
1104
1150
|
"%s: run status has changed TERMINATING -> %s, reason: %s",
|
|
1105
|
-
fmt(
|
|
1106
|
-
|
|
1107
|
-
|
|
1151
|
+
fmt(run_model),
|
|
1152
|
+
run_model.status.name,
|
|
1153
|
+
run_model.termination_reason.name,
|
|
1108
1154
|
)
|
|
1109
1155
|
|
|
1110
1156
|
|
|
@@ -1137,9 +1183,12 @@ async def scale_run_replicas(session: AsyncSession, run_model: RunModel, replica
|
|
|
1137
1183
|
elif {JobStatus.PROVISIONING, JobStatus.PULLING} & statuses:
|
|
1138
1184
|
# if there are any provisioning or pulling jobs, the replica is active and has the importance of 1
|
|
1139
1185
|
active_replicas.append((1, is_out_of_date, replica_num, replica_jobs))
|
|
1140
|
-
|
|
1141
|
-
# all jobs are running, the replica is active and has the importance of 2
|
|
1186
|
+
elif not is_replica_ready(replica_jobs):
|
|
1187
|
+
# all jobs are running, but probes are failing, the replica is active and has the importance of 2
|
|
1142
1188
|
active_replicas.append((2, is_out_of_date, replica_num, replica_jobs))
|
|
1189
|
+
else:
|
|
1190
|
+
# all jobs are running and ready, the replica is active and has the importance of 3
|
|
1191
|
+
active_replicas.append((3, is_out_of_date, replica_num, replica_jobs))
|
|
1143
1192
|
|
|
1144
1193
|
# sort by is_out_of_date (up-to-date first), importance (desc), and replica_num (asc)
|
|
1145
1194
|
active_replicas.sort(key=lambda r: (r[1], -r[0], r[2]))
|
|
@@ -1222,5 +1271,32 @@ async def retry_run_replica_jobs(
|
|
|
1222
1271
|
session.add(new_job_model)
|
|
1223
1272
|
|
|
1224
1273
|
|
|
1274
|
+
def is_replica_ready(jobs: Iterable[JobModel]) -> bool:
|
|
1275
|
+
if not all(job.status == JobStatus.RUNNING for job in jobs):
|
|
1276
|
+
return False
|
|
1277
|
+
for job in jobs:
|
|
1278
|
+
job_spec: JobSpec = JobSpec.__response__.parse_raw(job.job_spec_data)
|
|
1279
|
+
for probe_spec, probe in zip(job_spec.probes, job.probes):
|
|
1280
|
+
if probe.success_streak < probe_spec.ready_after:
|
|
1281
|
+
return False
|
|
1282
|
+
return True
|
|
1283
|
+
|
|
1284
|
+
|
|
1225
1285
|
def _remove_job_spec_sensitive_info(spec: JobSpec):
|
|
1226
1286
|
spec.ssh_key = None
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def _get_next_triggered_at(run_spec: RunSpec) -> Optional[datetime]:
|
|
1290
|
+
if run_spec.merged_profile.schedule is None:
|
|
1291
|
+
return None
|
|
1292
|
+
now = common_utils.get_current_datetime()
|
|
1293
|
+
fire_times = []
|
|
1294
|
+
for cron in run_spec.merged_profile.schedule.crons:
|
|
1295
|
+
cron_trigger = CronTrigger.from_crontab(cron, timezone=timezone.utc)
|
|
1296
|
+
fire_times.append(
|
|
1297
|
+
cron_trigger.get_next_fire_time(
|
|
1298
|
+
previous_fire_time=None,
|
|
1299
|
+
now=now,
|
|
1300
|
+
)
|
|
1301
|
+
)
|
|
1302
|
+
return min(fire_times)
|
|
@@ -28,6 +28,7 @@ from dstack._internal.server.models import GatewayModel, JobModel, ProjectModel,
|
|
|
28
28
|
from dstack._internal.server.services.gateways import (
|
|
29
29
|
get_gateway_configuration,
|
|
30
30
|
get_or_add_gateway_connection,
|
|
31
|
+
get_project_default_gateway_model,
|
|
31
32
|
get_project_gateway_model_by_name,
|
|
32
33
|
)
|
|
33
34
|
from dstack._internal.server.services.logging import fmt
|
|
@@ -52,7 +53,9 @@ async def register_service(session: AsyncSession, run_model: RunModel, run_spec:
|
|
|
52
53
|
elif run_spec.configuration.gateway == False:
|
|
53
54
|
gateway = None
|
|
54
55
|
else:
|
|
55
|
-
gateway =
|
|
56
|
+
gateway = await get_project_default_gateway_model(
|
|
57
|
+
session=session, project=run_model.project
|
|
58
|
+
)
|
|
56
59
|
|
|
57
60
|
if gateway is not None:
|
|
58
61
|
service_spec = await _register_service_in_gateway(session, run_model, run_spec, gateway)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import dstack._internal.server.services.jobs as jobs_services
|
|
5
|
+
from dstack._internal.core.consts import DSTACK_RUNNER_SSH_PORT
|
|
6
|
+
from dstack._internal.core.models.backends.base import BackendType
|
|
7
|
+
from dstack._internal.core.models.instances import RemoteConnectionInfo, SSHConnectionParams
|
|
8
|
+
from dstack._internal.core.models.runs import JobProvisioningData
|
|
9
|
+
from dstack._internal.core.services.ssh.tunnel import SSH_DEFAULT_OPTIONS, SocketPair, SSHTunnel
|
|
10
|
+
from dstack._internal.server.models import JobModel
|
|
11
|
+
from dstack._internal.utils.common import get_or_error
|
|
12
|
+
from dstack._internal.utils.path import FileContent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def container_ssh_tunnel(
|
|
16
|
+
job: JobModel,
|
|
17
|
+
forwarded_sockets: Iterable[SocketPair] = (),
|
|
18
|
+
options: dict[str, str] = SSH_DEFAULT_OPTIONS,
|
|
19
|
+
) -> SSHTunnel:
|
|
20
|
+
"""
|
|
21
|
+
Build SSHTunnel for connecting to the container running the specified job.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
jpd: JobProvisioningData = JobProvisioningData.__response__.parse_raw(
|
|
25
|
+
job.job_provisioning_data
|
|
26
|
+
)
|
|
27
|
+
if not jpd.dockerized:
|
|
28
|
+
ssh_destination = f"{jpd.username}@{jpd.hostname}"
|
|
29
|
+
ssh_port = jpd.ssh_port
|
|
30
|
+
ssh_proxy = jpd.ssh_proxy
|
|
31
|
+
else:
|
|
32
|
+
ssh_destination = "root@localhost" # TODO(#1535): support non-root images properly
|
|
33
|
+
ssh_port = DSTACK_RUNNER_SSH_PORT
|
|
34
|
+
job_submission = jobs_services.job_model_to_job_submission(job)
|
|
35
|
+
jrd = job_submission.job_runtime_data
|
|
36
|
+
if jrd is not None and jrd.ports is not None:
|
|
37
|
+
ssh_port = jrd.ports.get(ssh_port, ssh_port)
|
|
38
|
+
ssh_proxy = SSHConnectionParams(
|
|
39
|
+
hostname=jpd.hostname,
|
|
40
|
+
username=jpd.username,
|
|
41
|
+
port=jpd.ssh_port,
|
|
42
|
+
)
|
|
43
|
+
if jpd.backend == BackendType.LOCAL:
|
|
44
|
+
ssh_proxy = None
|
|
45
|
+
ssh_head_proxy: Optional[SSHConnectionParams] = None
|
|
46
|
+
ssh_head_proxy_private_key: Optional[str] = None
|
|
47
|
+
instance = get_or_error(job.instance)
|
|
48
|
+
if instance.remote_connection_info is not None:
|
|
49
|
+
rci = RemoteConnectionInfo.__response__.parse_raw(instance.remote_connection_info)
|
|
50
|
+
if rci.ssh_proxy is not None:
|
|
51
|
+
ssh_head_proxy = rci.ssh_proxy
|
|
52
|
+
ssh_head_proxy_private_key = get_or_error(rci.ssh_proxy_keys)[0].private
|
|
53
|
+
ssh_proxies = []
|
|
54
|
+
if ssh_head_proxy is not None:
|
|
55
|
+
ssh_head_proxy_private_key = get_or_error(ssh_head_proxy_private_key)
|
|
56
|
+
ssh_proxies.append((ssh_head_proxy, FileContent(ssh_head_proxy_private_key)))
|
|
57
|
+
if ssh_proxy is not None:
|
|
58
|
+
ssh_proxies.append((ssh_proxy, None))
|
|
59
|
+
return SSHTunnel(
|
|
60
|
+
destination=ssh_destination,
|
|
61
|
+
port=ssh_port,
|
|
62
|
+
ssh_proxies=ssh_proxies,
|
|
63
|
+
identity=FileContent(instance.project.ssh_private_key),
|
|
64
|
+
forwarded_sockets=forwarded_sockets,
|
|
65
|
+
options=options,
|
|
66
|
+
)
|
|
@@ -2,7 +2,6 @@ import hashlib
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import uuid
|
|
5
|
-
from datetime import timezone
|
|
6
5
|
from typing import Awaitable, Callable, List, Optional, Tuple
|
|
7
6
|
|
|
8
7
|
from sqlalchemy import delete, select, update
|
|
@@ -195,7 +194,7 @@ def user_model_to_user(user_model: UserModel) -> User:
|
|
|
195
194
|
return User(
|
|
196
195
|
id=user_model.id,
|
|
197
196
|
username=user_model.name,
|
|
198
|
-
created_at=user_model.created_at
|
|
197
|
+
created_at=user_model.created_at,
|
|
199
198
|
global_role=user_model.global_role,
|
|
200
199
|
email=user_model.email,
|
|
201
200
|
active=user_model.active,
|
|
@@ -207,7 +206,7 @@ def user_model_to_user_with_creds(user_model: UserModel) -> UserWithCreds:
|
|
|
207
206
|
return UserWithCreds(
|
|
208
207
|
id=user_model.id,
|
|
209
208
|
username=user_model.name,
|
|
210
|
-
created_at=user_model.created_at
|
|
209
|
+
created_at=user_model.created_at,
|
|
211
210
|
global_role=user_model.global_role,
|
|
212
211
|
email=user_model.email,
|
|
213
212
|
active=user_model.active,
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from datetime import datetime, timedelta
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
3
|
from typing import List, Optional
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import and_, func, or_, select, update
|
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
7
|
from sqlalchemy.orm import joinedload, selectinload
|
|
8
8
|
|
|
9
|
-
from dstack._internal.core.backends import BACKENDS_WITH_VOLUMES_SUPPORT
|
|
10
9
|
from dstack._internal.core.backends.base.compute import ComputeWithVolumeSupport
|
|
10
|
+
from dstack._internal.core.backends.features import BACKENDS_WITH_VOLUMES_SUPPORT
|
|
11
11
|
from dstack._internal.core.errors import (
|
|
12
12
|
BackendNotAvailable,
|
|
13
13
|
ResourceExistsError,
|
|
14
14
|
ServerClientError,
|
|
15
15
|
)
|
|
16
|
-
from dstack._internal.core.models.users import GlobalRole
|
|
17
16
|
from dstack._internal.core.models.volumes import (
|
|
18
17
|
Volume,
|
|
19
18
|
VolumeAttachment,
|
|
@@ -40,7 +39,7 @@ from dstack._internal.server.services.locking import (
|
|
|
40
39
|
string_to_lock_id,
|
|
41
40
|
)
|
|
42
41
|
from dstack._internal.server.services.plugins import apply_plugin_policies
|
|
43
|
-
from dstack._internal.server.services.projects import
|
|
42
|
+
from dstack._internal.server.services.projects import list_user_project_models
|
|
44
43
|
from dstack._internal.utils import common, random_names
|
|
45
44
|
from dstack._internal.utils.logging import get_logger
|
|
46
45
|
|
|
@@ -57,10 +56,11 @@ async def list_volumes(
|
|
|
57
56
|
limit: int,
|
|
58
57
|
ascending: bool,
|
|
59
58
|
) -> List[Volume]:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
projects = await list_user_project_models(
|
|
60
|
+
session=session,
|
|
61
|
+
user=user,
|
|
62
|
+
only_names=True,
|
|
63
|
+
)
|
|
64
64
|
if project_name is not None:
|
|
65
65
|
projects = [p for p in projects if p.name == project_name]
|
|
66
66
|
volume_models = await list_projects_volume_models(
|
|
@@ -320,15 +320,15 @@ def volume_model_to_volume(volume_model: VolumeModel) -> Volume:
|
|
|
320
320
|
)
|
|
321
321
|
deleted_at = None
|
|
322
322
|
if volume_model.deleted_at is not None:
|
|
323
|
-
deleted_at = volume_model.deleted_at
|
|
323
|
+
deleted_at = volume_model.deleted_at
|
|
324
324
|
volume = Volume(
|
|
325
325
|
name=volume_model.name,
|
|
326
326
|
project_name=volume_model.project.name,
|
|
327
327
|
user=volume_model.user.name,
|
|
328
328
|
configuration=configuration,
|
|
329
329
|
external=configuration.volume_id is not None,
|
|
330
|
-
created_at=volume_model.created_at
|
|
331
|
-
last_processed_at=volume_model.last_processed_at
|
|
330
|
+
created_at=volume_model.created_at,
|
|
331
|
+
last_processed_at=volume_model.last_processed_at,
|
|
332
332
|
status=volume_model.status,
|
|
333
333
|
status_message=volume_model.status_message,
|
|
334
334
|
deleted=volume_model.deleted,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment variables read by the dstack server. Documented in reference/environment-variables.md
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
import os
|
|
2
6
|
import warnings
|
|
3
7
|
from pathlib import Path
|
|
@@ -50,6 +54,8 @@ SERVER_BACKGROUND_PROCESSING_ENABLED = not SERVER_BACKGROUND_PROCESSING_DISABLED
|
|
|
50
54
|
SERVER_EXECUTOR_MAX_WORKERS = int(os.getenv("DSTACK_SERVER_EXECUTOR_MAX_WORKERS", 128))
|
|
51
55
|
|
|
52
56
|
MAX_OFFERS_TRIED = int(os.getenv("DSTACK_SERVER_MAX_OFFERS_TRIED", 25))
|
|
57
|
+
MAX_PROBES_PER_JOB = int(os.getenv("DSTACK_SERVER_MAX_PROBES_PER_JOB", 10))
|
|
58
|
+
MAX_PROBE_TIMEOUT = int(os.getenv("DSTACK_SERVER_MAX_PROBE_TIMEOUT", 60 * 5))
|
|
53
59
|
|
|
54
60
|
SERVER_CONFIG_DISABLED = os.getenv("DSTACK_SERVER_CONFIG_DISABLED") is not None
|
|
55
61
|
SERVER_CONFIG_ENABLED = not SERVER_CONFIG_DISABLED
|
|
@@ -87,12 +93,22 @@ SERVER_METRICS_FINISHED_TTL_SECONDS = int(
|
|
|
87
93
|
os.getenv("DSTACK_SERVER_METRICS_FINISHED_TTL_SECONDS", 7 * 24 * 3600)
|
|
88
94
|
)
|
|
89
95
|
|
|
96
|
+
SERVER_INSTANCE_HEALTH_TTL_SECONDS = int(
|
|
97
|
+
os.getenv("DSTACK_SERVER_INSTANCE_HEALTH_TTL_SECONDS", 7 * 24 * 3600)
|
|
98
|
+
)
|
|
99
|
+
SERVER_INSTANCE_HEALTH_MIN_COLLECT_INTERVAL_SECONDS = int(
|
|
100
|
+
os.getenv("DSTACK_SERVER_INSTANCE_HEALTH_MIN_COLLECT_INTERVAL_SECONDS", 60)
|
|
101
|
+
)
|
|
102
|
+
|
|
90
103
|
SERVER_KEEP_SHIM_TASKS = os.getenv("DSTACK_SERVER_KEEP_SHIM_TASKS") is not None
|
|
91
104
|
|
|
92
105
|
DEFAULT_PROJECT_NAME = "main"
|
|
93
106
|
|
|
94
107
|
SENTRY_DSN = os.getenv("DSTACK_SENTRY_DSN")
|
|
95
108
|
SENTRY_TRACES_SAMPLE_RATE = float(os.getenv("DSTACK_SENTRY_TRACES_SAMPLE_RATE", 0.1))
|
|
109
|
+
SENTRY_TRACES_BACKGROUND_SAMPLE_RATE = float(
|
|
110
|
+
os.getenv("DSTACK_SENTRY_TRACES_BACKGROUND_SAMPLE_RATE", 0.01)
|
|
111
|
+
)
|
|
96
112
|
SENTRY_PROFILES_SAMPLE_RATE = float(os.getenv("DSTACK_SENTRY_PROFILES_SAMPLE_RATE", 0))
|
|
97
113
|
|
|
98
114
|
DEFAULT_CREDS_DISABLED = os.getenv("DSTACK_DEFAULT_CREDS_DISABLED") is not None
|
|
@@ -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-cc067b7fd1a8f33f97da.js"></script><link href="/main-03e818b110e1d5705378.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>
|