Skip to contentSkip to conversation

mm/zswap: Implement per-cgroup proactive writeback

PATCH Superseded Hao Jia 4 participants 21 messages May 11 – May 14

Subsystems (6)

v1 of 5 Superseded Latest is v5 →

lore
Share & tools
lore

Hao Jia

author
#0 lore raw

cover mm/zswap: Implement per-cgroup proactive writeback

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. However, this reactive approach makes writeback timing indeterminate and can disrupt latency-sensitive workloads when eviction happens to coincide with a critical execution window.

Furthermore, 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.

To address these issues, this patch series introduces a per-cgroup interface that allows users to proactively write back cold compressed pages from zswap to the backing swap device.

Users can trigger writeback by writing to this interface with the following parameters:

- "max=<bytes>" : Optional. The maximum amount of data to write back. (default: unlimited).
- "<age>" : Required. The minimum age of the pages to write back (in seconds). Only pages that have been in the zswap pool for at least this amount of time will be written back.

Example usage:
# Write back pages older than 1 hour (3600 seconds), max 10MB echo "max=10M 3600" > memory.zswap.proactive_writeback

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: Add the memory.zswap.proactive_writeback cgroupv2 interface,
allowing users to trigger writeback with optional size limit and age threshold.

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

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

 Documentation/admin-guide/cgroup-v2.rst |  28 +++
 include/linux/memcontrol.h              |   6 +
 include/linux/vm_event_item.h           |   1 +
 include/linux/zswap.h                   |  17 ++
 mm/memcontrol.c                         |  80 +++++++
 mm/vmstat.c                             |   1 +
 mm/zswap.c                              | 303 ++++++++++++++++++++----
 7 files changed, 390 insertions(+), 46 deletions(-)

--
2.34.1

author
#1 lore raw

PATCH 1/3 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, about to be introduced by memory.zswap.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 |   6 ++
 include/linux/zswap.h      |   9 +++
 mm/memcontrol.c            |   3 +
 mm/zswap.c                 | 116 +++++++++++++++++++++++++------------
 4 files changed, 98 insertions(+), 36 deletions(-)
include/linux/memcontrol.h +6 -0
index dc3fa687759b..00ae646a3a15 100644
@@ -228,6 +228,12 @@ struct mem_cgroup {
 	 * swap, and from being swapped out on zswap store failures.
 	 */
 	bool zswap_writeback;
+
+	/*
+	 * Per-memcg writeback cursor: root by shrink_worker, non-root by
+	 * proactive writeback.
+	 */
+	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 +80 -36

155 lines not on this page · raw message

Signature · 1 line
2.34.1
author
#2 lore raw

PATCH 2/3 mm/zswap: Implement proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Users can trigger writeback by writing to this file with the following parameters:
- max=<bytes>: The maximum amount of memory to write back (optional,
default: unlimited).
- <age>: The minimum age of the pages to write back. Only pages that have been in zswap for at least this duration will be written back.

Example usage:
# Write back pages older than 1 hour (3600 seconds), max 10MB echo "max=10M 3600" > memory.zswap.proactive_writeback

The implementation consists of:
1. Add store_time to struct zswap_entry to record when each entry was inserted into zswap, used for proactive writeback age comparison.
2. Introduce struct zswap_shrink_walk_arg, passed as the cb_arg to list_lru_walk_one() in both the shrinker and proactive paths. It carries the per-invocation cutoff_time and proactive flag down to shrink_memcg_cb(), and propagates the encountered_page_in_swapcache out-signal from the callback back to the caller.
3. Modify the callback function shrink_memcg_cb() to proactively writeback zswap_entries that meet the time threshold.
4. Add zswap_proactive_writeback() as the proactive writeback driver:
a per-node batched list_lru_walk_one() loop bounded by the writeback budget.

---
 Documentation/admin-guide/cgroup-v2.rst |  24 ++++
 include/linux/zswap.h                   |   8 ++
 mm/memcontrol.c                         |  76 ++++++++++
 mm/zswap.c                              | 176 ++++++++++++++++++++++--
 4 files changed, 276 insertions(+), 8 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +24 -0
index 6efd0095ed99..05b664b3b3e8 100644
@@ -1908,6 +1908,30 @@ The following nested keys are defined.
 	This setting has no effect if zswap is disabled, and swapping
 	is allowed unless memory.swap.max is set to 0.
 
+  memory.zswap.proactive_writeback
+	A write-only nested-keyed file which exists in non-root cgroups.
+
+	This interface allows proactive writeback of pages from the zswap
+	pool to the backing swap device. This is useful to offload cold
+	pages from the zswap pool to the slower swap device. It is only
+	available if zswap writeback is enabled.
+
+	Users can trigger writeback by writing to this file with the following
+	parameters:
+
+	- "max=<bytes>" : Optional. The maximum amount of data to write back.
+	  (default: unlimited). Please note that the kernel can over or under
+	  writeback this value.
+
+	- "<age>" : Required. The minimum age of the pages to write back
+	  (in seconds). Only pages that have been in the zswap pool for at
+	  least this amount of time will be written back.
+
+	Example::
+
+	  # Write back pages older than 1 hour (3600 seconds), max 10MB
+	  echo "max=10M 3600" > memory.zswap.proactive_writeback
+
   memory.pressure
 	A read-only nested-keyed file.
 
include/linux/zswap.h +8 -0
index efa6b551217e..7a51b4f95017 100644
@@ -44,6 +44,8 @@ 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 *root, unsigned long nr_max_writeback,
+			      ktime_t cutoff);
 #else
 
 struct zswap_lruvec_state {};
@@ -78,6 +80,12 @@ static inline bool zswap_never_enabled(void)
 	return true;
 }
 
