Spaces:
Running on Zero
Running on Zero
Upload 133 files
Browse files- app.py +147 -83
- obliteratus/abliterate.py +143 -18
- obliteratus/informed_pipeline.py +19 -0
- obliteratus/telemetry.py +21 -19
- obliteratus/tourney.py +23 -9
app.py
CHANGED
|
@@ -297,15 +297,16 @@ def _recover_after_obliterate():
|
|
| 297 |
_short = model_choice.split("/")[-1] if "/" in model_choice else model_choice
|
| 298 |
_label = f"{method} on {_short} ({_ts}) [recovered]"
|
| 299 |
_last_obliterated_label = _label
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
| 309 |
|
| 310 |
if not recovered:
|
| 311 |
with _lock:
|
|
@@ -430,15 +431,16 @@ def _recover_sessions_from_disk() -> None:
|
|
| 430 |
label = data.get("label", p.name)
|
| 431 |
if label in _session_models:
|
| 432 |
continue # already registered
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
|
|
|
| 442 |
found_any = True
|
| 443 |
# Track the latest for auto-select
|
| 444 |
_last_obliterated_label = label
|
|
@@ -453,10 +455,10 @@ def _recover_sessions_from_disk() -> None:
|
|
| 453 |
# If we recovered sessions and _state has no valid output_dir, set it to
|
| 454 |
# the most recent checkpoint so chat_respond can reload from disk.
|
| 455 |
# Also overwrite a stale output_dir that points to a non-existent path.
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
latest = _last_obliterated_label
|
| 461 |
if latest and latest in _session_models:
|
| 462 |
_state["output_dir"] = _session_models[latest]["output_dir"]
|
|
@@ -821,6 +823,15 @@ def push_session_to_hub(
|
|
| 821 |
if not output_dir or not Path(output_dir).exists():
|
| 822 |
yield f"**Error:** Model directory not found: `{output_dir}`", ""
|
| 823 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
|
| 825 |
# Resolve repo ID
|
| 826 |
repo_id = hub_repo_id.strip() if hub_repo_id else ""
|
|
@@ -1086,11 +1097,9 @@ def _cleanup_disk():
|
|
| 1086 |
shutil.rmtree(path, ignore_errors=True)
|
| 1087 |
freed += size
|
| 1088 |
|
| 1089 |
-
# Clear session model cache (checkpoints are gone)
|
| 1090 |
-
_session_models.clear()
|
| 1091 |
-
|
| 1092 |
-
# Clear stale output_dir reference (checkpoints were just deleted)
|
| 1093 |
with _lock:
|
|
|
|
| 1094 |
_state["output_dir"] = None
|
| 1095 |
_state["model_name"] = None
|
| 1096 |
_state["method"] = None
|
|
@@ -1574,14 +1583,15 @@ def benchmark(
|
|
| 1574 |
bench_save_path = f"/tmp/bench_{method_key}"
|
| 1575 |
if entry.get("error") is None:
|
| 1576 |
label = f"{entry['method']} on {model_id.split('/')[-1]}"
|
| 1577 |
-
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
|
|
|
|
| 1585 |
_persist_session_meta(bench_save_path, label, {
|
| 1586 |
"model_id": model_id,
|
| 1587 |
"model_choice": model_choice,
|
|
@@ -1928,14 +1938,15 @@ def benchmark_multi_model(
|
|
| 1928 |
mm_save_path = f"/tmp/bench_mm_{mi}"
|
| 1929 |
if entry.get("error") is None:
|
| 1930 |
label = f"{method_key} on {model_id.split('/')[-1]}"
|
| 1931 |
-
|
| 1932 |
-
|
| 1933 |
-
|
| 1934 |
-
|
| 1935 |
-
|
| 1936 |
-
|
| 1937 |
-
|
| 1938 |
-
|
|
|
|
| 1939 |
_persist_session_meta(mm_save_path, label, {
|
| 1940 |
"model_id": model_id,
|
| 1941 |
"model_choice": model_display,
|
|
@@ -2067,9 +2078,17 @@ def _restore_and_run_stage(pipeline, stage_method_name):
|
|
| 2067 |
Module-level function so it is picklable for ZeroGPU serialization.
|
| 2068 |
Wraps execution in try/except to preserve the full traceback before
|
| 2069 |
ZeroGPU's error handler reduces it to just the exception class name.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2070 |
"""
|
| 2071 |
try:
|
| 2072 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2073 |
getattr(pipeline, stage_method_name)()
|
| 2074 |
except Exception as e:
|
| 2075 |
import traceback as _tb
|
|
@@ -2209,7 +2228,15 @@ def obliterate(model_choice: str, method_choice: str,
|
|
| 2209 |
return
|
| 2210 |
|
| 2211 |
# Resolve dataset source — custom prompts override the dropdown
|
|
|
|
| 2212 |
use_custom = custom_harmful and custom_harmful.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2213 |
dataset_key = get_source_key_from_label(dataset_source_choice) if dataset_source_choice else "builtin"
|
| 2214 |
|
| 2215 |
# Unstick stale "obliterating" status left behind by ZeroGPU timeout
|
|
@@ -2366,37 +2393,50 @@ def obliterate(model_choice: str, method_choice: str,
|
|
| 2366 |
# ── Staged GPU execution (tourney-style) ──────────────────
|
| 2367 |
# Each stage gets its own 5-minute GPU allocation instead of
|
| 2368 |
# sharing a single 300s budget. Between stages the model is
|
| 2369 |
-
#
|
|
|
|
|
|
|
|
|
|
| 2370 |
on_log("[staged] ZeroGPU detected — using staged GPU execution (up to 5 min per stage)")
|
| 2371 |
|
| 2372 |
-
|
| 2373 |
-
|
| 2374 |
-
|
| 2375 |
-
|
| 2376 |
-
|
| 2377 |
-
|
| 2378 |
-
|
| 2379 |
-
|
| 2380 |
-
|
| 2381 |
-
|
| 2382 |
-
|
| 2383 |
-
|
| 2384 |
-
|
| 2385 |
-
|
| 2386 |
-
|
| 2387 |
-
|
| 2388 |
-
|
| 2389 |
-
|
| 2390 |
-
|
| 2391 |
-
|
| 2392 |
-
|
| 2393 |
-
|
| 2394 |
-
|
| 2395 |
-
|
| 2396 |
-
|
| 2397 |
-
|
| 2398 |
-
|
| 2399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2400 |
else:
|
| 2401 |
# ── Local/non-ZeroGPU: single-shot execution ──────────────
|
| 2402 |
on_log(f"[timing] Running locally (no GPU time limit)")
|
|
@@ -2531,6 +2571,20 @@ def obliterate(model_choice: str, method_choice: str,
|
|
| 2531 |
|
| 2532 |
worker.join(timeout=30)
|
| 2533 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2534 |
# Handle error
|
| 2535 |
if error_ref[0] is not None:
|
| 2536 |
_mark_live_log_finished()
|
|
@@ -2851,7 +2905,8 @@ def obliterate(model_choice: str, method_choice: str,
|
|
| 2851 |
# Set skip flag so the .change handler doesn't trigger a wasteful
|
| 2852 |
# GPU re-allocation — the model is already loaded.
|
| 2853 |
global _skip_session_load
|
| 2854 |
-
|
|
|
|
| 2855 |
_dd_update = gr.update(
|
| 2856 |
choices=_get_session_model_choices(),
|
| 2857 |
value=_last_obliterated_label or None,
|
|
@@ -3151,8 +3206,11 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 3151 |
# Skip if the obliterate function just set the dropdown value — the model
|
| 3152 |
# is already loaded and we'd just waste GPU quota re-allocating.
|
| 3153 |
global _skip_session_load
|
| 3154 |
-
|
| 3155 |
-
|
|
|
|
|
|
|
|
|
|
| 3156 |
# Verify the model is actually usable — not just that status says "ready".
|
| 3157 |
# ZeroGPU can evict the model while status stays "ready", and the counter
|
| 3158 |
# can get out of sync if only one dropdown .change fires instead of both.
|
|
@@ -3336,6 +3394,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 3336 |
_clear_gpu()
|
| 3337 |
with _lock:
|
| 3338 |
_state["status"] = "idle"
|
|
|
|
| 3339 |
yield (
|
| 3340 |
f"**Error:** Could not load {choice} from checkpoint (GPU too small).",
|
| 3341 |
get_chat_header(),
|
|
@@ -3391,6 +3450,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 3391 |
if error_ref[0] is not None:
|
| 3392 |
with _lock:
|
| 3393 |
_state["status"] = "idle"
|
|
|
|
| 3394 |
yield f"**Error loading {choice}:** {error_ref[0]}", get_chat_header()
|
| 3395 |
return
|
| 3396 |
|
|
@@ -3400,6 +3460,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 3400 |
_state["tokenizer"] = pipeline.handle.tokenizer
|
| 3401 |
_state["steering"] = None
|
| 3402 |
_state["status"] = "ready"
|
|
|
|
| 3403 |
_state["output_dir"] = "/tmp/obliterated" # re-abliteration fallback path
|
| 3404 |
|
| 3405 |
pipeline_ref[0] = None
|
|
@@ -3640,6 +3701,9 @@ def ab_chat_respond(message: str, history_left: list[dict], history_right: list[
|
|
| 3640 |
|
| 3641 |
except Exception as e:
|
| 3642 |
original_response = f"*Could not load original model for comparison: {e}*"
|
|
|
|
|
|
|
|
|
|
| 3643 |
|
| 3644 |
# Restore abliterated model to GPU for subsequent chat/operations.
|
| 3645 |
# Use torch.device("cuda") rather than the captured abl_device, since
|
|
@@ -3728,7 +3792,7 @@ def strength_sweep(model_choice: str, method_choice: str,
|
|
| 3728 |
nonlocal run_error
|
| 3729 |
run_error = e
|
| 3730 |
|
| 3731 |
-
worker = threading.Thread(target=_run_sweep_point)
|
| 3732 |
worker.start()
|
| 3733 |
while worker.is_alive():
|
| 3734 |
worker.join(timeout=2.0)
|
|
@@ -4122,15 +4186,16 @@ def export_artifacts():
|
|
| 4122 |
import zipfile
|
| 4123 |
import os
|
| 4124 |
|
| 4125 |
-
|
| 4126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4127 |
|
| 4128 |
export_dir = tempfile.mkdtemp(prefix="obliteratus_export_")
|
| 4129 |
|
| 4130 |
-
model_name = _state.get("model_name", "unknown")
|
| 4131 |
-
method = _state.get("method", "unknown")
|
| 4132 |
-
log_lines = _state.get("log", [])
|
| 4133 |
-
|
| 4134 |
exported_files = []
|
| 4135 |
|
| 4136 |
# 1. Pipeline log
|
|
@@ -4145,7 +4210,6 @@ def export_artifacts():
|
|
| 4145 |
exported_files.append("pipeline_log.txt")
|
| 4146 |
|
| 4147 |
# 2. Steering metadata (refusal directions + strong layers)
|
| 4148 |
-
steering = _state.get("steering")
|
| 4149 |
if steering:
|
| 4150 |
# Save directions as .pt
|
| 4151 |
directions = steering.get("refusal_directions", {})
|
|
|
|
| 297 |
_short = model_choice.split("/")[-1] if "/" in model_choice else model_choice
|
| 298 |
_label = f"{method} on {_short} ({_ts}) [recovered]"
|
| 299 |
_last_obliterated_label = _label
|
| 300 |
+
with _lock:
|
| 301 |
+
_session_models[_label] = {
|
| 302 |
+
"model_id": data.get("model_id", model_choice),
|
| 303 |
+
"model_choice": model_choice,
|
| 304 |
+
"method": method or "unknown",
|
| 305 |
+
"dataset_key": "",
|
| 306 |
+
"prompt_volume": 0,
|
| 307 |
+
"output_dir": save_dir,
|
| 308 |
+
"source": "recovered",
|
| 309 |
+
}
|
| 310 |
|
| 311 |
if not recovered:
|
| 312 |
with _lock:
|
|
|
|
| 431 |
label = data.get("label", p.name)
|
| 432 |
if label in _session_models:
|
| 433 |
continue # already registered
|
| 434 |
+
with _lock:
|
| 435 |
+
_session_models[label] = {
|
| 436 |
+
"model_id": data.get("model_id", ""),
|
| 437 |
+
"model_choice": data.get("model_choice", data.get("model_id", "")),
|
| 438 |
+
"method": data.get("method", "unknown"),
|
| 439 |
+
"dataset_key": data.get("dataset_key", ""),
|
| 440 |
+
"prompt_volume": data.get("prompt_volume", 0),
|
| 441 |
+
"output_dir": str(p),
|
| 442 |
+
"source": data.get("source", "recovered"),
|
| 443 |
+
}
|
| 444 |
found_any = True
|
| 445 |
# Track the latest for auto-select
|
| 446 |
_last_obliterated_label = label
|
|
|
|
| 455 |
# If we recovered sessions and _state has no valid output_dir, set it to
|
| 456 |
# the most recent checkpoint so chat_respond can reload from disk.
|
| 457 |
# Also overwrite a stale output_dir that points to a non-existent path.
|
| 458 |
+
with _lock:
|
| 459 |
+
_cur_dir = _state.get("output_dir")
|
| 460 |
+
_needs_update = not _cur_dir or not Path(_cur_dir).exists()
|
| 461 |
+
if found_any and _needs_update:
|
| 462 |
latest = _last_obliterated_label
|
| 463 |
if latest and latest in _session_models:
|
| 464 |
_state["output_dir"] = _session_models[latest]["output_dir"]
|
|
|
|
| 823 |
if not output_dir or not Path(output_dir).exists():
|
| 824 |
yield f"**Error:** Model directory not found: `{output_dir}`", ""
|
| 825 |
return
|
| 826 |
+
# Validate output_dir is under /tmp to prevent directory traversal
|
| 827 |
+
try:
|
| 828 |
+
_resolved = Path(output_dir).resolve()
|
| 829 |
+
if not str(_resolved).startswith("/tmp/"):
|
| 830 |
+
yield "**Error:** Model directory must be under `/tmp`.", ""
|
| 831 |
+
return
|
| 832 |
+
except Exception:
|
| 833 |
+
yield "**Error:** Invalid model directory path.", ""
|
| 834 |
+
return
|
| 835 |
|
| 836 |
# Resolve repo ID
|
| 837 |
repo_id = hub_repo_id.strip() if hub_repo_id else ""
|
|
|
|
| 1097 |
shutil.rmtree(path, ignore_errors=True)
|
| 1098 |
freed += size
|
| 1099 |
|
| 1100 |
+
# Clear session model cache and stale state (checkpoints are gone)
|
|
|
|
|
|
|
|
|
|
| 1101 |
with _lock:
|
| 1102 |
+
_session_models.clear()
|
| 1103 |
_state["output_dir"] = None
|
| 1104 |
_state["model_name"] = None
|
| 1105 |
_state["method"] = None
|
|
|
|
| 1583 |
bench_save_path = f"/tmp/bench_{method_key}"
|
| 1584 |
if entry.get("error") is None:
|
| 1585 |
label = f"{entry['method']} on {model_id.split('/')[-1]}"
|
| 1586 |
+
with _lock:
|
| 1587 |
+
_bench_configs[label] = {
|
| 1588 |
+
"model_id": model_id,
|
| 1589 |
+
"model_choice": model_choice,
|
| 1590 |
+
"method": method_key,
|
| 1591 |
+
"dataset_key": dataset_key,
|
| 1592 |
+
"prompt_volume": prompt_volume,
|
| 1593 |
+
"output_dir": bench_save_path,
|
| 1594 |
+
}
|
| 1595 |
_persist_session_meta(bench_save_path, label, {
|
| 1596 |
"model_id": model_id,
|
| 1597 |
"model_choice": model_choice,
|
|
|
|
| 1938 |
mm_save_path = f"/tmp/bench_mm_{mi}"
|
| 1939 |
if entry.get("error") is None:
|
| 1940 |
label = f"{method_key} on {model_id.split('/')[-1]}"
|
| 1941 |
+
with _lock:
|
| 1942 |
+
_bench_configs[label] = {
|
| 1943 |
+
"model_id": model_id,
|
| 1944 |
+
"model_choice": model_display,
|
| 1945 |
+
"method": method_key,
|
| 1946 |
+
"dataset_key": dataset_key,
|
| 1947 |
+
"prompt_volume": prompt_volume,
|
| 1948 |
+
"output_dir": mm_save_path,
|
| 1949 |
+
}
|
| 1950 |
_persist_session_meta(mm_save_path, label, {
|
| 1951 |
"model_id": model_id,
|
| 1952 |
"model_choice": model_display,
|
|
|
|
| 2078 |
Module-level function so it is picklable for ZeroGPU serialization.
|
| 2079 |
Wraps execution in try/except to preserve the full traceback before
|
| 2080 |
ZeroGPU's error handler reduces it to just the exception class name.
|
| 2081 |
+
|
| 2082 |
+
If the pipeline model is not in memory (ZeroGPU state loss), the stage
|
| 2083 |
+
method itself handles recovery via ``_reload_model_for_stage()`` and
|
| 2084 |
+
``_load_staged_state()`` when ``_staged_state_dir`` is set.
|
| 2085 |
"""
|
| 2086 |
try:
|
| 2087 |
+
# Try to restore model to GPU if it's already in memory (same-process
|
| 2088 |
+
# case or non-ZeroGPU). If the model is None (ZeroGPU state loss),
|
| 2089 |
+
# skip — the stage method handles recovery.
|
| 2090 |
+
if pipeline.handle is not None and pipeline.handle.model is not None:
|
| 2091 |
+
pipeline._restore_to_gpu()
|
| 2092 |
getattr(pipeline, stage_method_name)()
|
| 2093 |
except Exception as e:
|
| 2094 |
import traceback as _tb
|
|
|
|
| 2228 |
return
|
| 2229 |
|
| 2230 |
# Resolve dataset source — custom prompts override the dropdown
|
| 2231 |
+
_MAX_CUSTOM_PROMPT_LINES = 10_000
|
| 2232 |
use_custom = custom_harmful and custom_harmful.strip()
|
| 2233 |
+
if use_custom and custom_harmful.count("\n") > _MAX_CUSTOM_PROMPT_LINES:
|
| 2234 |
+
yield (
|
| 2235 |
+
f"**Error:** Custom prompts exceed {_MAX_CUSTOM_PROMPT_LINES} lines. "
|
| 2236 |
+
"Please reduce the number of prompts to avoid memory exhaustion.",
|
| 2237 |
+
"", gr.update(), gr.update(), gr.update(), gr.update(),
|
| 2238 |
+
)
|
| 2239 |
+
return
|
| 2240 |
dataset_key = get_source_key_from_label(dataset_source_choice) if dataset_source_choice else "builtin"
|
| 2241 |
|
| 2242 |
# Unstick stale "obliterating" status left behind by ZeroGPU timeout
|
|
|
|
| 2393 |
# ── Staged GPU execution (tourney-style) ──────────────────
|
| 2394 |
# Each stage gets its own 5-minute GPU allocation instead of
|
| 2395 |
# sharing a single 300s budget. Between stages the model is
|
| 2396 |
+
# saved to disk so state survives ZeroGPU's cross-process
|
| 2397 |
+
# serialization (each @spaces.GPU call runs in a separate
|
| 2398 |
+
# worker process that pickles args, so in-memory mutations
|
| 2399 |
+
# to the pipeline don't propagate back).
|
| 2400 |
on_log("[staged] ZeroGPU detected — using staged GPU execution (up to 5 min per stage)")
|
| 2401 |
|
| 2402 |
+
# Create a temp dir for cross-process state persistence
|
| 2403 |
+
import tempfile as _tempfile
|
| 2404 |
+
_staged_dir = _tempfile.mkdtemp(prefix="obliterate_staged_")
|
| 2405 |
+
pipeline._staged_state_dir = _staged_dir
|
| 2406 |
+
on_log(f"[staged] State persistence dir: {_staged_dir}")
|
| 2407 |
+
|
| 2408 |
+
try:
|
| 2409 |
+
if method == "informed":
|
| 2410 |
+
# Informed pipeline: SUMMON+PROBE | ANALYZE+DISTILL+EXCISE | VERIFY+REBIRTH
|
| 2411 |
+
on_log("\n\u26a1 [staged] GPU Stage 1/3: SUMMON + PROBE")
|
| 2412 |
+
_gpu_run_picklable(pipeline, pipeline.run_stage_summon_probe, time.time())
|
| 2413 |
+
on_log("[staged] GPU released after Stage 1\n")
|
| 2414 |
+
|
| 2415 |
+
on_log("\u26a1 [staged] GPU Stage 2/3: ANALYZE + DISTILL + EXCISE")
|
| 2416 |
+
_gpu_run_picklable(pipeline, _restore_and_run_stage, pipeline, "run_stage_analyze_distill_excise")
|
| 2417 |
+
on_log("[staged] GPU released after Stage 2\n")
|
| 2418 |
+
|
| 2419 |
+
on_log("\u26a1 [staged] GPU Stage 3/3: VERIFY + REBIRTH")
|
| 2420 |
+
_gpu_run_picklable(pipeline, _restore_and_run_stage, pipeline, "run_stage_verify_rebirth_informed")
|
| 2421 |
+
else:
|
| 2422 |
+
# Standard pipeline: SUMMON+PROBE | DISTILL+EXCISE | VERIFY+REBIRTH
|
| 2423 |
+
on_log("\n\u26a1 [staged] GPU Stage 1/3: SUMMON + PROBE")
|
| 2424 |
+
_gpu_run_picklable(pipeline, pipeline.run_stage_summon_probe, time.time())
|
| 2425 |
+
on_log("[staged] GPU released after Stage 1\n")
|
| 2426 |
+
|
| 2427 |
+
on_log("\u26a1 [staged] GPU Stage 2/3: DISTILL + EXCISE")
|
| 2428 |
+
_gpu_run_picklable(pipeline, _restore_and_run_stage, pipeline, "run_stage_distill_excise")
|
| 2429 |
+
on_log("[staged] GPU released after Stage 2\n")
|
| 2430 |
+
|
| 2431 |
+
on_log("\u26a1 [staged] GPU Stage 3/3: VERIFY + REBIRTH")
|
| 2432 |
+
_gpu_run_picklable(pipeline, _restore_and_run_stage, pipeline, "run_stage_verify_rebirth")
|
| 2433 |
+
finally:
|
| 2434 |
+
# Clean up staged state temp dir
|
| 2435 |
+
import shutil as _shutil
|
| 2436 |
+
try:
|
| 2437 |
+
_shutil.rmtree(_staged_dir, ignore_errors=True)
|
| 2438 |
+
except Exception:
|
| 2439 |
+
pass
|
| 2440 |
else:
|
| 2441 |
# ── Local/non-ZeroGPU: single-shot execution ──────────────
|
| 2442 |
on_log(f"[timing] Running locally (no GPU time limit)")
|
|
|
|
| 2571 |
|
| 2572 |
worker.join(timeout=30)
|
| 2573 |
|
| 2574 |
+
# If worker is still alive after join timeout, it's hung — treat as error
|
| 2575 |
+
if worker.is_alive():
|
| 2576 |
+
_mark_live_log_finished()
|
| 2577 |
+
log_lines.append("\nERROR: Pipeline worker thread did not finish within 30s after loop exit.")
|
| 2578 |
+
with _lock:
|
| 2579 |
+
_state["status"] = "idle"
|
| 2580 |
+
_state["obliterate_started_at"] = None
|
| 2581 |
+
_state["log"] = log_lines
|
| 2582 |
+
yield (
|
| 2583 |
+
"**Error:** Pipeline worker hung after completion. Check logs for details.",
|
| 2584 |
+
"\n".join(log_lines), get_chat_header(), gr.update(), gr.update(), gr.update(),
|
| 2585 |
+
)
|
| 2586 |
+
return
|
| 2587 |
+
|
| 2588 |
# Handle error
|
| 2589 |
if error_ref[0] is not None:
|
| 2590 |
_mark_live_log_finished()
|
|
|
|
| 2905 |
# Set skip flag so the .change handler doesn't trigger a wasteful
|
| 2906 |
# GPU re-allocation — the model is already loaded.
|
| 2907 |
global _skip_session_load
|
| 2908 |
+
with _lock:
|
| 2909 |
+
_skip_session_load = 2 # both session_model_dd and ab_session_model_dd fire .change
|
| 2910 |
_dd_update = gr.update(
|
| 2911 |
choices=_get_session_model_choices(),
|
| 2912 |
value=_last_obliterated_label or None,
|
|
|
|
| 3206 |
# Skip if the obliterate function just set the dropdown value — the model
|
| 3207 |
# is already loaded and we'd just waste GPU quota re-allocating.
|
| 3208 |
global _skip_session_load
|
| 3209 |
+
with _lock:
|
| 3210 |
+
_should_skip = _skip_session_load > 0
|
| 3211 |
+
if _should_skip:
|
| 3212 |
+
_skip_session_load -= 1
|
| 3213 |
+
if _should_skip:
|
| 3214 |
# Verify the model is actually usable — not just that status says "ready".
|
| 3215 |
# ZeroGPU can evict the model while status stays "ready", and the counter
|
| 3216 |
# can get out of sync if only one dropdown .change fires instead of both.
|
|
|
|
| 3394 |
_clear_gpu()
|
| 3395 |
with _lock:
|
| 3396 |
_state["status"] = "idle"
|
| 3397 |
+
_state["obliterate_started_at"] = None
|
| 3398 |
yield (
|
| 3399 |
f"**Error:** Could not load {choice} from checkpoint (GPU too small).",
|
| 3400 |
get_chat_header(),
|
|
|
|
| 3450 |
if error_ref[0] is not None:
|
| 3451 |
with _lock:
|
| 3452 |
_state["status"] = "idle"
|
| 3453 |
+
_state["obliterate_started_at"] = None
|
| 3454 |
yield f"**Error loading {choice}:** {error_ref[0]}", get_chat_header()
|
| 3455 |
return
|
| 3456 |
|
|
|
|
| 3460 |
_state["tokenizer"] = pipeline.handle.tokenizer
|
| 3461 |
_state["steering"] = None
|
| 3462 |
_state["status"] = "ready"
|
| 3463 |
+
_state["obliterate_started_at"] = None
|
| 3464 |
_state["output_dir"] = "/tmp/obliterated" # re-abliteration fallback path
|
| 3465 |
|
| 3466 |
pipeline_ref[0] = None
|
|
|
|
| 3701 |
|
| 3702 |
except Exception as e:
|
| 3703 |
original_response = f"*Could not load original model for comparison: {e}*"
|
| 3704 |
+
# Ensure GPU memory is freed even if original model load/gen failed
|
| 3705 |
+
gc.collect()
|
| 3706 |
+
dev.empty_cache()
|
| 3707 |
|
| 3708 |
# Restore abliterated model to GPU for subsequent chat/operations.
|
| 3709 |
# Use torch.device("cuda") rather than the captured abl_device, since
|
|
|
|
| 3792 |
nonlocal run_error
|
| 3793 |
run_error = e
|
| 3794 |
|
| 3795 |
+
worker = threading.Thread(target=_run_sweep_point, daemon=True)
|
| 3796 |
worker.start()
|
| 3797 |
while worker.is_alive():
|
| 3798 |
worker.join(timeout=2.0)
|
|
|
|
| 4186 |
import zipfile
|
| 4187 |
import os
|
| 4188 |
|
| 4189 |
+
with _lock:
|
| 4190 |
+
if _state["status"] != "ready":
|
| 4191 |
+
return None, "No abliterated model loaded. Run obliteration first."
|
| 4192 |
+
model_name = _state.get("model_name", "unknown")
|
| 4193 |
+
method = _state.get("method", "unknown")
|
| 4194 |
+
log_lines = list(_state.get("log", [])) # copy to avoid mutation
|
| 4195 |
+
steering = _state.get("steering")
|
| 4196 |
|
| 4197 |
export_dir = tempfile.mkdtemp(prefix="obliteratus_export_")
|
| 4198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4199 |
exported_files = []
|
| 4200 |
|
| 4201 |
# 1. Pipeline log
|
|
|
|
| 4210 |
exported_files.append("pipeline_log.txt")
|
| 4211 |
|
| 4212 |
# 2. Steering metadata (refusal directions + strong layers)
|
|
|
|
| 4213 |
if steering:
|
| 4214 |
# Save directions as .pt
|
| 4215 |
directions = steering.get("refusal_directions", {})
|
obliteratus/abliterate.py
CHANGED
|
@@ -763,6 +763,7 @@ class AbliterationPipeline:
|
|
| 763 |
self.refinement_passes = min(self.refinement_passes, 1)
|
| 764 |
|
| 765 |
self.handle: ModelHandle | None = None
|
|
|
|
| 766 |
self._staged_t0: float = time.time() # overwritten in run_stage_summon_probe; init here for safety
|
| 767 |
self.refusal_directions: dict[int, torch.Tensor] = {} # per-layer primary direction
|
| 768 |
self.refusal_subspaces: dict[int, torch.Tensor] = {} # per-layer SVD subspace (n_dirs x hidden)
|
|
@@ -998,6 +999,10 @@ class AbliterationPipeline:
|
|
| 998 |
|
| 999 |
After this stage the caller should call ``_offload_to_cpu()`` to
|
| 1000 |
move the model off GPU before releasing the ZeroGPU allocation.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1001 |
"""
|
| 1002 |
for h in self._steering_hooks:
|
| 1003 |
h.remove()
|
|
@@ -1010,6 +1015,8 @@ class AbliterationPipeline:
|
|
| 1010 |
self._probe()
|
| 1011 |
self.log(f"[timing] PROBE complete at +{time.time() - self._staged_t0:.1f}s")
|
| 1012 |
self._free_gpu_memory()
|
|
|
|
|
|
|
| 1013 |
|
| 1014 |
def run_stage_distill_excise(self):
|
| 1015 |
"""GPU Stage 2: Extract directions, modify weights, save checkpoint
|
|
@@ -1019,14 +1026,19 @@ class AbliterationPipeline:
|
|
| 1019 |
move the model off GPU before releasing the ZeroGPU allocation.
|
| 1020 |
"""
|
| 1021 |
if self.handle is None or self.handle.model is None:
|
| 1022 |
-
|
| 1023 |
-
"Model not
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
# Reset time budget for this GPU allocation
|
| 1031 |
self._pipeline_start_time = time.time()
|
| 1032 |
self._distill()
|
|
@@ -1053,10 +1065,16 @@ class AbliterationPipeline:
|
|
| 1053 |
Path to the saved model directory.
|
| 1054 |
"""
|
| 1055 |
if self.handle is None or self.handle.model is None:
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
# Reset time budget for this GPU allocation
|
| 1061 |
self._pipeline_start_time = time.time()
|
| 1062 |
self._verify()
|
|
@@ -1111,11 +1129,9 @@ class AbliterationPipeline:
|
|
| 1111 |
self.refusal_subspaces[k] = v.cpu()
|
| 1112 |
|
| 1113 |
# Move baseline KL logits to CPU
|
| 1114 |
-
if
|
| 1115 |
-
self.
|
| 1116 |
-
|
| 1117 |
-
for t in self._kl_baseline_logits
|
| 1118 |
-
]
|
| 1119 |
|
| 1120 |
dev.free_gpu_memory()
|
| 1121 |
elapsed = time.time() - t0
|
|
@@ -1137,9 +1153,108 @@ class AbliterationPipeline:
|
|
| 1137 |
self.log(f"[staged] Moving model to {target}...")
|
| 1138 |
t0 = time.time()
|
| 1139 |
self.handle.model.to(target)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
elapsed = time.time() - t0
|
| 1141 |
self.log(f"[staged] Model restored to {target} ({elapsed:.1f}s)")
|
| 1142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1143 |
# ── Stage 1: SUMMON ─────────────────────────���───────────────────────
|
| 1144 |
|
| 1145 |
def _summon(self):
|
|
@@ -6233,7 +6348,17 @@ class AbliterationPipeline:
|
|
| 6233 |
dat_file = Path(offload_dir) / f"{key}.dat"
|
| 6234 |
if safetensors_file.exists():
|
| 6235 |
data = load_file(str(safetensors_file))
|
| 6236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6237 |
elif dat_file.exists():
|
| 6238 |
# Accelerate's .dat format: raw tensor bytes with shape/dtype metadata
|
| 6239 |
import numpy as np
|
|
|
|
| 763 |
self.refinement_passes = min(self.refinement_passes, 1)
|
| 764 |
|
| 765 |
self.handle: ModelHandle | None = None
|
| 766 |
+
self._staged_state_dir: str | None = None # temp dir for cross-process state persistence (ZeroGPU)
|
| 767 |
self._staged_t0: float = time.time() # overwritten in run_stage_summon_probe; init here for safety
|
| 768 |
self.refusal_directions: dict[int, torch.Tensor] = {} # per-layer primary direction
|
| 769 |
self.refusal_subspaces: dict[int, torch.Tensor] = {} # per-layer SVD subspace (n_dirs x hidden)
|
|
|
|
| 999 |
|
| 1000 |
After this stage the caller should call ``_offload_to_cpu()`` to
|
| 1001 |
move the model off GPU before releasing the ZeroGPU allocation.
|
| 1002 |
+
|
| 1003 |
+
When ``_staged_state_dir`` is set, intermediate state (activations,
|
| 1004 |
+
means, routing data) is saved to disk at the end of this stage so
|
| 1005 |
+
it can survive ZeroGPU's cross-process serialization boundary.
|
| 1006 |
"""
|
| 1007 |
for h in self._steering_hooks:
|
| 1008 |
h.remove()
|
|
|
|
| 1015 |
self._probe()
|
| 1016 |
self.log(f"[timing] PROBE complete at +{time.time() - self._staged_t0:.1f}s")
|
| 1017 |
self._free_gpu_memory()
|
| 1018 |
+
# Save state to disk so it survives ZeroGPU worker process boundary
|
| 1019 |
+
self._save_staged_state()
|
| 1020 |
|
| 1021 |
def run_stage_distill_excise(self):
|
| 1022 |
"""GPU Stage 2: Extract directions, modify weights, save checkpoint
|
|
|
|
| 1026 |
move the model off GPU before releasing the ZeroGPU allocation.
|
| 1027 |
"""
|
| 1028 |
if self.handle is None or self.handle.model is None:
|
| 1029 |
+
if self._staged_state_dir:
|
| 1030 |
+
self.log("[staged] Model not in memory — recovering from ZeroGPU state loss...")
|
| 1031 |
+
self._reload_model_for_stage()
|
| 1032 |
+
self._load_staged_state()
|
| 1033 |
+
else:
|
| 1034 |
+
raise RuntimeError(
|
| 1035 |
+
"Model not loaded at start of DISTILL+EXCISE stage. "
|
| 1036 |
+
"This usually means ZeroGPU pickling lost pipeline state "
|
| 1037 |
+
"between GPU stages (model was loaded in stage 1's worker "
|
| 1038 |
+
"but not propagated back to the main process). "
|
| 1039 |
+
f"handle={self.handle!r}, _harmful_means has "
|
| 1040 |
+
f"{len(self._harmful_means)} layers"
|
| 1041 |
+
)
|
| 1042 |
# Reset time budget for this GPU allocation
|
| 1043 |
self._pipeline_start_time = time.time()
|
| 1044 |
self._distill()
|
|
|
|
| 1065 |
Path to the saved model directory.
|
| 1066 |
"""
|
| 1067 |
if self.handle is None or self.handle.model is None:
|
| 1068 |
+
if self._staged_state_dir:
|
| 1069 |
+
# Load the modified model from the quick checkpoint saved
|
| 1070 |
+
# by Stage 2 (weights were modified by EXCISE).
|
| 1071 |
+
self.log("[staged] Model not in memory — recovering from ZeroGPU state loss...")
|
| 1072 |
+
self._reload_model_for_stage(source=str(self.output_dir))
|
| 1073 |
+
else:
|
| 1074 |
+
raise RuntimeError(
|
| 1075 |
+
"Model not loaded at start of VERIFY+REBIRTH stage. "
|
| 1076 |
+
"Pipeline state may have been lost between GPU stages."
|
| 1077 |
+
)
|
| 1078 |
# Reset time budget for this GPU allocation
|
| 1079 |
self._pipeline_start_time = time.time()
|
| 1080 |
self._verify()
|
|
|
|
| 1129 |
self.refusal_subspaces[k] = v.cpu()
|
| 1130 |
|
| 1131 |
# Move baseline KL logits to CPU
|
| 1132 |
+
if self._baseline_first_token_logits is not None:
|
| 1133 |
+
if isinstance(self._baseline_first_token_logits, torch.Tensor) and self._baseline_first_token_logits.device.type != "cpu":
|
| 1134 |
+
self._baseline_first_token_logits = self._baseline_first_token_logits.cpu()
|
|
|
|
|
|
|
| 1135 |
|
| 1136 |
dev.free_gpu_memory()
|
| 1137 |
elapsed = time.time() - t0
|
|
|
|
| 1153 |
self.log(f"[staged] Moving model to {target}...")
|
| 1154 |
t0 = time.time()
|
| 1155 |
self.handle.model.to(target)
|
| 1156 |
+
|
| 1157 |
+
# Move cached tensors back to GPU (mirrors _offload_to_cpu)
|
| 1158 |
+
for tensor_dict in (
|
| 1159 |
+
self._harmful_means, self._harmless_means,
|
| 1160 |
+
self._jailbreak_means,
|
| 1161 |
+
):
|
| 1162 |
+
for k in list(tensor_dict.keys()):
|
| 1163 |
+
if isinstance(tensor_dict[k], torch.Tensor) and tensor_dict[k].device != target:
|
| 1164 |
+
tensor_dict[k] = tensor_dict[k].to(target)
|
| 1165 |
+
|
| 1166 |
+
for k in list(self.refusal_directions.keys()):
|
| 1167 |
+
v = self.refusal_directions[k]
|
| 1168 |
+
if isinstance(v, torch.Tensor) and v.device != target:
|
| 1169 |
+
self.refusal_directions[k] = v.to(target)
|
| 1170 |
+
for k in list(self.refusal_subspaces.keys()):
|
| 1171 |
+
v = self.refusal_subspaces[k]
|
| 1172 |
+
if isinstance(v, torch.Tensor) and v.device != target:
|
| 1173 |
+
self.refusal_subspaces[k] = v.to(target)
|
| 1174 |
+
|
| 1175 |
elapsed = time.time() - t0
|
| 1176 |
self.log(f"[staged] Model restored to {target} ({elapsed:.1f}s)")
|
| 1177 |
|
| 1178 |
+
def _save_staged_state(self):
|
| 1179 |
+
"""Save intermediate pipeline state to disk for ZeroGPU cross-process persistence.
|
| 1180 |
+
|
| 1181 |
+
ZeroGPU runs each ``@spaces.GPU``-decorated call in a separate worker
|
| 1182 |
+
process. Mutations to the pipeline (model loading, activation
|
| 1183 |
+
collection) happen on a pickled copy inside the worker and are NOT
|
| 1184 |
+
propagated back. This method saves the state that subsequent stages
|
| 1185 |
+
need to a temporary directory on disk so it survives the process
|
| 1186 |
+
boundary.
|
| 1187 |
+
"""
|
| 1188 |
+
if not self._staged_state_dir:
|
| 1189 |
+
return
|
| 1190 |
+
import os
|
| 1191 |
+
os.makedirs(self._staged_state_dir, exist_ok=True)
|
| 1192 |
+
t0 = time.time()
|
| 1193 |
+
|
| 1194 |
+
def _cpu(t):
|
| 1195 |
+
return t.cpu() if isinstance(t, torch.Tensor) and t.device.type != "cpu" else t
|
| 1196 |
+
|
| 1197 |
+
state = {
|
| 1198 |
+
"_staged_t0": self._staged_t0,
|
| 1199 |
+
"_pipeline_start_time": getattr(self, "_pipeline_start_time", self._staged_t0),
|
| 1200 |
+
"_harmful_acts": {k: [_cpu(t) for t in v] for k, v in self._harmful_acts.items()},
|
| 1201 |
+
"_harmless_acts": {k: [_cpu(t) for t in v] for k, v in self._harmless_acts.items()},
|
| 1202 |
+
"_jailbreak_acts": {k: [_cpu(t) for t in v] for k, v in self._jailbreak_acts.items()},
|
| 1203 |
+
"_harmful_means": {k: _cpu(v) for k, v in self._harmful_means.items()},
|
| 1204 |
+
"_harmless_means": {k: _cpu(v) for k, v in self._harmless_means.items()},
|
| 1205 |
+
"_jailbreak_means": {k: _cpu(v) for k, v in self._jailbreak_means.items()},
|
| 1206 |
+
"_routing_harmful": {k: [_cpu(t) for t in v] for k, v in self._routing_harmful.items()},
|
| 1207 |
+
"_routing_harmless": {k: [_cpu(t) for t in v] for k, v in self._routing_harmless.items()},
|
| 1208 |
+
"_baseline_first_token_logits": _cpu(self._baseline_first_token_logits) if self._baseline_first_token_logits is not None else None,
|
| 1209 |
+
"_kl_eval_prompts": list(self._kl_eval_prompts),
|
| 1210 |
+
}
|
| 1211 |
+
state_path = os.path.join(self._staged_state_dir, "staged_state.pt")
|
| 1212 |
+
torch.save(state, state_path)
|
| 1213 |
+
elapsed = time.time() - t0
|
| 1214 |
+
self.log(f"[staged] Saved intermediate state to disk ({elapsed:.1f}s)")
|
| 1215 |
+
|
| 1216 |
+
def _load_staged_state(self):
|
| 1217 |
+
"""Load intermediate pipeline state saved by ``_save_staged_state``.
|
| 1218 |
+
|
| 1219 |
+
Called at the start of Stage 2 (and later) when ZeroGPU pickling
|
| 1220 |
+
caused the in-memory state to be lost.
|
| 1221 |
+
"""
|
| 1222 |
+
import os
|
| 1223 |
+
state_path = os.path.join(self._staged_state_dir, "staged_state.pt")
|
| 1224 |
+
if not os.path.exists(state_path):
|
| 1225 |
+
raise RuntimeError(
|
| 1226 |
+
f"Staged state file not found at {state_path}. "
|
| 1227 |
+
"Stage 1 may not have saved its state correctly."
|
| 1228 |
+
)
|
| 1229 |
+
t0 = time.time()
|
| 1230 |
+
state = torch.load(state_path, weights_only=False)
|
| 1231 |
+
for key, value in state.items():
|
| 1232 |
+
setattr(self, key, value)
|
| 1233 |
+
elapsed = time.time() - t0
|
| 1234 |
+
self.log(f"[staged] Loaded intermediate state from disk ({elapsed:.1f}s)")
|
| 1235 |
+
|
| 1236 |
+
def _reload_model_for_stage(self, source: str | None = None):
|
| 1237 |
+
"""Reload the model for a new GPU stage after ZeroGPU state loss.
|
| 1238 |
+
|
| 1239 |
+
Args:
|
| 1240 |
+
source: Path to load the model from. If None, reloads from the
|
| 1241 |
+
original HuggingFace model (for Stage 2, where weights are
|
| 1242 |
+
unmodified). If a path (e.g. ``self.output_dir``), loads
|
| 1243 |
+
from a previously saved checkpoint (for Stage 3, after
|
| 1244 |
+
weight modification by EXCISE).
|
| 1245 |
+
"""
|
| 1246 |
+
model_name = source or self.model_name
|
| 1247 |
+
self.log(f"[staged] Reloading model from {'checkpoint' if source else 'HF cache'}: {model_name}")
|
| 1248 |
+
self.handle = load_model(
|
| 1249 |
+
model_name=model_name,
|
| 1250 |
+
task="causal_lm",
|
| 1251 |
+
device=self.device,
|
| 1252 |
+
dtype=self.dtype,
|
| 1253 |
+
trust_remote_code=self.trust_remote_code,
|
| 1254 |
+
quantization=self.quantization,
|
| 1255 |
+
)
|
| 1256 |
+
self.log(f"[staged] Model reloaded successfully")
|
| 1257 |
+
|
| 1258 |
# ── Stage 1: SUMMON ─────────────────────────���───────────────────────
|
| 1259 |
|
| 1260 |
def _summon(self):
|
|
|
|
| 6348 |
dat_file = Path(offload_dir) / f"{key}.dat"
|
| 6349 |
if safetensors_file.exists():
|
| 6350 |
data = load_file(str(safetensors_file))
|
| 6351 |
+
if key in data:
|
| 6352 |
+
state_dict[key] = data[key]
|
| 6353 |
+
elif len(data) == 1:
|
| 6354 |
+
# Accelerate stores single-tensor files; use the only tensor
|
| 6355 |
+
state_dict[key] = next(iter(data.values()))
|
| 6356 |
+
else:
|
| 6357 |
+
raise RuntimeError(
|
| 6358 |
+
f"Cannot resolve meta tensor '{key}': safetensors file "
|
| 6359 |
+
f"contains {len(data)} tensors but key not found. "
|
| 6360 |
+
f"Available keys: {list(data.keys())}"
|
| 6361 |
+
)
|
| 6362 |
elif dat_file.exists():
|
| 6363 |
# Accelerate's .dat format: raw tensor bytes with shape/dtype metadata
|
| 6364 |
import numpy as np
|
obliteratus/informed_pipeline.py
CHANGED
|
@@ -306,6 +306,16 @@ class InformedAbliterationPipeline(AbliterationPipeline):
|
|
| 306 |
|
| 307 |
def run_stage_analyze_distill_excise(self):
|
| 308 |
"""GPU Stage 2 (informed): ANALYZE + DISTILL + EXCISE + quick checkpoint."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
# Reset time budget for this GPU allocation
|
| 310 |
self._pipeline_start_time = time.time()
|
| 311 |
self._analyze()
|
|
@@ -319,6 +329,15 @@ class InformedAbliterationPipeline(AbliterationPipeline):
|
|
| 319 |
Stores the result path and report in instance attributes so the
|
| 320 |
caller can retrieve them after the GPU allocation returns.
|
| 321 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
# Reset time budget for this GPU allocation
|
| 323 |
self._pipeline_start_time = time.time()
|
| 324 |
self._verify_and_compensate()
|
|
|
|
| 306 |
|
| 307 |
def run_stage_analyze_distill_excise(self):
|
| 308 |
"""GPU Stage 2 (informed): ANALYZE + DISTILL + EXCISE + quick checkpoint."""
|
| 309 |
+
if self.handle is None or self.handle.model is None:
|
| 310 |
+
if self._staged_state_dir:
|
| 311 |
+
self.log("[staged] Model not in memory — recovering from ZeroGPU state loss...")
|
| 312 |
+
self._reload_model_for_stage()
|
| 313 |
+
self._load_staged_state()
|
| 314 |
+
else:
|
| 315 |
+
raise RuntimeError(
|
| 316 |
+
"Model not loaded at start of ANALYZE+DISTILL+EXCISE stage. "
|
| 317 |
+
"Pipeline state may have been lost between GPU stages."
|
| 318 |
+
)
|
| 319 |
# Reset time budget for this GPU allocation
|
| 320 |
self._pipeline_start_time = time.time()
|
| 321 |
self._analyze()
|
|
|
|
| 329 |
Stores the result path and report in instance attributes so the
|
| 330 |
caller can retrieve them after the GPU allocation returns.
|
| 331 |
"""
|
| 332 |
+
if self.handle is None or self.handle.model is None:
|
| 333 |
+
if self._staged_state_dir:
|
| 334 |
+
self.log("[staged] Model not in memory — recovering from ZeroGPU state loss...")
|
| 335 |
+
self._reload_model_for_stage(source=str(self.output_dir))
|
| 336 |
+
else:
|
| 337 |
+
raise RuntimeError(
|
| 338 |
+
"Model not loaded at start of VERIFY+REBIRTH stage. "
|
| 339 |
+
"Pipeline state may have been lost between GPU stages."
|
| 340 |
+
)
|
| 341 |
# Reset time budget for this GPU allocation
|
| 342 |
self._pipeline_start_time = time.time()
|
| 343 |
self._verify_and_compensate()
|
obliteratus/telemetry.py
CHANGED
|
@@ -622,31 +622,33 @@ def restore_from_hub() -> int:
|
|
| 622 |
return 0
|
| 623 |
|
| 624 |
try:
|
| 625 |
-
# Read existing local keys for dedup
|
| 626 |
-
existing_keys: set[tuple[str, str]] = set()
|
| 627 |
-
if TELEMETRY_FILE.exists():
|
| 628 |
-
try:
|
| 629 |
-
with open(TELEMETRY_FILE) as f:
|
| 630 |
-
for line in f:
|
| 631 |
-
line = line.strip()
|
| 632 |
-
if not line:
|
| 633 |
-
continue
|
| 634 |
-
try:
|
| 635 |
-
r = json.loads(line)
|
| 636 |
-
existing_keys.add(
|
| 637 |
-
(r.get("session_id", ""), r.get("timestamp", ""))
|
| 638 |
-
)
|
| 639 |
-
except json.JSONDecodeError:
|
| 640 |
-
continue
|
| 641 |
-
except Exception:
|
| 642 |
-
pass
|
| 643 |
-
|
| 644 |
hub_records = fetch_hub_records()
|
| 645 |
if not hub_records:
|
| 646 |
return 0
|
| 647 |
|
| 648 |
new_count = 0
|
| 649 |
with _write_lock:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
with open(TELEMETRY_FILE, "a") as f:
|
| 651 |
for r in hub_records:
|
| 652 |
key = (r.get("session_id", ""), r.get("timestamp", ""))
|
|
|
|
| 622 |
return 0
|
| 623 |
|
| 624 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
hub_records = fetch_hub_records()
|
| 626 |
if not hub_records:
|
| 627 |
return 0
|
| 628 |
|
| 629 |
new_count = 0
|
| 630 |
with _write_lock:
|
| 631 |
+
# Read existing local keys for dedup INSIDE the write lock to
|
| 632 |
+
# prevent a concurrent log_benchmark() from inserting between
|
| 633 |
+
# our read and append, which would cause duplicate records.
|
| 634 |
+
existing_keys: set[tuple[str, str]] = set()
|
| 635 |
+
if TELEMETRY_FILE.exists():
|
| 636 |
+
try:
|
| 637 |
+
with open(TELEMETRY_FILE) as f:
|
| 638 |
+
for line in f:
|
| 639 |
+
line = line.strip()
|
| 640 |
+
if not line:
|
| 641 |
+
continue
|
| 642 |
+
try:
|
| 643 |
+
r = json.loads(line)
|
| 644 |
+
existing_keys.add(
|
| 645 |
+
(r.get("session_id", ""), r.get("timestamp", ""))
|
| 646 |
+
)
|
| 647 |
+
except json.JSONDecodeError:
|
| 648 |
+
continue
|
| 649 |
+
except Exception:
|
| 650 |
+
pass
|
| 651 |
+
|
| 652 |
with open(TELEMETRY_FILE, "a") as f:
|
| 653 |
for r in hub_records:
|
| 654 |
key = (r.get("session_id", ""), r.get("timestamp", ""))
|
obliteratus/tourney.py
CHANGED
|
@@ -70,10 +70,15 @@ def composite_score(metrics: dict[str, Any]) -> float:
|
|
| 70 |
spec = metrics.get("spectral_certification")
|
| 71 |
degen = metrics.get("degenerate_count", 0) or 0
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
# Spectral certification: GREEN=1.0, YELLOW=0.5, RED=0.0, None=0.5 (neutral)
|
| 79 |
if spec == "GREEN":
|
|
@@ -254,7 +259,11 @@ def _save_checkpoint(
|
|
| 254 |
"timestamp": datetime.now().isoformat(),
|
| 255 |
}
|
| 256 |
path = output_dir / CHECKPOINT_FILENAME
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
return path
|
| 259 |
|
| 260 |
|
|
@@ -1202,10 +1211,10 @@ class TourneyRunner:
|
|
| 1202 |
# Determine alive list from checkpoint
|
| 1203 |
alive = list(checkpoint.get("alive", self.methods))
|
| 1204 |
|
| 1205 |
-
#
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
else:
|
| 1210 |
# Checkpoint doesn't match current config — start fresh
|
| 1211 |
checkpoint = None
|
|
@@ -1393,6 +1402,11 @@ class TourneyRunner:
|
|
| 1393 |
result.rounds.append(rnd)
|
| 1394 |
alive = list(rnd.advanced_to)
|
| 1395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1396 |
# Schedule next round dynamically
|
| 1397 |
if round_num == 1 and len(alive) > 1:
|
| 1398 |
r2_advance = max(2, math.ceil(len(alive) / 2))
|
|
|
|
| 70 |
spec = metrics.get("spectral_certification")
|
| 71 |
degen = metrics.get("degenerate_count", 0) or 0
|
| 72 |
|
| 73 |
+
import math
|
| 74 |
+
|
| 75 |
+
def _finite(v: Any) -> bool:
|
| 76 |
+
return v is not None and isinstance(v, (int, float)) and math.isfinite(v)
|
| 77 |
+
|
| 78 |
+
refusal_score = (1.0 - rr) if _finite(rr) else 0.0
|
| 79 |
+
coherence_score = co if _finite(co) else 0.0
|
| 80 |
+
kl_score = 1.0 / (1.0 + max(kl, 0.0)) if _finite(kl) else 0.5
|
| 81 |
+
ppl_score = 1.0 / (1.0 + max(pp, 0.0) / 100.0) if _finite(pp) else 0.5
|
| 82 |
|
| 83 |
# Spectral certification: GREEN=1.0, YELLOW=0.5, RED=0.0, None=0.5 (neutral)
|
| 84 |
if spec == "GREEN":
|
|
|
|
| 259 |
"timestamp": datetime.now().isoformat(),
|
| 260 |
}
|
| 261 |
path = output_dir / CHECKPOINT_FILENAME
|
| 262 |
+
# Write to temp file then rename for crash-safe atomicity —
|
| 263 |
+
# prevents corrupted checkpoint if process is killed mid-write.
|
| 264 |
+
tmp_path = path.with_suffix(".tmp")
|
| 265 |
+
tmp_path.write_text(json.dumps(checkpoint, indent=2))
|
| 266 |
+
tmp_path.replace(path)
|
| 267 |
return path
|
| 268 |
|
| 269 |
|
|
|
|
| 1211 |
# Determine alive list from checkpoint
|
| 1212 |
alive = list(checkpoint.get("alive", self.methods))
|
| 1213 |
|
| 1214 |
+
# NOTE: Don't delete checkpoint here — if the round crashes
|
| 1215 |
+
# before completion, we lose all progress. The checkpoint is
|
| 1216 |
+
# overwritten by _save_checkpoint() on quota error, or deleted
|
| 1217 |
+
# after the resumed round finishes successfully (see below).
|
| 1218 |
else:
|
| 1219 |
# Checkpoint doesn't match current config — start fresh
|
| 1220 |
checkpoint = None
|
|
|
|
| 1402 |
result.rounds.append(rnd)
|
| 1403 |
alive = list(rnd.advanced_to)
|
| 1404 |
|
| 1405 |
+
# Clean up stale checkpoint now that the round completed successfully
|
| 1406 |
+
_stale_ckpt = self.output_dir / CHECKPOINT_FILENAME
|
| 1407 |
+
if _stale_ckpt.exists():
|
| 1408 |
+
_stale_ckpt.unlink(missing_ok=True)
|
| 1409 |
+
|
| 1410 |
# Schedule next round dynamically
|
| 1411 |
if round_num == 1 and len(alive) > 1:
|
| 1412 |
r2_advance = max(2, math.ceil(len(alive) / 2))
|