Skip to content

fix(cache): release memory a liquid array holds beyond its charge - #61

Merged
anoop-narang merged 1 commit into
mainfrom
fix/compact-liquid-arrays
Nov 12, 2026
Merged

anoop-narang merged 1 commit into
mainfrom
fix/compact-liquid-arrays

Conversation

@anoop-narang

Copy link
Copy Markdown
Collaborator

What

Each array LiquidArray::from_arrow_array compresses is round-tripped through to_bytes / from_bytes, the IPC serialization the disk tier already uses. That detaches the entry from its source column's buffers and from the compressor's spare capacity.

Why

The cache charges an in-memory liquid entry by Vortex nbytes(), which sums buffer lengths. Two kinds of compressor output hold more than that:

Both are freed only when the entry is, so a full cache can hold much more than its budget.

Effect

Measured over 200 batches × 25 columns of a real 540 MB Parquet file (log-style data with many string columns):

                 charged    held     held/charged
before           112 MB     201 MB   1.78x
after            112 MB     146 MB   1.29x

The round trip took 135 ms against 3,820 ms of compression for the same batches (about 3.5%). Most of the remaining gap is per-node overhead (symbol tables, tree nodes) that nbytes() also does not count.

Tests

  • constant_strings_do_not_pin_the_source_buffers: after compressing a constant column and dropping the source, the source's views and data buffers are uniquely owned again. Fails without the fix.
  • fsst_codes_growth_allocation_is_released: keeps a handle to the compressor's FSST codes buffer (len 544,535, capacity 1,048,577 in this fixture), compacts, and requires that handle to be uniquely owned again. Fails without the fix.

Both pass with the fix, along with cargo test --workspace --exclude dev-tools.

The charge stays approximate: nbytes() still leaves out per-node overhead such as tree nodes and decoded FSST symbol tables.

Once the Vortex pin includes #11382 and a fix for #11420, this round trip can be reconsidered.

This change was investigated and written with AI assistance (Claude Code).

The cache charges an in-memory liquid entry by its Vortex nbytes(),
the sum of its buffers' lengths. Two kinds of compressor output keep
more memory alive than that:

