dstack 0.19.33__py3-none-any.whl → 0.19.35__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.

Files changed (56) hide show
  1. dstack/_internal/cli/services/configurators/run.py +1 -1
  2. dstack/_internal/core/backends/base/compute.py +20 -1
  3. dstack/_internal/core/backends/base/models.py +10 -0
  4. dstack/_internal/core/backends/base/offers.py +4 -1
  5. dstack/_internal/core/backends/features.py +5 -0
  6. dstack/_internal/core/backends/gcp/compute.py +24 -9
  7. dstack/_internal/core/backends/gcp/models.py +4 -1
  8. dstack/_internal/core/backends/nebius/compute.py +28 -16
  9. dstack/_internal/core/backends/nebius/configurator.py +1 -1
  10. dstack/_internal/core/backends/nebius/models.py +4 -0
  11. dstack/_internal/core/backends/nebius/resources.py +41 -20
  12. dstack/_internal/core/backends/runpod/api_client.py +245 -59
  13. dstack/_internal/core/backends/runpod/compute.py +161 -14
  14. dstack/_internal/core/compatibility/runs.py +25 -4
  15. dstack/_internal/core/models/compute_groups.py +39 -0
  16. dstack/_internal/core/models/fleets.py +6 -1
  17. dstack/_internal/core/models/instances.py +2 -1
  18. dstack/_internal/core/models/profiles.py +3 -1
  19. dstack/_internal/core/models/runs.py +4 -0
  20. dstack/_internal/core/services/ssh/key_manager.py +56 -0
  21. dstack/_internal/server/app.py +14 -2
  22. dstack/_internal/server/background/__init__.py +7 -0
  23. dstack/_internal/server/background/tasks/process_compute_groups.py +164 -0
  24. dstack/_internal/server/background/tasks/process_instances.py +81 -49
  25. dstack/_internal/server/background/tasks/process_submitted_jobs.py +179 -84
  26. dstack/_internal/server/migrations/env.py +20 -2
  27. dstack/_internal/server/migrations/versions/7d1ec2b920ac_add_computegroupmodel.py +93 -0
  28. dstack/_internal/server/models.py +42 -0
  29. dstack/_internal/server/routers/metrics.py +6 -2
  30. dstack/_internal/server/routers/runs.py +15 -6
  31. dstack/_internal/server/routers/users.py +7 -0
  32. dstack/_internal/server/services/compute_groups.py +22 -0
  33. dstack/_internal/server/services/fleets.py +1 -0
  34. dstack/_internal/server/services/jobs/__init__.py +31 -9
  35. dstack/_internal/server/services/jobs/configurators/base.py +3 -2
  36. dstack/_internal/server/services/offers.py +1 -0
  37. dstack/_internal/server/services/requirements/combine.py +1 -0
  38. dstack/_internal/server/services/runs.py +21 -3
  39. dstack/_internal/server/services/users.py +3 -3
  40. dstack/_internal/server/statics/index.html +1 -1
  41. dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js → main-e79754c136f1d8e4e7e6.js} +11 -11
  42. dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js.map → main-e79754c136f1d8e4e7e6.js.map} +1 -1
  43. dstack/_internal/server/testing/common.py +55 -0
  44. dstack/_internal/server/utils/routers.py +18 -20
  45. dstack/_internal/settings.py +4 -1
  46. dstack/_internal/utils/version.py +22 -0
  47. dstack/api/_public/__init__.py +2 -2
  48. dstack/api/_public/runs.py +36 -39
  49. dstack/api/server/__init__.py +4 -0
  50. dstack/version.py +1 -1
  51. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/METADATA +4 -4
  52. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/RECORD +55 -50
  53. dstack/_internal/core/backends/nebius/fabrics.py +0 -49
  54. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/WHEEL +0 -0
  55. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/entry_points.txt +0 -0
  56. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,6 +1,6 @@
1
- from typing import List, Tuple
1
+ from typing import Annotated, List, Optional, Tuple, cast
2
2
 
3
- from fastapi import APIRouter, Depends
3
+ from fastapi import APIRouter, Depends, Request
4
4
  from sqlalchemy.ext.asyncio import AsyncSession
5
5
 
