콘텐츠로 이동

성능 최적화 팁

GPU가 항상 빠른 것은 아닙니다. 데이터 크기연산 종류 에 따라 달라집니다.

import torch
import time
def benchmark(size, device, n=100):
a = torch.randn(size, size, device=device)
b = torch.randn(size, size, device=device)
# GPU의 경우 워밍업 필요
if device == 'cuda':
torch.cuda.synchronize()
start = time.time()
for _ in range(n):
c = torch.matmul(a, b)
if device == 'cuda':
torch.cuda.synchronize() # GPU 연산 완료 대기
elapsed = time.time() - start
return elapsed / n * 1000 # ms 단위
device = 'cuda' if torch.cuda.is_available() else 'cpu'
for size in [32, 256, 1024, 4096]:
cpu_time = benchmark(size, 'cpu')
print(f"크기 {size}×{size}: CPU {cpu_time:.2f}ms", end="")
if torch.cuda.is_available():
gpu_time = benchmark(size, device)
speedup = cpu_time / gpu_time
print(f", GPU {gpu_time:.2f}ms (속도 향상: {speedup:.1f}x)")
else:
print()

일반적인 결론:

텐서 크기GPU 우위 여부이유
소형 (< 256×256)CPU가 유리하거나 동등GPU 초기화/전송 오버헤드
중형 (256~1024)GPU 점점 유리병렬화 효과 본격화
대형 (> 1024)GPU 압도적 우위수십~수백 배 빠름

CPU → GPU 데이터 전송은 느립니다. 이 비용을 숨기는 것이 핵심입니다.

from torch.utils.data import DataLoader, TensorDataset
dataset = TensorDataset(
torch.randn(10000, 784),
torch.randint(0, 10, (10000,))
)
# pin_memory=True: 페이지 고정 메모리 — GPU 전송 속도 향상
loader = DataLoader(
dataset,
batch_size=256,
pin_memory=True, # CPU → GPU 전송 속도 개선
num_workers=4, # 데이터 로딩을 별도 프로세스로
)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for x, y in loader:
# non_blocking=True: 전송과 연산을 겹쳐서 실행 (비동기)
x = x.to(device, non_blocking=True)
y = y.to(device, non_blocking=True)
# 이후 GPU 연산 ...
# 나쁜 예 — 매 반복마다 GPU → CPU 왕복
losses = []
for batch in loader:
x = batch[0].to(device)
loss = model(x).sum()
losses.append(loss.item()) # .item()은 GPU → CPU 동기화 발생
# 좋은 예 — GPU에 누적 후 마지막에 한 번만 CPU로
total_loss = torch.tensor(0.0, device=device)
for batch in loader:
x = batch[0].to(device)
loss = model(x).sum()
total_loss += loss # GPU에서 누적
print(total_loss.item()) # 마지막에 한 번만 CPU 동기화

기본 float32 대신 float16을 일부 연산에 활용하여 속도 향상 + 메모리 절감 을 동시에 얻습니다.

float32: 32비트 — 높은 정밀도, 많은 메모리
float16: 16비트 — 낮은 정밀도, 절반 메모리, GPU에서 2~3배 빠름
혼합: 순전파/손실은 float16, 파라미터 업데이트는 float32
import torch
import torch.nn as nn
from torch.cuda.amp import autocast, GradScaler
model = nn.Linear(1024, 1024).cuda()
optimizer = torch.optim.Adam(model.parameters())
scaler = GradScaler() # float16의 언더플로우 방지를 위한 스케일러
x = torch.randn(256, 1024, device='cuda')
y = torch.randn(256, 1024, device='cuda')
for step in range(5):
optimizer.zero_grad()
# autocast 블록 내에서 float16 연산 자동 적용
with autocast():
pred = model(x)
loss = nn.MSELoss()(pred, y)
# 스케일된 역전파 (float16 언더플로우 방지)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
print(f"스텝 {step+1}: loss = {loss.item():.4f}")

혼합 정밀도 효과 요약:

항목float32혼합 정밀도 (AMP)
메모리 사용량기준약 50% 절감
학습 속도기준1.5~3배 향상
정확도기준거의 동일
코드 변경autocast + GradScaler 추가

PyTorch 2.0에서 도입된 torch.compile() 은 모델을 컴파일하여 추가 최적화 를 적용합니다. 코드 한 줄로 상당한 속도 향상을 얻을 수 있습니다.

import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(1024, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
# 컴파일 — 첫 실행 시 트레이싱으로 최적화 커널 생성
compiled_model = torch.compile(model)
x = torch.randn(128, 1024)
# 첫 실행은 느림 (컴파일 시간 포함)
output = compiled_model(x)
print(output.shape) # torch.Size([128, 10])
# 이후 실행부터 최적화된 커널 사용 → 빠름

컴파일 모드 옵션:

# default: 균형 잡힌 최적화
compiled = torch.compile(model, mode='default')
# reduce-overhead: 소형 모델, 오버헤드 최소화
compiled = torch.compile(model, mode='reduce-overhead')
# max-autotune: 대형 모델, 최대 성능 (컴파일 시간 길어짐)
compiled = torch.compile(model, mode='max-autotune')

메모리 최적화 — 그래디언트 체크포인팅

섹션 제목: “메모리 최적화 — 그래디언트 체크포인팅”

매우 깊은 모델에서 중간 활성화값을 모두 저장하면 메모리 부족이 발생합니다. 그래디언트 체크포인팅 은 중간값을 저장하지 않고, 역전파 시 필요한 부분만 재계산합니다.

import torch
from torch.utils.checkpoint import checkpoint
class DeepBlock(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
)
def forward(self, x):
return self.layers(x)
block = DeepBlock()
x = torch.randn(64, 512, requires_grad=True)
# 일반 순전파 — 모든 중간 활성화값 저장
output_normal = block(x)
# 체크포인팅 — 중간값 저장 안 함, 역전파 시 재계산
output_ckpt = checkpoint(block, x, use_reentrant=False)
print(output_normal.shape) # torch.Size([64, 512])
print(output_ckpt.shape) # torch.Size([64, 512])

트레이드오프:

항목일반 순전파그래디언트 체크포인팅
메모리높음 (모든 활성화값 저장)낮음 (일부만 저장)
계산 시간기준약 30~40% 증가 (재계산 때문)
사용 시기메모리 여유 있을 때배치 크기를 늘리고 싶을 때

  • GPU는 대형 텐서 일수록 압도적으로 빠름 — 소형 연산은 CPU가 나을 수도 있음
  • pin_memory=True + non_blocking=True 로 CPU-GPU 전송 병목 최소화
  • autocast + GradScaler 로 혼합 정밀도 학습 — 메모리 50% 절감, 속도 최대 3배
  • torch.compile() 로 코드 한 줄 추가만으로 추가 속도 향상
  • 그래디언트 체크포인팅으로 메모리-속도 트레이드오프 조절

다음 장에서는 실제 학습 루프에서의 텐서 패턴과 전체 커리큘럼을 정리합니다.