Linux cgroups development
 help / color / mirror / Atom feed
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);
>  


  reply	other threads:[~2026-08-18  4:32 UTC|newest]

Thread overview: 12+ 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

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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox