惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Security Latest
Security Latest
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cybersecurity and Infrastructure Security Agency CISA
Cloudbric
Cloudbric
T
Threat Research - Cisco Blogs
大猫的无限游戏
大猫的无限游戏
C
CXSECURITY Database RSS Feed - CXSecurity.com
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
雷峰网
雷峰网
C
Cisco Blogs
V
Vulnerabilities – Threatpost
S
Security Archives - TechRepublic
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
J
Java Code Geeks
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Know Your Adversary
Know Your Adversary
博客园 - 叶小钗
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Privacy International News Feed
P
Palo Alto Networks Blog
博客园_首页
V
V2EX
WordPress大学
WordPress大学
Schneier on Security
Schneier on Security
月光博客
月光博客
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
The Cloudflare Blog
罗磊的独立博客
美团技术团队
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Last Week in AI
Last Week in AI
K
Kaspersky official blog
Google Online Security Blog
Google Online Security Blog
S
SegmentFault 最新的问题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog

又见苍岚

COLMAP PatchMatch Stereo 算法详解 事件驱动的状态机框架:从理论到工程实践 Git 在国内网络环境下无法 Push 的排查与修复 —— 配置 Clash 代理 分段五次多项式插值原理详解 路径插值方法深度对比研究 Claude Code 使用指南 OpenClaw 记忆管理与技能创建指南 CBS(Conflict-Based Search)算法详解 A* 算法及其变种详解 OpenClaw 配置多 Agents Windows Powershell 无法加载文件,因为在此系统上禁止运行脚本问题的解决方案 MaxClaw 安装流程 大模型 AI 名词介绍 AList 网盘聚合工具简介 Protobuf 简介与测试 Claude Code 简介以及 GLM 4.7 模型接入 Github 歌词下载工具 163MusicLyrics Python __getattr__ 懒加载 Python TypedDict 机器人仿真平台 Gazebo 安装记录 机器人仿真平台 Gazebo 简介 多机器人路径规划问题(Multi-Agent Path Finding, MAPF)简介 Python exifread 读取修改过的 jpeg 信息错误问题修复 3D 坐标系变换的理解 3D 旋转矩阵基本概念 MongoDB Compass 介绍 Python 环境管理工具 uv Flutter 开发指南 Snipaste 安装下载与黑屏问题解决方案 全局路径规划算法记录 2025 Python 版本性能测试 Flutter Hello World Flutter 安装环境配置 Ubuntu VMware 硬盘扩容后 SMBus Host controller not enabled 报错问题解决 Python NetworkX 教程 Docker GPU 报错 - Failed to initialize NVML Unknown Error 解决方案 Python matplotlib 图表绘制 cuda-toolkit 安装替代 Cuda 与 Cudnn Jinja2 Python 利用 docxtpl 和 Jinja2 生成基于模板的 Word 文档 Docker 实现 CPU 核心隔离 LoFTR 基于 Transformer 的特征提取匹配算法 OmniGlue 特征匹配 SuperGlue 使用图神经网络学习特征匹配 Ubuntu 下将 xlsx 文件按照 sheet 转换为 图片 Python 使用 SQLAlchemy Python FastAPI 教程 openwrt 软路由配置安装 Nav2 地图文件(PGM/YAML)规范标准 3D OBJ 模型转换为 glb 瓦片格式 Python 源码 Redis 数据库介绍 Ubuntu 22.04 内核自动升级导致 MongoDB 7.0.12 错误记录 ubuntu 20.04 安装 ROS Noetic ubuntu 18.04 安装 ROS Melodic VMware Workstation Pro 个人免费版下载、安装、使用指南 Hybrid A-star 路径规划 Reeds-Shepp 曲线 Dubins 曲线 Linux kvm 虚拟机网络不通的问题解决方法 Ubuntu 自动内存清理 BiliBili 缓存视频转 mp4 Python 求解线性规划 3D Gaussian Splatting 官方源码实践记录 ImageMagick 教程 Ubuntu 22.04 安装 Colmap 对数几率 odds Ubuntu nmcli 网络管理工具使用指南 SuperPoint 自监督深度学习特征点提取 SyncTV Music Tag Web 在线音乐信息整理工具 ncm 格式转 mp3 MusicBrainz 音乐元数据百科数据库 Ubuntu 网络流量监控工具 私人云音乐平台 Navidrome 入门 手眼标定 四元数(Quaternions) OHTTPS 实现免费自动 https 证书申请、更新、部署 ubuntu 22.04 安装 CloudCompare 单机 KVM 虚拟机冷迁移 Ubuntu 22.04 使用 mdadm 实现软 raid 小鱼 一键安装 ROS-humble Fluid -46- 基于 Simpletex API 构建公式识别页面 公式识别 API 简介 -- Simpletex 使用 Python web 部署库 waitress 3D Gaussian Splatting for Real-Time Radiance Field Rendering Ubuntu Swap 简介与空间扩展 Ubuntu 24.04 安装 forticlient Clash Verge 使用 MongoDB 7.0.17 集群 Docker 构建源码 Error code - 2013. Lost connection to MySQL server during query 问题解决 Python 日志记录库 loguru 使用指北 Python 实现 Web 日志查看服务 MySQL LOAD DATA LOCAL INFILE 极速数据加载 Image size exceeds limit of 89478485 pixels 解决方案 Docker 使用 NVIDIA GPU 驱动错误解决 阿里云 docker 镜像仓库 Ubuntu中没有wired connected的解决方案 MinIO 简介 subconverter 代理订阅格式转换 修复 node –openssl-legacy-provider is not allowed in NODE_OPTIONS 错误
Pytorch 内存分配与 max_split_size_mb
Yiwei Zhang · 2024-03-22 · via 又见苍岚

