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

@@ -155,8 +155,20 @@ class RiftClient:
155
155
  logger.debug("Terminating instance with request data: %s", request_data)
156
156
  response_data = self._make_request("instances/terminate", request_data)
157
157
  if isinstance(response_data, dict):
158
+ logger.debug("Terminating instance with response: %s", response_data)
158
159
  info = response_data.get("terminated", [])
159
- return len(info) > 0
160
+ is_terminated = len(info) > 0
161
+ if not is_terminated:
162
+ # check if the instance is already terminated
163
+ instance_info = self.get_instance_by_id(instance_id)
164
+ is_terminated = instance_info is None or instance_info.get("status") == "Inactive"
165
+ logger.debug(
166
+ "Instance %s is already terminated: %s response: %s",
167
+ instance_id,
168
+ is_terminated,
169
+ instance_info,
170
+ )
171
+ return is_terminated
160
172
 
161
173
  return False
162
174
 
@@ -26,7 +26,7 @@ from dstack import version
26
26
  from dstack._internal.core.backends.oci.region import OCIRegionClient
27
27
  from dstack._internal.core.errors import BackendError
28
28
  from dstack._internal.core.models.instances import InstanceOffer
29
- from dstack._internal.utils.common import split_chunks
29
+ from dstack._internal.utils.common import batched
30
30
  from dstack._internal.utils.logging import get_logger
31
31
 
32
32
  logger = get_logger(__name__)
@@ -667,21 +667,21 @@ def add_security_group_rules(
667
667
  security_group_id: str, rules: Iterable[SecurityRule], client: oci.core.VirtualNetworkClient
668
668
  ) -> None:
669
669
  rules_details = map(SecurityRule.to_sdk_add_rule_details, rules)
670
- for chunk in split_chunks(rules_details, ADD_SECURITY_RULES_MAX_CHUNK_SIZE):
670
+ for batch in batched(rules_details, ADD_SECURITY_RULES_MAX_CHUNK_SIZE):
671
671
  client.add_network_security_group_security_rules(
672
672
  security_group_id,
673
- oci.core.models.AddNetworkSecurityGroupSecurityRulesDetails(security_rules=chunk),
673
+ oci.core.models.AddNetworkSecurityGroupSecurityRulesDetails(security_rules=batch),
674
674
  )
675
675
 
676
676
 
677
677
  def remove_security_group_rules(
678
678
  security_group_id: str, rule_ids: Iterable[str], client: oci.core.VirtualNetworkClient
679
679
  ) -> None:
680
- for chunk in split_chunks(rule_ids, REMOVE_SECURITY_RULES_MAX_CHUNK_SIZE):
680
+ for batch in batched(rule_ids, REMOVE_SECURITY_RULES_MAX_CHUNK_SIZE):
681
681
  client.remove_network_security_group_security_rules(
682
682
  security_group_id,
683
683
  oci.core.models.RemoveNetworkSecurityGroupSecurityRulesDetails(
684
- security_rule_ids=chunk
684
+ security_rule_ids=batch
685
685
  ),
686
686
  )
687
687
 
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
  from typing import Optional
3
3
 
4
- from pydantic import UUID4, Field, validator
4
+ from pydantic import UUID4, Field
5
5
 
6
6
  from dstack._internal.core.models.common import CoreModel
7
7
 
@@ -15,11 +15,3 @@ class PollLogsRequest(CoreModel):
15
15
  next_token: Optional[str] = None
16
16
  limit: int = Field(100, ge=0, le=1000)
17
17
  diagnose: bool = False
18
-
19
- @validator("descending")
20
- @classmethod
21
- def validate_descending(cls, v):
22
- # Descending is not supported until we migrate from base64-encoded logs to plain text logs.
23
- if v is True:
24
- raise ValueError("descending: true is not supported")
25
- return v
@@ -55,6 +55,8 @@ class CloudWatchLogStorage(LogStorage):
55
55
  PAST_EVENT_MAX_DELTA = int((timedelta(days=14)).total_seconds()) * 1000 - CLOCK_DRIFT
56
56
  # "None of the log events in the batch can be more than 2 hours in the future."
57
57
  FUTURE_EVENT_MAX_DELTA = int((timedelta(hours=2)).total_seconds()) * 1000 - CLOCK_DRIFT
58
+ # Maximum number of retries when polling for log events to skip empty pages.
59
+ MAX_RETRIES = 10
58
60
 
