PyTorch num_workers에 관하여

seokj·2023년 1월 19일
3

torch.utils.data.DataLoader의 num_workers는 dataset의 데이터를 gpu로 전송할 때 필요한 전처리를 수행할 때 사용하는 subprocess의 수를 말한다. num_workers의 수를 늘리면 병렬처리를 통해 더 빠르게 gpu에 정보를 전달할 수 있어 성능이 좋아진다. 하지만 num_workers의 수가 너무 크면 다른 일을 수행하는 데 사용할 자원이 적어져 성능이 안좋아질 수 있다. 따라서 적절한 값을 찾기 위해 하이퍼파라미터 튜닝을 하듯 접근한다.


참고 자료
https://discuss.pytorch.org/t/guidelines-for-assigning-num-workers-to-dataloader/813


num_workers를 어떻게 정할지에 대해 이해하기 전에 데이터의 전반적인 흐름을 먼저 알아야 한다. 학습에 사용될 데이터는 다음 3가지 영역에 존재할 수 있다.

  1. HDD나 SSD와 같은 보조저장장치의 메모리
  2. CPU의 RAM
  3. GPU의 메모리

GPU로 학습을 시킨다면 데이터가 1에서 2를 지나 3으로 가서 학습이 되고 CPU로 학습을 시킨다면 3까지 가지 않고 2에서 학습이 된다. 여기서는 GPU로 학습을 시킨다는 가정을 하겠다.
Dataset__getitem__함수에 있는, 데이터를 읽는 부분에서 1에서 2로의 이동이 일어난다. 학습 데이터가 이미지라면 cv2.imread를 호출한다거나 비디오라면 cv2.VideoCapture().read함수를 호출하는 순간이다. Dataset은 항상 DataLoader에 의해 불리기 때문에 DataLoader를 반복하는 for문에서 이 과정이 수행된다.
이 for문 안에서 얻은 데이터는 배치로 묶여있는 형태이다. 이 for문 안에서 데이터를 2에서 3으로 전송한다. .to(device)함수를 호출하는 순간이다.

그래서 경우에 따라 3가지 영역의 메모리 초과 문제가 각각 발생할 수 있다. 메모리 초과 문제를 해결할 방법을 1번 영역을 제외하고 알아보겠다.

2번 영역의 메모리가 부족한 경우

  1. 가상메모리를 더 할당하여 보조저장장치의 메모리를 RAM으로 끌어다 사용한다. 속도가 느려질 수 있다.
  2. 무작위로 학습 데이터를 여러 묶음으로 나눈 다음 한 묶음씩 DataLoader를 만들고 학습한 뒤 del으로 지워준다. 전체 데이터를 RAM에 올려두는 것이 아니기 때문에 해결된다.
  3. 1에 저장된 데이터를 압축시켜놓고 DataLoader에서 압축된 데이터 그대로 로딩한다. 데이터를 GPU로 보내기 직전에 압축을 풀어 전달한다. 모델을 학습시킨 뒤 다음 데이터를 로딩하기 전에 압축을 푼 객체를 del으로 지워준다. RAM에 올라간 데이터는 압축된 상태이기 때문에 해결할 수 있다.

3번 영역의 메모리가 부족한 경우

  1. 배치 크기를 작게 하여 한 번에 GPU로 올라가는 데이터 크기를 작게 한다.
  2. 학습하는 모델도 함께 GPU에 올라가 있기 때문에 모델의 크기를 줄이는 것으로도 GPU 메모리의 공간을 확보하는 것에 도움이 된다.

GPU에서 모델을 학습하는 속도와 CPU에서 DataLoader가 데이터를 준비해주는 속도는 다르다. 보통 CPU에서 수행되는 속도가 더 느리기 때문에 DataLoader는 멀티프로세싱을 통해 더 빠르게 GPU에 데이터를 조달한다. 이 때 사용할 멀티프로세스의 수가 num_workers이다.

num_workers는 적당한 값이 좋다. 너무 작으면 멀티프로세싱의 효과를 덜 보기 때문에 느릴 것이고, 너무 많으면 프로세스끼리 손발을 맞추기 위해 들어가는 오버헤드가 더 크기 때문에 오히려 느려진다.

num_workers의 적당한 값은 다양한 요인에 영향을 받는다. CPU의 속도와 코어의 수 GPU의 속도와 수, 데이터의 크기와 수, 배치 크기 등등 아주 복잡하기 때문에 공식화할 수 없어서 일반적으로 매번 환경에 따라 실험을 통해 가장 빠를 때의 값을 측정하여 구한다.

하지만 실험 초기에 먼저 시도해 볼 법한 공식이 디스커션에 많이 제안되었다. 나열하자면 다음과 같다:

  1. num_workers = 4 * num_GPU (or 8, 16, 2 * num_GPU)
  2. entry * batch_size * num_worker = num_GPU * GPU_throughtput
  3. num_workers = batch_size / num_GPU
  4. num_workers = batch_size / num_CPU

하지만 환경에 따라 적절한 num_workers의 값은 항상 바뀌므로 항상 실험으로 검증하는 것이 좋다.


적절한 실험을 수행하는 방법으로 Greedy Hill-Climbing알고리즘이 있다:
num_workers와 batch_size등 정해야 할 여러 파라미터를 나열해놓고 하나씩 최적의 값을 실험으로 찾아낸다. 모두 찾았다면 처음 파라미터부터 다시 최적의 값을 찾아낸다. 수렴할 때까지 반복한다.

num_workers의 적절한 값을 찾을 때는 모델을 학습시키면 안된다. 단순히 DataLoader로 빈 반복문만 돌려야 한다. GPU의 속도가 너무 느려 병목이 발생하면 데이터를 준비하는데 걸리는 시간이 측정되지 않기 때문이다.


다음은 몇가지 팁이다.

멀티프로세싱을 사용하면 error trace가 알기 어렵게 뒤죽박죽으로 나온다. 디버깅할 때는 사용하지 않는 것이 좋다.

리눅스에서는 lscpu로 cpu 코어 수를 확인할 수 있고 윈도우에서는 작업관리자로 확인할 수 있다.

nvidia_smi명령어로 GPU사용 효율을 확인할 수 있다.

GPU로 데이터가 보내질 때마다 GPU커널이 몇몇 변수를 초기화하는 과정을 해야해서 배치 크기가 클수록 초기화를 덜하게 되고 전체 학습시간이 단축된다.

num_workers가 0이면 멀티프로세싱을 사용하지 않는다.

num_workers가 0보다 크면 멀티프로세싱을 사용하는데, 이 때 파이썬의 multiprocessing라이브러리가 pymp폴더를 생성한다. pymp는 임시폴더이고 파이썬이 종료되면 삭제되어야 하지만 파이썬의 버그로 남아있을 수 있다.

https://developer.nvidia.com/blog/how-optimize-data-transfers-cuda-cc/
https://zhuanlan.zhihu.com/p/39752167
https://github.com/developer0hye/Num-Workers-Search

profile
안녕하세요

0개의 댓글