Skip to contentSkip to conversation

Batched zswap_store() with compression batching

PATCH Superseded Kanchana P. Sridhar 3 participants 46 messages Oct 8 – Oct 20

Subsystems (3)

v1 of 3 Superseded Latest is v3 →

lore
Share & tools
lore
#0 lore raw

cover Batched zswap_store() with compression batching

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================
Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

usemem metrics averaged across 10 iterations*:
================================================

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 Average Throughput (KB/s)                  11,794      17,810    +51.00%
 Total Throughput (KB/s)                    47,179      71,242    +51.00%
 Average Time to Free Memory (usecs)       490,973     294,920    -39.93%
 Total Time to Free Memory (usecs)       1,963,892   1,179,682    -39.93%

--------------------------------------------------------------------------
* vmstats are as reported at the end of the 10th iteration:

 64kB-alloc                                834,567     828,199     -0.76%
 64kB-zswpout                              700,138     743,697      6.22%

64kB-swpout-fallback 35,342 17,978 -49.13%
64kB-swpout 0 0
--------------------------------------------------------------------------

The speedup with zswap store batching of large folios has a propelling effect on other parts of mm, especially, by enabling large folios to be swapped out without splitting, as the data shows. This enables other mm components that are optimized for large folios, such as munmap(), folio refcounts/mapcounts updates, PTE invalidation upon freeing memory, zswap_invalidate(); which benefit from xarray offsets/range continuity. As a result, subsequent large folio swapouts are more likely to find contiguous swap slots, there are fewer large folio splits during reclaim, more mTHP zswpouts and the cycle continues. As the data demonstrates, this directly contributes to more memory over-commit opportunity.

Detailed usemem metrics/cgroup memory utilization/vmstats:
==========================================================

A) MAX zswap pool_total_size vmstats:
=====================================
This is a snapshot at the 10th iteration's peak zswap pool usage:

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 pswpin                                    0                 0
 pswpout                                   0                 0
 thp_swpout                                0                 0
 thp_swpout_fallback                       0                 0
 swpin_zero                              594               548
 swpout_zero                           1,062               864
 zswpin                           10,603,882        11,070,673
 zswpout                          16,594,356        16,509,201     -0.51%
 zswpwb                                    0                 0           
 nrswpin                                   0                 0           
 nrswpout                                  0                 0           
 pgmajfault                        1,460,360         1,470,092      0.67%
 nr_free_pages                       897,249           908,935      1.30%
 nr_free_pages_blocks                892,928           903,680      1.20%
 64kB-alloc                          831,788           825,556     -0.75%
 64kB-zswpout                        690,961           735,863      6.50%
 64kB-swpout                               0                 0           
 64kB-swpout-fallback                 32,982            16,545    -49.84%
 64kB-swpin                                0                 0           
 zswap_pool_total_size         1,420,120,064     1,377,210,368     -3.02%
 memory.current                3,390,189,568     3,299,569,664     -2.67%
 memory.swap.current           3,221,221,376     3,221,221,376

--------------------------------------------------------------------------

B) STEADY STATE vmstats after usemem finishes:
==============================================
This is a snapshot after the 10th iteration finishes:

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 pswpin                                    0                 0 
 pswpout                                   0                 0
 thp_swpout                                0                 0
 thp_swpout_fallback                       0                 0
 swpin_zero                              594               548
 swpout_zero                           1,062               949
 zswpin                           11,526,695        11,963,729
 zswpout                          17,293,120        17,219,911     -0.42%
 zswpwb                                    0                 0           
 nrswpin                                   0                 0           
 nrswpout                                  0                 0           
 pgmajfault                        1,591,298         1,596,095      0.30%
 nr_free_pages                     1,752,789         1,753,264      0.03%
 nr_free_pages_blocks              1,258,496         1,240,064     -1.46%
 64kB-alloc                          834,567           828,199     -0.76%
 64kB-zswpout                        700,138           743,697      6.22%
 64kB-swpout                               0                 0           
 64kB-swpout-fallback                 35,342            17,978    -49.13%
 64kB-swpin                                0                 0           
 zswap_pool_total_size                     0                 0           

memory.current 757,760 802,816 5.95%
memory.swap.current 0 0
--------------------------------------------------------------------------
zswap debugfs counters:

  pool_limit_hit                           0                 0
  written_back_pages                       0                 0
  reject_alloc_fail                        0                 0
  reject_kmemcache_fail                    0                 0
  reject_reclaim_fail                      0                 0
  reject_compress_fail                     0                 0
  reject_compress_poor                     0                 0
  decompress_fail                          0                 0
  stored_incompressible_pages              0                 0
  stored_pages                             0                 0

--------------------------------------------------------------------------

[1]: https://patchwork.kernel.org/project/linux-mm/cover/[email protected]/
[2]: https://wanos.co/assets/silesia.tar

Changes since [1]:
==================
1) Better cacheline placement of struct zswap_pool and struct zswap_entry members for temporal memory access efficiency in zswap store/load hot paths, while minimizing holes.

2) Refactored the core compression batching functionality to a separate __zswap_compress() function, which is called by zswap_compress(), as per Nhat's and Yosry's comments. Hopefully this helps explain the outer "store batching" zswap_compress() vis-a-vis the inner "compress batching" __zswap_compress() distinction.

3) The acomp_ctx is locked once and is passed to the routine that compresses a batch, per the algorithm's batch-size. Similarly, the folio's node id and writeback_enabled status are computed once and passed to the routines that implement the different store functionalities, to save latency by avoid localized recomputes of information that doesn't change.

4) Moved the folio node id being stored in zswap_entry to a separate patch, per Yosry's comment.

5) Addressed the kunmap_local() issue flagged by Jixin He in the IS_ERR_VALUE(handle) error path
(https://patchwork.kernel.org/comment/27095925) - thanks!

6) Moved comments inline, per Yosry's suggestions.

Thanks,
Kanchana

Kanchana P. Sridhar (8):
crypto: acomp - Define a unit_size in struct acomp_req to enable
batching. mm: zswap: Set the unit size for zswap to PAGE_SIZE.
crypto: acomp - Add bit to indicate segmentation support.
crypto: acomp - Add trivial segmentation wrapper.
crypto: acomp - Add API to get a compression algorithm's batch-size.
mm: zswap: Store the folio's node id in the zswap_entry.
mm: zswap: Store large folios in batches.
mm: zswap: Optimally place members of struct zswap_pool, struct
zswap_entry.
mm: zswap: Batched zswap_compress() for storing large folios in
batches.

 crypto/acompress.c                  |  47 ++-
 include/crypto/acompress.h          |  62 +++
 include/crypto/algapi.h             |   5 +
 include/crypto/internal/acompress.h |   8 +
 include/linux/crypto.h              |   3 +
 mm/zswap.c                          | 571 ++++++++++++++++++++--------
 6 files changed, 528 insertions(+), 168 deletions(-)
Signature · 1 line
2.39.5
#1 lore raw

PATCH 1/8 crypto: acomp - Define a unit_size in struct acomp_req to enable batching. mm: zswap: Set the unit size for zswap to PAGE_SIZE.

We add a new @unit_size data member to struct acomp_req along with a helper function acomp_request_set_unit_size() for kernel modules to set the unit size to use while breaking down the request's src/dst scatterlists.

An acomp_alg can implement batching by using the @req->unit_size to break down the SG lists passed in via @req->dst and/or @req->src, to submit individual @req->slen/@req->unit_size compress jobs or @req->dlen/@req->unit_size decompress jobs, for batch compression and batch decompression respectively.

In case of batch compression, the folio's pages for the batch can be retrieved from the @req->src scatterlist by using a struct sg_page_iter after determining the number of pages as @req->slen/@req->unit_size.

Please note that acomp_request_set_callback() sets the @req->unit_size to 0.

In zswap_cpu_comp_prepare(), after the call to acomp_request_set_callback(), we set the unit size for zswap to PAGE_SIZE.

      acomp_request_set_unit_size(acomp_ctx->req, PAGE_SIZE);
---
 include/crypto/acompress.h | 49 ++++++++++++++++++++++++++++++++++++++
 mm/zswap.c                 |  6 +++++
 2 files changed, 55 insertions(+)
include/crypto/acompress.h +49 -0
index 5d5358dfab73..6462fc46f4ec 100644
@@ -79,6 +79,7 @@ struct acomp_req_chain {
  * @dvirt:	Destination virtual address
  * @slen:	Size of the input buffer
  * @dlen:	Size of the output buffer and number of bytes produced
+ * @unit_size:	Unit size for the request for use in batching
  * @chain:	Private API code data, do not use
  * @__ctx:	Start of private context data
  */
@@ -94,6 +95,7 @@ struct acomp_req {
 	};
 	unsigned int slen;
 	unsigned int dlen;
+	unsigned int unit_size;
 
 	struct acomp_req_chain chain;
 
@@ -329,9 +331,56 @@ static inline void acomp_request_set_callback(struct acomp_req *req,
 {
 	flgs &= ~CRYPTO_ACOMP_REQ_PRIVATE;
 	flgs |= req->base.flags & CRYPTO_ACOMP_REQ_PRIVATE;
+	req->unit_size = 0;
 	crypto_request_set_callback(&req->base, flgs, cmpl, data);
 }
 
+/**
+ * acomp_request_set_unit_size() -- Sets the unit size for the request.
+ *
+ * This is a helper function that enables batching for zswap, IPComp, etc.
+ * It allows multiple independent compression (or decompression) operations to
+ * be submitted in a single request's SG lists, where each SG list ("segment")
+ * is processed independently. The unit size helps derive segments from a
+ * single request. crypto_acomp does not expect the segments to be related in
+ * any way.
+ *
+ * Example usage model:
+ *
+ * A module such as zswap that's configured to use a batching compressor, can
+ * accomplish batch compression of "nr_pages" with crypto_acomp by creating an
+ * output SG table for the batch, initialized to contain "nr_pages" SG
+ * lists. Each scatterlist is mapped to the nth destination buffer for the
+ * batch. Depending on whether the @req is used for batch compress/decompress,
+ * zswap must set the @req's source/destination length to be
+ * "nr_pages * @req->unit_size" respectively.
+ *
+ * An acomp_alg can implement batch compression by using the @req->unit_size
+ * to break down the SG lists passed in via @req->dst to submit individual
+ * "@req->slen/@req->unit_size" compress jobs to be processed as a batch.
+ *
+ * Similarly, zswap can implement batch decompression by passing an
+ * SG table with "nr_pages" SG lists via @req->src to process
+ * "@req->dlen/@req->unit_size" decompress jobs as a batch.
+ *
+ * This API must be called after acomp_request_set_callback(),
+ * which sets @req->unit_size to 0. This makes it easy for users of
+ * crypto_acomp to rely on a default of not opting in to batching.
+ * Users such as zswap opt in to batching by defining @req->unit_size
+ * to a non-zero value for use by acomp_algs supporting batching.
+ *
+ * @du would be PAGE_SIZE for zswap, it could be the MTU for IPsec.
+ *
+ * @req:	asynchronous compress/decompress request
+ * @du:		data unit size of the input/output buffer scatterlist.
+ */
+static inline void acomp_request_set_unit_size(struct acomp_req *req,
+					       unsigned int du)
+{
+	req->unit_size = du;
+}
+
+
 /**
  * acomp_request_set_params() -- Sets request parameters
  *
mm/zswap.c +6 -0
index ae19e301fced..ca513a9f89e3 100644
@@ -845,6 +845,12 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 	acomp_request_set_callback(acomp_ctx->req, CRYPTO_TFM_REQ_MAY_BACKLOG,
 				   crypto_req_done, &acomp_ctx->wait);
 
+	/*
+	 * This should be called after acomp_request_set_callback(),
+	 * which initializes req->unit_size to 0.
+	 */
+	acomp_request_set_unit_size(acomp_ctx->req, PAGE_SIZE);
+
 	mutex_init(&acomp_ctx->mutex);
 	return 0;
 
Signature · 1 line
2.39.5
#2 lore raw

PATCH 2/8 crypto: acomp - Add bit to indicate segmentation support.

In reply to Kanchana P. Sridhar

This patch adds segmentation support for compression.

Add a bit to the crypto_alg flags to indicate support for segmentation. Also add a helper for acomp to test whether a given tfm supports segmentation.

---
 include/crypto/algapi.h             | 5 +++++
 include/crypto/internal/acompress.h | 5 +++++
 include/linux/crypto.h              | 3 +++
 3 files changed, 13 insertions(+)
include/crypto/algapi.h +5 -0
index 05deea9dac5e..7d406cfe5751 100644
@@ -280,6 +280,11 @@ static inline bool crypto_tfm_req_virt(struct crypto_tfm *tfm)
 	return tfm->__crt_alg->cra_flags & CRYPTO_ALG_REQ_VIRT;
 }
 
+static inline bool crypto_tfm_req_seg(struct crypto_tfm *tfm)
+{
+	return tfm->__crt_alg->cra_flags & CRYPTO_ALG_REQ_SEG;
+}
+
 static inline u32 crypto_request_flags(struct crypto_async_request *req)
 {
 	return req->flags & ~CRYPTO_TFM_REQ_ON_STACK;
include/crypto/internal/acompress.h +5 -0
index 9cd37df32dc4..93ae59623f13 100644
@@ -189,6 +189,11 @@ static inline bool crypto_acomp_req_virt(struct crypto_acomp *tfm)
 	return crypto_tfm_req_virt(&tfm->base);
 }
 
+static inline bool crypto_acomp_req_seg(struct crypto_acomp *tfm)
+{
+	return crypto_tfm_req_seg(&tfm->base);
+}
+
 void crypto_acomp_free_streams(struct crypto_acomp_streams *s);
 int crypto_acomp_alloc_streams(struct crypto_acomp_streams *s);
 
include/linux/crypto.h +3 -0
index b7c97f1c47c9..78c699ac3f82 100644
@@ -139,6 +139,9 @@
 /* Set if the algorithm cannot have a fallback (e.g., phmac). */
 #define CRYPTO_ALG_NO_FALLBACK		0x00080000
 
+/* Set if the algorithm supports segmentation. */
+#define CRYPTO_ALG_REQ_SEG		0x00100000
+
 /* The high bits 0xff000000 are reserved for type-specific flags. */
 
 /*
Signature · 1 line
2.39.5
#3 lore raw

PATCH 3/8 crypto: acomp - Add trivial segmentation wrapper.

In reply to Kanchana P. Sridhar

This patch provides a wrapper for existing algorithms so that they can accept a single segment while returning the compressed length or error through the dst SG list length.

This trivial segmentation wrapper only supports compression with a segment count of exactly one.

The reason is that the first user zswap will only allocate the extra memory if the underlying algorithm supports segmentation, and otherwise only one segment will be given at a time.

Having this wrapper means that the same calling convention can be used for all algorithms, regardless of segmentation support.

---
 crypto/acompress.c         | 33 ++++++++++++++++++++++++++-------
 include/crypto/acompress.h |  1 +
 2 files changed, 27 insertions(+), 7 deletions(-)
crypto/acompress.c +26 -7
index 4de1a2ad577f..8c9a76073159 100644
@@ -166,14 +166,20 @@ static void acomp_save_req(struct acomp_req *req, crypto_completion_t cplt)
 
 	state->compl = req->base.complete;
 	state->data = req->base.data;
+	state->unit_size = req->unit_size;
+	state->flags = req->base.flags & (CRYPTO_ACOMP_REQ_SRC_VIRT |
+					  CRYPTO_ACOMP_REQ_DST_VIRT);
+
 	req->base.complete = cplt;
 	req->base.data = req;
+	req->unit_size = 0;
 }
 
 static void acomp_restore_req(struct acomp_req *req)
 {
 	req->base.complete = req->chain.compl;
 	req->base.data = req->chain.data;
+	req->unit_size = req->chain.unit_size;
 }
 
 static void acomp_reqchain_virt(struct acomp_req *req)
@@ -192,9 +198,6 @@ static void acomp_virt_to_sg(struct acomp_req *req)
 {
 	struct acomp_req_chain *state = &req->chain;
 
-	state->flags = req->base.flags & (CRYPTO_ACOMP_REQ_SRC_VIRT |
-					  CRYPTO_ACOMP_REQ_DST_VIRT);
-
 	if (acomp_request_src_isvirt(req)) {
 		unsigned int slen = req->slen;
 		const u8 *svirt = req->svirt;
@@ -242,6 +245,10 @@ static int acomp_reqchain_finish(struct acomp_req *req, int err)
 {
 	acomp_reqchain_virt(req);
 	acomp_restore_req(req);
+
+	if (req->unit_size)
+		req->dst->length = unlikely(err) ? err : req->dlen;
+
 	return err;
 }
 
@@ -266,10 +273,13 @@ static int acomp_do_req_chain(struct acomp_req *req, bool comp)
 {
 	int err;
 
+	if (unlikely(req->unit_size && req->slen > req->unit_size))
+		return -ENOSYS;
+
 	acomp_save_req(req, acomp_reqchain_done);
 
 	err = acomp_do_one_req(req, comp);
-	if (err == -EBUSY || err == -EINPROGRESS)
+	if (unlikely(err == -EBUSY || err == -EINPROGRESS))
 		return err;
 
 	return acomp_reqchain_finish(req, err);
@@ -279,10 +289,19 @@ int crypto_acomp_compress(struct acomp_req *req)
 {
 	struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);
 
-	if (acomp_req_on_stack(req) && acomp_is_async(tfm))
+	if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm)))
 		return -EAGAIN;
+
+	if (likely(req->unit_size && acomp_request_issg(req))) {
+		if (!crypto_acomp_req_seg(tfm))
+			return acomp_do_req_chain(req, true);
+
+		return tfm->compress(req);
+	}
+
 	if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req))
-		return crypto_acomp_reqtfm(req)->compress(req);
+		return tfm->compress(req);
+
 	return acomp_do_req_chain(req, true);
 }
 EXPORT_SYMBOL_GPL(crypto_acomp_compress);
@@ -294,7 +313,7 @@ int crypto_acomp_decompress(struct acomp_req *req)
 	if (acomp_req_on_stack(req) && acomp_is_async(tfm))
 		return -EAGAIN;
 	if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req))
-		return crypto_acomp_reqtfm(req)->decompress(req);
+		return tfm->decompress(req);
 	return acomp_do_req_chain(req, false);
 }
 EXPORT_SYMBOL_GPL(crypto_acomp_decompress);
include/crypto/acompress.h +1 -0
index 6462fc46f4ec..fc0665b77a8f 100644
@@ -67,6 +67,7 @@ struct acomp_req_chain {
 		struct folio *dfolio;
 	};
 	u32 flags;
+	u32 unit_size;
 };
 
 /**
Signature · 1 line
2.39.5
#4 lore raw

PATCH 4/8 crypto: acomp - Add API to get a compression algorithm's batch-size.

In reply to Kanchana P. Sridhar

This commit adds a @batch_size data member to struct acomp_alg.

An acomp_alg compression algorithm that supports batching of compressions and decompressions must provide a @batch_size greater than 1, representing the maximum batch-size that the compressor supports, so that kernel users of crypto_acomp, such as zswap, can allocate resources for submitting multiple compress/decompress jobs that can be batched, and invoke batching of [de]compressions.

The new crypto_acomp_batch_size() API queries the crypto_acomp's acomp_alg for the batch-size. If the acomp_alg has registered a @batch_size greater than 1, this is returned. If not, a default of "1" is returned.

zswap can invoke crypto_acomp_batch_size() to query the maximum number of requests that can be batch [de]compressed. Based on this, zswap can use the minimum of any zswap-specific upper limits for batch-size and the compressor's max @batch_size, to allocate batching resources.

This enables zswap to compress/decompress multiple pages as a batch to improve swapout/swapin performance and memory savings.

---
 crypto/acompress.c                  | 14 ++++++++++++++
 include/crypto/acompress.h          | 12 ++++++++++++
 include/crypto/internal/acompress.h |  3 +++
 3 files changed, 29 insertions(+)
crypto/acompress.c +14 -0
index 8c9a76073159..192ea996e779 100644
@@ -318,6 +318,20 @@ int crypto_acomp_decompress(struct acomp_req *req)
 }
 EXPORT_SYMBOL_GPL(crypto_acomp_decompress);
 
+unsigned int crypto_acomp_batch_size(struct crypto_acomp *tfm)
+{
+	if (acomp_is_async(tfm) &&
+		(crypto_comp_alg_common(tfm)->base.cra_flags & CRYPTO_ALG_TYPE_ACOMPRESS)) {
+		struct acomp_alg *alg = crypto_acomp_alg(tfm);
+
+		if (alg && alg->batch_size > 1)
+			return alg->batch_size;
+	}
+
+	return 1;
+}
+EXPORT_SYMBOL_GPL(crypto_acomp_batch_size);
+
 void comp_prepare_alg(struct comp_alg_common *alg)
 {
 	struct crypto_alg *base = &alg->base;
include/crypto/acompress.h +12 -0
index fc0665b77a8f..06578c4367f6 100644
@@ -593,6 +593,18 @@ int crypto_acomp_compress(struct acomp_req *req);
  */
 int crypto_acomp_decompress(struct acomp_req *req);
 
+/**
+ * crypto_acomp_batch_size() -- Get the algorithm's batch size
+ *
+ * Function returns the algorithm's batch size for batching operations
+ *
+ * @tfm:	ACOMPRESS tfm handle allocated with crypto_alloc_acomp()
+ *
+ * Return:	@tfm's acomp_alg's @batch_size, if it has defined a
+ *		@batch_size greater than 1; else return 1.
+ */
+unsigned int crypto_acomp_batch_size(struct crypto_acomp *tfm);
+
 static inline struct acomp_req *acomp_request_on_stack_init(
 	char *buf, struct crypto_acomp *tfm)
 {
include/crypto/internal/acompress.h +3 -0
index 93ae59623f13..b790722c263a 100644
@@ -28,6 +28,8 @@
  *
  * @compress:	Function performs a compress operation
  * @decompress:	Function performs a de-compress operation
+ * @batch_size:	Maximum batch-size for batching compress/decompress
+ *		operations.
  * @init:	Initialize the cryptographic transformation object.
  *		This function is used to initialize the cryptographic
  *		transformation object. This function is called only once at
@@ -47,6 +49,7 @@
 struct acomp_alg {
 	int (*compress)(struct acomp_req *req);
 	int (*decompress)(struct acomp_req *req);
+	unsigned int batch_size;
 	int (*init)(struct crypto_acomp *tfm);
 	void (*exit)(struct crypto_acomp *tfm);
 
Signature · 1 line
2.39.5
#5 lore raw

PATCH 5/8 mm: zswap: Store the folio's node id in the zswap_entry.

In reply to Kanchana P. Sridhar

When storing a folio in zswap, save the folio's node id in the zswap entry instead of relying on the zswap_entry being allocated on the same node. This enables optimizations such as batch allocation of zswap entries for large folios using kmem_cache_alloc_bulk() that does not support specifying a node id.

The size of the zswap_entry remains unchanged as 'referenced' is lumped in with the 'length' (as it doesn't need a full unsigned int anyway).

---
 mm/zswap.c | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)
mm/zswap.c +11 -12
index ca513a9f89e3..c2731d3acb2c 100644
@@ -194,6 +194,7 @@ static struct shrinker *zswap_shrinker;
  *              writeback logic. The entry is only reclaimed by the writeback
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
+ * nid - NUMA node id of the page for which this is the zswap entry.
  * pool_idx - id of the zswap_pool that the entry's data is in.
  * handle - zsmalloc allocation handle that stores the compressed page data
  * objcg - the obj_cgroup that the compressed memory is charged to
@@ -201,8 +202,11 @@ static struct shrinker *zswap_shrinker;
  */
 struct zswap_entry {
 	swp_entry_t swpentry;
-	unsigned int length;
-	bool referenced;
+	struct {
+		unsigned int length:31;
+		bool referenced:1;
+	};
+	int nid;
 	u8 pool_idx;
 	unsigned long handle;
 	struct obj_cgroup *objcg;
@@ -672,14 +676,8 @@ static int zswap_enabled_param_set(const char *val,
 * lru functions
 **********************************/
 
-static inline int entry_to_nid(struct zswap_entry *entry)
-{
-	return page_to_nid(virt_to_page(entry));
-}
-
 static void zswap_lru_add(struct zswap_entry *entry)
 {
-	int nid = entry_to_nid(entry);
 	struct mem_cgroup *memcg;
 
 	/*
@@ -696,19 +694,18 @@ static void zswap_lru_add(struct zswap_entry *entry)
 	rcu_read_lock();
 	memcg = obj_cgroup_memcg(entry->objcg);
 	/* will always succeed */
-	list_lru_add(&zswap_list_lru, &entry->lru, nid, memcg);
+	list_lru_add(&zswap_list_lru, &entry->lru, entry->nid, memcg);
 	rcu_read_unlock();
 }
 
 static void zswap_lru_del(struct zswap_entry *entry)
 {
-	int nid = entry_to_nid(entry);
 	struct mem_cgroup *memcg;
 
 	rcu_read_lock();
 	memcg = obj_cgroup_memcg(entry->objcg);
 	/* will always succeed */
-	list_lru_del(&zswap_list_lru, &entry->lru, nid, memcg);
+	list_lru_del(&zswap_list_lru, &entry->lru, entry->nid, memcg);
 	rcu_read_unlock();
 }
 
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
 	swp_entry_t page_swpentry = folio_swap_entry(folio, index);
 	struct zswap_entry *entry, *old;
+	int nid = folio_nid(folio);
 
 	/* allocate entry */
-	entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+	entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);
 	if (!entry) {
 		zswap_reject_kmemcache_fail++;
 		return false;
@@ -1466,6 +1464,7 @@ static bool zswap_store_page(struct folio *folio, long index,
 	 * to an unrelated live pool.
 	 */
 	entry->pool_idx = pool->idx;
+	entry->nid = nid;
 
 	old = xa_store(swap_zswap_tree(page_swpentry),
 		       swp_offset(page_swpentry),
Signature · 1 line
2.39.5
maintainer
#6 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 30 lines · Kanchana P. Sridhar

When storing a folio in zswap, save the folio's node id in the zswap entry instead of relying on the zswap_entry being allocated on the same node. This enables optimizations such as batch allocation of zswap entries for large folios using kmem_cache_alloc_bulk() that does not support specifying a node id.

The size of the zswap_entry remains unchanged as 'referenced' is lumped in with the 'length' (as it doesn't need a full unsigned int anyway).

---
 mm/zswap.c | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)
mm/zswap.c +1 -0
index ca513a9f89e3..c2731d3acb2c 100644
@@ -194,6 +194,7 @@ static struct shrinker *zswap_shrinker;
  *              writeback logic. The entry is only reclaimed by the writeback
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
+ * nid - NUMA node id of the page for which this is the zswap entry.
  * pool_idx - id of the zswap_pool that the entry's data is in.
  * handle - zsmalloc allocation handle that stores the compressed page data
  * objcg - the obj_cgroup that the compressed memory is charged to

mm/zswap.c static struct shrinker *zswap_shrinker; · line 209

Quoted 10 lines
mm/zswap.c +5 -2
@@ -201,8 +202,11 @@ static struct shrinker *zswap_shrinker;
  */
 struct zswap_entry {
 	swp_entry_t swpentry;
-	unsigned int length;
-	bool referenced;
+	struct {
+		unsigned int length:31;
+		bool referenced:1;
+	};
+	int nid;

This will increase the size of the struct now with pool_idx, right?

Before you patch on x86_64:

struct zswap_entry {

	swp_entry_t                swpentry;             /*     0     8 */
	unsigned int               length;               /*     8     4 */
	bool                       referenced;           /*    12     1 */
	u8                         pool_idx;             /*    13     1 */
	/* XXX 2 bytes hole, try to pack */

	unsigned long              handle;               /*    16     8 */
	struct obj_cgroup *        objcg;                /*    24     8 */
	struct list_head           lru;                  /*    32    16 */

	/* size: 48, cachelines: 1, members: 7 */
	/* sum members: 46, holes: 1, sum holes: 2 */
	/* last cacheline: 48 bytes */
};

After:

struct zswap_entry {
	swp_entry_t                swpentry;             /*     0     8 */
	struct {
		unsigned int       length:31;            /*     8: 0  4 */

		/* Bitfield combined with next fields */

		bool               referenced:1;         /*    11: 7  1 */
	};                                               /*     8     4 */
	struct {
		unsigned int               length:31;            /*     0: 0  4 */

		/* Bitfield combined with next fields */

		bool                       referenced:1;         /*     3: 7  1 */

		/* size: 4, cachelines: 1, members: 2 */
		/* last cacheline: 4 bytes */
	};

	int                        nid;                  /*    12     4 */
	u8                         pool_idx;             /*    16     1 */

	/* XXX 7 bytes hole, try to pack */

	unsigned long              handle;               /*    24     8 */
	struct obj_cgroup *        objcg;                /*    32     8 */
	struct list_head           lru;                  /*    40    16 */

	/* size: 56, cachelines: 1, members: 7 */
	/* sum members: 49, holes: 1, sum holes: 7 */
	/* last cacheline: 56 bytes */
};

I think we want to reorder the fields. We can probably also avoid the bitfield if the maximum page size is 64K (is it?)

struct zswap_entry {

	swp_entry_t                swpentry;             /*     0     8 */
	u16                        length;               /*     8     2 */
	u8                         pool_idx;             /*    10     1 */
	bool                       referenced;           /*    11     1 */
	int                        nid;                  /*    12     4 */
	unsigned long              handle;               /*    16     8 */
	struct obj_cgroup *        objcg;                /*    24     8 */
	struct list_head           lru;                  /*    32    16 */
	/* size: 48, cachelines: 1, members: 8 */
	/* last cacheline: 48 bytes */
};
Quoted 41 lines
mm/zswap.c +2 -9
 	u8 pool_idx;
 	unsigned long handle;
 	struct obj_cgroup *objcg;
@@ -672,14 +676,8 @@ static int zswap_enabled_param_set(const char *val,
 * lru functions
 **********************************/
 
-static inline int entry_to_nid(struct zswap_entry *entry)
-{
-	return page_to_nid(virt_to_page(entry));
-}
-
 static void zswap_lru_add(struct zswap_entry *entry)
 {
-	int nid = entry_to_nid(entry);
 	struct mem_cgroup *memcg;
 
 	/*
@@ -696,19 +694,18 @@ static void zswap_lru_add(struct zswap_entry *entry)
 	rcu_read_lock();
 	memcg = obj_cgroup_memcg(entry->objcg);
 	/* will always succeed */
-	list_lru_add(&zswap_list_lru, &entry->lru, nid, memcg);
+	list_lru_add(&zswap_list_lru, &entry->lru, entry->nid, memcg);
 	rcu_read_unlock();
 }
 
 static void zswap_lru_del(struct zswap_entry *entry)
 {
-	int nid = entry_to_nid(entry);
 	struct mem_cgroup *memcg;
 
 	rcu_read_lock();
 	memcg = obj_cgroup_memcg(entry->objcg);
 	/* will always succeed */
-	list_lru_del(&zswap_list_lru, &entry->lru, nid, memcg);
+	list_lru_del(&zswap_list_lru, &entry->lru, entry->nid, memcg);
 	rcu_read_unlock();
 }
 

mm/zswap.c static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 8 lines
mm/zswap.c +2 -1
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
 	swp_entry_t page_swpentry = folio_swap_entry(folio, index);
 	struct zswap_entry *entry, *old;
+	int nid = folio_nid(folio);
 
 	/* allocate entry */
-	entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+	entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Quoted 14 lines
mm/zswap.c +1 -0
 	if (!entry) {
 		zswap_reject_kmemcache_fail++;
 		return false;
@@ -1466,6 +1464,7 @@ static bool zswap_store_page(struct folio *folio, long index,
 	 * to an unrelated live pool.
 	 */
 	entry->pool_idx = pool->idx;
+	entry->nid = nid;
 
 	old = xa_store(swap_zswap_tree(page_swpentry),
 		       swp_offset(page_swpentry),
Signature · 1 line
2.39.5
#7 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 36 lines · Yosry Ahmed
Quoted 34 lines · Kanchana P. Sridhar

When storing a folio in zswap, save the folio's node id in the zswap entry instead of relying on the zswap_entry being allocated on the same node. This enables optimizations such as batch allocation of zswap entries for large folios using kmem_cache_alloc_bulk() that does not support specifying a node id.

The size of the zswap_entry remains unchanged as 'referenced' is lumped in with the 'length' (as it doesn't need a full unsigned int anyway).

---
 mm/zswap.c | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)
mm/zswap.c +1 -1
index ca513a9f89e3..c2731d3acb2c 100644
@@ -194,6 +194,7 @@ static struct shrinker *zswap_shrinker;
  *              writeback logic. The entry is only reclaimed by the writeback
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
+ * nid - NUMA node id of the page for which this is the zswap entry.
  * pool_idx - id of the zswap_pool that the entry's data is in.
  * handle - zsmalloc allocation handle that stores the compressed page data
  * objcg - the obj_cgroup that the compressed memory is charged to
@@ -201,8 +202,11 @@ static struct shrinker *zswap_shrinker;
  */
 struct zswap_entry {
      swp_entry_t swpentry;
-     unsigned int length;
Quoted 8 lines

mm/zswap.c static struct shrinker *zswap_shrinker; · line 209

Quoted 6 lines
mm/zswap.c +5 -1
-     bool referenced;
+     struct {
+             unsigned int length:31;
+             bool referenced:1;
+     };
+     int nid;

This will increase the size of the struct now with pool_idx, right?

I was gathering the struct size with the struct {unsigned int length:31; bool referenced:1} as suggested in v14, for my before/after comparisons:

before:
struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:31; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 11: 7 1 */
}; /* 8 4 */
int nid; /* 12 4 */
u8 pool_idx; /* 16 1 */

/* XXX 7 bytes hole, try to pack */

long unsigned int handle; /* 24 8 */
struct obj_cgroup * objcg; /* 32 8 */
struct list_head lru; /* 40 16 */

/* size: 56, cachelines: 1, members: 7 */ /* sum members: 49, holes: 1, sum holes: 7 */ /* last cacheline: 56 bytes */
};

after:
struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:31; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 11: 7 1 */
}; /* 8 4 */
int nid; /* 12 4 */
long unsigned int handle; /* 16 8 */
struct obj_cgroup * objcg; /* 24 8 */
struct list_head lru; /* 32 16 */
u8 pool_idx; /* 48 1 */

/* size: 56, cachelines: 1, members: 7 */
/* padding: 7 */
/* last cacheline: 56 bytes */
};

And decided to go with the placement in patch 7, since it also separates out members by compress/decompress path accesses. pool_idx is set once, and never changes. Further, moving pool_idx to the end gets rid of holes.

Quoted 56 lines

Before you patch on x86_64:

struct zswap_entry {

        swp_entry_t                swpentry;             /*     0     8 */
        unsigned int               length;               /*     8     4 */
        bool                       referenced;           /*    12     1 */
        u8                         pool_idx;             /*    13     1 */
        /* XXX 2 bytes hole, try to pack */

        unsigned long              handle;               /*    16     8 */
        struct obj_cgroup *        objcg;                /*    24     8 */
        struct list_head           lru;                  /*    32    16 */

        /* size: 48, cachelines: 1, members: 7 */
        /* sum members: 46, holes: 1, sum holes: 2 */
        /* last cacheline: 48 bytes */
};

After:

struct zswap_entry {
        swp_entry_t                swpentry;             /*     0     8 */
        struct {
                unsigned int       length:31;            /*     8: 0  4 */

                /* Bitfield combined with next fields */

                bool               referenced:1;         /*    11: 7  1 */
        };                                               /*     8     4 */
        struct {
                unsigned int               length:31;            /*     0: 0  4 */

                /* Bitfield combined with next fields */

                bool                       referenced:1;         /*     3: 7  1 */

                /* size: 4, cachelines: 1, members: 2 */
                /* last cacheline: 4 bytes */
        };

        int                        nid;                  /*    12     4 */
        u8                         pool_idx;             /*    16     1 */

        /* XXX 7 bytes hole, try to pack */

        unsigned long              handle;               /*    24     8 */
        struct obj_cgroup *        objcg;                /*    32     8 */
        struct list_head           lru;                  /*    40    16 */

        /* size: 56, cachelines: 1, members: 7 */
        /* sum members: 49, holes: 1, sum holes: 7 */
        /* last cacheline: 56 bytes */
};
Quoted 2 lines

I think we want to reorder the fields. We can probably also avoid the bitfield if the maximum page size is 64K (is it?)

It seems on ARM64, the max page size is 64K, however on PPC, it can be 256K. So it appears we need at least 18 bits on currently supported architectures.
If you agree with the pool_idx placement to be moved to the end as proposed in patch 7, I can try and optimize this further, by limiting the "unsigned int length:18", if you think this makes sense.

Quoted 62 lines

struct zswap_entry {

        swp_entry_t                swpentry;             /*     0     8 */
        u16                        length;               /*     8     2 */
        u8                         pool_idx;             /*    10     1 */
        bool                       referenced;           /*    11     1 */
        int                        nid;                  /*    12     4 */
        unsigned long              handle;               /*    16     8 */
        struct obj_cgroup *        objcg;                /*    24     8 */
        struct list_head           lru;                  /*    32    16 */
        /* size: 48, cachelines: 1, members: 8 */
        /* last cacheline: 48 bytes */
};
Quoted 46 lines
mm/zswap.c +3 -9
      u8 pool_idx;
      unsigned long handle;
      struct obj_cgroup *objcg;
@@ -672,14 +676,8 @@ static int zswap_enabled_param_set(const char *val,
 * lru functions
 **********************************/
 
-static inline int entry_to_nid(struct zswap_entry *entry)
-{
-     return page_to_nid(virt_to_page(entry));
-}
-
 static void zswap_lru_add(struct zswap_entry *entry)
 {
-     int nid = entry_to_nid(entry);
      struct mem_cgroup *memcg;
 
      /*
@@ -696,19 +694,18 @@ static void zswap_lru_add(struct zswap_entry *entry)
      rcu_read_lock();
      memcg = obj_cgroup_memcg(entry->objcg);
      /* will always succeed */
-     list_lru_add(&zswap_list_lru, &entry->lru, nid, memcg);
+     list_lru_add(&zswap_list_lru, &entry->lru, entry->nid, memcg);
      rcu_read_unlock();
 }
 
 static void zswap_lru_del(struct zswap_entry *entry)
 {
-     int nid = entry_to_nid(entry);
      struct mem_cgroup *memcg;
 
      rcu_read_lock();
      memcg = obj_cgroup_memcg(entry->objcg);
      /* will always succeed */
-     list_lru_del(&zswap_list_lru, &entry->lru, nid, memcg);
+     list_lru_del(&zswap_list_lru, &entry->lru, entry->nid, memcg);
      rcu_read_unlock();
 }
 
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
Quoted 6 lines

mm/zswap.c static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 3 lines
mm/zswap.c +1 -1
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Quoted 15 lines
Quoted 15 lines
mm/zswap.c +1 -1
      if (!entry) {
              zswap_reject_kmemcache_fail++;
              return false;
@@ -1466,6 +1464,7 @@ static bool zswap_store_page(struct folio *folio, long index,
       * to an unrelated live pool.
       */
      entry->pool_idx = pool->idx;
+     entry->nid = nid;
 
      old = xa_store(swap_zswap_tree(page_swpentry),
                     swp_offset(page_swpentry),
--

2.39.5

maintainer
#8 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 96 lines · Kanchana P. Sridhar
Quoted 36 lines · Yosry Ahmed
Quoted 34 lines · Kanchana P. Sridhar

When storing a folio in zswap, save the folio's node id in the zswap entry instead of relying on the zswap_entry being allocated on the same node. This enables optimizations such as batch allocation of zswap entries for large folios using kmem_cache_alloc_bulk() that does not support specifying a node id.

The size of the zswap_entry remains unchanged as 'referenced' is lumped in with the 'length' (as it doesn't need a full unsigned int anyway).

---
 mm/zswap.c | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)
mm/zswap.c +1 -1
index ca513a9f89e3..c2731d3acb2c 100644
@@ -194,6 +194,7 @@ static struct shrinker *zswap_shrinker;
  *              writeback logic. The entry is only reclaimed by the writeback
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
+ * nid - NUMA node id of the page for which this is the zswap entry.
  * pool_idx - id of the zswap_pool that the entry's data is in.
  * handle - zsmalloc allocation handle that stores the compressed page data
  * objcg - the obj_cgroup that the compressed memory is charged to
@@ -201,8 +202,11 @@ static struct shrinker *zswap_shrinker;
  */
 struct zswap_entry {
      swp_entry_t swpentry;
-     unsigned int length;
Quoted 8 lines

mm/zswap.c static struct shrinker *zswap_shrinker; · line 209

Quoted 6 lines
mm/zswap.c +5 -1
-     bool referenced;
+     struct {
+             unsigned int length:31;
+             bool referenced:1;
+     };
+     int nid;

This will increase the size of the struct now with pool_idx, right?

I was gathering the struct size with the struct {unsigned int length:31; bool referenced:1} as suggested in v14, for my before/after comparisons:

before:
struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:31; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 11: 7 1 */
}; /* 8 4 */
int nid; /* 12 4 */
u8 pool_idx; /* 16 1 */

/* XXX 7 bytes hole, try to pack */

long unsigned int handle; /* 24 8 */
struct obj_cgroup * objcg; /* 32 8 */
struct list_head lru; /* 40 16 */

/* size: 56, cachelines: 1, members: 7 */ /* sum members: 49, holes: 1, sum holes: 7 */ /* last cacheline: 56 bytes */
};

after:
struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:31; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 11: 7 1 */
}; /* 8 4 */
int nid; /* 12 4 */
long unsigned int handle; /* 16 8 */
struct obj_cgroup * objcg; /* 24 8 */
struct list_head lru; /* 32 16 */
u8 pool_idx; /* 48 1 */

/* size: 56, cachelines: 1, members: 7 */
/* padding: 7 */
/* last cacheline: 56 bytes */
};

Quoted 4 lines

And decided to go with the placement in patch 7, since it also separates out members by compress/decompress path accesses. pool_idx is set once, and never changes. Further, moving pool_idx to the end gets rid of holes.

Please look at the layout I pasted (quoted below). Your patch increases the struct size from 48 bytes to 56 bytes, undoing the optimization introduced by pool_idx. We need to pack length, pool_idx, and referenced in a single u32 to keep the size constant.

FWIW, moving pool_idx to the end just trades holes for padding, I am not sure if that's necessarily an improvement.

Quoted 57 lines
Quoted 57 lines

Before you patch on x86_64:

struct zswap_entry {

        swp_entry_t                swpentry;             /*     0     8 */
        unsigned int               length;               /*     8     4 */
        bool                       referenced;           /*    12     1 */
        u8                         pool_idx;             /*    13     1 */
        /* XXX 2 bytes hole, try to pack */

        unsigned long              handle;               /*    16     8 */
        struct obj_cgroup *        objcg;                /*    24     8 */
        struct list_head           lru;                  /*    32    16 */

        /* size: 48, cachelines: 1, members: 7 */
        /* sum members: 46, holes: 1, sum holes: 2 */
        /* last cacheline: 48 bytes */
};

After:

struct zswap_entry {
        swp_entry_t                swpentry;             /*     0     8 */
        struct {
                unsigned int       length:31;            /*     8: 0  4 */

                /* Bitfield combined with next fields */

                bool               referenced:1;         /*    11: 7  1 */
        };                                               /*     8     4 */
        struct {
                unsigned int               length:31;            /*     0: 0  4 */

                /* Bitfield combined with next fields */

                bool                       referenced:1;         /*     3: 7  1 */

                /* size: 4, cachelines: 1, members: 2 */
                /* last cacheline: 4 bytes */
        };

        int                        nid;                  /*    12     4 */
        u8                         pool_idx;             /*    16     1 */

        /* XXX 7 bytes hole, try to pack */

        unsigned long              handle;               /*    24     8 */
        struct obj_cgroup *        objcg;                /*    32     8 */
        struct list_head           lru;                  /*    40    16 */

        /* size: 56, cachelines: 1, members: 7 */
        /* sum members: 49, holes: 1, sum holes: 7 */
        /* last cacheline: 56 bytes */
};
Quoted 6 lines
Quoted 2 lines

I think we want to reorder the fields. We can probably also avoid the bitfield if the maximum page size is 64K (is it?)

It seems on ARM64, the max page size is 64K, however on PPC, it can be 256K. So it appears we need at least 18 bits on currently supported architectures.

In this case unfortunately we need to stick with bitfields. We should probably add a build-time assertion that the width of length is enough to cover PAGE_SIZE.

Quoted 3 lines

If you agree with the pool_idx placement to be moved to the end as proposed in patch 7, I can try and optimize this further, by limiting the "unsigned int length:18", if you think this makes sense.

I don't think we can put pool_idx to the end and keep the struct size constant. For length, yeah let's use the minimum number of bits needed and add an assertion, so that the free to use bits are obvious.

Quoted 70 lines
Quoted 63 lines

struct zswap_entry {

        swp_entry_t                swpentry;             /*     0     8 */
        u16                        length;               /*     8     2 */
        u8                         pool_idx;             /*    10     1 */
        bool                       referenced;           /*    11     1 */
        int                        nid;                  /*    12     4 */
        unsigned long              handle;               /*    16     8 */
        struct obj_cgroup *        objcg;                /*    24     8 */
        struct list_head           lru;                  /*    32    16 */
        /* size: 48, cachelines: 1, members: 8 */
        /* last cacheline: 48 bytes */
};
Quoted 46 lines
mm/zswap.c +3 -9
      u8 pool_idx;
      unsigned long handle;
      struct obj_cgroup *objcg;
@@ -672,14 +676,8 @@ static int zswap_enabled_param_set(const char *val,
 * lru functions
 **********************************/
 
-static inline int entry_to_nid(struct zswap_entry *entry)
-{
-     return page_to_nid(virt_to_page(entry));
-}
-
 static void zswap_lru_add(struct zswap_entry *entry)
 {
-     int nid = entry_to_nid(entry);
      struct mem_cgroup *memcg;
 
      /*
@@ -696,19 +694,18 @@ static void zswap_lru_add(struct zswap_entry *entry)
      rcu_read_lock();
      memcg = obj_cgroup_memcg(entry->objcg);
      /* will always succeed */
-     list_lru_add(&zswap_list_lru, &entry->lru, nid, memcg);
+     list_lru_add(&zswap_list_lru, &entry->lru, entry->nid, memcg);
      rcu_read_unlock();
 }
 
 static void zswap_lru_del(struct zswap_entry *entry)
 {
-     int nid = entry_to_nid(entry);
      struct mem_cgroup *memcg;
 
      rcu_read_lock();
      memcg = obj_cgroup_memcg(entry->objcg);
      /* will always succeed */
-     list_lru_del(&zswap_list_lru, &entry->lru, nid, memcg);
+     list_lru_del(&zswap_list_lru, &entry->lru, entry->nid, memcg);
      rcu_read_unlock();
 }
 
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
Quoted 6 lines

mm/zswap.c static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 3 lines
mm/zswap.c +1 -1
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Quoted 3 lines

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

We allocate the entry on the same node to avoid having to store nid in the zswap entry (see entry_to_nid()). If we store it, then there's no need to allocate on the node.

Quoted 5 lines

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Keep in mind that zsmalloc is movable memory, while slab memory isn't. Zsmalloc may also be less likely to make a new allocation in the store path. There are other considerations beyond memory overhead and latency here.

Quoted 16 lines
Quoted 16 lines
Quoted 16 lines
mm/zswap.c +1 -1
      if (!entry) {
              zswap_reject_kmemcache_fail++;
              return false;
@@ -1466,6 +1464,7 @@ static bool zswap_store_page(struct folio *folio, long index,
       * to an unrelated live pool.
       */
      entry->pool_idx = pool->idx;
+     entry->nid = nid;
 
      old = xa_store(swap_zswap_tree(page_swpentry),
                     swp_offset(page_swpentry),
--

2.39.5

#9 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 104 lines · Yosry Ahmed
Quoted 96 lines · Kanchana P. Sridhar
Quoted 36 lines · Yosry Ahmed
Quoted 34 lines · Kanchana P. Sridhar

When storing a folio in zswap, save the folio's node id in the zswap entry instead of relying on the zswap_entry being allocated on the same node. This enables optimizations such as batch allocation of zswap entries for large folios using kmem_cache_alloc_bulk() that does not support specifying a node id.

The size of the zswap_entry remains unchanged as 'referenced' is lumped in with the 'length' (as it doesn't need a full unsigned int anyway).

---
 mm/zswap.c | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)
mm/zswap.c +1 -1
index ca513a9f89e3..c2731d3acb2c 100644
@@ -194,6 +194,7 @@ static struct shrinker *zswap_shrinker;
  *              writeback logic. The entry is only reclaimed by the writeback
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
+ * nid - NUMA node id of the page for which this is the zswap entry.
  * pool_idx - id of the zswap_pool that the entry's data is in.
  * handle - zsmalloc allocation handle that stores the compressed page data
  * objcg - the obj_cgroup that the compressed memory is charged to
@@ -201,8 +202,11 @@ static struct shrinker *zswap_shrinker;
  */
 struct zswap_entry {
      swp_entry_t swpentry;
-     unsigned int length;
Quoted 8 lines

mm/zswap.c static struct shrinker *zswap_shrinker; · line 209

Quoted 6 lines
mm/zswap.c +5 -1
-     bool referenced;
+     struct {
+             unsigned int length:31;
+             bool referenced:1;
+     };
+     int nid;

This will increase the size of the struct now with pool_idx, right?

I was gathering the struct size with the struct {unsigned int length:31; bool referenced:1} as suggested in v14, for my before/after comparisons:

before:
struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:31; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 11: 7 1 */
}; /* 8 4 */
int nid; /* 12 4 */
u8 pool_idx; /* 16 1 */

/* XXX 7 bytes hole, try to pack */

long unsigned int handle; /* 24 8 */
struct obj_cgroup * objcg; /* 32 8 */
struct list_head lru; /* 40 16 */

/* size: 56, cachelines: 1, members: 7 */ /* sum members: 49, holes: 1, sum holes: 7 */ /* last cacheline: 56 bytes */
};

after:
struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:31; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 11: 7 1 */
}; /* 8 4 */
int nid; /* 12 4 */
long unsigned int handle; /* 16 8 */
struct obj_cgroup * objcg; /* 24 8 */
struct list_head lru; /* 32 16 */
u8 pool_idx; /* 48 1 */

/* size: 56, cachelines: 1, members: 7 */
/* padding: 7 */
/* last cacheline: 56 bytes */
};

Quoted 4 lines

And decided to go with the placement in patch 7, since it also separates out members by compress/decompress path accesses. pool_idx is set once, and never changes. Further, moving pool_idx to the end gets rid of holes.

Quoted 4 lines

Please look at the layout I pasted (quoted below). Your patch increases the struct size from 48 bytes to 56 bytes, undoing the optimization introduced by pool_idx. We need to pack length, pool_idx, and referenced in a single u32 to keep the size constant.

Thanks for the suggestion. I have modified the layout such that the size remains unchanged at 48 bytes. The "u8 pool_idx" requires byte alignment, so I moved the 1-bit "referenced" field to be before this:

#define ZSWAP_MAX_COMPR_LENGTH_SHIFT 18U
struct zswap_entry {
swp_entry_t swpentry;

  struct {
    unsigned int length:ZSWAP_MAX_COMPR_LENGTH_SHIFT;
    bool referenced:1U;
    u8 pool_idx;

};
int nid;
unsigned long handle;
struct obj_cgroup *objcg;
struct list_head lru;
};

struct zswap_entry {
swp_entry_t swpentry; /* 0 8 */
struct {
unsigned int length:18; /* 8: 0 4 */

/* Bitfield combined with next fields */

bool referenced:1; /* 10: 2 1 */

/* XXX 5 bits hole, try to pack */

u8 pool_idx; /* 11 1 */
}; /* 8 4 */
int nid; /* 12 4 */
long unsigned int handle; /* 16 8 */
struct obj_cgroup * objcg; /* 24 8 */
struct list_head lru; /* 32 16 */

/* size: 48, cachelines: 1, members: 6 */
/* last cacheline: 48 bytes */
};

The macro is required to add a build time assertion in zswap_setup():

BUILD_BUG_ON(ZSWAP_MAX_COMPR_LENGTH_SHIFT < PAGE_SHIFT);

Quoted 72 lines

FWIW, moving pool_idx to the end just trades holes for padding, I am not sure if that's necessarily an improvement.

Quoted 57 lines
Quoted 57 lines

Before you patch on x86_64:

struct zswap_entry {

        swp_entry_t                swpentry;             /*     0     8 */
        unsigned int               length;               /*     8     4 */
        bool                       referenced;           /*    12     1 */
        u8                         pool_idx;             /*    13     1 */
        /* XXX 2 bytes hole, try to pack */

        unsigned long              handle;               /*    16     8 */
        struct obj_cgroup *        objcg;                /*    24     8 */
        struct list_head           lru;                  /*    32    16 */

        /* size: 48, cachelines: 1, members: 7 */
        /* sum members: 46, holes: 1, sum holes: 2 */
        /* last cacheline: 48 bytes */
};

After:

struct zswap_entry {
        swp_entry_t                swpentry;             /*     0     8 */
        struct {
                unsigned int       length:31;            /*     8: 0  4 */

                /* Bitfield combined with next fields */

                bool               referenced:1;         /*    11: 7  1 */
        };                                               /*     8     4 */
        struct {
                unsigned int               length:31;            /*     0: 0  4 */

                /* Bitfield combined with next fields */

                bool                       referenced:1;         /*     3: 7  1 */

                /* size: 4, cachelines: 1, members: 2 */
                /* last cacheline: 4 bytes */
        };

        int                        nid;                  /*    12     4 */
        u8                         pool_idx;             /*    16     1 */

        /* XXX 7 bytes hole, try to pack */

        unsigned long              handle;               /*    24     8 */
        struct obj_cgroup *        objcg;                /*    32     8 */
        struct list_head           lru;                  /*    40    16 */

        /* size: 56, cachelines: 1, members: 7 */
        /* sum members: 49, holes: 1, sum holes: 7 */
        /* last cacheline: 56 bytes */
};
Quoted 6 lines
Quoted 2 lines

I think we want to reorder the fields. We can probably also avoid the bitfield if the maximum page size is 64K (is it?)

It seems on ARM64, the max page size is 64K, however on PPC, it can be 256K. So it appears we need at least 18 bits on currently supported architectures.

In this case unfortunately we need to stick with bitfields. We should probably add a build-time assertion that the width of length is enough to cover PAGE_SIZE.

Quoted 7 lines
Quoted 3 lines

If you agree with the pool_idx placement to be moved to the end as proposed in patch 7, I can try and optimize this further, by limiting the "unsigned int length:18", if you think this makes sense.

I don't think we can put pool_idx to the end and keep the struct size constant. For length, yeah let's use the minimum number of bits needed and add an assertion, so that the free to use bits are obvious.

This is all taken care of in my latest version.

Quoted 71 lines
Quoted 71 lines
Quoted 70 lines

struct zswap_entry {

        swp_entry_t                swpentry;             /*     0     8 */
        u16                        length;               /*     8     2 */
        u8                         pool_idx;             /*    10     1 */
        bool                       referenced;           /*    11     1 */
        int                        nid;                  /*    12     4 */
        unsigned long              handle;               /*    16     8 */
        struct obj_cgroup *        objcg;                /*    24     8 */
        struct list_head           lru;                  /*    32    16 */
        /* size: 48, cachelines: 1, members: 8 */
        /* last cacheline: 48 bytes */
};
Quoted 41 lines
mm/zswap.c +2 -9
      u8 pool_idx;
      unsigned long handle;
      struct obj_cgroup *objcg;
@@ -672,14 +676,8 @@ static int zswap_enabled_param_set(const char *val,
 * lru functions
 **********************************/
 
-static inline int entry_to_nid(struct zswap_entry *entry)
-{
-     return page_to_nid(virt_to_page(entry));
-}
-
 static void zswap_lru_add(struct zswap_entry *entry)
 {
-     int nid = entry_to_nid(entry);
      struct mem_cgroup *memcg;
 
      /*
@@ -696,19 +694,18 @@ static void zswap_lru_add(struct zswap_entry *entry)
      rcu_read_lock();
      memcg = obj_cgroup_memcg(entry->objcg);
      /* will always succeed */
-     list_lru_add(&zswap_list_lru, &entry->lru, nid, memcg);
+     list_lru_add(&zswap_list_lru, &entry->lru, entry->nid, memcg);
      rcu_read_unlock();
 }
 
 static void zswap_lru_del(struct zswap_entry *entry)
 {
-     int nid = entry_to_nid(entry);
      struct mem_cgroup *memcg;
 
      rcu_read_lock();
      memcg = obj_cgroup_memcg(entry->objcg);
      /* will always succeed */
-     list_lru_del(&zswap_list_lru, &entry->lru, nid, memcg);
+     list_lru_del(&zswap_list_lru, &entry->lru, entry->nid, memcg);
      rcu_read_unlock();
 }
 

mm/zswap.c static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 8 lines
mm/zswap.c +2 -1
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Quoted 7 lines
Quoted 3 lines

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

We allocate the entry on the same node to avoid having to store nid in the zswap entry (see entry_to_nid()). If we store it, then there's no need to allocate on the node.

With patch 0006, we try to bulk allocate all entries for the batch. Only if this fails, we call zswap_entry_cache_alloc(gfp, nid). So patch 0006 addresses this already, and the nid is used on in the fallback case.

Quoted 11 lines
Quoted 6 lines

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Keep in mind that zsmalloc is movable memory, while slab memory isn't. Zsmalloc may also be less likely to make a new allocation in the store path. There are other considerations beyond memory overhead and latency here.

I realize this means we trade off page migration for writeback for the pages representing zswap compressed data. This might not necessarily be a bad thing for 2 main reasons, the way I see it in this specific context:

1) Page migration would only be transferring a memory pressure problem elsewhere. Not sure how well this would work with memcgs or containers. At some point someone would need to do writeback.
2) I don't believe there is a zswap specific page migration flow in the kernel other than zsmalloc compaction. IOW, memory tiering components like CXL are handled directly in the reclaim path. From what I understand, things wouldn't break with slab memory for zswap compressed storage, but I could be wrong. I hope to include the architecture level considerations/trade-offs in the RFC.

Quoted 17 lines
Quoted 17 lines
Quoted 17 lines
Quoted 17 lines
mm/zswap.c +1 -1
      if (!entry) {
              zswap_reject_kmemcache_fail++;
              return false;
@@ -1466,6 +1464,7 @@ static bool zswap_store_page(struct folio *folio, long index,
       * to an unrelated live pool.
       */
      entry->pool_idx = pool->idx;
+     entry->nid = nid;
 
      old = xa_store(swap_zswap_tree(page_swpentry),
                     swp_offset(page_swpentry),
--

2.39.5

maintainer
#10 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 17 lines
Quoted 17 lines
Quoted 16 lines
Quoted 6 lines
Quoted 6 lines
mm/zswap.c +1 -0
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
Quoted 6 lines

mm/zswap.c static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 3 lines
mm/zswap.c +1 -1
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

Quoted 8 lines
Quoted 3 lines

We allocate the entry on the same node to avoid having to store nid in the zswap entry (see entry_to_nid()). If we store it, then there's no need to allocate on the node.

With patch 0006, we try to bulk allocate all entries for the batch. Only if this fails, we call zswap_entry_cache_alloc(gfp, nid). So patch 0006 addresses this already, and the nid is used on in the fallback case.

If we are storing the nid in the zswap entry, there is no need for zswap_entry_cache_alloc() to use the nid anymore.

Quoted 18 lines
Quoted 8 lines
Quoted 7 lines

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Quoted 4 lines

Keep in mind that zsmalloc is movable memory, while slab memory isn't. Zsmalloc may also be less likely to make a new allocation in the store path. There are other considerations beyond memory overhead and latency here.

I realize this means we trade off page migration for writeback for the pages representing zswap compressed data. This might not necessarily be a bad thing for 2 main reasons, the way I see it in this specific context:

Quoted 9 lines

1) Page migration would only be transferring a memory pressure problem elsewhere. Not sure how well this would work with memcgs or containers. At some point someone would need to do writeback.
2) I don't believe there is a zswap specific page migration flow in the kernel other than zsmalloc compaction. IOW, memory tiering components like CXL are handled directly in the reclaim path. From what I understand, things wouldn't break with slab memory for zswap compressed storage, but I could be wrong. I hope to include the architecture level considerations/trade-offs in the RFC.

Zsmalloc memory is currently movable, so compaction can move it around to free contiguous blocks (e.g. for THP allocations). Slab memory is not movable. It can be reclaimable, but writing back memory from zswap when we are not under memory pressure is also bad.

Note that compressed memory can be very large compared to slab allocations (or kernel allocations in general), so it can increase unmovable (or reclaimable but not movable) memory significantly, causing fragmentation.

#11 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 22 lines · Yosry Ahmed
Quoted 22 lines
Quoted 21 lines
Quoted 14 lines
Quoted 7 lines
Quoted 7 lines
mm/zswap.c +1 -0
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
Quoted 6 lines

mm/zswap.c static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 3 lines
mm/zswap.c +1 -1
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Quoted 3 lines

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

We allocate the entry on the same node to avoid having to store nid in the zswap entry (see entry_to_nid()). If we store it, then there's no need to allocate on the node.

Quoted 7 lines
Quoted 4 lines

With patch 0006, we try to bulk allocate all entries for the batch. Only if this fails, we call zswap_entry_cache_alloc(gfp, nid). So patch 0006 addresses this already, and the nid is used on in the fallback case.

If we are storing the nid in the zswap entry, there is no need for zswap_entry_cache_alloc() to use the nid anymore.

As of now, zswap_entry_cache_alloc() uses the nid for kmem_cache_alloc_node(). Is the suggestion to change this to be kmem_cache_alloc()?

Quoted 29 lines
Quoted 19 lines
Quoted 9 lines
Quoted 8 lines

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Quoted 4 lines

Keep in mind that zsmalloc is movable memory, while slab memory isn't. Zsmalloc may also be less likely to make a new allocation in the store path. There are other considerations beyond memory overhead and latency here.

I realize this means we trade off page migration for writeback for the pages representing zswap compressed data. This might not necessarily be a bad thing for 2 main reasons, the way I see it in this specific context:

Quoted 9 lines

1) Page migration would only be transferring a memory pressure problem elsewhere. Not sure how well this would work with memcgs or containers. At some point someone would need to do writeback.
2) I don't believe there is a zswap specific page migration flow in the kernel other than zsmalloc compaction. IOW, memory tiering components like CXL are handled directly in the reclaim path. From what I understand, things wouldn't break with slab memory for zswap compressed storage, but I could be wrong. I hope to include the architecture level considerations/trade-offs in the RFC.

Quoted 4 lines

Zsmalloc memory is currently movable, so compaction can move it around to free contiguous blocks (e.g. for THP allocations). Slab memory is not movable. It can be reclaimable, but writing back memory from zswap when we are not under memory pressure is also bad.

All good points, thanks. I agree, writing back memory from zswap when not under memory pressure is bad, however I have not seen any writebacks in my experiments with usemem at 60% over-commit and with 64kB mTHP.

I am running these experiments in a memory constrained environment:

QEMU VM: 7G RAM
Swap: 3G
cgroup memory.high: 3G
4 usemem processes want to allocate 1200M each. To make the experiment even more stringent and representative of actual compression datasets, each usemem process reads the Silesia dataset from a pre-fork-allocated buffer into the 1200M. This causes realistic compression payloads in usemem.

Quoted 5 lines

Note that compressed memory can be very large compared to slab allocations (or kernel allocations in general), so it can increase unmovable (or reclaimable but not movable) memory significantly, causing fragmentation.

It took me a while to experiment with compressed size bins, until I settled on a compromise between memory wasted per slab and zswap latency in finding the correct size bin to minimize fragmentation. I have been gathering extensive SLUB stats which indicate:

1) Even under peak zswap compressed quota ("pool") utilization across 10 consecutive iterations, SLUB comes out ahead in less overall memory overhead.

2) At steady state (when 10 iterations of the workload are done), most of the slabs have been returned to the Buddy allocator, except for 3M worth of slab setup per compressed size bin (0 objects in use): this is as compared to 32K of zsmalloc slub metadata that remains at steady state.

I learned some really interesting things from these experiments, which pose questions about the memory penalty due to latency that zsmalloc adds to zswap compress/decompress paths vs. "internal fragmentation". I expect to quantify this with data from my experimental setup.

maintainer
#12 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 29 lines · Kanchana P. Sridhar
Quoted 27 lines · Yosry Ahmed
Quoted 26 lines
Quoted 14 lines
Quoted 14 lines
Quoted 13 lines

Review static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 10 lines
(quoted diff) +2 -1
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Quoted 7 lines
Quoted 3 lines

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

We allocate the entry on the same node to avoid having to store nid in the zswap entry (see entry_to_nid()). If we store it, then there's no need to allocate on the node.

With patch 0006, we try to bulk allocate all entries for the batch. Only if this fails, we call zswap_entry_cache_alloc(gfp, nid). So patch 0006 addresses this already, and the nid is used on in the fallback case.

Quoted 6 lines
Quoted 2 lines

If we are storing the nid in the zswap entry, there is no need for zswap_entry_cache_alloc() to use the nid anymore.

As of now, zswap_entry_cache_alloc() uses the nid for kmem_cache_alloc_node(). Is the suggestion to change this to be kmem_cache_alloc()?

Yes. I don't think we need to use kmem_cache_alloc_node() if we store nid in zswap_entry.

Quoted 35 lines
Quoted 30 lines
Quoted 20 lines
Quoted 10 lines
Quoted 9 lines

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Quoted 4 lines

Keep in mind that zsmalloc is movable memory, while slab memory isn't. Zsmalloc may also be less likely to make a new allocation in the store path. There are other considerations beyond memory overhead and latency here.

I realize this means we trade off page migration for writeback for the pages representing zswap compressed data. This might not necessarily be a bad thing for 2 main reasons, the way I see it in this specific context:

Quoted 9 lines

1) Page migration would only be transferring a memory pressure problem elsewhere. Not sure how well this would work with memcgs or containers. At some point someone would need to do writeback.
2) I don't believe there is a zswap specific page migration flow in the kernel other than zsmalloc compaction. IOW, memory tiering components like CXL are handled directly in the reclaim path. From what I understand, things wouldn't break with slab memory for zswap compressed storage, but I could be wrong. I hope to include the architecture level considerations/trade-offs in the RFC.

Quoted 4 lines

Zsmalloc memory is currently movable, so compaction can move it around to free contiguous blocks (e.g. for THP allocations). Slab memory is not movable. It can be reclaimable, but writing back memory from zswap when we are not under memory pressure is also bad.

Quoted 4 lines

All good points, thanks. I agree, writing back memory from zswap when not under memory pressure is bad, however I have not seen any writebacks in my experiments with usemem at 60% over-commit and with 64kB mTHP.

We can discuss further when you make your posting :)

Quoted 37 lines

I am running these experiments in a memory constrained environment:

QEMU VM: 7G RAM
Swap: 3G
cgroup memory.high: 3G
4 usemem processes want to allocate 1200M each. To make the experiment even more stringent and representative of actual compression datasets, each usemem process reads the Silesia dataset from a pre-fork-allocated buffer into the 1200M. This causes realistic compression payloads in usemem.

Quoted 5 lines

Note that compressed memory can be very large compared to slab allocations (or kernel allocations in general), so it can increase unmovable (or reclaimable but not movable) memory significantly, causing fragmentation.

It took me a while to experiment with compressed size bins, until I settled on a compromise between memory wasted per slab and zswap latency in finding the correct size bin to minimize fragmentation. I have been gathering extensive SLUB stats which indicate:

1) Even under peak zswap compressed quota ("pool") utilization across 10 consecutive iterations, SLUB comes out ahead in less overall memory overhead.

2) At steady state (when 10 iterations of the workload are done), most of the slabs have been returned to the Buddy allocator, except for 3M worth of slab setup per compressed size bin (0 objects in use): this is as compared to 32K of zsmalloc slub metadata that remains at steady state.

I learned some really interesting things from these experiments, which pose questions about the memory penalty due to latency that zsmalloc adds to zswap compress/decompress paths vs. "internal fragmentation". I expect to quantify this with data from my experimental setup.

#13 lore raw

Re: [PATCH v1 5/8] mm: zswap: Store the folio's node id in the zswap_entry.

Quoted 35 lines · Yosry Ahmed
Quoted 32 lines · Kanchana P. Sridhar
Quoted 29 lines · Yosry Ahmed
Quoted 18 lines
Quoted 18 lines
Quoted 17 lines
Quoted 7 lines
Quoted 7 lines
(quoted diff) +1 -0
@@ -1449,9 +1446,10 @@ static bool zswap_store_page(struct folio *folio, long index,
 {
      swp_entry_t page_swpentry = folio_swap_entry(folio, index);
      struct zswap_entry *entry, *old;
+     int nid = folio_nid(folio);
 
Quoted 6 lines

Review static bool zswap_store_page(struct folio *folio, long index, · line 1452

Quoted 3 lines
(quoted diff) +1 -1
      /* allocate entry */
-     entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
+     entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);

We don't need to allocate the entry on the same node anymore, should we drop this and let the slab allocator handle it as it sees fit?

Was this a recent change? I see the folio_nid(folio) on my version of mm-unstable as of 10-3-2026. Let me check, and yes, allowing the slab allocated handle this is probably a good idea.

Quoted 8 lines
Quoted 3 lines

We allocate the entry on the same node to avoid having to store nid in the zswap entry (see entry_to_nid()). If we store it, then there's no need to allocate on the node.

With patch 0006, we try to bulk allocate all entries for the batch. Only if this fails, we call zswap_entry_cache_alloc(gfp, nid). So patch 0006 addresses this already, and the nid is used on in the fallback case.

If we are storing the nid in the zswap entry, there is no need for zswap_entry_cache_alloc() to use the nid anymore.

Quoted 6 lines
Quoted 3 lines

As of now, zswap_entry_cache_alloc() uses the nid for kmem_cache_alloc_node(). Is the suggestion to change this to be kmem_cache_alloc()?

Yes. I don't think we need to use kmem_cache_alloc_node() if we store nid in zswap_entry.

Got it. I will make this change.

Quoted 36 lines
Quoted 36 lines
Quoted 35 lines
Quoted 21 lines
Quoted 11 lines
Quoted 10 lines

Along the same lines, I have been working on replacing zsmalloc with direct SLAB allocations for the zswap compressed data storage and see very promising results: memory savings and latency improvements. I will submit an RFC shortly.

Quoted 4 lines

Keep in mind that zsmalloc is movable memory, while slab memory isn't. Zsmalloc may also be less likely to make a new allocation in the store path. There are other considerations beyond memory overhead and latency here.

I realize this means we trade off page migration for writeback for the pages representing zswap compressed data. This might not necessarily be a bad thing for 2 main reasons, the way I see it in this specific context:

Quoted 9 lines

1) Page migration would only be transferring a memory pressure problem elsewhere. Not sure how well this would work with memcgs or containers. At some point someone would need to do writeback.
2) I don't believe there is a zswap specific page migration flow in the kernel other than zsmalloc compaction. IOW, memory tiering components like CXL are handled directly in the reclaim path. From what I understand, things wouldn't break with slab memory for zswap compressed storage, but I could be wrong. I hope to include the architecture level considerations/trade-offs in the RFC.

Zsmalloc memory is currently movable, so compaction can move it around to free contiguous blocks (e.g. for THP allocations). Slab memory is not movable. It can be reclaimable, but writing back memory from zswap when we are not under memory pressure is also bad.

Quoted 6 lines
Quoted 4 lines

All good points, thanks. I agree, writing back memory from zswap when not under memory pressure is bad, however I have not seen any writebacks in my experiments with usemem at 60% over-commit and with 64kB mTHP.

We can discuss further when you make your posting :)

Sounds great, thanks!

Quoted 38 lines
Quoted 38 lines

I am running these experiments in a memory constrained environment:

QEMU VM: 7G RAM
Swap: 3G
cgroup memory.high: 3G
4 usemem processes want to allocate 1200M each. To make the experiment even more stringent and representative of actual compression datasets, each usemem process reads the Silesia dataset from a pre-fork-allocated buffer into the 1200M. This causes realistic compression payloads in usemem.

Quoted 5 lines

Note that compressed memory can be very large compared to slab allocations (or kernel allocations in general), so it can increase unmovable (or reclaimable but not movable) memory significantly, causing fragmentation.

It took me a while to experiment with compressed size bins, until I settled on a compromise between memory wasted per slab and zswap latency in finding the correct size bin to minimize fragmentation. I have been gathering extensive SLUB stats which indicate:

1) Even under peak zswap compressed quota ("pool") utilization across 10 consecutive iterations, SLUB comes out ahead in less overall memory overhead.

