memcg: cache obj_stock by memcg, not by objcg pointer
Subsystems (3)
Shakeel Butt
maintainerPATCH v3 1/1 memcg: cache obj_stock by memcg, not by objcg pointer
Commit 01b9da291c49 ("mm: memcontrol: convert objcg to be per-memcg per-node type") split a memcg's single obj_cgroup into one per NUMA node, but the per-CPU obj_stock_pcp still keys cached_objcg by pointer. Cross-NUMA workloads now see a drain on every refill and a miss on every consume that targets a sibling per-node objcg of the same memcg, producing the 67.7% stress-ng switch-mq regression reported by LKP.
stock->nr_bytes are fungible across per-node objcgs of one memcg. Treat the cache as keyed by memcg in __consume_obj_stock() and __refill_obj_stock() so siblings share the reserve. Compare via READ_ONCE(objcg->memcg) directly: pointer-compare only, no deref, so the rcu_read_lock contract on obj_cgroup_memcg() does not apply.
Sharing the reserve without re-caching means bytes funded by one per-node objcg's slow path can be consumed/freed under a different sibling, leaving sub-page residue on whichever sibling was cached at drain time. The pre-existing obj_cgroup_release() path would WARN and silently drop that residue, leaking up to nr_node_ids * (PAGE_SIZE - 1) bytes per memcg lifecycle from the page_counter. Forward the residue into a per-node objcg of the same (post-reparent) memcg at release time instead, so it can be reconciled later via a refill atomic_xchg or another release; the chain terminates at root_mem_cgroup, whose page_counter has no enforced limit.
Please note that this is temporary fix and will be reverted when per-node kmem accounting is introduced.
Update the stale invariant comment on __account_obj_stock().
Qi Zheng built a specialized reproducer [1] for the corner case and confirmed the fix.
---
Changes since v2:
https://lore.kernel.org/[email protected]/
- Instead of handling sub-page charged residue at refill time, let's handle it at the obj_cgroup_release time.
Changes since v1:
https://lore.kernel.org/[email protected]/
- Fix the rcu warning (Sashiko).
- Fix the page counter possible underflow warning (Sashiko).
mm/memcontrol.c | 69 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 18 deletions(-)
mm/memcontrol.c +51 -18
@@ -142,14 +142,24 @@ static void obj_cgroup_release(struct percpu_ref *ref) struct obj_cgroup *objcg = container_of(ref, struct obj_cgroup, refcnt);unsigned int nr_bytes;unsigned int nr_pages; +unsigned int sub_bytes;unsigned long flags;/* -* At this point all allocated objects are freed, and -* objcg->nr_charged_bytes can't have an arbitrary byte value. -* However, it can be PAGE_SIZE or (x * PAGE_SIZE). +* At this point all allocated objects are freed, but +* objcg->nr_charged_bytes can still hold either +* - (x * PAGE_SIZE) if a small-alloc/drain race left whole pages +* stranded (see the historical sequence below), or +* - any sub-page residue, now that the stock is keyed by memcg and +* sibling per-node objcgs share its reserve: bytes consumed by +* one sibling can spill into another sibling's nr_charged_bytes +* when the stock is drained. * -* The following sequence can lead to it: +* Uncharge the page-aligned portion from this objcg's (post-reparent) +* memcg, and forward any sub-page residue into a per-node objcg of +* the same memcg so it can be reconciled later instead of being lost. +* +* Historical race producing the (x * PAGE_SIZE) case: * 1) CPU0: objcg == stock->cached_objcg * 2) CPU1: we do a small allocation (e.g. 92 bytes), * PAGE_SIZE bytes are charged
@@ -160,23 +170,33 @@ static void obj_cgroup_release(struct percpu_ref *ref)* 92 bytes are added to stock->nr_bytes * 6) CPU0: stock is flushed, * 92 bytes are added to objcg->nr_charged_bytes -* -* In the result, nr_charged_bytes == PAGE_SIZE. -* This page will be uncharged in obj_cgroup_release(). */ nr_bytes =atomic_read (&objcg->nr_charged_bytes); -WARN_ON_ONCE (nr_bytes & (PAGE_SIZE -1 )); nr_pages = nr_bytes >>PAGE_SHIFT ; + sub_bytes = nr_bytes & (PAGE_SIZE -1 ); -if (nr_pages) { +if (nr_pages || sub_bytes) {struct mem_cgroup *memcg; - memcg =get_mem_cgroup_from_objcg (objcg); -mod_memcg_state (memcg,MEMCG_KMEM , -nr_pages); -memcg1_account_kmem (memcg, -nr_pages); -if (!mem_cgroup_is_root (memcg)) -memcg_uncharge (memcg, nr_pages); -mem_cgroup_put (memcg); +rcu_read_lock (); + memcg =obj_cgroup_memcg (objcg); + +if (nr_pages) { +mod_memcg_state (memcg,MEMCG_KMEM , -nr_pages); +memcg1_account_kmem (memcg, -nr_pages); +if (!mem_cgroup_is_root (memcg)) +memcg_uncharge (memcg, nr_pages); + } + +if (sub_bytes && !mem_cgroup_is_root (memcg)) { +struct obj_cgroup *fwd; + + fwd =rcu_dereference ( + memcg->nodeinfo[numa_node_id ()]->objcg); +if (fwd) +atomic_add (sub_bytes, &fwd->nr_charged_bytes); + } +rcu_read_unlock (); }spin_lock_irqsave (&objcg_lock, flags);
@@ -3152,7 +3172,12 @@ static void unlock_stock(struct obj_stock_pcp *stock)local_unlock (&obj_stock.lock); } -/* Call after __refill_obj_stock() to ensure stock->cached_objg == objcg */ +/* +* Call after __consume_obj_stock() / __refill_obj_stock(). The stock may be +* cached for a sibling per-node objcg of the same memcg; in that case the +* vmstat batching slot does not match objcg and we fallthrough to the +* direct path. +*/ static void __account_obj_stock (struct obj_cgroup *objcg,struct obj_stock_pcp *stock,int nr,struct pglist_data *pgdat,enum node_stat_item idx)
@@ -3210,7 +3235,11 @@ static bool __consume_obj_stock(struct obj_cgroup *objcg, struct obj_stock_pcp *stock, unsigned int nr_bytes) { - if (objcg == READ_ONCE(stock->cached_objcg) && +struct obj_cgroup *cached =READ_ONCE (stock->cached_objcg); + +/* Sibling per-node objcgs share the reserve. */ +if ((cached == objcg || + (cached &&READ_ONCE (cached->memcg) ==READ_ONCE (objcg->memcg))) && stock->nr_bytes >= nr_bytes) { stock->nr_bytes -= nr_bytes;return true;
@@ -3318,6 +3347,7 @@ static void __refill_obj_stock(struct obj_cgroup *objcg, unsigned int nr_bytes, bool allow_uncharge) { +struct obj_cgroup *cached;unsigned int nr_pages =0 ;if (!stock) {
@@ -3327,7 +3357,10 @@ static void __refill_obj_stock(struct obj_cgroup *objcg, goto out; } -if (READ_ONCE (stock->cached_objcg) != objcg) {/* reset if necessary */ + cached =READ_ONCE (stock->cached_objcg); +/* Direct READ_ONCE due to just pointer comparison. */ +if (cached != objcg && + (!cached ||READ_ONCE (cached->memcg) !=READ_ONCE (objcg->memcg))) {drain_obj_stock (stock);obj_cgroup_get (objcg); stock->nr_bytes =atomic_read (&objcg->nr_charged_bytes)
Signature · 1 line
2.53.0-Meta
Shakeel Butt
maintainerQuoted 33 lines · Shakeel Butt
Commit
01b9da291c49("mm: memcontrol: convert objcg to be per-memcg per-node type") split a memcg's singleobj_cgroupinto one per NUMA node, but the per-CPUobj_stock_pcpstill keyscached_objcgby pointer. Cross-NUMA workloads now see a drain on every refill and a miss on every consume that targets a sibling per-node objcg of the same memcg, producing the 67.7% stress-ng switch-mq regression reported by LKP.
stock->nr_bytesare fungible across per-node objcgs of one memcg. Treat the cache as keyed by memcg in__consume_obj_stock()and__refill_obj_stock()so siblings share the reserve. Compare viaREAD_ONCE(objcg->memcg)directly: pointer-compare only, no deref, so thercu_read_lockcontract onobj_cgroup_memcg()does not apply.Sharing the reserve without re-caching means bytes funded by one per-node objcg's slow path can be consumed/freed under a different sibling, leaving sub-page residue on whichever sibling was cached at drain time. The pre-existing
obj_cgroup_release()path would WARN and silently drop that residue, leaking up tonr_node_ids* (PAGE_SIZE- 1) bytes per memcg lifecycle from thepage_counter. Forward the residue into a per-node objcg of the same (post-reparent) memcg at release time instead, so it can be reconciled later via a refillatomic_xchgor another release; the chain terminates atroot_mem_cgroup, whosepage_counterhas no enforced limit.Please note that this is temporary fix and will be reverted when per-node kmem accounting is introduced.
Update the stale invariant comment on
__account_obj_stock().Qi Zheng built a specialized reproducer [1] for the corner case and confirmed the fix.
Quoted 6 lines
Reported-by kernel test robot <[email protected]>Fixes 01b9da291c49 ("mm: memcontrol: convert objcg to be per-memcg per-node type")Signed-off-by Shakeel Butt <[email protected]>Debugged-by Qi Zheng <[email protected]>
Sashiko's review below and my response.
Quoted 68 lines
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Race condition during memcg offlining causes a permanent page counter leak when forwardingsub_bytes.
- [High] Unbounded accumulation ofsub_bytesin a non-allocating parent memcg causes a permanent page counter leak and integer overflow. --commit
e90ccbfce3bb9538bf1cdf185a1c10a7269e0939
Author: Shakeel Butt <[email protected]>memcg: cache
obj_stockby memcg, not by objcg pointerThis commit changes
obj_stock_pcpcaching from pointer-based to memcg-based so sibling per-node objcgs share the reserve. It also forwards sub-page residue into a per-node objcg of the same memcg during release so it can be reconciled later instead of being lost.Quoted 6 lines
mm/memcontrol.c +0
-0index d978e18b9b2d0..a547ec7c42d16 100644 --- a/mm/memcontrol.c +++ b/mm/memcontrol.c[ ... ]
Quoted 25 lines
mm/memcontrol.c +0
-0@@ -160,23 +170,33 @@ static void obj_cgroup_release(struct percpu_ref *ref)*/ nr_bytes =atomic_read (&objcg->nr_charged_bytes); -WARN_ON_ONCE (nr_bytes & (PAGE_SIZE -1 )); nr_pages = nr_bytes >>PAGE_SHIFT ; + sub_bytes = nr_bytes & (PAGE_SIZE -1 ); - if (nr_pages) { + if (nr_pages || sub_bytes) {struct mem_cgroup *memcg; - memcg =get_mem_cgroup_from_objcg (objcg); -mod_memcg_state (memcg,MEMCG_KMEM , -nr_pages); -memcg1_account_kmem (memcg, -nr_pages); - if (!mem_cgroup_is_root(memcg)) -memcg_uncharge (memcg, nr_pages); -mem_cgroup_put (memcg); +rcu_read_lock (); + memcg =obj_cgroup_memcg (objcg); + + if (nr_pages) { +mod_memcg_state (memcg,MEMCG_KMEM , -nr_pages); +memcg1_account_kmem (memcg, -nr_pages);mm/memcontrol.c static void obj_cgroup_release(struct percpu_ref *ref)
Quoted 10 lines
mm/memcontrol.c +0
-0+ if (!mem_cgroup_is_root(memcg)) +memcg_uncharge (memcg, nr_pages); + } + + if (sub_bytes && !mem_cgroup_is_root(memcg)) { + struct obj_cgroup *fwd; + + fwd =rcu_dereference ( + memcg->nodeinfo[numa_node_id ()]->objcg); + if (fwd)Could there be a race condition during memcg offlining here that leads to a page counter leak?
Quoted 7 lines
If this executes concurrently with the offlining of the memcg,
memcg_reparent_objcgs()might have already replaced the nodeinfo objcg pointer with NULL. In this race window, fwd evaluates to NULL, and thesub_byteswould be dropped.Since they are dropped without being uncharged or forwarded, it seems the page counter would permanently leak these charges.
This is not much a concern as it is bounded (i.e. nr_nodes * PAGE_SIZE - 1) and previously for the warning case, we were already dropping the sub-page charges.
Quoted 7 lines
Quoted 4 lines
+
atomic_add(sub_bytes, &fwd->nr_charged_bytes);Does this unbounded addition create a risk of integer overflow and page counter leaks if the parent memcg never allocates?
Quoted 9 lines
If the parent memcg is used strictly as a hierarchical container (like system.slice) and performs no kernel allocations, its objcg is never cached in any CPU's
obj_stock_pcp.Because
__refill_obj_stock()relies on the cache to periodically drainnr_charged_bytes, it would never be invoked for the parent. Could child residues continuously accumulate in the parent'snr_charged_byteswithout ever being uncharged, eventually overflowing the 32-bitatomic_t?
This can be a concern for a long running cgroup. However fixing this would add complexity not worth it. This is a temporary fix and will be reverted in newer kernels.
Quoted 9 lines
Quoted 5 lines
+ }
+rcu_read_unlock();
}Signature · 2 lines
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
Qi Zheng
Acked-byQuoted 140 lines · Shakeel Butt
Quoted 33 lines · Shakeel Butt
Commit
01b9da291c49("mm: memcontrol: convert objcg to be per-memcg per-node type") split a memcg's singleobj_cgroupinto one per NUMA node, but the per-CPUobj_stock_pcpstill keyscached_objcgby pointer. Cross-NUMA workloads now see a drain on every refill and a miss on every consume that targets a sibling per-node objcg of the same memcg, producing the 67.7% stress-ng switch-mq regression reported by LKP.
stock->nr_bytesare fungible across per-node objcgs of one memcg. Treat the cache as keyed by memcg in__consume_obj_stock()and__refill_obj_stock()so siblings share the reserve. Compare viaREAD_ONCE(objcg->memcg)directly: pointer-compare only, no deref, so thercu_read_lockcontract onobj_cgroup_memcg()does not apply.Sharing the reserve without re-caching means bytes funded by one per-node objcg's slow path can be consumed/freed under a different sibling, leaving sub-page residue on whichever sibling was cached at drain time. The pre-existing
obj_cgroup_release()path would WARN and silently drop that residue, leaking up tonr_node_ids* (PAGE_SIZE- 1) bytes per memcg lifecycle from thepage_counter. Forward the residue into a per-node objcg of the same (post-reparent) memcg at release time instead, so it can be reconciled later via a refillatomic_xchgor another release; the chain terminates atroot_mem_cgroup, whosepage_counterhas no enforced limit.Please note that this is temporary fix and will be reverted when per-node kmem accounting is introduced.
Update the stale invariant comment on
__account_obj_stock().Qi Zheng built a specialized reproducer [1] for the corner case and confirmed the fix.
Quoted 6 lines
Reported-by kernel test robot <[email protected]>Fixes 01b9da291c49 ("mm: memcontrol: convert objcg to be per-memcg per-node type")Signed-off-by Shakeel Butt <[email protected]>Debugged-by Qi Zheng <[email protected]>Sashiko's review below and my response.
Quoted 68 lines
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Race condition during memcg offlining causes a permanent page counter leak when forwardingsub_bytes.
- [High] Unbounded accumulation ofsub_bytesin a non-allocating parent memcg causes a permanent page counter leak and integer overflow. --commit
e90ccbfce3bb9538bf1cdf185a1c10a7269e0939
Author: Shakeel Butt <[email protected]>memcg: cache
obj_stockby memcg, not by objcg pointerThis commit changes
obj_stock_pcpcaching from pointer-based to memcg-based so sibling per-node objcgs share the reserve. It also forwards sub-page residue into a per-node objcg of the same memcg during release so it can be reconciled later instead of being lost.Quoted 6 lines
mm/memcontrol.c +0
-0index d978e18b9b2d0..a547ec7c42d16 100644 --- a/mm/memcontrol.c +++ b/mm/memcontrol.c[ ... ]
Quoted 25 lines
mm/memcontrol.c +0
-0@@ -160,23 +170,33 @@ static void obj_cgroup_release(struct percpu_ref *ref)*/ nr_bytes =atomic_read (&objcg->nr_charged_bytes); -WARN_ON_ONCE (nr_bytes & (PAGE_SIZE -1 )); nr_pages = nr_bytes >>PAGE_SHIFT ; + sub_bytes = nr_bytes & (PAGE_SIZE -1 ); - if (nr_pages) { + if (nr_pages || sub_bytes) {struct mem_cgroup *memcg; - memcg =get_mem_cgroup_from_objcg (objcg); -mod_memcg_state (memcg,MEMCG_KMEM , -nr_pages); -memcg1_account_kmem (memcg, -nr_pages); - if (!mem_cgroup_is_root(memcg)) -memcg_uncharge (memcg, nr_pages); -mem_cgroup_put (memcg); +rcu_read_lock (); + memcg =obj_cgroup_memcg (objcg); + + if (nr_pages) { +mod_memcg_state (memcg,MEMCG_KMEM , -nr_pages); +memcg1_account_kmem (memcg, -nr_pages);mm/memcontrol.c static void obj_cgroup_release(struct percpu_ref *ref)
Quoted 10 lines
mm/memcontrol.c +0
-0+ if (!mem_cgroup_is_root(memcg)) +memcg_uncharge (memcg, nr_pages); + } + + if (sub_bytes && !mem_cgroup_is_root(memcg)) { + struct obj_cgroup *fwd; + + fwd =rcu_dereference ( + memcg->nodeinfo[numa_node_id ()]->objcg); + if (fwd)Could there be a race condition during memcg offlining here that leads to a page counter leak?
Quoted 7 lines
If this executes concurrently with the offlining of the memcg,
memcg_reparent_objcgs()might have already replaced the nodeinfo objcg pointer with NULL. In this race window, fwd evaluates to NULL, and thesub_byteswould be dropped.Since they are dropped without being uncharged or forwarded, it seems the page counter would permanently leak these charges.
This is not much a concern as it is bounded (i.e.
nr_nodes*PAGE_SIZE- 1) and previously for the warning case, we were already dropping the sub-page charges.Quoted 7 lines
Quoted 4 lines
+
atomic_add(sub_bytes, &fwd->nr_charged_bytes);Does this unbounded addition create a risk of integer overflow and page counter leaks if the parent memcg never allocates?
Quoted 9 lines
If the parent memcg is used strictly as a hierarchical container (like system.slice) and performs no kernel allocations, its objcg is never cached in any CPU's
obj_stock_pcp.Because
__refill_obj_stock()relies on the cache to periodically drainnr_charged_bytes, it would never be invoked for the parent. Could child residues continuously accumulate in the parent'snr_charged_byteswithout ever being uncharged, eventually overflowing the 32-bitatomic_t?
Quoted 3 lines
This can be a concern for a long running cgroup. However fixing this would add complexity not worth it. This is a temporary fix and will be reverted in newer kernels.
I think this is fine as a temporary fix:
Thanks!
Quoted 10 lines
Quoted 10 lines
Quoted 6 lines
+ }
+rcu_read_unlock();
}Signature · 2 lines
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
Quoted 26 lines · Shakeel Butt
Quoted 25 lines · Shakeel Butt
Commit
01b9da291c49("mm: memcontrol: convert objcg to be per-memcg per-node type") split a memcg's singleobj_cgroupinto one per NUMA node, but the per-CPUobj_stock_pcpstill keyscached_objcgby pointer. Cross-NUMA workloads now see a drain on every refill and a miss on every consume that targets a sibling per-node objcg of the same memcg, producing the 67.7% stress-ng switch-mq regression reported by LKP.
stock->nr_bytesare fungible across per-node objcgs of one memcg. Treat the cache as keyed by memcg in__consume_obj_stock()and__refill_obj_stock()so siblings share the reserve. Compare viaREAD_ONCE(objcg->memcg)directly: pointer-compare only, no deref, so thercu_read_lockcontract onobj_cgroup_memcg()does not apply.Sharing the reserve without re-caching means bytes funded by one per-node objcg's slow path can be consumed/freed under a different sibling, leaving sub-page residue on whichever sibling was cached at drain time. The pre-existing
obj_cgroup_release()path would WARN and silently drop that residue, leaking up tonr_node_ids* (PAGE_SIZE- 1) bytes per memcg lifecycle from thepage_counter. Forward the residue into a per-node objcg of the same (post-reparent) memcg at release time instead, so it can be reconciled later via a refillatomic_xchgor another release; the chain terminates atroot_mem_cgroup, whosepage_counterhas no enforced limit.
Quoted 2 lines
Quoted 2 lines
Please note that this is temporary fix and will be reverted when per-node kmem accounting is introduced.
... because once per-node kmem accounting is introduced, "stock->nr_bytes are fungible across per-node objcgs of one memcg" no longer holds?
And the follow-up plain is to revert this and address it with a multi-objcg percpu stock [1], similar to a multi-memcg percpu charge cache we have now, right? (regardless of per-node kmem accounting's progress)
If this temporary fix imposes other potential correctness issues, would it make sense to land [1] in mainline before the next LTS release and skip this temporary fix?
[1] https://lore.kernel.org/oe-lkp/[email protected]
Signature · 2 lines
Cheers, Harry / Hyeonggon
Shakeel Butt
maintainerQuoted 29 lines · Harry Yoo
Quoted 26 lines · Shakeel Butt
Quoted 25 lines · Shakeel Butt
Commit
01b9da291c49("mm: memcontrol: convert objcg to be per-memcg per-node type") split a memcg's singleobj_cgroupinto one per NUMA node, but the per-CPUobj_stock_pcpstill keyscached_objcgby pointer. Cross-NUMA workloads now see a drain on every refill and a miss on every consume that targets a sibling per-node objcg of the same memcg, producing the 67.7% stress-ng switch-mq regression reported by LKP.
stock->nr_bytesare fungible across per-node objcgs of one memcg. Treat the cache as keyed by memcg in__consume_obj_stock()and__refill_obj_stock()so siblings share the reserve. Compare viaREAD_ONCE(objcg->memcg)directly: pointer-compare only, no deref, so thercu_read_lockcontract onobj_cgroup_memcg()does not apply.Sharing the reserve without re-caching means bytes funded by one per-node objcg's slow path can be consumed/freed under a different sibling, leaving sub-page residue on whichever sibling was cached at drain time. The pre-existing
obj_cgroup_release()path would WARN and silently drop that residue, leaking up tonr_node_ids* (PAGE_SIZE- 1) bytes per memcg lifecycle from thepage_counter. Forward the residue into a per-node objcg of the same (post-reparent) memcg at release time instead, so it can be reconciled later via a refillatomic_xchgor another release; the chain terminates atroot_mem_cgroup, whosepage_counterhas no enforced limit.
Quoted 6 lines
Quoted 2 lines
Quoted 2 lines
Please note that this is temporary fix and will be reverted when per-node kmem accounting is introduced.
... because once per-node kmem accounting is introduced, "
stock->nr_bytesare fungible across per-node objcgs of one memcg" no longer holds?
Yes
Quoted 5 lines
And the follow-up plain is to revert this and address it with a multi-objcg percpu stock [1], similar to a multi-memcg percpu charge cache we have now, right? (regardless of per-node kmem accounting's progress)
Yes
Quoted 6 lines
If this temporary fix imposes other potential correctness issues, would it make sense to land [1] in mainline before the next LTS release and skip this temporary fix?
The full clean solution might take one more cycle and I think we can not just ignore 67% regression on 7.1.
Quoted 40 lines · Shakeel Butt
Quoted 29 lines · Harry Yoo
Quoted 26 lines · Shakeel Butt
Quoted 25 lines · Shakeel Butt
Commit
01b9da291c49("mm: memcontrol: convert objcg to be per-memcg per-node type") split a memcg's singleobj_cgroupinto one per NUMA node, but the per-CPUobj_stock_pcpstill keyscached_objcgby pointer. Cross-NUMA workloads now see a drain on every refill and a miss on every consume that targets a sibling per-node objcg of the same memcg, producing the 67.7% stress-ng switch-mq regression reported by LKP.
stock->nr_bytesare fungible across per-node objcgs of one memcg. Treat the cache as keyed by memcg in__consume_obj_stock()and__refill_obj_stock()so siblings share the reserve. Compare viaREAD_ONCE(objcg->memcg)directly: pointer-compare only, no deref, so thercu_read_lockcontract onobj_cgroup_memcg()does not apply.Sharing the reserve without re-caching means bytes funded by one per-node objcg's slow path can be consumed/freed under a different sibling, leaving sub-page residue on whichever sibling was cached at drain time. The pre-existing
obj_cgroup_release()path would WARN and silently drop that residue, leaking up tonr_node_ids* (PAGE_SIZE- 1) bytes per memcg lifecycle from thepage_counter. Forward the residue into a per-node objcg of the same (post-reparent) memcg at release time instead, so it can be reconciled later via a refillatomic_xchgor another release; the chain terminates atroot_mem_cgroup, whosepage_counterhas no enforced limit.Quoted 6 lines
Quoted 2 lines
Quoted 2 lines
Please note that this is temporary fix and will be reverted when per-node kmem accounting is introduced.
... because once per-node kmem accounting is introduced, "
stock->nr_bytesare fungible across per-node objcgs of one memcg" no longer holds?Yes
Quoted 1 line
Quoted 6 lines
Quoted 4 lines
And the follow-up plain is to revert this and address it with a multi-objcg percpu stock [1], similar to a multi-memcg percpu charge cache we have now, right? (regardless of per-node kmem accounting's progress)
Yes
Thanks for confirming!
Quoted 9 lines
Quoted 6 lines
If this temporary fix imposes other potential correctness issues, would it make sense to land [1] in mainline before the next LTS release and skip this temporary fix?
The full clean solution might take one more cycle and I think we can not just ignore 67% regression on 7.1.
That is valid point, unfortunately.
One more thing I have to ask... for v7.1, wouldn't it be a safer option to revert the per-node object change and re-introduce it once we have a cleaner solution?
This change was introduced in v5, but the implementation before v4 had been exposed in -next for a while, and I think we don't have enough justification to keep the per-node objcgs change, at least for v7.1, given that we have an unexpected last-minute regression and correctness concerns (albeit slight).
Signature · 2 lines
Cheers, Harry / Hyeonggon
Shakeel Butt
maintainerQuoted 2 lines · Harry Yoo
[...]
Quoted 9 lines
Quoted 3 lines
The full clean solution might take one more cycle and I think we can not just ignore 67% regression on 7.1.
That is valid point, unfortunately.
One more thing I have to ask... for v7.1, wouldn't it be a safer option to revert the per-node object change and re-introduce it once we have a cleaner solution?
The issue with that revert is that we reintroduce all node lru locking in the objcg reparenting path.
Quoted 6 lines
This change was introduced in v5, but the implementation before v4 had been exposed in -next for a while, and I think we don't have enough justification to keep the per-node objcgs change, at least for v7.1, given that we have an unexpected last-minute regression and
correctness concerns (albeit slight).
I am waiting for Oliver to test the multi-objcg patch I sent. If that also resolves the regression then we have one more option i.e. backport that to 7.1 to fix the regression.
So to summarize, for future kernels we will be having multi-objcg in some form. For 7.1, we have to decide between (1) do nothing (2) this patch or (3) backport the multi-objcg path to 7.1.
Andrew, please don't send this patch to Linus until we decide on the option.
Andrew Morton
maintainerQuoted 1 line · On Tue, 19 May 2026 13:11:13 -0700 Shakeel Butt <[email protected]> wrote:
Andrew, please don't send this patch to Linus until we decide on the option.
No probs, I added a note-to-self.
Shakeel Butt
maintainerQuoted 5 lines · Andrew Morton
Quoted 1 line · On Tue, 19 May 2026 13:11:13 -0700 Shakeel Butt <[email protected]> wrote:
Andrew, please don't send this patch to Linus until we decide on the option.
No probs, I added a note-to-self.
Here is my suggestion on how to proceed.
1. Let's drop this patch (memcg: cache obj_stock by memcg, not by objcg pointer) from mm-tree.
2. Let's not add anything to 7.1.
3. 7.2+ will have the multi-objcg series [1] and the patches will have fixes tag. If someone uses non-LTS 7.1, they can easily find those patches and can backport them.
Please let me know if there are any concerns.
Quoted 12 lines · Shakeel Butt
Quoted 5 lines · Andrew Morton
Quoted 1 line · On Tue, 19 May 2026 13:11:13 -0700 Shakeel Butt <[email protected]> wrote:
Andrew, please don't send this patch to Linus until we decide on the option.
No probs, I added a note-to-self.
Here is my suggestion on how to proceed.
1. Let's drop this patch (memcg: cache
obj_stockby memcg, not by objcg pointer) from mm-tree.
Quoted 7 lines
2. Let's not add anything to 7.1.
3. 7.2+ will have the multi-objcg series [1] and the patches will have fixes tag. If someone uses non-LTS 7.1, they can easily find those patches and can backport them.
Please let me know if there are any concerns.
Sounds fair to me.
Quoted 1 line
Thanks!
Signature · 2 lines
Cheers, Harry / Hyeonggon
Quoted 8 lines · Shakeel Butt
Quoted 2 lines · Harry Yoo
[...]
Quoted 4 lines
Quoted 3 lines
The full clean solution might take one more cycle and I think we can not just ignore 67% regression on 7.1.
Quoted 8 lines
Quoted 5 lines
That is valid point, unfortunately.
One more thing I have to ask... for v7.1, wouldn't it be a safer option to revert the per-node object change and re-introduce it once we have a cleaner solution?
The issue with that revert is that we reintroduce all node lru locking in the objcg reparenting path.
I'm not sure the problems with all-node locking are serious enough to rule it out as an option for 7.1.
It is not ideal, but given that the critical section for reparenting is independent of folio count, would this actually be a significant problem in practice? (even large servers rarely go beyond 8 NUMA nodes...)
Quoted 9 lines
Quoted 5 lines
This change was introduced in v5, but the implementation before v4 had been exposed in -next for a while, and I think we don't have enough justification to keep the per-node objcgs change, at least for v7.1, given that we have an unexpected last-minute regression and
correctness concerns (albeit slight).I am waiting for Oliver to test the multi-objcg patch I sent. If that also resolves the regression then we have one more option i.e. backport that to 7.1 to fix the regression.
Yeah, If per-node objcg is required in future kernels anyway, this option would be more ideal (if available).
Quoted 3 lines
So to summarize, for future kernels we will be having multi-objcg in some form. For 7.1, we have to decide between (1) do nothing (2) this patch or (3) backport the multi-objcg path to 7.1.
Ack.
Thanks!
Signature · 2 lines
Cheers, Harry / Hyeonggon