6
6
  from dstack._internal.core.errors import ResourceNotExistsError
@@ -35,6 +35,11 @@ project_router = APIRouter(
35
35
  )
36
36
 
37
37
 
38
+ def use_legacy_default_working_dir(request: Request) -> bool:
39
+ client_release = cast(Optional[tuple[int, ...]], request.state.client_release)
40
+ return client_release is not None and client_release < (0, 19, 27)
41
+
42
+
38
43
  @root_router.post(
39
44
  "/list",
40
45
  response_model=List[Run],
@@ -103,8 +108,9 @@ async def get_run(
103
108
  )
104
109
  async def get_plan(
105
110
  body: GetRunPlanRequest,
106
- session: AsyncSession = Depends(get_session),
107
- user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
111
+ session: Annotated[AsyncSession, Depends(get_session)],
112
+ user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
113
+ legacy_default_working_dir: Annotated[bool, Depends(use_legacy_default_working_dir)],
108
114
  ):
109
115
  """
110
116
  Returns a run plan for the given run spec.
@@ -119,6 +125,7 @@ async def get_plan(
119
125
  user=user,
120
126
  run_spec=body.run_spec,
121
127
  max_offers=body.max_offers,
128
+ legacy_default_working_dir=legacy_default_working_dir,
122
129
  )
123
130
  return CustomORJSONResponse(run_plan)
124
131
 
@@ -129,8 +136,9 @@ async def get_plan(
129
136
  )
130
137
  async def apply_plan(
131
138
  body: ApplyRunPlanRequest,
132
- session: AsyncSession = Depends(get_session),
133
- user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
139
+ session: Annotated[AsyncSession, Depends(get_session)],
140
+ user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
141
+ legacy_default_working_dir: Annotated[bool, Depends(use_legacy_default_working_dir)],
134
142
  ):
135
143
  """
136
144
  Creates a new run or updates an existing run.
@@ -148,6 +156,7 @@ async def apply_plan(
148
156
  project=project,
149
157
  plan=body.plan,
150
158
  force=body.force,
159
+ legacy_default_working_dir=legacy_default_working_dir,
151
160
  )
152
161
  )
153
162
 
@@ -38,8 +38,15 @@ async def list_users(
38
38
 
39
39
  @router.post("/get_my_user", response_model=UserWithCreds)
40
40
  async def get_my_user(
41
+ session: AsyncSession = Depends(get_session),
41
42
  user: UserModel = Depends(Authenticated()),
42
43
  ):
44
+ if user.ssh_private_key is None or user.ssh_public_key is None:
45
+ # Generate keys for pre-0.19.33 users
46
+ updated_user = await users.refresh_ssh_key(session=session, user=user, username=user.name)
47
+ if updated_user is None:
48
+ raise ResourceNotExistsError()
49
+ user = updated_user
43
50
  return CustomORJSONResponse(users.user_model_to_user_with_creds(user))
44
51
 
45
52
 
@@ -0,0 +1,22 @@
1
+ from dstack._internal.core.models.compute_groups import ComputeGroup, ComputeGroupProvisioningData
2
+ from dstack._internal.server.models import ComputeGroupModel
3
+
4
+
5
+ def compute_group_model_to_compute_group(compute_group_model: ComputeGroupModel) -> ComputeGroup:
6
+ provisioning_data = get_compute_group_provisioning_data(compute_group_model)
7
+ return ComputeGroup(
8
+ id=compute_group_model.id,
9
+ project_name=compute_group_model.project.name,
10
+ status=compute_group_model.status,
11
+ name=provisioning_data.compute_group_name,
12
+ created_at=compute_group_model.created_at,
13
+ provisioning_data=provisioning_data,
14
+ )
15
+
16
+
17
+ def get_compute_group_provisioning_data(
18
+ compute_group_model: ComputeGroupModel,
19
+ ) -> ComputeGroupProvisioningData:
20
+ return ComputeGroupProvisioningData.__response__.parse_raw(
21
+ compute_group_model.provisioning_data
22
+ )
@@ -650,6 +650,7 @@ def get_fleet_requirements(fleet_spec: FleetSpec) -> Requirements:
650
650
  max_price=profile.max_price,
651
651
  spot=get_policy_map(profile.spot_policy, default=SpotPolicy.ONDEMAND),
652
652
  reservation=fleet_spec.configuration.reservation,
653
+ multinode=fleet_spec.configuration.placement == InstanceGroupPlacement.CLUSTER,
653
654
  )
