[Data] Estimate MCAP in-memory size from sampled messages, not file size - #65912
[Data] Estimate MCAP in-memory size from sampled messages, not file size#65912marwan116 wants to merge 5 commits into
Conversation
MCAPDatasource did not override estimate_inmemory_data_size(), so it inherited FileBasedDatasource's sum of on-disk file sizes. The value returned is not an approximation of the in-memory size, it is exactly the file size: 14,302 B against 1,188,720 B materialized, an 83x understatement on a 360-message recording. MCAP chunks are compressed, and the datasource materializes per-message columns -- topic, timestamps and, when include_metadata is set, the schema -- that have no per-message on-disk equivalent. Ray Data derives both the read parallelism and the memory it provisions for the read from this number. Sample a bounded number of files (1%, clamped to [2, 10], evenly spaced so that size-ordered inputs do not bias the sample), read at most 100 messages from each through the datasource's own conversion path, and scale the total on-disk size by the measured in-memory to on-disk ratio, following the ParquetDatasource precedent. Sampled files are opened seekable so that make_reader returns a SeekingReader and reads the summary from the footer index rather than rescanning the file. When the per-file iteration finishes before reaching the message cap it has seen the whole selection, so the measured size is used unscaled. Without that, a time_range selecting 1% of a recording is estimated as if the whole file were selected, since the summary cannot express a time range. Gated on DataContext.decoding_size_estimation. Every failure path falls back to a default ratio of 5 and logs a warning, since a silent fallback would restore the behaviour this change exists to replace. Also fixes a pre-existing assertion in test_read_mcap_invalid_time_range that can never match: the regex omits the values the error message interpolates. It fails on clean master, and is not caught in CI because mcap appears in no requirements file, so the whole module is skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marwan Sarieddine <sarieddine.marwan@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request implements in-memory size estimation for the MCAP datasource in Ray Data by sampling files and calculating an encoding ratio, addressing the issue where compressed MCAP files are significantly underestimated. It also adds comprehensive unit tests to validate this estimation logic. The review feedback correctly identifies a bug where an estimated size of zero is ignored due to a truthiness check, and suggests using an explicit is not None check instead.
| if in_memory_size: | ||
| ratios.append(in_memory_size / file_size) |
There was a problem hiding this comment.
Using if in_memory_size: will evaluate to False when in_memory_size is 0 (which is a valid estimated size when a filter selects zero messages). This causes the estimator to ignore valid zero-size estimates and potentially fall back to the default ratio of 5. Changing this to if in_memory_size is not None: correctly handles 0 as a valid size.
| if in_memory_size: | |
| ratios.append(in_memory_size / file_size) | |
| if in_memory_size is not None: | |
| ratios.append(in_memory_size / file_size) |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7419b3f. Configure here.
`_estimate_file_inmemory_size` returns None when a file cannot be sampled and 0 when the topic, time or message-type filter selects nothing from it. The caller tested truthiness, so it discarded the measured zero alongside the failures. A mixed input biased the mean ratio upward, and an input where every sampled file is empty under the filter fell back to the default ratio of 5 for a read that returns no rows at all. The ratio lower bound still floors the result at 1, so the empty case now estimates the on-disk size rather than five times it. Reported independently by gemini-code-assist and Cursor Bugbot on #65912. Also document the two properties of the MCAP format this module depends on -- the summary section sitting at the end of the file, addressed by an offset in the footer, and schemas being stored once per file rather than once per message -- and link the format specification. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marwan Sarieddine <sarieddine.marwan@gmail.com>
…count
The sample window is the first 100 messages a file yields, and its mean row
size was scaled by the file's whole message count. On multi-topic data that
makes the estimate depend on how many messages of each topic happen to land
in the window rather than on the file.
Measured on a 105-second FAST-LIVO ROS 2 recording (24,456 messages: a
200 Hz Livox IMU, a lidar, and two compressed camera streams), the window
spans 0.417s. The three heavy topics each hold a 4.3% share of the file, so
the window catches five of each where the file average is 4.3 -- one message
of quantization on the topics carrying nearly all of the bytes. That alone
put the estimate 18% over.
Weight per channel instead, using the exact per-channel counts the summary
already carries. Only a channel's row size is sampled, never the mix between
channels. Messages within a channel are near-uniform in size while channels
differ by orders of magnitude, so this removes the dominant error term.
corpus truth before after
8 zstd shards, 27x gradient 770.8 MB +2.9% +0.9%
single 845 MiB recording 946.4 MB +18.0% +2.6%
The estimate is also no longer sensitive to the window size: per-channel
lands at 102.6% of truth for every window from 10 to 800 messages, where
scaling by the total count ranges from 228% to 104% across the same span.
A channel absent from the sample falls back to the mean row size across the
whole sample, and a summary carrying no per-channel breakdown falls back to
the previous whole-sample scaling.
Reported by a reviewer testing the branch against real ROS 2 recordings
rather than synthetic files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marwan Sarieddine <sarieddine.marwan@gmail.com>
Two defects in the sampled estimate, both surfacing only when a filter
selects a small part of a file. A reviewer found them on a video-dominated
robotics episode; neither existing test could reach them.
The encoding ratio was floored at
MCAP_ENCODING_RATIO_ESTIMATE_LOWER_BOUND = 1. That guard exists so a
decompressed file is never reported as smaller than its compressed form.
Under a filter the quantity is not a decompression ratio at all -- it is
selected in-memory bytes over *total* on-disk bytes -- and on a recording
whose bytes are nearly all camera frames, reading only the state topics is
legitimately far below 1. The per-channel estimator computed the right
number and the clamp discarded it, reporting the whole file. Apply the
floor only when no filter narrows the read.
Separately, once sampling stopped at the message cap, the extrapolation
used whole-file per-channel counts. The summary cannot express a time
range, so a one-second window on a twenty-second recording was scaled by
twenty seconds of messages -- an error of exactly the ratio of the two
durations, and unbounded as windows shrink. Scale each channel's count by
the share of the file's span the window covers, which the summary does
carry as message_start_time/message_end_time. That assumes a roughly
constant publish rate per channel, which is what a sensor recording does.
Measured on a 21.8 MB episode (2 cameras of incompressible frames, 200 Hz
proprio, 50 Hz action), estimate over actual:
no filter 1.00x -> 1.00x
state topics only 24.30x -> 1.00x
state topics + 1 s window 485.90x -> 1.00x
A filter selecting nothing now estimates zero rather than the file's
on-disk size, completing the zero-sample fix in 195bee3: the floor was
what stopped the measured zero from being used.
New tests cover both, on a fixture with camera-dominated payloads and a
window above the sampling cap -- the regime the previous time-range test
could not reach, since its window selected fewer messages than the cap and
so was measured outright rather than extrapolated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marwan Sarieddine <sarieddine.marwan@gmail.com>
`MCAP_ENCODING_RATIO_ESTIMATE_LOWER_BOUND` was used only by the assertion that a filter selecting nothing floors at the on-disk size. That behaviour changed in the previous commit -- nothing selected now estimates zero -- so the import went stale with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marwan Sarieddine <sarieddine.marwan@gmail.com>

