Skip to contentSkip to conversation

mm/zswap: Implement per-cgroup proactive writeback

PATCH Superseded Hao Jia 5 participants 76 messages May 26 – Jun 17

Subsystems (10)

v3 of 5 Superseded Latest is v5 →

lore
Share & tools
lore

Hao Jia

author
#0 lore raw

v3 cover mm/zswap: Implement per-cgroup proactive writeback

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or by the pool reaching its size limit. Although proactive memory reclaim can automatically write back a portion of zswap pages via the shrinker, it cannot explicitly control the amount of writeback for a specific memory cgroup. Moreover, proactive memory reclaim may not always be triggered during a steady state.

In certain scenarios, it is desirable to trigger writeback in advance to free up memory. For example, users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle.

This patch series introduces a "zswap_writeback_only" key to memory.reclaim cgroup interface, allowing users to proactively write back cold compressed pages from zswap to the backing swap device. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Patch 1: Move the global zswap shrink cursor into struct mem_cgroup as a
per-memcg zswap_wb_iter, so patch 2 can scope writeback to a given memcg and make forward progress across its subtree on repeated invocations.

Patch 2: Extend the memory.reclaim cgroup v2 interface with a new
"zswap_writeback_only" key, allowing users to trigger proactive zswap writeback up to a requested budget.

Patch 3: Add a zswpwb_proactive counter to memory.stat and /proc/vmstat
to track the number of writebacks triggered by proactive writeback.

Patch 4: Add tests for zswap proactive writeback.

v2->v3:
- Align the return value of zswap_proactive_writeback() with memory.reclaim and update the corresponding documentation accordingly.
- Resolve conflicts in test_zswap.c on the mm-unstable branch.
- Enhance the zswap proactive writeback selftests to guard against potential future regressions.

v1->v2:
- As suggested by Yosry and Nhat, extend the memory.reclaim cgroup v2 interface with a "zswap_writeback_only" key instead of adding a new dedicated cgroup interface.
- Update the zswap documentation and add selftests for proactive writeback.

[v2] https://lore.kernel.org/all/[email protected]
[v1] https://lore.kernel.org/all/[email protected]

Hao Jia (4):
mm/zswap: Make shrink_worker writeback cursor per-memcg mm/zswap: Implement proactive writeback mm/zswap: Add per-memcg stat for proactive writeback selftests/cgroup: Add tests for zswap proactive writeback

 Documentation/admin-guide/cgroup-v2.rst     |  22 +-
 Documentation/admin-guide/mm/zswap.rst      |  11 +-
 include/linux/memcontrol.h                  |   3 +
 include/linux/vm_event_item.h               |   1 +
 include/linux/zswap.h                       |  16 ++
 mm/memcontrol.c                             |   4 +
 mm/vmscan.c                                 |  14 +
 mm/vmstat.c                                 |   1 +
 mm/zswap.c                                  | 292 +++++++++++++++++---
 tools/testing/selftests/cgroup/test_zswap.c | 155 ++++++++++-
 10 files changed, 471 insertions(+), 48 deletions(-)
Signature · 1 line
2.34.1
author
#1 lore raw

PATCH v3 1/4 mm/zswap: Make shrink_worker writeback cursor per-memcg

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

Because the cursor is now per-memcg, the offline cleanup must visit every ancestor that could be holding a reference to the dying memcg. Factor out __zswap_memcg_offline_cleanup() and walk from dead_memcg up to the root.

No functional change intended for shrink_worker().

---
 include/linux/memcontrol.h |   3 +
 include/linux/zswap.h      |   9 +++
 mm/memcontrol.c            |   3 +
 mm/zswap.c                 | 119 ++++++++++++++++++++++++++-----------
 4 files changed, 98 insertions(+), 36 deletions(-)
include/linux/memcontrol.h +3 -0
index bf1a6e131eca..5e29c2b7e376 100644
@@ -229,6 +229,9 @@ struct mem_cgroup {
 	 * swap, and from being swapped out on zswap store failures.
 	 */
 	bool zswap_writeback;
+
+	/* Per-memcg writeback cursor */
+	struct zswap_wb_iter zswap_wb_iter;
 #endif
 
 	/* vmpressure notifications */
include/linux/zswap.h +9 -0
index 30c193a1207e..efa6b551217e 100644
@@ -11,6 +11,15 @@ extern atomic_long_t zswap_stored_pages;
 
 #ifdef CONFIG_ZSWAP
 
+/* Iteration cursor for zswap writeback over a memcg's subtree. */
+struct zswap_wb_iter {
+	/* protects @pos against concurrent advances */
+	spinlock_t lock;
+	struct mem_cgroup *pos;
+};
+
+void zswap_wb_iter_init(struct zswap_wb_iter *iter);
+
 struct zswap_lruvec_state {
 	/*
 	 * Number of swapped in pages from disk, i.e not found in the zswap pool.
mm/memcontrol.c +3 -0
index 13f5d4b2a78e..e205e5de193d 100644
@@ -4024,6 +4024,9 @@ static struct mem_cgroup *mem_cgroup_alloc(struct mem_cgroup *parent)
 	INIT_LIST_HEAD(&memcg->memory_peaks);
 	INIT_LIST_HEAD(&memcg->swap_peaks);
 	spin_lock_init(&memcg->peaks_lock);
+#ifdef CONFIG_ZSWAP
+	zswap_wb_iter_init(&memcg->zswap_wb_iter);
+#endif
 	memcg->socket_pressure = get_jiffies_64();
 #if BITS_PER_LONG < 64
 	seqlock_init(&memcg->socket_pressure_seqlock);
mm/zswap.c +83 -36

158 lines not on this page · raw message

Signature · 1 line
2.34.1
maintainer Reviewed-by on v3 1/4
#2 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 17 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

Quoted 6 lines

Because the cursor is now per-memcg, the offline cleanup must visit every ancestor that could be holding a reference to the dying memcg. Factor out __zswap_memcg_offline_cleanup() and walk from dead_memcg up to the root.

No functional change intended for shrink_worker().

LGTM, if the memcg maintainers are happy with the overhead.

maintainer
#3 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

In reply to Hao Jia
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

If we really need to have per-memcg cursors (I am not a big fan), I think we can minimize the overhead by making the cursor updates use atomic cmpxchg instead of having a per-memcg lock.

Quoted 5 lines

Because the cursor is now per-memcg, the offline cleanup must visit every ancestor that could be holding a reference to the dying memcg. Factor out __zswap_memcg_offline_cleanup() and walk from dead_memcg up to the root.

Another reason why I don't like per-memcg cursors. There is too much complexity and I wonder if it's warranted. If we stick with per-memcg cursors please do the refactoring in separate patches to make the patches easier to review.

Thanks!

author
#4 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 12 lines · Yosry Ahmed
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Similar to how memory reclaim uses mem_cgroup_iter() (via struct mem_cgroup_reclaim_iter) and the old shrink_worker() used zswap_next_shrink, we need a shared cursor here.

Quoted 10 lines
Quoted 5 lines

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

If we really need to have per-memcg cursors (I am not a big fan), I think we can minimize the overhead by making the cursor updates use atomic cmpxchg instead of having a per-memcg lock.

Because mem_cgroup_iter() always calls css_put(&prev->css), we cannot simply update zswap_wb_iter.pos via cmpxchg() after calling it. Doing so could lead to a double css_put() issue on prev->css.

Therefore, if we switch to the cmpxchg() approach, we wouldn't be able to reuse the existing mem_cgroup_iter() logic. We would have to write a new function similar to cgroup_iter(), and its implementation might end up looking a bit obscure/complex.

Currently, this lock is only used in shrink_memcg(), proactive writeback, and mem_cgroup_css_offline(). Note that shrink_memcg() only acquires the lock of the root cgroup, and mem_cgroup_css_offline() is unlikely to be a hot path.

So, should we keep the spin_lock or go with the cmpxchg() approach? Yosry and Nhat, what are your thoughts on this?

Quoted 10 lines
Quoted 5 lines

Because the cursor is now per-memcg, the offline cleanup must visit every ancestor that could be holding a reference to the dying memcg. Factor out __zswap_memcg_offline_cleanup() and walk from dead_memcg up to the root.

Another reason why I don't like per-memcg cursors. There is too much complexity and I wonder if it's warranted. If we stick with per-memcg cursors please do the refactoring in separate patches to make the patches easier to review.

Sorry about that. I will try to keep each patch as simple as possible in the next version.

Thanks,
Hao

maintainer
#5 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 56 lines · Hao Jia
Quoted 12 lines · Yosry Ahmed
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Similar to how memory reclaim uses mem_cgroup_iter() (via struct mem_cgroup_reclaim_iter) and the old shrink_worker() used zswap_next_shrink, we need a shared cursor here.

Quoted 10 lines
Quoted 5 lines

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

If we really need to have per-memcg cursors (I am not a big fan), I think we can minimize the overhead by making the cursor updates use atomic cmpxchg instead of having a per-memcg lock.

Because mem_cgroup_iter() always calls css_put(&prev->css), we cannot simply update zswap_wb_iter.pos via cmpxchg() after calling it. Doing so could lead to a double css_put() issue on prev->css.

Therefore, if we switch to the cmpxchg() approach, we wouldn't be able to reuse the existing mem_cgroup_iter() logic. We would have to write a new function similar to cgroup_iter(), and its implementation might end up looking a bit obscure/complex.

Quoted 7 lines

Currently, this lock is only used in shrink_memcg(), proactive writeback, and mem_cgroup_css_offline(). Note that shrink_memcg() only acquires the lock of the root cgroup, and mem_cgroup_css_offline() is unlikely to be a hot path.

So, should we keep the spin_lock or go with the cmpxchg() approach? Yosry and Nhat, what are your thoughts on this?

TBH, I think the spinlock is simpler at this point if we need to do all of this explanation to justify correctness of cmpxchg :)

That said, if memcg folks feel like an extra spinlock per cgroup is a bit much, we can go with the cmpxchg() approach. Please include a FAT comment explains the compxchg() approach's nuance in the code though. Speaking from experience, I will forget why it is correct 2 months after the patch lands :)

maintainer
#6 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 10 lines · Nhat Pham

TBH, I think the spinlock is simpler at this point if we need to do all of this explanation to justify correctness of cmpxchg :)

That said, if memcg folks feel like an extra spinlock per cgroup is a bit much, we can go with the cmpxchg() approach. Please include a FAT comment explains the compxchg() approach's nuance in the code though. Speaking from experience, I will forget why it is correct 2 months after the patch lands :)

Another alternative is - can we repurpose any lock here? Locks seem to be per-lruvec or per-node, unfortunately, and we need something per-cgroup hmmmm.

maintainer
#7 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

In reply to Hao Jia
Quoted 31 lines · Hao Jia
Quoted 12 lines · Yosry Ahmed
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Quoted 3 lines

Similar to how memory reclaim uses mem_cgroup_iter() (via struct mem_cgroup_reclaim_iter) and the old shrink_worker() used zswap_next_shrink, we need a shared cursor here.

I think each proactive reclaim invocation just walk the entire subtree for page reclaim right (see shrink_node_memcgs())? Would that be acceptable for you?

I also wonder if we can at least make this structure dynamically allocated... In a system, you only really invoke proactive reclaim against a few target cgroups, no?

author
#8 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 32 lines · Nhat Pham
Quoted 31 lines · Hao Jia
Quoted 12 lines · Yosry Ahmed
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Quoted 7 lines
Quoted 3 lines

Similar to how memory reclaim uses mem_cgroup_iter() (via struct mem_cgroup_reclaim_iter) and the old shrink_worker() used zswap_next_shrink, we need a shared cursor here.

I think each proactive reclaim invocation just walk the entire subtree for page reclaim right (see shrink_node_memcgs())? Would that be acceptable for you?

Our current approach is very similar to how proactive memory reclaim works in shrink_node_memcgs().
shrink_node_memcgs() first calls memcg = mem_cgroup_iter(target_memcg, NULL, partial);. By doing this, it uses target_memcg->nodeinfo[nid]->iter->position to retrieve the child memcg where the last reclaim left off, and then resumes the iteration.

The catch is that zswap can't just reuse memcg->nodeinfo[nid]->iter->position, as that would mess up the cursor used by the memory reclaim.

Quoted 4 lines

I also wonder if we can at least make this structure dynamically allocated... In a system, you only really invoke proactive reclaim against a few target cgroups, no?

It is possible to allocate it dynamically, but I am concerned that it might introduce a slight performance overhead. We would need to add a **check** like if (READ_ONCE(memcg->zswap_wb_iter)) every time zswap_mem_cgroup_iter() is called. Furthermore, to handle concurrent allocations, we might also need to introduce cmpxchg() to resolve race conditions.

The additional code would look something like this:

static struct zswap_wb_iter *get_zswap_wb_iter(struct mem_cgroup *memcg)

{
    struct zswap_wb_iter *iter, *new_iter;

    iter = READ_ONCE(memcg->zswap_wb_iter);
    if (likely(iter))
        return iter;

    new_iter = kzalloc(sizeof(*new_iter), GFP_KERNEL);
    if (!new_iter)
        return NULL;
    spin_lock_init(&new_iter->lock);

    if (cmpxchg(&memcg->zswap_wb_iter, NULL, new_iter) != NULL) {
        /* Lost the race, someone else installed first. */
        kfree(new_iter);
    }
    return READ_ONCE(memcg->zswap_wb_iter);
}

Thanks,
Hao

maintainer
#9 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

In reply to Hao Jia
Quoted 23 lines · Hao Jia
Quoted 12 lines · Yosry Ahmed
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Right, so a fairness concern?

I wonder if we should just reclaim a batch from each memcg, then check if we reached the goal, otherwise start over. If the batch size is small enough that should work?

Quoted 4 lines

Similar to how memory reclaim uses mem_cgroup_iter() (via struct mem_cgroup_reclaim_iter) and the old shrink_worker() used zswap_next_shrink, we need a shared cursor here.

Right, I understand that in theory we need a cursor. I am just wondering if the complexity is justified in practice. Reclaim is a much larger beast than zswap writeback. I wonder if we can just get away with scanning a batch from each child memcg -- for per-memcg reclaim, not global.

We can always improve it later with a cursor if there's an actual need.

Quoted 13 lines
Quoted 12 lines
Quoted 7 lines

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

If we really need to have per-memcg cursors (I am not a big fan), I think we can minimize the overhead by making the cursor updates use atomic cmpxchg instead of having a per-memcg lock.

Quoted 8 lines

Because mem_cgroup_iter() always calls css_put(&prev->css), we cannot simply update zswap_wb_iter.pos via cmpxchg() after calling it. Doing so could lead to a double css_put() issue on prev->css.

Therefore, if we switch to the cmpxchg() approach, we wouldn't be able to reuse the existing mem_cgroup_iter() logic. We would have to write a new function similar to cgroup_iter(), and its implementation might end up looking a bit obscure/complex.

What if we do something like this (for the global cursor):

	do {
		memcg = xchg(zswap_next_shrink, NULL);
		memcg = mem_cgroup_iter(NULL, memcg, NULL);
		/* If the cursor was advanced from under us, try again */
		if (!try_cmpxchg(zswap_next_shrink, NULL, memcg))
			continue;
	} while (..);

There is a window where a racing shrinker will see the cursor as NULL and start over, but that should be fine. We can generalize this for the per-memcg cursor.

That being said..

Quoted 5 lines

Currently, this lock is only used in shrink_memcg(), proactive writeback, and mem_cgroup_css_offline(). Note that shrink_memcg() only acquires the lock of the root cgroup, and mem_cgroup_css_offline() is unlikely to be a hot path.

..this made me realize it's probably fine to just use a global lock for now?

IIUC the only additional contention to the existing lock will be from userspace proactive writeback, and that shouldn't be a big deal especially with the critical section being short?

Quoted 3 lines

So, should we keep the spin_lock or go with the cmpxchg() approach? Yosry and Nhat, what are your thoughts on this?

I think we should experiment with the global lock first. See if you observe any regressions with workloads that put a lot of pressure on the lock (a lot of threads in reclaim doing writeback + a few userspace threads doing proactive writeback). See if the userspace threads actually cause a meaningful regression.

Quoted 9 lines
Quoted 9 lines
Quoted 8 lines

Because the cursor is now per-memcg, the offline cleanup must visit every ancestor that could be holding a reference to the dying memcg. Factor out __zswap_memcg_offline_cleanup() and walk from dead_memcg up to the root.

Quoted 8 lines
Quoted 4 lines

Another reason why I don't like per-memcg cursors. There is too much complexity and I wonder if it's warranted. If we stick with per-memcg cursors please do the refactoring in separate patches to make the patches easier to review.

Sorry about that. I will try to keep each patch as simple as possible in the next version.

No worries, thanks!

Quoted 6 lines

Thanks,
Hao

author
#10 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 30 lines · Yosry Ahmed
Quoted 23 lines · Hao Jia
Quoted 12 lines · Yosry Ahmed
Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

The zswap background writeback worker shrink_worker() uses a global cursor zswap_next_shrink, protected by zswap_shrink_lock, to round-robin across the online memcgs under root_mem_cgroup.

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Quoted 5 lines

Right, so a fairness concern?

I wonder if we should just reclaim a batch from each memcg, then check if we reached the goal, otherwise start over. If the batch size is small enough that should work?

Even with a small batch size, for small writeback requests triggered by user-space (e.g., 2MB, which is batch size * N), it might still repeatedly write back from only the first N child memcgs. This could cause the user-space agent to prematurely give up on zswap writeback.

Quoted 38 lines
Quoted 5 lines

Similar to how memory reclaim uses mem_cgroup_iter() (via struct mem_cgroup_reclaim_iter) and the old shrink_worker() used zswap_next_shrink, we need a shared cursor here.

Right, I understand that in theory we need a cursor. I am just wondering if the complexity is justified in practice. Reclaim is a much larger beast than zswap writeback. I wonder if we can just get away with scanning a batch from each child memcg -- for per-memcg reclaim, not global.

We can always improve it later with a cursor if there's an actual need.

Quoted 13 lines
Quoted 12 lines
Quoted 7 lines

Naturally, group the cursor and its protecting spinlock into a zswap_wb_iter struct, and make it a member of struct mem_cgroup to realize per-memcg cursor management. Accordingly, shrink_worker() now uses the lock and cursor in root_mem_cgroup->zswap_wb_iter.

If we really need to have per-memcg cursors (I am not a big fan), I think we can minimize the overhead by making the cursor updates use atomic cmpxchg instead of having a per-memcg lock.

Quoted 8 lines

Because mem_cgroup_iter() always calls css_put(&prev->css), we cannot simply update zswap_wb_iter.pos via cmpxchg() after calling it. Doing so could lead to a double css_put() issue on prev->css.

Therefore, if we switch to the cmpxchg() approach, we wouldn't be able to reuse the existing mem_cgroup_iter() logic. We would have to write a new function similar to cgroup_iter(), and its implementation might end up looking a bit obscure/complex.

What if we do something like this (for the global cursor):

Quoted 9 lines
	do {
		memcg = xchg(zswap_next_shrink, NULL);
		memcg = mem_cgroup_iter(NULL, memcg, NULL);
		/* If the cursor was advanced from under us, try again */
		if (!try_cmpxchg(zswap_next_shrink, NULL, memcg))
			continue;
	} while (..);

Regarding the code above, IIRC, both the global and per-cgroup cursors suffer from race conditions. This race can cause mem_cgroup_iter(NULL, NULL, NULL) to return the root memcg or its descendants, leading zswap to write back pages from the wrong memcg.

Additionally, since mem_cgroup_iter() puts the prev memcg ref and gets the next memcg ref, a try_cmpxchg() failure on CPU1 might also lead to a ref leak for memcg1.

CPU1 CPU2
memcg1 = xchg(pos, NULL)

                               memcg2 = xchg(pos, NULL) memcg2 = NULL;

memcg1 = mem_cgroup_iter()
mem_cgroup_iter(NULL, **NULL**, NULL) error memcg
try_cmpxchg(pos,NULL,memcg2) succeed
try_cmpxchg(pos,NULL,memcg1) **fail**

I took a stab at implementing a cmpxchg()-based zswap_mem_cgroup_iter() modeled after mem_cgroup_iter(), and it actually doesn't look that complex after all :)

Of course, as Nhat mentioned, we definitely need to add plenty of comments for this function.

static struct mem_cgroup *zswap_mem_cgroup_iter(struct mem_cgroup *root)

{
	struct cgroup_subsys_state *css;
	struct mem_cgroup *pos, *next;

	if (mem_cgroup_disabled())
		return NULL;
	if (!root)
		root = root_mem_cgroup;

	rcu_read_lock();
restart:
	pos = READ_ONCE(root->zswap_wb_iter.pos);
	css = pos ? &pos->css : NULL;
	next = NULL;

	while ((css = css_next_descendant_pre(css, &root->css))) {
		if (css_tryget_online(css))
			break;
	}
	next = css ? mem_cgroup_from_css(css) : NULL;

	if (cmpxchg(&root->zswap_wb_iter.pos, pos, next) != pos) {
		if (next)
			css_put(&next->css);
		goto restart;
	}
	rcu_read_unlock();

	return next;
}
Quoted 12 lines

There is a window where a racing shrinker will see the cursor as NULL and start over, but that should be fine. We can generalize this for the per-memcg cursor.

That being said..

Quoted 5 lines

Currently, this lock is only used in shrink_memcg(), proactive writeback, and mem_cgroup_css_offline(). Note that shrink_memcg() only acquires the lock of the root cgroup, and mem_cgroup_css_offline() is unlikely to be a hot path.

Quoted 7 lines

..this made me realize it's probably fine to just use a global lock for now?

IIUC the only additional contention to the existing lock will be from userspace proactive writeback, and that shouldn't be a big deal especially with the critical section being short?

In the current patch implementation, this lock protects the cgroup's own cursor variable. During each writeback, we only acquire the spin_lock of the target cgroup itself; we do not attempt to **spin on any child cgroup's lock while iterating through the descendants**.

Specifically:

- shrink_memcg() will only attempt to acquire the root cgroup's lock
throughout the entire process.
- Proactive writeback will only acquire the lock of the target cgroup
**itself**.
- Only mem_cgroup_css_offline() might attempt to hold locks of other
cgroups, but normally, this shouldn't be a hot path.

Therefore, even if proactive writebacks are triggered concurrently on a parent cgroup and its child cgroup, there will be **no** lock contention at all (specifically referring to zswap_wb_iter.lock).

Lock contention would only occur if user-space **concurrently** triggers proactive writeback on the exact **same** cgroup. And IIRC, in such a scenario, the bottleneck is more likely to be on other locks anyway.

Quoted 9 lines
Quoted 3 lines

So, should we keep the spin_lock or go with the cmpxchg() approach? Yosry and Nhat, what are your thoughts on this?

I think we should experiment with the global lock first. See if you observe any regressions with workloads that put a lot of pressure on the lock (a lot of threads in reclaim doing writeback + a few userspace threads doing proactive writeback). See if the userspace threads actually cause a meaningful regression.

