codelion's picture
Curate a corpus: clean, calibrate, create, compose on a sample, plus a recipe and runner for your own machines
4f740eb verified
Raw History Blame Contribute Delete
4.48 kB
"""Compose: turn one prepared corpus into one or more training mixes that fit a token budget.
Defaults come from our own 1B-token study (huggingface.co/blog/codelion/optimal-dataset-mixing): a static mix of
50% textbook-style text, 30% filtered web and 20% educational web beat every curriculum we tried at 70M parameters
and 1B tokens. That is one model size and one budget, so treat it as a starting point and fork it.
When a source is too small for its share, it is repeated up to `max_epochs` (about four epochs of repetition cost
little in Muennighoff et al., 2023). Past that, the gap is filled by rephrasing that source's best documents,
up to `max_synthetic_share` of the mix, since a mix that leans on synthetic text generalized worst in our study.
"""
from __future__ import annotations
GROUP_OF_KIND = {"textbook": "textbook", "synthetic": "textbook", "web": "web", "multilingual": "web",
"educational": "educational", "math": "math", "code": "code", "other": "web"}
BASE_SHARES = {"textbook": 0.50, "web": 0.30, "educational": 0.20}
def default_weights(sources: list[dict]) -> dict[str, float]:
"""Share per source name. Math and code, when present, take 10% each off the top; the rest follows 50/30/20
over the groups that exist, split evenly inside each group."""
groups: dict[str, list[str]] = {}
for s in sources:
groups.setdefault(GROUP_OF_KIND.get(s.get("kind", "web"), "web"), []).append(s["name"])
shares: dict[str, float] = {}
fixed = {g: 0.10 for g in ("math", "code") if g in groups}
rest = 1.0 - sum(fixed.values())
base = {g: v for g, v in BASE_SHARES.items() if g in groups}
if not base:
# only math and/or code sources: split evenly
return {s["name"]: round(1 / len(sources), 4) for s in sources}
tot = sum(base.values())
for g, v in base.items():
shares[g] = rest * v / tot
shares.update(fixed)
out = {}
for g, names in groups.items():
for n in names:
out[n] = round(shares[g] / len(names), 4)
return out
def plan_mix(budget_tokens: int, sources: list[dict], weights: dict[str, float], max_epochs: float = 4.0,
max_synthetic_share: float = 0.25, rephrase_yield: float = 4.0, avg_out_tokens: int = 700) -> dict:
"""sources: [{name, kind, available_tokens (after cleaning and dedup), high_tokens (top tier)}].
Returns per-source rows and totals, including how much synthetic text to generate and roughly how many calls."""
tot_w = sum(weights.get(s["name"], 0) for s in sources) or 1.0
rows, synth_total, short_total = [], 0, 0
for s in sources:
share = weights.get(s["name"], 0) / tot_w
want = int(budget_tokens * share)
avail = max(0, int(s.get("available_tokens", 0)))
fresh = min(want, avail)
repeat_cap = int(avail * max_epochs)
from_repeat = min(want, repeat_cap)
gap = want - from_repeat
synth = min(gap, int(s.get("high_tokens", avail) * rephrase_yield)) if gap > 0 else 0
short = gap - synth
epochs = round(from_repeat / avail, 2) if avail else 0.0
rows.append({"name": s["name"], "kind": s.get("kind", "web"), "share": round(share, 4), "want_tokens": want,
"available_tokens": avail, "fresh_tokens": fresh, "epochs": epochs, "synthetic_tokens": synth,
"short_tokens": short})
synth_total += synth
short_total += short
cap = int(budget_tokens * max_synthetic_share)
warnings = []
if synth_total > cap:
warnings.append(f"The plan needs {fmt(synth_total)} synthetic tokens, over the {int(max_synthetic_share * 100)}% cap "
f"({fmt(cap)}). Add sources, lower the budget, or raise the cap knowingly.")
if short_total:
warnings.append(f"{fmt(short_total)} tokens cannot be filled even with repetition and rephrasing.")
for r in rows:
if r["epochs"] > 1.0:
warnings.append(f"{r['name']} repeats {r['epochs']}x to reach its share.")
return {"budget_tokens": budget_tokens, "rows": rows, "synthetic_tokens": synth_total,
"synthetic_calls": synth_total // max(1, avg_out_tokens), "short_tokens": short_total, "warnings": warnings}
def fmt(n: float) -> str:
n = float(n)
for unit, div in (("T", 1e12), ("B", 1e9), ("M", 1e6), ("K", 1e3)):
if abs(n) >= div:
return f"{n / div:.3g}{unit}"
return f"{int(n)}"