mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Zi Yan <ziy@nvidia.com>
To: Yafang Shao <laoar.shao@gmail.com>,
	Ying Huang <ying.huang@linux.alibaba.com>
Cc: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
	eddyz87@gmail.com, memxor@gmail.com, mingo@redhat.com,
	peterz@infradead.org, juri.lelli@redhat.com,
	vincent.guittot@linaro.org, akpm@linux-foundation.org,
	david@kernel.org, gourry@gourry.net, tj@kernel.org,
	hannes@cmpxchg.org, mkoutny@suse.com, shakeel.butt@linux.dev,
	roman.gushchin@linux.dev, yu.c.chen@intel.com,
	zhao1.liu@intel.com, tim.c.chen@linux.intel.com,
	lizhe.67@bytedance.com, mgorman@suse.de, bpf@vger.kernel.org,
	linux-mm@kvack.org, cgroups@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: Re: [RFC PATCH bpf-next v4 0/4] BPF-based per-cgroup NUMA balancing mode selection
Date: Mon, 09 Nov 2026 22:35:10 -0500	[thread overview]
Message-ID: <8D6452ED-CB2A-4D08-863E-039A9D0DA453@nvidia.com> (raw)
In-Reply-To: <20261110032913.71974-1-laoar.shao@gmail.com>

+Ying, who did a lot of work on NUMA balancing

On 9 Nov 2026, at 22:29, Yafang Shao wrote:

> Problem
> =======
>
> In our large fleet of Kubernetes-managed servers, NUMA balancing has
> historically been disabled globally on each server. With the growing
> deployment of multi-socket AMD EPYC servers, cross-NUMA access has become
> a critical performance issue, which led us to reconsider enabling NUMA
> balancing.
>
> kernel.numa_balancing is a single system-wide sysctl. Latency-sensitive
> workloads want NUMA balancing disabled, while NUMA-aware workloads want it
> enabled. No single global setting serves both.
>
> Design
> ======
>
> Core structure
> ~~~~~~~~~~~~~~
>
> This series follows Tejun's suggestion to store the existing NUMA
> balancing mode bits in the cgroup's memcg and expose them through BPF
> kfuncs.
>
> A new field, mem_cgroup::numab_mode, holds the per-cgroup NUMA balancing
> setting. Two kfuncs are added to set and read it:
>
>   bpf_numab_set_mode(): set the mode for a specific memory cgroup. The
>                         update is rejected with -EOPNOTSUPP if the cgroup
>                         has no visible, online memcg. Valid
>                         values are the NUMAB_* mode enum and
>                         NUMAB_FOLLOW_SYSTEM (-1); NUMAB_FOLLOW_SYSTEM
>                         uses the global sysctl state.
>
>   bpf_numab_get_mode(): return the combined task-side request of the
>                         cgroup's effective memcg and of all its ancestors.
>                         NUMAB_FOLLOW_SYSTEM is returned when no request is
>                         present. A cgroup without a memcg resolves to its
>                         nearest ancestor memcg. The following section
>                         describes how the request is combined across the
>                         hierarchy.
>
> Both kfuncs are registered for BPF_PROG_TYPE_SYSCALL. bpf_numab_set_mode()
> is KF_SLEEPABLE because it takes the mode mutex and the jump-label locks;
> bpf_numab_get_mode() does not sleep. The mode values are exposed as a BTF
> enum, so a BPF program can use NUMAB_FOLLOW_SYSTEM and the NUMAB_* mode
> names without hardcoding private integers.
>
> Effective mode semantics
> ~~~~~~~~~~~~~~~~~~~~~~~~
>
> In the current implementation, a memory cgroup can be set to the
> following modes:
>
>   NUMAB_FOLLOW_SYSTEM (-1):   use the global sysctl state
>   NUMAB_DISABLED (0):         disabled
>   NUMAB_NORMAL (0x1):         normal mode
>   NUMAB_TIERING (0x2):        tiering mode
>   NUMAB_NORMAL_TIERING (0x3): normal mode plus memory tiering
>
> The diagrams below assume that each cgroup shown has its own memcg. A
> cgroup without one resolves to its nearest ancestor memcg, as described
> in Storage of numab_mode.
>
> Except for the sentinel and the opt-out described below, the effective
> mode is the OR of this memcg's mode and that of all its ancestors.
>
> - An opt-out wins for the task side and binds the subtree
>
>   A DISABLED mode anywhere in the cgroup's ancestry opts the cgroup's
>   tasks out entirely. Given:
>                      Pods (normal)
>                      /
>           Pod-A (disabled)
>           /           \
>    cgroup-A1(normal)   cgroup-A2(tiering)
>
>   The effective task-side mode for tasks(*) in cgroup-A1 and cgroup-A2
>   is:
>
>                      Pods (normal)
>                      /
>           Pod-A (disabled)
>           /           \
>    cgroup-A1(disabled)   cgroup-A2(disabled)
>
>   (*) The folio side is described below.
>
> - NUMAB_FOLLOW_SYSTEM is skipped
>
>   Given:
>
>                      Pods (normal)
>                      /
>           Pod-A (-1)
>           /           \
>    cgroup-A1(none)   cgroup-A2(none)
>
>   The effective mode for tasks in cgroup-A1 and cgroup-A2 is:
>
>                      Pods (normal)
>                      /
>           Pod-A (normal)
>           /           \
>    cgroup-A1(normal)   cgroup-A2(normal)
>
> - For all other modes, the effective value is the OR of this memcg's
>   mode and that of its ancestors
>
>   Given:
>
>                      Pods (normal)
>                      /
>           Pod-A (-1)
>           /           \
>    cgroup-A1(none)   cgroup-A2(tiering)
>
>   The effective mode for tasks in cgroup-A1 and cgroup-A2 is:
>
>                      Pods (normal)
>                      /
>           Pod-A (-1)
>           /           \
>    cgroup-A1(normal)   cgroup-A2(normal|tiering)
>
> - The rationale behind these semantics
>
>   The cgroup hierarchy semantics invoked here are those of resource
>   limits: an ancestor's limit binds every descendant. That is the right
>   semantics for limits: a limit partitions a shared, finite resource,
>   and what a descendant consumes comes out of its ancestors' pool, so an
>   ancestor's limit must bind every level below it.
>
>   A policy request is a different kind of control: it is a preference
>   about how the kernel should treat the workload, not a quantity drawn
>   from a parent's pool. Expressing it consumes nothing, so the limit
>   semantics does not carry over; the question is only how the preferences
>   of several levels should combine.
>
>   We chose the additive interpretation: a descendant may ask for more on
>   top of what its ancestors asked for, while an opt-out at any level
>   still wins: DISABLED anywhere in the ancestry caps the whole subtree.
>
> Task and folio keying
> ~~~~~~~~~~~~~~~~~~~~~
>
> The NUMA balancing knob is consulted at some callsites with only a folio
> and no task, and at others with only a task and no folio.
>
> - Task only
>
>   kernel/sched/fair.c: task_numa_fault()
>   kernel/sched/fair.c: update_scan_period()
>   kernel/sched/fair.c: migrate_degrades_locality()
>   kernel/sched/fair.c: exceed_llc_capacity()
>   kernel/sched/fair.c: get_pref_llc()
>   kernel/sched/fair.c: get_scan_cpumasks()
>
> - Task and folio
>
>   kernel/sched/fair.c: should_numa_migrate_memory()
>
> - Folio only
>
>   mm/memory-tiers.c: folio_use_access_time()
>   mm/mempolicy.c: folio_can_map_prot_numa()
>   mm/migrate.c: folio_migrate_flags()
>   mm/migrate.c: migrate_misplaced_folio_prepare()
>   mm/migrate.c: migrate_misplaced_folio()
>
> Therefore, the helpers are split by side:
>
>   folio_numab_effective_mode()
>   folio_numab_in_*_mode()
>   task_numab_enabled()
>   task_numab_get_modes()
>
> Another concern is how to handle callsites that have both a task and a
> folio. The following anchor resolves it. Both the user fault path and the
> kswapd demotion path reach folio_migrate_flags():
>
> kswapd demotion (no task):
>
>   kswapd
>   + shrink_folio_list
>     + demote_folio_list
>       + migrate_pages
>         + folio_migrate_flags
>           + folio_numab_in_tiering_mode
>
> Fault path (user task):
>
>   do_numa_page
>   + mpol_misplaced
>     + should_numa_migrate_memory(task-m, folio-n) (A)
>       + task or folio?
>   + migrate_misplaced_folio_prepare(folio-n)
>   + migrate_misplaced_folio(folio-n)
>     + migrate_pages
>       + folio_migrate_flags(folio-n) (B)
>         + folio_numab_in_tiering_mode
>
> folio_migrate_flags() must key on the folio, since kswapd has no task to
> consult. In the user fault path, both should_numa_migrate_memory() and
> folio_numab_in_tiering_mode() are called. Since both refer to the same
> folio in this context, keying on the folio at B but not at A would be
> inconsistent. The rule is therefore simple: if a folio is available, key
> on the folio; otherwise, key on the task. This also keeps the keying
> robust to future refactoring: if either folio-keyed function is ever
> reached by a kernel thread, the answer does not change.
>
> Storage of numab_mode
> ~~~~~~~~~~~~~~~~~~~~~
>
> Some cgroups do not have the memory controller enabled:
>
> Given:
>           Pods (+memory)
>           /
>        Pod-A ()
>         /
>       cgroup-A1 ()
>
> cgroup-A1 has no memcg. Given the task/folio split, where should the
> mode for such a cgroup live?
>
> - If the mode were stored in struct cgroup and setting it on cgroup-A1
>   were allowed, tasks in cgroup-A1 would use cgroup-A1's request while
>   its folios would use Pod-A's memcg request, which is inconsistent.
>
> - Storing the mode in struct mem_cgroup avoids that: cgroup-A1 cannot be
>   set, and both its tasks and its folios resolve to the same ancestor
>   memcg (Pod-A).
>
> For consistency, the mode is stored in mem_cgroup, setting it on a
> cgroup without the memory controller enabled is rejected with
> -EOPNOTSUPP, and both task and folio lookups resolve a cgroup without a
> memcg to its nearest ancestor memcg. A request is effective only while
> its memcg is visible and online. Disabling the memory controller or
> offlining the memcg clears the request, and lookups skip such a memcg, so
> a pinned memcg cannot keep applying a stale request. A re-enabled memcg
> starts transparent.
>
> Shared memory semantics
> ~~~~~~~~~~~~~~~~~~~~~~~
>
> With the mode stored in the memcg, the longstanding shared-memory
> problem in memcg has to be addressed for per-memcg NUMA balancing as
> well.
>
> Given:
>
>     cgroup-A     cgroup-B
>            \       | (charge)
>            Memory_shared_by_cgroup-A_and_cgroup-B
>
> A choice is required when the NUMA modes of cgroup-A and cgroup-B
> differ. Since the folio-side lookup starts at the folio's memcg, a task
> in cgroup-A operating on this shared memory uses cgroup-B's folio-side
> mode. If DISABLED were applied to memory, cgroup-B's DISABLED would
> prevent cgroup-A from promoting the shared page. We avoid that by making
> DISABLED task-side only.
>
> In our experience, NUMA balancing is mostly disabled to avoid the
> potential latency spikes it can cause. The intent of disabling it for
> cgroup-B is to avoid latency spikes for tasks in cgroup-B; it does not
> matter whether tasks in other cgroups perform NUMA balancing. We
> therefore disable the task side only, not the memory side: DISABLED
> applies to tasks, not to memory.
>
> Demotion is global reclaim, not a per-cgroup balancing request.
> DISABLED only opts the cgroup's tasks out of NUMA balancing; it must
> not become a memory-side reclaim veto, or a cgroup could keep cold
> pages in the fast tier, leaving less fast-tier capacity for other
> cgroups' hot pages and defeating promotion for those pages.
>
> So, in the end, DISABLED is a task-side property and does not gate
> memory-side operations.
>
> Shared fast tier
> ~~~~~~~~~~~~~~~~
>
> A promotion competes for capacity in the shared fast tier, but it
> cannot consume more memory than the cgroup may already consume: it moves
> the cgroup's own folios, within its own memory limit, from the slow tier
> to the fast tier. Each contributor's own limit bounds its footprint,
> but the fast tier is still a shared resource; demotion is the global
> release valve that keeps cold pages from occupying it indefinitely.
>
> So a child can still enable tiering mode if its ancestor has only set
> normal.
>
> Given:
>
>                      Pod (normal)
>                      /        \
>           cgroup-A (none)  cgroup-B (tiering)
>
> the effective mode is:
>
>                       Pod (normal)
>                      /        \
>           cgroup-A (normal)  cgroup-B (normal|tiering)
>
> What an ancestor cannot do now is cap only tiering while leaving normal
> balancing available below. If that targeted cap is wanted, the natural
> extension is a negative request: NO_TIERING would propagate down the
> tree like the positive requests and mask TIERING out of the effective
> mode. It would cap promotion, not demotion; demotion remains global
> reclaim. With:
>
>                   Pod (normal | no_tiering)
>                      /            \
>             cgroup-A (none)   cgroup-B (tiering)
>
> the effective modes would be:
>
>                       Pod (normal)
>                      /            \
>             cgroup-A (normal)  cgroup-B (normal)
>
> For this version, I would keep the modes minimal and add NO_TIERING as a
> follow-up once there is a real user for it.
>
> Cost
> ====
>
> On the task side, an ancestor can opt out the whole subtree with
> DISABLED, but cannot cap only tiering while leaving normal balancing
> available below. On the memory side there is no opt-out at all:
> DISABLED is task-side only, so a descendant's TIERING request is enough
> to enable promotion for its own memory, and promotion is the mode that
> consumes the shared fast tier.
>
> The per-cgroup override was measured with schbench, 384 workers, on a
> two-node 384-core machine. In that configuration, a cgroup mode put the
> cgroup at the throughput of the equivalent system-wide setting, within a
> fraction of a percent. This is a throughput measurement, not a
> microbenchmark of the scan path: while the static key is on, a folio
> lookup is O(cgroup depth), and callers that need more than one bit now
> share a single lookup. An effective-mode cache may be worthwhile for
> very deep hierarchies; that is left for follow-up measurements.
>
> Limitations
> ===========
>
> The mode is per-memcg, not per-task. A cgroup without its own memcg
> cannot be set; its tasks and folios resolve to the nearest ancestor
> memcg. If multiple tasks in a single cgroup need different modes, place
> them in separate child cgroups with the memory controller enabled.
>
> For threaded cgroups, the mode must be set on the nearest domain
> ancestor that has the memory controller enabled, because the memory
> controller is not threaded.
>
> Changelog
> =========
>
> RFC v3 -> RFC v4:
> - Store numab_mode in mem_cgroup instead of struct cgroup
> - Refine the commit log
>
> RFC v3: https://lwn.net/Articles/1101442/
>
> RFC v2 -> RFC v3:
> - Replace the struct_ops interface with two kfuncs, following Tejun's
>   suggestion.
> - Improve function naming and simplify the code (David)
>
> RFC v2: https://lwn.net/Articles/1099962/
>
> RFC v1 -> RFC v2:
> - v1 keyed the hook on the memory cgroup and temporarily embedded the
>   cgroup id into the struct_ops value, because cgroup struct_ops support
>   was still under development.
> - v2 attaches through the cgroup struct_ops support that has since landed
>   in bpf-next, keys the task side on the task's cgroup and the memory side
>   on the folio's memcg, and adds the memory-tiering mode alongside enable
>   and disable.
>
> RFC v1: https://lwn.net/Articles/1054030/
>
> Yafang Shao (4):
>   sched: add helpers for numa balancing
>   sched: sample NUMA balancing mode once per path
>   sched: add BPF-based per-cgroup NUMA balancing mode selection
>   selftests/bpf: add tests for the numab kfuncs
>
>  include/linux/memcontrol.h                    |   6 +
>  include/linux/sched/numa_balancing.h          | 144 +++++++++
>  init/Kconfig                                  |  23 ++
>  kernel/sched/bpf_numab.c                      | 261 +++++++++++++++++
>  kernel/sched/build_policy.c                   |   4 +
>  kernel/sched/fair.c                           |  99 +++++--
>  kernel/sched/sched.h                          |   1 -
>  mm/memcontrol.c                               |  14 +
>  mm/memory-tiers.c                             |   9 +-
>  mm/mempolicy.c                                |   9 +-
>  mm/migrate.c                                  |   9 +-
>  mm/vmscan.c                                   |   6 +-
>  tools/testing/selftests/bpf/config            |   2 +
>  .../selftests/bpf/prog_tests/bpf_numab.c      | 273 ++++++++++++++++++
>  tools/testing/selftests/bpf/progs/bpf_numab.c |  32 ++
>  15 files changed, 844 insertions(+), 48 deletions(-)
>  create mode 100644 kernel/sched/bpf_numab.c
>  create mode 100644 tools/testing/selftests/bpf/prog_tests/bpf_numab.c
>  create mode 100644 tools/testing/selftests/bpf/progs/bpf_numab.c
>
> -- 
> 2.52.0


