目录
GART的基本原理请查看:AMD KFD的BO设计分析系列7-1:GPU GART 实现深度解析–基础架构与工作原理。
1. GART 绑定机制
1.1 绑定流程概览
ttm_tt_bind()
↓
amdgpu_ttm_backend_bind()
↓
amdgpu_gart_bind() ← 本章重点
├─> 安全检查
├─> 记录映射(调试)
├─> amdgpu_gart_map() ← 更新页表
│ └─> amdgpu_gmc_set_pte_pde() ← 硬件特定格式
├─> 内存屏障 mb()
├─> 刷新 HDP 缓存
└─> 刷新所有 VM Hub TLB
1.2 amdgpu_gart_bind() 详解
int amdgpu_gart_bind(struct amdgpu_device *adev, uint64_t offset,
int pages, struct page **pagelist,
dma_addr_t *dma_addr, uint64_t flags)
{
int r, i;
// ========== 1. 安全检查 ==========
if (!adev->gart.ready) {
WARN(1, "trying to bind memory to uninitialized GART!\n");
return -EINVAL;
}
// ========== 2. 记录映射关系(调试) ==========
#ifdef CONFIG_DRM_AMDGPU_GART_DEBUGFS
unsigned t, p;
t = offset / AMDGPU_GPU_PAGE_SIZE;
p = t / AMDGPU_GPU_PAGES_IN_CPU_PAGE;
for (i = 0; i < pages; i++, p++)
adev->gart.pages[p] = pagelist ? pagelist[i] : NULL;
#endif
// ========== 3. 页表未映射则跳过 ==========
if (!adev->gart.ptr)
return 0;
// ========== 4. 更新页表 ==========
r = amdgpu_gart_map(adev, offset, pages, dma_addr, flags,
adev->gart.ptr);
// ========== 5. 内存屏障 ==========
mb(); // 确保所有 PTE 写入完成
// ========== 6. 刷新 HDP 缓存 ==========
amdgpu_asic_flush_hdp(adev, NULL);
// ========== 7. 刷新所有 VM Hub 的 TLB ==========
for (i = 0; i < adev->num_vmhubs; i++)
amdgpu_gmc_flush_gpu_tlb(adev, 0, i, 0);
return 0;
}
1.3 参数详解
| 参数 | 类型 | 含义 | 示例 |
|---|---|---|---|
adev | struct amdgpu_device * | GPU 设备 | - |
offset | uint64_t | GART 起始偏移(字节) | 0x80000000 |
pages | int | 要绑定的 CPU 页数 | 256 (1MB) |
pagelist | struct page ** | CPU 页面指针数组 | ttm->pages |
dma_addr | dma_addr_t * | DMA 地址数组 | ttm_dma->dma_address |
flags | uint64_t | PTE 标志位 | READ|WRITE|WC |
offset 计算示例:
// ttm_tt 绑定到 GART
struct amdgpu_ttm_tt *gtt = ...;
uint64_t offset = (u64)bo_mem->start << PAGE_SHIFT;
// bo_mem->start 是页面索引
// 左移 PAGE_SHIFT 得到字节偏移
// 例如: start=2048, PAGE_SHIFT=12 → offset=0x800000 (8MB)
1.4 amdgpu_gart_map() - 页表填充
int amdgpu_gart_map(struct amdgpu_device *adev, uint64_t offset,
int pages, dma_addr_t *dma_addr, uint64_t flags,
void *dst)
{
uint64_t page_base;
unsigned t, i;
// 转换为 GPU 页索引
t = offset / AMDGPU_GPU_PAGE_SIZE;
// 遍历每个 CPU 页
for (i = 0; i < pages; i++) {
page_base = dma_addr[i]; // CPU 页的 DMA 地址
// 一个 CPU 页可能包含多个 GPU 页
// 例如:64KB CPU 页 = 16 个 4KB GPU 页
for (unsigned j = 0; j < AMDGPU_GPU_PAGES_IN_CPU_PAGE; j++) {
// 调用硬件特定函数设置 PTE
amdgpu_gmc_set_pte_pde(adev, dst, t,
page_base, flags);
page_base += AMDGPU_GPU_PAGE_SIZE; // 下一个 GPU 页
t++; // 下一个 PTE 索引
}
}
return 0;
}
处理 CPU 页 vs GPU 页差异:
CPU 页 (64KB):
┌────────────────────────────────────────────────┐
│ DMA Address: 0xF0000000 │
└────────────────────────────────────────────────┘
↓ 拆分为 16 个 GPU 页
GPU 页 (4KB 每个):
┌─────┬─────┬─────┬─────┬───┬─────┐
│ 0K │ 4K │ 8K │ 12K │...│ 60K │
└─────┴─────┴─────┴─────┴───┴─────┘
↓ ↓ ↓ ↓ ↓
PTE[t] PTE[t+1] ... PTE[t+15]
每个 PTE 存储:
PTE[t+0] = 0xF0000000 | flags
PTE[t+1] = 0xF0001000 | flags
PTE[t+2] = 0xF0002000 | flags
...
PTE[t+15] = 0xF000F000 | flags
1.5 amdgpu_gmc_set_pte_pde() - 硬件 PTE 格式
void amdgpu_gmc_set_pte_pde(struct amdgpu_device *adev,
void *cpu_pt_addr,
uint32_t gpu_page_idx,
uint64_t addr,
uint64_t flags)
{
uint64_t *pte = (uint64_t *)cpu_pt_addr;
// 组合物理地址和标志位
pte[gpu_page_idx] = (addr & AMDGPU_PTE_ADDR_MASK) | flags;
}
PTE 格式(64位):
63 12 11 0
┌──────────────────────────────────────────────────────┬──────────┐
│ Physical Address (52 bits) │ Flags │
│ 页对齐 (低 12 位为 0) │ (12 bits)│
└──────────────────────────────────────────────────────┴──────────┘
标志位详解:
| 位域 | 名称 | 含义 | 值 |
|---|---|---|---|
| [0] | VALID | PTE 有效 | 1 |
| [1] | READABLE | 可读 | 1 |
| [2] | WRITEABLE | 可写 | 1 |
| [3] | EXECUTE | 可执行(某些 GPU) | 0/1 |
| [4:5] | CACHE | 缓存策略 | 00=UC, 01=WC, 10=Cached |
| [6] | SNOOPED | Cache coherent | 0/1 |
| [7] | SYSTEM | 系统内存 | 1 |
| [8:11] | FRAG | Fragment size | 0-9 (4KB-2GB) |
标志位计算示例:
uint64_t amdgpu_ttm_tt_pte_flags(struct amdgpu_device *adev,
struct ttm_tt *ttm,
struct ttm_mem_reg *mem)
{
uint64_t flags = 0;
// 基础权限
flags |= AMDGPU_PTE_VALID; // 位 0
flags |= AMDGPU_PTE_READABLE; // 位 1
if (ttm->page_flags & TTM_PAGE_FLAG_WRITE)
flags |= AMDGPU_PTE_WRITEABLE; // 位 2
// 缓存策略
switch (ttm->caching_state) {
case tt_cached:
flags |= AMDGPU_PTE_SNOOPED; // Cache coherent
break;
case tt_wc:
flags |= AMDGPU_PTE_WC; // Write-combine
break;
case tt_uncached:
// 无额外标志,默认 uncached
break;
}
// 系统内存标记
flags |= AMDGPU_PTE_SYSTEM;
return flags;
}
实际 PTE 值示例:
物理地址: 0x00000000F0123000
标志位: VALID | READABLE | WRITEABLE | WC | SYSTEM
= 0x00000007
PTE = 0x00000000F0123000 | 0x00000007
= 0x00000000F0123007
GPU 读取此 PTE 后:
- 访问 GPU VA → 转换到物理地址 0xF0123000
- 权限: 可读可写
- 缓存: Write-combine 模式
1.6 内存屏障与缓存刷新
(1) mb() - 内存屏障
mb(); // Memory Barrier
作用:
- 确保之前的所有内存写入完成
- 防止 CPU 乱序执行导致 PTE 未写入就刷新 TLB
- x86 编译为
mfence指令
必要性示例:
// 错误:无内存屏障
pte[0] = addr0 | flags;
pte[1] = addr1 | flags;
// CPU 可能乱序执行,先刷新 TLB
flush_tlb(); // PTE 可能还在 CPU cache
// 正确:有内存屏障
pte[0] = addr0 | flags;
pte[1] = addr1 | flags;
mb(); // 强制完成所有写入
flush_tlb(); // PTE 已写入内存
(2) amdgpu_asic_flush_hdp() - HDP 刷新
amdgpu_asic_flush_hdp(adev, NULL);
HDP (Host Data Path):
- CPU 访问 VRAM 的数据路径
- 有自己的写缓存
- 需要显式刷新确保 VRAM 可见
工作流程:
CPU 写入 PTE
↓
CPU Cache
↓ (mb() 刷新)
HDP Write Cache ← 仍在这里
↓ (flush_hdp() 刷新)
VRAM (页表)
↓
GPU 可见
实现(硬件特定):
void amdgpu_asic_flush_hdp(struct amdgpu_device *adev,
struct amdgpu_ring *ring)
{
if (adev->asic_funcs->flush_hdp)
adev->asic_funcs->flush_hdp(adev, ring);
// 典型实现:
// WREG32(mmHDP_MEM_COHERENCY_FLUSH_CNTL, 1);
// 等待完成
}
2. GART 解绑机制
2.1 解绑流程
int amdgpu_gart_unbind(struct amdgpu_device *adev, uint64_t offset, int pages)
{
unsigned t, p, i;
// ========== 1. 安全检查 ==========
if (!adev->gart.ready)
return -EINVAL;
// ========== 2. 清除调试数组 ==========
#ifdef CONFIG_DRM_AMDGPU_GART_DEBUGFS
t = offset / AMDGPU_GPU_PAGE_SIZE;
p = t / AMDGPU_GPU_PAGES_IN_CPU_PAGE;
for (i = 0; i < pages; i++, p++)
adev->gart.pages[p] = NULL;
#endif
// ========== 3. 页表未映射则跳过 ==========
if (!adev->gart.ptr)
return 0;
// ========== 4. 用 dummy page 填充 PTE ==========
t = offset / AMDGPU_GPU_PAGE_SIZE;
for (i = 0; i < pages; i++) {
for (unsigned j = 0; j < AMDGPU_GPU_PAGES_IN_CPU_PAGE; j++) {
amdgpu_gmc_set_pte_pde(adev, adev->gart.ptr, t,
adev->dummy_page_addr,
AMDGPU_PTE_VALID | AMDGPU_PTE_READABLE);
t++;
}
}
// ========== 5. 刷新机制(同绑定) ==========
mb();
amdgpu_asic_flush_hdp(adev, NULL);
for (i = 0; i < adev->num_vmhubs; i++)
amdgpu_gmc_flush_gpu_tlb(adev, 0, i, 0);
return 0;
}
2.2 为什么用 Dummy Page?
对比方案:
| 方案 | PTE 值 | GPU 访问行为 | 优缺点 |
|---|---|---|---|
| 清零 PTE | 0x0 (Invalid) | 页面错误/崩溃 | GPU hang 风险 |
| Dummy Page | dummy_addr | VALID | 读到零值 | 安全降级 |
| 保留旧映射 | 原地址 | 读到已释放页 | 内存安全问题 |
实际场景:
// BO 解绑过程中 GPU 可能仍在访问
ttm_tt_unbind(ttm);
↓
amdgpu_gart_unbind(); // 用 dummy page 填充
↓
GPU 仍在执行着色器,访问已解绑地址
↓
读到 dummy page (全零) → 安全
vs.
读到无效地址 → GPU hang/崩溃 ❌
Dummy Page 标志:
flags = AMDGPU_PTE_VALID | AMDGPU_PTE_READABLE;
// 注意:
// - VALID: GPU 不会产生页面错误
// - READABLE: 允许读取(返回零)
// - 无 WRITEABLE: 写操作被忽略或产生错误(取决于硬件)
3. TLB 管理
3.1 TLB 概述
TLB (Translation Lookaside Buffer):
- GPU 的页表缓存
- 存储最近使用的 PTE
- 避免每次访问都查页表
层级结构:
GPU 内存访问
↓
L1 TLB (每个 CU/Shader Engine)
↓ Miss
L2 TLB (每个 VM Hub)
↓ Miss
Page Table Walk (访问 VRAM 中的页表)
↓
获取 PTE
3.2 VM Hub 架构
什么是 VM Hub?
- GPU 有多个独立的虚拟内存管理单元
- 每个引擎有自己的 Hub 和 TLB
典型配置(RDNA/CDNA):
| Hub ID | 名称 | 用途 | TLB 大小 |
|---|---|---|---|
| 0 | GFX Hub | 图形引擎 | 256 entries |
| 1 | MM Hub | 多媒体(视频编解码) | 128 entries |
| 2 | VC Hub | VCN (视频编码) | 64 entries |
为什么需要多个 Hub?
- 并行处理:图形和视频可同时运行
- 隔离性:不同引擎的内存访问互不干扰
- 性能:每个 Hub 专门优化
3.3 TLB 刷新机制
void amdgpu_gmc_flush_gpu_tlb(struct amdgpu_device *adev,
uint32_t vmid,
uint32_t vmhub,
uint32_t flush_type)
{
// 调用硬件特定函数
if (adev->gmc.gmc_funcs->flush_gpu_tlb)
adev->gmc.gmc_funcs->flush_gpu_tlb(adev, vmid, vmhub, flush_type);
}
参数说明:
| 参数 | 含义 | 典型值 |
|---|---|---|
vmid | 虚拟机 ID(上下文 ID) | 0-15 |
vmhub | VM Hub 索引 | 0=GFX, 1=MM |
flush_type | 刷新类型 | 0=全部, 1=部分 |
硬件实现示例(MMIO 寄存器):
// Vega/RDNA 系列
static void gmc_v9_0_flush_gpu_tlb(struct amdgpu_device *adev,
uint32_t vmid,
uint32_t vmhub,
uint32_t flush_type)
{
// 1. 准备刷新命令
uint32_t req = VM_INVALIDATE_REQ;
// 2. 写入刷新请求寄存器
WREG32_NO_KIQ(hub->vm_inv_eng0_req + eng, req);
// 3. 等待确认
for (i = 0; i < adev->usec_timeout; i++) {
ack = RREG32_NO_KIQ(hub->vm_inv_eng0_ack + eng);
if (ack & (1 << vmid))
break;
udelay(1);
}
if (i >= adev->usec_timeout)
DRM_ERROR("Timeout waiting for VM flush ACK!\n");
}
3.4 TLB 刷新时机
| 操作 | 是否刷新 TLB | 原因 |
|---|---|---|
| 绑定 | ✅ 是 | 新 PTE 需要可见 |
| 解绑 | ✅ 是 | 旧 PTE 需要失效 |
| 修改标志 | ✅ 是 | PTE 内容改变 |
| 只读 PTE | ❌ 否 | 无状态变化 |
| GPU 重置 | ✅ 是 | 清空所有缓存 |
刷新开销:
单次刷新时间: ~10-50 微秒
影响范围: 停止所有内存访问
批量优化: 合并多次刷新 → 1 次
3.5 批量刷新优化
问题:
// 绑定 1000 个页面
for (i = 0; i < 1000; i++) {
amdgpu_gart_bind(adev, offset + i * PAGE_SIZE, 1, ...);
// 每次都刷新 TLB → 1000 次刷新!
}
优化:
// 批量绑定
amdgpu_gart_bind(adev, offset, 1000, ...);
// 只刷新一次 TLB → 性能提升 100x
实现机制:
int amdgpu_gart_bind(...)
{
// 更新所有 PTE
for (i = 0; i < pages; i++)
update_pte(i);
mb(); // 确保所有 PTE 写入完成
// 只刷新一次
flush_tlb(); // ← 批量操作的关键
}
4. 性能优化
4.1 地址转换延迟
TLB 命中 vs 未命中:
| 场景 | 延迟 | 说明 |
|---|---|---|
| L1 TLB 命中 | 1-2 cycles | 最快 |
| L2 TLB 命中 | 10-20 cycles | 较快 |
| Page Table Walk | 100-200 cycles | 访问 VRAM |
| VRAM 页表在系统内存 | 1000+ cycles | 极慢(不应发生) |
影响:
高 TLB 命中率 (95%+):
平均延迟 = 0.95 * 2 + 0.05 * 100 = 6.9 cycles
低 TLB 命中率 (50%):
平均延迟 = 0.50 * 2 + 0.50 * 100 = 51 cycles
4.2 优化策略对比表
| 优化方法 | 实现 | 性能提升 | 适用场景 | 难度 |
|---|---|---|---|---|
| 大页支持 | 使用 2MB/1GB 页 | 10-100x TLB 效率 | 大型连续 BO | 中 |
| 批量刷新 | 合并 TLB 刷新 | 5-10x 绑定速度 | 多页面绑定 | 低 |
| 预取优化 | 连续地址访问 | 2-3x 带宽利用 | 线性访问模式 | 硬件自动 |
| TLB 预热 | 提前绑定热点数据 | 减少首次访问延迟 | 已知访问模式 | 中 |
| GART 分区 | 分离不同用途区域 | 减少冲突 | 多引擎并行 | 高 |
4.3 大页支持详解
页面大小对比:
| 页大小 | TLB Entry 覆盖 | 需要的 Entry 数(1GB 数据) |
|---|---|---|
| 4KB | 4KB | 262,144 |
| 2MB | 2MB | 512 |
| 1GB | 1GB | 1 |
PTE Fragment 字段:
// 在 PTE 标志中指定
flags |= AMDGPU_PTE_FRAG(frag_size);
// frag_size 编码:
// 0 = 4KB (2^12)
// 1 = 8KB (2^13)
// 2 = 16KB (2^14)
// ...
// 9 = 2MB (2^21)
启用大页的条件:
- CPU 页大小 >= 目标大页大小
- 物理地址对齐到大页边界
- 连续的物理内存
实现示例:
// 检查是否可用 2MB 页
if (num_pages >= 512 && // 至少 2MB
(dma_addr & 0x1FFFFF) == 0 && // 2MB 对齐
is_contiguous(dma_addr, 512)) { // 连续
flags |= AMDGPU_PTE_FRAG(9); // 使用 2MB 页
// 只需 1 个 PTE 覆盖 512 个 4KB 页
}
4.4 GART 地址空间布局优化
典型布局(优化后):
GPU 虚拟地址空间:
┌─────────────────────────────┐ 0x0000000000000000
│ 保留区域 (1GB) │
├─────────────────────────────┤ 0x0000000040000000
│ GART - 图形专用 (4GB) │ ← GFX Hub 主要区域
│ - Staging buffers │ TLB 命中率高
│ - 纹理溢出 │
├─────────────────────────────┤ 0x0000000140000000
│ GART - 计算专用 (4GB) │ ← Compute 隔离区域
│ - Userptr │ 减少 TLB 冲突
│ - Compute 输入/输出 │
├─────────────────────────────┤ 0x0000000240000000
│ GART - 视频专用 (2GB) │ ← MM Hub 专用
│ - DMA-BUF │
│ - 视频帧缓冲 │
├─────────────────────────────┤ 0x00000002C0000000
│ AGP Aperture (可选) │
├─────────────────────────────┤ 0x0000000300000000
│ VRAM Aperture │ ← 显存直接映射
│ (物理 VRAM 大小) │ 无 TLB 转换
└─────────────────────────────┘
分区优势:
- 不同引擎访问不同区域 → 减少 TLB 冲突
- 专用区域可预热 TLB
- 便于监控和调试
4.5 性能监控
关键指标:
| 指标 | 正常值 | 异常值 | 问题 |
|---|---|---|---|
| TLB 命中率 | >95% | <80% | 访问模式分散 |
| Page Walk 次数 | <5% 访问 | >20% | TLB 太小或抖动 |
| GART 利用率 | <80% | >95% | 接近耗尽 |
| 绑定延迟 | <1ms/1000页 | >10ms | 可能碎片化 |
5. 调试与监控
5.1 Debugfs 接口
# 查看 GART 信息
cat /sys/kernel/debug/dri/0/amdgpu_gtt_mm
# 输出示例:
# GART: size=16384MB, used=2048MB, free=14336MB
# num_gpu_pages: 4194304
# num_cpu_pages: 4194304
# table_size: 33554432 (32MB)
详细映射信息(CONFIG_DRM_AMDGPU_GART_DEBUGFS):
cat /sys/kernel/debug/dri/0/amdgpu_gart_pages | head -20
# 输出:
# [0] 0xF0000000 (CPU page 0x...)
# [1] 0xF0001000 (CPU page 0x...)
# [2] NULL (unmapped)
5.2 Trace Points
# 启用 GART 绑定跟踪
echo 1 > /sys/kernel/debug/tracing/events/amdgpu/amdgpu_gart_bind/enable
# 运行应用
./my_gpu_app
# 查看跟踪日志
cat /sys/kernel/debug/tracing/trace
# 示例输出:
# amdgpu_gart_bind: offset=0x80000000 pages=256 flags=0x7
# amdgpu_gart_bind: offset=0x80100000 pages=512 flags=0x7
5.3 常见问题诊断
问题 1: “GART bind failed”
原因:
- GART 未初始化(ready=false)
- DMA 地址无效
- 页表未映射
诊断:
dmesg | grep -i gart
# 查找:
# - "GART: num cpu pages ..." (初始化日志)
# - "Failed to allocate GART table" (分配失败)
# - "trying to bind memory to uninitialized GART" (未就绪)
问题 2: GPU Hang (TLB 相关)
症状:
- GPU 访问违例
- dmesg 显示 “VM fault”
诊断:
# 查看 VM fault 日志
dmesg | grep -i "vm fault"
# 输出示例:
# [drm:amdgpu_job_timedout] *ERROR* VM fault (0x01) at page 0x80001234
#
# 分析:
# - 地址 0x80001234 在 GART 范围内
# - 检查该地址是否已绑定
# - 可能是 TLB 未刷新或 PTE 错误
验证 TLB 刷新:
// 添加调试日志
printk("Flushing TLB for vmhub %d\n", i);
amdgpu_gmc_flush_gpu_tlb(adev, 0, i, 0);
printk("TLB flush completed\n");
问题 3: 性能下降
诊断步骤:
-
检查 TLB 命中率(需要性能计数器)
-
查看 GART 使用率
cat /sys/kernel/debug/dri/0/amdgpu_gtt_mm # 如果 used > 95%,可能频繁换页 -
监控绑定频率
# 统计 1 秒内的绑定次数 perf record -e probe:amdgpu_gart_bind -a sleep 1 perf report
5.4 调试技巧
技巧 1: 验证 PTE 内容
void dump_pte(struct amdgpu_device *adev, uint64_t offset)
{
uint64_t *pte = (uint64_t *)adev->gart.ptr;
unsigned idx = offset / AMDGPU_GPU_PAGE_SIZE;
printk("PTE[%u] = 0x%016llx\n", idx, pte[idx]);
printk(" Physical: 0x%016llx\n", pte[idx] & ~0xFFF);
printk(" Valid: %d\n", !!(pte[idx] & AMDGPU_PTE_VALID));
printk(" Readable: %d\n", !!(pte[idx] & AMDGPU_PTE_READABLE));
printk(" Writeable: %d\n", !!(pte[idx] & AMDGPU_PTE_WRITEABLE));
}
技巧 2: 强制 TLB 刷新测试
// 测试 TLB 刷新是否有效
amdgpu_gart_bind(...);
// 故意不刷新 TLB
// 然后访问 → 应该失败(证明 TLB 缓存了旧值)
amdgpu_gart_bind(...);
flush_all_tlb(); // 强制刷新
// 访问 → 应该成功
技巧 3: 监控 HDP 刷新
// 测试 HDP 缓存影响
write_pte(...);
// 不刷新 HDP
read_from_gpu(); // 可能读到旧 PTE
write_pte(...);
flush_hdp();
read_from_gpu(); // 应该读到新 PTE
小结
关键要点
-
绑定机制
- 三层函数:bind → map → set_pte_pde
- 必须刷新 mb、HDP、TLB
- 处理 CPU 页 vs GPU 页差异
-
TLB 管理
- 多个 VM Hub,各自独立 TLB
- 刷新开销大,需要批量优化
- 命中率直接影响性能
-
性能优化
- 大页减少 TLB 压力 10-100x
- 批量刷新减少开销 5-10x
- 地址空间分区减少冲突
-
调试方法
- Debugfs 查看状态
- Trace points 跟踪操作
- 性能计数器监控 TLB
825

被折叠的 条评论
为什么被折叠?



