모델 운영

MLOPS / 58번째 글

텐서 병렬 — 한 층을 카드 여러 장이 나눠 계산하기

모델이 카드 한 장에 안 들어갈 때 가중치를 어떻게 자르는지 봅니다. 세로로 자르고 가로로 자르면 왜 통신이 한 번으로 줄어드는지, 카드를 늘려도 왜 그만큼 안 빨라지는지 정리합니다.

PALDYN Team14 MIN READ

지난 글까지는 전부 카드 한 장 안에서 벌어지는 이야기였다. 모델이 그 안에 들어간다는 전제가 깔려 있었는데, 그 전제가 깨지는 순간부터 완전히 다른 문제가 시작된다.

카드 한 장에 안 들어갈 때

가중치가 차지하는 자리는 어림하기 쉽다. 파라미터 수에 값 하나의 바이트 수를 곱하면 된다. fp16이면 파라미터당 2바이트다.

  • 80억 파라미터 → 약 16GB. 80GB 카드에 넉넉히 들어간다
  • 700억 파라미터 → 약 140GB. 80GB 카드 한 장에 안 들어간다
  • 4,000억 파라미터 → 약 800GB. 카드 열 장으로도 모자란다

여기에 KV 캐시 자리까지 남겨야 하므로 실제 여유는 더 빠듯하다. 양자화로 파라미터당 바이트를 줄이는 것이 첫 번째 답이지만, 그것으로도 부족한 크기가 있고 품질을 따로 재야 한다는 부담도 있다.

그다음 답이 여러 장에 나눠 담는 것이고, 나누는 축이 셋이다.

축 무엇을 나누나 성격
텐서 병렬 층 하나의 가중치 행렬을 조각내 여러 카드가 나눠 든다 층마다 통신, 지연이 준다
파이프라인 병렬 층들을 그룹으로 잘라 카드마다 다른 층을 맡는다 통신이 적고 스케줄링이 까다롭다
데이터 병렬 모델을 통째로 복제하고 요청을 나눠 받는다 통신 없음, 메모리 문제는 안 풀린다

셋은 배타적이지 않고 대개 함께 쓴다. 이번 글은 텐서 병렬이고, 파이프라인 병렬은 따로 다룬다. 데이터 병렬은 모델이 이미 한 장에 들어갈 때 처리량을 늘리는 방법이라 성격이 다르다.

세로로 자르고 가로로 자른다

트랜스포머 블록의 MLP 부분을 예로 보자. 하는 일은 이렇다.

Y=GELU(XW1) W2Y = \mathrm{GELU}(XW_1)\,W_2

W1W_1은 차원을 네 배쯤 키우는 행렬이고 W2W_2는 도로 줄이는 행렬이다. 이 둘이 파라미터의 3분의 2쯤을 차지하므로 여기를 나누는 것이 핵심이다.

소박하게 생각하면 두 행렬을 아무렇게나 반씩 잘라 나눠 들면 될 것 같지만, 자르는 방향에 따라 필요한 통신량이 완전히 달라진다. 텐서 병렬의 요령은 첫 행렬은 세로로, 둘째 행렬은 가로로 자르는 것이다.

W1=[W1(0)W1(1)],W2=[W2(0)W2(1)]W_1 = \begin{bmatrix} W_1^{(0)} & W_1^{(1)} \end{bmatrix}, \qquad W_2 = \begin{bmatrix} W_2^{(0)} \\ W_2^{(1)} \end{bmatrix}

그러면 전체 결과가 이렇게 나온다.

Y=GELU(XW1(0)) W2(0)+GELU(XW1(1)) W2(1)Y = \mathrm{GELU}(XW_1^{(0)})\,W_2^{(0)} + \mathrm{GELU}(XW_1^{(1)})\,W_2^{(1)}

각 항이 카드 하나가 혼자 계산할 수 있는 형태다. 마지막에 둘을 더하기만 하면 된다.

가중치를 세로로 자르고 가로로 자르면 통신이 한 번뿐이다

