Claude Sonnet 4.5 Claude Fable 5 commited on
Commit
7444d60
·
unverified ·
1 Parent(s): 201b0f8

refactor: minimal hacks — own inference wrapper, trimmed stubs and deps, gradio 6.20

Browse files

- sam3d_inference.py replaces notebook/inference.py: imports only the
pipeline, dropping the kaolin.visualize/SceneVisualizer/plotly chain
- attention backend pinned to sdpa via import order (upstream env mechanism)
instead of source patching; flash_attn stub removed
- kaolin stub reduced to utils.testing.check_tensor (flexicubes); pytorch3d
stub reduced to transforms/structures/renderer
- requirements: drop ~25 training-only packages (verified unreferenced in the
inference import graph); move spconv/open3d/iopath from runtime pip into
requirements; pin hydra-core 1.3.2
- drop hydra patch script call: it was invoked with bash against a python
script and never ran — the successful e2e run proves it is not needed
- drop utils3d monkey-patches: the pinned commit ships those functions
- gradio 6.20.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 📦
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.13.0
8
  python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
 
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.20.0
8
  python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
app.py CHANGED
@@ -3,13 +3,17 @@ SAM 3D Objects MCP Server
3
  Image → 3D Object (GLB)
4
 
5
  SAM2 auto-detection + SAM 3D Objects reconstruction on ZeroGPU.
