Transformers 文档

调试

Hugging Face's logo
加入 Hugging Face 社区

并获得增强型文档体验

开始使用

调试

无论您遇到安装问题还是 GPU 之间的通信问题,在多个 GPU 上进行训练都是一项棘手的任务。本调试指南涵盖了一些您可能会遇到的问题以及如何解决它们。

DeepSpeed CUDA 安装

如果你正在使用 DeepSpeed,你可能已经使用以下命令安装了它。

pip install deepspeed

DeepSpeed 编译 CUDA C++ 代码,它可能是在构建需要 CUDA 的 PyTorch 扩展时出现错误的潜在来源。这些错误取决于 CUDA 在系统上的安装方式,本节重点介绍使用 *CUDA 10.2* 构建的 PyTorch。

对于任何其他安装问题,请 打开一个问题 与 DeepSpeed 团队。

不一致的 CUDA 工具包

PyTorch 自带 CUDA 工具包,但要在 PyTorch 中使用 DeepSpeed,你需要在系统范围内安装相同版本的 CUDA。例如,如果你在 Python 环境中使用 cudatoolkit==10.2 安装了 PyTorch,那么你也需要在系统范围内安装 CUDA 10.2。如果你没有在系统范围内安装 CUDA,你应该先安装它。

确切位置可能因系统而异,但 usr/local/cuda-10.2 是许多 Unix 系统上最常见的安装位置。当 CUDA 正确设置并添加到你的 PATH 环境变量中时,你可以使用以下命令找到安装位置

which nvcc

多个 CUDA 工具包

你可能还在系统范围内安装了多个 CUDA 工具包。

/usr/local/cuda-10.2
/usr/local/cuda-11.0

通常,软件包安装程序会将路径设置为安装的最后一个版本。如果软件包构建失败,因为它找不到正确的 CUDA 版本(尽管它已经安装在系统范围内),那么你需要配置 PATHLD_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 的架构

相同 GPU
特定 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 或者由于激活或权重中的 infnan 导致模型表现出其他异常行为,则需要发现第一个下溢或上溢发生在哪里以及导致它的原因。幸运的是,你可以通过激活一个特殊的模块来轻松地做到这一点,该模块将自动执行检测。

如果你使用的是Trainer,你只需要添加

--debug underflow_overflow

到正常的命令行参数中,或者在创建TrainingArguments对象时传递debug="underflow_overflow"

如果你使用的是自己的训练循环或其他 Trainer,你可以使用

from transformers.debug_utils import DebugUnderflowOverflow

debug_overflow = DebugUnderflowOverflow(model)

DebugUnderflowOverflow 在模型中插入钩子,在每次前向调用之后立即测试输入和输出变量以及相应模块的权重。一旦在激活或权重的至少一个元素中检测到infnan,程序将断言并打印类似于此的报告(这是在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)

你可以看到我们添加了两个这样的检测器,现在我们跟踪在forwarded_states中是否检测到infnan

实际上,检测器已经报告了这些,因为上面示例中的每个调用都是一个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
[...]

在这里,你将获得大量的帧转储 - 与你的模型中 forward 调用的次数一样多,因此你可能想要这样做,也可能不想要这样做,但有时它可能比普通调试器更容易用于调试目的。例如,如果问题在批次号 150 处开始发生。因此,你可以转储批次 149 和 150 的跟踪,并比较数字开始偏离的地方。

你也可以指定停止训练的批次号,使用

debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3], abort_after_batch_num=3)
< > 在 GitHub 上更新