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 结构体

c 复制代码
// 文件: 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

示例

c 复制代码
// 获取进程的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 详解

c 复制代码
// 文件: 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支持检查

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

检查条件

  1. 设备内存已注册pgmap.type != 0(dGPU有VRAM)
  2. 或者APU偏好GTTapu_prefer_gtt(APU使用系统内存)

示例

c 复制代码
// 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支持检查

c 复制代码
// 在create_process()中
process->xnack_enabled = kfd_process_xnack_mode(process, false);

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

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

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

检查方式

c 复制代码
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 详解

c 复制代码
// 文件: 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:停止异步工作
c 复制代码
// 取消延迟恢复工作
cancel_delayed_work_sync(&p->svms.restore_work);
// 如果工作正在运行,等待完成

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

为什么重要?

  • 避免在清理时还有异步操作在执行
  • 防止use-after-free
步骤3:排空缺页异常
c 复制代码
atomic_set(&p->svms.drain_pagefaults, 1);
svm_range_drain_retry_fault(&p->svms);

作用

复制代码
设置drain_pagefaults标志 → GPU缺页处理函数检查标志
                          ↓
                     如果为1,直接返回,不处理
                          ↓
                     等待所有进行中的缺页处理完成

实现

c 复制代码
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:清理所有范围
c 复制代码
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 做什么?

c 复制代码
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. 访问位图管理
c 复制代码
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缺页处理
c 复制代码
// 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);
}

迁移策略示例

c 复制代码
// 自适应迁移策略
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;
}

💡 重点提示

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

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

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

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

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


⚠️ 常见误区

误区1:"每个GPU有独立的SVM管理"

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

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

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

误区3:"bitmap_supported一直不变"

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

误区4:"清理顺序无关紧要"

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

📝 实践练习

  1. 代码追踪

    bash 复制代码
    # 查找进程创建
    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
  2. 思考题

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

    bash 复制代码
    # 查看进程的SVM信息(需要debugfs)
    cat /sys/kernel/debug/dri/0/kfd/proc/<pid>/svm
    
    # 查看GPU支持情况
    dmesg | grep "SVM.*supported"
  4. 画图练习

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


📚 本章小结

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

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


➡️ 下一步

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


🔗 导航

相关推荐
DeeplyMind1 天前
07 - SVM内存迁移机制
svm·amdgpu·rocm·kfd·rocr
DeeplyMind2 天前
06 - SVM范围管理
svm·amdgpu·rocm·kfd
啊阿狸不会拉杆2 天前
《机器学习导论》第 13 章-核机器
人工智能·python·算法·机器学习·支持向量机·svm·核机器
DeeplyMind4 天前
03 - AMDGPU驱动架构概览
svm·amdgpu·rocm·kfd
DeeplyMind6 天前
ROCm rocr-libhsakmt分析系列4: HsaMemFlags分析
rocm·rocr·libhsakmt·hsamemflags
DeeplyMind7 天前
附录A:AMDGPU SVM 属性类型
kfd·amdgpu svm
DeeplyMind7 天前
04 - SVM核心数据结构详解
svm·amdgpu·kfd
DeeplyMind9 天前
02 - SVM相关的Linux内核基础
hmm·rocm·kfd·共享虚拟内存·amdgpu svm
DeeplyMind10 天前
01 - 什么是SVM
svm·amdgpu·rocm·kfd