[PATCH 2/2] mm/memcg: migrate per-node hugetlb lruvec stat together with hugetlb folio
From: Hongfu Li
Date: Mon Sep 21 2026 - 05:18:52 EST
From: Hongfu Li <lihongfu@xxxxxxxxxx>
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@xxxxxxxxxxxxxxx
Signed-off-by: Hongfu Li <lihongfu@xxxxxxxxxx>
---
mm/memcontrol.c | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 1460cba53588..9c96ebd5436f 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -5598,6 +5598,34 @@ void mem_cgroup_replace_folio(struct folio *old, struct folio *new)
rcu_read_unlock();
}
+#ifdef CONFIG_HUGETLB_PAGE
+static void move_hugetlb_lruvec_stat(struct obj_cgroup *objcg,
+ struct folio *old, struct folio *new)
+{
+ long nr_pages = folio_nr_pages(old);
+ struct mem_cgroup *memcg;
+ int old_nid = folio_nid(old);
+ int new_nid = folio_nid(new);
+
+ if (old_nid == new_nid)
+ return;
+
+ rcu_read_lock();
+ memcg = obj_cgroup_memcg(objcg);
+ 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);
+ rcu_read_unlock();
+}
+#else /* CONFIG_HUGETLB_PAGE */
+static inline void move_hugetlb_lruvec_stat(struct obj_cgroup *objcg,
+ struct folio *old,
+ struct folio *new)
+{
+}
+#endif /* CONFIG_HUGETLB_PAGE */
+
/**
* mem_cgroup_migrate - Transfer the memcg data from the old to the new folio.
* @old: Currently circulating folio.
@@ -5635,6 +5663,9 @@ void mem_cgroup_migrate(struct folio *old, struct folio *new)
new_objcg = get_migration_objcg(old, new);
+ if (folio_test_hugetlb(old))
+ move_hugetlb_lruvec_stat(new_objcg, old, new);
+
/*
* @old was charged through a non-root objcg, so its charge is in the
* page counters. If the re-derivation walked up to the root objcg -
--
2.54.0