[ Research ]

Training Dyna-2 at million-hour scale, repeatably

Category:

Research

Author:

Dyna Robotics

Date:

August 2026

Read:

22 min

§ 1 Introduction

Earlier this week, we announced our flagship world-action model Dyna-2, trained on more than one million hours of egocentric video data. Training a robotics foundation model at this scale is unprecedented in the robotics literature, and introduced a new set of challenges across our data and training infrastructure. Most of what worked at ten thousand hours did not hold up at a million.
Robotics training is constrained by the data in a particular way. An episode is not one stream but many. Several cameras run alongside proprioceptive and action streams, each sampled at its own rate, and a training sample has to be assembled from all of them at a consistent point in time. Robotics foundation models are also more compact due to onboard deployment constraints. As a result, training can consume data quickly enough for the dataloader itself to become a bottleneck.
Robotics discussion tends to focus on models and data more than the infrastructure underneath them. We have weighted our focus the other way from the start. For most of the last year, the number of experiments we could run was limited not by a shortage of ideas, but by how long each experiment had to wait for its data. More compute would not have fixed that on its own, whether GPUs or additional CPU workers. As the data grows, the bottleneck keeps moving: first the storage format, then the ingestion pipeline, then the training manifest. The work is finding the current bottleneck, not simply adding machines behind it.
We picked concrete examples from across the training lifecycle, along with the job resilience that spans all of it, and describe what we actually changed to make one million hours work repeatedly:

Episode storage is ~68% smaller and sample reads ~2.9x faster through tuned compression and topic-group chunking

Data ingestion throughput is scaled from 14,000 episode-hours per week to 440,000

Time to first batch is reduced from about 48 hours to under a minute, across both halves of startup, building the curation manifest and then loading it on every rank

Steady-state training data I/O served from the cluster-local cache at ~2 GB/s per node, well above what a remote read sustains, and GPUs sit at 98% utilization on a warm multi-node run

Topology-aware Optimizer roughly 3x faster at scale by keeping sharding traffic inside the node instead of across the fabric

Job resilience is improved with preflight checks that catch degraded nodes before a run starts, automatic restarts from the latest checkpoint when one dies, and a standard cluster build that adds capacity in days rather than weeks

§ 2 Scaling challenges

Robot or vendorcollectionLanding bucketobject storageTraining-ready dataMCAP episodesTraining manifestcolumnar + mmapGPU clustertraininguploadsingestioncurationloading

Figure 1: Flowchart of data lifecycle

From collection to training on the GPU cluster, robot data moves through four stages: collection into a landing bucket, ingestion into training-ready episodes, curation into the dataset for a given experiment, and loading into training batches. The high-level paradigm is common across industries. But what differs is that existing infrastructure falls short on the specific requirements of large-scale robotics training, and each stage breaks in its own way. We take them in that order, and finish on the GPU cluster itself, where the same kind of scale-dependent bottleneck shows up in training.

Episode container: MCAP and topic-group chunking

For Dyna-2, which predicts both video and actions, each training sample needs a few decoded video frames but a much longer sequence of proprioceptive state, sized to the action chunk. That asymmetry isn't unique to Dyna-2: data for robotics is inherently multi-modal, and any data collection method usually carries a suite of sensors, all producing data continuously at different rates and structures, so the read pattern itself becomes a training hyperparameter that differs across modalities.
These I/O patterns force a direct tradeoff in the storage format. Independent, per-frame access makes an arbitrary-offset read trivial, but costs far more to store and stream, on the order of a hundred MB per camera-minute for per-frame JPEG, depending on resolution and frame rate. Inter-frame video compression (H.264 with GOPs) shrinks that dramatically, but a compressed frame is only decodable starting from its GOP's keyframe, which is at odds with random access to an arbitrary timestamp. Scaling up meant we needed a format that was both efficient to store and easy to inspect. Our earlier storage, H5 holding per-frame JPEG, fell short on both counts:

Not video-optimized: frames were stored independently with no inter-frame compression, inheriting the same cost as that JPEG baseline.

No native visualization: inspecting an episode meant converting it to a format some viewer could directly read.

We moved fully to MCAP, which is widely used in autonomous driving and stands out for its random access and flexible chunking. However, MCAP does not meet our training requirements out of the box, largely because its default chunking pattern is not optimized for video-action training. We heavily tuned its compression, video encoding, and chunking specific to our access pattern. Two choices mattered most:

