docs(interleaved): add image payload cache tutorial - #16
Open
VibhuJawa wants to merge 8 commits into
Open
Conversation
Interleaved corpora reference the same image many times. Measured on MINT-1T HTML, 1,582,193,028 image occurrences resolve to 356.0M unique images, so the average image is fetched 4.4 times per pass. Add an optional content-addressed PayloadCache and thread it through materialize_task_binary_content. Cached rows are removed from the image mask before dispatch, so the tar/range/direct paths are untouched and only genuine misses reach storage. Opt-in: without a cache the behaviour is byte-for-byte unchanged. Cache faults degrade to a miss rather than failing the pipeline.
Interleaved corpora reference the same image from many documents, so a materialization pass re-reads most images. On MINT-1T HTML, 1,582,193,028 image occurrences resolve to 356.0M unique images -- the average image is fetched 4.4 times per pass. Adds tutorials/interleaved/image_payload_cache/ showing how to route materialization through PayloadCache: a CachedMaterializeStage that passes a cache to materialize_task_binary_content, plus a README covering the reference-count distribution and measured hit rate by cache size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three issues found in review of the previous commit: - No shipped stage passed `cache=`, so the feature was unreachable without writing a custom stage. BaseInterleavedFilterStage now exposes `payload_cache` and forwards it. - `_store_in_cache` wrote once per row, so a key repeated inside a batch was written repeatedly. Write once per distinct key. - The docstring cited a 4.4x image re-reference rate without stating that entries are keyed by `source_ref`. That is the correct key when payloads are stored once and referenced many times (measured on MINT-1T HTML by counting distinct `source_ref` values), but it yields a zero hit rate for a corpus that stores a separate copy per sample. Say so.
An end-to-end measurement refuted part of the README's claims, and the tutorial branch carried a stale copy of the cache implementation. Merge the corrected feature branch in and rewrite the README around what was measured: - 4.4x is a SERIAL limit. Measured hit rate was 0.4720 at one worker -- exactly the static-oracle ideal -- and 0.0906 at 16 workers, because a worker that touches a key first cannot see a write an in-flight peer has not made yet. Key-affinity routing is what recovers it in a single pass; across passes the full multiplicity is realised. - Sizing figures replaced with the exact per-image-size reproduction: 1 TB 20.2%, 2 TB 31.0%, 3 TB 38.6%, 5 TB 48.9%. Size-aware (k-1)/size admission raises 2 TB to 46.4%. Note that PayloadCache implements neither admission nor eviction. - State the preconditions up front: the key is source_ref, the storage locator, so the cache only pays when the same locator recurs -- zero hit rate on a WebDataset that stores a copy per sample. And a hit costs 2.0 ms against a ~0.5 s object-store GET but loses to a 1.3 ms local packed-tar range read, so cache remote sources only. - Record why the tutorial keeps its own materialize stage: shipped filter stages accept payload_cache= but materialize into a scratch task and do not emit the bytes. Verified end to end on a synthetic corpus: a cold pass writes one cache entry per unique locator, and a second pass materializes every row with the source tar removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…stages The cache was reachable only by writing a bespoke stage: filter stages materialized into a scratch task purely to compute a keep-mask, and the writer called materialize_task_binary_content with no cache at all. Add build_payload_cache() and a payload_cache_root field on the filter and writer base classes. The root is a plain string so a stage stays picklable; the handle is built in setup() and is worker-local, never crossing the wire. Opt-in throughout: without a root, behaviour is unchanged.
…ache' into docs/image-payload-cache-tutorial
The writer now takes payload_cache_root, so the tutorial no longer needs a local materialize stage. Drops CachedMaterializeStage and materialize_on_write =False, leaving a two-stage pipeline with no tutorial-local code. Verified on a synthetic corpus: 18 image rows over 6 unique images wrote exactly 6 cache entries, and a second pass materialized all 18 after the source tar was deleted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Interleaved corpora store images by reference, and the same image is referenced by many documents. Materializing a corpus therefore fetches the same object repeatedly.
Measured on MINT-1T HTML:
Every unique image must be fetched at least once, so the ceiling on avoided fetches is
1 - 356.0M / 1,582,193,028= 77.5%. The distribution is skewed: 40.2% of images are referenced once and can never produce a hit, while a long tail referenced hundreds of times produces almost all of the savings.materialize_task_binary_contentalready deduplicates identical byte ranges within a task, but references to one image are spread across thousands of tasks and many workers, so the repeats survive.Cache design
PayloadCacheis a content-addressed directory keyed by thesource_refJSON locator. A hit returns the payload without touching the source; a miss falls through to normal I/O and stores the result.Two preconditions the README now states up front
The key is the locator, not the image. The cache pays off exactly as often as the same
source_refrecurs. That holds for a deduplicated store that keeps one copy per unique image and points many documents at it. It does not hold for a WebDataset-style layout that writes a separate copy into every sample's shard — no locator repeats and the hit rate is zero however often the image itself recurs.The source must be slower than the cache. Measured at a 140 kB mean payload on the shared filesystem (WekaFS), 16 workers: a
putcosts 10.5 ms and a hitget2.0 ms. Against a ~0.5 s object-store GET a hit is ~250x cheaper. Against a byte-range read into a local packed tar — 1.3 ms for the same payload — the cache is a net loss, because it replaces a few large batched range reads with many small whole-file reads. Cache a remote source; do not cache a fast local one.The measured concurrency caveat
4.4x is a serial limit, and a single concurrent pass gets a fraction of it. Measured end to end on a corpus whose static-oracle ideal hit rate was 0.4720:
This is not a defect in the cache: at one worker the measured hit rate matched the oracle to the last digit. A worker that touches a key for the first time simply cannot see a write an in-flight peer has not made yet, so the single-pass hit rate is governed by
partitions / workersrather than by reference multiplicity.What recovers it, in the README:
Sizing
Static-oracle hit rate for a full pass, exact per-image sizes, greedy admission by reference count:
Returns are sublinear, so size the cache against the cost of the fetches it removes, not against the corpus. Admitting by value density
(k-1)/sizeinstead of by reference count raises the 2 TB figure to 46.4% — worth roughly a 2.2x larger cache.PayloadCacheimplements neither admission nor eviction today: it stores every miss, so bound it with a filesystem quota and clear it between corpora.Contents
tutorials/interleaved/image_payload_cache/— amain.pyfollowing the existing interleaved tutorial conventions (RayClient, Pipeline, RayDataExecutor) whose only tutorial-local code is aCachedMaterializeStagethat hands aPayloadCachetomaterialize_task_binary_content, plus a README covering the preconditions, the reference-count motivation, the concurrency caveat, sizing, and run instructions.The tutorial keeps its own materialize stage on purpose: shipped filter stages take
payload_cache=directly, but they materialize into a scratch task to compute their keep-mask and do not emit the bytes, so a pipeline that must both filter and write payloads still needs a materialize stage of its own.Verification
Run end to end on a synthetic four-partition corpus (8 unique images, 24 references, real tar member offsets): the cold pass wrote exactly 8 cache entries and materialized all 24 image rows with zero errors, and a second pass materialized all 24 rows with zero errors after the source tar was removed — every payload came from the cache.
ruff checkandruff format --checkpass on the tutorial.pytest tests/stages/interleaved -m "not gpu"is 291 passed / 6 skipped / 4 failed, where the 4 failures are pre-existing loguru-to-caplogpropagation failures intest_multimodal_reader.pyandtest_schema_utils.py, in files this branch does not touch.🤖 Generated with Claude Code