Repository navigation
[PoC] feat: read Vortex files through t4, a memory and SSD cache of object stores - #10954
XiangpengHao wants to merge 5 commits into
Conversation
… to 2 MiB A dictionary closes once its values reach `max_bytes` before compression. At 1 MiB, high-cardinality string columns close one every few tens of thousands of rows, so each column's bytes are laid out in runs of a few hundred KiB between other columns' segments, and a scan of one column with object-store coalescing also reads the segments in between. Raise the default to 2 MiB, and let `WriteStrategyBuilder` set it with `with_dict_max_bytes`. Signed-off-by: Xiangpeng Hao <xiangpeng@spiraldb.com>
T4ReadAt, a VortexReadAt over a t4 CachedFile: t4 caches an object store in memory and on local drives. It reads as ObjectStoreReadAt does, the same coalescing and concurrency unless set otherwise, but its bytes come from t4, whose one-buffer answers become the reads' buffers without a copy when aligned as asked and the session allocates statically. vortex-io re-exports t4's T4Caching, T4CachingBuilder and CachedFile. vortex-file gains a `t4` feature, and a test writes a file to an in-memory store and scans it back through T4ReadAt, with and without coalescing. t4 is a path dependency while it is unpublished. Signed-off-by: Xiangpeng Hao <xiangpeng@spiraldb.com>
…ough t4 VortexFormat::with_vortex_reader_factory: the factory creates the reader of every file the format opens, to infer schemas and statistics, and in every VortexSource it creates. Unset, files are read through the session's object store, as before. A test checks that a listing table infers its schema and scans through the factory. The vortex crate gains a `t4` feature. datafusion-bench puts t4 in front of the benchmark's S3 or GCS store when `--opt t4-policy` asks (clock|memory|disk|none, with t4-memory-gib, t4-disk and t4-coalesce), and reads every Vortex file through T4ReadAt. Signed-off-by: Xiangpeng Hao <xiangpeng@spiraldb.com>
…the t4 test never meets Nightly rustfmt reflows reader_factory. clippy's tests_outside_test_module doesn't fire on #[tokio::test], so the expectation in vortex-file/tests/t4_read_at.rs failed clippy. Signed-off-by: Xiangpeng Hao <xiangpeng@spiraldb.com>
Options for comparing readers tier by tier: evict-page-cache=on drops the data files' pages before every run, so Vortex reads from the drives in a warm process; cpu-log logs each run's process CPU and the bytes the md RAID read; get-log logs each run's GETs; rest-runs and rest-ms rest the drives before chosen runs. The runner checks every run's row count against the first, as a cache serves the later runs. t4 is taken by git, pinned to XiangpengHao/t4@0cb2fbf, while it is unpublished. Signed-off-by: Xiangpeng Hao <xiangpeng@spiraldb.com>
Merging this PR will degrade performance by 3.15%2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
Summary
A proof of concept, not meant to merge as is. It shows what Vortex gains by reading its files through t4, a cache of an object store in memory and on local NVMe, and asks which parts, if any, you'd want.
t4 keeps an object store's files in 1 MiB blocks, in memory and in cache files on local SSDs, which it reads with direct I/O. On a miss it fetches from the store, racing straggling GETs. The integration is thin: t4 exposes "open a file once, then read ranges of it", and Vortex wraps that as a
VortexReadAt. Vortex's crates use t4 only with the newt4feature on (the benchmark binary always does), and t4 never depends on Vortex.On Vortex's own datafusion-bench (TPC-H SF10 and ClickBench q20–q34), on three AWS instance types:
The design and every run are written up in t4's doc/09.
Changes
feat: make the dictionary size cap configurable and raise its default to 2 MiBis feat: make the dictionary size cap configurable and raise its default to 2 MiB #10766. The benchmark files were written with it.feat: read Vortex files through t4 (vortex-io t4 feature)addsvortex_io::t4::T4ReadAt, aVortexReadAtover t4'sCachedFile.with_coalesce_config, the object-store config by default).T4Caching,T4CachingBuilderandCachedFile, so the cache is built with the same t4 Vortex links.vortex-fileandvortexforward the feature.vortex-file/tests/t4_read_at.rswrites a Vortex file of over 2 MiB to an in-memory store and scans it back throughT4ReadAttwice: fetched, then from t4's tiers, with Vortex's coalescing and without. It compares the arrays read with those written.feat: VortexFormat takes a reader factory; datafusion-bench reads through t4addsVortexFormat::with_vortex_reader_factory. The factory is used for schema and statistics inference and handed to everyVortexSource.--opt t4-policy=...puts t4 in front of the benchmark's store through this hook.chore: format VortexFormat's reader factory; drop a lint expectation the t4 test never meets: nightly rustfmt, and an unmet clippy expectation.feat(datafusion-bench): measure tiers apart; take t4 by git: bench options, used for the numbers below:evict-page-cache=on: drops the data files' pages (posix_fadvise(DONTNEED)) before every run. Vortex then reads from the SSDs in a warm process, as t4 does, without paying a fresh process's ~400 ms first run.cpu-log=<path>: each run's process CPU time, and the bytes the md RAID read.get-log=<path>: each run's GETs: count, bytes, the slowest.rest-runs=<n>,rest-ms=<ms>: rests before runs 1 to n (see "The SSD burst" below).t4-policy,t4-memory-gib,t4-disk,t4-disk-gib,t4-coalesce: t4's tiers.t4-policy=memory|disk|nonepins every block to one tier, so a run measures one tier alone.API Changes
Additive only:
t4feature andT4ReadAt, forwarded byvortex-fileandvortex.VortexFormat::with_vortex_reader_factory.Nothing changes with the feature off.
Results
Local SSDs, 28 Oct 2026
All of each box's instance-store SSDs as one md RAID0 (XFS), on the box's defaults. Vortex reads its files there with the page cache emptied before every run. t4 reads its cache file on the same RAID, filled from S3 by a first run and pinned to its disk tier.
Each query runs in one process per reader: a first run (not counted), five runs each after a 2 s rest, then twelve back to back. The figure is the median of the last five, summed over the suite, t4's over Vortex's (below 1, t4 is faster).
t4 from the SSDs against Vortex from memory (the page cache): ×0.95 to ×1.05.
Remote and memory, 27 Oct 2026
An i7i.24xlarge in us-east-1, the data in S3 in the same region, each query in a process per reader, five runs. Geometric means over the suite of each query's median and slowest run. t4's merging of waiting GETs was still off then; it is on by default now.
The slowest runs include each process's first, which also reads the footers and opens connections. Without them, the remote row holds: TPC-H slowest ×0.80, median ×0.82; ClickBench slowest ×0.97, median ×1.05.
Things to know when reading these
preadat the kernel's default readahead. On an md RAID0 that default is twice the stripe (2 MiB on the i3en), which doubles the bytes Vortex reads there (TPC-H q6: 825 MB a run, against the 400 MB its reads ask for). t4's direct reads don't read ahead. These numbers keep the defaults, as a user would run them.Dependencies
t4 is unpublished, so it is taken by git, pinned at
XiangpengHao/t4@0cb2fbf. It bringsbbtree,vstdand five Verus crates into the lockfile, andvstdis pinned to an exact version. That needs settling before anything merges: t4 should keep Verus out of a normal build.How to reproduce
Build the bench (t4 is on in its
vortexdependency):Set up the data:
vortex-bench/dataon the RAID. t4 reads the same files from S3 throughremote-data-dir.Run one process per query and reader, after
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches. Here TPC-H q6, 18 runs: a first, five rested, twelve back to back.ClickBench is the same with
clickbench ... --opt flavor=partitioned,...andremote-data-dir=s3://<bucket>/clickbench_partitioned/. On the i3en.2xlarge (64 GiB),t4-memory-gib=8; witht4-policy=diskthe memory tier holds nothing. Report the median of the last five runs'wall_msincpu.jsonl.What I'd like to know
VortexFormatwelcome on its own? It lets anyVortexReadAtserve a DataFusion table.t4feature in vortex-io be acceptable, or would you rather the adapter live in a separate crate?T4FileSystem), random access by URL, and t4's default (unpinned) placement across memory and SSD.