- A constant string column stores a scalar that is a zero-copy slice
  of the whole source views or data buffer (vortex-data/vortex#10599,
  fixed upstream in #11382 after our pinned revision).
- An FSST codes buffer grows by doubling and is frozen with its spare
  capacity (vortex-data/vortex#11420).

Both are freed only with the entry, so the cache can hold close to
twice what it counts. Round-trip each compressed array through the
IPC serialization the disk tier already uses, which detaches it from
both allocations.
@anoop-narang
anoop-narang requested a review from a team as a code owner November 12, 2026 20:33
@anoop-narang
anoop-narang requested review from eddietejeda and removed request for a team November 12, 2026 20:33
@anoop-narang anoop-narang changed the title fix(cache): own exactly the bytes a liquid array holds fix(cache): release memory a liquid array holds beyond its charge Nov 12, 2026
@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

📊 Benchmark Comparison

Current: 06388270 (Liquid) vs Baseline: 06388270 (DataFusionDefault)

Query Cold Time Δ Warm Time Δ CPU Time Δ
Q1 2.0ms (2.0ms) +0.0% 0.000ms (0.000ms) +0.0% 0.000ms (0.000ms) +0.0%
Q2 6.0ms (4.0ms) +50.0% 3.0ms (3.0ms) +0.0% 5.0ms (4.0ms) +25.0%
Q3 12.0ms (8.0ms) +50.0% 4.0ms (8.0ms) -50.0% 3.5ms (15.0ms) -76.7%
Q4 13.0ms (6.0ms) +116.7% 3.5ms (6.0ms) -41.7% 2.0ms (11.0ms) -81.8%
Q5 48.0ms (38.0ms) +26.3% 40.0ms (38.5ms) +3.9% 5.0ms (14.0ms) -64.3%
Q6 171.0ms (78.0ms) +119.2% 66.0ms (81.5ms) -19.0% 19.5ms (55.5ms) -64.9%
Q7 1.0ms (1.0ms) +0.0% 0.500ms (0.000ms) +inf% 0.000ms (0.000ms) +0.0%
Q8 6.0ms (4.0ms) +50.0% 5.5ms (4.0ms) +37.5% 5.0ms (4.0ms) +25.0%
Q9 70.0ms (97.0ms) -27.8% 64.5ms (60.5ms) +6.6% 6.5ms (26.0ms) -75.0%
Q10 78.0ms (60.0ms) +30.0% 62.5ms (61.0ms) +2.5% 7.5ms (40.0ms) -81.2%
Q11 145.0ms (16.0ms) +806.2% 135.5ms (15.5ms) +774.2% 382.5ms (19.0ms) +1913.2%
Q12 151.0ms (18.0ms) +738.9% 126.5ms (18.5ms) +583.8% 376.5ms (23.5ms) +1502.1%
Q13 248.0ms (91.0ms) +172.5% 122.0ms (86.5ms) +41.0% 181.5ms (58.5ms) +210.3%
Q14 342.0ms (101.0ms) +238.6% 98.5ms (99.5ms) -1.0% 73.0ms (71.0ms) +2.8%
Q15 269.0ms (76.0ms) +253.9% 110.0ms (75.0ms) +46.7% 145.0ms (67.0ms) +116.4%
Q16 73.0ms (89.0ms) -18.0% 67.0ms (73.0ms) -8.2% 3.0ms (13.0ms) -76.9%
Q17 308.0ms (162.0ms) +90.1% 169.5ms (164.0ms) +3.4% 41.5ms (68.0ms) -39.0%
Q18 366.0ms (154.0ms) +137.7% 172.5ms (164.5ms) +4.9% 43.0ms (70.0ms) -38.6%
Q19 657.0ms (360.0ms) +82.5% 294.0ms (306.0ms) -3.9% 57.0ms (105.5ms) -46.0%
Q20 9.0ms (6.0ms) +50.0% 2.5ms (6.5ms) -61.5% 5.5ms (12.0ms) -54.2%
Q21 1.18s (127.0ms) +830.7% 415.5ms (127.0ms) +227.2% 194.0ms (203.5ms) -4.7%
Q22 1.49s (130.0ms) +1043.1% 565.5ms (124.0ms) +356.0% 103.0ms (252.0ms) -59.1%
Q23 3.07s (346.0ms) +787.6% 1.59s (349.0ms) +356.9% 345.5ms (530.0ms) -34.8%
Q24 20.23s (692.0ms) +2823.4% 959.0ms (711.0ms) +34.9% 325.0ms (1.95s) -83.3%
Q25 278.0ms (49.0ms) +467.3% 36.5ms (41.5ms) -12.0% 57.5ms (85.5ms) -32.7%
Q26 170.0ms (31.0ms) +448.4% 68.0ms (32.5ms) +109.2% 187.5ms (57.0ms) +228.9%
Q27 256.0ms (41.0ms) +524.4% 39.5ms (43.0ms) -8.1% 73.0ms (85.0ms) -14.1%
Q28 1.21s (158.0ms) +665.8% 738.5ms (156.5ms) +371.9% 118.0ms (210.5ms) -43.9%
Q29 1.78s (641.0ms) +177.2% 863.0ms (652.5ms) +32.3% 267.5ms (261.0ms) +2.5%
Q30 28.0ms (18.0ms) +55.6% 25.0ms (20.0ms) +25.0% 11.0ms (15.5ms) -29.0%
Q31 481.0ms (95.0ms) +406.3% 72.5ms (78.5ms) -7.6% 72.0ms (102.0ms) -29.4%
Q32 708.0ms (72.0ms) +883.3% 87.0ms (75.0ms) +16.0% 57.0ms (105.0ms) -45.7%
Q33 274.0ms (284.0ms) -3.5% 249.5ms (277.5ms) -10.1% 7.5ms (54.0ms) -86.1%
Q34 1.34s (346.0ms) +288.2% 538.0ms (331.5ms) +62.3% 104.0ms (208.0ms) -50.0%
Q35 1.27s (344.0ms) +268.3% 530.0ms (341.5ms) +55.2% 96.0ms (205.0ms) -53.2%
Q36 66.0ms (73.0ms) -9.6% 59.5ms (72.5ms) -17.9% 3.0ms (16.0ms) -81.2%
Q37 229.0ms (86.0ms) +166.3% 53.5ms (79.0ms) -32.3% 16.5ms (57.0ms) -71.1%
Q38 60.0ms (34.0ms) +76.5% 21.0ms (33.5ms) -37.3% 15.0ms (18.5ms) -18.9%
Q39 226.0ms (39.0ms) +479.5% 21.5ms (37.5ms) -42.7% 10.0ms (57.0ms) -82.5%
Q40 660.0ms (145.0ms) +355.2% 172.5ms (137.5ms) +25.5% 37.0ms (100.5ms) -63.2%
Q41 20.0ms (14.0ms) +42.9% 9.0ms (14.0ms) -35.7% 6.0ms (12.0ms) -50.0%
Q42 16.0ms (13.0ms) +23.1% 8.0ms (12.0ms) -33.3% 6.5ms (10.0ms) -35.0%
Q43 14.0ms (11.0ms) +27.3% 8.5ms (10.0ms) -15.0% 6.0ms (7.0ms) -14.3%

⚠️ LiquidCache is slower on 18 queries (warm)

  • Q7: warm +inf% (0.500ms vs 0.000ms)
  • Q11: warm +774.2% (135.5ms vs 15.5ms)
  • Q12: warm +583.8% (126.5ms vs 18.5ms)
  • Q28: warm +371.9% (738.5ms vs 156.5ms)
  • Q23: warm +356.9% (1.59s vs 349.0ms)
  • Q22: warm +356.0% (565.5ms vs 124.0ms)
  • Q21: warm +227.2% (415.5ms vs 127.0ms)
  • Q26: warm +109.2% (68.0ms vs 32.5ms)
  • Q34: warm +62.3% (538.0ms vs 331.5ms)
  • Q35: warm +55.2% (530.0ms vs 341.5ms)
  • Q15: warm +46.7% (110.0ms vs 75.0ms)
  • Q13: warm +41.0% (122.0ms vs 86.5ms)
  • Q8: warm +37.5% (5.5ms vs 4.0ms)
  • Q24: warm +34.9% (959.0ms vs 711.0ms)
  • Q29: warm +32.3% (863.0ms vs 652.5ms)
  • Q40: warm +25.5% (172.5ms vs 137.5ms)
  • Q30: warm +25.0% (25.0ms vs 20.0ms)
  • Q32: warm +16.0% (87.0ms vs 75.0ms)

Compared Liquid vs DataFusionDefault on the same runner
Regressions: warm-time increases of at least 15%. Cold Time: first iteration; Warm Time: median of remaining iterations.

@anoop-narang

Copy link
Copy Markdown
Collaborator Author

The TPC-DS failure is unrelated to this change, and it reproduced on a rerun. It fails in "Setup TPC-DS data" before any Rust runs. The runner now has Python 3.14, whose argparse rejects type=Optional[Path] at benchmark/tpcds/tpcds_gen.py:30 (TypeError: pathlib.Path | None is not callable). That line is unchanged on main. Every other check passes, including TPC-H, ClickBench, Address Sanitizer and Shuttle. The script fix belongs in its own PR.

@anoop-narang
anoop-narang merged commit 1fa6f95 into main Nov 12, 2026
24 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant