1. 背景

目标是使用 Docker 在 GPU 服务器上跑通一套代码,并复现 README 中给出的结果。

项目入口类似:

inference/run_classifier_multiview.py

给出的环境要求包括 Python 3.9、CUDA 12.8、PyTorch 2.8.0、transformers、scikit-learn、dpkt 等依赖,并且数据处理流程包括 PCAP 处理、bi-gram 编码、训练/验证/测试集划分,以及基于 Dirichlet 不确定性的 open-set 评估。前期我已经完成了 Docker 环境部署、数据路径确认和快速训练验证。后续真正开始复现指标时,遇到的核心问题就是:

RuntimeError: CUDA out of memory

这篇文章主要复盘这次 CUDA OOM 的排查过程,以及我最终如何确定稳定训练配置并复现 README 指标。


2. OOM 是什么

OOM 是 Out of Memory,意思是内存不足。

在深度学习训练中,最常见的是 CUDA OOM,也就是 GPU 显存不足。它和普通服务器内存不一样。

比如服务器有一张 40GB 显存的 A100,看起来显存很大,但如果已经有其他进程占用了十几 GB,实际可用于训练的显存就会少很多。训练 Transformer 类模型时,还需要保存前向传播的中间激活、梯度、优化器状态等内容,显存开销会明显高于推理。

本次项目中,模型不是简单的传统机器学习模型,而是带有 Transformer/UER 结构、多视角输入和 Dirichlet 不确定性建模的深度学习分类模型。项目目录中包含 uer 框架、bert_base_config.jsonpre-trained_model.bin 等文件,README 也说明了会微调预训练模型并计算 Dirichlet 不确定性。因此,CUDA OOM 并不意外。


3. 第一次 OOM:默认 batch_size 太大

恢复训练代码后,我第一次直接按默认配置运行。脚本能正常读取数据,也能进入训练阶段,但随后在模型 forward 阶段报错:

RuntimeError: CUDA out of memory.
Tried to allocate 384.00 MiB
GPU 0; 39.49 GiB total capacity;
22.81 GiB already allocated;
337.44 MiB free;
23.39 GiB reserved in total by PyTorch

这个报错信息很关键,它说明了几件事:

1. GPU 总显存约 39.49 GiB;
2. PyTorch 已经占用了 22GB 以上;
3. 剩余可用显存只有几百 MiB;
4. 模型还想再申请 384 MiB,但已经不够了。

工作日志里也记录了:默认 batch_size=1024 太大,加上 GPU 上已有其他进程占用显存,多视角 Transformer attention 计算显存开销较高,所以训练进入 forward 阶段后触发 CUDA OOM。


4. 第一反应:不要盲目 kill 进程

遇到 OOM 时,第一反应不能是直接杀进程。

在多人共用 GPU 服务器时,nvidia-smi 里看到的进程可能是别人正在运行的服务或者实验。直接 kill 可能影响别人工作。

我的处理方式是先查看 GPU 状态:

nvidia-smi

重点看:

Memory-Usage
GPU-Util
Processes

如果 GPU 上已有其他进程长期占用显存,就应该优先考虑:

1. 换另一张 GPU;
2. 降低 batch size;
3. 等待空闲时间;
4. 和负责人确认是否可以释放 GPU。

本次实践中,我没有直接 kill 其他进程,而是选择通过调整 batch size 来解决。


5. 为什么 batch size 会影响显存

batch_size 表示一次送入模型训练的样本数量。

对于深度学习训练,显存大致会被这些部分占用:

模型参数
梯度
优化器状态
输入 batch
中间激活
attention 计算缓存
loss 相关中间变量

其中,batch size 越大,输入数据和中间激活越大,显存占用越高。

本项目中还有一个额外因素:多视角输入。也就是说,每个流量样本并不是单一路径输入,而是会被拆成多个 view,再分别进入编码器。这会进一步增加显存消耗。

所以虽然 A100 40GB 很强,但默认 batch_size=1024 对这个模型来说仍然太大。


6. 逐步降低 batch size,而不是一次改到很小

我没有直接随便改一个 batch size,而是做了一个简单的 batch 探测实验。

6.1 batch_size=16:能跑通

先把 batch size 从默认值降到 16,并且只跑 1 个 epoch 做快速验证:

PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 python run_classifier_multiview.py \
  --seq_length 128 \
  --labels_num 200 \
  --threshold 1 \
  --lambda_epochs 1 \
  --epochs_num 1 \
  --batch_size 16 \
  --packet_num 4 \
  --packet_len 128 \
  --device_id 0 \
  --dataset_name android \
  --train_dataset ./processed_data/android_train.txt \
  --valid_dataset ./processed_data/android_valid.txt \
  --test_dataset ./processed_data/android_encrypted_test.txt \
  --open_dataset ./processed_data/attack_encrypted_open.txt

结果是能跑通。单轮训练耗时约 248 秒,并成功生成 1.2G 的 fine-tuned 模型。这个阶段的准确率很低,Closed Balanced Acc 为 0.0050,Open Balanced Acc 为 0.0000,但这很正常,因为这里只是 1 epoch 快速验证,不代表模型真实性能。

这一步的价值是:

证明环境、数据读取、训练循环、模型保存和推理流程都能跑通。

6.2 batch_size=32:稳定可用

接着我把 batch size 提到 32:

PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 python run_classifier_multiview.py \
  --seq_length 128 \
  --labels_num 200 \
  --threshold 1 \
  --lambda_epochs 1 \
  --epochs_num 1 \
  --batch_size 32 \
  --packet_num 4 \
  --packet_len 128 \
  --device_id 0 \
  --dataset_name android \
  --train_dataset ./processed_data/android_train.txt \
  --valid_dataset ./processed_data/android_valid.txt \
  --test_dataset ./processed_data/android_encrypted_test.txt \
  --open_dataset ./processed_data/attack_encrypted_open.txt \
  2>&1 | tee batch32_probe.log

batch_size=32 可以正常跑完 1 epoch,说明当前 GPU 剩余显存下,32 是一个可用配置。


6.3 batch_size=64:再次 OOM

继续尝试 batch_size=64 时,程序再次 OOM:

RuntimeError: CUDA out of memory.
Tried to allocate 96.00 MiB
GPU 0; 39.49 GiB total capacity;
23.54 GiB already allocated;
1.44 MiB free;
23.72 GiB reserved in total by PyTorch

这说明在当前 GPU 占用情况下,64 已经超出安全范围。因此,我最终确定:

batch_size=32 是当前最合适的训练配置。

7. PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 的作用

我在训练命令前加了:

PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128

它主要用于缓解 PyTorch 显存碎片问题。

有时 GPU 总显存看起来还有剩余,但由于显存被分割成很多不连续的小块,PyTorch 找不到足够连续的显存块,也可能 OOM。这个参数可以调整 PyTorch 的显存分配策略,降低碎片化风险。

但是它不是万能的。

如果 batch size 本身太大,显存总量确实不够,那还是会 OOM。本次 batch_size=64 就是这种情况:即使加了这个参数,仍然不够。


8. 从“能跑”到“能复现指标”

确定 batch_size=32 可用后,我没有直接跑默认 50 epoch,而是先跑了一个小完整测试:

batch_size=32
epochs_num=5
lambda_epochs=20

5 epoch 训练耗时约 17 分钟,结果如下:

加密应用流量 Closed Balanced Acc = 0.9187
非加密应用流量 Closed Balanced Acc = 0.9557

这说明模型已经学起来了,closed-set 分类基本接近 README 目标。但当时 open-set 异常检测还很低,说明仅 5 epoch 可能不够充分,尤其是 Dirichlet 不确定性建模还没有完全收敛。

随后我继续使用:

batch_size=32
epochs_num=20
lambda_epochs=20

进行完整训练。训练命令如下:

PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 python run_classifier_multiview.py \
  --seq_length 128 \
  --labels_num 200 \
  --threshold 1 \
  --lambda_epochs 20 \
  --epochs_num 20 \
  --batch_size 32 \
  --packet_num 4 \
  --packet_len 128 \
  --device_id 0 \
  --dataset_name android \
  --train_dataset ./processed_data/android_train.txt \
  --valid_dataset ./processed_data/android_valid.txt \
  --test_dataset ./processed_data/android_encrypted_test.txt \
  --open_dataset ./processed_data/attack_encrypted_open.txt \
  2>&1 | tee train_bs32_e20_lambda20_retry.log

最终 20 epoch 训练耗时约 1 小时 08 分钟,并成功完成测试。训练日志显示,batch_size=32、epochs_num=20、lambda_epochs=20 下,程序正常读取训练集、验证集、测试集,完成 20/20 轮训练,并在测试集上得到 Closed Balanced Acc = 0.9438。


9. 最终复现结果

在当前可运行 Docker 环境下,虽然没有完全使用 README 的 Python 3.9 + PyTorch 2.8.0 环境,但通过调整 batch size 和训练轮数,仍然成功复现并超过了 README 中的核心指标。

10. 为什么 1 epoch 结果很低,而 20 epoch 结果很高

一开始 1 epoch 的结果是:

Closed Balanced Acc = 0.0050
Open Balanced Acc = 0.0000

看起来几乎没效果。但这不是模型坏了,而是训练远远不充分。

对于 200 类分类任务,随机猜测的水平本来就很低。1 epoch 只是为了测试代码链路,不足以让模型学习到有效分类能力。

更关键的是,这个项目还使用 Dirichlet 不确定性做 open-set 检测。open-set 检测不仅要学会“这个样本属于哪一类”,还要学会“这个样本是不是未知类”。这种不确定性建模通常需要更充分训练。

所以实验过程呈现出很典型的趋势:

1 epoch:链路验证,指标很低
5 epoch:closed 分类明显提升,open 检测仍弱
20 epoch:closed 和 open 都达到 README 目标

这也是为什么实验日志不能只看一次结果,而要结合训练配置解释。


11. 这次 OOM 排查的经验总结

11.1 先看显存,再改参数

遇到 CUDA OOM,第一步应该是:

nvidia-smi

看清楚:

GPU 总显存
当前显存占用
剩余显存
是否有其他进程

11.2 不要一上来就跑完整训练

我这次的顺序是:

batch=16, epoch=1:验证链路
batch=32, epoch=1:探测稳定 batch
batch=64, epoch=1:确认显存上限
batch=32, epoch=5:小完整测试
batch=32, epoch=20:正式复现

这种方式比直接跑 50 epoch 更稳,因为可以尽早发现 OOM、训练无效、模型未保存等问题。


11.3 OOM 不是代码逻辑错误

这点很重要。OOM 通常是资源配置问题,不是算法一定错了。关键是:

为什么 OOM
怎么定位
改了什么参数
每次实验结果是什么
最终如何确定稳定配置

本次 OOM 的根因可以总结为:

默认 batch_size=1024 过大;
模型是多视角 Transformer,显存开销高;
训练阶段需要保存梯度和中间激活;
GPU 上已有其他进程占用显存;
因此需要降低 batch size。

11.4 batch size 变小会影响速度和训练行为

把 batch size 从 1024 降到 32 后,单个 epoch 的 step 数会明显增加。比如 batch_size=32 时,训练实例数显示为 632。这意味着训练更慢,但能稳定运行。所以实际工程中经常要在这几者之间平衡:

显存占用
训练速度
指标复现
GPU 共享情况

11.5 记录日志非常重要

我所有关键命令都加了:

2>&1 | tee xxx.log

这样做的好处是:

终端能实时看到输出;
日志文件能留存证据;
后续可以整理实验表;
出问题时可以回溯。

这次最终也把训练日志、评估日志和模型信息统一打包归档,方便后续交付和复盘。


12. 怎么讲

“你遇到过深度学习训练的 OOM 吗”

遇到过。我在部署一个模型时,恢复训练代码后默认 batch_size=1024,训练进入 Transformer forward 阶段后触发 CUDA OOM。通过 nvidia-smi 发现 A100 40GB 显卡上已有其他进程占用显存,实际可用显存不足。

我没有直接 kill 进程,而是通过逐步降低 batch size 做探测:batch_size=16 能跑通,32 稳定可用,64 仍然 OOM,因此确定当前环境下的安全配置为 batch_size=32。随后用 batch_size=32、epochs_num=20、lambda_epochs=20 完成完整训练,最终复现了 README 的指标。这个过程中我也保留了训练日志和评估日志,并把可运行 Docker 环境固化成镜像,保证后续可以复现。

这个回答能体现三点:

1. 我知道 OOM 的原理;
2. 我会用工程方法定位问题;
3. 我最后不是只解决报错,而是完成了指标复现。

13. 最终总结

这次 OOM 排查让我最大的感受是:深度学习工程不是简单地“跑一条命令”,而是要在模型、数据、GPU、Docker 环境、训练参数之间做系统性排查。

本次实践中,我从默认 batch size OOM 开始,逐步定位到显存不足的原因,并通过 batch size 探测确定了稳定配置。最终在 batch_size=32 下完成 20 epoch 训练,并复现 README 指标。

整个过程可以总结为:

看错误日志
查 GPU 状态
确认是否资源问题
降低 batch size
做 batch 探测
小规模训练验证趋势
完整训练复现指标
保存模型和日志
总结可复用经验

更多推荐