RSS
AI 자동화

GPU 병렬화 세 가지: 데이터 병렬, 텐서 병렬, 파이프라인 병렬

작성자
듀오랩스 대표·19분 읽기

vLLM 문서에 GPU 16장으로 모델 하나를 띄우는 예시가 있습니다. 서버 두 대에 8장씩 꽂힌 구성인데, 명령에는 숫자가 두 개 들어갑니다.

vllm serve /path/to/the/model/in/the/container \
    --tensor-parallel-size 8 \
    --pipeline-parallel-size 2 \
    --distributed-executor-backend ray

16장이면 그냥 16으로 나누면 될 것 같은데, 문서는 8과 2로 갈라 적고 이것이 흔한 방식이라고 설명합니다. 같은 문서에 --tensor-parallel-size 16 하나로 쓰는 예시도 있으니 둘 다 돌아가기는 합니다. 그런데도 굳이 둘로 나누는 데는 이유가 있고, 그 이유가 이 글의 주제입니다.

GPU를 늘리면 빨라진다는 기대가 빗나가는 조건

흔히 GPU를 두 배로 늘리면 두 배 빨라진다고 생각합니다. 여러 GPU에 모델을 올리는 방법도 하나쯤으로 여깁니다. 둘 다 틀렸습니다.

일을 나누는 축은 적어도 세 가지입니다. 데이터를 나눌 수도 있고, 층 안의 행렬을 나눌 수도 있고, 층을 순서대로 끊어 나눌 수도 있습니다. 축마다 GPU끼리 주고받는 통신의 양과 빈도가 다릅니다. 그리고 GPU 사이를 잇는 선의 속도는 같은 서버 안의 NVLink와 서버 사이의 네트워크가 열 배 넘게 차이 납니다. 어떤 축이 쓸 만한지는 결국 이 선이 정합니다.

그래서 GPU를 더하는 것이 오히려 손해인 경우도 생깁니다. 한 장에 넉넉히 들어가는 작은 모델을 텐서 병렬로 여러 장에 쪼개면, 장마다 맡는 계산은 줄어드는데 층마다 치르는 통신은 줄지 않습니다. 계산이 짧아질수록 통신이 차지하는 몫이 커지고, 어느 선을 넘으면 한 장으로 돌릴 때보다 느려집니다. 그 선이 정확히 어디인지는 모델 크기, 배치, 연결 방식에 따라 달라서 숫자 하나로 말할 수 없습니다. 다만 그런 선이 있다는 사실은 구조에서 바로 나옵니다.

모델을 통째로 복제하는 데이터 병렬과 그 전제

가장 단순한 방식부터 보겠습니다. 데이터 병렬은 GPU마다 모델 전체를 하나씩 올리고, 서로 다른 데이터를 먹입니다. 모델은 쪼개지 않습니다.

추론에서는 정말 이것으로 끝입니다. 같은 모델을 띄운 서버 여러 개를 로드밸런서 뒤에 세우는 것과 다르지 않고, 요청 하나를 처리하는 동안 GPU끼리 주고받을 것이 없습니다. vLLM도 --data-parallel-size로 이 구성을 지원하고, 문서는 이를 모델 가중치를 복제해 독립된 요청 묶음을 처리하는 방식이라고 설명합니다. 요청이 늘어나는 만큼 처리량이 따라 느는 방식은 사실상 이것 하나입니다.

학습에서는 통신이 하나 생깁니다. GPU마다 다른 데이터로 기울기(gradient)를 계산하니, 다음 걸음을 내딛기 전에 기울기를 모아 평균을 내야 복제본들이 같은 가중치를 유지합니다. 이 집계가 all-reduce입니다. PyTorch의 DDP를 설명한 논문은 이 구조를 모델을 복제해 기울기를 따로 구하고 매 반복마다 그 기울기를 주고받는 것으로 요약하고, 설정을 잘 맞추면 GPU 256장까지 거의 선형으로 늘어난다고 보고합니다. 통신이 반복 한 번에 한 차례 몰려 있고, 계산과 겹쳐 숨길 수 있다는 점이 이 방식을 다루기 쉽게 만듭니다.

