dstack 0.19.0rc1__py3-none-any.whl → 0.19.2__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 (37) 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/core/backends/base/compute.py +3 -0
  5. dstack/_internal/core/backends/base/models.py +7 -7
  6. dstack/_internal/core/backends/configurators.py +9 -0
  7. dstack/_internal/core/backends/models.py +8 -0
  8. dstack/_internal/core/backends/nebius/__init__.py +0 -0
  9. dstack/_internal/core/backends/nebius/backend.py +16 -0
  10. dstack/_internal/core/backends/nebius/compute.py +270 -0
  11. dstack/_internal/core/backends/nebius/configurator.py +74 -0
  12. dstack/_internal/core/backends/nebius/models.py +108 -0
  13. dstack/_internal/core/backends/nebius/resources.py +222 -0
  14. dstack/_internal/core/errors.py +14 -0
  15. dstack/_internal/core/models/backends/base.py +2 -0
  16. dstack/_internal/proxy/lib/schemas/model_proxy.py +3 -3
  17. dstack/_internal/server/background/tasks/process_instances.py +26 -12
  18. dstack/_internal/server/routers/prometheus.py +5 -12
  19. dstack/_internal/server/security/permissions.py +19 -1
  20. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +1 -1
  21. dstack/_internal/server/services/prometheus.py +175 -112
  22. dstack/_internal/server/statics/index.html +1 -1
  23. dstack/_internal/server/statics/{main-4fd5a4770eff59325ee3.js → main-bcb3228138bc8483cc0b.js} +7278 -131
  24. dstack/_internal/server/statics/{main-4fd5a4770eff59325ee3.js.map → main-bcb3228138bc8483cc0b.js.map} +1 -1
  25. dstack/_internal/server/statics/{main-da9f8c06a69c20dac23e.css → main-c0bdaac8f1ea67d499eb.css} +1 -1
  26. dstack/_internal/utils/event_loop.py +30 -0
  27. dstack/version.py +1 -1
  28. {dstack-0.19.0rc1.dist-info → dstack-0.19.2.dist-info}/METADATA +27 -11
  29. {dstack-0.19.0rc1.dist-info → dstack-0.19.2.dist-info}/RECORD +37 -28
  30. tests/_internal/server/background/tasks/test_process_instances.py +68 -2
  31. tests/_internal/server/routers/test_backends.py +116 -0
  32. tests/_internal/server/routers/test_prometheus.py +158 -120
  33. tests/_internal/utils/test_event_loop.py +18 -0
  34. {dstack-0.19.0rc1.dist-info → dstack-0.19.2.dist-info}/LICENSE.md +0 -0
  35. {dstack-0.19.0rc1.dist-info → dstack-0.19.2.dist-info}/WHEEL +0 -0
  36. {dstack-0.19.0rc1.dist-info → dstack-0.19.2.dist-info}/entry_points.txt +0 -0
  37. {dstack-0.19.0rc1.dist-info → dstack-0.19.2.dist-info}/top_level.txt +0 -0
@@ -1,40 +1,38 @@
1
1
  import itertools
2
+ from collections import defaultdict
2
3
  from collections.abc import Generator, Iterable
3
4
  from datetime import timezone
5
+ from typing import ClassVar
6
+ from uuid import UUID
4
7
 
5
8
  from prometheus_client import Metric
6
9
  from prometheus_client.parser import text_string_to_metric_families
7
10
  from prometheus_client.samples import Sample
8
- from sqlalchemy import select
11
+ from sqlalchemy import func, select
9
12
  from sqlalchemy.ext.asyncio import AsyncSession
10
- from sqlalchemy.orm import joinedload
13
+ from sqlalchemy.orm import aliased, joinedload
11
14
 
12
15
  from dstack._internal.core.models.instances import InstanceStatus
13
- from dstack._internal.core.models.runs import JobStatus, RunSpec
16
+ from dstack._internal.core.models.runs import JobStatus, RunSpec, RunStatus
14
17
  from dstack._internal.server.models import (
15
18
  InstanceModel,
19
+ JobMetricsPoint,
16
20
  JobModel,
17
21
  JobPrometheusMetrics,
18
22
  ProjectModel,
19
23
  RunModel,
24
+ UserModel,
20
25
  )
21
26
  from dstack._internal.server.services.instances import get_instance_offer
22
27
  from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
23
28
  from dstack._internal.utils.common import get_current_datetime
24
29
 
