Solutions / Physical AI
From MCAPs to policies.
Views and indexes over your fleet's logs. Instead of per-use-case pipelines, each landing in its own format.
Without Spiral
- Copies of your data
- 4
- MCAP, LeRobot, RLDS, HDF5, none authoritative
- Per-use-case pipelines
- 3–5
- each landing in its own format
- GPU utilization
- <10%
- training runs starved by data loading
The problem
A bucket of MCAPs is not a dataset.
MCAP is message-ordered because recording needs it that way. Training wants columnar, random access by episode and timestep. Specialized data formats force you to maintain pipelines: MCAP to LeRobot to RLDS to HDF5, each copy lossy in its own direction, none of them authoritative.
Camera streams are 90% of the bytes, entombed in chunks no reader can address, so the pipeline decodes everything to extract anything. Every derived copy starts drifting from the source the day it is written, and the question “which version trained checkpoint 47” becomes archaeology instead of a lookup.
Spiral maintains views and indexes to serve every access pattern, powered by Vortex, and materializes only when absolutely necessary.
The work
The twelve stages.
Index
Your MCAPs are the source of truth. Keep them that way, and query them immediately.
01
Ingest and indexing
Query logs in log order and train in training order. MCAP is message-ordered because recording needs it that way; training wants columnar, random access by episode and timestep. One substrate serves both, and the conversion pipeline between them stops existing.
You don't need to think about
Whether your MCAP writer actually emitted the summary section and chunk indexes, or why a random shuffle over episodes degrades into full-file scans when the only order your storage knows is the order the robot lived it.
02
Random access into video
Camera streams are 90% of your bytes, entombed in chunks no reader can address. Fetch camera 2, frames 1400–1440, straight from object storage, without unpacking everything around them.
You don't need to think about
H.264 access units split across MCAP chunks, decode closures that reach back to the previous keyframe, or why “frame 1400” quietly means fetching and decoding everything since frame 1280 unless someone indexed the GOP structure at ingest.
With Spiral
# Camera 2, frames 1400–1440, from object storage project.episodes.get(ep_id) .streams["/camera_2/image"] .frames[1400:1440]03
Schema drift
Firmware v2.3 renames /gripper/state and widens a float. You find out at ingest, not three months later in a training run. Old and new episodes project onto one schema at read time; nobody writes migration code, nobody handles it in the loader.
You don't need to think about
The loader's try/except that silently zero-fills a renamed topic, or the float widening that trains for three weeks before anyone notices half the fleet's gripper state was coerced to garbage.
Curate
Query your fleet like a database, not a filesystem. Views, not copies.
04
Filtering and search
Filter on what happened instead of what someone tagged. “Episodes where the gripper slipped.” “Left-handed grasps on deformable objects in low light.” Answered from stream values and embeddings that sit next to the episodes they describe, not from whatever metadata the ingest script thought to extract. No vector-store sidecar drifting out of sync with the metadata DB drifting out of sync with S3.
You don't need to think about
The extraction script that has to be rerun over the entire fleet because nobody thought to pull gripper width into metadata at ingest, or which of three stores is right when they disagree about whether an episode exists.
With Spiral
# Filter on stream values and semantics project.episodes .where(gripper.commanded_close & (gripper.width.diff() > slip_eps)) .search("left-handed grasp, low light", k=500)05
Temporal joins
Time is the join key. Force/torque in the two seconds around every detected slip, across a fleet's worth of episodes: a query, not a bespoke Python function rerun per investigation.
You don't need to think about
Log time versus publish time versus sensor time, or why a naive nearest-timestamp join pairs the force spike with the frame after contact in every episode where the camera driver buffered.
With Spiral
# Force/torque around every slip, fleet-wide project.events.where(kind == "slip") .window(before="2s", after="2s") .align(wrist.force_torque, tolerance="2ms")06
Resampling and alignment
Joining a 15Hz camera to 500Hz proprioception forces a per-column decision (hold, interpolate, sparse) that every lab reimplements differently. Declare it once; get the same aligned windows every time.
You don't need to think about
Why every lab's align_streams() disagrees about what “state at t” means when the nearest proprioception message is 3ms in the future, and which off-by-one-timestep bug shows up later as a policy that jerks.
With Spiral
# Per-column policy, declared once project.episodes.resample("50ms", camera=hold(), proprio=interpolate(), events=sparse())07
Format exports
A LeRobot export for the fine-tune, RLDS for the collaborator. If something downstream needs a format, it's a view: generated from source, regenerated when things change, never hand-maintained. Storage grows with data, not with experiments.
You don't need to think about
The exporter flag someone changed in April, or which of the seven LeRobot exports in the bucket was cut before the takedown request and which after.
With Spiral
# The export is a view, not a copy you maintain project.views.create("fold_towel_lerobot", episodes.where(task == "fold_towel"), format="lerobot")08
Agent-scale curation
Curation experiments, labeling sweeps, mixture searches: run by agents at 100x human query volume, on a substrate with atomic commits, concurrent writers, and time travel. The blast radius of a bad write is a rolled-back commit, not a restore from backup.
Read: Reversibility is the bottleneckYou don't need to think about
What an agent with S3 write access can do to a directory-convention dataset at 3am, dataset_final_v2_fixed/, or two ingest jobs writing the same episode prefix at once and a manifest that describes neither.
Train
From object storage to GPU. No shards in between.
09
Windowed multimodal sampling
State at t, camera frames t−1.5s to t at 10fps, actions t to t+1s. Three streams, three rates, one query, per sample, at batch rate. Change the window length without re-sharding the archive.
You don't need to think about
Re-sharding the archive because the policy's context window grew from 1s to 1.5s, or the padding-and-masking bug class that lives wherever three sample rates meet a fixed tensor shape.
With Spiral
# Three streams, three rates, one query project.asof(revision).samples( state = at(t), frames = camera.window(before="1.5s", fps=10), actions = actions.window(after="1s") ).to_tensor(batch=256, device="cuda")10
Loading and decode
Frames decoded ≈ frames delivered. Closure-aware video reads instead of seek-and-decode-forward. The entire video solution lives inside this stage: physical AI is video plus time-aligned everything else.
Read: Video solutionsYou don't need to think about
GOP structure and B-frame reordering, or why sampling at 2fps from 30fps video can decode ten times the frames it delivers once every window lands mid-GOP. At fleet scale that ratio is your GPU bill.
11
Dataset mixtures
Mixtures you can rerun. Weighted sampling across dozens of datasets with filters, as declarative config. “Same mixture as last month's checkpoint” means rerunning it, not excavating it.
You don't need to think about
The sampling script's silent dependence on file listing order, or why “the same mixture” stops being reproducible the day one source dataset grows, unless sampling is keyed to content rather than storage position.
With Spiral
# Same mixture as last month's checkpoint project.episodes.sample_mixture( {kitchen: 0.5, warehouse: 0.3, teleop: 0.2}, key=episode_id, seed=47)12
Checkpoint lineage
Checkpoint to episodes is a lookup. When a policy regresses, the diff between what two checkpoints trained on is a query result, not forensics.
You don't need to think about
Reconstructing a training set from a shuffle seed, the sampler's git SHA, and a directory listing from two months ago. That is the forensics a regression investigation starts with today.
With Spiral
# What changed between checkpoint 46 and 47? project.episodes.changes( after="ckpt-46", through="ckpt-47")
What you get
Your fleet, queryable.
Point Spiral at the MCAPs you already have. They stay the source of truth, in the format the robot wrote them. What you get back is every access pattern above: episode filters over stream values, time-aligned windows, semantic search, format exports as views, and batches served from object storage to GPU, with a straight answer about which data trained which checkpoint.
The engineers you hired to build policies get to build policies. And not mess with robot logs.