본문 바로가기
Inference/Optimization

[Opt] prefill vs decode

by AteN 2024. 3. 11.

프리필과 디코드, 왜 첫 글자는 빠른데 그 뒤가 느린가

요약

  • LLM 추론은 성격이 정반대인 두 단계로 나뉩니다. prefill은 프롬프트를 한 번에 병렬 처리해 KV 캐시를 채우고 첫 토큰까지 걸리는 시간(TTFT)을 결정합니다. decode는 토큰을 하나씩 순차 생성하며 초당 토큰 수(TPS)를 결정합니다.
  • 둘을 가르는 건 산술 강도(FLOPs/byte)입니다. prefill은 행렬끼리 곱해서 연산 한계에 걸리고, decode는 토큰 하나를 만들려고 전체 가중치를 읽어야 해서 메모리 대역폭 한계에 걸립니다.
  • 이 둘을 한 그림에 올린 게 루프라인 모델입니다.
  • 실용적인 소득이 하나 있습니다. "내 PC에서 이 모델이 몇 tok/s 나와야 정상인가"를 암산으로 검산할 수 있게 됩니다.
  • 흔한 오해. GPU FLOP/s를 키워도 생성 속도는 거의 안 오릅니다. decode는 연산이 안 바쁘고 대역폭이 천장입니다.

두 단계로 나뉜다

prefill은 입력 프롬프트의 모든 토큰을 한 번에 병렬로 통과시킵니다. 각 층과 헤드의 KV 캐시를 채우고 첫 출력 토큰을 냅니다. 토큰들이 동시에 있으니 연산이 행렬 곱하기 행렬(GEMM) 형태가 되고, 칩의 연산기를 꽉 채웁니다.

decode는 첫 토큰 이후 한 번에 하나씩 만듭니다. 새 토큰의 Query만 계산해서 캐시된 K, V 전체와 어텐션하고, 새로 생긴 K와 V를 캐시에 덧붙입니다. 토큰이 하나뿐이라 연산은 행렬 곱하기 벡터(GEMV)가 됩니다. 가중치는 잔뜩 읽는데 곱셈은 적습니다.

이 둘은 하드웨어를 쓰는 방식이 정반대입니다. 한 덩어리로 뭉뚱그리면 "왜 첫 글자는 금방 나오는데 그 뒤가 느린가", "왜 GPU를 키워도 생성 속도가 안 오르나" 같은 질문에 답할 수 없습니다.

prefill decode
처리 단위 프롬프트 전체 토큰 병렬 토큰 1개씩 순차
연산 형태 행렬 x 행렬 (GEMM) 행렬 x 벡터 (GEMV)
산술 강도 높음 낮음
병목 연산 (compute-bound) 메모리 대역폭 (memory-bound)
결정하는 지표 TTFT (첫 토큰) TPOT, ITL, 그리고 TPS
더 빠르게 하려면 더 센 연산기 (FLOP/s) 더 넓은 메모리 대역폭 (byte/s)

여기서 흔한 오해가 셋 나옵니다.

첫째, "토큰 하나만 만드니 decode가 가벼워서 빠르다"고 생각하기 쉽습니다. 아닙니다. 토큰 하나를 만들려고 모델 가중치 전체와 KV 캐시 전체를 읽어야 합니다.

둘째, "GPU FLOP/s를 키우면 생성이 빨라진다"도 틀렸습니다. decode는 연산이 안 바빠서 거의 안 빨라집니다. 대역폭이 천장이기 때문입니다.

셋째, "TTFT가 좋으면 빠른 모델"도 성급합니다. TTFT와 TPS는 다른 자원이 결정하니 따로 봐야 합니다.

산술 강도와 루프라인

