跳到主要内容

Linux 7.3 的 VRAM Overcommit:显存不够用时,内核级兜底怎么做

· 阅读需 9 分钟

2026 年 8 月,两条看起来不相关的新闻放在一起读,信息量很大:

  1. Tom's Hardware 报道 DRAM 价格 12 个月涨了约 500%,HBM/GDDR 的供应紧张让"显存"成了整个 AI 产业链最贵的消耗品;
  2. 一位 Linux 图形栈开发者(pixelcluster.dev)多年打磨的 VRAM 管理补丁合入主线、随 Linux 7.3 发布:当 GPU 显存被"超卖"(overcommit)时,系统不再随机崩溃,而是尽量优雅地降速。

第一条讲的是"显存为什么不够用",第二条讲的是"不够用了之后系统该怎么办"。第二条对 AI 推理工程师尤其有参考价值——因为 vLLM 的 CPU offload、llama.cpp 的 mmap、推理服务的显存超卖,本质上是同一个问题:物理显存放不下,被迫把数据换到 PCIe 那头的内存里,系统怎么把损失降到最小。

一、为什么"显存不够"会导致崩溃,而不是变慢​

理论上,显存超卖只该带来性能损失,不该带来稳定性问题:GPU 驱动允许应用申请超过物理显存的内存,内核决定哪些数据放进显存、哪些放系统内存。性能损失的来源也很直观——PCIe 带宽是硬瓶颈:

  • PCIe 4.0 x16 的理论带宽不到 32 GiB/s,约 32.2 MiB/ms;
  • 以 30 FPS(每帧 33.3ms)为例,GPU 每帧最多能从系统内存搬回约 1 GiB 数据;
  • 一旦"被逐出的数据"每帧需要访问超过 1 GiB,30 FPS 在物理上就不可能达成。

但真正的崩溃点藏在更深处:radv/amdgpu: Not enough memory for command submission。

这个报错不是"分配内存失败"——命令缓冲早就分配成功了。它发生在提交(submission)阶段:内核在让 GPU 执行命令前,必须保证所有可能被引用的内存可访问(bindless API 下,任何已分配的内存都可能被引用,所以驱动假设"全部都要可访问")。当显存吃紧时,部分分配被换到了系统内存,提交前要把它们搬回显存;搬回需要先逐出别人,逐出需要加锁……于是:

Submit A 锁住了内存 X(要逐出),需要锁内存 Y(Submit B 已锁)
Submit B 锁住了内存 Y,需要锁内存 X(Submit A 已锁)
→ 教科书级 ABBA 死锁

内核有一套成熟的死锁解决机制:wound-abort。锁操作被组织成"事务",检测到死锁就把其中一个事务标记为"受伤",下次加锁时返回 -EDEADLCK,事务整体回滚重试。这套机制在 DRM 子系统里被封装成 drm_exec helper——但共享内存管理层 TTM 一直没有接入 drm_exec,代码里甚至留了注释说"-EDEADLCK 会导致逐出失败"。

结果就是:显存压力大时,逐出撞上死锁,内核直接拒绝提交命令——应用瞬间崩溃。作者把 2024 年就提交过但没合入的 drm_exec 补丁集捡起来重做,修掉了遗留 bug(一周的"游戏随机卡死"调试),这次随 7.3 合入。修复后,逐出遇到死锁会 wound-abort-retry,而不是直接失败。

二、不崩了,但性能去哪了:eviction 乒乓​

修好崩溃问题后,作者用 gpuvis(基于内核 tracepoint 的 GPU 时间线工具)观察"显存不够"时的真实行为,发现大部分时间根本不在执行 GPU 任务:

gfx_0.0.0(实际执行): ▂▂▂▂
sdma0(内存搬运) : ████████████████████████

时间几乎全花在搬内存上。原因是"逐出乒乓":两个竞争显存的进程(典型场景是 gamescope 合成器 + 游戏本体)轮流把同一块内存踢出去、再拉回来——每次提交都搬一次,搬来搬去比不搬还慢。

最极端的案例是显示扫描输出(scanout)缓冲:显示硬件不走 GPU 虚拟内存,直接访问物理连续的内存。当显存碎片化时,要为约 32 MiB 的 scanout 图像找一块连续空间,LRU 逐出算法可能把多达 4 GiB 的显存全部踢掉(逐出循环只按 LRU 顺序逐个试,完全不考虑物理连续性),光搬走就是 130ms 起。

