From: Tao Cui <cui.tao@linux.dev>
To: liuqiqi@kylinos.cn, linux-mm@kvack.org
Cc: cui.tao@linux.dev, tj@kernel.org, mkoutny@suse.com,
hannes@cmpxchg.org, mhocko@kernel.org, roman.gushchin@linux.dev,
shakeel.butt@linux.dev, muchun.song@linux.dev,
akpm@linux-foundation.org, cgroups@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: Re: [RFC PATCH 4/8] mm/memcontrol: add per-tier charge and uncharge
Date: Tue, 18 Aug 2026 12:32:42 +0800 [thread overview]
Message-ID: <ebffdd23-5291-47f8-88e7-0f10d5f7d3b2@linux.dev> (raw)
In-Reply-To: <20260818023121.100613-5-liuqiqi@kylinos.cn>
Hi,Qiqi
在 2026/8/18 10:31, liuqiqi@kylinos.cn 写道:
> From: Qiqi Liu <liuqiqi@kylinos.cn>
>
> Extend the memcg charging infrastructure to support per-tier accounting.
> After a successful global charge (try_charge_memcg), charge_memcg()
> attempts per-tier charging. If the tier charge fails, the global charge
> is rolled back via refill_stock() to maintain a consistent state.
>
> Per-tier limits are enforced via dedicated page_counter objects with
> both a hard limit (max) and a soft limit (high):
>
> - Hard limit: Charging beyond the tier's max triggers reclaim scoped
> to that tier's NUMA nodes and retries the charge. If reclaim fails,
> OOM is invoked, mirroring memory.max behavior. PF_MEMALLOC
> allocations force-charge past the tier max without reclaim (recursion
> guard); __GFP_NOFAIL/__GFP_HIGH allocations are force-charged past
> the tier max only after reclaim and OOM both fail.
>
> - Soft limit: The charge always succeeds, but if a tier exceeds its
> high watermark, the charge path schedules tier_high_work to perform
> asynchronous reclaim.
>
> The folio lifecycle is updated accordingly:
>
> - Charging: Attempt per-tier charging after global success; roll back
> the global charge on tier failure.
> - Uncharging: Directly uncharge the tier counter.
> - Replacement: Force-charge the new folio to its tier; the old folio
> is uncharged upon freeing.
> - Migration: Keep the global usage unchanged; uncharge the old tier
> and charge the new tier to reflect the folio's new location.
>
> This patch implements per-tier accounting for LRU folios (anon and file).
>
> Signed-off-by: Qiqi Liu <liuqiqi@kylinos.cn>
> ---
> include/linux/memcontrol.h | 1 +
> mm/memcontrol.c | 198 ++++++++++++++++++++++++++++++++++++-
> 2 files changed, 197 insertions(+), 2 deletions(-)
>
> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
> index c0f5929a87bf..fa944e4bc5ad 100644
> --- a/include/linux/memcontrol.h
> +++ b/include/linux/memcontrol.h
> @@ -330,6 +330,7 @@ struct mem_cgroup {
>
> spinlock_t tier_lock;
> struct list_head tier_counters;
> + struct work_struct tier_high_work;
>
> struct mem_cgroup_per_node *nodeinfo[];
> };
> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
> index 70efe01bc36f..30f24604010c 100644
> --- a/mm/memcontrol.c
> +++ b/mm/memcontrol.c
> @@ -2433,6 +2433,46 @@ static int memcg_tier_counter_create(struct mem_cgroup *memcg,
> return 0;
> }
>
> +static void memcg_charge_tier_id(struct mem_cgroup *memcg, int tier_id,
> + unsigned long nr_pages)
> +{
> + struct memcg_tier_counter *tc;
> +
> + if (tier_id < 0)
> + return;
> + rcu_read_lock();
> + tc = memcg_tier_counter_find(memcg, tier_id);
> + if (tc)
> + page_counter_charge(&tc->counter, nr_pages);
> + rcu_read_unlock();
> +}
> +
> +static void memcg_uncharge_tier_id(struct mem_cgroup *memcg, int tier_id,
> + unsigned long nr_pages)
> +{
> + struct memcg_tier_counter *tc;
> +
> + if (tier_id < 0)
> + return;
> + rcu_read_lock();
> + tc = memcg_tier_counter_find(memcg, tier_id);
> + if (tc)
> + page_counter_uncharge(&tc->counter, nr_pages);
> + rcu_read_unlock();
> +}
> +
> +static void memcg_charge_tier(struct mem_cgroup *memcg, struct folio *folio,
> + unsigned long nr_pages)
> +{
> + memcg_charge_tier_id(memcg, node_to_tier_id(folio_nid(folio)), nr_pages);
> +}
> +
> +static void memcg_uncharge_tier(struct mem_cgroup *memcg, struct folio *folio,
> + unsigned long nr_pages)
> +{
> + memcg_uncharge_tier_id(memcg, node_to_tier_id(folio_nid(folio)), nr_pages);
> +}
> +
> static void memcg_free_tier_counters(struct mem_cgroup *memcg)
> {
> struct memcg_tier_counter *tc, *tmp;
> @@ -2445,6 +2485,49 @@ static void memcg_free_tier_counters(struct mem_cgroup *memcg)
> spin_unlock(&memcg->tier_lock);
> }
>
> +static unsigned long
> +reclaim_tier(struct mem_cgroup *memcg, unsigned int nr_pages, gfp_t gfp_mask,
> + nodemask_t *nmp)
> +{
> + unsigned long nr_reclaimed, pflags;
> +
> + psi_memstall_enter(&pflags);
> + nr_reclaimed = try_to_free_mem_cgroup_pages_nodemask(memcg, nr_pages, gfp_mask,
> + MEMCG_RECLAIM_MAY_SWAP, NULL, nmp);
> + psi_memstall_leave(&pflags);
> + return nr_reclaimed;
> +}
> +
> +static void tier_high_work_func(struct work_struct *work)
> +{
> + struct mem_cgroup *memcg;
> + struct memcg_tier_counter *tc;
> + /* Few tiers in practice (2-4); cap is generous. */
> + int over_ids[16];
> + int nr_over = 0;
> + int i;
> +
> + memcg = container_of(work, struct mem_cgroup, tier_high_work);
> +
> + spin_lock(&memcg->tier_lock);
> + list_for_each_entry(tc, &memcg->tier_counters, list) {
> + if (page_counter_read(&tc->counter) > READ_ONCE(tc->counter.high)) {
> + if (nr_over < ARRAY_SIZE(over_ids))
> + over_ids[nr_over++] = tc->tier_id;
> + }
> + }
> + spin_unlock(&memcg->tier_lock);
> +
> + for (i = 0; i < nr_over; i++) {
> + nodemask_t nodes;
> +
> + if (tier_id_to_nodemask(over_ids[i], &nodes) ||
> + nodes_empty(nodes))
> + continue;
> + reclaim_tier(memcg, MEMCG_CHARGE_BATCH, GFP_KERNEL, &nodes);
> + }
> +}
> +
> /*
> * Clamp the maximum sleep time per allocation batch to 2 seconds. This is
> * enough to still cause a significant slowdown in most cases, while still
> @@ -2873,6 +2956,90 @@ static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
> return 0;
> }
>
> +static int try_charge_memcg_tier(struct mem_cgroup *memcg, gfp_t gfp_mask,
> + unsigned int nr_pages, int tier_id)
> +{
> + struct memcg_tier_counter *tc;
> + struct page_counter *counter;
> + int nr_retries = MAX_RECLAIM_RETRIES;
> + unsigned long nr_reclaimed = 0;
> + bool passed_oom = false;
> + nodemask_t nodes, *nmp = NULL;
> +
> + if (tier_id < 0)
> + return 0;
> +
> + rcu_read_lock();
> + tc = memcg_tier_counter_find(memcg, tier_id);
> + rcu_read_unlock();
> + if (!tc)
> + return 0;
> +
> +retry:
> + if (page_counter_try_charge(&tc->counter, nr_pages, &counter))
> + goto success;
> +
> + /* Over max -> reclaim. */
> + if (unlikely(current->flags & PF_MEMALLOC))
> + goto force;
> + if (unlikely(task_in_memcg_oom(current)))
> + goto nomem;
> + if (!gfpflags_allow_blocking(gfp_mask))
> + goto nomem;
> +
> + if (!tier_id_to_nodemask(tier_id, &nodes) && !nodes_empty(nodes))
> + nmp = &nodes;
> +
> + nr_reclaimed = reclaim_tier(memcg, nr_pages, gfp_mask, nmp);
> +
If the nodemask lookup fails, nmp stays NULL and reclaim scans all
nodes - enforcing tier4.max evicts from tier22, and the freed pages
are on the wrong tier so it doesn't even help. I'd goto nomem here.
Also two nits: struct page_counter *counter is never read; and the
function relies on the caller's css reference to keep tc valid across
reclaim_tier()/mem_cgroup_oom() (the RCU lock only covers the list
walk) - worth a comment before someone adds a new caller.
Thanks,
Tao
> + if (page_counter_read(&tc->counter) + nr_pages <= READ_ONCE(tc->counter.max))
> + goto retry;
> + if (gfp_mask & __GFP_NORETRY)
> + goto nomem;
> + if (nr_reclaimed && nr_pages <= (1 << PAGE_ALLOC_COSTLY_ORDER))
> + goto retry;
> + if (nr_retries--)
> + goto retry;
> + if (gfp_mask & __GFP_RETRY_MAYFAIL)
> + goto nomem;
> + if (passed_oom && task_is_dying())
> + goto nomem;
> + if (mem_cgroup_oom(memcg, gfp_mask, get_order(nr_pages * PAGE_SIZE))) {
> + passed_oom = true; /* tier max is a hard limit: OOM, like memory.max */
> + nr_retries = MAX_RECLAIM_RETRIES;
> + goto retry;
> + }
> + goto nomem;
> +success:
> + do {
> + struct memcg_tier_counter *tc_this;
> +
> + rcu_read_lock();
> + tc_this = memcg_tier_counter_find(memcg, tier_id);
> + if (tc_this &&
> + page_counter_read(&tc_this->counter) > READ_ONCE(tc_this->counter.high) &&
> + !work_pending(&memcg->tier_high_work)) {
> + schedule_work(&memcg->tier_high_work);
> + rcu_read_unlock();
> + break;
> + }
> + rcu_read_unlock();
> + } while ((memcg = parent_mem_cgroup(memcg)));
> + return 0;
> +nomem:
> + if (!(gfp_mask & (__GFP_NOFAIL | __GFP_HIGH)))
> + return -ENOMEM;
> +
> +force:
> + /*
> + * Force-charge past the tier max for reclaim/privileged allocations
> + * (PF_MEMALLOC, or __GFP_NOFAIL/__GFP_HIGH that fell through from
> + * nomem) -- not skip -- so the page is in tier.current too.
> + */
> + page_counter_charge(&tc->counter, nr_pages);
> + return 0;
> +}
> +
> static inline int try_charge(struct mem_cgroup *memcg, gfp_t gfp_mask,
> unsigned int nr_pages)
> {
> @@ -4216,6 +4383,7 @@ static struct mem_cgroup *mem_cgroup_alloc(struct mem_cgroup *parent)
> INIT_WORK(&memcg->high_work, high_work_func);
> spin_lock_init(&memcg->tier_lock);
> INIT_LIST_HEAD(&memcg->tier_counters);
> + INIT_WORK(&memcg->tier_high_work, tier_high_work_func);
> vmpressure_init(&memcg->vmpressure);
> INIT_LIST_HEAD(&memcg->memory_peaks);
> INIT_LIST_HEAD(&memcg->swap_peaks);
> @@ -4439,6 +4607,7 @@ static void mem_cgroup_css_free(struct cgroup_subsys_state *css)
>
> vmpressure_cleanup(&memcg->vmpressure);
> cancel_work_sync(&memcg->high_work);
> + cancel_work_sync(&memcg->tier_high_work);
> memcg_free_tier_counters(memcg);
> memcg1_remove_from_trees(memcg);
> free_shrinker_info(memcg);
> @@ -5224,8 +5393,17 @@ static int charge_memcg(struct folio *folio, struct mem_cgroup *memcg,
>
> objcg = get_obj_cgroup_from_memcg(memcg);
> /* Do not account at the root objcg level. */
> - if (!obj_cgroup_is_root(objcg))
> + if (!obj_cgroup_is_root(objcg)) {
> ret = try_charge_memcg(memcg, gfp, folio_nr_pages(folio));
> + if (!ret) {
> + int tid = node_to_tier_id(folio_nid(folio));
> +
> + ret = try_charge_memcg_tier(memcg, gfp, folio_nr_pages(folio), tid);
> + /* tier over max / OOM: undo the memory charge */
> + if (ret)
> + refill_stock(memcg, folio_nr_pages(folio));
> + }
> + }
> if (ret) {
> obj_cgroup_put(objcg);
> return ret;
> @@ -5385,8 +5563,11 @@ static void uncharge_folio(struct folio *folio, struct uncharge_gather *ug)
> ug->nr_kmem += nr_pages;
> } else {
> /* LRU pages aren't accounted at the root level */
> - if (!obj_cgroup_is_root(objcg))
> + if (!obj_cgroup_is_root(objcg)) {
> ug->nr_memory += nr_pages;
> + memcg_uncharge_tier(obj_cgroup_memcg(objcg), folio,
> + nr_pages);
> + }
> ug->pgpgout++;
>
> WARN_ON_ONCE(folio_unqueue_deferred_split(folio));
> @@ -5461,6 +5642,7 @@ void mem_cgroup_replace_folio(struct folio *old, struct folio *new)
> page_counter_charge(&memcg->memory, nr_pages);
> if (do_memsw_account())
> page_counter_charge(&memcg->memsw, nr_pages);
> + memcg_charge_tier(memcg, new, nr_pages);
> }
>
> obj_cgroup_get(objcg);
> @@ -5503,6 +5685,18 @@ void mem_cgroup_migrate(struct folio *old, struct folio *new)
> if (!objcg)
> return;
>
> + /* Re-account the per-tier breakdown if the folio moved across tiers. */
> + if (!obj_cgroup_is_root(objcg)) {
> + struct mem_cgroup *memcg = obj_cgroup_memcg(objcg);
> + int old_tier = node_to_tier_id(folio_nid(old));
> + int new_tier = node_to_tier_id(folio_nid(new));
> +
> + if (old_tier != new_tier) {
> + memcg_uncharge_tier_id(memcg, old_tier, folio_nr_pages(old));
> + memcg_charge_tier_id(memcg, new_tier, folio_nr_pages(new));
> + }
> + }
> +
> /* Transfer the charge and the objcg ref */
> commit_charge(new, objcg);
>
next prev parent reply other threads:[~2026-08-18 4:32 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-18 2:31 [RFC PATCH 0/8] mm/memcontrol: introduce per-tier memory accounting and control liuqiqi
2026-08-18 2:31 ` [RFC PATCH 1/8] mm/memory-tiers: add node_to_tier_id and tier_id_to_nodemask liuqiqi
2026-08-18 2:31 ` [RFC PATCH 2/8] mm/vmscan: add try_to_free_mem_cgroup_pages_nodemask liuqiqi
2026-08-18 2:31 ` [RFC PATCH 3/8] mm/memcontrol: add per-tier page counter infrastructure and lifecycle liuqiqi
2026-08-18 2:31 ` [RFC PATCH 4/8] mm/memcontrol: add per-tier charge and uncharge liuqiqi
2026-08-18 4:32 ` Tao Cui [this message]
2026-08-18 2:31 ` [RFC PATCH 5/8] mm/memcontrol: add per-cpu stock for tier charge/uncharge liuqiqi
2026-08-18 2:31 ` [RFC PATCH 6/8] mm/memcontrol: add memory.tier control file liuqiqi
2026-08-18 2:31 ` [RFC PATCH 7/8] mm/memcontrol: auto-derive tier high/max from memory.high/max liuqiqi
2026-08-18 4:46 ` Tao Cui
2026-08-18 2:31 ` [RFC PATCH 8/8] cgroup: add memory_tiered_limits cgroup mount option liuqiqi
2026-08-18 4:56 ` Tao Cui
2026-08-18 8:12 ` [RFC PATCH 0/8] mm/memcontrol: introduce per-tier memory accounting and control Michal Hocko
2026-08-18 8:24 ` [syzbot ci] " syzbot ci
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=ebffdd23-5291-47f8-88e7-0f10d5f7d3b2@linux.dev \
--to=cui.tao@linux.dev \
--cc=akpm@linux-foundation.org \
--cc=cgroups@vger.kernel.org \
--cc=hannes@cmpxchg.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=liuqiqi@kylinos.cn \
--cc=mhocko@kernel.org \
--cc=mkoutny@suse.com \
--cc=muchun.song@linux.dev \
--cc=roman.gushchin@linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=tj@kernel.org \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.