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.
- dstack/_internal/core/backends/cloudrift/api_client.py +13 -1
- dstack/_internal/core/backends/oci/resources.py +5 -5
- dstack/_internal/server/schemas/logs.py +1 -9
- dstack/_internal/server/services/logs/aws.py +45 -3
- dstack/_internal/server/services/logs/filelog.py +121 -11
- dstack/_internal/server/services/prometheus/custom_metrics.py +20 -0
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-64f8273740c4b52c18f5.js → main-39a767528976f8078166.js} +7 -26
- dstack/_internal/server/statics/{main-64f8273740c4b52c18f5.js.map → main-39a767528976f8078166.js.map} +1 -1
- dstack/_internal/server/statics/{main-d58fc0460cb0eae7cb5c.css → main-8f9ee218d3eb45989682.css} +2 -2
- dstack/_internal/utils/common.py +10 -21
- dstack/version.py +1 -1
- {dstack-0.19.19.dist-info → dstack-0.19.20.dist-info}/METADATA +1 -1
- {dstack-0.19.19.dist-info → dstack-0.19.20.dist-info}/RECORD +17 -17
- {dstack-0.19.19.dist-info → dstack-0.19.20.dist-info}/WHEEL +0 -0
- {dstack-0.19.19.dist-info → dstack-0.19.20.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.19.dist-info → dstack-0.19.20.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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=
|
|
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
|
|
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.
|
|
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
|
|
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)
|
|
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
|
-
|
|
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-
|
|
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>
|