news 2026/7/2 16:58:27

nvidia-smi 显示 8GB 空闲,为什么 PyTorch 报 CUDA out of memory?——CUDA 缓存分配器底层原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
nvidia-smi 显示 8GB 空闲,为什么 PyTorch 报 CUDA out of memory?——CUDA 缓存分配器底层原理

nvidia-smi 显示 8GB 空闲,为什么 PyTorch 报 CUDA out of memory?——PyTorch CUDA 缓存分配器底层原理

2026 年 6 月,PyTorch 官方发布了一篇 devlog:《When does fragmentation occur in the CUDA caching allocator?》。这篇文章解释了每个 AI 开发者都遇到但几乎没人真正理解的问题——“明明 nvidia-smi 显示还有 8GB 空闲显存,为什么 PyTorch 还是报 OOM?”


一、nvidia-smi 和 PyTorch 看到的不是同一个"显存"

打开终端,跑两行:

$ nvidia-smi|0NVIDIA RTX4090On|00000000:01:00.0 Off|Off||30% 45C P2 72W / 450W|15360MiB / 24564MiB|62% Default|

24564 MiB是 GPU 物理显存。15360 MiB是 nvidia-smi 报告的"已使用"。

但 PyTorch 告诉你:

>>>torch.cuda.memory_allocated()/1024**38.2# GB>>>torch.cuda.memory_reserved()/1024**311.5# GB

allocated是 PyTorch 实际在用的。reserved是 PyTorch 从 CUDA 驱动"预支"但可能空闲的。

这三个数字的关系:

指标含义工具
GPU 物理总量硬件固定值nvidia-smi
PyTorch reserved从驱动申请的段(segment),不释放torch.cuda.memory_reserved()
PyTorch allocated段内实际分配给张量的块torch.cuda.memory_allocated()

关键矛盾:PyTorch reserved 的内存不还给驱动。即使 Python 删了所有张量、调了gc.collect(),nvidia-smi 仍然显示"已使用"。因为 PyTorch 的缓存分配器缓存了这些段——它在等下次分配时复用,而不是还给 CUDA 驱动。

这就是为什么你明明del了一个 20GB 的模型,nvidia-smi 还是显示 20GB 被占用——PyTorch 把它藏在缓存里了。


二、段(Segment)和块(Block):分配器的两层结构

PyTorch CUDA 缓存分配器的核心数据结构:

cudaMalloc → Segment(大块连续显存) ├── Block A(已分配,active=true) ├── Block B(空闲,active=false) └── Block C(已分配,active=true)
  • 段(Segment):通过cudaMalloccuMemMap从 CUDA 驱动获取的连续显存区域。段之间不连续
  • 块(Block):从一个段上切分出来的子区域,服务于具体的张量分配。
  • 分裂(Splitting):当一个空闲块比请求大时,前面部分分配出去,剩余部分作为新的空闲块。
  • 合并(Merging):两个相邻的空闲块可以合并为一个更大的空闲块。

关键规则:只有同一个段内的相邻空闲块才能合并。不同段之间的块永远不能合并。

这是碎片化问题的根源。


三、碎片化:为什么"有空闲"但"分配不了"

看一个例子。8 个 16 MiB 的张量,释放后想分配 4 个 32 MiB 的张量:

importtorch MiB=1024*1024# 分配 8 个 16 MiB 张量small=[torch.empty(16*MiB,dtype=torch.uint8,device='cuda')for_inrange(8)]# 此时:8 个独立的 16 MiB 段,共 128 MiB reserved# 释放全部small.clear()# 此时:8 个段各有 1 个 16 MiB 空闲块,但 GPU 仍占 128 MiB reserved# 尝试分配 4 个 32 MiBlarge=[torch.empty(32*MiB,dtype=torch.uint8,device='cuda')for_inrange(4)]# 💣 CUDA OOM!

发生了什么

  1. 8 次cudaMalloc创建了 8 个独立的段,每个 16 MiB
  2. 释放后,8 个段各有 1 个 16 MiB 空闲块——但它们分属不同段,无法合并
  3. 32 MiB 的请求在任何一个段里都找不到 ≥ 32 MiB 的连续空闲块
  4. 分配器调用新的cudaMalloc分配 4 个新的 32 MiB 段
  5. 总共需要 128 MiB(旧的 8 个 16 MiB 段)+ 128 MiB(新的 4 个 32 MiB 段)=256 MiB reserved

