在现代异构计算架构中,CPU与GPU之间的高效数据交互是性能的关键瓶颈。AMD的共享虚拟内存(SVM)技术通过智能的内存迁移机制,实现了CPU和GPU对同一内存空间的透明访问,极大地简化了编程模型并提升了数据吞吐效率。本文将深入剖析SVM内存迁移的核心实现,揭示其如何在系统RAM和GPU VRAM之间自动、高效地搬运数据,为构建高性能后端计算服务提供底层支撑。
1. 内存迁移:异构计算的“智能物流系统”
想象一下,在一个大型物流中心,CPU和GPU分别是两个独立的仓库管理员。 系统RAM是CPU的本地仓库,CPU访问极快,但GPU需要长途跋涉才能取货; GPU VRAM则是GPU的专属仓库,GPU访问迅速,但CPU要么无法直接访问,要么速度很慢。SVM的内存迁移机制,就是在这两个仓库之间建立的一套智能物流系统。
这套系统的核心搬运工是 SDMA引擎(System DMA),它是GPU内部专门负责数据搬运的硬件单元。而 GART表(Graphics Address Remapping Table)则扮演着“地址翻译清单”的角色,确保SDMA能够准确找到系统内存中的源地址和目标地址。迁移主要分为两个方向:
RAM → VRAM (上行迁移)
触发场景:
- GPU频繁访问某段内存
- 用户显式请求 (prefetch)
- GPU页面错误触发
流程:
┌──────┐ ┌──────┐ ┌──────┐
│ RAM │ →→→ │ SDMA │ →→→ │ VRAM │
│pages │ │ copy │ │pages │
└──────┘ └──────┘ └──────┘
VRAM → RAM (下行迁移)
触发场景:
- CPU需要访问VRAM页面
- 内存不足需要回收VRAM
- 进程退出清理
流程:
┌──────┐ ┌──────┐ ┌──────┐
│ VRAM │ →→→ │ SDMA │ →→→ │ RAM │
│pages │ │ copy │ │pages │
└──────┘ └──────┘ └──────┘
这种双向、按需迁移的能力,使得应用程序可以像使用单一、统一的内存空间一样编程,而无需关心数据实际存放在何处,这正是SVM技术的魅力所在,也是构建高效微服务和后端架构中计算密集型任务的关键基础。
2. 迁移引擎的核心组件与工作流程
SVM的迁移子系统是一个精密的软件工程模块,它紧密集成在Linux内核的HMM(Heterogeneous Memory Management)框架中。其核心组件构成了一个完整的处理流水线:
// 主要组件
struct migrate_vma {
struct vm_area_struct *vma; // 虚拟内存区域
unsigned long start; // 起始地址
unsigned long end; // 结束地址
unsigned long *src; // 源页面数组
unsigned long *dst; // 目标页面数组
unsigned long cpages; // 收集的页面数
// ...
};
整个迁移过程通过三个核心API进行控制,形成了一个典型的两阶段提交模式:
migrate_vma_setup(): 迁移准备阶段。此函数锁定相关内存页面,防止在迁移过程中被修改,并收集需要迁移的页面信息。这类似于物流系统中的“订单确认和货物打包”。migrate_vma_pages(): 迁移执行阶段。这是最关键的一步,它指示SDMA引擎开始实际的数据拷贝,并在拷贝完成后,更新CPU和GPU的页表,使新的物理地址生效。这是“货物运输和仓库入库”的过程。migrate_vma_finalize(): 迁移完成阶段。清理临时资源,释放锁定的页面。完成“物流单据归档和场地清理”。
理解这个流程对于诊断迁移性能问题至关重要,尤其是在涉及大量数据交换的数据库或AI推理中间件中。
[AFFILIATE_SLOT_1]3. 上行迁移:从RAM到VRAM的深度剖析
当GPU需要访问某个数据,但该数据还存放在系统RAM中时,就会触发上行迁移。这个过程的主入口函数是:
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,在多GPU系统中尤为重要。start_mgr/last_mgr: 定义了需要迁移的内存范围(起始页号和页数)。mm: 进程的内存描述符,用于在正确的虚拟地址空间内操作。trigger: 迁移触发原因,帮助内核进行不同的优化决策。
具体的迁移流程是一个精细化的操作序列:
┌─────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────┘
其中,migrate_vma_setup是Linux HMM框架提供的核心函数,它的作用是:
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页可迁移)
4. SDMA与GART:数据搬运的硬件加速与地址翻译
实际的数据拷贝工作由GPU的SDMA引擎完成,这是迁移性能的保障。相关函数为:
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本质上是GPU的IOMMU(输入输出内存管理单元),它解决了SDMA引擎无法直接理解系统物理地址的问题。其原理是:
问题: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
具体的映射过程由以下函数实现:
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 │
│ ... │
└─────────────────────────────────────────────────────────┘
重点提示:理解GART是理解整个SVM迁移机制的基石。它就像连接CPU地址世界和GPU地址世界的桥梁和翻译官。
[AFFILIATE_SLOT_2]5. 迁移优化策略与最佳实践
在实际的后端架构中,直接进行大规模迁移会面临各种挑战。SVM实现了一系列优化技术来保证系统的响应性和效率。
5.1 分批处理:为了避免一次性占用过多资源(如有限的GART窗口)或长时间持有锁,迁移操作会被自动分成多个批次执行。
// 避免一次迁移过大的范围
const u64 GTT_MAX_PAGES = AMDGPU_GTT_MAX_TRANSFER_SIZE; // 256MB
while (npages) {
size = min(GTT_MAX_PAGES, npages);
// 迁移 size 页
npages -= size;
}
5.2 部分迁移:并非所有页面都能被迁移。内核需要灵活处理“钉子户”页面。
// migrate_vma_setup 可能只收集部分页面
if (cpages != npages)
pr_debug("partial migration, 0x%lx/0x%llx pages\n",
cpages, npages);
5.3 异步执行:SDMA操作是异步的,内核提交拷贝任务后立即返回一个“fence”(围栏),后续可以等待这个fence完成,从而实现CPU与GPU的并行。
// SDMA操作返回fence,允许异步执行
struct dma_fence *mfence = NULL;
// 提交SDMA任务(立即返回)
svm_migrate_copy_memory_gart(..., &mfence);
// 继续其他工作...
migrate_vma_pages(&migrate);
// 需要时等待完成
svm_migrate_copy_done(adev, mfence);
5.4 利用先进互连:在AMD的XGMI(Infinity Fabric)等高带宽互连架构下,CPU可以直接访问其他设备的VRAM,这从根本上减少了迁移的需求。
// 对于XGMI连接的GPU,可以直接访问
if (adev->gmc.xgmi.connected_to_cpu) {
// 使用设备一致性内存
migrate.flags = MIGRATE_VMA_SELECT_DEVICE_COHERENT;
} else {
// 使用设备私有内存
migrate.flags = MIGRATE_VMA_SELECT_DEVICE_PRIVATE;
}
⚠️ 开发者常见陷阱:
❌ 陷阱1:忘记等待SDMA的fence,导致在数据未就绪时就访问。
✅ 正确做法:务必调用svm_migrate_copy_done()等待异步操作完成。
❌ 陷阱2:试图迁移被pin_user_pages锁定的页面(常见于RDMA或GPU直接访问场景)。
✅ 正确做法:检查cpages和mpages返回值,妥善处理部分迁移失败的情况。
6. 总结与展望
AMD SVM的内存迁移机制是一个融合了Linux内核内存管理、硬件DMA引擎和GPU地址翻译的复杂系统。它通过双向按需迁移、基于GART的地址重映射、SDMA硬件加速拷贝以及异步分批执行等关键技术,实现了CPU与GPU内存空间的统一管理。这不仅降低了异构编程的门槛,也为高性能计算、AI推理中间件和实时数据库提供了至关重要的底层支持。
掌握迁移机制后,下一步便是理解GPU如何通过页表来访问这些被迁移的页面,即“页面映射与GPU页表”机制,这将构成SVM技术栈的完整闭环。
实践与探索:
你可以通过以下方式加深理解:
追踪一次完整的迁移内核路径:
# 启用迁移调试
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映射实现
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
浙公网安备 33010602011771号