622 lines
27 KiB
Python
622 lines
27 KiB
Python
import json
|
||
import re
|
||
import shutil
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
import torch
|
||
from safetensors import safe_open
|
||
from safetensors.torch import save_file
|
||
|
||
import config
|
||
from core import rebase
|
||
from core.ablation import abliteration_direction, effective_coeffs
|
||
|
||
EDITS_DIR = config.DATA_DIR / "edits"
|
||
PRESETS_DIR = config.DATA_DIR / "presets"
|
||
|
||
# Residual writes edited by the global abliteration (embed aside)
|
||
TARGET_SUFFIXES = ("self_attn.o_proj", "mlp.down_proj")
|
||
|
||
|
||
def _now():
|
||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||
|
||
|
||
def list_presets():
|
||
PRESETS_DIR.mkdir(parents=True, exist_ok=True)
|
||
out = []
|
||
for path in sorted(PRESETS_DIR.glob("*.json")):
|
||
data = json.loads(path.read_text(encoding="utf-8"))
|
||
out.append({"name": path.stem, "n_rules": len(data.get("rules", [])), "model_id": data.get("model_id")})
|
||
return out
|
||
|
||
|
||
def save_preset(name, rules, model_id, scale=1.0):
|
||
PRESETS_DIR.mkdir(parents=True, exist_ok=True)
|
||
payload = {"model_id": model_id, "saved_at": _now(), "scale": scale, "rules": rules}
|
||
(PRESETS_DIR / f"{name}.json").write_text(
|
||
json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8"
|
||
)
|
||
return payload
|
||
|
||
|
||
def load_preset(name):
|
||
path = PRESETS_DIR / f"{name}.json"
|
||
if not path.exists():
|
||
raise ValueError(f"unknown preset {name}")
|
||
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
||
|
||
def delete_preset(name):
|
||
(PRESETS_DIR / f"{name}.json").unlink(missing_ok=True)
|
||
|
||
|
||
def compute_abliteration(rules, jl, scale=1.0):
|
||
"""Global pure-weight edit reproducing the abliteration-mode preview.
|
||
|
||
Applies to EVERY residual write (embed_tokens + o_proj/down_proj of every
|
||
layer) the same transform as the abliteration-mode hooks: for each rule,
|
||
``out += scale·(v̂_A·out)·w`` (applied sequentially, like the hooks). Since
|
||
the residual is the sum of all these writes, the direction is
|
||
removed/redirected across the whole residual — hence the fidelity (~0.97
|
||
cosine on the logits). This is the pure-weights path for architectures the
|
||
rebase does not support (write norms, Gemma style).
|
||
|
||
Returns ``(tensors, info)``:
|
||
- ``tensors``: {param_name: W_new (cpu, float32)}
|
||
- ``info``: {tied, embed_key, lm_head_key, path, delta_max, lowrank}
|
||
where ``lowrank`` = {param_name: (B [out, r], A [r, in])} — the SAME edit
|
||
as per-rule rank-1 factors (delta = B·A), exact, for the LoRA export.
|
||
For the embed, delta = (B·A)ᵀ (PEFT lookup convention).
|
||
"""
|
||
# layers=[] = disabled rule, in this mode too (consistent with the preview)
|
||
rules = [r for r in rules if r["layers"]]
|
||
if not rules:
|
||
raise ValueError("no active rule (all have 0 layers): nothing to export")
|
||
path = jl.layout.path
|
||
weight_u = jl._lm_head.weight
|
||
# (v_a, w_eff) per rule, with w_eff = alpha·v̂_A + beta·v̂_B: the SAME effective
|
||
# coefficients (saturation included) as the preview hooks
|
||
pairs = []
|
||
for r in rules:
|
||
v_a, v_b = abliteration_direction(weight_u, r)
|
||
alpha, beta = effective_coeffs(r["mode"], r["factor"], scale)
|
||
w_eff = alpha * v_a
|
||
if beta:
|
||
w_eff = w_eff + beta * v_b
|
||
pairs.append((v_a, w_eff))
|
||
|
||
# bake on CPU: the float32 matrices (embed ~1.5 GB) don't fit alongside the
|
||
# model on the GPU (OOM measured on 12 GB with a 4B loaded)
|
||
def apply_cols(W): # [d_model, d_in]: residual output = rows
|
||
cur, us, rows = W, [], []
|
||
for v_a, w in pairs:
|
||
row = v_a @ cur # composed over the previous rules
|
||
us.append(w)
|
||
rows.append(row)
|
||
cur = cur + torch.outer(w, row)
|
||
return cur, torch.stack(us, dim=1), torch.stack(rows, dim=0)
|
||
|
||
def apply_rows(E): # [vocab, d_model]: each ROW is a residual vector
|
||
cur, us, rows = E, [], []
|
||
for v_a, w in pairs:
|
||
col = cur @ v_a # [vocab]
|
||
us.append(w)
|
||
rows.append(col)
|
||
cur = cur + torch.outer(col, w)
|
||
return cur, torch.stack(us, dim=1), torch.stack(rows, dim=0)
|
||
|
||
tensors = {}
|
||
lowrank = {}
|
||
delta_max = 0.0
|
||
|
||
embed_key = f"{path}.{jl.layout.embed}.weight"
|
||
E = jl._embed_tokens.weight.detach().float().cpu()
|
||
E_new, B, A = apply_rows(E)
|
||
delta_max = max(delta_max, (E_new - E).abs().max().item())
|
||
tensors[embed_key] = E_new
|
||
lowrank[embed_key] = (B, A) # delta_embed = (B·A)ᵀ = summed outer(A_k, B_k)
|
||
|
||
skipped_writes = 0
|
||
for i, block in enumerate(jl.layers):
|
||
for suffix in TARGET_SUFFIXES:
|
||
module = block
|
||
for part in suffix.split("."):
|
||
module = getattr(module, part, None)
|
||
if module is None:
|
||
break
|
||
if module is None: # e.g. linear-attention blocks (no self_attn)
|
||
skipped_writes += 1
|
||
continue
|
||
W = module.weight.detach().float().cpu()
|
||
W_new, B, A = apply_cols(W)
|
||
delta_max = max(delta_max, (W_new - W).abs().max().item())
|
||
name = f"{path}.layers.{i}.{suffix}.weight"
|
||
tensors[name] = W_new
|
||
lowrank[name] = (B, A)
|
||
|
||
tied = jl._lm_head.weight.data_ptr() == jl._embed_tokens.weight.data_ptr()
|
||
info = {
|
||
"tied": tied,
|
||
"embed_key": embed_key,
|
||
"lm_head_key": f"{jl.layout.lm_head}.weight",
|
||
"path": path,
|
||
"delta_max": delta_max,
|
||
"lowrank": lowrank,
|
||
"skipped_writes": skipped_writes,
|
||
}
|
||
return tensors, info
|
||
|
||
|
||
def _abliteration_warnings(rules):
|
||
warns = []
|
||
for r in rules:
|
||
if r["mode"] == "scale" and r["factor"] > 1.0:
|
||
warns.append(
|
||
f"\"{(r['token'] or '').strip()}\" ×{r['factor']}: amplifying (factor > 1) "
|
||
"is approximate in pure weights (the hook composes over the layers)"
|
||
)
|
||
return warns
|
||
|
||
|
||
def export_abliteration(rules, jl, model_meta, *, fmt, name, source_dir=None, scale=1.0):
|
||
"""Pure-weight export (global abliteration). Formats: ``full`` (full
|
||
checkpoint), ``layers`` (safetensors of only the modified matrices) and
|
||
``lora`` (exact PEFT adapter, rank = n_rules; embed omitted if embeddings
|
||
are tied). Unties ``lm_head`` (full/layers) if the model has tied embeddings,
|
||
to preserve the original un-embedding."""
|
||
rules = [r for r in rules if r["layers"]] # layers=[] = disabled rule
|
||
if not rules:
|
||
raise ValueError("no active intervention to export")
|
||
if fmt not in ("full", "layers", "lora"):
|
||
raise ValueError(f"unknown format for abliteration: {fmt}")
|
||
|
||
tensors, info = compute_abliteration(rules, jl, scale=scale)
|
||
if info["delta_max"] < 1e-8:
|
||
raise ValueError(
|
||
"the bake changes no weight (neutral factors, scale=0 or null "
|
||
"directions) — the export would be identical to the original model"
|
||
)
|
||
out_dir = EDITS_DIR / name
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
dtype = torch.bfloat16 if model_meta.get("dtype") == "bf16" else torch.float16
|
||
lm_head_key = info["lm_head_key"]
|
||
summary = [
|
||
{k: r[k] for k in ("token_id", "token", "mode", "factor", "replacement_id", "replacement")}
|
||
for r in rules
|
||
]
|
||
meta = {
|
||
"name": name,
|
||
"format": fmt,
|
||
"method": "abliteration-global",
|
||
"model_id": model_meta.get("model_id"),
|
||
"model_revision": model_meta.get("revision"),
|
||
"dtype": model_meta.get("dtype"),
|
||
"global_scale": scale,
|
||
"untied_lm_head": info["tied"] and fmt in ("full", "layers"),
|
||
"rules": summary,
|
||
"modified_params_count": len(tensors) + (1 if info["tied"] else 0),
|
||
"warnings": _abliteration_warnings(rules) + (
|
||
[f"{info['skipped_writes']} residual write(s) without o_proj/down_proj "
|
||
"(hybrid architecture) left untouched — the bake is partial there; "
|
||
"prefer read projection when the architecture supports it"]
|
||
if info["skipped_writes"] else []
|
||
),
|
||
"note": (
|
||
"global abliteration: the token's direction is removed/redirected in "
|
||
"every residual write (embed + o_proj/down_proj of all layers). "
|
||
"Reproduces the abliteration-mode preview (~0.97 cosine on the logits). "
|
||
"Pure weights: a standard safetensors checkpoint."
|
||
),
|
||
"created_at": _now(),
|
||
}
|
||
|
||
if fmt == "layers":
|
||
out = {k: v.to(dtype) for k, v in tensors.items()}
|
||
if info["tied"]:
|
||
# original un-embedding (unedited embed) to write separately
|
||
out[lm_head_key] = jl._embed_tokens.weight.detach().to(dtype).cpu()
|
||
save_file(out, str(out_dir / "modified_layers.safetensors"))
|
||
|
||
elif fmt == "lora":
|
||
# The abliteration delta is EXACTLY rank-n_rules per matrix (delta = B·A),
|
||
# so the LoRA is exact — except the embed of a tied-embeddings model: PEFT
|
||
# can't untie lm_head, and editing the embed would corrupt the shared
|
||
# un-embedding → we omit it (reduced fidelity).
|
||
include_embed = not info["tied"]
|
||
if not include_embed:
|
||
meta["warnings"] = meta["warnings"] + [
|
||
"tied embeddings: the embed is not included in the LoRA (PEFT "
|
||
"cannot untie lm_head) — prefer \"full checkpoint\" for maximum "
|
||
"fidelity"
|
||
]
|
||
out = {}
|
||
target_modules = set()
|
||
for pname, (B, A) in info["lowrank"].items():
|
||
base = pname.removesuffix(".weight")
|
||
if pname == info["embed_key"]:
|
||
if not include_embed:
|
||
continue
|
||
target_modules.add(base.rsplit(".", 1)[-1])
|
||
# PEFT Embedding convention: delta_lookup = (B·A)ᵀ,
|
||
# A = lora_embedding_A [r, vocab], B = lora_embedding_B [d_model, r]
|
||
out[f"base_model.model.{base}.lora_embedding_A"] = A.contiguous()
|
||
out[f"base_model.model.{base}.lora_embedding_B"] = B.contiguous()
|
||
else:
|
||
target_modules.add(base.rsplit(".", 1)[-1])
|
||
out[f"base_model.model.{base}.lora_A.weight"] = A.contiguous()
|
||
out[f"base_model.model.{base}.lora_B.weight"] = B.contiguous()
|
||
rank = len(rules)
|
||
save_file(out, str(out_dir / "adapter_model.safetensors"))
|
||
adapter_config = {
|
||
"peft_type": "LORA",
|
||
"base_model_name_or_path": model_meta.get("model_id"),
|
||
"r": rank,
|
||
"lora_alpha": rank,
|
||
"lora_dropout": 0.0,
|
||
"target_modules": sorted(target_modules),
|
||
"bias": "none",
|
||
"fan_in_fan_out": False,
|
||
"task_type": "CAUSAL_LM",
|
||
}
|
||
(out_dir / "adapter_config.json").write_text(
|
||
json.dumps(adapter_config, indent=1), encoding="utf-8"
|
||
)
|
||
|
||
elif fmt == "full":
|
||
if source_dir is None or not Path(source_dir).is_dir():
|
||
raise ValueError("full checkpoint: model source folder not found")
|
||
source_dir = Path(source_dir)
|
||
shards = sorted(source_dir.glob("*.safetensors"))
|
||
if not shards:
|
||
raise ValueError("full checkpoint: no safetensors in the source")
|
||
|
||
lm_head_value = None # original un-embedding (if tied) = original embed from disk
|
||
embed_shard_name = None
|
||
seen = set()
|
||
for shard in shards:
|
||
ino = shard.stat().st_ino
|
||
if ino in seen:
|
||
continue
|
||
seen.add(ino)
|
||
out = {}
|
||
with safe_open(str(shard), framework="pt") as f:
|
||
keys = list(f.keys())
|
||
for key in keys:
|
||
original = f.get_tensor(key)
|
||
if info["tied"] and key == info["embed_key"]:
|
||
lm_head_value = original.clone() # BEFORE editing
|
||
embed_shard_name = shard.name
|
||
out[key] = tensors[key].to(original.dtype) if key in tensors else original
|
||
# if this shard already carries lm_head (untied model), don't touch it
|
||
save_file(out, str(out_dir / shard.name))
|
||
|
||
# untie: add lm_head.weight (= original embed) into the embed's shard
|
||
if info["tied"]:
|
||
if lm_head_value is None:
|
||
raise ValueError("cannot untie: embed not found in the source")
|
||
target_shard = out_dir / embed_shard_name
|
||
with safe_open(str(target_shard), framework="pt") as f:
|
||
merged = {k: f.get_tensor(k) for k in f.keys()}
|
||
merged[lm_head_key] = lm_head_value
|
||
save_file(merged, str(target_shard))
|
||
|
||
# config.json: copy, force tie_word_embeddings=False if untied
|
||
cfg_path = source_dir / "config.json"
|
||
if cfg_path.exists():
|
||
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
||
if info["tied"]:
|
||
cfg["tie_word_embeddings"] = False
|
||
(out_dir / "config.json").write_text(
|
||
json.dumps(cfg, ensure_ascii=False, indent=1), encoding="utf-8"
|
||
)
|
||
# other tokenizer/config files (json, merges.txt, tokenizer.model…):
|
||
# copy as-is, then fix the index if present
|
||
for pattern in ("*.json", "*.txt", "*.model", "*.tiktoken", "*.jinja"):
|
||
for extra in source_dir.glob(pattern):
|
||
if extra.name == "config.json":
|
||
continue
|
||
shutil.copy2(extra, out_dir / extra.name)
|
||
index_path = out_dir / "model.safetensors.index.json"
|
||
if info["tied"] and index_path.exists():
|
||
index = json.loads(index_path.read_text(encoding="utf-8"))
|
||
wm = index.setdefault("weight_map", {})
|
||
wm[lm_head_key] = embed_shard_name
|
||
if "metadata" in index and "total_size" in index["metadata"]:
|
||
index["metadata"]["total_size"] += lm_head_value.numel() * lm_head_value.element_size()
|
||
index_path.write_text(json.dumps(index, indent=1), encoding="utf-8")
|
||
|
||
(out_dir / "edit_meta.json").write_text(
|
||
json.dumps(meta, ensure_ascii=False, indent=1), encoding="utf-8"
|
||
)
|
||
return {"out_dir": str(out_dir), **meta}
|
||
|
||
|
||
def _disk_mapper(mem_embed_key, disk_keys):
|
||
"""Memory keys (instantiated model's layout) → disk checkpoint keys.
|
||
|
||
transformers renames on load: e.g. Qwen3.5 is instantiated as ForCausalLM
|
||
("model.layers.*" in memory) but saved in ConditionalGeneration format
|
||
("model.language_model.layers.*"). Without this mapping, a "full" export
|
||
would copy the source verbatim without transforming anything. We anchor the
|
||
disk prefix on the embed, whose suffix is unique in the checkpoint."""
|
||
if mem_embed_key in disk_keys:
|
||
return lambda key: key
|
||
suffix = "." + ".".join(mem_embed_key.rsplit(".", 2)[-2:]) # ".embed_tokens.weight"
|
||
candidates = [k for k in disk_keys if k.endswith(suffix)]
|
||
if len(candidates) != 1:
|
||
raise ValueError(
|
||
f"checkpoint prefix undecidable: {mem_embed_key} absent from the source "
|
||
f"and {len(candidates)} key(s) end with {suffix}"
|
||
)
|
||
mem_prefix = mem_embed_key.removesuffix(suffix)
|
||
disk_prefix = candidates[0].removesuffix(suffix)
|
||
|
||
def to_disk(key):
|
||
if key == mem_prefix or key.startswith(mem_prefix + "."):
|
||
return disk_prefix + key[len(mem_prefix):]
|
||
return key
|
||
|
||
return to_disk
|
||
|
||
|
||
def export_rebase(rules, jl, model_meta, *, fmt, name, source_dir=None, scale=1.0, exact=False):
|
||
"""Pure-weight export by change of basis of the reads (cf. core/rebase).
|
||
|
||
``readthrough`` (exact=False): the downstream read matrices + lm_head.
|
||
``exact``: adds the counter-transform of the downstream writes.
|
||
Formats: ``full`` (checkpoint), ``layers`` (safetensors of the modified
|
||
matrices) and ``lora`` (PEFT adapter = the exact low-rank diff between the
|
||
baked weights and the originals; the lm_head delta is applied at forward
|
||
time, so tied embeddings need no untying). The bake is done streaming, one
|
||
float32 CPU matrix at a time. Tied-embeddings model (full/layers): the embed
|
||
stays INTACT, it's lm_head (untied) that receives the final read transform."""
|
||
method = "rebase-exact" if exact else "rebase-readthrough"
|
||
if fmt not in ("full", "layers", "lora"):
|
||
raise ValueError(f"unknown format for {method}: {fmt}")
|
||
transforms, info = rebase.build_plan(rules, jl, scale, exact=exact)
|
||
lm_head_key = info["lm_head_key"]
|
||
|
||
delta_max = 0.0
|
||
applied = set()
|
||
|
||
def bake(key, tensor):
|
||
nonlocal delta_max
|
||
W = tensor.detach().to("cpu", torch.float32)
|
||
W_new, _B, _A = rebase.apply_transform(transforms[key], W)
|
||
delta_max = max(delta_max, (W_new - W).abs().max().item())
|
||
applied.add(key)
|
||
return W_new
|
||
|
||
out_dir = EDITS_DIR / name
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
dtype = torch.bfloat16 if model_meta.get("dtype") == "bf16" else torch.float16
|
||
warnings = []
|
||
if exact and info["regularized_layers"]:
|
||
warnings.append(
|
||
"regularized inverse (full zap ⇒ singular transform) on layers "
|
||
f"{info['regularized_layers']} — the effect there equals readthrough; "
|
||
"prefer readthrough mode for full removals"
|
||
)
|
||
if fmt == "lora" and info["tied"]:
|
||
warnings.append(
|
||
"tied embeddings: use the adapter at runtime (PEFT applies the "
|
||
"lm_head delta at forward time, leaving the shared embed intact); "
|
||
"merging it into the base weights (merge_and_unload) would write "
|
||
"that delta into the embed too — export a full checkpoint if you "
|
||
"need merged weights"
|
||
)
|
||
|
||
def source_weight(state, key):
|
||
source = state.get(key)
|
||
if source is None and key == lm_head_key and info["tied"]:
|
||
source = state[info["embed_key"]] # tied: the un-embedding IS the embed
|
||
if source is None:
|
||
raise ValueError(
|
||
f"parameter {key} not found in the loaded model — "
|
||
"unexpected layout, export cancelled"
|
||
)
|
||
return source
|
||
|
||
if fmt == "layers":
|
||
state = jl._hf_model.state_dict()
|
||
to_disk = lambda key: key # noqa: E731 — refined if the source is available
|
||
if source_dir is not None and Path(source_dir).is_dir():
|
||
disk_keys = set()
|
||
for shard in Path(source_dir).glob("*.safetensors"):
|
||
with safe_open(str(shard), framework="pt") as f:
|
||
disk_keys.update(f.keys())
|
||
if disk_keys:
|
||
to_disk = _disk_mapper(info["embed_key"], disk_keys)
|
||
tensors = {}
|
||
for key in transforms:
|
||
tensors[to_disk(key)] = bake(key, source_weight(state, key)).to(dtype)
|
||
save_file(tensors, str(out_dir / "modified_layers.safetensors"))
|
||
|
||
elif fmt == "lora":
|
||
# The rebase delta is low-rank by construction (delta = B·A exactly, cf.
|
||
# rebase.apply_transform): the adapter is the exact diff between the
|
||
# baked weights and the originals, not an approximation. lm_head: PEFT
|
||
# adds the delta at forward time without writing to the (possibly tied)
|
||
# weight, so the un-embedding is effectively untied while the embed
|
||
# stays intact. Module names follow the model as instantiated by
|
||
# AutoModelForCausalLM (the same loading path as the UI).
|
||
state = jl._hf_model.state_dict()
|
||
factors = {}
|
||
max_rank = 0
|
||
for key in transforms:
|
||
W = source_weight(state, key).detach().to("cpu", torch.float32)
|
||
W_new, B, A = rebase.apply_transform(transforms[key], W)
|
||
delta_max = max(delta_max, (W_new - W).abs().max().item())
|
||
applied.add(key)
|
||
factors[key] = (B, A)
|
||
max_rank = max(max_rank, B.shape[1])
|
||
tensors = {}
|
||
module_paths = []
|
||
for key, (B, A) in factors.items():
|
||
base = key.removesuffix(".weight")
|
||
module_paths.append(base)
|
||
if B.shape[1] < max_rank: # pad so a single config `r` fits every module
|
||
pad = max_rank - B.shape[1]
|
||
B = torch.cat([B, torch.zeros(B.shape[0], pad)], dim=1)
|
||
A = torch.cat([A, torch.zeros(pad, A.shape[1])], dim=0)
|
||
tensors[f"base_model.model.{base}.lora_A.weight"] = A.contiguous()
|
||
tensors[f"base_model.model.{base}.lora_B.weight"] = B.contiguous()
|
||
save_file(tensors, str(out_dir / "adapter_model.safetensors"))
|
||
# target_modules as an anchored regex over the modules actually edited:
|
||
# a plain suffix list would wrap the same projections in EVERY layer and
|
||
# leave benign but alarming "missing adapter keys" warnings at load time
|
||
target_regex = "(.*\\.)?(" + "|".join(re.escape(p) for p in sorted(module_paths)) + ")"
|
||
adapter_config = {
|
||
"peft_type": "LORA",
|
||
"base_model_name_or_path": model_meta.get("model_id"),
|
||
"r": max_rank,
|
||
"lora_alpha": max_rank, # scaling alpha/r = 1: B·A is the raw delta
|
||
"lora_dropout": 0.0,
|
||
"target_modules": target_regex,
|
||
"bias": "none",
|
||
"fan_in_fan_out": False,
|
||
"task_type": "CAUSAL_LM",
|
||
}
|
||
(out_dir / "adapter_config.json").write_text(
|
||
json.dumps(adapter_config, indent=1), encoding="utf-8"
|
||
)
|
||
|
||
elif fmt == "full":
|
||
if source_dir is None or not Path(source_dir).is_dir():
|
||
raise ValueError("full checkpoint: model source folder not found")
|
||
source_dir = Path(source_dir)
|
||
shards = sorted(source_dir.glob("*.safetensors"))
|
||
if not shards:
|
||
raise ValueError("full checkpoint: no safetensors in the source")
|
||
|
||
disk_keys = set()
|
||
seen = set()
|
||
for shard in shards:
|
||
ino = shard.stat().st_ino
|
||
if ino in seen:
|
||
continue
|
||
seen.add(ino)
|
||
with safe_open(str(shard), framework="pt") as f:
|
||
disk_keys.update(f.keys())
|
||
to_disk = _disk_mapper(info["embed_key"], disk_keys)
|
||
transforms = {to_disk(k): fn for k, fn in transforms.items()}
|
||
lm_head_key = to_disk(lm_head_key)
|
||
embed_key = to_disk(info["embed_key"])
|
||
|
||
lm_head_written = False
|
||
embed_shard_name = None
|
||
seen = set()
|
||
for shard in shards:
|
||
ino = shard.stat().st_ino
|
||
if ino in seen:
|
||
continue
|
||
seen.add(ino)
|
||
out = {}
|
||
with safe_open(str(shard), framework="pt") as f:
|
||
for key in f.keys():
|
||
original = f.get_tensor(key)
|
||
if key in transforms:
|
||
out[key] = bake(key, original).to(original.dtype)
|
||
if key == lm_head_key:
|
||
lm_head_written = True
|
||
else:
|
||
out[key] = original
|
||
if key == embed_key:
|
||
embed_shard_name = shard.name
|
||
save_file(out, str(out_dir / shard.name))
|
||
del out
|
||
|
||
# untie: the transformed un-embedding becomes a separate lm_head, baked
|
||
# from the original embed (which stays intact)
|
||
if info["tied"] and not lm_head_written:
|
||
if embed_shard_name is None:
|
||
raise ValueError("cannot untie: embed not found in the source")
|
||
target_shard = out_dir / embed_shard_name
|
||
with safe_open(str(target_shard), framework="pt") as f:
|
||
merged = {k: f.get_tensor(k) for k in f.keys()}
|
||
embed_original = merged[embed_key]
|
||
lm_head_value = bake(lm_head_key, embed_original).to(embed_original.dtype)
|
||
merged[lm_head_key] = lm_head_value
|
||
save_file(merged, str(out_dir / embed_shard_name))
|
||
del merged
|
||
|
||
missing = set(transforms) - applied
|
||
if missing:
|
||
sample = sorted(missing)[:3]
|
||
raise ValueError(
|
||
f"{len(missing)} parameter(s) to transform absent from the source "
|
||
f"checkpoint (e.g. {sample}) — unexpected key names, export cancelled "
|
||
"(the written checkpoint would be partially original)"
|
||
)
|
||
|
||
cfg_path = source_dir / "config.json"
|
||
if cfg_path.exists():
|
||
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
||
if info["tied"]:
|
||
cfg["tie_word_embeddings"] = False
|
||
text_cfg = cfg.get("text_config")
|
||
if isinstance(text_cfg, dict) and "tie_word_embeddings" in text_cfg:
|
||
text_cfg["tie_word_embeddings"] = False
|
||
(out_dir / "config.json").write_text(
|
||
json.dumps(cfg, ensure_ascii=False, indent=1), encoding="utf-8"
|
||
)
|
||
for pattern in ("*.json", "*.txt", "*.model", "*.tiktoken", "*.jinja"):
|
||
for extra in source_dir.glob(pattern):
|
||
if extra.name == "config.json":
|
||
continue
|
||
shutil.copy2(extra, out_dir / extra.name)
|
||
index_path = out_dir / "model.safetensors.index.json"
|
||
if info["tied"] and not lm_head_written and index_path.exists():
|
||
index = json.loads(index_path.read_text(encoding="utf-8"))
|
||
wm = index.setdefault("weight_map", {})
|
||
wm[lm_head_key] = embed_shard_name
|
||
if "metadata" in index and "total_size" in index["metadata"]:
|
||
index["metadata"]["total_size"] += (
|
||
lm_head_value.numel() * lm_head_value.element_size()
|
||
)
|
||
index_path.write_text(json.dumps(index, indent=1), encoding="utf-8")
|
||
|
||
if delta_max < 1e-8:
|
||
shutil.rmtree(out_dir, ignore_errors=True)
|
||
raise ValueError(
|
||
"the bake changes no weight (null directions?) — the export would be "
|
||
"identical to the original model, folder deleted"
|
||
)
|
||
|
||
summary = [
|
||
{k: r[k] for k in ("token_id", "token", "mode", "factor", "replacement_id", "replacement", "layers")}
|
||
for r in rules if r["layers"]
|
||
]
|
||
meta = {
|
||
"name": name,
|
||
"format": fmt,
|
||
"method": method,
|
||
"model_id": model_meta.get("model_id"),
|
||
"model_revision": model_meta.get("revision"),
|
||
"dtype": model_meta.get("dtype"),
|
||
"global_scale": scale,
|
||
# lora: no physical untying — the lm_head delta lives in the adapter
|
||
"untied_lm_head": info["tied"] and fmt != "lora",
|
||
"rules": summary,
|
||
"layers_span": info["layers_span"],
|
||
"rank": info["rank_final"],
|
||
"modified_params_count": len(transforms),
|
||
"delta_max": delta_max,
|
||
"min_gamma": info["min_gamma"],
|
||
"warnings": warnings,
|
||
"note": (
|
||
"change of basis of the reads: every matrix that READS the residual "
|
||
"downstream of the hooked layers (q/k/v, in_proj*, gate/up + lm_head) sees "
|
||
"the residual transformed by the same J-space directions as the live preview"
|
||
+ (" ; downstream writes counter-transformed (exact mode)" if exact else "")
|
||
+ ". Pure weights: a standard safetensors checkpoint."
|
||
),
|
||
"created_at": _now(),
|
||
}
|
||
(out_dir / "edit_meta.json").write_text(
|
||
json.dumps(meta, ensure_ascii=False, indent=1), encoding="utf-8"
|
||
)
|
||
return {"out_dir": str(out_dir), **meta}
|