第2篇:GPU 共享虚拟内存抽象层 — DRM GPUSVM

第2篇:GPU 共享虚拟内存抽象层 — DRM GPUSVM

源码:drivers/gpu/drm/drm_gpusvm.c | 头文件:include/drm/drm_gpusvm.h
系列目录:NVIDIA AI Infra 内核源码深度解析

1. GPUSVM 是什么

在上一篇中,我们深入了 HMM(mm/hmm.c),它提供了 hmm_range_fault() 来镜像 CPU 页表。但 HMM 只是一个底层工具——它不知道”GPU”的概念,不知道 GPU 故障、DMA 映射、设备内存迁移。

DRM GPUSVM 是 DRM 框架在 HMM 之上构建的 GPU SVM 抽象层。它由 Intel 的 Matthew Brost 于 2024 年贡献(drivers/gpu/drm/drm_gpusvm.c:1-7),为所有 DRM GPU 驱动提供统一的 SVM 框架,负责:

  • Notifier 管理:将 mmu_interval_notifier 包装为 GPU SVM Notifier,按地址区间跟踪 CPU 页表变更
  • Range 管理:将 GPU 故障地址映射为”Range”(GPU 视角的页表区域),支持动态创建/销毁
  • DMA 映射:将 CPU 页的 PFN 数组转换为 GPU 可以理解的 DMA 地址
  • 设备内存迁移:提供与 dev_pagemap / drm_pagemap 协作的设备内存迁移框架

NVIDIA 内核驱动(nouveau_svm.c)就是 GPUSVM 的一个驱动侧调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────┐
│ 用户空间 (CUDA) │
│ cudaMallocManaged() / cudaMemPrefetchAsync()
├─────────────────────────────────────────────────────────┤
│ DRM 驱动层 (nouveau_svm.c) │
│ GPU 页故障处理 ← drm_gpusvm_range_find_or_insert()
│ GPU 页表编程 ← drm_gpusvm_range_get_pages()
├─────────────────────────────────────────────────────────┤
│ DRM GPUSVM (drm_gpusvm.c) ← 本篇 │
│ Notifier → Range → Pages (DMA) → Evict/Migrate │
├─────────────────────────────────────────────────────────┤
│ HMM (mm/hmm.c) │
│ hmm_range_fault() ← 第1篇 │
├─────────────────────────────────────────────────────────┤
│ 内存管理子系统 (MM) │
struct page, PTEs, migrate, ZONE_DEVICE │
└─────────────────────────────────────────────────────────┘

2. 核心数据结构

2.1 struct drm_gpusvm — 整个 SVM 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// include/drm/drm_gpusvm.h:202-222
struct drm_gpusvm {
const char *name;
struct drm_device *drm;
struct mm_struct *mm; // 对应的 CPU 地址空间
unsigned long mm_start; // SVM 起始地址
unsigned long mm_range; // SVM 范围大小
unsigned long notifier_size; // 每个 notifier 的大小 (建议 >= 512M)
const struct drm_gpusvm_ops *ops; // 驱动实现的 vtable
const unsigned long *chunk_sizes; // Range chunk 大小数组 (降序 2的幂)
int num_chunks;
struct rw_semaphore notifier_lock; // 全局 notifier 读写锁
struct rb_root_cached root; // notifier 红黑树根
struct list_head notifier_list; // notifier 链表 (快速遍历)
};

核心设计:一个 drm_gpusvm 对应一个 GPU 进程(一个 mm_struct)。notifier_size 控制一个 notifier 覆盖的地址范围(建议 ≥ 512MB),chunk_sizes 控制 GPU 故障处理的粒度。

2.2 struct drm_gpusvm_ops — 驱动 vtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// include/drm/drm_gpusvm.h:29-77
struct drm_gpusvm_ops {
// 可选:自定义 notifier 分配
struct drm_gpusvm_notifier *(*notifier_alloc)(void);
void (*notifier_free)(struct drm_gpusvm_notifier *notifier);

// 可选:自定义 range 分配
struct drm_gpusvm_range *(*range_alloc)(struct drm_gpusvm *gpusvm);
void (*range_free)(struct drm_gpusvm_range *range);

// 必须:驱动侧的 GPU 页表失效回调
void (*invalidate)(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_notifier *notifier,
const struct mmu_notifier_range *mmu_range);
};