但如果你反过来分配——先分配大的,再分配小的

large=[torch.empty(32*MiB,dtype=torch.uint8,device='cuda')for_inrange(4)]# 4 个 32 MiB 段 → 128 MiB reservedlarge.clear()# 4 个 32 MiB 空闲段small=[torch.empty(16*MiB,dtype=torch.uint8,device='cuda')for_inrange(8)]# ✅ 从已有的 32 MiB 段上分裂出 16 MiB 块!无需新的 cudaMalloc

这就是碎片化的本质:分配顺序决定了显存利用率。


四、expandable_segments:一个虚拟大段解决碎片化

PyTorch 2.x 引入了expandable_segments。不再为每个cudaMalloc创建独立段,而是使用cuMemMap创建一个虚拟地址空间

cuMemMap → ExpandableSegment(虚拟 1TB 连续地址空间) ├── Block A(16 MiB,物理显存已提交) ├── Block B(32 MiB,物理显存已提交) ├── Block C(空闲,虚拟地址已预留) └── ...

关键:所有 Block 都在同一个虚拟段内——相邻空闲块可以合并

同一个"先小后大"的场景,用expandable_segments=True

# 设置环境变量后重启# export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True# 先 8 个 16 MiB,释放,再 4 个 32 MiBsmall=[torch.empty(16*MiB,dtype=torch.uint8,device='cuda')for_inrange(8)]small.clear()large=[torch.empty(32*MiB,dtype=torch.uint8,device='cuda')for_inrange(4)]# ✅ 不崩!因为释放的 16 MiB 块在同一个虚拟段内,相邻的已经合并成大块了

但这不免费cuMemMap的虚拟地址管理有开销。PyTorch 官方建议 CUDA Graph 场景用expandable_segments:True,普通推理用默认值。


五、max_split_size_mb:你一直在用但可能不理解

CSDN 上大量文章教你设:

exportPYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128

但它到底做了什么?在分配器的maybe_split_block函数中:

