Frameworks/PyTorch

[PyTorch] 재현성 (Reproducibility)

Jonghyuk Baek 2021. 5. 18. 00:22

Introduction

파이토치를 이용하다 보면, 분명 똑같은 세팅으로 실험을 돌렸음에도 불구하고 학습 loss나 세세한 inference 결과 등이 조금씩 차이나는 현상을 볼 수 있다. 분명 non-deterministic 한 부분 (랜덤 시드 등) 을 다 통제했다고 생각했는데, 결과가 매번 다르니 굉장히 난감할 것이다.

 

오늘은 이에 대한 원인을 간략히 짚어보고, 파이토치의 재현성 (reproducibility) 을 어떻게 보장할 수 있는가에 대해 간단히 정리할 생각이다.


본론부터 말하면, 파이토치가 랜덤한 동작을 보이는 원인은 크게

 

  • 파이토치 내/외부의 난수 생성기 (random number generator)
  • cuDNN의 컨볼루션 벤치마킹
  • 그리고 PyTorch에서 사용하는 알고리즘 자체의 non-determinism

정도로 나눌 수 있다. 하나씩 알아보자.

1. Random number generator (RNG)

네트워크 웨이트 초기화 등 많은 코드 지점에서 난수 생성기를 이용하고 있기 때문에, 동일한 실험 결과를 얻기 위해서는 난수 생성기의 시드 (seed) 를 일정한 값으로 유지시켜 프로그램 실행 중 동일한 지점에 다른 값이 대입되는 것을 막아야 한다.

1.1. Python RNG

import random
random.seed(8888)

 

파이썬 내장 난수 생성 모듈이 사용될 가능성이 있으므로, 본격적인 코드 실행에 앞서 특정한 값으로 시드를 초기화 시켜주어야 한다. 글에서는 아무 값이나 넣어줬는데, 필요하다면 바꿔서 사용하면 된다.

1.2. PyTorch RNG

import torch
torch.manual_seed(8888)

파이썬 난수 생성기 이외에, 파이토치 자체의 난수 생성기도 시드를 설정해 주어야 한다. torch.manual_seed() 를 호출하면 CPU와 모든 GPU 디바이스에 대해서 난수 시드가 설정된다.

1.3. Numpy RNG

import numpy as np
np.random.seed(8888)

만약 파이썬 내장 기능과 파이토치 이외에 다른 라이브러리 혹은 NumPy 라이브러리를 이용하고 있고, 해당 부분에 NumPy 난수 생성기가 관여하는 부분이 있다면 Numpy RNG에 대해서도 시드를 설정해야 한다.

import numpy as np
rng = np.random.default_rng(8888)
rfloat = rng.random()

만약 위와 같이 기본 난수 생성기 이외에 random 모듈의 Generator 클래스 오브젝트를 따로 생성해 난수를 생성하고 있다면, 빼놓지 말고 따로 시드를 설정해 주자.

 

위와 같은 시드 설정 라인은 최대한 프로그램 앞 단에서 일괄적으로 실행시켜 혹시 모를 non-determinism을 방지하는 것이 좋다. 혹은 프로그램 앞 단에서의 난수 생성 횟수를 가늠하기가 어렵지만 이후의 과정에서 재현성이 요구된다면 (동일한 웨이트 초기값 등) 중간 지점에서 시드를 다시 설정해 주는 것도 좋다.

1.4. Multi-process data loading

추가로, 파이토치 데이터로더의 각 워커는 기본적으로 base_seed + worker_id 의 값으로 개별적으로 시드 세팅이 되기 때문에 다음과 같이 worker_init_fn() 을 정의해 시드 세팅을 수동으로 해 주어야 한다.

import torch
from torch.utils.data import DataLoader
import numpy as np
import random

def seed_worker(worker_id):
    worker_seed = torch.initial_seed() % 2**32
    np.random.seed(worker_seed)
    random.seed(worker_seed)

DataLoader(
    train_dataset,
    batch_size=batch_size,
    num_workers=num_workers,
    worker_init_fn=seed_worker
)

하지만 이와 같은 일련의 노력에도 불구하고 여전히 매 실행 결과가 달라질 수 있다. 어떤 원인이 더 남아있는 걸까?

 

2. CUDA convolution benchmarking

CUDA convolution에 이용되는 cuDNN 라이브러리에서는, 매번 새로운 크기의 convolution이 요구될 때마다 여러 convolution 알고리즘을 벤치마킹 해 현재 세팅에서 최적의 알고리즘을 찾아준다. 선정된 convolution 알고리즘은 프로그램 실행 중 해당되는 사이즈의 convolution이 호출될 때 마다 이용된다. 문제는 이런 과정이 알아서 진행되며, 세세한 하드웨어 세팅이나 벤치마크 오차에 의해 선택되는 알고리즘이 매번 달라질 수 있다는 점이다.

import torch
torch.backends.cudnn.benchmark = False

