Spaces:
Running on Zero
Running on Zero
Use prebuilt diff-gaussian-rasterization wheel (splatting_cuda decoder); drop gsplat JIT
Browse files- README.md +7 -3
- app.py +1 -69
- codecsplat_pipeline.py +12 -10
- requirements.txt +1 -2
- src/model/decoder/__init__.py +16 -9
README.md
CHANGED
|
@@ -30,13 +30,17 @@ How the Space works:
|
|
| 30 |
runs the **complete bitstream path** from `ModelWrapper.test_step`: encode context →
|
| 31 |
compress to a real bitstream at the chosen λ → decompress → predict Gaussians, using bf16
|
| 32 |
autocast like `trainer.precision=bf16-mixed`.
|
| 33 |
-
4. The authors'
|
|
|
|
| 34 |
against the real photos) and a fly-through video. The decoded Gaussians are shown in an
|
| 35 |
interactive `gr.Model3D` splat viewer, and the `.bin` bitstream is available to download.
|
| 36 |
|
| 37 |
Notes:
|
| 38 |
-
*
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
* The viewer gets a compact copy of the Gaussians: DC colour only, near-transparent splats
|
| 41 |
pruned, re-centred so the scene is framed.
|
| 42 |
|
|
|
|
| 30 |
runs the **complete bitstream path** from `ModelWrapper.test_step`: encode context →
|
| 31 |
compress to a real bitstream at the chosen λ → decompress → predict Gaussians, using bf16
|
| 32 |
autocast like `trainer.precision=bf16-mixed`.
|
| 33 |
+
4. The authors' `splatting_cuda` decoder (Inria `diff-gaussian-rasterization`, prebuilt sm_120
|
| 34 |
+
wheel) renders held-out views (PSNR is reported
|
| 35 |
against the real photos) and a fly-through video. The decoded Gaussians are shown in an
|
| 36 |
interactive `gr.Model3D` splat viewer, and the `.bin` bitstream is available to download.
|
| 37 |
|
| 38 |
Notes:
|
| 39 |
+
* The authors' evaluation scripts render with gsplat (antialiased). This Space uses the
|
| 40 |
+
repo's other supported decoder, `model/decoder=splatting_cuda`, because a prebuilt Blackwell
|
| 41 |
+
wheel exists for it. Renders can differ very slightly from the paper's numbers.
|
| 42 |
+
* COLMAP runs in a separate process with 16 threads. On the ~192-CPU ZeroGPU hosts COLMAP's
|
| 43 |
+
default thread count makes the mapper fail to register images.
|
| 44 |
* The viewer gets a compact copy of the Gaussians: DC colour only, near-transparent splats
|
| 45 |
pruned, re-centred so the scene is framed.
|
| 46 |
|
app.py
CHANGED
|
@@ -3,15 +3,13 @@
|
|
| 3 |
Upload overlapping photos of a static scene. The Space runs COLMAP (pycolmap, CPU) to get
|
| 4 |
camera poses, then CodecSplat (authors' DL3DV 8-view 256x448 checkpoint):
|
| 5 |
context views -> 2D latent features -> learned entropy coder -> real bitstream
|
| 6 |
-
-> decoder -> 3D Gaussians ->
|
| 7 |
You get an interactive splat viewer, a fly-through video, held-out novel views, the
|
| 8 |
size of the compressed scene bitstream (and the bitstream itself).
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
-
import ctypes
|
| 14 |
-
import glob
|
| 15 |
import os
|
| 16 |
import sys
|
| 17 |
import tempfile
|
|
@@ -25,8 +23,6 @@ sys.path.insert(0, str(APP_DIR))
|
|
| 25 |
# threads each spin up a multi-threaded BLAS; keep BLAS single-threaded.
|
| 26 |
os.environ["OPENBLAS_NUM_THREADS"] = "1"
|
| 27 |
os.environ.setdefault("OMP_NUM_THREADS", "4")
|
| 28 |
-
os.environ.setdefault("TORCH_CUDA_ARCH_LIST", "12.0")
|
| 29 |
-
os.environ.setdefault("MAX_JOBS", str(min(32, (os.cpu_count() or 8))))
|
| 30 |
|
| 31 |
import spaces # noqa: E402 (must precede any CUDA-touching import)
|
| 32 |
import torch # noqa: E402
|
|
@@ -34,67 +30,6 @@ import torch # noqa: E402
|
|
| 34 |
torch.set_float32_matmul_precision("high")
|
| 35 |
|
| 36 |
|
| 37 |
-
# ---------------------------------------------------------------------------
|
| 38 |
-
# 1. gsplat CUDA backend: JIT-compile once at startup against the CUDA toolkit
|
| 39 |
-
# shipped in the ZeroGPU image (same approach as hugging-apps/resplat-recurrent-gs).
|
| 40 |
-
# ---------------------------------------------------------------------------
|
| 41 |
-
def _pick_cuda_home() -> str | None:
|
| 42 |
-
torch_cuda = (torch.version.cuda or "").split(".")[0]
|
| 43 |
-
candidates: list[str] = []
|
| 44 |
-
for pattern in ("/cuda-image/usr/local/cuda-*", "/usr/local/cuda-*"):
|
| 45 |
-
candidates += sorted(glob.glob(pattern), reverse=True)
|
| 46 |
-
candidates += ["/cuda-image/usr/local/cuda", "/usr/local/cuda"]
|
| 47 |
-
usable = [c for c in candidates if os.path.isfile(os.path.join(c, "bin", "nvcc"))]
|
| 48 |
-
if not usable:
|
| 49 |
-
return None
|
| 50 |
-
for c in usable:
|
| 51 |
-
if torch_cuda and f"cuda-{torch_cuda}" in c:
|
| 52 |
-
return c
|
| 53 |
-
return usable[0]
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
os.environ.setdefault("_ORIG_LD_LIBRARY_PATH", os.environ.get("LD_LIBRARY_PATH", ""))
|
| 57 |
-
CUDA_HOME = _pick_cuda_home()
|
| 58 |
-
print(f"[setup] torch {torch.__version__} (cuda {torch.version.cuda}) | CUDA_HOME={CUDA_HOME}", flush=True)
|
| 59 |
-
if CUDA_HOME:
|
| 60 |
-
os.environ["CUDA_HOME"] = CUDA_HOME
|
| 61 |
-
os.environ["CUDA_PATH"] = CUDA_HOME
|
| 62 |
-
os.environ["PATH"] = os.path.join(CUDA_HOME, "bin") + os.pathsep + os.environ.get("PATH", "")
|
| 63 |
-
_libdir = os.path.join(CUDA_HOME, "lib64")
|
| 64 |
-
os.environ["LD_LIBRARY_PATH"] = _libdir + os.pathsep + os.environ.get("LD_LIBRARY_PATH", "")
|
| 65 |
-
for _so in sorted(glob.glob(os.path.join(_libdir, "libcudart.so*")), reverse=True):
|
| 66 |
-
try:
|
| 67 |
-
ctypes.CDLL(_so, mode=ctypes.RTLD_GLOBAL)
|
| 68 |
-
break
|
| 69 |
-
except OSError:
|
| 70 |
-
continue
|
| 71 |
-
|
| 72 |
-
def _cuda_diag(stage: str) -> None:
|
| 73 |
-
"""Log whether the CUDA driver got loaded into the main (GPU-less) process."""
|
| 74 |
-
try:
|
| 75 |
-
maps = open("/proc/self/maps").read()
|
| 76 |
-
libcuda = "libcuda.so" in maps
|
| 77 |
-
except Exception:
|
| 78 |
-
libcuda = "?"
|
| 79 |
-
print(f"[diag] {stage}: libcuda mapped={libcuda} "
|
| 80 |
-
f"torch.cuda.is_initialized={torch.cuda.is_initialized()}", flush=True)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
_cuda_diag("after torch import")
|
| 84 |
-
import torch.utils.cpp_extension as _cppext # noqa: E402
|
| 85 |
-
|
| 86 |
-
_cppext._check_cuda_version = lambda *a, **k: None
|
| 87 |
-
|
| 88 |
-
print("[setup] compiling gsplat CUDA kernels (one-off, several minutes)...", flush=True)
|
| 89 |
-
_t0 = time.time()
|
| 90 |
-
from gsplat.cuda._backend import _C as _GSPLAT_C # noqa: E402
|
| 91 |
-
|
| 92 |
-
if _GSPLAT_C is None:
|
| 93 |
-
raise RuntimeError(f"gsplat CUDA backend unavailable (CUDA_HOME={CUDA_HOME}).")
|
| 94 |
-
print(f"[setup] gsplat backend ready in {time.time() - _t0:.1f}s", flush=True)
|
| 95 |
-
_cuda_diag("after gsplat build")
|
| 96 |
-
|
| 97 |
-
|
| 98 |
# ---------------------------------------------------------------------------
|
| 99 |
# 2. Model
|
| 100 |
# ---------------------------------------------------------------------------
|
|
@@ -106,7 +41,6 @@ from PIL import Image # noqa: E402
|
|
| 106 |
|
| 107 |
import codecsplat_pipeline as cp # noqa: E402
|
| 108 |
|
| 109 |
-
_cuda_diag("after pipeline import")
|
| 110 |
from colmap_sfm import IMAGE_EXT, run_sfm_subprocess # noqa: E402
|
| 111 |
|
| 112 |
MODEL_REPO = "pengpeng-yu/CodecSplat"
|
|
@@ -121,7 +55,6 @@ print("[setup] building CodecSplat...", flush=True)
|
|
| 121 |
ENCODER = cp.build_encoder(ckpt_path).to("cuda")
|
| 122 |
DECODER = cp.build_decoder().to("cuda")
|
| 123 |
print("[setup] model ready", flush=True)
|
| 124 |
-
_cuda_diag("after model build")
|
| 125 |
|
| 126 |
|
| 127 |
def _u8(t: torch.Tensor) -> np.ndarray:
|
|
@@ -237,7 +170,6 @@ def reconstruct(images, lmb: float = 1024.0, matcher: str = "exhaustive",
|
|
| 237 |
raise gr.Error(str(exc)) from exc
|
| 238 |
t_sfm = time.time() - t_start
|
| 239 |
img_paths, c2w, K = _scene_arrays(sfm["frames"])
|
| 240 |
-
_cuda_diag("before GPU call")
|
| 241 |
|
| 242 |
progress(0.55, desc="CodecSplat: encode -> bitstream -> decode -> render")
|
| 243 |
r = codecsplat_gpu(img_paths, c2w, K, float(lmb))
|
|
|
|
| 3 |
Upload overlapping photos of a static scene. The Space runs COLMAP (pycolmap, CPU) to get
|
| 4 |
camera poses, then CodecSplat (authors' DL3DV 8-view 256x448 checkpoint):
|
| 5 |
context views -> 2D latent features -> learned entropy coder -> real bitstream
|
| 6 |
+
-> decoder -> 3D Gaussians -> Gaussian-splatting rasterizer.
|
| 7 |
You get an interactive splat viewer, a fly-through video, held-out novel views, the
|
| 8 |
size of the compressed scene bitstream (and the bitstream itself).
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
| 12 |
|
|
|
|
|
|
|
| 13 |
import os
|
| 14 |
import sys
|
| 15 |
import tempfile
|
|
|
|
| 23 |
# threads each spin up a multi-threaded BLAS; keep BLAS single-threaded.
|
| 24 |
os.environ["OPENBLAS_NUM_THREADS"] = "1"
|
| 25 |
os.environ.setdefault("OMP_NUM_THREADS", "4")
|
|
|
|
|
|
|
| 26 |
|
| 27 |
import spaces # noqa: E402 (must precede any CUDA-touching import)
|
| 28 |
import torch # noqa: E402
|
|
|
|
| 30 |
torch.set_float32_matmul_precision("high")
|
| 31 |
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# ---------------------------------------------------------------------------
|
| 34 |
# 2. Model
|
| 35 |
# ---------------------------------------------------------------------------
|
|
|
|
| 41 |
|
| 42 |
import codecsplat_pipeline as cp # noqa: E402
|
| 43 |
|
|
|
|
| 44 |
from colmap_sfm import IMAGE_EXT, run_sfm_subprocess # noqa: E402
|
| 45 |
|
| 46 |
MODEL_REPO = "pengpeng-yu/CodecSplat"
|
|
|
|
| 55 |
ENCODER = cp.build_encoder(ckpt_path).to("cuda")
|
| 56 |
DECODER = cp.build_decoder().to("cuda")
|
| 57 |
print("[setup] model ready", flush=True)
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def _u8(t: torch.Tensor) -> np.ndarray:
|
|
|
|
| 170 |
raise gr.Error(str(exc)) from exc
|
| 171 |
t_sfm = time.time() - t_start
|
| 172 |
img_paths, c2w, K = _scene_arrays(sfm["frames"])
|
|
|
|
| 173 |
|
| 174 |
progress(0.55, desc="CodecSplat: encode -> bitstream -> decode -> render")
|
| 175 |
r = codecsplat_gpu(img_paths, c2w, K, float(lmb))
|
codecsplat_pipeline.py
CHANGED
|
@@ -76,19 +76,21 @@ def build_encoder(checkpoint_path: str):
|
|
| 76 |
|
| 77 |
|
| 78 |
def build_decoder():
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
)
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
DecoderSplattingGsplatCfg,
|
| 88 |
-
OmegaConf.to_container(dec_cfg, resolve=True),
|
| 89 |
-
config=DaciteConfig(strict=False),
|
| 90 |
)
|
| 91 |
-
decoder = DecoderSplattingGsplat(dec_cfg, SimpleNamespace(background_color=[0.0, 0.0, 0.0]))
|
| 92 |
return decoder.eval()
|
| 93 |
|
| 94 |
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def build_decoder():
|
| 79 |
+
"""The authors' `model/decoder=splatting_cuda` decoder (Inria diff-gaussian-rasterization).
|
| 80 |
+
|
| 81 |
+
The evaluation scripts use the gsplat decoder; on ZeroGPU we use the repo's other
|
| 82 |
+
supported decoder because a prebuilt sm_120 wheel exists for it (gsplat would have to
|
| 83 |
+
be JIT-compiled against a CUDA toolkit at startup).
|
| 84 |
+
"""
|
| 85 |
+
from src.model.decoder.decoder_splatting_cuda import (
|
| 86 |
+
DecoderSplattingCUDA,
|
| 87 |
+
DecoderSplattingCUDACfg,
|
| 88 |
)
|
| 89 |
|
| 90 |
+
decoder = DecoderSplattingCUDA(
|
| 91 |
+
DecoderSplattingCUDACfg(name="splatting_cuda"),
|
| 92 |
+
SimpleNamespace(background_color=[0.0, 0.0, 0.0]),
|
|
|
|
|
|
|
|
|
|
| 93 |
)
|
|
|
|
| 94 |
return decoder.eval()
|
| 95 |
|
| 96 |
|
requirements.txt
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
torch==2.11.0
|
| 2 |
torchvision==0.26.0
|
| 3 |
-
|
| 4 |
-
ninja
|
| 5 |
compressai==1.2.8
|
| 6 |
numpy<2
|
| 7 |
pycolmap==4.1.1
|
|
|
|
| 1 |
torch==2.11.0
|
| 2 |
torchvision==0.26.0
|
| 3 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt211-cu130-cp312/diff_gaussian_rasterization-0.0.0-cp312-cp312-linux_x86_64.whl
|
|
|
|
| 4 |
compressai==1.2.8
|
| 5 |
numpy<2
|
| 6 |
pycolmap==4.1.1
|
src/model/decoder/__init__.py
CHANGED
|
@@ -16,15 +16,22 @@ except ImportError as exc:
|
|
| 16 |
else:
|
| 17 |
CUDA_SPLATTING_IMPORT_ERROR = None
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
DecoderCfg = DecoderSplattingGsplatCfg | DecoderSplattingCUDACfg
|
| 30 |
|
|
|
|
| 16 |
else:
|
| 17 |
CUDA_SPLATTING_IMPORT_ERROR = None
|
| 18 |
|
| 19 |
+
try: # gsplat is optional in this Space (we render with diff-gaussian-rasterization)
|
| 20 |
+
from .decoder_splatting_gsplat import (
|
| 21 |
+
DecoderSplattingGsplat,
|
| 22 |
+
DecoderSplattingGsplatCfg,
|
| 23 |
+
)
|
| 24 |
+
except ImportError:
|
| 25 |
+
DecoderSplattingGsplat = None
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class DecoderSplattingGsplatCfg:
|
| 29 |
+
name: Literal["splatting_gsplat"]
|
| 30 |
+
|
| 31 |
+
DECODERS = {}
|
| 32 |
+
if DecoderSplattingGsplat is not None:
|
| 33 |
+
DECODERS["splatting_gsplat"] = DecoderSplattingGsplat
|
| 34 |
+
DECODERS["gsplat"] = DecoderSplattingGsplat
|
| 35 |
|
| 36 |
DecoderCfg = DecoderSplattingGsplatCfg | DecoderSplattingCUDACfg
|
| 37 |
|