难度: 🔴🔴 高级 预计学习时间: 2.5-3小时 前置知识: 第6章、DMA基础、SDMA/TTM概念
📋 概述
内存迁移是SVM的核心特性,负责在系统RAM和GPU VRAM之间自动移动页面。打个比方,迁移就像在两个仓库间搬货物:
- 🏢 系统RAM: CPU的本地仓库,访问快但GPU访问慢
- 🏭 GPU VRAM: GPU的本地仓库,GPU访问快但CPU访问慢(或无法访问)
- 🚚 SDMA引擎: GPU的搬运工,专门负责数据搬运
- 📋 GART表: 地址翻译清单,让SDMA找到正确地址
本章深入迁移机制的实现细节。
7.1 迁移机制概览
迁移的两个方向
RAM → VRAM (上行迁移)
触发场景:
– GPU频繁访问某段内存
– 用户显式请求 (prefetch)
– GPU页面错误触发
流程:
┌──────┐ ┌──────┐ ┌──────┐
│ RAM │ →→→ │ SDMA │ →→→ │ VRAM │
│pages │ │ copy │ │pages │
└──────┘ └──────┘ └──────┘
VRAM → RAM (下行迁移)
触发场景:
– CPU需要访问VRAM页面
– 内存不足需要回收VRAM
– 进程退出清理
流程:
┌──────┐ ┌──────┐ ┌──────┐
│ VRAM │ →→→ │ SDMA │ →→→ │ RAM │
│pages │ │ copy │ │pages │
└──────┘ └──────┘ └──────┘
迁移子系统组件
// 主要组件
struct migrate_vma {
struct vm_area_struct *vma; // 虚拟内存区域
unsigned long start; // 起始地址
unsigned long end; // 结束地址
unsigned long *src; // 源页面数组
unsigned long *dst; // 目标页面数组
unsigned long cpages; // 收集的页面数
// …
};
核心API:
7.2 从RAM到VRAM的迁移
主函数:svm_migrate_ram_to_vram
int svm_migrate_ram_to_vram(struct svm_range *prange,
uint32_t best_loc,
unsigned long start_mgr,
unsigned long last_mgr,
struct mm_struct *mm,
uint32_t trigger)
参数详解:
- best_loc: 目标GPU节点ID
- start_mgr/last_mgr: 迁移范围(页号)
- mm: 进程的mm_struct
- trigger: 触发原因(KFD_MIGRATE_TRIGGER_xxx)
迁移流程详解
┌─────────────────────────────────────────┐
│ 1. 预检查 │
│ – 验证范围有效性 │
│ – 获取目标GPU节点 │
│ – 预留VRAM空间 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. 分配VRAM │
│ svm_range_vram_node_new() │
│ – 通过TTM分配VRAM │
│ – 记录在prange->ttm_res │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. 按VMA遍历迁移 │
│ for each VMA in [start, end]: │
│ svm_migrate_vma_to_vram() │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. 更新统计信息 │
│ pdd->page_in += mpages │
└─────────────────────────────────────────┘
svm_migrate_vma_to_vram 详解
/**
* svm_migrate_ram_to_vram – migrate svm range from system to device
* @prange: range structure
* @best_loc: the device to migrate to
* @start_mgr: start page to migrate
* @last_mgr: last page to migrate
* @mm: the process mm structure
* @trigger: reason of migration
*
* Context: Process context, caller hold mmap read lock, svms lock, prange lock
*
* Return:
* 0 – OK, otherwise error code
*/
static long svm_migrate_vma_to_vram(struct kfd_node *node,
struct svm_range *prange,
struct vm_area_struct *vma,
uint64_t start, uint64_t end,
uint32_t trigger, u64 ttm_res_offset)
{
// 1. 准备迁移上下文
struct migrate_vma migrate;
memset(&migrate, 0, sizeof(migrate));
migrate.vma = vma;
migrate.start = start;
migrate.end = end;
migrate.flags = MIGRATE_VMA_SELECT_SYSTEM; // 只迁移系统内存
migrate.pgmap_owner = SVM_ADEV_PGMAP_OWNER(adev);
// 2. 分配临时缓冲区
// src数组 + dst数组 + DMA地址数组
buf = kvcalloc(npages,
2 * sizeof(*migrate.src) + sizeof(u64) + sizeof(dma_addr_t),
GFP_KERNEL);
migrate.src = buf;
migrate.dst = migrate.src + npages;
scratch = (dma_addr_t *)(migrate.dst + npages);
// 3. 收集需要迁移的页面
r = migrate_vma_setup(&migrate);
if (r) {
// 失败可能是页面已被锁定或正在使用
}
cpages = migrate.cpages; // 实际收集的页面数
// 4. 执行SDMA拷贝
r = svm_migrate_copy_to_vram(node, prange, &migrate,
&mfence, scratch, ttm_res_offset);
// 5. 更新页表
migrate_vma_pages(&migrate);
// 6. 等待SDMA完成
svm_migrate_copy_done(adev, mfence);
// 7. 完成迁移
migrate_vma_finalize(&migrate);
// 8. 清理DMA映射
svm_range_dma_unmap_dev(adev->dev, scratch, 0, npages);
return mpages; // 成功迁移的页面数
}
migrate_vma_setup 做了什么?
这是Linux内核HMM框架提供的API:
1. 锁定VMA和页表
2. 遍历页表,找到满足条件的页面:
– 页面在系统RAM (MIGRATE_VMA_SELECT_SYSTEM)
– 页面未被锁定 (pin_user_pages)
– 页面不是特殊页 (VM_SPECIAL)
3. 增加页面引用计数,防止被释放
4. 记录到migrate.src数组:
src[i] = page_to_pfn(page) | MIGRATE_PFN_VALID
示例:
VMA: [0x1000-0x3000] 8页
页0: 在RAM → src[0] = PFN(页0) | MIGRATE_PFN_VALID
页1: 在RAM → src[1] = PFN(页1) | MIGRATE_PFN_VALID
页2: 已锁定 → src[2] = 0 (跳过)
页3: 在RAM → src[3] = PFN(页3) | MIGRATE_PFN_VALID
…
cpages = 3 (实际只有3页可迁移)
7.3 SDMA拷贝实现
svm_migrate_copy_to_vram
static int svm_migrate_copy_to_vram(struct kfd_node *node,
struct svm_range *prange,
struct migrate_vma *migrate,
struct dma_fence **mfence,
dma_addr_t *scratch,
u64 ttm_res_offset)
{
u64 npages = migrate->npages;
struct amdgpu_device *adev = node->adev;
struct device *dev = adev->dev;
dma_addr_t *src;
u64 *dst;
int i, j;
// 1. 为源和目标分配地址数组
src = scratch;
dst = (u64 *)(src + npages);
// 2. 映射源页面到设备可见地址
r = svm_migrate_copy_to_vram_map_src(dev, migrate, scratch, &npages);
// 3. 准备目标VRAM地址
r = svm_migrate_copy_to_vram_map_dst(node, prange, migrate,
dst, ttm_res_offset);
// 4. 通过GART执行SDMA拷贝
r = svm_migrate_copy_memory_gart(adev, src, dst, npages,
FROM_RAM_TO_VRAM, mfence);
return r;
}
GART映射原理
什么是GART?
GART (Graphics Address Remapping Table) 是GPU的MMU,类似CPU的页表。
问题:SDMA只能访问GPU地址空间
系统RAM不在GPU地址空间内
解决:GART映射
1. 将系统RAM的物理地址映射到GART表
2. GART表项指向系统RAM
3. SDMA通过GART地址访问系统RAM
示例:
RAM物理地址: 0x123456000
GART表入口: entry[0] = 0x123456000 + flags
GART虚拟地址: 0xA00000000 (GPU地址空间)
SDMA读取 0xA00000000 → 硬件查GART表 → 访问 0x123456000
svm_migrate_copy_memory_gart
static int svm_migrate_copy_memory_gart(struct amdgpu_device *adev,
dma_addr_t *sys, // 系统内存DMA地址
u64 *vram, // VRAM地址
u64 npages,
enum MIGRATION_COPY_DIR direction,
struct dma_fence **mfence)
{
const u64 GTT_MAX_PAGES = AMDGPU_GTT_MAX_TRANSFER_SIZE; // 256MB
struct amdgpu_ring *ring = adev->mman.buffer_funcs_ring;
u64 gart_s, gart_d;
mutex_lock(&adev->mman.gtt_window_lock);
// 分批处理(每次最多256MB)
while (npages) {
size = min(GTT_MAX_PAGES, npages);
if (direction == FROM_RAM_TO_VRAM) {
// 1. 映射源系统内存到GART
r = svm_migrate_gart_map(ring, size, sys, &gart_s,
KFD_IOCTL_SVM_FLAG_GPU_RO);
// 2. 目标VRAM直接映射
gart_d = svm_migrate_direct_mapping_addr(adev, *vram);
// 3. SDMA拷贝: GART地址 → VRAM地址
r = amdgpu_copy_buffer(ring, gart_s, gart_d,
size * PAGE_SIZE,
NULL, &next, false, true, false);
} else if (direction == FROM_VRAM_TO_RAM) {
// 反向操作
}
// 更新fence链
dma_fence_put(*mfence);
*mfence = next;
// 更新指针
npages -= size;
sys += size;
*vram += size * PAGE_SIZE;
}
mutex_unlock(&adev->mman.gtt_window_lock);
return r;
}
GART映射详细流程:
static int svm_migrate_gart_map(struct amdgpu_ring *ring,
u64 npages,
dma_addr_t *addr, // 系统内存DMA地址
u64 *gart_addr, // 输出:GART虚拟地址
u64 flags)
{
struct amdgpu_device *adev = ring->adev;
// 1. 使用GART窗口0(预留的映射空间)
*gart_addr = adev->gmc.gart_start;
// 2. 准备PTE(页表项)标志
pte_flags = AMDGPU_PTE_VALID | AMDGPU_PTE_READABLE;
pte_flags |= AMDGPU_PTE_SYSTEM | AMDGPU_PTE_SNOOPED;
if (!(flags & KFD_IOCTL_SVM_FLAG_GPU_RO))
pte_flags |= AMDGPU_PTE_WRITEABLE;
// 3. 分配Job(GPU命令缓冲区)
r = amdgpu_job_alloc_with_ib(adev, &adev->mman.high_pr,
..., &job, ...);
// 4. 生成GART更新命令
// 将DMA地址数组转换为PTE条目
cpu_addr = &job->ibs[0].ptr[num_dw];
amdgpu_gart_map(adev, 0, npages, addr, pte_flags, cpu_addr);
// 5. 使用SDMA将PTE写入GART表
src_addr = job->ibs[0].gpu_addr + offset; // PTE数据的GPU地址
dst_addr = amdgpu_bo_gpu_offset(adev->gart.bo); // GART表的GPU地址
amdgpu_emit_copy_buffer(adev, &job->ibs[0],
src_addr, dst_addr, num_bytes, 0);
// 6. 提交并获取fence
fence = amdgpu_job_submit(job);
dma_fence_put(fence);
return 0;
}
图解GART映射过程:
步骤1: 准备PTE数据
┌────────────────────────┐
│ CPU生成PTE数组 │
│ PTE[0] = sys_addr[0] │
│ PTE[1] = sys_addr[1] │
│ … │
└────────────────────────┘
↓
步骤2: SDMA写入GART表
┌────────────────────────┐ SDMA拷贝 ┌────────────────────────┐
│ GPU内存中的PTE数据 │ ────────→ │ GART表(在VRAM) │
│ @ job->ibs[0].gpu_addr │ │ @ adev->gart.bo │
└────────────────────────┘ └────────────────────────┘
↓
步骤3: 映射生效
┌─────────────────────────────────────────────────────────┐
│ GART地址空间 │
│ [0xA00000000-0xA00001000] → 系统RAM @ 0x123456000 │
│ [0xA00001000-0xA00002000] → 系统RAM @ 0x123457000 │
│ … │
└─────────────────────────────────────────────────────────┘
7.4 从VRAM到RAM的迁移
主函数:svm_migrate_vram_to_ram
// 文件: kfd_migrate.c, 行: 800-920
int svm_migrate_vram_to_ram(struct svm_range *prange,
struct mm_struct *mm,
unsigned long start_mgr,
unsigned long last_mgr,
uint32_t trigger,
struct page *fault_page)
关键特性:
- fault_page: CPU页面错误时传入,指定具体的故障页
- 下行迁移通常由CPU页面错误触发
迁移流程
1. 检查actual_loc
if (!prange->actual_loc)
return 0; // 已经在RAM,无需迁移
2. 按VMA遍历
for each VMA:
svm_migrate_vma_to_ram()
3. 更新页面计数
prange->vram_pages -= mpages
4. 释放VRAM(如果全部迁移完)
if (prange->vram_pages == 0)
svm_range_vram_node_free()
svm_migrate_vma_to_ram 详解
static long svm_migrate_vma_to_ram(struct kfd_node *node,
struct svm_range *prange,
struct vm_area_struct *vma,
uint64_t start, uint64_t end,
uint32_t trigger,
struct page *fault_page)
{
// 1. 准备迁移上下文
memset(&migrate, 0, sizeof(migrate));
migrate.vma = vma;
migrate.start = start;
migrate.end = end;
migrate.pgmap_owner = SVM_ADEV_PGMAP_OWNER(adev);
// 选择设备页面(在VRAM的页面)
if (adev->gmc.xgmi.connected_to_cpu)
migrate.flags = MIGRATE_VMA_SELECT_DEVICE_COHERENT;
else
migrate.flags = MIGRATE_VMA_SELECT_DEVICE_PRIVATE;
migrate.fault_page = fault_page; // 指定故障页
// 2. 收集VRAM页面
r = migrate_vma_setup(&migrate);
cpages = migrate.cpages;
// 3. 分配RAM页面并执行SDMA拷贝
r = svm_migrate_copy_to_ram(adev, prange, &migrate,
&mfence, scratch, npages);
// 4. 更新页表
migrate_vma_pages(&migrate);
// 5. 等待完成
svm_migrate_copy_done(adev, mfence);
migrate_vma_finalize(&migrate);
return mpages;
}
svm_migrate_copy_to_ram
static int svm_migrate_copy_to_ram(struct amdgpu_device *adev,
struct svm_range *prange,
struct migrate_vma *migrate,
struct dma_fence **mfence,
dma_addr_t *scratch, u64 npages)
{
// 1. 分配目标RAM页面
for (i = 0, j = 0; i < npages; i++) {
if (migrate->src[i] & MIGRATE_PFN_VALID) {
// 分配新页面
dst_page = alloc_page_vma(GFP_HIGHUSER, vma, addr);
// 映射到设备可见地址
r = svm_migrate_copy_to_ram_map_page(dev, dst_page,
&scratch[j]);
// 记录目标页面
migrate->dst[i] = migrate_pfn(page_to_pfn(dst_page));
// 准备源VRAM地址
src[j] = svm_migrate_direct_mapping_addr(adev,
prange->start + i – prange->offset);
j++;
}
}
// 2. SDMA拷贝: VRAM → RAM
r = svm_migrate_copy_memory_gart(adev, scratch, src, npages,
FROM_VRAM_TO_RAM, mfence);
return r;
}
7.5 迁移优化技术
7.5.1 分批处理
// 避免一次迁移过大的范围
const u64 GTT_MAX_PAGES = AMDGPU_GTT_MAX_TRANSFER_SIZE; // 256MB
while (npages) {
size = min(GTT_MAX_PAGES, npages);
// 迁移 size 页
npages -= size;
}
原因:
- GART窗口有限(通常256MB)
- 避免长时间锁定gtt_window_lock
- 允许其他操作穿插执行
7.5.2 部分迁移
// migrate_vma_setup 可能只收集部分页面
if (cpages != npages)
pr_debug("partial migration, 0x%lx/0x%llx pages\\n",
cpages, npages);
常见原因:
- 页面被pin_user_pages锁定(RDMA、GPU直接访问)
- 页面正在被其他操作使用
- 页面是特殊页(huge pages, VM_SPECIAL)
7.5.3 异步执行
// SDMA操作返回fence,允许异步执行
struct dma_fence *mfence = NULL;
// 提交SDMA任务(立即返回)
svm_migrate_copy_memory_gart(..., &mfence);
// 继续其他工作…
migrate_vma_pages(&migrate);
// 需要时等待完成
svm_migrate_copy_done(adev, mfence);
7.5.4 XGMI优化
// 对于XGMI连接的GPU,可以直接访问
if (adev->gmc.xgmi.connected_to_cpu) {
// 使用设备一致性内存
migrate.flags = MIGRATE_VMA_SELECT_DEVICE_COHERENT;
} else {
// 使用设备私有内存
migrate.flags = MIGRATE_VMA_SELECT_DEVICE_PRIVATE;
}
XGMI(AMD Infinity Fabric):
- CPU可以直接访问VRAM
- 无需迁移即可访问
- 性能接近本地内存
7.6 迁移触发场景
触发原因(trigger参数)
enum KFD_MIGRATE_TRIGGERS {
KFD_MIGRATE_TRIGGER_PREFETCH, // 用户预取
KFD_MIGRATE_TRIGGER_PAGEFAULT_GPU,// GPU页面错误
KFD_MIGRATE_TRIGGER_PAGEFAULT_CPU,// CPU页面错误
KFD_MIGRATE_TRIGGER_TTM_EVICTION, // TTM驱逐(内存不足)
KFD_MIGRATE_TRIGGER_SUSPEND, // 系统挂起
};
触发流程示例
场景1:GPU页面错误
1. GPU访问地址0x1234
↓
2. 地址未映射,触发GPU页面错误
↓
3. XNACK重试机制暂停GPU执行
↓
4. GPU中断CPU
↓
5. 中断处理程序调用:
svm_range_restore_pages()
↓
6. 检查页面位置:
if (在RAM)
→ 只需建立映射
else
→ 调用svm_migrate_ram_to_vram()
↓
7. 迁移完成,更新GPU页表
↓
8. XNACK恢复GPU执行
场景2:CPU页面错误
1. CPU访问VRAM地址0x5678
↓
2. 页表项标记为设备私有,触发页面错误
↓
3. 内核页面错误处理程序调用:
vm_ops->fault()
↓
4. AMDGPU驱动注册的处理函数:
svm_migrate_to_ram()
↓
5. 调用svm_migrate_vram_to_ram()
参数: fault_page = vmf->page
↓
6. 迁移完成,更新CPU页表
↓
7. CPU重新执行访问指令
场景3:用户预取
用户程序:
hipMemPrefetchAsync(ptr, size, deviceId);
↓
内核处理:
svm_range_prefault()
↓
svm_migrate_ram_to_vram(trigger=PREFETCH)
7.7 代码示例
完整迁移示例
// 将一个范围迁移到GPU 0
struct kfd_process *p = kfd_get_process(current);
struct svm_range *prange = ...;
uint32_t gpu_id = 0;
// 锁定必要的锁
mmap_read_lock(p->mm);
mutex_lock(&prange->migrate_mutex);
// 执行迁移
r = svm_migrate_ram_to_vram(prange,
gpu_id,
prange->start, // 整个范围
prange->last,
p->mm,
KFD_MIGRATE_TRIGGER_PREFETCH);
if (r > 0) {
pr_info("migrated %d pages to GPU %u\\n", r, gpu_id);
prange->actual_loc = gpu_id;
} else if (r < 0) {
pr_err("migration failed: %d\\n", r);
}
mutex_unlock(&prange->migrate_mutex);
mmap_read_unlock(p->mm);
监控迁移统计
// 每个GPU有独立的统计
struct kfd_process_device *pdd = ...;
pr_info("GPU %u statistics:\\n", pdd->dev->id);
pr_info(" Pages migrated in: %llu\\n", pdd->page_in);
pr_info(" Pages migrated out: %llu\\n", pdd->page_out);
💡 重点提示
GART是关键:理解GART映射机制是理解迁移的基础。
两阶段提交:
- SDMA拷贝数据(异步)
- migrate_vma_pages 更新页表(同步)
部分迁移是常态:不要期望所有页面都能迁移。
锁顺序:mmap_lock → migrate_mutex,避免死锁。
异步操作:SDMA返回fence,允许并发执行。
⚠️ 常见陷阱
❌ 陷阱1:“忘记等待SDMA完成”
- ✅ 正确:调用svm_migrate_copy_done()等待fence。
❌ 陷阱2:“大范围迁移不分批”
- ✅ 正确:使用GTT_MAX_PAGES分批处理。
❌ 陷阱3:“假设所有页面都能迁移”
- ✅ 正确:检查cpages和mpages,处理部分失败。
❌ 陷阱4:“在中断上下文调用迁移”
- ✅ 正确:迁移需要进程上下文(可能睡眠)。
📝 实践练习
追踪迁移路径:
# 启用迁移调试
echo 'file kfd_migrate.c +p' > /sys/kernel/debug/dynamic_debug/control
# 运行GPU程序并观察迁移
dmesg | grep "svm_migrate"
查看迁移统计:
# 查看进程的SVM统计
cat /sys/kernel/debug/kfd/proc/<pid>/svm_ranges
思考题:
- 为什么需要GART表?不能直接拷贝吗?
- CPU页面错误时为什么要传入fault_page?
- 如果迁移过程中页面被修改会怎样?
- XGMI连接的优势是什么?
代码探索:
# 查看GART映射实现
grep -A 50 "svm_migrate_gart_map" drivers/gpu/drm/amd/amdkfd/kfd_migrate.c
# 查看SDMA拷贝命令
grep -A 20 "amdgpu_emit_copy_buffer" drivers/gpu/drm/amd/amdgpu/amdgpu_job.c
📚 本章小结
- 两个方向:RAM→VRAM(上行)和VRAM→RAM(下行)
- 核心API:HMM框架的migrate_vma_xxx系列函数
- GART映射:让SDMA能够访问系统RAM
- SDMA拷贝:GPU的DMA引擎执行实际数据搬运
- 异步执行:使用fence机制允许并发
- 分批处理:避免长时间占用资源
- 部分迁移:灵活处理不可迁移的页面
📖 扩展阅读
- AMDGPU的GART设计与实现分析
- AMD KFD的BO设计分析
➡️ 下一步
掌握了迁移机制后,下一章我们将学习页面映射与GPU页表——如何让GPU能够访问这些页面。
🔗 导航
- 上一章:06 – SVM范围管理
- 下一章:08 – 页面映射与GPU页表
- 返回目录: AMD ROCm-SVM技术的实现与应用深度分析目录
网硕互联帮助中心




评论前必须登录!
注册