mmap 复兴:当'内存不够'不再是唯一答案
TL;DR
GraphZero 用 C++ 和 mmap 绕过 PyTorch Geometric 的内存限制,证明了被忽视几十年的 Unix 技术正在 AI 时代复兴。当行业默认”内存不够就加内存”时,真正的工程师选择重新思考问题的边界。
📋 本文结构
OOM:ML 工程师的宿命?
如果你用 PyTorch Geometric (PyG) 训练过图神经网络,一定见过这个画面:
RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB
( GPU 0 has 10.00 GiB total capacity)
或者更糟的:
MemoryError: Unable to allocate array with shape (17448000000,)
and data type float32
这是 Graph Neural Network (GNN) 开发者的日常噩梦。GNN 不同于普通神经网络——它需要存储整张图的邻接关系,内存消耗随节点数呈平方级增长。
行业的默认解法是什么?
- 加内存 — 买更大的 GPU(A100 40GB → H100 80GB)
- 买更多机器 — 分布式训练,把图切分到多个节点
- 妥协模型 — 减小 batch size,降低模型复杂度
这些方案都有效,但都有一个共同点:用资源换解决问题。
mmap 是什么?为什么被遗忘了?
Unix 的古老智慧
mmap(memory-mapped files)是 Unix 系统调用,最早出现在 1970 年代的 Research Unix。它的核心思想很简单:
让文件看起来像内存,让内存看起来像文件。
传统 I/O 流程:
磁盘文件 → 读入内存 → 处理 → 写回磁盘
mmap 流程:
磁盘文件 → 映射到虚拟内存空间 → 直接访问(按需加载)
关键区别:mmap 不一次性把整个文件读入内存,而是按需将磁盘页加载到内存。操作系统负责换页,开发者像访问普通内存一样访问文件。
为什么被遗忘了?
- SSD 之前,磁盘太慢
- HDD 随机读取 1-10 ms
- mmap 的页错误(page fault)代价太高
- 一次性读入内存反而更快
- 框架的”易用性优先”哲学
- PyTorch、TensorFlow 默认把所有数据加载到内存/显存
- mmap 需要手动管理,API 不够友好
- “开发者体验”战胜了”资源效率”
- 硬件摩尔定律的诅咒
- 内存便宜了 → 懒得优化
- GPU 显存大了 → 更懒得优化
- 问题被硬件进步掩盖了
SSD 改变了一切
现代 NVMe SSD 的随机读取延迟已经降到 10-100 微秒——比 HDD 快 100-1000 倍。
这意味着:
- mmap 的页错误代价大幅降低
- SSD 带宽足以支撑许多 AI 工作负载
- “内存不够加内存”不再是唯一答案
GraphZero 的工程智慧
问题背景
GraphZero 的开发者遇到了一个典型问题:
- 图数据太大,无法装入内存
- PyG 的 DataLoader 试图一次性加载整张图
- 即使买了 64GB 内存的机器,OOM 依然发生
解决方案
他们没有选择:
- ❌ 买更大内存的机器
- ❌ 改用小 batch 的采样策略(损失精度)
- ❌ 用分布式训练(复杂度爆炸)
而是选择了:
- ✅ 用 C++ 和 mmap 直接从 SSD 加载数据
- ✅ 绕过 PyG 的内存分配,自己管理数据流
- ✅ 让操作系统处理 paging,开发者专注于算法
核心实现思路
// 简化示意
int fd = open("graph_data.bin", O_RDONLY);
void* data = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
// 现在 data 指向虚拟内存空间
// 访问时,OS 自动从 SSD 加载所需页面
for (int i = 0; i < num_nodes; i++) {
// 第一次访问会触发 page fault,从 SSD 加载
// 后续访问直接从内存读取
process_node(data + offset[i]);
}
性能权衡
| 指标 | 全内存方案 | mmap 方案 |
|---|---|---|
| 首次访问延迟 | 低(已加载) | 高(page fault) |
| 内存占用 | 高(全图常驻) | 低(按需加载) |
| 吞吐量 | 极高 | 中等(受 SSD 限制) |
| 可处理图规模 | 受限于内存 | 受限于磁盘 |
| 复杂度 | 低(框架默认) | 高(需自定义) |
GraphZero 的选择逻辑:
“慢但可用 > 快但崩溃”
技术深潜:mmap 在 AI 场景的应用
场景 1:大模型推理的 KV Cache
现代 LLM 的 KV Cache 可以占用数十 GB 显存。某些场景下:
- 长上下文需要存储大量历史状态
- 但访问模式是顺序/局部的
- mmap 可以把不活跃的 KV Cache 换出到 SSD
场景 2:大规模数据集训练
ImageNet、LAION-5B 级别的数据集:
- 无法全部装入内存
- 传统方案:复杂的数据加载 pipeline
- mmap 方案:直接映射文件,让 OS 优化缓存
场景 3:图神经网络(GraphZero 的场景)
GNN 的特殊性:
- 邻接矩阵稀疏但巨大
- 消息传递是局部的(只访问邻居)
- 完美的 mmap 场景:按需加载局部子图
场景 4:Embedding 查找表
推荐系统的 embedding 表:
- 几亿用户 × 几百维 = TB 级
- 访问模式极度稀疏(一次只查几个)
- mmap 让 TB 级 lookup 在 GB 级内存机器上可行
复古而优雅的工程哲学
GraphZero 的做法让我想起 Unix 哲学的核心原则:
1. 做一件事,并做好
mmap 只做一件事:把文件映射到内存。不试图解决所有问题。
对比现代框架的”一站式”哲学,mmap 是克制的艺术。
2. 利用操作系统的力量
GraphZero 没有自己实现 paging 算法,而是:
- 信任 Linux 的页面置换策略
- 利用 OS 的文件系统缓存
- 站在巨人的肩膀上
3. 延迟加载(Lazy Loading)
不预分配,不预加载,按需获取。
这与现代软件开发的”提前优化”倾向形成对比。
4. 资源约束催生创新
历史案例:
- Doom (1993):用二进制空间分割 (BSP) 在 4MB 内存运行 3D 游戏
- SQLite:把整个数据库映射到内存,无需服务器
- Redis:内存数据库,但用 fork + COW 做持久化
GraphZero 继承了这一传统:限制不是障碍,是创新的催化剂。
什么时候该用,什么时候不该用
✅ 适合 mmap 的场景
| 场景 | 原因 |
|---|---|
| 数据 > 内存,但访问局部 | 只需要一小部分数据在内存 |
| 访问模式可预测 | OS 的预读机制能有效工作 |
| 对延迟不敏感 | 可以接受偶尔的 page fault |
| 只读或写少 | 避免复杂的 write-back 逻辑 |
| SSD 存储 | 随机读取性能足够好 |
❌ 不适合 mmap 的场景
| 场景 | 原因 |
|---|---|
| 随机访问整个数据集 | page fault 风暴,性能崩溃 |
| 对延迟极度敏感 | 实时系统无法容忍不可预测的延迟 |
| 频繁写入 | write-back 复杂性,数据一致性风险 |
| HDD 存储 | 机械硬盘的随机读取太慢 |
| 网络文件系统 | NFS 的 mmap 支持有问题 |
GraphZero 为什么适合?
- GNN 训练是批量顺序访问(遍历邻接节点)
- 图数据只读(训练时不动图结构)
- 用SSD存储数据集
- 可以容忍毫秒级的加载延迟(相对于训练时间微不足道)
结语:回归工程的本质
GraphZero 让我想起了计算机科学的一条基本真理:
“计算机科学中的所有问题都可以通过增加一个抽象层来解决,除了抽象层太多的问题。”
在 AI 基础设施疯狂堆砌抽象的今天(Kubernetes → Docker → CUDA → PyTorch → PyG → …),GraphZero 选择了一条相反的路:
回到 Unix 的基础,用最原始的系统调用解决问题。
这不是倒退,是进化。
当行业沉迷于”scale up”(买更大的机器)时,GraphZero 证明了”scale smart”(更聪明地使用资源)的价值。
mmap 不是银弹。但在正确的场景下,它是一把被忽视的利器。
最好的优化往往不是买更多硬件,而是重新思考问题边界。
参考与延伸阅读
- GraphZero GitHub Repository
- mmap(2) - Linux Manual
- The Evolution of Storage: From HDD to NVMe
- Memory Management in PyTorch
- “What Every Programmer Should Know About Memory” - Ulrich Drepper
本文灵感源自 2026-03-16 Reddit Daily Digest 中关于 GraphZero 的讨论。
💬 评论
💡 使用 GitHub 账号登录 即可参与讨论