All of lore.kernel.org
 help / color / mirror / Atom feed
From: Matthew Brost <matthew.brost@intel.com>
To: <sashiko-reviews@lists.linux.dev>
Cc: <intel-xe@lists.freedesktop.org>
Subject: Re: [PATCH v3 3/3] drm/xe: Add a minimum age gate to the shrinker
Date: Wed, 21 Oct 2026 15:13:34 -0700	[thread overview]
Message-ID: <atk5DqK1nGPcoSEu@gsse-cloud1.jf.intel.com> (raw)
In-Reply-To: <sashiko-outbox-179512@kernel.org>

On Tue, Oct 20, 2026 at 07:36:05PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> - [High] drm/xe: xe_bo_bump_lru_locked() and xe_bo_shrink_too_young() ignore bulk_move
> - [High] drm/xe: xe_bo_move() bumps LRU and stamps VM on VRAM-to-TT evictions
> - [Medium] drm/xe: xe_bo_init_locked() bumps LRU before setting bo->vm and bulk_move
> - [Medium] drm/xe: xe_bo_shrink_backup_candidate() includes deleted and ghost BOs
> --
> 
> commit 7080502f8c1d2ffa8fd84f4e7f1ff61a34a0e8c9
> Author: Matthew Brost <matthew.brost@intel.com>
> 
> drm/xe: Add a minimum age gate to the shrinker
> 
> This commit adds a per-device shrinker_min_age_ms sysfs attribute and Kconfig
> default to prevent the shrinker from backing up buffer objects that were
> recently moved into TT or bumped on the LRU during exec. It timestamps BOs and
> VMs on LRU bumps and skips LRU priorities whose first shrinkable backup
> candidate is younger than the configured minimum age.
> 
> > diff --git a/drivers/gpu/drm/xe/xe_bo.c b/drivers/gpu/drm/xe/xe_bo.c
> > index bc4801c48fce5..fc8fe68ca0373 100644
> > --- a/drivers/gpu/drm/xe/xe_bo.c
> > +++ b/drivers/gpu/drm/xe/xe_bo.c
> > @@ -1359,9 +1359,114 @@ static int xe_bo_move(struct ttm_buffer_object *ttm_bo, bool evict,
> >  		xe_tt_unmap_sg(xe, ttm_bo->ttm);
> >  	}
> >  
> > +	if (!ret && ttm_bo->resource && ttm_bo->resource->mem_type == XE_PL_TT)
> > +		xe_bo_bump_lru(bo);
> 
> [Severity: High]
> Should xe_bo_move() avoid calling xe_bo_bump_lru() when evict is true or when
> ttm_bo->resource->placement has TTM_PL_FLAG_TEMPORARY set?
> 
> Under VRAM pressure on discrete GPUs, ttm_bo_evict() calls xe_evict_flags(),
> which selects tt_placement (XE_PL_TT) for BOs in VRAM or stolen memory:
> 
> drivers/gpu/drm/xe/xe_bo.c:xe_evict_flags() {
> 	...
> 	switch (tbo->resource->mem_type) {
> 	case XE_PL_VRAM0:
> 	case XE_PL_VRAM1:
> 	case XE_PL_STOLEN:
> 		*placement = tt_placement;
> 		break;
> 	...
> }
> 
> A multihop move in xe_bo_move() can also bounce a BO through a temporary
> XE_PL_TT resource with TTM_PL_FLAG_TEMPORARY set.
> 
> When a single cold VM-private BO is evicted from XE_PL_VRAM0 or XE_PL_VRAM1
> into XE_PL_TT, calling xe_bo_bump_lru() -> xe_bo_bump_lru_locked() updates
> bo->lru_stamp and vm->lru_stamp to jiffies and calls
> ttm_lru_bulk_move_tail(&vm->lru_bulk_move). Because ttm_lru_bulk_move_tail()
> moves the VM's bulk-move ranges across all TTM_NUM_MEM_TYPES and advances
> active eviction cursors via ttm_bulk_move_adjust_cursors(), evicting one BO
> from VRAM moves all remaining cold VRAM BOs of that VM to the tail of the
> VRAM LRU so hotter VMs are evicted instead. It also stamps the evicted BO and
> all XE_PL_TT BOs of that idle VM as freshly used, protecting them from the
> shrinker for min_age_ms.
> 

This is correct. Will fix.

> > +
> >  	return ret;
> >  }
> >  
> > +/**
> > + * xe_bo_bump_lru_locked() - Bump a BO on the LRU and stamp its last use
> > + * @bo: The buffer object, reserved by the caller.
> > + *
> > + * Move @bo to the tail of its LRU and record the time for the shrinker's
> > + * minimum age gate. A BO private to a VM is anchored inside that VM's
> > + * bulk-move range, so moving it alone would only reorder it within the
> > + * range, leaving it wherever the range sits on the LRU. Move the whole
> > + * range to the tail and stamp the VM as well, matching xe_vm_bump_lru().
> > + * The caller must hold the device LRU lock.
> > + */
> > +void xe_bo_bump_lru_locked(struct xe_bo *bo)
> > +{
> > +	struct xe_vm *vm = bo->vm;
> > +
> > +	if (vm) {
> > +		WRITE_ONCE(vm->lru_stamp, jiffies);
> > +		ttm_lru_bulk_move_tail(&vm->lru_bulk_move);
> > +	}
> > +
> > +	WRITE_ONCE(bo->lru_stamp, jiffies);
> > +	ttm_bo_move_to_lru_tail(&bo->ttm);
> > +}
> 
> [Severity: Medium]
> What happens when a VM-private user BO is initially created in XE_PL_TT via
> __xe_bo_create_locked() -> xe_bo_init_locked() -> ttm_bo_init_reserved() ->
> ttm_bo_validate() -> xe_bo_move()?
> 
> In xe_bo_init_locked(), ttm_bo_init_reserved() runs before
> ttm_bo_set_bulk_move():
> 
> drivers/gpu/drm/xe/xe_bo.c:xe_bo_init_locked() {
> 	...
> 	err = ttm_bo_init_reserved(&xe->ttm, &bo->ttm, type,
> 				   placement, alignment,
> 				   &ctx, NULL, resv, xe_ttm_bo_destroy);
> 	...
> 	bo->created = true;
> 	if (bulk)
> 		ttm_bo_set_bulk_move(&bo->ttm, bulk);
> 	else
> 		ttm_bo_move_to_lru_tail_unlocked(&bo->ttm);
> 
> 	return bo;
> }
> 
> And __xe_bo_create_locked() only sets bo->vm = vm after xe_bo_init_locked()
> returns.
> 
> When xe_bo_move() calls xe_bo_bump_lru() during ttm_bo_init_reserved(), both
> bo->vm and bo->ttm.bulk_move are still NULL, so xe_bo_bump_lru_locked() only
> stamps bo->lru_stamp and moves the new BO to the LRU tail without updating
> vm->lru_stamp or calling ttm_lru_bulk_move_tail(&vm->lru_bulk_move).
> 
> Immediately afterward in xe_bo_init_locked(), ttm_bo_set_bulk_move() calls
> ttm_lru_bulk_move_add() -> ttm_lru_bulk_move_pos_tail(), which executes
> list_move(&res->lru.link, &pos->last->lru.link) if the VM already has BOs in
> (XE_PL_TT, XE_BO_PRIORITY_NORMAL). That pulls the newly stamped young BO
> backward from the LRU tail to the end of the VM's existing bulk-move range.
> If that range sits near the head of the XE_PL_TT LRU ahead of older BOs from
> other VMs, the shrinker can encounter the young BO near the head of the list
> and call ttm_bo_lru_cursor_skip_priority(), skipping older cold BOs behind
> the VM's range.

