On Wed, Nov 04, 2026 at 06:49:12PM -0800, Matthew Brost wrote:
On Wed, Nov 04, 2026 at 05:16:27PM -0800, Niranjana Vishwanathapura wrote:
On Wed, Nov 04, 2026 at 01:52:28PM -0800, Matthew Brost wrote:
> From: Thomas Hellström <[email protected]>
>
> Expose per-page-order DMA mapping counts for the system memory that the
> xe driver maps for GPU access, split into two categories: TTM buffer
> objects, and the GPU SVM / userptr ranges (which share a single
> drm_gpusvm instance per VM).
>
> The stats are visible at:
>
>  <debugfs>/dri/<N>/dma_mapped_pages
>
> and are broken out into two rows (bo, svm/userptr), one column per page
> order, mirroring the layout of the TTM pool page_pool stat.
>
> For TTM BOs the per-order counts are derived from the DMA scatter-gather
> table: each mapped segment is physically contiguous, so its page count is
> split into the largest power-of-two runs (a segment of N pages contributes
> a 2^k chunk for the largest k with 2^k <= N, repeated for the remainder).
> Driving the accounting off the sg table rather than the pages[] array keeps
> it correct even when pages[] entries are cleared while still DMA-mapped,
> as happens on the defrag page-borrowing path. Dma-buf BOs are not
> accounted for in dma_mapped_pages stats.
>
> For the SVM and userptr ranges the accounting is driven by the
> drm_gpusvm @dma_map_account callback, which fires once per dma_addr[]
> entry at the exact point the entry is DMA-mapped and unmapped. This
> makes the accounting symmetric by construction and avoids driver-side
> walks that could drift across migration, partial unmaps and the iova vs
> non-iova paths. Only DRM_INTERCONNECT_SYSTEM entries are real DMA maps
> and counted; device interconnect (VRAM, P2P) entries are skipped, so the
> counter reflects only system pages actually mapped through the DMA layer.
>
> The callback is shared between the full SVM instance (fault mode) and the
> core drm_gpusvm_pages-only instance used for userptr, so userptr
> mappings are accounted in both the CONFIG_DRM_XE_GPUSVM=y and =n (but
> CONFIG_DRM_GPUSVM=y) configurations, into the same svm/userptr counter.
>
> Since the counts are only ever consumed through debugfs, gate the counter
> storage and all accounting on CONFIG_DEBUG_FS so that builds without
> debugfs carry no extra atomic traffic or device state; the drm_gpusvm
> callback is simply left unregistered in that case.
>
> On device teardown assert that every counter has returned to zero, to
> catch any unbalanced accounting (a leaked DMA mapping) during testing.
>
> Assisted-by: LLM
> Signed-off-by: Thomas Hellström <[email protected]>
> Signed-off-by: Matthew Brost <[email protected]>
> ---
> drivers/gpu/drm/xe/xe_bo.c           | 53 ++++++++++++++++++++---
> drivers/gpu/drm/xe/xe_debugfs.c      | 26 ++++++++++++
> drivers/gpu/drm/xe/xe_device.c       | 30 +++++++++++++
> drivers/gpu/drm/xe/xe_device_types.h | 17 ++++++++
> drivers/gpu/drm/xe/xe_svm.c          | 44 ++++++++++++++++++-
> drivers/gpu/drm/xe/xe_svm.h          |  4 +-
> drivers/gpu/drm/xe/xe_userptr.c      | 63 ++++++++++++++++++++++++++++
> drivers/gpu/drm/xe/xe_userptr.h      |  1 +
> 8 files changed, 230 insertions(+), 8 deletions(-)
>
> diff --git a/drivers/gpu/drm/xe/xe_bo.c b/drivers/gpu/drm/xe/xe_bo.c
> index 42a6aa1a2e4a..a8031cb9c74f 100644
> --- a/drivers/gpu/drm/xe/xe_bo.c
> +++ b/drivers/gpu/drm/xe/xe_bo.c
> @@ -396,6 +396,50 @@ struct xe_ttm_tt {
>    bool purgeable;
> };
>
> +/*
> + * xe_tt_account_dma_pages - account DMA-mapped tt pages per order
> + * @xe: the xe device
> + * @xe_tt: the xe_ttm_tt whose sg table to walk
> + * @sign: +1 to add, -1 to subtract
> + *
> + * Walk the tt's DMA scatter-gather table and split each (physically
> + * contiguous) segment into the largest power-of-two page runs: a segment of 
N
> + * pages contributes a 2^k chunk for the largest k with 2^k <= N, repeated 
for
> + * the remainder. Accumulate the count of 2^k pages into
> + * xe->mem.dma_mapped_pages[k]. Driving this off the sg table rather than
> + * tt->pages keeps the accounting correct even when pages[] entries are 
cleared
> + * while still DMA-mapped (e.g. the defrag page-borrowing path).
> + */
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +static void xe_tt_account_dma_pages(struct xe_device *xe,
> +                              struct xe_ttm_tt *xe_tt, int sign)
> +{
> +  struct scatterlist *sg;
> +  unsigned int i;
> +
> +  for_each_sgtable_sg(xe_tt->sg, sg, i) {
> +          unsigned long nr_pages = sg->length >> PAGE_SHIFT;
> +
> +          while (nr_pages) {
> +                  unsigned int order = min_t(unsigned int,
> +                                             __fls(nr_pages),
> +                                             MAX_PAGE_ORDER);
> +                  unsigned long chunk = 1UL << order;
> +
> +                  atomic_long_add((long)chunk * sign,
> +                                  &xe->mem.dma_mapped_pages[order]);
> +

Here both array index and array value is clamped.
But in the svm case below, only the array index is clamped, but not the value.
I guess here we don't expect huge pages? If so, should we be clamping here
or asserting if order is bigger than MAX_PAGE_ORDER?


What? I'm not following this at all.

The SVM case we clamp the index to the highest order + add the actual order.

Here we decompose a segment to into individual orders, clamping at the
highest order.

I mean, in this case, the value is 'chunk' which is based on clamped order.
In svm case below, the value is '(1UL << addr->order)', where addr->order is 
not clamped.

Niranjana


> +                  nr_pages -= chunk;
> +          }
> +  }
> +}
> +#else
> +static void xe_tt_account_dma_pages(struct xe_device *xe,
> +                              struct xe_ttm_tt *xe_tt, int sign)
> +{
> +}
> +#endif
> +
> static int xe_tt_map_sg(struct xe_device *xe, struct ttm_tt *tt)
> {
>    struct xe_ttm_tt *xe_tt = container_of(tt, struct xe_ttm_tt, ttm);
> @@ -425,6 +469,7 @@ static int xe_tt_map_sg(struct xe_device *xe, struct 
ttm_tt *tt)
>            return ret;
>    }
>
> +  xe_tt_account_dma_pages(xe, xe_tt, 1);
>    return 0;
> }
>
> @@ -433,6 +478,7 @@ static void xe_tt_unmap_sg(struct xe_device *xe, struct 
ttm_tt *tt)
>    struct xe_ttm_tt *xe_tt = container_of(tt, struct xe_ttm_tt, ttm);
>
>    if (xe_tt->sg) {
> +          xe_tt_account_dma_pages(xe, xe_tt, -1);
>            dma_unmap_sgtable(xe->drm.dev, xe_tt->sg,
>                              DMA_BIDIRECTIONAL, 0);
>            sg_free_table(xe_tt->sg);
> @@ -1800,11 +1846,8 @@ int xe_bo_dma_unmap_pinned(struct xe_bo *bo)
>                    ttm_bo->sg = NULL;
>                    xe_tt->sg = NULL;
>            } else if (xe_tt->sg) {
> -                  dma_unmap_sgtable(ttm_to_xe_device(ttm_bo->bdev)->drm.dev,
> -                                    xe_tt->sg,
> -                                    DMA_BIDIRECTIONAL, 0);
> -                  sg_free_table(xe_tt->sg);
> -                  xe_tt->sg = NULL;
> +                  xe_tt_unmap_sg(ttm_to_xe_device(ttm_bo->bdev),
> +                                 &xe_tt->ttm);
>            }
>    }
>
> diff --git a/drivers/gpu/drm/xe/xe_debugfs.c b/drivers/gpu/drm/xe/xe_debugfs.c
> index e59d4964d811..1a306b6523e7 100644
> --- a/drivers/gpu/drm/xe/xe_debugfs.c
> +++ b/drivers/gpu/drm/xe/xe_debugfs.c
> @@ -7,6 +7,7 @@
>
> #include <linux/bits.h>
> #include <linux/debugfs.h>
> +#include <linux/mmzone.h>
> #include <linux/fault-inject.h>
> #include <linux/string_helpers.h>
>
> @@ -345,6 +346,28 @@ static int dgfx_pcie_link_residencies_show(struct 
seq_file *m, void *data)
>    return 0;
> }
>
> +static int dma_mapped_pages_show(struct seq_file *m, void *data)
> +{
> +  struct xe_device *xe = m->private;
> +  unsigned int i;
> +
> +  seq_printf(m, "%-11s ", "");
> +  for (i = 0; i < NR_PAGE_ORDERS; i++)
> +          seq_printf(m, " ---%2u---", i);
> +  seq_printf(m, "\n%-11s:", "bo");
> +  for (i = 0; i < NR_PAGE_ORDERS; i++)
> +          seq_printf(m, " %8lu",
> +                     atomic_long_read(&xe->mem.dma_mapped_pages[i]));
> +  seq_printf(m, "\n%-11s:", "svm/userptr");
> +  for (i = 0; i < NR_PAGE_ORDERS; i++)
> +          seq_printf(m, " %8lu",
> +                     atomic_long_read(&xe->mem.dma_mapped_pages_svm[i]));

The array values are signed long, but printed as unsigned.
Should we use '%8l' instead of '%8lu'?


If we ever underflow that is problem, I guess %8l would have that clear.

Also, under each order column, we are not printing the number of pages
of that order, but instead the 4kb page count mapped in that order.
Is that intended?

Yes.

As we are counting huge pages in the highest order bucket, counting the
4kb page count seems necessary here. But perhaps needs to be documented.


A segment can be larger NR_PAGE_ORDERS, thus we clamp large segments
into the highest order. I don't we really need much an explaination, the
debugfs clearly shows if we getting higher order pages, which is the
information we are looking for during debug of perf problems on iGPU.

Niranjana

> +  seq_puts(m, "\n");
> +
> +  return 0;
> +}
> +DEFINE_SHOW_ATTRIBUTE(dma_mapped_pages);
> +
> static const struct drm_info_list debugfs_list[] = {
>    {"info", info, 0},
>    { .name = "sriov_info", .show = sriov_info, },
> @@ -765,6 +788,9 @@ void xe_debugfs_register(struct xe_device *xe)
>
>    xe_ttm_vram_debugfs_init(xe, root);
>
> +  debugfs_create_file("dma_mapped_pages", 0444, root, xe,
> +                      &dma_mapped_pages_fops);
> +
>    for_each_tile(tile, xe, tile_id)
>            xe_tile_debugfs_register(tile);
>
> diff --git a/drivers/gpu/drm/xe/xe_device.c b/drivers/gpu/drm/xe/xe_device.c
> index 273e2c45fea9..c40bb8f7c697 100644
> --- a/drivers/gpu/drm/xe/xe_device.c
> +++ b/drivers/gpu/drm/xe/xe_device.c
> @@ -377,12 +377,42 @@ bool xe_device_is_admin_only(const struct xe_device *xe)
> /* Number of allocated struct xe_device */
> static atomic_t xe_device_count;
>
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +static void xe_device_assert_dma_pages_zero(struct xe_device *xe)
> +{
> +  unsigned int i;
> +
> +  /*
> +   * All BOs, userptr VMAs and SVM ranges must have been torn down by the
> +   * time the device is destroyed, so every DMA-mapped-pages counter must
> +   * have returned to zero. A non-zero value indicates unbalanced
> +   * accounting, i.e. a missing unmap-side decrement.
> +   */
> +  for (i = 0; i < NR_PAGE_ORDERS; i++) {
> +          xe_assert(xe, !atomic_long_read(&xe->mem.dma_mapped_pages[i]));
> +          xe_assert(xe, !atomic_long_read(&xe->mem.dma_mapped_pages_svm[i]));
> +  }

xe_assert() is only valid with CONFIG_DRM_XE_DEBUG.
So, it is kind of tied to CONFIG_DRM_XE_DEBUG. So, it goes back to original
comment as to if we should make it xe debug specific and only make the
debugfs interface part CONFIG_DEBUG_FS specific?
I am fine either way.

> +}
> +#else
> +static void xe_device_assert_dma_pages_zero(struct xe_device *xe)
> +{
> +}
> +#endif
> +
> static void xe_device_destroy(struct drm_device *dev, void *dummy)
> {
>    struct xe_device *xe = to_xe_device(dev);
>
>    xe_bo_dev_fini(&xe->bo_device);
>
> +  /*
> +   * Assert the DMA-mapped-pages accounting only after xe_bo_dev_fini()
> +   * has flushed the async BO-free worker: BOs pending async free still
> +   * hold their DMA mappings (and counts) until the worker runs, so
> +   * asserting before the flush could trip a false positive.
> +   */
> +  xe_device_assert_dma_pages_zero(xe);
> +
>    if (xe->preempt_fence_wq)
>            destroy_workqueue(xe->preempt_fence_wq);
>
> diff --git a/drivers/gpu/drm/xe/xe_device_types.h 
b/drivers/gpu/drm/xe/xe_device_types.h
> index d99881c6e1d5..e816db751da4 100644
> --- a/drivers/gpu/drm/xe/xe_device_types.h
> +++ b/drivers/gpu/drm/xe/xe_device_types.h
> @@ -6,6 +6,7 @@
> #ifndef _XE_DEVICE_TYPES_H_
> #define _XE_DEVICE_TYPES_H_
>
> +#include <linux/mmzone.h>
> #include <linux/pci.h>
>
> #include <drm/drm_device.h>
> @@ -302,6 +303,22 @@ struct xe_device {
>            struct xe_shrinker *shrinker;
>            /** @mem.stolen_mgr: stolen memory manager. */
>            struct xe_ttm_stolen_mgr *stolen_mgr;
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +          /**
> +           * @mem.dma_mapped_pages: number of DMA-mapped pages per page
> +           * order currently live for this device, for TTM BOs. Only
> +           * accounted when CONFIG_DEBUG_FS is enabled, since it is solely
> +           * exposed through debugfs.
> +           */
> +          atomic_long_t dma_mapped_pages[NR_PAGE_ORDERS];
> +          /**
> +           * @mem.dma_mapped_pages_svm: number of DMA-mapped pages per
> +           * page order currently live for this device, for SVM ranges
> +           * and userptr VMAs (both share a single drm_gpusvm instance).
> +           * Only accounted when CONFIG_DEBUG_FS is enabled.
> +           */
> +          atomic_long_t dma_mapped_pages_svm[NR_PAGE_ORDERS];
> +#endif
>    } mem;
>
>    /** @sriov: device level virtualization data */
> diff --git a/drivers/gpu/drm/xe/xe_svm.c b/drivers/gpu/drm/xe/xe_svm.c
> index 909a602d091e..7a4988289749 100644
> --- a/drivers/gpu/drm/xe/xe_svm.c
> +++ b/drivers/gpu/drm/xe/xe_svm.c
> @@ -845,10 +845,50 @@ static int xe_svm_get_pagemaps(struct xe_vm *vm)
> }
> #endif
>
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +/*
> + * xe_svm_dma_map_account - drm_gpusvm DMA-mapping accounting callback
> + * @gpusvm: The GPU SVM the mapping belongs to
> + * @addr: The address descriptor of the chunk being (un)mapped
> + * @sign: +1 when @addr was DMA-mapped, -1 when it is being unmapped
> + *
> + * Maintain per-order counts of the system-memory pages that the full SVM
> + * instance (SVM ranges, and userptr VMAs in fault mode) has DMA-mapped for 
GPU
> + * access. Only DRM_INTERCONNECT_SYSTEM entries are real DMA maps and 
counted;
> + * device interconnect (VRAM, P2P) entries are skipped. The counts are solely
> + * exposed through debugfs, so the callback is only registered when
> + * CONFIG_DEBUG_FS is enabled.
> + */
> +static void xe_svm_dma_map_account(struct drm_gpusvm *gpusvm,
> +                             const struct drm_pagemap_addr *addr,
> +                             int sign)
> +{
> +  struct xe_device *xe = gpusvm_to_vm(gpusvm)->xe;
> +  unsigned int order = addr->order;
> +
> +  if (addr->proto != DRM_INTERCONNECT_SYSTEM)
> +          return;
> +
> +  /*
> +   * The CPU-PTE map order reported by hmm can exceed MAX_PAGE_ORDER
> +   * (e.g. an order-18 1GB mapping), so clamp the array index to avoid an
> +   * out-of-bounds access. Clamping is symmetric across the +1/-1 calls,
> +   * so the per-order counters stay balanced.
> +   */
> +  order = min(order, (unsigned int)NR_PAGE_ORDERS - 1);
> +
> +  atomic_long_add((long)(1UL << addr->order) * sign,
> +                  &xe->mem.dma_mapped_pages_svm[order]);
> +}

This seems identical to xe_userptr_dma_map_account() except that there
gpusvm_to_vm() helper is not there, hence needs to be expanded.
Can we combine these two helpers into one?


I guess one of files can export it and they can share code.

Matt

> +#endif
> +
> static const struct drm_gpusvm_ops gpusvm_ops = {
>    .range_alloc = xe_svm_range_alloc,
>    .range_free = xe_svm_range_free,
>    .invalidate = xe_svm_invalidate,
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +  .dma_map_account = xe_svm_dma_map_account,
> +#endif
> };
>
> static const unsigned long fault_chunk_sizes[] = {
> @@ -946,8 +986,8 @@ int xe_svm_init(struct xe_vm *vm)
>            }
>    } else {
>            err = drm_gpusvm_init(&vm->svm.gpusvm, "Xe SVM (simple)",
> -                                NULL, 0, 0, 0, NULL,
> -                                NULL, 0);
> +                                NULL, 0, 0, 0,
> +                                xe_userptr_gpusvm_ops_get(), NULL, 0);
>            if (err)
>                    goto out_err;
>    }
> diff --git a/drivers/gpu/drm/xe/xe_svm.h b/drivers/gpu/drm/xe/xe_svm.h
> index ffc1cc808102..f8b908a74ba9 100644
> --- a/drivers/gpu/drm/xe/xe_svm.h
> +++ b/drivers/gpu/drm/xe/xe_svm.h
> @@ -249,6 +249,7 @@ struct drm_pagemap *xe_drm_pagemap_from_fd(int fd, u32 
region_instance);
>
> #else
> #include <linux/interval_tree.h>
> +#include "xe_userptr.h"
> #include "xe_vm.h"
>
> struct drm_pagemap_addr;
> @@ -296,7 +297,8 @@ int xe_svm_init(struct xe_vm *vm)
> {
> #if IS_ENABLED(CONFIG_DRM_GPUSVM)
>    return drm_gpusvm_init(&vm->svm.gpusvm, "Xe SVM (simple)",
> -                         NULL, 0, 0, 0, NULL, NULL, 0);
> +                         NULL, 0, 0, 0,
> +                         xe_userptr_gpusvm_ops_get(), NULL, 0);
> #else
>    return 0;
> #endif
> diff --git a/drivers/gpu/drm/xe/xe_userptr.c b/drivers/gpu/drm/xe/xe_userptr.c
> index 9c1dac0fce6f..40f97bd4751d 100644
> --- a/drivers/gpu/drm/xe/xe_userptr.c
> +++ b/drivers/gpu/drm/xe/xe_userptr.c
> @@ -8,9 +8,72 @@
>
> #include <linux/mm.h>
>
> +#include <drm/drm_pagemap.h>
> +
> #include "xe_tlb_inval.h"
> #include "xe_trace_bo.h"
>
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +/*
> + * xe_userptr_dma_map_account - drm_gpusvm DMA-mapping accounting callback
> + * @gpusvm: The GPU SVM the mapping belongs to
> + * @addr: The address descriptor of the chunk being (un)mapped
> + * @sign: +1 when @addr was DMA-mapped, -1 when it is being unmapped
> + *
> + * Account, per page order, the system-memory pages DMA-mapped for GPU access
> + * through the drm_gpusvm_pages instance shared by userptr (and, in fault 
mode,
> + * SVM). Only DRM_INTERCONNECT_SYSTEM entries are real DMA maps and counted;
> + * device interconnect (VRAM, P2P) entries are skipped. The counts are solely
> + * exposed through debugfs, so the callback is only registered when
> + * CONFIG_DEBUG_FS is enabled.
> + */
> +static void xe_userptr_dma_map_account(struct drm_gpusvm *gpusvm,
> +                                 const struct drm_pagemap_addr *addr,
> +                                 int sign)
> +{
> +  struct xe_vm *vm = container_of(gpusvm, struct xe_vm, svm.gpusvm);
> +  unsigned int order = addr->order;
> +
> +  if (addr->proto != DRM_INTERCONNECT_SYSTEM)
> +          return;
> +
> +  /*
> +   * The CPU-PTE map order reported by hmm can exceed MAX_PAGE_ORDER
> +   * (e.g. an order-18 1GB mapping), so clamp the array index to avoid an
> +   * out-of-bounds access. Clamping is symmetric across the +1/-1 calls,
> +   * so the per-order counters stay balanced.
> +   */
> +  order = min(order, (unsigned int)NR_PAGE_ORDERS - 1);
> +
> +  atomic_long_add((long)(1UL << addr->order) * sign,
> +                  &vm->xe->mem.dma_mapped_pages_svm[order]);
> +}
> +
> +static const struct drm_gpusvm_ops xe_userptr_gpusvm_ops = {
> +  .dma_map_account = xe_userptr_dma_map_account,
> +};
> +#endif
> +
> +/**
> + * xe_userptr_gpusvm_ops_get() - Accounting ops for the simple gpusvm 
instance
> + *
> + * The core drm_gpusvm_pages-only instance used for userptr (and, in fault
> + * mode, shared with SVM) is initialised with these ops so that DMA-mapped
> + * system pages are accounted and exposed through debugfs. Returns NULL when
> + * CONFIG_DEBUG_FS is disabled, in which case no accounting is kept and the
> + * instance is initialised without ops.
> + *
> + * Return: Pointer to the restricted &drm_gpusvm_ops, or NULL.
> + */
> +const struct drm_gpusvm_ops *xe_userptr_gpusvm_ops_get(void)
> +{
> +#if IS_ENABLED(CONFIG_DEBUG_FS)
> +  return &xe_userptr_gpusvm_ops;
> +#else
> +  return NULL;
> +#endif
> +}
> +
> static void xe_userptr_assert_in_notifier(struct xe_vm *vm)
> {
>    lockdep_assert(lockdep_is_held_type(&vm->svm.gpusvm.notifier_lock, 0) ||
> diff --git a/drivers/gpu/drm/xe/xe_userptr.h b/drivers/gpu/drm/xe/xe_userptr.h
> index 2a3cd1b5efbb..1e9601be5713 100644
> --- a/drivers/gpu/drm/xe/xe_userptr.h
> +++ b/drivers/gpu/drm/xe/xe_userptr.h
> @@ -108,6 +108,7 @@ int __xe_vm_userptr_needs_repin(struct xe_vm *vm);
> int xe_vm_userptr_check_repin(struct xe_vm *vm);
> int xe_vma_userptr_pin_pages(struct xe_userptr_vma *uvma);
> int xe_vma_userptr_check_repin(struct xe_userptr_vma *uvma);
> +const struct drm_gpusvm_ops *xe_userptr_gpusvm_ops_get(void);
> #else
> static inline void xe_userptr_remove(struct xe_userptr_vma *uvma) {}
>
> --
> 2.34.1
>

Reply via email to