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 7/8] mm/memcontrol: auto-derive tier high/max from memory.high/max
Date: Tue, 18 Aug 2026 12:46:35 +0800 [thread overview]
Message-ID: <749beeb5-cc97-42ab-b3a4-aeee378fbb12@linux.dev> (raw)
In-Reply-To: <20260818023121.100613-8-liuqiqi@kylinos.cn>
在 2026/8/18 10:31, liuqiqi@kylinos.cn 写道:
> From: Qiqi Liu <liuqiqi@kylinos.cn>
>
> Per-tier limits are derived by default from the cgroup-wide memory.high
> and memory.max knobs based on the tier's capacity ratio:
>
> tierN.high = memory.high * tierN_capacity / total_capacity
> tierN.max = memory.max * tierN_capacity / total_capacity
>
> This provides zero-config tier partitioning. Setting memory.high or
> memory.max automatically splits the budget across tiers proportionally
> to their physical size. For instance, a 4G cgroup on a system with a
> 75%/25% DRAM/CXL split automatically receives ~3G DRAM and ~1G CXL.
>
> The capacity cache and the derived limits are refreshed on node memory
> hotplug, both when node memory is added and when it is removed.
>
> Writing an explicit value pins the limit and disables auto-derivation
> for that tier, decoupling it from subsequent memory.high/max updates.
>
> Signed-off-by: Qiqi Liu <liuqiqi@kylinos.cn>
> ---
> include/linux/memcontrol.h | 2 +
> mm/memcontrol.c | 162 +++++++++++++++++++++++++++++++++----
> 2 files changed, 150 insertions(+), 14 deletions(-)
>
> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
> index fa944e4bc5ad..8848bc5eeb24 100644
> --- a/include/linux/memcontrol.h
> +++ b/include/linux/memcontrol.h
> @@ -205,6 +205,8 @@ struct memcg_tier_counter {
> int tier_id;
> struct list_head list;
> struct rcu_head rcu;
> + bool max_derived; /* true: derive from memory.max by capacity ratio */
> + bool high_derived; /* true: derive from memory.high by capacity ratio */
> };
>
> struct mem_cgroup {
> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
> index a6c6057f0e4f..f39a702d2301 100644
> --- a/mm/memcontrol.c
> +++ b/mm/memcontrol.c
> @@ -2443,6 +2443,8 @@ static int memcg_tier_counter_create(struct mem_cgroup *memcg,
> page_counter_init(&new->counter, parent_cnt, false);
> page_counter_set_high(&new->counter, PAGE_COUNTER_MAX);
> INIT_LIST_HEAD(&new->list);
> + new->max_derived = true;
> + new->high_derived = true;
>
> spin_lock(&memcg->tier_lock);
> if (memcg_tier_counter_find(memcg, tier_id)) {
> @@ -2514,6 +2516,115 @@ reclaim_tier(struct mem_cgroup *memcg, unsigned int nr_pages, gfp_t gfp_mask,
> return nr_reclaimed;
> }
>
> +/* Global per-tier capacity cache.
> + * Populated at boot and on node hotplug (walks online nodes only, no memcg touch).
> + * Stores (tier_id, capacity) pairs to support sparse tier IDs (e.g., 4, 22, 52)
> + * without requiring a dense index.
> + */
> +#define NR_TIER_CAP_ENTRIES 16
> +struct tier_cap_entry {
> + int tier_id;
> + unsigned long capacity;
> +};
> +static struct tier_cap_entry tier_cap_entries[NR_TIER_CAP_ENTRIES];
> +static int nr_tier_entries;
> +static unsigned long tier_total_capacity;
> +/* Guards the per-tier capacity cache. */
> +static DEFINE_SPINLOCK(tier_cap_lock);
> +
"Populated at boot" doesn't hold: the boot-time call sits in
mem_cgroup_init(), which runs from start_kernel() (init/main.c:1177),
before any initcall. Nodes are assigned tiers in
memory_tier_late_init() (late_initcall), which fires no node notifier.
So tier_total_capacity stays 0 and every memory.high/max write hits
the early return in tier_update_derived_limits(). The hotplug
callback is the only other refresh point - your QEMU setup likely
works because the CXL tier comes up through it.
Either hook a refresh into the tier-assignment path in memory-tiers,
or compute lazily when the cache is empty.
Thanks,
Tao
> +static void update_tier_capacity_cache(void)
> +{
> + int nid;
> + int i;
> +
> + spin_lock(&tier_cap_lock);
> + nr_tier_entries = 0;
> + tier_total_capacity = 0;
> + for (i = 0; i < NR_TIER_CAP_ENTRIES; i++) {
> + tier_cap_entries[i].tier_id = -1;
> + tier_cap_entries[i].capacity = 0;
> + }
> +
> + for_each_online_node(nid) {
> + int tid = node_to_tier_id(nid);
> + unsigned long cap;
> +
> + if (tid < 0)
> + continue;
> + cap = node_present_pages(nid);
> + for (i = 0; i < nr_tier_entries; i++)
> + if (tier_cap_entries[i].tier_id == tid) {
> + tier_cap_entries[i].capacity += cap;
> + tier_total_capacity += cap;
> + break;
> + }
> + if (i == nr_tier_entries && nr_tier_entries < NR_TIER_CAP_ENTRIES) {
> + tier_cap_entries[nr_tier_entries].tier_id = tid;
> + tier_cap_entries[nr_tier_entries].capacity = cap;
> + nr_tier_entries++;
> + tier_total_capacity += cap;
> + } else if (i == nr_tier_entries) {
> + pr_warn_ratelimited("memcg: tier capacity cache full (>%d tiers), tier %d not tracked\n",
> + NR_TIER_CAP_ENTRIES, tid);
> + }
> + }
> + spin_unlock(&tier_cap_lock);
> +}
> +
> +/* Push derived high/max limits to tier counters of @memcg.
> + * Invoked on memory.high/max writes and capacity changes (hotplug).
> + */
> +static void tier_update_derived_limits(struct mem_cgroup *memcg)
> +{
> + struct memcg_tier_counter *tc;
> + struct tier_cap_entry local_entries[NR_TIER_CAP_ENTRIES];
> + unsigned long total;
> + int i, nr_entries;
> +
> + spin_lock(&tier_cap_lock);
> + total = tier_total_capacity;
> + nr_entries = nr_tier_entries;
> + memcpy(local_entries, tier_cap_entries, sizeof(local_entries));
> + spin_unlock(&tier_cap_lock);
> +
> + if (total == 0)
> + return;
> +
> + /* Serialize flag check + limit store against memory.tier writes. */
> + spin_lock(&memcg->tier_lock);
> + list_for_each_entry_rcu(tc, &memcg->tier_counters, list,
> + lockdep_is_held(&memcg->tier_lock)) {
> + unsigned long cap = 0;
> +
> + for (i = 0; i < nr_entries; i++)
> + if (local_entries[i].tier_id == tc->tier_id) {
> + cap = local_entries[i].capacity;
> + break;
> + }
> + if (cap == 0)
> + continue;
> + if (READ_ONCE(tc->max_derived)) {
> + unsigned long mem_max = READ_ONCE(memcg->memory.max);
> +
> + if (mem_max == PAGE_COUNTER_MAX)
> + xchg(&tc->counter.max, PAGE_COUNTER_MAX);
> + else
> + xchg(&tc->counter.max,
> + mul_u64_u64_div_u64(mem_max, cap, total));
> + }
> + if (READ_ONCE(tc->high_derived)) {
> + unsigned long mem_high = READ_ONCE(memcg->memory.high);
> +
> + if (mem_high == PAGE_COUNTER_MAX)
> + xchg(&tc->counter.high, PAGE_COUNTER_MAX);
> + else
> + xchg(&tc->counter.high,
> + mul_u64_u64_div_u64(mem_high, cap, total));
> + }
> + }
> + spin_unlock(&memcg->tier_lock);
> +}
> +
> static void tier_high_work_func(struct work_struct *work)
> {
> struct mem_cgroup *memcg;
> @@ -4841,6 +4952,8 @@ static void mem_cgroup_css_reset(struct cgroup_subsys_state *css)
> list_for_each_entry_rcu(tc, &memcg->tier_counters, list) {
> page_counter_set_max(&tc->counter, PAGE_COUNTER_MAX);
> page_counter_set_high(&tc->counter, PAGE_COUNTER_MAX);
> + WRITE_ONCE(tc->max_derived, true);
> + WRITE_ONCE(tc->high_derived, true);
> }
> rcu_read_unlock();
> }
> @@ -5251,6 +5364,7 @@ static ssize_t memory_high_write(struct kernfs_open_file *of,
> return err;
>
> page_counter_set_high(&memcg->memory, high);
> + tier_update_derived_limits(memcg);
>
> if (of->file->f_flags & O_NONBLOCK)
> goto out;
> @@ -5303,6 +5417,7 @@ static ssize_t memory_max_write(struct kernfs_open_file *of,
> return err;
>
> xchg(&memcg->memory.max, max);
> + tier_update_derived_limits(memcg);
>
> if (of->file->f_flags & O_NONBLOCK)
> goto out;
> @@ -5390,10 +5505,17 @@ static ssize_t memory_tier_write(struct kernfs_open_file *of,
> if (!tier_id_to_nodemask(tier_id, &nodes) && !nodes_empty(nodes))
> nmp = &nodes;
>
> - if (!strcmp(knob, "high"))
> + if (!strcmp(knob, "high")) {
> + spin_lock(&memcg->tier_lock);
> + WRITE_ONCE(tc->high_derived, false);
> page_counter_set_high(&tc->counter, val);
> - else
> + spin_unlock(&memcg->tier_lock);
> + } else {
> + spin_lock(&memcg->tier_lock);
> + WRITE_ONCE(tc->max_derived, false);
> xchg(&tc->counter.max, val);
> + spin_unlock(&memcg->tier_lock);
> + }
>
> if (of->file->f_flags & O_NONBLOCK)
> return nbytes;
> @@ -6145,18 +6267,28 @@ static int __meminit memcg_tier_hotplug_cb(struct notifier_block *self,
> struct mem_cgroup *memcg;
> int tid;
>
> - if (action != NODE_ADDED_FIRST_MEMORY)
> - return notifier_from_errno(0);
> -
> - tid = node_to_tier_id(nn->nid);
> - if (tid < 0)
> - return notifier_from_errno(0);
> -
> - for_each_mem_cgroup(memcg) {
> - if (!mem_cgroup_is_root(memcg) &&
> - memcg_tier_counter_create(memcg, parent_mem_cgroup(memcg), tid))
> - pr_warn_ratelimited("memcg: tier %d counter alloc failed;"
> - " tier accounting degraded\n", tid);
> + switch (action) {
> + case NODE_ADDED_FIRST_MEMORY:
> + tid = node_to_tier_id(nn->nid);
> + if (tid < 0)
> + return notifier_from_errno(0);
> +
> + update_tier_capacity_cache();
> + for_each_mem_cgroup(memcg) {
> + if (!mem_cgroup_is_root(memcg) &&
> + memcg_tier_counter_create(memcg, parent_mem_cgroup(memcg), tid))
> + pr_warn_ratelimited("memcg: tier %d counter alloc failed;"
> + " tier accounting degraded\n", tid);
> + }
> + for_each_mem_cgroup(memcg)
> + tier_update_derived_limits(memcg);
> + break;
> + case NODE_REMOVED_LAST_MEMORY:
> + /* Node memory removed: refresh capacity and derived limits. */
> + update_tier_capacity_cache();
> + for_each_mem_cgroup(memcg)
> + tier_update_derived_limits(memcg);
> + break;
> }
> return notifier_from_errno(0);
> }
> @@ -6181,6 +6313,8 @@ int __init mem_cgroup_init(void)
> memcg_wq = alloc_workqueue("memcg", WQ_PERCPU, 0);
> WARN_ON(!memcg_wq);
>
> + update_tier_capacity_cache();
> +
> #if defined(CONFIG_MEMORY_HOTPLUG) && defined(CONFIG_NUMA)
> hotplug_node_notifier(memcg_tier_hotplug_cb, MEMCG_TIER_NODE_PRI);
> #endif
next prev parent reply other threads:[~2026-08-18 4:46 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
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 [this message]
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=749beeb5-cc97-42ab-b3a4-aeee378fbb12@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