Skip to contentSkip to conversation

mm/zswap: Implement per-cgroup proactive writeback

PATCH Superseded Hao Jia 3 participants 11 messages May 25 – May 30 1 message not in the archive

Subsystems (10)

v2 of 5 Superseded Latest is v5 →

lore
Share & tools
lore

Hao Jia

author
#0 lore raw

v2 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.

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.

[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 | 161 ++++++++++-
 10 files changed, 470 insertions(+), 55 deletions(-)
Signature · 1 line
2.34.1
author
#1 lore raw

PATCH v2 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 dc3fa687759b..b8323c8d6565 100644
@@ -228,6 +228,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 c03d4787d466..409c41359dc8 100644
@@ -4022,6 +4022,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
author
#2 lore raw

PATCH v2 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. The interface returns -EAGAIN if no pages were successfully written back.

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 bd1b1aa12581..6249176b9886 100644
@@ -64,6 +64,7 @@
 
 #include <linux/swapops.h>
 #include <linux/sched/sysctl.h>
+#include <linux/zswap.h>
 
 #include "internal.h"
 #include "swap.h"
@@ -7894,11 +7895,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 },
 };
 
@@ -7908,6 +7911,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;
@@ -7938,11 +7942,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

A message that is not in the archive (sent off-list or not yet received): [email protected]

author
#4 lore raw

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

Quoted 67 lines · On 2026/5/25 21:50, [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

New issues:
- [High] Premature abort of proactive writeback due to flawed failure tracking across the memcg hierarchy.
- [Medium] zswap_proactive_writeback incorrectly returns 0 (success) on partial writebacks, breaking memory.reclaim semantics.

Pre-existing issues:
- [High] Double decrement of nlru->nr_items in list_lru when shrink_memcg_cb successfully writes back a page. --

commit df0f700584d1310ed3a326adfe34b6963c7222ce
Author: Hao Jia <[email protected]>

mm/zswap: Implement proactive writeback

This commit introduces a "zswap_writeback_only" key to the memory.reclaim cgroup interface. It allows users to proactively write back zswap pages to the backing swap up to a specified budget without invoking standard memory reclaim.

Quoted 4 lines
mm/zswap.c +0 -0
index 6519f646b496b..947507b9a1852 100644

[ ... ]

Quoted 5 lines
mm/zswap.c +1 -0
@@ -1684,6 +1684,144 @@ int zswap_load(struct folio *folio)
  	return 0;
  }
 
+/*

[ ... ]

Quoted 3 lines
mm/zswap.c +3 -0
+static long zswap_proactive_shrink_memcg(struct mem_cgroup *memcg,
+					 unsigned long nr_to_write)
+{

[ ... ]

Quoted 8 lines
mm/zswap.c +8 -0
+		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;
+
+			/*

mm/zswap.c int zswap_load(struct folio *folio) · line 1752

Quoted 10 lines
mm/zswap.c +10 -0
+			 * 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);

[Severity: High]
This is a pre-existing issue, but proactive writeback heavily triggers it on demand. When shrink_memcg_cb() initiates a successful writeback, it calls zswap_writeback_entry() which frees the entry. This removes the entry from the list and decrements the LRU item count. Then shrink_memcg_cb() returns LRU_REMOVED_RETRY.

Quoted 4 lines

Does __list_lru_walk_one() fall through to the LRU_REMOVED case and perform a second decrement of nlru->nr_items? If so, will this double decrement corrupt the global node LRU count and cause underflows?

As for this pre-existing issue, nlru->nr_items could indeed be double-decremented. IIRC, we don't have any code reading nlru->nr_items for the zswap LRU; zswap only uses list_lru_count_one() (l->nr_items), so things are working fine for now.

This issue wasn't introduced by this patchset, but it will cause problems if nlru->nr_items is ever used in the future. Therefore, I will try to address it in a separate patch later, or ask for help if needed.

Quoted 33 lines

[ ... ]

Quoted 6 lines
mm/zswap.c +6 -0
+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;

[ ... ]

Quoted 1 line
mm/zswap.c +1 -0
+	while (nr_written < nr_to_writeback) {

[ ... ]

Quoted 4 lines
mm/zswap.c +4 -0
+		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);

mm/zswap.c int zswap_load(struct folio *folio) · line 1817

Quoted 10 lines
mm/zswap.c +10 -0
+
+		/* Writeback-disabled or offline: skip without counting. */
+		if (shrunk == -ENOENT)
+			continue;
+
+		++attempts;
+		if (shrunk > 0)
+			nr_written += shrunk;
+		else if (++failures == MAX_RECLAIM_RETRIES)
+			goto out;

[Severity: High]
If shrunk > 0, shouldn't we reset the failures counter to zero?

Quoted 8 lines

