Add MADV_PAGEOUT support for eager eviction of compressed spill - #36948
Conversation
Extend the pager to support MADV_PAGEOUT so the column pager can proactively evict its lz4-compressed spill chunks from RSS on the swap backend. `mz_ore::pager` previously only exposed `MADV_COLD` (via the ownership-taking `pageout_swap`), which is lazy: the kernel reclaims only at the pressure cliff, so the resident budget is fiction. The column pager's lz4 + swap path keeps the compressed bytes resident in `CompressedInner::Memory` as unmanaged anonymous memory with no madvise at all. - `mz_ore::pager::advise_pageout(&[u8])`: a borrow-only `MADV_PAGEOUT` over the page-aligned interior of a byte buffer. Unlike `pageout_swap` it does not take ownership — the allocation stays addressable and a later read re-faults the swapped-out pages. The page-alignment logic is shared with `madvise_cold` via a new `madvise_aligned` core; no-op off Linux. - The column pager's lz4 `Backend::Swap` branch issues `advise_pageout` over the compressed bytes, gated by a new process-global `SWAP_PAGEOUT` flag. - New `column_paged_batcher_swap_pageout` dyncfg (default off) drives the flag through `apply_tiered_config`, mirroring the backend/codec selection. Gated off until proven: eager reclaim is the kernel interaction the pager design singled out as risky. https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn
- swap.rs: drop the `as` cast in the advise_pageout test (use `u8::try_from`) and demote the `pageout_swap` doc links to plain code spans so the public `advise_pageout` rustdoc no longer links a private item. - Register the new `column_paged_batcher_swap_pageout` dyncfg with parallel-workload's FlipFlagsAction and mzcompose's UNINTERESTING_SYSTEM_PARAMETERS, mirroring `column_paged_batcher_lz4`. https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn
The public `apply_tiered_config` rustdoc must not intra-doc-link the private `SWAP_PAGEOUT` static. https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn
DAlperin
left a comment
There was a problem hiding this comment.
The idea is good, but I have some layering concerns. CompressedInner::Memory probably isn't the right place to hold explicitly non resident memory. I think its probably acceptable to use the same pad_u8_to_u64 in the File branch and make the madvise property of the swap backend. We could delete the ::Memory variant entirely if we decide its not needed.
Do you have benchmarks?
| /// backend is swap (no scratch directory): the compressed bytes are kept | ||
| /// resident in the process address space, and `MADV_PAGEOUT` swaps them out at | ||
| /// spill time so RSS is held at the budget instead of the kernel reclaiming | ||
| /// lazily at the pressure cliff (the `MADV_COLD` default). A later page-in |
There was a problem hiding this comment.
This isn't true though right? Currently the memory variant of compressed gets no madvise at all
There was a problem hiding this comment.
Same issue in the PR description, but does point to a middle option (that I assume you tested but worth saying anyway) of unconditionally madvise cold-ing the compressed bytes?
There was a problem hiding this comment.
Yeah, Claude got confused with data from benchmarks, sorry for that.
| /// Off by default: the eager-reclaim syscall is the one kernel interaction the | ||
| /// pager design singled out as risky, so it stays gated until proven on the | ||
| /// target workload. | ||
| pub const COLUMN_PAGED_BATCHER_SWAP_PAGEOUT: Config<bool> = Config::new( |
There was a problem hiding this comment.
Pedantic but maybe something like column_paged_batcher_swap_eager_reclaim
There was a problem hiding this comment.
I think pageout is the right term, because it's not reclaiming memory (or, only in a very specific sense). pageout is the actual mechanism we use, so it removes ambiguity.
The compressed-memory variant currently receives no madvise at all, not `MADV_COLD` — that was only ever a benchmark comparison point. Reword the doc to state the actual current behavior (un-advised, lazily reclaimed) and note that eager eviction only pays off on the lz4 path. https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn
|
Thanks for the review! I agree, the PR was lacking details, so trying to correct this here. I ran measurements and they landed on lz4 + swap benefitting from MADV_PAGEOUT. Just swap wouldn't benefit because it'd be bandwidth limited, but once the data was smaller, we started to see breakevens. Additionally, in a redlined cluster, always doing MADV_PAGEOUT seems sometimes better than not doing it. The problem with the policy as implemented is that we only start paging after 50% (or so) memory utilization, at which point we've put swap into a position to need to work through half of our memory, consuming I/O bandwidth when we need it most. I ran benchmarks, but not end-to-end, which is why I'm not yet publishing numbers. I'd say let's merge this and run some e2e benches and see where we land. |
|
Happy to merge and test. Still slightly partial to pushing the decision down to the pager level or changing the Enum variants to make it less confusing but I won't block on it |
Motivation
On the column pager's lz4 + swap spill path, the compressed bytes are kept resident in the process address space (
CompressedInner::Memory). Today they receive nomadviseat all — they're plain anonymous memory the kernel reclaims only lazily under LRU pressure. So while the byte budget bounds the logical resident working set, actual RSS drifts up toward the cgroup cap and is only relieved at the kernel's pressure cliff, with no headroom.This PR teaches the pager to issue
MADV_PAGEOUTover those bytes, evicting them eagerly at spill time so RSS is held at the budget. It is gated behind a new, default-off dynamic config, so behavior is unchanged until explicitly enabled.Why
MADV_PAGEOUT, and why only on the lz4 pathThis is one half of a combined spill strategy (lz4 +
MADV_PAGEOUT) that benchmarking converged on for the swap backend; full data and reasoning live in CLU-108. In short:MADV_COLDhint would have the same lazy character.MADV_PAGEOUTis proactive: it evicts at spill time and holds RSS at the budget (measured no-cap peak RSS: lz4+PAGEOUT 0.40 GiB vs anMADV_COLD-on-compressed variant 0.97 GiB — the full compressed working set).MADV_PAGEOUT's cost is that a later read must fault the pages back. lz4 shrinks the byte volume ~5.6×, so the re-fault is cheap — that is what makes eager eviction pay off. On the uncompressed path it is a measured net loss (raw+PAGEOUT 77s vs raw+COLD 72s): the full-size re-swap penalty has nothing to cancel it. That is why the flag only takes effect when lz4 is on, and why the rawpageout_swappath is left on its existingMADV_COLDhint.(Note:
MADV_COLD-on-the-compressed-bytes was only ever a benchmark comparison point, never a shipped code path — the compressed-memory variant has always been un-advised. This PR adds the first eviction hint it gets.)Description
Core pager (
src/ore/src/pager/swap.rs):MADV_COLDhint into amadvise_aligned(ptr, len, advice)helper.advise_pageout(&[u8])that issuesMADV_PAGEOUTover a byte slice without transferring ownership (unlikepageout_swap, which moves theVec<u64>into a handle and hintsMADV_COLD). The caller keeps the allocation addressable; a later read re-faults the pages.Column pager (
src/timely-util/src/column_pager.rs):SWAP_PAGEOUTatomic, set byapply_tiered_config, mirroring how the backend/codec are configured.advise_pageouton the compressed bytes when the flag is set. Addedround_trip_swap_lz4_pageoutcovering correctness with it enabled.Configuration (
src/compute-types/src/dyncfgs.rs,src/compute/src/compute_state.rs):column_paged_batcher_swap_pageoutdynamic config (defaultfalse), read inapply_worker_configand threaded toapply_tiered_config. Registered with parallel-workload and mzcompose's system-parameter lists.Public API (
src/ore/src/pager.rs): re-exportedadvise_pageout.Verification
madvise).MADV_PAGEOUTis a non-mutating reclaim hint — pages stay readable and re-fault on access, so data integrity is preserved.false; existing behavior is unchanged until enabled.https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn