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

05 - 进程与SVM的关系

难度: 🟡 进阶 预计学习时间: 1-1.5小时 前置知识: 前面章节内容、Linux进程管理基础


📋 概述

在AMDGPU驱动中,每个使用GPU计算的进程都有一个kfd_process结构。SVM功能是以进程为单位组织的——每个进程都有独立的SVM范围列表。本章将深入探讨进程如何管理SVM,以及在进程生命周期的各个阶段SVM如何初始化、使用和清理。

想象一下进程就像一个图书馆:

  • 📖 kfd_process 是图书馆本身
  • 📚 svm_range_list 是图书管理系统
  • 📘 每个 svm_range 是一本书
  • 🔑 进程拥有所有书籍的管理权

5.1 kfd_process 中的 SVM 管理

kfd_process 结构体

// 文件: kfd_priv.h
struct kfd_process {
// === 进程身份 ===
struct hlist_node kfd_processes; // 全局进程哈希表节点
void *mm; // mm_struct指针(不持有引用)
struct task_struct *lead_thread; // 主线程
uint32_t pasid; // Process Address Space ID
u16 context_id; // 上下文ID

// === 引用计数 ===
struct kref ref; // 引用计数
struct work_struct release_work; // 释放工作

// === 同步保护 ===
struct mutex mutex; // 进程互斥锁

// === GPU设备 ===
struct kfd_process_device *pdds[MAX_GPU_INSTANCE]; // 每GPU的设备数据
uint32_t n_pdds; // 设备数量

// === 队列管理 ===
struct process_queue_manager pqm; // 队列管理器

// === SVM管理 ★ 重点 ===
struct svm_range_list svms; // SVM范围列表
bool xnack_enabled; // XNACK是否启用

// === MMU通知 ===
struct mmu_notifier mmu_notifier; // MMU通知器

// === 事件管理 ===
struct mutex event_mutex;
struct idr event_idr;
struct kfd_signal_page *signal_page;

// === 内存驱逐 ===
void *kgd_process_info; // GPU驱动信息
struct dma_fence __rcu *ef; // 驱逐fence
struct delayed_work eviction_work;
struct delayed_work restore_work;

// === 调试支持 ===
bool debug_trap_enabled;
struct kfd_process *debugger_process;

// === 其他 ===
bool is_32bit_user_mode;
struct kobject *kobj;
// …更多字段
};

SVM在进程中的位置

kfd_process (进程)

├─ pdds[0..7] (每GPU设备数据)
│ ├─ GPU 0相关数据
│ ├─ GPU 1相关数据
│ └─ …

├─ pqm (队列管理)
│ ├─ 计算队列
│ └─ SDMA队列

├─ svms ★ (SVM管理)
│ ├─ objects (区间树:快速查找)
│ ├─ list (链表:顺序遍历)
│ ├─ restore_work (恢复工作)
│ └─ deferred_list (延迟处理)

└─ mmu_notifier (MMU通知)
└─ 监听CPU页表变化

进程与SVM的关系

一对一关系:

一个进程 ←→ 一个svm_range_list
←→ 多个svm_range

示例:

// 获取进程的SVM管理结构
struct kfd_process *p = ...;
struct svm_range_list *svms = &p->svms;

// 查找包含某地址的SVM范围
struct svm_range *prange;
prange = svm_range_from_addr(svms, addr, NULL);

// 遍历进程的所有SVM范围
list_for_each_entry(prange, &svms->list, list) {
pr_debug("Range [0x%lx-0x%lx]\\n", prange->start, prange->last);
}


5.2 进程创建时的SVM初始化

进程创建流程

用户打开 /dev/kfd

kfd_open()

kfd_create_process(current)

create_process(thread, true)

┌─────────────────────────┐
│1. 分配kfd_process │
│2. 初始化基本字段 │
│3. 初始化队列管理器 │
│4. 初始化apertures │
│5. ★svm_range_list_init()│ ← SVM初始化
│6. 注册MMU notifier │
│7. 添加到全局哈希表 │
└─────────────────────────┘

