Skip to content

Commit e97b98e

Browse files
vastzubyclaude
andcommitted
feat(AUTO-1452): perf-only cached rows, spread, --no-cache
Cached benchmark rows now report measured perf only. A cached row's $/hr would have to come from a different machine than the one benchmarked, so perf/$ is omitted until benchmark rows carry the machine's price (AUTO-1130 follow-up). The score-biased current-offers price median is removed. - show median + range (min-max) + n on the cached line, since perf variance is real and workload-dependent - a cache hit with no current offers is flagged "no offers available to rent right now" instead of showing a result you can't act on - rename --fresh to --no-cache; drop --max_age (30d is internal) - sdk: search_benchmarks accepts str | dict Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a634d0e commit e97b98e

3 files changed

Lines changed: 60 additions & 56 deletions

File tree

tests/cli/test_benchmarks_commands.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,10 @@ def test_delete_failure_does_not_raise(self):
219219

220220
def _run_cli(parse_argv, argv, *, create_resp=None, workers_seq=None,
221221
rental_dph=None, template=None, preflight_offers=1,
222-
create_workergroup_raises=None, benchmark_rows=None,
223-
offer_dphs=None):
222+
create_workergroup_raises=None, benchmark_rows=None):
224223
"""Parse argv and invoke the command with a mocked VastAI."""
225224
template = template if template is not None else _FAKE_TEMPLATE
226-
if offer_dphs is not None:
227-
fake_offer_list = [{"id": i, "dph_total": d}
228-
for i, d in enumerate(offer_dphs)]
229-
else:
230-
fake_offer_list = [{"id": i} for i in range(preflight_offers)]
225+
fake_offer_list = [{"id": i} for i in range(preflight_offers)]
231226
instance_resp = {"dph_total": rental_dph} if rental_dph is not None else {}
232227

233228
vast = MagicMock()
@@ -365,23 +360,38 @@ def test_cache_hit_skips_rental_and_uses_median(self, parse_argv):
365360
vast.create_endpoint.assert_not_called()
366361
vast.create_workergroup.assert_not_called()
367362

368-
def test_cached_price_is_current_market_median(self, parse_argv):
363+
def test_cached_rows_have_no_price(self, parse_argv):
364+
# Cached rows report perf only; $/hr would come from a different
365+
# machine than the one benchmarked, so it is omitted.
369366
rows, _ = _run_cli(
370367
parse_argv,
371368
["run", "benchmarks", "--template_id", "99999",
372369
"--gpus", "RTX_3060", "-y", "--raw"],
373370
benchmark_rows=[_bench_row(20.0)],
374-
offer_dphs=[0.2, 0.6, 0.4],
375371
)
376-
assert rows[0]["rental_dph"] == 0.4
377-
assert rows[0]["perf_per_dollar"] == 50.0
372+
assert rows[0]["rental_dph"] is None
373+
assert rows[0]["perf_per_dollar"] is None
374+
375+
def test_cached_unrentable_is_flagged(self, parse_argv, capsys):
376+
rows, vast = _run_cli(
377+
parse_argv,
378+
["run", "benchmarks", "--template_id", "99999",
379+
"--gpus", "RTX_3060", "-y", "--raw"],
380+
benchmark_rows=[_bench_row(10.0)],
381+
preflight_offers=0,
382+
)
383+
assert rows[0]["status"] == "cached"
384+
assert rows[0]["measured_perf"] == 10.0
385+
vast.create_endpoint.assert_not_called()
386+
err = " ".join(capsys.readouterr().err.split()) # rich may wrap lines
387+
assert "no offers available to rent" in err.lower()
378388

379-
def test_fresh_bypasses_cache(self, parse_argv):
389+
def test_no_cache_bypasses_cache(self, parse_argv):
380390
rows, vast = _run_cli(
381391
parse_argv,
382392
["run", "benchmarks", "--template_id", "99999",
383393
"--gpus", "RTX_3060", "--timeout", "60", "-y", "--raw",
384-
"--fresh"],
394+
"--no-cache"],
385395
benchmark_rows=[_bench_row(10.0)],
386396
workers_seq=[[{"id": 1, "measured_perf": 5.0, "status": "idle"}]],
387397
rental_dph=0.5,
@@ -417,16 +427,18 @@ def test_row_matching_by_template_id_only(self, parse_argv):
417427

418428
def test_cache_query_shape(self, parse_argv):
419429
import time
430+
from vastai.cli.commands.benchmarks import _DEFAULT_CACHE_MAX_AGE_DAYS
420431
_, vast = _run_cli(
421432
parse_argv,
422433
["run", "benchmarks", "--template_id", "99999",
423-
"--gpus", "RTX_3060", "--max_age", "7", "-y", "--raw"],
434+
"--gpus", "RTX_3060", "-y", "--raw"],
424435
benchmark_rows=[_bench_row(20.0)],
425436
)
426437
query = vast.search_benchmarks.call_args.kwargs["query"]
427438
assert query["type"] == {"eq": "perf"}
428439
assert query["gpu_name"] == {"eq": "RTX 3060"}
429440
assert query["num_gpus"] == {"eq": 1}
430441
cutoff = query["last_update"]["gte"]
431-
assert abs((time.time() - cutoff) - 7 * 86400) < 60
442+
assert abs((time.time() - cutoff)
443+
- _DEFAULT_CACHE_MAX_AGE_DAYS * 86400) < 60
432444

vastai/cli/commands/benchmarks.py

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -272,19 +272,15 @@ def _pick_num_gpus(gpu_name, extra_filters):
272272
return _get_gpu_chunk_size(raw_min)
273273

274274

275-
# ------------------------------------------------------------------------------------
276-
# cached-benchmark pre-flight
277-
# every run reports its measured perf to the benchmarks table (autoscaler PUT
278-
# /benchmarks, type="perf"), so the table doubles as a cross-user cache: specs
279-
# benchmarked recently can be served from it instead of rented again
280-
# ------------------------------------------------------------------------------------
275+
# Cache pre-flight: runs report measured perf to the benchmarks table, so a
276+
# recent matching row lets us serve this spec instead of renting it again.
281277

282278
_DEFAULT_CACHE_MAX_AGE_DAYS = 30
283279

284280

285281
def _lookup_cached_benchmark(vast, *, gpu_name, num_gpus, template_hash,
286282
template_id, max_age_days):
287-
"""Median of recent reported benchmarks for this exact spec, or None.
283+
"""Recent reported benchmarks for this exact spec (median + spread), or None.
288284
289285
Template is matched client-side: rows carry template_hash or template_id
290286
depending on how the benchmarked workergroup was created.
@@ -307,26 +303,17 @@ def _lookup_cached_benchmark(vast, *, gpu_name, num_gpus, template_hash,
307303
]
308304
if not matched:
309305
return None
306+
values = [r["value"] for r in matched]
310307
newest = max((r.get("last_update") or 0) for r in matched)
311308
return {
312-
"median": statistics.median(r["value"] for r in matched),
309+
"median": statistics.median(values),
310+
"low": min(values),
311+
"high": max(values),
313312
"n": len(matched),
314313
"age_days": max(0.0, (time.time() - newest) / 86400),
315314
}
316315

317316

318-
def _current_median_dph(vast, *, gpu_name, num_gpus, extra_filters):
319-
"""Median $/hr of the offers a run would rent from today. Cached rows
320-
carry no price of their own; perf/$ should reflect renting now."""
321-
query = dict(extra_filters or {})
322-
query["gpu_name"] = {"eq": gpu_name}
323-
query["num_gpus"] = {"eq": num_gpus}
324-
offers = vast.search_offers(query=query, limit=100)
325-
prices = [o.get("dph_total") for o in (offers or []) if isinstance(o, dict)]
326-
prices = [p for p in prices if isinstance(p, (int, float)) and p > 0]
327-
return statistics.median(prices) if prices else None
328-
329-
330317
def _update_worker_states(worker_states, current_workers, gpu_name):
331318
"""Update worker_states with current poll, and print only on worker
332319
rotation (the live rich table already shows current status / elapsed).
@@ -620,10 +607,8 @@ def _benchmark_gpu(vast, *, gpu_name, num_gpus, timeout,
620607
help="GPUs per instance for tokens without an Nx prefix (default 1); overridden by inline Nx in --gpus"),
621608
argument("--timeout", type=int, default=_DEFAULT_BENCHMARK_TIMEOUT,
622609
help=f"max seconds to wait for a benchmark before giving up (default {_DEFAULT_BENCHMARK_TIMEOUT})"),
623-
argument("--fresh", action="store_true",
624-
help="re-measure even when cached benchmark results exist"),
625-
argument("--max_age", type=float, default=_DEFAULT_CACHE_MAX_AGE_DAYS,
626-
help=f"reuse cached benchmark results up to this many days old (default {_DEFAULT_CACHE_MAX_AGE_DAYS}); ignored with --fresh"),
610+
argument("--no-cache", dest="no_cache", action="store_true",
611+
help="re-measure instead of reusing recent cached benchmark results"),
627612
argument("-y", "--yes", action="store_true",
628613
help="Skip confirmation prompt"),
629614
argument("--auto_instance", type=str, default=None, help=argparse.SUPPRESS),
@@ -634,10 +619,9 @@ def _benchmark_gpu(vast, *, gpu_name, num_gpus, timeout,
634619
Rents one instance per GPU in parallel, measures perf, tears down.
635620
Each rental runs for up to --timeout seconds and costs real money.
636621
637-
Specs already benchmarked within the last --max_age days (same
638-
template, GPU, and count, by any user) are served from the benchmarks
639-
table instead of rented; their $/hr is the current market median.
640-
Pass --fresh to re-measure.
622+
Specs benchmarked recently (same template, GPU, and count, by any
623+
user) are served from the benchmarks table as an estimated perf
624+
instead of being rented again. Pass --no-cache to re-measure.
641625
642626
Examples:
643627
# auto-sweep the default GPUs against TGI
@@ -656,7 +640,7 @@ def _benchmark_gpu(vast, *, gpu_name, num_gpus, timeout,
656640
vastai run benchmarks --template_hash 393fa8572e6c73c927c8275fe4dffd53 --timeout 1800 -y
657641
658642
# ignore cached results and re-measure everything
659-
vastai run benchmarks --template_hash 79ebdd2ebfb9d42cedf7a221c42d37a5 --fresh
643+
vastai run benchmarks --template_hash 79ebdd2ebfb9d42cedf7a221c42d37a5 --no-cache
660644
661645
# raw JSON output for piping into another tool
662646
vastai run benchmarks --template_hash 40ef49becc953aa910ee05bd4653b9b3 --raw
@@ -720,30 +704,38 @@ def run__benchmarks(args):
720704
deduped.append((g, n))
721705
gpu_specs = deduped
722706

723-
# Pre-flight: serve specs from cached benchmark results unless --fresh,
707+
# Pre-flight: serve specs from cached benchmark results unless --no-cache,
724708
# then skip specs that have 0 matching offers before prompting,
725709
# so the user sees skip reasons before approving the rentals.
726710
compatible_specs = []
727711
skipped_results = []
728712
cached_results = []
729713
for g, n in gpu_specs:
730-
if not args.fresh:
714+
if not args.no_cache:
731715
hit = _lookup_cached_benchmark(
732716
vast, gpu_name=g, num_gpus=n,
733717
template_hash=template.get("hash_id"),
734718
template_id=template.get("id"),
735-
max_age_days=args.max_age,
719+
max_age_days=_DEFAULT_CACHE_MAX_AGE_DAYS,
736720
)
737721
if hit:
738-
dph = _current_median_dph(vast, gpu_name=g, num_gpus=n,
739-
extra_filters=extra_filters)
722+
# Perf only: a cached row's $/hr would come from a different
723+
# machine than the one benchmarked. TODO: serve real perf/$
724+
# once benchmark rows carry the machine's price (AUTO-1130).
725+
rentable = _has_matching_offer(
726+
vast, gpu_name=g, num_gpus=n, extra_filters=extra_filters)
740727
age = (f"{hit['age_days']:.0f}d" if hit["age_days"] >= 1
741728
else "<1d")
742-
console.print(
743-
f"[green][{n}x {g}] cached:[/green] median perf "
744-
f"{hit['median']:.1f} (n={hit['n']}, newest {age} ago); "
745-
f"pass --fresh to re-measure", highlight=False)
746-
cached_results.append((g, n, "cached", hit["median"], None, dph))
729+
note = None if rentable else "no offers available to rent right now"
730+
msg = (f"[green][{n}x {g}] cached:[/green] median perf "
731+
f"{hit['median']:.1f} "
732+
f"(n={hit['n']}, range {hit['low']:.1f}-{hit['high']:.1f}, "
733+
f"newest {age} ago)")
734+
if note:
735+
msg += f"; [yellow]{note}[/yellow]"
736+
console.print(msg + "; pass --no-cache to re-measure",
737+
highlight=False)
738+
cached_results.append((g, n, "cached", hit["median"], note, None))
747739
continue
748740
if not _has_matching_offer(
749741
vast, gpu_name=g, num_gpus=n,

vastai/sdk.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import warnings
6-
from typing import Dict, List, Optional
6+
from typing import Dict, List, Optional, Union
77

88
from vastai._base import _resolve_api_key, _APIKEY_SENTINEL
99
from vastai.api.client import VastClient
@@ -276,7 +276,7 @@ def search_templates(self, query: Optional[str] = None) -> list[dict]:
276276
"""Search for templates."""
277277
return offers.search_templates(self.client, query=query)
278278

279-
def search_benchmarks(self, query: Optional[str] = None) -> list[dict]:
279+
def search_benchmarks(self, query: Optional[Union[str, dict]] = None) -> list[dict]:
280280
"""Search for benchmarks."""
281281
return offers.search_benchmarks(self.client, query=query)
282282

0 commit comments

Comments
 (0)