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.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""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)")
|
||||
Reference in New Issue
Block a user