AQIT 0.1.0
Loading...
Searching...
No Matches
publish_public_sae.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Upload SAE + norm + UMAP to R2 and register in Supabase public_saes (no Node required)."""
3
4from __future__ import annotations
5
6import json
7import os
8import sys
9from pathlib import Path
10from typing import Any
11
12
13def _load_env_files() -> None:
14 roots = [
15 Path.cwd(),
16 Path.cwd().parent,
17 Path(__file__).resolve().parents[2],
18 ]
19 seen: set[Path] = set()
20 for root in roots:
21 for name in (".env.local", ".env"):
22 path = (root / name).resolve()
23 if path in seen or not path.is_file():
24 continue
25 seen.add(path)
26 for line in path.read_text(encoding="utf-8").splitlines():
27 line = line.strip()
28 if not line or line.startswith("#") or "=" not in line:
29 continue
30 key, val = line.split("=", 1)
31 key = key.strip()
32 val = val.strip().strip('"').strip("'")
33 os.environ.setdefault(key, val)
34
35
36def _parse_flag(args: list[str], name: str) -> str | None:
37 for i, a in enumerate(args):
38 if a == name and i + 1 < len(args):
39 return args[i + 1]
40 return None
41
42
43def _parse_num(args: list[str], name: str, fallback: float | int | None = None) -> float | int | None:
44 raw = _parse_flag(args, name)
45 if raw is None or raw == "":
46 return fallback
47 try:
48 n = float(raw)
49 return int(n) if n == int(n) else n
50 except ValueError:
51 return fallback
52
53
54def _read_json(path: Path) -> dict[str, Any] | None:
55 if not path.is_file():
56 return None
57 try:
58 return json.loads(path.read_text(encoding="utf-8"))
59 except Exception:
60 return None
61
62
63def _r2_client():
64 try:
65 import boto3
66 except ImportError as exc:
67 raise RuntimeError("pip install boto3") from exc
68
69 endpoint = os.environ.get("R2_ENDPOINT")
70 access_key = os.environ.get("R2_ACCESS_KEY_ID")
71 secret = os.environ.get("R2_SECRET_ACCESS_KEY")
72 missing = [
73 name
74 for name, val in (
75 ("R2_ENDPOINT", endpoint),
76 ("R2_ACCESS_KEY_ID", access_key),
77 ("R2_SECRET_ACCESS_KEY", secret),
78 ("R2_BUCKET_NAME", os.environ.get("R2_BUCKET_NAME")),
79 )
80 if not val
81 ]
82 if missing:
83 raise RuntimeError(f"Missing env: {', '.join(missing)} (set in .env.local)")
84
85 return boto3.client(
86 "s3",
87 endpoint_url=endpoint,
88 aws_access_key_id=access_key,
89 aws_secret_access_key=secret,
90 region_name="auto",
91 )
92
93
94def _upload_r2(client: Any, bucket: str, key: str, file_path: Path, content_type: str) -> None:
95 body = file_path.read_bytes()
96 client.put_object(Bucket=bucket, Key=key, Body=body, ContentType=content_type)
97 print(f" uploaded {key} ({len(body) / 1_000_000:.2f} MB)", flush=True)
99
100def _supabase_upsert(row: dict[str, Any]) -> dict[str, Any]:
101 try:
102 import requests
103 except ImportError as exc:
104 raise RuntimeError("pip install requests") from exc
105
106 url = os.environ.get("NEXT_PUBLIC_SUPABASE_URL") or os.environ.get("SUPABASE_URL")
107 key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
108 if not url or not key:
109 raise RuntimeError("Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env.local")
110
111 base = url.rstrip("/")
112 headers = {
113 "apikey": key,
114 "Authorization": f"Bearer {key}",
115 "Content-Type": "application/json",
116 "Prefer": "resolution=merge-duplicates,return=representation",
117 }
118 resp = requests.post(
119 f"{base}/rest/v1/public_saes?on_conflict=model_slug,layer",
120 headers=headers,
121 json=row,
122 timeout=60,
123 )
124 if resp.status_code in (200, 201):
125 data = resp.json()
126 return data[0] if isinstance(data, list) and data else data
127
128 # Fallback: plain insert
129 resp2 = requests.post(
130 f"{base}/rest/v1/public_saes",
131 headers={**headers, "Prefer": "return=representation"},
132 json=row,
133 timeout=60,
134 )
135 if resp2.status_code not in (200, 201):
136 raise RuntimeError(f"Supabase error {resp.status_code}: {resp.text}; insert: {resp2.text}")
137 data = resp2.json()
138 return data[0] if isinstance(data, list) and data else data
139
140
141def publish_public_sae(args: list[str]) -> None:
143
144 sae_path_s = _parse_flag(args, "--sae")
145 if not sae_path_s:
146 print(
147 "Usage: aquin sae publish --sae <path.pt> --model-slug <slug> "
148 "[--layer N] [--model-name ...] [--d-model 1024] [--d-sae 32768]",
149 file=sys.stderr,
150 )
151 sys.exit(1)
152
153 sae_abs = Path(sae_path_s).expanduser().resolve()
154 if not sae_abs.is_file():
155 print(f"SAE not found: {sae_abs}", file=sys.stderr)
156 sys.exit(1)
157
158 meta = _read_json(sae_abs.with_suffix(".meta.json")) or {}
159 model_slug = _parse_flag(args, "--model-slug") or str(meta.get("model_id") or "").strip()
160 if not model_slug:
161 print("Pass --model-slug or ensure .meta.json has model_id", file=sys.stderr)
162 sys.exit(1)
163
164 layer_raw = _parse_num(args, "--layer") or meta.get("layer")
165 if layer_raw is None:
166 print("Pass --layer or ensure .meta.json has layer", file=sys.stderr)
167 sys.exit(1)
168 layer = int(layer_raw)
169
170 d_model = int(_parse_num(args, "--d-model", meta.get("d_model") or 2048) or 2048)
171 d_sae = int(_parse_num(args, "--d-sae", meta.get("n_features") or 32768) or 32768)
172 train_steps = _parse_num(args, "--train-steps", meta.get("steps"))
173 l0 = _parse_num(args, "--l0")
174 dead_pct = _parse_num(args, "--dead-pct")
175 interp_score = _parse_num(args, "--interp-score")
176 model_name = _parse_flag(args, "--model-name") or model_slug
177 arch = _parse_flag(args, "--arch") or "sparse_autoencoder"
178 k = int(_parse_num(args, "--k") or round(d_sae / d_model))
179
180 metrics = _read_json(sae_abs.with_suffix(".metrics.json"))
181 if metrics and interp_score is None and metrics.get("interp_score") is not None:
182 interp_score = float(metrics["interp_score"])
183 print(f" metrics sidecar: {sae_abs.with_suffix('.metrics.json').name}", flush=True)
184
185 umap_path_s = _parse_flag(args, "--umap")
186 umap_path = Path(umap_path_s).expanduser() if umap_path_s else sae_abs.with_suffix(".umap.json")
187 if not umap_path.is_file():
188 umap_path = None # type: ignore[assignment]
189
190 norm_path_s = _parse_flag(args, "--norm")
191 if norm_path_s:
192 norm_path = Path(norm_path_s).expanduser()
193 else:
194 parent = sae_abs.parent
195 candidates = [
196 parent / f"norm_layer{layer}.pt",
197 parent / f"_acts_layer{layer}" / "norm.pt",
198 parent / "norm.pt",
199 ]
200 norm_path = next((p for p in candidates if p.is_file()), None)
201 if norm_path is None or not norm_path.is_file():
202 print("Norm file not found. Pass --norm or train with aquin sae train.", file=sys.stderr)
203 sys.exit(1)
204
205 bucket = os.environ["R2_BUCKET_NAME"]
206 r2_sae_key = f"saes/{model_slug}/l{layer}/sae.pt"
207 r2_norm_key = f"saes/{model_slug}/l{layer}/norm.pt"
208 r2_umap_key = f"saes/{model_slug}/l{layer}/umap.json"
209
210 print(f"Publishing {model_slug} layer {layer}...", flush=True)
211 print("R2:", flush=True)
212 s3 = _r2_client()
213 _upload_r2(s3, bucket, r2_sae_key, sae_abs, "application/octet-stream")
214 _upload_r2(s3, bucket, r2_norm_key, norm_path.resolve(), "application/octet-stream")
215 umap_key = _parse_flag(args, "--umap-key")
216 if umap_path and umap_path.is_file():
217 _upload_r2(s3, bucket, r2_umap_key, umap_path, "application/json")
218 umap_key = umap_key or r2_umap_key
219 print(f" umap: {umap_path.name}", flush=True)
220 else:
221 print(" umap: (skipped — run aquin sae catalog-metrics first)", flush=True)
222
223 row = {
224 "model_slug": model_slug,
225 "model_name": model_name,
226 "layer": layer,
227 "d_model": d_model,
228 "d_sae": d_sae,
229 "k": k,
230 "arch": arch,
231 "train_steps": train_steps,
232 "final_loss": _parse_num(args, "--final-loss"),
233 "l0": l0,
234 "dead_pct": dead_pct,
235 "interp_score": interp_score,
236 "umap_key": umap_key,
237 "r2_key": r2_sae_key,
238 "meta_key": r2_norm_key,
239 }
240
241 data = _supabase_upsert(row)
242 print(f"\nRegistered: {data}", flush=True)
243 print(f"Pull: aquin load sae {model_slug}-l{layer}", flush=True)
244 print(f"Load: aquin load sae {model_slug}-l{{n}}", flush=True)
dict[str, Any] _supabase_upsert(dict[str, Any] row)
dict[str, Any]|None _read_json(Path path)
str|None _parse_flag(list[str] args, str name)
float|int|None _parse_num(list[str] args, str name, float|int|None fallback=None)
None _upload_r2(Any client, str bucket, str key, Path file_path, str content_type)