TL;DR

GraphZero 用 C++ 和 mmap 绕过 PyTorch Geometric 的内存限制,证明了被忽视几十年的 Unix 技术正在 AI 时代复兴。当行业默认”内存不够就加内存”时,真正的工程师选择重新思考问题的边界。


📋 本文结构

  1. OOM:ML 工程师的宿命?
  2. mmap 是什么?为什么被遗忘了?
  3. GraphZero 的工程智慧
  4. 技术深潜:mmap 在 AI 场景的应用
  5. 复古而优雅的工程哲学
  6. 什么时候该用,什么时候不该用

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 不同于普通神经网络——它需要存储整张图的邻接关系,内存消耗随节点数呈平方级增长。

行业的默认解法是什么?

  1. 加内存 — 买更大的 GPU(A100 40GB → H100 80GB)
  2. 买更多机器 — 分布式训练,把图切分到多个节点
  3. 妥协模型 — 减小 batch size,降低模型复杂度

这些方案都有效,但都有一个共同点:用资源换解决问题


mmap 是什么?为什么被遗忘了?

Unix 的古老智慧

mmap(memory-mapped files)是 Unix 系统调用,最早出现在 1970 年代的 Research Unix。它的核心思想很简单:

让文件看起来像内存,让内存看起来像文件。

传统 I/O 流程:

磁盘文件 → 读入内存 → 处理 → 写回磁盘

mmap 流程:

磁盘文件 → 映射到虚拟内存空间 → 直接访问(按需加载)

关键区别:mmap 不一次性把整个文件读入内存,而是按需将磁盘页加载到内存。操作系统负责换页,开发者像访问普通内存一样访问文件。

为什么被遗忘了?

  1. SSD 之前,磁盘太慢
    • HDD 随机读取 1-10 ms
    • mmap 的页错误(page fault)代价太高
    • 一次性读入内存反而更快
  2. 框架的”易用性优先”哲学
    • PyTorch、TensorFlow 默认把所有数据加载到内存/显存
    • mmap 需要手动管理,API 不够友好
    • “开发者体验”战胜了”资源效率”
  3. 硬件摩尔定律的诅咒
    • 内存便宜了 → 懒得优化
    • 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 不是银弹。但在正确的场景下,它是一把被忽视的利器。

最好的优化往往不是买更多硬件,而是重新思考问题边界。


参考与延伸阅读


本文灵感源自 2026-03-16 Reddit Daily Digest 中关于 GraphZero 的讨论。

发布于 postcodeengineering.com