diff --git a/src/dstack/_internal/core/backends/aws/compute.py b/src/dstack/_internal/core/backends/aws/compute.py index 747cc55f0..a5ec98722 100644 --- a/src/dstack/_internal/core/backends/aws/compute.py +++ b/src/dstack/_internal/core/backends/aws/compute.py @@ -168,8 +168,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability if quota is not None and not quota: availability = InstanceAvailability.NO_QUOTA availability_offers.append( - InstanceOfferWithAvailability( - **offer.dict(), + offer.with_availability( availability=availability, availability_zones=regions_to_zones[offer.region], ) diff --git a/src/dstack/_internal/core/backends/azure/compute.py b/src/dstack/_internal/core/backends/azure/compute.py index 44812b138..42f7a6c23 100644 --- a/src/dstack/_internal/core/backends/azure/compute.py +++ b/src/dstack/_internal/core/backends/azure/compute.py @@ -473,9 +473,7 @@ def get_location_quotas(location: str) -> List[str]: availability = InstanceAvailability.NO_QUOTA if (offer.instance.name, offer.region) in has_quota: availability = InstanceAvailability.UNKNOWN - offers_with_availability.append( - InstanceOfferWithAvailability(**offer.dict(), availability=availability) - ) + offers_with_availability.append(offer.with_availability(availability=availability)) return offers_with_availability diff --git a/src/dstack/_internal/core/backends/base/offers.py b/src/dstack/_internal/core/backends/base/offers.py index 6212037d8..a7e0239c8 100644 --- a/src/dstack/_internal/core/backends/base/offers.py +++ b/src/dstack/_internal/core/backends/base/offers.py @@ -78,6 +78,8 @@ def catalog_item_to_offer( requirements: Optional[Requirements], configurable_disk_size: Range[Memory], ) -> Optional[InstanceOffer]: + # Gpu() keeps validation for vendor normalization. + # The rest use construct() to skip redundant validation — data comes from already validated CatalogItem. gpus = [] if item.gpu_count > 0: gpu = Gpu( @@ -93,17 +95,17 @@ def catalog_item_to_offer( ) if disk_size_mib is None: return None - resources = Resources( + resources = Resources.construct( cpu_arch=item.cpu_arch, cpus=item.cpu, memory_mib=round(item.memory * 1024), gpus=gpus, spot=item.spot, - disk=Disk(size_mib=disk_size_mib), + disk=Disk.construct(size_mib=disk_size_mib), ) - return InstanceOffer( + return InstanceOffer.construct( backend=backend, - instance=InstanceType( + instance=InstanceType.construct( name=item.instance_name, resources=resources, ), @@ -236,7 +238,6 @@ def modifier(offer: InstanceOfferWithAvailability) -> Optional[InstanceOfferWith offer_copy.instance.resources.disk = Disk( size_mib=get_or_error(disk_size_range.min) * 1024 ) - offer_copy.instance.resources.update_description() return offer_copy return modifier diff --git a/src/dstack/_internal/core/backends/cloudrift/compute.py b/src/dstack/_internal/core/backends/cloudrift/compute.py index dc467f1c1..4316d47ec 100644 --- a/src/dstack/_internal/core/backends/cloudrift/compute.py +++ b/src/dstack/_internal/core/backends/cloudrift/compute.py @@ -63,9 +63,7 @@ def _get_offers_with_availability( for offer in offers: key = (offer.instance.name, offer.region) availability = region_availabilities.get(key, InstanceAvailability.NOT_AVAILABLE) - availability_offers.append( - InstanceOfferWithAvailability(**offer.dict(), availability=availability) - ) + availability_offers.append(offer.with_availability(availability=availability)) return availability_offers diff --git a/src/dstack/_internal/core/backends/crusoe/compute.py b/src/dstack/_internal/core/backends/crusoe/compute.py index 85293f5d5..fe1411fe7 100644 --- a/src/dstack/_internal/core/backends/crusoe/compute.py +++ b/src/dstack/_internal/core/backends/crusoe/compute.py @@ -162,12 +162,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability else InstanceAvailability.NO_QUOTA ) break - result.append( - InstanceOfferWithAvailability( - **offer.dict(), - availability=availability, - ) - ) + result.append(offer.with_availability(availability=availability)) return result def _get_quota_map(self) -> dict[str, int]: diff --git a/src/dstack/_internal/core/backends/cudo/compute.py b/src/dstack/_internal/core/backends/cudo/compute.py index d1228b86a..edf8d4dc2 100644 --- a/src/dstack/_internal/core/backends/cudo/compute.py +++ b/src/dstack/_internal/core/backends/cudo/compute.py @@ -52,9 +52,7 @@ def get_offers_by_requirements( requirements=requirements, ) offers = [ - InstanceOfferWithAvailability( - **offer.dict(), availability=InstanceAvailability.AVAILABLE - ) + offer.with_availability(availability=InstanceAvailability.AVAILABLE) for offer in offers # in-hyderabad-1 is known to have provisioning issues if offer.region not in ["in-hyderabad-1"] diff --git a/src/dstack/_internal/core/backends/digitalocean_base/compute.py b/src/dstack/_internal/core/backends/digitalocean_base/compute.py index 76cf2c3ee..1128cc530 100644 --- a/src/dstack/_internal/core/backends/digitalocean_base/compute.py +++ b/src/dstack/_internal/core/backends/digitalocean_base/compute.py @@ -63,10 +63,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability catalog=self.catalog, ) return [ - InstanceOfferWithAvailability( - **offer.dict(), - availability=InstanceAvailability.AVAILABLE, - ) + offer.with_availability(availability=InstanceAvailability.AVAILABLE) for offer in offers ] diff --git a/src/dstack/_internal/core/backends/gcp/compute.py b/src/dstack/_internal/core/backends/gcp/compute.py index 37685303d..b7310e4a5 100644 --- a/src/dstack/_internal/core/backends/gcp/compute.py +++ b/src/dstack/_internal/core/backends/gcp/compute.py @@ -160,8 +160,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability if _has_gpu_quota(quotas[region], offer.instance.resources): availability = InstanceAvailability.UNKNOWN # todo quotas: cpu, memory, global gpu, tpu - offer_with_availability = InstanceOfferWithAvailability( - **offer.dict(), + offer_with_availability = offer.with_availability( availability=availability, availability_zones=zones_by_key.get(key, []), ) diff --git a/src/dstack/_internal/core/backends/hotaisle/compute.py b/src/dstack/_internal/core/backends/hotaisle/compute.py index 7c4341f8d..b206a71d4 100644 --- a/src/dstack/_internal/core/backends/hotaisle/compute.py +++ b/src/dstack/_internal/core/backends/hotaisle/compute.py @@ -60,10 +60,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability extra_filter=_supported_instances, ) return [ - InstanceOfferWithAvailability( - **offer.dict(), - availability=InstanceAvailability.AVAILABLE, - ) + offer.with_availability(availability=InstanceAvailability.AVAILABLE) for offer in offers ] diff --git a/src/dstack/_internal/core/backends/lambdalabs/compute.py b/src/dstack/_internal/core/backends/lambdalabs/compute.py index 432cea0ad..1007d33be 100644 --- a/src/dstack/_internal/core/backends/lambdalabs/compute.py +++ b/src/dstack/_internal/core/backends/lambdalabs/compute.py @@ -130,9 +130,7 @@ def _get_offers_with_availability( availability = InstanceAvailability.NOT_AVAILABLE if offer.region in instance_availability.get(offer.instance.name, []): availability = InstanceAvailability.AVAILABLE - availability_offers.append( - InstanceOfferWithAvailability(**offer.dict(), availability=availability) - ) + availability_offers.append(offer.with_availability(availability=availability)) return availability_offers diff --git a/src/dstack/_internal/core/backends/nebius/compute.py b/src/dstack/_internal/core/backends/nebius/compute.py index fdef79373..9d5451339 100644 --- a/src/dstack/_internal/core/backends/nebius/compute.py +++ b/src/dstack/_internal/core/backends/nebius/compute.py @@ -128,11 +128,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability extra_filter=_supported_instances, ) return [ - InstanceOfferWithAvailability( - **offer.dict(), - availability=InstanceAvailability.UNKNOWN, - ) - for offer in offers + offer.with_availability(availability=InstanceAvailability.UNKNOWN) for offer in offers ] def get_offers_modifiers(self, requirements: Requirements) -> Iterable[OfferModifier]: diff --git a/src/dstack/_internal/core/backends/oci/compute.py b/src/dstack/_internal/core/backends/oci/compute.py index ae85dd384..f5a6c8439 100644 --- a/src/dstack/_internal/core/backends/oci/compute.py +++ b/src/dstack/_internal/core/backends/oci/compute.py @@ -92,8 +92,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability else: availability = InstanceAvailability.NO_QUOTA offers_with_availability.append( - InstanceOfferWithAvailability( - **offer.dict(), + offer.with_availability( availability=availability, availability_zones=shapes_availability[offer.region].get( offer.instance.name, [] diff --git a/src/dstack/_internal/core/backends/runpod/compute.py b/src/dstack/_internal/core/backends/runpod/compute.py index 52bc1da9e..5a3a23210 100644 --- a/src/dstack/_internal/core/backends/runpod/compute.py +++ b/src/dstack/_internal/core/backends/runpod/compute.py @@ -84,9 +84,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability extra_filter=lambda o: _is_secure_cloud(o.region) or self.config.allow_community_cloud, ) offers = [ - InstanceOfferWithAvailability( - **offer.dict(), availability=InstanceAvailability.AVAILABLE - ) + offer.with_availability(availability=InstanceAvailability.AVAILABLE) for offer in offers ] return offers diff --git a/src/dstack/_internal/core/backends/template/compute.py.jinja b/src/dstack/_internal/core/backends/template/compute.py.jinja index fbb02be97..cb4c4a8b0 100644 --- a/src/dstack/_internal/core/backends/template/compute.py.jinja +++ b/src/dstack/_internal/core/backends/template/compute.py.jinja @@ -61,10 +61,7 @@ class {{ backend_name }}Compute( ) # TODO: Add availability info to offers return ( - InstanceOfferWithAvailability( - **offer.dict(), - availability=InstanceAvailability.UNKNOWN, - ) + offer.with_availability(availability=InstanceAvailability.UNKNOWN) for offer in offers ) diff --git a/src/dstack/_internal/core/backends/vastai/compute.py b/src/dstack/_internal/core/backends/vastai/compute.py index 254da38e3..adf11c8d8 100644 --- a/src/dstack/_internal/core/backends/vastai/compute.py +++ b/src/dstack/_internal/core/backends/vastai/compute.py @@ -67,8 +67,7 @@ def get_offers_by_requirements( catalog=self.catalog, ) offers = [ - InstanceOfferWithAvailability( - **offer.dict(), + offer.with_availability( availability=InstanceAvailability.AVAILABLE, instance_runtime=InstanceRuntime.RUNNER, ) diff --git a/src/dstack/_internal/core/backends/verda/compute.py b/src/dstack/_internal/core/backends/verda/compute.py index 30a6756c7..f25da7bf9 100644 --- a/src/dstack/_internal/core/backends/verda/compute.py +++ b/src/dstack/_internal/core/backends/verda/compute.py @@ -87,9 +87,7 @@ def _get_offers_with_availability( for offer in offers: key = (offer.instance.name, offer.region) availability = region_availabilities.get(key, InstanceAvailability.NOT_AVAILABLE) - availability_offers.append( - InstanceOfferWithAvailability(**offer.dict(), availability=availability) - ) + availability_offers.append(offer.with_availability(availability=availability)) return availability_offers diff --git a/src/dstack/_internal/core/backends/vultr/compute.py b/src/dstack/_internal/core/backends/vultr/compute.py index 7225f5c87..17877d3d4 100644 --- a/src/dstack/_internal/core/backends/vultr/compute.py +++ b/src/dstack/_internal/core/backends/vultr/compute.py @@ -55,9 +55,7 @@ def get_all_offers_with_availability(self) -> List[InstanceOfferWithAvailability extra_filter=_supported_instances, ) offers = [ - InstanceOfferWithAvailability( - **offer.dict(), availability=InstanceAvailability.AVAILABLE - ) + offer.with_availability(availability=InstanceAvailability.AVAILABLE) for offer in offers ] return offers diff --git a/src/dstack/_internal/core/models/instances.py b/src/dstack/_internal/core/models/instances.py index a7e615902..dfce209c3 100644 --- a/src/dstack/_internal/core/models/instances.py +++ b/src/dstack/_internal/core/models/instances.py @@ -63,31 +63,6 @@ class Resources(CoreModel): str, Field(description="Deprecated: generated client-side. Will be removed in 0.21."), ] = "" - """`description` is deprecated because it is now generated client-side.""" - - @root_validator - def _description(cls, values) -> Dict: - try: - description = values["description"] - if not description: - cpus = values["cpus"] - memory_mib = values["memory_mib"] - gpus = values["gpus"] - disk_size_mib = values["disk"].size_mib - spot = values["spot"] - cpu_arch = values["cpu_arch"] - values["description"] = Resources._pretty_format( - cpus=cpus, - cpu_arch=cpu_arch, - memory_mib=memory_mib, - disk_size_mib=disk_size_mib, - gpus=gpus, - spot=spot, - include_spot=True, - ) - except KeyError: - return values - return values def pretty_format(self, include_spot: bool = False, gpu_only: bool = False) -> str: return Resources._pretty_format( @@ -101,20 +76,6 @@ def pretty_format(self, include_spot: bool = False, gpu_only: bool = False) -> s gpu_only, ) - def update_description(self): - """ - Call to update `description` after patching other properties. - """ - self.description = Resources._pretty_format( - cpus=self.cpus, - cpu_arch=self.cpu_arch, - memory_mib=self.memory_mib, - disk_size_mib=self.disk.size_mib, - gpus=self.gpus, - spot=self.spot, - include_spot=True, - ) - @staticmethod def _pretty_format( cpus: int, @@ -232,6 +193,12 @@ class InstanceOffer(CoreModel): price: float backend_data: dict[str, Any] = {} + def with_availability(self, **kwargs) -> "InstanceOfferWithAvailability": + """Convert to InstanceOfferWithAvailability without re-serializing/re-validating fields. + The result shares nested objects with self. This is generally safe because callers + discard the original InstanceOffer after conversion.""" + return InstanceOfferWithAvailability.construct(**self.__dict__, **kwargs) + class InstanceOfferWithAvailability(InstanceOffer): availability: InstanceAvailability diff --git a/src/tests/_internal/server/routers/test_fleets.py b/src/tests/_internal/server/routers/test_fleets.py index 8fe17ba62..44971db9a 100644 --- a/src/tests/_internal/server/routers/test_fleets.py +++ b/src/tests/_internal/server/routers/test_fleets.py @@ -1110,7 +1110,7 @@ async def test_creates_ssh_fleet(self, test_db, session: AsyncSession, client: A "gpus": [], "spot": False, "disk": {"size_mib": 102400}, - "description": "cpu=2 mem=0GB disk=100GB", + "description": "", }, }, "name": f"{spec.configuration.name}-0", @@ -1327,7 +1327,7 @@ async def test_updates_ssh_fleet(self, test_db, session: AsyncSession, client: A "gpus": [], "spot": False, "disk": {"size_mib": 102400}, - "description": "cpu=2 mem=0GB disk=100GB", + "description": "", }, }, "name": "test-ssh-fleet-0", @@ -1362,7 +1362,7 @@ async def test_updates_ssh_fleet(self, test_db, session: AsyncSession, client: A "gpus": [], "spot": False, "disk": {"size_mib": 102400}, - "description": "cpu=2 mem=0GB disk=100GB", + "description": "", }, }, "name": "test-ssh-fleet-1",