From: Baolin Wang <baolin.wang@linux.alibaba.com>
To: Xiang Liu <liuxiang.277@bytedance.com>,
Andrew Morton <akpm@linux-foundation.org>
Cc: Johannes Weiner <hannes@cmpxchg.org>,
David Hildenbrand <david@kernel.org>,
Michal Hocko <mhocko@kernel.org>, Qi Zheng <qi.zheng@linux.dev>,
Shakeel Butt <shakeel.butt@linux.dev>,
Lorenzo Stoakes <ljs@kernel.org>,
Kairui Song <kasong@tencent.com>, Barry Song <baohua@kernel.org>,
Axel Rasmussen <axelrasmussen@google.com>,
Yuanchu Xie <yuanchu@google.com>, Wei Xu <weixugc@google.com>,
Baoquan He <baoquan.he@linux.dev>,
linux-mm@kvack.org, linux-kernel@vger.kernel.org,
Bo Zhang <zhangbo56@xiaomi.com>
Subject: Re: [RFC PATCH v2] mm: vmscan: avoid anon scanning for GFP_NOIO with low swapcache in MGLRU
Date: Wed, 21 Oct 2026 15:28:18 +0800 [thread overview]
Message-ID: <7f4daafd-a3c5-4ccc-80bd-05a7ac09e4ab@linux.alibaba.com> (raw)
In-Reply-To: <179248830836.1744016.8424217811286616919.noio-mglru-rfc-v2@bytedance.com>
On 10/20/26 11:18 PM, Xiang Liu wrote:
> Bo Zhang's GFP_NOIO fix [1] avoids low-yield anonymous scanning in the
> traditional LRU but explicitly leaves MGLRU as a TODO. Address that case
> by applying can_reclaim_anon_pages() to MGLRU's scan policy.
>
> Under GFP_NOIO, anonymous folios that need swap IO cannot be reclaimed.
> When swapcache is below 1/64 of the anon LRU, scanning anon can therefore
> spend substantial time isolating, sorting and processing folios with
> little reclaim progress. Reuse the existing eligibility check, including
> its swap and demotion handling, without adding another density threshold.
>
> Introduce get_scan_swappiness() and use its result for the lruvec size
> estimate, scan budget, aging decision and eviction. This selects file when
> anon is effectively unreclaimable, while preserving explicit file-only
> and anon-only policies. For anon-only proactive reclaim, get_nr_to_scan()
> already returns 0 when anon cannot be reclaimed; do not duplicate that
> rejection in lruvec_is_sizable() or turn the request into file reclaim.
>
> Size accounting must follow the scan policy as well. Otherwise an lruvec
> with no file pages and abundant unscannable anon can have a zero scan
> budget but still appear sizable to shrink_one(). With no aging to request
> rotation, it can remain in the old memcg generation and delay advancing
> to the next memcg generation. Share the policy between
> lruvec_is_sizable() and try_to_shrink_lruvec() to keep them consistent.
>
> Use scan_swappiness to decide whether aging is needed, but keep the
> original swappiness when executing try_to_inc_max_seq(). In particular,
> file-only scanning with two file generations needs aging even if anon
> still has four generations. Passing a forced zero to the aging execution
> would instead let inc_min_seq() skip processing the oldest anon folios
> and could introduce a cold/hot inversion through the single-type shortcut.
>
> The tradeoff is synchronous aging: with four anon generations and two
> file generations, advancing the shared max_seq can require moving the
> oldest anon folios to the next generation. Accept that cost to avoid
> low-yield anon eviction, without extending use of the existing shortcut.
>
> The following measurements are carried over from v1, tested on mm-unstable
> at commit 55ff3dc8fced ("mm/hugetlb_cgroup: add trailing #endif comment for
> include guard"). They are not new runtime measurements of this v2 rebase.
>
> The workload used dm-verity hash-block reads through dm-bufio, exercising
> the real allocation path with unchanged flags:
>
> verity_verify_io -> verity_hash_for_block -> verity_verify_level
> -> dm_bufio_read_with_ioprio -> new_read -> __bufio_new -> alloc_buffer
>
> GFP_NOIO | __GFP_NORETRY | __GFP_NOMEMALLOC | __GFP_NOWARN
>
> Allocation time spans alloc_buffer() entry to return with a valid buffer
> and data. Direct reclaim time spans try_to_free_pages() entry to return.
> Eviction and aging are the cumulative evict_folios() and
> try_to_inc_max_seq() times within that reclaim, respectively; aging
> already includes inc_max_seq(). These are wall-clock function timings,
> not end-to-end IO completion times.
>
> 1. NOIO allocations under memory pressure
>
> Generations evolved naturally, without forcing the four-anon/two-file
> layout. For successful allocations that entered direct reclaim, median
> times were as follows. Reclaim time sums the direct-reclaim episodes
> within each allocation before taking the median; eviction is likewise
> accumulated within each allocation.
>
> Time (ms, median) Baseline Patched
> =========================== ======== =======
> Successful alloc_buffer() 181.196 0.465
> Direct reclaim 181.185 0.131
> Eviction within reclaim 179.147 0.091
>
> This run captured 28 valid buffers and no NULL returns on the baseline,
> versus 268 valid buffers and 62 NULL returns with v1; 28 and 266 successful
> allocations, respectively, entered direct reclaim. All reader operations
> completed. The table describes successful-call costs, not allocation
> failure probability or end-to-end IO speedup.
>
> 2. Synchronous-aging cost
>
> Prepare four anon generations and two file generations, with zero
> swapcache and pages in the oldest file generation. Verify these conditions
> at the NOIO reclaim entry, rather than relying only on preparation
> snapshots. Vary the oldest anon population to compare baseline eviction
> against the synchronous aging paid by the patch.
>
> Each row is the first eligible successful allocation from an independently
> prepared trial at that size. Patched samples must actually execute aging
> and advance anon's oldest sequence. Oldest anon is its measured population
> at the matched entry; reclaim and its eviction/aging costs cover the
> associated try_to_free_pages() invocation, including other memcgs visited.
>
> Kernel Oldest anon Allocation Reclaim Eviction Aging
> (MiB) (ms) (ms) (ms) (ms)
> ======== =========== ========== ======== ======== =======
> Baseline 1755.266 131.704 131.699 127.985 1.672
> Patched 1840.422 38.316 38.311 0.307 37.785
> Baseline 4835.418 681.796 681.766 675.477 4.382
> Patched 4813.180 91.161 91.140 0.450 90.407
> Baseline 8941.582 1474.245 1474.202 1469.084 0.488
> Patched 9012.660 143.748 143.709 1.026 142.341
> Baseline 13104.676 1098.372 1098.354 1087.976 2.307
> Patched 13046.621 139.450 139.390 1.083 137.954
> Baseline 17187.043 2694.909 2694.902 2686.470 0.784
> Patched 17124.902 209.240 209.233 0.638 208.270
>
> Across these recorded sizes, patched synchronous aging was cheaper than
> baseline eviction and successful allocation time decreased.
>
> [1]
> https://patchew.org/linux/20260908062649.1045883-1-zhangbo56@xiaomi.com/
>
> Signed-off-by: Xiang Liu <liuxiang.277@bytedance.com>
> ---
> Changes in v2:
> - Share the scan policy through get_scan_swappiness(), as suggested by Barry.
> - Explain why the aging decision uses the scan policy but aging execution
> retains the original swappiness and its synchronous cost.
> - Remove lruvec_is_sizable()'s extra anon-only rejection; leave that handling
> in get_nr_to_scan() and preserve anon-only requests in the helper.
> - Explain why the ordinary NOIO size adjustment is still needed for memcg
> rotation, and keep the source comment concise.
> - Rebase onto the official mm-unstable head 3eb3e848d72c.
>
> v1: https://lore.kernel.org/20261010141057.599595-1-liuxiang.277@bytedance.com/
> v1 test base: 55ff3dc8fced48c98667c19769c55103a23eabce
> Discussion: https://lkml.iu.edu/2610.1/08079.html
>
> Regarding Barry's review:
>
> The scan/aging split is intentional. should_run_aging() sees the restricted
> scan policy, while try_to_inc_max_seq() still receives the original
> swappiness. The comment now explains why this retains synchronous anon
> aging instead of introducing another use of the cold/hot shortcut.
>
> The duplicate anon-only rejection is removed. get_scan_swappiness()
> preserves SWAPPINESS_ANON_ONLY, leaving get_nr_to_scan() responsible for
> returning 0 when that request cannot reclaim anon.
>
> The ordinary NOIO size adjustment remains necessary: without it, a
> zero-file lruvec can get a zero scan budget but still look sizable due to
> anon. shrink_one() can then return 0 without rotation. Since shrink_many()
> visits the current memcg generation and that generation advances when its
> bins empty, this can delay reaching memcgs in the next generation.
> Both callers therefore share the helper rather than duplicate the policy.
>
>
> mm/vmscan.c | 37 +++++++++++++++++++++++++++----------
> 1 file changed, 27 insertions(+), 10 deletions(-)
>
> diff --git a/mm/vmscan.c b/mm/vmscan.c
> index fc823fe3bd1f..e881b30dbe2e 100644
> --- a/mm/vmscan.c
> +++ b/mm/vmscan.c
> @@ -379,13 +379,6 @@ static inline bool reclaimable_anon_is_low(struct mem_cgroup *memcg,
> if (!sc || (sc->gfp_mask & __GFP_IO))
> return false;
>
> - /*
> - * FIXME: MGLRU doesn't fully respect can_reclaim_anon_pages() for the
> - * scanning type, so only apply this to the traditional LRU for now.
> - */
> - if (lru_gen_enabled())
> - return false;
> -
> if (memcg) {
> struct lruvec *lruvec = mem_cgroup_lruvec(memcg, pgdat);
>
> @@ -4351,12 +4344,35 @@ static unsigned long lruvec_evictable_size(struct lruvec *lruvec, int swappiness
> return total;
> }
>
> +/*
> + * Use the same policy for sizing, eviction and aging decisions, so anon
> + * excluded from scanning cannot prevent memcg rotation.
> + *
> + * Keep the original swappiness in try_to_inc_max_seq(): a forced zero
> + * would let inc_min_seq() skip the oldest anon folios and risk cold/hot
> + * inversions. Accept the synchronous aging cost instead.
> + */
> +static int get_scan_swappiness(struct lruvec *lruvec, struct scan_control *sc,
> + int swappiness)
> +{
> + if (swappiness == MIN_SWAPPINESS || swappiness == SWAPPINESS_ANON_ONLY)
> + return swappiness;
> +
> + if (!can_reclaim_anon_pages(lruvec_memcg(lruvec),
> + lruvec_pgdat(lruvec)->node_id, sc))
> + return MIN_SWAPPINESS;
> +
> + return swappiness;
> +}
> +
> static bool lruvec_is_sizable(struct lruvec *lruvec, struct scan_control *sc)
> {
> unsigned long total;
> int swappiness = get_swappiness(lruvec, sc);
> struct mem_cgroup *memcg = lruvec_memcg(lruvec);
>
> + swappiness = get_scan_swappiness(lruvec, sc, swappiness);
> +
> total = lruvec_evictable_size(lruvec, swappiness);
>
> /* whether the size is big enough to be helpful */
> @@ -5249,9 +5265,10 @@ static bool try_to_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
> bool need_rotate = false, should_age = false;
> long nr_batch, nr_to_scan;
> int swappiness = get_swappiness(lruvec, sc);
> + int scan_swappiness = get_scan_swappiness(lruvec, sc, swappiness);
IMO, 'swappiness' and 'scan_swappiness' are really confusing. Why move
this logic into get_swappiness()? In get_swappiness(), we already have
some similar logic to adjust swappiness when there is no available swap
space.
if (!can_demote(pgdat->node_id, sc, memcg) &&
mem_cgroup_get_nr_swap_pages(memcg) < MIN_LRU_BATCH)
return 0;
> struct mem_cgroup *memcg = lruvec_memcg(lruvec);
>
> - nr_to_scan = get_nr_to_scan(lruvec, sc, memcg, swappiness);
> + nr_to_scan = get_nr_to_scan(lruvec, sc, memcg, scan_swappiness);
> while (nr_to_scan > 0) {
> int delta;
> DEFINE_MAX_SEQ(lruvec);
> @@ -5261,14 +5278,14 @@ static bool try_to_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
> break;
> }
>
> - if (should_run_aging(lruvec, max_seq, sc, swappiness)) {
> + if (should_run_aging(lruvec, max_seq, sc, scan_swappiness)) {
> if (try_to_inc_max_seq(lruvec, max_seq, swappiness, false))
> need_rotate = true;
> should_age = true;
> }
>
> nr_batch = min(nr_to_scan, MIN_LRU_BATCH);
> - delta = evict_folios(nr_batch, lruvec, sc, swappiness);
> + delta = evict_folios(nr_batch, lruvec, sc, scan_swappiness);
> if (!delta)
> break;
>
>
> base-commit: 3eb3e848d72cde2156447cc960ca321bb2ac41a9
prev parent reply other threads:[~2026-10-21 7:28 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-20 15:18 [RFC PATCH v2] mm: vmscan: avoid anon scanning for GFP_NOIO with low swapcache in MGLRU Xiang Liu
2026-10-21 7:28 ` Baolin Wang [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=7f4daafd-a3c5-4ccc-80bd-05a7ac09e4ab@linux.alibaba.com \
--to=baolin.wang@linux.alibaba.com \
--cc=akpm@linux-foundation.org \
--cc=axelrasmussen@google.com \
--cc=baohua@kernel.org \
--cc=baoquan.he@linux.dev \
--cc=david@kernel.org \
--cc=hannes@cmpxchg.org \
--cc=kasong@tencent.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=liuxiang.277@bytedance.com \
--cc=ljs@kernel.org \
--cc=mhocko@kernel.org \
--cc=qi.zheng@linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=weixugc@google.com \
--cc=yuanchu@google.com \
--cc=zhangbo56@xiaomi.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox