diff --git a/src/dstack/_internal/cli/services/configurators/fleet.py b/src/dstack/_internal/cli/services/configurators/fleet.py index 60f2f9267..d54f2c369 100644 --- a/src/dstack/_internal/cli/services/configurators/fleet.py +++ b/src/dstack/_internal/cli/services/configurators/fleet.py @@ -312,7 +312,7 @@ def th(s: str) -> str: offer.instance.name, resources.pretty_format(), "yes" if resources.spot else "no", - f"${offer.price:g}", + f"${offer.price:3f}".rstrip("0").rstrip("."), availability, style=None if index == 1 else "secondary", ) diff --git a/src/dstack/_internal/cli/utils/fleet.py b/src/dstack/_internal/cli/utils/fleet.py index 40dc93748..1c96e2b41 100644 --- a/src/dstack/_internal/cli/utils/fleet.py +++ b/src/dstack/_internal/cli/utils/fleet.py @@ -79,7 +79,9 @@ def get_fleets_table( "BACKEND": backend, "REGION": region, "RESOURCES": resources, - "PRICE": f"${instance.price:.4}" if instance.price is not None else "", + "PRICE": f"${instance.price:.4f}".rstrip("0").rstrip(".") + if instance.price is not None + else "", "STATUS": status, "CREATED": format_date(instance.created), "ERROR": error, diff --git a/src/dstack/_internal/cli/utils/run.py b/src/dstack/_internal/cli/utils/run.py index 638ff7976..089bba7f2 100644 --- a/src/dstack/_internal/cli/utils/run.py +++ b/src/dstack/_internal/cli/utils/run.py @@ -1,3 +1,4 @@ +import os from typing import Any, Dict, List, Optional, Union from rich.markup import escape @@ -35,7 +36,7 @@ def print_run_plan( req = job_plan.job_spec.requirements pretty_req = req.pretty_format(resources_only=True) - max_price = f"${req.max_price:g}" if req.max_price else "-" + max_price = f"${req.max_price:3f}".rstrip("0").rstrip(".") if req.max_price else "-" max_duration = ( format_pretty_duration(job_plan.job_spec.max_duration) if job_plan.job_spec.max_duration @@ -93,14 +94,12 @@ def th(s: str) -> str: props.add_row(th("Inactivity duration"), inactivity_duration) props.add_row(th("Reservation"), run_plan.run_spec.configuration.reservation or "-") - offers = Table(box=None) + offers = Table(box=None, expand=os.get_terminal_size()[0] <= 110) offers.add_column("#") - offers.add_column("BACKEND") - offers.add_column("REGION") - offers.add_column("INSTANCE TYPE") - offers.add_column("RESOURCES") - offers.add_column("SPOT") - offers.add_column("PRICE") + offers.add_column("BACKEND", style="grey58", ratio=2) + offers.add_column("RESOURCES", ratio=4) + offers.add_column("INSTANCE TYPE", style="grey58", no_wrap=True, ratio=2) + offers.add_column("PRICE", style="grey58", ratio=1) offers.add_column() job_plan.offers = job_plan.offers[:max_offers] if max_offers else job_plan.offers @@ -121,14 +120,12 @@ def th(s: str) -> str: instance += f" ({offer.blocks}/{offer.total_blocks})" offers.add_row( f"{i}", - offer.backend.replace("remote", "ssh"), - offer.region, + offer.backend.replace("remote", "ssh") + " (" + offer.region + ")", + r.pretty_format(include_spot=True), instance, - r.pretty_format(), - "yes" if r.spot else "no", - f"${offer.price:g}", + f"${offer.price:.4f}".rstrip("0").rstrip("."), availability, - style=None if i == 1 else "secondary", + style=None if i == 1 or not include_run_properties else "secondary", ) if job_plan.total_offers > len(job_plan.offers): offers.add_row("", "...", style="secondary") @@ -140,7 +137,8 @@ def th(s: str) -> str: if job_plan.total_offers > len(job_plan.offers): console.print( f"[secondary] Shown {len(job_plan.offers)} of {job_plan.total_offers} offers, " - f"${job_plan.max_price:g} max[/]" + f"${job_plan.max_price:3f}".rstrip("0").rstrip(".") + + "max[/]" ) console.print() else: @@ -150,19 +148,18 @@ def th(s: str) -> str: def get_runs_table( runs: List[Run], verbose: bool = False, format_date: DateFormatter = pretty_date ) -> Table: - table = Table(box=None) - table.add_column("NAME", style="bold", no_wrap=True) - table.add_column("BACKEND", style="grey58") + table = Table(box=None, expand=os.get_terminal_size()[0] <= 110) + table.add_column("NAME", style="bold", no_wrap=True, ratio=2) + table.add_column("BACKEND", style="grey58", ratio=2) + table.add_column("RESOURCES", ratio=3 if not verbose else 2) if verbose: - table.add_column("INSTANCE", no_wrap=True) - table.add_column("RESOURCES") + table.add_column("INSTANCE", no_wrap=True, ratio=1) + table.add_column("RESERVATION", no_wrap=True, ratio=1) + table.add_column("PRICE", style="grey58", ratio=1) + table.add_column("STATUS", no_wrap=True, ratio=1) + table.add_column("SUBMITTED", style="grey58", no_wrap=True, ratio=1) if verbose: - table.add_column("RESERVATION", no_wrap=True) - table.add_column("PRICE", no_wrap=True) - table.add_column("STATUS", no_wrap=True) - table.add_column("SUBMITTED", style="grey58", no_wrap=True) - if verbose: - table.add_column("ERROR", no_wrap=True) + table.add_column("ERROR", no_wrap=True, ratio=2) for run in runs: run_error = _get_run_error(run) @@ -201,10 +198,10 @@ def get_runs_table( job_row.update( { "BACKEND": f"{jpd.backend.value.replace('remote', 'ssh')} ({jpd.region})", - "INSTANCE": instance, "RESOURCES": resources.pretty_format(include_spot=True), + "INSTANCE": instance, "RESERVATION": jpd.reservation, - "PRICE": f"${jpd.price:.4}", + "PRICE": f"${jpd.price:.4f}".rstrip("0").rstrip("."), } ) if len(run.jobs) == 1: diff --git a/src/dstack/_internal/core/models/instances.py b/src/dstack/_internal/core/models/instances.py index 65d1a1c95..6919baba8 100644 --- a/src/dstack/_internal/core/models/instances.py +++ b/src/dstack/_internal/core/models/instances.py @@ -57,7 +57,7 @@ def pretty_format(self, include_spot: bool = False) -> str: if self.memory_mib > 0: resources["memory"] = f"{self.memory_mib / 1024:.0f}GB" if self.disk.size_mib > 0: - resources["disk_size"] = f"{self.disk.size_mib / 1024:.1f}GB" + resources["disk_size"] = f"{self.disk.size_mib / 1024:.0f}GB" if self.gpus: gpu = self.gpus[0] resources["gpu_name"] = gpu.name @@ -66,7 +66,7 @@ def pretty_format(self, include_spot: bool = False) -> str: resources["gpu_memory"] = f"{gpu.memory_mib / 1024:.0f}GB" output = pretty_resources(**resources) if include_spot and self.spot: - output += ", SPOT" + output += " (spot)" return output diff --git a/src/dstack/_internal/core/models/runs.py b/src/dstack/_internal/core/models/runs.py index 6a4fb6211..1e02b6c9c 100644 --- a/src/dstack/_internal/core/models/runs.py +++ b/src/dstack/_internal/core/models/runs.py @@ -162,7 +162,7 @@ def pretty_format(self, resources_only: bool = False): if self.spot is not None: res += f", {'spot' if self.spot else 'on-demand'}" if self.max_price is not None: - res += f" under ${self.max_price:g} per hour" + res += f" under ${self.max_price:3f}".rstrip("0").rstrip(".") + " per hour" return res diff --git a/src/dstack/_internal/utils/common.py b/src/dstack/_internal/utils/common.py index a12c177ea..7611062b8 100644 --- a/src/dstack/_internal/utils/common.py +++ b/src/dstack/_internal/utils/common.py @@ -110,25 +110,26 @@ def pretty_resources( """ parts = [] if cpus is not None: - parts.append(f"{cpus}xCPU") + parts.append(f"cpu={cpus}") if memory is not None: - parts.append(f"{memory}") + parts.append(f"mem={memory}") + if disk_size: + parts.append(f"disk={disk_size}") if gpu_count: gpu_parts = [] + gpu_parts.append(f"{gpu_name or 'gpu'}") if gpu_memory is not None: gpu_parts.append(f"{gpu_memory}") + if gpu_count is not None: + gpu_parts.append(f"{gpu_count}") if total_gpu_memory is not None: - gpu_parts.append(f"total {total_gpu_memory}") + gpu_parts.append(f"{total_gpu_memory}") if compute_capability is not None: gpu_parts.append(f"{compute_capability}") - gpu = f"{gpu_count}x{gpu_name or 'GPU'}" - if gpu_parts: - gpu += f" ({', '.join(gpu_parts)})" + gpu = ":".join(gpu_parts) parts.append(gpu) - if disk_size: - parts.append(f"{disk_size} (disk)") - return ", ".join(parts) + return " ".join(parts) def since(timestamp: str) -> datetime: