Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/dstack/_internal/core/backends/base/offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ def offer_to_catalog_item(offer: InstanceOffer) -> gpuhunt.CatalogItem:
gpu_vendor = gpu.vendor
gpu_name = gpu.name
gpu_memory = gpu.memory_mib / 1024
# Resources.disk.size_mib = 0 -> gpuhunt.CatalogItem.disk_size = None -> gpuhunt.matches()
# doesn't check disk size (gpuhunt.QueryFilter.{min_disk_size,max_disk_size} are ignored)
disk_size: Optional[float] = None
if (disk_size_mib := offer.instance.resources.disk.size_mib) != 0:
disk_size = disk_size_mib / 1024
return gpuhunt.CatalogItem(
provider=offer.backend.value,
instance_name=offer.instance.name,
Expand All @@ -152,7 +157,7 @@ def offer_to_catalog_item(offer: InstanceOffer) -> gpuhunt.CatalogItem:
gpu_name=gpu_name,
gpu_memory=gpu_memory,
spot=offer.instance.resources.spot,
disk_size=offer.instance.resources.disk.size_mib / 1024,
disk_size=disk_size,
)


Expand Down
1 change: 0 additions & 1 deletion src/dstack/_internal/core/backends/slurm/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,6 @@ def _adjust_resources(resources: Resources, requested_resources: RequestedResour
resources.cpus = requested_resources.cpu_count
resources.memory_mib = requested_resources.memory_mib
resources.gpus = resources.gpus[: requested_resources.gpu_count]
resources.disk = Disk(size_mib=requested_resources.disk_mib)


def _build_image_uri(image_name: str) -> str:
Expand Down
6 changes: 0 additions & 6 deletions src/dstack/_internal/core/backends/slurm/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ class RequestedResources:
cpu_count: int
memory_mib: int
gpu_count: int
disk_mib: int


def get_requested_resources_from_resources_spec(spec: ResourcesSpec) -> RequestedResources:
Expand All @@ -136,13 +135,8 @@ def get_requested_resources_from_resources_spec(spec: ResourcesSpec) -> Requeste
if spec.gpu is not None:
gpu_count = spec.gpu.count.min or 0

disk_mib: int = 0
if spec.disk is not None:
disk_mib = round((spec.disk.size.min or 0) * 1024)

return RequestedResources(
cpu_count=cpu_count,
memory_mib=memory_mib,
gpu_count=gpu_count,
disk_mib=disk_mib,
)
1 change: 1 addition & 0 deletions src/dstack/_internal/core/models/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def validate_name_and_vendor(cls, values):

class Disk(CoreModel):
size_mib: int
"""`size_mib=0` has a special meaning -- size is unknown"""


class Resources(CoreModel):
Expand Down
66 changes: 63 additions & 3 deletions src/tests/_internal/core/backends/base/test_offers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import gpuhunt
import pytest

from dstack._internal.core.backends.base.offers import gpu_matches_gpu_spec
from dstack._internal.core.models.instances import Gpu
from dstack._internal.core.models.resources import GPUSpec
from dstack._internal.core.backends.base.offers import (
filter_offers_by_requirements,
gpu_matches_gpu_spec,
)
from dstack._internal.core.models.backends.base import BackendType
from dstack._internal.core.models.instances import (
Disk,
Gpu,
InstanceOffer,
InstanceType,
Resources,
)
from dstack._internal.core.models.resources import GPUSpec, ResourcesSpec
from dstack._internal.core.models.runs import Requirements

NVIDIA = gpuhunt.AcceleratorVendor.NVIDIA
AMD = gpuhunt.AcceleratorVendor.AMD
Expand Down Expand Up @@ -92,3 +103,52 @@ def test_single_failing_constraint_rejects_gpu(self):
# Everything matches except memory, which is below the minimum.
spec = GPUSpec(vendor="nvidia", name="A100", memory="40GB..80GB")
assert not gpu_matches_gpu_spec(make_gpu(name="A100", memory_gib=24), spec)


class TestFilterOffersByRequirementsDiskSize:
"""
An offer with `disk.size_mib == 0` has an unknown disk size and must not be
filtered out by the disk size requirement. This exercises both the
`offer_to_catalog_item` mapping (`size_mib=0` -> `disk_size=None`) and
`gpuhunt.matches` (ignores disk bounds when `disk_size is None`).
"""

def make_offer(self, disk_size_mib: int) -> InstanceOffer:
# cpus/memory are chosen to satisfy the default requirements so that only the
# disk size decides whether the offer is filtered out.
return InstanceOffer(
backend=BackendType.SLURM,
instance=InstanceType(
name="test-instance",
resources=Resources(
cpus=8,
memory_mib=64 * 1024,
gpus=[],
spot=False,
disk=Disk(size_mib=disk_size_mib),
),
),
region="test-region",
price=1.0,
)

def test_unknown_disk_size_ignores_disk_requirement(self):
# An offer with a real 0 GB disk would satisfy neither the min nor the max
# bound, so a match here proves the disk requirement is ignored entirely.
offer = self.make_offer(disk_size_mib=0)
requirements = Requirements(resources=ResourcesSpec(disk="100GB..200GB"))

assert list(filter_offers_by_requirements([offer], requirements)) == [offer]

def test_known_disk_size_below_min_is_filtered_out(self):
# A non-zero disk size is still checked, so the requirement stays effective.
offer = self.make_offer(disk_size_mib=50 * 1024)
requirements = Requirements(resources=ResourcesSpec(disk="100GB.."))

assert list(filter_offers_by_requirements([offer], requirements)) == []

def test_known_disk_size_within_range_matches(self):
offer = self.make_offer(disk_size_mib=150 * 1024)
requirements = Requirements(resources=ResourcesSpec(disk="100GB..200GB"))

assert list(filter_offers_by_requirements([offer], requirements)) == [offer]
6 changes: 3 additions & 3 deletions src/tests/_internal/core/backends/slurm/test_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def _node_offer() -> InstanceOfferWithAvailability:
memory_mib=128 * 1024,
gpus=[Gpu(name="H100", memory_mib=80 * 1024) for _ in range(8)],
spot=False,
disk=Disk(size_mib=500 * 1024),
disk=Disk(size_mib=0),
),
),
region="cluster1",
Expand Down Expand Up @@ -66,7 +66,7 @@ def test_get_offers_with_full_offers_keeps_full_node_resources():
assert full_resources.cpus == 16
assert full_resources.memory_mib == 128 * 1024
assert len(full_resources.gpus) == 8
assert full_resources.disk.size_mib == 500 * 1024
assert full_resources.disk.size_mib == 0


def test_get_offers_without_full_offers_adjusts_to_requested_slice():
Expand All @@ -89,5 +89,5 @@ def test_get_offers_without_full_offers_adjusts_to_requested_slice():
assert adjusted_resources.cpus == 2
assert adjusted_resources.memory_mib == 8 * 1024
assert len(adjusted_resources.gpus) == 1
assert adjusted_resources.disk.size_mib == 100 * 1024
assert adjusted_resources.disk.size_mib == 0
assert adjusted_offers[0].availability_zones == ["partition1"]
Loading