驱动实现 .invalidate 回调,当 CPU 页表变化时,GPUSVM 框架调用此函数通知驱动更新 GPU 页表。

2.3 struct drm_gpusvm_notifier — 页表变化监听

1
2
3
4
5
6
7
8
9
10
// include/drm/drm_gpusvm.h:96-106
struct drm_gpusvm_notifier {
struct drm_gpusvm *gpusvm;
struct mmu_interval_notifier notifier; // HMM 的 interval notifier
struct interval_tree_node itree; // 插入 gpusvm->root 的节点
struct list_head entry; // 链表节点
struct rb_root_cached root; // 该 notifier 下的 range RB树
struct list_head range_list; // range 链表
struct { u32 removed : 1; } flags;
};

Notifier 是 GPUSVM 的核心监听单元。每个 notifier 覆盖一个地址区间(由 notifier_size 决定),内部维护一棵 Range RB 树。当 CPU 页表在该区间内变化时,mmu_interval_notifier 回调触发驱动 invalidate。

1
2
3
4
5
6
7
8
9
10
11
gpusvm->root (RB树)

├── notifier [0, 512M] ← notifier_size = 512M
│ │
│ ├── range [0, 2M] ← chunk_size = 2M
│ ├── range [2M, 6M]
│ └── range [8M, 10M]

└── notifier [512M, 1G]

└── range [512M, 514M]

2.4 struct drm_gpusvm_range — GPU 页表区域

1
2
3
4
5
6
7
8
9
// include/drm/drm_gpusvm.h:167-174
struct drm_gpusvm_range {
struct drm_gpusvm *gpusvm;
struct drm_gpusvm_notifier *notifier;
struct kref refcount; // 引用计数
struct interval_tree_node itree; // 插入 notifier->root 的节点
struct list_head entry;
struct drm_gpusvm_pages pages; // 该 range 的页映射信息
};

Range 是 GPUSVM 的最小管理单元,代表一段已被 GPU 映射的 CPU 虚拟地址区间。每个 GPU 故障对应一个 Range(按 chunk_size 对齐)。

2.5 struct drm_gpusvm_pages — 页映射

1
2
3
4
5
6
7
// include/drm/drm_gpusvm.h:147-152
struct drm_gpusvm_pages {
struct drm_pagemap_addr *dma_addr; // DMA 地址数组
struct drm_pagemap *dpagemap; // 设备页的 drm_pagemap
unsigned long notifier_seq; // notifier 序列号
struct drm_gpusvm_pages_flags flags;
};

dma_addr[] 是 GPUSVM 的关键创新:它将 HMM 返回的 PFN 数组转换为 GPU 可用的 DMA 地址数组。每个 dma_addr[i] 编码了 DMA 地址、互连类型(system/devmem)、order 和方向。

2.6 struct drm_gpusvm_ctx — 控制上下文

1
2
3
4
5
6
7
8
9
10
11
// include/drm/drm_gpusvm.h:244-253
struct drm_gpusvm_ctx {
void *device_private_page_owner;
unsigned long check_pages_threshold;
unsigned long timeslice_ms;
unsigned int in_notifier :1;
unsigned int read_only :1;
unsigned int devmem_possible :1; // 是否允许迁移到设备内存
unsigned int devmem_only :1; // 是否仅使用设备内存
unsigned int allow_mixed :1; // 是否允许混合 system/devmem 映射
};

驱动通过 ctx 控制 GPUSVM 的行为。例如:

  • devmem_possible=1, devmem_only=0:优先设备内存,但可回退到系统内存
  • devmem_only=1:必须有设备内存,否则失败
  • read_only=1:只读映射(DMA_TO_DEVICE)
  • allow_mixed=1:允许一个 range 内同时有 system 和 device 页

3. 核心流程

3.1 初始化

1
2
3
4
5
6
7
8
// drm_gpusvm.c:383-426
int drm_gpusvm_init(struct drm_gpusvm *gpusvm,
const char *name, struct drm_device *drm,
struct mm_struct *mm,
unsigned long mm_start, unsigned long mm_range,
unsigned long notifier_size,
const struct drm_gpusvm_ops *ops,
const unsigned long *chunk_sizes, int num_chunks)

