mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/2] mm: fix hugetlb NR_HUGETLB accounting on folio migration
@ 2026-09-23  2:05 Hongfu Li
  2026-09-23  2:05 ` [PATCH v2 1/2] mm/hugetlb: account migration target folio in per-node NR_HUGETLB vmstat Hongfu Li
  2026-09-23  2:05 ` [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio Hongfu Li
  0 siblings, 2 replies; 8+ messages in thread
From: Hongfu Li @ 2026-09-23  2:05 UTC (permalink / raw)
  To: Muchun Song, Oscar Salvador, David Hildenbrand, Andrew Morton,
	Shakeel Butt, Michal Hocko, Johannes Weiner, Joshua Hahn,
	Nhat Pham, Michal Hocko, Roman Gushchin
  Cc: hongfu.li, Chris Down, linux-mm, linux-kernel, cgroups,
	Hongfu Li, stable

The hugeTLB counters added by 05d4532b60e3 ("memcg/hugetlb: add hugeTLB
counters to memcg") are maintained in two per-node places:
  - the per-node vmstat counter NR_HUGETLB, exposed as nr_hugetlb in
    /proc/vmstat;
  - the per-node memcg lruvec stat, exposed via memory.numa_stat.

Both are accounted against the folio's node, and both drift when a
hugetlb folio is migrated, though in different ways.

A migration target folio is allocated by alloc_hugetlb_folio_nodemask()
and inherits the old folio's state without ever being accounted, while
the old folio is freed right after and its free is accounted. That
alone loses vmstat accounting: the target node has no matching increment
for the decrement on the old node, so /proc/vmstat's nr_hugetlb shrinks
by nr_pages per migration. Patch 1 accounts the folio where it is
obtained, so the increment pairs with the free in free_huge_folio() on
the successful as well as the failed migration path. The memfd page
cache preallocation helper has the same asymmetry and is fixed in the
same patch.

The per-node lruvec stat breaks differently. mem_cgroup_migrate()
moves the charge to the new folio and drops the old folio's memcg data,
so the old folio's free right after migration skips the memcg per-node
decrement; the count stays attributed to the old node for the rest of
the charge's life, and the target folio never gets an increment on its
new node. Patch 2 moves that per-node accounting along with the charge,
in move_hugetlb_state().

---
Changes in v2:
- In patch 2/2, move_hugetlb_lruvec_stat() no longer takes the objcg
  parameter; it takes the memcg from the new folio and uses guard(rcu)().
- In patch 2/2, move_hugetlb_lruvec_stat() is moved to mm/hugetlb.c and is
  now called from move_hugetlb_state() instead of mem_cgroup_migrate().
- In patch 2/2, make mod_memcg_lruvec_state() non-static (declared in
  include/linux/memcontrol.h).
- Collected tags on both patches.

To: Muchun Song <muchun.song@linux.dev>
To: Oscar Salvador <osalvador@suse.de>
To: David Hildenbrand <david@kernel.org>
To: Andrew Morton <akpm@linux-foundation.org>
To: Chris Down <chris@chrisdown.name>
To: Johannes Weiner <hannes@cmpxchg.org>
To: Shakeel Butt <shakeel.butt@linux.dev>
To: Nhat Pham <nphamcs@gmail.com>
To: Michal Hocko <mhocko@kernel.org>
To: Roman Gushchin <roman.gushchin@linux.dev>
Cc: hongfu.li@linux.dev
Cc: Michal Hocko <mhocko@suse.com>
Cc: linux-mm@kvack.org
Cc: linux-kernel@vger.kernel.org
Cc: Joshua Hahn <joshua.hahnjy@gmail.com>
Cc: cgroups@vger.kernel.org

---
Hongfu Li (2):
      mm/hugetlb: account migration target folio in per-node NR_HUGETLB vmstat
      mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio

 include/linux/memcontrol.h |  8 +++++++
 mm/hugetlb.c               | 60 ++++++++++++++++++++++++++++++++++++----------
 mm/memcontrol.c            |  5 ++--
 3 files changed, 58 insertions(+), 15 deletions(-)
---
base-commit: 8d29b5365d528da545c1fe0768a55babadee95d0
change-id: 20260922-for-hugetlb_state3-573f27215d4e

Best regards,
--  
Hongfu Li <lihongfu@kylinos.cn>


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v2 1/2] mm/hugetlb: account migration target folio in per-node NR_HUGETLB vmstat
  2026-09-23  2:05 [PATCH v2 0/2] mm: fix hugetlb NR_HUGETLB accounting on folio migration Hongfu Li
@ 2026-09-23  2:05 ` Hongfu Li
  2026-09-23  2:05 ` [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio Hongfu Li
  1 sibling, 0 replies; 8+ messages in thread
From: Hongfu Li @ 2026-09-23  2:05 UTC (permalink / raw)
  To: Muchun Song, Oscar Salvador, David Hildenbrand, Andrew Morton,
	Shakeel Butt, Michal Hocko, Johannes Weiner, Joshua Hahn,
	Nhat Pham, Michal Hocko, Roman Gushchin
  Cc: hongfu.li, Chris Down, linux-mm, linux-kernel, cgroups,
	Hongfu Li, stable

From: Hongfu Li <lihongfu@kylinos.cn>

The NR_HUGETLB vmstat counter is maintained per folio's node: incremented
when a huge page is handed to a user via hugetlb_alloc_folio() and
decremented when it is returned to the pool via free_huge_folio().

A folio obtained by alloc_hugetlb_folio_nodemask() never goes through
hugetlb_alloc_folio(), so it is never accounted, while its free always
is. For a migration target this means the target node gets no matching
increment for the decrement on the old node, so the global nr_hugetlb in
/proc/vmstat drops by nr_pages for each migration. The same asymmetry
affects the failed migration path, which frees the target again right
away, and the temporary folio hugetlb_mfill_atomic_pte() takes from the
same helper.

alloc_hugetlb_folio_reserve(), used to preallocate the memfd page cache
folios, has the same asymmetry: the folio is handed to a user without
being accounted, while its free is accounted through free_huge_folio().

Account the folio where it is obtained, in alloc_hugetlb_folio_nodemask()
and alloc_hugetlb_folio_reserve(), so that the increment pairs with the
decrement in free_huge_folio(): a successful migration hands the folio
to a user, a failed one frees it again.

Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
Cc: stable@vger.kernel.org
Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
Acked-by: Muchun Song <muchun.song@linux.dev>
Acked-by: Oscar Salvador <osalvador@suse.de>
---
 mm/hugetlb.c | 35 +++++++++++++++++++++++------------
 1 file changed, 23 insertions(+), 12 deletions(-)

diff --git a/mm/hugetlb.c b/mm/hugetlb.c
index da980377d353..519c30b338a8 100644
--- a/mm/hugetlb.c
+++ b/mm/hugetlb.c
@@ -2206,6 +2206,11 @@ struct folio *alloc_hugetlb_folio_reserve(struct hstate *h, int preferred_nid,
 	}
 
 	spin_unlock_irq(&hugetlb_lock);
+
+	if (folio)
+		lruvec_stat_mod_folio(folio, NR_HUGETLB,
+				      folio_nr_pages(folio));
+
 	return folio;
 }
 
@@ -2213,24 +2218,30 @@ struct folio *alloc_hugetlb_folio_reserve(struct hstate *h, int preferred_nid,
 struct folio *alloc_hugetlb_folio_nodemask(struct hstate *h, int preferred_nid,
 		nodemask_t *nmask, gfp_t gfp_mask, bool allow_alloc_fallback)
 {
-	spin_lock_irq(&hugetlb_lock);
-	if (available_huge_pages(h)) {
-		struct folio *folio;
+	struct folio *folio = NULL;
 
+	spin_lock_irq(&hugetlb_lock);
+	if (available_huge_pages(h))
 		folio = dequeue_hugetlb_folio_nodemask(h, gfp_mask,
 						preferred_nid, nmask);
-		if (folio) {
-			spin_unlock_irq(&hugetlb_lock);
-			return folio;
-		}
-	}
 	spin_unlock_irq(&hugetlb_lock);
 
-	/* We cannot fallback to other nodes, as we could break the per-node pool. */
-	if (!allow_alloc_fallback)
-		gfp_mask |= __GFP_THISNODE;
+	if (!folio) {
+		/*
+		 * We cannot fallback to other nodes, as we could break the
+		 * per-node pool.
+		 */
+		if (!allow_alloc_fallback)
+			gfp_mask |= __GFP_THISNODE;
 
-	return alloc_migrate_hugetlb_folio(h, gfp_mask, preferred_nid, nmask);
+		folio = alloc_migrate_hugetlb_folio(h, gfp_mask, preferred_nid,
+						    nmask);
+	}
+
+	if (folio)
+		lruvec_stat_mod_folio(folio, NR_HUGETLB, folio_nr_pages(folio));
+
+	return folio;
 }
 
 static nodemask_t *policy_mbind_nodemask(gfp_t gfp)

-- 
2.54.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
  2026-09-23  2:05 [PATCH v2 0/2] mm: fix hugetlb NR_HUGETLB accounting on folio migration Hongfu Li
  2026-09-23  2:05 ` [PATCH v2 1/2] mm/hugetlb: account migration target folio in per-node NR_HUGETLB vmstat Hongfu Li
@ 2026-09-23  2:05 ` Hongfu Li
  2026-09-23  2:40   ` Muchun Song
  1 sibling, 1 reply; 8+ messages in thread
From: Hongfu Li @ 2026-09-23  2:05 UTC (permalink / raw)
  To: Muchun Song, Oscar Salvador, David Hildenbrand, Andrew Morton,
	Shakeel Butt, Michal Hocko, Johannes Weiner, Joshua Hahn,
	Nhat Pham, Michal Hocko, Roman Gushchin
  Cc: hongfu.li, Chris Down, linux-mm, linux-kernel, cgroups,
	Hongfu Li, stable

From: Hongfu Li <lihongfu@kylinos.cn>

memory.numa_stat exposes per-node hugetlb counters from per-node lruvec
stats. These stats are accounted against folio_nid(): incremented on
the folio's node when handed to a user, decremented when the folio is
returned to the pool.

During hugetlb folio migration, mem_cgroup_migrate() moves the charge
to the new folio and drops the memcg data of the old one, so the free
of the old folio right after the migration skips the memcg per-node
lruvec decrement. The hugetlb count stays attributed to the old node
for the rest of the life of the charge, while the target folio gets no
increment on the new node; its later free decrements a counter that
was never incremented.

Migrate the per-node lruvec accounting alongside migration. Global
memcg totals remain balanced because they track resource consumption,
not node placement.

Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
Cc: stable@vger.kernel.org
Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
Reviewed-by: Oscar Salvador <osalvador@suse.de>
---
 include/linux/memcontrol.h |  8 ++++++++
 mm/hugetlb.c               | 25 +++++++++++++++++++++++++
 mm/memcontrol.c            |  5 ++---
 3 files changed, 35 insertions(+), 3 deletions(-)

diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
index a8358f297b65..74110a324f9e 100644
--- a/include/linux/memcontrol.h
+++ b/include/linux/memcontrol.h
@@ -984,6 +984,9 @@ unsigned long lruvec_page_state_monotonic(const struct lruvec *lruvec,
 unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
 				      enum node_stat_item idx);
 
+void mod_memcg_lruvec_state(struct lruvec *lruvec,
+			    enum node_stat_item idx, int val);
+
 void mem_cgroup_flush_stats(struct mem_cgroup *memcg);
 void mem_cgroup_flush_stats_ratelimited(struct mem_cgroup *memcg);
 
@@ -1452,6 +1455,11 @@ static inline unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
 	return node_page_state(lruvec_pgdat(lruvec), idx);
 }
 
+static inline void mod_memcg_lruvec_state(struct lruvec *lruvec,
+					  enum node_stat_item idx, int val)
+{
+}
+
 static inline void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
 {
 }
diff --git a/mm/hugetlb.c b/mm/hugetlb.c
index 519c30b338a8..76d019594b39 100644
--- a/mm/hugetlb.c
+++ b/mm/hugetlb.c
@@ -23,6 +23,7 @@
 #include <linux/mmdebug.h>
 #include <linux/sched/signal.h>
 #include <linux/rmap.h>
+#include <linux/rcupdate.h>
 #include <linux/string_choices.h>
 #include <linux/string_helpers.h>
 #include <linux/swap.h>
@@ -7378,12 +7379,36 @@ void folio_putback_hugetlb(struct folio *folio)
 	folio_put(folio);
 }
 
+static void move_hugetlb_lruvec_stat(struct folio *old_folio,
+				     struct folio *new_folio)
+{
+	struct mem_cgroup *memcg;
+	long nr_pages = folio_nr_pages(old_folio);
+	int old_nid = folio_nid(old_folio);
+	int new_nid = folio_nid(new_folio);
+
+	if (old_nid == new_nid)
+		return;
+
+	guard(rcu)();
+
+	memcg = folio_memcg(new_folio);
+	if (!memcg)
+		return;
+
+	mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(old_nid)),
+			       NR_HUGETLB, -nr_pages);
+	mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(new_nid)),
+			       NR_HUGETLB, nr_pages);
+}
+
 void move_hugetlb_state(struct folio *old_folio, struct folio *new_folio,
 			enum migrate_reason reason)
 {
 	struct hstate *h = folio_hstate(old_folio);
 
 	hugetlb_cgroup_migrate(old_folio, new_folio);
+	move_hugetlb_lruvec_stat(old_folio, new_folio);
 	folio_set_owner_migrate_reason(new_folio, reason);
 
 	/*
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 88824f783571..a5335da5d425 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -1015,9 +1015,8 @@ static void __mod_memcg_lruvec_state(struct mem_cgroup_per_node *pn,
 	put_cpu();
 }
 
-static void mod_memcg_lruvec_state(struct lruvec *lruvec,
-				     enum node_stat_item idx,
-				     int val)
+void mod_memcg_lruvec_state(struct lruvec *lruvec,
+			    enum node_stat_item idx, int val)
 {
 	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
 	struct mem_cgroup_per_node *pn;

-- 
2.54.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
  2026-09-23  2:05 ` [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio Hongfu Li
@ 2026-09-23  2:40   ` Muchun Song
  2026-09-23  3:40     ` Hongfu Li
  0 siblings, 1 reply; 8+ messages in thread
From: Muchun Song @ 2026-09-23  2:40 UTC (permalink / raw)
  To: Hongfu Li
  Cc: Chris Down, linux-mm, linux-kernel, cgroups, stable, Hongfu Li,
	Oscar Salvador, David Hildenbrand, Andrew Morton, Shakeel Butt,
	Michal Hocko, Johannes Weiner, Joshua Hahn, Nhat Pham,
	Michal Hocko, Roman Gushchin



On 2026/9/23 10:05, Hongfu Li wrote:
> From: Hongfu Li <lihongfu@kylinos.cn>
>
> memory.numa_stat exposes per-node hugetlb counters from per-node lruvec
> stats. These stats are accounted against folio_nid(): incremented on
> the folio's node when handed to a user, decremented when the folio is
> returned to the pool.
>
> During hugetlb folio migration, mem_cgroup_migrate() moves the charge
> to the new folio and drops the memcg data of the old one, so the free
> of the old folio right after the migration skips the memcg per-node
> lruvec decrement. The hugetlb count stays attributed to the old node
> for the rest of the life of the charge, while the target folio gets no
> increment on the new node; its later free decrements a counter that
> was never incremented.
>
> Migrate the per-node lruvec accounting alongside migration. Global
> memcg totals remain balanced because they track resource consumption,
> not node placement.
>
> Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
> Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
> Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
> Reviewed-by: Oscar Salvador <osalvador@suse.de>
> ---
>   include/linux/memcontrol.h |  8 ++++++++
>   mm/hugetlb.c               | 25 +++++++++++++++++++++++++
>   mm/memcontrol.c            |  5 ++---
>   3 files changed, 35 insertions(+), 3 deletions(-)
>
> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
> index a8358f297b65..74110a324f9e 100644
> --- a/include/linux/memcontrol.h
> +++ b/include/linux/memcontrol.h
> @@ -984,6 +984,9 @@ unsigned long lruvec_page_state_monotonic(const struct lruvec *lruvec,
>   unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>   				      enum node_stat_item idx);
>   
> +void mod_memcg_lruvec_state(struct lruvec *lruvec,
> +			    enum node_stat_item idx, int val);
> +
>   void mem_cgroup_flush_stats(struct mem_cgroup *memcg);
>   void mem_cgroup_flush_stats_ratelimited(struct mem_cgroup *memcg);
>   
> @@ -1452,6 +1455,11 @@ static inline unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>   	return node_page_state(lruvec_pgdat(lruvec), idx);
>   }
>   
> +static inline void mod_memcg_lruvec_state(struct lruvec *lruvec,
> +					  enum node_stat_item idx, int val)
> +{
> +}
> +
>   static inline void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
>   {
>   }
> diff --git a/mm/hugetlb.c b/mm/hugetlb.c
> index 519c30b338a8..76d019594b39 100644
> --- a/mm/hugetlb.c
> +++ b/mm/hugetlb.c
> @@ -23,6 +23,7 @@
>   #include <linux/mmdebug.h>
>   #include <linux/sched/signal.h>
>   #include <linux/rmap.h>
> +#include <linux/rcupdate.h>
>   #include <linux/string_choices.h>
>   #include <linux/string_helpers.h>
>   #include <linux/swap.h>
> @@ -7378,12 +7379,36 @@ void folio_putback_hugetlb(struct folio *folio)
>   	folio_put(folio);
>   }
>   
> +static void move_hugetlb_lruvec_stat(struct folio *old_folio,
> +				     struct folio *new_folio)
> +{
> +	struct mem_cgroup *memcg;
> +	long nr_pages = folio_nr_pages(old_folio);
> +	int old_nid = folio_nid(old_folio);
> +	int new_nid = folio_nid(new_folio);
> +
> +	if (old_nid == new_nid)
> +		return;
> +
> +	guard(rcu)();
> +
> +	memcg = folio_memcg(new_folio);
> +	if (!memcg)
> +		return;
> +
> +	mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(old_nid)),
> +			       NR_HUGETLB, -nr_pages);

