Cion-lab commited on
Commit
d4f2435
·
verified ·
1 Parent(s): 5749ec3

p0d v2: version-free tree API + subprocess rayon bench (fixes 3 API-skew faults)

Browse files
Files changed (1) hide show
  1. probes/p0d_ingest_throughput.py +110 -126
probes/p0d_ingest_throughput.py CHANGED
@@ -1,111 +1,52 @@
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):
@@ -127,62 +68,105 @@ def http_throughput(url, max_bytes=300 * 1024**2, chunk=4 * 1024**2):
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:
 
1
+ # Phase 0 CPU probe D v2 -- ingest throughput for sizing Phase 2. Quota-free (CPU) by rule.
 
2
  #
3
+ # v1 measured one thing and failed at three others, all for the same reason: it used APIs that exist
4
+ # in this workspace's huggingface_hub 1.32 / tokenizers 0.22 and do not exist in the Kaggle image's
5
+ # (hub 1.11 has no HfApi.list_files_info; tokenizers 0.22 does not export PreTrainedTokenizerFast at
6
+ # top level), and it used multiprocessing.Pool, which cannot work in a kernel that was exec()'d
7
+ # because the worker tries to pickle functions by qualified name out of a __main__ that is not an
8
+ # importable module. See ERRORS.md E-007/E-009.
9
  #
10
+ # v2 therefore: plain HTTP against the public /api/.../tree endpoint for file listings, the generic
11
+ # tokenizers.Tokenizer for encoding, and re-exec of THIS file as a subprocess per thread setting so
12
+ # rayon's pool size is chosen before the library is imported. No credentials in this file.
13
  #
14
+ # Measures: (a) sustained Hub download MB/s, (b) datasets streaming rows/s + KB text/s,
15
+ # (c) BPE tokens/s at 1, 2 and 4 cores -- which is what says whether Phase 2 is hours or days.
16
 
17
  import json
 
18
  import os
19
  import socket
20
  import subprocess
21
  import sys
22
  import time
23
 
24
+ BUDGET_S = 1500
 
 
 
25
  START = time.monotonic()
26
+ CORPUS_MB = 24
27
+ TOK_MB = 6
28
+ GPT2_CHARS_PER_TOKEN = 3.6
29
 
30
 
31
  def over():
32
  return time.monotonic() - START > BUDGET_S
33
 
34
 
35
+ def http_json(url):
36
+ import urllib.request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ req = urllib.request.Request(url, headers={"User-Agent": "ounce100m-probe"})
39
+ with urllib.request.urlopen(req, timeout=90) as r:
40
+ return json.loads(r.read().decode())
41
 
 
 
 
 
42
 
43
+ def tree_parquet(repo_id, repo_type="datasets", limit=10):
44
+ """Largest files in a repo, via the public tree API -- no client library, no version skew."""
45
+ url = f"https://huggingface.co/api/{repo_type}/{repo_id}/tree/main?recursive=true"
46
+ out = [e for e in http_json(url)
47
+ if e.get("type") == "file" and str(e.get("path", "")).endswith(".parquet")]
48
+ out.sort(key=lambda e: -(e.get("size") or 0))
49
+ return [{"path": e["path"], "size_MB": round((e.get("size") or 0) / 1024**2, 1)} for e in out[:limit]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
 
52
  def http_throughput(url, max_bytes=300 * 1024**2, chunk=4 * 1024**2):
 
68
  "ttfb_s": round(ttfb, 2) if ttfb else None}
69
 
70
 
71
+ def grab_corpus(target_bytes):
72
+ import datasets
 
 
 
73
 
74
+ keep, total, t0, rows = [], 0, time.monotonic(), 0
75
+ for row in datasets.load_dataset("wikimedia/wikipedia", "20231101.en", split="train",
76
+ streaming=True):
77
+ txt = row["text"]
78
+ keep.append(txt)
79
+ total += len(txt)
80
+ rows += 1
81
+ if total >= target_bytes or time.monotonic() - t0 > 420:
82
+ break
83
+ el = time.monotonic() - t0
84
+ return "\n".join(keep), {"rows": rows, "MB_text": round(total / 1024**2, 1),
85
+ "seconds": round(el, 1), "rows_per_s": round(rows / el, 1),
86
+ "KB_text_per_s": round(total / el / 1024, 0)}
87
+
88
+
89
+ # ---------------------------------------------------------------- worker mode: one thread budget
90
+ if len(sys.argv) > 1 and sys.argv[1] == "tokworker":
91
+ from tokenizers import Tokenizer
92
+
93
+ tk = Tokenizer.from_file(os.environ["TOK_JSON"])
94
+ tk.no_truncate()
95
+ with open("/dev/shm/" + sys.argv[2]) as f:
96
+ chunks = json.load(f)
97
+ t = time.monotonic()
98
+ enc = tk.encode_batch(chunks) # rayon-parallel across cores, released GIL
99
+ el = time.monotonic() - t
100
+ n = sum(len(e.ids) for e in enc)
101
+ print("TOKRESULT " + json.dumps({"tokens": n, "seconds": round(el, 2),
102
+ "rayon": os.environ.get("RAYON_NUM_THREADS")}), flush=True)
103
+ sys.exit(0)
104
+
105
+ # ---------------------------------------------------------------- orchestrator
106
+ R = {"host": socket.gethostname()}
107
+ text = ""
108
 
