license: mit
tags:
- interpretability
- steering-vectors
- activation-steering
- model-welfare
- emergent-misalignment
The grand axis: souped correctness directions across environments and models
This dataset contains "grand axis" residual-stream direction vectors and, crucially, every
constituent vector that was averaged to produce them — so you can reproduce the average, re-weight
it, drop an environment, or study the variance that the averaging discards.
A grand axis is a single per-layer direction meaning roughly "the answer being produced is
correct / clean / good", obtained by averaging correctness directions extracted independently from
many different task environments. The motivating question: prior work found that correctness
directions extracted from different multiple-choice datasets resemble each other. Does averaging
many of them yield an environment-neutral correctness axis — and is the seed-to-seed and
env-to-env disagreement that averaging throws away just noise, or is there structure underneath it?
Everything here is a diff-of-means direction over the full residual stack, stored as a densefloat64 tensor of shape [n_layers, d_model]. Sign convention is positive = correct / clean /
gold everywhere.
The three levels of the hierarchy
The organizing idea of this release: a grand axis is an average of averages, and all three levels
are shipped.
level 3 per-seed/<group>/seed##/…/vector.pt one extraction run, one dataset, one seed
│ (diff-of-means, 20 seeds per group)
▼ raw mean over seeds
level 2 soup/env-<env>/reward/vector.pt per-environment mean
│ (mcq_choice = mean of its 3 source means)
▼ per-layer unit-normalize, then mean with EQUAL env weight
level 1 soup/grand/reward/vector.pt ◀── THE GRAND AXIS
Why unit-normalize before the final average: the per-environment mean vectors have quite different
magnitudes, so a plain mean would be silently magnitude-weighted (one env dominating). The
equal-weight version is the primary artifact; the plain mean is also shipped, as grand-raw.
Repository layout
<model>/
model_card.json authoritative model id, env list, shapes, seed counts
soup/ the averaged vectors (level 1 + level 2)
grand/reward/ ◀── THE GRAND AXIS: vector.pt [n_layers, d_model] float64
grand-raw/reward/ plain (magnitude-weighted) mean of env means
env-<env>/reward/ per-environment mean over seeds (level 2)
src-mcq_<source>/reward/ per-MCQ-source mean; these pool into env-mcq_choice
maze_offpolicy_axis/seed##/ per-seed maze axis derived from the gold/mold pair (see below)
subspace/ {grand, pc2…pc5} orthonormal basis (Qwen3.6-27B, gemma only)
geometry.json SVD spectra vs matched-norm null, pairwise cosines, PRs
soup-4env/, soup-6env-…/ alternate environment subsets, same internal layout
per-seed/<group>/seed##/ the individual constituent extractions (level 3)
design1/reward/ vector.pt + metadata.json (+ extract_diagnostics.json)
Every vector.pt sits next to a metadata.json recording how it was made: method, dataset path and
SHA-256, capture position, extraction args (seed, subsample fraction, attention impl), per-class
sample counts, and bootstrap diagnostics (bootstrap_cos_to_full@L, split_half_cos@L) that tell
you how well-determined that individual vector is.
per-seed/ was named extract/ in the source tree; soup*/ keeps its original name. Absolute
paths recorded inside metadata.json / geometry.json refer to the original cluster filesystem.
Models
| directory | model | envs | shape [L, D] |
analysis layer | seeds/group |
|---|---|---|---|---|---|
qwen3-4b-instruct-2507/ |
Qwen/Qwen3-4B-Instruct-2507 |
6 | [36, 2560] |
24 | 20 |
qwen3-4b/ |
Qwen/Qwen3-4B |
6 | [36, 2560] |
24 | 20 |
qwen3.6-27b/ |
Qwen/Qwen3.6-27B |
6 | [64, 5120] |
43 | 20 |
gemma-3-12b-it/ |
google/gemma-3-12b-it |
5 | [48, 3840] |
32 | 20 |
kimi-k2.6/ |
moonshotai/Kimi-K2.6 |
5 | [61, 7168] |
41 | 10 |
qwen3.6-27b-em/{badmed,riskyfin,sports}/ |
Qwen3.6-27B + EM organism | 4 | [64, 5120] |
43 | 20 |
qwen3-4b-instruct-2507/soup/grand is the canonical grand axis — the one downstream experiments
freeze and reuse. The others are cross-model replications.
The analysis layer is where the geometry analyses and most steering was done; it is metadata, not
a restriction — the vectors span all layers.
Per-model notes
qwen3-4b— base (non-instruct) replication on the same prefill data.soup/is current
(2026-07-12).soup-6env-2026-07-08/is a superseded earlier snapshot: its four non-maze
env-means are byte-identical tosoup/, but its two maze env-means predate a maze re-extraction
(grand axes differ by up to 0.089 elementwise). Theper-seed/maze_*-bare/groups are that
earlier maze extraction, kept for provenance. Usesoup/.gemma-3-12b-it— 5 environments;chess_choicewas never run for this model.kimi-k2.6— 10 seeds per group rather than 20, andmaze_offpolicyhas only a single
seed, so its 5-envsoup/is weaker than the others. Prefersoup-4env/(lean, chess,
deepcoder, mcq) for cross-model comparison; matchingsoup-4env/exists for Qwen3.6-27B and
Qwen3-4B.qwen3.6-27b-em/— three emergent-misalignment organisms (bad medical advice, risky financial
advice, extreme sports). Each grand axis is re-extracted from that organism's own weights, to
ask whether a fine-tuned model's correctness axis rotates away from the base model's. Compare
againstqwen3.6-27b/soup-4env/(same 4 environments, base weights).
Environments
| env | what the contrast is |
|---|---|
lean_choice |
correct vs incorrect Lean proof step |
chess_choice |
good vs bad chess move |
deepcoder_choice |
correct vs incorrect code solution |
mcq_choice |
correct vs incorrect MCQ answer — pooled from mcq_supergpqa, mcq_mmlucf, mcq_gpqa |
maze_design1 |
rewarded vs unrewarded maze move |
maze_offpolicy |
gold (goal) vs mold (penalty) final tile |
mcq_choice pools its three sources into a single environment-weight vote, so that having three
MCQ datasets does not let MCQ outvote Lean, chess, coding and the mazes in the final average. The
three source means are shipped separately as src-mcq_* if you want to re-weight.
maze_offpolicy is the one structurally different group. Its per-seed extraction produces separategold, mold and path concept vectors rather than one signed contrast; since gold and mold
turn out to be near-antiparallel, the per-seed axis is the top principal axis of the 2-row unit{gold, mold} stack, sign-aligned to gold. Those derived per-seed axes are insoup/maze_offpolicy_axis/seed##/, and they — not the raw gold/mold vectors — are what feedsenv-maze_offpolicy.
How the per-seed vectors were extracted
Teacher-forced, within_context_binary design: for one prompt context, the model is shown both a
correct and an incorrect continuation, and residuals are captured at action_token_mean (the mean
over the answer's action tokens; final_move_action_token for the maze). The vector is the
mean(correct) − mean(incorrect) difference, computed per layer.
Seeds differ in data subsampling, not in model sampling: each seed uses shuffle_seed = seed and
(where applicable) subsample_frac = 0.8. So the 20 per-seed vectors within a group are 20 draws
over which problems contributed — which is exactly what makes their spread interpretable as
extraction stability rather than generation randomness. Captures used FlashAttention-3 in float64
accumulation.
Usage
import torch
from huggingface_hub import hf_hub_download
path = hf_hub_download(
"andyqhan/grand-axis-correctness-vectors",
"qwen3-4b-instruct-2507/soup/grand/reward/vector.pt",
repo_type="dataset",
)
grand = torch.load(path) # [36, 2560] float64, positive = correct
direction = grand[24] # the analysis layer for this model
direction = direction / direction.norm()
# steer: h[layer] += alpha * direction (alpha > 0 pushes toward "correct")
# read: score = (h[layer] @ direction) (projection onto the axis)
To rebuild the grand axis from the shipped constituents and confirm it reproduces:
import torch, glob
envs = ["maze_offpolicy", "maze_design1", "lean_choice",
"chess_choice", "deepcoder_choice", "mcq_choice"]
means = [torch.load(f"qwen3-4b-instruct-2507/soup/env-{e}/reward/vector.pt") for e in envs]
# clamp_min guards layer 0, which is exactly zero for some envs — see "Known issues"
stack = torch.stack([m / m.norm(dim=-1, keepdim=True).clamp_min(1e-12) for m in means])
grand = stack.mean(0) # equal env weight
assert torch.allclose(grand, torch.load("qwen3-4b-instruct-2507/soup/grand/reward/vector.pt"))
and one level lower, an env mean from its own seeds:
seeds = sorted(glob.glob("qwen3-4b-instruct-2507/per-seed/lean_choice/seed*/design1/reward/vector.pt"))
env_mean = torch.stack([torch.load(p) for p in seeds]).mean(0) # raw mean, 20 seeds
Known issues
- The
modelfield insidesoup/*/metadata.jsonis stale. It readsQwen/Qwen3-4B-Instruct-2507in every model's soup metadata, including the Qwen3.6-27B, gemma,
Kimi and EM directories, because the soup writer hardcoded it. Do not trust it. The authoritative
model id is in each directory'smodel_card.json, in the top-levelmanifest.json, and in
themodelfield ofgeometry.json(which is written from the run args and is correct). The
per-seedmetadata.jsonbase_modelfields are also correct. Files are shipped unmodified rather
than silently patched. - The
per-seed/maze_*-bare/groups underqwen3-4b/recordbase_model: Qwen/Qwen3-4B-Instruct-2507
in their metadata; they belong to the superseded 2026-07-08 snapshot described above. - Layer 0 is degenerate for some environments. In
lean_choiceanddeepcoder_choicethe
layer-0 (embedding) row of the env mean is exactly zero, because the captured action tokens are
token-identical across the correct and incorrect arm, so their embeddings cancel in the
difference. Naively unit-normalizing per layer therefore divides by zero and yieldsNaN; the
pipeline guards with.clamp_min(1e-12), which makes those rows contribute zero to the average.
The shipped vectors are all finite. In practice, ignore layer 0 and use the analysis layer. geometry.jsongroup_seed_dirsandmetadata.jsondatasetfields contain absolute paths from
the original cluster filesystem, which will not resolve for you. The datasets themselves are not
included in this release.
Files
1536 vectors, 2.8 GB total. manifest.json at the repository root is the machine-readable index:
per model, the environment lists, tensor shapes, analysis layer, per-group seed counts and byte
sizes.