isaac commited on
Commit
5d4272a
·
1 Parent(s): a960052

feat: add INFERENCE_BACKEND provider switch, Space stays as API

Browse files
Files changed (3) hide show
  1. app.py +13 -3
  2. backend/__init__.py +0 -0
  3. backend/inference_backend.py +70 -0
app.py CHANGED
@@ -38,6 +38,7 @@ import spaces # noqa: E402
38
  import gradio as gr # noqa: E402
39
  from huggingface_hub import snapshot_download # noqa: E402
40
  from inference_demo import initialize, evaluate # noqa: E402
 
41
 
42
  REPO_ID = "radar-generalist/RADAR"
43
  CHECKPOINT_NAME = "checkpoint_radar_pretrain.pth"
@@ -95,7 +96,12 @@ def _score_case(tmpdir, outdir):
95
 
96
 
97
  def diagnose(ct_files):
98
- """Gradio entry (CPU): stage uploads, then score under a short GPU reservation."""
 
 
 
 
 
99
  items = ct_files if isinstance(ct_files, list) else [ct_files]
100
  paths = [p if isinstance(p, str) else p.name for p in items]
101
  tmpdir = tempfile.mkdtemp(prefix="radar_case_")
@@ -128,7 +134,9 @@ def diagnose(ct_files):
128
  _series_to_nifti(staged, tmpdir)
129
  finally:
130
  shutil.rmtree(staged, ignore_errors=True)
131
- return _score_case(tmpdir, outdir)
 
 
132
  finally:
133
  shutil.rmtree(tmpdir, ignore_errors=True)
134
  shutil.rmtree(outdir, ignore_errors=True)
@@ -147,8 +155,10 @@ demo = gr.Interface(
147
  description=(
148
  "Expert-level generalist AI for contrast-enhanced abdominal CT "
149
  "(18 structures, 146 findings). Non-commercial research demo "
150
- "(CC BY-NC-SA 4.0); assistance tool, not a diagnosis."
 
151
  ),
 
152
  )
153
 
154
  if __name__ == "__main__":
 
38
  import gradio as gr # noqa: E402
39
  from huggingface_hub import snapshot_download # noqa: E402
40
  from inference_demo import initialize, evaluate # noqa: E402
41
+ from backend.inference_backend import VastAIBackend, ZeroGPUBackend, get_backend_kind # noqa: E402
42
 
43
  REPO_ID = "radar-generalist/RADAR"
44
  CHECKPOINT_NAME = "checkpoint_radar_pretrain.pth"
 
96
 
97
 
98
  def diagnose(ct_files):
99
+ """Gradio entry (CPU): stage uploads, then score on the active backend."""
100
+ kind = get_backend_kind()
101
+ if kind not in ("zerogpu", "vastai"):
102
+ raise gr.Error("unknown INFERENCE_BACKEND")
103
+ if kind == "vastai":
104
+ VastAIBackend().ensure_available() # fail before mkdtemp/staging
105
  items = ct_files if isinstance(ct_files, list) else [ct_files]
106
  paths = [p if isinstance(p, str) else p.name for p in items]
107
  tmpdir = tempfile.mkdtemp(prefix="radar_case_")
 
134
  _series_to_nifti(staged, tmpdir)
135
  finally:
136
  shutil.rmtree(staged, ignore_errors=True)
137
+ if kind == "vastai":
138
+ return VastAIBackend().score_case(tmpdir, outdir)
139
+ return ZeroGPUBackend(_score_case).score_case(tmpdir, outdir)
140
  finally:
141
  shutil.rmtree(tmpdir, ignore_errors=True)
142
  shutil.rmtree(outdir, ignore_errors=True)
 
155
  description=(
156
  "Expert-level generalist AI for contrast-enhanced abdominal CT "
157
  "(18 structures, 146 findings). Non-commercial research demo "
158
+ "(CC BY-NC-SA 4.0); assistance tool, not a diagnosis. "
159
+ "Full UI: https://radar-ct.pages.dev — this Space remains as API."
160
  ),
161
+ api_name="diagnose",
162
  )
163
 
164
  if __name__ == "__main__":
backend/__init__.py ADDED
File without changes
backend/inference_backend.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Provider switch for RADAR scoring backends.
2
+
3
+ ZeroGPU remains the active backend; Vast.ai is a stub holding config fields
4
+ until a Serverless endpoint is provisioned (see backend/vastai/). Frontend
5
+ code branches on the same kind strings ("zerogpu" | "vastai").
6
+
7
+ Import cost: stdlib only. Importing this module must never pull torch,
8
+ CUDA, or app state — ZeroGPUBackend resolves app._score_case lazily.
9
+ """
10
+
11
+ import os
12
+ from typing import Protocol
13
+
14
+
15
+ class InferenceBackend(Protocol):
16
+ def score_case(self, tmpdir: str, outdir: str) -> tuple[dict, object]:
17
+ """Score one staged case; returns (scores dict, findings DataFrame)."""
18
+ ...
19
+
20
+
21
+ def get_backend_kind() -> str:
22
+ """Active backend from env; unknown values are validated by callers."""
23
+ return (os.getenv("INFERENCE_BACKEND", "zerogpu").strip().lower() or "zerogpu")
24
+
25
+
26
+ class ZeroGPUBackend:
27
+ """Active backend: delegates to app._score_case (the @spaces.GPU step).
28
+
29
+ score_fn is injected by app.diagnose so this module never imports app
30
+ at load time (avoids a circular import and keeps CUDA init in app).
31
+ """
32
+
33
+ def __init__(self, score_fn=None):
34
+ self._score_fn = score_fn
35
+
36
+ def score_case(self, tmpdir: str, outdir: str) -> tuple[dict, object]:
37
+ fn = self._score_fn
38
+ if fn is None: # pragma: no cover — production path always injects
39
+ from app import _score_case as fn
40
+ self._score_fn = fn
41
+ return fn(tmpdir, outdir)
42
+
43
+
44
+ class VastAIBackend:
45
+ """Future Vast.ai Serverless backend — not enabled yet.
46
+
47
+ Holds the config surface the later cutover needs; score_case fails fast
48
+ so a misconfigured INFERENCE_BACKEND=vastai can never silently hang.
49
+ Real key lives server-side in VAST_API_KEY env, never in frontend code.
50
+ """
51
+
52
+ NOT_ENABLED = "Vast.ai backend not enabled: set INFERENCE_BACKEND=zerogpu"
53
+
54
+ def __init__(
55
+ self,
56
+ endpoint: str = "",
57
+ api_key_env: str = "VAST_API_KEY",
58
+ ):
59
+ self.endpoint = endpoint or os.getenv("VAST_ENDPOINT", "")
60
+ self.api_key_env = api_key_env
61
+
62
+ def ensure_available(self) -> None:
63
+ """Fail fast before any staging when this backend is selected."""
64
+ import gradio as gr
65
+
66
+ raise gr.Error(self.NOT_ENABLED)
67
+
68
+ def score_case(self, tmpdir: str, outdir: str) -> tuple[dict, object]:
69
+ self.ensure_available()
70
+ raise RuntimeError("unreachable: Vast.ai backend is not enabled")