On Fri, Jul 17, 2026 at 03:00:33PM +0200, Thomas Hellström via Kernel-dev wrote:
> On Fri, 2026-07-17 at 09:04 -0300, Thadeu Lima de Souza Cascardo wrote:
> > Based on the work of Thomas Hellström to test dmem.max eviction, add
> > a
> > test for dmem.current usage after allocations and setting dmem.max.
> >
> > Create a dmem cgroup, allocate close to capacity (or at most 4GiB),
> > check current usage is within a small slack of the expected
> > allocation.
> > Then, set max to a small value and check allocations and current
> > usage
> > are limited to the max set. Set max to less than a single BO size,
> > then
> > check no allocations are allowed and current usage is also within the
> > slack. After each allocation, release memory and check current usage
> > has gone down.
> >
> > Signed-off-by: Thadeu Lima de Souza Cascardo <[email protected]>
>
> In what situations do you see the need for a slack? I've seen it only
> when max is set close to 0. For higher max limit, the slack usage may
> potentially mask bugs. Is there a way the slack can be activated only
> when it's needed?
>
> Thanks,
> Thomas
>
I found this was necessary when testing on Xe and I suspected it could be
related to page tables or some extra pages I noticed in one specific code
path.
Now that I looked more into the logs that I have, I noticed around 56KiB
extra bytes when allocating as many BOs as possible when there is no max.
And when releasing them all, 32KiB bytes were still accounted for.
Reading the code, I could not find where these exactly these come from. And
given I ended up changing the way we allocate memory on Xe due to the
allocation trigger on write_eviction_interruptible and
write_eviction_nonblock, this might not even be true.
Unfortunately, Rodrigo Siqueira, who helped me run those tests on Xe, is
not available for the next few weeks, so I will have to chase some other
discrete Xe GPUs around.
I will chase this down and at try to pinpoint exactly where these are
coming from. They might even point out to a bug.
This might take me a few days. If there is anything else that you want me
to fix in the meantime, let me know.
Thanks.
Cascardo.
>
>
> > ---
> > tests/cgroup_dmem.c | 251
> > +++++++++++++++++++++++++++++++++++++++++++-
> > 1 file changed, 246 insertions(+), 5 deletions(-)
> >
> > diff --git a/tests/cgroup_dmem.c b/tests/cgroup_dmem.c
> > index 442c965f9bbf..fa4694b45035 100644
> > --- a/tests/cgroup_dmem.c
> > +++ b/tests/cgroup_dmem.c
> > @@ -1,6 +1,8 @@
> > // SPDX-License-Identifier: MIT
> > /*
> > * Copyright © 2025 Intel Corporation
> > + * Copyright © 2026 Intel Corporation
> > + * Copyright 2026 Valve Corporation
> > */
> >
> > /**
> > @@ -17,10 +19,214 @@
> > * Test category: uapi
> > */
> >
> > +#include <errno.h>
> > #include <inttypes.h>
> > +#include <signal.h>
> > +#include <stdatomic.h>
> > +#include <stdint.h>
> > +#include <stdlib.h>
> > +#include <string.h>
> > +#include <unistd.h>
> >
> > +#include "drmtest.h"
> > #include "igt.h"
> > +#include "igt_aux.h"
> > #include "igt_cgroup.h"
> > +#include "igt_dmem_driver.h"
> > +
> > +#define BO_SIZE SZ_64M
> > +#define MAX_LIMIT ((uint64_t)4 * SZ_1G)
> > +#define USAGE_SLACK SZ_4M /* tolerance above
> > the set max */
> > +#define USAGE_POLL_MS 10
> > +#define USAGE_DROP_TIMEOUT_MS 1000
> > +
> > +/**
> > + * SUBTEST: simple
> > + * DESCRIPTION:
> > + * Creates a cgroup, moves the process into it, enumerates all dmem
> > regions,
> > + * prints their capacity, system-wide current usage, per-cgroup
> > current usage
> > + * and configured limits, then destroys the cgroup.
> > + */
> > +
> > +/**
> > + * SUBTEST: current
> > + * DESCRIPTION:
> > + * Create a dmem cgroup, allocate close to capacity (or at most
> > 4GiB),
> > + * check current usage is within a small slack of the expected
> > allocation.
> > + * Then, set max to a small value and check allocations and current
> > usage
> > + * are limited to the max set.
> > + * Set max to less than a single BO size, then check no allocations
> > are allowed
> > + * and current usage is also within the slack.
> > + * After each allocation, release memory and check current usage
> > has gone
> > + * down.
> > + * REQUIREMENTS: xe or amdgpu device with at least one VRAM region
> > + */
> > +
> > +static uint64_t wait_for_usage_drop(struct igt_cgroup *cg, const
> > char *region,
> > + uint64_t limit)
> > +{
> > + uint64_t current;
> > + unsigned int elapsed = 0;
> > +
> > + do {
> > + igt_cgroup_dmem_get_current(cg, region, ¤t);
> > + if (current <= limit)
> > + return current;
> > + usleep(USAGE_POLL_MS * 1000);
> > + elapsed += USAGE_POLL_MS;
> > + } while (elapsed < USAGE_DROP_TIMEOUT_MS);
> > +
> > + return current;
> > +}
> > +
> > +static int allocate_vram(const struct igt_dmem_driver *drv, void
> > *ctx, int fd, int max_bo, size_t len)
> > +{
> > + int n_bo, err = 0;
> > + for (n_bo = 0; n_bo < max_bo; n_bo++) {
> > + err = drv->allocate_vram(ctx, n_bo, len);
> > + if (err)
> > + break;
> > + }
> > + /* These are expected failures we can ignore. */
> > + if (err == -ENOMEM || err == -ENOSPC)
> > + err = 0;
> > + return err ?: n_bo;
> > +}
> > +
> > +static void free_vram(const struct igt_dmem_driver *drv, void *ctx,
> > int max_bo)
> > +{
> > + int i;
> > + for (i = 0; i < max_bo; i++)
> > + drv->free_vram(ctx, i);
> > +}
> > +
> > +static void test_current(int fd, char *cg_region, unsigned int
> > flags, const struct igt_dmem_driver *drv)
> > +{
> > + struct igt_cgroup *cg;
> > + void *ctx;
> > + uint64_t current, capacity, cg_max;
> > + int n_bo = 0, max_bo;
> > + int err;
> > +
> > + igt_cgroup_dmem_get_capacity(cg_region, &capacity);
> > + igt_require_f(capacity >= 4 * BO_SIZE,
> > + "VRAM capacity (%"PRIu64" MiB) too small to
> > test\n",
> > + capacity / SZ_1M);
> > +
> > + /*
> > + * Use up to 4 GiB, or the full capacity if the device has
> > less.
> > + * Leave one BO_SIZE worth of headroom so the device isn't
> > completely
> > + * exhausted before the cgroup limit is hit.
> > + */
> > + cg_max = min(MAX_LIMIT, capacity - BO_SIZE - USAGE_SLACK);
> > + cg_max = ALIGN_DOWN(cg_max, BO_SIZE);
> > +
> > + /* Create cgroup and move into it */
> > + cg = igt_cgroup_new("igt_cgroups_test");
> > + igt_cgroup_move_current(cg);
> > +
> > + max_bo = cg_max / BO_SIZE;
> > +
> > + err = drv->init(&ctx, fd, max_bo);
> > + igt_assert_f(!err, "Failed to initialize driver");
> > +
> > + n_bo = allocate_vram(drv, ctx, fd, max_bo, BO_SIZE);
> > + igt_assert_f(n_bo > 0, "failed to allocate VRAM\n");
> > +
> > + igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
> > + igt_debug("After fill: cgroup current = %"PRIu64" MiB, "
> > + "max = %"PRIu64" MiB\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > + igt_assert_f(current < cg_max + USAGE_SLACK && current >
> > cg_max - USAGE_SLACK,
> > + "current usage (%"PRIu64" MiB) is not within
> > margin of allocation (%"PRIu64" MiB)\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > +
> > + free_vram(drv, ctx, n_bo);
> > + wait_for_usage_drop(cg, cg_region, USAGE_SLACK);
> > +
> > + igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
> > + igt_debug("After free: cgroup current = %"PRIu64" MiB, "
> > + "max = %"PRIu64" MiB\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > + igt_assert_f(current < USAGE_SLACK,
> > + "current usage (%"PRIu64" MiB) is not within
> > margin (%d MiB)\n",
> > + current / SZ_1M, USAGE_SLACK / SZ_1M);
> > +
> > + /* Allow for a slack as there might be some extra pages
> > allocated. */
> > + igt_cgroup_dmem_set_max(cg, cg_region, 2 * BO_SIZE +
> > USAGE_SLACK, false);
> > +
> > + n_bo = allocate_vram(drv, ctx, fd, max_bo, BO_SIZE);
> > + igt_assert_f(n_bo > 0, "failed to allocate VRAM\n");
> > +
> > + igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
> > + igt_debug("After fill: cgroup current = %"PRIu64" MiB, "
> > + "max = %"PRIu64" MiB\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > + igt_assert_f(current < 2 * BO_SIZE + USAGE_SLACK && current
> > > 2 * BO_SIZE - USAGE_SLACK,
> > + "current usage (%"PRIu64" MiB) is not within
> > margin of allocation (%"PRIu64" MiB)\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > +
> > + free_vram(drv, ctx, n_bo);
> > + wait_for_usage_drop(cg, cg_region, USAGE_SLACK);
> > +
> > + igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
> > + igt_debug("After free: cgroup current = %"PRIu64" MiB, "
> > + "max = %"PRIu64" MiB\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > + igt_assert_f(current < USAGE_SLACK,
> > + "current usage (%"PRIu64" MiB) is not within
> > margin (%d MiB)\n",
> > + current / SZ_1M, USAGE_SLACK / SZ_1M);
> > +
> > + igt_cgroup_dmem_set_max(cg, cg_region, USAGE_SLACK, false);
> > +
> > + n_bo = allocate_vram(drv, ctx, fd, max_bo, BO_SIZE);
> > +
> > + /*
> > + * amdgpu may succeed the allocation, by falling back to
> > GTT, so no assertion here.
> > + * Verify by reading current usage.
> > + */
> > +
> > + igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
> > + igt_debug("After fill: cgroup current = %"PRIu64" MiB, "
> > + "max = %"PRIu64" MiB\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > + igt_assert_f(current < USAGE_SLACK,
> > + "current usage (%"PRIu64" MiB) is not within
> > margin\n",
> > + current / SZ_1M);
> > +
> > + if (n_bo > 0)
> > + free_vram(drv, ctx, n_bo);
> > + wait_for_usage_drop(cg, cg_region, USAGE_SLACK);
> > +
> > + igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
> > + igt_debug("After free: cgroup current = %"PRIu64" MiB, "
> > + "max = %"PRIu64" MiB\n",
> > + current / SZ_1M, cg_max / SZ_1M);
> > + igt_assert_f(current < USAGE_SLACK,
> > + "current usage (%"PRIu64" MiB) is not within
> > margin (%d MiB)\n",
> > + current / SZ_1M, USAGE_SLACK / SZ_1M);
> > +
> > + drv->deinit(ctx);
> > + igt_cgroup_free(cg);
> > +}
> > +
> > +static const struct {
> > + const char *name;
> > + void (*test_fn)(int fd, char *cg_region, unsigned int flags,
> > const struct igt_dmem_driver *drv);
> > + unsigned int flags;
> > +} subtests[] = {
> > + { "current", test_current, 0 },
> > + { }
> > +};
> > +
> > +static const struct {
> > + int driver_flag;
> > + const struct igt_dmem_driver *driver;
> > +} drivers[] = {
> > + { DRIVER_XE, &xe_dmem_driver },
> > + { DRIVER_AMDGPU, &amdgpu_dmem_driver },
> > + { },
> > +};
> >
> > IGT_TEST_DESCRIPTION("Exercises the cgroup v2 dmem controller
> > interface.");
> >
> > @@ -32,7 +238,7 @@ static void fmt_bytes(uint64_t v, char *buf,
> > size_t len)
> > snprintf(buf, len, "%" PRIu64, v);
> > }
> >
> > -int igt_simple_main()
> > +static void simple_cgroup(void)
> > {
> > struct igt_cgroup *cg;
> > const char *region;
> > @@ -42,10 +248,6 @@ int igt_simple_main()
> > char min_s[32], low_s[32], max_s[32];
> > int i;
> >
> > - igt_require_f(igt_cgroup_dmem_available(),
> > - "No dmem regions found; is cgroup v2 with the
> > "
> > - "dmem controller available?\n");
> > -
> > cg = igt_cgroup_new("igt-cgroup-dmem-test");
> > igt_assert_f(cg, "Failed to create cgroup\n");
> >
> > @@ -90,3 +292,42 @@ int igt_simple_main()
> > igt_cgroup_dmem_regions_free(regions);
> > igt_cgroup_free(cg);
> > }
> > +
> > +int igt_main()
> > +{
> > + igt_fixture() {
> > + igt_require_f(getuid() == 0, "Test requires
> > root\n");
> > + /* Check dmem cgroup controller is available before
> > doing anything else */
> > + igt_require_f(igt_cgroup_dmem_available(),
> > + "dmem cgroup controller not available
> > (no cgroup v2 or no registered regions)\n");
> > +
> > + }
> > +
> > + igt_subtest("simple")
> > + simple_cgroup();
> > +
> > + for (int d = 0; drivers[d].driver; d++) {
> > + igt_subtest_group() {
> > + int fd = -1;
> > + char *cg_region;
> > + igt_fixture() {
> > + fd =
> > drm_open_driver(drivers[d].driver_flag);
> > + igt_require_f(fd >= 0,
> > + "No %s device found,
> > skipping\n",
> > + drivers[d].driver->name);
> > + cg_region = drivers[d].driver-
> > >get_region_name(fd);
> > + igt_require_f(cg_region, "Region not
> > tracked by dmem cgroup controller\n");
> > + }
> > +
> > + for (int i = 0; subtests[i].name; i++)
> > + igt_subtest_f("%s-%s",
> > drivers[d].driver->name, subtests[i].name)
> > + subtests[i].test_fn(fd,
> > cg_region, subtests[i].flags, drivers[d].driver);
> > +
> > + igt_fixture() {
> > + if (fd >= 0)
> > + drm_close_driver(fd);
> > + free(cg_region);
> > + }
> > + }
> > + }
> > +}