1. 강의 내용
Multi-GPU 학습 (최성철 교수님)
1) 개념정리
Multi-GPU는 1개의 GPU가 아닌 여러개의 GPU를 말합니다. Node(system)은 1대의 컴퓨터를 이야기하고 GPU는 Node안에 속합니다. 다중 GPU에 학습을 분산하는 두 가지 방법으로는 모델을 나누기 & 데이터를 나누기가 있습니다.
2) Model parallel
- 모델을 나누는 것은 생각보다 예전부터 썼음 (alexnet)
- 모델의 병목, 파이프라인의 어려움 등으로 인해 모델 병렬화는 고난이도 과제
위 그림은 alexnet으로, 위 아래의 구조가 교차되는 지점이 각 GPU간의 병렬적인 처리를 지원하기 위해 적용한 것 입니다.
위 그림에서 첫번째 그림을 보면 GPU간에 번갈아가며 작업이 진행되기 때문에 파이프라인이 만들어져 있지 않기 때문에 병렬화라고 볼 수 없습니다. 두번째 그림이 좋은 병렬화라고 볼 수 있는데, 위 아래의 GPU에서 처리되는 과정이 겹치기 때문입니다.
class ModelParallelResNet50(ResNet):
def __init__(self, *args, **kwargs):
super(ModelParallelResNet50, self).__init__(
Bottleneck, [3, 4, 6, 3], num_classes=num_classes, *args, **kwargs)
self.seq1 = nn.Sequential(
self.conv1, self.bn1, self.relu, self.maxpool, self.layer1, self.layer2
).to('cuda:0') # 첫번째 모델을 cuda 0에 할당
self.seq2 = nn.Sequential(
self.layer3, self.layer4, self.avgpool,
).to('cuda:1')# 두번째 모델을 cuda 1에 할당
self.fc.to('cuda:1')
def forward(self, x): # 두 모델 연결
x = self.seq2(self.seq1(x).to('cuda:1'))
return self.fc(x.view(x.size(0), -1))
3) Data parallel
데이터를 나눠 GPU에 할당후 결과의 평균을 취하는 방법으로 minibatch 수식과 유사한데 한번에 여러 GPU에서 수행합니다.
PyTorch에서는 DataParallel, DistributedDataParallel 두 가지 방식을 제공합니다.
- DataParallel – 단순히 데이터를 분배한후 평균을 취함(위 그림처럼 하나의 GPU에 모아서 평균)
→ GPU 사용 불균형 문제 발생, 부담이 큰 GPU에 맞춰 Batch 사이즈를 감소시켜야합니다. (한 GPU가 병목), GIL - DistributedDataParallel – 하나의 GPU에 모으는 과정이 없으며 각 CPU마다 process 생성하여 개별 GPU에 할당
→ 기본적으로 DataParallel로 하나 개별적으로 연산의 평균을 냄
# DataParallel
parallel_model = torch.nn.DataParallel(model) # 이게 끝
predictions = parallel_model(inputs) # Forward pass on multi-GPUs
loss = loss_function(predictions, labels) # Compute loss function
loss.mean().backward() # Average GPU-losses + backward pass
optimizer.step() # Optimizer step
predictions = parallel_model(inputs) # Forward pass with new parameters
# DistributedDataParallel
train_sampler = torch.utils.data.distributed.DistributedSampler(train_data) # Sampler를 사용해서 넣어줌
shuffle = False
pin_memory = True # 메모리(DRAM)에 데이터를 바로바로 올릴 수 있도록 절차를 간소하게 데이터를 저장하는 방식
trainloader = torch.utils.data.DataLoader(train_data, batch_size=20, shuffle=True
pin_memory=pin_memory, num_workers=3,
shuffle=shuffle, sampler=train_sampler)
다음으로 main 함수에 적용하는 과정에서 Python 멀티프로세싱 기능의 map reduce 개념을 활용해 각 GPU에 mapping 시켜줍니다.
def main():
n_gpus = torch.cuda.device_count()
torch.multiprocessing.spawn(main_worker, nprocs=n_gpus, args=(n_gpus, ))
def main_worker(gpu, n_gpus):
image_size = 224
batch_size = 512
num_worker = 8
epochs = ...
# 데이터를 잘라주는 기준인 batch_size, num_worker를 gpu의 개수만큼 나눠줌
batch_size = int(batch_size / n_gpus)
num_worker = int(num_worker / n_gpus)
# 멀티프로세싱 통신 규약 정의
torch.distributed.init_process_group(
backend='nccl’, init_method='tcp://127.0.0.1:2568’, world_size=n_gpus, rank=gpu)
model = MODEL
torch.cuda.set_device(gpu)
model = model.cuda(gpu)
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[gpu]) # Distributed DataParallel 정의
Hyperparameter Tuning (최성철 교수님)
성능은 모델, 데이터, Hyperparameter Tuning 등에 따라 좌우되는데, Hyperparameter Tuning은 마지막에 마른 수건을 짜는 느낌으로 시도하게 됩니다.
learning rate, 모델의 크기, optimizer 등과 같은 모델 스스로 학습하지 않는 값은 사람이 지정하게 됩니다. Hyperparameter Tuning은 이러한 값들을 조정해 주는 것으로 이것에 의해 값이 크게 좌우 될 때도 있지만 요즘은 크게 효과가 있지는 않습니다.
가장 기본적인 방법으로 grid, random search가 있으며 최근에는 베이지안 기반 기법들이 주도하고 있습니다.
1) Ray
- ML/DL의 병렬 처리를 위해 개발된 모듈
- multi-node multi processing 지원 모듈
- 기본적으로 현재의 분산병렬 ML/DL 모듈의 표준
- Hyperparameter Search를 위한 다양한 모듈 제공
data_dir = os.path.abspath("./data")
load_data(data_dir)
# config 에 search space 지정
config = {
"l1": tune.sample_from(lambda _: 2 ** np.random.randint(2, 9)),
"l2": tune.sample_from(lambda _: 2 ** np.random.randint(2, 9)),
"lr": tune.loguniform(1e-4, 1e-1),
"batch_size": tune.choice([2, 4, 8, 16])
}
# 학습 스케줄링 알고리즘 지정
# ASHAScheduler: 알고리즘이 실행되며 중간중간 loss값이 안나와 의미없다고 생각되는 metric을 쳐냄
scheduler = ASHAScheduler(
metric="loss",
mode="min",
max_t=max_num_epochs,
grace_period=1,
reduction_factor=2)
# 결과 출력 양식 지정
reporter = CLIReporter(
# parameter_columns=["l1", "l2", "lr", "batch_size"],
metric_columns=["loss", "accuracy", "training_iteration"])
# 병렬 처리 양식으로 학습
# partial: 데이터를 쪼개 가가 GPU에 넘겨줌
# resources_per_trial 한 번 trial 시 사용할 수 있는 gpu와 cpu의 개수
# train_cifar: 학습 시 실행될 함수
result = tune.run(
partial(train_cifar, data_dir=data_dir),
resources_per_trial={"cpu": 2, "gpu": gpus_per_trial},
config=config,
num_samples=num_samples,
scheduler=scheduler,
progress_reporter=reporter)
PyTorch Troubleshooting (최성철 교수님)
코딩을 하다보면 여러 문제에 직면하는데, 대표적으로 OOM(Out Of Memory)에러가 있습니다. 해당 에러가 발생할 경우 보통 batch size를 줄이는 방식으로 해결합니다. 이외에도 다양한 문제가 발생하는데, 이에 대한 Troubleshooting 방식들이 있습니다.
1) GPUUtil
- nvidia-smi 처럼 GPU의 상태를 보여주는 모듈
- Colab은 환경에서 GPU 상태 보여주기 편함
- iter마다 메모리가 늘어나는지 확인
!pip install GPUtil
import GPUtil
GPUtil.showUtilization()
'''
------------------
| 0 | 2% | 6% |
| 1 | 0% | 90% |
| ID | GPU | MEM |
------------------
'''
2) torch.cuda.empty_cache()
- 사용되지 않은 GPU상 cache를 정리
- 가용 메모리를 확보
- del 과는 구분이 필요
- reset 대신 쓰기 좋은 함수
import torch
from GPUtil import showUtilization as gpu_usage
print("Initial GPU Usage")
gpu_usage()
'''
Initial GPU Usage
| ID | GPU | MEM |
------------------
| 0 | 0% | 0% |
| 1 | 0% | 0% |
| 2 | 0% | 0% |
| 3 | 0% | 0% |
'''
tensorList = []
for x in range(10):
tensorList.append(torch.randn(10000000,10).cuda())
print("GPU Usage after allcoating a bunch of Tensors")
gpu_usage()
'''
GPU Usage after allcoating a bunch of Tensors
| ID | GPU | MEM |
------------------
| 0 | 0% | 40% |
| 1 | 0% | 0% |
| 2 | 0% | 0% |
| 3 | 0% | 0% |
'''
del tensorList
# tensorList를 삭제하면 garbage collector가 메모리를 확보할 때 해당 메모리를 사용할 수 있습니다.
print("GPU Usage after deleting the Tensors")
gpu_usage()
'''
GPU Usage after deleting the Tensors
| ID | GPU | MEM |
------------------
| 0 | 0% | 40% |
| 1 | 0% | 0% |
| 2 | 0% | 0% |
| 3 | 0% | 0% |
'''
# empty_cache가 위의 garbage collector기능을 강제해 gpu 내부의 남는 메모리를 가져옵니다.
print("GPU Usage after emptying the cache")
torch.cuda.empty_cache()
gpu_usage()
'''
GPU Usage after emptying the cache
| ID | GPU | MEM |
------------------
| 0 | 0% | 5% |
| 1 | 0% | 0% |
| 2 | 0% | 0% |
| 3 | 0% | 0% |
'''
3) trainning loop에 tensor로 축적 되는 변수는 확인할 것
- tensor로 처리된 변수는 GPU 상에 메모리 사용
- 해당 변수가 loop 안에 연산에 있을 때 GPU에 computational graph를 생성(메모리 잠식)
- 1-d tensor의 경우 python 기본 객체로 변환하여 처리할 것
total_loss = 0
for i in range(10000):
optimizer.zero_grad()
output = model(input)
loss = criterion(output)
loss.backward()
optimizer.step()
total_loss += loss # 이 과정에서 더할때만 필요한 loss가 loss1, loss2,... 계속 쌓임
# iter_loss를 파이썬의 기본 객체로 변경해주면 GPU에 쌓이는 경우가 없어짐.
# iter_loss.item, float(iter_loss) 등으로 기본객체로 변경
total_loss = 0
for x in range(10):
iter_loss = torch.randn(3,4).mean()
iter_loss.requires_grad = True
total_loss += iter_loss # iter_loss.item, float(iter_loss)
4) del 명령어를 적절히 사용하기
- 필요가 없어진 변수는 적절한 삭제가 필요함
- python의 메모리 배치 특성상 loop 이 끝나도 메모리를 차지함(ex. for loop의 i)
5) 가능 batch 사이즈 실험해보기
- 학습시 OOM 이 발생했다면 batch 사이즈를 1로 해서 실험해보기
oom = False
try:
run_model(batch_size)
except RuntimeError: # Out of memory
oom = True
if oom:
for _ in range(batch_size):
run_model(1)
6) torch.no_grad()
- Inference 시점에서는 torch.no_grad() 구문을 사용
- backward pass 으로 인해 쌓이는 메모리에서 자유로움
# no_grad를 사용하면 backward가 일어나더라도 메모리를 추가적으로 만들지 않음
with torch.no_grad():
for data, target in test_loader:
output = network(data)
test_loss += F.nll_loss(output, target, size_average=False).item()
pred = output.data.max(1, keepdim=True)[1]
correct += pred.eq(target.data.view_as(pred)).sum()
7) 그외 방식
- colab에서 너무 큰 사이즈는 실행하지 말 것 (linear, CNN, LSTM)
- CNN의 대부분의 에러는 크기가 안 맞아서 생기는 경우 (torchsummary 등으로 사이즈를 맞출 것)
- tensor의 float precision을 16bit로 줄일 수도 있음
2. 과제 수행 과정 / 결과물 정리
선택과제를 마무리하지 못했습니다..ㅠ
3. 피어세션 정리
https://hackmd.io/LjDJQUpDTVCSQw2FqRwc1w
4. 학습 회고
이번주의 강의를 모두 거치며 파이토치에 대해 제대로 알게 되었습니다. 특히 이번주 두개의 필수과제가 너무 알차서 감탄하며 풀었습니다! 또 거의 코드로 이루어져있어 따로 정리하진 않았지만 다양하고 테크니컬한 시각화 강의를 들으며 수업자료만으로도 앞으로 구글링해서 시각화하는 일이 거의 사라질 것 같습니다.
'네이버 부스트캠프 > LEVEL-1' 카테고리의 다른 글
[부스트캠프][P-stage][WK04 / Day2] Image Classification 2 (0) | 2021.08.27 |
---|---|
[부스트캠프][P-stage][WK04 / Day1] Image Classification 1 (0) | 2021.08.27 |
[부스트캠프][WK03 / Day13] PyTorch 구조 학습하기 2 (0) | 2021.08.20 |
[부스트캠프][WK03 / Day12] PyTorch 구조 학습하기 (0) | 2021.08.18 |
[부스트캠프][WK03 / Day11] PyTorch 기본 (1) | 2021.08.18 |