dstack 0.18.44__py3-none-any.whl → 0.19.0__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 (267) hide show
  1. dstack/_internal/cli/commands/gateway.py +15 -3
  2. dstack/_internal/cli/commands/logs.py +0 -22
  3. dstack/_internal/cli/commands/stats.py +8 -17
  4. dstack/_internal/cli/main.py +1 -5
  5. dstack/_internal/cli/services/configurators/fleet.py +4 -39
  6. dstack/_internal/cli/services/configurators/run.py +22 -21
  7. dstack/_internal/cli/services/profile.py +34 -83
  8. dstack/_internal/cli/utils/gateway.py +1 -1
  9. dstack/_internal/core/backends/__init__.py +56 -39
  10. dstack/_internal/core/backends/aws/__init__.py +0 -25
  11. dstack/_internal/core/backends/aws/auth.py +1 -10
  12. dstack/_internal/core/backends/aws/backend.py +26 -0
  13. dstack/_internal/core/backends/aws/compute.py +20 -45
  14. dstack/_internal/{server/services/backends/configurators/aws.py → core/backends/aws/configurator.py} +46 -85
  15. dstack/_internal/core/backends/aws/models.py +135 -0
  16. dstack/_internal/core/backends/aws/resources.py +1 -1
  17. dstack/_internal/core/backends/azure/__init__.py +0 -20
  18. dstack/_internal/core/backends/azure/auth.py +2 -11
  19. dstack/_internal/core/backends/azure/backend.py +21 -0
  20. dstack/_internal/core/backends/azure/compute.py +13 -27
  21. dstack/_internal/{server/services/backends/configurators/azure.py → core/backends/azure/configurator.py} +141 -210
  22. dstack/_internal/core/backends/azure/models.py +89 -0
  23. dstack/_internal/core/backends/base/__init__.py +0 -12
  24. dstack/_internal/core/backends/base/backend.py +18 -0
  25. dstack/_internal/core/backends/base/compute.py +153 -33
  26. dstack/_internal/core/backends/base/configurator.py +105 -0
  27. dstack/_internal/core/backends/base/models.py +14 -0
  28. dstack/_internal/core/backends/configurators.py +138 -0
  29. dstack/_internal/core/backends/cudo/__init__.py +0 -15
  30. dstack/_internal/core/backends/cudo/backend.py +16 -0
  31. dstack/_internal/core/backends/cudo/compute.py +8 -26
  32. dstack/_internal/core/backends/cudo/configurator.py +72 -0
  33. dstack/_internal/core/backends/cudo/models.py +37 -0
  34. dstack/_internal/core/backends/datacrunch/__init__.py +0 -15
  35. dstack/_internal/core/backends/datacrunch/backend.py +16 -0
  36. dstack/_internal/core/backends/datacrunch/compute.py +8 -25
  37. dstack/_internal/core/backends/datacrunch/configurator.py +66 -0
  38. dstack/_internal/core/backends/datacrunch/models.py +38 -0
  39. dstack/_internal/core/{models/backends/dstack.py → backends/dstack/models.py} +7 -7
  40. dstack/_internal/core/backends/gcp/__init__.py +0 -16
  41. dstack/_internal/core/backends/gcp/auth.py +2 -11
  42. dstack/_internal/core/backends/gcp/backend.py +17 -0
  43. dstack/_internal/core/backends/gcp/compute.py +13 -43
  44. dstack/_internal/{server/services/backends/configurators/gcp.py → core/backends/gcp/configurator.py} +46 -103
  45. dstack/_internal/core/backends/gcp/models.py +125 -0
  46. dstack/_internal/core/backends/kubernetes/__init__.py +0 -15
  47. dstack/_internal/core/backends/kubernetes/backend.py +16 -0
  48. dstack/_internal/core/backends/kubernetes/compute.py +16 -5
  49. dstack/_internal/core/backends/kubernetes/configurator.py +55 -0
  50. dstack/_internal/core/backends/kubernetes/models.py +72 -0
  51. dstack/_internal/core/backends/lambdalabs/__init__.py +0 -16
  52. dstack/_internal/core/backends/lambdalabs/backend.py +17 -0
  53. dstack/_internal/core/backends/lambdalabs/compute.py +7 -28
  54. dstack/_internal/core/backends/lambdalabs/configurator.py +82 -0
  55. dstack/_internal/core/backends/lambdalabs/models.py +37 -0
  56. dstack/_internal/core/backends/local/__init__.py +0 -13
  57. dstack/_internal/core/backends/local/backend.py +14 -0
  58. dstack/_internal/core/backends/local/compute.py +16 -2
  59. dstack/_internal/core/backends/models.py +128 -0
  60. dstack/_internal/core/backends/oci/__init__.py +0 -15
  61. dstack/_internal/core/backends/oci/auth.py +1 -5
  62. dstack/_internal/core/backends/oci/backend.py +16 -0
  63. dstack/_internal/core/backends/oci/compute.py +9 -23
  64. dstack/_internal/{server/services/backends/configurators/oci.py → core/backends/oci/configurator.py} +40 -85
  65. dstack/_internal/core/{models/backends/oci.py → backends/oci/models.py} +24 -25
  66. dstack/_internal/core/backends/oci/region.py +1 -1
  67. dstack/_internal/core/backends/runpod/__init__.py +0 -15
  68. dstack/_internal/core/backends/runpod/backend.py +16 -0
  69. dstack/_internal/core/backends/runpod/compute.py +7 -3
  70. dstack/_internal/core/backends/runpod/configurator.py +59 -0
  71. dstack/_internal/core/backends/runpod/models.py +54 -0
  72. dstack/_internal/core/backends/template/__init__.py +0 -0
  73. dstack/_internal/core/backends/tensordock/__init__.py +0 -15
  74. dstack/_internal/core/backends/tensordock/backend.py +16 -0
  75. dstack/_internal/core/backends/tensordock/compute.py +8 -27
  76. dstack/_internal/core/backends/tensordock/configurator.py +68 -0
  77. dstack/_internal/core/backends/tensordock/models.py +38 -0
  78. dstack/_internal/core/backends/vastai/__init__.py +0 -15
  79. dstack/_internal/core/backends/vastai/backend.py +16 -0
  80. dstack/_internal/core/backends/vastai/compute.py +2 -2
  81. dstack/_internal/core/backends/vastai/configurator.py +66 -0
  82. dstack/_internal/core/backends/vastai/models.py +37 -0
  83. dstack/_internal/core/backends/vultr/__init__.py +0 -15
  84. dstack/_internal/core/backends/vultr/backend.py +16 -0
  85. dstack/_internal/core/backends/vultr/compute.py +10 -24
  86. dstack/_internal/core/backends/vultr/configurator.py +64 -0
  87. dstack/_internal/core/backends/vultr/models.py +34 -0
  88. dstack/_internal/core/models/backends/__init__.py +0 -184
  89. dstack/_internal/core/models/backends/base.py +0 -19
  90. dstack/_internal/core/models/configurations.py +20 -15
  91. dstack/_internal/core/models/envs.py +4 -3
  92. dstack/_internal/core/models/fleets.py +17 -22
  93. dstack/_internal/core/models/gateways.py +3 -3
  94. dstack/_internal/core/models/instances.py +24 -0
  95. dstack/_internal/core/models/profiles.py +41 -46
  96. dstack/_internal/core/models/projects.py +1 -1
  97. dstack/_internal/core/models/repos/base.py +0 -5
  98. dstack/_internal/core/models/repos/local.py +3 -3
  99. dstack/_internal/core/models/repos/remote.py +26 -12
  100. dstack/_internal/core/models/repos/virtual.py +1 -1
  101. dstack/_internal/core/models/resources.py +45 -76
  102. dstack/_internal/core/models/runs.py +17 -19
  103. dstack/_internal/core/models/volumes.py +1 -3
  104. dstack/_internal/core/services/profiles.py +7 -16
  105. dstack/_internal/core/services/repos.py +0 -4
  106. dstack/_internal/server/app.py +0 -3
  107. dstack/_internal/server/background/tasks/process_gateways.py +4 -8
  108. dstack/_internal/server/background/tasks/process_instances.py +14 -9
  109. dstack/_internal/server/background/tasks/process_metrics.py +1 -1
  110. dstack/_internal/server/background/tasks/process_placement_groups.py +4 -1
  111. dstack/_internal/server/background/tasks/process_prometheus_metrics.py +1 -1
  112. dstack/_internal/server/background/tasks/process_running_jobs.py +14 -5
  113. dstack/_internal/server/background/tasks/process_submitted_jobs.py +16 -37
  114. dstack/_internal/server/background/tasks/process_volumes.py +5 -2
  115. dstack/_internal/server/migrations/versions/7bc2586e8b9e_make_instancemodel_pool_id_optional.py +36 -0
  116. dstack/_internal/server/migrations/versions/bc8ca4a505c6_store_backendtype_as_string.py +171 -0
  117. dstack/_internal/server/models.py +48 -9
  118. dstack/_internal/server/routers/backends.py +14 -23
  119. dstack/_internal/server/routers/instances.py +3 -4
  120. dstack/_internal/server/routers/metrics.py +10 -8
  121. dstack/_internal/server/routers/prometheus.py +1 -1
  122. dstack/_internal/server/routers/repos.py +1 -2
  123. dstack/_internal/server/routers/runs.py +13 -59
  124. dstack/_internal/server/schemas/gateways.py +14 -23
  125. dstack/_internal/server/schemas/projects.py +7 -2
  126. dstack/_internal/server/schemas/repos.py +2 -38
  127. dstack/_internal/server/schemas/runner.py +1 -0
  128. dstack/_internal/server/schemas/runs.py +1 -24
  129. dstack/_internal/server/services/backends/__init__.py +85 -158
  130. dstack/_internal/server/services/config.py +52 -576
  131. dstack/_internal/server/services/fleets.py +8 -103
  132. dstack/_internal/server/services/gateways/__init__.py +12 -4
  133. dstack/_internal/server/services/{pools.py → instances.py} +22 -329
  134. dstack/_internal/server/services/jobs/__init__.py +9 -6
  135. dstack/_internal/server/services/jobs/configurators/base.py +16 -0
  136. dstack/_internal/server/services/jobs/configurators/dev.py +9 -1
  137. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +42 -0
  138. dstack/_internal/server/services/metrics.py +39 -13
  139. dstack/_internal/server/services/offers.py +1 -1
  140. dstack/_internal/server/services/projects.py +23 -14
  141. dstack/_internal/server/services/prometheus.py +176 -18
  142. dstack/_internal/server/services/runs.py +24 -16
  143. dstack/_internal/server/services/volumes.py +8 -4
  144. dstack/_internal/server/statics/index.html +1 -1
  145. dstack/_internal/server/statics/{main-4eb116b97819badd1e2c.js → main-4a0fe83e84574654e397.js} +18 -14
  146. dstack/_internal/server/statics/{main-4eb116b97819badd1e2c.js.map → main-4a0fe83e84574654e397.js.map} +1 -1
  147. dstack/_internal/server/testing/common.py +58 -32
  148. dstack/_internal/utils/json_schema.py +6 -0
  149. dstack/_internal/utils/ssh.py +2 -1
  150. dstack/api/__init__.py +4 -0
  151. dstack/api/_public/__init__.py +16 -20
  152. dstack/api/_public/backends.py +1 -1
  153. dstack/api/_public/repos.py +36 -36
  154. dstack/api/_public/runs.py +167 -83
  155. dstack/api/server/__init__.py +11 -13
  156. dstack/api/server/_backends.py +12 -16
  157. dstack/api/server/_fleets.py +15 -57
  158. dstack/api/server/_gateways.py +3 -14
  159. dstack/api/server/_repos.py +1 -4
  160. dstack/api/server/_runs.py +21 -100
  161. dstack/api/server/_volumes.py +10 -5
  162. dstack/version.py +1 -1
  163. {dstack-0.18.44.dist-info → dstack-0.19.0.dist-info}/METADATA +1 -1
  164. {dstack-0.18.44.dist-info → dstack-0.19.0.dist-info}/RECORD +218 -204
  165. tests/_internal/cli/services/configurators/test_profile.py +6 -6
  166. tests/_internal/core/backends/aws/test_configurator.py +35 -0
  167. tests/_internal/core/backends/aws/test_resources.py +1 -1
  168. tests/_internal/core/backends/azure/test_configurator.py +61 -0
  169. tests/_internal/core/backends/cudo/__init__.py +0 -0
  170. tests/_internal/core/backends/cudo/test_configurator.py +37 -0
  171. tests/_internal/core/backends/datacrunch/__init__.py +0 -0
  172. tests/_internal/core/backends/datacrunch/test_configurator.py +17 -0
  173. tests/_internal/core/backends/gcp/test_configurator.py +42 -0
  174. tests/_internal/core/backends/kubernetes/test_configurator.py +43 -0
  175. tests/_internal/core/backends/lambdalabs/__init__.py +0 -0
  176. tests/_internal/core/backends/lambdalabs/test_configurator.py +38 -0
  177. tests/_internal/core/backends/oci/test_configurator.py +55 -0
  178. tests/_internal/core/backends/runpod/__init__.py +0 -0
  179. tests/_internal/core/backends/runpod/test_configurator.py +33 -0
  180. tests/_internal/core/backends/tensordock/__init__.py +0 -0
  181. tests/_internal/core/backends/tensordock/test_configurator.py +38 -0
  182. tests/_internal/core/backends/vastai/__init__.py +0 -0
  183. tests/_internal/core/backends/vastai/test_configurator.py +33 -0
  184. tests/_internal/core/backends/vultr/__init__.py +0 -0
  185. tests/_internal/core/backends/vultr/test_configurator.py +33 -0
  186. tests/_internal/server/background/tasks/test_process_gateways.py +4 -0
  187. tests/_internal/server/background/tasks/test_process_instances.py +49 -48
  188. tests/_internal/server/background/tasks/test_process_metrics.py +0 -3
  189. tests/_internal/server/background/tasks/test_process_placement_groups.py +2 -0
  190. tests/_internal/server/background/tasks/test_process_prometheus_metrics.py +0 -3
  191. tests/_internal/server/background/tasks/test_process_running_jobs.py +0 -21
  192. tests/_internal/server/background/tasks/test_process_runs.py +8 -22
  193. tests/_internal/server/background/tasks/test_process_submitted_jobs.py +3 -40
  194. tests/_internal/server/background/tasks/test_process_submitted_volumes.py +2 -0
  195. tests/_internal/server/background/tasks/test_process_terminating_jobs.py +10 -15
  196. tests/_internal/server/routers/test_backends.py +6 -764
  197. tests/_internal/server/routers/test_fleets.py +0 -26
  198. tests/_internal/server/routers/test_gateways.py +27 -3
  199. tests/_internal/server/routers/test_instances.py +0 -10
  200. tests/_internal/server/routers/test_metrics.py +27 -0
  201. tests/_internal/server/routers/test_projects.py +56 -0
  202. tests/_internal/server/routers/test_prometheus.py +116 -27
  203. tests/_internal/server/routers/test_repos.py +0 -15
  204. tests/_internal/server/routers/test_runs.py +4 -219
  205. tests/_internal/server/routers/test_volumes.py +2 -3
  206. tests/_internal/server/services/backends/__init__.py +0 -0
  207. tests/_internal/server/services/jobs/configurators/test_task.py +35 -0
  208. tests/_internal/server/services/test_config.py +7 -4
  209. tests/_internal/server/services/test_fleets.py +1 -4
  210. tests/_internal/server/services/{test_pools.py → test_instances.py} +11 -49
  211. tests/_internal/server/services/test_metrics.py +9 -5
  212. tests/_internal/server/services/test_repos.py +1 -14
  213. tests/_internal/server/services/test_runs.py +0 -4
  214. dstack/_internal/cli/commands/pool.py +0 -581
  215. dstack/_internal/cli/commands/run.py +0 -75
  216. dstack/_internal/core/backends/aws/config.py +0 -18
  217. dstack/_internal/core/backends/azure/config.py +0 -12
  218. dstack/_internal/core/backends/base/config.py +0 -5
  219. dstack/_internal/core/backends/cudo/config.py +0 -9
  220. dstack/_internal/core/backends/datacrunch/config.py +0 -9
  221. dstack/_internal/core/backends/gcp/config.py +0 -22
  222. dstack/_internal/core/backends/kubernetes/config.py +0 -6
  223. dstack/_internal/core/backends/lambdalabs/config.py +0 -9
  224. dstack/_internal/core/backends/nebius/__init__.py +0 -15
  225. dstack/_internal/core/backends/nebius/api_client.py +0 -319
  226. dstack/_internal/core/backends/nebius/compute.py +0 -220
  227. dstack/_internal/core/backends/nebius/config.py +0 -6
  228. dstack/_internal/core/backends/nebius/types.py +0 -37
  229. dstack/_internal/core/backends/oci/config.py +0 -6
  230. dstack/_internal/core/backends/runpod/config.py +0 -17
  231. dstack/_internal/core/backends/tensordock/config.py +0 -9
  232. dstack/_internal/core/backends/vastai/config.py +0 -6
  233. dstack/_internal/core/backends/vultr/config.py +0 -9
  234. dstack/_internal/core/models/backends/aws.py +0 -86
  235. dstack/_internal/core/models/backends/azure.py +0 -68
  236. dstack/_internal/core/models/backends/cudo.py +0 -43
  237. dstack/_internal/core/models/backends/datacrunch.py +0 -44
  238. dstack/_internal/core/models/backends/gcp.py +0 -67
  239. dstack/_internal/core/models/backends/kubernetes.py +0 -40
  240. dstack/_internal/core/models/backends/lambdalabs.py +0 -43
  241. dstack/_internal/core/models/backends/nebius.py +0 -54
  242. dstack/_internal/core/models/backends/runpod.py +0 -42
  243. dstack/_internal/core/models/backends/tensordock.py +0 -44
  244. dstack/_internal/core/models/backends/vastai.py +0 -43
  245. dstack/_internal/core/models/backends/vultr.py +0 -40
  246. dstack/_internal/core/models/pools.py +0 -43
  247. dstack/_internal/server/routers/pools.py +0 -142
  248. dstack/_internal/server/schemas/pools.py +0 -38
  249. dstack/_internal/server/services/backends/configurators/base.py +0 -72
  250. dstack/_internal/server/services/backends/configurators/cudo.py +0 -87
  251. dstack/_internal/server/services/backends/configurators/datacrunch.py +0 -79
  252. dstack/_internal/server/services/backends/configurators/kubernetes.py +0 -63
  253. dstack/_internal/server/services/backends/configurators/lambdalabs.py +0 -98
  254. dstack/_internal/server/services/backends/configurators/nebius.py +0 -85
  255. dstack/_internal/server/services/backends/configurators/runpod.py +0 -67
  256. dstack/_internal/server/services/backends/configurators/tensordock.py +0 -82
  257. dstack/_internal/server/services/backends/configurators/vastai.py +0 -80
  258. dstack/_internal/server/services/backends/configurators/vultr.py +0 -80
  259. dstack/api/_public/pools.py +0 -41
  260. dstack/api/_public/resources.py +0 -105
  261. dstack/api/server/_pools.py +0 -63
  262. tests/_internal/server/routers/test_pools.py +0 -612
  263. /dstack/_internal/{server/services/backends/configurators → core/backends/dstack}/__init__.py +0 -0
  264. {dstack-0.18.44.dist-info → dstack-0.19.0.dist-info}/LICENSE.md +0 -0
  265. {dstack-0.18.44.dist-info → dstack-0.19.0.dist-info}/WHEEL +0 -0
  266. {dstack-0.18.44.dist-info → dstack-0.19.0.dist-info}/entry_points.txt +0 -0
  267. {dstack-0.18.44.dist-info → dstack-0.19.0.dist-info}/top_level.txt +0 -0
