Transformers 深度调试指南:多 GPU 通信故障定位与数值下溢/溢出检测实战
【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers
调试分布式训练问题通常可以归为几大类:数值问题(inf/nan/loss=NaN)、进程间通信失败、运行时错误和构建错误。本文以 Hugging Face Transformers 官方调试文档(docs/source/ko/debugging.md,英文版见 docs/source/en/debugging.md)为主线,结合本仓库的实际源码,系统讲解两大核心调试手段:多 GPU 通信网络诊断脚本与自动化的下溢/溢出检测模块,并补充 DeepSpeed 场景下常见的排错路径。读完本文,你将能够:用一条命令诊断多卡/多机 NCCL 通信是否正常;在训练出现loss=NaN时自动定位到产生inf/nan的第一个模块、第一个批次,并据此修复;针对特定批次跟踪张量绝对值的演变,快速锁定数值发散起点。
多 GPU 网络通信问题诊断
使用DistributedDataParallel进行多 GPU 训练或推理时,进程之间、节点之间的相互通信是最容易出错的环节之一。卡住、超时、barrier挂起等表现往往并非模型代码问题,而是底层网络通信问题。此时应当先用独立的最小诊断脚本确认"GPU 之间能否通信、能否分配显存",而不是在庞大训练脚本里大海捞针。
诊断脚本与快速上手
仓库中内置了官方诊断脚本 scripts/distributed/torch-distributed-gpu-test.py,它会在集群(单节点或多节点)中通过nccl后端检查所有 GPU 能否互相通信并成功分配显存。
以测试 2 块 GPU 的交互为例:
python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py如果两个进程成功通信并分配了 GPU 内存,每个进程都会打印OK状态。更多 GPU 或更多节点时,只需调整脚本的启动参数:
--nproc_per_node:每个节点上的进程数(通常等于每节点 GPU 数);--nnodes:参与训练的节点总数;- 使用自定义地址/端口时追加
--master_addr $MASTER_ADDR --master_port $MASTER_PORT; - 也可以改用 rdzv API:
--rdzv_endpoint $MASTER_ADDR:$MASTER_PORT --rdzv_backend c10d; - 若 PyTorch 版本低于 1.9,请使用
torch.distributed.launch替代torch.distributed.run。
从源码理解诊断逻辑
阅读 scripts/distributed/torch-distributed-gpu-test.py 的源码,可以看到它依次完成以下几项关键检查:
- 进程绑定 GPU:通过环境变量
LOCAL_RANK读取本地进程编号,并调用torch.cuda.set_device(local_rank)将每个进程绑定到对应的 GPU; - 初始化进程组:
dist.init_process_group("nccl")建立 NCCL 通信组,这一步失败通常意味着网络端口、MASTER_ADDR/MASTER_PORT配置有问题; - 通信验证:执行
dist.all_reduce(torch.ones(1).to(device), op=dist.ReduceOp.SUM)做一次全局求和,再调用dist.barrier()同步所有进程——如果程序挂在barrier调用上,说明存在网络问题(脚本源码注释中对此有明确提示); - 显存分配验证:
torch.ones(1).cuda(local_rank)触发 CUDA 显存分配,检查 GPU 是否可用; - 输出结果:每个进程通过
printflock打印形如[hostname-local_rank] is OK (global rank: x/y)的信息,rank 0 额外打印pt=torch 版本, cuda=CUDA 版本, nccl=NCCL 版本,便于核对各节点环境是否一致。
值得注意的是脚本中的printflock辅助函数:它利用文件锁(fcntl.flock)保证多进程并发打印时输出不会互相交错,这也是分布式诊断输出可读性的细节实现。
启用 NCCL 详细日志
如果基础诊断失败,可以为命令追加NCCL_DEBUG=INFO环境变量,让 NCCL 输出大量底层调试信息:
NCCL_DEBUG=INFO python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py这会打印出 NCCL 初始化、连接握手、通信通道建立等大量日志。拿到这些日志后,你可以自行检索关键词定位问题;如果不会解读,也可以将日志文件附在 issue 中提交给维护者。需要注意的是NCCL_DEBUG=INFO输出量很大,建议把日志重定向到文件后再分析。
在 SLURM 环境中运行
诊断脚本同样适用于 SLURM 调度环境,其源码注释中给出了完整的 SLURM 提交脚本配方,核心要点如下:
#SBATCH --job-name=test-nodes # 任务名 #SBATCH --nodes=2 # 节点数 #SBATCH --ntasks-per-node=1 # 关键:每个分布式节点只分配 1 个任务! #SBATCH --cpus-per-task=10 # 每个任务的核心数 #SBATCH --gres=gpu:4 # 每节点 GPU 数量 #SBATCH --time 0:05:00 # 最大执行时间 (HH:MM:SS) #SBATCH --output=%x-%j.out # 输出文件名 GPUS_PER_NODE=4 MASTER_ADDR=$(scontrol show hostnames $SLURM_JOB_NODELIST | head -n 1) MASTER_PORT=6000 srun --jobid $SLURM_JOBID bash -c 'python -m torch.distributed.run \ --nproc_per_node $GPUS_PER_NODE --nnodes $SLURM_NNODES --node_rank $SLURM_PROCID \ --master_addr $MASTER_ADDR --master_port $MASTER_PORT \ torch-distributed-gpu-test.py'其中--ntasks-per-node=1是必须的:每个节点只启动一个srun任务,再由torch.distributed.run在节点内部拉起--nproc_per_node个进程,避免任务与进程两层调度互相冲突。
数值下溢/溢出检测:DebugUnderflowOverflow
当训练出现loss=NaN,或模型因inf/nan表现出其他异常行为时,关键在于找出第一次出现下溢/溢出(underflow/overflow)的位置。激活值或权重到达inf/nan,通常意味着计算过程中数值范围失控。Transformers 提供了专门模块DebugUnderflowOverflow自动完成检测,避免人工逐步排查。
何时使用与使用前提
- 该功能当前仅在 PyTorch 下可用;
- 多 GPU 训练需要使用 DDP(
torch.distributed.launch/torchrun)。实际上 src/transformers/trainer.py 中的实现会在args.n_gpu > 1时直接抛出异常,提示Currently --debug underflow_overflow is not supported under DP. Please use DDP with torchrun,因为nn.DataParallel会复制模型,导致注册的 hook 在其他 GPU 上失效; - 该功能适用于基于
nn.Module的模型。
方式一:通过 Trainer 启用
使用 [Trainer] 时,只需在原有命令行参数中追加:
--debug underflow_overflow或者在创建 [TrainingArguments] 对象时传入:
from transformers import TrainingArguments args = TrainingArguments( debug="underflow_overflow", ... )该参数的定义见 src/transformers/training_args.py:debug是str或list[DebugOption],默认值为"",可选项包括underflow_overflow(检测模型输入输出的溢出)和tpu_metrics_debug(打印 TPU 指标)。多个选项以空格分隔的字符串传入时,会在内部被解析为DebugOption列表(见 training_args.py)。Trainer 在训练初始化阶段检测到DebugOption.UNDERFLOW_OVERFLOW in args.debug后,会自动实例化DebugUnderflowOverflow(self.model)(见 trainer.py)。
方式二:在自定义训练循环中启用
不使用 Trainer 或使用其他训练框架时,可以手动实例化调试器:
from transformers.debug_utils import DebugUnderflowOverflow debug_overflow = DebugUnderflowOverflow(model)工作原理:forward hook 与帧缓冲
[DebugUnderflowOverflow] 的实现位于 src/transformers/debug_utils.py。从源码看,其工作机制是:
- 注册 forward hook:
register_forward_hook对模型执行self.model.apply(self._register_forward_hook),即递归地为模型内每一个nn.Module注册forward_hook。该 hook 在对应模块的forward返回后立即触发,因此报告在每个forward刚结束时就生成; - 逐帧记录:每一帧(frame)记录三部分信息——该模块的完全限定名与类名(如
encoder.block.2.layer.1.layer_norm T5LayerNorm)、模块自身参数的绝对最小/最大值、每个输入/输出张量的绝对最小/最大值;非张量输入输出(None、元组等)会标记为None或not a tensor(见analyse_variable与create_frame,debug_utils.py); - 帧缓冲:内部维护一个
collections.deque([], max_frames_to_save),默认保存最近 21 帧(max_frames_to_save=21),一旦检测到溢出,就倒序倾倒出问题出现前的最新 21 个帧,为定位问题提供上下文; - 检测逻辑:对每个张量调用
detect_overflow(var, ctx)(debug_utils.py),通过torch.isnan(var).any()与torch.isinf(var).any()判断是否含nan或inf;一旦任一激活或权重元素出现inf/nan,程序会抛出ValueError断言中止,并打印报告。
解读检测报告
下面是一份典型的检测报告(示例取自 fp16 混合精度下google/mt5-small的训练,中间部分为节省篇幅已省略):
Detected inf/nan during batch_number=0 Last 21 forward frames: abs min abs max metadata encoder.block.1.layer.1.DenseReluDense.dropout Dropout 0.00e+00 2.57e+02 input[0] 0.00e+00 2.85e+02 output [...] encoder.block.2.layer.0 T5LayerSelfAttention 6.78e-04 3.15e+03 input[0] 2.65e-04 3.42e+03 output[0] None output[1] 2.25e-01 1.00e+04 output[2] encoder.block.2.layer.1.layer_norm T5LayerNorm 8.69e-02 4.18e-01 weight 2.65e-04 3.42e+03 input[0] 1.79e-06 4.65e+00 output encoder.block.2.layer.1.DenseReluDense.wi_0 Linear 2.17e-07 4.50e+00 weight 1.79e-06 4.65e+00 input[0] 2.68e-06 3.70e+01 output encoder.block.2.layer.1.DenseReluDense.wi_1 Linear 8.08e-07 2.66e+01 weight 1.79e-06 4.65e+00 input[0] 1.27e-04 2.37e+02 output encoder.block.2.layer.1.DenseReluDense.dropout Dropout 0.00e+00 8.76e+03 input[0] 0.00e+00 9.74e+03 output encoder.block.2.layer.1.DenseReluDense.wo Linear 1.01e-06 6.44e+00 weight 0.00e+00 9.74e+03 input[0] 3.18e-04 6.27e+04 output encoder.block.2.layer.1.DenseReluDense T5DenseGatedGeluDense 1.79e-06 4.65e+00 input[0] 3.18e-04 6.27e+04 output encoder.block.2.layer.1.dropout Dropout 3.18e-04 6.27e+04 input[0] 0.00e+00 inf output报告解读要点如下:
- 第一行给出问题出现的批次号,
Detected inf/nan during batch_number=0表示问题发生在第一个批次(batch 从 0 开始计数); - 表格结构:
abs min/abs max是张量所有元素绝对值的极小值与极大值(科学计数法),metadata列标识张量角色——weight为模块参数,input[i]为第 i 个输入,output[i]为第 i 个输出,无编号的output表示唯一输出; - 模块定位:例如
encoder.block.2.layer.1.layer_norm T5LayerNorm表示编码器第二个块中第一层的层归一化,其forward调用对应类为T5LayerNorm; - 数值趋势:观察最后几帧,
T5DenseGatedGeluDense的输出激活绝对最大值已达约 6.27e+04,而 fp16 中溢出(inf)前的最大可表示数字约为 64e3(6.4e+04)。fp16 下激活值应远小于 1e4,因为矩阵乘法中1e4 * 1e4 = 1e8会直接触发数值溢出条件。最后一帧Dropout将部分元素置零后重新归一化权重,把绝对最大值推过 64K,最终产生inf——这说明需要往前看溢出前几帧,而不是只看最后一帧。
结合模型源码定位根因
报告中的encoder.block.2.layer.1.DenseReluDense.dropout等路径可以直接与模型实现代码对应。文档示例给出的是 T5 前馈模块的经典实现(在 src/transformers/models/t5/modeling_t5.py 中,文档示例的类名T5DenseGatedGeluDense在当前代码库中对应为T5DenseGatedActDense,见 modeling_t5.py 中T5LayerFF对is_gated_act的分支选择):
class T5DenseGatedGeluDense(nn.Module): def __init__(self, config): super().__init__() self.wi_0 = nn.Linear(config.d_model, config.d_ff, bias=False) self.wi_1 = nn.Linear(config.d_model, config.d_ff, bias=False) self.wo = nn.Linear(config.d_ff, config.d_model, bias=False) self.dropout = nn.Dropout(config.dropout_rate) self.gelu_act = ACT2FN["gelu_new"] def forward(self, hidden_states): hidden_gelu = self.gelu_act(self.wi_0(hidden_states)) hidden_linear = self.wi_1(hidden_states) hidden_states = hidden_gelu * hidden_linear hidden_states = self.dropout(hidden_states) hidden_states = self.wo(hidden_states) return hidden_states对照这份代码,报告中的调用序列一目了然:wi_0、wi_1两次线性投影 →T5DenseGatedGeluDense前向(门控 GELU 乘积)→dropout。问题出在T5DenseGatedGeluDense.forward产生约 62.7K 的激活值后,随后的Dropout把最大值推过 fp16 上限。
修复方案:局部切换 fp32
定位到溢出发生的模块后,常见的修复是在数值开始变大的前几帧切换到 fp32 计算,避免乘法/加法过程中溢出。一种做法是把原来的forward逻辑抽到辅助方法_forward中,然后在forward里用torch.amp.autocast(..., enabled=False)包裹,临时关闭自动混合精度:
def _forward(self, hidden_states): hidden_gelu = self.gelu_act(self.wi_0(hidden_states)) hidden_linear = self.wi_1(hidden_states) hidden_states = hidden_gelu * hidden_linear hidden_states = self.dropout(hidden_states) hidden_states = self.wo(hidden_states) return hidden_states import torch def forward(self, hidden_states): device_type = hidden_states.device.type if torch.is_autocast_enabled(device_type): with torch.amp.autocast(device_type, enabled=False): return self._forward(hidden_states) else: return self._forward(hidden_states)torch.is_autocast_enabled(device_type)用于判断当前设备类型(cuda/cpu)是否开启了 autocast,只有开启时才显式关闭。当然修复方案不止这一种,例如可以临时关闭 AMP 混合精度训练整体排查,也可以调整缩放策略或改用 bf16。
检测 forward 内部的中间值
自动检测器只报告完整帧(模块级)的输入与输出。如果某个forward内部包含多个计算步骤,想知道具体是哪一步产生异常,可以使用detect_overflow辅助函数在任意位置手动插入检测点:
from transformers.debug_utils import detect_overflow class T5LayerFF(nn.Module): [...] def forward(self, hidden_states): forwarded_states = self.layer_norm(hidden_states) detect_overflow(forwarded_states, "after layer_norm") forwarded_states = self.DenseReluDense(forwarded_states) detect_overflow(forwarded_states, "after DenseReluDense") return hidden_states + self.dropout(forwarded_states)这里添加了两个检测点,分别检查 layer_norm 之后与 DenseReluDense 之后的forwarded_states是否出现inf/nan。detect_overflow(debug_utils.py)会打印形如xxx has nans/xxx has infs的信息并返回布尔值;源码中还内置了按阈值统计大元素数量(如绝对值超过 100/1000/10000 的元素个数)的辅助调试代码,可按需开启。
调整保存的帧数
自定义实例化调试器时,可以通过max_frames_to_save调整溢出时输出的帧数,默认值为 21:
from transformers.debug_utils import DebugUnderflowOverflow debug_overflow = DebugUnderflowOverflow(model, max_frames_to_save=100)更大的帧数可以提供更长的数值演变历史,便于观察数值从哪个位置开始失控。
特定批次绝对最小/最大值追踪
同一个调试类还提供第二种工作模式:关闭下溢/溢出检测,仅按批次追踪每个forward调用的绝对最小/最大值。这在"知道程序在某个批次之后开始异常"时尤其有用,可以直接把追踪聚焦到目标区域,对比数值从何处开始发散。
指定要追踪的批次
例如只追踪批次 1 和 3 的完整前向过程(批次从 0 开始计数):
debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3])此时批次 1 和 3 的每个forward帧都会以与检测模式相同的格式输出。样本输出如下(中间部分省略):
*** Starting batch number=1 *** abs min abs max metadata shared Embedding 1.01e-06 7.92e+02 weight 0.00e+00 2.47e+04 input[0] 5.36e-05 7.92e+02 output [...] decoder.dropout Dropout 1.60e-07 2.27e+01 input[0] 0.00e+00 2.52e+01 output decoder T5Stack not a tensor output lm_head Linear 1.01e-06 7.92e+02 weight 0.00e+00 1.11e+00 input[0] 6.06e-02 8.39e+01 output T5ForConditionalGeneration not a tensor output *** Starting batch number=3 *** abs min abs max metadata shared Embedding 1.01e-06 7.92e+02 weight 0.00e+00 2.78e+04 input[0] 5.36e-05 7.92e+02 output [...]输出中*** Starting batch number=N ***标记批次开始,not a tensor output表示该模块返回了非张量输出(如包含张量的复杂结构,DebugUnderflowOverflow会如实标注而非报错)。由于会为模型的每一次forward调用 dump 一帧,追踪模式会产生大量输出——这既可能是一种负担,也可能比通用调试器更直观:例如当问题从批次 150 开始出现时,只 dump 批次 149 与 150 的追踪,对比两组数据从何处开始不同,即可快速锁定。
从源码看,追踪模式由forward_hook中的trace_mode = self.batch_number in self.trace_batch_nums判定:处于追踪批次时先清空帧缓冲(reset_saved_frames),随后逐帧trace_frames()实时打印(debug_utils.py);而检测逻辑在detected_overflow and not trace_mode时才会触发(debug_utils.py),两种模式互斥。
在指定批次后停止训练
还可以通过abort_after_batch_num指定停止训练的批次号:
debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3], abort_after_batch_num=3)该参数在追踪模式下最常用,但任意模式都可以使用。源码中对应的中止逻辑位于 debug_utils.py:当self.batch_number > self.abort_after_batch_num时抛出ValueError并附上提示信息,避免调试脚本无限运行。
性能注意事项
DebugUnderflowOverflow会在每次forward时对模型的所有权重逐一计算绝对最小/最大值(见源码 docstring 的Performance一节),这会显著拖慢训练速度。因此务必在调试需求满足后立即移除该模块。官方文档还建议:如果要在耗时数小时的长训练中使用检测模式,先在追踪模式下对少量批次试运行,以确认调试器配置正确(例如 debug_utils.py 中的相关提示)。
DeepSpeed 场景的补充排错路径
如果训练启用了 DeepSpeed(TrainingArguments.deepspeed参数,见 training_args.py),遇到报错时应先判断是否为 DeepSpeed 所致:去掉 DeepSpeed 重跑一遍,若错误依旧,说明问题与 DeepSpeed 集成无关。下面列举几个常见问题(详见 docs/source/en/debugging.md)。
启动时进程被杀
如果 DeepSpeed 进程在启动阶段没有 traceback 就被杀掉,通常是程序申请的内存超过了可用/允许的 CPU 内存上限,被操作系统内核直接终止。此时应检查配置文件中是否配置了offload_optimizer、offload_param(或两者)将参数/优化器状态卸载到 CPU;如果环境具备 NVMe 且使用 ZeRO-3,可以改为卸载到 NVMe,并先估算模型的内存需求。
NaN loss 与 fp16 溢出
NaN loss常见于模型以 bf16 预训练、却以 fp16 使用的情况(TPU 训练的模型尤为常见)。此时应改用 fp32,或在硬件支持时改用 bf16(TPU、Ampere 及更新的 GPU)。fp16 本身也容易引发溢出,例如下面这种配置:
{ "fp16": { "enabled": "auto", "loss_scale": 0, "loss_scale_window": 1000, "initial_scale_power": 16, "hysteresis": 2, "min_loss_scale": 1 } }日志中反复出现的[deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: ..., reducing to ...表示 DeepSpeed 的 loss scaler 找不到能克服损失溢出的缩放系数。文档建议尝试更大的initial_scale_power值(32通常有效)。这本质上与本文第二部分讨论的溢出问题是同一类数值问题,只是发生在 DeepSpeed 的 AMP 缩放层。
调试流程速查
综合全文,遇到分布式训练异常时可按如下顺序排查:
- 通信问题:运行 scripts/distributed/torch-distributed-gpu-test.py 确认多卡/多机 NCCL 通信与显存分配正常;失败时加
NCCL_DEBUG=INFO获取底层日志; - 数值问题:给训练命令追加
--debug underflow_overflow(Trainer),或手动实例化DebugUnderflowOverflow(model)(自定义循环),等待自动报告定位第一个异常批次与模块; - 定向追踪:已知异常批次号时,用
trace_batch_nums=[a, b]对比相邻批次的绝对值演变,用abort_after_batch_num提前终止; - 修复与验证:在溢出模块处局部切换 fp32,或用
detect_overflow细化检测点;修复后务必移除调试器(它会显著拖慢训练); - DeepSpeed 场景:先去掉 DeepSpeed 复现判断责任方,再针对启动被杀、NaN loss、fp16 溢出分别按上文方案处理。
这套从"通信"到"数值"再到"定向追踪"的调试方法论,配合 src/transformers/debug_utils.py 的自动检测能力,能够把"大海捞针"式的排错转化为分钟级定位,是日常训练与模型微调中的实用工具。
【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考