pliny-the-prompter commited on
Commit
823b2ba
Β·
verified Β·
1 Parent(s): e2a8771

Upload 133 files

Browse files
Files changed (3) hide show
  1. app.py +156 -27
  2. obliteratus/abliterate.py +9 -1
  3. obliteratus/tourney.py +2 -0
app.py CHANGED
@@ -24,9 +24,14 @@ import os
24
  import re
25
  import time
26
  import threading
 
27
  from datetime import datetime
28
  from pathlib import Path
29
 
 
 
 
 
30
  logger = logging.getLogger(__name__)
31
 
32
  # ── Container environment fixes ──────────────────────────────────────
@@ -90,18 +95,32 @@ except (ImportError, AttributeError):
90
  def _is_quota_error(exc: BaseException) -> bool:
91
  """Return True if *exc* is a ZeroGPU quota or session error.
92
 
93
- Matches quota-exceeded errors ("exceeded your GPU quota") and expired
94
- proxy tokens ("Expired ZeroGPU proxy token") β€” both mean the GPU is
95
- unavailable and the user should retry later.
 
96
  """
97
  msg = str(exc).lower()
98
  if "exceeded" in msg and "gpu quota" in msg:
99
  return True
100
  if "expired" in msg and "zerogpu" in msg:
101
  return True
 
 
102
  return False
103
 
104
 
 
 
 
 
 
 
 
 
 
 
 
105
  def _load_model_to_device(
106
  pretrained_path: str,
107
  *,
@@ -209,8 +228,8 @@ def _persist_session_meta(output_dir: str, label: str, meta: dict) -> None:
209
  p = Path(output_dir) / _SESSION_META_FILE
210
  data = {"label": label, **meta}
211
  p.write_text(_json.dumps(data, indent=2))
212
- except Exception:
213
- pass # best-effort
214
 
215
 
216
  def _recover_sessions_from_disk() -> None:
@@ -1280,6 +1299,8 @@ def benchmark(
1280
  except Exception as e:
1281
  nonlocal run_error
1282
  run_error = e
 
 
1283
 
1284
  worker = threading.Thread(target=run_pipeline, daemon=True)
1285
  worker.start()
@@ -1635,6 +1656,8 @@ def benchmark_multi_model(
1635
  except Exception as e:
1636
  nonlocal run_error
1637
  run_error = e
 
 
1638
 
1639
  worker = threading.Thread(target=run_pipeline, daemon=True)
1640
  worker.start()
@@ -1912,9 +1935,10 @@ def obliterate(model_choice: str, method_choice: str,
1912
  f"Adaptive: using architecture default `{method}` "
1913
  f"(no telemetry data yet)"
1914
  )
1915
- except Exception:
 
1916
  method = "advanced"
1917
- _adaptive_info = "Adaptive: fallback to `advanced` (could not detect architecture)"
1918
 
1919
  # Early validation: gated model access
1920
  from obliteratus.presets import is_gated
@@ -1982,6 +2006,11 @@ def obliterate(model_choice: str, method_choice: str,
1982
 
1983
  def run_pipeline():
1984
  try:
 
 
 
 
 
1985
  # Load prompts β€” custom overrides dataset dropdown
1986
  if use_custom:
1987
  on_log("Using custom user-provided prompts...")
@@ -1993,6 +2022,7 @@ def obliterate(model_choice: str, method_choice: str,
1993
  on_log(f"Loading dataset: {dataset_key}...")
1994
  harmful_all, harmless_all = load_dataset_source(dataset_key)
1995
  on_log(f"Dataset loaded: {len(harmful_all)} harmful, {len(harmless_all)} harmless prompts")
 
1996
 
1997
  # Apply volume cap (-1 = use all)
1998
  if prompt_volume > 0:
@@ -2076,6 +2106,9 @@ def obliterate(model_choice: str, method_choice: str,
2076
  pipeline.run()
2077
  except Exception as e:
2078
  error_ref[0] = e
 
 
 
2079
 
2080
  if use_custom:
2081
  source_label = "Custom (user-provided)"
@@ -2097,32 +2130,112 @@ def obliterate(model_choice: str, method_choice: str,
2097
  worker.start()
2098
 
2099
  # Stream log updates while pipeline runs (max 400 hours for large-model Optuna optimization)
 
 
2100
  _max_pipeline_secs = 400 * 60 * 60
2101
  _pipeline_start = time.time()
2102
  status_msg = "**Obliterating\u2026** (0s)"
2103
- while worker.is_alive():
2104
- status_msg = f"**Obliterating\u2026** ({_elapsed()})"
2105
- if len(log_lines) > last_yielded[0]:
2106
- last_yielded[0] = len(log_lines)
2107
- yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2108
  else:
2109
- yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
2110
- if time.time() - _pipeline_start > _max_pipeline_secs:
2111
- log_lines.append("\nTIMEOUT: Pipeline exceeded 400-hour limit.")
2112
- break
2113
- time.sleep(0.5)
 
 
2114
 
2115
  worker.join(timeout=30)
2116
 
2117
  # Handle error
2118
  if error_ref[0] is not None:
2119
- err_msg = str(error_ref[0]) or repr(error_ref[0])
2120
- log_lines.append(f"\nERROR: {err_msg}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2121
  with _lock:
2122
  _state["status"] = "idle"
2123
  _state["obliterate_started_at"] = None
2124
  _state["log"] = log_lines
2125
- yield f"**Error:** {err_msg}", "\n".join(log_lines), get_chat_header(), gr.update(), gr.update(), gr.update()
2126
  return
2127
 
2128
  # Success β€” keep model in memory for chat.
@@ -2285,7 +2398,8 @@ def obliterate(model_choice: str, method_choice: str,
2285
  can_generate = True
2286
  log_lines.append("Reloaded in 4-bit β€” chat is ready!")
2287
  except Exception as e:
2288
- log_lines.append(f"4-bit reload failed: {e}")
 
2289
  _clear_gpu()
2290
 
2291
  # -- Attempt 2: CPU offloading (slower but no extra dependencies)
@@ -2327,7 +2441,8 @@ def obliterate(model_choice: str, method_choice: str,
2327
  can_generate = True
2328
  log_lines.append("Reloaded with CPU offload β€” chat is ready (may be slower).")
2329
  except Exception as e:
2330
- log_lines.append(f"CPU offload reload failed: {e}")
 
2331
  log_lines.append("Chat unavailable. Load the saved model on a larger instance.")
2332
  with _lock:
2333
  _state["status"] = "idle"
@@ -2374,8 +2489,12 @@ def obliterate(model_choice: str, method_choice: str,
2374
 
2375
  except Exception as e:
2376
  # Ensure status never gets stuck on "obliterating"
2377
- err_msg = str(e) or repr(e)
 
 
 
2378
  log_lines.append(f"\nERROR (post-pipeline): {err_msg}")
 
2379
  with _lock:
2380
  _state["status"] = "idle"
2381
  _state["obliterate_started_at"] = None
@@ -2460,7 +2579,8 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
2460
  _needs_reload = True
2461
  else:
2462
  model.to(dev.get_device())
2463
- except Exception:
 
2464
  _needs_reload = True
2465
 
2466
  # Reload from saved checkpoint if model is missing or stale
@@ -2502,8 +2622,15 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
2502
  _state["model"] = model
2503
  _state["tokenizer"] = tokenizer
2504
  _state["status"] = "ready"
2505
- except Exception:
2506
- yield "Model failed to reload from checkpoint. Try re-obliterating."
 
 
 
 
 
 
 
2507
  return
2508
  else:
2509
  yield "No model loaded yet. Go to the **Obliterate** tab first and liberate a model."
@@ -2570,6 +2697,7 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
2570
  model.generate(**kwargs)
2571
  except Exception as e:
2572
  gen_error[0] = e
 
2573
  # Signal the streamer to stop so the main thread doesn't hang
2574
  try:
2575
  streamer.end()
@@ -2584,8 +2712,9 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
2584
  for token in streamer:
2585
  partial += token
2586
  yield partial
2587
- except Exception:
2588
  # Streamer timeout or broken pipe β€” yield whatever we have so far
 
2589
  if partial:
2590
  yield partial
2591
 
 
24
  import re
25
  import time
26
  import threading
27
+ import traceback
28
  from datetime import datetime
29
  from pathlib import Path
30
 
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
34
+ )
35
  logger = logging.getLogger(__name__)
36
 
37
  # ── Container environment fixes ──────────────────────────────────────
 
95
  def _is_quota_error(exc: BaseException) -> bool:
96
  """Return True if *exc* is a ZeroGPU quota or session error.
97
 
98
+ Matches quota-exceeded errors ("exceeded your GPU quota"), expired
99
+ proxy tokens ("Expired ZeroGPU proxy token"), and aborted GPU tasks
100
+ ("GPU task aborted") β€” all mean the GPU is unavailable and the user
101
+ should retry later.
102
  """
103
  msg = str(exc).lower()
104
  if "exceeded" in msg and "gpu quota" in msg:
105
  return True
106
  if "expired" in msg and "zerogpu" in msg:
107
  return True
108
+ if "gpu task aborted" in msg:
109
+ return True
110
  return False
111
 
112
 
113
+ def _is_zerogpu_abort(exc: BaseException) -> bool:
114
+ """Return True if *exc* is specifically a ZeroGPU 'GPU task aborted' error.
115
+
116
+ This happens when ZeroGPU's internal multiprocessing kills the worker
117
+ mid-execution β€” typically because the GPU allocation timed out, a
118
+ concurrent request conflicted, or ZeroGPU infrastructure had an issue.
119
+ """
120
+ msg = str(exc).lower()
121
+ return "gpu task aborted" in msg
122
+
123
+
124
  def _load_model_to_device(
125
  pretrained_path: str,
126
  *,
 
228
  p = Path(output_dir) / _SESSION_META_FILE
229
  data = {"label": label, **meta}
230
  p.write_text(_json.dumps(data, indent=2))
231
+ except Exception as e:
232
+ logger.debug("Failed to persist session metadata: %s", e)
233
 
234
 
235
  def _recover_sessions_from_disk() -> None:
 
1299
  except Exception as e:
1300
  nonlocal run_error
1301
  run_error = e
1302
+ logger.error("Benchmark pipeline failed: %s\n%s", e, traceback.format_exc())
1303
+ on_log(f"\n--- TRACEBACK ---\n{traceback.format_exc()}")
1304
 
1305
  worker = threading.Thread(target=run_pipeline, daemon=True)
1306
  worker.start()
 
1656
  except Exception as e:
1657
  nonlocal run_error
1658
  run_error = e
1659
+ logger.error("Tournament pipeline failed: %s\n%s", e, traceback.format_exc())
1660
+ on_log(f"\n--- TRACEBACK ---\n{traceback.format_exc()}")
1661
 
1662
  worker = threading.Thread(target=run_pipeline, daemon=True)
1663
  worker.start()
 
1935
  f"Adaptive: using architecture default `{method}` "
1936
  f"(no telemetry data yet)"
1937
  )
1938
+ except Exception as e:
1939
+ logger.warning("Adaptive method detection failed: %s", e, exc_info=True)
1940
  method = "advanced"
1941
+ _adaptive_info = f"Adaptive: fallback to `advanced` (detection error: {e})"
1942
 
1943
  # Early validation: gated model access
1944
  from obliteratus.presets import is_gated
 
2006
 
2007
  def run_pipeline():
2008
  try:
2009
+ _t_pipeline_start = time.time()
2010
+ on_log(f"[timing] Pipeline thread started (ZeroGPU allocation: {300}s)")
2011
+ if _ZEROGPU_AVAILABLE:
2012
+ on_log("[timing] Running on ZeroGPU β€” GPU may be deallocated if pipeline exceeds duration")
2013
+
2014
  # Load prompts β€” custom overrides dataset dropdown
2015
  if use_custom:
2016
  on_log("Using custom user-provided prompts...")
 
2022
  on_log(f"Loading dataset: {dataset_key}...")
2023
  harmful_all, harmless_all = load_dataset_source(dataset_key)
2024
  on_log(f"Dataset loaded: {len(harmful_all)} harmful, {len(harmless_all)} harmless prompts")
2025
+ on_log(f"[timing] Dataset loaded at +{time.time() - _t_pipeline_start:.1f}s")
2026
 
2027
  # Apply volume cap (-1 = use all)
2028
  if prompt_volume > 0:
 
2106
  pipeline.run()
2107
  except Exception as e:
2108
  error_ref[0] = e
2109
+ tb = traceback.format_exc()
2110
+ logger.error("Obliteration pipeline failed: %s\n%s", e, tb)
2111
+ on_log(f"\n--- TRACEBACK ---\n{tb}")
2112
 
2113
  if use_custom:
2114
  source_label = "Custom (user-provided)"
 
2130
  worker.start()
2131
 
2132
  # Stream log updates while pipeline runs (max 400 hours for large-model Optuna optimization)
2133
+ # Wrapped in try/except to catch ZeroGPU "GPU task aborted" β€” the abort is thrown
2134
+ # INTO the generator at the yield/sleep points, not into the worker thread.
2135
  _max_pipeline_secs = 400 * 60 * 60
2136
  _pipeline_start = time.time()
2137
  status_msg = "**Obliterating\u2026** (0s)"
2138
+ try:
2139
+ while worker.is_alive():
2140
+ status_msg = f"**Obliterating\u2026** ({_elapsed()})"
2141
+ if len(log_lines) > last_yielded[0]:
2142
+ last_yielded[0] = len(log_lines)
2143
+ yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
2144
+ else:
2145
+ yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
2146
+ if time.time() - _pipeline_start > _max_pipeline_secs:
2147
+ log_lines.append("\nTIMEOUT: Pipeline exceeded 400-hour limit.")
2148
+ break
2149
+ time.sleep(0.5)
2150
+ except Exception as e:
2151
+ # ZeroGPU can abort the generator mid-yield with "GPU task aborted"
2152
+ # or other errors. Catch here so we can show a useful message and
2153
+ # reset state instead of leaving status stuck on "obliterating".
2154
+ tb = traceback.format_exc()
2155
+ logger.error("Obliterate generator interrupted: %s\n%s", e, tb)
2156
+ log_lines.append(f"\n--- INTERRUPTED ---")
2157
+ log_lines.append(f"Generator killed after {_elapsed()}: {type(e).__qualname__}: {e}")
2158
+ log_lines.append(f"\nLast pipeline log before abort:")
2159
+ for line in log_lines[-10:]:
2160
+ if line.startswith("[timing]") or line.startswith(" ["):
2161
+ log_lines.append(f" {line}")
2162
+ with _lock:
2163
+ _state["status"] = "idle"
2164
+ _state["obliterate_started_at"] = None
2165
+ _state["log"] = log_lines
2166
+ err_msg = str(e).strip() or repr(e)
2167
+ if _is_zerogpu_abort(e):
2168
+ hint = (
2169
+ "\n\n**ZeroGPU aborted the GPU task** after " + _elapsed() + ". "
2170
+ "This is a known ZeroGPU issue β€” common causes:\n"
2171
+ "- **Timeout:** Model loading + probing exceeded the 5-minute GPU allocation\n"
2172
+ "- **Concurrent users:** Another request conflicted with yours\n"
2173
+ "- **ZeroGPU internal error:** Transient infrastructure issue\n\n"
2174
+ "**Try:** Click Obliterate again (often works on retry). "
2175
+ "If it keeps failing, try a smaller model or reduce prompt volume."
2176
+ )
2177
+ elif _is_quota_error(e):
2178
+ hint = "\n\n**ZeroGPU quota exceeded.** Wait a few minutes and retry."
2179
  else:
2180
+ hint = ""
2181
+ yield (
2182
+ f"**Error:** {type(e).__qualname__}: {err_msg}{hint}",
2183
+ "\n".join(log_lines), get_chat_header(),
2184
+ gr.update(), gr.update(), gr.update(),
2185
+ )
2186
+ return
2187
 
2188
  worker.join(timeout=30)
2189
 
2190
  # Handle error
2191
  if error_ref[0] is not None:
2192
+ err = error_ref[0]
2193
+ err_type = type(err).__qualname__
2194
+ err_str = str(err).strip()
2195
+ if err_str:
2196
+ err_msg = f"{err_type}: {err_str}"
2197
+ else:
2198
+ err_msg = repr(err)
2199
+ # Classify the error for actionable user guidance
2200
+ err_lower = err_msg.lower()
2201
+ if _is_zerogpu_abort(err):
2202
+ err_hint = (
2203
+ "\n\n**ZeroGPU task aborted.** The GPU worker was killed mid-pipeline. "
2204
+ "This is a known ZeroGPU infrastructure issue β€” common causes:\n"
2205
+ "- **Timeout:** Model loading + probing exceeded the 5-minute GPU allocation\n"
2206
+ "- **Concurrent users:** Another request conflicted with yours\n"
2207
+ "- **ZeroGPU internal error:** Transient infrastructure issue\n\n"
2208
+ "**Try:** Click Obliterate again (often works on retry). "
2209
+ "If it keeps failing, try a smaller model or reduce prompt volume."
2210
+ )
2211
+ elif _is_quota_error(err):
2212
+ err_hint = (
2213
+ "\n\n**ZeroGPU quota exceeded.** Your HuggingFace GPU quota has "
2214
+ "been used up. Wait a few minutes and try again, or run locally."
2215
+ )
2216
+ elif "cuda" in err_lower or "out of memory" in err_lower:
2217
+ err_hint = (
2218
+ "\n\n**GPU out of memory.** Try a smaller model or enable "
2219
+ "quantization (the pipeline auto-detects this for large models)."
2220
+ )
2221
+ elif "meta" in err_lower and "tensor" in err_lower:
2222
+ err_hint = (
2223
+ "\n\n**ZeroGPU device error.** The GPU was deallocated mid-run. "
2224
+ "This is a transient ZeroGPU issue β€” please retry."
2225
+ )
2226
+ elif "connection" in err_lower or "timeout" in err_lower or "resolve" in err_lower:
2227
+ err_hint = (
2228
+ "\n\n**Network error.** Could not download model weights. "
2229
+ "Check your internet connection and try again."
2230
+ )
2231
+ else:
2232
+ err_hint = ""
2233
+ log_lines.append(f"\nERROR ({err_type}): {err_msg}")
2234
  with _lock:
2235
  _state["status"] = "idle"
2236
  _state["obliterate_started_at"] = None
2237
  _state["log"] = log_lines
2238
+ yield f"**Error:** {err_msg}{err_hint}", "\n".join(log_lines), get_chat_header(), gr.update(), gr.update(), gr.update()
2239
  return
2240
 
2241
  # Success β€” keep model in memory for chat.
 
2398
  can_generate = True
2399
  log_lines.append("Reloaded in 4-bit β€” chat is ready!")
2400
  except Exception as e:
2401
+ logger.error("4-bit reload failed: %s\n%s", e, traceback.format_exc())
2402
+ log_lines.append(f"4-bit reload failed ({type(e).__qualname__}): {e}")
2403
  _clear_gpu()
2404
 
2405
  # -- Attempt 2: CPU offloading (slower but no extra dependencies)
 
2441
  can_generate = True
2442
  log_lines.append("Reloaded with CPU offload β€” chat is ready (may be slower).")
2443
  except Exception as e:
2444
+ logger.error("CPU offload reload failed: %s\n%s", e, traceback.format_exc())
2445
+ log_lines.append(f"CPU offload reload failed ({type(e).__qualname__}): {e}")
2446
  log_lines.append("Chat unavailable. Load the saved model on a larger instance.")
2447
  with _lock:
2448
  _state["status"] = "idle"
 
2489
 
2490
  except Exception as e:
2491
  # Ensure status never gets stuck on "obliterating"
2492
+ tb = traceback.format_exc()
2493
+ logger.error("Post-pipeline error: %s\n%s", e, tb)
2494
+ err_type = type(e).__qualname__
2495
+ err_msg = f"{err_type}: {str(e).strip() or repr(e)}"
2496
  log_lines.append(f"\nERROR (post-pipeline): {err_msg}")
2497
+ log_lines.append(f"\n--- TRACEBACK ---\n{tb}")
2498
  with _lock:
2499
  _state["status"] = "idle"
2500
  _state["obliterate_started_at"] = None
 
2579
  _needs_reload = True
2580
  else:
2581
  model.to(dev.get_device())
2582
+ except Exception as e:
2583
+ logger.warning("Model device check failed, triggering reload: %s", e)
2584
  _needs_reload = True
2585
 
2586
  # Reload from saved checkpoint if model is missing or stale
 
2622
  _state["model"] = model
2623
  _state["tokenizer"] = tokenizer
2624
  _state["status"] = "ready"
2625
+ except Exception as e:
2626
+ tb = traceback.format_exc()
2627
+ logger.error("Chat model reload failed: %s\n%s", e, tb)
2628
+ err_type = type(e).__qualname__
2629
+ err_str = str(e).strip() or repr(e)
2630
+ yield (
2631
+ f"Model failed to reload from checkpoint: **{err_type}:** {err_str}\n\n"
2632
+ "Try re-obliterating the model. If this persists, check the Space logs."
2633
+ )
2634
  return
2635
  else:
2636
  yield "No model loaded yet. Go to the **Obliterate** tab first and liberate a model."
 
2697
  model.generate(**kwargs)
2698
  except Exception as e:
2699
  gen_error[0] = e
2700
+ logger.error("Chat generation failed: %s\n%s", e, traceback.format_exc())
2701
  # Signal the streamer to stop so the main thread doesn't hang
2702
  try:
2703
  streamer.end()
 
2712
  for token in streamer:
2713
  partial += token
2714
  yield partial
2715
+ except Exception as e:
2716
  # Streamer timeout or broken pipe β€” yield whatever we have so far
2717
+ logger.warning("Chat streamer interrupted: %s", e)
2718
  if partial:
2719
  yield partial
2720
 
obliteratus/abliterate.py CHANGED
@@ -936,11 +936,15 @@ class AbliterationPipeline:
936
  for h in self._steering_hooks:
937
  h.remove()
938
  self._steering_hooks.clear()
 
939
  self._summon()
 
940
  self._free_gpu_memory()
941
  self._probe()
 
942
  self._free_gpu_memory()
943
  self._distill()
 
944
  # Free raw per-prompt activations now that means/subspaces are extracted
945
  self._harmful_acts.clear()
946
  self._harmless_acts.clear()
@@ -955,10 +959,14 @@ class AbliterationPipeline:
955
  self._free_gpu_memory()
956
  self._capture_baseline_kl_logits()
957
  self._excise()
 
958
  self._free_gpu_memory()
959
  self._verify()
 
960
  self._free_gpu_memory()
961
- return self._rebirth()
 
 
962
 
963
  # ── Stage 1: SUMMON ─────────────────────────────────────────────────
964
 
 
936
  for h in self._steering_hooks:
937
  h.remove()
938
  self._steering_hooks.clear()
939
+ _t0 = time.time()
940
  self._summon()
941
+ self.log(f"[timing] SUMMON complete at +{time.time() - _t0:.1f}s")
942
  self._free_gpu_memory()
943
  self._probe()
944
+ self.log(f"[timing] PROBE complete at +{time.time() - _t0:.1f}s")
945
  self._free_gpu_memory()
946
  self._distill()
947
+ self.log(f"[timing] DISTILL complete at +{time.time() - _t0:.1f}s")
948
  # Free raw per-prompt activations now that means/subspaces are extracted
949
  self._harmful_acts.clear()
950
  self._harmless_acts.clear()
 
959
  self._free_gpu_memory()
960
  self._capture_baseline_kl_logits()
961
  self._excise()
962
+ self.log(f"[timing] EXCISE complete at +{time.time() - _t0:.1f}s")
963
  self._free_gpu_memory()
964
  self._verify()
965
+ self.log(f"[timing] VERIFY complete at +{time.time() - _t0:.1f}s")
966
  self._free_gpu_memory()
967
+ result = self._rebirth()
968
+ self.log(f"[timing] REBIRTH complete at +{time.time() - _t0:.1f}s β€” pipeline finished")
969
+ return result
970
 
971
  # ── Stage 1: SUMMON ─────────────────────────────────────────────────
972
 
obliteratus/tourney.py CHANGED
@@ -1135,6 +1135,8 @@ class TourneyRunner:
1135
  return True
1136
  if "expired" in msg and "zerogpu" in msg:
1137
  return True
 
 
1138
  return False
1139
 
1140
  def _run_one_method(self, method, harmful, harmless, save_dir, verify_sz, gpu_wrapper):
 
1135
  return True
1136
  if "expired" in msg and "zerogpu" in msg:
1137
  return True
1138
+ if "gpu task aborted" in msg:
1139
+ return True
1140
  return False
1141
 
1142
  def _run_one_method(self, method, harmful, harmless, save_dir, verify_sz, gpu_wrapper):