Spaces:
Sleeping
Sleeping
File size: 5,727 Bytes
eb23a18 | 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 | """Hugging Face Spaces entry point for the Proteoform Analyzer (ZeroGPU-ready).
This file lives at the *repository root* of the Space. The actual application
code is the ``proteoform_analyzer`` Python package in the sibling folder of the
same name. Hugging Face Spaces automatically runs ``app.py`` at the repo root,
so this module:
1. imports ``spaces`` *before* torch / CUDA (required by ZeroGPU) and installs
a no-op shim when ``spaces`` is not available (so the same file runs
unchanged on a normal machine, in CI, or in a local dry-run);
2. puts the repo root on ``sys.path`` and imports the package as
``proteoform_analyzer`` so the package's relative imports resolve;
3. builds the Gradio Blocks app, enables the request queue, and launches with
ZeroGPU-safe settings (``ssr_mode=False``, ``server_name="0.0.0.0"``).
ZeroGPU notes
-------------
ZeroGPU allocates a physical GPU only for the duration of a function decorated
with ``@spaces.GPU``. In this app the only work that runs a torch model *in the
same process* is the ESM2 sequence embedding (UMAP tab) and the ESM2 zero-shot
ddG scorer; those are decorated so they transparently use CUDA on a ZeroGPU
Space and CPU everywhere else. The heavier structure tools (Boltz-2 folding /
docking, RFAntibody, BoltzGen, DiffSBDD) run as *external APIs or subprocesses*
and are therefore not GPU-accelerated by ZeroGPU in-process; configure the Boltz
API key (Space secret ``BOLTZ_API_KEY``) for those, or they skip cleanly. See
DEPLOY_HF_ZEROGPU.md for the full matrix.
"""
from __future__ import annotations
import os
import sys
# ---------------------------------------------------------------------------
# 1) ZeroGPU: ``import spaces`` MUST happen before any torch / CUDA import.
# When the ``spaces`` package is unavailable (local machine, CI, dry-run),
# install a minimal shim that provides a no-op ``@spaces.GPU`` decorator so
# the identical codebase runs without Hugging Face infrastructure.
# ---------------------------------------------------------------------------
try:
import spaces # noqa: F401 (import-for-side-effects + decorator source)
_HAS_SPACES = True
except Exception: # pragma: no cover - exercised only off-platform
import types
def _gpu_noop(*dargs, **dkwargs):
"""Mimic ``spaces.GPU``: usable both bare and with keyword arguments.
Supports ``@spaces.GPU``, ``@spaces.GPU()`` and
``@spaces.GPU(duration=...)`` without requiring the real package.
"""
# Called as a bare decorator: @spaces.GPU
if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
return dargs[0]
# Called with arguments: @spaces.GPU(duration=120)
def _wrap(fn):
return fn
return _wrap
spaces = types.ModuleType("spaces")
spaces.GPU = _gpu_noop # type: ignore[attr-defined]
sys.modules["spaces"] = spaces
_HAS_SPACES = False
# Make the ESM2 in-process steps GPU-aware only when a real ZeroGPU runtime is
# present. The package reads this env var (see core/steps/_device.py) to decide
# whether to move torch models onto CUDA. Off-platform it stays "cpu" so the
# dry-run and local installs behave exactly as before.
os.environ.setdefault("PROTEOFORM_ZEROGPU", "1" if _HAS_SPACES else "0")
# ---------------------------------------------------------------------------
# 2) Import the application package. On a Space the repo root holds this app.py
# plus the proteoform_analyzer/ package folder; adding the repo root to
# sys.path lets ``import proteoform_analyzer`` resolve with its relative
# imports intact.
# ---------------------------------------------------------------------------
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
from proteoform_analyzer.gui import build_app # noqa: E402
# ---------------------------------------------------------------------------
# 3) Wrap the ESM2 in-process torch entry points with @spaces.GPU so ZeroGPU
# allocates a GPU for their duration. This is done by monkey-patching the
# step modules *after* import; it is a no-op decorator off-platform.
# ---------------------------------------------------------------------------
def _install_gpu_wrappers() -> None:
try:
from proteoform_analyzer.core.steps import esm2 as _esm2
if hasattr(_esm2, "_embed_sequences") and not getattr(
_esm2._embed_sequences, "_zerogpu_wrapped", False
):
_wrapped = spaces.GPU(duration=120)(_esm2._embed_sequences)
_wrapped._zerogpu_wrapped = True # type: ignore[attr-defined]
_esm2._embed_sequences = _wrapped
except Exception:
pass
try:
from proteoform_analyzer.core.steps import ddg as _ddg
if hasattr(_ddg, "_esm2_zeroshot_ddg") and not getattr(
_ddg._esm2_zeroshot_ddg, "_zerogpu_wrapped", False
):
_wrapped = spaces.GPU(duration=120)(_ddg._esm2_zeroshot_ddg)
_wrapped._zerogpu_wrapped = True # type: ignore[attr-defined]
_ddg._esm2_zeroshot_ddg = _wrapped
except Exception:
pass
_install_gpu_wrappers()
# Build the Gradio app at import time so ``gradio``'s auto-reload and the Spaces
# runtime can both find a module-level ``demo``/``app`` object.
demo = build_app()
demo.queue() # required for long-running pipeline + ZeroGPU scheduling
app = demo # alias some tooling looks for
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", "7860")),
ssr_mode=False, # ZeroGPU / Spaces requirement
show_error=True,
)
|