가장 먼저 이해해야 할 형태

분산 학습의 출발점은 data parallel이다. 각 GPU가 같은 모델 복사본을 갖고 서로 다른 mini-batch를 처리한 뒤, backward가 끝나면 gradient를 평균내고 optimizer step을 수행한다.

겉으로는 간단하다.

  1. 모델을 모든 GPU에 복제한다
  2. 입력 배치를 rank마다 나눈다
  3. 각 rank가 forward/backward를 수행한다
  4. gradient를 all-reduce로 모은다
  5. 각 rank가 동일한 optimizer step을 수행한다

이 구조가 중요한 이유는, 이후의 기법들이 거의 다 이 기본형의 약점을 보완하기 위해 등장했기 때문이다.

왜 synchronous SGD를 기본으로 쓰는가?

실무에서 대부분의 학습은 비동기보다 동기식 업데이트를 기본으로 한다. 이유는 단순하다. 모든 rank가 같은 step에서 같은 파라미터 상태를 공유하면 디버깅과 재현성이 훨씬 낫다. 반대로 비동기 업데이트는 stale gradient 문제와 수렴 불안정성이 빨리 나타난다.

특히 LLM처럼 이미 학습 자체가 민감한 경우에는, 통신을 조금 아끼겠다고 optimizer 동작을 더 복잡하게 만드는 쪽이 오히려 손해일 때가 많다.

data parallel이 좋은 이유

data parallel의 장점은 분명하다.

  • 구현이 가장 단순하다
  • 모델 코드를 크게 바꾸지 않아도 된다
  • 각 GPU가 동일한 계산 그래프를 실행하므로 디버깅이 상대적으로 쉽다
  • 배치가 충분히 크면 계산량을 비교적 잘 분산할 수 있다

그래서 모델이 한 GPU에 충분히 들어가고, 네트워크가 너무 느리지 않다면 data parallel은 가장 먼저 시도할 전략이다.

그러나 두 가지 비용이 곧바로 드러난다

1. 모델 복제 비용

모든 rank가 전체 모델을 들고 있으므로 파라미터, gradient, optimizer state가 그대로 복제된다. Adam 계열 optimizer를 쓰면 파라미터보다 optimizer state가 더 큰 부담이 되기도 한다.

예를 들어 파라미터가 P byte라면 대략 다음이 함께 따라온다.

  • parameters: P
  • gradients: P
  • optimizer state: 대개 2P 이상

즉 data parallel은 계산은 잘 나누지만 메모리는 잘 나누지 못한다.

2. gradient synchronization 비용

각 rank의 backward가 끝난 뒤에는 gradient를 모아야 한다. 이때 발생하는 all-reduce는 모델이 커질수록 비싸진다. 특히 inter-node 구간이 느리면 계산은 빨리 끝났는데 통신 때문에 다음 step으로 못 넘어가는 상황이 쉽게 생긴다.

global batch size는 왜 중요할까?

rank 수를 늘리면 global batch size도 커진다.

global_batch = micro_batch_per_rank * num_ranks * grad_accum_steps

이 값이 커지면 throughput은 좋아질 수 있지만, optimizer와 학습률 스케줄은 다시 조정해야 한다. 단순히 GPU를 더 붙였다고 항상 더 잘 학습되는 것은 아니다.

실무에서 보는 첫 번째 판단 기준

data parallel을 볼 때는 다음을 먼저 확인하는 편이 좋다.

  • 모델이 한 GPU 메모리에 충분히 들어가는가
  • all-reduce가 backward보다 짧은가
  • global batch size를 키워도 학습이 안정적인가
  • dataloader와 input pipeline이 rank 수 증가를 따라가는가

이 질문에서 하나라도 크게 흔들리면 다른 전략이 필요해진다.

다음 글에서는 data parallel의 핵심 통신인 all-reduce를 더 깊게 본다. ring all-reduce가 왜 많이 쓰이는지, 그리고 통신 비용을 어떻게 읽어야 하는지를 정리한다.