전제는 하나입니다. 모델이 GPU 한 장에 들어가야 합니다. 그리고 학습에서는 이 "들어간다"의 기준이 생각보다 훨씬 높습니다. ZeRO 논문의 계산으로는 혼합 정밀도로 Adam을 돌리면 파라미터 하나에 16바이트가 듭니다. 15억 파라미터인 GPT-2는 fp16 가중치만 3GB인데 학습 상태까지 합치면 최소 24GB가 필요하고, 논문은 이 모델을 32GB GPU 한 장에서 텐서플로나 파이토치로 학습할 수 없다고 적습니다. 추론이라면 넉넉히 들어갈 모델이 학습에서는 넘친다는 뜻입니다.

층 안의 행렬을 쪼개는 텐서 병렬, 매 층마다 치르는 통신

모델이 한 장에 안 들어가면 모델 자체를 쪼개야 합니다. 첫 번째 방법은 층 하나를 이루는 행렬을 여러 GPU에 나눠 맡기는 텐서 병렬입니다. MIG와 NVLink를 다룬 글에서 한 문단으로 짚고 넘어갔던 방식인데, 여기서는 통신이 왜 그렇게 잦은지까지 들어가 보겠습니다.

지금 쓰이는 형태는 대부분 2019년 Megatron-LM 논문에서 왔습니다. 트랜스포머의 MLP 블록은 행렬 곱 두 번으로 이루어지는데, 논문은 첫 번째 행렬을 열 방향으로, 두 번째 행렬을 행 방향으로 자릅니다. 이렇게 자르면 첫 번째 곱의 결과를 GPU끼리 맞춰 볼 필요 없이 각자 들고 두 번째 곱으로 넘어갈 수 있고, 두 번째 곱이 끝난 뒤에 한 번만 합치면 됩니다. 셀프 어텐션은 헤드가 원래 서로 독립이라 헤드 단위로 나누면 같은 모양이 됩니다.

그 결과를 논문은 이렇게 정리합니다.

This enables us to perform all GEMMs in a simple transformer layer using only two all-reduces in the forward path and two in the backward path.

"only"라는 단어가 붙어 있습니다. 저자들 입장에서는 줄이고 줄인 결과가 층 하나에 순방향 all-reduce 두 번입니다. 문제는 이것이 층마다 붙는다는 점입니다. 추론에서 토큰 하나를 생성하려면 모든 층을 순방향으로 한 번 지나야 하니, 층이 수십 개인 모델은 토큰 하나에 all-reduce를 그 두 배만큼 치릅니다. 다음 토큰에서 다시 같은 횟수를 치릅니다. 그리고 각 all-reduce가 끝나야 다음 층 계산을 시작할 수 있습니다. 통신이 계산 사이사이에 끼어 있어서 기다리는 시간이 고스란히 지연으로 쌓입니다.

이 때문에 텐서 병렬은 연결이 가장 빠른 범위, 대개 NVLink로 묶인 서버 한 대 안에 둡니다. Megatron-LM 실험 환경도 그랬습니다. 논문은 서버 안 GPU끼리는 NVSwitch로 300GB/s, 서버 사이는 InfiniBand 어댑터 8개로 100GB/s였다고 적습니다. 서버 한 대에 GPU가 16장이었고, 텐서 병렬은 8장 단위로 걸었으니 서버 밖으로 나가지 않았습니다. 그 위에 복제본 64개의 데이터 병렬을 얹어 83억 파라미터 모델을 GPU 512장으로 학습했고, 단일 GPU 기준 대비 76%의 스케일링 효율을 냈습니다. GPU 수에 맞춰 모델도 키운 실험이라 "같은 모델이 몇 배 빨라졌다"는 숫자는 아닙니다. 그래도 가장 잘 짠 경우조차 100%에 못 미친다는 점은 기억해 둘 만합니다.

대신 얻는 것이 있습니다. 행렬 곱 자체를 나눠 하니, GPU마다 계산량이 줄어 토큰 하나의 지연이 짧아질 여지가 있습니다. 세 방식 중 요청 하나의 응답 속도를 직접 줄일 수 있는 것은 텐서 병렬뿐입니다. 단, 앞에서 말한 대로 통신 비용이 그 이득을 넘지 않을 때 이야기입니다.

