klon8-base / src /scripts /build_pool.py
voraseth's picture
Upload folder using huggingface_hub
7d3eb8f verified
Raw History Blame Contribute Delete
7.1 kB
"""Build a judged pool to the pinned recipe in docs/judge_frozen.md section 2.1.
Pool composition is part of the instrument: the same 20 human poems scored 85% perfect in one pool
and 100% in another, and an unanchored pool runs ~0.45 bad-วรรค against 0.26 anchored. So the recipe
is enforced here rather than remembered: the pinned anchor set, five system poems per anchor, and
every batch carrying the pool's own composition (10 system + 2 anchors).
Usage:
build_pool.py OUT_DIR LABEL=FILE [LABEL=FILE ...] [--bot]
--bot builds a 4-วรรค (one บท) pool: the 1-บท anchors and the 4-line judge prompt.
--no-drop never drops a poem: anchors = ceil(n/5) (<= 20), ratio at most 5:1 (final confirmation only).
Writes batch_00.txt .. (frozen prompt + items) and keymap.json ({key: {src, id}}).
"""
import sys, os, json, random, hashlib
# Two lengths, two instruments. The 8-วรรค pool is the frozen v3.2 setup. The 1-บท pool is the
# 4-วรรค target: the same wording with "8 numbered lines" -> "4 numbered lines" and <1-8> -> <1-4>,
# and the same twenty anchors reduced to their first บท.
FORMS = {
8: dict(anchors="data/pools/anchor20.jsonl",
anchor_sha="0dd2a105b6c9b41f8999b7511f47e82dc897d094a9e7d7a03c48e2080577cd26",
prompt="docs/judge_v3.2_prompt.txt",
prompt_sha="9f051d5d6c2f05e7cce70770691e35fb5af8e6ed0335221a8f3425bfa5705eec"),
# The 4-วรรค anchor is twenty poems their authors WROTE as one บท (posts that were four lines and
# nothing else), not the first บท sliced out of an 8-วรรค poem. A slice is a fragment: its วรรคส่ง
# is built to hand off to the next บท and its topic may only be developed later, so judging it
# would penalise the human for our cut.
4: dict(anchors="data/pools/anchor20_bot_native.jsonl",
anchor_sha="34335cea2fc210e9b480d48336258d39fd1fbe73a1d8c58afae1d13cecac1e16",
prompt="docs/judge_v4bot_prompt.txt",
prompt_sha="60db6eedf758c081c272ff3fc7d0c01a69568d1479c6c9c5558952a66508c6e6"),
}
ALIGN_PROMPT = "docs/judge_align_bot_prompt.txt"
ALIGN_SHA = "f7c7f004e417f18576bed5e7286b03727ff9195f088cf3ad22c2c6501d868d98"
PER_ANCHOR, BATCH_SYS, BATCH_ANC = 5, 10, 2
def sha(path):
return hashlib.sha256(open(path, "rb").read()).hexdigest()
def rows(path, nlines=8):
out, seen = [], set()
for l in open(path, encoding="utf-8"):
r = json.loads(l)
if not l.strip() or r["id"] in seen: continue
seen.add(r["id"])
poem = r["poem"] if isinstance(r["poem"], str) else "\n".join(r["poem"])
if len(poem.split("\n")) != nlines: continue
out.append({"id": r["id"], "topic": r.get("prompt") or r.get("topic") or r.get("text"), "poem": poem})
return out
def build(out_dir, systems, nlines=8, align=False, no_drop=False):
F = dict(FORMS[nlines])
if align: # same pool recipe, the alignment instrument instead
F["prompt"], F["prompt_sha"] = ALIGN_PROMPT, ALIGN_SHA
assert sha(F["anchors"]) == F["anchor_sha"], f'anchor set has changed: {sha(F["anchors"])}'
assert sha(F["prompt"]) == F["prompt_sha"], f'judge prompt has changed: {sha(F["prompt"])}'
anchors = rows(F["anchors"], nlines)
sysrows = [(lab, r) for lab, f in systems for r in rows(f, nlines)]
assert sysrows, f"no {nlines}-line poems found in {[f for _, f in systems]}"
n_anchor = min(len(anchors), max(1, len(sysrows) // PER_ANCHOR))
if no_drop:
# final confirmation (runs/CSEL/CONFIRM_PREREG.md): never drop a poem - round the anchor count UP, so the
# ratio is at most 5 system poems per anchor; the pool must fit the 20 pinned anchors
n_anchor = -(-len(sysrows) // PER_ANCHOR)
assert n_anchor <= len(anchors), f"{len(sysrows)} poems exceed {len(anchors)} anchors x {PER_ANCHOR}"
anchors = sorted(anchors, key=lambda r: r["id"])[:n_anchor] # subsample by id, never at random
assert len(sysrows) >= PER_ANCHOR, "too few system poems for even one anchor"
# round-robin by label FIRST, then cap: capping the concatenation would take the whole quota
# from the first system and silently drop the second arm of an A/B pool.
by_lab = {}
for lab, r in sysrows: by_lab.setdefault(lab, []).append(r)
order, labs = [], sorted(by_lab)
while any(by_lab[l] for l in labs):
for l in labs:
if by_lab[l]: order.append((l, by_lab[l].pop(0)))
if no_drop: assert len(order) <= PER_ANCHOR * len(anchors)
order = order[:PER_ANCHOR * len(anchors)] # hold the 5:1 ratio exactly
if len(labs) > 1:
c = {l: sum(1 for x, _ in order if x == l) for l in labs}
assert max(c.values()) - min(c.values()) <= 1, f"A/B pool is unbalanced: {c}"
nb = max(1, len(order) // BATCH_SYS)
batches = [[] for _ in range(nb)]
# deal each arm separately with its own offset. Dealing the interleaved list with i % nb aliases
# when nb is even: base lands on even batches, B on odd, and every batch is single-arm again.
for li, lab in enumerate(labs):
mine = [(lab, r) for l, r in order if l == lab]
for j, item in enumerate(mine): batches[(j + li) % nb].append(item)
for i, r in enumerate(anchors): batches[i % nb].append(("human", r))
for b, items in enumerate(batches):
present = set(l for l, _ in items)
assert present >= set(labs) | {"human"}, f"batch {b} is missing {set(labs) | {'human'} - present}"
os.makedirs(out_dir, exist_ok=True)
prompt = open(F["prompt"], encoding="utf-8").read().rstrip()
keymap, kr = {}, random.Random(0)
for b, items in enumerate(batches):
assert any(l == "human" for l, _ in items), f"batch {b} carries no anchor"
random.Random(f"{out_dir}:{b}").shuffle(items)
lines = [prompt, ""]
for lab, r in items:
key = "%06x" % kr.randrange(16 ** 6)
keymap[key] = {"src": lab, "id": r["id"]}
lines.append(f'item {key} topic: {r["topic"]}')
lines += [f"{i}. {ln}" for i, ln in enumerate(r["poem"].split("\n"), 1)]
lines.append("")
open(os.path.join(out_dir, f"batch_{b:02d}.txt"), "w", encoding="utf-8").write("\n".join(lines))
json.dump(keymap, open(os.path.join(out_dir, "keymap.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=0)
comp = {l: sum(1 for k, v in keymap.items() if v["src"] == l) for l in set(v["src"] for v in keymap.values())}
print(f"{len(keymap)} poems in {nb} batches -> {out_dir}")
print(f" composition {comp} (anchors {100*comp.get('human',0)/len(keymap):.0f}% of the pool)")
print(f" per batch: " + ", ".join(f"b{b}={len(x)}" for b, x in enumerate(batches)))
return keymap
if __name__ == "__main__":
args = [a for a in sys.argv[1:] if not a.startswith("--")]
n = 4 if "--bot" in sys.argv else 8
build(args[0], [a.split("=", 1) for a in args[1:]], nlines=n, align="--align" in sys.argv, no_drop="--no-drop" in sys.argv)