| """Data layer for the embedding-fleet control plane. |
| |
| One poll tick = bucket reads (run manifest + worker heartbeats) + Jobs API reads |
| (stage, durations, one metrics sample per running job). All aggregation to the |
| run level happens here; app.py only renders. |
| |
| Cost figures are client-side estimates (flavor unit price x running time), NOT |
| billing — always presented as "~$". |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import logging |
| import tempfile |
| import time |
| from concurrent.futures import ThreadPoolExecutor |
| from dataclasses import dataclass, field |
| from pathlib import Path |
|
|
| from huggingface_hub import ( |
| download_bucket_files, |
| fetch_job_metrics, |
| list_bucket_tree, |
| list_jobs, |
| list_jobs_hardware, |
| ) |
|
|
| TERMINAL_OK = {"COMPLETED"} |
| TERMINAL_BAD = {"ERROR", "CANCELED", "DELETED"} |
|
|
|
|
| def _stage_name(job) -> str: |
| stage = job.status.stage if job.status else None |
| return getattr(stage, "value", None) or str(stage or "UNKNOWN") |
|
|
|
|
| @dataclass |
| class WorkerRow: |
| rank: int |
| job_id: str | None = None |
| stage: str = "UNKNOWN" |
| rows_done: int = 0 |
| rows_total: int | None = None |
| rows_per_sec: float = 0.0 |
| tokens_done_est: int | None = None |
| gpu_util: float | None = None |
| cost_usd: float | None = None |
| state: str | None = None |
|
|
|
|
| @dataclass |
| class RunView: |
| run_id: str |
| manifest: dict |
| workers: list[WorkerRow] = field(default_factory=list) |
|
|
| |
| rows_done: int = 0 |
| rows_total: int = 0 |
| tokens_done_est: int = 0 |
| cost_usd: float = 0.0 |
| cost_ceiling_usd: float | None = None |
| eta_secs: float | None = None |
| gpu_util: float | None = None |
| healthy: int = 0 |
| errored: int = 0 |
| done: int = 0 |
| num_shards: int = 0 |
|
|
|
|
| _PRICING: dict | None = None |
|
|
|
|
| def pricing() -> dict: |
| global _PRICING |
| if _PRICING is None: |
| _PRICING = {hw.name: hw for hw in list_jobs_hardware()} |
| return _PRICING |
|
|
|
|
| def parse_timeout_secs(timeout) -> float | None: |
| """'20m' / '1h' / '90s' / plain seconds -> seconds.""" |
| if timeout is None: |
| return None |
| s = str(timeout).strip().lower() |
| try: |
| mult = {"s": 1, "m": 60, "h": 3600, "d": 86400}.get(s[-1]) |
| return float(s[:-1]) * mult if mult else float(s) |
| except (ValueError, IndexError): |
| return None |
|
|
|
|
| def list_runs(bucket: str) -> list[str]: |
| """Run ids under runs/, newest first (ids are timestamp-prefixed).""" |
| try: |
| ids = [Path(e.path.rstrip("/")).name |
| for e in list_bucket_tree(bucket, prefix="runs/", recursive=False) |
| if e.__class__.__name__ == "BucketFolder"] |
| |
| return sorted(set(ids), key=lambda r: (r[:8].isdigit(), r), reverse=True) if ids else [] |
| except Exception: |
| return [] |
|
|
|
|
| def _read_bucket_json(bucket: str, paths: list[str]) -> dict[str, dict]: |
| """Fetch small JSON files from the bucket; missing files are skipped.""" |
| out: dict[str, dict] = {} |
| if not paths: |
| return out |
| with tempfile.TemporaryDirectory() as td: |
| pairs = [(p, Path(td) / p.replace("/", "__")) for p in paths] |
| try: |
| download_bucket_files(bucket, pairs, raise_on_missing_files=False) |
| except Exception: |
| return out |
| for src, dst in pairs: |
| if dst.exists(): |
| try: |
| out[src] = json.loads(dst.read_text()) |
| except (json.JSONDecodeError, OSError): |
| pass |
| return out |
|
|
|
|
| def _sample_gpu_util(job_id: str, timeout: float = 3.0) -> float | None: |
| """One metrics sample -> mean GPU utilization. Never blocks past `timeout`.""" |
|
|
| def _one(): |
| gen = iter(fetch_job_metrics(job_id=job_id)) |
| try: |
| raw = next(gen) |
| finally: |
| getattr(gen, "close", lambda: None)() |
| gpus = raw.get("gpus") or {} |
| utils = [float(g.get("utilization") or 0) for g in gpus.values()] |
| return sum(utils) / len(utils) if utils else None |
|
|
| with ThreadPoolExecutor(max_workers=1) as pool: |
| fut = pool.submit(_one) |
| try: |
| return fut.result(timeout=timeout) |
| except Exception: |
| return None |
|
|
|
|
| def _accrued_cost(job, hw_pricing: dict) -> float | None: |
| flavor = getattr(job.flavor, "value", None) or (str(job.flavor) if job.flavor else None) |
| hw = hw_pricing.get(flavor) |
| if not hw: |
| return None |
| secs = job.durations.running_secs if job.durations else None |
| if not secs and job.started_at and _stage_name(job) == "RUNNING": |
| secs = time.time() - job.started_at.timestamp() |
| if not secs: |
| return None |
| return secs / 60.0 * hw.unit_cost_usd |
|
|
|
|
| def load_run(bucket: str, run_id: str, namespace: str | None = None) -> RunView | None: |
| """One full poll tick: manifest + heartbeats + job stages + metrics samples -> RunView.""" |
| manifest = _read_bucket_json(bucket, [f"runs/{run_id}/run.json"]).get(f"runs/{run_id}/run.json") |
| if not manifest: |
| return None |
| n = manifest["num_shards"] |
| view = RunView(run_id=run_id, manifest=manifest, num_shards=n, |
| rows_total=manifest.get("rows_total") or 0) |
|
|
| status_paths = [f"runs/{run_id}/status/{i:05d}.json" for i in range(n)] |
| statuses = _read_bucket_json(bucket, status_paths) |
|
|
| |
| jobs_by_id = {} |
| try: |
| for j in list_jobs(labels={"embedding-fleet-run": run_id}, namespace=namespace): |
| jobs_by_id[j.id] = j |
| except Exception as e: |
| logging.getLogger("control-plane").warning(f"list_jobs failed: {e!r}") |
| manifest_job_ids = manifest.get("job_ids") or [] |
|
|
| hw_pricing = pricing() |
| workers: list[WorkerRow] = [] |
| running_job_ids: list[str] = [] |
| for rank in range(n): |
| row = WorkerRow(rank=rank) |
| st = statuses.get(f"runs/{run_id}/status/{rank:05d}.json") |
| if st: |
| row.state = st.get("state") |
| row.rows_done = st.get("rows_done") or 0 |
| row.rows_total = st.get("rows_total") |
| row.rows_per_sec = st.get("rows_per_sec") or 0.0 |
| row.tokens_done_est = st.get("tokens_done_est") |
| row.job_id = st.get("job_id") |
| if row.job_id is None and rank < len(manifest_job_ids): |
| row.job_id = manifest_job_ids[rank] |
| job = jobs_by_id.get(row.job_id) |
| if job is None and str(rank) in {j.labels.get("rank") for j in jobs_by_id.values() if j.labels}: |
| job = next(j for j in jobs_by_id.values() if (j.labels or {}).get("rank") == str(rank)) |
| if job is not None: |
| row.stage = _stage_name(job) |
| row.cost_usd = _accrued_cost(job, hw_pricing) |
| if row.stage == "RUNNING": |
| running_job_ids.append(row.job_id) |
| workers.append(row) |
|
|
| |
| if running_job_ids: |
| with ThreadPoolExecutor(max_workers=min(8, len(running_job_ids))) as pool: |
| samples = dict(zip(running_job_ids, |
| pool.map(_sample_gpu_util, running_job_ids))) |
| for row in workers: |
| if row.job_id in samples: |
| row.gpu_util = samples[row.job_id] |
|
|
| |
| consolidator_cost = sum( |
| _accrued_cost(j, hw_pricing) or 0.0 |
| for j in jobs_by_id.values() |
| if (j.labels or {}).get("role") == "consolidate" |
| ) |
|
|
| |
| view.workers = workers |
| view.rows_done = sum(w.rows_done for w in workers) |
| view.tokens_done_est = sum(w.tokens_done_est or 0 for w in workers) |
| view.cost_usd = sum(w.cost_usd or 0.0 for w in workers) + consolidator_cost |
| view.done = sum(1 for w in workers if w.state == "done" or w.stage in TERMINAL_OK) |
| view.errored = sum(1 for w in workers if w.state == "error" or w.stage in TERMINAL_BAD) |
| view.healthy = n - view.errored |
| gpu_vals = [w.gpu_util for w in workers if w.gpu_util is not None] |
| view.gpu_util = sum(gpu_vals) / len(gpu_vals) if gpu_vals else None |
|
|
| timeout_secs = parse_timeout_secs(manifest.get("timeout")) |
| hw = hw_pricing.get(manifest.get("flavor")) |
| if timeout_secs and hw: |
| view.cost_ceiling_usd = n * timeout_secs / 60.0 * hw.unit_cost_usd |
|
|
| active_rps = sum(w.rows_per_sec for w in workers |
| if w.state == "running" and w.stage not in TERMINAL_BAD) |
| remaining = max((view.rows_total or 0) - view.rows_done, 0) |
| if active_rps > 0 and remaining > 0: |
| view.eta_secs = remaining / active_rps |
| elif remaining == 0 and view.rows_total: |
| view.eta_secs = 0.0 |
| return view |
|
|