drm/ttm: Hook up a cgroup-aware reclaim callback for the dmem controller

Add ttm_bo_evict_cgroup() to evict buffer objects charged to a specific
dmem cgroup pool from a resource manager's LRU until a byte target is
met.  Add ttm_resource_manager_set_dmem_region() to associate a dmem
cgroup region with a resource manager; drivers supply their own
dmem_cgroup_ops with ttm_resource_manager_dmem_reclaim as the reclaim
function and the manager pointer as reclaim_priv in the dmem_cgroup_init
to wire up TTM eviction as the reclaim callback.

The eviction context is interruptible; signals abort the operation and
propagate back through the write() syscall.

Introduce a new mode for the bo LRU walker so that sleeping locks
can be taken. This can be used when the caller doesn't hold any
previous dma_resv locks, and where it intends to hold at most
one lock at a time.

Like the rest of the TTM eviction this should sooner than later
be converted to full WW transactions.

v3:
- Fix ttm_resource_manager_set_dmem_region() storing an error pointer
  in man->cg unconditionally. (Sashiko-bot)
- Fix kernel-doc function name format for ttm_bo_evict_cgroup() and
  ttm_resource_manager_set_dmem_region().

v5:
- Rebased on the introduction of struct dmem_cgroup_init.
- Handle NULL region in ttm_resource_manager_set_dmem_region() to clear
  the reclaim callback, preventing use-after-free when the manager is
  torn down while the dmem region outlives it. (Sashiko-bot)
- Return 0 on any progress (even partial eviction), -ENOSPC only when
  nothing was freed; fixes callers that expected 0 on partial success.
- Document that the reclaim callback should return 0 if some progress
  was made, -ENOSPC if no progress at all, or another error for fatal
  failures.

v8:
- Fix ttm_resource_manager_set_dmem_region() using IS_ERR_OR_NULL(),
  which skipped the assignment for a NULL region and thus never
  cleared man->cg. Use IS_ERR() so that a NULL region detaches the
  region as the kernel-doc and the v5 changelog intended. (Sashiko-bot)

v9:
- Don't leak cgroup charges for bos that may have survived dmemcg
  region fini.
- Drop the misleading "Capture size before eviction in case res is
  cleared" comment in ttm_bo_evict_cb(). (Maarten Lankhorst)

Assisted-by: GitHub_Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
Reviewed-by: Maarten Lankhorst <maarten.lankhorst@linux.intel.com> #v7
Tested-by: Thadeu Lima de Souza Cascardo <cascardo@igalia.com>
Link: https://patch.msgid.link/20260725100036.2372-5-thomas.hellstrom@linux.intel.com
Signed-off-by: Maarten Lankhorst <dev@lankhorst.se>
This commit is contained in:
Thomas Hellström 2026-07-25 12:00:34 +02:00 committed by Maarten Lankhorst
parent 747c4bb450
commit 425a783e1b
5 changed files with 166 additions and 5 deletions

View File

