- Scope:
results/qwen25_fv/gets the four claims it lacked — FV (write-feature) ablation, read-feature ablation, read-injection → write-feature formation, FV presence vs accuracy — with the GPT-J methodology unchanged (same scripts, parameterised; only model, layers, dims, pool). Pool = the 96-task pruned split (77/19), FV = the 140-headext_steerability_qwen25_96selection. - Read band for the task-unique direction: û_A = unit mean of the carrier-removed L11–13 label-token means (Qwen read-steering peak L12 ± 1; GPT-J used {5,6,7} around its L6 peak). The steering vector s_A = c + u_A uses the same band. Only the û_A rows are ported — the rank-1 raw m̂_A row and the attention-mask control of the GPT-J Appendix D table are NOT.
- No a-priori write / presence layer. The presence capture stores all 28 block outputs; the
headline layer quoted in tables and figures (Studies B and D) is the layer where the 6-shot
mean cos(cue residual, v̂_A) profile peaks, written to
write_feature_and_model_accuracy/headline_layer.txtand stated wherever a number is quoted. The GPT-J band variants (max/mean over L9–20) stay for comparability. (The read→write ridge's best read layer, L20–22, is a read-side quantity and is NOT a write layer.) - Counterfactual pairing: the 68 tasks shared with the 69-pool keep their GPT-J family tags;
the 28 new tasks were tagged by a Haiku subagent with the make_cf_task_pairs.py prompt (the
8 tags Haiku assigned differently for shared tasks were discarded). Same seeded different-family
sampling over the sorted 96-task list →
artifacts/qwen25_fv/cf_task_pairs.json. - Baselines: Qwen has no sixshot_dummy CSV, so the FV-ablation eval carries in-run, seed-matched
zero_shot(0-demo prompt) andreal{n}_baselineconditions; the read-ablation plots read those (same prompts, same T=1 readout) — the analogue of GPT-J copying its baselines from another run. - Compute pattern (user request): a cheap orchestrator GPU pod on the shared volume runs every
CPU-side step and dispatches sharded, resumable chains to a fleet of RTX PRO 4500 workers
(
logs/qwen25_fv/port/). Never touch pods this session did not create.
2026-09-03 — Task-unique part of the read feature = mean of the carrier-removed L5–7 residuals (user decision)
- For each ℓ ∈ {5,6,7}:
$\hat c(\ell)$ = unit cross-task mean of$m_A(\ell)$ (shared carrier direction);$r_A(\ell) = m_A(\ell) - \langle m_A(\ell), \hat c(\ell)\rangle \hat c(\ell)$ (carrier PROJECTED OUT, not subtracted).$u_A = \tfrac13 \sum_\ell r_A(\ell)$ is the task-unique part — a vector with natural magnitude (median ‖u_A‖ 28 vs ‖c‖ 47). -
Ablation object:
$\hat u_A = u_A/‖u_A‖$ (bankA/meanresid_top1_bases.pt). Steering object:$s_A = c + u_A$ with$c$ the layer-averaged L5–7 carrier (bankA/carrier_plus_meanresid_vectors.pt); no coefficient to fit. - Supersedes the SVD definition (top singular direction
$v_1$ of the three unit-normed residuals;$n_A = \langle \bar m_A - c, v_1\rangle$ ;$c + n_A v_1$ ): the two agree at |cos| median 0.9993, ‖u_A‖/|n_A| ≈ 1.001, and give identical ablation/steering numbers (±0.002). Reason for the switch: simpler to explain, no arbitrary SVD sign, natural magnitude. The SVD variant stays in App I as a labelled row; App F/K/presence prose notes the equivalence where their numbers were computed with$v_1$ . - 2026-09-03 (later): every downstream result that had been computed with the SVD
$v_1$ (layer sweep, 6-shot by layer,$w_A$ variant, presence maps, Claim-4 cue effect, swap, App K cos) was re-run with$\hat u_A$ /$s_A$ and reproduces to ±0.005 — the paper reports only the mean-residual numbers (WORKLOG 2026-09-03). The swap's α is now in natural units ($\alpha u_A$ , α=1 = natural magnitude), not$s_1$ units. - Claim 6 (read→write map) is likewise stated on
$u_A$ : after train-mean centering the carrier drops out, so X =$u_A$ (task level and per prompt). Raw per-layer$m_A(\ell)$ maps stay only as the App G layer-readability sweep and one App J reference sentence. - Notation in the draft:
$u_A$ = task-unique part,$s_A$ = steering vector. (Before 2026-09-03,$u_A$ denoted the steering vector$c + n_A v_1$ — WORKLOG 2026-09-02 entries use that older meaning.)
- The read feature
$m_A(\ell)$ is estimated fromlabel_resid_means: the residual (block output) at the LAST token of the FINAL demonstration's target, averaged over the 150 clean 10-shot prompts. This is the paper's convention everywhere: ablation bases, steering vectors, regression inputs, presence maps. - The ten-site-average estimator (
label_avg10*, mean over all 10 demo targets) is retired to an appendix comparison (paper-draft Appendix K). It is a lower-noise estimate of the same feature (top-1 directions match at |cos| median 0.979) and gives sharper numbers where estimation precision matters (few-direction bases, ridge X) — quote it only as the estimator variant, never as the definition. - Canonical artifacts: bases in
bottom_up_ablation/bankA/, eval runs inbankA_*subdirs, per-prompt X inlabel_resid_perprompt/. Plot scripts takeBANKA=1to read them; the results/ deliverables are regenerated from bank (a) as of 2026-09-01. -
Headline ablation object (user decision 2026-09-01): the L5–7 top-1 task-unique direction
(
bankA/L5to7_top1_bases.pt) — the same$v_1$ the composite steering vector boosts, so necessity and sufficiency are stated for one direction. The 11-dir/top-3/top-1 (L5–15) and L6–9 variants stay in the App F ladder as comparisons. -
Headline steering object (user decision 2026-09-02):
$u_A = c + n_A v_1$ — the task-agnostic L5–7 carrier plus the same ablated$v_1$ at its natural coordinate (bankA/carrier_plus_top1_vectors.pt,build_carrier_plus_top1_vectors.py); injected at L0 (6-shot 0.570 @α=2, 0.596 best). Full mean, mean-free, swap,$w_A$ = App I variants. - Not yet re-run under (a): App G's detailed ridge sub-analyses (seed splits, centroid decomposition, oracle) and App I's carrier-gap hypothesis tests — marked with convention notes in the draft.
- The paper draft (
write_up/read_and_write_features_for_in_context_learning_paper_draft.md) and every figure it embeds say target token(s) — never "label token(s)". Same for derived phrases: target slots, demo targets, pre-target, dummy-target scaffold, random-target scaffold, mixed-task mixed-target 10-shot, gold target. - Applies to visible plot text (titles, legends, axis labels, annotations). NOT renamed:
artifact/results paths and setting keys (
label_resid_means,labeltoken_fv_ridge,sixshot_randomlabel,test_mixedtask10, …), task names (pos_label,verb_tense_label), and code variable names. - Figure scripts updated + figures regenerated 2026-08-31 (see WORKLOG). The circuit SVG
(
write_up/graphics/icl_read_write_circuit.svg) also uses "target".
- New top-level bucket
results/qwen25_fv/(QWEN25_FV_DIR): repeat of the GPT-J 69-task FV pipeline (competence screen → pooled sparse head selection → zs/shuf/mix steering eval → prune-refit) on Qwen2.5-7B-Instruct. Sibling of chat_template_transfer, not inside it. - Prompt convention: base sampling, NO prefix. Qwen prompts start directly with
Q:— no chat template, no<|endoftext|>literal (that token is Qwen's PAD; Qwen trains with no BOS). The GPT-J<|endoftext|>-prefix convention is now dispatched per model family viamodel_utils.use_bos_literal(GPT-2-family only) — GPT-J runs are bit-identical. - λ grid extended to [0.002, 0.005, 0.01, 0.05, 0.2] (both GPT-J runs chose the boundary 0.005; superset keeps comparability).
- Prune threshold is user-adjudicated this time: after round 1 the zs_best distribution is shown to the user, who picks the threshold (GPT-J used auto zs_best < 0.4). Qwen-pruned tasks are recorded ONLY in the qwen25 split JSON + results README — dataset_files/ is not touched (the GPT-J pool stays intact).
- Qwen pool: 104/117 manifest tasks pass acc6 ≥ 0.30 (screen = the ext117 plain-arm artifacts,
same locked protocol; adapter
make_qwen25_acc6_csv.py). Split seed 42 → 83/21. Capped small tasks kept (next_in_group/next_in_period: 52 train prompts; capture assert relaxed to ≥40 + all-consumed).
2026-08-28 — New research branch: chat-template transfer → results/chat_template_transfer/ (user decision)
-
New live branch, separate folder by explicit user decision: does ICL transfer when demos are chat turns (user = input, assistant = label) instead of
Q:/A:? Results go in the new top-levelresults/chat_template_transfer/(CHAT_TEMPLATE_TRANSFER_DIRin paths.py) — NEVER intoresults/69_task_run/("should not pollute" the settled line) and not exploratory (it is live, forward-looking work). -
Model: Qwen/Qwen2.5-7B-Instruct (bf16). No thinking mode exists for Qwen2.5. System-prompt handling is an experimental variable: arms
chat_blank_system(explicit empty system message — Qwen2.5's template otherwise auto-inserts its default "You are Qwen, …") andchat_no_system(system block stripped entirely), plus aplainQ:/A: arm on the same model as the model-change control. Exact rendered chat strings user-confirmed 2026-08-28 (country-capital example);eval_chat_template_ext117.assert_templateguards the rendering at run time. -
Protocol = the locked extended n-shot sweep (
compute_extended_nshot_sampled.py, 2026-08-13): 117-task manifest pool (user chose the FULL pool, not the filtered 69), n=6 (user revised from the initial 10), 50 prompts/task with the identical sha256 sampling seeds, T=1.0 pure ancestral sampling, first-line/strip/exact-match metric. Main deliverable = ranked bar chart in the style ofextended_tasks_nshot_sweep/nshot_bar_6shot.png. -
The mainstream research line is the 69-task read/write-feature study (write feature = 37-head FV, read feature = early-layer label-token mean). Mainstream results buckets:
results/69_task_run/andresults/steering_vector_comparison/.write_up/is entirely mainstream. Anything essential to 69_task_run work counts as mainstream regardless of where it lives (e.g.src/sandbox/ext_steerability/— its compute backend — andsrc/eval_scripts/regress_activation_to_fv_fulldim_ridge.py, a ridge library the labeltoken-ridge scripts import). -
Retired directions moved to
results/exploratory/(git mv, history preserved):direction1_ambiguous/,direction2_label_geometry/,direction3_fv_formation/,general/. They did not pan out; kept for possible revisits, but NEVER build new results on them without explicit user promotion (same rule as sandbox). Seeresults/exploratory/README.md. -
Their scripts moved to
src/eval_scripts/exploratory/(129 files): every script whose only results bucket was a moved one (by grep on theAMBIGUOUS_DIR/LABEL_GEOMETRY_DIR/FV_FORMATION_DIR/GENERAL_DIRconstants), plus artifacts-only compute scripts of those directions (paired captures, payload/preimage fits, magid plots) and the retired Qwen3 side line (run_qwen3_*,run_varicl_qwen3). Their sys.path bootstraps were bumped one level andeval_scripts.<module>imports of moved modules rewritten toeval_scripts.exploratory.<module>. -
results/sandbox/andsrc/sandbox/are unchanged (already quarantined by the 2026-08-06/08 decisions). -
paths.pyis why this move was cheap: the four bucket constants were reparented under the newEXPLORATORY_ROOT; no runtime code hardcoded the old paths, so scripts on old branches pick up the new locations automatically after merging main. Docstring/help-text path mentions insrc/were sed-updated; WORKLOG/DECISIONS history intentionally keeps old paths (prependexploratory/when reading pre-2026-08-28 entries).
- FV ablation study design (user decisions): ablate the unit 37-head task-FV direction
from the residual stream at the FINAL CUE token of 6-shot prompts, prefill only; ops =
zero-projection AND mean-ablation; BOTH layer clamps L9–27 and L0–27 reported side by side;
readout = T=1 sampled exact match (sixshot_dummy protocol); counterfactual control =
the shared
artifacts/69_task_run/bottom_up_ablation/cf_task_pairs.json(different semantic family, Haiku-assigned, used verbatim by both ablation studies). - Mean-ablation reference at the cue token =
grand_mean_cue6.pt: equal-task-weighted grand mean over all 69 tasks of cue-token block-input residuals of the same 6-shot prompts (per layer). Note this differs from the pc50 label-token grand mean (token-count weighted) and lives at a different site — don't mix them. - LESSON — stored T=1 sampled baselines don't reproduce exactly across runs even with
shared crc32 seeds (fp16 logit differences flip borderline samples; antonym real6 0.36 vs
stored 0.32). When comparing ablation arms against a baseline, recompute the baseline
in-run (
--with_baseline) so all arms share the run's exact logit stream; use stored numbers only as floors/references. - LESSON — GPT-J snapshot glob is unsafe: the volume HF cache now has a second, stale
snapshot dir (
f3f..., only a stray model.safetensors) thatsorted(glob)[-1]picks over the complete47e...one → "Unrecognized model" errors. Pass--model_dir(or pin the snapshot hash) in pod drivers. - Generation-stage OOM guard for long-prompt tasks (ag_news) on 31 GiB cards: token_budget
11000 / batch_cap 16 (pc50 values) +
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True.
- All future train/test splits and downstream experiments on extended tasks use ONLY the
69-task working pool unless the user explicitly asks otherwise. The canonical split is
task_splits/extended_steerable_69_prunedfail.json(seed 43, 55 train / 14 heldout). Do NOT build new splits from the old 90-taskextended_steerable_90.json, and do NOT reintroduce the pruned tasks implicitly. - Provenance: 138 generated tasks → 48 fail the 6-shot ≥ 0.30 competence filter (kept in
dataset_files/extended_tasks/as documentation, grey in the figures) → of the 90 competent tasks, 21 could not be steered by the pooled sparse-opt FV (zero-shot best-layer acc < 0.4). Those 21 are MOVED todataset_files/extended_tasks/head_intensive_pruned_tasks/(own manifest + README with per-task pooled-zs labels) and are out of the working pool — to be revisited later as their own study, never silently mixed back in. - The main
manifest.json(117 tasks) defines the working pool; the plotting scripts (plot_extended_nshot_{sampled,bar}.py,plot_extended_tasks_catalog.py) filter to it. - Current pooled head selection on this pool: 37 heads (λ=0.005), from
artifacts/sandbox/ext_steerability/prunedfail_seed43/pooled_sparse/selection.json; steers 69/69 tasks ≥ 0.4 zero-shot (train .748 / heldout .734 mean best-layer).
2026-08-12 — LESSON: wordfreq/lemminflect mechanical dataset draws aren't reproducible across environments; freeze the final curated word list
- While strictly recurating
dataset_files/extended_tasks/countable_uncountable.json(audit found ~186 dual count/mass-sense nouns mislabeled countable, e.g.water, blood, energy, plus a non-nounonce, plus 4 mislabeled uncountable —childhood, adulthood, yogurt, theft), reran the task's generator'sbuild_uncountable()/build_countable()(which gate candidates onwordfreq.zipf_frequency) in this repo'spython3.12(wordfreq 3.1.1) and got a different total (1076, not the original 1000) than the file already on disk — several borderline zipf values evidently differ from whatever environment/library version produced the original file, flipping which candidates the mechanical filter drops. - Fix applied: for
countable_uncountable.py, froze the final, already-audited word lists as literal data (_resources/generators/_frozen_lists.py) and mademain()emit those directly instead of recomputing the mechanical draw at runtime. The draw functions/candidate lists stay in the generator as documented provenance but are offmain()'s execution path. - Any other
generation_method="knowledge"generator that gates onwordfreq.zipf_frequencyorlemminflect.getInflectionfor its final output (not just as a one-time curation aid) has this same latent reproducibility risk. Once a word list has been manually audited/accepted, freeze it as literal data rather than trusting the mechanical filter to reproduce the exact same set on a future rerun in a different environment.
- Canonical dataset files overwritten (originals appended-to, backups kept): present-past
293→885, singular-plural 205→1034, prev/next_item 225→674 each (script-generated sequence
families, mirrored/inverse-checked), person-sport 318→733, person-instrument 510→670,
product-company 522→906, national_parks 451→454 (NPS inventory exhausted). All LLM-generated
entries restricted to pre-2021 well-known entities; contested forms excluded. Backups:
dataset_files/abstractive/<task>.pre_expansion_2026-08-12.json(original = byte-identical prefix of the new file). country-capital/country-currency deliberately unchanged (hard-limited inventories). Old FV/capture artifacts built from the smaller datasets are unaffected on disk but no longer reflect the full example pools. - No-validation example split:
load_dataset(..., merge_valid_into_train=True)merges the 9% valid split into train (train 79% / test 21%); TEST MEMBERSHIP IS BIT-IDENTICAL to the old two-stage split. Opt-in flag (default unchanged); returned dict has no 'valid' key so stale callers fail loudly. New isolation-methods work uses this split. dataset_files/isolation_prompts/<task>/(29 tasks, generatordataset_files/generate/generate_isolation_prompts.py, seed 42, pools deduplicated by input): 150 fixed 10-shot train prompts (demos+query from TRAIN, correct labels, distinct queries) + 30 test prompts per setting {zeroshot, sametask_shuffled10, mixedtask10 (10 demos from 10 distinct other tasks, correct pairs, all 28 eligible)}; the SAME 30 test queries across settings (paired design). Queries from TEST, demos always from TRAIN. See write_up/isolation_methods_levers.md for the study context.
-
src/sandbox/sparse_head_selection/implements Hu et al. 2025 (arXiv:2505.05145 §3.1) joint sparse head selection on GPT-J (73 heads at c>0.2, λ=0.01 by leave-one-task-out CV), andvanilla_sparse_opt23= the SANDBOX FV definition built from the 23 heads with c > 0.8: UNWEIGHTED sum of varicl mean head outputs (canonical stage-2 construction, the learned coefficients pick heads only). FV set:artifacts/function_vectors/gpt-j/sandbox/vanilla_sparse_opt23/. -
Despite clearly beating canonical AIE selection on held-out tasks (test-task best-layer zero-shot mean 0.600 vs 0.400 for varicl top-40; LOTO train-task 0.421 vs 0.193), this is a SANDBOX trial: "function vectors" still means
train_varicl_top40(2026-07-10) and nothing may build on the sparse sets without explicit user promotion. Known weakness: loses on lexical-relation tasks (antonym, synonym). -
Method conventions fixed by the user: zero-shot queries from valid split (cap 100 / min 80, train top-up), loss = raw −log p(full label) over greedy contextualized label tokens, injection ONCE at the cue token (block-9 output), c clipped to [0,1], LOTO CV for λ.
-
Lesson: HF gradient checkpointing breaks (CheckpointError, saved-tensor count mismatch) when a forward hook injects a grad-requiring tensor inside a checkpointed block — use micro-batch gradient accumulation instead (frozen weights ⇒ only post-injection layers store activations).## 2026-08-06 — Sparse-optimization head selection is SANDBOX-only (not a new head-set standard)
-
src/sandbox/sparse_head_selection/+artifacts/sandbox/sparse_head_selection/implement Hu et al. 2025 (arXiv:2505.05145 §3.1) joint sparse head selection on GPT-J (73 heads, λ=0.01, LOTO-CV). Despite clearly beating the canonical AIE top-40 on held-out-task zero-shot steering (0.421 vs 0.193 mean acc @L9), this is a SANDBOX trial: "function vectors" still meanstrain_varicl_top40(2026-07-10 decision) and nothing may build on the sparse head set without explicit user promotion. -
Method conventions fixed by the user for any follow-up: zero-shot queries from the valid split (cap 100 / min 80 per task, top-up from train split), loss = raw −log p(full label) with greedy contextualized label tokens, injection ONCE at the cue token (block-9 output), λ by leave-one-task-out CV, c clipped to [0,1].
-
Lesson: HF gradient checkpointing breaks (CheckpointError, saved-tensor count mismatch) when a grad-requiring tensor is injected by a forward hook inside a checkpointed block — use micro-batch gradient accumulation instead (only layers after the injection store activations when the model is frozen).
- Two-step subspace-replacement op (user-specified, ordering is definitional): to steer a
source-task prompt toward a target task inside the two k=4 d_payload subspaces, FIRST set the
source-basis coords to the target task's mean coords there
(
v += (c(tgt->src) − v@B_src^T)@B_src), THEN set the target-basis coords toalpha·c(tgt->tgt). Running the target write LAST guarantees that wherever the two subspaces overlap the final state matches the steer-toward task while the source projection is minimised. alpha scales ONLY the target-write coords. - Steering targets = UNPAIRED 10-shot task means (last demo's label token, train-split
capture prompts,
capture_payload_switch_means.py) projected into the cached bases — the point of the study is that no paired-prompt Delta is needed. Projection of the mean equals the mean of the projections (linear), so storing mean coords is exact. - Eval comparability rule: task-switch steering studies with logit readout must rebuild the steer_switch_logit.py test pools verbatim (same build_pools, seed, n_test) so the alpha=0 clean baseline doubles as a reproduction check against the stored old-study baseline (reproduced here to 0.001 on both syn<->ant directions).
- Single-layer edits (block L output, capture stack row L+1); site = demo label token. See WORKLOG part 12 for findings.
results/direction3_fv_formation/is organized as:ablation/(causal projection- ablation studies, splitpreimages/vsattention_head_mechanisms/by direction source),attention_head_analysis/(observational head studies),activation_to_fv_ decoding/(ridge/PCA/OLS/cosine readability studies, controls nested under their study),activation_geometry/,preimage_analysis/(pre-image work not tied to an ablation setup). NEW outputs must go under the matching folder — do not create new top-level dirs there. Old->new path map:results/direction3_fv_formation/README.md; migration scriptlogs/direction3_reorg/migrate.sh; WORKLOG 2026-07-30 part 8. Pre-reorg paths cited in older WORKLOG/DECISIONS entries resolve via the README table.
- Subspace definition (user-decided; leading direction REDEFINED 2026-07-29): ablation
subspace = QR-orthonormalized span{m_t − g, top-k CENTERED PCs} of the task's 170
per-prompt-FV pre-images, k ∈ {0,2,3,4}; m_t = task-mean pre-image, g = grand mean over ALL
27 tasks' 4590 pre-images at that cell. QR is sequential so subspaces are nested and the
first basis vector is always unit(m_t − g). Mean-replacement target = the subspace component
of g (matched population). Banks:
fit_preimage_pca_subspace_banks.py --mean_mode taskoffset(the default), fp64 SVD/QR on CPU (CUDA gesvdj rule stays moot). - NEVER use a raw-space pre-image POINT mean as an ablation direction. Pre-image points are reconstructed as x_raw = (x_c + x̄)·std + μ, so their mean is dominated by the population-mean activation (measured cos(m_t, g) = 0.99, cross-task cos 0.97+ at early layers): its removal is task-GENERIC devastation with a pathological late-L profile, and counterfactual controls degenerate (~1.25× vs Stream W's 4–9×). Directions must be built from DIFFERENCES of points (offsets cancel: m_t − g = std·P⁺(ȳ_task − ȳ_all)) — the same reason Stream W's dx = std·dz target-inverse never contained μ. The first run used the raw mean; the user was not shown this consequence beforehand (they should have been — definitional/geometry choices are user-gated like metric choices, 2026-07-14) and mandated redefinition + in-place OVERWRITE of run-1 outputs (recoverable only from git history ≤ 064cf65).
- Zero-op vs mean-op, the recurring lesson: clamping a subspace containing the task-mean pre-image to ZERO is devastating but almost task-unspecific (cf arms ≈ same damage) — the mean pre-image carries a big all-task shared component, so proj-to-0 is an off-manifold intervention, not a task-content removal. Report the MEAN op as the interpretable one and keep zero-op results only next to their cf controls. (Same asymmetry as the sink/proj findings in the persona-vector ICL study.)
- Comparability gates for Stream W-derived studies: any sibling script must import
build_prompts/make_chunks/build_cf_map from
ablate_oneshot_preimage_logprob.py(never re-implement) and hard-gate on prompt-identity equality (query/answer/answer_token_id arrays) vs the stored Stream W npz; clean-log-p agreement is ADVISORY only (cross-GPU fp noise ~1.7e-2, same-GPU rule from 2026-07-16 applies).
Reusable conventions, project decisions, and open questions. Append; date entries. Resolved questions move from "Open" to "Decided" with the rationale.
torch.linalg.svdon CUDA fp32 defaults to the Jacobi driver (gesvdj), which showed backend error ‖W−USVʰ‖/σ₁ ≈ 1e-3 on 4096×4096 ridge maps (torch 2.12+cu130, RTX PRO 4500) — enough to fail a 1e-3 self-consistency gate on truncated-SVD pseudo-inverses.driver="gesvd"reaches CPU-LAPACK-level accuracy (~1e-5) and was FASTER (1.2s vs 3.9s). TF32 was off; purely the SVD backend. Convention: any CUDA SVD feeding singular values, ranks, or (pseudo-)inverses must passdriver="gesvd"(CPU tensors take no driver arg). Diagnostic pattern kept inlogs/sandbox_perprompt/preimages/diag_svd_driver.py.- rank90 convention for truncated inversions: k = smallest k with cum σ²/Σσ² ≥ 0.90 — the
diagnose_weight_spectrum.spectrum_statsdefinition (σ² energy, NOT the plain σ sum; user-adjudicated 2026-07-27).
- The two-shot token-pair steering script now supports two pairing semantics:
label-matched (default, original study: pairs share demo labels + query, demo inputs differ
by function — input2 carries the counterfactual) and input-matched (
--pair_mode input: pairs share demo INPUTS + query, each function supplies its own labels — label1/label2 carry the counterfactual, input2 is clean). Requested by the project owner 2026-07-20. - Input-mode eligibility: shared input words with differing outputs across the two functions and all four demo labels single-token (mirrors the label-mode single-token constraint); one pair per eligible input word, no cap (user choices 2026-07-20). ant/syn 987 pairs, digits 200.
- Output roots:
twoshot_tokenpair_perpair_inputmatch_cos_heatmap/andtwoshot_tokenpair_perpair_inputmatch_cumclamp_cos_heatmap/under direction2_label_geometry; summaries carry apair_modekey. The label-matched dirs/behaviour are unchanged (regression: 90/90 smoke grids bit-identical to the pre-change script). - Interpretation guard: in input-matched pairs, label1→{input2, prelabel2} dircos ≡ +1.0 by construction (full causal patch for reads before label2) — treat those cells as sanity checks, not findings.
- From now on the number tasks are the DIGIT variants by default (
prev_number_digits,next_number_digits); the word variants (prev_number/next_number) are used only when explicitly requested. Stated by the project owner 2026-07-20. - Why: GPT-J executes the digit tasks near-perfectly (top-1 exact ≈ 1.0 from 1–2 shots vs
judged 0.70/0.45 plateaus for antonym/synonym — see the by_nshots judged study), inputs and
outputs are 100% single GPT-J tokens (0–250), and there is no leakage adjacency to the
prev_item/next_itemtrain tasks that confounds the word variants' held-out status. - Applied 2026-07-20:
--tasksdefaults flipped incosine_activation_to_fv_varicl_pertask.pyandplot_pertask_r2_best_lines.py(also its--input_csvdefault → the..._plus_number_digitsridge study). Existing word-task results stay in place (tracked results are never deleted) but are no longer the reference.
- Metric per (task, icl_example_index, token_role, layer) cell: cosine(activation, that task's train_varicl_top40 FV) computed per prompt, then averaged over all 170 prompts (130 train + 40 test pooled). Raw cosine — no centering. All three choices (per-prompt mean rather than cosine-to-the-mean-activation; both splits; raw) confirmed by the user 2026-07-20 before compute.
- Differs from the 2026-06-15 pooled cosine study (
cosine_activation_to_task_fv) in FV definition (that one used the task-specificgptj_fvvectors) and granularity (that one averaged over the 29-task manifest; this one is per task, the 4 tasks of the per-task ridge R² study). Don't compare levels across the two studies. - Interpretation caveat (observed): raw-cosine ordering across tasks (word > number) is the REVERSE of ridge-R² decodability ordering (number > word) — cosine alignment and linear decodability are distinct notions; don't use one as a proxy for the other.
plot_pertask_r2_best_lines.pyis now metric-generic via--value_column/--ylabel/ --title_metric/--suptitle; defaults unchanged (R² plot regenerates byte-identical).
train_varicl_max4_top40was a DEBUG test set (variable-ICL capped at 4 shots), not a canonical FV definition. It has been moved toartifacts/function_vectors/gpt-j/debug/train_varicl_max4_top40(README indebug/). Unless a study explicitly says otherwise, "function vectors" meansartifacts/function_vectors/gpt-j/train_varicl_top40(variable 1–10-shot CIE, top-40 multitask heads). Stated by the project owner 2026-07-10.- Consequences: everything fit against the max4 FVs is debug-only — the Stream S ridge maps +
preimage banks (
preimage_pairdiff/train_varicl_max4_top40,..._tsvdk16/...), the Stream S cosine analyses, and the Stream W v1 ablation run. Theirrun_config.jsonfiles record the pre-move fv_root path (left as historical provenance;fit_tsvd_preimages_multicell.pyhas adebug/fallback). Script--fv_rootdefaults that pointed at the max4 set now point at thedebug/path. - Note the FV-definition spread in existing results: the full-dim ridge R² study
(
fulldim_ridge_activation_to_fv/) usedtrain_selected(top-10 multitask heads); Stream S/W-v1 used the max4 debug set. New preimage/causal work should refit ridge maps againsttrain_varicl_top40targets rather than reusing those maps.
- Ablation/edit-layer indexing, pinned: the preimage banks are keyed by
edit_layer = capture_layer − 1∈ 0..27; edit layer b means hooking the output oftransformer.h.{b}(= capture entry b+1 of the 29-entry residual stack whose entry 0 is the embedding). A "start layer L, all downstream layers" ablation therefore gates a hook overmodel_config["layer_hook_names"]withif block_idx < L: return outputand NEVER touches the embedding entry (no preimage exists for it; maps start at capture layer 1). Start-layer sweeps run L = 0..27. - Left-padded batching is safe for GPT-J logit readout without position_ids correction: GPT-J
uses rotary (relative) attention and pads are masked, so a uniform left-pad shift leaves
real-token logits unchanged; verified in
ablate_oneshot_preimage_logprob.py's built-in batched-vs-unbatched cross-check. Site positions must still be offset by the per-row pad amount. - Single-task TSVD preimage banks (
fit_tsvd_preimages_multicell.py --tasks ...,<cell>/preimages/{task}_tsvd_preimage_bank.pt): the rank-16 inverse is linear in its target, so per-task banks were validated against the existing pairdiff banks viatsvd(fv_A) − tsvd(fv_B) == tsvd(fv_A − fv_B)(rel err ~7e-6). The script's skip-guard is now per-requested-bank-file (the old per-celldiagnostics.jsonmarker would have skipped existing cells entirely and could never add new banks incrementally).
- The activation→FV ridge R² is real signal, not pipeline leakage. Permuting the train-task→FV
assignment (task-level; test targets true; identical pipeline incl. LOO-task alpha CV; 3 seeds
averaged) collapses test R² from median 0.346 / max 0.465 to median −0.021 / max 0.0000
(best "control" cell is layer 0, the trivial train-mean predictor; only 1.2% of cells are even
microscopically > 0, worst per-seed max 0.016). Ridge responds by pinning alpha high → predicts
the train-mean FV. Control outputs:
fulldim_ridge_activation_to_fv_shuffled{,_seed0,1,2}/; worker flags--shuffle_train_labels --shuffle_seed. - Row-level variant (
--shuffle_mode row, 2026-07-09) is equally dead and even cleaner: permuting the row→FV assignment across all 3400 train rows (balanced, fixed per seed) gives seed-mean test R² median 0.0000 / max 0.0028, median test MSE = V to 5 decimals (fulldim_ridge_activation_to_fv_rowshuffled{,_seed0,1,2}/). Two mode differences worth remembering: (a) row mode kills TRAIN fit too (train_mse ≈ V_train — conflicting targets for near-identical rows), task mode can still memorize train; (b) row mode's median R² is ~0 rather than slightly negative — shuffled targets are balanced within every LOO-CV fold, so the CV finds the global-mean predictor exactly optimal. - Gotcha fixed en route:
artifacts/residual_activations/*/index.jsonshard paths are ABSOLUTE and predate the 2026-06-19 results→artifacts reorg; any loader following them must fall back to the split dir (done inregress_activation_to_fv_fulldim_ridge.py, commit 42044d9). Other scripts copying this loader pattern have the same latent issue.
- The full-dim activation→FV ridge maps have true rank ≤ #train tasks (20) — the targets are
20 distinct FVs, so W = (ZᵀZ+αI)⁻¹ZᵀY inherits rank(Y). Singular values below ~rank 20
(cond(W) ~ 1e9) are float round-off, not structure. Consequences (Stream S diagnostics,
diagnose_pairdiff_preimage_spectrum.py+preimage_diagnostics/):- The EXACT pre-image direction is float noise: |dz_exact|/|dz_damped| ~ 3e7–1e8, and rounding W to fp16 (1e-3 perturbation) reorients it to cos ≈ 0.001 with the fp32 version. Any result computed from an exact inverse of these maps measures numerics, not the model.
- The honest inverse is the rank-k truncated pseudo-inverse (k ≈ 16, ≤ 20) —
fit_tsvd_preimages_multicell.py, banks inartifacts/preimage_pairdiff_tsvdk16/. It beat every other direction on the digits pair (mean cos +0.73–0.76 @L4, sustained 0.4–0.66 at label tokens where raw fv_diff is ~0) and slightly beat the gamma-selected Tikhonov damped variant (soft version of the same cutoff). torch.svd_lowrank(q = k+32, niter=8) matches the exact fp64 top-k SVD to ~4e-7 on these spectra — use it instead of full 4096² SVDs (seconds vs minutes).
- Inverting the k=16 PCA ridge instead does NOT work as well (peaks ≪ fv_diff): its FV-PC basis covers only 24–38% of held-out fv_diff targets, and its top-variance activation PCs discard the low-variance directions that carry early-layer task identity. Variance-chosen ≠ regression-chosen subspaces.
- When a direction is needed for a task-pair difference under the ridge maps, damp the inversion
of the DIFFERENCE target
fv_A − fv_Bdirectly (fit_ridge_preimages_multicell.py,pairdiff_preimages/banks). The Tikhonov-damped inverse is linear in the target only at FIXED gamma; subtracting two per-task damped pre-images mixes two different gammas (the norm-cap selection rule picks per-target) and injects damping artifacts into the difference direction. Exact (gamma=0) inversion commutes with subtraction, so only the damped variant cares. - Unit-normalizing does NOT make exact vs damped pre-images interchangeable. cond(W)~1e9-1e10 means the exact inverse's direction (not just norm) is dominated by the smallest singular values — Stream S confirms it empirically: exact pre-image direction explains ~random-floor levels of the two-shot pair-diff variance while the damped direction explains up to 0.43 (uncentered, digits query_final L4).
- Reusable intermediates:
artifacts/preimage_pairdiff/train_varicl_max4_top40/<role>_icl{k}/maps/holds materialized 4096x4096 ridge maps (W_std fp16 + scaler) for 6 cells x 28 layers with the varicl-max4-top40 FV target — any future pre-image/decoding study at those cells can skip the ~25 s/layer refits. Same layout as Stream R'sartifacts/preimage_steering/maps (icl10 pre_label, train_selected_top40 target). - Centered vs uncentered explained-variance for paired diffs diverge wildly (digits demo2_label L4: top-1-direction bound 0.85 uncentered vs 0.13 centered): the pair-diff mean is a huge shared component. Report both; say which one a claim uses.
compute_function_vectors.pysaves each FV as a DICT{'function_vector' [resid_dim], 'top_heads', 'n_top_heads'}— NOT a bare tensor.top_headsholds numpy int64 scalars, so on torch≥2.6torch.load(p)fails (defaultweights_only=True). Load pattern:obj = torch.load(p, weights_only=False); fv = obj['function_vector'].reshape(-1).float().- GPT-J task-specific FVs live in
artifacts/gptj_fv/<task>/<task>_function_vector.pt(top-10, n_shots=10). The_digitsvariants didn't exist and were computed 2026-07-04 (mean-acts 100 trials + indirect-effect 25 trials ≈ 9 min/task on an RTX 4000 Ada, 20 GB). - Logit readout without the full lm_head: run
model.transformer(...)under the edit hook, thenmodel.lm_head(last_hidden[:, -1, :]). GPTJModel already appliesln_f, solast_hidden_stateis post-norm — applying lm_head to the qfinal slice gives correct next-token logits while avoiding the[B, seq, 50400]allocation. - FV-projection-ablation ordering: when steering (add) and ablation (project-out
u) target the SAME token (qfinal), apply steer-THEN-ablate within each layer's hook. - Localize BOTH the layer and the token when asking whether a direction mediates an intervention
(Stream T lesson). "Steer across all layers" was ambiguous and cost two reruns. Three progressively
finer designs gave different answers: (1) add the steer vec at ALL 29 layers at once → swamps a
single-direction ablation, retention≈1, looks like the FV doesn't mediate; (2) one layer at a time
but all 3 steer tokens together → reveals mid-net peak + ~0.33–0.48 α=2 retention; (3) one layer AND
one token at a time (the right design) → a separate
steer(ℓ)/steer+ablate(ℓ)curve per site. Result at (3): steering works at every site (qfinal peaks mid-net L10–14 gain up to +7.9; demo label tokens peak earlier and weaker), and ablating F'⊥F at qfinal removes ~50–70 % of the localized gain at α=2 at ALL sites — including gain injected back at the demo label tokens. Retention rises with α (strong steering brute-forces other directions), so the gentle-α regime is the clean test. Lesson: an all-at-once intervention conflates "does this direction matter" with "can I overwhelm the readout"; sweep every axis you're claiming localization over, and confirm the sweep granularity with the user before spending GPU. (Supersedes the first pass's mediation≈0 conclusion.) - Which FV-derived direction to ablate — F'−F beats F'⊥F as an anti-steering knockout (2026-07-07).
steer_twoshot_fv_ablation_logitgap.py --ablate_variant {fperp,fdiff}:fperpremovesnormalize(F'−proj_F F')(target-FV-specific),fdiffremovesnormalize(F'−F)(raw FV difference). At α=2 both give similar peak retention (~0.28–0.45); the difference shows at high α — F'⊥F retention climbs to 0.79–0.91 (steering brute-forces past it) while F'−F stays 0.00–0.66. Reason: the mean-difference steer vector is a (target−source) activation difference, so it aligns with the FV difference F'−F; F'⊥F throws away the F-parallel component and is a strictly partial knockout. Prefer F'−F when the goal is to maximally cancel difference-of-means steering. (Cheap variant runs: rename the old outputs with a_<variant>tag + re-run only the new variant;steer/cleancolumns must match byte-for-byte across variants since they're ablation-independent — a free correctness check.)
- When you only need hidden states, call
model.transformer(...), NOTmodel(...).model(...)computeslm_head(hidden_states)→ a[batch, seq, vocab≈50400]fp32 tensor (e.g. 1.25 GB at batch 64 × seq 97), which we never use (we read residual streams via baukit TraceDict). This is what OOM'd the 10-shot run at the unsteered pass.model.transformer(input_ids, attention_mask)runs the embedding + blocks (where the hooks live) and skips lm_head/final head — identical captured outputs, ~1.25 GB less, and faster. The 2-shot script usedmodel(...)and got away with it only because its prompts were short. - Batch vs the 24 GB 4090 for these 29-layer
retain_outputcaptures: batch 64 pins memory at the ~23.4 GB ceiling (a longer-prompt batch then OOMs); batch 48 sits at ~21.7 GB with margin — use 48. The 29-layer retain_output (holds every block's[B,seq,D]output each forward) is the memory AND time driver, so runs are slow (~80 s/grid) regardless of the short prompts. SetPYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. - Kill remote runs by PID, not
pkill -f <pattern>when the pattern also appears in the killing shell's own command line —pkill -f steer_tenshot_stripmatches the ssh shell running it and self-terminates (SSH exits 255, though it does kill the target first). Safe form:ps -eo pid,args | grep '[s]teer_tenshot_strip_cos' | awk '{print $1}' | xargs -r kill -9(the[s]regex trick prevents grep from matching itself). - Unmatched-demo mean-difference steering (10-shot, no matched labels): steer_vec at a structural slot = difference of the two tasks' MEAN activations there. Result: the steerable signal lives almost entirely in demo LABEL tokens (input/pre-label ≈ 0), mid-layer→late-read, distributed across demo positions (not just the last). Only the read token (qfinal) is byte-matched, keeping baseline_cos valid.
- Steering ≠ patching ⇒ byte-identity is NOT required. The readout adds
α·mean_pairs[tgt−src]to a residual stream; that direction is well-defined at any token. So the search space can include the demo-2 INPUT token (t2 input2) even though it differs across functions. BUT its steer vector then carries a lexical/content component (not pure function context; at layer 0 it is literally a mean token-embedding diff) and its READ baseline cos is < 1 (vs ≈1 at the 5 clean tokens). Keep it, but flag it (suptitle + summarynote_input2); don't compare its magnitude naively against clean tokens. - Structural asserts that hold exactly: lower-triangle
k ≤ i≡ 0 (a position-i edit at layer i reaches a later position only at read layers> i, even across different tokens); embedding columni=0≡ 0 for CLEAN source tokens (same token ⇒ identicaltransformer.dropoutput ⇒ steer_vec=0; GPT-J has no additive positional emb at the embedding, so position shifts don't matter).input2is the documented exception (assert skipped for it). - Token positions for the 6-token 2-shot search space come from the inlined
selected_token_records(loops both demos, icl 1 & 2): label1=last_label_token@1, prelabel2=pre_label_token@2, label2=last_label_token@2, qfinal=last_prompt_token@None; the two INPUT tokens areprelabel2−1andqfinal−1(no named role). Assert strictly-increasing positions per prompt. - Memory-lean pattern for ≤20 GB cards: keep captured acts
[N,6,29,D]in fp16; compute steer_vec and baseline cos in a chunked loop over pairs (CB≈64) to avoid full-N float copies; convert only per-read-token slices to float in the inner loop. Fits gpt-j-6b + acts at ~18.5/20 GB. Full-precision copies OOM. - GPU-pod gotchas (RunPod): (1) a fresh pod created with
--networkVolumeIdbut the DEFAULT--volumePathmounts the shared volume at/runpod, NOT/workspace(the templated pods use/workspace). Find the repo at/runpod/function_vectors, HF cache/runpod/.cache/huggingface. (2) Other pods only trust the USER's SSH key; to reach a self-created pod, pass our pubkey via--env PUBLIC_KEY="$(cat ~/.ssh/runpod_gpu.pub)" --startSSH. (3) The project docker image is now the upgraded torch 2.8 / cu128 stack (runs on Ada AND Blackwell sm_120) — butmatplotlibis ABI-broken against numpy 2.1.2;pip install -U matplotlib(→3.11) before plotting. Compute scripts (numpy+torch only) are unaffected. - Plotting is CPU-only — the plot scripts just read saved
.npy/.csvgrids. Installnumpy matplotlibon the editing (CPU) pod (python3 -m pip install numpy matplotlib; note the system has TWO pythons —/usr/bin/python3.10is the one the scripts use, NOT the/usr/local/bin/pippython3.12) and iterate on figures locally — no GPU pod needed once the grids exist on the shared volume. - Comparability across many sub-plots: when per-figure color scales span a wide dynamic range (here ~100× across token-pairs), per-figure scaling is misleading. Add (a) a single GLOBAL vmax across all grids, (b) a token×token "matrix of heatmaps" master figure (spatially arranged, one colorbar), and (c) a scalar peak-Δcos overview matrix (annotated). Keep per-pair per-scale figures for drill-down.
- GPT-J (transformers 4.49.0) uses EAGER attention by default (no
GPTJSdpaAttentionclass), so pre-softmax attention scores are directly editable.load_gpt_model_and_tokenizeralready loads eager; assertmodel.config._attn_implementation == "eager". - Knockout recipe: monkeypatch
GPTJAttention._attnwith a faithful copy of the 4.49.0 source, and right AFTERattn_weights = attn_weights + causal_maskand BEFORE softmax, set the target (query,key) entries totorch.finfo(attn_weights.dtype).min. After softmax the key's weight is 0 and the row renormalizes to sum 1 — "ablate by zeroing the score, attention still sums to 1". Per-row, all-heads indexing:attn_weights[rows[:,None], heads[None,:], q_idx[:,None], k_idx[:,j][:,None]] = mn. - Threading per-row indices: stash
(q_idx[B], k_idx[B,K])on eachmodel.transformer.h[l].attn._kobefore the forward (helperset_knockout/clear_knockout); the patched_attnreads_ko(None = clean). No baukit needed — read logits from the model output. Editing the 4Dattention_maskdoes NOT work for per-row key positions (it's shared across the batch); editingself.biasfails under left-padding (same for all rows). Monkeypatch is the way. - Verify with one
output_attentions=Trueforward: knocked-out key weight ≈ 0, edited rows sum to 1. - Result: qfin reads the task directly from the LABEL tokens, not from the demo-2 pre-label (cutting the pre-label edge ≈ cutting a structural-token edge; cutting the label edges collapses the task).
- The two patching experiments take
--patch_from_entry(default 6 = "L6 and above"; 0 = all entries incl. embedding). Output goes to a per-experiment regime subfolder<exp>/{L6_and_above,all_layers}/("all_layers" if pe==0 else f"L{pe}_and_above"); plotters take--regime. Implemented by rebinding the module globalPATCH_FROM_ENTRYat the top ofmain()so the existing hooks/asserts pick it up. - Patching the embedding (entry 0) is a no-op for byte-identical positions (labels, query, pre-label in the paired design) — so the label-follow result is unchanged between regimes. It is NOT a no-op for the demo INPUT tokens (they differ across base/target): patching entry 0 there swaps the literal input word, which is why the interval grid's demo2-input cells jump in all_layers. Keep this in mind when reading any "all layers" patch that touches positions whose tokens differ across the pair.
- Conclusion: the label-token findings (Streams M/N) are robust to patch onset; document the all_layers run as a completeness check, not a new result.
- "Open" set-patching does NOT isolate the patched tokens. Overwriting token positions ← target with
nothing else frozen lets the patched info be relayed: later tokens attend to the patched positions
(from entry 7+) and pass the signal on. To measure a token set's DIRECT contribution to the output,
also pin every other non-output token to its base value at the patched entries
(
h[:, :-1, :] = base_full[:, :-1, e], then set the patched roles → target; leave the last/output column free). Then only the patched positions carry target and only the direct patched→output path remains. The gap (open recovery − isolated recovery) = the relayed share. - Requires capturing the base full residual stack (all positions, patched entries) — keep it
per-chunk (seq differs per chunk under left-pad); ~3GB at N=544 on the 4090, fits with batch 64 +
expandable_segments:True. (Stream M only needed the 6 named positions; isolation needs all positions.) - Result: label tokens recover 74% (words) / 95% (numbers) of the task switch even fully isolated → the labels drive the output largely directly; pre-label ≈ 0. See [[stream-m-interval-patch]] WORKLOG.
- Interval-patch mechanic (vs Δ-vector steering): run the base prompt and, at residual entries
6..28, overwrite (assign, not+=) tokeni← the other prompt's activation and tokenj← the base's own clean activation.ithen carries the other function for L6+;jis pinned to original (blocks relay). Hooks fire in forward order, so a downstream tokenkfirst differs at the entry after the first patched entry →Δcos(k, entry 6) ≡ 0(good cheap invariant; asserted <1e-3). - Pinning the OUTPUT token (
qfin) to original zeroes any logit effect exactly — the entirej=qfincolumn is +0.000. Expected, not a bug: the final-position residual is restored to baseline so logits are unchanged. Keep the column (it's the causal boundary), don't special-case it. - Reuse, don't reinvent the trace plumbing: the exact 2-arg
(output, layer_name)factory closure, tuple-vs-tensor dispatch (output[0]fortransformer.h.*, bare tensor fortransformer.drop), and left-pad per-row position table all carry over verbatim fromsteer_label_cos_heatmap.py. The ONLY change for patching is assign instead of add, and a per-row position vector (pos6[:,i]) so the hook can edit two positions at once. - Positions are NOT fixed even when every word is single-token. In-context tokenization shifts the
query block by ±1 token for some words (e.g.
acceptable), so the "all single-token ⇒ fixed index" shortcut fails. Always compute positions per row and align by ROLE (the named 6 positions), never by absolute index — base and target prompts of a pair can also differ in length. retain_outputover all 29 layers is the memory bottleneck, not the stored captures: it holds every position × every layer for the whole batch. batch 256 OOMs the 4090 (~23 GB); batch 64 +PYTORCH_CUDA_ALLOC_CONF=expandable_segments:Trueis ample and still ~17 short forwards/task.- Single-token-input filter is free for antonym/synonym: 100% of demo-input options for the 544
used labels and 100% of the shared query pool are single-token (verified), so requiring it drops 0
tuples. Demo-input-1 and demo-input-2 sample from the SAME distribution (
o2i[L]); only the query comes from the narrowershared_inpool.
New deliverable src/eval_scripts/steer_label_cos_heatmap.py (Stream L): 29×29 (intervention-layer ×
read-layer) heatmap of how injecting α·steer_vec(i) at the demo label token pushes the source
prompt's query-final activation toward the (unsteered) target's, measured as Δcos = cos(steered src_final, tgt_final) − cos(src_final, tgt_final), meaned over overlapping prompt pairs. Layer 0 =
embedding (transformer.drop), 1–28 = block outputs. Reusable facts:
- The grid is structurally upper-triangular and the embedding row/col is exactly 0. A label-token
edit reaches the query-final token only via later blocks' attention, so
Δcos = 0for readk ≤ intervene i(incl. diagonal) — use this as a free correctness check. Embedding diff is 0 because the label + query-final tokens are byte-identical across f1/f2 (the overlapping design), so baseline cos at layer 0 = 1 andsteer_vec[0] = 0. - baukit
edit_outputhooks MUST be exact 2-arg(output, layer_name)closures (already noted 2026-06-11). Hit again here: passing per-chunk state via extra default-kwargs made the hook silently no-op (Δcos = 0 everywhere). Fix = a factory returning a 2-arg closure that captures state via scope. Also handle BOTH output shapes:transformer.h.{l}returns a tuple (editoutput[0]), but the embedding moduletransformer.dropreturns a bare tensor (editoutput). matplotlib3.7.1 is ABI-incompatible with this box'snumpy2.1.2 (numpy.core.multiarray failed to importonimport matplotlib). Fixed bypip install -U "matplotlib>=3.8"→ 3.11.0. Any plotting script will fail to import until this is in place.- Shared output dir → name summaries per task (
<task_pair>_summary.json, not a singlesummary.json) so a second task's run doesn't clobber the first's metadata. Grids/PNGs/CSVs are already per-task-named. - Finding: mid-layer intervention band (words L5–12 peak L7–9; digits earlier, L4–8) feeding read L16–18, dead by intervene ~L16 — mirrors the 1-D causal window. Digits steer ~2× words; α=2 > α=4 at the peak (the toward-target-cosine metric overshoots sooner than the logit-flip readout). Absolute shifts are small (baseline cos near ceiling ~0.98–0.99); the location in the grid is the signal.
Convention (use for all new scripts): import paths from src/utils/paths.py; never hardcode
results/.... Roots are REPO_ROOT-anchored and env-overridable (FV_ARTIFACTS_ROOT, FV_RESULTS_ROOT,
FV_LOGS_ROOT):
ARTIFACTS_ROOT(artifacts/, gitignored) — intermediate caches: activations, captures, function vectors, head selections, scratch. Write all recomputable.pt/.jsonlhere.RESULTS_ROOT(results/, TRACKED) — study deliverables, in 5 bucket constants by research direction:AMBIGUOUS_DIR(d1),LABEL_GEOMETRY_DIR(d2),FV_FORMATION_DIR(d3),STEERING_COMPARISON_DIR,GENERAL_DIR. Put figures in<bucket>/<exp>/figures/or<bucket>/figures/.LOGS_ROOT(logs/, gitignored) — run logs.
What's committed: under results/ only *.png/*.pdf + summary *.csv/*.json (binaries *.pt/*.npy/ *.npz/*.jsonl/*.log and raw dumps per_query.csv/raw.json/recreate_fig8/**/*.md are globally
ignored). Exception — head selections ARE committed even though they live in artifacts/: the top-N
head rankings are the output of the expensive CIE computation, so multitask_top_aie_heads{.pt,_metadata.json},
heads.pt, heads_metadata.json, fv_manifest*.json, selected_heads.json are re-included via
.gitignore negations (so FVs can be rebuilt without recomputing CIE). Scratch dirs (artifacts/_*/**)
are re-ignored.
Gotchas: (1) .gitignore has NO inline comments — pattern # note makes the comment part of the
pattern; put comments on their own line. (2) To track files under an ignored dir, ignore by glob
(artifacts/**) + re-include directories (!artifacts/**/) + re-include specific filenames, with the
negations LAST so they beat earlier rules (incl. the global *.pt).
Extension of the 1-shot paired-difference design to two demos. Use this construction for any 2-shot paired study:
- Matched labels, distinct within a prompt: demos carry labels
(L1,L2), both from the shared-output pool, withL1≠L2. The label SEQUENCE is identical across the two functions (f1 prompt and f2 prompt use the same(L1,L2)and the same query); only the demo INPUT tokens differ by function (antonym-of-L vs synonym-of-L; for digits next-inputL−1vs prev-inputL+1). So at every demo-label / pre-label position the token is byte-identical across f1/f2 — pure contextualization, as in the 1-shot design. - Enumeration: one tuple per shared-output label word
w→L1=w,L2=random distinct label, query from shared-input pool minus{L1,L2,4 demo inputs}, deterministic per(seed,task_pair,w). Yields n_prompts ≈ |label pool| (≈544 ant/syn, ≈198 digits) — comparable to the 1-shot run. - NO train/test split for activation gathering (full-pool capture). The split only matters where Δ is fit-then-applied (steering); geometry is descriptive. (User-confirmed.)
- 5 captured roles per prompt:
demo1_prelabel,demo1_label,demo2_prelabel,demo2_label,query_final. The*_prelabelroles are theA:token before each demo label (pre_label_token@ icl_example_index 1/2);demo2_prelabelwas the specifically-requested new position.selected_token_recordsnumbers demos 1-based (icl_example_index 1,2) and handles 2 demos with no special-casing. - Script:
src/eval_scripts/capture_and_grade_twoshot_paired.py(sibling of the 1-shot version; reusesget_residual_stack/selected_token_records/flush_shard+word_pairs_to_prompt_data). Output is an INTERMEDIATE →artifacts/twoshot_paired_graded/<pair>/(git-ignoredARTIFACTS_ROOT), NOTresults/. First-token rank grading stamped per row. - Finding: a second matched demo ~doubles first-token top-1 on word tasks (antonym 0.232→0.439, synonym 0.066→0.221) and cuts the 1-shot copy-the-query failure; digits at ceiling (0.98–0.99).
For task-switch steering where the gold answers are single tokens, prefer the logit readout over the sample+judge harness — it's exact, ~100× cheaper, and needs no OpenAI key.
- Metric: steer source→target, then in ONE forward read
logit(target_gold) - logit(source_gold)at the query final token. α=0 = clean baseline (negative = prompt favors source answer); steering pushes it up across zero = the switch. Script:src/eval_scripts/steer_switch_logit.py, plotplot_switch_logit.py(shaded band = 95% CI of the mean over the n=100 test queries = mean ± 1.96·SEM/√n, NOT per-query spread). - DIGIT numerals are all single GPT-J tokens (verified 0–250, with leading space) — vs word-form
numbers where compound forms >20 are multi-token. So for number tasks use the digit datasets
(
dataset_files/abstractive/{next,prev}_number_digits.json, inputs 1–200) to get single-token labels/golds. Word next/prev dropped for single-token analyses. - CLEAN TRAIN/TEST SPLIT (no leakage in either the ICL example or the final query): the two positions Δ is read at are the demo label token (Δ_label) and the query final token (Δ_final). So hold out a TEST set of 100 label-overlap (shared-output) words AND 100 input-overlap (shared-input, single-token-gold-both) words; estimate Δ ONLY from disjoint TRAIN words, and draw the 1-shot demo for test prompts only from non-test words too (so a test query can't sneak in via the demo slot). Use equal n_train (=100 prompt pairs) for every task pair so Δ quality is comparable.
- Derive Δ from our own forward passes on train pairs, NOT
load_capture_diffsover the full-pooloneshot_paired_gradedcapture — the latter pools over the whole shared vocabulary and overlaps the eval queries (train-on-test). Only hold out the test query words from Δ (the positions read); excluding their golds too is unnecessary and decimates the small digit pool (collapsed Δ to 5 words in one run). - No correctness filtering on Δ collection (deliberate): activations are averaged over all train
pairs regardless of whether the model answers correctly — this diverges from the standard
ICL-correct-filtered FV recipe (
get_mean_head_activations) and likely leaves Δ noisier (1-shot competence is modest), but the user chose to keep it blind. Revisit if Δ looks weak. - Finding: two distinct causal windows by injection site — demo-label peaks early (L4–8, dead by ~L14); final-prompt-token peaks later (L10–12) and reaches larger logit-diffs. All effects gone by ~L16–20.
src/eval_scripts/steer_switch_judge.py (Stream E) is the behavioral analog of the logit-level
steer_label_to_query.py: inject sign·α·Δ_site(L) into a source-task 1-shot prompt, generate
n=10 temp-1 samples, GPT-4-judge them for the target task, sweep layer×α → accuracy curves.
- Reuse, don't recompute: the per-site steering vectors come straight from the existing
results/oneshot_paired_graded/<pair>/captures viaload_capture_diffs(sourcerole → Δ_label,targetrole → Δ_final). No new capture. sign = +1 if target is f1 (Δ is f1−f2) else −1. - Steering hook during generation: baukit
edit_output, add the vector at a per-row position only on the prompt forward (output[0].shape[1] > 1); cached single-token steps are skipped so the perturbation propagates through the KV cache. Replicate prompts ×n_samples explicitly (prompt-major order) + left-pad; label-site idx = pad_len + label_pos, final-site idx = −1. - α=0 baseline is layer/site-independent → generate & judge it once per direction, draw flat. Sanity: an unsteered source-task prompt judged for the target task scores LOW (antonym-demo → synonym ≈ 0.08), confirming headroom for steering to lift.
- GPT-4 judge MUST be concurrent. The stock sequential
judge()injudge_oneshot_paired.pyruns ~7 batches/min (~8s/request) → ~27h for this sweep's 11k batches. AThreadPoolExecutor(judge_parallel,--judge_workers 40, order-preserving, with exp-backoff retry on transient HTTP/JSON errors) cuts it to ~40 min. Use this pattern for any large judge run. - Throughput seen on the 24GB Blackwell: GPT-J generation ~140 samples/s (gen_batch=20 queries × n_samples=10 = 200 rows/generate call); the full 564k-sample sweep generated in ~50 min.
- OPENAI_API_KEY is NOT in this session's env or /proc/1/environ (unlike earlier streams). An
interactive-shell
exportdoes NOT reach the agent's fresh shells. Workable route: write the key to a gitignored file (.openai_key, chmod 600) andset -a; . ./.openai_key; set +abefore the run.
2026-06-18 — variable-ICL FV variant capped at max 4 demos (top-40); zero-shot steering cost is small
New train_varicl sibling that caps the per-prompt random ICL count at 1–4 demos (--max_shots 4)
instead of 1–10, top-40 heads pooled over the 20 train tasks. Single behavioral change; isolated roots
so the original max-10 artifacts are untouched. NEW scripts: src/eval_scripts/run_varicl_max4_pipeline.sh
(build driver — handles the stage-2 gotcha: test-task activations computed to results/_varicl_testtasks_max4/
then copied into the main root before the 29-task FV build) and src/eval_scripts/evaluate_heldout_varicl_max4.py
(steering eval + 3-series plot; reuses evaluate_fv/get_filter_set/summarize_results, reads the
max-10 N=40 curve from results/heldout_varicl_nheads_sweep/<task>/nheads_sweep_by_layer.json and the
task-specific curve from results/heldout_multitask_head_eval/<task>/comparison_summary.json — no
recompute of those lines, no no-FV-baseline recompute). Artifact roots:
results/multitask_aie_heads_varicl_max4/, results/function_vectors/gpt-j/train_varicl_max4_top40/
(29 FVs, n_top_heads=40), results/heldout_varicl_max4_top40/ (9 per-task PNGs + AGGREGATE + summaries).
FINDING: shrinking the extraction ICL window to ≤4 demos costs a little zero-shot steering (mean best-layer top-1 0.353 vs max-10's 0.400; task-specific 0.483) and ~nothing few-shot (10-shot-shuffled 0.777 vs 0.784). Same mid-layer (L6–12) causal window, max-4 a notch below max-10 throughout; no task collapses. So the variable-ICL FV is fairly robust to a shorter extraction window — context at inference time compensates almost entirely, and even zero-shot the FV is only modestly weaker. The top-40 pooled head set is the canonical varicl ranking ((9,14)(15,5)(8,1)…), so the shot-cap changes the mean activations (and thus FV magnitude/direction) more than the head selection.
PCA-ridge activation→FV heatmap (k_act=16, k_fv=16) on the max-4 FVs: reran
regress_activation_to_fv_pca_ridge.py (×10 ICL shards) + merge_fulldim_ridge_results.py with the
FV target re-pointed to train_varicl_max4_top40 (cached residual activations in
results/residual_activations/ are FV-target-agnostic → only --fv_root/--output_dir change). Out:
results/pca_ridge_activation_to_fv_varicl_max4_top40/. Same canonical mid-layer (L9–15) bowl; best
cell icl09/pre @ L13 = 0.1596 (FV-PC floor 0.1433). Compare via gap-above-floor (the
FV-target-agnostic regression-quality metric, since each FV set has its own 16-PC variance floor):
train_selected 0.0155, varicl_max4 0.0162, varicl_top40 (max10) 0.0214 → the max-4 FVs are as
regressable from the residual stream as train_selected and a touch tighter than the max-10 varicl FVs.
Do NOT compare absolute test_mse across FV targets (different floors); use gap-above-floor.
2026-06-18 — Paired 1-shot captures carry extra correctness notions (n=5 temp=1 fraction + prob-gated)
The paired 1-shot activation rows (results/oneshot_paired_graded/{antonym_synonym, next_number_prev_number}/shard_*.pt + grading.json) now hold these per-prompt correctness labels,
all GPT-4.1-judged with the strict same-word-is-false prompts:
judge_top1— greedy decode (original).frac_correct_temp1_n5(float) +n_correct_temp1_n5(int 0..5) — 5 temperature=1 samples (seed 42), each judged; the row stores the FRACTION correct (k/5). This REPLACED an earlier single-samplejudge_top1_temp1(one noisy Bernoulli draw) — the n=5 fraction is the robust signal; the single-sample field was dropped from all rows.sample_judge_oneshot_paired.py(draws vianum_return_sequences, reuses the judge helpers).greedy_answer_prob(float) — model's probability of the greedy answer = product of greedy per-token (top-1) probs over the answer span (tokens before first "\n"); equals first-token prob for single-token answers, joint product for multi-token (numbers). NB it is the probability of the greedy path itself, NOT the gold answer's probability (the answer judged IS the greedy output).judge_top1_p50(bool) =judge_top1ANDgreedy_answer_prob≥ 0.50. (Threshold walked 0.70 → 0.60 → 0.50; each step a free instant re-gate from the stored float, no model rerun — the payoff of saving the continuous prob. p70/p60 fields removed. To change again, re-gate from the float.)
Activations are decode-independent (same forward pass), so all decode-dependent labels (temp=1
fraction, greedy prob/gate) are computed retroactively and stamped by match key
(function_task, output_word, query) — same scheme as the original judge_top1 tagging. The prob
gate makes NO API calls (reuses greedy judge_top1).
Lesson / caveat: in 1-shot, open-ended word tasks (antonym/synonym) are rarely high-confidence
(mean greedy answer prob ~0.17) so even a 0.50 gate keeps ≤19 prompts (synonym 0) — too few for a
correct-vs-incorrect geometry contrast on those tasks; use the continuous greedy_answer_prob or
frac_correct_temp1_n5 as covariates, or the number pair (gate keeps 47–98). Confidence ≠ correctness
(synonym: 8 prompts ≥0.50 but 0 judge-correct; prev_number 58 ≥0.50, 47 correct). Under temp=1 sampling
numbers drop vs greedy (greedy argmax was right; sampling injects errors) while synonym RISES (.143 →
mean frac .229 — many valid non-argmax answers). any-correct ≫ all-correct (most prompts are sometimes
but not always right), which is exactly the spread the single sample missed and the fraction captures.
dataset_files/ambiguous/{magnitude,identity}.json were extended from 300 → 600 entries
(300 integer + 300 decimal, ≤3 dp) by dataset_files/generate/add_decimals_magnitude_identity.py.
Integer-only originals backed up at {magnitude,identity}.int_only.json (restore from these to
revert). Decimal entries mirror the integer structure: +d overlap (mag=id=d), −d differentiator
(mag=d, id=−d). Split is now overlap=300 / differentiator=300. Impact: any re-run of FV
extraction / disambiguation evals on these tasks now includes decimals; previously-saved FV .pt
files (e.g. gptj_fv_ambiguous_constrained*) are integer-derived and unchanged. When judging
numeric-answer correctness, use numeric equality (float(a)==float(b)), NOT normalize_answer
(it strips .). Decimal labels are multi-token in BOTH tasks (e.g. 97.96→[' 97','.','96']),
so the first/last-label-token capture finally matters for magnitude too.
2026-06-16 — Qwen3-8B 3+1+1 disambiguation + CAVEAT: first-token top-k invalid for digit-splitting tokenizers
Ran the Stream-F 3+1+1 ambiguous eval for Qwen/Qwen3-8B-Base (eval_ambiguous_disambiguation.py
already takes --model_name). CAVEAT: the token-level topk scoring (score_topk, first answer
token via tok(" "+ans).input_ids[0]) is invalid for Qwen3 numeric tasks — Qwen3 tokenizes
" 47" as [space, "4", "7"], so the first token is a bare space for every numeric answer →
gold and partner both "match" → round/truncate/first_digit/last_digit/count_* spuriously read
top1=partner@1=1.00. GPT-J merges " 47"→single token, so it's fine. Use the whole-answer
wordtopk (beam) scoring for cross-model comparison (eval_wordtopk_3plus1plus1_qwen3.json).
Result (whole-answer): Qwen3 mean top1 0.678 vs GPT-J 0.561, mean partner@1 0.170 vs 0.294 — Qwen3
disambiguates better and is less prior-bound (round 0.06→1.00, british 0.33→0.87, reverse 0.00→0.28),
but the prior asymmetry persists (last_letter 0.10/partner 0.60, largest_city 0.11/partner 0.70).
Lesson: any first-token metric is tokenizer-dependent; prefer whole-answer scoring across models.
ICLDataset.__getitem__(int) does self.raw_data.iloc[i].to_dict(), building a 1-row pandas
Series. A Series is single-dtype, so a row with a float column (e.g. round/truncate inputs
like 3.7) upcasts an int output gold 4 → 4.0. List-indexing (ds[[i]], orient='list')
preserves per-column dtype and is safe. Symptom: round/truncate scored 0.00 because golds were
0.77, truncate 0.00→~1.00. Lesson: never grab a single ICLDataset row with int
indexing when columns have mixed dtypes; use list-indexing or coerce ints before '4.0'; normalize_answer then strips the '.' → '40', so the model's correct '4' never matched.
Only these two tasks are affected (only float-input + int-output tasks); demos were unaffected
(they use list-indexing). Fix in recreate_fig8.py: extract the query via ds[[idx]]. After fix:
round 0.00→str().
4 task names exist in BOTH dataset_files/abstractive and dataset_files/ambiguous:
count_consonants, count_vowels, identity, magnitude (the ambiguous copies are the
overlap/differentiator-structured variants; load_dataset only scans abstractive+extractive and
would silently grab the abstractive one). Any results store keyed by task name alone
(results/.../<task>.json) will have the second folder's run clobber the first. Convention going
forward: key per-folder results by <model>/<folder>/<task> (recreate_fig8.py does this).
For the Fig 8 recreation (Stream H), accuracy is greedy generation stopped at "\n" then scored
with normalized exact_match_score over the full answer — matching Stream G's correctness
definition, NOT the cached first-token clean_topk. Efficient multi-shot readout: per trial sample
10 demos + 1 query, and for k=0..10 use the first k demos (nested prefixes of the 10-shot prompt) so
shots 1..9 come from the same draw. Batched generation with token-budget batching (left-pad) keeps
GPT-J's full-MHA KV cache under 80GB. All 2200 responses/task are saved to <task>_responses.jsonl
for inspection. (A single-forward "read every demo answer position" trick is ~11× cheaper but only
works for first-token scoring; generation requires real decoding, so we batch instead.)
To mark capture prompts correct/incorrect, do not reuse fs_results_layer_sweep.json: it scores
a different dataset partition (the 504-example valid split) than the captures (130 train + 40 test
sampled by stable_rng(seed, task, split, "query_indices")), and stores only first-token ranks.
Instead, compute_capture_prompt_correctness.py regenerates the exact capture prompts by importing
the capture's own deterministic helpers (sample_query_indices / sample_demo_indices / make_prompt
from extract_targeted_residual_stream_activations.py, seed 42) and asserts the regenerated
query_source_index order equals the capture index.json's query_indices (byte-identical prompts).
"Correct" = full-answer match, not first-token rank (per request): greedy generate (do_sample=False,
max_new_tokens=16), then the repo's parse_generation(out, [target], exact_match_score) — its regex
([\w. ]+)[\nQ]* truncates at the newline / next Q:, and exact_match_score is SQuAD-normalized.
Join correctness to activations by (split, query_source_index) (robust; verified zero misses).
Correctness is one bit per prompt, shared across all 31 token positions of that prompt.
Same 31-token-position × 29-layer grid as the full-dim ridge MSE study, metric = per-example mean
raw cosine (no centering) between each residual activation and that task's OWN task-specific FV
(results/gptj_fv/<task>/...), averaged over all 29 tasks. cosine_activation_to_task_fv.py is
model-free; reuses the ridge loader constants + the merge script's position_key/label axis ordering;
diverging heatmap centered at 0, shared scale across the all/correct PNGs. Each activation directory
targets one ICL index, so token_role alone uniquely buckets a prompt's rows (no icl filter needed).
Result: all-positive, mid-layer bowl (peak L10), role banding last_prompt_token≈pre_label_token
≫ first/last_label_token; correct-only ≈ all at the aggregate level (max|Δ|=0.0016).
New dataset family dataset_files/ambiguous/ (generator create_ambiguous_datasets.py) for the
task-DISAMBIGUATION study: pairs (f1,f2) that AGREE on an overlap region and DISAGREE on a
differentiator region (matched input set; overlap entries byte-identical in both files). Four pairs:
magnitude|identity (50/50), past_tense|past_participle (50/50), first_letter|last_letter
(50/50), capital_city|largest_city (50 overlap / 35 differ — only ~35 real capital≠largest
countries exist, ~20 GPT-J-plausible; recall caveat, trim/restrict downstream).
Eval eval_ambiguous_disambiguation.py (GPT-J, n=100/task, cross-prompt batched greedy gen,
token-level exact match; matches_partner = model emitted the OTHER function's answer). Prompt =
3 overlap demos + 1 differentiator demo (task's output) + 1 differentiator query.
FINDING — a single disambiguating demo only redirects GPT-J in the prior-aligned direction. magnitude/identity are ~perfect both ways (.98/1.00). The other pairs are strongly asymmetric: the model nails the prior-aligned task and ignores the 4th demo for the anti-prior task, emitting the prior answer instead — past_tense .93 vs past_participle .36 (partner .60); first_letter .97 vs last_letter .04 (partner .89); capital .57 / largest .47 (noisy, recall-limited). So 3 ambiguous
- 1 disambiguating demos are enough only when the two functions are equally "natural"; a strong prior
(past-tense over participle, first- over last-letter) is not overcome by one differentiating example.
The
matches_partnerrate is the key diagnostic (separates "did the other rule" from "neither"). Next probes: a control arm (4th demo also ambiguous → prior baseline) and k>1 disambiguating demos.
The train-pooled (train_selected) FVs exist at three head counts: top-10 (original,
results/gptj_fv_multitask_top10/), top-20 (..._top20/), top-40 (..._top40/), all 29 tasks.
Built from the same results/multitask_aie_heads/multitask_top_aie_heads.pt (stores top-40) +
cached mean activations — no CIE/forward recompute. Organized views:
results/function_vectors/gpt-j/train_selected_top{20,40}/. FV norms grow with n (more out_proj
terms summed): top-10 ~30–47, top-20 ~34–54, top-40 ~41–66. NB the n=20/40 head sets are strict
prefixes of the top-40 ranking, so head sets nest (top-10 ⊂ top-20 ⊂ top-40). Relevant to the
n=10 degeneracy note below: train vs train+test first diverge ~n=11, so the top-20/40 train_selected
FVs are the right inputs for a non-degenerate train-vs-train+test comparison (rebuild train+test at
matching n first).
Held-out steering finding (9 test tasks, evaluate_heldout_multitask_head_fvs.py --n_top_heads {10,20,40}; combined results/heldout_multitask_head_eval_nheads_comparison.json): adding heads
helps ZERO-SHOT steering but not ICL-context steering. Best-layer zero-shot+FV mean top-1 rises
0.376 (n10) → 0.381 (n20) → 0.446 (n40), closing most of the gap to task-specific (0.483); some
tasks jump a lot (capitalize 0.75→0.96, capitalize_first_letter 0.70→0.95). But 10-shot-shuffled+FV
is flat/down (0.796→0.785→0.780; task-specific 0.812) — with real context present the extra heads add
nothing. Caveat: at n40 several tasks' best zero-shot layer drifts to L0–1 (larger-norm FV), so verify
those before citing; the mid-layer L8–13 optimum is stable for antonym/synonym/capitalize_first_letter.
2026-06-11 — FINDING (Phase 2): the mean label-token function axis is CAUSAL — steering it flips synonym→antonym
steer_label_to_query.py, results in results/oneshot_steering/. Injecting α·Δ_label (mean
antonym−synonym label-token difference, computed on the 530 Phase-1 words) at the demo label token of a
synonym-context 1-shot prompt, scored on 1,003 disjoint queries with single-token gold ant+syn:
- Geometric propagation: the induced query-token shift aligns with the natural syn→ant direction — cos(shift, Δ_final) ≈ 0.71–0.76 (steer L6, read L8; >0.5 through L15), matched-norm random control ≈ 0.01–0.06. α-stable over 0.5–4 (linear regime), degrades at 8.
- Behavioral flip: flip rate 0.283 (clean) → 0.64 (L6, α=8); mean logit(ant)−logit(syn) −1.44 → +1.12, zero-crossing at α≈2. Random control stays negative (−0.92 @ α=8) — effect is direction-specific. Best steer layers 6≈9 > 11.
- Reconciles Phase 1's negative map result: the dominant shared axis propagates causally and predictably label→query; what the (weak, high-rank) linear map could not capture is the per-word high-dim tail. So "label-token arithmetic moves the next position predictably" is TRUE for the function axis specifically, FALSE for the full per-word geometry.
- Geography (landmark↔park) propagates weakly (cos 0.13–0.23, n=84, geometry-only — no behavioral flip exists since both functions output the same country).
- Causal window [L4, ~L15] (full L1–L24 sweep): steering is null at L1–2 (flip ≈ baseline despite
maximal downstream depth; natural ‖Δ_label‖ ~0.7–0.9, the axis doesn't exist yet), onsets sharply
L3→L4 (flip 0.34→0.57 @α4), plateaus L4–9 (peak cos 0.76, flip 0.60), decays L11–12, and is
exactly baseline-dead by L16 (L16/20/24: flip 0.282–0.284, cos ≤0.09). Interpretation: the
function fingerprint is written into the label token ~L3–6 and transported to the query position
before ~L16 — congruent with the CIE top-head band (L8–15). Profile:
results/oneshot_steering/fig_steer_layer_profile_L1to24.png. - Engineering gotchas (reusable): baukit
edit_outputhooks MUST be exact 2-arg(output, layer_name)closures — extra default-kwargs are mis-bound positionally byinvoke_with_optional_args. The query-position shift at L_read==L_steer is identically zero (cross-position effects only enter via later blocks' attention) — read strictly downstream.
2026-06-11 — FINDING: function is decodable at the query token in FV space, but the label→query map is not low-rank/rotation-like (1-shot)
Phase-1 of the function-geometry experiment (Stream E; scripts capture_oneshot_paired.py +
analyze_oneshot_geometry.py; results in results/oneshot_paired_analysis/). Same-output-word,
different-function paired 1-shot prompts; source = demo label token, target = query final token.
- Function fingerprint is present and linearly decodable at the QUERY token along the FV difference
axis
(fv_f1−fv_f2): peak L11 AUC 0.941 / d 2.22 (antonym↔synonym), 0.894 (landmark↔park), mid-layer band L9–15. This is the robust, positive result. - The label-token difference = one dominant shared axis + a broad high-dim tail. STABLE rank
(Σσ²/σ₁²) of the L11
D_labelmatrix is 5.2 (antonym↔syn, 544×4096) / 3.3 (geography, 84×4096): σ₁ alone holds ~19%/~30% of the energy → a single clear "function axis." BUT 90% of the energy needs k≈315 dims (antonym↔syn) — the residual is high-dimensional/per-word. Stable rank is lowest at mid layers (min ~4.7 @ L9, ~5 through L15), coinciding with peak query-token FV separation; rises to ~9 by L24. (The entropy "effective rank" was ~150; the two disagree precisely because of the fat tail — stable rank keys on the dominant axis.) Centered stable rank (mean axis removed) = 18.8 / 9.4. Robust to magnitude: unit-normalizing each difference vector before stacking leaves stable rank essentially unchanged (5.4 / 3.5 vs raw 5.2 / 3.3) — so the dominant axis is DIRECTIONAL, not driven by a few high-norm vectors. Magnitudes are themselves tight (CV ~23%/18%) BUT 14/544 antonym↔syn rows are exactly zero (degenerate: demo input collided across functions, e.g. borrow←lend in both → identical prompts; excluded when normalizing → 530 rows). Artifacts:D_label.pt,fig_Dlabel_svd{,_unit}_L11.png,fig_Dlabel_magnitude_hist_L11.png,fig_Dlabel_stable_rank_by_layer{,_unit}.png. - But the demo-label → query-token linear map is weak / high-rank / not rotation-like (held-out R² ≤0.16 mid-layers, eff rank ~190–280, Procrustes gap large). So the original "low-rank manifold + predictable rotation from label to next position" hypothesis is not supported in 1-shot — even though a single dominant function axis DOES exist at the label token.
- Method notes (reusable): (1) hold the OUTPUT token fixed and vary function → the source-token activation difference is pure contextualization (the label token is literally identical across f1/f2; assertion enforced). (2) For a W-sample map into d=4096 with W≪d, the full-matrix M structural metrics (eff-rank, reduced-rank, ‖MᵀM−I‖) are rank-limited/regularization-dominated — trust held-out map_R2 and the FV-separation AUC, not in-sample reduced-rank R². (3) Project the source→target map into the ≤2W-dim data span before any d×d op — all reported metrics are invariant, ~30× faster.
- Decision for Phase 2 (if pursued): steer along the FV difference axis at/near the query token (well-motivated by the separation); do NOT assume steering the demo label token propagates predictably to the query (Phase 1 contradicts it). A multi-shot variant (stronger function identification) is the natural next probe before drawing strong conclusions.
For the mixed-task ICL / function-geometry substrate, a good pair (f1, f2) must encode the function ONLY in the (input, output) relation: neither the input token alone nor the output/label token alone may be predictive of which task it is. Formally P(input|f1)=P(input|f2) AND P(output|f1)=P(output|f2). Shared input domain + same output type is NOT enough — the output marginal must match too.
This rejects pairs that looked superficially fine:
- capitalize_first ↔ last: output leak (first- vs last-letter distributions differ).
- english-fr ↔ -de, country-capital ↔ -currency, person-occupation ↔ -sport: output leak (you can ID the task from one output token: a language / a city vs currency / etc.).
- hypernym ↔ hyponym: input leak (specific vs general words) — inverse pairs only satisfy the criterion over a symmetric domain (input set = output set).
Two construction recipes that satisfy it by design: (1) inverse functions over a symmetric/closed domain (next/prev over a cycle; +k/−k; Caesar ±n); (2) two distinct relations whose outputs land in the same word pool as the inputs (antonym/synonym; synonym/rhyme). Operational test: a probe predicting the task from input-token-only or output-token-only embeddings should be at chance.
A second, sharper constraint — in-context learnability — makes the design space much smaller than it looks. Matched output marginals already require the two functions to share an output pool (a lone output token can't reveal the task). That alone kills factual-attribute pairs (capital vs largest-city → disjoint answer sets), translation (disjoint by language), and morphology (suffix leak). On top of that, the function must be inferable from ~5 ICL demos — which kills otherwise-legal ideas like alphabetical neighbor word (model can't know the candidate lexicon). What survives BOTH = "a general rule the model already knows, over a domain with a shared output pool" → arithmetic on numbers, or meaning/sound relations over words. That is exactly the antonym/synonym/ rhyme/number region — the apparent "everything looks similar" is the constraint, not a lack of imagination. Decision: ship 3 pairs (antonym|synonym, synonym|rhyme, antonym|rhyme, next_number|prev_number); do NOT force a 4th. Letters were generated then dropped (26-cap + no new legal+learnable axis). If more variety is ever needed, the only honest routes are to relax a constraint deliberately and measure the cost (e.g. morphology with the input/output leakage probe), or curate an association corpus offline.
Datasets live in dataset_files/paired_tasks/ (generator
dataset_files/generate/create_paired_tasks_datasets.py): next/prev_number (words),
rhyme (CMUdict, outputs in syn/ant vocab), plus copied synonym/antonym. Note:
number-words avoid digit clumping but compound forms >20 are multi-token (word-based) —
score at first/last label-token positions.
Geography mostly LEAKS: (a) attribute relations (country→capital vs →currency vs
→continent) have disjoint output pools → output-token leak; (b) entity-type→country
relations leak via the input — verified the repo's park-country has "Park"/"National"
in 747/749 inputs, so landmark|park is identifiable from the input alone (why Stream E
could only use it geometry-only). The single legal family is place→place spatial
relations over one shared pool — the geographic analog of next/prev_number. Built
east_neighbor / west_neighbor (dataset_files/generate/create_geography_neighbor_datasets.py):
country → nearest country in the E/W bearing quadrant, from most-populous-city coords
(geonamescache, offline; deterministic). Both country→country → matched input AND output
marginals; identical input set (244 countries with both an E and W neighbour). Multi-token
country names allowed (first/last label-token, like compound numbers). Approximate for
island/edge countries (e.g. Bermuda→Cabo Verde) — inherent to nearest-in-quadrant. North/south
and bordering (needs adjacency data, not available offline) are possible extensions.
Installed pycountry-convert (continents) + geonamescache (city coords), and nltk+WordNet
(unused so far — semantic co-hyponym/hypernym/meronym pairs remain an option).
General principle (what makes geography/periodic-table work): take a domain the model
knows that has internal STRUCTURE (1D order, 2D grid, hierarchy) and pair two "neighbour"-type
relations over it; the shared pool + the structure give matched marginals for free. Instances:
numbers (1D line: next/prev), geography (2D map: east/west), periodic table (2D grid:
next_in_period=right / next_in_group=below). Built next_in_period/next_in_group via
mendeleev (offline): 66 main-group+transition elements with both a right and below neighbour,
identical input set, multi-token names allowed. f-block excluded (no group_id; model knows them
worst). Skipped US-presidents (ordinal over a known list, ~46 — too small). Open structured
domains not yet built: north/south geo neighbours, WordNet taxonomy (co-hyponym; is-a vs has-part).
Probe (src/eval_scripts/mixed_icl_antonym_synonym_topk.py): n=200 prompts, 10 ICL
demos = 5 antonym + 5 synonym, query from one task; top-1/2/3 = correct first token
in model's top-k. Ran both demo orders + a same-task 10-shot reference overlay.
(Query word is excluded from the ICL demos; n=50 SE≈0.06 was too noisy → use n≥200.)
- Antonym query ≫ synonym query (top-1 0.47–0.55 vs 0.12–0.20), every cut. This is task difficulty — synonyms have many valid answers so the single gold first-token frequently misses top-3. First-token top-k understates synonym competence; use any-acceptable-synonym scoring if synonym is the focus.
- Recency is real (~+0.08 top-1): each task scores higher when its demo block is the last one before the query (antonym 0.47→0.55, synonym 0.12→0.20). An earlier n=50 run wrongly called recency "weak/inconsistent" — that was sampling noise.
- Antonym is fairly robust to halved demos (mixed 0.47–0.55 vs same-task ref 0.565, matching the ref when its demos are recent); synonym degrades hard (mixed 0.12–0.20 vs ref 0.365) → synonym pays most for diluted demos.
Repro convention for model-loading scripts on this box: deps not preinstalled —
pip install transformers==4.49.0 accelerate (per fv_environment.yml); baukit is
not on PyPI, so scripts that only need eval_utils' pure helpers should inline them
rather than import the baukit-laden module. GPT-J weights cached under
/workspace/.cache/huggingface; run with HF_HOME=… HF_HUB_OFFLINE=1.
- Model: GPT-J (29 layers, d=4096). FP16 + Qwen variants exist (see git log).
- Task split:
task_splits/abstractive_train_test_tasks_29.json= 20 train + 9 test. - Results layout: one dir per experiment under
results/; per-task subdirs hold.ptartifacts + a*_metadata.json/*_summary.jsondescribing the run config. - Head set artifact:
multitask_top_aie_heads.pt(+_metadata.json) — ranked (layer, head) list by prompt-weighted CIE. Stored top-40; top-10 typically used. - FV artifact:
{task}_function_vector*.pt= sum over selected heads of out_proj applied to the head's mean last-token activation. _multitask_top10suffix on a result/FV dir means it was built from the multitask top-10 head set rather than per-task FVs.- Save intermediates (within storage reason). When a big experiment has an expensive stage (forward passes, CIE, activation capture), persist the general intermediate — not just the final answer — so variations rerun in minutes instead of hours. Concretely: store the full per-(layer,head) CIE grid + a generously-sized ranking (top-40, even if top-10 is used) rather than only the chosen heads; store mean activations for all heads/positions needed by any plausible N/k, not just the selected ones; keep per-prompt effects when subset re-aggregation might matter; cache residual-stream activations once and reuse across regression targets. Payoff observed 2026-06-11: top-20/30/40 varicl FVs + a whole new PCA-ridge heatmap cost minutes because stage-1 saved all-head activations and the top-40 ranking. Guardrail: this is for O(GB) per-task tensors and grids, not for dumping every forward pass — if an intermediate would cost ≫ recomputing it on demand, skip it and note the recompute command in the metadata instead.
Same 3-method derivation as the trio below (manifest now task_splits/paired_tasks_7.json;
builder manifests fv_manifest_paired2.json; auto no-filter retry available but unused — all 4
tasks pass the filter). All 12 FVs in the organized folder, norms 24–44. CAVEAT: GPT-J is weak
at all four — ICL-correct counts: canonical 10-shot east 5/51, west 7/51, period 2/14, group
5/14; varicl east 2, west 3, period 1, group 1 → the correct-only mean activations (esp. varicl,
1–3 prompts) are HIGH-VARIANCE. Filter kept ON for consistency with the 29 originals. If these
tasks matter downstream, prefer no-filter variants or bigger datasets. (Effective-n hierarchy among
paired tasks: next/prev_number excellent (42/42) ≫ elements/geo (1–7) ≥ rhyme (0 → no-filter).)
The new dataset_files/paired_tasks/ trio now has FVs under all of task_specific,
train_selected, train_varicl (organized folder + underlying caches; fv_manifest_paired.json
in each builder root so the original 29-task manifests are untouched). Conventions adopted:
- The 3 task JSONs are symlinked into
dataset_files/abstractive/(user-approved; the loader hardcodes abstractive/extractive). Split seed 42 → 140/18/42; valid is only 18. task_splits/paired_tasks_3.json= manifest for stage-2 builders (they validate--tasks).- rhyme FVs are NO-FILTER (
--no_filter_to_correct_icl, both regimes): GPT-J is 0/18 ICL-correct on rhyme valid, so the standard correct-only averaging is impossible. Treat rhyme's FV as "attempted-task" activations; flag in any cross-task comparison. next_number (18/18) and prev_number (15/18) are standard. - Repro trick: running varicl stage-1 for extra tasks with
--num_shards N>1keepswrites_global=False→ per-task files only, existing pooled head artifact safe. We still used an isolated root (results/multitask_aie_heads_varicl_paired/, kept for provenance) and copied the*_mean_head_activations_varicl.ptintoresults/multitask_aie_heads_varicl/<task>/.
Companion to the full-dim ridge decoder (Stream C below), run in a 16-PC bottleneck across all 31
token positions × 29 layers. Per cell: activation PCA (k_act=16) fit per-cell on 20 train; FV PCA
(k_fv=16) fit once on 20 train FVs; ridge 16→16, λ by leave-one-train-task-out CV, single
20-train standardizer; predict 7 test tasks, reconstruct to 4096-d, score there. Direct
projection (not the deprecated joint). FV target train_selected. cc/pc excluded.
- Scripts: NEW
regress_activation_to_fv_pca_ridge.py; launcherrun_fulldim_ridge_shards.shgeneralized with aWORKERenv override (drives both sweeps); merge reused unchanged. - Output:
results/pca_ridge_activation_to_fv/(combined_metrics.csv = 899 rows + heatmaps). - HEADLINE: the 16-PC bottleneck is free at the optimum — PCA best
icl10/finaltok @ L13 = 0.1147vs full-dim@ L11 = 0.1161(PCA marginally better; it denoises). In the mid-layer sweet spot (L8–13) PCA ≈/< full-dim; in later/embedding layers PCA is worse (L28 0.158 vs 0.149). Net mean Δ(pca−full)=+0.003. → 16 activation PCs hold all recoverable activation→FV signal where it's concentrated; the regression target genuinely lives in a ~16-dim subspace. - Metric identity:
fv_test_mse = (k_fv/4096)·pca_test_mse + floor(FV-PCs orthonormal), verified to 4e-8. FV-PC reconstruction floor (test) ≈ 0.099. So selecting α on PCA-space CV MSE == selecting on reconstructed MSE (differ by the constant floor). - Comparable metric note: this reconstructed-4096-d MSE is the same unit as the full-dim ridge (0.116) and the k/layer sweeps — NOT the joint-PCA-space MSE (Open Q3).
2026-06-10 — train_varicl RAN to completion; GOTCHA: stage-2 needs test-task activations precomputed
The variable-ICL method is fully built for GPT-J: 29 FVs + heads.pt + manifest + per-task
selected_heads.json under results/function_vectors/gpt-j/train_varicl/. Pooled top-10 (varicl)
= [(9,14),(15,5),(8,1),(12,10),(11,0),(8,0),(14,0),(24,6),(21,2),(10,0)]; 8/10 overlap with
train_selected (top-2 identical), so the variable-ICL regime selects nearly the same head
subspace as fixed-ICL train pooling — overlap, not identity, as expected.
GOTCHA (pipeline gap, now handled): the documented step sequence (stage-1 --task_split_key train_tasks → stage-2 compute_all_task_fvs_varicl.py) is INCOMPLETE. Stage-2 builds FVs for all
29 tasks and requires <task>_mean_head_activations_varicl.pt to already exist for every task
(it raises FileNotFoundError, does NOT compute on the fly). Stage-1 over train_tasks only writes
the 20 train tasks' activations, so the build crashes on the first test task. Fix: run stage-1
once more on the test split to an isolated dir (so writes_global can't clobber the train-pooled
head artifact), then copy the 9 activation files in:
python src/eval_scripts/compute_multitask_varicl_heads.py --task_split_key test_tasks \
--abstractive_only --query_split valid --demo_split train --n_top_heads 40 --batch_size 8 \
--min_shots 1 --max_shots 10 --max_successful_prompts 170 --filter_to_correct_icl \
--save_per_prompt_effects --save_path_root results/_varicl_testtasks --num_shards 1
# then: cp results/_varicl_testtasks/<task>/<task>_mean_head_activations_varicl.pt
# results/multitask_aie_heads_varicl/<task>/ (for the 9 test tasks)
python src/eval_scripts/compute_all_task_fvs_varicl.py --overwrite
This recomputes CIE for the 9 test tasks too (unavoidable; they're short-sequence so it's cheap and
the resulting test-pooled head artifact is harmless/unused). Future fix option: teach stage-2 to
compute missing activations itself, or have the runner also process test_tasks for activations only.
A new head-selection + FV method alongside task_specific / train / train+test. Same two-stage shape, but the prompt regime changes:
- Variable ICL: each prompt draws a random 1–10 demonstration count (deterministic per (task_index, query_idx); shard-invariant via global task_index).
- Correctness filter, capped: keep only correctly-answered prompts, ≤170 per task.
- Read position: mean head activations AND the CIE intervention both read the query
predictive (last) token, T=-1 (user-confirmed). Single, length-independent position → mean
activations stored as
(n_layers, n_heads, head_dim)(no token axis), so a new save filename<task>_mean_head_activations_varicl.ptand adapted indexing are required (the canonical builder's[...,-1]would grab a scalar — see the three corrections below). - CIE: variable ICL + shuffled labels, intervention at the query token, on the same
correctly-answered query set; CIE prompts use seed
args.seed + cie_seed_offset(default 500000) so they differ from the activation pass but stay reproducible. - Pooling: CIE averaged across the 20 train tasks (same as
train_selected), top-N heads.
New sibling scripts (existing engines untouched): src/utils/varicl_utils.py,
src/eval_scripts/compute_multitask_varicl_heads.py,
src/eval_scripts/compute_all_task_fvs_varicl.py,
src/eval_scripts/run_multitask_varicl_all_tasks.sh. New args: --min_shots, --max_shots,
--max_successful_prompts, --cie_seed_offset.
Three corrections vs the canonical code (handled): (1) the fixed-n_shots correctness filter
can't vary shots → new varicl_correctness_filter; (2) CIE avg_activations[L,H,token_idx] →
avg_activations[L,H] for single-position; (3) FV builder mean_activations[L,H,-1] →
mean_activations[L,H].
Outputs (when run): results/multitask_aie_heads_varicl/ (head set + per-task CIE/activations);
results/function_vectors/gpt-j/train_varicl/ (FVs + heads.pt + selected_heads.json). Run commands
- smoke verification: WORKLOG Stream D and plan
/root/.claude/plans/immutable-finding-boole.md.
New experiment line, deliberately PCA-free (distinct from regress_activation_to_fv_joint_pca*).
For each (token position, layer) cell, fit one ridge map from the raw 4096-d residual activation
to the raw 4096-d train_selected FV. λ chosen by leave-one-train-task-out CV (20 folds); a single
standardizer fit on the pooled 20-train rows is reused everywhere; MSE reported natively in 4096-d
on the 7 test tasks (cc/pc excluded). 31 token positions × 29 layers = 899 cells.
- Scripts:
regress_activation_to_fv_fulldim_ridge.py(worker, one shard = one ICL idx; GPU ridge via eigendecomposition reuse so the α grid is ~free),run_fulldim_ridge_shards.sh(tmux sharding, round-robin),merge_fulldim_ridge_results.py(combined CSV + heatmaps + summary). - Outputs:
results/fulldim_ridge_activation_to_fv/. - GOTCHA (now handled): in the
4tokensdir the final prompt token (last_prompt_token) hasicl_example_index = None; the 3 label roles use10. Filter the final-token role onNone. - Result: best decode = final prompt token @ layer 11, test_mse 0.116; clean layer bowl min at L11 (band L10–14); query position (ICL 10) beats earlier ICL demos. Embedding-layer pre-label/final tokens are constant across tasks (→ predict-the-mean baseline 0.217).
- Metric note: this 4096-d MSE is NOT comparable to the joint-PCA-space MSE (Open Q3); it is comparable to the reconstructed-4096-d MSE used by the k/layer sweeps.
FV derivation is two stages: (1) select top-N heads by CIE; (2) per task, sum
out_proj(mean_head_activation[L,H,-1]) over the selected heads. The three methods
differ only in stage 1 — which tasks' per-task CIE is pooled to rank heads.
Stage 2 (mean head activations) is shared. All three use top-10, GPT-J-6B,
query_split=valid, demo_split=train → directly comparable.
| Method | Stage-1 pool | Head set | Per-task FVs |
|---|---|---|---|
| task-specific | 1 task | gptj_fv/<task>/<task>_indirect_effect.pt |
gptj_fv/<task>/<task>_function_vector.pt |
| train | 20 train tasks | multitask_aie_heads/multitask_top_aie_heads.pt |
gptj_fv_multitask_top10/ (29) |
| train+test | 29 tasks | multitask_aie_heads_all_tasks/multitask_top_aie_heads.pt |
BUILT → function_vectors/gpt-j/train_test_selected/ (29 + manifest) |
Organized access point: results/function_vectors/{task_specific,train_selected,train_test_selected}/
— a clean view with uniform <task>_function_vector.pt naming so any --fv_root
points straight at a method. See results/function_vectors/README.md.
Symlinks, not hard moves. results/gptj_fv is dual-role (task-specific FVs +
the shared *_mean_head_activations.pt cache + CIE + eval JSONs) and ~15 scripts
default to the old paths. Hard-moving would break the pipeline, so the FV folder
symlinks into the existing caches. Underlying dirs remain source of truth.
Reversible; revisit if we want a true single-source-of-truth migration (would
require rewiring all --fv_root defaults).
All activation→FV k-sweep result dirs now live under results/k_sweeps/ (was flat in
results/). Names trimmed of the redundant k_sweep_ prefix (the parent conveys it):
results/k_sweeps/activation_to_fv_ols_multitask_top10_log2/— original, 9 test tasks.results/k_sweeps/activation_to_fv_ols_multitask_top10_log2_exclude_cc_pc/— 7 test tasks (dropscountry-currency,product-company). Default--output_dirinsweep_k_activation_to_fv_ols_log2.pyandsweep_k_activation_to_fv_ols.pyupdated to match;run_config.jsonself-paths fixed.
sweep_k_activation_to_fv_direct_log2.py now has --fix_act_k K (mutually exclusive with
--fix_fv_k): pins k_activations=K and sweeps k_FV (doubling grid reinterpreted as k_FV, capped
at fv_k_cap). Run at k_act=16 (7 tasks): test MSE falls monotonically with k_FV, best at the
k_FV=16 cap (14/15 series), riding just above the recon floor (gap ~0.01–0.03). No k_FV
overfitting — k_FV is bounded by discarded-FV-variance (floor) and by the FV-PCA rank cap (16),
not by overfitting. With 16 activation PCs the regression recovers any k_FV target nearly to the
floor. Combined over all three direct cuts (coupled diagonal / fix k_FV / fix k_act): the joint
optimum is (k_act≈16, k_FV=16) — the corner. k_act peaks ~16 then overfits; k_FV climbs
monotonically to its 16 cap. The four direct dirs in results/k_sweeps/:
..._log2, ..._log2_exclude_cc_pc, ..._log2_fixedfvk16_exclude_cc_pc,
..._log2_fixedactk16_exclude_cc_pc.
Per user decision, the k-sweep result dirs are now direct-method only. The three joint runs
were deleted and replaced by direct equivalents; results/k_sweeps/ holds exactly:
activation_to_fv_direct_ols_multitask_top10_log2/— coupled diagonal, full 9 test tasks.activation_to_fv_direct_ols_multitask_top10_log2_exclude_cc_pc/— coupled diagonal, 7 tasks.activation_to_fv_direct_ols_multitask_top10_log2_fixedfvk16_exclude_cc_pc/— k_FV pinned 16, sweep k_act, 7 tasks. The joint script (sweep_k_activation_to_fv_ols_log2.py) is retained as a tool; only its results were removed. Settling finding (direct, k_FV=16): k_act=1 → test MSE ≈ 0.18–0.19 (≈ predict-the-mean ~0.21), k_act=16 → ≈ 0.126. A single activation-PC does NOT recover the 16-dim FV — the joint method's apparent "1 PC suffices" was the 16 appended FV-basis features, not genuine low-rank recoverability. Optimal remains k_act≈16 (first-label 32), k_FV=16, ICL5.
Two regression definitions now coexist for the activation→FV PCA decode:
- joint (
sweep_k_activation_to_fv_ols_log2.py+ theregress_*_joint_pca*family): X and Y both projected onto the SAME concatenated [act-PCA (k_act) | FV-PCA (k_fv)] basis; OLS maps the (k_act+k_fv)-dim joint projection of the activation → the joint projection of the FV. The FV-PC half of the prediction is reconstructed for the 4096-d MSE. - direct (NEW
sweep_k_activation_to_fv_direct_log2.py): X = activation→act-PCA (k_act) only; Y = FV→FV-PCA (k_FV); OLS regresses R^{k_act}→R^{k_FV}; predicted FV-PCs reconstruct to 4096-d.
Why they differ: joint's feature vector additionally includes the activation projected onto
the FV basis (k_fv extra inputs), so joint fits slightly better. Measured: direct is
~0.002–0.003 higher test MSE than joint at the optimum (icl5/first 0.1261 vs 0.1232). The
direct setup is the cleaner "regress activation space → FV space" and should be preferred as
the headline definition; joint numbers were mildly optimistic. Optimal k (≈16, first-label 32),
ICL monotonicity, and token-role ordering are all unchanged between the two. The new script
mirrors all log2 flags incl. --fix_fv_k.
The joint-PCA regression has two independent dimensionalities: k_FV (FV-target PCs) and
k_activations (input PCs). The original log2 sweep coupled them below the cap
(fv_k = min(k, 16)), conflating the two. New flag --fix_fv_k on
sweep_k_activation_to_fv_ols_log2.py holds k_FV at fv_k_cap for every k, isolating
k_activations. Finding (k_FV=16): first/last-label tokens optimal at k_activations≈16–32;
pre-label token optimal at k_activations≈1–8 (more PCs hurt — little task signal there).
Beyond ~16–32 the activation side overfits regardless of token. Pinning k_FV=16 only changes
the k<16 regime vs the coupled run (it's strictly better there since the FV target isn't shrunk).
Canonical config: layer 11, last/first-label, ICL 5, k_activations≈16, k_FV=16.
Re-ran the log2 sweep without country-currency + product-company (whose
train-multitask-selected FVs steer much worse than task-specific). The activation→FV
regression test-MSE moved by only ~±0.001 and the optimal-k structure (k≈16–32, fv_k 16,
overfit past 32) was unchanged. Implication: "bad for steering" ≠ "hard to regress from
activations" — the two task-quality notions are decoupled, so the cc/pc weakness is not
visible in (and not fixable via) this reconstruction-MSE metric. If their weakness matters,
measure it on the steering/FV-effectiveness side (cf. evaluate_heldout_multitask_head_fvs.py).
The all-tasks head set was computed on query_split=valid (not train). Evidence:
per-task files are suffixed _valid (*_mean_indirect_effect_over_valid.pt,
*_per_prompt_indirect_effect_valid.pt) — the suffix is emitted from the actual
split — and the runner passes --query_split valid. The metadata top-level
query_split=train is a stale default, almost certainly written by the
--reduce step (which didn't receive the arg). Both head sets therefore share
query=valid, demo=train and are comparable. Follow-up (low priority): fix the
--reduce path in compute_multitask_top_aie_heads.py to record the true split.
Built 2026-06-10 with (reuses cached activations; zero forward passes):
python src/eval_scripts/compute_all_task_fvs_from_multitask_heads.py \
--heads_path results/multitask_aie_heads_all_tasks/multitask_top_aie_heads.pt \
--n_top_heads 10 --fv_root results/gptj_fv \
--output_root results/function_vectors/gpt-j/train_test_selected \
--task_manifest task_splits/abstractive_train_test_tasks_29.json
→ results/function_vectors/gpt-j/train_test_selected/<task>/<task>_function_vector.pt
(29) + fv_manifest.json. All three methods now complete for GPT-J. The builder also
supports --tasks (subset shard) and --manifest_name for future models with no
cached activations (each shard runs prompts at batch size).
Every task folder under results/function_vectors/<model>/<method>/<task>/ carries a
selected_heads.json listing the [layer, head, mean_indirect_effect] heads that built
its FV, plus selection_pool. Generated (idempotent) by:
python src/eval_scripts/write_fv_head_metadata.py --model_root results/function_vectors/gpt-j --n_top_heads 10
task_specific reads each task's own top_heads (unique per task); the two multitask
methods read the shared heads.pt (top-N). Rerun after building FVs for a new model.
At top-10, train_selected and train_test_selected select the same head set
(same membership; rank order and CIE scores differ slightly). Since the FV is an
order-independent sum over selected heads, the function_vector tensors are exactly
equal for all 29 tasks (verified: global max|Δ|=0; norms match). NB the .pt files
are not byte-identical — cmp differs on dict metadata (top_heads order, paths) — only
the tensors match. They diverge only at larger
n (34/40 overlap at n=40; first differences ~n=11+). Implication for experiments: a
train-vs-train+test comparison at n=10 measures nothing — raise n_top_heads (rebuild
with a larger --n_top_heads) or use a per-task metric to study the selection-leakage
effect. task_specific is genuinely distinct from both at every n.
-
Canonical head set for held-out eval.
evaluate_heldout_multitask_head_fvs.pyused the train-only (20-task) head set. Should held-out eval use the all-tasks set instead, or both as a comparison? -
One FV builder, not three.
compute_task_fv_from_multitask_heads.py,compute_all_task_fvs_from_multitask_heads.py, andcompute_fv_from_selected_heads.pyoverlap. Leaning:compute_all_task_fvs_from_multitask_heads.pyis the canonical batch builder (one model load, all 29 tasks, standard<task>_function_vector.ptfilenames +fv_manifest.json) — it's the one in the build command above.compute_fv_from_selected_heads.pyis the flexible single/ad-hoc builder (arbitrary head source,--tasks, non-standard_fv_<tag>.ptfilename). Markcompute_task_fv_from_multitask_heads.py(single-task) legacy. -
Canonical regression metric. Joint-PCA-space MSE vs reconstructed 4096-d FV-space MSE are not comparable. Pick one as the headline metric; report the other as secondary only.
-
Canonical FV target for regression. Some regression scripts target
gptj_fv, othersgptj_fv_multitask_top10. Decide which is the primary target (now also reachable asfunction_vectors/task_specificvsfunction_vectors/train_selected). -
Standardize ICL index range across regression/sweep scripts (currently mixes 1–4 and 2–5).
-
Save per-prompt CIE effects on the baseline too? The train-only head set lacks per-prompt effects, so
select_heads_from_cie_subset.pycannot re-subset it. If subset analysis matters for the baseline, re-run with--save_per_prompt_effects. -
Smoke-test dirs.
results/joint_pca_activation_to_fv_regression_smokeandresults/pca_abstractive_fv_activation_scatter_smoke— keep or remove?
- Workers: register a stream in WORKLOG.md before editing; don't co-edit the same
source file without coordinating (per CLAUDE.md).
compute_multitask_top_aie_heads.pyandevaluate_heldout_multitask_head_fvs.pyare the currently-modified shared files. - Nothing is committed yet; all experimental scripts + results are uncommitted.
The 10-shot strip studies selected the "input" slot as pre_label − 1, silently landing on the
constant "A" template token for months of grids (labels carry their leading space, so the input word
is 2+ tokens further left). Lesson: when a script picks "the X token" by positional arithmetic,
print/assert the decoded token string for a sample prompt at startup (or derive positions from the
get_token_meta_labels roles, e.g. demonstration_{i}_token), never trust offsets from adjacent
structural tokens. Fixed in both steer_tenshot_strip_{cos,norm}_heatmap.py (see WORKLOG Stream R).
steer_twoshot_tokenpair_cos_heatmap.py --steer_mode perpair= per-pair (matched-counterfactual, the user's "nearest neighbour") steering: each source prompt is injected with its OWN pair's tgt−src activation diff at the edited (token, layer); α=1 ≡ exact single-site activation patching (asserted at runtime). Results root:twoshot_tokenpair_perpair_cos_heatmap/(α∈{0.5,1,2}).- The 2026-07-13 "verify token positions" lesson applied to this script as well (contrary to
Stream R's "two-shot study unaffected" note): its
input2/qinputwerepre_label−1/qfinal−1= the constant "A". Consequently in the ORIGINALtwoshot_tokenpair_intervention_cos_heatmap/results, every grid whose source OR read token is input2/qinput measured the "A" token — do not cite those; grids among {label1, prelabel2, label2, qfinal} are unaffected. A position-correct mean rerun (α∈{2,4}) lives intwoshot_tokenpair_mean_fixedpos_cos_heatmap/; the original dir is kept until the user decides to replace it.
steer_twoshot_tokenpair_cos_heatmap.py --layer_mode cumulative (perpair only) hard-clamps the
intervention token to the matched counterfactual's activations at every layer from the start
layer i through 28 — per-pair trajectory patching, no strength sweep (α irrelevant under clamping).
File/figure names carry a NOMINAL alpha1 tag purely for plot-script compatibility
(plot_twoshot_tokenpair_heatmap_grid.py --alphas 1); do not read it as a strength. The grid
x-axis is the clamp START layer, and the clean-token embedding-column≡0 invariant does NOT apply
in this mode. Results: twoshot_tokenpair_perpair_cumclamp_cos_heatmap/.
Per user (emphatic): the intended steering-impact metric was always the DIRECTION-ALIGNMENT cosine dircos = mean_pairs[ cos( act_tgt − act_src , act_src_steered − act_src ) ] (at the read site) — "does the displacement caused by the intervention point along the counterfactual direction" — NOT the previously-implemented Δcos-to-target (cos(steered,tgt) − cos(src,tgt)), which is bounded by 1 − baseline (~0.1–0.2 on near-identical paired prompts) and conflates alignment with proximity.
- Every cos grid/figure/summary number computed BEFORE 2026-07-14 is in the deprecated metric —
do not cite (this includes Stream Q/R's tenshot "input rows ≈ 0" cos numbers and all prior
twoshot token-pair results). All grids under
twoshot_tokenpair_*_cos_heatmap/andtenshot_strip_intervention_cos_heatmap/were recomputed as dircos on 2026-07-14 (summary JSONs carry a "metric" key; old data recoverable from git history). - Convention: structurally-unaffected cells (zero displacement, e.g. read layer ≤ edit layer) render as 0 (F.cosine_similarity's eps does this naturally); dircos ∈ [-1,1] ⇒ scalar-overview panels use symmetric diverging scales, not one-sided Reds.
- Lesson: confirm the METRIC DEFINITION with the user before building/extending measurement studies — the formula in a docstring is not evidence the user wants that formula.
2026-07-16 — Propagated ablation as a MODE of the perlayer script; pod CUDA filter; same-GPU regression tests
- The propagated fixed-direction ablation (Stream X3) is
--mode propagatedonablate_oneshot_preimage_logprob.py, NOT a separate script: only the direction rule (fixed U[L] vs per-layer U[b]), the edited positions (anchor + all later tokens vs anchor only), and the default output root differ; prompts/arms/cf-map/sanity checks are shared. Modes write to SEPARATE results roots (oneshot_preimage_ablationvs..._propagated) so the summary rebuild never mixes them. Follow this pattern for future edit-rule variants. - RunPod: always pass
allowedCudaVersions: ["12.8","12.9","13.0"]in podFindAndDeployOnDemand for the function-vectors-dev template. The image needs CUDA ≥ 12.8; without the filter the scheduler happily lands on 12.4-driver hosts and the pod sits at uptime 0 / ports null forever (two 4090 pods lost to this on 2026-07-16). - Regression-test model outputs on the SAME GPU. fp16 GPT-J forwards differ by up to ~1.7e-2 in delta-log-p across GPU architectures (L4 vs prior pod, identical code). A code no-op check must compare old-code-vs-new-code on one GPU (expect exactly 0.0), never new-code-on-GPU-A vs stored-run-from-GPU-B with a tight tolerance.
- All steering work under
results/style_properties/steering/is exploratory. There is NO default, headline, or recommended steering technique. Promotion of any variant to repo standard requires an explicit user decision recorded here. Do not describe any cell as "the" steering result, and do not create root-level headline figures in that folder. - Variant grid (the only organising structure): {
meandiff,meanact,sparsehead} × {kall,k4} × {succno,succyes} = 12 cells, named<technique>__<kfilter>__<successfilter>. Registry:src/sandbox/ext_styleprops/variants.py(single source of truth; adding a technique = adding one entry, not a script). - Fixed shared protocol so cells are comparable: first cue token per document · 200 docs · 32-token T=1 seeded rollouts · LLM coherence judge drops gibberish · primary metric = strict rate with an unscorable rollout counted as NOT adopting · counterfactual + reverse controls expected for every variant · shared sweep grid layers 2-24 × doses 0.5-32. A cell that searched less records it in its spec and is flagged in the comparison figure.
- Comparison presentation: alphabetical, unranked, no best-cell highlight (so the table cannot imply a default).
- Excluded from the grid (kept, not deleted): the pre-2026-09-02 runs at all cue sites
with unjudged short rollouts (
artifacts/.../_archive_allk), and arms whose vector was derived from EVIDENCE tokens rather than cue tokens — derivation site is not a grid axis. - LESSON — arm keys are not stable literals: the runner named a property's arm
<vec>_cue_nat2altwhen that vector won the property's sweep and<vec>_cue_nat2alt_bestwhen it did not, with per-property_L<l>_a<a>shortlist suffixes; andrawalt_cue_cue_*(cue-derived) vsrawalt_cue_*(evidence-derived) differ by one segment. Match arms by REGEX with explicit vector gating, and verify with a losslessness check against previously reported numbers (build_variant_results.py --check) — a literal-key assignment silently mis-assigned three properties before that check caught it.
- What it is: an alternative way to teach a style convention in context — the document is
given in Spanish (
Spanish:header), then anEnglish:section translates it back in the target convention, truncated after the cue token; readout = the plain prescreen's sampled continuation, scored for style AND translation correctness. Lives inresults/style_properties/translation_framing/; exploratory, not a headline result. - Spanish is neutral and fixed (user decision): one Gemini translation per base doc in
standard Spanish typography (single spaces, straight quotes, digits,
%, sentence caps, parenthetical dashes →-), shared by both English polarities, enforced by a deterministic fix-up and audited (es_audit.csv). The English prefix is the only carrier of the convention. Do not build "mirrored-convention Spanish" variants without a new user decision. - Translation metric (user decision): Gemini judge PRIMARY (conventions explicitly ignored, gold reference = the document's own next tokens), normalised exact match SECONDARY. Accuracy of the framing = style adopted AND judge-correct, over style-scorable judged items. English-only comparison carries exact match only (free continuation has no translation target) — state the asymmetry wherever the two are plotted together.
- Prompt layout (user decision): language labels
Spanish:/English:(not bare text). - Scope (user decision): only the translation framing is sampled; the English-only curve is the 2026-09-01/02 prescreen record set (same items, seeds differ by the framing tag).
- Mechanics to keep: the header is tokenised separately and PREPENDED to the unchanged
English ids so the stored cue index/id assertions still hold;
prescreen_adherence.py --add_refsbackfillsref_nat/ref_alt/ctx_tailinto existing records inbuild_itemsorder (asserted) rather than re-sampling.
2026-09-07 — Translation framing covers all 17 style properties; pool re-admission is a pending user decision
- Scope extension (user request): the 4 properties excluded by the English-only prescreen
(
whilst,ellipsis,brit_t_past,ise_ize) were run through the translation framing and the Stage-A4 gate re-evaluated under both framings for all 17 (results/style_properties/translation_framing/gate.csv). The pruning reason (scorable rate below 15 %) does not hold under the framing (0.51–0.69 scorable), because the source anchors content and the model reaches the lexical slot. - Pool status is UNCHANGED until the user decides.
task_splits/style_properties_pool.jsonstill lists 13;ise_izepasses the gate under the framing (n = 118),whilst/ellipsispass on thin cells (18 / 8),brit_t_pastfails on separation. Re-admitting any of them — and whether that applies to the English-only pool too — requires an explicit user decision. - Gate reimplementation rule:
plot_translation_framing.gate_statsmust reproducebehavioral_prescreen/prescreen_summary.csvon the English-only records before any new-framing gate number is reported (it does; the nan-at-k=0 prior-pole fallback and constant-curve Spearman = undefined are the two details that had to matchplot_prescreen.py). - Repo hygiene (user-approved 2026-09-07): all
worktree-*/claude-*side branches deleted on origin and locally, all worktrees removed; the repo has onlymain. Background sessions must never call EnterWorktree here (.claude/settings.jsonhasbgIsolation: none); if the Edit-tool guard still refuses in-place edits, write via shell/python instead of creating a worktree. Files another agent has uncommitted in the shared checkout are never staged.
- All translation-framing data and results are deleted at the user's request: the Spanish
corpora (
base_corpus_es.json, the k>=5 supplementbase_corpus_k5*.jsonandprops_k5/), the sampled records (artifacts/style_properties/prescreen_translate*,prescreen_k5), the results folderresults/style_properties/translation_framing/, and the three study scripts (translate_corpus.py,judge_translation.py,plot_translation_framing.py);prescreen_adherence.pyis back to its 2026-09-06 form and the backfilledref_nat/ref_alt/ctx_tail/trans_exactkeys were stripped fromartifacts/style_properties/ prescreen/*.json. The 2026-09-06/07 DECISIONS entries and WORKLOG findings about that study are void. Git history keeps the code if a redesign wants to reuse pieces. - Why: the Spanish source was not held to ONE fixed convention — the translation prompt preserved each number's source form, so ordinals were mixed digit/word within and across documents (ordinal_words docs: 177 of 323 mixed). USER PRINCIPLE (must hold in any redesign): the Spanish text is identical for both polarities AND internally in one fixed convention for every feature; the English translation in context is the ONLY thing that varies and the only carrier of the convention. Also: say "English translation so far", never "English prefix".
- Kept (predates the study, other work depends on it): the 878-doc base corpus, the 17 property datasets and registry, the A4 prescreen records, decodability, and the steering sandbox. The pool is still 13.
- Redesign: to be agreed with the user BEFORE any generation or compute.
- Everything downstream of the style registry was removed:
dataset_files/style_properties/(base corpus + 17 datasets),task_splits/style_properties_pool.json, allsrc/sandbox/ext_styleprops/scripts exceptproperties.py(the 17-style registry, KEPT), andartifacts/style_properties/(26 GB, gitignored — gone for good).results/style_properties/was archived with history toarchive/style_properties/(prescreen, decodability, steering sandbox, adjudication memo).STYLE_PROPERTIES_DIRinsrc/utils/paths.pyis kept for the restart. - Reason: accumulated confounds (translation source not in one fixed convention, resampled number items, thin k-cells, wording confusion) — the user prefers a clean restart over stacked fixes.
- Redesign of the free-form style study is to be agreed with the user BEFORE any generation or compute. Rules carried forward: one fixed convention per feature in any source text (audited), >=100 samples per k for k=0..5 per property, features must not occur inside the first 12 tokens of a document.
2026-09-07 — Style-translation study, step 1: Spanish source corpus conventions and verification rule (user decisions)
- Corpus:
dataset_files/style_translation/final/<family>.json= 200 unique verified Spanish paragraphs per family (17 families fromsrc/sandbox/ext_styleprops/properties.py), each with >=5 (aim 5-7) places where the English translation must choose between the family's two styles. The Spanish is identical whatever style is tested later and never carries the tested style. - Fixed Spanish convention (user): RAE standard with ALL numbers as digits — cardinals
3 días, ordinals in ONE form3.º/2.ª(apocope3.ernormalised), percentages15 %, quotes« », rayas attached—inciso—, ellipsis..., single space, normal case, no markup. Enforced bygen_spanish.fixup, audited bygen_spanish.violations(0 violations in 5,103 candidates). Any future source text for this study must pass the same audit before use. - Verification rule: LLM judging is ONE text per call with a strict JSON schema (Claude Haiku 4.5 via OpenRouter, T=0): coherent, fluent, anchors -> k_found. Length and conventions are judged deterministically, never by the model. pass = coherent & fluent & k_found>=5 & regex_k>=5 & 0 violations & 100-230 words, then de-duplication. Agent-tool sub-agents handed hundreds of texts wrote heuristics instead of reading (WORKLOG 2026-09-07) — do not use them for bulk text judging.
- Scripts:
src/sandbox/style_translation/{families,gen_spanish,verify_spanish,collect_verdicts,calibration}.py; path constantSTYLE_TRANSLATION_DATAinsrc/utils/paths.py. Step 2 (English side / prompts / GPT-J) awaits the user.
- Minimal pairs, not two translations (user): one Gemini translation per Spanish text, in a fixed English house style = the nat pole of all 17 families (standard American English; list in
dataset_files/style_translation/README.md), enforced deterministically with the registry renderers; the alt twin flips exactly one family viarender(..., "alt"). Twins are identical outside the opportunity spans (checked, 0 diffs). - Judge scope (user): Haiku checks only the initial translation; gates = coherent + faithful. Style/conventions are judged deterministically (
k_en >= 5,audit_nat == []) — the judge hallucinated style verdicts, as it did for the Spanish conventions.fluentandstyle_consistentare recorded, not gating. - Registry fixes (properties.py): Contractions skips the modal "have to"; NumWords/OrdinalWords capitalise a spelled-out word that opens a sentence. Any later scoring uses the same detectors, so this is the definition from now on.
- Corpus of record for step 3:
dataset_files/style_translation/pairs/<family>.json(200 per family;opps[k].nat_span/alt_spanare the cue sites).english/keeps all attempts;final/stays the Spanish-only step-1 output.
2026-09-07 — Paper figure house style (user decision: "use this type of style" for all paper visuals)
- Schematics (circuit, method, decomposition explainers) are TikZ vector figures in
write_up/graphics/, sharingfigstyle.tex(Inter text, CM math, palette,\vstack/\bstackresidual-cell macros). Build:pdflatex <name>.tex && pdftoppm -r 300 -png -singlefile <name>.pdf <name>. They stay purely conceptual — no measured numbers (2026-09-02 rule). - Data plots call
apply_paper_style()fromsrc/utils/paper_style.pybefore creating figures and take colours from its paletteC: read feature / targets →C.read(green), write feature / FV / cue →C.write(blue), linear map / ridge →C.map(teal), baselines / unsteered →C.grey, counterfactual / other-task controls →C.accent(gold), secondary method variant →C.purple. Value labels vialabel_bars. No per-script rcParams, facecolors, or ad-hoc palettes. - Restyling is style-only: titles, labels, numbers, filenames and output dirs never change with it.
-
accuracy(family, style, k) = P(the completion uses the context's convention at cue k AND is a faithful, coherent English translation), one seeded T=1 sample per (text, style, k), n = 200. Style decision = registry classifier on the completion (word-internal cues prefixed by the already-generated word start); faithfulness/coherence = Gemini 2.5 Flash, one completion per call, told when the completion was cut by the 48-token cap. A completion that avoids the decision is inaccurate; its share is reported separately (
unscorable.csv). -
Cue token (definition recorded 2026-09-07): the last token identical whichever style follows; k>=1 judged on the context as it reads; sentence families use the closing period (newline after
English:for the first sentence); curly_quotes = one decision per quotation. -
Results bucket:
results/style_translation/(STYLE_TRANSLATION_RESULTSin paths.py); artifactsartifacts/style_translation/{prompts,rollouts}. -
percent_sign classifier (user decision 2026-09-07):
50 %(space before the sign) counts as the sign convention,per centas spelled out. Rationale: the space is copied from the Spanish source typography, not a third English convention.properties.PercentSign.classifychanged; step-3 percent_sign rollouts re-scored (134 decisions changed), plots regenerated. Angular quotes « » copied into curly_quotes completions stay unscorable.
- Prompt = the English twin alone cut after cue token k (no Spanish, no header); cues recomputed on the header-free tokenisation; empty-context prompts start from
<|endoftext|>. Judge = coherence/fluency only (no source to be faithful to — user decision); accuracy = convention AND coherent. Punctuation-only completions that close the sentence and carry a decision count as coherent (analysis rule; judge artefact without a reference).style_only_vs_translation.pngis the like-for-like comparison with the translation run (judge-independent). Results bucketresults/style_free_text/(STYLE_FREE_TEXT_RESULTS), artifactsartifacts/style_free_text/.results/style_translation/and its scripts unchanged.
- Steering vector: for family F, layer L (output of block L, L = 1..28),
v_nat(L) = mean h_L(cue) over step-3 prompts whose completion was CORRECT in the nat context − the same mean in the alt context; "correct" = used the context's convention AND judged faithful/coherent (style_ok and judge.ok), all k = 0..4 pooled.v_alt = −v_nat. One vector per layer per style per family; no normalisation, no projection (capture_cues.py,artifacts/style_translation/steering/vectors/). - Injection site: the k = 0 prompt (Spanish + English up to and including the FIRST cue token; identical for both styles),
α·vadded to the residual stream at layer L at the cue token only (last prompt position, prefill pass; generated tokens untouched —steer_hooks.CueSteer, unit-tested). - Sweep: α ∈ {0, 0.5, 1, 2, 4} (α = 0 = unsteered, one grid point); screen layers {2,4,6,8,10,12,16,20,24} on 50 texts, style-only, 16-token completions; confirm the top-2 screen settings per (family, target) on all 200 texts with the FULL step-3 grading (style at the cue AND Gemini faithful/coherent, 48-token cap,
cappedflag). Final (L, α) = the confirmed setting with the higher full accuracy — a setting that forces the style but breaks the translation cannot win. - Controls: unsteered α = 0 on the same 200 texts/seeds (plus step-3 k = 0 and k = 4 as references) and a counterfactual-family vector (another family's v, same style role, cyclic within the pod's shard) at the family's own top-1 (L, α).
- Results bucket
results/style_translation/steering/; artifactsartifacts/style_translation/steering/{vectors,screen,confirm}.
2026-09-08 — Style-translation step 5: sparse head selection for FV-style convention steering (user decisions)
- Head vectors: raw mean out_proj-projected output of each of GPT-J's 448 attention heads at the cue token, over step-3 prompts whose completion used the ALT convention and was judged faithful (
style_ok and judge.ok, all k pooled) — the function-vector recipe, not a nat/alt difference. Computed from the 120 fit texts only (my default, so held-out texts never enter the vectors). - Sparse optimisation: one coefficient vector c ∈ [0,1]^448 SHARED across the 17 families (pooled prompts), L1 penalty λ, AdamW lr .01, batch 64, early stopping on a 20-text-per-family validation slice — the sandbox implementation
src/sandbox/sparse_head_selection/train_sparse_heads.pyreused unchanged except for a per-token MEAN loss wrapper (my default: all_caps/sentence_caps labels are whole first sentences and would otherwise dominate the pooled sum). - Injection: v_F = Σ_h c_h C_alt[F,h] added at the cue token (last prompt position) of the k = 0 prompt at ONE layer; L ∈ {6, 9, 12, 16, 20} fitted separately, L* and λ* chosen by validation NLL (λ* = largest λ within 0.02 nats of the best).
- Golden completion / loss scope: the text_alt twin's continuation, teacher-forced, loss only on the tokens from cue+1 through the end of the alt style span (the style decision), not the whole sentence.
- Direction: alt only. Split: doc_id order 0..99 train / 100..119 validation / 120..199 test (80 per family; all graded numbers on the test texts).
- Grading as steps 3–4 (registry classifier ∧ Gemini judge). Arms: unsteered · sparse learned-c · sparse unweighted indicator sum (canonical FV construction) · other family's sparse vector (control) · step-4 residual vector and step-3 k = 4 on the same 80 texts.
- Results bucket
results/style_translation/sparse_heads/; artifactsartifacts/style_translation/sparse_heads/{heads,points,train,eval,eval_lambda}.
- Why: the step-4 vectors averaged over each pole's correct prompts independently. The pools differed in size (nat 2–3× alt where nat is the default) and in k (15–20 % k = 0 on the nat side, ≈0 % on the alt side, since the rare convention is almost never produced unprompted), so
v_nat − v_altcould carry "short context / no prior decision" rather than the convention. - Definition now: a (text, k) prompt pair enters both means only if the nat-context AND the alt-context completions were correct (
style_ok and judge.okfor both twins). Both pools then hold the same texts at the same k (87–589 pairs per family; brit_t_past smallest).capture_cues.pydefault;--unpairedreproduces the old vectors. - Scope:
results/style_translation/steering/is overwritten with the paired rerun (same screen/confirm/judge/analysis protocol). The unpaired results remain in git history (commit 17c52c5e for the figures, 146e6a6a/aac4cc6f for the failure-mode notes) and their artifacts inartifacts/style_translation/steering_unpaired/.results/style_translation/sparse_heads/(step 5) was compared against the UNPAIRED residual steering; its comparison rows readartifacts/style_translation/steering/confirmandsteering/best_config.csv, so re-runningsparse_heads_analyze.pynow compares against the paired vectors.
- Lexically diverse (the convention applies across many different words, so the model must generalise a rule): us_uk, ise_ize, brit_t_past, contractions, num_words, ordinal_words.
- Lexically identical (one fixed marker or a pure formatting choice, the same every time): whilst, ampersand, percent_sign, em_dash, ellipsis, curly_quotes, quote_punct, double_space, oxford_comma, sentence_caps, all_caps. (whilst/while, &/and and %/percent are single fixed swaps, hence this group — user ruling.)
- Every per-family figure under
results/style_translation/usesfamily_groups.grouped_grid: left block = lexically diverse (blue tint), right block = lexically identical (warm tint), each with its own title. Lists live insrc/sandbox/style_translation/family_groups.py.
- Evidence tokens = the tokens of the English context that realise the convention, in the tokenizer's NATURAL segmentation (never a forced split): for one rendered instance, the style-bearing tokens from the first token where the twin and its single-instance flip differ (cue_idx + 1; the cue token is never included) to the last token starting inside the rendered span (trailing whitespace trimmed unless the span is all whitespace). Whole-word tokens stay whole (" learned"/" learnt", " colour"); where the tokenizer itself splits, only the differing pieces count ("'s" vs " is", "st" in "Among|st"); if the span is entirely shared with the other twin (nat "Among" vs "Among|st") the divergence token itself is the evidence (" these"), as for the absence poles (" and" for no serial comma). curly_quotes: opening + paired closing mark.
evidence_tokens.py; per-family examples inresults/style_translation/read_features/evidence_tokens.csv. - Read feature r_pole(F, L) = mean over the 200 texts of the per-prompt average of hidden_states[L] over ALL evidence tokens of the 4 instances in the step-3 k = 4 translation prompt (no success filtering; both poles use the same texts). Per-instance-index means (1st…4th instance) are stored as well. All 28 layers captured; the 9 screened layers reported.
capture_evidence.py→artifacts/style_translation/read_features/<family>.npz. - Diagnostic (not a decision): cosine between the read difference r_nat − r_alt and the paired cue-token steering vector v_nat per layer.
- Read steering vector u_nat(L) = r_nat(L) − r_alt(L) (step-6 evidence-token means), u_alt = −u_nat; no normalisation. Note: ‖u‖ is 0.2–0.5 of the residual norm and 10–35× the cue-token write vector at early layers; α = 1 ≈ a full swap of the evidence-token state.
- Setting: the step-3 k = 3 translation prompts (3 rendered instances in context, model about to make the 4th choice), both context poles. α·u added at layer L at EVERY evidence token of the 3 instances (positions from the step-6 extraction; the cue token is untouched unless it is itself an evidence token — adjacent opportunities, 5.5 % of prompts, almost all all_caps where the 3rd sentence's final period is the 4th decision's cue). Directions: nat context → alt (u_alt), alt context → nat (u_nat). Sweep layers {2,4,6,8,10,12,16,20,24} × α ∈ {0.5,1,2,4}; screen 50 texts style-only → confirm top-2 on 200 texts with full grading, as in step 4.
- Metric: accuracy = TARGET convention at the 4th decision AND judge OK. Baseline = the unsteered k = 3 prompt scored toward the target (how often the model deviates from its context anyway). Reference = step-3 accuracy at k = 3 when the context genuinely was the target pole. Control (user choice) = another family's read vector at the same positions and (L, α).
- Results bucket
results/style_translation/read_steer/; artifactsartifacts/style_translation/read_steer/{screen,confirm}; hooksteer_hooks.PositionSteer.
- Families added (chosen by the user from a 20-item brainstorm; items judged overlapping or trivial were dropped):
uk_vocab(truck/lorry),register(begin/commence),unit_abbr(5 km/5 kilometers),diacritics(cafe/café),latin_abbr(for example/e.g.),hyphen_compound(email/e-mail),flat_adverb(drive slowly/drive slow),irreg_past(dove/dived),latin_plural(indexes/indices),title_abbr(Doctor/Dr.). All ten are LEXICAL infamily_groups.py(16 LEXICAL vs 11 FIXED). Registrysrc/sandbox/ext_styleprops/properties.py(27 properties), Spanish specsfamilies.py(27). - Scope: the new families get the Spanish→English translation setting only (no English-only
style_free_textvariant), then steps 3 (accuracy vs k), 4 (paired cue-token steering), 6 (read features at the evidence tokens) and 7 (evidence-token steering incl. the full 0–27 layer screen). Step 5 (sparse heads) is not rerun for them. - House style = nat pole of all 27 families. The 17 original families' corpus, prompts and results stay as built on 2026-09-07 (17-family house style); their texts may contain the new families' alt forms ("repair", "railway", "well-being"), which does not affect their own decisions.
pairs_check.pytherefore reports ~100 "nat audit bad" texts per original family and 0 per new family — expected, not a defect. - Detector design for ambiguous surfaces (flat_adverb): -ly adverbs are detected positionally (after any non-auxiliary word, clause-final: before punctuation / conjunction / preposition); bare flat forms only after a verbal word within 8 words with no copula in between, never after a measure ("30 cm deep") and never "even" (focus adverb). When the conservative alt detector under-counts on an alt twin,
collect_english.build_twinsaccepts the pair if the classifier reads every rendered site as alt (and every nat site as nat). Rationale: the classifier at the cue is what scores the model; find_opps on running text must not corrupt house-style normalisation. - Latin-plural lexicon: only pairs whose anglicised plural is standard American English; funguses / nucleuses / larvas / vertebras / alumnuses removed (Haiku judged them non-fluent).
- Haiku
style_consistentis not gating for the English pass (unreliable for lexical families); the deterministicaudit_natis the style authority — same principle as the Spanish step (agent violations not used, audit is authority). - Deterministic translator fixes: "that's," → "that is," before house-style normalisation (Gemini's rendering of «es decir»); the connective is exempt from the contractions renderer.
- Sentence cutter (2026-09-11, scoring fix):
scoring.cut_sentenceno longer treats the period of an abbreviation (Dr., Mr., Mrs., Ms., Prof., St., Mt., Ave., Rd., Blvd., Sr., Jr., No., vs., approx., e.g., i.e., single initials) as a sentence end. Before the fix 513 of title_abbr's 1,236 unscorable completions were the model's "Mr."/"Dr." cut off before the name. The ten new families' step-3 records were rescored fromtail_raw(rescore_rollouts.py; title_abbr 680 tails / 502 decisions changed, unit_abbr 121 / 1, latin_abbr 70 / 0) and the changed tails re-judged. The 17 original families were NOT rescored (dry run: ≤ 7 tails and 2 decisions per 2,000 records would change) — their published results stand.
src/sandbox/style_translation/models.pyis the model registry;--model <key>oncue_tokens.py,build_prompts.py,rollout.py,analyze.py. The default (gptj) keeps every existing path; any other model getsartifacts/style_translation/<key>/{cues,prompts,rollouts}andresults/style_translation/<key>/. Cue tokens are tokeniser-specific and are recomputed per model; the GPT-J cues indataset_files/style_translation/pairs/stay the dataset's cues.qwen25_base=Qwen/Qwen2.5-7B(base, not Instruct), bf16, left padding, no BOS. Step 3 only (accuracy vs k) — steering/read-feature steps were not requested for it.
- Pool = lexically diverse families whose k = 4 accuracy (convention ∧ faithful) is ≥ .30 for BOTH poles on the full 200-text corpus:
16 families — us_uk, ise_ize, contractions, num_words, ordinal_words, uk_vocab, register, diacritics, unit_abbr, latin_plural, title_abbr,
zh_simp_trad, pt_acordo_eu, pt_br_eu, es_rae2010, ru_yo (
results/style_translation/qwen25_base/pool.json). Fixed-marker families are excluded from both train and test. de_1996 and zh_tw_hk passed the cheap 45–60-text check but fell below on the full corpus → dropped. - Feature gate before a family enters the map: steered accuracy's Wilson CI excludes the unsteered rate, in both directions.
- Generalisation test: leave-one-family-out over the pool + one fixed 2/3–1/3 split stratified by axis (British, Portuguese, Chinese, digits); within-axis vs cross-axis hold-outs reported separately. Coverage diagnostics are computed before steering.
- Multilingual corpora: English base text → Gemini adaptation in the natural pole with anchors → Gemini back-translation = source; Gemini verification (fluent, natural pole only, anchors, faithful); cap 200 per family.
- 2026-09-14 (later) — Feature gate variants: the strict gate (significant lift in BOTH directions) admits only 5 of 16 Qwen families because the natural pole of 7 families is at ceiling unsteered (.72–.94); the headroom-aware variant (a direction with unsteered ≥ .70 is exempt) admits 12. Both are reported; the 12-family pool is the main map result pending the user's ruling. Judge prompt: empty "so far" context (first opportunity opens the text) is now described as the beginning of the translation; 1,852 verdicts re-judged (Qwen steering confirm + step-3 rollouts).
- Coding-language conventions are a third family group (
family_groups.CODE, domain "code", src "Task"): the model writes code for a task description and the convention applies across many identifiers / literals / statements. 60 specs; 55 pass the cheap k=4 both-poles ≥ .30 check on 50 tasks and again on the full 200-task corpus (results/style_translation/qwen25_base/code_pool_full.json). - Full corpora only for the 55 that passed at 50 tasks; steering confirm runs WITHOUT the counterfactual (other family's vector) control arm
for now (
steer_confirm.py --no_control). - Cheap-corpus (50-task) rollouts and steering artefacts are kept aside under
artifacts/style_translation/qwen25_base/{rollouts_cheap_k4,steering_cheap}/; the canonical code results are the full-corpus ones underresults/style_translation/qwen25_base/{code,steering}/.
- Evidence tokens for code = the tokens of each earlier in-context opportunity that actually DIFFER between the nat and alt renderings
(token-level difflib inside the opportunity span; shared content tokens inside merged spans such as
['rock', 'paper', …]are dropped). Whitespace-only evidence (indentation, blank lines, operator spaces) counts like any other token; no family excluded. Text families keep the 2026-09-10 whole-span rule (evidence_tokens.py,diff_only/n_shared_droppedper instance). - The cue position is never steered in the read test: if the last evidence token of the last in-context instance is the cue (adjacent
opportunities, ~0–1 % of prompts), that position is dropped (
read_steer_screen.k_items); prompts left without evidence are skipped and counted. - Read vector = paired mean difference of the evidence-token means over k = 4 prompts (documents with evidence in both poles); capture and
evidence-steering screen over ALL layers 0–27; evidence steering tested on k = 1 AND k = 3 prompts (
--k_ctx; artifactsscreen_k1/confirm_k1, resultsread_steer_k1/); no control arm (as on the write side). - Gate for "the read feature works": steered accuracy toward the target ≥ 50 % of the flipped reference (step-3 accuracy at the same k of
prompts whose context genuinely shows the target) — reported as
reach/pass_50inbest_config.csvtogether with the Wilson-CI-vs- unsteered flag; the user judges the final results and decides the gate. Read-out check = cosine per layer between the read vector and the write vector (same layer and L24); no linear probe. read_features_analyze.py,read_steer_analyze.py,read_steer_layers_analyze.pytake--tag/--familiesso code results live underresults/style_translation/qwen25_base/code/{read_features,read_steer,read_steer_k1}/, never mixed into the text buckets.- 2026-09-15 (later) — Shared read layer for code = L8 (user decision; write layer stays L24). Screen evidence: the mean curve over the 55 families peaks at L6–L8 with α = 4; at L8 with the best α per family the mean target rate is .62 vs .71 for each family's own best layer at k = 3 (loss .085; .55 vs .65 at k = 1), and α = 4 is the best shared α at L8 (nat→alt .54, alt→nat .63 at k = 3). The judged confirm arms were run at each family's top-2 screen cells, so a judged L8-only number needs a small extra confirm run.
- 2026-09-15 (later) — Read→write map on the code pool (user decisions): all 55 families; ONE fixed random split 2/3–1/3 stratified by
convention category (seed 2026,
code/read_write_map/fixed_split.json, committed before fitting), no LOFO; per-prompt ridge from the evidence-token read activation (L8 main, sweep L5–L10) to the cue-token write activation (L24); judged by held-out geometry only (convention-vector cosine vs the shuffled-pairing control and the mean-training-vector baseline, prompt-level R², coverage diagnostics). Steering with predicted vectors is a separate task. - 2026-09-15 (later) — Working read layer for the code map = L10 (user decision): per-prompt held-out centroid R² .340 at L10 vs .333 at L8 (80/20 split; spread over L5–L10 is .03). L10 is the working choice for improving the map; the other layers get re-optimised once an improvement is found. L8 stays the reported read-steering layer.
- 2026-09-16 — k = 8 probe for code (user request): k = 8 only (no k = 5–7); k = 8 prompt = 8 in-context opportunities, decision at the 9th;
documents with ≥ 9 opportunities only; headline = PAIRED k = 4 vs k = 8 on the same documents. k = 8 items live in the same prompt and
rollout files (append mode) so write vectors can pool or select k = 8; the canonical write vectors remain the k = 0..4 ones
(
steering/vectors/); any k = 8 vector goes tosteering/vectors_k8/viacapture_cues.py --ks 8 --out_tag k8.
- Finding: in the identifier-naming families a large share of decisions were FORCED by the code rather than by the convention — once an
identifier is defined, later mentions must reuse its spelling for the code to work (
absolute_value…current_number = absolute|_value);selfmust match the parameter name; lines inside a block must keep its indentation; the task text can pin a function name. Forced share of the k = 4 decision: py_self_name .99, py_loop_vars .63, py_snake_camel .59, js_camel_snake .56, py_abbrev .56, js_hungarian .41, py_bool_prefix .39, py_class_naming .35 (py_private .04, py_const_naming .02). Forced decisions were far easier (e.g. py_class_naming k = 4 nat .26 free vs .82 forced), so k-shot accuracy and some steering numbers were inflated. Quotes, keywords, operators, literals and the other formatting families are not affected (either rendering stays legal). - Decision: rebuild the 12 affected families (9 identifier families + py_self_name + py_indent + py_tabs) so that every opportunity is a
genuine choice (
code_free.py): identifier families — no identifier of the span, in either rendering, may occur earlier in the code or be pinned by the task text (backticks / call form); py_loop_vars — only the binding in aforheader; py_self_name — only theselfparameter of adef; py_indent / py_tabs — only the first line of a new block. Pairs keep only free opportunities (≥ 5, 5th before 75 %); documents that fall short are replaced by newly generated twins whose generation hint asks for many fresh choice points (code_build.EXTRA_HINT,--target 200). The old data for these families is archived underdataset_files/style_translation/pairs_v1_forced/andartifacts/style_translation/qwen25_base/v1_forced/; every downstream step (step 3, write and read features, map) is rerun for them. - 2026-09-16 (later) — After the free-opportunity rebuild the both-poles ≥ .30 cutoff drops py_bool_prefix (alt .12) and py_indent
(2-space .24); the code pool is now 53 families (
code_pool_full.json, old 55-list kept aspool_v1_forced). js_hungarian (nat .66 → .38) and py_abbrev (nat .72 → .40) lost most but stay. The k = 8 numbers incode/k8/predate the rebuild for the 12 affected families.
- py_indent, py_tabs, blank_lines, operator_spaces, comma_space, line_wrap:
text_alt = code_whitespace_alt.transform(family, text_nat)(tokenizer-aware; strings, docstrings and comments are never edited; both twins must have the same AST). The Gemini rewrite had slipped content edits into 11 of ~1,100 documents and had "applied" the convention inside prose (docstrings, commented-out code), which the rule refuses — so an opportunity in these families is always a genuine code-formatting choice. - line_wrap: joining a wrapped call / def / literal also drops Black's trailing comma (a 1-tuple's or subscript's comma is kept) — the one non-whitespace difference allowed in these families (user-reviewed as legitimate, bug-4 explainer t109).
- Records carry
alt_rule: true;code_build.build_oneapplies the rule for these families instead of the REWRITE prompt.
- early_return, py_with_open, py_ternary: an opportunity counts only if its two renderings differ beyond whitespace. A block choice
(
else:,with, an if/else block for a ternary) forces the indentation of every following line; those diff spans are kept inopps_allbut never counted, never cued, never used for prompts or features. - A whitespace-only difference before the first real choice is a rewrite slip, not a convention: it takes the natural rendering
(
code_free.harmonise_leading_ws), so the k = 0 prompts of the two poles are byte-identical. - Generator guard: a Python twin that does not parse is rejected (
code_build.valid_python; py2_print / py2_except alternatives exempt).
- Every Python twin must parse (
code_build.valid_python, py2_print / py2_except alternatives exempt); the generator rejects otherwise. Style hints must say what stays untouched (py_paren_if, trailing_commas, comment_case EXTRA_HINTs) — Gemini over-applies a bare hint. - bash_subst:
text_alt = code_subst_alt.convert(text_nat); nested substitutions use Bash's escaping (`…` inner pairs, backslashes before $ \ ` doubled per level). Acceptance = independent parse-back to the same substitution tree + bash -n on both twins.
- A rename x → y in the alternative twin is invalid when y already exists as a standalone name in the natural code (
code_free.rename_collisions, token-aware); such pairs are rejected byconsistent_twinsand regenerated. Generation hints for py_class_naming / py_bool_prefix forbid the collision up front.opps_all(the full diff) is mandatory on every free-filtered record — never re-filter from the filtered list.
- A closing bracket or closing quote is never an opportunity (
code_free.closing_symbol); the free-opportunity machinery (opps_allfull diff,oppscounted) now applies to every code family. Every family has exactly 200 documents (css_shorthand pending its follow-up run). - The builder's re-filter (
apply_free) may only remove a pass; generation-time guards (parsers, degenerate, shared fraction) are final.
- bash_subst (
$(…)vs backticks) and py_with_open (with open(…) as f:vsf = open(…)…f.close()): the second half is guaranteed by the first, so only the opener is counted (code_free.OPENER_ONLY). Diagnostic to remember: an accuracy / unscorable curve that alternates with the parity of k means the opportunity list interleaves free and forced decisions.
- rust_question, c_comment_style, py_ternary, py_enumerate: a construct that sits on one line of the natural twin is ONE opportunity — the first
counted span on that line; later spans on the line follow from it (
code_free.one_per_line).code_free.counted(fam, rec)is the single definition of the counted list (per-span free rule, then the line rule); sweeps must use it. - Solution length guidance is per family (
code_build.LENGTH), so families that need ≥ 8 line-level constructs get 30–50 lines.
- For first-mention (identifier) families a completion's convention is read from the first NEW name it introduces; names already in the code so
far or pinned by the task are forced reuses and never decide (
code_scoring.decide_code). Same principle as the free-opportunity rule, applied to scoring. hex_constants: first integer literal. py_abbrev / py_bool_prefix: LLM-labelled lexicon of new identifiers (Claude Sonnet 5). - Any change to a scorer must be validated against independent LLM labels on a random sample (
validate_scoring.pypattern) and reported as agreement before / after; the log-prob margin at the cue is the reference measure that no classifier can bias.
- A counted opportunity must be the FIRST diff span of its construct: no earlier content-or-eligible span on the same line of either twin (docstring_style: per docstring; sql_join_style: per statement). Name / self / indentation families keep their own unit (new name, new block).
- The integrity sweep now includes
two_counted_on_a_line; any new family must pass it. When one family shows a forced-decision artefact, audit ALL families for the same class at once (structural check + log-prob first-vs-later comparison) instead of fixing symptoms one by one.
- Steering vector per family and layer: v = μ_nat − μ_alt, where μ is the unpaired mean cue-token activation (output of block L) over k ∈ {3, 4}
prompts whose sampled completion followed the context's convention AND passed the judge, built from the 150 training documents only.
Held-out split is deterministic:
write_split.heldout(doc_id)= crc32 % 4 == 0 (~50 docs); held-out 0-shot prompts are the only test set. - Intervention: add ±α·v at the cue token (last prompt token) of the 0-shot prompt during prefill, one layer at a time; nothing at generated tokens. α is a multiple of the raw difference. "Every other layer" = blocks 2, 4, …, 26; block 28 excluded (post-norm mean does not match the hook site and cannot affect later tokens).
- Grading identical to unsteered 0-shot rollouts (T = 1 sample, 48 tokens, cut_code, context-aware decision, Gemini judge); success = target convention AND judge OK. First-token log-prob margin kept as the classifier-free check.
- Hyperparameters are chosen ONCE on 10 random pool families and shared by all 56: the cell maximising mean success over families and both directions (ties → smaller α, then earlier layer). Result: layer 26, α = 2 (pending user approval). Late-layer-only effect and the first-token-not-convention behaviour are recorded in WORKLOG; do not present L26 steering as evidence about mid-layer write features.
- All 56 pool families are steered at (L24, L26) × (α 2, 4) on the first 40 held-out documents (sorted doc_id;
write_sweep.py --limit 40), each towards natural and towards alternative; the 10 sweep families are reused and cut to the same 40 documents. No single cell is selected;full56/by_cell.csvreports the four means side by side with the unsteered baseline and the k = 4 in-context accuracy. - Sweep lesson carried over: only layers 24/26 respond and the effect is largely a first-token push; families whose convention needs follow-through, or whose construct the 0-shot model never writes, do not respond. Do not read these numbers as evidence about mid-layer write features.
- The write feature of a code-convention family is the cue-token mean-difference vector at the output of block 24,
v = μ_nat − μ_alt(μ from k ∈ {3, 4} correct completions on the 150 training documents), applied as ±2·v at the cue token of a 0-shot prompt. This is the go-to definition for all further code-convention work (read→write maps, comparisons, write-ups); the other three cells infull56/stay as reported context. Headline numbers at this setting over 56 families: → natural .56 (unsteered .40), → alternative .43 (unsteered .13), judge OK .84; k = 4 in context .74 / .69.
- Comment families (comment_language, comment_case), bug 14: one opportunity per comment unit. A
#comment is a unit; of a docstring only its FIRST line with text is a unit (later lines are free); a#line that continues the previous#comment's sentence (previous line without terminal punctuation, this comment starting lowercase) is free. A unit counts only if its first word differs between the poles, so the cue is the comment opener (#or the docstring line's indentation). String literals are never units. First words that differ only by a suffix (Increment|ar) stay counted. Residuals accepted as they are: 18 docs (3 below 5 opportunities, 15 whose leading unit is free so their k = 0 prompts differ). comment_case keeps its docstring units (few, 51). - Audit of the other families: the token diff merges the closer of one construct with the opener of the next across a shared
line break, so cues land before a forced closer or inside a construct that already showed its style. Four fixes were proposed:
1 split raw spans at shared line breaks — NOT adopted in general (in k-shot prompts a closer still tests whether the convention
is continued sensibly); flagged for a possible revisit; applied only inside the families that fixes 2–4 touch, where needed.
2 a content span blocks every line it covers (gated to sql_keyword_case, php_array, py_literal_ctor [with the split],
py_join_concat, c_comment_style) — ADOPTED. 3 closers followed by
,/;are forced closers — ADOPTED, universal. 4 line-level alignment for the block-restructuring families py_ternary, py_comprehension, rust_question, js_arrow, py_with_open (code_line_align.line_opportunities: each replaced block of lines = one construct, cue at the first divergence; blocks with several adjacent constructs are split one per construct; forced closers/rewrite artefacts free) — ADOPTED with the split. Families that only show the fix-1 pattern (py2_print, py_quotes, js_quotes, py_fstring, bash_test, line_wrap) are left as they are. - sql_keyword_case: the LLM-written alternative twin lower-cased identifiers, string literals and comments too; the alternative
twin is rebuilt BY RULE from the natural one (
code_sql_alt.py), lower-casing only SQL keywords, as for the whitespace families. - Documents that fall below 5 opportunities, whose alternative rewrite violates the family rule, or whose k = 0 prompts differ are regenerated from their own tasks under all guards; nothing else is regenerated. Changed families are re-sampled for step 3 and for write steering (L24, α = 2 plus the other three cells for the full56 table).
- Evidence-token definition agreed so far: the token right after each demonstration's cue, plus every other demonstration token that
differs between the two poles' renderings (opening AND closing quotation marks; all comment words; the closing position of an
absence pole such as
:vs):). Scope of the alignment (whole prompt vs demonstrations only) and the treatment of tokens that differ only by a leading space after an inserted symbol: still open. - 2026-09-18 additions after the bug 14/15 re-sample:
write_sweep.pyskips held-out documents whose k = 0 prompts differ between poles (accepted comment_language residuals) instead of asserting; js_arrow generation hint now asks for ≥ 10 arrow functions (30–45 lines) because the line-level rule counts ONE opportunity per arrow; 41 documents whose 5th opportunity sits past 75 % of the document are accepted as a residual (user decision); the pool stays 56/60 after the re-sample.
Evidence tokens of a k-shot prompt = (1) every token before the query cue is in scope (demonstrations, free occurrences between them,
and anything after the last demonstration); (2) the token right after each demonstration's cue, plus every other token that differs
between the two poles' renderings (token-level alignment of the twins; absence poles get their closing position, e.g. : vs ):;
free repeated uses such as later self/this count); (3) SECTION RULE for comment_language, comment_case, docstring_style: every
token of each comment / docstring before the query cue, opener to closer, shared tokens included (the query's own, just-opened section
excluded); (4) a word that differs only by the whitespace in front of it (an inserted symbol took its space: (idx vs idx) is NOT
evidence, EXCEPT in comma_space, operator_spaces, line_wrap and py_tabs where that whitespace is the convention; pure whitespace tokens
always count; (5) cue tokens (demonstration and query) and the task header are never evidence. Identical words inside a rewritten
region are matched as equal by the alignment and are not evidence (an earlier claim to the contrary was a measurement error).
Evidence sets range from 3 tokens (js_strict_eq, r_assignment) to ~150 (docstring_style) per k = 3 prompt; the read feature is the
mean over them, so per-family results must be read with that in mind.
- Read feature of a family, pole and layer = mean over prompts of the per-prompt mean activation over the prompt's evidence tokens
(
code_evidence.pyrule), at the output of block L (1..28, layer 0 stored too); prompts = exactly the write feature's set (k ∈ {3, 4}, training documents, completion correct, unpaired). Read vector r_L = mean_nat − mean_alt. - Read steering test = held-out k = 3 prompts whose demonstrations are in the WRONG style, ±α·r_L added at EVERY evidence token of the prompt at layer L (prefill only, nothing at the query cue or generated tokens); success = target convention AND judge OK; baseline = the same prompts unsteered; ceiling = the right-style k = 3 accuracy. Sweep families = the 10 write-sweep families. Selection rule as for the write sweep (one shared cell, max mean success). Result: L8, α = 4 (pending approval; α not saturated).
- 2026-09-18 USER DECISION: read-steering setting = layer 8, α = 4 (top-5 cells L8–L18 at α4 tied within 1.6 points; layer choice
within 6–18 is arbitrary, α still rising at 4 — a wider α range was offered and declined for now). Full-pool run done at that
setting; results in
results/code_styles/read_steering/full56/.
- Generated code must not contain padding: filler / placeholder / dummy / illustrative lines, discarded results, comments announcing
examples or counting occurrences, stacked negations, meta-comments naming the convention.
code_build.PADDING_PATTERNS+padding_lines()is a builder guard and the sweep checkpadding_line(code_pairs_check.py);# Example usageblocks andnot a or not bare NOT padding. - For py_not_in, rust_question, py_is_none, float_literals the numeric occurrence hint is REMOVED ("use the construct wherever the
logic calls for it"; a realistic module with a driver is allowed; LENGTH 40–80; 5th opportunity < 85 %); all 800 documents were
regenerated with Claude Opus 5 and accepted only if TWO independent reviewers (Opus 5, GPT-5; 14-item checklist; strict JSON with
quoted lines) both pass, with a feedback-repair loop (reviewer issues fed back to the generator; ~1.6× the per-attempt yield).
The ≥ 8 hint stays for the other families. The other 52 pool families still trip
padding_linein 2,281 documents — open. - Task pools whose specs do not naturally need the construct cannot yield honest documents with many occurrences; the honest count
of
not inis ~5 per document. Prefer tasks that inherently need the construct when new families are built.
- Reviewer: GPT-5 only (Opus 5 + GPT-5 with an AND rule was too strict; user decision). Automatic guards carry the padding burden: padding_lines,
leak_lines, pyflakes unused/undefined names, JS unused declarations, sandboxed run of the natural twin, string-literal equality (
code_static_checks.py). - Alternative twins come from exact, validated rules wherever one exists (
code_rule_alt.py+code_rules_a/b/c.py, 37 families); a declining rule falls back to the LLM rewrite and is counted. Tasks come from the designed pools (tasks_designed_<family>.json); the generic pools are retired for code. - Lesson: task reservations across parallel drivers must exclude every accepted-but-unmerged id (regen8 collision bug).
Hungarian-notation prefixes encode a variable's type, so the alternative twin cannot be derived by an exact rule (the only naming family
where that holds); model rewrites leaked prefixes into string literals and task-pinned parameters stayed unprefixed, so no regeneration passed
review (0 / 9, then 0 / 20). The family stays in the dataset and the artefacts but is excluded from code_pool.json by
code_cutoff.py --exclude js_hungarian (default) and is not reported as a data issue any more. Pool = 53 / 60.