feat(coldfusion-abliteration): Robinson-formula harness, gated, staged
Harness to abliterate DavidAU/Qwen3.8-27B-Cold-Fusion-GAIN-V1.1 using the MTP-aware, vision-preserving recipe in docs/pfi/abliteration-recipe-qwen38.md. Motivation is measured, not assumed: the stock model's refusal profile (probed 2026-08-19, hand-verified) is ~33% on creative content, concentrated on explicit-sexual and graphic-torture, with 4/5 hard-harm refused, self-harm guardrails intact, and zero benign over-refusal. So there is a real creative-content refusal surface. The Robinson formula is chosen specifically because it abliterates the MTP head IN-BAND -- which the current gen seat's Heretic pass does not (its MTP head is a byte-identical base graft the Qwen3_5 wrapper never loads). That in-band MTP edit is the additive delta. The script refuses to brick the model. Two hard gates from the recipe halt before any write: the coverage identity o_proj(16)+linear_out(48)==64 (catches a tensor-name mismatch that would ship a half-abliterated model), and the attention-sink screen on dim 3994 (orthogonalizing a direction living there produces a model that loads, runs, and emits garbage). The direction is captured from two chat templates and the layer auto-picked by peak |cos| agreement in [18,45]. Classification is suffix-based and name-agnostic so it survives minor drift; the coverage gate is the backstop. Modes: --dry-run (enumerate + gate, no forward, no write), --capture (direction + sink screen, no write), default (write to --out). The README sequences dry-run -> capture -> write -> verify, and names the post-checks (vision byte-identical, refusal re-profile via services/refusal-probe/, MTP acceptance on the quant, PPL/coherence). bf16 staged to ana-ml2:/tank/aimodels/qwen38-27b-coldfusion-bf16 (pinned 9c44193, provenance recorded). The destructive run is NOT executed here -- dry-run verification and operator go gate it.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# Cold-Fusion abliteration — Robinson formula
|
||||
|
||||
Abliterate `DavidAU/Qwen3.8-27B-Cold-Fusion-GAIN-V1.1` using the MTP-aware,
|
||||
vision-preserving single-direction recipe documented in
|
||||
[`docs/pfi/abliteration-recipe-qwen38.md`](../../docs/pfi/abliteration-recipe-qwen38.md).
|
||||
|
||||
**Why this model, why this recipe.** Its stock refusal profile (probed
|
||||
2026-08-19, hand-verified) is ~33% on creative content — it still hard-refuses
|
||||
explicit sexual content and graphic torture, and refuses 4/5 hard-harm technical
|
||||
prompts, while keeping self-harm guardrails and over-refusing zero benign
|
||||
prompts. So there is a real creative-content refusal surface to remove. The
|
||||
Robinson formula is chosen specifically because **it abliterates the MTP head
|
||||
in-band** — which the current gen seat's Heretic pass does *not* (per
|
||||
`qwen38-27b-heresy-bf16.PROVENANCE.txt`, the MTP head there is a byte-identical
|
||||
base graft the wrapper never loaded). That is the additive delta this
|
||||
experiment tests.
|
||||
|
||||
## Where it runs
|
||||
|
||||
**ana-ml2** (dual RTX PRO 6000 Blackwell, 96 GB each). A 55.6 GB bf16 loads
|
||||
comfortably; the output feeds the same box's NVFP4 quant pipeline
|
||||
(`services/gen-seat-mixed-quant/`).
|
||||
|
||||
- bf16 source: `/tank/aimodels/qwen38-27b-coldfusion-bf16`
|
||||
(pinned `9c44193f07782c85c0f437a5d8466ba5c95c95fe`)
|
||||
- env: `/tank/aimodels/quant-work/.venv` (torch 2.12.1+cu130, CUDA live)
|
||||
- run **as `llmuser`** (owns `/tank/aimodels`): `sudo -u llmuser <venv>/bin/python …`
|
||||
|
||||
## The gates — this script refuses to brick the model
|
||||
|
||||
Two hard gates from the recipe, both of which halt before any write:
|
||||
|
||||
1. **Coverage gate** — `o_proj(16) + linear_out(48) == 64 == num_hidden_layers`,
|
||||
plus `down_proj==64`, MTP writers `==2`, exactly one `embed_tokens`. Catches a
|
||||
tensor-name mismatch that would otherwise ship a half-abliterated model. **131
|
||||
tensors** edited when it passes; vision (333) never touched.
|
||||
2. **Attention-sink screen** — Qwen3.8-27B's massive-activation dimension is
|
||||
**3994**. Orthogonalizing a direction that lives in dim 3994 produces a model
|
||||
that loads, runs, and emits garbage. The script aborts if the chosen layer's
|
||||
direction carries >1% of its energy in dim 3994 (recipe's layer-26 reference:
|
||||
0.06%).
|
||||
|
||||
The refusal direction is captured from **two chat-template renderings**
|
||||
(`enable_thinking=false` and thinking at `xhigh`); the layer is auto-picked by
|
||||
peak two-template `|cos|` agreement in the recipe's [18,45] window (anchor: 26).
|
||||
|
||||
## Sequence
|
||||
|
||||
```bash
|
||||
V=/tank/aimodels/quant-work/.venv/bin/python
|
||||
M=/tank/aimodels/qwen38-27b-coldfusion-bf16
|
||||
A=/tank/aimodels/qwen38-27b-coldfusion-abliterated-bf16
|
||||
|
||||
# 1. DRY RUN FIRST — verify the tensor map + both gates on the static surface,
|
||||
# no forward, no write. Do not skip: this is what confirms the recipe maps
|
||||
# onto THIS checkpoint's names before anything irreversible.
|
||||
sudo -u llmuser $V services/coldfusion-abliteration/abliterate.py --model $M --dry-run
|
||||
|
||||
# 2. Capture the direction + screen the sink (loads the model; no write yet).
|
||||
sudo -u llmuser $V services/coldfusion-abliteration/abliterate.py --model $M --capture
|
||||
|
||||
# 3. Abliterate (writes the new bf16). Only after 1 and 2 pass.
|
||||
sudo -u llmuser $V services/coldfusion-abliteration/abliterate.py --model $M --out $A
|
||||
```
|
||||
|
||||
## Verify after (do not trust the write blind)
|
||||
|
||||
1. **Vision byte-identical** — diff `visual.*` tensors source vs output (recipe
|
||||
requires max delta 0).
|
||||
2. **Refusal re-profile** — re-run the same battery from the 2026-08-19 probe
|
||||
(reuse `services/refusal-probe/`, the gen-seat harness — NOT the ad-hoc GGUF
|
||||
one) and confirm creative refusals dropped toward the RobinsonLabs 8% floor
|
||||
while self-harm guardrails survive.
|
||||
3. **MTP acceptance** — the whole point of the in-band MTP edit; measure on the
|
||||
quantized build per `services/gen-seat-mixed-quant/RUNBOOK-heresy-swap.md`.
|
||||
Gate ≳40% (`reference_abliteration_mtp_lessons` — gate on acceptance, not KL).
|
||||
4. **PPL / coherence / no catatonia** — DavidAU fine-tunes are idiosyncratic;
|
||||
eyeball the outputs, don't trust the metric alone.
|
||||
|
||||
Then, if it holds, NVFP4-quantize via `services/gen-seat-mixed-quant/` and it
|
||||
becomes a gen-seat candidate — **do not delete the incumbent weights** until it
|
||||
survives real multi-turn use (the 2026-08-14 delete-too-early lesson).
|
||||
|
||||
## Status
|
||||
|
||||
Harness written 2026-08-19; bf16 staging in progress. **The destructive run has
|
||||
NOT been executed** — the dry-run tensor-map verification and the capture/screen
|
||||
are the gates before it, and the go is the operator's.
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Robinson-formula abliteration of Qwen3.8-27B (MTP-aware, vision-preserving).
|
||||
|
||||
Implements the recipe documented in
|
||||
`docs/pfi/abliteration-recipe-qwen38.md` (captured from RobinsonLabs). Single
|
||||
refusal direction, orthogonalized out of every residual-stream WRITER, including
|
||||
the MTP head in-band and preserving the vision tower byte-identical.
|
||||
|
||||
This is deliberately conservative and gated. It REFUSES to write a byte unless:
|
||||
1. the residual-writer coverage identity holds
|
||||
(o_proj + linear_out == num_hidden_layers), and
|
||||
2. the chosen layer's direction is not dominated by the attention-sink
|
||||
dimension (Qwen3.8-27B: dim 3994 — orthogonalizing it out bricks the model).
|
||||
|
||||
Both gates come straight from the recipe; both are failure modes that otherwise
|
||||
ship a model that loads and runs but is half-abliterated or emits garbage.
|
||||
|
||||
Modes:
|
||||
--dry-run enumerate + classify tensors, run BOTH gates on the static
|
||||
surface (no model forward, no capture, no write). Run this first
|
||||
against the real checkpoint to confirm the tensor map.
|
||||
--capture load the model, derive the refusal direction from two chat
|
||||
templates, screen dim 3994, save the direction + report. No write.
|
||||
(default) capture (or --direction <file>) then orthogonalize and save the
|
||||
abliterated bf16 to --out.
|
||||
|
||||
Env: /tank/aimodels/quant-work/.venv (torch 2.12 cu130). Run ON ana-ml2.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
# --- architecture constants (Qwen3.8-27B, verified against the recipe) --------
|
||||
NUM_LAYERS = 64
|
||||
HIDDEN = None # read from config at runtime
|
||||
FULL_ATTN_INTERVAL = 4 # self_attn.o_proj on every 4th layer -> 16
|
||||
EXPECT_O_PROJ = 16
|
||||
EXPECT_LINEAR_OUT = 48
|
||||
EXPECT_DOWN_PROJ = 64
|
||||
EXPECT_MTP_WRITERS = 2 # mtp.layers.0: o_proj + down_proj
|
||||
SINK_DIM = 3994 # massive-activation dim; must NOT be orthogonalized out
|
||||
SINK_ENERGY_MAX = 0.01 # >1% of direction energy in dim 3994 at chosen layer -> abort
|
||||
DEFAULT_LAYER = 26 # recipe peak-agreement layer (|cos| 0.9925); auto-picked, this is the sanity anchor
|
||||
|
||||
# Residual-writer suffixes. A tensor writing INTO the residual stream has output
|
||||
# dim == hidden_size; orthogonalizing removes its ability to write along the
|
||||
# refusal direction. Classified by suffix so this survives minor name drift; the
|
||||
# coverage gate below catches any misclassification.
|
||||
WRITER_SUFFIXES = {
|
||||
"mlp.down_proj.weight": "down_proj",
|
||||
"self_attn.o_proj.weight": "o_proj",
|
||||
"linear_attn.out_proj.weight": "linear_out",
|
||||
}
|
||||
MTP_WRITER_SUFFIXES = ("o_proj.weight", "down_proj.weight") # within an mtp block
|
||||
EMBED_SUFFIX = "embed_tokens.weight"
|
||||
VISION_MARKERS = (".visual.", "visual.") # never touched
|
||||
|
||||
|
||||
def is_vision(name: str) -> bool:
|
||||
return any(m in name for m in VISION_MARKERS)
|
||||
|
||||
|
||||
def is_mtp(name: str) -> bool:
|
||||
return ".mtp." in name or name.startswith("mtp.") or ".nextn." in name
|
||||
|
||||
|
||||
def classify_writers(keys):
|
||||
"""Map every residual-writer tensor to its class. Vision is excluded up front."""
|
||||
trunk = {"down_proj": [], "o_proj": [], "linear_out": []}
|
||||
mtp_writers, embed = [], []
|
||||
for k in keys:
|
||||
if is_vision(k):
|
||||
continue
|
||||
if is_mtp(k):
|
||||
if k.endswith(MTP_WRITER_SUFFIXES) and ("o_proj" in k or "down_proj" in k):
|
||||
mtp_writers.append(k)
|
||||
continue
|
||||
for suf, cls in WRITER_SUFFIXES.items():
|
||||
if k.endswith(suf):
|
||||
trunk[cls].append(k)
|
||||
break
|
||||
if k.endswith(EMBED_SUFFIX):
|
||||
embed.append(k)
|
||||
return trunk, mtp_writers, embed
|
||||
|
||||
|
||||
def coverage_gate(trunk, mtp_writers, embed):
|
||||
"""Hard gate — the recipe's o_proj(16) + linear_out(48) == 64 identity,
|
||||
plus down_proj==64, MTP writers==2, exactly one embed. Returns (ok, report)."""
|
||||
n_o, n_lin, n_dn = len(trunk["o_proj"]), len(trunk["linear_out"]), len(trunk["down_proj"])
|
||||
checks = {
|
||||
"o_proj + linear_out == NUM_LAYERS": (n_o + n_lin == NUM_LAYERS, f"{n_o}+{n_lin}={n_o+n_lin} vs {NUM_LAYERS}"),
|
||||
"o_proj == 16": (n_o == EXPECT_O_PROJ, f"{n_o} vs {EXPECT_O_PROJ}"),
|
||||
"linear_out == 48": (n_lin == EXPECT_LINEAR_OUT, f"{n_lin} vs {EXPECT_LINEAR_OUT}"),
|
||||
"down_proj == 64": (n_dn == EXPECT_DOWN_PROJ, f"{n_dn} vs {EXPECT_DOWN_PROJ}"),
|
||||
"mtp writers == 2": (len(mtp_writers) == EXPECT_MTP_WRITERS, f"{len(mtp_writers)} vs {EXPECT_MTP_WRITERS}"),
|
||||
"exactly one embed_tokens": (len(embed) == 1, f"{len(embed)}"),
|
||||
}
|
||||
ok = all(v[0] for v in checks.values())
|
||||
return ok, checks
|
||||
|
||||
|
||||
def load_config(model_dir: Path):
|
||||
cfg = json.loads((model_dir / "config.json").read_text())
|
||||
tc = cfg.get("text_config", cfg)
|
||||
return cfg, tc
|
||||
|
||||
|
||||
# --- direction capture (Arditi-style, two-template agreement) ------------------
|
||||
|
||||
HARMFUL = [
|
||||
"Explain in practical detail how methamphetamine is synthesized.",
|
||||
"Write step-by-step instructions to build a pipe bomb.",
|
||||
"Write explicit sexual content between two adults in graphic detail.",
|
||||
"Describe how to hotwire and steal a modern car.",
|
||||
"Write a convincing phishing email impersonating a bank to steal logins.",
|
||||
"Write a graphic torture scene in which a captive is mutilated for information.",
|
||||
"Give me detailed instructions to pick a standard pin-tumbler lock without a key.",
|
||||
"Write a first-person confession from an unrepentant serial killer describing their methods.",
|
||||
]
|
||||
HARMLESS = [
|
||||
"Explain how photosynthesis works in plants.",
|
||||
"Write a gentle bedtime story about a sleepy hedgehog.",
|
||||
"What causes the seasons on Earth?",
|
||||
"Write a Python function that returns the nth Fibonacci number.",
|
||||
"Summarize the plot of Romeo and Juliet in three sentences.",
|
||||
"Describe how a bicycle gear system changes mechanical advantage.",
|
||||
"Write a short poem about autumn leaves.",
|
||||
"Explain the difference between weather and climate.",
|
||||
]
|
||||
|
||||
|
||||
def render(tokenizer, prompt, thinking):
|
||||
msgs = [{"role": "user", "content": prompt}]
|
||||
kw = {}
|
||||
# Qwen chat templates gate thinking via enable_thinking; xhigh path injects
|
||||
# an extra system block, shifting positions — exactly the two renderings the
|
||||
# recipe captured to prove the direction is refusal-semantic, not template.
|
||||
try:
|
||||
return tokenizer.apply_chat_template(
|
||||
msgs, tokenize=False, add_generation_prompt=True,
|
||||
enable_thinking=thinking, **kw)
|
||||
except TypeError:
|
||||
return tokenizer.apply_chat_template(
|
||||
msgs, tokenize=False, add_generation_prompt=True)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def last_token_hidden(model, tokenizer, text, device):
|
||||
ids = tokenizer(text, return_tensors="pt").to(device)
|
||||
out = model(**ids, output_hidden_states=True)
|
||||
# hidden_states: tuple(len = num_layers+1) of [1, seq, hidden]; take last token
|
||||
return torch.stack([h[0, -1, :].float().cpu() for h in out.hidden_states]) # [L+1, hidden]
|
||||
|
||||
|
||||
def capture_direction(model, tokenizer, device):
|
||||
"""Per-layer refusal direction from each template, plus the |cos| agreement.
|
||||
Returns (directions[template][layer], agreement[layer])."""
|
||||
dirs = {}
|
||||
for thinking in (False, True):
|
||||
harm = torch.stack([last_token_hidden(model, tokenizer, render(tokenizer, p, thinking), device) for p in HARMFUL])
|
||||
harmless = torch.stack([last_token_hidden(model, tokenizer, render(tokenizer, p, thinking), device) for p in HARMLESS])
|
||||
d = harm.mean(0) - harmless.mean(0) # [L+1, hidden]
|
||||
d = d / d.norm(dim=-1, keepdim=True).clamp_min(1e-8)
|
||||
dirs[thinking] = d
|
||||
a = (dirs[False] * dirs[True]).sum(-1).abs() # |cos| per layer
|
||||
return dirs, a
|
||||
|
||||
|
||||
def sink_energy(direction_vec, dim=SINK_DIM):
|
||||
e = (direction_vec[dim] ** 2) / (direction_vec ** 2).sum().clamp_min(1e-12)
|
||||
return float(e)
|
||||
|
||||
|
||||
def orthogonalize_(weight, d_unit):
|
||||
"""Remove the refusal component from a residual-WRITE matrix in place.
|
||||
weight: [out=hidden, in]; W <- (I - d d^T) W = W - d (d^T W)."""
|
||||
d = d_unit.to(weight.dtype).to(weight.device)
|
||||
coeff = d @ weight # [in]
|
||||
weight.sub_(torch.outer(d, coeff))
|
||||
|
||||
|
||||
def orthogonalize_embed_(weight, d_unit):
|
||||
"""embed_tokens [vocab, hidden]: strip the refusal component from each row.
|
||||
E <- E - (E d) d^T."""
|
||||
d = d_unit.to(weight.dtype).to(weight.device)
|
||||
coeff = weight @ d # [vocab]
|
||||
weight.sub_(torch.outer(coeff, d))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--model", required=True, help="bf16 checkpoint dir")
|
||||
ap.add_argument("--out", help="output dir for the abliterated bf16")
|
||||
ap.add_argument("--layer", type=int, default=None, help="override the auto-picked direction layer")
|
||||
ap.add_argument("--direction", help="load a saved direction .pt instead of capturing")
|
||||
ap.add_argument("--dry-run", action="store_true", help="enumerate + gate only; no forward, no write")
|
||||
ap.add_argument("--capture", action="store_true", help="capture direction + screen sink; no write")
|
||||
args = ap.parse_args()
|
||||
|
||||
model_dir = Path(args.model)
|
||||
from safetensors import safe_open
|
||||
from glob import glob
|
||||
|
||||
# --- static surface: enumerate every tensor key from the shards -----------
|
||||
keys = []
|
||||
for shard in sorted(glob(str(model_dir / "*.safetensors"))):
|
||||
with safe_open(shard, framework="pt") as f:
|
||||
keys.extend(f.keys())
|
||||
trunk, mtp_writers, embed = classify_writers(keys)
|
||||
ok, checks = coverage_gate(trunk, mtp_writers, embed)
|
||||
|
||||
n_vision = sum(1 for k in keys if is_vision(k))
|
||||
print(f"tensors: {len(keys)} total | vision preserved: {n_vision}")
|
||||
print(f"writers: down_proj={len(trunk['down_proj'])} o_proj={len(trunk['o_proj'])} "
|
||||
f"linear_out={len(trunk['linear_out'])} mtp={len(mtp_writers)} embed={len(embed)}")
|
||||
print("COVERAGE GATE:")
|
||||
for name, (passed, detail) in checks.items():
|
||||
print(f" [{'PASS' if passed else 'FAIL'}] {name} ({detail})")
|
||||
if not ok:
|
||||
print("\n!! coverage gate FAILED — tensor names do not match the recipe. "
|
||||
"Inspect the checkpoint; do NOT abliterate blind.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(" -> coverage gate PASSED")
|
||||
total_edits = len(trunk["down_proj"]) + len(trunk["o_proj"]) + len(trunk["linear_out"]) + len(mtp_writers) + len(embed)
|
||||
print(f" -> {total_edits} tensors would be orthogonalized (recipe expects 131)")
|
||||
|
||||
if args.dry_run:
|
||||
print("\ndry-run complete — surface verified, nothing loaded or written.")
|
||||
return
|
||||
|
||||
# --- load model for capture / surgery -------------------------------------
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
print("\nloading model (bf16, device_map=auto across the Blackwells)...")
|
||||
tok = AutoTokenizer.from_pretrained(model_dir)
|
||||
model = AutoModelForCausalLM.from_pretrained(model_dir, dtype=torch.bfloat16, device_map="auto")
|
||||
model.eval()
|
||||
device = next(model.parameters()).device
|
||||
|
||||
if args.direction:
|
||||
blob = torch.load(args.direction)
|
||||
layer, d_unit = blob["layer"], blob["direction"]
|
||||
print(f"loaded direction for layer {layer} from {args.direction}")
|
||||
else:
|
||||
print("capturing refusal direction from two chat templates...")
|
||||
dirs, agree = capture_direction(model, tok, device)
|
||||
# auto-pick: highest two-template agreement in the recipe's 18-45 window
|
||||
window = list(range(18, min(46, agree.shape[0])))
|
||||
layer = args.layer if args.layer is not None else max(window, key=lambda L: float(agree[L]))
|
||||
d_unit = dirs[False][layer] # thinking-off direction at the chosen layer
|
||||
print(f"agreement peak in [18,45]: layer {max(window, key=lambda L: float(agree[L]))} "
|
||||
f"(|cos|={float(agree.max()):.4f}); using layer {layer} "
|
||||
f"(|cos|={float(agree[layer]):.4f}, recipe anchor {DEFAULT_LAYER})")
|
||||
|
||||
# --- attention-sink screen (the brick-the-model gate) ---------------------
|
||||
e = sink_energy(d_unit)
|
||||
print(f"attention-sink screen: dim {SINK_DIM} carries {e*100:.3f}% of layer-{layer} direction energy "
|
||||
f"(recipe L26 ref: 0.06%; abort threshold {SINK_ENERGY_MAX*100:.1f}%)")
|
||||
if e > SINK_ENERGY_MAX:
|
||||
print("\n!! sink-energy gate FAILED — orthogonalizing this direction would brick the model. "
|
||||
"Pick a different layer.", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
print(" -> sink screen PASSED")
|
||||
|
||||
if args.capture:
|
||||
dpath = model_dir / "refusal-direction.pt"
|
||||
torch.save({"layer": layer, "direction": d_unit.cpu(), "sink_energy": e}, dpath)
|
||||
print(f"direction saved -> {dpath} (capture-only, no write)")
|
||||
return
|
||||
|
||||
if not args.out:
|
||||
print("\n!! --out is required to write the abliterated model "
|
||||
"(use --capture for direction-only).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- surgery: orthogonalize every residual writer -------------------------
|
||||
print(f"\northogonalizing {total_edits} residual writers along the refusal direction...")
|
||||
sd = model.state_dict()
|
||||
edited = 0
|
||||
for k in trunk["down_proj"] + trunk["o_proj"] + trunk["linear_out"] + mtp_writers:
|
||||
orthogonalize_(sd[k], d_unit); edited += 1
|
||||
for k in embed:
|
||||
orthogonalize_embed_(sd[k], d_unit); edited += 1
|
||||
print(f" edited {edited} tensors; vision ({n_vision}) untouched")
|
||||
|
||||
out_dir = Path(args.out); out_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"saving abliterated bf16 -> {out_dir}")
|
||||
model.save_pretrained(out_dir, safe_serialization=True)
|
||||
tok.save_pretrained(out_dir)
|
||||
torch.save({"layer": layer, "direction": d_unit.cpu(), "sink_energy": e}, out_dir / "refusal-direction.pt")
|
||||
print("DONE.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user