NUMA-04 显存迁回内存:AMD/Intel 的 SVM 与Eviction的实现是否考虑了NUMA

📅 2026/7/31 8:27:31 👁️ 阅读次数 📝 编程学习
NUMA-04 显存迁回内存:AMD/Intel 的 SVM 与Eviction的实现是否考虑了NUMA

前三篇的迁移都在“普通内存 ↔ 普通内存”。但 GPU 显存 CPU 根本访问不到,它是怎么被纳入进程地址空间、又怎么在缺页时自动迁回内存,以及回迁时怎么选择numa node。这将是本文的主题。


0. 本章要回答的问题

  1. GPU 显存这种 CPU 不能直接读的内存,怎么变成进程页表里的一个页?(ZONE_DEVICE/ device private page)
  2. CPU 去访问一个“其实在显存里”的地址时会发生什么?(migrate_to_ram缺页回调)
  3. device→host 迁移时,那个“目标 CPU 页”是在哪分配、落在哪个 node 的?

第 3 问是重点。本文更多的是分析当前的实现是否考虑了NUMA,至于怎么优化放在后文分析。


1. HMM 与 ZONE_DEVICE:把设备内存塞进内存地图

1.1 为什么需要它

统一内存(SVM / HMM)的目标是:CPU 和 GPU 共享同一套虚拟地址,一个指针两边都能用。难点在于——同一个虚拟地址,其物理页可能此刻在系统内存、下一刻被迁到了显存。内核需要一种办法,把“显存里的页”也表示成一个struct page,挂进进程页表,这样缺页、迁移、反向映射这些既有机制才能复用。

ZONE_DEVICE就是这个办法:它为设备内存创建struct page,但标记成特殊类型。其中 GPU 显存用的是MEMORY_DEVICE_PRIVATEinclude/linux/memremap.h):

// include/linux/memremap.henummemory_type{/* 0 is reserved to catch uninitialized type fields */MEMORY_DEVICE_PRIVATE=1,// 设备私有内存:CPU 不能直接访问// ...};

MEMORY_DEVICE_PRIVATE的含义:这些页有struct page、能进页表,但 CPU 不能直接解引用——CPU 一旦访问,就触发缺页,由驱动把它迁回真正的系统内存。

1.2 显存怎么注册进来:devm_memremap_pages

驱动在初始化时,把一段显存通过devm_memremap_pages()(或memremap_pages())注册为ZONE_DEVICE,内核为这段显存建立struct page数组,并绑定一个struct dev_pagemapinclude/linux/memremap.h):

// include/linux/memremap.hstructdev_pagemap{// ...conststructdev_pagemap_ops*ops;// 关键:设备页的行为回调// ...};structdev_pagemap_ops{void(*page_free)(structpage*page);vm_fault_t(*migrate_to_ram)(structvm_fault*vmf);// CPU 访问设备页时的缺页回调};

GPU VRAM 一段物理区间

devm_memremap_pages()
注册为 ZONE_DEVICE

生成 struct page 数组
type = MEMORY_DEVICE_PRIVATE

绑定 dev_pagemap
.ops.migrate_to_ram = 驱动回调

这些 page 可以挂进进程页表


2. CPU 访问设备页:migrate_to_ram缺页回调

当一段 SVM 内存当前在显存(页表项指向 device private page),而 CPU 代码去读写它时,MMU 发现这是特殊页,触发缺页异常,内核回到dev_pagemap_ops.migrate_to_ram这个回调——把页迁回系统内存,再让 CPU 访问继续。

KFD 路径的注册(drivers/gpu/drm/amd/amdkfd/kfd_migrate.c):

// drivers/gpu/drm/amd/amdkfd/kfd_migrate.cstaticconststructdev_pagemap_opssvm_migrate_pgmap_ops={.page_free=svm_migrate_page_free,.migrate_to_ram=svm_migrate_to_ram,// CPU 缺页 → 迁回系统内存};

svm_migrate_to_ram()最终调用svm_migrate_vram_to_ram(),走的正是第 03 章的migrate_vma三段式,方向标志选设备页(kfd_migrate.c):

// kfd_migrate.c svm_migrate_vma_to_ram()if(adev->gmc.xgmi.connected_to_cpu)migrate.flags=MIGRATE_VMA_SELECT_DEVICE_COHERENT;elsemigrate.flags=MIGRATE_VMA_SELECT_DEVICE_PRIVATE;// 挑出设备私有页// ...r=migrate_vma_setup(&migrate);// 收集源(显存)页// ... 驱动为每个源页分配一个“系统内存页”填进 migrate.dst[]migrate_vma_pages(&migrate);migrate_vma_finalize(&migrate);

整条缺页迁回链路:

否,就在内存

CPU 读写一个 SVM 地址

页表项指向
device private page?

MMU 触发缺页

dev_pagemap_ops.migrate_to_ram()
= svm_migrate_to_ram

migrate_vma 三段式
flags = DEVICE_PRIVATE

为每个显存页分配
一个系统内存页 → dst[]

拷贝、重映射、收尾
CPU 访问继续

这部分的详细原理,可以从参考Linux 内存管理子系统的进化——从 MM 到 HMM。


3. 目标 CPU 页在哪分配、落哪个 node

migrate_vma三段式里“填dst[]”这一步在驱动手里,填的时候用哪个 node 分配,就决定了页迁回后落在哪个 node

3.1 AMD KFD 路径:alloc_page_vma(跟随 VMA 策略)

