Skip to content

Phase 2 — fine-tune fleet + trained store + ground-truth leakage (#664)#487

Draft
superkaiba wants to merge 67 commits into
mainfrom
issue-664
Draft

Phase 2 — fine-tune fleet + trained store + ground-truth leakage (#664)#487
superkaiba wants to merge 67 commits into
mainfrom
issue-664

Conversation

@superkaiba

Copy link
Copy Markdown
Owner

Closes task #664. Phase 2 fine-tune fleet + trained store + ground-truth leakage. Plan v3, 88 GPU-h auto-approved.

superkaiba and others added 30 commits June 25, 2026 02:42
## Consistency Check: #664 vs re

[task.py]
…entation v2 -->

## Implementation

[task.py]
[task.py]
## Code-Reviewer Verdict — PASS

[task.py]
…g panel)

- 64-cell realized grid (16 gate marker + 40 content transfer + 4 nulls + 4 seed-1042)
- contexts via on-main issue594_common 50-context battery
- per-behavior recipes from #537 methodology doc §2 + marker rules
- EM/bad-medical/insecure-code expressed in-process via train_lora (#545 opt-in
  overrides) -- the named §4.4 divergence (turner_em.yaml not on main)
- contrastive negative panel with HARD panel∩sources=∅ assert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er/fact/EM carve-outs)

Builds prompt-completion JSONL per (source, behavior, arm, dose) cell per
plan v3 §4: marker programmatic carve-out, taught-fact carve-out, EM
published-corpus replication, on-policy sycophancy/refusal with 80% yield
floor; zero-truncation gate + panel-disjoint-from-sources + marker-token
asserts. Also de-ambiguate unicode x/- in common.py comments (ruff RUF002/3).

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

