Skip to content

Vast.ai: Support spot instances#4025

Draft
trashhalo wants to merge 1 commit into
dstackai:masterfrom
Hanno-Labs:vastai-spot
Draft

Vast.ai: Support spot instances#4025
trashhalo wants to merge 1 commit into
dstackai:masterfrom
Hanno-Labs:vastai-spot

Conversation

@trashhalo

@trashhalo trashhalo commented Jul 13, 2026

Copy link
Copy Markdown

Vast.ai offers interruptible (spot) instances, but the vastai backend has never been able to provision them.

Currently, get_offers_by_requirements() strips every spot offer:

# TODO(egor-s): spots currently not supported
extra_filter=lambda offer: not offer.instance.resources.spot,

and create_instance() never bids, so only on-demand instances are provisioned. gpuhunt already emits Vast spot offers (each on-demand offer is duplicated with price set to the machine's min_bid and spot=True), so these offers reach dstack today and are then discarded.

Vast's create endpoint is the same for both instance types. PUT /asks/{id}/ creates an interruptible instance when the payload includes a price (per-machine bid in $/hour), and an on-demand instance when it is omitted. This matches the official vast-python client, whose create instance --bid_price sends the same price field to the same endpoint.

The fix:

  1. Drop the extra_filter that removed spot offers in get_offers_by_requirements(). Spot vs. on-demand selection is already driven by the run's spot_policy through the catalog query, so no offers are surfaced that the user did not ask for.
  2. In run_job(), bid the spot offer's price (which gpuhunt already set to min_bid) and pass it to create_instance(); on-demand offers pass no bid.
  3. Add the price field to the create_instance() payload (None for on-demand preserves the existing behavior).

Interruption handling requires no backend changes: the server infers a reclaimed spot instance from the lost runner connection and, because the offer is marked spot, terminates the job with INTERRUPTED_BY_NO_CAPACITY, which maps to the generic interruption retry event. This is the same path RunPod, OCI, and Nebius spot instances already use.

Tested against a live Vast.ai account: spot offers carry a min_bid below dph_base (e.g. an RTX 4090 at $0.24/hr on-demand vs. $0.12/hr min_bid), and the create endpoint accepts the price field. Unit tests cover that run_job() bids on spot offers and does not bid on on-demand offers.

Vast.ai API reference: https://docs.vast.ai/api/create-instance

AI assistance: This PR was written primarily by Claude Code (investigation, implementation, and tests) and reviewed by me before submitting.

Vast.ai offers interruptible (spot) instances, but the backend filtered
out every spot offer with an `extra_filter` and never bid on one, so only
on-demand instances could be provisioned.

Vast's `PUT /asks/{id}/` endpoint creates an interruptible instance when
the payload includes a `price` (per-machine bid in $/hour); omitting it
creates an on-demand instance. gpuhunt already emits Vast spot offers
(price set to the offer's `min_bid`), so no catalog changes are needed.

* Drop the `extra_filter` that removed spot offers in
  `get_offers_by_requirements`.
* In `run_job`, bid the spot offer's price and pass it to
  `create_instance`; on-demand offers pass no bid.
* Add the `price` field to the `create_instance` payload.

Interruption detection and retry are backend-agnostic (the server treats
a lost spot instance as `INTERRUPTED_BY_NO_CAPACITY`), so no further
changes are required.
@trashhalo trashhalo marked this pull request as draft July 13, 2026 14:48
@jvstme jvstme self-requested a review July 13, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant