[PATCH RFC 0/4] mm: KUnit tests for the page allocator

Brendan Jackman posted 4 patches 9 months, 3 weeks ago
drivers/base/memory.c    |   5 +-
include/linux/memory.h   |   4 +
include/linux/nodemask.h |  13 +++
kernel/kthread.c         |   3 +
lib/kunit/assert.c       |   2 +-
lib/kunit/resource.c     |   2 +-
lib/kunit/test.c         |   2 +-
mm/.kunitconfig          |  10 ++
mm/Kconfig               |   8 ++
mm/Makefile              |   2 +
mm/internal.h            |  11 ++
mm/memory_hotplug.c      |  26 +++--
mm/numa_memblks.c        |  22 ++++
mm/page_alloc.c          |  37 +++++-
mm/page_alloc_test.c     | 296 +++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 429 insertions(+), 14 deletions(-)
[PATCH RFC 0/4] mm: KUnit tests for the page allocator
Posted by Brendan Jackman 9 months, 3 weeks ago
The page allocator does a lot of stuff that is not visible to the user
in any deterministic way. But this stuff is still important and it would
be nice to test that behaviour.

KUnit is a tool for unit-testing kernel-internal APIs. This is an
attempt to adopt it the page allocator.

I have been hacking on this as a way to try and test the code I'm
writing for my ASI page_alloc integration proposal [0]. It's been
extremely useful to be able to "just call it and see what it does". So I
wanna gather some feedback on whether this basic idea is of interest
before I invest too much more time in it.

You can run these tests like this:

tools/testing/kunit/kunit.py run \
	--arch=x86_64 --kernel_args="movablecore=2G" \
	--qemu_args="-m 4G" --kunitconfig mm/.kunitconfig

Unit-testing code that has mutable global variables can be a pain.
Unit-testing code with mutable global variables _that can change
concurrently with the tests_ is basically impossible. So, we need some
way to isolate an "instance" of the allocator that doesn't refer to any
such concurrently-mutated state.

Luckily, the allocator only has one really important global variable:
node_data. So, the approach here is to carve out a subset of that
variable which is as isolated as possible from the rest of rthe system,
which can be used for deterministic testing. This is achieved by crating
a fake "isolated" node at boot, and plugging in memory at test init
time.

This is an RFC and not a PATCH because:

1. I have not taken much care to ensure the isolation is complete.
   There are probably sources of flakiness and nondeterminism in here.

2. I suspect the the basic idea might be over-complicated: do we really
   need memory hotplug here? Do we even need the instance of the
   allocator we're testing to actual memory behind the pages it's
   allocating, or could we just hallucinate a new region of vmemmap
   without any of that awkwardness?

   One significant downside of relying on memory hotplug is that the
   test won't run if we can't hotplug anything out. That means you have
   to fiddle with the platform to even run the tests - see the
   --kernel_args and --qemu_args I had to add to my kunit.py command
   above.

   So yeah, other suggestions welcome.

   2b. I'm not very confident I'm using the hotplug API properly.

3. There's no point in merging this without actually having at least a
   few tests that are actually interesting!

   Maybe a "build it and they will come" approach can be justified to
   some extent, but there's a nonzero cost to the infrastructure so we
   should probably have some confidence that they will indeed come.

[0] https://lore.kernel.org/linux-mm/20250129144320.2675822-1-jackmanb@google.com/ 

Signed-off-by: Brendan Jackman <jackmanb@google.com>
---
Brendan Jackman (4):
      kunit: Allocate assertion data with GFP_ATOMIC
      mm/page_alloc_test: Add empty KUnit boilerplate
      mm/page_alloc_test: Add logic to isolate a node for testing
      mm/page_alloc_test: Add smoke-test for page allocation

 drivers/base/memory.c    |   5 +-
 include/linux/memory.h   |   4 +
 include/linux/nodemask.h |  13 +++
 kernel/kthread.c         |   3 +
 lib/kunit/assert.c       |   2 +-
 lib/kunit/resource.c     |   2 +-
 lib/kunit/test.c         |   2 +-
 mm/.kunitconfig          |  10 ++
 mm/Kconfig               |   8 ++
 mm/Makefile              |   2 +
 mm/internal.h            |  11 ++
 mm/memory_hotplug.c      |  26 +++--
 mm/numa_memblks.c        |  22 ++++
 mm/page_alloc.c          |  37 +++++-
 mm/page_alloc_test.c     | 296 +++++++++++++++++++++++++++++++++++++++++++++++
 15 files changed, 429 insertions(+), 14 deletions(-)
---
base-commit: d082ecbc71e9e0bf49883ee4afd435a77a5101b6
change-id: 20250219-page-alloc-kunit-df76ef8d8eb9

Best regards,
-- 
Brendan Jackman <jackmanb@google.com>
Re: [PATCH RFC 0/4] mm: KUnit tests for the page allocator
Posted by David Hildenbrand 9 months, 3 weeks ago
On 24.02.25 15:47, Brendan Jackman wrote:
> The page allocator does a lot of stuff that is not visible to the user
> in any deterministic way. But this stuff is still important and it would
> be nice to test that behaviour.
> 
> KUnit is a tool for unit-testing kernel-internal APIs. This is an
> attempt to adopt it the page allocator.
> 
> I have been hacking on this as a way to try and test the code I'm
> writing for my ASI page_alloc integration proposal [0]. It's been
> extremely useful to be able to "just call it and see what it does". So I
> wanna gather some feedback on whether this basic idea is of interest
> before I invest too much more time in it.
> 
> You can run these tests like this:
> 
> tools/testing/kunit/kunit.py run \
> 	--arch=x86_64 --kernel_args="movablecore=2G" \
> 	--qemu_args="-m 4G" --kunitconfig mm/.kunitconfig
> 
> Unit-testing code that has mutable global variables can be a pain.
> Unit-testing code with mutable global variables _that can change
> concurrently with the tests_ is basically impossible. So, we need some
> way to isolate an "instance" of the allocator that doesn't refer to any
> such concurrently-mutated state.
> 
> Luckily, the allocator only has one really important global variable:
> node_data. So, the approach here is to carve out a subset of that
> variable which is as isolated as possible from the rest of rthe system,
> which can be used for deterministic testing. This is achieved by crating
> a fake "isolated" node at boot, and plugging in memory at test init
> time.
> 
> This is an RFC and not a PATCH because:
> 
> 1. I have not taken much care to ensure the isolation is complete.
>     There are probably sources of flakiness and nondeterminism in here.
> 
> 2. I suspect the the basic idea might be over-complicated: do we really
>     need memory hotplug here? Do we even need the instance of the
>     allocator we're testing to actual memory behind the pages it's
>     allocating, or could we just hallucinate a new region of vmemmap
>     without any of that awkwardness?
> 
>     One significant downside of relying on memory hotplug is that the
>     test won't run if we can't hotplug anything out. That means you have
>     to fiddle with the platform to even run the tests - see the
>     --kernel_args and --qemu_args I had to add to my kunit.py command
>     above.
> 
>     So yeah, other suggestions welcome.
> 
>     2b. I'm not very confident I'm using the hotplug API properly.

Me neither ;)

Dynamically adding memory to that "fake" node is certainly interesting, 
but which guarantees do we have that some other features (page 
migration, memory offlining, page reporting ...) don't interact in weird 
ways with this "fake" node? Adding special-casing all over the place for 
that feels wrong. I assume this is point 1. you note above.

So I don't quite love the idea on first sight ... but I haven't grasped 
all details of the full picture yet I'm afraid.

-- 
Cheers,

David / dhildenb
Re: [PATCH RFC 0/4] mm: KUnit tests for the page allocator
Posted by Brendan Jackman 9 months, 3 weeks ago
On Tue, Feb 25, 2025 at 11:01:47AM +0100, David Hildenbrand wrote:
> > This is an RFC and not a PATCH because:
> > 
> > 1. I have not taken much care to ensure the isolation is complete.
> >     There are probably sources of flakiness and nondeterminism in here.
> > 
> > 2. I suspect the the basic idea might be over-complicated: do we really
> >     need memory hotplug here? Do we even need the instance of the
> >     allocator we're testing to actual memory behind the pages it's
> >     allocating, or could we just hallucinate a new region of vmemmap
> >     without any of that awkwardness?
> > 
> >     One significant downside of relying on memory hotplug is that the
> >     test won't run if we can't hotplug anything out. That means you have
> >     to fiddle with the platform to even run the tests - see the
> >     --kernel_args and --qemu_args I had to add to my kunit.py command
> >     above.
> > 
> >     So yeah, other suggestions welcome.
> > 
> >     2b. I'm not very confident I'm using the hotplug API properly.
> 
> Me neither ;)
> 
> Dynamically adding memory to that "fake" node is certainly interesting, but
> which guarantees do we have that some other features (page migration, memory
> offlining, page reporting ...) don't interact in weird ways with this "fake"
> node? Adding special-casing all over the place for that feels wrong. I
> assume this is point 1. you note above.

Yeah, basically this is the big downside. Changing the system we're
trying to test in order to make it testable can't be avoided entirely,
but I am also pretty unhappy with sprinkling "if (node_isolated(node))"
everywhere.

I guess the ideal approach is one where, instead of having to modify
the meaning of node_data, we just support replacing it completely,
e.g:

struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned int order,
		int preferred_nid, nodemask_t *nodemask, 
		struct pagelist_data *node_data)
{
	struct alloc_context ac = { .node_data = node_data };

	// ...
}

Ideally this could be done in such a way that it disappears completely
outside of KUnit builds, the interface should be private and we'd
wanna get rid of any unnecessary pointer chasing with stuff like:

#ifdef CONFIG_PAGE_ALLOC_KUNIT_TESTS
static inline struct ac_node_data(struct alloc_context *ac, int node)
{
	return ac->node_data[node];
}
#else
#define ac_node_data(ac, node) (NODE_DATA(node))
#endif

I initially rejected this approach because it felt "too intrusive",
but now that I've actually written this RFC I think it could be less
intrusive than the node_isolated() thing I've proposed here.

The most obvious challenges I can see there are:

- There might be places that page_alloc.c calls out to that care about
  node_data but where we really don't want to plumb the alloc_context
  through (maybe vmscan.c is already such a place)?

- I dunno how many more such helpers we'd need beyond ac_node_data(),
  like do we need ac_nodes_possible_mask() etc etc etc?

But maybe worth a try - can you see any obvious reason this idea is
stupid?

> So I don't quite love the idea on first sight ... but I haven't grasped all
> details of the full picture yet I'm afraid.

Do you have any thoughts about "making up" memory instead of
hot-unplugging real memory for test usage? That might simplify things
a bit?

It seems possible that very little mm code cares if the memory we're
managing actually exists. (For ASI code we did briefly experiment with
tracking information about free pages in the page itself, but it's
pretty sketchy and the presence of debug_pagealloc makes me think
nobody does it today).

There might be arch-specific issues there, but for unit tests it
seems OK if they don't work on every ISA.
Re: [PATCH RFC 0/4] mm: KUnit tests for the page allocator
Posted by David Hildenbrand 9 months, 3 weeks ago
On 25.02.25 13:56, Brendan Jackman wrote:
> On Tue, Feb 25, 2025 at 11:01:47AM +0100, David Hildenbrand wrote:
>>> This is an RFC and not a PATCH because:
>>>
>>> 1. I have not taken much care to ensure the isolation is complete.
>>>      There are probably sources of flakiness and nondeterminism in here.
>>>
>>> 2. I suspect the the basic idea might be over-complicated: do we really
>>>      need memory hotplug here? Do we even need the instance of the
>>>      allocator we're testing to actual memory behind the pages it's
>>>      allocating, or could we just hallucinate a new region of vmemmap
>>>      without any of that awkwardness?
>>>
>>>      One significant downside of relying on memory hotplug is that the
>>>      test won't run if we can't hotplug anything out. That means you have
>>>      to fiddle with the platform to even run the tests - see the
>>>      --kernel_args and --qemu_args I had to add to my kunit.py command
>>>      above.
>>>
>>>      So yeah, other suggestions welcome.
>>>
>>>      2b. I'm not very confident I'm using the hotplug API properly.
>>
>> Me neither ;)
>>
>> Dynamically adding memory to that "fake" node is certainly interesting, but
>> which guarantees do we have that some other features (page migration, memory
>> offlining, page reporting ...) don't interact in weird ways with this "fake"
>> node? Adding special-casing all over the place for that feels wrong. I
>> assume this is point 1. you note above.
> 
> Yeah, basically this is the big downside. Changing the system we're
> trying to test in order to make it testable can't be avoided entirely,
> but I am also pretty unhappy with sprinkling "if (node_isolated(node))"
> everywhere.
> 
> I guess the ideal approach is one where, instead of having to modify
> the meaning of node_data, we just support replacing it completely,
> e.g:
> 
> struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned int order,
> 		int preferred_nid, nodemask_t *nodemask,
> 		struct pagelist_data *node_data)
> {
> 	struct alloc_context ac = { .node_data = node_data };
> 
> 	// ...
> }
> 
> Ideally this could be done in such a way that it disappears completely
> outside of KUnit builds, the interface should be private and we'd
> wanna get rid of any unnecessary pointer chasing with stuff like:
> 
> #ifdef CONFIG_PAGE_ALLOC_KUNIT_TESTS
> static inline struct ac_node_data(struct alloc_context *ac, int node)
> {
> 	return ac->node_data[node];
> }
> #else
> #define ac_node_data(ac, node) (NODE_DATA(node))
> #endif
> 
> I initially rejected this approach because it felt "too intrusive",
> but now that I've actually written this RFC I think it could be less
> intrusive than the node_isolated() thing I've proposed here.
> 
> The most obvious challenges I can see there are:
> 
> - There might be places that page_alloc.c calls out to that care about
>    node_data but where we really don't want to plumb the alloc_context
>    through (maybe vmscan.c is already such a place)?
> 
> - I dunno how many more such helpers we'd need beyond ac_node_data(),
>    like do we need ac_nodes_possible_mask() etc etc etc?
> 
> But maybe worth a try - can you see any obvious reason this idea is
> stupid?
> 
>> So I don't quite love the idea on first sight ... but I haven't grasped all
>> details of the full picture yet I'm afraid.
> 
> Do you have any thoughts about "making up" memory instead of
> hot-unplugging real memory for test usage? That might simplify things
> a bit?
 > > It seems possible that very little mm code cares if the memory we're
> managing actually exists. (For ASI code we did briefly experiment with
> tracking information about free pages in the page itself, but it's
> pretty sketchy and the presence of debug_pagealloc makes me think
> nobody does it today).

At least when it comes to the buddy, only page zeroing+poisoning should 
access actual page content.

So making up memory might actually work in quite some setups, assuming 
that it will never get allocated.

The "complicated" thing is still that we are trying to test parts of the 
buddy in a well-controlled way while other kernel infrastructure is 
using the buddy in rather uncontrolled ways.


> 
> There might be arch-specific issues there, but for unit tests it
> seems OK if they don't work on every ISA.

Just pointing it out: for memblock tests (tools/testing/memblock/) we 
actually compile memblock.c to be used in a user space application, 
stubbing all external function calls etc such that we get the basics 
running.

It'd probably be quite some work to get page_alloc.c into a similar 
shape, likely we'd have to move a lot of unrelated-for-the tests stuff, 
and think about how to handle some nasty details like pcp etc. Just 
wondering, did you think about that option as well?

The nice thing about such an approach is that we can test the allcator 
without any possible side effects from the running system.

-- 
Cheers,

David / dhildenb
Re: [PATCH RFC 0/4] mm: KUnit tests for the page allocator
Posted by Brendan Jackman 9 months, 3 weeks ago
On Wed, 26 Feb 2025 at 12:52, David Hildenbrand <david@redhat.com> wrote:
>  > > It seems possible that very little mm code cares if the memory we're
> > managing actually exists. (For ASI code we did briefly experiment with
> > tracking information about free pages in the page itself, but it's
> > pretty sketchy and the presence of debug_pagealloc makes me think
> > nobody does it today).
>
> At least when it comes to the buddy, only page zeroing+poisoning should
> access actual page content.
>
> So making up memory might actually work in quite some setups, assuming
> that it will never get allocated.
>
> The "complicated" thing is still that we are trying to test parts of the
> buddy in a well-controlled way while other kernel infrastructure is
> using the buddy in rather uncontrolled ways.

Thanks, yeah that makes sense, and I agree that's the hard part. If we
can design a way to actually test the interface in an isolated way,
where we get the "memory" that we use to do that is kinda secondary
and can be changed later.

> > There might be arch-specific issues there, but for unit tests it
> > seems OK if they don't work on every ISA.
>
> Just pointing it out: for memblock tests (tools/testing/memblock/) we
> actually compile memblock.c to be used in a user space application,
> stubbing all external function calls etc such that we get the basics
> running.
>
> It'd probably be quite some work to get page_alloc.c into a similar
> shape, likely we'd have to move a lot of unrelated-for-the tests stuff,
> and think about how to handle some nasty details like pcp etc. Just
> wondering, did you think about that option as well?
>
> The nice thing about such an approach is that we can test the allcator
> without any possible side effects from the running system.

Yeah Lorenzo also pointed me to tools/testing/vma and I am pretty sold
that it's a better approach than KUnit where it's possible. But, I'm
doubtful about using it for page_alloc.

I think it could definitely be a good idea for the really core buddy
logic (like rmqueue_buddy() and below), where I'm sure we could stub
out stuff like percpu_* and locking and have the tests still be
meaningful. But I'm not sure that really low-level code is calling out
for more testing.

Whereas I suspect if you zoom out even just to the level of
__alloc_frozen_pages_noprof(), it starts to get a bit impractical
already. And that's where I really wanna get coverage.

Anyway, I'm thinking the next step here is to explore how to get away
from the node_isolated() stuff in this RFC, so I'll keep that idea in
mind and try to get a feel for whether it looks possible.