Because zswap_mem_cgroup_iter() repeatedly walks the cgroup subtree, visiting multiple empty memcgs across iterations could cumulatively increment failures to MAX_RECLAIM_RETRIES.

Could this cause the proactive writeback to abort prematurely with -EAGAIN even when it is successfully writing back pages from other memcgs in the hierarchy?

I would rather use a cumulative retry counter (like in shrink_worker() and user_proactive_reclaim()) to prevent the code from repeatedly trying to writeback the memcg under certain conditions.

Quoted 11 lines

mm/zswap.c int zswap_load(struct folio *folio) · line 1822

Quoted 5 lines
mm/zswap.c +5 -0
+
+		cond_resched();
+	}
+out:
+	return nr_written ? 0 : -EAGAIN;

[Severity: Medium]
If the requested target is not fully met but some pages were written back (0 < nr_written < nr_to_writeback), does returning 0 here break the expected semantics of the memory.reclaim interface?

Quoted 5 lines

Should it return -EAGAIN instead of 0 so userspace tools know the full requested amount was not satisfied?

Quoted 1 line

+}

Both this and the remaining selftests issues have been fixed in v3.
https://lore.kernel.org/all/[email protected]

Thanks,
Hao

maintainer
#5 lore raw

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

Quoted 74 lines · Hao Jia
Quoted 66 lines · On 2026/5/25 21:50, [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

New issues:
- [High] Premature abort of proactive writeback due to flawed failure tracking across the memcg hierarchy.
- [Medium] zswap_proactive_writeback incorrectly returns 0 (success) on partial writebacks, breaking memory.reclaim semantics.

Pre-existing issues:
- [High] Double decrement of nlru->nr_items in list_lru when shrink_memcg_cb successfully writes back a page. --

commit df0f700584d1310ed3a326adfe34b6963c7222ce
Author: Hao Jia <[email protected]>

mm/zswap: Implement proactive writeback

This commit introduces a "zswap_writeback_only" key to the memory.reclaim cgroup interface. It allows users to proactively write back zswap pages to the backing swap up to a specified budget without invoking standard memory reclaim.

Quoted 4 lines
mm/zswap.c +0 -0
index 6519f646b496b..947507b9a1852 100644

[ ... ]

Quoted 4 lines
mm/zswap.c +1 -0
@@ -1684,6 +1684,144 @@ int zswap_load(struct folio *folio)
  	return 0;
  }
+/*

[ ... ]

Quoted 3 lines
mm/zswap.c +3 -0
+static long zswap_proactive_shrink_memcg(struct mem_cgroup *memcg,
+					 unsigned long nr_to_write)
+{

[ ... ]

Quoted 8 lines
mm/zswap.c +8 -0
+		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;
+
+			/*

mm/zswap.c int zswap_load(struct folio *folio) · line 1752

Quoted 10 lines
mm/zswap.c +10 -0
+			 * 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);

[Severity: High]
This is a pre-existing issue, but proactive writeback heavily triggers it on demand. When shrink_memcg_cb() initiates a successful writeback, it calls zswap_writeback_entry() which frees the entry. This removes the entry from the list and decrements the LRU item count. Then shrink_memcg_cb() returns LRU_REMOVED_RETRY.

Quoted 4 lines

Does __list_lru_walk_one() fall through to the LRU_REMOVED case and perform a second decrement of nlru->nr_items? If so, will this double decrement corrupt the global node LRU count and cause underflows?

Quoted 4 lines

As for this pre-existing issue, nlru->nr_items could indeed be double-decremented. IIRC, we don't have any code reading nlru->nr_items for the zswap LRU; zswap only uses list_lru_count_one() (l->nr_items), so things are working fine for now.

Yes, but it seems like we're misusing the list_lru infra here. I think we shouldn't end up calling list_lru_del() from the list_lru_walk callback?

Nhat, WDYT?

Quoted 4 lines

This issue wasn't introduced by this patchset, but it will cause problems if nlru->nr_items is ever used in the future. Therefore, I will try to address it in a separate patch later, or ask for help if needed.

author
#6 lore raw

PATCH v2 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 409c41359dc8..67de71b2a659 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
author
#7 lore raw

PATCH v2 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 | 161 +++++++++++++++++++-
 1 file changed, 153 insertions(+), 8 deletions(-)
tools/testing/selftests/cgroup/test_zswap.c +153 -8
index a7bdcdd09d62..b80ed13bc5e2 100644
@@ -57,6 +57,11 @@ static long get_cg_wb_count(const char *cg)
 	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)
 {
 	return cg_read_key_long(cgroup, "memory.stat", "zswpout ");
@@ -323,11 +328,17 @@ 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 wb_cnt, pwb_cnt;
+
+	wb_cnt = get_cg_wb_count(cgroup);
+	if (wb_cnt != 0) {
+		ksft_print_msg("zswpwb_before = %ld instead of 0\n", wb_cnt);
+		return -1;
+	}
 
-	zswpwb_before = get_cg_wb_count(cgroup);
-	if (zswpwb_before != 0) {
-		ksft_print_msg("zswpwb_before = %ld instead of 0\n", zswpwb_before);
+	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;
 	}
 
@@ -335,13 +346,24 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 
 	/* Verify that zswap writeback occurred only if writeback was enabled */
