제2부 사전학습(Pretraining)
2.4 분산학습 I — 병렬화·ZeRO/FSDP
사전학습을 처음 수행해 보는 사람이 가장 먼저 부딪히는 장벽은 알고리즘이 아니라 계산이다. 2.3에서 학습 한 스텝을 해부할 때 조용히 전제한 가정이 하나 있었다 — 모델과 그 학습 상태가 GPU 한 장에 들어간다는 것. 이 가정은 놀랄 만큼 빨리 깨진다. 70억(7B) 파라미터 모델은 요즘 기준으로 작은 축인데, 그것조차 학습 상태를 전부 펼치면 80GB급 GPU 한 장을 넘어선다. 왜 그런지에는 신비로운 데가 없다 — 파라미터 하나가 메모리에서 몇 바이트를 차지하는지 세어 보면 곧바로 나온다.
이 장은 그 계산에서 시작한다. 무엇이 GPU를 넘치게 하는지 바이트 단위로 명시하고 나면, 이후의 모든 병렬화 전략 — 데이터 병렬, ZeRO/FSDP, 텐서 병렬, 파이프라인 병렬, 전문가 병렬 — 이 결국 "이 회계장부의 어느 항목을 어느 축으로 쪼개고, 그 대가로 어디서 통신을 무는가"의 변주임이 드러난다. 병렬화는 비용을 없애는 기술이 아니라 비용을 옮기는 기술이다. 메모리를 아끼면 통신이 늘고, 통신을 줄이면 유휴가 생기고, 유휴를 없애면 메모리가 증가한다. 이 맞바꿈의 지형을 손에 쥐는 것이 이 장의 목표다. 클러스터·인터커넥트·NCCL·체크포인팅 같은 하드웨어 쪽 구현은 2.5로 넘긴다 — 이 장은 "무엇을 왜 나누나", 다음 장이 "그 나눔을 어떤 하드웨어 위에 어떻게 얹나"다.
앞으로 나올 바이트·통신 숫자는 전부 개략 규모를 보여 주기 위한 개략값이다. 실제 값은 프레임워크·커널·정밀도·아키텍처·병렬 구성에 따라 변동하고, 특히 활성값 산식은 어텐션 구현(FlashAttention 여부)에 크게 좌우된다. 무게는 특정 숫자가 아니라 "무엇을 나누면 무엇이 줄고 무엇이 늘어나는가"의 방향에 있다.
2.4.1 모델 상태 — 파라미터 하나에 16바이트
학습 한 스텝이 소모하는 GPU 메모리는 성질이 다른 두 부분으로 나뉜다. 하나는 모델 상태(model states) — 파라미터, 그래디언트, 옵티마이저 상태 — 로, 배치·시퀀스와 무관하게 파라미터 수 $\Psi$ 에만 비례한다. 다른 하나는 잔여 상태(residual states) — 활성값, 임시 버퍼, 단편화 — 로, 배치·시퀀스·층 수에 비례해 변동한다. 둘은 공략법이 다르므로 따로 세야 한다. 먼저 예측하기 쉬운 모델 상태부터.
현대 대형 모델 학습은 거의 예외 없이 혼합정밀도(mixed precision)로 실행된다. 순전파·역전파의 부하가 큰 행렬곱은 BF16이나 FP16으로 빠르게 처리하되, 파라미터를 실제로 갱신하는 옵티마이저 단계는 FP32로 한다. 왜 갱신만 FP32인지가 이 회계의 핵심이라 확인하고 넘어갈 값어치가 있다. AdamW의 갱신량은 $-\eta\, m/(\sqrt{v}+\epsilon)$ 인데, 학습이 무르익으면 이 값이 파라미터 자체보다 몇 자릿수 작아진다. BF16은 가수부가 7비트뿐이라, 큰 수에 아주 작은 수를 더하면 작은 쪽이 반올림으로 전부 사라진다(swamping). 즉 BF16 파라미터에 갱신을 직접 더하면 학습 후반의 미세한 갱신이 소리 없이 버려져 모델이 더 나아지지 않는다. 그래서 정밀한 FP32 마스터 사본을 따로 두어 거기에 갱신을 누적하고, 매 스텝 그것을 BF16으로 내려 계산용 사본을 만든다.
Adam(정확히는 AdamW, 2.3)을 쓰면 파라미터마다 다음이 메모리에 상주한다.
| 항목 | 정밀도 | 파라미터당 바이트 | 왜 필요한가 |
|---|---|---|---|
| 모델 파라미터(계산용 사본) | BF16/FP16 | 2 | 순전파·역전파의 실제 행렬곱 |
| 그래디언트 | BF16/FP16 | 2 | 역전파 결과 |
| FP32 마스터 가중치 | FP32 | 4 | 갱신 누적(swamping 방지) |
| Adam 1차 모멘트 $m$ | FP32 | 4 | 모멘텀 |
| Adam 2차 모멘트 $v$ | FP32 | 4 | 적응 학습률(분산 추정) |
| 합 | 16 |
파라미터 하나당 16바이트다. 주목할 것은 옵티마이저 상태가 12바이트로 가장 큰 몫이라는 점 — 마스터 사본 4 + $m$ 4 + $v$ 4다. "Adam은 모멘트를 둘 들어서 메모리가 파라미터의 2배"라는 통설은 실제보다 작게 추정한 것이다. FP32 마스터 사본까지 세면 옵티마이저 관련 상태만 파라미터(BF16 2바이트)의 여섯 배다. 그리고 바로 이 12바이트가 뒤에 나올 ZeRO의 첫 표적이 된다.
$$M_{\text{model}} = \underbrace{2\Psi}_{\text{bf16 param}} + \underbrace{2\Psi}_{\text{bf16 grad}} + \underbrace{12\Psi}_{\text{Adam(fp32): }4+4+4} = 16\,\Psi \ \text{bytes}$$
숫자를 넣어 보자. $\Psi = 7\times 10^9$ 인 7B 모델이면(편의상 $1\,\text{GB}=10^9$ 바이트로 계산하며, 실제 GiB는 약 7% 작다):
$$M_{\text{model}}^{7\text{B}} = 16 \times 7\times10^9 = 112\times10^9\ \text{bytes} = 112\ \text{GB}$$
내역을 뜯으면 BF16 파라미터 14GB, BF16 그래디언트 14GB, FP32 마스터·$m$·$v$ 가 각각 28GB다. 활성값을 단 한 바이트도 세기 전에 이미 112GB이고, 이것이 첫 번째 장벽이다 — 80GB GPU 한 장에 모델 상태만으로 안 들어간다. 70B로 키우면 $16\times 70\times10^9 = 1120$ GB, 즉 파라미터를 갱신하기 위한 장부만으로 80GB GPU 열네 장 분량이 필요하다. 스케일링 법칙(2.2)이 "더 크게"를 요구할수록 이 장벽은 선형으로 높아진다.
이 16바이트는 가장 흔한 구성의 대표값이지 불변의 상수가 아니다. 그래디언트를 FP32로 누적하는 구현이면 2바이트가 더 붙고, 8비트 Adam(bitsandbytes)으로 옵티마이저 상태를 압축하면 12바이트가 3바이트 안팎으로 줄며, Adafactor나 Lion처럼 상태가 경량 옵티마이저를 쓰면 또 달라진다(2.3). SGD+모멘텀이면 옵티마이저 상태가 모멘텀 하나(4바이트)뿐이라 훨씬 경량이다. 그래도 "옵티마이저 상태가 무겁고, 파라미터 수에만 비례하며, 배치와 무관하다"는 구조는 어떤 구성에서도 유지된다. 그리고 메모리 산정에서 이 12바이트를 빠뜨리는 것 — 파라미터와 활성값만 세는 것 — 이 실무에서 가장 흔한 과소평가다. 그렇게 계산하면 실제 필요량의 절반 이하가 나온다.
2.4.2 활성값 — 배치가 키우는 실제 복병
모델 상태는 배치와 무관해 한 번 계산하면 끝이다. 골치는 활성값(activations)이다. 역전파(1.3)에서 각 층의 그래디언트를 계산하려면 순전파 때의 중간 결과 — 행렬곱 입력, 활성함수 입력, 어텐션 점수 등 — 를 유지하고 있어야 한다. 이 양이 배치 $b$ × 시퀀스 $s$ × 히든 $h$ × 층 수 $L$ 에 추가되어 폭증한다. 트랜스포머 한 층이 순전파 활성값으로 남기는 바이트는 대략(설명용·설정 의존):
$$M_{\text{act/layer}} \approx s\,b\,h\left(34 + 5\,\frac{a\,s}{h}\right)\ \text{bytes}$$
여기서 $a$ 는 어텐션 헤드 수이고, 계수에는 활성값을 2바이트로 저장한다는 가정이 이미 반영되어 있다. 앞 항 $34\,sbh$ 는 층 안 여러 텐서(QKV 투영 입력, 어텐션 출력, FFN 중간·활성함수 입력, 드롭아웃 마스크, LayerNorm 입력 등)의 합으로 시퀀스 $s$ 에 선형이다. 뒤 항은 풀어 쓰면 $5\frac{as}{h}\cdot sbh = 5\,a\,s^2 b$ 로, 어텐션 점수 행렬($s\times s$ 짜리가 헤드마다, 1.4.4)에서 오는 $s^2$ 제곱 항이다. 두 항의 성장 속도가 다르다는 것이 이 절의 전부다.
한 층씩 직접 따라가 보자. $h=4096$, $L=32$, $a=32$, $s=2048$, 마이크로배치 $b=1$ 로 두면 먼저 $sbh$ 를 구하고:
$$sbh = 2048 \times 1 \times 4096 \approx 8.39\times10^6$$
선형 항은 $34\,sbh = 34\times 8.39\times10^6 \approx 2.85\times10^8$ 바이트 $\approx 0.285$ GB다. 제곱 항의 계수는 $5\,as/h = 5\times32\times2048/4096 = 80$ 이므로 제곱 항은 $80\,sbh = 80\times 8.39\times10^6 \approx 6.71\times10^8$ 바이트 $\approx 0.671$ GB. 층당 활성값은 둘을 더해 약 0.956 GB, 32층 전체면:
$$M_{\text{act}} \approx 32 \times 0.956\ \text{GB} \approx 30.6\ \text{GB}\quad(b=1)$$
마이크로배치 하나인데도 활성값이 30GB다. 여기서 배치를 4로 키우면 활성값은 선형으로 늘어 약 122GB가 되어 모델 상태(112GB)를 넘어선다. 두 가지가 곧장 보인다. 첫째, 이 예에서 이미 제곱 항(0.671GB)이 선형 항(0.285GB)보다 크다. 시퀀스가 길어질수록 $s^2$ 항이 폭주하며 나머지를 압도하고, 이것이 롱컨텍스트 학습이 활성값에서 유독 비용이 큰 이유다(1.6). 둘째, 어텐션 구현이 이 그림 전체를 바꾼다. FlashAttention은 $s\times s$ 점수 행렬을 메모리에 전부 남기지 않고 블록 단위로 재계산하므로(1.4), 저 제곱 항 0.671GB가 대부분 사라진다. 그러면 층당 약 0.285GB, 32층에 약 9GB로 급감한다. 현대 학습 스택에서 FlashAttention이 사실상 표준인 이유의 큰 몫이 여기 있다.
결정적 차이는 통제 가능성이다. 모델 상태는 파라미터 수가 정하니 손댈 여지가 없지만, 활성값은 마이크로배치·시퀀스 길이·재계산·어텐션 구현으로 조절할 수 있는 유일한 큰 조정 변수다. 그래서 이후 전략도 둘로 갈린다 — 모델 상태는 ZeRO·텐서 병렬·파이프라인 병렬로 여러 GPU에 나누고, 활성값은 그 병렬화에 더해 재계산(2.4.11)과 FlashAttention과 시퀀스 병렬로 공략한다. 이 이분법을 놓치면 "ZeRO-3만 켜면 다 된다"는 흔한 오해에 빠진다. 곧 보겠지만 ZeRO는 활성값을 전혀 줄이지 않는다.
2.4.3 데이터 병렬 — 복제하고 그래디언트를 all-reduce
가장 오래되고 단순한 병렬화다. 모델 전체를 GPU마다 똑같이 복제해 두고, 전역 배치를 GPU 수만큼 분할해 각 GPU가 서로 다른 데이터를 입력한다. 한 스텝은 세 단계로 진행된다. 각 GPU가 자기 몫 데이터로 순전파·역전파를 독립적으로 수행해 로컬 그래디언트를 얻고, 모든 GPU의 그래디언트를 all-reduce로 평균 내 모두가 같은 평균 그래디언트를 갖게 하고, 각 GPU가 동일한 옵티마이저 스텝을 밟아 복제본들이 계속 똑같이 유지되게 한다.
all-reduce의 통신량을 정확히 봐야 뒤가 풀린다. 대역폭에 최적인 링 all-reduce는 두 단계로 나뉜다 — 먼저 reduce-scatter로 각 GPU가 전체 그래디언트의 서로 다른 $1/N$ 조각에 대한 최종 합을 나눠 갖고, 이어 all-gather로 그 조각들을 서로 순환 전달해 모두가 전체를 갖는다. 각 단계에서 GPU 하나가 주고받는 데이터는 대략 $\frac{N-1}{N}\Psi$ 로, $N$ 이 크면 $\Psi$ 에 수렴한다. 두 단계를 합치면 GPU당 통신량이 약 $2\Psi$ 다. 여기서 결정적인 사실은 이 양이 GPU 수 $N$ 에 (거의) 무관하다는 것이다. GPU를 두 배로 늘려도 각 GPU가 옮기는 바이트는 그대로다 — 이 $N$-무관성이 데이터 병렬이 수천 장까지 잘 스케일하는 근본 이유다.
시간으로 바꿔 보면 규모가 드러난다. 7B 모델의 그래디언트는 BF16으로 $2\Psi = 14$ GB다. 링 all-reduce는 GPU당 대략 그 두 배인 28GB를 주고받는다(reduce-scatter 14 + all-gather 14). 노드 안 고대역폭 링(집합 실효 대역폭을 예로 300 GB/s로 가정하자)이면 $28/300 \approx 93$ ms, 노드 사이의 느린 링(예로 25 GB/s)이면 $28/25 \approx 1.1$ 초다. 이 숫자가 무섭게 들리지만, 데이터 병렬의 실제 강점은 이 통신을 가릴 수 있다는 데 있다. 역전파는 층마다 순차로 그래디언트를 내놓으므로, 뒤쪽 층의 그래디언트가 나오는 대로 버킷 단위로 all-reduce를 시작해 앞쪽 층의 역전파 계산 뒤에 겹쳐 숨긴다. 그래서 노드 안 93ms는 대부분 계산에 가려 실질 오버헤드가 작다. 문제는 노드 밖으로 넘어가 대역폭이 한 자릿수 GB/s로 떨어질 때인데, 그때는 all-reduce가 계산 밖으로 노출되어 스텝을 늘린다.
데이터 병렬은 유효 배치를 $N$ 배로 키운다(GPU당 마이크로배치 × $N$). 여기에 그래디언트 누적(gradient accumulation)을 겹치면 — 옵티마이저 스텝 전에 여러 마이크로배치의 그래디언트를 로컬에서 더함 — GPU 수보다 큰 전역 배치도 만든다. 스케일링 법칙이 요구하는 큰 배치(2.2·2.3)를 이렇게 맞춘다. 다만 배치를 무한정 키운다고 학습이 그만큼 빨라지진 않는다 — 임계 배치 크기(critical batch size)를 넘으면 스텝당 진전이 둔해져 추가 데이터가 낭비된다(2.3).
데이터 병렬의 치명적 한계는 정의 자체에 내재되어 있다. 모든 GPU가 전체 모델 상태 $16\Psi$ 를 전부 복제한다. GPU를 아무리 늘려도 한 장에 모델 상태가 안 들어가면 데이터 병렬은 무력하다 — 7B의 112GB가 80GB에 안 들어가는 문제를 데이터 병렬은 전혀 풀지 못한다. 데이터 병렬은 "데이터를 더 빨리 먹는" 방법이지 "큰 모델을 담는" 방법이 아니다. 게다가 $N$ 장에 똑같은 $16\Psi$ 를 $N$ 벌 저장하는 것은 명백한 낭비다. 이 중복을 도려내는 것이 다음의 ZeRO다. 참고로 PyTorch의 DDP(DistributedDataParallel)가 이 고전 데이터 병렬이고, 뒤에 나올 FSDP는 데이터 병렬의 틀(데이터를 나눠 먹음)은 그대로 두되 복제만 없앤 ZeRO식 데이터 병렬이다. 둘 다 "데이터 병렬" 계열이지만 메모리 성질은 정반대다.
2.4.4 ZeRO — 데이터 병렬의 중복을 도려낸다
ZeRO(Zero Redundancy Optimizer, DeepSpeed)의 착상은 한 문장이다. 데이터 병렬에서 $N$ 개 GPU가 똑같은 $16\Psi$ 를 $N$ 벌 들고 있는데, 이것을 $N$ 조각으로 나눠 각자 $1/N$ 만 상주시키다가 필요할 때만 통신으로 모으면 어떤가? 계산은 여전히 데이터 병렬처럼 각 GPU가 다른 데이터로 수행하되, 모델 상태 저장의 중복만 제거하는 것이다. 나누는 대상을 옵티마이저 상태 → 그래디언트 → 파라미터 순으로 넓혀 가며 세 단계로 나뉜다.
$N$ 을 데이터 병렬 GPU 수, $K=12$ 를 Adam 옵티마이저 상태의 파라미터당 바이트로 두면, GPU당 모델 상태 메모리와 통신량은 다음과 같이 정리된다.
| 단계 | 나누는 것 | GPU당 모델 상태 | 통신량(요소 수) |
|---|---|---|---|
| 기준 데이터 병렬 | (없음, 전부 복제) | $(2+2+K)\Psi = 16\Psi$ | $2\Psi$ |
| ZeRO-1 | 옵티마이저 상태 | $4\Psi + \dfrac{12\Psi}{N}$ | $2\Psi$ |
| ZeRO-2 | + 그래디언트 | $2\Psi + \dfrac{14\Psi}{N}$ | $2\Psi$ |
| ZeRO-3 | + 파라미터 | $\dfrac{16\Psi}{N}$ | $\approx 3\Psi$ |
한 줄씩 뜯어 보자. ZeRO-1은 12바이트 크기의 옵티마이저 상태만 $N$ 조각으로 나눈다. 각 GPU는 자기가 맡은 파라미터 구간의 마스터 사본·$m$·$v$ 만 들고 그 구간만 갱신한다. 파라미터와 그래디언트는 여전히 전부 복제한다. 놀라운 점은 통신량이 데이터 병렬과 똑같다는 것이다. 원래 데이터 병렬의 all-reduce는 개념상 reduce-scatter + all-gather인데, ZeRO-1은 reduce-scatter로 각 GPU가 자기 구간 그래디언트를 모으고(갱신하고), all-gather로 갱신된 파라미터를 재배포한다 — 같은 $2\Psi$ 다. 메모리는 크게 줄고 통신은 그대로이니 거의 무비용에 가까운 이득이라, 실무에서 데이터 병렬을 쓸 때 기본으로 켜는 경우가 많다.
ZeRO-2는 그래디언트도 나눈다. 각 GPU는 자기 옵티마이저 샤드에 해당하는 그래디언트 구간만 최종 보유하면 되므로, all-reduce 대신 reduce-scatter로 그래디언트를 분배해 나머지는 버린다. 통신량은 여전히 $2\Psi$ 인데 그래디언트 저장이 $2\Psi \to 2\Psi/N$ 로 준다. ZeRO-3는 파라미터까지 나눈다. 각 GPU는 파라미터의 $1/N$ 만 상주시키고, 어떤 층을 계산하기 직전에 그 층의 파라미터를 all-gather로 모아 쓰고 즉시 버린다. 순전파와 역전파 양쪽에서 all-gather가 필요하니 통신이 늘어난다 — 순전파 all-gather $\Psi$ + 역전파 all-gather $\Psi$ + 그래디언트 reduce-scatter $\Psi$ = $3\Psi$ 로, 데이터 병렬의 $2\Psi$ 대비 정확히 1.5배다. 대가로 GPU당 모델 상태가 $16\Psi/N$ 이 되어 $N$ 에 반비례해 얼마든지 작아진다.
7B, $N=8$ 로 숫자를 넣으면 차이가 극명하다. 총 모델 상태는 112GB다.
$$\text{ZeRO-1}: 4\Psi + \frac{12\Psi}{8} = 28 + 10.5 = 38.5\ \text{GB}$$ $$\text{ZeRO-2}: 2\Psi + \frac{14\Psi}{8} = 14 + 12.25 = 26.25\ \text{GB}$$ $$\text{ZeRO-3}: \frac{112}{8} = 14\ \text{GB}$$
$N=64$ 로 키우면 ZeRO-3는 $112/64 = 1.75$ GB까지 내려간다. 여기서 하한값을 주목해야 한다. ZeRO-1은 $N$ 을 무한대로 보내도 파라미터·그래디언트 복제분 $4\Psi = 28$ GB 밑으로 못 내려간다. ZeRO-2의 하한은 $2\Psi = 14$ GB다. 오직 ZeRO-3만이 세 항목을 다 나눠 $16\Psi/N \to 0$ 으로 수렴한다. 그래서 정말 큰 모델은 ZeRO-3(또는 FSDP)가 아니면 데이터 병렬 계열로는 담을 수 없다.
이 세 단계가 정확히 2.4.1의 회계장부를 위에서부터 하나씩 지워 나가는 것임을 알아 두면 좋다. 12바이트 옵티마이저 상태를 먼저, 다음 그래디언트 2바이트를, 마지막에 파라미터 2바이트를 나눈다 — 큰 몫부터 공략하니 ZeRO-1만으로도 이득이 크고, ZeRO-3까지 가야 비로소 하한이 깨진다.
2.4.5 FSDP — PyTorch가 품은 ZeRO-3
FSDP(Fully Sharded Data Parallel)는 PyTorch에 내장된 ZeRO 계열 구현으로, 동작 원리는 ZeRO-3와 본질적으로 같다. 파라미터·그래디언트·옵티마이저 상태를 데이터 병렬 랭크에 걸쳐 샤딩하고, 층(정확히는 래핑 단위)을 계산하기 직전 파라미터를 all-gather했다가 쓰고 버리며, 그래디언트는 reduce-scatter로 분배한다. ZeRO가 DeepSpeed 진영의 이름이라면 FSDP는 PyTorch 진영의 이름이라고 봐도 크게 틀리지 않는다. 다만 FSDP는 샤딩 범위를 골라 ZeRO의 여러 단계를 흉내 낼 수 있다.
| FSDP 전략 | 무엇을 샤딩 | 대응 ZeRO |
|---|---|---|
NO_SHARD | (없음) | DDP(고전 데이터 병렬) |
SHARD_GRAD_OP | 그래디언트 + 옵티마이저 상태 | ZeRO-2 |
FULL_SHARD | 파라미터 + 그래디언트 + 옵티마이저 상태 | ZeRO-3 |
HYBRID_SHARD | 노드 안은 full-shard, 노드 사이는 복제 | ZeRO-3 + 계층적 |
HYBRID_SHARD가 실무에서 특히 중요하다. ZeRO-3의 약점은 층마다 파라미터를 all-gather한다는 것인데, 이것을 느린 노드 간 링크로 매 층 순환시키면 통신이 노출되어 급격히 느려진다. HYBRID_SHARD는 노드 안(고대역폭)에서만 full-shard로 메모리를 아끼고, 노드 사이는 복제해 스텝당 한 번 all-reduce만 하는 절충이다. 노드 안에서 이미 메모리를 $1/8$(노드당 8 GPU 기준)로 줄였으니 노드 간에는 그 줄어든 상태를 복제해도 부담이 적고, 대신 매 층 노드 간 all-gather를 피한다. 큰 클러스터에서 ZeRO-3의 통신 부담을 다스리는 핵심 기법이며, DeepSpeed의 ZeRO++ 계열이 같은 정신을 공유한다.
ZeRO-3/FSDP를 실전에서 견딜 만하게 만드는 두 번째 핵심은 프리페치(prefetch)다. 층 $\ell$ 을 계산하는 동안 층 $\ell{+}1$ 의 파라미터 all-gather를 미리 겹쳐 시작해, 통신 지연을 계산 뒤에 숨긴다. 인터커넥트가 빠르고 프리페치가 잘 겹치면 $3\Psi$ 통신의 실질 오버헤드가 작다. 이 겹침을 놓치면 — 프리페치를 끄거나 노드 간 대역폭이 부족하면 — all-gather가 계산 밖으로 벗어나 느려진다. 그래서 ZeRO-3 통신을 프리페치로 계산과 겹치지 않는 것, 그리고 노드 간 대역폭이 약한데 FULL_SHARD를 단순히 전 클러스터에 펼치는 것이 흔한 실패다. 후자의 처방이 바로 HYBRID_SHARD다.
한 가지 반드시 명시할 것이 있다. ZeRO/FSDP는 모델 상태를 나눌 뿐, 계산은 여전히 각 GPU가 모은 파라미터로 층 전체를 수행한다. 즉 활성값은 GPU마다 자기 마이크로배치 몫을 그대로 들고 있어서, ZeRO만으로는 활성값 메모리가 한 바이트도 줄지 않는다. 2.4.2에서 본 30GB 크기의 활성값은 ZeRO-3를 아무리 세게 켜도 그대로 남는다. 활성값이 장벽이면 재계산(2.4.11)이나 시퀀스 병렬로, 혹은 계산 자체를 나누는 텐서 병렬로 따로 공략해야 한다. "ZeRO-3면 임의로 큰 모델이 다 들어간다"는 말은 모델 상태에 대해서만 참이다.
2.4.6 텐서 병렬 — 층 내부의 행렬곱을 분할한다
ZeRO/FSDP는 저장을 나눴지만 한 층의 계산은 한 GPU 전체가 한다. 텐서 병렬(Tensor Parallelism, TP, Megatron-LM)은 그 지점에서 다르다 — 한 층 안의 행렬곱 자체를 여러 GPU가 나눠 계산한다. 파라미터·그래디언트·옵티마이저 상태뿐 아니라 활성값과 계산까지 $t$(TP 차수)로 분할된다는 점에서 ZeRO와 성질이 근본적으로 다르다. ZeRO가 못 줄이던 활성값을 텐서 병렬은 줄인다.
핵심 관용구는 FFN에서 가장 깨끗하게 보인다. 트랜스포머 FFN은 $Z = \sigma(XA)\,B$ 로, 두 행렬 $A$($h\times 4h$)와 $B$($4h\times h$)를 통과한다(1.4). 이를 GPU 2대에 나눈다. 먼저 $A$ 를 열 방향으로 분할해 $A=[A_1, A_2]$ 로 두면 $XA = [XA_1,\, XA_2]$ 가 되고, 활성함수 $\sigma$ 는 원소별(element-wise)이라 각 조각에 독립으로 적용된다 — GPU 0은 $\sigma(XA_1)$ 을, GPU 1은 $\sigma(XA_2)$ 를 각자 계산하며 여기까지 통신이 전혀 없다. 이어 $B$ 를 행 방향으로 분할해 $B=[B_1; B_2]$ 로 두면 $Z = \sigma(XA_1)B_1 + \sigma(XA_2)B_2$ 로, 각 GPU가 부분합을 내고 이것들을 all-reduce로 더해야 최종 $Z$ 가 완성된다. 열 분할로 활성함수 앞을 통신 없이 분할하고, 행 분할의 끝에서 정확히 한 번 모으는 것 — 이것이 텐서 병렬의 기본 문형이다. FFN 한 블록에 순전파 1회 all-reduce, 역전파에서 대칭으로 1회가 든다.
멀티헤드 어텐션(1.4)은 더 자연스럽게 분할된다 — 헤드를 GPU에 나눠 주면 된다. 각 GPU가 헤드의 부분집합에 대한 $Q,K,V$ 투영과 어텐션을 독립으로 계산하고(헤드끼리는 원래 독립이다), 마지막 출력 투영 $W^O$ 를 행 분할해 부분합을 만든 뒤 all-reduce로 합친다. FFN과 똑같은 패턴이라, 어텐션 블록도 순전파 1회 + 역전파 1회 all-reduce다. 층 하나당 순전파 2회 + 역전파 2회 = 층마다 4회 all-reduce가 나오고, 각 all-reduce는 $(b, s, h)$ 모양의 활성값 텐서 위에서 벌어진다.
이 4회가 텐서 병렬의 아킬레스건이다. 통신 시간을 계산하면 왜 노드 안에 가둬야 하는지가 숫자로 나온다. $b=1$, $s=2048$, $h=4096$ 이면 활성값 텐서는 $bsh \approx 8.39\times10^6$ 요소, BF16으로 약 16.8 MB다. all-reduce 한 번의 링 통신량은 그 두 배 남짓인 약 33.6 MB. 층마다 4회, 32층이면 스텝당 $4\times32 = 128$ 회 all-reduce이니 GPU당 통신량은 대략 $128 \times 33.6\ \text{MB} \approx 4.3$ GB다. 노드 안 300 GB/s 링이면 $4.3/300 \approx 14$ ms, 노드 사이 25 GB/s면 $4.3/25 \approx 172$ ms. 그런데 데이터 병렬 all-reduce와 달리 이 통신은 계산과 겹칠 수 없다 — all-reduce 결과가 나와야 다음 층 계산이 진행되는 동기 의존이기 때문이다. 노드 간 172ms가 매 스텝 순수 유휴로 쌓이면 처리량이 붕괴한다. 그래서 결론은 확고하다. 텐서 병렬은 노드 안(NVLink 같은 고대역폭 도메인)에 가둔다. 노드 하나에 GPU 8장을 담는 흔한 구성에서 그 8이 TP 차수의 실질 상한이다.
여기서 통설 하나를 조용히 깨 둔다. "텐서 병렬을 키우면 빨라진다"는 틀리다. 텐서 병렬 차수를 노드 밖으로 늘리면 방금 본 대로 통신이 급증해 오히려 느려진다. 텐서 병렬은 메모리를 위한, 그리고 한 GPU에 안 들어갈 만큼 넓은 층($h$ 가 큰 경우)을 담기 위한 도구이지 속도를 위한 도구가 아니다. 처리량은 데이터 병렬이 담당한다. 텐서 병렬이 주는 것은 저장뿐 아니라 활성값과 계산까지 $t$ 로 나눈다는 것 — ZeRO가 손대지 못한 활성값을 줄일 수 있다는 점 — 이고, 그 대가가 층마다 부하가 큰 동기 all-reduce와 노드 내 제한이다.
2.4.7 파이프라인 병렬 — 층을 스테이지로 나눠 순차 처리한다
텐서 병렬이 한 층을 옆으로(가로) 분할한다면, 파이프라인 병렬(Pipeline Parallelism, PP, GPipe·PipeDream)은 층을 세로로 나눠 스테이지로 묶는다. 32층 모델을 4스테이지로 나누면 GPU 0이 1–8층, GPU 1이 9–16층 하는 식이다. 순전파는 활성값을 다음 스테이지로 점대점으로 넘기고, 역전파는 그래디언트를 앞 스테이지로 되돌린다. 스테이지가 층의 일부만 들므로 모델 상태가 $p$(스테이지 수)로 나뉜다.
문제는 직렬 의존이다. 순진하게 배치 하나를 투입하면 한 순간에 한 스테이지만 일하고 나머지는 유휴 상태다. 스테이지 0이 순전파하는 동안 1·2·3은 대기하고, 마지막 스테이지가 역전파하는 동안 앞들이 대기한다. 이 유휴가 파이프라인 버블(bubble)이다. 해법은 배치를 마이크로배치 $m$ 개로 잘게 분할해 파이프라인에 연속 투입하는 것이다. 마이크로배치 1이 스테이지 1로 넘어가면 스테이지 0은 곧장 마이크로배치 2를 시작해, 파이프라인이 채워질수록 모든 스테이지가 바빠진다. 그래도 채우는 초입과 비우는 말미에는 버블이 남는다.
버블 분율을 유도해 보자. 스테이지 $p$ 개, 마이크로배치 $m$ 개일 때, 순전파만 놓고 보면 마이크로배치 1이 마지막 스테이지에 도달하기까지 $p-1$ 단계의 채움이 필요하다. 이후 마이크로배치가 한 단계씩 줄줄이 완성되어, 마지막 마이크로배치가 끝나는 것은 $m + (p-1)$ 단계째다. 이상적으로 유휴가 없다면 각 스테이지는 $m$ 단계만 일하면 되는데 실제로는 $m + (p-1)$ 단계에 걸쳐 있으니, 각 스테이지가 유휴 상태인 시간이 $p-1$ 단계다. 따라서 전체 시간 중 버블이 차지하는 분율은:
$$\text{버블 분율} = \frac{p-1}{m + (p-1)}$$
$p=4$, $m=4$ 면 $3/7 \approx 43\%$ — 절반 가까이 유휴 상태다. $m=32$ 로 키우면 $3/35 \approx 8.6\%$ 로 급감한다. 처방은 명확하다. 마이크로배치를 스테이지 수보다 훨씬 크게($m \gg p$) 설정해 버블을 희석한다. (문헌에 따라 유휴를 이상 시간 $m$ 에 대한 비로 $\frac{p-1}{m}$ 로 적기도 하는데, 이는 "초과 시간의 비율"이고 위 식은 "전체 벽시계 중 유휴 분율"로, 같은 현상의 두 표현이다.)
그런데 마이크로배치를 늘리는 것이 무비용이 아니다 — 활성값과 충돌한다. 역전파를 하려면 순전파 활성값을 유지하고 있어야 하는데, 파이프라인에 마이크로배치를 많이 투입하면 그만큼 활성값이 쌓인다. 스케줄이 이 균형을 정한다.
- GPipe(all-forward-then-all-backward)는 $m$ 개 마이크로배치를 전부 순전파한 뒤 전부 역전파한다. 단순하지만 $m$ 개 마이크로배치의 활성값을 동시에 들고 있어야 해 활성값 메모리가 $O(m)$ 으로 크다.
- 1F1B(one-forward-one-backward, PipeDream)는 파이프라인이 차면 순전파 하나·역전파 하나를 번갈아 한다. 각 마이크로배치의 활성값을 역전파 직후 곧장 해제하므로, 정상 상태에서 동시에 들고 있는 활성값이 약 $p$ 개 수준으로 억제된다. 버블 분율은 GPipe와 같은데 활성값 메모리는 $O(m)$ 에서 $O(p)$ 로 준다. 그래서 대형 학습의 사실상 표준이다.
- 인터리브드 1F1B(interleaved, Megatron)는 각 GPU가 연속되지 않은 여러 스테이지 조각(가상 파이프라인)을 맡아 버블을 $\frac{p-1}{v\,m}$ 로 더 줄인다($v$ 는 조각 수). 대가는 스테이지 경계가 늘어 통신 횟수가 $v$ 배가 되는 것이다.
그림에서 회색 계단이 버블이다. 스테이지 3(S3)이 F1을 끝내자마자 B1을 시작해(1F1B) 그 마이크로배치의 활성값을 일찍 해제하는 흐름을 주목하라 — 이것이 GPipe 대비 메모리 이득의 정체다. 파이프라인 병렬의 성질을 정리하면, 통신이 유독 부하가 작다. 스테이지 경계에서 활성값·그래디언트를 점대점(point-to-point)으로만 주고받고($\approx bsh$ 규모, 마이크로배치·경계당), all-reduce 같은 집합 통신이 없다. 통신량이 텐서 병렬·데이터 병렬보다 훨씬 적고 점대점이라, 파이프라인 병렬은 느린 노드 간 네트워크로 넘겨도 견딘다. 텐서 병렬을 노드 안에, 파이프라인 병렬을 노드 사이에 두는 배치가 바로 여기서 나온다. 약점은 버블과 스케줄 복잡도, 그리고 부하 균형이다 — 층을 스테이지에 고르게 나눠야 특정 스테이지가 병목이 안 된다. 임베딩이나 마지막 출력층이 유독 부하가 크면 스테이지 경계를 조정해 균형을 맞춰야 한다.
2.4.8 시퀀스·컨텍스트 병렬 — 시퀀스 축을 나눈다
데이터·텐서·파이프라인 세 축 외에, 활성값의 시퀀스 차원을 나누는 축이 둘 더 있다. 이름이 비슷해 자주 혼동되는데, 겨냥하는 문제가 다르다.
시퀀스 병렬(Sequence Parallelism, SP, Megatron)은 텐서 병렬의 짝이다. 텐서 병렬은 어텐션·FFN의 행렬곱은 나누지만, 그 사이의 LayerNorm·드롭아웃·잔차 같은 부분은 텐서 병렬에서 복제된 채 남아 활성값 메모리를 소모한다. 이 복제 구간을 시퀀스 차원으로 분할하는 것이 시퀀스 병렬이다. 텐서 병렬 구간(행렬곱)과 시퀀스 병렬 구간(정규화 등)의 경계에서 all-gather와 reduce-scatter로 표현을 전환한다. 영리한 점은 이 두 집합 연산을 합친 통신 총량이 텐서 병렬 단독의 all-reduce와 대략 같다는 것 — all-reduce 하나가 reduce-scatter + all-gather로 분해되기 때문이다. 통신을 늘리지 않고 텐서 병렬이 남긴 복제 활성값을 마저 줄인다. 그래서 실무에서 텐서 병렬을 쓰면 시퀀스 병렬을 함께 켜는 경우가 많다.
컨텍스트 병렬(Context Parallelism, CP)은 목적이 다르다. 롱컨텍스트(1.6) 학습에서 시퀀스 $s$ 가 수만~수백만 토큰이면 $s^2$ 활성값·어텐션 비용을 한 GPU가 감당 못 한다. 컨텍스트 병렬은 토큰(시퀀스)을 GPU에 나눠 각 GPU가 시퀀스의 한 조각만 들게 한다. 걸림돌은 어텐션이다 — 각 질의는 모든 키·값을 봐야 하는데 이들이 여러 GPU에 흩어져 있다. Ring Attention이 이를 푼다. KV 블록을 GPU들의 링을 따라 순환시키면서, 각 GPU가 이웃의 KV를 받아 자기 질의에 대한 부분 어텐션을 누적하고, 온라인 softmax(1.4의 max 빼기 트릭)로 조각을 정확히 합쳐 간다. KV를 순환시키는 통신과 어텐션 계산을 겹쳐 지연을 숨긴다. 컨텍스트 병렬은 수십만~수백만 토큰 컨텍스트 학습을 가능케 하는 축이다.
요약하면 시퀀스 병렬은 텐서 병렬의 짝으로 정규화 등 복제 구간을 시퀀스로 분할해 활성값을 줄이는 것이고, 컨텍스트 병렬은 롱컨텍스트를 위해 어텐션 자체를 시퀀스로 분산하는 것이다. 둘 다 활성값의 시퀀스 축을 겨냥하지만 프레임워크마다 용어가 섞이니 문맥으로 판단해야 한다.
2.4.9 전문가 병렬 — MoE의 전문가를 GPU에 흩는다
전문가 혼합(Mixture of Experts, MoE, 1.6)에서 전문가가 수십~수백 개면 한 GPU에 다 적재할 수 없다. 전문가 병렬(Expert Parallelism, EP)은 서로 다른 전문가를 서로 다른 GPU에 배치한다. 그러면 각 토큰은 자기가 라우팅된 전문가가 있는 GPU로 가야 하므로, MoE 층마다 두 번의 all-to-all 통신이 발생한다 — 토큰을 담당 전문가 GPU로 보내는 dispatch, 계산 결과를 원래 GPU로 되돌리는 combine이다.
all-to-all은 비용이 큰 통신이다. 모든 GPU가 모든 GPU와 데이터를 교환하므로 인터커넥트 대역폭을 크게 차지한다. 더 고약한 것은 라우팅이 입력 의존이라는 점이다 — 어떤 전문가에 토큰이 몰리면 그 GPU가 병목이 되고 나머지는 유휴 상태다(부하 불균형, 1.6의 로드 밸런싱 손실이 이것을 완화하려는 장치다). 이 all-to-all과 라우팅 쏠림이 MoE 스케일링의 주 병목이다. "전문가 수 = 공짜 용량"이 아니다 — 전문가를 늘릴수록 통신과 불균형이 새 비용으로 추가된다.
전문가 병렬은 데이터·텐서·파이프라인 축과 직교하게 조합된다. 흔히 전문가 병렬을 데이터 병렬 도메인과 겹쳐 배치하거나(전문가를 데이터 병렬 그룹에 분산) 전용 프로세스 그룹으로 둔다. all-to-all이 부하가 크기 때문에 전문가 병렬도 가능하면 고대역폭 도메인 안에 두려 하며, 규모가 커지면 노드 간 all-to-all을 계층적으로 처리하는 최적화가 추가된다. 구체 배치는 프레임워크마다 다르다.
2.4.10 3D·4D 조합 — 어느 축을 어느 하드웨어에 적재하나
지금까지의 축을 정리하면 데이터 병렬(처리량·저장 중복 제거 ZeRO 포함), 텐서 병렬(층 내부·활성값·계산), 파이프라인 병렬(층 묶음·스테이지), 그리고 MoE면 전문가 병렬이다. 대형 학습은 이들을 곱해서 쓴다 — 총 GPU 수 $= D \times T \times P\ (\times E)$. 이것이 3D(전문가 병렬 포함이면 4D) 병렬이다. 조합의 원리는 하나다. 각 축의 통신 성질이 다르므로, 통신 빈도·크기·겹침 가능성에 맞는 하드웨어 도메인에 배치한다.
| 축 | 통신 종류 | 빈도·크기 | 계산과 겹침 | 어느 도메인에 |
|---|---|---|---|---|
| 텐서 병렬 | all-reduce | 층마다·큼·동기 | 어렵다 | 노드 안(NVLink 등 최고 대역폭) |
| 전문가 병렬 | all-to-all | MoE 층마다·큼 | 부분적 | 노드 안 또는 고대역 도메인 |
| 파이프라인 병렬 | 점대점(활성값) | 스테이지 경계·작음 | 잘 됨 | 노드 사이(느린 링크 견딤) |
| 데이터 병렬/ZeRO | all-reduce·reduce-scatter·all-gather | 스텝마다(ZeRO-3는 층마다) | 잘 됨(프리페치) | 가장 바깥, 남는 축 전부 |
원칙은 이렇다. 가장 비용이 크고 자주 통신하며 겹치기 어려운 텐서 병렬을 가장 빠른 도메인(노드 안)에 배치하고, 통신이 부하가 작고 겹치기 좋은 파이프라인 병렬을 노드 사이로 빼며, 데이터 병렬을 최외곽에 둔다. 512 GPU를 $D{=}16 \times T{=}8 \times P{=}4$ 로 구성한다면 — 텐서 병렬 8은 노드 하나 안의 8 GPU(NVLink)에, 파이프라인 병렬 4와 데이터 병렬 16은 노드 경계를 넘어 배치한다.
이 구성이 70B 모델을 실제로 담는지 메모리로 확인해 보자. 70B의 모델 상태는 $16\times 70\times10^9 = 1120$ GB다. 텐서 병렬과 파이프라인 병렬이 모델 상태를 나누고(데이터 병렬은 복제하므로 나누지 않는다), 나누는 GPU 수는 $T\times P = 8\times 4 = 32$ 다. 따라서 GPU당 모델 상태는:
$$\frac{1120\ \text{GB}}{32} = 35\ \text{GB / GPU}$$
여기에 활성값이 추가되는데, 텐서 병렬이 활성값을 $t=8$ 로, 파이프라인 병렬이 스테이지당 층 수를 $1/4$ 로 줄이므로 활성값도 상당히 줄어든다. 35GB 모델 상태에 줄어든 활성값과 여유를 더해도 80GB GPU에 들어가고, 남는 여유는 데이터 병렬 최외곽에 ZeRO-1을 추가해 옵티마이저 상태 중복까지 마저 제거하면 더 벌 수 있다. 이렇게 텐서 병렬로 넓은 층을, 파이프라인 병렬로 깊은 층을, 데이터 병렬로 처리량을 각각 맡겨 곱으로 담는 것이 3D 병렬의 실체다.
배치를 정하는 개략 순서는 이렇다. 먼저 텐서 병렬과 파이프라인 병렬로 "한 벌의 모델"이 GPU들에 담기게 만든다 — 모델 상태와 활성값이 $T\times P$ 개 GPU에 들어갈 때까지. 텐서 병렬은 노드 안 상한(흔히 8) 안에서 키우고, 부족하면 파이프라인 병렬을 노드 사이로 늘린다. 그다음 남는 GPU를 데이터 병렬로 복제해 처리량과 전역 배치를 키우고, 거기에 ZeRO-1/2나 FSDP를 추가해 중복을 제거한다. MoE면 전문가 병렬을 축으로 추가하고, 그래도 활성값이 벅차면 재계산(2.4.11)을 켠다. 다만 이 배치와 차수는 성향이지 공식이 아니다. 최적값은 모델 형태(깊이 대 폭)·시퀀스 길이·클러스터 토폴로지·인터커넥트 대역폭(2.5)에 좌우되고, 실제로는 여러 구성을 프로파일링해 고른다. 안정적인 것은 "텐서 병렬은 노드 안, 파이프라인 병렬은 노드 사이, 데이터 병렬은 바깥"이라는 원칙이지 구체 숫자가 아니다.
2.4.11 활성값 재계산 — 메모리를 연산으로 되산다
병렬화가 저장을 여러 GPU로 나눴다면, 재계산은 저장을 아예 버렸다가 다시 만든다. 활성값 재계산(activation checkpointing, gradient checkpointing)은 순전파에서 모든 중간 활성값을 저장하지 않고 일부 체크포인트(대개 각 층의 입력)만 남긴다. 역전파에서 어떤 층의 그래디언트가 필요해지면, 그 층의 저장된 입력에서 순전파를 다시 실행해 활성값을 즉석 복원한 뒤 역전파한다. 2.4.2의 활성값 30GB를 층 경계 입력만 남기는 수준으로 낮춰 수 GB로 억제할 수 있다.
대가는 연산이다. 역전파 중에 순전파를 한 번 더 하는 셈이다. 트랜스포머의 순전파 대 역전파 FLOP 비가 대략 1:2(역전파가 순전파의 두 배 일을 한다)이므로, 원래 한 스텝은 순전파 1 + 역전파 2 = 3 단위이고, 전면 재계산은 여기에 재계산 순전파 1을 더해 4 단위다. 추가 연산은:
$$\frac{\text{재계산 순전파}}{\text{원래 fwd+bwd}} = \frac{1}{1+2} \approx 33\%$$
"메모리를 크게 사되 시간의 3분의 1을 더 낸다"는 거래다. 이 33%가 전면 재계산의 정가이고, 실무는 대개 이것을 다 내지 않는다.
선택적 재계산(selective, Megatron)이 그 절충이다. 전부 재계산하는 대신, 재계산 비용은 저렴한데 저장 비용은 큰 부분만 골라 버린다 — 대표적으로 2.4.2에서 지배 항이던 어텐션의 $s^2$ 활성값이다. 이 부분만 재계산하면 활성값의 큰 몫을 되찾으면서 추가 연산은 33%보다 훨씬 작게 유지한다. 사실 FlashAttention이 이미 이 정신 — 점수 행렬을 저장하지 않고 재계산 — 을 어텐션에 내장한 것이라, 현대 스택에서는 FlashAttention과 선택적 재계산의 조합이 흔하다.
언제 켜는가는 병목이 정한다. 재계산은 연산이 남고 메모리가 부족할 때 — GPU에 계산 여유는 있는데 활성값 때문에 배치·시퀀스를 못 키울 때 — 켠다. 재계산으로 확보한 메모리로 마이크로배치를 키우면, 33% 추가 연산을 상쇄하고도 처리량이 오르는 경우가 많다(파이프라인 병렬에서 $m$ 을 키워 버블을 줄이는 데도 유용하다). 반대로 이미 연산이 병목이면 재계산의 +33%는 순손실이다. 그러니 무턱대고 전면 재계산을 켜기 전에 선택적 재계산과 FlashAttention으로 먼저 줄여 보는 것이 낫다. 재계산은 병렬화(ZeRO·텐서·파이프라인)와 독립적으로 겹쳐 쓸 수 있는 마지막 조정 변수라, 3D 배치에서 활성값이 넘칠 때 맨 끝에 켜는 스위치로 둔다.
2.4.12 무엇을 언제 — 비용을 옮기는 선택들
각 전략을 "무엇을 나누고, 계산도 나누나, 통신은 어디서 얼마나 들고, 무엇을 얻고 무엇을 지불하나"로 한 표에 모은다. 통신량은 개략이다.
| 전략 | 나누는 것 | 계산도 나누나 | 통신(개략) | 통신 위치 | 얻는 것 | 지불하는 것 |
|---|---|---|---|---|---|---|
| DP(DDP) | (없음, 복제) | 아니오 | 스텝당 all-reduce $\approx 2\Psi$ | 어디든(겹침 쉬움) | 처리량·큰 배치 | 모델 상태 $N$ 중복, 큰 모델 못 담음 |
| ZeRO-1 | 옵티마이저 상태 | 아니오 | $\approx 2\Psi$ | DP와 동일 | 메모리 절감, 거의 무비용 | 하한 $4\Psi$ |
| ZeRO-2 | +그래디언트 | 아니오 | $\approx 2\Psi$ | DP와 동일 | 더 절감 | 하한 $2\Psi$ |
| ZeRO-3 / FSDP | +파라미터 | 아니오(층은 전체 계산) | $\approx 3\Psi$ | DP 도메인 | 메모리 $16\Psi/N$, 임의로 작게 | 층마다 all-gather, 대역폭 민감 |
| TP | 층 내부 행렬 | 예(활성값·계산도) | 층마다 all-reduce×4 | 노드 안 필수 | 활성값·계산 분할, 넓은 층 수용 | 잦은 부하가 큰 동기 통신 |
| PP | 층(스테이지) | 예(스테이지별) | 경계 점대점, 작음 | 노드 사이 OK | 경량 통신으로 층 방향 분할 | 버블($m\gg p$ 필요), 스케줄 복잡 |
| SP | 정규화·드롭아웃 구간(시퀀스) | 예 | TP와 합쳐 비슷 | TP와 동반 | TP 잔여 활성값 절감 | TP 의존 |
| CP | 시퀀스(토큰) | 예 | Ring 어텐션 통신 | 도메인 의존 | 초장문 컨텍스트 수용 | 어텐션 통신 복잡 |
| EP(MoE) | 전문가 | 예 | MoE 층마다 all-to-all | 고대역 도메인 | 많은 전문가 분산 | all-to-all·부하 불균형 |
| 재계산 | (활성값 버림) | — | 없음(연산으로 대체) | — | 활성값 메모리 크게↓ | 연산 +33%(전면) |
이 표에서 뽑아 갈 원칙은 셋이다. 첫째, 병렬화는 비용을 없애지 않고 옮긴다. ZeRO는 메모리를 줄이고 통신을 늘리며, 텐서 병렬은 활성값·계산을 줄이고 통신을 (노드 안에서) 폭증시키며, 파이프라인 병렬은 통신을 줄이고 버블을 만들며, 재계산은 메모리를 줄이고 연산을 늘린다. 어떤 전략을 고르든 "무엇을 아끼고 무엇을 더 내는가"로 판단해야 한다. 둘째, 축마다 맞는 하드웨어 도메인이 있다 — 통신이 잦고 부하가 크고 동기적인 순서(텐서·전문가 병렬 > ZeRO-3 > 데이터·파이프라인 병렬)로 빠른 도메인에 배치한다. 이 원칙은 2.5의 인터커넥트 토폴로지와 직결된다. 셋째, 초대형 모델은 단일 전략으로 못 담는다 — ZeRO(또는 FSDP)로 처리량 축을, 텐서 병렬로 넓은 층을, 파이프라인 병렬로 깊은 층을, 재계산으로 활성값을, 전문가 병렬로 전문가를 각각 맡겨 곱으로 쓴다.
규모별로 성향을 펼치면 다음과 같다. 모델이 한 GPU에 들어가는 소~중형(파인튜닝 등)이면 단순히 DDP에 ZeRO-1을 추가한다 — 여러 GPU에서 얻을 것은 처리량뿐이고, 활성값이 부족하면 재계산을 켠다(PEFT·LoRA로 더 경량화하는 길은 3.8). 모델 상태가 한 GPU를 넘지만 노드 하나(8 GPU)에는 들어가면 ZeRO-3/FSDP(구현이 단순하고 통신이 노드 안이라 빠르다)나 텐서 병렬(활성값까지 나눠야 하면)을 쓴다. 롱컨텍스트면 재계산·시퀀스 병렬을 병행한다. 노드 여러 대가 필요한 대형이면 3D — 텐서 병렬(노드 안) × 파이프라인 병렬(노드 사이) × 데이터 병렬/ZeRO(바깥) — 로 가고 활성값은 재계산·시퀀스·컨텍스트 병렬로 다스린다. MoE면 그 위에 전문가 병렬 축을 추가하는데, all-to-all 대역폭이 관건이 된다.
실전에서 이 모든 것을 굴릴 때 되풀이되는 함정 몇을 미리 알아 두면 값을 아낀다. "GPU를 늘리면 큰 모델이 들어간다"는 오해는 데이터 병렬이 모델 상태를 복제한다는 사실을 잊은 것이다 — 큰 모델은 ZeRO-3·텐서·파이프라인 병렬로 상태를 나눠야 한다. 메모리를 계획할 때 Adam의 12바이트나 활성값을 빠뜨리면 실제의 절반 이하로 과소평가한다. 텐서 병렬을 노드 밖으로 늘리거나 ZeRO-3 통신을 프리페치로 겹치지 않으면 통신이 계산을 압도한다. 파이프라인 버블은 $m\gg p$ 와 1F1B로, MoE의 부하 불균형은 로드 밸런싱으로 다스려야 한다. 그리고 여러 축을 한꺼번에 켜면 이득도 겹치지만 손실 스파이크·수치 불안정·성능 저하의 원인 분리가 어려워진다 — 하나씩 도입하고 프로파일하며, 정확성은 작은 설정에서 동일 시드·동일 손실 곡선 재현으로 검증하는 것이 표준이다.
이 장은 "무엇을 어떻게 나누나"였다. 다음 장은 그 나눔이 실제로 실행되는 하드웨어다 — GPU 클러스터와 인터커넥트(NVLink·InfiniBand)의 토폴로지, NCCL 집합통신이 all-reduce와 all-to-all을 어떻게 구현하는지, 학습 체크포인팅과 수천 장 규모에서의 실패 복구. 이 장에서 "텐서 병렬은 노드 안, 파이프라인 병렬은 노드 사이"라 부른 그 도메인의 정체가 2.5에서 드러난다. 혼합정밀도의 수치 안정성(FP16/BF16/FP8·손실 스파이크)은 2.6에서, 서빙 쪽 GPU 운영은 5.5에서 다시 만난다.