성능 최적화 팁
CPU vs GPU 성능 비교
섹션 제목: “CPU vs GPU 성능 비교”GPU가 항상 빠른 것은 아닙니다. 데이터 크기 와 연산 종류 에 따라 달라집니다.
import torchimport 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 데이터 전송은 느립니다. 이 비용을 숨기는 것이 핵심입니다.
DataLoader의 pin_memory와 non_blocking
섹션 제목: “DataLoader의 pin_memory와 non_blocking”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 연산 ...불필요한 CPU-GPU 왕복 피하기
섹션 제목: “불필요한 CPU-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 동기화혼합 정밀도 학습 (torch.cuda.amp)
섹션 제목: “혼합 정밀도 학습 (torch.cuda.amp)”기본 float32 대신 float16을 일부 연산에 활용하여 속도 향상 + 메모리 절감 을 동시에 얻습니다.
float32: 32비트 — 높은 정밀도, 많은 메모리float16: 16비트 — 낮은 정밀도, 절반 메모리, GPU에서 2~3배 빠름혼합: 순전파/손실은 float16, 파라미터 업데이트는 float32import torchimport torch.nn as nnfrom 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 추가 |
torch.compile() — 모델 컴파일
섹션 제목: “torch.compile() — 모델 컴파일”PyTorch 2.0에서 도입된 torch.compile() 은 모델을 컴파일하여 추가 최적화 를 적용합니다. 코드 한 줄로 상당한 속도 향상을 얻을 수 있습니다.
import torchimport 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 torchfrom 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()로 코드 한 줄 추가만으로 추가 속도 향상- 그래디언트 체크포인팅으로 메모리-속도 트레이드오프 조절
다음 장에서는 실제 학습 루프에서의 텐서 패턴과 전체 커리큘럼을 정리합니다.