Files
2026-07-13 13:05:14 +08:00

162 lines
4.6 KiB
Python

"""Stream a Rerun catalog into PyTorch with the experimental dataloader."""
from __future__ import annotations
from pathlib import Path
import torch
import torch.multiprocessing
from torch import nn
import rerun as rr
# Rerun's tokio runtime is not fork-safe, so DataLoader workers must use
# `spawn`. Set this before constructing any DataLoader, even with
# `num_workers=0`, so bumping the worker count later doesn't deadlock on the
# first catalog call.
torch.multiprocessing.set_start_method("spawn", force=True)
# In a real workflow you'd start a long-running OSS server (`rerun server`)
# and point a `CatalogClient` at it. For this self-contained snippet we use
# a short-lived in-process server and the DROID sample dataset shipped with
# the repo.
sample_5_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server()
rrd_paths = list(sample_5_path.glob("*.rrd"))
# region: register
client = rr.catalog.CatalogClient(server.url())
dataset = client.create_dataset("my_robot_data", exist_ok=True)
uris = [f"file://{p.resolve()}" for p in rrd_paths]
dataset.register(uris).wait()
# endregion: register
# region: describe_sample
from rerun.experimental.dataloader import (
DataSource,
Field,
FixedRateSampling,
NumericDecoder,
RerunIterableDataset,
)
source = DataSource(
dataset=client.get_dataset("my_robot_data"),
segments=[
"ILIAD_50aee79f_2023_07_12_20h_55m_08s",
"ILIAD_5e938e3b_2023_07_20_10h_40m_10s",
],
)
fields = {
"state": Field(
"/observation/joint_positions:Scalars:scalars", decode=NumericDecoder()
),
"action": Field(
"/action/joint_positions:Scalars:scalars", decode=NumericDecoder()
),
}
ds = RerunIterableDataset(
source=source,
index="real_time",
fields=fields,
timeline_sampling=FixedRateSampling(rate_hz=15.0),
)
# endregion: describe_sample
# region: window
# Each sample now carries the next 50 action steps instead of a single value.
# Offsets are in the index timeline's native unit: integer steps for integer
# indices, or nanoseconds for timestamp indices (use multiples of the
# FixedRateSampling period).
windowed_action = Field(
"/action/joint_positions:Scalars:scalars",
decode=NumericDecoder(),
window=(1, 50),
)
# endregion: window
# region: video_decoder
# Decode a compressed video stream as part of each sample.
# `keyframe_interval` must be at least the actual GOP length. For timestamp
# timelines, `fps_estimate` should also approximate the true frame rate.
from rerun.experimental.dataloader import VideoFrameDecoder
image_field = Field(
"/camera/wrist:VideoStream:sample",
decode=VideoFrameDecoder(
codec="h264", keyframe_interval=500, fps_estimate=15.0
),
)
# endregion: video_decoder
# region: dataloader
from torch.utils.data import DataLoader
from rerun.experimental.dataloader import RerunMapDataset
def my_collate(
samples: list[dict[str, torch.Tensor]],
) -> dict[str, torch.Tensor]:
# Drop samples that landed outside the underlying data (FixedRateSampling
# may overshoot the end of a segment by one grid point).
samples = [
s for s in samples if s["state"].numel() > 0 and s["action"].numel() > 0
]
return {
"state": torch.stack([s["state"] for s in samples]).float(),
"action": torch.stack([s["action"] for s in samples]).float(),
}
loader = DataLoader(
ds,
batch_size=8,
num_workers=0,
shuffle=isinstance(ds, RerunMapDataset), # iterable shuffles internally
collate_fn=my_collate,
)
# endregion: dataloader
# A one-layer stand-in for the actual policy. The point of the snippet is
# the dataloader, not the model.
class TinyPolicy(nn.Module):
def __init__(self, state_dim: int = 7, action_dim: int = 7) -> None:
super().__init__()
self.linear = nn.Linear(state_dim, action_dim)
def forward(
self, batch: dict[str, torch.Tensor]
) -> tuple[torch.Tensor, dict[str, float]]:
prediction = self.linear(batch["state"])
loss = nn.functional.mse_loss(prediction, batch["action"])
return loss, {}
policy = TinyPolicy()
optimizer = torch.optim.AdamW(policy.parameters(), lr=1e-4)
device = torch.device("cpu")
policy.to(device)
epochs = 1
# region: train
for epoch in range(epochs):
if isinstance(ds, RerunIterableDataset):
ds.set_epoch(epoch)
for batch in loader:
batch = {k: v.to(device) for k, v in batch.items()}
loss, _ = policy.forward(batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# endregion: train