第四章:TTM分析: 4.8.1 TTM Eviction 机制概述与触发流程
前置阅读: 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 */intttm_bo_mem_space(structttm_buffer_object*bo,structttm_placement*placement,structttm_resource**res,structttm_operation_ctx*ctx){bool force_space=false;intret;do{ret=ttm_bo_alloc_resource(bo,placement,ctx,force_space,res);force_space=!force_space;/* 第二轮开启强制模式 */}while(ret==-ENOSPC&&force_space);returnret;}| 轮次 | 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.2ttm_bo_alloc_resource()– 分配与驱逐的统一入口
这是整个触发逻辑的核心函数:
/* drivers/gpu/drm/ttm/ttm_bo.c (简化) */staticintttm_bo_alloc_resource(structttm_buffer_object*bo,structttm_placement*placement,structttm_operation_ctx*ctx,bool force_space,structttm_resource**res){structttm_device*bdev=bo->bdev;structww_acquire_ctx*ticket;inti,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))returnret;/* 遍历 placement 中的每个候选域 */for(i=0;i<placement->num_placement;++i){conststructttm_place*place=&placement->placement[i];structttm_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)returnret;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)returnret;}/* 3. 分配成功后,添加流水线驱逐 fence */ret=ttm_bo_add_pipelined_eviction_fences(bo,man,...);if(unlikely(ret)){ttm_resource_free(bo,res);if(ret==-EBUSY)continue;returnret;}return0;/* 成功! */}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 -> 跳过 (这个域没戏) | '-- 其他错误 -> 返回错误 | 所有域尝试完毕 -> 返回 -ENOSPC4. 关键概念:Placement(放置策略)
4.1 数据结构
每个 BO 在创建时声明自己可以住在哪些内存域,优先级从高到低排列:
structttm_placement{unsignednum_placement;/* 候选域的数量 */conststructttm_place*placement;/* 候选域数组(按优先级排列)*/};structttm_place{unsignedfpfn;/* 起始页帧号限制 (0 = 无限制) */unsignedlpfn;/* 结束页帧号限制 (0 = 无限制) */uint32_tmem_type;/* TTM_PL_VRAM / TTM_PL_TT / TTM_PL_SYSTEM */uint32_tflags;/* 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) <-- 可能触发 eviction5.3 主动清理 (manager cleanup)
当 resource manager 需要清空时(如驱动卸载或 suspend),调用:
ttm_resource_manager_evict_all() -> ttm_bo_evict_first() <-- 逐个驱逐所有 BO5.4 内存压力回收 (shrinker)
TTM 注册了 shrinker,在系统内存压力下将 GTT 中的 BO 换出到 SYSTEM/swap:
Linux MM: 内存回收 -> ttm_global_swapout() -> ttm_device_swapout() -> ttm_lru_walk_for_evict() <-- swap 方向的 eviction6.ttm_bo_validate()– Eviction 的上层入口
ttm_bo_validate()是 eviction 最常见的上层入口。它不仅用于 BO 创建,也用于 CS 提交时的 BO 重新放置:
/* drivers/gpu/drm/ttm/ttm_bo.c (简化) */intttm_bo_validate(structttm_buffer_object*bo,structttm_placement*placement,structttm_operation_ctx*ctx){structttm_resource*res;structttm_placehop;bool force_space;intret;/* 没有候选域 -> 释放 backing store */if(!placement->num_placement)returnttm_bo_pipeline_gutting(bo);force_space=false;do{/* BO 已经在合适的位置了?不需要移动 */if(bo->resource&&ttm_resource_compatible(bo->resource,placement,force_space))return0;/* 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)returnret;/* 执行搬迁(可能需要多跳 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)gotobounce;/* 中转完毕,再次尝试 */}if(ret){ttm_resource_free(bo,&res);returnret;}}while(ret&&force_space);return0;}7.ttm_operation_ctx– 操作上下文
所有 eviction 相关操作都受ttm_operation_ctx控制:
structttm_operation_ctx{bool interruptible;/* 等待 fence 时是否可被信号中断 */bool no_wait_gpu;/* 是否禁止等待 GPU (trylock 模式) */bool gfp_retry_mayfail;/* 内存分配是否允许重试 */uint64_tbytes_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中:
staticintttm_bo_add_pipelined_eviction_fences(structttm_buffer_object*bo,structttm_resource_manager*man,bool no_wait_gpu){structdma_fence*fence;inti;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);returndma_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 (向后兼容) 或 -ENOSPC11.2 级联驱逐
victim BO 从 VRAM 移到 GTT 时,ttm_bo_evict()内部会调用ttm_bo_mem_space()为 victim 在 GTT 中分配空间。如果 GTT 也满了,会递归触发 GTT 中其他 BO 的驱逐。理论上可以级联到 SYSTEM。
驱逐 victim_VRAM -> 需要 GTT 空间 -> 驱逐 victim_GTT -> 降级到 SYSTEM11.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()回调机制