初始化时:

  1. 调用 mmgrab(mm) 持有 mm_struct 引用(line 394)
  2. 初始化 RB 树根 gpusvm->root = RB_ROOT_CACHED(line 411)
  3. 初始化 notifier 链表(line 412)
  4. 初始化 notifier_lock 读写信号量(line 414)
  5. 注册 lockdep 注释(line 420-422)

支持两种模式:

  • 完整 SVM 模式:提供 mm + ops + chunk_sizes 等全部参数
  • 简单 pages API 模式:只提供 name + drm,仅使用 get/unmap/free 页面操作

3.2 GPU 故障处理 — drm_gpusvm_range_find_or_insert

当 GPU 访问一个不在 TLB 中的虚拟地址时,驱动调用这个函数来查找或创建对应的 Range:

1
2
3
4
5
6
// drm_gpusvm.c:1013-1117
struct drm_gpusvm_range *
drm_gpusvm_range_find_or_insert(struct drm_gpusvm *gpusvm,
unsigned long fault_addr,
unsigned long gpuva_start, unsigned long gpuva_end,
const struct drm_gpusvm_ctx *ctx)

核心流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1. fault_addr 必须落在 [mm_start, mm_start+mm_range]
↓ (line 1031-1033)
2. mmget_not_zero(mm) — 持有 mm 引用
↓ (line 1035)
3. drm_gpusvm_notifier_find() — 查找 fault_addr 所在的 notifier
├─ 找到 → line 1068: 直接查找已有 range
└─ 未找到 → 分配新 notifier, mmu_interval_notifier_insert()
↓ (line 1038-1053)
4. vma_lookup(mm, fault_addr) — 查找 VMA
├─ 未找到 → -ENOENT
└─ 检查 VM_WRITE 标志
↓ (line 1057-1066)
5. drm_gpusvm_range_find(notifier, fault_addr, fault_addr+1)
├─ 找到 → 直接返回已有 range
└─ 未找到 → 创建新 range
↓ (line 1068-1070)
6. 判断是否可迁移到设备内存:
migrate_devmem = ctx->devmem_possible && vma_is_anonymous(vas)
&& !is_vm_hugetlb_page(vas)
↓ (line 1076-1077)
7. drm_gpusvm_range_chunk_size() — 选择 range 大小
├─ 遍历 chunk_sizes[] 从大到小
├─ 找到第一个落在 VMA/notifier/gpuva 边界内的对齐区间
├─ 如果是非 4K 页,检查是否与已有 range 重叠
└─ 可选:drm_gpusvm_check_pages() 检查 CPU 是否已 fault 这些页
↓ (line 1079-1087)
8. drm_gpusvm_range_alloc() — 分配 range 结构,设置 itree.start/end
↓ (line 1089-1094)
9. drm_gpusvm_range_insert() — 插入 notifier->root RB 树
↓ (line 1096)
10. drm_gpusvm_notifier_insert() — 如果是新 notifier,插入 gpusvm->root

Range chunk_size 选择的关键drm_gpusvm_range_chunk_size,line 884-946):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
chunk_sizes = [2M, 64K, 4K]

fault_addr = 0x1005000 (约 16.3M)

尝试 2M: start = ALIGN_DOWN(0x1005000, 2M) = 0x1000000
end = ALIGN(0x1005000+1, 2M) = 0x1200000
→ 在 VMA/notifier/gpuva 边界内吗?
→ 不与已有 range 重叠吗?
✓ → 返回 2M

否则尝试 64K:
start = 0x1000000, end = 0x1010000
✓ → 返回 64K

否则尝试 4K:
start = 0x1005000, end = 0x1006000
✓ → 返回 4K

都不行 → 返回 LONG_MAX(错误)

3.3 获取页映射 — drm_gpusvm_get_pages

一旦有了 Range,驱动调用此函数将 CPU 页转换为 GPU 可用的 DMA 地址:

1
2
3
4
5
6
7
// drm_gpusvm.c:1382-1587
int drm_gpusvm_get_pages(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages,
struct mm_struct *mm,
struct mmu_interval_notifier *notifier,
unsigned long pages_start, unsigned long pages_end,
const struct drm_gpusvm_ctx *ctx)