Description
MCAPDatasourcedoes not overrideestimate_inmemory_data_size(), so it inheritsFileBasedDatasource's implementation, which sums on-disk file sizes. The value returned is not an approximation of the in-memory size — it is exactly the file size:Two effects compound. MCAP chunks are usually compressed, so the file is smaller on disk than in memory. And the datasource materializes per-message columns — topic, timestamps and, when
include_metadatais set, the schema — that have no per-message on-disk equivalent at all. Ray Data derives both the read parallelism and the memory it provisions for the read from this number, so both are wrong for every MCAP read.Fix. Follow the
ParquetDatasourceprecedent: sample a bounded number of files (1% of the input, clamped to[2, 10], evenly spaced so size-ordered inputs don't bias the sample), read at most 100 messages from each, build a block through the datasource's own_message_to_dict+DelegatingBlockBuilderpath, and scale the total on-disk size by the measured in-memory to on-disk ratio.Accuracy after the change (actual / estimate), across shapes beyond the repro:
0.97x–1.30x, against 83x before.
Related issues
Related to #65640, #65641 and #65787 — none of them duplicates this. #65787 (
batch_size) modifies__init__and_read_streamonly; #65640 and #65641 concern block granularity and batched reads. No open issue or PR coversestimate_inmemory_data_sizefor MCAP.Additional information
Two MCAP-specific implementation details worth review.
Measure a materialized block rather than undoing compression. The chunk index reports a ~37x decompression ratio, but the true expansion is 83x — the gap is the per-row metadata columns. Building a real block keeps the estimate correct automatically as
include_metadata,include_pathsand filters change.Open sampled files seekable.
_open_input_sourceyields a non-seekableBufferedInputStream, which makesmake_readerreturn aNonSeekingReaderwhoseget_summary()rescans the whole file and is single-use. Sampling usesopen_input_filesomake_readerreturns aSeekingReaderand reads the summary from the footer index. Message counts come fromstatistics.message_count; a topic filter is resolved exactly viachannel_message_counts.Filters the summary cannot express. If the per-file iteration finishes before reaching the 100-message cap, it has seen the whole selection, so the measured size is used unscaled rather than extrapolated through the summary's message count. This matters because
time_rangeandmessage_typesare not representable in the summary. Without it, atime_rangeselecting 1% of a recording is estimated as if the whole file were selected:Failure behaviour. Gated on
DataContext.decoding_size_estimation. Every failure path falls back to a default ratio of 5 and logs a warning — over-estimating is the safe direction, consistent with the comment in_estimate_files_encoding_ratio, but a silent fallback would quietly restore the behaviour this change exists to replace.Two bounded approximations remain, both erring high. Messages are sampled as a prefix of each file rather than spread through it, so a recording whose message size changes markedly from start to end is estimated from its opening; the sample across files is spread, so this is a per-file effect. And
MCAP_ENCODING_RATIO_ESTIMATE_LOWER_BOUNDfloors the ratio at 1, which binds under a filter narrow enough that the selected rows are smaller than the file on disk — the 1.7x above.Drive-by fix.
test_read_mcap_invalid_time_rangeasserts on the regex"start_time must be less than end_time", but the code raisesf"start_time ({self.start_time}) must be less than end_time ({self.end_time})". The pattern can never match, and the test fails on clean master. It is not caught in CI becausemcapappears in no requirements file in the repo, sopytestmark = skipif(not MCAP_AVAILABLE)skips the entire module. That gap is worth addressing separately and I'm happy to file it; the one-line assertion fix is included here because this PR adds 10 tests to the same file.Tests
28 passed. Baseline on master is 1 failed, 17 passed.
New tests cover: estimate exceeds on-disk size, estimate tracks
include_metadata, estimate respects a topic filter, multi-file aggregation, files with no summary statistics, sampling disabled viaDataContext,include_paths(which adds a column downstream of_read_stream, so sampling has to add it too), a narrowtime_range, log-time ordering (the fixture is written in descending log time, so passing requires the reader's sort rather than the file layout), and binary payload round-trip.Lint:
black,ruffclean on both changed files.Self-contained repro (needs only
ray[data]andmcap)AI assistance (Claude) was used for this change. I reviewed every changed line and ran the tests locally.