AEGLOSLAB ChatGPT 같은 AI를 만들고 서비스하기

제2부 사전학습(Pretraining)

2.5 분산학습 II — 인프라·하드웨어·통신

2.4의 병렬화 전략은 종이 위의 기하학이다. 데이터·텐서·파이프라인·전문가 병렬을 어떤 비율로 조합하고 ZeRO로 옵티마이저 상태를 어디까지 분할할지는, 아직 칩도 케이블도 등장하지 않은 도면이다. 이 장은 그 도면을 실제 하드웨어 위에 추가한다 — 그리고 추가하는 순간, 우아하던 전략이 배선 위상(topology) 앞에서 다시 구성된다. 텐서 병렬은 왜 노드 하나를 벗어나지 못하는가, 데이터 병렬의 그래디언트 동기화는 왜 스텝마다 backward 계산 뒤에 숨겨야만 하는가, 수천 장의 GPU는 왜 하루에도 몇 번씩 중단되는가. 이 질문들의 답은 전부 모델이 아니라 케이블과 스위치와 메모리의 물리에 있다.

한 문장으로 이 장을 요약하면 이렇다. 프런티어 학습에서 GPU는 계산하는 시간보다 서로를 기다리는 시간이 많다. 수천 개의 칩이 협동해 하나의 손실을 내리려면 매 스텝 방대한 텐서를 주고받아야 하는데, 칩의 연산 속도는 지난 십 년간 폭발적으로 올랐지만 칩 사이를 잇는 배선의 대역폭은 그만큼 따라오지 못했다. 그래서 대규모 학습의 실력은 "얼마나 빠른 GPU를 샀나"가 아니라 "그 GPU들이 서로를 기다리는 시간을 얼마나 줄였나"로 갈린다. 이 장의 모든 주제 — 인터커넥트 계층, 집합통신 알고리즘, 연산-통신 오버랩, 데이터 로딩, 체크포인팅, 장애 복구 — 는 결국 그 기다림을 줄여 나가는 서로 다른 접근이다.

이 장에 나오는 대역폭·지연·MTBF·가격은 세대와 벤더에 묶인 변동값이라 개략 규모를 보여 주는 개략 예시로 읽어야 한다. 장의 무게는 특정 숫자가 아니라 계층 사이의 트레이드오프에 있고, 그 방향은 세대가 바뀌어도 유지된다. 앞 장 2.4의 네 병렬 축과, 2.2에서 세운 학습 총연산 $6ND$ 를 전제로 삼는다 — 뒤에서 이 $6ND$ 를 하드웨어의 달성 성능으로 나눠 학습 일수를 직접 뽑아낸다.

2.5.1 하나의 가속기를 규정하는 세 숫자

브랜드도 코어 이름도 마케팅 명칭도 잊어버리자. 학습의 관점에서 하나의 가속기(GPU·TPU·NPU)를 규정하는 것은 실질적으로 세 숫자다. 이 셋의 관계가 모델을 몇 장에 분할할지, 어떤 커널이 느릴지, MFU가 왜 100%에 못 미치는지를 전부 지배한다.

숫자무엇무엇을 결정하나개략 규모
peak FLOPS초당 부동소수 연산량(행렬곱 기준, BF16·FP8)학습 속도의 이론 상한데이터센터급 GPU에서 BF16 $\sim 10^{15}$ FLOPS(≈1 PFLOPS) 언저리
HBM 용량온패키지 고대역폭 메모리 크기파라미터·옵티마이저·활성값이 여기 올라감 → 모델을 몇 장에 분할할지수십 GB(세대마다 증가)
HBM 대역폭그 메모리를 초당 얼마나 읽나행렬곱 아닌 연산(어텐션·정규화)의 속도수 TB/s

세 숫자의 긴장이 핵심이다. 현대 가속기는 연산이 압도적으로 빠른데 메모리는 작고, 그 작은 메모리의 대역폭조차 연산 속도에 비해 상대적으로 느리다. peak FLOPS와 HBM 대역폭의 비 — 초당 할 수 있는 연산 대 초당 읽을 수 있는 바이트 — 를 하드웨어의 균형점이라 부르는데, 이 비가 대략 수백 FLOP/byte 수준이다. 어떤 커널이 바이트당 그보다 많은 연산을 하면 연산에 묶이고(FLOPS 상한), 적게 하면 대역폭에 묶인다(대역폭 상한). 이 판정을 산술 강도(arithmetic intensity)와 루프라인으로 정밀하게 다루는 것은 5.1의 몫이고, 여기서는 결론만 가져오자. 행렬곱은 산술 강도가 높아 FLOPS를 잘 살리지만, 어텐션의 softmax·정규화·활성함수 같은 비-행렬곱 연산은 바이트당 연산이 적어 대역폭에 묶인다. 그래서 학습 커널의 상당 부분이 peak FLOPS를 못 쓰고, 이것이 MFU가 100%에 도달할 수 없는 첫 번째 이유다(2.5.14).

용량 숫자는 병렬화를 강제한다. 파라미터 $P$ 를 fp32로 담으면 $4P$ 바이트, Adam 옵티마이저 상태(1차·2차 모멘텀)까지 세면 파라미터당 대략 12~16바이트가 든다(2.4). 70B 모델이면 옵티마이저 상태만 $7\times10^{10}\times16\approx 1.1$ TB로, 수십 GB 크기의 HBM 한 장에 결코 들어가지 않는다. 모델이 한 장에 안 들어가는 순간 텐서·파이프라인·ZeRO로 분할해야 하고(2.4), 분할한 조각들이 서로 통신하기 시작하면서 이 장의 문제가 열린다. 요컨대 세 숫자 중 용량이 "쪼갤지 말지"를, 대역폭이 "쪼갠 뒤 얼마나 빠른지"를, FLOPS가 "쪼개고 통신하고 남은 시간에 얼마나 계산하는지"를 정한다.

시장에는 여러 계열의 학습 가속기가 있고 구체 모델명·스펙은 몇 달 단위로 바뀌지만, 골격은 어느 것이든 같다 — 행렬곱 특화 연산코어 + 온패키지 HBM + 고속 인터커넥트 포트. NVIDIA 계열은 텐서코어와 CUDA·NCCL 생태계에 NVLink·NVSwitch를 추가하고, Google TPU는 팟 단위로 설계되어 2D·3D 토러스로 칩을 잇고, AMD Instinct는 대용량 HBM과 ROCm에 Infinity Fabric을 쓴다. 어느 계열이 "무조건 낫다"는 판정은 소프트웨어 생태계·가용성·가격·대역폭 균형의 함수이고 시점에 의존하므로 이 책의 범위를 벗어난다. 이 장이 다루는 통신·메모리·장애의 물리는 벤더와 무관하게 공통이다.

2.5.2 노드·랙·팟 — 배선의 계단

프런티어 학습 클러스터는 "아주 큰 컴퓨터 한 대"가 아니라 수천 개의 가속기를 계층적으로 배선한 구조물이다. 그 계층을 올라갈수록 세 가지가 단조롭게 변한다 — 규모는 커지고, 대역폭은 떨어지고, 지연은 늘어난다. 이 단조성이 이 장의 모든 설계 결정을 지배한다.

NVLink/NVSwitch 노드 내 InfiniBand/RoCE 노드 간 가속기(칩) 1개 연산코어 + HBM (수십 GB · 수 TB/s) 노드(서버 1대) 가속기 8개 + CPU·RAM·NIC 랙(rack) 노드 여러 대 + ToR 스위치 팟/클러스터 랙 수십~수백 + 스파인 네트워크 규모 예시: 가속기 1 → 노드 8 → 랙 수십 → 팟 수천~수만 가속기 대역폭: HBM(TB/s) ≫ NVLink (수백 GB/s) ≫ IB(수십 GB/s)
GPU 클러스터의 계층 — 위로 갈수록 대역폭↓·지연↑·규모↑