Per-cell P2.2 worker: answer-side mean v_plus(C') on all 50 contexts x 28
layers + v0(C') spine/own-battery + v0(C_neg) + last-input c_C (trained+base)
+ t_CB/r_plus at source; per-probe (R>=8) probe-split inputs; target_context_role
per row (kill-3b exclusion); marker four-float slot stats (#530 contract) via
compute_marker_slot_stats with the gauge assert. Merge-read-delete per cell
(§9.2 disk budget); left-pad position_ids; vLLM spawn + reap.

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

A source below the 80% on-policy yield floor crashed the whole fleet: the builder
exited 1 and the dispatcher's subprocess.run(check=True) raised CalledProcessError.
Plan v4 §11 mandates graceful degradation (drop + report, never crash). The builder
now exits DROPPED_SOURCE_EXIT (3) + writes a per-cell drop sentinel; the dispatcher
treats rc==3 as skip-this-cell-and-continue (rc!=0 stays fatal), writes a top-level
dropped-cells manifest, and excludes dropped cells from train / extract / eval /
manifest / upload / repro-card. Pinned by tests/test_issue664_pivot_invariants.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
superkaiba and others added 27 commits June 26, 2026 21:48
Per-probe AutoTokenizer.from_pretrained calls trigger an HF Hub
model_info() HTTP probe inside transformers' _patch_mistral_regex,
hitting the 2500/5min HF API rate-limit mid-fleet (crashed after
~16 cells at bm_librarian_posonly_d1_seed42 gen-phase, 04:24 UTC).

Module-level cache fix: single load per process, no behavior change.
Lines 181 + 315 retained — they're once-per-cell, not per-probe.
…s + wrapper)

Add data-parallel sharding to the Phase-2 dispatcher so the independent
(source x behavior x arm x dose) cells fan across 8 GPUs instead of running
serially on one (the ~30h/7-idle-GPU recovery). Pure orchestration -- per-cell
science is unchanged; cells partitioned by i % num_shards == shard_id after the
post-P2.0 drop filter.

- issue664_dispatch.py: --shard-id/--num-shards args (default 0/1 = no sharding,
  backward-compatible); _validate_shard + _shard_filter helpers; shard-0-only
  gates on phase0/manifest/readability-assert/live-judge/upload over the FULL
  post-drop fleet (all_cells), train/extract/eval loops over the shard subset.
- Refactor the epm:results sentinel into _write_results_sentinel and fire it on
  --phase p3 shard 0 too: the 8-way wrapper runs phased (never --phase all), so
  the terminal contract would otherwise never be written (poll_pipeline #448).
- issue664_launch_parallel.sh: pod-side wrapper sequencing p0 (1 GPU) ->
  p2 8-way (CVD-pinned per shard) -> p3 (1 GPU); emits the watched
  [phase=...]/[phase=done] contract + PID file; aborts before p3 on any shard
  failure (rc=1 -> poll reads NOT-done).
- tests/test_issue664_sharding.py + tests/manual/print_issue664_shards.py: unit
  test + dry-run pinning disjoint-union cell coverage + validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
r7 placed _write_manifest / _marker_readability_assert / _live_judge_smoke
inside the --phase p2 block, so shard 0's p2 process fired the A7 readability
HALT on its own ~2 of 20 marker cells the instant it finished -- racing the
~18 marker cells on shards 1-7 whose marker_slot_stats.json did not exist yet
(silently continue'd; checked>0 masked the gap). That re-broke the #664
round-2 B3 readability gate. Move the three finalizers to the top of --phase
p3 (ahead of upload_artifacts), which the wrapper invokes only after waiting
for every p2 shard, so the full marker_slot_stats.json set exists. Rename the
relocated phase_log names p2_* -> p3_*.

Also fix the open raw-completions-upload-ignores-cells-arg concern:
_upload_raw_completions now globs per selected cell.eval_key (>=1 file per
cell, fail-loud on zero) instead of a blind reg_root.rglob.

Adds two structural tests pinning WHERE each finalizer runs (fail pre-fix,
pass post-fix).

Closes BLOCKER post-p2-finalizers-race + CONCERN raw-completions-upload-ignores-cells-arg.

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

A single llm.generate(3000 prompts, ...) call in _elicit_secure_code
deadlocks pod-664's CUDA worker (vLLM v1 EngineCore stuck on IPC, 0% GPU
util forever). Chunk _greedy and _sample internally into batches of
VLLM_GREEDY_CHUNK_SIZE (default 500, env EPM_VLLM_GREEDY_CHUNK_SIZE);
order- and structure-preserving, behavior-identical for every caller. The
per-chunk [vllm-chunk] INFO log keeps poll_pipeline's freshness check alive
over the previously 60-min-silent stretch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eadlock pivot, both-pods futex_wait_queue)

The #664 r9 chunked-greedy fix landed but the EngineCore still deadlocks
on futex_wait_queue across all threads inside the first llm.generate()
on BOTH the wedged previous pod and the fresh recovery pod (epm:progress
v54). r10 strategy pivot: env override to flip enforce_eager=True at
launch, bypassing CUDA-graph capture entirely. Default unchanged (off).

To use: EPM_VLLM_ENFORCE_EAGER=1 bash scripts/issue664_launch_parallel.sh
…CURE_CODE_MAX_NEW (prefix-cache scheduler deadlock pivot)

enforce_eager (r10) was not enough: the first _greedy(300_prompts, max_new=1024)
from _elicit_secure_code still deadlocks the vLLM v0.11.0 EngineCore on
futex_wait_queue even with CUDA graphs bypassed (epm:progress v55). The remaining
engine-config suspect is the prefix-cache scheduler: the secure-code batch is the
FIRST call where all 300 prompts share one long system-prompt prefix, unlike the
earlier marker_R per-source calls. Add enable_prefix_caching passthrough (default
on, "0" disables) and an env-overridable secure-code max_new (default 1024
unchanged) as the escalation knob. Both defaults preserve current behavior.

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

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

r11 wired EPM_VLLM_PREFIX_CACHING into the dispatcher's _vllm_engine only; the
dispatcher shells out to issue664_eval (p2 eval-gen) and issue664_extract_store
(p2 extract), each with its own LLM() defaulting enable_prefix_caching=True, so
the vLLM v0.11.0 shared-prefix EngineCore deadlock would recur at p2 eval-gen on
the AdvBench battery AFTER the train+extract GPU spend (concern
p2-llm-constructors-prefix-cache, reconciler-binding FAIL).

Centralize the two knob reads into issue664_common.vllm_env_kwargs() (honors
EPM_VLLM_ENFORCE_EAGER / EPM_VLLM_PREFIX_CACHING, case-insensitive, raises on a
typo instead of silently re-enabling caching) and **-splat it into all three
production LLM() constructors. Regression test pins the env semantics + AST-asserts
every production LLM() routes through the helper (fails pre-fix, passes post-fix).

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

The r11/r12 smokes used identical placeholder prompts (vLLM dedupes them, ran
9.5s) and never reproduced the production deadlock on 300 DIVERSE prompts from
phase2_insecure_code.jsonl. This harness drives the real production prompt set
through the same chunked _greedy path so a hang here reproduces production.

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

v16 'hang' root cause: _render() reloaded AutoTokenizer.from_pretrained on
EVERY call, and the p0 elicitation loops render hundreds of prompts per ctx in
tight list comprehensions (marker_R alone = 300 questions x every source +
negative ctx). The repeated disk loads added MINUTES of dead silence before each
_greedy call (v16 sat ~5.5 min between 'engine built' and the first
[vllm-chunk]), making a still-progressing phase read as a 0%-GPU hang to the
poller and to the diagnosing observer.

Real-diverse-prompt reproducer (scripts/issue664_real_prompt_smoke.py) proves
the batch itself is fine under the r12 prefix_caching=False fix: 4 configs all
PASS on pod-664 -- fresh n=300 (15.4s), prior-calls n=300 (15.4s),
production-faithful n=3000 chunk=500 (6 chunks clean), and the exact v16
marker_R shape n=300 max_new=2048 (24.9s). The r11/r12 smokes used IDENTICAL
placeholder prompts (vLLM dedupes them) so never reproduced production load.

Fix: cache the tokenizer (functools.lru_cache, one load/process) + add per-ctx
[marker_R] progress logging so a long phase is never silent. Mirrors the r6
_prompt_text_for cache (cf73e52) applied to the dispatcher's own renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s; correct r11 false-positive memory

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s p2 rehydrated-adapter crash)

The tokenizer is invariant under a LoRA merge, so load it from base_model_path
rather than the adapter dir. A rehydrated adapter dir (downloaded from HF with
only the LoRA artifacts, or one that does not exist locally yet) lacks
tokenizer_config.json, and AutoTokenizer.from_pretrained then treats the
absolute local path as a Hub repo id and raises HFValidationError (the p2
fan-out crash on all 8 shards). Same fix independently arrived at in
scripts/rerun_arms_ac.py::merge_lora_fixed.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…training; rehydrate path declined per plan §4)

