spec-b300 / source /src /speculators /train /dataloader.py
khazic's picture
Archive three-epoch run: logs and provenance part 3
e65937c verified
Raw History Blame Contribute Delete
8.07 kB
from __future__ import annotations
import copy
import logging
import os
from typing import TYPE_CHECKING, Any, Literal
if TYPE_CHECKING:
from collections.abc import Callable
import torch
from torch.utils.data import DataLoader
from hs_connectors import HiddenStatesTransfer
from speculators.train.data import (
ArrowDataset,
BaseDataset,
CollateFn,
)
from speculators.train.distributed import get_dp_rank, get_dp_size
from speculators.train.distributed_batch_sampler import (
MultipackDistributedBatchSamplerV2,
)
from speculators.train.noise_transforms import AddUniformNoise
logger = logging.getLogger(__name__)
BatchType = dict[str, Any]
def _limit_worker_threads() -> None:
"""Limit per-worker thread pools to avoid thread exhaustion.
With ``multiprocessing_context='spawn'``, each worker is a full process
that re-imports numpy (OpenBLAS) and torch, each creating thread pools
sized to the core count. DataLoader workers only do I/O and tensor
slicing — they don't benefit from intra-op parallelism.
The env vars must be set before numpy/torch are imported to take effect
on OpenBLAS/OMP. Call this at the top of the training entry point,
before DataLoader construction.
"""
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("MKL_NUM_THREADS", "1")
def _worker_init_fn(worker_id: int) -> None: # noqa: ARG001
torch.set_num_threads(1)
def _make_worker_dataset(dataset: BaseDataset) -> BaseDataset:
"""Drop parent-only sampler state from the dataset sent to spawned workers.
``approx_lengths`` can contain hundreds of thousands of Python integers. The
batch sampler copies it into its own NumPy array before worker startup, and
``Dataset.__getitem__`` never reads it. Avoiding that field in every spawn pickle
keeps the parent from blocking while each child imports the training stack.
"""
worker_dataset = copy.copy(dataset)
worker_dataset.approx_lengths = []
return worker_dataset
def _setup_dataloader(
dataset: BaseDataset,
total_seq_len: int,
hidden_size: int,
num_workers: int = 12,
num_target_layers: int = 3,
prefetch_factor: int | None = 4,
preprocess: Callable[[BatchType], BatchType] | None = None,
in_order: bool = True,
max_batches: int | None = None,
) -> DataLoader:
batch_sampler = MultipackDistributedBatchSamplerV2(
batch_max_length=total_seq_len,
lengths=dataset.approx_lengths,
num_replicas=get_dp_size(),
rank=get_dp_rank(),
max_batches=max_batches,
)
use_workers = num_workers > 0
if not in_order and not use_workers:
# Ordering is a property of the multi-worker result queue; with a single
# process there is nothing to reorder, so silently ignoring the flag would
# hide a misconfigured run behind unchanged (slow) behaviour.
raise ValueError("in_order=False requires num_workers > 0")
worker_dataset = _make_worker_dataset(dataset) if use_workers else dataset
return DataLoader(
worker_dataset,
batch_sampler=batch_sampler,
num_workers=num_workers,
prefetch_factor=prefetch_factor if use_workers else None,
pin_memory=True,
collate_fn=CollateFn(
total_seq_len,
hidden_size,
num_target_layers=num_target_layers,
dtype=dataset.hidden_states_dtype,
preprocess=preprocess,
),
persistent_workers=use_workers,
multiprocessing_context="spawn" if use_workers else None,
worker_init_fn=_worker_init_fn if use_workers else None,
# Online hidden-state fetches are heavy-tailed, so the strict round-robin
# of in-order delivery makes the rank wait on worker i even when workers
# i+1.. already hold finished batches -- and every peer rank then waits on
# this one at the all-reduce. Out-of-order delivery lets the prefetch queue
# absorb the tail instead of serialising on it. The epoch still covers the
# same batches; only mid-epoch resume degrades, since its fast-skip slices
# the sampler by count and no longer names the exact batches consumed.
in_order=in_order or not use_workers,
)
def create_train_val_loaders(
*,
data_path: str,
total_seq_len: int,
hidden_states_dtype: torch.dtype,
noise_std: float,
transfer: HiddenStatesTransfer | None = None,
vllm_endpoint: str,
on_missing: Literal["generate", "skip", "warn", "raise"],
on_generate: Literal["cache", "delete"],
verifier_name_or_path: str,
request_timeout: float | None,
max_retries: int,
generation_validation_retries: int,
max_consecutive_generation_failures: int,
hidden_size: int,
num_target_layers: int,
num_workers: int,
prefetch_factor: int,
preprocess: Callable[[BatchType], BatchType] | None,
train_data_ratio: float = 0.9,
fail_on_hidden_state_error: bool = False,
fetch_threads: int = 1,
dataloader_in_order: bool = True,
vllm_http_keepalive: bool = True,
max_train_batches: int | None = None,
) -> tuple[DataLoader, DataLoader]:
"""Create training and validation DataLoaders.
Non-data SP ranks get lightweight loaders with no workers (they receive
batches via scatter). Reads DP/SP topology from
:mod:`speculators.train.distributed`.
"""
_limit_worker_threads()
noise_transform = AddUniformNoise(std=noise_std)
if not (0.0 < train_data_ratio < 1.0):
raise ValueError(f"train_data_ratio must be in (0, 1), got {train_data_ratio}")
train_dataset: BaseDataset = ArrowDataset(
datapath=data_path,
max_len=total_seq_len,
transfer=transfer,
vllm_endpoint=vllm_endpoint,
on_missing=on_missing,
on_generate=on_generate,
transform=noise_transform,
train_ratio=train_data_ratio,
split="train",
model=verifier_name_or_path,
hidden_states_dtype=hidden_states_dtype,
request_timeout=request_timeout,
max_retries=max_retries,
fail_on_hidden_state_error=fail_on_hidden_state_error,
fetch_threads=fetch_threads,
http_keepalive=vllm_http_keepalive,
generation_validation_retries=generation_validation_retries,
max_consecutive_generation_failures=max_consecutive_generation_failures,
)
val_dataset: BaseDataset = ArrowDataset(
datapath=data_path,
max_len=total_seq_len,
transfer=transfer,
vllm_endpoint=vllm_endpoint,
on_missing=on_missing,
on_generate=on_generate,
train_ratio=train_data_ratio,
split="val",
model=verifier_name_or_path,
hidden_states_dtype=hidden_states_dtype,
request_timeout=request_timeout,
max_retries=max_retries,
fail_on_hidden_state_error=fail_on_hidden_state_error,
fetch_threads=fetch_threads,
http_keepalive=vllm_http_keepalive,
generation_validation_retries=generation_validation_retries,
max_consecutive_generation_failures=max_consecutive_generation_failures,
)
train_loader = _setup_dataloader(
train_dataset,
total_seq_len,
hidden_size,
num_target_layers=num_target_layers,
num_workers=num_workers,
prefetch_factor=prefetch_factor,
preprocess=preprocess,
in_order=dataloader_in_order,
max_batches=max_train_batches,
)
# Validation reduces a mean over the whole split, so its batch order is
# irrelevant too and it sees the same heavy-tailed fetch latency.
val_loader = _setup_dataloader(
val_dataset,
total_seq_len,
hidden_size,
num_target_layers=num_target_layers,
num_workers=num_workers,
prefetch_factor=prefetch_factor,
preprocess=preprocess,
in_order=dataloader_in_order,
)
return train_loader, val_loader