04 - SVM核心数据结构详解

难度 : 🟡 进阶
预计学习时间 : 1.5-2小时
前置知识: C语言结构体、链表、红黑树基础


📋 概述

理解数据结构是掌握SVM实现的关键。AMDGPU SVM的核心数据结构复杂,承载了虚拟内存范围管理、页面状态跟踪、GPU映射信息等关键功能。本章将深入剖析四个核心数据结构,理解它们的设计意图和使用方式。

想象一下:每个SVM内存范围就像图书馆的一本书,我们需要:

  • 📚 记录它的位置(起始地址、大小)
  • 🏷️ 标记它的属性(权限、位置偏好)
  • 🔗 维护它的关系(与其他范围的关系)
  • 📊 跟踪它的状态(是否有效、是否已映射)

4.1 svm_range - 内存范围表示

结构体定义

变量有点多,一个方法是先理解range这个概念所代表的地址范围相关属性;然后在理解因share所带来的属性;最后理解因管理的需求所需要的属性。

c 复制代码
// 文件: kfd_svm.h
struct svm_range {
    // === 组织管理 ===
    struct svm_range_list  *svms;          // 所属的范围列表
    struct interval_tree_node it_node;        // 区间树节点
    struct list_head  list;   // 链表节点
    struct list_head  update_list;  // 更新队列链表
    struct list_head  deferred_list; // 延迟处理链表
    struct list_head  child_list; // 子范围列表
    
    // === 地址范围 ===
    unsigned long  start;  // 起始地址(页号)
    unsigned long  last;   // 结束地址(页号)
    uint64_t npages;       // 页面数量
    
    // === 同步保护 ===
    struct mutex  migrate_mutex;  // 迁移互斥锁
    struct mutex lock;  // 范围锁
    unsigned int  saved_flags; // 保存的内存分配标志
    
    // === 内存位置 ===
    uint32_t  preferred_loc;  // 偏好位置 (0=CPU, GPU ID)
    uint32_t  prefetch_loc;   // 预取位置
    uint32_t  actual_loc;     // 实际位置
    uint64_t  vram_pages;     // VRAM页面数
    
    // === 物理映射 ===
    dma_addr_t *dma_addr[MAX_GPU_INSTANCE];  // DMA地址数组
    struct ttm_resource   *ttm_res;       // TTM资源(VRAM)
    uint64_t offset;   // VRAM内偏移
    struct svm_range_bo *svm_bo; // 关联的BO
    struct list_head svm_bo_list;    // BO的范围列表节点
    
    // === 访问控制 ===
    uint32_t  flags;   // 标志位
    uint8_t granularity; // 迁移粒度(log2页数)
    DECLARE_BITMAP(bitmap_access, MAX_GPU_INSTANCE);  // 访问位图
    DECLARE_BITMAP(bitmap_aip, MAX_GPU_INSTANCE);     // AIP位图
    bool mapped_to_gpu;  // 是否已映射到GPU
    
    // === 状态跟踪 ===
    atomic_t invalid;   // 无效标志
    ktime_t  validate_timestamp;  // 验证时间戳
    atomic_t queue_refcount; // 队列引用计数
    
    // === MMU通知 ===
    struct mmu_interval_notifier notifier;      // MMU通知器
    
    // === 工作队列 ===
    struct svm_work_list_item   work_item;      // 工作项
};

关键字段详解

1. 地址范围字段
c 复制代码
unsigned long start;    // 起始地址(页号)
unsigned long last;     // 结束地址(页号)
uint64_t npages;        // 页面数量

重要startlast页号,不是字节地址!

复制代码
示例:
start = 0x7f8a2c001   (虚拟地址 0x7f8a2c001000 的页号)
last  = 0x7f8a2c100   (虚拟地址 0x7f8a2c100000 的页号)
npages = last - start + 1 = 0x100 = 256 页

覆盖地址范围:
  0x7f8a2c001000 - 0x7f8a2c100FFF (1MB)

为什么用页号

  • 节省存储空间(页号比字节地址小12位)
  • 页面对齐是自然的
  • 与MMU的工作单位一致
