Intel Discrete GPU - Memory - Alloc
在应用使用 GPU 渲染一帧图像的过程中,需要向 GPU 传递 Vertex/Texture 数据信息,渲染完成后应用将得到一帧图像的数据信息,而这些数据信息的承载者便是通过显存,显存的术语通常被叫做 BO (Buffer Object);
显存位置定义
先来了解一下几种描述显存存储位置的定义,
-
XE_PL_SYSTEM
- GPU 映射系统内存的场景,一般用于显存回收时暂存的场景,Xe 驱动称其为 System cache,这里的内存由 shmem 分配,系统可随时回收
-
XE_PL_TT
- GPU 映射系统内存的场景,这里的系统内存由
ttm_pool所承载,系统不可直接回收
- GPU 映射系统内存的场景,这里的系统内存由
-
XE_PL_VRAM0
-
XE_PL_VRAM1
- GPU 映射独立显存的场景
-
XE_PL_STOLEN
- 与 VRAM 链路基本相同,一般代表从独立显存中预留一块空间给到 Display 使用

显存分配流程
以上介绍了 BO 可能存在的三种存储位置,实际上 BO 期望开辟的位置是由用户态驱动所指定,内核在 BO 创建时会借助__xe_bo_placement_for_flags来解析用户的请求。这个位置并非指定,它可以是多个,依照更佳适合 BO 存在的位置传递到内核。
用户期望 BO 开辟的位置最终会存储在bo->placements中,在当前 Xe 驱动中,期望开辟的位置最多有可以有三种,通过XE_BO_MAX_PLACEMENTS指定。
显存分配的入口为XE_GEM_CREATE,核心函数是ttm_bo_validate,它的职责不仅仅是起到显存分配作用,如它的命名一样,它用来核对当前 BO 的所在位置是否与预期placements匹配,否则将可能会涉及到显存的分配、迁移。
显存初次分配时是通过ttm_bo_init_reserved来实现的,完成xe_bo结构的初始化后会通过刚提及的核心函数ttm_bo_validate验证显存开辟位置,它通过一些逻辑比较bo->resource与placement是否匹配,否则将通过ttm_bo_alloc_resource分配资源,继而通过ttm_bo_handle_move_mem做迁移。
起初这里的流程真是把我绕晕,为什么显存的分配的流程要涉及 validate 和 move ?怎么不是简简单单的 alloc 呢。
后面再以全局视角来看这里的话,是蕴藏着开发者的智慧在里面的。因为ttm_bo_validate在显存的 Alloc / Evict / Swap 等均有调用,它是一个通用接口。
在显存的初次分配时,bo->resource是不存在的,所以 validate 中的ttm_resource_compatible检查一定不符合。我们假设用户期望的显存开辟位置是 VRAM,那么显存的分配将会在ttm_bo_alloc_resource中完成,独显的内存分配是通过 DRM BUDDY 机制实现,分配的函数为drm_buddy_alloc_blocks,分配完成后会涉及 move 函数的调用,这里要注意的是,显存初次分配时并不会涉及实际的 move 操作,仅会调用ttm_bo_move_null来更新bo->resource,这时 resource 将代表显存的实际存在位置了,后续的 Evict / Swap 操作将借助它来进一步操作。
如果用户期望的显存开辟位置是 SYSTEM,显存的分配流程又是什么样呢?

-
VRAM 类型显存通过 drm_buddy 在独显空间进行分配
-
TT 类型显存在系统内存中分配是通过
ttm_tt_create实现,它的分配请求转达到ttm_pool,内部再借助alloc_pages或dma_alloc申请系统内存资源
关于 DRM_BUDDY
可以像内存子系统 Buddy 内存管理一样理解 Drm Buddy,它能够按照 order 管理 Pages,也能够 Split / Merge;
区别比较大的点是,
- Drm Buddy 能够支持的 order 比较大
/* Order-zero must be at least SZ_4K */
#define DRM_BUDDY_MAX_ORDER (63 - 12)
- 可以支持指定 PA 段的方式分配显存
一段 Pages 的分配状态在 Drm Buddy 通过drm_buddy_block来管理,其中header成员各个字段的定义如下,

11, 10指明当前 block 是否空闲、是否已被 Split5, 0指明当前 block 对就的 order 值
初始化
核心接口 - drm_buddy_init
假如独显的内存大小是 4G,那么 drm buddy 初始化时 max_order 为 20,
mm->max_order = ilog2(size) - ilog2(chunk_size);
每个 order 均搭配有一个 Free Pages List,
mm->free_list = kmalloc_array(mm->max_order + 1,
sizeof(struct list_head), GFP_KERNEL);
除了上述的 free_list 关联空闲 Pages 外,Drm Buddy 还提供了一个 roots 链表,记录 Buddy 初始化完成时的 block 信息,
mm->n_roots = hweight64(size);
mm->roots = kmalloc_array(mm->n_roots,
sizeof(struct drm_buddy_block *), GFP_KERNEL);
假如独显的内存大小是 6G,基础页面大小为 4K,初始化时将通过一个 order 20 的 4G Block 和一个 order 19 的 2G Block 关联完整的独显空间,这两个 Block 都将串联至 roots 中。
内存分配
核心接口 - drm_buddy_alloc_blocks
Block 管理策略

- 优化从高地址处获取符合要求的 Order Block;
- 被拆分的 Block 会从对应的 Order List 上摘除,但 Block 结构并不会被释放,它仍要通过 Left / Right 指针来串联子级 Block,便于其它流程中找到相邻的 Buddy Block 或进行 Merge 操作;
大致流程

- 常规显存分配场景下,会根据所请求的 Size 计算出目标 Order,如果 Free List 能够满足,则直接获取;
- 如果独立显存有压力,会尝试从较小的 Order 聚合出满足
min_order要求的 Block; - 最终得到的 Blocks 可能会因为 Min Order 对齐等要求,实际得到的 Size 要比期望分配的 Size 要大,往往需求进行一步 Trim 操作;
- TODO: alloc pa & try harder
内存释放
核心接口 - drm_buddy_free_list
大致流程

- 当前用户触发独立显存释放时,会将 Block 以 Objects 链表的形式传递至释放接口,随后 Buddy 依次遍历它们;
- 通过 Block 找到对应 Buddy,如果 Buddy 也为空闲状态,则通过
drm_block_free彻底释放 Block,并将它们的 Parent,也是将 Order 拆分前的 Block 重新加入 Free List;
关于 TTM_POOL
ttm_pool 是 TTM 架构的一个能力子集,它作为系统内存的缓冲器,为硬件加速提供内存推动力。
ttm_pool 的全局初始化接口为ttm_pool_init,一般情况下它在 TTM 初始化ttm_device_init中被调用。
每个 TTM 设备内部嵌着 pool 结构,每个 pool 中包含以下具体的内存池,
struct {
struct ttm_pool_type orders[NR_PAGE_ORDERS];
} caching[TTM_NUM_CACHING_TYPES];
其中TTM_NUM_CACHING_TYPES当前共定义为以下三种 cache 类型,

当驱动中有具体的分配需求时,将从指定的 cache 类型池中获取到内存。
Xe 驱动中在初始化 ttm_pool 时默认禁用了 dma_alloc 及 dma32 相关属性,这时 ttm_pool 将启用全局的内存池作为缓冲,它们分别是
- global_write_combined[NR_PAGE_ORDERS]
- global_uncached[NR_PAGE_ORDERS]
为什么没有 cached 的池子呢,正如 Code 中注释写道,
DGFX system memory is always WB / ttm_cached, since other caching modes are only supported on x86. DGFX GPU system memory accesses are always coherent with the CPU.
这难道不是说明更应该需要一个 cached 内存池,或者初始化 ttm 时应该默认使能 use_dma_alloc?
内存分配
对外接口 - ttm_pool_alloc
大致流程

- 根据指定的 cache 策略和 order 大小选择对应的 pool,并从 pool 中取下 order 对应的 pages
- Cache 策略应用;映射所得到的 Pages
内存释放
对外接口 - ttm_pool_free
大致流程

- 从 page private 取出 order,再根据 cache 类型匹配对应的 pool
- 清空内存并放回 pool,通过 lru 节点成员串联至 pool 的 pages 链表
缓存释放
对外接口 - 注册 Shrinker,核心实现为ttm_pool_shrink
大致流程

- 每种 cache 类型及每个 order 等级都对应一个 pool,它们在初始化时会被注册到 shrinker_list 链接中,在 shrink 流程中会依次选出一个目标 pool
- 根据这个目标 pool 取出对应 order 的 pages 进行释放
显存关联场景
关联 DMABUF
系统中的进程间或硬件 IP 间均有共享 Buffer 的需求,DRM 中实现共享的机制称之为 Prime,它由 Dave Airlie 提出。
这个名字的由来也是故事的,Nvidia 最早实现双显卡,名为 Optimus, Dave 参考并实现后,有趣地将 Linux 下实现的机制命名为了 Prime;
而 Optimus Prime 结合在一起意味着什么?

Prime 当前的架构是借助 DMA-BUF 来实现的,具体的说 Prime 的使用场景主要是 fd 的导入与导出,导入与的导出的操作接口分别对应drm_prime_fd_to_handle_ioctl和drm_prime_handle_to_fd_ioctl,
在导入的场景中,DMA BUF 背后的内存可能来自于其它模块分配,Prime 先要判断这个 DMA BUF 是否已有 BO 关联,有则转换为 Handle,无则新建 BO 并关联;
在导出的场景中,用户会先分配一块 BO 内存,然后通过这个 BO 对应的 Handle 进一步将其转换为 fd;
关联 USER-PTR
用户 CPU 侧空间分配的内存也可以直接被 GPU 访问,这一部分使用场景的内存分配完全在用户层面,用户可以通过 Malloc 或 MMAP 分配,然后在 MAP 阶段映射到 GPU 页表中,具体操作后面篇章再介绍。
需要注意的是,用户通过 Malloc 或 MMAP 分配空间后,这仅代表 VA 已完成分配,实际上给到 GPU 映射时极有可能存在物理内存未分配完整的情况,这样该如何处理?
在 Xe 驱动的实现中,GPU 在映射 USER-PTR 类型内存时会通过xe_vma_userptr_pin_pages对内存页面做出 Pin 操作,确保物理内存完全分配。Pin 操作的核心是借助hmm_range_fault实现,这是 MM 子系统专门为异构处理器提供的一个内存分配手段,我们可以将它称之为 HMM
关于 HMM