Re: [PATCH v7 0/5] mm/page_counter: move stock from mem_cgroup to page_counter
From: Joshua Hahn
Date: Mon Oct 26 2026 - 12:05:14 EST
On Mon, 26 Oct 2026 10:03:58 +0100 Michal Hocko <mhocko@xxxxxxxx> wrote:
> [Sorry for coming to this really late]
Hi Michal, no worries at all : -) Thanks for taking a look!
> On Wed 21-10-26 15:55:09, Joshua Hahn wrote:
> > v6 --> v7
> > =========
> > - Preserve two cgroup v1 proprerties that previously followed implicitly
> > from sharing a stock:
> > - memory.usage_in_bytes <= memory.memsw.usage_in_bytes
> > - Reclaim does not swap when memory and memsw have equal limits.
> > - Wordsmithing commit messages for 4/5 and 5/5 to document the
> > cgroupv1 divergence (bounded) between memory and memsw caused by
> > independent stock.
> >
> > INTRODUCTION
> > ============
> > Memcg keeps a per-CPU stock of precharged pages so that small, frequent
> > allocations do not walk the page_counter hierarchy every time.
> > Today, the stock implementation is within memcontrol code, even though
> > the operation it caches is a page_counter charge. This makes it
> > difficult to add new page_counters to a memcg and preserve the fast
> > path behavior.
> >
> > This matters for future work like my tiered memcg limits series [1]
> > which introduces multiple new page_counters to memcg. Without making
> > stock a page_counter-level property, it means that every memcg charge
> > now goes through multiple page_counter hierarchy walks, instead of
> > being able to cache these charges.
> >
> > To make future page_counters scalable and performant, move stock from
> > mem_cgroup to page_counter so that each page_counter can opt into its
> > own per-CPU cache of pre-charged pages.
>
> What does it mean that each counter can opt into its own per-cpu
> caching?
So right now for cgroupv2, there's kind of a 1-1 mapping between
page_counter and memcg. I say "kind of" because there still is a
page_counter for swap, but it takes a completely different charging
path (i.e. not try_charge_memcg).
This means that currently stocking charge for memcg is equivalent to
stocking charge for the memcg->memory page_counter.
However, one of the series that I'm currently working on [1] adds new
page_counters to struct memcg to track the usage of memory
per-memcg, per-tier. The goal is to (1) track how much memory of a
cgroup lives on which tier, and (2) to limit the usage so that a single
cgroup doesn't use up all the fast tier memory alone, and shares it
fairly with all the other cgroups on the host.
When adding a new per-tier page_counter to struct mem_cgroup that gets
charged for every memcg_charge, I cannot use the stock fastpath since
the stock does not distinguish where the memory resides. It would
essentialy boil down to this pattern:
try_charge_memcg
\__ page_couner_try_charge(memcg->tier[0])
\__ consume_stock()
Where every path still has to do a hierarchy charge to the tier it's
charging to, whereas this patch gets us:
try_charge_memcg
\__ page_counter_try_charge(memcg->tier[0])
\__ consume_stock(memcg->tier[0])
\__ page_countertry_charge(memcg->memory)
\__ consume_stock(memcg->memory)
So that we can avoid the hierarchy walk for most charges.
I'd be happy to submit a new cover letter which better explains this
idea!
> > We get an added benefit of simplifying try_charge_memcg(), which now
> > has all the stock management handled transparently within the
> > page_counter layer.
>
> This is quite a large change and I believe it will be really benefitial
> to have some high level design overview in the cover rather than trying to
> chase it in respective patches.
Sounds good with me. I can add the call traces above which should
explain my thought process a bit more. Hopefully the problem I am
trying to solve is evident there.
> Namely, are there any changes to the locking (which is arguably quite
> tricky to allow completely lockless fast path)?
No changes to locking. In fact this series is mostly just moving code
from mm/memcontrol.c to mm/page_counter.c, everything is essentially
the same.
> Also what are implications to caching at different levels of the
> hierarchy? An example of cache propagation up/down the hierarchy during
> charge/uncharge/draining would be really useful to wrap ones head around
> how this is supposed to work.
This series should not have any changes for cgroup v2, including when
and where the cache is stocked / drained in the hierarchy. I can
include a snippet on how stocking currently works to add a visual
explanation, if that helps too.
> > EFFECT ON MEMCG V2 USERS
> > ========================
> > This series has no functional changes intended for memcg v2 users. We
> > preserve all draining, refilling, and (un)charging behavior, including
> > the uncharge path's refills / direct uncharges.
> >
> > EFFECT ON MEMCG V1 USERS
> > ========================
> > For memcg v1 users, the decoupling of the memsw and memory stock means
> > that each of them now manage their own independent stocks and can lead
> > to a different size of precharged cache for each.
> >
> > Cgroup v1 has an invariant that memory.memsw.usage_in_bytes is larger
> > than or equal to memory.usage_in_bytes, because memsw is a superset of
> > memory. With separate stocking, this could have been broken in scenarios
> > where the memory stock is bigger than the memsw stock, leading to
> > memory usage appearing to be inflated and greater than memsw usage,
> > even though the real usage preserves the invariant.
>
> How can this happen when counters are always charged together?
Yeah, so this part is a little bit annoying. Let's just look at a single
leaf cgroup for example, where the limit for memsw and memory are both
64 pages.
charge_memcg(64)
raw memory: 64 raw memsw: 64
reported memory: 64 reported memsw: 64
memory stock: 0 memory stock: 0
swap out 32 pages
raw memory: 32 raw memsw: 64
reported memory: 32 reported memsw: 64
memory stock: 0 memsw stock: 0
free 32 pages
raw memory: 0 raw memsw: 32
reported memory: 0 reported memsw: 32
memory stock: 0 memsw stock: 0
charge_memcg(1)
raw memory: 1 raw memsw: 33
reported memory: 64 reported memsw: 33
memory stock: 63 memsw stock: 0
Where the last charge_memcg() causes the inversion. memsw tries to
charge a batch of 64 to refill the stock, but fails since 32+64=96
puts the memsw over its max limit. So it falls back to direct charging
where it charges the page_counter directly, and ends up with 0 stock
and reporting a usage of 33.
On the other hand, memory will greedily refill the stock with 64 pages
and since usage is 0, the batch refill will succeed, and it will report
a usage of 64.
So the idea with reporting the minimum is to report the minimum of the
two reported values, we will just fuzzily report the stocked value.
IIRC usage_in_bytes has always been fuzzy anyways, due to how much
memory has been stocked.
Thanks again for taking a look, Michal! I hope you have a great day : -)
Joshua
> --
> Michal Hocko
> SUSE Labs
[1] https://lore.kernel.org/all/20260807202059.2620949-1-joshua.hahnjy@xxxxxxxxx/