2. 位置标记字段
c 复制代码
uint32_t preferred_loc;   // 偏好位置
uint32_t prefetch_loc;    // 预取位置
uint32_t actual_loc;      // 实际位置

位置编码

  • 0: CPU(系统内存)
  • 1, 2, 3, ...: GPU ID
c 复制代码
// 示例:范围偏好在GPU 0
prange->preferred_loc = 0;  // GPU 0
prange->actual_loc = 0;     // 当前在系统内存

// GPU访问后可能迁移
prange->actual_loc = 0;     // 现在在GPU 0的VRAM

三个位置的区别

  • preferred_loc: 用户指定的偏好(通过IOCTL设置)
  • prefetch_loc: 上次预取到的位置
  • actual_loc: 当前实际所在位置
3. 物理映射字段
c 复制代码
dma_addr_t *dma_addr[MAX_GPU_INSTANCE];  // 每个GPU的DMA地址数组
struct ttm_resource *ttm_res;            // VRAM的TTM资源
uint64_t offset;                          // VRAM内的偏移

DMA地址数组

复制代码
prange->dma_addr[0] → [页0的DMA地址, 页1的DMA地址, ..., 页N的DMA地址]
prange->dma_addr[1] → [页0的DMA地址, 页1的DMA地址, ..., 页N的DMA地址]
...

每个GPU都需要独立的DMA映射,因为:
- 不同GPU有不同的MMU
- DMA地址可能不同

示例:256页的范围

c 复制代码
// 系统内存情况
prange->npages = 256;
prange->dma_addr[0] = kvcalloc(256, sizeof(dma_addr_t), ...);
prange->dma_addr[0][0] = 0x8000_1000;  // 页0的DMA地址
prange->dma_addr[0][1] = 0x8000_2000;  // 页1的DMA地址
// ...

// VRAM情况
prange->ttm_res = (TTM资源描述VRAM块);
prange->offset = 0x1000;  // 在VRAM块内的偏移
4. 访问控制字段
c 复制代码
DECLARE_BITMAP(bitmap_access, MAX_GPU_INSTANCE);  // 哪些GPU可访问
DECLARE_BITMAP(bitmap_aip, MAX_GPU_INSTANCE);     // 哪些GPU可就地访问

位图使用

c 复制代码
// GPU 0和GPU 2可以访问这个范围
set_bit(0, prange->bitmap_access);
set_bit(2, prange->bitmap_access);

// 检查GPU 1是否可以访问
if (test_bit(1, prange->bitmap_access)) {
    // 可以访问
}

// AIP: Access In Place(无需迁移即可访问)
// 通常通过PCIe或XGMI直接访问其他GPU的VRAM
set_bit(1, prange->bitmap_aip);  // GPU 1可以直接访问

可视化

复制代码
8个GPU系统:
bitmap_access: [1][0][1][1][0][0][0][0]
               GPU0可访问  GPU2和3可访问
               
bitmap_aip:    [1][1][0][0][0][0][0][0]
               GPU0和1可以就地访问(无需迁移)
5. 同步字段
c 复制代码
struct mutex migrate_mutex;  // 迁移互斥锁
struct mutex lock;           // 范围锁

锁的层次

c 复制代码
// migrate_mutex: 保护迁移操作(粗粒度)
mutex_lock(&prange->migrate_mutex);
    // 执行页面迁移操作
    svm_migrate_ram_to_vram(...);
mutex_unlock(&prange->migrate_mutex);

// lock: 保护范围的元数据(细粒度)
svm_range_lock(prange);  // 也会保存内存分配标志
    prange->start = new_start;
    prange->last = new_last;
svm_range_unlock(prange);

为什么需要两个锁

  • migrate_mutex: 迁移操作耗时长,需要独立的锁
  • lock: 快速访问/修改元数据,不能被迁移阻塞

4.2 svm_range_list - 范围管理

结构体定义

c 复制代码
// 文件: kfd_priv.h
struct svm_range_list {
    // === 组织结构 ===
    struct mutex lock;   // 保护整个列表
    struct rb_root_cached  objects; // 区间树根(红黑树)
    struct list_head  list; // 链表头
    