Best Regards,
Yan, Zi

      parent reply	other threads:[~2026-11-10  3:35 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-10  3:29 Yafang Shao
2026-11-10  3:29 ` [RFC PATCH bpf-next v4 1/4] sched: add helpers for numa balancing Yafang Shao
2026-11-10  3:37   ` Zi Yan
2026-11-10  3:29 ` [RFC PATCH bpf-next v4 2/4] sched: sample NUMA balancing mode once per path Yafang Shao
2026-11-10  3:29 ` [RFC PATCH bpf-next v4 3/4] sched: add BPF-based per-cgroup NUMA balancing mode selection Yafang Shao
2026-11-10  3:29 ` [RFC PATCH bpf-next v4 4/4] selftests/bpf: add tests for the numab kfuncs Yafang Shao
2026-11-10  3:35 ` Zi Yan [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=8D6452ED-CB2A-4D08-863E-039A9D0DA453@nvidia.com \
    --to=ziy@nvidia.com \
    --cc=akpm@linux-foundation.org \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=cgroups@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=david@kernel.org \
    --cc=eddyz87@gmail.com \
    --cc=gourry@gourry.net \
    --cc=hannes@cmpxchg.org \
    --cc=juri.lelli@redhat.com \
    --cc=laoar.shao@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=lizhe.67@bytedance.com \
    --cc=memxor@gmail.com \
    --cc=mgorman@suse.de \
    --cc=mingo@redhat.com \
    --cc=mkoutny@suse.com \
    --cc=peterz@infradead.org \
    --cc=roman.gushchin@linux.dev \
    --cc=shakeel.butt@linux.dev \
    --cc=tim.c.chen@linux.intel.com \
    --cc=tj@kernel.org \
    --cc=vincent.guittot@linaro.org \
    --cc=ying.huang@linux.alibaba.com \
    --cc=yu.c.chen@intel.com \
    --cc=zhao1.liu@intel.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®