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,
    )