Spaces:
Sleeping
Sleeping
| """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, | |
| ) | |