Back to Benchmarks
Throughput Benchmark

Loader Throughput:
knonik vs lance vs LeRobot

A GPU can only train as fast as the data reaches it. On robot datasets that data is video, and getting it into a training step means finding the right frames, decoding them, and moving them onto the card, thousands of times a second. This benchmark measures how many frames each storage format and loader can actually deliver, on three real datasets, and what each one costs in memory and disk space along the way.

Reproduce this benchmarkgithub.com/knonik/benchmark_throughput
1.28-1.56×
faster than lance
7.8-12.3×
faster than lerobot
1.5-1.8× smaller
on disk than lance
up to 18.6× smaller
on the storage profile
Fig 1: decoded frames/s onto the GPU, higher is better
Fig 1: decoded frames/s onto the GPU, higher is better

The headline result. Pale bar: the loader alone. Saturated bar: the loader plus the copy onto the GPU, which is what a training step actually waits for. Whiskers show the fastest and slowest of the three repeats. One panel per dataset, because the batch size differs between them.

Overview

What the Benchmark Represents

Training a robot policy means repeatedly asking for a short clip: a handful of frames spaced a few steps apart, together with the robot state and the actions that follow. Those clips are drawn in shuffled order from across the whole dataset, so the loader is doing random access into compressed video, over and over, for the entire run. If it cannot keep up, the GPU sits idle and the extra hardware buys nothing.

Most storage benchmarks do not measure this. Bytes per second off disk does not capture it, because the formats differ in how much work they must do after the read. Rows per second does not capture it either, because a row is a different amount of pixels in each format. What matters is decoded, correctly windowed frames per second, in GPU memory.

So the measured quantity here is the training loop with the model taken out: fetch a batch, move the pixels to the GPU, wait for them to land, repeat. No forward pass, no backward pass, no optimizer. The goal is the ceiling the data path imposes on any model you might put on top of it.

Each loader is driven through its own public API: knonik through ShardedDataLoader, lance through lance.dataset(...).take() inside a torch DataLoader, LeRobot through LeRobotDataset(delta_timestamps=…). There is no shared abstraction layer, so what is being measured is a property of the loaders rather than of the harness.

Data

The Three Datasets

Chosen to span an order of magnitude in frame size, because the ranking of a data path is not scale-invariant: what wins at 180×320 is not automatically what wins at 720p.

DatasetFramesCamerasResolutionFPSBatchSourceSource media
droid_100 (180×320)32,2123 stored / 1 read180 × 3201564DROID stills
thread_velcro (480×640)34,3001480 × 6405064LeRobot aloha_static_thread_velcro stills
so101-task2 (720p)33,8011720 × 1280308LeRobot so101-task2-720p HEVC video

droid_100 stores three cameras but the benchmark reads one: reading three would triple the pixels per sample and make the dataset incomparable with the single-camera ones. so101-task2 runs at batch 8 rather than 64.

Backends

The Five Configurations

Three knonik configurations, two external baselines. The two knonik storage profiles are a real trade, and the third knonik row exists purely as an internal control.

knonik KDELTA zctraining_compressed profile
API used

ShardedDataLoader, decode_mode=random_access, zero-copy on

The throughput-oriented storage profile, read in zero-copy mode. Bigger on disk than SHDR, still smaller than lance, and the fastest configuration measured here.

knonik KDELTA clstraining_compressed profile
API used

ShardedDataLoader, decode_mode=random_access, zero-copy off

The same data through the same loader with zero-copy switched off. It is here so the effect of the zero-copy path can be read separately from the effect of the storage profile.

knonik SHDR zccompact_video profile, gop 5
API used

ShardedDataLoader, decode_mode=random_access, zero-copy on

The storage-oriented profile: gop-5 video, far smaller on disk. A random read has to touch a whole group of pictures, and that is the price it pays for the size.

lance.lance table, JPEG q95 blobs + numeric columns
API used

lance.dataset(...).take() inside a torch DataLoader, imagecodecs JPEG decode in the worker

Windowing is done over the table's own (episode, step) index; column roles are resolved from the table schema, so a differently-named table still runs.

