From 60e320d23ebe32c1eefaa8e33d06dab73403a7b7 Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Mon, 5 Oct 2026 13:45:09 +0200 Subject: [PATCH 1/4] Use configured Vultr credentials for GPU offers --- pyproject.toml | 7 +- .../_internal/core/backends/vultr/compute.py | 7 ++ .../core/backends/vultr/test_compute.py | 68 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/tests/_internal/core/backends/vultr/test_compute.py diff --git a/pyproject.toml b/pyproject.toml index bba8433701..aad13d630c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,8 @@ dependencies = [ "python-multipart>=0.0.16", "filelock", "psutil", - "gpuhunt==0.1.32", + # TODO: Replace with a released gpuhunt version after dstackai/gpuhunt#270 is released. + "gpuhunt @ git+https://github.com/dstackai/gpuhunt.git@pr_vultr_vdm", "argcomplete>=3.5.0", "ignore-python>=0.2.0", "apscheduler<4", @@ -75,6 +76,10 @@ artifacts = [ "skills/dstack/SKILL.md" = "dstack/_internal/cli/services/presets/resources/skills/dstack/SKILL.md" "skills/dstack-prototyping/SKILL.md" = "dstack/_internal/cli/services/presets/resources/skills/dstack-prototyping/SKILL.md" +[tool.hatch.metadata] +# TODO: Remove after replacing the temporary gpuhunt Git dependency with a released version. +allow-direct-references = true + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" diff --git a/src/dstack/_internal/core/backends/vultr/compute.py b/src/dstack/_internal/core/backends/vultr/compute.py index 8afcb79369..9b85e71eb0 100644 --- a/src/dstack/_internal/core/backends/vultr/compute.py +++ b/src/dstack/_internal/core/backends/vultr/compute.py @@ -2,7 +2,9 @@ import re from typing import List, Optional +import gpuhunt import requests +from gpuhunt.providers.vultr import VultrProvider from dstack._internal.core.backends.base.backend import Compute from dstack._internal.core.backends.base.compute import ( @@ -46,6 +48,10 @@ def __init__(self, config: VultrConfig): super().__init__() self.config = config self.api_client = VultrApiClient(config.creds.api_key) + self.catalog = gpuhunt.Catalog(balance_resources=False, auto_reload=False) + self.catalog.add_provider( + VultrProvider(api_key=config.creds.api_key, regions=config.regions) + ) def get_all_offers_with_availability( self, unallocated_resources: bool @@ -55,6 +61,7 @@ def get_all_offers_with_availability( requirements=None, locations=self.config.regions or None, extra_filter=_supported_instances, + catalog=self.catalog, ) offers = [ offer.with_availability(availability=InstanceAvailability.AVAILABLE) diff --git a/src/tests/_internal/core/backends/vultr/test_compute.py b/src/tests/_internal/core/backends/vultr/test_compute.py new file mode 100644 index 0000000000..b93bb3893e --- /dev/null +++ b/src/tests/_internal/core/backends/vultr/test_compute.py @@ -0,0 +1,68 @@ +from gpuhunt.providers.vultr import API_URL + +from dstack._internal.core.backends.vultr.compute import VultrCompute +from dstack._internal.core.backends.vultr.models import VultrConfig, VultrCreds +from dstack._internal.core.models.instances import InstanceAvailability + + +class TestGetAllOffersWithAvailability: + def test_uses_backend_credentials_for_gpu_availability(self, requests_mock, monkeypatch): + monkeypatch.setenv("VULTR_API_KEY", "environment-key") + requests_mock.get(f"{API_URL}/plans-metal?per_page=500", json={"plans_metal": []}) + requests_mock.get( + f"{API_URL}/plans?type=all&per_page=500", + json={ + "plans": [ + { + "id": "vc2-2c-4gb", + "type": "vc2", + "vcpu_count": 2, + "ram": 4096, + "disk": 80, + "hourly_cost": 0.027, + "locations": ["blr", "fra"], + }, + { + "id": "vcg-a40-24c-120g-48vram", + "type": "vdm", + "vcpu_count": 24, + "ram": 122880, + "disk": 1400, + "hourly_cost": 1.712, + "deploy_ondemand": True, + "locations": [], + }, + ], + }, + ) + requests_mock.get( + f"{API_URL}/regions/blr/availability?type=vdm", + request_headers={"Authorization": "Bearer backend-key"}, + json={ + "available_plans": ["vcg-a40-24c-120g-48vram"], + "available_vpc_only_plans": [], + }, + ) + compute = VultrCompute( + VultrConfig(creds=VultrCreds(api_key="backend-key"), regions=["blr"]) + ) + + offers = compute.get_all_offers_with_availability(unallocated_resources=False) + + assert [offer.instance.name for offer in offers] == [ + "vc2-2c-4gb", + "vcg-a40-24c-120g-48vram", + ] + assert all(offer.region == "blr" for offer in offers) + assert all(offer.availability == InstanceAvailability.AVAILABLE for offer in offers) + cpu_offer, gpu_offer = offers + assert cpu_offer.instance.resources.cpus == 2 + assert cpu_offer.instance.resources.memory_mib == 4 * 1024 + assert cpu_offer.instance.resources.gpus == [] + assert cpu_offer.price == 0.027 + assert gpu_offer.instance.resources.cpus == 24 + assert gpu_offer.instance.resources.memory_mib == 120 * 1024 + [gpu] = gpu_offer.instance.resources.gpus + assert gpu.name == "A40" + assert gpu.memory_mib == 48 * 1024 + assert gpu_offer.price == 1.712 From f2a29ed0e91f086402926b2a4d614825d701ae68 Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Thu, 8 Oct 2026 11:15:15 +0200 Subject: [PATCH 2/4] Pin temporary gpuhunt dependency to merged commit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aad13d630c..5963a7e33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "filelock", "psutil", # TODO: Replace with a released gpuhunt version after dstackai/gpuhunt#270 is released. - "gpuhunt @ git+https://github.com/dstackai/gpuhunt.git@pr_vultr_vdm", + "gpuhunt @ git+https://github.com/dstackai/gpuhunt.git@3b389574a62dd0036b41169921beaba81f7dd369", "argcomplete>=3.5.0", "ignore-python>=0.2.0", "apscheduler<4", From 3665c30f0621867b4e8d0fae2e2e99e253d8f8f1 Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Thu, 8 Oct 2026 11:40:21 +0200 Subject: [PATCH 3/4] Update gpuhunt to 0.1.33 --- pyproject.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5963a7e33c..0e08aacdcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,7 @@ dependencies = [ "python-multipart>=0.0.16", "filelock", "psutil", - # TODO: Replace with a released gpuhunt version after dstackai/gpuhunt#270 is released. - "gpuhunt @ git+https://github.com/dstackai/gpuhunt.git@3b389574a62dd0036b41169921beaba81f7dd369", + "gpuhunt==0.1.33", "argcomplete>=3.5.0", "ignore-python>=0.2.0", "apscheduler<4", @@ -76,10 +75,6 @@ artifacts = [ "skills/dstack/SKILL.md" = "dstack/_internal/cli/services/presets/resources/skills/dstack/SKILL.md" "skills/dstack-prototyping/SKILL.md" = "dstack/_internal/cli/services/presets/resources/skills/dstack-prototyping/SKILL.md" -[tool.hatch.metadata] -# TODO: Remove after replacing the temporary gpuhunt Git dependency with a released version. -allow-direct-references = true - [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" From 7132ad45e279967c51924b94a6d57b9c33babf24 Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Thu, 8 Oct 2026 11:52:38 +0200 Subject: [PATCH 4/4] Focus Vultr offer test on configured credentials --- .../core/backends/vultr/test_compute.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/tests/_internal/core/backends/vultr/test_compute.py b/src/tests/_internal/core/backends/vultr/test_compute.py index b93bb3893e..530f43ec1e 100644 --- a/src/tests/_internal/core/backends/vultr/test_compute.py +++ b/src/tests/_internal/core/backends/vultr/test_compute.py @@ -2,7 +2,6 @@ from dstack._internal.core.backends.vultr.compute import VultrCompute from dstack._internal.core.backends.vultr.models import VultrConfig, VultrCreds -from dstack._internal.core.models.instances import InstanceAvailability class TestGetAllOffersWithAvailability: @@ -49,20 +48,8 @@ def test_uses_backend_credentials_for_gpu_availability(self, requests_mock, monk offers = compute.get_all_offers_with_availability(unallocated_resources=False) - assert [offer.instance.name for offer in offers] == [ + assert {offer.instance.name for offer in offers} == { "vc2-2c-4gb", "vcg-a40-24c-120g-48vram", - ] + } assert all(offer.region == "blr" for offer in offers) - assert all(offer.availability == InstanceAvailability.AVAILABLE for offer in offers) - cpu_offer, gpu_offer = offers - assert cpu_offer.instance.resources.cpus == 2 - assert cpu_offer.instance.resources.memory_mib == 4 * 1024 - assert cpu_offer.instance.resources.gpus == [] - assert cpu_offer.price == 0.027 - assert gpu_offer.instance.resources.cpus == 24 - assert gpu_offer.instance.resources.memory_mib == 120 * 1024 - [gpu] = gpu_offer.instance.resources.gpus - assert gpu.name == "A40" - assert gpu.memory_mib == 48 * 1024 - assert gpu_offer.price == 1.712