Harshith Reddy commited on
Commit
fde793b
·
1 Parent(s): 91a070d

Add Gradio head-to-head baseline (V1/V2/V3) and bench Space entrypoint

Browse files

- app_gradio_baseline.py: three idiomatic Gradio variants (V1/V2/V3)
exposing the three failure modes claimed in the paper.
- bench_gradio_baseline.py: headless 9-call benchmark across all variants,
emits CSV + per-variant summary table.
- app_bench_space.py: HF Space entrypoint with browser UI for
reproducibility (used by the voxpixel-baseline Space).
- prepare_runpod.sh: alternative RunPod bootstrap script.
- Dockerfile: switch CMD to use APP_FILE env var (defaults to app.py;
backward compatible with the production Space).
- .gitignore: exclude patient data (Test Images/), build artifacts,
benchmark outputs, and Paper.tex.

.gitignore CHANGED
@@ -1,3 +1,24 @@
1
  SRMA-Mamba/selective_scan/build/
2
  *.so
3
  *.o
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  SRMA-Mamba/selective_scan/build/
2
  *.so
3
  *.o
4
+
5
+ # Patient data and benchmark outputs (must not be committed)
6
+ Test Images/
7
+ gradio_baseline_results.csv
8
+ v2_pred.nii.gz
9
+ v3_pred.nii.gz
10
+
11
+ # Python build/cache artifacts
12
+ __pycache__/
13
+ *.pyc
14
+ *.pyo
15
+
16
+ # Research paper / LaTeX build artifacts (kept local only)
17
+ Paper.tex
18
+ *.aux
19
+ *.log
20
+ *.out
21
+ *.synctex.gz
22
+ *.bbl
23
+ *.blg
24
+ *.toc
Dockerfile CHANGED
@@ -95,4 +95,5 @@ PY
95
 
96
  EXPOSE 7860
97
 
98
- CMD ["python", "app.py"]
 
 
95
 
96
  EXPOSE 7860
97
 
98
+ ENV APP_FILE=app.py
99
+ CMD ["sh", "-c", "exec python ${APP_FILE}"]
README.md CHANGED
@@ -1086,6 +1086,66 @@ If you use LiverProfile AI in your research, please cite:
1086
  }
1087
  ```
1088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  ## Disclaimer
1090
 
1091
  **Important**: This software is intended for **research purposes only**. It is not approved for clinical use or diagnostic purposes without proper validation and regulatory approval. Always consult with qualified medical professionals for clinical decision-making.
 
1086
  }
1087
  ```
1088
 
1089
+ ## Reproducing the Gradio head-to-head baseline (paper Section V)
1090
+
1091
+ The accompanying paper compares VoxPixel against three idiomatic Gradio
1092
+ configurations on the same SRMA-Mamba checkpoints, the same volumes, and
1093
+ the same GPU. The comparison is fully reproducible from this repository.
1094
+
1095
+ ### Files
1096
+
1097
+ - `app_gradio_baseline.py` — three Gradio variants in one file:
1098
+ - **V1** (`gr.Image`): demonstrates that NIfTI ingestion is not
1099
+ supported by the default image component.
1100
+ - **V2** (`gr.File` + naive single-shot full-volume forward pass):
1101
+ demonstrates CUDA OOM on full 3D volumes when no sliding-window
1102
+ aggregator is wired in.
1103
+ - **V3** (`gr.File` + manually-wired MONAI `SlidingWindowInferer`):
1104
+ demonstrates that even with sliding-window inference, the naive
1105
+ Gradio I/O contract drops the NIfTI affine, biasing the reported
1106
+ liver volume in ml.
1107
+ - `bench_gradio_baseline.py` — headless benchmark that runs the same
1108
+ 9-call protocol used for VoxPixel (Table III) for each variant and
1109
+ writes a CSV.
1110
+
1111
+ ### Launch a single variant interactively
1112
+
1113
+ ```bash
1114
+ python app_gradio_baseline.py --variant v1 # try uploading .nii.gz; rejected
1115
+ python app_gradio_baseline.py --variant v2 # full-volume single-shot
1116
+ python app_gradio_baseline.py --variant v3 # + sliding window
1117
+ ```
1118
+
1119
+ ### Run the 9-call benchmark across all three variants
1120
+
1121
+ ```bash
1122
+ python bench_gradio_baseline.py \
1123
+ --volumes ./test_volumes/56.nii.gz \
1124
+ ./test_volumes/58.nii.gz \
1125
+ ./test_volumes/60.nii.gz \
1126
+ --trials 3 \
1127
+ --modality T1 \
1128
+ --out gradio_baseline_results.csv
1129
+ ```
1130
+
1131
+ This produces 27 rows (3 variants × 3 volumes × 3 trials) in
1132
+ `gradio_baseline_results.csv`, plus a printed per-variant summary
1133
+ table whose columns map directly to the head-to-head table in the
1134
+ paper:
1135
+
1136
+ | Column | Meaning |
1137
+ | --- | --- |
1138
+ | `status` | `OK`, `OOM`, `UI_REJECT`, or `ERROR` |
1139
+ | `runtime_s` | End-to-end inference time |
1140
+ | `peak_vram_gb` | Peak `torch.cuda.max_memory_allocated()` |
1141
+ | `naive_volume_ml` | Liver volume computed under the identity affine the naive Gradio app writes to disk (the value a downstream user would see) |
1142
+ | `true_volume_ml` | Liver volume computed under the original NIfTI affine (the spatially-correct value, which Gradio loses) |
1143
+ | `voxpixel_volume_ml` | The corresponding VoxPixel value from Table III |
1144
+ | `abs_err_naive_vs_voxpixel_ml` | Affine-loss bias in ml |
1145
+
1146
+ Run on the same GPU as your VoxPixel measurements so the comparison is
1147
+ apples-to-apples.
1148
+
1149
  ## Disclaimer
1150
 
1151
  **Important**: This software is intended for **research purposes only**. It is not approved for clinical use or diagnostic purposes without proper validation and regulatory approval. Always consult with qualified medical professionals for clinical decision-making.
__pycache__/app.cpython-312.pyc DELETED
Binary file (26.8 kB)
 
__pycache__/config.cpython-312.pyc DELETED
Binary file (7.63 kB)
 