The launcher ran p0 -> p2 -> p3, SKIPPING p1 (training) on the assumption that
an external step trained the fleet. On a fresh pod with no local-volume
persistence that assumption breaks silently: p2's extract_and_eval_cell finds
no local adapter to merge and crashes (concern p2-no-adapter-rehydrate-train-skipped).

Plan §4 "Recipe-only reuse (HARD constraint)" requires training EVERY cell
fresh -- no prior adapter loaded at the weight level. So the launcher now trains
the whole fleet itself: a p1 (train) block with the IDENTICAL 8-way CVD-pinned
fan-out as p2, inserted between p0 and p2. The dispatcher already exposes
--phase p1 standalone (run_all trains the shard subset via train_cell), so this
is a launcher-only change. Shard i trains then extract+evals the same cells it
owns; train_cell writes the adapter dir p2 reads AND pushes it to HF, and is
idempotent (skip-if-adapter-exists) so a restart resumes cleanly. The 48 r1-r6
adapters on HF are NOT reused (plan §4); fresh training overwrites them via
train_lora's normal upload.

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

The smoke marker-mix built 0 rows during the r15 launcher smoke because the p0
skip-if-cache-exists guard on the SHARED smoke/production marker_R cache path
left the 4 smoke questions absent from the production-keyed cache. Generalizable
trap for any builder keyed by question text with a shared-path cache guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P1 (training) crashed all 8 shards onto physical GPU 0 with OOM on the
2026-06-28 H200 launch. Root cause: the launcher passed `--gpu-id 0` to
every parallel shard while relying on `CUDA_VISIBLE_DEVICES=$shard` in
the shell env. But `_warn_if_cvd_disagrees` in src/explore_persona_space/
train/sft.py:151 warns then UNCONDITIONALLY rewrites
`os.environ["CUDA_VISIBLE_DEVICES"] = str(cfg.gpu_id)` (line 1221 for
train_lora, 1583 for merge_lora). So the gpu_id=0 clobber wins, every
shard pins to physical GPU 0, all 8 OOM on H200 (143GB single-GPU cap).
The pre-r16 comment "in-process clobber is silently defeated by
import-time cuInit" was wrong — the warning's own message says "the env
value is NOT respected here. ... pass +gpu_id=N instead".