25
- _INSTANCE_DURATION = "dstack_instance_duration_seconds_total"
26
- _INSTANCE_PRICE = "dstack_instance_price_dollars_per_hour"
27
- _INSTANCE_GPU_COUNT = "dstack_instance_gpu_count"
28
- _JOB_DURATION = "dstack_job_duration_seconds_total"
29
- _JOB_PRICE = "dstack_job_price_dollars_per_hour"
30
- _JOB_GPU_COUNT = "dstack_job_gpu_count"
31
-
32
30
 
33
31
  async def get_metrics(session: AsyncSession) -> str:
34
32
  metrics_iter = itertools.chain(
35
33
  await get_instance_metrics(session),
34
+ await get_run_metrics(session),
36
35
  await get_job_metrics(session),
37
- await get_job_gpu_metrics(session),
38
36
  )
39
37
  return "\n".join(_render_metrics(metrics_iter)) + "\n"
40
38
 
@@ -61,19 +59,7 @@ async def get_instance_metrics(session: AsyncSession) -> Iterable[Metric]:
61
59
  )
62
60
  )
63
61
  instances = res.unique().scalars().all()
64
- metrics: dict[str, Metric] = {
65
- _INSTANCE_DURATION: Metric(
66
- name=_INSTANCE_DURATION,
67
- documentation="Total seconds the instance is running",
68
- typ="counter",
69
- ),
70
- _INSTANCE_PRICE: Metric(
71
- name=_INSTANCE_PRICE, documentation="Instance price, USD/hour", typ="gauge"
72
- ),
73
- _INSTANCE_GPU_COUNT: Metric(
74
- name=_INSTANCE_GPU_COUNT, documentation="Instance GPU count", typ="gauge"
75
- ),
76
- }
62
+ metrics = _InstanceMetrics()
77
63
  now = get_current_datetime()
78
64
  for instance in instances:
79
65
  fleet = instance.fleet
@@ -94,15 +80,36 @@ async def get_instance_metrics(session: AsyncSession) -> Iterable[Metric]:
94
80
  "dstack_gpu": gpu,
95
81
  }
96
82
  duration = (now - instance.created_at.replace(tzinfo=timezone.utc)).total_seconds()
97
- metrics[_INSTANCE_DURATION].add_sample(
98
- name=_INSTANCE_DURATION, labels=labels, value=duration
99
- )
100
- metrics[_INSTANCE_PRICE].add_sample(
101
- name=_INSTANCE_PRICE, labels=labels, value=instance.price or 0.0
102
- )
103
- metrics[_INSTANCE_GPU_COUNT].add_sample(
104
- name=_INSTANCE_GPU_COUNT, labels=labels, value=gpu_count
105
- )
83
+ metrics.add_sample(_INSTANCE_DURATION, labels, duration)
84
+ metrics.add_sample(_INSTANCE_PRICE, labels, instance.price or 0.0)
85
+ metrics.add_sample(_INSTANCE_GPU_COUNT, labels, gpu_count)
86
+ return metrics.values()
87
+
88
+
89
+ async def get_run_metrics(session: AsyncSession) -> Iterable[Metric]:
90
+ res = await session.execute(
91
+ select(ProjectModel.name, UserModel.name, RunModel.status, func.count(RunModel.id))
92
+ .join_from(RunModel, ProjectModel)
93
+ .join_from(RunModel, UserModel, RunModel.user_id == UserModel.id)
94
+ .group_by(ProjectModel.name, UserModel.name, RunModel.status)
95
+ .order_by(ProjectModel.name, UserModel.name, RunModel.status)
96
+ )
97
+ projects: dict[str, dict[str, dict[RunStatus, int]]] = defaultdict(
98
+ lambda: defaultdict(lambda: defaultdict(int))
99
+ )
100
+ for project_name, user_name, status, count in res.all():
101
+ projects[project_name][user_name][status] = count
102
+ metrics = _RunMetrics()
103
+ for project_name, users in projects.items():
104
+ for user_name, statuses in users.items():
105
+ labels: dict[str, str] = {
106
+ "dstack_project_name": project_name,
107
+ "dstack_user_name": user_name,
108
+ }
109
+ metrics.add_sample(_RUN_COUNT_TOTAL, labels, sum(statuses.values()))
110
+ metrics.add_sample(_RUN_COUNT_TERMINATED, labels, statuses[RunStatus.TERMINATED])
111
+ metrics.add_sample(_RUN_COUNT_FAILED, labels, statuses[RunStatus.FAILED])
112
+ metrics.add_sample(_RUN_COUNT_DONE, labels, statuses[RunStatus.DONE])
106
113
  return metrics.values()