    // === 延迟工作 ===
    struct work_struct deferred_list_work; // 延迟工作
    struct list_head deferred_range_list;// 延迟范围列表
    spinlock_t deferred_list_lock; // 延迟列表锁
    
    // === 状态跟踪 ===
    atomic_t evicted_ranges; // 被驱逐的范围数
    atomic_t drain_pagefaults;   // 排空缺页标志
    struct delayed_work restore_work; // 恢复工作
    
    // === GPU支持 ===
    DECLARE_BITMAP(bitmap_supported, MAX_GPU_INSTANCE);  // 支持SVM的GPU
    
    // === 缺页处理 ===
    struct task_struct  *faulting_task; // 缺页任务
    uint64_t checkpoint_ts[MAX_GPU_INSTANCE];  // 检查点时间戳
    
    // === 配置 ===
    uint8_t default_granularity;// 默认迁移粒度
    
    // === CRIU支持 ===
    struct list_heea criu_svm_metadata_list;  // CRIU元数据
};

数据组织方式

svm_range_list 同时使用了两种数据结构:

1. 区间树(Interval Tree)

基于红黑树实现,用于快速查找地址范围:

复制代码
查询:给定地址0x7f8a2c005000,找到包含它的范围

            [0x1000-0x2000]
           /                \
    [0x500-0x800]      [0x5000-0x8000]  ← 找到了!
     /                        \
[0x100-0x200]           [0x9000-0xA000]

时间复杂度:O(log N)

使用示例

c 复制代码
// 查找包含地址的范围
struct svm_range *prange;
unsigned long addr = 0x7f8a2c005;  // 页号

prange = svm_range_from_addr(&p->svms, addr, NULL);
if (prange) {
    pr_debug("Found range [0x%lx-0x%lx]\n", 
             prange->start, prange->last);
}
2. 链表(Linked List)

用于顺序遍历所有范围:

复制代码
svms->list → [range1] → [range2] → [range3] → NULL

优势:
- 顺序遍历所有范围
- 插入/删除 O(1)
- 适合"对所有范围执行操作"的场景

使用示例

c 复制代码
// 遍历所有SVM范围
struct svm_range *prange;
list_for_each_entry(prange, &p->svms.list, list) {
    pr_debug("Range [0x%lx-0x%lx]\n", 
             prange->start, prange->last);
}

为什么需要两种结构

复制代码
区间树:擅长查找
  - "这个地址属于哪个范围?"  → O(log N)
  
链表:擅长遍历
  - "遍历所有范围并驱逐"     → O(N),但常数小
  - "插入/删除"            → O(1)
  
组合使用:各取所长!

工作队列机制

c 复制代码
struct work_struct deferred_list_work;      // 延迟工作
struct delayed_work restore_work;           // 恢复工作

延迟工作的用途

c 复制代码
// GPU缺页时,不在中断上下文处理,而是延迟
svm_range_add_list_work(
  &p->svms, prange, mm, SVM_OP_ADD_RANGE_AND_MAP);

// 稍后在工作队列上下文执行
// → svm_range_deferred_list_work()
//   → 处理实际的迁移和映射

恢复工作

c 复制代码
// VRAM被驱逐后,延迟恢复
schedule_delayed_work(&svms->restore_work, 
                      msecs_to_jiffies(AMDGPU_SVM_RANGE_RESTORE_DELAY_MS));

// 稍后执行恢复
// → svm_range_restore_work()

4.3 svm_range_bo - Buffer Object管理

结构体定义

c 复制代码
// 文件: kfd_svm.h
struct svm_range_bo {
    struct amdgpu_bo  *bo;  // TTM Buffer Object
    struct kref  kref;  // 引用计数
    struct list_head  range_list;   // 共享此BO的范围列表
    spinlock_t  list_lock;  // 列表锁
    
    struct amdgpu_amdkfd_fence *eviction_fence;// 驱逐fence
    struct work_struct eviction_work;  // 驱逐工作
    uint32_t  evicting; // 驱逐中标志
    