Blockmaybe_split_block(Pool pool,size_t size,Block block){remaining=block.size-size;should_split=(size<1MB&&remaining>512)||(size>=1MB&&remaining>1MB);// max_split_size_mb 控制的是这里的逻辑if(!should_split)returnblock;block,rest=split(block,size);pool.add(rest);returnblock;}

max_split_size_mb设的是"允许分裂的最大剩余块大小"。默认没有上限。当你设为 128 时:如果分裂后剩余块 > 128 MiB,不允许分裂——整个大块直接分配给请求。

为什么这能缓解碎片化?因为分裂产生的小块是最难合并的碎片源。限制分裂 = 减少小碎片的产生。

但这也会浪费显存——一个 500 MiB 的块分配给 100 MiB 的请求时,如果max_split_size_mb=128,剩余 400 MiB 不能分裂出来给别人用。


六、CUDA Graph 与分配器的致命交互

CUDA Graph 捕获期间,PyTorch 分配器会记录所有 tensor 的内存地址。回放时,图必须使用相同的地址——这意味着捕获期间的显存分配不能被释放。

这就是为什么你在 vLLM 中看到:

AssertionError: Workspace is locked but allocation requires 0.76 MB. Workspace growth is not allowed after locking.

CUDA Graph 捕获完成后,分配器锁定了 workspace。任何新的分配请求——即使是 0.76 MB——都会触发断言失败。

expandable_segments在这里有帮助:虚拟地址空间预留了位置,物理显存可以按需提交。但这是两刃剑——物理显存不够时仍然会 OOM。


七、四类 OOM 的分配器级诊断

下次看到 CUDA OOM,先判断是哪一类:

OOM 类型allocatedvsreservednvidia-smi根因
真实 OOMallocated ≈ GPU 总量接近 100%模型太大或 batch 太大
碎片化 OOMallocated ≪ reserved ≪ GPU< 100% 但报 OOM段间碎片化,空闲块不连续
CUDA Graph OOMreserved ≈ GPU~95%Graph workspace 锁定时新分配
缓存 OOMallocated 正常,reserved 暴涨忽高忽低大量小块分配产生碎片

诊断命令

# 看 reserved vs allocated 缺口print(f"allocated:{torch.cuda.memory_allocated()/1024**3:.1f}GB")print(f"reserved:{torch.cuda.memory_reserved()/1024**3:.1f}GB")print(f"gap:{(torch.cuda.memory_reserved()-torch.cuda.memory_allocated())/1024**3:.1f}GB")# 看段和块的分布print(torch.cuda.memory_summary())# 看碎片化程度snap=torch.cuda.memory_snapshot()forseginsnap:free_blocks=sum(1forbinseg['blocks']ifb['state']=='free')total_blocks=len(seg['blocks'])print(f"seg{seg['total_size']//1024**2}MiB:{free_blocks}/{total_blocks}free blocks")

八、总结

PyTorch CUDA 分配器的核心矛盾:缓存策略(不还显存给驱动)提升性能,但制造碎片化假象。

环境变量作用何时用
expandable_segments:True虚拟大段,消除段间碎片CUDA Graph + vLLM/SGLang
max_split_size_mb:128限制分裂,减少小块碎片碎片化 OOM
roundup_power2_divisions:4减少对齐浪费大量不规则 size 的推理

下次你看到"CUDA out of memory, 11 GiB free"时,你知道那不是显存不够——是分配器的段无法合并了。


本文参考了 PyTorch DevLog (2026-06-01)、Zach DeVito’s Blog (2022-08-04) 以及 PyTorch 源码CUDACachingAllocator.cpp

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 16:58:19

大模型原生能力崛起:中间层技术归零趋势与架构重构指南

1. 项目概述&#xff1a;这不是一次普通更新&#xff0c;而是一次架构级“静默坍缩”“Anthropic Just Shipped the Layer That’s Already Going to Zero”——这个标题不是修辞&#xff0c;不是营销话术&#xff0c;更不是对某款新模型的夸张吹捧。它直指一个正在发生的、肉眼…

作者头像 李华
网站建设 2026/7/2 16:56:09

Mac散热控制技术挑战与smcFanControl智能解决方案

Mac散热控制技术挑战与smcFanControl智能解决方案 【免费下载链接】smcFanControl Control the fans of every Intel Mac to make it run cooler 项目地址: https://gitcode.com/gh_mirrors/smc/smcFanControl Intel Mac用户经常面临设备过热和风扇噪音问题&#xff0c;…

作者头像 李华
网站建设 2026/7/2 16:55:33

工业复杂工况下智能配电改造方案:宽温、抗谐波、离线自持技术解析

摘要&#xff1a;全国各类工业园区、工矿场站、光伏储能场景普遍存在环境温差大、非线性负载谐波高、厂区钢结构遮挡导致网络不稳定、老旧配电无预判能力等问题。传统机械式断路器仅支持过载、短路被动保护&#xff0c;无法适配现代工业数字化运维、预测性维护、能耗精细化管理…

作者头像 李华
网站建设 2026/7/2 16:54:45

革命性LOL换肤方案:R3nzSkin国服特供版深度解析

革命性LOL换肤方案&#xff1a;R3nzSkin国服特供版深度解析 【免费下载链接】R3nzSkin-For-China-Server Skin changer for League of Legends (LOL) 项目地址: https://gitcode.com/gh_mirrors/r3/R3nzSkin-For-China-Server 还在为英雄联盟皮肤的价格而犹豫不决&#…

作者头像 李华
网站建设 2026/7/2 16:54:28

从电视盒子到服务器:Armbian系统改造终极指南

从电视盒子到服务器&#xff1a;Armbian系统改造终极指南 【免费下载链接】amlogic-s9xxx-armbian Supports running Armbian on Amlogic, Allwinner, and Rockchip devices. Support a311d, s922x, s905x3, s905x2, s912, s905d, s905x, s905w, s905, s905l, rk3588, rk3568, …

作者头像 李华
网站建设 2026/7/2 16:54:10

Encoder-Decoder数据流契约:从Tensor Shape看清NLP模型接口本质

1. 这不是教科书里的对称结构&#xff1a;为什么搞懂编码器和解码器的区别&#xff0c;比背公式重要十倍“Encoder 和 Decoder 的区别”——这个标题在深度学习入门资料里出现频率高得离谱&#xff0c;但绝大多数人看完之后&#xff0c;脑子里留下的只有“左边压缩、右边还原”…

作者头像 李华