作者的缓解方案是一组启发式(hard/soft throttle):

  1. 检测到应用内存被逐出 → 进入硬限流:几毫秒内完全不把内存搬回显存;
  2. 之后进入软限流:可以搬回自己的内存,但不主动逐出别人的;
  3. 稳定一段时间后解除限制,恢复正常。

配合之前引入的 dmem cgroup 显存保护(受保护的内存不允许被随意逐出),效果实测:8 GiB 显卡跑请求 9 GiB 的游戏(1 GiB 超卖)平均帧时间 19.6ms,基本可玩;请求 10 GiB(2 GiB 超卖)平均 29.8ms,但方差明显变大、频繁越过 33.3ms 的 30 FPS 线。

三、给应用留后门:优先级提示​

作者最后指出:访问模式只有应用自己知道。驱动可以做的都做了,剩下的要靠应用告诉驱动"哪块内存最不该被踢"。

Vulkan 的 VK_EXT_pageable_device_local_memory 扩展(vkSetDeviceMemoryPriorityEXT)正是干这个的。内核实现起来比想象中简单:TTM 本来就维护一个 LRU 逐出列表,只需把同一应用内的分配按优先级重排(优先级只在同一 context 内可比,跨应用没有统一标尺),逐出时先踢低优先级的。

生态侧的现状是:原生 Vulkan 游戏基本没人用这个扩展,但 vkd3d-proton 已经把 D3D12 的 SetResidencyPriority / MakeResident / Evict 翻译成对应的 Vulkan 优先级调用,所以大量 D3D12 游戏其实已经带上了 hint。作者估计正确执行优先级后,部分场景性能提升最高约 30%(前提是之前"逐出靠运气",这个数字波动很大)。

四、对 AI 推理工程的映射:同一个问题的三份答案​

这套内核工作的每个环节,在 LLM 推理栈里都有对应物,值得对照着抄:

Linux 内核(游戏)LLM 推理(AI)共同本质
eviction 到系统内存vLLM --cpu-offload-gb、llama.cpp mmap、swap放不下就换出,PCIe 是硬瓶颈
TTM 逐出死锁 → wound-abort显存不足时 CUDA OOM / 进程被杀"放不下"必须是性能问题,不能是稳定性问题
scanout 必须物理连续KV cache 分页(PagedAttention)物理约束决定分配策略,碎片化是隐形杀手
dmem cgroup 保护多租户推理的显存配额与优先级"受保护的内存不可被抢"的租户隔离语义
VK_EXT_pageable_device_local_memory 优先级CUDA 内存池优先级 / 自定义 eviction 策略应用给底层 hint,按语义而非 LRU 逐出

几点具体的借鉴:

  1. PCIe 带宽预算要先算账。PCIe 4.0 x16 约 32 GiB/s 的约束同样适用于推理:如果某条请求的 offload 数据量除以生成时长超过可用带宽,延迟必然崩。vLLM 的 CPU offload 只适合"换出的数据极少被访问"的场景——这正好对应文章里"cache 友好 + 低频访问"的内存最适合被逐出的结论。KV cache 是理想的先换出对象(冷前缀命中率低),权重反而不适合(每 token 都访问)。
  2. "放不下"必须是性能问题,不能是稳定性问题。内核这次修的是"提交阶段随机崩溃",推理侧对应的是:显存压力下 CUDA OOM 杀进程、vLLM 的 CUDA out of memory 拖垮整个实例。工程上应该做的是提前逐出 + 优雅降级(降低并发、减少 KV 缓存上限),而不是等到分配失败。
  3. 优先级 hint 是性价比最高的合作机制。给内核/驱动"哪些内存最不该被踢"的 hint,比任何通用启发式都有效。推理框架如果做显存超卖,应该把 KV cache 池、权重、激活、中间 buffer 分优先级管理,而不是一视同仁地 LRU。

五、批判性视角​

两点必须说清:其一,这批补丁目前完整形态主要在 SteamOS 上发布(主线合入的是其中一部分,完整启发式 + 优先级支持仍在 upstream 路上,作者提供了实验分支,明确提示"未经过同等强度的测试");其二,文章的所有实测数据来自游戏负载(帧时间、scanout),AI 推理负载的访问模式完全不同(权重全量高频访问、KV cache 顺序扫描),内核层效果需要独立验证,不能直接套用"超卖 1 GiB 可玩"的结论。

但方向是确定的:显存是新的稀缺资源,而"超卖 + 优雅降级"正在从"不可行"变成"工程上值得做"。在 HBM 价格持续高企的背景下,这条内核路线和 vLLM 的 offload/swap 路线会越来越像同一件事。

相关阅读​