GELU가 중간에 끼어도 괜찮다는 점이 이 방식의 핵심이다. GELU는 원소마다 따로 계산하는 함수라, 내가 든 열에 해당하는 값들만 있으면 옆 카드 값이 없어도 정확히 계산된다. 만약 W1W_1을 가로로 잘랐다면 각 카드가 부분합만 들고 있게 되어 GELU 전에 한 번 더 합쳐야 했다. 통신이 두 배가 된다는 뜻이다.

마지막에 둘을 더해 모든 카드가 같은 결과를 갖게 하는 연산을 All-Reduce라고 부른다 — 여러 장비의 값을 모아 하나로 합치고 그 결과를 전부에게 돌려주는 집합 통신이다.

어텐션은 헤드로 나눈다

어텐션 쪽은 더 자연스럽다. 멀티헤드 어텐션은 애초에 헤드마다 독립적으로 계산하고 마지막에 이어 붙이는 구조라, 헤드를 나눠 맡기면 그대로 병렬이 된다. 32개 헤드를 카드 두 장이 16개씩 든다.

이어 붙인 뒤 곱하는 출력 행렬은 MLP의 W2W_2와 같은 자리라 가로로 자른다. 그래서 여기도 All-Reduce 한 번으로 끝난다.

여기서 실무 제약이 하나 나온다. 헤드 수가 카드 수로 나눠떨어져야 한다. 헤드가 32개면 2, 4, 8, 16, 32장까지 되고 3이나 5는 안 된다. 텐서 병렬 크기를 2의 거듭제곱으로 잡는 관행이 여기서 왔다.

요즘 모델이 쓰는 GQA처럼 KV 헤드 수가 훨씬 적은 구조에서는 조건이 더 빡빡하다. KV 헤드가 8개인데 카드가 16장이면 나눌 수가 없어서, 엔진이 KV 헤드를 복제해 들고 있는 식으로 처리한다. 그만큼 캐시 메모리 이득이 줄어든다.

통신이 얼마나 붙는가

블록 하나에 All-Reduce가 두 번이다 — 어텐션 뒤 한 번, MLP 뒤 한 번. 층이 80개면 토큰 한 걸음에 160번이다.

한 번에 오가는 크기는 이번 걸음의 토큰 수 × 은닉 차원 × 값의 바이트 수다. 동시 요청 32개에 은닉 차원 8,192, fp16이면 한 번에 512KB쯤이고, 160번이면 걸음마다 수십 MB가 카드 사이를 오간다.

이 양 자체보다 중요한 것은 각 통신이 계산을 멈춰 세운다는 점이다. All-Reduce가 끝나야 다음 층으로 갈 수 있으므로, 통신이 느리면 GPU가 그 시간 동안 논다.

연결 어림 대역폭 결과
NVLink(한 서버 안) 수백 GB/s 통신 시간이 계산에 묻힌다
PCIe(한 서버 안, NVLink 없음) 수십 GB/s 걸음마다 눈에 띄게 붙는다
이더넷(서버 사이) 그보다 훨씬 적고 지연도 크다 텐서 병렬로 쓸 자리가 아니다

그래서 텐서 병렬은 한 서버 안에서만 쓴다는 것이 사실상의 규칙이다. 서버를 넘어야 하는 크기라면 그 경계는 파이프라인 병렬로 넘고, 서버 안을 텐서 병렬로 채운다.

카드를 늘려도 그만큼 안 빨라진다

텐서 병렬은 메모리를 늘리는 수단이면서 지연을 줄이는 수단이기도 하다. 층 하나의 계산이 반으로 나뉘니 걸음이 빨라진다. 그런데 두 배로 빨라지지는 않는다.

  • 통신은 카드가 늘수록 더 많아진다. All-Reduce 참여자가 늘기 때문이다
  • 카드마다 맡는 조각이 작아지면 행렬 곱이 GPU를 다 못 채운다. 커널 실행 비용 같은 고정 비용의 비중이 커진다
  • 나눌 수 없는 부분(정규화, 활성 함수의 일부, 스케줄링)은 그대로 남는다