+static inline int zswap_proactive_writeback(struct mem_cgroup *root,
+					    unsigned long nr_max_writeback, ktime_t cutoff)
+{
+	return 0;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
mm/memcontrol.c +76 -0

94 lines not on this page · raw message

mm/zswap.c +168 -8

247 lines not on this page · raw message

Signature · 1 line
2.34.1
maintainer
#3 lore raw

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

Quoted 13 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

maintainer
#4 lore raw

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

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

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 8 lines
Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

+1, why do we need to specifically proactively reclaim the compressed memory?

Also, if we do need to minimize the compressed memory and force higher writeback rates, we can do so with memory.zswap.max, right?

author
#5 lore raw

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

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

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 4 lines
Quoted 4 lines
Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Thanks Nhat, Yosry — let me address both comments together.

Quoted 5 lines
Quoted 4 lines

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

Apologies for the lack of detailed explanation in the patch description, which led to the confusion.

While we are already utilizing memory.reclaim, it does not fully address our requirements.

Our deployment runs a userspace proactive reclaimer that drives memory.reclaim based on the system's runtime state (memory/CPU/IO pressure, refault rate, ...) and workload-specific policy. That first stage compresses cold anon pages into zswap. Entries that then remain in zswap past a policy-defined age threshold are considered "twice cold", and the reclaimer wants to write them back to the backing swap device at a moment of its own choosing, to further reclaim the DRAM still held by the compressed data.

This is the "second-level offloading" pattern described in Meta's TMO paper [1]. zswap proactive writeback is what this series introduces to address that second-level offloading stage.

[1] https://www.pdl.cmu.edu/ftp/NVM/tmo_asplos22.pdf

Quoted 4 lines

+1, why do we need to specifically proactively reclaim the compressed memory?

Also, if we do need to minimize the compressed memory and force higher writeback rates, we can do so with memory.zswap.max, right?

Here are a few reasons why memory.zswap.max is not enough:

1. Writing memory.zswap.max itself does not trigger any writeback immediately. For a memcg that has reached steady state (on which the userspace reclaimer is no longer invoking memory.reclaim), after enough time has passed, the reclaimer has no good way to trigger proactive writeback for second-level offloading by lowering memory.zswap.max, because in steady state nothing drives the zswap_store() -> shrink_memcg() path. The userspace reclaimer still has no control over when proactive writeback happens.

2. memory.zswap.max currently triggers zswap writeback via zswap_store() -> shrink_memcg(), and each over-limit event can write back at most NR_NODES entries. If zswap residency is far above memory.zswap.max, converging to the target size requires at least O(over-limit pages / NR_NODES) zswap_store() events, with no batching — proactive writeback therefore has significant latency.

3. memory.zswap.max is a stateful interface. If the userspace reclaimer crashes for any reason mid-operation, it may leave memory.zswap.max at some set value, putting the application in a persistently throttled bad state.

4. Once the userspace reclaimer has lowered memory.zswap.max, if the workload is rapidly expanding and triggers memory reclaim via memory.high / kswapd / etc., the actual amount written back can exceed what was intended.

Thanks,
Hao

maintainer
#6 lore raw

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

Quoted 48 lines · Hao Jia
Quoted 16 lines · Yosry Ahmed
Quoted 15 lines · Nhat Pham
Quoted 13 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 4 lines
Quoted 4 lines
Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Thanks Nhat, Yosry — let me address both comments together.

Quoted 5 lines
Quoted 4 lines

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

Apologies for the lack of detailed explanation in the patch description, which led to the confusion.

While we are already utilizing memory.reclaim, it does not fully address our requirements.

Our deployment runs a userspace proactive reclaimer that drives memory.reclaim based on the system's runtime state (memory/CPU/IO pressure, refault rate, ...) and workload-specific
policy. That first stage compresses cold anon pages into zswap. Entries that then remain in zswap past a policy-defined age threshold are considered "twice cold", and the reclaimer wants
to write them back to the backing swap device at a moment of its own choosing, to further reclaim the DRAM still held by the compressed data.

Quoted 5 lines

This is the "second-level offloading" pattern described in Meta's TMO paper [1]. zswap proactive writeback is what this series introduces to address that second-level offloading stage.

[1] https://www.pdl.cmu.edu/ftp/NVM/tmo_asplos22.pdf

Yeah that's what we've been trying to work on as well :) We are working on a couple of improvements to the mechanism side of this path (cc Alex) - hopefully it will help your use case too!

Anyway, back to my original inquiry: I understand your use case. It's pretty similar to our goal. What I'm not getting is why is memory.reclaim (which you already use) not sufficient for zswap -> disk swap offloading too?

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Furthermore, we already have a way to detect the "twice cold" entries you mentioned: the referenced bit. This is analogous to the way we treat uncompressed pages.

Quoted 32 lines
Quoted 6 lines

+1, why do we need to specifically proactively reclaim the compressed memory?

Also, if we do need to minimize the compressed memory and force higher writeback rates, we can do so with memory.zswap.max, right?

Here are a few reasons why memory.zswap.max is not enough:

1. Writing memory.zswap.max itself does not trigger any writeback immediately. For a memcg that has reached steady state (on which the userspace reclaimer is no longer invoking
memory.reclaim), after enough time has passed, the reclaimer has no good way to trigger proactive writeback for second-level offloading by lowering memory.zswap.max, because in steady
state nothing drives the zswap_store() -> shrink_memcg() path. The userspace reclaimer still has no control over when proactive writeback happens.

2. memory.zswap.max currently triggers zswap writeback via zswap_store() -> shrink_memcg(), and each over-limit event can write back at most NR_NODES entries. If zswap residency is far
above memory.zswap.max, converging to the target size requires at least O(over-limit pages / NR_NODES) zswap_store() events, with no batching — proactive writeback therefore has
significant latency.

3. memory.zswap.max is a stateful interface. If the userspace reclaimer crashes for any reason mid-operation, it may leave memory.zswap.max at some set value, putting the application in a
persistently throttled bad state.

Quoted 4 lines

4. Once the userspace reclaimer has lowered memory.zswap.max, if the workload is rapidly expanding and triggers memory reclaim via memory.high / kswapd / etc., the actual amount written back can exceed what was intended.

One more reason: IIRC, when you set memory.zswap.max to a value other than 0 max, every zswap store incurs a pretty expensive check (obj_cgroup_may_zswap), which does a force flush (__mem_cgroup_flush_stats). That was pretty expensive last time some of our internal services played with it. So yeah, it's not ideal...

