AQIT 0.1.0
Loading...
Searching...
No Matches
sae_stats_cli.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""aquin sae-stats — multi-layer SAE statistics export."""
3
4import sys
5
6from aquin.cli_flags import reject_legacy_output_flags
7from pathlib import Path
8from typing import Any
9
10
11def _parse_flag(args: list[str], name: str) -> str | None:
12 for i, a in enumerate(args):
13 if a == name and i + 1 < len(args):
14 return args[i + 1]
15 return None
16
17
18def _has_flag(args: list[str], name: str) -> bool:
19 return name in args
20
21
22def _parse_int_flag(args: list[str], name: str, default: int) -> int:
23 raw = _parse_flag(args, name)
24 if raw is None:
25 return default
26 try:
27 return max(1, int(raw))
28 except ValueError:
29 print(f"Error: {name} must be an integer")
30 sys.exit(1)
31
32
33def _parse_topk_flag(args: list[str], default: int) -> int:
34 for name in ("--topk", "--top-k", "--top_k"):
35 raw = _parse_flag(args, name)
36 if raw is not None:
37 try:
38 return max(1, int(raw))
39 except ValueError:
40 print(f"Error: {name} must be an integer")
41 sys.exit(1)
42 return default
43
44
45def _ensure_compute_env() -> None:
46 from aquin.compute.loader_shim import apply as _shim_apply
47 from aquin.engine.local_server import start as _start_local_server
48
49 _shim_apply()
50 _start_local_server()
51
52
53def _require_loaded_model_id() -> str:
54 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
55
56 active = (get_active_model_id() or "").strip()
57 if not active:
58 print("Error: no model loaded. Run: aquin load --model <id>")
59 sys.exit(1)
60 try:
61 return resolve_model_id(active)
62 except ValueError as e:
63 print(f"Error: {e}")
64 sys.exit(1)
65
66
67def _print_help() -> None:
68 print("Export multi-layer SAE statistics over a probe dataset (LLM).")
69 print("")
70 print("Prerequisite: aquin load --model <id>")
71 print(" aquin load sae <model-l{n}>")
72 print("")
73 print("Usage: aquin sae-stats --prompts <json|jsonl>")
74 print(" [--layers <all|9|0,9,15>]")
75 print(" [--topk <n>]")
76 print(" [--save <path>]")
77 print(" [--check] [--umap]")
78 print("")
79 print(" --prompts Probe file. Each row: text (or prompt) + optional id, stressor, lang, quant_run_id.")
80 print(" --layers all (default) or comma-separated layer indices with SAE checkpoints on disk.")
81 print(" --topk Top firing features per layer (default: 10).")
82 print(" --save Write schema_version=1 JSON export to this path.")
83 print(" --check Save sae-stats-check.json and sae-stats-check.png in the current directory.")
84 print(" --umap Load SAE UMAP projection after the export (web explorer).")
85 print("")
86 print("Example:")
87 print(" aquin sae-stats --prompts probes.jsonl --layers 8 --topk 10 --check")
88 print(" aquin sae-stats --prompts probes.jsonl --layers 8 --umap")
89 print("")
90 print("Docs: https://aquin.app/docs/inspection-sae")
91
92
93def cmd_sae_stats(args: list[str]) -> None:
94 if _has_flag(args, "--help") or _has_flag(args, "-h"):
96 return
98 reject_legacy_output_flags(args)
99
100 prompts = _parse_flag(args, "--prompts")
101 if not prompts:
102 print("Error: --prompts is required.")
103 print("")
105 sys.exit(1)
106
107 from aquin.compute.activation_capture import resolve_prompts_path
108
109 resolved = resolve_prompts_path(Path(prompts).expanduser())
110 if resolved is None:
111 print(f"Error: probe file not found: {prompts}")
112 print(" --prompts must be a path to a .json or .jsonl file (inline JSON is not supported).")
113 print("")
114 print("Quick fix:")
115 print(' printf \'%s\\n\' \'{"id":"p1","text":"The cat sat on the mat"}\' \\')
116 print(' \'{"id":"p2","text":"Paris is the capital of France"}\' > /tmp/probes.jsonl')
117 print(" aquin sae-stats --prompts /tmp/probes.jsonl --layers 11")
118 sys.exit(1)
119 prompts = str(resolved)
120
121 layers = _parse_flag(args, "--layers")
122 top_k = _parse_topk_flag(args, 10)
123 save_path = _parse_flag(args, "--save")
124 do_check = _has_flag(args, "--check")
125 want_umap = _has_flag(args, "--umap")
126
127 if _parse_flag(args, "--model") is not None:
128 print("Error: sae-stats uses the loaded session model only.")
129 print(" Run: aquin load --model <id>")
130 sys.exit(1)
131
133
134 from aquin.cli import _build_tool_ctx
135 from aquin.engine.sync_dispatch import dispatch_with_sync, require_active_session
136
138 ctx = _build_tool_ctx(model_id=mid)
139 require_active_session(ctx, label="aquin sae-stats")
140
141 tool_args: dict[str, Any] = {
142 "model_id": mid,
143 "prompts": prompts,
144 "top_k": top_k,
145 }
146 if layers:
147 tool_args["layers"] = layers
148 if save_path:
149 tool_args["save"] = save_path
150
151 try:
152 print(f"[sae-stats] model={mid} probes={prompts} layers={layers or 'all'} top-k={top_k}")
153 result = dispatch_with_sync("run_sae_stats", tool_args, ctx)
154 except Exception as e:
155 print(f"Error: {e}")
156 sys.exit(1)
157
158 from aquin.cli_output import print_tool_result
159
160 print_tool_result("sae-stats", result)
161
162 if isinstance(result, dict) and result.get("error"):
163 sys.exit(1)
164
165 if do_check:
166 if isinstance(result, dict) and result.get("error"):
167 print(f"[sae-stats --check] skipped save: {result['error']}", file=sys.stderr)
168 else:
169 import os
170 from aquin.sae_stats_check import write_sae_stats_check
171
172 try:
173 json_path, png_path = write_sae_stats_check(
174 result, tool_name="run_sae_stats", cwd=os.getcwd(),
175 )
176 print(f"Saved {json_path}")
177 print(f"Saved {png_path}")
178 except Exception as exc:
179 print(f"[sae-stats --check] failed to save files: {exc}", file=sys.stderr)
180 import traceback
181 traceback.print_exc()
182 sys.exit(1)
183
184 if want_umap:
185 from aquin.cli import _run_umap_followup
186
187 _run_umap_followup(
188 ctx,
189 result=result,
190 tool_args=tool_args,
191 ensure_model=mid,
192 )
int _parse_topk_flag(list[str] args, int default)
bool _has_flag(list[str] args, str name)
None cmd_sae_stats(list[str] args)
str|None _parse_flag(list[str] args, str name)
int _parse_int_flag(list[str] args, str name, int default)