실무에서 자주 보는 모습이 이렇다. 2장에서는 1.7~1.9배쯤 붙고, 4장에서는 3배 언저리, 8장에서는 5배를 넘기기 어렵다. 정확한 값은 모델·연결·부하에 따라 다르므로 자기 환경에서 재 봐야 한다.

여기서 갈리는 판단이 있다.

  • 카드 한 장에 모델이 들어간다면 텐서 병렬을 쓰는 이유는 지연 하나뿐이다. 처리량만 필요하면 카드마다 독립 인스턴스를 띄우는 편(데이터 병렬)이 거의 항상 낫다. 통신이 0이기 때문이다
  • 안 들어간다면 선택의 여지가 없다. 필요한 최소 장수를 쓴다
  • 대화형 서비스라 첫 토큰이 급하다면 처리량을 조금 손해 보더라도 장수를 늘려 지연을 줄이는 것이 맞을 수 있다

실무에서 만지는 지점

vLLM 계열에서는 옵션 하나다.

vllm serve meta-llama/Llama-3.3-70B-Instruct \
  --tensor-parallel-size 4 \
  --gpu-memory-utilization 0.90 \
  --max-model-len 8192

확인할 것들이 있다.

  • 장수는 2의 거듭제곱으로. 헤드 수 나눠떨어짐 조건 때문이다
  • 가중치만 겨우 들어가는 장수는 피한다. 남은 자리가 KV 캐시 몫이라, 딱 맞게 잡으면 동시 처리량이 거의 안 나온다. 메모리 튜닝에서 이 계산을 먼저 한다
  • NVLink가 있는지 확인한다. nvidia-smi topo -m으로 카드 사이 연결을 본다. PCIe로만 붙어 있으면 장수를 늘려도 이득이 금방 사라진다
  • 한 서버를 넘지 않는다. 넘어야 하면 파이프라인 병렬과 섞는다
  • 출력이 미세하게 달라질 수 있다. 부분합을 더하는 순서가 장수에 따라 바뀌므로 부동소수점 결과가 완전히 같지는 않다. 회귀 테스트를 정확한 문자열 일치로 짜 두면 장수를 바꿀 때마다 깨진다

정리

  • 가중치 자리는 파라미터 수 × 값의 바이트 수다. 700억 파라미터를 fp16으로 두면 140GB라 80GB 카드 한 장에 안 들어간다
  • 나누는 축이 셋이다 — 텐서 병렬(층 하나를 조각냄), 파이프라인 병렬(층을 그룹으로 나눔), 데이터 병렬(모델을 복제)
  • 텐서 병렬은 첫 행렬을 세로로, 둘째 행렬을 가로로 잘라 카드마다 완결된 항을 계산하게 만든다
  • GELU가 원소별 함수라 중간에 통신이 필요 없다. 자르는 방향을 반대로 하면 통신이 두 배가 된다
  • 어텐션은 헤드를 나눠 맡긴다. 그래서 헤드 수가 카드 수로 나눠떨어져야 하고, 장수를 2의 거듭제곱으로 잡는 관행이 여기서 나왔다
  • 블록마다 All-Reduce가 두 번이다. 층이 80개면 걸음마다 160번이고, 각각이 계산을 멈춰 세운다
  • 그래서 연결이 결정적이다. NVLink면 묻히고 PCIe면 붙으며 이더넷이면 쓸 자리가 아니다. 텐서 병렬은 한 서버 안에서만 쓴다
  • 카드를 두 배 늘려도 두 배 안 빨라진다. 통신이 늘고, 조각이 작아져 GPU를 다 못 채우기 때문이다
  • 모델이 한 장에 들어가고 처리량만 필요하면 독립 인스턴스를 여럿 띄우는 편이 거의 항상 낫다
  • 가중치만 겨우 들어가는 장수는 KV 캐시 자리를 남기지 않아 동시 처리량이 안 나온다

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

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

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

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

16 MIN
모델 운영2026.09.04

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

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

21 MIN
모델 운영2026.09.03

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

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

15 MIN