|
| 1 | +import shutil |
| 2 | +from typing import List |
| 3 | + |
| 4 | +from rich.table import Table |
| 5 | + |
| 6 | +from dstack._internal.cli.utils.common import console |
| 7 | +from dstack._internal.core.models.profiles import SpotPolicy |
| 8 | +from dstack._internal.core.models.runs import Requirements, RunSpec, get_policy_map |
| 9 | +from dstack._internal.server.schemas.gpus import GpuGroup |
| 10 | + |
| 11 | + |
| 12 | +def print_gpu_json(gpu_response, run_spec, group_by_cli, api_project): |
| 13 | + """Print GPU information in JSON format.""" |
| 14 | + req = Requirements( |
| 15 | + resources=run_spec.configuration.resources, |
| 16 | + max_price=run_spec.merged_profile.max_price, |
| 17 | + spot=get_policy_map(run_spec.merged_profile.spot_policy, default=SpotPolicy.AUTO), |
| 18 | + reservation=run_spec.configuration.reservation, |
| 19 | + ) |
| 20 | + |
| 21 | + if req.spot is None: |
| 22 | + spot_policy = "auto" |
| 23 | + elif req.spot: |
| 24 | + spot_policy = "spot" |
| 25 | + else: |
| 26 | + spot_policy = "on-demand" |
| 27 | + |
| 28 | + output = { |
| 29 | + "project": api_project, |
| 30 | + "user": "admin", # TODO: Get actual user name |
| 31 | + "resources": req.resources.dict(), |
| 32 | + "spot_policy": spot_policy, |
| 33 | + "max_price": req.max_price, |
| 34 | + "reservation": run_spec.configuration.reservation, |
| 35 | + "group_by": group_by_cli, |
| 36 | + "gpus": [], |
| 37 | + } |
| 38 | + |
| 39 | + for gpu_group in gpu_response.gpus: |
| 40 | + gpu_data = { |
| 41 | + "name": gpu_group.name, |
| 42 | + "memory_mib": gpu_group.memory_mib, |
| 43 | + "vendor": gpu_group.vendor.value, |
| 44 | + "availability": [av.value for av in gpu_group.availability], |
| 45 | + "spot": gpu_group.spot, |
| 46 | + "count": {"min": gpu_group.count.min, "max": gpu_group.count.max}, |
| 47 | + "price": {"min": gpu_group.price.min, "max": gpu_group.price.max}, |
| 48 | + } |
| 49 | + |
| 50 | + if gpu_group.backend: |
| 51 | + gpu_data["backend"] = gpu_group.backend.value |
| 52 | + if gpu_group.backends: |
| 53 | + gpu_data["backends"] = [b.value for b in gpu_group.backends] |
| 54 | + if gpu_group.region: |
| 55 | + gpu_data["region"] = gpu_group.region |
| 56 | + if gpu_group.regions: |
| 57 | + gpu_data["regions"] = gpu_group.regions |
| 58 | + |
| 59 | + output["gpus"].append(gpu_data) |
| 60 | + |
| 61 | + import json |
| 62 | + |
| 63 | + print(json.dumps(output, indent=2)) |
| 64 | + |
| 65 | + |
| 66 | +def print_gpu_table(gpus: List[GpuGroup], run_spec: RunSpec, group_by: List[str], project: str): |
| 67 | + """Print GPU information in a formatted table.""" |
| 68 | + print_filter_info(run_spec, group_by, project) |
| 69 | + |
| 70 | + has_single_backend = any(gpu_group.backend for gpu_group in gpus) |
| 71 | + has_single_region = any(gpu_group.region for gpu_group in gpus) |
| 72 | + has_multiple_regions = any(gpu_group.regions for gpu_group in gpus) |
| 73 | + |
| 74 | + if has_single_backend and has_single_region: |
| 75 | + backend_column = "BACKEND" |
| 76 | + region_column = "REGION" |
| 77 | + elif has_single_backend and has_multiple_regions: |
| 78 | + backend_column = "BACKEND" |
| 79 | + region_column = "REGIONS" |
| 80 | + else: |
| 81 | + backend_column = "BACKENDS" |
| 82 | + region_column = None |
| 83 | + |
| 84 | + table = Table(box=None, expand=shutil.get_terminal_size(fallback=(120, 40)).columns <= 110) |
| 85 | + table.add_column("#") |
| 86 | + table.add_column("GPU", no_wrap=True, ratio=2) |
| 87 | + table.add_column("SPOT", style="grey58", ratio=1) |
| 88 | + table.add_column("$/GPU", style="grey58", ratio=1) |
| 89 | + table.add_column(backend_column, style="grey58", ratio=2) |
| 90 | + if region_column: |
| 91 | + table.add_column(region_column, style="grey58", ratio=2) |
| 92 | + table.add_column() |
| 93 | + |
| 94 | + for i, gpu_group in enumerate(gpus, start=1): |
| 95 | + backend_text = "" |
| 96 | + if gpu_group.backend: |
| 97 | + backend_text = gpu_group.backend.value |
| 98 | + elif gpu_group.backends: |
| 99 | + backend_text = ", ".join(b.value for b in gpu_group.backends) |
| 100 | + |
| 101 | + region_text = "" |
| 102 | + if gpu_group.region: |
| 103 | + region_text = gpu_group.region |
| 104 | + elif gpu_group.regions: |
| 105 | + if len(gpu_group.regions) <= 3: |
| 106 | + region_text = ", ".join(gpu_group.regions) |
| 107 | + else: |
| 108 | + region_text = f"{len(gpu_group.regions)} regions" |
| 109 | + |
| 110 | + if not region_column: |
| 111 | + if gpu_group.regions and len(gpu_group.regions) > 3: |
| 112 | + shortened_region_text = f"{len(gpu_group.regions)} regions" |
| 113 | + backends_display = ( |
| 114 | + f"{backend_text} ({shortened_region_text})" |
| 115 | + if shortened_region_text |
| 116 | + else backend_text |
| 117 | + ) |
| 118 | + else: |
| 119 | + backends_display = ( |
| 120 | + f"{backend_text} ({region_text})" if region_text else backend_text |
| 121 | + ) |
| 122 | + else: |
| 123 | + backends_display = backend_text |
| 124 | + |
| 125 | + memory_gb = f"{gpu_group.memory_mib // 1024}GB" |
| 126 | + if gpu_group.count.min == gpu_group.count.max: |
| 127 | + count_range = str(gpu_group.count.min) |
| 128 | + else: |
| 129 | + count_range = f"{gpu_group.count.min}..{gpu_group.count.max}" |
| 130 | + |
| 131 | + gpu_spec = f"{gpu_group.name}:{memory_gb}:{count_range}" |
| 132 | + |
| 133 | + spot_types = [] |
| 134 | + if "spot" in gpu_group.spot: |
| 135 | + spot_types.append("spot") |
| 136 | + if "on-demand" in gpu_group.spot: |
| 137 | + spot_types.append("on-demand") |
| 138 | + spot_display = ", ".join(spot_types) |
| 139 | + |
| 140 | + if gpu_group.price.min == gpu_group.price.max: |
| 141 | + price_display = f"{gpu_group.price.min:.4f}".rstrip("0").rstrip(".") |
| 142 | + else: |
| 143 | + min_formatted = f"{gpu_group.price.min:.4f}".rstrip("0").rstrip(".") |
| 144 | + max_formatted = f"{gpu_group.price.max:.4f}".rstrip("0").rstrip(".") |
| 145 | + price_display = f"{min_formatted}..{max_formatted}" |
| 146 | + |
| 147 | + availability = "" |
| 148 | + has_available = any(av.is_available() for av in gpu_group.availability) |
| 149 | + has_unavailable = any(not av.is_available() for av in gpu_group.availability) |
| 150 | + |
| 151 | + if has_unavailable and not has_available: |
| 152 | + for av in gpu_group.availability: |
| 153 | + if av.value in {"not_available", "no_quota", "idle", "busy"}: |
| 154 | + availability = av.value.replace("_", " ").lower() |
| 155 | + break |
| 156 | + |
| 157 | + secondary_style = "grey58" |
| 158 | + row_data = [ |
| 159 | + f"[{secondary_style}]{i}[/]", |
| 160 | + gpu_spec, |
| 161 | + f"[{secondary_style}]{spot_display}[/]", |
| 162 | + f"[{secondary_style}]{price_display}[/]", |
| 163 | + f"[{secondary_style}]{backends_display}[/]", |
| 164 | + ] |
| 165 | + if region_column: |
| 166 | + row_data.append(f"[{secondary_style}]{region_text}[/]") |
| 167 | + row_data.append(f"[{secondary_style}]{availability}[/]") |
| 168 | + |
| 169 | + table.add_row(*row_data) |
| 170 | + |
| 171 | + console.print(table) |
| 172 | + |
| 173 | + |
| 174 | +def print_filter_info(run_spec: RunSpec, group_by: List[str], project: str): |
| 175 | + """Print filter information for GPU display.""" |
| 176 | + props = Table(box=None, show_header=False) |
| 177 | + props.add_column(no_wrap=True) |
| 178 | + props.add_column() |
| 179 | + |
| 180 | + req = Requirements( |
| 181 | + resources=run_spec.configuration.resources, |
| 182 | + max_price=run_spec.merged_profile.max_price, |
| 183 | + spot=get_policy_map(run_spec.merged_profile.spot_policy, default=SpotPolicy.AUTO), |
| 184 | + reservation=run_spec.merged_profile.reservation, |
| 185 | + ) |
| 186 | + |
| 187 | + pretty_req = req.pretty_format(resources_only=True) |
| 188 | + max_price = f"${req.max_price:3f}".rstrip("0").rstrip(".") if req.max_price else "-" |
| 189 | + |
| 190 | + if req.spot is None: |
| 191 | + spot_policy = "auto" |
| 192 | + elif req.spot: |
| 193 | + spot_policy = "spot" |
| 194 | + else: |
| 195 | + spot_policy = "on-demand" |
| 196 | + |
| 197 | + def th(s: str) -> str: |
| 198 | + return f"[bold]{s}[/bold]" |
| 199 | + |
| 200 | + props.add_row(th("Project"), project) |
| 201 | + props.add_row(th("User"), "admin") # TODO: Get actual user name |
| 202 | + props.add_row(th("Resources"), pretty_req) |
| 203 | + props.add_row(th("Spot policy"), spot_policy) |
| 204 | + props.add_row(th("Max price"), max_price) |
| 205 | + props.add_row(th("Reservation"), run_spec.configuration.reservation or "-") |
| 206 | + if group_by: |
| 207 | + props.add_row(th("Group by"), ", ".join(group_by)) |
| 208 | + |
| 209 | + console.print(props) |
| 210 | + console.print() |
0 commit comments