from __future__ import annotations import random import time from decon import is_contaminated STAGE_BOUNDARIES = [0.7, 0.9, 1.0] STAGE_MIX = [{'web': 0.74, 'math': 0.09, 'code': 0.07, 'arithmetic': 0.05, 'dialogue': 0.05}, {'web': 0.59, 'math': 0.18, 'code': 0.12, 'arithmetic': 0.06, 'dialogue': 0.05}, {'web': 0.5, 'math': 0.24, 'code': 0.11, 'arithmetic': 0.09, 'dialogue': 0.06}] WEB_MIX = {'fineweb_edu': 0.6, 'dclm': 0.4} MIX_PRESETS = {'smollm2': {'fineweb_edu': 0.576, 'dclm': 0.384, 'cosmopedia': 0.04}, 'dclm_heavy': {'fineweb_edu': 0.384, 'dclm': 0.576, 'cosmopedia': 0.04}, 'smollm2_math': {'fineweb_edu': 0.55, 'dclm': 0.37, 'cosmopedia': 0.04, 'math': 0.04}, 'dclm_heavy_math': {'fineweb_edu': 0.37, 'dclm': 0.55, 'cosmopedia': 0.04, 'math': 0.04}, 'cooldown': {'fineweb_edu': 0.75, 'cosmopedia': 0.25}} MUSEKO_MIX = {'web': 0.9, 'arithmetic': 0.1} EOS = '<|endoftext|>' def get_stage_mix(progress: float, mix_mode: str='default', arith_frac: float | None=None) -> dict: if mix_mode == 'museko': mix = dict(MUSEKO_MIX) else: mix = None for boundary, stage in zip(STAGE_BOUNDARIES, STAGE_MIX): if progress <= boundary: mix = dict(stage) break if mix is None: mix = dict(STAGE_MIX[-1]) if arith_frac is None: return mix other = {k: v for k, v in mix.items() if k != 'arithmetic'} total_other = sum(other.values()) or 1.0 scale = (1.0 - arith_frac) / total_other out = {k: v * scale for k, v in other.items()} out['arithmetic'] = arith_frac return out def _cycling_stream(load_fn, name: str): backoff = 5.0 while True: try: it = iter(load_fn()) backoff = 5.0 except Exception as e: msg = str(e)[:140] print(f'[data] stream {name} failed to start, retry in {backoff:.0f}s: {msg}', flush=True) time.sleep(backoff) backoff = min(backoff * 2, 300.0) continue while True: try: row = next(it) except StopIteration: break except Exception as e: print(f'[data] stream {name} row error, skipping: {e}', flush=True) continue yield row def _split(ds, rank: int, world_size: int, name: str=''): if world_size <= 1: return ds n = None try: n = ds.n_shards except Exception: pass if n is not None and n >= world_size: return ds.shard(num_shards=world_size, index=rank) print(f'[data] {name}: {n} files < {world_size} slices, striping rows instead', flush=True) return _RowStripe(ds, rank, world_size) class _RowStripe: def __init__(self, ds, rank, world_size): self.ds, self.rank, self.world_size = (ds, rank, world_size) def __iter__(self): for i, row in enumerate(self.ds): if i % self.world_size == self.rank: yield row def _shard(ds, rank: int, world_size: int): if world_size > 1: return ds.shard(num_shards=world_size, index=rank) return ds def _rank_filter(gen, rank: int, world_size: int): if world_size <= 1: yield from gen return for i, item in enumerate(gen): if i % world_size == rank: yield item def _fineweb_edu_texts(rank: int=0, world_size: int=1): from datasets import load_dataset def _load(): ds = load_dataset('HuggingFaceFW/fineweb-edu', 'sample-100BT', split='train', streaming=True) return _split(ds, rank, world_size, 'fineweb-edu') for row in _cycling_stream(_load, 'fineweb-edu'): yield row['text'] def _dclm_texts(rank: int=0, world_size: int=1): from datasets import load_dataset def _load(): ds = load_dataset('mlfoundations/dclm-baseline-1.0', split='train', streaming=True) return _split(ds, rank, world_size, 'dclm') for row in _cycling_stream(_load, 'dclm'): yield row['text'] def _cosmopedia_texts(rank: int=0, world_size: int=1): from datasets import load_dataset def _load(): ds = load_dataset('HuggingFaceTB/smollm-corpus', 'cosmopedia-v2', split='train', streaming=True) return _split(ds, rank, world_size, 'cosmopedia-v2') for row in _cycling_stream(_load, 'cosmopedia-v2'): t = row.get('text') or '' if t: yield t def _finemath_texts(rank: int=0, world_size: int=1): from datasets import load_dataset def _load(): return load_dataset('HuggingFaceTB/finemath', 'finemath-4plus', split='train', streaming=True) gen = (row['text'] for row in _cycling_stream(_load, 'finemath')) yield from _rank_filter(gen, rank, world_size) def _code_texts(rank: int=0, world_size: int=1): from datasets import load_dataset def _load(): return load_dataset('semeru/Code-Text-Python', split='train', streaming=True) def _texts(): for row in _cycling_stream(_load, 'code-text-python'): docstring = row.get('docstring') or '' code = row.get('code') or row.get('original_string') or '' if not code: continue yield (f'"""{docstring}"""\n{code}' if docstring else code) yield from _rank_filter(_texts(), rank, world_size) def _smoltalk_texts(rank: int=0, world_size: int=1): from datasets import load_dataset def _load(): return load_dataset('HuggingFaceTB/smol-smoltalk', split='train', streaming=True) def _texts(): for row in _cycling_stream(_load, 'smol-smoltalk'): parts = [] for msg in row['messages']: role = msg.get('role', 'user') parts.append(f"<|{role}|>\n{msg.get('content', '')}") yield '\n'.join(parts) yield from _rank_filter(_texts(), rank, world_size) def _realmath_texts(rank: int=0, world_size: int=1): import re as _re from datasets import load_dataset def _orca(): return load_dataset('microsoft/orca-math-word-problems-200k', split='train', streaming=True) def _gsm8k(): return load_dataset('openai/gsm8k', 'main', split='train', streaming=True) calc = _re.compile('<<[^>]*>>') def _texts(): orca = _cycling_stream(_orca, 'orca-math') gsm = _cycling_stream(_gsm8k, 'gsm8k') while True: row = next(orca) if random.random() < 0.85 else next(gsm) q = (row.get('question') or '').strip() a = calc.sub('', row.get('answer') or '').strip() if not q or not a: continue yield f'{q}\n{a}' yield from _rank_filter(_texts(), rank, world_size) def _arithmetic_texts(rank: int=0, world_size: int=1, source: str='wordmath'): import random as _random rng = _random.Random(1000 + rank) if source == 'drill': from gen_arithmetic import gen_example as _gen while True: yield _gen() from gen_wordmath import gen_example as _gen if source == 'wordmath': while True: yield _gen(rng) real = _realmath_texts(rank, world_size) while True: yield (next(real) if rng.random() < 0.75 else _gen(rng)) def build_web_stream(reference_ngrams, decon_enabled=True, rank=0, world_size=1): fw = _fineweb_edu_texts(rank, world_size) dclm = _dclm_texts(rank, world_size) while True: src = fw if random.random() < WEB_MIX['fineweb_edu'] else dclm text = next(src) if decon_enabled and is_contaminated(text, reference_ngrams): continue yield text def build_preset_streams(preset: str, reference_ngrams, decon_enabled=True, rank=0, world_size=1): def _filtered(gen): for text in gen: if decon_enabled and is_contaminated(text, reference_ngrams): continue yield text mix = MIX_PRESETS[preset] builders = {'fineweb_edu': _fineweb_edu_texts, 'dclm': _dclm_texts, 'cosmopedia': _cosmopedia_texts, 'math': _finemath_texts} return {k: _filtered(builders[k](rank, world_size)) for k in mix} def build_source_streams(reference_ngrams, decon_enabled=True, rank=0, world_size=1, mix_mode='default', arith_source='wordmath'): def _filtered(gen, apply_decon): for text in gen: if apply_decon and decon_enabled and is_contaminated(text, reference_ngrams): continue yield text streams = {'web': build_web_stream(reference_ngrams, decon_enabled, rank, world_size), 'arithmetic': _filtered(_arithmetic_texts(rank, world_size, arith_source), True)} if mix_mode != 'museko': streams['math'] = _filtered(_finemath_texts(rank, world_size), True) streams['code'] = _filtered(_code_texts(rank, world_size), True) streams['dialogue'] = _filtered(_smoltalk_texts(rank, world_size), True) return streams class MixedTokenStream: def __init__(self, tokenizer, streams: dict, seq_len: int, seed: int=0, mix_mode: str='default', arith_frac: float | None=None, preset: str | None=None): self.preset = preset self.tokenizer = tokenizer self.streams = streams self.seq_len = seq_len self.eos_id = tokenizer.token_to_id(EOS) assert self.eos_id is not None, 'tokenizer is missing <|endoftext|>' self.buffer: list[int] = [] self.rng = random.Random(seed) self.progress = 0.0 self.mix_mode = mix_mode self.arith_frac = arith_frac def _pull_one_doc_tokens(self) -> list[int]: if self.preset is not None: mix = MIX_PRESETS[self.preset] else: mix = get_stage_mix(self.progress, self.mix_mode, self.arith_frac) names = list(mix.keys()) weights = list(mix.values()) source_name = self.rng.choices(names, weights=weights, k=1)[0] text = next(self.streams[source_name]) ids = self.tokenizer.encode(text).ids return ids + [self.eos_id] def next_sequence(self): while len(self.buffer) < self.seq_len + 1: self.buffer.extend(self._pull_one_doc_tokens()) seq = self.buffer[:self.seq_len + 1] self.buffer = self.buffer[self.seq_len:] return seq def next_batch(self, batch_size: int): return [self.next_sequence() for _ in range(batch_size)] if __name__ == '__main__': import time from tokenizers import Tokenizer tok = Tokenizer.from_file('artifacts/tokenizer.json') print('eos id:', tok.token_to_id(EOS), flush=True) streams = build_source_streams(reference_ngrams=set(), decon_enabled=False) mixer = MixedTokenStream(tok, streams, seq_len=64) t0 = time.time() for i in range(5): seq = mixer.next_sequence() print(f'seq {i}: len={len(seq)} sample={seq[:10]}', flush=True) print(f'5 sequences in {time.time() - t0:.2f}s', flush=True)