__pycache__/inference.cpython-312.pyc DELETED
Binary file (74.5 kB)
 
__pycache__/model.cpython-312.pyc DELETED
Binary file (8.58 kB)
 
__pycache__/model_loader.cpython-312.pyc DELETED
Binary file (17.1 kB)
 
__pycache__/processing.cpython-312.pyc DELETED
Binary file (58.1 kB)
 
app_bench_space.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Space entrypoint: head-to-head Gradio baseline benchmark
3
+ for the VoxPixel paper (Section V).
4
+
5
+ This is a SEPARATE Space from the production VoxPixel app. Same repo,
6
+ same Dockerfile, same dependencies; the entrypoint is selected by the
7
+ APP_FILE environment variable in the Space's "Variables and secrets"
8
+ settings (set APP_FILE=app_bench_space.py for this Space).
9
+
10
+ Purpose: let any reviewer reproduce the V1/V2/V3 vs. VoxPixel head-to-
11
+ head comparison from the browser. Upload one or more NIfTI volumes,
12
+ choose how many trials per volume, click "Run benchmark"; the app runs
13
+ the same protocol used in Table III of the paper, displays a summary
14
+ inline, and writes a downloadable CSV.
15
+
16
+ Failure modes the experiment exposes (paper Section II / V):
17
+ V1: gr.Image rejects .nii / .nii.gz at upload (NIfTI ingestion).
18
+ V2: single-shot full-volume forward pass -> CUDA OOM (no sliding window).
19
+ V3: + manually-wired MONAI sliding window, but the output mask
20
+ is written with affine = identity, biasing liver volume in ml
21
+ (no spatial-metadata round-trip in the Gradio I/O contract).
22
+ """
23
+
24
+ import csv
25
+ import gc
26
+ import os
27
+ import sys
28
+ import tempfile
29
+ from pathlib import Path
30
+
31
+ import gradio as gr
32
+ import torch
33
+
34
+ from app_gradio_baseline import predict_v2, predict_v3
35
+ from bench_gradio_baseline import (
36
+ VOXPIXEL_TRUTH_ML,
37
+ run_v1,
38
+ run_with_safety,
39
+ )
40
+
41
+
42
+ CSV_FIELDS = [
43
+ "variant", "trial", "volume", "size_mb",
44
+ "status", "runtime_s", "peak_vram_gb",
45
+ "voxel_count", "naive_volume_ml", "true_volume_ml",
46
+ "voxpixel_volume_ml", "abs_err_naive_vs_voxpixel_ml",
47
+ "error",
48
+ ]
49
+
50
+
51
+ def _file_path(file_obj) -> str:
52
+ """Gradio 4.x returns a list of objects with .name; older versions
53
+ sometimes pass a path string directly. Handle both defensively."""
54
+ return getattr(file_obj, "name", str(file_obj))
55
+
56
+
57
+ def _row(variant: str, trial: int, vol_path: str, res: dict) -> dict:
58
+ vol_name = Path(vol_path).name
59
+ size_mb = round(os.path.getsize(vol_path) / (1024 ** 2), 2)
60
+ naive_ml = res.get("naive_volume_ml")
61
+ voxpixel = VOXPIXEL_TRUTH_ML.get(vol_name)
62
+ if (voxpixel is not None and naive_ml is not None
63
+ and naive_ml == naive_ml): # NaN check
64
+ abs_err = abs(naive_ml - voxpixel)
65
+ else:
66
+ abs_err = float("nan")
67
+ return {
68
+ "variant": variant,
69
+ "trial": trial,
70
+ "volume": vol_name,
71
+ "size_mb": size_mb,
72
+ "status": res.get("status", "?"),
73
+ "runtime_s": round(res.get("runtime_s", 0.0), 3),
74
+ "peak_vram_gb": round(res.get("peak_vram_gb", 0.0), 2),
75
+ "voxel_count": res.get("voxel_count", 0),
76
+ "naive_volume_ml": naive_ml,
77
+ "true_volume_ml": res.get("true_volume_ml"),
78
+ "voxpixel_volume_ml": voxpixel,
79
+ "abs_err_naive_vs_voxpixel_ml": abs_err,
80
+ "error": res.get("error", ""),
81
+ }
82
+
83
+
84
+ def _summary_markdown(rows: list) -> str:
85
+ by_variant: dict = {}
86
+ for r in rows:
87
+ v = r["variant"]
88
+ d = by_variant.setdefault(v, {
89
+ "n": 0, "ok": 0, "oom": 0, "ui_reject": 0, "err": 0,
90
+ "rt_sum": 0.0, "vram_max": 0.0,
91
+ "naive_err_sum": 0.0, "naive_err_n": 0,
92
+ })
93
+ d["n"] += 1
94
+ s = r["status"]
95
+ if s == "OK":
96
+ d["ok"] += 1
97
+ d["rt_sum"] += r["runtime_s"]
98
+ d["vram_max"] = max(d["vram_max"], r["peak_vram_gb"])
99
+ err = r["abs_err_naive_vs_voxpixel_ml"]
100
+ if err == err:
101
+ d["naive_err_sum"] += err
102
+ d["naive_err_n"] += 1
103
+ elif s == "OOM":
104
+ d["oom"] += 1
105
+ elif s == "UI_REJECT":
106
+ d["ui_reject"] += 1
107
+ else:
108
+ d["err"] += 1
109
+
110
+ lines = [
111
+ "### Per-variant summary",
112
+ "",
113
+ "| Variant | Success / Total | OOM | UI reject | Other err | "
114
+ "Mean runtime (s) | Peak VRAM (GB) | "
115
+ "Mean \\|naive − VoxPixel\\| (ml) |",
116
+ "|---|---|---|---|---|---|---|---|",
117
+ ]
118
+ for v, d in by_variant.items():
119
+ mean_rt = (d["rt_sum"] / d["ok"]) if d["ok"] else float("nan")
120
+ mean_err = (
121
+ (d["naive_err_sum"] / d["naive_err_n"])
122
+ if d["naive_err_n"] else float("nan")
123
+ )
124
+ lines.append(
125
+ f"| **{v}** | {d['ok']}/{d['n']} | {d['oom']} | {d['ui_reject']} "
126
+ f"| {d['err']} | {mean_rt:.2f} | {d['vram_max']:.2f} "
127
+ f"| {mean_err:.2f} |"
128
+ )
129
+ return "\n".join(lines)
130
+
131
+
132
+ def _table_data(rows: list) -> list:
133
+ return [
134
+ [r["variant"], r["trial"], r["volume"], r["size_mb"],
135
+ r["status"], r["runtime_s"], r["peak_vram_gb"],
136
+ r["voxel_count"], r["naive_volume_ml"], r["true_volume_ml"],
137
+ r["voxpixel_volume_ml"], r["abs_err_naive_vs_voxpixel_ml"]]
138
+ for r in rows
139
+ ]
140
+
141
+
142
+ def run_benchmark(files, trials: int, modality: str,
143
+ progress=gr.Progress()):
144
+ if not files:
145
+ return ("**Please upload at least one .nii / .nii.gz volume.**",
146
+ None, None, "")
147
+
148
+ paths = [_file_path(f) for f in files]
149
+ log_lines: list = []
150
+
151
+ progress(0.0, desc="Loading model...")
152
+ try:
153
+ from app_gradio_baseline import _get_model
154
+ _get_model(modality)
155
+ except Exception as e:
156
+ return (f"**Failed to load model: {e}**\n\n"
157
+ "Check that the Space hardware has a GPU and that "
158
+ "mamba_ssm / selective_scan_cuda_oflex built successfully.",
159
+ None, None, "")
160
+
161
+ log_lines.append(
162
+ f"Model loaded ({modality}). Hardware: "
163
+ f"{torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'}"
164
+ )
165
+
166
+ rows: list = []
167
+ runners = [
168
+ ("V1", lambda p, m: run_v1(p, m)),
169
+ ("V2", lambda p, m: run_with_safety(predict_v2, p, m)),
170
+ ("V3", lambda p, m: run_with_safety(predict_v3, p, m)),
171
+ ]
172
+ total_calls = len(paths) * trials * len(runners)
173
+ done = 0
174
+
175
+ for vol_path in paths:
176
+ vol_name = Path(vol_path).name
177
+ for variant, runner in runners:
178
+ for trial in range(1, int(trials) + 1):
179
+ msg = (f"[{variant}] trial {trial}/{int(trials)} on "
180
+ f"{vol_name}")
181
+ log_lines.append(msg)
182
+ progress(done / total_calls, desc=msg)
183
+ res = runner(vol_path, modality)
184
+ rows.append(_row(variant, trial, vol_path, res))
185
+ done += 1
186
+ if torch.cuda.is_available():
187
+ torch.cuda.empty_cache()
188
+ gc.collect()
189
+
190
+ csv_path = os.path.join(
191
+ tempfile.gettempdir(), "gradio_baseline_results.csv"
192
+ )
193
+ with open(csv_path, "w", newline="") as f:
194
+ w = csv.DictWriter(f, fieldnames=CSV_FIELDS)
195
+ w.writeheader()
196
+ w.writerows(rows)
197
+
198
+ summary_md = _summary_markdown(rows)
199
+ return summary_md, _table_data(rows), csv_path, "\n".join(log_lines)
200
+
201
+
202
+ with gr.Blocks(title="VoxPixel — Head-to-Head Gradio Baseline") as demo:
203
+ gr.Markdown("""
204
+ # VoxPixel — Head-to-Head Gradio Baseline (paper §V)
205
+
206
+ This Space reproduces the head-to-head experiment between three idiomatic
207
+ Gradio configurations and the production VoxPixel deployment, using the
208
+ **same SRMA-Mamba checkpoints** and the **same protocol** as Table III of
209
+ the paper.
210
+
211
+ | Variant | What it implements | Failure mode demonstrated |
212
+ | --- | --- | --- |
213
+ | **V1** | `gr.Image` interface | NIfTI ingestion not supported by Gradio's image components |
214
+ | **V2** | `gr.File` + nibabel + single-shot full-volume forward pass | CUDA OOM on full 3D volumes (no sliding window) |
215
+ | **V3** | `gr.File` + nibabel + manually-wired MONAI `SlidingWindowInferer` | Spatial metadata loss — output mask written with `affine = identity`, biasing liver volume in ml |
216
+
217
+ Upload one or more `.nii / .nii.gz` volumes, choose trials per volume, and
218
+ click **Run benchmark**. Three trials per volume across three volumes
219
+ reproduces the 9-call protocol used for VoxPixel.
220
+
221
+ Reference values for the affine-loss bias column come from VoxPixel
222
+ runs in the paper:
223
+ """)
224
+ gr.Markdown(
225
+ "\n".join(
226
+ f"- `{k}` → **{v:.2f} ml** (VoxPixel)"
227
+ for k, v in VOXPIXEL_TRUTH_ML.items()
228
+ )
229
+ )
230
+
231
+ with gr.Row():
232
+ with gr.Column(scale=2):
233
+ files = gr.File(
234
+ label="NIfTI volumes (.nii / .nii.gz)",
235
+ file_count="multiple",
236
+ file_types=[".nii", ".gz"],
237
+ )
238
+ trials = gr.Slider(
239
+ label="Trials per volume",
240
+ minimum=1, maximum=5, value=3, step=1,
241
+ )
242
+ modality = gr.Radio(
243
+ label="Modality",
244
+ choices=["T1", "T2"], value="T1",
245
+ )
246
+ run_btn = gr.Button(
247
+ "Run head-to-head benchmark", variant="primary",
248
+ )
249
+ gr.Markdown(
250
+ "First run takes ~30–60 s extra to load the model. "
251
+ "Each V2/V3 trial typically takes 2–5 s on an L40S."
252
+ )
253
+
254
+ with gr.Column(scale=3):
255
+ summary_md = gr.Markdown(label="Summary")
256
+ results_df = gr.Dataframe(
257
+ headers=[
258
+ "variant", "trial", "volume", "size_mb",
259
+ "status", "runtime_s", "peak_vram_gb",
260
+ "voxel_count", "naive_volume_ml", "true_volume_ml",
261
+ "voxpixel_volume_ml", "abs_err_naive_vs_voxpixel_ml",
262
+ ],
263
+ label="Per-run results",
264
+ wrap=True,
265
+ )
266
+ csv_file = gr.File(label="Download results CSV")
267
+ log_box = gr.Textbox(label="Log", lines=10)
268
+
269
+ run_btn.click(
270
+ fn=run_benchmark,
271
+ inputs=[files, trials, modality],
272
+ outputs=[summary_md, results_df, csv_file, log_box],
273
+ )
274
+
275
+
276
+ if __name__ == "__main__":
277
+ demo.launch(
278
+ server_name="0.0.0.0",
279
+ server_port=int(os.environ.get("PORT", "7860")),
280
+ )
app_gradio_baseline.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Head-to-head Gradio baseline for the VoxPixel paper (Section V).
3
+
4
+ This file deliberately implements the SRMA-Mamba liver segmentation model
5
+ in three idiomatic Gradio configurations to expose, on the same hardware
6
+ and with the same checkpoints used by VoxPixel, the three failure modes
7
+ called out by reviewers:
8
+
9
+ V1: gr.Image(...) -> failure mode 1: NIfTI ingestion not
10
+ supported. The component rejects
11
+ .nii / .nii.gz at upload time.
12
+
13
+ V2: gr.File + nibabel + -> failure mode 3: CUDA OOM. A single
14
+ single-shot full-volume forward pass on the full 3D volume
15
+ forward pass. exceeds GPU memory on standard scans
16
+ because no sliding-window aggregator
17
+ is available out of the box.
18
+
19
+ V3: gr.File + nibabel + -> failure mode 2: spatial metadata
20
+ manually-wired MONAI loss. Inference succeeds, but the
21
+ SlidingWindowInferer. output mask is written with
22
+ affine = identity because Gradio's
23
+ I/O contract does not preserve the
24
+ NIfTI affine. Reported liver volume
25
+ in ml is therefore biased relative
26
+ to the spatially-correct VoxPixel
27
+ output.
28
+
29
+ This is NOT a production app. See app.py for VoxPixel itself. This file
30
+ exists so reviewers can reproduce the head-to-head table in the paper.
31
+
32
+ Usage (interactive):
33
+ python app_gradio_baseline.py --variant v1
34
+ python app_gradio_baseline.py --variant v2
35
+ python app_gradio_baseline.py --variant v3
36
+
37
+ Usage (programmatic, called by bench_gradio_baseline.py):
38
+ from app_gradio_baseline import predict_v2, predict_v3
39
+ """
40
+
41
+ import os
42
+ import sys
43
+ import time
44
+ import argparse
45
+ from pathlib import Path
46
+
47
+ import numpy as np
48
+ import torch
49
+ import nibabel as nib
50
+ import gradio as gr
51
+
52
+
53
+ _model_cache: dict = {}
54
+
55
+
56
+ def _load_bare_model(modality: str = "T1") -> torch.nn.Module:
57
+ """Load architecture + checkpoint with no sliding-window inferer,
58
+ no warm-up, no torch.compile, no channels-last layout. This is what
59
+ a researcher writing a naive Gradio wrapper would produce."""
60
+ # Imported lazily so V1 (which never calls this) can run on a laptop
61
+ # without mamba_ssm / selective_scan_cuda_oflex installed.
62
+ from config import (
63
+ build_SRMAMamba,
64
+ BUILD_SRMAMAMBA_AVAILABLE,
65
+ SRMA_MAMBA_DIR,
66
+ )
67
+
68
+ if not BUILD_SRMAMAMBA_AVAILABLE or build_SRMAMamba is None:
69
+ raise ImportError(
70
+ "SRMA-Mamba builder not available. Run setup.sh / install "
71
+ "mamba_ssm and selective_scan_cuda_oflex first."
72
+ )
73
+
74
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
75
+
76
+ if SRMA_MAMBA_DIR:
77
+ original_cwd = os.getcwd()
78
+ try:
79
+ os.chdir(SRMA_MAMBA_DIR)
80
+ model = build_SRMAMamba()
81
+ finally:
82
+ os.chdir(original_cwd)
83
+ else:
84
+ model = build_SRMAMamba()
85
+
86
+ ckpt_path = f"checkpoint_{modality}.pth"
87
+ if not os.path.exists(ckpt_path):
88
+ local = os.path.join(os.path.dirname(__file__), ckpt_path)
89
+ if os.path.exists(local):
90
+ ckpt_path = local
91
+ else:
92
+ from huggingface_hub import hf_hub_download
93
+ repo_id = os.environ.get(
94
+ "HF_MODEL_REPO", "HarshithReddy01/srmamamba-liver-segmentation"
95
+ )
96
+ ckpt_path = hf_hub_download(
97
+ repo_id=repo_id, filename=f"checkpoint_{modality}.pth", cache_dir="."
98
+ )
99
+
100
+ ckpt = torch.load(ckpt_path, map_location=device)
101
+ state = (
102
+ ckpt["state_dict"]
103
+ if isinstance(ckpt, dict) and "state_dict" in ckpt
104
+ else ckpt
105
+ )
106
+ model.load_state_dict(state)
107
+ model.eval().to(device)
108
+ return model
109
+
110
+
111
+ def _get_model(modality: str = "T1") -> torch.nn.Module:
112
+ if modality not in _model_cache:
113
+ _model_cache[modality] = _load_bare_model(modality)
114
+ return _model_cache[modality]
115
+
116
+
117
+ def _normalize(vol: np.ndarray) -> np.ndarray:
118
+ """Z-score normalization over nonzero voxels. Mirrors what
119
+ NormalizeIntensityd(nonzero=True) does in the VoxPixel pipeline,
120
+ inlined here so the comparison is about deployment, not preprocessing."""
121
+ vol = vol.astype(np.float32)
122
+ nz = vol[vol > 1e-6]
123
+ if nz.size == 0:
124
+ return vol
125
+ mu = float(nz.mean())
126
+ sigma = float(nz.std()) + 1e-8
127
+ out = np.where(vol > 1e-6, (vol - mu) / sigma, 0.0)
128
+ return out.astype(np.float32)
129
+
130
+
131
+ def _threshold_for(modality: str) -> float:
132
+ if modality.upper() == "T1":
133
+ return float(os.environ.get("T1_THRESHOLD", "0.65"))
134
+ return float(os.environ.get("SEGMENTATION_THRESHOLD", "0.5"))
135
+
136
+
137
+ def _summary(img: nib.Nifti1Image, mask: np.ndarray, t0: float) -> dict:
138
+ """Build the result record. naive_volume_ml uses the identity affine
139
+ that the V2/V3 predict() functions write to disk; true_volume_ml uses
140
+ the original affine the researcher should have preserved."""
141
+ runtime = time.time() - t0
142
+ if torch.cuda.is_available():
143
+ peak = torch.cuda.max_memory_allocated() / (1024 ** 3)
144
+ else:
145
+ peak = 0.0
146
+ voxels = int(mask.sum())
147
+ naive_ml = voxels / 1000.0
148
+ true_voxel_mm3 = abs(float(np.linalg.det(img.affine[:3, :3])))
149
+ true_ml = voxels * true_voxel_mm3 / 1000.0
150
+ return {
151
+ "status": "OK",
152
+ "runtime_s": runtime,
153
+ "peak_vram_gb": peak,
154
+ "voxel_count": voxels,
155
+ "naive_volume_ml": naive_ml,
156
+ "true_volume_ml": true_ml,
157
+ }
158
+
159
+
160
+ def predict_v2(nifti_path: str, modality: str = "T1") -> dict:
161
+ """V2: gr.File + nibabel + single-shot full-volume forward pass.
162
+
163
+ Demonstrates failure mode 3 (CUDA OOM on full volumes) and, when the
164
+ forward pass does fit, failure mode 2 (output mask written with
165
+ identity affine, losing voxel spacing)."""
166
+ t0 = time.time()
167
+ if torch.cuda.is_available():
168
+ torch.cuda.reset_peak_memory_stats()
169
+ torch.cuda.empty_cache()
170
+
171
+ img = nib.load(nifti_path)
172
+ vol = np.asarray(img.get_fdata(dtype=np.float32))
173
+ norm = _normalize(vol)
174
+ x = torch.from_numpy(norm)[None, None].contiguous()
175
+
176
+ model = _get_model(modality)
177
+ device = next(model.parameters()).device
178
+ x = x.to(device)
179
+
180
+ with torch.no_grad():
181
+ if device.type == "cuda":
182
+ from torch.amp import autocast
183
+ with autocast(device_type="cuda"):
184
+ y_tuple = model(x)
185
+ else:
186
+ y_tuple = model(x)
187
+
188
+ y1 = y_tuple[0] if isinstance(y_tuple, (list, tuple)) else y_tuple
189
+ pred = torch.sigmoid(y1)[0, 0]
190
+ mask = (pred > _threshold_for(modality)).to(torch.uint8).cpu().numpy()
191
+
192
+ out_path = os.path.join(
193
+ os.path.dirname(nifti_path) or ".", "v2_pred.nii.gz"
194
+ )
195
+ nib.Nifti1Image(mask, affine=np.eye(4)).to_filename(out_path)
196
+
197
+ res = _summary(img, mask, t0)
198
+ res["out_path"] = out_path
199
+ return res
200
+
201
+
202
+ def predict_v3(nifti_path: str, modality: str = "T1") -> dict:
203
+ """V3: gr.File + nibabel + manually-wired MONAI sliding window.
204
+
205
+ Inference succeeds even on full volumes, but the output is still
206
+ written with affine = identity, demonstrating failure mode 2."""
207
+ from monai.inferers import SlidingWindowInferer
208
+
209
+ t0 = time.time()
210
+ if torch.cuda.is_available():
211
+ torch.cuda.reset_peak_memory_stats()
212
+ torch.cuda.empty_cache()
213
+
214
+ img = nib.load(nifti_path)
215
+ vol = np.asarray(img.get_fdata(dtype=np.float32))
216
+ norm = _normalize(vol)
217
+ x = torch.from_numpy(norm)[None, None].contiguous()
218
+
219
+ model = _get_model(modality)
220
+ device = next(model.parameters()).device
221
+ x = x.to(device)
222
+
223
+ inferer = SlidingWindowInferer(
224
+ roi_size=[224, 224, 64],
225
+ sw_batch_size=1,
226
+ overlap=0.1,
227
+ )
228
+
229
+ def predictor(inp: torch.Tensor) -> torch.Tensor:
230
+ out = model(inp)
231
+ return out[0] if isinstance(out, (list, tuple)) else out
232
+
233
+ with torch.no_grad():
234
+ if device.type == "cuda":
235
+ from torch.amp import autocast
236
+ with autocast(device_type="cuda"):
237
+ y = inferer(x, predictor)
238
+ else:
239
+ y = inferer(x, predictor)
240
+
241
+ pred = torch.sigmoid(y)[0, 0]
242
+ mask = (pred > _threshold_for(modality)).to(torch.uint8).cpu().numpy()
243
+
244
+ out_path = os.path.join(
245
+ os.path.dirname(nifti_path) or ".", "v3_pred.nii.gz"
246
+ )
247
+ nib.Nifti1Image(mask, affine=np.eye(4)).to_filename(out_path)
248
+
249
+ res = _summary(img, mask, t0)
250
+ res["out_path"] = out_path
251
+ return res
252
+
253
+
254
+ def _format_log(res: dict) -> str:
255
+ return (
256
+ f"voxels: {res['voxel_count']:,}\n"
257
+ f"naive volume (ml, identity affine): {res['naive_volume_ml']:.2f}\n"
258
+ f"true volume (ml, original affine): {res['true_volume_ml']:.2f}\n"
259
+ f"peak VRAM (GB): {res['peak_vram_gb']:.2f}\n"
260
+ f"runtime (s): {res['runtime_s']:.2f}"
261
+ )
262
+
263
+
264
+ def _wrap_v2(file_obj):
265
+ if file_obj is None:
266
+ return None, "Please upload a NIfTI file."
267
+ res = predict_v2(file_obj.name)
268
+ return res["out_path"], _format_log(res)
269
+
270
+
271
+ def _wrap_v3(file_obj):
272
+ if file_obj is None:
273
+ return None, "Please upload a NIfTI file."
274
+ res = predict_v3(file_obj.name)
275
+ return res["out_path"], _format_log(res)
276
+
277
+
278
+ def build_v1_demo() -> gr.Interface:
279
+ """V1: gr.Image. The component does not accept .nii / .nii.gz; the
280
+ upload is rejected before predict() is ever invoked. This is the
281
+ exact failure observed when a researcher reaches for the default
282
+ image component for a volumetric workflow."""
283
+
284
+ def _identity(image):
285
+ return image
286
+
287
+ return gr.Interface(
288
+ fn=_identity,
289
+ inputs=gr.Image(label="Upload an image (will not accept .nii.gz)"),
290
+ outputs=gr.Image(label="Output"),
291
+ title="V1: Idiomatic Gradio (gr.Image)",
292
+ description=(
293
+ "gr.Image only accepts standard 2D image formats (PNG, JPG, "
294
+ "TIFF). Uploading a NIfTI volume (.nii / .nii.gz) is rejected "
295
+ "at the upload step. This is failure mode 1 in the paper: "
296
+ "native NIfTI ingestion is not supported by Gradio's image "
297
+ "components."
298
+ ),
299
+ )
300
+
301
+
302
+ def build_v2_demo() -> gr.Interface:
303
+ return gr.Interface(
304
+ fn=_wrap_v2,
305
+ inputs=gr.File(label="Upload .nii / .nii.gz"),
306
+ outputs=[
307
+ gr.File(label="Predicted mask (.nii.gz)"),
308
+ gr.Textbox(label="Log", lines=6),
309
+ ],
310
+ title="V2: gr.File + naive single-shot full-volume inference",
311
+ description=(
312
+ "Loads the NIfTI manually with nibabel and runs a single "
313
+ "forward pass on the full 3D volume. Expect CUDA OOM on "
314
+ "standard scans on most GPUs (failure mode 3). When inference "
315
+ "does succeed, the mask is written with affine=identity, so "
316
+ "the reported liver volume in ml has identity-affine bias "
317
+ "(failure mode 2)."
318
+ ),
319
+ )
320
+
321
+
322
+ def build_v3_demo() -> gr.Interface:
323
+ return gr.Interface(
324
+ fn=_wrap_v3,
325
+ inputs=gr.File(label="Upload .nii / .nii.gz"),
326
+ outputs=[
327
+ gr.File(label="Predicted mask (.nii.gz)"),
328
+ gr.Textbox(label="Log", lines=6),
329
+ ],
330
+ title="V3: gr.File + manually-wired MONAI sliding window",
331
+ description=(
332
+ "Adds MONAI SlidingWindowInferer manually to avoid OOM. "
333
+ "Inference now succeeds on full volumes, but the mask is "
334
+ "still written with affine=identity because Gradio's I/O "
335
+ "contract does not preserve spatial metadata (failure mode 2)."
336
+ ),
337
+ )
338
+
339
+
340
+ def main() -> None:
341
+ parser = argparse.ArgumentParser(
342
+ description="Head-to-head Gradio baseline (paper Section V)."
343
+ )
344
+ parser.add_argument(
345
+ "--variant", choices=["v1", "v2", "v3"], required=True,
346
+ help="Which baseline variant to launch.",
347
+ )
348
+ parser.add_argument("--port", type=int, default=7861)
349
+ parser.add_argument("--share", action="store_true")
350
+ args = parser.parse_args()
351
+
352
+ if args.variant == "v1":
353
+ demo = build_v1_demo()
354
+ elif args.variant == "v2":
355
+ demo = build_v2_demo()
356
+ else:
357
+ demo = build_v3_demo()
358
+
359
+ demo.launch(
360
+ server_name="0.0.0.0",
361
+ server_port=args.port,
362
+ share=args.share,
363
+ )
364
+
365
+
366
+ if __name__ == "__main__":
367
+ main()
bench_gradio_baseline.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Headless benchmark for the head-to-head comparison in the VoxPixel paper
3
+ (Section V). Runs the same nine-call protocol used for VoxPixel
4
+ (Table III in the paper) for each of V1, V2, V3 and writes one CSV row
5
+ per call, plus a printed summary suitable for direct inclusion in the
6
+ paper.
7
+
8
+ Usage (single command):
9
+
10
+ python bench_gradio_baseline.py \
11
+ --volumes ./test_volumes/56.nii.gz \
12
+ ./test_volumes/58.nii.gz \
13
+ ./test_volumes/60.nii.gz \
14
+ --trials 3 \
15
+ --modality T1 \
16
+ --out gradio_baseline_results.csv
17
+
18
+ The 9 runs match the 9 VoxPixel runs in Table III. Use the same
19
+ GPU and same checkpoints when reporting numbers.
20
+ """
21
+
22
+ import argparse
23
+ import csv
24
+ import gc
25
+ import os
26
+ import time
27
+ from pathlib import Path
28
+
29
+ import torch
30
+
31
+ # Keep this import local so the script can still print V1's deterministic
32
+ # UI_REJECT row even if the model dependencies fail to load.
33
+ def _load_predict_fns():
34
+ from app_gradio_baseline import predict_v2, predict_v3
35
+ return predict_v2, predict_v3
36
+
37
+
38
+ # Reference numbers from the existing VoxPixel runs (Table III in paper).
39
+ # Used to compute the affine-loss bias ("Δ vs. VoxPixel") in the report.
40
+ VOXPIXEL_TRUTH_ML = {
41
+ "56.nii.gz": 1156.13,
42
+ "58.nii.gz": 896.77,
43
+ "60.nii.gz": 904.30,
44
+ }
45
+
46
+
47
+ def _classify_error(exc: BaseException) -> tuple[str, str]:
48
+ msg = str(exc)
49
+ if isinstance(exc, getattr(torch.cuda, "OutOfMemoryError", RuntimeError)):
50
+ if "out of memory" in msg.lower():
51
+ return "OOM", msg[:200]
52
+ if isinstance(exc, RuntimeError) and "out of memory" in msg.lower():
53
+ return "OOM", msg[:200]
54
+ return "ERROR", msg[:200]
55
+
56
+
57
+ def run_v1(volume_path: str, modality: str) -> dict:
58
+ """gr.Image rejects .nii.gz before predict() is ever called.
59
+ Recorded deterministically without launching the UI so the row
60
+ is reproducible in CI."""
61
+ return {
62
+ "status": "UI_REJECT",
63
+ "runtime_s": 0.0,
64
+ "peak_vram_gb": 0.0,
65
+ "voxel_count": 0,
66
+ "naive_volume_ml": float("nan"),
67
+ "true_volume_ml": float("nan"),
68
+ "error": "gr.Image does not accept .nii / .nii.gz uploads",
69
+ }
70
+
71
+
72
+ def run_with_safety(fn, volume_path: str, modality: str) -> dict:
73
+ try:
74
+ return fn(volume_path, modality=modality)
75
+ except BaseException as exc:
76
+ status, msg = _classify_error(exc)
77
+ if torch.cuda.is_available():
78
+ torch.cuda.empty_cache()
79
+ gc.collect()
80
+ return {"status": status, "error": msg}
81
+
82
+
83
+ def main() -> None:
84
+ parser = argparse.ArgumentParser()
85
+ parser.add_argument("--volumes", nargs="+", required=True,
86
+ help="Paths to NIfTI test volumes.")
87
+ parser.add_argument("--trials", type=int, default=3,
88
+ help="Trials per (variant, volume). 3 reproduces "
89
+ "the 9-call protocol in Table III.")
90
+ parser.add_argument("--modality", default="T1")
91
+ parser.add_argument("--out", default="gradio_baseline_results.csv")
92
+ parser.add_argument("--variants", nargs="+", default=["V1", "V2", "V3"],
93
+ choices=["V1", "V2", "V3"])
94
+ args = parser.parse_args()
95
+
96
+ for v in args.volumes:
97
+ if not os.path.exists(v):
98
+ raise FileNotFoundError(f"Volume not found: {v}")
99
+
100
+ predict_v2 = predict_v3 = None
101
+ if any(v in ("V2", "V3") for v in args.variants):
102
+ predict_v2, predict_v3 = _load_predict_fns()
103
+
104
+ runners = {
105
+ "V1": lambda p, m: run_v1(p, m),
106
+ "V2": lambda p, m: run_with_safety(predict_v2, p, m),
107
+ "V3": lambda p, m: run_with_safety(predict_v3, p, m),
108
+ }
109
+
110
+ fields = [
111
+ "variant", "trial", "volume", "size_mb",
112
+ "status", "runtime_s", "peak_vram_gb",
113
+ "voxel_count", "naive_volume_ml", "true_volume_ml",
114
+ "voxpixel_volume_ml", "abs_err_naive_vs_voxpixel_ml",
115
+ "abs_err_true_vs_voxpixel_ml",
116
+ "error",
117
+ ]
118
+ rows: list[dict] = []
119
+
120
+ for variant in args.variants:
121
+ runner = runners[variant]
122
+ for vol_path in args.volumes:
123
+ vol_name = Path(vol_path).name
124
+ size_mb = round(os.path.getsize(vol_path) / (1024 ** 2), 2)
125
+ for trial in range(1, args.trials + 1):
126
+ print(f"[{variant}] trial {trial}/{args.trials} on {vol_name} "
127
+ f"({size_mb} MB)...")
128
+ t0 = time.time()
129
+ result = runner(vol_path, args.modality)
130
+ elapsed = time.time() - t0
131
+
132
+ naive_ml = result.get("naive_volume_ml")
133
+ true_ml = result.get("true_volume_ml")
134
+ voxpixel = VOXPIXEL_TRUTH_ML.get(vol_name)
135
+
136
+ def _abs_err(estimate):
137
+ if (voxpixel is None or estimate is None
138
+ or estimate != estimate): # NaN check
139
+ return float("nan")
140
+ return abs(estimate - voxpixel)
141
+
142
+ row = {
143
+ "variant": variant,
144
+ "trial": trial,
145
+ "volume": vol_name,
146
+ "size_mb": size_mb,
147
+ "status": result.get("status", "?"),
148
+ "runtime_s": round(result.get("runtime_s", elapsed), 3),
149
+ "peak_vram_gb": round(result.get("peak_vram_gb", 0.0), 2),
150
+ "voxel_count": result.get("voxel_count", 0),
151
+ "naive_volume_ml": naive_ml,
152
+ "true_volume_ml": true_ml,
153
+ "voxpixel_volume_ml": voxpixel,
154
+ "abs_err_naive_vs_voxpixel_ml": _abs_err(naive_ml),
155
+ "abs_err_true_vs_voxpixel_ml": _abs_err(true_ml),
156
+ "error": result.get("error", ""),
157
+ }
158
+ rows.append(row)
159
+ print(f" -> status={row['status']} "
160
+ f"runtime={row['runtime_s']}s "
161
+ f"peak_vram={row['peak_vram_gb']}GB")
162
+
163
+ if torch.cuda.is_available():
164
+ torch.cuda.empty_cache()
165
+ gc.collect()
166
+
167
+ with open(args.out, "w", newline="") as f:
168
+ writer = csv.DictWriter(f, fieldnames=fields)
169
+ writer.writeheader()
170
+ writer.writerows(rows)
171
+ print(f"\nWrote {len(rows)} rows to {args.out}")
172
+
173
+ print("\n=== Per-variant summary (paper Table) ===")
174
+ by_variant: dict[str, dict] = {}
175
+ for r in rows:
176
+ v = r["variant"]
177
+ d = by_variant.setdefault(v, {
178
+ "n": 0, "ok": 0, "oom": 0, "ui_reject": 0, "err": 0,
179
+ "runtime_sum": 0.0, "vram_max": 0.0,
180
+ "naive_err_sum": 0.0, "naive_err_n": 0,
181
+ })
182
+ d["n"] += 1
183
+ s = r["status"]
184
+ if s == "OK":
185
+ d["ok"] += 1
186
+ d["runtime_sum"] += r["runtime_s"]
187
+ d["vram_max"] = max(d["vram_max"], r["peak_vram_gb"])
188
+ err = r["abs_err_naive_vs_voxpixel_ml"]
189
+ if err == err: # not NaN
190
+ d["naive_err_sum"] += err
191
+ d["naive_err_n"] += 1
192
+ elif s == "OOM":
193
+ d["oom"] += 1
194
+ elif s == "UI_REJECT":
195
+ d["ui_reject"] += 1
196
+ else:
197
+ d["err"] += 1
198
+
199
+ header = (
200
+ f"{'Variant':<6} {'Success':>10} {'OOM':>5} {'UI_REJ':>7} "
201
+ f"{'ERR':>5} {'mean_rt(s)':>11} {'peak_vram(GB)':>14} "
202
+ f"{'mean_|naive-voxpixel|(ml)':>26}"
203
+ )
204
+ print(header)
205
+ print("-" * len(header))
206
+ for v, d in by_variant.items():
207
+ mean_rt = (d["runtime_sum"] / d["ok"]) if d["ok"] else float("nan")
208
+ mean_err = (
209
+ (d["naive_err_sum"] / d["naive_err_n"])
210
+ if d["naive_err_n"] else float("nan")
211
+ )
212
+ print(
213
+ f"{v:<6} {d['ok']:>4}/{d['n']:<5} "
214
+ f"{d['oom']:>5} {d['ui_reject']:>7} {d['err']:>5} "
215
+ f"{mean_rt:>11.2f} {d['vram_max']:>14.2f} "
216
+ f"{mean_err:>26.2f}"
217
+ )
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
prepare_runpod.sh ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # One-shot bootstrap for the head-to-head Gradio benchmark on a RunPod
3
+ # (or any Ubuntu 22.04 + CUDA 12.1 L40S box).
4
+ #
5
+ # Usage on the pod (after you've uploaded your three .nii.gz files into
6
+ # /workspace/test_volumes/):
7
+ #
8
+ # bash prepare_runpod.sh <hf_space_or_repo_url>
9
+ #
10
+ # Example:
11
+ # bash prepare_runpod.sh https://huggingface.co/spaces/HarshithReddy01/voxpixel
12
+ #
13
+ # What it does:
14
+ # 1. Clones the VoxPixel repo into /workspace/voxpixel
15
+ # 2. Installs system deps + Python deps + builds mamba_ssm and
16
+ # selective_scan_cuda_oflex (this is the slow step, ~10-15 min)
17
+ # 3. Runs the 9-call x 3-variant benchmark
18
+ # 4. Tars the CSV + log into /workspace/results.tar.gz so you can
19
+ # download it from the Jupyter file browser
20
+
21
+ set -euo pipefail
22
+
23
+ REPO_URL="${1:-https://huggingface.co/spaces/HarshithReddy01/voxpixel}"
24
+ WORKDIR="/workspace/voxpixel"
25
+ VOLDIR="/workspace/test_volumes"
26
+ RESULTS="/workspace/gradio_baseline_results.csv"
27
+ LOG="/workspace/bench.log"
28
+
29
+ echo "===> [1/5] Sanity checks"
30
+ nvidia-smi | head -n 20
31
+ test -d "$VOLDIR" || { echo "ERROR: $VOLDIR missing. Upload your .nii.gz volumes there first."; exit 1; }
32
+ ls -lh "$VOLDIR"
33
+
34
+ echo "===> [2/5] Cloning repo into $WORKDIR"
35
+ if [ ! -d "$WORKDIR" ]; then
36
+ git clone "$REPO_URL" "$WORKDIR"
37
+ fi
38
+ cd "$WORKDIR"
39
+ git pull --rebase || true
40
+
41
+ echo "===> [3/5] Installing system + Python deps + CUDA extensions"
42
+ apt-get update -qq
43
+ apt-get install -y -qq build-essential ninja-build git libgl1 libglib2.0-0 >/dev/null
44
+
45
+ python -m pip install --upgrade pip wheel setuptools packaging ninja >/dev/null
46
+
47
+ if ! python -c "import torch" 2>/dev/null; then
48
+ pip install --index-url https://download.pytorch.org/whl/cu121 \
49
+ torch torchvision torchaudio
50
+ fi
51
+
52
+ pip install --no-cache-dir -r requirements.txt
53
+
54
+ if ! python -c "import mamba_ssm" 2>/dev/null; then
55
+ echo " -> Building mamba-ssm (~5-10 min)..."
56
+ pip install "mamba-ssm>=2.2.2" --no-build-isolation
57
+ fi
58
+
59
+ if ! python -c "import selective_scan_cuda_oflex" 2>/dev/null; then
60
+ echo " -> Building selective_scan_cuda_oflex (~3-5 min)..."
61
+ cd SRMA-Mamba/selective_scan
62
+ pip install --no-build-isolation -e . -v
63
+ cd "$WORKDIR"
64
+ fi
65
+
66
+ echo "===> [4/5] Verifying the stack"
67
+ python - <<'PY'
68
+ import torch, mamba_ssm, selective_scan_cuda_oflex
69
+ print("torch:", torch.__version__, "cuda:", torch.cuda.is_available(),
70
+ "device:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "cpu")
71
+ print("mamba_ssm OK:", mamba_ssm.__file__)
72
+ print("selective_scan_cuda_oflex OK:", selective_scan_cuda_oflex.__file__)
73
+ PY
74
+
75
+ echo "===> [5/5] Running 9-call benchmark across V1, V2, V3"
76
+ VOLS=( "$VOLDIR"/56.nii.gz "$VOLDIR"/58.nii.gz "$VOLDIR"/60.nii.gz )
77
+ for v in "${VOLS[@]}"; do
78
+ test -f "$v" || { echo "ERROR: missing volume $v"; exit 1; }
79
+ done
80
+
81
+ python bench_gradio_baseline.py \
82
+ --volumes "${VOLS[@]}" \
83
+ --trials 3 \
84
+ --modality T1 \
85
+ --out "$RESULTS" 2>&1 | tee "$LOG"
86
+
87
+ echo "===> Done. Bundling results."
88
+ tar -czf /workspace/results.tar.gz \
89
+ -C /workspace \
90
+ "$(basename "$RESULTS")" \
91
+ "$(basename "$LOG")"
92
+ ls -lh /workspace/results.tar.gz "$RESULTS" "$LOG"
93
+ echo
94
+ echo "Download /workspace/results.tar.gz from the Jupyter file browser,"
95
+ echo "then 'Stop' or 'Terminate' this pod from the RunPod dashboard."