Sorry, it seems there are some implementation issues with the global lock approach.

In practice, our user-space agent mostly operates in the following two scenarios:
- Triggering proactive writeback on the same cgroup at different times
(sequentially).
- Triggering proactive writeback on different cgroups at the same time
(concurrently).

In both cases, there is no lock contention. So, the current lock works perfectly fine for us.

However, if we really hate zswap_wb_iter.lock, I can try replacing it with the cmpxchg() approach.

Thanks,
Hao

maintainer
#11 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 21 lines
Quoted 21 lines
Quoted 13 lines
Quoted 5 lines
Quoted 4 lines

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Right, so a fairness concern?

Quoted 7 lines
Quoted 3 lines

I wonder if we should just reclaim a batch from each memcg, then check if we reached the goal, otherwise start over. If the batch size is small enough that should work?

Even with a small batch size, for small writeback requests triggered by user-space (e.g., 2MB, which is batch size * N), it might still repeatedly write back from only the first N child memcgs.

Yes, I understand, I am asking if this is a problem in practice. For this to be a problem we'd need to trigger small writeback requests and have many memcgs.

Quoted 2 lines

This could cause the user-space agent to prematurely give up on zswap writeback.

Why? The kernel should not return before trying to writeback from all memcgs. If we scan the first N child memcgs and did not writeback enough, we should keep going, right?

Quoted 12 lines
Quoted 11 lines

What if we do something like this (for the global cursor):

	do {
		memcg = xchg(zswap_next_shrink, NULL);
		memcg = mem_cgroup_iter(NULL, memcg, NULL);
		/* If the cursor was advanced from under us, try again */
		if (!try_cmpxchg(zswap_next_shrink, NULL, memcg))
			continue;
	} while (..);
Quoted 4 lines

Regarding the code above, IIRC, both the global and per-cgroup cursors suffer from race conditions. This race can cause mem_cgroup_iter(NULL, NULL, NULL) to return the root memcg or its descendants, leading zswap to write back pages from the wrong memcg.

Not the wrong memcg, it will just go back to the first memcg again, which should be fine as I mentioned below.

Quoted 6 lines

Additionally, since mem_cgroup_iter() puts the prev memcg ref and gets the next memcg ref, a try_cmpxchg() failure on CPU1 might also lead to a ref leak for memcg1.

Quoted 8 lines

CPU1 CPU2
memcg1 = xchg(pos, NULL)

                               memcg2 = xchg(pos, NULL) memcg2 = NULL;

memcg1 = mem_cgroup_iter()
mem_cgroup_iter(NULL, **NULL**, NULL) error memcg
try_cmpxchg(pos,NULL,memcg2) succeed
try_cmpxchg(pos,NULL,memcg1) **fail**

Yes, we can probably just take a ref on the memcg before calling mem_cgroup_iter(). That being said, I think we can just keep the lock, see below.

Quoted 4 lines

I took a stab at implementing a cmpxchg()-based zswap_mem_cgroup_iter() modeled after mem_cgroup_iter(), and it actually doesn't look that complex after all :)

I don't think we should re-implement mem_cgroup_iter() here.

[..]

Quoted 20 lines
Quoted 12 lines

There is a window where a racing shrinker will see the cursor as NULL and start over, but that should be fine. We can generalize this for the per-memcg cursor.

That being said..

Quoted 5 lines

Currently, this lock is only used in shrink_memcg(), proactive writeback, and mem_cgroup_css_offline(). Note that shrink_memcg() only acquires the lock of the root cgroup, and mem_cgroup_css_offline() is unlikely to be a hot path.

Quoted 7 lines

..this made me realize it's probably fine to just use a global lock for now?

IIUC the only additional contention to the existing lock will be from userspace proactive writeback, and that shouldn't be a big deal especially with the critical section being short?

Quoted 4 lines

In the current patch implementation, this lock protects the cgroup's own cursor variable. During each writeback, we only acquire the spin_lock of the target cgroup itself; we do not attempt to **spin on any child cgroup's lock while iterating through the descendants**.

Oh, I did not say anything about the current patch adding contention. I am suggesting we just keep using the global lock for the per-memcg cursors, if we keep them.

Right now, without this series, the global lock protects against concurrent changes to the global cursor from concurrent shrinkers. After the series, the only added contenders are userspace proactive writeback threads. Unless you have 10s or 100s of those, it should be fine to keep a single global lock, right?

Yes, userspace can affect writeback efficiency, but we can split the lock when it actually causes a problem.

Quoted 22 lines
Quoted 11 lines
Quoted 5 lines

So, should we keep the spin_lock or go with the cmpxchg() approach? Yosry and Nhat, what are your thoughts on this?

I think we should experiment with the global lock first. See if you observe any regressions with workloads that put a lot of pressure on the lock (a lot of threads in reclaim doing writeback + a few userspace threads doing proactive writeback). See if the userspace threads actually cause a meaningful regression.

Sorry, it seems there are some implementation issues with the global lock approach.

In practice, our user-space agent mostly operates in the following two scenarios:
- Triggering proactive writeback on the same cgroup at different times
(sequentially).
- Triggering proactive writeback on different cgroups at the same time
(concurrently).

Quoted 2 lines

In both cases, there is no lock contention. So, the current lock works perfectly fine for us.

Would using the existing global lock work for your use case? How many different cgroups can you end up reclaiming from concurrently?

Quoted 6 lines

However, if we really hate zswap_wb_iter.lock, I can try replacing it with the cmpxchg() approach.

Thanks,
Hao

author
#12 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 33 lines · Yosry Ahmed
Quoted 21 lines
Quoted 21 lines
Quoted 13 lines
Quoted 5 lines
Quoted 4 lines

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Right, so a fairness concern?

Quoted 7 lines
Quoted 3 lines

I wonder if we should just reclaim a batch from each memcg, then check if we reached the goal, otherwise start over. If the batch size is small enough that should work?

Even with a small batch size, for small writeback requests triggered by user-space (e.g., 2MB, which is batch size * N), it might still repeatedly write back from only the first N child memcgs.

Yes, I understand, I am asking if this is a problem in practice. For this to be a problem we'd need to trigger small writeback requests and have many memcgs.

Quoted 7 lines
Quoted 2 lines

This could cause the user-space agent to prematurely give up on zswap writeback.

Why? The kernel should not return before trying to writeback from all memcgs. If we scan the first N child memcgs and did not writeback enough, we should keep going, right?

Yes, this issue is not caused by the kernel, but rather by our user-space agent itself.

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Of course, real-world scenarios are much more complex, and this kind of case is extremely rare in our environment.

That being said, your suggestion of using the global lock for the per-memcg cursors makes the writeback fairer and would resolve these corner cases.

Quoted 76 lines
Quoted 12 lines
Quoted 11 lines

What if we do something like this (for the global cursor):

	do {
		memcg = xchg(zswap_next_shrink, NULL);
		memcg = mem_cgroup_iter(NULL, memcg, NULL);
		/* If the cursor was advanced from under us, try again */
		if (!try_cmpxchg(zswap_next_shrink, NULL, memcg))
			continue;
	} while (..);
Quoted 4 lines

Regarding the code above, IIRC, both the global and per-cgroup cursors suffer from race conditions. This race can cause mem_cgroup_iter(NULL, NULL, NULL) to return the root memcg or its descendants, leading zswap to write back pages from the wrong memcg.

Not the wrong memcg, it will just go back to the first memcg again, which should be fine as I mentioned below.

Quoted 6 lines

Additionally, since mem_cgroup_iter() puts the prev memcg ref and gets the next memcg ref, a try_cmpxchg() failure on CPU1 might also lead to a ref leak for memcg1.

Quoted 8 lines

CPU1 CPU2
memcg1 = xchg(pos, NULL)

                                memcg2 = xchg(pos, NULL) memcg2 = NULL;

memcg1 = mem_cgroup_iter()
mem_cgroup_iter(NULL, **NULL**, NULL) error memcg
try_cmpxchg(pos,NULL,memcg2) succeed
try_cmpxchg(pos,NULL,memcg1) **fail**

Yes, we can probably just take a ref on the memcg before calling mem_cgroup_iter(). That being said, I think we can just keep the lock, see below.

Quoted 4 lines

I took a stab at implementing a cmpxchg()-based zswap_mem_cgroup_iter() modeled after mem_cgroup_iter(), and it actually doesn't look that complex after all :)

I don't think we should re-implement mem_cgroup_iter() here.

[..]

Quoted 20 lines
Quoted 12 lines

There is a window where a racing shrinker will see the cursor as NULL and start over, but that should be fine. We can generalize this for the per-memcg cursor.

That being said..

Quoted 5 lines

Currently, this lock is only used in shrink_memcg(), proactive writeback, and mem_cgroup_css_offline(). Note that shrink_memcg() only acquires the lock of the root cgroup, and mem_cgroup_css_offline() is unlikely to be a hot path.

Quoted 7 lines

..this made me realize it's probably fine to just use a global lock for now?

IIUC the only additional contention to the existing lock will be from userspace proactive writeback, and that shouldn't be a big deal especially with the critical section being short?

Quoted 4 lines

In the current patch implementation, this lock protects the cgroup's own cursor variable. During each writeback, we only acquire the spin_lock of the target cgroup itself; we do not attempt to **spin on any child cgroup's lock while iterating through the descendants**.

Oh, I did not say anything about the current patch adding contention. I am suggesting we just keep using the global lock for the per-memcg cursors, if we keep them.

Quoted 6 lines

Right now, without this series, the global lock protects against concurrent changes to the global cursor from concurrent shrinkers. After the series, the only added contenders are userspace proactive writeback threads. Unless you have 10s or 100s of those, it should be fine to keep a single global lock, right?

Ah yes, sorry about that, I misunderstood what you meant. Thanks a lot for the suggestion and for taking the time to explain it so patiently. I'll switch to using the global lock in v4 patch.

Quoted 2 lines

Yes, userspace can affect writeback efficiency, but we can split the lock when it actually causes a problem.

Agreed.

Quoted 23 lines
Quoted 23 lines
Quoted 4 lines
Quoted 4 lines
Quoted 8 lines
Quoted 2 lines

So, should we keep the spin_lock or go with the cmpxchg() approach? Yosry and Nhat, what are your thoughts on this?

I think we should experiment with the global lock first. See if you observe any regressions with workloads that put a lot of pressure on the lock (a lot of threads in reclaim doing writeback + a few userspace threads doing proactive writeback). See if the userspace threads actually cause a meaningful regression.

Sorry, it seems there are some implementation issues with the global lock approach.

In practice, our user-space agent mostly operates in the following two scenarios:
- Triggering proactive writeback on the same cgroup at different times
(sequentially).
- Triggering proactive writeback on different cgroups at the same time
(concurrently).

Quoted 5 lines
Quoted 2 lines

In both cases, there is no lock contention. So, the current lock works perfectly fine for us.

Would using the existing global lock work for your use case? How many different cgroups can you end up reclaiming from concurrently?

It should work fine. We typically only have a dozen or so user-space agents triggering zswap writeback, and the critical section is very short anyway. I will implement this next.

Thanks,
Hao

maintainer
#13 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 47 lines · Hao Jia
Quoted 33 lines · Yosry Ahmed
Quoted 21 lines
Quoted 21 lines
Quoted 13 lines
Quoted 5 lines
Quoted 4 lines

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Right, so a fairness concern?

Quoted 7 lines
Quoted 3 lines

I wonder if we should just reclaim a batch from each memcg, then check if we reached the goal, otherwise start over. If the batch size is small enough that should work?

Even with a small batch size, for small writeback requests triggered by user-space (e.g., 2MB, which is batch size * N), it might still repeatedly write back from only the first N child memcgs.

Yes, I understand, I am asking if this is a problem in practice. For this to be a problem we'd need to trigger small writeback requests and have many memcgs.

Quoted 7 lines
Quoted 2 lines

This could cause the user-space agent to prematurely give up on zswap writeback.

Why? The kernel should not return before trying to writeback from all memcgs. If we scan the first N child memcgs and did not writeback enough, we should keep going, right?

Yes, this issue is not caused by the kernel, but rather by our user-space agent itself.

Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

Quoted 5 lines

Of course, real-world scenarios are much more complex, and this kind of case is extremely rare in our environment.

That being said, your suggestion of using the global lock for the per-memcg cursors makes the writeback fairer and would resolve these corner cases.

Right, but I'd rather not do per-memcg cursors at all if we can avoid it. Will using batches help make reclaim fair over all memcgs without a cursor?

We can always add the cursor later if needed.

author
#14 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 57 lines · Yosry Ahmed
Quoted 47 lines · Hao Jia
Quoted 33 lines · Yosry Ahmed
Quoted 21 lines
Quoted 21 lines
Quoted 13 lines
Quoted 5 lines
Quoted 4 lines

Proactive writeback also wants a similar per-memcg cursor that is scoped to the specified memcg, so that repeated invocations against the same memcg make forward progress across its descendant memcgs instead of restarting from the first child memcg each time.

Quoted 7 lines

Is this a problem in practice?

Is the concern the overhead of scanning memcgs repeatedly, or lack of fairness? I wonder if we should just do writeback in batches from all memcgs, similar to how reclaim does it, then evaluate at the end if we need to start over?

Quoted 5 lines

Not using a per-cgroup cursor will cause issues for "repeated small-budget calls" cases. For example, repeatedly triggering a 2MB writeback might result in only writing back pages from the first few child memcgs every time. In the worst-case scenario (where the writeback amount is less than WB_BATCH), it might only ever write back from the first child memcg.

Right, so a fairness concern?

Quoted 7 lines
Quoted 3 lines

I wonder if we should just reclaim a batch from each memcg, then check if we reached the goal, otherwise start over. If the batch size is small enough that should work?

Even with a small batch size, for small writeback requests triggered by user-space (e.g., 2MB, which is batch size * N), it might still repeatedly write back from only the first N child memcgs.

Yes, I understand, I am asking if this is a problem in practice. For this to be a problem we'd need to trigger small writeback requests and have many memcgs.

Quoted 7 lines
Quoted 2 lines

This could cause the user-space agent to prematurely give up on zswap writeback.

Why? The kernel should not return before trying to writeback from all memcgs. If we scan the first N child memcgs and did not writeback enough, we should keep going, right?

Yes, this issue is not caused by the kernel, but rather by our user-space agent itself.

Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Quoted 4 lines

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

I gave it some thought. Not using a cursor could lead to unfairness issues with certain writeback sizes:

