调试
在多GPU上进行训练可能是一项棘手的任务,无论您遇到的是安装问题还是GPU之间的通信问题。本调试指南涵盖了一些您可能会遇到的问题以及如何解决它们。
DeepSpeed CUDA 安装
如果您正在使用DeepSpeed,您可能已经通过以下命令安装了它。
pip install deepspeed
DeepSpeed 编译 CUDA C++ 代码,在构建需要 CUDA 的 PyTorch 扩展时,这可能是错误的潜在来源。这些错误取决于 CUDA 在您的系统上的安装方式,本节重点介绍使用 CUDA 10.2 构建的 PyTorch。
对于任何其他安装问题,请提交问题给DeepSpeed团队。
非相同的CUDA工具包
PyTorch 自带其自己的 CUDA 工具包,但要使用 DeepSpeed 与 PyTorch,你需要在系统范围内安装相同版本的 CUDA。例如,如果你在 Python 环境中安装了带有 cudatoolkit==10.2
的 PyTorch,那么你还需要在系统范围内安装 CUDA 10.2。如果你没有在系统范围内安装 CUDA,你应该先安装它。
确切的位置可能因系统而异,但在许多Unix系统上,usr/local/cuda-10.2
是最常见的位置。当CUDA正确设置并添加到您的PATH
环境变量中时,您可以使用以下命令找到安装位置:
which nvcc
多个CUDA工具包
您也可能在系统范围内安装了多个CUDA工具包。
/usr/local/cuda-10.2 /usr/local/cuda-11.0
通常,包安装程序会将路径设置为最后安装的版本。如果包构建失败,因为它找不到正确的CUDA版本(尽管它已经在系统范围内安装),那么你需要配置PATH
和LD_LIBRARY_PATH
环境变量,使其指向正确的路径。
首先查看这些环境变量的内容:
echo $PATH
echo $LD_LIBRARY_PATH
PATH
列出了可执行文件的位置,而 LD_LIBRARY_PATH
列出了共享库的查找位置。较早的条目优先于较晚的条目,并且使用 :
来分隔多个条目。要告诉构建程序在哪里找到你想要的特定 CUDA 工具包,请首先插入正确的路径。此命令是前置而不是覆盖现有值。
# adjust the version and full path if needed
export PATH=/usr/local/cuda-10.2/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-10.2/lib64:$LD_LIBRARY_PATH
此外,您还应检查您分配的目录是否实际存在。lib64
子目录包含各种CUDA .so
对象(如 libcudart.so
),虽然您的系统不太可能以不同的方式命名它们,但您应检查实际名称并相应地进行更改。
旧版CUDA版本
有时,较旧的CUDA版本可能拒绝与较新的编译器一起构建。例如,如果您有gcc-9
,但CUDA需要gcc-7
。通常,安装最新的CUDA工具包可以支持较新的编译器。
你也可以安装一个旧版本的编译器,除了你当前使用的那个(或者它可能已经安装,但默认情况下没有使用,构建系统看不到它)。为了解决这个问题,你可以创建一个符号链接,让构建系统能够看到旧版本的编译器。
# adapt the path to your system
sudo ln -s /usr/bin/gcc-7 /usr/local/cuda-10.2/bin/gcc
sudo ln -s /usr/bin/g++-7 /usr/local/cuda-10.2/bin/g++
预构建
如果您在安装DeepSpeed时仍然遇到问题,或者您在运行时构建DeepSpeed,您可以尝试在安装之前预构建DeepSpeed模块。要为DeepSpeed进行本地构建:
git clone https://github.com/microsoft/DeepSpeed/
cd DeepSpeed
rm -rf build
TORCH_CUDA_ARCH_LIST="8.6" DS_BUILD_CPU_ADAM=1 DS_BUILD_UTILS=1 pip install . \
--global-option="build_ext" --global-option="-j8" --no-cache -v \
--disable-pip-version-check 2>&1 | tee build.log
要使用NVMe卸载,请将DS_BUILD_AIO=1
参数添加到构建命令中,并确保在系统范围内安装libaio-dev包。
接下来,您需要通过编辑TORCH_CUDA_ARCH_LIST
变量来指定您的GPU架构(在此页面上找到NVIDIA GPU及其对应架构的完整列表)。要检查与您的架构对应的PyTorch版本,请运行以下命令:
python -c "import torch; print(torch.cuda.get_arch_list())"
使用以下命令查找GPU的架构:
CUDA_VISIBLE_DEVICES=0 python -c "import torch; print(torch.cuda.get_device_capability())"
如果你得到 8, 6
,那么你可以设置 TORCH_CUDA_ARCH_LIST="8.6"
。对于具有不同架构的多个GPU,可以像这样列出它们 TORCH_CUDA_ARCH_LIST="6.1;8.6"
。
也可以不指定TORCH_CUDA_ARCH_LIST
,构建程序会自动查询构建的GPU架构。然而,这可能与目标机器上的实际GPU匹配或不匹配,这就是为什么最好明确指定正确的架构。
对于在具有相同设置的多台机器上进行训练,您需要制作一个二进制轮子:
git clone https://github.com/microsoft/DeepSpeed/
cd DeepSpeed
rm -rf build
TORCH_CUDA_ARCH_LIST="8.6" DS_BUILD_CPU_ADAM=1 DS_BUILD_UTILS=1 \
python setup.py build_ext -j8 bdist_wheel
此命令生成一个二进制轮子,看起来像 dist/deepspeed-0.3.13+8cd046f-cp38-cp38-linux_x86_64.whl
。现在你可以在本地或另一台机器上安装这个轮子。
pip install deepspeed-0.3.13+8cd046f-cp38-cp38-linux_x86_64.whl
多GPU网络问题调试
在使用DistributedDataParallel
和多个GPU进行训练或推理时,如果遇到进程和/或节点之间的通信问题,可以使用以下脚本来诊断网络问题。
wget https://raw.githubusercontent.com/huggingface/transformers/main/scripts/distributed/torch-distributed-gpu-test.py
例如,要测试2个GPU如何交互,请执行以下操作:
python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py
如果两个进程可以相互通信并分配GPU内存,每个进程将打印一个OK状态。
如需更多GPU或节点,请调整脚本中的参数。
您将在诊断脚本中找到更多详细信息,甚至还有一个关于如何在SLURM环境中运行它的指南。
额外的调试级别是添加NCCL_DEBUG=INFO
环境变量,如下所示:
NCCL_DEBUG=INFO python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py
这将输出大量与NCCL相关的调试信息,如果您发现报告了一些问题,可以在线搜索这些信息。或者如果您不确定如何解释输出,可以在Issue中分享日志文件。
下溢和上溢检测
此功能目前仅适用于PyTorch。
对于多GPU训练,需要DDP(torch.distributed.launch
)。
此功能可以与任何基于nn.Module
的模型一起使用。
如果你开始遇到loss=NaN
或模型由于激活或权重中的inf
或nan
而表现出其他异常行为,你需要发现第一次下溢或上溢发生的地方以及导致它的原因。幸运的是,你可以通过激活一个特殊的模块来轻松完成这一任务,该模块会自动进行检测。
如果你正在使用Trainer,你只需要添加:
--debug underflow_overflow
除了正常的命令行参数外,或者在创建TrainingArguments对象时传递debug="underflow_overflow"
。
如果你使用自己的训练循环或其他Trainer,你可以通过以下方式实现相同的效果:
from transformers.debug_utils import DebugUnderflowOverflow
debug_overflow = DebugUnderflowOverflow(model)
DebugUnderflowOverflow 在模型中插入钩子,每次前向调用后立即测试输入和输出变量以及相应模块的权重。一旦在激活或权重中至少一个元素中检测到 inf
或 nan
,程序将断言并打印如下报告(这是在 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
为了简洁起见,示例输出在中间被截断了。
第二列显示的是绝对最大元素的值,所以如果你仔细观察最后几帧,输入和输出都在1e4
的范围内。因此,当这个训练在fp16混合精度下完成时,最后一步溢出了(因为在fp16
下,inf
之前的最大数字是64e3
)。为了避免在fp16
下溢出,激活值必须远低于1e4
,因为1e4 * 1e4 = 1e8
,所以任何具有大激活值的矩阵乘法都会导致数值溢出情况。
在跟踪的最开始,你可以发现问题发生在哪个批次号(这里Detected inf/nan during batch_number=0
表示问题发生在第一个批次)。
每个报告的帧首先声明该帧所对应的模块的完全限定入口。如果我们只看这个帧:
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.layer_norm
表示它是编码器第二块第一层的层归一化。而 forward
的具体调用是 T5LayerNorm
。
让我们看看报告的最后几帧:
Detected inf/nan during batch_number=0
Last 21 forward frames:
abs min abs max metadata
[...]
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.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
最后一帧报告了Dropout.forward
函数,第一个条目是唯一的输入,第二个条目是唯一的输出。你可以看到它是从DenseReluDense
类中的dropout
属性调用的。我们可以看到,这发生在第一个批次中的第二块的第一层。最后,输入元素的绝对最大值是6.27e+04
,输出的绝对最大值是inf
。
你可以在这里看到,T5DenseGatedGeluDense.forward
导致了输出激活,其绝对最大值约为62.7K,非常接近fp16的上限64K。在下一帧中,我们有Dropout
,它在将一些元素置零后重新归一化权重,这导致绝对最大值超过了64K,从而产生了溢出(inf
)。
正如你所看到的,当数字开始变得非常大时,我们需要查看之前的帧,特别是对于fp16数字。
让我们将报告与来自models/t5/modeling_t5.py
的代码进行匹配:
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
现在很容易看到dropout
调用,以及所有之前的调用。
由于检测是在前向钩子中进行的,这些报告会在每次forward
返回后立即打印。
回到完整报告,为了采取行动并解决问题,我们需要回溯几帧,找到数字开始上升的地方,并很可能切换到fp32
模式,这样在数字相乘或相加时不会溢出。当然,可能还有其他解决方案。例如,如果启用了amp
,我们可以在将原始的forward
移动到一个辅助包装器后,暂时关闭它,如下所示:
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):
if torch.is_autocast_enabled():
with torch.cuda.amp.autocast(enabled=False):
return self._forward(hidden_states)
else:
return self._forward(hidden_states)
由于自动检测器仅报告完整帧的输入和输出,一旦你知道在哪里查找,你可能还想分析任何特定forward
函数的中间阶段。在这种情况下,你可以使用detect_overflow
辅助函数在你想要的地方注入检测器,例如:
from 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)
你可以看到我们添加了2个这些,现在我们跟踪是否在某个地方检测到inf
或nan
用于forwarded_states
。
实际上,检测器已经报告了这些,因为上面示例中的每个调用都是nn.Module
,但假设你有一些本地的直接计算,这就是你如何做到这一点。
此外,如果您在自己的代码中实例化调试器,您可以调整打印的帧数,例如:
from transformers.debug_utils import DebugUnderflowOverflow
debug_overflow = DebugUnderflowOverflow(model, max_frames_to_save=100)
特定批次的绝对最小值和最大值追踪
相同的调试类可以用于每批次的跟踪,同时关闭下溢/上溢检测功能。
假设你想观察给定批次中每个forward
调用的所有成分的绝对最小值和最大值,并且只对批次1和3进行此操作。那么你可以这样实例化这个类:
debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3])
现在,完整的批次1和3将使用与下溢/上溢检测器相同的格式进行跟踪。
批次从0开始索引。
如果您知道程序在某个批次号后开始出现问题,这将非常有用,因此您可以快速跳转到该区域。以下是此类配置的示例截断输出:
*** 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
[...]
在这里,你将获得大量的帧转储——与模型中前向调用的次数一样多,所以这可能不是你想要的,但有时它比普通调试器更容易用于调试目的。例如,如果问题在第150批次开始发生。因此,你可以转储第149和150批次的跟踪,并比较数字开始分歧的地方。
你也可以指定在哪个批次后停止训练,使用:
debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3], abort_after_batch_num=3)