107
114
 
108
115
 
@@ -127,106 +134,162 @@ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
127
134
  )
128
135
  )
129
136
  jobs = res.scalars().all()
130
- metrics: dict[str, Metric] = {
131
- _JOB_DURATION: Metric(
132
- name=_JOB_DURATION, documentation="Total seconds the job is running", typ="counter"
133
- ),
134
- _JOB_PRICE: Metric(
135
- name=_JOB_PRICE, documentation="Job instance price, USD/hour", typ="gauge"
136
- ),
137
- _JOB_GPU_COUNT: Metric(name=_JOB_GPU_COUNT, documentation="Job GPU count", typ="gauge"),
138
- }
137
+ job_ids = {job.id for job in jobs}
138
+ job_metrics_points = await _get_job_metrics_points(session, job_ids)
139
+ job_prometheus_metrics = await _get_job_prometheus_metrics(session, job_ids)
140
+
141
+ metrics = _JobMetrics()
139
142
  now = get_current_datetime()
140
143
  for job in jobs:
141
144
  jpd = get_job_provisioning_data(job)
142
145
  if jpd is None:
143
146
  continue
144
147
  jrd = get_job_runtime_data(job)
145
- gpus = jpd.instance_type.resources.gpus
148
+ resources = jpd.instance_type.resources
146
149
  price = jpd.price
147
150
  if jrd is not None and jrd.offer is not None:
148
- gpus = jrd.offer.instance.resources.gpus
151
+ resources = jrd.offer.instance.resources
149
152
  price = jrd.offer.price
153
+ gpus = resources.gpus
154
+ cpus = resources.cpus
150
155
  run_spec = RunSpec.__response__.parse_raw(job.run.run_spec)
151
- labels = _get_job_labels(job)
152
- labels["dstack_run_type"] = run_spec.configuration.type
153
- labels["dstack_backend"] = jpd.get_base_backend().value
154
- labels["dstack_gpu"] = gpus[0].name if gpus else ""
156
+ labels = {
157
+ "dstack_project_name": job.project.name,
158
+ "dstack_user_name": job.run.user.name,
159
+ "dstack_run_name": job.run_name,
160
+ "dstack_run_id": str(job.run_id),
161
+ "dstack_job_name": job.job_name,
162
+ "dstack_job_id": str(job.id),
163
+ "dstack_job_num": str(job.job_num),
164
+ "dstack_replica_num": str(job.replica_num),
165
+ "dstack_run_type": run_spec.configuration.type,
166
+ "dstack_backend": jpd.get_base_backend().value,
167
+ "dstack_gpu": gpus[0].name if gpus else "",
168
+ }
155
169
  duration = (now - job.submitted_at.replace(tzinfo=timezone.utc)).total_seconds()
156
- metrics[_JOB_DURATION].add_sample(name=_JOB_DURATION, labels=labels, value=duration)
157
- metrics[_JOB_PRICE].add_sample(name=_JOB_PRICE, labels=labels, value=price)
158
- metrics[_JOB_GPU_COUNT].add_sample(name=_JOB_GPU_COUNT, labels=labels, value=len(gpus))
170
+ metrics.add_sample(_JOB_DURATION, labels, duration)
171
+ metrics.add_sample(_JOB_PRICE, labels, price)
172
+ metrics.add_sample(_JOB_GPU_COUNT, labels, len(gpus))
173
+ metrics.add_sample(_JOB_CPU_COUNT, labels, cpus)
174
+ metrics.add_sample(_JOB_MEMORY_TOTAL, labels, resources.memory_mib * 1024 * 1024)
175
+ jmp = job_metrics_points.get(job.id)
176
+ if jmp is not None:
177
+ metrics.add_sample(_JOB_CPU_TIME, labels, jmp.cpu_usage_micro / 1_000_000)
178
+ metrics.add_sample(_JOB_MEMORY_USAGE, labels, jmp.memory_usage_bytes)
179
+ metrics.add_sample(_JOB_MEMORY_WORKING_SET, labels, jmp.memory_working_set_bytes)
180
+ jpm = job_prometheus_metrics.get(job.id)
181
+ if jpm is not None:
182
+ for metric in text_string_to_metric_families(jpm.text):
183
+ metrics.add_metric(metric, labels)
159
184
  return metrics.values()
160
185
 
