dstack 0.18.40rc1__py3-none-any.whl → 0.18.41__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 (98) hide show
  1. dstack/_internal/cli/commands/apply.py +8 -5
  2. dstack/_internal/cli/services/configurators/base.py +4 -2
  3. dstack/_internal/cli/services/configurators/fleet.py +21 -9
  4. dstack/_internal/cli/services/configurators/gateway.py +15 -0
  5. dstack/_internal/cli/services/configurators/run.py +6 -5
  6. dstack/_internal/cli/services/configurators/volume.py +15 -0
  7. dstack/_internal/cli/services/repos.py +3 -3
  8. dstack/_internal/cli/utils/fleet.py +44 -33
  9. dstack/_internal/cli/utils/run.py +27 -7
  10. dstack/_internal/cli/utils/volume.py +21 -9
  11. dstack/_internal/core/backends/aws/compute.py +92 -52
  12. dstack/_internal/core/backends/aws/resources.py +22 -12
  13. dstack/_internal/core/backends/azure/compute.py +2 -0
  14. dstack/_internal/core/backends/base/compute.py +20 -2
  15. dstack/_internal/core/backends/gcp/compute.py +30 -23
  16. dstack/_internal/core/backends/gcp/resources.py +0 -15
  17. dstack/_internal/core/backends/oci/compute.py +10 -5
  18. dstack/_internal/core/backends/oci/resources.py +23 -26
  19. dstack/_internal/core/backends/remote/provisioning.py +65 -27
  20. dstack/_internal/core/backends/runpod/compute.py +1 -0
  21. dstack/_internal/core/models/backends/azure.py +3 -1
  22. dstack/_internal/core/models/configurations.py +24 -1
  23. dstack/_internal/core/models/fleets.py +46 -0
  24. dstack/_internal/core/models/instances.py +5 -1
  25. dstack/_internal/core/models/pools.py +4 -1
  26. dstack/_internal/core/models/profiles.py +10 -4
  27. dstack/_internal/core/models/runs.py +20 -0
  28. dstack/_internal/core/models/volumes.py +3 -0
  29. dstack/_internal/core/services/ssh/attach.py +92 -53
  30. dstack/_internal/core/services/ssh/tunnel.py +58 -31
  31. dstack/_internal/proxy/gateway/routers/registry.py +2 -0
  32. dstack/_internal/proxy/gateway/schemas/registry.py +2 -0
  33. dstack/_internal/proxy/gateway/services/registry.py +4 -0
  34. dstack/_internal/proxy/lib/models.py +3 -0
  35. dstack/_internal/proxy/lib/services/service_connection.py +8 -1
  36. dstack/_internal/server/background/tasks/process_instances.py +72 -33
  37. dstack/_internal/server/background/tasks/process_metrics.py +9 -9
  38. dstack/_internal/server/background/tasks/process_running_jobs.py +73 -26
  39. dstack/_internal/server/background/tasks/process_runs.py +2 -12
  40. dstack/_internal/server/background/tasks/process_submitted_jobs.py +109 -42
  41. dstack/_internal/server/background/tasks/process_terminating_jobs.py +1 -1
  42. dstack/_internal/server/migrations/versions/1338b788b612_reverse_job_instance_relationship.py +71 -0
  43. dstack/_internal/server/migrations/versions/1e76fb0dde87_add_jobmodel_inactivity_secs.py +32 -0
  44. dstack/_internal/server/migrations/versions/51d45659d574_add_instancemodel_blocks_fields.py +43 -0
  45. dstack/_internal/server/migrations/versions/63c3f19cb184_add_jobterminationreason_inactivity_.py +83 -0
  46. dstack/_internal/server/models.py +10 -4
  47. dstack/_internal/server/routers/runs.py +1 -0
  48. dstack/_internal/server/schemas/runner.py +1 -0
  49. dstack/_internal/server/services/backends/configurators/azure.py +34 -8
  50. dstack/_internal/server/services/config.py +9 -0
  51. dstack/_internal/server/services/fleets.py +27 -2
  52. dstack/_internal/server/services/gateways/client.py +9 -1
  53. dstack/_internal/server/services/jobs/__init__.py +215 -43
  54. dstack/_internal/server/services/jobs/configurators/base.py +47 -2
  55. dstack/_internal/server/services/offers.py +91 -5
  56. dstack/_internal/server/services/pools.py +95 -11
  57. dstack/_internal/server/services/proxy/repo.py +17 -3
  58. dstack/_internal/server/services/runner/client.py +1 -1
  59. dstack/_internal/server/services/runner/ssh.py +33 -5
  60. dstack/_internal/server/services/runs.py +48 -179
  61. dstack/_internal/server/services/services/__init__.py +9 -1
  62. dstack/_internal/server/statics/index.html +1 -1
  63. dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js → main-2ac66bfcbd2e39830b88.js} +30 -31
  64. dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js.map → main-2ac66bfcbd2e39830b88.js.map} +1 -1
  65. dstack/_internal/server/statics/{main-fc56d1f4af8e57522a1c.css → main-ad5150a441de98cd8987.css} +1 -1
  66. dstack/_internal/server/testing/common.py +117 -52
  67. dstack/_internal/utils/common.py +22 -8
  68. dstack/_internal/utils/env.py +14 -0
  69. dstack/_internal/utils/ssh.py +1 -1
  70. dstack/api/server/_fleets.py +25 -1
  71. dstack/api/server/_runs.py +23 -2
  72. dstack/api/server/_volumes.py +12 -1
  73. dstack/version.py +1 -1
  74. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/METADATA +1 -1
  75. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/RECORD +98 -89
  76. tests/_internal/cli/services/configurators/test_profile.py +3 -3
  77. tests/_internal/core/services/ssh/test_tunnel.py +56 -4
  78. tests/_internal/proxy/gateway/routers/test_registry.py +30 -7
  79. tests/_internal/server/background/tasks/test_process_instances.py +138 -20
  80. tests/_internal/server/background/tasks/test_process_metrics.py +12 -0
  81. tests/_internal/server/background/tasks/test_process_running_jobs.py +192 -0
  82. tests/_internal/server/background/tasks/test_process_runs.py +27 -3
  83. tests/_internal/server/background/tasks/test_process_submitted_jobs.py +48 -3
  84. tests/_internal/server/background/tasks/test_process_terminating_jobs.py +126 -13
  85. tests/_internal/server/routers/test_fleets.py +15 -2
  86. tests/_internal/server/routers/test_pools.py +6 -0
  87. tests/_internal/server/routers/test_runs.py +27 -0
  88. tests/_internal/server/services/jobs/__init__.py +0 -0
  89. tests/_internal/server/services/jobs/configurators/__init__.py +0 -0
  90. tests/_internal/server/services/jobs/configurators/test_base.py +72 -0
  91. tests/_internal/server/services/test_pools.py +4 -0
  92. tests/_internal/server/services/test_runs.py +5 -41
  93. tests/_internal/utils/test_common.py +21 -0
  94. tests/_internal/utils/test_env.py +38 -0
  95. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/LICENSE.md +0 -0
  96. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/WHEEL +0 -0
  97. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/entry_points.txt +0 -0
  98. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/top_level.txt +0 -0