2) At steady state (when 10 iterations of the workload are done), most of the slabs have been returned to the Buddy allocator, except for 3M worth of slab setup per compressed size bin (0 objects in use): this is as compared to 32K of zsmalloc slub metadata that remains at steady state.

I learned some really interesting things from these experiments, which pose questions about the memory penalty due to latency that zsmalloc adds to zswap compress/decompress paths vs. "internal fragmentation". I expect to quantify this with data from my experimental setup.

#14 lore raw

PATCH 6/8 mm: zswap: Store large folios in batches.

In reply to Kanchana P. Sridhar

Use batching when storing large folios in zswap. If the underlying compressor supports batching (e.g. hardware parallel compression), allocate multiple compression buffers, otherwise allocate one. The number of buffers is bounded by a new constant, ZSWAP_MAX_BATCH_SIZE, to limit the memory overhead. For existing software compressors, the only extra overhead is the extra 'buffers' pointer, so 8 bytes per-CPU on x86_64. Only the first buffer is currently used.

Regardless of compression batching, always process large folios in batches. For hardware compressors, the batch size is the compressor batch size, otherwise ZSWAP_MAX_BATCH_SIZE is used.

zswap_store_page() is replaced with zswap_store_pages(), which processes a batch of pages and allows for batching optimizations. For now, only optimize allocating entries by using batch allocations from the slab cache.