161
186
 
162
- async def get_job_gpu_metrics(session: AsyncSession) -> Iterable[Metric]:
163
- res = await session.execute(
164
- select(JobPrometheusMetrics)
165
- .join(JobModel)
166
- .join(ProjectModel)
167
- .where(JobModel.status.in_([JobStatus.RUNNING]))
168
- .order_by(ProjectModel.name, JobModel.job_name)
169
- .options(
170
- joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project),
171
- joinedload(JobPrometheusMetrics.job)
172
- .joinedload(JobModel.run)
173
- .joinedload(RunModel.user),
174
- )
175
- )
176
- metrics_models = res.scalars().all()
177
- return _parse_and_enrich_job_gpu_metrics(metrics_models)
187
+ _COUNTER = "counter"
188
+ _GAUGE = "gauge"
178
189
 
190
+ _INSTANCE_DURATION = "dstack_instance_duration_seconds_total"
191
+ _INSTANCE_PRICE = "dstack_instance_price_dollars_per_hour"
192
+ _INSTANCE_GPU_COUNT = "dstack_instance_gpu_count"
193
+ _RUN_COUNT_TOTAL = "dstack_run_count_total"
194
+ _RUN_COUNT_TERMINATED = "dstack_run_count_terminated_total"
195
+ _RUN_COUNT_FAILED = "dstack_run_count_failed_total"
196
+ _RUN_COUNT_DONE = "dstack_run_count_done_total"
197
+ _JOB_DURATION = "dstack_job_duration_seconds_total"
198
+ _JOB_PRICE = "dstack_job_price_dollars_per_hour"
199
+ _JOB_GPU_COUNT = "dstack_job_gpu_count"
200
+ _JOB_CPU_COUNT = "dstack_job_cpu_count"
201
+ _JOB_CPU_TIME = "dstack_job_cpu_time_seconds_total"
202
+ _JOB_MEMORY_TOTAL = "dstack_job_memory_total_bytes"
203
+ _JOB_MEMORY_USAGE = "dstack_job_memory_usage_bytes"
204
+ _JOB_MEMORY_WORKING_SET = "dstack_job_memory_working_set_bytes"
179
205
 
180
- async def get_project_metrics(session: AsyncSession, project: ProjectModel) -> str:
181
- res = await session.execute(
182
- select(JobPrometheusMetrics)
183
- .join(JobModel)
184
- .where(
185
- JobModel.project_id == project.id,
186
- JobModel.status.in_([JobStatus.RUNNING]),
206
+
207
+ class _Metrics(dict[str, Metric]):
208
+ metrics: ClassVar[list[tuple[str, str, str]]]
209
+
210
+ def __init__(self):
211
+ super().__init__()
212
+ for name, typ, documentation in self.metrics:
213
+ self[name] = Metric(name=name, documentation=documentation, typ=typ)
214
+
215
+ def add_sample(self, name: str, labels: dict[str, str], value: float) -> None:
216
+ # NOTE: Keeps reference to labels.
217
+ self[name].add_sample(name=name, labels=labels, value=value)
218
+
219
+ def add_metric(self, metric: Metric, labels: dict[str, str]) -> None:
220
+ # NOTE: Modifies and keeps reference to metric.
221
+ name = metric.name
222
+ samples = metric.samples
223
+ stored_metric = self.get(name)
224
+ if stored_metric is None:
225
+ stored_metric = metric
226
+ stored_metric.samples = []
227
+ self[name] = stored_metric
228
+ for sample in samples:
229
+ sample.labels.update(labels)
230
+ # text_string_to_metric_families "fixes" counter names appending _total,
231
+ # we rebuild Sample to revert this
232
+ stored_metric.samples.append(Sample(name, *sample[1:]))
233
+
234
+
235
+ class _InstanceMetrics(_Metrics):
236
+ metrics = [
237
+ (_INSTANCE_DURATION, _COUNTER, "Total seconds the instance is running"),
238
+ (_INSTANCE_PRICE, _GAUGE, "Instance price, USD/hour"),
239
+ (_INSTANCE_GPU_COUNT, _GAUGE, "Instance GPU count"),
240
+ ]
241
+
242
+
243
+ class _RunMetrics(_Metrics):
244
+ metrics = [
245
+ (_RUN_COUNT_TOTAL, _COUNTER, "Total runs count"),
246
+ (_RUN_COUNT_TERMINATED, _COUNTER, "Terminated runs count"),
247
+ (_RUN_COUNT_FAILED, _COUNTER, "Failed runs count"),
248
+ (_RUN_COUNT_DONE, _COUNTER, "Done runs count"),
249
+ ]
250
+
251
+
252
+ class _JobMetrics(_Metrics):
253
+ metrics = [
254
+ (_JOB_DURATION, _COUNTER, "Total seconds the job is running"),
255
+ (_JOB_PRICE, _GAUGE, "Job instance price, USD/hour"),
256
+ (_JOB_GPU_COUNT, _GAUGE, "Job GPU count"),
257
+ (_JOB_CPU_COUNT, _GAUGE, "Job CPU count"),
258
+ (_JOB_CPU_TIME, _COUNTER, "Total CPU time consumed by the job, seconds"),
259
+ (_JOB_MEMORY_TOTAL, _GAUGE, "Total memory allocated for the job, bytes"),
260
+ (_JOB_MEMORY_USAGE, _GAUGE, "Memory used by the job (including cache), bytes"),
261
+ (_JOB_MEMORY_WORKING_SET, _GAUGE, "Memory used by the job (not including cache), bytes"),
262
+ ]
263
+
264
+
265
+ async def _get_job_metrics_points(
266
+ session: AsyncSession, job_ids: Iterable[UUID]
267
+ ) -> dict[UUID, JobMetricsPoint]:
268
+ subquery = select(
269
+ JobMetricsPoint,
270
+ func.row_number()
271
+ .over(
272
+ partition_by=JobMetricsPoint.job_id,
273
+ order_by=JobMetricsPoint.timestamp_micro.desc(),
187
274
  )
188
- .order_by(JobModel.job_name)
189
- .options(
190
- joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project),
191
- joinedload(JobPrometheusMetrics.job)
192
- .joinedload(JobModel.run)
193
- .joinedload(RunModel.user),
275
+ .label("row_number"),
276
+ ).subquery()
277
+ res = await session.execute(
278
+ select(aliased(JobMetricsPoint, subquery)).where(
279
+ subquery.c.row_number == 1,
280
+ subquery.c.job_id.in_(job_ids),
194
281
  )
195
282
  )
196
- metrics_models = res.scalars().all()
197
- return "\n".join(_render_metrics(_parse_and_enrich_job_gpu_metrics(metrics_models))) + "\n"
198
-
199
-
200
- def _parse_and_enrich_job_gpu_metrics(
201
- metrics_models: Iterable[JobPrometheusMetrics],
202
- ) -> Iterable[Metric]:
203
- metrics: dict[str, Metric] = {}
204
- for metrics_model in metrics_models:
205
- for metric in text_string_to_metric_families(metrics_model.text):
206
- samples = metric.samples
207
- metric.samples = []
208
- name = metric.name
209
- metric = metrics.setdefault(name, metric)
210
- for sample in samples:
211
- labels = sample.labels
212
- labels.update(_get_job_labels(metrics_model.job))
213
- # text_string_to_metric_families "fixes" counter names appending _total,
214
- # we rebuild Sample to revert this
215
- metric.samples.append(Sample(name, labels, *sample[2:]))
216
- return metrics.values()
283
+ return {p.job_id: p for p in res.scalars().all()}
217
284
 
218
285
 
219
- def _get_job_labels(job: JobModel) -> dict[str, str]:
220
- return {
221
- "dstack_project_name": job.project.name,
222
- "dstack_user_name": job.run.user.name,
223
- "dstack_run_name": job.run_name,
224
- "dstack_run_id": str(job.run_id),
225
- "dstack_job_name": job.job_name,
226
- "dstack_job_id": str(job.id),
227
- "dstack_job_num": str(job.job_num),
228
- "dstack_replica_num": str(job.replica_num),
229
- }
286
+ async def _get_job_prometheus_metrics(
287
+ session: AsyncSession, job_ids: Iterable[UUID]
288
+ ) -> dict[UUID, JobPrometheusMetrics]:
289
+ res = await session.execute(
290
+ select(JobPrometheusMetrics).where(JobPrometheusMetrics.job_id.in_(job_ids))
291
+ )
292
+ return {p.job_id: p for p in res.scalars().all()}
230
293
 
231
294
 
232
295
  def _render_metrics(metrics: Iterable[Metric]) -> Generator[str, None, 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-4fd5a4770eff59325ee3.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-bcb3228138bc8483cc0b.js"></script><link href="/main-c0bdaac8f1ea67d499eb.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>