59
61
  def __init__(self, *, group: str, region: Optional[str] = None) -> None:
60
62
  with self._wrap_boto_errors():
@@ -80,7 +82,7 @@ class CloudWatchLogStorage(LogStorage):
80
82
  next_token: Optional[str] = None
81
83
  with self._wrap_boto_errors():
82
84
  try:
83
- cw_events, next_token = self._get_log_events(stream, request)
85
+ cw_events, next_token = self._get_log_events_with_retry(stream, request)
84
86
  except botocore.exceptions.ClientError as e:
85
87
  if not self._is_resource_not_found_exception(e):
86
88
  raise
@@ -101,7 +103,47 @@ class CloudWatchLogStorage(LogStorage):
101
103
  )
102
104
  for cw_event in cw_events
103
105
  ]
104
- return JobSubmissionLogs(logs=logs, next_token=next_token if len(logs) > 0 else None)
106
+ return JobSubmissionLogs(logs=logs, next_token=next_token)
107
+
108
+ def _get_log_events_with_retry(
109
+ self, stream: str, request: PollLogsRequest
110
+ ) -> Tuple[List[_CloudWatchLogEvent], Optional[str]]:
111
+ current_request = request
112
+ previous_next_token = request.next_token
113
+
114
+ for attempt in range(self.MAX_RETRIES):
115
+ cw_events, next_token = self._get_log_events(stream, current_request)
116
+
117
+ if cw_events:
118
+ return cw_events, next_token
119
+
120
+ if not next_token or next_token == previous_next_token:
121
+ return [], None
122
+
123
+ previous_next_token = next_token
124
+ current_request = PollLogsRequest(
125
+ run_name=request.run_name,
126
+ job_submission_id=request.job_submission_id,
127
+ start_time=request.start_time,
128
+ end_time=request.end_time,
129
+ descending=request.descending,
130
+ next_token=next_token,
131
+ limit=request.limit,
132
+ diagnose=request.diagnose,
133
+ )
134
+
135
+ if not request.descending:
136
+ logger.debug(
137
+ "Stream %s: exhausted %d retries without finding logs, returning empty response",
138
+ stream,
139
+ self.MAX_RETRIES,
140
+ )
141
+ # Only return the next token after exhausting retries if going descending—
142
+ # AWS CloudWatch guarantees more logs in that case. In ascending mode,
143
+ # next token is always returned, even if no logs remain.
144
+ # So descending works reliably; ascending has limits if gaps are too large.
145
+ # In the future, UI/CLI should handle retries, and we can return next token for ascending too.
146
+ return [], next_token if request.descending else None
105
147
 
106
148
  def _get_log_events(
107
149
  self, stream: str, request: PollLogsRequest
@@ -115,7 +157,7 @@ class CloudWatchLogStorage(LogStorage):
115
157
  }
116
158
 
117
159
  if request.start_time:
118
- parameters["startTime"] = datetime_to_unix_time_ms(request.start_time) + 1
160
+ parameters["startTime"] = datetime_to_unix_time_ms(request.start_time)
119
161
 
120
162
  if request.end_time:
121
163
  parameters["endTime"] = datetime_to_unix_time_ms(request.end_time)
@@ -1,5 +1,6 @@
1
+ import os
1
2
  from pathlib import Path
2
- from typing import List, Union
3
+ from typing import Generator, List, Optional, Tuple, Union
3
4
  from uuid import UUID
4
5
 
5
6
  from dstack._internal.core.errors import ServerClientError
@@ -37,18 +38,17 @@ class FileLogStorage(LogStorage):
37
38
  producer=log_producer,
38
39
  )
39
40
 
41
+ if request.descending:
42
+ return self._poll_logs_descending(log_file_path, request)
43
+ else:
44
+ return self._poll_logs_ascending(log_file_path, request)
45
+
46
+ def _poll_logs_ascending(
47
+ self, log_file_path: Path, request: PollLogsRequest
48
+ ) -> JobSubmissionLogs:
40
49
  start_line = 0
41
50
  if request.next_token:
42
- try:
43
- start_line = int(request.next_token)
44
- if start_line < 0:
45
- raise ServerClientError(
46
- f"Invalid next_token: {request.next_token}. Must be a non-negative integer."
47
- )
48
- except ValueError:
49
- raise ServerClientError(
50
- f"Invalid next_token: {request.next_token}. Must be a valid integer."
51
- )
51
+ start_line = self._next_token(request)
52
52
 
53
53
  logs = []
54
54
  next_token = None
@@ -94,6 +94,102 @@ class FileLogStorage(LogStorage):
94
94
 
95
95
  return JobSubmissionLogs(logs=logs, next_token=next_token)
96
96
 
97
+ def _poll_logs_descending(
98
+ self, log_file_path: Path, request: PollLogsRequest
99
+ ) -> JobSubmissionLogs:
100
+ start_offset = self._next_token(request)
101
+
102
+ candidate_logs = []
103
+
104
+ try:
105
+ line_generator = self._read_lines_reversed(log_file_path, start_offset)
106
+
107
+ for line_bytes, line_start_offset in line_generator:
108
+ try:
109
+ line_str = line_bytes.decode("utf-8")
110
+ log_event = LogEvent.__response__.parse_raw(line_str)
111
+ except Exception:
112
+ continue # Skip malformed lines
113
+
114
+ if request.end_time is not None and log_event.timestamp > request.end_time:
115
+ continue
116
+ if request.start_time and log_event.timestamp <= request.start_time:
117
+ break
118
+
119
+ candidate_logs.append((log_event, line_start_offset))
120
+
121
+ if len(candidate_logs) > request.limit:
122
+ break
123
+ except FileNotFoundError:
124
+ return JobSubmissionLogs(logs=[], next_token=None)
125
+
126
+ logs = [log for log, offset in candidate_logs[: request.limit]]
127
+ next_token = None
128
+ if len(candidate_logs) > request.limit:
129
+ # We fetched one more than the limit, so there are more pages.
130
+ # The next token should point to the start of the last log we are returning.
131
+ _last_log_event, last_log_offset = candidate_logs[request.limit - 1]
132
+ next_token = str(last_log_offset)
133
+
134
+ return JobSubmissionLogs(logs=logs, next_token=next_token)
135
+
136
+ @staticmethod
137
+ def _read_lines_reversed(
138
+ filepath: Path, start_offset: Optional[int] = None, chunk_size: int = 8192
139
+ ) -> Generator[Tuple[bytes, int], None, None]:
140
+ """
141
+ A generator that yields lines from a file in reverse order, along with the byte
142
+ offset of the start of each line. This is memory-efficient for large files.
143
+ """
144
+ with open(filepath, "rb") as f:
145
+ f.seek(0, os.SEEK_END)
146
+ file_size = f.tell()
147
+ cursor = file_size
148
+
149
+ # If a start_offset is provided, optimize by starting the read
150
+ # from a more specific location instead of the end of the file.
151
+ if start_offset is not None and start_offset < file_size:
152
+ # To get the full content of the line that straddles the offset,
153
+ # we need to find its end (the next newline character).
154
+ f.seek(start_offset)
155
+ chunk = f.read(chunk_size)
156
+ newline_pos = chunk.find(b"\n")
157
+ if newline_pos != -1:
158
+ # Found the end of the line. The cursor for reverse reading
159
+ # should start from this point to include the full line.
160
+ cursor = start_offset + newline_pos + 1
161
+ else:
162
+ # No newline found, which means the rest of the file is one line.
163
+ # The default cursor pointing to file_size is correct.
164
+ pass
165
+
166
+ buffer = b""
167
+
168
+ while cursor > 0:
169
+ seek_pos = max(0, cursor - chunk_size)
170
+ amount_to_read = cursor - seek_pos
171
+ f.seek(seek_pos)
172
+ chunk = f.read(amount_to_read)
173
+ cursor = seek_pos
174
+
175
+ buffer = chunk + buffer
176
+
177
+ while b"\n" in buffer:
178
+ newline_pos = buffer.rfind(b"\n")
179
+ line = buffer[newline_pos + 1 :]
180
+ line_start_offset = cursor + newline_pos + 1
181
+
182
+ # Skip lines that start at or after the start_offset
183
+ if start_offset is None or line_start_offset < start_offset:
184
+ yield line, line_start_offset
185
+
186
+ buffer = buffer[:newline_pos]
187
+
188
+ # The remaining buffer is the first line of the file.
189
+ # Only yield it if we're not using start_offset or if it starts before start_offset
190
+ if buffer and (start_offset is None or 0 < start_offset):
191
+ yield buffer, 0
192
+
97
193
  def write_logs(
98
194
  self,
99
195
  project: ProjectModel,
@@ -148,3 +244,17 @@ class FileLogStorage(LogStorage):
148
244
  log_source=LogEventSource.STDOUT,
149
245
  message=runner_log_event.message.decode(errors="replace"),
150
246
  )
247
+
248
+ def _next_token(self, request: PollLogsRequest) -> Optional[int]:
249
+ next_token = request.next_token
250
+ if next_token is None:
251
+ return None
252
+ try:
253
+ value = int(next_token)
254
+ if value < 0:
255
+ raise ValueError("Offset must be non-negative")
256
+ return value
257
+ except (ValueError, TypeError):
258
+ raise ServerClientError(
259
+ f"Invalid next_token: {next_token}. Must be a non-negative integer."
260
+ )
@@ -1,4 +1,5 @@
1
1
  import itertools
2
+ import json
2
3
  from collections import defaultdict
3
4
  from collections.abc import Generator, Iterable
4
5
  from datetime import timezone
@@ -177,6 +178,19 @@ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
177
178
  metrics.add_sample(_JOB_CPU_TIME, labels, jmp.cpu_usage_micro / 1_000_000)
178
179
  metrics.add_sample(_JOB_MEMORY_USAGE, labels, jmp.memory_usage_bytes)
179
180
  metrics.add_sample(_JOB_MEMORY_WORKING_SET, labels, jmp.memory_working_set_bytes)
181
+ if gpus:
182
+ gpu_memory_total = gpus[0].memory_mib * 1024 * 1024
183
+ for gpu_num, (gpu_util, gpu_memory_usage) in enumerate(
184
+ zip(
185
+ json.loads(jmp.gpus_util_percent),
186
+ json.loads(jmp.gpus_memory_usage_bytes),
187
+ )
188
+ ):
189
+ gpu_labels = labels.copy()
190
+ gpu_labels["dstack_gpu_num"] = gpu_num
191
+ metrics.add_sample(_JOB_GPU_USAGE_RATIO, gpu_labels, gpu_util / 100)
192
+ metrics.add_sample(_JOB_GPU_MEMORY_TOTAL, gpu_labels, gpu_memory_total)
193
+ metrics.add_sample(_JOB_GPU_MEMORY_USAGE, gpu_labels, gpu_memory_usage)
180
194
  jpm = job_prometheus_metrics.get(job.id)
181
195
  if jpm is not None:
182
196
  for metric in text_string_to_metric_families(jpm.text):
@@ -202,6 +216,9 @@ _JOB_CPU_TIME = "dstack_job_cpu_time_seconds_total"
202
216
  _JOB_MEMORY_TOTAL = "dstack_job_memory_total_bytes"
203
217
  _JOB_MEMORY_USAGE = "dstack_job_memory_usage_bytes"
204
218
  _JOB_MEMORY_WORKING_SET = "dstack_job_memory_working_set_bytes"
219
+ _JOB_GPU_USAGE_RATIO = "dstack_job_gpu_usage_ratio"
220
+ _JOB_GPU_MEMORY_TOTAL = "dstack_job_gpu_memory_total_bytes"
221
+ _JOB_GPU_MEMORY_USAGE = "dstack_job_gpu_memory_usage_bytes"
205
222
 
206
223
 
207
224
  class _Metrics(dict[str, Metric]):
@@ -259,6 +276,9 @@ class _JobMetrics(_Metrics):
259
276
  (_JOB_MEMORY_TOTAL, _GAUGE, "Total memory allocated for the job, bytes"),
260
277
  (_JOB_MEMORY_USAGE, _GAUGE, "Memory used by the job (including cache), bytes"),
261
278
  (_JOB_MEMORY_WORKING_SET, _GAUGE, "Memory used by the job (not including cache), bytes"),
279
+ (_JOB_GPU_USAGE_RATIO, _GAUGE, "Job GPU usage, percent (as 0.0-1.0)"),
280
+ (_JOB_GPU_MEMORY_TOTAL, _GAUGE, "Total GPU memory allocated for the job, bytes"),
281
+ (_JOB_GPU_MEMORY_USAGE, _GAUGE, "GPU memory used by the job, bytes"),
262
282
  ]
263
283
 
264
284
 
@@ -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-64f8273740c4b52c18f5.js"></script><link href="/main-d58fc0460cb0eae7cb5c.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-39a767528976f8078166.js"></script><link href="/main-8f9ee218d3eb45989682.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>