I think it is temporarily not in the bulk-move list, but as soon as
xe_bo_init_locked() is called, it is moved into the bulk-move list. The
BO should end up at the tail of the bulk-move list after
ttm_bo_set_bulk_move(), so this is a non-issue. 

> 
> [ ... ]
> 
> > +/**
> > + * xe_bo_shrink_backup_candidate() - Whether the shrinker may back up a BO
> > + * @bo: The TTM buffer object, reserved by the caller.
> > + *
> > + * A BO is a backup candidate if backing it up is the only way to reclaim it.
> > + * BOs advised DONTNEED, which sit at XE_BO_PRIORITY_LOW, and purgeable ones
> > + * are not: their pages are simply dropped, which is cheap, and they are
> > + * accounted to the purgeable shrinker. Neither are BOs whose tt isn't
> > + * TTM_TT_FLAG_EXTERNAL_MAPPABLE, which xe_bo_shrink() rejects outright.
> > + *
> > + * Return: true if @bo would be backed up by a non-purge shrink pass.
> > + */
> > +bool xe_bo_shrink_backup_candidate(struct ttm_buffer_object *bo)
> > +{
> > +	struct xe_ttm_tt *xe_tt;
> > +
> > +	if (!bo->ttm || !(bo->ttm->page_flags & TTM_TT_FLAG_EXTERNAL_MAPPABLE) ||
> > +	    bo->priority <= XE_BO_PRIORITY_LOW)
> > +		return false;
> > +
> > +	xe_tt = container_of(bo->ttm, struct xe_ttm_tt, ttm);
> > +
> > +	return !xe_tt->purgeable;
> > +}
> 
> [Severity: Medium]
> Could xe_bo_shrink_backup_candidate() misclassify non-xe ghost BOs or deleted
> BOs awaiting delayed_delete that xe_bo_shrink() purges rather than backs up?
> 
> In xe_bo_shrink(), both non-xe BOs and xe_bo objects whose GEM refcount has
> reached zero are purged via xe_bo_shrink_purge():
> 
> drivers/gpu/drm/xe/xe_bo.c:xe_bo_shrink() {
> 	...
> 	if (!xe_bo_is_xe_bo(bo) || !xe_bo_get_unless_zero(xe_bo))
> 		return xe_bo_shrink_purge(ctx, bo, scanned);
> 	...
> }
> 
> When a recently stamped user BO is freed while busy (or with
> want_init_on_free() enabled) via xe_gem_object_free() -> ttm_bo_fini() ->
> ttm_bo_release(), its GEM refcount is zero and bo->deleted is true while it
> remains on man->lru[XE_BO_PRIORITY_NORMAL] awaiting delayed_delete. Once its
> fences signal before the delayed_delete worker runs, ttm_bo_shrink_suitable()
> returns true. Because neither xe_bo_shrink_backup_candidate() nor
> xe_bo_shrink_too_young() checks bo->deleted or kref_read(&bo->base.refcount),
> xe_bo_shrink_too_young() treats the freed BO as a young backup candidate,
> preventing __xe_shrinker_walk() from purging it and causing both
> __xe_shrinker_walk() and xe_shrinker_lru_young() to skip the rest of the
> priority via ttm_bo_lru_cursor_skip_priority().
> 
> Additionally, because xe_bo_shrink_backup_candidate() does not check
> xe_bo_is_xe_bo(bo), if xe_shrinker_lru_young() encounters a non-xe TTM ghost
> BO at the head of a priority list, it treats the ghost BO as that priority's
> first backup candidate and immediately returns false (since
> xe_bo_shrink_too_young() returns false for !xe_bo_is_xe_bo(bo)) without
> inspecting the first real backup candidate in that priority.
> 

xe_bo_shrink_backup_candidate should likely check !xe_bo_is_xe_bo ||
!kref_read(xe_bo) and return false for both.

> > +
> > +/**
> > + * xe_bo_shrink_too_young() - Whether a BO was used too recently to shrink
> > + * @bo: The TTM buffer object, reserved by the caller.
> > + * @min_age: Minimum age in jiffies. 0 disables the check.
> > + *
> > + * The age is taken from the later of the BO's own &xe_bo.lru_stamp and, for
> > + * user BOs private to a VM, the VM's &xe_vm.lru_stamp.
> > + *
> > + * BOs that the shrink walk would not back up anyway are never considered
> > + * young: non-xe BOs and everything rejected by
> > + * xe_bo_shrink_backup_candidate(). Neither are BOs in XE_PL_SYSTEM: they are
> > + * not GPU mapped, hence not part of a working set, and must be moved back to
> > + * TT, refreshing the stamp, before use.
> > + *
> > + * Return: true if @bo was touched less than @min_age jiffies ago.
> > + */
> > +bool xe_bo_shrink_too_young(struct ttm_buffer_object *bo, unsigned long min_age)
> > +{
> > +	struct xe_bo *xe_bo;
> > +	unsigned long stamp;
> > +
> > +	if (!min_age || !xe_bo_is_xe_bo(bo) ||
> > +	    bo->resource->mem_type == XE_PL_SYSTEM ||
> > +	    !xe_bo_shrink_backup_candidate(bo))
> > +		return false;
> > +
> > +	xe_bo = ttm_to_xe_bo(bo);
> > +	stamp = READ_ONCE(xe_bo->lru_stamp);
> > +	if (xe_bo->vm && xe_bo_is_user(xe_bo)) {
> > +		unsigned long vm_stamp = READ_ONCE(xe_bo->vm->lru_stamp);
> > +
> > +		if (vm_stamp && (!stamp || time_after(vm_stamp, stamp)))
> > +			stamp = vm_stamp;
> > +	}
> > +
> > +	return stamp && time_before(jiffies, stamp + min_age);
> > +}
> 
> [Severity: High]
> Should xe_bo_bump_lru_locked() and xe_bo_shrink_too_young() check
> bo->ttm.bulk_move instead of bo->vm?
> 


The check should be...