6
- kaolin/pytorch3d/flash_attn are stubbed (only their pure-python surface is
7
- used since texture baking and mesh postprocessing are disabled); spconv and
8
- gsplat are installed at runtime against the ZeroGPU-provided torch.
 
 
 
 
9
  """
10
  import os
11
- import sys
12
  import subprocess
 
13
  import tempfile
14
  from pathlib import Path
15
 
@@ -29,106 +33,39 @@ from huggingface_hub import snapshot_download, login
29
  if os.environ.get("HF_TOKEN"):
30
  login(token=os.environ["HF_TOKEN"])
31
 
32
- # --- Stubs (must be on sys.path before sam3d imports) ---
33
  APP_ROOT = Path(__file__).parent
34
- for stub in ["kaolin_stub", "pytorch3d_stub", "flash_attn_stub"]:
35
- stub_path = APP_ROOT / stub
36
- if stub_path.exists():
37
- sys.path.insert(0, str(stub_path))
38
- print(f"Stub added: {stub}")
39
 
40
 
41
- # --- Runtime pip installs (need torch present, so not in requirements.txt) ---
42
- def _pip(*a):
43
  r = subprocess.run(
44
- [sys.executable, "-m", "pip", "install", "--no-cache-dir"] + list(a),
45
  capture_output=True, text=True, timeout=1200,
46
  )
47
- tag = a[-1][:60]
48
- print(f" pip {'OK' if r.returncode == 0 else 'FAIL'}: {tag}")
49
  if r.returncode != 0:
50
- print(f" {r.stderr[-300:]}")
51
  return r.returncode == 0
52
 
53
 
54
- print("=== Runtime installs ===")
55
- _pip("open3d>=0.18.0")
56
- # utils3d pinned to the commit MoGe expects — git HEAD removed points_to_normals
57
  _pip("--no-deps", "git+https://github.com/EasternJournalist/utils3d.git@3913c65d81e05e47b9f367250cf8c0f7462a0900")
58
- _pip("iopath")
59
  _pip("--no-deps", "sam2>=1.1.0")
60
  _pip("--no-deps", "git+https://github.com/microsoft/MoGe.git@a8c37341bc0325ca99b9d57981cc3bb2bd3e255b")
61
- # gsplat: no prebuilt wheels beyond pt24 — PyPI sdist installs in JIT mode;
62
- # kernels never compile here because rendering paths are disabled.
63
  _pip("--no-deps", "gsplat")
64
- # spconv cu124 wheel is forward-compatible with the cu128 runtime
65
- _pip("spconv-cu124==2.3.8")
66
 
67
- # --- Clone sam-3d-objects ---
68
  SAM3D_PATH = APP_ROOT / "sam-3d-objects"
69
  if not SAM3D_PATH.exists():
70
  print("Cloning sam-3d-objects...")
71
  subprocess.run(["git", "clone", "--depth", "1",
72
  "https://github.com/facebookresearch/sam-3d-objects.git",
73
  str(SAM3D_PATH)], check=True)
74
-
75
- subprocess.run([sys.executable, "-m", "pip", "install", "-e", str(SAM3D_PATH), "--no-deps"],
76
- capture_output=True, text=True)
77
-
78
- hydra_patch = SAM3D_PATH / "patching" / "hydra"
79
- if hydra_patch.exists():
80
- subprocess.run(["bash", str(hydra_patch)], capture_output=True, cwd=str(SAM3D_PATH))
81
-
82
- # Patch: inference_pipeline.py forces flash_attn on A100/H100/H200,
83
- # but only the stub is installed — keep sdpa.
84
- ip_file = SAM3D_PATH / "sam3d_objects" / "pipeline" / "inference_pipeline.py"
85
- if ip_file.exists():
86
- src = ip_file.read_text()
87
- old = ('if "A100" in gpu_name or "H100" in gpu_name or "H200" in gpu_name:\n'
88
- ' # logger.info("Use flash_attn")\n'
89
- ' os.environ["ATTN_BACKEND"] = "flash_attn"\n'
90
- ' os.environ["SPARSE_ATTN_BACKEND"] = "flash_attn"')
91
- if old in src:
92
- ip_file.write_text(src.replace(
93
- old,
94
- '# PATCHED: flash_attn not available on ZeroGPU, keep sdpa\n'
95
- ' os.environ.setdefault("ATTN_BACKEND", "sdpa")\n'
96
- ' os.environ.setdefault("SPARSE_ATTN_BACKEND", "sdpa")'
97
- ))
98
- print("PATCHED: inference_pipeline.py — forced sdpa backend")
99
- else:
100
- print("INFO: inference_pipeline.py patch marker not found (upstream changed?)")
101
-
102
  sys.path.insert(0, str(SAM3D_PATH))
103
- sys.path.insert(0, str(SAM3D_PATH / "notebook"))
104
-
105
- # --- Patch: utils3d from git lacks depth_edge/normals_edge (needed by layout code) ---
106
- try:
107
- import utils3d.numpy as _u3d_np
108
- if not hasattr(_u3d_np, "depth_edge"):
109
- def _depth_edge(depth, rtol=0.03, mask=None):
110
- from scipy.ndimage import sobel
111
- d = np.where(mask, depth, 0.0) if mask is not None else depth.copy()
112
- grad = np.sqrt(sobel(d, axis=1) ** 2 + sobel(d, axis=0) ** 2)
113
- denom = np.abs(d)
114
- denom[denom < 1e-6] = 1e-6
115
- edge = (grad / denom) > rtol
116
- return edge & mask if mask is not None else edge
117
- _u3d_np.depth_edge = _depth_edge
118
-
119
- def _normals_edge(normals, tol=0.1, mask=None):
120
- from scipy.ndimage import sobel
121
- edges = np.zeros(normals.shape[:2], dtype=bool)
122
- for c in range(normals.shape[-1]):
123
- ch = np.where(mask, normals[..., c], 0.0) if mask is not None else normals[..., c]
124
- edges |= np.sqrt(sobel(ch, axis=1) ** 2 + sobel(ch, axis=0) ** 2) > tol
125
- return edges & mask if mask is not None else edges
126
- _u3d_np.normals_edge = _normals_edge
127
- print("Injected depth_edge + normals_edge into utils3d.numpy")
128
- except Exception as e:
129
- print(f"utils3d patch skipped: {e}")
130
-
131
- # --- Pre-download checkpoints ---
132
  print("Downloading SAM3D checkpoints...")
133
  CKPT_DIR = snapshot_download(repo_id="facebook/sam-3d-objects",
134
  token=os.environ.get("HF_TOKEN"))
@@ -143,8 +80,7 @@ print("=== Startup complete ===")
143
 
144
 
145
  def _gaussians_to_glb(result, out_dir):
146
- """Extract gaussians from pipeline result and Poisson-mesh them to GLB."""
147
- import torch
148
  gs = None
149
  if hasattr(result, "save_ply"):
150
  gs = result
@@ -184,7 +120,11 @@ def diagnose():
184
  for mod in ["kaolin", "pytorch3d", "spconv", "gsplat", "utils3d", "open3d", "moge"]:
185
  try:
186
  m = __import__(mod)
187
- lines.append(f"{mod}: OK ({getattr(m, '__version__', '-')})")
 
 
 
 
188
  except Exception as e:
189
  lines.append(f"{mod}: FAIL - {e}")
190
  try:
@@ -193,10 +133,10 @@ def diagnose():
193
  except Exception as e:
194
  lines.append(f"sam2: FAIL - {e}")
195
  try:
196
- from inference import Inference # noqa: F401
197
- lines.append("SAM3D Inference: importable")
198
  except Exception as e:
199
- lines.append(f"SAM3D Inference: FAIL - {e}")
200
  lines.append(f"config: {Path(CONFIG_PATH).exists()}")
201
  return "\n".join(lines)
202
 
@@ -216,8 +156,8 @@ def reconstruct_objects(image: np.ndarray):
216
  if image is None:
217
  return None, None, "❌ No image provided"
218
  try:
219
- import torch
220
  import time
 
221
  t0 = time.time()
222
  print(f"GPU: {torch.cuda.get_device_name()}")
223
 
@@ -225,7 +165,7 @@ def reconstruct_objects(image: np.ndarray):
225
  sam2_gen = SAM2AutomaticMaskGenerator.from_pretrained("facebook/sam2-hiera-small")
226
  print(f" SAM2 loaded ({time.time()-t0:.0f}s)")
227
 
228
- image_np = np.array(image) if not isinstance(image, np.ndarray) else image
229
  masks = sam2_gen.generate(image_np)
230
  if not masks:
231
  return None, image_np, "⚠️ No objects detected"
@@ -240,8 +180,8 @@ def reconstruct_objects(image: np.ndarray):
240
  del sam2_gen
241
  torch.cuda.empty_cache()
242
 
243
- from inference import Inference
244
- sam3d = Inference(CONFIG_PATH, compile=False)
245
  print(f" SAM3D loaded ({time.time()-t0:.0f}s)")
246
 
247
  result = sam3d(image=image_np, mask=best_mask, seed=42)
@@ -260,14 +200,13 @@ def reconstruct_objects(image: np.ndarray):
260
  except Exception:
261
  n_faces = 0
262
  return glb, preview, f"✓ {len(masks)} objects detected, {n_faces:,} faces ({int(time.time()-t0)}s)"
263
- except Exception as e:
264
  import traceback
265
  tb = traceback.format_exc()
266
  print(tb)
267
  return None, None, f"❌ Error:\n{tb[-1500:]}"
268
 
269
 
270
- # Gradio Interface
271
  with gr.Blocks(title="SAM 3D Objects MCP") as demo:
272
  gr.Markdown("""
