Files
esh-pfi-infrastructure/scripts/r49-corpus/build_booth.py
T
vh 935b35ac2e Unwrap the Brontë corpus and launch the 1.7B rung
Operator: "start the 1.7b training."

The 0.6B adapter learned the Gutenberg transcription's ~70-character line breaks
along with the prose -- its output wrapped at a mid-length-line ratio of 0.85
against the base model's 0.00. That is typography rather than style, and every
further rung would have inherited it, so the corpus is reflowed before rung 2
rather than after the sweep.

The reflow joins 57,430 of 85,380 paragraph blocks and keeps 27,950. Verse is the
hazard a blind join would destroy, so the decision is per block by median line
length: blocks whose lines cluster near the wrap width are flowed prose, blocks of
consistently short lines keep their breaks. Every kept multi-line block in the
sample was genuinely verse with its lineation intact. No line ended in a lone
hyphen, so the space-join could not split a word across lines. The acceptance
check is content identity -- " ".join(text.split()) byte-identical before and
after -- and it passed on all 852 records, proving only whitespace changed.

Concrete cost of the old defect: 5.7% of the training budget was newline tokens.
The same words pack to 5,210,112 tokens unwrapped against 5,525,504 wrapped.

The 1.7B run is live at 159 steps and roughly 18.7 s/it. Everything but the
carrier and the corpus is held from the 0.6B run: seed 4919, rank 32, lr 1e-4, seq
4096, batch 1 by accum 8, one epoch, eval and save every 25 steps so the minimum
is located rather than assumed.

That corpus change is a second variable and it is named as one. A 0.6B-vs-1.7B
comparison is descriptive, not attributable, until the chained 0.6B rerun on the
same unwrapped corpus lands behind it -- gated on the 1.7B actually producing an
adapter, because a chain that fires on failure turns one lost run into two.
"Did sense come back at 1.7B" is a within-arm reading and survives the confound;
any between-rung delta does not.

The original wrapped corpus is untouched, so the 0.6B run's pinned corpus sha
3959036cf851bf62 stays reproducible.
2026-09-10 15:37:51 -07:00

121 lines
6.4 KiB
Python

