dstack 0.19.33__py3-none-any.whl → 0.19.34__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 (28) hide show
  1. dstack/_internal/core/backends/base/offers.py +3 -1
  2. dstack/_internal/core/backends/gcp/compute.py +24 -9
  3. dstack/_internal/core/backends/gcp/models.py +4 -1
  4. dstack/_internal/core/backends/runpod/compute.py +4 -1
  5. dstack/_internal/core/compatibility/runs.py +25 -4
  6. dstack/_internal/core/models/instances.py +2 -1
  7. dstack/_internal/core/models/runs.py +1 -0
  8. dstack/_internal/core/services/ssh/key_manager.py +56 -0
  9. dstack/_internal/server/models.py +3 -0
  10. dstack/_internal/server/routers/metrics.py +6 -2
  11. dstack/_internal/server/routers/users.py +7 -0
  12. dstack/_internal/server/services/jobs/__init__.py +18 -9
  13. dstack/_internal/server/services/offers.py +1 -0
  14. dstack/_internal/server/services/runs.py +4 -0
  15. dstack/_internal/server/services/users.py +3 -3
  16. dstack/_internal/server/statics/index.html +1 -1
  17. dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js → main-e79754c136f1d8e4e7e6.js} +11 -11
  18. dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js.map → main-e79754c136f1d8e4e7e6.js.map} +1 -1
  19. dstack/_internal/server/testing/common.py +4 -0
  20. dstack/api/_public/__init__.py +2 -2
  21. dstack/api/_public/runs.py +36 -39
  22. dstack/api/server/__init__.py +4 -0
  23. dstack/version.py +1 -1
  24. {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/METADATA +2 -2
  25. {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/RECORD +28 -27
  26. {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/WHEEL +0 -0
  27. {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/entry_points.txt +0 -0
  28. {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/licenses/LICENSE.md +0 -0
@@ -23,7 +23,8 @@ SUPPORTED_GPUHUNT_FLAGS = [
23
23
  "oci-spot",
24
24
  "lambda-arm",
25
25
  "gcp-a4",
26
- "gcp-g4-preview",
26
+ "gcp-g4",
27
+ "gcp-dws-calendar-mode",
27
28
  ]
28
29
 
29
30
 
@@ -94,6 +95,7 @@ def catalog_item_to_offer(
94
95
  ),
95
96
  region=item.location,
96
97
  price=item.price,
98
+ backend_data=item.provider_data,
97
99
  )
98
100
 
99
101
 
@@ -90,6 +90,10 @@ RESOURCE_NAME_PATTERN = re.compile(r"[a-z0-9-]+")
90
90
  TPU_VERSIONS = [tpu.name for tpu in KNOWN_TPUS]
91
91
 
92
92
 
93
+ class GCPOfferBackendData(CoreModel):
94
+ is_dws_calendar_mode: bool = False
95
+
96
+
93
97
  class GCPVolumeDiskBackendData(CoreModel):
94
98
  type: Literal["disk"] = "disk"
95
99
  disk_type: str
@@ -142,19 +146,13 @@ class GCPCompute(
142
146
  offer_keys_to_offers = {}
143
147
  offers_with_availability = []
144
148
  for offer in offers:
145
- preview = False
146
- if offer.instance.name.startswith("g4-standard-"):
147
- if self.config.preview_features and "g4" in self.config.preview_features:
148
- preview = True
149
- else:
150
- continue
151
149
  region = offer.region[:-2] # strip zone
152
150
  key = (_unique_instance_name(offer.instance), region)
153
151
  if key in offer_keys_to_offers:
154
152
  offer_keys_to_offers[key].availability_zones.append(offer.region)
155
153
  continue
156
154
  availability = InstanceAvailability.NO_QUOTA
157
- if preview or _has_gpu_quota(quotas[region], offer.instance.resources):
155
+ if _has_gpu_quota(quotas[region], offer.instance.resources):
158
156
  availability = InstanceAvailability.UNKNOWN
159
157
  # todo quotas: cpu, memory, global gpu, tpu
160
158
  offer_with_availability = InstanceOfferWithAvailability(
@@ -202,6 +200,23 @@ class GCPCompute(
202
200
  modifiers.append(get_offers_disk_modifier(CONFIGURABLE_DISK_SIZE, requirements))
203
201
  return modifiers
204
202
 
203
+ def get_offers_post_filter(
204
+ self, requirements: Requirements
205
+ ) -> Optional[Callable[[InstanceOfferWithAvailability], bool]]:
206
+ if requirements.reservation is None:
207
+
208
+ def reserved_offers_filter(offer: InstanceOfferWithAvailability) -> bool:
209
+ """Remove reserved-only offers"""
210
+ if GCPOfferBackendData.__response__.parse_obj(
211
+ offer.backend_data
212
+ ).is_dws_calendar_mode:
213
+ return False
214
+ return True
215
+
216
+ return reserved_offers_filter
217
+
218
+ return None
219
+
205
220
  def terminate_instance(
206
221
  self, instance_id: str, region: str, backend_data: Optional[str] = None
207
222
  ) -> None:
@@ -1006,8 +1021,8 @@ def _has_gpu_quota(quotas: Dict[str, float], resources: Resources) -> bool:
1006
1021
  gpu = resources.gpus[0]
1007
1022
  if _is_tpu(gpu.name):
1008
1023
  return True
1009
- if gpu.name in ["B200", "H100"]:
1010
- # B200, H100 and H100_MEGA quotas are not returned by `regions_client.list`
1024
+ if gpu.name in ["B200", "H100", "RTXPRO6000"]:
1025
+ # B200, H100, H100_MEGA, and RTXPRO6000 quotas are not returned by `regions_client.list`
1011
1026
  return True
1012
1027
  quota_name = f"NVIDIA_{gpu.name}_GPUS"
1013
1028
  if gpu.name == "A100" and gpu.memory_mib == 80 * 1024:
@@ -92,7 +92,10 @@ class GCPBackendConfig(CoreModel):
92
92
  preview_features: Annotated[
93
93
  Optional[List[Literal["g4"]]],
94
94
  Field(
95
- description=("The list of preview GCP features to enable. Supported values: `g4`"),
95
+ description=(
96
+ "The list of preview GCP features to enable."
97
+ " There are currently no preview features"
98
+ ),
96
99
  max_items=1,
97
100
  ),
98
101
  ] = None
@@ -232,9 +232,12 @@ class RunpodCompute(
232
232
  def create_volume(self, volume: Volume) -> VolumeProvisioningData:
233
233
  volume_name = generate_unique_volume_name(volume, max_length=MAX_RESOURCE_NAME_LEN)
234
234
  size_gb = volume.configuration.size_gb
235
+ # Runpod regions must be uppercase.
236
+ # Lowercase regions are accepted in the API but they break Runpod in several ways.
237
+ region = volume.configuration.region.upper()
235
238
  volume_id = self.api_client.create_network_volume(
236
239
  name=volume_name,
237
- region=volume.configuration.region,
240
+ region=region,
238
241
  size=size_gb,
239
242
  )
240
243
  return VolumeProvisioningData(
@@ -33,6 +33,8 @@ def get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[IncludeExcludeD
33
33
  current_resource_excludes["deployment_num"] = True
34
34
  if current_resource.fleet is None:
35
35
  current_resource_excludes["fleet"] = True
36
+ if current_resource.next_triggered_at is None:
37
+ current_resource_excludes["next_triggered_at"] = True
36
38
  apply_plan_excludes["current_resource"] = current_resource_excludes
37
39
  current_resource_excludes["run_spec"] = get_run_spec_excludes(current_resource.run_spec)
38
40
  job_submissions_excludes: IncludeExcludeDictType = {}
@@ -47,10 +49,20 @@ def get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[IncludeExcludeD
47
49
  job_submissions_excludes["job_provisioning_data"] = {
48
50
  "instance_type": {"resources": {"cpu_arch"}}
49
51
  }
52
+ jrd_offer_excludes = {}
53
+ if any(
54
+ js.job_runtime_data and js.job_runtime_data.offer for js in job_submissions
55
+ ) and all(
56
+ not js.job_runtime_data
57
+ or not js.job_runtime_data.offer
58
+ or not js.job_runtime_data.offer.backend_data
59
+ for js in job_submissions
60
+ ):
61
+ jrd_offer_excludes["backend_data"] = True
50
62
  if all(map(_should_exclude_job_submission_jrd_cpu_arch, job_submissions)):
51
- job_submissions_excludes["job_runtime_data"] = {
52
- "offer": {"instance": {"resources": {"cpu_arch"}}}
53
- }
63
+ jrd_offer_excludes["instance"] = {"resources": {"cpu_arch"}}
64
+ if jrd_offer_excludes:
65
+ job_submissions_excludes["job_runtime_data"] = {"offer": jrd_offer_excludes}
54
66
  if all(js.exit_status is None for js in job_submissions):
55
67
  job_submissions_excludes["exit_status"] = True
56
68
  if all(js.status_message == "" for js in job_submissions):
@@ -69,9 +81,18 @@ def get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[IncludeExcludeD
69
81
  latest_job_submission_excludes["job_provisioning_data"] = {
70
82
  "instance_type": {"resources": {"cpu_arch"}}
71
83
  }
84
+ latest_job_submission_jrd_offer_excludes = {}
85
+ if (
86
+ latest_job_submission.job_runtime_data
87
+ and latest_job_submission.job_runtime_data.offer
88
+ and not latest_job_submission.job_runtime_data.offer.backend_data
89
+ ):
90
+ latest_job_submission_jrd_offer_excludes["backend_data"] = True
72
91
  if _should_exclude_job_submission_jrd_cpu_arch(latest_job_submission):
92
+ latest_job_submission_jrd_offer_excludes["instance"] = {"resources": {"cpu_arch"}}
93
+ if latest_job_submission_jrd_offer_excludes:
73
94
  latest_job_submission_excludes["job_runtime_data"] = {
74
- "offer": {"instance": {"resources": {"cpu_arch"}}}
95
+ "offer": latest_job_submission_jrd_offer_excludes
75
96
  }
76
97
  if latest_job_submission.exit_status is None:
77
98
  latest_job_submission_excludes["exit_status"] = True
@@ -1,6 +1,6 @@
1
1
  import datetime
2
2
  from enum import Enum
3
- from typing import Dict, List, Optional
3
+ from typing import Any, Dict, List, Optional
4
4
  from uuid import UUID
5
5
 
6
6
  import gpuhunt
@@ -184,6 +184,7 @@ class InstanceOffer(CoreModel):
184
184
  instance: InstanceType
185
185
  region: str
186
186
  price: float
187
+ backend_data: dict[str, Any] = {}
187
188
 
188
189
 
189
190
  class InstanceOfferWithAvailability(InstanceOffer):
@@ -553,6 +553,7 @@ class Run(CoreModel):
553
553
  deployment_num: int = 0 # default for compatibility with pre-0.19.14 servers
554
554
  error: Optional[str] = None
555
555
  deleted: Optional[bool] = None
556
+ next_triggered_at: Optional[datetime] = None
556
557
 
557
558
  def is_deployment_in_progress(self) -> bool:
558
559
  return any(
@@ -0,0 +1,56 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timedelta
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from dstack._internal.core.models.users import UserWithCreds
8
+
9
+ if TYPE_CHECKING:
10
+ from dstack.api.server import APIClient
11
+
12
+ KEY_REFRESH_RATE = timedelta(minutes=10) # redownload the key periodically in case it was rotated
13
+
14
+
15
+ @dataclass
16
+ class UserSSHKey:
17
+ public_key: str
18
+ private_key_path: Path
19
+
20
+
21
+ class UserSSHKeyManager:
22
+ def __init__(self, api_client: "APIClient", ssh_keys_dir: Path) -> None:
23
+ self._api_client = api_client
24
+ self._key_path = ssh_keys_dir / api_client.get_token_hash()
25
+ self._pub_key_path = self._key_path.with_suffix(".pub")
26
+
27
+ def get_user_key(self) -> Optional[UserSSHKey]:
28
+ """
29
+ Return the up-to-date user key, or None if the user has no key (if created before 0.19.33)
30
+ """
31
+ if (
32
+ not self._key_path.exists()
33
+ or not self._pub_key_path.exists()
34
+ or datetime.now() - datetime.fromtimestamp(self._key_path.stat().st_mtime)
35
+ > KEY_REFRESH_RATE
36
+ ):
37
+ if not self._download_user_key():
38
+ return None
39
+ return UserSSHKey(
40
+ public_key=self._pub_key_path.read_text(), private_key_path=self._key_path
41
+ )
42
+
43
+ def _download_user_key(self) -> bool:
44
+ user = self._api_client.users.get_my_user()
45
+ if not (isinstance(user, UserWithCreds) and user.ssh_public_key and user.ssh_private_key):
46
+ return False
47
+
48
+ def key_opener(path, flags):
49
+ return os.open(path, flags, 0o600)
50
+
51
+ with open(self._key_path, "w", opener=key_opener) as f:
52
+ f.write(user.ssh_private_key)
53
+ with open(self._pub_key_path, "w") as f:
54
+ f.write(user.ssh_public_key)
55
+
56
+ return True
@@ -190,6 +190,9 @@ class UserModel(BaseModel):
190
190
  # deactivated users cannot access API
191
191
  active: Mapped[bool] = mapped_column(Boolean, default=True)
192
192
 
193
+ # SSH keys can be null for users created before 0.19.33.
194
+ # Keys for those users are being gradually generated on /get_my_user calls.
195
+ # TODO: make keys required in a future version.
193
196
  ssh_private_key: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
194
197
  ssh_public_key: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
195
198
 
@@ -1,5 +1,6 @@
1
1
  from datetime import datetime
2
2
  from typing import Optional, Tuple
3
+ from uuid import UUID
3
4
 
4
5
  from fastapi import APIRouter, Depends
5
6
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -29,6 +30,7 @@ router = APIRouter(
29
30
  )
30
31
  async def get_job_metrics(
31
32
  run_name: str,
33
+ run_id: Optional[UUID] = None,
32
34
  replica_num: int = 0,
33
35
  job_num: int = 0,
34
36
  limit: int = 1,
@@ -39,8 +41,9 @@ async def get_job_metrics(
39
41
  ):
40
42
  """
41
43
  Returns job-level metrics such as hardware utilization
42
- given `run_name`, `replica_num`, and `job_num`.
43
- If only `run_name` is specified, returns metrics of `(replica_num=0, job_num=0)`.
44
+ given `run_name`, `run_id`, `replica_num`, and `job_num`.
45
+ If only `run_name` is specified, returns metrics of `(replica_num=0, job_num=0)`
46
+ of the latest run with the given name.
44
47
  By default, returns one latest sample. To control time window/number of samples, use
45
48
  `limit`, `after`, `before`.
46
49
 
@@ -61,6 +64,7 @@ async def get_job_metrics(
61
64
  session=session,
62
65
  project=project,
63
66
  run_name=run_name,
67
+ run_id=run_id,
64
68
  replica_num=replica_num,
65
69
  job_num=job_num,
66
70
  )
@@ -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
 
@@ -97,19 +97,28 @@ def find_job(jobs: List[Job], replica_num: int, job_num: int) -> Job:
97
97
 
98
98
 
99
99
  async def get_run_job_model(
100
- session: AsyncSession, project: ProjectModel, run_name: str, replica_num: int, job_num: int
100
+ session: AsyncSession,
101
+ project: ProjectModel,
102
+ run_name: str,
103
+ run_id: Optional[UUID],
104
+ replica_num: int,
105
+ job_num: int,
101
106
  ) -> Optional[JobModel]:
107
+ filters = [
108
+ RunModel.project_id == project.id,
109
+ RunModel.run_name == run_name,
110
+ JobModel.replica_num == replica_num,
111
+ JobModel.job_num == job_num,
112
+ ]
113
+ if run_id is not None:
114
+ filters.append(RunModel.id == run_id)
115
+ else:
116
+ # Assuming run_name is unique for non-deleted runs
117
+ filters.append(RunModel.deleted == False)
102
118
  res = await session.execute(
103
119
  select(JobModel)
104
120
  .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
- )
121
+ .where(*filters)
113
122
  .order_by(JobModel.submission_num.desc())
114
123
  .limit(1)
115
124
  )
@@ -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,
@@ -715,6 +715,9 @@ def run_model_to_run(
715
715
  status_message = _get_run_status_message(run_model)
716
716
  error = _get_run_error(run_model)
717
717
  fleet = _get_run_fleet(run_model)
718
+ next_triggered_at = None
719
+ if not run_model.status.is_finished():
720
+ next_triggered_at = _get_next_triggered_at(run_spec)
718
721
  run = Run(
719
722
  id=run_model.id,
720
723
  project_name=run_model.project.name,
@@ -734,6 +737,7 @@ def run_model_to_run(
734
737
  deployment_num=run_model.deployment_num,
735
738
  error=error,
736
739
  deleted=run_model.deleted,
740
+ next_triggered_at=next_triggered_at,
737
741
  )
738
742
  run.cost = _get_run_cost(run)
739
743
  return run
@@ -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>