@ -629,6 +629,7 @@ static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object *
struct ttm_bo_evict_walk *evict_walk =
container_of(walk, typeof(*evict_walk), walk);
struct dmem_cgroup_pool_state *limit_pool, *ancestor = NULL;
s64 bo_size = bo->base.size;
bool evict_valuable;
s64 lret;
@ -672,6 +673,12 @@ static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object *
if (!evict_valuable)
return 0;
/*
* evict_walk->place is NULL in cgroup drain mode. Drivers'
* eviction_valuable() callbacks must handle a NULL place, treating it
* as "any placement": the TTM base implementation already does so via
* ttm_resource_intersects().
*/
if (bo->pin_count || !bo->bdev->funcs->eviction_valuable(bo, evict_walk->place))
return 0;
@ -687,13 +694,17 @@ static s64 ttm_bo_evict_cb(struct ttm_lru_walk *walk, struct ttm_buffer_object *
goto out;
evict_walk->evicted++;
if (evict_walk->res)
if (evict_walk->res) {
lret = ttm_bo_alloc_at_place(evict_walk->evictor,
evict_walk->place, false,
evict_walk->res,
evict_walk->alloc_state);
if (lret == 0)
return 1;
if (lret == 0)
return 1;
} else {
/* Cgroup drain: return bytes freed for byte-denominated progress. */
return bo_size;
}
out:
/* Errors that should terminate the walk. */
if (lret == -ENOSPC)
@ -775,6 +786,86 @@ static int ttm_bo_evict_alloc(struct ttm_device *bdev,
return 0;
}
/**
* ttm_bo_evict_cgroup() - Evict buffer objects charged to a specific cgroup.
* @bdev: The TTM device.
* @man: The resource manager whose LRU to walk.
* @limit_pool: The cgroup pool state whose members should be evicted.
* @target_bytes: Number of bytes to free.
* @ctx: The TTM operation context.
*
* Walk the LRU of @man and evict buffer objects that are charged to the
* cgroup identified by @limit_pool, until at least @target_bytes have been
* freed. Mirrors the two-pass (trylock -> sleeping-lock, low-watermark)
* strategy used by ttm_bo_evict_alloc().
*
* Return: >= @target_bytes on full success, 0..target_bytes-1 if partial,
* negative error code on fatal error.
*/
s64 ttm_bo_evict_cgroup(struct ttm_device *bdev,
struct ttm_resource_manager *man,
struct dmem_cgroup_pool_state *limit_pool,
s64 target_bytes,
struct ttm_operation_ctx *ctx)
{
struct ttm_bo_evict_walk evict_walk = {
.walk = {
.ops = &ttm_evict_walk_ops,
.arg = { .ctx = ctx },
},
.alloc_state = &(struct ttm_bo_alloc_state) {
.limit_pool = limit_pool,
.in_evict = true,
},
/* place, evictor, res left NULL: selects cgroup drain mode */
};
s64 lret, pass;
evict_walk.walk.arg.trylock_only = true;
lret = ttm_lru_walk_for_evict(&evict_walk.walk, bdev, man, target_bytes);
if (lret < 0 || lret >= target_bytes)
return lret;
/* Second pass: also evict BOs at the low watermark. */
if (evict_walk.hit_low) {
evict_walk.try_low = true;
pass = ttm_lru_walk_for_evict(&evict_walk.walk, bdev, man,
target_bytes - lret);
if (pass < 0)
return pass;
lret += pass;
if (lret >= target_bytes)
return lret;
}
/* Full sleeping-lock pass for remaining target. */
evict_walk.try_low = evict_walk.hit_low = false;
evict_walk.walk.arg.trylock_only = false;
retry:
evict_walk.walk.arg.sleeping_lock = true;
do {
evict_walk.evicted = 0;
pass = ttm_lru_walk_for_evict(&evict_walk.walk, bdev, man,
target_bytes - lret);
if (pass < 0) {
lret = pass;
goto out;
}
lret += pass;
} while (lret < target_bytes && evict_walk.evicted);
/* One more attempt if we hit the low limit during sleeping-lock pass. */
if (lret < target_bytes && evict_walk.hit_low && !evict_walk.try_low) {
evict_walk.try_low = true;
goto retry;
}
out:
return lret;
}
EXPORT_SYMBOL(ttm_bo_evict_cgroup);
/**
* ttm_bo_pin - Pin the buffer object.
* @bo: The buffer object to pin

View File

@ -999,7 +999,8 @@ __ttm_bo_lru_cursor_next(struct ttm_bo_lru_cursor *curs)
bo = res->bo;
if (ttm_lru_walk_trylock(curs, bo))
bo_locked = true;
else if (!arg->ticket || arg->ctx->no_wait_gpu || arg->trylock_only)
else if ((!arg->ticket && !arg->sleeping_lock) || arg->ctx->no_wait_gpu ||
arg->trylock_only)
continue;
if (!ttm_bo_get_unless_zero(bo)) {

View File

@ -456,7 +456,7 @@ void ttm_resource_free(struct ttm_buffer_object *bo, struct ttm_resource **res)
man = ttm_manager_type(bo->bdev, (*res)->mem_type);
man->func->free(man, *res);
*res = NULL;
if (man->cg)
if (pool)
dmem_cgroup_uncharge(pool, bo->base.size);
}
EXPORT_SYMBOL(ttm_resource_free);
@ -972,3 +972,55 @@ void ttm_resource_manager_create_debugfs(struct ttm_resource_manager *man,
#endif
}
EXPORT_SYMBOL(ttm_resource_manager_create_debugfs);
/**
* ttm_resource_manager_dmem_reclaim() - dmem cgroup reclaim callback for TTM
* resource managers.
* @pool: The dmem cgroup pool state for the cgroup being reclaimed.
* @target_bytes: Number of bytes to try to free.
* @priv: The &ttm_resource_manager pointer, passed as @init.reclaim_priv to
* dmem_cgroup_register_region().
*
* Drivers should use this as the @reclaim member of their own
* &struct dmem_cgroup_ops, with the &ttm_resource_manager pointer as
* @init.reclaim_priv.
*
* Return: 0 if some memory was freed, -ENOSPC if nothing was freed, or
* another negative error code on fatal failure.
*/
int ttm_resource_manager_dmem_reclaim(struct dmem_cgroup_pool_state *pool,
u64 target_bytes, void *priv)
{
struct ttm_resource_manager *man = priv;
struct ttm_operation_ctx ctx = { .interruptible = true };
s64 freed;
freed = ttm_bo_evict_cgroup(man->bdev, man, pool, target_bytes, &ctx);
if (freed < 0)
return freed;
return freed > 0 ? 0 : -ENOSPC;
}
EXPORT_SYMBOL(ttm_resource_manager_dmem_reclaim);
/**
* ttm_resource_manager_set_dmem_region() - Associate a dmem cgroup region with a
* resource manager.
* @man: The resource manager.
* @region: The dmem cgroup region to associate, may be NULL or IS_ERR().
*
* When @region is valid, stores it in @man->cg so that TTM can look up the
* associated pool during charging and eviction-target selection. When
* @region is %NULL, clears @man->cg to detach the region before teardown.
* An IS_ERR() @region is ignored, leaving @man->cg unchanged.
* The reclaim callback must be wired up using ttm_resource_manager_dmem_reclaim()
* in the driver's own &struct dmem_cgroup_ops, with the manager pointer as
* @init.reclaim_priv.
*/
void ttm_resource_manager_set_dmem_region(struct ttm_resource_manager *man,
struct dmem_cgroup_region *region)
{
if (!IS_ERR(region))
man->cg = region;
}
EXPORT_SYMBOL(ttm_resource_manager_set_dmem_region);

View File

@ -226,6 +226,11 @@ struct ttm_lru_walk_arg {
struct ww_acquire_ctx *ticket;
/** @trylock_only: Only use trylock for locking. */
bool trylock_only;
/**
* @sleeping_lock: Use sleeping locks even with %NULL @ticket.
* @trylock_only has precedence over this field.
*/
bool sleeping_lock;
};
/**
@ -431,6 +436,11 @@ void ttm_bo_unpin(struct ttm_buffer_object *bo);
int ttm_bo_evict_first(struct ttm_device *bdev,
struct ttm_resource_manager *man,
struct ttm_operation_ctx *ctx);
s64 ttm_bo_evict_cgroup(struct ttm_device *bdev,
struct ttm_resource_manager *man,
struct dmem_cgroup_pool_state *limit_pool,
s64 target_bytes,
struct ttm_operation_ctx *ctx);
int ttm_bo_access(struct ttm_buffer_object *bo, unsigned long offset,
void *buf, int len, int write);
vm_fault_t ttm_bo_vm_reserve(struct ttm_buffer_object *bo,

View File

@ -39,6 +39,7 @@
struct dentry;
struct dmem_cgroup_device;
struct dmem_cgroup_region;
struct drm_printer;
struct ttm_device;
struct ttm_resource_manager;
@ -481,6 +482,12 @@ void ttm_resource_manager_init(struct ttm_resource_manager *man,
struct ttm_device *bdev,
uint64_t size);
void ttm_resource_manager_set_dmem_region(struct ttm_resource_manager *man,
struct dmem_cgroup_region *region);
int ttm_resource_manager_dmem_reclaim(struct dmem_cgroup_pool_state *pool,
u64 target_bytes, void *priv);
int ttm_resource_manager_evict_all(struct ttm_device *bdev,
struct ttm_resource_manager *man);