"""Render the base-vs-tuned voice A/B into a booth page.
Layout is the argument. A flat gallery would let you read one arm at a time, which
is exactly how you talk yourself into seeing a difference. So every prompt is one
row, the arms are side by side, and BOTH seeds of each arm sit in the same cell --
so the within-arm variation is visible in the same glance as the between-arm
variation. If the two base samples differ from each other as much as base differs
from tuned, there is nothing here, and the layout should make that obvious rather
than hide it.
Prompts are ordered by tier, hardest first: modern/mundane, then period-neutral,
then Victorian-adjacent. The modern tier is the one that matters -- Brontë showing
up there is the adapter's doing, whereas Brontë showing up in the period tier could
just be the prompt.
"""
import html
import json
import sys
from collections import defaultdict
from pathlib import Path
base_f, tuned_f, out_dir = sys.argv[1], sys.argv[2], Path(sys.argv[3])
out_dir.mkdir(parents=True, exist_ok=True)
def load(p):
d = defaultdict(dict)
for line in Path(p).read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
r = json.loads(line)
d[r["id"]][r["seed"]] = r
return d
base, tuned = load(base_f), load(tuned_f)
ids = [i for i in base if i in tuned]
TIER = {"modern": ("Tier A — modern / mundane",
"Nothing here invites Victorian prose. Brontë in this tier is the adapter's doing."),
"neutral": ("Tier B — period-neutral",
"Could be any century. A voice shift shows cleanly without the prompt supplying it."),
"period": ("Tier C — Victorian-adjacent, plainly worded",
"The setting leans period but the diction does not. Easiest tier; weakest evidence.")}
order = {"modern": 0, "neutral": 1, "period": 2}
ids.sort(key=lambda i: (order.get(base[i][list(base[i])[0]]["tier"], 9), i))
def cell(rec_by_seed):
parts = []
for seed in sorted(rec_by_seed):
t = (rec_by_seed[seed]["continuation"] or "").strip()
parts.append(f'<div class="s"><span class="seed">seed {seed}</span>'
f'<p>{html.escape(t) or "<em>(empty)</em>"}</p></div>')
return "".join(parts)
rows, seen = [], set()
for i in ids:
any_rec = base[i][list(base[i])[0]]
tier = any_rec["tier"]
if tier not in seen:
seen.add(tier)
title, sub = TIER.get(tier, (tier, ""))
rows.append(f'<h2>{html.escape(title)}</h2><p class="tsub">{html.escape(sub)}</p>')
rows.append(f"""
<section class="row">
<div class="prompt"><span class="pid">{html.escape(i)}</span>{html.escape(any_rec["prompt"])}</div>
<div class="arms">
<div class="arm"><h3>Base <small>Qwen3-0.6B-Base, no adapter</small></h3>{cell(base[i])}</div>
<div class="arm tuned"><h3>Tuned <small>+ H02 LoRA, 1 epoch, seed 4919</small></h3>{cell(tuned[i])}</div>
</div>
</section>""")
page = f"""<!doctype html><meta charset="utf-8"><title>BabyBronte — voice A/B</title>
<style>
:root{{--bg:#faf8f5;--fg:#1c1a17;--mut:#6b6560;--line:#e0dad2;--acc:#7a3b2e;--tint:#fdfbf7}}
*{{box-sizing:border-box}}
body{{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 Georgia,"Iowan Old Style",serif;padding:2.5rem 1.5rem 5rem}}
.wrap{{max-width:1180px;margin:0 auto}}
h1{{font-size:1.9rem;margin:0 0 .3rem}}
.lede{{color:var(--mut);max-width:70ch;margin:0 0 .9rem}}
.warn{{border-left:3px solid var(--acc);background:#fff;padding:.8rem 1rem;margin:1.2rem 0 2rem;max-width:80ch;font-size:.93rem}}
h2{{font-size:1.15rem;margin:2.8rem 0 .2rem;padding-top:1rem;border-top:1px solid var(--line)}}
.tsub{{color:var(--mut);font-size:.9rem;margin:0 0 1.2rem;font-style:italic}}
.row{{margin:0 0 2.2rem}}
.prompt{{background:#fff;border:1px solid var(--line);border-left:3px solid var(--acc);padding:.7rem .9rem;font-size:1.02rem;margin-bottom:.7rem}}
.pid{{display:inline-block;font:600 .72rem/1 ui-monospace,monospace;color:var(--mut);background:var(--bg);border:1px solid var(--line);padding:.22rem .4rem;margin-right:.6rem;vertical-align:1px}}
.arms{{display:grid;grid-template-columns:1fr 1fr;gap:1rem}}
@media(max-width:820px){{.arms{{grid-template-columns:1fr}}}}
.arm{{background:#fff;border:1px solid var(--line);padding:.9rem 1rem}}
.arm.tuned{{background:var(--tint);border-color:#d8ccbe}}
.arm h3{{margin:0 0 .6rem;font-size:.9rem;letter-spacing:.04em;text-transform:uppercase;color:var(--acc)}}
.arm h3 small{{display:block;text-transform:none;letter-spacing:0;color:var(--mut);font-weight:400;font-size:.82rem;margin-top:.15rem}}
.s{{border-top:1px dotted var(--line);padding-top:.6rem;margin-top:.6rem}}
.arm .s:first-of-type{{border-top:0;padding-top:0;margin-top:0}}
.seed{{display:block;font:600 .7rem/1 ui-monospace,monospace;color:var(--mut);margin-bottom:.25rem}}
.s p{{margin:0;white-space:pre-wrap;font-size:.95rem}}
footer{{margin-top:3rem;padding-top:1rem;border-top:1px solid var(--line);color:var(--mut);font-size:.85rem;max-width:80ch}}
</style>
<div class="wrap">
<h1>BabyBronte — did the voice move?</h1>
<p class="lede">Same prompts, same sampler, same box, same seeds. The only difference between the
columns is the H02 LoRA adapter (Charlotte Brontë, 680k words, 1 epoch, seed 4919).</p>
<div class="warn"><strong>Read this as an eyeball test, not a result.</strong> Two samples per arm
per prompt is enough to see whether the gap between the columns is bigger than the gap between the
two seeds <em>inside</em> a column — and not enough for anything else. No scoring, no statistics.
The frozen adjudication rule and the Burrows's-Delta instrument are untouched by this page and
nothing here feeds them.<br><br>
Both arms are <strong>Qwen3-0.6B-Base doing continuation</strong>, not instruction-following. The
adapter was trained as pure continuation, so each prompt is an opening line the model carries on
from — asking a base model to "rewrite this in Brontë's voice" would test instruction-following
instead of voice.</div>
{''.join(rows)}
<footer>Generated on pfi-gx10 (GB10), bf16, sdpa. Sampler pinned identical across arms:
temperature 0.9, top_p 0.95, 400 new tokens, seeds 1234 and 5678. Adapter binding proven at
generation time (196/196 lora_B tensors non-zero) — a silent no-op looks exactly like a tune that
changed nothing.</footer>
</div>"""
(out_dir / "index.html").write_text(page, encoding="utf-8")
print(f"wrote {out_dir/'index.html'} ({len(ids)} prompts x 2 arms x 2 seeds)")