(if you're using this, might wanna profile this as well).

Quoted 3 lines

Thanks,
Hao

author
#7 lore raw

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

Quoted 64 lines · Nhat Pham
Quoted 48 lines · Hao Jia
Quoted 16 lines · Yosry Ahmed
Quoted 15 lines · Nhat Pham
Quoted 13 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 4 lines
Quoted 4 lines
Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Thanks Nhat, Yosry — let me address both comments together.

Quoted 5 lines
Quoted 4 lines

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

Apologies for the lack of detailed explanation in the patch description, which led to the confusion.

While we are already utilizing memory.reclaim, it does not fully address our requirements.

Our deployment runs a userspace proactive reclaimer that drives memory.reclaim based on the system's runtime state (memory/CPU/IO pressure, refault rate, ...) and workload-specific
policy. That first stage compresses cold anon pages into zswap. Entries that then remain in zswap past a policy-defined age threshold are considered "twice cold", and the reclaimer wants
to write them back to the backing swap device at a moment of its own choosing, to further reclaim the DRAM still held by the compressed data.

Quoted 5 lines

This is the "second-level offloading" pattern described in Meta's TMO paper [1]. zswap proactive writeback is what this series introduces to address that second-level offloading stage.

[1] https://www.pdl.cmu.edu/ftp/NVM/tmo_asplos22.pdf

Yeah that's what we've been trying to work on as well :) We are working on a couple of improvements to the mechanism side of this path (cc Alex) - hopefully it will help your use case too!

Anyway, back to my original inquiry: I understand your use case. It's pretty similar to our goal. What I'm not getting is why is memory.reclaim (which you already use) not sufficient for zswap -> disk swap offloading too?

Quoted 5 lines

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Yes, the memory.reclaim path does drive some zswap writeback, but it is not enough for our case.

1. For a memcg that has reached steady state (a common case being when memory.current is below the policy target), the userspace reclaimer may not invoke memory.reclaim on it for a long time, and so no second-level offloading happens through memory.reclaim. In this state we want memory.zswap.proactive_writeback to write back entries that have sat in zswap past an age threshold, to further reclaim the DRAM still held by the compressed data.

2. Even when memory.reclaim is running, the fraction of zswap residency that ends up reaching the backing swap device is still very small for many of our workloads, and the userspace reclaimer has no way to participate in or control the granularity of zswap writeback. So in our deployment we prefer to leave the zswap shrinker disabled, decouple LRU -> zswap from zswap -> swap, and use a dedicated proactive-writeback interface that lifts the writeback policy into userspace where it can evolve independently of the kernel.

Thanks,
Hao

Quoted 52 lines

Furthermore, we already have a way to detect the "twice cold" entries you mentioned: the referenced bit. This is analogous to the way we treat uncompressed pages.

Quoted 32 lines
Quoted 6 lines

+1, why do we need to specifically proactively reclaim the compressed memory?

Also, if we do need to minimize the compressed memory and force higher writeback rates, we can do so with memory.zswap.max, right?

Here are a few reasons why memory.zswap.max is not enough:

1. Writing memory.zswap.max itself does not trigger any writeback immediately. For a memcg that has reached steady state (on which the userspace reclaimer is no longer invoking
memory.reclaim), after enough time has passed, the reclaimer has no good way to trigger proactive writeback for second-level offloading by lowering memory.zswap.max, because in steady
state nothing drives the zswap_store() -> shrink_memcg() path. The userspace reclaimer still has no control over when proactive writeback happens.

2. memory.zswap.max currently triggers zswap writeback via zswap_store() -> shrink_memcg(), and each over-limit event can write back at most NR_NODES entries. If zswap residency is far
above memory.zswap.max, converging to the target size requires at least O(over-limit pages / NR_NODES) zswap_store() events, with no batching — proactive writeback therefore has
significant latency.

3. memory.zswap.max is a stateful interface. If the userspace reclaimer crashes for any reason mid-operation, it may leave memory.zswap.max at some set value, putting the application in a
persistently throttled bad state.

Quoted 4 lines

4. Once the userspace reclaimer has lowered memory.zswap.max, if the workload is rapidly expanding and triggers memory reclaim via memory.high / kswapd / etc., the actual amount written back can exceed what was intended.

One more reason: IIRC, when you set memory.zswap.max to a value other than 0 max, every zswap store incurs a pretty expensive check (obj_cgroup_may_zswap), which does a force flush (__mem_cgroup_flush_stats). That was pretty expensive last time some of our internal services played with it. So yeah, it's not ideal...