273
  # 📦 SAM 3D Objects MCP Server
 
3
  Image → 3D Object (GLB)
4
 
5
  SAM2 auto-detection + SAM 3D Objects reconstruction on ZeroGPU.
6
+
7
+ torch is provided by ZeroGPU. kaolin and pytorch3d are replaced by pure-python
8
+ stubs covering exactly the surface the pipeline touches — texture baking, mesh
9
+ postprocessing and layout postprocessing are disabled, so their compiled ops
10
+ are never called. Packages that need torch at install time are installed at
11
+ startup. The attention backend is pinned to sdpa via import order in
12
+ sam3d_inference.py (flash_attn is not available on ZeroGPU).
13
  """
14
  import os
 
15
  import subprocess
16
+ import sys
17
  import tempfile
18
  from pathlib import Path
19
 
 
33
  if os.environ.get("HF_TOKEN"):
34
  login(token=os.environ["HF_TOKEN"])
35
 
 
36
  APP_ROOT = Path(__file__).parent
37
+ for stub in ["kaolin_stub", "pytorch3d_stub"]:
38
+ sys.path.insert(0, str(APP_ROOT / stub))
 
 
 
39
 
40
 
41
+ def _pip(*args):
 
42
  r = subprocess.run(
43
+ [sys.executable, "-m", "pip", "install", "--no-cache-dir", *args],
44
  capture_output=True, text=True, timeout=1200,
45
  )
46
+ print(f" pip {'OK' if r.returncode == 0 else 'FAIL'}: {args[-1][:70]}")
 
47
  if r.returncode != 0:
48
+ print(f" {r.stderr[-500:]}")
49
  return r.returncode == 0
50
 
51
 
52
+ print("=== Runtime installs (need torch present) ===")
53
+ # utils3d pinned to the commit MoGe expects — newer commits dropped points_to_normals
 
54
  _pip("--no-deps", "git+https://github.com/EasternJournalist/utils3d.git@3913c65d81e05e47b9f367250cf8c0f7462a0900")
 
55
  _pip("--no-deps", "sam2>=1.1.0")
56
  _pip("--no-deps", "git+https://github.com/microsoft/MoGe.git@a8c37341bc0325ca99b9d57981cc3bb2bd3e255b")
57
+ # no prebuilt gsplat wheels beyond torch 2.4 the PyPI sdist installs in JIT
58
+ # mode; kernels never compile here because rendering paths are disabled
59
  _pip("--no-deps", "gsplat")
 
 
60
 
 
61
  SAM3D_PATH = APP_ROOT / "sam-3d-objects"
62
  if not SAM3D_PATH.exists():
63
  print("Cloning sam-3d-objects...")
64
  subprocess.run(["git", "clone", "--depth", "1",
65
  "https://github.com/facebookresearch/sam-3d-objects.git",
66
  str(SAM3D_PATH)], check=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  sys.path.insert(0, str(SAM3D_PATH))
68
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  print("Downloading SAM3D checkpoints...")
70
  CKPT_DIR = snapshot_download(repo_id="facebook/sam-3d-objects",
71
  token=os.environ.get("HF_TOKEN"))
 
80
 
81
 
82
  def _gaussians_to_glb(result, out_dir):
83
+ """Extract gaussians from the pipeline result and Poisson-mesh them to GLB."""
 
84
  gs = None
85
  if hasattr(result, "save_ply"):
86
  gs = result
 
120
  for mod in ["kaolin", "pytorch3d", "spconv", "gsplat", "utils3d", "open3d", "moge"]:
121
  try:
122
  m = __import__(mod)
123
+ try:
124
+ ver = getattr(m, "__version__", "-")
125
+ except Exception:
126
+ ver = "-"
127
+ lines.append(f"{mod}: OK ({ver})")
128
  except Exception as e:
129
  lines.append(f"{mod}: FAIL - {e}")
130
  try:
 
133
  except Exception as e:
134
  lines.append(f"sam2: FAIL - {e}")
135
  try:
136
+ from sam3d_inference import SAM3DInference # noqa: F401
137
+ lines.append("SAM3DInference: importable")
138
  except Exception as e:
139
+ lines.append(f"SAM3DInference: FAIL - {e}")
140
  lines.append(f"config: {Path(CONFIG_PATH).exists()}")
141
  return "\n".join(lines)
142
 
 
156
  if image is None:
157
  return None, None, "❌ No image provided"
158
  try:
 
159
  import time
160
+ import torch
161
  t0 = time.time()
162
  print(f"GPU: {torch.cuda.get_device_name()}")
163
 
 
165
  sam2_gen = SAM2AutomaticMaskGenerator.from_pretrained("facebook/sam2-hiera-small")
166
  print(f" SAM2 loaded ({time.time()-t0:.0f}s)")
167
 
168
+ image_np = np.asarray(image)
169
  masks = sam2_gen.generate(image_np)
170
  if not masks:
171
  return None, image_np, "⚠️ No objects detected"
 
180
  del sam2_gen
181
  torch.cuda.empty_cache()
182
 
183
+ from sam3d_inference import SAM3DInference
184
+ sam3d = SAM3DInference(CONFIG_PATH)
185
  print(f" SAM3D loaded ({time.time()-t0:.0f}s)")
186
 
187
  result = sam3d(image=image_np, mask=best_mask, seed=42)
 
200
  except Exception:
201
  n_faces = 0
202
  return glb, preview, f"✓ {len(masks)} objects detected, {n_faces:,} faces ({int(time.time()-t0)}s)"
203
+ except Exception:
204
  import traceback
205
  tb = traceback.format_exc()
206
  print(tb)
207
  return None, None, f"❌ Error:\n{tb[-1500:]}"
208
 
209
 
 
210
  with gr.Blocks(title="SAM 3D Objects MCP") as demo:
211
  gr.Markdown("""
