* [PATCH 0/2] mm: memcg: settle memory.high debt for non-blocking THP charges
@ 2026-09-04 3:54 Qinyun Tan
2026-09-04 3:54 ` [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp Qinyun Tan
2026-09-04 3:54 ` [PATCH 2/2] mm: memcg: settle memory.high debt after large folio swapin Qinyun Tan
0 siblings, 2 replies; 6+ messages in thread
From: Qinyun Tan @ 2026-09-04 3:54 UTC (permalink / raw)
To: Andrew Morton
Cc: Johannes Weiner, Michal Hocko, Roman Gushchin, Shakeel Butt,
Muchun Song, David Hildenbrand, Lorenzo Stoakes, Zi Yan,
Baolin Wang, Xunlei Pang, Liam R . Howlett, Nico Pache,
Ryan Roberts, Dev Jain, Barry Song, Lance Yang, Usama Arif,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Chris Down,
Chuanhua Han, Kairui Song, linux-mm, cgroups, linux-kernel,
Qinyun Tan
memory.high is enforced at two points after a charge succeeds: on
return to userspace, and synchronously in try_charge_memcg() for
large overcharges, the latter gated on gfpflags_allow_blocking().
THP charge paths pass the physical allocation gfp from
vma_thp_gfp_mask() to the memcg charge. With the default
defrag=madvise (and no MADV_HUGEPAGE), as well as with defrag=defer,
that gfp does not allow blocking. This is the right allocation
policy -- a THP is not worth direct compaction, fall back to smaller
orders instead -- but the charge code also reads it as "this context
cannot sleep" and skips the synchronous enforcement, even though
fault context sleeps just fine. Inside a single-syscall populate
loop (mlock(), MADV_POPULATE_*, any GUP-driven population) the
return-to-userspace hook is not reached between faults either, so
nothing throttles at all: a memcg's usage runs from memory.high
straight up to memory.max with zero reclaim and zero penalty sleep,
consuming the reaction window that userspace OOM handlers (oomd,
Kubernetes) depend on.
Patch 1 fixes the anonymous THP/mTHP fault path. The pre-existing
selftest test_memcg_high_sync readily reproduces the problem: with
transparent_hugepage/enabled=always it fails without the patch and
passes with it.
Patch 2 fixes the same problem for large folio swapin on
SWP_SYNCHRONOUS_IO devices (zram) with mTHP swapin enabled. Its
changelog carries measured before/after numbers for both the
reclaim-keeps-up regime and the penalty-sleep regime.
In both patches the charge gfp deliberately stays coupled to the
allocation gfp, so the fail-fast fallback at memory.max is fully
preserved; the over-high debt is instead settled from the fault
path, which knows its context can sleep.
A note for stable backports: mem_cgroup_handle_over_high() only
gained its gfp_mask argument in v6.6, from commit 9ea9cb00a82b ("mm:
memcontrol: fix GFP_NOFS recursion in memory.high enforcement");
older kernels take no argument. On kernels predating the swap table
rework the swapin charge sits in alloc_swap_folio() rather than
__swap_cache_alloc(), but patch 2's fix location at the end of
do_swap_page() applies unchanged.
Qinyun Tan (2):
mm: memcg: settle memory.high debt after THP faults with
non-blocking gfp
mm: memcg: settle memory.high debt after large folio swapin
mm/huge_memory.c | 8 ++++++++
mm/memory.c | 10 ++++++++++
2 files changed, 18 insertions(+)
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp
2026-09-04 3:54 [PATCH 0/2] mm: memcg: settle memory.high debt for non-blocking THP charges Qinyun Tan
@ 2026-09-04 3:54 ` Qinyun Tan
2026-09-04 6:50 ` Baolin Wang
2026-09-04 15:10 ` Zi Yan
2026-09-04 3:54 ` [PATCH 2/2] mm: memcg: settle memory.high debt after large folio swapin Qinyun Tan
1 sibling, 2 replies; 6+ messages in thread
From: Qinyun Tan @ 2026-09-04 3:54 UTC (permalink / raw)
To: Andrew Morton
Cc: Johannes Weiner, Michal Hocko, Roman Gushchin, Shakeel Butt,
Muchun Song, David Hildenbrand, Lorenzo Stoakes, Zi Yan,
Baolin Wang, Xunlei Pang, Liam R . Howlett, Nico Pache,
Ryan Roberts, Dev Jain, Barry Song, Lance Yang, Usama Arif,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Chris Down,
Chuanhua Han, Kairui Song, linux-mm, cgroups, linux-kernel,
Qinyun Tan, stable
Anonymous THP faults happening in a kernel loop that does not return
to userspace -- the populate loop of a single mlock() call, or any
GUP-driven population -- can drive a memcg's usage from memory.high
all the way up to memory.max with zero reclaim and zero penalty
sleep.
This defeats the containment memory.high is supposed to provide:
above high, the documented promise is that "the processes of the
cgroup are throttled and put under heavy reclaim pressure", and
userspace OOM handlers (oomd, Kubernetes) rely on the high..max
buffer as their reaction window. Only after hitting memory.max does
the non-blocking charge fail, THP fall back to 4K, and
folio_prealloc()'s GFP_KERNEL charge finally restore throttling --
by which point the entire buffer has been consumed.
memory.high is enforced at two points after a charge succeeds:
1. from resume_user_mode_work() on return to userspace, requested
via set_notify_resume();
2. synchronously in try_charge_memcg() for large overcharges, added
by commit c9afe31ec443 ("memcg: synchronously enforce memory.high
for large overcharges"), gated on gfpflags_allow_blocking().
A populate loop does not return to userspace between faults, so gate
1 never runs. Gate 2 is defeated by the charge gfp: since
commit 3b3636924dfe ("mm, memcg: sync allocation and memcg charge
gfp flags for THP"), the THP fault path passes the allocation gfp
from vma_thp_gfp_mask() to mem_cgroup_charge(). With defrag=defer
that gfp is GFP_TRANSHUGE_LIGHT | __GFP_KSWAPD_RECLAIM; with the
default defrag=madvise and no MADV_HUGEPAGE it is plain
GFP_TRANSHUGE_LIGHT. Neither allows blocking. This is the right
policy for the physical allocation -- a THP is not worth direct
compaction, fall back to 4K instead -- but try_charge_memcg() also
interprets it as "this context cannot sleep" and skips the
synchronous enforcement, even
though fault context sleeps just fine (it holds the mmap or per-VMA
read lock).
Fix this in the fault paths, which know their context can sleep:
after a successful THP/mTHP charge, settle any accrued over-high
debt via mem_cgroup_handle_over_high(GFP_KERNEL). This reuses the
existing throttling machinery (reclaim + calculate_high_delay()
penalty sleep) and is a no-op read of
current->memcg_nr_pages_over_high when there is no debt.
Deliberately not changed:
- The charge gfp itself is kept coupled to the allocation gfp, so the
fail-fast behaviour at memory.max (charge fails -> fall back to 4K
instead of reclaiming or OOMing for a THP) that the coupling was
introduced for is fully preserved.
- try_charge_memcg() is not touched: gfpflags_allow_blocking() is the
only signal it has, and it must stay conservative for callers that
genuinely cannot sleep.
The pre-existing selftest test_memcg_high_sync, added alongside the
synchronous enforcement by commit 6323ec54b450 ("selftests: memcg:
test high limit for single entry allocation"), readily reproduces
this: it mlocks 200M against memory.high=30M and memory.max=140M
with swap disabled, and expects high events with no max events. On
systems with transparent_hugepage/enabled=always it fails without
this patch -- the population bursts through to memory.max -- and
passes with it.
Fixes: c9afe31ec443 ("memcg: synchronously enforce memory.high for large overcharges")
Cc: <stable@vger.kernel.org>
Signed-off-by: Qinyun Tan <qinyuntan@linux.alibaba.com>
---
Note for stable backports: mem_cgroup_handle_over_high() only gained
its gfp_mask argument in v6.6, from commit 9ea9cb00a82b ("mm:
memcontrol: fix GFP_NOFS recursion in memory.high enforcement"); on
older kernels the call sites take no argument.
mm/huge_memory.c | 8 ++++++++
mm/memory.c | 2 ++
2 files changed, 10 insertions(+)
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index ced400f72d43..543ba4a74dc3 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -1329,6 +1329,14 @@ static struct folio *vma_alloc_anon_folio_pmd(struct vm_area_struct *vma,
return NULL;
}
+ /*
+ * The charge gfp encodes THP allocation policy and may not allow
+ * blocking, which makes try_charge skip its synchronous memory.high
+ * throttling. Fault context can sleep, so settle any over-high debt
+ * here instead of letting usage grow unthrottled up to memory.max.
+ */
+ mem_cgroup_handle_over_high(GFP_KERNEL);
+
if (folio_memcg_alloc_deferred(folio)) {
folio_put(folio);
count_vm_event(THP_FAULT_FALLBACK);
diff --git a/mm/memory.c b/mm/memory.c
index 8b0c2c735d3d..24cbf2a26905 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -5362,6 +5362,8 @@ static struct folio *alloc_anon_folio(struct vm_fault *vmf)
folio_put(folio);
goto next;
}
+ /* Same reasoning as in vma_alloc_anon_folio_pmd(). */
+ mem_cgroup_handle_over_high(GFP_KERNEL);
if (order > 1 && folio_memcg_alloc_deferred(folio)) {
folio_put(folio);
goto fallback;
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 2/2] mm: memcg: settle memory.high debt after large folio swapin
2026-09-04 3:54 [PATCH 0/2] mm: memcg: settle memory.high debt for non-blocking THP charges Qinyun Tan
2026-09-04 3:54 ` [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp Qinyun Tan
@ 2026-09-04 3:54 ` Qinyun Tan
1 sibling, 0 replies; 6+ messages in thread
From: Qinyun Tan @ 2026-09-04 3:54 UTC (permalink / raw)
To: Andrew Morton
Cc: Johannes Weiner, Michal Hocko, Roman Gushchin, Shakeel Butt,
Muchun Song, David Hildenbrand, Lorenzo Stoakes, Zi Yan,
Baolin Wang, Xunlei Pang, Liam R . Howlett, Nico Pache,
Ryan Roberts, Dev Jain, Barry Song, Lance Yang, Usama Arif,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Chris Down,
Chuanhua Han, Kairui Song, linux-mm, cgroups, linux-kernel,
Qinyun Tan, stable
On SWP_SYNCHRONOUS_IO swap devices (zram) with mTHP swapin enabled,
swapping a range back in from within a single kernel entry -- the
populate loop of mlock() or MADV_POPULATE_READ, or any GUP-driven
population -- drives a memcg's usage from memory.high straight up to
memory.max with zero reclaim and zero penalty sleep.
This defeats the containment memory.high is supposed to provide: the
high..max buffer that userspace OOM handlers (oomd, Kubernetes) rely
on as their reaction window is consumed in well under a second, and
when the swapped-in pages are mlocked the burst ends in memcg OOM.
The cause is the swapin instance of the problem fixed by the
previous patch for anonymous THP faults. memory.high is enforced
either on return to userspace, which a populate loop does not reach
between faults, or synchronously in try_charge_memcg() for large
overcharges, which is gated on gfpflags_allow_blocking(). Since
commit 242d12c98174 ("mm: support large folios swap-in for sync io
devices"), swapping in a large folio charges it with the gfp derived
from vma_thp_gfp_mask() (nowadays via __swap_cache_alloc(), which
overrides the caller's GFP_HIGHUSER_MOVABLE for order > 0 before
calling mem_cgroup_swapin_charge_folio()). With the default
defrag=madvise and no MADV_HUGEPAGE the resulting gfp is
GFP_TRANSHUGE_LIGHT based and does not allow blocking. That is the
right policy for the physical allocation, but try_charge_memcg()
also reads it as "this context cannot sleep" and skips the
synchronous enforcement, even though swapin fault context sleeps
just fine. Order-0 swapin is unaffected: it charges with the
caller's GFP_HIGHUSER_MOVABLE and throttles as expected.
Fix this by settling the debt at the end of do_swap_page(), where
sleeping is known to be safe: the folio lock, the page table lock
and the swap device reference have all been dropped, only the
mmap/VMA read lock is held -- the same context in which the order-0
charge path already throttles today. This is a no-op read of
current->memcg_nr_pages_over_high when there is no debt.
The charge gfp is deliberately kept coupled to the allocation gfp so
the fail-fast fallback to order-0 at memory.max is preserved,
matching the previous patch.
Verified on zram swap with hugepages-64kB/enabled=always and zswap
disabled at boot: memory.high=30M, memory.max=140M, then a single
MADV_POPULATE_READ over a 200M swapped-out range (~3200 64k large
folio swapins, confirmed via mTHP swpin stats). Without this patch
the populate bursts through the whole high..max buffer in 0.24s and
memory.events max goes 0->1. With it, max stays 0 across repeated
runs and usage is held at memory.high by reclaim throughout (high
0->~680). Populate time is unchanged (0.22s): settling on every
fault keeps the overage within one charge batch, reclaim keeps up
with the swapin rate and the penalty sleep never needs to engage.
The penalty-sleep regime (mlock'd populate, so swapped-in pages are
immediately unreclaimable) was verified separately: without this
patch the locked burst blows through the entire high..max buffer
(~132M of 64k swapins) within 0.1s and ends in memcg OOM; with it,
usage climbs from memory.high at a quadratically decaying pace (33M
after 5s, reproducibly) and never approaches memory.max within the
observation window.
Fixes: 242d12c98174 ("mm: support large folios swap-in for sync io devices")
Cc: <stable@vger.kernel.org>
Signed-off-by: Qinyun Tan <qinyuntan@linux.alibaba.com>
---
mm/memory.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/mm/memory.c b/mm/memory.c
index 8b0c2c735d3d..dd56b43d3aa3 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -5267,6 +5267,14 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
out:
if (si)
put_swap_device(si);
+ /*
+ * Large folio swapin charges with the THP allocation gfp, which may
+ * not allow blocking, making try_charge skip its synchronous
+ * memory.high throttling. Settle any over-high debt here instead,
+ * where sleeping is safe: the folio lock, the page table lock and
+ * the swap device reference have all been dropped.
+ */
+ mem_cgroup_handle_over_high(GFP_KERNEL);
return ret;
out_nomap:
if (vmf->pte)
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp
2026-09-04 3:54 ` [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp Qinyun Tan
@ 2026-09-04 6:50 ` Baolin Wang
2026-09-04 9:05 ` Qinyun Tan
2026-09-04 15:10 ` Zi Yan
1 sibling, 1 reply; 6+ messages in thread
From: Baolin Wang @ 2026-09-04 6:50 UTC (permalink / raw)
To: Qinyun Tan, Andrew Morton
Cc: Johannes Weiner, Michal Hocko, Roman Gushchin, Shakeel Butt,
Muchun Song, David Hildenbrand, Lorenzo Stoakes, Zi Yan,
Xunlei Pang, Liam R . Howlett, Nico Pache, Ryan Roberts, Dev Jain,
Barry Song, Lance Yang, Usama Arif, Vlastimil Babka,
Mike Rapoport, Suren Baghdasaryan, Chris Down, Chuanhua Han,
Kairui Song, linux-mm, cgroups, linux-kernel, stable
On 9/4/26 11:54 AM, Qinyun Tan wrote:
> Anonymous THP faults happening in a kernel loop that does not return
> to userspace -- the populate loop of a single mlock() call, or any
> GUP-driven population -- can drive a memcg's usage from memory.high
> all the way up to memory.max with zero reclaim and zero penalty
> sleep.
>
> This defeats the containment memory.high is supposed to provide:
> above high, the documented promise is that "the processes of the
> cgroup are throttled and put under heavy reclaim pressure", and
> userspace OOM handlers (oomd, Kubernetes) rely on the high..max
> buffer as their reaction window. Only after hitting memory.max does
> the non-blocking charge fail, THP fall back to 4K, and
> folio_prealloc()'s GFP_KERNEL charge finally restore throttling --
> by which point the entire buffer has been consumed.
>
> memory.high is enforced at two points after a charge succeeds:
>
> 1. from resume_user_mode_work() on return to userspace, requested
> via set_notify_resume();
> 2. synchronously in try_charge_memcg() for large overcharges, added
> by commit c9afe31ec443 ("memcg: synchronously enforce memory.high
> for large overcharges"), gated on gfpflags_allow_blocking().
>
> A populate loop does not return to userspace between faults, so gate
> 1 never runs. Gate 2 is defeated by the charge gfp: since
> commit 3b3636924dfe ("mm, memcg: sync allocation and memcg charge
> gfp flags for THP"), the THP fault path passes the allocation gfp
> from vma_thp_gfp_mask() to mem_cgroup_charge(). With defrag=defer
> that gfp is GFP_TRANSHUGE_LIGHT | __GFP_KSWAPD_RECLAIM; with the
> default defrag=madvise and no MADV_HUGEPAGE it is plain
> GFP_TRANSHUGE_LIGHT. Neither allows blocking. This is the right
> policy for the physical allocation -- a THP is not worth direct
> compaction, fall back to 4K instead -- but try_charge_memcg() also
> interprets it as "this context cannot sleep" and skips the
> synchronous enforcement, even
> though fault context sleeps just fine (it holds the mmap or per-VMA
> read lock).
>
> Fix this in the fault paths, which know their context can sleep:
> after a successful THP/mTHP charge, settle any accrued over-high
> debt via mem_cgroup_handle_over_high(GFP_KERNEL). This reuses the
> existing throttling machinery (reclaim + calculate_high_delay()
> penalty sleep) and is a no-op read of
> current->memcg_nr_pages_over_high when there is no debt.
This doesn't convince me. If the user sets defrag=defer, it means large
folio allocations should not block, so using gfpflags_allow_blocking()
to decide whether to call mem_cgroup_handle_over_high() is reasonable.
If you call mem_cgroup_handle_over_high() directly in the allocation
functions, it would definitely increase allocation latency. This is not
what we expect when setting defrag=defer.
On the other hand, I wonder if briefly exceeding memory.high is really a
problem in the real products. If this is only to fix the
test_memcg_high_sync selftest below, which is full of magic numbers, I
don't think it makes much sense.
> Deliberately not changed:
>
> - The charge gfp itself is kept coupled to the allocation gfp, so the
> fail-fast behaviour at memory.max (charge fails -> fall back to 4K
> instead of reclaiming or OOMing for a THP) that the coupling was
> introduced for is fully preserved.
>
> - try_charge_memcg() is not touched: gfpflags_allow_blocking() is the
> only signal it has, and it must stay conservative for callers that
> genuinely cannot sleep.
>
> The pre-existing selftest test_memcg_high_sync, added alongside the
> synchronous enforcement by commit 6323ec54b450 ("selftests: memcg:
> test high limit for single entry allocation"), readily reproduces
> this: it mlocks 200M against memory.high=30M and memory.max=140M
> with swap disabled, and expects high events with no max events. On
> systems with transparent_hugepage/enabled=always it fails without
> this patch -- the population bursts through to memory.max -- and
> passes with it.
>
> Fixes: c9afe31ec443 ("memcg: synchronously enforce memory.high for large overcharges")
> Cc: <stable@vger.kernel.org>
> Signed-off-by: Qinyun Tan <qinyuntan@linux.alibaba.com>
> ---
>
> Note for stable backports: mem_cgroup_handle_over_high() only gained
> its gfp_mask argument in v6.6, from commit 9ea9cb00a82b ("mm:
> memcontrol: fix GFP_NOFS recursion in memory.high enforcement"); on
> older kernels the call sites take no argument.
>
> mm/huge_memory.c | 8 ++++++++
> mm/memory.c | 2 ++
> 2 files changed, 10 insertions(+)
>
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index ced400f72d43..543ba4a74dc3 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -1329,6 +1329,14 @@ static struct folio *vma_alloc_anon_folio_pmd(struct vm_area_struct *vma,
> return NULL;
> }
>
> + /*
> + * The charge gfp encodes THP allocation policy and may not allow
> + * blocking, which makes try_charge skip its synchronous memory.high
> + * throttling. Fault context can sleep, so settle any over-high debt
> + * here instead of letting usage grow unthrottled up to memory.max.
> + */
> + mem_cgroup_handle_over_high(GFP_KERNEL);
> +
> if (folio_memcg_alloc_deferred(folio)) {
> folio_put(folio);
> count_vm_event(THP_FAULT_FALLBACK);
> diff --git a/mm/memory.c b/mm/memory.c
> index 8b0c2c735d3d..24cbf2a26905 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -5362,6 +5362,8 @@ static struct folio *alloc_anon_folio(struct vm_fault *vmf)
> folio_put(folio);
> goto next;
> }
> + /* Same reasoning as in vma_alloc_anon_folio_pmd(). */
> + mem_cgroup_handle_over_high(GFP_KERNEL);
> if (order > 1 && folio_memcg_alloc_deferred(folio)) {
> folio_put(folio);
> goto fallback;
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp
2026-09-04 6:50 ` Baolin Wang
@ 2026-09-04 9:05 ` Qinyun Tan
0 siblings, 0 replies; 6+ messages in thread
From: Qinyun Tan @ 2026-09-04 9:05 UTC (permalink / raw)
To: Baolin Wang, Andrew Morton
Cc: Johannes Weiner, Michal Hocko, Roman Gushchin, Shakeel Butt,
Muchun Song, David Hildenbrand, Lorenzo Stoakes, Zi Yan,
Xunlei Pang, Liam R . Howlett, Nico Pache, Ryan Roberts, Dev Jain,
Barry Song, Lance Yang, Usama Arif, Vlastimil Babka,
Mike Rapoport, Suren Baghdasaryan, Chris Down, Chuanhua Han,
Kairui Song, linux-mm, cgroups, linux-kernel, stable
Hi Baolin,
On 9/4/26 2:50 PM, Baolin Wang wrote:
>
>
> On 9/4/26 11:54 AM, Qinyun Tan wrote:
>
> This doesn't convince me. If the user sets defrag=defer, it means large folio allocations should not block, so using gfpflags_allow_blocking() to decide whether to call mem_cgroup_handle_over_high() is reasonable.
>
> If you call mem_cgroup_handle_over_high() directly in the allocation functions, it would definitely increase allocation latency. This is not what we expect when setting defrag=defer.
>
> On the other hand, I wonder if briefly exceeding memory.high is really a problem in the real products. If this is only to fix the test_memcg_high_sync selftest below, which is full of magic numbers, I don't think it makes much sense.
>
I agree defrag expresses that the THP *allocation* should not block,
and the patch keeps the allocation path untouched.
That said, let me share a small comparison I ran, which shows what
this means for memory.high in practice. Same kernel, one mlock(40M)
in a cgroup with memory.high=30M (no memory.max set), measuring
end-to-end mlock() time:
no memory.high memory.high=30M
THP on 0.00s 2.03s
THP off 0.01s 33.73s
With 4K pages the populate is throttled on every charge batch and
the cost scales with the overshoot -- memory.high working as
documented. With THP, all 40M is populated unthrottled and the
entire enforcement collapses into one penalty sleep on return to
userspace, clamped at MEMCG_MAX_HIGH_DELAY_JIFFIES (2s) -- a flat
fee independent of the overshoot (a 200M mlock pays the same 2s).
So the same workload under the same memory.high gets a completely
different level of enforcement depending on the folio order of the
allocation. That looks more like a design question than anything
else, and I'm not sure the current behavior is what we expect --
would like to hear your thoughts.
Thanks again for taking the time to review, much appreciated!
Thanks,
Qinyun Tan
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp
2026-09-04 3:54 ` [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp Qinyun Tan
2026-09-04 6:50 ` Baolin Wang
@ 2026-09-04 15:10 ` Zi Yan
1 sibling, 0 replies; 6+ messages in thread
From: Zi Yan @ 2026-09-04 15:10 UTC (permalink / raw)
To: Qinyun Tan
Cc: Andrew Morton, Johannes Weiner, Michal Hocko, Roman Gushchin,
Shakeel Butt, Muchun Song, David Hildenbrand, Lorenzo Stoakes,
Baolin Wang, Xunlei Pang, Liam R . Howlett, Nico Pache,
Ryan Roberts, Dev Jain, Barry Song, Lance Yang, Usama Arif,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Chris Down,
Chuanhua Han, Kairui Song, linux-mm, cgroups, linux-kernel,
stable
On 3 Sep 2026, at 23:54, Qinyun Tan wrote:
> Anonymous THP faults happening in a kernel loop that does not return
> to userspace -- the populate loop of a single mlock() call, or any
> GUP-driven population -- can drive a memcg's usage from memory.high
> all the way up to memory.max with zero reclaim and zero penalty
> sleep.
>
> This defeats the containment memory.high is supposed to provide:
> above high, the documented promise is that "the processes of the
> cgroup are throttled and put under heavy reclaim pressure", and
> userspace OOM handlers (oomd, Kubernetes) rely on the high..max
> buffer as their reaction window. Only after hitting memory.max does
> the non-blocking charge fail, THP fall back to 4K, and
Why not force THP to fall back to 4KB when memory.high is reached?
If reaching memory.high means the processes are under heavy reclaim
pressure, I do not think it is reasonable to give any more THP.
> folio_prealloc()'s GFP_KERNEL charge finally restore throttling --
> by which point the entire buffer has been consumed.
>
> memory.high is enforced at two points after a charge succeeds:
>
> 1. from resume_user_mode_work() on return to userspace, requested
> via set_notify_resume();
> 2. synchronously in try_charge_memcg() for large overcharges, added
> by commit c9afe31ec443 ("memcg: synchronously enforce memory.high
> for large overcharges"), gated on gfpflags_allow_blocking().
>
> A populate loop does not return to userspace between faults, so gate
> 1 never runs. Gate 2 is defeated by the charge gfp: since
> commit 3b3636924dfe ("mm, memcg: sync allocation and memcg charge
> gfp flags for THP"), the THP fault path passes the allocation gfp
> from vma_thp_gfp_mask() to mem_cgroup_charge(). With defrag=defer
> that gfp is GFP_TRANSHUGE_LIGHT | __GFP_KSWAPD_RECLAIM; with the
> default defrag=madvise and no MADV_HUGEPAGE it is plain
> GFP_TRANSHUGE_LIGHT. Neither allows blocking. This is the right
> policy for the physical allocation -- a THP is not worth direct
> compaction, fall back to 4K instead -- but try_charge_memcg() also
Right, why doesn’t memcg just turn the THP allocation into a 4KB fallback?
> interprets it as "this context cannot sleep" and skips the
> synchronous enforcement, even
> though fault context sleeps just fine (it holds the mmap or per-VMA
> read lock).
>
> Fix this in the fault paths, which know their context can sleep:
> after a successful THP/mTHP charge, settle any accrued over-high
> debt via mem_cgroup_handle_over_high(GFP_KERNEL). This reuses the
Why a magic GFP_KERNEL? Would adding __GFP_RECLAIM to the existing gfp work?
> existing throttling machinery (reclaim + calculate_high_delay()
> penalty sleep) and is a no-op read of
> current->memcg_nr_pages_over_high when there is no debt.
>
> Deliberately not changed:
>
> - The charge gfp itself is kept coupled to the allocation gfp, so the
> fail-fast behaviour at memory.max (charge fails -> fall back to 4K
> instead of reclaiming or OOMing for a THP) that the coupling was
> introduced for is fully preserved.
>
> - try_charge_memcg() is not touched: gfpflags_allow_blocking() is the
> only signal it has, and it must stay conservative for callers that
> genuinely cannot sleep.
>
> The pre-existing selftest test_memcg_high_sync, added alongside the
> synchronous enforcement by commit 6323ec54b450 ("selftests: memcg:
> test high limit for single entry allocation"), readily reproduces
> this: it mlocks 200M against memory.high=30M and memory.max=140M
> with swap disabled, and expects high events with no max events. On
> systems with transparent_hugepage/enabled=always it fails without
> this patch -- the population bursts through to memory.max -- and
> passes with it.
>
> Fixes: c9afe31ec443 ("memcg: synchronously enforce memory.high for large overcharges")
> Cc: <stable@vger.kernel.org>
> Signed-off-by: Qinyun Tan <qinyuntan@linux.alibaba.com>
> ---
>
> Note for stable backports: mem_cgroup_handle_over_high() only gained
> its gfp_mask argument in v6.6, from commit 9ea9cb00a82b ("mm:
> memcontrol: fix GFP_NOFS recursion in memory.high enforcement"); on
> older kernels the call sites take no argument.
>
> mm/huge_memory.c | 8 ++++++++
> mm/memory.c | 2 ++
> 2 files changed, 10 insertions(+)
>
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index ced400f72d43..543ba4a74dc3 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -1329,6 +1329,14 @@ static struct folio *vma_alloc_anon_folio_pmd(struct vm_area_struct *vma,
> return NULL;
> }
>
> + /*
> + * The charge gfp encodes THP allocation policy and may not allow
> + * blocking, which makes try_charge skip its synchronous memory.high
> + * throttling. Fault context can sleep, so settle any over-high debt
> + * here instead of letting usage grow unthrottled up to memory.max.
> + */
> + mem_cgroup_handle_over_high(GFP_KERNEL);
> +
> if (folio_memcg_alloc_deferred(folio)) {
> folio_put(folio);
> count_vm_event(THP_FAULT_FALLBACK);
> diff --git a/mm/memory.c b/mm/memory.c
> index 8b0c2c735d3d..24cbf2a26905 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -5362,6 +5362,8 @@ static struct folio *alloc_anon_folio(struct vm_fault *vmf)
> folio_put(folio);
> goto next;
> }
> + /* Same reasoning as in vma_alloc_anon_folio_pmd(). */
> + mem_cgroup_handle_over_high(GFP_KERNEL);
> if (order > 1 && folio_memcg_alloc_deferred(folio)) {
> folio_put(folio);
> goto fallback;
> --
> 2.55.0
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-09-04 15:11 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-04 3:54 [PATCH 0/2] mm: memcg: settle memory.high debt for non-blocking THP charges Qinyun Tan
2026-09-04 3:54 ` [PATCH 1/2] mm: memcg: settle memory.high debt after THP faults with non-blocking gfp Qinyun Tan
2026-09-04 6:50 ` Baolin Wang
2026-09-04 9:05 ` Qinyun Tan
2026-09-04 15:10 ` Zi Yan
2026-09-04 3:54 ` [PATCH 2/2] mm: memcg: settle memory.high debt after large folio swapin Qinyun Tan
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox