디버깅

디버깅 (Debugging)

분산 학습 문제 디버깅은 보통 다음 범주 중 하나에 속해요: 수치적 문제, 통신 실패, 런타임 에러, 빌드 에러.

출처: 문서

본문

언더플로와 오버플로 감지

활성화(activation)나 가중치가 inf 또는 nan에 도달하거나, loss=NaN이 되면 언더플로와 오버플로가 발생해요. 이를 감지하려면 TrainingArguments.debug()에서 DebugUnderflowOverflow 모듈을 활성화하거나, 직접 import해서 학습 루프에 추가해요.

from transformers import TrainingArguments

args = TrainingArguments(
    debug="underflow_overflow",
    ...
)
from transformers.debug_utils import DebugUnderflowOverflow

debug_overflow = DebugUnderflowOverflow(model)

DebugUnderflowOverflow는 모델에 훅을 삽입해서 각 forward 호출 후에 입력·출력 변수와 해당 모델 가중치를 검사해요. 활성화 또는 가중치의 요소 하나라도 inf 또는 nan이 감지되면 모듈은 아래와 같은 보고서를 출력해요.

아래 예시는 google/mt5-small을 사용한 fp16 혼합 정밀도 학습에 대한 것이에요.

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

첫 번째 줄은 에러가 발생한 배치 번호를 보여줘요. 이 경우 배치 0에서 발생했어요.

각 프레임은 보고하는 모듈을 설명해요. 예를 들어 아래 프레임은 인코더의 두 번째 블록 첫 번째 레이어의 레이어 norm인 encoder.block.2.layer.1.layer_norm에 대해 보고해요. forward 호출은 T5LayerNorm에 대한 것이에요.

                  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

마지막 프레임은 DenseReluDense 클래스 안의 dropout 속성을 호출하는 Dropout.forward 함수에 대해 보고해요. 오버플로(inf)는 첫 번째 배치의 인코더 두 번째 블록에서 발생했어요. 가장 큰 입력 요소는 6.27e+04였어요.

                  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

T5DenseGatedGeluDense.forward 출력 활성화는 최대 6.27e+04에 도달했는데, 이는 fp16의 최대값 6.4e+04에 가까워요. 다음 단계에서 Dropout은 일부 요소를 0으로 만든 후 가중치를 재정규화해서 최대값을 6.4e+04 위로 밀어 올려 오버플로를 발생시켰어요.

이제 에러가 어디서 발생하는지 알았으니 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

한 가지 해결책은 값이 너무 커지기 몇 단계 전에 fp32로 전환해서, 곱하거나 더할 때 숫자가 오버플로하지 않게 하는 거예요. 또 다른 옵션은 혼합 정밀도 학습(amp)을 임시로 비활성화하는 거예요.

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)

보고서는 전체 프레임의 입력과 출력만 다뤄요. 어떤 forward 함수 안의 중간 값을 분석하려면 각 forward 호출 뒤에 detect_overflow를 추가해서 forwarded_states에서 inf 또는 nan을 추적해요.

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)

DebugUnderflowOverflow가 출력하는 프레임 수를 구성해요.

from transformers.debug_utils import DebugUnderflowOverflow

debug_overflow = DebugUnderflowOverflow(model, max_frames_to_save=100)

배치 추적

DebugUnderflowOverflow는 언더플로/오버플로 감지 없이 각 배치의 절대 최소·최대 값도 추적할 수 있어요. 이는 모델에서 값이 어디서부터 발산하기 시작하는지 찾는 데 도움이 돼요.

아래 예시는 배치 1과 3을 추적해요 (배치는 0부터 인덱싱됨).

debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3])
                  *** 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
[...]

DebugUnderflowOverflow는 많은 프레임을 보고해서 값이 어디서 발산하는지 찾기 쉽게 만들어요. 문제가 배치 150 근처에 있다는 걸 안다면 추적을 배치 149와 150으로 집중해서 숫자가 어디서부터 달라지기 시작하는지 비교해요.

특정 배치 번호 이후로 추적을 중지할 수도 있어요, 예를 들어 배치 3 이후.

debug_overflow = DebugUnderflowOverflow(model, trace_batch_nums=[1, 3], abort_after_batch_num=3)

통신

분산 학습은 프로세스 간, 노드 간 통신이 필요하며, 이는 흔한 에러의 원인이에요.

아래 스크립트를 다운로드해서 네트워크 문제를 진단하고, 실행해서 GPU 통신을 테스트해요. 아래 명령은 GPU 두 개를 테스트해요. 시스템에 맞게 --nproc_per_node와 --nnodes를 조정해요.

wget https://raw.githubusercontent.com/huggingface/transformers/main/scripts/distributed/torch-distributed-gpu-test.py
python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py

두 GPU가 통신하고 메모리를 성공적으로 할당하면 스크립트는 OK를 출력해요. 진단 스크립트와 SLURM 환경에서 실행하는 레시피에 대한 자세한 내용은 스크립트를 참고해요.

상세한 NCCL 디버깅 출력을 얻으려면 NCCL_DEBUG=INFO를 설정해요.

NCCL_DEBUG=INFO python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py

DeepSpeed

에러가 발생하면 먼저 DeepSpeed가 원인인지 확인해요. DeepSpeed 없이 설정을 다시 시도해 보고, 에러가 지속되면 이슈를 보고해요. Transformers 통합과 무관한 이슈는 DeepSpeed 저장소에 이슈를 열어주세요.

Transformers 통합과 관련된 이슈라면 다음 정보를 포함해요.

  • 전체 DeepSpeed config 파일.

  • Trainer의 커맨드라인 인자 또는 (본인이 Trainer 설정을 스크립팅한다면) 그 TrainingArguments — 관련 없는 항목이 많은 전체 TrainingArguments를 덤프하지 마세요.

  • 다음 명령들의 출력.

    python -c 'import torch; print(f"torch: {torch.__version__}")'
    python -c 'import transformers; print(f"transformers: {transformers.__version__}")'
    python -c 'import deepspeed; print(f"deepspeed: {deepspeed.__version__}")'
    
  • 이슈를 재현할 Google Colab 노트북 링크.

  • 이슈를 재현할 표준 또는 비-커스텀 데이터셋이나 기존 예시.

시작 시 프로세스가 종료됨

DeepSpeed 프로세스가 트레이스백 없이 시작 중에 종료된다면, 프로그램이 사용 가능하거나 허용된 것보다 더 많은 CPU 메모리를 할당하려 한 거예요. 두 경우 모두 OS 커널이 프로세스를 종료해요.

config 파일에 offload_optimizer, offload_param, 또는 둘 다 CPU로 오프로드하도록 구성되어 있는지 확인해요.

NVMe와 ZeRO-3를 설정했다면 대신 NVMe로 오프로드해 보세요. 먼저 모델의 메모리 요구 사항을 추정해요.

NaN loss

NaN loss는 모델이 bf16으로 사전 학습된 후 fp16으로 사용될 때 자주 발생해요 (특히 TPU 학습된 모델에서 흔함). 하드웨어가 지원한다면(TPU, Ampere GPU 이상) fp32 또는 bf16을 사용해요.

fp16은 오버플로도 일으킬 수 있어요. config 파일이 아래처럼 생겼다면 로그에서 오버플로 에러가 보일 수 있어요.

{
    "fp16": {
        "enabled": "auto",
        "loss_scale": 0,
        "loss_scale_window": 1000,
        "initial_scale_power": 16,
        "hysteresis": 2,
        "min_loss_scale": 1
    }
}

아래 OVERFLOW! 에러는 DeepSpeed loss scaler가 loss 오버플로를 극복할 스케일링 계수를 찾지 못했다는 뜻이에요. 더 높은 initial_scale_power 값(보통 32가 작동함)을 시도해 보세요.

0%|                                                                                                                             | 0/189 [00:00<?, ?it/s]
 [deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: 262144, reducing to 262144
  1%|▌                                                                                                                    | 1/189 [00:00<01:26,  2.17it/s]
 [deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: 262144, reducing to 131072.0
  1%|█▏
 [...]
 [deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: 1, reducing to 1
 14%|████████████████▌                                                                                                   | 27/189 [00:14<01:13,  2.21it/s]
 [deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: 1, reducing to 1
 15%|█████████████████▏                                                                                                  | 28/189 [00:14<01:13,  2.18it/s]
 [deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: 1, reducing to 1
 15%|█████████████████▊                                                                                                  | 29/189 [00:15<01:13,  2.18it/s]
 [deepscale] OVERFLOW! Rank 0 Skipping step. Attempted loss scale: 1, reducing to 1
[...]

DeepSpeed CUDA

DeepSpeed는 CUDA C++ 코드를 컴파일하는데, 이는 CUDA가 필요한 PyTorch 확장의 흔한 빌드 에러 원인이에요. 이 에러들은 시스템에 CUDA가 어떻게 설치되어 있는지에 따라 달라요.

pip install deepspeed

[!TIP] 다른 설치 이슈는 DeepSpeed 팀에 이슈를 열어 주세요.

동일하지 않은 툴킷

PyTorch는 자체 CUDA 툴킷을 포함하지만, DeepSpeed는 시스템 전체에 동일한 CUDA 버전이 설치되어 있어야 해요. Python 환경에 cudatoolkit==10.2로 PyTorch를 설치했다면, 모든 곳에 CUDA 10.2도 설치해야 해요.

정확한 위치는 시스템마다 다르지만 /usr/local/cuda-10.2가 Unix 시스템에서 가장 흔한 경로예요. CUDA가 설정되고 PATH에 추가되면 이 명령으로 설치 위치를 찾아요.

which nvcc

여러 툴킷

시스템에 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 하위 디렉토리는 libcudart.so 같은 CUDA .so 객체를 담아요. 실제 파일명을 확인하고 그에 따라 업데이트해요.

이전 버전

더 오래된 CUDA 버전은 더 오래된 컴파일러 버전을 요구할 때가 있어요. 예를 들어 CUDA가 gcc-7을 요구하는데 시스템에 gcc-9만 있다면 빌드가 실패해요. 요구되는 더 오래된 컴파일러를 설치하고 CUDA 빌드 시스템이 찾을 수 있도록 심링크(symlink)를 만들어요.

# adjust 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++

사전 빌드 (Prebuild)

DeepSpeed를 설치하거나 런타임에서 빌드하는 데 여전히 문제가 있다면 먼저 DeepSpeed 모듈을 사전 빌드해요. 로컬 빌드에는 아래 명령을 실행해요.

git clone https://github.com/deepspeedai/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

[!TIP] NVMe offload를 사용하려면 빌드 명령에 DS_BUILD_AIO=1을 추가하세요. 시스템 전체에 libaio-dev 패키지를 설치해야 합니다.

다음으로 TORCH_CUDA_ARCH_LIST에 GPU 아키텍처를 설정해요. NVIDIA GPU와 그 아키텍처의 전체 목록은 CUDA GPUs page에 있어요. 아키텍처에 해당하는 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())"

GPU 0의 아키텍처를 찾으려면 다음 명령을 실행해요. 출력은 GPU 아키텍처를 함께 이루는 major와 minor 값을 보여줘요. 아래 예시는 아키텍처 8.6을 보여줘요.

CUDA_VISIBLE_DEVICES=0 python -c "import torch; \
print(torch.cuda.get_device_properties(torch.device('cuda')))
"_CudaDeviceProperties(name='GeForce RTX 3090', major=8, minor=6, total_memory=24268MB, multi_processor_count=82)"

결과가 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/deepspeedai/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

더 알아보기 (Learn more)

  • DeepSpeed 디버깅에 대한 더 자세한 내용은 DeepSpeed 문서를 참고해 주세요.