第3篇:NVIDIA HMM 调用者 — nouveau_svm.c
源码:drivers/gpu/drm/nouveau/nouveau_svm.c | 头文件:drivers/gpu/drm/nouveau/nouveau_svm.h
系列目录:NVIDIA AI Infra 内核源码深度解析
1. 本篇位置
前两篇我们深入了 HMM(mm/hmm.c)和 DRM GPUSVM(drm_gpusvm.c)框架层。本篇聚焦 NVIDIA 的 HMM 调用者——nouveau_svm.c,看 NVIDIA 开源驱动如何将 GPU 页故障转化为 HMM 调用,并最终编程 GPU 页表。
1 2 3 4 5 6 7 8 9
| GPU 硬件故障 ↓ nouveau_svm_fault_buffer (ring buffer) ↓ nouveau_svm_fault() work handler ← 本篇核心 ├─ 解析故障 (addr, access, inst) ├─ 查找 SVMM (GPU 实例 → 进程) ├─ hmm_range_fault() / make_device_exclusive() └─ nvif_vmm_pfnmap() → 编程 GPU 页表
|
注意:nouveau 是 NVIDIA GPU 的开源驱动,与闭源的 nvidia.ko 不同。但 SVM 架构设计思想是相通的。
2. 核心数据结构
2.1 struct nouveau_svm — GPU 设备级 SVM
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 32
| struct nouveau_svm { struct nouveau_drm *drm; struct mutex mutex; struct list_head inst;
struct nouveau_svm_fault_buffer { int id; struct nvif_object object; u32 entries; u32 getaddr; u32 putaddr; u32 get; u32 put; struct nvif_event notify; struct work_struct work;
struct nouveau_svm_fault { u64 inst; u64 addr; u64 time; u32 engine; u8 gpc; u8 hub; u8 access; u8 client; u8 fault; struct nouveau_svmm *svmm; } **fault; int fault_nr; } buffer[]; };
|
struct nouveau_svm 是设备级别的 SVM 结构。核心是 buffer[] 柔性数组——GPU 的多条故障 buffer 环。GPU 通过寄存器 getaddr/putaddr 暴露 buffer 的读写指针,CPU 通过 nvif_rd32 读取。
2.2 struct nouveau_svmm — 进程级 SVM
1 2 3 4 5 6 7 8 9 10
| struct nouveau_svmm { struct mmu_notifier notifier; struct nouveau_vmm *vmm; struct { unsigned long start; unsigned long limit; } unmanaged; struct mutex mutex; };
|
每个开启 SVM 的 GPU 进程都有一个 nouveau_svmm。它连接了 CPU 侧(mmu_notifier)和 GPU 侧(nouveau_vmm)。
2.3 故障访问类型
1 2 3 4 5
| #define FAULT_ACCESS_READ 0 #define FAULT_ACCESS_WRITE 1 #define FAULT_ACCESS_ATOMIC 2 #define FAULT_ACCESS_PREFETCH 3
|
四种故障类型由 info 寄存器的 [19:16] 位编码(nouveau_svm.c:493)。
2.4 struct svm_notifier — 局部 interval notifier
1 2 3 4 5
| struct svm_notifier { struct mmu_interval_notifier notifier; struct nouveau_svmm *svmm; };
|
这是 nouveau 自己的 mmu_interval_notifier 包装——用于单页故障处理中的 per-fault interval notifier,与 GPUSVM 的 drm_gpusvm_notifier 是独立的概念。
3. 初始化链路
3.1 设备级初始化 — nouveau_svm_init
1 2
| void nouveau_svm_init(struct nouveau_drm *drm)
|
流程:
- 平台检查(line 1062):仅 Pascal 及以下启用 SVM(Volta+ 暂时禁用,因 channel recovery 未修复)
- **分配
struct nouveau_svm**(line 1065):kzalloc_flex(*drm->svm, buffer, 1) — 一个故障 buffer
- 查找故障 buffer 类(line 1073):
nvif_mclass() 查找 GPU 支持的故障 buffer 类(Volta → Maxwell 降级尝试)
- 构造故障 buffer(line 1080):
nouveau_svm_fault_buffer_ctor()
3.2 故障 Buffer 构造 — nouveau_svm_fault_buffer_ctor
1 2 3
| static int nouveau_svm_fault_buffer_ctor(struct nouveau_svm *svm, s32 oclass, int id)
|
流程:
- 创建 GPU 对象(line 995):
nvif_object_ctor(device, "svmFaultBuffer", 0, oclass, ...) — 在 GPU 上分配故障 buffer
- 映射到 CPU(line 1002):
nvif_object_map() — 将 GPU 寄存器映射到 CPU 可访地址
- 读取 buffer 参数(line 1003-1005):entries, getaddr, putaddr
- 注册 work(line 1006):
INIT_WORK(&buffer->work, nouveau_svm_fault) — 故障处理器
- 注册 GPU 中断(line 1008):
nvif_event_ctor(..., nouveau_svm_event, ...) — GPU 写故障后触发中断 → schedule_work()
- 分配 fault 数组(line 1013):
kvzalloc_objs(*buffer->fault, buffer->entries)
- 初始化(line 1017):同步 get/put 指针,启用中断
3.3 进程级初始化 — nouveau_svmm_init
1 2 3
| int nouveau_svmm_init(struct drm_device *dev, void *data, struct drm_file *file_priv)
|
这是用户空间 ioctl 路径(NOUVEAU_SVM_INIT)。流程:
- **分配
struct nouveau_svmm**(line 329)
- 设置非托管区域(line 332-333):某些地址范围不属于 SVM(由用户空间指定)
- 创建可重放故障的 GPU VMM(line 349-356):
nvif_vmm_ctor(..., "svmVmm", ..., MANAGED, ..., .fault_replay = true)
MANAGED 模式告诉 GPU:缺页不要崩溃,记录到故障 buffer
fault_replay = true:故障处理后 GPU 可以重放(重试)访问
- 注册 CPU MMU Notifier(line 358-361):
__mmu_notifier_register(&svmm->notifier, current->mm) — 监听 CPU 页表变化
4. GPU 故障处理 — 核心路径
4.1 故障中断触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| GPU MMU 缺页 ↓ GPU 写入故障信息到 fault buffer (ring buffer) ├─ inst (GPU 实例 ID) ├─ addr (故障地址) ├─ access (读/写/原子/预取) ├─ hub/gpc/client/engine (故障源) └─ fault (故障类型码) ↓ GPU 更新 PUT 指针 ↓ GPU 产生中断 → nvif_event 回调 ↓ nouveau_svm_event() → schedule_work(&buffer->work) ↓ nouveau_svm_fault() work handler ← 核心处理
|
1 2 3 4 5 6 7 8 9
| static int nouveau_svm_event(struct nvif_event *event, void *argv, u32 argc) { struct nouveau_svm_fault_buffer *buffer = container_of(event, typeof(*buffer), notify); schedule_work(&buffer->work); return NVIF_EVENT_KEEP; }
|
4.2 故障读取 — nouveau_svm_fault_cache
1 2 3 4
| static void nouveau_svm_fault_cache(struct nouveau_svm *svm, struct nouveau_svm_fault_buffer *buffer, u32 offset)
|
从 GPU 故障 buffer 的每个 32 字节条目(偏移 0x00-0x1c)中读取故障详情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 偏移 内容 0x00 inst 低32位 ← nvif_rd32(memory, offset + 0x00) 0x04 inst 高32位 0x08 addr 低32位 0x0c addr 高32位 0x10 time 低32位 0x14 time 高32位 0x18 engine 0x1c info: [31] valid (0x80000000) [27:24] gpc [20] hub [19:16] access [14:8] client [4:0] fault type
|
读取后清除 valid 位(line 475),让 GPU 可以复用该条目。
4.3 故障排序 — nouveau_svm_fault_cmp
1 2 3 4 5 6 7 8 9 10 11 12
| static int nouveau_svm_fault_cmp(const void *a, const void *b) { const struct nouveau_svm_fault *fa = *(struct nouveau_svm_fault **)a; const struct nouveau_svm_fault *fb = *(struct nouveau_svm_fault **)b; int ret; if ((ret = (s64)fa->inst - fb->inst)) return ret; if ((ret = (s64)fa->addr - fb->addr)) return ret; return nouveau_svm_fault_priority(fa->access) - nouveau_svm_fault_priority(fb->access); }
|
排序优先级(nouveau_svm_fault_priority,line 420-436):
1
| PREFETCH(0) < READ(1) < WRITE(2) < ATOMIC(3)
|
这样 WRITE 和 ATOMIC 故障排最前面,同一地址只需处理一次——WRITE 处理已经满足了 READ 需求。
4.4 故障处理主循环 — nouveau_svm_fault
1 2 3
| static void nouveau_svm_fault(struct work_struct *work)
|
这是整个 nouveau SVM 的核心:
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
| Phase 1: 读取 while (GET != PUT) { nouveau_svm_fault_cache() ← 从 GPU buffer 读一个 32B 条目 GET++ } nvif_wr32(device, getaddr, GET) ← 更新 GPU 侧 GET 指针
Phase 2: 排序 sort(buffer->fault, buffer->fault_nr, ..., nouveau_svm_fault_cmp, NULL)
Phase 3: 实例查找 mutex_lock(&svm->mutex) for each fault: ivmm = nouveau_ivmm_find(svm, fault->inst) ← inst → svmm fault->svmm = ivmm ? ivmm->svmm : NULL ← 非 SVM 通道 → NULL
Phase 4: 逐故障处理 for each fault: ├─ svmm == NULL → nouveau_svm_fault_cancel() ← 非 SVM 通道,取消故障 ├─ ATOMIC → nouveau_atomic_range_fault() ← 原子操作专用路径 └─ 其他 → nouveau_range_fault() ← 标准 HMM 路径
Phase 5: 故障重放 if (replay > 0) nouveau_svm_fault_replay(svm) ← 通知 GPU 重试之前故障的访问
|
4.5 故障重放与取消
1 2 3 4 5 6 7 8
| static void nouveau_svm_fault_replay(struct nouveau_svm *svm) { WARN_ON(nvif_object_mthd(&svm->drm->client.vmm.vmm.object, GP100_VMM_VN_FAULT_REPLAY, &(struct gp100_vmm_fault_replay_vn) {}, ...)); }
|
重放:告诉 GPU “之前故障的地址现在有页表了,重试你的访问”。
1 2 3 4 5 6 7 8
| static void nouveau_svm_fault_cancel(struct nouveau_svm *svm, u64 inst, u8 hub, u8 gpc, u8 client) { WARN_ON(nvif_object_mthd(&svm->drm->client.vmm.vmm.object, GP100_VMM_VN_FAULT_CANCEL, ...)); }
|
取消:无法处理的故障(非 SVM 通道、mm 已销毁等)→ 告诉 GPU “放弃,触发引擎重置,杀掉通道”(GPU SIGSEGV)。
4.6 标准 HMM 路径 — nouveau_range_fault
1 2 3 4 5 6
| static int nouveau_range_fault(struct nouveau_svmm *svmm, struct nouveau_drm *drm, struct nouveau_pfnmap_args *args, u32 size, unsigned long hmm_flags, struct svm_notifier *notifier)
|
这是 nouveau 调用 HMM 的关键函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 1. mmu_interval_notifier_insert() — 注册 per-fault interval notifier ↓ (line 671-673) 2. 循环 (timeout 保护): ├─ mmu_interval_read_begin() ← 获取当前 seqno ├─ mmap_read_lock(mm) ← 持有 mmap 读锁 ├─ hmm_range_fault(&range) ← 核心 HMM 调用! ├─ mmap_read_unlock(mm) └─ mutex_lock(&svmm->mutex) ├─ mmu_interval_read_retry()? ← seqno 变了?重试 └─ 不变 → break ↓ (line 680-703) 3. nouveau_hmm_convert_pfn() — 将 HMM PFN 转为 nouveau 内部格式 ↓ (line 705) 4. nvif_object_ioctl(..., NVIF_VMM_V0_PFNMAP, ...) — 编程 GPU 页表 ↓ (line 707) 5. mmu_interval_notifier_remove() — 清理
|
nouveau_hmm_convert_pfn 是 PFN 格式转换的关键(line 537-583):
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
| static void nouveau_hmm_convert_pfn(struct nouveau_drm *drm, struct hmm_range *range, struct nouveau_pfnmap_args *args) { if (!(range->hmm_pfns[0] & HMM_PFN_VALID)) { args->p.phys[0] = 0; return; }
page = hmm_pfn_to_page(range->hmm_pfns[0]);
if (hmm_pfn_to_map_order(range->hmm_pfns[0])) { args->p.page = hmm_pfn_to_map_order(range->hmm_pfns[0]) + PAGE_SHIFT; args->p.size = 1UL << args->p.page; }
if (is_device_private_page(page)) args->p.phys[0] = nouveau_dmem_page_addr(page) | NVIF_VMM_PFNMAP_V0_V | NVIF_VMM_PFNMAP_V0_VRAM; else args->p.phys[0] = page_to_phys(page) | NVIF_VMM_PFNMAP_V0_V | NVIF_VMM_PFNMAP_V0_HOST;
if (range->hmm_pfns[0] & HMM_PFN_WRITE) args->p.phys[0] |= NVIF_VMM_PFNMAP_V0_W; }
|
关键标志位:
NVIF_VMM_PFNMAP_V0_V:页表项有效
NVIF_VMM_PFNMAP_V0_VRAM:指向 VRAM(设备物理地址→nouveau_dmem_page_addr)
NVIF_VMM_PFNMAP_V0_HOST:指向系统内存(物理地址→page_to_phys)
NVIF_VMM_PFNMAP_V0_W:可写
NVIF_VMM_PFNMAP_V0_A:原子操作支持
4.7 原子操作路径 — nouveau_atomic_range_fault
1 2 3 4 5
| static int nouveau_atomic_range_fault(struct nouveau_svmm *svmm, struct nouveau_drm *drm, struct nouveau_pfnmap_args *args, u32 size, struct svm_notifier *notifier)
|
原子操作(ATOMIC)不能通过 HMM 的标准路径处理,因为需要 exclusive 访问权:
1 2 3 4 5 6 7 8 9 10
| 1. mmu_interval_notifier_insert() ← 注册 interval notifier 2. make_device_exclusive(mm, start, drm->dev, &folio) ← 核心! ↓ 让 CPU 侧该页 exclusive 给设备 3. mutex_lock(&svmm->mutex) ├─ mmu_interval_read_retry()? └─ 不变 → break 4. nvif_object_ioctl(..., NVIF_VMM_PFNMAP_V0_V | V0_W | V0_A | V0_HOST, ...) ↓ 编程 GPU 页表,标记原子操作支持 5. folio_unlock(folio); folio_put(folio) 6. mmu_interval_notifier_remove()
|
注意:原子操作只能映射到 HOST 内存(系统内存),不可能是 VRAM(NVIF_VMM_PFNMAP_V0_HOST)。
4.8 故障分组优化
同一 SVMM 的连续故障可以批处理(line 778-864):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| for (fi = 0; fn = fi + 1, fi < buffer->fault_nr; fi = fn) { start = buffer->fault[fi]->addr; limit = start + PAGE_SIZE; ret = nouveau_range_fault(svmm, drm, args, ..., hmm_flags, ¬ifier);
limit = args->p.addr + args->p.size; for (fn = fi; ++fn < buffer->fault_nr; ) { if (buffer->fault[fn]->svmm != svmm || buffer->fault[fn]->addr >= limit || (fi->access == READ && !V) || (fi->access != READ && fi->access != PREFETCH && !W) || ...) break; } }
|
5. CPU 侧 MMU Notifier — 页表失效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static int nouveau_svmm_invalidate_range_start(struct mmu_notifier *mn, const struct mmu_notifier_range *update) { struct nouveau_svmm *svmm = container_of(mn, struct nouveau_svmm, notifier);
if (update->event == MMU_NOTIFY_MIGRATE && update->owner == svmm->vmm->cli->drm->dev) goto out;
if (limit > svmm->unmanaged.start && start < svmm->unmanaged.limit) { if (start < svmm->unmanaged.start) nouveau_svmm_invalidate(svmm, start, svmm->unmanaged.limit); start = svmm->unmanaged.limit; }
nouveau_svmm_invalidate(svmm, start, limit); }
|
当 CPU 侧页表变化(munmap, mprotect, madvise 等)时,通过 mmu_notifier 回调失效对应的 GPU 页表项。关键是跳过 self-migration——如果 GPU 自己正在迁移页,不需要再失效自己刚刚修改的页表。
nouveau_svmm_invalidate(line 239-249)通过 NVIF_VMM_V0_PFNCLR 方法清除 GPU 页表项。
6. Blockable interval notifier
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static bool nouveau_svm_range_invalidate(struct mmu_interval_notifier *mni, const struct mmu_notifier_range *range, unsigned long cur_seq) { if (range->event == MMU_NOTIFY_EXCLUSIVE && range->owner == sn->svmm->vmm->cli->drm->dev) return true;
if (mmu_notifier_range_blockable(range)) mutex_lock(&sn->svmm->mutex); else if (!mutex_trylock(&sn->svmm->mutex)) return false; mmu_interval_set_seq(mni, cur_seq); mutex_unlock(&sn->svmm->mutex); return true; }
|
这是 per-fault 处理的 interval notifier——与设备级 mmu_notifier 配合,在 HMM range 操作期间保护页表一致性。
7. 完整流程图
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 32
| 应用: cudaMallocManaged(p, size) ↓ GPU 访问 p[0] → MMU 缺页 ↓ GPU 写入 fault buffer: { inst, addr=0x7f..., access=WRITE } GPU 更新 PUT 指针 GPU 触发中断 ↓ nvif_event → schedule_work(&buffer->work) ↓ nouveau_svm_fault(): ├─ 读取所有故障条目 ├─ 排序 (inst → addr → access priority) ├─ inst → svmm 映射 ├─ 对于每个故障: │ ├─ svmm 不存在 → cancel (GPU SIGSEGV) │ ├─ ATOMIC → nouveau_atomic_range_fault() │ │ └─ make_device_exclusive() → nvif_vmm_pfnmap() │ └─ READ/WRITE/PREFETCH → nouveau_range_fault() │ ├─ mmu_interval_notifier_insert() │ ├─ loop: │ │ ├─ mmu_interval_read_begin() │ │ ├─ mmap_read_lock(mm) │ │ ├─ hmm_range_fault() ← HMM 调用! │ │ ├─ mmap_read_unlock(mm) │ │ └─ mmu_interval_read_retry()? ── YES → 重试 │ ├─ nouveau_hmm_convert_pfn() ← PFN → GPU 地址 │ │ ├─ device_private_page? → nouveau_dmem_page_addr() │ │ └─ normal page? → page_to_phys() │ ├─ nvif_vmm_pfnmap() ← 编程 GPU 页表 │ └─ mmu_interval_notifier_remove() └─ nouveau_svm_fault_replay() → GPU 重试访问
|
8. 与 DRM GPUSVM 的关系
本篇 nouveau_svm.c 使用的是一个较老的自有实现(Copyright 2018),不依赖 drm_gpusvm.c(后者是 2024 年的新框架)。但它们的设计思想高度一致:
| 概念 |
nouveau_svm.c (2018) |
drm_gpusvm.c (2024) |
| Notifier |
mmu_notifier + svm_notifier |
drm_gpusvm_notifier (interval tree) |
| Range |
隐式(单页 fault) |
drm_gpusvm_range (多页 chunk) |
| HMM 调用 |
hmm_range_fault() 1 页 |
hmm_range_fault() N 页 |
| DMA 映射 |
GPU 内部处理 |
drm_gpusvm_pages |
| 锁 |
svmm->mutex |
gpusvm->notifier_lock (rw_sem) |
nouveau 的实现更紧密地耦合到 NVIDIA 硬件(nvif_vmm_pfnmap / nvif_object_ioctl),而 GPUSVM 提供了更通用的框架层。
下一篇文章
第4篇:NVIDIA 设备显存管理:nouveau_dmem.c