#!/usr/bin/env python3 """Build a DS4 directional-steering vector from paired prompt sets. The extractor asks ds4 to dump one 4096-wide activation row per layer, averages the target and control rows, and writes a flat f32 file with 43 layer vectors. At runtime ds4 applies: y = y - scale * direction[layer] * dot(direction[layer], y) Positive scale suppresses the target direction. Negative scale amplifies it. """ import argparse import array import json import math import os import subprocess import tempfile from pathlib import Path N_LAYER = 43 N_EMBD = 4096 SPECIALS = { "bos": "<|begin▁of▁sentence|>", "user": "<|User|>", "assistant": "<|Assistant|>", "think": "", "nothink": "", } def read_prompt_file(path: Path) -> list[str]: """Read one prompt per non-empty line, ignoring shell-style comments.""" prompts: list[str] = [] for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#"): continue prompts.append(line) if not prompts: raise SystemExit(f"{path}: no prompts found") return prompts def render_ds4_prompt(system: str, user: str, think: bool) -> str: """Render the minimal DS4 chat prefix used for activation capture.""" pieces = [SPECIALS["bos"]] if system: pieces.append(system) pieces += [ SPECIALS["user"], user, SPECIALS["assistant"], SPECIALS["think"] if think else SPECIALS["nothink"], ] return "".join(pieces) def normalize(v: list[float]) -> list[float]: n2 = sum(x * x for x in v) if n2 <= 0.0: return v inv = 1.0 / math.sqrt(n2) return [x * inv for x in v] def dot(a: list[float], b: list[float]) -> float: return sum(x * y for x, y in zip(a, b)) def run_capture( ds4: Path, model: Path, prompt: str, system: str, think: bool, ctx: int, component: str, work: Path, ) -> list[list[float]]: """Run ds4 once and return the last prompt-row dump for every layer.""" prompt_path = work / "prompt.txt" prompt_path.write_text(render_ds4_prompt(system, prompt, think), encoding="utf-8") dump_prefix = work / "dump" env = os.environ.copy() env["DS4_METAL_GRAPH_DUMP_PREFIX"] = str(dump_prefix) env["DS4_METAL_GRAPH_DUMP_NAME"] = component env["DS4_METAL_GRAPH_DUMP_POS"] = "0" cmd = [ str(ds4), "-m", str(model), "--ctx", str(ctx), "--prompt-file", str(prompt_path), "-n", "1", ] subprocess.run(cmd, cwd=ds4.parent, env=env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) rows: list[list[float]] = [] for layer in range(N_LAYER): path = work / f"dump_{component}-{layer}_pos0.bin" data = array.array("f") with path.open("rb") as f: data.fromfile(f, path.stat().st_size // 4) if len(data) < N_EMBD or len(data) % N_EMBD != 0: raise RuntimeError(f"bad dump shape for {path}: {len(data)} floats") rows.append(list(data[-N_EMBD:])) return rows def add_rows(total: list[list[float]], rows: list[list[float]]) -> None: for layer in range(N_LAYER): dst = total[layer] src = rows[layer] for i, value in enumerate(src): dst[i] += value def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--ds4", default="./ds4", help="path to the ds4 CLI") ap.add_argument("--model", default="ds4flash.gguf", help="GGUF model path") ap.add_argument("--good-file", required=True, help="desired/target prompts, one per line") ap.add_argument("--bad-file", required=True, help="contrast/control prompts, one per line") ap.add_argument("--out", default="dir-steering/out/direction.json", help="metadata JSON path; .f32 is written next to it") ap.add_argument("--ctx", type=int, default=512) ap.add_argument("--system", default="You are a helpful assistant.") ap.add_argument("--component", default="ffn_out", choices=("ffn_out", "attn_out"), help="runtime-editable 4096-wide activation stream") ap.add_argument("--think", action="store_true", help="capture after ; default captures direct answers") ap.add_argument("--pair-normalize", action="store_true", help="average normalized per-pair differences") ap.add_argument("--no-orthogonalize", action="store_true", help="do not remove the component parallel to the control mean") args = ap.parse_args() ds4 = Path(args.ds4).resolve() model = Path(args.model).resolve() good_prompts = read_prompt_file(Path(args.good_file)) bad_prompts = read_prompt_file(Path(args.bad_file)) n = min(len(good_prompts), len(bad_prompts)) good_prompts = good_prompts[:n] bad_prompts = bad_prompts[:n] good_sum = [[0.0] * N_EMBD for _ in range(N_LAYER)] bad_sum = [[0.0] * N_EMBD for _ in range(N_LAYER)] pair_sum = [[0.0] * N_EMBD for _ in range(N_LAYER)] with tempfile.TemporaryDirectory(prefix="ds4-dir-steer-") as td: root = Path(td) for i, (good, bad) in enumerate(zip(good_prompts, bad_prompts), 1): print(f"pair {i}/{n}", flush=True) gw = root / f"good-{i}" bw = root / f"bad-{i}" gw.mkdir() bw.mkdir() good_rows = run_capture(ds4, model, good, args.system, args.think, args.ctx, args.component, gw) bad_rows = run_capture(ds4, model, bad, args.system, args.think, args.ctx, args.component, bw) add_rows(good_sum, good_rows) add_rows(bad_sum, bad_rows) if args.pair_normalize: for layer in range(N_LAYER): diff = normalize([ good_rows[layer][j] - bad_rows[layer][j] for j in range(N_EMBD) ]) for j, value in enumerate(diff): pair_sum[layer][j] += value layers = [] for layer in range(N_LAYER): good_mean = [x / n for x in good_sum[layer]] bad_mean = [x / n for x in bad_sum[layer]] if args.pair_normalize: direction = normalize([x / n for x in pair_sum[layer]]) else: direction = normalize([ good_mean[i] - bad_mean[i] for i in range(N_EMBD) ]) if not args.no_orthogonalize: base = normalize(bad_mean) projection = dot(direction, base) direction = normalize([ direction[i] - projection * base[i] for i in range(N_EMBD) ]) layers.append(direction) out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) payload = { "format": "ds4-directional-steering-v1", "shape": [N_LAYER, N_EMBD], "component": args.component, "thinking": bool(args.think), "pair_normalize": bool(args.pair_normalize), "orthogonalize_control_mean": not args.no_orthogonalize, "good_file": str(Path(args.good_file)), "bad_file": str(Path(args.bad_file)), "model": str(model), "note": "runtime positive scale suppresses this direction; negative scale amplifies it", } out.write_text(json.dumps(payload, indent=2), encoding="utf-8") flat = array.array("f") for direction in layers: flat.extend(direction) f32_out = out.with_suffix(".f32") with f32_out.open("wb") as f: flat.tofile(f) print(f"wrote {out}") print(f"wrote {f32_out}") if __name__ == "__main__": main()