Fix: pass `--gpu-id "$shard"` in BOTH the p1 and p2 loops so train/sft.py
pins each shard to its own physical GPU. The leading
`CUDA_VISIBLE_DEVICES=$shard` is kept as belt-and-suspenders (it agrees
with cfg.gpu_id, so _warn_if_cvd_disagrees stays silent).

Also add `START_PHASE` arg so we can resume at p1 after a crash without
redoing the ~25-min p0 setup that already wrote baseline_propensity.json
and all training mixes. Default p0 runs the full sequence; each phase is
guarded by `should_run`.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
P2 (extract+eval) crashed 7 of 8 shards with `torch.AcceleratorError:
CUDA error: invalid device ordinal` at `mem_get_info(index)` inside
`AutoModelForCausalLM.from_pretrained(... device_map={"": gpu_id} ...)`
(extract_store.py:153/_answer_side_means, then _marker_slots). Root
cause is the same family as r16: r16 made the launcher pass
`--gpu-id "$shard"` to subprocess.run, but extract_store.py THEN used
`gpu_id` (1-7) as the in-process device index. After the dispatcher's
env pins `CUDA_VISIBLE_DEVICES=$shard`, the in-process CUDA view has
exactly one device at index 0, so `cuda:1..7` is an invalid ordinal.
Only shard 0 survived (index 0 is valid in the restricted view).

Fix three sites in scripts/issue664_extract_store.py:
- `_extract_all`: `device = f"cuda:{gpu_id}"` → `device = "cuda:0"`
- `_marker_slots`: same change, preserving the CPU fallback branch
- `_marker_slots` AutoModelForCausalLM.from_pretrained:
    `device_map={"": gpu_id}` → `device_map={"": 0}`

The `--gpu-id` CLI arg is retained: it is forwarded to `merge_lora`,
which handles its own CVD pin (sft.py:_warn_if_cvd_disagrees + clobber).
Inside extract_store after that pin, ALL device math is on index 0.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
…nly)

Plan §11 Option A->B routing: the a7-assert HALTed the fleet at p3 because 7
band-stopped marker cells read non-clean at the faithful use_rslora=True gauge.
Adopt Option B (staged classic alpha/r=2.0, use_rslora=False, eval-apply only)
per #601's recovery procedure.

- merge_lora: add force_use_rslora / force_classic_alpha_over_r override kwargs
  (default None = faithful Option A). Override patches a COPY of adapter_config
  in a temp dir; original adapter dir + HF artifacts never mutated.
- extract_store: --read-gauge {optA,optB}; optB+marker -> staged classic gauge.
- dispatch: --marker-read-gauge {optA,optB} threaded to extract subprocs;
  --behavior filter restricts per-cell loops (p3 finalizers stay full-fleet).
- launch_parallel.sh: EPM_MARKER_READ_GAUGE env -> --marker-read-gauge; optB
  also adds --behavior marker at p2 (16 marker cells re-extract; 32 non-marker
  p2 results stay valid).

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

