chadstthomas commited on
Commit
832785c
·
verified ·
1 Parent(s): 4aa330e

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. MODEL_CARD.md +58 -0
  2. farm_runtime.py +169 -0
  3. runtime_config.json +6 -0
MODEL_CARD.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ library_name: transformers
4
+ tags:
5
+ - modelfarm
6
+ - persona
7
+ - roleplay
8
+ - sysprompt
9
+ - interpretability
10
+ - qwen2.5-1.5b-instruct
11
+ ---
12
+
13
+ # ModelFarm · wisp-1.5b-sysprompt
14
+
15
+ **System-prompt** persona package on `Qwen/Qwen2.5-1.5B-Instruct`, produced by the Arnold Persona Lab
16
+ persona-stack harness. Persona: **Wisp the Friendly Ghost**.
17
+
18
+ ## What this is
19
+
20
+ No weights or activations are modified — a plain system prompt. Included as the reference arm: strongest identity, zero capability cost.
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ pip install torch transformers
26
+ python farm_runtime.py . --chat # or: python farm_runtime.py . "Who are you?"
27
+ python farm_runtime.py . --alpha 0.5 "Who are you?" # dose override
28
+ ```
29
+
30
+ ## Persona-stack measurements
31
+
32
+ | identity | catch/100w | register | QA | neutral NLL |
33
+ |---|---|---|---|---|
34
+ | 3 | 0.0 | 0.0 | 0.8 | 3.236 |
35
+
36
+ *Scoring: regex/lexicon judge, symmetric across arms — arm-vs-arm deltas are meaningful,
37
+ absolute numbers are indicative. QA = 5-question factual mini-suite. NLL = token loss on a
38
+ neutral corpus (higher = more capability damage).*
39
+
40
+ ## Dose & safety
41
+
42
+ - No dose. Identity quality depends entirely on the base model's knowledge of the persona.
43
+
44
+ ## Provenance & rights
45
+
46
+ - Persona corpus: quote/aphorism collections in the lab's `data/` (fan-archived public statements).
47
+ - Trained/derived by the Arnold Persona Lab harness, Qwen/Qwen2.5-1.5B-Instruct base.
48
+
49
+ **ModelFarm packages are research artifacts.** Real-person personas are likeness studies for
50
+ interpretability research, not impersonation products: no commercial use, do not represent
51
+ outputs as statements by the real person. Prefer fictional personas for public roleplay use.
52
+
53
+ ## Method note for steering packages
54
+
55
+ Steering packages do not modify weights: the base model is loaded unchanged and a
56
+ runtime hook applies the persona direction / neuron recipe at inference. Dose can be
57
+ varied live (`--alpha` / `--dose`), and the base model stays swappable for equal-size
58
+ checkpoints of the same family.
farm_runtime.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """ModelFarm runtime — chat with any persona package produced by persona_lab.
3
+
4
+ A package is a directory (exported by `persona_lab.py export`) containing:
5
+ runtime_config.json method + base model + steering parameters
6
+ persona_vector.pt (engram arm) or neuron_recipe.json (neurons arm)
7
+ adapter/ (lora arm) or nothing for pure-prompt packages
8
+
9
+ Usage:
10
+ python farm_runtime.py <package_dir> --chat
11
+ python farm_runtime.py <package_dir> "Say something in character."
12
+ python farm_runtime.py <package_dir> --alpha 0.5 "Who are you?" # dose override
13
+
14
+ Requires: torch, transformers (+ peft only for lora packages).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import re
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ import torch
26
+ from transformers import AutoModelForCausalLM, AutoTokenizer
27
+
28
+ DECODER_LAYER_RE = re.compile(r"^(?:.*\.)?layers\.(\d+)$")
29
+
30
+
31
+ def locate_layers(model):
32
+ found = {}
33
+ for name, module in model.named_modules():
34
+ match = DECODER_LAYER_RE.fullmatch(name)
35
+ if not match:
36
+ continue
37
+ try:
38
+ module.get_submodule("mlp.down_proj")
39
+ except AttributeError:
40
+ continue
41
+ idx = int(match.group(1))
42
+ if idx not in found or len(name) < found[idx]._farm_path: # type: ignore[attr-defined]
43
+ module._farm_path = name # type: ignore[attr-defined]
44
+ found[idx] = module
45
+ if not found:
46
+ raise RuntimeError("No decoder layers with mlp.down_proj found.")
47
+ return found
48
+
49
+
50
+ def install_engram(model, layers, config, package: Path, alpha: float):
51
+ blob = torch.load(package / config["vector_file"], weights_only=False)
52
+ layer = blob["chosen"]["layer"]
53
+ vec = blob["vectors"][str(layer)].float()
54
+ unit = vec / vec.norm()
55
+ median = blob["medians"][str(layer)]
56
+ vector = (unit * (alpha * median)).to(next(model.parameters()).device, torch.bfloat16)
57
+
58
+ def hook(_mod, _args, output):
59
+ if isinstance(output, tuple):
60
+ return (output[0] + vector,) + tuple(output[1:])
61
+ return output + vector
62
+
63
+ handle = layers[layer].register_forward_hook(hook)
64
+ print(f"[modelfarm] engram: layer {layer}, |v·alpha·median| = "
65
+ f"{vector.norm():.2f} (alpha={alpha})")
66
+ return handle
67
+
68
+
69
+ def install_neurons(model, layers, config, package: Path, dose: float):
70
+ recipe = json.loads((package / config["recipe_file"]).read_text())
71
+ handles = []
72
+ by_layer = {}
73
+ for entry in recipe["recipe"]:
74
+ by_layer.setdefault(entry["layer"], []).append({**entry, "delta": entry["delta"] * dose})
75
+ for layer, entries in by_layer.items():
76
+ down_proj = layers[layer].get_submodule("mlp.down_proj")
77
+
78
+ def make_hook(items):
79
+ def hook(_mod, args):
80
+ hidden = args[0].clone()
81
+ for item in items:
82
+ hidden[..., item["neuron"]] = hidden[..., item["neuron"]] + item["delta"]
83
+ return (hidden,)
84
+
85
+ return hook
86
+
87
+ handles.append(down_proj.register_forward_pre_hook(make_hook(entries)))
88
+ print(f"[modelfarm] neurons: {len(recipe['recipe'])} neurons, dose x{dose}")
89
+ return handles
90
+
91
+
92
+ def install_lora(model, package: Path):
93
+ from peft import PeftModel
94
+
95
+ model = PeftModel.from_pretrained(model, str(package / "adapter"))
96
+ print("[modelfarm] lora adapter loaded")
97
+ return model
98
+
99
+
100
+ @torch.no_grad()
101
+ def generate(model, tokenizer, messages, device, max_new_tokens=160):
102
+ try:
103
+ inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True,
104
+ return_tensors="pt", return_dict=True,
105
+ enable_thinking=False).to(device)
106
+ except TypeError:
107
+ inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True,
108
+ return_tensors="pt", return_dict=True).to(device)
109
+ out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False,
110
+ pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id)
111
+ return tokenizer.decode(out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
112
+
113
+
114
+ def main() -> None:
115
+ parser = argparse.ArgumentParser(description=__doc__)
116
+ parser.add_argument("package", type=Path)
117
+ parser.add_argument("prompt", nargs="?", default=None)
118
+ parser.add_argument("--chat", action="store_true")
119
+ parser.add_argument("--alpha", type=float, default=None, help="engram dose override")
120
+ parser.add_argument("--dose", type=float, default=None, help="neuron dose override")
121
+ parser.add_argument("--max-new-tokens", type=int, default=160)
122
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
123
+ args = parser.parse_args()
124
+
125
+ config = json.loads((args.package / "runtime_config.json").read_text())
126
+ tokenizer = AutoTokenizer.from_pretrained(config["base_model"])
127
+ dtype = torch.bfloat16 if args.device != "cpu" else torch.float32
128
+ model = AutoModelForCausalLM.from_pretrained(config["base_model"], dtype=dtype).to(args.device)
129
+ model.eval()
130
+ layers = locate_layers(model)
131
+
132
+ handles = []
133
+ method = config["method"]
134
+ if method == "engram":
135
+ handles.append(install_engram(model, layers, config, args.package,
136
+ args.alpha if args.alpha is not None else config.get("alpha", 1.0)))
137
+ elif method == "neurons":
138
+ handles.extend(install_neurons(model, layers, config, args.package,
139
+ args.dose if args.dose is not None else config.get("dose", 1.0)))
140
+ elif method == "lora":
141
+ model = install_lora(model, args.package)
142
+ elif method in ("sysprompt", "prompt"):
143
+ pass # plain system-prompt package, nothing to install
144
+ else:
145
+ raise SystemExit(f"unknown method {method}")
146
+ if method in ("sysprompt", "prompt"):
147
+ print("[modelfarm] plain system-prompt package")
148
+
149
+ system = config.get("system_prompt") or "You are a helpful assistant."
150
+ if args.chat:
151
+ print(f"chatting with {args.package.name} ({method}); Ctrl+C to exit\n")
152
+ try:
153
+ while True:
154
+ user = input("you: ").strip()
155
+ if user:
156
+ print(f"{args.package.name}: {generate(model, tokenizer, [{'role': 'system', 'content': system}, {'role': 'user', 'content': user}], args.device, args.max_new_tokens)}\n")
157
+ except (KeyboardInterrupt, EOFError):
158
+ print("\nbye")
159
+ else:
160
+ if not args.prompt:
161
+ raise SystemExit("give a prompt or --chat")
162
+ messages = [{"role": "system", "content": system}, {"role": "user", "content": args.prompt}]
163
+ print(generate(model, tokenizer, messages, args.device, args.max_new_tokens))
164
+ for handle in handles:
165
+ handle.remove()
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
runtime_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "method": "sysprompt",
3
+ "base_model": "Qwen/Qwen2.5-1.5B-Instruct",
4
+ "persona": "Wisp the Friendly Ghost",
5
+ "system_prompt": "You are Wisp the Friendly Ghost."
6
+ }