109
  try:
110
+ from huggingface_hub import hf_hub_download
111
+
112
+ os.environ["TOK_JSON"] = hf_hub_download("gpt2", "tokenizer.json")
113
+ R["tokenizer"] = {"id": "gpt2/tokenizer.json", "path": os.environ["TOK_JSON"]}
114
  except Exception as e:
115
+ R["tokenizer"] = {"error": f"{type(e).__name__}: {e}"[:200]}
116
 
117
+ for repo in ("wikimedia/wikipedia", "HuggingFaceFW/fineweb-edu"):
 
118
  try:
119
+ R[f"tree_{repo}"] = tree_parquet(repo)
 
120
  except Exception as e:
121
+ R[f"tree_{repo}"] = {"error": f"{type(e).__name__}: {e}"[:200]}
122
 
123
+ try:
124
+ text, R["streaming"] = grab_corpus(CORPUS_MB * 1024**2)
125
+ R["corpus_MB"] = round(len(text) / 1024**2, 1)
126
+ except Exception as e:
127
+ R["streaming"] = {"error": f"{type(e).__name__}: {e}"[:250]}
128
+
129
+ if text and "error" not in R["tokenizer"]:
130
+ sample = text[:TOK_MB * 1024**2]
131
+ # Chunks sized for rayon: many small pieces keep all cores fed far better than one blob.
132
+ step = max(1, len(sample) // 64)
133
+ chunks = [sample[i:i + step] for i in range(0, len(sample), step)]
134
+ with open("/dev/shm/p0d_chunks.json", "w") as f:
135
+ json.dump(chunks, f)
136
+ # The launcher wrote this exact filename into cwd (/kaggle/working), so re-exec that rather than
137
+ # sys.argv[0], which points at the kernel's own wrapper script, not at this probe.
138
+ here = "p0d_ingest_throughput.py"
139
+ for threads in (1, 2, 4):
140
  if over():
 
141
  break
142
+ env = {**os.environ, "RAYON_NUM_THREADS": str(threads), "OMP_NUM_THREADS": str(threads)}
143
+ p = subprocess.run([sys.executable, here, "tokworker"], capture_output=True, text=True,
144
+ env=env, timeout=900)
145
+ line = next((l for l in p.stdout.splitlines() if l.startswith("TOKRESULT")), None)
146
+ if line:
147
+ w = json.loads(line[len("TOKRESULT"):])
148
+ tps = w["tokens"] / w["seconds"]
149
+ R[f"tokenize_{threads}core"] = {
150
+ **w, "tokens_per_s": round(tps),
151
+ "hours_to_tokenize_1B_tokens": round(1e9 / tps / 3600, 2),
152
+ "GB_text_per_hour": round(tps * 3600 * GPT2_CHARS_PER_TOKEN / 1024**3, 2),
153
+ "chars_per_token": round(len(sample) / w["tokens"], 2),
154
  }
155
+ else:
156
+ R[f"tokenize_{threads}core"] = {"error": (p.stderr or p.stdout)[-300:]}
157
+ with open("/dev/shm/p0d_chunks.json", "w") as f:
158
+ f.write("{}")
159
 
 
160
  for repo in ("wikimedia/wikipedia", "HuggingFaceFW/fineweb-edu"):
161
  if over():
162
  break
163
  key = f"http_{repo}"
164
+ big = [f for f in R.get(key.replace("http", "tree"), []) if f["size_MB"] > 150]
165
+ if not big:
166
+ R[key] = {"skipped": "no parquet >150MB listed"}
167
+ continue
168
+ url = f"https://huggingface.co/datasets/{repo}/resolve/main/{big[0]['path']}"
169
  try:
 
 
 
 
 
170
  R[key] = {"file_MB": big[0]["size_MB"]}
171
  R[key].update(http_throughput(url))
172
  except Exception as e: