* [PATCH i-g-t 0/6] Initial dmem cgroup support
@ 2026-04-28 6:54 Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers Thomas Hellström
` (7 more replies)
0 siblings, 8 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
This series introduces library support for some trivial cgroup v2
dmem manipulation.
It also adds a test for the library support itself and then an
xe test where cgroup functionality is exercised. In particular
lowering the max allocated amount of vram beyond what's already
allocated.
That functionality is not in the kernel yet, but a series for it
is pending.
Patch 1 adds library functionality.
Patch 2 adds a test for the library functionality. It's device
agnostic.
Patch 3 adds a function to get the xe driver cgroup controller
region name for an xe device VRAM region. Also library
functionality.
Patch 4 adds some vm_bind functionality from Sobin Thomas
Patch 5 adds the xe dmem cgroup test
Patch 6 adds an additional subtest testing the O_NONBLOCK functionality
v2:
- Use Sobin's patch for patch 4 rather than a local crafted one.
- Add the O_NONBLOCK subtest.
Sobin Thomas (1):
lib/xe: Add failable variant of xe_vm_bind_lr_sync()
Thomas Hellström (5):
lib/igt_cgroup: add cgroup v2 and dmem controller helpers
tests/cgroup_dmem: add dmem cgroup controller test
lib/xe: add xe_cgroup_region_name() helper
tests/xe_cgroups: add dmem cgroup eviction test
tests/xe_cgroups: add write_eviction_nonblock subtest
lib/igt.h | 1 +
lib/igt_cgroup.c | 638 +++++++++++++++++++++++++++++++++++++++
lib/igt_cgroup.h | 56 ++++
lib/meson.build | 1 +
lib/xe/xe_ioctl.c | 35 ++-
lib/xe/xe_ioctl.h | 2 +
lib/xe/xe_query.c | 32 ++
lib/xe/xe_query.h | 2 +
tests/cgroup_dmem.c | 92 ++++++
tests/intel/xe_cgroups.c | 307 +++++++++++++++++++
tests/meson.build | 2 +
11 files changed, 1158 insertions(+), 10 deletions(-)
create mode 100644 lib/igt_cgroup.c
create mode 100644 lib/igt_cgroup.h
create mode 100644 tests/cgroup_dmem.c
create mode 100644 tests/intel/xe_cgroups.c
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test Thomas Hellström
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add igt_cgroup, a library module providing helpers to create and manage
cgroup v2 sub-cgroups from IGT tests, with support for the dmem
controller that governs device memory (e.g. GPU VRAM) limits.
The API covers:
- igt_cgroup_new() / igt_cgroup_free(): create and destroy a named
sub-cgroup under the unified cgroupv2 hierarchy, enabling the dmem
controller automatically.
- igt_cgroup_move_current(): move the calling process into a cgroup.
- igt_cgroup_dmem_set/get_max/min/low(): write and read dmem.max,
dmem.min and dmem.low for a named device memory region.
- igt_cgroup_dmem_get_current(): read current per-cgroup device memory
usage.
- igt_cgroup_dmem_get_system_current(): read system-wide device memory
usage from the root cgroup.
- igt_cgroup_dmem_get_capacity(): read total region capacity from the
root cgroup's dmem.capacity file.
- igt_cgroup_dmem_regions() / igt_cgroup_dmem_regions_free(): enumerate
all registered device memory regions.
All public API functions that can fail use igt_assert internally rather
than returning error codes, following the IGT convention.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
lib/igt.h | 1 +
lib/igt_cgroup.c | 638 +++++++++++++++++++++++++++++++++++++++++++++++
lib/igt_cgroup.h | 56 +++++
lib/meson.build | 1 +
4 files changed, 696 insertions(+)
create mode 100644 lib/igt_cgroup.c
create mode 100644 lib/igt_cgroup.h
diff --git a/lib/igt.h b/lib/igt.h
index 173ca70bf..d8e5de7dc 100644
--- a/lib/igt.h
+++ b/lib/igt.h
@@ -27,6 +27,7 @@
#include "drmtest.h"
#include "i915_3d.h"
#include "igt_aux.h"
+#include "igt_cgroup.h"
#include "igt_configfs.h"
#include "igt_core.h"
#include "igt_debugfs.h"
diff --git a/lib/igt_cgroup.c b/lib/igt_cgroup.c
new file mode 100644
index 000000000..60586ccc4
--- /dev/null
+++ b/lib/igt_cgroup.c
@@ -0,0 +1,638 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+/**
+ * SECTION:igt_cgroup
+ * @short_description: cgroup v2 helpers for IGT tests
+ * @title: cgroup
+ * @include: igt_cgroup.h
+ *
+ * This library provides helpers for creating and managing cgroup v2
+ * sub-cgroups from IGT tests, including support for the dmem controller
+ * which governs device memory (e.g. GPU VRAM) limits.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "igt_cgroup.h"
+#include "igt_core.h"
+#include "igt_fs.h"
+
+#ifndef CGROUP2_SUPER_MAGIC
+#define CGROUP2_SUPER_MAGIC 0x63677270
+#endif
+
+/**
+ * struct igt_cgroup - Opaque handle to a cgroup v2 sub-cgroup.
+ * @dirfd: File descriptor for the cgroup directory.
+ * @path: Absolute path to the cgroup directory.
+ * @parent_path: Absolute path to the parent cgroup directory.
+ *
+ * Allocated by igt_cgroup_new() and freed by igt_cgroup_free().
+ */
+struct igt_cgroup {
+ int dirfd;
+ char *path;
+ char *parent_path;
+};
+
+static const char *cgroupv2_mount(void)
+{
+ static const char *path;
+ static const char * const candidates[] = {
+ "/sys/fs/cgroup",
+ "/sys/fs/cgroup/unified",
+ NULL,
+ };
+ struct statfs st;
+ int i;
+
+ if (path)
+ return path;
+
+ for (i = 0; candidates[i]; i++) {
+ if (statfs(candidates[i], &st) == 0 &&
+ (unsigned long)st.f_type == CGROUP2_SUPER_MAGIC) {
+ path = candidates[i];
+ return path;
+ }
+ }
+
+ return NULL;
+}
+
+/*
+ * Write "+controller" to @cgroup_path/cgroup.subtree_control to enable
+ * the named controller for children of that cgroup.
+ */
+static int enable_controller(const char *cgroup_path, const char *controller)
+{
+ char path[PATH_MAX];
+ char cmd[64];
+ ssize_t ret;
+ int fd;
+
+ snprintf(path, sizeof(path), "%s/cgroup.subtree_control", cgroup_path);
+ snprintf(cmd, sizeof(cmd), "+%s", controller);
+
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+
+ ret = write(fd, cmd, strlen(cmd));
+ close(fd);
+
+ return (ret < 0) ? -errno : 0;
+}
+
+/*
+ * Move every PID listed in @cgroup_path/cgroup.procs to
+ * @parent_path/cgroup.procs. Silently ignores individual failures
+ * (a PID may have exited between reading and writing).
+ */
+static void drain_procs_to_parent(const char *cgroup_path,
+ const char *parent_path)
+{
+ char proc_path[PATH_MAX];
+ char parent_procs[PATH_MAX];
+ int parent_fd;
+ FILE *f;
+ int pid;
+
+ snprintf(proc_path, sizeof(proc_path), "%s/cgroup.procs", cgroup_path);
+ snprintf(parent_procs, sizeof(parent_procs), "%s/cgroup.procs", parent_path);
+
+ parent_fd = open(parent_procs, O_WRONLY);
+ if (parent_fd < 0)
+ return;
+
+ f = fopen(proc_path, "r");
+ if (f) {
+ while (fscanf(f, "%d", &pid) == 1) {
+ char pidbuf[32];
+ ssize_t len = snprintf(pidbuf, sizeof(pidbuf), "%d", pid);
+
+ write(parent_fd, pidbuf, len);
+ }
+ fclose(f);
+ }
+
+ close(parent_fd);
+}
+
+/**
+ * igt_cgroup_new() - Create a new cgroup v2 sub-cgroup.
+ * @name: Name for the new cgroup directory.
+ *
+ * Creates a sub-cgroup named @name under the system's unified cgroupv2
+ * hierarchy. The dmem controller is enabled in the parent's
+ * subtree_control so that igt_cgroup_dmem_set_max() and friends take effect
+ * immediately.
+ *
+ * Return: Pointer to an &struct igt_cgroup on success, %NULL on failure.
+ */
+struct igt_cgroup *igt_cgroup_new(const char *name)
+{
+ struct igt_cgroup *cg;
+ const char *mount;
+ int ret;
+
+ mount = cgroupv2_mount();
+ if (!mount) {
+ igt_debug("cgroup v2 not found\n");
+ return NULL;
+ }
+
+ cg = calloc(1, sizeof(*cg));
+ if (!cg)
+ return NULL;
+
+ cg->parent_path = strdup(mount);
+ if (!cg->parent_path)
+ goto err_free;
+
+ if (asprintf(&cg->path, "%s/%s", mount, name) < 0) {
+ cg->path = NULL;
+ goto err_parent;
+ }
+
+ /*
+ * Try to enable the dmem controller in the parent's subtree_control.
+ * Ignore EINVAL which the kernel returns when the controller is already
+ * listed (i.e. already enabled).
+ */
+ ret = enable_controller(mount, "dmem");
+ if (ret < 0 && ret != -EINVAL)
+ igt_debug("Failed to enable dmem controller in %s: %d\n",
+ mount, ret);
+
+ if (mkdir(cg->path, 0755) < 0 && errno != EEXIST) {
+ igt_debug("Failed to create cgroup %s: %m\n", cg->path);
+ goto err_path;
+ }
+
+ cg->dirfd = open(cg->path, O_RDONLY | O_DIRECTORY);
+ if (cg->dirfd < 0) {
+ igt_debug("Failed to open cgroup dir %s: %m\n", cg->path);
+ goto err_rmdir;
+ }
+
+ return cg;
+
+err_rmdir:
+ rmdir(cg->path);
+err_path:
+ free(cg->path);
+err_parent:
+ free(cg->parent_path);
+err_free:
+ free(cg);
+ return NULL;
+}
+
+/**
+ * igt_cgroup_free() - Destroy a cgroup and release its resources.
+ * @cg: The cgroup to destroy.
+ *
+ * Moves any processes still running inside @cg back to the parent cgroup,
+ * removes the cgroup directory, and frees all associated memory.
+ * After this call @cg must not be used.
+ */
+void igt_cgroup_free(struct igt_cgroup *cg)
+{
+ if (!cg)
+ return;
+
+ drain_procs_to_parent(cg->path, cg->parent_path);
+
+ close(cg->dirfd);
+
+ if (rmdir(cg->path) < 0)
+ igt_debug("Failed to remove cgroup %s: %m\n", cg->path);
+
+ free(cg->path);
+ free(cg->parent_path);
+ free(cg);
+}
+
+/**
+ * igt_cgroup_move_current() - Move the calling process into a cgroup.
+ * @cg: Target cgroup.
+ *
+ * Writes the calling process's PID to @cg's cgroup.procs file, transferring
+ * it into the cgroup. All threads of the process move together.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_move_current(struct igt_cgroup *cg)
+{
+ char pidbuf[32];
+ ssize_t len;
+ int fd, ret;
+
+ len = snprintf(pidbuf, sizeof(pidbuf), "%d", (int)getpid());
+
+ fd = openat(cg->dirfd, "cgroup.procs", O_WRONLY);
+ igt_assert_f(fd >= 0, "Failed to open cgroup.procs: %m\n");
+
+ ret = write(fd, pidbuf, len);
+ close(fd);
+
+ igt_assert_f(ret == len, "Failed to write PID to cgroup.procs: %m\n");
+}
+
+/*
+ * Parse a single dmem interface file line of the form "region_name value\n"
+ * where value is either a decimal byte count or the string "max".
+ * Returns 0 and writes to *out on success, -EINVAL on parse error.
+ */
+static int dmem_parse_line(char *line, const char *region, uint64_t *out)
+{
+ char *space = strchr(line, ' ');
+
+ if (!space)
+ return -EINVAL;
+
+ *space = '\0';
+ if (strcmp(line, region) != 0)
+ return -ENOENT;
+
+ if (strcmp(space + 1, "max") == 0) {
+ *out = IGT_CGROUP_DMEM_MAX;
+ return 0;
+ }
+
+ errno = 0;
+ *out = strtoull(space + 1, &space, 10);
+ if (errno || *space != '\0')
+ return -EINVAL;
+
+ return 0;
+}
+
+/*
+ * Read a dmem interface file opened relative to @dirfd, searching for
+ * @region. On success writes the region's value to @out and returns 0.
+ * Returns -ENOENT when @region is absent, or a negative errno otherwise.
+ */
+static int dmem_read_region(int dirfd, const char *file,
+ const char *region, uint64_t *out)
+{
+ char buf[4096];
+ char *line, *saveptr;
+ ssize_t n;
+ int fd;
+
+ fd = openat(dirfd, file, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+
+ n = igt_readn(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return (int)n;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ int ret = dmem_parse_line(line, region, out);
+
+ if (ret != -ENOENT)
+ return ret;
+ }
+
+ return -ENOENT;
+}
+
+/*
+ * Write "region_name value" (or "region_name max") to the dmem interface
+ * file @file opened relative to @dirfd.
+ * If @nonblock is true the file is opened with O_NONBLOCK, causing any
+ * eviction triggered by the limit change to be skipped rather than waited
+ * for; the write still succeeds (returns 0).
+ * Returns 0 on success, negative errno on failure.
+ */
+static int dmem_write_region(int dirfd, const char *file,
+ const char *region, uint64_t bytes, bool nonblock)
+{
+ char buf[PATH_MAX + 64];
+ ssize_t len;
+ int fd, ret;
+ int flags = O_WRONLY;
+
+ if (bytes == IGT_CGROUP_DMEM_MAX)
+ len = snprintf(buf, sizeof(buf), "%s max", region);
+ else
+ len = snprintf(buf, sizeof(buf), "%s %" PRIu64, region, bytes);
+
+ if (nonblock)
+ flags |= O_NONBLOCK;
+
+ fd = openat(dirfd, file, flags);
+ if (fd < 0)
+ return -errno;
+
+ do {
+ ret = write(fd, buf, len);
+ if (ret < 0 && errno == EINTR)
+ igt_debug("dmem cgroup write interrupted by signal, retrying\n");
+ } while (ret < 0 && errno == EINTR);
+ close(fd);
+
+ return (ret < 0) ? -errno : 0;
+}
+
+/**
+ * igt_cgroup_dmem_set_max() - Set the hard device memory limit for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name (e.g. "drm/0000:03:00.0/vram0").
+ * @bytes: Hard limit in bytes. Use %IGT_CGROUP_DMEM_MAX for no limit.
+ * @nonblock: If true, open the file with O_NONBLOCK so that eviction
+ * triggered by the limit change is skipped rather than awaited.
+ *
+ * Writes @bytes to dmem.max for @region inside @cg. Allocation attempts
+ * that would push usage past this limit fail with -EAGAIN in the kernel.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_set_max(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes, bool nonblock)
+{
+ igt_assert_f(dmem_write_region(cg->dirfd, "dmem.max", region, bytes,
+ nonblock) == 0,
+ "Failed to set dmem.max for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_set_min() - Set the hard protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @bytes: Hard protection threshold in bytes. Pass 0 to disable.
+ *
+ * Writes @bytes to dmem.min for @region inside @cg. Device memory below
+ * this threshold is never reclaimed regardless of system pressure.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_set_min(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes)
+{
+ igt_assert_f(dmem_write_region(cg->dirfd, "dmem.min", region, bytes,
+ false) == 0,
+ "Failed to set dmem.min for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_set_low() - Set the soft protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @bytes: Soft protection threshold in bytes. Pass 0 to disable.
+ *
+ * Writes @bytes to dmem.low for @region inside @cg. Device memory below
+ * this threshold is only reclaimed when no unprotected memory remains.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_set_low(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes)
+{
+ igt_assert_f(dmem_write_region(cg->dirfd, "dmem.low", region, bytes,
+ false) == 0,
+ "Failed to set dmem.low for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_current() - Read current device memory usage for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the current usage in bytes.
+ *
+ * Reads dmem.current from @cg and returns the usage for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_current(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.current", region, out) == 0,
+ "Failed to read dmem.current for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_max() - Read the configured hard limit for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the limit in bytes, or %IGT_CGROUP_DMEM_MAX if unset.
+ *
+ * Reads dmem.max from @cg for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_max(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.max", region, out) == 0,
+ "Failed to read dmem.max for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_min() - Read the configured hard protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the threshold in bytes.
+ *
+ * Reads dmem.min from @cg for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_min(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.min", region, out) == 0,
+ "Failed to read dmem.min for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_low() - Read the configured soft protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the threshold in bytes.
+ *
+ * Reads dmem.low from @cg for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_low(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.low", region, out) == 0,
+ "Failed to read dmem.low for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_available() - Check if the dmem cgroup controller is available.
+ *
+ * Probes the cgroup v2 hierarchy for the presence of a dmem.capacity file at
+ * the root, indicating that the kernel dmem controller is compiled in and at
+ * least one device memory region has been registered.
+ *
+ * Return: %true if the dmem controller is available, %false otherwise.
+ */
+bool igt_cgroup_dmem_available(void)
+{
+ char **regions = igt_cgroup_dmem_regions();
+
+ if (!regions)
+ return false;
+
+ igt_cgroup_dmem_regions_free(regions);
+ return true;
+}
+
+/**
+ * igt_cgroup_dmem_regions() - Enumerate all registered device memory regions.
+ *
+ * Reads the root cgroup's dmem.capacity file and returns a NULL-terminated
+ * array of region name strings. Each name can be passed directly to
+ * igt_cgroup_dmem_get_capacity(), igt_cgroup_dmem_get_current(), and the
+ * igt_cgroup_dmem_set_*() / igt_cgroup_dmem_get_*() family.
+ *
+ * Free the returned array with igt_cgroup_dmem_regions_free().
+ *
+ * Return: A NULL-terminated array of strings on success, %NULL if cgroupv2
+ * is unavailable or no regions are registered.
+ */
+char **igt_cgroup_dmem_regions(void)
+{
+ char buf[4096];
+ char *line, *saveptr, *space, *name;
+ char **regions = NULL, **tmp;
+ int count = 0;
+ const char *mount;
+ ssize_t n;
+ int dirfd, fd;
+
+ mount = cgroupv2_mount();
+ if (!mount)
+ return NULL;
+
+ dirfd = open(mount, O_RDONLY | O_DIRECTORY);
+ if (dirfd < 0)
+ return NULL;
+
+ fd = openat(dirfd, "dmem.capacity", O_RDONLY);
+ close(dirfd);
+ if (fd < 0)
+ return NULL;
+
+ n = igt_readn(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0)
+ return NULL;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ space = strchr(line, ' ');
+
+ if (!space)
+ continue;
+ *space = '\0';
+
+ name = strdup(line);
+ if (!name)
+ goto err;
+
+ tmp = realloc(regions, (count + 2) * sizeof(*regions));
+ if (!tmp) {
+ free(name);
+ goto err;
+ }
+ regions = tmp;
+ regions[count++] = name;
+ regions[count] = NULL;
+ }
+
+ return regions;
+
+err:
+ igt_cgroup_dmem_regions_free(regions);
+ return NULL;
+}
+
+/**
+ * igt_cgroup_dmem_regions_free() - Free a region list returned by igt_cgroup_dmem_regions().
+ * @regions: NULL-terminated array returned by igt_cgroup_dmem_regions().
+ *
+ * Frees each string in @regions and the array itself. Safe to call with
+ * %NULL.
+ */
+void igt_cgroup_dmem_regions_free(char **regions)
+{
+ int i;
+
+ if (!regions)
+ return;
+
+ for (i = 0; regions[i]; i++)
+ free(regions[i]);
+
+ free(regions);
+}
+
+/**
+ * igt_cgroup_dmem_get_capacity() - Read total device memory capacity for a region.
+ * @region: Device memory region name.
+ * @out: Receives the total capacity in bytes.
+ *
+ * Reads dmem.capacity from the root cgroup and returns the capacity for
+ * @region. This reflects the maximum allocatable bytes, excluding memory
+ * reserved by the kernel for internal use.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_capacity(const char *region, uint64_t *out)
+{
+ const char *mount;
+ int dirfd, ret;
+
+ mount = cgroupv2_mount();
+ igt_assert_f(mount, "cgroup v2 not available\n");
+
+ dirfd = open(mount, O_RDONLY | O_DIRECTORY);
+ igt_assert_f(dirfd >= 0, "Failed to open cgroup root: %m\n");
+
+ ret = dmem_read_region(dirfd, "dmem.capacity", region, out);
+ close(dirfd);
+
+ igt_assert_f(ret == 0, "Failed to read dmem.capacity for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_system_current() - Read system-wide device memory usage for a region.
+ * @region: Device memory region name.
+ * @out: Receives the total system-wide usage in bytes.
+ *
+ * Reads dmem.current from the root cgroup for @region. This reflects the
+ * aggregate device memory usage across all cgroups on the system.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_system_current(const char *region, uint64_t *out)
+{
+ const char *mount;
+ int dirfd, ret;
+
+ mount = cgroupv2_mount();
+ igt_assert_f(mount, "cgroup v2 not available\n");
+
+ dirfd = open(mount, O_RDONLY | O_DIRECTORY);
+ igt_assert_f(dirfd >= 0, "Failed to open cgroup root: %m\n");
+
+ ret = dmem_read_region(dirfd, "dmem.current", region, out);
+ close(dirfd);
+
+ igt_assert_f(ret == 0, "Failed to read root dmem.current for region %s\n", region);
+}
diff --git a/lib/igt_cgroup.h b/lib/igt_cgroup.h
new file mode 100644
index 000000000..379de457a
--- /dev/null
+++ b/lib/igt_cgroup.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+#ifndef __IGT_CGROUP_H__
+#define __IGT_CGROUP_H__
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * IGT_CGROUP_DMEM_MAX - Sentinel value meaning "no device memory limit".
+ *
+ * Pass this to igt_cgroup_dmem_set_max() to remove a previously set limit,
+ * equivalent to writing "max" to the dmem.max interface file.
+ */
+#define IGT_CGROUP_DMEM_MAX UINT64_MAX
+
+/**
+ * struct igt_cgroup - Opaque handle to a cgroup v2 sub-cgroup.
+ *
+ * Allocated by igt_cgroup_new() and freed by igt_cgroup_free().
+ * All other functions in this module take a pointer to this type.
+ */
+struct igt_cgroup;
+
+struct igt_cgroup *igt_cgroup_new(const char *name);
+void igt_cgroup_free(struct igt_cgroup *cg);
+
+void igt_cgroup_move_current(struct igt_cgroup *cg);
+
+void igt_cgroup_dmem_set_max(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes, bool nonblock);
+void igt_cgroup_dmem_set_min(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes);
+void igt_cgroup_dmem_set_low(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes);
+
+void igt_cgroup_dmem_get_max(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+void igt_cgroup_dmem_get_min(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+void igt_cgroup_dmem_get_low(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+
+void igt_cgroup_dmem_get_current(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+void igt_cgroup_dmem_get_capacity(const char *region, uint64_t *out);
+void igt_cgroup_dmem_get_system_current(const char *region, uint64_t *out);
+
+bool igt_cgroup_dmem_available(void);
+char **igt_cgroup_dmem_regions(void);
+void igt_cgroup_dmem_regions_free(char **regions);
+
+#endif /* __IGT_CGROUP_H__ */
diff --git a/lib/meson.build b/lib/meson.build
index 0e7efadf3..fb4679ffd 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -18,6 +18,7 @@ lib_sources = [
'i915/i915_crc.c',
'igt_collection.c',
'igt_color_encoding.c',
+ 'igt_cgroup.c',
'igt_configfs.c',
'igt_facts.c',
'igt_crc.c',
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper Thomas Hellström
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add a test that exercises the cgroup v2 dmem controller interface using
the new igt_cgroup library.
The test uses igt_simple_main and:
- Skips if no dmem regions are registered (no cgroup v2 or no
dmem-capable device).
- Creates a sub-cgroup and moves the test process into it.
- Enumerates all registered device memory regions and prints their
capacity, system-wide current usage, per-cgroup current usage, and
configured min, low and max limits.
- Destroys the cgroup on completion.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/cgroup_dmem.c | 92 +++++++++++++++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 93 insertions(+)
create mode 100644 tests/cgroup_dmem.c
diff --git a/tests/cgroup_dmem.c b/tests/cgroup_dmem.c
new file mode 100644
index 000000000..442c965f9
--- /dev/null
+++ b/tests/cgroup_dmem.c
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+/**
+ * TEST: cgroup dmem
+ * Description: Exercises the cgroup v2 dmem controller interface. 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.
+ * Category: Core
+ * Mega feature: General Core features
+ * Sub-category: uapi
+ * Functionality: cgroup
+ * Feature: dmem
+ * Test category: uapi
+ */
+
+#include <inttypes.h>
+
+#include "igt.h"
+#include "igt_cgroup.h"
+
+IGT_TEST_DESCRIPTION("Exercises the cgroup v2 dmem controller interface.");
+
+static void fmt_bytes(uint64_t v, char *buf, size_t len)
+{
+ if (v == IGT_CGROUP_DMEM_MAX)
+ snprintf(buf, len, "max");
+ else
+ snprintf(buf, len, "%" PRIu64, v);
+}
+
+int igt_simple_main()
+{
+ struct igt_cgroup *cg;
+ const char *region;
+ char **regions;
+ uint64_t capacity, sys_current, cg_current, min, low, max;
+ char cap_s[32], sys_s[32], cg_s[32];
+ 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");
+
+ igt_cgroup_move_current(cg);
+
+ regions = igt_cgroup_dmem_regions();
+ igt_assert_f(regions, "Failed to enumerate dmem regions\n");
+
+ igt_info("%-40s %16s %16s %16s %16s %16s %16s\n",
+ "region", "capacity", "system-current",
+ "cgroup-current", "min", "low", "max");
+ igt_info("%-40s %16s %16s %16s %16s %16s %16s\n",
+ "------", "--------", "--------------",
+ "--------------", "---", "---", "---");
+
+ for (i = 0; regions[i]; i++) {
+ region = regions[i];
+
+ igt_cgroup_dmem_get_capacity(region, &capacity);
+ fmt_bytes(capacity, cap_s, sizeof(cap_s));
+
+ igt_cgroup_dmem_get_system_current(region, &sys_current);
+ fmt_bytes(sys_current, sys_s, sizeof(sys_s));
+
+ igt_cgroup_dmem_get_current(cg, region, &cg_current);
+ fmt_bytes(cg_current, cg_s, sizeof(cg_s));
+
+ igt_cgroup_dmem_get_min(cg, region, &min);
+ fmt_bytes(min, min_s, sizeof(min_s));
+
+ igt_cgroup_dmem_get_low(cg, region, &low);
+ fmt_bytes(low, low_s, sizeof(low_s));
+
+ igt_cgroup_dmem_get_max(cg, region, &max);
+ fmt_bytes(max, max_s, sizeof(max_s));
+
+ igt_info("%-40s %16s %16s %16s %16s %16s %16s\n",
+ region, cap_s, sys_s, cg_s,
+ min_s, low_s, max_s);
+ }
+
+ igt_cgroup_dmem_regions_free(regions);
+ igt_cgroup_free(cg);
+}
diff --git a/tests/meson.build b/tests/meson.build
index 60cea3aa8..02fbb8c4e 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,4 +1,5 @@
test_progs = [
+ 'cgroup_dmem',
'core_auth',
'core_debugfs',
'core_getclient',
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync() Thomas Hellström
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add xe_cgroup_region_name(fd, region) which constructs the dmem cgroup
region path for a given xe memory region. The returned string has the
form "drm/<pci-slot>/<region>" (e.g. "drm/0000:03:00.0/vram0"),
matching the name registered by the kernel via drmm_cgroup_register_region().
Only VRAM regions are tracked by the dmem controller; system and stolen
memory regions return NULL.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
lib/xe/xe_query.c | 32 ++++++++++++++++++++++++++++++++
lib/xe/xe_query.h | 2 ++
2 files changed, 34 insertions(+)
diff --git a/lib/xe/xe_query.c b/lib/xe/xe_query.c
index 3286a3b37..ad8b16f74 100644
--- a/lib/xe/xe_query.c
+++ b/lib/xe/xe_query.c
@@ -7,6 +7,7 @@
*/
#include <fcntl.h>
+#include <limits.h>
#include <stdlib.h>
#include <pthread.h>
@@ -21,6 +22,7 @@
#include "drmtest.h"
#include "igt_debugfs.h"
+#include "igt_device.h"
#include "ioctl_wrappers.h"
#include "igt_map.h"
#include "intel_pat.h"
@@ -1270,6 +1272,36 @@ int xe_query_eu_thread_count(int fd, int gt)
xe_hwconfig_lookup_value_u32(fd, INTEL_HWCONFIG_NUM_THREADS_PER_EU);
}
+/**
+ * xe_cgroup_region_name() - Build the dmem cgroup region name for an xe memory region.
+ * @fd: xe device fd.
+ * @region: Region mask (as used by xe_mem_region(), xe_region_name(), etc.).
+ *
+ * Constructs the full dmem cgroup region path for @region on the device
+ * identified by @fd. The returned string has the form
+ * ``drm/<pci-slot>/<region>`` (e.g. ``drm/0000:03:00.0/vram0``), matching
+ * the name registered by the kernel driver via drmm_cgroup_register_region().
+ *
+ * Only VRAM regions are registered with the dmem controller; passing a
+ * system-memory region returns %NULL.
+ *
+ * Return: A newly allocated string that the caller must free(), or %NULL if
+ * @region is not tracked by the dmem cgroup controller.
+ */
+char *xe_cgroup_region_name(int fd, uint64_t region)
+{
+ char pci_slot[NAME_MAX];
+ char *name;
+
+ if (xe_region_class(fd, region) != DRM_XE_MEM_REGION_CLASS_VRAM)
+ return NULL;
+
+ igt_device_get_pci_slot_name(fd, pci_slot);
+
+ igt_assert(asprintf(&name, "drm/%s/%s", pci_slot, xe_region_name(region)) > 0);
+ return name;
+}
+
igt_constructor
{
xe_device_cache_init();
diff --git a/lib/xe/xe_query.h b/lib/xe/xe_query.h
index 8815c6c66..b01a5dac1 100644
--- a/lib/xe/xe_query.h
+++ b/lib/xe/xe_query.h
@@ -204,4 +204,6 @@ void xe_device_put(int fd);
int xe_query_eu_count(int fd, int gt);
int xe_query_eu_thread_count(int fd, int gt);
+char *xe_cgroup_region_name(int fd, uint64_t region);
+
#endif /* XE_QUERY_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync()
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (2 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test Thomas Hellström
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Sobin Thomas, Thomas Hellström
From: Sobin Thomas <sobin.thomas@intel.com>
Add __xe_vm_bind_lr_sync helper function which returns standard error
codes instead of asserting on failure. This allows calling function
to handle VM bind failures explicitly while preserving the existing
xe_vm_bind_lr_sync() wrapper for tests. This enables callers that
expect bind / overcommit failures.
v7: Introduced xe_vm_bind_lr_sync_failable (Thomas)
v8: Modified xe_vm_bind_lr_sync_failable and xe_vm_bind_lr_sync to call
__xe_vm_bind_lr_sync
v9: Removed redundant typecast and removed xe_vm_bind_lr_sync_failable
Signed-off-by: Sobin Thomas <sobin.thomas@intel.com>
Reviewed-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
lib/xe/xe_ioctl.c | 35 +++++++++++++++++++++++++----------
lib/xe/xe_ioctl.h | 2 ++
2 files changed, 27 insertions(+), 10 deletions(-)
diff --git a/lib/xe/xe_ioctl.c b/lib/xe/xe_ioctl.c
index 1dae56444..e13195e16 100644
--- a/lib/xe/xe_ioctl.c
+++ b/lib/xe/xe_ioctl.c
@@ -860,23 +860,38 @@ uint32_t xe_vm_madvise_purgeable(int fd, uint32_t vm_id, uint64_t start,
}
#define BIND_SYNC_VAL 0x686868
-void xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
- uint64_t addr, uint64_t size, uint32_t flags)
+int __xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
+ uint64_t addr, uint64_t size, uint32_t flags)
{
- volatile uint64_t *sync_addr = malloc(sizeof(*sync_addr));
+ uint64_t *sync_addr = malloc(sizeof(*sync_addr));
struct drm_xe_sync sync = {
.flags = DRM_XE_SYNC_FLAG_SIGNAL,
.type = DRM_XE_SYNC_TYPE_USER_FENCE,
- .addr = to_user_pointer((uint64_t *)sync_addr),
+ .addr = to_user_pointer(sync_addr),
.timeline_value = BIND_SYNC_VAL,
};
-
- igt_assert(!!sync_addr);
- xe_vm_bind_async_flags(fd, vm, 0, bo, 0, addr, size, &sync, 1, flags);
- if (*sync_addr != BIND_SYNC_VAL)
- xe_wait_ufence(fd, (uint64_t *)sync_addr, BIND_SYNC_VAL, 0, NSEC_PER_SEC * 10);
+ int ret = 0;
+
+ if (!sync_addr)
+ return -ENOMEM;
+ WRITE_ONCE(*sync_addr, 0);
+ ret = __xe_vm_bind(fd, vm, 0, bo, offset, addr, size, DRM_XE_VM_BIND_OP_MAP, flags,
+ &sync, 1, 0, DEFAULT_PAT_INDEX, 0);
+ if (ret)
+ goto out;
+
+ if (READ_ONCE(*sync_addr) != BIND_SYNC_VAL)
+ xe_wait_ufence(fd, sync_addr, BIND_SYNC_VAL, 0, NSEC_PER_SEC * 10);
/* Only free if the wait succeeds */
- free((void *)sync_addr);
+out:
+ free(sync_addr);
+ return ret;
+}
+
+void xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
+ uint64_t addr, uint64_t size, uint32_t flags)
+{
+ igt_assert_eq(__xe_vm_bind_lr_sync(fd, vm, bo, offset, addr, size, flags), 0);
}
void xe_vm_unbind_lr_sync(int fd, uint32_t vm, uint64_t offset,
diff --git a/lib/xe/xe_ioctl.h b/lib/xe/xe_ioctl.h
index ceb380685..768f77246 100644
--- a/lib/xe/xe_ioctl.h
+++ b/lib/xe/xe_ioctl.h
@@ -120,6 +120,8 @@ struct drm_xe_mem_range_attr
void xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo,
uint64_t offset, uint64_t addr,
uint64_t size, uint32_t flags);
+int __xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
+ uint64_t addr, uint64_t size, uint32_t flags);
void xe_vm_unbind_lr_sync(int fd, uint32_t vm, uint64_t offset,
uint64_t addr, uint64_t size);
#endif /* XE_IOCTL_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (3 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync() Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest Thomas Hellström
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add xe_cgroups, a test exercising the dmem cgroup controller on xe
devices.
The write_eviction subtest:
- Skips if the dmem cgroup controller is not available.
- Skips if no VRAM region is registered with the dmem controller.
- Creates a sub-cgroup and moves the test process into it.
- Sets a dmem.max limit on the first VRAM region (up to 4 GiB, or
the full capacity if smaller).
- Creates an LR VM and fills VRAM by repeatedly creating BOs with
DRM_XE_GEM_CREATE_FLAG_DEFER_BACKING and binding them via
__xe_vm_bind_lr_sync() until -ENOMEM or -ENOSPC is returned.
- Verifies that cgroup current usage is within the expected range when
the limit is hit.
- Lowers dmem.max in 256 MiB steps, waiting for usage to follow each
reduction.
The write_eviction_interruptible subtest runs the same test with
SIGCONT signals injected via igt_fork_signal_helper() and reports the
number of signals received. When a signal interrupts kernel-side
eviction, a small BO allocation is used to re-trigger it.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/intel/xe_cgroups.c | 276 +++++++++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 277 insertions(+)
create mode 100644 tests/intel/xe_cgroups.c
diff --git a/tests/intel/xe_cgroups.c b/tests/intel/xe_cgroups.c
new file mode 100644
index 000000000..8b0f4381f
--- /dev/null
+++ b/tests/intel/xe_cgroups.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * TEST: xe_cgroups
+ * DESCRIPTION: Tests exercising the dmem cgroup controller on xe devices.
+ * Category: Core
+ * Mega feature: General Core features
+ * Sub-category: cgroup
+ * FUNCTIONALITY: cgroup dmem controller
+ * SUBSETS: xe
+ */
+
+#include <errno.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 "xe_drm.h"
+#include "xe/xe_ioctl.h"
+#include "xe/xe_query.h"
+
+#define BO_SIZE SZ_128M
+#define MAX_LIMIT ((uint64_t)4 * SZ_1G)
+#define EVICT_STEP SZ_256M
+#define BIND_BASE 0x100000000ULL /* 4 GiB VA base */
+
+#define TEST_INTERRUPTIBLE (1 << 0)
+
+/**
+ * SUBTEST: write_eviction
+ * DESCRIPTION:
+ * Create a dmem cgroup, move the current process into it and set the max
+ * device memory limit for the first VRAM region to 4 GiB. Then fill VRAM
+ * by creating BOs with %DRM_XE_GEM_CREATE_FLAG_DEFER_BACKING (so that the
+ * physical allocation is deferred until VM_BIND) and binding them into an
+ * LR VM until the cgroup limit is hit. Verify that the reported cgroup
+ * current usage is within the expected range when the error occurs.
+ * Finally lower the max limit in 256 MiB steps and verify that the cgroup
+ * usage follows.
+ * REQUIREMENTS: must run as root; xe device with at least one VRAM region
+ */
+
+/**
+ * SUBTEST: write_eviction_interruptible
+ * DESCRIPTION:
+ * Same as write_eviction but with SIGCONT signals injected throughout via
+ * igt_fork_signal_helper() to verify that the dmem.max write path handles
+ * signal interruption correctly. A signal handler counts received signals
+ * and the count is reported as debug output at the end of the test.
+ * A signal interrupts the set-time eviction, and further eviction can be
+ * triggered by an explicit allocation.
+ * REQUIREMENTS: must run as root; xe device with at least one VRAM region
+ */
+
+static atomic_int signal_count;
+static struct sigaction sigcont_oldact;
+
+static void sigcont_handler(int sig)
+{
+ atomic_fetch_add(&signal_count, 1);
+
+ /* Chain to the previous handler (IGT's dummy sig_handler) */
+ if (sigcont_oldact.sa_handler &&
+ sigcont_oldact.sa_handler != SIG_IGN &&
+ sigcont_oldact.sa_handler != SIG_DFL)
+ sigcont_oldact.sa_handler(sig);
+}
+
+static void install_sigcont_counter(void)
+{
+ struct sigaction sa;
+
+ atomic_store(&signal_count, 0);
+ igt_fork_signal_helper();
+ /*
+ * Install the counter after igt_fork_signal_helper() so our handler
+ * is not overwritten. Save the old handler so we can chain to it.
+ */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sigcont_handler;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGCONT, &sa, &sigcont_oldact);
+}
+
+static int fill_vram(int fd, uint32_t vm, uint64_t vram_region,
+ uint32_t *handles, int max_bo)
+{
+ uint32_t handle;
+ uint64_t addr = BIND_BASE;
+ int n_bo, err = 0;
+
+ for (n_bo = 0; n_bo < max_bo; n_bo++) {
+ err = __xe_bo_create(fd, 0, BO_SIZE, vram_region,
+ DRM_XE_GEM_CREATE_FLAG_DEFER_BACKING,
+ NULL, &handle);
+ if (err)
+ break;
+
+ handles[n_bo] = handle;
+
+ err = __xe_vm_bind_lr_sync(fd, vm, handle, 0, addr, BO_SIZE, 0);
+ if (err)
+ break;
+
+ addr += BO_SIZE;
+ }
+
+ igt_assert_f(err == -ENOMEM || err == -ENOSPC,
+ "Expected -ENOMEM or -ENOSPC, got %d (%s)\n",
+ err, strerror(-err));
+
+ return n_bo;
+}
+
+static void unfill_vram(int fd, uint32_t vm, uint32_t *handles, int n_bo)
+{
+ uint64_t addr = BIND_BASE;
+ int i;
+
+ for (i = 0; i < n_bo; i++) {
+ if (handles[i]) {
+ xe_vm_unbind_lr_sync(fd, vm, 0, addr, BO_SIZE);
+ gem_close(fd, handles[i]);
+ }
+ addr += BO_SIZE;
+ }
+ free(handles);
+}
+
+static void test_write_eviction(int fd, unsigned int flags)
+{
+ struct igt_cgroup *cg;
+ char *cg_region;
+ uint32_t vm;
+ uint64_t vram_region = 0;
+ uint64_t region;
+ uint32_t *handles = NULL;
+ int n_bo = 0, max_bo;
+ uint64_t current, capacity, cg_max, limit, after;
+
+ /* 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");
+
+ /* Find first VRAM region */
+ xe_for_each_mem_region(fd, all_memory_regions(fd), region) {
+ if (xe_region_class(fd, region) == DRM_XE_MEM_REGION_CLASS_VRAM) {
+ vram_region = region;
+ break;
+ }
+ }
+ igt_require_f(vram_region, "No VRAM region found on this device\n");
+
+ cg_region = xe_cgroup_region_name(fd, vram_region);
+ igt_require_f(cg_region, "Region not tracked by dmem cgroup controller\n");
+
+ 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);
+ cg_max = ALIGN_DOWN(cg_max, EVICT_STEP);
+
+ if (flags & TEST_INTERRUPTIBLE)
+ install_sigcont_counter();
+
+ /* Create cgroup and move into it */
+ cg = igt_cgroup_new("xe_cgroups_test");
+ igt_cgroup_move_current(cg);
+ igt_cgroup_dmem_set_max(cg, cg_region, cg_max, false);
+
+ vm = xe_vm_create(fd, DRM_XE_VM_CREATE_FLAG_LR_MODE, 0);
+
+ max_bo = (cg_max / BO_SIZE) + 8; /* headroom for overcommit */
+ handles = calloc(max_bo, sizeof(*handles));
+ igt_assert(handles);
+
+ n_bo = fill_vram(fd, vm, vram_region, handles, max_bo);
+
+ 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 %"PRIu64" MiB exceeds max %"PRIu64" MiB + slack\n",
+ current / SZ_1M, cg_max / SZ_1M);
+
+ /* Phase 2: lower max in 256 MiB steps, verify usage follows */
+ limit = cg_max;
+ while (limit >= EVICT_STEP) {
+
+ limit -= EVICT_STEP;
+ igt_cgroup_dmem_set_max(cg, cg_region, limit, false);
+
+ igt_cgroup_dmem_get_current(cg, cg_region, &after);
+ igt_debug("Lowered max to %"PRIu64" MiB: usage = %"PRIu64" MiB\n",
+ limit / SZ_1M, after / SZ_1M);
+
+ if (limit > EVICT_STEP) {
+ if ((flags & TEST_INTERRUPTIBLE) && after > limit) {
+ uint32_t handle;
+
+ /* Let a new bo creation trigger eviction. */
+ handle = xe_bo_create(fd, 0, BO_SIZE / 8, vram_region, 0);
+ gem_close(fd, handle);
+
+ igt_cgroup_dmem_get_current(cg, cg_region, &after);
+ igt_debug("Forced eviction max is %"PRIu64
+ " MiB: usage = %"PRIu64" MiB\n",
+ limit / SZ_1M, after / SZ_1M);
+ }
+
+ igt_assert_f(after <= limit,
+ "Usage %"PRIu64" MiB did not follow max %"PRIu64" MiB\n",
+ after / SZ_1M, limit / SZ_1M);
+ }
+ }
+
+ if (flags & TEST_INTERRUPTIBLE) {
+ igt_stop_signal_helper();
+ igt_info("Signals received during test: %d\n",
+ atomic_load(&signal_count));
+ }
+
+ /* Cleanup */
+ igt_cgroup_dmem_set_max(cg, cg_region, IGT_CGROUP_DMEM_MAX, false);
+ unfill_vram(fd, vm, handles, n_bo);
+ handles = NULL;
+ xe_vm_destroy(fd, vm);
+ free(cg_region);
+ igt_cgroup_free(cg);
+}
+
+static const struct {
+ const char *name;
+ unsigned int flags;
+} subtests[] = {
+ { "write_eviction", 0 },
+ { "write_eviction_interruptible", TEST_INTERRUPTIBLE },
+ { }
+};
+
+int igt_main()
+{
+ int fd = -1;
+
+ igt_fixture() {
+ fd = drm_open_driver(DRIVER_XE);
+ igt_require_f(getuid() == 0, "Test requires root\n");
+ }
+
+ for (int i = 0; subtests[i].name; i++)
+ igt_subtest(subtests[i].name)
+ test_write_eviction(fd, subtests[i].flags);
+
+ igt_fixture() {
+ drm_close_driver(fd);
+ }
+}
diff --git a/tests/meson.build b/tests/meson.build
index 02fbb8c4e..eb1158510 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -292,6 +292,7 @@ intel_xe_progs = [
'xe_dma_buf_sync',
'xe_drm_fdinfo',
'xe_eu_stall',
+ 'xe_cgroups',
'xe_evict',
'xe_evict_ccs',
'xe_exec_atomic',
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (4 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 7:59 ` ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2) Patchwork
2026-04-28 8:19 ` ✗ i915.CI.BAT: failure " Patchwork
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add write_eviction_nonblock to exercise the O_NONBLOCK path of the dmem
cgroup max interface. After filling VRAM to the cgroup limit, each
limit-lowering step writes dmem.max with O_NONBLOCK so that synchronous
eviction is skipped. The test then verifies that usage has not yet
dropped below the new limit, allocates a small BO to trigger eviction
explicitly, and finally confirms that usage falls within bounds.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/intel/xe_cgroups.c | 43 ++++++++++++++++++++++++++++++++++------
1 file changed, 37 insertions(+), 6 deletions(-)
diff --git a/tests/intel/xe_cgroups.c b/tests/intel/xe_cgroups.c
index 8b0f4381f..aa7a8c3b2 100644
--- a/tests/intel/xe_cgroups.c
+++ b/tests/intel/xe_cgroups.c
@@ -35,6 +35,7 @@
#define BIND_BASE 0x100000000ULL /* 4 GiB VA base */
#define TEST_INTERRUPTIBLE (1 << 0)
+#define TEST_NONBLOCK (1 << 1)
/**
* SUBTEST: write_eviction
@@ -62,6 +63,18 @@
* REQUIREMENTS: must run as root; xe device with at least one VRAM region
*/
+/**
+ * SUBTEST: write_eviction_nonblock
+ * DESCRIPTION:
+ * Same fill phase as write_eviction. In the limit-lowering phase dmem.max
+ * is written with O_NONBLOCK, which causes the kernel to skip synchronous
+ * eviction. After each nonblock write the test verifies that usage has not
+ * yet dropped below the new limit, then triggers eviction explicitly by
+ * allocating a small BO. Finally verifies that usage falls within bounds
+ * after the forced eviction.
+ * REQUIREMENTS: must run as root; xe device with at least one VRAM region
+ */
+
static atomic_int signal_count;
static struct sigaction sigcont_oldact;
@@ -146,7 +159,7 @@ static void test_write_eviction(int fd, unsigned int flags)
uint64_t region;
uint32_t *handles = NULL;
int n_bo = 0, max_bo;
- uint64_t current, capacity, cg_max, limit, after;
+ uint64_t current, capacity, cg_max, limit, after, before;
/* Check dmem cgroup controller is available before doing anything else */
igt_require_f(igt_cgroup_dmem_available(),
@@ -207,18 +220,35 @@ static void test_write_eviction(int fd, unsigned int flags)
while (limit >= EVICT_STEP) {
limit -= EVICT_STEP;
- igt_cgroup_dmem_set_max(cg, cg_region, limit, false);
+
+ if (flags & TEST_NONBLOCK)
+ igt_cgroup_dmem_get_current(cg, cg_region, &before);
+
+ igt_cgroup_dmem_set_max(cg, cg_region, limit,
+ !!(flags & TEST_NONBLOCK));
igt_cgroup_dmem_get_current(cg, cg_region, &after);
igt_debug("Lowered max to %"PRIu64" MiB: usage = %"PRIu64" MiB\n",
limit / SZ_1M, after / SZ_1M);
+ if (flags & TEST_NONBLOCK) {
+ /*
+ * O_NONBLOCK skips eviction: verify usage has not
+ * dropped below the new limit yet.
+ */
+ igt_assert_f(after == before,
+ "Expected no eviction with O_NONBLOCK, but "
+ "usage dropped from %"PRIu64" MiB to %"PRIu64" MiB "
+ "(limit %"PRIu64" MiB)\n",
+ before / SZ_1M, after / SZ_1M, limit / SZ_1M);
+ }
+
if (limit > EVICT_STEP) {
- if ((flags & TEST_INTERRUPTIBLE) && after > limit) {
+ if ((flags & (TEST_INTERRUPTIBLE | TEST_NONBLOCK)) && after > limit) {
uint32_t handle;
- /* Let a new bo creation trigger eviction. */
- handle = xe_bo_create(fd, 0, BO_SIZE / 8, vram_region, 0);
+ handle = xe_bo_create(fd, 0, BO_SIZE / 8,
+ vram_region, 0);
gem_close(fd, handle);
igt_cgroup_dmem_get_current(cg, cg_region, &after);
@@ -252,8 +282,9 @@ static const struct {
const char *name;
unsigned int flags;
} subtests[] = {
- { "write_eviction", 0 },
+ { "write_eviction", 0 },
{ "write_eviction_interruptible", TEST_INTERRUPTIBLE },
+ { "write_eviction_nonblock", TEST_NONBLOCK },
{ }
};
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2)
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (5 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest Thomas Hellström
@ 2026-04-28 7:59 ` Patchwork
2026-04-28 8:19 ` ✗ i915.CI.BAT: failure " Patchwork
7 siblings, 0 replies; 9+ messages in thread
From: Patchwork @ 2026-04-28 7:59 UTC (permalink / raw)
To: Thomas Hellström; +Cc: igt-dev
[-- Attachment #1: Type: text/plain, Size: 1060 bytes --]
== Series Details ==
Series: Initial dmem cgroup support (rev2)
URL : https://patchwork.freedesktop.org/series/163935/
State : success
== Summary ==
CI Bug Log - changes from XEIGT_8874_BAT -> XEIGTPW_15072_BAT
====================================================
Summary
-------
**SUCCESS**
No regressions found.
Participating hosts (13 -> 13)
------------------------------
No changes in participating hosts
Changes
-------
No changes found
Build changes
-------------
* IGT: IGT_8874 -> IGTPW_15072
* Linux: xe-4940-b6f6b69b2dffa9ad1c43b2149786b4630d41acbf -> xe-4944-aea2c496abcf55b647c14fe720bfc4ea555aac6a
IGTPW_15072: 15072
IGT_8874: 4568b2c141ab630c34f8eb2b9afab8cbf8f3ce9e @ https://gitlab.freedesktop.org/drm/igt-gpu-tools.git
xe-4940-b6f6b69b2dffa9ad1c43b2149786b4630d41acbf: b6f6b69b2dffa9ad1c43b2149786b4630d41acbf
xe-4944-aea2c496abcf55b647c14fe720bfc4ea555aac6a: aea2c496abcf55b647c14fe720bfc4ea555aac6a
== Logs ==
For more details see: https://intel-gfx-ci.01.org/tree/intel-xe/IGTPW_15072/index.html
[-- Attachment #2: Type: text/html, Size: 1619 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread
* ✗ i915.CI.BAT: failure for Initial dmem cgroup support (rev2)
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (6 preceding siblings ...)
2026-04-28 7:59 ` ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2) Patchwork
@ 2026-04-28 8:19 ` Patchwork
7 siblings, 0 replies; 9+ messages in thread
From: Patchwork @ 2026-04-28 8:19 UTC (permalink / raw)
To: Thomas Hellström; +Cc: igt-dev
[-- Attachment #1: Type: text/plain, Size: 3213 bytes --]
== Series Details ==
Series: Initial dmem cgroup support (rev2)
URL : https://patchwork.freedesktop.org/series/163935/
State : failure
== Summary ==
CI Bug Log - changes from IGT_8874 -> IGTPW_15072
====================================================
Summary
-------
**FAILURE**
Serious unknown changes coming with IGTPW_15072 absolutely need to be
verified manually.
If you think the reported changes have nothing to do with the changes
introduced in IGTPW_15072, please notify your bug team (I915-ci-infra@lists.freedesktop.org) to allow them
to document this new failure mode, which will reduce false positives in CI.
External URL: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/index.html
Participating hosts (42 -> 40)
------------------------------
Missing (2): bat-dg2-13 fi-snb-2520m
Possible new issues
-------------------
Here are the unknown changes that may have been introduced in IGTPW_15072:
### IGT changes ###
#### Possible regressions ####
* igt@i915_selftest@live@workarounds:
- bat-arlh-3: [PASS][1] -> [INCOMPLETE][2]
[1]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-arlh-3/igt@i915_selftest@live@workarounds.html
[2]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-arlh-3/igt@i915_selftest@live@workarounds.html
Known issues
------------
Here are the changes found in IGTPW_15072 that come from known issues:
### IGT changes ###
#### Issues hit ####
* igt@i915_selftest@live:
- bat-arlh-3: [PASS][3] -> [INCOMPLETE][4] ([i915#15622])
[3]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-arlh-3/igt@i915_selftest@live.html
[4]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-arlh-3/igt@i915_selftest@live.html
* igt@i915_selftest@live@workarounds:
- bat-mtlp-9: [PASS][5] -> [DMESG-FAIL][6] ([i915#12061]) +1 other test dmesg-fail
[5]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-mtlp-9/igt@i915_selftest@live@workarounds.html
[6]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-mtlp-9/igt@i915_selftest@live@workarounds.html
#### Possible fixes ####
* igt@i915_selftest@live:
- bat-dg2-8: [DMESG-FAIL][7] ([i915#12061]) -> [PASS][8] +1 other test pass
[7]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-dg2-8/igt@i915_selftest@live.html
[8]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-dg2-8/igt@i915_selftest@live.html
[i915#12061]: https://gitlab.freedesktop.org/drm/i915/kernel/-/issues/12061
[i915#15622]: https://gitlab.freedesktop.org/drm/i915/kernel/-/issues/15622
Build changes
-------------
* CI: CI-20190529 -> None
* IGT: IGT_8874 -> IGTPW_15072
* Linux: CI_DRM_18369 -> CI_DRM_18373
CI-20190529: 20190529
CI_DRM_18369: b6f6b69b2dffa9ad1c43b2149786b4630d41acbf @ git://anongit.freedesktop.org/gfx-ci/linux
CI_DRM_18373: aea2c496abcf55b647c14fe720bfc4ea555aac6a @ git://anongit.freedesktop.org/gfx-ci/linux
IGTPW_15072: 15072
IGT_8874: 4568b2c141ab630c34f8eb2b9afab8cbf8f3ce9e @ https://gitlab.freedesktop.org/drm/igt-gpu-tools.git
== Logs ==
For more details see: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/index.html
[-- Attachment #2: Type: text/html, Size: 3994 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-04-28 8:19 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync() Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest Thomas Hellström
2026-04-28 7:59 ` ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2) Patchwork
2026-04-28 8:19 ` ✗ i915.CI.BAT: failure " Patchwork
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox