prepare data on a CPU job, train from the prepared files
Browse files- v2/train.py +83 -8
v2/train.py
CHANGED
|
@@ -106,14 +106,34 @@ def enron_text(batch):
|
|
| 106 |
return {"text": texts}
|
| 107 |
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
from datasets import load_dataset
|
| 112 |
|
| 113 |
procs = cpus()
|
| 114 |
fineweb = load_dataset("parquet", data_files=[FINEWEB.format(i) for i in range(args.fineweb_files)], split="train")
|
| 115 |
-
fineweb = fineweb.select_columns(["text"])
|
|
|
|
|
|
|
| 116 |
smoltalk = load_dataset("parquet", data_files=[SMOLTALK.format(i) for i in range(args.smoltalk_files)], split="train")
|
|
|
|
|
|
|
|
|
|
| 117 |
smoltalk = smoltalk.map(smoltalk_text, batched=True, num_proc=procs, remove_columns=smoltalk.column_names)
|
| 118 |
enron = load_dataset("parquet", data_files=[ENRON.format(i) for i in range(3)], split="train")
|
| 119 |
enron = enron.map(enron_text, batched=True, num_proc=procs, remove_columns=enron.column_names)
|
|
@@ -449,6 +469,55 @@ def headline(scored: dict) -> dict:
|
|
| 449 |
return out
|
| 450 |
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
# ---- Main ------------------------------------------------------------------------------------------------------
|
| 453 |
|
| 454 |
|
|
@@ -476,6 +545,10 @@ def parse_args():
|
|
| 476 |
parser.add_argument("--folder", default="v2")
|
| 477 |
parser.add_argument("--name", default="tinynext-v2")
|
| 478 |
parser.add_argument("--trackio-space", default="cfahlgren1/tinynext-trackio")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
parser.add_argument("--smoke", action="store_true", help="Tiny settings to check the pipeline end to end")
|
| 480 |
parser.add_argument("--no-push", action="store_true")
|
| 481 |
args = parser.parse_args()
|
|
@@ -518,13 +591,15 @@ def main():
|
|
| 518 |
tracker = start_tracker(args)
|
| 519 |
|
| 520 |
started = time.time()
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
rows, val_rows, counts = build_rows(sources, tokenizer, weights, args)
|
| 526 |
data_minutes = (time.time() - started) / 60
|
| 527 |
print(f"data ready in {data_minutes:.1f} min: {rows.size / 1e6:.0f}M train tokens, {val_rows.size / 1e6:.1f}M val", flush=True)
|
|
|
|
|
|
|
|
|
|
| 528 |
|
| 529 |
# The whole token array lives on the GPU; each step just indexes into it.
|
| 530 |
rows_gpu = torch.from_numpy(rows.astype(np.int32)).to(device)
|
|
|
|
| 106 |
return {"text": texts}
|
| 107 |
|
| 108 |
|
| 109 |
+
# Byte-level BPE with an 8K vocabulary averages a little over 4 characters per token on English. Reading 40% more
|
| 110 |
+
# text than that covers the variance, and anything short gets repeated (and logged) in flatten().
|
| 111 |
+
CHARS_PER_TOKEN = 4.0
|
| 112 |
+
TEXT_MARGIN = 1.4
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def enough_rows(ds, budget_tokens: int, lengths: np.ndarray, seed: int):
|
| 116 |
+
"""A random subset of rows whose text adds up to roughly the token budget, so only that much gets cleaned."""
|
| 117 |
+
order = np.random.default_rng(seed).permutation(len(ds))
|
| 118 |
+
needed = np.searchsorted(np.cumsum(lengths[order]), budget_tokens * CHARS_PER_TOKEN * TEXT_MARGIN) + 1
|
| 119 |
+
return ds.select(np.sort(order[:needed]))
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def load_sources(args, budgets: dict) -> dict:
|
| 123 |
+
"""Downloads parquet files, keeps only as many documents as each source's budget needs, and cleans them on
|
| 124 |
+
every core."""
|
| 125 |
+
import pyarrow.compute as pc
|
| 126 |
from datasets import load_dataset
|
| 127 |
|
| 128 |
procs = cpus()
|
| 129 |
fineweb = load_dataset("parquet", data_files=[FINEWEB.format(i) for i in range(args.fineweb_files)], split="train")
|
| 130 |
+
fineweb = fineweb.select_columns(["text"])
|
| 131 |
+
fineweb = enough_rows(fineweb, budgets["fineweb"], pc.utf8_length(fineweb.data.column("text")).to_numpy(), seed=0)
|
| 132 |
+
fineweb = fineweb.map(lambda b: {"text": [to_ascii(t) for t in b["text"]]}, batched=True, num_proc=procs)
|
| 133 |
smoltalk = load_dataset("parquet", data_files=[SMOLTALK.format(i) for i in range(args.smoltalk_files)], split="train")
|
| 134 |
+
sample = smoltalk.shuffle(seed=0).select(range(min(2000, len(smoltalk))))
|
| 135 |
+
average = np.mean([sum(len(m["content"]) for m in row["messages"]) for row in sample])
|
| 136 |
+
smoltalk = enough_rows(smoltalk, budgets["smoltalk"], np.full(len(smoltalk), average), seed=1)
|
| 137 |
smoltalk = smoltalk.map(smoltalk_text, batched=True, num_proc=procs, remove_columns=smoltalk.column_names)
|
| 138 |
enron = load_dataset("parquet", data_files=[ENRON.format(i) for i in range(3)], split="train")
|
| 139 |
enron = enron.map(enron_text, batched=True, num_proc=procs, remove_columns=enron.column_names)
|
|
|
|
| 469 |
return out
|
| 470 |
|
| 471 |
|
| 472 |
+
# ---- Prepared data ---------------------------------------------------------------------------------------------
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def data_key(args) -> str:
|
| 476 |
+
"""Prepared data depends only on these settings, so a training run can find the matching files."""
|
| 477 |
+
return f"{args.name}-v{args.vocab}-s{args.seq}-t{int(args.tokens / 1e6)}M-fim{args.fim_rate}-{args.mix}".replace(",", "_").replace("=", "")
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def prepare(args, weights: dict, output: Path):
|
| 481 |
+
budgets = {name: int((args.tokens + args.val_tokens) * weight) for name, weight in weights.items()}
|
| 482 |
+
started = time.time()
|
| 483 |
+
sources = load_sources(args, budgets)
|
| 484 |
+
print(f"sources ready in {time.time() - started:.0f}s: " + ", ".join(f"{n} {len(d):,} docs" for n, d in sources.items()), flush=True)
|
| 485 |
+
tokenizer = train_tokenizer(sources, weights, args.vocab, args.tokenizer_docs)
|
| 486 |
+
tokenizer.save(str(output / "tokenizer.json"))
|
| 487 |
+
rows, val_rows, counts = build_rows(sources, tokenizer, weights, args)
|
| 488 |
+
return tokenizer, rows, val_rows, counts
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def upload_prepared(args, output: Path, rows: np.ndarray, val_rows: np.ndarray, counts: dict, minutes: float):
|
| 492 |
+
from huggingface_hub import HfApi
|
| 493 |
+
|
| 494 |
+
folder = output / "data"
|
| 495 |
+
folder.mkdir(exist_ok=True)
|
| 496 |
+
rows.astype(np.uint16).tofile(folder / "train.bin")
|
| 497 |
+
val_rows.astype(np.uint16).tofile(folder / "val.bin")
|
| 498 |
+
shutil.copy(output / "tokenizer.json", folder / "tokenizer.json")
|
| 499 |
+
meta = {"seq": args.seq, "train_rows": len(rows), "val_rows": len(val_rows), "counts": counts, "minutes": round(minutes, 1), "args": vars(args)}
|
| 500 |
+
(folder / "meta.json").write_text(json.dumps(meta, indent=1))
|
| 501 |
+
api = HfApi(token=os.environ.get("HF_TOKEN"))
|
| 502 |
+
api.create_repo(args.data_repo, repo_type="dataset", private=True, exist_ok=True)
|
| 503 |
+
api.upload_folder(folder_path=str(folder), repo_id=args.data_repo, repo_type="dataset", path_in_repo=data_key(args),
|
| 504 |
+
commit_message=f"prepared data: {data_key(args)}")
|
| 505 |
+
print(f"Prepared data at https://huggingface.co/datasets/{args.data_repo}/tree/main/{data_key(args)}", flush=True)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def download_prepared(args, output: Path):
|
| 509 |
+
from huggingface_hub import snapshot_download
|
| 510 |
+
from tokenizers import Tokenizer
|
| 511 |
+
|
| 512 |
+
local = Path(snapshot_download(args.data_repo, repo_type="dataset", allow_patterns=[f"{data_key(args)}/*"], max_workers=16))
|
| 513 |
+
folder = local / data_key(args)
|
| 514 |
+
meta = json.loads((folder / "meta.json").read_text())
|
| 515 |
+
shutil.copy(folder / "tokenizer.json", output / "tokenizer.json")
|
| 516 |
+
rows = np.fromfile(folder / "train.bin", dtype=np.uint16).reshape(meta["train_rows"], meta["seq"] + 1)
|
| 517 |
+
val_rows = np.fromfile(folder / "val.bin", dtype=np.uint16).reshape(meta["val_rows"], meta["seq"] + 1)
|
| 518 |
+
return Tokenizer.from_file(str(output / "tokenizer.json")), rows, val_rows, meta["counts"]
|
| 519 |
+
|
| 520 |
+
|
| 521 |
# ---- Main ------------------------------------------------------------------------------------------------------
|
| 522 |
|
| 523 |
|
|
|
|
| 545 |
parser.add_argument("--folder", default="v2")
|
| 546 |
parser.add_argument("--name", default="tinynext-v2")
|
| 547 |
parser.add_argument("--trackio-space", default="cfahlgren1/tinynext-trackio")
|
| 548 |
+
parser.add_argument("--stage", choices=("all", "prepare", "train"), default="all",
|
| 549 |
+
help="prepare: build the tokenizer and token arrays on a CPU job and save them to --data-repo. "
|
| 550 |
+
"train: download them and train on the GPU. all: both in one job.")
|
| 551 |
+
parser.add_argument("--data-repo", default="cfahlgren1/tinynext-data")
|
| 552 |
parser.add_argument("--smoke", action="store_true", help="Tiny settings to check the pipeline end to end")
|
| 553 |
parser.add_argument("--no-push", action="store_true")
|
| 554 |
args = parser.parse_args()
|
|
|
|
| 591 |
tracker = start_tracker(args)
|
| 592 |
|
| 593 |
started = time.time()
|
| 594 |
+
if args.stage == "train":
|
| 595 |
+
tokenizer, rows, val_rows, counts = download_prepared(args, output)
|
| 596 |
+
else:
|
| 597 |
+
tokenizer, rows, val_rows, counts = prepare(args, weights, output)
|
|
|
|
| 598 |
data_minutes = (time.time() - started) / 60
|
| 599 |
print(f"data ready in {data_minutes:.1f} min: {rows.size / 1e6:.0f}M train tokens, {val_rows.size / 1e6:.1f}M val", flush=True)
|
| 600 |
+
if args.stage == "prepare":
|
| 601 |
+
upload_prepared(args, output, rows, val_rows, counts, data_minutes)
|
| 602 |
+
return
|
| 603 |
|
| 604 |
# The whole token array lives on the GPU; each step just indexes into it.
|
| 605 |
rows_gpu = torch.from_numpy(rows.astype(np.int32)).to(device)
|