Skip to contentSkip to conversation

mm: fix reclaim storms in defrag_mode

PATCH In review Johannes Weiner 6 participants 20 messages Jul 22 – Jul 27

Subsystems (4)
lore
Share & tools
lore

Johannes Weiner

author reviewer
#0 lore raw

v2 cover mm: fix reclaim storms in defrag_mode

As we deployed vm.defrag_mode=1 in Meta production, some workloads regressed with recurring pressure spikes and swap storms (which in turn triggered userspace OOM rules on pressure and swap utilization levels).

Tracing pinned this to non-movable requests spinning and reclaiming unproductively when kswapd/kcompactd are overwhelmed. Direct reclaim predominantly frees up pages in movable blocks, but those requests cannot use that space under defrag_mode rules; and it is unlikely to free up whole blocks incidentally for __rmqueue_claim() to work.

This series fixes it by making non-movable requests participate in pageblock production in the allocator slowpath - meaning, they will invoke direct reclaim and direct compaction with pageblock_order.

That requires some small-ish adjustments up front in the allocator and the compaction code: three prep patches and the fix last.

The series has been in production against one of the affected workloads for several weeks and restores the OOM kill rate to !defrag_mode baseline.

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

Changes in v2:
- Patch 3 is now Vlastimil's version of the capture_control move: the allocator owns and arms the capture control entirely, and compaction just aims it at the zone being compacted.
- Comments on the migration source scan restrictions in patch 2, and spelled out "small" as <pageblock_order (Vlastimil, Zi Yan)
- Corrected the Fixes tag in patch 4 - the referenced SHA was from an old development branch (Zi Yan)
- Clarified in the cover letter what block production means (Shakeel)
- Added review/ack tags
- Cc stable on the fix (Vlastimil)
- Rebased onto mm-stable

Based on mm-stable (2026-07-06).

 include/linux/compaction.h |  3 +-
 mm/compaction.c            | 96 +++++++++++++++++++++++++---------------------
 mm/internal.h              | 10 ++++-
 mm/page_alloc.c            | 76 ++++++++++++++++++++++++++++++------
 4 files changed, 129 insertions(+), 56 deletions(-)
author reviewer
#1 lore raw

PATCH v2 1/4 mm: page_alloc: __GFP_FS lockdep annotation for direct compaction

A subsequent patch will have some order-0 allocations participate in compaction under defrag_mode, to stave off extfrag events.

Since this is a sprawling expansion of entry points, and compaction can enter filesystem paths, add lockdep annotations that catches __GFP_FS passing errors.

Direct reclaim has had this annotation for a while, and since reclaim and compaction are usually used in conjunction, this is unlikely to unearth old bugs. It's more about future proofing and peace of mind.

---
 mm/page_alloc.c | 2 ++
 1 file changed, 2 insertions(+)
mm/page_alloc.c +2 -0
index ee902a468c2f..cb422505c6ef 100644
@@ -4152,12 +4152,14 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 
 	psi_memstall_enter(&pflags);
 	delayacct_compact_start();
+	fs_reclaim_acquire(gfp_mask);
 	noreclaim_flag = memalloc_noreclaim_save();
 
 	*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
 								prio, &page);
 
 	memalloc_noreclaim_restore(noreclaim_flag);
+	fs_reclaim_release(gfp_mask);
 	psi_memstall_leave(&pflags);
 	delayacct_compact_end();
 
Signature · 1 line
2.55.0
author reviewer
#2 lore raw

PATCH v2 2/4 mm: compaction: support non-movable compaction for pageblock requests

In reply to Johannes Weiner

While trying to fix a reclaim storm in defrag_mode, I noticed that non-movable direct compaction is extremely inefficient.

When searching for space to evacuate, compaction only allows blocks of the same type as the incoming request. This is to prevent migratetype pollution, where a small non-movable request frees space in a movable block and provokes the allocator to fall back and pollute it.

This protection is reasonable on one hand, but the downside is that it makes non-movable direct compaction nearly useless: if we get the type annotations right, by definition there aren't any movable pages inside the non-movable blocks it is allowed to scan.

With defrag_mode, the goal is the production of whole blocks, which are essentially type neutral: __rmqueue_claim() will convert them wholesale on alloc. This makes type mixing and pollution a non-issue.

Fix the pollution gates to take the requested order into account, and allow whole-block requests to scan blocks of other types.

The only exception is CMA blocks. That type is sticky and these blocks cannot be claimed to other types. Continue to be strict with them, and allow only explicit ALLOC_CMA requests and kcompactd to evacuate them.

---
 mm/compaction.c | 46 +++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 39 insertions(+), 7 deletions(-)
mm/compaction.c +39 -7
index f08765ade014..225862a00380 100644
@@ -1381,12 +1381,44 @@ static bool suitable_migration_source(struct compact_control *cc,
 	if (pageblock_skip_persistent(page))
 		return false;
 
-	if ((cc->mode != MIGRATE_ASYNC) || !cc->direct_compaction)
+	/*
+	 * Background compaction produces blocks for the zone at
+	 * large, with no particular allocation context. Allow all
+	 * block types, including CMA.
+	 */
+	if (!cc->direct_compaction)
 		return true;
 
 	block_mt = get_pageblock_migratetype(page);
 
-	if (cc->migratetype == MIGRATE_MOVABLE)
+	/*
+	 * CMA pages can only be taken by ALLOC_CMA requests. For anybody
+	 * else, vacating a CMA block consumes free pages the caller
+	 * could have used, and produces free pages it cannot.
+	 */
+	if (is_migrate_cma(block_mt) && !(cc->alloc_flags & ALLOC_CMA))
+		return false;
+
+	/*
+	 * Per default, scans are restricted to blocks compatible with
+	 * the request, to prevent cross-contamination. Once
+	 * compaction priority escalates to synchronous scans, though,
+	 * scan all blocks to try to make forward progress. For
+	 * movable request, this likely helps little: there shouldn't
+	 * be many migratable pages inside non-movable blocks besides
+	 * allocator fallbacks. For non-movable requests, this helps a
+	 * lot, as they can finally scan movable blocks.
+	 */
+	if (cc->mode != MIGRATE_ASYNC)
+		return true;
+
+	/*
+	 * Prevent <pageblock_order unmovable/reclaimable requests from
+	 * polluting movable blocks through fallbacks. Whole-block production
+	 * (directly requested, or defrag_mode) is exempt as the allocator
+	 * claims and converts these.
+	 */
+	if (cc->migratetype == MIGRATE_MOVABLE || cc->order >= pageblock_order)
 		return is_migrate_movable(block_mt);
 	else
 		return block_mt == cc->migratetype;
@@ -1974,12 +2006,12 @@ static unsigned long fast_find_migrateblock(struct compact_control *cc)
 		return pfn;
 
 	/*
-	 * Only allow kcompactd and direct requests for movable pages to
-	 * quickly clear out a MOVABLE pageblock for allocation. This
-	 * reduces the risk that a large movable pageblock is freed for
-	 * an unmovable/reclaimable small allocation.
+	 * Prevent <pageblock_order unmovable/reclaimable requests from
+	 * polluting movable blocks through fallbacks. Whole-block production
+	 * is exempt as the allocator claims and converts these.
 	 */
-	if (cc->direct_compaction && cc->migratetype != MIGRATE_MOVABLE)
+	if (cc->direct_compaction && cc->migratetype != MIGRATE_MOVABLE &&
+	    cc->order < pageblock_order)
 		return pfn;
 
 	/*
Signature · 1 line
2.55.0
Reviewed-by on v2 2/4
#3 lore raw

Re: [PATCH v2 2/4] mm: compaction: support non-movable compaction for pageblock requests

Quoted 20 lines · Johannes Weiner

While trying to fix a reclaim storm in defrag_mode, I noticed that non-movable direct compaction is extremely inefficient.

When searching for space to evacuate, compaction only allows blocks of the same type as the incoming request. This is to prevent migratetype pollution, where a small non-movable request frees space in a movable block and provokes the allocator to fall back and pollute it.

This protection is reasonable on one hand, but the downside is that it makes non-movable direct compaction nearly useless: if we get the type annotations right, by definition there aren't any movable pages inside the non-movable blocks it is allowed to scan.

With defrag_mode, the goal is the production of whole blocks, which are essentially type neutral: __rmqueue_claim() will convert them wholesale on alloc. This makes type mixing and pollution a non-issue.

Fix the pollution gates to take the requested order into account, and allow whole-block requests to scan blocks of other types.

Quoted 6 lines

The only exception is CMA blocks. That type is sticky and these blocks cannot be claimed to other types. Continue to be strict with them, and allow only explicit ALLOC_CMA requests and kcompactd to evacuate them.

The CMA interaction took a second to grok but this makes sense.

author reviewer
#4 lore raw

PATCH v2 3/4 mm: page_alloc: move capture_control to the page allocator

In reply to Johannes Weiner

From: "Vlastimil Babka (SUSE)" <[email protected]>

The compaction capturing code assumes the allocation request order and compaction target order are the same. That won't be true once defrag_mode promotes sub-block allocations to pageblock-order
compaction: compaction targets the larger order, while capture should remain at the original allocation order.

Move the capture_control to the page allocator and give it its own copies of what the page freeing path matches against - zone, migratetype and the allocation order - rather than reaching into compaction's live compact_control. __alloc_pages_direct_compact() fills in migratetype and order, and installs and hides current->capture_control around the whole compaction call; try_to_compact_pages() aims capc->zone at each zone while it is being compacted. compact_zone_order() no longer deals with capture at all.

Pass the capture_control through try_to_compact_pages() / compact_zone_order() in place of the bare struct page **.

No functional change.

---
 include/linux/compaction.h |  3 ++-
 mm/compaction.c            | 50 +++++++++++---------------------------
 mm/internal.h              |  4 ++-
 mm/page_alloc.c            | 45 ++++++++++++++++++++++++++++------
 4 files changed, 57 insertions(+), 45 deletions(-)
include/linux/compaction.h +2 -1
index f29ef0653546..66a2f70e9e01 100644
@@ -58,6 +58,7 @@ enum compact_result {
 };
 
 struct alloc_context; /* in mm/internal.h */
+struct capture_control; /* in mm/internal.h */
 
 /*
  * Number of free order-0 pages that should be available above given watermark
@@ -92,7 +93,7 @@ extern int fragmentation_index(struct zone *zone, unsigned int order);
 extern enum compact_result try_to_compact_pages(gfp_t gfp_mask,
 		unsigned int order, unsigned int alloc_flags,
 		const struct alloc_context *ac, enum compact_priority prio,
-		struct page **page);
+		struct capture_control *capc);
 extern void reset_isolation_suitable(pg_data_t *pgdat);
 extern bool compaction_suitable(struct zone *zone, int order,
 				unsigned long watermark, int highest_zoneidx);
mm/compaction.c +14 -36

82 lines not on this page · raw message

mm/internal.h +3 -1
index 181e79f1d6a2..6d3402001b93 100644
@@ -1059,7 +1059,9 @@ struct compact_control {
  * immediately when one is created during the free path.
  */
 struct capture_control {
-	struct compact_control *cc;
+	struct zone *zone;
+	int migratetype;
+	int order;
 	struct page *page;
 };
 
mm/page_alloc.c +38 -7

78 lines not on this page · raw message

Signature · 1 line
2.55.0
Reviewed-by on v2 3/4
#5 lore raw

Re: [PATCH v2 3/4] mm: page_alloc: move capture_control to the page allocator

Quoted 32 lines · Johannes Weiner

From: "Vlastimil Babka (SUSE)" <[email protected]>

The compaction capturing code assumes the allocation request order and compaction target order are the same. That won't be true once defrag_mode promotes sub-block allocations to pageblock-order
compaction: compaction targets the larger order, while capture should remain at the original allocation order.

Move the capture_control to the page allocator and give it its own copies of what the page freeing path matches against - zone, migratetype and the allocation order - rather than reaching into compaction's live compact_control. __alloc_pages_direct_compact() fills in migratetype and order, and installs and hides current->capture_control around the whole compaction call; try_to_compact_pages() aims capc->zone at each zone while it is being compacted. compact_zone_order() no longer deals with capture at all.

Pass the capture_control through try_to_compact_pages() / compact_zone_order() in place of the bare struct page **.

No functional change.

---
 include/linux/compaction.h |  3 ++-
 mm/compaction.c            | 50 +++++++++++---------------------------
 mm/internal.h              |  4 ++-
 mm/page_alloc.c            | 45 ++++++++++++++++++++++++++++------
 4 files changed, 57 insertions(+), 45 deletions(-)

... snip ...

mm/compaction.c enum compact_result try_to_compact_pages(gfp_t gfp_mask, unsigned int order, · line 2875

Quoted 12 lines
mm/compaction.c +10 -1
+		WRITE_ONCE(capc->zone, zone);
+
 		status = compact_zone_order(zone, order, gfp_mask, prio,
-				alloc_flags, ac->highest_zoneidx, capture);
+				alloc_flags, ac->highest_zoneidx, capc);
+
+		WRITE_ONCE(capc->zone, NULL);
+
+		/* Stop if a page has been captured */
+		if (READ_ONCE(capc->page))
+			status = COMPACT_SUCCESS;
+

Might be worth a comment to explain what the WRITE/READ once is dealing with here since it's now detached from the main barrier(), but otherwise

author reviewer
#6 lore raw

PATCH v2 4/4 mm: page_alloc: fix non-movable reclaim storm in defrag_mode

In reply to Johannes Weiner

As we deployed defrag_mode into Meta production, pressure spikes and excessive swapping were observed on some workloads. Tracing confirmed that this is unmovable/reclaimable requests spinning in the allocator and direct reclaim, causing excessive amounts of swap.

The initial plan for defrag_mode was to rely on kswapd/kcompactd to produce blocks, and if those are overwhelmed under high pressure, let the allocator fall back (__rmqueue_steal()) after its retry loops. However, that retrying results in more reclaim on some of these workloads than we'd hoped, sometimes excessively so, spurred on by the !costly order conditions in should_reclaim_retry().

The storms are dependent on the request type. Reclaim will inevitably make room in existing movable blocks, since that's where the LRU pages live. So if movable requests retry on reclaim, they make progress.

When non-movable requests spin in reclaim that isn't productive. They cannot use the individually freed pages, and the process is unlikely to accidentally free whole blocks to meet the ALLOC_NOFRAGMENT bar. They spin and overreclaim excessively, which tanks performance and triggers userspace guards like swap exhaustion or pressure based OOM.

To fix this, send non-movable requests, regardless of order, into pageblock reclaim/compaction. This way, they help move things along to meet the ALLOC_NOFRAGMENT bar. After this patch, the reclaim storms and excess OOM rates are no longer observed in production.

The longer-term plan is still to have all requests, including the movable ones, help make blocks to spread the cost of defragmenting more evenly and fairly; combined with proper watermarking to reduce allocation latencies in the common case. However, doing this naively unearths scaling and concurrency limitations in compaction that need to be addressed first. Promoting just non-movables for now is the minimally viable bug fix for the above issue.

Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")
---
 mm/internal.h   |  6 ++++++
 mm/page_alloc.c | 31 ++++++++++++++++++++++++++-----
 2 files changed, 32 insertions(+), 5 deletions(-)
mm/internal.h +6 -0
index 6d3402001b93..5acba3470659 100644
@@ -1061,6 +1061,12 @@ struct compact_control {
 struct capture_control {
 	struct zone *zone;
 	int migratetype;
+	/*
+	 * Allocation request order. May differ from the compaction
+	 * order: defrag_mode promotes sub-block allocations to
+	 * pageblock-order compaction; capture still matches at the
+	 * original allocation order so prep_new_page() is consistent.
+	 */
 	int order;
 	struct page *page;
 };
mm/page_alloc.c +26 -5
index 4b9686fedb71..f92055827ae9 100644
@@ -4152,8 +4152,24 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 		.order = order,
 		.page = NULL,
 	};
+	int compact_order = order;
 
-	if (!order)
+	/*
+	 * If fallbacks are not permitted (defrag_mode), we either
+	 * need to reclaim space in a block of matching type, or clear
+	 * out an entire block to allow __rmqueue_claim() to convert.
+	 *
+	 * Reclaim by itself is primarily freeing space in movable
+	 * blocks, since that's where the LRU pages live. So this
+	 * works for movable requests, but not for others.
+	 *
+	 * For those, promote the order to help make blocks, instead
+	 * of spinning in reclaim alone unproductively.
+	 */
+	if ((alloc_flags & ALLOC_NOFRAGMENT) && ac->migratetype != MIGRATE_MOVABLE)
+		compact_order = max(order, pageblock_order);
+
+	if (!compact_order)
 		return NULL;
 
 	psi_memstall_enter(&pflags);
@@ -4169,8 +4185,8 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 	barrier();
 	WRITE_ONCE(current->capture_control, &capc);
 
-	*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
-							       prio, &capc);
+	*compact_result = try_to_compact_pages(gfp_mask, compact_order,
+					       alloc_flags, ac, prio, &capc);
 
 	/*
 	 * Make sure we hide capture control first before we read the captured
@@ -4215,7 +4231,7 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 		struct zone *zone = page_zone(page);
 
 		zone->compact_blockskip_flush = false;
-		compaction_defer_reset(zone, order, true);
+		compaction_defer_reset(zone, compact_order, true);
 		count_vm_event(COMPACTSUCCESS);
 		return page;
 	}
@@ -4455,9 +4471,14 @@ __alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
 	struct page *page = NULL;
 	unsigned long pflags;
 	bool drained = false;
+	int reclaim_order = order;
+
+	/* Match the slowpath compaction promotion in __alloc_pages_direct_compact */
+	if ((alloc_flags & ALLOC_NOFRAGMENT) && ac->migratetype != MIGRATE_MOVABLE)
+		reclaim_order = max(order, pageblock_order);
 
 	psi_memstall_enter(&pflags);
