Spaces:
Running on Zero
Running on Zero
| #!/usr/bin/env python3 | |
| """ | |
| Headless atlas pipeline for meta-llama/Llama-3.2-1B-Instruct. | |
| Runs the full GWIQ-atlas pipeline end-to-end: | |
| 1. activation census extraction | |
| 2. finalize any leftover .npz chunks | |
| 3. per-layer analysis (taxonomy, heatmap, separation, co-activation, code) | |
| 4. optional compliance-behaviour axis extraction | |
| 5. atlas init + merge layers + merge compliance-behaviour + index | |
| Memory-safe defaults for 8 GB unified/system RAM: | |
| - bfloat16 weights | |
| - CPU-only / no accelerate device_map | |
| - max_length=128 | |
| - batch_size=1 | |
| - components={"mlp", "gate", "up"} (attention hooks are expensive) | |
| Usage: | |
| python app.py \ | |
| --corpus prompts/prompts_balanced.jsonl \ | |
| --outdir outputs/llama-3.2-1b-census \ | |
| --atlas atlas/llama-3.2-1b | |
| With compliance-behaviour axis: | |
| python app.py \ | |
| --corpus prompts/prompts_balanced.jsonl \ | |
| --outdir outputs/llama-3.2-1b-census \ | |
| --atlas atlas/llama-3.2-1b \ | |
| --positive prompts/authentic.jsonl \ | |
| --negative prompts/corporate.jsonl \ | |
| --positive-key text \ | |
| --negative-key text | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import concurrent.futures | |
| import gc | |
| import os | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| from qwip_atlas.config import ( | |
| AtlasRunConfig, | |
| ComplianceBehaviourRunConfig, | |
| CorpusSpec, | |
| ModelSpec, | |
| ) | |
| from qwip_atlas.extractors import run_local_census | |
| from qwip_atlas.layers import parse_layer_spec | |
| from transformers import AutoConfig | |
| DEFAULT_MODEL_ID = "meta-llama/Llama-3.2-1B-Instruct" | |
| DEFAULT_COMPONENTS = {"mlp", "gate", "up"} | |
| def _analyze_one(npz: Path, analysis_dir: Path, pooling: str, skip_existing: bool) -> int: | |
| layer = int(npz.stem.split("_")[0][1:]) | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.analyze_layers", | |
| "--layer", | |
| str(layer), | |
| "--input", | |
| str(npz), | |
| "--outdir", | |
| str(analysis_dir), | |
| "--top_k", | |
| "300", | |
| "--pooling", | |
| pooling, | |
| ] | |
| if skip_existing: | |
| cmd.append("--skip-existing") | |
| print(f"[analyze] layer {layer} (pooling={pooling})") | |
| subprocess.run(cmd, check=True) | |
| gc.collect() | |
| return layer | |
| def _layer_analysis_done(layer: int, analysis_dir: Path) -> bool: | |
| """Return True if all expected analysis artifacts for a layer exist.""" | |
| required = [ | |
| f"l{layer}_mlp_neuron_taxonomy.json", | |
| f"l{layer}_gate_neuron_taxonomy.json", | |
| f"l{layer}_up_neuron_taxonomy.json", | |
| f"l{layer}_component_comparison.json", | |
| ] | |
| return all((analysis_dir / name).exists() for name in required) | |
| def _ram_gb() -> float | None: | |
| """Best-effort total RAM in GB without external dependencies.""" | |
| try: | |
| if hasattr(os, "sysconf") and "SC_PHYS_PAGES" in os.sysconf_names and "SC_PAGE_SIZE" in os.sysconf_names: | |
| pages = os.sysconf("SC_PHYS_PAGES") | |
| page_size = os.sysconf("SC_PAGE_SIZE") | |
| return pages * page_size / (1024**3) | |
| except Exception: | |
| pass | |
| try: | |
| result = subprocess.run( | |
| ["sysctl", "-n", "hw.memsize"], | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) | |
| return int(result.stdout.strip()) / (1024**3) | |
| except Exception: | |
| pass | |
| return None | |
| def _parse_components(s: str | None) -> set[str]: | |
| if not s: | |
| return set(DEFAULT_COMPONENTS) | |
| return {c.strip() for c in s.split(",")} | |
| def _layer_count(model_id: str, token: str | None) -> tuple[int, float]: | |
| cfg = AutoConfig.from_pretrained(model_id, token=token, trust_remote_code=True) | |
| # Some models nest the language-model config under text_config. | |
| cfg_search = cfg | |
| if hasattr(cfg, "text_config") and cfg.text_config is not None: | |
| cfg_search = cfg.text_config | |
| n_layers = ( | |
| getattr(cfg_search, "num_hidden_layers", None) | |
| or getattr(cfg_search, "n_layer", None) | |
| or getattr(cfg, "num_hidden_layers", None) | |
| or getattr(cfg, "n_layer", None) | |
| ) | |
| if n_layers is None: | |
| raise ValueError(f"Could not determine layer count from config for {model_id}") | |
| # Rough bf16 weight estimate: 2 bytes × num_params. Fall back to hidden-size heuristic. | |
| num_params = getattr(cfg_search, "num_parameters", None) or getattr(cfg, "num_parameters", None) | |
| if num_params is None: | |
| h = ( | |
| getattr(cfg_search, "hidden_size", None) | |
| or getattr(cfg_search, "d_model", None) | |
| or getattr(cfg, "hidden_size", None) | |
| or getattr(cfg, "d_model", None) | |
| or 2048 | |
| ) | |
| num_params = 12 * n_layers * h * h # standard transformer approximation | |
| weight_gb = num_params * 2 / (1024 ** 3) | |
| return n_layers, weight_gb | |
| def _check_ram(layer_count: int, components: set[str], max_length: int, weight_gb: float) -> None: | |
| ram = _ram_gb() | |
| per_component_gb = 0.3 if max_length <= 128 else 0.6 | |
| estimated = weight_gb + len(components) * per_component_gb | |
| if ram is not None: | |
| print(f"[ram] detected {ram:.1f} GB unified/system RAM") | |
| print(f"[ram] estimated need ~{estimated:.1f} GB for {len(components)} components, max_length={max_length}") | |
| if ram is not None and ram < estimated: | |
| print(f"[ram] WARNING: estimated need ({estimated:.1f} GB) exceeds available RAM ({ram:.1f} GB).") | |
| print("[ram] Reduce --components or --max-length if the process starts swapping.") | |
| def _finalize_dir(outdir: Path) -> int: | |
| """Finalize leftover .tmp chunk dirs. Returns the number of .tmp dirs found.""" | |
| tmp_dirs = [d for d in outdir.iterdir() if d.is_dir() and d.suffix == ".tmp"] | |
| if not tmp_dirs: | |
| return 0 | |
| print(f"[finalize] found {len(tmp_dirs)} unfinished .tmp chunk directories") | |
| finalize_script = Path(__file__).resolve().parent / "finalize_census.py" | |
| if finalize_script.exists(): | |
| subprocess.run([sys.executable, str(finalize_script), str(outdir)], check=True) | |
| else: | |
| print("[finalize] finalize_census.py not found; skipping") | |
| return len(tmp_dirs) | |
| def _begin_stage_run(args, group: str, stage: str, config: dict, tags=None) -> bool: | |
| """Init a per-stage W&B run tied to the pipeline ``group``. No-op if no | |
| --wandb-project. Returns True if wandb is active. Caller MUST call | |
| finish_wandb() at the stage end so the next stage's init isn't short-circuited. | |
| """ | |
| if not args.wandb_project: | |
| return False | |
| from qwip_atlas.atlas_wandb import init_wandb, wandb_active | |
| init_wandb( | |
| project=args.wandb_project, | |
| entity=args.wandb_entity, | |
| run_name=f"{group}-{stage}", | |
| group=group, | |
| config={"stage": stage, **config}, | |
| tags=list(tags) if tags else [stage], | |
| ) | |
| return wandb_active() | |
| def _run_analysis(outdir: Path, components: set[str], skip_existing: bool, | |
| pooling: str = "mean", | |
| wandb_project: str | None = None, | |
| wandb_entity: str = "ricks-holmberg-juiceb0xc0de", | |
| wandb_group: str | None = None, | |
| wandb_tags: list[str] | None = None) -> Path: | |
| analysis_dir = outdir / "analysis" | |
| analysis_dir.mkdir(parents=True, exist_ok=True) | |
| npz_files = sorted(outdir.glob("l*_census_raw.npz"), key=lambda p: int(p.stem.split("_")[0][1:])) | |
| if not npz_files: | |
| raise SystemExit(f"[error] no l*_census_raw.npz files in {outdir}") | |
| todo = [] | |
| for npz in npz_files: | |
| layer = int(npz.stem.split("_")[0][1:]) | |
| if skip_existing and _layer_analysis_done(layer, analysis_dir): | |
| print(f"[analyze] skip existing layer {layer}") | |
| continue | |
| todo.append(npz) | |
| if not todo: | |
| print("[analyze] all layers already processed") | |
| return analysis_dir | |
| n_workers = min(len(todo), 4, os.cpu_count() or 1) | |
| print(f"[analyze] running {len(todo)} layers in parallel with {n_workers} workers") | |
| # Per-stage W&B run (one writer = app.py, so 4-way parallel subprocesses | |
| # can't race on the run). Each analyze_layers.py subprocess writes an | |
| # l{N}_metrics.json sidecar; we read it here as the futures complete and | |
| # wlog per-layer timing/counts to the "analysis" run. | |
| import time as _time | |
| from qwip_atlas.io import read_json | |
| _wandb_on = False | |
| if wandb_project: | |
| from qwip_atlas.atlas_wandb import init_wandb, wandb_active, wlog, wstage, wsummary, finish_wandb | |
| init_wandb( | |
| project=wandb_project, | |
| entity=wandb_entity, | |
| run_name=f"{wandb_group}-analysis" if wandb_group else f"analysis-{outdir.name}", | |
| group=wandb_group, | |
| config={"stage": "analysis", "n_layers": len(todo), "pooling": pooling, | |
| "components": sorted(components)}, | |
| tags=wandb_tags or ["analysis"], | |
| ) | |
| _wandb_on = wandb_active() | |
| _t_an = _time.time() | |
| if _wandb_on: | |
| _t_an = wstage("analysis_start", _t_an) | |
| # ThreadPoolExecutor, NOT ProcessPoolExecutor: _analyze_one just shells out | |
| # to `python -m qwip_atlas.analyze_layers` via subprocess.run, so the real | |
| # work already runs in separate subprocesses -- the pool only orchestrates | |
| # the launches. A process pool needlessly grabs POSIX semaphores in /dev/shm | |
| # (SemLock), which ENOSPC-crashes on a full tmpfs (RunPod /dev/shm is small | |
| # and shared). Threads block on subprocess.run (GIL released) with zero | |
| # /dev/shm semaphores -- identical concurrency, no tmpfs dependency. | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=n_workers) as pool: | |
| futures = { | |
| pool.submit(_analyze_one, npz, analysis_dir, pooling, skip_existing): npz | |
| for npz in todo | |
| } | |
| _step = 0 | |
| for fut in concurrent.futures.as_completed(futures): | |
| layer = fut.result() | |
| print(f"[analyze] layer {layer} done") | |
| if _wandb_on: | |
| _m_path = analysis_dir / f"l{layer}_metrics.json" | |
| _m = {} | |
| try: | |
| _m = read_json(_m_path) if _m_path.exists() else {} | |
| except Exception: | |
| _m = {} | |
| _row = { | |
| "layer": layer, | |
| "analyze_sec": float(_m.get("analyze_sec", 0.0)), | |
| "n_features_total": int(_m.get("n_features_total", 0)), | |
| "pooling": _m.get("pooling", pooling), | |
| } | |
| # Flatten per-component neuron counts + sep scores for the UI. | |
| for comp, n in (_m.get("components") or {}).items(): | |
| _row[f"{comp}/n_neurons"] = int(n) | |
| for comp, s in (_m.get("top_sep_scores") or {}).items(): | |
| _row[f"{comp}/top_sep"] = float(s) | |
| wlog(_row, step=_step) | |
| _step += 1 | |
| if _wandb_on: | |
| wsummary({"final/analysis_total_sec": _time.time() - _t_an, | |
| "final/analysis_n_layers": len(todo)}) | |
| finish_wandb() | |
| return analysis_dir | |
| def _run_compliance( | |
| model_id: str, | |
| layers: list[int], | |
| outdir: Path, | |
| positive: Path, | |
| negative: Path, | |
| pos_key: str, | |
| neg_key: str, | |
| components: set[str], | |
| token: str | None, | |
| trust_remote: bool = False, | |
| attn_implementation: str | None = None, | |
| chat_template: bool = False, | |
| wandb_project: str | None = None, | |
| wandb_entity: str = "ricks-holmberg-juiceb0xc0de", | |
| wandb_group: str | None = None, | |
| wandb_tags: list[str] | None = None, | |
| ) -> None: | |
| from qwip_atlas.extractors import run_compliance_behaviour | |
| cfg = ComplianceBehaviourRunConfig( | |
| model=ModelSpec( | |
| model_id=model_id, | |
| dtype="bfloat16", | |
| device_map="", | |
| max_length=128, | |
| trust_remote_code=trust_remote, | |
| attn_implementation=attn_implementation, | |
| chat_template=chat_template, | |
| ), | |
| positive_corpus=CorpusSpec(path=positive, prompt_key=pos_key), | |
| negative_corpus=CorpusSpec(path=negative, prompt_key=neg_key), | |
| layers=layers, | |
| output=outdir / "compliance_behaviour_scores.json", | |
| batch_size=1, | |
| components=set(components), | |
| # --positive is the authentic corpus, --negative is corporate. | |
| # Labels drive the corp/auth aliases downstream, so they must match the actual corpora. | |
| positive_label="authentic", | |
| negative_label="corporate", | |
| truncate_to_deepest_layer=True, | |
| wandb_project=wandb_project, | |
| wandb_entity=wandb_entity, | |
| wandb_group=wandb_group, | |
| wandb_tags=wandb_tags, | |
| ) | |
| run_compliance_behaviour(cfg, hf_token=token) | |
| def _atlas_init(atlas_dir: Path, model_id: str, census_sample: Path, token: str | None = None) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.build_atlas", | |
| "--atlas", | |
| str(atlas_dir), | |
| "init", | |
| "--model-id", | |
| model_id, | |
| "--census", | |
| str(census_sample), | |
| ] | |
| if token: | |
| cmd += ["--hf-token", token] | |
| print(f"\n[atlas] init -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _atlas_merge_layers(atlas_dir: Path, census_dir: Path, analysis_dir: Path) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.build_atlas", | |
| "--atlas", | |
| str(atlas_dir), | |
| "merge-all-layers", | |
| "--census-dir", | |
| str(census_dir), | |
| "--analysis-dir", | |
| str(analysis_dir), | |
| "--no-census-copy", | |
| ] | |
| print(f"[atlas] merge-all-layers -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _atlas_merge_compliance(atlas_dir: Path, report: Path) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.build_atlas", | |
| "--atlas", | |
| str(atlas_dir), | |
| "merge-compliance-behaviour", | |
| "--report", | |
| str(report), | |
| ] | |
| print(f"[atlas] merge-compliance-behaviour -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _atlas_merge_ov(atlas_dir: Path, report: Path) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.build_atlas", | |
| "--atlas", | |
| str(atlas_dir), | |
| "merge-ov", | |
| "--report", | |
| str(report), | |
| ] | |
| print(f"[atlas] merge-ov -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _atlas_merge_subzero(atlas_dir: Path, report: Path) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.build_atlas", | |
| "--atlas", | |
| str(atlas_dir), | |
| "merge-subzero", | |
| "--report", | |
| str(report), | |
| ] | |
| print(f"[atlas] merge-subzero -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _run_per_token_analysis( | |
| outdir: Path, | |
| analysis_dir: Path, | |
| components: set[str], | |
| model_id: str, | |
| token: str | None, | |
| trust_remote: bool, | |
| ) -> None: | |
| """Run per-token profiling for every layer/component that stored per-token arrays. | |
| The analysis dir holds flat files (e.g. ``l11_mlp_separation_scores.npy``). We | |
| create a small per-component scratch dir with the required ``separation_scores.npy`` | |
| so ``qwip_atlas.analyze_tokens`` can run unmodified. | |
| """ | |
| import shutil | |
| from qwip_atlas.analyze_tokens import PER_TOKEN_KEYS | |
| reports_dir = analysis_dir / "per_token" | |
| reports_dir.mkdir(parents=True, exist_ok=True) | |
| npz_files = sorted(outdir.glob("l*_census_raw.npz"), key=lambda p: int(p.stem.split("_")[0][1:])) | |
| for npz in npz_files: | |
| layer = int(npz.stem.split("_")[0][1:]) | |
| prefix = f"l{layer}_" | |
| for component in components: | |
| key = PER_TOKEN_KEYS.get(component) | |
| if not key: | |
| continue | |
| output = reports_dir / f"l{layer}_{component}_per_token.json" | |
| if output.exists(): | |
| print(f"[per-token] skip existing {output}") | |
| continue | |
| sep_src = analysis_dir / f"{prefix}{component}_separation_scores.npy" | |
| if not sep_src.exists(): | |
| print(f"[per-token] missing separation scores for {prefix}{component}; skipping") | |
| continue | |
| # Build a per-component scratch dir expected by analyze_tokens. | |
| comp_dir = analysis_dir / "per_token_components" / f"{prefix}{component}" | |
| comp_dir.mkdir(parents=True, exist_ok=True) | |
| sep_dst = comp_dir / "separation_scores.npy" | |
| if not sep_dst.exists(): | |
| shutil.copy(sep_src, sep_dst) | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.analyze_tokens", | |
| "--census", | |
| str(npz), | |
| "--analysis-dir", | |
| str(comp_dir), | |
| "--component", | |
| component, | |
| "--model", | |
| model_id, | |
| "--output", | |
| str(output), | |
| ] | |
| if token: | |
| cmd += ["--hf-token", token] | |
| if trust_remote: | |
| cmd.append("--trust-remote") | |
| print(f"[per-token] layer {layer} component {component} -> {output}") | |
| subprocess.run(cmd, check=True) | |
| gc.collect() | |
| def _run_logit_lens( | |
| model_id: str, | |
| atlas_dir: Path, | |
| output: Path, | |
| token: str | None, | |
| trust_remote: bool, | |
| ) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "analyze_logit_lens.py", | |
| "--model", | |
| model_id, | |
| "--atlas", | |
| str(atlas_dir), | |
| "--output", | |
| str(output), | |
| ] | |
| if token: | |
| cmd += ["--hf-token", token] | |
| if trust_remote: | |
| cmd.append("--trust-remote") | |
| print(f"[logit_lens] {model_id} -> {output}") | |
| subprocess.run(cmd, check=True) | |
| def _atlas_merge_logit_lens(atlas_dir: Path, report: Path) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.build_atlas", | |
| "--atlas", | |
| str(atlas_dir), | |
| "merge-logit-lens", | |
| "--report", | |
| str(report), | |
| ] | |
| print(f"[atlas] merge-logit-lens -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _run_sub_zero( | |
| model_id: str, | |
| corpora_dir: Path, | |
| output: Path, | |
| report: Path, | |
| token: str | None, | |
| trust_remote: bool = False, | |
| attn_implementation: str | None = None, | |
| chat_template: bool = False, | |
| pooling: str = "mean", | |
| all_layers: bool = True, | |
| wandb_project: str | None = None, | |
| wandb_entity: str = "ricks-holmberg-juiceb0xc0de", | |
| wandb_run_name: str | None = None, | |
| wandb_tags: str | None = None, | |
| wandb_group: str | None = None, | |
| ) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "qwip_atlas.run_sub_zero", | |
| "--model", | |
| model_id, | |
| "--corpora-dir", | |
| str(corpora_dir), | |
| "--output", | |
| str(output), | |
| "--report", | |
| str(report), | |
| "--pooling", | |
| pooling, | |
| ] | |
| if all_layers: | |
| cmd.append("--all-layers") | |
| if chat_template: | |
| cmd.append("--chat-template") | |
| if wandb_project: | |
| cmd += ["--wandb-project", wandb_project, | |
| "--wandb-entity", wandb_entity] | |
| if wandb_run_name: | |
| cmd += ["--wandb-run-name", wandb_run_name] | |
| if wandb_group: | |
| cmd += ["--wandb-group", wandb_group] | |
| if wandb_tags: | |
| cmd += ["--wandb-tags", wandb_tags] | |
| if token: | |
| cmd += ["--hf-token", token] | |
| if trust_remote: | |
| cmd.append("--trust-remote") | |
| if attn_implementation: | |
| cmd += ["--attn-implementation", attn_implementation] | |
| print(f"[sub_zero] running run_sub_zero.py (pooling={pooling}, all_layers={all_layers}) -> {report}") | |
| subprocess.run(cmd, check=True) | |
| def _atlas_index(atlas_dir: Path) -> None: | |
| cmd = [sys.executable, "-m", "qwip_atlas.build_atlas", "--atlas", str(atlas_dir), "index"] | |
| print(f"[atlas] index -> {atlas_dir}") | |
| subprocess.run(cmd, check=True) | |
| def _pause(stage_name: str, pause: bool) -> None: | |
| if not pause: | |
| return | |
| print(f"\n[PAUSE] {stage_name} complete.") | |
| print(" Press Enter to continue, or Ctrl-C to stop and resume later with skip flags.") | |
| try: | |
| input(" Continue? ") | |
| except (EOFError, KeyboardInterrupt): | |
| raise SystemExit("\n[exit] stopped at stage boundary. resume with --skip-census / --skip-existing-analysis / --skip-existing-atlas") | |
| def _run_ov_circuits(model_id: str, output: Path, token: str | None, trust_remote: bool = False) -> None: | |
| cmd = [ | |
| sys.executable, | |
| "analyze_ov_circuits.py", | |
| "--model", | |
| model_id, | |
| "--output", | |
| str(output), | |
| ] | |
| if token: | |
| cmd += ["--hf-token", token] | |
| if trust_remote: | |
| cmd.append("--trust-remote") | |
| print(f"[ov] running analyze_ov_circuits.py -> {output}") | |
| subprocess.run(cmd, check=True) | |
| def main() -> None: | |
| p = argparse.ArgumentParser(description="Run full atlas pipeline") | |
| p.add_argument("--model", default=DEFAULT_MODEL_ID, help="HuggingFace model ID") | |
| p.add_argument("--corpus", required=True, type=Path, help="JSONL prompt corpus") | |
| p.add_argument("--outdir", required=True, type=Path, help="census output directory") | |
| p.add_argument("--atlas", required=True, type=Path, help="master atlas directory") | |
| p.add_argument("--positive", type=Path, help="positive-axis JSONL for compliance-behaviour (authentic axis)") | |
| p.add_argument("--negative", type=Path, help="negative-axis JSONL for compliance-behaviour (corporate axis)") | |
| p.add_argument("--positive-key", default="text", help="prompt field name in --positive corpus") | |
| p.add_argument("--negative-key", default="text", help="prompt field name in --negative corpus") | |
| p.add_argument("--components", default="mlp,gate,up", help="comma-separated components to capture") | |
| p.add_argument("--max-length", type=int, default=128) | |
| p.add_argument("--batch-size", type=int, default=1) | |
| p.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "bf16", "float16", "fp16", "float32", "fp32"]) | |
| p.add_argument("--attn-implementation", default=None, choices=["eager", "sdpa", "flash_attention_2"]) | |
| p.add_argument("--trust-remote", action="store_true", help="Enable trust_remote_code for custom model architectures") | |
| p.add_argument("--chat-template", action="store_true", | |
| help="Wrap every prompt in the model's chat template (user turn + assistant " | |
| "generation prompt) before tokenizing, across census, compliance, and " | |
| "Sub-Zero. For *-Instruct models this captures activations in-distribution.") | |
| p.add_argument("--timing-every", type=int, default=250, help="print extraction timing every N batches; use 0 to disable") | |
| p.add_argument("--layers", default="all", help="layer spec like '0-15' or 'all'") | |
| p.add_argument("--sub-zero", action="store_true", | |
| help="run the Sub-Zero surgery probe (DAS rotational analysis) and fold it into the atlas") | |
| p.add_argument("--sub-zero-corpora", type=Path, default=Path("prompts"), | |
| help="dir holding the four Sub-Zero corpora files: corporate.jsonl, authentic.jsonl, " | |
| "and optionally neutral.jsonl / red_team.jsonl") | |
| # --all-layers defaults True: Rick runs full 100% coverage (SVD + AtP + DAS on | |
| # every transformer layer, not just the deepest 50%). Use --no-all-layers to | |
| # fall back to the cheap --sacred-top-k-percent scoped probe for a smoke test. | |
| p.add_argument("--all-layers", dest="all_layers", action="store_true", default=True, | |
| help="Sub-Zero: fully probe EVERY layer (SVD + AtP + DAS). Default True (100%% coverage).") | |
| p.add_argument("--no-all-layers", dest="all_layers", action="store_false", | |
| help="Sub-Zero: only fully probe the deepest --sacred-top-k-percent of layers (smoke test).") | |
| p.add_argument("--pooling", default="mean", choices=["last", "mean"], | |
| help="How to pool per-token activations for layer analysis, Sub-Zero, and logit-lens. Default: mean") | |
| p.add_argument("--store-per-token", action="store_true", | |
| help="store full per-token activation arrays (increases .npz size; useful for token attribution)") | |
| p.add_argument("--per-token-analysis", action="store_true", | |
| help="run qwip_atlas.analyze_tokens on every layer/component after analysis (needs --store-per-token)") | |
| p.add_argument("--logit-lens", action="store_true", | |
| help="run analyze_logit_lens.py and merge results into the atlas (loads full model weights)") | |
| p.add_argument("--track-residuals", action="store_true", | |
| help="capture residual stream states pre/post attention and MLP (increases .npz size)") | |
| p.add_argument("--npz-compressed", action="store_true", | |
| help="write compressed .npz files (smaller but slower). Default is uncompressed for speed.") | |
| p.add_argument("--skip-census", action="store_true", help="skip extraction if all census .npz files already exist") | |
| p.add_argument("--skip-existing-analysis", action="store_true") | |
| p.add_argument("--skip-existing-atlas", action="store_true") | |
| p.add_argument("--pause-between-stages", action="store_true", | |
| help="pause after each major stage and wait for Enter before continuing") | |
| p.add_argument("--hf-token", default=os.environ.get("HF_TOKEN")) | |
| p.add_argument("--wandb-project", default=None, | |
| help="W&B project name. If set, the Sub-Zero probe logs metrics + custom graphs " | |
| "(loss, RSS, GPU peak, per-step memory) to W&B. Entity defaults to " | |
| "ricks-holmberg-juiceb0xc0de.") | |
| p.add_argument("--wandb-entity", default="ricks-holmberg-juiceb0xc0de") | |
| p.add_argument("--wandb-run-name", default=None) | |
| p.add_argument("--wandb-tags", default=None, | |
| help="comma-separated W&B tags (e.g. sub-zero,llama-3.1-8b,a100)") | |
| p.add_argument("--no-auth-prompt", action="store_true", | |
| help="Skip the interactive HF/W&B key prompt; fall back to env vars only.") | |
| p.add_argument("--persist-login", action="store_true", | |
| help="Cache the pasted keys via huggingface_hub/wandb login so the pod " | |
| "stays logged in for later commands and stages.") | |
| args = p.parse_args() | |
| if not args.corpus.exists(): | |
| raise SystemExit(f"corpus not found: {args.corpus}") | |
| # Resolve HF + W&B credentials before the model download (HF is the source). | |
| # Hidden-input prompt only for keys not already in env / cached login. | |
| from qwip_atlas.auth import ensure_credentials | |
| hf_token, _ = ensure_credentials( | |
| hf_token=args.hf_token, | |
| prompt=not args.no_auth_prompt, | |
| persist_login=args.persist_login, | |
| ) | |
| if hf_token and not args.hf_token: | |
| args.hf_token = hf_token | |
| model_id = args.model | |
| token = args.hf_token | |
| components = _parse_components(args.components) | |
| # Pipeline group: ties every stage's W&B run together in the UI group pane. | |
| # When --wandb-run-name is set it IS the group (stage runs become | |
| # "{group}-census", "{group}-analysis", ...); otherwise auto-derive one. | |
| group = args.wandb_run_name or f"{model_id.split('/')[-1]}-{'_'.join(sorted(components))}" | |
| layer_count, weight_gb = _layer_count(model_id, token) | |
| if args.layers == "all": | |
| layers = list(range(layer_count)) | |
| else: | |
| layers = parse_layer_spec(args.layers) | |
| layers = [l for l in layers if 0 <= l < layer_count] | |
| print("=" * 70) | |
| print(" GWIQ-atlas headless runner") | |
| print(f" model: {model_id}") | |
| print(f" layers: {layers[0]}..{layers[-1]} ({len(layers)} total)") | |
| print(f" components: {components}") | |
| print(f" max_length: {args.max_length}") | |
| print(f" batch_size: {args.batch_size}") | |
| print(f" dtype: {args.dtype}") | |
| print(f" output: {args.outdir}") | |
| print(f" atlas: {args.atlas}") | |
| print("=" * 70) | |
| _check_ram(layer_count, components, args.max_length, weight_gb) | |
| args.outdir.mkdir(parents=True, exist_ok=True) | |
| # 1. Census extraction | |
| existing_npz = [args.outdir / f"l{l}_census_raw.npz" for l in layers] | |
| if args.skip_census and all(p.exists() for p in existing_npz): | |
| print("\n[1/6] skipping census extraction (all .npz files present)") | |
| counts = {l: 0 for l in layers} | |
| else: | |
| if args.skip_census: | |
| missing = [p.name for p in existing_npz if not p.exists()] | |
| print(f"\n[1/6] --skip-census set but missing: {missing}; running extraction") | |
| else: | |
| print("\n[1/6] activation census extraction") | |
| cfg = AtlasRunConfig( | |
| model=ModelSpec( | |
| model_id=model_id, | |
| dtype=args.dtype, | |
| device_map="", | |
| max_length=args.max_length, | |
| trust_remote_code=args.trust_remote, | |
| attn_implementation=args.attn_implementation, | |
| chat_template=args.chat_template, | |
| ), | |
| corpus=CorpusSpec( | |
| path=args.corpus, | |
| prompt_key="prompt", | |
| category_key="category", | |
| bucket_key="bucket", | |
| ), | |
| layers=layers, | |
| outdir=args.outdir, | |
| batch_size=args.batch_size, | |
| components=set(components), | |
| truncate_to_deepest_layer=True, | |
| timing_every=args.timing_every, | |
| store_per_token=args.store_per_token, | |
| track_residuals=args.track_residuals, | |
| compressed=args.npz_compressed, | |
| wandb_project=args.wandb_project, | |
| wandb_entity=args.wandb_entity, | |
| wandb_run_name=args.wandb_run_name, | |
| wandb_tags=args.wandb_tags.split(",") if args.wandb_tags else None, | |
| wandb_group=group, | |
| ) | |
| counts = run_local_census(cfg, hf_token=token) | |
| print(f"[census] wrote records per layer: {counts}") | |
| # 2. Finalize any leftovers | |
| print("\n[2/6] finalize census chunks") | |
| import time as _time | |
| _fin_t0 = _time.time() | |
| _n_tmp = _finalize_dir(args.outdir) | |
| if _n_tmp and args.wandb_project: | |
| if _begin_stage_run(args, group, "finalize", | |
| {"n_tmp_dirs": _n_tmp}, tags=["finalize"]): | |
| from qwip_atlas.atlas_wandb import wsummary, finish_wandb | |
| wsummary({ | |
| "final/finalize_sec": _time.time() - _fin_t0, | |
| "final/finalize_n_tmp": _n_tmp, | |
| }) | |
| finish_wandb() | |
| _pause("census finalize", args.pause_between_stages) | |
| # 3. Layer analysis | |
| print("\n[3/6] per-layer analysis") | |
| analysis_dir = _run_analysis( | |
| args.outdir, components, args.skip_existing_analysis, pooling=args.pooling, | |
| wandb_project=args.wandb_project, wandb_entity=args.wandb_entity, | |
| wandb_group=group, wandb_tags=args.wandb_tags.split(",") if args.wandb_tags else None, | |
| ) | |
| _pause("per-layer analysis", args.pause_between_stages) | |
| # 3b. Per-token deep-dive (optional, requires --store-per-token) | |
| per_token_report_dir = None | |
| if args.per_token_analysis: | |
| if not args.store_per_token: | |
| print("\n[3b/6] skipping per-token analysis (--store-per-token is required)") | |
| else: | |
| print("\n[3b/6] per-token deep-dive") | |
| _run_per_token_analysis( | |
| args.outdir, analysis_dir, components, model_id, | |
| token, trust_remote=args.trust_remote, | |
| ) | |
| per_token_report_dir = analysis_dir / "per_token" | |
| _pause("per-token analysis", args.pause_between_stages) | |
| else: | |
| print("\n[3b/6] skipping per-token analysis (pass --per-token-analysis to run)") | |
| # 4. OV-circuit spectral analysis | |
| ov_report = args.outdir / "ov_circuit_scores.json" | |
| if args.skip_existing_analysis and ov_report.exists(): | |
| print(f"\n[4/6] --skip-existing-analysis: {ov_report.name} exists; skipping OV-circuit analysis") | |
| else: | |
| print("\n[4/6] OV-circuit analysis") | |
| import time as _time | |
| _ov_t0 = _time.time() | |
| _run_ov_circuits(model_id, ov_report, token, trust_remote=args.trust_remote) | |
| if args.wandb_project: | |
| if _begin_stage_run(args, group, "ov", | |
| {"ov_report": str(ov_report)}, tags=["ov"]): | |
| from qwip_atlas.atlas_wandb import wsummary, wstage, finish_wandb | |
| from qwip_atlas.io import read_json | |
| _ov_summary: dict = { | |
| "final/ov_total_sec": _time.time() - _ov_t0, | |
| "final/ov_n_heads": 0, | |
| } | |
| try: | |
| _recs = read_json(ov_report) if ov_report.exists() else [] | |
| # records: [{layer, head, ov_top_singular_val, ov_eff_rank, | |
| # induction_score, qk_*, fc_*, ...}, ...] | |
| _by_layer: dict[int, list] = {} | |
| for _r in _recs: | |
| _by_layer.setdefault(int(_r.get("layer", -1)), []).append(_r) | |
| _ov_summary["final/ov_n_heads"] = len(_recs) | |
| _ov_summary["final/ov_n_layers"] = len(_by_layer) | |
| _ind_max = 0.0 | |
| for _l, _rs in sorted(_by_layer.items()): | |
| _ov_summary[f"l{_l}/ov_top_sv_max"] = float( | |
| max((_r.get("ov_top_singular_val") or 0) for _r in _rs)) | |
| _ov_summary[f"l{_l}/ov_eff_rank_mean"] = float( | |
| sum((_r.get("ov_eff_rank") or 0) for _r in _rs) / max(len(_rs), 1)) | |
| _l_ind = max((_r.get("induction_score") or 0) for _r in _rs) | |
| _ov_summary[f"l{_l}/induction_max"] = float(_l_ind) | |
| _ind_max = max(_ind_max, float(_l_ind)) | |
| _ov_summary["final/ov_induction_max"] = _ind_max | |
| except Exception as _e: | |
| print(f"[ov] could not summarize report for wandb: {_e}") | |
| wstage("ov_done", _ov_t0) | |
| wsummary(_ov_summary) | |
| finish_wandb() | |
| _pause("OV-circuit analysis", args.pause_between_stages) | |
| # 5. Optional compliance-behaviour extraction | |
| compliance_report = args.outdir / "compliance_behaviour_scores.json" | |
| if not (args.positive and args.negative): | |
| print("\n[5/6] skipping compliance-behaviour (pass --positive and --negative to run)") | |
| elif args.skip_existing_analysis and compliance_report.exists(): | |
| print(f"\n[5/6] --skip-existing-analysis: {compliance_report.name} exists; skipping compliance-behaviour") | |
| else: | |
| print("\n[5/6] compliance-behaviour extraction") | |
| _run_compliance( | |
| model_id, | |
| layers, | |
| args.outdir, | |
| args.positive, | |
| args.negative, | |
| args.positive_key, | |
| args.negative_key, | |
| components, | |
| token, | |
| trust_remote=args.trust_remote, | |
| attn_implementation=args.attn_implementation, | |
| chat_template=args.chat_template, | |
| wandb_project=args.wandb_project, | |
| wandb_entity=args.wandb_entity, | |
| wandb_group=group, | |
| wandb_tags=args.wandb_tags.split(",") if args.wandb_tags else None, | |
| ) | |
| _pause("compliance-behaviour extraction", args.pause_between_stages) | |
| # 5b. Optional Sub-Zero surgery probe (DAS rotational analysis) | |
| subzero_report = args.outdir / "subzero_report.json" | |
| if args.sub_zero: | |
| print("\n[5b/6] Sub-Zero surgery probe (DAS rotational)") | |
| _run_sub_zero( | |
| model_id, | |
| args.sub_zero_corpora, | |
| args.outdir / "sub_zero_brain_atlas.json", | |
| subzero_report, | |
| token, | |
| trust_remote=args.trust_remote, | |
| attn_implementation=args.attn_implementation, | |
| chat_template=args.chat_template, | |
| pooling=args.pooling, | |
| all_layers=args.all_layers, | |
| wandb_project=args.wandb_project, | |
| wandb_entity=args.wandb_entity, | |
| wandb_run_name=args.wandb_run_name, | |
| wandb_tags=args.wandb_tags, | |
| wandb_group=group, | |
| ) | |
| _pause("Sub-Zero surgery probe", args.pause_between_stages) | |
| else: | |
| print("\n[5b/6] skipping Sub-Zero probe (pass --sub-zero to run)") | |
| # 6. Atlas build | |
| print("\n[6/6] atlas build + index") | |
| census_sample = args.outdir / f"l{layers[0]}_census_raw.npz" | |
| if not census_sample.exists(): | |
| raise SystemExit(f"[error] expected census file missing: {census_sample}") | |
| if not args.skip_existing_atlas or not (args.atlas / "manifest.json").exists(): | |
| import time as _time | |
| _atlas_t0 = _time.time() | |
| _atlas_wandb = False | |
| if args.wandb_project: | |
| _atlas_wandb = _begin_stage_run( | |
| args, group, "atlas", | |
| {"atlas_dir": str(args.atlas), "components": sorted(components)}, | |
| tags=["atlas"], | |
| ) | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wstage as _wstage_atlas | |
| _atlas_t0 = _wstage_atlas("atlas_init", _atlas_t0) | |
| _atlas_init(args.atlas, model_id, census_sample, token) | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wstage as _wstage_atlas | |
| _wstage_atlas("atlas_merge_layers", _time.time()) | |
| _atlas_merge_layers(args.atlas, args.outdir, analysis_dir) | |
| if ov_report.exists(): | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wstage as _wstage_atlas | |
| _wstage_atlas("atlas_merge_ov", _time.time()) | |
| _atlas_merge_ov(args.atlas, ov_report) | |
| if compliance_report.exists(): | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wstage as _wstage_atlas | |
| _wstage_atlas("atlas_merge_compliance", _time.time()) | |
| _atlas_merge_compliance(args.atlas, compliance_report) | |
| if subzero_report.exists(): | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wstage as _wstage_atlas | |
| _wstage_atlas("atlas_merge_subzero", _time.time()) | |
| _atlas_merge_subzero(args.atlas, subzero_report) | |
| # 6b. Optional logit-lens projection | |
| logit_lens_report = args.outdir / "logit_lens_scores.json" | |
| if args.logit_lens: | |
| print("\n[6b/6] logit-lens projection") | |
| _run_logit_lens(model_id, args.atlas, logit_lens_report, token, trust_remote=args.trust_remote) | |
| if logit_lens_report.exists(): | |
| _atlas_merge_logit_lens(args.atlas, logit_lens_report) | |
| _pause("logit-lens projection", args.pause_between_stages) | |
| else: | |
| print("\n[6b/6] skipping logit-lens (pass --logit-lens to run)") | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wstage as _wstage_atlas | |
| _wstage_atlas("atlas_index", _time.time()) | |
| _atlas_index(args.atlas) | |
| if _atlas_wandb: | |
| from qwip_atlas.atlas_wandb import wsummary, finish_wandb | |
| from qwip_atlas.io import read_json | |
| _atlas_summary = {"final/atlas_total_sec": _time.time() - _atlas_t0} | |
| try: | |
| _manifest = read_json(args.atlas / "manifest.json") | |
| _atlas_summary["final/atlas_n_layers"] = int(len(_manifest.get("layers", []))) | |
| _atlas_summary["final/atlas_n_subzero_layers"] = int(len(_manifest.get("subzero_layers", []))) | |
| _atlas_summary["final/atlas_model_id"] = str(_manifest.get("model_id", "")) | |
| # components_per_layer is {layer_str: [comp, ...]}; count distinct comps. | |
| _all_comps: set[str] = set() | |
| for _comps in (_manifest.get("components_per_layer") or {}).values(): | |
| _all_comps.update(_comps) | |
| _atlas_summary["final/atlas_n_components"] = len(_all_comps) | |
| except Exception as _e: | |
| print(f"[atlas] could not read manifest for wandb: {_e}") | |
| wsummary(_atlas_summary) | |
| finish_wandb() | |
| else: | |
| print(f"[atlas] manifest exists and --skip-existing-atlas set; skipping build") | |
| print("\n" + "=" * 70) | |
| print(" DONE") | |
| print(f" census: {args.outdir}") | |
| print(f" analysis: {analysis_dir}") | |
| if per_token_report_dir is not None: | |
| print(f" per-token: {per_token_report_dir}") | |
| print(f" atlas: {args.atlas}") | |
| print("=" * 70) | |
| if __name__ == "__main__": | |
| main() | |