가장 아래는 방금 본 가속기 한 장이다. 그 위 노드(node)는 한 대의 서버로, 전형적으로 가속기 8개(4·16도 있다)에 호스트 CPU·시스템 RAM·네트워크 인터페이스 카드(NIC) 여러 장을 담는다. 노드 안의 가속기들은 범용 버스(PCIe)가 아니라 전용 고속 링크로 직결되며, 이것이 이 장 전체에서 가장 빠른 GPU-간 경로다. 그 위 랙(rack)은 노드 여러 대를 물리적으로 담고 랙 상단 스위치(Top-of-Rack, ToR)로 묶는다 — 노드 경계를 넘는 통신은 여기서부터 네트워크를 탄다. 맨 위 팟·슈퍼팟은 랙 수십~수백을 스파인(spine) 네트워크로 잇고, 프런티어 학습은 수천~수만 가속기가 이 층에서 하나의 작업으로 협동한다.

대역폭의 계단을 숫자로 놓으면 규모가 드러난다. HBM은 칩 바로 옆에 붙어 초당 수 TB를 읽고, 노드 내 전용 링크는 수백 GB/s, 노드 간 네트워크는 링크당 수십 GB/s다. 한 계단 내려갈 때마다 대역폭이 대략 한 자릿수씩 증가하는 셈이고, 반대로 올라갈 때마다 한 자릿수씩 좁아진다. 통신량이 많은 병렬 축을 어느 계단에 놓느냐가 전체 효율을 좌우하는 이유가 여기 있다 — 자주·많이 통신하는 축을 좁은 계단에 추가하면 GPU가 대부분을 기다림으로 허비한다. 이 배치 규칙을 정량화하는 것이 2.5.4다.

2.5.3 두 세계의 인터커넥트 — 스케일업과 스케일아웃

분산학습에서 GPU끼리 데이터를 주고받는 경로는 두 세계로 갈린다. 노드 경계가 그 분수령이고, 두 세계의 대역폭·지연 격차가 흔히 한 자릿수를 넘는다. 이 격차가 병렬화 배치의 물리 법칙이다.

노드 안에서 8개 GPU는 전용 GPU-간 링크로 직결된다. NVIDIA 계열에서는 NVLink가 GPU 쌍을 고대역폭으로 잇고, NVSwitch가 노드 안 모든 GPU를 풀메시로 연결해 어느 쌍이든 동시에 최대 대역폭으로 통신하게 한다. 한 GPU가 노드 내 다른 GPU와 수백 GB/s급으로 통신할 수 있다는 것이 핵심이며, 이것은 범용 PCIe(수십 GB/s급)보다 대역폭이 훨씬 크고 지연도 낮다. 이렇게 "하나의 큰 가속기처럼 보이는 밀결합 도메인"을 노드 안에 만드는 것을 스케일업(scale-up)이라 부른다. 뒤에서 보겠지만 텐서 병렬처럼 통신이 극도로 잦은 축은 반드시 이 도메인 안에 들어가야 한다.

노드 경계를 넘는 순간 통신은 네트워크를 탄다. 두 갈래가 흔하다. InfiniBand(IB)는 HPC용 저지연·고대역폭 네트워크로, 원격 직접 메모리 접근(Remote Direct Memory Access, RDMA)을 통해 CPU를 거의 거치지 않고 한 GPU 메모리에서 다른 GPU 메모리로 데이터를 옮긴다. NIC가 GPU HBM을 직접 읽어 네트워크로 내보내는 이 경로를 GPUDirect RDMA라 하고, 이것이 없으면 데이터가 GPU→시스템 RAM→NIC를 거치며 CPU와 메모리 복사를 낭비한다. RoCE(RDMA over Converged Ethernet)는 같은 RDMA를 표준 이더넷 위에서 구현해 IB에 준하는 성능을 범용 장비로 노리지만, 손실·혼잡 제어 튜닝이 까다로워 잘못 설정하면 성능이 급락한다. 이 스케일아웃(scale-out) 네트워크는 보통 팻트리(fat-tree)나 레일 최적화(rail-optimized) 위상으로 짜여, 어느 노드 쌍이든 병목 없이(non-blocking에 가깝게) 통신하도록 설계된다. 그럼에도 노드 내 링크보다 대역폭은 낮고 지연은 높다는 사실은 바뀌지 않는다.

노드 A (밀결합 도메인) G0 G1 G2 G3 G4 G5 G6 G7 NVSwitch(풀메시) 노드 B G0 G1 G2 G3 G4 G5 G6 G7 NVSwitch(풀메시) IB/RoCE 노드 내: 수백 GB/s · 저지연 노드 간: 수십 GB/s · 높은 지연 — 대역폭 격차 대략 한 자릿수
두 세계 — 노드 내 스케일업(NVLink) vs 노드 간 스케일아웃(IB/RoCE).

지연도 대역폭 못지않게 중요하다. 노드 내 링크의 왕복 지연은 마이크로초 이하지만, 네트워크를 거치는 노드 간 통신은 스위치 홉·직렬화·프로토콜 처리를 거치며 그보다 한참 크다. 뒤에서 보겠지만 집합통신의 비용은 "옮기는 바이트 ÷ 대역폭"에 "메시지 수 × 지연"을 더한 것이라(2.5.6), 작은 메시지를 자주 보내는 통신 패턴은 대역폭이 아니라 지연에 지배된다 — 그리고 노드 간에서 이 지연이 특히 문제가 된다. 이것이 통신을 큰 묶음으로 묶어 보내는 버킷화(2.5.9)와, 통신이 잦은 축을 노드 안에 가두는 배치 규칙의 공통 근거다.

2.5.4 대역폭·지연이 병렬화 축의 배치를 정한다

이 절이 이 장이 2.4과 만나는 지점이다. 병렬화의 네 축은 각기 통신 원형·빈도·통신량·지연 민감도가 다르고, 그에 따라 어느 계층에 놓아야 하는지가 물리로 정해진다. 이 배치를 3D(또는 4D) 병렬의 토폴로지 매핑이라 부른다.

병렬 축통신 원형빈도통신량놓을 자리
텐서 병렬(TP)all-reduce(활성값)매 층·매 마이크로배치큼노드 내(NVLink) — TP 차수 ≤ 노드당 GPU
전문가 병렬(EP, MoE)all-to-all(토큰 라우팅)매 MoE 층큼·불균형노드 내 우선, 넘치면 고대역폭 노드 간
파이프라인 병렬(PP)point-to-point(경계 활성값)스테이지 경계중간노드 간 가능
데이터 병렬(DP)·FSDPall-reduce·reduce-scatter+all-gather(그래디언트·파라미터)스텝당 1회 위주큼(전 파라미터)노드 간·랙 간 가능

규칙은 한 줄이다. 자주·많이·즉시 통신하는 축일수록 빠른 링크 아래에 둔다. 실무의 표준 레시피가 여기서 나온다 — 텐서 병렬은 노드 안 8-GPU 도메인에 가두고, 데이터 병렬·FSDP를 가장 바깥(노드·랙 간)에 두고, 파이프라인 병렬을 그 사이에 삽입한다. 왜 이 순서인지는 숫자로 따져야 설득력이 있다.

텐서 병렬을 노드 밖으로 못 내보내는 이유를 계산해 보자. 텐서 병렬은 각 트랜스포머 층에서 활성값 텐서에 all-reduce를 적용한다. 순전파에 두 번(어텐션 뒤, MLP 뒤), 역전파에 두 번, 층 하나당 네 번이다. all-reduce 한 번의 메시지 크기는 활성값 텐서 크기와 같은데, 마이크로배치 토큰 수 $b\cdot s$ 에 은닉차원 $d_{\text{model}}$ 과 정밀도 바이트를 곱한 것이다. 마이크로배치 $b=1$, 시퀀스 $s=8192$, $d_{\text{model}}=8192$, BF16(2바이트)이면

$$S_{\text{act}} = b\cdot s \cdot d_{\text{model}} \cdot 2 = 1\times 8192 \times 8192 \times 2 \approx 134\ \text{MB}$$

한 all-reduce가 옮기는 바이트는 GPU당 대략 $2S_{\text{act}}$ 다(다음 절에서 유도한다). 층이 80개, 층마다 네 번이면 마이크로배치 하나에 $4\times 80 = 320$ 번의 all-reduce가 발생하고, GPU 하나가 옮기는 총 바이트는

$$320 \times 2 \times 134\ \text{MB} \approx 86\ \text{GB}$$

이것을 노드 내 NVLink의 유효 대역폭 $\sim 400$ GB/s로 전송하면 마이크로배치당 통신 시간이 $86/400 \approx 0.21$ 초다. 같은 통신을 노드 간 IB($\sim 50$ GB/s)로 전송하면 $86/50 \approx 1.7$ 초 — 여덟 배가 넘게 느려진다. 그런데 이 통신이 마이크로배치마다 반복된다. 텐서 병렬을 네트워크에 연결하는 순간 GPU는 계산 시간의 몇 배를 통신 대기로 허비하게 되고, 그래서 TP 차수는 사실상 노드당 GPU 수(보통 8)를 상한으로 갖는다.

같은 all-reduce라도 데이터 병렬은 사정이 정반대다. DP는 그래디언트를 스텝당 정확히 한 번 동기화한다. 통신량 자체는 전 파라미터 크기라 크지만(7B fp16 그래디언트면 14 GB), 스텝당 한 번이고 무엇보다 backward 계산 뒤에 숨길 수 있다(2.5.8) — 텐서 병렬의 활성값 통신은 그 층의 계산 결과를 다음 층이 즉시 기다리므로 숨길 수 없지만, DP 그래디언트 통신은 이번 스텝의 계산이 끝난 뒤 다음 스텝 전까지의 여유에 겹칠 수 있기 때문이다. 빈도가 수백 배 차이 나고 오버랩 가능성이 갈린다는 이 두 사실이, 텐서 병렬을 노드 안에 가두고 데이터 병렬을 가장 바깥에 두는 배치를 강제한다. 전문가 병렬의 all-to-all은 토큰을 전문가가 있는 GPU로 흩뿌리는 통신이라 불균형이 심하고 지연에 민감해 노드 내를 선호하며, 파이프라인 병렬의 스테이지 경계 활성값 전송은 통신량이 중간이고 파이프라인 버블에 흡수될 여지가 있어 노드 간을 견딘다.

2.5.5 집합통신의 다섯 원형

여러 GPU가 협동하려면 정형화된 다대다 통신 패턴이 필요하다. 이를 집합통신(collective communication)이라 하고, 딥러닝에서 압도적으로 중요한 것이 all-reduce다 — 데이터 병렬 학습에서 각 GPU가 계산한 그래디언트를 모두 합쳐 모든 GPU에 되돌리는 연산이 정확히 all-reduce이기 때문이다.

broadcast — 한 GPU의 데이터를 전원에게 reduce-scatter — 합치되 각자 조각만 all-gather — 각자 조각을 전원이 다 모음 all-reduce = reduce-scatter + all-gather G0: [A] G1: [A] G2: [A] G3: [A] G0..G3 각자 [a,b,c,d] → G0:Σa, G1:Σb, G2:Σc, G3:Σd G0:[a] G1:[b] G2:[c] G3:[d] → 전원 [a,b,c,d] 전원 [x₀..x₃] → 전원 [Σx₀..Σx₃]
집합통신 네 원형 — broadcast·reduce-scatter·all-gather·all-reduce

원형은 다섯(보너스 하나 포함 여섯)이다. broadcast는 한 GPU(root)의 값을 나머지 전원에게 복사한다 — 초기 가중치를 배포할 때 쓴다. reduce는 전원의 값을 연산(합·최대 등)으로 합쳐 한 GPU에 모은다. all-reduce는 전원의 값을 합쳐 전원에게 되돌려, 끝나면 모든 GPU가 같은 합을 갖는다 — 데이터 병렬 그래디언트 동기화의 핵심이다. reduce-scatter는 합치되 각 GPU가 결과의 자기 조각($1/N$)만 받고, all-gather는 각 GPU가 가진 조각을 모아 전원이 전체를 갖게 한다. 이 둘은 FSDP·ZeRO의 그래디언트 분산과 파라미터 재수집에 각각 쓰인다(2.4). 보너스로 all-to-all은 각 GPU가 다른 모든 GPU에게 서로 다른 조각을 보내는 것으로, MoE 전문가 병렬의 토큰 라우팅에 필수다.

이 여섯 중에 다음 절 전체를 뒷받침하는 항등식이 하나 있다.

$$\text{all-reduce} = \text{reduce-scatter} \;\Rightarrow\; \text{all-gather}$$

전원이 각자 전체 벡터를 갖고 시작해서, 먼저 reduce-scatter로 "각자 최종 합의 서로 다른 한 조각"을 만든 다음, all-gather로 그 조각들을 서로 나눠 전원이 전체 합을 갖게 한다. all-reduce를 하나로 구현하지 않고 이 두 국면으로 분해하는 것이 링 알고리즘의 골격이자 통신량 산식의 출발점이다.

2.5.6 ring all-reduce와 통신량 2S의 유도

순진하게 all-reduce를 구현하면 "모두가 root에게 보내 합치고(reduce) root가 다시 뿌린다(broadcast)"가 된다. 그러면 root의 링크로 모든 트래픽이 몰려, 통신 시간이 GPU 수 $N$ 에 비례해 늘어난다. root 하나가 $N-1$ 개로부터 받고 다시 $N-1$ 개로 보내니 그 링크가 병목이다. ring all-reduce는 이 병목을 없앤다. $N$ 개 GPU를 논리적 고리로 배열하고, 데이터를 $N$ 조각으로 분할한 뒤 각자 옆 GPU로만 전달한다 — 어느 GPU도 특별히 많이 보내거나 받지 않으므로 병목이 사라진다.

G0 G1 G2 G3 국면 1: reduce-scatter — N−1 스텝, 각 스텝 S/N 전송 국면 2: all-gather — N−1 스텝, 각 스텝 S/N 전송 GPU당 총 전송 ≈ 2·(N−1)/N · S ≈ 2S N에 거의 무관 → 대역폭 최적
ring all-reduce — 각자 옆으로만 보낸다(2국면: reduce-scatter → all-gather).

크기 $S$ 바이트의 그래디언트가 $N$ 개 GPU에 각각 복제돼 있다고 하자. 링 알고리즘은 앞 절의 분해를 그대로 따른다. 먼저 reduce-scatter 국면은 $N-1$ 스텝이다. 매 스텝 각 GPU는 크기 $S/N$ 짜리 조각 하나를 옆으로 보내고, 반대편에서 하나를 받아 자기 것에 더한다. $N-1$ 스텝이 끝나면 각 GPU는 최종 합의 서로 다른 $1/N$ 조각을 완성해 갖는다. 이어 all-gather 국면도 $N-1$ 스텝인데, 이번엔 더하지 않고 완성된 조각을 고리를 따라 순환시켜 전원이 전체 합을 갖게 한다.

GPU 하나가 보내는(또는 받는) 총 바이트를 세어 보자. 각 국면이 $N-1$ 스텝이고 스텝마다 $S/N$ 을 옮기므로,

$$V_{\text{ring}} = \underbrace{(N-1)\cdot\frac{S}{N}}_{\text{reduce-scatter}} + \underbrace{(N-1)\cdot\frac{S}{N}}_{\text{all-gather}} = 2\,\frac{N-1}{N}\,S \;\xrightarrow[N\to\infty]{}\; 2S$$

이 식의 놀라운 점은 GPU당 통신량이 $N$ 에 (거의) 무관하게 $\approx 2S$ 로 고정된다는 것이다. GPU를 아무리 늘려도 각 GPU가 옮기는 바이트 총량은 그래디언트 크기의 두 배 언저리에 머문다. 그래서 링을 대역폭 최적(bandwidth-optimal)이라 부른다. 왜 하필 $2S$ 이고 $S$ 가 아닌지도 분해가 설명한다 — 합을 만드는 데 한 바퀴(reduce-scatter), 그 합을 나눠 갖는 데 또 한 바퀴(all-gather), 데이터가 고리를 두 번 실행되는 셈이라 정확히 두 배다.

통신 시간은 지연과 대역폭 두 항의 합으로 나온다. 스텝마다 고정 지연 $\alpha$ 가 붙고, 옮기는 바이트는 대역폭 $B$ 로 나뉜다.

$$T_{\text{ring}} \approx \underbrace{2(N-1)\,\alpha}_{\text{지연 항}} + \underbrace{2\,\frac{N-1}{N}\,\frac{S}{B}}_{\text{대역폭 항}}$$

두 항의 대비가 다음 절의 알고리즘 선택을 낳는다. 대역폭 항은 $N$ 에 거의 무관하지만, 지연 항은 스텝 수 $2(N-1)$ 이 그대로 들어가 $N$ 에 선형이다. 메시지 $S$ 가 커서 대역폭 항이 지배하면 링이 이상적이고, $S$ 가 작아 지연 항이 지배하면 링의 $O(N)$ 스텝이 독이 된다.

숫자를 넣어 보자. 7B 모델의 그래디언트를 fp16으로 all-reduce한다면 $S = 7\times10^9 \times 2 = 14$ GB다. 링의 GPU당 전송은 $\approx 2S = 28$ GB. 유효 대역폭 $B=100$ GB/s면 대역폭 항이 $28/100 = 0.28$ 초다. 이 값은 GPU가 512장이든 4096장이든 거의 그대로다 — 지연 항($2(N-1)\alpha$, $\alpha$ 가 마이크로초 규모)만 조금 늘 뿐이다. 그리고 이 0.28초는 스텝마다 발생한다. 이것을 backward 계산 뒤에 숨기지 못하면(2.5.8) 매 스텝에 그대로 추가되어 학습이 느려진다. 링이 대역폭 최적이라는 사실이 곧 "통신이 공짜"라는 뜻은 아닌 것이다 — 옮겨야 할 절대량 $2S$ 자체가 이미 부하가 크고, 그것을 계산 뒤에 감추는 것이 이 장의 다음 과제다.

2.5.7 ring 대 tree, 그리고 계층 알고리즘

링만으로는 부족한 경우가 지연 항에서 온다. 앞 식에서 지연 항이 $2(N-1)\alpha$ 로 $N$ 에 선형이므로, GPU가 많고 메시지가 작으면 이 항이 통신 시간을 지배한다. 트리(tree) 알고리즘이 여기서 등장한다. 이진 트리로 reduce와 broadcast를 하면 스텝 수가 $O(\log N)$ 으로 줄어 지연 항이 $2\log_2 N \cdot \alpha$ 수준으로 내려앉는다. 대신 트리는 큰 메시지에서 링만큼 대역폭을 살리지 못한다 — 루트 쪽 노드가 상대적으로 더 많은 트래픽을 받기 때문이다. 그래서 대략적인 판정은 이렇다. 큰 메시지·많은 GPU(그래디언트 동기화의 전형)에서는 링이, 작은 메시지·지연이 지배하는 경우에는 트리가 유리하다.

실제 라이브러리는 둘을 섞는다. NCCL은 메시지 크기가 커질수록 링 쪽으로, 작아질수록 트리(정확히는 이중 이진 트리) 쪽으로 자동 전환하고, 무엇보다 배선 위상을 알고리즘에 반영한다. 이것이 계층 알고리즘의 핵심 아이디어다 — 노드 안에서 8개를 NVLink로 먼저 합치고, 노드 대표끼리만 IB로 합친 뒤, 다시 노드 안으로 분배한다. 이렇게 하면 느린 노드 간 링크에는 "노드 대표들의 합"이라는 작은 트래픽만 보내고, 부하가 큰 대부분의 트래픽은 빠른 노드 내 링크에서 처리된다. 앞 절의 대역폭 격차(2.5.3)를 알고리즘이 흡수하는 셈이다.

이 모든 집합통신을 GPU 위에서·토폴로지를 인지해·최적 알고리즘으로 구현한 라이브러리가 NCCL(NVIDIA Collective Communications Library, 흔히 "니클")이다. 다른 벤더는 RCCL·oneCCL 등 대응물을 두며, PyTorch의 DDP·FSDP, DeepSpeed, Megatron 등 거의 모든 분산학습 스택이 그 아래에서 NCCL을 호출한다. NCCL이 대신 해 주는 일은 네 가지로 요약된다. 배선 위상을 발견해 최적 경로·알고리즘을 고르고, GPUDirect로 CPU·시스템 메모리를 우회해 GPU 메모리와 NIC 사이를 직접 잇고, 통신을 GPU 커널로 실행해 계산 스트림과 겹칠 수 있게 하고, 그 통신을 별도 CUDA 스트림에 제출해 계산과 병행시킨다. 마지막 두 가지가 다음 절 오버랩의 하드웨어적 근거다.

분산학습이 느리거나 멈추는 문제의 상당수는 모델이 아니라 이 통신 계층의 설정에서 온다. IB를 인식하지 못해 느린 이더넷으로 폴백하거나, NIC 바인딩이 어긋나거나, GPUDirect가 꺼져 있거나, 방화벽 탓에 특정 rank에 패킷이 도달하지 못해 집합통신이 영원히 대기하는 식이다. 어느 알고리즘과 경로가 실제로 선택됐는지 로그로 확인하는 것 — NCCL이라면 디버그 로그를 켜는 것 — 이 이런 문제를 푸는 출발점이며, 환경변수·플래그의 정확한 이름은 라이브러리 버전에 의존하니 공식 문서로 확인해야 한다.

2.5.8 연산과 통신을 겹치기

앞 절들에서 데이터 병렬 그래디언트 all-reduce가 스텝마다 수백 밀리초 걸릴 수 있음을 봤다. 만약 "계산 다 하고 → 통신 다 하고 → 다음 스텝"을 직렬로 밟으면 이 통신 시간이 고스란히 학습 시간에 더해진다. 대규모 학습의 가장 중요한 최적화 하나가 이 통신을 계산 뒤에 숨기는 것이다 — 계산이 실행되는 동안 통신을 병행해, 노출된 통신(exposed communication, 계산과 못 겹쳐 그대로 지연이 되는 통신)을 0에 가깝게 만든다.

