关注

第四章:TTM分析: 4.8.1 TTM Eviction 机制概述与触发流程

前置阅读: 01 — TTM 内存管理基础 (TTM 内存管理基础)

本文是 TTM Eviction 系列的第一篇,建立全局视角。后续章节:


1. 核心问题:为什么需要 Eviction?

GPU 显存 (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 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 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 Share
AMDGPU_GEM_DOMAIN_GWSAMDGPU_PL_GWS片上 Global Wave Sync
AMDGPU_GEM_DOMAIN_OAAMDGPU_PL_OA片上 Ordered Append
(内部)AMDGPU_PL_PREEMPT可抢占 BO (KFD 用)
(内部)AMDGPU_PL_DOORBELLDoorbell 寄存器映射

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 的影响
interruptibletrue等待 victim 的 fence 时可以被信号 (Ctrl+C) 打断,返回 -ERESTARTSYS
interruptiblefalse不可中断,必须等到完成(通常用于内核内部操作)
no_wait_gputrue如果 victim BO 的 fence 未完成,立即放弃,返回 -EBUSY
no_wait_gpufalse可以等待 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);
}

为什么需要这一步?

考虑这样的时序:

  1. BO_A 正在被从 VRAM 驱逐到 GTT(SDMA 正在搬数据)
  2. BO_A 腾出的 VRAM 空间被分配给了新 BO_B
  3. 如果 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-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() 回调机制

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/shenjunpeng/article/details/160335840

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--