Cion-lab commited on
Commit
2d5accf
·
verified ·
1 Parent(s): 1f702fe

Phase 0: add ingest-throughput probe p0d

Browse files
Files changed (1) hide show
  1. probes/p0d_ingest_throughput.py +197 -0
probes/p0d_ingest_throughput.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 0 CPU probe D -- measures the three numbers that decide whether Phase 2 is schedulable.
2
+ # Zero GPU quota (§3.7: data work is a CPU job by rule).
3
+ #
4
+ # (a) sustained Hub download MB/s from inside a Kaggle session,
5
+ # (b) `datasets` 5.0 streaming rows/s on a real multi-GB corpus,
6
+ # (c) BPE tokenization tokens/s on 1 core and on 2 and 4 cores.
7
+ #
8
+ # Why (c) matters more than it looks: the mix is ~1B tokens and the CPU shape has 4 vCPUs and no
9
+ # swap. If tokenizing runs at a few thousand tok/s/core, Phase 2 costs days of wall clock before any
10
+ # training starts and the shard layout has to be designed around that. Measure, don't hope.
11
+ #
12
+ # Readable canonical copy: Cion-lab/ounce100m-code, probes/p0d_ingest_throughput.py
13
+ # Contains NO credentials.
14
+
15
+ import json
16
+ import multiprocessing as mp
17
+ import os
18
+ import socket
19
+ import subprocess
20
+ import sys
21
+ import time
22
+
23
+ R = {"host": socket.gethostname(), "python": sys.version.split()[0]}
24
+ BUDGET_S = 1500 # self-imposed; this must never turn into a data-engineering run
25
+ CORPUS_MB = 24
26
+ TOK_CHUNK = 6 * 1024**2 # bytes of text used for each tokenize timing
27
+ START = time.monotonic()
28
+
29
+
30
+ def over():
31
+ return time.monotonic() - START > BUDGET_S
32
+
33
+
34
+ # --------------------------------------------------------------------------- tokenizer
35
+ def load_tokenizer():
36
+ from huggingface_hub import hf_hub_download
37
+
38
+ path = hf_hub_download("gpt2", "tokenizer.json")
39
+ os.environ["TOK_JSON"] = path
40
+ return path
41
+
42
+
43
+ _g = {}
44
+
45
+
46
+ def _init():
47
+ from tokenizers import PreTrainedTokenizerFast
48
+
49
+ _g["tk"] = PreTrainedTokenizerFast.from_file(os.environ["TOK_JSON"])
50
+
51
+
52
+ def _encode_slice(text):
53
+ return len(_g["tk"].encode(text, add_special_tokens=False).ids)
54
+
55
+
56
+ def tokenize(text, workers):
57
+ """tokens/s via `tokenizers`, sharded across `workers` processes.
58
+
59
+ Slices go through memory, not /kaggle/working: the working volume is 19.5 GB and the point of
60
+ this probe is not to fill it.
61
+ """
62
+ step = max(1, len(text) // workers)
63
+ slices = [text[i * step:(i + 1) * step] for i in range(workers)]
64
+ slices[-1] = text[(workers - 1) * step:]
65
+ if workers == 1:
66
+ _init()
67
+ return sum(_encode_slice(s) for s in slices)
68
+ # fork, not spawn: this script is exec()'d inside a kernel, so a spawned child would try to
69
+ # re-import a __main__ that is not the probe and would recurse instead of tokenizing.
70
+ ctx = mp.get_context("fork")
71
+ with ctx.Pool(workers, initializer=_init) as pool:
72
+ return int(sum(pool.map(_encode_slice, slices)))
73
+
74
+
75
+ # --------------------------------------------------------------------------- corpus
76
+ def grab_corpus(target_bytes):
77
+ """Stream real English off the Hub into memory, returning text plus streaming stats."""
78
+ import datasets
79
+
80
+ keep, total, t0, rows = [], 0, time.monotonic(), 0
81
+ ds = datasets.load_dataset("wikimedia/wikipedia", "20231101.en", split="train",
82
+ streaming=True)
83
+ for row in ds:
84
+ txt = row["text"]
85
+ keep.append(txt)
86
+ total += len(txt)
87
+ rows += 1
88
+ if total >= target_bytes or time.monotonic() - t0 > 420:
89
+ break
90
+ el = time.monotonic() - t0
91
+ stats = {"rows": rows, "MB_text": round(total / 1024**2, 1), "seconds": round(el, 1),
92
+ "rows_per_s": round(rows / el, 1) if el else None,
93
+ "KB_text_per_s": round(total / el / 1024, 0) if el else None,
94
+ "hit_row_cap": total >= target_bytes}
95
+ return "\n".join(keep), stats
96
+
97
+
98
+ # --------------------------------------------------------------------------- raw HTTP
99
+ def list_parquet(repo_id, limit=8):
100
+ from huggingface_hub import HfApi
101
+
102
+ out = []
103
+ for f in HfApi().list_files_info(repo_id=repo_id, repo_type="dataset", revision="main"):
104
+ if f.path.endswith(".parquet"):
105
+ out.append({"path": f.path, "size_MB": round((f.size or 0) / 1024**2, 1)})
106
+ if len(out) >= limit:
107
+ break
108
+ return out
109
+
110
+
111
+ def http_throughput(url, max_bytes=300 * 1024**2, chunk=4 * 1024**2):
112
+ import urllib.request
113
+
114
+ req = urllib.request.Request(url, headers={"User-Agent": "ounce100m-probe"})
115
+ got, t0, ttfb = 0, time.monotonic(), None
116
+ with urllib.request.urlopen(req, timeout=120) as r:
117
+ while got < max_bytes:
118
+ b = r.read(chunk)
119
+ if not b:
120
+ break
121
+ if ttfb is None:
122
+ ttfb = time.monotonic() - t0
123
+ got += len(b)
124
+ el = time.monotonic() - t0
125
+ return {"bytes_read_MB": round(got / 1024**2, 1), "seconds": round(el, 1),
126
+ "MB_per_s": round(got / el / 1024**2, 1) if el else None,
127
+ "ttfb_s": round(ttfb, 2) if ttfb else None}
128
+
129
+
130
+ # --------------------------------------------------------------------------- run
131
+ try:
132
+ R["tokenizer"] = {"path": load_tokenizer(), "id": "gpt2 tokenizer.json (stand-in)"}
133
+ except Exception as e:
134
+ R["tokenizer"] = {"error": f"{type(e).__name__}: {e}"[:220]}
135
+
136
+ try:
137
+ R["listing_wikipedia"] = list_parquet("wikimedia/wikipedia")[:3]
138
+ except Exception as e:
139
+ R["listing_wikipedia"] = {"error": f"{type(e).__name__}: {e}"[:220]}
140
+
141
+ try:
142
+ R["listing_fineweb_edu"] = list_parquet("HuggingFaceFW/fineweb-edu")[:3]
143
+ except Exception as e:
144
+ R["listing_fineweb_edu"] = {"error": f"{type(e).__name__}: {e}"[:220]}
145
+
146
+ text = ""
147
+ if not over():
148
+ try:
149
+ text, R["streaming"] = grab_corpus(CORPUS_MB * 1024**2)
150
+ R["corpus_MB"] = round(len(text) / 1024**2, 1)
151
+ except Exception as e:
152
+ R["streaming"] = {"error": f"{type(e).__name__}: {e}"[:250]}
153
+
154
+ tok_sample = text[:TOK_CHUNK]
155
+ if tok_sample and "error" not in R["tokenizer"]:
156
+ for w in (1, 2, 4):
157
+ if over():
158
+ R["tokenize_stopped_at"] = w
159
+ break
160
+ try:
161
+ t0 = time.monotonic()
162
+ n = tokenize(tok_sample, w)
163
+ el = time.monotonic() - t0
164
+ tps = n / el
165
+ R[f"tokenize_{w}core"] = {
166
+ "tokens": n, "seconds": round(el, 1), "tokens_per_s": round(tps),
167
+ "hours_to_tokenize_1B_tokens": round(1e9 / tps / 3600, 1),
168
+ "chars_per_token": round(len(tok_sample) / n, 2) if n else None,
169
+ "GB_per_hour_at_1B_target": round(tps * 3600 * 4 / 1024**3, 2),
170
+ }
171
+ except Exception as e:
172
+ R[f"tokenize_{w}core"] = {"error": f"{type(e).__name__}: {e}"[:220]}
173
+ _g.clear()
174
+
175
+ # raw HTTP last: it is the noisiest measurement and the least likely to be cache-warm
176
+ for repo in ("wikimedia/wikipedia", "HuggingFaceFW/fineweb-edu"):
177
+ if over():
178
+ break
179
+ key = f"http_{repo}"
180
+ try:
181
+ big = [f for f in list_parquet(repo, limit=10) if f["size_MB"] > 150]
182
+ if not big:
183
+ R[key] = {"skipped": "no parquet >150 MB in the first 10"}
184
+ continue
185
+ url = f"https://huggingface.co/datasets/{repo}/resolve/main/{big[0]['path']}"
186
+ R[key] = {"file_MB": big[0]["size_MB"]}
187
+ R[key].update(http_throughput(url))
188
+ except Exception as e:
189
+ R[key] = {"error": f"{type(e).__name__}: {e}"[:220]}
190
+
191
+ R["_budget_exhausted"] = over()
192
+ R["_seconds_used"] = round(time.monotonic() - START, 1)
193
+ R["disk_after"] = subprocess.run("df -h /kaggle/working /dev/shm", shell=True,
194
+ capture_output=True, text=True).stdout.strip()
195
+ print("PROBE_JSON_BEGIN")
196
+ print(json.dumps(R, indent=1, default=str))
197
+ print("PROBE_JSON_END")