H.264 encoding with larger-GOP: group-of-pictures provides a typical tradeoff between compression and random access, because seeking to an arbitrary frame requires decoding forward from the preceding keyframe. VLA training samples short, sparse windows, so it pays that seek penalty on every sample. World-action models read long contiguous sequences, which amortize one keyframe over many frames, so we can afford larger GOPs.

Topic-group chunking: MCAP's default writer gives each topic its own chunks, so assembling one sample costs a read per topic. We instead group topics that share a read pattern and write each group time-major: cameras interleaved with each other in one stream, proprioception and actions in another. The two never share a chunk, because a sample takes a few decoded frames but a long dense window of state. A fetch then costs one read per group rather than one per topic, so adding a camera or a state topic no longer adds a round trip.

Assembling one training sample at t1Cameras tick at t1 and t3, state every step — solid cells are the sampleDefault: one chunk per topicOne topic’s whole window per chunkall topicschunk 1camA 1camA 3chunk 2camB 1camB 3chunk 3prop 1prop 2prop 3chunk 4act 1act 2act 34 readsTopic-group chunking: two streams, each time-majorOne chunk stream per read patternvideochunk 1camA 1camB 1camA 3camB 32 readsOne read per group, not one per topicstatechunk 1prop 1act 1prop 2act 2prop 3act 3

Figure 2. Default MCAP writes each topic's whole window in turn, so one training sample costs a read per topic. We instead group topics that share a read pattern — cameras with cameras, state with state — and write each group time-major. A sample is then two reads, one per group, however many topics there are. The figure uses four topics at two different rates for clarity. A real episode carries more of both, so the measured saving below is larger.

On real teleop episodes, the compression cut the storage size by roughly 68% against a per-frame JPEG baseline. Topic chunking further reduced I/O round trips, roughly 3.4× fewer chunk fetches per sample, and in turn reads roughly 2.9× faster than the default chunking under the same reader:
1. Storage footprintMB per camera-minute02040608080.3JPEGframes25.1MCAP+H.264(per-topic)25.1Ours(topic chunking)2. Read latency per samplems010203027.0MCAP+H.264(per-topic)9.4Ours(topic chunking)3. I/O localitychunk fetches per sample03691211.33MCAP+H.264(per-topic)3.29Ours(topic chunking)

Figure 3: storage size savings and read performance improvements from our compression and chunking. The two are orthogonal — chunking changes the order messages are written, not their size, so it is identical to MCAP+H.264 in panel 1 and does its work in panels 2 and 3.

Ingestion: DAG decomposition, staggered starts, and bin-packed batches

Our ingestion pipeline was once capped at 14,000 episode-hours per week. At that rate, a million hours would have taken over a year.
A typical ingestion pipeline to process robot data has three stages: data transformation, quality check, and feature enrichment. Data transformation resamples all camera and proprioception streams onto a common timestamp grid, computes derived signals, encodes video into a canonical MCAP, and indexes the resulting metadata for manifest queries. During quality checks specifically, we detected and filtered out quality issues such as camera blackout, choppy joint states, missing/occluded hand positions, bad frames, etc. Feature enrichment generates performance labels, video captions, segmentations, and other data used in training. These are standard practice; the hard part is running it at scale.
Our original data processing pipelines, implemented as a single Kubernetes job, had multiple issues beyond scalability. Earlier this year we rewrote our data processing pipelines with Airflow, where each step was defined explicitly in a DAG. This bought us three things:

Separate scalability: Each DAG step is dynamically allocated resources tailored to its specific requirements (CPU, memory, or GPU), maximizing physical resource utilization, without inflating the whole worker pool to the worst-case step's footprint.

Better decoupling and observability: steps are tagged critical or non-critical, so a non-critical failure no longer cancels the whole DAG run. The status of each step is clearly observed from the DAG and resumed from there.

Dynamic orchestration: Processing needs change constantly, especially once we began ingesting from external vendors, each with its own data types, formats, and quality quirks. Any subset of steps can be toggled on or off per run. Quality checks, for example, run as their own pre/post gate around transformation rather than being folded into it. This is controlled at runtime, not through a code change or a new script.