(if you're using this, might wanna profile this as well).

Quoted 3 lines

Thanks,
Hao

maintainer
#8 lore raw

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

Quoted 18 lines
Quoted 5 lines

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Yes, the memory.reclaim path does drive some zswap writeback, but it is not enough for our case.

1. For a memcg that has reached steady state (a common case being when memory.current is below the policy target), the userspace reclaimer may not invoke memory.reclaim on it for a long time, and so no second-level offloading happens through memory.reclaim. In this state we want memory.zswap.proactive_writeback to write back entries that have sat in zswap past an age threshold, to further reclaim the DRAM still held by the compressed data.

Quoted 9 lines

2. Even when memory.reclaim is running, the fraction of zswap residency that ends up reaching the backing swap device is still very small for many of our workloads, and the userspace reclaimer has no way to participate in or control the granularity of zswap writeback. So in our deployment we prefer to leave the zswap shrinker disabled, decouple LRU -> zswap from zswap -> swap, and use a dedicated proactive-writeback interface that lifts the writeback policy into userspace where it can evolve independently of the kernel.

To be honest I see the point of proactively reclaiming compressed memory in zswap. If you use memory.reclaim, you are also reclaiming hotter memory in the process, and you are not necessarily getting as much writeback as you want. The memory in zswap is a more conservative choice for proactive reclaim because it's memory that's guaranteed to be cold(ish) and not being accessed.

That being said, the interface is not great any way you cut it :/

I don't like the 'memory.zswap.proactive_writeback' name, maybe we can stay consistent by doing 'memory.zswap.reclaim', but that just as easily reads as "reclaim using zswap". Maybe 'memory.zswap.do_writeback' or something, idk.

I also don't like having two proactive reclaim interfaces, so a voice in my head wants to tie this into 'memory.reclaim' somehow, but that includes adding a pretty specific argument (e.g. 'memory.reclaim zswap_writeback_only=1'.

I don't like any of these options, and we also need to consider what the memcg maintainers think. I see the use case of proactive writeback but I am struggling to come up with a clean interface.

I also think we should take the 'age' aspect out of the conversation for now, it can be a separate discussion. Well, unless we decide to tie it to memory.reclaim. If memory.reclaim broadly supports age-based reclaim then zswap writeback can be a natural part of that without requiring a specific interface.

maintainer
#9 lore raw

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

Quoted 52 lines · Yosry Ahmed
Quoted 19 lines
Quoted 6 lines

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Yes, the memory.reclaim path does drive some zswap writeback, but it is not enough for our case.

1. For a memcg that has reached steady state (a common case being when memory.current is below the policy target), the userspace reclaimer may not invoke memory.reclaim on it for a long time, and so no second-level offloading happens through memory.reclaim. In this state we want memory.zswap.proactive_writeback to write back entries that have sat in zswap past an age threshold, to further reclaim the DRAM still held by the compressed data.

Quoted 9 lines

2. Even when memory.reclaim is running, the fraction of zswap residency that ends up reaching the backing swap device is still very small for many of our workloads, and the userspace reclaimer has no way to participate in or control the granularity of zswap writeback. So in our deployment we prefer to leave the zswap shrinker disabled, decouple LRU -> zswap from zswap -> swap, and use a dedicated proactive-writeback interface that lifts the writeback policy into userspace where it can evolve independently of the kernel.

To be honest I see the point of proactively reclaiming compressed memory in zswap. If you use memory.reclaim, you are also reclaiming hotter memory in the process, and you are not necessarily getting as much writeback as you want. The memory in zswap is a more conservative choice for proactive reclaim because it's memory that's guaranteed to be cold(ish) and not being accessed.

That being said, the interface is not great any way you cut it :/

I don't like the 'memory.zswap.proactive_writeback' name, maybe we can stay consistent by doing 'memory.zswap.reclaim', but that just as easily reads as "reclaim using zswap". Maybe 'memory.zswap.do_writeback' or something, idk.

I also don't like having two proactive reclaim interfaces, so a voice in my head wants to tie this into 'memory.reclaim' somehow, but that includes adding a pretty specific argument (e.g. 'memory.reclaim zswap_writeback_only=1'.

I don't like any of these options, and we also need to consider what the memcg maintainers think. I see the use case of proactive writeback but I am struggling to come up with a clean interface.

Quoted 5 lines

I also think we should take the 'age' aspect out of the conversation for now, it can be a separate discussion. Well, unless we decide to tie it to memory.reclaim. If memory.reclaim broadly supports age-based reclaim then zswap writeback can be a natural part of that without requiring a specific interface.

Yeah perhaps extending memory.reclaim is best... Sort of analogous to the way we have swappiness to balance file v.s anon....

author
#10 lore raw

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

Quoted 53 lines · Nhat Pham
Quoted 52 lines · Yosry Ahmed
Quoted 19 lines
Quoted 6 lines

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Yes, the memory.reclaim path does drive some zswap writeback, but it is not enough for our case.

1. For a memcg that has reached steady state (a common case being when memory.current is below the policy target), the userspace reclaimer may not invoke memory.reclaim on it for a long time, and so no second-level offloading happens through memory.reclaim. In this state we want memory.zswap.proactive_writeback to write back entries that have sat in zswap past an age threshold, to further reclaim the DRAM still held by the compressed data.

Quoted 9 lines

2. Even when memory.reclaim is running, the fraction of zswap residency that ends up reaching the backing swap device is still very small for many of our workloads, and the userspace reclaimer has no way to participate in or control the granularity of zswap writeback. So in our deployment we prefer to leave the zswap shrinker disabled, decouple LRU -> zswap from zswap -> swap, and use a dedicated proactive-writeback interface that lifts the writeback policy into userspace where it can evolve independently of the kernel.

To be honest I see the point of proactively reclaiming compressed memory in zswap. If you use memory.reclaim, you are also reclaiming hotter memory in the process, and you are not necessarily getting as much writeback as you want. The memory in zswap is a more conservative choice for proactive reclaim because it's memory that's guaranteed to be cold(ish) and not being accessed.

That being said, the interface is not great any way you cut it :/

I don't like the 'memory.zswap.proactive_writeback' name, maybe we can stay consistent by doing 'memory.zswap.reclaim', but that just as easily reads as "reclaim using zswap". Maybe 'memory.zswap.do_writeback' or something, idk.

I also don't like having two proactive reclaim interfaces, so a voice in my head wants to tie this into 'memory.reclaim' somehow, but that includes adding a pretty specific argument (e.g. 'memory.reclaim zswap_writeback_only=1'.

I don't like any of these options, and we also need to consider what the memcg maintainers think. I see the use case of proactive writeback but I am struggling to come up with a clean interface.

Quoted 8 lines
Quoted 5 lines

I also think we should take the 'age' aspect out of the conversation for now, it can be a separate discussion. Well, unless we decide to tie it to memory.reclaim. If memory.reclaim broadly supports age-based reclaim then zswap writeback can be a natural part of that without requiring a specific interface.

Yeah perhaps extending memory.reclaim is best... Sort of analogous to the way we have swappiness to balance file v.s anon....

Thanks for the suggestions, Yosry and Nhat.

My only concern is that if we eventually need to add more parameters to zswap_writeback (such as age or others) in the future, would it make the parameter parsing and the functionality of memory.reclaim overly complex?

As you mentioned, if the memcg maintainers have no objections, I will attempt to implement it in v2.

How about something like this?
echo "100M zswap_writeback_only" > memory.reclaim

Thanks,
Hao

maintainer
#11 lore raw

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

In reply to Hao Jia
Quoted 86 lines · Hao Jia
Quoted 64 lines · Nhat Pham
Quoted 48 lines · Hao Jia
Quoted 16 lines · Yosry Ahmed
Quoted 15 lines · Nhat Pham
Quoted 13 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 4 lines
Quoted 4 lines
Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Thanks Nhat, Yosry — let me address both comments together.

Quoted 5 lines
Quoted 4 lines

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

Apologies for the lack of detailed explanation in the patch description, which led to the confusion.

While we are already utilizing memory.reclaim, it does not fully address our requirements.

Our deployment runs a userspace proactive reclaimer that drives memory.reclaim based on the system's runtime state (memory/CPU/IO pressure, refault rate, ...) and workload-specific
policy. That first stage compresses cold anon pages into zswap. Entries that then remain in zswap past a policy-defined age threshold are considered "twice cold", and the reclaimer wants
to write them back to the backing swap device at a moment of its own choosing, to further reclaim the DRAM still held by the compressed data.

Quoted 5 lines

This is the "second-level offloading" pattern described in Meta's TMO paper [1]. zswap proactive writeback is what this series introduces to address that second-level offloading stage.

[1] https://www.pdl.cmu.edu/ftp/NVM/tmo_asplos22.pdf

Yeah that's what we've been trying to work on as well :) We are working on a couple of improvements to the mechanism side of this path (cc Alex) - hopefully it will help your use case too!