训练 Pytorch 模型时会遇到 CUDA Out of Memory 的问题,大部分情况下是模型本身占用显存超过硬件极限,但是有时是Pytorch 内存分配机制导致预留显存太多,从而报出显存不足的错误,针对这种情况,本文记录 Pytorch 内存分配机制,与通过配置 max_split_size_mb 来解决上述问题。

问题复现

假如我们当前的显存分配如上图所示,假设当前想分配 800MB 显存,虽然空闲的总显存有 1000MB,但是上方图的空闲显存由地址不连续的两个 500MB 的块组成,不够分配这 800MB 显存;而下方的图中,如果两个 500MB 的空闲块地址连续,就可以通过显存碎片的整理组成一个 1000MB 的整块,足够分配 800MB。上方图的这种情况就被称为显存碎片化

PyTorch 显存管理

基础概念

Block
  • 分配 / 管理内存块的基本单位,(stream_id, size, ptr) 三元组可以特异性定位一个 Block,即 Block 维护一个 ptr 指向大小为 size 的内存块,隶属于 stream_id 的 CUDA Stream。
  • 所有地址连续的 Block(不论是否为空闲,只要是由 Allocator::malloc 得来的)都被组织在一个双向链表里,便于在释放某一个 Block 时快速检查前后是否存在相邻碎片,若存在可以直接将这三个 Block 合成为一个。
BlockPool
  • 内存池,用 std::set 存储 Block 的指针,按照 (cuda_stream_id -> block size -> addr) 的优先级从小到大排序。所有保存在 BlockPool 中的 Block 都是空闲的。

  • DeviceCachingAllocator 中维护两种 BlockPool (large_blocks, small_blocks),分别存放较小的块和较大的块(为了分别加速小需求和大需求),简单地将 <= 1MB 的 Block 归类为小块,> 1MB 的为大块。

Block 在 Allocator 内有两种组织方式,一种是显式地组织在 BlockPool(红黑树)中,按照大小排列;另一种是具有连续地址的 Block 隐式地组织在一个双向链表里(通过结构体内的 prev, next 指针),可以以 O(1) 时间查找前后 Block 是否空闲,便于在释放当前 Block 时合并碎片。

显存申请

Pytorch 申请显存需要用到 malloc 函数:返回一个可用的 Block(L466)

表述定义:

  • size:请求分配的显存大小
  • alloc_size:需要 cudaMalloc 时的显存大小

hard-coded:

根据 size 决定实际上的 alloc_size(get_allocation_size 函数,L1078):

  • 为小于 1MB 的 size 分配 2MB;
  • 为 1MB ~ 10MB 的 size 分配 20MB;
  • 为 >= 10MB 的 size 分配 { size 向上取整至 2MB 倍数 } MB。

申请步骤

Pytorch 在申请显存时会寻找是否有合适的 block, 该过程有五个步骤,如果这五个步骤都没找到合适的 Block,就会报经典的 [CUDA out of memory. Tried to allocate …] 错误。

