第四章: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. 核心问题为什么需要 EvictionGPU 显存 (VRAM) 是有限的稀缺资源。当多个应用同时使用 GPU 时VRAM 的总需求量往往远超物理容量。TTM eviction 机制要回答一个核心问题VRAM 不够用了谁该被踢出去怎么踢踢到哪这与操作系统的内存页面回收 (page reclaim) 本质相同----当物理内存不足时OS 选择部分页面换出到 swap。TTM eviction 就是 GPU 世界的 “swap out”概念Linux MMTTM稀缺资源物理内存 (RAM)显存 (VRAM)管理对象struct pagettm_buffer_object退化目标swap 分区/文件GTT (系统内存) 或 SYSTEM选择策略LRU / active-inactiveLRU per resource_manager异步机制kswapd / writebackdma_fence / SDMA copy2. 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_spacefalse;intret;do{retttm_bo_alloc_resource(bo,placement,ctx,force_space,res);force_space!force_space;/* 第二轮开启强制模式 */}while(ret-ENOSPCforce_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*bdevbo-bdev;structww_acquire_ctx*ticket;inti,ret;ticketdma_resv_locking_ctx(bo-base.resv);/* 预留 fence 槽位后续 move 操作需要往 dma_resv 中加 fence */retdma_resv_reserve_fences(bo-base.resv,TTM_NUM_MOVE_FENCES);if(unlikely(ret))returnret;/* 遍历 placement 中的每个候选域 */for(i0;iplacement-num_placement;i){conststructttm_place*placeplacement-placement[i];structttm_resource_manager*man;bool may_evict;manttm_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_spaceplace-mem_type!TTM_PL_SYSTEM);/* 1. 先尝试直接分配 */retttm_resource_alloc(bo,place,res,...);if(ret){if(ret!-ENOSPCret!-EAGAIN)returnret;if(!may_evict)continue;/* 第一轮不允许驱逐跳到下一个域 *//* 2. 分配失败 允许驱逐 - 启动 eviction */retttm_bo_evict_alloc(bdev,man,place,bo,ctx,ticket,res,limit_pool);if(ret-EBUSY)continue;/* 这个域驱逐也腾不出来试下一个 */if(ret)returnret;}/* 3. 分配成功后添加流水线驱逐 fence */retttm_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 FlagTTM mem_type含义AMDGPU_GEM_DOMAIN_VRAMTTM_PL_VRAMGPU 显存性能最高AMDGPU_GEM_DOMAIN_GTTTTM_PL_TT通过 GART 映射的系统内存AMDGPU_GEM_DOMAIN_CPUTTM_PL_SYSTEM纯系统内存GPU 不可直接访问AMDGPU_GEM_DOMAIN_GDSAMDGPU_PL_GDS片上 Global Data ShareAMDGPU_GEM_DOMAIN_GWSAMDGPU_PL_GWS片上 Global Wave SyncAMDGPU_GEM_DOMAIN_OAAMDGPU_PL_OA片上 Ordered Append(内部)AMDGPU_PL_PREEMPT可抢占 BO (KFD 用)(内部)AMDGPU_PL_DOORBELLDoorbell 寄存器映射4.3 Placement 与 Eviction 的关系Placement 决定了两个关键问题① 新 BO 分配时触发谁的 evictionttm_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_spacetrue) - ttm_bo_evict_alloc() -- eviction!5.2 BO 放置变更 (validate)当用户态提交 command buffer 时所有引用的 BO 必须在 GPU 可访问的域中。如果某个 BO 之前被踢到了 SYSTEM需要移回 VRAM/GTTCommand 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/swapLinux 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_spacefalse;do{/* BO 已经在合适的位置了不需要移动 */if(bo-resourcettm_resource_compatible(bo-resource,placement,force_space))return0;/* pinned BO 不能移动 */if(bo-pin_count)return-EINVAL;/* 分配新位置可能触发 eviction*/retttm_bo_alloc_resource(bo,placement,ctx,force_space,res);force_space!force_space;if(ret-ENOSPC)continue;if(ret)returnret;/* 执行搬迁可能需要多跳 bounce*/bounce:retttm_bo_handle_move_mem(bo,res,false,ctx,hop);if(ret-EMULTIHOP){retttm_bo_bounce_temp_buffer(bo,ctx,hop);if(!ret)gotobounce;/* 中转完毕再次尝试 */}if(ret){ttm_resource_free(bo,res);returnret;}}while(retforce_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 的影响interruptibletrue等待 victim 的 fence 时可以被信号 (CtrlC) 打断返回-ERESTARTSYSinterruptiblefalse不可中断必须等到完成通常用于内核内部操作no_wait_gputrue如果 victim BO 的 fence 未完成立即放弃返回-EBUSYno_wait_gpufalse可以等待 GPU 完成 victim BO 上的操作后再驱逐8. Pipelined Eviction Fence在ttm_bo_alloc_resource()成功分配资源后有一个容易被忽略但非常重要的步骤retttm_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(i0;iTTM_NUM_MOVE_FENCES;i){fenceman-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 驱逐到 GTTSDMA 正在搬数据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(forcefalse) | | |---- ttm_resource_alloc() ---------| | |--------- -ENOSPC -----------------| | | (VRAM 满了, 第一轮不驱逐) | | | | | ttm_bo_alloc_resource(forcetrue) | | |---- 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-hopVRAM - SYSTEM 需要经过 GTT 中转Pipelined fence新 BO 必须等待之前的驱逐搬迁完成级联驱逐目标域也满了 - 递归驱逐到更低级别域13. 推荐阅读顺序顺序文件关键函数理解什么1ttm/ttm_bo.cttm_bo_mem_space()两轮分配机制2ttm/ttm_bo.cttm_bo_alloc_resource()统一的分配/驱逐入口3ttm/ttm_bo.cttm_bo_validate()multi-hop bounce下一篇-4.8.2 – Eviction 选择策略LRU 与候选筛选– 深入ttm_lru_walk_for_evict()和eviction_valuable()回调机制