-	zswpwb_after = get_cg_wb_count(cgroup);
-	if (zswpwb_after < 0)
+	wb_cnt = get_cg_wb_count(cgroup);
+	if (wb_cnt < 0)
 		return -1;
 
-	if (wb != !!zswpwb_after) {
+	if (wb != !!wb_cnt) {
 		ksft_print_msg("zswpwb_after is %ld while wb is %s\n",
-				zswpwb_after, wb ? "enabled" : "disabled");
+				wb_cnt, wb ? "enabled" : "disabled");
+		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;
 	}
 
@@ -709,6 +731,128 @@ 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)
+{
+	long pagesize = sysconf(_SC_PAGESIZE);
+	size_t memsize = MB(4);
+	char reclaim_cmd[64];
+	char buf[pagesize];
+	int ret = -1;
+	char *mem;
+
+	mem = (char *)malloc(memsize);
+	if (!mem)
+		return ret;
+
+	for (int i = 0; i < pagesize; i++)
+		buf[i] = i < pagesize / 2 ? (char)i : 0;
+	for (int i = 0; i < memsize; i += pagesize)
+		memcpy(&mem[i], buf, pagesize);
+
+	/* 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;
+	}
+
+	/* Trigger proactive zswap writeback for the same amount. */
+	snprintf(reclaim_cmd, sizeof(reclaim_cmd), "%zu zswap_writeback_only", memsize);
+	if (cg_write(cgroup, "memory.reclaim", reclaim_cmd)) {
+		ksft_print_msg("memory.reclaim zswap_writeback_only failed\n");
+		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);
@@ -722,6 +866,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
#8 lore raw
In reply to Hao Jia
Quoted 12 lines · On Mon, 25 May 2026 20:22:38 +0800 Hao Jia <[email protected]> wrote:

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.

Quoted 6 lines

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.

Thanks. AI review found a few things to complain about, one of them described as "preexisting".

author
#9 lore raw
Quoted 21 lines · Andrew Morton
Quoted 12 lines · On Mon, 25 May 2026 20:22:38 +0800 Hao Jia <[email protected]> wrote:

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.

Quoted 6 lines

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.

Quoted 3 lines

Thanks. AI review found a few things to complain about, one of them described as "preexisting".

Thanks Andrew. I have replied to the AI's review comments in a separate email and posted v3.
https://lore.kernel.org/all/[email protected]

Thanks,
Hao

maintainer
#10 lore raw
Quoted 25 lines · Hao Jia
Quoted 21 lines · Andrew Morton
Quoted 18 lines · On Mon, 25 May 2026 20:22:38 +0800 Hao Jia <[email protected]> wrote:

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.

Quoted 7 lines
Quoted 3 lines

Thanks. AI review found a few things to complain about, one of them described as "preexisting".

Thanks Andrew. I have replied to the AI's review comments in a separate email and posted v3.
https://lore.kernel.org/all/[email protected]

Generally speaking, please give time for reviewers to take a look before sending a new version. Less than a day is usually too fast (unless you're iterating super fast with the reviewers). Review feedback does not have to be addressed immediately, usually wait for a bit to collect as much feedback as possible before spinning a new version.

I will take a look at v3 soon, thank you.

author
#11 lore raw
Quoted 34 lines · Yosry Ahmed
Quoted 25 lines · Hao Jia
Quoted 21 lines · Andrew Morton
Quoted 18 lines · On Mon, 25 May 2026 20:22:38 +0800 Hao Jia <[email protected]> wrote:

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.

Quoted 7 lines
Quoted 3 lines

Thanks. AI review found a few things to complain about, one of them described as "preexisting".

Thanks Andrew. I have replied to the AI's review comments in a separate email and posted v3.
https://lore.kernel.org/all/[email protected]

Quoted 7 lines

Generally speaking, please give time for reviewers to take a look before sending a new version. Less than a day is usually too fast (unless you're iterating super fast with the reviewers). Review feedback does not have to be addressed immediately, usually wait for a bit to collect as much feedback as possible before spinning a new version.

Thanks for the advice, Yosry. Got it.

Quoted 1 line

I will take a look at v3 soon, thank you.

Appreciate you taking a look at v3.

Thakns,
Hao