步骤一:get_free_block 函数(L1088)

TLDR:尝试在 Allocator 自己维护的池子中找一个大小适中的空闲 Block 返回。
*** TLDR = Too Long; Didn’t Read**

  • 用当前的 (size, stream_id) 这二元组制作 Block Key 在对应的 BlockPool 中查找;
  • 环境变量 PYTORCH_CUDA_ALLOC_CONF 中指定了一个阈值 max_split_size_mb,有两种情况不会在此步骤分配:
  1. 需要的 size 小于阈值但查找到的 Block 的比阈值大(避免浪费block);
  2. 两方都大于阈值但 block size 比需要的 size 大得超过了 buffer(此处是 20MB,这样最大的碎片不超过 buffer 大小)。
  • 这里的这个阈值 max_split_size_mb 涉及一个有趣的特性,后面会讲到。
  • 若成功分配一个 Block,则将这个 Block 从 BlockPool 中删除。后续用完释放(free)时会再 insert 进去,见free_block : L976
步骤二:trigger_free_memory_callbacks 函数(L1106)

TLDR:手动进行一波垃圾回收,回收掉没人用的 Block,再执行步骤一。

  • 若第一步的 get_free_block 失败,则在第二步中先调用 trigger_free_memory_callbacks,再调用一次第一步的 get_free_block
  • trigger_free_memory_callbacks 本质上通过注册调用了 CudaIPCCollect 函数,进而调用 CudaIPCSentDataLimbo::collect 函数(torch/csrc/CudaIPCTypes.cpp : L64);
  • CudaIPCSentDataLimbo 类管理所有 reference 不为 0 的 Block,所以实际上 collect 函数的调用算是一种懒更新,直到无 Block 可分配的时候才调用来清理那些 reference 已经为 0 的 Block(值得一提的是,该类的析构函数会首先调用 collect 函数,见 torch/csrc/CudaIPCTypes.cpp : L58);
  • 相关源码可以看 torch/csrc/CudaIPCTypes.h & .cpp
步骤三:alloc_block 函数(L1115)

TLDR:Allocator 在已有的 Block 中找不出可分配的了,就调用 cudaMalloc 创建新的 Block。

  • 步骤一、二中重用 block 失败,于是用 cudaMalloc 分配内存,大小为 alloc_size;
  • 注意有一个参数 set_fraction 会限制可分配的显存为当前剩余的显存 * fraction(若需要分配的超过这一限制则失败),但还没搞清楚哪里会指定这个(TODO);
  • 新分配的内存指针会被用于创建一个新 Block,新 Block 的 device 与 cuda_stream_id 与 caller 保持一致。

上面几个步骤都是试图找到一些空闲显存,下面是两个步骤是尝试进行碎片整理,凑出一个大块显存

步骤四:release_available_cached_blocks 函数(L1175)

TLDR:先在自己的池子里释放一些比较大的 Block,再用 cudaMalloc 分配看看

  • 如果上面的 alloc_block 失败了,就会尝试先调用这一函数,找到比 size 小的 Block 中最大的,由大至小依次释放 Block,直到释放的 Block 大小总和 >= size(需要注意,这一步骤只会释放那些大小大于阈值 max_split_size_mb 的 Block,可以理解为先释放一些比较大的);
  • 释放 block 的函数见 release_block(L1241),主要就是 cudaFree 掉指针,再处理一些 CUDA graphs 的依赖,更新其他数据结构等等,最后把这个 Block 从 BlockPool 中移除;
  • 当前整个 BlockPool(作为提醒,Allocator 有两个 BlockPool,这里指的是最初根据 size 指定的大 pool 或小 pool)中,可以释放的 Block 需要满足两个条件:cuda_stream_id 相同的,且大小要大于阈值 max_split_size_mb。如果将这样的 Block 全部释放的空间仍比 size 小,那么这一步就会失败。
  • 释放掉了一批 Block 之后,再次执行步骤三中的 alloc_block 函数,创建新 Block。
步骤五:release_cached_blocks 函数(L1214)
  • 如果释放一些 Block 还不够分配,则把整个 Allocator 中的 large / small pool 全部释放掉(同样调用 release_block:L1241),再次调用 alloc_block 函数。

分配失败

如果经历上述5个步骤还是没有找到合适的块用于显存申请,则会报出经典的 CUDA out of memory. Tried to allocate ... 错误,例如:

