libdrm中的amdgpu_va_manager的作用和实现分析
版本:基于 libdrm amdgpu 用户态驱动(
amdgpu_vamgr.c)
文件位置:amdgpu/amdgpu_vamgr.c、amdgpu/amdgpu_internal.h
目录
1. 背景与动机
1.1 为什么需要 GPU 虚拟地址空间?
现代 GPU 使用 IOMMU / GPU MMU 对显存和系统内存进行地址翻译。应用程序(以及内核 DRM 驱动)需要为每个 GPU Buffer Object (BO) 分配一段 GPU 虚拟地址(VA),再将该 VA 映射到物理页面,GPU 着色器才能通过 VA 访问数据。
GPU 执行单元
│
│ GPU VA (64-bit 虚拟地址)
▼
GPU MMU (页表硬件)
│
│ 物理地址翻译
▼
DDR / VRAM 物理页面
1.2 为什么在用户态管理 VA?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 每次分配都陷入内核 | 安全、内核统一管理 | 系统调用开销大,频繁分配场景性能差 |
| 用户态预分配 VA 池 | 零系统调用,亚微秒延迟 | 需要用户态自行管理碎片和并发 |
libdrm amdgpu 采用用户态 VA 管理:设备初始化时向内核查询可用的 VA 地址范围(virtual_address_offset / virtual_address_max),然后在用户空间维护一个空洞链表(free-hole list),应用程序分配 BO 时直接从链表中划取 VA,无需每次陷入内核。
2. 数据结构设计
2.1 层次关系
amdgpu_device
└── amdgpu_va_manager ← 顶层 VA 管理器(每设备一个)
├── amdgpu_bo_va_mgr vamgr_32 ← 低地址 32-bit 区域
├── amdgpu_bo_va_mgr vamgr_low ← 低地址 64-bit 区域
├── amdgpu_bo_va_mgr vamgr_high_32 ← 高地址 32-bit 区域
└── amdgpu_bo_va_mgr vamgr_high ← 高地址 64-bit 区域
└── va_holes: list_head ← 空洞链表(降序排列)
├── amdgpu_bo_va_hole { offset=0xC000, size=0x4000 }
├── amdgpu_bo_va_hole { offset=0x8000, size=0x1000 }
└── amdgpu_bo_va_hole { offset=0x0000, size=0x3000 }
2.2 结构体定义
struct amdgpu_bo_va_hole — 空闲 VA 块
struct amdgpu_bo_va_hole {
struct list_head list; // 链表节点(按 offset 降序排列)
uint64_t offset; // 空洞起始虚拟地址
uint64_t size; // 空洞大小(字节)
};
不变量:链表按
offset降序排列(高地址在前,低地址在后)。
这使得从低地址分配时只需反向遍历(LIST_FOR_EACH_ENTRY_SAFE_REV),效率高。
struct amdgpu_bo_va_mgr — 单区域子管理器
struct amdgpu_bo_va_mgr {
uint64_t va_max; // 本区域的地址上限
struct list_head va_holes; // 空洞链表(降序)
pthread_mutex_t bo_va_mutex; // 保护 va_holes 的互斥锁
uint32_t va_alignment; // 强制地址对齐粒度
};
struct amdgpu_va — 已分配 VA 句柄
struct amdgpu_va {
uint64_t address; // 分配到的 GPU 虚拟地址
uint64_t size; // 大小(已对齐到 va_alignment)
enum amdgpu_gpu_va_range range; // 范围类型(当前只有 general=0)
struct amdgpu_bo_va_mgr *vamgr; // 归属哪个子管理器(用于释放)
};
struct amdgpu_va_manager — 顶层管理器
struct amdgpu_va_manager {
struct amdgpu_bo_va_mgr vamgr_low; // 低地址 64-bit 区
struct amdgpu_bo_va_mgr vamgr_32; // 低地址 32-bit 区
struct amdgpu_bo_va_mgr vamgr_high; // 高地址 64-bit 区
struct amdgpu_bo_va_mgr vamgr_high_32; // 高地址 32-bit 区
};
3. 地址空间分区模型
3.1 分区逻辑
amdgpu_va_manager_init() 根据内核上报的 low_va_offset、low_va_max、high_va_offset、high_va_max 将地址空间切分为四个子区域:
低地址空间 (low VA space):
┌─────────────────────────────────────────────────────┐
│ vamgr_32 [low_va_offset, min(low_va_max, 4GB)) │ ← 可被 32-bit 指针寻址
├─────────────────────────────────────────────────────┤
│ vamgr_low [4GB, low_va_max) │ ← 64-bit 低地址区
└─────────────────────────────────────────────────────┘
高地址空间 (high VA space):
┌─────────────────────────────────────────────────────┐
│ vamgr_high_32 [high_va_offset, high_va_offset+4GB) │ ← 高区内 32-bit 偏移可寻址
├─────────────────────────────────────────────────────┤
│ vamgr_high [high_va_offset+4GB, high_va_max) │ ← 高区 64-bit 区
└─────────────────────────────────────────────────────┘
对应代码:
// vamgr_32:低地址空间中 4GB 以下的部分
start = low_va_offset;
max = MIN2(low_va_max, 0x100000000ULL); // 上限截至 4GB
amdgpu_vamgr_init(&va_mgr->vamgr_32, start, max, alignment);
// vamgr_low:低地址空间中 4GB 以上的部分
start = max;
max = MAX2(low_va_max, 0x100000000ULL);
amdgpu_vamgr_init(&va_mgr->vamgr_low, start, max, alignment);
// vamgr_high_32:高地址空间内偏移 4GB 以下的部分
start = high_va_offset;
max = MIN2(high_va_max, (start & ~0xffffffffULL) + 0x100000000ULL);
amdgpu_vamgr_init(&va_mgr->vamgr_high_32, start, max, alignment);
// vamgr_high:高地址空间内偏移 4GB 以上的部分
start = max;
max = MAX2(high_va_max, (start & ~0xffffffffULL) + 0x100000000ULL);
amdgpu_vamgr_init(&va_mgr->vamgr_high, start, max, alignment);
3.2 子管理器选择逻辑(分配时)
分配标志位(flags)决定使用哪个子管理器:
flags 组合 |
选择的子管理器 | 说明 |
|---|---|---|
0(无标志) |
vamgr_low |
默认:低地址 64-bit 区 |
AMDGPU_VA_RANGE_32_BIT |
vamgr_32 |
强制 32-bit 可寻址 |
AMDGPU_VA_RANGE_HIGH |
vamgr_high |
高地址 64-bit 区 |
HIGH | 32_BIT |
vamgr_high_32 |
高地址 32-bit 区 |
AMDGPU_VA_RANGE_REPLAYABLE |
(任意)+ search_from_top=true |
可重放命令流专用,从高地址向下分配 |
降级回退:若非 32-bit 请求分配失败,自动尝试对应的 32-bit 区域。
4. 初始化与销毁
4.1 子管理器初始化 amdgpu_vamgr_init()
amdgpu_vamgr_init(mgr, start, max, alignment)
│
├── 设置 mgr->va_max = max
├── 设置 mgr->va_alignment = alignment
├── 初始化空洞链表 list_inithead(&mgr->va_holes)
├── 初始化互斥锁 pthread_mutex_init(...)
└── 创建初始空洞 hole = {offset=start, size=max-start}
并插入链表(此时链表仅一个节点,覆盖整个区域)
初始状态:链表中只有一个 “big bang” 空洞,覆盖整个地址区间。
4.2 销毁 amdgpu_vamgr_deinit()
遍历链表,逐个 free() 所有空洞节点,最后销毁互斥锁。
5. VA 分配算法
5.1 入口:amdgpu_vamgr_find_va()
函数签名:
static int amdgpu_vamgr_find_va(
struct amdgpu_bo_va_mgr *mgr,
uint64_t size, // 请求大小
uint64_t alignment, // 对齐要求
uint64_t base_required, // 强制指定地址(0 = 任意)
bool search_from_top, // true = 从高地址分配(REPLAYABLE)
uint64_t *va_out // 输出:分配到的 VA
);
5.2 分配流程
请求到来
│
├── 对齐处理
│ alignment = MAX2(alignment, mgr->va_alignment)
│ size = ALIGN(size, mgr->va_alignment)
│
├── 检查 base_required % alignment == 0,否则返回 -EINVAL
│
├── 加锁 pthread_mutex_lock
│
├── search_from_top == false?(正常模式,从低地址分配)
│ └── LIST_FOR_EACH_ENTRY_SAFE_REV(反向遍历,低地址优先)
│ ├── base_required != 0:检查空洞是否包含 [base_required, base_required+size)
│ └── base_required == 0:
│ waste = 对齐 hole->offset 所需的填充字节
│ offset = hole->offset + waste
│ 检查 offset + size <= hole->offset + hole->size
│
└── search_from_top == true?(REPLAYABLE,从高地址分配)
└── LIST_FOR_EACH_ENTRY_SAFE(正向遍历,高地址优先)
├── base_required != 0:同上
└── base_required == 0:
offset = hole->offset + hole->size - size (尽量靠近空洞顶端)
offset -= offset % alignment (向下对齐)
检查 offset >= hole->offset
│
找到合适空洞后:
└── 调用 amdgpu_vamgr_subtract_hole(hole, offset, offset+size)
解锁,返回 offset
5.3 空洞分割:amdgpu_vamgr_subtract_hole()
从空洞中"挖去"已分配范围 [start_va, end_va),共有四种情形:
情形 1:分割中间(两侧都有剩余)
原空洞: [ offset ............. offset+size )
分配区: [ start_va ... end_va )
结果: [offset, start_va) + [end_va, offset+size)
→ 原节点缩小为右半段,新建节点插入左半段
before: ┌────────────────────────────┐
│ hole │
└────────────────────────────┘
after: ┌──────────┐ ████████ ┌───┐
│ 新 hole │ 已分配 │old│
└──────────┘ └───┘
情形 2:trim 右侧(start_va > offset,但 end_va == offset+size)
→ hole->size = start_va - offset
情形 3:trim 左侧(start_va == offset,但 end_va < offset+size)
→ hole->offset = end_va; hole->size -= (end_va - offset)
情形 4:整个空洞被消耗(start_va==offset && end_va==offset+size)
→ list_del + free(hole)
判断条件汇总:
| 条件 | 情形 |
|---|---|
start_va > offset 且 end_va - offset < size |
中间分割(情形 1) |
start_va > offset |
trim 右(情形 2) |
end_va - offset < size |
trim 左(情形 3) |
| 否则 | 全部消耗(情形 4) |
6. VA 释放与合并算法
6.1 amdgpu_vamgr_free_va()
amdgpu_vamgr_free_va(mgr, va, size)
│
├── 忽略无效地址 AMDGPU_INVALID_VA_ADDRESS (0xfff...f)
├── size = ALIGN(size, mgr->va_alignment)
├── 加锁
│
├── 在降序链表中定位插入位置:
│ 找到第一个 next->offset < va 的节点
│ hole 指向其前驱(offset >= va 的最后一个节点)
│
├── 尝试向上合并(与 hole 合并):
│ if hole->offset == va + size:
│ hole->offset = va; hole->size += size
│ 尝试向下合并(hole 与 next 合并):
│ if next->offset + next->size == va(新 hole 起始):
│ next->size += hole->size; 删除 hole
│
├── 否则,尝试向下合并(仅与 next 合并):
│ if next->offset + next->size == va:
│ next->size += size
│
└── 否则,创建新空洞节点插入链表
calloc(1, sizeof(amdgpu_bo_va_hole))
next = {offset=va, size=size}
list_add(&next->list, &hole->list) ← 插在 hole 和 next 之间
6.2 合并示意图
场景:已有空洞 H1=[0x0000,0x2000) 和 H2=[0x6000,0x2000)
现在释放 [0x2000, 0x4000)
链表(降序):H2(0x6000) → H1(0x0000)
步骤:
1. 找到 hole=H2(0x6000 > 0x2000),next=H1(0x0000 < 0x2000)
2. H2->offset(0x6000) ≠ va+size(0x6000)? → 恰好等于!
H2->offset = 0x2000; H2->size = 0x4000 (向上合并)
3. next=H1,(H1->offset + H1->size) = 0x2000 == va(0x2000)
H1->size += H2->size = 0x6000 (向下合并)
删除 H2
最终链表:H1=[0x0000, 0x6000) ← 三段合并为一个大空洞
7. 公共 API
7.1 API 总览
| 函数 | 可见性 | 说明 |
|---|---|---|
amdgpu_va_range_alloc() |
drm_public |
从设备 VA 管理器分配 VA |
amdgpu_va_range_alloc2() |
drm_public |
从独立的 VA manager 句柄分配(支持多 VM 实例) |
amdgpu_va_range_free() |
drm_public |
归还 VA 并释放句柄 |
amdgpu_va_range_query() |
drm_public |
查询设备 VA 地址范围(start/end) |
amdgpu_va_get_start_addr() |
drm_public |
从句柄获取分配到的 VA |
amdgpu_va_manager_alloc() |
drm_public |
动态分配独立的 VA manager(用于多 VM) |
amdgpu_va_manager_init() |
drm_public |
初始化 VA manager(四个子区域) |
amdgpu_va_manager_deinit() |
drm_public |
销毁 VA manager |
amdgpu_vamgr_init() |
drm_private |
初始化单个子管理器(内部用) |
amdgpu_vamgr_deinit() |
drm_private |
销毁单个子管理器(内部用) |
7.2 典型使用流程
/* 1. 打开设备(va_mgr 嵌入在 amdgpu_device 中,由 amdgpu_device_initialize 初始化) */
amdgpu_device_handle dev;
amdgpu_device_initialize(fd, &major, &minor, &dev);
/* 2. 查询 VA 范围(可选,用于了解当前硬件能力) */
uint64_t va_start, va_end;
amdgpu_va_range_query(dev, amdgpu_gpu_va_range_general, &va_start, &va_end);
/* 3. 分配一段 GPU 虚拟地址 */
amdgpu_va_handle va_handle;
uint64_t va_addr;
amdgpu_va_range_alloc(dev,
amdgpu_gpu_va_range_general,
size, // 请求大小
alignment, // 对齐(0=使用默认)
0, // base_required(0=任意地址)
&va_addr, // 输出:分配到的 VA
&va_handle, // 输出:VA 句柄
0); // flags(0=低地址 64-bit 区)
/* 4. 将 BO 映射到这个 VA(通过 amdgpu_bo_va_op) */
amdgpu_bo_va_op(bo, 0, size, va_addr, 0, AMDGPU_VA_OP_MAP);
/* 5. GPU 执行完后,解除映射并归还 VA */
amdgpu_bo_va_op(bo, 0, size, va_addr, 0, AMDGPU_VA_OP_UNMAP);
amdgpu_va_range_free(va_handle);
7.3 flags 标志位
#define AMDGPU_VA_RANGE_32_BIT 0x1 // 要求地址在 32-bit 可寻址范围内
#define AMDGPU_VA_RANGE_HIGH 0x2 // 从高 VA 地址空间分配(如 VM=1 的地址段)
#define AMDGPU_VA_RANGE_REPLAYABLE 0x4 // 可重放命令流专用,从高地址向下分配
8. 线程安全模型
8.1 锁粒度
每个 amdgpu_bo_va_mgr 有一个独立的 pthread_mutex_t bo_va_mutex,覆盖对 va_holes 链表的所有读写操作。
线程 A 分配 vamgr_low ─────────────────────┐ 持有 vamgr_low.bo_va_mutex
线程 B 分配 vamgr_high ────────────────────┐│ 持有 vamgr_high.bo_va_mutex
线程 C 分配 vamgr_32 ───────────────────┐││ 持有 vamgr_32.bo_va_mutex
▼▼▼
三个线程完全并行,互不阻塞
优点:四个子管理器各自独立加锁,低地址和高地址的分配完全并发。
8.2 锁的获取与释放
amdgpu_vamgr_find_va():加锁 → 修改链表 → 解锁(成功/失败都解锁)amdgpu_vamgr_free_va():加锁 → 修改链表 →goto out解锁amdgpu_vamgr_init():初始化时主动加锁插入初始节点,完成后解锁
9. 用户态与内核态职责划分
┌─────────────────────────────────────────────────────────────┐
│ 用户态(libdrm amdgpu) │
│ │
│ amdgpu_va_manager │
│ ├── VA 地址范围划分(4个子区域) │
│ ├── 空洞链表管理(分配/释放/合并) │
│ ├── 对齐对齐处理 │
│ ├── 线程安全(pthread_mutex) │
│ └── VA 句柄(amdgpu_va)生命周期管理 │
│ │
│ 仅管理"VA 号码",不涉及页表! │
└──────────────────────────┬──────────────────────────────────┘
│ ioctl: AMDGPU_GEM_VA
│ (DRM_AMDGPU_GEM_VA)
┌──────────────────────────▼──────────────────────────────────┐
│ 内核态(amdgpu KMD) │
│ │
│ amdgpu_vm │
│ ├── 页表分配(PD/PT 物理页) │
│ ├── TLB 刷新 │
│ ├── VA → 物理地址映射写入硬件页表 │
│ ├── VM 故障处理(page fault handler) │
│ └── IOMMU 配置 │
└─────────────────────────────────────────────────────────────┘
关键区别:用户态 VA manager 只负责"哪个 VA 号码已被占用",它不操作页表。真正建立 VA→物理地址映射是通过 amdgpu_bo_va_op() 触发 DRM_AMDGPU_GEM_VA ioctl 由内核完成。
10. 与 IGT amd_vm 测试的关联
IGT(Intel GPU Tools)的 tests/amdgpu/amd_vm.c 直接测试上述 VA 管理机制:
| IGT 子测试 | 对应的 VA 管理操作 |
|---|---|
bo-map-test |
amdgpu_va_range_alloc + amdgpu_bo_va_op(MAP) |
vmid-reserve-test |
测试 VMID 资源预留,间接验证 VA 分配一致性 |
vm-mapping-test |
全流程:分配 VA → 映射 → GPU 读写 → 解映射 → 释放 VA |
large-bar-test |
测试高地址(AMDGPU_VA_RANGE_HIGH)分配路径 |
调试建议:在 VS Code 中使用 Debug amd_vm 配置(sudo-gdb.sh 方案),在以下关键断点处检查 VA 管理状态:
amdgpu_va_range_alloc2 ← 观察子管理器选择逻辑
amdgpu_vamgr_find_va ← 观察空洞搜索过程
amdgpu_vamgr_subtract_hole ← 观察空洞分割
amdgpu_vamgr_free_va ← 观察合并逻辑
11. 总结
11.1 设计亮点
| 特性 | 实现方式 | 收益 |
|---|---|---|
| 零内核调用分配 | 用户态空洞链表 | 亚微秒级 VA 分配延迟 |
| 四区域分治 | 4个独立子管理器 | 支持 32/64-bit 及 low/high 差异化策略 |
| 细粒度锁 | per-manager mutex | 四个区域完全并发,减少锁争用 |
| 自动合并 | free 时三向合并 | 避免 VA 碎片化 |
| 降级回退 | 64-bit 失败→32-bit | 提高分配成功率 |
| REPLAYABLE 支持 | search_from_top=true |
可重放命令流从高地址分配,避免与普通分配冲突 |
11.2 已知局限
- VA 泄漏无报警:
amdgpu_vamgr_free_va()中calloc失败时只有/* FIXME */注释,VA 会静默泄漏。 - 无碎片整理:纯 first-fit 策略,长期运行后可能出现碎片,但 VA 空间(48-bit = 256TB)远大于实际使用量,实践中影响有限。
amdgpu_gpu_va_range枚举只有一个值:当前amdgpu_gpu_va_range_general=0是唯一合法范围,flags 机制承担了更精细的区域选择职责。
11.3 整体数据流
应用程序
│
│ amdgpu_va_range_alloc(dev, size, align, 0, &va, &handle, flags)
▼
amdgpu_va_range_alloc2()
├── 根据 flags 选择 vamgr(4选1)
├── amdgpu_vamgr_find_va() ← 从空洞链表找可用地址
│ └── amdgpu_vamgr_subtract_hole() ← 从空洞中裁出已分配段
└── calloc amdgpu_va 句柄,记录 address/size/vamgr
│
│ 返回 va(GPU 虚拟地址)和 handle
▼
应用程序调用 amdgpu_bo_va_op(bo, offset, size, va, 0, AMDGPU_VA_OP_MAP)
│
│ ioctl DRM_AMDGPU_GEM_VA
▼
内核 amdgpu KMD:建立页表映射,GPU 可访问该 VA
│
│ ... GPU 执行 ...
▼
应用程序调用 amdgpu_bo_va_op(..., AMDGPU_VA_OP_UNMAP)
│
▼
应用程序调用 amdgpu_va_range_free(handle)
└── amdgpu_vamgr_free_va() ← 归还 VA,合并相邻空洞
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)