"""Document-level Causal LM dataset (traditional pretraining packing). On-disk layout (mirrors V1 QA layout, but with document indices):: / metadata.json # same schema as V1 (tokenizer_info, max_seq_len, total_length) tokens.npy # int token stream (mmap) epoch_0/ doc_start.npy # int64, start offset into tokens.npy doc_len.npy # int32/int64, length in tokens (>= 2) epoch_1/ ... Each document is packed as a standard next-token LM example: inputs = doc[:-1], labels = doc[1:] prefix_lens = 0, causal_lens = len(inputs) # full causal attention Use with ``data.format=document`` (and preferably ``lm_mode=causal``). """ from __future__ import annotations from dataclasses import dataclass, fields from pathlib import Path from typing import Optional, Any import os import json import numpy as np import pydantic import torch from torch.utils.data import IterableDataset, get_worker_info from models.common import IGNORE_LABEL_ID from models.prefixlm_attention import compute_aux_seq_tensors_scalars from models.layers import find_multiple from multipack_sampler import MultipackDistributedBatchSampler class DocumentDatasetConfig(pydantic.BaseModel): seed: int dataset_path: str batch_max_length: int drop_last_batch: bool rank: int num_replicas: int fixed_epoch: Optional[int] = None start_data_epoch: int = 0 resume_sampler_start_index: int = 0 class DocumentDatasetMeta(pydantic.BaseModel): tokenizer_info: dict[str, Any] = {} vocab_size: Optional[int] = None max_seq_len: int total_length: int # Optional marker written by 111fineweb / prepare_document_data layout: Optional[str] = None @dataclass class DocumentDatasetIndices: doc_start: np.ndarray doc_len: np.ndarray class DocumentDataset(IterableDataset): """Packed document Causal LM loader (drop-in sibling of ``V1Dataset``).""" def __init__(self, config: DocumentDatasetConfig): super().__init__() self.config = config self.metadata = self._load_metadata() self._data: Optional[np.ndarray] = None self._data_indices: Optional[DocumentDatasetIndices] = None self._sampler: Optional[MultipackDistributedBatchSampler] = None self._epoch = int(config.start_data_epoch) self._resume_sampler_start = int(config.resume_sampler_start_index) self._active_data_epoch: Optional[int] = None def _load_metadata(self) -> DocumentDatasetMeta: with open(os.path.join(self.config.dataset_path, "metadata.json"), "r") as f: metadata = DocumentDatasetMeta(**json.load(f)) metadata.max_seq_len -= 1 # autoregressive shift assert metadata.vocab_size is None metadata.vocab_size = find_multiple(metadata.tokenizer_info.pop("vocab_size"), 256) return metadata def _count_data_epochs(self) -> int: cached = getattr(self, "_num_data_epochs_cached", None) if cached is not None: return int(cached) root = Path(self.config.dataset_path) n = sum(1 for p in root.glob("epoch_*") if p.is_dir()) if n <= 0: raise FileNotFoundError(f"no epoch_* dirs under {root}") self._num_data_epochs_cached = n return n def _load_dataset_before_epoch_begin(self): if self._data is None: self._data = np.load(os.path.join(self.config.dataset_path, "tokens.npy"), mmap_mode="r") if self.config.fixed_epoch is not None: epoch_idx = int(self.config.fixed_epoch) else: epoch_idx = int(self._epoch) % self._count_data_epochs() self._active_data_epoch = int(epoch_idx) epoch_dir = os.path.join(self.config.dataset_path, f"epoch_{epoch_idx}") self._data_indices = DocumentDatasetIndices( **{ f.name: np.load(os.path.join(epoch_dir, f"{f.name}.npy"), mmap_mode="r") for f in fields(DocumentDatasetIndices) } ) if self.config.fixed_epoch is None: self._epoch += 1 # Each doc of length L contributes L-1 supervised positions. lengths = np.maximum(self._data_indices.doc_len.astype(np.int64) - 1, 1) # seed≠0: permute docs so resume-without-cursor does not replay the same prefix. if int(self.config.seed) != 0: rng = np.random.RandomState(int(self.config.seed) + int(epoch_idx) * 10007) perm = rng.permutation(int(lengths.size)) lengths = lengths[perm] self._data_indices = DocumentDatasetIndices( doc_start=self._data_indices.doc_start[perm], doc_len=self._data_indices.doc_len[perm], ) self._sampler = MultipackDistributedBatchSampler( lengths=lengths, batch_max_length=self.config.batch_max_length, drop_last_batch=self.config.drop_last_batch, rank=self.config.rank, num_replicas=self.config.num_replicas, ) def _load_batch(self, indices: np.ndarray): assert self._data is not None and self._data_indices is not None docs = [] starts = self._data_indices.doc_start[indices] lens = self._data_indices.doc_len[indices] for s, n in zip(starts, lens): n = int(n) if n < 2: continue docs.append(self._data[int(s) : int(s) + n].astype(np.int32)) batch = {k: [] for k in ["inputs", "labels", "position_ids"]} # prefix_lens = 0 for every doc → full causal path in PrefixLM kernels seqlens_prefix = [] seqlens_causal = [] for doc in docs: inputs = doc[:-1] labels = doc[1:] batch["inputs"].append(inputs) batch["labels"].append(labels) batch["position_ids"].append(np.arange(len(inputs), dtype=np.int32)) seqlens_prefix.append(0) seqlens_causal.append(len(inputs)) if not batch["inputs"]: # Degenerate batch: emit a fully-padded empty step (should be rare). pad_len = self.config.batch_max_length empty = { "inputs": np.zeros(pad_len, dtype=np.int32), "labels": np.full(pad_len, IGNORE_LABEL_ID, dtype=np.int32), "position_ids": np.zeros(pad_len, dtype=np.int32), } tensors, scalars = compute_aux_seq_tensors_scalars( np.zeros(0, dtype=np.int32), np.zeros(0, dtype=np.int32), self.config.batch_max_length, ) return {k: torch.from_numpy(v) for k, v in (empty | tensors).items()}, scalars batch = {k: np.concatenate(v, dtype=np.int32) for k, v in batch.items()} pad_len = self.config.batch_max_length - batch["inputs"].shape[0] if pad_len > 0: pad_values = { "inputs": 0, "labels": IGNORE_LABEL_ID, "position_ids": 0, } for k, fill in pad_values.items(): batch[k] = np.pad(batch[k], (0, pad_len), mode="constant", constant_values=fill) seqlen_i = np.array(seqlens_prefix, dtype=np.int32) seqlen_o = np.array(seqlens_causal, dtype=np.int32) tensors, scalars = compute_aux_seq_tensors_scalars(seqlen_i, seqlen_o, self.config.batch_max_length) return {k: torch.from_numpy(v) for k, v in (batch | tensors).items()}, scalars def __iter__(self): worker_info = get_worker_info() assert worker_info is None or worker_info.num_workers == 1 self._load_dataset_before_epoch_begin() assert self._sampler is not None start_index = int(self._resume_sampler_start) self._resume_sampler_start = 0 for indices in self._sampler.iter(start_index=start_index): batch, scalars = self._load_batch(indices) scalars = { **scalars, "_resume_data_epoch": int(self._active_data_epoch if self._active_data_epoch is not None else 0), "_resume_sampler_start": int(getattr(self._sampler, "cursor", 0)), "_resume_epoch_num_samples": int(getattr(self._sampler, "lengths").size), } yield batch, scalars