이전 글에서 Pytorch framework에서 성능 최적화하는 방법을 소개해드렸습니다. 이번 글에서는 설명드린 각 방법들이 얼마만큼 time cost 성능 최적화가 되는지 실험해보도록 하겠습니다. 실험 코드는 여기서 확인가능합니다.
실험해볼 최적화 방법 목록입니다.
- Data Loading 최적화
- num worker 설정
- pinned memory 사용
- Data Operation 최적화
- tensor.to(non_blocking=True) 사용
- Training 최적화
- Architecture design과 batch size를 8의 배수로 설정
- Mixed Precision Training 사용
- Optimizer로 weight를 update하기 전에 gradient을 None으로 설정
- Gradient accumulation 사용
- Inference 최적화
- Inference시에 gradient calculation 끄기
- CNN 최적화
- torch.backends.cudnn.benchmark = True 사용
- 4D NCHW tensors에 대해 channel_last memory format를 사용
0. 실험 환경
- Device 및 PyPI
- CPU: Intel(R) Xeon(R) Gold 5120 CPU @ 2.20GHz (가상 core수: 56)
- GPU: Tesla V100
- CUDA: 11.2
- Driver Version: 460.73.01
- torch: 1.8.1
- Dataset
- CIFAR10
- shape: 32(H)x32(W)x3(C)
- CIFAR10
- Model
- ResNet18, 50, 101
- MobileNetv2
- 기본 Config
- num_workers=4, pin_memory=True
- batch_size=128
- epochs=5
※ 모든 실험의 결과의 단위는 초(s)이며 5번의 epoch에 대한 평균치를 낸 것입니다. (첫 epoch제외)
1. DataLoading 최적화
1.1 & 1.2 num_workers와 pinned_memory의 사용
num_workers와 pinned_memory 설정은 각각 단독으로 사용하기보다는 같이 사용합니다. 그렇기 때문에 두 설정을 동시에 사용했을 때와 그렇지 않을 때의 성능 차이를 보도록 하겠습니다. 사용 방법은 아래와 같습니다.
# Use num_workers=4 and pin_memory=True
Dataloader(dataset, num_workers=4*num_GPU, pin_memory=True)
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
num_workers=0, pin_memory=False |
train: 36.5s test: 2.8s |
train: 74.2s test: 5.1s |
train: 113.0s test: 7.3s |
train: 35.8s test: 2.8s |
num_workers=4, pin_memory=True |
train: 20.5s test: 1.5s |
train: 58.0s test: 3.6s |
train: 96.8s test: 5.8s |
train: 20.1s test: 1.4s |
num_workers=8, pin_memory=True |
train: 20.9s test: 1.8s |
train: 58.2s test: 3.8s |
train: 97.2s test: 6.0s |
train: 20.5s test: 1.7s |
위의 결과를 분석하자면 다음과 같다.
- num_workers와 pin_memory 설정을 사용하였을 때 time cost가 줄어든 것을 확인 가능
- data loading에 최적화된 방법이므로 model에 상관없이 고정된 time cost 성능 효과를 보임
- (GPU 1개 기준) num_workers의 optimal한 값은 4이고 그 이상인 경우(i.e. 8) 성능 효과가 보이지 않음
2. Data operation 최적화
2.1 tensor.to(non_blocking=True) 사용
non_blocking에 대한 설정은 아래와 같이 input, target(label)에서 가능합니다.
for input, target in Dataloader:
# 아래 2 lines을 통해 non-blocking과 overlapping이 진행
input = input.to('cuda:0', non_blocking=True)
target = target.to('cuda:0', non_blocking=True)
# 해당 구간에서 input과 target의 변수가 사용되지 않는 선에서 코딩을 할경우
# 비동기적으로 실행되므로 execution time을 줄일 수 있음
# synchronization시점으로 위의 2 lines을 기다리는 구간
output = model(input)
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o non_blocking | train: 20.4s test: 1.4s |
train: 58.1s test: 3.6s |
train: 96.9s test: 5.8s |
train: 20.1s test: 1.4s |
w non_blocking | train: 20.4s test: 1.4s |
train: 57.9s test: 3.6s |
train: 97.0s test: 5.8s |
train: 20.0s test: 1.4s |
위 결과를 분석하면 다음과 같습니다.
- 성능 효과가 없는 것으로 보임
- 구글링해보니 설정으로 time cost성능 효과를 내지 못한 경우가 있다고 함..ㅠ
- 이에 대해 더 궁금한 점은 패트릭형님의 답변을 보세용.
3. Training 최적화
3.1 Architecture design과 batch size를 8의 배수로 설정
기존의 network보다 input, output channel을 1씩 줄이고 batch size는 1을 올려서 실험하였습니다. (이에 해당하는 모델들은 아래 표에서 rec model이라고 명명하고 기존 모델을 base model이라고 칭하겠습니다.) 일반적으로 생각하면 batch가 클수록 channel수가 작을수록(model이 작을수록) time cost가 줄어야 하지만 해당 최적화 방법에 근거하면 batch size와 channel수가 8의 배수가 아닐 경우 NVIDIA GPU 최적화가 되어있지 않아 있으므로 time cost가 늘어나게 됩니다.
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
base model | train: 20.4s test: 1.5s |
train: 57.9s test: 3.6s |
train: 96.9s test: 5.8s |
train: 19.9s test: 1.4s |
rec model | train: 22.7s test: 1.5s |
train: 59.9s test: 3.7s |
train: 103.0s test: 5.8s |
train: 20.0s test: 1.4s |
위 결과를 분석하면 다음과 같습니다.
- batch size가 커지고 channel수가 줄어듬에도 불구하고 training time이 느려지는 것을 확인 가능!
3.2 Mixed Precision Training 사용
import torch
scaler = torch.cuda.amp.GradScaler() # Training시에 생성
for data, label in data_iter:
optimizer.zero_grad()
with torch.cuda.amp.autocast(): # Mixed precision으로 operation들을 casting
outputs = model(data)
scaler.scale(loss).backward() # Loss를 scaling한 후에 backward진행
scaler.step(optimizer) # 원래 scale에 맞추어 gradient를 unscale하고 optimizer를 통한 gradient update
scaler.update() # 다음 iteration을 위해 scale update
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o mixed precision | train: 20.4s test: 1.4s |
train: 58.0s test: 3.6s |
train: 97.1s test: 5.8s |
train: 20.4s test: 1.5s |
w mixed precision | train: 10.4s test: 1.4s |
train: 25.9s test: 3.6s |
train: 43.9s test: 5.8s |
train: 22.2s test: 1.5s |
위 결과를 분석하면 다음과 같습니다. (해당 방법은 training time성능에만 영향을 미침)
- mixed precision사용 시 50%정도의 training time cost성능 향상을 보임
- MobileNetv2과 같은 depthwise conv가 있는 경우 또는 작은 model인 경우에는 time cost가 줄어들지 않고 늘어나는 것으로 추측
3.3 Optimizer로 weight를 update하기 전에 gradient을 None으로 설정
# gradient를 None으로 설정 (PyTorch >= 1.7)
optimizer.zero_grad(set_to_none=True)
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o gradient none | train: 20.5s test: 1.4s |
train: 58.0s test: 3.5s |
train: 97.2s test: 5.8s |
train: 20.3s test: 1.4s |
w gradient none | train: 20.0s test: 1.4s |
train: 57.2s test: 3.7s |
train: 95.9s test: 5.8s |
train: 19.6s test: 1.4s |
위 결과를 분석하면 다음과 같습니다. (해당 방법은 training time성능에만 영향을 미침)
- 모델이 작을 경우 time이 거의 줄지 않지만 모델이 커질수록 해당 설정으로 인한 time cost성능 향상 효과를 보임
3.4 Gradient accumulation 사용
for i, (input, target) in enumerate(dataloader):
output = model(features)
loss = criterion(output, target)
loss.backward()
# 매 2번의 iteration이 끝난 뒤에 weight를 update하여 batch size가 doubled되어 학습하는 효과를 줌
if (i+1) % 2 == 0 or (i+1) == len(dataloader):
optimizer.step() # weight update
optimizer.zero_grad(set_to_none=True)
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o gradient accumulation |
train: 20.2s test: 1.4s |
train: 57.5s test: 3.6s |
train: 96.2s test: 5.8s |
train: 19.8s test: 1.4s |
w gradient accumulation |
train: 19.9s test: 1.4s |
train: 56.9s test: 3.6s |
train: 95.3s test: 5.8s |
train: 19.4s test: 1.4s |
위 결과를 분석하면 다음과 같습니다. (해당 방법은 training time성능에만 영향을 미침)
- 모델에 커짐에 따라 조금의 time cost향상이 보임
- 위의 근거를 뒷받침하기에는 실험이 적으므로 더 큰 모델로 실험해볼 필요가 있음
4. Inference 최적화
4.1 Inference시에 gradient calculation 끄기
# inference코드에서 (decorator) torch.no_grad() 사용
@torch.no_grad()
def validation(model, input):
output = model(input)
return output
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o no_grad | test: 1.4s | test: 3.6s | test: 5.8s | test: 1.4s |
w no_grad | test: 1.4s | test: 3.6s | test: 5.7s | test: 1.4s |
위 결과를 분석하면 다음과 같습니다. (Inference에 대한 최적화이므로 test에 대한 수치만 표시함)
- time cost의 성능에 향상은 없다고 보임
- 아마.. memory cost측면에서 성능 개선이 있을 것으로 추측
5. CNN 최적화
5.1 torch.backends.cudnn.benchmark = True 사용
torch.backends.cudnn.benchmark = True
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o cudnn. benchmark |
train: 20.2s test: 1.4s |
train: 55.1s test: 3.6s |
train: 93.8s test: 5.7s |
train: 19.8s test: 1.4s |
w cudnn. benchmark |
train: 20.2s test: 1.4s |
train: 55.2s test: 3.6s |
train: 93.8s test: 5.8s |
train: 19.7s test: 1.4s |
위 결과를 분석하면 다음과 같습니다.
- cudnn.benchmark사용설정을 해도 time cost성능 효과는 없어 보입니다..ㅠㅠ
5.2 4D NCHW tensors에 대해 channel_last memory format를 사용
inputs = inputs.to(self.device, memory_format=torch.channels_last)
ResNet18 | ResNet50 | ResNet101 | MobileNetv2 | |
w/o channel_last | train: 10.4s test: 1.4s |
train: 26.0s test: 3.6s |
train: 46.3s test: 5.8 s |
train: 22.1s test: 1.4s |
w channel_last | train: 10.0s test: 1.4s |
train: 22.8s test: 3.6s |
train: 40.8s test: 5.8s |
train: 20.9s test: 1.4s |
위 결과를 분석하면 다음과 같습니다. (test때는 channel last를 안썻습니다..ㅋㅋ)
- 모델의 크기가 증가할수록 memory format의 설정이 time cost를 줄이는 데 효과적임
'AI Engineering > PyTorch' 카테고리의 다른 글
PyTorch training/inference 성능 최적화 (1/2) (0) | 2022.11.13 |
---|---|
Mixed Precision Training 이해 및 설명 (0) | 2022.11.02 |
[Torch2TFLite] Torch 모델 TFLite 변환 (feat. yolov5) (1) | 2022.06.26 |
PyTorch MultiGPU (2) - Single-GPU vs Multi-GPU (DistributedDataParallel) (0) | 2022.03.12 |
PyTorch MultiGPU (1) - Single-GPU vs Multi-GPU (DataParallel) (0) | 2022.03.11 |