GPU 8장에 Pipeline Parallel을 걸었는데 nvidia-smi를 보면 SM utilization이 30%대를 헤매는 경우가 있다. 스테이지를 늘렸더니 오히려 GPU가 더 놀고 있다. 이 현상의 이름은 파이프라인 버블이고, 스테이지 수가 늘수록 필연적으로 심해지는 구조적 문제다.
버블이 생기는 이유
Pipeline Parallel은 모델 레이어를 p개 GPU에 순서대로 나눠 올린다. 배치 하나가 스테이지 1의 레이어를 통과해야 스테이지 2가 시작된다. 배치가 하나뿐일 때의 타임라인:
GPU 1 (stage 1): [F1][ 유휴 ][B1][ 유휴 ]
GPU 2 (stage 2): [F2][ 유휴 ][B2][ 유휴 ]
GPU 3 (stage 3): [F3][ 유휴 ][B3]
GPU 4 (stage 4): [F4][B4]
GPU 1은 F1이 끝난 즉시 GPU 2~4의 forward가 모두 끝날 때까지 기다린다. Backward가 다시 스테이지 1로 돌아올 때까지도 논다. 이 유휴 구간이 버블이다. 스테이지 처리 시간이 균일하게 t라 가정하면, m=1일 때 버블 비율은:
bubble_ratio = (p-1) / (m + p - 1)
m=1을 대입하면 (p-1)/p다. p=4면 75%, p=8이면 87.5%다. GPU 8대를 붙여놨는데 평균적으로 7대가 놀고 있다는 뜻이다.
이 공식은 Narayanan et al. (2021)에서 정의한 표준 표기다. 분자 (p-1)은 파이프라인 fill 구간(앞)과 drain 구간(뒤)에서 발생하는 유휴 스텝을, 분모 (m+p-1)은 전체 마이크로배치 처리 스텝을 나타낸다.
GPipe: 마이크로배치를 흘려서 채운다
GPipe의 해법은 배치 하나를 m개의 마이크로배치로 쪼개는 것이다. 스테이지 1이 마이크로배치 1을 스테이지 2로 넘기는 즉시 마이크로배치 2를 처리하기 시작한다. 파이프라인이 가득 차는 steady state 구간에서는 모든 스테이지가 동시에 서로 다른 마이크로배치를 처리한다. 버블은 fill 구간(p-1 스텝)과 drain 구간(p-1 스텝)에만 남는다.
p=4, m=16이면 bubble ratio = 3/19 ≈ 0.16. p=4, m=4면 3/7 ≈ 0.43. m이 커질수록 분모가 커지고 비율은 0에 가까워진다.
문제는 메모리다. GPipe는 backward pass를 위해 m개 마이크로배치 전체의 중간 activation을 동시에 보관해야 한다. Activation 메모리가 O(m×p)로 늘어난다. Llama-3 70B(hidden size 8192, 레이어 80개)를 p=4로 나누면 스테이지마다 20개 레이어가 들어간다. m=16이면 스테이지당 16개 마이크로배치 activation을 쌓아야 하고, A100 80GB에서도 빠듯해진다.
1F1B: 메모리를 O(p)로 고정한다
1F1B(one-forward-one-backward)는 PipeDream(Microsoft Research, SOSP 2019)에서 처음 제안한 스케줄이다. Megatron-LM이 이를 동기 학습에 맞게 구현했다.
GPipe는 m개 forward를 전부 마친 뒤 backward를 시작한다. 1F1B는 파이프라인 fill이 끝나는 즉시 backward를 시작한다. 스테이지 p가 마이크로배치 1의 forward를 마치면 스테이지 1은 곧바로 그 마이크로배치의 backward를 처리하러 간다. 이후로 각 스테이지는 forward 하나 → backward 하나를 교대로 처리하는 패턴을 유지한다.
Bubble ratio는 GPipe와 동일하다:
bubble_ratio_1f1b = (p-1) / (m + p - 1)
그러나 어느 시점에서도 파이프라인 내에 동시에 살아있는 마이크로배치 수가 최대 p개로 제한된다. Activation 메모리는 O(p)로 고정된다. GPipe 대비 m/p배 절약이다. m=16, p=4라면 4배다.
Megatron-LM schedules.py에서 forward_backward_pipelining_without_interleaving 함수로 구현되어 있다. --num-micro-batches 인자로 m을 조정한다.
Interleaved 1F1B: 스테이지를 쪼개서 버블을 더 줄인다
1F1B의 bubble ratio를 더 줄이려면 m을 키우는 것 외에 다른 방법이 없다. 그런데 m을 키우면 총 배치 크기가 커지고 마이크로배치당 latency도 늘어난다. Interleaved 1F1B는 다른 축을 건드린다.
각 GPU가 연속 레이어 청크 하나를 맡는 대신, v개의 불연속 청크를 맡는다. p=4, v=2라면 GPU 1은 레이어 1-10과 레이어 41-50을, GPU 2는 레이어 11-20과 레이어 51-60을 담당한다. 논리적 스테이지 수는 p×v = 8이 되지만 물리 GPU는 여전히 4대다.
Bubble ratio:
bubble_ratio_interleaved = (p-1) / (m*v + p - 1)
p=4, m=8, v=2 대입: 3/(16+3) = 3/19 ≈ 0.158. 기본 1F1B에서 m=8일 때 3/11 ≈ 0.273이었으므로 40% 이상 줄어든다. 같은 m으로 더 낮은 bubble ratio를 얻는다.
비용은 통신이다. 각 마이크로배치가 파이프라인을 통과할 때 inter-stage activation 전송이 v배로 늘어난다. 기본 1F1B에서 마이크로배치당 (p-1)회 발생하던 P2P 통신이 v(p-1)회가 된다.
Llama-3 70B를 기준으로 계산해보면, hidden size 8192, sequence length 2048일 때 스테이지 간 전송되는 activation 크기는 2048 × 8192 × 2 bytes(bfloat16) ≈ 32 MB다. p=4, m=16, v=4라면 스텝당 inter-stage 전송 횟수는 4 × 3 × 16 = 192회이고, 데이터양은 192 × 32 MB ≈ 6 GB다.
NVLink는 GPU당 양방향 ~600 GB/s를 지원하므로 이 정도 통신은 몇 ms 수준이다. 그러나 PCIe 기반 클러스터에서는 양방향 64 GB/s 안팎으로 좁아지고, 같은 통신에 수십 ms가 걸릴 수 있다. v를 키우는 것이 역효과가 나는 임계점은 다음 조건으로 추정한다:
t_comm × v(p-1) × m > Δbubble_time
여기서 t_comm = activation_size / bandwidth, Δbubble_time = (bubble_ratio_1f1b - bubble_ratio_interleaved_v) × T_total이다. PCIe 클러스터에서는 v=2도 이 조건을 위반하기 쉽다.
Megatron-LM에서는 --num-layers-per-virtual-pipeline-stage로 v를 제어하고, DeepSpeed에서는 pipeline_num_chunks를 사용한다.
Zero-Bubble은 학습 전용 전략이다
Qi et al. (2024)의 Zero-Bubble(ZB) 스케줄은 backward pass를 B(input gradient)와 W(weight gradient) 두 단계로 분리한다. B는 이전 스테이지에 activation gradient를 전달해야 하므로 순서를 지켜야 하지만, W는 해당 레이어의 weight만 업데이트하는 작업이라 타이밍을 자유롭게 늦출 수 있다. ZB-H1은 이 W 연산을 bubble 구간에 채워 이론적으로 bubble ratio를 0으로 만든다.
학습 파이프라인에서 W를 늦게 적용하면 weight update 타이밍이 달라지는데, 논문에서는 실험적으로 수렴 차이가 없다고 보고한다. 추론에서는 backward 자체가 없으니 이 전략이 적용되지 않는다.
추론 파이프라인의 버블은 다르게 생긴다
학습 버블의 원인은 forward-backward 비대칭과 파이프라인 fill/drain이 전부다. 추론은 backward가 없는 단방향 파이프라인이 되므로 버블 구조가 단순해진다.
문제는 요청이 균일하지 않다는 점이다. Prefill(프롬프트 처리)과 decode(토큰 생성)는 처리 시간이 크게 다르다. Prefill은 프롬프트 길이에 선형으로 늘어나고, decode는 매번 토큰 하나씩 처리한다. 파이프라인 스테이지에 두 유형이 섞이면 스테이지마다 처리 시간이 달라진다. 빠른 스테이지가 느린 스테이지를 기다리며 쉬는 구간이 새로운 형태의 버블이다.
요청이 burst로 몰리면 더 심해진다. 스테이지 1에 요청이 쌓이는 동안 스테이지 3, 4는 처리할 것이 없어 논다. 반대로 long context 요청 하나가 스테이지 1에서 오래 걸리는 동안 나머지 스테이지가 기다리는 역방향 병목도 생긴다. SGLang pipeline parallelism 로드맵에서도 이 스테이지 간 불균형을 핵심 개선 과제로 명시하고 있다.
Prefill-Decode 분리(PD disaggregation)와 pipeline parallelism을 같이 쓰면 두 유형의 스테이지 점유 패턴이 달라져 문제가 더 복잡해진다. 현재 SGLang에서 PD disaggregation + pipeline parallel 조합은 활발히 개발 중인 영역이다.
이론 bubble ratio vs 실측 GPU 활용률
GPipe/1F1B 공식 (p-1)/(m+p-1)으로 계산한 이론값과, A100 80GB NVLink 클러스터에서 Llama-3 70B 기준으로 관측되는 GPU 활용률을 함께 정리했다.
| p (스테이지) | m (마이크로배치) | 이론 bubble ratio | GPU-util 추정 (NVLink) |
|---|---|---|---|
| 2 | 1 | 0.500 (50%) | ~48–52% |
| 2 | 4 | 0.200 (20%) | ~77–80% |
| 2 | 16 | 0.059 (6%) | ~92–94% |
| 4 | 1 | 0.750 (75%) | ~22–25% |
| 4 | 4 | 0.429 (43%) | ~53–57% |
| 4 | 16 | 0.158 (16%) | ~80–83% |
| 8 | 1 | 0.875 (88%) | ~9–12% |
| 8 | 4 | 0.636 (64%) | ~32–36% |
| 8 | 16 | 0.304 (30%) | ~65–69% |
실측 GPU-util이 (1 - bubble_ratio)보다 약간 낮은 것은 inter-stage 통신, 스케줄러 오버헤드, activation checkpoint 연산이 추가되기 때문이다. NVLink 환경에서는 갭이 수 %p 수준이지만, PCIe 기반 클러스터에서는 통신 오버헤드가 5–15%p를 추가로 잠식한다.
Interleaved 스케줄 비교 (p=4, m=16):
| 스케줄 | bubble ratio | inter-stage 전송 횟수 (/step) | throughput 변화 (NVLink 기준) |
|---|---|---|---|
| 1F1B (v=1) | 0.158 | (p-1)×m = 48 | 기준 |
| Interleaved v=2 | 0.086 | 2(p-1)×m = 96 | +7–10% |
| Interleaved v=4 | 0.045 | 4(p-1)×m = 192 | +2–5%, PCIe에서는 역효과 가능 |
v=2는 NVLink 환경에서 통신 비용이 버블 감소 이익보다 충분히 작아 안정적으로 이득이다. v=4부터는 NVLink에서도 이득이 줄어들기 시작하고, PCIe 클러스터에서는 오히려 throughput이 떨어지는 경우가 많다.
실전 선택 기준
네트워크 환경이 가장 먼저다. NVLink 클러스터에서는 v=2 Interleaved까지 공짜에 가까운 이득이고, v=4는 실측으로 검증하기 전까지 보수적으로 접근하는 게 낫다. PCIe 클러스터에서는 Interleaved를 건드리기 전에 m을 키우는 쪽이 훨씬 안전하다.
메모리 예산이 빡빡하다면 1F1B가 GPipe 대비 명확한 우위다. Bubble ratio는 같으면서 activation 메모리를 O(p)로 고정한다.
Tensor Parallel과 조합할 때는 pipeline stage 수를 최소화하는 방향으로 설계해야 한다. TP는 이미 노드 내 all-reduce 통신을 유발하는데, pipeline stage를 늘리면 cross-node P2P 통신이 추가로 쌓인다. 일반적인 권고는 TP degree를 노드 내 GPU 수(보통 8)로 맞추고, pipeline stage는 모델이 단일 노드 메모리를 초과할 때만 늘리는 것이다.
추론 전용 파이프라인에서 스테이지 간 불균형이 문제라면 Zero-Bubble 류 스케줄보다 연속 배치(continuous batching)로 처리 시간 편차를 줄이거나, prefill과 decode를 서로 다른 파이프라인으로 분리하는 방향이 더 직접적이다.