Cache preprocessed training data in Python

If every training restart repeats preprocessing, cache the serialized result once and read it on subsequent runs. Define a named iterable data source that packs each Python record or dictionary with pickle or dill and yields its bytes in CacheInput. The record can contain arrays, tensors, and nested data structures. Your read transform unpacks the payload for training.

Install and run

Install dataset-rt with uv add dataset-rt. Save the complete example below as cache_text.py and run uv run cache_text.py twice from the same directory.

"""Cache pickled training dictionaries and unpack delivered records."""

import pickle
from collections.abc import Iterator
from pathlib import Path

from pydantic import TypeAdapter
from typing_extensions import TypedDict

import dataset_rt as drt


class TrainingRecord(TypedDict):
    """Keep the serialized record shape explicit at the decoding boundary."""

    text: str
    tokens: list[int]


RECORD_ADAPTER = TypeAdapter(TrainingRecord)


class TextSamples:
    """Keep the source version in its name so reuse has an explicit boundary."""

    name = "text_records_v1"

    def __iter__(self) -> Iterator[drt.CacheInput]:
        """Pack each preprocessed dictionary without retaining the full source."""
        for text, label in [("  Hello world  ", 0), ("Dataset caching", 1)]:
            normalized = text.strip().lower()
            record = TrainingRecord(
                text=normalized, tokens=[ord(char) for char in normalized]
            )
            yield drt.CacheInput(
                data=pickle.dumps(record, protocol=pickle.HIGHEST_PROTOCOL),
                metadata={"label": label, "split": "train"},
            )


def main() -> None:
    """Reuse complete caches and report failures before consuming samples."""
    runtime = drt.DatasetRuntime(num_workers=2)
    result = runtime.from_cache_sources(
        TextSamples(),
        Path("cache_text"),
        reader_config=drt.ReaderConfig(seed=42, shuffle=False),
    )
    match result:
        case drt.CacheSourcesDatasetSuccess(dataset=dataset, results=results):
            print(results)
        case failure:
            raise RuntimeError(failure)

    for sample in dataset:
        record = RECORD_ADAPTER.validate_python(pickle.loads(sample.data))
        print(record["text"], record["tokens"], sample.metadata["label"])


if __name__ == "__main__":
    main()

The first run writes cache_text/text_records_v1. The second reuses the complete cache without iterating the source. Both read hello world and dataset caching, with their character-code lists, in physical order. The dictionary is small here to keep the example runnable; the same iterable contract applies to large per-sample structures. TypeAdapter validates the decoded dictionary's shape at the application boundary.

A source name identifies a cache; reuse does not detect changes to preprocessing, input files, or labels. Change the source name or cache root when those change.

What is persisted

Each cache contains a publication manifest, Arrow metadata, a binary index, and payload shards. The manifest marks a complete cache. Payloads stay immutable; metadata filtering and weighting at runtime do not rewrite these files.

Primitive metadata values support columnar inspection. Keep large arrays, tensors, and domain objects in the serialized payload, and decode them in your training code. See the serialization boundary and storage format.

pickle comes with Python. If your objects require dill, install it with uv add dill and use matching dill.dumps() / dill.loads() calls. DatasetRT does not choose or invoke either serializer for you.

Load existing caches directly

When source discovery is unnecessary, load known cache directories:

dataset = runtime.cached_dataset(
    [Path("cache_text/text_records_v1")],
    reader_config=drt.ReaderConfig(seed=42, shuffle=False),
)

This excerpt uses runtime, Path, and drt from the complete example. Default loading validates structural requirements without hashing every shard. Use ReaderConfig(seed=42, validate_cache=True) when you explicitly need the additional integrity checks.

Native queues remain bounded by configured prefetch capacity. Your source must also stream its inputs; constructing a full Python list of payloads before yielding would defeat that memory boundary.

Next: filter and rebalance samples with Polars or build a PyTorch DataLoader.