* [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source folio order
@ 2026-10-28 23:17 Konstantin Khorenko
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Konstantin Khorenko @ 2026-10-28 23:17 UTC (permalink / raw)
To: Pavel Tikhomirov; +Cc: devel
memory.numa_migrate, our memcg file that vcmmd uses to move a cgroup's
memory between NUMA nodes, allocates a 2 MB destination for every large
folio it migrates. Page cache folios of a smaller order get a
destination of the wrong size, and the page cache falls apart around
it. Patch 1 is the fix, patch 2 a selftest that catches it.
What the customer hit
=====================
A two-node VMware host runs VMs with vcmmd's NUMA policy enabled, so
every few minutes vcmmd writes memory.numa_migrate for a VM's cgroup.
qemu writes its backup scratch file on NFS, and NFS sizes each page
cache folio to the write, so that file is full of order-1 to order-5
folios. Closing the scratch file dies in nfs4_evict_inode():
kernel BUG at fs/inode.c:737!
RIP: 0010:clear_inode+0x75/0x80
mapping->nrpages of the inode is negative, and the vmcore holds
order-9 folios of that file with an index not aligned to 512, the
refcount of a small folio, and whole xarray ranges of live folios that
are no longer in the tree. Reads of the migrated ranges return garbage,
so the backups taken on that host while the policy was active should
be verified. The same crash was filed as VSTOR-151676.
Where the 2 MB comes from
=========================
memcg_numa_migrate_new_folio() is the migrate_pages() allocation
callback. Its large folio branch dates from vz8 (4.18) and follows what
new_page() in mm/mempolicy.c did on the 4.x kernels:
if (thp_migration_supported() && PageTransHuge(page))
alloc ... HPAGE_PMD_ORDER
A compound page on the LRU could only be a PMD-sized THP then, so the
test implied the order. Upstream replaced that with folio_order() of
the source when its allocators moved to folios, ffe06786b540
("mm/migrate: Use a folio in alloc_migration_target()"). Our copy kept
the shape, only PageTransHuge() became folio_test_large(), which is
folio_test_head() and is true for any order from 1 up. NFS and XFS
large folios, shmem and anonymous mTHP now all reach it.
Why an oversized destination breaks the page cache
==================================================
The two sides of the page cache size the folio differently.
On insertion __folio_migrate_mapping() takes everything from the
source: nr = folio_nr_pages(old), the xarray state at the old index
with no xas_set_order(), newfolio->index = old->index,
folio_ref_add(newfolio, nr), and xas_store() replaces exactly the slots
the source occupied. folio_copy() copies the source pages only. So a
2 MB folio enters the cache as, say, a 4-page one: 4 xarray slots, 4
extra references, nrpages unchanged, 4 pages of valid data.
Everything afterwards asks the folio itself, and it answers order 9:
- page_cache_delete(): xas_set_order(index, 9) and nrpages -= 512.
512 slots wiped instead of 4, with the index rounded down to 512 so
that other live folios lose their slots, and 512 subtracted instead
of 4. That is the negative nrpages and the orphaned folios.
- __remove_mapping(): expects 1 + 512 references and finds 1 + 4.
Reclaim can never free the folio.
- filemap_free_folio(): drops 512 references from a handful at
eviction. The refcount goes negative and the folio leaks.
- reads: folio_file_page() picks the page by its offset inside the
folio, but only the first pages were copied.
The data damage shows on the first read of the folio: filemap_read()
takes the page at ki_pos & (folio_size - 1) of the destination, which
is uninitialised unless the index is 2 MB aligned, and a longer read
that starts there continues through the destination past the
neighbours. The structural damage waits for the first operation that
applies folio_nr_pages() or folio_order() to it: invalidation,
truncate, reclaim, inode eviction. On NFS cache invalidation is
routine (an attribute change), so the bug shows up there instead of
staying a silent 2 MB leak per folio.
In short: inserted by the source size, removed by the destination
size. That only works when the two match, which for a real THP they
always did. A debug kernel says the same on the first migration, in
mem_cgroup_migrate():
VM_BUG_ON_FOLIO(folio_nr_pages(old) != folio_nr_pages(new))
The patches
===========
Patch 1 allocates the destination with folio_order() of the source,
GFP_TRANSHUGE_LIGHT for large folios as before. The
thp_migration_supported() test goes: migrate_pages() splits PMD-mappable
folios itself when THP migration is unsupported, and every other large
folio still needs a matching destination.
Patch 2 adds tools/testing/selftests/ve/ve_numa_migrate_test.c. It
binds a cgroup to one NUMA node, writes a file on a loop-mounted XFS
image in 64K chunks so that the page cache holds large folios, widens
cpuset.mems to both nodes (the destination allocation honours the
writer's cpuset), migrates the cgroup to the other node through
memory.numa_migrate and compares what reads back. The chunks at 2 MB
aligned offsets are left out: a folio there reads right even from an
oversized destination. memory.current cannot serve as the detector:
mem_cgroup_migrate() only transfers the charge, whatever the size
mismatch. On an unfixed kernel the comparison fails.
The folio sizes are not taken on faith: the test records, page by
page, the folio order through /proc/self/pagemap and /proc/kpageflags
and the node through move_pages() in query mode, before and after the
migration. The file must hold a large folio below the PMD size before
(PMD-sized folios got a correct destination from the old code too).
After, every such folio must have kept its order on the target node,
been split into base pages or stayed behind, which migrate_pages() may
do under memory pressure, and at least one must have kept its order.
A failed check is a failure, not a skip: a skip would hide that the
test no longer reaches the bug. memory.numa_stat is only
logged, polled for a few seconds since memcg flushes its per-cpu
counters lazily. It needs root, two NUMA nodes with memory
(numa=fake=2 is enough), cgroup2 with memory and cpuset, and mkfs.xfs;
it skips otherwise.
Testing
=======
On a VM booted with numa=fake=2:
- the reproducer (64K and 8K writes on XFS from a node-bound cgroup,
memory.numa_migrate to the other node, unlink) kills the unfixed
debug kernel on the VM_BUG_ON above and the release kernel in
clear_inode(); with patch 1 migration succeeds and unlink and
eviction complete;
- the selftest passes on the fixed kernel and fails on the comparison
on 22.11.
Other branches
==============
The same patch applies as is to the 20.x branch (the customer's
kernel); the function is a plain callback, so it is a ReadyKernel
candidate.
vz9 carries the same PageTransHuge() code, but the file systems we
ship on 5.14 (XFS, NFS, ext4, shmem) put no non-PMD large folios on
the LRU, so it is not reachable there in practice.
https://virtuozzo.atlassian.net/browse/VSTOR-151607
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Changes in v4:
- Patch 2: the folios are compared one by one before and after the
migration, so a destination of another order is caught directly;
the log says how many kept their order, were split or stayed behind
(Sashiko).
- Patch 2: the memory.numa_stat poll waits for the bytes actually
written, not FILE_SIZE, which the holes made unreachable (Sashiko).
- Patch 2: memory.numa_migrate is written with a scan limit, so the
migration does not depend on the lazily flushed LRU stats on hosts
with the classic LRU; a negative shift in compare_folios() for the
holes, a missing bound in its page loop (independent review).
- Patch 2: the commit message says that the read-back catches the bug
and the folio checks prove the large folio path ran, and why
cpuset.mems is widened (independent review).
- Patch 2: the failure messages give the errno of a failed
memory.numa_migrate write, the offset of a chunk that reads back
wrong and the likely cause of a folio check failure; the binary is
added to .gitignore (independent review).
- Patch 2: the test no longer tries to spare the node on an unfixed
kernel; what happens after a failure is left to whoever debugs it.
- Link to v3: http://lore.virtuozzo.com/20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-v3-0-893494e8db76@virtuozzo.com
Changes in v3:
- Patch 2: the large folio guard requires a folio below the PMD size
and the page size comes from sysconf() (Sashiko). After the
migration the test checks through move_pages() that at least one
such folio sits on the target node in one piece, instead of
requiring the whole file in memory.numa_stat, which migrate_pages()
does not guarantee (Sashiko); the stat is logged only.
- Patch 2: the chunks at 2 MB aligned offsets are not written, so an
oversized destination always shows in the read-back, and the image
stays mounted until the last check passes; the folio sizes per node
are logged before and after the migration; the mkfs.xfs check goes,
a failing mkfs skips the test anyway (independent review).
- Patch 2: Reviewed-by tags dropped, the test changed too much since
the review.
- Link to v2: http://lore.virtuozzo.com/20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-v2-0-b973a82a5b0c@virtuozzo.com
Changes in v2:
- Patch 1: the commit message says why the thp_migration_supported()
check goes (Pavel).
- Patch 2: the folio sizes are checked through /proc/self/pagemap and
/proc/kpageflags before the migration; a file without a large folio
fails the test on purpose (Pavel).
- Patch 2: the read-back comparison runs first and the memory.numa_stat
check after it, polled for a few seconds, since memory_numa_stat_show()
flushes the per-cpu counters only past a per-CPU threshold (Vasileios).
- Reviewed-by from Pavel and Vasileios on both patches.
- Link to v1: http://lore.virtuozzo.com/20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-v1-0-268336526677@virtuozzo.com
---
Konstantin Khorenko (2):
mm/memcontrol: allocate numa_migrate targets of the source folio order
selftests/ve: check memory.numa_migrate keeps the folio order
mm/memcontrol.c | 11 +-
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/ve_numa_migrate_test.c | 562 ++++++++++++++++++++++
4 files changed, 570 insertions(+), 5 deletions(-)
---
base-commit: b499efa17aa9fb8f20512d1831d1cb61b22e60d3
change-id: 20261028-b4-prep-khorenko-vstor-151607-numa-migrate-order-4e3a664a89d4
Best regards,
--
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order
2026-10-28 23:17 [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko
@ 2026-10-28 23:17 ` Konstantin Khorenko
2026-10-28 23:21 ` no-reply
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko
2026-10-29 9:40 ` [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source " Konstantin Khorenko
2 siblings, 1 reply; 6+ messages in thread
From: Konstantin Khorenko @ 2026-10-28 23:17 UTC (permalink / raw)
To: Pavel Tikhomirov; +Cc: devel
memcg_numa_migrate_new_folio() treats every large folio as a PMD-sized
THP and allocates an HPAGE_PMD_ORDER destination for it. The branch is
the 4.18 pattern of new_page() in mm/mempolicy.c: a compound page on
the LRU could only be a THP then, so PageTransHuge() implied
HPAGE_PMD_ORDER.
Today the page cache holds large folios of any order (NFS sizes a folio
to the write, XFS and shmem have them too) and anonymous memory has
mTHP, while folio_test_large() is still just folio_test_head() and is
true for any order from 1 up. migrate_pages() never checks that the
destination order matches the source.
For an order-1 or order-4 file folio the destination is an order-9
folio that inherits the source index and page cache reference count,
so every later page cache operation that sizes the folio by its order
goes wrong: nrpages underflows, neighbouring xarray slots get wiped,
the folio can never be freed.
On a two-node host a VM migration to another NUMA node along with a
backup to NFS ends up with a negative nrpages, and closing the backup
file dies in nfs4_evict_inode():
kernel BUG at fs/inode.c:737!
RIP: 0010:clear_inode+0x75/0x80
Call Trace:
nfs4_evict_inode+0x21/0x80 [nfsv4]
evict+0x115/0x2a0
nfs4_delegreturn_release+0x55/0xa0 [nfsv4]
rpc_free_task+0x39/0x60 [sunrpc]
__rpc_execute+0x3d1/0x3e0 [sunrpc]
A debug build dies earlier, on the first migration of a large folio of
any file system, in mem_cgroup_migrate():
VM_BUG_ON_FOLIO(folio_nr_pages(old) != folio_nr_pages(new))
Make memcg_numa_migrate_new_folio() handle large folios properly.
The thp_migration_supported() check goes: migrate_pages() splits PMD
THPs itself when unsupported, other large folios migrate whole anyway.
Fixes: 9f8a754ad6c2 ("mm: memcontrol: add memory.numa_migrate file")
https://virtuozzo.atlassian.net/browse/VSTOR-151607
Feature: mm: interface to migrate memory between NUMA nodes upon userspace request
Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
mm/memcontrol.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 0734c0fa21d05..d8a87913ace37 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -3480,6 +3480,7 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio,
{
struct memcg_numa_migrate_struct *ms = (void *)private;
gfp_t gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_NORETRY | __GFP_NOWARN;
+ unsigned int order = 0;
ms->current_node = next_node(ms->current_node, *ms->target_nodes);
if (ms->current_node >= MAX_NUMNODES) {
@@ -3487,13 +3488,13 @@ static struct folio *memcg_numa_migrate_new_folio(struct folio *old_folio,
VM_BUG_ON(ms->current_node >= MAX_NUMNODES);
}
- if (thp_migration_supported() && folio_test_large(old_folio)) {
- return __folio_alloc(GFP_TRANSHUGE_LIGHT | __GFP_THISNODE,
- HPAGE_PMD_ORDER, ms->current_node,
- ms->target_nodes);
+ if (folio_test_large(old_folio)) {
+ gfp_mask = GFP_TRANSHUGE_LIGHT | __GFP_THISNODE;
+ order = folio_order(old_folio);
}
- return __folio_alloc(gfp_mask, 0, ms->current_node, ms->target_nodes);
+ return __folio_alloc(gfp_mask, order, ms->current_node,
+ ms->target_nodes);
}
/*
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* [Devel] [PATCH hci-8.1 v4 2/2] selftests/ve: check memory.numa_migrate keeps the folio order
2026-10-28 23:17 [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko
@ 2026-10-28 23:17 ` Konstantin Khorenko
2026-10-28 23:25 ` no-reply
2026-10-29 9:40 ` [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source " Konstantin Khorenko
2 siblings, 1 reply; 6+ messages in thread
From: Konstantin Khorenko @ 2026-10-28 23:17 UTC (permalink / raw)
To: Pavel Tikhomirov; +Cc: devel
memcg_numa_migrate_new_folio() used to give every large folio a
PMD-sized destination. A smaller page cache folio then read back as
garbage, and its inode died in clear_inode() on eviction.
The test writes a file on an XFS image in 64K chunks, which gives
order-4 folios, from a cgroup bound to one NUMA node. It widens the
cgroup's cpuset.mems to both nodes, as the destination allocation obeys
the writer's cpuset, migrates the cgroup to the second node with
memory.numa_migrate and compares the file with the written pattern.
The comparison is what catches the bug.
The chunks at 2 MB aligned offsets are not written: a folio there
reads right even from an oversized destination.
The folios are tracked page by page through /proc/self/pagemap,
/proc/kpageflags and move_pages(), so that a pass means the large folio
path really ran:
- before the migration the page cache must hold a large folio below
the PMD size;
- after it every such folio must have kept its order on the target
node, been split into base pages or stayed behind, and at least one
must have kept its order.
A failed check is a failure, not a skip, so that a setup where the
test stops reaching the bug shows up. A fragmented target node fails
it too, as the large destinations are allocated without reclaim.
On an unfixed release kernel the test fails on the comparison, or on
the folio checks when the target node has no free 2 MB block. A debug
kernel that gets an order-9 destination dies earlier, on the
VM_BUG_ON_FOLIO() in mem_cgroup_migrate().
Needs root, two NUMA nodes with memory (numa=fake=2 is enough),
cgroup2 with the memory and cpuset controllers and memory.numa_migrate,
and mkfs.xfs with loop devices; skips otherwise.
https://virtuozzo.atlassian.net/browse/VSTOR-151607
Feature: ve selftests
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/ve_numa_migrate_test.c | 562 ++++++++++++++++++++++
3 files changed, 564 insertions(+)
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 8ab1bfe6f5e4b..3d1179bd2c5f4 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -3,3 +3,4 @@ ve_perms_test
ve_mount_accounting_test
ve_cpuid_override_test
ve_vtty_stress
+ve_numa_migrate_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index 51f9316beb99b..e96320a183844 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -6,6 +6,7 @@ TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
TEST_GEN_PROGS += ve_mount_accounting_test
TEST_GEN_PROGS += ve_vtty_stress
+TEST_GEN_PROGS += ve_numa_migrate_test
# CPUID faulting is x86-64 only. ARCH says "x86" for 32-bit too and lib.mk
# picks the clang target by its emptiness, so decide by the compiler.
diff --git a/tools/testing/selftests/ve/ve_numa_migrate_test.c b/tools/testing/selftests/ve/ve_numa_migrate_test.c
new file mode 100644
index 0000000000000..02e134bc914d4
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_numa_migrate_test.c
@@ -0,0 +1,562 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * memory.numa_migrate must give a large page cache folio a destination of
+ * the same order. An oversized destination inherits the index and the
+ * reference count of the source, only the source pages are copied into it,
+ * a read of the folio is served from the destination by file offset and
+ * lands in its uninitialised part, and the inode dies in clear_inode() on
+ * eviction.
+ *
+ * Write a file on an XFS image (XFS allocates large folios for 64K writes)
+ * from a cgroup bound to one NUMA node, migrate the cgroup to another node
+ * and check that the data reads back intact.
+ *
+ * Needs root, two NUMA nodes with memory (numa=fake=2 will do), cgroup2
+ * with the memory and cpuset controllers and memory.numa_migrate, and
+ * mkfs.xfs with loop devices. Before and after the migration the folio of
+ * every page is read through /proc/self/pagemap and /proc/kpageflags and
+ * its node through move_pages(): a file without large folios below the PMD
+ * size, or a migration that moved none of them in one piece, fails the
+ * test, as it would pass without touching the bug.
+ */
+#define _GNU_SOURCE
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sched.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <sys/mount.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <linux/kernel-page-flags.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CG_NAME "ve_numa_migrate_test"
+#define DIR_TEMPLATE "/var/tmp/ve_numa_migrate.XXXXXX"
+/* mkfs.xfs refuses anything smaller; the image is sparse. */
+#define IMG_SIZE (320UL << 20)
+#define FILE_SIZE (4UL << 20)
+#define CHUNK_SIZE (64UL << 10)
+#define PM_PFN_MASK ((1ULL << 55) - 1)
+#define PM_PRESENT (1ULL << 63)
+#ifndef MADV_POPULATE_READ
+#define MADV_POPULATE_READ 22
+#endif
+#define SCAN_ORDERS 16
+/* The periodic memcg stats flush runs every two seconds. */
+#define STAT_RETRIES 50
+#define STAT_RETRY_US 100000
+
+FIXTURE(numa_migrate)
+{
+ int cgv2_fd;
+ int cg_fd;
+ int node_from;
+ int node_to;
+ char dir[64];
+ char img[80];
+ char mnt[80];
+ int mounted;
+ int data_fd;
+};
+
+/* First two node ids of a sysfs node list such as "0-1" or "0,2-3". */
+static int two_nodes_with_memory(int *from, int *to)
+{
+ char buf[256], *tok, *save;
+ int found = 0;
+
+ if (read_file_at(AT_FDCWD, "/sys/devices/system/node/has_memory",
+ buf, sizeof(buf)) <= 0)
+ return -1;
+
+ for (tok = strtok_r(buf, ",\n", &save); tok && found < 2;
+ tok = strtok_r(NULL, ",\n", &save)) {
+ int a, b, n;
+
+ if (sscanf(tok, "%d-%d", &a, &b) != 2) {
+ if (sscanf(tok, "%d", &a) != 1)
+ return -1;
+ b = a;
+ }
+ for (n = a; n <= b && found < 2; n++) {
+ if (found == 0)
+ *from = n;
+ else
+ *to = n;
+ found++;
+ }
+ }
+ return found == 2 ? 0 : -1;
+}
+
+static int run_cmd(const char *fmt, ...)
+{
+ char cmd[256];
+ va_list ap;
+ int status;
+
+ va_start(ap, fmt);
+ vsnprintf(cmd, sizeof(cmd), fmt, ap);
+ va_end(ap);
+
+ status = system(cmd);
+ if (status < 0 || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+/* Bytes of file pages the cgroup has on @node, from memory.numa_stat. */
+static int file_bytes_on_node(int cg_fd, int node, unsigned long long *out)
+{
+ static char buf[32768];
+ char key[16], *p;
+
+ if (read_file_at(cg_fd, "memory.numa_stat", buf, sizeof(buf)) <= 0)
+ return -1;
+ p = strstr(buf, "\nfile ");
+ if (!p)
+ return -1;
+ snprintf(key, sizeof(key), " N%d=", node);
+ p = strstr(p, key);
+ if (!p)
+ return -1;
+ *out = strtoull(p + strlen(key), NULL, 10);
+ return 0;
+}
+
+static unsigned long page_size;
+
+/* A folio at a PMD-aligned index reads right from an oversized one. */
+static bool chunk_is_hole(unsigned long off, unsigned long pmd_size)
+{
+ return !((off / CHUNK_SIZE * CHUNK_SIZE) % pmd_size);
+}
+
+/* One per page of the file: the node it sits on and the folio around it. */
+struct page_info {
+ int node;
+ int order; /* -1 in a hole */
+ bool head;
+};
+
+static void fill_run(struct page_info *pi, unsigned long start, long run,
+ int node)
+{
+ int order = 0;
+ long i;
+
+ while ((1L << order) < run)
+ order++;
+ for (i = 0; i < run; i++) {
+ pi[start + i].node = node;
+ pi[start + i].order = order;
+ pi[start + i].head = !i;
+ }
+}
+
+/*
+ * Walk the page cache of @fd through /proc/self/pagemap, /proc/kpageflags
+ * and move_pages(2) in query mode, leaving the holes alone: faulting them
+ * in would add folios to the file. Returns -1 when that is not possible.
+ */
+static int scan_file_folios(int fd, unsigned long size, unsigned long pmd_size,
+ struct page_info *pi)
+{
+ unsigned long nr = size / page_size, i, off, start = 0;
+ long run = 0;
+ int run_node = -1, ret = -1;
+ int pagemap, kpageflags, *status;
+ unsigned char *map;
+ void **pages;
+ uint64_t *pm;
+
+ for (i = 0; i < nr; i++)
+ pi[i].order = -1;
+ pagemap = open("/proc/self/pagemap", O_RDONLY);
+ kpageflags = open("/proc/kpageflags", O_RDONLY);
+ pm = malloc(nr * sizeof(*pm));
+ pages = malloc(nr * sizeof(*pages));
+ status = malloc(nr * sizeof(*status));
+ map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ if (pagemap < 0 || kpageflags < 0 || !pm || !pages || !status ||
+ map == MAP_FAILED)
+ goto out;
+ for (off = 0; off < size; off += CHUNK_SIZE) {
+ if (!chunk_is_hole(off, pmd_size) &&
+ madvise(map + off, CHUNK_SIZE, MADV_POPULATE_READ))
+ goto out;
+ }
+ if (pread(pagemap, pm, nr * sizeof(*pm),
+ (unsigned long)map / page_size * sizeof(*pm)) !=
+ (ssize_t)(nr * sizeof(*pm)))
+ goto out;
+ for (i = 0; i < nr; i++)
+ pages[i] = map + i * page_size;
+ if (syscall(__NR_move_pages, 0, nr, pages, NULL, status, 0))
+ goto out;
+ for (i = 0; i < nr; i++) {
+ uint64_t flags, pfn = pm[i] & PM_PFN_MASK;
+
+ if (chunk_is_hole(i * page_size, pmd_size)) {
+ fill_run(pi, start, run, run_node);
+ run = 0;
+ continue;
+ }
+ if (!(pm[i] & PM_PRESENT) || !pfn || status[i] < 0 ||
+ pread(kpageflags, &flags, sizeof(flags),
+ pfn * sizeof(flags)) != sizeof(flags))
+ goto out;
+ if ((flags & (1ULL << KPF_COMPOUND_TAIL)) && run) {
+ run++;
+ continue;
+ }
+ fill_run(pi, start, run, run_node);
+ start = i;
+ run = 1;
+ run_node = status[i];
+ if (!(flags & (1ULL << KPF_COMPOUND_HEAD))) {
+ fill_run(pi, start, run, run_node);
+ run = 0;
+ }
+ }
+ fill_run(pi, start, run, run_node);
+ ret = 0;
+out:
+ if (map != MAP_FAILED)
+ munmap(map, size);
+ free(status);
+ free(pages);
+ free(pm);
+ if (pagemap >= 0)
+ close(pagemap);
+ if (kpageflags >= 0)
+ close(kpageflags);
+ return ret;
+}
+
+/* The folio sizes of a scan, "62 x 64K, 3 x 4K", or "none". */
+static const char *folio_sizes_str(const struct page_info *pi,
+ unsigned long nr, char *buf, size_t len)
+{
+ long hist[SCAN_ORDERS] = {};
+ size_t pos = 0;
+ unsigned long i;
+ int order;
+
+ for (i = 0; i < nr; i++) {
+ if (pi[i].order >= 0 && pi[i].head)
+ hist[pi[i].order < SCAN_ORDERS ? pi[i].order :
+ SCAN_ORDERS - 1]++;
+ }
+ for (order = SCAN_ORDERS - 1; order >= 0; order--) {
+ if (!hist[order])
+ continue;
+ pos += snprintf(buf + pos, len - pos, "%s%ld x %luK",
+ pos ? ", " : "", hist[order],
+ (page_size << order) / 1024);
+ if (pos >= len)
+ break;
+ }
+ return pos ? buf : "none";
+}
+
+/* Large folios below the PMD size in a scan. */
+static long large_below_pmd(const struct page_info *pi, unsigned long nr,
+ long pmd_pages)
+{
+ unsigned long i;
+ long count = 0;
+
+ for (i = 0; i < nr; i++) {
+ if (pi[i].head && pi[i].order > 0 &&
+ (1L << pi[i].order) < pmd_pages)
+ count++;
+ }
+ return count;
+}
+
+/* What became of the large folios below the PMD size of @before. */
+struct migrated {
+ long intact; /* the same folio, now on the target node */
+ long split; /* its pages on the target node as base pages */
+ long left; /* still on the source node */
+ long wrong; /* another order, or moved in part */
+};
+
+static void compare_folios(const struct page_info *before,
+ const struct page_info *after, unsigned long nr,
+ int node, long pmd_pages, struct migrated *m)
+{
+ unsigned long i, j;
+
+ memset(m, 0, sizeof(*m));
+ for (i = 0; i < nr; i++) {
+ long pages, on_node = 0, base = 0;
+
+ if (before[i].order <= 0 || !before[i].head)
+ continue;
+ pages = 1L << before[i].order;
+ if (pages >= pmd_pages)
+ continue;
+ for (j = i; j < i + pages && j < nr; j++) {
+ on_node += after[j].node == node;
+ base += !after[j].order;
+ }
+ if (!on_node)
+ m->left++;
+ else if (on_node != pages)
+ m->wrong++;
+ else if (after[i].head && after[i].order == before[i].order)
+ m->intact++;
+ else if (base == pages)
+ m->split++;
+ else
+ m->wrong++;
+ }
+}
+
+static void cleanup(FIXTURE_DATA(numa_migrate) *self)
+{
+ char path[PATH_MAX];
+
+ if (self->data_fd >= 0) {
+ close(self->data_fd);
+ self->data_fd = -1;
+ }
+ if (self->dir[0]) {
+ snprintf(path, sizeof(path), "%s/data", self->mnt);
+ unlink(path);
+ if (self->mounted)
+ umount(self->mnt);
+ unlink(self->img);
+ rmdir(self->mnt);
+ rmdir(self->dir);
+ self->dir[0] = '\0';
+ }
+ if (self->cg_fd >= 0) {
+ close(self->cg_fd);
+ self->cg_fd = -1;
+ }
+ if (self->cgv2_fd >= 0) {
+ /* The test task may still sit in the cgroup after a failure. */
+ write_u64_at(self->cgv2_fd, "cgroup.procs", getpid());
+ unlinkat(self->cgv2_fd, CG_NAME, AT_REMOVEDIR);
+ close(self->cgv2_fd);
+ self->cgv2_fd = -1;
+ }
+}
+
+/*
+ * Returns 0, or 1 with everything undone when the host cannot run the test.
+ * The harness runs no teardown after a failed setup, so the undo is here.
+ */
+static int prepare(FIXTURE_DATA(numa_migrate) *self, const char **why)
+{
+ int fd;
+
+ self->cgv2_fd = mount_cg2_fd();
+ if (self->cgv2_fd < 0) {
+ *why = "cannot mount cgroup2";
+ goto fail;
+ }
+ if (write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+ "+memory +cpuset")) {
+ *why = "cannot enable the memory and cpuset controllers";
+ goto fail;
+ }
+ if (mkdirat(self->cgv2_fd, CG_NAME, 0755) && errno != EEXIST) {
+ *why = "cannot create the test cgroup";
+ goto fail;
+ }
+ self->cg_fd = openat(self->cgv2_fd, CG_NAME, O_PATH | O_DIRECTORY);
+ if (self->cg_fd < 0) {
+ *why = "cannot open the test cgroup";
+ goto fail;
+ }
+ if (faccessat(self->cg_fd, "memory.numa_migrate", W_OK, 0)) {
+ *why = "no memory.numa_migrate";
+ goto fail;
+ }
+ if (write_u64_at(self->cg_fd, "cpuset.mems", self->node_from)) {
+ *why = "cannot bind the cgroup to a node";
+ goto fail;
+ }
+
+ snprintf(self->dir, sizeof(self->dir), DIR_TEMPLATE);
+ if (!mkdtemp(self->dir)) {
+ self->dir[0] = '\0';
+ *why = "cannot create a temporary directory";
+ goto fail;
+ }
+ snprintf(self->img, sizeof(self->img), "%s/img", self->dir);
+ snprintf(self->mnt, sizeof(self->mnt), "%s/mnt", self->dir);
+ if (mkdir(self->mnt, 0755)) {
+ *why = "cannot create the mount point";
+ goto fail;
+ }
+ fd = open(self->img, O_CREAT | O_RDWR, 0600);
+ if (fd < 0) {
+ *why = "cannot create the image file";
+ goto fail;
+ }
+ if (ftruncate(fd, IMG_SIZE)) {
+ close(fd);
+ *why = "cannot size the image file";
+ goto fail;
+ }
+ close(fd);
+ if (run_cmd("mkfs.xfs -q -f %s", self->img)) {
+ *why = "mkfs.xfs failed";
+ goto fail;
+ }
+ if (run_cmd("mount -o loop %s %s", self->img, self->mnt)) {
+ *why = "cannot loop-mount the XFS image";
+ goto fail;
+ }
+ self->mounted = 1;
+ return 0;
+fail:
+ cleanup(self);
+ return 1;
+}
+
+FIXTURE_SETUP(numa_migrate)
+{
+ const char *why;
+
+ self->cgv2_fd = -1;
+ self->cg_fd = -1;
+ self->data_fd = -1;
+ self->dir[0] = '\0';
+ page_size = sysconf(_SC_PAGESIZE);
+
+ if (geteuid())
+ SKIP(return, "needs root");
+ if (two_nodes_with_memory(&self->node_from, &self->node_to))
+ SKIP(return, "needs two NUMA nodes with memory");
+ if (prepare(self, &why))
+ SKIP(return, "%s", why);
+}
+
+FIXTURE_TEARDOWN(numa_migrate)
+{
+ cleanup(self);
+}
+
+TEST_F(numa_migrate, large_folio_order)
+{
+ unsigned long long on_to_before, on_to_after;
+ unsigned char *buf, *check;
+ char path[PATH_MAX], list[64];
+ unsigned long long pmd_size, written = 0;
+ struct page_info *before, *after;
+ struct migrated m;
+ char sizes[128];
+ long pmd_pages;
+ unsigned long off;
+ int fd, i;
+
+ ASSERT_EQ(write_u64_at(self->cg_fd, "cgroup.procs", getpid()), 0);
+
+ buf = malloc(CHUNK_SIZE);
+ check = malloc(CHUNK_SIZE);
+ before = calloc(FILE_SIZE / page_size, sizeof(*before));
+ after = calloc(FILE_SIZE / page_size, sizeof(*after));
+ ASSERT_NE(buf, NULL);
+ ASSERT_NE(check, NULL);
+ ASSERT_NE(before, NULL);
+ ASSERT_NE(after, NULL);
+
+ /* PMD-sized folios got a correct destination from the old code too. */
+ if (read_u64_at(AT_FDCWD,
+ "/sys/kernel/mm/transparent_hugepage/hpage_pmd_size",
+ &pmd_size))
+ pmd_size = 2UL << 20;
+ pmd_pages = pmd_size / page_size;
+
+ snprintf(path, sizeof(path), "%s/data", self->mnt);
+ fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0600);
+ ASSERT_GE(fd, 0);
+ self->data_fd = fd;
+ for (off = 0; off < FILE_SIZE; off += CHUNK_SIZE) {
+ if (chunk_is_hole(off, pmd_size))
+ continue;
+ memset(buf, (off / CHUNK_SIZE) & 0xff, CHUNK_SIZE);
+ ASSERT_EQ(pwrite(fd, buf, CHUNK_SIZE, off), (ssize_t)CHUNK_SIZE);
+ written += CHUNK_SIZE;
+ }
+ ASSERT_EQ(fsync(fd), 0);
+ ASSERT_EQ(scan_file_folios(fd, FILE_SIZE, pmd_size, before), 0)
+ TH_LOG("cannot read the page cache folio sizes");
+ TH_LOG("before: %s", folio_sizes_str(before, FILE_SIZE / page_size,
+ sizes, sizeof(sizes)));
+ ASSERT_GT(large_below_pmd(before, FILE_SIZE / page_size, pmd_pages), 0)
+ TH_LOG("no large folio below the PMD size, the bug is out of reach");
+
+ ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to,
+ &on_to_before), 0);
+
+ snprintf(list, sizeof(list), "%d,%d", self->node_from, self->node_to);
+ ASSERT_EQ(write_file_at(self->cg_fd, "cpuset.mems", list), 0);
+
+ /*
+ * With a scan limit the kernel walks the LRUs in batches until a pass
+ * finds nothing, instead of sizing the walk from the LRU stats, which
+ * lag on the classic LRU; the limit counts folios scanned, far above
+ * the need.
+ */
+ snprintf(list, sizeof(list), "%d %lu", self->node_to,
+ IMG_SIZE / page_size);
+ ASSERT_EQ(write_file_at(self->cg_fd, "memory.numa_migrate", list), 0)
+ TH_LOG("memory.numa_migrate: %s", strerror(errno));
+
+ for (off = 0; off < FILE_SIZE; off += CHUNK_SIZE) {
+ if (chunk_is_hole(off, pmd_size))
+ continue;
+ memset(buf, (off / CHUNK_SIZE) & 0xff, CHUNK_SIZE);
+ ASSERT_EQ(pread(fd, check, CHUNK_SIZE, off), (ssize_t)CHUNK_SIZE);
+ ASSERT_EQ(memcmp(buf, check, CHUNK_SIZE), 0)
+ TH_LOG("the chunk at %lu reads back wrong", off);
+ }
+
+ /* memory.numa_stat flushes per-cpu counters lazily, give it time. */
+ for (i = 0; i < STAT_RETRIES; i++) {
+ ASSERT_EQ(file_bytes_on_node(self->cg_fd, self->node_to,
+ &on_to_after), 0);
+ if (on_to_after >= on_to_before + written)
+ break;
+ usleep(STAT_RETRY_US);
+ }
+ TH_LOG("file bytes on node %d: %llu -> %llu", self->node_to,
+ on_to_before, on_to_after);
+
+ ASSERT_EQ(scan_file_folios(fd, FILE_SIZE, pmd_size, after), 0)
+ TH_LOG("cannot read the page cache folio sizes");
+ compare_folios(before, after, FILE_SIZE / page_size, self->node_to,
+ pmd_pages, &m);
+ TH_LOG("after: %s; %ld intact, %ld split, %ld left, %ld wrong",
+ folio_sizes_str(after, FILE_SIZE / page_size, sizes,
+ sizeof(sizes)),
+ m.intact, m.split, m.left, m.wrong);
+ ASSERT_EQ(m.wrong, 0)
+ TH_LOG("a folio changed its order or moved in part (memory pressure?)");
+ ASSERT_GT(m.intact, 0)
+ TH_LOG("no folio below the PMD size reached node %d in one piece (fragmented?)",
+ self->node_to);
+
+ free(after);
+ free(before);
+ free(buf);
+ free(check);
+}
+
+TEST_HARNESS_MAIN
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko
@ 2026-10-28 23:21 ` no-reply
0 siblings, 0 replies; 6+ messages in thread
From: no-reply @ 2026-10-28 23:21 UTC (permalink / raw)
To: Konstantin Khorenko, Pavel Tikhomirov; +Cc: devel
> memcg_numa_migrate_new_folio() treats every large folio as a PMD-sized
> THP and allocates an HPAGE_PMD_ORDER destination for it. The branch is
> the 4.18 pattern of new_page() in mm/mempolicy.c: a compound page on
> the LRU could only be a THP then, so PageTransHuge() implied
> HPAGE_PMD_ORDER.
>
> Today the page cache holds large folios of any order (NFS sizes a folio
> to the write, XFS and shmem have them too) and anonymous memory has
> mTHP, while folio_test_large() is still just folio_test_head() and is
> true for any order from 1 up. migrate_pages() never checks that the
> destination order matches the source.
>
> For an order-1 or order-4 file folio the destination is an order-9
> folio that inherits the source index and page cache reference count,
> so every later page cache operation that sizes the folio by its order
> [ ... ]
>
> Feature: mm: interface to migrate memory between NUMA nodes upon userspace request
> Reviewed-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
> Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261029-b4-prep-khorenko-vstor-151607-numa-migrate-order-v4-0-cf966bf93405@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [Devel] [PATCH hci-8.1 v4 2/2] selftests/ve: check memory.numa_migrate keeps the folio order
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko
@ 2026-10-28 23:25 ` no-reply
0 siblings, 0 replies; 6+ messages in thread
From: no-reply @ 2026-10-28 23:25 UTC (permalink / raw)
To: Pavel Tikhomirov, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests/ve: numa_migrate test leaves +memory +cpuset enabled in root cgroup
--
commit cd13e136e28785e40b5f6accc767a4d1481054d6
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
selftests/ve: check memory.numa_migrate keeps the folio order
This adds a selftest that writes a file on an XFS image from a cgroup
bound to one NUMA node, migrates the cgroup to a second node with
memory.numa_migrate, and checks that the data reads back intact and that
large folios kept their order. It tracks the folios through
/proc/self/pagemap, /proc/kpageflags and move_pages().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-151607
> diff --git a/tools/testing/selftests/ve/ve_numa_migrate_test.c b/tools/testing/selftests/ve/ve_numa_migrate_test.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_numa_migrate_test.c
[ ... ]
> +static void cleanup(FIXTURE_DATA(numa_migrate) *self)
> +{
> + char path[PATH_MAX];
> +
[ ... ]
> + if (self->cgv2_fd >= 0) {
> + /* The test task may still sit in the cgroup after a failure. */
> + write_u64_at(self->cgv2_fd, "cgroup.procs", getpid());
> + unlinkat(self->cgv2_fd, CG_NAME, AT_REMOVEDIR);
> + close(self->cgv2_fd);
> + self->cgv2_fd = -1;
> + }
> +}
> +
[ ... ]
> +static int prepare(FIXTURE_DATA(numa_migrate) *self, const char **why)
> +{
> + int fd;
> +
> + self->cgv2_fd = mount_cg2_fd();
> + if (self->cgv2_fd < 0) {
> + *why = "cannot mount cgroup2";
> + goto fail;
> + }
> + if (write_file_at(self->cgv2_fd, "cgroup.subtree_control",
> + "+memory +cpuset")) {
[Severity: Low]
This isn't a functional defect in the checked logic, but prepare() writes
"+memory +cpuset" to the root cgroup.subtree_control, and cleanup() only
removes the CG_NAME cgroup with unlinkat(AT_REMOVEDIR).
Is it intended that cleanup() never writes "-cpuset -memory" back?
On a host where the root had neither controller enabled in
subtree_control, every run changes the host-wide cgroup2 controller state,
since the mount from mount_cg2_fd() is a view of the single cgroup2
hierarchy. Later tests may then see different controller delegation.
If the test is killed or times out, the harness skips teardown, so the
loop mount, the image file and the /var/tmp/ve_numa_migrate.* directory
can also be left behind.
Could prepare() record whether the controllers were already enabled, and
could cleanup() restore the previous state?
> + *why = "cannot enable the memory and cpuset controllers";
> + goto fail;
> + }
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261029-b4-prep-khorenko-vstor-151607-numa-migrate-order-v4-0-cf966bf93405@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source folio order
2026-10-28 23:17 [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko
@ 2026-10-29 9:40 ` Konstantin Khorenko
2 siblings, 0 replies; 6+ messages in thread
From: Konstantin Khorenko @ 2026-10-29 9:40 UTC (permalink / raw)
To: Pavel Tikhomirov, Konstantin Khorenko; +Cc: devel
On Thu, 29 Oct 2026 00:17:30 +0100, Konstantin Khorenko wrote:
> mm/memcontrol: numa_migrate destinations of the source folio order
>
> memory.numa_migrate, our memcg file that vcmmd uses to move a cgroup's
> memory between NUMA nodes, allocates a 2 MB destination for every large
> folio it migrates. Page cache folios of a smaller order get a
> destination of the wrong size, and the page cache falls apart around
> it. Patch 1 is the fix, patch 2 a selftest that catches it.
>
> [...]
Applied to branch-rh10-6.12.0-211.52.1.22.x.vz10-ovz, will be released in rh10-6.12.0-211.52.1.22.12.vz10, thanks!
[1/2] mm/memcontrol: allocate numa_migrate targets of the source folio order
https://bitbucket.org/virtuozzocore/vzkernel.vzs/commits/a804befd5e7f
[2/2] selftests/ve: check memory.numa_migrate keeps the folio order
https://bitbucket.org/virtuozzocore/vzkernel.vzs/commits/5ef9d1af4b0f
--
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-10-29 9:41 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-28 23:17 [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source folio order Konstantin Khorenko
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 1/2] mm/memcontrol: allocate numa_migrate targets " Konstantin Khorenko
2026-10-28 23:21 ` no-reply
2026-10-28 23:17 ` [Devel] [PATCH hci-8.1 v4 2/2] selftests/ve: check memory.numa_migrate keeps the " Konstantin Khorenko
2026-10-28 23:25 ` no-reply
2026-10-29 9:40 ` [Devel] [PATCH hci-8.1 v4 0/2] mm/memcontrol: numa_migrate destinations of the source " Konstantin Khorenko
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.