    struct work_struct release_work;   // 释放工作
    struct kfd_node  *node;  // 所属的KFD节点
};

为什么需要svm_range_bo

SVM范围可能很大,不适合分配一整块VRAM。解决方案:

复制代码
大范围分割成多个小块:

原始SVM范围: [0x1000 - 0x10000]  (60MB)
                ↓ 分割
    BO1[4MB] BO2[4MB] BO3[4MB] ... BO15[4MB]
       ↓        ↓        ↓
    Range1   Range2   Range3   ...

多个svm_range可以共享一个svm_range_bo

引用计数管理

c 复制代码
struct kref kref;  // 引用计数

使用模式

c 复制代码
// 增加引用
struct svm_range_bo *svm_bo = svm_range_bo_ref(prange->svm_bo);

// 减少引用(可能释放)
svm_range_bo_unref(svm_bo);
// 如果引用计数为0 → 释放BO

为什么需要引用计数

复制代码
BO可以被多个范围共享:

svm_bo ← range1
      ← range2
      ← range3

只有当所有范围都释放后,BO才能被释放

驱逐机制

c 复制代码
struct amdgpu_amdkfd_fence *eviction_fence;  // 驱逐fence
struct work_struct eviction_work;            // 驱逐工作
uint32_t evicting;                           // 驱逐状态

驱逐流程

复制代码
1. VRAM不足,TTM需要驱逐
   ↓
2. 触发eviction_fence信号
   ↓
3. 调度eviction_work
   ↓ svm_range_evict_svm_bo_worker()
4. 处理驱逐:
   - 取消GPU映射
   - 标记范围为无效
   - 释放VRAM
   ↓
5. 标记evicting状态

代码示例

c 复制代码
// 文件: kfd_svm.c
static void svm_range_evict_svm_bo_worker(struct work_struct *work)
{
    struct svm_range_bo *svm_bo;
    
    svm_bo = container_of(work, struct svm_range_bo, eviction_work);
    
    // 驱逐所有使用此BO的范围
    list_for_each_entry(prange, &svm_bo->range_list, svm_bo_list) {
        // 取消映射
        svm_range_unmap_from_gpu(...);
        // 标记无效
        atomic_set(&prange->invalid, 1);
    }
    
    // 释放VRAM BO
    amdgpu_bo_unref(&svm_bo->bo);
}

4.4 svm_work_list_item - 异步工作项

结构体定义

c 复制代码
// 文件: kfd_svm.h
enum svm_work_list_ops {
    SVM_OP_NULL,
    SVM_OP_UNMAP_RANGE,
    SVM_OP_UPDATE_RANGE_NOTIFIER,
    SVM_OP_UPDATE_RANGE_NOTIFIER_AND_MAP,
    SVM_OP_ADD_RANGE,
    SVM_OP_ADD_RANGE_AND_MAP
};

struct svm_work_list_item {
    enum svm_work_list_ops  op; // 操作类型
    struct mm_struct  *mm; // 进程内存描述符
};

使用场景

某些SVM操作不能在当前上下文执行(如中断上下文),需要延迟:

复制代码
GPU Page Fault (中断上下文)
    ↓ 不能直接处理(不能睡眠)
创建work_item
    ↓
添加到deferred_list
    ↓ schedule_work()
工作队列上下文
    ↓ 可以睡眠,可以分配内存
执行实际操作

操作类型详解

c 复制代码
// SVM_OP_ADD_RANGE_AND_MAP: 添加范围并映射到GPU
work_item.op = SVM_OP_ADD_RANGE_AND_MAP;
work_item.mm = current->mm;

// 稍后在工作队列执行
// → 分配物理页面
// → 建立GPU页表映射

常见操作

  1. SVM_OP_UNMAP_RANGE: 取消GPU映射

    • CPU修改了页面 → 需要使GPU映射失效
  2. SVM_OP_UPDATE_RANGE_NOTIFIER: 更新MMU notifier

    • 范围大小改变 → 需要重新注册notifier
  3. SVM_OP_ADD_RANGE_AND_MAP: 添加范围并映射

    • GPU缺页 → 需要迁移页面并建立映射

4.5 数据结构关系图

让我们把这些结构串联起来:

复制代码
┌─────────────────────────────────────────────────────┐
│              kfd_process (进程)                      │
│  ┌───────────────────────────────────────────────┐  │
│  │    svm_range_list (svms)                      │  │
│  │  ┌────────────────────────────────────────┐   │  │
│  │  │  objects (区间树)                       │   │  │
│  │  │     ┌─────────────────────────────┐    │   │  │
│  │  │     │  svm_range (范围1)           │    │   │  │
│  │  │     │  • start=0x1000             │    │   │  │
│  │  │     │  • last=0x2000              │    │   │  │
│  │  │     │  • actual_loc=0 (CPU)       │    │   │  │
│  │  │     │  • dma_addr[0] → [地址数组]  │    │   │  │
│  │  │     │  • svm_bo → ────────────────│─┐  │   │  │
│  │  │     └───────────────┼─────────────┘ │  │   │  │
│  │  │     ┌───────────────┼─────────────┐ │  │   │  │
│  │  │     │  svm_range (范围2)           │ │  │   │  │
│  │  │     │  • start=0x3000             │ │  │   │  │
│  │  │     │  • actual_loc=1             │ │  │   │  │
│  │  │     │  • ttm_res → VRAM           │ │  │   │  │
│  │  │     │  • svm_bo → ─────────────┐  │ │  │   │  │
│  │  │     └──────────────────────────│──┘ │  │   │  │ 
│  │  └────────────────────────────────┼────┼──┘   │  │
│  │                                   │    │      │  │
│  │  list (链表): range1 → range2 →    │    │      │  │
│  └───────────────────────────────────┼────┼──────┘  │
└──────────────────────────────────────┼────┼─────────┘
                                       ↓    ↓
                        ┌──────────────────────────────┐
                        │  svm_range_bo (BO1)          │
                        │  • bo → amdgpu_bo (VRAM 4MB) │
                        │  • kref = 2 (两个范围共享)     │
                        │  • range_list: [range1,      │
                        │                 range2]      │
                        │  • eviction_fence            │
                        └──────────────────────────────┘

典型场景演示

场景1:CPU分配内存

c 复制代码
// 用户调用malloc
void *ptr = malloc(8MB);

// 内核创建svm_range
struct svm_range *prange = kzalloc(...);
prange->start = 0x7f8a2c000;     // 起始页号
prange->last = 0x7f8a2d000;      // 结束页号
prange->npages = 0x1000;         // 4096页 = 16MB
prange->actual_loc = 0;          // 在CPU(系统内存)
prange->preferred_loc = 0;       // 偏好CPU
prange->svms = &process->svms;   // 属于进程的svms

// 添加到svms
svm_range_add_to_svms(prange);   // 加入区间树和链表

场景2:GPU首次访问

c 复制代码
// GPU访问ptr → Page Fault
// → svm_range_restore_pages()

// 1. 查找范围
prange = svm_range_from_addr(&p->svms, fault_addr, NULL);

// 2. 决定是否迁移(假设迁移到GPU 0)
if (should_migrate_to_vram(prange)) {
    // 3. 分配VRAM
    svm_range_vram_node_new(node, prange, false);
    // 创建svm_range_bo和amdgpu_bo
    
    // 4. 迁移页面
    svm_migrate_ram_to_vram(...);
    
    // 5. 更新状态
    prange->actual_loc = 1;  // GPU 0
    prange->vram_pages = prange->npages;
}

// 6. 建立GPU页表映射
svm_range_map_to_gpu(prange, ...);
set_bit(0, prange->bitmap_access);  // GPU 0可访问
prange->mapped_to_gpu = true;

场景3:CPU修改页面

c 复制代码
// CPU写入ptr[0] = 42;
// → MMU Notifier触发

// → svm_range_cpu_invalidate_pagetables()
atomic_set(&prange->invalid, 1);  // 标记无效

// 创建工作项
prange->work_item.op = SVM_OP_UNMAP_RANGE;
svm_range_add_list_work(&p->svms, prange, mm, SVM_OP_UNMAP_RANGE);

