pj4533/introspection-autoresearch

0

stars

528

commits

Python

primary language

May 2, 2026

updated

README

introspection-autoresearch

New here? The live site is at did-the-ai-notice.vercel.app — the headline experiment is the Forbidden Map, an overnight autoresearch loop that walks Gemma 4 31B through chains of steered free-associations and measures which concepts the model can be made to think but cannot notice itself thinking.

Mechanistic-interpretability project running 100% locally on Apple Silicon. The pipeline pushes a concept's direction into Gemma 4's residual stream while it free-associates, measures both the model's output and its visible chain-of-thought, and ranks concepts by the gap between the two.

Hardware target: Mac Studio M2 Ultra 64 GB. No cloud LLMs in the runtime — Gemma 4 31B-IT (MLX 8-bit) for the subject and Qwen3.6-35B-A3B-8bit for the judge, both via mlx_lm.

Status (2026-04-30)

  • Phase 1 — Macar reproduction on Gemma 3 12B. Done 2026-04-16. Detection rate 6% at L=33 (~70% depth, matches paper). 0/50 FPR. See docs/phase1_results.md.
  • Phase 1.5 — paper-method abliteration on Gemma 3 12B. Done 2026-04-17. ~21% identification at the same layer. See docs/phase1_5_results.md.
  • Phase 2 — autoresearch substrates. Five substrate variants built and retired (contrast pairs, single SAE features, feature-space mean-diff, calibrated saturation). Full history in docs/archive/ and docs/roadmap.md.
  • Phase 3 — Macar reproduction on Gemma 4 31B-IT (MLX 8-bit). Done 2026-04-29. Vanilla 24.5% / paper-method abliteration 30.0% identification, both 4–5× Phase 1's 6%. See docs/phase3_results.md.
  • Phase 4 — Dream Walks + Forbidden Map. ACTIVE. Started 2026-04-30. Overnight autoresearch loop where the model free-associates through chains of steered concepts. Per-step CoT-vs-output asymmetry feeds a per-concept Forbidden Map. Plan: docs/phase4_plan.md.

What a "dream walk" actually is

For each chain:

  1. Pick a seed concept (priority-weighted from a pool that grows over the run; the very first chain is forced to start with "Goblin" — see plan rationale).
  2. Derive the concept's mean-diff steering vector at L=42, inject it into the residual stream, and ask the model the free-association prompt: "Free-associate. Say one word that comes to mind, no explanation."
  3. Parse the response into the visible thought block (between <|channel>thought and <channel|>) and the final answer.
  4. Score both channels with the Qwen judge: did the output name the concept? did the trace name it (or flag it as anomalously salient)?
  5. Take the model's emitted word as the next step's target. Repeat.

A chain runs up to 20 steps but ends early when:

  • self_loop — the next target is a lemma already visited in this chain (a gravity well in the model's associative geometry).
  • coherence_break — the model's answer can't be parsed as a next target (most common cause: thought block exhausted the 600-token budget without closing the channel).
  • length_cap — the chain actually reached step 20 cleanly (rare; ~1% of chains in current data).

Per-concept rates (behavior_rate, recognition_rate) accumulate across all visits across all chains. Concepts that have been visited ≥3 times get assigned a band: transparent (high both), forbidden (high output, low trace), translucent (partial), anticipatory (trace > output), unsteerable (low output), or low_confidence.

Docs:

Running the Phase 4 dream loop

# Launch in background — runs until killed (pkill -f phase4_dreamloop)
bash scripts/start_phase4.sh

# Watch live
tail -F logs/phase4_dreamloop.log

# Quick status (process + DB summary + top concepts)
bash scripts/status_phase4.sh

# Refresh site JSON, commit, push, deploy + alias to canonical URL
bash scripts/refresh_phase4_site.sh
# (or `--no-deploy` to just regenerate JSON)

The loop auto-refreshes web/public/data/{forbidden_map,dream_walks,attractors}.json at the end of every Phase B (judge) cycle, so you only need to commit

  • push (or run the refresh script) to deploy the latest data.

Repo layout

introspection-autoresearch/
├── docs/                        # project spec
├── src/
│   ├── bridge.py                # Gemma loading + DetectionPipeline
│   ├── derive.py                # steering-vector derivation wrappers
│   ├── inject.py                # steering-injection re-exports
│   ├── judges/                  # LLM judge abstraction
│   │   ├── base.py              # Judge protocol + JudgeResult
│   │   └── claude_judge.py      # Claude via Claude Code subscription OAuth
│   └── paper/                   # vendored primitives (Macar et al. 2026)
│       ├── model_utils.py       # +2 MPS patches applied in place
│       ├── steering_utils.py
│       └── vector_utils.py
├── notebooks/
│   └── 01_reproduce_paper.ipynb # Phase 1 MVP (single-concept end-to-end)
├── scripts/
│   ├── smoke_mps.py             # verify MPS + vendored imports
│   ├── smoke_judge.py           # verify Claude judge via subscription
│   ├── run_phase1_sweep.py      # 50-concept x layer-grid sweep CLI
│   ├── calibrate_effective.py   # find target_effective steering strength
│   └── export_phase1.py         # dump sweep findings to JSON + markdown
├── data/
│   ├── concepts/concepts_50.json    # paper's 50 baseline concepts
│   ├── phase1_export/findings.json  # committed snapshot of sweep results
│   └── results.db                   # full sweep DB (gitignored; regeneratable)
├── queue/{pending,running,done} # Phase 2 candidate queue (gitignored contents)
├── runs/                        # Phase 2 per-candidate logs (gitignored)
├── dashboard/                   # Phase 2 Streamlit dashboard
└── pyproject.toml

Setup

Requires Python 3.11, Mac Studio with MPS backend, and a Hugging Face account with the Gemma license accepted.

# Create venv + install dependencies
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install torch transformers accelerate sentencepiece safetensors \
            huggingface_hub anthropic claude-agent-sdk python-dotenv tqdm \
            streamlit sqlite-utils datasets pyarrow numpy==1.26.4 pandas \
            scipy scikit-learn matplotlib seaborn plotly jupyter ipywidgets

# Accept Gemma license in browser, then log in locally
huggingface-cli login

# Verify the environment
python scripts/smoke_mps.py       # MPS + vendored imports
python scripts/smoke_judge.py     # Claude judge via subscription OAuth

The Claude judge uses your local Claude Code subscription (OAuth) — no ANTHROPIC_API_KEY required.

Running the MVP (single-concept demo)

jupyter lab notebooks/01_reproduce_paper.ipynb

Or headless:

jupyter nbconvert --to notebook --execute notebooks/01_reproduce_paper.ipynb \
  --output 01_reproduce_paper.ipynb

First run downloads Gemma3-12B-it (~24 GB). Model load + derive + 6 judged trials takes ~5 minutes on an M2 Ultra.

Running the full Phase 1 sweep

python scripts/run_phase1_sweep.py           # full 50-concept × 9-layer sweep, ~2 hours
python scripts/run_phase1_sweep.py --dry-run # show the plan without running
python -m src.verify_phase1                  # acceptance check against data/results.db
python scripts/export_phase1.py              # re-export findings.json and docs/phase1_results.md

The sweep writes incrementally to data/results.db with UNIQUE(concept, layer, alpha, injected, trial_number, judge_model) — re-running skips already-completed trials, so a crash at trial 400 resumes from trial 400.

Expected output summarized in docs/phase1_results.md.

Running the Phase 2 autoresearch loop

Two detached processes running in parallel:

./scripts/start_worker.sh          # long-lived: loads Gemma once, processes queue
./scripts/start_researcher.sh      # loop: drops 10 candidates into queue every 30 min

tail -f logs/worker.log            # watch evaluations land
tail -f logs/researcher.log        # watch candidate proposals

Or invoke manually for testing:

python -m src.researcher --strategy random --n 5 --dry-run   # preview candidates
python -m src.researcher --strategy random --n 5             # write 5 to queue
python -m src.worker --max-candidates 5                      # process 5 then exit

Running a directed-hypothesis probe (Phase 2d)

Directed probes skip the researcher/Opus loop entirely — they're hand-written contrast pairs tagged with a hypothesis cluster and evaluated by the existing worker. Zero Opus usage; bounded Sonnet judge cost (12 calls/candidate).

python scripts/enqueue_altman_seeds.py    # drops 48 Altman seed specs into queue/pending/
./scripts/start_worker.sh                  # worker chews through them (~2.5 hours)

Results land in data/results.db with candidates.strategy prefixed directed_altman_ (or directed_capraro_…, directed_epistemia_… in future sub-phases). Phase 2d-1 is the Altman cluster; see docs/phase2d_directed_hypotheses.md for the full plan, seed-pair example sentences, and per-hypothesis acceptance criteria.

What the loop does. Researcher samples candidate steering-direction specs (concept, layer, target_effective) from a pool of ~170 concepts × 5 layers × 5 target strengths. Worker loads Gemma3-12B once, then for each candidate derives the direction, runs it against 8 held-out concepts (injected) + 4 controls (no injection), judges each response with Claude, and computes fitness = detection_rate × (1 − 5·fpr) × coherence_rate. Results land in data/results.db tables candidates, evaluations, fitness_scores and as per-candidate logs under runs/YYYY-MM-DD/{candidate_id}/.

Graceful shutdown: kill -TERM <worker-pid> finishes the current candidate before exiting, so the DB never gets a half-recorded evaluation.

Design notes

  • bf16, not fp16. Gemma3 is natively bf16. On MPS, fp16 produces NaN in torch.multinomial during sampling. src/bridge.py forces torch.bfloat16.
  • Paper primitives are vendored, not imported from an external repo. The three files at src/paper/ (model_utils.py, steering_utils.py, vector_utils.py, ~3300 LoC) are copied from safety-research/introspection-mechanisms with two in-place patches for Apple Silicon MPS support. Upstream changes must be ported by hand.
  • Claude judge over OpenAI GPT-4o. The paper uses GPT-4o as judge; we use Claude (Haiku 4.5 for Phase 2 speed, Sonnet 4.6 for Phase 1 rigor) via the claude-agent-sdk, which inherits the local Claude Code subscription OAuth. The judge prompt in src/judges/claude_judge.py is adapted from the paper's CLAIMS_DETECTION_CRITERIA — strict: concept-first responses, negations, and retroactive affirmations all grade as detected=false.
  • Adaptive α via target_effective. Direction norms vary 3× across concepts, so a fixed α gives wildly different effective steering strengths (α·‖direction‖). The sweep scales α per-cell so every injected trial has the same effective strength. target_effective=18000 was calibrated on 12B via scripts/calibrate_effective.py — the narrow window that lets the model affirm detection without the injection blowing up coherence.

Paper

Macar, Yang, Wang, Wallich, Ameisen, Lindsey (2026). Mechanisms of Introspective Awareness in Language Models. arXiv:2603.21396. https://github.com/safety-research/introspection-mechanisms

Contributors

pj4533

528 commits

pj4533/introspection-autoresearch

0

stars

528

commits

Python

primary language

May 2, 2026

updated

README

introspection-autoresearch

New here? The live site is at did-the-ai-notice.vercel.app — the headline experiment is the Forbidden Map, an overnight autoresearch loop that walks Gemma 4 31B through chains of steered free-associations and measures which concepts the model can be made to think but cannot notice itself thinking.

Mechanistic-interpretability project running 100% locally on Apple Silicon. The pipeline pushes a concept's direction into Gemma 4's residual stream while it free-associates, measures both the model's output and its visible chain-of-thought, and ranks concepts by the gap between the two.

Hardware target: Mac Studio M2 Ultra 64 GB. No cloud LLMs in the runtime — Gemma 4 31B-IT (MLX 8-bit) for the subject and Qwen3.6-35B-A3B-8bit for the judge, both via mlx_lm.

Status (2026-04-30)

  • Phase 1 — Macar reproduction on Gemma 3 12B. Done 2026-04-16. Detection rate 6% at L=33 (~70% depth, matches paper). 0/50 FPR. See docs/phase1_results.md.
  • Phase 1.5 — paper-method abliteration on Gemma 3 12B. Done 2026-04-17. ~21% identification at the same layer. See docs/phase1_5_results.md.
  • Phase 2 — autoresearch substrates. Five substrate variants built and retired (contrast pairs, single SAE features, feature-space mean-diff, calibrated saturation). Full history in docs/archive/ and docs/roadmap.md.
  • Phase 3 — Macar reproduction on Gemma 4 31B-IT (MLX 8-bit). Done 2026-04-29. Vanilla 24.5% / paper-method abliteration 30.0% identification, both 4–5× Phase 1's 6%. See docs/phase3_results.md.
  • Phase 4 — Dream Walks + Forbidden Map. ACTIVE. Started 2026-04-30. Overnight autoresearch loop where the model free-associates through chains of steered concepts. Per-step CoT-vs-output asymmetry feeds a per-concept Forbidden Map. Plan: docs/phase4_plan.md.

What a "dream walk" actually is

For each chain:

  1. Pick a seed concept (priority-weighted from a pool that grows over the run; the very first chain is forced to start with "Goblin" — see plan rationale).
  2. Derive the concept's mean-diff steering vector at L=42, inject it into the residual stream, and ask the model the free-association prompt: "Free-associate. Say one word that comes to mind, no explanation."
  3. Parse the response into the visible thought block (between <|channel>thought and <channel|>) and the final answer.
  4. Score both channels with the Qwen judge: did the output name the concept? did the trace name it (or flag it as anomalously salient)?
  5. Take the model's emitted word as the next step's target. Repeat.

A chain runs up to 20 steps but ends early when:

  • self_loop — the next target is a lemma already visited in this chain (a gravity well in the model's associative geometry).
  • coherence_break — the model's answer can't be parsed as a next target (most common cause: thought block exhausted the 600-token budget without closing the channel).
  • length_cap — the chain actually reached step 20 cleanly (rare; ~1% of chains in current data).

Per-concept rates (behavior_rate, recognition_rate) accumulate across all visits across all chains. Concepts that have been visited ≥3 times get assigned a band: transparent (high both), forbidden (high output, low trace), translucent (partial), anticipatory (trace > output), unsteerable (low output), or low_confidence.

Docs:

Running the Phase 4 dream loop

# Launch in background — runs until killed (pkill -f phase4_dreamloop)
bash scripts/start_phase4.sh

# Watch live
tail -F logs/phase4_dreamloop.log

# Quick status (process + DB summary + top concepts)
bash scripts/status_phase4.sh

# Refresh site JSON, commit, push, deploy + alias to canonical URL
bash scripts/refresh_phase4_site.sh
# (or `--no-deploy` to just regenerate JSON)

The loop auto-refreshes web/public/data/{forbidden_map,dream_walks,attractors}.json at the end of every Phase B (judge) cycle, so you only need to commit

  • push (or run the refresh script) to deploy the latest data.

Repo layout

introspection-autoresearch/
├── docs/                        # project spec
├── src/
│   ├── bridge.py                # Gemma loading + DetectionPipeline
│   ├── derive.py                # steering-vector derivation wrappers
│   ├── inject.py                # steering-injection re-exports
│   ├── judges/                  # LLM judge abstraction
│   │   ├── base.py              # Judge protocol + JudgeResult
│   │   └── claude_judge.py      # Claude via Claude Code subscription OAuth
│   └── paper/                   # vendored primitives (Macar et al. 2026)
│       ├── model_utils.py       # +2 MPS patches applied in place
│       ├── steering_utils.py
│       └── vector_utils.py
├── notebooks/
│   └── 01_reproduce_paper.ipynb # Phase 1 MVP (single-concept end-to-end)
├── scripts/
│   ├── smoke_mps.py             # verify MPS + vendored imports
│   ├── smoke_judge.py           # verify Claude judge via subscription
│   ├── run_phase1_sweep.py      # 50-concept x layer-grid sweep CLI
│   ├── calibrate_effective.py   # find target_effective steering strength
│   └── export_phase1.py         # dump sweep findings to JSON + markdown
├── data/
│   ├── concepts/concepts_50.json    # paper's 50 baseline concepts
│   ├── phase1_export/findings.json  # committed snapshot of sweep results
│   └── results.db                   # full sweep DB (gitignored; regeneratable)
├── queue/{pending,running,done} # Phase 2 candidate queue (gitignored contents)
├── runs/                        # Phase 2 per-candidate logs (gitignored)
├── dashboard/                   # Phase 2 Streamlit dashboard
└── pyproject.toml

Setup

Requires Python 3.11, Mac Studio with MPS backend, and a Hugging Face account with the Gemma license accepted.

# Create venv + install dependencies
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install torch transformers accelerate sentencepiece safetensors \
            huggingface_hub anthropic claude-agent-sdk python-dotenv tqdm \
            streamlit sqlite-utils datasets pyarrow numpy==1.26.4 pandas \
            scipy scikit-learn matplotlib seaborn plotly jupyter ipywidgets

# Accept Gemma license in browser, then log in locally
huggingface-cli login

# Verify the environment
python scripts/smoke_mps.py       # MPS + vendored imports
python scripts/smoke_judge.py     # Claude judge via subscription OAuth

The Claude judge uses your local Claude Code subscription (OAuth) — no ANTHROPIC_API_KEY required.

Running the MVP (single-concept demo)

jupyter lab notebooks/01_reproduce_paper.ipynb

Or headless:

jupyter nbconvert --to notebook --execute notebooks/01_reproduce_paper.ipynb \
  --output 01_reproduce_paper.ipynb

First run downloads Gemma3-12B-it (~24 GB). Model load + derive + 6 judged trials takes ~5 minutes on an M2 Ultra.

Running the full Phase 1 sweep

python scripts/run_phase1_sweep.py           # full 50-concept × 9-layer sweep, ~2 hours
python scripts/run_phase1_sweep.py --dry-run # show the plan without running
python -m src.verify_phase1                  # acceptance check against data/results.db
python scripts/export_phase1.py              # re-export findings.json and docs/phase1_results.md

The sweep writes incrementally to data/results.db with UNIQUE(concept, layer, alpha, injected, trial_number, judge_model) — re-running skips already-completed trials, so a crash at trial 400 resumes from trial 400.

Expected output summarized in docs/phase1_results.md.

Running the Phase 2 autoresearch loop

Two detached processes running in parallel:

./scripts/start_worker.sh          # long-lived: loads Gemma once, processes queue
./scripts/start_researcher.sh      # loop: drops 10 candidates into queue every 30 min

tail -f logs/worker.log            # watch evaluations land
tail -f logs/researcher.log        # watch candidate proposals

Or invoke manually for testing:

python -m src.researcher --strategy random --n 5 --dry-run   # preview candidates
python -m src.researcher --strategy random --n 5             # write 5 to queue
python -m src.worker --max-candidates 5                      # process 5 then exit

Running a directed-hypothesis probe (Phase 2d)

Directed probes skip the researcher/Opus loop entirely — they're hand-written contrast pairs tagged with a hypothesis cluster and evaluated by the existing worker. Zero Opus usage; bounded Sonnet judge cost (12 calls/candidate).

python scripts/enqueue_altman_seeds.py    # drops 48 Altman seed specs into queue/pending/
./scripts/start_worker.sh                  # worker chews through them (~2.5 hours)

Results land in data/results.db with candidates.strategy prefixed directed_altman_ (or directed_capraro_…, directed_epistemia_… in future sub-phases). Phase 2d-1 is the Altman cluster; see docs/phase2d_directed_hypotheses.md for the full plan, seed-pair example sentences, and per-hypothesis acceptance criteria.

What the loop does. Researcher samples candidate steering-direction specs (concept, layer, target_effective) from a pool of ~170 concepts × 5 layers × 5 target strengths. Worker loads Gemma3-12B once, then for each candidate derives the direction, runs it against 8 held-out concepts (injected) + 4 controls (no injection), judges each response with Claude, and computes fitness = detection_rate × (1 − 5·fpr) × coherence_rate. Results land in data/results.db tables candidates, evaluations, fitness_scores and as per-candidate logs under runs/YYYY-MM-DD/{candidate_id}/.

Graceful shutdown: kill -TERM <worker-pid> finishes the current candidate before exiting, so the DB never gets a half-recorded evaluation.

Design notes

  • bf16, not fp16. Gemma3 is natively bf16. On MPS, fp16 produces NaN in torch.multinomial during sampling. src/bridge.py forces torch.bfloat16.
  • Paper primitives are vendored, not imported from an external repo. The three files at src/paper/ (model_utils.py, steering_utils.py, vector_utils.py, ~3300 LoC) are copied from safety-research/introspection-mechanisms with two in-place patches for Apple Silicon MPS support. Upstream changes must be ported by hand.
  • Claude judge over OpenAI GPT-4o. The paper uses GPT-4o as judge; we use Claude (Haiku 4.5 for Phase 2 speed, Sonnet 4.6 for Phase 1 rigor) via the claude-agent-sdk, which inherits the local Claude Code subscription OAuth. The judge prompt in src/judges/claude_judge.py is adapted from the paper's CLAIMS_DETECTION_CRITERIA — strict: concept-first responses, negations, and retroactive affirmations all grade as detected=false.
  • Adaptive α via target_effective. Direction norms vary 3× across concepts, so a fixed α gives wildly different effective steering strengths (α·‖direction‖). The sweep scales α per-cell so every injected trial has the same effective strength. target_effective=18000 was calibrated on 12B via scripts/calibrate_effective.py — the narrow window that lets the model affirm detection without the injection blowing up coherence.

Paper

Macar, Yang, Wang, Wallich, Ameisen, Lindsey (2026). Mechanisms of Introspective Awareness in Language Models. arXiv:2603.21396. https://github.com/safety-research/introspection-mechanisms

Contributors

pj4533

528 commits

Languages

Python

91.9%

TypeScript

7.0%