On top of the DAG orchestrator, we also backed the pipeline states with durable storage rather than keeping them in a live process. This greatly increased the operability of the pipelines: checkpointable multi-day runs, replay from any step, cross-DAG artifact sharing, concurrency across dozens of active runs, and selective reprocessing. Additionally, the same design applies to both data collection use case where one run processes one episode, and batch use case for millions of episodes, down to the same set of sub-DAG steps. A run profile decides which of those steps actually run (full processing, a metadata-only backfill, a re-label pass), so neither a new trigger type nor a partial rerun needs a forked pipeline.
Every processed episode also carries a stamp of what produced it: the schema version, the ingestion pipeline version, and the software version running on the robot when it was recorded. Reprocessing a million hours takes weeks, so any pipeline change leaves the corpus mixed-version for a while, sometimes permanently. The stamp is what makes that survivable. We can find exactly which episodes are stale and reprocess only those, and a curation query can ask for whatever version range an experiment needs.
ONLINEOne run per episode, as it landslatency-first · high run concurrencyBATCHOne run per shard of a manifestthroughput-first · batch · shard · staggerMaster DAG — resolves the run profile, triggers only the sub-DAGs it needsTransform & syncCRITICAL PATHformat conversiondata validationtime synchronizationMetadatacatalog registrationmetadata extractionintegrity checksLabels & artifactssegmentationfeature extractionnon-critical · failure isolatedMediaDISABLED THIS RUNtranscodingthumbnailsTraining-ready episodes + metadataRUN PROFILES · SAME GRAPH, DIFFERENT SUB-DAGS ENABLEDFull processingSYNCMETALABELSMEDIAMetadata backfillSYNCMETALABELSMEDIARe-label onlySYNCMETALABELSMEDIA

Figure 4: logical DAG structure

However, this design does not necessarily solve the scalability issues since throughput does not improve linearly by simply throwing more compute to the pool. There are two problems at scale. First, when millions of runs happen concurrently, the scheduler becomes the choke point, because bursts of writes flood the scheduler database and stall the whole process. Second, data files vary widely in size, which creates imbalanced workloads and leaves resources underused.
The fix addresses each wall directly:

We first staggered the start times of different batches so identical steps no longer finished in lockstep, smoothing the write bursts that overwhelmed the scheduler DB.

We also introduced a joint optimizer that uses bin-packing to split input batches into near-equal bytes. These splits are further broken down into chunks, processed in parallel by an optimal number of airflow workers. This approach ensures we stay within physical constraints, such as network and I/O throughput, node, storage, and database capacity.

episode-hours processed / week0100k200k300k400kFebMarAprMayJun10k14k103k288k387k440k

Figure 5: data processing throughput improvement over time. The series opens at 10k in February. 14k is where the original single-job pipeline plateaued, and it is that ceiling the rewrite lifted.

Together with the storage-level optimizations, these changes fully unlocked horizontal scaling and increased throughput from 14,000 to 440,000 episode-hours per week, a 31× improvement. That takes one million hours from about 16 months of processing down to under three weeks.

Curation: warehouse queries and memory-mapped tables

At a million hours, building the training manifest took about 48 hours before a training run could start.
Every training run begins by building a training manifest: exactly which episodes are in this run, and where each one starts and stops. Batching, sharding across GPUs, and epoch length all depend on it. And because each experiment filters the corpus differently- by task, by robot, by whether the run succeeded, by whether a camera dropped out- the manifest gets rebuilt every time.
Initially we built it from the files themselves. The metadata DB gave us a list of candidate paths, but a path alone is not a manifest. We still had to confirm each file was really there, read its sidecar for the quality flags, open its header for the time range, and then open it once more to count the steps it held. That is four trips to storage per episode, and our million-hour dataset is 43 million episodes. A single pre-training run can absorb that cost once. Paying it again for every experiment, however, becomes a significant bottleneck to iteration speed.
Our metadata DB does store this information. But it is also on the critical path of our data collection and annotation operations, and transactional databases are just not optimized to support large-scale columnar scans. So rather than push one database to be good at two opposed jobs, we split them by workload and keep them in sync with near-real-time change-data-capture:

Production DB — the transactional-heavy system of record, enforcing data integrity and completeness.

