chore(r49): stage lv-mccarthy ckpt300 on fv-ml1, and guard the ship script against a local ~

The adapter is STAGED, NOT SHIPPED: adapter_config.json and
adapter_model.safetensors are in /tank/aimodels/voice-adapters/lv-mccarthy-4b-v1
on fv-ml1, verified byte-identical to gx10's checkpoint-300 at the source, after
the local hop and at the destination. The seat CANNOT load it -- nothing in
--lora-modules references it -- and /v1/models was checked after staging and still
serves exactly the three registered voices plus base.

The adapter carries a README recording the full gate result AND its cost, so it
cannot be read as clean by anyone who finds the directory without the gate record.
A copy is committed at scripts/mccarthy-corpus/gate-results/ADAPTER-README.md for
review. It leads with "STAGED, NOT SHIPPED" and says that if lv-mccarthy is absent
from the seat's model list, the decision has not been made.

The compose edit that would actually register it is prepared in the working tree
and deliberately LEFT UNCOMMITTED. stacks/ is canonical intent that deploy-stack.sh
reads; committing it would assert an intent that is precisely the open question.

Ship-script fix: <run-dir> must be an absolute REMOTE path. A leading ~ is expanded
by the local shell, so ~/r49-runs/... became /home/lkraven/... and gx10 answered
"Permission denied" -- a confusing error for a path problem. It now refuses a
non-absolute path outright, with the reason. Verified: the guard fires on a ~ path
and the real staging succeeded on the absolute one.
This commit is contained in:
Vuong Hoang
2026-09-21 17:57:12 -07:00
parent e8eb1594d9
commit 968ec71cce
2 changed files with 161 additions and 1 deletions
@@ -0,0 +1,149 @@
# lv-mccarthy-4b-v1
LoRA adapter over Qwen3-4B-Instruct for the prose voice of **Cormac McCarthy**.
Checkpoint: `mccarthy-4b-pairs-3ep/checkpoints/checkpoint-300` (epoch 0.652, eval_loss 2.4490).
Rank 32, alpha 64, seq 1536, lr 1e-4, seed 4919. Trained on 3,673 instruction pairs over a
584,684-word six-novel corpus, renamed. Staged 2026-09-21.
⚠⚠ **STAGED, NOT SHIPPED.** At the time of writing this directory is **not** referenced by
`stacks/voices-seat/compose.yaml --lora-modules`, so the seat cannot load it. It passes the
gate design of record on all three axes but carries a real length-discipline cost (below)
that is an operator trade, not a gate reading. **If `lv-mccarthy` is not in the seat's model
list, that decision has not been made yet.**
Gate design pre-registered before any generation existed, with three dated amendments:
eshpfi `scripts/mccarthy-corpus/GATE-PREREG.md`. Raw artifacts for all five arms:
eshpfi `scripts/mccarthy-corpus/gate-results/`.
## The gate result — 5 arms, 60 held-out beats × 4 seeds = 240 generations per arm
| axis | result | numbers |
|---|---|---|
| **A. VOICE** | ✅ **PASS, 3.2× the floor — best arm of five** | +0.177 delta_cb toward held-out McCarthy vs the base control, pairwise floor 0.055. Best point estimate AND best margin in the run |
| **B. NOT COPIED** | ✅ **PASS — indistinguishable from the author** | 0.12 hit-rate against **held-out McCarthy's own 0.12**; longest match **10 words against the author's coincidental 12** |
| **C. NO DAMAGE** | ⚠ **PASSES the rule, FAILS on the axis the rule cannot see — read below** | ran-on +0.12 vs a 0.200 floor (40% headroom). But **in-band 0.65 against base's 0.89** |
```
same-author target (held-out McCarthy vs itself) delta_cb 0.370 <- best achievable
ckpt300 (this adapter) 0.484
ckpt225 0.485
ckpt900 (the eval-loss minimum) 0.490
ckpt450 0.509
base-unadapted 0.661
```
Reachable span 0.661 → 0.370 = 0.291; this adapter closed **0.177 = 60.8%**. For scale,
lv-bronte closed 48% and lv-hemingway 73.8% — and McCarthy's voice axis is **deliberately
harder** than either: the `mccarthy` register names its own punctuation tics, and the eval
drives the base control with the same prompt, so the adapter earns no delta for "emit no
quotation marks". Verified: base's quote density is 19.9 per 10k against the reference's 0.0.
⭐ **The gain is not the punctuation trick.** A pre-registered secondary read strips *every*
punctuation mark from the reference and every arm: this adapter still scores **+0.128 at 2.8×
its floor**, so roughly three quarters of the voice gain is carried by words rather than marks.
## ✅ AXIS B: the cleanest safety result in the lv- line
McCarthy is **in copyright with a living estate**, so this axis carries more weight here than
for lv-bronte (public domain) or lv-hemingway.
```
sample n hit-rate mean-longest max
HELD-OUT McCARTHY (never trained) 364 0.12 1.0 12 <- the innocent rate
base-unadapted 240 0.00 0.0 8
ckpt300 (this adapter) 240 0.12 1.0 10
ckpt450 240 0.12 1.1 11
ckpt900 240 0.22 1.9 11
positive control (train vs train) 160 <- detector not blind
```
⚠ **The base-unadapted control is DEFECTIVE and its zero is unearned** — base writes summary,
the adapted arms write pastiche, and text that does not imitate a register cannot collide with
its n-grams. The honest reference is the author himself. Against base, this adapter's 0.12 vs
0.00 reads as a 12× red flag; against the author's own 0.12 it is **1.0×**, and its longest
verbatim run is **shorter** than the one genuinely unseen McCarthy shares with the train split
by coincidence.
⭐ Measured, not assumed: the "his plain register makes 8-gram collisions inevitable" story
that was **FALSE for Hemingway** (0.01) is **TRUE for McCarthy** (0.12). His idiom really does
self-collide. Neither number transfers between authors.
**All 31 matched runs were READ, not counted.** Every one is stock grammar in the commonest
words:
```
looked at the wolf and he looked at the boy
took off his hat and set it on the
and wiped his mouth on the back of his
He was naked to the waist and he
```
The name-shaped hits (`Adam Caleb`, `Colton`) are the **RENAMED invented names**, not
McCarthy's. No plot, no imagery, nothing protectable.
## ⚠⚠ AXIS C: THE COST. READ THIS BEFORE PUTTING IT ON A LENGTH-SENSITIVE PATH
The gate's axis C is `ran-on` only (operator-ratified 2026-09-15, because the unadapted
carrier maxes the other terms). This adapter passes it with 40% headroom. **That rule is
structurally blind to the cost that matters here**, and the cost is real:
```
arm n median p90 max in-band under-90 over-140 TOTAL out-of-band
base 240 110 126 149 0.89 10% 1% 11%
ckpt300 240 101 151 216 0.65 22% 13% 35%
ckpt450 240 107 171 297 0.67 14% 20% 33%
ckpt900 240 114 190 279 0.67 5% 28% 33%
ckpt225 240 127 206 296 0.52 10% 38% 48%
```
**35% of this adapter's generations miss the requested 90–140 band, against base's 11%**, and
on-beat keyword coverage drops 0.42 vs base's 0.71. Base is *good* at length here — McCarthy's
register prompt is prescriptive and Qwen3-4B-Instruct follows it — so the adapter measurably
makes instruction-following worse.
⚠ **No checkpoint choice fixes this.** Every adapted arm sits at 33–48% out-of-band. What
changes with epoch is the *direction* of the miss and its size, non-monotonically: ran-on runs
0.38 → **0.13** → 0.20 → 0.28 across epochs 0.49 / 0.65 / 0.98 / 1.96. This checkpoint is the
measured minimum. The fix, if the cost is intolerable, is a retrain targeting length — pair
construction or the length target — not a different checkpoint.
**If Skaldsong or any caller depends on a word budget, expect one generation in three to miss
it.** Unload is 0.003 s and one compose line.
## Why checkpoint-300 and NOT ckpt900, the eval-loss minimum
⭐ **The two-epoch recipe is now 0 for 3.** ckpt900 is the curve's minimum (2.38706) and
ckpt300 is +0.062 above it — **15.8× the 0.00393 median neighbour jitter**, i.e. the loss curve
is *confident* ckpt900 is better. It is wrong on every axis that resolves:
| | ckpt300 (this adapter) | ckpt900 (loss min) |
|---|---|---|
| voice margin | **3.2× floor** | 1.2× floor |
| seed spread | **0.055** | 0.148 — **2.7× wider**, one outlier seed |
| memorisation vs the author's 0.12 | **0.12 (1.0×)** | 0.22 (1.8×) |
| ran-on | **0.13** | 0.28 |
| epochs of overfit | **0.65** | 1.96 |
ckpt900's spread is one seed — 0.605 against 0.457 / 0.531 / 0.554 — the third occurrence of
that shape in the later checkpoint, after lv-bronte's ckpt925 and lv-hemingway's ckpt1750.
**On this schedule the eval-loss minimum is not the ship candidate, and the curve's confidence
about it carries no information.**
## Corpus provenance
Six novels (Blood Meridian, All the Pretty Horses, The Crossing, Cities of the Plain, No
Country for Old Men, The Road), 167 units, 584,684 words, 6 renamed copies, val split held at
chapters 7 and 17 = 11 units / 40,653 words per copy.
Leak gate at gate time: **0 of 75 renameable, 0 of 37 sub-threshold, 0 separator-split**, four
controls green. Beat-contamination audit: **0 of 3,942 beats and 0 of 3,942 responses** —
against lv-hemingway's 70 of 7,094 — because `build_sft_pairs.py --source-entities` was
mandatory for this corpus's pair build.
⚠ Sensitivity floor, part of the result and not a footnote: a name under 8 capitals per work is
never detected, a phrase under 5 recurrences never audited, and **no phrase map exists for this
corpus, so the phrase audit never ran at all.** lv-yarros and lv-bronte both had one. A clean
leak gate here is a narrower claim than a clean leak gate there.
⚠ **`repair_typography.py` MUST NOT be run on this corpus** — it would normalise quotation
marks back into a corpus that measures 0.0 per 10k against Hemingway's 838.
+12 -1
View File
@@ -2,7 +2,12 @@
# Ship an r49 voice LoRA from the training box to the voices seat's adapter store.
#
# scripts/r49-corpus/ship-voice-adapter.sh <run-dir> <checkpoint> <adapter-name>
# e.g. ... ~/r49-runs/mccarthy-4b-pairs-3ep checkpoint-900 lv-mccarthy-4b-v1
# e.g. ... /home/infra-ops/r49-runs/mccarthy-4b-pairs-3ep checkpoint-300 lv-mccarthy-4b-v1
#
# ⚠ <run-dir> MUST BE AN ABSOLUTE REMOTE PATH. A leading `~` is expanded by the LOCAL
# shell before the script sees it, so `~/r49-runs/...` becomes /home/lkraven/... and the
# source read fails with "Permission denied" on gx10 -- a confusing error for a path
# problem. The guard below refuses it outright rather than letting ssh report it.
#
# Run from nh3-dev. gx10 and fv-ml1 both hold only authorized_keys, so neither can
# INITIATE to the other -- the relay through this box is the path, and it costs nothing
@@ -26,6 +31,12 @@ NAME=${3:?missing adapter name}
SRC_HOST=${SRC_HOST:-infra-ops@10.100.50.60} # pfi-gx10
DST_HOST=${DST_HOST:-infra-ops@10.251.50.54} # fv-ml1
DST_ROOT=${DST_ROOT:-/tank/aimodels/voice-adapters}
case "$RUN" in
/*) ;;
*) echo "== REFUSING: <run-dir> must be an ABSOLUTE remote path, got '$RUN'."
echo " A leading ~ is expanded by the local shell and will point at the wrong user."
exit 2 ;;
esac
STAGE=$(mktemp -d)
trap 'rm -rf "$STAGE"' EXIT