From a6928bf954e91c224124130f6440fc8aa6a46961 Mon Sep 17 00:00:00 2001 From: Jvst Me Date: Tue, 13 Jan 2026 17:05:42 +0100 Subject: [PATCH] Display `InstanceAvailability.NO_BALANCE` in CLI In apply plans, display the `NO_BALANCE` availability as `no balance` rather than an empty string. Small related changes: - Refactor availability formatting so that it is consistent across run plans, fleet plans, and `dstack offer`. In fleet plans, availabilities are now displayed in lower case (previously, this was the only place where they were capitalized). - In `dstack offer --group-by gpu`, if a GPU is unavailable due to more than one reason, display all those reasons (previously, only one of the availabilities was displayed). - Default to dispalying unknown availabilities rather that falling back to an empty string. This will allow new availability types added in the future to automatically become visible in the CLI. --- frontend/src/pages/Offers/List/index.tsx | 4 ++++ .../_internal/cli/services/configurators/fleet.py | 11 +++-------- src/dstack/_internal/cli/utils/common.py | 7 +++++++ src/dstack/_internal/cli/utils/gpu.py | 13 +++++-------- src/dstack/_internal/cli/utils/run.py | 12 ++---------- src/dstack/_internal/core/models/instances.py | 4 +--- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/frontend/src/pages/Offers/List/index.tsx b/frontend/src/pages/Offers/List/index.tsx index f782a7fb42..edf747d251 100644 --- a/frontend/src/pages/Offers/List/index.tsx +++ b/frontend/src/pages/Offers/List/index.tsx @@ -181,6 +181,10 @@ export const OfferList: React.FC = ({ withSearchParams, onChange { id: 'availability', content: (gpu: IGpu) => { + // FIXME: array to string comparison never passes. + // Additionally, there are more availability statuses that are worth displaying, + // and several of them may be present at once. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error if (gpu.availability === 'not_available') { diff --git a/src/dstack/_internal/cli/services/configurators/fleet.py b/src/dstack/_internal/cli/services/configurators/fleet.py index 89278feb94..27b607cb4a 100644 --- a/src/dstack/_internal/cli/services/configurators/fleet.py +++ b/src/dstack/_internal/cli/services/configurators/fleet.py @@ -14,6 +14,7 @@ NO_OFFERS_WARNING, confirm_ask, console, + format_instance_availability, ) from dstack._internal.cli.utils.fleet import get_fleets_table from dstack._internal.cli.utils.rich import MultiItemStatus @@ -32,7 +33,7 @@ FleetSpec, InstanceGroupPlacement, ) -from dstack._internal.core.models.instances import InstanceAvailability, InstanceStatus, SSHKey +from dstack._internal.core.models.instances import InstanceStatus, SSHKey from dstack._internal.core.services.diff import diff_models from dstack._internal.utils.common import local_time from dstack._internal.utils.logging import get_logger @@ -420,12 +421,6 @@ def th(s: str) -> str: for index, offer in enumerate(print_offers, start=1): resources = offer.instance.resources - availability = "" - if offer.availability in { - InstanceAvailability.NOT_AVAILABLE, - InstanceAvailability.NO_QUOTA, - }: - availability = offer.availability.value.replace("_", " ").title() offers_table.add_row( f"{index}", offer.backend.replace("remote", "ssh"), @@ -434,7 +429,7 @@ def th(s: str) -> str: resources.pretty_format(), "yes" if resources.spot else "no", f"${offer.price:3f}".rstrip("0").rstrip("."), - availability, + format_instance_availability(offer.availability), style=None if index == 1 else "secondary", ) if len(plan.offers) > offers_limit: diff --git a/src/dstack/_internal/cli/utils/common.py b/src/dstack/_internal/cli/utils/common.py index c5b185a4b1..d53b84567b 100644 --- a/src/dstack/_internal/cli/utils/common.py +++ b/src/dstack/_internal/cli/utils/common.py @@ -12,6 +12,7 @@ from dstack._internal import settings from dstack._internal.cli.utils.rich import DstackRichHandler from dstack._internal.core.errors import CLIError, DstackError +from dstack._internal.core.models.instances import InstanceAvailability from dstack._internal.utils.common import get_dstack_dir, parse_since _colors = { @@ -146,3 +147,9 @@ def resolve_url(url: str, timeout: float = 5.0) -> str: except requests.exceptions.ConnectionError as e: raise ValueError(f"Failed to resolve url {url}") from e return response.url + + +def format_instance_availability(v: InstanceAvailability) -> str: + if v in (InstanceAvailability.UNKNOWN, InstanceAvailability.AVAILABLE): + return "" + return v.value.replace("_", " ").lower() diff --git a/src/dstack/_internal/cli/utils/gpu.py b/src/dstack/_internal/cli/utils/gpu.py index 89638cb62f..3d19b173ba 100644 --- a/src/dstack/_internal/cli/utils/gpu.py +++ b/src/dstack/_internal/cli/utils/gpu.py @@ -4,7 +4,7 @@ from rich.table import Table from dstack._internal.cli.models.offers import OfferCommandGroupByGpuOutput, OfferRequirements -from dstack._internal.cli.utils.common import console +from dstack._internal.cli.utils.common import console, format_instance_availability from dstack._internal.core.models.gpus import GpuGroup from dstack._internal.core.models.profiles import SpotPolicy from dstack._internal.core.models.runs import Requirements, RunSpec, get_policy_map @@ -117,13 +117,10 @@ def print_gpu_table(gpus: List[GpuGroup], run_spec: RunSpec, group_by: List[str] availability = "" has_available = any(av.is_available() for av in gpu_group.availability) - has_unavailable = any(not av.is_available() for av in gpu_group.availability) - - if has_unavailable and not has_available: - for av in gpu_group.availability: - if av.value in {"not_available", "no_quota", "idle", "busy"}: - availability = av.value.replace("_", " ").lower() - break + if not has_available: + availability = ", ".join( + map(format_instance_availability, set(gpu_group.availability)) + ) secondary_style = "grey58" row_data = [ diff --git a/src/dstack/_internal/cli/utils/run.py b/src/dstack/_internal/cli/utils/run.py index 1b6dfbaeda..dec354e984 100644 --- a/src/dstack/_internal/cli/utils/run.py +++ b/src/dstack/_internal/cli/utils/run.py @@ -11,11 +11,11 @@ NO_OFFERS_WARNING, add_row_from_dict, console, + format_instance_availability, ) from dstack._internal.core.models.backends.base import BackendType from dstack._internal.core.models.configurations import DevEnvironmentConfiguration from dstack._internal.core.models.instances import ( - InstanceAvailability, InstanceOfferWithAvailability, InstanceType, ) @@ -168,14 +168,6 @@ def th(s: str) -> str: for i, offer in enumerate(job_plan.offers, start=1): r = offer.instance.resources - availability = "" - if offer.availability in { - InstanceAvailability.NOT_AVAILABLE, - InstanceAvailability.NO_QUOTA, - InstanceAvailability.IDLE, - InstanceAvailability.BUSY, - }: - availability = offer.availability.value.replace("_", " ").lower() instance = offer.instance.name if offer.total_blocks > 1: instance += f" ({offer.blocks}/{offer.total_blocks})" @@ -185,7 +177,7 @@ def th(s: str) -> str: r.pretty_format(include_spot=True), instance, f"${offer.price:.4f}".rstrip("0").rstrip("."), - availability, + format_instance_availability(offer.availability), style=None if i == 1 or not include_run_properties else "secondary", ) if job_plan.total_offers > len(job_plan.offers): diff --git a/src/dstack/_internal/core/models/instances.py b/src/dstack/_internal/core/models/instances.py index 2bc0c1f898..bf1696758d 100644 --- a/src/dstack/_internal/core/models/instances.py +++ b/src/dstack/_internal/core/models/instances.py @@ -205,9 +205,7 @@ class InstanceAvailability(Enum): AVAILABLE = "available" NOT_AVAILABLE = "not_available" NO_QUOTA = "no_quota" - NO_BALANCE = ( - "no_balance" # Introduced in 0.19.24, may be used after a short compatibility period - ) + NO_BALANCE = "no_balance" # For dstack Sky IDLE = "idle" BUSY = "busy"