<aside> 🔥
이 내용은 반드시 컴퓨터의 소수점 표현을 이해하고 읽는 것을 추천드립니다.
이제부터 GPU에 어떤 데이터를 올라가는지 차근차근 알아봅시다!
<aside> 🔥
간단하게 모델 파라미터 사이즈에 따른 메모리 사용량을 측정하는 방식을 알아봅니다.
</aside>
모델 파라미터 사이즈는 곧 숫자의 갯수를 의미하므로, 파라미터 사이즈를 안다면 모델이 사용하는 GPU를 계산해볼 수 있습니다. Solar 10.7B 모델의 GPU 사용량을 계산해봅시다.
모델 사이즈 개수 x 2byte
가 필요한 총 메모리의 양입니다.<aside> 🔥
학습에서 사용하는 GPU 메모리를 측정하는 방식을 알아봅니다.
</aside>
학습에서 GPU 메모리가 추가로 들어가는 작업은 다음과 같습니다.
즉, 모델 학습에는 모델 파라미터(N) + forward + backward(optimizer) + gradient = 4N의 메모리가 필요합니다. Solar 10.7B를 학습하기 위해서는 10.7 x 2 x 4 = 85.6GB 이상의 메모리를 필요로 합니다. 또한, 학습 데이터도 gpu 할당이 필요하므로 사실상 학습에 필요한 GPU는 이보다 훨씬 많다고 보시면 됩니다.
코드 예시
def estimate_memory_usage(
model,
max_sequence_length: int = 1024,
batch_size: int = 1,
) -> float:
dtype = model.dtype
type2byte = {"torch.float32": 4, "torch.float16": 2, "torch.bfloat16": 2, "torch.uint8": 1}
byte = type2byte[str(dtype)]
print(f"Model dtype: {dtype}")
total_param_size = 0
total_gradient_size = 0
for k, p in model.named_parameters():
total_param_size += p.numel() * byte
if p.requires_grad:
total_gradient_size += p.numel() * byte
print(f"Parameter size : {total_param_size:,}")
print(f"Model size : {total_param_size/1024**3:.2f} GB")
print(f"Gradients : {total_gradient_size/1024**3:.2f} GB")
print(f"Optimizer : {2 * total_gradient_size/1024**3:.2f} GB")
activation_size = batch_size * max_sequence_length * model.config.hidden_size * model.config.num_hidden_layers * byte
print(f"Activation : {activation_size/1024**3:.2f} GB")
total_gpu_usage = (total_param_size + total_gradient_size + 2 * total_gradient_size + activation_size) / 1024**3
print(f"Total GPU memory usage : {total_gpu_usage:.2f} GB")
return total_gpu_usage
estimate_memory_usage(model)
------------------------------------------------------------------------
Model dtype: torch.float16
Parameter size : 21,987,336,192
Model size : 20.48 GB
Gradients : 20.48 GB
Optimizer : 40.95 GB
Activation : 0.38 GB
Total GPU memory usage : 82.28 GB
GPU를 1개만 사용해서 큰 모델을 학습시키고자 한다면 위에서 알아본 메모리를 사용하는 다양한 부분에서 메모리를 절약하는 방식을 적용해야 합니다. 이제부터 메모리를 어떻게 절약할 수 있을지 알아봅니다.
<aside> 🔥
그래디언트 누적은 모델을 학습시킬 때 각 배치마다 모델을 업데이트하지 않고 여러 배치의 학습 데이터를 연산 후 모델을 업데이트해서 마치 더 큰 배치 크기를 사용하는 것 같은 효과를 내는 방법을 말한다.
</aside>
학습 데이터에서 batch size를 키우면 순전파 상태 저장(Activation)에 필요한 메모리가 증가하게 됩니다. 따라서, batch size를 줄일 수 있다면 메모리를 절약할 수 있습니다.
업데이트 설정을 4로 두면 loss를 4 step으로 나누고 4번 걸쳐서 loss update를 진행합니다.
코드 예시
gradient_accumulation_steps = 4
for step, batch in enumerate(train_dataloader, start=1):
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
loss = loss / gradient_accumulation_steps
loss.backward()
if step % gradient_accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
장점 : gradient accumulation을 하면 학습을 많이 하는 효과가 발생하여 성능이 향상된다고 알려져 있습니다.
단점 : 여러 번의 계산 과정이 필요하므로 학습 시간이 늘어나게 됩니다.
<aside> 🔥
모델 업데이트를 위해서 optimizer에서 역전파를 계산하는데, 이때 역전파 계산을 하기 위해서는 순전파의 정보를 알고 있어야 합니다. 그래디언트 체크포인팅은 순전파의 정보를 모두 저장하는 방식이 아닌 중간중간 포인트만 저장하는 방식으로 메모리를 효율적으로 사용합니다.
</aside>
GPU 자원이 충분하다면 하나의 GPU에 모델 학습을 진행하는 것이 아닌 여러 GPU에 모델 학습을 진행하는 것이 더 효율적일 것입니다. 데이터 혹은 모델을 여러 GPU에 올리는 방법에 대해 알아봅니다.
<aside> 🔥
여러 GPU에 각각 모델을 올리고 학습 데이터를 병렬로 처리해 학습 속도를 높이는 방식입니다.
</aside>
<aside> 🔥
여러 GPU에 하나의 모델을 쪼개서 올리는 방식입니다. 주로 큰 모델을 학습시킬때 사용합니다.
</aside>
모델을 vertical하게 쪼개냐 horizontal하게 쪼개냐에 따라서 Pipeline parallelism(파이프라인 병렬화), tensor parallelism(텐서 병렬화)으로 나뉩니다.
<aside> 🔥
하나의 모델을 각 레이어 단위로 각각의 GPU에 쪼개서 올리는 방식입니다.
</aside>
<aside> 🔥
한층의 레이어도 전부 나눠서 GPU에 올리는 방식입니다.
</aside>
<aside> 🔥
데이터 병렬화에서 동일 모델을 여러 GPU에 복사해서 올리는 방식을 사용합니다. 이 경우 중복되는 것들이 존재하기 때문에 효율적이지 않습니다. 따라서, 데이터 병렬화를 텐서 병렬화처럼 수행하는 방식을 Zero Redundancy Opimizer(ZeRO)라고 합니다.
</aside>
참고 : https://huggingface.co/docs/accelerate/en/usage_guides/deepspeed
모델을 전부 업데이트하지 않고 일부만 업데이트 하는 방식인 LoRA에 대해서 알아봅니다.
<aside> 🔥
</aside>
<aside> 🔥
</aside>
<aside> 🔥
</aside>