- If the writeback size is an odd multiple of WB_BATCH (e.g.,
triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the writeback ratio might end up being 2:1.
- If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
triggered, it might repeatedly write back from only the first 2 child cgroups.

Although setting a smaller WB_BATCH might mitigate this unfairness, it could hurt writeback efficiency. Let's just use per-memcg cursors to completely fix these corner cases.

Thanks,
Hao

Quoted 11 lines
Quoted 5 lines

Of course, real-world scenarios are much more complex, and this kind of case is extremely rare in our environment.

That being said, your suggestion of using the global lock for the per-memcg cursors makes the writeback fairer and would resolve these corner cases.

Right, but I'd rather not do per-memcg cursors at all if we can avoid it. Will using batches help make reclaim fair over all memcgs without a cursor?

We can always add the cursor later if needed.

maintainer
#15 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 24 lines
Quoted 9 lines
Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Quoted 4 lines

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

I gave it some thought. Not using a cursor could lead to unfairness issues with certain writeback sizes:

- If the writeback size is an odd multiple of WB_BATCH (e.g.,
triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the writeback ratio might end up being 2:1.
- If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
triggered, it might repeatedly write back from only the first 2 child cgroups.

Quoted 3 lines

Although setting a smaller WB_BATCH might mitigate this unfairness, it could hurt writeback efficiency. Let's just use per-memcg cursors to completely fix these corner cases.

Exactly, the batch size should be small enough that any unfairness is not a problem. I would honestly just do batching without a per-memcg cursor, unless we have numbers to prove that the efficiency is affected when we use a small batch size. Let's only introduce complexity when needed please.

author
#16 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 28 lines · Yosry Ahmed
Quoted 24 lines
Quoted 9 lines
Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Quoted 4 lines

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

I gave it some thought. Not using a cursor could lead to unfairness issues with certain writeback sizes:

- If the writeback size is an odd multiple of WB_BATCH (e.g.,
triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the writeback ratio might end up being 2:1.
- If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
triggered, it might repeatedly write back from only the first 2 child cgroups.

Quoted 3 lines

Although setting a smaller WB_BATCH might mitigate this unfairness, it could hurt writeback efficiency. Let's just use per-memcg cursors to completely fix these corner cases.

Quoted 5 lines

Exactly, the batch size should be small enough that any unfairness is not a problem. I would honestly just do batching without a per-memcg cursor, unless we have numbers to prove that the efficiency is affected when we use a small batch size. Let's only introduce complexity when needed please.

If you prefer not to use per-cgroup cursors, do we still need to keep the global cursor (i.e., the root cgroup's cursor) zswap_next_shrink? I found this part to be quite tricky when trying to reuse the main logic of shrink_worker() in zswap_proactive_writeback().

Of course, I think we could also keep zswap_next_shrink and write a small helper to check if it's the root cgroup, allowing us to use different memcg iteration methods.

Thanks,
Hao

maintainer
#17 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 39 lines · Hao Jia
Quoted 28 lines · Yosry Ahmed
Quoted 24 lines
Quoted 9 lines
Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Quoted 4 lines

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

I gave it some thought. Not using a cursor could lead to unfairness issues with certain writeback sizes:

- If the writeback size is an odd multiple of WB_BATCH (e.g.,
triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the writeback ratio might end up being 2:1.
- If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
triggered, it might repeatedly write back from only the first 2 child cgroups.

Quoted 3 lines

Although setting a smaller WB_BATCH might mitigate this unfairness, it could hurt writeback efficiency. Let's just use per-memcg cursors to completely fix these corner cases.

Quoted 5 lines

Exactly, the batch size should be small enough that any unfairness is not a problem. I would honestly just do batching without a per-memcg cursor, unless we have numbers to prove that the efficiency is affected when we use a small batch size. Let's only introduce complexity when needed please.

Quoted 8 lines

If you prefer not to use per-cgroup cursors, do we still need to keep the global cursor (i.e., the root cgroup's cursor) zswap_next_shrink? I found this part to be quite tricky when trying to reuse the main logic of shrink_worker() in zswap_proactive_writeback().

Of course, I think we could also keep zswap_next_shrink and write a small helper to check if it's the root cgroup, allowing us to use different memcg iteration methods.

I think we want to keep the global cursor, at least for now.

maintainer
#18 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

In reply to Hao Jia
Quoted 32 lines · Hao Jia
Quoted 28 lines · Yosry Ahmed
Quoted 27 lines
Quoted 9 lines
Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Quoted 4 lines

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

I gave it some thought. Not using a cursor could lead to unfairness issues with certain writeback sizes:

- If the writeback size is an odd multiple of WB_BATCH (e.g.,
triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the writeback ratio might end up being 2:1.
- If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
triggered, it might repeatedly write back from only the first 2 child cgroups.

Although setting a smaller WB_BATCH might mitigate this unfairness, it could hurt writeback efficiency. Let's just use per-memcg cursors to completely fix these corner cases.

Quoted 5 lines
Quoted 5 lines

Exactly, the batch size should be small enough that any unfairness is not a problem. I would honestly just do batching without a per-memcg cursor, unless we have numbers to prove that the efficiency is affected when we use a small batch size. Let's only introduce complexity when needed please.

I'm impartial towards the complexity of per-memcg cursor. I don't think it's that big of a deal, but only if it's warranted.

Hao, if you're convinced that doing small batch is not efficient, could you run some experiments to show the improvement bigger batchign and fairness? Maybe implement a small batch, no-memcg cursor first. Then implement a patch on top of it to add per-memcg cursor, and show how much performance win we can get from that patch on top of the patch series?

FWIW, zswap writeback right now is not that batch-efficient :) There is no IO batching, or batched lock operations (we drop the lock whenever we attempt to writeback a page), etc. Might be a good avenue to optimize.

author
#19 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 42 lines · Nhat Pham
Quoted 32 lines · Hao Jia
Quoted 28 lines · Yosry Ahmed
Quoted 27 lines
Quoted 9 lines
Quoted 8 lines

For instance, suppose a parent memcg has two children, memcg1 and memcg2, each with 200MB of zswap (100MB inactive). Triggering proactive writeback on the parent memcg will exhaust memcg1's inactive zswap pages. After that, even though memcg2 still has plenty of inactive zswap pages, it will continue to write back memcg1's active zswap pages. Writing back active zswap pages causes the user-space agent to prematurely abort the writeback because it detects that certain memcg metrics have exceeded predefined thresholds.

Quoted 4 lines

This will only happen if the reclaim size is smaller than the batch size, right? Otherwise the kernel should reclaim more or less equally from both memcgs?

I gave it some thought. Not using a cursor could lead to unfairness issues with certain writeback sizes:

- If the writeback size is an odd multiple of WB_BATCH (e.g.,
triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the writeback ratio might end up being 2:1.
- If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
triggered, it might repeatedly write back from only the first 2 child cgroups.

Although setting a smaller WB_BATCH might mitigate this unfairness, it could hurt writeback efficiency. Let's just use per-memcg cursors to completely fix these corner cases.

Quoted 5 lines
Quoted 5 lines

Exactly, the batch size should be small enough that any unfairness is not a problem. I would honestly just do batching without a per-memcg cursor, unless we have numbers to prove that the efficiency is affected when we use a small batch size. Let's only introduce complexity when needed please.

I'm impartial towards the complexity of per-memcg cursor. I don't think it's that big of a deal, but only if it's warranted.

Quoted 7 lines

Hao, if you're convinced that doing small batch is not efficient, could you run some experiments to show the improvement bigger batchign and fairness? Maybe implement a small batch, no-memcg cursor first. Then implement a patch on top of it to add per-memcg cursor, and show how much performance win we can get from that patch on top of the patch series?

Thanks for the suggestion!

I ran some tests and found that neither the per-memcg cursor nor different batch sizes have a significant impact on proactive writeback performance. However, exactly as we suspected, without the per-memcg cursor, the writeback distribution among child memcgs is highly unfair.

Test Setup:

zswap config: 18G capacity, LZ4 compression.
cgroup hierarchy: 1 parent test memcg with 10 child memcgs.
Allocation: Allocated 1600MB of anonymous pages in each child memcg.
To ensure compressibility, the first half of each page was filled with random data and the second half with zeros.
Force to zswap: Ran echo "1600M" > memory.reclaim on each child memcg
to squeeze all their memory into zswap.
Trigger writeback: Ran echo "<size> zswap_writeback_only" >
memory.reclaim on the parent cgroup 200 times, with a 2-second interval between each run.
Metric: Monitored the zswpwb_proactive metric in memory.stat to
observe the writeback volume.
**Note**: The size here refers to the uncompressed memory size. Also,
since the second-chance algorithm would cause many writebacks to fall short of the target size, I **bypassed** it during these tests to avoid interference.

Without cursor (size: 1M, batch: 32)

  child        wb_pages        wb_MB     share%
  child0           6368        24.88      12.50
  child1           6368        24.88      12.50
  child2           6368        24.88      12.50
  child3           6368        24.88      12.50
  child4           6368        24.88      12.50
  child5           6368        24.88      12.50
  child6           6368        24.88      12.50
  child7           6368        24.88      12.50
  child8              0         0.00       0.00
  child9              0         0.00       0.00
Without cursor (size: 1M, batch: 128)
  child        wb_pages        wb_MB     share%
  child0          25472        99.50      50.00
  child1          25472        99.50      50.00
  child2              0         0.00       0.00
  child3              0         0.00       0.00
  child4              0         0.00       0.00
  child5              0         0.00       0.00
  child6              0         0.00       0.00
  child7              0         0.00       0.00
  child8              0         0.00       0.00
  child9              0         0.00       0.00
Without cursor (size: 6M, batch: 128)
  child        wb_pages        wb_MB     share%
  child0          51200       200.00      16.67
  child1          51200       200.00      16.67
  child2          25600       100.00       8.33
  child3          25600       100.00       8.33
  child4          25600       100.00       8.33
  child5          25600       100.00       8.33
  child6          25600       100.00       8.33
  child7          25600       100.00       8.33
  child8          25600       100.00       8.33
  child9          25600       100.00       8.33

With cursor (size: 1M, batch: 32)

  child        wb_pages        wb_MB     share%
  child0           5120        20.00      10.00
  child1           5120        20.00      10.00
  child2           5120        20.00      10.00
  child3           5120        20.00      10.00
  child4           5120        20.00      10.00
  child5           5120        20.00      10.00
  child6           5120        20.00      10.00
  child7           5120        20.00      10.00
  child8           5120        20.00      10.00
  child9           5120        20.00      10.00
With cursor (size: 1M, batch: 128)
  child        wb_pages        wb_MB     share%
  child0           5120        20.00      10.00
  child1           5120        20.00      10.00
  child2           5120        20.00      10.00
  child3           5120        20.00      10.00
  child4           5120        20.00      10.00
  child5           5120        20.00      10.00
  child6           5120        20.00      10.00
  child7           5120        20.00      10.00
  child8           5120        20.00      10.00
  child9           5120        20.00      10.00

Thakns,
Hao

maintainer
#20 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 83 lines · Hao Jia
Quoted 1 line · Nhat Pham

Thanks for the suggestion!

I ran some tests and found that neither the per-memcg cursor nor different batch sizes have a significant impact on proactive writeback performance. However, exactly as we suspected, without the per-memcg cursor, the writeback distribution among child memcgs is highly unfair.

Test Setup:

zswap config: 18G capacity, LZ4 compression.
cgroup hierarchy: 1 parent test memcg with 10 child memcgs.
Allocation: Allocated 1600MB of anonymous pages in each child memcg.
To ensure compressibility, the first half of each page was filled with random data and the second half with zeros.
Force to zswap: Ran echo "1600M" > memory.reclaim on each child memcg
to squeeze all their memory into zswap.
Trigger writeback: Ran echo "<size> zswap_writeback_only" >
memory.reclaim on the parent cgroup 200 times, with a 2-second interval between each run.
Metric: Monitored the zswpwb_proactive metric in memory.stat to
observe the writeback volume.
**Note**: The size here refers to the uncompressed memory size. Also,
since the second-chance algorithm would cause many writebacks to fall short of the target size, I **bypassed** it during these tests to avoid interference.

Without cursor (size: 1M, batch: 32)

   child        wb_pages        wb_MB     share%
   child0           6368        24.88      12.50
   child1           6368        24.88      12.50
   child2           6368        24.88      12.50
   child3           6368        24.88      12.50
   child4           6368        24.88      12.50
   child5           6368        24.88      12.50
   child6           6368        24.88      12.50
   child7           6368        24.88      12.50
   child8              0         0.00       0.00
   child9              0         0.00       0.00
Without cursor (size: 1M, batch: 128)
   child        wb_pages        wb_MB     share%
   child0          25472        99.50      50.00
   child1          25472        99.50      50.00
   child2              0         0.00       0.00
   child3              0         0.00       0.00
   child4              0         0.00       0.00
   child5              0         0.00       0.00
   child6              0         0.00       0.00
   child7              0         0.00       0.00
   child8              0         0.00       0.00
   child9              0         0.00       0.00
Without cursor (size: 6M, batch: 128)
   child        wb_pages        wb_MB     share%
   child0          51200       200.00      16.67
   child1          51200       200.00      16.67
   child2          25600       100.00       8.33
   child3          25600       100.00       8.33
   child4          25600       100.00       8.33
   child5          25600       100.00       8.33
   child6          25600       100.00       8.33
   child7          25600       100.00       8.33
   child8          25600       100.00       8.33
   child9          25600       100.00       8.33

With cursor (size: 1M, batch: 32)

   child        wb_pages        wb_MB     share%
   child0           5120        20.00      10.00
   child1           5120        20.00      10.00
   child2           5120        20.00      10.00
   child3           5120        20.00      10.00
   child4           5120        20.00      10.00
   child5           5120        20.00      10.00
   child6           5120        20.00      10.00
   child7           5120        20.00      10.00
   child8           5120        20.00      10.00
   child9           5120        20.00      10.00
With cursor (size: 1M, batch: 128)
   child        wb_pages        wb_MB     share%
   child0           5120        20.00      10.00
   child1           5120        20.00      10.00
Quoted 9 lines
   child2           5120        20.00      10.00
   child3           5120        20.00      10.00
   child4           5120        20.00      10.00
   child5           5120        20.00      10.00
   child6           5120        20.00      10.00
   child7           5120        20.00      10.00
   child8           5120        20.00      10.00
   child9           5120        20.00      10.00

Yeah OTOH, we don't really make fairness an API contract here. When you set up a proactive reclaim scheme, if you decide to target a cgroup (and not its children separately), everything underneath it is fair game to the kernel in any split that we fancy. If you want true fairness or a desired split, you have to treat them as independent memory domains and set up proactive reclaim to hit each child cgroup separately (i.e one "echo > memory.reclaim" for each of them). This is necessary for example if each child represents a separate, isolated service/container/tenant. And maybe this is actually what you really want - hit the ancestor cgroup very lightly for the stuff it owns, but then dedidcate most of the reclaim effort at the leaf cgroups independently?

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Yosry, what's the concern here? Is it space overhead, or overall code complexity?

maintainer
#21 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 85 lines · Nhat Pham
Quoted 83 lines · Hao Jia
Quoted 1 line · Nhat Pham

Thanks for the suggestion!

I ran some tests and found that neither the per-memcg cursor nor different batch sizes have a significant impact on proactive writeback performance. However, exactly as we suspected, without the per-memcg cursor, the writeback distribution among child memcgs is highly unfair.

Test Setup:

zswap config: 18G capacity, LZ4 compression.
cgroup hierarchy: 1 parent test memcg with 10 child memcgs.
Allocation: Allocated 1600MB of anonymous pages in each child memcg.
To ensure compressibility, the first half of each page was filled with random data and the second half with zeros.
Force to zswap: Ran echo "1600M" > memory.reclaim on each child memcg
to squeeze all their memory into zswap.
Trigger writeback: Ran echo "<size> zswap_writeback_only" >
memory.reclaim on the parent cgroup 200 times, with a 2-second interval between each run.
Metric: Monitored the zswpwb_proactive metric in memory.stat to
observe the writeback volume.
**Note**: The size here refers to the uncompressed memory size. Also,
since the second-chance algorithm would cause many writebacks to fall short of the target size, I **bypassed** it during these tests to avoid interference.

Without cursor (size: 1M, batch: 32)

   child        wb_pages        wb_MB     share%
   child0           6368        24.88      12.50
   child1           6368        24.88      12.50
   child2           6368        24.88      12.50
   child3           6368        24.88      12.50
   child4           6368        24.88      12.50
   child5           6368        24.88      12.50
   child6           6368        24.88      12.50
   child7           6368        24.88      12.50
   child8              0         0.00       0.00
   child9              0         0.00       0.00
Without cursor (size: 1M, batch: 128)
   child        wb_pages        wb_MB     share%
   child0          25472        99.50      50.00
   child1          25472        99.50      50.00
   child2              0         0.00       0.00
   child3              0         0.00       0.00
   child4              0         0.00       0.00
   child5              0         0.00       0.00
   child6              0         0.00       0.00
   child7              0         0.00       0.00
   child8              0         0.00       0.00
   child9              0         0.00       0.00
Without cursor (size: 6M, batch: 128)
   child        wb_pages        wb_MB     share%
   child0          51200       200.00      16.67
   child1          51200       200.00      16.67
   child2          25600       100.00       8.33
   child3          25600       100.00       8.33
   child4          25600       100.00       8.33
   child5          25600       100.00       8.33
   child6          25600       100.00       8.33
   child7          25600       100.00       8.33
   child8          25600       100.00       8.33
   child9          25600       100.00       8.33

With cursor (size: 1M, batch: 32)

   child        wb_pages        wb_MB     share%
   child0           5120        20.00      10.00
   child1           5120        20.00      10.00
   child2           5120        20.00      10.00
   child3           5120        20.00      10.00
   child4           5120        20.00      10.00
   child5           5120        20.00      10.00
   child6           5120        20.00      10.00
   child7           5120        20.00      10.00
   child8           5120        20.00      10.00
   child9           5120        20.00      10.00
With cursor (size: 1M, batch: 128)
   child        wb_pages        wb_MB     share%
   child0           5120        20.00      10.00
   child1           5120        20.00      10.00
Quoted 8 lines
Quoted 8 lines
   child2           5120        20.00      10.00
   child3           5120        20.00      10.00
   child4           5120        20.00      10.00
   child5           5120        20.00      10.00
   child6           5120        20.00      10.00
   child7           5120        20.00      10.00
   child8           5120        20.00      10.00
   child9           5120        20.00      10.00

Yes, the per-memcg cursor is more fair, and you can synthesize scenarios that show that. However, I don't think this is a problem in practice:

1. The unfairness is limited to the batch size per-invocation. If the batch size is 128 pages (your highest one here), that's 0.5 MB (on x86), which is fairly low? If the batch size is 32, it's even less.

2. Realistically, if you have a parent cgroup with with >10G of memory, you wouldn't be reclaiming in steps of 1M. If you want to reclaim 200MB, why are you doing it over 200 invocations? If you do it in a single one (or over a few retries) the shares should become much more even.

We're trying to fix a practical use case, not finding reasons why a simple implementation won't work -- right?

More below (to Nhat's point).

Quoted 13 lines

Yeah OTOH, we don't really make fairness an API contract here. When you set up a proactive reclaim scheme, if you decide to target a cgroup (and not its children separately), everything underneath it is fair game to the kernel in any split that we fancy. If you want true fairness or a desired split, you have to treat them as independent memory domains and set up proactive reclaim to hit each child cgroup separately (i.e one "echo > memory.reclaim" for each of them). This is necessary for example if each child represents a separate, isolated service/container/tenant. And maybe this is actually what you really want - hit the ancestor cgroup very lightly for the stuff it owns, but then dedidcate most of the reclaim effort at the leaf cgroups independently?

I would go a bit farther and claim that ideally fairness shouldn't even be a factor. If you invoke proactive reclaim on a parent cgroup with 100MB, you want to reclaim the coldest 100MB in that parent, no matter what child they reside in. If one child cgroup has 100% hot memory and one child cgroup has 100% cold memory, ideally you'd reclaim all the cold memory from the second child.

However, the implementation of the LRUs and the coldness tracking doesn't allow for doing this, so we "fallback" to reclaiming in batches from each child because we don't really know where the coldest pages overall are. If that changes in the future (somehow), I argue that the correct thing to do is reclaim the absolute coldest memory at the parent level.

If you want to reclaim evenly among the children, you can do that and directly reclaim from the children.

Quoted 8 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Yosry, what's the concern here? Is it space overhead, or overall code complexity?

Mostly the complexity (e.g. the zombie memcg cleanup) and a tiny bit the unnecessary space (8 bytes is not a lot, but these things add up).

maintainer
#22 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

In reply to Nhat Pham
Quoted 4 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

maintainer
#23 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 11 lines · Yosry Ahmed
Quoted 5 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

Ok then yeah, I think we should shelve per-memcg cursor for the next version. Down the line, if we have more data that unfairness is an issue, we can always fix it. One step at a time :)

author
#24 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 13 lines · Nhat Pham
Quoted 11 lines · Yosry Ahmed
Quoted 5 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

Quoted 3 lines

Ok then yeah, I think we should shelve per-memcg cursor for the next version. Down the line, if we have more data that unfairness is an issue, we can always fix it. One step at a time :)

Thanks a lot to Yosry, Nhat, and Shakeel for the great suggestions!

Let me summarize what I plan to do in the next version to make sure we are on the same page:

- Drop the per-memcg cursor and keep the root cgroup cursor
(zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the
proactive writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and
shrink_memcg(). Enable batch writeback in the shrink_worker() path, while keeping the writeback behavior in the zswap_store() path unchanged.

Please let me know if I missed or misunderstood anything. Thanks again for clearing things up!

Thanks,
Hao

maintainer
#25 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 33 lines · Hao Jia
Quoted 13 lines · Nhat Pham
Quoted 11 lines · Yosry Ahmed
Quoted 5 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

Quoted 3 lines

Ok then yeah, I think we should shelve per-memcg cursor for the next version. Down the line, if we have more data that unfairness is an issue, we can always fix it. One step at a time :)

Thanks a lot to Yosry, Nhat, and Shakeel for the great suggestions!

Let me summarize what I plan to do in the next version to make sure we are on the same page:

- Drop the per-memcg cursor and keep the root cgroup cursor
(zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the proactive
writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and
shrink_memcg(). Enable batch writeback in the shrink_worker() path, while keeping the writeback behavior in the zswap_store() path unchanged.

Quoted 2 lines

Please let me know if I missed or misunderstood anything. Thanks again for clearing things up!

Sorry for the late response, yes I think this makes sense. However, I have some comment about how this interacts with swap tiering, let me reply to the other thread.

Quoted 3 lines

Thanks,
Hao

maintainer
#26 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 34 lines · Yosry Ahmed
Quoted 33 lines · Hao Jia
Quoted 13 lines · Nhat Pham
Quoted 11 lines · Yosry Ahmed
Quoted 5 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

Quoted 3 lines

Ok then yeah, I think we should shelve per-memcg cursor for the next version. Down the line, if we have more data that unfairness is an issue, we can always fix it. One step at a time :)

Thanks a lot to Yosry, Nhat, and Shakeel for the great suggestions!

Let me summarize what I plan to do in the next version to make sure we are on the same page:

- Drop the per-memcg cursor and keep the root cgroup cursor
(zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the proactive
writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and
shrink_memcg(). Enable batch writeback in the shrink_worker() path, while keeping the writeback behavior in the zswap_store() path unchanged.

Quoted 7 lines
Quoted 2 lines

Please let me know if I missed or misunderstood anything. Thanks again for clearing things up!

Sorry for the late response, yes I think this makes sense. However, I have some comment about how this interacts with swap tiering, let me reply to the other thread.

I think the swap tiers interaction will be figured out over next cycle. However Hao can/should continue to push and we may decide to let it in orthogonal to swap tiers.

maintainer
#27 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 39 lines · Shakeel Butt
Quoted 37 lines · Yosry Ahmed
Quoted 35 lines · Hao Jia
Quoted 13 lines · Nhat Pham
Quoted 11 lines · Yosry Ahmed
Quoted 5 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

Quoted 3 lines

Ok then yeah, I think we should shelve per-memcg cursor for the next version. Down the line, if we have more data that unfairness is an issue, we can always fix it. One step at a time :)

Thanks a lot to Yosry, Nhat, and Shakeel for the great suggestions!

Let me summarize what I plan to do in the next version to make sure we are on the same page:

- Drop the per-memcg cursor and keep the root cgroup cursor
(zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the proactive
writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and
shrink_memcg(). Enable batch writeback in the shrink_worker() path, while keeping the writeback behavior in the zswap_store() path unchanged.

Please let me know if I missed or misunderstood anything. Thanks again for clearing things up!

Quoted 8 lines
Quoted 4 lines

Sorry for the late response, yes I think this makes sense. However, I have some comment about how this interacts with swap tiering, let me reply to the other thread.

I think the swap tiers interaction will be figured out over next cycle. However Hao can/should continue to push and we may decide to let it in orthogonal to swap tiers.

Yeah I think there are a lot of changes we discussed outside of the memcg interface, so maybe keep the interface as-is for now, work on a new version with the other changes, and we can finalize the interface at the end?

author
#28 lore raw

Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg

Quoted 45 lines · Yosry Ahmed
Quoted 44 lines · Shakeel Butt
Quoted 41 lines · Yosry Ahmed
Quoted 33 lines · Hao Jia
Quoted 13 lines · Nhat Pham
Quoted 11 lines · Yosry Ahmed
Quoted 5 lines

But OTOH, this does seem like a recipe for inefficient reclaim. We might exhaust hotter memory of a cgroup while sparing colder memory of another cgroup... But maybe if they're all cold anyway, then who cares, and eventually you'll get to the cold stuff of other child?

Forgot to respond to this part, the unfairness is limited to the batch size per-invocation, so it should be fine as long as you don't divide the amount over 100 iterations for some reason. Also yes, all memory in zswap is cold, the relative coldness is not that important (e.g. compared to relative coldness during reclaim).

Quoted 3 lines

Ok then yeah, I think we should shelve per-memcg cursor for the next version. Down the line, if we have more data that unfairness is an issue, we can always fix it. One step at a time :)

Thanks a lot to Yosry, Nhat, and Shakeel for the great suggestions!

Let me summarize what I plan to do in the next version to make sure we are on the same page:

- Drop the per-memcg cursor and keep the root cgroup cursor
(zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the proactive
writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and
shrink_memcg(). Enable batch writeback in the shrink_worker() path, while keeping the writeback behavior in the zswap_store() path unchanged.

Quoted 2 lines

Please let me know if I missed or misunderstood anything. Thanks again for clearing things up!

Sorry for the late response, yes I think this makes sense. However, I have some comment about how this interacts with swap tiering, let me reply to the other thread.

Quoted 8 lines
Quoted 3 lines

I think the swap tiers interaction will be figured out over next cycle. However Hao can/should continue to push and we may decide to let it in orthogonal to swap tiers.

Yeah I think there are a lot of changes we discussed outside of the memcg interface, so maybe keep the interface as-is for now, work on a new version with the other changes, and we can finalize the interface at the end?

Okay, I will split the non-memcg interface parts into a few separate patches. These will serve as the preparation work for proactive writeback and enable batch writeback in the shrink_worker() path.

However, I will still send the complete patchset using the zswap_writeback_only key approach in the next version. This should make it easier to review whether the preparation logic is reasonable, and to decide whether it should eventually be merged independently of the swap tiers.

Thanks,
Hao

author
#29 lore raw

PATCH v3 2/4 mm/zswap: Implement proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Note that the actual amount written back may be less than requested due to the zswap second-chance algorithm: referenced entries are rotated on the LRU on the first encounter and only written back on a second pass. If fewer bytes are written back than requested, -EAGAIN is returned, matching the existing memory.reclaim semantics.

Internally, extend user_proactive_reclaim() to parse the new "zswap_writeback_only" token and invoke the dedicated handler. Add zswap_proactive_writeback() to walk the target memcg subtree via the per-memcg writeback cursor, draining per-node zswap LRUs through list_lru_walk_one() with the shrink_memcg_cb() callback.

---
 Documentation/admin-guide/cgroup-v2.rst |  18 +++-
 Documentation/admin-guide/mm/zswap.rst  |  11 +-
 include/linux/zswap.h                   |   7 ++
 mm/vmscan.c                             |  14 +++
 mm/zswap.c                              | 138 ++++++++++++++++++++++++
 5 files changed, 185 insertions(+), 3 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +16 -2
index 6efd0095ed99..6564abf0dec5 100644
@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.
 
 The following nested keys are defined.
 
-	  ==========            ================================
+	  ====================  ==================================================
 	  swappiness            Swappiness value to reclaim with
-	  ==========            ================================
+	  zswap_writeback_only  Only perform proactive zswap writeback
+	  ====================  ==================================================
 
 	Specifying a swappiness value instructs the kernel to perform
 	the reclaim with that swappiness value. Note that this has the
@@ -1437,6 +1438,19 @@ The following nested keys are defined.
 	The valid range for swappiness is [0-200, max], setting
 	swappiness=max exclusively reclaims anonymous memory.
 
+	The zswap_writeback_only key skips ordinary memory reclaim and
+	writes back pages from zswap to the backing swap device until
+	the requested amount has been written or no further candidates
+	are found. This is useful to proactively offload cold pages from
+	the zswap pool to the swap device. It is only available if
+	zswap writeback is enabled. zswap_writeback_only cannot be combined
+	with swappiness; specifying both returns -EINVAL.
+
+	Example::
+
+	  # Write back up to 100MB of pages from zswap to the backing swap
+	  echo "100M zswap_writeback_only" > memory.reclaim
+
   memory.peak
 	A read-write single value file which exists on non-root cgroups.
 
Documentation/admin-guide/mm/zswap.rst +10 -1
index 2464425c783d..1c0598e77958 100644
@@ -131,7 +131,16 @@ User can enable it as follows::
   echo Y > /sys/module/zswap/parameters/shrinker_enabled
 
 This can be enabled at the boot time if ``CONFIG_ZSWAP_SHRINKER_DEFAULT_ON`` is
-selected.
+selected. Once enabled, the shrinker automatically writes back zswap pages to
+backing swap during memory reclaim.
+
+If users want to explicitly trigger proactive zswap writeback for a specific
+memory cgroup without invoking standard page reclaim, it can be done as follows::
+
+	echo "100M zswap_writeback_only" > /sys/fs/cgroup/<cgroup-name>/memory.reclaim
+
+Both of the methods mentioned above are subject to the ``memory.zswap.writeback``
+control. This means that ``memory.zswap.writeback`` can reject all zswap writeback.
 
 A debugfs interface is provided for various statistic about pool size, number
 of pages stored, same-value filled pages and various counters for the reasons
include/linux/zswap.h +7 -0
index efa6b551217e..98434d39339a 100644
@@ -44,6 +44,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);
 void zswap_folio_swapin(struct folio *folio);
 bool zswap_is_enabled(void);
 bool zswap_never_enabled(void);
+int zswap_proactive_writeback(struct mem_cgroup *memcg, unsigned long nr_to_writeback);
 #else
 
 struct zswap_lruvec_state {};
@@ -78,6 +79,12 @@ static inline bool zswap_never_enabled(void)
 	return true;
 }
 
+static inline int zswap_proactive_writeback(struct mem_cgroup *memcg,
+					    unsigned long nr_to_writeback)
+{
+	return -EOPNOTSUPP;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
mm/vmscan.c +14 -0
index ca4533eba701..63fa4341b823 100644
@@ -64,6 +64,7 @@
 
 #include <linux/swapops.h>
 #include <linux/sched/sysctl.h>
+#include <linux/zswap.h>
 
 #include "internal.h"
 #include "swap.h"
@@ -7856,11 +7857,13 @@ static unsigned long __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask,
 enum {
 	MEMORY_RECLAIM_SWAPPINESS = 0,
 	MEMORY_RECLAIM_SWAPPINESS_MAX,
+	MEMORY_RECLAIM_ZSWAP_WRITEBACK_ONLY,
 	MEMORY_RECLAIM_NULL,
 };
 static const match_table_t tokens = {
 	{ MEMORY_RECLAIM_SWAPPINESS, "swappiness=%d"},
 	{ MEMORY_RECLAIM_SWAPPINESS_MAX, "swappiness=max"},
+	{ MEMORY_RECLAIM_ZSWAP_WRITEBACK_ONLY, "zswap_writeback_only"},
 	{ MEMORY_RECLAIM_NULL, NULL },
 };
 
@@ -7870,6 +7873,7 @@ int user_proactive_reclaim(char *buf,
 	unsigned int nr_retries = MAX_RECLAIM_RETRIES;
 	unsigned long nr_to_reclaim, nr_reclaimed = 0;
 	int swappiness = -1;
+	bool zswap_writeback_only = false;
 	char *old_buf, *start;
 	substring_t args[MAX_OPT_ARGS];
 	gfp_t gfp_mask = GFP_KERNEL;
@@ -7900,11 +7904,21 @@ int user_proactive_reclaim(char *buf,
 		case MEMORY_RECLAIM_SWAPPINESS_MAX:
 			swappiness = SWAPPINESS_ANON_ONLY;
 			break;
+		case MEMORY_RECLAIM_ZSWAP_WRITEBACK_ONLY:
+			zswap_writeback_only = true;
+			break;
 		default:
 			return -EINVAL;
 		}
 	}
 
+	if (zswap_writeback_only) {
+		/* zswap_writeback_only and swappiness are mutually exclusive. */
+		if (swappiness != -1)
+			return -EINVAL;
+		return zswap_proactive_writeback(memcg, nr_to_reclaim);
+	}
+
 	while (nr_reclaimed < nr_to_reclaim) {
 		/* Will converge on zero, but reclaim enforces a minimum */
 		unsigned long batch_size = (nr_to_reclaim - nr_reclaimed) / 4;
mm/zswap.c +138 -0

144 lines not on this page · raw message

Signature · 1 line
2.34.1
maintainer
#30 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 10 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Quoted 8 lines

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Hmmm, so this 100MB is the pre-compression size? i.e if this 100 MB compresses to 25 MB, then you're only freeing 25 MB?

I'm ok-ish with this, but can you document it?

The rest seems solid to me, FWIW. I'll defer to Johannes and Yosry for opinions on zswap-only proactive reclaim.

maintainer
#31 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 16 lines · Nhat Pham
Quoted 15 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 8 lines
Quoted 3 lines

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Hmmm, so this 100MB is the pre-compression size? i.e if this 100 MB compresses to 25 MB, then you're only freeing 25 MB?

I'm ok-ish with this, but can you document it?

That's a good point. I think pre-compressed size doesn't make sense to be honest. We should care about how much memory we are actually trying to save by doing writeback here.

The pre-compressed size is only useful in determining the blast radius, how many actual pages are going to have slower page faults now. But then, I don't think there's a reasonable way for userspace to decide that.

I understand passing in the compressed size is tricky because we need to keep track of the size of the compressed pages we end up writing back, but it should be doable.

If we really want pre-compressed size here, then yes we need to make it very clear, and I vote that we use a separate interface in this case because memory.reclaim having different meanings for the amount of memory written to it is extremely counter-intuitive.

Quoted 3 lines

The rest seems solid to me, FWIW. I'll defer to Johannes and Yosry for opinions on zswap-only proactive reclaim.

author
#32 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 30 lines · Yosry Ahmed
Quoted 16 lines · Nhat Pham
Quoted 15 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 8 lines
Quoted 3 lines

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Hmmm, so this 100MB is the pre-compression size? i.e if this 100 MB compresses to 25 MB, then you're only freeing 25 MB?

I'm ok-ish with this, but can you document it?

That's a good point. I think pre-compressed size doesn't make sense to be honest. We should care about how much memory we are actually trying to save by doing writeback here.

Quoted 8 lines

The pre-compressed size is only useful in determining the blast radius, how many actual pages are going to have slower page faults now. But then, I don't think there's a reasonable way for userspace to decide that.

I understand passing in the compressed size is tricky because we need to keep track of the size of the compressed pages we end up writing back, but it should be doable.

Agreed. Using pre-compressed size is probably easier to implement. IIRC, interfaces like ZRAM writeback_limit are also calculated using the pre-compressed size.

I'll clarify this in the documentation in the next version.

Quoted 6 lines

If we really want pre-compressed size here, then yes we need to make it very clear, and I vote that we use a separate interface in this case because memory.reclaim having different meanings for the amount of memory written to it is extremely counter-intuitive.

Agree. This would indeed break the semantics of memory.reclaim. I will use a separate interface for proactive writeback in the next version.

Thanks,
Hao

Quoted 3 lines
Quoted 3 lines

The rest seems solid to me, FWIW. I'll defer to Johannes and Yosry for opinions on zswap-only proactive reclaim.

maintainer
#33 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 49 lines · Hao Jia
Quoted 30 lines · Yosry Ahmed
Quoted 16 lines · Nhat Pham
Quoted 15 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 8 lines
Quoted 3 lines

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Hmmm, so this 100MB is the pre-compression size? i.e if this 100 MB compresses to 25 MB, then you're only freeing 25 MB?

I'm ok-ish with this, but can you document it?

That's a good point. I think pre-compressed size doesn't make sense to be honest. We should care about how much memory we are actually trying to save by doing writeback here.

Quoted 8 lines

The pre-compressed size is only useful in determining the blast radius, how many actual pages are going to have slower page faults now. But then, I don't think there's a reasonable way for userspace to decide that.

I understand passing in the compressed size is tricky because we need to keep track of the size of the compressed pages we end up writing back, but it should be doable.

Agreed. Using pre-compressed size is probably easier to implement. IIRC, interfaces like ZRAM writeback_limit are also calculated using the pre-compressed size.

I'll clarify this in the documentation in the next version.

Quoted 1 line
Quoted 7 lines
Quoted 5 lines

If we really want pre-compressed size here, then yes we need to make it very clear, and I vote that we use a separate interface in this case because memory.reclaim having different meanings for the amount of memory written to it is extremely counter-intuitive.

Agree. This would indeed break the semantics of memory.reclaim. I will use a separate interface for proactive writeback in the next version.

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Johannes, Nhat, WDYT?

maintainer
#34 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 56 lines · Yosry Ahmed
Quoted 54 lines · Hao Jia
Quoted 30 lines · Yosry Ahmed
Quoted 16 lines · Nhat Pham
Quoted 15 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 8 lines
Quoted 3 lines

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Hmmm, so this 100MB is the pre-compression size? i.e if this 100 MB compresses to 25 MB, then you're only freeing 25 MB?

I'm ok-ish with this, but can you document it?

That's a good point. I think pre-compressed size doesn't make sense to be honest. We should care about how much memory we are actually trying to save by doing writeback here.

Quoted 8 lines

The pre-compressed size is only useful in determining the blast radius, how many actual pages are going to have slower page faults now. But then, I don't think there's a reasonable way for userspace to decide that.

I understand passing in the compressed size is tricky because we need to keep track of the size of the compressed pages we end up writing back, but it should be doable.

Agreed. Using pre-compressed size is probably easier to implement. IIRC, interfaces like ZRAM writeback_limit are also calculated using the pre-compressed size.

I'll clarify this in the documentation in the next version.

Quoted 6 lines

If we really want pre-compressed size here, then yes we need to make it very clear, and I vote that we use a separate interface in this case because memory.reclaim having different meanings for the amount of memory written to it is extremely counter-intuitive.

Quoted 6 lines
Quoted 2 lines

Agree. This would indeed break the semantics of memory.reclaim. I will use a separate interface for proactive writeback in the next version.

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

author
#35 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 64 lines · Nhat Pham
Quoted 56 lines · Yosry Ahmed
Quoted 54 lines · Hao Jia
Quoted 30 lines · Yosry Ahmed
Quoted 16 lines · Nhat Pham
Quoted 15 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 8 lines
Quoted 3 lines

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Hmmm, so this 100MB is the pre-compression size? i.e if this 100 MB compresses to 25 MB, then you're only freeing 25 MB?

I'm ok-ish with this, but can you document it?

That's a good point. I think pre-compressed size doesn't make sense to be honest. We should care about how much memory we are actually trying to save by doing writeback here.

Quoted 8 lines

The pre-compressed size is only useful in determining the blast radius, how many actual pages are going to have slower page faults now. But then, I don't think there's a reasonable way for userspace to decide that.

I understand passing in the compressed size is tricky because we need to keep track of the size of the compressed pages we end up writing back, but it should be doable.

Agreed. Using pre-compressed size is probably easier to implement. IIRC, interfaces like ZRAM writeback_limit are also calculated using the pre-compressed size.

I'll clarify this in the documentation in the next version.

Quoted 6 lines

If we really want pre-compressed size here, then yes we need to make it very clear, and I vote that we use a separate interface in this case because memory.reclaim having different meanings for the amount of memory written to it is extremely counter-intuitive.

Quoted 6 lines
Quoted 2 lines

Agree. This would indeed break the semantics of memory.reclaim. I will use a separate interface for proactive writeback in the next version.

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Got it. I will change it to use the compressed size in the next version.

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

Thanks,
Hao

maintainer
#36 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 13 lines
Quoted 4 lines
Quoted 3 lines

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Quoted 4 lines

Got it. I will change it to use the compressed size in the next version.

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

I *really* want the memcg maintainers to chime in here, it's ultimately their call.

Michal? Johannes? Shakeel? Roman? Anyone? :D

maintainer
#37 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 15 lines · Yosry Ahmed
Quoted 15 lines
Quoted 4 lines
Quoted 3 lines

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Got it. I will change it to use the compressed size in the next version.

Quoted 7 lines
Quoted 2 lines

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

I *really* want the memcg maintainers to chime in here, it's ultimately their call.

Michal? Johannes? Shakeel? Roman? Anyone? :D

Sorry for the delay, I will take a look in a day or two.

maintainer
#38 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

In reply to Yosry Ahmed
Quoted 15 lines · Yosry Ahmed
Quoted 15 lines
Quoted 4 lines
Quoted 3 lines

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Got it. I will change it to use the compressed size in the next version.

Quoted 7 lines
Quoted 2 lines

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

I *really* want the memcg maintainers to chime in here, it's ultimately their call.

Michal? Johannes? Shakeel? Roman? Anyone? :D

Between the options of having an explicit interface (i.e. memory.zswap.writeback*) or a key (i.e. zswap_writeback_only) to memory.reclaim interface, I prefer the key option. I have not looked into how much proactively reclaiming zswap memory or proactively triggering zswap writeback makes sense but from the perspective of memcg interface, I think the key option would give a more clean solution if we decide in the future that this whole thing was a bad idea.

Next regarding future proofing zswap writeback trigger, do we expect any potential additions/changes/new-features for this interface? For example do we expect in future we may want to trigger the zswap writeback only from a specific node or lowest memory tier?

maintainer
#39 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

+Youngjun

Quoted 32 lines · Shakeel Butt
Quoted 15 lines · Yosry Ahmed
Quoted 15 lines
Quoted 4 lines
Quoted 3 lines

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Got it. I will change it to use the compressed size in the next version.

Quoted 7 lines
Quoted 2 lines

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

I *really* want the memcg maintainers to chime in here, it's ultimately their call.

Michal? Johannes? Shakeel? Roman? Anyone? :D

Between the options of having an explicit interface (i.e. memory.zswap.writeback*) or a key (i.e. zswap_writeback_only) to memory.reclaim interface, I prefer the key option. I have not looked into how much proactively reclaiming zswap memory or proactively triggering zswap writeback makes sense but from the perspective of memcg interface, I think the key option would give a more clean solution if we decide in the future that this whole thing was a bad idea.

Quoted 4 lines

Next regarding future proofing zswap writeback trigger, do we expect any potential additions/changes/new-features for this interface? For example do we expect in future we may want to trigger the zswap writeback only from a specific node or lowest memory tier?

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

BTW does zswap folks think of zswap as a top swap tier or something different?

maintainer
#40 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 40 lines · Shakeel Butt

+Youngjun

Quoted 36 lines · Shakeel Butt
Quoted 15 lines · Yosry Ahmed
Quoted 15 lines
Quoted 4 lines
Quoted 3 lines

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Got it. I will change it to use the compressed size in the next version.

Quoted 7 lines
Quoted 2 lines

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

I *really* want the memcg maintainers to chime in here, it's ultimately their call.

Michal? Johannes? Shakeel? Roman? Anyone? :D

Between the options of having an explicit interface (i.e. memory.zswap.writeback*) or a key (i.e. zswap_writeback_only) to memory.reclaim interface, I prefer the key option. I have not looked into how much proactively reclaiming zswap memory or proactively triggering zswap writeback makes sense but from the perspective of memcg interface, I think the key option would give a more clean solution if we decide in the future that this whole thing was a bad idea.

Next regarding future proofing zswap writeback trigger, do we expect any potential additions/changes/new-features for this interface? For example do we expect in future we may want to trigger the zswap writeback only from a specific node or lowest memory tier?

The way I see it, zswap writeback is just a "special" type of proactive reclaim, but the goal is still proactively freeing cold memory. In that regard, I think it makes sense to have things like node-specific reclaim. Not sure about other extensions, but Hao initially suggested making this age-based, so I think the answer is yes.

For both of these examples (node-specific reclaim, age-based reclaim), I think the same semantics could apply to memory.reclaim in general, which is why I suggested making it a part of memory.reclaim. I also like the idea of having a single proactive reclaim interface in general, but maybe we don't want to overload it too much.

Quoted 6 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

BTW does zswap folks think of zswap as a top swap tier or something different?

I haven't been following the swap tiers work closely, but personally I do think of zswap as a top swap tier. Things will probably get more blurry with memory tiers and compressed memory nodes though.

maintainer
#41 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 49 lines · Yosry Ahmed
Quoted 40 lines · Shakeel Butt

+Youngjun

Quoted 36 lines · Shakeel Butt
Quoted 15 lines · Yosry Ahmed
Quoted 15 lines
Quoted 4 lines
Quoted 3 lines

But doesn't it make more sense to specify the compressed size, which is ultimately the amount of memory you actually want to reclaim.

Quoted 8 lines

I personally prefer compressed size to pre-compressed size. That's kinda what user cares about, no?

One thing we can do is let users prescribe a compressed size, but internally, we can multiply that by the average compression ratio. That gives us a guesstimate of how many pages we need to reclaim, and you can follow the rest of your implementation as is (perhaps with short-circuit when we reach the goal with fewer pages reclaimed).

Got it. I will change it to use the compressed size in the next version.

Quoted 7 lines
Quoted 2 lines

Yosry, Nhat, should we continue using the zswap_writeback_only key to trigger proactive writeback?

I *really* want the memcg maintainers to chime in here, it's ultimately their call.

Michal? Johannes? Shakeel? Roman? Anyone? :D

Between the options of having an explicit interface (i.e. memory.zswap.writeback*) or a key (i.e. zswap_writeback_only) to memory.reclaim interface, I prefer the key option. I have not looked into how much proactively reclaiming zswap memory or proactively triggering zswap writeback makes sense but from the perspective of memcg interface, I think the key option would give a more clean solution if we decide in the future that this whole thing was a bad idea.

Next regarding future proofing zswap writeback trigger, do we expect any potential additions/changes/new-features for this interface? For example do we expect in future we may want to trigger the zswap writeback only from a specific node or lowest memory tier?

The way I see it, zswap writeback is just a "special" type of proactive reclaim, but the goal is still proactively freeing cold memory. In that regard, I think it makes sense to have things like node-specific reclaim. Not sure about other extensions, but Hao initially suggested making this age-based, so I think the answer is yes.

Quoted 5 lines

For both of these examples (node-specific reclaim, age-based reclaim), I think the same semantics could apply to memory.reclaim in general, which is why I suggested making it a part of memory.reclaim. I also like the idea of having a single proactive reclaim interface in general, but maybe we don't want to overload it too much.

IMHO it is fine.

Quoted 10 lines
Quoted 7 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

BTW does zswap folks think of zswap as a top swap tier or something different?

I haven't been following the swap tiers work closely, but personally I do think of zswap as a top swap tier.

Same for me though I imagine swap tiers would introduce some duplication i.e. different way (interface) to set limits for swap tiers for a given memcg.

Quoted 2 lines

Things will probably get more
blurry with memory tiers and compressed memory nodes though.

I think there will still be distinction between byte addressable and fault on access devices.

maintainer
#42 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 10 lines
Quoted 9 lines
Quoted 6 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

BTW does zswap folks think of zswap as a top swap tier or something different?

I haven't been following the swap tiers work closely, but personally I do think of zswap as a top swap tier.

Quoted 8 lines

Same for me though I imagine swap tiers would introduce some duplication i.e. different way (interface) to set limits for swap tiers for a given memcg.

Quoted 2 lines

Things will probably get more
blurry with memory tiers and compressed memory nodes though.

I think there will still be distinction between byte addressable and fault on access devices.

Yeah, I think it makes sense to define "swap" as fault on access (zswap, SSD, etc), and memory tiers as byte-addressable (even if you put an SSD behind CXL and make it byte-addressable). But I also remember seeing discussions about unifying memory tiers and swap in a way, and it makes sense from a reclaim perspective (swap or demote first?).

#43 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

On Mon, Jun 08, 2026 at 03:27:07PM -0700, Yosry Ahmed wrote:

+Chris +Kairui +Baoquan

Hello

Thanks for inviting me to the discussion, Shakeel.

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

Speaking of my work on swap tiers, I recently submitted a patch and am currently considering memcg integration:
https://lore.kernel.org/linux-mm/[email protected]/

The future use-cases imagined above seem to align with this direction. (BTW, I am currently waiting for reviews/feedback from the memcg folks on this patch. Any reviews would be highly appreciated!)

We could potentially assign a target tier
for writeback within the existing memory.zswap.writeback interface.

For instance, '0' could mean disabled, while non-zero values could represent specific tiers, which would maintain backward compatibility with the current version. Alternatively, if zswap is treated as the default top tier, the memory.swap.tiers interface could potentially replace memory.zswap.writeback.

Furthermore, this could be expanded so that each swap tier can demote data user-triggered demotion between swap tiers.

Based on the current patch's ideas combined with my swap tiers concept:

Assuming a hierarchy like:
zswap -> tier1 (SSD swap) -> tier2 (HDD swap) -> tier3 (Network swap)

We could configure the active tiers via a setting like memory.swap.tiers (tier2 enabled, tier3 enabled).

For example, the concept of echo "100M zswap_writeback_only > memory.reclaim" could be extended. A user could run echo "100M tier2 > memory.reclaim" to explicitly trigger demotion from tier2 to tier3.
(BTW, if we combine these features, my personal preference for the keyword format would be <size> <demote_prefix><tier_name>. I think it would be better to explicitly indicate that it is a swap demotion by using a specific prefix followed by the tier name.
Or make demote prefix another key is also possible)

So, the whole picture would look something like this:
- memory.swap.tiers : Interface for configuring the tier mask.
- memory.reclaim : Entry point for user-triggered demotion.

Quoted 6 lines
Quoted 5 lines
Quoted 2 lines

Things will probably get more
blurry with memory tiers and compressed memory nodes though.

I think there will still be distinction between byte addressable and fault on access devices.

Quoted 15 lines

Yeah, I think it makes sense to define "swap" as fault on access (zswap, SSD, etc), and memory tiers as byte-addressable (even if you put an SSD behind CXL and make it byte-addressable). But I also remember seeing discussions about unifying memory tiers and swap in a way, and it makes sense from a reclaim perspective (swap or demote first?).

Quoted 7 lines
Quoted 7 lines
Quoted 4 lines

will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

BTW does zswap folks think of zswap as a top swap tier or something different?

I haven't been following the swap tiers work closely, but personally I do think of zswap as a top swap tier.

Regarding zswap's position, I agree it needs to be defined as the default, top-most tier in swap_tier. In my early RFC, I allocated a separate tier specifically for zswap:
(https://lore.kernel.org/linux-mm/[email protected]/)

Quoted 3 lines
Quoted 3 lines

Same for me though I imagine swap tiers would introduce some duplication i.e. different way (interface) to set limits for swap tiers for a given memcg.

I also agree with the concern about interface duplication. We will eventually need a mechanism to control swap amounts per tier, which requires thinking about its relationship with swap.max. (I raised this as an open question in my early RFC).
https://lore.kernel.org/linux-mm/[email protected]/
(Further Discussion and Open Questions Part)
However, since this feature is necessary anyway, wouldn't the proposed interface be acceptable without causing conflicts at this early stage?

Additionally, memory.zswap.writeback seems redundant. Restricting a cgroup
to only use the zswap tier (assuming it's the first tier) is practically
identical to disabling memory.zswap.writeback (correct me if I'm wrong).
But there is no problem to integrate it as I think
e.g memory.zswap.writeback could internally act as an alias for setting memory.swap.tier to 'zswap only'.

BR,
Youngjun Park

maintainer
#44 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 40 lines · YoungJun Park

On Mon, Jun 08, 2026 at 03:27:07PM -0700, Yosry Ahmed wrote:

+Chris +Kairui +Baoquan

Hello

Thanks for inviting me to the discussion, Shakeel.

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

Speaking of my work on swap tiers, I recently submitted a patch and am currently considering memcg integration:
https://lore.kernel.org/linux-mm/[email protected]/

The future use-cases imagined above seem to align with this direction. (BTW, I am currently waiting for reviews/feedback from the memcg folks on this patch. Any reviews would be highly appreciated!)

We could potentially assign a target tier
for writeback within the existing memory.zswap.writeback interface.

For instance, '0' could mean disabled, while non-zero values could represent specific tiers, which would maintain backward compatibility with the current version. Alternatively, if zswap is treated as the default top tier, the memory.swap.tiers interface could potentially replace memory.zswap.writeback.

Furthermore, this could be expanded so that each swap tier can demote data user-triggered demotion between swap tiers.

Based on the current patch's ideas combined with my swap tiers concept:

Assuming a hierarchy like:
zswap -> tier1 (SSD swap) -> tier2 (HDD swap) -> tier3 (Network swap)

We could configure the active tiers via a setting like memory.swap.tiers (tier2 enabled, tier3 enabled).

Quoted 8 lines

For example, the concept of echo "100M zswap_writeback_only > memory.reclaim" could be extended. A user could run echo "100M tier2 > memory.reclaim" to explicitly trigger demotion from tier2 to tier3.
(BTW, if we combine these features, my personal preference for the keyword format would be <size> <demote_prefix><tier_name>. I think it would be better to explicitly indicate that it is a swap demotion by using a specific prefix followed by the tier name.
Or make demote prefix another key is also possible)

I am not sure if proactive demotion between swap tiers would be driven by memory.reclaim, I am guessing a new interface might be more suitable. But yes, you are right that it's very possible that 'zswap_writeback_only' with memory.reclaim will become obsolete once swap tiering matures and starts supporting things like proactive demotion.

Part of me wants to wait until the swap tiering interfaces are figured out so that we don't end up with redundant interfaces, but I also don't want to hold Hao's work since it doesn't directly depend on swap tiering.

Shakeel, how do you want to handle this? I think there's a few options:

1. Add zswap_writeback_only now, and when we have swap tiering demotion it becomes a redundant interface, like memory.zswap.writeback -- or maybe we try to deprecate both of them at that point. It's difficult to remove interfaces tho, but maybe easier to stop supporting zswap_writeback_only.

2. Add zswap_writeback_only behind an experimental config option, to unblock development but have a line of sight to dropping support once we have a swap tiering interface.

3. Wait until we figure out the swap tiering interfaces and then add the proactive zswap writeback as part of it.

WDYT?

maintainer
#45 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 70 lines · Yosry Ahmed
Quoted 40 lines · YoungJun Park

On Mon, Jun 08, 2026 at 03:27:07PM -0700, Yosry Ahmed wrote:

+Chris +Kairui +Baoquan

Hello

Thanks for inviting me to the discussion, Shakeel.

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

Speaking of my work on swap tiers, I recently submitted a patch and am currently considering memcg integration:
https://lore.kernel.org/linux-mm/[email protected]/

The future use-cases imagined above seem to align with this direction. (BTW, I am currently waiting for reviews/feedback from the memcg folks on this patch. Any reviews would be highly appreciated!)

We could potentially assign a target tier
for writeback within the existing memory.zswap.writeback interface.

For instance, '0' could mean disabled, while non-zero values could represent specific tiers, which would maintain backward compatibility with the current version. Alternatively, if zswap is treated as the default top tier, the memory.swap.tiers interface could potentially replace memory.zswap.writeback.

Furthermore, this could be expanded so that each swap tier can demote data user-triggered demotion between swap tiers.

Based on the current patch's ideas combined with my swap tiers concept:

Assuming a hierarchy like:
zswap -> tier1 (SSD swap) -> tier2 (HDD swap) -> tier3 (Network swap)

We could configure the active tiers via a setting like memory.swap.tiers (tier2 enabled, tier3 enabled).

Quoted 8 lines

For example, the concept of echo "100M zswap_writeback_only > memory.reclaim" could be extended. A user could run echo "100M tier2 > memory.reclaim" to explicitly trigger demotion from tier2 to tier3.
(BTW, if we combine these features, my personal preference for the keyword format would be <size> <demote_prefix><tier_name>. I think it would be better to explicitly indicate that it is a swap demotion by using a specific prefix followed by the tier name.
Or make demote prefix another key is also possible)

I am not sure if proactive demotion between swap tiers would be driven by memory.reclaim, I am guessing a new interface might be more suitable. But yes, you are right that it's very possible that 'zswap_writeback_only' with memory.reclaim will become obsolete once swap tiering matures and starts supporting things like proactive demotion.

Part of me wants to wait until the swap tiering interfaces are figured out so that we don't end up with redundant interfaces, but I also don't want to hold Hao's work since it doesn't directly depend on swap tiering.

Shakeel, how do you want to handle this? I think there's a few options:

1. Add zswap_writeback_only now, and when we have swap tiering demotion it becomes a redundant interface, like memory.zswap.writeback -- or maybe we try to deprecate both of them at that point. It's difficult to remove interfaces tho, but maybe easier to stop supporting zswap_writeback_only.

Quoted 8 lines

2. Add zswap_writeback_only behind an experimental config option, to unblock development but have a line of sight to dropping support once we have a swap tiering interface.

3. Wait until we figure out the swap tiering interfaces and then add the proactive zswap writeback as part of it.

WDYT?

Is Hao's work needed for some followup work/development? The earliest Hao's work can is 7.3, so if we aim to figure out swap tiering interfaces in next couple of weeks then option 3 is the way to go. If swap tiers take more time then we can discuss other options as well.

However I would need zswap folks (Yosry & Nhat) help in figuring out swap tiers interfaces. Zswap is the current top tier swap usage in real world. I want zswap users to eaily (and hopefully transparently) migrate to swap tiers.

#46 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 83 lines · Shakeel Butt
Quoted 57 lines · Yosry Ahmed
Quoted 40 lines · YoungJun Park

On Mon, Jun 08, 2026 at 03:27:07PM -0700, Yosry Ahmed wrote:

+Chris +Kairui +Baoquan

Hello

Thanks for inviting me to the discussion, Shakeel.

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

Speaking of my work on swap tiers, I recently submitted a patch and am currently considering memcg integration:
https://lore.kernel.org/linux-mm/[email protected]/

The future use-cases imagined above seem to align with this direction. (BTW, I am currently waiting for reviews/feedback from the memcg folks on this patch. Any reviews would be highly appreciated!)

We could potentially assign a target tier
for writeback within the existing memory.zswap.writeback interface.

For instance, '0' could mean disabled, while non-zero values could represent specific tiers, which would maintain backward compatibility with the current version. Alternatively, if zswap is treated as the default top tier, the memory.swap.tiers interface could potentially replace memory.zswap.writeback.

Furthermore, this could be expanded so that each swap tier can demote data user-triggered demotion between swap tiers.

Based on the current patch's ideas combined with my swap tiers concept:

Assuming a hierarchy like:
zswap -> tier1 (SSD swap) -> tier2 (HDD swap) -> tier3 (Network swap)

We could configure the active tiers via a setting like memory.swap.tiers (tier2 enabled, tier3 enabled).

Quoted 8 lines

For example, the concept of echo "100M zswap_writeback_only > memory.reclaim" could be extended. A user could run echo "100M tier2 > memory.reclaim" to explicitly trigger demotion from tier2 to tier3.
(BTW, if we combine these features, my personal preference for the keyword format would be <size> <demote_prefix><tier_name>. I think it would be better to explicitly indicate that it is a swap demotion by using a specific prefix followed by the tier name.
Or make demote prefix another key is also possible)

I am not sure if proactive demotion between swap tiers would be driven by memory.reclaim, I am guessing a new interface might be more suitable. But yes, you are right that it's very possible that 'zswap_writeback_only' with memory.reclaim will become obsolete once swap tiering matures and starts supporting things like proactive demotion.

Quoted 4 lines

Part of me wants to wait until the swap tiering interfaces are figured out so that we don't end up with redundant interfaces, but I also don't want to hold Hao's work since it doesn't directly depend on swap tiering.

However I would need zswap folks (Yosry & Nhat) help in figuring out swap tiers interfaces. Zswap is the current top tier swap usage in real world. I want zswap users to eaily (and hopefully transparently) migrate to swap tiers.

Quoted 8 lines

Shakeel, how do you want to handle this? I think there's a few options:

1. Add zswap_writeback_only now, and when we have swap tiering demotion it becomes a redundant interface, like memory.zswap.writeback -- or maybe we try to deprecate both of them at that point. It's difficult to remove interfaces tho, but maybe easier to stop supporting zswap_writeback_only.

Quoted 8 lines

2. Add zswap_writeback_only behind an experimental config option, to unblock development but have a line of sight to dropping support once we have a swap tiering interface.

3. Wait until we figure out the swap tiering interfaces and then add the proactive zswap writeback as part of it.

WDYT?

Quoted 7 lines

Is Hao's work needed for some followup work/development? The earliest Hao's work can is 7.3, so if we aim to figure out swap tiering interfaces in next couple of weeks then option 3 is the way to go. If swap tiers take more time then we can discuss other options as well.
However I would need zswap folks (Yosry & Nhat) help in figuring out swap tiers interfaces. Zswap is the current top tier swap usage in real world. I want zswap users to eaily (and hopefully transparently) migrate to swap tiers.

I am looking forward to the discussion on this interface!

To help boost the discussion and progress, I would like to share a few of my thoughts. We could either introduce a new interface to trigger demotion/promotion, or we could reuse the existing one (using tier just internally)

Based on the memcg interface currently proposed in swap_tier (memory.swap.tiers, memory.swap.tiers.effective), I think it aligns well with the current direction. It provides a foundation for selectively targeting devices in tier order.

To summarize the discussions so far, the following points align well.

- Per-cgroup swap control, as I suggested.
- Proactive zswap writeback (Hao's usecase)
- Swap device target demotion(if it wants selective, then it is more better), as you mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- Virtual Swap on/off in the future, as Nhat mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- The memory.zswap.writeback alternative (no hierarchy model conflict)
- zswap is first swap tier.
- Promotion. (Also better for selectve usage)
- tier based swap policy (e.g round-robin...)

To accelerate this work, I believe we should reach a consensus and merge the currently proposed swap_tier interface :)

If the above approach is difficult, I would like to suggest an alternative for progress with the memcg interfaces removed:

1) We could make zswap the first tier and create
a use case where memory.zswap.writeback internally is handled by tier logic.

2) Or simply merge the swap_tier infrastructure itself first.

This would allow the swap_tier infrastructure to be merged and discussed more easily.

If it takes longer to adopt swap_tier anyway, by doing so we progress next step as a experimental feature.

- Apply per-cgroup swap as an experimental (debugfs) feature.
- Apply Hao's use case experimentally or as it is as Yosry suggested. (future migration to swap tier)

How do you think?

(FYI: My emails to kernel.org are failing due to internal server issues.)

Thank you
Youngjun Park

maintainer
#47 lore raw

[swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Changed the subject to separate the discussion on swap tiers.

Quoted 94 lines · YoungJun Park
Quoted 83 lines · Shakeel Butt
Quoted 57 lines · Yosry Ahmed
Quoted 40 lines · YoungJun Park

On Mon, Jun 08, 2026 at 03:27:07PM -0700, Yosry Ahmed wrote:

+Chris +Kairui +Baoquan

Hello

Thanks for inviting me to the discussion, Shakeel.

Quoted 4 lines
Quoted 4 lines
Quoted 4 lines
Quoted 4 lines

Youngjun is working on swap tiers. At the moment he is more interested in allowing a specific swap device to a memcg or not. I can imagine in future there will be use-cases where there will be a need to demote data on higher tier swap to lower tier swap. What would be the appropriate interface?

Speaking of my work on swap tiers, I recently submitted a patch and am currently considering memcg integration:
https://lore.kernel.org/linux-mm/[email protected]/

The future use-cases imagined above seem to align with this direction. (BTW, I am currently waiting for reviews/feedback from the memcg folks on this patch. Any reviews would be highly appreciated!)

We could potentially assign a target tier
for writeback within the existing memory.zswap.writeback interface.

For instance, '0' could mean disabled, while non-zero values could represent specific tiers, which would maintain backward compatibility with the current version. Alternatively, if zswap is treated as the default top tier, the memory.swap.tiers interface could potentially replace memory.zswap.writeback.

Furthermore, this could be expanded so that each swap tier can demote data user-triggered demotion between swap tiers.

Based on the current patch's ideas combined with my swap tiers concept:

Assuming a hierarchy like:
zswap -> tier1 (SSD swap) -> tier2 (HDD swap) -> tier3 (Network swap)

We could configure the active tiers via a setting like memory.swap.tiers (tier2 enabled, tier3 enabled).

Quoted 8 lines

For example, the concept of echo "100M zswap_writeback_only > memory.reclaim" could be extended. A user could run echo "100M tier2 > memory.reclaim" to explicitly trigger demotion from tier2 to tier3.
(BTW, if we combine these features, my personal preference for the keyword format would be <size> <demote_prefix><tier_name>. I think it would be better to explicitly indicate that it is a swap demotion by using a specific prefix followed by the tier name.
Or make demote prefix another key is also possible)

I am not sure if proactive demotion between swap tiers would be driven by memory.reclaim, I am guessing a new interface might be more suitable. But yes, you are right that it's very possible that 'zswap_writeback_only' with memory.reclaim will become obsolete once swap tiering matures and starts supporting things like proactive demotion.

Quoted 4 lines

Part of me wants to wait until the swap tiering interfaces are figured out so that we don't end up with redundant interfaces, but I also don't want to hold Hao's work since it doesn't directly depend on swap tiering.

However I would need zswap folks (Yosry & Nhat) help in figuring out swap tiers interfaces. Zswap is the current top tier swap usage in real world. I want zswap users to eaily (and hopefully transparently) migrate to swap tiers.

Quoted 8 lines

Shakeel, how do you want to handle this? I think there's a few options:

1. Add zswap_writeback_only now, and when we have swap tiering demotion it becomes a redundant interface, like memory.zswap.writeback -- or maybe we try to deprecate both of them at that point. It's difficult to remove interfaces tho, but maybe easier to stop supporting zswap_writeback_only.

Quoted 8 lines

2. Add zswap_writeback_only behind an experimental config option, to unblock development but have a line of sight to dropping support once we have a swap tiering interface.

3. Wait until we figure out the swap tiering interfaces and then add the proactive zswap writeback as part of it.

WDYT?

Quoted 7 lines

Is Hao's work needed for some followup work/development? The earliest Hao's work can is 7.3, so if we aim to figure out swap tiering interfaces in next couple of weeks then option 3 is the way to go. If swap tiers take more time then we can discuss other options as well.
However I would need zswap folks (Yosry & Nhat) help in figuring out swap tiers interfaces. Zswap is the current top tier swap usage in real world. I want zswap users to eaily (and hopefully transparently) migrate to swap tiers.

I am looking forward to the discussion on this interface!

Quoted 8 lines

To help boost the discussion and progress, I would like to share a few of my thoughts. We could either introduce a new interface to trigger demotion/promotion, or we could reuse the existing one (using tier just internally)

Based on the memcg interface currently proposed in swap_tier (memory.swap.tiers, memory.swap.tiers.effective), I think it aligns well with the current direction. It provides a foundation for selectively targeting devices in tier order.

Here instead of cpuset like interface, we may want more zswap like interface where you can put limit on the usage i.e. memory.swap.tier*.max. We can start with allowing only two values i.e. 0 and max which effectively will be the same as what you need.

I will respond to your other points later when I have time.

Quoted 41 lines

To summarize the discussions so far, the following points align well.

- Per-cgroup swap control, as I suggested.
- Proactive zswap writeback (Hao's usecase)
- Swap device target demotion(if it wants selective, then it is more better), as you mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- Virtual Swap on/off in the future, as Nhat mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- The memory.zswap.writeback alternative (no hierarchy model conflict)
- zswap is first swap tier.
- Promotion. (Also better for selectve usage)
- tier based swap policy (e.g round-robin...)

To accelerate this work, I believe we should reach a consensus and merge the currently proposed swap_tier interface :)

If the above approach is difficult, I would like to suggest an alternative for progress with the memcg interfaces removed:

1) We could make zswap the first tier and create
a use case where memory.zswap.writeback internally is handled by tier logic.

2) Or simply merge the swap_tier infrastructure itself first.

This would allow the swap_tier infrastructure to be merged and discussed more easily.

If it takes longer to adopt swap_tier anyway, by doing so we progress next step as a experimental feature.

- Apply per-cgroup swap as an experimental (debugfs) feature.
- Apply Hao's use case experimentally or as it is as Yosry suggested. (future migration to swap tier)

How do you think?

(FYI: My emails to kernel.org are failing due to internal server issues.)

Thank you
Youngjun Park

maintainer
#48 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 19 lines
Quoted 10 lines
Quoted 7 lines

Is Hao's work needed for some followup work/development? The earliest Hao's work can is 7.3, so if we aim to figure out swap tiering interfaces in next couple of weeks then option 3 is the way to go. If swap tiers take more time then we can discuss other options as well.
However I would need zswap folks (Yosry & Nhat) help in figuring out swap tiers interfaces. Zswap is the current top tier swap usage in real world. I want zswap users to eaily (and hopefully transparently) migrate to swap tiers.

I am looking forward to the discussion on this interface!

Quoted 8 lines

To help boost the discussion and progress, I would like to share a few of my thoughts. We could either introduce a new interface to trigger demotion/promotion, or we could reuse the existing one (using tier just internally)

Based on the memcg interface currently proposed in swap_tier (memory.swap.tiers, memory.swap.tiers.effective), I think it aligns well with the current direction. It provides a foundation for selectively targeting devices in tier order.

Quoted 6 lines

Here instead of cpuset like interface, we may want more zswap like interface where you can put limit on the usage i.e. memory.swap.tier*.max. We can start with allowing only two values i.e. 0 and max which effectively will be the same as what you need.

I will respond to your other points later when I have time.

If we will have one interface for all the tiers for memory tiering, I'd rather we do the swap for swap tiering. So maybe memory.swap.tiers.max or memory.swap.tiered max?

The file can show the limits for all tiers when read, and maybe write something like "echo 'tierX max' > memory.swap.tiers.max" to it to set a new limit. We can support only 0/max for now to enable/disable tiers. In the future, we can also allow something like "auto" to automatically scale the limit based on the swapfile size and memory.swap.max, similar to the direction memory tiering is heading in.

I think we can start with just this interface for now, and expand incrementally. For proactive zswap writeback, we can add memory.swap.tiers.demote or something, and only support zswap initially?

#49 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

In reply to Shakeel Butt

....

Quoted 10 lines
Quoted 4 lines

Based on the memcg interface currently proposed in swap_tier (memory.swap.tiers, memory.swap.tiers.effective), I think it aligns well with the current direction. It provides a foundation for selectively targeting devices in tier order.

Here instead of cpuset like interface, we may want more zswap like interface where you can put limit on the usage i.e. memory.swap.tier*.max. We can start with allowing only two values i.e. 0 and max which effectively will be the same as what you need.

Good idea, and it's certainly feasible. When I considered this a while ago, the reasons I didn't take this direction were:

1. There's no real-world usage for adjusting the swap tier amount (it's either 0 or MAX). That said, your suggestion to initially allow only 0 and max is the killing point, and it's making me reconsider.

2. The implementation cost seems high. The current implementation handles this at runtime via simple masking.

3. Relationship with swap.max:
- If we tie it to the current interface, wouldn't limiting the swap amount within a selected tier already be possible? I wonder if that alone is enough.
- If we add tier.max, it would need to be a subset of swap.max. (Any other complexities here?)

4. vswap enable/disable: vswap doesn't seem to have an amount-control aspect, so an on/off semantic would be clearer.
https://lore.kernel.org/linux-mm/ai5kOOmR1LPTWs1J@yjaykim-PowerEdge-T330/T/#m8831ec057bf9387978d3bd698f51920600e09a04

In that case, the internal logic could stay roughly the same rather than counting via a page counter. Something like:

1. Change the interface shell: tier.*.max — allow only 0 ~ max.
2. Keep the internal logic as is: 0 disables the mask (child memcgs off too), max enables it (child memcgs on too).
3. memory.zswap.max integrates naturally (it's memory."tier_name".max).
4. Extend later if use cases arise.

On balance I still lean toward the current interface, but if a per-tier max is the better fit for memcg's direction and others feel the same, I'm happy to switch. I'd like to hear Shakeel's thoughts again, and I'm curious about others' opinions too.

A few more perspectives on the points below.

Quoted 43 lines

I will respond to your other points later when I have time.

Quoted 41 lines

To summarize the discussions so far, the following points align well.

- Per-cgroup swap control, as I suggested.
- Proactive zswap writeback (Hao's usecase)
- Swap device target demotion(if it wants selective, then it is more better), as you mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- Virtual Swap on/off in the future, as Nhat mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- The memory.zswap.writeback alternative (no hierarchy model conflict)
- zswap is first swap tier.
- Promotion. (Also better for selectve usage)
- tier based swap policy (e.g round-robin...)

To accelerate this work, I believe we should reach a consensus and merge the currently proposed swap_tier interface :)

If the above approach is difficult, I would like to suggest an alternative for progress with the memcg interfaces removed:

1) We could make zswap the first tier and create
a use case where memory.zswap.writeback internally is handled by tier logic.

2) Or simply merge the swap_tier infrastructure itself first.

This would allow the swap_tier infrastructure to be merged and discussed more easily.

If it takes longer to adopt swap_tier anyway, by doing so we progress next step as a experimental feature.

- Apply per-cgroup swap as an experimental (debugfs) feature.
- Apply Hao's use case experimentally or as it is as Yosry suggested. (future migration to swap tier)

How do you think?

(FYI: My emails to kernel.org are failing due to internal server issues.)

Thank you
Youngjun Park

Let me clarify a part I wrote confusingly. Handling memory.zswap.writeback via tiers is possible, but I don't think the interface itself would be replaced even if memory.swap.tiers is adopted.

Selecting only zswap in memory.swap.tiers would not just disable writeback.it would also block regular swap entirely, which differs slightly from the current semantic. (... "Per the cgroup v2 docs: a zswap-only tier setting is subtly different from setting memory.swap.max to 0, since it still allows pages to be written to the zswap pool; this has no effect if zswap is disabled, and swapping is allowed unless memory.swap.max is set to 0.")

So the interface itself needs to be retained, and it could be extended toward selective writeback — e.g., passing a desired tier into memory.zswap.writeback so writeback targets only that tier. Currently it only controls on/off. Other tiers probably don't need this. demotion based on the selected tier should be enough.

Thanks,
Youngjun Park

maintainer
#50 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 4 lines

In that case, the internal logic could stay roughly the same rather than counting via a page counter. Something like:

1. Change the interface shell: tier.*.max — allow only 0 ~ max.

What about a single interface as I suggested to remain consistent with memory tiering?

Quoted 2 lines

2. Keep the internal logic as is: 0 disables the mask (child memcgs off too), max enables it (child memcgs on too).

I think a child should be able to disable a swap tier enabled by the parent, but not vice versa.

Quoted 1 line

3. memory.zswap.max integrates naturally (it's memory."tier_name".max).

Not really. memory.zswap.max is in terms of memory usage (compressed size), not swap usage (uncompressed size).

[..]

Quoted 11 lines

Let me clarify a part I wrote confusingly. Handling memory.zswap.writeback via tiers is possible, but I don't think the interface itself would be replaced even if memory.swap.tiers is adopted.

Selecting only zswap in memory.swap.tiers would not just disable writeback.it would also block regular swap entirely, which differs slightly from the current semantic. (... "Per the cgroup v2 docs: a zswap-only tier setting is subtly different from setting memory.swap.max to 0, since it still allows pages to be written to the zswap pool; this has no effect if zswap is disabled, and swapping is allowed unless memory.swap.max is set to 0.")

I don't understand. How is disabling zswap writeback not equivalent to only enabling zswap as a tier?

Do you just mean the fact that disabling zswap writeback is a noop of zswap is disabled? It's a different interface so I think a small semantic difference is okay. In practice, I doubt that zswap is being disabled at runtime.

Quoted 10 lines

So the interface itself needs to be retained, and it could be extended toward selective writeback — e.g., passing a desired tier into memory.zswap.writeback so writeback targets only that tier. Currently it only controls on/off. Other tiers probably don't need this. demotion based on the selected tier should be enough.

Thanks,
Youngjun Park

Quoted 115 lines · YoungJun Park

....

Quoted 10 lines
Quoted 4 lines

Based on the memcg interface currently proposed in swap_tier (memory.swap.tiers, memory.swap.tiers.effective), I think it aligns well with the current direction. It provides a foundation for selectively targeting devices in tier order.

Here instead of cpuset like interface, we may want more zswap like interface where you can put limit on the usage i.e. memory.swap.tier*.max. We can start with allowing only two values i.e. 0 and max which effectively will be the same as what you need.

Good idea, and it's certainly feasible. When I considered this a while ago, the reasons I didn't take this direction were:

1. There's no real-world usage for adjusting the swap tier amount (it's either 0 or MAX). That said, your suggestion to initially allow only 0 and max is the killing point, and it's making me reconsider.

2. The implementation cost seems high. The current implementation handles this at runtime via simple masking.

3. Relationship with swap.max:
- If we tie it to the current interface, wouldn't limiting the swap amount within a selected tier already be possible? I wonder if that alone is enough.
- If we add tier.max, it would need to be a subset of swap.max. (Any other complexities here?)

4. vswap enable/disable: vswap doesn't seem to have an amount-control aspect, so an on/off semantic would be clearer.
https://lore.kernel.org/linux-mm/ai5kOOmR1LPTWs1J@yjaykim-PowerEdge-T330/T/#m8831ec057bf9387978d3bd698f51920600e09a04

In that case, the internal logic could stay roughly the same rather than counting via a page counter. Something like:

1. Change the interface shell: tier.*.max — allow only 0 ~ max.
2. Keep the internal logic as is: 0 disables the mask (child memcgs off too), max enables it (child memcgs on too).
3. memory.zswap.max integrates naturally (it's memory."tier_name".max).
4. Extend later if use cases arise.

On balance I still lean toward the current interface, but if a per-tier max is the better fit for memcg's direction and others feel the same, I'm happy to switch. I'd like to hear Shakeel's thoughts again, and I'm curious about others' opinions too.

A few more perspectives on the points below.

Quoted 43 lines

I will respond to your other points later when I have time.

Quoted 41 lines

To summarize the discussions so far, the following points align well.

- Per-cgroup swap control, as I suggested.
- Proactive zswap writeback (Hao's usecase)
- Swap device target demotion(if it wants selective, then it is more better), as you mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- Virtual Swap on/off in the future, as Nhat mentioned:
https://lore.kernel.org/linux-mm/[email protected]/
- The memory.zswap.writeback alternative (no hierarchy model conflict)
- zswap is first swap tier.
- Promotion. (Also better for selectve usage)
- tier based swap policy (e.g round-robin...)

To accelerate this work, I believe we should reach a consensus and merge the currently proposed swap_tier interface :)

If the above approach is difficult, I would like to suggest an alternative for progress with the memcg interfaces removed:

1) We could make zswap the first tier and create
a use case where memory.zswap.writeback internally is handled by tier logic.

2) Or simply merge the swap_tier infrastructure itself first.

This would allow the swap_tier infrastructure to be merged and discussed more easily.

If it takes longer to adopt swap_tier anyway, by doing so we progress next step as a experimental feature.

- Apply per-cgroup swap as an experimental (debugfs) feature.
- Apply Hao's use case experimentally or as it is as Yosry suggested. (future migration to swap tier)

How do you think?

(FYI: My emails to kernel.org are failing due to internal server issues.)

Thank you
Youngjun Park

Let me clarify a part I wrote confusingly. Handling memory.zswap.writeback via tiers is possible, but I don't think the interface itself would be replaced even if memory.swap.tiers is adopted.

Selecting only zswap in memory.swap.tiers would not just disable writeback.it would also block regular swap entirely, which differs slightly from the current semantic. (... "Per the cgroup v2 docs: a zswap-only tier setting is subtly different from setting memory.swap.max to 0, since it still allows pages to be written to the zswap pool; this has no effect if zswap is disabled, and swapping is allowed unless memory.swap.max is set to 0.")

So the interface itself needs to be retained, and it could be extended toward selective writeback — e.g., passing a desired tier into memory.zswap.writeback so writeback targets only that tier. Currently it only controls on/off. Other tiers probably don't need this. demotion based on the selected tier should be enough.

Thanks,
Youngjun Park

#51 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 7 lines · Yosry Ahmed
Quoted 4 lines

In that case, the internal logic could stay roughly the same rather than counting via a page counter. Something like:

1. Change the interface shell: tier.*.max — allow only 0 ~ max.

What about a single interface as I suggested to remain consistent with memory tiering?

Hello Yosry!

I agree. As I was implementing the interface for seeing feasibility , I reconsidered it. Since swap tiers can be added or removed at runtime, having static memory."tier_name".max files seems unnatural.

A single interface like swap.tiers.max would be better. We can use a flat-keyed format (similar to io.weight. same as you suggested)

echo ["tier_name"] ["0 or max"] > swap.tiers.max

I am now leaning towards this is a better direction than what I initially suggested (memory.swap.tiers and memory.swap.tiers.effective).

Considering other reviews and Shakeel's reply, I will update my swap tier patch accordingly.

Quoted 5 lines
Quoted 2 lines

2. Keep the internal logic as is: 0 disables the mask (child memcgs off too), max enables it (child memcgs on too).

I think a child should be able to disable a swap tier enabled by the parent, but not vice versa.

Yes, we are on the same page. I missed a part in my explanation. I meant that the child's selected tiers should be a subset of the parent's (which is how the current swap tier suggestion works).
A child cannot enable a tier that the parent has disabled.

Quoted 4 lines
Quoted 1 line

3. memory.zswap.max integrates naturally (it's memory."tier_name".max).

Not really. memory.zswap.max is in terms of memory usage (compressed size), not swap usage (uncompressed size).

I see, memory.zswap.max needs to be maintained separately. I will look closer into its semantics. I might have misunderstood this part!

Quoted 13 lines

[..]

Quoted 11 lines

Let me clarify a part I wrote confusingly. Handling memory.zswap.writeback via tiers is possible, but I don't think the interface itself would be replaced even if memory.swap.tiers is adopted.

Selecting only zswap in memory.swap.tiers would not just disable writeback.it would also block regular swap entirely, which differs slightly from the current semantic. (... "Per the cgroup v2 docs: a zswap-only tier setting is subtly different from setting memory.swap.max to 0, since it still allows pages to be written to the zswap pool; this has no effect if zswap is disabled, and swapping is allowed unless memory.swap.max is set to 0.")

Quoted 2 lines

I don't understand. How is disabling zswap writeback not equivalent to only enabling zswap as a tier?

Isn't there a case where zswap_store() fails and pages fall back to the backing swap device?

- "zswap tier only": Only zswap is allowed. Fallback to other swap is blocked.
- "zswap writeback disabled": zswap is allowed, but if zswap_store() fails, pages can still fall back to other swap devices.

Because of this slight semantic difference, I thought they couldn't be fully unified. If my understanding is correct, we could extend the zswap tier to select the target swap device for writeback, but replacing the writeback interface entirely might be difficult.

Quoted 2 lines

Do you just mean the fact that disabling zswap writeback is a noop of zswap is disabled? It's a different interface so I think a small

Yes, I think so too.

Quoted 2 lines

semantic difference is okay. In practice, I doubt that zswap is being disabled at runtime.

I thought disabling zswap at runtime might have some use cases, but we can discuss this further when we talk about the patch extending the zswap tier.

Best regards,
Youngjun

#52 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

...

Quoted 4 lines

- "zswap tier only": Only zswap is allowed. Fallback to other swap is blocked.
- "zswap writeback disabled": zswap is allowed, but if zswap_store() fails, pages can still fall back to other swap devices.

Upon double-checking the code, my previous clarification was wrong. You are right. Sorry for the confusion. "zswap tier only" is indeed equivalent to "zswap writeback disabled".
(I'm not sure why I read the code that way...)

As I initially thought, it might be possible to replace the zswap writeback control with the tiering mechanism.

If we need to keep the existing interface, we can integrate or share the underlying logic (though the specific details need more thought anyway).

It can be summarized as follows:

- "zswap tier only" + "zswap writeback disable" -> meaningless (noop)
- "zswap tier only" + "zswap writeback enable" -> meaningless (no writabck backend exist)
- "zswap tier with other tiers" + "zswap writeback disable" -> uses only zswap (can be replaced by "zswap tier only". This code could be intergrated, modified or something.)
- "zswap tier with other tiers" + "zswap writeback enable" -> works as is

As mentioned in the previous email, the zswap tier on/off control comes as a bonus (though, as you pointed out, we may need to discuss if it's actually needed).

BR,
Youngjun

maintainer
#53 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 18 lines · YoungJun Park

...

Quoted 4 lines

- "zswap tier only": Only zswap is allowed. Fallback to other swap is blocked.
- "zswap writeback disabled": zswap is allowed, but if zswap_store() fails, pages can still fall back to other swap devices.

Upon double-checking the code, my previous clarification was wrong. You are right. Sorry for the confusion. "zswap tier only" is indeed equivalent to "zswap writeback disabled".
(I'm not sure why I read the code that way...)

As I initially thought, it might be possible to replace the zswap writeback control with the tiering mechanism.

If we need to keep the existing interface, we can integrate or share the underlying logic (though the specific details need more thought anyway).

Quoted 7 lines

It can be summarized as follows:

- "zswap tier only" + "zswap writeback disable" -> meaningless (noop)
- "zswap tier only" + "zswap writeback enable" -> meaningless (no writabck backend exist)
- "zswap tier with other tiers" + "zswap writeback disable" -> uses only zswap (can be replaced by "zswap tier only". This code could be intergrated, modified or something.)
- "zswap tier with other tiers" + "zswap writeback enable" -> works as is

Hmm we might want to somehow disable memory.zswap.writeback if tiering is enabled, to avoid having to deal with this. But I am not sure how possible this is.

If swap tiering is behind a config option, maybe we can disable memory.zswap.writeback under that config option? But then if distros start enabling the config option it might break some users. Not sure what's the right way to do this, but having both interfaces active at the same time is annoying.

Quoted 7 lines

As mentioned in the previous email, the zswap tier on/off control comes as a bonus (though, as you pointed out, we may need to discuss if it's actually needed).

BR,
Youngjun

maintainer
#54 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 20 lines · Yosry Ahmed
Quoted 18 lines · YoungJun Park

...

Quoted 4 lines

- "zswap tier only": Only zswap is allowed. Fallback to other swap is blocked.
- "zswap writeback disabled": zswap is allowed, but if zswap_store() fails, pages can still fall back to other swap devices.

Upon double-checking the code, my previous clarification was wrong. You are right. Sorry for the confusion. "zswap tier only" is indeed equivalent to "zswap writeback disabled".
(I'm not sure why I read the code that way...)

As I initially thought, it might be possible to replace the zswap writeback control with the tiering mechanism.

If we need to keep the existing interface, we can integrate or share the underlying logic (though the specific details need more thought anyway).

Quoted 7 lines
Quoted 7 lines

It can be summarized as follows:

- "zswap tier only" + "zswap writeback disable" -> meaningless (noop)
- "zswap tier only" + "zswap writeback enable" -> meaningless (no writabck backend exist)
- "zswap tier with other tiers" + "zswap writeback disable" -> uses only zswap (can be replaced by "zswap tier only". This code could be intergrated, modified or something.)
- "zswap tier with other tiers" + "zswap writeback enable" -> works as is

TBH, without vswap, we should not allow setting zswap as its own tier. It's meaningless. Maybe makes it a no-op, and warn users what they're setting is gibberish?

Quoted 4 lines

Hmm we might want to somehow disable memory.zswap.writeback if tiering is enabled, to avoid having to deal with this. But I am not sure how possible this is.

With tiering and without vswap, you still need an interface to prescribe that a cgroup can:

1. Allocate slots from a certain swap device.

2. Use zswap, backed by those slots.

3. But no IO is allowed to those slots (no writeback or fallback on zswap failure).

So we still need memory.zswap.writeback, until we get rid of non-vswap case for zswap.

maintainer
#55 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 30 lines · Nhat Pham
Quoted 20 lines · Yosry Ahmed
Quoted 18 lines · YoungJun Park

...

Quoted 4 lines

- "zswap tier only": Only zswap is allowed. Fallback to other swap is blocked.
- "zswap writeback disabled": zswap is allowed, but if zswap_store() fails, pages can still fall back to other swap devices.

Upon double-checking the code, my previous clarification was wrong. You are right. Sorry for the confusion. "zswap tier only" is indeed equivalent to "zswap writeback disabled".
(I'm not sure why I read the code that way...)

As I initially thought, it might be possible to replace the zswap writeback control with the tiering mechanism.

If we need to keep the existing interface, we can integrate or share the underlying logic (though the specific details need more thought anyway).

Quoted 7 lines
Quoted 7 lines

It can be summarized as follows:

- "zswap tier only" + "zswap writeback disable" -> meaningless (noop)
- "zswap tier only" + "zswap writeback enable" -> meaningless (no writabck backend exist)
- "zswap tier with other tiers" + "zswap writeback disable" -> uses only zswap (can be replaced by "zswap tier only". This code could be intergrated, modified or something.)
- "zswap tier with other tiers" + "zswap writeback enable" -> works as is

Quoted 3 lines

TBH, without vswap, we should not allow setting zswap as its own tier. It's meaningless. Maybe makes it a no-op, and warn users what they're setting is gibberish?

Why? vswap is transparent to the user. Why can't zswap be its own tier?

maintainer
#56 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 8 lines · Yosry Ahmed
Quoted 4 lines · Nhat Pham

TBH, without vswap, we should not allow setting zswap as its own tier. It's meaningless. Maybe makes it a no-op, and warn users what they're setting is gibberish?

Why? vswap is transparent to the user. Why can't zswap be its own tier?

Without vswap, if you set zswap as its own tier, which phys swap device should we allocate from for the backing slot? :)

With vswap then it makes sense (and would probably be the "default" for zswap setup until we enable zswap writeback).

maintainer
#57 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 5 lines · Nhat Pham
Quoted 3 lines · Yosry Ahmed
Quoted 1 line · Nhat Pham
Quoted 8 lines
Quoted 5 lines
Quoted 3 lines

TBH, without vswap, we should not allow setting zswap as its own tier. It's meaningless. Maybe makes it a no-op, and warn users what they're setting is gibberish?

Why? vswap is transparent to the user. Why can't zswap be its own tier?

Without vswap, if you set zswap as its own tier, which phys swap device should we allocate from for the backing slot? :)

Today we just allocate a swap slot in a swapfile during reclaim, before swapout, and zswap will just writeback to that one. I assume the same will work with swap tiering, except that maybe the way that swap slot will respect the allowed swap tiers?

Quoted 3 lines

With vswap then it makes sense (and would probably be the "default" for zswap setup until we enable zswap writeback).

maintainer
#58 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 13 lines · Yosry Ahmed
Quoted 11 lines · Nhat Pham
Quoted 8 lines · Yosry Ahmed
Quoted 4 lines · Nhat Pham

TBH, without vswap, we should not allow setting zswap as its own tier. It's meaningless. Maybe makes it a no-op, and warn users what they're setting is gibberish?

Why? vswap is transparent to the user. Why can't zswap be its own tier?

Quoted 7 lines
Quoted 2 lines

Without vswap, if you set zswap as its own tier, which phys swap device should we allocate from for the backing slot? :)

Today we just allocate a swap slot in a swapfile during reclaim, before swapout, and zswap will just writeback to that one. I assume the same will work with swap tiering, except that maybe the way that swap slot will respect the allowed swap tiers?

Yep! So if we set zswap as the only tier, then it wouldn't be able to allocate a swap slot in swapfile right?

Or are you suggesting that if we set zswap as the only tier then we can allocate from any swapfile (since we're not doing any IO anyway)?

maintainer
#59 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 18 lines · Nhat Pham
Quoted 16 lines · Yosry Ahmed
Quoted 13 lines · Nhat Pham
Quoted 8 lines · Yosry Ahmed
Quoted 4 lines · Nhat Pham

TBH, without vswap, we should not allow setting zswap as its own tier. It's meaningless. Maybe makes it a no-op, and warn users what they're setting is gibberish?

Why? vswap is transparent to the user. Why can't zswap be its own tier?

Without vswap, if you set zswap as its own tier, which phys swap device should we allocate from for the backing slot? :)

Quoted 7 lines
Quoted 4 lines

Today we just allocate a swap slot in a swapfile during reclaim, before swapout, and zswap will just writeback to that one. I assume the same will work with swap tiering, except that maybe the way that swap slot will respect the allowed swap tiers?

Yep! So if we set zswap as the only tier, then it wouldn't be able to allocate a swap slot in swapfile right?

Ohh I thought you meant we shouldn't allow zswap to be a tier at all, not the *only* tier.

Quoted 2 lines

Or are you suggesting that if we set zswap as the only tier then we can allocate from any swapfile (since we're not doing any IO anyway)?

Hmm, technically having zswap as the only tier should be equivalent to disabling writeback, but you're right that if zswap is the only tier than the memcg is not allowed to use swap slots from any swapfile, so zswap cannot be used. Very good point :)

In this case I think yes, we need vswap to be enabled to allow making zswap the only tier. That's one gap between zswap being the only tier and disabling zswap writeback, the former requires vswap while the latter doesn't.

maintainer
#60 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 6 lines · Yosry Ahmed

On Tue, Jun 16, 2026 at 1:24 PM Nhat Pham <[email protected]> wrote:

Ohh I thought you meant we shouldn't allow zswap to be a tier at all, not the *only* tier.

Quoted 7 lines
Quoted 2 lines

Or are you suggesting that if we set zswap as the only tier then we can allocate from any swapfile (since we're not doing any IO anyway)?

Hmm, technically having zswap as the only tier should be equivalent to disabling writeback, but you're right that if zswap is the only tier than the memcg is not allowed to use swap slots from any swapfile, so zswap cannot be used. Very good point :)

Yeah the coupling of swap/zswap makes reasoning about these kinds of things so annoying. :)

If anything, with vswap, I'll stop having to explain to folks why they have to provision on-disk swapfile when they only want to use in-memory compressed swap, and that's a win in my book.

Quoted 5 lines

In this case I think yes, we need vswap to be enabled to allow making zswap the only tier. That's one gap between zswap being the only tier and disabling zswap writeback, the former requires vswap while the latter doesn't.

Yup! Anyway, I think Youngjun sent out v8 - let's take a look.

maintainer
#61 lore raw

Re: [swap tier discussion] Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

In reply to YoungJun Park
Quoted 18 lines · YoungJun Park
Quoted 7 lines · Yosry Ahmed
Quoted 4 lines

In that case, the internal logic could stay roughly the same rather than counting via a page counter. Something like:

1. Change the interface shell: tier.*.max — allow only 0 ~ max.

What about a single interface as I suggested to remain consistent with memory tiering?

Hello Yosry!

I agree. As I was implementing the interface for seeing feasibility , I reconsidered it. Since swap tiers can be added or removed at runtime, having static memory."tier_name".max files seems unnatural.

A single interface like swap.tiers.max would be better. We can use a flat-keyed format (similar to io.weight. same as you suggested)

Quoted 7 lines

echo ["tier_name"] ["0 or max"] > swap.tiers.max

I am now leaning towards this is a better direction than what I initially suggested (memory.swap.tiers and memory.swap.tiers.effective).

Considering other reviews and Shakeel's reply, I will update my swap tier patch accordingly.

I like Yosry's proposal. Let's go with that.

(I am travelling, so will be slow to respond)

maintainer
#62 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

In reply to Hao Jia
Quoted 64 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Note that the actual amount written back may be less than requested due to the zswap second-chance algorithm: referenced entries are rotated on the LRU on the first encounter and only written back on a second pass. If fewer bytes are written back than requested, -EAGAIN is returned, matching the existing memory.reclaim semantics.

Internally, extend user_proactive_reclaim() to parse the new "zswap_writeback_only" token and invoke the dedicated handler. Add zswap_proactive_writeback() to walk the target memcg subtree via the per-memcg writeback cursor, draining per-node zswap LRUs through list_lru_walk_one() with the shrink_memcg_cb() callback.

---
 Documentation/admin-guide/cgroup-v2.rst |  18 +++-
 Documentation/admin-guide/mm/zswap.rst  |  11 +-
 include/linux/zswap.h                   |   7 ++
 mm/vmscan.c                             |  14 +++
 mm/zswap.c                              | 138 ++++++++++++++++++++++++
 5 files changed, 185 insertions(+), 3 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +5 -2
index 6efd0095ed99..6564abf0dec5 100644
@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.
 
 The following nested keys are defined.
 
-	  ==========            ================================
+	  ====================  ==================================================
 	  swappiness            Swappiness value to reclaim with
-	  ==========            ================================
+	  zswap_writeback_only  Only perform proactive zswap writeback
+	  ====================  ==================================================
 
 	Specifying a swappiness value instructs the kernel to perform
 	the reclaim with that swappiness value. Note that this has the
@@ -1437,6 +1438,19 @@ The following nested keys are defined.
 	The valid range for swappiness is [0-200, max], setting
 	swappiness=max exclusively reclaims anonymous memory.
 
+	The zswap_writeback_only key skips ordinary memory reclaim and
+	writes back pages from zswap to the backing swap device until

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1452

Quoted 10 lines
Documentation/admin-guide/cgroup-v2.rst +10 -0
+	the requested amount has been written or no further candidates
+	are found. This is useful to proactively offload cold pages from
+	the zswap pool to the swap device. It is only available if
+	zswap writeback is enabled. zswap_writeback_only cannot be combined
+	with swappiness; specifying both returns -EINVAL.
+
+	Example::
+
+	  # Write back up to 100MB of pages from zswap to the backing swap
+	  echo "100M zswap_writeback_only" > memory.reclaim

memcg folks need to chime in about the interface here. An alternative would be a separate interface (e.g. memory.zswap.do_writeback or memory.zswap.writeback.reclaim or sth).

Quoted 136 lines
mm/zswap.c +128 -0
index 73e64a635690..7bcbf788f634 100644
@@ -1679,6 +1679,144 @@ int zswap_load(struct folio *folio)
 	return 0;
 }
 
+/*
+ * Maximum LRU scan limit:
+ * number of entries to scan per page of remaining budget.
+ */
+#define ZSWAP_PROACTIVE_WB_SCAN_RATIO	16UL
+/*
+ * Batch size for proactive writeback:
+ * - As the per-memcg writeback target in the outer memcg loop.
+ * - As the per-walk budget passed to list_lru_walk_one().
+ */
+#define ZSWAP_PROACTIVE_WB_BATCH	128UL
+
+/*
+ * Walk the per-node LRUs of @memcg to write back up to @nr_to_write pages.
+ * Returns the number of pages written back, or -ENOENT if @memcg is a
+ * zombie or has writeback disabled.
+ */
+static long zswap_proactive_shrink_memcg(struct mem_cgroup *memcg,
+					 unsigned long nr_to_write)
+{
+	unsigned long nr_written = 0;
+	int nid;
+
+	if (!mem_cgroup_zswap_writeback_enabled(memcg))
+		return -ENOENT;
+
+	if (!mem_cgroup_online(memcg))
+		return -ENOENT;
+
+	for_each_node_state(nid, N_NORMAL_MEMORY) {
+		bool encountered_page_in_swapcache = false;
+		unsigned long nr_to_scan, nr_scanned = 0;
+
+		/*
+		 * Cap by LRU length: bounds rewalks when referenced
+		 * entries keep rotating to the tail.
+		 */
+		nr_to_scan = list_lru_count_one(&zswap_list_lru, nid, memcg);
+		if (!nr_to_scan)
+			continue;
+
+		/*
+		 * Cap by SCAN_RATIO * remaining budget: bounds scan cost
+		 * to the remaining writeback budget.
+		 */
+		nr_to_scan = min(nr_to_scan,
+				 (nr_to_write - nr_written) * ZSWAP_PROACTIVE_WB_SCAN_RATIO);
+
+		while (nr_scanned < nr_to_scan) {
+			unsigned long nr_to_walk = min(ZSWAP_PROACTIVE_WB_BATCH,
+						       nr_to_scan - nr_scanned);
+
+			if (signal_pending(current))
+				return nr_written;
+
+			/*
+			 * Account for the committed budget rather than the walker's
+			 * actual delta. If the list is emptied concurrently, the
+			 * walker visits nothing and nr_scanned would never advance.
+			 */
+			nr_scanned += nr_to_walk;
+
+			nr_written += list_lru_walk_one(&zswap_list_lru, nid, memcg,
+							&shrink_memcg_cb,
+							&encountered_page_in_swapcache,
+							&nr_to_walk);
+
+			if (nr_written >= nr_to_write)
+				return nr_written;
+			if (encountered_page_in_swapcache)
+				break;
+
+			cond_resched();
+		}
+	}
+
+	return nr_written;
+}
+
+int zswap_proactive_writeback(struct mem_cgroup *memcg,
+			      unsigned long nr_to_writeback)
+{
+	struct mem_cgroup *iter_memcg;
+	unsigned long nr_written = 0;
+	int failures = 0, attempts = 0;
+
+	if (!memcg)
+		return -EINVAL;
+	if (!nr_to_writeback)
+		return 0;
+
+	/*
+	 * Writeback will be aborted with -EAGAIN if we encounter
+	 * the following MAX_RECLAIM_RETRIES times:
+	 * - No writeback-candidate memcgs found in a subtree walk.
+	 * - A writeback-candidate memcg wrote back zero pages.
+	 */
+	while (nr_written < nr_to_writeback) {
+		unsigned long batch_size;
+		long shrunk;
+
+		if (signal_pending(current))
+			return -EINTR;
+
+		iter_memcg = zswap_mem_cgroup_iter(memcg);
+
+		if (!iter_memcg) {
+			/*
+			 * Continue without incrementing failures if we found
+			 * candidate memcgs in the last subtree walk.
+			 */
+			if (!attempts && ++failures == MAX_RECLAIM_RETRIES)
+				return -EAGAIN;
+			attempts = 0;
+			continue;
+		}
+
+		batch_size = min(nr_to_writeback - nr_written,
+				 ZSWAP_PROACTIVE_WB_BATCH);
+		shrunk = zswap_proactive_shrink_memcg(iter_memcg, batch_size);
+		mem_cgroup_put(iter_memcg);
+
+		/* Writeback-disabled or offline: skip without counting. */
+		if (shrunk == -ENOENT)
+			continue;
+
+		++attempts;
+		if (shrunk > 0)

mm/zswap.c int zswap_load(struct folio *folio) · line 1819

Quoted 10 lines
mm/zswap.c +10 -0
+			nr_written += shrunk;
+		else if (++failures == MAX_RECLAIM_RETRIES)
+			return -EAGAIN;
+
+		cond_resched();
+	}
+
+	return 0;
+}
+

There is a lot of copy+paste from shrink_worker() and shrink_memcg() here. We really should be able to reuse shrink_memcg().

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Nhat, what do you think?

author
#63 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 222 lines · Yosry Ahmed
Quoted 64 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Note that the actual amount written back may be less than requested due to the zswap second-chance algorithm: referenced entries are rotated on the LRU on the first encounter and only written back on a second pass. If fewer bytes are written back than requested, -EAGAIN is returned, matching the existing memory.reclaim semantics.

Internally, extend user_proactive_reclaim() to parse the new "zswap_writeback_only" token and invoke the dedicated handler. Add zswap_proactive_writeback() to walk the target memcg subtree via the per-memcg writeback cursor, draining per-node zswap LRUs through list_lru_walk_one() with the shrink_memcg_cb() callback.

---
  Documentation/admin-guide/cgroup-v2.rst |  18 +++-
  Documentation/admin-guide/mm/zswap.rst  |  11 +-
  include/linux/zswap.h                   |   7 ++
  mm/vmscan.c                             |  14 +++
  mm/zswap.c                              | 138 ++++++++++++++++++++++++
  5 files changed, 185 insertions(+), 3 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +5 -2
index 6efd0095ed99..6564abf0dec5 100644
@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.
 
  The following nested keys are defined.
 
-	  ==========            ================================
+	  ====================  ==================================================
  	  swappiness            Swappiness value to reclaim with
-	  ==========            ================================
+	  zswap_writeback_only  Only perform proactive zswap writeback
+	  ====================  ==================================================
 
  	Specifying a swappiness value instructs the kernel to perform
  	the reclaim with that swappiness value. Note that this has the
@@ -1437,6 +1438,19 @@ The following nested keys are defined.
  	The valid range for swappiness is [0-200, max], setting
  	swappiness=max exclusively reclaims anonymous memory.
 
+	The zswap_writeback_only key skips ordinary memory reclaim and
+	writes back pages from zswap to the backing swap device until

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1452

Quoted 10 lines
Documentation/admin-guide/cgroup-v2.rst +10 -0
+	the requested amount has been written or no further candidates
+	are found. This is useful to proactively offload cold pages from
+	the zswap pool to the swap device. It is only available if
+	zswap writeback is enabled. zswap_writeback_only cannot be combined
+	with swappiness; specifying both returns -EINVAL.
+
+	Example::
+
+	  # Write back up to 100MB of pages from zswap to the backing swap
+	  echo "100M zswap_writeback_only" > memory.reclaim

memcg folks need to chime in about the interface here. An alternative would be a separate interface (e.g. memory.zswap.do_writeback or memory.zswap.writeback.reclaim or sth).

Quoted 141 lines
mm/zswap.c +133 -0
index 73e64a635690..7bcbf788f634 100644
@@ -1679,6 +1679,144 @@ int zswap_load(struct folio *folio)
  	return 0;
  }
 
+/*
+ * Maximum LRU scan limit:
+ * number of entries to scan per page of remaining budget.
+ */
+#define ZSWAP_PROACTIVE_WB_SCAN_RATIO	16UL
+/*
+ * Batch size for proactive writeback:
+ * - As the per-memcg writeback target in the outer memcg loop.
+ * - As the per-walk budget passed to list_lru_walk_one().
+ */
+#define ZSWAP_PROACTIVE_WB_BATCH	128UL
+
+/*
+ * Walk the per-node LRUs of @memcg to write back up to @nr_to_write pages.
+ * Returns the number of pages written back, or -ENOENT if @memcg is a
+ * zombie or has writeback disabled.
+ */
+static long zswap_proactive_shrink_memcg(struct mem_cgroup *memcg,
+					 unsigned long nr_to_write)
+{
+	unsigned long nr_written = 0;
+	int nid;
+
+	if (!mem_cgroup_zswap_writeback_enabled(memcg))
+		return -ENOENT;
+
+	if (!mem_cgroup_online(memcg))
+		return -ENOENT;
+
+	for_each_node_state(nid, N_NORMAL_MEMORY) {
+		bool encountered_page_in_swapcache = false;
+		unsigned long nr_to_scan, nr_scanned = 0;
+
+		/*
+		 * Cap by LRU length: bounds rewalks when referenced
+		 * entries keep rotating to the tail.
+		 */
+		nr_to_scan = list_lru_count_one(&zswap_list_lru, nid, memcg);
+		if (!nr_to_scan)
+			continue;
+
+		/*
+		 * Cap by SCAN_RATIO * remaining budget: bounds scan cost
+		 * to the remaining writeback budget.
+		 */
+		nr_to_scan = min(nr_to_scan,
+				 (nr_to_write - nr_written) * ZSWAP_PROACTIVE_WB_SCAN_RATIO);
+
+		while (nr_scanned < nr_to_scan) {
+			unsigned long nr_to_walk = min(ZSWAP_PROACTIVE_WB_BATCH,
+						       nr_to_scan - nr_scanned);
+
+			if (signal_pending(current))
+				return nr_written;
+
+			/*
+			 * Account for the committed budget rather than the walker's
+			 * actual delta. If the list is emptied concurrently, the
+			 * walker visits nothing and nr_scanned would never advance.
+			 */
+			nr_scanned += nr_to_walk;
+
+			nr_written += list_lru_walk_one(&zswap_list_lru, nid, memcg,
+							&shrink_memcg_cb,
+							&encountered_page_in_swapcache,
+							&nr_to_walk);
+
+			if (nr_written >= nr_to_write)
+				return nr_written;
+			if (encountered_page_in_swapcache)
+				break;
+
+			cond_resched();
+		}
+	}
+
+	return nr_written;
+}
+
+int zswap_proactive_writeback(struct mem_cgroup *memcg,
+			      unsigned long nr_to_writeback)
+{
+	struct mem_cgroup *iter_memcg;
+	unsigned long nr_written = 0;
+	int failures = 0, attempts = 0;
+
+	if (!memcg)
+		return -EINVAL;
+	if (!nr_to_writeback)
+		return 0;
+
+	/*
+	 * Writeback will be aborted with -EAGAIN if we encounter
+	 * the following MAX_RECLAIM_RETRIES times:
+	 * - No writeback-candidate memcgs found in a subtree walk.
+	 * - A writeback-candidate memcg wrote back zero pages.
+	 */
+	while (nr_written < nr_to_writeback) {
+		unsigned long batch_size;
+		long shrunk;
+
+		if (signal_pending(current))
+			return -EINTR;
+
+		iter_memcg = zswap_mem_cgroup_iter(memcg);
+
+		if (!iter_memcg) {
+			/*
+			 * Continue without incrementing failures if we found
+			 * candidate memcgs in the last subtree walk.
+			 */
+			if (!attempts && ++failures == MAX_RECLAIM_RETRIES)
+				return -EAGAIN;
+			attempts = 0;
+			continue;
+		}
+
+		batch_size = min(nr_to_writeback - nr_written,
+				 ZSWAP_PROACTIVE_WB_BATCH);
+		shrunk = zswap_proactive_shrink_memcg(iter_memcg, batch_size);
+		mem_cgroup_put(iter_memcg);
+
+		/* Writeback-disabled or offline: skip without counting. */
+		if (shrunk == -ENOENT)
+			continue;
+
+		++attempts;
+		if (shrunk > 0)
+			nr_written += shrunk;
+		else if (++failures == MAX_RECLAIM_RETRIES)
+			return -EAGAIN;
+
+		cond_resched();
Quoted 9 lines

mm/zswap.c int zswap_load(struct folio *folio) · line 1798

Quoted 5 lines
mm/zswap.c +5 -0
+	}
+
+	return 0;
+}
+

There is a lot of copy+paste from shrink_worker() and shrink_memcg() here. We really should be able to reuse shrink_memcg().

I will do some consolidation and code reuse in the next version.

Quoted 7 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

Of course, we could pass a parameter to control whether batch writeback is needed, so as to preserve the original behavior of shrink_memcg().

Thanks,
Hao

maintainer
#64 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 6 lines
Quoted 6 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Quoted 7 lines
Quoted 1 line

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

Of course, we could pass a parameter to control whether batch writeback is needed, so as to preserve the original behavior of shrink_memcg().

Yeah probably best to parameterize the batch size and keep the current behavior of the shrinker for this series. We can play with using batches for the shrinker later.

Quoted 3 lines

Thanks,
Hao

maintainer
#65 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

In reply to Hao Jia
Quoted 244 lines · Hao Jia
Quoted 222 lines · Yosry Ahmed
Quoted 64 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 100MB of pages from zswap to the backing swap echo "100M zswap_writeback_only" > memory.reclaim

Note that the actual amount written back may be less than requested due to the zswap second-chance algorithm: referenced entries are rotated on the LRU on the first encounter and only written back on a second pass. If fewer bytes are written back than requested, -EAGAIN is returned, matching the existing memory.reclaim semantics.

Internally, extend user_proactive_reclaim() to parse the new "zswap_writeback_only" token and invoke the dedicated handler. Add zswap_proactive_writeback() to walk the target memcg subtree via the per-memcg writeback cursor, draining per-node zswap LRUs through list_lru_walk_one() with the shrink_memcg_cb() callback.

---
  Documentation/admin-guide/cgroup-v2.rst |  18 +++-
  Documentation/admin-guide/mm/zswap.rst  |  11 +-
  include/linux/zswap.h                   |   7 ++
  mm/vmscan.c                             |  14 +++
  mm/zswap.c                              | 138 ++++++++++++++++++++++++
  5 files changed, 185 insertions(+), 3 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +5 -2
index 6efd0095ed99..6564abf0dec5 100644
@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.
 
  The following nested keys are defined.
 
-      ==========            ================================
+      ====================  ==================================================
       swappiness            Swappiness value to reclaim with
-      ==========            ================================
+      zswap_writeback_only  Only perform proactive zswap writeback
+      ====================  ==================================================
 
     Specifying a swappiness value instructs the kernel to perform
     the reclaim with that swappiness value. Note that this has the
@@ -1437,6 +1438,19 @@ The following nested keys are defined.
     The valid range for swappiness is [0-200, max], setting
     swappiness=max exclusively reclaims anonymous memory.
 
+    The zswap_writeback_only key skips ordinary memory reclaim and
+    writes back pages from zswap to the backing swap device until

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1452

Quoted 10 lines
Documentation/admin-guide/cgroup-v2.rst +10 -0
+    the requested amount has been written or no further candidates
+    are found. This is useful to proactively offload cold pages from
+    the zswap pool to the swap device. It is only available if
+    zswap writeback is enabled. zswap_writeback_only cannot be combined
+    with swappiness; specifying both returns -EINVAL.
+
+    Example::
+
+      # Write back up to 100MB of pages from zswap to the backing swap
+      echo "100M zswap_writeback_only" > memory.reclaim

memcg folks need to chime in about the interface here. An alternative would be a separate interface (e.g. memory.zswap.do_writeback or memory.zswap.writeback.reclaim or sth).

Quoted 141 lines
mm/zswap.c +133 -0
index 73e64a635690..7bcbf788f634 100644
@@ -1679,6 +1679,144 @@ int zswap_load(struct folio *folio)
     return 0;
  }
 
+/*
+ * Maximum LRU scan limit:
+ * number of entries to scan per page of remaining budget.
+ */
+#define ZSWAP_PROACTIVE_WB_SCAN_RATIO       16UL
+/*
+ * Batch size for proactive writeback:
+ * - As the per-memcg writeback target in the outer memcg loop.
+ * - As the per-walk budget passed to list_lru_walk_one().
+ */
+#define ZSWAP_PROACTIVE_WB_BATCH    128UL
+
+/*
+ * Walk the per-node LRUs of @memcg to write back up to @nr_to_write pages.
+ * Returns the number of pages written back, or -ENOENT if @memcg is a
+ * zombie or has writeback disabled.
+ */
+static long zswap_proactive_shrink_memcg(struct mem_cgroup *memcg,
+                                     unsigned long nr_to_write)
+{
+    unsigned long nr_written = 0;
+    int nid;
+
+    if (!mem_cgroup_zswap_writeback_enabled(memcg))
+            return -ENOENT;
+
+    if (!mem_cgroup_online(memcg))
+            return -ENOENT;
+
+    for_each_node_state(nid, N_NORMAL_MEMORY) {
+            bool encountered_page_in_swapcache = false;
+            unsigned long nr_to_scan, nr_scanned = 0;
+
+            /*
+             * Cap by LRU length: bounds rewalks when referenced
+             * entries keep rotating to the tail.
+             */
+            nr_to_scan = list_lru_count_one(&zswap_list_lru, nid, memcg);
+            if (!nr_to_scan)
+                    continue;
+
+            /*
+             * Cap by SCAN_RATIO * remaining budget: bounds scan cost
+             * to the remaining writeback budget.
+             */
+            nr_to_scan = min(nr_to_scan,
+                             (nr_to_write - nr_written) * ZSWAP_PROACTIVE_WB_SCAN_RATIO);
+
+            while (nr_scanned < nr_to_scan) {
+                    unsigned long nr_to_walk = min(ZSWAP_PROACTIVE_WB_BATCH,
+                                                   nr_to_scan - nr_scanned);
+
+                    if (signal_pending(current))
+                            return nr_written;
+
+                    /*
+                     * Account for the committed budget rather than the walker's
+                     * actual delta. If the list is emptied concurrently, the
+                     * walker visits nothing and nr_scanned would never advance.
+                     */
+                    nr_scanned += nr_to_walk;
+
+                    nr_written += list_lru_walk_one(&zswap_list_lru, nid, memcg,
+                                                    &shrink_memcg_cb,
+                                                    &encountered_page_in_swapcache,
+                                                    &nr_to_walk);
+
+                    if (nr_written >= nr_to_write)
+                            return nr_written;
+                    if (encountered_page_in_swapcache)
+                            break;
+
+                    cond_resched();
+            }
+    }
+
+    return nr_written;
+}
+
+int zswap_proactive_writeback(struct mem_cgroup *memcg,
+                          unsigned long nr_to_writeback)
+{
+    struct mem_cgroup *iter_memcg;
+    unsigned long nr_written = 0;
+    int failures = 0, attempts = 0;
+
+    if (!memcg)
+            return -EINVAL;
+    if (!nr_to_writeback)
+            return 0;
+
+    /*
+     * Writeback will be aborted with -EAGAIN if we encounter
+     * the following MAX_RECLAIM_RETRIES times:
+     * - No writeback-candidate memcgs found in a subtree walk.
+     * - A writeback-candidate memcg wrote back zero pages.
+     */
+    while (nr_written < nr_to_writeback) {
+            unsigned long batch_size;
+            long shrunk;
+
+            if (signal_pending(current))
+                    return -EINTR;
+
+            iter_memcg = zswap_mem_cgroup_iter(memcg);
+
+            if (!iter_memcg) {
+                    /*
+                     * Continue without incrementing failures if we found
+                     * candidate memcgs in the last subtree walk.
+                     */
+                    if (!attempts && ++failures == MAX_RECLAIM_RETRIES)
+                            return -EAGAIN;
+                    attempts = 0;
+                    continue;
+            }
+
+            batch_size = min(nr_to_writeback - nr_written,
+                             ZSWAP_PROACTIVE_WB_BATCH);
+            shrunk = zswap_proactive_shrink_memcg(iter_memcg, batch_size);
+            mem_cgroup_put(iter_memcg);
+
+            /* Writeback-disabled or offline: skip without counting. */
+            if (shrunk == -ENOENT)
+                    continue;
+
+            ++attempts;
+            if (shrunk > 0)
+                    nr_written += shrunk;
+            else if (++failures == MAX_RECLAIM_RETRIES)
+                    return -EAGAIN;
+
+            cond_resched();
Quoted 9 lines

mm/zswap.c int zswap_load(struct folio *folio) · line 1798

Quoted 5 lines
mm/zswap.c +5 -0
+    }
+
+    return 0;
+}
+

There is a lot of copy+paste from shrink_worker() and shrink_memcg() here. We really should be able to reuse shrink_memcg().

I will do some consolidation and code reuse in the next version.

Quoted 6 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Quoted 4 lines
Quoted 1 line

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

I just take a look at shrink_memcg() and realized it's reclaiming one page at a time. Thanks for the reminder - I hated it.

Please batchify it if it makes your life easier :) We don't reclaim "just one page/object" anywhere else in the reclaim path, Sure, it adds a bit of latency to zswap_store() if we reached cgroup limit, but IMHO if we hit zswap.max limit at zswap_store() time, that is already the slowest of slow path that you should have avoided with proactive reclaim/zswap shrinker in the first place. And, setting zswap.max does not make sense to me in the first place (I can write a whole essay about it).

maintainer
#66 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 14 lines
Quoted 10 lines
Quoted 7 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

I just take a look at shrink_memcg() and realized it's reclaiming one page at a time. Thanks for the reminder - I hated it.

Quoted 8 lines

Please batchify it if it makes your life easier :) We don't reclaim "just one page/object" anywhere else in the reclaim path, Sure, it adds a bit of latency to zswap_store() if we reached cgroup limit, but IMHO if we hit zswap.max limit at zswap_store() time, that is already the slowest of slow path that you should have avoided with proactive reclaim/zswap shrinker in the first place. And, setting zswap.max does not make sense to me in the first place (I can write a whole essay about it).

Should we batchify shrink_memcg() from the shrinker and background writeback, but leave the synchronous zswap_store() path to reclaim one page for this series at least to avoid potential regressions?

I think this change specifically needs more intensive testing (vs the other code paths).

maintainer
#67 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 24 lines · Yosry Ahmed
Quoted 15 lines
Quoted 11 lines
Quoted 8 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

I just take a look at shrink_memcg() and realized it's reclaiming one page at a time. Thanks for the reminder - I hated it.

Quoted 8 lines

Please batchify it if it makes your life easier :) We don't reclaim "just one page/object" anywhere else in the reclaim path, Sure, it adds a bit of latency to zswap_store() if we reached cgroup limit, but IMHO if we hit zswap.max limit at zswap_store() time, that is already the slowest of slow path that you should have avoided with proactive reclaim/zswap shrinker in the first place. And, setting zswap.max does not make sense to me in the first place (I can write a whole essay about it).

Quoted 6 lines

Should we batchify shrink_memcg() from the shrinker and background writeback, but leave the synchronous zswap_store() path to reclaim one page for this series at least to avoid potential regressions?

I think this change specifically needs more intensive testing (vs the other code paths).

I'm fine with having shrink_memcg() takes a batch_size argument for now :)

I suspect not a lot of people invokes the shrink_memcg() synchronous path in zswap store though. Setting zswap.max is hard (as it involves guessing compression ratio ahead of time) and induces quite a bit of overhead (obj_cgroup_may_zswap() does a force flush for every store if you set zswap.max to a value other than 0 and max).

maintainer
#68 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 33 lines · Nhat Pham
Quoted 24 lines · Yosry Ahmed
Quoted 15 lines
Quoted 11 lines
Quoted 8 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

I just take a look at shrink_memcg() and realized it's reclaiming one page at a time. Thanks for the reminder - I hated it.

Quoted 8 lines

Please batchify it if it makes your life easier :) We don't reclaim "just one page/object" anywhere else in the reclaim path, Sure, it adds a bit of latency to zswap_store() if we reached cgroup limit, but IMHO if we hit zswap.max limit at zswap_store() time, that is already the slowest of slow path that you should have avoided with proactive reclaim/zswap shrinker in the first place. And, setting zswap.max does not make sense to me in the first place (I can write a whole essay about it).

Quoted 6 lines

Should we batchify shrink_memcg() from the shrinker and background writeback, but leave the synchronous zswap_store() path to reclaim one page for this series at least to avoid potential regressions?

I think this change specifically needs more intensive testing (vs the other code paths).

Quoted 7 lines

I'm fine with having shrink_memcg() takes a batch_size argument for now :)

I suspect not a lot of people invokes the shrink_memcg() synchronous path in zswap store though. Setting zswap.max is hard (as it involves guessing compression ratio ahead of time) and induces quite a bit of overhead (obj_cgroup_may_zswap() does a force flush for every store if you set zswap.max to a value other than 0 and max).

Yeah I agree, but I just wanna make sure we don't completely kill performance in case anyone is actually doing that.

Regarding the force flush, it's unfortunate that we need to rely on a stat for internal limit checking. It might make sense to use page counters here, which already support hierarchical charging and whatnot. We'll need something similar to the objcg approach to charge sub-pages (or maybe just reuse that somehow?).

maintainer
#69 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 43 lines · Yosry Ahmed
Quoted 33 lines · Nhat Pham
Quoted 24 lines · Yosry Ahmed
Quoted 15 lines
Quoted 11 lines
Quoted 8 lines

Is the main difference that we are scanning in batches here? I think we can have shrink_memcg() do that too. If anything, it might make the shrinker more efficient. Over-reclaim is ofc a concern, and especially in the zswap_store() path as the overhead can be noticeable. Maybe we can parameterize the batch size based on the code path.

Nhat, what do you think?

Nhat, since we now have the referenced-based second chance algorithm, should we consider doing batch writeback for shrink_memcg() as well?

I just take a look at shrink_memcg() and realized it's reclaiming one page at a time. Thanks for the reminder - I hated it.

Quoted 8 lines

Please batchify it if it makes your life easier :) We don't reclaim "just one page/object" anywhere else in the reclaim path, Sure, it adds a bit of latency to zswap_store() if we reached cgroup limit, but IMHO if we hit zswap.max limit at zswap_store() time, that is already the slowest of slow path that you should have avoided with proactive reclaim/zswap shrinker in the first place. And, setting zswap.max does not make sense to me in the first place (I can write a whole essay about it).

Quoted 6 lines

Should we batchify shrink_memcg() from the shrinker and background writeback, but leave the synchronous zswap_store() path to reclaim one page for this series at least to avoid potential regressions?

I think this change specifically needs more intensive testing (vs the other code paths).

Quoted 7 lines

I'm fine with having shrink_memcg() takes a batch_size argument for now :)

I suspect not a lot of people invokes the shrink_memcg() synchronous path in zswap store though. Setting zswap.max is hard (as it involves guessing compression ratio ahead of time) and induces quite a bit of overhead (obj_cgroup_may_zswap() does a force flush for every store if you set zswap.max to a value other than 0 and max).

Quoted 8 lines

Yeah I agree, but I just wanna make sure we don't completely kill performance in case anyone is actually doing that.

Regarding the force flush, it's unfortunate that we need to rely on a stat for internal limit checking. It might make sense to use page counters here, which already support hierarchical charging and whatnot. We'll need something similar to the objcg approach to charge sub-pages (or maybe just reuse that somehow?).

Conceptually, I've moved towards not capping zswap.max, and using it only as a binary knob for enablement/disablement :)

It's consuming the same resources it saves, so if we cap memory.max (and swap.max until we have vswap), then we already have isolation. The split between uncompressed/compressed should then be dynamically determined by workload characteristics (working set size, access pattern, compressibility) and service's SLO, and reclaimers (preferably proactive reclaimers) will drive us towards the optimal split/equilibrium.

maintainer
#70 lore raw

Re: [PATCH v3 2/4] mm/zswap: Implement proactive writeback

Quoted 18 lines
Quoted 6 lines
Quoted 5 lines

I suspect not a lot of people invokes the shrink_memcg() synchronous path in zswap store though. Setting zswap.max is hard (as it involves guessing compression ratio ahead of time) and induces quite a bit of overhead (obj_cgroup_may_zswap() does a force flush for every store if you set zswap.max to a value other than 0 and max).

Quoted 8 lines

Yeah I agree, but I just wanna make sure we don't completely kill performance in case anyone is actually doing that.

Regarding the force flush, it's unfortunate that we need to rely on a stat for internal limit checking. It might make sense to use page counters here, which already support hierarchical charging and whatnot. We'll need something similar to the objcg approach to charge sub-pages (or maybe just reuse that somehow?).

Conceptually, I've moved towards not capping zswap.max, and using it only as a binary knob for enablement/disablement :)

Quoted 7 lines

It's consuming the same resources it saves, so if we cap memory.max (and swap.max until we have vswap), then we already have isolation. The split between uncompressed/compressed should then be dynamically determined by workload characteristics (working set size, access pattern, compressibility) and service's SLO, and reclaimers (preferably proactive reclaimers) will drive us towards the optimal split/equilibrium.

Yeah I guess we can wait until someone actually has a use case for a non-binary zswap.max and complains about performance.

author
#71 lore raw

PATCH v3 3/4 mm/zswap: Add per-memcg stat for proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Currently, zswap writeback can be triggered by either the pool limit being hit or by the proactive writeback mechanism. However, the existing 'zswpwb' metric in memory.stat and /proc/vmstat counts all written back pages, making it difficult to distinguish between pages written back due to the pool limit and those written back proactively.

Add a new statistic 'zswpwb_proactive' to memory.stat and /proc/vmstat. This counter tracks the number of pages written back due to proactive writeback. This allows users to better monitor and tune the proactive writeback mechanism.

---
 Documentation/admin-guide/cgroup-v2.rst |  4 +++
 include/linux/vm_event_item.h           |  1 +
 mm/memcontrol.c                         |  1 +
 mm/vmstat.c                             |  1 +
 mm/zswap.c                              | 41 ++++++++++++++++++-------
 5 files changed, 37 insertions(+), 11 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +4 -0
index 6564abf0dec5..7d65aef83f7b 100644
@@ -1748,6 +1748,10 @@ The following nested keys are defined.
 	  zswpwb
 		Number of pages written from zswap to swap.
 
+	  zswpwb_proactive
+		Number of pages written from zswap to swap by proactive
+		writeback. This is a subset of zswpwb.
+
 	  zswap_incomp
 		Number of incompressible pages currently stored in zswap
 		without compression. These pages could not be compressed to
include/linux/vm_event_item.h +1 -0
index 03fe95f5a020..7a5bee0a20b6 100644
@@ -138,6 +138,7 @@ enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT,
 		ZSWPIN,
 		ZSWPOUT,
 		ZSWPWB,
+		ZSWPWB_PROACTIVE,
 #endif
 #ifdef CONFIG_X86
 		DIRECT_MAP_LEVEL2_SPLIT,
mm/memcontrol.c +1 -0
index e205e5de193d..7648b3fd940e 100644
@@ -571,6 +571,7 @@ static const unsigned int memcg_vm_event_stat[] = {
 	ZSWPIN,
 	ZSWPOUT,
 	ZSWPWB,
+	ZSWPWB_PROACTIVE,
 #endif
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
 	THP_FAULT_ALLOC,
mm/vmstat.c +1 -0
index f534972f517d..66fd06d1bb01 100644
@@ -1452,6 +1452,7 @@ const char * const vmstat_text[] = {
 	[I(ZSWPIN)]				= "zswpin",
 	[I(ZSWPOUT)]				= "zswpout",
 	[I(ZSWPWB)]				= "zswpwb",
+	[I(ZSWPWB_PROACTIVE)]			= "zswpwb_proactive",
 #endif
 #ifdef CONFIG_X86
 	[I(DIRECT_MAP_LEVEL2_SPLIT)]		= "direct_map_level2_splits",
mm/zswap.c +30 -11

108 lines not on this page · raw message

Signature · 1 line
2.34.1
maintainer Reviewed-by on v3 3/4
#72 lore raw

Re: [PATCH v3 3/4] mm/zswap: Add per-memcg stat for proactive writeback

Quoted 28 lines · Hao Jia

From: Hao Jia <[email protected]>

Currently, zswap writeback can be triggered by either the pool limit being hit or by the proactive writeback mechanism. However, the existing 'zswpwb' metric in memory.stat and /proc/vmstat counts all written back pages, making it difficult to distinguish between pages written back due to the pool limit and those written back proactively.

Add a new statistic 'zswpwb_proactive' to memory.stat and /proc/vmstat. This counter tracks the number of pages written back due to proactive writeback. This allows users to better monitor and tune the proactive writeback mechanism.

---
 Documentation/admin-guide/cgroup-v2.rst |  4 +++
 include/linux/vm_event_item.h           |  1 +
 mm/memcontrol.c                         |  1 +
 mm/vmstat.c                             |  1 +
 mm/zswap.c                              | 41 ++++++++++++++++++-------
 5 files changed, 37 insertions(+), 11 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +0 -0
index 6564abf0dec5..7d65aef83f7b 100644

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1754

Quoted 7 lines
Documentation/admin-guide/cgroup-v2.rst +4 -0
@@ -1748,6 +1748,10 @@ The following nested keys are defined.
          zswpwb
                Number of pages written from zswap to swap.
 
+         zswpwb_proactive
+               Number of pages written from zswap to swap by proactive
+               writeback. This is a subset of zswpwb.
+

nit: I think this is specifically the zswap_writeback_only mode right?

Technically, normal proactive reclaim (memory.reclaim) can also hit zswap :)

Maybe some clarification here?

Quoted 66 lines
Documentation/admin-guide/cgroup-v2.rst +0 -0
          zswap_incomp
                Number of incompressible pages currently stored in zswap
                without compression. These pages could not be compressed to
include/linux/vm_event_item.h +1 -0
index 03fe95f5a020..7a5bee0a20b6 100644
@@ -138,6 +138,7 @@ enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT,
                ZSWPIN,
                ZSWPOUT,
                ZSWPWB,
+               ZSWPWB_PROACTIVE,
 #endif
 #ifdef CONFIG_X86
                DIRECT_MAP_LEVEL2_SPLIT,
mm/memcontrol.c +1 -0
index e205e5de193d..7648b3fd940e 100644
@@ -571,6 +571,7 @@ static const unsigned int memcg_vm_event_stat[] = {
        ZSWPIN,
        ZSWPOUT,
        ZSWPWB,
+       ZSWPWB_PROACTIVE,
 #endif
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
        THP_FAULT_ALLOC,
mm/vmstat.c +1 -0
index f534972f517d..66fd06d1bb01 100644
@@ -1452,6 +1452,7 @@ const char * const vmstat_text[] = {
        [I(ZSWPIN)]                             = "zswpin",
        [I(ZSWPOUT)]                            = "zswpout",
        [I(ZSWPWB)]                             = "zswpwb",
+       [I(ZSWPWB_PROACTIVE)]                   = "zswpwb_proactive",
 #endif
 #ifdef CONFIG_X86
        [I(DIRECT_MAP_LEVEL2_SPLIT)]            = "direct_map_level2_splits",
mm/zswap.c +7 -1
index 7bcbf788f634..b45d094f532a 100644
@@ -160,6 +160,11 @@ struct zswap_pool {
        char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
+struct zswap_shrink_walk_arg {
+       bool proactive;
+       bool encountered_page_in_swapcache;
+};
+
 /* Global LRU lists shared by all zswap pools. */
 static struct list_lru zswap_list_lru;
 
@@ -1042,7 +1047,8 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)
  * freed.
  */
 static int zswap_writeback_entry(struct zswap_entry *entry,
-                                swp_entry_t swpentry)
+                                swp_entry_t swpentry,
+                                bool proactive)
 {
        struct xarray *tree;
        pgoff_t offset = swp_offset(swpentry);

mm/zswap.c static int zswap_writeback_entry(struct zswap_entry *entry, · line 1111

Quoted 9 lines
mm/zswap.c +6 -0
@@ -1097,6 +1103,12 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
        if (entry->objcg)
                count_objcg_events(entry->objcg, ZSWPWB, 1);
 
+       if (proactive) {
+               count_vm_event(ZSWPWB_PROACTIVE);
+               if (entry->objcg)
+                       count_objcg_events(entry->objcg, ZSWPWB_PROACTIVE, 1);
+       }
+

With the above clarification, the rest LGTM.

author
#73 lore raw

Re: [PATCH v3 3/4] mm/zswap: Add per-memcg stat for proactive writeback

Quoted 39 lines · Nhat Pham
Quoted 28 lines · Hao Jia

From: Hao Jia <[email protected]>

Currently, zswap writeback can be triggered by either the pool limit being hit or by the proactive writeback mechanism. However, the existing 'zswpwb' metric in memory.stat and /proc/vmstat counts all written back pages, making it difficult to distinguish between pages written back due to the pool limit and those written back proactively.

Add a new statistic 'zswpwb_proactive' to memory.stat and /proc/vmstat. This counter tracks the number of pages written back due to proactive writeback. This allows users to better monitor and tune the proactive writeback mechanism.

---
  Documentation/admin-guide/cgroup-v2.rst |  4 +++
  include/linux/vm_event_item.h           |  1 +
  mm/memcontrol.c                         |  1 +
  mm/vmstat.c                             |  1 +
  mm/zswap.c                              | 41 ++++++++++++++++++-------
  5 files changed, 37 insertions(+), 11 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +0 -0
index 6564abf0dec5..7d65aef83f7b 100644

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1754

Quoted 7 lines
Documentation/admin-guide/cgroup-v2.rst +4 -0
@@ -1748,6 +1748,10 @@ The following nested keys are defined.
           zswpwb
                 Number of pages written from zswap to swap.
 
+         zswpwb_proactive
+               Number of pages written from zswap to swap by proactive
+               writeback. This is a subset of zswpwb.
+

nit: I think this is specifically the zswap_writeback_only mode right?

Quoted 3 lines

Technically, normal proactive reclaim (memory.reclaim) can also hit zswap :)

Maybe some clarification here?

Thanks for the review. I will clarify this further in the next version.

Thanks,
Hao

author
#74 lore raw

PATCH v3 4/4 selftests/cgroup: Add tests for zswap proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Add test_zswap_proactive_writeback() to cover the new memory.reclaim "zswap_writeback_only" key. The test populates a memory cgroup zswap pool, triggers proactive writeback, and verifies the behavior by observing the change in zswpwb_proactive. Invalid input combinations are also covered.

Extend test_zswap_writeback_one() to assert that the existing non-proactive writeback path leaves zswpwb_proactive at zero.

---
 tools/testing/selftests/cgroup/test_zswap.c | 155 +++++++++++++++++++-
 1 file changed, 154 insertions(+), 1 deletion(-)
tools/testing/selftests/cgroup/test_zswap.c +154 -1
index 49b36ee79160..6ab9394a37cc 100644
@@ -60,7 +60,12 @@ static int get_zswap_stored_pages(size_t *value)
 
 static long get_cg_wb_count(const char *cg)
 {
-	return cg_read_key_long(cg, "memory.stat", "zswpwb");
+	return cg_read_key_long(cg, "memory.stat", "zswpwb ");
+}
+
+static long get_cg_pwb_count(const char *cg)
+{
+	return cg_read_key_long(cg, "memory.stat", "zswpwb_proactive ");
 }
 
 static long get_zswpout(const char *cgroup)
@@ -355,6 +360,7 @@ static int attempt_writeback(const char *cgroup, void *arg)
 static int test_zswap_writeback_one(const char *cgroup, bool wb)
 {
 	long zswpwb_before, zswpwb_after;
+	long pwb_cnt;
 
 	zswpwb_before = get_cg_wb_count(cgroup);
 	if (zswpwb_before != 0) {
@@ -362,6 +368,12 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 	}
 
+	pwb_cnt = get_cg_pwb_count(cgroup);
+	if (pwb_cnt != 0) {
+		ksft_print_msg("zswpwb_proactive_before = %ld instead of 0\n", pwb_cnt);
+		return -1;
+	}
+
 	if (cg_run(cgroup, attempt_writeback, (void *) &wb))
 		return -1;
 
@@ -379,6 +391,17 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 	}
 
+	/*
+	 * attempt_writeback() does not use the proactive writeback path, so
+	 * zswpwb_proactive must stay at zero regardless of whether writeback
+	 * was enabled.
+	 */
+	pwb_cnt = get_cg_pwb_count(cgroup);
+	if (pwb_cnt != 0) {
+		ksft_print_msg("zswpwb_proactive_after is %ld, expected 0\n", pwb_cnt);
+		return -1;
+	}
+
 	return 0;
 }
 
@@ -770,6 +793,135 @@ static int test_zswap_incompressible(const char *root)
 	return ret;
 }
 
+/*
+ * Trigger proactive zswap writeback with the following steps:
+ * 1. Allocate memory.
+ * 2. Push allocated memory into zswap.
+ * 3. Proactively write back zswap pages to swap
+ *    using "zswap_writeback_only".
+ */
+static int proactive_writeback_workload(const char *cgroup, void *arg)
+{
+	size_t memsize = page_size * 1024;
+	char reclaim_cmd[64];
+	char buf[page_size];
+	long zswap_usage;
+	int ret = -1;
+	char *mem;
+
+	mem = (char *)malloc(memsize);
+	if (!mem)
+		return ret;
+
+	for (int i = 0; i < page_size; i++)
+		buf[i] = i < page_size / 2 ? (char)i : 0;
+	for (int i = 0; i < memsize; i += page_size)
+		memcpy(&mem[i], buf, page_size);
+
+	/* Evict allocated memory into zswap. */
+	if (cg_write_numeric(cgroup, "memory.reclaim", memsize)) {
+		ksft_print_msg("Failed to push pages into zswap\n");
+		goto out;
+	}
+
+	zswap_usage = cg_read_long(cgroup, "memory.zswap.current");
+	if (zswap_usage <= 0) {
+		ksft_print_msg("no zswap pool to write back\n");
+		goto out;
+	}
+
+	/* Trigger proactive zswap writeback. */
+	snprintf(reclaim_cmd, sizeof(reclaim_cmd), "%zu zswap_writeback_only", memsize);
+	int rc = cg_write(cgroup, "memory.reclaim", reclaim_cmd);
+	if (rc && rc != -EAGAIN) {
+		ksft_print_msg("proactive zswap writeback failed: %d\n", rc);
+		goto out;
+	}
+
+	ret = 0;
+out:
+	free(mem);
+	return ret;
+}
+
+static int check_writeback_invalid_inputs(const char *cgroup)
+{
+	static char * const bad_inputs[] = {
+		"zswap_writeback_only",
+		"1M zswap_writeback_only swappiness=60",
+		"1M swappiness=60 zswap_writeback_only",
+		"1M zswap_writeback_only swappiness=max",
+		"1M swappiness=max zswap_writeback_only",
+	};
+	int i, rc;
+
+	for (i = 0; i < ARRAY_SIZE(bad_inputs); i++) {
+		rc = cg_write(cgroup, "memory.reclaim", bad_inputs[i]);
+		if (rc != -EINVAL) {
+			ksft_print_msg("memory.reclaim '%s': returned %d, expected %d\n",
+				       bad_inputs[i], rc, -EINVAL);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+static int test_zswap_proactive_writeback(const char *root)
+{
+	long pwb_before, wb_before, pwb_after, wb_after;
+	long pwb_delta, wb_delta;
+	int ret = KSFT_FAIL;
+	char *test_group;
+
+	if (cg_read_strcmp(root, "memory.zswap.writeback", "1"))
+		return KSFT_SKIP;
+
+	test_group = cg_name(root, "zswap_proactive_test");
+	if (!test_group)
+		return KSFT_FAIL;
+	if (cg_create(test_group))
+		goto out;
+	if (check_writeback_invalid_inputs(test_group))
+		goto out;
+
+	pwb_before = get_cg_pwb_count(test_group);
+	wb_before = get_cg_wb_count(test_group);
+	if (pwb_before < 0 || wb_before < 0)
+		goto out;
+
+	if (cg_run(test_group, proactive_writeback_workload, NULL))
+		goto out;
+
+	pwb_after = get_cg_pwb_count(test_group);
+	wb_after = get_cg_wb_count(test_group);
+	if (pwb_after < 0 || wb_after < 0)
+		goto out;
+
+	pwb_delta = pwb_after - pwb_before;
+	wb_delta = wb_after - wb_before;
+
+	if (pwb_delta <= 0) {
+		ksft_print_msg("zswpwb_proactive did not increase: delta=%ld\n",
+			       pwb_delta);
+		goto out;
+	}
+	if (wb_delta <= 0) {
+		ksft_print_msg("zswpwb did not increase: delta=%ld\n", wb_delta);
+		goto out;
+	}
+	if (pwb_delta > wb_delta) {
+		ksft_print_msg("zswpwb_proactive delta (%ld) > zswpwb delta (%ld)\n",
+			       pwb_delta, wb_delta);
+		goto out;
+	}
+
+	ret = KSFT_PASS;
+out:
+	cg_destroy(test_group);
+	free(test_group);
+	return ret;
+}
+
 #define T(x) { x, #x }
 struct zswap_test {
 	int (*fn)(const char *root);
@@ -783,6 +935,7 @@ struct zswap_test {
 	T(test_no_kmem_bypass),
 	T(test_no_invasive_cgroup_shrink),
 	T(test_zswap_incompressible),
+	T(test_zswap_proactive_writeback),
 };
 #undef T
 
Signature · 1 line
2.34.1
maintainer Reviewed-by on v3 4/4
#75 lore raw

Re: [PATCH v3 4/4] selftests/cgroup: Add tests for zswap proactive writeback

Quoted 9 lines · Hao Jia

From: Hao Jia <[email protected]>

Add test_zswap_proactive_writeback() to cover the new memory.reclaim "zswap_writeback_only" key. The test populates a memory cgroup zswap pool, triggers proactive writeback, and verifies the behavior by observing the change in zswpwb_proactive. Invalid input combinations are also covered.

Quoted 4 lines

Extend test_zswap_writeback_one() to assert that the existing non-proactive writeback path leaves zswpwb_proactive at zero.

LGTM.