| from pathlib import Path |
| import json |
| import os |
| from datetime import datetime |
|
|
| from tqdm import tqdm |
| import numpy as np |
| import torch |
| from torch.utils.data import Dataset |
|
|
| class DiskDataset(Dataset): |
| def __init__(self, file_path, max_seq_len, stride_fraction=None, allow_cycling=False): |
| """ |
| Args: |
| file_path: path to the .bin file |
| max_seq_len: |
| - 在 stride_fraction=-1 (预填充模式) 下,必须与生成数据时的 pad_to_length 一致。 |
| - 在普通模式下,表示窗口大小。 |
| stride_fraction: |
| - If > 0: Sliding window (standard pre-training). |
| - If == -1: Pre-padded Sample Mode. Assumes data on disk is already |
| padded to blocks of size `max_seq_len`. |
| """ |
| self.file_path = Path(file_path).resolve() |
| assert self.file_path.is_file(), f"File not found: {self.file_path}" |
| |
| self.max_seq_len = max_seq_len |
| self.stride_fraction = stride_fraction if stride_fraction is not None else 1.0 |
| |
| |
| |
| self.pre_padded_mode = (self.stride_fraction == -1) |
|
|
| self.data = np.memmap(self.file_path, dtype="int32", mode="r") |
| self.file_size = len(self.data) |
| |
| if self.pre_padded_mode: |
| |
| |
| if self.file_size % self.max_seq_len != 0: |
| print(f"Warning: File size ({self.file_size}) is not a multiple of max_seq_len ({self.max_seq_len}). " |
| f"Last partial sample might be ignored or dataset might be corrupted.") |
| |
| self.n_samples = self.file_size // self.max_seq_len |
| self.stride = self.max_seq_len |
| else: |
| |
| self.stride = int(self.max_seq_len * self.stride_fraction) |
| self.n_samples = 1 + max(0, (self.file_size - self.max_seq_len) // self.stride) |
| |
| self.allow_cycling = allow_cycling and not self.pre_padded_mode |
|
|
| def __len__(self): |
| return self.n_samples |
|
|
| def get_token_count(self): |
| return self.file_size |
|
|
| def __getitem__(self, idx): |
| if self.allow_cycling: |
| idx = idx % self.n_samples |
| |
| |
| |
| start = idx * self.stride |
| end = start + self.max_seq_len |
| |
| |
| seq = np.array(self.data[start:end], dtype=np.int32, copy=True) |
| return torch.from_numpy(seq).long() |
|
|
| @staticmethod |
| def generate_bin( |
| dataset_iterator, |
| tokenizer, |
| output_path, |
| add_eos=True, |
| column="text", |
| token_limit=None, |
| metadata_path=None, |
| pad_to_length=None, |
| pad_token_id=-100, |
| append: bool = False, |
| existing_metadata_path=None, |
| ): |
| output_path = Path(output_path) |
| output_dir = output_path.parent |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| def tokenize_fn(text): |
| ids = tokenizer.encode(text) |
| if add_eos: |
| ids.append(tokenizer.eos_token_id) |
| return ids |
|
|
| print(f"Building binary file at: {output_path}") |
| if pad_to_length is not None: |
| print(f"Mode: Fixed Block Size (Padded/Truncated to {pad_to_length})") |
| else: |
| print(f"Mode: Continuous Stream (Packed)") |
|
|
| dtype = np.int32 |
| bytes_per_token = np.dtype(dtype).itemsize |
|
|
| |
| pos = 0 |
| allocated = 0 |
| mm = None |
|
|
| |
| if append and output_path.exists(): |
| file_bytes = output_path.stat().st_size |
| if file_bytes % bytes_per_token != 0: |
| raise ValueError( |
| f"Corrupted bin? File bytes {file_bytes} not divisible by token size {bytes_per_token}" |
| ) |
| pos = file_bytes // bytes_per_token |
| allocated = pos |
| if allocated == 0: |
| |
| append = False |
| else: |
| mm = np.memmap(output_path, dtype=dtype, mode="r+", shape=(allocated,)) |
| print(f"[INFO] Appending to existing file. Current tokens on disk: {pos}") |
| else: |
| |
| if output_path.exists(): |
| output_path.unlink() |
|
|
| |
| unit = "samples" |
| pbar = tqdm(total=None, unit=unit) |
|
|
| |
| initial_alloc_tokens = 1_000_000 |
| if pad_to_length: |
| initial_alloc_tokens = (initial_alloc_tokens // pad_to_length) * pad_to_length |
|
|
| def _grow(new_alloc_tokens): |
| nonlocal mm, allocated |
| if mm is not None: |
| mm.flush() |
| del mm |
| mm = None |
|
|
| new_bytes = new_alloc_tokens * bytes_per_token |
| with open(output_path, "a+b") as f: |
| if new_bytes > 0: |
| f.seek(new_bytes - 1) |
| f.write(b"\0") |
| f.flush() |
| os.fsync(f.fileno()) |
|
|
| allocated = new_alloc_tokens |
| mm = np.memmap(output_path, dtype=dtype, mode="r+", shape=(allocated,)) |
|
|
| |
| if mm is None: |
| _grow(initial_alloc_tokens) |
| else: |
| |
| |
| pass |
|
|
| |
| prev_total_tokens = 0 |
| prev_total_samples = 0 |
| prev_pad_to_length = None |
|
|
| meta_path_to_read = None |
| if append: |
| |
| if existing_metadata_path is not None and Path(existing_metadata_path).exists(): |
| meta_path_to_read = Path(existing_metadata_path) |
| |
| elif metadata_path is not None and Path(metadata_path).exists(): |
| meta_path_to_read = Path(metadata_path) |
|
|
| if meta_path_to_read is not None: |
| try: |
| with open(meta_path_to_read, "r", encoding="utf-8") as f: |
| prev = json.load(f) |
| prev_total_tokens = int(prev.get("total_tokens", 0)) |
| prev_total_samples = int(prev.get("total_samples", 0)) |
| prev_pad_to_length = prev.get("pad_to_length", None) |
| except Exception as e: |
| print(f"[WARN] Failed to read previous metadata for append: {e}") |
|
|
| |
| if append and prev_pad_to_length != pad_to_length: |
| raise ValueError( |
| f"pad_to_length mismatch when appending: previous={prev_pad_to_length}, new={pad_to_length}" |
| ) |
|
|
| |
| total_tokens_written = 0 |
| sample_count = 0 |
| done = False |
|
|
| for example in dataset_iterator: |
| if done: |
| break |
|
|
| ids = tokenize_fn(example[column]) |
|
|
| if pad_to_length is not None: |
| if len(ids) > pad_to_length: |
| ids = ids[:pad_to_length] |
| if len(ids) < pad_to_length: |
| ids.extend([pad_token_id] * (pad_to_length - len(ids))) |
| arr = np.asarray(ids, dtype=dtype) |
| else: |
| if not ids: |
| continue |
| arr = np.asarray(ids, dtype=dtype) |
|
|
| |
| if token_limit is not None: |
| if total_tokens_written + arr.size > token_limit: |
| done = True |
| if pad_to_length is None: |
| remaining = token_limit - total_tokens_written |
| arr = arr[:remaining] |
| else: |
| break |
|
|
| needed = pos + arr.size |
| if needed > allocated: |
| new_alloc = max(max(allocated * 2, 1_000_000), needed) |
| if pad_to_length: |
| new_alloc = ((new_alloc + pad_to_length - 1) // pad_to_length) * pad_to_length |
| _grow(new_alloc) |
|
|
| mm[pos:pos + arr.size] = arr |
| pos += arr.size |
| total_tokens_written += arr.size |
| sample_count += 1 |
| pbar.update(1) |
|
|
| pbar.close() |
|
|
| if mm is not None: |
| mm.flush() |
| del mm |
| mm = None |
|
|
| |
| with open(output_path, "r+b") as f: |
| f.truncate(pos * bytes_per_token) |
| f.flush() |
| os.fsync(f.fileno()) |
|
|
| |
| if metadata_path is not None: |
| meta = { |
| "last_modified": datetime.now().isoformat(), |
| "total_tokens": int(prev_total_tokens + total_tokens_written), |
| "total_samples": int(prev_total_samples + sample_count), |
| "pad_to_length": pad_to_length, |
| "dtype": str(np.dtype(dtype)), |
| "append": bool(append), |
| } |
| with open(metadata_path, "w", encoding="utf-8") as f: |
| json.dump(meta, f, indent=2) |
|
|
| print(f"✅ Wrote {total_tokens_written} tokens to {output_path} (int32)") |
| if append: |
| print(f"✅ Total tokens in file now ≈ {prev_total_tokens + total_tokens_written}") |
|
|
| return total_tokens_written |
|
|