@@ -7,8 +7,11 @@ from typing import Optional
7
7
  from sqlalchemy import select
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
 
10
+ from dstack._internal.core.models.instances import Resources
10
11
  from dstack._internal.core.models.metrics import JobMetrics, Metric
11
12
  from dstack._internal.server.models import JobMetricsPoint, JobModel
13
+ from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
14
+ from dstack._internal.utils.common import get_or_error
12
15
  from dstack._internal.utils.logging import get_logger
13
16
 
14
17
  logger = get_logger(__name__)
@@ -47,10 +50,10 @@ async def get_job_metrics(
47
50
  # we need at least 2 points to calculate cpu_usage_percent
48
51
  if len(points) < 2:
49
52
  return JobMetrics(metrics=[])
50
- return _calculate_job_metrics(points)
53
+ return _calculate_job_metrics(job_model, points)
51
54
 
52
55
 
53
- def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
56
+ def _calculate_job_metrics(job_model: JobModel, points: Sequence[JobMetricsPoint]) -> JobMetrics:
54
57
  timestamps: list[datetime] = []
55
58
  cpu_usage_points: list[int] = []
56
59
  memory_usage_points: list[int] = []
@@ -58,6 +61,23 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
58
61
  gpus_memory_usage_points: defaultdict[int, list[int]] = defaultdict(list)
59
62
  gpus_util_points: defaultdict[int, list[int]] = defaultdict(list)
60
63
 
64
+ cpus_detected_num: Optional[int] = None
65
+ memory_total: Optional[int] = None
66
+ gpu_memory_total: Optional[int] = None
67
+ resources: Optional[Resources] = None
68
+ jrd = get_job_runtime_data(job_model)
69
+ if jrd is not None and jrd.offer is not None:
70
+ resources = jrd.offer.instance.resources
71
+ else:
72
+ jpd = get_job_provisioning_data(job_model)
73
+ if jpd is not None:
74
+ resources = jpd.instance_type.resources
75
+ if resources is not None:
76
+ cpus_detected_num = resources.cpus
77
+ memory_total = resources.memory_mib * 1024 * 1024
78
+ if len(resources.gpus) > 0:
79
+ gpu_memory_total = resources.gpus[0].memory_mib * 1024 * 1024
80
+
61
81
  gpus_detected_num: Optional[int] = None
62
82
  gpus_detected_num_mismatch: bool = False
63
83
  for point, prev_point in zip(points, points[1:]):
@@ -93,6 +113,10 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
93
113
  values=memory_working_set_points,
94
114
  ),