요리사 한 명의 주방으로 비유해봅니다. 연산기를 요리사, 메모리를 식자재 창고라고 하겠습니다. prefill은 단체 손님 100명 주문을 한꺼번에 받아서 재료를 한 번 꺼내 100인분을 동시에 조리하는 상황입니다. 요리사가 병목이죠. decode는 손님 한 명이 한 입 먹고 다음 주문을 하는 식이라, 한 입마다 창고를 다시 다 뒤져야 합니다. 이번엔 창고 통로가 병목입니다. 같은 주방인데 일감 모양에 따라 병목이 바뀝니다.

산술 강도(arithmetic intensity) 는 연산량을 메모리 이동량으로 나눈 값, 그러니까 FLOPs/byte입니다. "1바이트를 읽어서 몇 번 계산하느냐"를 뜻합니다. prefill은 가중치를 한 번 읽어 여러 토큰에 재사용하니 높고, decode는 한 번 읽어 토큰 하나에만 쓰니 낮습니다.

루프라인 모델은 가로축을 산술 강도, 세로축을 도달 성능으로 놓고 천장을 두 개 그립니다.

하나는 대역폭 경사입니다. 도달 성능은 아무리 잘해야 메모리 대역폭 곱하기 산술 강도를 넘을 수 없습니다. 산술 강도가 낮으면 이 비스듬한 천장에 막힙니다.

다른 하나는 연산 평지입니다. 도달 성능은 칩 최대 FLOP/s를 넘을 수 없습니다. 산술 강도가 충분히 높으면 이 수평 천장에 막힙니다.

두 천장이 만나는 지점을 능선점(ridge point) 이라고 부릅니다.

도달 성능
(FLOP/s)
   ▲
   │                       ┌───────────────  연산 천장 (칩 최대 FLOP/s)
   │                      ╱
   │                     ╱
   │                    ●  능선점 (ridge point)
   │                   ╱
   │                  ╱
   │                 ╱   대역폭 경사
   │                ╱    성능 = 대역폭 x 산술강도
   │               ╱
   │      ○       ╱          ○
   │    decode   ╱         prefill
   └──────────────────────────────────▶  산술 강도 (FLOPs/byte)

   왼쪽    대역폭이 막는 구간
   오른쪽  연산이 막는 구간

decode는 왼쪽 비탈 아래에, prefill은 오른쪽 평지 아래에 놓입니다. 같은 칩인데 앉는 자리가 다릅니다.

내 PC에서 몇 tok/s가 나와야 정상인가

루프라인이 추상적으로 들리지만, decode가 대역폭에 묶여 있다는 사실 하나에서 쓸모 있는 검산식이 나옵니다.

decode는 토큰 하나마다 모델 가중치를 통째로 한 번씩 읽습니다. 그러니 아무리 잘해도 이보다 빠를 수 없습니다.

최대 tok/s  ≈  메모리 대역폭(GB/s)  ÷  모델 파일 크기(GB)

주요 하드웨어의 메모리 대역폭은 대략 이렇습니다.

하드웨어 메모리 대역폭
DDR4-3200 듀얼채널 51 GB/s
DDR5-5600 듀얼채널 90 GB/s
Apple M3 Pro 150 GB/s
Apple M3 Max 400 GB/s
RTX 4090 1,008 GB/s

일반적인 데스크톱(DDR5 듀얼채널, 90 GB/s)에 Q4_K_M 양자화 모델을 올린다고 해봅시다.

모델 파일 크기 이론 천장 현실값(70%)
1B 0.8 GB 112 tok/s 약 78
3B 2.0 GB 45 tok/s 약 32
8B 4.7 GB 19 tok/s 약 13
14B 8.5 GB 11 tok/s 약 7

이 표가 실전에서 하는 일이 있습니다. 측정값이 이론 천장을 넘으면 뭔가 잘못된 겁니다.

예를 들어 CPU에서 8B 모델을 돌리는데 50 tok/s가 나왔다고 해봅시다. 그러려면 50 곱하기 4.7GB, 그러니까 235 GB/s가 필요한데 데스크톱 메모리에는 그런 대역폭이 없습니다. 물리적으로 불가능한 숫자입니다. 그렇다면 셋 중 하나입니다.

  1. 실제로는 GPU가 일하고 있다
  2. 생각보다 작은 모델을 돌리고 있다
  3. 로컬이 아니라 원격에서 실행되고 있다

반대로 8B에서 13 tok/s가 나왔다면 걱정할 게 없습니다. 그게 그 하드웨어의 정상값입니다.

벤치마크를 돌리기 전에 기대값을 알 수 있고, 이상한 숫자를 걸러낼 수 있다는 게 이 계산의 실용적 가치입니다.

지표 세 가지

지표 결정하는 단계
TTFT (Time-To-First-Token) 요청부터 첫 토큰까지 지연. 큐잉과 prefill, 네트워크 포함 prefill
TPOT / ITL 연속 토큰 사이 평균 간격. 둘은 같은 뜻 decode
TPS (Tokens Per Second) 초당 토큰 수. 사용자당 TPS는 대략 1/ITL decode

NVIDIA 정의로는 ITL = (e2e_latency − TTFT) / (출력 토큰수 − 1) 이고, 사용자당 TPS는 출력 길이를 e2e 지연으로 나눈 값이라 대략 1/ITL 입니다.

온디바이스 환경은 대역폭이 천장이라 decode가 곧 사용자 체감 속도입니다. 속도 지표로 decode TPS를 잡는 게 타당한 이유가 여기 있습니다. prefill은 첫 토큰 한 번뿐이지만 decode는 출력 길이만큼 반복되니, 답변이 길수록 TPS가 지배적이 됩니다.

배치가 decode 처리량을 올리는 이유

배치 1로 decode를 돌리면 가중치를 읽어서 토큰 하나에만 씁니다. 낭비죠. 배치가 B라면 같은 가중치를 한 번 읽어 B개 토큰을 동시에 처리하니 산술 강도가 약 B배가 되고, 루프라인에서 능선점 쪽으로 이동합니다. 대역폭 비용을 여러 요청이 나눠 지는 셈입니다.

그래서 개별 지연은 크게 변하지 않으면서 총 처리량이 오릅니다. 다만 배치를 키우면 KV 메모리와 큐잉 때문에 TTFT가 나빠질 수 있습니다.

온디바이스에서 혼자 쓰는 경우는 배치가 보통 1이라 이 이점을 못 누립니다. decode가 그대로 대역폭에 막힙니다. 서버 벤치마크 숫자를 로컬에 그대로 기대하면 안 되는 이유가 이겁니다.

어떤 기법이 어느 단계를 돕나

기법 주로 돕는 단계 어떻게
양자화 decode 가중치 바이트가 줄어 이동량이 줄고 대역폭 병목이 완화됨
KV 압축과 양자화, GQA decode 매 스텝 읽는 KV 바이트가 줄어듦
FlashAttention prefill, 긴 컨텍스트 어텐션 메모리 이동을 줄임
연속 배칭 decode 처리량 빈 슬롯에 새 요청을 채워 가동률을 올림
투기적 디코딩 decode 작은 모델이 초안을 쓰고 큰 모델이 한 번에 검증
MTP decode 한 스텝에 여러 토큰 예측해서 순차 스텝 수를 줄임

표를 보면 decode 쪽 기법들이 전부 같은 목표를 향합니다. 가중치를 한 번 읽어서 더 많은 토큰을 뽑는 것입니다. 대역폭이 천장이라는 사실에서 곧바로 따라 나오는 전략입니다.

분리 서빙과 청크 프리필

한 GPU에 prefill과 decode를 섞으면 서로 간섭합니다. 긴 prefill이 들어오면 진행 중이던 decode가 멈칫하며 ITL이 튀고, 반대로 decode가 GPU를 점유하면 TTFT가 늦어집니다.

Sarathi의 chunked prefill은 긴 prefill을 작은 청크로 쪼개고, 빈 배치 슬롯을 decode로 채워서 함께 실행합니다. GPU 가동률을 올리면서 긴 prefill이 decode를 멈추는 문제를 완화합니다.

Splitwise와 DistServe의 분리 서빙은 prefill 전용 머신과 decode 전용 머신을 물리적으로 나누고 KV 캐시만 둘 사이로 전송합니다. Splitwise는 같은 전력에서 최대 2.35배 처리량을, DistServe는 쿼리당 비용 최대 7.4배 절감을 보고했습니다. 둘 다 논문 주장이고 데이터센터 멀티 GPU 조건입니다.

온디바이스에서는 분리 서빙을 쓸 수 없습니다. 하지만 "prefill과 decode는 자원 프로파일이 다르니 따로 다뤄야 한다"는 통찰 자체는 그대로 유효합니다.

메모리를 줄이면 느려질까

"메모리를 줄이면 느려지고 늘리면 빨라진다"는 단순한 트레이드오프가 아닙니다. decode 속도는 대략 대역폭을 토큰당 옮기는 바이트로 나눈 값이라, 메모리가 줄었느냐가 방향을 정합니다.

메모리 절감 방법 메모리 속도 이유
가중치 양자화 (bf16에서 int4) 감소 빨라짐 토큰당 바이트가 줄어듦
구조적 pruning 감소 빨라짐 계산과 바이트가 동시에 줄어듦
비구조적 sparsity 감소 대개 그대로 GPU가 sparse matmul을 가속하지 못함
distillation 감소 빨라짐 근본적으로 모델이 작음
MoE, active 축소 총량은 비슷 빨라짐 active 바이트만 읽음
GQA, MQA, MLA KV 감소 빨라짐 KV 읽기가 줄어듦
KV 양자화 KV 감소 빨라짐 어텐션이 읽을 KV가 줄어듦
offloading (CPU나 디스크로) 감소 느려짐 느린 계층 때문에 유효 대역폭이 폭락
KV 캐시 끄고 재계산 감소 매우 느려짐 매 스텝 어텐션을 다시 계산

원칙은 이렇습니다. 메모리를 토큰당 옮길 바이트를 줄여서 아끼면 빨라지고, 일을 느린 자원이나 재계산으로 떠넘겨서 아끼면 느려집니다. 바이트가 실제로 안 줄면 그대로고요.

그래서 여유 RAM을 늘리는 것 자체로는 안 빨라집니다. 대역폭이 천장이니까요. 진짜 트레이드오프는 "메모리 대 속도"가 아니라 "메모리 대 품질"(양자화)입니다.

마치며

핵심 세 가지로 정리합니다.

  1. prefill은 병렬이라 연산 한계에 걸리고 TTFT를 결정합니다. decode는 순차라 메모리 대역폭 한계에 걸리고 TPS를 결정합니다. 성격이 정반대입니다.
  2. 루프라인에서 decode는 대역폭 천장에 막히므로, FLOP/s를 키워도 생성 속도는 안 빨라집니다.
  3. 양자화, KV 절약, 투기적 디코딩, 분리 서빙은 전부 "가중치 1회 읽기로 더 많은 토큰"을 노립니다.

용어 정리

용어 한 줄 뜻
prefill / decode 프롬프트 병렬 처리 / 토큰 1개씩 순차 생성
산술 강도 FLOPs/byte. 낮으면 대역폭 병목, 높으면 연산 병목
루프라인 모델 산술강도와 도달성능 축에 두 천장을 그린 성능 모델
능선점 (ridge point) 대역폭 천장과 연산 천장이 만나는 지점
TTFT / TPOT, ITL / TPS 첫 토큰 지연 / 토큰 간 간격 / 초당 토큰
GEMM / GEMV 행렬 x 행렬 / 행렬 x 벡터
chunked prefill 긴 prefill을 청크로 쪼개 decode와 함께 실행
disaggregation prefill과 decode를 다른 머신으로 분리
Q4_K_M GGUF 4비트 양자화 방식 중 하나

참고자료

'Inference > Optimization' 카테고리의 다른 글

FlashAttention  (0) 2024.08.27
KV Cache 란  (0) 2024.07.02

댓글