"안 들어간다"를 정확히 이해하려면 GPU 메모리를 무엇이 차지하는지 알아야 한다. 파라미터만 세면 오판한다.
차지하는 것
설명
어떤 병렬이 줄여 주나
파라미터(가중치)
모델 그 자체
텐서/파이프라인 병렬, 파라미터 샤딩
그래디언트
파라미터와 같은 크기로 또 필요
그래디언트 샤딩
옵티마이저 상태
Adam 계열은 모멘텀 등 추가 상태를 더 든다
옵티마이저 상태 샤딩
활성값(activation)
배치·시퀀스 길이에 비례해 커짐
배치 축소, 활성값 체크포인팅
데이터 병렬은 이 중 아무것도 줄이지 않는다 — 각 GPU에 전체 복제본이 그대로 올라가기 때문이다. OOM에 데이터 병렬이 무력한 이유가 여기 있다.
메모리를 당장 줄이는 저비용 수단: 혼합 정밀도, 배치 축소 + 그래디언트 누적, 활성값 체크포인팅(연산을 더 하고 메모리를 아낌).
💡 관련 이론: 데이터 병렬의 수학적 근거는 미니배치 그래디언트의 선형성이다. 전체 배치의 그래디언트는 각 GPU가 계산한 부분 배치 그래디언트의 평균과 같다. 그래서 GPU마다 모델 복제본으로 부분 배치를 처리한 뒤 all-reduce로 그래디언트를 합쳐 평균하면, 한 대로 큰 배치를 처리한 것과 수학적으로 동등한 업데이트가 된다. 모델 병렬은 다르다. 연산 그래프 자체를 잘라 한 GPU의 출력(활성값)이 다음 GPU의 입력이 되므로, GPU 간 의존성과 통신 비용이 본질적으로 더 크다.
데이터 병렬은 가장 흔한 분산 방식이다. 각 GPU(혹은 장비)가 모델 전체 복제본을 갖고, 서로 다른 데이터 조각으로 forward/backward를 수행한 뒤 그래디언트를 모아 평균한다.
[GPU 0] 배치 조각 0 → 그래디언트 g0 ┐
[GPU 1] 배치 조각 1 → 그래디언트 g1 ├─ all-reduce → 평균 ḡ → 모든 GPU 동일 갱신
[GPU 2] 배치 조각 2 → 그래디언트 g2 ┘
from sagemaker.pytorch import PyTorchestimator = PyTorch( entry_point='train.py', instance_type='ml.p4d.24xlarge', instance_count=2, # 여러 장비 framework_version='2.0', py_version='py310', distribution={ # SageMaker 데이터 병렬 활성화 'smdistributed': {'dataparallel': {'enabled': True}} })
SageMaker Distributed Data Parallel (SMDDP)은 AWS 네트워크(EFA 등)에 최적화된 all-reduce를 제공해, 표준 PyTorch DDP보다 통신 효율이 좋다. 데이터 병렬을 늘리면 처리량이 거의 선형으로 증가하지만, 통신 오버헤드 때문에 무한히 선형은 아니다.
실무의 대규모 학습은 아래 축을 조합한다. 각 축이 "무엇을 쪼개고 무엇을 얻는지"가 시험의 구분 지점이다.
병렬 축
쪼개는 대상
얻는 것
통신 부담
데이터 병렬
배치(데이터)
처리량
스텝마다 그래디언트 all-reduce
텐서 병렬
한 레이어의 텐서 연산
GPU당 메모리 ↓
매우 큼 → 노드 안(NVLink)에서 쓴다
파이프라인 병렬
레이어 묶음(스테이지)
GPU당 메모리 ↓
스테이지 간 활성값 전달. 버블(유휴) 발생
샤딩(ZeRO/FSDP 계열)
파라미터·그래디언트·옵티마이저 상태
GPU당 메모리 ↓↓
필요할 때 파라미터를 모았다 흩는 통신
하이브리드 병렬 — 통신 비용을 토폴로지에 맞춘다 노드 0 [GPU0 GPU1 GPU2 GPU3] ← 노드 안: 텐서/파이프라인 병렬 (빠른 NVLink) │ EFA ← 노드 간: 데이터 병렬 (상대적으로 느린 네트워크, all-reduce만) │ 노드 1 [GPU0 GPU1 GPU2 GPU3]
개념: 샤딩은 "메모리와 통신의 교환"이다. 파라미터를 GPU마다 조각내 두면 평소 메모리는 크게 줄지만, 계산 직전에 필요한 조각을 모아야(gather) 하고 끝나면 다시 흩는다. 그래서 샤딩 단계를 올릴수록(옵티마이저 상태만 → +그래디언트 → +파라미터) 메모리는 더 줄고 통신은 더 늘어난다. "가장 강한 샤딩이 최선"이 아니라 모델이 들어가는 선에서 가장 약한 샤딩이 보통 가장 빠르다.
거대 모델을 쪼갤 때 핵심 트레이드오프는 "메모리 절약 vs 통신 비용"이다. 모델을 잘게 쪼갤수록 GPU당 메모리는 줄지만 GPU 간 활성값·파라미터 전송이 늘어 느려진다. 그래서 실무에서는 데이터 병렬·텐서 병렬·파이프라인 병렬·샤딩을 조합한 "하이브리드 병렬"을 쓴다. 예를 들어 노드 안에서는 텐서 병렬(빠른 NVLink), 노드 간에는 데이터 병렬(상대적으로 느린 네트워크)로 통신 패턴을 토폴로지에 맞춘다.
EFA (Elastic Fabric Adapter): 노드 간 저지연 고대역 네트워크. 분산 학습의 all-reduce 통신 병목을 줄인다. p4d/p5 같은 GPU 인스턴스에서 활성화.
FSx for Lustre: 고처리량 병렬 파일시스템. 대용량 학습 데이터를 S3보다 빠르게 GPU에 공급해 데이터 로딩 병목을 줄인다.
인스턴스 선택: 다중 GPU 단일 노드(예: p4d.24xlarge = 8 GPU)면 노드 내 NVLink로 빠르다. 노드를 여러 개 쓰면 EFA로 노드 간 통신.
S3 (원천 데이터)
└─ FSx for Lustre (고속 캐시) → 여러 학습 노드에 빠르게 공급
학습 노드들 ── EFA(저지연 네트워크) ── all-reduce / 모델 병렬 통신
병목이 어디냐에 따라 손대는 곳이 다르다.
병목 증상
진짜 원인
손댈 곳
GPU 활용률은 높은데 확장이 안 됨
노드 간 all-reduce 통신
EFA 지원 인스턴스, 노드 수 대신 노드당 GPU 늘리기
GPU 활용률이 낮고 들쭉날쭉
데이터 공급이 못 따라감
FSx for Lustre 캐시, 로더 워커 증가, 프리페치
매 학습 시작 때 수십 분 대기
File 모드로 대용량 전체 복사
FastFile/Pipe 모드 또는 FSx
장시간 학습이 중간에 통째로 날아감
노드 장애·스팟 중단
체크포인트를 S3에 주기적으로, 재개 로직
개념: 스케일링 효율(scaling efficiency). GPU를 N배 늘렸을 때 처리량이 N배가 되면 효율 100%다. 현실은 통신·동기화 때문에 항상 100% 미만이고, 노드를 늘릴수록 떨어진다. 그래서 "GPU 8장짜리 노드 1대"가 "GPU 2장짜리 노드 4대"보다 대체로 빠르다 — 같은 GPU 수라도 노드 안 통신(NVLink)이 노드 간 통신보다 훨씬 빠르기 때문이다. 먼저 노드를 키우고, 그다음에 노드를 늘린다가 순서다.
한 연구팀이 70억 파라미터 모델을 단일 ml.g5 인스턴스에서 학습하려다 GPU OOM에 부딪혔다. 데이터 병렬로 노드를 늘려도 복제본이 여전히 한 GPU에 안 들어가 소용없었다. SMP의 모델 병렬+샤딩으로 파라미터·옵티마이저 상태를 GPU 간에 분산하자 메모리에 들어갔고, 노드 간엔 EFA를, 데이터 공급엔 FSx for Lustre를 붙여 학습 시간을 크게 줄였다. 교훈: OOM은 GPU 추가(데이터 병렬)가 아니라 모델 병렬로 푼다.
모델 복제본 하나가 GPU 1장에 들어가는가? ├─ 들어간다 │ ├─ 그런데 느리다 / 데이터가 크다 → 데이터 병렬 (SMDDP·torch_distributed) │ └─ 데이터 공급이 못 따라간다 → FSx for Lustre + 로더 워커·프리페치 └─ 안 들어간다 (OOM) ├─ 조금 모자란다 → 혼합 정밀도 · 배치↓+그래디언트 누적 · 활성값 체크포인팅 ├─ 많이 모자란다 → 샤딩(ZeRO/FSDP) → 텐서/파이프라인 병렬 (SMP) └─ 노드 여러 대로도 커버 안 됨 → 하이브리드 병렬 + EFA
"더 빨리" → 데이터 병렬. "안 들어가서" → 모델 병렬. 이 한 줄이 시험 대응의 핵심이다.
OOM에 GPU를 추가하는 보기, 느린 학습에 모델 병렬을 켜는 보기는 모두 오답 패턴이다.
다음 글에서는 학습이 잘 돌아가는지, 어디서 느려지고 어디서 망가지는지 들여다보는 디버깅·프로파일링을 본다.