95
115
  ]
116
+ if cpus_detected_num is not None:
117
+ metrics.append(_make_constant_metric("cpus_detected_num", timestamps, cpus_detected_num))
118
+ if memory_total is not None:
119
+ metrics.append(_make_constant_metric("memory_total_bytes", timestamps, memory_total))
96
120
  if gpus_detected_num_mismatch:
97
121
  # If number of GPUs changed in the time window, skip GPU metrics altogether, otherwise
98
122
  # results can be unpredictable (e.g, one GPU takes place of another, as they are
@@ -100,18 +124,12 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
100
124
  logger.warning("gpus_detected_num mismatch, skipping GPU metrics")
101
125
  else:
102
126
  metrics.append(
103
- # As gpus_detected_num expected to be constant, we add only two points — the latest
104
- # and the earliest in the batch
105
- Metric(
106
- name="gpus_detected_num",
107
- timestamps=[timestamps[0], timestamps[-1]]
108
- if len(timestamps) > 1
109
- else [timestamps[0]],
110
- values=[gpus_detected_num, gpus_detected_num]
111
- if len(timestamps) > 1
112
- else [gpus_detected_num],
113
- )
127
+ _make_constant_metric("gpus_detected_num", timestamps, get_or_error(gpus_detected_num))
114
128
  )