CUDA out of memory. Tried to allocate 1.24 GiB (GPU 0; 15.78 GiB total capacity; 10.34 GiB already allocated; 435.50 MiB free; 14.21 GiB reserved in total by PyTorch)

  • Tried to allocate:指本次 malloc 时预计分配的 alloc_size;
  • total capacity:由 cudaMemGetInfo 返回的 device 显存总量;
  • already allocated:由统计数据记录,当前为止请求分配的 size 的总和;
  • free:由 cudaMemGetInfo 返回的 device 显存剩余量;
  • reserved:BlockPool 中所有 Block 的大小,与已经分配的 Block 大小的总和。
    即 [reserved] = [already allocated] + [sum size of 2 BlockPools]

注意,reserved + free 并不等同于 total capacity,因为 reserved 只记录了通过 PyTorch 分配的显存,如果用户手动调用 cudaMalloc 或通过其他手段分配到了显存,是没法在这个报错信息中追踪到的(又因为一般 PyTorch 分配的显存占大部分,分配失败的报错信息一般也是由 PyTorch 反馈的)。

在这个例子里,device 只剩 435.5MB,不够 1.24GB,而 PyTorch 自己保留了 14.21GB(储存在 Block 里),其中分配了 10.3GB,剩 3.9GB。那为何不能从这 3.9GB 剩余当中分配 1.2GB 呢?原因肯定是碎片化了,而且是做了整理也不行的情况。

max_split_size_mb

我们已经经历了努力寻找显存但是没有成功的过程,那么当我们空闲显存明显比需要申请的显存大很多时候这个过程就存在不太合理的地方了。解决问题的关键在于 CUDA 中的 max_split_size_mb 变量设置。

官方定义

官方文档

1
max_split_size_mb prevents the native allocator from splitting blocks larger than this size (in MB). This can reduce fragmentation and may allow some borderline workloads to complete without running out of memory. Performance cost can range from ‘zero’ to ‘substantial’ depending on allocation patterns. Default value is unlimited, i.e. all blocks can be split. The memory_stats() and memory_summary() methods are useful for tuning. This option should be used as a last resort for a workload that is aborting due toout of memory’ and showing a large amount of inactive split blocks. max_split_size_mb is only meaningful with backend:native. With backend:cudaMallocAsync, max_split_size_mb is ignored.

max_split_size_mb 可以防止本机分配程序拆分大于此大小的块(以 MB 为单位),以此减少显存碎片,并且可能允许在不耗尽内存的情况下完成一些边界工作负载。

实现逻辑

根据 知乎大神 的Pytorch 显存分配源码解读,max_split_size_mb 的作用应该是限制分配显存时连续空闲显存块的大小的,通过这个阈值降低分配显存时直接拆分大块连续显存的概率。

他起作用的核心在于 get_free_block 函数中:我当前需要申请 size 大小的显存,阈值为 max_split_size_mb,此时我找到了 Block 大小的空闲块:

  1. 如果 size < max_split_size_mb (一个小块),但是 Block > max_split_size_mb不会直接执行显存分配(避免拆分大块空闲显存,导致 Block 浪费)
  2. 如果 size > max_split_size_mb 并且 Block > max_split_size_mb,但是 Block - size > 20MB,也不会执行显存分配,这个机制使得大于 20MB 的显存碎片不那么容易产生

至于 max_split_size_mb 影响数据、模型拆分、是内存分配的最大值等说法,个人不敢苟同。

错误信息

典型的使用 max_split_size_mb 可以大概率解决的错误信息类似这种:

1
RuntimeError: CUDA out of memory. Tried to allocate 6.18 GiB (GPU 0; 24.00 GiB total capacity; 11.39 GiB already allocated; 3.43 GiB free; 17.62 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF

这里 Pytorch 保留显存 17.62 GiB,已分配内存 11.39 GiB,中间 6 个多 g 显存没有充分利用,表示当前碎片化比较严重,这种情况可以尝试降低 max_split_size_mb 的值来降低碎片出现的概率。

修改 max_split_size_mb

直接修改环境变量即可,建议在 Python 运行过程中临时修改,避免不必要的性能降低

1
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:256'

单位 MB

参考资料

文章链接:
https://www.zywvvd.com/notes/environment/cuda/cuda-outof-memory/cuda-outof-memory/