Upload src/xscript/data/pack.py with huggingface_hub
Browse files- src/xscript/data/pack.py +97 -0
src/xscript/data/pack.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tokenize text pools into uint16 token shards (one .bin per pool shard).
|
| 2 |
+
|
| 3 |
+
Documents are shuffled within each pool shard (seeded) and packed as
|
| 4 |
+
<bos> doc <eos> <bos> doc <eos> ... with no padding; the training loader
|
| 5 |
+
reads contiguous (seq_len+1)-token windows. Vocab is 65536 so uint16 is
|
| 6 |
+
exact.
|
| 7 |
+
"""
|
| 8 |
+
import json
|
| 9 |
+
import multiprocessing as mp
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
from ..paths import pool_dir, shard_dir, tokenizer_dir, ensure
|
| 15 |
+
|
| 16 |
+
ENCODE_BATCH = 512
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _iter_pool_docs(path: Path):
|
| 20 |
+
import zstandard
|
| 21 |
+
with open(path, "rb") as raw:
|
| 22 |
+
reader = zstandard.ZstdDecompressor().stream_reader(raw)
|
| 23 |
+
import io
|
| 24 |
+
for line in io.TextIOWrapper(reader, encoding="utf-8"):
|
| 25 |
+
if line.strip():
|
| 26 |
+
yield json.loads(line)["text"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _pack_one(args) -> tuple[str, int]:
|
| 30 |
+
pool_shard, out_path, tok_dir, seed = args
|
| 31 |
+
from ..tok.wrapper import Tok
|
| 32 |
+
import random
|
| 33 |
+
tok = Tok(tok_dir)
|
| 34 |
+
docs = list(_iter_pool_docs(Path(pool_shard)))
|
| 35 |
+
random.Random(seed).shuffle(docs)
|
| 36 |
+
n = 0
|
| 37 |
+
with open(out_path, "wb") as out:
|
| 38 |
+
for i in range(0, len(docs), ENCODE_BATCH):
|
| 39 |
+
batch = tok.encode_batch(docs[i:i + ENCODE_BATCH], bos=True, eos=True)
|
| 40 |
+
flat = np.concatenate([np.asarray(ids, dtype=np.uint16) for ids in batch])
|
| 41 |
+
flat.tofile(out)
|
| 42 |
+
n += len(flat)
|
| 43 |
+
return str(out_path), n
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def pack(lang: str, tok_name: str, workers: int = 8, seed: int = 1234,
|
| 47 |
+
max_tokens: float | None = None) -> dict:
|
| 48 |
+
src = pool_dir(lang)
|
| 49 |
+
all_shards = sorted(src.glob("pool_*.jsonl.zst"))
|
| 50 |
+
if not all_shards:
|
| 51 |
+
raise FileNotFoundError(f"no pool shards in {src} - run `xscript pool --lang {lang}`")
|
| 52 |
+
# While `xscript pool` is still running, its writer may have the highest-
|
| 53 |
+
# indexed shard open (only closed on the next roll/checkpoint) -- hold it
|
| 54 |
+
# back so we never read a truncated file mid-download. Safe to include
|
| 55 |
+
# once the pool has reached its byte budget; picked up on the next
|
| 56 |
+
# incremental `pack` call otherwise.
|
| 57 |
+
pool_done = False
|
| 58 |
+
stats_path = src / "stats.json"
|
| 59 |
+
if stats_path.exists():
|
| 60 |
+
pst = json.loads(stats_path.read_text())
|
| 61 |
+
pool_done = pst["text_bytes"] >= pst["budget_bytes"] * 0.99
|
| 62 |
+
shards = all_shards if pool_done else all_shards[:-1]
|
| 63 |
+
out = ensure(shard_dir(lang, tok_name))
|
| 64 |
+
tok_dir = str(tokenizer_dir(tok_name))
|
| 65 |
+
|
| 66 |
+
index_path = out / "index.json"
|
| 67 |
+
index = json.loads(index_path.read_text()) if index_path.exists() else {}
|
| 68 |
+
import zlib
|
| 69 |
+
jobs = []
|
| 70 |
+
for s in shards:
|
| 71 |
+
dst = out / (s.name.replace(".jsonl.zst", ".bin"))
|
| 72 |
+
if dst.name not in index:
|
| 73 |
+
jobs.append((str(s), str(dst), tok_dir, seed ^ zlib.crc32(s.name.encode())))
|
| 74 |
+
|
| 75 |
+
total = sum(index.values())
|
| 76 |
+
if jobs:
|
| 77 |
+
with mp.Pool(workers) as pool:
|
| 78 |
+
for dst, n in pool.imap_unordered(_pack_one, jobs):
|
| 79 |
+
if n == 0:
|
| 80 |
+
# Empty pool checkpoint shards contain no documents. Do
|
| 81 |
+
# not expose a zero-byte file to np.memmap in PackedStream.
|
| 82 |
+
Path(dst).unlink(missing_ok=True)
|
| 83 |
+
continue
|
| 84 |
+
index[Path(dst).name] = n
|
| 85 |
+
total += n
|
| 86 |
+
index_path.write_text(json.dumps(index, indent=2, sort_keys=True))
|
| 87 |
+
print(f"[pack] {lang}/{tok_name}: {Path(dst).name} = {n/1e6:.1f}M tokens "
|
| 88 |
+
f"(total {total/1e9:.2f}B)")
|
| 89 |
+
if max_tokens and total >= max_tokens:
|
| 90 |
+
print("[pack] reached max_tokens; stopping")
|
| 91 |
+
pool.terminate()
|
| 92 |
+
break
|
| 93 |
+
meta = {"lang": lang, "tokenizer": tok_name, "total_tokens": total,
|
| 94 |
+
"n_shards": len(index)}
|
| 95 |
+
(out / "meta.json").write_text(json.dumps(meta, indent=2))
|
| 96 |
+
print(f"[pack] {lang}/{tok_name}: {total/1e9:.3f}B tokens in {len(index)} shards")
|
| 97 |
+
return meta
|