129
+ if gpu_memory_total is not None:
130
+ metrics.append(
131
+ _make_constant_metric("gpu_memory_total_bytes", timestamps, gpu_memory_total)
132
+ )
115
133
  for index, gpu_memory_usage_points in gpus_memory_usage_points.items():
116
134
  metrics.append(
117
135
  Metric(
@@ -131,6 +149,14 @@ def _calculate_job_metrics(points: Sequence[JobMetricsPoint]) -> JobMetrics:
131
149
  return JobMetrics(metrics=metrics)
132
150
 
133
151
 
152
+ def _make_constant_metric(name: str, timestamps: list[datetime], value: float) -> Metric:
153
+ return Metric(
154
+ name=name,
155
+ timestamps=timestamps,
156
+ values=[value] * len(timestamps),
157
+ )
158
+
159
+
134
160
  def _get_cpu_usage(last_point: JobMetricsPoint, prev_point: JobMetricsPoint) -> int:
135
161
  window = last_point.timestamp_micro - prev_point.timestamp_micro
136
162
  if window == 0:
@@ -7,7 +7,7 @@ from dstack._internal.core.backends import (
7
7
  BACKENDS_WITH_MULTINODE_SUPPORT,
8
8
  BACKENDS_WITH_RESERVATION_SUPPORT,
9
9
  )
10
- from dstack._internal.core.backends.base import Backend
10
+ from dstack._internal.core.backends.base.backend import Backend
11
11
  from dstack._internal.core.models.backends.base import BackendType
12
12
  from dstack._internal.core.models.instances import (
13
13
  InstanceOfferWithAvailability,
@@ -7,19 +7,22 @@ from sqlalchemy import func as safunc
7
7
  from sqlalchemy.ext.asyncio import AsyncSession
8
8
  from sqlalchemy.orm import joinedload
9
9
 
10
- from dstack._internal.core.errors import ForbiddenError, ResourceExistsError, ServerClientError
11
- from dstack._internal.core.models.backends import BackendInfo
12
- from dstack._internal.core.models.backends.dstack import (
13
- DstackBaseBackendConfigInfo,
14
- DstackConfigInfo,
10
+ from dstack._internal.core.backends.configurators import get_configurator
11
+ from dstack._internal.core.backends.dstack.models import (
12
+ DstackBackendConfig,
13
+ DstackBaseBackendConfig,
15
14
  )
15
+ from dstack._internal.core.backends.models import BackendInfo
16
+ from dstack._internal.core.errors import ForbiddenError, ResourceExistsError, ServerClientError
16
17
  from dstack._internal.core.models.common import is_core_model_instance
17
18
  from dstack._internal.core.models.projects import Member, MemberPermissions, Project
18
19
  from dstack._internal.core.models.users import GlobalRole, ProjectRole
19
20
  from dstack._internal.server.models import MemberModel, ProjectModel, UserModel
20
21
  from dstack._internal.server.schemas.projects import MemberSetting
21
22
  from dstack._internal.server.services import users
22
- from dstack._internal.server.services.backends import get_configurator
23
+ from dstack._internal.server.services.backends import (
24
+ get_backend_config_from_backend_model,
25
+ )
23
26
  from dstack._internal.server.services.permissions import get_default_permissions
24
27
  from dstack._internal.server.settings import DEFAULT_PROJECT_NAME
25
28
  from dstack._internal.utils.common import get_current_datetime, run_async
@@ -176,12 +179,16 @@ async def set_project_members(
176
179
  # FIXME: potentially long write transaction
177
180
  # clear_project_members() issues DELETE without commit
178
181
  await clear_project_members(session=session, project=project)
179
- usernames = [m.username for m in members]
180
- res = await session.execute(select(UserModel).where(UserModel.name.in_(usernames)))
182
+ names = [m.username for m in members]
183
+ res = await session.execute(
184
+ select(UserModel).where((UserModel.name.in_(names)) | (UserModel.email.in_(names)))
185
+ )
181
186
  users = res.scalars().all()
187
+ # Create lookup maps for both username and email
182
188
  username_to_user = {user.name: user for user in users}
189
+ email_to_user = {user.email: user for user in users if user.email}
183
190
  for i, member in enumerate(members):
184
- user_to_add = username_to_user.get(member.username)
191
+ user_to_add = username_to_user.get(member.username) or email_to_user.get(member.username)
185
192
  if user_to_add is None:
186
193
  continue
187
194
  await add_project_member(
@@ -376,20 +383,22 @@ def project_model_to_project(
376
383
  b.type.value,
377
384
  )
378
385
  continue
379
- config_info = configurator.get_config_info(model=b, include_creds=False)
380
- if is_core_model_instance(config_info, DstackConfigInfo):
381
- for backend_type in config_info.base_backends:
386
+ backend_config = get_backend_config_from_backend_model(
387
+ configurator, b, include_creds=False
388
+ )
389
+ if is_core_model_instance(backend_config, DstackBackendConfig):
390
+ for backend_type in backend_config.base_backends:
382
391
  backends.append(
383
392
  BackendInfo(
384
393
  name=backend_type,
385
- config=DstackBaseBackendConfigInfo(type=backend_type),
394
+ config=DstackBaseBackendConfig(type=backend_type),
386
395
  )
387
396
  )
388
397
  else:
389
398
  backends.append(
390
399
  BackendInfo(
391
400
  name=b.type,
392
- config=config_info,
401
+ config=backend_config,
393
402
  )
394
403
  )
395
404
  return Project(
@@ -1,4 +1,6 @@
1
+ import itertools
1
2
  from collections.abc import Generator, Iterable
3
+ from datetime import timezone
2
4
 
3
5
  from prometheus_client import Metric
4
6
  from prometheus_client.parser import text_string_to_metric_families
@@ -7,21 +9,172 @@ from sqlalchemy import select
7
9
  from sqlalchemy.ext.asyncio import AsyncSession
8
10
  from sqlalchemy.orm import joinedload
9
11
 
10
- from dstack._internal.core.models.runs import JobStatus
11
- from dstack._internal.server.models import JobModel, JobPrometheusMetrics, ProjectModel
12
+ from dstack._internal.core.models.instances import InstanceStatus
13
+ from dstack._internal.core.models.runs import JobStatus, RunSpec
14
+ from dstack._internal.server.models import (
15
+ InstanceModel,
16
+ JobModel,
17
+ JobPrometheusMetrics,
18
+ ProjectModel,
19
+ RunModel,
20
+ )
21
+ from dstack._internal.server.services.instances import get_instance_offer
22
+ from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
23
+ from dstack._internal.utils.common import get_current_datetime
24
+
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"
12
31
 
13
32
 
14
33
  async def get_metrics(session: AsyncSession) -> str:
34
+ metrics_iter = itertools.chain(
35
+ await get_instance_metrics(session),
36
+ await get_job_metrics(session),
37
+ await get_job_gpu_metrics(session),
38
+ )
39
+ return "\n".join(_render_metrics(metrics_iter)) + "\n"
40
+
41
+
42
+ async def get_instance_metrics(session: AsyncSession) -> Iterable[Metric]:
43
+ res = await session.execute(
44
+ select(InstanceModel)
45
+ .join(ProjectModel)
46
+ .where(
47
+ InstanceModel.deleted == False,
48
+ InstanceModel.status.in_(
49
+ [
50
+ InstanceStatus.PROVISIONING,
51
+ InstanceStatus.IDLE,
52
+ InstanceStatus.BUSY,
53
+ InstanceStatus.TERMINATING,
54
+ ]
55
+ ),
56
+ )
57
+ .order_by(ProjectModel.name, InstanceModel.name)
58
+ .options(
59
+ joinedload(InstanceModel.project),
60
+ joinedload(InstanceModel.fleet),
61
+ )
62
+ )
63
+ 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
+ }
77
+ now = get_current_datetime()
78
+ for instance in instances:
79
+ fleet = instance.fleet
80
+ offer = get_instance_offer(instance)
81
+ gpu = ""
82
+ gpu_count = 0
83
+ if offer is not None and len(offer.instance.resources.gpus) > 0:
84
+ gpu = offer.instance.resources.gpus[0].name
85
+ gpu_count = len(offer.instance.resources.gpus)
86
+ labels: dict[str, str] = {
87
+ "dstack_project_name": instance.project.name,
88
+ "dstack_fleet_name": fleet.name if fleet is not None else "",
89
+ "dstack_fleet_id": str(fleet.id) if fleet is not None else "",
90
+ "dstack_instance_name": str(instance.name),
91
+ "dstack_instance_id": str(instance.id),
92
+ "dstack_instance_type": offer.instance.name if offer is not None else "",
93
+ "dstack_backend": instance.backend.value if instance.backend is not None else "",
94
+ "dstack_gpu": gpu,
95
+ }
96
+ 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
+ )
106
+ return metrics.values()
107
+
108
+
109
+ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
110
+ res = await session.execute(
111
+ select(JobModel)
112
+ .join(ProjectModel)
113
+ .where(
114
+ JobModel.status.in_(
115
+ [
116
+ JobStatus.PROVISIONING,
117
+ JobStatus.PULLING,
118
+ JobStatus.RUNNING,
119
+ JobStatus.TERMINATING,
120
+ ]
121
+ )
122
+ )
123
+ .order_by(ProjectModel.name, JobModel.job_name)
124
+ .options(
125
+ joinedload(JobModel.project),
126
+ joinedload(JobModel.run).joinedload(RunModel.user),
127
+ )
128
+ )
129
+ 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
+ }
139
+ now = get_current_datetime()
140
+ for job in jobs:
141
+ jpd = get_job_provisioning_data(job)
142
+ if jpd is None:
143
+ continue
144
+ jrd = get_job_runtime_data(job)
145
+ gpus = jpd.instance_type.resources.gpus
146
+ price = jpd.price
147
+ if jrd is not None and jrd.offer is not None:
148
+ gpus = jrd.offer.instance.resources.gpus
149
+ price = jrd.offer.price
150
+ 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 ""
155
+ 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))
159
+ return metrics.values()
160
+
161
+
162
+ async def get_job_gpu_metrics(session: AsyncSession) -> Iterable[Metric]:
15
163
  res = await session.execute(
16
164
  select(JobPrometheusMetrics)
17
165
  .join(JobModel)
18
166
  .join(ProjectModel)
19
167
  .where(JobModel.status.in_([JobStatus.RUNNING]))
20
168
  .order_by(ProjectModel.name, JobModel.job_name)
21
- .options(joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project))
169
+ .options(
170
+ joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project),
171
+ joinedload(JobPrometheusMetrics.job)
172
+ .joinedload(JobModel.run)
173
+ .joinedload(RunModel.user),
174
+ )
22
175
  )