Data warehouse — the analytical backend, which enables horizontal scalability. A curation reads a few columns across the whole table rather than every row end to end. The episodes table has since passed 50 million rows with no change to how manifests are built.

The warehouse trails the production DB by seconds, which we can afford, because a curation selects over episodes that finished processing well before anyone trains on them.
A curation is now a SQL query. It writes the manifest as a columnar file. One rank downloads that file once, and every rank then memory-maps the local copy at the start of training, the downloading rank included. Building it became one query instead of tens of millions of lookups, and cold startup dropped from about 48 hours to under a minute. What changed is not just the constant. The query plans over a table instead of walking a file list, so its cost no longer tracks the number of episodes the dataset holds, and a curation over the full table, now past 50 million rows, comes back in a few seconds. (That is building the manifest. Loading it is a separate cost, and what Figure 7 measures.)
Time to build the training manifesthours of wall clock0102030405060BEFOREwalk every file, then count steps, before the run can start~48 hrsAFTERone query, then a copy to local disk, too small to plot here< 1 min

Figure 6: time to build the training manifest at one million hours

That fixed how the manifest is built. Loading it was a separate problem: it was fine at 100,000 hours, but at one million it broke down in two distinct ways:

Memory: a GPU node typically has around 2 TB of CPU RAM, and loading a one-million-hour manifest across all ranks exceeds the RAM ceiling and crashes the training job without finishing a single step.

Load time: the manifest sits on a network-backed mount where each read becomes its own request, and a columnar format's footer-first, scattered-seek access pattern is close to worst case for that, so a cold read costs far more than pulling the same bytes sequentially.

We addressed both with three changes that only work as a set:

Download once, not per rank: a single rank pulls the manifest with a direct, parallel transfer from the object storage API onto local node disk, bypassing the network mount entirely.

Map instead of read: every rank memory-maps that local copy as a columnar table, so the file stays on disk and only the pages actually touched become resident.

Shard on load: each rank takes a zero-copy slice covering its own 1/N of the rows, where N is the number of ranks. That only means anything on a mapped table. On an in-memory read you have already materialized the whole table before you can cut it, which is why the two ship as a single switch rather than independent options.

1. Manifest load timeseconds (cold read vs staged table)0200400600737.0read intomemory12.4memory-mapped2. Manifest memory per nodeGB resident per node05001000150020002151read intomemory218memory-mapped

Figure 7: load time and memory improvements for the million-hour training manifest

Together, these changes took the manifest off the startup critical path: building the manifest dropped from days of filesystem crawling to a single query, and loading it went from minutes to seconds of memory mapping.

Delivery: cluster-local cache on node NVMe

Our training data lives in cloud object storage. But our training clusters do not necessarily live next to it. Since the LLM boom, GPU capacity has been scarce enough that we take it wherever we can get it, which usually means several vendors at once.
Copying the corpus to each cluster is not a real option at that point. Our million hours of training data is PB scale, where even a simple copy becomes a serious undertaking:

The corpus is not static. Ingestion keeps extending it and corrections land after the fact, so every copy is one more thing to keep in sync.

Every copy costs. Its own storage bill, plus the egress to fill it, multiplied by each cluster we run on.

Not every vendor sells object storage. Most offer a parallel file system like Weka or VAST, a different product at a different price tier.

So the corpus stays in one place and the compute moves around it. But reading straight from cloud storage leaves the GPUs exposed to egress latency and packet losses. A short job can shrug off the occasional stall, whereas a run that has to hold throughput for weeks cannot afford any of them. The fix was to stop reading from the cloud during training and keep the working set on the cluster itself.
On-cluster data orchestration
The idea is to use the NVMe already sitting in the GPU nodes. A GPU node usually ships with plenty of it, and the cost is already in the price of the node, so the capacity is there whether we use it or not. We have used Alluxio's solution as our on-cluster caching layer since last year, for three reasons:

It is natively distributed. Compared to traditional distributed cache solutions, Alluxio removes the single point of failure on the control plane, and it fits multinode large-scale training very well.

It caches pages, not whole files. A worker holds only the parts of an episode that were actually read, so the same NVMe covers far more distinct episodes, and eviction drops a page at a time instead of a whole multi-gigabyte file. This is where the chunking work earlier pays off again: grouping topics so a sample touches few chunks means few pages go resident per sample.

