모델 운영

MLOPS / 59번째 글

파이프라인 병렬 — 층을 나눠 맡을 때 생기는 빈틈

층을 그룹으로 잘라 카드마다 맡기면 통신은 거의 안 붙지만 카드 대부분이 노는 시간이 생깁니다. 그 빈틈이 어디서 오는지, 어떻게 줄이는지, 텐서 병렬과 어떻게 섞는지 정리합니다.

PALDYN Team13 MIN READ

지난 글에서 텐서 병렬은 한 서버 안에서만 쓴다고 했다. 층마다 두 번씩 All-Reduce가 돌기 때문에 서버 사이 연결로는 감당이 안 된다는 이야기였다. 그러면 모델이 서버 하나보다 크면 어떻게 하는가. 그 자리가 파이프라인 병렬이다.

층을 그룹으로 잘라 맡긴다

발상은 텐서 병렬보다 훨씬 단순하다. 80층짜리 모델이 있으면 카드 넷이 20층씩 나눠 든다. 요청은 GPU 0에서 시작해 1층부터 20층을 지나고, 그 결과를 GPU 1에 넘겨 21층부터 40층을 지나고, 그렇게 마지막 카드까지 흘러간다. 한 카드가 맡는 구간을 스테이지(stage)라고 부른다.

통신량이 확 다르다는 점이 이 방식의 값이다.

텐서 병렬 파이프라인 병렬
통신이 일어나는 곳 층마다 두 번 스테이지 경계에서 한 번
층 80개, 카드 4장이면 걸음마다 160번 걸음마다 3번
무엇을 보내나 층 출력 전체를 모두에게(All-Reduce) 경계의 활성값을 옆 카드에게만
필요한 연결 NVLink 급 이더넷으로도 된다

한 방향으로 한 번만 보내면 된다는 점이 특히 크다. All-Reduce는 모두가 모두와 값을 맞춰야 끝나는 연산이라 가장 느린 링크가 전체를 잡아먹는데, 파이프라인은 옆 카드에게 활성값 한 덩어리를 던지고 끝이다. 서버 경계를 넘길 수 있는 이유가 여기 있다.

그런데 카드 대부분이 논다

대신 다른 문제가 생긴다. 요청 묶음 하나를 통째로 흘려보내면, GPU 0이 일하는 동안 나머지 셋은 자기 차례를 기다린다.

파이프라인의 빈틈은 잘게 썰어야 줄어든다

위쪽이 그 모습이다. 카드 넷을 쓰는데 어느 순간에도 한 장만 일하니 이용률이 25%다. 이 노는 자리를 파이프라인 버블(pipeline bubble)이라고 부른다.

해법은 공장의 컨베이어와 같다. 한 덩어리를 통째로 밀지 말고 잘게 썰어 연달아 흘려보낸다. 아래쪽처럼 네 조각으로 나누면 GPU 0이 두 번째 조각을 잡을 때 GPU 1이 첫 조각을 받으므로 계단이 채워진다. 이 조각을 마이크로배치(micro-batch)라고 한다.

버블의 비율은 식으로 나온다. 스테이지 수를 pp, 마이크로배치 수를 mm이라 하면

버블 비율=p−1m+p−1\text{버블 비율} = \frac{p-1}{m + p - 1}

카드 넷에 조각 넷이면 3/7≈0.433/7 \approx 0.43이라 이용률이 57%다. 조각을 여덟으로 늘리면 3/11≈0.273/11 \approx 0.27, 열여섯이면 3/19≈0.163/19 \approx 0.16이다. 조각을 잘게 썰수록 빈틈이 줄지만 0이 되지는 않는다 — 파이프라인을 채우는 시작과 비우는 끝은 남는다.

추론에서는 사정이 다르다

여기까지가 학습에서 쓰던 이야기이고, 추론에는 비틀린 지점이 있다.

학습은 배치 하나가 크고 계산이 무거워서 잘게 써는 것이 자연스럽다. 그런데 디코드 단계는 요청당 토큰 하나를 만드는 아주 짧은 작업이라, 그것을 다시 넷으로 쪼개면 조각이 너무 작아져 GPU를 못 채운다. 마이크로배치를 만들려고 잘랐는데 그 조각들이 각각 GPU를 놀리는 꼴이 된다.

그래서 추론 엔진은 다르게 푼다. 연속 배칭의 요청들을 그대로 마이크로배치로 쓴다. 지금 돌고 있는 요청이 64개면 그것을 16개씩 네 묶음으로 나눠 파이프라인에 연달아 밀어 넣는다. 새로 쪼개는 것이 아니라 이미 여럿인 것을 나눠 보내는 것이라 조각이 작아지지 않는다.

여기서 실무 결론이 하나 나온다. 동시 요청이 적으면 파이프라인 병렬은 이득이 거의 없다. 흘려보낼 조각이 없으니 버블이 그대로 남는다. 동시 요청이 충분히 많아야 계단이 채워진다.

지연은 오히려 나빠진다

텐서 병렬과 정반대라는 점을 분명히 해 두는 것이 좋다.

  • 텐서 병렬은 층 하나의 계산을 나누므로 요청 하나가 빨라진다. 대신 통신이 층마다 붙어 효율이 떨어진다
  • 파이프라인 병렬은 요청 하나가 여전히 모든 층을 순서대로 지나야 하므로 빨라지지 않는다. 오히려 스테이지 경계를 넘는 시간이 붙어 조금 느려진다

파이프라인이 벌어 주는 것은 지연이 아니라 담을 수 있는 모델 크기와 총 처리량이다. 첫 토큰이 급한 대화형 서비스에서 카드가 남는다면 파이프라인보다 텐서 병렬 쪽을 먼저 본다.

층을 균등하게 나누기

버블 말고 조용히 성능을 깎는 것이 하나 더 있다. 가장 느린 스테이지가 전체 속도를 정한다. 컨베이어에서 한 공정이 느리면 앞뒤가 다 그 속도에 맞춰지는 것과 같다.

층 수를 똑같이 나눠도 실제 부하가 같지 않은 경우가 있다.

  • 첫 스테이지는 임베딩 층을 함께 든다
  • 마지막 스테이지는 어휘 크기만큼 큰 출력 층을 든다. 어휘가 12만 개면 이 행렬 하나가 층 몇 개 값이다
  • 층 수가 스테이지 수로 나눠떨어지지 않으면 한 스테이지가 한 층을 더 든다

그래서 엔진들은 층 수가 아니라 걸리는 시간이 비슷해지도록 배분을 조정하는 옵션을 준다. 실측해서 스테이지별 처리 시간을 보고, 한쪽이 뚜렷이 느리면 층을 한둘 옮긴다.

텐서 병렬과 섞기

실무에서는 둘을 함께 쓴다. 규칙은 연결이 빠른 경계일수록 통신이 잦은 방식을 둔다는 것 하나다.

  • 서버 안(NVLink) → 텐서 병렬
  • 서버 사이(이더넷·InfiniBand) → 파이프라인 병렬

카드 8장짜리 서버 두 대로 아주 큰 모델을 띄운다면, 서버마다 텐서 병렬 8, 서버 둘 사이에 파이프라인 병렬 2가 자연스러운 배치다.

vllm serve <아주-큰-모델> \
  --tensor-parallel-size 8 \
  --pipeline-parallel-size 2 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 256

--max-num-seqs를 넉넉히 두는 것이 파이프라인에서는 더 중요하다. 흘려보낼 요청이 있어야 계단이 채워지기 때문이다.

언제 쓰고 언제 안 쓰나

쓸 자리

  • 모델이 서버 한 대에 안 들어간다 — 선택의 여지가 없다
  • 카드는 여럿인데 NVLink가 없다. 텐서 병렬이 PCIe에서 죽는 상황이라면 파이프라인이 덜 손해다
  • 동시 요청이 많은 일괄 처리 부하다. 버블을 채울 조각이 넉넉하다