KFD 用svm_migrate_get_sys_page()分配目标系统页(kfd_migrate.c):

// kfd_migrate.cstaticstructpage*svm_migrate_get_sys_page(structvm_area_struct*vma,unsignedlongaddr){structpage*page;page=alloc_page_vma(GFP_HIGHUSER,vma,addr);// ← 落点由 VMA 的 mempolicy 决定if(page)lock_page(page);returnpage;}

alloc_page_vma()这段 VMA 的 NUMA 内存策略分配:没有特殊策略时,就是 first-touch/当前 node。(这里的“VMA 策略”就是第 02 章mbind设定的“地址段级策略”,内核里挂在 VMA 上;没设时退化成第 01 章讲的 first-touch。)也就是说——KFD 迁回的落点,取决于 VMA 策略或触发缺页的那个 CPU 所在 node,而不是“GPU 物理上最近的 node”。

3.2 DRM(drm_pagemap)路径:vma_alloc_folio/folio_alloc

Intel 的 DRM SVM 后端走drm_pagemap。它填目标页的函数是drm_pagemap_migrate_populate_ram_pfn()drivers/gpu/drm/drm_pagemap.c):

// drivers/gpu/drm/drm_pagemap.c drm_pagemap_migrate_populate_ram_pfn()/* TODO: Support fallback to single pages if THP allocation fails */if(vas)folio=vma_alloc_folio(GFP_HIGHUSER,order,vas,addr);// 有 VMA:跟随 VMA 策略elsefolio=folio_alloc(GFP_HIGHUSER,order);// 无 VMA:直接落当前 node
  • vas(VMA)时:vma_alloc_folio跟随 VMA 的 mempolicy,等价于 first-touch,落点是“碰它的 CPU”所在 node。
  • vas时(如后台驱逐 evict):folio_alloc完全不带 node 偏好,落在当前执行线程所在的 node。

两种情况都没有把“发起迁移的 GPU 最近的那个 CPU node”作为参数。在 NPS4 / 多 socket 机器上,这意味着页很可能迁到一个离目标 GPU 较远的 node,后续 CPU 访问就吃了远程延迟——这正是发现的问题。

3.3 两条路径对照

路径分配目标页的函数落点依据是否考虑 GPU 邻近 node
KFDalloc_page_vma(GFP_HIGHUSER, vma, addr)VMA mempolicy / 当前 node
DRMdrm_pagemap(有 VMA)vma_alloc_folio(...)VMA mempolicy
DRMdrm_pagemap(evict,无 VMA)folio_alloc(GFP_HIGHUSER, order)当前 node,无偏好

结论:现有 device→host 迁移的落点,从来不是“GPU 最近的 node”,而是“恰好碰它的 CPU”或“当前 node”。


4. evict 路径

除了 CPU 缺页触发的迁回,还有一类是显存吃紧时的驱逐(evict):把 SVM 显存页赶回系统内存腾空间。

evict 往往发生在没有用户 VMA 上下文的后台流程里,正对应folio_allocvas == NULL)分支——连 VMA 策略都没有,纯落当前 node,离“最近 node”更远。所以 evict 路径是这个问题最明显的受害场景。

触发迁回的两种来源

CPU 缺页
migrate_to_ram

populate_ram_pfn 分配目标页

显存驱逐
evict_to_ram

有 VMA 上下文?

vma_alloc_folio
跟随 VMA 策略

folio_alloc
落当前 node

落点都可能远离
发起 GPU 的最近 node


5. 本章小结

把“目标 node”的旅程接到本章:

现状应该怎样
用户态/上层只说“迁回 SYSMEM”传入/推导出“GPU 最近的 node id”
drm_pagemap 分配vma_alloc_folio/folio_alloc,无 node 偏好alloc_pages_node(nid, ...)之类带上目标 node
落点碰它的 CPU / 当前 nodeGPU 邻近 node)

也就是说,第 01 章“怎么反推 GPU 最近 node”、第 02 章“move_pages/分配 API 怎么指定 node”、第 03 章“node 参数怎么决定落点”,到这里合流成一个具体的问题:**在drm_pagemap的 populate_ram 分配处,把正确的 node 传进去。

总结下本文的技术要点:

  • ZONE_DEVICE/MEMORY_DEVICE_PRIVATE让显存拥有struct page、能进页表,但 CPU 不能直接访问(include/linux/memremap.h)。
  • 驱动用devm_memremap_pages()注册显存,绑定dev_pagemap.ops.migrate_to_ram回调。
  • CPU 访问设备页触发缺页 →migrate_to_rammigrate_vma三段式迁回系统内存。

下面是本文的两个焦点问题:

  • 焦点1:目标 CPU 页由alloc_page_vma(KFD)/vma_alloc_folio·folio_alloc(DRM)分配,落点是 VMA 策略或当前 node,都不考虑 GPU 邻近 node
  • 焦点2:evict 路径(无 VMA)用folio_alloc纯落当前 node,是问题最突出的场景。

接下来讨论下可能的优化以及helios和GB200这种机架式系统中的numa。


🔗关联阅读

  • 01 你的内存不是一整块:看懂 NUMA 与机器拓扑
  • 02 一段内存到底在哪个 node:用户态 NUMA 编程接口
  • 03 页是怎么在 node 间搬家的:内核 NUMA 与页迁移机制
  • ZONE_DEVICE:为设备内存创建 struct page