
"로컬에서 잘 돌던 코드가 GPU 서버에 올리니 터진다"는 경험, 한 번쯤 있지 않은가?
들어가며
PyTorch로 딥러닝 모델을 개발하다 보면, 모델 아키텍처 자체보다 '학습 환경 설정(Boilerplate Code)' 때문에 스트레스를 받는 순간이 반드시 온다.
"로컬(CPU)에서 짤 때는 잘 돌아갔는데, 서버(GPU)에 올리니 에러가 나네?"
"단일 GPU 코드를 멀티 GPU(DDP)로 바꾸려니 코드를 다 뜯어고쳐야 하네?"
이런 하드웨어 의존적인 코드를 획기적으로 줄여주는 Hugging Face Accelerate 라이브러리를 소개한다. 기존 PyTorch 코드와 비교하여 얼마나 생산성이 높아지는지 살펴보자.
The "Before": 순수 PyTorch의 고통
PyTorch만 사용하여 멀티 GPU 환경과 Mixed Precision(FP16) 학습을 구현하려면, 우리는 비즈니스 로직(모델 학습)과 상관없는 코드를 덕지덕지 붙여야 한다.
문제점
Device 관리의 귀찮음
모든 텐서와 모델에 .to(device)를 명시해야 한다.
복잡한 DDP 설정
DistributedDataParallel 래핑, Sampler 설정, 프로세스 그룹 초기화 등 설정이 복잡하다.
AMP(Mixed Precision) 코드
GradScaler, autocast 등을 직접 관리해야 한다.
😣 기존 코드 예시 (Boilerplate의 늪)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler
def train():
# 1. 하드웨어 설정 (복잡함)
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
dist.init_process_group(backend='nccl')
device = torch.device("cuda", local_rank)
# 2. 모델을 GPU로 이동 후 DDP 래핑
model = MyModel().to(device)
model = DDP(model, device_ids=[local_rank])
# 3. 데이터 로더에 Sampler 필수
sampler = DistributedSampler(dataset)
dataloader = DataLoader(dataset, sampler=sampler)
optimizer = optim.Adam(model.parameters())
# 4. Mixed Precision을 위한 Scaler 준비
scaler = torch.cuda.amp.GradScaler()
for batch in dataloader:
optimizer.zero_grad()
# 5. 데이터도 일일이 device로 이동
inputs, targets = batch
inputs, targets = inputs.to(device), targets.to(device)
# 6. Autocast 컨텍스트 매니저 사용
with torch.cuda.amp.autocast():
outputs = model(inputs)
loss = criterion(outputs, targets)
# 7. Scaler를 통한 역전파 및 스텝
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
보시다시피 학습 로직보다 환경 설정 코드가 더 길다. 만약 이 코드를 다시 CPU 환경에서 테스트하려면 if torch.cuda.is_available(): 같은 분기문을 수없이 넣어야 한다.
The "After": Accelerate의 우아함
Accelerate는 하드웨어 설정을 추상화한다. 개발자는 "어디서 실행될지" 고민하지 않고 "무엇을 실행할지"에만 집중하면 된다.
✨ Accelerate 코드 예시
from accelerate import Accelerator
def train():
# 1. Accelerator 객체 생성
# fp16, cpu, multi-gpu 등 환경을 알아서 감지
accelerator = Accelerator(mixed_precision="fp16")
device = accelerator.device # device 할당도 알아서
model = MyModel()
optimizer = optim.Adam(model.parameters())
dataloader = DataLoader(dataset) # Sampler 안 넣어도 됨!
# 2. Prepare: 마법의 메서드
# 모델, 옵티마이저, 데이터로더를 현재 하드웨어에 맞게 자동 변환
model, optimizer, dataloader = accelerator.prepare(
model, optimizer, dataloader
)
for batch in dataloader:
optimizer.zero_grad()
# 3. .to(device) 불필요 (자동 처리됨)
inputs, targets = batch
outputs = model(inputs)
loss = criterion(outputs, targets)
# 4. 역전파: 문법 통일
accelerator.backward(loss)
optimizer.step()
무엇이 바뀌었나?
.to(device) 삭제prepare() 메서드를 통과한 DataLoader는 배치 데이터를 자동으로 올바른 장치(GPU/TPU)에 올려준다.
scaler 삭제accelerator.backward(loss)가 내부적으로 Mixed Precision scaling을 알아서 처리한다.
단일 코드베이스
위 코드는 수정 없이 CPU, 싱글 GPU, 멀티 GPU, 심지어 TPU에서도 그대로 돌아간다.
실행 방법 (CLI)
코드 내에서 하드웨어를 지정하지 않았기 때문에, 실행 시점에 CLI로 환경을 주입한다.
설정 마법사 실행 (최초 1회)
accelerate config
# 질문: GPU 몇 개 쓸 거야? FP16 쓸 거야?
# → 답변하면 config 파일 생성됨
학습 실행
accelerate launch train.py
이제 파이썬 스크립트는 accelerate launch가 던져주는 환경 설정에 맞춰 유연하게 동작한다.
실전 사례: 팀 마이그레이션 경험
우리 팀에서 RT-DETR 학습 코드를 Accelerate로 마이그레이션한 경험을 공유한다.
Before: 하드웨어별 코드 분기
# 로컬 개발용
if torch.cuda.is_available():
device = torch.device("cuda")
model = model.to(device)
else:
device = torch.device("cpu")
# 멀티 GPU용 (별도 파일)
model = DDP(model, device_ids=[local_rank])
sampler = DistributedSampler(train_dataset)
로컬에서 테스트하고, 서버에 배포할 때마다 코드를 바꿔야 했다.
After: 통합 코드베이스
from accelerate import Accelerator
accelerator = Accelerator(
mixed_precision="fp16",
gradient_accumulation_steps=4
)
model = RTDETRModel()
optimizer = AdamW(model.parameters(), lr=1e-4)
train_loader = DataLoader(train_dataset, batch_size=8)
# 마법의 prepare
model, optimizer, train_loader = accelerator.prepare(
model, optimizer, train_loader
)
for epoch in range(num_epochs):
for batch in train_loader:
with accelerator.accumulate(model):
outputs = model(batch['images'])
loss = criterion(outputs, batch['targets'])
accelerator.backward(loss)
optimizer.step()
optimizer.zero_grad()
결과:
- 로컬(Mac M2): 그대로 실행 ✅
- 서버(RTX 4060 1장): 그대로 실행 ✅
- 서버(A100 4장): 그대로 실행 ✅
코드 한 줄도 안 바꿨다.
알아두면 좋은 기능들
Gradient Accumulation
배치 크기를 늘리지 않고 그래디언트만 누적할 수 있다.
accelerator = Accelerator(gradient_accumulation_steps=4)
for batch in train_loader:
with accelerator.accumulate(model): # 4번에 1번만 업데이트
outputs = model(batch)
loss = criterion(outputs, targets)
accelerator.backward(loss)
optimizer.step()
optimizer.zero_grad()
GPU 메모리 부족할 때 유용하다.
체크포인트 저장/로드
# 저장
accelerator.save_state(output_dir="./checkpoints")
# 불러오기
accelerator.load_state(input_dir="./checkpoints")
DDP 상태까지 알아서 저장해준다.
로깅
# 메인 프로세스에서만 로그 출력
if accelerator.is_main_process:
print(f"Epoch {epoch}, Loss: {loss.item()}")
# 또는
accelerator.print(f"Epoch {epoch}, Loss: {loss.item()}")
# 이건 자동으로 메인 프로세스에서만 출력됨
멀티 GPU 학습할 때 로그가 중복으로 찍히는 거 방지할 수 있다.
실제 성능 비교
우리 팀에서 RT-DETR 학습할 때 측정한 수치다.
개발 시간:
- Before: 로컬/서버 코드 분기 때문에 2~3시간 소요
- After: 통합 코드로 30분 단축
디버깅 시간:
- Before: DDP 에러 디버깅에 하루 날림
- After: 에러 없음 (Accelerate가 알아서 처리)
코드 라인 수:
- Before: 하드웨어 설정 코드 ~100줄
- After: Accelerator 설정 ~10줄
주의사항
커스텀 학습 루프에서만 사용
Trainer API(Hugging Face Transformers)를 쓰면 Accelerate가 이미 내장돼 있다. 커스텀 학습 루프를 짤 때만 직접 써야 한다.
디버깅 시
가끔 Accelerate 내부에서 에러가 나면 스택 트레이스가 복잡할 수 있다. 그럴 땐 ACCELERATE_DEBUG_MODE=1로 실행하면 자세한 로그를 볼 수 있다.
ACCELERATE_DEBUG_MODE=1 accelerate launch train.py
기존 코드 마이그레이션
한 번에 다 바꾸려고 하지 말고, 단계적으로 마이그레이션하라.
1단계: Accelerator 생성 + prepare만 적용
2단계: .to(device) 제거
3단계: scaler 제거
4단계: DDP 코드 제거결론: 왜 도입해야 하는가?
개발 팀 리더로서 Accelerate 도입을 추천하는 이유는 단순한 '편리함' 때문만은 아니다.
유지보수성 향상
하드웨어 종속 코드가 사라져 비즈니스 로직(모델링)이 명확해진다.
실험 속도 가속화
로컬(Mac/CPU)에서 짠 코드를 배포(A100/Multi-GPU) 할 때 코드를 수정할 필요가 없다.
실용적인 접근
Hugging Face 생태계의 도구지만, PyTorch의 네이티브 기능을 해치지 않고 얇은 래퍼(Wrapper)로 동작하여 디버깅도 용이하다.
복잡한 torch.distributed 문서와 씨름하는 시간을 줄이고, 모델 성능 최적화에 그 시간을 투자하라.
'AI · ML > Computer Vision' 카테고리의 다른 글
| YOLO만 쓰던 개발자가 RT-DETR을 선택한 이유 (1) | 2026.01.21 |
|---|---|
| YOLO26: 엣지 디바이스를 위한 차세대 객체 탐지 모델 (0) | 2026.01.19 |