Fable-Inject / hpc_fable_inject.py
CompressedGemma's picture
Upload 3 files
ddf2414 verified
Raw
History Blame Contribute Delete
8.25 kB
#!/usr/bin/env python3
"""
hpc_fable_inject.py β€” FABLE 5 β†’ HPC gate weight injection for Ornith
Extracts gate patterns from FABLE 5 training traces using HPC Pauli decomposition,
then injects them as an additive correction into Ornith-1.0-9B.
Formula (derived from Pauli decomposition I + X + c_ZΒ·Z of edge matrix):
z_i = [E[i]/Ο„ | ΞΌ_i/Ο„ | Ξ΅_i/Ο„] (3 blocks Γ— 4096 = 12288 dims)
where:
ΞΌ_i = Ξ£_j P(j|i)Β·E[j] β€” X-component: expected next embedding
Ξ΅_i = Ξ£_j c_Z(i,j)Β·E[j] β€” Z-component: Pauli-coupling-weighted sum
c_Z(i,j) = (1 - w_ij) / 2 β€” Pauli Z coefficient
w_ij = f_ij / √(f_iΒ·f_j) β€” normalized edge weight
The gate weight W is solved via ridge regression: E_nΒ·W β‰ˆ z,
then scaled to match original gate_proj weight norm (~0.012),
then added to the original weights: W_inj = W_orig + Ξ±Β·W_hpc.
Usage:
python3 hpc_fable_inject.py --data /tmp/fable5_sft.jsonl \\
--model ./Ornith-1.0-9B \\
--output ./Ornith-1.0-9B-hpc \\
--alpha 0.3 \\
--tau 0.003 \\
--topk 30000
"""
import argparse, json, math, os, shutil, sys, time
from collections import Counter, defaultdict
from pathlib import Path
import numpy as np
import torch
from safetensors.torch import safe_open, save_file
from transformers import AutoTokenizer
def load_model_shards(model_dir):
"""Return list of shard filenames and the weight index."""
index_path = Path(model_dir) / "model.safetensors.index.json"
with open(index_path) as f:
index = json.load(f)
shards = sorted(set(index["weight_map"].values()))
return shards, index
def save_model_shards(src_dir, dst_dir, shards, modify_fn):
"""Load each shard, apply modify_fn to selected tensors, save."""
dst_dir = Path(dst_dir)
dst_dir.mkdir(parents=True, exist_ok=True)
for shard_name in shards:
src_path = Path(src_dir) / shard_name
dst_path = dst_dir / shard_name
tensors = {}
with safe_open(str(src_path), framework="pt") as sf:
for k in sf.keys():
tensors[k] = sf.get_tensor(k).clone()
tensors = modify_fn(tensors)
save_file(tensors, str(dst_path))
def copy_config(src_dir, dst_dir, filenames=None):
"""Copy model config/tokenizer files."""
if filenames is None:
filenames = [
"model.safetensors.index.json",
"config.json",
"tokenizer.json",
"tokenizer_config.json",
"generation_config.json",
]
src_dir, dst_dir = Path(src_dir), Path(dst_dir)
for fn in filenames:
src = src_dir / fn
if src.exists():
shutil.copy2(src, dst_dir / fn)
def tokenize_fable(data_path, tokenizer):
"""Tokenize FABLE assistant turns. Returns flat token id list."""
all_ids = []
with open(data_path) as f:
for line in f:
d = json.loads(line)
text = d.get("text", "")
for part in text.split("<|im_start|>"):
if part.startswith("assistant\n"):
content = part[len("assistant\n"):].replace("<|im_end|>", "").strip()
if content:
all_ids.extend(tokenizer.encode(content, add_special_tokens=False))
return all_ids
def compute_bigram_stats(all_ids, vocab_size, topk):
"""Build token frequency and bigram counter for top-k tokens."""
cnt = Counter()
bigram = defaultdict(lambda: Counter())
for i, tid in enumerate(all_ids):
cnt[tid] += 1
if i > 0:
bigram[all_ids[i - 1]][tid] += 1
top_tokens = [t for t, _ in cnt.most_common(topk)]
token_to_idx = {t: i for i, t in enumerate(top_tokens)}
return cnt, bigram, top_tokens, token_to_idx
def recover_gate_weight(embed, cnt, bigram, top_tokens, token_to_idx, tau):
"""
Recover gate weight via Pauli-decomposition regression.
Returns: W_hpc as float32 numpy array (12288, 4096), unscaled.
"""
D = embed.shape[1]
V = len(top_tokens)
E_raw = np.array([embed[t] for t in top_tokens]).astype(np.float64)
mu = np.zeros_like(E_raw) # X-component: probability-weighted
eps = np.zeros_like(E_raw) # Z-component: c_Z-weighted
for i, ti in enumerate(top_tokens):
total = sum(bigram[ti].values())
fi = cnt[ti] or 1
if total == 0:
continue
for tj, c in bigram[ti].items():
if tj not in token_to_idx:
continue
fj = cnt[tj] or 1
p = c / total
w = c / math.sqrt(fi * fj)
c_Z = (1.0 - w) / 2.0
mu[i] += p * embed[tj]
eps[i] += c_Z * embed[tj]
# Build target: [E/Ο„ | ΞΌ/Ο„ | Ξ΅/Ο„]
z = np.hstack([E_raw / tau, mu / tau, eps / tau])
z = np.clip(z, -30.0, 30.0)
# Ridge regression: E_n Β· W = z
E_n = (E_raw - E_raw.mean(0, keepdims=True)) / (E_raw.std(0, keepdims=True) + 1e-10)
reg = 1e-3 * np.eye(D)
W = np.linalg.solve(E_n.T @ E_n + reg, E_n.T @ z)
return W.T.astype(np.float32) # (12288, 4096)
def main():
parser = argparse.ArgumentParser(description="FABLE β†’ HPC gate weight injector")
parser.add_argument("--data", default="/tmp/fable5_sft.jsonl", help="FABLE 5 JSONL path")
parser.add_argument("--model", default="./Ornith-1.0-9B", help="Source model directory")
parser.add_argument("--output", default="./Ornith-1.0-9B-hpc", help="Output model directory")
parser.add_argument("--alpha", type=float, default=0.3, help="Additive correction strength")
parser.add_argument("--tau", type=float, default=0.003, help="Temperature for target scaling")
parser.add_argument("--topk", type=int, default=30000, help="Top-k tokens to use")
args = parser.parse_args()
t0 = time.time()
# ── 1. Tokenizer & model info ──
print("[1/5] Loading tokenizer & embeddings...")
tokenizer = AutoTokenizer.from_pretrained(args.model, trust_remote_code=True)
shards, index = load_model_shards(args.model)
# Load embedding matrix from first shard
embed = None
with safe_open(str(Path(args.model) / shards[0]), framework="pt") as sf:
for k in sf.keys():
if "embed_tokens" in k:
embed = sf.get_tensor(k).float().numpy()
break
assert embed is not None, "Could not find embed_tokens in shards"
print(f" Embeddings: {embed.shape}, dtype={embed.dtype}")
# ── 2. Tokenize FABLE data ──
print(f"[2/5] Tokenizing {args.data}...")
all_ids = tokenize_fable(args.data, tokenizer)
print(f" {len(all_ids)} tokens")
# ── 3. Bigram statistics ──
print(f"[3/5] Computing bigrams (top-{args.topk})...")
cnt, bigram, top_tokens, token_to_idx = compute_bigram_stats(
all_ids, tokenizer.vocab_size, args.topk
)
print(f" {len(top_tokens)} tokens with {sum(len(bigram[t]) for t in top_tokens)} edges")
# ── 4. Recover gate weight ──
print(f"[4/5] Recovering gate weight (Ο„={args.tau})...")
W_hpc = recover_gate_weight(embed, cnt, bigram, top_tokens, token_to_idx, args.tau)
# Scale to match original weight norm (~0.012)
target_std = 0.012
scale = target_std / W_hpc.std()
W_hpc *= scale
print(f" W_hpc: {W_hpc.shape}, std={W_hpc.std():.6f}, "
f"range=[{W_hpc.min():.4f},{W_hpc.max():.4f}]")
# ── 5. Inject as additive correction ──
print(f"[5/5] Injecting (Ξ±={args.alpha}) β†’ {args.output}...")
W_torch = torch.from_numpy(W_hpc).to(torch.bfloat16).contiguous()
def modify_fn(tensors):
for k in list(tensors.keys()):
if "gate_proj" in k and "visual" not in k:
tensors[k] = tensors[k] + args.alpha * W_torch
return tensors
save_model_shards(args.model, args.output, shards, modify_fn)
copy_config(args.model, args.output)
elapsed = time.time() - t0
print(f"Done in {elapsed:.1f}s. Model saved to {args.output}/")
print(f"\nTo test: python3 test_recovered.py")
if __name__ == "__main__":
main()