Spaces:
Running on Zero
Running on Zero
Upload 130 files
Browse files- app.py +83 -22
- obliteratus/.DS_Store +0 -0
- obliteratus/abliterate.py +119 -27
app.py
CHANGED
|
@@ -95,6 +95,9 @@ _state: dict = {
|
|
| 95 |
"log": [],
|
| 96 |
# Activation steering metadata (survives model reload)
|
| 97 |
"steering": None, # dict with refusal_directions, strong_layers, steering_strength
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
_lock = threading.Lock()
|
| 100 |
|
|
@@ -1328,7 +1331,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1328 |
f" or locally: `export HF_TOKEN=hf_...`\n\n"
|
| 1329 |
f"Get your token at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)\n\n"
|
| 1330 |
f"Alternatively, choose a non-gated model (those without the \U0001f512 icon).",
|
| 1331 |
-
"", gr.update(),
|
| 1332 |
)
|
| 1333 |
return
|
| 1334 |
|
|
@@ -1337,14 +1340,14 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1337 |
if not re.match(r'^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$', push_to_hub):
|
| 1338 |
yield (
|
| 1339 |
"**Error:** Invalid Hub repo format. Use `username/model-name`.",
|
| 1340 |
-
"", gr.update(),
|
| 1341 |
)
|
| 1342 |
return
|
| 1343 |
if not os.environ.get("HF_TOKEN"):
|
| 1344 |
yield (
|
| 1345 |
"**Error:** HF_TOKEN not set. Push to Hub requires a write token. "
|
| 1346 |
"Set it via `export HF_TOKEN=hf_...` or in your Space secrets.",
|
| 1347 |
-
"", gr.update(),
|
| 1348 |
)
|
| 1349 |
return
|
| 1350 |
|
|
@@ -1355,7 +1358,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1355 |
_clear_gpu()
|
| 1356 |
with _lock:
|
| 1357 |
if _state["status"] == "obliterating":
|
| 1358 |
-
yield "**Error:** An obliteration is already in progress.", "", gr.update()
|
| 1359 |
return
|
| 1360 |
_state["log"] = []
|
| 1361 |
_state["status"] = "obliterating"
|
|
@@ -1506,9 +1509,9 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1506 |
status_msg = f"**Obliterating\u2026** ({_elapsed()})"
|
| 1507 |
if len(log_lines) > last_yielded[0]:
|
| 1508 |
last_yielded[0] = len(log_lines)
|
| 1509 |
-
yield status_msg, "\n".join(log_lines), gr.update()
|
| 1510 |
else:
|
| 1511 |
-
yield status_msg, "\n".join(log_lines), gr.update()
|
| 1512 |
if time.time() - _pipeline_start > _max_pipeline_secs:
|
| 1513 |
log_lines.append("\nTIMEOUT: Pipeline exceeded 45-minute limit.")
|
| 1514 |
break
|
|
@@ -1523,7 +1526,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1523 |
err_msg = str(error_ref[0]) or repr(error_ref[0])
|
| 1524 |
log_lines.append(f"\nERROR: {err_msg}")
|
| 1525 |
_state["log"] = log_lines
|
| 1526 |
-
yield f"**Error:** {err_msg}", "\n".join(log_lines), get_chat_header()
|
| 1527 |
return
|
| 1528 |
|
| 1529 |
# Success — keep model in memory for chat.
|
|
@@ -1596,6 +1599,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1596 |
}
|
| 1597 |
with _lock:
|
| 1598 |
_state["steering"] = steering_meta
|
|
|
|
| 1599 |
|
| 1600 |
if can_generate:
|
| 1601 |
# Model fits — use it directly (steering hooks already installed)
|
|
@@ -1624,7 +1628,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1624 |
if bnb_available:
|
| 1625 |
log_lines.append("\nModel too large for chat at float16 — reloading in 4-bit...")
|
| 1626 |
last_yielded[0] = len(log_lines)
|
| 1627 |
-
yield status_msg, "\n".join(log_lines), gr.update()
|
| 1628 |
try:
|
| 1629 |
from transformers import BitsAndBytesConfig
|
| 1630 |
bnb_cfg = BitsAndBytesConfig(
|
|
@@ -1671,7 +1675,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1671 |
else "Falling back to CPU offload..."
|
| 1672 |
)
|
| 1673 |
last_yielded[0] = len(log_lines)
|
| 1674 |
-
yield status_msg, "\n".join(log_lines), gr.update()
|
| 1675 |
try:
|
| 1676 |
offload_dir = tempfile.mkdtemp(prefix="obliteratus_offload_")
|
| 1677 |
model_reloaded = AutoModelForCausalLM.from_pretrained(
|
|
@@ -1725,7 +1729,13 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1725 |
f"**{model_choice}** liberated with `{method}` method. "
|
| 1726 |
f"Saved to `{save_dir}`. Chat requires a larger GPU."
|
| 1727 |
)
|
| 1728 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1729 |
|
| 1730 |
except Exception as e:
|
| 1731 |
# Ensure status never gets stuck on "obliterating"
|
|
@@ -1734,7 +1744,7 @@ def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
|
| 1734 |
err_msg = str(e) or repr(e)
|
| 1735 |
log_lines.append(f"\nERROR (post-pipeline): {err_msg}")
|
| 1736 |
_state["log"] = log_lines
|
| 1737 |
-
yield f"**Error:** {err_msg}", "\n".join(log_lines), get_chat_header()
|
| 1738 |
|
| 1739 |
|
| 1740 |
# ---------------------------------------------------------------------------
|
|
@@ -1798,14 +1808,41 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
|
|
| 1798 |
return
|
| 1799 |
|
| 1800 |
# ZeroGPU safety: ensure model is on GPU if available.
|
| 1801 |
-
# Between GPU allocations, ZeroGPU may have moved the model to CPU/meta
|
| 1802 |
-
#
|
|
|
|
|
|
|
| 1803 |
try:
|
| 1804 |
dev = next(model.parameters()).device
|
| 1805 |
if torch.cuda.is_available() and dev.type != "cuda":
|
| 1806 |
model.to("cuda")
|
| 1807 |
except (StopIteration, RuntimeError):
|
| 1808 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1809 |
|
| 1810 |
# Sanitize inputs to prevent resource exhaustion
|
| 1811 |
system_prompt = (system_prompt or "")[:4096]
|
|
@@ -1984,6 +2021,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 1984 |
_state["tokenizer"] = tokenizer_loaded
|
| 1985 |
_state["steering"] = None
|
| 1986 |
_state["status"] = "ready"
|
|
|
|
| 1987 |
progress(1.0, desc="Ready!")
|
| 1988 |
yield (
|
| 1989 |
f"**Loaded!** `{choice}` is ready in the Chat tab (loaded from checkpoint).",
|
|
@@ -2019,6 +2057,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 2019 |
_state["tokenizer"] = tokenizer_loaded
|
| 2020 |
_state["steering"] = None
|
| 2021 |
_state["status"] = "ready"
|
|
|
|
| 2022 |
progress(1.0, desc="Ready!")
|
| 2023 |
yield (
|
| 2024 |
f"**Loaded!** `{choice}` is ready in the Chat tab (4-bit from checkpoint).",
|
|
@@ -2093,6 +2132,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
|
| 2093 |
_state["tokenizer"] = pipeline.handle.tokenizer
|
| 2094 |
_state["steering"] = None
|
| 2095 |
_state["status"] = "ready"
|
|
|
|
| 2096 |
|
| 2097 |
pipeline_ref[0] = None
|
| 2098 |
|
|
@@ -2132,13 +2172,35 @@ def ab_chat_respond(message: str, history_left: list[dict], history_right: list[
|
|
| 2132 |
"#### Abliterated")
|
| 2133 |
return
|
| 2134 |
|
| 2135 |
-
# ZeroGPU safety: ensure model is on GPU if available
|
|
|
|
|
|
|
| 2136 |
try:
|
| 2137 |
dev = next(abliterated_model.parameters()).device
|
| 2138 |
if torch.cuda.is_available() and dev.type != "cuda":
|
| 2139 |
abliterated_model.to("cuda")
|
| 2140 |
except (StopIteration, RuntimeError):
|
| 2141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2142 |
|
| 2143 |
# Build header strings showing model name on each side
|
| 2144 |
header_left = f"#### Original (Pre-Abliteration)\n`{model_name}`"
|
|
@@ -3935,17 +3997,16 @@ Built on the shoulders of:
|
|
| 3935 |
)
|
| 3936 |
|
| 3937 |
# Wire obliterate button (after all tabs so chat_status is defined)
|
|
|
|
|
|
|
| 3938 |
obliterate_btn.click(
|
| 3939 |
fn=obliterate,
|
| 3940 |
inputs=[model_dd, method_dd, hub_repo, prompt_vol_dd, dataset_dd,
|
| 3941 |
custom_harmful_tb, custom_harmless_tb] + _adv_controls,
|
| 3942 |
-
outputs=[status_md, log_box, chat_status],
|
| 3943 |
).then(
|
| 3944 |
-
fn=lambda: (
|
| 3945 |
-
|
| 3946 |
-
_get_vram_html(),
|
| 3947 |
-
),
|
| 3948 |
-
outputs=[session_model_dd, vram_display],
|
| 3949 |
)
|
| 3950 |
|
| 3951 |
# Wire session model loading (Chat tab)
|
|
|
|
| 95 |
"log": [],
|
| 96 |
# Activation steering metadata (survives model reload)
|
| 97 |
"steering": None, # dict with refusal_directions, strong_layers, steering_strength
|
| 98 |
+
# Checkpoint directory for ZeroGPU reload (model tensors may become stale
|
| 99 |
+
# after GPU deallocation — this path lets chat_respond reload from disk)
|
| 100 |
+
"output_dir": None,
|
| 101 |
}
|
| 102 |
_lock = threading.Lock()
|
| 103 |
|
|
|
|
| 1331 |
f" or locally: `export HF_TOKEN=hf_...`\n\n"
|
| 1332 |
f"Get your token at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)\n\n"
|
| 1333 |
f"Alternatively, choose a non-gated model (those without the \U0001f512 icon).",
|
| 1334 |
+
"", gr.update(), gr.update(),
|
| 1335 |
)
|
| 1336 |
return
|
| 1337 |
|
|
|
|
| 1340 |
if not re.match(r'^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$', push_to_hub):
|
| 1341 |
yield (
|
| 1342 |
"**Error:** Invalid Hub repo format. Use `username/model-name`.",
|
| 1343 |
+
"", gr.update(), gr.update(),
|
| 1344 |
)
|
| 1345 |
return
|
| 1346 |
if not os.environ.get("HF_TOKEN"):
|
| 1347 |
yield (
|
| 1348 |
"**Error:** HF_TOKEN not set. Push to Hub requires a write token. "
|
| 1349 |
"Set it via `export HF_TOKEN=hf_...` or in your Space secrets.",
|
| 1350 |
+
"", gr.update(), gr.update(),
|
| 1351 |
)
|
| 1352 |
return
|
| 1353 |
|
|
|
|
| 1358 |
_clear_gpu()
|
| 1359 |
with _lock:
|
| 1360 |
if _state["status"] == "obliterating":
|
| 1361 |
+
yield "**Error:** An obliteration is already in progress.", "", gr.update(), gr.update()
|
| 1362 |
return
|
| 1363 |
_state["log"] = []
|
| 1364 |
_state["status"] = "obliterating"
|
|
|
|
| 1509 |
status_msg = f"**Obliterating\u2026** ({_elapsed()})"
|
| 1510 |
if len(log_lines) > last_yielded[0]:
|
| 1511 |
last_yielded[0] = len(log_lines)
|
| 1512 |
+
yield status_msg, "\n".join(log_lines), gr.update(), gr.update()
|
| 1513 |
else:
|
| 1514 |
+
yield status_msg, "\n".join(log_lines), gr.update(), gr.update()
|
| 1515 |
if time.time() - _pipeline_start > _max_pipeline_secs:
|
| 1516 |
log_lines.append("\nTIMEOUT: Pipeline exceeded 45-minute limit.")
|
| 1517 |
break
|
|
|
|
| 1526 |
err_msg = str(error_ref[0]) or repr(error_ref[0])
|
| 1527 |
log_lines.append(f"\nERROR: {err_msg}")
|
| 1528 |
_state["log"] = log_lines
|
| 1529 |
+
yield f"**Error:** {err_msg}", "\n".join(log_lines), get_chat_header(), gr.update()
|
| 1530 |
return
|
| 1531 |
|
| 1532 |
# Success — keep model in memory for chat.
|
|
|
|
| 1599 |
}
|
| 1600 |
with _lock:
|
| 1601 |
_state["steering"] = steering_meta
|
| 1602 |
+
_state["output_dir"] = save_dir # for ZeroGPU checkpoint reload
|
| 1603 |
|
| 1604 |
if can_generate:
|
| 1605 |
# Model fits — use it directly (steering hooks already installed)
|
|
|
|
| 1628 |
if bnb_available:
|
| 1629 |
log_lines.append("\nModel too large for chat at float16 — reloading in 4-bit...")
|
| 1630 |
last_yielded[0] = len(log_lines)
|
| 1631 |
+
yield status_msg, "\n".join(log_lines), gr.update(), gr.update()
|
| 1632 |
try:
|
| 1633 |
from transformers import BitsAndBytesConfig
|
| 1634 |
bnb_cfg = BitsAndBytesConfig(
|
|
|
|
| 1675 |
else "Falling back to CPU offload..."
|
| 1676 |
)
|
| 1677 |
last_yielded[0] = len(log_lines)
|
| 1678 |
+
yield status_msg, "\n".join(log_lines), gr.update(), gr.update()
|
| 1679 |
try:
|
| 1680 |
offload_dir = tempfile.mkdtemp(prefix="obliteratus_offload_")
|
| 1681 |
model_reloaded = AutoModelForCausalLM.from_pretrained(
|
|
|
|
| 1729 |
f"**{model_choice}** liberated with `{method}` method. "
|
| 1730 |
f"Saved to `{save_dir}`. Chat requires a larger GPU."
|
| 1731 |
)
|
| 1732 |
+
# Update session dropdown directly (don't rely on .then() which can
|
| 1733 |
+
# fail to fire on ZeroGPU after generator teardown)
|
| 1734 |
+
_dd_update = gr.update(
|
| 1735 |
+
choices=_get_session_model_choices(),
|
| 1736 |
+
value=_last_obliterated_label or None,
|
| 1737 |
+
)
|
| 1738 |
+
yield status_msg, "\n".join(log_lines), get_chat_header(), _dd_update
|
| 1739 |
|
| 1740 |
except Exception as e:
|
| 1741 |
# Ensure status never gets stuck on "obliterating"
|
|
|
|
| 1744 |
err_msg = str(e) or repr(e)
|
| 1745 |
log_lines.append(f"\nERROR (post-pipeline): {err_msg}")
|
| 1746 |
_state["log"] = log_lines
|
| 1747 |
+
yield f"**Error:** {err_msg}", "\n".join(log_lines), get_chat_header(), gr.update()
|
| 1748 |
|
| 1749 |
|
| 1750 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1808 |
return
|
| 1809 |
|
| 1810 |
# ZeroGPU safety: ensure model is on GPU if available.
|
| 1811 |
+
# Between GPU allocations, ZeroGPU may have moved the model to CPU/meta,
|
| 1812 |
+
# or tensors may be stale from a previous GPU context.
|
| 1813 |
+
# The @spaces.GPU decorator guarantees a GPU is available here.
|
| 1814 |
+
_needs_reload = False
|
| 1815 |
try:
|
| 1816 |
dev = next(model.parameters()).device
|
| 1817 |
if torch.cuda.is_available() and dev.type != "cuda":
|
| 1818 |
model.to("cuda")
|
| 1819 |
except (StopIteration, RuntimeError):
|
| 1820 |
+
_needs_reload = True
|
| 1821 |
+
|
| 1822 |
+
# If model tensors are stale/meta, reload from the saved checkpoint
|
| 1823 |
+
if _needs_reload and _ZEROGPU_AVAILABLE:
|
| 1824 |
+
checkpoint = _state.get("output_dir")
|
| 1825 |
+
if checkpoint and Path(checkpoint).exists():
|
| 1826 |
+
try:
|
| 1827 |
+
is_preset = (_state.get("model_name") or "") in MODELS
|
| 1828 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 1829 |
+
checkpoint, device_map="auto", torch_dtype=torch.float16,
|
| 1830 |
+
trust_remote_code=is_preset,
|
| 1831 |
+
)
|
| 1832 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 1833 |
+
checkpoint, trust_remote_code=is_preset,
|
| 1834 |
+
)
|
| 1835 |
+
if tokenizer.pad_token is None:
|
| 1836 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 1837 |
+
with _lock:
|
| 1838 |
+
_state["model"] = model
|
| 1839 |
+
_state["tokenizer"] = tokenizer
|
| 1840 |
+
except Exception:
|
| 1841 |
+
yield "Model failed to reload from checkpoint. Try re-obliterating."
|
| 1842 |
+
return
|
| 1843 |
+
else:
|
| 1844 |
+
yield "Model tensors are stale (ZeroGPU). Re-obliterate to create a fresh checkpoint."
|
| 1845 |
+
return
|
| 1846 |
|
| 1847 |
# Sanitize inputs to prevent resource exhaustion
|
| 1848 |
system_prompt = (system_prompt or "")[:4096]
|
|
|
|
| 2021 |
_state["tokenizer"] = tokenizer_loaded
|
| 2022 |
_state["steering"] = None
|
| 2023 |
_state["status"] = "ready"
|
| 2024 |
+
_state["output_dir"] = checkpoint_dir
|
| 2025 |
progress(1.0, desc="Ready!")
|
| 2026 |
yield (
|
| 2027 |
f"**Loaded!** `{choice}` is ready in the Chat tab (loaded from checkpoint).",
|
|
|
|
| 2057 |
_state["tokenizer"] = tokenizer_loaded
|
| 2058 |
_state["steering"] = None
|
| 2059 |
_state["status"] = "ready"
|
| 2060 |
+
_state["output_dir"] = checkpoint_dir
|
| 2061 |
progress(1.0, desc="Ready!")
|
| 2062 |
yield (
|
| 2063 |
f"**Loaded!** `{choice}` is ready in the Chat tab (4-bit from checkpoint).",
|
|
|
|
| 2132 |
_state["tokenizer"] = pipeline.handle.tokenizer
|
| 2133 |
_state["steering"] = None
|
| 2134 |
_state["status"] = "ready"
|
| 2135 |
+
_state["output_dir"] = "/tmp/obliterated" # re-abliteration fallback path
|
| 2136 |
|
| 2137 |
pipeline_ref[0] = None
|
| 2138 |
|
|
|
|
| 2172 |
"#### Abliterated")
|
| 2173 |
return
|
| 2174 |
|
| 2175 |
+
# ZeroGPU safety: ensure model is on GPU if available.
|
| 2176 |
+
# If tensors are stale from a prior GPU context, reload from checkpoint.
|
| 2177 |
+
_needs_reload = False
|
| 2178 |
try:
|
| 2179 |
dev = next(abliterated_model.parameters()).device
|
| 2180 |
if torch.cuda.is_available() and dev.type != "cuda":
|
| 2181 |
abliterated_model.to("cuda")
|
| 2182 |
except (StopIteration, RuntimeError):
|
| 2183 |
+
_needs_reload = True
|
| 2184 |
+
|
| 2185 |
+
if _needs_reload and _ZEROGPU_AVAILABLE:
|
| 2186 |
+
checkpoint = _state.get("output_dir")
|
| 2187 |
+
if checkpoint and Path(checkpoint).exists():
|
| 2188 |
+
try:
|
| 2189 |
+
is_preset = (model_name or "") in MODELS
|
| 2190 |
+
abliterated_model = AutoModelForCausalLM.from_pretrained(
|
| 2191 |
+
checkpoint, device_map="auto", torch_dtype=torch.float16,
|
| 2192 |
+
trust_remote_code=is_preset,
|
| 2193 |
+
)
|
| 2194 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 2195 |
+
checkpoint, trust_remote_code=is_preset,
|
| 2196 |
+
)
|
| 2197 |
+
if tokenizer.pad_token is None:
|
| 2198 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 2199 |
+
with _lock:
|
| 2200 |
+
_state["model"] = abliterated_model
|
| 2201 |
+
_state["tokenizer"] = tokenizer
|
| 2202 |
+
except Exception:
|
| 2203 |
+
pass # Fall through — will fail at generation with a clear error
|
| 2204 |
|
| 2205 |
# Build header strings showing model name on each side
|
| 2206 |
header_left = f"#### Original (Pre-Abliteration)\n`{model_name}`"
|
|
|
|
| 3997 |
)
|
| 3998 |
|
| 3999 |
# Wire obliterate button (after all tabs so chat_status is defined)
|
| 4000 |
+
# session_model_dd is a direct output (4th) so the dropdown updates
|
| 4001 |
+
# reliably even on ZeroGPU where .then() may not fire after generator teardown.
|
| 4002 |
obliterate_btn.click(
|
| 4003 |
fn=obliterate,
|
| 4004 |
inputs=[model_dd, method_dd, hub_repo, prompt_vol_dd, dataset_dd,
|
| 4005 |
custom_harmful_tb, custom_harmless_tb] + _adv_controls,
|
| 4006 |
+
outputs=[status_md, log_box, chat_status, session_model_dd],
|
| 4007 |
).then(
|
| 4008 |
+
fn=lambda: _get_vram_html(),
|
| 4009 |
+
outputs=[vram_display],
|
|
|
|
|
|
|
|
|
|
| 4010 |
)
|
| 4011 |
|
| 4012 |
# Wire session model loading (Chat tab)
|
obliteratus/.DS_Store
CHANGED
|
Binary files a/obliteratus/.DS_Store and b/obliteratus/.DS_Store differ
|
|
|
obliteratus/abliterate.py
CHANGED
|
@@ -1595,6 +1595,38 @@ class AbliterationPipeline:
|
|
| 1595 |
else:
|
| 1596 |
self._strong_layers = knee_layers
|
| 1597 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1598 |
# Cap layer count for inversion modes — reflecting too many weak-signal
|
| 1599 |
# layers destroys coherence. Limit to top 40% of total layers.
|
| 1600 |
if self.invert_refusal and len(self._strong_layers) > 0:
|
|
@@ -3443,6 +3475,15 @@ class AbliterationPipeline:
|
|
| 3443 |
sorted_layers = sorted(norms.items(), key=lambda x: x[1], reverse=True)
|
| 3444 |
self._strong_layers = self._select_layers_knee(sorted_layers)
|
| 3445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3446 |
# Re-apply jailbreak-contrastive blending with data-driven alpha
|
| 3447 |
if self.use_jailbreak_contrast and self._jailbreak_means:
|
| 3448 |
for idx in self._strong_layers:
|
|
@@ -3593,8 +3634,10 @@ class AbliterationPipeline:
|
|
| 3593 |
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 3594 |
with torch.no_grad():
|
| 3595 |
outputs = model(**inputs, labels=inputs["input_ids"])
|
| 3596 |
-
|
| 3597 |
-
|
|
|
|
|
|
|
| 3598 |
del inputs, outputs
|
| 3599 |
except Exception:
|
| 3600 |
pass
|
|
@@ -3860,7 +3903,9 @@ class AbliterationPipeline:
|
|
| 3860 |
original_norm = saved_norms[param_name]
|
| 3861 |
if original_norm > 0:
|
| 3862 |
new_norm = param.data.norm().item()
|
| 3863 |
-
if new_norm
|
|
|
|
|
|
|
| 3864 |
param.data.mul_(original_norm / new_norm)
|
| 3865 |
|
| 3866 |
@staticmethod
|
|
@@ -3896,6 +3941,10 @@ class AbliterationPipeline:
|
|
| 3896 |
W, is_quantized = AbliterationPipeline._dequantize_weight(proj)
|
| 3897 |
d = direction.to(device=W.device, dtype=W.dtype)
|
| 3898 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3899 |
if W.shape[-1] == d.shape[0]:
|
| 3900 |
# Standard Linear: W is (out_features, hidden_dim)
|
| 3901 |
original_norm_sq = W.pow(2).sum().item() if norm_preserve else 0.0
|
|
@@ -3905,6 +3954,15 @@ class AbliterationPipeline:
|
|
| 3905 |
W.sub_(d.T * (scale * coeff)) # in-place rank-1 update
|
| 3906 |
del coeff
|
| 3907 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3908 |
# Analytical norm: ||W'||² = ||W||² - scale(2-scale)||coeff||²
|
| 3909 |
if norm_preserve and original_norm_sq > 0:
|
| 3910 |
new_norm_sq = max(0.0, original_norm_sq - scale * (2 - scale) * coeff_norm_sq)
|
|
@@ -3926,6 +3984,11 @@ class AbliterationPipeline:
|
|
| 3926 |
W.sub_((scale * d) * coeff) # in-place rank-1 update
|
| 3927 |
del coeff
|
| 3928 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3929 |
# Analytical norm: ||W'||² = ||W||² - scale(2-scale)||coeff||²
|
| 3930 |
if norm_preserve and original_norm_sq > 0:
|
| 3931 |
new_norm_sq = max(0.0, original_norm_sq - scale * (2 - scale) * coeff_norm_sq)
|
|
@@ -4902,24 +4965,42 @@ class AbliterationPipeline:
|
|
| 4902 |
self.log("Measuring perplexity on reference texts...")
|
| 4903 |
total_loss = 0.0
|
| 4904 |
n_tokens = 0
|
|
|
|
| 4905 |
for text in reference_texts:
|
| 4906 |
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=self.max_seq_length or 256)
|
| 4907 |
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 4908 |
with torch.no_grad():
|
| 4909 |
outputs = model(**inputs, labels=inputs["input_ids"])
|
|
|
|
| 4910 |
seq_len = inputs["input_ids"].shape[1]
|
| 4911 |
-
|
| 4912 |
-
|
|
|
|
|
|
|
|
|
|
| 4913 |
del inputs, outputs
|
| 4914 |
self._free_gpu_memory()
|
| 4915 |
|
| 4916 |
-
|
| 4917 |
-
|
| 4918 |
-
perplexity = math.exp(min(avg_loss, 100.0)) # clamp to avoid OverflowError
|
| 4919 |
-
except OverflowError:
|
| 4920 |
perplexity = float("inf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4921 |
self._quality_metrics["perplexity"] = perplexity
|
| 4922 |
-
self.log(f" Perplexity: {perplexity:.2f}")
|
| 4923 |
|
| 4924 |
# 2. Generation coherence test
|
| 4925 |
test_prompts = [
|
|
@@ -5181,24 +5262,35 @@ class AbliterationPipeline:
|
|
| 5181 |
post_logits = torch.cat(all_post_logits, dim=0)
|
| 5182 |
pre_logits = self._baseline_first_token_logits[:post_logits.shape[0]]
|
| 5183 |
|
| 5184 |
-
#
|
| 5185 |
-
|
| 5186 |
-
|
| 5187 |
-
|
| 5188 |
-
|
| 5189 |
-
).sum(dim=-1).clamp(min=0.0)
|
| 5190 |
-
kl_divergence = kl_per_prompt.mean().item()
|
| 5191 |
-
|
| 5192 |
-
self._quality_metrics["kl_divergence"] = kl_divergence
|
| 5193 |
-
if kl_divergence < 0.2:
|
| 5194 |
-
kl_label = "excellent"
|
| 5195 |
-
elif kl_divergence < 0.5:
|
| 5196 |
-
kl_label = "good"
|
| 5197 |
-
elif kl_divergence < 1.0:
|
| 5198 |
-
kl_label = "moderate"
|
| 5199 |
else:
|
| 5200 |
-
|
| 5201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5202 |
except Exception as e:
|
| 5203 |
self.log(f" KL divergence computation failed (non-fatal): {e}")
|
| 5204 |
self._quality_metrics["kl_divergence"] = None
|
|
|
|
| 1595 |
else:
|
| 1596 |
self._strong_layers = knee_layers
|
| 1597 |
|
| 1598 |
+
# ── Small-model safeguards ────────────────────────────────────
|
| 1599 |
+
# Models with few layers (≤16) are highly sensitive to ablation.
|
| 1600 |
+
# Projecting early layers (0, 1) or too many layers relative to
|
| 1601 |
+
# total can destroy the model's ability to produce coherent output.
|
| 1602 |
+
#
|
| 1603 |
+
# Guard 1: Exclude the first 2 layers (layers 0 and 1) — these
|
| 1604 |
+
# encode fundamental token representations, not refusal.
|
| 1605 |
+
# The COSMIC cosine-similarity metric often selects layer 0
|
| 1606 |
+
# because it naturally has divergent harmful/harmless representations
|
| 1607 |
+
# (token-level differences, not refusal-specific).
|
| 1608 |
+
# Guard 2: Cap selected layers to at most 25% of total for small
|
| 1609 |
+
# models (≤16 layers). Even 30% ablation of a 12-layer model
|
| 1610 |
+
# causes decoherence.
|
| 1611 |
+
if self._strong_layers and n_layers > 0:
|
| 1612 |
+
min_safe_layer = min(2, n_layers // 4) # layers 0..(min_safe-1) are off-limits
|
| 1613 |
+
early_excluded = [idx for idx in self._strong_layers if idx < min_safe_layer]
|
| 1614 |
+
if early_excluded:
|
| 1615 |
+
self._strong_layers = [idx for idx in self._strong_layers if idx >= min_safe_layer]
|
| 1616 |
+
self.log(
|
| 1617 |
+
f"Excluded early layers {early_excluded} from ablation "
|
| 1618 |
+
f"(first {min_safe_layer} layers encode fundamental representations)"
|
| 1619 |
+
)
|
| 1620 |
+
|
| 1621 |
+
if n_layers <= 16 and len(self._strong_layers) > 0:
|
| 1622 |
+
max_small_model_layers = max(1, int(n_layers * 0.25))
|
| 1623 |
+
if len(self._strong_layers) > max_small_model_layers:
|
| 1624 |
+
self._strong_layers = self._strong_layers[:max_small_model_layers]
|
| 1625 |
+
self.log(
|
| 1626 |
+
f"Capped to {max_small_model_layers} layers for small model "
|
| 1627 |
+
f"(25% of {n_layers} layers)"
|
| 1628 |
+
)
|
| 1629 |
+
|
| 1630 |
# Cap layer count for inversion modes — reflecting too many weak-signal
|
| 1631 |
# layers destroys coherence. Limit to top 40% of total layers.
|
| 1632 |
if self.invert_refusal and len(self._strong_layers) > 0:
|
|
|
|
| 3475 |
sorted_layers = sorted(norms.items(), key=lambda x: x[1], reverse=True)
|
| 3476 |
self._strong_layers = self._select_layers_knee(sorted_layers)
|
| 3477 |
|
| 3478 |
+
# Apply small-model safeguards (matching _distill)
|
| 3479 |
+
if self._strong_layers and n_layers > 0:
|
| 3480 |
+
min_safe_layer = min(2, n_layers // 4)
|
| 3481 |
+
self._strong_layers = [idx for idx in self._strong_layers if idx >= min_safe_layer]
|
| 3482 |
+
if n_layers <= 16 and len(self._strong_layers) > 0:
|
| 3483 |
+
max_small = max(1, int(n_layers * 0.25))
|
| 3484 |
+
if len(self._strong_layers) > max_small:
|
| 3485 |
+
self._strong_layers = self._strong_layers[:max_small]
|
| 3486 |
+
|
| 3487 |
# Re-apply jailbreak-contrastive blending with data-driven alpha
|
| 3488 |
if self.use_jailbreak_contrast and self._jailbreak_means:
|
| 3489 |
for idx in self._strong_layers:
|
|
|
|
| 3634 |
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 3635 |
with torch.no_grad():
|
| 3636 |
outputs = model(**inputs, labels=inputs["input_ids"])
|
| 3637 |
+
loss_val = outputs.loss.item()
|
| 3638 |
+
if not math.isnan(loss_val) and not math.isinf(loss_val):
|
| 3639 |
+
total_loss += loss_val * inputs["input_ids"].shape[1]
|
| 3640 |
+
n_tokens += inputs["input_ids"].shape[1]
|
| 3641 |
del inputs, outputs
|
| 3642 |
except Exception:
|
| 3643 |
pass
|
|
|
|
| 3903 |
original_norm = saved_norms[param_name]
|
| 3904 |
if original_norm > 0:
|
| 3905 |
new_norm = param.data.norm().item()
|
| 3906 |
+
if math.isnan(new_norm) or math.isinf(new_norm) or new_norm == 0:
|
| 3907 |
+
continue # Skip — weight is degenerate after projection
|
| 3908 |
+
if abs(new_norm - original_norm) > 1e-6:
|
| 3909 |
param.data.mul_(original_norm / new_norm)
|
| 3910 |
|
| 3911 |
@staticmethod
|
|
|
|
| 3941 |
W, is_quantized = AbliterationPipeline._dequantize_weight(proj)
|
| 3942 |
d = direction.to(device=W.device, dtype=W.dtype)
|
| 3943 |
|
| 3944 |
+
# Skip projection if weight or direction contains NaN/Inf
|
| 3945 |
+
if not torch.isfinite(W).all() or not torch.isfinite(d).all():
|
| 3946 |
+
continue
|
| 3947 |
+
|
| 3948 |
if W.shape[-1] == d.shape[0]:
|
| 3949 |
# Standard Linear: W is (out_features, hidden_dim)
|
| 3950 |
original_norm_sq = W.pow(2).sum().item() if norm_preserve else 0.0
|
|
|
|
| 3954 |
W.sub_(d.T * (scale * coeff)) # in-place rank-1 update
|
| 3955 |
del coeff
|
| 3956 |
|
| 3957 |
+
# Verify projection didn't produce NaN (can happen with
|
| 3958 |
+
# degenerate weights on small models)
|
| 3959 |
+
if not torch.isfinite(W).all():
|
| 3960 |
+
# Revert: re-load from the Linear module's original data
|
| 3961 |
+
# This shouldn't normally happen but guards against
|
| 3962 |
+
# numerical catastrophe
|
| 3963 |
+
W.copy_(proj.weight.data if not is_quantized else W)
|
| 3964 |
+
continue
|
| 3965 |
+
|
| 3966 |
# Analytical norm: ||W'||² = ||W||² - scale(2-scale)||coeff||²
|
| 3967 |
if norm_preserve and original_norm_sq > 0:
|
| 3968 |
new_norm_sq = max(0.0, original_norm_sq - scale * (2 - scale) * coeff_norm_sq)
|
|
|
|
| 3984 |
W.sub_((scale * d) * coeff) # in-place rank-1 update
|
| 3985 |
del coeff
|
| 3986 |
|
| 3987 |
+
# Verify projection didn't produce NaN
|
| 3988 |
+
if not torch.isfinite(W).all():
|
| 3989 |
+
W.copy_(proj.weight.data if not is_quantized else W)
|
| 3990 |
+
continue
|
| 3991 |
+
|
| 3992 |
# Analytical norm: ||W'||² = ||W||² - scale(2-scale)||coeff||²
|
| 3993 |
if norm_preserve and original_norm_sq > 0:
|
| 3994 |
new_norm_sq = max(0.0, original_norm_sq - scale * (2 - scale) * coeff_norm_sq)
|
|
|
|
| 4965 |
self.log("Measuring perplexity on reference texts...")
|
| 4966 |
total_loss = 0.0
|
| 4967 |
n_tokens = 0
|
| 4968 |
+
has_nan_loss = False
|
| 4969 |
for text in reference_texts:
|
| 4970 |
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=self.max_seq_length or 256)
|
| 4971 |
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 4972 |
with torch.no_grad():
|
| 4973 |
outputs = model(**inputs, labels=inputs["input_ids"])
|
| 4974 |
+
loss_val = outputs.loss.item()
|
| 4975 |
seq_len = inputs["input_ids"].shape[1]
|
| 4976 |
+
if math.isnan(loss_val) or math.isinf(loss_val):
|
| 4977 |
+
has_nan_loss = True
|
| 4978 |
+
else:
|
| 4979 |
+
total_loss += loss_val * seq_len
|
| 4980 |
+
n_tokens += seq_len
|
| 4981 |
del inputs, outputs
|
| 4982 |
self._free_gpu_memory()
|
| 4983 |
|
| 4984 |
+
if has_nan_loss and n_tokens == 0:
|
| 4985 |
+
# All reference texts produced NaN loss — model is completely broken
|
|
|
|
|
|
|
| 4986 |
perplexity = float("inf")
|
| 4987 |
+
self.log(" Perplexity: inf (model produces NaN outputs — weights may be destroyed)")
|
| 4988 |
+
elif has_nan_loss:
|
| 4989 |
+
# Some texts produced NaN — compute from valid ones but warn
|
| 4990 |
+
avg_loss = total_loss / n_tokens
|
| 4991 |
+
try:
|
| 4992 |
+
perplexity = math.exp(min(avg_loss, 100.0))
|
| 4993 |
+
except OverflowError:
|
| 4994 |
+
perplexity = float("inf")
|
| 4995 |
+
self.log(f" Perplexity: {perplexity:.2f} (WARNING: some reference texts produced NaN loss)")
|
| 4996 |
+
else:
|
| 4997 |
+
avg_loss = total_loss / n_tokens if n_tokens > 0 else float("inf")
|
| 4998 |
+
try:
|
| 4999 |
+
perplexity = math.exp(min(avg_loss, 100.0)) # clamp to avoid OverflowError
|
| 5000 |
+
except OverflowError:
|
| 5001 |
+
perplexity = float("inf")
|
| 5002 |
+
self.log(f" Perplexity: {perplexity:.2f}")
|
| 5003 |
self._quality_metrics["perplexity"] = perplexity
|
|
|
|
| 5004 |
|
| 5005 |
# 2. Generation coherence test
|
| 5006 |
test_prompts = [
|
|
|
|
| 5262 |
post_logits = torch.cat(all_post_logits, dim=0)
|
| 5263 |
pre_logits = self._baseline_first_token_logits[:post_logits.shape[0]]
|
| 5264 |
|
| 5265 |
+
# Check for NaN/Inf in post-ablation logits (model may be broken)
|
| 5266 |
+
if torch.isnan(post_logits).any() or torch.isinf(post_logits).any():
|
| 5267 |
+
self.log(" KL divergence: inf (model produces NaN/Inf logits — weights may be destroyed)")
|
| 5268 |
+
kl_divergence = float("inf")
|
| 5269 |
+
self._quality_metrics["kl_divergence"] = kl_divergence
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5270 |
else:
|
| 5271 |
+
# Use F.kl_div for numerical stability
|
| 5272 |
+
log_p = torch.nn.functional.log_softmax(pre_logits.float(), dim=-1)
|
| 5273 |
+
log_q = torch.nn.functional.log_softmax(post_logits.float(), dim=-1)
|
| 5274 |
+
kl_per_prompt = torch.nn.functional.kl_div(
|
| 5275 |
+
log_q, log_p, log_target=True, reduction="none"
|
| 5276 |
+
).sum(dim=-1).clamp(min=0.0)
|
| 5277 |
+
kl_divergence = kl_per_prompt.mean().item()
|
| 5278 |
+
|
| 5279 |
+
# Guard against NaN from numerical issues in KL computation
|
| 5280 |
+
if math.isnan(kl_divergence) or math.isinf(kl_divergence):
|
| 5281 |
+
kl_divergence = float("inf")
|
| 5282 |
+
self.log(" First-token KL divergence: inf (numerical overflow — model may be severely damaged)")
|
| 5283 |
+
else:
|
| 5284 |
+
if kl_divergence < 0.2:
|
| 5285 |
+
kl_label = "excellent"
|
| 5286 |
+
elif kl_divergence < 0.5:
|
| 5287 |
+
kl_label = "good"
|
| 5288 |
+
elif kl_divergence < 1.0:
|
| 5289 |
+
kl_label = "moderate"
|
| 5290 |
+
else:
|
| 5291 |
+
kl_label = "high"
|
| 5292 |
+
self.log(f" First-token KL divergence: {kl_divergence:.4f} ({kl_label})")
|
| 5293 |
+
self._quality_metrics["kl_divergence"] = kl_divergence
|
| 5294 |
except Exception as e:
|
| 5295 |
self.log(f" KL divergence computation failed (non-fatal): {e}")
|
| 5296 |
self._quality_metrics["kl_divergence"] = None
|