云计算百科
云计算领域专业知识百科平台

07 - SVM内存迁移机制

难度: 🔴🔴 高级 预计学习时间: 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:

  • migrate_vma_setup(): 准备迁移,锁定页面
  • migrate_vma_pages(): 执行迁移,更新页表
  • migrate_vma_finalize(): 完成迁移,清理资源

  • 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技术的实现与应用深度分析目录
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 07 - SVM内存迁移机制
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!