이 벤치마킹 기능을 꺼둠으로써 convolution 알고리즘의 차이에 의한 랜덤성을 제거할 수 있다. 다만 이 경우에 최적의 알고리즘을 사용하지 않게 되므로, 성능 저하가 발생할 가능성도 있다.

 

사실 이에 대해서 한번 실험을 해 봤는데, 몇 번의 간단한 실험을 통해선 벤치마킹 기능을 끈다고 노이즈 이상의 수준으로 inference time이나 convolution 결과 값에 차이가 나진 않았다... 하지만 모든 상황을 다 체크해 보지는 못 했기 때문에, strict한 재현성이 필요 없는 상황에서는 웬만하면 benckmark 기능을 켜 두는 편이 성능 면에서 더 낫다고 알아두면 좋을 것 같다.

import torch
...
model.forward({random input}) # cuda warm-up
torch.cuda.synchronize() # wait for warm-up
...
## Benchmark code block
...

참고로 네트워크 시간 성능을 벤치마킹 할 때, 네트워크 이니셜라이즈 후 미리 한번 포워드를 시켜 두어야 정확한 시간 측정이 가능하다. 위의 벤치마킹과 관련이 있을 수도 없을 수도 있다...

 

3. Non-deterministic algorithm

파이토치 내에는 기본적으로 non-deterministic 하게 작동하는 알고리즘들이 존재하며, 설정을 통해서 deterministic implementation으로 이들을 대체할 수 있다.

import torch
torch.use_deterministic_algorithms(True)

이 설정은 위에서 언급한 torch.backends.cudnn.benckmark 와는 별개로 따로 설정해 주어야 한다. 이 설정이 적용되어 있으면, PyTorch의 모든 오퍼레이션들은 deterministic한 알고리즘으로 수행되며, 만약 deterministic implementation이 존재하지 않는다면 RuntimeError를 throw 해 준다.

 

아래에는 deterministic 하게 전환될 수 있는, 혹은 불가능한 오퍼레이션들을 나열해 놓았다. 참고해서 이용하면 좋을 듯 하다. 내용은 파이토치 도큐멘테이션에서 그대로 가져왔다.

 

Deterministic 전환 가능한 오퍼레이션

Deterministic 전환 불가능한 오퍼레이션

Optional: Over CUDA version 10.2

참고로 CUDA 10.2 버전 이상부터는 추가적으로 CUBLAS_WORKSPACE_CONFIG=:4096:2CUBLAS_WORKSPACE_CONFIG=:16:8 과 같이 환경 변수를 설정해 주어야 하며, 그러지 않을 경우 아래의 오퍼레이션들에 대해 RuntimeError를 발생시킨다. 

자세한 사항은 여기를 참고하자.

Optional: RNN, LSTM non-determinism

그리고 파이토치의 RNN, LSTM implementation과 관련해 non-determinism 이슈가 따로 존재하는데, 퍼포먼스 영향을 감수하고 (...) 환경 변수를 다음과 같이 설정함으로써 해결할 수 있다.

$ export CUDA_LAUNCH_BLOCKING=1

만약 영구적으로 변경하고 싶다면 /etc/bash.bashrc 파일을 열어 위 라인을 직접 추가해 주어야 한다.

 


 

사실 모델의 non-determinism은 전혀 예상치 못한 부분에서도 발생할 수 있고, 위의 방법들은 단지 한번 시도해 볼 만한 해결책들 중 하나이다.

 

또한 backward() 오퍼레이션 같이 멀티스레드 환경에서 어쩔 수 없이 atomicAdd와 같은 non-deterministic cuDNN 연산을 호출하는 경우도 있어 (non-deterministic한 순서로 add 연산이 수행됨), 무시할 수 없을 정도의 재현 오차가 생긴다면 데이터로더 등에서 num_worker 값을 0으로 설정해 보는 것도 도움이 될 것이다.

 

이 이외에 정말 원인을 찾기 힘들다면, non-deterministic 양상을 보이는 모듈만을 따로 CPU에 올려 오퍼레이션을 수행시켜 봄으로써 GPU 연산에서 문제가 발생하는 지 확인해 보는 것도 좋은 방법이다.

 

아래는 참고할 만한 사이트들이다.

References

PyTorch Reproducibility 관련 도큐멘테이션 (많이 참고를 했다)

https://pytorch.org/docs/stable/notes/randomness.html

NumPy Random Generator

https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.Generator

PyTorch 데이터로더에서의 randomness

https://pytorch.org/docs/stable/data.html#randomness-in-multi-process-data-loading

torch.use_deterministic_algorithms

https://pytorch.org/docs/stable/generated/torch.use_deterministic_algorithms.html#torch.use_deterministic_algorithms

CublasAPI reproducibility

https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility

atomicAdd와 관련된 파이토치 포럼 스레드

https://discuss.pytorch.org/t/reproducibility-of-cudaextension/113011/5

반응형