核心流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. hmm_range_fault() — 调用 HMM 获取 PFN 数组
↓ (line 1430-1444)
2. 持有 notifier_lock,检查 seqno 是否重试
↓ (line 1455-1468)
3. 遍历 hmm_pfns[],逐个建立 DMA 映射:
├─ 设备私有/一致页 → dpagemap->ops->device_map()
│ 检查 allow_mixed, devmem_only 标志
├─ 普通页 → dma_map_page() (DMA_BIDIRECTIONAL 或 DMA_TO_DEVICE)
│ 编码为 drm_pagemap_addr_encode(addr, DRM_INTERCONNECT_SYSTEM, ...)
↓ (line 1485-1558)
4. 设置 pages.flags (has_dma_mapping, has_devmem_pages)
↓ (line 1560-1568)
5. 记录 notifier_seq
↓ (line 1573)

关键设计:

  • DMA 映射在 notifier_lock 保护下进行,notifier 回调可以安全地取消映射
  • allow_mixed=0 时,不允许单个 range 中混合多个 dpagemap(line 1494-1498)
  • devmem_only=1 时,遇到普通页直接返回 -EFAULT(line 1537-1540)
  • 使用 hmm_pfn_to_map_order() 支持大页映射,一次 DMA 映射覆盖多个页(line 1488, 1542-1549)

3.4 notifier 回调 — 页表失效

当 CPU 页表变化时(例如 munmapmprotect 等),mmu_interval_notifier 触发 GPUSVM 的回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// drm_gpusvm.c:331-349
static bool
drm_gpusvm_notifier_invalidate(struct mmu_interval_notifier *mni,
const struct mmu_notifier_range *mmu_range,
unsigned long cur_seq)
{
struct drm_gpusvm_notifier *notifier =
container_of(mni, typeof(*notifier), notifier);
struct drm_gpusvm *gpusvm = notifier->gpusvm;

if (!mmu_notifier_range_blockable(mmu_range))
return false;

down_write(&gpusvm->notifier_lock); // 写锁
mmu_interval_set_seq(mni, cur_seq); // 设置 seqno
gpusvm->ops->invalidate(gpusvm, notifier, mmu_range); // 驱动回调
up_write(&gpusvm->notifier_lock);

return true;
}

这对应 HMM 文档中的 driver->update 锁(drm_gpusvm.c:96-99)。notifier 回调持有写锁,与 get_pages 的读锁互斥——确保 DMA 映射期间页不会被释放。

3.5 Page 有效性检查 — drm_gpusvm_range_pages_valid

在驱动最终提交 GPU 绑定时,需要做最后一次检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// drm_gpusvm.c:1333-1338
bool drm_gpusvm_range_pages_valid(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_range *range)
{
return drm_gpusvm_pages_valid(gpusvm, &range->pages);
}

// drm_gpusvm.c:1311-1317
static bool drm_gpusvm_pages_valid(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages)
{
lockdep_assert_held(&gpusvm->notifier_lock);
return svm_pages->flags.has_devmem_pages || svm_pages->flags.has_dma_mapping;
}

这是 GPUSVM 的”最后一公里”保护——对每个 Range 做细粒度检查(而非整个 notifier 的 seqno 检查),因为 notifier 覆盖多个 range,只靠 notifier seqno 不够精确。

3.6 扫描内存状态 — drm_gpusvm_scan_mm

GPUSVM 提供了一个”咨询性”扫描函数,用于判断 Range 对应的物理页当前处于什么迁移状态:

1
2
3
// drm_gpusvm.c:762-865
enum drm_gpusvm_scan_result drm_gpusvm_scan_mm(struct drm_gpusvm_range *range,
void *dev_private_owner, const struct dev_pagemap *pagemap)

扫描结果枚举(include/drm/drm_gpusvm.h:347-353):

1
2
3
4
5
6
DRM_GPUSVM_SCAN_UNPOPULATED  — 至少有一个页未 present
DRM_GPUSVM_SCAN_EQUAL — 所有页都属于 @pagemap
DRM_GPUSVM_SCAN_OTHER — 所有页都属于另一个 pagemap
DRM_GPUSVM_SCAN_SYSTEM — 所有页都是系统内存
DRM_GPUSVM_SCAN_MIXED_DEVICE — 混合了多个 dev_pagemap
DRM_GPUSVM_SCAN_MIXED — 混合了系统页和设备页

状态转换逻辑(line 816-857):

