Spaces:
Running on Zero
Running on Zero
File size: 8,816 Bytes
093748c 0ba0d88 093748c 860cace 093748c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | """Start-up of the AI server on the Space: weights, helper environments and settings, before the backend starts.
The Space's disk does not survive a restart, so every start (about 1-2 minutes, in parallel):
* downloads the weights at fixed revisions: projectxddi/bindsite-weights (AdaBind checkpoints, ESM-IF1, baselines,
precomputed ESMFold structures) and ESM-2 / ProtT5 from their own repositories (byte-identical to training);
* builds the two helper environments of the backend with uv, at the exact versions verified on the 4090 machine
(docker/constraints-*.txt): ESM-IF (Python 3.10, torch 2.4.1 CPU, numpy<2) and the baselines (Python 3.11,
torch 2.2.2 CPU);
* fetches the IP-location file from the private bucket (the backup machine refreshes it monthly);
* restores the visit / prediction log from the bucket (done by the backend's Tracker).
Then it sets the BINDSITE_* variables the backend reads (backend/app/config.py). Everything runs on the CPU.
"""
import logging
import os
import shutil
import subprocess
import sys
import tarfile
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
APP = Path(__file__).resolve().parent
ROOT = Path(os.environ.get("BINDSITE_HOME", "/tmp/bindsite"))
WEIGHTS = ROOT / "weights"
WEIGHTS_REPO = ("projectxddi/bindsite-weights", "8548592d5628e0cd8a970993f9a2005c6ede5f7a")
FOUNDATION = { # foundation_models/MANIFEST.json
"esm2_t33_650M_UR50D": ("facebook/esm2_t33_650M_UR50D", "08e4846e537177426273712802403f7ba8261b6c",
["config.json", "special_tokens_map.json", "tokenizer_config.json", "vocab.txt",
"model.safetensors"]),
"prot_t5_xl_half_uniref50-enc": ("Rostlab/prot_t5_xl_half_uniref50-enc", "94a6abc029ae13029317b140b7424e012bf8dfbf",
["config.json", "special_tokens_map.json", "spiece.model", "tokenizer_config.json",
"pytorch_model.bin"]),
}
BUCKET = "projectxddi/bindsite-logs"
STATIC_SITE = "https://projectproteinbinding-bindsite-explorer.static.hf.space"
log = logging.getLogger("bindsite.bootstrap")
report: dict = {} # step -> {"seconds": .., "message": ..}; shown on the admin page
def _step(name):
def wrap(fn):
def run(*args, **kwargs):
started = time.perf_counter()
try:
message = fn(*args, **kwargs) or "ok"
report[name] = {"status": "ok", "seconds": round(time.perf_counter() - started, 1), "message": message}
except Exception as exc:
report[name] = {"status": "error", "seconds": round(time.perf_counter() - started, 1),
"message": f"{type(exc).__name__}: {exc}"}
log.exception("Start-up step %s failed", name)
log.info("Start-up step %s: %s", name, report[name])
return run
return wrap
@_step("weights")
def download_weights() -> str:
from huggingface_hub import snapshot_download
repo, revision = WEIGHTS_REPO
snapshot_download(repo, revision=revision, local_dir=WEIGHTS, max_workers=16)
with tarfile.open(WEIGHTS / "esmfold_files.tar.gz") as tar:
tar.extractall(WEIGHTS, filter="data")
hub = WEIGHTS / "torch_home" / "hub" / "checkpoints"
hub.mkdir(parents=True, exist_ok=True)
link = hub / "esm_if1_gvp4_t16_142M_UR50.pt"
if not link.exists():
link.symlink_to(WEIGHTS / "esm_if1" / "esm_if1_gvp4_t16_142M_UR50.pt")
for name, (repo, revision, files) in FOUNDATION.items():
snapshot_download(repo, revision=revision, allow_patterns=files, local_dir=WEIGHTS / name, max_workers=8)
n = len(list((WEIGHTS / "esmfold_files").glob("*.pdb")))
return f"{WEIGHTS_REPO[0]}@{WEIGHTS_REPO[1][:8]} + ESM-2 + ProtT5, {n} precomputed structures"
def _uv(*args: str) -> None:
out = subprocess.run(["uv", *args], capture_output=True, text=True, timeout=1200)
if out.returncode != 0:
raise RuntimeError(f"uv {' '.join(args[:3])} ...: {(out.stdout + out.stderr).strip()[-600:]}")
@_step("esmif environment")
def build_esmif() -> str:
venv = ROOT / "venv-esmif"
py = str(venv / "bin" / "python")
_uv("venv", str(venv), "--python", "3.10.22", "--quiet")
_uv("pip", "install", "--quiet", "--python", py, "torch==2.4.1", "--index-url", "https://download.pytorch.org/whl/cpu")
_uv("pip", "install", "--quiet", "--python", py, "torch-scatter==2.1.2", "-f",
"https://data.pyg.org/whl/torch-2.4.0+cpu.html")
_uv("pip", "install", "--quiet", "--python", py, "-r", str(APP / "backend" / "requirements-esmif.txt"),
"-c", str(APP / "docker" / "constraints-esmif.txt"))
return py
@_step("baselines environment")
def build_baselines() -> str:
venv = ROOT / "venv-baselines"
py = str(venv / "bin" / "python")
_uv("venv", str(venv), "--python", "3.11", "--quiet")
_uv("pip", "install", "--quiet", "--python", py, "torch==2.2.2", "--index-url", "https://download.pytorch.org/whl/cpu")
_uv("pip", "install", "--quiet", "--python", py, "-r", str(APP / "backend" / "requirements-baselines.txt"),
"-c", str(APP / "docker" / "constraints-baselines.txt"))
return py
@_step("ip location")
def download_geoip() -> str:
from huggingface_hub import download_bucket_files
target = ROOT / "dbip-city-lite.mmdb"
download_bucket_files(BUCKET, files=[("geoip/dbip-city-lite.mmdb", str(target))], token=os.environ.get("HF_TOKEN"))
if not target.exists():
raise FileNotFoundError(f"geoip/dbip-city-lite.mmdb not in bucket {BUCKET}")
return f"{target.stat().st_size / 1e6:.0f} MB"
def baselines_dir() -> Path:
"""Code from this repository + checkpoints from the weights repo, in the layout baseline_worker.py expects."""
root = ROOT / "baselines"
pairs = {"bindEmbed21DL/src": APP / "baselines_2124" / "bindEmbed21DL" / "src",
"bindEmbed21DL/trained_models": WEIGHTS / "baselines" / "bindEmbed21DL" / "trained_models",
"bindNode24/src": APP / "baselines_2124" / "bindNode24" / "src",
"bindNode24/trained_model_dicts": WEIGHTS / "baselines" / "bindNode24" / "trained_model_dicts"}
for rel, target in pairs.items():
link = root / rel
link.parent.mkdir(parents=True, exist_ok=True)
if not link.is_symlink() and not link.exists():
link.symlink_to(target)
return root
def prepare() -> None:
started = time.perf_counter()
ROOT.mkdir(parents=True, exist_ok=True)
if os.environ.get("BINDSITE_SKIP_DOWNLOADS") == "1": # local check of the pages with the mock predictor
report["skipped"] = {"status": "warning", "seconds": 0, "message": "BINDSITE_SKIP_DOWNLOADS=1"}
else:
if shutil.which("uv") is None:
raise RuntimeError("uv is missing (requirements.txt)")
with ThreadPoolExecutor(max_workers=4) as pool:
for job in (download_weights, build_esmif, build_baselines, download_geoip):
pool.submit(job)
report["total"] = {"status": "ok", "seconds": round(time.perf_counter() - started, 1), "message": ""}
admin_password = os.environ.get("ADMIN_PASSWORD", "")
env = {
"BINDSITE_WEIGHTS_DIR": str(WEIGHTS),
"BINDSITE_FINAL_MODEL_DIR": str(WEIGHTS / "final_model"),
"BINDSITE_GOLDEN_DIR": str(APP / "adabind" / "golden_set"),
"BINDSITE_ESMIF_PYTHON": str(ROOT / "venv-esmif" / "bin" / "python"),
"BINDSITE_BASELINES_PYTHON": str(ROOT / "venv-baselines" / "bin" / "python"),
"BINDSITE_BASELINES_DIR": str(baselines_dir()),
"BINDSITE_CACHE_DIR": str(ROOT / "cache"),
"BINDSITE_LOG_DIR": str(ROOT / "logs"),
"BINDSITE_DEVICE": "cpu", # outside @spaces.GPU, CUDA is only emulated on ZeroGPU
"BINDSITE_ESMFOLD_LOCAL": "off", # ESMFold comes from the backup machine (BINDSITE_WORKER_KEY)
"BINDSITE_CORS_ORIGINS": f"{STATIC_SITE},http://localhost:4200",
"BINDSITE_TRACKING_DB": str(ROOT / "tracking.sqlite"),
"BINDSITE_TRACKING_BUCKET": BUCKET,
"BINDSITE_GEOIP_DB": str(ROOT / "dbip-city-lite.mmdb"),
"BINDSITE_WORKER_KEY": os.environ.get("WORKER_KEY", ""),
"BINDSITE_ADMIN_KEY": admin_password,
"BINDSITE_RATE_LIMIT": os.environ.get("BINDSITE_RATE_LIMIT", "120"),
"HF_HUB_OFFLINE": "0", # the tracking log is copied to the bucket through the Hub API
}
for key, value in env.items():
os.environ.setdefault(key, value)
log.info("Start-up done in %.0f s: %s", time.perf_counter() - started,
{k: v["status"] for k, v in report.items()})
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
prepare()
|