ai technology
추론 시의 GPU 효율화/병렬화와 학습 시의 GPU 효율화/병렬화 방법론들의 차이점 정리
Junyoung Park · 2025-12-26 · 9 min
병렬 GPU를 사용하는 이유
일반적인 경우에는(Gradient를 쓰는 대부분의 딥러닝 학습 코드에서는), 추론보다 학습에 더 큰 리소스가 들게 된다. 또한 데이터셋을 학습하기 위한 구조에 대해, 단순히 모델 크기만의 문제가 아니라 데이터셋의 규모가 커질수록 학습 배치 수에 따른 학습 효율성이 떨어진다. 추론 과정도 순탄치는 않다. 최근 연구를 보게 되면 LLM/VLM 사이즈가 단일 서버 GPU로도 서빙이 어려운 경우가 많아, 여러 GPU 리소스에 동시에 띄워 사용하게 된다. 흔히 huggingface model에서 device_map=”auto” 등으로 많이 적용하거나, vLLM 서빙 시에 백엔드 API를 띄우기 위한 argument로 tensor_parallel_size=4 를 적용하게 된다.
추론 단계에서 GPU를 묶는 방식과 학습 시의 병렬화(Distributed Data Parallel)는 겉보기에는 비슷하지만 목적과 내부 알고리즘이 다르다. 이 차이를 기억하려고 정리글로 남긴다.
- 학습 병렬화: 같은 모델을 여러 GPU가 함께 학습(Gradient를 업데이트)하기 위한 방식으로, 사용 목적의 핵심은 다중 GPU에서 계산된 gradient를 ‘동기화’하는 것이다.
- 추론 병렬화: 추론 모델이 너무 큰 경우 단일 GPU에 올리지 못하거나 추론 시 용량이 넘어가는(OOM) 문제를 피하기 위해, 모델 파라미터를 쪼갬으로서 여러 GPU의 메모리를 마치 하나의 “통합”메모리처럼 사용하고자 함. 핵심은 메모리(모델 파라미터 및 추론 시 activation)를 ‘분산’하는 것이다.
병렬화 대상의 차이
학습 병렬화와 DDP
학습 병렬화에서는 모델 전체가 각 GPU에 똑같이 복제된다. 추론 병렬화와 달리 모델 파라미터가 각 GPU에 동일하게 올라가있는 상태가 되며, 각 프로세스는 할당된 미니 배치를 처리한다.
- 병렬화한 각 GPU에 모델 파라미터 전체가 그대로 복제됨.
- 각 GPU에는 서로 다른(겹치지 않게 샘플링된) 미니배치(mini-batch)를 forward/backward하게끔.
- Backward 도중, 혹은 끝에 gradient를
all-reduce로 평균내어 모든 GPU가 동일한 파라미터로 업데이트됨.
여기서, 마지막으로 언급한 “Backward” 도중, 혹은 끝에 gradient를 all-reduce로 업데이트한다는 관점이 잘 이해가 가지 않아 조금 더 면밀하게 살펴보았다. Loss 자체를 평균 내서 한다는 것인지에 대한 궁금증이다.
Forward → Loss 계산은 GPU 독립적으로 수행.
각 Rank(분리된 여러 프로세스에게 할당된 GPU)는 서로 겹치지 않게 샘플링된 미니배치를 받는다.
- GPU0 : batch0 → forward → loss0
- GPU1 : batch1 → forward → loss1
- GPU2 : batch2 → forward → loss2
- GPU3 : batch3 → forward → loss3 여기서, loss는 각 GPU마다 하나씩 따로 생긴다.
‘Loss’를 평균 낸다는 관점(X) → ‘Gradient’를 쌓아간다는 관점.
일반적인 DDP 학습 패턴은 다음과 같이 수행된다.
loss = criterion(model(x), y) # 각 GPU에서 local batch로 계산
loss.backward() # 각 GPU에서 backward 실행
optimizer.step()
여기서 Loss를 굳이 모아서 평균낼 필요가 없다. 왜냐하면 결국 DDP를 사용하는 주 목적은 Loss 값 자체가 아니라, Loss에서 나온 Gradient이기 때문이다.
Backward는 단번에 끝난다는 느낌이 아니라, 모델의 입출력에 대한 ‘연산’ 그래프를 역방향으로 거슬러 올라간다는 개념이다. 따라서 레이어가 예를 들어 4개인 모델이라고 한다면,
- Loss → Layer4의 Gradient 계산
- Layer4의 Gradient → Layer3의 Gradient 계산
- Layer3의 Gradient → Layer2의 Gradient 계산
- Layer2의 Gradient → Layer1의 Gradient 계산
고로 DDP는 Loss가 아닌 각 레이어(파라미터 단위)에서 gradient가 계산되어 파라미터 업데이트에 적용되기 전에 각 프로세스별 결과를 통신(GPU0 ~ GPU3)하여 적용하는 것이다.
즉, 특정 파라미터에 대한 grad가 모든 프로세스에서 계산된 상태면 그 grad를 모든 GPU에서의 결과와 all-reduce(sum), 이후 world_size로 나누어(평균) 해당 파라미터를 업데이트하게 된다. 따라서 DDP의 all-reduce는 결국 파라미터 단위(backward)의 관점에서 진행된다.
각 GPU는 각자 본인이 담당한 배치로 계산한 Local Gradient를 업데이트를 위해 계산해놓는다:
.
.
.
.
DDP all-reduce는 모든 GPU가 계산한 gradient의 평균인: 를 업데이트 대상으로 공유하게 되고, 모든 GPU 상의 모델 파라미터가 매번 같은 값을 가질 수 있게 된다.
추론 병렬화
여기까지 왔을때, 추론 병렬화는 당연히 다음과 같이 진행된다고 생각했다:
모델이 있고, 이 모델의 파라미터가 너무 큰 탓에 개의 GPU에 병렬로 올렸다. 이 모델에 단일 입력을 넣고 출력 결과를 얻고자 했을때, 추론 병렬화는 다음과 같이 진행될 것이다.
- 단일 입력을 개의 GPU에 복사한다.
- 각각의 GPU에서 계산된 activation을 merge(통신)한 뒤, 합쳐진 activation을 다시 다음 파라미터로 넘긴다.
- 이를 반복한다 (activation 흩뿌리기 → activation 합치기 …).
여기서 뇌내망상을 하면서 좀 이상했던 것은, 이렇게 해버리면 통신 과정 때문에 속도가 너무 느리지 않을까..? 싶었는데 아니나 다를까 이런 식으로는 하지 않는단다. 그리고 실제로 추론 과정에서 병렬화를 적용하는 방식에는 다양한 접근이 있는데, 각자 동작이 다르다는 것도 알게 되었다.
(1) HF device_map=”auto”: 계주 경기 스타일
모델을 보통 “레이어 단위”로 GPU에 나눠서 올리는 방식이다. 동일 레이어의 파라미터 자체를 분리하지 않고 레이어 단위로 나눈다는 것. 입력은 처음 GPU(e.g. GPU0)에 들어가고, GPU0이 본인이 가지고 있는 레이어들을 통과시켜 activation을 획득 → activation을 GPU1으로 넘겨서 계산 → 반복하는 식이다. 결국 마지막 GPU가 출력을 생성하게 되며, 일종의 ‘계주 경기’처럼 GPU들이 토스하게끔 했다.
(2) Tensor Parallel (Megatron / vLLM Tensor Parallel): 행렬곱 분업 스타일
여기가 사실 내가 처음에 생각한 구조와 유사한 케이스인데, 그래도 모든 GPU에 매번 merge하고 뿌리는 구조는 아니다. Tensor Parallel 방식은 모델을 레이어 단위로 GPU에 나누는(device_map) 방식과는 다르게, 한 레이어(Linear/GEMM)를 여러 GPU가 분업해서 같이 계산하는 방식이다.
큰 모델에서 주로 무거운 계산이라고 하면 다음과 같다: ().
Hidden이 너무 커지면 사이즈가 커지게 되고, 한 GPU에 가 못들어가거나, 계산이 너무 느려지는 케이스가 발생한다. TP는 이렇게 연산이 무거운 녀석들을 여러 GPU에 쪼개서 동시에 처리하게끔 한다. 이때, 쪼갤 때 를 쪼개는 방식과 를 쪼개는 방식이 있다.
출력 채널을 쪼개는 방식
가중치 를 열(column) 기준으로 쪼개면: .
그렇다면 각 GPU는 다음만 계산하면 되고, 이어붙이면 된다:
,
이 경우에는 입력 가 “각 GPU”에 복사된다는 조건이 필요하며, 각 GPU의 결과물은 출력을 결국 all-gather로 모아 붙이는 형태가 된다.
비유: 모두가 같은 재료()를 받고, 각자 다른 맛(출력 조각)을 만들어서 합친다.
입력 채널을 쪼개는 방식
이번에는 를 행 기준으로 쪼개볼 수도 있다:
이에 맞게 입력 도 함께 쪼갠다: .
이때는 각 GPU가 쪼갠 결과들을 다음과 같이 부분 연산의 합으로 계산하게 된다:
. .
그래서 여기서는 마지막에 all-reduce(sum)하는 과정이 된다.
비유: 각자 재료의 일부( 조각)로 같은 요리()를 만들고, 마지막에 국물(부분합)을 다 합쳐 완성한다.
(3) Data-parallel inference: 합치기 자체가 없음.
추론 병렬 처리도 DDP가 가능하다. 다만 파라미터를 나누거나 연산을 분담하는 형태가 아니라 결국 GPU마다 모델을 통째로 복사, 요청을 GPU들에 나눠서 처리한다는 관점이다. 파라미터 효율화는 아니지만 배치 단위의 작업에는 확실히 가장 유리한 방법이라고 볼 수 있다.
의문점 (학습 시 파라미터의 효율화)
이쯤에서 하나의 의문이 들기 시작한다. 그렇다면 DDP는 근본적으로는 “큰 파라미터 모델”의 리소스 효율화가 불가능하다는 사실이다. 결국 그렇다면 단일 서버 GPU에 모델이 올라가지 않는 경우에는 어떤 방식으로 학습을 진행하게 될까?
순수 체급으로 가능한 경우
1~2B 모델과 같이 사이즈가 상대적으로 작거나 학습 설정에 메모리를 적게 쓰는 경우(일부 파라미터만 학습 등)라면 DDP만 써도 가능하다.
대형 모델을 학습할 경우
그러나 최소 수십억에서 최대 수백억, 수천억 파라미터에 달하는 모델을 학습하게 될 경우에는 순수 DDP만 사용하면 메모리가 터지기 때문에, 실제로는 다음과 같은 방식을 섞는다:
ZeRO / FSDP (파라미터, 그래디언트, 옵티마 상태를 sharding)
DDP는 각 GPU가 모델의 파라미터, gradient 그리고 optimizer state까지 모두 포함하기 때문에 복제하는 비용이 크다. ZeRO/FSDP는 이걸 GPU들에 분리해서 저장(shard)하는 방식으로 메모리를 최소화한다.
ZeRO는 다음과 같은 3가지 단계로 나뉜다:
- ZeRO-1: Optimizer state만 분리해서 저장
- ZeRO-2: Optimizer state + Gradient 분리해서 저장
- ZeRO-3: Optimizer state + Gradient + Parameter까지 분리해서 저장
FSDP(Fully-Sharded Data Parallel)는 Pytorch 쪽에서 ZeRO-3와 유사한 형태의 기능을 제공한다. 필요할 때 연산 결과를 모으고 계산한 뒤 다시 쪼개는 과정을 일괄적으로 자동화시켰다.
ZeRO/FSDP의 Forward/Backward에서 실제로 무슨 일이 발생하는가?
- forward 직전에는 해당 레이어 파라미터를 각 GPU가 shard로 들고 있으니, 계산하려면 all-gather로 full weight를 잠깐 모음
- forward 계산 후: 이미 계산된 레이어 파라미터는 더 이상 필요 없으니 다시 shard 형태로 돌려놓음
- backward에서도 유사하게 필요한 순간 gather/reshard
- optimizer step 시에도 optimizer state가 샤딩되어 업데이트됨
즉, forward/backward를 레이어별 파라미터의 gather/sharding의 효율적인 전환으로 돌리는 방식이다.
Tensor Parallel (TP) 및 Pipeline Parallel (PP)
앞서 이미 말했던 내용과 같이 TP는 모델 연산들 중 일부 연산량이 큰 Linear 연산들을 분업해서 수행하는 구조라고 했었고, 이 또한 학습에서도 활용 가능하다. 그와는 유사한 단어이지만 Pipeline Parallel은 레이어의 덩어리를 GPU에 분리해서 담는 과정이다 .
여기까지만 보면 huggingface model의 옵션 중 device_map=”auto”와 뭐가 다르냐고 생각할 수 있지만, 학습 시의 프레임워크는 다음과 같은 차별점을 가진다. “Pipeline” parallel이라는 이름에 걸맞게, 이 방식은 학습 파이프라인을 최적화한다. 만약 한 번에 하나의 배치만 흘리게 되면 GPU들 중 일부 GPU는 놀게 된다. 이 방식은 앞에서 묘사했던 것처럼 릴레이 경주처럼 앞에서 계산한 결과를 뒤에서 받아서 순차 처리하는 방식이기 때문에, 이런 직렬적 구성은 추론에서는 괜찮을 수 있어도 병렬적 구성에서는 괜찮지가 않다.
그래서 PP를 통해서 batch 또한 여러 micro-batch로 쪼개서 파이프라인을 비지 않게 만든다:
- microbatch1이 GPU0에서 layer처리 중일 때
- microbatch2는 GPU0에서 시작
- microbatch1은 GPU1으로 넘어가서 layer… 처리
이런 식으로 릴레이 주자들이 바통을 넘기면 다시 그 다음 작업을 할당하는 방식으로 주자들이 쉬는 시간을 최소화시킨다.
GPU가 여러 장이 있는데요, 그러면 DDP + 메모리 효율화를 하려면 어떻게 할당하나요?
보통은 파라미터, 그래디언트 등을 저장하는 전용 GPU를 따로 떼어두지 않고, 모든 GPU가 학습에 참여하면서 동시에 샤딩/통신을 수행하는 구조가 일반적이다.
즉, ZeRO/FSDP에서 옵티마 상태/파라미터를 나눠 들고 있다는 건 특정 GPU만 저장 담당이 아니라 모든 GPU가 각자 자기 shard를 저장하는 것이다. 다만 “GPU가 역할별로 나뉘는 것처럼 보이는” 구조는 있긴 하다.
(A) DP 그룹 / TP 그룹 / PP stage처럼 “그룹/스테이지”로 나뉨
예를 들어 128 GPU를 이렇게 쓸 수 있다:
TP=8(한 모델 인스턴스가 8 GPU로 구성)PP=2(레이어를 2 스테이지로)DP=8(그 모델 인스턴스를 8개 복제)
해당 셋팅에서는 총 개의 GPU가 있다고 가정해보자.
여기서 GPU들은 역할이 있음:
- 어떤 GPU는 pipeline stage 0, 어떤 GPU는 stage 1, …etc
- TP에서는 각자 weight slice가 다름 (저장하고 있는 부분이 다르다)
- DP에서는 동일한 (
TP, PP) 묶음이 여러 개 반복됨
하지만 이건 저장 전용이 아니라 모델 계산 자체를 위해 구조적으로 나뉘는 구조에 해당된다.
(B) 오프로딩(offload)은 예외적으로 “저장”을 CPU/NVMe로 뺌
- ZeRO-Offload 같은 방식은 optimizer state를 CPU 메모리에 두기도 한다.
- 메모리를 분리하기는 했지만 결국 GPU별로 역할이 분리되는 느낌이라기보다는 CPU로 옵티마 로드를 빼놓은 것이라 볼 수 있다.
라스트 요약 (학습 시 효율화를 위한 방법론 정리)
- DDP: 모델 복제, 데이터 분할, grad all-reduce
- ZeRO/FSDP: 파라미터/grad/옵티마 상태를 샤딩 → 복제 깨기
- TP: 레이어 연산(Linear/Attention)을 분할 → 레이어 내부 통신
- PP: 레이어 블록을 스테이지로 분할 → activation이 stage 간 이동
- 대규모 학습은 보통 (TP/PP/FSDP 중 하나 이상) + DP 조합
- 모든 GPU가 계산+샤딩에 참여하는 구조가 기본
결론
겉으로는 ‘둘 다 여러 GPU를 묶어서 돌린다’지만, 학습 병렬화(DDP)와 추론 병렬화(device_map/TP/DP inference)는 분산하는 대상이 완전히 달라서 통신 패턴과 병목이 정반대로 나타난다.
1) 학습(DDP)의 한 줄 정의
DDP는 “loss를 평균내는 것”이 핵심이 아니라 업데이트에 쓰이는 gradient를 평균으로 합의하는 구조다.
모델을 GPU마다 복제하고, 각 GPU가 서로 다른 미니배치를 처리한 뒤 gradient를 all-reduce(평균)로 동기화해서 모든 GPU의 파라미터가 항상 동일하게 업데이트되도록 만든다.
2) 추론 병렬화의 한 줄 정의
추론은 목적에 따라 방식이 갈린다. 핵심은 메모리/연산을 분산해서 OOM을 피하거나 속도를 올리는 것.
- Layer/PP(sharding,
device_map="auto"): 모델을 레이어 단위로 GPU에 나눠 올려 적재 문제를 해결(activation은 다음 GPU로 전달되는 계주 형태). - Tensor Parallel(tensor_parallel_size): 큰 Linear/Attention을 GPU들이 동시에 분업해 계산(필요 지점에서 all-reduce/all-gather 통신).
- Data-parallel inference(복제 서빙): 모델을 GPU마다 통째로 올리고 요청을 분산 처리(통신 거의 없음, throughput에 강함).
3) 선택 기준 요약
- 모델이 1장 GPU에 안 들어간다 → PP(device_map) 또는 TP로 적재 문제부터 해결
- 단일 요청 지연시간/토큰 생성 속도가 중요 → 보통 TP가 유리한 경우가 많음
- 처리량(동시 요청/배치)이 중요 + 모델이 1장에 들어간다 → DP inference(복제)가 가장 단순하고 강력