Skip to content

Commit c2484d6

Browse files
committed
Merge remote-tracking branch 'origin/master' into issue_3959_distinguish_router_and_non_router_replicas
2 parents 0eb5606 + 5c955f5 commit c2484d6

54 files changed

Lines changed: 1107 additions & 452 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/src/pages/Offers/List/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const getRequestParams = ({
4848
env: {},
4949
resources: {
5050
// cpu/memory/disk should match ResourcesSpec.unconstrained() used by `dstack offer` CLI command
51-
cpu: { min: 1 },
51+
cpu: { count: { min: 1 } },
5252
memory: { min: 0.0 },
5353
disk: null,
5454
gpu: {

frontend/src/types/gpu.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ declare interface IGPUSpecRequest {
2222
compute_capability?: any[];
2323
}
2424

25+
declare interface ICPUSpecRequest {
26+
arch?: 'x86' | 'arm' | null;
27+
count?: TRange | number | string;
28+
}
29+
2530
declare interface IResourcesSpecRequest {
26-
cpu?: TRange | number | string;
31+
cpu?: ICPUSpecRequest | number | string;
2732
memory?: TRange | number | string;
2833
shm_size?: number | string;
2934
gpu?: IGPUSpecRequest | number | string;

frontend/src/types/run.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ declare type TRange = { min?: number; max?: number };
4949

5050
declare type TResourceRequest = {
5151
gpu?: TGPUResources | string | number;
52-
cpu?: string | number | TRange;
52+
cpu?: string | number | ICPUSpecRequest;
5353
memory?: string | number | TRange;
5454
shm_size?: string | number;
5555
disk?:

mkdocs/blog/posts/agentic-orchestration.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,12 @@ $ dstack event --within-run train-qwen
250250
```shell
251251
$ dstack metrics train-qwen
252252
253-
NAME STATUS CPU MEMORY GPU
254-
train-qwen running 92% 118GB/200GB gpu=0 mem=71GB/80GB util=97%
253+
UTILIZATION MEMORY
254+
cpu ▅▄▄▆▆▆▆▆▆▆▆▆▆▆▆▆▆▅▆▆▆▆▆▆▆▆▆ 91% of 32 ▃▃▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 116GB/200GB
255+
256+
gpu=0 ▁▂▃▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆ 92% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 70GB/80GB
257+
258+
4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now 4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now
255259
```
256260

257261
</div>

mkdocs/blog/posts/dstack-metrics.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,23 @@ While it's possible to use third-party monitoring tools with `dstack`, it is oft
1616
track metrics out of the box. That's why, with the latest release, `dstack` introduced [`dstack stats`](../../docs/reference/cli/dstack/metrics.md), a new CLI (and API)
1717
for monitoring container metrics, including GPU usage for `NVIDIA`, `AMD`, and other accelerators.
1818

19-
<img src="https://dstack.ai/static-assets/static-assets/images/dstack-stats-v2.png" width="725"/>
19+
<div class="termy">
20+
21+
```shell
22+
$ dstack metrics llama-70b-sft
23+
24+
UTILIZATION MEMORY
25+
cpu ▅▄▄▄▃▃▃▃▃▃▃▃▃▃▃▃▅▅▄▂▃▃▃▃▃▃▃ 39% of 64 ▃▃▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 297GB/480GB
26+
27+
gpu=0 ▁▂▃▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆ 89% ▄▅▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 67GB/80GB
28+
gpu=1 ▁▂▆▆▆▅▆▆▆▆▆▆▆▆▆▆▁▁▅▆▆▅▆▆▆▆▆ 84% ▄▅▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 67GB/80GB
29+
gpu=2 ▁▂▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆▆ 87% ▄▅▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 67GB/80GB
30+
gpu=3 ▂▃▆▅▅▅▅▅▅▆▅▅▆▆▆▆▁▅▅▅▅▅▆▅▅▅▅ 82% ▄▅▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 67GB/80GB
31+
32+
4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now 4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now
33+
```
34+
35+
</div>
2036

2137
<!-- more -->
2238

@@ -31,6 +47,9 @@ difference is that `dstack stats` includes GPU VRAM usage and GPU utilization pe
3147
Similar to `kubectl top`, if a run consists of multiple jobs (such as distributed training or an auto-scalable service),
3248
`dstack stats` will display metrics per job.
3349

50+
> Note, `dstack metrics` now shows one job at a time, like `dstack logs`. Use `--replica` and `--job` to
51+
> choose it; both default to `0`.
52+
3453
!!! info "HTTP API"
3554
In addition to the `dstack stats` CLI commands, metrics can be obtained via the
3655
[`/api/project/{project_name}/metrics/job/{run_name}`](../../docs/reference/http/metrics.md) HTTP endpoint.

mkdocs/docs/concepts/metrics.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,36 @@ This tab displays key CPU, memory, and GPU metrics collected during the last hou
1818

1919
## CLI
2020

21-
As an alternative to the UI, you can track real-time essential metrics via the CLI.
22-
The `dstack metrics` command displays the most recently tracked CPU, memory, and GPU metrics.
21+
As an alternative to the UI, you can track essential metrics via the CLI.
22+
The `dstack metrics` command charts CPU, memory, and GPU utilization over the last hour of the
23+
job, with the latest value beside each chart.
2324

2425
<div class="termy">
2526

2627
```shell
2728
dstack metrics gentle-mayfly-1
2829

29-
NAME STATUS CPU MEMORY GPU
30-
gentle-mayfly-1 done 0% 16.27GB/2000GB gpu=0 mem=72.48GB/80GB util=0%
31-
gpu=1 mem=64.99GB/80GB util=0%
32-
gpu=2 mem=580MB/80GB util=0%
33-
gpu=3 mem=4MB/80GB util=0%
34-
gpu=4 mem=4MB/80GB util=0%
35-
gpu=5 mem=4MB/80GB util=0%
36-
gpu=6 mem=4MB/80GB util=0%
37-
gpu=7 mem=292MB/80GB util=0%
30+
UTILIZATION MEMORY
31+
cpu ▅▄▄▄▃▃▃▃▃▃▃▃▃▃▃▃▅▅▄▃▃▃▃▃▃▃▃ 41% of 128 ▃▃▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 581GB/960GB
32+
33+
gpu=0 ▁▂▃▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆ 89% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
34+
gpu=1 ▁▂▆▆▆▅▆▆▆▆▆▆▆▆▆▆▁▁▅▆▆▅▆▆▆▆▆ 84% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
35+
gpu=2 ▁▂▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆▆ 87% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
36+
gpu=3 ▂▃▆▅▅▅▅▅▅▆▅▅▆▆▆▆▁▅▅▅▅▅▆▅▅▅▅ 82% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
37+
gpu=4 ▂▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▁▆▆▆▆▆▆▆▆▆▆ 90% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
38+
gpu=5 ▂▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆▆▆ 85% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
39+
gpu=6 ▃▆▆▅▅▆▆▆▆▆▆▆▆▆▆▁▅▅▅▅▆▆▆▅▅▅▅ 83% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
40+
gpu=7 ▃▆▆▆▆▆▆▆▆▆▆▆▆▆▁▁▆▆▆▆▆▆▆▆▆▆▆ 88% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
41+
42+
4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now 4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now
3843
```
3944

4045
</div>
4146

47+
Like [`dstack logs`](../reference/cli/dstack/logs.md), the command shows a single job.
48+
Use `--replica` and `--job` to select one; both default to `0`.
49+
Pass `-w` to keep the charts updating.
50+
4251
## Prometheus
4352

4453
To enable exporting metrics to Prometheus, set the

mkdocs/docs/guides/migration/slurm.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,8 +1475,13 @@ Check real-time metrics:
14751475

14761476
```shell
14771477
$ dstack metrics training-job
1478-
NAME STATUS CPU MEMORY GPU
1479-
training-job running 45% 16.27GB/200GB gpu=0 mem=72.48GB/80GB util=95%
1478+
1479+
UTILIZATION MEMORY
1480+
cpu ▅▄▄▄▃▃▃▃▃▃▃▃▃▃▃▃▅▅▄▃▃▃▃▃▃▃▃ 45% of 32 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 16GB/200GB
1481+
1482+
gpu=0 ▁▂▃▆▆▆▆▆▆▆▆▆▆▆▆▆▆▁▆▆▆▆▆▆▆▆▆ 90% ▄▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 71GB/80GB
1483+
1484+
4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now 4 Aug 14:10 ┄┄┄┄┄┄┄┄┄┄┄ now
14801485
```
14811486

14821487
</div>

runner/docs/shim.openapi.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.2
22

33
info:
44
title: dstack-shim API
5-
version: v2/0.20.30
5+
version: v2/0.21.0
66
x-logo:
77
url: https://avatars.githubusercontent.com/u/54146142?s=260
88
description: >
@@ -90,7 +90,7 @@ paths:
9090
get:
9191
summary: Get instance info
9292
description: >
93-
(since [0.20.30](https://github.com/dstackai/dstack/releases/tag/0.20.30))
93+
(since [0.21.0](https://github.com/dstackai/dstack/releases/tag/0.21.0))
9494
Returns facts about the host observed by shim, e.g., the GPU driver version.
9595
Unlike `/components`, the reported entities are not managed by shim.
9696
tags: [Instance]
Lines changed: 49 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import argparse
22
import time
3-
from typing import Any, List, Optional
43

54
from rich.live import Live
6-
from rich.table import Table
75

86
from dstack._internal.cli.commands import APIBaseCommand
97
from dstack._internal.cli.services.completion import RunNameCompleter
108
from dstack._internal.cli.utils.common import (
11-
LIVE_TABLE_PROVISION_INTERVAL_SECS,
129
LIVE_TABLE_REFRESH_RATE_PER_SEC,
13-
add_row_from_dict,
1410
console,
1511
)
12+
from dstack._internal.cli.utils.metrics import (
13+
MAX_SAMPLES,
14+
WATCH_INTERVAL_SECONDS,
15+
get_metrics_table,
16+
)
1617
from dstack._internal.core.errors import CLIError
17-
from dstack._internal.core.models.instances import Resources
1818
from dstack._internal.core.models.metrics import JobMetrics
19+
from dstack._internal.core.models.runs import Job
1920
from dstack.api._public import Client
2021
from dstack.api._public.runs import Run
2122

@@ -33,121 +34,61 @@ def _register(self):
3334
help="Watch run metrics in realtime",
3435
action="store_true",
3536
)
37+
self._parser.add_argument(
38+
"--replica",
39+
help="The replica number. Defaults to 0.",
40+
type=int,
41+
default=0,
42+
)
43+
self._parser.add_argument(
44+
"--job",
45+
help="The job number inside the replica. Defaults to 0.",
46+
type=int,
47+
default=0,
48+
)
3649

3750
def _command(self, args: argparse.Namespace):
3851
super()._command(args)
39-
run = self.api.runs.get(run_name=args.run_name)
40-
if run is None:
41-
raise CLIError(f"Run {args.run_name} not found")
42-
metrics = _get_run_jobs_metrics(api=self.api, run=run)
52+
job, metrics = self._fetch(args)
4353

4454
if not args.watch:
45-
console.print(_get_metrics_table(run, metrics))
55+
console.print(get_metrics_table(job, metrics))
4656
return
4757

4858
try:
4959
with Live(console=console, refresh_per_second=LIVE_TABLE_REFRESH_RATE_PER_SEC) as live:
5060
while True:
51-
live.update(_get_metrics_table(run, metrics))
52-
time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS)
53-
run = self.api.runs.get(run_name=args.run_name)
54-
if run is None:
55-
raise CLIError(f"Run {args.run_name} not found")
56-
metrics = _get_run_jobs_metrics(api=self.api, run=run)
61+
live.update(get_metrics_table(job, metrics))
62+
time.sleep(WATCH_INTERVAL_SECONDS)
63+
job, metrics = self._fetch(args)
5764
except KeyboardInterrupt:
5865
pass
5966

67+
def _fetch(self, args: argparse.Namespace) -> tuple[Job, JobMetrics]:
68+
run = self.api.runs.get(run_name=args.run_name)
69+
if run is None:
70+
raise CLIError(f"Run {args.run_name} not found")
71+
job = _get_job(run, args.replica, args.job)
72+
return job, _get_job_metrics(self.api, run, job)
6073

61-
def _get_run_jobs_metrics(api: Client, run: Run) -> List[JobMetrics]:
62-
metrics = []
63-
for job in run._run.jobs:
64-
job_metrics = api.client.metrics.get_job_metrics(
65-
project_name=api.project,
66-
run_name=run.name,
67-
replica_num=job.job_spec.replica_num,
68-
job_num=job.job_spec.job_num,
69-
)
70-
metrics.append(job_metrics)
71-
return metrics
72-
73-
74-
def _get_metrics_table(run: Run, metrics: List[JobMetrics]) -> Table:
75-
table = Table(box=None)
76-
table.add_column("NAME", style="bold", no_wrap=True)
77-
table.add_column("STATUS")
78-
table.add_column("CPU")
79-
table.add_column("MEMORY")
80-
table.add_column("GPU")
81-
82-
run_row = {"NAME": run.name, "STATUS": run.status.value}
83-
if len(run._run.jobs) != 1:
84-
add_row_from_dict(table, run_row)
85-
86-
for job, job_metrics in zip(run._run.jobs, metrics):
87-
jrd = job.job_submissions[-1].job_runtime_data
88-
jpd = job.job_submissions[-1].job_provisioning_data
89-
resources: Optional[Resources] = None
90-
if jrd is not None and jrd.offer is not None:
91-
resources = jrd.offer.instance.resources
92-
elif jpd is not None:
93-
resources = jpd.instance_type.resources
94-
cpu_usage = _get_metric_value(job_metrics, "cpu_usage_percent")
95-
if cpu_usage is not None:
96-
if resources is not None:
97-
cpu_usage = cpu_usage / resources.cpus
98-
cpu_usage = f"{cpu_usage:.0f}%"
99-
memory_usage = _get_metric_value(job_metrics, "memory_working_set_bytes")
100-
if memory_usage is not None:
101-
memory_usage = _format_memory(memory_usage, 2)
102-
if resources is not None:
103-
memory_usage += f"/{_format_memory(resources.memory_mib * 1024 * 1024, 2)}"
104-
gpu_metrics = ""
105-
gpus_detected_num = _get_metric_value(job_metrics, "gpus_detected_num")
106-
if gpus_detected_num is not None:
107-
for i in range(gpus_detected_num):
108-
gpu_memory_usage = _get_metric_value(job_metrics, f"gpu_memory_usage_bytes_gpu{i}")
109-
gpu_util_percent = _get_metric_value(job_metrics, f"gpu_util_percent_gpu{i}")
110-
if gpu_memory_usage is not None:
111-
if i != 0:
112-
gpu_metrics += "\n"
113-
gpu_metrics += f"gpu={i} mem={_format_memory(gpu_memory_usage, 2)}"
114-
if resources is not None:
115-
gpu_metrics += (
116-
f"/{_format_memory(resources.gpus[i].memory_mib * 1024 * 1024, 2)}"
117-
)
118-
gpu_metrics += f" util={gpu_util_percent}%"
119-
120-
job_row = {
121-
"NAME": f" replica={job.job_spec.replica_num} job={job.job_spec.job_num}",
122-
"STATUS": job.job_submissions[-1].status.value,
123-
"CPU": cpu_usage or "-",
124-
"MEMORY": memory_usage or "-",
125-
"GPU": gpu_metrics or "-",
126-
}
127-
if len(run._run.jobs) == 1:
128-
job_row.update(run_row)
129-
add_row_from_dict(table, job_row)
130-
131-
return table
132-
133-
134-
def _get_metric_value(job_metrics: JobMetrics, name: str) -> Optional[Any]:
135-
for metric in job_metrics.metrics:
136-
if metric.name == name:
137-
return metric.values[-1]
138-
return None
139-
140-
141-
def _format_memory(memory_bytes: int, decimal_places: int) -> str:
142-
"""See test_format_memory in tests/_internal/cli/commands/test_metrics.py for examples."""
143-
memory_mb = memory_bytes / 1024 / 1024
144-
if memory_mb >= 1024:
145-
value = memory_mb / 1024
146-
unit = "GB"
147-
else:
148-
value = memory_mb
149-
unit = "MB"
15074

151-
if decimal_places == 0:
152-
return f"{round(value)}{unit}"
153-
return f"{value:.{decimal_places}f}".rstrip("0").rstrip(".") + unit
75+
def _get_job(run: Run, replica_num: int, job_num: int) -> Job:
76+
for job in run._run.jobs:
77+
if job.job_spec.replica_num == replica_num and job.job_spec.job_num == job_num:
78+
return job
79+
raise CLIError(
80+
f"Run {run.name} has no replica={replica_num} job={job_num}."
81+
" Use --replica and --job to select one."
82+
)
83+
84+
85+
def _get_job_metrics(api: Client, run: Run, job: Job) -> JobMetrics:
86+
"""`limit` must be sent explicitly: the endpoint declares it `limit: int = 1`, not
87+
Optional, so omitting it caps the response at one sample."""
88+
return api.client.metrics.get_job_metrics(
89+
project_name=api.project,
90+
run_name=run.name,
91+
replica_num=job.job_spec.replica_num,
92+
job_num=job.job_spec.job_num,
93+
limit=MAX_SAMPLES,
94+
)

src/dstack/_internal/cli/models/presets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from dstack._internal.core.models.common import CoreModel
1515
from dstack._internal.core.models.configurations import ServiceConfiguration
1616
from dstack._internal.core.models.profiles import ProfileParams
17-
from dstack._internal.core.models.resources import CPUSpec, ResourcesSpec
17+
from dstack._internal.core.models.resources import ResourcesSpec
1818

1919

2020
class PresetBenchmarkWorkload(CoreModel):
@@ -159,7 +159,7 @@ class PresetListOutput(CoreModel):
159159

160160

161161
def _validate_exact_resources(resources: ResourcesSpec) -> None:
162-
cpu = CPUSpec.model_validate(resources.cpu)
162+
cpu = resources.cpu
163163
if not _is_exact(cpu.count) or not _is_exact(resources.memory):
164164
raise ValueError("preset validation resources must be exact")
165165
if resources.disk is None or not _is_exact(resources.disk.size):

0 commit comments

Comments
 (0)