lerobotLeRobot v3.0 directory, videos untouched
API used

LeRobotDataset(delta_timestamps=…) inside a torch DataLoader

Its own public API with its own delta_timestamps, exactly as a LeRobot training script would use it. It returns float32, which is 4× the pixel bytes of every other backend.

Methodology

The Measurement Contract

Identical for every backend. Anything a backend cannot honour is disclosed rather than quietly relaxed.

Sample shape
T = 4 frames at stride 5: frames 0, 5, 10, 15 of a 16-frame span
Numeric streams
state at the same 4 offsets, plus a 20-step action window
Batch
64 (so101-task2: 8)
Workers
8 (lerobot: 4, see handicaps)
Steps
200 per repeat, first 30 discarded as warm-up
Repeats
3 per backend; the middle result is published and the fastest and slowest become the error bars
Pinned memory
on, every backend
Shuffle
global, seed 3072
GPU
NVIDIA RTX 4070 Ti

Two modes, and why both exist

pure mode runs three repeats with no instrumentation beyond the timer. It is where the headline throughput numbers come from, and the published value is the middle of the three, with the difference between the fastest and slowest printed beside it. The middle result rather than the average, because a single slow repeat, a background process or a thermal blip, should not move a published number.

metrics mode runs once with a sampler walking /proc twice a second for the parent process and every descendant, recording resident memory and /dev/shm. That sampler costs a few percent of throughput, which is why memory and speed are never read off the same run.

Fairness

Handicaps, Stated Up Front

lerobot runs at 4 workers, not 8

LeRobot returns float32, so one batch carries 4× the pixel bytes of every other backend: 943.7 MB against 235.9 MB on thread_velcro. With pinned memory on, in-flight batches cannot be swapped out, and at 8 workers it repeatedly exhausted a 62 GB host. Fewer workers is the only knob that bounds it without changing the batch and window contract. It is a handicap in lerobot's favour, and it is disclosed on every figure.

Results

Throughput: decoded frames/s onto the GPU

End to end: decoded, copied to the device, and confirmed to have arrived. Middle of 3 repeats, pure mode. Higher is better.

frames/s, end to end (loader plus the copy onto the GPU)

Backenddroid_100 (180×320) f/sthread_velcro (480×640) f/sso101-task2 (720p) f/s
knonik KDELTA zc18,4315,9231,398
knonik KDELTA cls11,1182,299709
knonik SHDR zc3,5782,5381,021
lance13,5373,7951,090
lerobot1,680757113

frames/s, loader only (before the copy onto the GPU)

Backenddroid_100 (180×320) f/sthread_velcro (480×640) f/sso101-task2 (720p) f/s
knonik KDELTA zc21,9667,7561,686
knonik KDELTA cls14,1873,152970
knonik SHDR zc3,7062,8501,169
lance15,1334,5261,313
lerobot1,767870120

samples/s: complete T=4 windows, the unit a training step consumes

Batch 64, except so101-task2 at 8.

Backenddroid_100 (180×320) samp/sthread_velcro (480×640) samp/sso101-task2 (720p) samp/s
knonik KDELTA zc4,6081,481349
knonik KDELTA cls2,779575177
knonik SHDR zc895634255
lance3,384949272
lerobot42018928
droid_100 (180×320)
1.36×
knonik KDELTA zc vs lance
vs lerobot11.0×
thread_velcro (480×640)
1.56×
knonik KDELTA zc vs lance
vs lerobot7.8×
so101-task2 (720p)
1.28×
knonik KDELTA zc vs lance
vs lerobot12.3×

knonik KDELTA (zero-copy) leads on frames/s and samples/s on all three datasets. The one place a knonik row loses is SHDR on droid_100. That is the storage profile paying for its 10.2× smaller footprint at the smallest frame size in the set, and it is discussed below rather than hidden.

Figures

The rest of the plots produced by bench/native_plots.py, in the order they are generated.

Fig 2: throughput in samples/s
Fig 2: throughput in samples/s

The same run counted in complete T=4 windows, which is the unit a training step actually consumes, rather than in frames. Batch 64 everywhere except so101-task2 at 8.

Fig 3: throughput relative to lerobot (=1.0)
Fig 3: throughput relative to lerobot (=1.0)

Every backend divided by the slowest common baseline, on a linear axis so bar length stays proportional to the value it encodes. knonik KDELTA zc runs off the top of the plotted range on droid_100 and so101-task2, at 11.0× and 12.3×.

Fig 4: host memory during the measured window
Fig 4: host memory during the measured window

Saturated bar: maximum resident set of the parent process plus every descendant, sampled twice a second over the timed window. Pale bar: mean /dev/shm occupancy.

Fig 5: on-disk footprint
Fig 5: on-disk footprint

kB per frame per camera, so datasets with different frame counts and camera counts are directly comparable. Normalising by frames × cameras is what stops a dataset that stores three cameras from looking three times more expensive per frame than it is.

Fig 6: speed vs storage
Fig 6: speed vs storage

Throughput against bytes on disk, storage on a log axis. The upper-left corner is the desirable one. This is the figure that makes the KDELTA / SHDR choice concrete: SHDR moves left, KDELTA moves up, and both stay left of lance.

Fig 7: throughput under the memory sampler
Fig 7: throughput under the memory sampler

The same measurement with the memory sampler running and only one repeat. It is here to show the instrumentation cost: throughput drops by a few percent on every backend and the ordering does not change.

Fig 8: samples/s in metrics mode
Fig 8: samples/s in metrics mode

The metrics-mode counterpart of Fig 2.

Fig 9: per-batch wall time in metrics mode
Fig 9: per-batch wall time in metrics mode

Each bar is one batch of wall time in metrics mode, split into the coloured loader segment and the grey host to device copy.

Full Results

Every Measured Row (pure mode)

The complete contents of results/native_results.csv for pure mode: throughput, where the time goes inside a batch, bytes moved, dtype, and the spread across repeats.

DatasetBackendBatchWorkerssamples/sf/s loaderf/s e2eMB/sstep msloader msH2D msH2D %pix MB/batchdtypespread %
droid_100 (180×320)knonik KDELTA zc6484607.621,96618,4313,18513.911.72.21644.2uint84.9
droid_100 (180×320)knonik KDELTA cls6482779.414,18711,1181,92123.018.05.02244.2uint811.8
droid_100 (180×320)knonik SHDR zc648894.53,7063,57861871.569.12.5344.2uint84.5
droid_100 (180×320)lance6483384.215,13313,5372,33918.916.92.01144.2uint83.3
droid_100 (180×320)lerobot644420.11,7671,6801,161152.4144.97.55176.9float323.3
thread_velcro (480×640)knonik KDELTA zc6481480.77,7565,9235,45943.233.010.224235.9uint81.1
thread_velcro (480×640)knonik KDELTA cls648574.73,1522,2992,119111.481.230.127235.9uint87.8
thread_velcro (480×640)knonik SHDR zc648634.42,8502,5382,339100.989.811.011235.9uint81.6
thread_velcro (480×640)lance648948.84,5263,7953,49867.556.610.916235.9uint86.8
thread_velcro (480×640)lerobot644189.38707572,792338.0294.243.913943.7float323.9
so101-task2 (720p)knonik KDELTA zc88349.41,6861,3983,86522.919.03.91788.5uint88.9
so101-task2 (720p)knonik KDELTA cls88177.49707091,96245.133.012.12788.5uint83.4
so101-task2 (720p)knonik SHDR zc88255.31,1691,0212,82331.327.44.01388.5uint83.5
so101-task2 (720p)lance88272.41,3131,0903,01329.424.45.01788.5uint810.6
so101-task2 (720p)lerobot8428.41201131,254282.2267.314.95353.9float322.7

H2D is the host to device copy: the time spent moving the decoded batch onto the GPU, and its share of the whole step.

frames/s in metrics mode

Single repeat, memory sampler running. Every backend loses a few percent; the ordering is unchanged.

Backenddroid_100 (180×320) f/sthread_velcro (480×640) f/sso101-task2 (720p) f/s
knonik KDELTA zc17,7935,8571,259
knonik KDELTA cls10,9772,204617
knonik SHDR zc3,6762,593836
lance13,7743,7001,057
lerobot1,683797107
Host Memory

What Each Loader Costs in RAM

Sampled twice a second over the timed window across the parent process and every descendant. All columns in MB. Growth is the last fifth of the window minus the first fifth: a positive number means the loader was still accumulating memory when the measurement ended.

DatasetBackendMax total RSSMean total RSSMean parentMean workers/dev/shmProcsGrowth
droid_100 (180×320)knonik KDELTA zc2,2801,9631,4814812,1599+1593
droid_100 (180×320)knonik KDELTA cls2,8552,5721,7078661,3485+713
droid_100 (180×320)knonik SHDR zc2,5712,4812,0074742,2829+185
droid_100 (180×320)lance7,5844,9721,4163,55689+5711
droid_100 (180×320)lerobot10,1939,2394,9974,242125+1558
thread_velcro (480×640)knonik KDELTA zc7,3316,2535,2041,04912,0749+4217
thread_velcro (480×640)knonik KDELTA cls11,2509,9775,9953,9827,1885+3673
thread_velcro (480×640)knonik SHDR zc10,36110,0059,47553012,2269+1411
thread_velcro (480×640)lance14,02210,3254,8515,4741299+8148
thread_velcro (480×640)lerobot31,42427,85019,7408,1112825+6021
so101-task2 (720p)knonik KDELTA zc3,9753,3492,3589919,8399+1639
so101-task2 (720p)knonik KDELTA cls6,5845,5403,0372,5028,0875+2215
so101-task2 (720p)knonik SHDR zc5,1944,1763,5016759,8949+254
so101-task2 (720p)lance9,1785,9241,8804,04449+6683
so101-task2 (720p)lerobot16,83715,14210,9264,215505+2823

Peak memory of the parent process alone is deliberately not published: it is a process-lifetime high-water mark, so a backend measured later in the same process would inherit the peaks of the ones before it. Everything above comes from the sampler instead. Note also that knonik's /dev/shm figures are shared, file-backed and reclaimable memory, which is a materially different cost from anonymous per-worker memory.

Storage

On-Disk Footprint

Apparent size of each dataset directory, normalised by frames × cameras so the three datasets are comparable. knonik and lance figures include the numeric columns; lerobot is the whole dataset directory.

DatasetFormatTotal on diskB / frame / camkB / frame / camvs lance
droid_100 (180×320)knonik KDELTA1.49 GB15,43415.11.5× smaller
droid_100 (180×320)knonik SHDR g50.21 GB2,2192.210.2× smaller
droid_100 (180×320)lance2.19 GB22,63722.11.0×
droid_100 (180×320)lerobot0.47 GB4,8714.84.6× smaller
thread_velcro (480×640)knonik KDELTA1.20 GB35,04634.21.6× smaller
thread_velcro (480×640)knonik SHDR g50.17 GB5,0074.911.0× smaller
thread_velcro (480×640)lance1.88 GB54,89553.61.0×
thread_velcro (480×640)lerobot0.27 GB7,8537.77.0× smaller
so101-task2 (720p)knonik KDELTA2.90 GB85,70283.71.8× smaller
so101-task2 (720p)knonik SHDR g50.29 GB8,4778.318.6× smaller
so101-task2 (720p)lance5.32 GB157,358153.71.0×
so101-task2 (720p)lerobot0.03 GB9550.9164.8× smaller *

* so101-task2 ships as HEVC video, which LeRobot stores as-is while knonik and lance both re-encode. That row compares codecs rather than containers.

Reproduce

Run It Yourself

The harness is MIT-licensed, standalone, and public. It runs in whatever environment you activate, it detects the datasets it finds on disk, and a backend that does not import is reported as unavailable rather than quietly dropped from a chart. The repository has the install steps, the conversion scripts for each dataset, and every command used to produce the numbers on this page.