Skip to content

Add MADV_PAGEOUT support for eager eviction of compressed spill - #36948

Merged
antiguru merged 4 commits into
mainfrom
claude/blissful-gates-90ga5j
Jun 10, 2026
Merged

Add MADV_PAGEOUT support for eager eviction of compressed spill#36948
antiguru merged 4 commits into
mainfrom
claude/blissful-gates-90ga5j

Conversation

@antiguru

@antiguru antiguru commented Jun 9, 2026

Copy link
Copy Markdown
Member

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 no madvise at 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_PAGEOUT over 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 path

This 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:

  • Eager vs lazy. Leaving the bytes unmanaged (today) reclaims only at the pressure cliff, so RSS pins near the cap with no headroom. An MADV_COLD hint would have the same lazy character. MADV_PAGEOUT is proactive: it evicts at spill time and holds RSS at the budget (measured no-cap peak RSS: lz4+PAGEOUT 0.40 GiB vs an MADV_COLD-on-compressed variant 0.97 GiB — the full compressed working set).
  • Why it is coupled to lz4. 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 raw pageout_swap path is left on its existing MADV_COLD hint.

(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):

  • Extracted the page-alignment logic shared by the existing MADV_COLD hint into a madvise_aligned(ptr, len, advice) helper.
  • Added a public advise_pageout(&[u8]) that issues MADV_PAGEOUT over a byte slice without transferring ownership (unlike pageout_swap, which moves the Vec<u64> into a handle and hints MADV_COLD). The caller keeps the allocation addressable; a later read re-faults the pages.
  • No-op on non-Linux targets. Unit tests cover data preservation and the empty/sub-page edge cases.

Column pager (src/timely-util/src/column_pager.rs):

  • Process-global SWAP_PAGEOUT atomic, set by apply_tiered_config, mirroring how the backend/codec are configured.
  • The lz4 + swap spill path calls advise_pageout on the compressed bytes when the flag is set. Added round_trip_swap_lz4_pageout covering correctness with it enabled.

Configuration (src/compute-types/src/dyncfgs.rs, src/compute/src/compute_state.rs):

  • New column_paged_batcher_swap_pageout dynamic config (default false), read in apply_worker_config and threaded to apply_tiered_config. Registered with parallel-workload and mzcompose's system-parameter lists.

Public API (src/ore/src/pager.rs): re-exported advise_pageout.

Verification

  • Unit + integration tests as above; skipped under Miri (no madvise).
  • MADV_PAGEOUT is a non-mutating reclaim hint — pages stay readable and re-fault on access, so data integrity is preserved.
  • Flag defaults to false; existing behavior is unchanged until enabled.

https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn

claude added 2 commits June 9, 2026 19:28
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
@antiguru
antiguru marked this pull request as ready for review June 9, 2026 19:40
@antiguru
antiguru requested review from a team as code owners June 9, 2026 19:40
The public `apply_tiered_config` rustdoc must not intra-doc-link the private
`SWAP_PAGEOUT` static.

https://claude.ai/code/session_012Ji6Nd26mZVJCRGTxbUBfn
@antiguru
antiguru requested a review from DAlperin June 9, 2026 19:43

@DAlperin DAlperin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 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?

Comment thread src/compute-types/src/dyncfgs.rs Outdated
/// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't true though right? Currently the memory variant of compressed gets no madvise at all

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pedantic but maybe something like column_paged_batcher_swap_eager_reclaim

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@antiguru

Copy link
Copy Markdown
Member Author

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.

@DAlperin

Copy link
Copy Markdown
Member

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

@antiguru
antiguru requested a review from DAlperin June 10, 2026 17:10
@antiguru
antiguru merged commit 7615b24 into main Jun 10, 2026
121 checks passed
@antiguru
antiguru deleted the claude/blissful-gates-90ga5j branch June 10, 2026 17:12
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.

3 participants