숨길 여지는 역전파의 구조에서 나온다. 역전파는 출력층에서 입력층 방향으로 진행하며, 각 층의 그래디언트가 계산되는 즉시 그 층의 all-reduce를 시작할 수 있다. 마지막 층의 그래디언트가 나오자마자 통신을 시작해 두면, 앞쪽 층들의 backward 계산이 실행되는 동안 그 통신이 겹쳐서 흐른다. backward가 다 끝날 무렵에는 대부분의 그래디언트가 이미 동기화를 마친 상태가 되고, 노출되는 것은 가장 앞쪽 층의 통신 꼬리 정도만 남는다.

오버랩 없음 — 통신이 그대로 노출 오버랩 — 통신을 backward 뒤에 숨김 층 끝날 때마다 backward 전체 계산 all-reduce 전체 다음 스텝 backward 층 L..1 (계산) all-reduce (병행) 다음 스텝 (노출 통신 ≈ 0)

이것이 PyTorch DDP의 기본 동작이다. FSDP는 한 발 더 나간다. FSDP는 파라미터마저 GPU들에 분할해 두므로(2.4) 층을 계산하기 직전에 그 층 파라미터를 all-gather로 다시 모아야 하는데, 이 all-gather를 현재 층을 계산하는 동안 다음 층 몫으로 미리(prefetch) 실행해 겹친다. 그래디언트 쪽은 reduce-scatter로 backward와 겹친다. 파이프라인 병렬은 또 다른 방식으로 겹치는데, 1F1B(one-forward-one-backward) 같은 스케줄로 마이크로배치를 교차시켜 파이프라인 버블을 줄이면서 스테이지 간 활성값 전송을 계산에 숨긴다. 현대 프레임워크는 이렇게 계산·통신·메모리를 동시에 스케줄링한다.

사용자 관점의 원칙은 단순하다. 통신이 노출되면 MFU가 떨어진다. 그래서 대규모 학습 튜닝의 일상은 프로파일러로 GPU 타임라인을 열어, GPU가 통신 완료를 기다리며 유휴 상태인 빈 구간(스톨)을 찾아 오버랩을 조정하는 일이다. 겹칠 통신이 남아 있는데 GPU가 유휴 상태라면, 통신을 더 일찍 시작하거나 계산 순서를 바꿔 겹칠 여지를 만든다. 이 조정을 실제 통신 단위로 내리는 장치가 다음 절의 버킷화다.

2.5.9 버킷화와 노출 통신 관리

통신을 겹치려 해도, 통신을 어떤 단위로 분할해 발사하느냐에 따라 효율이 갈린다. 트랜스포머 한 층에는 큰 가중치 행렬(어텐션의 Q·K·V·O 투영, MLP의 업·다운 투영)뿐 아니라 정규화 스케일·편향 같은 아주 작은 텐서가 수십 개씩 섞여 있다. 모델 전체로 보면 수백 개의 파라미터 텐서가 있고, 이 각각에 따로 all-reduce를 적용하면 2.5.6의 지연 항 $2(N-1)\alpha$ 를 텐서 수만큼 되풀이해 물게 된다.

숫자로 보자. 파라미터 텐서가 400개이고 집합통신 하나의 고정 비용(커널 런치 + 핸드셰이크 + 지연)이 대략 50 마이크로초라면, 바이트를 옮기기도 전에 스텝마다

$$400 \times 50\,\mu\text{s} = 20\ \text{ms}$$

의 순수 오버헤드가 쌓인다. 한 스텝의 계산이 수백 밀리초 규모라면 이 20 ms는 몇 %의 손실이고, 게다가 작은 통신은 대역폭도 못 살리며(메시지가 짧아 대역폭 항이 지연 항에 묻힌다) 계산과 겹칠 기회도 줄인다. 버킷화(bucketing)가 이를 고친다. 여러 층의 그래디언트를 일정 크기 버킷(예: 25 MB)에 모아, 버킷이 차면 그 묶음 하나를 한 번에 all-reduce한다. 수백 개의 작은 통신이 수십 개의 큰 통신으로 합쳐져, 지연 항의 반복이 줄고 각 통신이 대역폭을 충분히 쓰는 크기가 된다.

버킷 크기에는 양쪽으로 함정이 있다. 버킷이 너무 작으면 통신 횟수가 늘어 지연 오버헤드가 커지고 오버랩 기회가 줄어든다. 버킷이 너무 크면 첫 통신이 늦게 시작되는 문제가 생긴다 — 역전파는 마지막 층부터 그래디언트를 내놓는데, 버킷이 크면 그 버킷이 다 찰 때까지 통신을 시작하지 못하고, 결국 backward가 끝날 무렵에야 통신이 시작되어 계산과 겹칠 시간이 없어진다. 그래서 버킷 크기는 "통신을 충분히 크게 묶되, 첫 통신을 backward 초반에 발사할 수 있을 만큼은 작게"라는 튜닝 대상이고, 프레임워크마다 기본값을 두되 모델·하드웨어에 따라 조정한다. 노출 통신을 프로파일에서 직접 보며 이 값을 맞추는 것이 오버랩 튜닝의 실무다.

2.5.10 데이터 로딩 — GPU 기아 방지 — 데이터 공급

수천 GPU가 초당 방대한 토큰을 소비하는데 데이터가 제때 도착하지 않으면 GPU는 유휴 상태다. 학습 데이터셋은 정제 후에도 수십~수백 TB라(2.1) 한 노드에 담을 수 없어, 데이터는 원격 오브젝트 스토리지에서 스트리밍된다. 그런데 대규모 학습에서 이 입력 파이프라인의 실제 병목이 어디인지는 흔히 오해된다.

먼저 자릿수를 파악하자. 데이터가 얼마나 빨리 도착해야 하는지는 클러스터의 토큰 소비율에서 나온다. 2.5.15에서 유도할 학습 시간 공식을 뒤집으면 초당 학습 토큰이 $n\cdot\text{FLOPS}\cdot\text{MFU}/(6N)$ 인데, 70B 모델을 1024 GPU(각 1 PFLOPS)로 MFU 40%에 가동하면 초당 약 100만 토큰이다. 사전 토크나이즈된 토큰 ID를 2바이트로 저장했다면 필요한 읽기 대역폭은

$$10^6\ \text{tok/s} \times 2\ \text{B/tok} = 2\ \text{MB/s}$$

에 불과하다. 오브젝트 스토리지의 집계 대역폭에 비하면 사실상 0이다. 이것이 흔한 오해를 깬다 — 거대한 클러스터에 데이터를 공급하는 데 필요한 원시 바이트 대역폭은 놀랍도록 작다. 그렇다면 왜 데이터 로딩이 GPU를 기아 상태로 만드는가? 병목은 대역폭이 아니라 접근 패턴과 CPU 전처리에 있다.

프리패치: 다음 배치를 미리 오브젝트 스토리지 (수십~수백 TB · 샤딩된 shard) 네트워크 fetch 디코드·토크나이즈 (CPU worker 여러 개) 셔플 버퍼 배치 구성 + pin memory GPU(H2D 전송)
병목증상대응
랜덤 접근(작은 파일)수백만 개 작은 파일 random read → 낮은 IOPS로 스톨큰 shard(수백 MB~GB)로 패킹, 순차 읽기
디코드·토크나이즈 CPUGPU는 유휴 상태이고 CPU 100%worker 수↑, 오프라인 사전 토크나이즈
셔플완전 셔플은 random read를 유발shard 순서 셔플 + 버퍼 내 로컬 셔플로 근사
H2D 전송CPU→GPU 복사 지연pinned memory + 비동기 복사, 프리패치

세 원칙이 이 대응들을 관통한다. 첫째, 순차 읽기로 바꾼다. 오브젝트 스토리지와 디스크는 큰 파일을 순차로 읽을 때 압도적으로 빠르고, 작은 파일을 무작위로 여는 것은 파일당 고정 비용(메타데이터 조회·연결 수립) 때문에 극도로 느리다. 그래서 수백만 개의 작은 샘플을 큰 shard로 묶고(WebDataset·tar류), 완전 셔플 대신 shard 순서를 섞고 각 shard를 읽어 온 뒤 버퍼 안에서 로컬 셔플로 근사한다. 둘째, 재개 가능한 스트리밍이어야 한다. 학습이 중단되고 재시작될 때(2.5.12) 데이터 순서를 정확히 이어가지 못하면 재시작마다 데이터 앞부분만 반복 학습해 편향이 생긴다 — 어느 shard·어느 샘플까지 소비했는지를 체크포인트에 함께 저장해야 한다. 셋째, CPU와 GPU 파이프라인을 겹친다. DataLoader worker가 다음 배치를 준비하는 동안 GPU가 현재 배치를 계산하도록 프리패치 깊이를 충분히 두고, 토크나이즈 같은 부하가 큰 전처리는 아예 학습 전에 오프라인으로 끝내 토큰 ID로 저장한다. 온-더-플라이 토크나이즈는 편해 보이지만 CPU를 병목으로 만들어 GPU를 기아 상태로 만드는 흔한 원인이고, 프로파일에서 GPU 유휴가 보이면 모델보다 먼저 이 데이터 경로를 의심하는 것이 맞다.

2.5.11 체크포인팅 — 분산·비동기·원자적

체크포인트는 모델 파라미터에 옵티마이저 상태, 스케줄러·스텝·데이터 위치, RNG 상태까지 담은 것으로, 학습이 죽었을 때 되살리는 유일한 수단이다. 대규모에서 이것은 만만치 않은데, 먼저 크기부터 크다. 옵티마이저 상태까지 세면 저장량이 파라미터의 몇 배로 증가한다 — fp32 파라미터에 Adam의 1차·2차 모멘텀을 더하면 파라미터당 대략 12바이트, fp16 가중치 사본까지 두면 더 든다. 7B 모델이면 $7\times10^9\times12\approx 84$ GB, 70B면 800 GB급이다. 이 큰 것을 학습 내내 수백~수천 번 저장하고 지워야 한다.

파라미터·옵티마이저 상태가 이미 여러 GPU에 분할되어 있으므로(FSDP·ZeRO·TP·PP), 각 rank가 자기 조각을 병렬로 저장하는 것이 자연스럽다.

단일 파일 (안티패턴) 분산 체크포인트 (권장) 전 rank → rank0로 gather → 파일 1개 병목: rank0 메모리· 직렬 쓰기 rank0 → shard_0 rank1 → shard_1 rank2 → shard_2 rankN → shard_N + 메타데이터(어느 조각이 어디)

모든 파라미터를 rank 0으로 모아 파일 하나로 저장하는 것은 안티패턴이다 — rank 0의 메모리를 넘기고 쓰기가 직렬이라 병목이다. 대신 각 rank가 자기 shard를 독립 파일로 병렬 저장하고, 어느 파라미터가 어느 shard에 있는지를 메타데이터로 남긴다(PyTorch Distributed Checkpoint·DeepSpeed 등이 지원). 저장 시간이 GPU 수에 따라 거의 나뉘고, 메타데이터 덕에 GPU 수나 병렬 구성이 바뀐 채로 재개(재샤딩)하는 것도 가능해진다.

저장 중 학습을 멈추면 그 시간만큼 GPU가 유휴 상태다. 비동기 체크포인트가 이를 숨긴다. GPU 상태를 CPU 메모리로 빠르게 복사한 뒤(짧게 멈추거나 별도 스트림) 학습을 즉시 재개하고, 백그라운드 스레드가 그 CPU 사본을 디스크·오브젝트 스토리지에 천천히 기록한다. 이러면 학습 중단이 "GPU→CPU 복사" 시간만으로 줄고, 느린 원격 쓰기는 계산과 겹친다. 대가는 CPU 메모리 여유가 필요하고, 앞 저장이 끝나기 전에 다음 체크포인트가 겹치지 않도록 관리해야 한다는 것이다. 그리고 원자성이 중요하다 — 저장 도중 중단되면 불완전한 체크포인트가 남을 수 있으므로, 임시 경로에 다 쓴 뒤 이름을 바꾸는(atomic rename) 방식으로 "완성된 것만 유효"하게 한다.

2.5.11.1 얼마나 자주 저장할까 — 최적 간격의 계산

체크포인트를 너무 자주 저장하면 저장 오버헤드와 스토리지 비용이 늘고, 너무 드물게 저장하면 죽었을 때 잃는 진척이 는다. 이 균형점에는 잘 알려진 근사 답이 있다. 체크포인트 저장 비용(멈추는 시간)을 $C$, 시스템의 평균 무고장 시간을 $M$(다음 절에서 다룬다), 저장 간격(그 사이 순수 계산 시간)을 $\tau$ 라 하자. 한 간격에서 낭비되는 시간은 두 몫이다 — 매 간격 지불하는 저장 비용 $C$, 그리고 그 간격 안에서 고장이 나면 마지막 체크포인트 이후의 진척을 재계산해야 하는 손실이다. 고장은 간격 안 어디서든 균일하게 날 수 있으니 평균적으로 간격의 절반 $\tau/2$ 를 잃고, 한 간격에서 고장 날 확률은 대략 $\tau/M$ 이다. 유효 계산 $\tau$ 당 낭비 비율은

$$f(\tau) \approx \frac{C}{\tau} + \frac{\tau}{2M}$$

로 쓸 수 있다. 앞 항은 저장 오버헤드(간격이 길수록 준다), 뒤 항은 기대 재계산 손실(간격이 길수록 는다)이다. 이 둘의 균형을 미분으로 찾자.

$$f'(\tau) = -\frac{C}{\tau^2} + \frac{1}{2M} = 0 \;\Longrightarrow\; \tau^{\star} = \sqrt{2\,C\,M}$$

이것이 Young·Daly 근사로 알려진 최적 체크포인트 간격이다. 저장이 저렴할수록·MTBF가 길수록 간격을 늘리고, 저장이 비용이 크거나 자주 중단되면 간격을 좁힌다는 직관이 제곱근 안에 정확히 담겨 있다. 숫자를 넣어 보자. 시스템 실효 MTBF가 50시간($M=1.8\times10^5$ 초), 저장에 2분 멈춘다($C=120$ 초)면

$$\tau^{\star} = \sqrt{2\times 120 \times 1.8\times10^{5}} \approx \sqrt{4.32\times10^{7}} \approx 6600\ \text{초} \approx 1.8\ \text{시간}$$

즉 대략 두 시간마다 저장하는 것이 최적이다. 이때 낭비 비율은 두 항이 같아져($C/\tau^\star = \tau^\star/2M$) $f(\tau^\star)=2\sqrt{C/2M}\approx 3.6\%$ — 잘 조율하면 장애가 이 정도 오버헤드에 머문다는 뜻이다. 비동기 체크포인트로 멈추는 시간 $C$ 를 몇 초 수준으로 줄이면 $\tau^\star$ 가 더 짧아져도 오버헤드가 낮아지니, 더 자주 저장해 재계산 손실까지 줄일 수 있다. 이 산식은 다음 절의 MTBF와 맞물려 "얼마나 자주 저장할지"를 어림하는 표준 도구다.