Avoid repeatedly calling mem_cgroup_zswap_writeback_enabled() for every page and only call it once for the folio, since the entire folio is charged to a single memcg. Similarly, obtain the nid once to use during each batch store of a folio.

---
 mm/zswap.c | 304 +++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 212 insertions(+), 92 deletions(-)
mm/zswap.c +212 -92
index c2731d3acb2c..8ae67fd10522 100644
@@ -84,6 +84,9 @@ static bool zswap_pool_reached_full;
 
 #define ZSWAP_PARAM_UNSET ""
 
+/* Limit the batch size to limit per-CPU memory usage for dst buffers. */
+#define ZSWAP_MAX_BATCH_SIZE 8U
+
 static int zswap_setup(void);
 
 /* Enable/disable zswap */
@@ -141,7 +144,7 @@ struct crypto_acomp_ctx {
 	struct crypto_acomp *acomp;
 	struct acomp_req *req;
 	struct crypto_wait wait;
-	u8 *buffer;
+	u8 **buffers;
 	struct mutex mutex;
 };
 
@@ -150,10 +153,14 @@ struct crypto_acomp_ctx {
  * The only case where lru_lock is not acquired while holding tree.lock is
  * when a zswap_entry is taken off the lru for writeback, in that case it
  * needs to be verified that it's still valid in the tree.
+ *
+ * @compr_batch_size: The max batch size of the compression algorithm,
+ *                    bounded by ZSWAP_MAX_BATCH_SIZE.
  */
 struct zswap_pool {
 	struct zs_pool *zs_pool;
 	struct crypto_acomp_ctx __percpu *acomp_ctx;
+	u8 compr_batch_size;
 	struct percpu_ref ref;
 	struct rcu_work release_rwork;
 	struct hlist_node node;
@@ -269,8 +276,10 @@ static inline struct xarray *swap_zswap_tree(swp_entry_t swp)
 **********************************/
 static void __zswap_pool_empty(struct percpu_ref *ref);
 
-static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)
+static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx, u8 nr_buffers)
 {
+	u8 i;
+
 	if (!acomp_ctx)
 		return;
 
@@ -293,8 +302,12 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)
 
 	acomp_ctx->acomp = NULL;
 
-	kfree(acomp_ctx->buffer);
-	acomp_ctx->buffer = NULL;
+	if (acomp_ctx->buffers) {
+		for (i = 0; i < nr_buffers; ++i)
+			kfree(acomp_ctx->buffers[i]);
+		kfree(acomp_ctx->buffers);
+	}
+	acomp_ctx->buffers = NULL;
 }
 
 static struct zswap_pool *zswap_pool_create(char *compressor)
@@ -378,7 +391,9 @@ static struct zswap_pool *zswap_pool_create(char *compressor)
 
 cpuhp_add_fail:
 	for_each_possible_cpu(cpu)
-		acomp_ctx_free(per_cpu_ptr(pool->acomp_ctx, cpu));
+		acomp_ctx_free(per_cpu_ptr(pool->acomp_ctx, cpu),
+			       pool->compr_batch_size);
+
 error:
 	if (pool->acomp_ctx)
 		free_percpu(pool->acomp_ctx);
@@ -416,7 +431,8 @@ static void zswap_pool_destroy(struct zswap_pool *pool)
 	cpuhp_state_remove_instance(CPUHP_MM_ZSWP_POOL_PREPARE, &pool->node);
 
 	for_each_possible_cpu(cpu)
-		acomp_ctx_free(per_cpu_ptr(pool->acomp_ctx, cpu));
+		acomp_ctx_free(per_cpu_ptr(pool->acomp_ctx, cpu),
+			       pool->compr_batch_size);
 
 	free_percpu(pool->acomp_ctx);
 
@@ -767,6 +783,41 @@ static void zswap_entry_cache_free(struct zswap_entry *entry)
 	kmem_cache_free(zswap_entry_cache, entry);
 }
 
+static __always_inline void zswap_entries_cache_free_batch(
+	struct zswap_entry **entries,
+	u8 nr_entries)
+{
+	/*
+	 * It is okay to use this to free entries allocated separately
+	 * by zswap_entry_cache_alloc().
+	 */
+	kmem_cache_free_bulk(zswap_entry_cache, nr_entries, (void **)entries);
+}
+
+static bool zswap_entries_cache_alloc_batch(
+	struct zswap_entry **entries,
+	u8 nr_entries,
+	gfp_t gfp,
+	int nid)
+{
+	if (unlikely(!kmem_cache_alloc_bulk(zswap_entry_cache, gfp,
+					    nr_entries, (void **)entries))) {
+		u8 i;
+
+		for (i = 0; i < nr_entries; ++i) {
+			entries[i] = zswap_entry_cache_alloc(GFP_KERNEL, nid);
+
+			if (unlikely(!entries[i])) {
+				zswap_reject_kmemcache_fail++;
+				zswap_entries_cache_free_batch(entries, i);
+				return false;
+			}
+		}
+	}
+
+	return true;
+}
+
 /*
  * Carries out the common pattern of freeing an entry's zsmalloc allocation,
  * freeing the entry itself, and decrementing the number of stored pages.
@@ -797,7 +848,9 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 {
 	struct zswap_pool *pool = hlist_entry(node, struct zswap_pool, node);
 	struct crypto_acomp_ctx *acomp_ctx = per_cpu_ptr(pool->acomp_ctx, cpu);
+	int nid = cpu_to_node(cpu);
 	int ret = -ENOMEM;
+	u8 i;
 
 	/*
 	 * To handle cases where the CPU goes through online-offline-online
@@ -808,15 +861,11 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 		return 0;
 	}
 
-	acomp_ctx->buffer = kmalloc_node(PAGE_SIZE, GFP_KERNEL, cpu_to_node(cpu));
-	if (!acomp_ctx->buffer)
-		return ret;
-
 	/*
 	 * In case of an error, crypto_alloc_acomp_node() returns an
 	 * error pointer, never NULL.
 	 */
-	acomp_ctx->acomp = crypto_alloc_acomp_node(pool->tfm_name, 0, 0, cpu_to_node(cpu));
+	acomp_ctx->acomp = crypto_alloc_acomp_node(pool->tfm_name, 0, 0, nid);
 	if (IS_ERR(acomp_ctx->acomp)) {
 		pr_err("could not alloc crypto acomp %s : %pe\n",
 				pool->tfm_name, acomp_ctx->acomp);
@@ -824,6 +873,13 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 		goto fail;
 	}
 
+	/*
+	 * Allocate up to ZSWAP_MAX_BATCH_SIZE dst buffers if the
+	 * compressor supports batching.
+	 */
+	pool->compr_batch_size = min(ZSWAP_MAX_BATCH_SIZE,
+				     crypto_acomp_batch_size(acomp_ctx->acomp));
+
 	/* acomp_request_alloc() returns NULL in case of an error. */
 	acomp_ctx->req = acomp_request_alloc(acomp_ctx->acomp);
 	if (!acomp_ctx->req) {
@@ -832,6 +888,17 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 		goto fail;
 	}
 
+	acomp_ctx->buffers = kcalloc_node(pool->compr_batch_size, sizeof(u8 *),
+					  GFP_KERNEL, nid);
+	if (!acomp_ctx->buffers)
+		goto fail;
+
+	for (i = 0; i < pool->compr_batch_size; ++i) {
+		acomp_ctx->buffers[i] = kmalloc_node(PAGE_SIZE, GFP_KERNEL, nid);
+		if (!acomp_ctx->buffers[i])
+			goto fail;
+	}
+
 	crypto_init_wait(&acomp_ctx->wait);
 
 	/*
@@ -852,12 +919,13 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 	return 0;
 
 fail:
-	acomp_ctx_free(acomp_ctx);
+	acomp_ctx_free(acomp_ctx, pool->compr_batch_size);
 	return ret;
 }
 
 static bool zswap_compress(struct folio *folio, long index,
-			   struct zswap_entry *entry, struct zswap_pool *pool)
+			   struct zswap_entry *entry, struct zswap_pool *pool,
+			   bool wb_enabled)
 {
 	struct crypto_acomp_ctx *acomp_ctx;
 	struct scatterlist input, output;
@@ -871,7 +939,7 @@ static bool zswap_compress(struct folio *folio, long index,
 	acomp_ctx = raw_cpu_ptr(pool->acomp_ctx);
 	mutex_lock(&acomp_ctx->mutex);
 
-	dst = acomp_ctx->buffer;
+	dst = acomp_ctx->buffers[0];
 	sg_init_table(&input, 1);
 	sg_set_folio(&input, folio, PAGE_SIZE, index * PAGE_SIZE);
 
@@ -901,13 +969,10 @@ static bool zswap_compress(struct folio *folio, long index,
 	 * to the active LRU list in the case.
 	 */
 	if (comp_ret || !dlen || dlen >= PAGE_SIZE) {
-		rcu_read_lock();
-		if (!mem_cgroup_zswap_writeback_enabled(folio_memcg(folio))) {
-			rcu_read_unlock();
+		if (!wb_enabled) {
 			comp_ret = comp_ret ? comp_ret : -EINVAL;
 			goto unlock;
 		}
-		rcu_read_unlock();
 		comp_ret = 0;
 		dlen = PAGE_SIZE;
 		dst = kmap_local_folio(folio, index * PAGE_SIZE);
@@ -1440,91 +1505,122 @@ static void shrink_worker(struct work_struct *w)
 * main API
 **********************************/
 
-static bool zswap_store_page(struct folio *folio, long index,
-			     struct obj_cgroup *objcg,
-			     struct zswap_pool *pool)
+/*
+ * Store multiple pages in @folio, starting from the page at index @start up to
+ * the page at index @end-1.
+ */
+static bool zswap_store_pages(struct folio *folio,
+			      long start,
+			      long end,
+			      struct zswap_pool *pool,
+			      int nid,
+			      bool wb_enabled,
+			      struct obj_cgroup *objcg)
 {
-	swp_entry_t page_swpentry = folio_swap_entry(folio, index);
-	struct zswap_entry *entry, *old;
-	int nid = folio_nid(folio);
+	struct zswap_entry *entries[ZSWAP_MAX_BATCH_SIZE];
+	u8 i, store_fail_idx = 0, nr_pages = end - start;
 
-	/* allocate entry */
-	entry = zswap_entry_cache_alloc(GFP_KERNEL, nid);
-	if (!entry) {
-		zswap_reject_kmemcache_fail++;
-		return false;
-	}
+	VM_WARN_ON_ONCE(nr_pages <= 0 || nr_pages > ZSWAP_MAX_BATCH_SIZE);
 
-	if (!zswap_compress(folio, index, entry, pool))
-		goto compress_failed;
+	if (unlikely(!zswap_entries_cache_alloc_batch(entries, nr_pages,
+						      GFP_KERNEL, nid)))
+		return false;
 
 	/*
-	 * Set pool_idx before the xa_store() below publishes the entry, or a
-	 * concurrent reader could resolve a stale pool_idx left by slab reuse
-	 * to an unrelated live pool.
+	 * We co-locate entry initialization as much as possible here to
+	 * minimize potential cache misses.
 	 */
-	entry->pool_idx = pool->idx;
-	entry->nid = nid;
-
-	old = xa_store(swap_zswap_tree(page_swpentry),
-		       swp_offset(page_swpentry),
-		       entry, GFP_KERNEL);
-	if (xa_is_err(old)) {
-		int err = xa_err(old);
+	for (i = 0; i < nr_pages; ++i) {
+		entries[i]->handle = (unsigned long)ERR_PTR(-EINVAL);
+		/*
+		 * Set pool_idx before the xa_store() below publishes the entry, or a
+		 * concurrent reader could resolve a stale pool_idx left by slab reuse
+		 * to an unrelated live pool.
+		 */
+		entries[i]->pool_idx = pool->idx;
+		entries[i]->swpentry = folio_swap_entry(folio, start + i);
+		entries[i]->objcg = objcg;
+		entries[i]->referenced = true;
+		entries[i]->nid = nid;
+		INIT_LIST_HEAD(&entries[i]->lru);
+	}
 
-		WARN_ONCE(err != -ENOMEM, "unexpected xarray error: %d\n", err);
-		zswap_reject_alloc_fail++;
-		goto store_failed;
+	for (i = 0; i < nr_pages; ++i) {
+		if (!zswap_compress(folio, start + i, entries[i], pool, wb_enabled))
+			goto store_pages_failed;
 	}
 
-	/*
-	 * We may have had an existing entry that became stale when
-	 * the folio was redirtied and now the new version is being
-	 * swapped out. Get rid of the old.
-	 */
-	if (old)
-		zswap_entry_free(old);
+	for (i = 0; i < nr_pages; ++i) {
+		struct zswap_entry *old, *entry = entries[i];
+
+		old = xa_store(swap_zswap_tree(entry->swpentry),
+			       swp_offset(entry->swpentry),
+			       entry, GFP_KERNEL);
+		if (unlikely(xa_is_err(old))) {
+			int err = xa_err(old);
+
+			WARN_ONCE(err != -ENOMEM, "unexpected xarray error: %d\n", err);
+			zswap_reject_alloc_fail++;
+			/*
+			 * Entries up to this point have been stored in the
+			 * xarray. zswap_store() will erase them from the xarray
+			 * and call zswap_entry_free(). Local cleanup in
+			 * 'store_pages_failed' only needs to happen for
+			 * entries from [@i to @nr_pages).
+			 */
+			store_fail_idx = i;
+			goto store_pages_failed;
+		}
 
-	/*
-	 * The entry is successfully compressed and stored in the tree, there is
-	 * no further possibility of failure. Grab refs to the pool and objcg,
-	 * charge zswap memory, and increment zswap_stored_pages.
-	 * The opposite actions will be performed by zswap_entry_free()
-	 * when the entry is removed from the tree.
-	 */
-	zswap_pool_get(pool);
-	if (objcg) {
-		obj_cgroup_get(objcg);
-		obj_cgroup_charge_zswap(objcg, entry->length);
-	}
-	atomic_long_inc(&zswap_stored_pages);
-	if (entry->length == PAGE_SIZE)
-		atomic_long_inc(&zswap_stored_incompressible_pages);
+		/*
+		 * We may have had an existing entry that became stale when
+		 * the folio was redirtied and now the new version is being
+		 * swapped out. Get rid of the old.
+		 */
+		if (unlikely(old))
+			zswap_entry_free(old);
 
-	/*
-	 * We finish initializing the entry while it's already in xarray.
-	 * This is safe because:
-	 *
-	 * 1. Concurrent stores and invalidations are excluded by folio lock.
-	 *
-	 * 2. Writeback is excluded by the entry not being on the LRU yet.
-	 *    The publishing order matters to prevent writeback from seeing
-	 *    an incoherent entry.
-	 */
-	entry->swpentry = page_swpentry;
-	entry->objcg = objcg;
-	entry->referenced = true;
-	if (entry->length) {
-		INIT_LIST_HEAD(&entry->lru);
-		zswap_lru_add(entry);
+		/*
+		 * The entry is successfully compressed and stored in the tree,
+		 * and further failures will be cleaned up in zswap_store().
+		 * Grab refs to the pool and objcg, charge zswap memory, and
+		 * increment zswap_stored_pages. The opposite actions will be
+		 * performed by zswap_entry_free() when the entry is removed
+		 * from the tree.
+		 */
+		zswap_pool_get(pool);
+		if (objcg) {
+			obj_cgroup_get(objcg);
+			obj_cgroup_charge_zswap(objcg, entry->length);
+		}
+		atomic_long_inc(&zswap_stored_pages);
+		if (entry->length == PAGE_SIZE)
+			atomic_long_inc(&zswap_stored_incompressible_pages);
+
+		/*
+		 * We finish by adding the entry to the LRU while it's already
+		 * in xarray. This is safe because:
+		 *
+		 * 1. Concurrent stores and invalidations are excluded by folio lock.
+		 *
+		 * 2. Writeback is excluded by the entry not being on the LRU yet.
+		 *    The publishing order matters to prevent writeback from seeing
+		 *    an incoherent entry.
+		 */
+		if (likely(entry->length))
+			zswap_lru_add(entry);
 	}
 
 	return true;
 
-store_failed:
-	zs_free(pool->zs_pool, entry->handle);
-compress_failed:
-	zswap_entry_cache_free(entry);
+store_pages_failed:
+	for (i = store_fail_idx; i < nr_pages; ++i) {
+		if (!IS_ERR_VALUE(entries[i]->handle))
+			zs_free(pool->zs_pool, entries[i]->handle);
+	}
+	zswap_entries_cache_free_batch(&entries[store_fail_idx],
+				       nr_pages - store_fail_idx);
+
 	return false;
 }
 
@@ -1534,9 +1630,11 @@ bool zswap_store(struct folio *folio)
 	swp_entry_t swp = folio->swap;
 	struct obj_cgroup *objcg = NULL;
 	struct mem_cgroup *memcg = NULL;
+	bool wb_enabled, ret = false;
+	int nid = folio_nid(folio);
 	struct zswap_pool *pool;
-	bool ret = false;
-	long index;
+	u8 store_batch_size;
+	long start, end;
 
 	VM_WARN_ON_ONCE(!folio_test_locked(folio));
 	VM_WARN_ON_ONCE(!folio_test_swapcache(folio));
@@ -1570,8 +1668,30 @@ bool zswap_store(struct folio *folio)
 		mem_cgroup_put(memcg);
 	}
 
-	for (index = 0; index < nr_pages; ++index) {
-		if (!zswap_store_page(folio, index, objcg, pool))
+	rcu_read_lock();
+	wb_enabled = mem_cgroup_zswap_writeback_enabled(folio_memcg(folio));
+	rcu_read_unlock();
+
+	/*
+	 * For batching compressors, store the folio in batches of the
+	 * compressor's batch_size.
+	 *
+	 * For non-batching compressors, store the folio in batches
+	 * of ZSWAP_MAX_BATCH_SIZE, where each page in the batch is
+	 * compressed sequentially. This gives better performance than
+	 * invoking zswap_store_pages() per-page, due to cache locality
+	 * of working set structures.
+	 */
+	store_batch_size = (pool->compr_batch_size > 1) ?
+			    pool->compr_batch_size : ZSWAP_MAX_BATCH_SIZE;
+
+	for (start = 0; start < nr_pages; start += store_batch_size) {
+		end = min(start + store_batch_size, nr_pages);
+
+		ret = zswap_store_pages(folio, start, end, pool,
+					nid, wb_enabled, objcg);
+
+		if (unlikely(!ret))
 			goto put_pool;
 	}
 
Signature · 1 line
2.39.5
#15 lore raw

PATCH 7/8 mm: zswap: Optimally place members of struct zswap_pool, struct zswap_entry.

In reply to Kanchana P. Sridhar

Place and align the members of struct zswap_pool and struct zswap_entry such that members used together in compress/decompress hot paths are able to share the same cacheline. Minimize holes within these structs while doing so.

---
 mm/zswap.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
mm/zswap.c +5 -5
index 8ae67fd10522..ade84ac4076e 100644
@@ -160,11 +160,11 @@ struct crypto_acomp_ctx {
 struct zswap_pool {
 	struct zs_pool *zs_pool;
 	struct crypto_acomp_ctx __percpu *acomp_ctx;
-	u8 compr_batch_size;
 	struct percpu_ref ref;
-	struct rcu_work release_rwork;
-	struct hlist_node node;
 	u8 idx;
+	u8 compr_batch_size;
+	struct hlist_node node;
+	struct rcu_work release_rwork;
 	char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
@@ -202,10 +202,10 @@ static struct shrinker *zswap_shrinker;
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
  * nid - NUMA node id of the page for which this is the zswap entry.
- * pool_idx - id of the zswap_pool that the entry's data is in.
  * 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.
+ * pool_idx - id of the zswap_pool that the entry's data is in.
  */
 struct zswap_entry {
 	swp_entry_t swpentry;
@@ -214,10 +214,10 @@ struct zswap_entry {
 		bool referenced:1;
 	};
 	int nid;
-	u8 pool_idx;
 	unsigned long handle;
 	struct obj_cgroup *objcg;
 	struct list_head lru;
+	u8 pool_idx;
 };
 
 /*
Signature · 1 line
2.39.5
maintainer
#16 lore raw

Re: [PATCH v1 7/8] mm: zswap: Optimally place members of struct zswap_pool, struct zswap_entry.

Quoted 16 lines · Kanchana P. Sridhar

Place and align the members of struct zswap_pool and struct zswap_entry such that members used together in compress/decompress hot paths are able to share the same cacheline. Minimize holes within these structs while doing so.

---
 mm/zswap.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
mm/zswap.c +0 -0
index 8ae67fd10522..ade84ac4076e 100644
@@ -160,11 +160,11 @@ struct crypto_acomp_ctx {
 struct zswap_pool {

mm/zswap.c struct crypto_acomp_ctx { · line 167

Quoted 10 lines
mm/zswap.c +3 -3
 	struct zs_pool *zs_pool;
 	struct crypto_acomp_ctx __percpu *acomp_ctx;
-	u8 compr_batch_size;
 	struct percpu_ref ref;
-	struct rcu_work release_rwork;
-	struct hlist_node node;
 	u8 idx;
+	u8 compr_batch_size;
+	struct hlist_node node;
+	struct rcu_work release_rwork;

compr_batch_size is added by this series, why not just add it before idx to begin with?

Quoted 16 lines
mm/zswap.c +1 -1
 	char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
@@ -202,10 +202,10 @@ static struct shrinker *zswap_shrinker;
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
  * nid - NUMA node id of the page for which this is the zswap entry.
- * pool_idx - id of the zswap_pool that the entry's data is in.
  * 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.
+ * pool_idx - id of the zswap_pool that the entry's data is in.
  */
 struct zswap_entry {
 	swp_entry_t swpentry;

mm/zswap.c struct zswap_entry { · line 220

Quoted 8 lines
mm/zswap.c +1 -1
@@ -214,10 +214,10 @@ struct zswap_entry {
 		bool referenced:1;
 	};
 	int nid;
-	u8 pool_idx;
 	unsigned long handle;
 	struct obj_cgroup *objcg;
 	struct list_head lru;
+	u8 pool_idx;

As mentioned previously, this needs to be packed with the length.

Quoted 6 lines
mm/zswap.c +0 -0
 };
 
 /*
Signature · 1 line
2.39.5
#17 lore raw

Re: [PATCH v1 7/8] mm: zswap: Optimally place members of struct zswap_pool, struct zswap_entry.

Quoted 23 lines · Yosry Ahmed
Quoted 21 lines · Kanchana P. Sridhar

Place and align the members of struct zswap_pool and struct zswap_entry such that members used together in compress/decompress hot paths are able to share the same cacheline. Minimize holes within these structs while doing so.

---
 mm/zswap.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
mm/zswap.c +0 -2
index 8ae67fd10522..ade84ac4076e 100644
@@ -160,11 +160,11 @@ struct crypto_acomp_ctx {
 struct zswap_pool {
      struct zs_pool *zs_pool;
      struct crypto_acomp_ctx __percpu *acomp_ctx;
-     u8 compr_batch_size;
      struct percpu_ref ref;
-     struct rcu_work release_rwork;
Quoted 8 lines

mm/zswap.c struct crypto_acomp_ctx { · line 167

Quoted 5 lines
mm/zswap.c +3 -1
-     struct hlist_node node;
      u8 idx;
+     u8 compr_batch_size;
+     struct hlist_node node;
+     struct rcu_work release_rwork;

compr_batch_size is added by this series, why not just add it before idx to begin with?

In patch 0006, where compr_batch_size is introduced, I was attempting to place it in the same cacheline as the acomp_ctx. This increased the size of struct zswap_pool to 248 bytes. I later optimized this in patch 0007, bringing back the size to 240 with the addition of compr_batch_size.

As things stand:

Baseline mm-unstable:

struct zswap_pool {

        struct zs_pool *           zs_pool;              /*     0     8 */
        struct crypto_acomp_ctx *  acomp_ctx;            /*     8     8 */
        struct percpu_ref          ref;                  /*    16    16 */
        struct rcu_work            release_rwork
__attribute__((__aligned__(8))); /*    32    56 */
        /* --- cacheline 1 boundary (64 bytes) was 24 bytes ago --- */
        struct hlist_node          node;                 /*    88    16 */
        u8                         idx;                  /*   104     1 */
        char                       tfm_name[128];        /*   105   128 */

        /* size: 240, cachelines: 4, members: 7 */
        /* padding: 7 */
        /* forced alignments: 1 */
        /* last cacheline: 48 bytes */
} __attribute__((__aligned__(8)));

My latest version makes changes to struct zswap_pool in 2 separate
patches: the first introduces compr_batch_size without changing the size, the second optimizes for hot-path members' co-location on the same cacheline:

struct zswap_pool {
struct zs_pool *zs_pool;
struct crypto_acomp_ctx __percpu *acomp_ctx;
struct percpu_ref ref;
u8 idx;
u8 compr_batch_size;
struct hlist_node node;
struct rcu_work release_rwork;
char tfm_name[CRYPTO_MAX_ALG_NAME];
};

struct zswap_pool {

        struct zs_pool *           zs_pool;              /*     0     8 */
        struct crypto_acomp_ctx *  acomp_ctx;            /*     8     8 */
        struct percpu_ref          ref;                  /*    16    16 */
        u8                         idx;                  /*    32     1 */
        u8                         compr_batch_size;     /*    33     1 */

/* XXX 6 bytes hole, try to pack */

struct hlist_node node; /* 40 16 */
struct rcu_work release_rwork
__attribute__((__aligned__(8))); /* 56 56 */

        /* --- cacheline 1 boundary (64 bytes) was 48 bytes ago --- */
        char                       tfm_name[128];        /*   112   128 */

        /* size: 240, cachelines: 4, members: 8 */
        /* sum members: 234, holes: 1, sum holes: 6 */
        /* forced alignments: 1 */
        /* last cacheline: 48 bytes */
} __attribute__((__aligned__(8)));
Quoted 19 lines
Quoted 19 lines
mm/zswap.c +1 -1
      char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
@@ -202,10 +202,10 @@ static struct shrinker *zswap_shrinker;
  *              logic if referenced is unset. See comments in the shrinker
  *              section for context.
  * nid - NUMA node id of the page for which this is the zswap entry.
- * pool_idx - id of the zswap_pool that the entry's data is in.
  * 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.
+ * pool_idx - id of the zswap_pool that the entry's data is in.
  */
 struct zswap_entry {
      swp_entry_t swpentry;
@@ -214,10 +214,10 @@ struct zswap_entry {
              bool referenced:1;
      };
Quoted 8 lines

mm/zswap.c struct zswap_entry { · line 220

Quoted 6 lines
mm/zswap.c +1 -1
      int nid;
-     u8 pool_idx;
      unsigned long handle;
      struct obj_cgroup *objcg;
      struct list_head lru;
+     u8 pool_idx;

As mentioned previously, this needs to be packed with the length.

Done.

Quoted 7 lines
Quoted 7 lines
mm/zswap.c +0 -1
 };
 
 /*
--

2.39.5

maintainer
#18 lore raw

Re: [PATCH v1 7/8] mm: zswap: Optimally place members of struct zswap_pool, struct zswap_entry.

Quoted 34 lines · Kanchana P. Sridhar
Quoted 23 lines · Yosry Ahmed
Quoted 21 lines · Kanchana P. Sridhar

Place and align the members of struct zswap_pool and struct zswap_entry such that members used together in compress/decompress hot paths are able to share the same cacheline. Minimize holes within these structs while doing so.

---
 mm/zswap.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
mm/zswap.c +0 -2
index 8ae67fd10522..ade84ac4076e 100644
@@ -160,11 +160,11 @@ struct crypto_acomp_ctx {
 struct zswap_pool {
      struct zs_pool *zs_pool;
      struct crypto_acomp_ctx __percpu *acomp_ctx;
-     u8 compr_batch_size;
      struct percpu_ref ref;
-     struct rcu_work release_rwork;
Quoted 8 lines

mm/zswap.c struct crypto_acomp_ctx { · line 167

Quoted 5 lines
mm/zswap.c +3 -1
-     struct hlist_node node;
      u8 idx;
+     u8 compr_batch_size;
+     struct hlist_node node;
+     struct rcu_work release_rwork;

compr_batch_size is added by this series, why not just add it before idx to begin with?

Quoted 5 lines

In patch 0006, where compr_batch_size is introduced, I was attempting to place it in the same cacheline as the acomp_ctx. This increased the size of struct zswap_pool to 248 bytes. I later optimized this in patch 0007, bringing back the size to 240 with the addition of compr_batch_size.

This doesn't make much sense, and the size of zswap_pool is not that important (usually only one globally). I would drop the patch and add compr_batch_size directly where it needs to end up.

#19 lore raw

Re: [PATCH v1 7/8] mm: zswap: Optimally place members of struct zswap_pool, struct zswap_entry.

Quoted 43 lines · Yosry Ahmed
Quoted 34 lines · Kanchana P. Sridhar
Quoted 23 lines · Yosry Ahmed
Quoted 21 lines · Kanchana P. Sridhar

Place and align the members of struct zswap_pool and struct zswap_entry such that members used together in compress/decompress hot paths are able to share the same cacheline. Minimize holes within these structs while doing so.

---
 mm/zswap.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
mm/zswap.c +0 -2
index 8ae67fd10522..ade84ac4076e 100644
@@ -160,11 +160,11 @@ struct crypto_acomp_ctx {
 struct zswap_pool {
      struct zs_pool *zs_pool;
      struct crypto_acomp_ctx __percpu *acomp_ctx;
-     u8 compr_batch_size;
      struct percpu_ref ref;
-     struct rcu_work release_rwork;
Quoted 8 lines

mm/zswap.c struct crypto_acomp_ctx { · line 167

Quoted 5 lines
mm/zswap.c +3 -1
-     struct hlist_node node;
      u8 idx;
+     u8 compr_batch_size;
+     struct hlist_node node;
+     struct rcu_work release_rwork;

compr_batch_size is added by this series, why not just add it before idx to begin with?

Quoted 5 lines

In patch 0006, where compr_batch_size is introduced, I was attempting to place it in the same cacheline as the acomp_ctx. This increased the size of struct zswap_pool to 248 bytes. I later optimized this in patch 0007, bringing back the size to 240 with the addition of compr_batch_size.

Quoted 3 lines

This doesn't make much sense, and the size of zswap_pool is not that important (usually only one globally). I would drop the patch and add compr_batch_size directly where it needs to end up.

Sounds good. Thanks!

#20 lore raw

PATCH 8/8 mm: zswap: Batched zswap_compress() for storing large folios in batches.

In reply to Kanchana P. Sridhar

A new batching implementation of zswap_compress() is provided for compressors that do and do not support batching. This eliminates code duplication and facilitates code maintainability with the introduction of compress batching.

The vectorized implementation of calling the earlier zswap_compress() sequentially, one page at a time in zswap_store_pages(), is replaced with this new version of zswap_compress() that accepts multiple pages to compress as a batch.

If the compressor does not support batching, each page in the batch is compressed and stored sequentially.

If the batch is compressed without errors, the compressed buffers for the batch are stored in zsmalloc. In case of compression errors, the current behavior based on whether the folio is enabled for zswap writeback, is preserved.

The batched zswap_compress() incorporates Herbert's suggestion for SG lists to represent the batch's inputs/outputs to interface with the crypto API [1].

Performance data: usemem with 60% over-commit:
==============================================
This patch series was tested on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:
cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then exits.

usemem metrics averaged across 10 iterations*:
================================================

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 Average Throughput (KB/s)                  11,794      17,810    +51.00%
 Total Throughput (KB/s)                    47,179      71,242    +51.00%
 Average Time to Free Memory (usecs)       490,973     294,920    -39.93%
 Total Time to Free Memory (usecs)       1,963,892   1,179,682    -39.93%
 --------------------------------------------------------------------------
 * vmstats are as reported at the end of the 10th iteration:
 64kB-alloc                                834,567     828,199     -0.76%
 64kB-zswpout                              700,138     743,697      6.22%

64kB-swpout-fallback 35,342 17,978 -49.13%
64kB-swpout 0 0
--------------------------------------------------------------------------

The speedup with zswap store batching of large folios has a propelling effect on other parts of mm, especially, by enabling large folios to be swapped out without splitting, as the data shows. This enables other mm components that are optimized for large folios, such as munmap(), folio refcounts/mapcounts updates, PTE invalidation upon freeing memory, zswap_invalidate(); which benefit from xarray offsets/range continuity. As a result, subsequent large folio swapouts are more likely to find contiguous swap slots, there are fewer large folio splits during reclaim, more mTHP zswpouts and the cycle continues. As the data demonstrates, this directly contributes to more memory over-commit opportunity.

Architectural considerations for the zswap batching framework:
==============================================================
The zswap batching framework is designed to be hardware-agnostic. It can be leveraged by any hardware accelerator or software-based compressor.

Potential future clients of the batching framework:
===================================================
This patch-series demonstrates the performance benefits of compression batching when used in zswap_store() of large folios. Compression batching can be used for other use cases such as batching compression in zram, batch compression of different folios during reclaim, kcompressd, file systems, etc. Decompression batching can be used to improve efficiency of zswap writeback (Thanks Nhat for this idea), batching decompressions in zram, etc.

[1]: https://lore.kernel.org/all/[email protected]/T/#m99aea2ce3d284e6c5a3253061d97b08c4752a798
[2]: https://wanos.co/assets/silesia.tar

---
 mm/zswap.c | 250 +++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 187 insertions(+), 63 deletions(-)
mm/zswap.c +187 -63
index ade84ac4076e..68748349c3ae 100644
@@ -145,6 +145,7 @@ struct crypto_acomp_ctx {
 	struct acomp_req *req;
 	struct crypto_wait wait;
 	u8 **buffers;
+	struct sg_table *sg_table;
 	struct mutex mutex;
 };
 
@@ -308,6 +309,12 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx, u8 nr_buffers)
 		kfree(acomp_ctx->buffers);
 	}
 	acomp_ctx->buffers = NULL;
+
+	if (acomp_ctx->sg_table) {
+		sg_free_table(acomp_ctx->sg_table);
+		kfree(acomp_ctx->sg_table);
+	}
+	acomp_ctx->sg_table = NULL;
 }
 
 static struct zswap_pool *zswap_pool_create(char *compressor)
@@ -849,6 +856,7 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 	struct zswap_pool *pool = hlist_entry(node, struct zswap_pool, node);
 	struct crypto_acomp_ctx *acomp_ctx = per_cpu_ptr(pool->acomp_ctx, cpu);
 	int nid = cpu_to_node(cpu);
+	struct scatterlist *sg;
 	int ret = -ENOMEM;
 	u8 i;
 
@@ -899,6 +907,21 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 			goto fail;
 	}
 
+	acomp_ctx->sg_table = kmalloc(sizeof(*acomp_ctx->sg_table), GFP_KERNEL);
+	if (!acomp_ctx->sg_table)
+		goto fail;
+
+	if (sg_alloc_table(acomp_ctx->sg_table, pool->compr_batch_size,
+			   GFP_KERNEL))
+		goto fail;
+
+	/*
+	 * Statically map the per-CPU destination buffers to the per-CPU
+	 * SG lists; crucial for performance.
+	 */
+	for_each_sg(acomp_ctx->sg_table->sgl, sg, pool->compr_batch_size, i)
+		sg_set_buf(sg, acomp_ctx->buffers[i], PAGE_SIZE);
+
 	crypto_init_wait(&acomp_ctx->wait);
 
 	/*
@@ -923,85 +946,184 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 	return ret;
 }
 
-static bool zswap_compress(struct folio *folio, long index,
-			   struct zswap_entry *entry, struct zswap_pool *pool,
-			   bool wb_enabled)
+/*
+ * The inner compress batching and pool storage function.
+ */
+static bool __zswap_compress(struct folio *folio,
+			     long folio_start,
+			     u8 batch_start,
+			     u8 nr_batch_pages,
+			     struct crypto_acomp_ctx *acomp_ctx,
+			     struct zswap_entry *entries[],
+			     struct zswap_pool *pool,
+			     int nid,
+			     bool wb_enabled)
 {
-	struct crypto_acomp_ctx *acomp_ctx;
-	struct scatterlist input, output;
-	int comp_ret = 0, alloc_ret = 0;
-	unsigned int dlen = PAGE_SIZE;
+	gfp_t gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
+	unsigned int slen = nr_batch_pages * PAGE_SIZE;
+	u8 batch_iter, comp_batch_iter;
+	struct scatterlist *sg;
 	unsigned long handle;
-	gfp_t gfp;
+	int err, dlen;
 	u8 *dst;
-	bool mapped = false;
 
-	acomp_ctx = raw_cpu_ptr(pool->acomp_ctx);
-	mutex_lock(&acomp_ctx->mutex);
+	/*
+	 * @slen indicates the total source length bytes for
+	 * @nr_batch_pages.
+	 *
+	 * The source folio pages in the batch are directly submitted to
+	 * crypto_acomp via acomp_request_set_src_folio().
+	 *
+	 * The pool's compressor batch size is at least @nr_batch_pages,
+	 * hence the acomp_ctx has at least @nr_batch_pages dst @buffers.
+	 */
+	acomp_request_set_src_folio(acomp_ctx->req, folio,
+				    (folio_start + batch_start) * PAGE_SIZE,
+				    slen);
+
+	acomp_ctx->sg_table->sgl->length = slen;
 
-	dst = acomp_ctx->buffers[0];
-	sg_init_table(&input, 1);
-	sg_set_folio(&input, folio, PAGE_SIZE, index * PAGE_SIZE);
+	/*
+	 * The per-CPU @acomp_ctx->sg_table scatterlists are statically
+	 * mapped to the per-CPU dst @buffers at pool creation time.
+	 */
+	acomp_request_set_dst_sg(acomp_ctx->req,
+				 acomp_ctx->sg_table->sgl,
+				 slen);
 
-	sg_init_one(&output, dst, PAGE_SIZE);
-	acomp_request_set_params(acomp_ctx->req, &input, &output, PAGE_SIZE, dlen);
+	err = crypto_wait_req(crypto_acomp_compress(acomp_ctx->req),
+			      &acomp_ctx->wait);
 
 	/*
-	 * it maybe looks a little bit silly that we send an asynchronous request,
-	 * then wait for its completion synchronously. This makes the process look
-	 * synchronous in fact.
-	 * Theoretically, acomp supports users send multiple acomp requests in one
-	 * acomp instance, then get those requests done simultaneously. but in this
-	 * case, zswap actually does store and load page by page, there is no
-	 * existing method to send the second page before the first page is done
-	 * in one thread doing zswap.
-	 * but in different threads running on different cpu, we have different
-	 * acomp instance, so multiple threads can do (de)compression in parallel.
+	 * If a page cannot be compressed into a size smaller than
+	 * PAGE_SIZE, save the content as is without a compression, to
+	 * keep the LRU order of writebacks.  If writeback is disabled,
+	 * reject the page since it only adds metadata overhead.
+	 * swap_writeout() will put the page back to the active LRU list
+	 * in the case.
+	 *
+	 * It is assumed that any compressor that sets the output length
+	 * to 0 or a value >= PAGE_SIZE will also return a negative
+	 * error status in @err; i.e, will not return a successful
+	 * compression status in @err in this case.
 	 */
-	comp_ret = crypto_wait_req(crypto_acomp_compress(acomp_ctx->req), &acomp_ctx->wait);
-	dlen = acomp_ctx->req->dlen;
+	if (unlikely(err && !wb_enabled))
+		goto compress_error;
 
 	/*
-	 * If a page cannot be compressed into a size smaller than PAGE_SIZE,
-	 * save the content as is without a compression, to keep the LRU order
-	 * of writebacks.  If writeback is disabled, reject the page since it
-	 * only adds metadata overhead.  swap_writeout() will put the page back
-	 * to the active LRU list in the case.
+	 * For each output SG list in @acomp_ctx->req->sg_table->sgl,
+	 * the @sg->length should be set to either the page's compressed
+	 * length (success), or it's negative value compression error.
 	 */
-	if (comp_ret || !dlen || dlen >= PAGE_SIZE) {
-		if (!wb_enabled) {
-			comp_ret = comp_ret ? comp_ret : -EINVAL;
-			goto unlock;
+	for_each_sg(acomp_ctx->sg_table->sgl, sg, nr_batch_pages,
+		    comp_batch_iter) {
+		batch_iter = batch_start + comp_batch_iter;
+		dst = acomp_ctx->buffers[comp_batch_iter];
+		dlen = sg->length;
+
+		if (unlikely(dlen <= 0 || dlen >= PAGE_SIZE)) {
+			dlen = PAGE_SIZE;
+			dst = kmap_local_folio(folio,
+					(folio_start + batch_iter) * PAGE_SIZE);
 		}
-		comp_ret = 0;
-		dlen = PAGE_SIZE;
-		dst = kmap_local_folio(folio, index * PAGE_SIZE);
-		mapped = true;
+
+		handle = zs_malloc(pool->zs_pool, dlen, gfp, nid);
+
+		if (unlikely(IS_ERR_VALUE(handle))) {
+			if (PTR_ERR((void *)handle) == -ENOSPC)
+				zswap_reject_compress_poor++;
+			else
+				zswap_reject_alloc_fail++;
+
+			if (dst != acomp_ctx->buffers[comp_batch_iter])
+				kunmap_local(dst);
+
+			return false;
+		}
+
+		zs_obj_write(pool->zs_pool, handle, dst, dlen);
+		entries[batch_iter]->handle = handle;
+		entries[batch_iter]->length = dlen;
+		if (dst != acomp_ctx->buffers[comp_batch_iter])
+			kunmap_local(dst);
 	}
 
-	gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
-	handle = zs_malloc(pool->zs_pool, dlen, gfp, folio_nid(folio));
-	if (IS_ERR_VALUE(handle)) {
-		alloc_ret = PTR_ERR((void *)handle);
-		goto unlock;
+	return true;
+
+compress_error:
+	for_each_sg(acomp_ctx->sg_table->sgl, sg, nr_batch_pages,
+		    comp_batch_iter) {
+		if ((int)sg->length < 0) {
+			if ((int)sg->length == -ENOSPC)
+				zswap_reject_compress_poor++;
+			else
+				zswap_reject_compress_fail++;
+		}
 	}
 
-	zs_obj_write(pool->zs_pool, handle, dst, dlen);
-	entry->handle = handle;
-	entry->length = dlen;
+	return false;
+}
 
-unlock:
-	if (mapped)
-		kunmap_local(dst);
-	if (comp_ret == -ENOSPC || alloc_ret == -ENOSPC)
-		zswap_reject_compress_poor++;
-	else if (comp_ret)
-		zswap_reject_compress_fail++;
-	else if (alloc_ret)
-		zswap_reject_alloc_fail++;
+/*
+ * zswap_compress() batching implementation for sequential and batching
+ * compressors.
+ *
+ * Description:
+ * ============
+ * Compress multiple @nr_pages in @folio starting from the @folio_start index
+ * in batches of @nr_batch_pages; the latter representing the lesser of the
+ * @nr_pages to be batch compressed and the compressor's batch size.
+ *
+ * @nr_pages can be in (1, ZSWAP_MAX_BATCH_SIZE] even if the compressor does not
+ * support batching.
+ *
+ * If @nr_batch_pages is 1, each page is processed sequentially.
+ *
+ * If @nr_batch_pages is > 1, the compressor may use batching to optimize
+ * compression.
+ */
+static bool zswap_compress(struct folio *folio,
+			   long folio_start,
+			   u8 nr_pages,
+			   u8 nr_batch_pages,
+			   struct zswap_entry *entries[],
+			   struct zswap_pool *pool,
+			   int nid,
+			   bool wb_enabled)
+{
+	struct crypto_acomp_ctx *acomp_ctx;
+	bool ret = true;
+	u8 batch_start;
+
+	/*
+	 * Locking the acomp_ctx mutex once per store batch results in better
+	 * performance as compared to locking per compress batch.
+	 */
+	acomp_ctx = raw_cpu_ptr(pool->acomp_ctx);
+	mutex_lock(&acomp_ctx->mutex);
+
+	/*
+	 * Compress the @nr_pages in @folio starting at index @folio_start
+	 * in batches of @nr_batch_pages.
+	 */
+	for (batch_start = 0; batch_start < nr_pages;
+	     batch_start += nr_batch_pages) {
+		/*
+		 * Send @nr_batch_pages to crypto_acomp for compression:
+		 *
+		 * These pages are in @folio's range of indices in the interval
+		 *     [@folio_start + @batch_start,
+		 *      @folio_start + @batch_start + @nr_batch_pages).
+		 */
+		ret = __zswap_compress(folio, folio_start, batch_start,
+				       nr_batch_pages, acomp_ctx, entries,
+				       pool, nid, wb_enabled);
+		if (unlikely(!ret))
+			break;
+	}
 
 	mutex_unlock(&acomp_ctx->mutex);
-	return comp_ret == 0 && alloc_ret == 0;
+	return ret;
 }
 
 static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)
@@ -1519,6 +1641,7 @@ static bool zswap_store_pages(struct folio *folio,
 {
 	struct zswap_entry *entries[ZSWAP_MAX_BATCH_SIZE];
 	u8 i, store_fail_idx = 0, nr_pages = end - start;
+	bool ret;
 
 	VM_WARN_ON_ONCE(nr_pages <= 0 || nr_pages > ZSWAP_MAX_BATCH_SIZE);
 
@@ -1545,10 +1668,11 @@ static bool zswap_store_pages(struct folio *folio,
 		INIT_LIST_HEAD(&entries[i]->lru);
 	}
 
-	for (i = 0; i < nr_pages; ++i) {
-		if (!zswap_compress(folio, start + i, entries[i], pool, wb_enabled))
-			goto store_pages_failed;
-	}
+	ret = zswap_compress(folio, start, nr_pages,
+			     min(nr_pages, pool->compr_batch_size),
+			     entries, pool, nid, wb_enabled);
+	if (unlikely(!ret))
+		goto store_pages_failed;
 
 	for (i = 0; i < nr_pages; ++i) {
 		struct zswap_entry *old, *entry = entries[i];
Signature · 1 line
2.39.5
maintainer
#21 lore raw
In reply to Kanchana P. Sridhar
Quoted 30 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

Quoted 5 lines

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================

How much of the performance benefits come from patch 6 vs patch 8?

Quoted 204 lines

Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

usemem metrics averaged across 10 iterations*:
================================================

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 Average Throughput (KB/s)                  11,794      17,810    +51.00%
 Total Throughput (KB/s)                    47,179      71,242    +51.00%
 Average Time to Free Memory (usecs)       490,973     294,920    -39.93%
 Total Time to Free Memory (usecs)       1,963,892   1,179,682    -39.93%

--------------------------------------------------------------------------
* vmstats are as reported at the end of the 10th iteration:

 64kB-alloc                                834,567     828,199     -0.76%
 64kB-zswpout                              700,138     743,697      6.22%

64kB-swpout-fallback 35,342 17,978 -49.13%
64kB-swpout 0 0
--------------------------------------------------------------------------

The speedup with zswap store batching of large folios has a propelling effect on other parts of mm, especially, by enabling large folios to be swapped out without splitting, as the data shows. This enables other mm components that are optimized for large folios, such as munmap(), folio refcounts/mapcounts updates, PTE invalidation upon freeing memory, zswap_invalidate(); which benefit from xarray offsets/range continuity. As a result, subsequent large folio swapouts are more likely to find contiguous swap slots, there are fewer large folio splits during reclaim, more mTHP zswpouts and the cycle continues. As the data demonstrates, this directly contributes to more memory over-commit opportunity.

Detailed usemem metrics/cgroup memory utilization/vmstats:
==========================================================

A) MAX zswap pool_total_size vmstats:
=====================================
This is a snapshot at the 10th iteration's peak zswap pool usage:

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 pswpin                                    0                 0
 pswpout                                   0                 0
 thp_swpout                                0                 0
 thp_swpout_fallback                       0                 0
 swpin_zero                              594               548
 swpout_zero                           1,062               864
 zswpin                           10,603,882        11,070,673
 zswpout                          16,594,356        16,509,201     -0.51%
 zswpwb                                    0                 0           
 nrswpin                                   0                 0           
 nrswpout                                  0                 0           
 pgmajfault                        1,460,360         1,470,092      0.67%
 nr_free_pages                       897,249           908,935      1.30%
 nr_free_pages_blocks                892,928           903,680      1.20%
 64kB-alloc                          831,788           825,556     -0.75%
 64kB-zswpout                        690,961           735,863      6.50%
 64kB-swpout                               0                 0           
 64kB-swpout-fallback                 32,982            16,545    -49.84%
 64kB-swpin                                0                 0           
 zswap_pool_total_size         1,420,120,064     1,377,210,368     -3.02%
 memory.current                3,390,189,568     3,299,569,664     -2.67%
 memory.swap.current           3,221,221,376     3,221,221,376

--------------------------------------------------------------------------

B) STEADY STATE vmstats after usemem finishes:
==============================================
This is a snapshot after the 10th iteration finishes:

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 pswpin                                    0                 0 
 pswpout                                   0                 0
 thp_swpout                                0                 0
 thp_swpout_fallback                       0                 0
 swpin_zero                              594               548
 swpout_zero                           1,062               949
 zswpin                           11,526,695        11,963,729
 zswpout                          17,293,120        17,219,911     -0.42%
 zswpwb                                    0                 0           
 nrswpin                                   0                 0           
 nrswpout                                  0                 0           
 pgmajfault                        1,591,298         1,596,095      0.30%
 nr_free_pages                     1,752,789         1,753,264      0.03%
 nr_free_pages_blocks              1,258,496         1,240,064     -1.46%
 64kB-alloc                          834,567           828,199     -0.76%
 64kB-zswpout                        700,138           743,697      6.22%
 64kB-swpout                               0                 0           
 64kB-swpout-fallback                 35,342            17,978    -49.13%
 64kB-swpin                                0                 0           
 zswap_pool_total_size                     0                 0           

memory.current 757,760 802,816 5.95%
memory.swap.current 0 0
--------------------------------------------------------------------------
zswap debugfs counters:

  pool_limit_hit                           0                 0
  written_back_pages                       0                 0
  reject_alloc_fail                        0                 0
  reject_kmemcache_fail                    0                 0
  reject_reclaim_fail                      0                 0
  reject_compress_fail                     0                 0
  reject_compress_poor                     0                 0
  decompress_fail                          0                 0
  stored_incompressible_pages              0                 0
  stored_pages                             0                 0

--------------------------------------------------------------------------

[1]: https://patchwork.kernel.org/project/linux-mm/cover/[email protected]/
[2]: https://wanos.co/assets/silesia.tar

Changes since [1]:
==================
1) Better cacheline placement of struct zswap_pool and struct zswap_entry members for temporal memory access efficiency in zswap store/load hot paths, while minimizing holes.

2) Refactored the core compression batching functionality to a separate __zswap_compress() function, which is called by zswap_compress(), as per Nhat's and Yosry's comments. Hopefully this helps explain the outer "store batching" zswap_compress() vis-a-vis the inner "compress batching" __zswap_compress() distinction.

3) The acomp_ctx is locked once and is passed to the routine that compresses a batch, per the algorithm's batch-size. Similarly, the folio's node id and writeback_enabled status are computed once and passed to the routines that implement the different store functionalities, to save latency by avoid localized recomputes of information that doesn't change.

4) Moved the folio node id being stored in zswap_entry to a separate patch, per Yosry's comment.

5) Addressed the kunmap_local() issue flagged by Jixin He in the IS_ERR_VALUE(handle) error path
(https://patchwork.kernel.org/comment/27095925) - thanks!

6) Moved comments inline, per Yosry's suggestions.

Thanks,
Kanchana

Kanchana P. Sridhar (8):
crypto: acomp - Define a unit_size in struct acomp_req to enable
batching. mm: zswap: Set the unit size for zswap to PAGE_SIZE.
crypto: acomp - Add bit to indicate segmentation support.
crypto: acomp - Add trivial segmentation wrapper.
crypto: acomp - Add API to get a compression algorithm's batch-size.
mm: zswap: Store the folio's node id in the zswap_entry.
mm: zswap: Store large folios in batches.
mm: zswap: Optimally place members of struct zswap_pool, struct
zswap_entry.
mm: zswap: Batched zswap_compress() for storing large folios in
batches.

 crypto/acompress.c                  |  47 ++-
 include/crypto/acompress.h          |  62 +++
 include/crypto/algapi.h             |   5 +
 include/crypto/internal/acompress.h |   8 +
 include/linux/crypto.h              |   3 +
 mm/zswap.c                          | 571 ++++++++++++++++++++--------
 6 files changed, 528 insertions(+), 168 deletions(-)
Signature · 1 line
2.39.5
#22 lore raw
Quoted 32 lines · Yosry Ahmed
Quoted 30 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

Quoted 7 lines
Quoted 5 lines

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

patch 3 with this change in acompress.c: 19,578 KB/s

crypto/acompress.c +6 -2
index 192ea996e779..dc609bd7d927 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;
 
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (!crypto_acomp_req_seg(tfm)) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }
 
                return tfm->compress(req);
        }

Herbert, would you be Ok with this change in patch 3?

Thanks,
Kanchana

Quoted 205 lines
Quoted 205 lines

Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

usemem metrics averaged across 10 iterations*:
================================================

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 Average Throughput (KB/s)                  11,794      17,810    +51.00%
 Total Throughput (KB/s)                    47,179      71,242    +51.00%
 Average Time to Free Memory (usecs)       490,973     294,920    -39.93%
 Total Time to Free Memory (usecs)       1,963,892   1,179,682    -39.93%

--------------------------------------------------------------------------
* vmstats are as reported at the end of the 10th iteration:

 64kB-alloc                                834,567     828,199     -0.76%
 64kB-zswpout                              700,138     743,697      6.22%

64kB-swpout-fallback 35,342 17,978 -49.13%
64kB-swpout 0 0
--------------------------------------------------------------------------

The speedup with zswap store batching of large folios has a propelling effect on other parts of mm, especially, by enabling large folios to be swapped out without splitting, as the data shows. This enables other mm components that are optimized for large folios, such as munmap(), folio refcounts/mapcounts updates, PTE invalidation upon freeing memory, zswap_invalidate(); which benefit from xarray offsets/range continuity. As a result, subsequent large folio swapouts are more likely to find contiguous swap slots, there are fewer large folio splits during reclaim, more mTHP zswpouts and the cycle continues. As the data demonstrates, this directly contributes to more memory over-commit opportunity.

Detailed usemem metrics/cgroup memory utilization/vmstats:
==========================================================

A) MAX zswap pool_total_size vmstats:
=====================================
This is a snapshot at the 10th iteration's peak zswap pool usage:

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 pswpin                                    0                 0
 pswpout                                   0                 0
 thp_swpout                                0                 0
 thp_swpout_fallback                       0                 0
 swpin_zero                              594               548
 swpout_zero                           1,062               864
 zswpin                           10,603,882        11,070,673
 zswpout                          16,594,356        16,509,201     -0.51%
 zswpwb                                    0                 0
 nrswpin                                   0                 0
 nrswpout                                  0                 0
 pgmajfault                        1,460,360         1,470,092      0.67%
 nr_free_pages                       897,249           908,935      1.30%
 nr_free_pages_blocks                892,928           903,680      1.20%
 64kB-alloc                          831,788           825,556     -0.75%
 64kB-zswpout                        690,961           735,863      6.50%
 64kB-swpout                               0                 0
 64kB-swpout-fallback                 32,982            16,545    -49.84%
 64kB-swpin                                0                 0
 zswap_pool_total_size         1,420,120,064     1,377,210,368     -3.02%
 memory.current                3,390,189,568     3,299,569,664     -2.67%
 memory.swap.current           3,221,221,376     3,221,221,376

--------------------------------------------------------------------------

B) STEADY STATE vmstats after usemem finishes:
==============================================
This is a snapshot after the 10th iteration finishes:

--------------------------------------------------------------------------
Baseline Patch Change
mm-unstable series with
10-3-2026 v1 v1
--------------------------------------------------------------------------

 pswpin                                    0                 0
 pswpout                                   0                 0
 thp_swpout                                0                 0
 thp_swpout_fallback                       0                 0
 swpin_zero                              594               548
 swpout_zero                           1,062               949
 zswpin                           11,526,695        11,963,729
 zswpout                          17,293,120        17,219,911     -0.42%
 zswpwb                                    0                 0
 nrswpin                                   0                 0
 nrswpout                                  0                 0
 pgmajfault                        1,591,298         1,596,095      0.30%
 nr_free_pages                     1,752,789         1,753,264      0.03%
 nr_free_pages_blocks              1,258,496         1,240,064     -1.46%
 64kB-alloc                          834,567           828,199     -0.76%
 64kB-zswpout                        700,138           743,697      6.22%
 64kB-swpout                               0                 0
 64kB-swpout-fallback                 35,342            17,978    -49.13%
 64kB-swpin                                0                 0
 zswap_pool_total_size                     0                 0

memory.current 757,760 802,816 5.95%
memory.swap.current 0 0
--------------------------------------------------------------------------
zswap debugfs counters:

  pool_limit_hit                           0                 0
  written_back_pages                       0                 0
  reject_alloc_fail                        0                 0
  reject_kmemcache_fail                    0                 0
  reject_reclaim_fail                      0                 0
  reject_compress_fail                     0                 0
  reject_compress_poor                     0                 0
  decompress_fail                          0                 0
  stored_incompressible_pages              0                 0
  stored_pages                             0                 0

--------------------------------------------------------------------------

[1]: https://patchwork.kernel.org/project/linux-mm/cover/[email protected]/
[2]: https://wanos.co/assets/silesia.tar

Changes since [1]:
==================
1) Better cacheline placement of struct zswap_pool and struct zswap_entry members for temporal memory access efficiency in zswap store/load hot paths, while minimizing holes.

2) Refactored the core compression batching functionality to a separate __zswap_compress() function, which is called by zswap_compress(), as per Nhat's and Yosry's comments. Hopefully this helps explain the outer "store batching" zswap_compress() vis-a-vis the inner "compress batching" __zswap_compress() distinction.

3) The acomp_ctx is locked once and is passed to the routine that compresses a batch, per the algorithm's batch-size. Similarly, the folio's node id and writeback_enabled status are computed once and passed to the routines that implement the different store functionalities, to save latency by avoid localized recomputes of information that doesn't change.

4) Moved the folio node id being stored in zswap_entry to a separate patch, per Yosry's comment.

5) Addressed the kunmap_local() issue flagged by Jixin He in the IS_ERR_VALUE(handle) error path
(https://patchwork.kernel.org/comment/27095925) - thanks!

6) Moved comments inline, per Yosry's suggestions.

Thanks,
Kanchana

Kanchana P. Sridhar (8):
crypto: acomp - Define a unit_size in struct acomp_req to enable
batching. mm: zswap: Set the unit size for zswap to PAGE_SIZE.
crypto: acomp - Add bit to indicate segmentation support.
crypto: acomp - Add trivial segmentation wrapper.
crypto: acomp - Add API to get a compression algorithm's batch-size.
mm: zswap: Store the folio's node id in the zswap_entry.
mm: zswap: Store large folios in batches.
mm: zswap: Optimally place members of struct zswap_pool, struct
zswap_entry.
mm: zswap: Batched zswap_compress() for storing large folios in
batches.

 crypto/acompress.c                  |  47 ++-
 include/crypto/acompress.h          |  62 +++
 include/crypto/algapi.h             |   5 +
 include/crypto/internal/acompress.h |   8 +
 include/linux/crypto.h              |   3 +
 mm/zswap.c                          | 571 ++++++++++++++++++++--------
 6 files changed, 528 insertions(+), 168 deletions(-)

--
2.39.5

maintainer
#23 lore raw
Quoted 45 lines · Kanchana P. Sridhar
Quoted 32 lines · Yosry Ahmed
Quoted 30 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

Quoted 7 lines
Quoted 5 lines

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

Quoted 8 lines

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

Why add it to begin with in this case?

Quoted 24 lines

patch 3 with this change in acompress.c: 19,578 KB/s

crypto/acompress.c +6 -2
index 192ea996e779..dc609bd7d927 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;
 
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (!crypto_acomp_req_seg(tfm)) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }
 
                return tfm->compress(req);
        }

Herbert, would you be Ok with this change in patch 3?

#24 lore raw
Quoted 51 lines · Yosry Ahmed
Quoted 48 lines · Kanchana P. Sridhar
Quoted 32 lines · Yosry Ahmed
Quoted 30 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

Quoted 7 lines
Quoted 5 lines

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

Quoted 7 lines
Quoted 5 lines

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

Why add it to begin with in this case?

Patch 3 ("crypto: acomp - Add trivial segmentation wrapper.") was co-developed with Herbert. The request chaining for non-segmentation algorithms was as in Herbert's original implementation. Since this causes a regression, I would like to get Herbert's Ok on replacing the request chaining in this part of crypto_acomp_compress().

Quoted 25 lines
Quoted 25 lines

patch 3 with this change in acompress.c: 19,578 KB/s

crypto/acompress.c +6 -2
index 192ea996e779..dc609bd7d927 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;
 
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (!crypto_acomp_req_seg(tfm)) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }
 
                return tfm->compress(req);
        }

Herbert, would you be Ok with this change in patch 3?

maintainer
#25 lore raw
Quoted 59 lines · Kanchana P. Sridhar
Quoted 57 lines · Yosry Ahmed
Quoted 53 lines · Kanchana P. Sridhar
Quoted 32 lines · Yosry Ahmed
Quoted 30 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

Quoted 7 lines
Quoted 5 lines

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

Quoted 7 lines
Quoted 1 line

Why add it to begin with in this case?

Patch 3 ("crypto: acomp - Add trivial segmentation wrapper.") was co-developed with Herbert. The request chaining for non-segmentation algorithms was as in Herbert's original implementation. Since this causes a regression, I would like to get Herbert's Ok on replacing the request chaining in this part of crypto_acomp_compress().

I mean, why add patch 8 if it doesn't improve performance? Seems like even with your diff, we only recover performance, but we don't improve it after patch 6?

#26 lore raw
Quoted 70 lines · Yosry Ahmed
Quoted 59 lines · Kanchana P. Sridhar
Quoted 57 lines · Yosry Ahmed
Quoted 53 lines · Kanchana P. Sridhar
Quoted 32 lines · Yosry Ahmed
Quoted 30 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

Quoted 7 lines
Quoted 5 lines

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

Quoted 7 lines
Quoted 1 line

Why add it to begin with in this case?

Patch 3 ("crypto: acomp - Add trivial segmentation wrapper.") was co-developed with Herbert. The request chaining for non-segmentation algorithms was as in Herbert's original implementation. Since this causes a regression, I would like to get Herbert's Ok on replacing the request chaining in this part of crypto_acomp_compress().

Quoted 3 lines

I mean, why add patch 8 if it doesn't improve performance? Seems like even with your diff, we only recover performance, but we don't improve it after patch 6?

Patch 8 does improve performance over patch 6 (19,578 KB/s vs. 19,119 KB/s) by 2.4%. Besides, patch 8 provides a compress batching framework in zswap, which is the purpose of this patch series. zswap is the first user of the crypto_acomp compress batching API provided earlier in this series, which I think, is a great starting point for us to evolve this further, as mentioned in the cover letter.

Thanks,
Kanchana

maintainer
#27 lore raw
Quoted 22 lines
Quoted 22 lines
Quoted 21 lines
Quoted 8 lines
Quoted 8 lines
Quoted 1 line

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

Quoted 7 lines
Quoted 5 lines

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

Why add it to begin with in this case?

Patch 3 ("crypto: acomp - Add trivial segmentation wrapper.") was co-developed with Herbert. The request chaining for non-segmentation algorithms was as in Herbert's original implementation. Since this causes a regression, I would like to get Herbert's Ok on replacing the request chaining in this part of crypto_acomp_compress().

Quoted 7 lines
Quoted 3 lines

I mean, why add patch 8 if it doesn't improve performance? Seems like even with your diff, we only recover performance, but we don't improve it after patch 6?

Patch 8 does improve performance over patch 6 (19,578 KB/s vs. 19,119 KB/s) by 2.4%. Besides, patch 8 provides a compress batching framework in zswap, which is the purpose of this patch series.

No, the purpose of this patch series is to improve performance, be it through the compression batching framework or otherwise. The compression batching framework made sense as it provided greater performance gains with HW batching/concurrency. Without that, I am honestly skeptical. 2.4% can be within the noise.

Quoted 4 lines

zswap is the
first user of the crypto_acomp compress batching API provided earlier in this series, which I think, is a great starting point for us to evolve this further, as mentioned in the cover letter.

Without a non-trivial performance benefit (e.g. with HW batching), I don't see enough justification. My advice is to try to push 6 (and its dependencies) separately.

#28 lore raw
Quoted 31 lines · Yosry Ahmed
Quoted 23 lines
Quoted 23 lines
Quoted 22 lines
Quoted 9 lines
Quoted 9 lines
Quoted 2 lines

How much of the performance benefits come from patch 6 vs patch 8?

This is how the performance changes: Average throughput is averaged from 10 iterations of usemem.

patch 6: 19,119 KB/s
patch 8: 17,593 KB/s

Quoted 7 lines
Quoted 5 lines

So the acomp_do_req_chain() path that ZSTD goes through with patch 8 causes a regression, even though it provides a generic batching framework in acomp.

I was able to retrieve performance with the following change in acompress.c:

Why add it to begin with in this case?

Patch 3 ("crypto: acomp - Add trivial segmentation wrapper.") was co-developed with Herbert. The request chaining for non-segmentation algorithms was as in Herbert's original implementation. Since this causes a regression, I would like to get Herbert's Ok on replacing the request chaining in this part of crypto_acomp_compress().

Quoted 7 lines
Quoted 3 lines

I mean, why add patch 8 if it doesn't improve performance? Seems like even with your diff, we only recover performance, but we don't improve it after patch 6?

Patch 8 does improve performance over patch 6 (19,578 KB/s vs. 19,119 KB/s) by 2.4%. Besides, patch 8 provides a compress batching framework in zswap, which is the purpose of this patch series.

Quoted 5 lines

No, the purpose of this patch series is to improve performance, be it through the compression batching framework or otherwise. The compression batching framework made sense as it provided greater performance gains with HW batching/concurrency. Without that, I am honestly skeptical. 2.4% can be within the noise.

I seem to have left out patch 8 after making the crypto_acomp change to not use request chaining.
My apologies! This was an honest mistake..

I have regathered the 10 iterations average throughput of usemem after applying patch 8:

                                        patch 6: 19,119 KB/s

crypto_acomp change + patch 8: 20,946 KB/s (+9.56%)

The purpose of this patch series is still the same: to set the framework for batching of zswap compressions, and not just of zswap stores of large folios which is what stopping at patch 6 would do.

With SW compressors (ZSTD), patch 8, i.e., batching of compressions and storing the compressed data in a pool, demonstrates significant performance improvement (sorry again for having missed applying the patch earlier today). This is because we isolate the working set to just these ops, which benefits from cache locality and minimizes
evictions: as against the vectorized implementation without the patch.

One of the immediate benefits of this framework is that I am using it for bulk allocations of compressed data for a batch, for entries that map to the same slab bin. This proves the forward-looking benefits of isolating compression and storage from other zswap_store_pages() ops.

The crypto_acomp change I had mentioned earlier does not hinder HW compressors in any way. As mentioned in the commit logs, patches 1-4 provide the API that can be used by HW compressors as they see fit, as long as they register the segmentation bit in the algorithm creation.

If you are Ok with all this, we will need Herbert's Ok for this change in patch 3:

crypto/acompress.c +6 -2
index 192ea996e779..db8e4c82b900 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;
 
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (likely(!crypto_acomp_req_seg(tfm))) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }
 
                return tfm->compress(req);
        }
Quoted 9 lines
Quoted 5 lines

zswap is the
first user of the crypto_acomp compress batching API provided earlier in this series, which I think, is a great starting point for us to evolve this further, as mentioned in the cover letter.

Without a non-trivial performance benefit (e.g. with HW batching), I don't see enough justification. My advice is to try to push 6 (and its dependencies) separately.

This doesn't penalize HW compressors, while representing the status of what exists today (sequential).

maintainer
#29 lore raw
Quoted 10 lines · Kanchana P. Sridhar

If you are Ok with all this, we will need Herbert's Ok for this change in patch 3:

crypto/acompress.c +0 -0
index 192ea996e779..db8e4c82b900 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;

crypto/acompress.c int crypto_acomp_compress(struct acomp_req *req) · line 301

Quoted 10 lines
crypto/acompress.c +6 -2
 
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (likely(!crypto_acomp_req_seg(tfm))) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }

What if you put this short-circuit into acomp_do_req_chain instead, does it still give you the same performance boost?

Cheers,

Signature · 3 lines
Email: Herbert Xu <[email protected]>
Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt
#30 lore raw
Quoted 13 lines · Herbert Xu
Quoted 11 lines · Kanchana P. Sridhar

If you are Ok with all this, we will need Herbert's Ok for this change in patch 3:

crypto/acompress.c +0 -0
index 192ea996e779..db8e4c82b900 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;
 
Quoted 12 lines

crypto/acompress.c int crypto_acomp_compress(struct acomp_req *req) · line 301

Quoted 9 lines
crypto/acompress.c +6 -2
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (likely(!crypto_acomp_req_seg(tfm))) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }

What if you put this short-circuit into acomp_do_req_chain instead, does it still give you the same performance boost?

Sure, that's a great idea! I will let you know if this gives the same performance boost.

Thanks,
Kanchana

Quoted 6 lines

Cheers,
--
Email: Herbert Xu <[email protected]> Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt

#31 lore raw
In reply to Herbert Xu
Quoted 13 lines · Herbert Xu
Quoted 11 lines · Kanchana P. Sridhar

If you are Ok with all this, we will need Herbert's Ok for this change in patch 3:

crypto/acompress.c +0 -0
index 192ea996e779..db8e4c82b900 100644
@@ -293,8 +293,12 @@ int crypto_acomp_compress(struct acomp_req *req)
                return -EAGAIN;
 
Quoted 12 lines

crypto/acompress.c int crypto_acomp_compress(struct acomp_req *req) · line 301

Quoted 9 lines
crypto/acompress.c +6 -2
        if (likely(req->unit_size && acomp_request_issg(req))) {
-               if (!crypto_acomp_req_seg(tfm))
-                       return acomp_do_req_chain(req, true);
+               if (likely(!crypto_acomp_req_seg(tfm))) {
+                       int err = tfm->compress(req);
+
+                       req->dst->length = unlikely(err) ? err : req->dlen;
+                       return err;
+               }

What if you put this short-circuit into acomp_do_req_chain instead, does it still give you the same performance boost?

I made the suggested change as follows:

crypto/acompress.c +9 -1
index 192ea996e779..6697cf5944a3 100644
@@ -271,11 +271,19 @@ static void acomp_reqchain_done(void *data, int err)
 
 static int acomp_do_req_chain(struct acomp_req *req, bool comp)
 {
+ struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);
  int err;
 
  if (unlikely(req->unit_size && req->slen > req->unit_size))
  return -ENOSYS;
 
+ if (likely(acomp_request_issg(req) &&
+   !crypto_acomp_req_seg(tfm) && comp)) {
+ err = tfm->compress(req);
+ req->dst->length = unlikely(err) ? err : req->dlen;
+ return err;
+ }
+
  acomp_save_req(req, acomp_reqchain_done);
 
  err = acomp_do_one_req(req, comp);
@@ -293,7 +301,7 @@ int crypto_acomp_compress(struct acomp_req *req)
  return -EAGAIN;
 
  if (likely(req->unit_size && acomp_request_issg(req))) {
- if (!crypto_acomp_req_seg(tfm))
+ if (likely(!crypto_acomp_req_seg(tfm)))
  return acomp_do_req_chain(req, true);
 
  return tfm->compress(req);

The Average throughput of 10 iterations usemem is 19,058 KB/s. IOW, I don't get the same performance boost as making the localized change in crypto_acomp_compress(), likely because of added computes per compress call (one function call, two conditionals).

I think the localized change in crypto_acomp_compress() looks a bit cleaner as compared to the special-casing for "comp" in the generic acomp_do_req_chain().

In any case, please let me know if the above data helps.

Thanks!

Quoted 6 lines

Cheers,
--
Email: Herbert Xu <[email protected]> Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt

maintainer
#32 lore raw
Quoted 5 lines · Kanchana P. Sridhar

The Average throughput of 10 iterations usemem is 19,058 KB/s. IOW, I don't get the same performance boost as making the localized change in crypto_acomp_compress(), likely because of added computes per compress call (one function call, two conditionals).

What are we comparing against?

The thing is that the conditional will have to be there at some point as otherwise this becomes pointless.

What if you just do the conditional and short-circuit in crypto_acomp_compress?

Thanks,

Signature · 3 lines
Email: Herbert Xu <[email protected]>
Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt
#33 lore raw
Quoted 9 lines · Herbert Xu
Quoted 5 lines · Kanchana P. Sridhar

The Average throughput of 10 iterations usemem is 19,058 KB/s. IOW, I don't get the same performance boost as making the localized change in crypto_acomp_compress(), likely because of added computes per compress call (one function call, two conditionals).

What are we comparing against?

Sorry, I should have clarified! The comparison is against having the conditional and short-circuit in crypto_acomp_compress() [1]. I was referring to this as the "localized change in crypto_acomp_compress".

10 iterations usemem Average throughput with Conditional & short-circuit placement in:

crypto_acomp_compress(): 20,946 KB/s
acomp_do_req_chain(): 19,058 KB/s

Quoted 6 lines

The thing is that the conditional will have to be there at some point as otherwise this becomes pointless.

What if you just do the conditional and short-circuit in crypto_acomp_compress?

Yes, this is the first option I tried (20,946 KB/s).

[1]:

int crypto_acomp_compress(struct acomp_req *req) {
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);

if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm))) return -EAGAIN;

if (likely(req->unit_size && acomp_request_issg(req))) { if (likely(!crypto_acomp_req_seg(tfm))) {
int err = tfm->compress(req);

req->dst->length = unlikely(err) ? err : req->dlen; return err;
}

return tfm->compress(req);
}

if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req)) return tfm->compress(req);

return acomp_do_req_chain(req, true);
}

Thanks,
Kanchana

Quoted 6 lines

Thanks,
--
Email: Herbert Xu <[email protected]> Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt

maintainer
#34 lore raw
Quoted 23 lines · Kanchana P. Sridhar
Quoted 3 lines

What if you just do the conditional and short-circuit in crypto_acomp_compress?

Yes, this is the first option I tried (20,946 KB/s).

[1]:

int crypto_acomp_compress(struct acomp_req *req) {
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);

if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm))) return -EAGAIN;

if (likely(req->unit_size && acomp_request_issg(req))) { if (likely(!crypto_acomp_req_seg(tfm))) {
int err = tfm->compress(req);

req->dst->length = unlikely(err) ? err : req->dlen; return err;
}

Quoted 8 lines

return tfm->compress(req);
}

