dstack 0.19.1__py3-none-any.whl → 0.19.3__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.
Files changed (68) hide show
  1. dstack/_internal/cli/commands/metrics.py +138 -0
  2. dstack/_internal/cli/commands/stats.py +5 -119
  3. dstack/_internal/cli/main.py +2 -0
  4. dstack/_internal/cli/services/profile.py +9 -0
  5. dstack/_internal/core/backends/aws/configurator.py +1 -0
  6. dstack/_internal/core/backends/base/compute.py +4 -1
  7. dstack/_internal/core/backends/base/models.py +7 -7
  8. dstack/_internal/core/backends/configurators.py +9 -0
  9. dstack/_internal/core/backends/cudo/compute.py +2 -0
  10. dstack/_internal/core/backends/cudo/configurator.py +0 -13
  11. dstack/_internal/core/backends/datacrunch/compute.py +118 -32
  12. dstack/_internal/core/backends/datacrunch/configurator.py +16 -11
  13. dstack/_internal/core/backends/gcp/compute.py +140 -26
  14. dstack/_internal/core/backends/gcp/configurator.py +2 -0
  15. dstack/_internal/core/backends/gcp/features/__init__.py +0 -0
  16. dstack/_internal/core/backends/gcp/features/tcpx.py +34 -0
  17. dstack/_internal/core/backends/gcp/models.py +13 -1
  18. dstack/_internal/core/backends/gcp/resources.py +64 -27
  19. dstack/_internal/core/backends/lambdalabs/compute.py +2 -4
  20. dstack/_internal/core/backends/lambdalabs/configurator.py +0 -21
  21. dstack/_internal/core/backends/models.py +8 -0
  22. dstack/_internal/core/backends/nebius/__init__.py +0 -0
  23. dstack/_internal/core/backends/nebius/backend.py +16 -0
  24. dstack/_internal/core/backends/nebius/compute.py +272 -0
  25. dstack/_internal/core/backends/nebius/configurator.py +74 -0
  26. dstack/_internal/core/backends/nebius/models.py +108 -0
  27. dstack/_internal/core/backends/nebius/resources.py +240 -0
  28. dstack/_internal/core/backends/tensordock/api_client.py +5 -4
  29. dstack/_internal/core/backends/tensordock/compute.py +2 -15
  30. dstack/_internal/core/errors.py +14 -0
  31. dstack/_internal/core/models/backends/base.py +2 -0
  32. dstack/_internal/core/models/profiles.py +3 -0
  33. dstack/_internal/proxy/lib/schemas/model_proxy.py +3 -3
  34. dstack/_internal/server/background/tasks/process_instances.py +12 -7
  35. dstack/_internal/server/background/tasks/process_running_jobs.py +20 -0
  36. dstack/_internal/server/background/tasks/process_submitted_jobs.py +3 -2
  37. dstack/_internal/server/routers/prometheus.py +5 -0
  38. dstack/_internal/server/security/permissions.py +19 -1
  39. dstack/_internal/server/services/instances.py +14 -6
  40. dstack/_internal/server/services/jobs/__init__.py +3 -3
  41. dstack/_internal/server/services/offers.py +4 -2
  42. dstack/_internal/server/services/runs.py +0 -2
  43. dstack/_internal/server/statics/index.html +1 -1
  44. dstack/_internal/server/statics/{main-da9f8c06a69c20dac23e.css → main-8f9c66f404e9c7e7e020.css} +1 -1
  45. dstack/_internal/server/statics/{main-4a0fe83e84574654e397.js → main-e190de603dc1e9f485ec.js} +7306 -149
  46. dstack/_internal/server/statics/{main-4a0fe83e84574654e397.js.map → main-e190de603dc1e9f485ec.js.map} +1 -1
  47. dstack/_internal/utils/common.py +8 -2
  48. dstack/_internal/utils/event_loop.py +30 -0
  49. dstack/_internal/utils/ignore.py +2 -0
  50. dstack/api/server/_fleets.py +3 -5
  51. dstack/api/server/_runs.py +6 -7
  52. dstack/version.py +1 -1
  53. {dstack-0.19.1.dist-info → dstack-0.19.3.dist-info}/METADATA +27 -11
  54. {dstack-0.19.1.dist-info → dstack-0.19.3.dist-info}/RECORD +67 -57
  55. tests/_internal/core/backends/datacrunch/test_configurator.py +6 -2
  56. tests/_internal/server/background/tasks/test_process_instances.py +4 -2
  57. tests/_internal/server/background/tasks/test_process_submitted_jobs.py +29 -0
  58. tests/_internal/server/routers/test_backends.py +116 -0
  59. tests/_internal/server/routers/test_fleets.py +2 -0
  60. tests/_internal/server/routers/test_prometheus.py +21 -0
  61. tests/_internal/server/routers/test_runs.py +4 -0
  62. tests/_internal/utils/test_common.py +16 -1
  63. tests/_internal/utils/test_event_loop.py +18 -0
  64. dstack/_internal/core/backends/datacrunch/api_client.py +0 -77
  65. {dstack-0.19.1.dist-info → dstack-0.19.3.dist-info}/LICENSE.md +0 -0
  66. {dstack-0.19.1.dist-info → dstack-0.19.3.dist-info}/WHEEL +0 -0
  67. {dstack-0.19.1.dist-info → dstack-0.19.3.dist-info}/entry_points.txt +0 -0
  68. {dstack-0.19.1.dist-info → dstack-0.19.3.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ from dstack._internal.core.models.instances import InstanceType
8
8
  from dstack._internal.utils.logging import get_logger
9
9
 
10
10
  logger = get_logger(__name__)
11
- REQUEST_TIMEOUT = 12
11
+ REQUEST_TIMEOUT = 20
12
12
 
13
13
 
14
14
  class TensorDockAPIClient:
@@ -80,7 +80,7 @@ class TensorDockAPIClient:
80
80
  data["password"] = form["password"]
81
81
  return data
82
82
 
83
- def delete_single(self, instance_id: str):
83
+ def delete_single_if_exists(self, instance_id: str):
84
84
  logger.debug("Deleting instance %s", instance_id)
85
85
  resp = self.s.post(
86
86
  self._url("/client/delete/single"),
@@ -91,10 +91,11 @@ class TensorDockAPIClient:
91
91
  },
92
92
  timeout=REQUEST_TIMEOUT,
93
93
  )
94
- resp.raise_for_status()
95
94
  try:
96
95
  data = resp.json()
97
- if not data["success"]:
96
+ if "already terminated" in data.get("error", ""):
97
+ return
98
+ if not data.get("success"):
98
99
  raise BackendError(data)
99
100
  except ValueError: # json parsing error
100
101
  raise BackendError(resp.text)
@@ -12,7 +12,7 @@ from dstack._internal.core.backends.base.compute import (
12
12
  from dstack._internal.core.backends.base.offers import get_catalog_offers
13
13
  from dstack._internal.core.backends.tensordock.api_client import TensorDockAPIClient
14
14
  from dstack._internal.core.backends.tensordock.models import TensorDockConfig
15
- from dstack._internal.core.errors import BackendError, NoCapacityError
15
+ from dstack._internal.core.errors import NoCapacityError
16
16
  from dstack._internal.core.models.backends.base import BackendType
17
17
  from dstack._internal.core.models.instances import (
18
18
  InstanceAvailability,
@@ -117,17 +117,4 @@ class TensorDockCompute(
117
117
  def terminate_instance(
118
118
  self, instance_id: str, region: str, backend_data: Optional[str] = None
119
119
  ):
120
- try:
121
- self.api_client.delete_single(instance_id)
122
- except requests.HTTPError as e:
123
- logger.error(
124
- "An HTTP error occurred when trying to terminate TensorDock instance %s: %s",
125
- instance_id,
126
- e,
127
- )
128
- except BackendError as e:
129
- logger.error(
130
- "TensorDock returned an error when trying to terminate instance %s: %s",
131
- instance_id,
132
- e,
133
- )
120
+ self.api_client.delete_single_if_exists(instance_id)
@@ -102,6 +102,20 @@ class PlacementGroupInUseError(ComputeError):
102
102
  pass
103
103
 
104
104
 
105
+ class NotYetTerminated(ComputeError):
106
+ """
107
+ Used by Compute.terminate_instance to signal that instance termination is not complete
108
+ and the method should be called again after some time to continue termination.
109
+ """
110
+
111
+ def __init__(self, details: str) -> None:
112
+ """
113
+ Args:
114
+ details: some details about the termination status
115
+ """
116
+ return super().__init__(details)
117
+
118
+
105
119
  class CLIError(DstackError):
106
120
  pass
107
121
 
@@ -12,6 +12,7 @@ class BackendType(str, enum.Enum):
12
12
  DATACRUNCH (BackendType): DataCrunch
13
13
  KUBERNETES (BackendType): Kubernetes
14
14
  LAMBDA (BackendType): Lambda Cloud
15
+ NEBIUS (BackendType): Nebius AI Cloud
15
16
  OCI (BackendType): Oracle Cloud Infrastructure
16
17
  RUNPOD (BackendType): Runpod Cloud
17
18
  TENSORDOCK (BackendType): TensorDock Marketplace
@@ -29,6 +30,7 @@ class BackendType(str, enum.Enum):
29
30
  LAMBDA = "lambda"
30
31
  LOCAL = "local"
31
32
  REMOTE = "remote" # TODO: replace for LOCAL
33
+ NEBIUS = "nebius"
32
34
  OCI = "oci"
33
35
  RUNPOD = "runpod"
34
36
  TENSORDOCK = "tensordock"
@@ -240,6 +240,9 @@ class ProfileParams(CoreModel):
240
240
  Optional[UtilizationPolicy],
241
241
  Field(description="Run termination policy based on utilization"),
242
242
  ] = None
243
+ fleets: Annotated[
244
+ Optional[list[str]], Field(description="The fleets considered for reuse")
245
+ ] = None
243
246
 
244
247
  # Deprecated and unused. Left for compatibility with 0.18 clients.
245
248
  pool_name: Annotated[Optional[str], Field(exclude=True)] = None
@@ -57,11 +57,11 @@ class ChatCompletionsResponse(CoreModel):
57
57
 
58
58
 
59
59
  class ChatCompletionsChunk(CoreModel):
60
- id: str
60
+ id: Optional[str] = None
61
61
  choices: List[ChatCompletionsChunkChoice]
62
- created: int
62
+ created: Optional[int] = None
63
63
  model: str
64
- system_fingerprint: str = ""
64
+ system_fingerprint: Optional[str] = ""
65
65
  object: Literal["chat.completion.chunk"] = "chat.completion.chunk"
66
66
 
67
67
 
@@ -39,7 +39,7 @@ from dstack._internal.core.backends.remote.provisioning import (
39
39
  from dstack._internal.core.consts import DSTACK_SHIM_HTTP_PORT
40
40
 
41
41
  # FIXME: ProvisioningError is a subclass of ComputeError and should not be used outside of Compute
42
- from dstack._internal.core.errors import BackendError, ProvisioningError
42
+ from dstack._internal.core.errors import BackendError, NotYetTerminated, ProvisioningError
43
43
  from dstack._internal.core.models.backends.base import BackendType
44
44
  from dstack._internal.core.models.fleets import InstanceGroupPlacement
45
45
  from dstack._internal.core.models.instances import (
@@ -846,12 +846,17 @@ async def _terminate(instance: InstanceModel) -> None:
846
846
  instance.first_termination_retry_at = get_current_datetime()
847
847
  instance.last_termination_retry_at = get_current_datetime()
848
848
  if _next_termination_retry_at(instance) < _get_termination_deadline(instance):
849
- logger.warning(
850
- "Failed to terminate instance %s. Will retry. Error: %r",
851
- instance.name,
852
- e,
853
- exc_info=not isinstance(e, BackendError),
854
- )
849
+ if isinstance(e, NotYetTerminated):
850
+ logger.debug(
851
+ "Instance %s termination in progress: %s", instance.name, e
852
+ )
853
+ else:
854
+ logger.warning(
855
+ "Failed to terminate instance %s. Will retry. Error: %r",
856
+ instance.name,
857
+ e,
858
+ exc_info=not isinstance(e, BackendError),
859
+ )
855
860
  return
856
861
  logger.error(
857
862
  "Failed all attempts to terminate instance %s."
@@ -434,6 +434,10 @@ def _process_provisioning_with_shim(
434
434
  for volume, volume_mount in zip(volumes, volume_mounts):
435
435
  volume_mount.name = volume.name
436
436
 
437
+ instance_mounts += _get_instance_specific_mounts(
438
+ job_provisioning_data.backend, job_provisioning_data.instance_type.name
439
+ )
440
+
437
441
  container_user = "root"
438
442
 
439
443
  job_runtime_data = get_job_runtime_data(job_model)
@@ -825,3 +829,19 @@ def _submit_job_to_runner(
825
829
  # do not log here, because the runner will send a new status
826
830
 
827
831
  return True
832
+
833
+
834
+ def _get_instance_specific_mounts(
835
+ backend_type: BackendType, instance_type_name: str
836
+ ) -> List[InstanceMountPoint]:
837
+ if backend_type == BackendType.GCP and instance_type_name == "a3-megagpu-8g":
838
+ return [
839
+ InstanceMountPoint(
840
+ instance_path="/dev/aperture_devices", path="/dev/aperture_devices"
841
+ ),
842
+ InstanceMountPoint(instance_path="/var/lib/tcpxo/lib64", path="/var/lib/tcpxo/lib64"),
843
+ InstanceMountPoint(
844
+ instance_path="/var/lib/fastrak/lib64", path="/var/lib/fastrak/lib64"
845
+ ),
846
+ ]
847
+ return []
@@ -212,6 +212,7 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
212
212
  InstanceModel.deleted == False,
213
213
  InstanceModel.total_blocks > InstanceModel.busy_blocks,
214
214
  )
215
+ .options(joinedload(InstanceModel.fleet))
215
216
  .execution_options(populate_existing=True)
216
217
  )
217
218
  pool_instances = list(res.unique().scalars().all())
@@ -612,7 +613,7 @@ def _get_offer_mount_point_volume(
612
613
  for volume in volumes:
613
614
  if (
614
615
  volume.configuration.backend != offer.backend
615
- or volume.configuration.region != offer.region
616
+ or volume.configuration.region.lower() != offer.region.lower()
616
617
  ):
617
618
  continue
618
619
  return volume
@@ -640,7 +641,7 @@ async def _attach_volumes(
640
641
  try:
641
642
  if (
642
643
  job_provisioning_data.get_base_backend() != volume.configuration.backend
643
- or job_provisioning_data.region != volume.configuration.region
644
+ or job_provisioning_data.region.lower() != volume.configuration.region.lower()
644
645
  ):
645
646
  continue
646
647
  if volume.provisioning_data is not None and volume.provisioning_data.attachable:
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from typing import Annotated
2
3
 
3
4
  from fastapi import APIRouter, Depends
@@ -6,12 +7,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
7
 
7
8
  from dstack._internal.server import settings
8
9
  from dstack._internal.server.db import get_session
10
+ from dstack._internal.server.security.permissions import OptionalServiceAccount
9
11
  from dstack._internal.server.services import prometheus
10
12
  from dstack._internal.server.utils.routers import error_not_found
11
13
 
14
+ _auth = OptionalServiceAccount(os.getenv("DSTACK_PROMETHEUS_AUTH_TOKEN"))
15
+
12
16
  router = APIRouter(
13
17
  tags=["prometheus"],
14
18
  default_response_class=PlainTextResponse,
19
+ dependencies=[Depends(_auth)],
15
20
  )
16
21
 
17
22
 
@@ -1,4 +1,4 @@
1
- from typing import Tuple
1
+ from typing import Annotated, Optional, Tuple
2
2
 
3
3
  from fastapi import Depends, HTTPException, Security
4
4
  from fastapi.security import HTTPBearer
@@ -99,6 +99,24 @@ class ProjectMember:
99
99
  return await get_project_member(session, project_name, token.credentials)
100
100
 
101
101
 
102
+ class OptionalServiceAccount:
103
+ def __init__(self, token: Optional[str]) -> None:
104
+ self._token = token
105
+
106
+ async def __call__(
107
+ self,
108
+ token: Annotated[
109
+ Optional[HTTPAuthorizationCredentials], Security(HTTPBearer(auto_error=False))
110
+ ],
111
+ ) -> None:
112
+ if self._token is None:
113
+ return
114
+ if token is None:
115
+ raise error_forbidden()
116
+ if token.credentials != self._token:
117
+ raise error_invalid_token()
118
+
119
+
102
120
  async def get_project_member(
103
121
  session: AsyncSession, project_name: str, token: str
104
122
  ) -> Tuple[UserModel, ProjectModel]:
@@ -176,23 +176,29 @@ def filter_pool_instances(
176
176
  regions = [master_job_provisioning_data.region]
177
177
  regions = [r for r in regions if r == master_job_provisioning_data.region]
178
178
 
179
+ if regions is not None:
180
+ regions = [r.lower() for r in regions]
181
+ instance_types = profile.instance_types
182
+ if instance_types is not None:
183
+ instance_types = [i.lower() for i in instance_types]
184
+
179
185
  for instance in pool_instances:
180
186
  if fleet_model is not None and instance.fleet_id != fleet_model.id:
181
187
  continue
182
188
  if instance.unreachable:
183
189
  continue
190
+ fleet = instance.fleet
191
+ if profile.fleets is not None and (fleet is None or fleet.name not in profile.fleets):
192
+ continue
184
193
  if status is not None and instance.status != status:
185
194
  continue
186
195
  jpd = get_instance_provisioning_data(instance)
187
196
  if jpd is not None:
188
197
  if backend_types is not None and jpd.get_base_backend() not in backend_types:
189
198
  continue
190
- if regions is not None and jpd.region not in regions:
199
+ if regions is not None and jpd.region.lower() not in regions:
191
200
  continue
192
- if (
193
- profile.instance_types is not None
194
- and jpd.instance_type.name not in profile.instance_types
195
- ):
201
+ if instance_types is not None and jpd.instance_type.name.lower() not in instance_types:
196
202
  continue
197
203
  if (
198
204
  jpd.availability_zone is not None
@@ -268,10 +274,12 @@ async def get_pool_instances(
268
274
  project: ProjectModel,
269
275
  ) -> List[InstanceModel]:
270
276
  res = await session.execute(
271
- select(InstanceModel).where(
277
+ select(InstanceModel)
278
+ .where(
272
279
  InstanceModel.project_id == project.id,
273
280
  InstanceModel.deleted == False,
274
281
  )
282
+ .options(joinedload(InstanceModel.fleet))
275
283
  )
276
284
  instance_models = list(res.unique().scalars().all())
277
285
  return instance_models
@@ -668,15 +668,15 @@ def _get_job_mount_point_attached_volume(
668
668
  for volume in volumes:
669
669
  if (
670
670
  volume.configuration.backend != job_provisioning_data.get_base_backend()
671
- or volume.configuration.region != job_provisioning_data.region
671
+ or volume.configuration.region.lower() != job_provisioning_data.region.lower()
672
672
  ):
673
673
  continue
674
674
  if (
675
675
  volume.provisioning_data is not None
676
676
  and volume.provisioning_data.availability_zone is not None
677
677
  and job_provisioning_data.availability_zone is not None
678
- and volume.provisioning_data.availability_zone
679
- != job_provisioning_data.availability_zone
678
+ and volume.provisioning_data.availability_zone.lower()
679
+ != job_provisioning_data.availability_zone.lower()
680
680
  ):
681
681
  continue
682
682
  return volume
@@ -101,7 +101,8 @@ async def get_offers_by_requirements(
101
101
  offers = [(b, o) for b, o in offers if o.backend in backend_types]
102
102
 
103
103
  if regions is not None:
104
- offers = [(b, o) for b, o in offers if o.region in regions]
104
+ regions = [r.lower() for r in regions]
105
+ offers = [(b, o) for b, o in offers if o.region.lower() in regions]
105
106
 
106
107
  if availability_zones is not None:
107
108
  new_offers = []
@@ -116,7 +117,8 @@ async def get_offers_by_requirements(
116
117
  offers = new_offers
117
118
 
118
119
  if profile.instance_types is not None:
119
- offers = [(b, o) for b, o in offers if o.instance.name in profile.instance_types]
120
+ instance_types = [i.lower() for i in profile.instance_types]
121
+ offers = [(b, o) for b, o in offers if o.instance.name.lower() in instance_types]
120
122
 
121
123
  if blocks == 1:
122
124
  return offers
@@ -831,8 +831,6 @@ def _validate_run_spec_and_set_defaults(run_spec: RunSpec):
831
831
  for mount_point in run_spec.configuration.volumes:
832
832
  if not is_valid_docker_volume_target(mount_point.path):
833
833
  raise ServerClientError(f"Invalid volume mount path: {mount_point.path}")
834
- if mount_point.path.startswith("/workflow"):
835
- raise ServerClientError("Mounting volumes inside /workflow is not supported")
836
834
  if run_spec.repo_id is None and run_spec.repo_data is not None:
837
835
  raise ServerClientError("repo_data must not be set if repo_id is not set")
838
836
  if run_spec.repo_id is not None and run_spec.repo_data is 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-4a0fe83e84574654e397.js"></script><link href="/main-da9f8c06a69c20dac23e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>
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-e190de603dc1e9f485ec.js"></script><link href="/main-8f9c66f404e9c7e7e020.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>