from __future__ import annotations import argparse import json import os import random from collections import Counter from pathlib import Path import torch from tokenizers import Tokenizer from torch.utils.data import Dataset, IterableDataset, get_worker_info T5_TOKENIZER = "/e2e-data/evad-tech-vla/wanghan58/models/hf/t5-small/tokenizer.json" TEXT_KEYS = ("text", "content", "document", "raw_text") def load_tokenizer(path: str = T5_TOKENIZER): tok = Tokenizer.from_file(path) bos = tok.token_to_id("") eos = tok.token_to_id("") pad = tok.token_to_id("") return tok, int(bos), int(eos), int(pad) def token_name(tok: Tokenizer, idx: int) -> str: return tok.id_to_token(int(idx)) or f"" class CachedTokenDataset(Dataset): def __init__(self, cache_path: str, tokenizer_path: str = T5_TOKENIZER) -> None: self.tokenizer, self.bos_id, self.eos_id, self.pad_id = load_tokenizer(tokenizer_path) cache = torch.load(cache_path, map_location="cpu") self.ids = cache["ids"].to(torch.int32) self.seen_count = int(cache.get("seen_count", 0)) self.skipped_count = int(cache.get("skipped_count", 0)) self.bos_id = int(cache.get("bos_id", self.bos_id)) self.eos_id = int(cache.get("eos_id", self.eos_id)) self.pad_id = int(cache.get("pad_id", self.pad_id)) def __len__(self) -> int: return self.ids.size(0) def __getitem__(self, idx: int) -> torch.Tensor: return self.ids[idx].long() def data_files(path: str) -> list[Path]: p = Path(path) if p.is_file(): return [p] return sorted(x for x in p.rglob("*") if x.suffix.lower() in {".txt", ".jsonl", ".json", ".parquet"}) def iter_file_text(f: Path, text_column: str): if f.suffix == ".txt": for line in f.read_text(encoding="utf-8", errors="ignore").splitlines(): if line.strip(): yield line.strip() elif f.suffix in {".jsonl", ".json"}: for line in f.read_text(encoding="utf-8", errors="ignore").splitlines(): if line.strip(): obj = json.loads(line) yield obj.get(text_column) or next(obj[k] for k in TEXT_KEYS if k in obj) elif f.suffix == ".parquet": import pyarrow.parquet as pq pf = pq.ParquetFile(f) names = set(pf.schema.names) col = text_column if text_column in names else next(k for k in TEXT_KEYS if k in names) for batch in pf.iter_batches(columns=[col], batch_size=2048): for item in batch.column(0).to_pylist(): if item: yield item def short(text: str, n: int = 80) -> str: return text.replace("\n", "\\n").replace("\t", "\\t")[:n] def max_run_info(row: list[int], tok: Tokenizer) -> tuple[int, str]: best = cur = 1 best_token = row[0] if row else 0 for a, b in zip(row, row[1:]): if a == b: cur += 1 if cur > best: best = cur best_token = b else: cur = 1 return best, short(tok.decode([best_token], skip_special_tokens=False)) def repetitive_pattern_info(row: list[int], tok: Tokenizer, max_pattern_len: int = 16, max_repeats: int = 3) -> tuple[int, int, str]: n = len(row) for width in range(2, max_pattern_len + 1): i = 0 while i + width * (max_repeats + 1) <= n: pattern = row[i : i + width] repeats = 1 j = i + width while j + width <= n and row[j : j + width] == pattern: repeats += 1 if repeats > max_repeats: return repeats, width, short(tok.decode(pattern, skip_special_tokens=False)) j += width i += 1 return 0, 0, "" def repeated_phrase_ngram_info(row: list[int], n: int, tok: Tokenizer, threshold: int = 16) -> tuple[int, str]: counts = Counter(zip(*(row[i:] for i in range(n)))) for ngram, count in counts.most_common(): if count < threshold: break text = tok.decode(list(ngram), skip_special_tokens=False).strip() if len(text) >= 3 and " " in text and any(ch.isalnum() for ch in text): return count, short(text) return 0, "" def reject_reasons(row: list[int], unk_id: int, tok: Tokenizer) -> list[str]: reasons = [] unique = len(set(row)) if unique <= 48: reasons.append(f"token_unique<=48(unique={unique})") bigram_count, bigram_text = repeated_phrase_ngram_info(row, 2, tok) if bigram_count >= 16: reasons.append(f"bigram_repeat>=16(count={bigram_count},text={bigram_text})") trigram_count, trigram_text = repeated_phrase_ngram_info(row, 3, tok) if trigram_count >= 16: reasons.append(f"trigram_repeat>=16(count={trigram_count},text={trigram_text})") if unk_id in row: reasons.append(f"has_unk(token=,id={unk_id})") run_count, run_token = max_run_info(row, tok) if run_count > 10: reasons.append(f"single_token_run>10(count={run_count},token={run_token})") pattern_repeats, pattern_width, pattern_text = repetitive_pattern_info(row, tok) if pattern_repeats > 3: reasons.append(f"repetitive_pattern>3(repeats={pattern_repeats},width={pattern_width},text={pattern_text})") return reasons def append_reject(path: str, line: str) -> None: import fcntl with open(path, "a", encoding="utf-8", buffering=1) as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) f.write(line + "\n") fcntl.flock(f.fileno(), fcntl.LOCK_UN) class OnlinePackedDataset(IterableDataset): def __init__( self, data_path: str, tokenizer_path: str = T5_TOKENIZER, text_column: str = "text", pack_len: int = 1023, append_eos: bool = True, shuffle_buffer: int = 8192, reject_txt: str = "cache/online_rejected.txt", seed: int = 1234, rank: int = 0, world: int = 1, ) -> None: self.tokenizer_path = tokenizer_path self.tokenizer, self.bos_id, self.eos_id, self.pad_id = load_tokenizer(tokenizer_path) self.unk_id = int(self.tokenizer.token_to_id("")) self.files = data_files(data_path) self.text_column = text_column self.pack_len = pack_len self.append_eos = append_eos self.shuffle_buffer = shuffle_buffer self.reject_txt = reject_txt self.seed = seed self.rank = rank self.world = world self.seen_count = 0 self.skipped_count = 0 Path(reject_txt).parent.mkdir(parents=True, exist_ok=True) if rank == 0: Path(reject_txt).write_text("", encoding="utf-8") def __iter__(self): info = get_worker_info() worker_id = info.id if info else 0 num_workers = info.num_workers if info else 1 rank_files = self.files[self.rank :: self.world] files = rank_files[worker_id::num_workers] if not files: return tok = Tokenizer.from_file(self.tokenizer_path) eos_id = int(tok.token_to_id("")) unk_id = int(tok.token_to_id("")) rng = random.Random(self.seed + 1009 * self.rank + worker_id) stream: list[int] = [] rows: list[torch.Tensor] = [] seen = accepted = rejected = packed = 0 while True: rng.shuffle(files) for file_name in files: for text in iter_file_text(file_name, self.text_column): seen += 1 ids = [int(x) for x in tok.encode(text, add_special_tokens=False).ids] reasons = reject_reasons(ids, unk_id, tok) if reasons: rejected += 1 clean_text = text.replace("\n", "\\n").replace("\t", "\\t") append_reject( self.reject_txt, f"rank={self.rank}\tworker={worker_id}\tdoc={seen}\tntok={len(ids)}\treason={','.join(reasons)}\ttext={clean_text}", ) continue accepted += 1 stream.extend(ids) stream.append(eos_id) while len(stream) >= self.pack_len: payload = stream[: self.pack_len] del stream[: self.pack_len] row = payload + ([eos_id] if self.append_eos else []) rows.append(torch.tensor(row, dtype=torch.long)) packed += 1 if len(rows) >= self.shuffle_buffer: i = rng.randrange(len(rows)) rows[i], rows[-1] = rows[-1], rows[i] yield rows.pop() if seen % 10000 == 0: print( f"[online data rank={self.rank} worker={worker_id}] docs={seen} accepted={accepted} rejected={rejected} rows={packed} buffered={len(stream)}", flush=True, ) while rows: i = rng.randrange(len(rows)) rows[i], rows[-1] = rows[-1], rows[i] yield rows.pop() class ELFMultipartPackedDataset(IterableDataset): """Stream ELF's pre-tokenized OWT HFDS shards as direct fixed-length rows.""" def __init__( self, data_path: str, tokenizer_path: str = T5_TOKENIZER, pack_len: int = 1023, append_eos: bool = True, shuffle_buffer: int = 8192, seed: int = 1234, rank: int = 0, world: int = 1, ) -> None: self.data_path = Path(data_path) self.tokenizer_path = tokenizer_path self.tokenizer, self.bos_id, self.eos_id, self.pad_id = load_tokenizer(tokenizer_path) self.max_len = int(pack_len) + int(bool(append_eos)) self.shuffle_buffer = shuffle_buffer self.seed = seed self.rank = rank self.world = world self.seen_count = 0 self.skipped_count = 0 parts_root = self.data_path / "parts" if parts_root.exists(): self.parts = sorted(x for x in parts_root.iterdir() if x.is_dir() and x.name.startswith("part-")) self.shard_rows = False else: self.parts = [self.data_path] self.shard_rows = True if not self.parts: raise FileNotFoundError(f"no ELF HFDS parts under {self.data_path}") def _to_fixed_row(self, ids) -> torch.Tensor: row = [int(x) for x in ids[: self.max_len]] if len(row) < self.max_len: row.extend([int(self.pad_id)] * (self.max_len - len(row))) return torch.tensor(row, dtype=torch.long) def __iter__(self): from datasets import Sequence, load_from_disk from datasets.features import features as hf_features # Some ELF/T5 caches were saved with HF metadata feature type "List". # Newer datasets builds may not register that alias, so keep old caches loadable. hf_features._FEATURE_TYPES.setdefault("List", Sequence) info = get_worker_info() worker_id = info.id if info else 0 num_workers = info.num_workers if info else 1 if self.shard_rows: # HF datasets saved as one root with data-*.arrow need row-level sharding. # Otherwise only rank0 would own the single "part" and all other ranks are empty. parts = self.parts shard_count = max(1, self.world * num_workers) shard_index = self.rank * num_workers + worker_id else: rank_parts = self.parts[self.rank :: self.world] parts = rank_parts[worker_id::num_workers] if not parts: return shard_count = 1 shard_index = 0 rng = random.Random(self.seed + 1009 * self.rank + worker_id) rows: list[torch.Tensor] = [] seen = yielded = 0 while True: rng.shuffle(parts) for part in parts: ds = load_from_disk(str(part)) if self.shard_rows: ds = ds.shard(num_shards=shard_count, index=shard_index, contiguous=True) for item in ds: ids = item.get("input_ids") if ids is None or len(ids) == 0: continue seen += 1 rows.append(self._to_fixed_row(ids)) if len(rows) >= self.shuffle_buffer: i = rng.randrange(len(rows)) rows[i], rows[-1] = rows[-1], rows[i] yielded += 1 yield rows.pop() if seen % 100000 == 0: print( f"[elf_hfds direct rank={self.rank} worker={worker_id}] docs={seen} rows={yielded} buffered={len(rows)} part={part.name}", flush=True, ) while rows: i = rng.randrange(len(rows)) rows[i], rows[-1] = rows[-1], rows[i] yielded += 1 yield rows.pop() def main() -> None: p = argparse.ArgumentParser() p.add_argument("--cache_path", required=True) p.add_argument("--tokenizer_path", default=T5_TOKENIZER) args = p.parse_args() ds = CachedTokenDataset(args.cache_path, args.tokenizer_path) tok = ds.tokenizer print(f"rows={len(ds)} length={ds.ids.size(1)} seen={ds.seen_count} dropped={ds.skipped_count}") print(f"bos={ds.bos_id}:{token_name(tok, ds.bos_id)} eos={ds.eos_id}:{token_name(tok, ds.eos_id)}") print("head", [token_name(tok, x) for x in ds.ids[0, :16].tolist()]) print("tail", [token_name(tok, x) for x in ds.ids[0, -16:].tolist()]) print(f"cache_size={Path(args.cache_path).stat().st_size / 1024**3:.2f} GiB") if __name__ == "__main__": main()