Repository navigation
perf(cloudfetch): cut peak memory of large CloudFetch results - #1032
vuanhphung wants to merge 2 commits into
Conversation
python-lz4 allocated each decompressed CloudFetch file in the download thread's malloc arena, next to short-lived response bodies. On large results the fragmentation added up to ~40% to peak RSS. Decompress with pyarrow's LZ4 frame codec into a private anonymous mmap sized from the link's bytesNum instead; the mapping goes back to the OS when the Arrow table is released. Falls back to python-lz4 when pyarrow's codec is unavailable, the size hint is wrong, or the data is corrupt. Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
There was a problem hiding this comment.
Verdict: 2 Low
Looks good overall: the mmap path is well guarded, falls back correctly, and the edge cases are well tested. Two low concerns: per-file mappings add up on very large results, and no test runs run() end-to-end with real data. Nit: convert_arrow_based_file_to_arrow_table / create_arrow_table_from_arrow_file in utils.py are still annotated file_bytes: bytes, but they now also receive pyarrow.Buffer.
Pass mmap flags explicitly instead of via **kwargs (typeshed's mmap gained a bool trackfd parameter in 3.13), annotate the IPC helpers in utils.py for pyarrow.Buffer input, and add an unpatched run() test with real LZ4 data. Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
|
Review-body nit: done in a9b039a. |
There was a problem hiding this comment.
Verdict: 1 Low
Looks good overall; one low-severity note on the fallback cost. The mmap path is limited to Thrift-style bytesNum (larger than the compressed length), falls back cleanly, and returns a pyarrow.Buffer that works with both len() in run() and ipc.open_stream. The tests cover the boundary sizes, concatenated frames, and corrupt input. Nit: the CompressedInputStream is never closed explicitly; this is harmless because garbage collection handles it.
|
Re the second review's nit about the unclosed |
What type of PR is this?
Description
Large Thrift CloudFetch results used 1.3-1.7x the Arrow table size in peak RSS (a 1.7 GB
fetchall_arrow()peaked at +2.2 GB). Arrow IPC reads are already zero-copy, so the extra memory was not a second copy of the data. memray and glibcmalloc_stats()show it is allocator fragmentation: python-lz4 allocates each decompressed file inside a download thread's malloc arena, next to that thread's short-lived response bodies. At peak, 17 arenas held 752 MB of free-but-resident memory, and the overhead grows across repeated fetches in one process.This PR decompresses each file with pyarrow's LZ4 frame codec (
CompressedInputStream.readinto) into its own private anonymousmmap, sized from the link'sbytesNum. The Arrow table still references the buffer zero-copy. The mapping is returned to the OS when the table is released. The decompressed size is checked against the stream, and the old python-lz4 path is used when pyarrow or its codec is missing, whenbytesNumis too small, when the data is corrupt (so the error is unchanged), and on SEA, wherebytesNumis the compressed size. As a side effect, decompression is faster: multi-frame files no longer go through python-lz4's GIL-bound chunk loop. Onlycloudfetch/downloader.pychanges; inline results and the kernel backend are untouched.How is this tested?
New unit tests cover the mmap path (private mapping, exact size when
bytesNumis too large, fallback when it is too small, missing, equal to the compressed size, or whenmmapfails), concatenated frames, corrupt and truncated input raising the sameRuntimeErroras before, and no Arrow codec. They also pass with pyarrow 14.0.2. The full unit suite passes. E2E:TestPySQLLargeWideResultSet,TestPySQLLargeNarrowResultSet(Thrift and SEA, lz4 on and off), andtest_complex_types.pypass. CloudFetch and inline results were identical for 2M rows of mixed types (nulls, decimals, timestamps, dates, arrays, structs, maps) viafetchall_arrow,fetchmany_arrow, andfetchall.Peak RSS increase during the fetch, live Thrift warehouse, median of 5 fresh processes per cell, interleaved with
main(13 runs for 1.7 GBfetchall_arrow):fetchall_arrow()110 MBfetchall_arrow()562 MBfetchall_arrow()1.7 GBfetchmany_arrow(100k)loop, 562 MBfetchmany_arrow(100k)loop, 1.7 GBfetchall()rows, 562 MBLive wall time is network-bound and within noise. With the same captured files served locally over HTTPS with 60 ms latency (5 runs),
fetchall_arrow()went from 0.56 s to 0.17 s at 110 MB and from 3.84 s to 3.61 s at 1.7 GB. RSS after the result is freed is unchanged on the warehouse (~150 MB). Four back-to-back 1.7 GB fetches in one process peak at 2160 MB instead of 3132 MB.Related Tickets & Documents
None.
This pull request and its description were written by Isaac.