我的服务器里跑着一张闲置的 GPU——它大部分时间什么都不干,但它的 8GB GDDR6 显存比我整台机器的 SSD 还快。今天 Hacker News 上一个 305 分的热门项目给了我灵感:c0deJedi/nbd-vram,一个把 NVIDIA 显卡的 VRAM 直接当 Linux swap 用的工具。
听起来像黑客的奇技淫巧?我也是这么想的。但读完源码和 84 条 HN 评论后,我改变主意了——这个方案背后藏着一个关于内存层次结构、PCIe 瓶颈和延迟 vs 吞吐的精彩技术故事。
一、问题从哪来:焊死的内存和闲置的显卡
作者 c0deJedi 的出发点很实在:他用的是一台混合显卡笔记本(AMD 集显负责显示输出 + NVIDIA RTX 3070 独显)。独显在日常使用中基本闲置,8GB VRAM 完全浪费,而系统内存只有 16GB——编译个大型项目就开始 swap 到 SSD。
这个问题比想象中普遍:
- 现在越来越多笔记本采用板载内存,焊死了就不能升级
- 游戏本 / AI 开发本通常配了 16-32GB VRAM,但不跑模型不玩游戏时完全浪费
- HN 评论区有人说"我的开发机有 32GB RAM + 32GB VRAM,不跑 AI 模型时显卡就在那发呆"
于是问题变成了:能不能让闲置的 VRAM 给系统内存当后备?
二、为什么不能直接把 VRAM 映射给 CPU
这是最直觉的思路——PCIe 总线把 GPU 和 CPU 连在一起,直接把 VRAM 映射到 CPU 地址空间不就行了?
作者试了,而且碰了两堵墙:
第一堵墙:NVIDIA 的消费者卡限制。nvidia_p2p_get_pages_persistent 这个 API 可以把 VRAM 页固定到 BAR1 空间让 CPU 直接访问。但 NVIDIA 在驱动层做了 SKU 限制——只有 Quadro / 数据中心卡能用,GeForce 消费卡一律返回 EINVAL。不管你的驱动多新,不管你用什么 flag,都不行。
第二堵墙:BAR1 空间本身就很小。就算绕过 P2P API 直接 ioremap_wc 映射 BAR1 物理地址,GPU 内部页表也只映射了约 16MB(刚好够显示帧缓冲区)。读其他区域全返回零——mkswap 看起来成功了,swapon 直接报错。
这两堵墙把"直接映射"的路堵死了。社区评论也印证了这一点:PCIe 本身不是缓存一致总线,如果 CPU 缓存了一个在 GPU 上的缓存行,GPU 修改了它,CPU 缓存不会自动失效。这就是为什么企业级的系统内存扩展要走 CXL(带缓存一致性协议),而不是简单的 PCIe。
三、NBD 方案:绕墙而行
作者的解法很巧妙——既然不能直接映射,那就走网络块设备(NBD)这条路:
架构:一个轻量守护进程用 CUDA 的 cuMemcpyHtoD / cuMemcpyDtoH 在 VRAM 中分配内存,然后通过 NBD 协议(走 Unix socket)把这块内存暴露为 /dev/nbdX 块设备。内核的 nbd 驱动连接后,它就变成了一个正常的 swap 设备。
数据路径:
内核 swap 子系统 → /dev/nbdX → nbd 内核驱动 → Unix socket
→ nbd-vram 守护进程 → cuMemcpyHtoD/DtoH → GPU VRAM
这个方案的好处是:零内核模块。cuMemcpyHtoD 在任何 CUDA GPU 上都能用,不需要特殊权限。内核更新、驱动更新都不需要重新编译任何东西。
安装只需三步:
git clone https://github.com/c0dejedi/nbd-vram
cd nbd-vram
sudo ./install.sh
装完后 swapon --show 就能看到 /dev/nbd0 作为 swap 分区,优先级 1500(高于 zram 和 SSD swap)。
三、基准测试:延迟赢了,吞吐输了
作者在 RTX 3070 Laptop(8GB VRAM)上做了三组基准测试,对比 NVMe cryptswap(dm-crypt, PCIe 4.0)。结果非常有趣:
顺序吞吐量:NVMe 领先
| 指标 | NVMe | VRAM (NBD) |
|---|---|---|
| 写入 | 2.7 GB/s | 1.1 GB/s |
| 读取 | 2.9 GB/s | 2.3 GB/s |
VRAM 在大块顺序传输上慢了。瓶颈在于 NBD + CUDA 的用户态往返——每个数据块都要跨 Unix socket、触发一次 cuMemcpy 调用。而 NVMe 走的是内核直接块设备路径,零拷贝。
随机 IOPS:NVMe 仍然领先
| 指标 | NVMe | VRAM (NBD) |
|---|---|---|
| 读 IOPS | 45.4k | 28.7k |
| 写 IOPS | 45.3k | 28.7k |
| 平均延迟 | 343 μs | 550 μs |
iodepth=32 时 NVMe 能真正并发 32 个请求,而 NBD+CUDA 路径被守护进程序列化,队列深度优势被削弱。
但关键来了: sporadic(偶发)访问延迟,VRAM 完胜
| 指标 | NVMe | VRAM (NBD) |
|---|---|---|
| 最小延迟 | 120 μs | 134 μs |
| 平均延迟 | 9.05 ms | 335 μs |
| 最大延迟 | 10.1 ms | 490 μs |
VRAM 的平均延迟比 NVMe 低 27 倍。
原因很简单:NVMe 的 APST(自主电源状态转换)会在请求间隔把硬盘休眠。每秒 1 次请求——这正是偶发 swap 访问的典型频率——每次都要从冷唤醒,支付约 9ms 的惩罚。VRAM 没有电源状态,每次响应都是 134-490 μs,稳定如一。
这才是关键。HN 评论区的总结非常到位:
"笔记本上的内存压力很少是 GB/s 级别的持续洪水——而是间隔数秒到达的单个 4K 页错误。每一次页错误都在等 swap 设备响应。9ms/次你能感知到,335μs/次你感觉不到。"
四、HN 社区的质疑和改进建议
这个项目的讨论质量很高,社区提出了几个有价值的批评:
"吞吐量太低了"
有人指出 RTX 3070 的 PCIe 4.0 x16 理论带宽是 32 GB/s,GDDR6 带宽是 448 GB/s,但实测顺序吞吐只有 1.1-1.3 GB/s。原因如前所述:NBD 的用户态 bounce buffer + CUDA 调用开销。社区建议迁移到 ublk 驱动,可能避免用户态 bounce buffer,还能支持多写队列并行 CUDA 拷贝。
"打游戏的时候怎么办?"
如果 VRAM 被 swap 占用了,启动游戏需要显存怎么办?作者承认这是个 TODO 项。但评论区指出 Linux 可以用 swapoff 主动释放 swap 空间——只要不在释放瞬间内存溢出就行。最佳实践是先关掉生产力应用,再 swapoff,然后开游戏。
"NVD 本身就很慢"
社区指出 NBD 驱动本身就不适合高队列深度场景,也不支持相邻访问合并。内核每 swap 一个 4K 页就要一次内核/用户态上下文切换——4 GB/s 的吞吐意味着每秒百万次切换。这确实是目前实现的最大瓶颈。
五、它适合谁?
我的判断很直接——这个方案不是万能药,但对特定场景确实有价值:
✅ 适合的场景:
- 焊死内存的笔记本,内存经常不够用
- 开发机有闲置大显存 GPU(不跑模型/不玩游戏时)
- 偶发性内存压力(编译、多标签页、开发工具)
- 想减少 SSD swap 写入,延长 NAND 寿命
❌ 不适合的场景:
- 持续高内存压力(大数据集处理、大型编译)
- 需要同时跑 GPU 密集型任务(游戏、AI 推理)
- 对延迟极度敏感的生产环境
- 没有 NVIDIA GPU 或驱动不支持的系统
六、更大的启示:内存层次的未来
这个项目让我想到一个更深层的问题:为什么 GPU 的海量显存不能成为系统内存层次的一部分?
评论区有人说得对:
"这应该是内核的内置功能。内核的工作是管理资源——GPU 显存也是一种资源,它可以用于和常规内存相同的用途。"
确实,如果 CXL 成熟后,异构内存统一管理成为现实,GPU 显存、HBM、NVDIMM 都能作为系统内存的不同层级,那 Linux 的内存管理将进入一个全新的维度。而 nbd-vram 这个"黑魔法",可能就是那个未来的一个粗糙原型。
在那之前——如果你有一张闲置的 NVIDIA 显卡,git clone 一个下午的折腾,也许就能让你的笔记本从卡顿中复活。这本身就是一种工程师的快乐。
原文链接:c0dejedi/nbd-vram
HN 讨论:305 points, 84 comments