Visualize dstack metrics as sparklines - #4086
Open
peterschmidt85 wants to merge 6 commits into
Open
Conversation
peterschmidt85
force-pushed
the
worktree-metrics-sparklines
branch
from
August 4, 2026 10:12
3be1e89 to
3b83f2e
Compare
`dstack metrics` printed one line of raw numbers per GPU, repeated per poll under `--watch`. Eight GPUs meant eight near-identical lines, and nothing about them showed whether a number was steady, climbing or a spike — the shape of a run was only visible by watching the command. Draw each series as a sparkline instead, over the metrics the server retains (~1h for a running job). Glyph height is the bucket's peak so a brief 100% cannot hide, colour is its mean so a card that *held* 100% reads differently from one that *touched* it, and the latest value is printed beside every sparkline — colour is never the only carrier. A labelled rule under the table gives the span, ending in `now` while the job is running and an absolute local time once it is not. Like `dstack logs`, this shows a single job: `--replica` and `--job` select one, both defaulting to 0. Previously every job of a service was printed at once, so comparing replicas now means one invocation each. Also send `limit` explicitly from the API client. The endpoint declares it as `limit: int = 1` rather than Optional, so omitting it silently capped the response at a single sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`test_two_columns_reused_by_every_resource` asserted the column headers read `UTILIZATION` and `MEMORY` and the row labels read `cpu`, `gpu=0`, `gpu=1` — the same literals written a few lines up in `get_metrics_table`. There is no way for the layout to break that this catches and the other tests do not: a row losing its number is covered, the axis falling out of alignment with the cells is covered, and the table overflowing the terminal is covered. All it added was a second place to edit whenever the columns change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`concepts/metrics.md` and `guides/migration/slurm.md` both printed the old `NAME STATUS CPU MEMORY GPU` table as their example, which no longer exists. Both samples are rendered from the current code rather than written by hand. The concepts page also claimed the command shows "the most recently tracked" metrics; it now charts the last hour. Records the single-job behaviour and `--replica`/`--job` there too, next to the `dstack logs` reference it mirrors. Drops a docstring on `format_memory` pointing at its test file: the path went stale when the test moved, and the function did not carry one before it was made public. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`agentic-orchestration.md` printed the old `NAME STATUS CPU MEMORY GPU` table, and `dstack-metrics.md` illustrated the command with a screenshot of that same output. Both now show output rendered from the current code. `dstack-metrics.md` also said the command displays metrics per job for multi-job runs. Rather than rewrite the 2024 text, this adds a note in the style the post already uses for the `dstack stats` rename. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every sample rendered flat -- twenty-seven identical glyphs per row. The generator drove them with a sine of period ~19 samples while a cell buckets ~13, so each bucket's peak came out the same and the variation averaged itself away. A chart that never changes argues against the feature. Drives them from profiles slower than one cell instead: a warmup ramp while weights and data load, steady training, a checkpoint stall two thirds in where the accelerators drop to idle and the CPU picks up, and memory that allocates during warmup then holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`slices` draws one cell per sample and never invents more, so a run younger than the terminal is wide fills only part of the row. `_axis` drew the full requested width regardless, which claimed a span nothing had been measured over -- and because the axis cell was then wider than the sparkline cell, Rich widened the whole UTILIZATION column to fit it, pushing MEMORY sideways and opening a gap after every label. On a nine-minute-old run at 200 columns that was 53 cells of data under a 70-column rule. Runs older than about fifteen minutes were unaffected, which is why it went unnoticed: every fixture had more samples than cells. Draws the rule dotted rather than solid while here. It only has to bind each pair of stamps -- with plain whitespace the gap inside a pair grows wider than the gap between the two columns past ~120 columns, so the stamps group with the wrong neighbour -- and it need not compete with the sparklines above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
peterschmidt85
force-pushed
the
worktree-metrics-sparklines
branch
from
August 4, 2026 14:03
7afabba to
c4a8d0c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before
dstack metricsprinted a line of raw numbers per GPU, repeated on every poll under--watch:Eight GPUs meant eight near-identical lines, and none of them showed whether a number was steady, climbing, or a momentary spike. The shape of a run was only visible by sitting and watching the command.
After
Each series is drawn over the metrics the server retains — about an hour for a running job.
Design notes
Height is the peak, colour is the mean. A cell covers a few minutes of samples. Using the mean for height would let a card that touched 100% for ten seconds disappear entirely; using the peak alone can't distinguish that card from one that held 100% for the whole bucket. Both are drawn, so both questions are answerable.
Colour is never the only carrier. The latest value is printed beside every sparkline, and the fixed 0–100% scale means glyph height always means the same thing as the number next to it — no autoscaling, so a row at 19% of capacity is not drawn nearly full. The number itself is left unstyled: colouring it just repeats what the sparkline already says.
Glyphs stop at
▇, not█. A full block fills its cell to the top edge, so a column of them in consecutive rows fuses into a single mass and the rows stop reading as separate series.Device rows come from the offer rather than from the metrics, so the shape of a run is visible before any samples arrive. The sparklines take whatever width the terminal has (10–80 cells, clamped), and fall back to ASCII glyphs where the console encoding can't carry block characters.
Behaviour change
Like
dstack logs, this now shows a single job:--replicaand--jobselect one, both defaulting to0. Previously every job of a service was printed at once, so comparing replicas now means one invocation each.Why
api/server/_metrics.pychanges tooThe metrics endpoint takes a
limiton how many samples to return, declared aslimit: int = 1rather thanOptional[int]. The Python client had nolimitparameter at all, so every request it made fell through to that default of 1.That was correct until now — the old output only ever printed the latest reading, so one sample was exactly what it needed. Sparklines need the series, so the client has to start asking for it. Without this the command still renders, but on a single sample: one glyph, however long the run has been up, which looks plausible rather than broken.
The client now passes
limit(plusafterandbefore, which the endpoint already accepts), and the CLI requests 1000 samples — the same number the UI's metrics page asks for. That is a sample count rather than a time window, so it outruns the hour of history a running job retains and a young run is never under-filled.Backward compatibility
Output format changes, and only one job is printed. Anything parsing
dstack metricsoutput will break — it was aNAME STATUS CPU MEMORY GPUtable and is now charts. Comparing replicas means one invocation per replica.--jsonsupport for scripted use will follow in a separate PR.No CLI flags removed. The command took only
-w/--watch;--replicaand--jobare additive and default to0.Older servers are fine.
limithas been on the endpoint since 0.18.44, and unknown query params are ignored rather than rejected, so the worst case against an older server is a single sample rather than an error.MetricsAPIClient.get_job_metricsstays source-compatible.after,beforeandlimitare appended after the existing parameters and default toNone, and are left out of the request when unset — an existing call sends the same request it sent before.🤖 Generated with Claude Code