Anyway, back to my original inquiry: I understand your use case. It's pretty similar to our goal. What I'm not getting is why is memory.reclaim (which you already use) not sufficient for zswap -> disk swap offloading too?

Quoted 5 lines

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Yes, the memory.reclaim path does drive some zswap writeback, but it is not enough for our case.

1. For a memcg that has reached steady state (a common case being when memory.current is below the policy target), the userspace reclaimer may not invoke memory.reclaim on it for a long time, and so no second-level offloading happens through memory.reclaim. In this state we want memory.zswap.proactive_writeback to write back entries that have sat in zswap past an age threshold, to further reclaim the DRAM still held by the compressed data.

Quoted 9 lines

2. Even when memory.reclaim is running, the fraction of zswap residency that ends up reaching the backing swap device is still very small for many of our workloads, and the userspace reclaimer has no way to participate in or control the granularity of zswap writeback. So in our deployment we prefer to leave the zswap shrinker disabled, decouple LRU -> zswap from zswap -> swap, and use a dedicated proactive-writeback interface that lifts the writeback policy into userspace where it can evolve independently of the kernel.

I see. It's interesting - we've been dealing with the opposite problems (reclaiming too much from zswap) that it's refreshing to see the other end of the spectrum :) We should invest more into this to see why we are not reclaiming enough, but I see the value of adding a knob to hit zswap exclusively.

Regarding age-based reclaim, I agree with Yosry here. Let us try to land an interface to do targeted reclaim on compressed memory first. I do see the value of age information: with it, you can track zswap entries ages and the distribution of refault ages, and only reclaim the tail. However, I wonder if you can just build a system that adapt the reclaim request size based on PSI, refault rate etc. similar to how you're adjusting memory.reclaim on uncompressed memories with a senpai-like system. Something along the line of - if we are swapping in too much from disk (or if IO pressure is high), back off, and if not, stealing a bit more from zswap pool (perhaps with a bigger step size), etc. Is there a reason why zswap cannot adopt a similar strategy?

author
#12 lore raw

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

Quoted 103 lines · Nhat Pham
Quoted 86 lines · Hao Jia
Quoted 64 lines · Nhat Pham
Quoted 48 lines · Hao Jia
Quoted 16 lines · Yosry Ahmed
Quoted 15 lines · Nhat Pham
Quoted 13 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Quoted 4 lines
Quoted 4 lines
Quoted 3 lines

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Thanks Nhat, Yosry — let me address both comments together.

Quoted 5 lines
Quoted 4 lines

We already have memory.reclaim, no? Would that not work to create headroom generally for your use case? Is there a reason why we are treating zswap memory as special here?

Apologies for the lack of detailed explanation in the patch description, which led to the confusion.

While we are already utilizing memory.reclaim, it does not fully address our requirements.

Our deployment runs a userspace proactive reclaimer that drives memory.reclaim based on the system's runtime state (memory/CPU/IO pressure, refault rate, ...) and workload-specific
policy. That first stage compresses cold anon pages into zswap. Entries that then remain in zswap past a policy-defined age threshold are considered "twice cold", and the reclaimer wants
to write them back to the backing swap device at a moment of its own choosing, to further reclaim the DRAM still held by the compressed data.

Quoted 5 lines

This is the "second-level offloading" pattern described in Meta's TMO paper [1]. zswap proactive writeback is what this series introduces to address that second-level offloading stage.

[1] https://www.pdl.cmu.edu/ftp/NVM/tmo_asplos22.pdf

Yeah that's what we've been trying to work on as well :) We are working on a couple of improvements to the mechanism side of this path (cc Alex) - hopefully it will help your use case too!

Anyway, back to my original inquiry: I understand your use case. It's pretty similar to our goal. What I'm not getting is why is memory.reclaim (which you already use) not sufficient for zswap -> disk swap offloading too?

Quoted 5 lines

Zswap objects are organized into LRU and exposed to the shrinker interface. Echo-ing to memory.reclaim should also offload some zswap entries, correct? Are there still cold zswap entries that escape this, somehow?

Yes, the memory.reclaim path does drive some zswap writeback, but it is not enough for our case.

1. For a memcg that has reached steady state (a common case being when memory.current is below the policy target), the userspace reclaimer may not invoke memory.reclaim on it for a long time, and so no second-level offloading happens through memory.reclaim. In this state we want memory.zswap.proactive_writeback to write back entries that have sat in zswap past an age threshold, to further reclaim the DRAM still held by the compressed data.

Quoted 9 lines

2. Even when memory.reclaim is running, the fraction of zswap residency that ends up reaching the backing swap device is still very small for many of our workloads, and the userspace reclaimer has no way to participate in or control the granularity of zswap writeback. So in our deployment we prefer to leave the zswap shrinker disabled, decouple LRU -> zswap from zswap -> swap, and use a dedicated proactive-writeback interface that lifts the writeback policy into userspace where it can evolve independently of the kernel.

