How to measure this without being wrong
The pitfall taxonomy is Raasveldt, Holanda, Gubner & Mühleisen, Fair
Benchmarking Considered Difficult (DBTest'18). Each section below names the
pitfall, then what measurement/baselines does about it — including where it
still falls in.
§3.2 Failure to optimize the baseline
The most common way a format comparison lies, and the one our Parquet baseline currently falls into.
Parquet's random-access throughput moves by roughly 64× on row-group and
page configuration alone: ~5,500 rows/sec at defaults against ~350,000 with
8 KiB pages (Lance, VLDB 2025 — [summarised], verify before quoting). Our
baseline used defaults with a row group of 1,000. That alone could account
for most of the measured gap.
The rule: sweep the tuning knobs on both sides, or state plainly that you did not and that the baseline is therefore a floor rather than a fair fight.
FFCV's paper is the precedent for handling this honestly when a full sweep is out of reach — it concedes "we compare FFCV to the standard PyTorch dataloader rather than specialized solutions like NVIDIA DALI" rather than implying it beat them.
§3.3 Apples vs oranges
MCAP is on our table and should not be there in that form. Its own evaluation document (Foxglove, 2021-10-06) says:
"A recording format should be optimized for write throughput first and foremost."
Its published benchmarks measure write throughput on a ramdisk and contain no random-access measurement at all. Comparing a read-optimised columnar layout against an append-optimised container on shuffled reads is this pitfall by definition.
The repair is not to drop it but to label it: the incumbent recording format in robotics, which was not designed for this access pattern. That is a fair and interesting comparison, and stating the asymmetry is what makes it fair.
§3.5 Cold, warm and hot runs
Our timings are warm — every target reads bytes already resident. That was a deliberate choice to isolate the layout, and it is stated in the harness.
But cache state changes rankings, not just magnitudes. Mohan et al. found page-cache thrashing causing "85 % of the dataset fetched from storage every epoch" when only 65 % should have been, which reorders formats rather than scaling them.
A file larger than page cache could not be written on the machine that ran this — 63.8 GiB of RAM against 38.5 GiB of free disk — so bytes fetched was counted instead, as a cache-independent proxy. That is a reasonable substitute and it is not a cold measurement; see the next section for why the proxy is weaker than it looks.
Bytes fetched is not a cost function
Not in Raasveldt's list, and it should be. Below a filesystem's block and readahead threshold — "in almost all system, this lies below 2MB" (FFCV) — fetching fewer bytes costs the same as fetching more. Random 4 KiB SSD reads run at 200–900 MB/s against multi-GB/s sequential (AIStore).
A 1,000× advantage in bytes that yields 1.2× in wall clock is a common and real outcome. On object storage the currency is requests rather than bytes at all: Zeng et al. found ORC issuing "≈4× S3 GET than Parquet", which reverses the SSD ranking.
The rule: report an I/O operation count beside the byte count, and report wall clock through a real loader. Bytes alone is a structural argument, not a performance claim, and should be described as one.
Take rate is an axis, not a constant
Our benchmark reads 10,000 of 100,000 samples — 10 % selectivity — and calls it random access. At 10 %, any chunked format touches essentially every chunk, so what is measured is per-chunk read amplification rather than random-access capability.
Lance's evaluation avoids this deliberately: 256 random indices against
datasets of one billion rows, with the stated reason that "benefits of
coalesced access are easily overstated in benchmarking and less frequent in
real-world use cases" ([summarised]).
The rule: sweep selectivity across orders of magnitude — 0.01 %, 0.1 %, 1 %, 10 % — and scale the dataset until chunk-granularity effects can appear. A result that holds at 10 % on a large dataset is strong; one measured only at 10 % on a small one cannot distinguish the layout from the chunk size.
§3.1 Non-reproducibility, and §3.7 incorrect code
Publish the artifact. Every credible comparison in this area does:
msr-fiddle/DS-Analyzer (Mohan), XinyuZeng/EvaluationOfColumnarFormats
(Zeng), scDataset/scDataset. measurement/baselines is in this repository
for that reason.
Check that every implementation returns the same answer. The harness computes an index-weighted checksum per target and prints every target's, not only the failures — a reader that returned the right blocks in the wrong order disagrees rather than passes, and a line that appears only on failure cannot be told from a check that never ran.
Two errors were caught this way before any number was quotable, and both had
flattered CA3: the Parquet target opened the file inside the random loop
(10,000 open() calls, reported as 6.9 s), and MCAP's writer defaulted to zstd,
putting its file below the raw tensor size. Both are recorded in the harness
rather than silently fixed.
§3.6 Ignoring preprocessing
Our scan and random columns measure reaching a sample, not copying it
out. A loader that copies the whole block pays the same memcpy in every row.
This matters more than it sounds, because prep frequently dominates fetch. Mohan et al. separate the two and found that even fully cached, 8-GPU ResNet18 spent "65 % and 50 % of the epoch time on prep stall" under TensorFlow and MXNet.
A format whose samples need little decode has a real advantage here — a §5.8 block form is a memcpy into a tensor — and that is worth measuring directly rather than assuming.
End-to-end, or it did not happen
Every credible comparison ends with a wall-clock training time or an accuracy number, not a micro-benchmark: FFCV, CorgiPile, scDataset, AIStore, Mohan et al. AIStore states the aim explicitly — "to measure the end-to-end performance of the whole (compute + storage) system and to avoid common DL-benchmarking pitfalls."
The route to a number someone else designed
DLIO accepts a custom data loader as a plugin. Implement BaseDataLoader,
place it in the plugins directory, and configure:
data_loader_classname: dlio_benchmark.plugins.experimental.src.data_loader.pytorch_custom_data_loader.CustomTorchDataLoader
data_loader_sampler: index # map-style random access
# data_loader_sampler: iterative # sequential streaming
Then run the bundled unet3d_h100, cosmoflow_h100 and resnet50_h100
workloads with CA3 substituted, reporting accelerator utilisation against the
thresholds MLPerf Storage publishes — 90 % for UNet3D and ResNet50, 70 % for
CosmoFlow.
That converts a self-designed benchmark into a self-run instance of a benchmark MLCommons designed and audits. It is the single highest-leverage change available, and MLPerf Storage's OPEN division exists to accept exactly this kind of submission with full disclosure.
Note what those workloads actually model, because it is not what we measured:
whole-file random access at multi-megabyte records (unet3d: 168 files, one
sample each, ~146 MB), or sequential streaming inside shards (resnet50: 1,024
files, 1,251 samples each, no shuffle keys set). Fitting CA3 to them is itself
informative.