cfahlgren1 HF Staff commited on
Commit
63b64fd
·
verified ·
1 Parent(s): 9bba47b

prepare data on a CPU job, train from the prepared files

Browse files
Files changed (1) hide show
  1. v2/train.py +83 -8
v2/train.py CHANGED
@@ -106,14 +106,34 @@ def enron_text(batch):
106
  return {"text": texts}
107
 
108
 
109
- def load_sources(args) -> dict:
110
- """Downloads whole parquet files and cleans them on every core."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"]).map(lambda b: {"text": [to_ascii(t) for t in b["text"]]}, batched=True, num_proc=procs)
 
 
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
- sources = load_sources(args)
522
- print(f"sources ready in {time.time() - started:.0f}s: " + ", ".join(f"{n} {len(d):,} docs" for n, d in sources.items()), flush=True)
523
- tokenizer = train_tokenizer(sources, weights, args.vocab, args.tokenizer_docs)
524
- tokenizer.save(str(output / "tokenizer.json"))
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)