Neon-tech commited on
Commit
1340c9c
Β·
verified Β·
1 Parent(s): db6a360

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +335 -0
app.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import socket
5
+ import threading
6
+ import re
7
+ import requests
8
+ import pyarrow.parquet as pq
9
+ import pyarrow as pa
10
+ import gc
11
+ from pathlib import Path
12
+ from huggingface_hub import HfApi
13
+
14
+ # ── Config ───────────────────────────────────────────────────────────────────
15
+ HF_TOKEN = os.environ.get("HF_TOKEN")
16
+ RAW_DIR = "/data/raw"
17
+ STATE_FILE = "/data/state.json"
18
+ WORKER_TIMEOUT = 600
19
+ MAX_BUFFERED = 50
20
+ ROWS_PER_CHUNK = 50_000
21
+
22
+ os.makedirs(RAW_DIR, exist_ok=True)
23
+ api = HfApi(token=HF_TOKEN)
24
+
25
+ AUTH_HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"}
26
+
27
+ # ── Sources ───────────────────────────────────────────────────────────────────
28
+ # Each source: (name, type, urls_or_config)
29
+ # Types: parquet_list, hf_list
30
+ # For hf_list: uses HF API to discover files
31
+ SOURCES = [
32
+ {
33
+ "name" : "fineweb",
34
+ "type" : "hf_list",
35
+ "repo" : "HuggingFaceFW/fineweb-edu",
36
+ "prefix" : "data/CC-MAIN-2025-26",
37
+ "skip" : 0, # already in state from prev run β€” coordinator skips done ones
38
+ "take" : 10,
39
+ "text_col": "text",
40
+ },
41
+ {
42
+ "name" : "wikipedia",
43
+ "type" : "hf_list",
44
+ "repo" : "wikimedia/wikipedia",
45
+ "prefix" : "20231101.en/train-",
46
+ "skip" : 2, # first 2 already done in 50M
47
+ "take" : 18,
48
+ "text_col": "text",
49
+ },
50
+ {
51
+ "name" : "openwebmath",
52
+ "type" : "hf_list",
53
+ "repo" : "open-web-math/open-web-math",
54
+ "prefix" : "data/train-",
55
+ "skip" : 0,
56
+ "take" : 6,
57
+ "text_col": "text",
58
+ },
59
+ {
60
+ "name" : "phi",
61
+ "type" : "url_list",
62
+ "urls" : [
63
+ "https://huggingface.co/datasets/open-phi/programming_books_llama/resolve/main/data/train-00000-of-00004-ea05c5cb63b570a8.parquet?download=true",
64
+ "https://huggingface.co/datasets/open-phi/programming_books_llama/resolve/main/data/train-00001-of-00004-d99cbe052bab0d4e.parquet?download=true",
65
+ "https://huggingface.co/datasets/open-phi/programming_books_llama/resolve/main/data/train-00002-of-00004-2c25f0e11d537eaf.parquet?download=true",
66
+ "https://huggingface.co/datasets/open-phi/programming_books_llama/resolve/main/data/train-00003-of-00004-faa8dbb07e5f02e8.parquet?download=true",
67
+ ],
68
+ "text_col": "markdown",
69
+ },
70
+ {
71
+ "name" : "code",
72
+ "type" : "url_list",
73
+ "urls" : [
74
+ # 12 new languages Γ— 2 shards = 24 files
75
+ # Base: https://huggingface.co/datasets/Neon-tech/Dataset-arranger/resolve/main/by-language
76
+ *[
77
+ f"https://huggingface.co/datasets/Neon-tech/Dataset-arranger/resolve/main/by-language/{lang}/shard_{str(i).zfill(6)}.jsonl?download=true"
78
+ for lang in ["C", "C++", "Java", "Go", "Rust", "Ruby", "PHP", "SQL", "C#", "Scala", "Lua", "Perl"]
79
+ for i in range(2)
80
+ ],
81
+ ],
82
+ "text_col": "text",
83
+ "fmt" : "jsonl",
84
+ },
85
+ ]
86
+
87
+ # ── Keep-alive ────────────────────────────────────────────────────────────────
88
+ def serve():
89
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
90
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
91
+ s.bind(("0.0.0.0", 7860))
92
+ s.listen(5)
93
+ print("βœ“ Listening on port 7860")
94
+ while True:
95
+ conn, _ = s.accept()
96
+ conn.send(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
97
+ conn.close()
98
+
99
+ # ── Friendly name ─────────────────────────────────────────────────────────────
100
+ def friendly_name(source_name, url_or_path):
101
+ # Strip query string
102
+ base = url_or_path.split("?")[0].split("/")[-1]
103
+ return f"{source_name}__{base}"
104
+
105
+ # ── State ─────────────────────────────────────────────────────────────────────
106
+ def load_state():
107
+ if os.path.exists(STATE_FILE):
108
+ with open(STATE_FILE) as f:
109
+ state = json.load(f)
110
+ shards = state["shards"]
111
+ queue = state.get("queue", [])
112
+ done = sum(1 for v in shards.values() if v["status"] == "done")
113
+ claimed = sum(1 for v in shards.values() if v["status"] == "claimed")
114
+ pending = sum(1 for v in shards.values() if v["status"] == "pending")
115
+ print(f"Resuming β€” {done} done / {claimed} claimed / {pending} buffered / {len(queue)} queued")
116
+ else:
117
+ state = {"shards": {}, "queue": []}
118
+ print("Starting fresh")
119
+ return state
120
+
121
+ def save_state(state):
122
+ tmp = STATE_FILE + ".tmp"
123
+ with open(tmp, "w") as f:
124
+ json.dump(state, f, indent=2)
125
+ os.replace(tmp, STATE_FILE)
126
+
127
+ # ── Discover all sources ──────────────────────────────────────────────────────
128
+ def discover_all(state):
129
+ known = {v["url"] for v in state["shards"].values()} | {e["url"] for e in state.get("queue", [])}
130
+ new_count = 0
131
+
132
+ for src in SOURCES:
133
+ name = src["name"]
134
+ print(f"\nDiscovering: {name}")
135
+
136
+ if src["type"] == "hf_list":
137
+ files = list(api.list_repo_files(src["repo"], repo_type="dataset"))
138
+ files = [f for f in files if f.startswith(src["prefix"]) and f.endswith(".parquet")]
139
+ files = sorted(files)
140
+ files = files[src["skip"]: src["skip"] + src["take"]]
141
+ base = f"https://huggingface.co/datasets/{src['repo']}/resolve/main/"
142
+ urls = [base + f for f in files]
143
+ fmt = "parquet"
144
+ else:
145
+ urls = src["urls"]
146
+ fmt = src.get("fmt", "parquet")
147
+
148
+ for url in urls:
149
+ if url not in known:
150
+ state["queue"].append({
151
+ "url" : url,
152
+ "source" : name,
153
+ "text_col" : src["text_col"],
154
+ "fmt" : fmt,
155
+ })
156
+ known.add(url)
157
+ new_count += 1
158
+
159
+ print(f" {name}: {len(urls)} files | {new_count} new queued")
160
+
161
+ save_state(state)
162
+ print(f"\nTotal queued: {len(state['queue'])} | In state: {len(state['shards'])}")
163
+
164
+ # ── Reclaim stale ─────────────────────────────────────────────────────────────
165
+ def reclaim_stale(state):
166
+ now = time.time()
167
+ for name, info in state["shards"].items():
168
+ if info["status"] == "claimed" and info.get("claimed_at"):
169
+ if now - info["claimed_at"] > WORKER_TIMEOUT:
170
+ print(f" ⚠ Reclaiming: {name}")
171
+ info["status"] = "pending"
172
+ info["worker"] = None
173
+ info["claimed_at"] = None
174
+ save_state(state)
175
+
176
+ # ── Split parquet into chunks ─────────────────────────────────────────────────
177
+ def split_parquet(src_path, name, text_col):
178
+ pf = pq.ParquetFile(src_path)
179
+ chunk_paths = []
180
+ chunk_idx = 0
181
+ current = []
182
+
183
+ for batch in pf.iter_batches(batch_size=10_000, columns=[text_col]):
184
+ current.append(batch)
185
+ if sum(len(b) for b in current) >= ROWS_PER_CHUNK:
186
+ chunk_name = name.replace(".parquet", f"_chunk{chunk_idx:03d}.parquet")
187
+ chunk_path = Path(RAW_DIR) / chunk_name
188
+ table = pa.Table.from_batches(current)
189
+ pq.write_table(table, chunk_path)
190
+ print(f" βœ“ {chunk_name} ({len(table):,} rows)")
191
+ chunk_paths.append((chunk_name, text_col, "parquet"))
192
+ chunk_idx += 1
193
+ current = []
194
+ del table; gc.collect()
195
+
196
+ if current:
197
+ chunk_name = name.replace(".parquet", f"_chunk{chunk_idx:03d}.parquet")
198
+ chunk_path = Path(RAW_DIR) / chunk_name
199
+ table = pa.Table.from_batches(current)
200
+ pq.write_table(table, chunk_path)
201
+ print(f" βœ“ {chunk_name} ({len(table):,} rows)")
202
+ chunk_paths.append((chunk_name, text_col, "parquet"))
203
+ del table; gc.collect()
204
+
205
+ return chunk_paths
206
+
207
+ def copy_jsonl(src_path, name):
208
+ """JSONL files are small enough to use directly β€” just copy to raw dir."""
209
+ import shutil
210
+ dst = Path(RAW_DIR) / name
211
+ shutil.copy2(src_path, dst)
212
+ return [(name, "text", "jsonl")]
213
+
214
+ # ── Download loop ─────────────────────────────────────────────────────────────
215
+ def download_loop(state):
216
+ while True:
217
+ # Reload state
218
+ try:
219
+ with open(STATE_FILE) as f:
220
+ fresh = json.load(f)
221
+ state["shards"] = fresh["shards"]
222
+ state["queue"] = fresh.get("queue", [])
223
+ except Exception:
224
+ pass
225
+
226
+ reclaim_stale(state)
227
+
228
+ buffered = sum(1 for v in state["shards"].values() if v["status"] == "pending")
229
+ if buffered >= MAX_BUFFERED:
230
+ time.sleep(30)
231
+ continue
232
+
233
+ if not state["queue"]:
234
+ done = sum(1 for v in state["shards"].values() if v["status"] == "done")
235
+ total = len(state["shards"])
236
+ if done == total and total > 0:
237
+ print("βœ“ All shards complete!")
238
+ break
239
+ print(" Queue empty β€” sleeping...")
240
+ time.sleep(60)
241
+ continue
242
+
243
+ entry = state["queue"][0]
244
+ url = entry["url"]
245
+ source = entry["source"]
246
+ text_col = entry["text_col"]
247
+ fmt = entry.get("fmt", "parquet")
248
+ ext = ".jsonl" if fmt == "jsonl" else ".parquet"
249
+ name = friendly_name(source, url)
250
+ if not name.endswith(ext):
251
+ name = name.split(".")[0] + ext
252
+ raw_path = Path(RAW_DIR) / name
253
+ tmp_path = Path(RAW_DIR) / f"{name}.tmp"
254
+
255
+ print(f" Downloading: {source} | {url.split('/')[-1].split('?')[0]}")
256
+ try:
257
+ resp = requests.get(url, headers=AUTH_HEADERS, timeout=300, stream=True)
258
+ resp.raise_for_status()
259
+ with open(tmp_path, "wb") as f:
260
+ for chunk in resp.iter_content(chunk_size=8 * 1024 * 1024):
261
+ f.write(chunk)
262
+ tmp_path.rename(raw_path)
263
+ except Exception as e:
264
+ print(f" βœ— Download failed: {e} β€” retrying in 30s")
265
+ tmp_path.unlink(missing_ok=True)
266
+ time.sleep(30)
267
+ continue
268
+
269
+ print(f" Processing: {name}")
270
+ try:
271
+ if fmt == "parquet":
272
+ chunks = split_parquet(raw_path, name, text_col)
273
+ else:
274
+ chunks = copy_jsonl(raw_path, name)
275
+ except Exception as e:
276
+ print(f" βœ— Processing failed: {e}")
277
+ raw_path.unlink(missing_ok=True)
278
+ time.sleep(30)
279
+ continue
280
+
281
+ raw_path.unlink(missing_ok=True)
282
+ state["queue"].pop(0)
283
+
284
+ for chunk_name, col, chunk_fmt in chunks:
285
+ state["shards"][chunk_name] = {
286
+ "status" : "pending",
287
+ "url" : url,
288
+ "source" : source,
289
+ "text_col" : col,
290
+ "fmt" : chunk_fmt,
291
+ "worker" : None,
292
+ "claimed_at": None,
293
+ "error" : None,
294
+ }
295
+ save_state(state)
296
+ print(f" βœ“ {len(chunks)} chunks ready from {name}")
297
+ time.sleep(3)
298
+
299
+ # ── Monitor ───────────────────────────────────────────────────────────────────
300
+ def monitor_loop():
301
+ while True:
302
+ time.sleep(120)
303
+ try:
304
+ with open(STATE_FILE) as f:
305
+ s = json.load(f)
306
+ shards = s["shards"]
307
+ queue = s.get("queue", [])
308
+ done = sum(1 for v in shards.values() if v["status"] == "done")
309
+ claimed = sum(1 for v in shards.values() if v["status"] == "claimed")
310
+ pending = sum(1 for v in shards.values() if v["status"] == "pending")
311
+ total = len(shards) + len(queue)
312
+ pct = (done / total * 100) if total else 0
313
+
314
+ # Per-source breakdown
315
+ src_done = {}
316
+ for v in shards.values():
317
+ src = v.get("source", "?")
318
+ if v["status"] == "done":
319
+ src_done[src] = src_done.get(src, 0) + 1
320
+
321
+ print(f"[MONITOR] {done}/{total} ({pct:.1f}%) | {claimed} active | {pending} buffered | {len(queue)} queued")
322
+ for src, cnt in sorted(src_done.items()):
323
+ print(f" {src}: {cnt} done")
324
+ except Exception:
325
+ pass
326
+
327
+ # ── Entry point ───────────────────────────────────────────────────────────────
328
+ if __name__ == "__main__":
329
+ threading.Thread(target=serve, daemon=True).start()
330
+ state = load_state()
331
+ discover_all(state)
332
+ threading.Thread(target=monitor_loop, daemon=True).start()
333
+ threading.Thread(target=download_loop, args=(state,), daemon=True).start()
334
+ while True:
335
+ time.sleep(60)