23
176
  metrics_models = res.scalars().all()
24
- return _process_metrics(metrics_models)
177
+ return _parse_and_enrich_job_gpu_metrics(metrics_models)
25
178
 
26
179
 
27
180
  async def get_project_metrics(session: AsyncSession, project: ProjectModel) -> str:
@@ -33,20 +186,20 @@ async def get_project_metrics(session: AsyncSession, project: ProjectModel) -> s
33
186
  JobModel.status.in_([JobStatus.RUNNING]),
34
187
  )
35
188
  .order_by(JobModel.job_name)
36
- .options(joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project))
189
+ .options(
190
+ joinedload(JobPrometheusMetrics.job).joinedload(JobModel.project),
191
+ joinedload(JobPrometheusMetrics.job)
192
+ .joinedload(JobModel.run)
193
+ .joinedload(RunModel.user),
194
+ )
37
195
  )
38
196
  metrics_models = res.scalars().all()
39
- return _process_metrics(metrics_models)
40
-
41
-
42
- def _process_metrics(metrics_models: Iterable[JobPrometheusMetrics]) -> str:
43
- metrics = _parse_and_enrich_metrics(metrics_models)
44
- if not metrics:
45
- return ""
46
- return "\n".join(_render_metrics(metrics)) + "\n"
197
+ return "\n".join(_render_metrics(_parse_and_enrich_job_gpu_metrics(metrics_models))) + "\n"
47
198
 
48
199
 
49
- def _parse_and_enrich_metrics(metrics_models: Iterable[JobPrometheusMetrics]) -> list[Metric]:
200
+ def _parse_and_enrich_job_gpu_metrics(
201
+ metrics_models: Iterable[JobPrometheusMetrics],
202
+ ) -> Iterable[Metric]:
50
203
  metrics: dict[str, Metric] = {}
51
204
  for metrics_model in metrics_models:
52
205
  for metric in text_string_to_metric_families(metrics_model.text):
@@ -56,18 +209,21 @@ def _parse_and_enrich_metrics(metrics_models: Iterable[JobPrometheusMetrics]) ->
56
209
  metric = metrics.setdefault(name, metric)
57
210
  for sample in samples:
58
211
  labels = sample.labels
59
- labels.update(_get_dstack_labels(metrics_model.job))
212
+ labels.update(_get_job_labels(metrics_model.job))
60
213
  # text_string_to_metric_families "fixes" counter names appending _total,
61
214
  # we rebuild Sample to revert this
62
215
  metric.samples.append(Sample(name, labels, *sample[2:]))
63
- return list(metrics.values())
216
+ return metrics.values()
64
217
 
65
218
 
66
- def _get_dstack_labels(job: JobModel) -> dict[str, str]:
219
+ def _get_job_labels(job: JobModel) -> dict[str, str]:
67
220
  return {
68
221
  "dstack_project_name": job.project.name,
222
+ "dstack_user_name": job.run.user.name,
69
223
  "dstack_run_name": job.run_name,
224
+ "dstack_run_id": str(job.run_id),
70
225
  "dstack_job_name": job.job_name,
226
+ "dstack_job_id": str(job.id),
71
227
  "dstack_job_num": str(job.job_num),
72
228
  "dstack_replica_num": str(job.replica_num),
73
229
  }