I see. It's interesting - we've been dealing with the opposite problems (reclaiming too much from zswap) that it's refreshing to see the other end of the spectrum :) We should invest more into this to see why we are not reclaiming enough, but I see the value of adding a knob to hit zswap exclusively.

Quoted 12 lines

Regarding age-based reclaim, I agree with Yosry here. Let us try to land an interface to do targeted reclaim on compressed memory first. I do see the value of age information: with it, you can track zswap entries ages and the distribution of refault ages, and only reclaim the tail. However, I wonder if you can just build a system that adapt the reclaim request size based on PSI, refault rate etc. similar to how you're adjusting memory.reclaim on uncompressed memories with a senpai-like system. Something along the line of - if we are swapping in too much from disk (or if IO pressure is high), back off, and if not, stealing a bit more from zswap pool (perhaps with a bigger step size), etc. Is there a reason why zswap cannot adopt a similar strategy?

I'm not sure, as we haven't tested the case of tuning proactive zswap writeback without using age. As you pointed out, age provides a deterministic target that allows the userspace reclaimer to converge faster in a closed-loop, which helps avoid performance jitters.

That said, using age as a zswap writeback parameter indeed warrants further independent discussion. So I'll remove the age-related parts in v2.

Thanks,
Hao

maintainer
#13 lore raw

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

In reply to Hao Jia
Quoted 246 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. This reactive approach offers no precise control over when writeback happens, which can disturb latency-sensitive workloads, and it cannot direct writeback at a specific memory cgroup. However, there are scenarios where users might want to proactively write back cold pages from zswap to the backing swap device, for example, to free up memory for other applications or to prepare for upcoming memory-intensive workloads.

Therefore, implement a proactive writeback mechanism for zswap by adding a new cgroup interface file memory.zswap.proactive_writeback within the memory controller.

Users can trigger writeback by writing to this file with the following parameters:
- max=<bytes>: The maximum amount of memory to write back (optional,
default: unlimited).
- <age>: The minimum age of the pages to write back. Only pages that have been in zswap for at least this duration will be written back.

Example usage:
# Write back pages older than 1 hour (3600 seconds), max 10MB echo "max=10M 3600" > memory.zswap.proactive_writeback

The implementation consists of:
1. Add store_time to struct zswap_entry to record when each entry was inserted into zswap, used for proactive writeback age comparison.
2. Introduce struct zswap_shrink_walk_arg, passed as the cb_arg to list_lru_walk_one() in both the shrinker and proactive paths. It carries the per-invocation cutoff_time and proactive flag down to shrink_memcg_cb(), and propagates the encountered_page_in_swapcache out-signal from the callback back to the caller.
3. Modify the callback function shrink_memcg_cb() to proactively writeback zswap_entries that meet the time threshold.
4. Add zswap_proactive_writeback() as the proactive writeback driver:
a per-node batched list_lru_walk_one() loop bounded by the writeback budget.

---
 Documentation/admin-guide/cgroup-v2.rst |  24 ++++
 include/linux/zswap.h                   |   8 ++
 mm/memcontrol.c                         |  76 ++++++++++
 mm/zswap.c                              | 176 ++++++++++++++++++++++--
 4 files changed, 276 insertions(+), 8 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +24 -0
index 6efd0095ed99..05b664b3b3e8 100644
@@ -1908,6 +1908,30 @@ The following nested keys are defined.
        This setting has no effect if zswap is disabled, and swapping
        is allowed unless memory.swap.max is set to 0.
 
+  memory.zswap.proactive_writeback
+       A write-only nested-keyed file which exists in non-root cgroups.
+
+       This interface allows proactive writeback of pages from the zswap
+       pool to the backing swap device. This is useful to offload cold
+       pages from the zswap pool to the slower swap device. It is only
+       available if zswap writeback is enabled.
+
+       Users can trigger writeback by writing to this file with the following
+       parameters:
+
+       - "max=<bytes>" : Optional. The maximum amount of data to write back.
+         (default: unlimited). Please note that the kernel can over or under
+         writeback this value.
+
+       - "<age>" : Required. The minimum age of the pages to write back
+         (in seconds). Only pages that have been in the zswap pool for at
+         least this amount of time will be written back.
+
+       Example::
+
+         # Write back pages older than 1 hour (3600 seconds), max 10MB
+         echo "max=10M 3600" > memory.zswap.proactive_writeback
+
   memory.pressure
        A read-only nested-keyed file.
 
include/linux/zswap.h +8 -0
index efa6b551217e..7a51b4f95017 100644
@@ -44,6 +44,8 @@ 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 *root, unsigned long nr_max_writeback,
+                             ktime_t cutoff);
 #else
 
 struct zswap_lruvec_state {};
@@ -78,6 +80,12 @@ static inline bool zswap_never_enabled(void)
        return true;
 }
 
+static inline int zswap_proactive_writeback(struct mem_cgroup *root,
+                                           unsigned long nr_max_writeback, ktime_t cutoff)
+{
+       return 0;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
mm/memcontrol.c +76 -0
index 409c41359dc8..ba7f7b1954a8 100644
@@ -70,6 +70,7 @@
 #include "memcontrol-v1.h"
 
 #include <linux/uaccess.h>
+#include <linux/parser.h>
 
 #define CREATE_TRACE_POINTS
 #include <trace/events/memcg.h>
@@ -5891,6 +5892,76 @@ static ssize_t zswap_writeback_write(struct kernfs_open_file *of,
        return nbytes;
 }
 
+enum {
+       ZSWAP_WRITEBACK_MAX,
+       ZSWAP_WRITEBACK_AGE,
+       ZSWAP_WRITEBACK_ERR,
+};
+
+static const match_table_t zswap_writeback_tokens = {
+       { ZSWAP_WRITEBACK_MAX, "max=%s" },
+       { ZSWAP_WRITEBACK_AGE, "%u" },
+       { ZSWAP_WRITEBACK_ERR, NULL },
+};
+
+static ssize_t zswap_proactive_writeback_write(struct kernfs_open_file *of,
+                                              char *buf, size_t nbytes,
+                                              loff_t off)
+{
+       struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
+       unsigned long nr_max_writeback = ULONG_MAX;
+       substring_t args[MAX_OPT_ARGS];
+       unsigned int age_sec;
+       bool age_set = false;
+       ktime_t cutoff_time;
+       char *token, *end;
+       int err;
+
+       if (!mem_cgroup_zswap_writeback_enabled(memcg))
+               return -EINVAL;
+
+       buf = strstrip(buf);
+
+       while ((token = strsep(&buf, " ")) != NULL) {
+               if (!strlen(token))
+                       continue;
+
+               switch (match_token(token, zswap_writeback_tokens, args)) {
+               case ZSWAP_WRITEBACK_MAX:
+                       nr_max_writeback = memparse(args[0].from, &end);
+                       if (*end != '\0')
+                               return -EINVAL;
+                       nr_max_writeback >>= PAGE_SHIFT;
+                       break;
+               case ZSWAP_WRITEBACK_AGE:
+                       if (age_set)
+                               return -EINVAL;
+
+                       if (match_uint(&args[0], &age_sec))
+                               return -EINVAL;
+                       age_set = true;
+                       break;
+               default:
+                       return -EINVAL;
+               }
+       }
+
+       if (!age_set || !age_sec || !nr_max_writeback)
+               return -EINVAL;
+
+       cutoff_time = ktime_sub(ktime_get_boottime(),
+                               ns_to_ktime((u64)age_sec * NSEC_PER_SEC));
+       /* age_sec >= uptime: no entry can be that old, skip the walk. */
+       if (ktime_to_ns(cutoff_time) <= 0)
+               return nbytes;
+
+       err = zswap_proactive_writeback(memcg, nr_max_writeback, cutoff_time);
+       if (err)
+               return err;
+
+       return nbytes;
+}
+
 static struct cftype zswap_files[] = {
        {
                .name = "zswap.current",
@@ -5908,6 +5979,11 @@ static struct cftype zswap_files[] = {
                .seq_show = zswap_writeback_show,
                .write = zswap_writeback_write,
        },
+       {
+               .name = "zswap.proactive_writeback",
+               .flags = CFTYPE_NOT_ON_ROOT,
+               .write = zswap_proactive_writeback_write,
+       },
        { }     /* terminate */
 };
 #endif /* CONFIG_ZSWAP */
mm/zswap.c +8 -0
index 19538d6f169a..1173ac6836fa 100644
@@ -36,6 +36,7 @@
 #include <linux/workqueue.h>
 #include <linux/list_lru.h>
 #include <linux/zsmalloc.h>
+#include <linux/timekeeping.h>
 
 #include "swap.h"
 #include "internal.h"
@@ -160,6 +161,12 @@ struct zswap_pool {
        char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
+struct zswap_shrink_walk_arg {
+       ktime_t cutoff_time;
+       bool proactive;
+       bool encountered_page_in_swapcache;
+};
+
 /* Global LRU lists shared by all zswap pools. */
 static struct list_lru zswap_list_lru;
 
@@ -183,6 +190,7 @@ static struct shrinker *zswap_shrinker;
  * handle - zsmalloc allocation handle that stores the compressed page data
  * objcg - the obj_cgroup that the compressed memory is charged to
  * lru - handle to the pool's lru used to evict pages.
+ * store_time - Time when the entry was stored, for proactive writeback.
  */
 struct zswap_entry {
        swp_entry_t swpentry;

mm/zswap.c struct zswap_entry { · line 203

Quoted 4 lines
mm/zswap.c +1 -0
@@ -192,6 +200,7 @@ struct zswap_entry {
        unsigned long handle;
        struct obj_cgroup *objcg;
        struct list_head lru;
+       ktime_t store_time;

On the implementation side - will this blow up struct zswap_entry memory footprint? If so, can you guard this behind a CONFIG option, if we are to go this route?

author
#14 lore raw

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

Quoted 35 lines · Nhat Pham
Quoted 34 lines · Hao Jia
mm/zswap.c +8 -0
index 19538d6f169a..1173ac6836fa 100644
@@ -36,6 +36,7 @@
  #include <linux/workqueue.h>
  #include <linux/list_lru.h>
  #include <linux/zsmalloc.h>
+#include <linux/timekeeping.h>
 
  #include "swap.h"
  #include "internal.h"
@@ -160,6 +161,12 @@ struct zswap_pool {
         char tfm_name[CRYPTO_MAX_ALG_NAME];
  };
 
+struct zswap_shrink_walk_arg {
+       ktime_t cutoff_time;
+       bool proactive;
+       bool encountered_page_in_swapcache;
+};
+
  /* Global LRU lists shared by all zswap pools. */
  static struct list_lru zswap_list_lru;
 
@@ -183,6 +190,7 @@ static struct shrinker *zswap_shrinker;
   * handle - zsmalloc allocation handle that stores the compressed page data
   * objcg - the obj_cgroup that the compressed memory is charged to
   * lru - handle to the pool's lru used to evict pages.
+ * store_time - Time when the entry was stored, for proactive writeback.
   */
  struct zswap_entry {
         swp_entry_t swpentry;
@@ -192,6 +200,7 @@ struct zswap_entry {
Quoted 8 lines

mm/zswap.c struct zswap_entry { · line 203

Quoted 4 lines
mm/zswap.c +1 -0
         unsigned long handle;
         struct obj_cgroup *objcg;
         struct list_head lru;
+       ktime_t store_time;

On the implementation side - will this blow up struct zswap_entry memory footprint? If so, can you guard this behind a CONFIG option, if we are to go this route?

Thanks for the review. I'll address this in v2.

Thanks,
Hao

author
#15 lore raw

PATCH 3/3 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                              | 11 +++++++++--
 5 files changed, 16 insertions(+), 2 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +4 -0
index 05b664b3b3e8..29a189b18efc 100644
@@ -1734,6 +1734,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 ba7f7b1954a8..830d895e77c3 100644
@@ -572,6 +572,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 +9 -2
index 1173ac6836fa..bf23c46e838e 100644
@@ -1048,7 +1048,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);
@@ -1108,6 +1109,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);
+	}
+
 	zswap_entry_free(entry);
 
 	/* folio is up to date */
@@ -1223,7 +1230,7 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 	 */
 	spin_unlock(&l->lock);
 
-	writeback_result = zswap_writeback_entry(entry, swpentry);
+	writeback_result = zswap_writeback_entry(entry, swpentry, proactive_wb);
 
 	if (writeback_result) {
 		zswap_reject_reclaim_fail++;
Signature · 1 line
2.34.1
maintainer
#16 lore raw

Re: [PATCH 3/3] 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                              | 11 +++++++++--
 5 files changed, 16 insertions(+), 2 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +0 -0
index 05b664b3b3e8..29a189b18efc 100644

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1743

Quoted 10 lines
Documentation/admin-guide/cgroup-v2.rst +4 -0
@@ -1734,6 +1734,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

nit: once we have reached consensus on an interface, can you add documentation for the new knob in cgroup v2 doc and zswap doc too, and how it interacts with the other interface (memory.zswap.writeback, shrinker_enabled sysfs knob).

A kselftest would be very much appreciated too :)

author
#17 lore raw

Re: [PATCH 3/3] mm/zswap: Add per-memcg stat for proactive writeback

Quoted 45 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                              | 11 +++++++++--
  5 files changed, 16 insertions(+), 2 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +0 -0
index 05b664b3b3e8..29a189b18efc 100644

Documentation/admin-guide/cgroup-v2.rst The following nested keys are defined. · line 1743

Quoted 10 lines
Documentation/admin-guide/cgroup-v2.rst +4 -0
@@ -1734,6 +1734,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

nit: once we have reached consensus on an interface, can you add documentation for the new knob in cgroup v2 doc and zswap doc too, and how it interacts with the other interface (memory.zswap.writeback, shrinker_enabled sysfs knob).

Quoted 1 line

A kselftest would be very much appreciated too :)

Thanks, will do in v2

Thanks,
Hao

maintainer
#18 lore raw
In reply to Hao Jia
Quoted 8 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. However, this reactive approach makes writeback timing indeterminate and can disrupt latency-sensitive workloads when eviction happens to coincide with a critical execution window.

Quoted 4 lines

Furthermore, 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.

I can imagine the zswap writeout can come at the least possible moment...

Quoted 3 lines

To address these issues, this patch series introduces a per-cgroup interface that allows users to proactively write back cold compressed pages from zswap to the backing swap device.

...but I see this series is not only per-cgroup proactive reclaim but it's also age-based reclaim.

The per-cg consumption and limits (and regular memory reclaim) are all measured in sizes. This age-based invocations don't seem commensurable (e.g. how would users in practice determine what is the desired input to here).

Could you explain more reasoning behind this design?

Thanks,
Michal

author
#19 lore raw
Quoted 21 lines · Michal Koutný
Quoted 8 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. However, this reactive approach makes writeback timing indeterminate and can disrupt latency-sensitive workloads when eviction happens to coincide with a critical execution window.

Quoted 4 lines

Furthermore, 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.

I can imagine the zswap writeout can come at the least possible moment...

Quoted 3 lines

To address these issues, this patch series introduces a per-cgroup interface that allows users to proactively write back cold compressed pages from zswap to the backing swap device.

Quoted 8 lines

...but I see this series is not only per-cgroup proactive reclaim but it's also age-based reclaim.

The per-cg consumption and limits (and regular memory reclaim) are all measured in sizes. This age-based invocations don't seem commensurable (e.g. how would users in practice determine what is the desired input to here).

Thanks Michal — you are right. The series is both per-memcg *and* age-based.

The interface carries a size budget, like memory.reclaim. The two parameters play different roles:

"write back up to <max> bytes, chosen from entries whose residency
in zswap is at least <age>"

Size stays the unit of *amount*; age is just how we describe *which* entries are eligible.

Quoted 2 lines

Could you explain more reasoning behind this design?

Context on the use case:

Our deployment runs a userspace proactive reclaimer driven by the system's runtime state (memory/CPU/IO pressure, refault rate, ...) and workload-specific policy. It uses memory.reclaim to drive reclaim, which compresses cold anon pages into zswap as the first stage. For entries that then remain in zswap past a policy-defined age threshold, the reclaimer wants to write them back to the backing swap device at a moment of its own choosing, to further reclaim the DRAM still held by the compressed data.

Why age is a reasonable selector at this stage:

Pages in zswap have already passed a first-stage coldness judgement (otherwise they would not have been compressed). For second-level offloading, the question is which of them are cold *enough*. Time-in-zswap is a natural proxy for that. A swap-in invalidates the corresponding zswap entry and resets the clock, so by construction an entry that has sat in zswap for N seconds has not been faulted in for at least N seconds. Residency in zswap is therefore a strong signal that the entry is not about to refault.

In our deployment the userspace reclaimer starts from a conservative threshold (the starting value depends on the workload) and adjusts it through closed-loop feedback:

- on one side, the age distribution of zswap entries, to see

    whether there is a meaningful population past the threshold;

- on the other side, the post-writeback refault rate and related signals, to confirm that entries written back were in fact cold enough.

Both <age> and max=<bytes> are tuned against these signals until the realized writeback volume matches target. This is the same control-loop style already used to drive the first-stage memory.reclaim budget.

Thanks,
Hao

maintainer
#20 lore raw
In reply to Hao Jia
Quoted 8 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap devices reactively, triggered either by memory pressure via the shrinker or by the pool reaching its size limit. However, this reactive approach makes writeback timing indeterminate and can disrupt latency-sensitive workloads when eviction happens to coincide with a critical execution window.

You can make the same argument about ordinary memory reclaim :) That's why we have kswapd (asynchronous reclaim ahead of time) and proactive reclaim solutions (memory.reclaim), which would all target zswap as well.

Quoted 5 lines

Furthermore, 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.

Would memory.reclaim not work here? Why are we treating zswap memory footprint as special here, and spare file and anon?