Ownership is spread by hash. Each file's pages live on one worker, chosen by a consistent hash of its path, so with tens of millions of files the read traffic spreads evenly across the cluster instead of piling onto whichever node happens to hold a popular episode.

Building on top of their solution, we developed an on-cluster data orchestration service. Before training launches, the service resolves its manifest and warms the exact working set into cluster local storages. Training launches after the storage is warmed, and during training, cached data is served close to compute, while cache misses fall back to cloud storage.
Cloud storagedurable source of truthwarmsControl planeresolves the manifest · warms the working set before the job startsCLUSTER 1On-cluster cache orchestrationGPU node 1NVMe cacheGPU node 2NVMe cacheCluster NFSshared tierCLUSTER 2On-cluster cache orchestrationGPU node 1NVMe cacheGPU node 2NVMe cacheCluster NFSshared tiercache miss falls through

Figure 8: on-cluster data orchestration

Remote read speed depends mostly on how far the cluster sits from the bucket, and a single reader against object storage sustains roughly 200 MB/s. The bucket itself is not the bottleneck. Its aggregate bandwidth is enormous, but one connection pays a round trip per request, and a rank reading one file at a time is exactly one connection. The cache serves roughly 2 GB/s per node regardless, about ten times faster, and the NVMe underneath is quicker still, so the ceiling is the read path rather than the disk. That predictability mattered as much as the speed. Loading stayed uniform across clusters, held for weeks, and hid the mount differences, so training configurations stay cluster-independent. More on our multi-cloud architecture in a future report.
Time to read one petabytedays (single reader, one full pass)010203040506057.9Cloudstorage5.8Cluster-localcache

Figure 9: one pass over a petabyte, cloud storage versus cluster-local cache. Both bars are a single reader, so the ratio is the point rather than the elapsed days. A real run reads in parallel across every node.

Training: topology-aware optimizer sharding

Feeding the GPUs is one problem. What runs on them is another, and an optimization tuned at one scale does not necessarily survive the next. We made many changes on that side of the system, and rather than walk through all of them, here is the one that shows the pattern most clearly. Muon, our optimizer, was taking roughly half the wall-clock step time. So we split its state across every rank in the job, with each rank updating every Nth parameter and the results all-reduced afterwards. On a handful of nodes that worked very well.
Connecting many nodes together started to slow this down. Intra-node GPU to GPU communication is blazing-fast with NVLink, roughly 1.8TB/s per GPU on our B200 nodes. But separate nodes are connected via InfiniBand, which is more than an order of magnitude slower per GPU. We found that scaling up the number of nodes dramatically increases the slower inter-node comms.
Taking inspiration from FSDP Hybrid sharding:

Shard inside the node. Ranks split the work over NVLink using per-node NCCL sub-groups, so broadcast traffic never leaves the node.

Let nodes duplicate the work. Each node recomputes the same updates instead of exchanging them: more arithmetic, but no fabric traffic, and volume stops depending on node count.

Pick the strategy from the scale. At small node counts world-sharding is still faster, so the optimizer chooses by node count rather than committing to one.

Sharded across the jobbroadcasts grow with node countnode 1node 2node 3node 4every result crosses the inter-node fabricSharded inside each nodebroadcasts independent of node countNVLinkNVLinkNVLinkNVLinknode 1node 2node 3node 4traffic stays inside the node; nodes recompute the same update

Figure 10: optimizer sharding across the job versus inside each node

Once a job is big enough for the switch to kick in, the optimizer step runs about 3x faster on average than the fully-sharded design it replaced, and the gap grows as nodes are added, though the figure below measures a single node count. That only holds at scale, though. Small jobs have less inter-node traffic to begin with, so the original global sharding still wins there. The trainer therefore picks between the two at runtime, from the node count it sees.
1. Optimizer step timeseconds — median and mean2. Muon broadcastsper step, relative to hybridmedianmean0.00.20.40.60.620.62Unsharded0.140.14Hybrid0.180.47Fully shardedmean 2.6x mediancost is all tail~0UnshardedHybridNVLink7.6×Fully shardedInfiniBand

Figure 11: measured optimizer step cost by sharding strategy, at the node count where the switch triggers. Fully sharded looks competitive at the median but pays 2.6x that on the mean, because it moves 7.6x the broadcast traffic and moves it across InfiniBand rather than keeping it on NVLink. In synchronous training a slow step is paid by every rank, and wall-clock is the sum of all steps, so the mean is the number to compare.

Job resilience: preflight gating and auto-restart

When a job is small, a node dying isn't really a problem. You restart it and you've lost a few minutes. That changes once a run holds the fleet for weeks. The run only moves when every rank is healthy, so each machine you add is one more thing that can stall all the others. And when the job does go down, you lose everything since the last checkpoint. You will hit bad nodes, and most never show up in an alert:

Degraded GPUs that report healthy: the node passes every obvious check and jobs keep dying on it anyway. One of ours had logged over 250,000 corrected ECC errors before we connected the crashes to the hardware.

Drained nodes nobody resumes: one flaky driver query is enough to drain a healthy machine, and nothing pages you, because a drained node looks like a normal state. One sat idle about ten hours before we noticed we were short.

Stale GPU reservations: when a controller reboots mid-job, nodes come back idle but still holding their old reservations. Everything reports healthy, but the scheduler won't put work on them.

Not enough GPUs: a million-hour pre-training run would happily take the whole fleet for weeks, but everyone else still needs to train and evaluate while it runs. Supply is tight enough across the industry that no single vendor covers both, so we take capacity wherever it is free, which only works if we can stand a cluster up quickly.

So we build assuming some part of the fleet is broken at any given time:

Preflight checks: Before a job lands on a node, Slurm checks its GPU inventory, error counters, kernel log, local disk, and container runtime, and drains anything that fails. The tricky part was choosing which error counter to trust. Lifetime totals can't be cleared on some GPUs, so one bad stretch long ago would condemn a healthy node forever. We gate on the counters that reset at boot instead.

Auto-restarts: The job restarts its workers in place, and if the whole job dies Slurm requeues it and training resumes from the last checkpoint. Diagnostic traces are keyed to the attempt number so earlier ones survive, because otherwise you lose the evidence for why it died. Nodes drained on a transient come back on their own, and reservations stranded by a crashed controller are swept up on a timer.

One-click deployments: Cluster setup is a single Ansible playbook, with each cluster's differences kept in its own inventory. The clusters aren't alike: some vendors hand over machines with the scheduler and container stack already installed, others give us an image and nothing else. The same roles cover both, with the install steps switched off where the vendor already did the work. Standing up a new cluster is a config change now, and it takes days.

§ 3 Looking forward

Million-hour training is not a single scaling problem. Instead, it is an end-to-end vertical system re-design with different considerations at each stage. The lasting result goes beyond a single large training run. Ad-hoc scripts get one run out the door, then have to be written again for the next. Reusable infrastructure lets each experiment start where the last one finished, and that is what makes this scale repeatable rather than a one-off. It is why we built the foundation instead of scripting our way through: researchers can now ingest, curate, and experiment with orders of magnitude larger data without rebuilding the path each time, which accelerates our iteration cycles and widens the range of experiments worth designing.
Robotics will continue to place unusual demands on both research and infrastructure. If working on these systems sounds interesting, visit dyna.co/careers.

§ References

01

Foxglove, MCAP: a serialization-agnostic container file format for robotics. mcap.dev

02

Apache Software Foundation, Airflow: workflow orchestration for the ingestion DAGs. airflow.apache.org

03

Kubernetes: container orchestration, which ran the original single-job pipeline. kubernetes.io

04

Alluxio, Data orchestration for the cluster cache tiers. alluxio.io

05

Keller Jordan, Muon: an optimizer for hidden layers in neural networks. kellerjordan.github.io/posts/muon/

06

Liu et al. (Moonshot AI), Muon is Scalable for LLM Training — distributed Muon with ZeRO-1 style optimizer sharding. arxiv.org/abs/2502.16982

07

PyTorch, FullyShardedDataParallel. docs.pytorch.org/docs/2.13/fsdp.html

08

SchedMD, Slurm Workload Manager: scheduling, node health gating, and job requeue. slurm.schedmd.com

09

Red Hat, Ansible: playbook-driven cluster provisioning. ansible.com

[ Stay Updated ]

Our research straight to your inbox.

[ DYNA ]

Newsletter Signup

© 2026 DYNA Robotics Inc.