资讯动态

深度学习内存、CPU、显存全解析:训练与推理瓶颈排查实战

发布时间:2026/10/21 12:06:32 来源:尧图企业网站定制
跑 Python 深度学习的人大概率都经历过这几件事训练刚启动就报显存不足GPU 利用率忽高忽低CPU 却先拉满进程看着没多少活跃数据内存却悄悄涨到了一二十个 GB。这些问题的本质往往不是模型“太大”而是没有把内存、CPU、显存在训练和推理里各自负责的部分理清楚。我这篇想做的事很简单把一条训练/推理链路从头到尾拆开讲明白内存RAM、CPU、显存VRAM到底分别承担什么任务哪里最容易出瓶颈出问题时怎么快速判断该调数据加载、该换卡、还是该省显存。内容不挑框架PyTorch 场景为主但你换到其他深度学习框架思路同样成立。适合谁看刚跑通第一个模型、还不清楚资源分配的新手已经遇到 OOM 或训练速度上不去、想系统排查的进阶用户也包括平时只拿推理服务跑业务、对硬件资源边界不敏感的工程同学。1. 先理清训练和推理的硬件协作框架1.1 一段训练代码背后的资源流动很多人以为深度学习训练就是把数据丢给 GPU其实从数据到模型再到结果资源流动是分阶段完成的。以 PyTorch 里最典型的一段训练循环为例for batch in dataloader: # 1. CPU 读取并预处理数据 batch batch.to(device) # 2. RAM - 显存 optimizer.zero_grad() # 3. 清空梯度显存中的操作 outputs model(batch) # 4. GPU 前向计算 loss criterion(outputs, labels) # 5. 计算损失 loss.backward() # 6. 反向传播生成梯度 optimizer.step() # 7. 用梯度更新权重可以把这三块资源用一个后厨类比来理解显存是灶台。真正煎炒烹炸的地方模型计算、中间激活、梯度都在这里。CPU 是厨师兼调度员。负责从冰箱或者货架把食材拿出来、洗干净、切好按顺序交给灶台。内存是货架和冷藏库。食材原始数据、预处理后的 batch暂时放在这里等厨师要用了再取。只看这段代码你会觉得 GPU 是主角。但实际上CPU 和内存如果拖后腿GPU 有再大的显存也算不动。1.2 为什么 GPU 不能单独完成所有事显存不是唯一主角GPU 擅长的是大规模并行矩阵运算但它不擅长做两件事一是数据读取和复杂预处理二是任务调度。文件从磁盘读出来以后要先在内存里做解码、resize、归一化、token 化这些工作现代框架都能放到 CPU 线程上去做。GPU 只负责拿到已经整理好的张量进行数学计算。显存本身容量有限而且通常比内存小很多。一张消费级显卡可能只有 8GB 到 16GB 显存而内存反而有 32GB 到 64GB。所以系统会有一个基本分工数据量大、不需要频繁参与计算的部分放内存真正要参与矩阵运算的部分才放显存。这个分工决定了我们在排查问题时不能只盯着一张显卡看而要看整条流水线。训练和推理还有一个重要区别训练是“数据反复来回”推理是“请求进来、结果出去”。这两个场景下三块资源的压力模型完全不同这也是下文要分开讲的原因。2. 内存RAM的关键任务和瓶颈点2.1 内存承担了哪些不可替代的工作内存最核心的角色是“中转站”。具体来说它在训练和推理中承担下面几类工作存放原始数据集。DataSet 是个 list 或者 dict 时所有样本的索引、原始图片路径、文本内容都驻留在内存里直到 epoch 跑完。存放预处理后的临时张量。数据增强、编码后产生的新数组在collate拼 batch 之前都在内存里。跨线程传递数据。DataLoader 的多个 worker 进程读到的 batch需要先放到共享内存或进程内存里主进程再取走。CPU 前向 / 推理的中间结果。如果你在纯 CPU 环境跑模型那么所有张量、中间激活都在 RAM 里此时 RAM 承担的职责和显存类似。断点保存和日志统计。保存 checkpoint 时如果先拼接大张量再写盘也会瞬时占用大片内存。很多新手会在内存任务里踩一个隐性坑他们把所有训练数据预先读成完整张量比如把所有图片np.array塞进一个 list。这样做的好处是训练时少做 IO坏处是一份原始数据可能占几个 GB数据增强后又有几份副本内存很容易翻倍。正常情况下数据不一定全部放在内存里从磁盘按需读取反而更健康。2.2 最容易爆内存的四个场景我实际排查过不少内存打满的案例最典型的四类场景如下一是数据集构建方式不对。比如在__getitem__里读入整张高清图并转成 float但不释放原始对象或者把 image 和 label 都存成 Python 对象一个 epoch 跑完也不清理。二是 DataLoader 的 worker 数量开得过大。num_workers8本身对内存不一定是灾难但如果每个 worker 都维持一份大的预处理缓存内存就会成倍上涨。很多人碰到num_workers调大后 CPU 上去了内存也翻了倍就是这个原因。三是训练的统计信息无限累积。比如每个 iteration 都把loss.item()存进 list一跑就是几万步又比如验证阶段把每个 batch 的outputs.cpu()存下来到最后一个大 tensor 全部堆在内存里。作为中间实验记录可以理解但长时间跑下来会变成内存黑洞。四是 checkpoint 保存方式太暴力。模型有几个 GB直接torch.save前又做了一次深拷贝然后序列化瞬时内存峰值可能是模型参数的好几倍。这种情况在推理服务里尤其明显因为服务端经常把多版本模型同时加载。2.3 怎么估算内存开销一个实战计价单排查内存问题不能靠感觉我一般先用两个维度去量化Python 侧分配的内存量和整个进程的 RSS常驻内存。Python 侧可以用tracemallocimport tracemalloc tracemalloc.start() # 跑一段你要观测的代码比如一个 epoch 的 dataloader 读取 current, peak tracemalloc.get_traced_memory() print(f当前内存: {current / 1024**3:.2f} GB) print(f峰值内存: {peak / 1024**3:.2f} GB)它统计的是 Python 对象分配不包含 CUDA 上的显存但非常适合定位是不是列表、字典、字符串等 Python 对象在无限增长。整个进程的真实内存占用可以直接看 RSSimport os import psutil process psutil.Process(os.getpid()) rss_gb process.memory_info().rss / 1024**3 print(f当前进程 RSS: {rss_gb:.2f} GB)如果发现 RSS 涨上去了而tracemalloc峰值不高说明内存很可能是被底层库分配的数组或碎片占掉的这种问题靠 Python 脚本不好查可以用resource模块看最大 RSS或者直接在系统监控里看进程的内存增长曲线。判断标准我通常这样定一次 epoch 结束后进程内存应回到起始水平附近如果同一份 model 和 dataset跑 10 个 epoch 内存涨了 3GB 以上基本可以认定有显式泄漏或数据累积。3. CPU 的关键任务和瓶颈点3.1 CPU 在训练和推理里到底在干什么CPU 在训练里不是打杂而是整条流水线的“节奏控制器”。主要工作分三块数据读取和预处理。图片解码、resize、归一化、数据增强、token 化这些操作在 PyTorch 里默认跑在 CPU 上。DataLoader 的 worker 进程也是 CPU 上执行的。内存拷贝和命令下发。把 CPU 张量搬到显存把 GPU 结果拷回 CPU在训练循环里由 CPU 发起并等待。这个过程比计算本身快不了多少一旦频繁做张量拷贝CPU 占用会明显上升。Python 解释和调度逻辑。Python 是解释型语言循环、条件判断、zip、enumerate、optimizer.step()里的大量逻辑都在 CPU 上执行。GPU 计算越快这部分解释开销就越显得刺眼。推理阶段同理。如果在 GPU 上做推理CPU 要根据请求动态拼 batch、预处理输入、调用模型再把结果序列化返回。如果推理服务用的 CPU 推理那 CPU 就同时是灶台和厨师压力更为直接。3.2 CPU 瓶颈的典型表现和定位方式CPU 瓶颈最常见的表象是GPU 利用率不高但 CPU 已经 100%。比如训练一个小模型显存占用率只有 30%GPU 算力利用率只有 20%而 CPU 的 16 个线程全部打满。这就是典型的 CPU 侧追不上 GPU 侧。定位方法也不复杂。训练时开一个终端循环看 GPU 状态另一个终端看 CPU# 终端一每 1 秒刷新一次 GPU 状态 watch -n 1 nvidia-smi # 终端二查看 CPU 占用和进程状态 htop如果 GPU 利用率常常低于 50%而且 CPU 一直 100%那你瓶颈大概率不在模型算力而在数据管线。还有一种情况是 CPU 占用不高但每一步之间有空档这可能是数据加载的等待时间可能是batch.to(device)和拷回操作之间的同步开销不能只盯 CPU 百分比。推理阶段怎么看用同样的思路。如果服务请求排队而 CPU 占用已经打满先看输入预处理是否复杂再看是否在 CPU 上做了太多不必要的矩阵乘法。如果 CPU 占用不高但延迟仍高那瓶颈可能是网络 IO、磁盘 IO 或锁竞争而不是 CPU 本身。3.3 数据加载和预处理CPU 的隐藏大坑针对 PyTorch 的 DataLoader有几个参数直接影响 CPU 的压力num_workers控制子进程数量。调大能并行预处理但过大会增加 CPU 调度和内存复制开销。prefetch_factor每个 worker 预取多少批数据。数值越大当前 batch 在 GPU 上计算时后续 batch 会提前准备好。pin_memory开启后内存页被固定住CPU 到 GPU 的拷贝可以走更快的 DMA 路径。一个我比较推荐的起步配置dataloader DataLoader( dataset, batch_size32, num_workers4, prefetch_factor2, pin_memoryTrue, persistent_workersTrue, )不是说这四个值对所有任务都最优。如果你的机器只有 8 核 CPUnum_workers4可能已经合适如果是读取远程文件或解码视频瓶颈在 IOnum_workers开大反会让磁盘争抢更严重。另外预处理逻辑如果特别重比如每张图都要做随机裁剪、旋转、拼图那 CPU 消耗和内存消耗几乎是线性关系这时候先用prefetch_factor缓一缓再看是不是需要把一部分增强操作移到 GPU 上做。我还踩过另一个坑在__getitem__里做全局随机种子设置。看起来无害但每个 worker 每次取数据都会重新设置随机种子CPU 调度开销暴涨。正确做法是用框架自带的分布式采样器或 worker 初始化函数避免在热路径里反复设置全局状态。4. 显存VRAM的关键任务和瓶颈点4.1 显存开销从哪里来一张 Tensor 的生命周期显存里装的东西比很多人想象得多。按生命周期来看至少包括以下几类模型参数。这是最小的一部分但也是很多人对照模型文件大小估算显存时唯一考虑的一项。梯度。训练时才存在反向传播后保留到优化器 step 完。优化器状态。Adam 这类优化器会额外保存动量项和方差项通常是参数量的 2 倍。中间激活值。前向传播的每一层输出如果自动求导需要它们会保留到反向传播。这是最容易吃满显存的部分。我用一个生活类比显存像一个临时工位。参数是文件柜梯度和优化器状态是草稿纸中间激活值是你在每一步计算时摊在桌上的过程文件。你真正能主动控制的往往不是文件柜而是桌上那堆过程文件。4.2 训练阶段显存占用分析训练阶段显存占用的核心公式可以粗看成显存占用 ≈ 模型参数 梯度 优化器状态 激活值 临时缓冲区以 FP32 的 AdamW 训练为例单参数占用的基础量级大约是 16 字节参数 4 字节、梯度 4 字节、一阶动量 4 字节、二阶动量 4 字节。一个 10 亿参数的模型光参数、梯度、优化器状态就需要约 16GB 显存这还不算激活值。很多人在这一步就开始觉得“卡不够”其实理解没错但要省钱得从激活值入手。中间激活值的显存和 batch size、序列长度、模型深度强相关。Transformer 这类模型在长序列下激活值可能远超权重本身。举个例子同样的模型sequence length 从 512 加到 1024注意力层和 MLP 的中间张量往往不止翻一倍因为很多张量形状是batch * seq_len * hidden有的甚至带batch * seq_len * seq_len的注意力矩阵。所以训练阶段的显存优化优先级一般是适当减小 batch size配合梯度累积保证训练效果。开启混合精度用半精度存激活值和权重副本减少显存占用。开启梯度检查点gradient checkpointing用重计算换显存。最后才考虑把优化器状态或参数搬到 CPU。不要把“把模型加载到 CPU 再一层层搬到 GPU”当成万能解。显存不够时 CPU offload 确实能跑起来但每层数据都要走一次 PCIe 拷贝训练速度可能慢到无法接受只适合卡显存刚好差一点点的场景。4.3 推理阶段显存占用分析推理阶段没有反向传播也没有梯度按理说显存压力小很多。但它有另一个问题一次性加载多 batch、放大推理、长序列生成的中间状态依然可能拉满显存。推理的显存主要由模型权重、CUDA context、临时激活值、以及服务端的 batch 缓存组成。如果是自回归式生成还有一类特殊激活值键值缓存KV Cache。它随序列长度线性增长长对话场景下甚至可以超过模型权重本身。我遇到过最典型的问题不是模型太大而是推理服务里同时加载了多个版本模型。表面上只有两个模型每个模型 3GB加起来 6GB显存 16GB 应该够。但框架加载模型时通常会多一份权重副本、一份优化后的推理图、一份缓存实际占用可能接近 2 倍于模型大小。这种情况下把一个一个模型的加载时序错开或者用统一推理后端动态调度比换一张大显存卡更划算。推理阶段可以放心开启torch.inference_mode()它比torch.no_grad()更严格会省掉一部分和自动求导相关的额外开销。import torch model.eval() with torch.inference_mode(): output model(input_tensor)如果你的推理代码还在跑backward那不是推理是训练显存当然下不去。4.4 显存不够的常规解法与误区显存不够时的第一反应大多数人都是“换一张更大的卡”但在换卡之前可以先检查这几个点是否开了混合精度。FP16/BF16 训练能把激活值和部分参数的显存减半是性价比最高的一步。是否正确用了梯度累积。小 batch 多步累积可以等效成大 batch显存峰值大幅下降。是否开启了torch.cuda.empty_cache()。这句命令只是清空缓存分配器里的空闲块不一定能解决真正的显存占用。如果显存已经因为活跃张量被打满调它没有任何帮助反而可能增加碎片。是否在排障时开了太多无关进程。显卡驱动、集成显卡、浏览器都会占用显存尤其在服务器上其他用户的残留进程也可能占着卡。关于“显存不够就上 CPU offload”我希望大家理性看待。它解决的是“跑起来”的问题不是“跑得快”的问题。数据在 PCIe 上来回搬带宽再高也远低于显存带宽一旦 offload 比例高训练吞吐会断崖式下跌。我自己只在两种情况下用 offload一是显存只缺 2GB 以内二是实验只关心是否能收敛不关心速度。5. 瓶颈排查与工具定位5.1 怎么确认瓶颈在三者中的哪一个排查瓶颈不靠猜我常用“排除法 分层计时”两步走。先看宏观指标。训练一个 epoch记录三个数CPU 平均利用率、内存峰值、GPU 利用率。如果 GPU 利用率和显存占用都很高说明模型计算和显存容量是瓶颈如果 GPU 利用率低CPU 高优先查 DataLoader 和预处理如果 GPU 利用率低CPU 也不高很可能是同步等待、网络传输或小 batch 导致 GPU 喂不饱。再看细粒度耗时。用torch.profiler给单步训练做个画像from torch.profiler import profile, ProfilerActivity with profile( activities[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapesTrue, ) as prof: for _ in range(5): train_one_step() print(prof.key_averages().table(sort_bycuda_time_total, row_limit20))这个表格会告诉你到底哪类算子吃掉了 GPU 时间哪些 CPU 算子在下发前耗时高。如果 CPU 表格里DataLoader相关操作耗时高优先优化数据加载如果 CUDA 表格里某个大矩阵乘耗时高才考虑模型结构或 batch size。一个容易被忽视的细节是PyTorch 的默认线程池会占用 CPU但数据加载子进程又是独立进程。单看主进程 CPU 可能只有 50%加上 worker 进程之后已经超过 100%。所以在系统监控里要看进程列表和线程总和而不是只看某一个 PID。5.2 实测下来好用的监控工具监控工具不需要多够用就行。我最常组合使用的是下面几类显卡实时状态用显卡驱动自带命令行工具看利用率、显存、功耗和温度。关注的是利用率波动和显存峰值不只是温度。系统级进程内存htop或者psutil脚本看进程 RSS 和虚拟内存。深度学习进程虚拟内存经常很高RSS 才是真正值得关注的量。Python 内存细化tracemalloc定位 Python 对象泄漏objgraph看对象引用关系。框架内置分析器torch.profiler看算子和数据管线的耗时占比。这些工具配合起来基本能覆盖“内存涨了、CPU 满了、GPU 闲了”这三类问题。我自己的习惯是在长时间训练脚本里每 500 步打一次系统监控日志包含内存 RSS、GPU 利用率、显存占用、当前 batch 的耗时移动平均。这样训练到中途出问题不用靠回看图直接看日志就能定位是从哪个 epoch 开始劣化的。6. 常见问题速查与实操心得我把自己在项目里真实遇到过的几个问题整理成了一个小速查表你可以把它当成排查清单用现象优先怀疑对象建议动作显存直接 OOM且 GPU 利用率高激活值或 batch size 过大减小 batch、开启混合精度、梯度检查点GPU 利用率低CPU 100%数据加载/预处理瓶颈调大num_workers、优化预处理、使用pin_memory内存随训练时长持续上涨Python 列表累积或 worker 泄漏用tracemalloc定位、限制历史记录长度推理延迟高但 CPU 不高锁竞争或 IO 等待查队列、网络和磁盘而不是盲目加线程模型加载后显存比预期大很多CUDA context 和多模型同时驻留错峰加载、统一推理后端、释放不再用的模型CPU 推理时速度波动大线程数设置/共享资源争抢固定 CPU 亲和性、设置线程数关于线程数还有一个容易忽略的点。torch.set_num_threads(1)和torch.set_num_interop_threads(1)适合在线程池环境下限制资源但不适合所有场景。如果你的推理服务本身是并发模型多个请求同时跑强行限制线程数反而会让单请求延迟上升。需要先想清楚你是要响应延迟更低还是吞吐更高再决定是不是要限制线程。最后分享一个我保留了很久的习惯在任何训练或推理脚本的主入口处先打印一次硬件资源概览包括 CPU 核心数、内存大小、显卡数量、显存大小、当前进程可用线程数。别小看这几行日志很多看起来玄乎的卡死问题最后都只是因为进程跑错了一张卡、机器内存被其他任务占掉了一半、或者线程池大小和机器核数不匹配。把这些信息打出来排查的时候心里才有一本明白账。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