Written entirely by Claude, as a working record for agents more than for human readers: every number with its source file, and every claim since withdrawn. Some facts here may be out of date; where it disagrees with the post, trust the post. For the readable version, see the companion.

Phase 2 — the recipe search

Nine four-trait training runs on 2026-08-19/20 that moved the zoo recipe from a saturating rsLoRA DPO to Open Character Training's own objective, policed by six gates written so they could fail.

historicalverified 2026-09-07

Between the design of the Qwen3.5-4B experiment (2026-08-19) and the main 134-trait sweep (completed 2026-08-20 11:29 UTC) sit nine training runs of four traits each — extraverted, warm, organized, imaginative — whose job was to decide the recipe. The final recipe is written up as Stage-one training configuration; this page is the search that produced it.

Phase 2 is budgeted at $6 in qwen35/plan.json and carries actual: null — "NOT MEASURED, never zero" — because of an app-name collision that put the main sweep inside phase 2's billing. See costs.

The gates

qwen35/phase2_gates.py is the instrument, and its own docstring states the standard: "Every gate is written so that it CAN fail; a gate that can only print is not a gate." It runs against written artefacts and exits nonzero on any failure. It has three states, not two — PASS, FAIL, and UNVERIFIED, and UNVERIFIED is not survivable either, because these gates stand in front of roughly $500 of GPU.

gate name what it asserts
0 TREATMENT APPLIED the container recorded the lora_alpha (and optionally beta) that was asked for. "A control that verifies its outcome and not its INPUT can be perfectly executed and meaningless."
1 TARGETING module count equals the count derived from the downloaded config, and at least one linear-attention lora_A tensor exists
2 IDENTITY BY SHAPE down_proj LoRA dimensions match the config's intermediate_size and hidden_size — "Metadata can be copied; a tensor shape cannot"
3 EFFECTIVE SCALE the effective LoRA scale is 2.0, whichever rule produced it
4 STACK local library versions match the versions the container recorded
5 TRAINING HEALTH loss fell and the logged reward margin is <= 30
6 DECOMPOSITION READS IT a Gram over the adapters is finite and positive on the diagonal with no off-diagonal cosine above 0.99

Three of these carry a correction in their own body, and each is a method lesson:

The arms

Each log is four traits, one epoch, 445 pairs, 13 optimizer steps, on Qwen/Qwen3.5-4B with 248 targeted modules. Stack recorded in the container: torch 2.13.0, transformers 5.15.1, trl 1.10.0, peft 0.20.0, accelerate 1.14.0.