if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req)) return tfm->compress(req);

return acomp_do_req_chain(req, true);
}

That isn't quite right though. We need to ensure that it really is a singleton before taking the short-cut. So you need at least one conditional:

	if (likely(req->unit_size == req->slen))
		take short-cut

Cheers,

Signature · 3 lines
Email: Herbert Xu <[email protected]>
Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt
#35 lore raw
Quoted 34 lines · Herbert Xu
Quoted 23 lines · Kanchana P. Sridhar
Quoted 3 lines

What if you just do the conditional and short-circuit in crypto_acomp_compress?

Yes, this is the first option I tried (20,946 KB/s).

[1]:

int crypto_acomp_compress(struct acomp_req *req) {
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);

if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm))) return -EAGAIN;

if (likely(req->unit_size && acomp_request_issg(req))) { if (likely(!crypto_acomp_req_seg(tfm))) {
int err = tfm->compress(req);

req->dst->length = unlikely(err) ? err : req->dlen; return err;
}

Quoted 8 lines

return tfm->compress(req);
}

if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req)) return tfm->compress(req);

return acomp_do_req_chain(req, true);
}

Quoted 7 lines

That isn't quite right though. We need to ensure that it really is a singleton before taking the short-cut. So you need at least one conditional:

        if (likely(req->unit_size == req->slen))
                take short-cut

Got it. I will give this a try and let you know. Thanks!

Quoted 5 lines

Cheers,
--
Email: Herbert Xu <[email protected]> Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt

#36 lore raw
In reply to Herbert Xu
Quoted 10 lines · Herbert Xu

On Fri, Oct 16, 2026 at 10:02:13PM -0700, Kanchana P. Sridhar wrote:
[ .. ]

That isn't quite right though. We need to ensure that it really is a singleton before taking the short-cut. So you need at least one conditional:

        if (likely(req->unit_size == req->slen))
                take short-cut

I added the check for the singleton req [2]. I hope this looks Ok.

With the latest crypto_acomp_compress(), the batch compress latency improves considerably:

usemem 10 iterations average throughput:

patch 6 (store batching only): 19,119 KB/s
patch 8 (+ compress batching): 24,076 KB/s (+25.93%)

bpftrace measurement of zswap compress latency per-page:

Sequential_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 1,722,489 43,947 57,791 88,489
130,165 323,961

Batched_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 665,267 41,511 54,915 77,707
124,648 213,962
=================================================================================
Latency saving (ns) -2,435 -2,876 -10,782
-5,517 -109,999
=================================================================================

Please let me know if [2] looks good and I can include this in v2, thanks!

[2]:

int crypto_acomp_compress(struct acomp_req *req) {
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);

if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm))) return -EAGAIN;

if (likely(req->unit_size && acomp_request_issg(req))) { if (likely(!crypto_acomp_req_seg(tfm))) {
if (likely(req->unit_size == req->slen)) {
int err = tfm->compress(req);

req->dst->length = unlikely(err) ? err : req->dlen; return err;
} else {
return acomp_do_req_chain(req, true);
}
}

return tfm->compress(req);
}

if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req)) return tfm->compress(req);

return acomp_do_req_chain(req, true);
}

Quoted 6 lines

Cheers,
--
Email: Herbert Xu <[email protected]> Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt

maintainer
#37 lore raw
Quoted 24 lines · Kanchana P. Sridhar

Please let me know if [2] looks good and I can include this in v2, thanks!

[2]:

int crypto_acomp_compress(struct acomp_req *req) {
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);

if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm))) return -EAGAIN;

if (likely(req->unit_size && acomp_request_issg(req))) { if (likely(!crypto_acomp_req_seg(tfm))) {
if (likely(req->unit_size == req->slen)) {
int err = tfm->compress(req);

req->dst->length = unlikely(err) ? err : req->dlen; return err;
} else {
return acomp_do_req_chain(req, true);
}
}

Quoted 8 lines

return tfm->compress(req);
}

if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req)) return tfm->compress(req);

return acomp_do_req_chain(req, true);
}

This looks good to me.

Thanks,

Signature · 3 lines
Email: Herbert Xu <[email protected]>
Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt
#38 lore raw
Quoted 29 lines · Herbert Xu
Quoted 27 lines · Kanchana P. Sridhar

Please let me know if [2] looks good and I can include this in v2, thanks!

[2]:

int crypto_acomp_compress(struct acomp_req *req) {
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);

if (unlikely(acomp_req_on_stack(req) && acomp_is_async(tfm))) return -EAGAIN;

if (likely(req->unit_size && acomp_request_issg(req))) { if (likely(!crypto_acomp_req_seg(tfm))) {
if (likely(req->unit_size == req->slen)) {
int err = tfm->compress(req);

req->dst->length = unlikely(err) ? err : req->dlen; return err;
} else {
return acomp_do_req_chain(req, true);
}
}

return tfm->compress(req);
}

Quoted 8 lines
Quoted 5 lines

if (crypto_acomp_req_virt(tfm) || acomp_request_issg(req)) return tfm->compress(req);

return acomp_do_req_chain(req, true);
}

This looks good to me.

Amazing! Many thanks, Herbert.

Quoted 5 lines

Thanks,
--
Email: Herbert Xu <[email protected]> Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt

maintainer
#39 lore raw
In reply to Kanchana P. Sridhar
Quoted 14 lines · Kanchana P. Sridhar
Quoted 10 lines · Herbert Xu

On Fri, Oct 16, 2026 at 10:02:13PM -0700, Kanchana P. Sridhar wrote:
[ .. ]

That isn't quite right though. We need to ensure that it really is a singleton before taking the short-cut. So you need at least one conditional:

        if (likely(req->unit_size == req->slen))
                take short-cut

I added the check for the singleton req [2]. I hope this looks Ok.

Quoted 7 lines

With the latest crypto_acomp_compress(), the batch compress latency improves considerably:

usemem 10 iterations average throughput:

   patch 6 (store batching only): 19,119 KB/s

patch 8 (+ compress batching): 24,076 KB/s (+25.93%)

This is exactly what I mean by the numbers varying too much to draw any conclusions. I am not sure if it's the VM setup or the usemem test itself, but previously it was argued that the short-circuit in crypto_acomp_compress() was ~10% faster than the one in acomp_do_req_chain() (20,946 vs 19,058 KB/s) due to avoiding a function call and two conditionals, and now *adding* a conditional improves performance by 15% (20,946 -> 24,076 KB/s)?

Moreover, for zswap with ZSTD the added check is always true: the batch size is 1, so slen == unit_size == PAGE_SIZE for every request. The previous version and this one execute the same code, so the 15% difference can only be noise. With the stddev you reported (~5.4k KB/s over 10 iterations), both differences are well within the noise.

I think something is wrong with the performance testing methodology here. The results are self-contradicting and do not make much sense to me. Did I miss something?

Quoted 9 lines

bpftrace measurement of zswap compress latency per-page:

Sequential_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 1,722,489 43,947 57,791 88,489
130,165 323,961

Quoted 9 lines

Batched_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 665,267 41,511 54,915 77,707
124,648 213,962
=================================================================================
Latency saving (ns) -2,435 -2,876 -10,782
-5,517 -109,999
=================================================================================

I measured zswap_store() (not zswap_compress()), to get the per-folio latency, and it didn't seem to change much beyond noise in my testing. I also notice that the number of samples is very different between cases.

#40 lore raw
Quoted 24 lines · Yosry Ahmed
Quoted 14 lines · Kanchana P. Sridhar
Quoted 10 lines · Herbert Xu

On Fri, Oct 16, 2026 at 10:02:13PM -0700, Kanchana P. Sridhar wrote:
[ .. ]

That isn't quite right though. We need to ensure that it really is a singleton before taking the short-cut. So you need at least one conditional:

        if (likely(req->unit_size == req->slen))
                take short-cut

I added the check for the singleton req [2]. I hope this looks Ok.

Quoted 7 lines

With the latest crypto_acomp_compress(), the batch compress latency improves considerably:

usemem 10 iterations average throughput:

   patch 6 (store batching only): 19,119 KB/s

patch 8 (+ compress batching): 24,076 KB/s (+25.93%)

Quoted 7 lines

This is exactly what I mean by the numbers varying too much to draw any conclusions. I am not sure if it's the VM setup or the usemem test itself, but previously it was argued that the short-circuit in crypto_acomp_compress() was ~10% faster than the one in acomp_do_req_chain() (20,946 vs 19,058 KB/s) due to avoiding a function call and two conditionals, and now *adding* a conditional improves performance by 15% (20,946 -> 24,076 KB/s)?

I would like to clarify a few things:

1) The short-circuit in crypto_acomp_compress() that gave the 20,946 KB/s was missing the req->unit_size == req->slen "singleton check". However, it gave better performance than the baseline patch 3 in v1, which went through the complete acomp_do_req_chain() protocol (save req, do one req, finish req, etc.).

2) When I added the same short-circuit in acomp_do_req_chain(), it did not give as good performance as (1).

3) Herbert pointed out the short-circuit was missing the singleton check.

4) I added the singleton check into the original short-circuit (1), and this did not degrade performance. It meets the functional requirements for crypto, per Herbert's feedback.

5) I cannot make comparisons between "a function call + 2 conditionals" (2) and the functionally correct implementation in (4). The functionally correct solution is qualified with "likely" annotations, which can explain the performance improvement.

Quoted 10 lines

Moreover, for zswap with ZSTD the added check is always true: the batch size is 1, so slen == unit_size == PAGE_SIZE for every request. The previous version and this one execute the same code, so the 15% difference can only be noise. With the stddev you reported (~5.4k KB/s over 10 iterations), both differences are well within the noise.

I think something is wrong with the performance testing methodology here. The results are self-contradicting and do not make much sense to me. Did I miss something?

The previous version and this one don't execute the same code, as clarified above.

I don't see how the results are self-contradicting. Can you clarify what you mean by this?

OTOH, the zswap_compress() bpftrace results are clearly reinforcing the benefits of compress batching.

"Sequential_zswap_compress" is stopping after patch 6. "Batched_zswap_compress" is stopping after patch 8.

I hope we can agree based on the bpftrace data that Batched_zswap_compress outperforms Sequential_zswap_compress.

Quoted 20 lines
Quoted 10 lines

bpftrace measurement of zswap compress latency per-page:

Sequential_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 1,722,489 43,947 57,791 88,489
130,165 323,961

Quoted 9 lines

Batched_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 665,267 41,511 54,915 77,707
124,648 213,962
=================================================================================
Latency saving (ns) -2,435 -2,876 -10,782
-5,517 -109,999
=================================================================================

Quoted 4 lines

I measured zswap_store() (not zswap_compress()), to get the per-folio latency, and it didn't seem to change much beyond noise in my testing. I also notice that the number of samples is very different between cases.

I was addressing your earlier question about why not just stop after store batching, and why batching of compressions is needed. I would greatly appreciate it if you can review the zswap_compress() sequential vs. batching per-page latency data from the bpftrace and let me know if this convinces you?

I thought the overall zswap_store_pages() architecture had general buy-in, since we bulk allocate entries, etc optimizations to minimize locks. Please correct me if I am wrong. IOW, I am focusing on gathering bpftrace data of zswap_compress() with batching since this was the main patch that needs buy-in.

If we are on the same page thus far, can you please let me know why bpftrace of zswap_store() is important, and if I should gather this in my environment. I am happy to do so, but not sure what it will prove/disprove.

Thanks,
Kanchana

maintainer
#41 lore raw
In reply to Kanchana P. Sridhar
Quoted 58 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================
Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

Quoted 7 lines

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

What is --read-silesia? Is this a local change you made to usemem?

#42 lore raw
Quoted 67 lines · Yosry Ahmed
Quoted 64 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================
Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

Quoted 3 lines
Quoted 1 line
    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

What is --read-silesia? Is this a local change you made to usemem?

Yes, this is a local change I made because I wanted to get compressed lengths besides 565, which is the only compressed length of data swapped out from the original usemem.

To replicate my setup:

1) Setup Silesia dataset to be read by usemem:

wget https://wanos.co/assets/silesia.tar
tar -xvf silesia.tar

# concatenate the 12 binary files into one
cat * >> /tmp/silesia.bin

2) Modifications to usemem:

usemem_orig.c is
https://git.kernel.org/pub/scm/linux/kernel/git/wfg/vm-scalability.git/tree/usemem.c.

diff usemem.c usemem_orig.c:

75,76d74
< unsigned long silesia_bytes = 211938580UL;
< char *silesia_dataset;
103d100
< int opt_read_silesia = 0;
166d162
< " --read-silesia write memory with data from
Silesia.tar to allocate the pages from Linux kernel after malloc/mmap\n"
209d204
< { "read-silesia", 0, NULL, 0 },
352,373d346
< static void fill_buffer_from_silesia(char *buf, size_t length, const char *silesia_path)
< {
< size_t bytes_read = 0;
<
< int fd = open(silesia_path, O_RDONLY);
< if (fd < 0) {
< perror("Failed to open Silesia dataset");
< return;
< }
<
< while (bytes_read < length) {
< ssize_t res = read(fd, buf + bytes_read, length - bytes_read);
< if (res <= 0) {
< /* Loop back to beginning of silesia.bin if allocation
exceeds ~210MB */
< lseek(fd, 0, SEEK_SET);
< continue;
< }
< bytes_read += res;
< }
< close(fd);
< }
<
613,623d585
< static inline void copy_silesia_to_buffer(char *from, char *to, size_t length) < {
< size_t nr_bytes_copied = 0, remaining_bytes;
<
< while (nr_bytes_copied < length) {

<               remaining_bytes = min(silesia_bytes, length - nr_bytes_copied);
<               memcpy(to + nr_bytes_copied, from, remaining_bytes);
<               nr_bytes_copied += remaining_bytes;

< }
< }
<
638,642d599
<
< if (opt_read_silesia) {
< copy_silesia_to_buffer(silesia_dataset, (char
*)p, (size_t)bytes);
< }
<
1053,1054d1009
< } else if (strcmp(opts[opt_index].name,
"read-silesia") == 0) {
< opt_read_silesia = 1;
1263,1268d1217
< }
<
<
< if (opt_read_silesia) {
< silesia_dataset = (char *)allocate(silesia_bytes);
< fill_buffer_from_silesia(silesia_dataset,
(size_t)silesia_bytes, "/tmp/silesia.bin");

maintainer
#43 lore raw
Quoted 71 lines · Kanchana P. Sridhar
Quoted 69 lines · Yosry Ahmed
Quoted 65 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================
Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M
Quoted 7 lines
Quoted 1 line

What is --read-silesia? Is this a local change you made to usemem?

Yes, this is a local change I made because I wanted to get compressed lengths besides 565, which is the only compressed length of data swapped out from the original usemem.

To replicate my setup:

Thanks for sharing the details. I am having trouble reproducing the results, mainly because the standard deviation seems to be too high to make conclusions. What is the standard deviation in your runs?

Also, I tried measuring the latency of zswap_store() itself through bpftrace, and it doesn't seem to change much throughout the series.

#44 lore raw
Quoted 80 lines · Yosry Ahmed
Quoted 77 lines · Kanchana P. Sridhar
Quoted 67 lines · Yosry Ahmed
Quoted 64 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================
Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

Quoted 3 lines
Quoted 1 line
    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

What is --read-silesia? Is this a local change you made to usemem?

Yes, this is a local change I made because I wanted to get compressed lengths besides 565, which is the only compressed length of data swapped out from the original usemem.

Quoted 5 lines
Quoted 1 line

To replicate my setup:

Thanks for sharing the details. I am having trouble reproducing the results, mainly because the standard deviation seems to be too high to make conclusions. What is the standard deviation in your runs?

Sure thing! Let me know if it would be useful to upstream the --read-silesia option to the vm-scalability tree.

Yes, standard deviation is considerable, which is why I run usemem consecutively for 10 iterations and report the mean Average throughput. I could also report the median going forward if that helps. The cover letter vmstats are after 10 iterations. The last set of data I reported in this thread after applying patch 8 [batched version of zswap_compress()], had a mean of 20,946 KB/s, standard deviation of 5,391 KB/s and median of 22,477 KB/s. For reference, the stddev for the baseline mm-unstable was 5,397 KB/s.

I have found this methodology to be reliable in drawing conclusions because as zswap_store() performance improves, the outliers are fewer and the data is more clustered near the mean.

Quoted 3 lines

Also, I tried measuring the latency of zswap_store() itself through bpftrace, and it doesn't seem to change much throughout the series.

I ran bpftrace today to gather zswap_compress() latency per-page, before and after patch 8. It shows a clear performance improvement with compress batching for the reasons mentioned before:

Sequential_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 1,722,489 43,947 57,791 88,489
130,165 323,961

Batched_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 339,088 42,081 52,633 63,185
118,461 191,710
=================================================================================
Latency saving (ns) -1,866 -5,159 -25,305
-11,704 -132,251
=================================================================================

Hope this helps. Thanks!

maintainer
#45 lore raw
Quoted 99 lines · Kanchana P. Sridhar
Quoted 80 lines · Yosry Ahmed
Quoted 77 lines · Kanchana P. Sridhar
Quoted 67 lines · Yosry Ahmed
Quoted 64 lines · Kanchana P. Sridhar

v1: Batched zswap_store() with compression batching
===================================================

This series is a spin-off of "v14: zswap compression batching with optimized iaa_crypto driver" [1], and focuses on the generic crypto_acomp and zswap compression batching enablers. Resuming work on this series following a gap since v14 (January 2026). Please note that my email address and affiliation have changed from [email protected] to [email protected]. I have retained the original Intel sign-offs on prior patches for historical attribution while signing off v1 of this series with my current address.

To summarize the main contribution of this patch series: Batching is introduced in zswap_store() of large folios: we carve out a large folio into batches of ZSWAP_MAX_BATCH_SIZE (8 pages) for software compressors, and store each batch. Moving the fundamental operations that need to be happen to store a page to now work at the granularity of a batch offers opportunities to optimize them from these perspectives:

1) Utilize existing kernel API that minimize obtaining locks for bulk operations, e.g. kmem_cache_alloc_bulk()/kmem_cache_free_bulk().

2) We keep the working set for each of the core store functionalities (alloc entries, compress pages, store pages in zsmalloc) to be focused on a batch, thereby minimizing cache evictions and benefiting from cache locality.

3) Along the lines of (1), the main zswap_compress() batching routine obtains the acomp_ctx mutex lock once per batch, and releases it after it has processed all pages in that batch.

4) Any zswap store functionality that works on a batch can potentially be parallelized as a next step, to further improve performance.

Performance data: usemem with 60% over-commit:
==============================================
Validation was done with mm-unstable as of 10-3-2026 (Baseline) and with this patch series.

Testing was done on a Chromebook with a QEMU "vng" VM running in Crostini.

Chromebook Diagnostics reports:
Intel(R) Core(TM) Ultra 5 115U (10 CPUs, 4.200GHz)
RAM: 16GB

vng setup:

         Memory: 7G
           Swap: 3G
    Pinned CPUs: 4 (E-Cores: 4,5,6,7)

zswap compressor:
ZSTD

mTHP enabled:
64kB

vm-scalability/usemem setup:

cgroup memory.high: 3G
4 usemem processes, each allocates 1200M anonymous memory, reads the Silesia dataset [2] from a pre-fork-allocated buffer into the 1200M, writes to each unsigned long in the 1200M, sleeps for 10 sec, then frees the memory and exits.

Quoted 3 lines
Quoted 1 line
    usemem --read-silesia -w -O -b 1 -s 10 -n 4 1200M

What is --read-silesia? Is this a local change you made to usemem?

Yes, this is a local change I made because I wanted to get compressed lengths besides 565, which is the only compressed length of data swapped out from the original usemem.

Quoted 5 lines
Quoted 1 line

To replicate my setup:

Thanks for sharing the details. I am having trouble reproducing the results, mainly because the standard deviation seems to be too high to make conclusions. What is the standard deviation in your runs?

Sure thing! Let me know if it would be useful to upstream the --read-silesia option to the vm-scalability tree.

Yes, standard deviation is considerable, which is why I run usemem consecutively for 10 iterations and report the mean Average throughput. I could also report the median going forward if that helps. The cover letter vmstats are after 10 iterations. The last set of data I reported in this thread after applying patch 8 [batched version of zswap_compress()], had a mean of 20,946 KB/s, standard deviation of 5,391 KB/s and median of 22,477 KB/s. For reference, the stddev for the baseline mm-unstable was 5,397 KB/s.

Quoted 3 lines

I have found this methodology to be reliable in drawing conclusions because as zswap_store() performance improves, the outliers are fewer and the data is more clustered near the mean.

I think the stddev is too high to make any conclusions tbh, I will respond to the other thread with Herbert.

Quoted 26 lines
Quoted 4 lines

Also, I tried measuring the latency of zswap_store() itself through bpftrace, and it doesn't seem to change much throughout the series.

I ran bpftrace today to gather zswap_compress() latency per-page, before and after patch 8. It shows a clear performance improvement with compress batching for the reasons mentioned before:

Sequential_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 1,722,489 43,947 57,791 88,489
130,165 323,961

Batched_zswap_compress nr_samples p25 p50 p75
p95 p99
=================================================================================
zswap_compress_ns_per_page 339,088 42,081 52,633 63,185
118,461 191,710
=================================================================================
Latency saving (ns) -1,866 -5,159 -25,305
-11,704 -132,251
=================================================================================

Hope this helps. Thanks!