From: "Thomas Hellström" <thomas.hellstrom@linux.intel.com>
To: igt-dev@lists.freedesktop.org
Cc: "Thomas Hellström" <thomas.hellstrom@linux.intel.com>,
"Kamil Konieczny" <kamil.konieczny@linux.intel.com>,
"Himal Prasad Ghimiray" <himal.prasad.ghimiray@intel.com>,
"Rodrigo Vivi" <rodrigo.vivi@intel.com>
Subject: [PATCH i-g-t] tests/intel/xe_numa: Add multi-device NUMA placement tests
Date: Wed, 28 Oct 2026 09:30:45 +0100 [thread overview]
Message-ID: <20261028083045.35833-1-thomas.hellstrom@linux.intel.com> (raw)
Add a new xe_numa test with three subtests:
bo-placement: For each xe device on a distinct NUMA node, create a
system memory BO, share it with the other devices using PRIME and bind
it in every device's LR VM. Use the per-client bo_numa debugfs file to
verify that at least 90% of the BO's pages reside on the creating
device's NUMA node, before and after each import, and after each
importing device has blitted the BO to a CPU-cleared BO and the
pseudo-random content was verified.
userptr-move: Bind a malloc'ed userptr once in every device's LR VM,
fill it with pseudo-random content, move it to every online NUMA node
with move_pages() and verify placement and content after each move.
userptr-move-blit: As userptr-move, but verify the content by having
each device blit the userptr to a CPU-cleared BO after each move,
without rebinding.
bo-placement depends on the xe per-client bo_numa debugfs file, which is
not yet upstream, and skips if it is missing.
Cc: Kamil Konieczny <kamil.konieczny@linux.intel.com>
Cc: Himal Prasad Ghimiray <himal.prasad.ghimiray@intel.com>
Cc: Rodrigo Vivi <rodrigo.vivi@intel.com>
Assisted-by: LLM
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/intel/xe_numa.c | 809 ++++++++++++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 810 insertions(+)
create mode 100644 tests/intel/xe_numa.c
diff --git a/tests/intel/xe_numa.c b/tests/intel/xe_numa.c
new file mode 100644
index 000000000..2f7e564af
--- /dev/null
+++ b/tests/intel/xe_numa.c
@@ -0,0 +1,809 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * TEST: Multi-device NUMA tests
+ * Description: Exercise system memory buffer objects shared between xe
+ * devices attached to different NUMA nodes
+ * Category: Core
+ * Mega feature: MultiGPU
+ * Functionality: NUMA
+ * Test category: functionality test
+ *
+ * SUBTEST: bo-placement
+ * Sub-category: dmabuf
+ * Functionality: NUMA, prime sharing
+ * Description:
+ * For each xe device on a distinct NUMA node, create a system memory
+ * BO, share it with the other devices using PRIME and bind it to every
+ * device's LR VM. Use the xe per-client bo_numa debugfs file to verify
+ * that at least 90% of the BO's pages are on the creating device's
+ * NUMA node. Check this before the import and after each import.
+ * Then fill the BO with pseudo-random content, have each importing
+ * device blit it to a CPU-cleared BO and verify the content. Finally
+ * check the placement again. Requires a kernel providing the bo_numa
+ * debugfs file.
+ *
+ * SUBTEST: userptr-move
+ * Sub-category: userptr
+ * Description:
+ * Bind malloc'ed memory as a userptr once in the LR VM of every xe
+ * device, write pseudo-random content to it and move it to every
+ * online NUMA node in turn. After each move verify that at least 90% of
+ * the pages reside on the target node and that the content is intact
+ * when read with the CPU.
+ *
+ * SUBTEST: userptr-move-blit
+ * Sub-category: userptr
+ * Description:
+ * As userptr-move, but after each move clear a BO with the CPU and have
+ * each device blit the userptr content to it, without rebinding. Verify
+ * the pseudo-random content of the BO.
+ */
+
+/*
+ * Running the tests
+ *
+ * The tests need at least two xe devices on distinct NUMA nodes, and at
+ * least two online NUMA nodes. They need root access, and bo-placement
+ * needs a kernel providing the xe per-client bo_numa debugfs file.
+ *
+ * On a machine without real NUMA nodes, fake nodes can be created with a
+ * kernel built with CONFIG_NUMA_EMU=y, by booting with "numa=fake=2U" on
+ * the kernel command line. This divides each physical node into two
+ * emulated nodes. Check the result with
+ * "cat /sys/devices/system/node/online".
+ *
+ * The xe driver captures a device's NUMA node when it probes the device,
+ * so the devices need to be redistributed across the nodes while the
+ * driver is unloaded:
+ *
+ * - Unload xe: "xe_module_load --r unload"
+ * - Assign each device a node, for example
+ * "echo 0 > /sys/bus/pci/devices/0000:03:00.0/numa_node" and
+ * "echo 1 > /sys/bus/pci/devices/0000:08:00.0/numa_node".
+ * Writing numa_node taints the kernel.
+ * - Load xe again: "xe_module_load --r load"
+ *
+ * The devices are then probed with the new nodes, which can be verified
+ * with "cat /sys/bus/pci/devices/<BDF>/numa_node".
+ */
+
+#include <linux/mempolicy.h>
+#include <sys/random.h>
+#include <sys/syscall.h>
+
+#include "igt.h"
+#include "igt_debugfs.h"
+#include "igt_device.h"
+#include "igt_drm_fdinfo.h"
+#include "igt_rand.h"
+#include "intel_mocs.h"
+#include "igt_sysfs.h"
+#include "xe/xe_ioctl.h"
+#include "xe/xe_query.h"
+
+#define MAX_NUMA_DEVS 16
+#define BO_SIZE SZ_2M
+#define BO_BASE_ADDR 0x100000000ull
+#define MIN_LOCAL_PCT 90
+#define USERPTR_SIZE SZ_2M
+#define USERPTR_ADDR 0x200000000ull
+#define USERPTR_FENCE 0xdeadbeefull
+#define BLIT_DST_ADDR 0x300000000ull
+#define BLIT_BB_ADDR 0x400000000ull
+#define BLIT_PITCH SZ_4K
+#define BLIT_SIZE SZ_2M
+#define BLIT_FENCE_ADDR 0x500000000ull
+#define MAX_NUMA_NODES 64
+
+/**
+ * struct numa_dev - An xe device on a distinct NUMA node
+ * @fd: DRM device file descriptor.
+ * @node: NUMA node the device is attached to, -1 if unknown.
+ * @vm: LR VM created on the device.
+ * @pci_slot: PCI slot name of the device.
+ */
+struct numa_dev {
+ int fd;
+ int node;
+ uint32_t vm;
+ char pci_slot[NAME_MAX];
+};
+
+/**
+ * struct numa_bo_ref - A device's view of a shared BO
+ * @dev: The device holding the handle.
+ * @handle: GEM handle of the BO on @dev.
+ */
+struct numa_bo_ref {
+ struct numa_dev *dev;
+ uint32_t handle;
+};
+
+/**
+ * struct numa_aware_bo - A shared BO and all its handles
+ * @exporter: The device that created and exported the BO.
+ * @importers: The devices that imported the BO.
+ * @num_importers: Number of valid entries in @importers.
+ * @dmabuf: dma-buf file descriptor exported from @exporter.
+ * @size: Size of the BO.
+ * @addr: GPU virtual address the BO is bound at in all VMs.
+ */
+struct numa_aware_bo {
+ struct numa_bo_ref exporter;
+ struct numa_bo_ref importers[MAX_NUMA_DEVS - 1];
+ unsigned int num_importers;
+ int dmabuf;
+ uint64_t size;
+ uint64_t addr;
+};
+
+static int device_numa_node(int fd)
+{
+ int32_t node = -1;
+ int dir;
+
+ dir = igt_sysfs_open(fd);
+ igt_assert_f(dir >= 0, "Failed to open sysfs dir\n");
+ if (!__igt_sysfs_get_s32(dir, "device/numa_node", &node))
+ node = -1;
+ close(dir);
+
+ return node;
+}
+
+static bool node_taken(const struct numa_dev *devs, int num_devs, int node)
+{
+ for (int i = 0; i < num_devs; i++)
+ if (devs[i].node == node)
+ return true;
+
+ return false;
+}
+
+/*
+ * Open all xe devices, keeping only the first device found on each
+ * NUMA node. A node of -1 (unknown) is treated as a node of its own.
+ */
+static int open_numa_devs(struct numa_dev *devs, int max_devs)
+{
+ int num_devs = 0;
+ int fd;
+
+ for (int i = 0; num_devs < max_devs; i++) {
+ int node;
+
+ fd = __drm_open_driver_another(i, DRIVER_XE);
+ if (fd < 0)
+ break;
+
+ node = device_numa_node(fd);
+ if (node_taken(devs, num_devs, node)) {
+ igt_info("Skipping xe device %d on already used NUMA node %d\n",
+ i, node);
+ drm_close_driver(fd);
+ continue;
+ }
+
+ devs[num_devs].fd = fd;
+ devs[num_devs].node = node;
+ igt_device_get_pci_slot_name(fd, devs[num_devs].pci_slot);
+ igt_info("Using xe device %s on NUMA node %d\n",
+ devs[num_devs].pci_slot, node);
+ num_devs++;
+ }
+
+ return num_devs;
+}
+
+static void close_numa_devs(struct numa_dev *devs, int num_devs)
+{
+ for (int i = 0; i < num_devs; i++)
+ drm_close_driver(devs[i].fd);
+}
+
+static void create_vms(struct numa_dev *devs, int num_devs)
+{
+ for (int i = 0; i < num_devs; i++)
+ devs[i].vm = xe_vm_create(devs[i].fd,
+ DRM_XE_VM_CREATE_FLAG_LR_MODE, 0);
+}
+
+static void destroy_vms(struct numa_dev *devs, int num_devs)
+{
+ for (int i = 0; i < num_devs; i++)
+ xe_vm_destroy(devs[i].fd, devs[i].vm);
+}
+
+struct bo_numa_info {
+ unsigned long nodes[MAX_NUMA_NODES];
+ unsigned long total;
+ unsigned long backed_up;
+ bool pages_unknown;
+};
+
+static int open_bo_numa(int fd)
+{
+ struct drm_client_fdinfo info = {};
+ char path[PATH_MAX];
+
+ if (!igt_debugfs_mount() ||
+ !igt_parse_drm_fdinfo(fd, &info, NULL, 0, NULL, 0))
+ return -1;
+
+ snprintf(path, sizeof(path), "%s/dri/client-%lu/bo_numa",
+ igt_debugfs_mount(), info.id);
+
+ return open(path, O_RDWR);
+}
+
+/*
+ * Request the NUMA page distribution of @handle through the
+ * bo_numa debugfs file @bo_numa_fd and parse the response.
+ */
+static void read_bo_numa(int bo_numa_fd, uint32_t handle,
+ struct bo_numa_info *info)
+{
+ char req[16], buf[4096], *line, *save = NULL;
+ ssize_t len, n;
+
+ memset(info, 0, sizeof(*info));
+ snprintf(req, sizeof(req), "%u", handle);
+ igt_assert_eq(write(bo_numa_fd, req, strlen(req)), strlen(req));
+
+ for (len = 0; len < sizeof(buf) - 1; len += n) {
+ n = read(bo_numa_fd, buf + len, sizeof(buf) - 1 - len);
+ igt_assert(n >= 0);
+ if (!n)
+ break;
+ }
+ buf[len] = 0;
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ unsigned int node;
+ unsigned long val;
+
+ if (!strncmp(line, "pages: unknown", strlen("pages: unknown"))) {
+ info->pages_unknown = true;
+ } else if (sscanf(line, "node%u: %lu", &node, &val) == 2) {
+ igt_assert_lt(node, MAX_NUMA_NODES);
+ info->nodes[node] = val;
+ } else if (sscanf(line, "total: %lu", &val) == 1) {
+ info->total = val;
+ } else if (sscanf(line, "backed_up: %lu", &val) == 1) {
+ info->backed_up = val;
+ }
+ }
+}
+
+/*
+ * Record the NUMA node holding most of the BO's pages and the percentage
+ * of pages on that node, and verify that at least MIN_LOCAL_PCT percent of
+ * the pages are on the creating device's NUMA node. If the device's node
+ * is unknown, only report. @stage describes when the check is made.
+ */
+static void check_bo_placement(const struct numa_aware_bo *bo,
+ const char *stage)
+{
+ struct numa_dev *dev = bo->exporter.dev;
+ struct bo_numa_info info;
+ unsigned int best = 0;
+ int bo_numa_fd;
+
+ bo_numa_fd = open_bo_numa(dev->fd);
+ igt_assert_f(bo_numa_fd >= 0, "Failed to open bo_numa for %s\n",
+ dev->pci_slot);
+ read_bo_numa(bo_numa_fd, bo->exporter.handle, &info);
+ close(bo_numa_fd);
+
+ igt_assert_f(!info.pages_unknown,
+ "BO at 0x%" PRIx64 " pages not inspectable (imported BO?)\n",
+ bo->addr);
+ igt_assert_f(info.total, "BO at 0x%" PRIx64 " has no populated pages (%lu backed up)\n",
+ bo->addr, info.backed_up);
+
+ for (unsigned int i = 1; i < MAX_NUMA_NODES; i++)
+ if (info.nodes[i] > info.nodes[best])
+ best = i;
+
+ igt_info("BO 0x%" PRIx64 " created on %s (node %d), %s: dominant node %u, %lu%% of %lu pages\n",
+ bo->addr, dev->pci_slot, dev->node, stage, best,
+ info.nodes[best] * 100 / info.total, info.total);
+
+ if (dev->node < 0)
+ return;
+
+ igt_assert_f(dev->node < MAX_NUMA_NODES &&
+ info.nodes[dev->node] * 100 >= info.total * MIN_LOCAL_PCT,
+ "Only %lu of %lu pages on NUMA node %d, need %d%%\n",
+ dev->node < MAX_NUMA_NODES ? info.nodes[dev->node] : 0,
+ info.total, dev->node, MIN_LOCAL_PCT);
+}
+
+/*
+ * Create a system memory BO on @exporter, which gives it the exporter's
+ * NUMA affinity, export it and import it into all other devices. Bind
+ * the BO at the same address in all devices' VMs. Check the exporter's
+ * placement before the first import and after each import, to verify that
+ * importing doesn't change it.
+ */
+static void numa_aware_bo_create_dev(struct numa_aware_bo *bo,
+ struct numa_dev *devs, int num_devs,
+ struct numa_dev *exporter,
+ uint64_t size, uint64_t addr)
+{
+ memset(bo, 0, sizeof(*bo));
+ bo->size = size;
+ bo->addr = addr;
+ bo->exporter.dev = exporter;
+ bo->exporter.handle = xe_bo_create(exporter->fd, 0, size,
+ system_memory(exporter->fd), 0);
+ bo->dmabuf = prime_handle_to_fd(exporter->fd, bo->exporter.handle);
+ xe_vm_bind_lr_sync(exporter->fd, exporter->vm, bo->exporter.handle,
+ 0, addr, size, 0);
+ check_bo_placement(bo, "before import");
+
+ for (int i = 0; i < num_devs; i++) {
+ struct numa_bo_ref *imp;
+
+ if (&devs[i] == exporter)
+ continue;
+
+ imp = &bo->importers[bo->num_importers++];
+ imp->dev = &devs[i];
+ imp->handle = prime_fd_to_handle(imp->dev->fd, bo->dmabuf);
+ xe_vm_bind_lr_sync(imp->dev->fd, imp->dev->vm, imp->handle,
+ 0, addr, size, 0);
+ check_bo_placement(bo, "after import");
+ }
+}
+
+static void numa_aware_bo_destroy(struct numa_aware_bo *bo)
+{
+ for (unsigned int i = 0; i < bo->num_importers; i++) {
+ struct numa_bo_ref *imp = &bo->importers[i];
+
+ xe_vm_unbind_lr_sync(imp->dev->fd, imp->dev->vm, 0,
+ bo->addr, bo->size);
+ gem_close(imp->dev->fd, imp->handle);
+ }
+
+ close(bo->dmabuf);
+ xe_vm_unbind_lr_sync(bo->exporter.dev->fd, bo->exporter.dev->vm, 0,
+ bo->addr, bo->size);
+ gem_close(bo->exporter.dev->fd, bo->exporter.handle);
+}
+
+/*
+ * Parse the online NUMA nodes from sysfs, a range list such as "0-1,3".
+ */
+static int online_numa_nodes(int *nodes, int max_nodes)
+{
+ char buf[128], *p = buf;
+ int num = 0;
+ FILE *f;
+
+ f = fopen("/sys/devices/system/node/online", "r");
+ igt_require_f(f, "No NUMA node information in sysfs\n");
+ igt_assert(fgets(buf, sizeof(buf), f));
+ fclose(f);
+
+ while (*p && *p != '\n') {
+ long first, last;
+ char *end;
+
+ first = strtol(p, &end, 10);
+ last = first;
+ igt_assert(end != p);
+ if (*end == '-')
+ last = strtol(end + 1, &end, 10);
+
+ for (; first <= last; first++) {
+ igt_assert_lt(num, max_nodes);
+ nodes[num++] = first;
+ }
+
+ p = *end == ',' ? end + 1 : end;
+ }
+
+ return num;
+}
+
+static long numa_move_pages(void **pages, unsigned long count,
+ const int *nodes, int *status)
+{
+ return syscall(SYS_move_pages, 0, count, pages, nodes, status,
+ MPOL_MF_MOVE);
+}
+
+/*
+ * Record the NUMA node holding most of the pages and the percentage of
+ * pages on that node. Return the number of pages on @node.
+ */
+static unsigned long userptr_placement(void *ptr, uint64_t size, int node)
+{
+ unsigned long count = size / SZ_4K, per_node[MAX_NUMA_NODES] = {};
+ void **pages = calloc(count, sizeof(*pages));
+ int *status = calloc(count, sizeof(*status));
+ unsigned int best = 0;
+
+ igt_assert(pages && status);
+ for (unsigned long i = 0; i < count; i++)
+ pages[i] = ptr + i * SZ_4K;
+
+ igt_assert_eq(numa_move_pages(pages, count, NULL, status), 0);
+
+ for (unsigned long i = 0; i < count; i++)
+ if (status[i] >= 0 && status[i] < MAX_NUMA_NODES)
+ per_node[status[i]]++;
+
+ for (unsigned int i = 1; i < MAX_NUMA_NODES; i++)
+ if (per_node[i] > per_node[best])
+ best = i;
+
+ igt_info("Userptr: dominant node %u, %lu%% of %lu pages\n", best,
+ per_node[best] * 100 / count, count);
+
+ free(pages);
+ free(status);
+
+ return per_node[node];
+}
+
+static void move_userptr(void *ptr, uint64_t size, int node)
+{
+ unsigned long count = size / SZ_4K;
+ void **pages = calloc(count, sizeof(*pages));
+ int *nodes = calloc(count, sizeof(*nodes));
+ int *status = calloc(count, sizeof(*status));
+
+ igt_assert(pages && nodes && status);
+ for (unsigned long i = 0; i < count; i++) {
+ pages[i] = ptr + i * SZ_4K;
+ nodes[i] = node;
+ }
+
+ /* Pages not moved are caught by the placement check. */
+ igt_assert_f(numa_move_pages(pages, count, nodes, status) >= 0,
+ "move_pages() failed: %m\n");
+
+ free(pages);
+ free(nodes);
+ free(status);
+}
+
+static void fill_random(uint32_t *map, uint64_t size, uint32_t seed)
+{
+ uint32_t prng = seed;
+
+ for (uint64_t i = 0; i < size / sizeof(*map); i++)
+ map[i] = hars_petruska_f54_1_random(&prng);
+}
+
+static void verify_random(const uint32_t *map, uint64_t size, uint32_t seed)
+{
+ uint32_t prng = seed;
+
+ for (uint64_t i = 0; i < size / sizeof(*map); i++) {
+ uint32_t val = hars_petruska_f54_1_random(&prng);
+
+ igt_assert_f(map[i] == val,
+ "Content not preserved at offset 0x%016" PRIx64
+ ". Failed value is 0x%08x, recorded 0x%08x.\n",
+ i * sizeof(*map), map[i], val);
+ }
+}
+
+/**
+ * struct numa_blit - Per-device resources for blitting system memory
+ * @fence: User fence memory for exec completion.
+ * @queue: Copy engine exec queue.
+ * @dst: Destination BO bound at BLIT_DST_ADDR.
+ * @dst_map: CPU mapping of @dst.
+ * @bb: Batch buffer BO bound at BLIT_BB_ADDR.
+ */
+struct numa_blit {
+ uint64_t *fence;
+ uint32_t queue;
+ uint32_t dst;
+ uint32_t bb;
+ uint32_t *dst_map;
+};
+
+static_assert(BO_SIZE == BLIT_SIZE && USERPTR_SIZE == BLIT_SIZE,
+ "Blits copy whole BOs and userptrs");
+
+/*
+ * Emit an XY_FAST_COPY_BLT blitting BLIT_SIZE bytes of system memory
+ * from @src to BLIT_DST_ADDR, using explicit VM addresses.
+ */
+static void emit_blit(int fd, uint32_t *batch, uint64_t src)
+{
+ unsigned int ip_ver = intel_graphics_ver(intel_get_drm_devid(fd));
+ uint32_t width = BLIT_PITCH / sizeof(uint32_t);
+ uint32_t height = BLIT_SIZE / BLIT_PITCH;
+ uint32_t depth_32bit = 3, mem_system = 1;
+ int i = 0;
+
+ batch[i++] = 0x2 << 29 | 0x42 << 22 | 8;
+ if (ip_ver >= IP_VER(20, 0))
+ batch[i++] = BLIT_PITCH | intel_get_wb_mocs_index(fd) << 20 |
+ depth_32bit << 24 | 1u << 30 | 1u << 31;
+ else
+ batch[i++] = BLIT_PITCH | depth_32bit << 24 |
+ mem_system << 28 | mem_system << 29;
+ batch[i++] = 0;
+ batch[i++] = width | height << 16;
+ batch[i++] = lower_32_bits(BLIT_DST_ADDR);
+ batch[i++] = upper_32_bits(BLIT_DST_ADDR);
+ batch[i++] = 0;
+ if (ip_ver >= IP_VER(20, 0))
+ batch[i++] = BLIT_PITCH | intel_get_wb_mocs_index(fd) << 20;
+ else
+ batch[i++] = BLIT_PITCH;
+ batch[i++] = lower_32_bits(src);
+ batch[i++] = upper_32_bits(src);
+ batch[i++] = MI_BATCH_BUFFER_END;
+}
+
+/*
+ * Set up a copy queue, a destination BO and a batch in @dev's LR VM that
+ * blits from VM address @src.
+ */
+static void numa_blit_init(struct numa_dev *dev, struct numa_blit *blit,
+ uint64_t src)
+{
+ struct drm_xe_sync sync = {
+ .type = DRM_XE_SYNC_TYPE_USER_FENCE,
+ .flags = DRM_XE_SYNC_FLAG_SIGNAL,
+ .timeline_value = USERPTR_FENCE,
+ };
+ uint64_t *bind_fence;
+ uint32_t *batch;
+
+ blit->fence = aligned_alloc(SZ_4K, SZ_4K);
+ igt_assert(blit->fence);
+
+ blit->queue = xe_exec_queue_create_class(dev->fd, dev->vm,
+ DRM_XE_ENGINE_CLASS_COPY);
+ blit->dst = xe_bo_create(dev->fd, 0, BLIT_SIZE,
+ system_memory(dev->fd), 0);
+ blit->dst_map = xe_bo_map(dev->fd, blit->dst, BLIT_SIZE);
+ blit->bb = xe_bo_create(dev->fd, 0, SZ_4K, system_memory(dev->fd), 0);
+
+ batch = xe_bo_map(dev->fd, blit->bb, SZ_4K);
+ emit_blit(dev->fd, batch, src);
+ munmap(batch, SZ_4K);
+
+ xe_vm_bind_lr_sync(dev->fd, dev->vm, blit->dst, 0, BLIT_DST_ADDR,
+ BLIT_SIZE, 0);
+ xe_vm_bind_lr_sync(dev->fd, dev->vm, blit->bb, 0, BLIT_BB_ADDR,
+ SZ_4K, 0);
+
+ /*
+ * The GPU signals the exec user fence through the VM, so map the
+ * fence memory at BLIT_FENCE_ADDR.
+ */
+ bind_fence = aligned_alloc(sizeof(*bind_fence), sizeof(*bind_fence));
+ igt_assert(bind_fence);
+ *bind_fence = 0;
+ sync.addr = to_user_pointer(bind_fence);
+ xe_vm_bind_userptr_async(dev->fd, dev->vm, 0,
+ to_user_pointer(blit->fence),
+ BLIT_FENCE_ADDR, SZ_4K,
+ &sync, 1);
+ xe_wait_ufence(dev->fd, bind_fence, USERPTR_FENCE, 0,
+ NSEC_PER_SEC * 10);
+ free(bind_fence);
+}
+
+static void numa_blit_fini(struct numa_dev *dev, struct numa_blit *blit)
+{
+ xe_vm_unbind_lr_sync(dev->fd, dev->vm, 0, BLIT_DST_ADDR, BLIT_SIZE);
+ xe_vm_unbind_lr_sync(dev->fd, dev->vm, 0, BLIT_BB_ADDR, SZ_4K);
+ xe_vm_unbind_lr_sync(dev->fd, dev->vm, 0, BLIT_FENCE_ADDR, SZ_4K);
+ munmap(blit->dst_map, BLIT_SIZE);
+ gem_close(dev->fd, blit->dst);
+ gem_close(dev->fd, blit->bb);
+ xe_exec_queue_destroy(dev->fd, blit->queue);
+ free(blit->fence);
+}
+
+/* The only userptr bind, it is never redone. */
+static void numa_userptr_bind(struct numa_dev *dev, void *userptr)
+{
+ struct drm_xe_sync sync = {
+ .type = DRM_XE_SYNC_TYPE_USER_FENCE,
+ .flags = DRM_XE_SYNC_FLAG_SIGNAL,
+ .timeline_value = USERPTR_FENCE,
+ };
+ uint64_t *fence = aligned_alloc(SZ_4K, SZ_4K);
+
+ igt_assert(fence);
+ *fence = 0;
+ sync.addr = to_user_pointer(fence);
+ xe_vm_bind_userptr_async(dev->fd, dev->vm, 0, to_user_pointer(userptr),
+ USERPTR_ADDR, USERPTR_SIZE, &sync, 1);
+ xe_wait_ufence(dev->fd, fence, USERPTR_FENCE, 0, NSEC_PER_SEC * 10);
+ free(fence);
+}
+
+/* Clear the destination BO with the CPU, blit the userptr to it and verify. */
+static void blit_and_verify(struct numa_dev *dev, struct numa_blit *blit,
+ uint32_t seed)
+{
+ struct drm_xe_sync sync = {
+ .type = DRM_XE_SYNC_TYPE_USER_FENCE,
+ .flags = DRM_XE_SYNC_FLAG_SIGNAL,
+ .addr = BLIT_FENCE_ADDR,
+ .timeline_value = USERPTR_FENCE,
+ };
+ struct drm_xe_exec exec = {
+ .exec_queue_id = blit->queue,
+ .syncs = to_user_pointer(&sync),
+ .num_syncs = 1,
+ .address = BLIT_BB_ADDR,
+ .num_batch_buffer = 1,
+ };
+
+ memset(blit->dst_map, 0, BLIT_SIZE);
+ *blit->fence = 0;
+ xe_exec(dev->fd, &exec);
+ xe_wait_ufence(dev->fd, blit->fence, USERPTR_FENCE, blit->queue,
+ NSEC_PER_SEC * 10);
+ verify_random(blit->dst_map, BLIT_SIZE, seed);
+}
+
+static void test_bo_placement(struct numa_dev *devs, int num_devs)
+{
+ struct numa_aware_bo bos[MAX_NUMA_DEVS];
+ int num_bos = 0;
+ uint32_t seed;
+
+ for (int i = 0; i < num_devs; i++) {
+ int fd = open_bo_numa(devs[i].fd);
+
+ igt_require_f(fd >= 0,
+ "No bo_numa debugfs file for device %s\n",
+ devs[i].pci_slot);
+ close(fd);
+ }
+
+ igt_assert_eq(getrandom(&seed, sizeof(seed), 0), sizeof(seed));
+ igt_info("Random seed is 0x%08x.\n", seed);
+
+ create_vms(devs, num_devs);
+
+ for (int i = 0; i < num_devs; i++, num_bos++)
+ numa_aware_bo_create_dev(&bos[num_bos], devs, num_devs,
+ &devs[i], BO_SIZE,
+ BO_BASE_ADDR + num_bos * BO_SIZE);
+
+ for (int i = 0; i < num_bos; i++) {
+ uint32_t *map = xe_bo_map(bos[i].exporter.dev->fd,
+ bos[i].exporter.handle, BO_SIZE);
+
+ fill_random(map, BO_SIZE, seed);
+ munmap(map, BO_SIZE);
+ }
+
+ /*
+ * Access each BO from every importer, then verify that the
+ * exporter's placement is unchanged.
+ */
+ for (int i = 0; i < num_bos; i++) {
+ struct numa_aware_bo *bo = &bos[i];
+
+ for (unsigned int j = 0; j < bo->num_importers; j++) {
+ struct numa_dev *dev = bo->importers[j].dev;
+ struct numa_blit blit;
+
+ igt_info("Device %s: blitting BO 0x%" PRIx64 "\n",
+ dev->pci_slot, bo->addr);
+ numa_blit_init(dev, &blit, bo->addr);
+ blit_and_verify(dev, &blit, seed);
+ numa_blit_fini(dev, &blit);
+ }
+
+ check_bo_placement(bo, "after blit");
+ }
+
+ for (int i = 0; i < num_bos; i++)
+ numa_aware_bo_destroy(&bos[i]);
+
+ destroy_vms(devs, num_devs);
+}
+
+/*
+ * Bind a malloc'ed userptr once in every device's LR VM and move it across
+ * all online NUMA nodes. After each move, verify the placement and the
+ * content, either with the CPU or, in @blit mode, by having each device
+ * blit the userptr to a CPU-cleared BO. The userptr is never rebound.
+ */
+static void test_userptr_move(struct numa_dev *devs, int num_devs, bool blit)
+{
+ struct numa_blit blits[MAX_NUMA_DEVS];
+ int nodes[MAX_NUMA_NODES];
+ int num_nodes = online_numa_nodes(nodes, ARRAY_SIZE(nodes));
+ unsigned long count = USERPTR_SIZE / SZ_4K;
+ uint32_t seed;
+ void *ptr;
+
+ igt_require_f(num_nodes >= 2, "Need at least 2 online NUMA nodes\n");
+ igt_assert_eq(getrandom(&seed, sizeof(seed), 0), sizeof(seed));
+ igt_info("Random seed is 0x%08x.\n", seed);
+
+ create_vms(devs, num_devs);
+ igt_assert_eq(posix_memalign(&ptr, SZ_4K, USERPTR_SIZE), 0);
+ fill_random(ptr, USERPTR_SIZE, seed);
+
+ for (int d = 0; d < num_devs; d++) {
+ if (blit)
+ numa_blit_init(&devs[d], &blits[d], USERPTR_ADDR);
+ numa_userptr_bind(&devs[d], ptr);
+ }
+
+ for (int n = 0; n < num_nodes; n++) {
+ unsigned long local;
+
+ igt_info("Moving userptr to node %d\n", nodes[n]);
+ move_userptr(ptr, USERPTR_SIZE, nodes[n]);
+ local = userptr_placement(ptr, USERPTR_SIZE, nodes[n]);
+ igt_assert_f(local * 100 >= count * MIN_LOCAL_PCT,
+ "Only %lu of %lu pages on NUMA node %d, need %d%%\n",
+ local, count, nodes[n], MIN_LOCAL_PCT);
+
+ if (!blit) {
+ verify_random(ptr, USERPTR_SIZE, seed);
+ continue;
+ }
+
+ for (int d = 0; d < num_devs; d++) {
+ igt_info("Device %s: blitting userptr\n",
+ devs[d].pci_slot);
+ blit_and_verify(&devs[d], &blits[d], seed);
+ }
+ }
+
+ for (int d = 0; d < num_devs; d++) {
+ xe_vm_unbind_lr_sync(devs[d].fd, devs[d].vm, 0, USERPTR_ADDR,
+ USERPTR_SIZE);
+ if (blit)
+ numa_blit_fini(&devs[d], &blits[d]);
+ }
+
+ free(ptr);
+ destroy_vms(devs, num_devs);
+}
+
+int igt_main()
+{
+ struct numa_dev devs[MAX_NUMA_DEVS];
+ int num_devs = 0;
+
+ igt_fixture() {
+ num_devs = open_numa_devs(devs, ARRAY_SIZE(devs));
+ igt_require_f(num_devs >= 2,
+ "Need xe devices on at least 2 NUMA nodes, found %d\n",
+ num_devs);
+ }
+
+ igt_describe("Verify that BO pages are placed on the creating device's NUMA node");
+ igt_subtest("bo-placement")
+ test_bo_placement(devs, num_devs);
+
+ igt_describe("Move userptr memory across NUMA nodes and verify placement and content");
+ igt_subtest("userptr-move")
+ test_userptr_move(devs, num_devs, false);
+
+ igt_describe("Move userptr memory across NUMA nodes and blit it to a BO on each device");
+ igt_subtest("userptr-move-blit")
+ test_userptr_move(devs, num_devs, true);
+
+ igt_fixture()
+ close_numa_devs(devs, num_devs);
+}
diff --git a/tests/meson.build b/tests/meson.build
index 519a9e727..4a097bdd0 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -325,6 +325,7 @@ intel_xe_progs = [
'xe_module_load',
'xe_multigpu_svm',
'xe_noexec_ping_pong',
+ 'xe_numa',
'xe_oa',
'xe_page_reclaim',
'xe_pat',
--
2.56.0
next reply other threads:[~2026-10-28 8:32 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 8:30 Thomas Hellström [this message]
2026-10-28 9:07 ` ✓ Xe.CI.BAT: success for tests/intel/xe_numa: Add multi-device NUMA placement tests Patchwork
2026-10-28 9:20 ` ✓ i915.CI.BAT: " Patchwork
2026-10-28 11:59 ` ✗ i915.CI.Full: failure " Patchwork
2026-10-28 15:31 ` ✓ Xe.CI.FULL: success " Patchwork
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261028083045.35833-1-thomas.hellstrom@linux.intel.com \
--to=thomas.hellstrom@linux.intel.com \
--cc=himal.prasad.ghimiray@intel.com \
--cc=igt-dev@lists.freedesktop.org \
--cc=kamil.konieczny@linux.intel.com \
--cc=rodrigo.vivi@intel.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox