Skip to content

issue #667 followup: tf-margin gate→behavior DV swap#587

Draft
superkaiba wants to merge 26 commits into
mainfrom
issue-667
Draft

issue #667 followup: tf-margin gate→behavior DV swap#587
superkaiba wants to merge 26 commits into
mainfrom
issue-667

Conversation

@superkaiba

Copy link
Copy Markdown
Owner

Same-issue follow-up round tf-margin-gate-vs-behavior on task #667 (source: user-chat 2026-07-01, plan v6 auto-approved at 6 GPU-h).

Closes-half task #667.

Recomputes #667's gate→behavior bridge with #722's continuous teacher-forced fixed +/- completion margin as the SECONDARY behavioral DV, on the #537 2-index grid (16 sources × 30 targets, 3 behaviors — em, sycophancy, fact). Everything else inherited from #667's a36 round: same #537 adapters, same 30 target contexts, same read layer L14, same 7-family clustered bootstrap, seed 42, same g0(C') per cell.

New in this round:

Backend: RunPod explicit override, one 8×H100 pod, wave_size=8.

superkaiba and others added 26 commits June 25, 2026 02:02
Port i537_contexts.py verbatim from issue-537 (#641 SHA fff2e92); all
deps (i406_conditions MARKER_ID/MARKER_TEXT, personas.PERSONAS) already on
main, no library drift. The ported registry + frozen P0 inputs reproduce
#537's registry_hash f12061d6... exactly (== G_meta pin), validating the
context-construction is byte-faithful.

gate_chain.py: realized activation gate (B1), rank-one residual + stacked-
Delta_V SVD (A3.8), whitened key-query gate + the B3 reduction unit test
(A3.9/A3.10), A3.7 source-write reads, partial-Spearman (A3.6, C10),
family-clustered bootstrap + shuffled null. Pure-CPU, GPU-free, unit-testable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loads #537 adapter as PeftModel (rsLoRA gauge asserts), teacher-forces the
frozen base greedy R through base+trained, reads mean-over-response v0/v+ at
L14/{7,21}; base-side c_C/c_Cp (all 28 layers) for the whitened gate; t+/t-
(A3.7) split from the untagged #537 mix by source-context prompt prefix;
fact r_B re-extracted fresh (#594 diff-in-means, absent from #658 r_b.pt).
Content-hygiene safe for em harmful rows (counts + activations only, no text).

CPU-verified: all 4 probe pools load at correct counts; build_messages
produces correct chat structures across all 7 context families.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B3 reduction unit test gates A3.9/A3.10 (HALT on failure). Reads the per-cell
npz store + #537 G_meta (git_commit pin) + #658 sigma_c/r_b (probe_pool_hash
pin); writes A3_{6..10}.json with reproducibility metadata. fact r_b read from
the store's re-extracted r_b_fact; em/syco from #658. --skip-store-pin runs a
synthetic-store CPU smoke (no HF, no model).

CPU-verified end-to-end on a structured synthetic store: A3.7 cos_pos=1.0 beats
null; A3.8 sigma1_frac=0.996 (rank-one by construction), chance=0.25; A3.6 runs
for em (broad_em r_b) + fact (re-extracted); B3 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dispatcher: prefetch (SHA-pins + B3 + rsLoRA parity probe) -> extract
(CVD-pinned per-source-adapter subprocess waves + HF tensor upload) ->
analysis (A3.6-A3.10). Every phase derives its cell subset from the SAME
--behaviors/--sources/--targets filters; smoke IS the sweep scaled down.
Emits [phase=...] + terminal [phase=done] + poll_pipeline-conformant sentinel;
explicit env={**os.environ} + load_dotenv() at main-top.

Phase-0 prefetch CPU-verified end-to-end: B3 PASS, registry_hash f12061d6 ==
G_meta pin, G_meta git_commit 34f2502 pin OK, #658 probe_pool_hash ad687bec
pin OK, rsLoRA parity probe em r=32/a=256/use_rslora=True. Dry-run extract:
CVD pinned per cell, terminal [phase=done], sentinel conforms to
_SENTINEL_REQUIRED_KEYS.

G_meta load: fail-loud (committed in git, NOT on HF; sparse worktrees add it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 tests: B3 reduction unit test (multi-dim), whitened-gate self=1 +
identity=cosine + diag-anisotropy, realized_gate exact/orthogonal/zero-raises,
stacked-ΔV SVD rank-one-recovery + full-rank-chance, A3.7 cos_pos/frac_ctx,
partial-Spearman confound removal/signal recovery, family_of grammar,
clustered bootstrap, shuffled null.

Extract CPU-runnable arithmetic (GPU-bound carve-out item 1): _row_to_messages
(em chat / prompt-completion / unknown-raises), _system_signature
(question-invariant), mean-resp span resolution + dual-side + c_C all-layers,
all against the REAL Qwen-2.5-7B tokenizer + a 2-layer CPU stub (no 7B load).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fix)

Root cause of a >2x compute deviation: the extractor generated the frozen base
R per-prompt via HF generate (~43 GPU-h projected, 6x the plan's 7.2 — and a
CLAUDE.md "use vLLM for generation" violation). Re-architected to Phase A
(vLLM batched-generate ALL R for a source's targets x probes from BASE, then
reap the engine per gotchas) -> Phase B (HF load base+trained, teacher-force
the pre-generated R for mean-resp v0/v+). Projection now ~1.3 GPU-h (0.18x
planned). HF greedy stays for the tiny fact-r_B path + CPU-only smoke fallback.

26 gate-chain tests still pass (CPU arithmetic unaffected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…3.9 full grid

Round-2 code-review fixes (3 BLOCKER + 4 MAJOR + 1 CONCERN):

- BLOCKER a310-oracle-gplus-not-extracted: extract post-FT key/query
  (c_C_postft/c_Cp_postft) under the loaded PeftModel; A3.10 computes the
  oracle g+=(k+,q+,M0) vs realized, g0 vs realized (distinct), g0 vs oracle,
  and key/query drift — no longer a relabel of A3.9.
- BLOCKER rslora-parity-gate-skipped-on-production-extract: phase_extract runs
  the NUMERIC diagonal-write parity probe (g_self==1 + write-ratio HALT) as its
  mandatory first step, so the production `extract` launch hits the gate.
- BLOCKER cached-artifact-coverage-unverified: fail-loud validators for r_b
  columns/recipe/layers, sigma_c shape/keys/layers, G_meta per-cell fields, and
  G_tensor cid equality; removed the silent continue on a missing G_meta cell.
- MAJOR a39-ablation-incomplete: A3.9 full 3x3 key{c_C,psi_t,psi_delta} x
  metric{I,diag,whitened} grid + shuffled-key/query controls + E0 + predict-mean
  baselines.
- MAJOR cosine-baseline-not-cosine: true_cosine helper (both norms); A3.9
  reports it as the cosine baseline; the self-normalized I metric kept + renamed.
- MAJOR a36 CI/null mismatch: A3.6 bootstrap + null now on the partial statistic.
- MAJOR lambda sweep: condition-number sweep recorded in A3.9.
- CONCERN a37-icl-source-tpos-tneg-gap: F3-ICL positives matched by demo
  role-pattern; A3.7 reports per-source t+/t- coverage + dropped sources.

Tests: 40/40 pass (oracle, validators-fail-loud, partial-bootstrap, lambda
sweep, true-cosine, ICL split). Smoke: Phase-0 prefetch CPU PASS on real
artifacts (pins + standalone validators + parity config); Phase-2 analysis PASS
on synthetic store (full grid + oracle + drift); dispatcher dry-run CVD-pinned +
sentinel + [phase=done].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-uses-tneg)

Round-3 BLOCKER fix. frac_ctx = ||v0(C) - v0(C_neg)|| / ||delta_contra|| (R3-1)
was reading t_neg (the negative-persona ANSWER activation that feeds
delta_contra) instead of v0(C_neg) (the negative persona's base-CONTEXT vector).

- extract: new extract_v0_C_neg() captures the negative-panel base-context
  activation under each negative persona's prompt (no answer), matched to the
  v0(C) mean-over-response recipe, panel-averaged; wired into Phase A (neg base R
  gen via vLLM) + stamped on the primary-layer source payload as v0_C_neg.
- analysis: run_a37 reads v0_C_neg from the store for frac_ctx; t_neg still feeds
  delta_contra only. Absent v0_C_neg -> NaN frac_ctx (non-silent) +
  v0_cneg_missing / n_sources_with_v0_cneg coverage fields.
- tests: regression test on a synthetic store with distinct t_neg vs v0_C_neg
  proves frac_ctx reads v0(C_neg) exactly (fails pre-fix with the t_neg value);
  plus an absent-v0_C_neg NaN test.

Open CONCERN frozen-r-cache-not-used left as a scope caveat (grep comment near
the vLLM R-gen call site).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…subprocess)

The rsLoRA NUMERIC parity probe ran in-process in the dispatcher parent,
loading the base model + PeftModel and initializing CUDA before the per-cell
extract subprocesses fork (vLLM forks its own EngineCore worker). A live CUDA
context in the parent poisoned that fork chain -> RuntimeError: Cannot
re-initialize CUDA in forked subprocess (bug_class
dispatcher_cuda_init_before_subprocess_fork).

Fix: the GPU numeric probe now runs in a one-shot subprocess (the new
`parity-probe` CLI entrypoint) so the dispatcher parent never touches CUDA.
HALT semantics preserved: a non-zero child rc (failed parity assert / crash)
or a missing result file re-raises before any extraction wave. The CPU-only
smoke path stays in-process (gauge config check, no CUDA, no subprocess).

Regression test tests/test_issue667_dispatcher.py pins that the GPU path
dispatches a subprocess (parent never calls _numeric_rslora_parity, CUDA stays
uninitialized) and the HALT gate fires on rc!=0 / missing result.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ess (gotcha #26)

Round-4 fixed the dispatcher CUDA-init-before-fork hazard (parity probe ->
subprocess). This closes the OTHER half: issue667_extract.py constructs
vllm.LLM() inside vllm_generate_R AFTER main() touches AutoTokenizer, which
poisons the EngineCore fork under vLLM V1's default fork() worker method
(silent worker death 1-4s after init). Per gotcha #26:

- Set os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") at the
  extract module top, before any `import vllm`.
- Belt-and-suspenders: inject the same var into the dispatcher's per-cell
  extract subprocess env (guards a future import-reorder).
- Regression test: (a) runtime env pin after import, (b) per-cell subprocess
  env carries spawn + threads to subprocess.run, (c) mechanizable scan that
  any issue667 extract/dispatch script importing vllm + AutoTokenizer sets the
  spawn guard above the vllm import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ct fix)

The r4 CUDA-isolation refactor moved the rsLoRA parity probe into a one-shot
`parity-probe` subprocess: the launch site passes `--behavior <x>` and main()
reads `args.behavior`, but the parser only ever registered `--behaviors`
(plural, for extract/analysis). The subprocess died deterministically with
`AttributeError: 'Namespace' object has no attribute 'behavior'` -> rc=1 ->
the dispatcher's fail-loud HALT.

Fix: register `--behavior` (singular) as a dedicated parity-probe arg next to
the existing `--source`/`--seed`/`--result-out`. Leaves `--behaviors` (plural)
untouched. Adds two regression tests that drive main()'s parity-probe argparse
end-to-end (GPU work + credential check mocked, CPU-only) — both fail pre-fix
with the exact AttributeError and pass post-fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mpleted cells

Fix the fact-behavior extract crash (AttributeError: 'dict' object has no
attribute 'strip' at i537_contexts._casualize): pool_fact_30.json's
ood_framings rows are {"framing","question"} dicts, not strings, so
load_eval_probes("fact") returned a mixed list that every downstream
message-builder threaded into a chat content / _casualize as if it were a
string. New _probe_text normalizer flattens any dict probe (question ->
prompt -> text) at the single load site so all consumers see a flat
list[str].

Add resume-skip (default ON, --no-resume-skip to force a full re-run) to the
extract dispatcher: cells whose .npz tensors already exist under
<TENSORS_DIR>/<behavior>/<source>_seed42 are NOT relaunched. The relaunch
will be FAST — 32/64 cells (all em + all sycophancy) are already on the pod's
disk and will be skipped; only the un-extracted fact cells run.

Tests: _probe_text on the real ood_framings dict shape + defensive variants;
build_messages_for over a dict probe through the _casualize F4 path (no
AttributeError); phase_extract resume-skips a completed cell and re-runs it
under --no-resume-skip; _cell_already_extracted predicate (empty dir != done).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flag

Round-7's resume-skip predicate `any(cell_dir.glob("*.npz"))` counted any
non-empty cell dir as done, silently accepting a partial dir left by a
mid-cell crash (the extractor writes per-(target,layer) .npz incrementally).
Default-on silent partial-acceptance is the prohibited fail-fast class.

- issue667_extract.write_cell_done_sentinel: atomic `.done` (tempfile +
  os.replace) written ONLY after every (target,layer) .npz lands; payload
  records targets/layers/expected-count for future validation.
- dispatcher _cell_already_extracted: gate resume-skip on `.done` presence,
  never on stray .npz.
- `--backfill-sentinels`: one-shot migration that validates each cell's .npz
  complement and writes the sentinel only for complete cells (incomplete ones
  reported + re-extracted). Lets the relaunch resume-skip the 32 complete
  on-disk cells.
- tests: flip the unsafe predicate test (.npz w/o sentinel = NOT done), add a
  partial-cell resume-skip regression + backfill complete/incomplete/idempotent
  tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mpty-acc class)

