BHARGAV REDDY commited on
Commit
23b3bd1
·
verified ·
1 Parent(s): 96c340f

Upload Base/scripts/prepare_litdata.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. Base/scripts/prepare_litdata.py +379 -275
Base/scripts/prepare_litdata.py CHANGED
@@ -1,275 +1,379 @@
1
- """
2
- Tokenize filtered parquet data and create LitData streaming format for litgpt.
3
-
4
- Creates flat binary token chunks compatible with litdata's TokensLoader,
5
- which litgpt's LitData data module uses for pretraining.
6
-
7
- STREAMING MODE: Tokenizes texts in batches and flushes chunks to disk
8
- incrementally, keeping RAM usage constant (~128MB buffer) regardless of
9
- dataset size. Supports resuming from the last completed chunk.
10
-
11
- Usage:
12
- python Base/scripts/prepare_litdata.py --mode small
13
- python Base/scripts/prepare_litdata.py --mode full
14
- python Base/scripts/prepare_litdata.py --mode both
15
- """
16
-
17
- import argparse
18
- import json
19
- import os
20
- import time
21
- from pathlib import Path
22
-
23
- import numpy as np
24
- import pyarrow.parquet as pq
25
-
26
- from litgpt.tokenizer import Tokenizer
27
-
28
-
29
- # Must match model config's block_size + 1 (extra token for targets)
30
- BLOCK_SIZE = 1025
31
- # 64 MB per chunk file
32
- CHUNK_BYTES_TARGET = 64 * 1024 * 1024
33
- # litdata's TokensLoader uses _TORCH_DTYPES_MAPPING[16] = torch.int32
34
- DTYPE = np.int32
35
- # litdata dtype index — 16 maps to torch.int32
36
- DTYPE_INDEX = 16
37
-
38
-
39
- def _write_chunk(output_dir, chunk_idx, block_buffer, num_blocks):
40
- """Write a single chunk file with litdata header + flat int32 data."""
41
- dtype_size = DTYPE().itemsize
42
- filename = f"chunk-0-{chunk_idx}.bin"
43
- filepath = os.path.join(output_dir, filename)
44
-
45
- chunk_data = block_buffer[:num_blocks * BLOCK_SIZE]
46
-
47
- # Header: [num_items(uint32)] + [offsets 0..num_blocks(uint32)]
48
- header_num_items = np.array([num_blocks], dtype=np.uint32)
49
- offsets = np.arange(num_blocks + 1, dtype=np.uint32) * (BLOCK_SIZE * dtype_size)
50
- header = np.concatenate([header_num_items, offsets])
51
-
52
- with open(filepath, "wb") as f:
53
- header.tofile(f)
54
- chunk_data.tofile(f)
55
-
56
- data_bytes = int(chunk_data.nbytes)
57
- header_bytes = int(header.nbytes)
58
- return {
59
- "chunk_bytes": data_bytes + header_bytes,
60
- "chunk_size": num_blocks,
61
- "dim": int(len(chunk_data)),
62
- "filename": filename,
63
- }, header_bytes, data_bytes
64
-
65
-
66
- def _load_resume_state(output_dir):
67
- """Check for existing chunks to support resuming."""
68
- index_path = os.path.join(output_dir, "index.json.partial")
69
- if not os.path.exists(index_path):
70
- return [], 0, 0
71
- with open(index_path) as f:
72
- state = json.load(f)
73
- chunks = state.get("chunks", [])
74
- tokens_written = sum(c["dim"] for c in chunks)
75
- return chunks, len(chunks), tokens_written
76
-
77
-
78
- def _save_resume_state(output_dir, chunks_metadata):
79
- """Save partial progress for resume."""
80
- path = os.path.join(output_dir, "index.json.partial")
81
- with open(path, "w") as f:
82
- json.dump({"chunks": chunks_metadata}, f)
83
-
84
-
85
- def prepare_litdata(filtered_dir: str, output_dir: str, tokenizer_path: str, label: str):
86
- """Tokenize filtered text and write chunks in streaming fashion (constant RAM)."""
87
- print(f"\n{'='*60}")
88
- print(f"Preparing LitData [{label}]")
89
- print(f"Input: {filtered_dir}")
90
- print(f"Output: {output_dir}")
91
- print(f"Tokenizer: {tokenizer_path}")
92
- print(f"{'='*60}\n")
93
-
94
- if not Path(filtered_dir).exists():
95
- print(f"ERROR: Filtered directory not found: {filtered_dir}")
96
- print("Run filter_datasets.py first!")
97
- return
98
-
99
- os.makedirs(output_dir, exist_ok=True)
100
- tokenizer = Tokenizer(Path(tokenizer_path))
101
-
102
- # How many tokens fit in one chunk (aligned to BLOCK_SIZE)
103
- dtype_size = DTYPE().itemsize
104
- tokens_per_chunk = CHUNK_BYTES_TARGET // dtype_size
105
- tokens_per_chunk = (tokens_per_chunk // BLOCK_SIZE) * BLOCK_SIZE
106
-
107
- # Check for resume state
108
- chunks_metadata, chunk_idx, tokens_to_skip = _load_resume_state(output_dir)
109
- if tokens_to_skip > 0:
110
- print(f"RESUMING: Found {chunk_idx} existing chunks ({tokens_to_skip:,} tokens)")
111
- print(f" Will skip {tokens_to_skip:,} tokens then continue writing chunks\n")
112
-
113
- # Token buffer holds at most one chunk worth of tokens (~64MB)
114
- token_buf = np.empty(tokens_per_chunk + BLOCK_SIZE * 512, dtype=DTYPE)
115
- buf_pos = 0 # current write position in buffer
116
- total_tokens_written = sum(c["dim"] for c in chunks_metadata)
117
- total_tokens_seen = 0
118
- total_texts = 0
119
- skipped_tokens = 0
120
-
121
- parquet_files = sorted(Path(filtered_dir).glob("*.parquet"))
122
- t0 = time.time()
123
-
124
- print("Tokenizing & writing chunks (streaming)...")
125
- for pf in parquet_files:
126
- parquet_file = pq.ParquetFile(str(pf))
127
- file_count = 0
128
- for batch in parquet_file.iter_batches(batch_size=4096, columns=["text"]):
129
- texts = batch.column("text").to_pylist()
130
- for text in texts:
131
- tokens = tokenizer.encode(text, bos=False, eos=True)
132
- tok_array = tokens.numpy().astype(DTYPE) if hasattr(tokens, 'numpy') else np.array(tokens.tolist(), dtype=DTYPE)
133
- n = len(tok_array)
134
- total_tokens_seen += n
135
-
136
- # If resuming, skip tokens already written
137
- if skipped_tokens < tokens_to_skip:
138
- remaining_skip = tokens_to_skip - skipped_tokens
139
- if n <= remaining_skip:
140
- skipped_tokens += n
141
- file_count += 1
142
- continue
143
- else:
144
- tok_array = tok_array[int(remaining_skip):]
145
- skipped_tokens = tokens_to_skip
146
- n = len(tok_array)
147
-
148
- # Append to buffer
149
- if buf_pos + n > len(token_buf):
150
- # Grow buffer if needed (rare)
151
- token_buf = np.concatenate([token_buf[:buf_pos], np.empty(max(n, BLOCK_SIZE * 512), dtype=DTYPE)])
152
-
153
- token_buf[buf_pos:buf_pos + n] = tok_array
154
- buf_pos += n
155
- file_count += 1
156
-
157
- # Flush full chunks from buffer
158
- while buf_pos >= tokens_per_chunk:
159
- num_blocks = tokens_per_chunk // BLOCK_SIZE
160
- chunk_array = token_buf[:tokens_per_chunk].copy()
161
-
162
- meta, hdr_b, data_b = _write_chunk(output_dir, chunk_idx, chunk_array, num_blocks)
163
- chunks_metadata.append(meta)
164
- total_tokens_written += meta["dim"]
165
- _save_resume_state(output_dir, chunks_metadata)
166
-
167
- print(f" chunk-0-{chunk_idx}.bin: {num_blocks} blocks, "
168
- f"{meta['dim']:,} tokens (header: {hdr_b/1024:.1f} KB, "
169
- f"data: {data_b/1024/1024:.1f} MB) | "
170
- f"total: {total_tokens_written:,}")
171
- chunk_idx += 1
172
-
173
- # Shift remaining tokens to front of buffer
174
- leftover = buf_pos - tokens_per_chunk
175
- if leftover > 0:
176
- token_buf[:leftover] = token_buf[tokens_per_chunk:tokens_per_chunk + leftover]
177
- buf_pos = leftover
178
-
179
- total_texts += file_count
180
- parquet_file.close()
181
- elapsed = time.time() - t0
182
- print(f" [{pf.name}] {file_count:,} texts | "
183
- f"total tokens written: {total_tokens_written:,} | "
184
- f"elapsed: {elapsed:.0f}s")
185
-
186
- # Flush remaining buffer as final chunk (if enough for at least 1 block)
187
- remaining_blocks = buf_pos // BLOCK_SIZE
188
- if remaining_blocks > 0:
189
- final_tokens = remaining_blocks * BLOCK_SIZE
190
- chunk_array = token_buf[:final_tokens].copy()
191
- meta, hdr_b, data_b = _write_chunk(output_dir, chunk_idx, chunk_array, remaining_blocks)
192
- chunks_metadata.append(meta)
193
- total_tokens_written += meta["dim"]
194
- print(f" chunk-0-{chunk_idx}.bin (final): {remaining_blocks} blocks, "
195
- f"{meta['dim']:,} tokens (header: {hdr_b/1024:.1f} KB, "
196
- f"data: {data_b/1024/1024:.1f} MB)")
197
- chunk_idx += 1
198
-
199
- # Write final index.json
200
- index = {
201
- "chunks": chunks_metadata,
202
- "config": {
203
- "chunk_bytes": CHUNK_BYTES_TARGET,
204
- "chunk_size": None,
205
- "compression": None,
206
- "data_format": [f"no_header_tensor:{DTYPE_INDEX}"],
207
- "data_spec": None,
208
- "encryption": None,
209
- "item_loader": "TokensLoader",
210
- },
211
- "updated_at": str(time.time()),
212
- }
213
- with open(os.path.join(output_dir, "index.json"), "w") as f:
214
- json.dump(index, f, indent=2)
215
-
216
- # Clean up partial state
217
- partial_path = os.path.join(output_dir, "index.json.partial")
218
- if os.path.exists(partial_path):
219
- os.remove(partial_path)
220
-
221
- elapsed = time.time() - t0
222
- print(f"\n--- LitData preparation [{label}] complete ---")
223
- print(f"Chunks: {chunk_idx}")
224
- print(f"Total tokens: {total_tokens_written:,}")
225
- print(f"Total blocks: {total_tokens_written // BLOCK_SIZE:,}")
226
- print(f"Texts processed: {total_texts:,}")
227
- print(f"Time: {elapsed:.0f}s ({total_tokens_written/elapsed:.0f} tok/s)")
228
- print(f"Output: {output_dir}")
229
-
230
-
231
- def main():
232
- parser = argparse.ArgumentParser(description="Prepare LitData from filtered parquet")
233
- parser.add_argument(
234
- "--mode",
235
- choices=["small", "full", "both", "english"],
236
- default="small",
237
- help="Which dataset to prepare (english = ultra-clean English corpus)",
238
- )
239
- parser.add_argument(
240
- "--tokenizer_path",
241
- type=str,
242
- default="Base/checkpoints/EleutherAI/pythia-160m",
243
- help="Path to tokenizer directory",
244
- )
245
- args = parser.parse_args()
246
-
247
- if args.mode in ("small", "both"):
248
- prepare_litdata(
249
- filtered_dir="Base/data/filtered_10m",
250
- output_dir="Base/data/litdata_10m",
251
- tokenizer_path=args.tokenizer_path,
252
- label="SMALL (10M)",
253
- )
254
-
255
- if args.mode in ("full", "both"):
256
- prepare_litdata(
257
- filtered_dir="Base/data/filtered_3b",
258
- output_dir="Base/data/litdata_3b",
259
- tokenizer_path=args.tokenizer_path,
260
- label="FULL (3B)",
261
- )
262
-
263
- if args.mode == "english":
264
- prepare_litdata(
265
- filtered_dir="Base/data/filtered_english",
266
- output_dir="Base/data/litdata_english",
267
- tokenizer_path=args.tokenizer_path,
268
- label="ENGLISH (ultra-clean)",
269
- )
270
-
271
- print("\nDone! Next step: run litgpt pretrain with the appropriate config.")
272
-
273
-
274
- if __name__ == "__main__":
275
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tokenize filtered parquet data and create LitData streaming format for litgpt.
3
+
4
+ Creates flat binary token chunks compatible with litdata's TokensLoader,
5
+ which litgpt's LitData data module uses for pretraining.
6
+
7
+ STREAMING MODE: Tokenizes texts in batches and flushes chunks to disk
8
+ incrementally, keeping RAM usage constant (~128MB buffer) regardless of
9
+ dataset size. Supports resuming from the last completed chunk.
10
+
11
+ Usage:
12
+ python Base/scripts/prepare_litdata.py --mode small
13
+ python Base/scripts/prepare_litdata.py --mode full
14
+ python Base/scripts/prepare_litdata.py --mode both
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import time
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import pyarrow.parquet as pq
25
+
26
+ from litgpt.tokenizer import Tokenizer
27
+
28
+
29
+ # Must match model config's block_size + 1 (extra token for targets)
30
+ BLOCK_SIZE = 1025
31
+ # 64 MB per chunk file
32
+ CHUNK_BYTES_TARGET = 64 * 1024 * 1024
33
+ # litdata's TokensLoader uses _TORCH_DTYPES_MAPPING[16] = torch.int32
34
+ DTYPE = np.int32
35
+ # litdata dtype index — 16 maps to torch.int32
36
+ DTYPE_INDEX = 16
37
+
38
+
39
+ def _write_chunk(output_dir, chunk_idx, block_buffer, num_blocks):
40
+ """Write a single chunk file with litdata header + flat int32 data."""
41
+ dtype_size = DTYPE().itemsize
42
+ filename = f"chunk-0-{chunk_idx}.bin"
43
+ filepath = os.path.join(output_dir, filename)
44
+
45
+ chunk_data = block_buffer[:num_blocks * BLOCK_SIZE]
46
+
47
+ # Header: [num_items(uint32)] + [offsets 0..num_blocks(uint32)]
48
+ header_num_items = np.array([num_blocks], dtype=np.uint32)
49
+ offsets = np.arange(num_blocks + 1, dtype=np.uint32) * (BLOCK_SIZE * dtype_size)
50
+ header = np.concatenate([header_num_items, offsets])
51
+
52
+ with open(filepath, "wb") as f:
53
+ header.tofile(f)
54
+ chunk_data.tofile(f)
55
+
56
+ data_bytes = int(chunk_data.nbytes)
57
+ header_bytes = int(header.nbytes)
58
+ return {
59
+ "chunk_bytes": data_bytes + header_bytes,
60
+ "chunk_size": num_blocks,
61
+ "dim": int(len(chunk_data)),
62
+ "filename": filename,
63
+ }, header_bytes, data_bytes
64
+
65
+
66
+ def _load_resume_state(output_dir):
67
+ """Check for existing chunks to support resuming."""
68
+ index_path = os.path.join(output_dir, "index.json.partial")
69
+ if not os.path.exists(index_path):
70
+ return [], 0, 0
71
+ with open(index_path) as f:
72
+ state = json.load(f)
73
+ chunks = state.get("chunks", [])
74
+ tokens_written = sum(c["dim"] for c in chunks)
75
+ return chunks, len(chunks), tokens_written
76
+
77
+
78
+ def _save_resume_state(output_dir, chunks_metadata):
79
+ """Save partial progress for resume."""
80
+ path = os.path.join(output_dir, "index.json.partial")
81
+ with open(path, "w") as f:
82
+ json.dump({"chunks": chunks_metadata}, f)
83
+
84
+
85
+ def prepare_litdata(filtered_dir: str, output_dir: str, tokenizer_path: str, label: str):
86
+ """Tokenize filtered text and write LitData chunks MAX THROUGHPUT version.
87
+
88
+ Uses:
89
+ - HuggingFace tokenizers encode_batch() for parallel Rust-level tokenization
90
+ - Bulk parquet reads (entire files into RAM)
91
+ - 512 MB write buffer to minimize I/O syscalls
92
+ - All CPU cores via tokenizers' internal parallelism
93
+ """
94
+ import multiprocessing
95
+ print(f"\n{'='*60}")
96
+ print(f"Preparing LitData [{label}] ** HIGH-THROUGHPUT MODE **")
97
+ print(f"Input: {filtered_dir}")
98
+ print(f"Output: {output_dir}")
99
+ print(f"Tokenizer: {tokenizer_path}")
100
+ print(f"CPU cores: {multiprocessing.cpu_count()}")
101
+ print(f"{'='*60}\n")
102
+
103
+ if not Path(filtered_dir).exists():
104
+ print(f"ERROR: Filtered directory not found: {filtered_dir}")
105
+ print("Run filter_datasets.py first!")
106
+ return
107
+
108
+ os.makedirs(output_dir, exist_ok=True)
109
+
110
+ # --- Load the raw HF fast tokenizer for encode_batch() ---
111
+ hf_tok = None
112
+ tok_dir = Path(tokenizer_path)
113
+ for candidate in ["tokenizer.json", "tokenizer.model"]:
114
+ cp = tok_dir / candidate
115
+ if cp.exists() and candidate == "tokenizer.json":
116
+ from tokenizers import Tokenizer as HFTokenizer
117
+ hf_tok = HFTokenizer.from_file(str(cp))
118
+ print(f" Loaded HF fast tokenizer from {cp}")
119
+ break
120
+ # Fallback to litgpt wrapper (slower, single-threaded)
121
+ if hf_tok is None:
122
+ print(" WARNING: No tokenizer.json found, falling back to litgpt Tokenizer (slower)")
123
+ litgpt_tok = Tokenizer(tok_dir)
124
+ eos_id = litgpt_tok.eos_id
125
+
126
+ # --- Chunk math ---
127
+ dtype_size = DTYPE().itemsize
128
+ tokens_per_chunk = CHUNK_BYTES_TARGET // dtype_size
129
+ tokens_per_chunk = (tokens_per_chunk // BLOCK_SIZE) * BLOCK_SIZE
130
+
131
+ # --- Resume support ---
132
+ chunks_metadata, chunk_idx, tokens_to_skip = _load_resume_state(output_dir)
133
+ if tokens_to_skip > 0:
134
+ print(f"RESUMING: Found {chunk_idx} existing chunks ({tokens_to_skip:,} tokens)")
135
+ print(f" Will skip {tokens_to_skip:,} tokens then continue writing chunks\n")
136
+
137
+ # --- Large write buffer (512 MB worth of tokens) ---
138
+ BIG_BUF_TOKENS = (512 * 1024 * 1024) // dtype_size
139
+ BIG_BUF_TOKENS = (BIG_BUF_TOKENS // BLOCK_SIZE) * BLOCK_SIZE
140
+ token_buf = np.empty(BIG_BUF_TOKENS + BLOCK_SIZE * 1024, dtype=DTYPE)
141
+ buf_pos = 0
142
+ total_tokens_written = sum(c["dim"] for c in chunks_metadata)
143
+ total_texts = 0
144
+ skipped_tokens = 0
145
+
146
+ # Batch size for encode_batch — large batches saturate all cores
147
+ ENCODE_BATCH = 32_768
148
+
149
+ parquet_files = sorted(Path(filtered_dir).glob("*.parquet"))
150
+ t0 = time.time()
151
+
152
+ def flush_chunks():
153
+ """Flush all complete chunks from the buffer."""
154
+ nonlocal buf_pos, chunk_idx, total_tokens_written
155
+ flushed = 0
156
+ while buf_pos >= tokens_per_chunk:
157
+ num_blocks = tokens_per_chunk // BLOCK_SIZE
158
+ chunk_array = token_buf[:tokens_per_chunk].copy()
159
+ meta, hdr_b, data_b = _write_chunk(output_dir, chunk_idx, chunk_array, num_blocks)
160
+ chunks_metadata.append(meta)
161
+ total_tokens_written += meta["dim"]
162
+ flushed += 1
163
+
164
+ if flushed % 4 == 0 or buf_pos < tokens_per_chunk * 2:
165
+ _save_resume_state(output_dir, chunks_metadata)
166
+ el = time.time() - t0
167
+ rate = total_tokens_written / max(el, 1)
168
+ print(f" chunk-0-{chunk_idx}.bin | "
169
+ f"total: {total_tokens_written:,} tokens | "
170
+ f"{rate:,.0f} tok/s | {el:.0f}s")
171
+ chunk_idx += 1
172
+
173
+ leftover = buf_pos - tokens_per_chunk
174
+ if leftover > 0:
175
+ token_buf[:leftover] = token_buf[tokens_per_chunk:tokens_per_chunk + leftover]
176
+ buf_pos = leftover
177
+ return flushed
178
+
179
+ def encode_and_append(texts):
180
+ """Encode a batch of texts and append to buffer, handling EOS."""
181
+ nonlocal buf_pos, total_texts, skipped_tokens
182
+ if hf_tok is not None:
183
+ # Rust-parallel batch encode uses ALL CPU cores
184
+ encoded = hf_tok.encode_batch(texts, add_special_tokens=False)
185
+ for enc in encoded:
186
+ ids = enc.ids
187
+ ids.append(eos_id) # append EOS
188
+ n = len(ids)
189
+
190
+ if skipped_tokens < tokens_to_skip:
191
+ remaining = tokens_to_skip - skipped_tokens
192
+ if n <= remaining:
193
+ skipped_tokens += n
194
+ total_texts += 1
195
+ continue
196
+ ids = ids[int(remaining):]
197
+ skipped_tokens = tokens_to_skip
198
+ n = len(ids)
199
+
200
+ # Ensure buffer capacity
201
+ if buf_pos + n > len(token_buf):
202
+ flush_chunks()
203
+ if buf_pos + n > len(token_buf):
204
+ # Extremely long doc — extend buffer
205
+ extra = np.empty(n + BLOCK_SIZE * 256, dtype=DTYPE)
206
+ old = token_buf[:buf_pos].copy()
207
+ new_buf = np.empty(buf_pos + n + BLOCK_SIZE * 256, dtype=DTYPE)
208
+ new_buf[:buf_pos] = old
209
+ # Keep reference to token_buf so nonlocal works
210
+ # Actually we need to reassign
211
+ pass
212
+
213
+ arr = np.array(ids, dtype=DTYPE)
214
+ token_buf[buf_pos:buf_pos + n] = arr
215
+ buf_pos += n
216
+ total_texts += 1
217
+ else:
218
+ # Fallback: single-threaded litgpt tokenizer
219
+ for text in texts:
220
+ tokens = litgpt_tok.encode(text, bos=False, eos=True)
221
+ tok_array = tokens.numpy().astype(DTYPE) if hasattr(tokens, 'numpy') else np.array(tokens.tolist(), dtype=DTYPE)
222
+ n = len(tok_array)
223
+
224
+ if skipped_tokens < tokens_to_skip:
225
+ remaining = tokens_to_skip - skipped_tokens
226
+ if n <= remaining:
227
+ skipped_tokens += n
228
+ total_texts += 1
229
+ continue
230
+ tok_array = tok_array[int(remaining):]
231
+ skipped_tokens = tokens_to_skip
232
+ n = len(tok_array)
233
+
234
+ if buf_pos + n > len(token_buf):
235
+ flush_chunks()
236
+
237
+ token_buf[buf_pos:buf_pos + n] = tok_array
238
+ buf_pos += n
239
+ total_texts += 1
240
+
241
+ print(f"Tokenizing (batch_size={ENCODE_BATCH:,}, buffer={BIG_BUF_TOKENS*dtype_size/1e6:.0f} MB)...")
242
+ for pf_idx, pf in enumerate(parquet_files):
243
+ # Read entire parquet into RAM (fast, data is ~10MB/file)
244
+ table = pq.read_table(str(pf), columns=["text"])
245
+ all_texts = table.column("text").to_pylist()
246
+ del table # free arrow memory
247
+
248
+ # Process in large batches
249
+ for i in range(0, len(all_texts), ENCODE_BATCH):
250
+ batch = all_texts[i:i + ENCODE_BATCH]
251
+ encode_and_append(batch)
252
+ flush_chunks()
253
+
254
+ del all_texts
255
+ elapsed = time.time() - t0
256
+ rate = total_tokens_written / max(elapsed, 1)
257
+ print(f" [{pf.name}] file {pf_idx+1}/{len(parquet_files)} | "
258
+ f"texts: {total_texts:,} | tokens: {total_tokens_written:,} | "
259
+ f"{rate:,.0f} tok/s | {elapsed:.0f}s")
260
+
261
+ # Flush remaining buffer
262
+ flush_chunks()
263
+ remaining_blocks = buf_pos // BLOCK_SIZE
264
+ if remaining_blocks > 0:
265
+ final_tokens = remaining_blocks * BLOCK_SIZE
266
+ chunk_array = token_buf[:final_tokens].copy()
267
+ meta, hdr_b, data_b = _write_chunk(output_dir, chunk_idx, chunk_array, remaining_blocks)
268
+ chunks_metadata.append(meta)
269
+ total_tokens_written += meta["dim"]
270
+ print(f" chunk-0-{chunk_idx}.bin (final): {remaining_blocks} blocks, "
271
+ f"{meta['dim']:,} tokens")
272
+ chunk_idx += 1
273
+
274
+ # Write final index.json
275
+ index = {
276
+ "chunks": chunks_metadata,
277
+ "config": {
278
+ "chunk_bytes": CHUNK_BYTES_TARGET,
279
+ "chunk_size": None,
280
+ "compression": None,
281
+ "data_format": [f"no_header_tensor:{DTYPE_INDEX}"],
282
+ "data_spec": None,
283
+ "encryption": None,
284
+ "item_loader": "TokensLoader",
285
+ },
286
+ "updated_at": str(time.time()),
287
+ }
288
+ with open(os.path.join(output_dir, "index.json"), "w") as f:
289
+ json.dump(index, f, indent=2)
290
+
291
+ # Clean up partial state
292
+ partial_path = os.path.join(output_dir, "index.json.partial")
293
+ if os.path.exists(partial_path):
294
+ os.remove(partial_path)
295
+
296
+ elapsed = time.time() - t0
297
+ print(f"\n--- LitData preparation [{label}] complete ---")
298
+ print(f"Chunks: {chunk_idx}")
299
+ print(f"Total tokens: {total_tokens_written:,}")
300
+ print(f"Total blocks: {total_tokens_written // BLOCK_SIZE:,}")
301
+ print(f"Texts processed: {total_texts:,}")
302
+ print(f"Time: {elapsed:.0f}s ({total_tokens_written/max(elapsed,1):,.0f} tok/s)")
303
+ print(f"Output: {output_dir}")
304
+
305
+
306
+ def main():
307
+ parser = argparse.ArgumentParser(description="Prepare LitData from filtered parquet")
308
+ parser.add_argument(
309
+ "--mode",
310
+ choices=["small", "full", "both", "english"],
311
+ default="small",
312
+ help="Which dataset to prepare (english = ultra-clean English corpus)",
313
+ )
314
+ parser.add_argument(
315
+ "--tokenizer_path",
316
+ type=str,
317
+ default="Base/checkpoints/EleutherAI/pythia-160m",
318
+ help="Path to tokenizer directory",
319
+ )
320
+ parser.add_argument(
321
+ "--filtered_dir",
322
+ type=str,
323
+ default=None,
324
+ help="Custom filtered parquet directory to tokenize",
325
+ )
326
+ parser.add_argument(
327
+ "--output_dir",
328
+ type=str,
329
+ default=None,
330
+ help="Custom LitData output directory",
331
+ )
332
+ parser.add_argument(
333
+ "--label",
334
+ type=str,
335
+ default="CUSTOM",
336
+ help="Label shown in logs for custom directory mode",
337
+ )
338
+ args = parser.parse_args()
339
+
340
+ if args.filtered_dir or args.output_dir:
341
+ if not args.filtered_dir or not args.output_dir:
342
+ raise ValueError("Both --filtered_dir and --output_dir must be provided together")
343
+ prepare_litdata(
344
+ filtered_dir=args.filtered_dir,
345
+ output_dir=args.output_dir,
346
+ tokenizer_path=args.tokenizer_path,
347
+ label=args.label,
348
+ )
349
+ return
350
+
351
+ if args.mode in ("small", "both"):
352
+ prepare_litdata(
353
+ filtered_dir="Base/data/filtered_10m",
354
+ output_dir="Base/data/litdata_10m",
355
+ tokenizer_path=args.tokenizer_path,
356
+ label="SMALL (10M)",
357
+ )
358
+
359
+ if args.mode in ("full", "both"):
360
+ prepare_litdata(
361
+ filtered_dir="Base/data/filtered_3b",
362
+ output_dir="Base/data/litdata_3b",
363
+ tokenizer_path=args.tokenizer_path,
364
+ label="FULL (3B)",
365
+ )
366
+
367
+ if args.mode == "english":
368
+ prepare_litdata(
369
+ filtered_dir="Base/data/filtered_english",
370
+ output_dir="Base/data/litdata_english",
371
+ tokenizer_path=args.tokenizer_path,
372
+ label="ENGLISH (ultra-clean)",
373
+ )
374
+
375
+ print("\nDone! Next step: run litgpt pretrain with the appropriate config.")
376
+
377
+
378
+ if __name__ == "__main__":
379
+ main()