log what varied outcome
phase2_run.log first attempt crashed by its own guard: RuntimeError: ZERO linear-attention modules targeted -- this is exactly the silent failure phase 2 exists to catch (train_qwen35.py:555)
phase2_run2.log rsLoRA r=64 alpha=128, effective scale 16.0, loss_type ["sigmoid"], kl_coef 0.0 saturated: loss 0.6931 -> 1.4e-11 (imaginative) and 4.07e-10 (organized), logged rewards/margins 40.55
phase2_alpha16.log asked for lora_alpha 16 the treatment did not travel: adapters still at the old scale, margins 40.49, losses 1.57e-11 to 1.44e-09. This is the incident gate 0 exists for
phase2_alpha16b.log lora_alpha 16 with rsLoRA, effective scale 2.0 (phase2_adapters_a16/extraverted/runmeta.json#expected_scaling) healthy: margins 13.38-15.46, losses 0.6931 -> 2.3e-06 (imaginative) to 0.0155 (warm)
phase2_beta05.log beta 0.5 margins 64.31 (imaginative), losses to 2.12e-30 (organized) — the arm whose loss reading motivated the margin threshold
phase2_nll.log add the OCT NLL-on-chosen term first-step loss rises to ~0.89-0.93 (the auxiliary term is present from step 0), final 0.137-0.164, margins 12.74-13.18
phase2_kl.log add the OCT KL term sq_approx_kl 5.606, kl_term 0.005606 visible in the log, losses 0.9068 -> 0.1529, margins 18.42
phase2_plainlora.log plain LoRA alpha 128 (same effective scale 2.0) plus both auxiliary terms losses 0.8893 -> 0.1659, margins 18.36
phase2_gateclear.log the candidate final config, run to clear all six gates losses 0.8893 -> 0.1668 (warm), 0.9018 -> 0.1427 (organized); recorded margins 10.63, 13.20, 13.76, 18.41

Timestamps from the log tails run 2026-08-20 00:53 (run2) through 01:34 (alpha16), 02:14 (alpha16b), 02:29 (beta05), 06:28 (nll), 07:22 (plainlora) to 10:43 (gateclear).

The configuration that cleared the gates

From qwen35/phase2_runs/archive/gateclear_plain_a128_kl.json, which is the container's own record:

field value
base model Qwen/Qwen3.5-4B
lora_r / lora_alpha 64 / 128
use_rslora false (plain LoRA)
expected_scaling 2.0
learning_rate 5e-05
beta 0.1
loss_type / loss_weights ["sigmoid","sft"] / [1.0, 0.1]
kl_coef / kl_applied 0.001 / true
epochs / effective batch / max length 1 / 32 / 1024
n_pairs / optimizer_steps 445 / 13
targeted modules 248 of 249 linear modules (only lm_head excluded)
reward margins over the four traits 10.632954665592738 (warm), 13.197356292179652 (imaginative), 13.763283048357282 (extraverted), 18.407941273280553 (organized)

The KL form is recorded verbatim in the runmeta and is not a generic one: "OCT sq_approx_kl: mean over completion tokens of the SQUARED per-token log-ratio (policy-ref), per sample, then batch mean over [chosen; rejected]; transcribed from maiush/OpenRLHF openrlhf/trainer/dpo_trainer.py _get_batch_kl. NOT the k1 or k3 estimator."

The NLL-on-chosen term at weight 0.1 is what stops DPO collapsing to a trivial discriminator, and it came from reading the OCT repository rather than the paper. qwen35/HANDOVER.md says plainly: do not drop it.

What was rejected, and why

The trap that made the whole phase have to run again

qwen35/HANDOVER.md records it as trap 1: a decision must live in the DEFAULT, not in a launch flag. PC_USE_RSLORA defaulted to rsLoRA while the sweep launched with it off, so a new launcher trained 240 adapters at effective scale 16.0 and the whole phase was rerun. The fix is that the plan banner now prints the effective scale and flags anything that is not 2.0. This, plus the alpha-16 non-travelling treatment above, are two of the four occurrences of the project's signature failure mode — see lesson-objective-must-travel.

Phase 5: the main sweep

qwen35/phase5_sweep.log (16,417 lines) is the 134-trait run under the cleared configuration, ending App completed after the Modal client timed out waiting for final logs at 2026-08-20T11:29:57+0000. Per-trait first and last losses are in the log's summary lines, e.g. warm: targeted=248 excl=0 loss 0.8893205523490906 -> 0.16580402851104736.

qwen35/phase5_margins.json records that per-trait margins cannot be attributed from that log, and says why in its own note field: TRL's rewards/margins lines are bare JSON dicts with no trait name, container id or PID, and about four concurrent containers interleave, so line order is arrival order. Every final_margin is null; only loss_start and loss_end come from the unambiguous per-trait summary lines. The per-trait margins for the 134 sweep were later recovered from container runmeta records into qwen35/phase2_runs/archive/phase5_sweep_134.json (134 records, each with reward_margin, e.g. active 10.964571339743477) — which is the same lesson as HANDOVER's trap 6: runmeta.json had it all along.

One stale artefact, flagged. qwen35/phase2_runs/results.json currently holds 40 records at use_rslora: true, expected_scaling: 16.0, loss_type ["sigmoid"], kl_coef 0.0, seed: 1, corpus_is_seedpaired: true — that is the seed-paired arm at the superseded objective, not any phase-2 arm. Phase 2's own four-trait results live in phase2_runs/archive/results.json. A reader following the gate script's default --results path would read the wrong file.

Related: Stage-one training configuration, The seed floor, The null control arms, costs, lesson-objective-must-travel, Open Character Training (Maiya et al., 2025).

Sources

  • qwen35/phase2_gates.py
  • qwen35/phase2_run.log
  • qwen35/phase2_run2.log
  • qwen35/phase2_alpha16.log
  • qwen35/phase2_alpha16b.log
  • qwen35/phase2_beta05.log
  • qwen35/phase2_kl.log
  • qwen35/phase2_nll.log
  • qwen35/phase2_plainlora.log
  • qwen35/phase2_gateclear.log
  • qwen35/phase2_runs/archive/gateclear_plain_a128_kl.json
  • qwen35/phase2_runs/results.json
  • qwen35/phase2_adapters_a16/extraverted/runmeta.json
  • qwen35/phase5_sweep.log
  • qwen35/phase5_margins.json
  • qwen35/plan.json

Linked from