층을 단계로 나누는 파이프라인 병렬과 빈 시간

두 번째 방법은 층을 쪼개지 않고 층 묶음을 나누는 것입니다. 80층짜리 모델이라면 앞 40층은 GPU 0에, 뒤 40층은 GPU 1에 두는 식입니다. 이것이 파이프라인 병렬이고, 2018년 GPipe 논문이 대표적인 출발점입니다.

통신은 단계와 단계 사이에서만 일어납니다. 앞 단계가 계산한 활성값(activation)을 다음 단계로 넘기면 그만이고, 층 안에서는 아무것도 주고받지 않습니다. GPipe 논문도 이 점을 장점으로 꼽으며, 그래서 고속 연결이 없는 가속기에서도 효율적으로 늘릴 수 있다고 적습니다. 텐서 병렬과는 통신 패턴이 정반대입니다. 한쪽은 모든 GPU가 층마다 모여 합치고, 다른 쪽은 이웃 단계에 순서대로 한 번 건넵니다.

대가는 빈 시간입니다. GPU 0이 첫 번째 데이터를 처리하는 동안 GPU 1은 할 일이 없습니다. 첫 결과가 넘어온 뒤에야 GPU 1이 일을 시작하고, 그때 GPU 0이 다음 데이터를 받지 못하면 이번에는 GPU 0이 놉니다. 이 공백을 흔히 버블(bubble)이라고 부릅니다. GPipe는 미니배치를 마이크로배치로 잘게 나눠 단계마다 다른 마이크로배치를 동시에 처리하게 해서 이 공백을 줄입니다. 논문은 버블 시간을 단계 수 K와 마이크로배치 수 M으로 O((K−1)/(M+K−1))로 적고, M이 K의 4배 이상이면 실험에서 버블이 거의 무시할 만했다고 보고합니다.

추론에서 이 구조가 의미하는 바는 조심해서 말해야 합니다. 요청이 하나뿐이라면 파이프라인 병렬은 그 요청을 빠르게 만들지 못합니다. 토큰 하나는 여전히 1단계, 2단계, 3단계를 차례로 다 지나야 하고, 단계가 넘어갈 때마다 전송이 한 번씩 더해집니다. 한 장에 다 들어가는 모델이었다면 한 장으로 돌릴 때보다 느려집니다. 파이프라인 병렬이 버는 것은 처리량입니다. 동시에 들어온 요청이 여럿이면 각 단계가 서로 다른 요청을 맡아 파이프라인이 채워지고, 그때 비로소 GPU들이 동시에 일합니다. 그리고 애초에 한 장에 안 들어가는 모델을 돌릴 수 있게 해 준다는 것, 그것이 이 방식의 첫 번째 쓸모입니다.

900GB/s와 400Gb/s, 단위부터 다른 연결 속도

세 방식의 선택을 실제로 가르는 것은 GPU 사이 연결의 속도입니다. 엔비디아 H100 사양표는 SXM 모델의 연결을 NVLink 900GB/s, PCIe Gen5 128GB/s로 적습니다. PCIe 카드형인 H100 NVL은 NVLink 브리지로 600GB/s입니다. 같은 서버 안에서도 NVLink로 묶였느냐 PCIe만 거치느냐에 따라 7배 넘게 차이가 납니다.

서버 밖으로 나가면 단위부터 달라집니다. 엔비디아의 Quantum-2 InfiniBand 페이지는 ConnectX-7 어댑터가 포트당 400Gb/s라고 적습니다. 여기서 소문자 b는 비트입니다. GPU 사양표의 GB/s는 바이트이고, 네트워크 장비는 관례상 Gb/s, 즉 비트로 적습니다. 400Gb/s를 바이트로 바꾸면 8로 나눠 50GB/s입니다. 숫자만 나란히 놓으면 400과 900이라 반쯤 되는 것처럼 보이지만, 실제로는 NVLink 900GB/s의 18분의 1입니다. 이 혼동은 문서를 오가며 읽다 보면 꽤 쉽게 일어납니다.

한 가지는 확실하지 않습니다. H100 사양표는 이 숫자들이 한 방향 기준인지 양방향을 더한 값인지 표에 적지 않습니다. 그래서 위의 배수는 사양표에 적힌 숫자끼리의 비율로만 읽는 것이 맞습니다. 그래도 자릿수가 다르다는 결론은 바뀌지 않습니다.

여기까지 놓고 보면 앞의 두 방식이 어디에 놓이는지 자연스럽게 정해집니다. 층마다, 토큰마다 all-reduce를 치르는 텐서 병렬은 900GB/s 선 위에 있어야 하고, 단계 경계에서만 활성값을 건네는 파이프라인 병렬은 50GB/s 선도 견딥니다.

통신 횟수와 연결 요구로 본 세 방식의 차이

데이터 병렬 텐서 병렬 파이프라인 병렬
나누는 대상 데이터 (모델은 복제) 층 안의 행렬 층 묶음 (단계)
모델이 한 장에 들어가야 하나 예 아니요 아니요
추론 시 GPU 간 통신 없음 층마다 all-reduce 단계 경계마다 활성값 전달
학습 시 추가 통신 반복마다 기울기 all-reduce 순방향·역방향 모두 층마다 역방향에도 경계마다 기울기 전달
필요한 연결 아무것이나 NVLink급, 보통 서버 한 대 안 서버 사이 네트워크도 가능
요청 하나의 지연 한 장과 같음 줄 여지가 있음 줄지 않음
주된 약점 메모리 중복 통신 지연 버블

표로 보면 세 방식은 경쟁 관계라기보다 서로 다른 문제를 푸는 도구입니다. 데이터 병렬은 처리량 문제를, 텐서 병렬과 파이프라인 병렬은 모델이 안 들어가는 문제를 풉니다. 그래서 실제 대규모 구성은 셋을 겹쳐 씁니다. 앞에서 본 Megatron-LM의 512장 구성도 텐서 병렬과 데이터 병렬을 겹친 것이었습니다.

학습 쪽 친척, ZeRO·FSDP와 전문가 병렬

데이터 병렬의 약점인 메모리 중복을 정면으로 다룬 것이 ZeRO입니다. 앞에서 본 16바이트 중 상당 부분은 옵티마이저 상태인데, 데이터 병렬에서는 모든 GPU가 이것을 똑같이 들고 있습니다. ZeRO는 이것을 GPU 수만큼 나눠 각자 자기 몫만 들게 합니다. 논문 기준으로 옵티마이저 상태만 나누면 메모리가 4배, 기울기까지 나누면 8배 줄고, 이 두 단계는 통신량이 일반 데이터 병렬과 같습니다. 파라미터까지 나누면 GPU 수에 비례해 줄고, 64장이면 64배입니다. PyTorch에서는 같은 생각이 FSDP라는 이름으로 들어가 있고 설계 경험을 정리한 논문이 따로 있습니다. 겉모양은 데이터 병렬이지만 파라미터를 필요할 때 모아 오는 통신이 더해지므로, 데이터 병렬의 "통신이 적다"는 성질은 단계가 올라갈수록 옅어집니다.

전문가 혼합(MoE) 모델에는 전문가 병렬이라는 축이 하나 더 있습니다. 전문가 층의 전문가들을 GPU마다 나눠 두고 토큰을 맞는 전문가에게 보내는 방식입니다. vLLM 문서는 MoE 모델에서 어텐션 층은 데이터 병렬로, 전문가 층은 전문가 병렬이나 텐서 병렬로 나누는 구성을 설명합니다. 주의할 점은 이때 데이터 병렬 랭크들이 더 이상 완전히 독립이 아니라는 것입니다. 문서에 따르면 전문가 층이 매 순방향마다 모든 랭크를 동기화해야 해서, 처리할 요청이 없는 랭크도 빈 순방향을 돌려야 합니다. 앞에서 "추론의 데이터 병렬은 통신이 없다"고 했는데, MoE에서는 이 말이 성립하지 않습니다.

vLLM 옵션에 그대로 드러나는 노드 안과 노드 사이의 구분

처음의 명령으로 돌아가면 이제 숫자 두 개가 읽힙니다. vLLM 문서는 선택지를 세 가지로 나눕니다. 모델이 한 장에 들어가면 분산 추론이 아마 필요 없습니다. 한 장에는 안 들어가지만 서버 한 대에는 들어가면 텐서 병렬을 쓰고, tensor_parallel_size를 그 서버의 GPU 수로 맞춥니다. 서버 한 대에도 안 들어가면 텐서 병렬과 파이프라인 병렬을 섞어, 텐서 병렬 크기는 서버당 GPU 수로, 파이프라인 병렬 크기는 서버 수로 둡니다. 8장짜리 서버 두 대에서 8과 2가 나온 이유가 이것입니다. 텐서 병렬은 NVLink 안에 가두고, 서버 사이 네트워크는 통신이 드문 파이프라인 경계에만 걸치게 한 것입니다.

같은 문서에는 연결이 느린 경우를 다룬 문장도 있습니다.

if the GPUs on the node do not have NVLINK interconnect (e.g. L40S), leverage pipeline parallelism instead of tensor parallelism for higher throughput and lower communication overhead.

서버 한 대 안이라도 GPU들이 PCIe로만 이어져 있다면 텐서 병렬보다 파이프라인 병렬이 낫다는 뜻입니다. 기준이 "같은 서버인가"가 아니라 "연결이 얼마나 빠른가"라는 점이 여기서 분명해집니다. 문서는 GPU 수가 모델을 고르게 나누지 못할 때도 파이프라인 병렬을 권합니다. 층 단위로 자르므로 균등하지 않은 분할을 받아 주기 때문입니다.

--tensor-parallel-size 16처럼 서버 두 대에 걸쳐 텐서 병렬을 거는 구성도 문서에 있습니다. 그때 문서는 InfiniBand 같은 고속 네트워크를 권하고, NCCL 로그에 NET/Socket이 보이면 평범한 TCP 소켓을 쓰는 중이라 서버 간 텐서 병렬에 효율적이지 않다고 경고합니다. 돌아가기는 하지만 대가가 크다는 이야기로 읽힙니다.

제가 고르는 순서와 확신이 끝나는 지점

저라면 순서를 이렇게 잡겠습니다. 먼저 모델이 GPU 한 장에 들어가는지 봅니다. 들어간다면 쪼개지 않고 복제본을 늘리는 데이터 병렬로 갑니다. 요청 하나가 빨라지지는 않지만 처리량은 GPU 수만큼 정직하게 늘고, GPU끼리 통신이 없어 연결이 느려도 상관없습니다. 한 장에 안 들어가고 서버 한 대가 NVLink로 묶여 있다면 그 안에서 텐서 병렬을 겁니다. 서버 한 대로도 모자라면 서버 안은 텐서 병렬, 서버 사이는 파이프라인 병렬로 겹칩니다. vLLM 문서가 권하는 순서와 같고, 저는 이 순서가 앞에서 본 통신 구조에서 그대로 따라 나온다고 봅니다.

이 순서에서 제가 가장 자주 의심할 대목은 첫 단계입니다. 모델이 한 장에 "간신히" 들어가는 경우입니다. 추론에는 가중치 말고도 KV 캐시가 자리를 차지하고, vLLM은 시작할 때 캐시에 담을 수 있는 토큰 수와 최대 동시 처리 수를 로그로 보여 줍니다. 가중치는 들어갔는데 캐시 자리가 모자라 동시 처리가 몇 건에 그친다면, 두 장에 텐서 병렬로 나눠 캐시 자리를 넓히는 편이 처리량에서 나을 수도 있습니다. 어느 쪽이 나은지는 모델과 요청 길이에 따라 갈리고, 저도 재 보기 전에는 답을 정할 수 없는 부분입니다.

연결이 PCIe뿐인 서버도 비슷하게 애매합니다. vLLM 문서는 L40S 같은 경우 파이프라인 병렬을 권하지만, 요청 하나의 지연이 가장 중요한 서비스라면 이야기가 달라집니다. 파이프라인 병렬은 그 지연을 줄여 주지 못하고, PCIe 위의 텐서 병렬은 통신에 발목이 잡힙니다. 이 조합에서 어느 쪽이 덜 나쁜지는 모델 크기와 배치에 따라 다르고, 일반 규칙으로 말할 수 있는 범위를 넘습니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.