atlasing / app.py
juiceb0xc0de's picture
Expose chat template flag in app
8662566
Raw
History Blame Contribute Delete
40.8 kB
#!/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()