654
655
  return requirements
655
656
 
@@ -96,20 +96,42 @@ def find_job(jobs: List[Job], replica_num: int, job_num: int) -> Job:
96
96
  )
97
97
 
98
98
 
99
+ def find_jobs(
100
+ jobs: List[Job],
101
+ replica_num: Optional[int] = None,
102
+ job_num: Optional[int] = None,
103
+ ) -> list[Job]:
104
+ res = jobs
105
+ if replica_num is not None:
106
+ res = [j for j in res if j.job_spec.replica_num == replica_num]
107
+ if job_num is not None:
108
+ res = [j for j in res if j.job_spec.job_num == job_num]
109
+ return res
110
+
111
+
99
112
  async def get_run_job_model(
100
- session: AsyncSession, project: ProjectModel, run_name: str, replica_num: int, job_num: int
113
+ session: AsyncSession,
114
+ project: ProjectModel,
115
+ run_name: str,
116
+ run_id: Optional[UUID],
117
+ replica_num: int,
118
+ job_num: int,
101
119
  ) -> Optional[JobModel]:
120
+ filters = [
121
+ RunModel.project_id == project.id,
122
+ RunModel.run_name == run_name,
123
+ JobModel.replica_num == replica_num,
124
+ JobModel.job_num == job_num,
125
+ ]
126
+ if run_id is not None:
127
+ filters.append(RunModel.id == run_id)
128
+ else:
129
+ # Assuming run_name is unique for non-deleted runs
130
+ filters.append(RunModel.deleted == False)
102
131
  res = await session.execute(
103
132
  select(JobModel)
104
133
  .join(JobModel.run)
105
- .where(
106
- RunModel.project_id == project.id,
107
- # assuming run_name is unique for non-deleted runs
108
- RunModel.run_name == run_name,
109
- RunModel.deleted == False,
110
- JobModel.replica_num == replica_num,
111
- JobModel.job_num == job_num,
112
- )
134
+ .where(*filters)
113
135
  .order_by(JobModel.submission_num.desc())
114
136
  .limit(1)
115
137
  )
@@ -161,7 +161,7 @@ class JobConfigurator(ABC):
161
161
  stop_duration=self._stop_duration(),
162
162
  utilization_policy=self._utilization_policy(),
163
163
  registry_auth=self._registry_auth(),
164
- requirements=self._requirements(),
164
+ requirements=self._requirements(jobs_per_replica),
165
165
  retry=self._retry(),
166
166
  working_dir=self._working_dir(),
167
167
  volumes=self._volumes(job_num),
@@ -295,13 +295,14 @@ class JobConfigurator(ABC):
295
295
  def _registry_auth(self) -> Optional[RegistryAuth]:
296
296
  return self.run_spec.configuration.registry_auth
297
297
 
298
- def _requirements(self) -> Requirements:
298
+ def _requirements(self, jobs_per_replica: int) -> Requirements:
299
299
  spot_policy = self._spot_policy()
300
300
  return Requirements(
301
301
  resources=self.run_spec.configuration.resources,
302
302
  max_price=self.run_spec.merged_profile.max_price,
303
303
  spot=None if spot_policy == SpotPolicy.AUTO else (spot_policy == SpotPolicy.SPOT),
304
304
  reservation=self.run_spec.merged_profile.reservation,
305
+ multinode=jobs_per_replica > 1,
305
306
  )
306
307
 
307
308
  def _retry(self) -> Optional[Retry]:
@@ -215,6 +215,7 @@ def generate_shared_offer(
215
215
  ),
216
216
  region=offer.region,
217
217
  price=offer.price,
218
+ backend_data=offer.backend_data,
218
219
  availability=offer.availability,
219
220
  blocks=blocks,
220
221
  total_blocks=total_blocks,
@@ -63,6 +63,7 @@ def combine_fleet_and_run_requirements(
63
63
  reservation=_get_single_value_optional(
64
64
  fleet_requirements.reservation, run_requirements.reservation
65
65
  ),
66
+ multinode=fleet_requirements.multinode or run_requirements.multinode,
66
67
  )
