-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdata.py
More file actions
326 lines (281 loc) · 12.2 KB
/
Copy pathdata.py
File metadata and controls
326 lines (281 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""
data.py — Mixed DCLM-edu + FinePhrase streaming data pipeline
=============================================================
Streams from HuggingFace, mixes datasets by ratio, tokenizes, and safely
shards for DDP.
Tokenizer / document-separator handling
---------------------------------------
The dataset supports multiple tokenizers and separator placements via flags
on `MixedStreamDataset` / `get_dataloader` / `get_eval_batches`:
tokenizer:
"cl100k" → tiktoken cl100k_base, vocab 100277. Default; preserves
the original behaviour of this file. Used by moe_train.py
for from-scratch training.
"olmoe" → AutoTokenizer.from_pretrained("allenai/OLMoE-1B-7B-0125"),
vocab 50304. Used by surgery_moe_train.py for continued
pretraining of OLMoE.
any HF id → AutoTokenizer.from_pretrained(<that id>). The default
separator id will be the tokenizer's `eos_token_id`.
insert_separator:
"before" → emit ONE separator token BEFORE each document. This is the
original behaviour of this file (matches moe_train.py runs).
"after" → emit ONE separator token AFTER each document. This matches
the pretraining recipe of OLMoE (`dolma tokens
--tokenizer.eos_token_id 50279`).
"none" → no separator between documents (rarely useful).
separator_id:
Optional explicit override of the separator token id. Defaults are:
cl100k → 100257 (<|endoftext|>)
olmoe → 50279 (<|endoftext|>, == tok.eos_token_id)
other → tokenizer.eos_token_id
Important: "before" and "after" placements are functionally equivalent on a
continuous concatenated stream — both put exactly one separator between every
pair of consecutive documents. The difference is only at the very first/last
document of the stream. What matters for matching pretraining is the choice of
*token id*, not the side of the doc it sits on.
"""
import torch
from torch.utils.data import IterableDataset, DataLoader
import torch.distributed as dist
import tiktoken
from datasets import load_dataset, interleave_datasets
# Built-in tokenizer presets. Keys map to (loader_kind, default_separator_id).
# loader_kind is "tiktoken:<name>" for tiktoken or "hf:<model_id>" for HF tokenizers.
_TOKENIZER_PRESETS = {
"cl100k": ("tiktoken:cl100k_base", 100257), # cl100k <|endoftext|>
"olmoe": ("hf:allenai/OLMoE-1B-7B-0125", 50279), # OLMoE <|endoftext|>
}
def _resolve_tokenizer(tokenizer_spec, separator_id_override):
"""Return (encoder_kind, encoder_obj, separator_id, vocab_size, name).
`encoder_kind` is one of "tiktoken" or "hf". `encoder_obj` is the actual
tokenizer (a tiktoken Encoding or an HF PreTrainedTokenizerBase), both of
which are picklable so DataLoader workers can be spawned safely on any
start method (fork, spawn, forkserver).
"""
if tokenizer_spec in _TOKENIZER_PRESETS:
loader_kind, default_sep_id = _TOKENIZER_PRESETS[tokenizer_spec]
name = tokenizer_spec
else:
# Treat as a generic HF model id.
loader_kind = f"hf:{tokenizer_spec}"
default_sep_id = None # filled in below from tokenizer.eos_token_id
name = tokenizer_spec
if loader_kind.startswith("tiktoken:"):
enc_name = loader_kind.split(":", 1)[1]
enc = tiktoken.get_encoding(enc_name)
encoder_kind = "tiktoken"
encoder_obj = enc
vocab_size = enc.n_vocab
elif loader_kind.startswith("hf:"):
from transformers import AutoTokenizer
hf_id = loader_kind.split(":", 1)[1]
tok = AutoTokenizer.from_pretrained(hf_id)
encoder_kind = "hf"
encoder_obj = tok
vocab_size = tok.vocab_size
if default_sep_id is None:
if tok.eos_token_id is None:
raise ValueError(
f"Tokenizer '{hf_id}' has no eos_token_id configured. "
f"Pass an explicit separator_id=<int> to get_dataloader / "
f"get_eval_batches."
)
default_sep_id = tok.eos_token_id
else:
raise ValueError(f"Unknown tokenizer loader: {loader_kind}")
sep_id = separator_id_override if separator_id_override is not None else default_sep_id
return encoder_kind, encoder_obj, sep_id, vocab_size, name
class MixedStreamDataset(IterableDataset):
"""
Streams DCLM-edu and FinePhrase, mixes them by ratio, tokenizes,
and packs into fixed-length sequences.
See module docstring for tokenizer / insert_separator / separator_id semantics.
"""
# Backwards-compat class attribute. The default separator id for the
# original (cl100k) tokenizer is the cl100k <|endoftext|> token id.
# External code that referenced `MixedStreamDataset.BOS_TOKEN` continues
# to work. New code should use `dataset.separator_id` instead.
BOS_TOKEN = 100257
def __init__(
self,
seq_len=512,
split="train",
max_tokens=None,
do_shard=True,
tokenizer="cl100k",
insert_separator="before",
separator_id=None,
):
super().__init__()
self.seq_len = seq_len
self.split = split
self.max_tokens = max_tokens
self.do_shard = do_shard
if insert_separator not in ("before", "after", "none"):
raise ValueError(
f"insert_separator must be 'before', 'after', or 'none' (got {insert_separator!r})"
)
self.insert_separator = insert_separator
# Resolve the tokenizer and separator id once so workers don't pay the
# AutoTokenizer download cost per __iter__ call. The stored
# _encoder_obj is a tiktoken Encoding or HF PreTrainedTokenizerBase —
# both are picklable across DataLoader worker start methods.
encoder_kind, encoder_obj, sep_id, vocab_size, tok_name = _resolve_tokenizer(
tokenizer, separator_id
)
self._encoder_kind = encoder_kind
self._encoder_obj = encoder_obj
self.separator_id = sep_id
self.vocab_size = vocab_size
self.tokenizer_name = tok_name
# Keep self.enc for full backwards-compat with anything that reached
# in and used the old attribute name (cl100k path only; for HF it's
# the AutoTokenizer object).
self.enc = encoder_obj
# Sanity: separator id must fit in the tokenizer's vocab.
if not (0 <= self.separator_id < self.vocab_size):
raise ValueError(
f"separator_id={self.separator_id} is out of range for tokenizer "
f"'{tok_name}' with vocab_size={self.vocab_size}"
)
def _encode(self, text):
"""Encode one document to a list of int token ids, no special tokens.
Method form (not a stored lambda) so the dataset is picklable across
DataLoader worker start methods (fork, spawn, forkserver alike).
"""
if self._encoder_kind == "tiktoken":
return self._encoder_obj.encode_ordinary(text)
else:
return self._encoder_obj.encode(text, add_special_tokens=False)
def _token_generator(self):
# 1. Load the raw streams
ds_dclm = load_dataset("HuggingFaceTB/dclm-edu", split=self.split, streaming=True)
ds_fp = load_dataset("HuggingFaceFW/finephrase", "all", split=self.split, streaming=True)
# 2. Add Shuffle Buffers (Crucial for mixed streaming)
# This keeps 10,000 documents in RAM per dataset and yields randomly from them.
ds_dclm = ds_dclm.shuffle(buffer_size=10_000, seed=42)
ds_fp = ds_fp.shuffle(buffer_size=10_000, seed=42)
# 3. Mix the datasets
# Update these probabilities to match the true original size ratio you want.
# Example: 75% DCLM-edu, 25% FinePhrase.
ds_mixed = interleave_datasets(
[ds_dclm, ds_fp],
probabilities=[0.75, 0.25],
seed=42
)
# 4. Perfect sharding for DDP and multi-worker DataLoaders
# NOTE: Sharding must happen AFTER interleaving to prevent DDP hangs.
if self.do_shard:
worker_info = torch.utils.data.get_worker_info()
world_size = dist.get_world_size() if dist.is_initialized() else 1
global_rank = dist.get_rank() if dist.is_initialized() else 0
worker_id = worker_info.id if worker_info else 0
num_workers = worker_info.num_workers if worker_info else 1
total_shards = world_size * num_workers
shard_idx = global_rank * num_workers + worker_id
if total_shards > 1:
ds_mixed = ds_mixed.shard(num_shards=total_shards, index=shard_idx)
# 5. Token Yielding Loop
# The insert_separator flag controls whether the separator token is emitted
# before, after, or not at all relative to each document's tokens.
sep_id = self.separator_id
emit_before = self.insert_separator == "before"
emit_after = self.insert_separator == "after"
total_tokens = 0
for example in ds_mixed:
text = example.get("text", "")
if not text or len(text) < 50:
continue
if emit_before:
yield sep_id
total_tokens += 1
if self.max_tokens and total_tokens >= self.max_tokens:
return
tokens = self._encode(text)
for t in tokens:
yield t
total_tokens += 1
if self.max_tokens and total_tokens >= self.max_tokens:
return
if emit_after:
yield sep_id
total_tokens += 1
if self.max_tokens and total_tokens >= self.max_tokens:
return
def __iter__(self):
"""Yield (input_ids, targets) of shape (seq_len,)."""
buffer = []
for token in self._token_generator():
buffer.append(token)
if len(buffer) == self.seq_len + 1:
t = torch.tensor(buffer, dtype=torch.long)
yield t[:-1], t[1:]
buffer = []
def get_dataloader(
seq_len=512,
batch_size=32,
num_workers=4,
max_tokens=None,
tokenizer="cl100k",
insert_separator="before",
separator_id=None,
):
"""Create a streaming DataLoader for the Mixed Dataset.
See module docstring for tokenizer / insert_separator / separator_id semantics.
Defaults preserve the original behaviour (cl100k + leading <|endoftext|>).
"""
dataset = MixedStreamDataset(
seq_len=seq_len,
max_tokens=max_tokens,
do_shard=True,
tokenizer=tokenizer,
insert_separator=insert_separator,
separator_id=separator_id,
)
return DataLoader(
dataset,
batch_size=batch_size,
num_workers=num_workers,
pin_memory=True,
drop_last=True,
)
def get_eval_batches(
seq_len=512,
batch_size=32,
n_batches=20,
tokenizer="cl100k",
insert_separator="before",
separator_id=None,
):
"""
Pre-fetch a fixed set of eval batches from the stream.
do_shard=False ensures every GPU evaluates on the exact same holdout set.
See module docstring for tokenizer / insert_separator / separator_id semantics.
The same values must be passed here as in get_dataloader so that training
and evaluation see the same document-boundary distribution.
"""
dataset = MixedStreamDataset(
seq_len=seq_len,
max_tokens=10_000_000 + n_batches * batch_size * (seq_len + 1),
do_shard=False,
tokenizer=tokenizer,
insert_separator=insert_separator,
separator_id=separator_id,
)
batches = []
buffer = []
skip_count = 0
skip_target = 10_000_000
for inp, tgt in dataset:
skip_count += seq_len
if skip_count < skip_target:
continue
buffer.append((inp, tgt))
if len(buffer) == batch_size:
input_ids = torch.stack([b[0] for b in buffer])
targets = torch.stack([b[1] for b in buffer])
batches.append((input_ids, targets))
buffer = []
if len(batches) >= n_batches:
break
return batches