피할 자리

  • 모델이 한 장에 들어간다. 그러면 카드마다 독립 인스턴스를 띄우는 편이 통신 0이라 거의 항상 낫다
  • 동시 요청이 적다. 버블이 그대로 남는다
  • 첫 토큰까지의 시간이 중요하다. 파이프라인은 그것을 개선하지 않는다

흔한 함정

  • 파이프라인을 지연 개선책으로 기대한다. 늘어나면 늘어났지 줄지 않는다
  • 동시성 1로 부하 시험을 한다. 이용률 25%짜리 그림이 그대로 나오고, 그 숫자로 「파이프라인은 느리다」고 결론 내린다
  • 스테이지별 시간을 안 본다. 층 수만 맞춰 놓고 마지막 스테이지의 출력 층이 병목인 줄 모른다
  • 서버 사이에 텐서 병렬을 건다. 되기는 되는데 통신에 잡아먹혀 카드를 늘린 보람이 없다
  • 양자화를 먼저 시도하지 않는다. 파라미터당 바이트를 절반으로 줄이면 아예 안 나눠도 되는 경우가 많다. 나누는 것은 그다음이다

정리

  • 파이프라인 병렬은 층을 그룹으로 잘라 카드마다 다른 구간을 맡긴다. 그 구간을 스테이지라고 한다
  • 통신이 스테이지 경계에서 한 방향으로 한 번뿐이라, 텐서 병렬과 달리 서버 경계를 넘길 수 있다
  • 대신 요청 묶음을 통째로 밀면 한 장만 일하고 나머지는 논다. 이 빈틈이 파이프라인 버블이다
  • 잘게 썰어 연달아 흘려보내면 계단이 채워진다. 버블 비율은 (p−1)/(m+p−1)(p-1)/(m+p-1)이고 조각을 늘릴수록 준다
  • 추론의 디코드는 원래 작아서 더 쪼개면 GPU를 못 채운다. 대신 연속 배칭의 요청들을 나눠 마이크로배치로 쓴다
  • 그래서 동시 요청이 적으면 이득이 거의 없다. 흘려보낼 조각이 있어야 한다
  • 텐서 병렬은 요청 하나를 빠르게 하고 파이프라인 병렬은 그렇지 않다. 파이프라인이 벌어 주는 것은 모델 크기와 총 처리량이다
  • 가장 느린 스테이지가 전체 속도를 정한다. 임베딩과 출력 층 때문에 층 수만 맞추면 균형이 안 맞는다
  • 섞을 때의 규칙은 하나다 — 서버 안은 텐서 병렬, 서버 사이는 파이프라인 병렬
  • 나누기 전에 양자화부터 본다. 파라미터당 바이트를 줄이면 안 나눠도 되는 경우가 많다

읽어주셔서 감사합니다. 😊

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

KV 캐시 양자화 — 가중치보다 이쪽이 먼저 넘친다

긴 문맥에서 GPU 메모리를 실제로 잡아먹는 것은 가중치가 아니라 KV 캐시입니다. 캐시 크기를 계산하는 법, K와 V를 다르게 다뤄야 하는 이유, 어디까지 줄여도 되는지를 정리합니다.

16 MIN
모델 운영2026.09.04

양자화 보정 — 데이터 128개가 모델 품질을 정한다

양자화에서 스케일을 정하는 절차가 보정입니다. 무엇을 재는지, 데이터를 어디서 몇 개 뽑아야 하는지, 자르는 지점을 어떻게 고르는지, 그리고 잘못된 보정이 어떤 모양으로 드러나는지를 정리합니다.

21 MIN
모델 운영2026.09.03

과제 특화 증류 — 큰 모델의 답을 작은 모델에 옮긴다

범용 성능이 아니라 우리 과제 하나만 잘하는 작은 모델을 만드는 방법입니다. 교사에게 무엇을 받아야 하는지, 데이터를 어떻게 모으고 거르는지, 손익 분기가 어디인지를 정리합니다.

15 MIN