@@ -29,7 +29,6 @@ from dstack._internal.core.models.runs import (
29
29
  ApplyRunPlanInput,
30
30
  Job,
31
31
  JobPlan,
32
- JobProvisioningData,
33
32
  JobSpec,
34
33
  JobStatus,
35
34
  JobSubmission,
@@ -45,8 +44,6 @@ from dstack._internal.core.models.users import GlobalRole
45
44
  from dstack._internal.core.models.volumes import (
46
45
  InstanceMountPoint,
47
46
  Volume,
48
- VolumeMountPoint,
49
- VolumeStatus,
50
47
  )
51
48
  from dstack._internal.core.services import validate_dstack_resource_name
52
49
  from dstack._internal.core.services.diff import diff_models
@@ -58,15 +55,15 @@ from dstack._internal.server.models import (
58
55
  RepoModel,
59
56
  RunModel,
60
57
  UserModel,
61
- VolumeModel,
62
58
  )
63
59
  from dstack._internal.server.services import repos as repos_services
64
60
  from dstack._internal.server.services import services
65
- from dstack._internal.server.services import volumes as volumes_services
66
61
  from dstack._internal.server.services.docker import is_valid_docker_volume_target
67
62
  from dstack._internal.server.services.jobs import (
63
+ check_can_attach_job_volumes,
68
64
  delay_job_instance_termination,
69
65
  get_instances_ids_with_detaching_volumes,
66
+ get_job_configured_volumes,
70
67
  get_jobs_from_run_spec,
71
68
  group_jobs_by_replica_latest,
72
69
  job_model_to_job_submission,
@@ -80,6 +77,7 @@ from dstack._internal.server.services.pools import (
80
77
  get_instance_offer,
81
78
  get_or_create_pool_by_name,
82
79
  get_pool_instances,
80
+ get_shared_pool_instances_with_offers,
83
81
  )
84
82
  from dstack._internal.server.services.projects import list_project_models, list_user_project_models
85
83
  from dstack._internal.server.services.users import get_user_model_by_name
@@ -164,7 +162,8 @@ async def list_projects_run_models(
164
162
  limit: int,
165
163
  ascending: bool,
166
164
  ) -> List[RunModel]:
167
- filters = [RunModel.deleted == False, RunModel.project_id.in_(p.id for p in projects)]
165
+ filters = []
166
+ filters.append(RunModel.project_id.in_(p.id for p in projects))
168
167
  if repo is not None:
169
168
  filters.append(RunModel.repo_id == repo.id)
170
169
  if runs_user is not None:
@@ -300,10 +299,11 @@ async def get_plan(
300
299
  # TODO(egor-s): do we need to generate all replicas here?
301
300
  jobs = await get_jobs_from_run_spec(run_spec, replica_num=0)
302
301
 
303
- volumes = await get_run_volumes(
302
+ volumes = await get_job_configured_volumes(
304
303
  session=session,
305
304
  project=project,
306
305
  run_spec=run_spec,
306
+ job_num=0,
307
307
  )
308
308
 
309
309
  pool = await get_or_create_pool_by_name(
@@ -450,7 +450,7 @@ async def submit_run(
450
450
  else:
451
451
  await delete_runs(session=session, project=project, runs_names=[run_spec.run_name])
452
452
 
453
- await validate_run(
453
+ await _validate_run(
454
454
  session=session,
455
455
  user=user,
456
456
  project=project,
@@ -677,27 +677,40 @@ async def _get_pool_offers(
677
677
  run_spec: RunSpec,
678
678
  job: Job,
679
679
  volumes: List[List[Volume]],
680
- ) -> List[InstanceOfferWithAvailability]:
680
+ ) -> list[InstanceOfferWithAvailability]:
681
+ pool_offers: list[InstanceOfferWithAvailability] = []
682
+
681
683
  detaching_instances_ids = await get_instances_ids_with_detaching_volumes(session)
682
684
  pool_instances = [i for i in get_pool_instances(pool) if i.id not in detaching_instances_ids]
683
- pool_filtered_instances = filter_pool_instances(
685
+ multinode = job.job_spec.jobs_per_replica > 1
686
+
687
+ if not multinode:
688
+ shared_instances_with_offers = get_shared_pool_instances_with_offers(
689
+ pool_instances=pool_instances,
690
+ profile=run_spec.merged_profile,
691
+ requirements=job.job_spec.requirements,
692
+ volumes=volumes,
693
+ )
694
+ for _, offer in shared_instances_with_offers:
695
+ pool_offers.append(offer)
696
+
697
+ nonshared_instances = filter_pool_instances(
684
698
  pool_instances=pool_instances,
685
699
  profile=run_spec.merged_profile,
686
700
  requirements=job.job_spec.requirements,
687
- multinode=job.job_spec.jobs_per_replica > 1,
701
+ multinode=multinode,
688
702
  volumes=volumes,
703
+ shared=False,
689
704
  )
690
- pool_offers: List[InstanceOfferWithAvailability] = []
691
- for instance in pool_filtered_instances:
705
+ for instance in nonshared_instances:
692
706
  offer = get_instance_offer(instance)
693
707
  if offer is None:
694
708
  continue
695
709
  offer.availability = InstanceAvailability.BUSY
696
710
  if instance.status == InstanceStatus.IDLE:
697
711
  offer.availability = InstanceAvailability.IDLE
698
- if instance.unreachable:
699
- offer.availability = InstanceAvailability.NOT_AVAILABLE
700
712
  pool_offers.append(offer)
713
+
701
714
  pool_offers.sort(key=lambda offer: offer.price)
702
715
  return pool_offers
703
716
 
@@ -722,186 +735,42 @@ async def _generate_run_name(
722
735
  idx += 1
723
736
 
724
737
 
725
- async def validate_run(
726
- session: AsyncSession,
727
- user: UserModel,
728
- project: ProjectModel,
729
- run_spec: RunSpec,
730
- ):
731
- volumes = await get_run_volumes(
732
- session=session,
733
- project=project,
734
- run_spec=run_spec,
735
- )
736
- check_can_attach_run_volumes(
737
- run_spec=run_spec,
738
- volumes=volumes,
738
+ def check_run_spec_requires_instance_mounts(run_spec: RunSpec) -> bool:
739
+ return any(
740
+ is_core_model_instance(mp, InstanceMountPoint) and not mp.optional
741
+ for mp in run_spec.configuration.volumes
739
742
  )
740
743
 
741
744
 
742
- async def get_run_volumes(
745
+ async def _validate_run(
743
746
  session: AsyncSession,
747
+ user: UserModel,
744
748
  project: ProjectModel,
745
749
  run_spec: RunSpec,
746
- ) -> List[List[Volume]]:
747
- """
748
- Returns list of run volumes grouped by mount points.
749
- """
750
- volume_models = await get_run_volume_models(
750
+ ):
751
+ await _validate_run_volumes(
751
752
  session=session,
752
753
  project=project,
753
754
  run_spec=run_spec,
754
755
  )
755
- return [
756
- [volumes_services.volume_model_to_volume(v) for v in mount_point_volume_models]
757
- for mount_point_volume_models in volume_models
758
- ]
759
756
 
760
757
 
761
- async def get_run_volume_models(
758
+ async def _validate_run_volumes(
762
759
  session: AsyncSession,
763
760
  project: ProjectModel,
764
761
  run_spec: RunSpec,
765
- ) -> List[List[VolumeModel]]:
766
- """
767
- Returns list of run volume models grouped by mount points.
768
- """
769
- if len(run_spec.configuration.volumes) == 0:
770
- return []
771
- volume_models = []
772
- for mount_point in run_spec.configuration.volumes:
773
- if not is_core_model_instance(mount_point, VolumeMountPoint):
774
- continue
775
- if isinstance(mount_point.name, str):
776
- names = [mount_point.name]
777
- else:
778
- names = mount_point.name
779
- mount_point_volume_models = []
780
- for name in names:
781
- volume_model = await volumes_services.get_project_volume_model_by_name(
782
- session=session,
783
- project=project,
784
- name=name,
785
- )
786
- if volume_model is None:
787
- raise ResourceNotExistsError(f"Volume {mount_point.name} not found")
788
- mount_point_volume_models.append(volume_model)
789
- volume_models.append(mount_point_volume_models)
790
- return volume_models
791
-
792
-
793
- def check_can_attach_run_volumes(
794
- run_spec: RunSpec,
795
- volumes: List[List[Volume]],
796
762
  ):
797
- """
798
- Performs basic checks if volumes can be attached.
799
- This is useful to show error ASAP (when user submits the run).
800
- If the attachment is to fail anyway, the error will be handled when proccessing submitted jobs.
801
- """
802
- if len(volumes) == 0:
803
- return
804
- expected_backends = {v.configuration.backend for v in volumes[0]}
805
- expected_regions = {v.configuration.region for v in volumes[0]}
806
- for mount_point_volumes in volumes:
807
- backends = {v.configuration.backend for v in mount_point_volumes}
808
- regions = {v.configuration.region for v in mount_point_volumes}
809
- if backends != expected_backends:
810
- raise ServerClientError(
811
- "Volumes from different backends specified for different mount points"
812
- )
813
- if regions != expected_regions:
814
- raise ServerClientError(
815
- "Volumes from different regions specified for different mount points"
816
- )
817
- for volume in mount_point_volumes:
818
- if volume.status != VolumeStatus.ACTIVE:
819
- raise ServerClientError(f"Cannot mount volumes that are not active: {volume.name}")
820
- volumes_names = [v.name for vs in volumes for v in vs]
821
- if len(volumes_names) != len(set(volumes_names)):
822
- raise ServerClientError("Cannot attach the same volume at different mount points")
823
-
824
-
825
- async def get_job_volumes(
826
- session: AsyncSession,
827
- project: ProjectModel,
828
- run_spec: RunSpec,
829
- job_provisioning_data: JobProvisioningData,
830
- ) -> List[Volume]:
831
- """
832
- Returns volumes attached to the job.
833
- """
834
- run_volumes = await get_run_volumes(
835
- session=session,
836
- project=project,
837
- run_spec=run_spec,
838
- )
839
- job_volumes = []
840
- for mount_point_volumes in run_volumes:
841
- job_volumes.append(get_job_mount_point_volume(mount_point_volumes, job_provisioning_data))
842
- return job_volumes
843
-
844
-
845
- def get_job_mount_point_volume(
846
- volumes: List[Volume],
847
- job_provisioning_data: JobProvisioningData,
848
- ) -> Volume:
849
- """
850
- Returns the volume attached to the job among the list of possible mount point volumes.
851
- """
852
- for volume in volumes:
853
- if (
854
- volume.configuration.backend != job_provisioning_data.get_base_backend()
855
- or volume.configuration.region != job_provisioning_data.region
856
- ):
857
- continue
858
- if (
859
- volume.provisioning_data is not None
860
- and volume.provisioning_data.availability_zone is not None
861
- and job_provisioning_data.availability_zone is not None
862
- and volume.provisioning_data.availability_zone
863
- != job_provisioning_data.availability_zone
864
- ):
865
- continue
866
- return volume
867
- raise ServerClientError("Failed to find an eligible volume for the mount point")
868
-
869
-
870
- def get_offer_volumes(
871
- volumes: List[List[Volume]],
872
- offer: InstanceOfferWithAvailability,
873
- ) -> List[Volume]:
874
- """
875
- Returns volumes suitable for the offer for each mount point.
876
- """
877
- offer_volumes = []
878
- for mount_point_volumes in volumes:
879
- offer_volumes.append(get_offer_mount_point_volume(mount_point_volumes, offer))
880
- return offer_volumes
881
-
882
-
883
- def get_offer_mount_point_volume(
884
- volumes: List[Volume],
885
- offer: InstanceOfferWithAvailability,
886
- ) -> Volume:
887
- """
888
- Returns the first suitable volume for the offer among possible mount point volumes.
889
- """
890
- for volume in volumes:
891
- if (
892
- volume.configuration.backend != offer.backend
893
- or volume.configuration.region != offer.region
894
- ):
895
- continue
896
- return volume
897
- raise ServerClientError("Failed to find an eligible volume for the mount point")
898
-
899
-
900
- def check_run_spec_requires_instance_mounts(run_spec: RunSpec) -> bool:
901
- return any(
902
- is_core_model_instance(mp, InstanceMountPoint) and not mp.optional
903
- for mp in run_spec.configuration.volumes
904
- )
763
+ # The volumes validation should be done here and not in job configurator
764
+ # since potentially we may need to validate volumes for jobs/replicas
765
+ # that won't be created immediately (e.g. range of replicas or nodes).
766
+ nodes = 1
767
+ if run_spec.configuration.type == "task":
768
+ nodes = run_spec.configuration.nodes
769
+ for job_num in range(nodes):
770
+ volumes = await get_job_configured_volumes(
771
+ session=session, project=project, run_spec=run_spec, job_num=job_num
772
+ )
773
+ check_can_attach_job_volumes(volumes=volumes)
905
774
 
906
775
 
907
776
  async def _get_run_repo_or_error(
@@ -21,6 +21,7 @@ from dstack._internal.core.errors import (
21
21
  from dstack._internal.core.models.common import is_core_model_instance
22
22
  from dstack._internal.core.models.configurations import SERVICE_HTTPS_DEFAULT, ServiceConfiguration
23
23
  from dstack._internal.core.models.gateways import GatewayConfiguration, GatewayStatus
24
+ from dstack._internal.core.models.instances import SSHConnectionParams
24
25
  from dstack._internal.core.models.runs import Run, RunSpec, ServiceModelSpec, ServiceSpec
25
26
  from dstack._internal.server import settings
26
27
  from dstack._internal.server.models import GatewayModel, JobModel, ProjectModel, RunModel
@@ -155,7 +156,12 @@ def get_service_spec(
155
156
 
156
157
 
157
158
  async def register_replica(
158
- session: AsyncSession, gateway_id: Optional[uuid.UUID], run: Run, job_model: JobModel
159
+ session: AsyncSession,
160
+ gateway_id: Optional[uuid.UUID],
161
+ run: Run,
162
+ job_model: JobModel,
163
+ ssh_head_proxy: Optional[SSHConnectionParams],
164
+ ssh_head_proxy_private_key: Optional[str],
159
165
  ):
160
166
  if gateway_id is None: # in-server proxy
161
167
  return
@@ -167,6 +173,8 @@ async def register_replica(
167
173
  await client.register_replica(
168
174
  run=run,
169
175
  job_submission=job_submission,
176
+ ssh_head_proxy=ssh_head_proxy,
177
+ ssh_head_proxy_private_key=ssh_head_proxy_private_key,
170
178
  )
171
179
  logger.info("%s: replica is registered for service %s", fmt(job_model), run.id.hex)
172
180
  except (httpx.RequestError, SSHError) as e:
@@ -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-11ec5e4a00ea6ec833e3.js"></script><link href="/main-fc56d1f4af8e57522a1c.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-2ac66bfcbd2e39830b88.js"></script><link href="/main-ad5150a441de98cd8987.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>