Recovery for the disk-cleaned pod-664: only the 20 mk_* cells remain local; the
48 non-rf/sy cells are fully on HF; the 16 rf/sy cells were never trained. The
r18 p3 finalizers assert LOCAL existence for the WHOLE 64-cell fleet and
re-upload unconditionally -> crashes on the 48 absent cells + blows the HF
256-commits/hr cap.

Cherry-pick (adapted) from main's WaveDispatcher dispatcher (lines 1197-1328):
_cell_extract_eval_done / _expected_eval_files / _expected_store_files /
_classify_cell_hub_state / _cell_artifacts_on_hub / _cell_done_anywhere, plus a
new _hub_data_listing (one retried full-repo listing for the whole loop, the data
repo intermittently 504s) and HF_MARKER_SLOT_PREFIX in issue664_common.

Wired into p2 (_run_p2_loop skips a cell done locally OR on HF) and the p3
finalizers (_upload_raw_completions / _upload_store_tensors skip HF-complete
cells, upload only fresh ones, exact-set verify the fresh uploads). ADAPTATIONS
vs main: marker-slot HF surface is OPTIONAL (r18 never uploads it; A7 assert
reads it locally; the 20 mk_* have it local), and _cell_artifacts_on_hub takes a
pre-fetched listing to avoid 64 full-repo round-trips. Preserves the issue-664
vLLM deadlock-escape knobs and does NOT introduce fleet.py.

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

Sequences p0/p1/p2 for sycophancy then refusal (each 4-shard data-parallel,
CUDA_VISIBLE_DEVICES per shard) then p3 full-fleet finalize. The cherry-picked
HF-aware-skip primitives make p2 skip the 48 HF-complete + 20 local mk_* cells
and p3 upload only the 16 fresh rf/sy cells; the dispatcher's own
_write_results_sentinel fires at p3 shard-0 and the wrapper emits the single
terminal [phase=done]. EPM_MARKER_READ_GAUGE=optB carried through for r22
consistency (no-op for rf/sy). Loads .env for HF_TOKEN/WANDB_API_KEY.

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

phase0 derived its elicitation behavior set + build-mixes loop from the FULL
realized grid (--behavior is applied in run_all AFTER phase0), so a
`--phase p0 --behavior sycophancy` recovery still ran marker_R elicitation +
build_marker and crashed on the under-filled `default` marker_R cache (4/300).
Fix: scope phase0's cells to --behavior when set (symmetric with the p1/p2
loops), so the 16-rf/sy recovery only builds rf/sy on-policy caches + mixes and
never touches marker_R. Also wire _cell_done_anywhere into the build-mixes loop
(one HF listing for the loop) so a partially-rehydrated pod skips re-building
mixes for cells already complete locally or on HF.

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

On-policy elicitation of the two alignment-conflicting content behaviors on
Qwen-2.5-7B-Instruct yields 0-5.5% judge-positive (epm:experiment-implementation
v22), below any trainable floor -> plan v7 Option C drops them (a finding, not a
code workaround; the 80% floor is unchanged). transfer_behaviors loses
sycophancy + refusal; realized_grid now returns 48 (= 64 - 16 rf/sy cells). The
syco/refusal eval COLUMNS still fire on surviving trained models per applies_to()
/ ROBUSTNESS_COLUMNS (v7 A18); only the trained adapter cells are removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 20 marker_slot_stats.json (PRIMARY marker-gate DV, §6.5 row 2) are present
locally on pod-664 (r22 marker leg, A7 PASS) but absent on HF -- the r18/r23
dispatcher does not upload the marker_slot surface by design. Standalone
idempotent uploader (content-hash skip; fail-loud post-upload count==20 +
hash-match) closes the gap BEFORE pod teardown so the off-pod analyzer can read
the primary DV (#613 teardown-loss class). --smoke uploads+verifies one cell.

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

The smoke caught it: list_repo_files(recursive=True) 504-Gateway-Timeouts on the
~67k-file data repo (same flakiness as r23). The upload itself worked (librarian
cell landed) but the verify crashed. Replace the full-recursive listing with a
targeted get_paths_info on the exactly-expected paths (per-path, fast, 504-immune,
1 retry on transient 5xx).

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