run_extraction now calls assert_full_npz_complement(cell_dir, targets, layers)
immediately before write_cell_done_sentinel, mirroring the --backfill-sentinels
complement check (issue667_dispatch._expected_npz_for_cell). A target whose
probes all return empty responses skips its per-layer .npz write
(_extract_one_target's `if not acc[li][0]: continue`); without this gate the
unconditional sentinel would stamp a TRUSTED .done over an incomplete cell that
the resume-skip then silently treats as complete (round-8 BLOCKER
resume-skip-empty-acc-unconditional-sentinel). The gate raises RuntimeError
listing the missing files, writes NO sentinel, and leaves the partial .npz on
disk so the dispatcher's resume-skip re-extracts the cell.

Regression test test_run_extraction_empty_acc_target_raises_and_writes_no_sentinel
drives run_extraction with one empty-acc target (CPU-only, heavy callees stubbed)
and asserts (a) RAISES, (b) no .done, (c) partial .npz preserved — fails pre-fix,
passes post-fix. Plus a direct unit test on the helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n-space wording, a36/a37 label-clip fix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gate-vs-tf-margin round

Same-issue follow-up tf-margin-gate-vs-behavior (plan v6): swap the gate->behavior
DV from #537's judged rate G to the continuous teacher-forced fixed +/- completion
margin (tf_margin_leak), computed 2-index (16 sources x 30 targets) on the #537 grid.

- scripts/issue667_tf_margin_extract.py: vendors score_answer_logprobs_batched +
  _assistant_suffix_len + build_fixed_pairs BYTE-IDENTICAL from the unmerged
  issue-722-tf-margin branch (AST-verified), ONE substitution messages_for_instance
  -> i537_contexts.build_messages via build_messages_for; adds extract_tf_margins_2index
  (base-margin cache reused across sources).
- scripts/issue667_tf_margin_analysis.py: vendors _spearman + clustered_bootstrap_spearman
  (N_BOOT=2000); run_gate_vs_tf_margin join + g0-correctness gate (reproduce #667's
  0.13/0.16/0.40 within +/-0.02, HALT rc=3 on miss) + measurement-validity gate
  (Spearman(tf,G) point est >0 AND CI excludes zero per behavior).
- scripts/issue667_build_fact_pool.py: the one new data artifact -- on-policy fact
  fixed +/- pool, Sonnet judge >50 pos / <50 neg, drop REFUSAL/malformed both arms,
  cap 40/side floor-N equalize-down, HF upload.
- scripts/issue667_tf_margin_apply_parity.py: Phase-0.5 rsLoRA apply-parity probe
  (reproduce #537 committed E_plus_source within +/-0.10; HALT rc=3 on drift).
- scripts/issue667_tf_margin_dispatch.py: PASS_UNIFIED dispatcher (parity -> fact-pool
  -> extract -> analysis); wave_size = min(detected device_count, --n-gpus) with a
  RAISE-on-0-GPU guard (the a36 wave-size bug fix); [phase=...] + [phase=done] +
  _SENTINEL_REQUIRED_KEYS sentinel.
- scripts/issue667_tf_margin_figure.py: hero gate-vs-tf-margin scatter + validation bars.
- tests: vendor byte-identity, the 2-index driver + base-cache reuse, both gates on
  synthetic pass/fail, and the wave-size regression (detected 8 -> 8, clamp, RAISE on 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…coverage gate + aggregate deliverables

Addresses the round-1 reconciled FAIL (2 BLOCKERs + 2 CONCERNs):
- BLOCKER 1 (fact-pool-adapter-generation-base-only): the 'adapter' arm now
  generates through the loaded `trained` PeftModel via HF .generate()
  (`_hf_generate_with_adapter`), not the base-only `vllm_generate_R` path; only
  the base arm keeps vLLM on GPU. Per-arm generation-provenance + adapter path
  in the pool provenance; hard `model is trained` guard.
- BLOCKER 2 (tf-margin-cell-coverage-not-enforced): `run_gate_vs_tf_margin`
  enforces the full expected off-diagonal key set per behavior BEFORE writing
  the headline — HARD-FAIL (CellCoverageError -> rc=4 -> epm:failure
  tf_margin_cell_coverage_incomplete) for em/sycophancy always and for fact
  unless dropped; SOFT-DROP for a dropped fact arm.
- CONCERN 1 (fact-arm-floor-drop-not-enforced): below-floor build writes a
  DROPPED_FROM_HEADLINE.sentinel; the dispatcher skips the fact extract and the
  analysis soft-drops fact (via --fact-dropped or the sentinel).
- CONCERN 2 (missing-aggregate-tf-margin-deliverables): the analysis writes
  margins.json + tf_margin_leak.json (plan v6 §6.5 primary_deliverable).

Regression tests: tests/test_issue667_build_fact_pool.py (adapter-vs-base
labeling + sentinel) + new BLOCKER 2/CONCERN 2 cases in
tests/test_issue667_tf_margin_analysis.py. 92 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ale-percell headline carry)

BLOCKER fact-arm-drop-stale-percell-still-carried: the fact soft-drop was
nested inside the cell-coverage `if missing:` block, so it only fired when
the store had missing cells. On a resume (resume_skip=True dispatcher default,
#667 multi-run history) a stale-but-COMPLETE per-cell fact store leaves
`missing` empty, and fact reached the headline with fact_softdropped=False —
contradicting plan v6 §4.3, which mandates the drop as a sentinel invariant.

Fix: unconditional `behavior == "fact" and fact_dropped` guard at the TOP of
the per-behavior loop, before the correctness/coverage/stat compute; `continue`
skips fact entirely. The coverage-gated fact branch is now dead-for-drop but
kept harmless. Regression tests fail pre-fix / pass post-fix.

Also hardens the _generate_completions adapter-arm guard with a `model is not
base` clause (catches a mis-load / loop-wiring drift that returns the base as
"trained") + a negative test (Codex round-2 Minor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al Betley judge

Fix the round-3 sweep-launch false-fail (epm:failure v8 / epm:strategy-pivot v2):
the +/-0.10 apply-parity tolerance was mis-calibrated for N_SAMPLES=10 (Wilson-CI
half-width ~0.27 at rate 0.7 -> guaranteed false-fail under a perfect apply).

- N_SAMPLES 10 -> 100 (Wilson half-width ~0.09 < 0.10 tol; ~3-4 min of judge calls).
- Replace the hand-rolled `misaligned` rubric with the project's canonical Betley
  two-axis EM judge (eval/alignment.format_betley_judge_user_msg + parse_judge_json;
  aligned<30 AND coherent>=50), drop-never-coerce on REFUSAL/CODE/out-of-range.
  Addresses deferred CONCERN apply-parity-betley-judge-restatement.
- Extract decide_parity() pure helper; record judge_prompt/judge_threshold in the
  provenance JSON.
- Add tests/test_issue667_tf_margin_apply_parity.py (5 tests: Wilson calibration,
  Betley-judge wiring, known-good PASS, known-bad drift-caught, drop-never-coerce).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…librated floor (import, no shadow)

The round-4 fix raised the apply-parity probe module's N_SAMPLES 10->100 (the
Wilson-CI floor for the +/-0.10 tolerance), but the tf-margin dispatcher's
--n-samples default stayed at 10 and phase_apply_parity forwards it explicitly
to the subprocess, SHADOWING the module default. A production `all ...` launch
would re-arm the round-3 false-fail. Import N_SAMPLES from the probe module so
one module owns the value; extract _build_parser() so a regression test can
assert the default >= the calibrated floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant