Re: [PATCH v2 2/2] mm/zswap: Support batch writeback in shrink_memcg()

From: Hao Jia

Date: Tue Jul 28 2026 - 07:25:05 EST




On 2026/7/28 00:18, Yosry Ahmed wrote:
On Sat, Jul 25, 2026 at 7:39 PM Hao Jia <jiahao.kernel@xxxxxxxxx> wrote:



On 2026/7/25 06:22, Yosry Ahmed wrote:
On Fri, Jul 24, 2026 at 12:35 PM Johannes Weiner <hannes@xxxxxxxxxxx> wrote:

On Fri, Jul 24, 2026 at 11:39:41AM -0700, Yosry Ahmed wrote:
On Fri, Jul 24, 2026 at 11:37 AM Nhat Pham <nphamcs@xxxxxxxxx> wrote:

On Fri, Jul 24, 2026 at 10:57 AM Yosry Ahmed <yosry@xxxxxxxxxx> wrote:

The numbers generally look good, the store rejection rate is
decreasing and we are naturally doing more writeback which makes
sense. The store rejection rate actually increases when the batch size
increases to 64, maybe aggressive writeback is more likely to fail and
cause a store rejection, so that also makes sense.

The only part I can't immediately reason about is pswpin. How are we
reading from physical swap more than we ever wrote to it?

The same page can be swapped out once, and swap in multiple times :)

Unlike zswap, physical swap in is not necessarily exclusive. You load
the page in memory, but if the swapfile is not full, etc. etc. you
don't invalidate the copy of the data on the swapfile. At reclaim
time, we notice the page is not dirty, so we just skip (z)swapout.

Oh right, I forgot about that, thanks.

I still don't fully understand why pswpin is significantly increased
with batching here. I would hope that with less LRU inversion we end
up with less disk swapin. Probably the test access patterns are just
too random compared to real workloads?

Maybe more time in zswap for those tail entries left behind with the
smaller batch?

That's a 32 entry window that could zswpin with the smaller batch but
pswpin with the larger one.

Oh I was talking about going from no batching to batch=32. pspwin
increased by 387,400 (~30%). We are writing back 272,880 more pages
but we also have 209,837 less store rejections. So overall ~ 63,043
more pages should end up on disk, which doesn't explain the increase
in pspwin.


In my previous runs, I also collected zswpin. could the following
explanation help account for what we are seeing?

Assuming stress-ng maintains a roughly similar memory access rate,
batching flushes significantly more pages back to disk (meaning pages in
the zswap pool are evicted faster, keeping zswap pool residency lower).
As a result, when stress-ng accesses swapped memory, it is more likely
to fault pages in from disk (pswpin) rather than hit zswap (zswpin).

We are not just writing back more pages to disk, we are also accepting
more pages into zswap. The whole point of writeback here is to evict
colder pages to disk and accept (relatively) hotter pages into zswap,
as they are more likely to be swapped in again.


In fact, zswpin in batch-32 dropped by 143,425 compared to
baseline—which aligns with batching having a lower zswpin and a higher
pswpin.

To be frank, under the Test Case 2 workload, we cannot strictly
guarantee that the total volume of page reads and writes remains
consistent across all three runs within the same time window.

baseline-cgroup batch-all-32-cgroup
batch-all-64-cgroup
shrink_worker wakeups 7,238 766 367
shrink_memcg calls 12,059,142 1,961,194 983,878
written_back 28,277 301,157 327,997
zswap_store calls 1,349,572 1,168,190 1,114,549
store succeeded 492,861 521,315 459,246
store rejected 856,712 646,875 655,303
store reject rate ~63% ~55% ~58%
pool_limit_hit 510,130 50,096 57,715
pswpout 884,989 948,032 983,300
pswpin 1,251,268 1,638,668 1,878,453
zswpout 492,861 521,314 459,245
zswpin 309,631 166,206 84,977 <-

One possible explanation is that we are shrinking too aggressively due
to concurrent shrinkers. That's one difference between hitting the
global limit and the memcg limit. There's a single global shrinker
that all store failures will just wake up, while all concurrent stores
hitting the memcg limit will start shrinking.

The other explanation is that, as I mentioned earlier, the access
pattern is just too random and we are consistently refaulting cold
memory. I am honestly a bit concerned about these results.

Hao, would you be able to try re-running the test with a single
stressor? It would help us figure out if concurrent shrinking is a
problem. It would also be useful to increase the global limit, to
isolate writeback from hitting the memcg limit. I don't think both
limits will often be used in conjunction, at least not in a way where
both limits are hit simultaneously.

It may also be useful to run an actual workload if you are able to,
like a kernel build or something, to see if the access patterns change
how much we end up swapping in from zswap vs. disk.

Apologies—there was a flaw in my previous test setup. I had configured zswap.max=320M alongside max_pool_percent=1. Since the global zswap pool capacity was roughly 314M (< 320M), the global pool limit was actually being hit before the cgroup zswap.max limit. Both limits were likely interleaving throughout the run, making the results difficult to analyze.

Re-running with **zswap.max=280M** yields results that align with expectations:

Test Case: zmax280 stress-4 pool1%
baseline batch-all32 batch-all64
shrink_worker wakeups 0 0 0
shrink_memcg calls 698,990 57,187 29,649
written_back 655,570 865,131 883,508
store succeeded 1,054,928 1,124,990 1,190,608
store rejected 229,722 22,642 11,659
store reject rate ~17% ~1% ~0%
reject_reclaim_fail 110 15,454 12,985
pswpout 885,437 888,027 895,478
pswpin 2,035,415 1,305,341 1,384,751
zswpout 1,054,928 1,124,990 1,190,608
zswpin 280,675 162,684 158,556

To isolate the cgroup zswap limit, I also tested with max_pool_percent=50 (which should behave similarly to the zswap.max=280M case above, bounded strictly by cgroup zswap.max):

Test Case: zmax320 stress-4 pool50%
baseline batch-all32 batch-all64
shrink_worker wakeups 0 0 0
shrink_memcg calls 687,608 54,002 28,116
written_back 639,176 846,663 862,347
store succeeded 992,816 1,208,123 1,213,286
store rejected 231,431 20,425 10,915
store reject rate ~18% ~1% ~0%
pswpout 870,745 867,360 873,553
pswpin 1,707,823 1,216,814 1,378,381
zswpout 992,816 1,208,123 1,213,286
zswpin 250,220 210,791 185,716


Finally, here is the dataset for zswap.max=320M and max_pool_percent=1 with a single stress-ng worker (stress-1), which also looks consistent with your expectations:

Test Case: zmax320 stress-1 pool1%
baseline batch-all32 batch-all64
shrink_worker wakeups 9,279 980 557
shrink_memcg calls 14,247,647 2,638,751 1,465,915
written_back 7,702 439,809 482,809
store succeeded 329,603 531,224 566,150
store rejected 883,945 517,578 482,644
store reject rate ~73% ~49% ~46%
pswpout 891,647 957,388 965,453
pswpin 692,255 628,246 582,781
zswpout 329,603 531,224 566,150
zswpin 248,871 3 7,872

Thanks,
Hao