Skip to contentSkip to conversation

mm: zswap: reduce request contention on loads

PATCH Superseded Usama Arif 4 participants 10 messages Oct 9 – Nov 4

Subsystems (3)

v2 of 4 Superseded Latest is v4 →

lore
Share & tools
lore

Usama Arif

author
#0 lore raw

v2 cover mm: zswap: reduce request contention on loads

Stores and loads share a per-CPU acomp request and mutex. A low-priority store can be preempted right after the compressor drops its stream lock, while it still holds the zswap mutex, and a higher-priority load on that CPU then waits for the store to run again. This follows the work from Sergey Senozhatsky's zram series which splits it for the same reason [1].

Patch 1 gives compression and decompression separate requests, waits and mutexes, so loads no longer wait for stores, though they can still wait for each other. Patch 2 decompresses with an on-stack request when the algorithm is synchronous and needs no request context, which covers all in-tree software compressors, so those loads take no zswap lock. Asynchronous algorithms keep the per-CPU request and mutex. For software compressors the series allocates the same number of requests as before; each per-CPU context grows by 72 bytes, and the load path is about 270 bytes deeper on x86-64.

The series does not fix two related cases:
- Stores still serialize on the compression mutex, so a high-priority task that reclaims (direct reclaim, MADV_PAGEOUT) can still wait for a preempted store.
- On PREEMPT_RT the codec stream locks are preemptible, so a load can still wait for a preempted store inside the codec.

The numbers below are the slowest read per run, as a median (min-max) of 5 runs. Each run is 12 seconds in a zstd VM with lazy preemption, vm.page-cluster=0 and swap on /dev/ram0. With 1 vCPU, four nice +10 workers page memory out and read it back while a nice 0 task spins. A nice -19 reader pages out its own buffer and measures how long each read of it takes. With 8 vCPUs there are 16 workers, 8 spinning tasks and 8 readers.

              Before series (ms)      With series (ms)
  1 vCPU      22.3 (21.6-22.6)        0.97 (0.72-1.4)
  8 vCPUs     314 (97-2542)           7.0 (5.0-98)

Reads over 10 ms fell from 26-35 per run to none with 1 vCPU, and from 3-18 per run to at most one with 8 vCPUs. The benchmark and test programs were written with the help of an LLM.

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

v1 -> v2:
- Group the compression request and output buffer in zswap_comp_ctx, documenting that the compression mutex protects the buffer (Yosry).
- Move the existing synchronous and request context size checks into the Crypto API helper acomp_can_use_stack_req() (Yosry).
- Share callback setup through zswap_set_acomp_req_callback() and explain the two decompression paths (Yosry).
- Clarify in patch 1's commit message that codecs and drivers synchronize shared transform state internally (Yosry).
- Add Nhat Pham's Acked-by on patch 2.

Usama Arif (2):
mm: zswap: use separate compression and decompression requests
mm: zswap: use stack requests for synchronous decompression

 include/crypto/acompress.h |  13 +++
 mm/zswap.c                 | 161 ++++++++++++++++++++++++-------------
 2 files changed, 119 insertions(+), 55 deletions(-)
Signature · 1 line
2.53.0-Meta
author
#1 lore raw

PATCH v2 1/2 mm: zswap: use separate compression and decompression requests

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

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

---
 mm/zswap.c | 108 +++++++++++++++++++++++++++++++----------------------
 1 file changed, 64 insertions(+), 44 deletions(-)
mm/zswap.c +64 -44
index ae19e301fced7..23c4298765795 100644
@@ -137,14 +137,25 @@ bool zswap_never_enabled(void)
 * data structures
 **********************************/
 
-struct crypto_acomp_ctx {
-	struct crypto_acomp *acomp;
+struct zswap_acomp_req {
 	struct acomp_req *req;
 	struct crypto_wait wait;
-	u8 *buffer;
 	struct mutex mutex;
 };
 
+/* The compression mutex also protects the output buffer. */
+struct zswap_comp_ctx {
+	struct zswap_acomp_req areq;
+	u8 *buffer;
+};
+
+/* Separate requests, so that decompression does not wait for compression. */
+struct crypto_acomp_ctx {
+	struct crypto_acomp *acomp;
+	struct zswap_comp_ctx comp;
+	struct zswap_acomp_req decomp;
+};
+
 /*
  * The lock ordering is zswap_tree.lock -> zswap_pool.lru_lock.
  * The only case where lru_lock is not acquired while holding tree.lock is
@@ -270,14 +281,10 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)
 	if (!acomp_ctx)
 		return;
 
-	/*
-	 * If there was an error in allocating @acomp_ctx->req, it
-	 * would be set to NULL.
-	 */
-	if (acomp_ctx->req)
-		acomp_request_free(acomp_ctx->req);
-
-	acomp_ctx->req = NULL;
+	acomp_request_free(acomp_ctx->comp.areq.req);
+	acomp_ctx->comp.areq.req = NULL;
+	acomp_request_free(acomp_ctx->decomp.req);
+	acomp_ctx->decomp.req = NULL;
 
 	/*
 	 * We have to handle both cases here: an error pointer return from
@@ -289,8 +296,8 @@ static void acomp_ctx_free(struct crypto_acomp_ctx *acomp_ctx)
 
 	acomp_ctx->acomp = NULL;
 
-	kfree(acomp_ctx->buffer);
-	acomp_ctx->buffer = NULL;
+	kfree(acomp_ctx->comp.buffer);
+	acomp_ctx->comp.buffer = NULL;
 }
 
 static struct zswap_pool *zswap_pool_create(char *compressor)
@@ -796,6 +803,28 @@ static void zswap_entry_free(struct zswap_entry *entry)
 /*********************************
 * compressed storage functions
 **********************************/
+static int zswap_acomp_req_init(struct zswap_acomp_req *areq,
+				struct crypto_acomp *acomp)
+{
+	/* acomp_request_alloc() returns NULL in case of an error. */
+	areq->req = acomp_request_alloc(acomp);
+	if (!areq->req)
+		return -ENOMEM;
+
+	crypto_init_wait(&areq->wait);
+
+	/*
+	 * if the backend of acomp is async zip, crypto_req_done() will wakeup
+	 * crypto_wait_req(); if the backend of acomp is scomp, the callback
+	 * won't be called, crypto_wait_req() will return without blocking.
+	 */
+	acomp_request_set_callback(areq->req, CRYPTO_TFM_REQ_MAY_BACKLOG,
+				   crypto_req_done, &areq->wait);
+
+	mutex_init(&areq->mutex);
+	return 0;
+}
+
 static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 {
 	struct zswap_pool *pool = hlist_entry(node, struct zswap_pool, node);
@@ -811,8 +840,8 @@ 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)
+	acomp_ctx->comp.buffer = kmalloc_node(PAGE_SIZE, GFP_KERNEL, cpu_to_node(cpu));
+	if (!acomp_ctx->comp.buffer)
 		return ret;
 
 	/*
@@ -827,25 +856,13 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 		goto fail;
 	}
 
-	/* acomp_request_alloc() returns NULL in case of an error. */
-	acomp_ctx->req = acomp_request_alloc(acomp_ctx->acomp);
-	if (!acomp_ctx->req) {
+	if (zswap_acomp_req_init(&acomp_ctx->comp.areq, acomp_ctx->acomp) ||
+	    zswap_acomp_req_init(&acomp_ctx->decomp, acomp_ctx->acomp)) {
 		pr_err("could not alloc crypto acomp_request %s\n",
 		       pool->tfm_name);
 		goto fail;
 	}
 
-	crypto_init_wait(&acomp_ctx->wait);
-
-	/*
-	 * if the backend of acomp is async zip, crypto_req_done() will wakeup
-	 * crypto_wait_req(); if the backend of acomp is scomp, the callback
-	 * won't be called, crypto_wait_req() will return without blocking.
-	 */
-	acomp_request_set_callback(acomp_ctx->req, CRYPTO_TFM_REQ_MAY_BACKLOG,
-				   crypto_req_done, &acomp_ctx->wait);
-
-	mutex_init(&acomp_ctx->mutex);
 	return 0;
 
 fail:
@@ -856,7 +873,7 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
 static bool zswap_compress(struct folio *folio, long index,
 			   struct zswap_entry *entry, struct zswap_pool *pool)
 {
-	struct crypto_acomp_ctx *acomp_ctx;
+	struct zswap_comp_ctx *comp_ctx;
 	struct scatterlist input, output;
 	int comp_ret = 0, alloc_ret = 0;
 	unsigned int dlen = PAGE_SIZE;
@@ -865,15 +882,16 @@ static bool zswap_compress(struct folio *folio, long index,
 	u8 *dst;
 	bool mapped = false;
 
-	acomp_ctx = raw_cpu_ptr(pool->acomp_ctx);
-	mutex_lock(&acomp_ctx->mutex);
+	comp_ctx = &raw_cpu_ptr(pool->acomp_ctx)->comp;
+	mutex_lock(&comp_ctx->areq.mutex);
 
-	dst = acomp_ctx->buffer;
+	dst = comp_ctx->buffer;
 	sg_init_table(&input, 1);
 	sg_set_folio(&input, folio, PAGE_SIZE, index * PAGE_SIZE);
 
 	sg_init_one(&output, dst, PAGE_SIZE);
-	acomp_request_set_params(acomp_ctx->req, &input, &output, PAGE_SIZE, dlen);
+	acomp_request_set_params(comp_ctx->areq.req, &input, &output,
+				 PAGE_SIZE, dlen);
 
 	/*
 	 * it maybe looks a little bit silly that we send an asynchronous request,
@@ -885,10 +903,12 @@ static bool zswap_compress(struct folio *folio, long index,
 	 * 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.
+	 * acomp instance, and compression and decompression use separate
+	 * requests, so multiple threads can do (de)compression in parallel.
 	 */
-	comp_ret = crypto_wait_req(crypto_acomp_compress(acomp_ctx->req), &acomp_ctx->wait);
-	dlen = acomp_ctx->req->dlen;
+	comp_ret = crypto_wait_req(crypto_acomp_compress(comp_ctx->areq.req),
+				   &comp_ctx->areq.wait);
+	dlen = comp_ctx->areq.req->dlen;
 
 	/*
 	 * If a page cannot be compressed into a size smaller than PAGE_SIZE,
@@ -932,7 +952,7 @@ static bool zswap_compress(struct folio *folio, long index,
 	else if (alloc_ret)
 		zswap_reject_alloc_fail++;
 
-	mutex_unlock(&acomp_ctx->mutex);
+	mutex_unlock(&comp_ctx->areq.mutex);
 	return comp_ret == 0 && alloc_ret == 0;
 }
 
@@ -948,7 +968,7 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)
 		return false;
 
 	acomp_ctx = raw_cpu_ptr(pool->acomp_ctx);
-	mutex_lock(&acomp_ctx->mutex);
+	mutex_lock(&acomp_ctx->decomp.mutex);
 	zs_obj_read_sg_begin(pool->zs_pool, entry->handle, input, entry->length);
 
 	/* zswap entries of length PAGE_SIZE are not compressed. */
@@ -965,15 +985,15 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)
 	} else {
 		sg_init_table(&output, 1);
 		sg_set_folio(&output, folio, PAGE_SIZE, 0);
-		acomp_request_set_params(acomp_ctx->req, input, &output,
+		acomp_request_set_params(acomp_ctx->decomp.req, input, &output,
 					 entry->length, PAGE_SIZE);
-		ret = crypto_acomp_decompress(acomp_ctx->req);
-		ret = crypto_wait_req(ret, &acomp_ctx->wait);
-		dlen = acomp_ctx->req->dlen;
+		ret = crypto_acomp_decompress(acomp_ctx->decomp.req);
+		ret = crypto_wait_req(ret, &acomp_ctx->decomp.wait);
+		dlen = acomp_ctx->decomp.req->dlen;
 	}
 
 	zs_obj_read_sg_end(pool->zs_pool, entry->handle);
-	mutex_unlock(&acomp_ctx->mutex);
+	mutex_unlock(&acomp_ctx->decomp.mutex);
 
 	if (!ret && dlen == PAGE_SIZE)
 		return true;
Signature · 1 line
2.53.0-Meta
maintainer Acked-by on v2 1/2
#2 lore raw

Re: [PATCH v2 1/2] mm: zswap: use separate compression and decompression requests

Quoted 19 lines · Usama Arif

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

Quoted 6 lines

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

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

Herbert, does this also look good to you?

mm/zswap.c static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node) · line 860

Quoted 5 lines
---
 mm/zswap.c | 108 +++++++++++++++++++++++++++++++----------------------
 1 file changed, 64 insertions(+), 44 deletions(-)
mm/zswap.c +2 -0
+       if (zswap_acomp_req_init(&acomp_ctx->comp.areq, acomp_ctx->acomp) ||
+           zswap_acomp_req_init(&acomp_ctx->decomp, acomp_ctx->acomp)) {

This really breaks my brain...

#3 lore raw

Re: [PATCH v2 1/2] mm: zswap: use separate compression and decompression requests

In reply to Usama Arif
Quoted 19 lines · Usama Arif

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

Quoted 6 lines

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

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

I have a question with regards to this patch. Is the intent to allow a decompress thread to run concurrently with a store thread for a given process? If so, I wonder if incurring context switch/PTE/TLB shootdowns performance penalty is worth it?

With the proposed change, will zswap_store() continue to run on the "old" CPU (i.e. the one to which the process got migrated), or is it a requirement that the "old" process is in sleep state? If so, should we codify/document this requirement?

On the new CPU, is the same process "runnable", and if so, how do we ensure that the "higher priority" decompress is handled while the zswap_store() prior to preemption is still not done?

Thanks,
Kanchana

author
#4 lore raw

Re: [PATCH v2 1/2] mm: zswap: use separate compression and decompression requests

Quoted 23 lines · On Mon, 12 Oct 2026 17:21:40 -0700 "Kanchana P. Sridhar"…
Quoted 22 lines · Usama Arif

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

Quoted 6 lines
Quoted 3 lines

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

I have a question with regards to this patch. Is the intent to allow a decompress thread to run concurrently with a store thread for a given process?

The request is shared by tasks using the same CPU and zswap pool. These may be different processes or threads of the same process. Each operation still runs synchronously in its caller.

The motivating case can happen entirely on one CPU:
1. Task A compresses page X and releases the codec's stream lock.
2. A is preempted while still holding the zswap request mutex.
3. Task B runs and faults in an already stored page Y.

With the shared request, B blocks until A runs again and releases the mutex. Separate requests let B load Y while A's store is unfinished. The codec/driver still synchronizes its own shared state.

Quoted 2 lines

If so, I wonder if incurring context switch/PTE/TLB shootdowns performance penalty is worth it?

The switch to B already occurs with the existing code. This patch adds no worker threads, forced migration, PTE changes or TLB shootdowns. It avoids B's subsequent wait on A's request mutex. The 1-vCPU benchmark in the cover letter exercises this without CPU migration.

Quoted 6 lines

With the proposed change, will zswap_store() continue to run on the "old" CPU (i.e. the one to which the process got migrated), or is it a requirement that the "old" process is in sleep state? If so, should we codify/document this requirement?

There is no requirement for A to sleep. A can remain runnable while preempted. Its zswap_store() invocation resumes in the same task, on whichever CPU the scheduler selects. If it migrates after selecting the context, it retains that pointer and the corresponding mutex, as the existing code does. The same task cannot execute on two CPUs at once.

Quoted 3 lines

On the new CPU, is the same process "runnable", and if so, how do we ensure that the "higher priority" decompress is handled while the zswap_store() prior to preemption is still not done?

Normal scheduler policy decides when B runs. Once it runs, the split removes its dependency on A releasing the shared request mutex. A load starting on another CPU already selects a different context today; migration is unnecessary for the benefit described here.

Existing folio/swap-cache locking still orders accesses to the same page. The example above concerns two independent pages.

Thanks for the review!
Usama

Quoted 4 lines

Thanks,
Kanchana

#5 lore raw

Re: [PATCH v2 1/2] mm: zswap: use separate compression and decompression requests

Quoted 72 lines · Usama Arif
Quoted 23 lines · On Mon, 12 Oct 2026 17:21:40 -0700 "Kanchana P. Sridhar"…
Quoted 22 lines · Usama Arif

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

Quoted 6 lines
Quoted 3 lines

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

I have a question with regards to this patch. Is the intent to allow a decompress thread to run concurrently with a store thread for a given process?

The request is shared by tasks using the same CPU and zswap pool. These may be different processes or threads of the same process. Each operation still runs synchronously in its caller.

The motivating case can happen entirely on one CPU:
1. Task A compresses page X and releases the codec's stream lock.
2. A is preempted while still holding the zswap request mutex.
3. Task B runs and faults in an already stored page Y.

With the shared request, B blocks until A runs again and releases the mutex. Separate requests let B load Y while A's store is unfinished. The codec/driver still synchronizes its own shared state.

Quoted 2 lines

If so, I wonder if incurring context switch/PTE/TLB shootdowns performance penalty is worth it?

The switch to B already occurs with the existing code. This patch adds no worker threads, forced migration, PTE changes or TLB shootdowns. It avoids B's subsequent wait on A's request mutex. The 1-vCPU benchmark in the cover letter exercises this without CPU migration.

Quoted 6 lines

With the proposed change, will zswap_store() continue to run on the "old" CPU (i.e. the one to which the process got migrated), or is it a requirement that the "old" process is in sleep state? If so, should we codify/document this requirement?

There is no requirement for A to sleep. A can remain runnable while preempted. Its zswap_store() invocation resumes in the same task, on whichever CPU the scheduler selects. If it migrates after selecting the context, it retains that pointer and the corresponding mutex, as the existing code does. The same task cannot execute on two CPUs at once.

Quoted 3 lines

On the new CPU, is the same process "runnable", and if so, how do we ensure that the "higher priority" decompress is handled while the zswap_store() prior to preemption is still not done?

Quoted 7 lines

Normal scheduler policy decides when B runs. Once it runs, the split removes its dependency on A releasing the shared request mutex. A load starting on another CPU already selects a different context today; migration is unnecessary for the benefit described here.

Existing folio/swap-cache locking still orders accesses to the same page. The example above concerns two independent pages.

Thanks for explaining the motivation for this change, and the detailed analysis! Is it possible that this could cause compression contexts to be starved of compute resources under memory pressure, potentially causing deadlocks with page-faults in the worst-case scenario? Since compression is inherently slower than decompression, could this be making it easier for swap page-faults to preempt compression. On a related note, should direct reclaim (only) be allowed to use a non-preemptible context via a spinlock?

Quoted 8 lines

Thanks for the review!
Usama

Quoted 4 lines

Thanks,
Kanchana

author
#6 lore raw

Re: [PATCH v2 1/2] mm: zswap: use separate compression and decompression requests

Quoted 78 lines · Kanchana P. Sridhar
Quoted 77 lines · Usama Arif
Quoted 23 lines · On Mon, 12 Oct 2026 17:21:40 -0700 "Kanchana P. Sridhar"…
Quoted 22 lines · Usama Arif

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

Quoted 6 lines
Quoted 3 lines

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

I have a question with regards to this patch. Is the intent to allow a decompress thread to run concurrently with a store thread for a given process?

The request is shared by tasks using the same CPU and zswap pool. These may be different processes or threads of the same process. Each operation still runs synchronously in its caller.

The motivating case can happen entirely on one CPU:
1. Task A compresses page X and releases the codec's stream lock.
2. A is preempted while still holding the zswap request mutex.
3. Task B runs and faults in an already stored page Y.

With the shared request, B blocks until A runs again and releases the mutex. Separate requests let B load Y while A's store is unfinished. The codec/driver still synchronizes its own shared state.

Quoted 2 lines

If so, I wonder if incurring context switch/PTE/TLB shootdowns performance penalty is worth it?

The switch to B already occurs with the existing code. This patch adds no worker threads, forced migration, PTE changes or TLB shootdowns. It avoids B's subsequent wait on A's request mutex. The 1-vCPU benchmark in the cover letter exercises this without CPU migration.

Quoted 6 lines

With the proposed change, will zswap_store() continue to run on the "old" CPU (i.e. the one to which the process got migrated), or is it a requirement that the "old" process is in sleep state? If so, should we codify/document this requirement?

There is no requirement for A to sleep. A can remain runnable while preempted. Its zswap_store() invocation resumes in the same task, on whichever CPU the scheduler selects. If it migrates after selecting the context, it retains that pointer and the corresponding mutex, as the existing code does. The same task cannot execute on two CPUs at once.

Quoted 3 lines

On the new CPU, is the same process "runnable", and if so, how do we ensure that the "higher priority" decompress is handled while the zswap_store() prior to preemption is still not done?

Normal scheduler policy decides when B runs. Once it runs, the split removes its dependency on A releasing the shared request mutex. A load starting on another CPU already selects a different context today; migration is unnecessary for the benefit described here.

Quoted 7 lines
Quoted 2 lines

Existing folio/swap-cache locking still orders accesses to the same page. The example above concerns two independent pages.

Thanks for explaining the motivation for this change, and the detailed analysis! Is it possible that this could cause compression contexts to be starved of compute resources under memory pressure, potentially causing deadlocks with page-faults in the worst-case scenario?

The split removes an unnecessary dependency between unrelated pages. Under fair scheduling, runnable reclaimers still get CPU time, and a fault needing memory can perform direct reclaim itself. Faults on a page being stored still wait for its folio lock, so I don't see a new deadlock cycle here.

Quoted 3 lines

Since
compression is inherently slower than decompression, could this be making it easier for swap page-faults to preempt compression.

On non-PREEMPT_RT kernels, software compression already runs under the codec's stream spinlock with preemption disabled. The delay addressed here is after that lock is released; the split does not make active compression newly preemptible.

Quoted 4 lines

On a
related note, should direct reclaim (only) be allowed to use a non-preemptible context via a spinlock?

I would keep the current split. The codec already protects its stream, while the outer context must remain sleepable for asynchronous completion waits. A spinlock cannot safely cover those waits.

maintainer Acked-by on v2 1/2
#7 lore raw

Re: [PATCH v2 1/2] mm: zswap: use separate compression and decompression requests

In reply to Usama Arif
Quoted 21 lines · Usama Arif

Stores and loads serialize on the same per-CPU acomp request and mutex. A low-priority store can be preempted as soon as the compressor drops its stream lock, while it still holds the mutex. A higher-priority load on that CPU then waits until the store runs again, which can take a long time when other tasks are runnable.

Give compression and decompression their own request, completion wait and mutex. Since commit e2c3b6b21c77f ("mm: zswap: use SG list decompression APIs from zsmalloc"), the per-CPU buffer is only used for compression, so group it with the compression request. Independent requests can share the per-CPU transform; the codecs and drivers synchronize their shared state internally. Loads can still wait for each other on the decompression mutex, and stores still serialize on the compression mutex.

acomp_request_free() ignores NULL requests, so drop the check in acomp_ctx_free().

This follows the proposal from Sergey Senozhatsky for the same split for zram [1].

Quoted 6 lines

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

---
 mm/zswap.c | 108 +++++++++++++++++++++++++++++++----------------------
 1 file changed, 64 insertions(+), 44 deletions(-)

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
author
#8 lore raw

PATCH v2 2/2 mm: zswap: use stack requests for synchronous decompression

In reply to Usama Arif

With separate requests for compression and decompression, loads still serialize on the per-CPU decompression mutex. A low-priority load that is preempted after the codec drops its stream lock keeps holding the mutex and stalls every other load on that CPU, including higher-priority ones.

Synchronous algorithms whose requests need no extra context can use an on-stack request, so decompress with one and take no zswap lock. All in-tree software compressors qualify. Add acomp_can_use_stack_req() to keep the synchronous and request-size checks in the Crypto API. Asynchronous algorithms, and synchronous ones with request context, keep the per-CPU request and mutex, which is still taken before the zsmalloc read lock.

Reading the per-CPU context without the mutex is safe. Since commit ef3c0f6cb798e ("mm: zswap: tie per-CPU acomp_ctx lifetime to the pool"), it is set up before its CPU comes online and is not torn down until the pool is destroyed. The codecs keep their own stream locks, and crypto_acomp_decompress() rejects on-stack requests only for asynchronous transforms, which never take this path.

For software compressors this drops the heap request added by the previous patch. The on-stack request and wait take 216 bytes, which makes the load path about 270 bytes deeper on x86-64. Asynchronous algorithms pay this too.

---
 include/crypto/acompress.h | 13 ++++++
 mm/zswap.c                 | 87 ++++++++++++++++++++++++++------------
 2 files changed, 72 insertions(+), 28 deletions(-)
include/crypto/acompress.h +13 -0
index 5d5358dfab734..12c1ce1f6243b 100644
@@ -203,6 +203,19 @@ static inline bool acomp_is_async(struct crypto_acomp *tfm)
 	       CRYPTO_ALG_ASYNC;
 }
 
+/**
+ * acomp_can_use_stack_req() - check whether a transform can use stack requests
+ * @tfm: ACOMPRESS transform
+ *
+ * Return: true if @tfm completes synchronously and its request context fits
+ * in the storage reserved by ACOMP_REQUEST_ON_STACK(), false otherwise.
+ */
+static inline bool acomp_can_use_stack_req(struct crypto_acomp *tfm)
+{
+	return !acomp_is_async(tfm) &&
+	       crypto_acomp_reqsize(tfm) <= MAX_SYNC_COMP_REQSIZE;
+}
+
 static inline struct crypto_acomp *crypto_acomp_reqtfm(struct acomp_req *req)
 {
 	return __crypto_acomp_tfm(req->base.tfm);
mm/zswap.c +59 -28

141 lines not on this page · raw message

Signature · 1 line
2.53.0-Meta
maintainer
#9 lore raw

PATCH 1/1 crypto: acomp - Enforce MAX_SYNC_COMP_REQSIZE in init_tfm

Quoted 10 lines · Usama Arif
include/crypto/acompress.h +1 -0
index 5d5358dfab734..12c1ce1f6243b 100644
@@ -203,6 +203,19 @@ static inline bool acomp_is_async(struct crypto_acomp *tfm)
 	       CRYPTO_ALG_ASYNC;
 }
 
+/**

include/crypto/acompress.h static inline bool acomp_is_async(struct crypto_acomp *tfm) · line 216

Quoted 10 lines
include/crypto/acompress.h +10 -0
+ * acomp_can_use_stack_req() - check whether a transform can use stack requests
+ * @tfm: ACOMPRESS transform
+ *
+ * Return: true if @tfm completes synchronously and its request context fits
+ * in the storage reserved by ACOMP_REQUEST_ON_STACK(), false otherwise.
+ */
+static inline bool acomp_can_use_stack_req(struct crypto_acomp *tfm)
+{
+	return !acomp_is_async(tfm) &&
+	       crypto_acomp_reqsize(tfm) <= MAX_SYNC_COMP_REQSIZE;

I think we should enforce MAX_SYNC_COMP_REQSIZE in the Crypto API and then you can simply test against acomp_is_async(tfm).

Feel free to include this patch in your series:

---8<---
Enforce the limit MAX_SYNC_COMP_REQSIZE when an acomp tfm is initialised. All existing algorithms obey this limit.

crypto/acompress.c +9 -9
index 4de1a2ad577f..c8b4b8589769 100644
@@ -92,10 +92,6 @@ static int crypto_acomp_init_tfm(struct crypto_tfm *tfm)
 		if (IS_ERR(fb))
 			return PTR_ERR(fb);
 
-		err = -EINVAL;
-		if (crypto_acomp_reqsize(fb) > MAX_SYNC_COMP_REQSIZE)
-			goto out_free_fb;
-
 		tfm->fb = crypto_acomp_tfm(fb);
 	}
 
@@ -105,15 +101,19 @@ static int crypto_acomp_init_tfm(struct crypto_tfm *tfm)
 
 	acomp->base.exit = crypto_acomp_exit_tfm;
 
-	if (!alg->init)
-		return 0;
-
-	err = alg->init(acomp);
-	if (err)
+	if (alg->init && ((err = alg->init(acomp))))
 		goto out_free_fb;
 
+	err = -EINVAL;
+	if (!acomp_is_async(acomp) &&
+	    crypto_acomp_reqsize(acomp) > MAX_SYNC_COMP_REQSIZE)
+		goto out_exit_tfm;
+
 	return 0;
 
+out_exit_tfm:
+	if (alg->exit)
+		alg->exit(acomp);
 out_free_fb:
 	crypto_free_acomp(fb);
 	return err;
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