212
  # 📦 SAM 3D Objects MCP Server
flash_attn_stub/flash_attn/__init__.py DELETED
@@ -1,191 +0,0 @@
1
- """flash_attn stub – implements flash attention API using torch SDPA.
2
-
3
- This replaces the real flash_attn package on systems where it cannot be compiled
4
- (e.g. ZeroGPU with PyTorch 2.10+cu128 and no matching wheel).
5
- All functions accept the same signatures as flash_attn 2.x and delegate to
6
- torch.nn.functional.scaled_dot_product_attention.
7
- """
8
- import torch
9
- import torch.nn.functional as F
10
-
11
-
12
- def _sdpa(q, k, v, causal=False, softmax_scale=None):
13
- """Apply SDPA. q/k/v are (B, H, L, D)."""
14
- return F.scaled_dot_product_attention(
15
- q, k, v,
16
- is_causal=causal,
17
- scale=softmax_scale,
18
- )
19
-
20
-
21
- # ---------- non-varlen ----------
22
-
23
- def flash_attn_func(q, k, v, dropout_p=0.0, softmax_scale=None, causal=False,
24
- window_size=(-1, -1), softcap=0.0, alibi_slopes=None,
25
- deterministic=False, return_attn_probs=False):
26
- """q/k/v: (B, L, H, D) -> out: (B, L, H, D)"""
27
- # Permute to (B, H, L, D) for SDPA
28
- q2 = q.transpose(1, 2)
29
- k2 = k.transpose(1, 2)
30
- v2 = v.transpose(1, 2)
31
- out = _sdpa(q2, k2, v2, causal=causal, softmax_scale=softmax_scale)
32
- out = out.transpose(1, 2) # back to (B, L, H, D)
33
- return out
34
-
35
-
36
- def flash_attn_qkvpacked_func(qkv, dropout_p=0.0, softmax_scale=None, causal=False,
37
- window_size=(-1, -1), softcap=0.0, alibi_slopes=None,
38
- deterministic=False, return_attn_probs=False):
39
- """qkv: (B, L, 3, H, D) -> out: (B, L, H, D)"""
40
- q, k, v = qkv.unbind(dim=2)
41
- return flash_attn_func(q, k, v, dropout_p=dropout_p, softmax_scale=softmax_scale,
42
- causal=causal)
43
-
44
-
45
- def flash_attn_kvpacked_func(q, kv, dropout_p=0.0, softmax_scale=None, causal=False,
46
- window_size=(-1, -1), softcap=0.0, alibi_slopes=None,
47
- deterministic=False, return_attn_probs=False):
48
- """q: (B, Lq, H, D), kv: (B, Lk, 2, H, D) -> out: (B, Lq, H, D)"""
49
- k, v = kv.unbind(dim=2)
50
- return flash_attn_func(q, k, v, dropout_p=dropout_p, softmax_scale=softmax_scale,
51
- causal=causal)
52
-
53
-
54
- # ---------- varlen ----------
55
-
56
- def _varlen_sdpa(q, k, v, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k,
57
- causal=False, softmax_scale=None):
58
- """
59
- q: (total_q, H, D), k: (total_k, H, D), v: (total_k, H, D)
60
- cu_seqlens_q/k: (batch+1,) int32
61
- Returns: (total_q, H, D)
62
- """
63
- batch = cu_seqlens_q.shape[0] - 1
64
- H = q.shape[1]
65
- D = q.shape[2]
66
-
67
- # Fast path: all seqlens are equal (common case)
68
- cu_q = cu_seqlens_q.tolist()
69
- cu_k = cu_seqlens_k.tolist()
70
-
71
- all_equal = True
72
- sq0 = cu_q[1] - cu_q[0]
73
- sk0 = cu_k[1] - cu_k[0]
74
- for i in range(1, batch):
75
- if cu_q[i + 1] - cu_q[i] != sq0 or cu_k[i + 1] - cu_k[i] != sk0:
76
- all_equal = False
77
- break
78
-
79
- if all_equal and sq0 == max_seqlen_q and sk0 == max_seqlen_k:
80
- # Reshape directly – no padding needed
81
- q2 = q.reshape(batch, sq0, H, D).transpose(1, 2) # (B, H, Lq, D)
82
- k2 = k.reshape(batch, sk0, H, D).transpose(1, 2)
83
- v2 = v.reshape(batch, sk0, H, D).transpose(1, 2)
84
- out = _sdpa(q2, k2, v2, causal=causal, softmax_scale=softmax_scale)
85
- return out.transpose(1, 2).reshape(-1, H, D)
86
-
87
- # Slow path: unequal lengths – pad, compute, then gather
88
- q_padded = q.new_zeros(batch, max_seqlen_q, H, D)
89
- k_padded = k.new_zeros(batch, max_seqlen_k, H, D)
90
- v_padded = v.new_zeros(batch, max_seqlen_k, H, D)
91
-
92
- for i in range(batch):
93
- sq = cu_q[i + 1] - cu_q[i]
94
- sk = cu_k[i + 1] - cu_k[i]
95
- q_padded[i, :sq] = q[cu_q[i]:cu_q[i + 1]]
96
- k_padded[i, :sk] = k[cu_k[i]:cu_k[i + 1]]
97
- v_padded[i, :sk] = v[cu_k[i]:cu_k[i + 1]]
98
-
99
- # Create attention mask for padding
100
- q_mask = torch.arange(max_seqlen_q, device=q.device).unsqueeze(0) # (1, Lq)
101
- k_mask = torch.arange(max_seqlen_k, device=k.device).unsqueeze(0) # (1, Lk)
102
- q_lens = torch.tensor([cu_q[i + 1] - cu_q[i] for i in range(batch)],
103
- device=q.device).unsqueeze(1) # (B, 1)
104
- k_lens = torch.tensor([cu_k[i + 1] - cu_k[i] for i in range(batch)],
105
- device=k.device).unsqueeze(1) # (B, 1)
106
- # (B, 1, 1, Lk) – True where valid
107
- attn_mask = (k_mask < k_lens).unsqueeze(1).unsqueeze(2)
108
- # Also mask out query positions that are padding (their output is ignored anyway)
109
- # Use float mask: -inf for invalid positions
110
- attn_bias = torch.zeros(batch, 1, max_seqlen_q, max_seqlen_k,
111
- device=q.device, dtype=q.dtype)
112
- attn_bias.masked_fill_(~attn_mask, float('-inf'))
113
-
114
- if causal:
115
- causal_mask = torch.triu(
116
- torch.ones(max_seqlen_q, max_seqlen_k, device=q.device, dtype=torch.bool),
117
- diagonal=1
118
- )
119
- attn_bias.masked_fill_(causal_mask.unsqueeze(0).unsqueeze(0), float('-inf'))
120
-
121
- q2 = q_padded.transpose(1, 2) # (B, H, Lq, D)
122
- k2 = k_padded.transpose(1, 2)
123
- v2 = v_padded.transpose(1, 2)
124
-
125
- out = F.scaled_dot_product_attention(q2, k2, v2, attn_mask=attn_bias,
126
- scale=softmax_scale)
127
- out = out.transpose(1, 2) # (B, Lq, H, D)
128
-
129
- # Gather results back to packed format
130
- parts = []
131
- for i in range(batch):
132
- sq = cu_q[i + 1] - cu_q[i]
133
- parts.append(out[i, :sq]) # (sq, H, D)
134
- return torch.cat(parts, dim=0)
135
-
136
-
137
- def flash_attn_varlen_func(q, k, v, cu_seqlens_q, cu_seqlens_k,
138
- max_seqlen_q, max_seqlen_k,
139
- dropout_p=0.0, softmax_scale=None, causal=False,
140
- window_size=(-1, -1), softcap=0.0, alibi_slopes=None,
141
- deterministic=False, return_attn_probs=False,
142
- block_table=None):
143
- """q/k/v: (total, H, D) -> out: (total_q, H, D)"""
144
- return _varlen_sdpa(q, k, v, cu_seqlens_q, cu_seqlens_k,
145
- max_seqlen_q, max_seqlen_k,
146
- causal=causal, softmax_scale=softmax_scale)
147
-
148
-
149
- def flash_attn_varlen_qkvpacked_func(qkv, cu_seqlens, max_seqlen,
150
- dropout_p=0.0, softmax_scale=None, causal=False,
151
- window_size=(-1, -1), softcap=0.0,
152
- alibi_slopes=None, deterministic=False,
153
- return_attn_probs=False):
154
- """qkv: (total, 3, H, D) -> out: (total, H, D)"""
155
- q, k, v = qkv.unbind(dim=1)
156
- return _varlen_sdpa(q, k, v, cu_seqlens, cu_seqlens,
157
- max_seqlen, max_seqlen,
158
- causal=causal, softmax_scale=softmax_scale)
159
-
160
-
161
- def flash_attn_varlen_kvpacked_func(q, kv, cu_seqlens_q, cu_seqlens_k,
162
- max_seqlen_q, max_seqlen_k,
163
- dropout_p=0.0, softmax_scale=None, causal=False,
164
- window_size=(-1, -1), softcap=0.0,
165
- alibi_slopes=None, deterministic=False,
166
- return_attn_probs=False):
167
- """q: (total_q, H, D), kv: (total_k, 2, H, D) -> out: (total_q, H, D)"""
168
- k, v = kv.unbind(dim=1)
169
- return _varlen_sdpa(q, k, v, cu_seqlens_q, cu_seqlens_k,
170
- max_seqlen_q, max_seqlen_k,
171
- causal=causal, softmax_scale=softmax_scale)
172
-
173
-
174
- # ---------- with_kvcache (used by some SAM2 code paths) ----------
175
-
176
- def flash_attn_with_kvcache(q, k_cache, v_cache, k=None, v=None,
177
- rotary_cos=None, rotary_sin=None,
178
- cache_seqlens=None, cache_batch_idx=None,
179
- block_table=None, softmax_scale=None, causal=False,
180
- window_size=(-1, -1), softcap=0.0,
181
- rotary_interleaved=True, alibi_slopes=None,
182
- num_splits=0, return_softmax_lse=False):
183
- """Simplified kv-cache attention fallback."""
184
- # Combine current k/v with cache if provided
185
- if k is not None:
186
- k_full = torch.cat([k_cache, k], dim=1)
187
- v_full = torch.cat([v_cache, v], dim=1)
188
- else:
189
- k_full = k_cache
190
- v_full = v_cache
191
- return flash_attn_func(q, k_full, v_full, softmax_scale=softmax_scale, causal=causal)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
kaolin_stub/kaolin/__init__.py CHANGED
@@ -1,2 +1 @@
1
- """Kaolin stub - minimum API for SAM3D."""
2
- from . import ops, render, visualize
 
