前置阅读: 01 — TTM 内存管理基础 (TTM 内存管理基础)
本文是 TTM Eviction 系列的第一篇,建立全局视角。后续章节:
- 4.8.2 – Eviction 选择策略:LRU 与候选筛选
- 4.8.3 – Eviction 搬迁执行:BO Move 路径
- 4.8.4 – [Eviction Fence 通知机制] 审核中…
- 4.8.5 – [AMD AMDGPU 驱动中的 Eviction 应用案例详析] 审核中…
1. 核心问题:为什么需要 Eviction?
GPU 显存 (VRAM) 是有限的稀缺资源。当多个应用同时使用 GPU 时,VRAM 的总需求量往往远超物理容量。TTM eviction 机制要回答一个核心问题:
VRAM 不够用了,谁该被踢出去,怎么踢,踢到哪?
这与操作系统的内存页面回收 (page reclaim) 本质相同----当物理内存不足时,OS 选择部分页面换出到 swap。TTM eviction 就是 GPU 世界的 “swap out”:
| 概念 | Linux MM | TTM |
|---|---|---|
| 稀缺资源 | 物理内存 (RAM) | 显存 (VRAM) |
| 管理对象 | struct page | ttm_buffer_object |
| 退化目标 | swap 分区/文件 | GTT (系统内存) 或 SYSTEM |
| 选择策略 | LRU / active-inactive | LRU per resource_manager |
| 异步机制 | kswapd / writeback | dma_fence / SDMA copy |
2. Eviction 全景图:四步走
整个 eviction 流程按 「谁触发 -> 怎么选 -> 怎么移 -> 怎么通知」 四步走:
+-------------------------------------------------------------+
| TTM Eviction 四步走 |
+-------------------------------------------------------------+
| |
| Step 1: 触发 --- 谁说 "VRAM 不够了"? |
| | ttm_bo_alloc_resource() 分配失败 |
| | |
| v |
| Step 2: 选择 --- 选哪个 BO 踢出去? |
| | LRU 遍历 + eviction_valuable 否决 |
| | (详见 4.8.2) |
| v |
| Step 3: 搬迁 --- 数据怎么从 VRAM -> GTT/SYSTEM? |
| | evict_flags -> move -> SDMA blit |
| | (详见 4.8.3) |
| v |
| Step 4: 通知 --- 怎么告诉使用者 "你被踢了"? |
| eviction fence + enable_signaling |
| (详见 4.8.4) |
| |
+-------------------------------------------------------------+
本文聚焦 Step 1:触发流程,以及四步之间如何串联。
3. Step 1: 触发点 – 谁说"VRAM 不够了"?
3.1 触发的调用链
Eviction 不是由一个后台线程主动扫描触发的(不同于 Linux 的 kswapd),而是 按需触发 (on-demand)----当某个 BO 需要 VRAM 但分配失败时,当场发起 eviction:
用户态: amdgpu ioctl (GEM_CREATE / CS)
|
v
amdgpu_bo_create() <-- 创建 BO
-> ttm_bo_init_reserved()
-> ttm_bo_validate() <-- 验证/放置 BO
-> ttm_bo_alloc_resource() <-- 核心分配入口
|
+-- force_space = false (第一轮)
| -> ttm_resource_alloc() <-- 尝试直接分配
| -> 成功? -> 返回
| -> -ENOSPC? -> 继续
|
-- force_space = true (第二轮)
-> ttm_resource_alloc() <-- 再试一次
-> -ENOSPC?
-> ttm_bo_evict_alloc() <-- 启动 eviction!
-> ttm_lru_walk_for_evict()
-> ttm_bo_evict_cb()
-> ttm_bo_evict()
关键设计:ttm_bo_mem_space() 会调用 ttm_bo_alloc_resource() 两轮:
/* drivers/gpu/drm/ttm/ttm_bo.c */
int ttm_bo_mem_space(struct ttm_buffer_object *bo,
struct ttm_placement *placement,
struct ttm_resource **res,
struct ttm_operation_ctx *ctx)
{
bool force_space = false;
int ret;
do {
ret = ttm_bo_alloc_resource(bo, placement, ctx,
force_space, res);
force_space = !force_space; /* 第二轮开启强制模式 */
} while (ret == -ENOSPC && force_space);
return ret;
}
| 轮次 | force_space | 行为 |
|---|---|---|
| 第 1 轮 | false | 只尝试分配,不驱逐任何 BO。跳过标记为 TTM_PL_FLAG_FALLBACK 的候选域 |
| 第 2 轮 | true | 允许驱逐。跳过标记为 TTM_PL_FLAG_DESIRED 的候选域,进入 eviction 路径 |
这个两轮设计配合 TTM_PL_FLAG_DESIRED / TTM_PL_FLAG_FALLBACK 标志,实现了"先试首选域,不行再用后备域"的优雅降级。
3.2 ttm_bo_alloc_resource() – 分配与驱逐的统一入口
这是整个触发逻辑的核心函数:
/* drivers/gpu/drm/ttm/ttm_bo.c (简化) */
static int ttm_bo_alloc_resource(struct ttm_buffer_object *bo,
struct ttm_placement *placement,
struct ttm_operation_ctx *ctx,
bool force_space,
struct ttm_resource **res)
{
struct ttm_device *bdev = bo->bdev;
struct ww_acquire_ctx *ticket;
int i, ret;
ticket = dma_resv_locking_ctx(bo->base.resv);
/* 预留 fence 槽位,后续 move 操作需要往 dma_resv 中加 fence */
ret = dma_resv_reserve_fences(bo->base.resv, TTM_NUM_MOVE_FENCES);
if (unlikely(ret))
return ret;
/* 遍历 placement 中的每个候选域 */
for (i = 0; i < placement->num_placement; ++i) {
const struct ttm_place *place = &placement->placement[i];
struct ttm_resource_manager *man;
bool may_evict;
man = ttm_manager_type(bdev, place->mem_type);
if (!man || !ttm_resource_manager_used(man))
continue;
/* 根据 force_space 决定跳过 DESIRED 还是 FALLBACK */
if (place->flags & (force_space ? TTM_PL_FLAG_DESIRED :
TTM_PL_FLAG_FALLBACK))
continue;
may_evict = (force_space && place->mem_type != TTM_PL_SYSTEM);
/* 1. 先尝试直接分配 */
ret = ttm_resource_alloc(bo, place, res, ...);
if (ret) {
if (ret != -ENOSPC && ret != -EAGAIN)
return ret;
if (!may_evict)
continue; /* 第一轮不允许驱逐,跳到下一个域 */
/* 2. 分配失败 + 允许驱逐 -> 启动 eviction */
ret = ttm_bo_evict_alloc(bdev, man, place, bo, ctx,
ticket, res, limit_pool);
if (ret == -EBUSY)
continue; /* 这个域驱逐也腾不出来,试下一个 */
if (ret)
return ret;
}
/* 3. 分配成功后,添加流水线驱逐 fence */
ret = ttm_bo_add_pipelined_eviction_fences(bo, man, ...);
if (unlikely(ret)) {
ttm_resource_free(bo, res);
if (ret == -EBUSY)
continue;
return ret;
}
return 0; /* 成功! */
}
return -ENOSPC; /* 所有候选域都失败了 */
}
函数的核心逻辑用流程图表示:
对每个候选 placement[i]:
|
+-- 资源管理器是否可用? --- 否 -> 跳过
|
+-- 被 DESIRED/FALLBACK 过滤? --- 是 -> 跳过
|
+-- ttm_resource_alloc() 直接分配
| +-- 成功 -> 添加 pipelined eviction fences -> 返回 0
| '-- -ENOSPC:
| +-- may_evict = false -> 跳过 (第一轮不驱逐)
| '-- may_evict = true -> ttm_bo_evict_alloc()
| +-- 成功 -> 添加 pipelined eviction fences -> 返回 0
| +-- -EBUSY -> 跳过 (这个域没戏)
| '-- 其他错误 -> 返回错误
|
所有域尝试完毕 -> 返回 -ENOSPC
4. 关键概念:Placement(放置策略)
4.1 数据结构
每个 BO 在创建时声明自己 可以住在哪些内存域,优先级从高到低排列:
struct ttm_placement {
unsigned num_placement; /* 候选域的数量 */
const struct ttm_place *placement; /* 候选域数组(按优先级排列)*/
};
struct ttm_place {
unsigned fpfn; /* 起始页帧号限制 (0 = 无限制) */
unsigned lpfn; /* 结束页帧号限制 (0 = 无限制) */
uint32_t mem_type; /* TTM_PL_VRAM / TTM_PL_TT / TTM_PL_SYSTEM */
uint32_t flags; /* TTM_PL_FLAG_CONTIGUOUS 等 */
};
4.2 AMD 的 Placement 域
AMDGPU 定义了以下 memory domain,通过 amdgpu_bo_placement_from_domain() 将用户态的 domain flags 转化为 ttm_placement:
| 用户态 Domain Flag | TTM mem_type | 含义 |
|---|---|---|
AMDGPU_GEM_DOMAIN_VRAM | TTM_PL_VRAM | GPU 显存,性能最高 |
AMDGPU_GEM_DOMAIN_GTT | TTM_PL_TT | 通过 GART 映射的系统内存 |
AMDGPU_GEM_DOMAIN_CPU | TTM_PL_SYSTEM | 纯系统内存,GPU 不可直接访问 |
AMDGPU_GEM_DOMAIN_GDS | AMDGPU_PL_GDS | 片上 Global Data Share |
AMDGPU_GEM_DOMAIN_GWS | AMDGPU_PL_GWS | 片上 Global Wave Sync |
AMDGPU_GEM_DOMAIN_OA | AMDGPU_PL_OA | 片上 Ordered Append |
| (内部) | AMDGPU_PL_PREEMPT | 可抢占 BO (KFD 用) |
| (内部) | AMDGPU_PL_DOORBELL | Doorbell 寄存器映射 |
4.3 Placement 与 Eviction 的关系
Placement 决定了两个关键问题:
① 新 BO 分配时触发谁的 eviction?
ttm_bo_alloc_resource() 按 placement 数组顺序尝试。如果 placement[0] 是 VRAM 且分配失败,就在 VRAM 的 LRU 中找 victim 驱逐。
② 被驱逐的 BO 去哪里?
由 evict_flags() 回调决定。AMD 的实现 amdgpu_evict_flags() 返回一个新的 ttm_placement,定义了被踢 BO 的降级路径:
VRAM 中的 BO 被驱逐时:
+-- buffer_funcs 未就绪? -> 降级到 SYSTEM (CPU memcpy)
+-- 在 CPU 可见 VRAM 区域 且不要求 CPU 访问?
| -> 先尝试移到 CPU 不可见 VRAM (DESIRED)
| -> 不行再移到 GTT (FALLBACK)
'-- 其他情况 -> 降级到 GTT 或 SYSTEM
GTT / PREEMPT 中的 BO 被驱逐时:
'-> 降级到 SYSTEM
这个降级链可以级联:当 VRAM 中的 BO 被踢到 GTT 时,如果 GTT 也满了,GTT 中的某个 BO 又会被踢到 SYSTEM,形成"级联驱逐 (cascade eviction)"。
5. 触发 Eviction 的场景分类
Eviction 不仅仅在 amdgpu_bo_create() 时触发,以下场景都可能触发:
5.1 BO 创建 (最常见)
用户态 ioctl: DRM_IOCTL_AMDGPU_GEM_CREATE
-> amdgpu_gem_create_ioctl()
-> amdgpu_bo_create()
-> ttm_bo_init_reserved()
-> ttm_bo_validate()
-> ttm_bo_alloc_resource(force_space=true)
-> ttm_bo_evict_alloc() <-- eviction!
5.2 BO 放置变更 (validate)
当用户态提交 command buffer 时,所有引用的 BO 必须在 GPU 可访问的域中。如果某个 BO 之前被踢到了 SYSTEM,需要移回 VRAM/GTT:
Command Submission:
-> amdgpu_cs_ioctl()
-> amdgpu_cs_bo_validate()
-> ttm_bo_validate(new_placement) <-- 可能触发 eviction
5.3 主动清理 (manager cleanup)
当 resource manager 需要清空时(如驱动卸载或 suspend),调用:
ttm_resource_manager_evict_all()
-> ttm_bo_evict_first() <-- 逐个驱逐所有 BO
5.4 内存压力回收 (shrinker)
TTM 注册了 shrinker,在系统内存压力下将 GTT 中的 BO 换出到 SYSTEM/swap:
Linux MM: 内存回收
-> ttm_global_swapout()
-> ttm_device_swapout()
-> ttm_lru_walk_for_evict() <-- swap 方向的 eviction
6. ttm_bo_validate() – Eviction 的上层入口
ttm_bo_validate() 是 eviction 最常见的上层入口。它不仅用于 BO 创建,也用于 CS 提交时的 BO 重新放置:
/* drivers/gpu/drm/ttm/ttm_bo.c (简化) */
int ttm_bo_validate(struct ttm_buffer_object *bo,
struct ttm_placement *placement,
struct ttm_operation_ctx *ctx)
{
struct ttm_resource *res;
struct ttm_place hop;
bool force_space;
int ret;
/* 没有候选域 -> 释放 backing store */
if (!placement->num_placement)
return ttm_bo_pipeline_gutting(bo);
force_space = false;
do {
/* BO 已经在合适的位置了?不需要移动 */
if (bo->resource &&
ttm_resource_compatible(bo->resource, placement, force_space))
return 0;
/* pinned BO 不能移动 */
if (bo->pin_count)
return -EINVAL;
/* 分配新位置(可能触发 eviction)*/
ret = ttm_bo_alloc_resource(bo, placement, ctx, force_space, &res);
force_space = !force_space;
if (ret == -ENOSPC)
continue;
if (ret)
return ret;
/* 执行搬迁(可能需要多跳 bounce)*/
bounce:
ret = ttm_bo_handle_move_mem(bo, res, false, ctx, &hop);
if (ret == -EMULTIHOP) {
ret = ttm_bo_bounce_temp_buffer(bo, ctx, &hop);
if (!ret)
goto bounce; /* 中转完毕,再次尝试 */
}
if (ret) {
ttm_resource_free(bo, &res);
return ret;
}
} while (ret && force_space);
return 0;
}
7. ttm_operation_ctx – 操作上下文
所有 eviction 相关操作都受 ttm_operation_ctx 控制:
struct ttm_operation_ctx {
bool interruptible; /* 等待 fence 时是否可被信号中断 */
bool no_wait_gpu; /* 是否禁止等待 GPU (trylock 模式) */
bool gfp_retry_mayfail;/* 内存分配是否允许重试 */
uint64_t bytes_moved; /* 统计:本次操作搬了多少字节 */
bool force_alloc; /* 是否强制分配(忽略 cgroup 限制)*/
};
关键参数对 eviction 行为的影响:
| 参数 | 值 | 对 eviction 的影响 |
|---|---|---|
interruptible | true | 等待 victim 的 fence 时可以被信号 (Ctrl+C) 打断,返回 -ERESTARTSYS |
interruptible | false | 不可中断,必须等到完成(通常用于内核内部操作) |
no_wait_gpu | true | 如果 victim BO 的 fence 未完成,立即放弃,返回 -EBUSY |
no_wait_gpu | false | 可以等待 GPU 完成 victim BO 上的操作后再驱逐 |
8. Pipelined Eviction Fence
在 ttm_bo_alloc_resource() 成功分配资源后,有一个容易被忽略但非常重要的步骤:
ret = ttm_bo_add_pipelined_eviction_fences(bo, man, ctx->no_wait_gpu);
这个函数将 resource manager 上已有的 eviction fence 添加到新 BO 的 dma_resv 中:
static int ttm_bo_add_pipelined_eviction_fences(struct ttm_buffer_object *bo,
struct ttm_resource_manager *man,
bool no_wait_gpu)
{
struct dma_fence *fence;
int i;
spin_lock(&man->eviction_lock);
for (i = 0; i < TTM_NUM_MOVE_FENCES; i++) {
fence = man->eviction_fences[i];
if (!fence)
continue;
if (no_wait_gpu) {
if (!dma_fence_is_signaled(fence)) {
spin_unlock(&man->eviction_lock);
return -EBUSY; /* 不等 -> 直接失败 */
}
} else {
/* 关键:新 BO 依赖这个 eviction fence */
dma_resv_add_fence(bo->base.resv, fence,
DMA_RESV_USAGE_KERNEL);
}
}
spin_unlock(&man->eviction_lock);
return dma_resv_reserve_fences(bo->base.resv, 1);
}
为什么需要这一步?
考虑这样的时序:
- BO_A 正在被从 VRAM 驱逐到 GTT(SDMA 正在搬数据)
- BO_A 腾出的 VRAM 空间被分配给了新 BO_B
- 如果 BO_B 在 SDMA 搬完 BO_A 之前就开始使用这块 VRAM,会读到脏数据
Pipelined eviction fence 确保新 BO_B 的任何操作都必须等待之前的驱逐搬迁完成。这就是 “pipelined” 的含义----驱逐和新分配可以在同一条流水线上有序执行。
9. 数据结构关系总览
ttm_device (adev->mman.bdev)
|
+-- funcs = &amdgpu_bo_driver
| +-- .eviction_valuable -> amdgpu_ttm_bo_eviction_valuable (4.8.2)
| +-- .evict_flags -> amdgpu_evict_flags (4.8.3)
| '-- .move -> amdgpu_bo_move (4.8.3)
|
+-- man_drv[TTM_PL_VRAM] --> amdgpu_vram_mgr
| +-- lru (LRU 链表)
| | +-- bo_A.resource
| | +-- bo_B.resource
| | '-- bo_C.resource (最久未用 -> 最先被驱逐)
| |
| +-- eviction_lock
| '-- eviction_fences[TTM_NUM_MOVE_FENCES]
| '-- 最近一次驱逐搬迁产生的 dma_fence
|
+-- man_drv[TTM_PL_TT] ---> amdgpu_gtt_mgr
| '-- lru (LRU 链表)
|
'-- sysman[TTM_PL_SYSTEM]
ttm_buffer_object (bo)
|
+-- resource --> ttm_resource
| +-- mem_type (当前在哪个域)
| '-- bo (回指)
|
+-- base.resv --> dma_resv
| '-- fences[]
| +-- GPU job fence (硬件 signal)
| +-- pipelined eviction fence (from resource_manager)
| '-- eviction fence (软件 signal, 4.8.4 详述)
|
+-- ttm --> ttm_tt (backing pages)
+-- pin_count (> 0 -> 不可驱逐)
+-- priority (LRU 优先级)
'-- bulk_move (批量 LRU 操作)
10. 完整触发时序图
以最典型的场景为例:用户创建一个 VRAM BO,但 VRAM 已满。
用户态 TTM 核心 VRAM Manager
| | |
| GEM_CREATE(VRAM) | |
| ----------------------->| |
| | |
| ttm_bo_validate() |
| | |
| ttm_bo_alloc_resource(force=false) |
| |---- ttm_resource_alloc() --------->|
| |<--------- -ENOSPC -----------------|
| | (VRAM 满了, 第一轮不驱逐) |
| | |
| ttm_bo_alloc_resource(force=true) |
| |---- ttm_resource_alloc() --------->|
| |<--------- -ENOSPC -----------------|
| | |
| ttm_bo_evict_alloc() |
| | |
| ttm_lru_walk_for_evict() |
| | |
| +----+----+ |
| | LRU Walk| (详见 4.8.2) |
| | 选择 | |
| | victim | |
| +----+----+ |
| | |
| ttm_bo_evict(victim) |
| | |
| +----+----+ |
| | Move | (详见 4.8.3) |
| | victim | |
| |VRAM->GTT| |
| +----+----+ |
| | |
| VRAM 有空间了 |
| |---- ttm_resource_alloc() --------->|
| |<--------- 成功 ---------------------|
| | |
| ttm_bo_add_pipelined_eviction_fences() |
| | |
| ttm_bo_handle_move_mem(new_bo) |
| | |
| <--- 成功 ------------- | |
| | |
11. 错误处理与边界情况
11.1 所有候选都不能驱逐
如果 LRU 中所有 BO 都被 eviction_valuable() 否决(如全是 pinned 或 KFD 保护的),ttm_bo_evict_alloc() 返回 -EBUSY,最终传播为 -ENOSPC:
用户态收到: -ENOMEM (向后兼容) 或 -ENOSPC
11.2 级联驱逐
victim BO 从 VRAM 移到 GTT 时,ttm_bo_evict() 内部会调用 ttm_bo_mem_space() 为 victim 在 GTT 中分配空间。如果 GTT 也满了,会递归触发 GTT 中其他 BO 的驱逐。理论上可以级联到 SYSTEM。
驱逐 victim_VRAM -> 需要 GTT 空间 -> 驱逐 victim_GTT -> 降级到 SYSTEM
11.3 死锁防护
TTM 使用 ww_mutex (wait-wound) 协议防止驱逐过程中的死锁。dma_resv_locking_ctx() 返回的 ticket 确保多个 BO 的锁定顺序一致。如果锁冲突,一方会收到 -EDEADLK 并释放锁后重试。
12. 小结
| 要点 | 内容 |
|---|---|
| 触发方式 | 按需触发 (on-demand),不是后台扫描 |
| 核心入口 | ttm_bo_alloc_resource() -> ttm_bo_evict_alloc() |
| 两轮机制 | 第 1 轮只分配不驱逐,第 2 轮允许驱逐 |
| Placement | 决定 BO 去哪里、从哪里驱逐 |
| Multi-hop | VRAM <-> SYSTEM 需要经过 GTT 中转 |
| Pipelined fence | 新 BO 必须等待之前的驱逐搬迁完成 |
| 级联驱逐 | 目标域也满了 -> 递归驱逐到更低级别域 |
13. 推荐阅读顺序
| 顺序 | 文件 | 关键函数 | 理解什么 |
|---|---|---|---|
| 1 | ttm/ttm_bo.c | ttm_bo_mem_space() | 两轮分配机制 |
| 2 | ttm/ttm_bo.c | ttm_bo_alloc_resource() | 统一的分配/驱逐入口 |
| 3 | ttm/ttm_bo.c | ttm_bo_validate() | multi-hop bounce |
下一篇
-> 4.8.2 – Eviction 选择策略:LRU 与候选筛选 – 深入 ttm_lru_walk_for_evict() 和 eviction_valuable() 回调机制
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/shenjunpeng/article/details/160335840



