pliny-the-prompter commited on
Commit
11fce6d
·
verified ·
1 Parent(s): 035b064

Upload 130 files

Browse files
Files changed (3) hide show
  1. app.py +83 -22
  2. obliteratus/.DS_Store +0 -0
  3. 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
- yield status_msg, "\n".join(log_lines), get_chat_header()
 
 
 
 
 
 
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
- # The @spaces.GPU decorator guarantees a GPU is available here, so move if needed.
 
 
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
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- gr.update(choices=_get_session_model_choices(), value=_last_obliterated_label or None),
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
- total_loss += outputs.loss.item() * inputs["input_ids"].shape[1]
3597
- n_tokens += inputs["input_ids"].shape[1]
 
 
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 > 0 and abs(new_norm - original_norm) > 1e-6:
 
 
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
- total_loss += outputs.loss.item() * seq_len
4912
- n_tokens += seq_len
 
 
 
4913
  del inputs, outputs
4914
  self._free_gpu_memory()
4915
 
4916
- avg_loss = total_loss / n_tokens if n_tokens > 0 else float("inf")
4917
- try:
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
- # Use F.kl_div for numerical stability
5185
- log_p = torch.nn.functional.log_softmax(pre_logits.float(), dim=-1)
5186
- log_q = torch.nn.functional.log_softmax(post_logits.float(), dim=-1)
5187
- kl_per_prompt = torch.nn.functional.kl_div(
5188
- log_q, log_p, log_target=True, reduction="none"
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
- kl_label = "high"
5201
- self.log(f" First-token KL divergence: {kl_divergence:.4f} ({kl_label})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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