67
68
  except CombineError:
68
69
  return None
@@ -34,6 +34,7 @@ from dstack._internal.core.models.profiles import (
34
34
  )
35
35
  from dstack._internal.core.models.repos.virtual import DEFAULT_VIRTUAL_REPO_ID, VirtualRunRepoData
36
36
  from dstack._internal.core.models.runs import (
37
+ LEGACY_REPO_DIR,
37
38
  ApplyRunPlanInput,
38
39
  Job,
39
40
  JobPlan,
@@ -308,6 +309,7 @@ async def get_plan(
308
309
  user: UserModel,
309
310
  run_spec: RunSpec,
310
311
  max_offers: Optional[int],
312
+ legacy_default_working_dir: bool = False,
311
313
  ) -> RunPlan:
312
314
  # Spec must be copied by parsing to calculate merged_profile
313
315
  effective_run_spec = RunSpec.parse_obj(run_spec.dict())
@@ -317,7 +319,11 @@ async def get_plan(
317
319
  spec=effective_run_spec,
318
320
  )
319
321
  effective_run_spec = RunSpec.parse_obj(effective_run_spec.dict())
320
- _validate_run_spec_and_set_defaults(user, effective_run_spec)
322
+ _validate_run_spec_and_set_defaults(
323
+ user=user,
324
+ run_spec=effective_run_spec,
325
+ legacy_default_working_dir=legacy_default_working_dir,
326
+ )
321
327
 
322
328
  profile = effective_run_spec.merged_profile
323
329
  creation_policy = profile.creation_policy
@@ -413,6 +419,7 @@ async def apply_plan(
413
419
  project: ProjectModel,
414
420
  plan: ApplyRunPlanInput,
415
421
  force: bool,
422
+ legacy_default_working_dir: bool = False,
416
423
  ) -> Run:
417
424
  run_spec = plan.run_spec
418
425
  run_spec = await apply_plugin_policies(
@@ -422,7 +429,9 @@ async def apply_plan(
422
429
  )
423
430
  # Spec must be copied by parsing to calculate merged_profile
424
431
  run_spec = RunSpec.parse_obj(run_spec.dict())
425
- _validate_run_spec_and_set_defaults(user, run_spec)
432
+ _validate_run_spec_and_set_defaults(
433
+ user=user, run_spec=run_spec, legacy_default_working_dir=legacy_default_working_dir
434
+ )
426
435
  if run_spec.run_name is None:
427
436
  return await submit_run(
428
437
  session=session,
@@ -600,6 +609,7 @@ def create_job_model_for_new_submission(
600
609
  job_spec_data=job.job_spec.json(),
601
610
  job_provisioning_data=None,
602
611
  probes=[],
612
+ waiting_master_job=job.job_spec.job_num != 0,
603
613
  )
604
614
 
605
615
 
@@ -715,6 +725,9 @@ def run_model_to_run(
715
725
  status_message = _get_run_status_message(run_model)
716
726
  error = _get_run_error(run_model)
717
727
  fleet = _get_run_fleet(run_model)
728
+ next_triggered_at = None
729
+ if not run_model.status.is_finished():
730
+ next_triggered_at = _get_next_triggered_at(run_spec)
718
731
  run = Run(
719
732
  id=run_model.id,
720
733
  project_name=run_model.project.name,
@@ -734,6 +747,7 @@ def run_model_to_run(
734
747
  deployment_num=run_model.deployment_num,
735
748
  error=error,
736
749
  deleted=run_model.deleted,
750
+ next_triggered_at=next_triggered_at,
737
751
  )
738
752
  run.cost = _get_run_cost(run)
739
753
  return run
@@ -981,7 +995,9 @@ def _get_job_submission_cost(job_submission: JobSubmission) -> float:
981
995
  return job_submission.job_provisioning_data.price * duration_hours
982
996
 
983
997
 
984
- def _validate_run_spec_and_set_defaults(user: UserModel, run_spec: RunSpec):
998
+ def _validate_run_spec_and_set_defaults(
999
+ user: UserModel, run_spec: RunSpec, legacy_default_working_dir: bool = False
1000
+ ):
985
1001
  # This function may set defaults for null run_spec values,
986
1002
  # although most defaults are resolved when building job_spec
987
1003
  # so that we can keep both the original user-supplied value (null in run_spec)
@@ -1036,6 +1052,8 @@ def _validate_run_spec_and_set_defaults(user: UserModel, run_spec: RunSpec):
1036
1052
  run_spec.ssh_key_pub = user.ssh_public_key
1037
1053
  else:
1038
1054
  raise ServerClientError("ssh_key_pub must be set if the user has no ssh_public_key")
1055
+ if run_spec.configuration.working_dir is None and legacy_default_working_dir:
1056
+ run_spec.configuration.working_dir = LEGACY_REPO_DIR
1039
1057
 
1040
1058
 
1041
1059
  _UPDATABLE_SPEC_FIELDS = ["configuration_path", "configuration"]
@@ -20,8 +20,8 @@ from dstack._internal.core.models.users import (
20
20
  from dstack._internal.server.models import DecryptedString, UserModel
21
21
  from dstack._internal.server.services.permissions import get_default_permissions
22
22
  from dstack._internal.server.utils.routers import error_forbidden
23
+ from dstack._internal.utils import crypto
23
24
  from dstack._internal.utils.common import run_async
24
- from dstack._internal.utils.crypto import generate_rsa_key_pair_bytes
25
25
  from dstack._internal.utils.logging import get_logger
26
26
 
27
27
  logger = get_logger(__name__)
@@ -88,7 +88,7 @@ async def create_user(
88
88
  raise ResourceExistsError()
89
89
  if token is None:
90
90
  token = str(uuid.uuid4())
91
- private_bytes, public_bytes = await run_async(generate_rsa_key_pair_bytes, username)
91
+ private_bytes, public_bytes = await run_async(crypto.generate_rsa_key_pair_bytes, username)
92
92
  user = UserModel(
93
93
  id=uuid.uuid4(),
94
94
  name=username,
@@ -135,7 +135,7 @@ async def refresh_ssh_key(
135
135
  logger.debug("Refreshing SSH key for user [code]%s[/code]", username)
136
136
  if user.global_role != GlobalRole.ADMIN and user.name != username:
137
137
  raise error_forbidden()
138
- private_bytes, public_bytes = await run_async(generate_rsa_key_pair_bytes, username)
138
+ private_bytes, public_bytes = await run_async(crypto.generate_rsa_key_pair_bytes, username)
139
139
  await session.execute(
140
140
  update(UserModel)
141
141
  .where(UserModel.name == username)
@@ -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-97c7e184573ca23f9fe4.js"></script><link href="/main-720ce3a11140daa480cc.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><script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id="11a9339d-20ce-4ddb-9ba3-1b6e29afe8eb" data-project-name="dstack" data-project-color="rgba(0, 0, 0, 0.87)" data-font-size-lg="0.78rem" data-button-hide="true" data-modal-image="/logo-notext.svg" data-modal-z-index="1100" data-modal-title="Ask me anything" data-font-family='metro-web, Metro, -apple-system, "system-ui", "Segoe UI", Roboto' data-project-logo="/assets/images/kapa.svg" data-modal-disclaimer="This is a custom LLM for dstack with access to Documentation, API references and GitHub issues. This feature is experimental - Give it a try!" data-user-analytics-fingerprint-enabled="true"></script></body></html>
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-e79754c136f1d8e4e7e6.js"></script><link href="/main-720ce3a11140daa480cc.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><script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id="11a9339d-20ce-4ddb-9ba3-1b6e29afe8eb" data-project-name="dstack" data-project-color="rgba(0, 0, 0, 0.87)" data-font-size-lg="0.78rem" data-button-hide="true" data-modal-image="/logo-notext.svg" data-modal-z-index="1100" data-modal-title="Ask me anything" data-font-family='metro-web, Metro, -apple-system, "system-ui", "Segoe UI", Roboto' data-project-logo="/assets/images/kapa.svg" data-modal-disclaimer="This is a custom LLM for dstack with access to Documentation, API references and GitHub issues. This feature is experimental - Give it a try!" data-user-analytics-fingerprint-enabled="true"></script></body></html>