Training with PyTorch¶
PyTorch’s
map-style dataset contract
requires integer __getitem__() and __len__(). SDS datasets provide both. Use an application
adapter to convert generic records into model tensors.
Install PyTorch in the training application; it is intentionally not an SDS dependency.
uv add 'torch>=2.3,<3'
import os
import signal_dataset as sds
import torch
from torch.utils.data import Dataset
class SDSTorchDataset(Dataset[dict[str, torch.Tensor]]):
def __init__(self, uri: str) -> None:
self._uri = uri
self._dataset = None
self._pid = None
def _open(self) -> sds.Dataset:
pid = os.getpid()
if self._dataset is None or self._pid != pid:
self._dataset = sds.open(self._uri)
self._pid = pid
return self._dataset
def __len__(self) -> int:
return len(self._open())
def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
record = self._open()[index]
return {
"inputs": torch.from_numpy(record["samples"].data.copy()),
"target": torch.tensor(record.metadata["label"], dtype=torch.long),
}
def __getstate__(self) -> dict[str, object]:
return {"_uri": self._uri}
def __setstate__(self, state: dict[str, object]) -> None:
self._uri = str(state["_uri"])
self._dataset = None
self._pid = None
Field.data is immutable. The example copies it before torch.from_numpy so model transforms
cannot mutate the SDS record or trigger PyTorch’s non-writable-array warning.
Use PyTorch-owned sampling and batching:
from torch.utils.data import DataLoader
dataset = SDSTorchDataset("captures.sds")
loader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=True,
num_workers=worker_count,
pin_memory=use_accelerator,
)
for batch in loader:
train_step(batch["inputs"], batch["target"])
The PID check reopens the same immutable SDS URI in both forked and spawned workers. Pickling also
drops the live reader. Return CPU tensors from workers; let pin_memory and the training step
manage accelerator transfer.
For distributed training, let DistributedSampler assign ordinals. Call set_epoch() before each
epoch so its shuffle order changes:
from torch.utils.data.distributed import DistributedSampler
sampler = DistributedSampler(dataset)
loader = DataLoader(dataset, batch_size=batch_size, sampler=sampler)
for epoch in range(num_epochs):
sampler.set_epoch(epoch)
for batch in loader:
train_step(batch["inputs"], batch["target"])
Keep model conversion, augmentation, sampling, and batching in the training application. SDS remains responsible only for immutable indexed records and aligned annotations.
Batched fetch¶
DataLoader calls __getitems__ on a map-style dataset when one exists (since
PyTorch 1.13), passing the whole batch of indices at once. That is worth
implementing here, because the underlying container is markedly faster reading a
list of indices in one call than the same indices one at a time:
def __getitems__(self, indices: list[int]) -> list[dict[str, torch.Tensor]]:
return [self[index] for index in indices]
That version still reads one at a time. To get the batched read as well, address
the shards yourself with dataset.read_instructions(...) and fetch each run in
one call. See bring your own loader. Note
that Grain does not call __getitems__; it looks for _getitems behind a
separate opt-in protocol, so a source used by both needs the method under both
names.