// 稍后在工作队列:
// → svm_range_unmap_from_gpu(prange, ...);
clear_bit(0, prange->bitmap_access);  // GPU 0不再可访问

💡 重点提示

  1. 页号vs字节地址startlast是页号,需要左移12位才是字节地址。

  2. 两种组织方式:区间树用于查找,链表用于遍历,各有优势。

  3. 引用计数svm_range_bo使用kref管理生命周期,可被多个范围共享。

  4. 异步处理:很多操作通过工作队列异步执行,避免阻塞。

  5. 位图的威力bitmap_accessbitmap_aip用一个字就能表示8个GPU的状态。


⚠️ 常见误区

误区1:"一个svm_range对应一个物理块"

  • ✅ 正确理解:一个范围可能跨越系统内存和VRAM,或者多个物理块。

误区2:"actual_loc改变时物理页面立即移动"

  • ✅ 正确理解:actual_loc是状态记录,实际迁移是独立的操作。

误区3:"bitmap_access和mapped_to_gpu是重复的"

  • ✅ 正确理解:bitmap记录哪些GPU可访问,mapped_to_gpu记录是否已建立映射。

误区4:"所有字段都需要svms->lock保护"

  • ✅ 正确理解:不同字段有不同的保护机制(svms->lock、prange->lock、atomic等)。

📝 实践练习

  1. 结构体计算

    计算struct svm_range的大小:

    bash 复制代码
    pahole drivers/gpu/drm/amd/amdkfd/kfd_svm.o -C svm_range
  2. 代码追踪

    bash 复制代码
    # 查找创建svm_range的位置
    grep -n "kzalloc.*svm_range" drivers/gpu/drm/amd/amdkfd/kfd_svm.c
    
    # 查找区间树的使用
    grep -n "interval_tree_" drivers/gpu/drm/amd/amdkfd/kfd_svm.c
  3. 思考题

    • 为什么需要migrate_mutexlock两个锁?
    • 区间树的键值是什么?
    • 如果多个GPU访问同一个范围,DMA地址如何管理?
  4. 画图练习

    画出3个SVM范围共享2个BO的关系图。


📚 本章小结

  • svm_range: 核心结构,表示一个虚拟内存范围,包含地址、位置、映射等信息
  • svm_range_list: 管理所有范围,使用区间树+链表双重组织
  • svm_range_bo: 管理VRAM Buffer Object,支持共享和引用计数
  • svm_work_list_item: 异步工作项,支持延迟执行操作

这四个结构是SVM实现的骨架,后续的所有操作都围绕它们展开。


📖 扩展阅读

➡️ 下一步

理解了核心数据结构后,我们将在下一章探讨进程如何管理这些SVM范围,以及进程生命周期中SVM的初始化和清理。


🔗 导航

相关推荐
DeeplyMind2 天前
02 - SVM相关的Linux内核基础
hmm·rocm·kfd·共享虚拟内存·amdgpu svm
DeeplyMind3 天前
01 - 什么是SVM
svm·amdgpu·rocm·kfd
DeeplyMind7 天前
AMD ROCm-SVM技术的实现与应用深度分析目录
svm·rocm·kfd
DeeplyMind9 天前
引文:当SVM转角遇上Copy-on-Write (COW)
svm·copy-on-write·cow·mmu notifier
DeeplyMind24 天前
AMD KFD的BO设计分析系列8-7:TLB管理与刷新
amdgpu·tlb·kfd
core5121 个月前
SVM (支持向量机):寻找最完美的“分界线”
算法·机器学习·支持向量机·svm
芥子沫1 个月前
《人工智能基础》[算法篇5]:SVM算法解析
人工智能·算法·机器学习·支持向量机·svm
DeeplyMind1 个月前
AMD KFD的BO设计分析系列7-2:GPU GART 实现深度解析--绑定机制与性能优化
驱动开发·amdgpu·kfd·gart
俊俊谢2 个月前
【机器学习】python使用支持向量机解决兵王问题(基于libsvm库)
python·机器学习·支持向量机·svm·libsvm