Spaces:
Running
Running
| import logging | |
| import os | |
| import re | |
| import threading | |
| import time | |
| from concurrent.futures import ThreadPoolExecutor | |
| from contextlib import asynccontextmanager | |
| import uvicorn | |
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.responses import FileResponse | |
| from pydantic import BaseModel | |
| from . import auth, config, history, prober, state | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") | |
| log = logging.getLogger("gpu_capacity") | |
| INDEX_HTML = os.path.join(os.path.dirname(__file__), "static", "index.html") | |
| def _sweep(instance_type: str, regions: list[str]) -> dict: | |
| with ThreadPoolExecutor(max_workers=config.MAX_PROBE_WORKERS) as pool: | |
| results = dict( | |
| zip(regions, pool.map(lambda r: prober.probe_region(r, instance_type), regions)) | |
| ) | |
| for region, result in results.items(): | |
| state.update(instance_type, region, result) | |
| try: | |
| history.record(instance_type, results) | |
| except Exception: | |
| log.exception("history record failed") | |
| return results | |
| def _auto_refresh_loop(): | |
| # If restored state is still fresh (e.g. restart right after a sweep), | |
| # don't re-probe everything — wait out the remainder of the period. | |
| # Types added to the config since the state was written are probed anyway. | |
| newest = state.newest_checked_at() | |
| if newest is not None: | |
| wait = config.AUTO_REFRESH_MINUTES * 60 - (time.time() - newest) | |
| if wait > 0: | |
| for instance_type in config.INSTANCE_TYPES: | |
| if instance_type not in state.get(): | |
| _sweep(instance_type, config.REGIONS) | |
| log.info("restored state is fresh, first full sweep in %.0fs", wait) | |
| time.sleep(wait) | |
| while True: | |
| log.info("auto-refresh: probing all configured types") | |
| started = time.time() | |
| for instance_type in config.INSTANCE_TYPES: | |
| _sweep(instance_type, config.REGIONS) | |
| log.info("auto-refresh: sweep done in %.0fs", time.time() - started) | |
| time.sleep(max(60, config.AUTO_REFRESH_MINUTES * 60 - (time.time() - started))) | |
| async def lifespan(app: FastAPI): | |
| state.load() | |
| history.load() | |
| history.start_writer() | |
| threading.Thread(target=prober.cleanup_leaked, daemon=True).start() | |
| if config.AUTO_REFRESH_MINUTES > 0: | |
| log.info("auto-refresh enabled: every %d min", config.AUTO_REFRESH_MINUTES) | |
| threading.Thread(target=_auto_refresh_loop, daemon=True).start() | |
| yield | |
| app = FastAPI(title="gpu-capacity", lifespan=lifespan) | |
| app.include_router(auth.router) | |
| class ProbeRequest(BaseModel): | |
| instance_type: str | |
| region: str | None = None | |
| TYPE_RE = re.compile(r"[a-z0-9\-]+\.[a-z0-9\-]+") | |
| def _validate(instance_type: str, region: str | None): | |
| if not TYPE_RE.fullmatch(instance_type): | |
| raise HTTPException(400, f"invalid instance type {instance_type!r}") | |
| if region is not None and region not in config.REGIONS: | |
| raise HTTPException(400, f"unknown region {region!r}") | |
| def index(): | |
| return FileResponse(INDEX_HTML) | |
| def get_state(): | |
| current = state.get() | |
| extras = sorted(t for t in current if t not in config.INSTANCE_TYPES) | |
| types = config.INSTANCE_TYPES + extras | |
| return { | |
| "instance_types": types, | |
| "regions": config.REGIONS, | |
| "stale_after_seconds": config.STALE_AFTER_SECONDS, | |
| "gpu_info": prober.gpu_info(types), | |
| "state": current, | |
| } | |
| def me(request: Request): | |
| user = auth.current_user(request) | |
| return { | |
| "authenticated": user is not None, | |
| "username": user.get("u") if user else None, | |
| "can_probe": (not auth.ENABLED) or bool(user and user.get("hf")), | |
| "auth_enabled": auth.ENABLED, | |
| } | |
| def availability(request: Request, instance_type: str, | |
| refresh: bool = False, region: str | None = None): | |
| """Cached availability for one instance type; refresh=true probes first.""" | |
| _validate(instance_type, region) | |
| regions = [region] if region else config.REGIONS | |
| if refresh: | |
| auth.require_probe_rights(request) | |
| _sweep(instance_type, regions) | |
| data = state.get().get(instance_type, {}) | |
| data = {r: data[r] for r in regions if r in data} | |
| return { | |
| "instance_type": instance_type, | |
| "available_regions": sorted(r for r, v in data.items() if v["status"] == "available"), | |
| "regions": data, | |
| } | |
| def history_series(instance_type: str, region: str, hours: float = 48): | |
| _validate(instance_type, region) | |
| return { | |
| "instance_type": instance_type, | |
| "region": region, | |
| "series": history.series(instance_type, region, hours), | |
| } | |
| def probe(req: ProbeRequest, request: Request): | |
| auth.require_probe_rights(request) | |
| _validate(req.instance_type, req.region) | |
| regions = [req.region] if req.region else config.REGIONS | |
| log.info("probing %s in %s", req.instance_type, ",".join(regions)) | |
| return {"instance_type": req.instance_type, "results": _sweep(req.instance_type, regions)} | |
| def run(): | |
| uvicorn.run(app, host=os.environ.get("HOST", "127.0.0.1"), port=int(os.environ.get("PORT", "8300"))) | |
| if __name__ == "__main__": | |
| run() | |