if (xe_bo->vm && bulk..

> There are two cases where a user BO has xe_bo->vm != NULL and
> XE_BO_FLAG_USER set, but bo->ttm.bulk_move is NULL:
> 
> 1. In __xe_bo_create_locked(), private user BOs in a fault-mode VM
> (xe_vm_in_fault_mode(vm)) are created with bulk = NULL while bo->vm = vm is
> still assigned:
> 
> drivers/gpu/drm/xe/xe_bo.c:__xe_bo_create_locked() {
> 	...
> 	bo = xe_bo_init_locked(xe, bo, tile, vm ? xe_vm_resv(vm) : NULL,
> 			       vm && !xe_vm_in_fault_mode(vm) &&
> 			       flags & XE_BO_FLAG_USER ?
> 			       &vm->lru_bulk_move : NULL, size,
> 			       cpu_caching, type, flags, NULL, exec);
> 	...
> 	bo->vm = vm;
> }
> 
> 2. In xe_gem_object_close(), closing the GEM handle of a private user BO that
> remains mapped in a non-fault-mode VM clears bo->ttm.bulk_move while leaving
> bo->vm set:
> 
> drivers/gpu/drm/xe/xe_bo.c:xe_gem_object_close() {
> 	struct xe_bo *bo = gem_to_xe_bo(obj);
> 
> 	if (bo->vm && !xe_vm_in_fault_mode(bo->vm)) {
> 		xe_assert(xe_bo_device(bo), xe_bo_is_user(bo));
> 
> 		xe_bo_lock(bo, false);
> 		ttm_bo_set_bulk_move(&bo->ttm, NULL);
> 		xe_bo_unlock(bo);

This is probably not correct in current code.

BOs can outlive their handles, so I'm reading this correctly after a
handle closes for private user BO, we never bump the LRU.

Matt

> 	}
> }
> 
> Non-user VM BOs such as XE_BO_FLAG_PAGETABLE BOs also have bo->vm == vm and
> bo->ttm.bulk_move == NULL.
> 
> Whenever any BO of that VM moves into XE_PL_TT (calling xe_bo_move() ->
> xe_bo_bump_lru() -> xe_bo_bump_lru_locked()) or when xe_vm_bump_lru() runs on
> exec, vm->lru_stamp is updated to jiffies. However,
> ttm_lru_bulk_move_tail(&vm->lru_bulk_move) only moves BOs attached to
> vm->lru_bulk_move, and the loop in xe_vm_bump_lru() skips private BOs because
> drm_gpuvm_is_extobj() is false when obj->resv == drm_gpuvm_resv(&vm->gpuvm).
> 
> As a result, detached or fault-mode user BOs with bo->ttm.bulk_move == NULL
> are never moved to the LRU tail and stay at the oldest head of
> man->lru[XE_BO_PRIORITY_NORMAL], yet xe_bo_shrink_too_young() gives them the
> fresh vm->lru_stamp. When xe_shrinker_lru_young() or __xe_shrinker_walk() in
> drivers/gpu/drm/xe/xe_shrinker.c inspects the head of
> man->lru[XE_BO_PRIORITY_NORMAL], it sees the un-moved cold BO at the head as
> young and calls ttm_bo_lru_cursor_skip_priority(&curs), skipping the entire
> XE_BO_PRIORITY_NORMAL list across all VMs on the device.
> 
> [ ... ]
> 
> -- 
> Sashiko AI review · https://sashiko.dev/#/patchset/20261020190840.854525-1-matthew.brost@intel.com?part=3

  reply	other threads:[~2026-10-21 22:14 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-20 19:08 [PATCH v3 0/3] Add a minimum age gate to the shrinker Matthew Brost
2026-10-20 19:08 ` [PATCH v3 1/3] drm/xe: Tie BO TTM LRU priority to DONTNEED/WILLNEED madvise state Matthew Brost
2026-10-20 19:08 ` [PATCH v3 2/3] drm/ttm: Allow skipping to the next priority in the bo LRU cursor Matthew Brost
2026-10-20 19:08 ` [PATCH v3 3/3] drm/xe: Add a minimum age gate to the shrinker Matthew Brost
2026-10-20 19:36   ` sashiko-bot
2026-10-21 22:13     ` Matthew Brost [this message]
2026-10-20 20:20 ` ✓ CI.KUnit: success for " Patchwork
2026-10-20 20:57 ` ✓ Xe.CI.BAT: " Patchwork
2026-10-21  2:24 ` ✓ Xe.CI.FULL: " 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=atk5DqK1nGPcoSEu@gsse-cloud1.jf.intel.com \
    --to=matthew.brost@intel.com \
    --cc=intel-xe@lists.freedesktop.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.