-	*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
+	*did_some_progress = __perform_reclaim(gfp_mask, reclaim_order, ac);
 	if (unlikely(!(*did_some_progress)))
 		goto out;
 
Signature · 1 line
2.55.0
#7 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 35 lines · Johannes Weiner

As we deployed defrag_mode into Meta production, pressure spikes and excessive swapping were observed on some workloads. Tracing confirmed that this is unmovable/reclaimable requests spinning in the allocator and direct reclaim, causing excessive amounts of swap.

The initial plan for defrag_mode was to rely on kswapd/kcompactd to produce blocks, and if those are overwhelmed under high pressure, let the allocator fall back (__rmqueue_steal()) after its retry loops. However, that retrying results in more reclaim on some of these workloads than we'd hoped, sometimes excessively so, spurred on by the !costly order conditions in should_reclaim_retry().

The storms are dependent on the request type. Reclaim will inevitably make room in existing movable blocks, since that's where the LRU pages live. So if movable requests retry on reclaim, they make progress.

When non-movable requests spin in reclaim that isn't productive. They cannot use the individually freed pages, and the process is unlikely to accidentally free whole blocks to meet the ALLOC_NOFRAGMENT bar. They spin and overreclaim excessively, which tanks performance and triggers userspace guards like swap exhaustion or pressure based OOM.

To fix this, send non-movable requests, regardless of order, into pageblock reclaim/compaction. This way, they help move things along to meet the ALLOC_NOFRAGMENT bar. After this patch, the reclaim storms and excess OOM rates are no longer observed in production.

The longer-term plan is still to have all requests, including the movable ones, help make blocks to spread the cost of defragmenting more evenly and fairly; combined with proper watermarking to reduce allocation latencies in the common case. However, doing this naively unearths scaling and concurrency limitations in compaction that need to be addressed first. Promoting just non-movables for now is the minimally viable bug fix for the above issue.

Quoted 3 lines
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")

Just a note that I'm rebasing my ALLOC_UNMAPPED series onto this, per the discusison here I have similar needs re making compaction run on orders above the allocation order:

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

I think the way you've done it (i.e. use the capture_control for the "other" order) is good.

maintainer
#8 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

In reply to Johannes Weiner
Quoted 35 lines · On Wed, 22 Jul 2026 10:56:47 -0400 Johannes Weiner <[email protected]> wrote:

As we deployed defrag_mode into Meta production, pressure spikes and excessive swapping were observed on some workloads. Tracing confirmed that this is unmovable/reclaimable requests spinning in the allocator and direct reclaim, causing excessive amounts of swap.

The initial plan for defrag_mode was to rely on kswapd/kcompactd to produce blocks, and if those are overwhelmed under high pressure, let the allocator fall back (__rmqueue_steal()) after its retry loops. However, that retrying results in more reclaim on some of these workloads than we'd hoped, sometimes excessively so, spurred on by the !costly order conditions in should_reclaim_retry().

The storms are dependent on the request type. Reclaim will inevitably make room in existing movable blocks, since that's where the LRU pages live. So if movable requests retry on reclaim, they make progress.

When non-movable requests spin in reclaim that isn't productive. They cannot use the individually freed pages, and the process is unlikely to accidentally free whole blocks to meet the ALLOC_NOFRAGMENT bar. They spin and overreclaim excessively, which tanks performance and triggers userspace guards like swap exhaustion or pressure based OOM.

To fix this, send non-movable requests, regardless of order, into pageblock reclaim/compaction. This way, they help move things along to meet the ALLOC_NOFRAGMENT bar. After this patch, the reclaim storms and excess OOM rates are no longer observed in production.

The longer-term plan is still to have all requests, including the movable ones, help make blocks to spread the cost of defragmenting more evenly and fairly; combined with proper watermarking to reduce allocation latencies in the common case. However, doing this naively unearths scaling and concurrency limitations in compaction that need to be addressed first. Promoting just non-movables for now is the minimally viable bug fix for the above issue.

Quoted 3 lines
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")

This cc:stable fix doesn't apply to current mainline because the preceding non-cc:stable patches get in the way.

This means that the -stable maintainers will have to ask you to redo the fix to make it backportable. And you'll then be preparing a patch which hasn't had standalone testing in mainline. All very cumbersome.

So is it practical to make the fix come first, as a cc:stable hotfix so we can upstream the other patches in the next merge window?

(An alternative would be to put cc:stable on all four patches. That's a bit abusive but we do it rarely).

Either way, I'll get these into mm.git for testing while we await review on [4/4], thanks.

#9 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

+ Greg and Sasha

Quoted 7 lines · Andrew Morton
Quoted 4 lines · On Wed, 22 Jul 2026 10:56:47 -0400 Johannes Weiner <[email protected]> wrote:
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")
Quoted 6 lines

This cc:stable fix doesn't apply to current mainline because the preceding non-cc:stable patches get in the way.

This means that the -stable maintainers will have to ask you to redo the fix to make it backportable. And you'll then be preparing a patch which hasn't had standalone testing in mainline. All very cumbersome.

Or they backport the whole series, even if only 4/4 has a Fixes: tag.

Quoted 2 lines

So is it practical to make the fix come first, as a cc:stable hotfix so we can upstream the other patches in the next merge window?

I think it can't come first, it's simply a complex fix that can't be done in a single patch that would be reviewable. 3/4 is certainly a strict dependency. I'm not sure if 2/4 as well. Perhaps 2/4 does deserve a Fixes:
tag and Cc: stable on its own, as it also fixes a defrag_mode deficiency.

Quoted 2 lines

(An alternative would be to put cc:stable on all four patches. That's a bit abusive but we do it rarely).

I don't think it's necessary, the stable team are quite capable of figuring out the deps and backporting them with a Stable-dep-off: tag. IIRC the "fake" tagging is discouraged? Maybe they can clarify.

Quoted 3 lines

Either way, I'll get these into mm.git for testing while we await review on [4/4], thanks.

author reviewer
#10 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 19 lines · Vlastimil Babka (SUSE)

+ Greg and Sasha

Quoted 7 lines · Andrew Morton
Quoted 4 lines · On Wed, 22 Jul 2026 10:56:47 -0400 Johannes Weiner <[email protected]> wrote:
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")
Quoted 6 lines

This cc:stable fix doesn't apply to current mainline because the preceding non-cc:stable patches get in the way.

This means that the -stable maintainers will have to ask you to redo the fix to make it backportable. And you'll then be preparing a patch which hasn't had standalone testing in mainline. All very cumbersome.

Or they backport the whole series, even if only 4/4 has a Fixes: tag.

Quoted 7 lines
Quoted 2 lines

So is it practical to make the fix come first, as a cc:stable hotfix so we can upstream the other patches in the next merge window?

I think it can't come first, it's simply a complex fix that can't be done in a single patch that would be reviewable. 3/4 is certainly a strict dependency. I'm not sure if 2/4 as well. Perhaps 2/4 does deserve a Fixes:
tag and Cc: stable on its own, as it also fixes a defrag_mode deficiency.

Yes. Only 1/4 could be argued away.

2/4 actually came after 4/4 during development. Without it, compaction cannot make sufficient forward progress on the promoted non-movable requests. So it is a hard functional requirement.

Whether it deserves an independent Fixes: depends on how many GFP_KERNEL pageblock_order requests we have :-) I'm guessing not many.

Quoted 6 lines
Quoted 2 lines

(An alternative would be to put cc:stable on all four patches. That's a bit abusive but we do it rarely).

I don't think it's necessary, the stable team are quite capable of figuring out the deps and backporting them with a Stable-dep-off: tag. IIRC the "fake" tagging is discouraged? Maybe they can clarify.

Yes, I *think* it should work with tagging just 4/4.

It won't apply cleanly or build without 3/4. So I'd assume if that dependency is lost, the automated backport will bounce and I can send a backport series by hand.

#11 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 28 lines · Johannes Weiner
Quoted 19 lines · Vlastimil Babka (SUSE)

+ Greg and Sasha

Quoted 7 lines · Andrew Morton
Quoted 4 lines · On Wed, 22 Jul 2026 10:56:47 -0400 Johannes Weiner <[email protected]> wrote:
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")
Quoted 6 lines

This cc:stable fix doesn't apply to current mainline because the preceding non-cc:stable patches get in the way.

This means that the -stable maintainers will have to ask you to redo the fix to make it backportable. And you'll then be preparing a patch which hasn't had standalone testing in mainline. All very cumbersome.

Or they backport the whole series, even if only 4/4 has a Fixes: tag.

Quoted 7 lines
Quoted 2 lines

So is it practical to make the fix come first, as a cc:stable hotfix so we can upstream the other patches in the next merge window?

I think it can't come first, it's simply a complex fix that can't be done in a single patch that would be reviewable. 3/4 is certainly a strict dependency. I'm not sure if 2/4 as well. Perhaps 2/4 does deserve a Fixes:
tag and Cc: stable on its own, as it also fixes a defrag_mode deficiency.

Quoted 5 lines

Yes. Only 1/4 could be argued away.

2/4 actually came after 4/4 during development. Without it, compaction cannot make sufficient forward progress on the promoted non-movable requests. So it is a hard functional requirement.

So that's the tricky part then. If it's a hard functional requirement for 4/4 and yet leaving it out results in no conflicts when applying 3/4 and 4/4 (as it indeed seems so, based on my local test), then it's quite possible stable folks will figure out themselves they need 3/4 as a stable-dep-off:
4/4 but won't backport 2/4, unless you act quickly enough on their mails.

Quoted 2 lines

Whether it deserves an independent Fixes: depends on how many GFP_KERNEL pageblock_order requests we have :-) I'm guessing not many.

I don't know how else to indicate it though. It's kind of a co-fix with 4/4?

Quoted 7 lines
Quoted 6 lines
Quoted 2 lines

(An alternative would be to put cc:stable on all four patches. That's a bit abusive but we do it rarely).

I don't think it's necessary, the stable team are quite capable of figuring out the deps and backporting them with a Stable-dep-off: tag. IIRC the "fake" tagging is discouraged? Maybe they can clarify.

Quoted 5 lines

Yes, I *think* it should work with tagging just 4/4.

It won't apply cleanly or build without 3/4. So I'd assume if that dependency is lost, the automated backport will bounce and I can send a backport series by hand.

The way I see if often happen is that Greg sends a mail that it bounced and soon after Sasha replies with fixed up version/series. Which could very easily be 3/4 + 4/4 without 2/4.

maintainer
#12 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 24 lines · On Thu, 23 Jul 2026 16:17:51 +0200 "Vlastimil Babka (SUSE)"…
Quoted 3 lines

2/4 actually came after 4/4 during development. Without it, compaction cannot make sufficient forward progress on the promoted non-movable requests. So it is a hard functional requirement.

So that's the tricky part then. If it's a hard functional requirement for 4/4 and yet leaving it out results in no conflicts when applying 3/4 and 4/4 (as it indeed seems so, based on my local test), then it's quite possible stable folks will figure out themselves they need 3/4 as a stable-dep-off:
4/4 but won't backport 2/4, unless you act quickly enough on their mails.

Quoted 2 lines

Whether it deserves an independent Fixes: depends on how many GFP_KERNEL pageblock_order requests we have :-) I'm guessing not many.

I don't know how else to indicate it though. It's kind of a co-fix with 4/4?

Quoted 9 lines
Quoted 6 lines
Quoted 2 lines

(An alternative would be to put cc:stable on all four patches. That's a bit abusive but we do it rarely).

I don't think it's necessary, the stable team are quite capable of figuring out the deps and backporting them with a Stable-dep-off: tag. IIRC the "fake" tagging is discouraged? Maybe they can clarify.

Yes, I *think* it should work with tagging just 4/4.

Quoted 7 lines
Quoted 3 lines

It won't apply cleanly or build without 3/4. So I'd assume if that dependency is lost, the automated backport will bounce and I can send a backport series by hand.

The way I see if often happen is that Greg sends a mail that it bounced and soon after Sasha replies with fixed up version/series. Which could very easily be 3/4 + 4/4 without 2/4.

My brain hurts. I'll include

Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")

in all four patches.

#13 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 34 lines · Andrew Morton
Quoted 24 lines · On Thu, 23 Jul 2026 16:17:51 +0200 "Vlastimil Babka (SUSE)"…
Quoted 3 lines

2/4 actually came after 4/4 during development. Without it, compaction cannot make sufficient forward progress on the promoted non-movable requests. So it is a hard functional requirement.

So that's the tricky part then. If it's a hard functional requirement for 4/4 and yet leaving it out results in no conflicts when applying 3/4 and 4/4 (as it indeed seems so, based on my local test), then it's quite possible stable folks will figure out themselves they need 3/4 as a stable-dep-off:
4/4 but won't backport 2/4, unless you act quickly enough on their mails.

Quoted 2 lines

Whether it deserves an independent Fixes: depends on how many GFP_KERNEL pageblock_order requests we have :-) I'm guessing not many.

I don't know how else to indicate it though. It's kind of a co-fix with 4/4?

Quoted 9 lines
Quoted 6 lines
Quoted 2 lines

(An alternative would be to put cc:stable on all four patches. That's a bit abusive but we do it rarely).

I don't think it's necessary, the stable team are quite capable of figuring out the deps and backporting them with a Stable-dep-off: tag. IIRC the "fake" tagging is discouraged? Maybe they can clarify.

Yes, I *think* it should work with tagging just 4/4.

Quoted 7 lines
Quoted 3 lines

It won't apply cleanly or build without 3/4. So I'd assume if that dependency is lost, the automated backport will bounce and I can send a backport series by hand.

The way I see if often happen is that Greg sends a mail that it bounced and soon after Sasha replies with fixed up version/series. Which could very easily be 3/4 + 4/4 without 2/4.

Quoted 6 lines

My brain hurts. I'll include

Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")

in all four patches.

Thank you! Don't make us "figure it out" as that way lies struggles and time delays and the huge chance I get it wrong. Tagging them all ensures none of this happens and it all "just works".

thanks,

greg k-h

maintainer
#14 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

In reply to Vlastimil Babka (SUSE)
Quoted 10 lines · On Thu, 23 Jul 2026 12:22:58 +0200 "Vlastimil Babka (SUSE)"…

+ Greg and Sasha

Quoted 7 lines · Andrew Morton
Quoted 4 lines · On Wed, 22 Jul 2026 10:56:47 -0400 Johannes Weiner <[email protected]> wrote:
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")
Quoted 8 lines
Quoted 6 lines

This cc:stable fix doesn't apply to current mainline because the preceding non-cc:stable patches get in the way.

This means that the -stable maintainers will have to ask you to redo the fix to make it backportable. And you'll then be preparing a patch which hasn't had standalone testing in mainline. All very cumbersome.

Or they backport the whole series, even if only 4/4 has a Fixes: tag.

Sure.

Quoted 2 lines

I don't think it's necessary, the stable team are quite capable of figuring out the deps and backporting them with a Stable-dep-off: tag.

What's that.

hp2:/usr/src/25> grep -ri stable-dep ../gitlog Documentation hp2:/usr/src/25>

Quoted 2 lines

IIRC the
"fake" tagging is discouraged? Maybe they can clarify.

maintainer Reviewed-by on v2 4/4
#15 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

In reply to Johannes Weiner
Quoted 35 lines · Johannes Weiner

As we deployed defrag_mode into Meta production, pressure spikes and excessive swapping were observed on some workloads. Tracing confirmed that this is unmovable/reclaimable requests spinning in the allocator and direct reclaim, causing excessive amounts of swap.

The initial plan for defrag_mode was to rely on kswapd/kcompactd to produce blocks, and if those are overwhelmed under high pressure, let the allocator fall back (__rmqueue_steal()) after its retry loops. However, that retrying results in more reclaim on some of these workloads than we'd hoped, sometimes excessively so, spurred on by the !costly order conditions in should_reclaim_retry().

The storms are dependent on the request type. Reclaim will inevitably make room in existing movable blocks, since that's where the LRU pages live. So if movable requests retry on reclaim, they make progress.

When non-movable requests spin in reclaim that isn't productive. They cannot use the individually freed pages, and the process is unlikely to accidentally free whole blocks to meet the ALLOC_NOFRAGMENT bar. They spin and overreclaim excessively, which tanks performance and triggers userspace guards like swap exhaustion or pressure based OOM.

To fix this, send non-movable requests, regardless of order, into pageblock reclaim/compaction. This way, they help move things along to meet the ALLOC_NOFRAGMENT bar. After this patch, the reclaim storms and excess OOM rates are no longer observed in production.

The longer-term plan is still to have all requests, including the movable ones, help make blocks to spread the cost of defragmenting more evenly and fairly; combined with proper watermarking to reduce allocation latencies in the common case. However, doing this naively unearths scaling and concurrency limitations in compaction that need to be addressed first. Promoting just non-movables for now is the minimally viable bug fix for the above issue.

Quoted 3 lines
Fixes e3aa7df331bc ("mm: page_alloc: defrag_mode")
Quoted 10 lines
---
 mm/internal.h   |  6 ++++++
 mm/page_alloc.c | 31 ++++++++++++++++++++++++++-----
 2 files changed, 32 insertions(+), 5 deletions(-)
mm/internal.h +0 -0
index 6d3402001b93..5acba3470659 100644

mm/internal.h struct compact_control { · line 1069

Quoted 9 lines
mm/internal.h +6 -0
@@ -1061,6 +1061,12 @@ struct compact_control {
 struct capture_control {
 	struct zone *zone;
 	int migratetype;
+	/*
+	 * Allocation request order. May differ from the compaction
+	 * order: defrag_mode promotes sub-block allocations to
+	 * pageblock-order compaction; capture still matches at the
+	 * original allocation order so prep_new_page() is consistent.
+	 */

Yeah, a premature capture can't jeopardize the full-pageblock clearing because compaction_capture() disallows capturing <pageblock_order page from MOVABLE pageblock for !MOVABLE allocations.

It's thus unlikely the capture will ever succeed in these scenarios. But it's not wrong.

Quoted 69 lines
mm/internal.h +0 -0
 	int order;
 	struct page *page;
 };
mm/page_alloc.c +26 -5
index 4b9686fedb71..f92055827ae9 100644
@@ -4152,8 +4152,24 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 		.order = order,
 		.page = NULL,
 	};
+	int compact_order = order;
 
-	if (!order)
+	/*
+	 * If fallbacks are not permitted (defrag_mode), we either
+	 * need to reclaim space in a block of matching type, or clear
+	 * out an entire block to allow __rmqueue_claim() to convert.
+	 *
+	 * Reclaim by itself is primarily freeing space in movable
+	 * blocks, since that's where the LRU pages live. So this
+	 * works for movable requests, but not for others.
+	 *
+	 * For those, promote the order to help make blocks, instead
+	 * of spinning in reclaim alone unproductively.
+	 */
+	if ((alloc_flags & ALLOC_NOFRAGMENT) && ac->migratetype != MIGRATE_MOVABLE)
+		compact_order = max(order, pageblock_order);
+
+	if (!compact_order)
 		return NULL;
 
 	psi_memstall_enter(&pflags);
@@ -4169,8 +4185,8 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 	barrier();
 	WRITE_ONCE(current->capture_control, &capc);
 
-	*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
-							       prio, &capc);
+	*compact_result = try_to_compact_pages(gfp_mask, compact_order,
+					       alloc_flags, ac, prio, &capc);
 
 	/*
 	 * Make sure we hide capture control first before we read the captured
@@ -4215,7 +4231,7 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 		struct zone *zone = page_zone(page);
 
 		zone->compact_blockskip_flush = false;
-		compaction_defer_reset(zone, order, true);
+		compaction_defer_reset(zone, compact_order, true);
 		count_vm_event(COMPACTSUCCESS);
 		return page;
 	}
@@ -4455,9 +4471,14 @@ __alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
 	struct page *page = NULL;
 	unsigned long pflags;
 	bool drained = false;
+	int reclaim_order = order;
+
+	/* Match the slowpath compaction promotion in __alloc_pages_direct_compact */
+	if ((alloc_flags & ALLOC_NOFRAGMENT) && ac->migratetype != MIGRATE_MOVABLE)
+		reclaim_order = max(order, pageblock_order);
 
 	psi_memstall_enter(&pflags);
-	*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
+	*did_some_progress = __perform_reclaim(gfp_mask, reclaim_order, ac);
 	if (unlikely(!(*did_some_progress)))
 		goto out;
 
#16 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

In reply to Johannes Weiner
Quoted 27 lines · Johannes Weiner

As we deployed defrag_mode into Meta production, pressure spikes and excessive swapping were observed on some workloads. Tracing confirmed that this is unmovable/reclaimable requests spinning in the allocator and direct reclaim, causing excessive amounts of swap.

The initial plan for defrag_mode was to rely on kswapd/kcompactd to produce blocks, and if those are overwhelmed under high pressure, let the allocator fall back (__rmqueue_steal()) after its retry loops. However, that retrying results in more reclaim on some of these workloads than we'd hoped, sometimes excessively so, spurred on by the !costly order conditions in should_reclaim_retry().

The storms are dependent on the request type. Reclaim will inevitably make room in existing movable blocks, since that's where the LRU pages live. So if movable requests retry on reclaim, they make progress.

When non-movable requests spin in reclaim that isn't productive. They cannot use the individually freed pages, and the process is unlikely to accidentally free whole blocks to meet the ALLOC_NOFRAGMENT bar. They spin and overreclaim excessively, which tanks performance and triggers userspace guards like swap exhaustion or pressure based OOM.

To fix this, send non-movable requests, regardless of order, into pageblock reclaim/compaction. This way, they help move things along to meet the ALLOC_NOFRAGMENT bar. After this patch, the reclaim storms and excess OOM rates are no longer observed in production.

Quoted 7 lines

The longer-term plan is still to have all requests, including the movable ones, help make blocks to spread the cost of defragmenting more evenly and fairly; combined with proper watermarking to reduce allocation latencies in the common case. However, doing this naively unearths scaling and concurrency limitations in compaction that need to be addressed first. Promoting just non-movables for now is the minimally viable bug fix for the above issue.

Please forgive the noob question, I'm still struggling to get a really good mental handle on this stuff. But what about compact_first here? When we're promoting the order for compaction does it also make sense to promote compaction itself?

I'm aware you said "minimally viable bug fix" so it's fine if this falls outside of that, I'm just trying to poke around to improve my understanding.

author reviewer
#17 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 36 lines · Brendan Jackman
Quoted 27 lines · Johannes Weiner

As we deployed defrag_mode into Meta production, pressure spikes and excessive swapping were observed on some workloads. Tracing confirmed that this is unmovable/reclaimable requests spinning in the allocator and direct reclaim, causing excessive amounts of swap.

The initial plan for defrag_mode was to rely on kswapd/kcompactd to produce blocks, and if those are overwhelmed under high pressure, let the allocator fall back (__rmqueue_steal()) after its retry loops. However, that retrying results in more reclaim on some of these workloads than we'd hoped, sometimes excessively so, spurred on by the !costly order conditions in should_reclaim_retry().

The storms are dependent on the request type. Reclaim will inevitably make room in existing movable blocks, since that's where the LRU pages live. So if movable requests retry on reclaim, they make progress.

When non-movable requests spin in reclaim that isn't productive. They cannot use the individually freed pages, and the process is unlikely to accidentally free whole blocks to meet the ALLOC_NOFRAGMENT bar. They spin and overreclaim excessively, which tanks performance and triggers userspace guards like swap exhaustion or pressure based OOM.

To fix this, send non-movable requests, regardless of order, into pageblock reclaim/compaction. This way, they help move things along to meet the ALLOC_NOFRAGMENT bar. After this patch, the reclaim storms and excess OOM rates are no longer observed in production.

Quoted 7 lines

The longer-term plan is still to have all requests, including the movable ones, help make blocks to spread the cost of defragmenting more evenly and fairly; combined with proper watermarking to reduce allocation latencies in the common case. However, doing this naively unearths scaling and concurrency limitations in compaction that need to be addressed first. Promoting just non-movables for now is the minimally viable bug fix for the above issue.

Quoted 4 lines

Please forgive the noob question, I'm still struggling to get a really good mental handle on this stuff. But what about compact_first here? When we're promoting the order for compaction does it also make sense to promote compaction itself?

My understanding is that this is just an optimization for requests that are more likely shape-limited (block contiguity) rather than capacity-limited (free order-0). In that case we do a quick async compaction attempt first and skip a potentially unnecessary but still disruptive reclaim invocation.

However, it does seem to me there is a bit of redundancy between the allocator and reclaim: direct reclaim also has that early bailout condition on compaction_ready() for costly orders.

In defrag_mode, since reclaim is promoted to pageblock_order (costly), we always bail reclaim on compaction_ready(). So I would assume there is not much difference from which we call first - although I have not tested it.

Quoted 3 lines

I'm aware you said "minimally viable bug fix" so it's fine if this falls outside of that, I'm just trying to poke around to improve my understanding.

It works in production, but I'm not entirely happy yet with the retry logic. I also find it somewhat difficult to understand, especially the split between should_reclaim_retry() and should_compact_retry().

I'm working on unifying them and put the defrag_mode order promotion directly into the slowpath function. This way we can use "work_order" and "order" throughout the functions and its callees as appropriate.

But yeah this is follow-up work to the fix here.

#18 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

In reply to Johannes Weiner

Review __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order, · line 4190

Quoted 9 lines · Johannes Weiner
(quoted diff) +2 -2
@@ -4169,8 +4185,8 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 	barrier();
 	WRITE_ONCE(current->capture_control, &capc);
 
-	*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
-							       prio, &capc);
+	*compact_result = try_to_compact_pages(gfp_mask, compact_order,
+					       alloc_flags, ac, prio, &capc);
 

Nit: the comment for try_to_compact_pages() is falsified by this, we need something like:

mm/compaction.c +1 -1
index 0568623d9384d..ca57403dc1522 100644
@@ -2823,7 +2823,7 @@ static enum compact_result compact_zone_order(struct zone *zone, int order,
 /**
  * try_to_compact_pages - Direct compact to satisfy a high-order allocation
  * @gfp_mask: The GFP mask of the current allocation
- * @order: The order of the current allocation
+ * @order: The order to try and make available
  * @alloc_flags: The allocation flags of the current allocation
  * @ac: The context of current allocation
  * @prio: Determines how hard direct compaction should try to succeed
author reviewer
#19 lore raw

Re: [PATCH v2 4/4] mm: page_alloc: fix non-movable reclaim storm in defrag_mode

Quoted 5 lines · Brendan Jackman
Quoted 4 lines · Johannes Weiner
(quoted diff) +0 -0
@@ -4169,8 +4185,8 @@ __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order,
 	barrier();
 	WRITE_ONCE(current->capture_control, &capc);
 
Quoted 8 lines

Review __alloc_pages_direct_compact(gfp_t gfp_mask, unsigned int order, · line 4190

Quoted 5 lines
(quoted diff) +2 -2
-	*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
-							       prio, &capc);
+	*compact_result = try_to_compact_pages(gfp_mask, compact_order,
+					       alloc_flags, ac, prio, &capc);
 

Nit: the comment for try_to_compact_pages() is falsified by this, we need something like:

Yes, good catch. Thanks.

Andrew, would you mind folding this difflet in place?

Quoted 13 lines
mm/compaction.c +1 -1
index 0568623d9384d..ca57403dc1522 100644
@@ -2823,7 +2823,7 @@ static enum compact_result compact_zone_order(struct zone *zone, int order,
 /**
  * try_to_compact_pages - Direct compact to satisfy a high-order allocation
  * @gfp_mask: The GFP mask of the current allocation
- * @order: The order of the current allocation
+ * @order: The order to try and make available
  * @alloc_flags: The allocation flags of the current allocation
  * @ac: The context of current allocation
  * @prio: Determines how hard direct compaction should try to succeed