Why not use mod_lruvec_state? mod_memcg_lruvec_state is an internal
API for memcg, I don't want it to be exported.

Thanks.

> +	mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(new_nid)),
> +			       NR_HUGETLB, nr_pages);
> +}
> +
>   void move_hugetlb_state(struct folio *old_folio, struct folio *new_folio,
>   			enum migrate_reason reason)
>   {
>   	struct hstate *h = folio_hstate(old_folio);
>   
>   	hugetlb_cgroup_migrate(old_folio, new_folio);
> +	move_hugetlb_lruvec_stat(old_folio, new_folio);
>   	folio_set_owner_migrate_reason(new_folio, reason);
>   
>   	/*
> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
> index 88824f783571..a5335da5d425 100644
> --- a/mm/memcontrol.c
> +++ b/mm/memcontrol.c
> @@ -1015,9 +1015,8 @@ static void __mod_memcg_lruvec_state(struct mem_cgroup_per_node *pn,
>   	put_cpu();
>   }
>   
> -static void mod_memcg_lruvec_state(struct lruvec *lruvec,
> -				     enum node_stat_item idx,
> -				     int val)
> +void mod_memcg_lruvec_state(struct lruvec *lruvec,
> +			    enum node_stat_item idx, int val)
>   {
>   	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
>   	struct mem_cgroup_per_node *pn;
>


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
  2026-09-23  2:40   ` Muchun Song
@ 2026-09-23  3:40     ` Hongfu Li
  2026-09-23  7:56       ` Muchun Song
  0 siblings, 1 reply; 8+ messages in thread
From: Hongfu Li @ 2026-09-23  3:40 UTC (permalink / raw)
  To: Muchun Song, Hongfu Li
  Cc: hongfu.li, Chris Down, linux-mm, linux-kernel, cgroups, stable,
	Oscar Salvador, David Hildenbrand, Andrew Morton, Shakeel Butt,
	Michal Hocko, Johannes Weiner, Joshua Hahn, Nhat Pham,
	Michal Hocko, Roman Gushchin


On 9/23/26 10:40 AM, Muchun Song wrote:
>
>
> On 2026/9/23 10:05, Hongfu Li wrote:
>> From: Hongfu Li <lihongfu@kylinos.cn>
>>
>> memory.numa_stat exposes per-node hugetlb counters from per-node lruvec
>> stats. These stats are accounted against folio_nid(): incremented on
>> the folio's node when handed to a user, decremented when the folio is
>> returned to the pool.
>>
>> During hugetlb folio migration, mem_cgroup_migrate() moves the charge
>> to the new folio and drops the memcg data of the old one, so the free
>> of the old folio right after the migration skips the memcg per-node
>> lruvec decrement. The hugetlb count stays attributed to the old node
>> for the rest of the life of the charge, while the target folio gets no
>> increment on the new node; its later free decrements a counter that
>> was never incremented.
>>
>> Migrate the per-node lruvec accounting alongside migration. Global
>> memcg totals remain balanced because they track resource consumption,
>> not node placement.
>>
>> Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
>> Cc: stable@vger.kernel.org
>> Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
>> Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>> Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>> Reviewed-by: Oscar Salvador <osalvador@suse.de>
>> ---
>>   include/linux/memcontrol.h |  8 ++++++++
>>   mm/hugetlb.c               | 25 +++++++++++++++++++++++++
>>   mm/memcontrol.c            |  5 ++---
>>   3 files changed, 35 insertions(+), 3 deletions(-)
>>
>> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
>> index a8358f297b65..74110a324f9e 100644
>> --- a/include/linux/memcontrol.h
>> +++ b/include/linux/memcontrol.h
>> @@ -984,6 +984,9 @@ unsigned long lruvec_page_state_monotonic(const 
>> struct lruvec *lruvec,
>>   unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>                         enum node_stat_item idx);
>>   +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>> +                enum node_stat_item idx, int val);
>> +
>>   void mem_cgroup_flush_stats(struct mem_cgroup *memcg);
>>   void mem_cgroup_flush_stats_ratelimited(struct mem_cgroup *memcg);
>>   @@ -1452,6 +1455,11 @@ static inline unsigned long 
>> lruvec_page_state_local(const struct lruvec *lruvec,
>>       return node_page_state(lruvec_pgdat(lruvec), idx);
>>   }
>>   +static inline void mod_memcg_lruvec_state(struct lruvec *lruvec,
>> +                      enum node_stat_item idx, int val)
>> +{
>> +}
>> +
>>   static inline void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
>>   {
>>   }
>> diff --git a/mm/hugetlb.c b/mm/hugetlb.c
>> index 519c30b338a8..76d019594b39 100644
>> --- a/mm/hugetlb.c
>> +++ b/mm/hugetlb.c
>> @@ -23,6 +23,7 @@
>>   #include <linux/mmdebug.h>
>>   #include <linux/sched/signal.h>
>>   #include <linux/rmap.h>
>> +#include <linux/rcupdate.h>
>>   #include <linux/string_choices.h>
>>   #include <linux/string_helpers.h>
>>   #include <linux/swap.h>
>> @@ -7378,12 +7379,36 @@ void folio_putback_hugetlb(struct folio *folio)
>>       folio_put(folio);
>>   }
>>   +static void move_hugetlb_lruvec_stat(struct folio *old_folio,
>> +                     struct folio *new_folio)
>> +{
>> +    struct mem_cgroup *memcg;
>> +    long nr_pages = folio_nr_pages(old_folio);
>> +    int old_nid = folio_nid(old_folio);
>> +    int new_nid = folio_nid(new_folio);
>> +
>> +    if (old_nid == new_nid)
>> +        return;
>> +
>> +    guard(rcu)();
>> +
>> +    memcg = folio_memcg(new_folio);
>> +    if (!memcg)
>> +        return;
>> +
>> +    mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, 
>> NODE_DATA(old_nid)),
>> +                   NR_HUGETLB, -nr_pages);
>
> Why not use mod_lruvec_state? mod_memcg_lruvec_state is an internal
> API for memcg, I don't want it to be exported.

Thank you for the review.

mod_lruvec_state() would update the node counter a second time.  It calls
mod_node_page_state() as well, and the target's node counter is already
updated in alloc_hugetlb_folio_nodemask() (patch 1/2):

     lruvec_stat_mod_folio(folio, NR_HUGETLB, folio_nr_pages(folio));

For an uncharged folio lruvec_stat_mod_folio() only updates the node
counter.  The target folio is not charged to any memcg at that point; its
charge only appears later in mem_cgroup_migrate().  So the node side is
already covered and only the per-memcg attribution has to follow the
charge here.

>
> Thanks.
>
>> + mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(new_nid)),
>> +                   NR_HUGETLB, nr_pages);
>> +}
>> +
>>   void move_hugetlb_state(struct folio *old_folio, struct folio 
>> *new_folio,
>>               enum migrate_reason reason)
>>   {
>>       struct hstate *h = folio_hstate(old_folio);
>>         hugetlb_cgroup_migrate(old_folio, new_folio);
>> +    move_hugetlb_lruvec_stat(old_folio, new_folio);
>>       folio_set_owner_migrate_reason(new_folio, reason);
>>         /*
>> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
>> index 88824f783571..a5335da5d425 100644
>> --- a/mm/memcontrol.c
>> +++ b/mm/memcontrol.c
>> @@ -1015,9 +1015,8 @@ static void __mod_memcg_lruvec_state(struct 
>> mem_cgroup_per_node *pn,
>>       put_cpu();
>>   }
>>   -static void mod_memcg_lruvec_state(struct lruvec *lruvec,
>> -                     enum node_stat_item idx,
>> -                     int val)
>> +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>> +                enum node_stat_item idx, int val)
>>   {
>>       struct pglist_data *pgdat = lruvec_pgdat(lruvec);
>>       struct mem_cgroup_per_node *pn;
>>
>
-- 
Best regards,
Hongfu


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
  2026-09-23  3:40     ` Hongfu Li
@ 2026-09-23  7:56       ` Muchun Song
  2026-09-23  9:55         ` Hongfu Li
  0 siblings, 1 reply; 8+ messages in thread
From: Muchun Song @ 2026-09-23  7:56 UTC (permalink / raw)
  To: Hongfu Li
  Cc: Hongfu Li, Chris Down, linux-mm, linux-kernel, cgroups, stable,
	Oscar Salvador, David Hildenbrand, Andrew Morton, Shakeel Butt,
	Michal Hocko, Johannes Weiner, Joshua Hahn, Nhat Pham,
	Michal Hocko, Roman Gushchin



> On Sep 23, 2026, at 11:40, Hongfu Li <hongfu.li@linux.dev> wrote:
> 
> 
> On 9/23/26 10:40 AM, Muchun Song wrote:
>> 
>> 
>> On 2026/9/23 10:05, Hongfu Li wrote:
>>> From: Hongfu Li <lihongfu@kylinos.cn>
>>> 
>>> memory.numa_stat exposes per-node hugetlb counters from per-node lruvec
>>> stats. These stats are accounted against folio_nid(): incremented on
>>> the folio's node when handed to a user, decremented when the folio is
>>> returned to the pool.
>>> 
>>> During hugetlb folio migration, mem_cgroup_migrate() moves the charge
>>> to the new folio and drops the memcg data of the old one, so the free
>>> of the old folio right after the migration skips the memcg per-node
>>> lruvec decrement. The hugetlb count stays attributed to the old node
>>> for the rest of the life of the charge, while the target folio gets no
>>> increment on the new node; its later free decrements a counter that
>>> was never incremented.
>>> 
>>> Migrate the per-node lruvec accounting alongside migration. Global
>>> memcg totals remain balanced because they track resource consumption,
>>> not node placement.
>>> 
>>> Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
>>> Cc: stable@vger.kernel.org
>>> Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
>>> Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>>> Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>>> Reviewed-by: Oscar Salvador <osalvador@suse.de>
>>> ---
>>>   include/linux/memcontrol.h |  8 ++++++++
>>>   mm/hugetlb.c               | 25 +++++++++++++++++++++++++
>>>   mm/memcontrol.c            |  5 ++---
>>>   3 files changed, 35 insertions(+), 3 deletions(-)
>>> 
>>> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
>>> index a8358f297b65..74110a324f9e 100644
>>> --- a/include/linux/memcontrol.h
>>> +++ b/include/linux/memcontrol.h
>>> @@ -984,6 +984,9 @@ unsigned long lruvec_page_state_monotonic(const struct lruvec *lruvec,
>>>   unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>>                         enum node_stat_item idx);
>>>   +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>> +                enum node_stat_item idx, int val);
>>> +
>>>   void mem_cgroup_flush_stats(struct mem_cgroup *memcg);
>>>   void mem_cgroup_flush_stats_ratelimited(struct mem_cgroup *memcg);
>>>   @@ -1452,6 +1455,11 @@ static inline unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>>       return node_page_state(lruvec_pgdat(lruvec), idx);
>>>   }
>>>   +static inline void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>> +                      enum node_stat_item idx, int val)
>>> +{
>>> +}
>>> +
>>>   static inline void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
>>>   {
>>>   }
>>> diff --git a/mm/hugetlb.c b/mm/hugetlb.c
>>> index 519c30b338a8..76d019594b39 100644
>>> --- a/mm/hugetlb.c
>>> +++ b/mm/hugetlb.c
>>> @@ -23,6 +23,7 @@
>>>   #include <linux/mmdebug.h>
>>>   #include <linux/sched/signal.h>
>>>   #include <linux/rmap.h>
>>> +#include <linux/rcupdate.h>
>>>   #include <linux/string_choices.h>
>>>   #include <linux/string_helpers.h>
>>>   #include <linux/swap.h>
>>> @@ -7378,12 +7379,36 @@ void folio_putback_hugetlb(struct folio *folio)
>>>       folio_put(folio);
>>>   }
>>>   +static void move_hugetlb_lruvec_stat(struct folio *old_folio,
>>> +                     struct folio *new_folio)
>>> +{
>>> +    struct mem_cgroup *memcg;
>>> +    long nr_pages = folio_nr_pages(old_folio);
>>> +    int old_nid = folio_nid(old_folio);
>>> +    int new_nid = folio_nid(new_folio);
>>> +
>>> +    if (old_nid == new_nid)
>>> +        return;
>>> +
>>> +    guard(rcu)();
>>> +
>>> +    memcg = folio_memcg(new_folio);
>>> +    if (!memcg)
>>> +        return;
>>> +
>>> +    mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(old_nid)),
>>> +                   NR_HUGETLB, -nr_pages);
>> 
>> Why not use mod_lruvec_state? mod_memcg_lruvec_state is an internal
>> API for memcg, I don't want it to be exported.
> 
> Thank you for the review.
> 
> mod_lruvec_state() would update the node counter a second time.  It calls
> mod_node_page_state() as well, and the target's node counter is already
> updated in alloc_hugetlb_folio_nodemask() (patch 1/2):
> 
>     lruvec_stat_mod_folio(folio, NR_HUGETLB, folio_nr_pages(folio));
> 
> For an uncharged folio lruvec_stat_mod_folio() only updates the node
> counter.  The target folio is not charged to any memcg at that point; its
> charge only appears later in mem_cgroup_migrate().  So the node side is
> already covered and only the per-memcg attribution has to follow the
> charge here.

Looking at the first patch again, I do not think the
overall accounting is incorrect. My concern is where NR_HUGETLB
accounting is performed.

The low-level allocation helpers only take a folio from the
hugetlb pool. They do not have enough context to decide how
the folio should be accounted. That decision should be left
to their callers, once the folio enters the relevant usage
lifecycle.

hugetlb_alloc_folio() is a good example. It first obtains a
folio, then calls mem_cgroup_charge_hugetlb(), and only
afterwards calls lruvec_stat_mod_folio(). At that point,
the folio has the correct memcg and lruvec, so all relevant
counters can be updated together.

After this series, alloc_hugetlb_folio_reserve() and
alloc_hugetlb_folio_nodemask() account NR_HUGETLB before
returning the folio. This makes them unsuitable for callers
that need to charge the folio afterwards. Before the charge,
lruvec_stat_mod_folio() can update only the node counter.
The later charge does not associate that earlier update with
the new memcg.

If a caller accounts again after charging, the node counter is
updated twice. If it does not, the memcg and per-node lruvec
counters remain missing. The caller then needs a memcg-only
correction, creating a special accounting protocol between
the helper and its callers.

Migration exposes the same issue. The target gets node
accounting before it inherits the source memcg. The second patch
must update the lruvec state and then cancel its node changes,
because the target allocation already updated the node counter.
This works, but makes the accounting lifecycle harder to follow.

Would it be cleaner to keep these helpers focused on allocation
and let each caller account at the correct lifecycle point?
Normal allocation can account after a successful memcg charge.
Migration can move the complete lruvec state after it succeeds.
An uncharged path can explicitly update only the node counter
if global accounting is still required.

This would keep policy out of the low-level allocator and make
the accounting lifecycle easier to follow.

Thanks.

> 
>> 
>> Thanks.
>> 
>>> + mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(new_nid)),
>>> +                   NR_HUGETLB, nr_pages);
>>> +}
>>> +
>>>   void move_hugetlb_state(struct folio *old_folio, struct folio *new_folio,
>>>               enum migrate_reason reason)
>>>   {
>>>       struct hstate *h = folio_hstate(old_folio);
>>>         hugetlb_cgroup_migrate(old_folio, new_folio);
>>> +    move_hugetlb_lruvec_stat(old_folio, new_folio);
>>>       folio_set_owner_migrate_reason(new_folio, reason);
>>>         /*
>>> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
>>> index 88824f783571..a5335da5d425 100644
>>> --- a/mm/memcontrol.c
>>> +++ b/mm/memcontrol.c
>>> @@ -1015,9 +1015,8 @@ static void __mod_memcg_lruvec_state(struct mem_cgroup_per_node *pn,
>>>       put_cpu();
>>>   }
>>>   -static void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>> -                     enum node_stat_item idx,
>>> -                     int val)
>>> +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>> +                enum node_stat_item idx, int val)
>>>   {
>>>       struct pglist_data *pgdat = lruvec_pgdat(lruvec);
>>>       struct mem_cgroup_per_node *pn;
>>> 
>> 
> -- 
> Best regards,
> Hongfu



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
  2026-09-23  7:56       ` Muchun Song
@ 2026-09-23  9:55         ` Hongfu Li
  2026-09-23 11:31           ` Muchun Song
  0 siblings, 1 reply; 8+ messages in thread
From: Hongfu Li @ 2026-09-23  9:55 UTC (permalink / raw)
  To: Muchun Song
  Cc: hongfu.li, Hongfu Li, Chris Down, linux-mm, linux-kernel,
	cgroups, stable, Oscar Salvador, David Hildenbrand,
	Andrew Morton, Shakeel Butt, Michal Hocko, Johannes Weiner,
	Joshua Hahn, Nhat Pham, Michal Hocko, Roman Gushchin


On 9/23/26 3:56 PM, Muchun Song wrote:
>
>> On Sep 23, 2026, at 11:40, Hongfu Li <hongfu.li@linux.dev> wrote:
>>
>>
>> On 9/23/26 10:40 AM, Muchun Song wrote:
>>>
>>> On 2026/9/23 10:05, Hongfu Li wrote:
>>>> From: Hongfu Li <lihongfu@kylinos.cn>
>>>>
>>>> memory.numa_stat exposes per-node hugetlb counters from per-node lruvec
>>>> stats. These stats are accounted against folio_nid(): incremented on
>>>> the folio's node when handed to a user, decremented when the folio is
>>>> returned to the pool.
>>>>
>>>> During hugetlb folio migration, mem_cgroup_migrate() moves the charge
>>>> to the new folio and drops the memcg data of the old one, so the free
>>>> of the old folio right after the migration skips the memcg per-node
>>>> lruvec decrement. The hugetlb count stays attributed to the old node
>>>> for the rest of the life of the charge, while the target folio gets no
>>>> increment on the new node; its later free decrements a counter that
>>>> was never incremented.
>>>>
>>>> Migrate the per-node lruvec accounting alongside migration. Global
>>>> memcg totals remain balanced because they track resource consumption,
>>>> not node placement.
>>>>
>>>> Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
>>>> Cc: stable@vger.kernel.org
>>>> Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
>>>> Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>>>> Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>>>> Reviewed-by: Oscar Salvador <osalvador@suse.de>
>>>> ---
>>>>    include/linux/memcontrol.h |  8 ++++++++
>>>>    mm/hugetlb.c               | 25 +++++++++++++++++++++++++
>>>>    mm/memcontrol.c            |  5 ++---
>>>>    3 files changed, 35 insertions(+), 3 deletions(-)
>>>>
>>>> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
>>>> index a8358f297b65..74110a324f9e 100644
>>>> --- a/include/linux/memcontrol.h
>>>> +++ b/include/linux/memcontrol.h
>>>> @@ -984,6 +984,9 @@ unsigned long lruvec_page_state_monotonic(const struct lruvec *lruvec,
>>>>    unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>>>                          enum node_stat_item idx);
>>>>    +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>> +                enum node_stat_item idx, int val);
>>>> +
>>>>    void mem_cgroup_flush_stats(struct mem_cgroup *memcg);
>>>>    void mem_cgroup_flush_stats_ratelimited(struct mem_cgroup *memcg);
>>>>    @@ -1452,6 +1455,11 @@ static inline unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>>>        return node_page_state(lruvec_pgdat(lruvec), idx);
>>>>    }
>>>>    +static inline void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>> +                      enum node_stat_item idx, int val)
>>>> +{
>>>> +}
>>>> +
>>>>    static inline void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
>>>>    {
>>>>    }
>>>> diff --git a/mm/hugetlb.c b/mm/hugetlb.c
>>>> index 519c30b338a8..76d019594b39 100644
>>>> --- a/mm/hugetlb.c
>>>> +++ b/mm/hugetlb.c
>>>> @@ -23,6 +23,7 @@
>>>>    #include <linux/mmdebug.h>
>>>>    #include <linux/sched/signal.h>
>>>>    #include <linux/rmap.h>
>>>> +#include <linux/rcupdate.h>
>>>>    #include <linux/string_choices.h>
>>>>    #include <linux/string_helpers.h>
>>>>    #include <linux/swap.h>
>>>> @@ -7378,12 +7379,36 @@ void folio_putback_hugetlb(struct folio *folio)
>>>>        folio_put(folio);
>>>>    }
>>>>    +static void move_hugetlb_lruvec_stat(struct folio *old_folio,
>>>> +                     struct folio *new_folio)
>>>> +{
>>>> +    struct mem_cgroup *memcg;
>>>> +    long nr_pages = folio_nr_pages(old_folio);
>>>> +    int old_nid = folio_nid(old_folio);
>>>> +    int new_nid = folio_nid(new_folio);
>>>> +
>>>> +    if (old_nid == new_nid)
>>>> +        return;
>>>> +
>>>> +    guard(rcu)();
>>>> +
>>>> +    memcg = folio_memcg(new_folio);
>>>> +    if (!memcg)
>>>> +        return;
>>>> +
>>>> +    mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(old_nid)),
>>>> +                   NR_HUGETLB, -nr_pages);
>>> Why not use mod_lruvec_state? mod_memcg_lruvec_state is an internal
>>> API for memcg, I don't want it to be exported.
>> Thank you for the review.
>>
>> mod_lruvec_state() would update the node counter a second time.  It calls
>> mod_node_page_state() as well, and the target's node counter is already
>> updated in alloc_hugetlb_folio_nodemask() (patch 1/2):
>>
>>      lruvec_stat_mod_folio(folio, NR_HUGETLB, folio_nr_pages(folio));
>>
>> For an uncharged folio lruvec_stat_mod_folio() only updates the node
>> counter.  The target folio is not charged to any memcg at that point; its
>> charge only appears later in mem_cgroup_migrate().  So the node side is
>> already covered and only the per-memcg attribution has to follow the
>> charge here.
> Looking at the first patch again, I do not think the
> overall accounting is incorrect. My concern is where NR_HUGETLB
> accounting is performed.
>
> The low-level allocation helpers only take a folio from the
> hugetlb pool. They do not have enough context to decide how
> the folio should be accounted. That decision should be left
> to their callers, once the folio enters the relevant usage
> lifecycle.
>
> hugetlb_alloc_folio() is a good example. It first obtains a
> folio, then calls mem_cgroup_charge_hugetlb(), and only
> afterwards calls lruvec_stat_mod_folio(). At that point,
> the folio has the correct memcg and lruvec, so all relevant
> counters can be updated together.
>
> After this series, alloc_hugetlb_folio_reserve() and
> alloc_hugetlb_folio_nodemask() account NR_HUGETLB before
> returning the folio. This makes them unsuitable for callers
> that need to charge the folio afterwards. Before the charge,
> lruvec_stat_mod_folio() can update only the node counter.
> The later charge does not associate that earlier update with
> the new memcg.
>
> If a caller accounts again after charging, the node counter is
> updated twice. If it does not, the memcg and per-node lruvec
> counters remain missing. The caller then needs a memcg-only
> correction, creating a special accounting protocol between
> the helper and its callers.
>
> Migration exposes the same issue. The target gets node
> accounting before it inherits the source memcg. The second patch
> must update the lruvec state and then cancel its node changes,
> because the target allocation already updated the node counter.
> This works, but makes the accounting lifecycle harder to follow.
>
> Would it be cleaner to keep these helpers focused on allocation
> and let each caller account at the correct lifecycle point?
> Normal allocation can account after a successful memcg charge.
> Migration can move the complete lruvec state after it succeeds.

Hi Muchun,

Thanks a lot for the detailed explanation.

One thing keeps the target accounting in alloc_hugetlb_folio_nodemask(): the
node counter is paired with the folio's free, and a failed migration 
frees its
target right away (put_new_folio()/folio_put() -> free_huge_folio(), which
always decrements the node counter). If the target were accounted only 
after a
successful migration, every failed migration would decrement a node counter
that was never incremented.

Unless there is a way to let free_huge_folio() tell that the folio comes
from a failed migration, and skip the decrement for it.

> An uncharged path can explicitly update only the node counter
> if global accounting is still required.
>
> This would keep policy out of the low-level allocator and make
> the accounting lifecycle easier to follow.
>
> Thanks.
>
>>> Thanks.
>>>
>>>> + mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(new_nid)),
>>>> +                   NR_HUGETLB, nr_pages);
>>>> +}
>>>> +
>>>>    void move_hugetlb_state(struct folio *old_folio, struct folio *new_folio,
>>>>                enum migrate_reason reason)
>>>>    {
>>>>        struct hstate *h = folio_hstate(old_folio);
>>>>          hugetlb_cgroup_migrate(old_folio, new_folio);
>>>> +    move_hugetlb_lruvec_stat(old_folio, new_folio);
>>>>        folio_set_owner_migrate_reason(new_folio, reason);
>>>>          /*
>>>> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
>>>> index 88824f783571..a5335da5d425 100644
>>>> --- a/mm/memcontrol.c
>>>> +++ b/mm/memcontrol.c
>>>> @@ -1015,9 +1015,8 @@ static void __mod_memcg_lruvec_state(struct mem_cgroup_per_node *pn,
>>>>        put_cpu();
>>>>    }
>>>>    -static void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>> -                     enum node_stat_item idx,
>>>> -                     int val)
>>>> +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>> +                enum node_stat_item idx, int val)
>>>>    {
>>>>        struct pglist_data *pgdat = lruvec_pgdat(lruvec);
>>>>        struct mem_cgroup_per_node *pn;
>>>>
>> -- 
>> Best regards,
>> Hongfu

-- 
Best regards,
Hongfu


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
  2026-09-23  9:55         ` Hongfu Li
@ 2026-09-23 11:31           ` Muchun Song
  0 siblings, 0 replies; 8+ messages in thread
From: Muchun Song @ 2026-09-23 11:31 UTC (permalink / raw)
  To: Hongfu Li
  Cc: Hongfu Li, Chris Down, linux-mm, linux-kernel, cgroups, stable,
	Oscar Salvador, David Hildenbrand, Andrew Morton, Shakeel Butt,
	Michal Hocko, Johannes Weiner, Joshua Hahn, Nhat Pham,
	Michal Hocko, Roman Gushchin



> On Sep 23, 2026, at 17:55, Hongfu Li <hongfu.li@linux.dev> wrote:
> 
> 
> On 9/23/26 3:56 PM, Muchun Song wrote:
>> 
>>> On Sep 23, 2026, at 11:40, Hongfu Li <hongfu.li@linux.dev> wrote:
>>> 
>>> 
>>> On 9/23/26 10:40 AM, Muchun Song wrote:
>>>> 
>>>> On 2026/9/23 10:05, Hongfu Li wrote:
>>>>> From: Hongfu Li <lihongfu@kylinos.cn>
>>>>> 
>>>>> memory.numa_stat exposes per-node hugetlb counters from per-node lruvec
>>>>> stats. These stats are accounted against folio_nid(): incremented on
>>>>> the folio's node when handed to a user, decremented when the folio is
>>>>> returned to the pool.
>>>>> 
>>>>> During hugetlb folio migration, mem_cgroup_migrate() moves the charge
>>>>> to the new folio and drops the memcg data of the old one, so the free
>>>>> of the old folio right after the migration skips the memcg per-node
>>>>> lruvec decrement. The hugetlb count stays attributed to the old node
>>>>> for the rest of the life of the charge, while the target folio gets no
>>>>> increment on the new node; its later free decrements a counter that
>>>>> was never incremented.
>>>>> 
>>>>> Migrate the per-node lruvec accounting alongside migration. Global
>>>>> memcg totals remain balanced because they track resource consumption,
>>>>> not node placement.
>>>>> 
>>>>> Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
>>>>> Cc: stable@vger.kernel.org
>>>>> Signed-off-by: Hongfu Li <lihongfu@kylinos.cn>
>>>>> Tested-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>>>>> Reviewed-by: Joshua Hahn <joshua.hahnjy@gmail.com>
>>>>> Reviewed-by: Oscar Salvador <osalvador@suse.de>
>>>>> ---
>>>>>   include/linux/memcontrol.h |  8 ++++++++
>>>>>   mm/hugetlb.c               | 25 +++++++++++++++++++++++++
>>>>>   mm/memcontrol.c            |  5 ++---
>>>>>   3 files changed, 35 insertions(+), 3 deletions(-)
>>>>> 
>>>>> diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
>>>>> index a8358f297b65..74110a324f9e 100644
>>>>> --- a/include/linux/memcontrol.h
>>>>> +++ b/include/linux/memcontrol.h
>>>>> @@ -984,6 +984,9 @@ unsigned long lruvec_page_state_monotonic(const struct lruvec *lruvec,
>>>>>   unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>>>>                         enum node_stat_item idx);
>>>>>   +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>>> +                enum node_stat_item idx, int val);
>>>>> +
>>>>>   void mem_cgroup_flush_stats(struct mem_cgroup *memcg);
>>>>>   void mem_cgroup_flush_stats_ratelimited(struct mem_cgroup *memcg);
>>>>>   @@ -1452,6 +1455,11 @@ static inline unsigned long lruvec_page_state_local(const struct lruvec *lruvec,
>>>>>       return node_page_state(lruvec_pgdat(lruvec), idx);
>>>>>   }
>>>>>   +static inline void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>>> +                      enum node_stat_item idx, int val)
>>>>> +{
>>>>> +}
>>>>> +
>>>>>   static inline void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
>>>>>   {
>>>>>   }
>>>>> diff --git a/mm/hugetlb.c b/mm/hugetlb.c
>>>>> index 519c30b338a8..76d019594b39 100644
>>>>> --- a/mm/hugetlb.c
>>>>> +++ b/mm/hugetlb.c
>>>>> @@ -23,6 +23,7 @@
>>>>>   #include <linux/mmdebug.h>
>>>>>   #include <linux/sched/signal.h>
>>>>>   #include <linux/rmap.h>
>>>>> +#include <linux/rcupdate.h>
>>>>>   #include <linux/string_choices.h>
>>>>>   #include <linux/string_helpers.h>
>>>>>   #include <linux/swap.h>
>>>>> @@ -7378,12 +7379,36 @@ void folio_putback_hugetlb(struct folio *folio)
>>>>>       folio_put(folio);
>>>>>   }
>>>>>   +static void move_hugetlb_lruvec_stat(struct folio *old_folio,
>>>>> +                     struct folio *new_folio)
>>>>> +{
>>>>> +    struct mem_cgroup *memcg;
>>>>> +    long nr_pages = folio_nr_pages(old_folio);
>>>>> +    int old_nid = folio_nid(old_folio);
>>>>> +    int new_nid = folio_nid(new_folio);
>>>>> +
>>>>> +    if (old_nid == new_nid)
>>>>> +        return;
>>>>> +
>>>>> +    guard(rcu)();
>>>>> +
>>>>> +    memcg = folio_memcg(new_folio);
>>>>> +    if (!memcg)
>>>>> +        return;
>>>>> +
>>>>> +    mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(old_nid)),
>>>>> +                   NR_HUGETLB, -nr_pages);
>>>> Why not use mod_lruvec_state? mod_memcg_lruvec_state is an internal
>>>> API for memcg, I don't want it to be exported.
>>> Thank you for the review.
>>> 
>>> mod_lruvec_state() would update the node counter a second time.  It calls
>>> mod_node_page_state() as well, and the target's node counter is already
>>> updated in alloc_hugetlb_folio_nodemask() (patch 1/2):
>>> 
>>>     lruvec_stat_mod_folio(folio, NR_HUGETLB, folio_nr_pages(folio));
>>> 
>>> For an uncharged folio lruvec_stat_mod_folio() only updates the node
>>> counter.  The target folio is not charged to any memcg at that point; its
>>> charge only appears later in mem_cgroup_migrate().  So the node side is
>>> already covered and only the per-memcg attribution has to follow the
>>> charge here.
>> Looking at the first patch again, I do not think the
>> overall accounting is incorrect. My concern is where NR_HUGETLB
>> accounting is performed.
>> 
>> The low-level allocation helpers only take a folio from the
>> hugetlb pool. They do not have enough context to decide how
>> the folio should be accounted. That decision should be left
>> to their callers, once the folio enters the relevant usage
>> lifecycle.
>> 
>> hugetlb_alloc_folio() is a good example. It first obtains a
>> folio, then calls mem_cgroup_charge_hugetlb(), and only
>> afterwards calls lruvec_stat_mod_folio(). At that point,
>> the folio has the correct memcg and lruvec, so all relevant
>> counters can be updated together.
>> 
>> After this series, alloc_hugetlb_folio_reserve() and
>> alloc_hugetlb_folio_nodemask() account NR_HUGETLB before
>> returning the folio. This makes them unsuitable for callers
>> that need to charge the folio afterwards. Before the charge,
>> lruvec_stat_mod_folio() can update only the node counter.
>> The later charge does not associate that earlier update with
>> the new memcg.
>> 
>> If a caller accounts again after charging, the node counter is
>> updated twice. If it does not, the memcg and per-node lruvec
>> counters remain missing. The caller then needs a memcg-only
>> correction, creating a special accounting protocol between
>> the helper and its callers.
>> 
>> Migration exposes the same issue. The target gets node
>> accounting before it inherits the source memcg. The second patch
>> must update the lruvec state and then cancel its node changes,
>> because the target allocation already updated the node counter.
>> This works, but makes the accounting lifecycle harder to follow.
>> 
>> Would it be cleaner to keep these helpers focused on allocation
>> and let each caller account at the correct lifecycle point?
>> Normal allocation can account after a successful memcg charge.
>> Migration can move the complete lruvec state after it succeeds.
> 
> Hi Muchun,

Hi,

> 
> Thanks a lot for the detailed explanation.
> 
> One thing keeps the target accounting in alloc_hugetlb_folio_nodemask(): the
> node counter is paired with the folio's free, and a failed migration frees its
> target right away (put_new_folio()/folio_put() -> free_huge_folio(), which
> always decrements the node counter). If the target were accounted only after a
> successful migration, every failed migration would decrement a node counter
> that was never incremented.

You're right, and HugeTLB is really tricky. I think HugeTLB might need some cleanup
in the future to better handle the accounting here. As a bug fix, the current
changes look good to me.

Acked-by: Muchun Song <muchun.song@linux.dev>

A future cleanup, might be to have all the underlying HugeTLB allocation functions
update only the global node statistics (use mod_node_page_state instead	of
lruvec_stat_mod_folio). Its caller, if it needs to charge memory
cgroup, can then call mod_memcg_lruvec_state that updates only the memory cgroup
statistics.

Thanks.

> 
> Unless there is a way to let free_huge_folio() tell that the folio comes
> from a failed migration, and skip the decrement for it.
> 
>> An uncharged path can explicitly update only the node counter
>> if global accounting is still required.
>> 
>> This would keep policy out of the low-level allocator and make
>> the accounting lifecycle easier to follow.
>> 
>> Thanks.
>> 
>>>> Thanks.
>>>> 
>>>>> + mod_memcg_lruvec_state(mem_cgroup_lruvec(memcg, NODE_DATA(new_nid)),
>>>>> +                   NR_HUGETLB, nr_pages);
>>>>> +}
>>>>> +
>>>>>   void move_hugetlb_state(struct folio *old_folio, struct folio *new_folio,
>>>>>               enum migrate_reason reason)
>>>>>   {
>>>>>       struct hstate *h = folio_hstate(old_folio);
>>>>>         hugetlb_cgroup_migrate(old_folio, new_folio);
>>>>> +    move_hugetlb_lruvec_stat(old_folio, new_folio);
>>>>>       folio_set_owner_migrate_reason(new_folio, reason);
>>>>>         /*
>>>>> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
>>>>> index 88824f783571..a5335da5d425 100644
>>>>> --- a/mm/memcontrol.c
>>>>> +++ b/mm/memcontrol.c
>>>>> @@ -1015,9 +1015,8 @@ static void __mod_memcg_lruvec_state(struct mem_cgroup_per_node *pn,
>>>>>       put_cpu();
>>>>>   }
>>>>>   -static void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>>> -                     enum node_stat_item idx,
>>>>> -                     int val)
>>>>> +void mod_memcg_lruvec_state(struct lruvec *lruvec,
>>>>> +                enum node_stat_item idx, int val)
>>>>>   {
>>>>>       struct pglist_data *pgdat = lruvec_pgdat(lruvec);
>>>>>       struct mem_cgroup_per_node *pn;
>>>>> 
>>> -- 
>>> Best regards,
>>> Hongfu
> 
> -- 
> Best regards,
> Hongfu



^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-09-23 11:31 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23  2:05 [PATCH v2 0/2] mm: fix hugetlb NR_HUGETLB accounting on folio migration Hongfu Li
2026-09-23  2:05 ` [PATCH v2 1/2] mm/hugetlb: account migration target folio in per-node NR_HUGETLB vmstat Hongfu Li
2026-09-23  2:05 ` [PATCH v2 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio Hongfu Li
2026-09-23  2:40   ` Muchun Song
2026-09-23  3:40     ` Hongfu Li
2026-09-23  7:56       ` Muchun Song
2026-09-23  9:55         ` Hongfu Li
2026-09-23 11:31           ` Muchun Song

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®