| |
| """ |
| 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) |
| eps = np.zeros_like(E_raw) |
|
|
| 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] |
|
|
| |
| z = np.hstack([E_raw / tau, mu / tau, eps / tau]) |
| z = np.clip(z, -30.0, 30.0) |
|
|
| |
| 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) |
|
|
|
|
| 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() |
|
|
| |
| print("[1/5] Loading tokenizer & embeddings...") |
| tokenizer = AutoTokenizer.from_pretrained(args.model, trust_remote_code=True) |
| shards, index = load_model_shards(args.model) |
|
|
| |
| 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}") |
|
|
| |
| print(f"[2/5] Tokenizing {args.data}...") |
| all_ids = tokenize_fable(args.data, tokenizer) |
| print(f" {len(all_ids)} tokens") |
|
|
| |
| 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") |
|
|
| |
| print(f"[4/5] Recovering gate weight (Ο={args.tau})...") |
| W_hpc = recover_gate_weight(embed, cnt, bigram, top_tokens, token_to_idx, args.tau) |
| |
| |
| 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}]") |
|
|
| |
| 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() |
|
|