Spaces:
Build error
Build error
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 +21 -0
- Dockerfile +2 -1
- README.md +60 -0
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/config.cpython-312.pyc +0 -0
- __pycache__/inference.cpython-312.pyc +0 -0
- __pycache__/model.cpython-312.pyc +0 -0
- __pycache__/model_loader.cpython-312.pyc +0 -0
- __pycache__/processing.cpython-312.pyc +0 -0
- app_bench_space.py +280 -0
- app_gradio_baseline.py +367 -0
- bench_gradio_baseline.py +221 -0
- prepare_runpod.sh +95 -0
.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 |
-
|
|
|
|
|
|
| 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."
|