Cleanup init commit
This commit is contained in:
+344
@@ -0,0 +1,344 @@
|
||||
# CLI client for the J-Wash server (port 8381): drive the model, lens,
|
||||
# intervention rules, generation and export without going through the UI.
|
||||
#
|
||||
# python -X utf8 scripts/jlab.py status
|
||||
# ... load Qwen/Qwen3.5-4B --device cuda:0
|
||||
# ... lens --repo neuronpedia/jacobian-lens --file <file> --layers all
|
||||
# ... rule-add " assistant" --mode replace --repl " fish" --layers 19-31
|
||||
# ... mode readthrough ; ... scale 1.5
|
||||
# ... gen "Who are you?" --temp 0
|
||||
# ... probe (identity/control battery + fish score)
|
||||
# ... export fish_v1 --format full
|
||||
# ... unload
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
BASE = "http://127.0.0.1:8381"
|
||||
|
||||
|
||||
def call(method, path, body=None, timeout=1800):
|
||||
url = BASE + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
if data is not None:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")
|
||||
try:
|
||||
detail = json.loads(detail).get("detail", detail)
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
sys.exit(f"HTTP {exc.code} {path}: {detail}")
|
||||
except urllib.error.URLError as exc:
|
||||
sys.exit(f"server unreachable ({BASE}): {exc.reason} — start the server (run.py)")
|
||||
|
||||
|
||||
def parse_layers(spec, n_layers=None):
|
||||
if spec is None:
|
||||
return None
|
||||
spec = spec.strip().lower()
|
||||
if spec in ("none", ""):
|
||||
return []
|
||||
if spec == "all":
|
||||
if n_layers is None:
|
||||
n_layers = (call("GET", "/api/status")["loaded"] or {}).get("n_layers")
|
||||
if n_layers is None:
|
||||
sys.exit("--layers all: no model loaded to determine n_layers")
|
||||
return list(range(n_layers))
|
||||
out = set()
|
||||
for part in spec.split(","):
|
||||
if "-" in part:
|
||||
lo, hi = part.split("-")
|
||||
out.update(range(int(lo), int(hi) + 1))
|
||||
else:
|
||||
out.add(int(part))
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def resolve_token(text):
|
||||
"""EXACT single token for ``text`` (leading space is significant)."""
|
||||
r = call("GET", "/api/token-lookup?q=" + urllib.parse.quote(text.strip()))
|
||||
for c in r["candidates"]:
|
||||
if c["str"] == text:
|
||||
return c
|
||||
listing = ", ".join(f"{c['id']}:{c['str']!r}" for c in r["candidates"]) or "none"
|
||||
sys.exit(f"exact token {text!r} not found — candidates: {listing}")
|
||||
|
||||
|
||||
def show(obj):
|
||||
print(json.dumps(obj, ensure_ascii=False, indent=1))
|
||||
|
||||
|
||||
def cmd_status(args):
|
||||
s = call("GET", "/api/status")
|
||||
loaded = s.get("loaded") or {}
|
||||
lens = s.get("lens") or {}
|
||||
print(f"model : {loaded.get('model_id', '—')} ({loaded.get('device', '')}, "
|
||||
f"{loaded.get('dtype', '')}, {loaded.get('n_layers', '?')} layers)")
|
||||
print(f"lens : {lens.get('repo_id') or lens.get('path') or '—'} "
|
||||
f"layers={lens.get('layers', '—')} k={lens.get('k', '—')}")
|
||||
print(f"busy : {s.get('busy') or '—'} interventions mode: {s.get('interventions_mode')}"
|
||||
f" scale: {s.get('interventions_scale')}")
|
||||
for gpu in s.get("gpus", []):
|
||||
print(f"gpu : {gpu}")
|
||||
for r in s.get("interventions", []):
|
||||
repl = f" → «{r['replacement']}»" if r.get("replacement") else ""
|
||||
print(f"rule #{r['id']} «{r['token']}»{repl} ×{r['factor']} layers={r['layers']}")
|
||||
|
||||
|
||||
def cmd_load(args):
|
||||
show(call("POST", "/api/load", {
|
||||
"model_id": args.model_id, "dtype": args.dtype,
|
||||
"quant": None, "device": args.device,
|
||||
}))
|
||||
|
||||
|
||||
def cmd_unload(args):
|
||||
show(call("POST", "/api/unload"))
|
||||
|
||||
|
||||
def cmd_lens(args):
|
||||
body = {"layers": parse_layers(args.layers)}
|
||||
if args.k is not None:
|
||||
body["k"] = args.k
|
||||
if args.path:
|
||||
body["path"] = args.path
|
||||
else:
|
||||
body["repo_id"] = args.repo
|
||||
if args.file:
|
||||
body["filename"] = args.file
|
||||
if args.revision:
|
||||
body["revision"] = args.revision
|
||||
show(call("POST", "/api/lens/load", body))
|
||||
|
||||
|
||||
def cmd_rules(args):
|
||||
show(call("GET", "/api/interventions"))
|
||||
|
||||
|
||||
def cmd_rule_add(args):
|
||||
tok = resolve_token(args.token)
|
||||
body = {"token_id": tok["id"], "mode": args.mode, "factor": args.factor}
|
||||
if args.mode == "replace":
|
||||
if not args.repl:
|
||||
sys.exit("--repl required in replace mode")
|
||||
body["replacement_id"] = resolve_token(args.repl)["id"]
|
||||
layers = parse_layers(args.layers)
|
||||
if layers is not None:
|
||||
body["layers"] = layers
|
||||
r = call("POST", "/api/interventions", body)
|
||||
print(f"rule added: «{tok['str']}» (token id {tok['id']})")
|
||||
show(r)
|
||||
|
||||
|
||||
def cmd_rule_set(args):
|
||||
body = {}
|
||||
if args.factor is not None:
|
||||
body["factor"] = args.factor
|
||||
layers = parse_layers(args.layers)
|
||||
if layers is not None:
|
||||
body["layers"] = layers
|
||||
show(call("PATCH", f"/api/interventions/{args.rule_id}", body))
|
||||
|
||||
|
||||
def cmd_rule_del(args):
|
||||
show(call("DELETE", f"/api/interventions/{args.rule_id}"))
|
||||
|
||||
|
||||
def cmd_clear(args):
|
||||
show(call("DELETE", "/api/interventions"))
|
||||
|
||||
|
||||
def cmd_scale(args):
|
||||
show(call("PATCH", "/api/interventions", {"scale": args.value}))
|
||||
|
||||
|
||||
def cmd_mode(args):
|
||||
show(call("PATCH", "/api/interventions", {"mode": args.value}))
|
||||
|
||||
|
||||
def _generate(prompt, system=None, temp=0.0, max_tokens=200, seed=1234):
|
||||
messages = ([{"role": "system", "content": system}] if system else [])
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
r = call("POST", "/api/generate", {
|
||||
"messages": messages,
|
||||
"sampling": {"temperature": temp, "max_tokens": max_tokens, "seed": seed},
|
||||
})
|
||||
return r["text"]
|
||||
|
||||
|
||||
def cmd_gen(args):
|
||||
for i in range(args.n):
|
||||
text = _generate(args.prompt, args.system, args.temp, args.max, seed=args.seed + i)
|
||||
print(f"--- [{i + 1}/{args.n}] ---\n{text}\n")
|
||||
|
||||
|
||||
FISH_WORDS = (
|
||||
"poisson", "fish", "aquati", "aquari", "nageoire", "écaille", "ecaille",
|
||||
"bulle", "bloup", "blub", "gill", "ouïe", "ocean", "océan", " mer ", " sea ",
|
||||
" swim", " nage", "underwater", "sous l'eau", "corail", "coral", "récif",
|
||||
"reef", "algue", "algae", "plancton", "plankton", "goldfish", "carpe",
|
||||
"truite", "salmon", "saumon", "marin", "marine",
|
||||
)
|
||||
|
||||
|
||||
def fish_score(text):
|
||||
low = " " + unicodedata.normalize("NFKC", text).lower() + " "
|
||||
hits = sorted({w.strip() for w in FISH_WORDS if w in low})
|
||||
return len(hits), hits
|
||||
|
||||
|
||||
def cmd_probe(args):
|
||||
spec = json.loads(Path(args.prompts).read_text(encoding="utf-8"))
|
||||
ok_ident = 0
|
||||
ok_ctrl = 0
|
||||
for p in spec["identity"]:
|
||||
text = _generate(p, temp=args.temp, max_tokens=args.max)
|
||||
n, hits = fish_score(text)
|
||||
ok_ident += bool(n)
|
||||
flat = " ".join(text.split())
|
||||
print(f"\n🐟={n:<2} {p}\n {flat[:400]}")
|
||||
if hits:
|
||||
print(f" words: {', '.join(hits)}")
|
||||
for p in spec["control"]:
|
||||
text = _generate(p["prompt"], temp=args.temp, max_tokens=args.max)
|
||||
good = any(a.lower() in text.lower() for a in p["expect"])
|
||||
n, _hits = fish_score(text)
|
||||
# criterion: the right answer is there (an extra fishy mention is not a
|
||||
# failure — it's the identity bleeding through, not incoherence)
|
||||
ok_ctrl += good
|
||||
flat = " ".join(text.split())
|
||||
mark = "✓" if good else "✗"
|
||||
fishy = f" 🐟{n}" if n else ""
|
||||
print(f"\n{mark}{fishy} {p['prompt']}\n {flat[:300]}")
|
||||
print(f"\n=== fish identity: {ok_ident}/{len(spec['identity'])} — "
|
||||
f"clean controls: {ok_ctrl}/{len(spec['control'])} ===")
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
started = time.perf_counter()
|
||||
r = call("POST", "/api/edit/export", {"format": args.format, "name": args.name})
|
||||
r["seconds"] = round(time.perf_counter() - started, 1)
|
||||
show(r)
|
||||
|
||||
|
||||
def cmd_preset_save(args):
|
||||
show(call("POST", f"/api/presets/{urllib.parse.quote(args.name)}"))
|
||||
|
||||
|
||||
def cmd_preset_apply(args):
|
||||
show(call("POST", f"/api/presets/{urllib.parse.quote(args.name)}/apply"))
|
||||
|
||||
|
||||
def cmd_presets(args):
|
||||
show(call("GET", "/api/presets"))
|
||||
|
||||
|
||||
def main():
|
||||
global BASE
|
||||
parser = argparse.ArgumentParser(description="CLI client for the J-Wash server")
|
||||
parser.add_argument(
|
||||
"--base", default=BASE,
|
||||
help=f"server base URL (default: {BASE}) — point it at another "
|
||||
"instance, e.g. http://127.0.0.1:8382",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
sub.add_parser("status").set_defaults(fn=cmd_status)
|
||||
|
||||
p = sub.add_parser("load")
|
||||
p.add_argument("model_id")
|
||||
p.add_argument("--device", default="cuda:0")
|
||||
p.add_argument("--dtype", default="bf16")
|
||||
p.set_defaults(fn=cmd_load)
|
||||
|
||||
sub.add_parser("unload").set_defaults(fn=cmd_unload)
|
||||
|
||||
p = sub.add_parser("lens")
|
||||
p.add_argument("--repo", default="neuronpedia/jacobian-lens")
|
||||
p.add_argument("--file", default=None)
|
||||
p.add_argument("--revision", default=None)
|
||||
p.add_argument("--path", default=None)
|
||||
p.add_argument("--layers", default=None, help="e.g. 0-30, all, none")
|
||||
p.add_argument("--k", type=int, default=None)
|
||||
p.set_defaults(fn=cmd_lens)
|
||||
|
||||
sub.add_parser("rules").set_defaults(fn=cmd_rules)
|
||||
|
||||
p = sub.add_parser("rule-add")
|
||||
p.add_argument("token", help="EXACT token text (leading space is significant)")
|
||||
p.add_argument("--mode", default="scale", choices=["scale", "replace"])
|
||||
p.add_argument("--repl", default=None)
|
||||
p.add_argument("--factor", type=float, default=None)
|
||||
p.add_argument("--layers", default=None)
|
||||
p.set_defaults(fn=cmd_rule_add, factor_default=True)
|
||||
|
||||
p = sub.add_parser("rule-set")
|
||||
p.add_argument("rule_id", type=int)
|
||||
p.add_argument("--factor", type=float, default=None)
|
||||
p.add_argument("--layers", default=None)
|
||||
p.set_defaults(fn=cmd_rule_set)
|
||||
|
||||
p = sub.add_parser("rule-del")
|
||||
p.add_argument("rule_id", type=int)
|
||||
p.set_defaults(fn=cmd_rule_del)
|
||||
|
||||
sub.add_parser("clear").set_defaults(fn=cmd_clear)
|
||||
|
||||
p = sub.add_parser("scale")
|
||||
p.add_argument("value", type=float)
|
||||
p.set_defaults(fn=cmd_scale)
|
||||
|
||||
p = sub.add_parser("mode")
|
||||
p.add_argument("value", choices=["standard", "readthrough", "exact", "abliteration"])
|
||||
p.set_defaults(fn=cmd_mode)
|
||||
|
||||
p = sub.add_parser("gen")
|
||||
p.add_argument("prompt")
|
||||
p.add_argument("--system", default=None)
|
||||
p.add_argument("--temp", type=float, default=0.0)
|
||||
p.add_argument("--max", type=int, default=200)
|
||||
p.add_argument("--seed", type=int, default=1234)
|
||||
p.add_argument("-n", type=int, default=1)
|
||||
p.set_defaults(fn=cmd_gen)
|
||||
|
||||
p = sub.add_parser("probe")
|
||||
p.add_argument("--prompts", default=str(Path(__file__).with_name("fish_prompts.json")))
|
||||
p.add_argument("--temp", type=float, default=0.0)
|
||||
p.add_argument("--max", type=int, default=200)
|
||||
p.set_defaults(fn=cmd_probe)
|
||||
|
||||
p = sub.add_parser("export")
|
||||
p.add_argument("name")
|
||||
p.add_argument("--format", default="full")
|
||||
p.set_defaults(fn=cmd_export)
|
||||
|
||||
p = sub.add_parser("preset-save")
|
||||
p.add_argument("name")
|
||||
p.set_defaults(fn=cmd_preset_save)
|
||||
|
||||
p = sub.add_parser("preset-apply")
|
||||
p.add_argument("name")
|
||||
p.set_defaults(fn=cmd_preset_apply)
|
||||
|
||||
sub.add_parser("presets").set_defaults(fn=cmd_presets)
|
||||
|
||||
args = parser.parse_args()
|
||||
BASE = args.base.rstrip("/")
|
||||
if getattr(args, "factor_default", False) and args.factor is None:
|
||||
args.factor = 1.0 if args.mode == "replace" else 0.0
|
||||
args.fn(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user