svm_range_list_init 详解

// 文件: kfd_svm.c
int svm_range_list_init(struct kfd_process *p)
{
struct svm_range_list *svms = &p->svms;
int i;

// 1. 初始化区间树(红黑树)
svms->objects = RB_ROOT_CACHED; // 空的红黑树

// 2. 初始化互斥锁
mutex_init(&svms->lock);

// 3. 初始化链表
INIT_LIST_HEAD(&svms->list);

// 4. 初始化状态计数器
atomic_set(&svms->evicted_ranges, 0); // 驱逐计数
atomic_set(&svms->drain_pagefaults, 0); // 排空标志

// 5. 初始化工作队列
INIT_DELAYED_WORK(&svms->restore_work, svm_range_restore_work);
INIT_WORK(&svms->deferred_list_work, svm_range_deferred_list_work);

// 6. 初始化延迟列表
INIT_LIST_HEAD(&svms->deferred_range_list);
spin_lock_init(&svms->deferred_list_lock);

// 7. 初始化CRIU列表
INIT_LIST_HEAD(&svms->criu_svm_metadata_list);

// 8. 检查哪些GPU支持SVM
for (i = 0; i < p->n_pdds; i++) {
if (KFD_IS_SVM_API_SUPPORTED(p->pdds[i]->dev->adev)) {
bitmap_set(svms->bitmap_supported, i, 1);
}
}

// 9. 设置默认粒度
svms->default_granularity = min_t(u8, amdgpu_svm_default_granularity, 0x1B);
pr_debug("Default SVM Granularity: %d\\n", svms->default_granularity);

return 0;
}

初始化后的状态:

p->svms:
├─ objects: 空的区间树
├─ list: 空链表
├─ lock: 已初始化
├─ evicted_ranges: 0
├─ drain_pagefaults: 0
├─ restore_work: 已初始化(未调度)
├─ deferred_list_work: 已初始化(未调度)
├─ bitmap_supported: [1][1][0][0][0][0][0][0]
│ (假设GPU 0和1支持SVM)
└─ default_granularity: 9 (512页 = 2MB)

GPU支持检查

// KFD_IS_SVM_API_SUPPORTED 宏定义
#define KFD_IS_SVM_API_SUPPORTED(adev) \\
((adev)->kfd.pgmap.type != 0 || (adev)->apu_prefer_gtt)

