* [PATCH 0/4] RFC: drm/xe: Dynamic Xe Memory Monitor
@ 2026-09-07 6:07 S Sebinraj
2026-09-07 6:07 ` [PATCH 1/4] RFC: ANDROID: drm/xe: add adaptive GPU memory growth monitor xe_mem_monitor S Sebinraj
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: S Sebinraj @ 2026-09-07 6:07 UTC (permalink / raw)
To: intel-xe; +Cc: S Sebinraj
Introduce xe_mem_monitor, a periodic worker that samples
xe->global_total_pages (the same counter that feeds the
gpu_mem/gpu_mem_total tracepoint) and tracks GPU memory growth using an
exponential moving average (EMA) over per-interval deltas:
delta(t) = max(0, mem(t) - mem(t-1))
EMA(t) = alpha * delta(t) + (1 - alpha) * EMA(t-1)
where alpha = 0.8, applied to every real sample
regardless of mode.
A single alpha and a single polling cadence (mem_monitor_poll_ms)
are used in both LATENCY and HIGH_THROUGHPUT mode, once
HIGH_THROUGHPUT mode is entered there is little benefit to re-checking
any faster, since mem_monitor_debounce_count consecutive
below-threshold samples are already required before switching back,
which itself provides the desired minimum dwell time
(debounce_count * poll_ms) at this single cadence.
When the EMA of growth exceeds a threshold, xe->gpu_mem_mode is switched
from XE_GPU_MEM_MODE_LATENCY to XE_GPU_MEM_MODE_HIGH_THROUGHPUT, and
back once mem_monitor_debounce_count consecutive below-threshold
samples are seen.
xe->gpu_mem_mode is intended to be read by the TTM page allocation path
to trade allocation latency for throughput during GPU memory growth
bursts; the GFP flag adjustment itself is added in a follow-up change.
For power, the worker sleeps after mem_monitor_idle_timeout_ms of no
significant activity in LATENCY mode. The worker is re-armed on
activity.
The worker is suspended/resumed across system and runtime PM
transitions with notify_activity() also disabled while suspended.
All tuning is exposed as module parameters (mem_monitor_*) under
/sys/module/xe/parameters/. Defaults:
mem_monitor_enabled false
mem_monitor_growth_threshold_mb 250
mem_monitor_poll_ms 2000
mem_monitor_debounce_count 5
mem_monitor_idle_timeout_ms 10000
S Sebinraj (3):
RFC: ANDROID: drm/xe: add adaptive GPU memory growth monitor
xe_mem_monitor
RFC: ANDROID: drm/xe: register ttm page alloc vendor hooks internally
RFC: ANDROID: drm/xe: Gate mem_monitor wake ups on order 9 external
fragmentation
Ryan Neph (1):
RFC: ANDROID: drm/xe: provide safe mem_monitor enable toggle via
debugfs
drivers/gpu/drm/BUILD.bazel | 4 +
drivers/gpu/drm/defconfig_xe | 1 +
drivers/gpu/drm/xe/Kconfig | 14 +
drivers/gpu/drm/xe/xe_bo.c | 3 +
drivers/gpu/drm/xe/xe_device.c | 12 +
drivers/gpu/drm/xe/xe_device_types.h | 13 +
drivers/gpu/drm/xe/xe_mem_monitor.c | 727 +++++++++++++++++++++++
drivers/gpu/drm/xe/xe_mem_monitor.h | 95 +++
drivers/gpu/drm/xe/xe_module.c | 78 +++
drivers/gpu/drm/xe/xe_module.h | 15 +
drivers/gpu/drm/xe/xe_pm.c | 5 +
drivers/gpu/drm/xe/xe_trace.h | 16 +
drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c | 135 +++++
drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h | 9 +
14 files changed, 1127 insertions(+)
create mode 100644 drivers/gpu/drm/xe/xe_mem_monitor.c
create mode 100644 drivers/gpu/drm/xe/xe_mem_monitor.h
create mode 100644 drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c
create mode 100644 drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 1/4] RFC: ANDROID: drm/xe: add adaptive GPU memory growth monitor xe_mem_monitor
2026-09-07 6:07 [PATCH 0/4] RFC: drm/xe: Dynamic Xe Memory Monitor S Sebinraj
@ 2026-09-07 6:07 ` S Sebinraj
2026-09-07 6:07 ` [PATCH 2/4] RFC: ANDROID: drm/xe: register ttm page alloc vendor hooks internally S Sebinraj
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: S Sebinraj @ 2026-09-07 6:07 UTC (permalink / raw)
To: intel-xe; +Cc: S Sebinraj, Carlos Santa, Ryan Neph, Renato Pereyra, S Sebinraj
From: S Sebinraj <s.sebinraj@intel.corp-partner.google.com>
Introduce xe_mem_monitor, a periodic worker that samples
xe->global_total_pages (the same counter that feeds the
gpu_mem/gpu_mem_total tracepoint) and tracks GPU memory growth using an
exponential moving average (EMA) over per-interval deltas:
delta(t) = max(0, mem(t) - mem(t-1))
EMA(t) = alpha * delta(t) + (1 - alpha) * EMA(t-1)
where alpha = 0.8, applied to every real sample
regardless of mode.
A single alpha and a single polling cadence (mem_monitor_poll_ms)
are used in both LATENCY and HIGH_THROUGHPUT mode, once
HIGH_THROUGHPUT mode is entered there is little benefit to re-checking
any faster, since mem_monitor_debounce_count consecutive
below-threshold samples are already required before switching back,
which itself provides the desired minimum dwell time
(debounce_count * poll_ms) at this single cadence.
When the EMA of growth exceeds a threshold, xe->gpu_mem_mode is switched
from XE_GPU_MEM_MODE_LATENCY to XE_GPU_MEM_MODE_HIGH_THROUGHPUT, and
back once mem_monitor_debounce_count consecutive below-threshold
samples are seen.
xe->gpu_mem_mode is intended to be read by the TTM page allocation path
to trade allocation latency for throughput during GPU memory growth
bursts; the GFP flag adjustment itself is added in a follow-up change.
For power, the worker sleeps after mem_monitor_idle_timeout_ms of no
significant activity in LATENCY mode. The worker is re-armed on
activity.
The worker is suspended/resumed across system and runtime PM
transitions with notify_activity() also disabled while suspended.
All tuning is exposed as module parameters (mem_monitor_*) under
/sys/module/xe/parameters/. Defaults:
mem_monitor_enabled false
mem_monitor_growth_threshold_mb 250
mem_monitor_poll_ms 2000
mem_monitor_debounce_count 5
mem_monitor_idle_timeout_ms 10000
Guarded by new CONFIG_DRM_XE_MEM_MONITOR (depends on DRM_XE, selects
TRACE_GPU_MEM), enabled by default in defconfig_xe.
mem_monitor_enabled for fatcat is set via kernel arg xe.mem_monitor_enabled=1
Test: Verified via kernel logs while running WebGL-Aquarium / WLEU benchmark,
mode switches to HIGH_THROUGHPUT on rapid allocation bursts and
back once memory stabilizes. Confirmed idle-sleep/wake, EMA decay
across sleep, clean suspend/resume behavior.
Cc: Carlos Santa <carlos.santa@intel.com>
Cc: Ryan Neph <ryanneph@google.com>
Cc: Renato Pereyra <renatopereyra@google.com>
Signed-off-by: S Sebinraj <s.sebinraj@intel.com>
---
drivers/gpu/drm/BUILD.bazel | 3 +
drivers/gpu/drm/defconfig_xe | 1 +
drivers/gpu/drm/xe/Kconfig | 14 +
drivers/gpu/drm/xe/xe_bo.c | 3 +
drivers/gpu/drm/xe/xe_device.c | 5 +
drivers/gpu/drm/xe/xe_device_types.h | 12 +
drivers/gpu/drm/xe/xe_mem_monitor.c | 462 +++++++++++++++++++++++++++
drivers/gpu/drm/xe/xe_mem_monitor.h | 82 +++++
drivers/gpu/drm/xe/xe_module.c | 55 ++++
drivers/gpu/drm/xe/xe_module.h | 13 +
drivers/gpu/drm/xe/xe_pm.c | 5 +
11 files changed, 655 insertions(+)
create mode 100644 drivers/gpu/drm/xe/xe_mem_monitor.c
create mode 100644 drivers/gpu/drm/xe/xe_mem_monitor.h
diff --git a/drivers/gpu/drm/BUILD.bazel b/drivers/gpu/drm/BUILD.bazel
index ac32f5cdecb9..274a1aa4ca93 100644
--- a/drivers/gpu/drm/BUILD.bazel
+++ b/drivers/gpu/drm/BUILD.bazel
@@ -154,6 +154,9 @@ ddk_module(
"CONFIG_DRM_XE_GPUFREQTRACER": {
True: ["xe/xe_gpufreqtracer.c"],
},
+ "CONFIG_DRM_XE_MEM_MONITOR": {
+ True: ["xe/xe_mem_monitor.c"],
+ },
"CONFIG_DRM_XE_GPUSVM": {
True: ["xe/xe_svm.c"],
},
diff --git a/drivers/gpu/drm/defconfig_xe b/drivers/gpu/drm/defconfig_xe
index c6e1c69fa663..4dc505580a37 100644
--- a/drivers/gpu/drm/defconfig_xe
+++ b/drivers/gpu/drm/defconfig_xe
@@ -3,3 +3,4 @@ CONFIG_DRM_XE_DISPLAY=y
CONFIG_DRM_XE_DP_TUNNEL=y
CONFIG_DRM_XE_FORCE_PROBE="*"
CONFIG_DRM_XE_GPUFREQTRACER=y
+CONFIG_DRM_XE_MEM_MONITOR=y
diff --git a/drivers/gpu/drm/xe/Kconfig b/drivers/gpu/drm/xe/Kconfig
index 1521975c35fc..d0a0fcb33d64 100644
--- a/drivers/gpu/drm/xe/Kconfig
+++ b/drivers/gpu/drm/xe/Kconfig
@@ -150,6 +150,20 @@ config DRM_XE_GPUFREQTRACER
If unsure, say N.
+config DRM_XE_MEM_MONITOR
+ bool "Enable XE GPU memory growth monitor"
+ depends on DRM_XE
+ select TRACE_GPU_MEM
+ default n
+ help
+ Enable adaptive GPU memory growth monitoring for the Intel XE driver.
+ Polls xe->global_total_pages at 500ms (active) or 2s (idle) intervals
+ and applies an exponential moving average to detect rapid allocation
+ bursts (>200 MB/interval). When a burst is detected the TTM vendor
+ hook is disabled so that the kernel's normal direct-reclaim path can
+ apply backpressure on high-order page allocations, preventing stalls
+ from an unresponsive page cache.
+
menu "drm/Xe Debugging"
depends on DRM_XE
depends on EXPERT
diff --git a/drivers/gpu/drm/xe/xe_bo.c b/drivers/gpu/drm/xe/xe_bo.c
index 493619229376..f2634d048cd4 100644
--- a/drivers/gpu/drm/xe/xe_bo.c
+++ b/drivers/gpu/drm/xe/xe_bo.c
@@ -27,6 +27,7 @@
#include "xe_ggtt.h"
#include "xe_gt.h"
#include "xe_map.h"
+#include "xe_mem_monitor.h"
#include "xe_migrate.h"
#include "xe_pm.h"
#include "xe_preempt_fence.h"
@@ -436,6 +437,8 @@ static void update_global_total_pages(struct ttm_device *ttm_dev,
trace_gpu_mem_total(xe->drm.primary->index, 0,
global_total_pages << PAGE_SHIFT);
+
+ xe_mem_monitor_notify_activity(xe);
#endif
}
diff --git a/drivers/gpu/drm/xe/xe_device.c b/drivers/gpu/drm/xe/xe_device.c
index e405dbedd6b8..676ffc258b94 100644
--- a/drivers/gpu/drm/xe/xe_device.c
+++ b/drivers/gpu/drm/xe/xe_device.c
@@ -36,6 +36,7 @@
#include "xe_force_wake.h"
#include "xe_ggtt.h"
#include "xe_gpufreqtracer.h"
+#include "xe_mem_monitor.h"
#include "xe_gsc_proxy.h"
#include "xe_gt.h"
#include "xe_gt_mcr.h"
@@ -931,6 +932,10 @@ int xe_device_probe(struct xe_device *xe)
if (err)
return err;
+ err = xe_mem_monitor_init(xe);
+ if (err)
+ return err;
+
err = xe_late_bind_init(&xe->late_bind);
if (err)
return err;
diff --git a/drivers/gpu/drm/xe/xe_device_types.h b/drivers/gpu/drm/xe/xe_device_types.h
index 7b6c4e9a12b1..48aa9d8f6f56 100644
--- a/drivers/gpu/drm/xe/xe_device_types.h
+++ b/drivers/gpu/drm/xe/xe_device_types.h
@@ -39,6 +39,7 @@ struct intel_display;
struct intel_dg_nvm_dev;
struct xe_ggtt;
struct xe_gpufreqtracer_data;
+struct xe_mem_monitor_data;
struct xe_i2c;
struct xe_pat_ops;
struct xe_pxp;
@@ -583,6 +584,9 @@ struct xe_device {
/** @gpufreqtracer_data: GPU frequency tracer data */
struct xe_gpufreqtracer_data *gpufreqtracer_data;
+ /** @mem_monitor: GPU memory growth monitor data */
+ struct xe_mem_monitor_data *mem_monitor;
+
/** @needs_flr_on_fini: requests function-reset on fini */
bool needs_flr_on_fini;
@@ -630,6 +634,14 @@ struct xe_device {
*/
atomic64_t global_total_pages;
#endif
+
+ /**
+ * @gpu_mem_mode: current GPU memory allocation mode (enum xe_gpu_mem_mode),
+ * switched between latency and high-throughput by xe_mem_monitor based on
+ * GPU memory growth rate.
+ */
+ atomic_t gpu_mem_mode;
+
/** @val: The domain for exhaustive eviction, which is currently per device. */
struct xe_validation_device val;
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.c b/drivers/gpu/drm/xe/xe_mem_monitor.c
new file mode 100644
index 000000000000..06fbd0624da7
--- /dev/null
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.c
@@ -0,0 +1,462 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright © 2026 Intel Corporation
+ *
+ * xe_mem_monitor - Adaptive GPU memory growth monitor
+ *
+ * Periodically samples xe->global_total_pages (the same counter that feeds
+ * the gpu_mem/gpu_mem_total tracepoint) and applies an exponential moving
+ * average (EMA) over per-interval growth. When growth exceeds a threshold
+ * xe->gpu_mem_mode is switched to XE_GPU_MEM_MODE_HIGH_THROUGHPUT, allowing
+ * the kernel's normal reclaim path to apply backpressure on TTM page
+ * allocations. Once memory stabilises, the mode is switched back to
+ * XE_GPU_MEM_MODE_LATENCY.
+ *
+ * Algorithm:
+ *
+ * delta(t) = max(0, mem(t) - mem(t-1)) // clamp frees to zero
+ * EMA(t) = (ALPHA_NUM * delta(t) +
+ * (2^ALPHA_SHIFT - ALPHA_NUM) * EMA(t-1) ) >> ALPHA_SHIFT
+ *
+ * where ALPHA_NUM/2^ALPHA_SHIFT = alpha = 13/16 = 0.8125,
+ * applied to every real sample regardless of mode, where ALPHA_SHIFT
+ * = 4, (2^4 = 16). Both modes poll at the same mem_monitor_poll_ms
+ * cadence and use the same smoothing weight.
+ *
+ * State machine (xe->gpu_mem_mode):
+ * LATENCY mode, poll
+ * -> EMA > mem_monitor_growth_threshold_mb: switch to HIGH_THROUGHPUT
+ * HIGH_THROUGHPUT mode, poll (single polling rate - see
+ * xe_mem_monitor_work()'s reschedule logic)
+ * -> EMA below threshold for mem_monitor_debounce_count consecutive
+ * samples -> switch back to LATENCY mode. Since samples are taken
+ * every mem_monitor_poll_ms regardless of mode,
+ * mem_monitor_debounce_count also acts as the implicit minimum dwell
+ * time in HIGH_THROUGHPUT mode (debounce_count * poll_ms).
+ *
+ * Power saving: while in LATENCY mode, if no GPU allocation activity has been
+ * observed (see xe_mem_monitor_notify_activity()) for mem_monitor_idle_timeout_ms,
+ * the polling worker stops rescheduling itself entirely (goes to sleep, zero
+ * wakeups) rather than continuing to poll indefinitely. It is re-armed the
+ * next time real GPU memory growth is observed via
+ * xe_mem_monitor_notify_activity(). Since the state machine only ever reacts
+ * to growth (frees are clamped to zero), notify_activity() itself only
+ * resets the idle timer / wakes a sleeping worker when the live memory total
+ * has actually grown since the last real sample, a pure free or a
+ * populate/unpopulate pair that nets to no change, is ignored rather than
+ * forcing a full poll+decay cycle that could never affect the LATENCY/
+ * HIGH_THROUGHPUT decision.
+ *
+ * Since the EMA depends on samples being taken at a roughly regular cadence,
+ * and no samples are taken while asleep, the worker treats any skipped
+ * mem_monitor_poll_ms intervals since its last real sample as implicit
+ * zero-growth samples and fast-forwards the EMA decay for them before
+ * folding in the real delta observed on the sample that woke it up. This
+ * prevents stale, pre-sleep growth history from persisting indefinitely
+ * across arbitrarily long idle gaps.
+ */
+
+#include "xe_mem_monitor.h"
+
+#include <linux/atomic.h>
+#include <linux/jiffies.h>
+#include <linux/minmax.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+
+#include "xe_device.h"
+#include "xe_device_types.h"
+#include "xe_module.h"
+
+/*
+ * EMA smoothing factor. alpha (13/16 = 0.8125) applies to every real
+ * sample, regardless of mode,
+ * since both LATENCY and HIGH_THROUGHPUT modes poll at the same
+ * mem_monitor_poll_ms cadence (see xe_mem_monitor_work()'s reschedule
+ * logic) and share the same smoothing weight.
+ */
+#define ALPHA_NUM 13U
+#define ALPHA_RETAINED 3U /* 16 - 13 */
+#define ALPHA_SHIFT 4U /* /16 */
+
+/*
+ * This is the max number of implicit zero-growth decay steps applied when waking
+ * from sleep (see xe_mem_monitor_work()). Each step degrades EMA by
+ * ALPHA_RETAINED/2^ALPHA_SHIFT = 3/16 per step,
+ * so after the number of steps below the EMA has already decayed to 0
+ * via integer truncation regardless of its starting value. No need to loop
+ * further for arbitrarily long sleeps.
+ */
+#define EMA_DECAY_SATURATE_PERIODS 32
+
+/*
+ * Minimum absolute change (bytes) since the worker's last real sample
+ * required for xe_mem_monitor_notify_activity() to treat it as activity
+ * worth acting on. Filters out negligible/noise-level trickle (a few KB of
+ * housekeeping churn) that would otherwise repeatedly wake the worker for no
+ * meaningful reason. Checked in both directions (growth or shrink): a
+ * significant free must also resync prev_mem_bytes, otherwise it can be left
+ * stuck at a stale high-water mark that later, real growth from a much lower
+ * baseline would never be able to cross.
+ * Current allocated bytes is compared to the prev bytes so we won't accumulate
+ * small errors less than 1 MB over multiple calls
+ */
+#define ACTIVITY_MIN_DELTA_BYTES (1ULL << 20) /* 1 MB */
+
+/**
+ * struct xe_mem_monitor_data - per-device GPU memory monitor state
+ * @xe: back-pointer to the xe device
+ * @work: self-rescheduling delayed work
+ * @prev_mem_bytes: GPU memory total from the previous poll. atomic64_t
+ * because it is also read from
+ * xe_mem_monitor_notify_activity(), which can run
+ * concurrently with the worker on another thread.
+ * @has_prev_sample: false until the first sample is taken (baseline)
+ * @ema_growth_bytes: EMA of per-interval memory growth (bytes)
+ * @is_high_throughput: true while in HIGH_THROUGHPUT mode; only gates
+ * the idle-sleep check below (idle-sleep only
+ * applies in LATENCY mode).
+ * @below_threshold_count: consecutive below-threshold samples since entering
+ * HIGH_THROUGHPUT mode
+ * @last_activity_jiffies: jiffies value of the most recent GPU memory growth
+ * notification (see xe_mem_monitor_notify_activity())
+ * @last_poll_jiffies: jiffies value of the most recent real sample taken
+ * by xe_mem_monitor_work(), used to detect and decay
+ * the EMA across skipped (slept-through) intervals
+ * @suspended: set while the device is suspended (system or
+ * runtime PM); xe_mem_monitor_notify_activity() is a
+ * no-op while set, so that BO eviction/restore
+ * traffic generated by suspend/resume itself cannot
+ * re-arm the worker mid-transition. atomic_t because
+ * it is read from xe_mem_monitor_notify_activity() on
+ * arbitrary caller threads.
+ * @was_pending_before_suspend: whether the worker had a poll actually
+ * scheduled at the moment xe_mem_monitor_suspend()
+ * cancelled it; used by xe_mem_monitor_resume() to
+ * decide whether to reschedule, so that a monitor
+ * that was legitimately asleep before suspend stays
+ * asleep after resume instead of being woken
+ * unconditionally.
+ */
+struct xe_mem_monitor_data {
+ struct xe_device *xe;
+ struct delayed_work work;
+
+ atomic64_t prev_mem_bytes;
+ bool has_prev_sample;
+ u64 ema_growth_bytes;
+ int below_threshold_count;
+ unsigned long last_activity_jiffies;
+ unsigned long last_poll_jiffies;
+ bool is_high_throughput;
+ atomic_t suspended;
+ bool was_pending_before_suspend;
+};
+
+static void xe_mem_monitor_set_high_throughput_mode(struct xe_mem_monitor_data *mon)
+{
+ /* TODO: wire up to the actual vendor hook (adds __GFP_RETRY_MAYFAIL, TBD) */
+ drm_dbg(&mon->xe->drm,
+ "xe_mem_monitor: switching to HIGH_THROUGHPUT mode (TBD)\n");
+ atomic_set(&mon->xe->gpu_mem_mode, XE_GPU_MEM_MODE_HIGH_THROUGHPUT);
+}
+
+static void xe_mem_monitor_set_latency_mode(struct xe_mem_monitor_data *mon)
+{
+ /* TODO: wire up to the actual vendor hook (TBD) */
+ drm_dbg(&mon->xe->drm,
+ "xe_mem_monitor: switching to LATENCY mode (TBD)\n");
+ atomic_set(&mon->xe->gpu_mem_mode, XE_GPU_MEM_MODE_LATENCY);
+}
+
+static void xe_mem_monitor_work(struct work_struct *work)
+{
+ struct xe_mem_monitor_data *mon =
+ container_of(work, struct xe_mem_monitor_data, work.work);
+ struct xe_device *xe = mon->xe;
+ u64 current_bytes, clamped_delta;
+ u64 growth_threshold_bytes;
+ s64 delta_signed;
+ unsigned long now = jiffies;
+ unsigned long next_delay;
+
+ /*
+ * Read the GPU memory total directly from Xe's own accounting counter.
+ * This is the same value that drives the gpu_mem/gpu_mem_total
+ * tracepoint, but read without any BPF or sysfs indirection.
+ */
+ current_bytes = (u64)atomic64_read(&xe->global_total_pages) << PAGE_SHIFT;
+
+ if (!mon->has_prev_sample) {
+ drm_dbg(&xe->drm, "xe_mem_monitor: baseline = %llu MB\n",
+ current_bytes >> 20);
+ atomic64_set(&mon->prev_mem_bytes, current_bytes);
+ mon->has_prev_sample = true;
+ mon->last_poll_jiffies = now;
+ goto reschedule;
+ }
+
+ /*
+ * If more than one nominal poll interval has elapsed since the
+ * last real sample, the worker must have been asleep (or otherwise
+ * delayed) for the extra time. Since no growth can occur unnoticed
+ * while asleep (any GPU allocation activity would have woken it via
+ * xe_mem_monitor_notify_activity()), treat each skipped interval as
+ * an implicit zero-growth sample and fast-forward the EMA decay for
+ * them before folding in the real delta observed below. Otherwise
+ * stale, pre-sleep growth history would persist indefinitely across
+ * arbitrarily long idle gaps.
+ */
+ {
+ unsigned long poll_jiffies =
+ msecs_to_jiffies(xe_modparam.mem_monitor_poll_ms);
+ unsigned long skipped_periods = poll_jiffies ?
+ (now - mon->last_poll_jiffies) / poll_jiffies : 0;
+
+ if (skipped_periods > 1) {
+ unsigned long decay_periods = min_t(unsigned long,
+ skipped_periods - 1,
+ EMA_DECAY_SATURATE_PERIODS);
+
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: woke after %lu skipped poll interval(s), "
+ "fast-forwarding EMA decay by %lu\n",
+ skipped_periods - 1, decay_periods);
+
+ while (decay_periods-- > 0)
+ mon->ema_growth_bytes = (ALPHA_RETAINED *
+ mon->ema_growth_bytes) >> ALPHA_SHIFT;
+ }
+ }
+
+ /* Clamp negative deltas (memory freed) to zero; only track growth. */
+ delta_signed = (s64)current_bytes - (s64)atomic64_read(&mon->prev_mem_bytes);
+ clamped_delta = delta_signed > 0 ? (u64)delta_signed : 0;
+
+ /* A single alpha smoothing weight is used regardless of mode. */
+ mon->ema_growth_bytes = (ALPHA_NUM * clamped_delta +
+ ALPHA_RETAINED * mon->ema_growth_bytes)
+ >> ALPHA_SHIFT;
+
+ atomic64_set(&mon->prev_mem_bytes, current_bytes);
+ mon->last_poll_jiffies = now;
+
+ growth_threshold_bytes = (u64)xe_modparam.mem_monitor_growth_threshold_mb << 20;
+
+ if (atomic_read(&xe->gpu_mem_mode) == XE_GPU_MEM_MODE_LATENCY) {
+ if (mon->ema_growth_bytes > growth_threshold_bytes) {
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: rapid GPU memory growth detected, delta=%llu MB, "
+ "(EMA=%llu MB/interval > threshold=%llu MB)\n",
+ clamped_delta >> 20,
+ mon->ema_growth_bytes >> 20,
+ growth_threshold_bytes >> 20);
+ xe_mem_monitor_set_high_throughput_mode(mon);
+ mon->is_high_throughput = true;
+ mon->below_threshold_count = 0;
+ }
+ } else {
+ /*
+ * mem_monitor_debounce_count consecutive below-threshold
+ * samples (taken at mem_monitor_poll_ms, same single
+ * polling rate used in both modes) are required before
+ * switching back to LATENCY mode. This also acts as the
+ * implicit minimum dwell time in HIGH_THROUGHPUT mode.
+ */
+ if (mon->ema_growth_bytes < growth_threshold_bytes) {
+ mon->below_threshold_count++;
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: GPU memory growth slowing "
+ "(EMA=%llu MB/interval), below-threshold sample %d/%u\n",
+ mon->ema_growth_bytes >> 20,
+ mon->below_threshold_count, xe_modparam.mem_monitor_debounce_count);
+ if (mon->below_threshold_count >= xe_modparam.mem_monitor_debounce_count) {
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: GPU memory stable for %u "
+ "consecutive samples\n",
+ xe_modparam.mem_monitor_debounce_count);
+ xe_mem_monitor_set_latency_mode(mon);
+ mon->is_high_throughput = false;
+ mon->below_threshold_count = 0;
+ }
+ } else {
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: GPU memory still growing "
+ "(EMA=%llu MB/interval), resetting debounce counter\n",
+ mon->ema_growth_bytes >> 20);
+ mon->below_threshold_count = 0;
+ }
+ }
+
+reschedule:
+ if (!mon->is_high_throughput &&
+ jiffies_to_msecs(jiffies - mon->last_activity_jiffies) >
+ xe_modparam.mem_monitor_idle_timeout_ms) {
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: no activity for %u ms, going to sleep\n",
+ xe_modparam.mem_monitor_idle_timeout_ms);
+ return;
+ }
+
+ /*
+ * Single polling rate (mem_monitor_poll_ms) regardless of mode -
+ * per review discussion, once HIGH_THROUGHPUT mode is entered we
+ * don't need to re-check any faster, since mem_monitor_debounce_count
+ * already provides the equivalent minimum dwell time at this cadence.
+ */
+ next_delay = msecs_to_jiffies(xe_modparam.mem_monitor_poll_ms);
+ schedule_delayed_work(&mon->work, next_delay);
+}
+
+static void xe_mem_monitor_cleanup(struct drm_device *drm, void *arg)
+{
+ struct xe_mem_monitor_data *mon = arg;
+
+ cancel_delayed_work_sync(&mon->work);
+}
+
+/**
+ * xe_mem_monitor_notify_activity - notify the monitor of GPU memory changes
+ * @xe: the Xe device
+ *
+ * Called from the TTM populate/unpopulate path whenever GPU memory is
+ * allocated or freed. No-op while the device is suspended (see
+ * xe_mem_monitor_suspend()), since suspend/resume's own BO eviction/restore
+ * traffic would otherwise re-arm the worker mid-transition. Otherwise only
+ * acts when the live memory total has changed by at least
+ * ACTIVITY_MIN_DELTA_BYTES (in either direction) since the worker's last real
+ * sample, negligible trickle growth/shrink is ignored to avoid waking the
+ * worker for no meaningful reason. A significant free must still be acted on
+ * (not just growth): it resyncs prev_mem_bytes down to reality by letting the
+ * worker run once, which is required so that later real growth measured from
+ * that new, lower baseline can still be detected, otherwise prev_mem_bytes
+ * would stay stuck at a stale high-water mark that new growth might never
+ * cross. If the worker is already scheduled (delayed_work_pending()), it
+ * will read the live counter fresh when it runs and pick up whatever
+ * accumulated, so there's no need to re-schedule again here.
+ */
+void xe_mem_monitor_notify_activity(struct xe_device *xe)
+{
+ struct xe_mem_monitor_data *mon = xe->mem_monitor;
+ u64 current_bytes;
+ s64 delta_signed, abs_delta;
+
+ if (!mon)
+ return;
+
+ if (atomic_read(&mon->suspended))
+ return;
+
+ current_bytes = (u64)atomic64_read(&xe->global_total_pages) << PAGE_SHIFT;
+ delta_signed = (s64)current_bytes - (s64)atomic64_read(&mon->prev_mem_bytes);
+ abs_delta = delta_signed < 0 ? -delta_signed : delta_signed;
+
+ if (abs_delta < ACTIVITY_MIN_DELTA_BYTES)
+ return;
+
+ mon->last_activity_jiffies = jiffies;
+
+ if (delayed_work_pending(&mon->work))
+ return;
+
+ schedule_delayed_work(&mon->work, 0);
+}
+
+/**
+ * xe_mem_monitor_suspend - stop the polling worker for a PM transition
+ * @xe: the Xe device
+ *
+ * Sets the suspended flag first so any xe_mem_monitor_notify_activity()
+ * call racing with (or generated by) the suspend sequence itself (e.g.
+ * xe_bo_evict_all()) is a no-op, then synchronously cancels the worker.
+ * Whether a poll was actually pending at that point is recorded so
+ * xe_mem_monitor_resume() can decide whether to reschedule.
+ */
+void xe_mem_monitor_suspend(struct xe_device *xe)
+{
+ struct xe_mem_monitor_data *mon = xe->mem_monitor;
+
+ if (!mon)
+ return;
+
+ atomic_set(&mon->suspended, 1);
+ mon->was_pending_before_suspend = cancel_delayed_work_sync(&mon->work);
+}
+
+/**
+ * xe_mem_monitor_resume - resume the polling worker after a PM transition
+ * @xe: the Xe device
+ *
+ * Only reschedules the worker if it was actually pending at the time
+ * xe_mem_monitor_suspend() cancelled it. If the monitor had already gone to
+ * sleep (idle timeout) before suspend, it stays asleep across the
+ * transition rather than being unconditionally woken.
+ */
+void xe_mem_monitor_resume(struct xe_device *xe)
+{
+ struct xe_mem_monitor_data *mon = xe->mem_monitor;
+
+ if (!mon)
+ return;
+
+ atomic_set(&mon->suspended, 0);
+
+ if (mon->was_pending_before_suspend) {
+ unsigned long delay = msecs_to_jiffies(xe_modparam.mem_monitor_poll_ms);
+
+ schedule_delayed_work(&mon->work, delay);
+ }
+}
+
+/**
+ * xe_mem_monitor_init - initialise and start the GPU memory monitor
+ * @xe: the Xe device
+ *
+ * Allocates monitor state, registers a cleanup action via drmm, and
+ * schedules the first poll after one poll interval.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int xe_mem_monitor_init(struct xe_device *xe)
+{
+ struct xe_mem_monitor_data *mon;
+ int ret;
+
+ if (!xe_modparam.mem_monitor_enabled) {
+ drm_info(&xe->drm,
+ "xe_mem_monitor: disabled via mem_monitor_enabled module parameter\n");
+ return 0;
+ }
+
+ mon = drmm_kzalloc(&xe->drm, sizeof(*mon), GFP_KERNEL);
+ if (!mon)
+ return -ENOMEM;
+
+ mon->xe = xe;
+ atomic_set(&xe->gpu_mem_mode, XE_GPU_MEM_MODE_LATENCY);
+ mon->last_activity_jiffies = jiffies;
+ INIT_DELAYED_WORK(&mon->work, xe_mem_monitor_work);
+
+ xe->mem_monitor = mon;
+
+ ret = drmm_add_action_or_reset(&xe->drm, xe_mem_monitor_cleanup, mon);
+ if (ret)
+ return ret;
+
+ schedule_delayed_work(&mon->work,
+ msecs_to_jiffies(xe_modparam.mem_monitor_poll_ms));
+
+ drm_info(&xe->drm,
+ "xe_mem_monitor: initialized: poll=%u ms, "
+ "threshold=%u MB, alpha=0.%03u, "
+ "debounce=%u, idle_timeout=%u ms\n",
+ xe_modparam.mem_monitor_poll_ms,
+ xe_modparam.mem_monitor_growth_threshold_mb,
+ (ALPHA_NUM * 1000U + (1U << (ALPHA_SHIFT - 1))) >> ALPHA_SHIFT,
+ xe_modparam.mem_monitor_debounce_count,
+ xe_modparam.mem_monitor_idle_timeout_ms);
+
+ return 0;
+}
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.h b/drivers/gpu/drm/xe/xe_mem_monitor.h
new file mode 100644
index 000000000000..846c0628ed93
--- /dev/null
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.h
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#ifndef _XE_MEM_MONITOR_H_
+#define _XE_MEM_MONITOR_H_
+
+struct xe_device;
+
+/**
+ * enum xe_gpu_mem_mode - GPU memory allocation mode controlled by xe_mem_monitor
+ * @XE_GPU_MEM_MODE_LATENCY: default mode; vendor hook strips reclaim flags
+ * from high-order TTM allocations for low latency.
+ * @XE_GPU_MEM_MODE_HIGH_THROUGHPUT: entered during rapid GPU memory growth;
+ * reclaim is allowed (and __GFP_RETRY_MAYFAIL added)
+ * so the kernel can shrink the page cache to satisfy
+ * high-order allocations.
+ */
+enum xe_gpu_mem_mode {
+ XE_GPU_MEM_MODE_LATENCY = 0,
+ XE_GPU_MEM_MODE_HIGH_THROUGHPUT,
+};
+
+/* Default values for module parameters, see xe_module.h/xe_module.c */
+#define XE_MEM_MONITOR_DEFAULT_ENABLED false
+#define XE_MEM_MONITOR_DEFAULT_GROWTH_THRESHOLD_MB 250
+#define XE_MEM_MONITOR_DEFAULT_POLL_MS 2000
+#define XE_MEM_MONITOR_DEFAULT_DEBOUNCE_COUNT 5
+#define XE_MEM_MONITOR_DEFAULT_IDLE_TIMEOUT_MS 10000
+
+/*
+ * Minimum enforced value for mem_monitor_poll_ms (no maximum is
+ * enforced). Guards against a misconfigured/absurdly low value (e.g. 0)
+ * causing the delayed work to effectively busy-loop.
+ */
+#define XE_MEM_MONITOR_MIN_POLL_MS 500
+
+#ifdef CONFIG_DRM_XE_MEM_MONITOR
+
+int xe_mem_monitor_init(struct xe_device *xe);
+
+/*
+ * Notify the monitor that GPU allocation activity occurred (called from the
+ * TTM populate/unpopulate path). Updates the last-activity timestamp and
+ * re-arms the polling worker if it had gone to sleep due to inactivity.
+ */
+void xe_mem_monitor_notify_activity(struct xe_device *xe);
+
+/*
+ * Suspend/resume the polling worker across system and runtime PM
+ * transitions. Must be called from the same suspend/resume paths for
+ * the same reason: schedule_delayed_work() on system_wq is not freezable, so
+ * without this the worker could keep firing (or be re-armed by the BO
+ * eviction/restore traffic that suspend/resume itself generates) across the
+ * transition.
+ */
+void xe_mem_monitor_suspend(struct xe_device *xe);
+void xe_mem_monitor_resume(struct xe_device *xe);
+
+#else /* CONFIG_DRM_XE_MEM_MONITOR */
+
+static inline int xe_mem_monitor_init(struct xe_device *xe)
+{
+ return 0;
+}
+
+static inline void xe_mem_monitor_notify_activity(struct xe_device *xe)
+{
+}
+
+static inline void xe_mem_monitor_suspend(struct xe_device *xe)
+{
+}
+
+static inline void xe_mem_monitor_resume(struct xe_device *xe)
+{
+}
+
+#endif /* CONFIG_DRM_XE_MEM_MONITOR */
+
+#endif /* _XE_MEM_MONITOR_H_ */
diff --git a/drivers/gpu/drm/xe/xe_module.c b/drivers/gpu/drm/xe/xe_module.c
index 4878463734eb..a49b0c0f958b 100644
--- a/drivers/gpu/drm/xe/xe_module.c
+++ b/drivers/gpu/drm/xe/xe_module.c
@@ -15,6 +15,7 @@
#include "xe_configfs.h"
#include "xe_gpufreqtracer.h"
#include "xe_hw_fence.h"
+#include "xe_mem_monitor.h"
#include "xe_pci.h"
#include "xe_pm.h"
#include "xe_observation.h"
@@ -45,6 +46,13 @@ struct xe_modparam xe_modparam = {
.svm_notifier_size = DEFAULT_SVM_NOTIFIER_SIZE,
#ifdef CONFIG_DRM_XE_GPUFREQTRACER
.gpufreq_monitoring_interval_ms = XE_GPUFREQ_MONITORING_DEFAULT_INTERVAL_MS,
+#endif
+#ifdef CONFIG_DRM_XE_MEM_MONITOR
+ .mem_monitor_enabled = XE_MEM_MONITOR_DEFAULT_ENABLED,
+ .mem_monitor_growth_threshold_mb = XE_MEM_MONITOR_DEFAULT_GROWTH_THRESHOLD_MB,
+ .mem_monitor_poll_ms = XE_MEM_MONITOR_DEFAULT_POLL_MS,
+ .mem_monitor_debounce_count = XE_MEM_MONITOR_DEFAULT_DEBOUNCE_COUNT,
+ .mem_monitor_idle_timeout_ms = XE_MEM_MONITOR_DEFAULT_IDLE_TIMEOUT_MS,
#endif
/* the rest are 0 by default */
};
@@ -108,6 +116,53 @@ MODULE_PARM_DESC(gpufreq_monitoring_interval_ms,
__stringify(XE_GPUFREQ_MONITORING_DEFAULT_INTERVAL_MS) ")");
#endif
+#ifdef CONFIG_DRM_XE_MEM_MONITOR
+module_param_named_unsafe(mem_monitor_enabled, xe_modparam.mem_monitor_enabled, bool, 0444);
+MODULE_PARM_DESC(mem_monitor_enabled,
+ "Master enable switch for the GPU memory growth monitor and its "
+ "vendor hooks. Intended to be set as a kernel boot argument"
+ " [default=" __stringify(XE_MEM_MONITOR_DEFAULT_ENABLED) " (enabled)]");
+
+module_param_named(mem_monitor_growth_threshold_mb,
+ xe_modparam.mem_monitor_growth_threshold_mb, uint, 0644);
+MODULE_PARM_DESC(mem_monitor_growth_threshold_mb,
+ "GPU memory growth EMA threshold in MiB per interval that triggers "
+ "HIGH_THROUGHPUT mode [default="
+ __stringify(XE_MEM_MONITOR_DEFAULT_GROWTH_THRESHOLD_MB) "])");
+
+static int param_set_mem_monitor_poll_ms(const char *val, const struct kernel_param *kp)
+{
+ return param_set_uint_minmax(val, kp, XE_MEM_MONITOR_MIN_POLL_MS, UINT_MAX);
+}
+
+static const struct kernel_param_ops param_ops_mem_monitor_poll_ms = {
+ .set = param_set_mem_monitor_poll_ms,
+ .get = param_get_uint,
+};
+
+module_param_cb(mem_monitor_poll_ms, ¶m_ops_mem_monitor_poll_ms,
+ &xe_modparam.mem_monitor_poll_ms, 0644);
+MODULE_PARM_DESC(mem_monitor_poll_ms,
+ "GPU memory monitor polling interval in milliseconds, minimum "
+ __stringify(XE_MEM_MONITOR_MIN_POLL_MS) "ms "
+ "[default=" __stringify(XE_MEM_MONITOR_DEFAULT_POLL_MS) "])");
+
+module_param_named(mem_monitor_debounce_count,
+ xe_modparam.mem_monitor_debounce_count, uint, 0644);
+MODULE_PARM_DESC(mem_monitor_debounce_count,
+ "Number of consecutive below-threshold samples required before "
+ "switching back to LATENCY mode [default="
+ __stringify(XE_MEM_MONITOR_DEFAULT_DEBOUNCE_COUNT) "])");
+
+module_param_named(mem_monitor_idle_timeout_ms,
+ xe_modparam.mem_monitor_idle_timeout_ms, uint, 0644);
+MODULE_PARM_DESC(mem_monitor_idle_timeout_ms,
+ "Time in milliseconds with no GPU allocation activity in LATENCY "
+ "mode after which the monitor's polling worker goes to sleep "
+ "(woken again on the next allocation) [default="
+ __stringify(XE_MEM_MONITOR_DEFAULT_IDLE_TIMEOUT_MS) "])");
+#endif
+
static int xe_check_nomodeset(void)
{
if (drm_firmware_drivers_only())
diff --git a/drivers/gpu/drm/xe/xe_module.h b/drivers/gpu/drm/xe/xe_module.h
index 6a64ee221da6..24e0b52e48ca 100644
--- a/drivers/gpu/drm/xe/xe_module.h
+++ b/drivers/gpu/drm/xe/xe_module.h
@@ -26,6 +26,19 @@ struct xe_modparam {
#ifdef CONFIG_DRM_XE_GPUFREQTRACER
u32 gpufreq_monitoring_interval_ms;
#endif
+#ifdef CONFIG_DRM_XE_MEM_MONITOR
+ /*
+ * Master enable switch for the GPU memory growth monitor and its
+ * associated vendor hooks. Defaults is set in xe_mem_monitor.h
+ * (XE_MEM_MONITOR_DEFAULT_ENABLED). Intended to be
+ * settable as a kernel boot argument
+ */
+ bool mem_monitor_enabled;
+ u32 mem_monitor_growth_threshold_mb;
+ u32 mem_monitor_poll_ms;
+ u32 mem_monitor_debounce_count;
+ u32 mem_monitor_idle_timeout_ms;
+#endif
};
extern struct xe_modparam xe_modparam;
diff --git a/drivers/gpu/drm/xe/xe_pm.c b/drivers/gpu/drm/xe/xe_pm.c
index b291265d18fc..a330a42ea187 100644
--- a/drivers/gpu/drm/xe/xe_pm.c
+++ b/drivers/gpu/drm/xe/xe_pm.c
@@ -24,6 +24,7 @@
#include "xe_i2c.h"
#include "xe_irq.h"
#include "xe_late_bind_fw.h"
+#include "xe_mem_monitor.h"
#include "xe_pcode.h"
#include "xe_pxp.h"
#include "xe_sriov_vf_ccs.h"
@@ -129,6 +130,7 @@ int xe_pm_suspend(struct xe_device *xe)
trace_xe_pm_suspend(xe, __builtin_return_address(0));
xe_gpufreqtracer_suspend_workers(xe);
+ xe_mem_monitor_suspend(xe);
err = xe_pxp_pm_suspend(xe->pxp);
if (err)
@@ -219,6 +221,7 @@ int xe_pm_resume(struct xe_device *xe)
goto err;
xe_gpufreqtracer_resume_workers(xe);
+ xe_mem_monitor_resume(xe);
xe_pxp_pm_resume(xe->pxp);
@@ -515,6 +518,7 @@ int xe_pm_runtime_suspend(struct xe_device *xe)
xe_rpm_lockmap_acquire(xe);
xe_gpufreqtracer_suspend_workers(xe);
+ xe_mem_monitor_suspend(xe);
err = xe_pxp_pm_suspend(xe->pxp);
if (err)
@@ -616,6 +620,7 @@ int xe_pm_runtime_resume(struct xe_device *xe)
}
xe_gpufreqtracer_resume_workers(xe);
+ xe_mem_monitor_resume(xe);
xe_pxp_pm_resume(xe->pxp);
--
2.34.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 2/4] RFC: ANDROID: drm/xe: register ttm page alloc vendor hooks internally
2026-09-07 6:07 [PATCH 0/4] RFC: drm/xe: Dynamic Xe Memory Monitor S Sebinraj
2026-09-07 6:07 ` [PATCH 1/4] RFC: ANDROID: drm/xe: add adaptive GPU memory growth monitor xe_mem_monitor S Sebinraj
@ 2026-09-07 6:07 ` S Sebinraj
2026-09-07 6:07 ` [PATCH 3/4] RFC: ANDROID: drm/xe: provide safe mem_monitor enable toggle via debugfs S Sebinraj
2026-09-07 6:07 ` [PATCH 4/4] RFC: ANDROID: drm/xe: Gate mem_monitor wake ups on order 9 external fragmentation S Sebinraj
3 siblings, 0 replies; 5+ messages in thread
From: S Sebinraj @ 2026-09-07 6:07 UTC (permalink / raw)
To: intel-xe; +Cc: S Sebinraj, Carlos Santa, Ryan Neph, S Sebinraj
From: S Sebinraj <s.sebinraj@intel.corp-partner.google.com>
Register the TTM page allocation vendor hooks
(android_vh_ttm_pool_alloc_max_page_order,
android_vh_ttm_pool_alloc_page_flags) directly from Xe, rather than via
the standalone intel_ttm_vendor_hooks.ko module, for direct, low
overhead access to xe_device state.
xe_register_android_vendor_hooks()/xe_unregister_android_vendor_hooks()
are called from xe_device_create()/xe_device_destroy() respectively.
Both are gated on the xe_modparam.mem_monitor_enabled master switch
(shared with xe_mem_monitor): if disabled, the hooks are never
registered at all, leaving TTM's default GFP flag behavior completely
untouched.
The hook caps the max TTM page order at 9 (2M pages) and adjusts GFP
flags for that order only based on xe->gpu_mem_mode, which is maintained
by xe_mem_monitor:
XE_GPU_MEM_MODE_LATENCY (default):
strip __GFP_DIRECT_RECLAIM and __GFP_KSWAPD_RECLAIM so 2M page
allocations fail fast rather than stalling on reclaim.
__GFP_NORETRY (already set by TTM for order>0) is left untouched.
XE_GPU_MEM_MODE_HIGH_THROUGHPUT:
explicitly set __GFP_DIRECT_RECLAIM and __GFP_KSWAPD_RECLAIM, clear
__GFP_NORETRY, and add __GFP_RETRY_MAYFAIL so the allocator retries
harder (including shrinking the page cache) to satisfy 2M page
allocations during bursts of GPU memory growth.
__GFP_NORETRY and __GFP_RETRY_MAYFAIL represent mutually exclusive
points on the same "costly allocation" retry spectrum (see
Documentation / gfp_types.h); __GFP_NORETRY is cleared explicitly
here since TTM sets it unconditionally for order>0 allocations and
leaving it set would blunt the effect of __GFP_RETRY_MAYFAIL.
- order-0: leave flags unmodified
- order-9: select based on gpu_mem_mode
- all others: ~gfp_flags &= ~(__GFP_DIRECT_RECLAIM | __GFP_KSWAPD_RECLAIM)
A message (rate-limited to mode transitions only, not per allocation) is
logged whenever the effective mode changes, to aid verification. A new
xe_gpu_mem_mode_switch tracepoint is also emitted on every mode
transition (from xe_mem_monitor, see xe_trace.h) so the switching
behavior can be recorded in Perfetto/ftrace traces for debugging and
parameter tuning.
Test: confirmed hook fires and GFP flags change correctly across mode
transitions via dmesg while running WebGL Aquarium / WLEU
benchmark. Confirmed xe_gpu_mem_mode_switch tracepoint fires on
each transition via ftrace. Confirmed hooks are not registered
when mem_monitor_enabled=0 is passed as a boot argument.
Cc: Carlos Santa <carlos.santa@intel.com>
Signed-off-by: Ryan Neph <ryanneph@google.com>
Signed-off-by: S Sebinraj <s.sebinraj@intel.com>
---
drivers/gpu/drm/BUILD.bazel | 1 +
drivers/gpu/drm/xe/xe_device.c | 6 +
drivers/gpu/drm/xe/xe_mem_monitor.c | 15 ++-
drivers/gpu/drm/xe/xe_trace.h | 16 +++
drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c | 135 +++++++++++++++++++++++
drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h | 9 ++
6 files changed, 178 insertions(+), 4 deletions(-)
create mode 100644 drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c
create mode 100644 drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h
diff --git a/drivers/gpu/drm/BUILD.bazel b/drivers/gpu/drm/BUILD.bazel
index 274a1aa4ca93..f4fc4b94f71f 100644
--- a/drivers/gpu/drm/BUILD.bazel
+++ b/drivers/gpu/drm/BUILD.bazel
@@ -127,6 +127,7 @@ ddk_module(
"xe/xe_trace_lrc.c",
"xe/xe_ttm_stolen_mgr.c",
"xe/xe_ttm_sys_mgr.c",
+ "xe/xe_ttm_vendor_hooks.c",
"xe/xe_ttm_vram_mgr.c",
"xe/xe_tuning.c",
"xe/xe_uc.c",
diff --git a/drivers/gpu/drm/xe/xe_device.c b/drivers/gpu/drm/xe/xe_device.c
index 676ffc258b94..d21068c1effc 100644
--- a/drivers/gpu/drm/xe/xe_device.c
+++ b/drivers/gpu/drm/xe/xe_device.c
@@ -69,6 +69,7 @@
#include "xe_tile.h"
#include "xe_ttm_stolen_mgr.h"
#include "xe_ttm_sys_mgr.h"
+#include "xe_ttm_vendor_hooks.h"
#include "xe_vm.h"
#include "xe_vm_madvise.h"
#include "xe_vram.h"
@@ -419,6 +420,7 @@ static void xe_device_destroy(struct drm_device *dev, void *dummy)
{
struct xe_device *xe = to_xe_device(dev);
+ xe_unregister_android_vendor_hooks(xe);
xe_bo_dev_fini(&xe->bo_device);
if (xe->preempt_fence_wq)
@@ -464,6 +466,10 @@ struct xe_device *xe_device_create(struct pci_dev *pdev,
if (err)
goto err;
+ err = xe_register_android_vendor_hooks(xe);
+ if (err)
+ goto err;
+
err = xe_shrinker_create(xe);
if (err)
goto err;
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.c b/drivers/gpu/drm/xe/xe_mem_monitor.c
index 06fbd0624da7..9205417bfb0b 100644
--- a/drivers/gpu/drm/xe/xe_mem_monitor.c
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.c
@@ -69,6 +69,7 @@
#include "xe_device.h"
#include "xe_device_types.h"
#include "xe_module.h"
+#include "xe_trace.h"
/*
* EMA smoothing factor. alpha (13/16 = 0.8125) applies to every real
@@ -157,18 +158,24 @@ struct xe_mem_monitor_data {
static void xe_mem_monitor_set_high_throughput_mode(struct xe_mem_monitor_data *mon)
{
- /* TODO: wire up to the actual vendor hook (adds __GFP_RETRY_MAYFAIL, TBD) */
+ /*
+ * The TTM vendor hook (xe_ttm_vendor_hooks.c) reads xe->gpu_mem_mode
+ * directly on the next 2M page allocation, so updating the mode here
+ * is sufficient - no separate hook call is needed.
+ */
drm_dbg(&mon->xe->drm,
- "xe_mem_monitor: switching to HIGH_THROUGHPUT mode (TBD)\n");
+ "xe_mem_monitor: switching to HIGH_THROUGHPUT mode\n");
atomic_set(&mon->xe->gpu_mem_mode, XE_GPU_MEM_MODE_HIGH_THROUGHPUT);
+ trace_xe_gpu_mem_mode_switch(XE_GPU_MEM_MODE_HIGH_THROUGHPUT, mon->ema_growth_bytes);
}
static void xe_mem_monitor_set_latency_mode(struct xe_mem_monitor_data *mon)
{
- /* TODO: wire up to the actual vendor hook (TBD) */
+ /* See xe_mem_monitor_set_high_throughput_mode() above. */
drm_dbg(&mon->xe->drm,
- "xe_mem_monitor: switching to LATENCY mode (TBD)\n");
+ "xe_mem_monitor: switching to LATENCY mode\n");
atomic_set(&mon->xe->gpu_mem_mode, XE_GPU_MEM_MODE_LATENCY);
+ trace_xe_gpu_mem_mode_switch(XE_GPU_MEM_MODE_LATENCY, mon->ema_growth_bytes);
}
static void xe_mem_monitor_work(struct work_struct *work)
diff --git a/drivers/gpu/drm/xe/xe_trace.h b/drivers/gpu/drm/xe/xe_trace.h
index 314f42fcbcbd..77bfc08613b9 100644
--- a/drivers/gpu/drm/xe/xe_trace.h
+++ b/drivers/gpu/drm/xe/xe_trace.h
@@ -441,6 +441,22 @@ TRACE_EVENT(xe_eu_stall_data_read,
__entry->read_size, __entry->total_size)
);
+TRACE_EVENT(xe_gpu_mem_mode_switch,
+ TP_PROTO(int mode, u64 growth_bytes),
+ TP_ARGS(mode, growth_bytes),
+
+ TP_STRUCT__entry(__field(int, mode)
+ __field(u64, growth_bytes)
+ ),
+
+ TP_fast_assign(__entry->mode = mode;
+ __entry->growth_bytes = growth_bytes;
+ ),
+
+ TP_printk("mode=%d, growth_bytes=%llu", __entry->mode,
+ __entry->growth_bytes)
+);
+
#endif
/* This part must be outside protection */
diff --git a/drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c b/drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c
new file mode 100644
index 000000000000..b4136ac6cc88
--- /dev/null
+++ b/drivers/gpu/drm/xe/xe_ttm_vendor_hooks.c
@@ -0,0 +1,135 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Google LLC.
+ *
+ * TTM page allocation vendor hooks, registered internally by Xe (rather than
+ * via the standalone intel_ttm_vendor_hooks.ko module) for direct, low
+ * overhead access to xe_device state.
+ *
+ * The GFP flags for 2M (order-9) page allocations are adjusted based on
+ * xe->gpu_mem_mode, which is maintained by xe_mem_monitor:
+ *
+ * XE_GPU_MEM_MODE_LATENCY: strip __GFP_DIRECT_RECLAIM and
+ * __GFP_KSWAPD_RECLAIM for fast, low
+ * latency allocation. __GFP_NORETRY (set
+ * by TTM for order>0) is left untouched.
+ * XE_GPU_MEM_MODE_HIGH_THROUGHPUT: ensure __GFP_DIRECT_RECLAIM and
+ * __GFP_KSWAPD_RECLAIM are set, clear
+ * __GFP_NORETRY (set by TTM for order>0,
+ * and mutually exclusive in intent with
+ * __GFP_RETRY_MAYFAIL below), and add
+ * __GFP_RETRY_MAYFAIL to maximize the
+ * chance of a successful 2M page
+ * allocation.
+ *
+ * All other orders (including order-0) are left completely unmodified.
+ */
+
+#include <linux/atomic.h>
+#include <linux/gfp.h>
+#include <linux/printk.h>
+#include <trace/hooks/ttm_pool.h>
+
+#include "xe_device_types.h"
+#include "xe_mem_monitor.h"
+#include "xe_module.h"
+#include "xe_ttm_vendor_hooks.h"
+
+/* 2M pages (order-9) are the only order whose GFP flags are adjusted. */
+#define XE_TTM_VENDOR_HOOKS_ORDER 9
+
+static void xe_android_vh_ttm_pool_alloc_max_page_order(void *data,
+ unsigned int *max_order)
+{
+ *max_order = XE_TTM_VENDOR_HOOKS_ORDER;
+}
+
+static void xe_android_vh_ttm_pool_alloc_page_flags(void *data,
+ unsigned int order,
+ gfp_t *gfp_flags)
+{
+ struct xe_device *xe = (struct xe_device *)data;
+ int mode;
+
+ /* order-0 must always be with __GFP_DIRECT_RECLAIM to avoid ENOMEM
+ * unless it is absolutely necessary (system memory is completely
+ * exhausted).
+ */
+ if (!order)
+ return;
+
+ mode = atomic_read(&xe->gpu_mem_mode);
+
+ if (order == XE_TTM_VENDOR_HOOKS_ORDER &&
+ mode == XE_GPU_MEM_MODE_HIGH_THROUGHPUT) {
+ /*
+ * Ensure reclaim is allowed, clear __GFP_NORETRY (set by TTM for
+ * order>0, but mutually exclusive in intent with
+ * __GFP_RETRY_MAYFAIL) and add __GFP_RETRY_MAYFAIL so the
+ * allocator retries harder for a 2M page instead of failing fast.
+ * Order-9 (2MB) is specifically targeted here as the underlying IOMMU
+ * hardware and Xe architecture operate most efficiently with 2MB alignments.
+ */
+ *gfp_flags |= (__GFP_DIRECT_RECLAIM | __GFP_KSWAPD_RECLAIM | __GFP_RETRY_MAYFAIL);
+ *gfp_flags &= ~__GFP_NORETRY;
+ } else {
+ *gfp_flags &= ~(__GFP_DIRECT_RECLAIM | __GFP_KSWAPD_RECLAIM);
+ }
+}
+
+int xe_register_android_vendor_hooks(struct xe_device *xe)
+{
+ int ret;
+
+ /*
+ * Gated by the same master switch as xe_mem_monitor: if the feature is
+ * disabled, do not register the hooks at all so TTM's default GFP flag
+ * behavior is left completely untouched.
+ */
+ if (!xe_modparam.mem_monitor_enabled)
+ return 0;
+
+ ret = register_trace_android_vh_ttm_pool_alloc_max_page_order(
+ xe_android_vh_ttm_pool_alloc_max_page_order, (void *)xe);
+ if (ret) {
+ pr_warn("failed to register ttm_pool_alloc_max_page_order hook: %d\n",
+ ret);
+ goto fail;
+ }
+
+ ret = register_trace_android_vh_ttm_pool_alloc_page_flags(
+ xe_android_vh_ttm_pool_alloc_page_flags, (void *)xe);
+ if (ret) {
+ pr_warn("failed to register ttm_pool_alloc_page_flags hook: %d\n",
+ ret);
+ goto fail_alloc_page_flags;
+ }
+
+ return 0;
+
+fail_alloc_page_flags:
+ unregister_trace_android_vh_ttm_pool_alloc_max_page_order(
+ xe_android_vh_ttm_pool_alloc_max_page_order, (void *)xe);
+fail:
+ return ret;
+}
+
+void xe_unregister_android_vendor_hooks(struct xe_device *xe)
+{
+ int ret;
+
+ if (!xe_modparam.mem_monitor_enabled)
+ return;
+
+ ret = unregister_trace_android_vh_ttm_pool_alloc_max_page_order(
+ xe_android_vh_ttm_pool_alloc_max_page_order, (void *)xe);
+ if (ret)
+ pr_warn("failed to unregister ttm_pool_alloc_max_page_order hook: %d\n",
+ ret);
+
+ ret = unregister_trace_android_vh_ttm_pool_alloc_page_flags(
+ xe_android_vh_ttm_pool_alloc_page_flags, (void *)xe);
+ if (ret)
+ pr_warn("failed to unregister ttm_pool_alloc_page_flags hook: %d\n",
+ ret);
+}
diff --git a/drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h b/drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h
new file mode 100644
index 000000000000..f2059a3822ae
--- /dev/null
+++ b/drivers/gpu/drm/xe/xe_ttm_vendor_hooks.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Google LLC.
+ */
+
+struct xe_device;
+
+int xe_register_android_vendor_hooks(struct xe_device *xe);
+void xe_unregister_android_vendor_hooks(struct xe_device *xe);
--
2.34.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 3/4] RFC: ANDROID: drm/xe: provide safe mem_monitor enable toggle via debugfs
2026-09-07 6:07 [PATCH 0/4] RFC: drm/xe: Dynamic Xe Memory Monitor S Sebinraj
2026-09-07 6:07 ` [PATCH 1/4] RFC: ANDROID: drm/xe: add adaptive GPU memory growth monitor xe_mem_monitor S Sebinraj
2026-09-07 6:07 ` [PATCH 2/4] RFC: ANDROID: drm/xe: register ttm page alloc vendor hooks internally S Sebinraj
@ 2026-09-07 6:07 ` S Sebinraj
2026-09-07 6:07 ` [PATCH 4/4] RFC: ANDROID: drm/xe: Gate mem_monitor wake ups on order 9 external fragmentation S Sebinraj
3 siblings, 0 replies; 5+ messages in thread
From: S Sebinraj @ 2026-09-07 6:07 UTC (permalink / raw)
To: intel-xe; +Cc: Ryan Neph, S Sebinraj
From: Ryan Neph <ryanneph@google.com>
Provide a way to safely enable/disable mem_monitor at runtime and modify
its parameters with the sequence:
```
su
echo 0 > /sys/kernel/debug/dri/0/mem_monitor/enabled
; # modify params: /sys/module/xe/parameters/mem_monitor_*
echo 1 > /sys/kernel/debug/dri/0/mem_monitor/enabled
```
Also adds to /sys/kernel/debug/dri/0/mem_monitor/:
- ./info: to show state of the mem_monitor
- ./mode: to show/set the current mode
Set the mode by writing "latency" or "throughput" to the file.
If mem_monitor is:
- disabled, this mode is persistent.
- enabled, this mode may be changed by the mem_monitor
Test: echo 0 > /sys/kernel/dri/0/mem_monitor/enabled # disables worker
Test: echo 1 > /sys/kernel/dri/0/mem_monitor/enabled # re-enables worker
Signed-off-by: Ryan Neph <ryanneph@google.com>
Signed-off-by: S Sebinraj <s.sebinraj@intel.com>
---
drivers/gpu/drm/xe/xe_device.c | 1 +
drivers/gpu/drm/xe/xe_device_types.h | 1 +
drivers/gpu/drm/xe/xe_mem_monitor.c | 153 ++++++++++++++++++++++++++-
drivers/gpu/drm/xe/xe_mem_monitor.h | 11 ++
drivers/gpu/drm/xe/xe_module.c | 1 +
5 files changed, 166 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/xe/xe_device.c b/drivers/gpu/drm/xe/xe_device.c
index d21068c1effc..c8cec778cc97 100644
--- a/drivers/gpu/drm/xe/xe_device.c
+++ b/drivers/gpu/drm/xe/xe_device.c
@@ -941,6 +941,7 @@ int xe_device_probe(struct xe_device *xe)
err = xe_mem_monitor_init(xe);
if (err)
return err;
+ xe_mem_monitor_debugfs_register(xe);
err = xe_late_bind_init(&xe->late_bind);
if (err)
diff --git a/drivers/gpu/drm/xe/xe_device_types.h b/drivers/gpu/drm/xe/xe_device_types.h
index 48aa9d8f6f56..b920a412026b 100644
--- a/drivers/gpu/drm/xe/xe_device_types.h
+++ b/drivers/gpu/drm/xe/xe_device_types.h
@@ -586,6 +586,7 @@ struct xe_device {
/** @mem_monitor: GPU memory growth monitor data */
struct xe_mem_monitor_data *mem_monitor;
+ struct dentry *mem_monitor_debugfs_root;
/** @needs_flr_on_fini: requests function-reset on fini */
bool needs_flr_on_fini;
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.c b/drivers/gpu/drm/xe/xe_mem_monitor.c
index 9205417bfb0b..a34cafe56976 100644
--- a/drivers/gpu/drm/xe/xe_mem_monitor.c
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.c
@@ -59,9 +59,12 @@
#include "xe_mem_monitor.h"
#include <linux/atomic.h>
+#include <linux/debugfs.h>
#include <linux/jiffies.h>
#include <linux/minmax.h>
+#include <linux/seq_file.h>
#include <linux/slab.h>
+#include <linux/string.h>
#include <linux/workqueue.h>
#include <drm/drm_managed.h>
#include <drm/drm_print.h>
@@ -154,6 +157,11 @@ struct xe_mem_monitor_data {
bool is_high_throughput;
atomic_t suspended;
bool was_pending_before_suspend;
+
+ struct {
+ atomic64_t current_bytes;
+ atomic64_t delta_signed;
+ } info;
};
static void xe_mem_monitor_set_high_throughput_mode(struct xe_mem_monitor_data *mon)
@@ -247,6 +255,9 @@ static void xe_mem_monitor_work(struct work_struct *work)
ALPHA_RETAINED * mon->ema_growth_bytes)
>> ALPHA_SHIFT;
+ atomic64_set(&mon->info.current_bytes, current_bytes);
+ atomic64_set(&mon->info.delta_signed, delta_signed);
+
atomic64_set(&mon->prev_mem_bytes, current_bytes);
mon->last_poll_jiffies = now;
@@ -320,8 +331,21 @@ static void xe_mem_monitor_work(struct work_struct *work)
static void xe_mem_monitor_cleanup(struct drm_device *drm, void *arg)
{
struct xe_mem_monitor_data *mon = arg;
+ struct xe_device *xe = mon->xe;
+ WARN_ON_ONCE(!xe->mem_monitor);
cancel_delayed_work_sync(&mon->work);
+ kfree(mon);
+ xe->mem_monitor = NULL;
+}
+
+void xe_mem_monitor_fini(struct xe_device *xe)
+{
+ if (!xe->mem_monitor)
+ return;
+
+ drmm_release_action(&xe->drm, xe_mem_monitor_cleanup, (void *)xe->mem_monitor);
+ drm_info(&xe->drm, "xe_mem_monitor: stopped");
}
/**
@@ -437,7 +461,7 @@ int xe_mem_monitor_init(struct xe_device *xe)
return 0;
}
- mon = drmm_kzalloc(&xe->drm, sizeof(*mon), GFP_KERNEL);
+ mon = kzalloc(sizeof(*mon), GFP_KERNEL);
if (!mon)
return -ENOMEM;
@@ -467,3 +491,130 @@ int xe_mem_monitor_init(struct xe_device *xe)
return 0;
}
+
+static ssize_t enabled_write(struct file *file,
+ const char __user *userbuf,
+ size_t count, loff_t *ppos)
+{
+ struct seq_file *s = file->private_data;
+ struct xe_device *xe = s->private;
+ bool enable;
+ int ret;
+
+ ret = kstrtobool_from_user(userbuf, count, &enable);
+ if (ret)
+ return ret;
+
+ if (!enable && xe->mem_monitor)
+ xe_mem_monitor_fini(xe);
+ else if (enable && !xe->mem_monitor)
+ xe_mem_monitor_init(xe);
+
+ return count;
+}
+static int enabled_show(struct seq_file *s, void *data)
+{
+ struct xe_device *xe = s->private;
+
+ seq_printf(s, "%c\n", xe->mem_monitor ? 'Y' : 'N');
+ return 0;
+}
+DEFINE_SHOW_STORE_ATTRIBUTE(enabled);
+
+static int info_show(struct seq_file *s, void *data)
+{
+ struct xe_device *xe = s->private;
+ struct xe_mem_monitor_data *mon = xe->mem_monitor;
+
+ if (!mon)
+ return 1;
+
+ seq_printf(s, "current=%llu MB, delta=%lld MB, EMA=%llu MB mode=%d\n",
+ atomic64_read(&mon->info.current_bytes) >> 20,
+ atomic64_read(&mon->info.delta_signed) >> 20,
+ mon->ema_growth_bytes >> 20,
+ atomic_read(&xe->gpu_mem_mode));
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(info);
+
+static ssize_t mode_write(struct file *file, const char __user *userbuf,
+ size_t count, loff_t *ppos)
+{
+ struct seq_file *s = file->private_data;
+ struct xe_device *xe = s->private;
+ char buf[32];
+ size_t len;
+
+ len = min(count, sizeof(buf) - 1);
+ if (copy_from_user(buf, userbuf, len))
+ return -EFAULT;
+ buf[len] = '\0';
+
+ if (sysfs_streq(buf, "latency"))
+ atomic_set(&xe->gpu_mem_mode, XE_GPU_MEM_MODE_LATENCY);
+ else if (sysfs_streq(buf, "throughput"))
+ atomic_set(&xe->gpu_mem_mode, XE_GPU_MEM_MODE_HIGH_THROUGHPUT);
+
+ return count;
+}
+
+static int mode_show(struct seq_file *s, void *data)
+{
+ struct xe_device *xe = s->private;
+ enum xe_gpu_mem_mode mode = atomic_read(&xe->gpu_mem_mode);
+
+ /*
+ * Print the options, placing brackets around the currently active mode.
+ */
+ switch (mode) {
+ case XE_GPU_MEM_MODE_LATENCY:
+ seq_puts(s, "[latency] throughput\n");
+ break;
+ case XE_GPU_MEM_MODE_HIGH_THROUGHPUT:
+ seq_puts(s, "latency [throughput]\n");
+ break;
+ default:
+ seq_puts(s, "latency throughput\n");
+ break;
+ }
+
+ return 0;
+}
+DEFINE_SHOW_STORE_ATTRIBUTE(mode);
+
+static void xe_mem_monitor_debugfs_unregister(struct drm_device *dev,
+ void *data)
+{
+ struct xe_device *xe = data;
+
+ debugfs_remove(xe->mem_monitor_debugfs_root);
+}
+
+/* Exposes controls through debugfs under the drm root for this device:
+ * /sys/kernel/dri/0/mem_monitor/
+ */
+int xe_mem_monitor_debugfs_register(struct xe_device *xe)
+{
+ struct dentry *root;
+ int ret;
+
+ root = debugfs_create_dir("mem_monitor", xe->drm.debugfs_root);
+ if (IS_ERR(root)) {
+ drm_warn(&xe->drm, "failed to create mem_monitor debug directory");
+ return 1;
+ }
+ root->d_inode->i_private = xe;
+ xe->mem_monitor_debugfs_root = root;
+
+ if (xe_modparam.mem_monitor_enabled) {
+ WARN_ON(!xe->mem_monitor);
+ debugfs_create_file("enabled", 0600, root, xe, &enabled_fops);
+ debugfs_create_file("info", 0400, root, xe, &info_fops);
+ }
+ debugfs_create_file("mode", 0600, root, xe, &mode_fops);
+
+ ret = drmm_add_action_or_reset(&xe->drm,
+ xe_mem_monitor_debugfs_unregister, xe);
+ return ret;
+}
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.h b/drivers/gpu/drm/xe/xe_mem_monitor.h
index 846c0628ed93..c617c10c1a12 100644
--- a/drivers/gpu/drm/xe/xe_mem_monitor.h
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.h
@@ -39,6 +39,8 @@ enum xe_gpu_mem_mode {
#ifdef CONFIG_DRM_XE_MEM_MONITOR
int xe_mem_monitor_init(struct xe_device *xe);
+void xe_mem_monitor_fini(struct xe_device *xe);
+int xe_mem_monitor_debugfs_register(struct xe_device *xe);
/*
* Notify the monitor that GPU allocation activity occurred (called from the
@@ -77,6 +79,15 @@ static inline void xe_mem_monitor_resume(struct xe_device *xe)
{
}
+static inline void xe_mem_monitor_fini(struct xe_device *xe)
+{
+}
+
+static inline int xe_mem_monitor_debugfs_register(struct xe_device *xe)
+{
+ return 0;
+}
+
#endif /* CONFIG_DRM_XE_MEM_MONITOR */
#endif /* _XE_MEM_MONITOR_H_ */
diff --git a/drivers/gpu/drm/xe/xe_module.c b/drivers/gpu/drm/xe/xe_module.c
index a49b0c0f958b..a38776734544 100644
--- a/drivers/gpu/drm/xe/xe_module.c
+++ b/drivers/gpu/drm/xe/xe_module.c
@@ -5,6 +5,7 @@
#include "xe_module.h"
+#include "linux/log2.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/stringify.h>
--
2.34.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 4/4] RFC: ANDROID: drm/xe: Gate mem_monitor wake ups on order 9 external fragmentation
2026-09-07 6:07 [PATCH 0/4] RFC: drm/xe: Dynamic Xe Memory Monitor S Sebinraj
` (2 preceding siblings ...)
2026-09-07 6:07 ` [PATCH 3/4] RFC: ANDROID: drm/xe: provide safe mem_monitor enable toggle via debugfs S Sebinraj
@ 2026-09-07 6:07 ` S Sebinraj
3 siblings, 0 replies; 5+ messages in thread
From: S Sebinraj @ 2026-09-07 6:07 UTC (permalink / raw)
To: intel-xe
Cc: S Sebinraj, Carlos Santa, Erin Park, Ryan Neph, Renato Pereyra,
S Sebinraj
From: S Sebinraj <s.sebinraj@intel.corp-partner.google.com>
xe_mem_monitor_notify_activity() previously woke the polling worker
based only on a raw byte-level delta (ACTIVITY_MIN_DELTA_BYTES), which
doesn't capture whether order 9 (2M) TTM allocations are actually
under any pressure.
Add xe_mem_monitor_zone_order9_free_bytes(), which reads the amount of
order 9 or larger free memory directly from struct zone's free_area
counts.
Add xe_mem_monitor_order9_supply_low(), which walks the populated zones
and returns true if any zone's order 9 free supply has dropped below
an adaptive floor. The floor is mon->max_delta_bytes, the largest
growth sample xe_mem_monitor_work() has ever observed on this device
clamped between the mem_monitor_order9_supply_min_mb and
mem_monitor_order9_supply_max_mb module parameters (in MiB).
Adapting the floor to the largest growth sample actually observed
avoids false positives from a single large but otherwise harmless
allocation burst when overall order 9 supply is still healthy.
The upper clamp (mem_monitor_order9_supply_max_mb) stops a single
anomalously large allocation from permanently pinning the floor too high.
Use this as an additional guard in xe_mem_monitor_notify_activity
both the existing byte-delta check and this free-supply check must
now indicate pressure before the worker is woken.
Test: Tested by running WLEU on a fragmented system and confirmed the
worker is only woken when some populated zone's order-9 free
supply drops below ORDER9_SUPPLY_MIN_PCT of total RAM.
Cc: Carlos Santa <carlos.santa@intel.com>
Cc: Erin Park <erin.park@intel.com>
Cc: Ryan Neph <ryanneph@google.com>
Cc: Renato Pereyra <renatopereyra@google.com>
Signed-off-by: S Sebinraj <s.sebinraj@intel.com>
---
drivers/gpu/drm/xe/xe_mem_monitor.c | 115 +++++++++++++++++++++++++++-
drivers/gpu/drm/xe/xe_mem_monitor.h | 2 +
drivers/gpu/drm/xe/xe_module.c | 22 ++++++
drivers/gpu/drm/xe/xe_module.h | 2 +
4 files changed, 137 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.c b/drivers/gpu/drm/xe/xe_mem_monitor.c
index a34cafe56976..24458af2700c 100644
--- a/drivers/gpu/drm/xe/xe_mem_monitor.c
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.c
@@ -59,9 +59,11 @@
#include "xe_mem_monitor.h"
#include <linux/atomic.h>
+#include <linux/compiler.h>
#include <linux/debugfs.h>
#include <linux/jiffies.h>
#include <linux/minmax.h>
+#include <linux/mmzone.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <linux/string.h>
@@ -129,6 +131,13 @@
* @last_poll_jiffies: jiffies value of the most recent real sample taken
* by xe_mem_monitor_work(), used to detect and decay
* the EMA across skipped (slept-through) intervals
+ * @max_delta_bytes: largest clamped_delta (growth-only, per real
+ * sample) ever observed by xe_mem_monitor_work(),
+ * used by xe_mem_monitor_order9_supply_low() to
+ * adapt its floor upward to this workload's
+ * demonstrated behaviour (clamped between
+ * mem_monitor_order9_supply_min_mb and
+ * mem_monitor_order9_supply_max_mb there).
* @suspended: set while the device is suspended (system or
* runtime PM); xe_mem_monitor_notify_activity() is a
* no-op while set, so that BO eviction/restore
@@ -155,6 +164,7 @@ struct xe_mem_monitor_data {
unsigned long last_activity_jiffies;
unsigned long last_poll_jiffies;
bool is_high_throughput;
+ u64 max_delta_bytes;
atomic_t suspended;
bool was_pending_before_suspend;
@@ -186,6 +196,26 @@ static void xe_mem_monitor_set_latency_mode(struct xe_mem_monitor_data *mon)
trace_xe_gpu_mem_mode_switch(XE_GPU_MEM_MODE_LATENCY, mon->ema_growth_bytes);
}
+/*
+ * Returns the total amount of memory (bytes) available as order-9 (2M) or
+ * larger contiguous blocks in the given zone. Computed directly from
+ * struct zone's free_area[] counts (see <linux/mmzone.h>)
+ */
+static u64 xe_mem_monitor_zone_order9_free_bytes(struct zone *zone)
+{
+ unsigned long free_blocks_suitable = 0;
+ unsigned int order;
+
+ for (order = 9; order < NR_PAGE_ORDERS; order++) {
+ /* nr_free is lockless/diagnostic-only */
+ unsigned long blocks = data_race(zone->free_area[order].nr_free);
+
+ free_blocks_suitable += blocks << (order - 9);
+ }
+
+ return (u64)free_blocks_suitable << (9 + PAGE_SHIFT);
+}
+
static void xe_mem_monitor_work(struct work_struct *work)
{
struct xe_mem_monitor_data *mon =
@@ -255,6 +285,8 @@ static void xe_mem_monitor_work(struct work_struct *work)
ALPHA_RETAINED * mon->ema_growth_bytes)
>> ALPHA_SHIFT;
+ mon->max_delta_bytes = max_t(u64, mon->max_delta_bytes, clamped_delta);
+
atomic64_set(&mon->info.current_bytes, current_bytes);
atomic64_set(&mon->info.delta_signed, delta_signed);
@@ -348,6 +380,58 @@ void xe_mem_monitor_fini(struct xe_device *xe)
drm_info(&xe->drm, "xe_mem_monitor: stopped");
}
+/*
+ * Companion check to ACTIVITY_MIN_DELTA_BYTES for
+ * xe_mem_monitor_notify_activity(): a large byte-level delta doesn't
+ * necessarily mean order-9 (2M) allocations are under any real pressure, so
+ * this looks instead at whether any single populated zone's absolute
+ * order-9-and-larger free supply has dropped below the adaptive floor
+ * clamp_t(mon->max_delta_bytes, mem_monitor_order9_supply_min_mb,
+ * mem_monitor_order9_supply_max_mb) independent of the size of whatever
+ * delta triggered this call, since a single large delta is not itself
+ * evidence that supply is actually running low. Taking mon->max_delta_bytes
+ * (the largest growth sample ever observed on this device, see
+ * xe_mem_monitor_work()) as the basis, clamped between the two module
+ * parameters, means the floor adapts upward to this workload's own
+ * demonstrated behaviour without being able to grow unbounded from a
+ * single anomalous allocation.
+ */
+#if IS_ENABLED(CONFIG_COMPACTION)
+static bool xe_mem_monitor_order9_supply_low(struct xe_mem_monitor_data *mon)
+{
+ struct xe_device *xe = mon->xe;
+ struct pglist_data *pgdat = NODE_DATA(0);
+ u64 min_free_bytes = clamp_t(u64, mon->max_delta_bytes,
+ (u64)xe_modparam.mem_monitor_order9_supply_min_mb << 20,
+ (u64)xe_modparam.mem_monitor_order9_supply_max_mb << 20);
+ unsigned int z;
+
+ for (z = 0; z < MAX_NR_ZONES; z++) {
+ struct zone *zone = &pgdat->node_zones[z];
+
+ if (!populated_zone(zone))
+ continue;
+
+ if (xe_mem_monitor_zone_order9_free_bytes(zone) < min_free_bytes) {
+ drm_dbg(&xe->drm,
+ "xe_mem_monitor: zone=%s order9_free=%llu MB < min=%llu MB\n",
+ zone->name,
+ xe_mem_monitor_zone_order9_free_bytes(zone) >> 20,
+ min_free_bytes >> 20);
+ return true;
+ }
+ }
+
+ return false;
+}
+#else
+static inline bool xe_mem_monitor_order9_supply_low(struct xe_mem_monitor_data *mon)
+{
+ /* No zone data available - don't gate on it. */
+ return true;
+}
+#endif
+
/**
* xe_mem_monitor_notify_activity - notify the monitor of GPU memory changes
* @xe: the Xe device
@@ -367,6 +451,12 @@ void xe_mem_monitor_fini(struct xe_device *xe)
* cross. If the worker is already scheduled (delayed_work_pending()), it
* will read the live counter fresh when it runs and pick up whatever
* accumulated, so there's no need to re-schedule again here.
+ *
+ * As a further guard, even a significant byte-level change is ignored unless
+ * xe_mem_monitor_order9_supply_low() also reports that some populated
+ * zone's order-9-and-larger free supply has dropped below the adaptive
+ * floor described there. A byte-level delta alone doesn't imply 2M
+ * allocations are under any real pressure.
*/
void xe_mem_monitor_notify_activity(struct xe_device *xe)
{
@@ -387,11 +477,14 @@ void xe_mem_monitor_notify_activity(struct xe_device *xe)
if (abs_delta < ACTIVITY_MIN_DELTA_BYTES)
return;
- mon->last_activity_jiffies = jiffies;
-
if (delayed_work_pending(&mon->work))
return;
+ if (!xe_mem_monitor_order9_supply_low(mon))
+ return;
+
+ mon->last_activity_jiffies = jiffies;
+
schedule_delayed_work(&mon->work, 0);
}
@@ -525,15 +618,29 @@ static int info_show(struct seq_file *s, void *data)
{
struct xe_device *xe = s->private;
struct xe_mem_monitor_data *mon = xe->mem_monitor;
+ int mode = atomic_read(&xe->gpu_mem_mode);
+ struct pglist_data *pgdat = NODE_DATA(0);
+ unsigned long long order9_free_bytes = 0;
if (!mon)
return 1;
- seq_printf(s, "current=%llu MB, delta=%lld MB, EMA=%llu MB mode=%d\n",
+ for (int z = 0; z < MAX_NR_ZONES; z++) {
+ struct zone *zone = &pgdat->node_zones[z];
+
+ if (!populated_zone(zone))
+ continue;
+ order9_free_bytes += xe_mem_monitor_zone_order9_free_bytes(zone);
+ }
+
+ seq_printf(s, "current=%llu MB, delta=%lld MB, EMA=%llu MB mode=%s, alpha=0.%03u, "
+ "order9_free_total=%llu MB\n",
atomic64_read(&mon->info.current_bytes) >> 20,
atomic64_read(&mon->info.delta_signed) >> 20,
mon->ema_growth_bytes >> 20,
- atomic_read(&xe->gpu_mem_mode));
+ mode == XE_GPU_MEM_MODE_LATENCY ? "latency" : "throughput",
+ (ALPHA_NUM * 1000U + (1U << (ALPHA_SHIFT - 1))) >> ALPHA_SHIFT,
+ order9_free_bytes >> 20);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(info);
diff --git a/drivers/gpu/drm/xe/xe_mem_monitor.h b/drivers/gpu/drm/xe/xe_mem_monitor.h
index c617c10c1a12..a30fabd5a8ea 100644
--- a/drivers/gpu/drm/xe/xe_mem_monitor.h
+++ b/drivers/gpu/drm/xe/xe_mem_monitor.h
@@ -28,6 +28,8 @@ enum xe_gpu_mem_mode {
#define XE_MEM_MONITOR_DEFAULT_POLL_MS 2000
#define XE_MEM_MONITOR_DEFAULT_DEBOUNCE_COUNT 5
#define XE_MEM_MONITOR_DEFAULT_IDLE_TIMEOUT_MS 10000
+#define XE_MEM_MONITOR_DEFAULT_ORDER9_SUPPLY_MIN_MB 512
+#define XE_MEM_MONITOR_DEFAULT_ORDER9_SUPPLY_MAX_MB 1024
/*
* Minimum enforced value for mem_monitor_poll_ms (no maximum is
diff --git a/drivers/gpu/drm/xe/xe_module.c b/drivers/gpu/drm/xe/xe_module.c
index a38776734544..69825baf100c 100644
--- a/drivers/gpu/drm/xe/xe_module.c
+++ b/drivers/gpu/drm/xe/xe_module.c
@@ -54,6 +54,8 @@ struct xe_modparam xe_modparam = {
.mem_monitor_poll_ms = XE_MEM_MONITOR_DEFAULT_POLL_MS,
.mem_monitor_debounce_count = XE_MEM_MONITOR_DEFAULT_DEBOUNCE_COUNT,
.mem_monitor_idle_timeout_ms = XE_MEM_MONITOR_DEFAULT_IDLE_TIMEOUT_MS,
+ .mem_monitor_order9_supply_min_mb = XE_MEM_MONITOR_DEFAULT_ORDER9_SUPPLY_MIN_MB,
+ .mem_monitor_order9_supply_max_mb = XE_MEM_MONITOR_DEFAULT_ORDER9_SUPPLY_MAX_MB,
#endif
/* the rest are 0 by default */
};
@@ -162,6 +164,26 @@ MODULE_PARM_DESC(mem_monitor_idle_timeout_ms,
"mode after which the monitor's polling worker goes to sleep "
"(woken again on the next allocation) [default="
__stringify(XE_MEM_MONITOR_DEFAULT_IDLE_TIMEOUT_MS) "])");
+
+module_param_named(mem_monitor_order9_supply_min_mb,
+ xe_modparam.mem_monitor_order9_supply_min_mb, uint, 0644);
+MODULE_PARM_DESC(mem_monitor_order9_supply_min_mb,
+ "Minimum order-9-and-larger free supply, in MiB, required per "
+ "populated zone before a significant GPU memory change wakes the "
+ "monitor's polling worker. The effective floor actually used is "
+ "the largest single growth sample ever observed by the worker, "
+ "clamped between this value and mem_monitor_order9_supply_max_mb, "
+ "so it adapts to this workload's own demonstrated behaviour without "
+ "growing unbounded from a single anomalous allocation "
+ "[default=" __stringify(XE_MEM_MONITOR_DEFAULT_ORDER9_SUPPLY_MIN_MB) "])");
+
+module_param_named(mem_monitor_order9_supply_max_mb,
+ xe_modparam.mem_monitor_order9_supply_max_mb, uint, 0644);
+MODULE_PARM_DESC(mem_monitor_order9_supply_max_mb,
+ "Upper bound, in MiB, on the adaptive order-9 free supply floor "
+ "described under mem_monitor_order9_supply_min_mb - caps how high "
+ "a single unusually large growth sample can permanently push that "
+ "floor [default=" __stringify(XE_MEM_MONITOR_DEFAULT_ORDER9_SUPPLY_MAX_MB) "])");
#endif
static int xe_check_nomodeset(void)
diff --git a/drivers/gpu/drm/xe/xe_module.h b/drivers/gpu/drm/xe/xe_module.h
index 24e0b52e48ca..19d0de96e078 100644
--- a/drivers/gpu/drm/xe/xe_module.h
+++ b/drivers/gpu/drm/xe/xe_module.h
@@ -38,6 +38,8 @@ struct xe_modparam {
u32 mem_monitor_poll_ms;
u32 mem_monitor_debounce_count;
u32 mem_monitor_idle_timeout_ms;
+ u32 mem_monitor_order9_supply_min_mb;
+ u32 mem_monitor_order9_supply_max_mb;
#endif
};
--
2.34.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-07 6:47 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-07 6:07 [PATCH 0/4] RFC: drm/xe: Dynamic Xe Memory Monitor S Sebinraj
2026-09-07 6:07 ` [PATCH 1/4] RFC: ANDROID: drm/xe: add adaptive GPU memory growth monitor xe_mem_monitor S Sebinraj
2026-09-07 6:07 ` [PATCH 2/4] RFC: ANDROID: drm/xe: register ttm page alloc vendor hooks internally S Sebinraj
2026-09-07 6:07 ` [PATCH 3/4] RFC: ANDROID: drm/xe: provide safe mem_monitor enable toggle via debugfs S Sebinraj
2026-09-07 6:07 ` [PATCH 4/4] RFC: ANDROID: drm/xe: Gate mem_monitor wake ups on order 9 external fragmentation S Sebinraj
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox