Align GPU compute across surfaces: shared flavor+cost catalog + OS-search route
Browse fileskey_farm/flavors.py is now the single source of truth for compute flavors
(name/kind/hourly_usd/vram). The scaler budget math costs each GPU flavor
from the catalog, and the OS-search route_query() answers gpu/cost/hardware/
flavor queries (previously 'Route not matched').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- key_farm/flavors.py +73 -0
- key_farm/scaler.py +8 -1
- meshscale_space/os_routes.py +36 -0
key_farm/flavors.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical compute-flavor catalog — ONE source of truth across all surfaces.
|
| 2 |
+
|
| 3 |
+
Consumed by:
|
| 4 |
+
- ``key_farm.scaler.ScalePolicy`` -> per-flavor hourly cost for the budget math
|
| 5 |
+
- ``meshscale_space.os_routes.route_query`` -> the OS-search "gpu"/"cost"/"hardware" route
|
| 6 |
+
- KEY controller/config -> GPU lane selection (``gpu_hardware`` / ``MESHSCALE_GPU_HARDWARE``)
|
| 7 |
+
|
| 8 |
+
Keep hardware facts HERE and nowhere else so every surface stays aligned.
|
| 9 |
+
Rates are approximate HF Spaces/Jobs list price (USD/hour, 2026); vram/ram are
|
| 10 |
+
approximate host specs for guidance, not billing. cpu-upgrade 0.03 matches the
|
| 11 |
+
existing ``cpu_space_hourly_usd`` default.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass(frozen=True)
|
| 19 |
+
class Flavor:
|
| 20 |
+
name: str
|
| 21 |
+
kind: str # "cpu" or "gpu"
|
| 22 |
+
hourly_usd: float
|
| 23 |
+
vram_gb: int # 0 for CPU flavors
|
| 24 |
+
ram_gb: int
|
| 25 |
+
note: str = ""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
FLAVORS: dict[str, Flavor] = {
|
| 29 |
+
f.name: f
|
| 30 |
+
for f in [
|
| 31 |
+
Flavor("cpu-basic", "cpu", 0.00, 0, 16, "2 vCPU - free tier"),
|
| 32 |
+
Flavor("cpu-upgrade", "cpu", 0.03, 0, 32, "8 vCPU - default farm worker"),
|
| 33 |
+
Flavor("t4-small", "gpu", 0.40, 16, 15, "NVIDIA T4 - cheapest GPU"),
|
| 34 |
+
Flavor("t4-medium", "gpu", 0.60, 16, 30, "NVIDIA T4 - more host RAM"),
|
| 35 |
+
Flavor("l4x1", "gpu", 0.80, 24, 48, "NVIDIA L4"),
|
| 36 |
+
Flavor("a10g-small", "gpu", 1.00, 24, 30, "NVIDIA A10G"),
|
| 37 |
+
Flavor("a10g-large", "gpu", 1.50, 24, 92, "NVIDIA A10G - large host"),
|
| 38 |
+
Flavor("a100-large", "gpu", 2.50, 80, 142, "NVIDIA A100 80GB - heavy brains fit"),
|
| 39 |
+
Flavor("rtx-pro-6000", "gpu", 2.75, 96, 188, "RTX PRO 6000 - 96GB VRAM"),
|
| 40 |
+
Flavor("h100", "gpu", 4.50, 80, 250, "NVIDIA H100"),
|
| 41 |
+
Flavor("h200", "gpu", 5.00, 141, 276, "NVIDIA H200 - 141GB VRAM"),
|
| 42 |
+
]
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_flavor(name: str | None) -> Flavor | None:
|
| 47 |
+
if not name:
|
| 48 |
+
return None
|
| 49 |
+
return FLAVORS.get(str(name).strip().lower())
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def list_flavors(kind: str | None = None) -> list[Flavor]:
|
| 53 |
+
"""Flavors sorted cheapest-first, optionally filtered to 'cpu' or 'gpu'."""
|
| 54 |
+
fs = [f for f in FLAVORS.values() if kind is None or f.kind == kind]
|
| 55 |
+
return sorted(fs, key=lambda f: f.hourly_usd)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def hourly_cost(name: str | None, default: float = 0.0) -> float:
|
| 59 |
+
"""Catalog hourly rate for a flavor; ``default`` if the flavor is unknown."""
|
| 60 |
+
f = get_flavor(name)
|
| 61 |
+
return f.hourly_usd if f else default
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def cheapest_gpu() -> Flavor:
|
| 65 |
+
return list_flavors("gpu")[0]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def workers_within_budget(name: str | None, budget_hourly_usd: float, default_cost: float = 0.0) -> int:
|
| 69 |
+
"""How many concurrent workers of ``name`` fit under an hourly budget."""
|
| 70 |
+
cost = hourly_cost(name, default_cost)
|
| 71 |
+
if cost <= 0:
|
| 72 |
+
return 0
|
| 73 |
+
return int(budget_hourly_usd // cost)
|
key_farm/scaler.py
CHANGED
|
@@ -53,7 +53,14 @@ class ScalePolicy:
|
|
| 53 |
|
| 54 |
def _effective_hourly_cost(self) -> float:
|
| 55 |
if self.gpu_hardware:
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
return self.cpu_space_hourly_usd
|
| 58 |
|
| 59 |
def effective_max_workers(self) -> int:
|
|
|
|
| 53 |
|
| 54 |
def _effective_hourly_cost(self) -> float:
|
| 55 |
if self.gpu_hardware:
|
| 56 |
+
# Prefer the canonical catalog rate for the chosen flavor so the
|
| 57 |
+
# budget math stays aligned with every other surface; fall back to
|
| 58 |
+
# the policy's explicit gpu_space_hourly_usd for unknown flavors.
|
| 59 |
+
try:
|
| 60 |
+
from .flavors import hourly_cost
|
| 61 |
+
return hourly_cost(self.gpu_hardware, self.gpu_space_hourly_usd)
|
| 62 |
+
except Exception:
|
| 63 |
+
return self.gpu_space_hourly_usd
|
| 64 |
return self.cpu_space_hourly_usd
|
| 65 |
|
| 66 |
def effective_max_workers(self) -> int:
|
meshscale_space/os_routes.py
CHANGED
|
@@ -144,6 +144,42 @@ def route_query(query: str, status: dict[str, Any], links: dict[str, str] | None
|
|
| 144 |
],
|
| 145 |
}
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
return {
|
| 148 |
"title": "Route not matched",
|
| 149 |
"message": "The OS route did not find a direct match. Start with status or ask for a guide.",
|
|
|
|
| 144 |
],
|
| 145 |
}
|
| 146 |
|
| 147 |
+
if any(token in q for token in (
|
| 148 |
+
"gpu", "cpu-upgrade", "hardware", "flavor", "accelerator", "cost",
|
| 149 |
+
"price", "budget", "rent", "rental", "cuda", "compute", "a100", "t4",
|
| 150 |
+
"h200", "h100", "l4", "a10g", "rtx",
|
| 151 |
+
)):
|
| 152 |
+
try:
|
| 153 |
+
from key_farm.flavors import list_flavors, cheapest_gpu
|
| 154 |
+
cpus = list_flavors("cpu")
|
| 155 |
+
gpus = list_flavors("gpu")
|
| 156 |
+
cheap = cheapest_gpu()
|
| 157 |
+
cpu_str = ", ".join(f"{f.name} ${f.hourly_usd:.2f}/hr" for f in cpus)
|
| 158 |
+
gpu_str = ", ".join(f"{f.name} ${f.hourly_usd:.2f}/hr" for f in gpus)
|
| 159 |
+
message = (
|
| 160 |
+
f"CPU flavors: {cpu_str}. GPU flavors: {gpu_str}. "
|
| 161 |
+
f"Cheapest GPU is {cheap.name} (${cheap.hourly_usd:.2f}/hr, {cheap.vram_gb}GB VRAM). "
|
| 162 |
+
"Route workers onto a GPU by setting gpu_hardware in the KEY run config "
|
| 163 |
+
"or MESHSCALE_GPU_HARDWARE on this Space; the budget then caps the worker count."
|
| 164 |
+
)
|
| 165 |
+
except Exception:
|
| 166 |
+
message = (
|
| 167 |
+
"Compute flavors: cpu-upgrade $0.03/hr (default), GPUs from t4-small "
|
| 168 |
+
"$0.40/hr up to h200 $5.00/hr. Set gpu_hardware / MESHSCALE_GPU_HARDWARE "
|
| 169 |
+
"to route workers onto a GPU; budget caps the count."
|
| 170 |
+
)
|
| 171 |
+
return {
|
| 172 |
+
"title": "Compute / GPU routes",
|
| 173 |
+
"message": message,
|
| 174 |
+
"severity": "info",
|
| 175 |
+
"actions": [
|
| 176 |
+
_action("command", "Plan capacity", "plan 420", "Estimate worker count under the budget for the chosen flavor."),
|
| 177 |
+
_action("command", "Dry-run scale", "scale dry", "Preview worker starts and the flavor the scaler will launch on."),
|
| 178 |
+
_action("link", "Worker template", links["worker_template"], "The image GPU/CPU workers run."),
|
| 179 |
+
_action("link", "Open KEY Space", links["key_space"], "Set gpu_hardware in the run config."),
|
| 180 |
+
],
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
return {
|
| 184 |
"title": "Route not matched",
|
| 185 |
"message": "The OS route did not find a direct match. Start with status or ask for a guide.",
|