2.5.12 장애는 예외가 아니라 상수 — MTBF의 계산

작은 실험은 며칠간 중단 없이 실행된다. 수천 GPU 학습은 하루에도 여러 번 중단된다. 이것은 운이 나빠서가 아니라 확률의 필연이다.

한 부품(GPU·노드·NIC·전원)의 평균 무고장 시간(Mean Time Between Failures, MTBF)이 $M$ 이라 하자. 고장이 독립적이고 지수분포를 따른다고 보면 각 부품의 순간 고장률은 $\lambda = 1/M$ 이다. 동기 학습 작업은 신뢰성 관점에서 직렬 시스템이다 — 어느 한 부품이 고장 나면 그 rank가 집합통신에 참여하지 못해 전체 작업이 멈추기 때문이다. 직렬 시스템의 고장률은 부품 고장률의 합이므로, $n$ 개 부품을 묶은 시스템의 고장률은 $n\lambda = n/M$ 이고 실효 MTBF는 그 역수다.

$$M_{\text{sys}} = \frac{1}{n\lambda} = \frac{M}{n}$$

부품이 많아질수록 "무언가 하나가 고장 날 때까지의 시간"이 선형으로 짧아진다. 이 단순한 식이 대규모 학습의 장애 빈도를 지배한다. 숫자를 넣자. 노드 하나의 MTBF가 $M=50{,}000$ 시간이고 노드가 $n=1{,}000$ 대면 시스템 실효 MTBF는

$$M_{\text{sys}} \approx \frac{50{,}000}{1{,}000} = 50\ \text{시간}$$

즉 평균 이틀에 한 번은 어딘가 고장 난다. 부품을 노드가 아니라 GPU 단위(노드당 8개)로 세면 8배 더 잦고, GPU를 만 단위로 늘리면 실효 MTBF가 시간 단위로 떨어져 하루에도 여러 번 중단된다. 30일 학습이라면 그동안 기대 고장 횟수가

$$\frac{30 \times 24}{50} = \frac{720}{50} \approx 14\ \text{회}$$

로, 학습이 십수 번 중단된다는 뜻이다. 공개된 대규모 학습 로그들도 수십 일 학습에서 수십~수백 회 규모의 중단을 보고한다.

게다가 학습을 멈추는 것이 하드 고장만은 아니다. 한 GPU만 느려지는 열화(straggler), ECC 정정이 폭주하는 메모리, 순간적으로 끊기는 네트워크, 수치가 발산해(2.6) 발생하는 NaN이나 정지(hang)도 사실상 중단이다. 여기에 앞서 본 직렬 시스템의 취약성이 겹친다 — 집합통신은 전원 참여를 전제하므로, 한 GPU가 죽거나 느려지면 all-reduce가 완성되지 못해 나머지 전부가 그 하나를 기다리며 멈춘다. 하나가 고장 나면 전체가 중단되는 이 구조가, MTBF 계산이 예고하는 잦은 고장을 곧바로 전체 작업의 정지로 번역한다.

2.5.13 중단되는 것을 전제로 설계하기 — 탄력적 학습과 goodput

MTBF는 늘릴 수 없다 — 부품이 많은 것은 필요해서다. 그래서 대규모 학습은 중단되지 않게 만드는 대신, 중단되는 것을 전제로 빨리 되살리는 쪽으로 설계한다.

장애 정상 학습 진행 장애 감지 (hang·NaN·rank 소실) 불량 노드 격리·교체 (예비 노드 투입) 최신 체크포인트 로드 (2.5.11) 데이터 위치·RNG 복원 → 재개

핵심 장치는 몇 가지다. 자동 재시작은 오케스트레이터(Kubernetes·Slurm·전용 런처)가 중단된 작업을 감지해 최신 체크포인트에서 자동으로 재개하는 것으로, 사람이 새벽에 깨어 손댈 필요를 없앤다. 탄력적 학습(elastic training)은 GPU 수가 변해도 — 불량 노드가 빠지고 예비 노드가 들어와도 — 학습을 이어가게 하며, rank 재구성과 재랑데부(rendezvous)를 프레임워크가 처리한다(PyTorch의 torchrun elastic 등). 핫 스페어는 예비 노드를 미리 기동해 두어 교체 시간을 줄이고, 건강 검진(health check)은 학습 전·중에 GPU·NIC·NVLink를 진단해 불량 노드를 미리 빼거나 집합통신 타임아웃으로 좀비 상태를 빠르게 판별한다. 그리고 앞 절에서 본 빈번한·비동기 체크포인트가 재계산 손실을 최소화한다.

이 모든 장치가 겨냥하는 하나의 지표가 goodput이다. goodput은 전체 벽시계 시간 중 실제로 유효한 학습 진척에 쓰인 비율로, 잦은 중단·재시작·손실된 진척을 뺀 나머지다. 여기서 놓치기 쉬운 점은 재시작 자체도 무비용이 아니라는 것이다 — 장애를 감지하고(집합통신 타임아웃까지 기다리는 시간), 불량 노드를 진단·교체하고, 최신 체크포인트를 로드하고, 데이터 위치를 복원하는 각 단계가 모두 비활성화되어 있는 시간이다. 그래서 goodput을 지키려면 체크포인트 간격만 좁히는 것으로는 부족하고, 감지·교체·로드의 각 단계를 빠르게 만들어야 한다. MFU가 아무리 높아도 자꾸 중단되어 진척을 잃으면 소용없다는 것 — 대규모 학습의 실전 목표가 원시 MFU가 아니라 goodput인 이유가 여기 있다. 다음 두 절에서 그 MFU를 정의하고, 마지막으로 MFU와 goodput을 함께 넣어 학습 일수와 비용을 뽑는다.

2.5.14 MFU와 HFU — 하드웨어를 얼마나 살렸나

클러스터가 하드웨어 성능을 얼마나 살리는지 재는 표준 지표가 MFU와 HFU다. 먼저 분자가 되는 "필요한 연산량"부터 명시하자. 트랜스포머 학습에서 토큰 하나를 처리하는 데 드는 FLOPs는 대략 $6N$($N$ 은 파라미터 수)이다. 순전파가 파라미터당 곱셈+덧셈 한 번씩 $\approx 2N$, 역전파가 입력 그래디언트와 가중치 그래디언트를 각각 계산하느라 그 두 배인 $\approx 4N$, 합쳐 $6N$ 이다(2.2·2.3). 데이터셋 전체 $D$ 토큰이면 총 $\approx 6ND$ 다.

MFU(Model FLOPs Utilization, 모델 FLOPs 이용률)는 이 "원리상 필요한 FLOPs"의 처리율을 하드웨어 peak FLOPS 총합으로 나눈 비율이다.

$$\text{MFU} = \frac{6N \cdot (\text{초당 학습 토큰})}{n_{\text{GPU}}\cdot \text{FLOPS}_{\text{peak}}}$$

여기에 짝을 이루는 HFU(Hardware FLOPs Utilization)는 하드웨어가 실제로 실행한 모든 FLOPs를 — 불필요·중복까지 포함해 — 세어 peak 대비로 나눈 것이다. 둘의 차이는 활성값 재계산(activation recomputation)에서 온다. 메모리를 아끼려고 역전파 때 순전파의 일부를 다시 계산하면(2.4·2.6), 하드웨어는 그 여분 FLOPs를 실제로 실행한다. 그 여분까지 세면 HFU, "필요한 것만" 세면 MFU다. 재계산을 쓰면 토큰당 실제 FLOPs가 $8N$ 언저리로 늘지만 유효한 것은 여전히 $6N$ 이라, 항상

$$\text{HFU} \ge \text{MFU}$$

이고 재계산을 쓸수록 둘의 격차가 벌어진다. 두 지표는 서로 다른 질문에 답한다 — MFU는 "쓸모 있는 효율"(비용·시간 산정에 맞다), HFU는 "칩이 얼마나 바쁜가"(커널 최적화 여지를 본다)다. 재계산을 켜 놓고 MFU만 보면 그 낭비가 안 보이고, HFU만 보면 재계산이 정말 유효 처리량을 잠식하는지 안 보인다. 그래서 둘을 함께 봐야 한다.

전형적 대규모 트랜스포머 학습의 MFU는 대략 30~50% 수준이다. 왜 100%가 안 나오는지를 항목별로 보면, 이 장 전체가 그 목록이었음이 드러난다.

손실 원인무엇완화
비-행렬곱 연산어텐션 softmax·정규화·활성함수·드롭아웃이 대역폭 바운드커널 융합·FlashAttention(1.4)
노출된 통신all-reduce·all-gather가 계산과 못 겹친 구간오버랩·버킷·토폴로지 매핑(2.5.8)
파이프라인 버블PP에서 스테이지가 유휴 상태인 구간마이크로배치↑·1F1B·인터리브
부하 불균형MoE 전문가 쏠림·시퀀스 길이 편차로드 밸런싱 손실(1.6)·패킹
데이터 스톨입력 파이프라인이 GPU를 기아 상태로 만듦프리패치·사전 토크나이즈(2.5.10)
커널 오버헤드커널 런치·동기화·작은 연산 비용CUDA Graphs·큰 배치
재계산 오버헤드활성값 재계산의 여분 FLOPs메모리와의 트레이드오프(HFU로 관찰)

peak FLOPS는 "행렬곱만, 데이터가 항상 대기, 통신 0, 부하 완벽 균형"이라는 이상에서만 나오는 숫자다. 실제 학습은 이 이상에서 벗어난 만큼 MFU를 잃고, 그래서 MFU를 50% 근처로 높이는 것 자체가 상당한 엔지니어링이다. 여기서 흔한 직관 하나를 정면으로 반박해 둘 필요가 있다 — "GPU를 두 배로 늘리면 학습이 두 배 빨라진다"는 대체로 틀리다. 규모가 커지면 통신 비중, 특히 노출 통신이 늘고 MFU가 떨어져, 스케일 효율(같은 일을 GPU 수에 반비례하는 시간에 끝내는 비율)이 1보다 작아진다. 예컨대 1024장에서 2048장으로 늘렸을 때 처리량이 1.7배만 오르면 스케일 효율은 85%다. 이 감소를 얼마나 막느냐가 인프라의 실력이고, 그 실력이 바로 다음 절의 비용 계산에 곧장 반영된다.

2.5.15 학습 시간·GPU-시간·비용의 수계산

MFU 정의를 뒤집으면 학습 시간을 어림하는 공식이 곧장 나온다. 필요한 총연산 $6ND$ 를 클러스터가 실제로 내는 유효 FLOPS로 나누면 된다.

$$T \approx \frac{6ND}{n_{\text{GPU}}\cdot \text{FLOPS}_{\text{peak}}\cdot \text{MFU}}$$

분자는 학습에 필요한 총 FLOPs, 분모는 클러스터의 유효 처리율(peak에 MFU와 GPU 수를 곱한 것)이다. 여기서 peak가 아니라 MFU를 곱한다는 점이 결정적이다 — peak로 계산하면 실제의 두세 배 낙관적인 일정이 나온다.

70B 모델을 2조 토큰으로 학습한다고 하자. $N = 7\times10^{10}$, $D = 2\times10^{12}$ 이면 총 연산은

$$C = 6ND = 6 \times 7\times10^{10} \times 2\times10^{12} = 8.4\times10^{23}\ \text{FLOPs}$$

GPU당 BF16 peak를 $10^{15}$ FLOPS, MFU를 40%, GPU 수를 1024라 하면 분모는

$$1024 \times 10^{15} \times 0.4 = 4.096\times10^{17}\ \text{유효 FLOPS}$$

이고 학습 시간은

$$T \approx \frac{8.4\times10^{23}}{4.096\times10^{17}} \approx 2.05\times10^{6}\ \text{초} \approx \frac{2.05\times10^{6}}{86400} \approx 23.7\ \text{일}$$

이다. 이것을 GPU-시간으로 환산하면

$$\text{GPU-시간} = n_{\text{GPU}} \times \frac{T}{3600} = 1024 \times \frac{2.05\times10^{6}}{3600} \approx 5.8\times10^{5}\ \text{GPU-시간}$$

이고, GPU-시간 단가를 대략 \$2라 하면 직접 컴퓨트 비용이 $5.8\times10^{5}\times 2 \approx$ \$1.17M이다. 이 값은 순수 계산 시간만 센 것임을 잊지 말자 — 여기에 goodput 손실(재시작·유휴, 2.5.13)로 실제 벽시계 시간과 청구 시간이 20~50% 더 늘 수 있고, 스토리지·네트워크·인건비는 별도다.

이 공식의 실제 쓸모는 수단을 파악하게 해 준다는 데 있다. MFU를 40%에서 50%로 높이면 분모가 그 비율만큼 커져 시간과 비용이 약 20% 준다 — 이 장이 다룬 오버랩·버킷·토폴로지 매핑·데이터 파이프라인 튜닝의 직접 보상이 여기 있다. GPU를 두 배로 늘리면 시간이 절반이 될 것 같지만, 앞 절에서 봤듯 MFU가 스케일에 따라 떨어지므로 실제로는 절반보다 덜 준다. 모델과 토큰을 키우면($N, D$) 분자 $6ND$ 가 선형으로 커지는데, 이것이 스케일링 법칙(2.2)이 말하는 "컴퓨트 예산"의 정확한 정체다 — 예산을 정한다는 것은 곧 이 분자를 정하는 것이고, 그것을 며칠에 얼마로 끝낼지가 이 장의 분모에 달려 있다.

물론 이 공식은 차수 감각용 어림식이다. 재계산으로 갈리는 HFU와 MFU, 시퀀스 길이에 따라 어텐션이 더하는 $O(s^2)$ FLOPs, 임베딩·언임베딩, 파이프라인 워밍업 등으로 보정이 필요하다. 그럼에도 "필요 FLOPs ÷ 달성 FLOPS"라는 골격은 확고하며, 예산·일정·GPU 수를 처음 가늠할 때 가장 먼저 꺼내는 도구다.

이 장은 병렬화 전략(2.4)을 케이블과 스위치와 HBM 위에 적층했다. 인터커넥트의 두 세계가 병렬화 축의 배치를 강제하고, 집합통신이 그래디언트를 실어 나르며, 오버랩과 버킷이 그 통신을 계산 뒤에 숨기고, 데이터 파이프라인과 체크포인트가 GPU를 기아 상태로 만들지도 중단된 채 두지도 않게 하고, MTBF 계산이 장애를 예외에서 상수로 바꾸며, MFU가 이 모든 것을 하나의 효율 숫자로 압축한다. 그러나 인프라가 아무리 튼튼해도 그 위에서 실행되는 수치가 발산하면 학습은 중단된다 — 손실이 발산하고 NaN이 번지면 십수 번의 장애 복구도 소용이 없다. 다음 장 2.6는 그 위에서 학습이 수치적으로 살아남는 법, FP16·BF16·FP8 혼합정밀도의 선택과 손실 스케일링, 손실 스파이크와 발산의 진단과 대응을 다룬다.