@@ -75,12 +231,14 @@ def _get_dstack_labels(job: JobModel) -> dict[str, str]:
75
231
 
76
232
  def _render_metrics(metrics: Iterable[Metric]) -> Generator[str, None, None]:
77
233
  for metric in metrics:
234
+ if not metric.samples:
235
+ continue
78
236
  yield f"# HELP {metric.name} {metric.documentation}"
79
237
  yield f"# TYPE {metric.name} {metric.type}"
80
238
  for sample in metric.samples:
81
239
  parts: list[str] = [f"{sample.name}{{"]
82
240
  parts.extend(",".join(f'{name}="{value}"' for name, value in sample.labels.items()))
83
- parts.append(f"}} {sample.value}")
241
+ parts.append(f"}} {float(sample.value)}")
84
242
  # text_string_to_metric_families converts milliseconds to float seconds
85
243
  if isinstance(sample.timestamp, float):
86
244
  parts.append(f" {int(sample.timestamp * 1000)}")
@@ -52,7 +52,6 @@ from dstack._internal.server import settings
52
52
  from dstack._internal.server.db import get_db
53
53
  from dstack._internal.server.models import (
54
54
  JobModel,
55
- PoolModel,
56
55
  ProjectModel,
57
56
  RepoModel,
58
57
  RunModel,
@@ -61,6 +60,12 @@ from dstack._internal.server.models import (
61
60
  from dstack._internal.server.services import repos as repos_services
62
61
  from dstack._internal.server.services import services
63
62
  from dstack._internal.server.services.docker import is_valid_docker_volume_target
63
+ from dstack._internal.server.services.instances import (
64
+ filter_pool_instances,
65
+ get_instance_offer,
66
+ get_pool_instances,
67
+ get_shared_pool_instances_with_offers,
68
+ )
64
69
  from dstack._internal.server.services.jobs import (
65
70
  check_can_attach_job_volumes,
66
71
  delay_job_instance_termination,
@@ -74,13 +79,6 @@ from dstack._internal.server.services.jobs import (
74
79
  from dstack._internal.server.services.locking import get_locker, string_to_lock_id
75
80
  from dstack._internal.server.services.logging import fmt
76
81
  from dstack._internal.server.services.offers import get_offers_by_requirements
77
- from dstack._internal.server.services.pools import (
78
- filter_pool_instances,
79
- get_instance_offer,
80
- get_or_create_pool_by_name,
81
- get_pool_instances,
82
- get_shared_pool_instances_with_offers,
83
- )
84
82
  from dstack._internal.server.services.projects import list_project_models, list_user_project_models
85
83
  from dstack._internal.server.services.users import get_user_model_by_name
86
84
  from dstack._internal.utils.logging import get_logger
@@ -308,12 +306,9 @@ async def get_plan(
308
306
  job_num=0,
309
307
  )
310
308
 
311
- pool = await get_or_create_pool_by_name(
312
- session=session, project=project, pool_name=profile.pool_name
313
- )
314
309
  pool_offers = await _get_pool_offers(
315
310
  session=session,
316
- pool=pool,
311
+ project=project,
317
312
  run_spec=run_spec,
318
313
  job=jobs[0],
319
314
  volumes=volumes,
@@ -342,8 +337,11 @@ async def get_plan(
342
337
  job_offers.extend(offer for _, offer in offers)
343
338
  job_offers.sort(key=lambda offer: not offer.availability.is_available())
344
339
 
340
+ job_spec = job.job_spec
341
+ _remove_job_spec_sensitive_info(job_spec)
342
+
345
343
  job_plan = JobPlan(
346
- job_spec=job.job_spec,
344
+ job_spec=job_spec,
347
345
  offers=job_offers[:50],
348
346
  total_offers=len(job_offers),
349
347
  max_price=max((offer.price for offer in job_offers), default=None),
@@ -619,7 +617,10 @@ async def delete_runs(
619
617
 
620
618
 
621
619
  def run_model_to_run(
622
- run_model: RunModel, include_job_submissions: bool = True, return_in_api: bool = False
620
+ run_model: RunModel,
621
+ include_job_submissions: bool = True,
622
+ return_in_api: bool = False,
623
+ include_sensitive: bool = False,
623
624
  ) -> Run:
624
625
  jobs: List[Job] = []
625
626
  run_jobs = sorted(run_model.jobs, key=lambda j: (j.replica_num, j.job_num, j.submission_num))
@@ -634,6 +635,8 @@ def run_model_to_run(
634
635
  for job_model in job_submissions:
635
636
  if job_spec is None:
636
637
  job_spec = JobSpec.__response__.parse_raw(job_model.job_spec_data)
638
+ if not include_sensitive:
639
+ _remove_job_spec_sensitive_info(job_spec)
637
640
  if include_job_submissions:
638
641
  job_submission = job_model_to_job_submission(job_model)
639
642
  if return_in_api:
@@ -680,7 +683,7 @@ def run_model_to_run(
680
683
 
681
684
  async def _get_pool_offers(
682
685
  session: AsyncSession,
683
- pool: PoolModel,
686
+ project: ProjectModel,
684
687
  run_spec: RunSpec,
685
688
  job: Job,
686
689
  volumes: List[List[Volume]],
@@ -688,7 +691,8 @@ async def _get_pool_offers(
688
691
  pool_offers: list[InstanceOfferWithAvailability] = []
689
692
 
690
693
  detaching_instances_ids = await get_instances_ids_with_detaching_volumes(session)
691
- pool_instances = [i for i in get_pool_instances(pool) if i.id not in detaching_instances_ids]
694
+ pool_instances = await get_pool_instances(session, project)
695
+ pool_instances = [i for i in pool_instances if i.id not in detaching_instances_ids]
692
696
  multinode = job.job_spec.jobs_per_replica > 1
693
697
 
694
698
  if not multinode:
@@ -1046,3 +1050,7 @@ async def retry_run_replica_jobs(
1046
1050
  # dirty hack to avoid passing all job submissions
1047
1051
  new_job_model.submission_num = job_model.submission_num + 1
1048
1052
  session.add(new_job_model)
1053
+
1054
+
1055
+ def _remove_job_spec_sensitive_info(spec: JobSpec):
1056
+ spec.ssh_key = None
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
7
7
  from sqlalchemy.orm import joinedload, selectinload
8
8
 
9
9
  from dstack._internal.core.backends import BACKENDS_WITH_VOLUMES_SUPPORT
10
+ from dstack._internal.core.backends.base.compute import ComputeWithVolumeSupport
10
11
  from dstack._internal.core.errors import (
11
12
  BackendNotAvailable,
12
13
  ResourceExistsError,
@@ -32,11 +33,11 @@ from dstack._internal.server.models import (
32
33
  VolumeModel,
33
34
  )
34
35
  from dstack._internal.server.services import backends as backends_services
36
+ from dstack._internal.server.services.instances import get_instance_provisioning_data
35
37
  from dstack._internal.server.services.locking import (
36
38
  get_locker,
37
39
  string_to_lock_id,
38
40
  )
39
- from dstack._internal.server.services.pools import get_instance_provisioning_data
40
41
  from dstack._internal.server.services.projects import list_project_models, list_user_project_models
41
42
  from dstack._internal.utils import common, random_names
42
43
  from dstack._internal.utils.logging import get_logger
@@ -375,10 +376,11 @@ async def generate_volume_name(session: AsyncSession, project: ProjectModel) ->
375
376
  def _validate_volume_configuration(configuration: VolumeConfiguration):
376
377
  if configuration.volume_id is None and configuration.size is None:
377
378
  raise ServerClientError("Volume must specify either volume_id or size")
379
+ backends_services.check_backend_type_available(configuration.backend)
378
380
  if configuration.backend not in BACKENDS_WITH_VOLUMES_SUPPORT:
379
381
  raise ServerClientError(
380
- f"Volumes are not supported for {configuration.backend.value} backend. "
381
- f"Supported backends: {[b.value for b in BACKENDS_WITH_VOLUMES_SUPPORT]}."
382
+ f"Volumes are not supported for {configuration.backend.value} backend."
383
+ f" Available backends with volumes support: {[b.value for b in BACKENDS_WITH_VOLUMES_SUPPORT]}."
382
384
  )
383
385
  if configuration.name is not None:
384
386
  validate_dstack_resource_name(configuration.name)
@@ -409,7 +411,9 @@ async def _delete_volume(session: AsyncSession, project: ProjectModel, volume_mo
409
411
  )
410
412
  return
411
413
 
414
+ compute = backend.compute()
415
+ assert isinstance(compute, ComputeWithVolumeSupport)
412
416
  await common.run_async(
413
- backend.compute().delete_volume,
417
+ compute.delete_volume,
414
418
  volume=volume,
415
419
  )
@@ -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-4eb116b97819badd1e2c.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-4a0fe83e84574654e397.js"></script><link href="/main-da9f8c06a69c20dac23e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>