1
2
3
4
5
6
7
8
9
10
11
遍历所有页:
if 页 == pagemap: new_state = EQUAL
else if 页 == other || !other: new_state = OTHER
else if 页是 device: new_state = MIXED_DEVICE
else (系统页): new_state = SYSTEM

合并到 state:
UNPOPULATED + ANY = ANY
EQUAL + SYSTEM => MIXED
EQUAL + other device => MIXED_DEVICE
SYSTEM + device => MIXED

注意:结果可能随时失效——这只是一个建议性的快照

4. 驱动使用模式

GPUSVM 文档(drm_gpusvm.c:128-253)给出了三个驱动组件示例:

4.1 GPU 页故障处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 伪代码,体现核心循环
int driver_gpu_fault(struct drm_gpusvm *gpusvm, unsigned long fault_addr,
unsigned long gpuva_start, unsigned long gpuva_end)
{
struct drm_gpusvm_ctx ctx = {};
// ...
retry:
driver_garbage_collector(gpusvm); // 先处理 pending UNMAP

range = drm_gpusvm_range_find_or_insert(gpusvm, fault_addr,
gpuva_start, gpuva_end, &ctx);

err = drm_gpusvm_range_get_pages(gpusvm, range, &ctx);
// -EOPNOTSUPP → evict + retry
// -EFAULT → retry
// -EAGAIN → driver_bind_range retry

err = driver_bind_range(gpusvm, range);
// ...
}

核心思想:循环重试直到页表稳定

4.2 垃圾回收器

1
2
3
4
5
6
7
8
9
void __driver_garbage_collector(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_range *range)
{
if (range->flags.partial_unmap) // 部分 unmap
drm_gpusvm_range_evict(gpusvm, range); // 迁回 RAM

driver_unbind_range(range);
drm_gpusvm_range_remove(gpusvm, range);
}

4.3 Notifier 回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void driver_invalidation(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_notifier *notifier,
const struct mmu_notifier_range *mmu_range)
{
struct drm_gpusvm_ctx ctx = { .in_notifier = true, };

drm_gpusvm_for_each_range(range, notifier, mmu_range->start, mmu_range->end) {
drm_gpusvm_range_unmap_pages(gpusvm, range, &ctx); // 取消 DMA 映射

if (mmu_range->event != MMU_NOTIFY_UNMAP)
continue;

drm_gpusvm_range_set_unmapped(range, mmu_range);
driver_garbage_collector_add(gpusvm, range);
}
}

5. Locking 架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──────────────────────────────────────────────┐
│ driver_svm_lock │
│ 保护: range_find_or_insert, range_remove, │
│ garbage_collector │
│ 类型: 驱动自定义锁 (或 lockdep 注解) │
├──────────────────────────────────────────────┤
gpusvm->notifier_lock │
│ 保护: notifier->root (RB树), range->pages │
│ (DMA 映射, seqno) │
│ get_pages → down_read │
│ notifier_invalidate → down_write │
│ pages_valid/unmap → 持锁检查 │
├──────────────────────────────────────────────┤
mm->mmap_lock │
│ hmm_range_fault() → mmap_read_lock(mm) │
└──────────────────────────────────────────────┘

drm_gpusvm.c:88-108 文档详细描述了锁定层次。最关键的是 notifier_lock 的读写锁设计——DMA 映射期间持 读锁,notifier 回调持 写锁,确保驱动永远不会在已释放的页上做 DMA 操作。

6. 总结

DRM GPUSVM 为 GPU 驱动提供了完整的 SVM 基础设施。它:

  • 在 notifier 层面:用 RB 树 + Interval Tree 管理 mmu_interval_notifier
  • 在 range 层面:按 chunk_size 将故障地址映射为 GPU 页表区域
  • 在 pages 层面:将 HMM PFN 转换为 GPU DMA 地址(支持大页/设备页)
  • 在迁移层面:通过 drm_gpusvm_ctx 控制设备内存迁移策略

下一篇文章将看 NVIDIA 驱动(nouveau_svm.c)如何使用这个框架处理真实的 GPU 页故障。

下一篇文章

第3篇:NVIDIA HMM 调用者:nouveau_svm.c


第2篇:GPU 共享虚拟内存抽象层 — DRM GPUSVM
https://realwujing.github.io/linux/drivers/gpu/nvidia-svm/02-drm-gpusvm/
作者
Wu Jing
发布于
2026年5月21日
许可协议