1
+ """Kaolin stub flexicubes only needs utils.testing.check_tensor."""
 
kaolin_stub/kaolin/ops/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Kaolin ops stub."""
2
- from . import mesh
 
 
 
kaolin_stub/kaolin/ops/mesh.py DELETED
@@ -1 +0,0 @@
1
- """Stub."""
 
 
kaolin_stub/kaolin/render/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Kaolin render stub."""
2
- from . import camera
 
 
 
kaolin_stub/kaolin/render/camera.py DELETED
@@ -1,22 +0,0 @@
1
- """Kaolin camera stub."""
2
- import torch
3
- import math
4
-
5
- class PinholeIntrinsics:
6
- def __init__(self, fov=None, focal_length=None, width=None, height=None, **kwargs):
7
- self.fov = fov
8
- self.focal_length = focal_length
9
- self.width = width
10
- self.height = height
11
-
12
- class CameraExtrinsics:
13
- def __init__(self, view_matrix=None, **kwargs):
14
- self.view_matrix = view_matrix
15
-
16
- class Camera:
17
- def __init__(self, extrinsics=None, intrinsics=None, **kwargs):
18
- self.extrinsics = extrinsics
19
- self.intrinsics = intrinsics
20
- @classmethod
21
- def from_args(cls, **kwargs):
22
- return cls(**kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
kaolin_stub/kaolin/visualize/__init__.py DELETED
@@ -1,4 +0,0 @@
1
- """Kaolin visualize stub."""
2
- class IpyTurntableVisualizer:
3
- def __init__(self, *a, **kw): pass
4
- def show(self, *a, **kw): pass
 
 
 
 
 
pytorch3d_stub/pytorch3d/renderer/camera_utils.py DELETED
@@ -1,11 +0,0 @@
1
- """pytorch3d.renderer.camera_utils stub."""
2
- import torch
3
-
4
- def camera_to_eye_at_up(camera):
5
- """Extract eye, at, up from camera. Minimal stub."""
6
- R = camera.R if hasattr(camera, "R") else torch.eye(3).unsqueeze(0)
7
- T = camera.T if hasattr(camera, "T") else torch.zeros(1, 3)
8
- eye = -torch.bmm(R, T.unsqueeze(-1)).squeeze(-1)
9
- at = torch.zeros_like(eye)
10
- up = torch.tensor([[0.0, 1.0, 0.0]])
11
- return eye, at, up
 
 
 
 
 
 
 
 
 
 
 
 
pytorch3d_stub/pytorch3d/renderer/cameras.py DELETED
@@ -1,3 +0,0 @@
1
- """pytorch3d.renderer.cameras stub."""
2
- from pytorch3d.renderer import PerspectiveCameras, CamerasBase
3
- __all__ = ["PerspectiveCameras", "CamerasBase"]
 
 
 
 
pytorch3d_stub/pytorch3d/renderer/mesh/__init__.py DELETED
@@ -1 +0,0 @@
1
- from pytorch3d.renderer import TexturesVertex
 
 
pytorch3d_stub/pytorch3d/renderer/mesh/textures.py DELETED
@@ -1 +0,0 @@
1
- from pytorch3d.renderer import TexturesVertex
 
 
pytorch3d_stub/pytorch3d/vis/__init__.py DELETED
@@ -1,17 +0,0 @@
1
- """pytorch3d.vis stub."""
2
- import importlib
3
- import warnings
4
-
5
- def __getattr__(name):
6
- if name.startswith("__") and name.endswith("__"):
7
- raise AttributeError(name)
8
- try:
9
- return importlib.import_module(f".{name}", __name__)
10
- except ImportError:
11
- pass
12
- warnings.warn(f"pytorch3d.vis stub: {name} not implemented", stacklevel=2)
13
- class _Dummy:
14
- def __init__(self, *a, **kw): pass
15
- def __call__(self, *a, **kw): return None
16
- _Dummy.__name__ = _Dummy.__qualname__ = name
17
- return _Dummy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pytorch3d_stub/pytorch3d/vis/plotly_vis.py DELETED
@@ -1,42 +0,0 @@
1
- """pytorch3d.vis.plotly_vis stub with catch-all."""
2
- import warnings
3
- from collections import namedtuple
4
-
5
- def __getattr__(name):
6
- if name.startswith("__") and name.endswith("__"):
7
- raise AttributeError(name)
8
- # Return a callable dummy for any missing name
9
- def _dummy(*a, **kw): return None
10
- _dummy.__name__ = name
11
- return _dummy
12
-
13
- def plot_scene(plots, **kwargs):
14
- return None
15
-
16
- def get_camera_wireframe(**kwargs):
17
- return None
18
-
19
- # These need to be namedtuples with _asdict() support because plot_scene.py calls ._asdict()
20
- AxisArgs = namedtuple("AxisArgs", ["showgrid", "backgroundcolor", "showticklabels", "tickcolor", "gridcolor", "zeroline"],
21
- defaults=[True, "rgb(230,230,230)", True, "black", "rgb(200,200,200)", False])
22
- Lighting = namedtuple("Lighting", ["ambient", "diffuse", "specular", "roughness", "fresnel"],
23
- defaults=[0.5, 1.0, 0.3, 0.5, 0.2])
24
-
25
- # Explicitly define all private functions that plot_scene imports
26
- def _add_camera_trace(fig, cameras, trace_name, subplot_idx, ncols, camera_scale, **kw):
27
- pass
28
-
29
- def _add_pointcloud_trace(fig, pointclouds, trace_name, subplot_idx, ncols, max_points=20000, marker_size=1, **kw):
30
- pass
31
-
32
- def _add_ray_bundle_trace(fig, ray_bundle, trace_name, subplot_idx, ncols, *a, **kw):
33
- pass
34
-
35
- def _is_ray_bundle(obj):
36
- return False
37
-
38
- def _scale_camera_to_bounds(value, vrange, is_position):
39
- return value
40
-
41
- def _update_axes_bounds(verts_center, max_expand, current_layout):
42
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,50 +1,38 @@
1
- numpy
2
- Pillow
3
- trimesh
4
- hydra-core
5
- hydra-submitit-launcher
 
6
  omegaconf
7
- einops
8
- einops-exts
9
  loguru
10
- seaborn
11
- jaxtyping
12
- rich
13
- torchvision
 
14
  opencv-python-headless
15
- ninja
16
- timm
17
- optree
18
- roma
19
- fvcore
20
- rootutils
21
  easydict
22
- scikit-image
23
- colorama
24
- ftfy
25
- astor
26
- av
27
- gdown
28
- deprecation
29
- orjson
30
- randomname
31
- simplejson
32
- pydot
33
- igraph
34
  plyfile
35
- point-cloud-utils
36
- pymeshfix
37
  xatlas
38
- pycocotools
39
- lightning
40
- pytorch-lightning
41
- scipy
42
  pyvista
43
- plotly
44
- kaleido
45
- webdataset
46
- wandb
47
- tensorboard
48
- sentence-transformers
49
- accelerate
50
- safetensors
 
1
+ # torch comes preinstalled from ZeroGPU (whitelisted versions only) — do not pin it here.
2
+ # sam2, MoGe, utils3d and gsplat need torch at install time → installed at startup in app.py.
3
+ torchvision
4
+
5
+ # config / orchestration
6
+ hydra-core==1.3.2
7
  omegaconf
 
 
8
  loguru
9
+ tqdm
10
+
11
+ # numerics / imaging
12
+ numpy
13
+ Pillow
14
  opencv-python-headless
15
+ scipy
16
+ matplotlib
 
 
 
 
17
  easydict
18
+ optree
19
+
20
+ # model loading (sam3d_objects.model.io uses lightning.pytorch)
21
+ lightning
22
+ safetensors
23
+ huggingface_hub
24
+
25
+ # 3D geometry / mesh processing
26
+ trimesh
 
 
 
27
  plyfile
28
+ open3d
 
29
  xatlas
 
 
 
 
30
  pyvista
31
+ pymeshfix
32
+ igraph
33
+
34
+ # sparse convolution — standalone binary (cumm runtime), independent of torch version
35
+ spconv-cu124==2.3.8
36
+
37
+ # sam2 runtime dependency
38
+ iopath
sam3d_inference.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal SAM 3D Objects inference wrapper.
2
+
3
+ Replaces upstream notebook/inference.py, whose module-level imports pull in
4
+ kaolin.visualize, SceneVisualizer and plotly — none of which are needed to
5
+ run the pipeline. Import this module only when sam-3d-objects is on sys.path
6
+ and a GPU context is available.
7
+ """
8
+ import os
9
+ from typing import Optional, Union
10
+
11
+ import numpy as np
12
+ from PIL import Image
13
+ from omegaconf import OmegaConf
14
+ from hydra.utils import instantiate
15
+
16
+ import sam3d_objects # noqa: F401 guarded by LIDRA_SKIP_INIT
17
+
18
+ # The attention modules read ATTN_BACKEND/SPARSE_ATTN_BACKEND from the
19
+ # environment exactly once, at import time. inference_pipeline's
20
+ # set_attention_backend() flips the env to flash_attn on datacenter GPUs,
21
+ # so the modules must be imported BEFORE the pipeline to stay on sdpa.
22
+ import sam3d_objects.model.backbone.tdfy_dit.modules.attention # noqa: F401
23
+ import sam3d_objects.model.backbone.tdfy_dit.modules.sparse # noqa: F401
24
+
25
+ from sam3d_objects.pipeline.inference_pipeline_pointmap import InferencePipelinePointMap
26
+
27
+
28
+ class SAM3DInference:
29
+ def __init__(self, config_file: str, compile: bool = False):
30
+ config = OmegaConf.load(config_file)
31
+ config.rendering_engine = "pytorch3d" # disable nvdiffrast
32
+ config.compile_model = compile
33
+ config.workspace_dir = os.path.dirname(config_file)
34
+ self._pipeline: InferencePipelinePointMap = instantiate(config)
35
+
36
+ @staticmethod
37
+ def merge_mask_to_rgba(image: np.ndarray, mask: np.ndarray) -> np.ndarray:
38
+ mask = mask.astype(np.uint8) * 255
39
+ return np.concatenate([image[..., :3], mask[..., None]], axis=-1)
40
+
41
+ def __call__(
42
+ self,
43
+ image: Union[Image.Image, np.ndarray],
44
+ mask: Optional[Union[Image.Image, np.ndarray]],
45
+ seed: Optional[int] = None,
46
+ pointmap=None,
47
+ ) -> dict:
48
+ image = self.merge_mask_to_rgba(np.asarray(image), np.asarray(mask))
49
+ return self._pipeline.run(
50
+ image,
51
+ None,
52
+ seed,
53
+ stage1_only=False,
54
+ with_mesh_postprocess=False,
55
+ with_texture_baking=False,
56
+ with_layout_postprocess=False,
57
+ use_vertex_color=True,
58
+ stage1_inference_steps=None,
59
+ pointmap=pointmap,
60
+ )