一、ZONE_DEVICE的设计目标核心:解决设备内存无法融入内核内存管理体系的问题
Linux 内核的内存管理(mm)子系统完全围绕 struct page 构建。页表管理、迁移、引用计数、cgroup 记账、OOM killer、rss 统计------所有这些机制都依赖每一块内存有一个对应的 struct page。
但是,设备内存(如 GPU VRAM、PMEM、CXL 内存)天然不在这个体系内 。GPU 的 16GB VRAM 没有对应的 struct page,内核对这块内存一无所知。这导致了一系列问题。
问题 1:分裂的地址空间(Split Address Space)
正如 Documentation/mm/hmm.rst 所述:
Devices with a large amount of on board memory have historically managed their memory through dedicated driver specific APIs. This creates a disconnect between memory allocated and managed by a device driver and regular application memory.
程序员必须显式地用设备专用 API(如 cudaMalloc)分配设备内存,再手动拷贝数据。对于复杂的数据结构(链表、树),重新映射所有指针几乎不可能正确完成。
问题 2:迁移框架不可用
Linux 的 migrate_vma_* 迁移框架基于 struct page 工作。没有 struct page,就不能用标准的页面迁移机制在 RAM 和 VRAM 之间搬移数据。驱动需要自己实现一整套迁移逻辑。
问题 3:CPU 页表无法追踪设备页
当数据从 RAM 迁移到 VRAM 后,CPU 页表中该地址的 PTE 该指向什么?在传统方案中只能直接 unmap。但这样内核就失去了对这块内存的追踪能力------它不知道该地址的数据去了哪个设备,CPU 再访问时也不知道该怎么处理。
问题 4:早期 HMM 的失败尝试
Documentation/mm/hmm.rst 中记录了历史:
Several different designs were tried. The first one used a device specific data structure to keep information about migrated memory and HMM hooked itself in various places of mm code. It turns out that this ended up replicating most of the fields of struct page and also needed many kernel code paths to be updated.
第一版 HMM 试图用自定义结构代替 struct page,结果发现几乎重新发明了 struct page 的所有字段,而且需要修改大量内核路径来识别这种"特殊内存"。
二、ZONE_DEVICE 的设计思路
认识到 "内核的一切都围绕 struct page 转" 这个现实后,ZONE_DEVICE 的设计思路很直接:
给设备内存也创建
struct page,但标记它们为特殊类型,让大部分内核代码无需修改就能处理。
具体设计:
传统 Zone 架构:
ZONE_DMA → struct page → 真实 RAM(低地址)
ZONE_NORMAL → struct page → 真实 RAM
ZONE_MOVABLE → struct page → 可迁移 RAM
ZONE_DEVICE 扩展:
ZONE_DEVICE → struct page → 设备内存(VRAM/PMEM/CXL)
↑
不是真实 RAM,但有了 struct page
就能融入内核的整个内存管理框架
三、ZONE_DEVICE 解决了什么
| 之前的问题 | ZONE_DEVICE 的解决方案 |
|---|---|
| 设备内存没有 struct page | 通过 devm_memremap_pages() 为每个设备内存页面分配 struct page |
| 迁移框架无法使用 | 有了 struct page,migrate_vma_* 可以直接在 RAM ↔ VRAM 之间迁移 |
| CPU 页表无法追踪 | MEMORY_DEVICE_PRIVATE 页面在 CPU 页表中表示为 device_private swap entry,CPU 访问触发 fault,自动回迁 |
| 需要修改大量内核代码 | 大部分内核代码只操作 struct page 元数据,从不访问页面内容,因此无需修改 |
| rss/cgroup 记账缺失 | 设备页面像普通页面一样被记账到 rss 和 memcg |
四、MEMORY_DEVICE 类型细分
ZONE_DEVICE 进一步细分为不同类型,针对不同硬件特性(定义在 include/linux/memremap.h):
ZONE_DEVICE
├── MEMORY_DEVICE_PRIVATE ← 独立显卡 VRAM(CPU 不可直接访问)
│ ├── struct page 存在,但 CPU 不能 map
│ ├── 页表中用 device_private swap entry 表示
│ ├── CPU 访问触发 fault → migrate_to_ram 回调 → SDMA 拷贝回 RAM
│ ├── 地址空间:devm_request_free_mem_region 分配的虚拟 PFN(非真实物理地址)
│ └── 初始化:add_pages()(仅分配 struct page,不创建线性映射)
│
├── MEMORY_DEVICE_COHERENT ← XGMI/CXL 连接的设备内存(CPU 可缓存一致访问)
│ ├── struct page 存在,CPU 可以直接访问
│ ├── 地址空间:使用真实的 MMIO BAR 地址(aper_base)
│ └── 初始化:arch_add_memory()(分配 struct page + 创建线性映射)
│
├── MEMORY_DEVICE_FS_DAX ← 持久内存(PMEM/DAX)
├── MEMORY_DEVICE_GENERIC ← 通用 DAX 设备
└── MEMORY_DEVICE_PCI_P2PDMA ← PCI P2P DMA 内存
PRIVATE 与 COHERENT 的关键区别
| 特性 | MEMORY_DEVICE_PRIVATE | MEMORY_DEVICE_COHERENT |
|---|---|---|
| CPU 可访问 | ❌ 不可访问 | ✅ 缓存一致访问 |
| 物理地址 | 虚拟 PFN(占位符) | 真实 BAR 地址 |
| CPU 页表 | swap entry | 真实 PTE |
| 线性映射 | 无 | 有(__va() 可用) |
| 典型硬件 | PCIe 独立 GPU | XGMI/CXL 连接的设备 |
| 注册方式 | add_pages() |
arch_add_memory() |
五、注册流程:devm_memremap_pages 内部机制
注册设备内存为 ZONE_DEVICE 的核心函数是 devm_memremap_pages(),定义在 mm/memremap.c:
devm_memremap_pages(dev, pgmap)
└─ memremap_pages(pgmap, nid)
├─ 参数校验
│ ├─ PRIVATE 类型:必须提供 migrate_to_ram、page_free、owner
│ └─ COHERENT 类型:必须提供 page_free、owner
├─ percpu_ref_init(引用计数初始化)
└─ pagemap_range(pgmap, params, range_id, nid)
├─ xa_store_range(pgmap_array)
│ └─ 将 PFN→pgmap 映射存入全局 xarray
│ 使 get_dev_pagemap(pfn) 可以通过 PFN 找到对应的 pgmap
├─ pfnmap_track(注册 PFN 范围追踪)
├─ 分配 struct page:
│ ├─ PRIVATE: add_pages() ← 仅分配 struct page,不创建线性映射
│ └─ COHERENT: arch_add_memory() ← 分配 struct page + 创建线性映射
├─ move_pfn_range_to_zone(ZONE_DEVICE, ...)
│ └─ 将这些 PFN 移入 ZONE_DEVICE 区域
└─ memmap_init_zone_device(zone, start_pfn, nr_pages, pgmap)
└─ 遍历每个 PFN,调用 __init_zone_device_page:
├─ 设置 page->pgmap = pgmap
├─ 设置 page 所属 zone = ZONE_DEVICE
└─ 设置初始引用计数为 0(空闲状态)
关键结构:dev_pagemap
c
struct dev_pagemap {
struct vmem_altmap altmap; // 可选:用设备内存本身存储 vmemmap
struct percpu_ref ref; // 引用计数,保护整个映射生命周期
struct completion done; // 引用降到 0 时的完成通知
enum memory_type type; // PRIVATE/COHERENT/FS_DAX/...
unsigned int flags; // PGMAP_ALTMAP_VALID 等
unsigned long vmemmap_shift; // compound page 支持
const struct dev_pagemap_ops *ops; // 回调函数表
void *owner; // 设备标识(用于多设备过滤)
int nr_range; // 地址范围数量
union {
struct range range; // 单个地址范围
struct range ranges[]; // 多个地址范围
};
};
回调函数表:dev_pagemap_ops
c
struct dev_pagemap_ops {
void (*page_free)(struct page *page);
// 当 ZONE_DEVICE 页面引用计数降到 0 时调用
// 驱动在此释放关联的资源(如 svm_range_bo 引用)
vm_fault_t (*migrate_to_ram)(struct vm_fault *vmf);
// 仅 MEMORY_DEVICE_PRIVATE 需要
// CPU 访问 device_private 页面时触发的 fault 处理器
// 驱动在此将数据从 VRAM 拷贝回 RAM
int (*memory_failure)(...);
// 可选:处理硬件内存错误
};
六、PRIVATE 类型的虚拟物理地址空间
虚拟物理地址空间,听起来很绕,但看完实现,你觉得是否合理了呢。
对于 MEMORY_DEVICE_PRIVATE,设备内存(如 GPU VRAM)在 CPU 物理地址空间中没有真实的地址。内核需要一种方式为每个 VRAM 页面分配一个唯一的 PFN,以便创建对应的 struct page。
devm_request_free_mem_region 的作用
c
res = devm_request_free_mem_region(
adev->dev, &iomem_resource, size);
该函数在CPU 物理地址空间的全局资源树中找到一段未使用的地址范围 ,标记为 IORES_DESC_DEVICE_PRIVATE_MEMORY。
关键理解:这些地址不对应任何真实的物理内存! 它们只是 PFN 空间中的占位符,目的是:
- 为每个 VRAM 页面提供一个唯一的 PFN
- 通过 PFN 可以用
pfn_to_page()找到对应的struct page - 通过
struct page可以用page_pgmap()找到设备的dev_pagemap
地址翻译
驱动需要在 VRAM 偏移和虚拟 PFN 之间做转换。以 KFD 的实现(kfd_migrate.c)为例:
c
// VRAM 偏移 → 虚拟 PFN
unsigned long svm_migrate_addr_to_pfn(struct amdgpu_device *adev, unsigned long addr)
{
return (addr + adev->kfd.pgmap.range.start) >> PAGE_SHIFT;
}
// struct page → VRAM 偏移
unsigned long svm_migrate_addr(struct amdgpu_device *adev, struct page *page)
{
unsigned long addr = page_to_pfn(page) << PAGE_SHIFT;
return (addr - adev->kfd.pgmap.range.start);
}
关系图:
VRAM 偏移 虚拟物理地址 struct page
0x0000_0000 ←→ pgmap.range.start + 0x0000_0000 ←→ pfn_to_page(PFN_0)
0x0000_1000 ←→ pgmap.range.start + 0x0000_1000 ←→ pfn_to_page(PFN_1)
...
real_vram_size ←→ pgmap.range.end ←→ pfn_to_page(PFN_N)
七、Swap Entry 的巧妙复用
对于 MEMORY_DEVICE_PRIVATE,当页面迁移到设备后,CPU 页表中使用特殊的 swap entry:
CPU 页表 PTE 内容:
迁移前: 有效 PTE → 物理页号 → 指向 RAM 中的页面
迁移后: device_private swap entry → 编码了设备页面的 PFN + 写权限
CPU 访问该地址时:
→ MMU 发现不是有效 PTE → page fault
→ do_swap_page() 识别出 device_private entry
→ 通过 PFN 找到 struct page → 通过 page_pgmap() 找到 pgmap
→ 调用 pgmap->ops->migrate_to_ram(vmf)
→ 驱动用 DMA 引擎将数据从 VRAM 拷贝回 RAM
→ 恢复正常 PTE,用户进程继续执行
这复用了内核已有的 swap 机制,无需新的 fault 处理路径。
八、Owner 机制:多设备场景
pgmap->owner 字段解决了多 GPU 场景下的设备识别问题。
KFD 的 owner 定义
c
#define SVM_ADEV_PGMAP_OWNER(adev) \
((adev)->hive ? (void *)(adev)->hive : (void *)(adev))
- 单 GPU :owner =
adev指针本身 - XGMI hive(多 GPU 互联) :owner =
hive指针(同一 hive 内所有 GPU 共享)
在迁移中的作用
在 migrate_vma_collect(mm/migrate_device.c)遍历页表时:
c
if (is_device_private_entry(entry)) {
page = pfn_swap_entry_to_page(entry);
pgmap = page_pgmap(page);
if (pgmap->owner != migrate->pgmap_owner)
goto next; // 跳过:这个页面属于其他设备
// 继续处理:这个页面属于我们,可以迁移
}
同时在 MMU notifier 中也传递 owner:
c
mmu_notifier_range_init_owner(&range, MMU_NOTIFY_MIGRATE, ...,
migrate->pgmap_owner);
这允许设备驱动在收到 invalidate 通知时,识别出迁移是自己发起的,从而跳过不必要的 GPU 页表刷新。
九、struct page 生命周期
注册完成后,ZONE_DEVICE 的 struct page 按如下方式工作:
1. 初始状态:refcount=0, 空闲在 ZONE_DEVICE
(注:不在任何 free list 中,由驱动自行管理分配)
2. 分配(迁移 RAM→VRAM 时,驱动调用):
zone_device_page_init(page)
├─ percpu_ref_tryget_live(&pgmap->ref) // 增加 pgmap 引用
├─ set_page_count(page, 1) // refcount = 1
└─ lock_page(page) // 锁定页面
page->zone_device_data = driver_data // 存储驱动私有数据
3. 安装到 CPU 页表:
migrate_vma_pages → __migrate_device_pages → migrate_vma_insert_page
└─ 写入 device_private swap entry(PRIVATE)
或写入真实 PTE(COHERENT)
4. CPU 访问触发回迁(仅 PRIVATE):
page fault → do_swap_page → 识别 device_private entry
→ pgmap->ops->migrate_to_ram(vmf)
└─ 驱动:分配 RAM 页面, DMA 拷贝 VRAM→RAM, 更新页表
5. 释放(refcount 降到 0 时):
put_page → free_zone_device_folio (mm/memremap.c)
├─ pgmap->ops->page_free(page) // 驱动释放关联资源
├─ page->zone_device_data = NULL
└─ set_page_count(page, 0) // 重置为空闲
(注:不调用 percpu_ref_put,PRIVATE/COHERENT 页面不持有 pgmap ref)
十、系统内存开销
每个 VRAM 页面需要一个 struct page(约 64 字节)。开销计算:
| VRAM 大小 | struct page 数量 | 系统内存开销 |
|---|---|---|
| 4 GB | 1,048,576 | 64 MB |
| 8 GB | 2,097,152 | 128 MB |
| 16 GB | 4,194,304 | 256 MB |
| 32 GB | 8,388,608 | 512 MB |
KFD 通过 amdgpu_amdkfd_reserve_system_mem() 将这笔开销记账到内存限制中。
十一、kgd2kfd_init_zone_device 实例分析
KFD 的 ZONE_DEVICE 注册函数(drivers/gpu/drm/amd/amdkfd/kfd_migrate.c)是一个完整的实例:
c
int kgd2kfd_init_zone_device(struct amdgpu_device *adev)
{
// 1. 硬件检查:GFX9+ 才支持
if (amdgpu_ip_version(adev, GC_HWIP, 0) < IP_VERSION(9, 0, 1))
return -EINVAL;
if (adev->apu_prefer_gtt) // APU 使用系统内存,跳过
return 0;
// 2. 大小对齐到 2MB(内存热插拔 section 粒度)
size = ALIGN(adev->gmc.real_vram_size, 2ULL << 20);
// 3. 根据硬件拓扑选择类型
if (adev->gmc.xgmi.connected_to_cpu) {
// XGMI 连接 CPU:使用真实 BAR 地址
pgmap->range.start = adev->gmc.aper_base;
pgmap->range.end = adev->gmc.aper_base + adev->gmc.aper_size - 1;
pgmap->type = MEMORY_DEVICE_COHERENT;
} else {
// 独立 GPU:分配虚拟地址范围
res = devm_request_free_mem_region(adev->dev, &iomem_resource, size);
pgmap->range.start = res->start;
pgmap->range.end = res->end;
pgmap->type = MEMORY_DEVICE_PRIVATE;
}
// 4. 配置回调和 owner
pgmap->ops = &svm_migrate_pgmap_ops; // page_free + migrate_to_ram
pgmap->owner = SVM_ADEV_PGMAP_OWNER(adev);
// 5. 执行注册
r = devm_memremap_pages(adev->dev, pgmap);
// 6. 记账系统内存开销
amdgpu_amdkfd_reserve_system_mem(SVM_HMM_PAGE_STRUCT_SIZE(size));
}
调用时机:amdgpu_device_init → kgd2kfd_init_zone_device → amdgpu_amdkfd_device_init
即:先注册 VRAM 的 struct page 基础设施,再初始化 KFD 子系统。
十二、总结
ZONE_DEVICE 本质上是一个适配器模式(Adapter Pattern):
问题:设备内存 ≠ 系统内存,但内核只认 struct page
解决:ZONE_DEVICE = 给设备内存穿上 struct page 的外衣
让内核以为这是"特殊的 RAM",而不是"完全陌生的东西"
这个设计使得:
- 迁移 可以复用
migrate_vma_*框架 - 页表管理可以复用 swap entry 机制
- 记账可以复用 rss/memcg 机制
- 设备驱动 只需实现
page_free和migrate_to_ram两个回调
代价是每个 VRAM 页面消耗约 64 字节系统内存(struct page),但这个代价换来了与内核 mm 子系统的无缝集成。
十三、关键问题:VRAM 的 struct page 为何必须提前全部分配?
13.1 现象
调用 devm_memremap_pages() 时,内核会立即 为整个 VRAM 范围分配所有 struct page,而非按需分配。对于 16GB VRAM,这意味着在设备初始化阶段就消耗 256MB 系统内存,即使 VRAM 可能只使用了很小一部分。
分配调用链:
devm_memremap_pages(dev, pgmap) // 传入整个 VRAM 大小
└─ memremap_pages(pgmap, nid)
└─ pagemap_range(pgmap, params, range_id, nid)
├─ add_pages(nid, start_pfn, nr_pages, params) // PRIVATE 路径
│ └─ for 循环,按 section 粒度(通常 128MB)迭代
│ └─ sparse_add_section(nid, pfn, nr_pages, altmap, pgmap)
│ └─ section_activate(nid, pfn, nr_pages, altmap, pgmap)
│ └─ populate_section_memmap(pfn, nr_pages, ...)
│ └─ vmemmap_populate()
│ // 为每个 section 的 struct page 数组
│ // 分配真实的物理内存页面
└─ memmap_init_zone_device(zone, start_pfn, nr_pages, pgmap)
└─ for (pfn = start_pfn; pfn < end_pfn; pfn++)
__init_zone_device_page(page, pfn, ...)
// 逐个初始化:设置 pgmap、zone、refcount=0
整个流程没有任何 lazy/on-demand 机制。
13.2 根本原因:pfn_to_page() 必须无条件有效
Linux 使用 SPARSEMEM_VMEMMAP 内存模型(x86-64 默认),pfn_to_page() 是一个简单的数组索引操作:
c
// arch/x86/include/asm/page.h 等效逻辑
#define pfn_to_page(pfn) (vmemmap + (pfn))
// vmemmap 是一个全局的 struct page 虚拟地址数组基址
这意味着:
- 对于注册范围内的任何 PFN ,
pfn_to_page(pfn)都会直接计算出一个虚拟地址并访问 - 如果该虚拟地址背后没有分配真实的物理页面(即没有 vmemmap 映射),CPU 会触发 page fault → kernel panic
pfn_to_page()没有有效性检查,它假定 vmemmap 中的每个条目都已初始化
内核中有大量代码路径调用 pfn_to_page(),且位于 hot path 上。加入有效性检查会严重影响性能。
13.3 谁在调用 pfn_to_page()?
在 ZONE_DEVICE 的使用场景中,以下关键路径依赖 pfn_to_page() 对所有注册 PFN 无条件有效:
| 调用场景 | 代码路径 | 说明 |
|---|---|---|
| 迁移分配 | svm_migrate_get_vram_page(pfn) → pfn_to_page(pfn) |
驱动用 VRAM 偏移换算 PFN,获取 struct page |
| CPU fault 回迁 | pfn_swap_entry_to_page(entry) → pfn_to_page(pfn) |
从 swap entry 中解码 PFN,找到设备页面 |
| pgmap 查找 | get_dev_pagemap(pfn) → xa_load(&pgmap_array, pfn) |
虽不直接用 pfn_to_page,但后续操作需要 |
| HMM range fault | hmm_range_fault() → 识别 device_private 页面 |
遍历页表时遇到设备页面需要获取 struct page |
| 迁移收集 | migrate_vma_collect() → pfn_swap_entry_to_page() |
遍历页表收集待迁移的设备页面 |
| 引用计数 | put_page(page) → free_zone_device_folio() |
释放时需要访问 page->pgmap |
13.4 为什么不能按需分配?
假设场景:只在迁移发生时才为对应 VRAM 页面分配 struct page。
问题 1:swap entry 解码会崩溃
当页面从 RAM 迁移到 VRAM 后,CPU 页表中写入 device_private swap entry,编码了设备页面的 PFN。如果后续 CPU 访问该地址:
c
// mm/memory.c: do_swap_page()
entry = pte_to_swp_entry(vmf->orig_pte);
if (is_device_private_entry(entry)) {
page = pfn_swap_entry_to_page(entry); // → pfn_to_page(pfn)
// 如果 struct page 未分配 → kernel panic
}
swap entry 只存了 PFN,没有任何元数据来判断 struct page 是否已分配。
问题 2:需要修改内核 hot path
要支持 lazy allocation,pfn_to_page() 必须变成:
c
// 假想的 lazy 版本(不存在)
struct page *pfn_to_page_lazy(unsigned long pfn) {
struct page *page = vmemmap + pfn;
if (!vmemmap_populated(pfn)) // 额外的检查
return NULL; // 或者触发按需分配
return page;
}
这个额外的检查会影响每一次 pfn_to_page() 调用,包括普通 RAM 的 hot path。这在性能上不可接受。
问题 3:并发安全
按需分配需要处理并发:多个 CPU 同时 fault 到同一个未分配的 PFN。这需要锁或原子操作,进一步增加开销。
13.5 社区的优化方向
社区没有尝试 lazy allocation,而是从减少每个 struct page 的实际内存占用入手:
方案 1:vmemmap 去重(Compound Devmap,2022 年合入)
提交 4917f55b4ef9 ("mm/sparse-vmemmap: improve memory savings for compound devmaps",Joao Martins,Oracle):
利用 compound page 中 tail pages 的 struct page 内容完全相同的特点,让多个 vmemmap 条目指向同一个物理页面:
传统方式(base page, vmemmap_shift=0):
512 个 struct page → 各自独立的 vmemmap 物理页面 → 8 页 = 32KB
Compound devmap(vmemmap_shift > 0, 如 2MB):
1 个 head page 的 vmemmap 页面(独立)
1 个 tail page 的 vmemmap 页面(模板)
剩余 6 个 vmemmap 条目 → 全部指向模板页面
实际开销:2 页 = 8KB,节省 75%
对于 1GB compound page:4096 个 vmemmap 页面 → 2 个,节省 99.95%。
方案 2:altmap(用设备内存存 vmemmap)
对于 CPU 可访问的设备内存(PMEM、MEMORY_DEVICE_COHERENT),可以用设备内存本身来存储 struct page 数组,完全不消耗系统 RAM。
限制 :不适用于 MEMORY_DEVICE_PRIVATE(GPU VRAM),因为 CPU 无法直接读写这些 struct page。
方案 3:Compound zone device pages(2025 年合入)
提交 82ba975e4c43 ("mm: allow compound zone device pages",Alistair Popple,NVIDIA):
解决了 page->compound_head 与 page->pgmap 共用 union 字段的冲突,将 pgmap 移入 folio。为 GPU 驱动使用 compound page 扫清了障碍。
方案 4:memdesc 抽象(2025 年进行中)
Matthew Wilcox 的 memdesc 重构(53fbef56e07d 等),将 struct page 抽象为更通用的内存描述符。长远来看可能允许不同内存类型使用不同大小的描述符。
13.6 当前 GPU 驱动的现状
尽管内核已支持 compound devmap 优化,当前的 GPU 驱动(KFD 和 Xe)都未采用:
c
// KFD: kgd2kfd_init_zone_device()
pgmap->flags = 0;
// vmemmap_shift 默认为 0 → 使用 base page → 无去重优化
// Xe: xe_devm_add()
vr->pagemap.type = MEMORY_DEVICE_PRIVATE;
// 同样未设置 vmemmap_shift → 无去重优化
未采用的原因:compound page 要求以大页粒度(如 2MB)整体管理迁移,而当前 GPU SVM 迁移以 4KB 基页为粒度。要使用 compound devmap 优化,需要迁移框架也支持大页粒度操作。
13.7 开销对照表
| VRAM | 无优化(当前) | compound 2MB | compound 1GB |
|---|---|---|---|
| 4 GB | 64 MB | ~16 MB | ~0.06 MB |
| 8 GB | 128 MB | ~32 MB | ~0.12 MB |
| 16 GB | 256 MB | ~64 MB | ~0.25 MB |
| 32 GB | 512 MB | ~128 MB | ~0.5 MB |
| 64 GB | 1024 MB | ~256 MB | ~1 MB |
13.8 总结
VRAM 的 struct page 必须提前全部分配,这是 Linux SPARSEMEM_VMEMMAP 内存模型的根本约束 :pfn_to_page() 是无条件的数组索引,不允许任何 PFN "空洞"。社区的策略不是改变这个约束,而是通过 vmemmap 去重(compound devmap)和 altmap 等手段减少实际的物理内存消耗。对于 GPU 驱动而言,采用 compound devmap 是最有前景的优化方向,但需要迁移框架配合支持大页粒度操作。