"""Enroll the v1 model line-up and write `arena/registry.json`. Every model goes through the same `runtime.enroll` path a user would: the revision is pinned to an immutable sha, a smoke test runs, and the result is recorded. Nothing is registered that has not actually produced a forecast. Latency is measured here too, and a CPU-tier model that misses its budget is demoted to GPU tier in the registry rather than left to time out in front of someone. The measurement is only meaningful on the hardware it was taken on, so the machine is recorded alongside the number -- run this on the Space to get numbers that describe the Space. HF_TOKEN=$(cat ../../key.txt) python scripts/bootstrap_registry.py python scripts/bootstrap_registry.py --dry-run # no writes """ from __future__ import annotations import argparse import json import logging import os import platform import sys import time from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from src import config, runtime # noqa: E402 from src.adapters import get_adapter # noqa: E402 from src.store import ArenaStore, now_utc # noqa: E402 logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s") log = logging.getLogger("bootstrap") # The v1 line-up. Order is the order they appear in the UI. SEED_MODELS = [ ("baseline", "baseline/random-walk"), ("baseline", "baseline/drift"), ("baseline", "baseline/bootstrap"), ("chronos", "amazon/chronos-bolt-tiny"), ("chronos", "amazon/chronos-bolt-mini"), ("chronos", "amazon/chronos-bolt-small"), ("chronos", "amazon/chronos-bolt-base"), ("timesfm", "google/timesfm-2.5-200m-pytorch"), ("kronos", "NeoQuasar/Kronos-mini"), ("kronos", "NeoQuasar/Kronos-small"), ("kronos", "NeoQuasar/Kronos-base"), ] # The forecast the budget is defined against: a default 1h forecast. BUDGET_HORIZON = 24 BUDGET_SAMPLES = config.DEFAULT_N_SAMPLES def _synthetic(n=config.DEFAULT_CONTEXT_BARS): rng = np.random.default_rng(7) close = 50000.0 * np.exp(np.cumsum(rng.normal(0, 0.01, n))) openp = np.concatenate([[50000.0], close[:-1]]) return pd.DataFrame({ "ts": pd.date_range("2025-01-01", periods=n, freq="1h", tz="UTC"), "open": openp, "high": np.maximum(openp, close) * 1.002, "low": np.minimum(openp, close) * 0.998, "close": close, "volume": np.abs(rng.normal(1000, 200, n)) + 1.0, }) def measure(family: str, model_id: str, context) -> dict: """Cold and warm latency for a default forecast.""" adapter = get_adapter(family, model_id) # Measure on the context the app actually sends, not on the model's own # ceiling: the cost is roughly linear in context length, so measuring at # `max_context` would report a number no user ever waits for. context = context.iloc[-min(len(context), config.DEFAULT_CONTEXT_BARS):] t0 = time.time() adapter.load() load_s = time.time() - t0 t0 = time.time() adapter.predict(context, horizon=BUDGET_HORIZON, n_samples=BUDGET_SAMPLES, seed=0) cold_s = time.time() - t0 warm = [] for _ in range(3): t0 = time.time() adapter.predict(context, horizon=BUDGET_HORIZON, n_samples=BUDGET_SAMPLES, seed=0) warm.append(time.time() - t0) warm_s = float(np.median(warm)) declared = adapter.capabilities().hardware # "Cold" for a user is load plus first forecast: the weights are not in # memory when they arrive. total_cold = load_s + cold_s misses = declared == "cpu" and ( total_cold > config.CPU_BUDGET_COLD_S or warm_s > config.CPU_BUDGET_WARM_S) return { "declared_hardware": declared, "hardware": "gpu" if misses else declared, "demoted": bool(misses), "load_s": round(load_s, 3), "cold_s": round(total_cold, 3), "warm_s": round(warm_s, 3), "horizon": BUDGET_HORIZON, "n_samples": BUDGET_SAMPLES, "machine": f"{platform.system()}-{platform.machine()}-py{platform.python_version()}", "on_space": bool(os.environ.get("SPACE_ID")), "measured_ts": now_utc().isoformat(), } def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true", help="measure and print, write nothing") ap.add_argument("--skip-measure", action="store_true", help="enroll without a latency run (declared tiers stand)") ap.add_argument("--local-root", default=None) args = ap.parse_args() offline = args.dry_run or not os.environ.get("HF_TOKEN") store = ArenaStore(local_root=args.local_root, offline=offline) registry = store.get_registry() context = _synthetic() for family, model_id in SEED_MODELS: log.info("enrolling %s (%s)", model_id, family) outcome = runtime.enroll(store, family, model_id, enrolled_by="bit-trading-company", registry=registry) if not outcome.ok: log.error(" refused: %s", outcome.message) continue if outcome.already: log.info(" already enrolled at this revision") entry = registry["models"][outcome.model_slug] if not args.skip_measure: try: latency = measure(family, model_id, context) entry["latency"] = latency entry["capabilities"]["hardware"] = latency["hardware"] flag = " DEMOTED to gpu" if latency["demoted"] else "" log.info(" cold=%.1fs warm=%.2fs tier=%s%s", latency["cold_s"], latency["warm_s"], latency["hardware"], flag) except Exception as e: log.error(" latency run failed: %s", e) entry["latency"] = {"error": str(e)[:200]} registry["updated_ts"] = now_utc().isoformat() registry["version"] = 1 if args.dry_run: print(json.dumps(registry, indent=2)[:4000]) log.info("dry run: nothing written") return 0 store.put_registry(registry) pushed = store.flush("arena: bootstrap model registry") log.info("registry written (%d file(s) pushed)", pushed) return 0 if __name__ == "__main__": raise SystemExit(main())