检查条件:

  • 设备内存已注册:pgmap.type != 0(dGPU有VRAM)
  • 或者APU偏好GTT:apu_prefer_gtt(APU使用系统内存)
  • 示例:

    // 4个GPU系统
    for (i = 0; i < p->n_pdds; i++) {
    struct amdgpu_device *adev = p->pdds[i]->dev->adev;

    if (adev->kfd.pgmap.type != 0) {
    // dGPU,VRAM已注册
    bitmap_set(svms->bitmap_supported, i, 1);
    pr_debug("GPU %d: dGPU with VRAM, SVM supported\\n", i);
    } else if (adev->apu_prefer_gtt) {
    // APU,使用系统内存
    bitmap_set(svms->bitmap_supported, i, 1);
    pr_debug("GPU %d: APU with GTT, SVM supported\\n", i);
    } else {
    pr_debug("GPU %d: SVM not supported\\n", i);
    }
    }

    XNACK支持检查

    // 在create_process()中
    process->xnack_enabled = kfd_process_xnack_mode(process, false);

    XNACK (Exception No ACK) 是GPU页面重试机制:

    XNACK启用:
    GPU访问地址 → 缺页 → 发送中断 → GPU暂停 → 驱动处理 → GPU重试 → 成功

    XNACK禁用:
    GPU访问地址 → 缺页 → GPU挂起(无法恢复)

    检查方式:

    bool kfd_process_xnack_mode(struct kfd_process *p, bool supported)
    {
    // 检查所有GPU是否支持XNACK
    for (i = 0; i < p->n_pdds; i++) {
    if (!p->pdds[i]->dev->kfd->device_info.needs_iommu_device)
    return false; // 不支持
    }
    return true;
    }


    5.3 进程销毁时的清理

    进程销毁流程

    进程退出

    kfd_process_wq_release()

    kfd_process_destroy_delayed()

    ┌──────────────────────────┐
    │1. 取消所有队列 │
    │2. 释放事件资源 │
    │3. ★ svm_range_list_fini()│ ← SVM清理
    │4. 释放apertures │
    │5. 销毁pdds │
    │6. 从哈希表移除 │
    │7. 释放kfd_process │
    └──────────────────────────┘

    svm_range_list_fini 详解

    // 文件: kfd_svm.c
    void svm_range_list_fini(struct kfd_process *p)
    {
    struct svm_range *prange;
    struct svm_range *next;

    pr_debug("process pid %d svms 0x%p\\n",
    p->lead_thread->pid, &p->svms);

    // 1. 取消恢复工作
    cancel_delayed_work_sync(&p->svms.restore_work);

    // 2. 确保延迟工作完成
    flush_work(&p->svms.deferred_list_work);

    // 3. 停止新的缺页处理
    atomic_set(&p->svms.drain_pagefaults, 1);
    svm_range_drain_retry_fault(&p->svms);

    // 4. 遍历并清理所有范围
    list_for_each_entry_safe(prange, next, &p->svms.list, list) {
    // 从列表和区间树移除
    svm_range_unlink(prange);

    // 移除MMU notifier
    svm_range_remove_notifier(prange);

    // 释放范围(包括DMA映射、VRAM等)
    svm_range_free(prange, true);
    }

    // 5. 销毁互斥锁
    mutex_destroy(&p->svms.lock);

    pr_debug("process pid %d svms 0x%p done\\n",
    p->lead_thread->pid, &p->svms);
    }

    清理步骤详解:

    步骤1-2:停止异步工作

    // 取消延迟恢复工作
    cancel_delayed_work_sync(&p->svms.restore_work);
    // 如果工作正在运行,等待完成

    // 刷新延迟列表工作
    flush_work(&p->svms.deferred_list_work);
    // 确保所有pending的工作都执行完

    为什么重要?

    • 避免在清理时还有异步操作在执行
    • 防止use-after-free
    步骤3:排空缺页异常

    atomic_set(&p->svms.drain_pagefaults, 1);
    svm_range_drain_retry_fault(&p->svms);

    作用:

    设置drain_pagefaults标志 → GPU缺页处理函数检查标志

    如果为1,直接返回,不处理

    等待所有进行中的缺页处理完成

    实现:

    void svm_range_drain_retry_fault(struct svm_range_list *svms)
    {
    struct task_struct *task;

    // 等待所有缺页处理任务完成
    while ((task = svms->faulting_task)) {
    // 等待任务退出缺页处理
    wait_event_interruptible(svms->wait_drain,
    svms->faulting_task != task);
    }
    }

    步骤4:清理所有范围

    list_for_each_entry_safe(prange, next, &p->svms.list, list) {
    // a. 从数据结构移除
    svm_range_unlink(prange);

    // b. 移除MMU notifier
    svm_range_remove_notifier(prange);

    // c. 释放资源
    svm_range_free(prange, true);
    }

    svm_range_free 做什么?

    void svm_range_free(struct svm_range *prange, bool do_unmap)
    {
    // 1. 取消GPU映射
    if (do_unmap && prange->mapped_to_gpu) {
    svm_range_unmap_from_gpu(...);
    }

    // 2. 释放DMA映射
    for (i = 0; i < MAX_GPU_INSTANCE; i++) {
    if (prange->dma_addr[i]) {
    svm_range_dma_unmap(prange, i);
    kvfree(prange->dma_addr[i]);
    }
    }

    // 3. 释放VRAM资源
    if (prange->svm_bo) {
    svm_range_vram_node_free(prange);
    }

    // 4. 释放范围结构本身
    kfree(prange);
    }

    清理过程可视化

    进程销毁前:
    p->svms
    ├─ range1 [0x1000-0x2000]
    │ ├─ DMA映射 GPU0: [addr0, addr1, …]
    │ ├─ GPU页表映射 ✓
    │ └─ MMU notifier ✓
    ├─ range2 [0x3000-0x5000]
    │ ├─ svm_bo (VRAM)
    │ ├─ GPU页表映射 ✓
    │ └─ MMU notifier ✓
    └─ range3 [0x6000-0x7000]

    清理步骤:
    1. drain_pagefaults = 1 → 停止新缺页
    2. 清理range1:
    – 移除MMU notifier
    – 取消GPU映射
    – 释放DMA映射
    – 释放结构
    3. 清理range2:
    – 移除MMU notifier
    – 取消GPU映射
    – 释放VRAM (svm_bo)
    – 释放结构
    4. 清理range3: …

    清理后:
    p->svms
    ├─ objects: 空树
    └─ list: 空链表


    5.4 多GPU场景下的管理

    进程的多GPU视图

    kfd_process
    ├─ pdds[0] → GPU 0
    │ ├─ 队列
    │ ├─ aperture
    │ └─ VM管理
    ├─ pdds[1] → GPU 1
    │ ├─ 队列
    │ ├─ aperture
    │ └─ VM管理
    ├─ pdds[2] → GPU 2
    └─ …

    svms (共享)
    ├─ range1: GPU 0和1可访问
    ├─ range2: 仅GPU 0可访问
    └─ range3: 所有GPU可访问

    多GPU的SVM策略

    1. 访问位图管理

    struct svm_range *prange = ...;

    // 设置GPU 0和2可以访问
    set_bit(0, prange->bitmap_access);
    set_bit(2, prange->bitmap_access);

    // GPU 1尝试访问 → 缺页
    if (!test_bit(1, prange->bitmap_access)) {
    // 决定策略:
    // 选项1: 迁移到GPU 1的VRAM
    // 选项2: 保持在原位,建立P2P映射
    // 选项3: 保持在系统内存,GPU 1通过PCIe访问
    }

    2. AIP (Access In Place) 支持

    AIP允许GPU无需迁移即可访问其他位置的内存:

    range在GPU 0的VRAM:
    prange->actual_loc = 0;
    prange->svm_bo->node = GPU 0;

    GPU 1想访问:
    选项1: 迁移到GPU 1 (慢,但后续快)
    选项2: AIP – 通过XGMI直接访问GPU 0的VRAM (无需迁移)

    if (supports_xgmi(GPU0, GPU1)) {
    // 使用AIP
    set_bit(1, prange->bitmap_aip);
    // 建立P2P映射,无需迁移
    }

    XGMI互连示例:

    GPU 0 ←─XGMI─→ GPU 1
    │ │
    VRAM VRAM
    │ │
    [range数据] (可直接访问)

    AIP映射

    3. 多GPU缺页处理

    // GPU 1访问range(actual_loc = 0,在GPU 0)
    int svm_range_restore_pages(...)
    {
    // 1. 检查访问位图
    if (!test_bit(gpu_id, prange->bitmap_access)) {
    // 2. 决定策略
    if (can_use_aip(prange, gpu_id)) {
    // 使用AIP,无需迁移
    set_bit(gpu_id, prange->bitmap_aip);
    // 建立P2P映射
    } else if (should_migrate(prange, gpu_id)) {
    // 迁移到目标GPU
    svm_migrate_to_gpu(prange, gpu_id);
    prange->actual_loc = gpu_id;
    } else {
    // 保持在当前位置,通过PCIe访问
    }
    }

    // 3. 建立GPU页表映射
    svm_range_map_to_gpu(prange, gpu_id, ...);
    set_bit(gpu_id, prange->bitmap_access);
    }

    迁移策略示例

    // 自适应迁移策略
    bool should_migrate_to_gpu(struct svm_range *prange, uint32_t gpu_id)
    {
    // 1. 已经在目标GPU?
    if (prange->actual_loc == gpu_id)
    return false;

    // 2. 检查访问模式
    if (prange->prefetch_loc == gpu_id)
    return true; // 用户预取提示

    // 3. 检查访问频率
    if (access_frequency[gpu_id] > threshold)
    return true;

    // 4. 检查AIP是否可用
    if (test_bit(gpu_id, prange->bitmap_aip))
    return false; // 已有AIP,无需迁移

    // 5. 默认:根据粒度和大小决定
    if (prange->npages < (1 << prange->granularity))
    return true; // 小范围,迁移成本低

    return false;
    }


    💡 重点提示

  • 进程级管理:SVM是以进程为单位管理的,每个进程有独立的svms。

  • 生命周期管理:SVM初始化在进程创建时,清理在进程销毁时,确保资源不泄漏。

  • 异步操作排空:进程销毁前必须确保所有异步操作完成,包括工作队列和缺页处理。

  • 多GPU共享:一个进程的SVM范围可以被多个GPU共享,通过位图管理访问权限。

  • XNACK是关键:没有XNACK,GPU缺页会导致挂起,SVM无法工作。


  • ⚠️ 常见误区

    ❌ 误区1:“每个GPU有独立的SVM管理”

    • ✅ 正确理解:一个进程只有一个svms,被所有GPU共享。

    ❌ 误区2:“进程退出时内核会自动清理SVM”

    • ✅ 正确理解:需要显式调用svm_range_list_fini清理资源。

    ❌ 误区3:“bitmap_supported一直不变”

    • ✅ 正确理解:GPU热插拔或runtime变化时可能改变。

    ❌ 误区4:“清理顺序无关紧要”

    • ✅ 正确理解:必须先排空异步操作,再清理数据结构,顺序错误会崩溃。

    📝 实践练习

  • 代码追踪:

    # 查找进程创建
    grep -n "create_process" drivers/gpu/drm/amd/amdkfd/kfd_process.c

    # 查找SVM初始化
    grep -A 20 "svm_range_list_init" drivers/gpu/drm/amd/amdkfd/kfd_svm.c

    # 查找SVM清理
    grep -A 30 "svm_range_list_fini" drivers/gpu/drm/amd/amdkfd/kfd_svm.c

  • 思考题:

    • 为什么要在清理前设置drain_pagefaults标志?
    • 如果不调用cancel_delayed_work_sync会发生什么?
    • 多个GPU访问同一个range时,如何避免重复迁移?
  • 调试练习:

    # 查看进程的SVM信息(需要debugfs)
    cat /sys/kernel/debug/dri/0/kfd/proc/<pid>/svm

    # 查看GPU支持情况
    dmesg | grep "SVM.*supported"

  • 画图练习: 画出进程创建和销毁时SVM状态的变化时序图。


  • 📚 本章小结

    • kfd_process: 每个使用GPU计算的进程都有一个,包含svms字段
    • 初始化: svm_range_list_init在进程创建时调用,初始化数据结构和工作队列
    • 清理: svm_range_list_fini在进程销毁时调用,排空异步操作并释放所有资源
    • 多GPU: 一个进程的SVM范围可被多GPU共享,通过位图管理访问权限
    • XNACK: GPU页面重试机制,是SVM正常工作的必要条件

    理解进程与SVM的关系是掌握SVM生命周期管理的关键。


    ➡️ 下一步

    完成了数据结构篇,我们已经理解了SVM的"骨架"。接下来进入核心功能篇,我们将学习SVM的核心操作:范围管理、页面迁移、GPU映射和缺页处理。


    🔗 导航

    • 上一章:04 – SVM核心数据结构详解
    • 下一章: 审核中…
    • 返回目录: AMD ROCm-SVM技术的实现与应用深度分析目录
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 05 - 进程与SVM的关系
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!