第4篇:NVIDIA 设备显存管理 — nouveau_dmem.c
源码:drivers/gpu/drm/nouveau/nouveau_dmem.c | 头文件:drivers/gpu/drm/nouveau/nouveau_dmem.h 系列目录:NVIDIA AI Infra 内核源码深度解析
1. 本篇位置 上一篇 nouveau_svm.c 通过 hmm_range_fault() 镜像了 CPU 页表——但 GPU 故障时,如果对应物理页在系统内存,直接映射即可。问题是:GPU 想把自己的显存 (VRAM) 也纳入统一内存系统 。
这就是 nouveau_dmem.c 的职责:将 GPU VRAM 暴露为 Linux 内核的 DEVICE_PRIVATE 内存,让内核像管理普通内存一样管理显存——缺页时自动迁移、空闲时回收。
1 2 3 4 5 6 7 8 9 10 11 应用: cudaMallocManaged(p , size ) → GPU 访问 p[0 ] ↓ nouveau_svm.c: hmm_range_fault() ├─ 页在系统内存 → 映射系统物理地址 └─ 页在 VRAM? → 查 ZONE_DEVICE page → nouveau_dmem_page_addr() ↓ CPU 访问 p[0 ] (页在 VRAM) ↓ CPU 缺页 → ZONE_DEVICE → .migrate_to_ram = nouveau_dmem_migrate_to_ram() ↓ ← 本篇核心! VRAM → DMA copy → RAM → 重新映射 PTE
2. 核心数据结构 2.1 struct nouveau_dmem — 设备内存管理器 1 2 3 4 5 6 7 8 9 10 struct nouveau_dmem { struct nouveau_drm *drm ; struct nouveau_dmem_migrate migrate ; struct list_head chunks ; struct mutex mutex ; struct page *free_pages ; struct folio *free_folios ; spinlock_t lock; };
每个 GPU 设备有且仅有一个 nouveau_dmem 实例,管理该设备所有用于 SVM 的 VRAM 块。
2.2 struct nouveau_dmem_chunk — VRAM 块 1 2 3 4 5 6 7 8 struct nouveau_dmem_chunk { struct list_head list ; struct nouveau_bo *bo ; struct nouveau_drm *drm ; unsigned long callocated; struct dev_pagemap pagemap ; };
每个 chunk 代表一块连续 VRAM。关键设计:
bo 是一个 pinned TTM Buffer Object,保证 VRAM 不会被驱逐
pagemap 将这块 VRAM 注册为 ZONE_DEVICE 内存
chunk 大小 = DMEM_CHUNK_SIZE * NR_CHUNKS = 2MB × 128 = 256MB
1 2 3 4 #define DMEM_CHUNK_SIZE (2UL << 20) #define DMEM_CHUNK_NPAGES (DMEM_CHUNK_SIZE >> PAGE_SHIFT) #define NR_CHUNKS (128)
2.3 struct nouveau_dmem_migrate — 迁移引擎 1 2 3 4 5 6 struct nouveau_dmem_migrate { nouveau_migrate_copy_t copy_func; nouveau_clear_page_t clear_func; struct nouveau_channel *chan ; };
拷贝/清零函数指针(line 61-65):
1 2 3 4 5 typedef int (*nouveau_migrate_copy_t ) (struct nouveau_drm *drm, u64 npages, enum nouveau_aper dst_aper, u64 dst_addr, enum nouveau_aper src_aper, u64 src_addr) ;typedef int (*nouveau_clear_page_t ) (struct nouveau_drm *drm, u32 length, enum nouveau_aper dst_aper, u64 dst_addr) ;
地址空间枚举(line 55-59):
1 2 3 4 5 enum nouveau_aper { NOUVEAU_APER_VIRT, NOUVEAU_APER_VRAM, NOUVEAU_APER_HOST, };
3. 初始化 — nouveau_dmem_init 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void nouveau_dmem_init (struct nouveau_drm *drm) { if (drm->client.device.info.family < NV_DEVICE_INFO_V0_PASCAL) return ; drm->dmem = kzalloc_obj(*drm->dmem); drm->dmem->drm = drm; mutex_init(&drm->dmem->mutex); INIT_LIST_HEAD(&drm->dmem->chunks); spin_lock_init(&drm->dmem->lock); ret = nouveau_dmem_migrate_init(drm); }
nouveau_dmem_migrate_init(line 684-699)根据 GPU 代选择 DMA 拷贝引擎:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static int nouveau_dmem_migrate_init (struct nouveau_drm *drm) { switch (drm->ttm.copy.oclass) { case PASCAL_DMA_COPY_A: case PASCAL_DMA_COPY_B: case VOLTA_DMA_COPY_A: case TURING_DMA_COPY_A: drm->dmem->migrate.copy_func = nvc0b5_migrate_copy; drm->dmem->migrate.clear_func = nvc0b5_migrate_clear; drm->dmem->migrate.chan = drm->ttm.chan; return 0 ; } return -ENODEV; }
注意:初始化时只设置了函数指针,没有预先分配 VRAM 。VRAM 按需分配(lazy allocation),在第一次迁移请求时才调用 nouveau_dmem_chunk_alloc。
4. VRAM 分配 — nouveau_dmem_chunk_alloc 1 2 3 4 static int nouveau_dmem_chunk_alloc (struct nouveau_drm *drm, struct page **ppage, bool is_large)
这个函数是 ZONE_DEVICE 化的核心,流程如下:
4.1 分配物理地址空间 1 2 3 res = request_free_mem_region(&iomem_resource, DMEM_CHUNK_SIZE * NR_CHUNKS, "nouveau_dmem" );
关键!VRAM 的物理地址不是”真实的系统物理地址”,而是在 iomem_resource 中分配的虚拟物理地址区间 。这是 DEVICE_PRIVATE 的设计——这些地址永远不会有真实的系统内存映射,它们只是一个”占位符”,用于创建 struct page 结构体。
4.2 设置 pagemap 1 2 3 4 5 6 7 8 chunk->drm = drm; chunk->pagemap.type = MEMORY_DEVICE_PRIVATE; chunk->pagemap.range.start = res->start; chunk->pagemap.range.end = res->end; chunk->pagemap.nr_range = 1 ; chunk->pagemap.ops = &nouveau_dmem_pagemap_ops; chunk->pagemap.owner = drm->dev;
MEMORY_DEVICE_PRIVATE 是关键类型。内核的 MM 子系统会特殊对待这种内存——CPU 无法直接访问,缺页时调用 .migrate_to_ram 回调。
4.3 分配真实 VRAM 1 2 3 ret = nouveau_bo_new_pin(&drm->client, NOUVEAU_GEM_DOMAIN_VRAM, DMEM_CHUNK_SIZE, &chunk->bo);
通过 TTM 在 VRAM 中分配一个 pinned BO(2MB)。pinned 意味着这个 BO 永远不会被 TTM 驱逐。
4.4 创建 struct pages 1 2 ptr = memremap_pages(&chunk->pagemap, numa_node_id());
memremap_pages() 是为物理地址区间批量创建 struct page 的神奇函数。它使用 pagemap 中的 ops 回调集合来管理这些页的生命周期。
4.5 初始化空闲列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pfn_first = chunk->pagemap.range.start >> PAGE_SHIFT; page = pfn_to_page(pfn_first);for (i = 0 ; i < NR_CHUNKS; i++) { if (!IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) || !is_large) { for (j = 0 ; j < DMEM_CHUNK_NPAGES - 1 ; j++, pfn++) { page = pfn_to_page(pfn); page->zone_device_data = drm->dmem->free_pages; drm->dmem->free_pages = page; } } else { page = pfn_to_page(pfn); page->zone_device_data = drm->dmem->free_folios; drm->dmem->free_folios = page_folio(page); pfn += DMEM_CHUNK_NPAGES; } }
空闲页通过 zone_device_data 字段形成单向链表——这是一个精巧的复用设计,无需额外分配链表结构。支持两种分配粒度:
单页模式 :512 个独立的 4K 页
大页模式 :1 个 2MB folio(当 CONFIG_TRANSPARENT_HUGEPAGE 启用时)
4.6 Chunk 设计的物理意义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌──────────────────────────────────────────────────────┐ │ nouveau_dmem │ │ │ │ chunk [ 0 ] : 256 MB VRAM 块 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ iomem_resource [ 0 x ???, 0 x ???+ 256 MB ) │ │ │ │ ↕ ( 虚拟物理地址 ↔ VRAM 偏移) │ │ │ │ nouveau_bo ( 2 MB pinned VRAM ) │ │ │ │ → 128 个子块,每块 2 MB │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ │ │2 MB │ │2 MB │ │2 MB │ ... │2 MB │ │ │ │ │ │folio │ │512 │ │512 │ │512 │ │ │ │ │ │ │ │4 K 页│ │4 K 页│ │4 K 页│ │ │ │ │ └────┘ └────┘ └────┘ └────┘ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ chunk [ 1 ] : 256 MB VRAM 块 ( 按需分配) │ │ ... │ └──────────────────────────────────────────────────────┘
5. VRAM→RAM 迁移 — nouveau_dmem_migrate_to_ram 1 2 static vm_fault_t nouveau_dmem_migrate_to_ram (struct vm_fault *vmf)
这是 CPU 缺页处理的核心——当 CPU 访问一个在 VRAM 中的页时:
1 2 3 4 5 6 7 CPU 访问地址 A ↓ 缺页处理程序查找页表 → PTE 指向 ZONE_DEVICE 页 ↓ 内核调用 dev_pagemap_ops ->migrate_to_ram = nouveau_dmem_migrate_to_ram (vmf ) ↓
内部流程:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 struct migrate_vma args = { .vma = vmf->vma, .pgmap_owner = drm->dev, .fault_page = vmf->page, .flags = MIGRATE_VMA_SELECT_DEVICE_PRIVATE | MIGRATE_VMA_SELECT_COMPOUND, }; sfolio = page_folio(vmf->page); order = folio_order(sfolio); nr = 1 << order;if (vmf->pte) { order = 0 ; nr = 1 ; } args.start = ALIGN_DOWN(vmf->address, (PAGE_SIZE << order)); args.end = args.start + (PAGE_SIZE << order); args.src = kcalloc(nr, sizeof (*args.src), GFP_KERNEL); args.dst = kcalloc(nr, sizeof (*args.dst), GFP_KERNEL); migrate_vma_setup(&args);if (order) dpage = folio_page(vma_alloc_folio(GFP_HIGHUSER | __GFP_ZERO, order, vmf->vma, vmf->address), 0 );else dpage = alloc_page_vma(GFP_HIGHUSER | __GFP_ZERO, vmf->vma, vmf->address); args.dst[0 ] = migrate_pfn(page_to_pfn(dpage));if (order) args.dst[0 ] |= MIGRATE_PFN_COMPOUND; svmm = folio_zone_device_data(sfolio); mutex_lock(&svmm->mutex); nouveau_svmm_invalidate(svmm, args.start, args.end); nouveau_dmem_copy_folio(drm, sfolio, dfolio, &dma_info); mutex_unlock(&svmm->mutex); nouveau_fence_new(&fence, dmem->migrate.chan); migrate_vma_pages(&args); nouveau_dmem_fence_done(&fence); dma_unmap_page(drm->dev->dev, dma_info.dma_addr, PAGE_SIZE, DMA_BIDIRECTIONAL); migrate_vma_finalize(&args);
5.1 DMA 拷贝细节 — nouveau_dmem_copy_folio 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_dmem_copy_folio (struct nouveau_drm *drm, struct folio *sfolio, struct folio *dfolio, struct nouveau_dmem_dma_info *dma_info) { struct device *dev = drm->dev->dev; struct page *dpage = folio_page(dfolio, 0 ); struct page *spage = folio_page(sfolio, 0 ); folio_lock(dfolio); dma_info->dma_addr = dma_map_page(dev, dpage, 0 , page_size(dpage), DMA_BIDIRECTIONAL); drm->dmem->migrate.copy_func(drm, folio_nr_pages(sfolio), NOUVEAU_APER_HOST, dma_info->dma_addr, NOUVEAU_APER_VRAM, nouveau_dmem_page_addr(spage)); return 0 ; }
注意拷贝方向:源是 VRAM,目标是系统内存 。copy_func 是 GPU 的 DMA 拷贝引擎(nvc0b5_migrate_copy),方向编码为 NOUVEAU_APER_VRAM → NOUVEAU_APER_HOST。
5.2 nouveau_dmem_page_addr — 从 page 获取 VRAM 地址 1 2 3 4 5 6 7 8 unsigned long nouveau_dmem_page_addr (struct page *page) { struct nouveau_dmem_chunk *chunk = nouveau_page_to_chunk(page); unsigned long off = (page_to_pfn(page) << PAGE_SHIFT) - chunk->pagemap.range.start; return chunk->bo->offset + off; }
1 2 3 page → chunk (via page_pgmap → container_of → chunk)off = page 的 iomem PFN - chunk 的 iomem 起始 VRAM 地址 = BO 的 VRAM 偏移 + off
6. RAM→VRAM 迁移 — nouveau_dmem_migrate_vma 1 2 3 4 5 6 7 int nouveau_dmem_migrate_vma (struct nouveau_drm *drm, struct nouveau_svmm *svmm, struct vm_area_struct *vma, unsigned long start, unsigned long end)
这是反向迁移——将系统内存迁移到 VRAM。由用户空间 ioctl 触发(nouveau_svmm_bind)或内核迁移策略触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct migrate_vma args = { .vma = vma, .start = start, .pgmap_owner = drm->dev, .flags = MIGRATE_VMA_SELECT_SYSTEM | MIGRATE_VMA_SELECT_COMPOUND , }; / / 批量迁移,每次最多 HPAGE_PMD_NR (512)页if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) ) if (max > (unsigned long)HPAGE_PMD_NR ) max = (unsigned long)HPAGE_PMD_NR ; for (i = 0; i < npages; i += max) { migrate_vma_setup(&args ) ; if (args.cpages) nouveau_dmem_migrate_chunk(drm , svmm , &args , dma_info , pfns ) ; args.start = args.end ; }
6.1 逐页拷贝 — nouveau_dmem_migrate_copy_one 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 33 34 static unsigned long nouveau_dmem_migrate_copy_one (struct nouveau_drm *drm, struct nouveau_svmm *svmm, unsigned long src, struct nouveau_dmem_dma_info *dma_info, u64 *pfn) { spage = migrate_pfn_to_page(src); dpage = nouveau_dmem_page_alloc_locked(drm, is_large); paddr = nouveau_dmem_page_addr(dpage); if (spage) { dma_map_page(dev, spage, 0 , page_size(spage), DMA_BIDIRECTIONAL); drm->dmem->migrate.copy_func(drm, ..., NOUVEAU_APER_VRAM, paddr, NOUVEAU_APER_HOST, dma_info->dma_addr); } else { drm->dmem->migrate.clear_func(drm, page_size(dpage), NOUVEAU_APER_VRAM, paddr); } dpage->zone_device_data = svmm; *pfn = NVIF_VMM_PFNMAP_V0_V | NVIF_VMM_PFNMAP_V0_VRAM | ((paddr >> PAGE_SHIFT) << NVIF_VMM_PFNMAP_V0_ADDR_SHIFT); if (src & MIGRATE_PFN_WRITE) *pfn |= NVIF_VMM_PFNMAP_V0_W; return migrate_pfn(page_to_pfn(dpage)); }
注意 dpage->zone_device_data = svmm——这个字段在迁移回 RAM 时被 nouveau_dmem_migrate_to_ram 用来查找 svmm 并失效 GPU 页表。
6.2 Chunk 级迁移提交 — nouveau_dmem_migrate_chunk 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 33 34 static void nouveau_dmem_migrate_chunk (struct nouveau_drm *drm, struct nouveau_svmm *svmm, struct migrate_vma *args, struct nouveau_dmem_dma_info *dma_info, u64 *pfns) { for (i = 0 ; addr < args->end; ) { args->dst[i] = nouveau_dmem_migrate_copy_one(drm, svmm, args->src[i], dma_info + nr_dma, pfns + i); if (!args->dst[i]) { i++; addr += PAGE_SIZE; continue ; } if (!dma_mapping_error(..., dma_info[nr_dma].dma_addr)) nr_dma++; folio = page_folio(migrate_pfn_to_page(args->dst[i])); order = folio_order(folio); i += 1 << order; addr += (1 << order) * PAGE_SIZE; } nouveau_fence_new(&fence, drm->dmem->migrate.chan); migrate_vma_pages(args); nouveau_dmem_fence_done(&fence); nouveau_pfns_map(svmm, args->vma->vm_mm, args->start, pfns, i, order); while (nr_dma--) dma_unmap_page(...); migrate_vma_finalize(args); }
注意 nouveau_pfns_map 调用——迁移完成后,需要立即更新 GPU 页表,让 GPU 知道这些页现在在 VRAM 中。
7. DMA 拷贝引擎 — nvc0b5_migrate_copy 1 2 3 4 5 static int nvc0b5_migrate_copy (struct nouveau_drm *drm, u64 npages, enum nouveau_aper dst_aper, u64 dst_addr, enum nouveau_aper src_aper, u64 src_addr)
这是 GPU DMA 拷贝引擎的命令构造——通过 nvif_push 写入 GPU 命令流:
1 2 3 4 5 6 7 8 9 10 11 12 13 1. SET_SRC_PHYS_MODE: LOCAL_FB (VRAM) 或 COHERENT_SYSMEM (系统内存) 2. SET_DST_PHYS_MODE: LOCAL_FB 或 COHERENT_SYSMEM 3. OFFSET_IN: 源地址 (64位) 4. OFFSET_OUT: 目标地址 (64位) 5. PITCH_IN/PITCH_OUT: PAGE_SIZE 6. LINE_LENGTH_IN: PAGE_SIZE 7. LINE_COUNT: npages (行数) 8. LAUNCH_DMA: - DATA_TRANSFER_TYPE: NON_PIPELINED - SRC_TYPE/DST_TYPE: PHYSICAL - FLUSH_ENABLE: TRUE - MULTI_LINE_ENABLE: TRUE - SRC/DST_MEMORY_LAYOUT: PITCH
GPU 收到 LAUNCH_DMA 命令后,硬件 DMA 引擎执行 VRAM←→系统内存的实际数据搬移。
nvc0b5_migrate_clear(line 627-681)用于清零 VRAM(使用 REMAP 功能,将目标映射为常量 0)。
8. Chunk 驱逐 — nouveau_dmem_evict_chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void nouveau_dmem_evict_chunk (struct nouveau_dmem_chunk *chunk) { migrate_device_range(src_pfns, chunk->pagemap.range.start >> PAGE_SHIFT, npages); for (i = 0 ; i < npages; i++) { if (src_pfns[i] & MIGRATE_PFN_MIGRATE) { dpage = alloc_page(GFP_HIGHUSER | __GFP_NOFAIL); dst_pfns[i] = migrate_pfn(page_to_pfn(dpage)); nouveau_dmem_copy_folio(chunk->drm, page_folio(migrate_pfn_to_page(src_pfns[i])), page_folio(dpage), &dma_info[i]); } } migrate_device_pages(src_pfns, dst_pfns, npages); migrate_device_finalize(src_pfns, dst_pfns, npages); }
与常规的 migrate_vma_* 不同,驱逐使用 migrate_device_* API——因为此时没有进程上下文(设备正在关闭)。
9. pagemap ops — 生命周期回调 1 2 3 4 5 6 static const struct dev_pagemap_ops nouveau_dmem_pagemap_ops = { .folio_free = nouveau_dmem_folio_free, .migrate_to_ram = nouveau_dmem_migrate_to_ram, .folio_split = nouveau_dmem_folio_split, };
folio_free(line 118-140):页被释放时,放回 chunk 的空闲链表。支持大页和单页两种路径
migrate_to_ram(line 183-278):CPU 缺页时自动调用,完成 VRAM→RAM 迁移
folio_split(line 280-287):大页分裂时,子页继承 pgmap 和 zone_device_data
10. 完整迁移状态机 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 33 34 CPU 访问 GPU 访问 │ │ ┌───────▼────────┐ ┌────────▼────────┐ │ 页在 VRAM 吗? │ │ 页在 VRAM 吗? │ └───────┬────────┘ └────────┬────────┘ │ YES │ YES ▼ ▼ ┌─────────────────────────┐ ┌──────────────────────────┐ │ nouveau_dmem_ │ │ 直接访问 VRAM │ │ migrate_to_ram () │ │ (通过 GPU 页表映射) │ │ ├─ DMA copy VRAM→RAM │ └──────────────────────────┘ │ ├─ migrate_vma_pages () │ │ └─ PTE → RAM │ └───────────┬─────────────┘ │ NO ▼ ▼ 页现在在 RAM 中 ┌──────────────────────────┐ │ nouveau_svm.c : │ │ hmm_range_fault () │ │ ├─ 页在 RAM → 映射 │ │ └─ 页在 VRAM → │ │ nouveau_dmem_page_addr│ └──────────────────────────┘ 用户: cudaMemPrefetchAsync (p, size, GPU) ↓ ┌────────────────────────────┐ │ nouveau_dmem_migrate_vma () │ │ ├─ migrate_vma_setup () │ │ ├─ DMA copy RAM→VRAM │ │ ├─ migrate_vma_pages () │ │ ├─ nouveau_pfns_map () │ ← 更新 GPU 页表 │ └─ 页现在在 VRAM 中 │ └────────────────────────────┘
11. 总结 nouveau_dmem.c 实现了 GPU 显存的 ZONE_DEVICE 化,核心要点:
DEVICE_PRIVATE 内存 :VRAM 通过 memremap_pages(MEMORY_DEVICE_PRIVATE) 获得 struct page,但 CPU 不可直接访问
缺页自动迁移 :CPU 缺页 → nouveau_dmem_migrate_to_ram → DMA 拷贝 → 重新映射
主动迁移 :用户/驱动调用 nouveau_dmem_migrate_vma → DMA 拷贝 → 更新 GPU 页表
DMA 引擎 :nvc0b5_migrate_copy/clear 通过 GPU push buffer 命令 DMA 引擎搬数据
Chunk 管理 :256MB 的 chunk,内部 128 个 2MB 子块,按需分配,空闲链表回收
下一篇文章将深入 TTM 框架,看 GPU 如何在 VRAM、GTT、SYSTEM 三种内存类型之间管理 BO placement。
下一篇文章 第5篇:GPU 多内存类型管理:TTM 框架