AEGLOSLAB ChatGPT 같은 AI를 만들고 서비스하기

제5부 추론·서빙(Inference/Serving)

5.1 추론 기초 — prefill/decode·KV 캐시·지연/처리량

추론에 관해 이 장이 남기려는 문장은 하나다. 생성의 시간은 연산이 아니라 메모리에서 온다. 학습을 잘 운용해 본 사람일수록 이 문장을 잘못 이해한다. 학습에서는 FLOPs가 왕이고, 배치를 키우고 병렬성을 극대화하는 직관이 통한다. 그 직관을 그대로 추론에 가져오면 대개 틀린 처방을 내린다. 자기회귀 생성의 병목은 GPU가 얼마나 빨리 계산하느냐가 아니라 메모리에서 얼마나 빨리 읽어 오느냐이기 때문이다. 이 장은 왜 그런지를 산식으로 세우고, 실제 하드웨어 숫자를 넣어 디코드 한 스텝이 몇 밀리초인지까지 계산한다. 여기서 세우는 모형 위에 제5부의 나머지가 전부 추가된다 — 양자화(5.2)는 이 장이 말하는 메모리 장벽을 낮추고, 연속 배칭과 PagedAttention(5.3)은 이 장이 드러내는 처리량·KV 문제의 답이며, 비용(7.5)은 이 장의 지표를 돈으로 환산한 것이다.

이 장은 1.4의 셀프어텐션, 특히 인과 마스킹(그것이 곧 KV 캐시를 정당화한다)과 1.6의 KV 헤드 축소 변형을 전제한다. 나오는 하드웨어 수치 — GPU FLOP/s, 메모리 대역폭, VRAM — 와 모델 하이퍼파라미터는 개략 규모를 보여 주기 위한 개략 예시다. 무게는 특정 숫자가 아니라 산식이 가리키는 방향에 있다.

5.1.1 자기회귀 추론은 왜 직렬인가

트랜스포머의 순전파는 그 자체로 한 번의 병렬 행렬곱 묶음이다(1.4). 그런데 언어모델이 텍스트를 생성할 때 이 순전파가 특이하게 쓰인다. 모델은 한 번에 다음 토큰 하나의 확률분포만 낸다. 그래서 문장을 만들려면 지금까지의 토큰으로 순전파해 토큰 하나를 뽑고, 그 토큰을 시퀀스 끝에 붙여 다시 순전파해 또 하나를 뽑고, 종료 토큰이나 길이 한계까지 이것을 반복한다. 출력 토큰 $N$ 개를 만들려면 순전파를 $N$ 번 실행해야 하고, 각 순전파는 직전 순전파가 만든 토큰에 의존한다.

이 의존이 핵심이다. $t$ 번째 토큰이 있어야 $t{+}1$ 번째를 계산할 수 있으니 생성은 원리상 직렬이다. 1.4에서 RNN이 학습 때 겪던 순차 병목이, 트랜스포머의 추론에서 되살아나는 셈이다. 트랜스포머가 이긴 결정적 이유였던 병렬성이 정작 생성 단계에서는 근본적으로 제약된다. 이것은 구현을 잘한다고 없앨 수 있는 문제가 아니다 — 다음 토큰이 무엇인지 모르는 채로 그다음 토큰을 계산할 방법은 없다. 나중에 볼 투기적 디코딩(5.3)이 이 직렬성을 부분적으로 우회하지만, 그것조차 "추측하고 검증"이라는 형태로 직렬성을 인정하며 실행된다.

같은 모델, 같은 순전파인데 학습과 추론의 병목이 갈리는 이유는 한 번의 순전파에 몇 개의 토큰을 태우느냐에 있다. 학습에서는 정답 시퀀스 전체가 이미 있으므로 길이 $n$ 시퀀스의 모든 위치를 한 번의 순전파에 병렬로 넣는다. 인과 마스크가 각 위치의 컨닝을 막아 주니 $n$ 개의 다음-토큰 예측을 한 번에 학습한다. 이때 GPU에는 크고 통통한 행렬곱 — 수천 토큰 곱하기 큰 차원 — 이 흐른다. 텐서 코어가 바쁘게 실행되고, 병목은 연산이다. 반면 디코드 중에는 아직 만들지 않은 토큰이 없으니 매 스텝 새 토큰 단 하나만 순전파에 투입한다. GPU에는 가늘고 긴 행렬-벡터곱이 흐른다. 계산량은 적은데 모델 가중치 전체를 메모리에서 읽어야 한다. 병목은 연산이 아니라 메모리 대역폭이고, 텐서 코어는 대부분 유휴 상태다.

학습 — 시퀀스 전체를 한 번에 (연산 바운드) 추론(디코드) — 토큰 하나씩 직렬 (대역폭 바운드) tok1 tok2 tok3 tok4 tok5 한 번의 순전파로 n개 위치 병렬 처리 (인과 마스크) 순전파 1 → tok 순전파 2 → tok 순전파 3 → tok 순전파 4 → tok

이 대비가 이 장 전체의 저류다. 왜 디코드가 대역폭 바운드인지는 5.1.8에서 산술강도로 정밀하게 따지고, 5.1.6에서는 아예 밀리초 단위로 계산한다. 그 전에 추론이 성격이 완전히 다른 두 국면으로 나뉜다는 사실부터 봐야 한다.

5.1.2 prefill과 decode — 한 요청의 두 국면

프롬프트를 받아 답을 생성하는 한 번의 요청은 성격이 정반대인 두 국면으로 나뉜다. 이 둘을 구분하지 못하면 서빙의 어떤 지표도 이해할 수 없다. 프리필은 입력 프롬프트 전체를 처리해 첫 출력 토큰 하나를 만드는 국면이다. 프롬프트가 $L_p$ 토큰이면 이 $L_p$ 개를 한 번의 순전파에 병렬로 넣는다 — 학습과 같은 방식이다. 이 과정에서 프롬프트 각 토큰의 키·값이 계산되어 KV 캐시를 채운다. 디코드는 첫 토큰 이후 토큰을 하나씩 생성하는 국면이다. 매 스텝 직전에 만든 토큰 하나만 순전파에 넣고, 그 토큰의 질의가 캐시된 과거 키·값 전부와 어텐션한다.

프롬프트 L_p 토큰 프리필 L_p개 병렬 순전파 1회 (연산 바운드) KV 캐시 채움 + 첫 토큰 디코드 1 토큰 1개 디코드 2 토큰 1개 디코드 … (대역폭 바운드, N−1회 반복) 완성된 응답

두 국면의 성격이 정반대다. 프리필은 많은 토큰을 한 번에 처리하니 크고 통통한 행렬곱이 흘러 연산 바운드가 되고 GPU를 잘 쓴다. 디코드는 한 토큰을 여러 번 처리하니 가늘고 긴 행렬-벡터곱이 흘러 대역폭 바운드가 되고 GPU를 유휴 상태로 둔다. 프리필은 요청당 한 번, 디코드는 출력 길이만큼 반복한다. 프리필은 프롬프트가 길수록 문제가 되고(어텐션이 $O(L_p^2)$), 디코드는 출력이 길수록·배치가 작을수록 문제가 된다. 사용자가 체감하는 지연도 두 국면에서 다르게 온다 — 프리필은 첫 토큰까지의 시간(TTFT)을, 디코드는 토큰 사이의 간격(TPOT)을 지배한다.

디코드에서 GPU가 유휴 상태인 이유를 조금 더 구체적으로 그려 보자. 디코드 한 스텝에서 새 토큰 하나, 즉 벡터 하나를 모델에 통과시키려면 모델의 모든 가중치 행렬과 곱해야 한다. 그런데 배치가 1이면 곱해지는 입력은 벡터 하나뿐이다. GPU는 그 큰 가중치 행렬을 HBM에서 전부 읽어 와서, 그걸로 고작 벡터 하나를 변환하고 버린다. 읽어 온 바이트에 비해 실제 계산량은 형편없이 적다. 연산 유닛은 데이터가 도착하기를 기다리며 대부분 유휴 상태다.

GPU 활용도 시간 → 프리필 Lp 토큰 병렬 TTFT ≈ 이 구간 디코드 — 토큰 1개씩 (GPU 대부분 유휴, 대역폭 대기) 각 간격 = TPOT/ITL 높은 활용도(연산 바운드) 낮은 활용도
한 요청의 시간선 — 프리필(GPU 포화) 하나 + 디코드(GPU 여유) 여러 조각

이 그림이 이 장의 그림이다. 프리필은 짧고 GPU를 꽉 채운 하나, 디코드는 GPU를 헐겁게 쓰는 조각이 길게 이어진다. 전형적인 챗 응답에서 총 시간의 대부분은 이 디코드 꼬리에서 나온다. 그래서 서빙 최적화의 상당 부분이 결국 유휴 상태인 디코드 GPU를 어떻게 채우느냐의 문제로 귀결된다.

5.1.3 KV 캐시는 무엇을 아끼나

디코드가 매 스텝 하는 어텐션을 정확히 보자. 스텝 $t$ 에서 새로 만든 토큰의 질의 $q_t$ 는 자신을 포함한 과거 모든 토큰의 키·값과 어텐션한다.

$$o_t = \sum_{j=1}^{t} \text{softmax}_j\!\left(\frac{q_t k_j^\top}{\sqrt{d_k}}\right) v_j$$

여기서 $k_1,\dots,k_{t-1}$ 과 $v_1,\dots,v_{t-1}$ 은 과거 토큰들의 키·값이다. 결정적인 관찰이 하나 있다. 인과 마스킹 덕분에 과거 토큰의 키·값은 미래에 절대 바뀌지 않는다. 토큰 3의 키·값은 토큰 4, 5, 6을 생성할 때도 똑같다. 그렇다면 매 스텝 그것을 다시 계산하는 것은 순수한 낭비다. 캐시가 없으면 스텝 $t$ 마다 토큰 $1$ 부터 $t$ 까지 전부의 키·값을 재계산해야 하고, 시퀀스 전체를 만드는 데 어텐션 재계산이 $O(N^2)$ 규모로 쌓인다. 같은 값을 수없이 다시 만드는 셈이다. KV 캐시는 이 낭비를 없앤다 — 프리필에서 프롬프트 전체의 키·값을 계산해 저장해 두고, 디코드 스텝마다 새 토큰 하나의 키·값만 계산해 캐시 끝에 덧붙인다. 과거 키·값은 캐시에서 읽기만 한다.

프리필 후 디코드 스텝 1 디코드 스텝 2 K,V: [1,2,3] (프롬프트 3토큰) 새 토큰 4의 k₄,v₄만 계산 → 캐시에 append K,V: [1,2,3,4] 새 토큰 5의 k₅,v₅만 계산 → append K,V: [1,2,3,4, 5]

세 텐서 중 질의는 캐시하지 않는다는 점이 이름의 절반을 설명한다. 스텝 $t$ 에서 필요한 질의는 현재 토큰의 $q_t$ 하나뿐이다. 과거 토큰들의 질의는 그들 자신의 출력을 이미 낸 뒤라 다시 쓰이지 않는다 — 인과성상 과거 토큰이 미래 토큰을 질의할 일은 없다. 반면 키·값은 미래의 모든 질의가 계속 참조하므로 캐시한다. 캐시 대상은 "미래가 다시 볼 것"인 키·값이고, 질의는 그 스텝에서 소모되고 버려진다.

여기서 분명히 해 둘 것은, KV 캐시가 무비용 이득이 아니라 맞바꿈이라는 사실이다. 재계산이라는 연산을 아낀 대신 그 키·값을 담을 메모리를 상시 점유하고, 디코드마다 그 전체를 읽는 대역폭을 쓴다. 다음 세 절에서 보겠지만 바로 이 맞바꿈이 서빙의 가장 큰 제약으로 돌아온다.

5.1.4 KV 캐시는 얼마나 큰가

KV 캐시가 차지하는 메모리는 정확한 산식으로 나온다. 토큰 하나부터 쌓아 보자. 한 토큰이 한 층에서 남기는 것은 키 벡터 하나와 값 벡터 하나다. 정밀도가 원소당 $b$ 바이트라면, 한 토큰이 전체 층에 걸쳐 남기는 KV 바이트는

$$\text{토큰당 KV} = \underbrace{2}_{K,V} \times\, n_{\text{layers}} \times n_{\text{kv-heads}} \times d_{\text{head}} \times b$$

여기서 $n_{\text{kv-heads}}$ 가 KV 헤드 수인데, MHA면 질의 헤드 수와 같고 GQA·MQA·MLA면 이보다 작다(1.6). 바로 이 인자를 줄이는 것이 KV 축소 변형의 존재 이유였다. 시퀀스에 토큰이 $L$ 개 쌓이고 동시에 처리하는 시퀀스가 $B$ 개면 전체는

$$\text{KV 캐시 총 바이트} = 2 \times n_{\text{layers}} \times n_{\text{kv-heads}} \times d_{\text{head}} \times b \times L \times B$$

읽는 법은 간단하다. KV 캐시는 시퀀스 길이 $L$ 과 배치 $B$ 에 정확히 선형으로 커진다. 모델 가중치는 고정이지만 KV는 "얼마나 길게, 얼마나 많이" 처리하느냐에 따라 무한정 증가한다. 이 선형성이 다음 절들의 병목을 만든다.

숫자를 넣어 보자. 8B급 모델을 $n_{\text{layers}}=32$, $n_{\text{heads}}=32$, $d_{\text{head}}=128$($d_{model}=4096$), fp16($b=2$), MHA로 두면 토큰 하나가 남기는 KV는

$$2 \times 32 \times 32 \times 128 \times 2 = 524{,}288 \text{ bytes} = 512\ \text{KB/토큰}$$

컨텍스트 $L=8192$ 에 배치 1이면 한 시퀀스의 KV만 4 GB다. 배치 32면 128 GB로 웬만한 단일 GPU를 넘긴다. 같은 모델을 GQA 4:1($n_{\text{kv-heads}}=8$)로 두면 토큰당 KV가 4분의 1로 줄어 128 KB, 컨텍스트 8192에 1 GB, 배치 32에 32 GB로 내려온다 — 산식에서 KV 헤드 수만 32에서 8로 바꾼 결과다. 70B급($n_{\text{layers}}=80$, $n_{\text{heads}}=64$, $d_{\text{head}}=128$, MHA)이면 토큰당 2.5 MB, 컨텍스트 8192에 약 20 GB로, 배치 몇 개만 돼도 수백 GB가 된다.

8B, MHA8B, GQA 870B, MHA
토큰당 KV512 KB128 KB2.5 MB
$L=8192$, $B=1$4 GB1 GB20 GB
$L=8192$, $B=32$128 GB32 GB640 GB
$L=128\text{K}$, $B=1$64 GB16 GB320 GB

이 값은 연속 저장을 가정한 이론 하한이라는 점을 정리해 둔다. 실제 서빙은 가변 길이와 동적 배치 탓에 메모리를 미리 블록으로 할당해 두므로 내부 단편화로 실측이 더 크다 — 이 낭비를 제거하는 것이 PagedAttention이다(5.3). 반대로 어텐션 정밀도를 fp8이나 int8로 낮춘 KV 캐시 양자화(5.2)로 $b$ 를 줄이면 위 값이 그만큼 준다.

5.1.5 두 개의 장벽

KV 캐시는 서빙을 두 방향에서 동시에 제약한다. 하나는 메모리 용량이고, 다른 하나는 메모리 대역폭이다.

용량 쪽부터 보자. GPU VRAM은 크게 모델 가중치, KV 캐시, 활성값과 기타로 나뉜다. 가중치는 고정 상수지만 KV는 $L\times B$ 로 자란다. 그래서 동시에 처리할 수 있는 요청 수와 최대 컨텍스트 길이는 "남은 VRAM ÷ 요청당 KV"로 결정된다.

$$B_{\max} \approx \frac{\text{VRAM} - \text{가중치} - \text{여유}}{\;\text{토큰당 KV} \times L\;}$$

80 GB GPU에 8B fp16 모델(가중치 약 16 GB)을 추가하면 KV에 쓸 수 있는 것은 대략 60 GB다. 앞의 8B MHA(컨텍스트 8192에 4 GB/요청)면 배치가 15 안팎에서 막히고, GQA(1 GB/요청)면 60 안팎까지 늘어난다. KV를 4분의 1로 줄이면 같은 GPU로 4배의 동시 요청을 처리한다는 뜻이다 — 처리량이 배치에 크게 좌우되므로(5.1.8), 이것이 GQA·MLA가 서빙에서 그토록 중요한 이유다.

배치 작음 모델 가중치 (고정) KV 남는 VRAM 배치 큼 모델 가중치 (고정) KV 캐시 (배치·길이에 비례해 팽창) 한계 초과 모델 가중치 KV가 VRAM 초과 → OOM / 요청 거절·대기 VRAM 상한
GPU VRAM 예산 — 가중치는 고정, KV는 배치·길이에 선형으로 증가한다

대역폭 쪽 장벽은 다른 방식으로 문제가 된다. 디코드 스텝마다 어텐션을 계산하려면 KV 캐시 전체를 읽어야 한다 — 과거 모든 키·값과 어텐션하기 때문이다. 컨텍스트가 길수록 스텝당 읽는 바이트가 커지고, 메모리 대역폭이 유한하므로 토큰당 시간이 컨텍스트 길이에 따라 늘어난다. 긴 대화일수록 토큰이 느려지는데, 그 이유가 연산이 아니라 KV를 읽는 대역폭이라는 것이 중요하다. 다음 절에서 이것을 실제 밀리초로 계산한다.

정리하면 KV 캐시는 용량으로는 배치와 컨텍스트의 상한을, 대역폭으로는 토큰당 속도를 규정한다. 그래서 서빙의 거의 모든 기법이 KV를 겨냥한다 — GQA·MQA·MLA는 KV 헤드를 줄이고(1.6), KV 양자화는 정밀도를 낮추고(5.2), PagedAttention은 단편화를 없애고(5.3), 프리픽스 캐시는 공유 프롬프트의 KV를 재사용한다(5.6). 여기서 가장 흔한 실무 실수를 명시해 두자. 서빙 용량을 모델 가중치 크기만으로 계획하면 반드시 문제가 생긴다. "8B 모델이니 16 GB면 되겠지"는 틀렸다 — 방금 봤듯 배치 15면 이미 KV가 60 GB로 가중치보다 크다.

5.1.6 디코드 한 스텝의 시간을 직접 계산하기

"디코드가 대역폭 바운드"라는 말을 밀리초로 바꿔 보면 모든 것이 명확해진다. 대역폭 바운드라는 것은 스텝 시간이 "읽어야 할 바이트 ÷ 메모리 대역폭"으로 결정된다는 뜻이다. 배치 1 디코드 한 스텝에서 읽어야 할 것은 두 가지다 — 모델 가중치 전부와, 현재 컨텍스트만큼의 KV 캐시다.

$$\text{TPOT} \approx \frac{\text{가중치 바이트} + \text{KV 바이트}}{\text{메모리 대역폭}}$$

8B fp16 모델(가중치 16 GB)을 대역폭 3.3 TB/s인 GPU에서 실행한다 하자(수치는 개략). 컨텍스트가 짧아 KV를 무시하면 스텝당 읽는 바이트가 16 GB이므로

$$\text{TPOT} \approx \frac{16\ \text{GB}}{3.3\ \text{TB/s}} \approx 4.8\ \text{ms} \;\Rightarrow\; \text{약 }200\ \text{tok/s (배치 1)}$$

이것이 이 하드웨어에서 이 모델의 대략적인 단일 요청 속도 상한이다. 연산 시간이 아니라 가중치를 한 번 읽는 시간이 이것을 정한다. 이제 컨텍스트를 채워 넣어 보자. $L=8192$ (MHA KV 4 GB)면 스텝당 $16+4=20$ GB를 읽어 TPOT가 약 6 ms로 늘고, $L=128\text{K}$ (KV 64 GB)면 $16+64=80$ GB를 읽어 약 24 ms, 즉 40 tok/s로 떨어진다. 같은 모델·같은 GPU인데 컨텍스트가 길어졌다는 이유만으로 토큰이 다섯 배 느려진 것이다 — 이것이 "긴 대화가 느려지는" 현상의 정확한 정체이고, KV 헤드를 줄이거나(GQA로 KV 4배 축소) KV를 양자화하면(fp8로 절반) 이 대역폭 부담이 그만큼 줄어 TPOT가 회복된다.

70B 모델은 가중치가 fp16으로 140 GB라 단일 GPU에 안 들어가 텐서 병렬로 여러 GPU에 분할한다(2.4). 이 경우 각 GPU가 자기 몫의 가중치만 읽고 GPU들의 대역폭이 합쳐지지만, 그 대신 매 스텝 GPU 간 통신이 개입해 지연에 더해진다 — 그래서 큰 모델의 TPOT는 대역폭뿐 아니라 인터커넥트에도 좌우된다(2.5). 요점은 변하지 않는다. 디코드 속도의 상한은 "스텝마다 읽어야 할 바이트를 대역폭으로 나눈 것"이고, 그 바이트의 두 축이 가중치(모델 크기)와 KV(컨텍스트 길이)다.

5.1.7 지연을 재는 법

추론 성능을 말하려면 어느 지표를 말하는지 명시해야 한다. "빠르다"는 한 단어로는 아무것도 결정할 수 없다. 프리필과 디코드가 다른 국면이듯 지표도 여럿이고, 각각 다른 것을 잰다.

TTFT(첫 토큰 지연)는 요청을 보낸 순간부터 첫 토큰이 나오기까지의 시간으로, 프리필 시간과 큐 대기가 지배한다. 프롬프트가 길수록 커진다. 스트리밍 챗에서 사용자가 체감하는 반응성 — 타이핑이 시작되기까지 얼마나 걸리나 — 이 바로 이것이다. TPOT(토큰당 시간, ITL이라고도 한다)는 첫 토큰 이후 토큰 하나가 나오는 간격으로, 앞 절에서 계산한 디코드 스텝 시간이 지배한다. 스트리밍에서 글자가 흐르는 속도로 체감되며, 그 역수가 사용자당 생성 속도다. 종단 지연은 요청부터 마지막 토큰까지의 총 시간이고, 세 지표가 이렇게 이어진다.

$$\text{종단 지연} \approx \text{TTFT} + (N_{\text{out}} - 1)\times \text{TPOT}$$

요청 TTFT (프리필+대기) 첫 토큰 마지막 토큰 TPOT/ITL (토큰 간격) 종단 지연 = TTFT + (Nout − 1)·TPOT
한 요청의 지연 해부 — TTFT + 토큰 간격들 = 종단 지연

처리량은 또 다른 축이다. 시스템이 단위 시간에 처리하는 양인데, 토큰/초와 요청/초 두 단위가 흔히 쓰인다. 여기서 반드시 구분할 것이 있다. 한 요청의 사용자 체감 속도(대략 TPOT의 역수)와 시스템 전체가 뽑아내는 토큰/초는 다른 수다. 배치를 키우면 후자는 오르지만 전자는 오히려 떨어질 수 있다. 그래서 "이 시스템 5000 tok/s"라는 말은 단일 요청 기준인지 시스템 전체인지, 배치와 컨텍스트 길이가 얼마인지를 밝히지 않으면 사실상 무의미하다.

어느 지표가 중요한지는 제품이 정한다. 대화형 챗과 자동완성에서는 TTFT가 체감 반응성을 좌우한다. 스트리밍 UX에서는 TPOT가 읽는 속도보다 빠르기만 하면 충분하다 — 사람이 초당 열몇 토큰보다 빨리 못 읽으니, 그 이상의 TPOT는 사용자에게 안 보이는 잉여다. 비스트리밍 API나 에이전트의 한 스텝에서는 종단 지연이, 오프라인 배치 작업에서는 처리량이 중요하다. 그리고 이 지표들은 평균이 아니라 분포의 꼬리(p50·p95·p99)로 봐야 한다. 평균 TTFT가 좋아도 p99가 나쁘면 일부 사용자가 크게 기다리고, 특히 큐 대기가 길어지면 TTFT p99가 급증한다. 한 걸음 더 나아가면 처리량은 goodput으로 재는 것이 정직하다 — SLO를 만족하며 처리한 양만 세는 것이다. SLO를 어긴 처리량은 장부상 숫자일 뿐, 실제로는 나쁜 경험을 대량 생산한 것이기 때문이다.

5.1.8 산술강도와 roofline

앞에서 디코드 스텝 시간을 대역폭으로 계산했는데, 왜 배치를 키우면 이 그림이 달라지는지를 산술강도와 roofline으로 정밀하게 만들 차례다. 이 하나를 이해하면 서빙의 거의 모든 처리량 결정을 스스로 판단할 수 있다. 산술강도는 한 연산에서 수행한 FLOPs를 메모리에서 옮긴 바이트로 나눈 값이다.

$$I = \frac{\text{FLOPs}}{\text{옮긴 바이트}}\quad[\text{FLOP/byte}]$$

이 값이 낮으면 메모리 바운드(연산 유닛이 데이터를 기다린다)이고, 높으면 연산 바운드(메모리는 여유롭고 연산이 병목)다. 트랜스포머 순전파는 토큰당 대략 $2P$ FLOPs를 쓰고($P$ 는 파라미터 수, 1.4.15의 $2P$ 규칙), 가중치를 fp16으로 읽으면 $2P$ 바이트를 옮긴다. 배치 1 디코드에서는 토큰 하나를 위해 가중치 $2P$ 바이트를 읽어 $2P$ FLOPs를 하므로 $I \approx 1$ 로 극단적으로 낮다. 배치 $B$ 로 디코드하면 가중치를 한 번 읽어 $B$ 개 토큰에 재사용한다. FLOPs는 $2PB$ 로 늘지만 옮긴 가중치 바이트는 여전히 $2P$ 라, $I \approx B$ 다.

디코드의 산술강도가 대략 배치 크기와 같다는 이 결과가 핵심이다. 배치를 키우면 같은 가중치 읽기로 더 많은 토큰을 처리해 산술강도가 선형으로 오른다 — 이것이 "배치로 가중치를 재사용한다"의 정확한 의미다. 프리필은 한 번에 $L_p$ 개 토큰을 처리하므로 이미 $I\approx L_p$ 로 높고, 그래서 처음부터 연산 바운드다.

하드웨어에는 두 개의 상한이 있다. 최대 연산 성능(peak FLOP/s)과 최대 메모리 대역폭(byte/s)이다. 어떤 작업이 실제로 낼 수 있는 성능은 둘 중 낮은 쪽에 갇힌다.

$$\text{달성 성능} \le \min\big(\underbrace{P_{\text{peak}}}_{\text{연산 지붕}},\ \underbrace{BW \times I}_{\text{대역폭 지붕}}\big)$$

두 지붕이 만나는 능선점의 산술강도는 $I^\star = P_{\text{peak}} / BW$ 다. $I$ 가 이보다 작으면 대역폭 바운드로 성능이 $BW\times I$ 에 갇히고, 크면 연산 바운드다. 현대 데이터센터 GPU의 능선점은 대략 수백 FLOP/byte 수준이다 — 이를테면 peak가 1000 TFLOP/s, 대역폭이 3.3 TB/s면 $I^\star \approx 300$ 이다.

산술강도 I (FLOP/byte, log 스케일) → 달성 성능 (FLOP/s, log) → 대역폭 지붕 (BW·I) 연산 지붕 (peak FLOP/s) 능선점 I⋆ = peak/BW 디코드 (배치 작음) I≈배치크기 → 대역폭에 갇힘 프리필 (많은 토큰 병렬) I≈프롬프트 길이 → 연산 지붕
Roofline — 낮은 산술강도(디코드)는 대역폭 지붕에 갇힌다

그림이 말하는 것은 이렇다. 배치 1 디코드는 산술강도가 1로 능선점(수백)보다 한참 왼쪽, 대역폭 지붕의 하단에 위치한다. GPU의 연산 능력을 수백분의 1도 못 쓴다 — 그래서 디코드에서 GPU가 유휴 상태다. 배치를 키우면 점이 대각선을 따라 오른쪽으로 이동해 능선점에 다가가고, 그만큼 GPU 활용도와 처리량이 오른다. 프리필은 이미 오른쪽 연산 지붕에 있어 배치의 이득이 적다.

이제 5.1.6과 이어 말할 수 있다. 디코드가 대역폭 바운드인 구간에서는 배치를 늘려도 스텝당 시간이 거의 그대로다 — 어차피 병목은 가중치를 읽는 시간이고, 그 시간에 토큰 하나를 처리하든 $B$ 개를 처리하든 읽는 가중치 바이트는 같기 때문이다. 그런데 그 한 번의 읽기로 $B$ 개 토큰이 나오므로 시스템 처리량이 배치에 거의 비례해 오른다. 뒤집어 말하면, 배치 1로 서빙하는 것은 GPU 대역폭의 대부분을 "가중치를 읽어 토큰 하나 만들고 버리는 데" 낭비하는 것이다. 다만 배치를 키우는 데는 두 한계가 있다. KV 캐시 메모리가 배치 상한을 정하고(5.1.5), 어느 지점을 넘어 능선점에 닿으면 연산 지붕에 걸려 더 키워도 처리량은 안 늘고 지연만 는다.

5.1.9 프리필의 비용과 TTFT

디코드에 가려 프리필이 사소해 보이지만, 프롬프트가 길어지면 TTFT의 주범이 된다. 프리필은 $L_p$ 개 토큰을 한 번에 처리하므로 연산 바운드이고, 그 FLOP은 토큰당 $2P$ 규칙에 따라 대략 $2 P L_p$ 다. 8B 모델에 1000 토큰 프롬프트면 $2\times 8\times10^9\times 1000 = 1.6\times10^{13}$, 즉 16 TFLOP이고, GPU를 절반가량 활용한다 치면(연산 바운드라 활용도가 높다) 수십 밀리초에 끝난다. 그런데 프롬프트가 길어지면 여기에 어텐션의 $O(L_p^2)$ 이 추가된다 — 1.4.15에서 봤듯 $L_p$ 가 수만을 넘으면 이 제곱항이 지배해 TTFT가 급격히 늘고, 롱컨텍스트에서 "첫 글자가 뜨기까지 한참 걸리는" 경험이 여기서 나온다.

프리필과 디코드가 한 GPU에서 섞이면 서로를 방해한다. 긴 프리필 하나가 GPU를 오래 점유하면, 그동안 다른 요청들의 디코드가 멈춰 그들의 TPOT가 급등한다 — 한 사용자의 긴 프롬프트가 다른 사용자들의 스트리밍을 끊는 것이다. 이것을 완화하는 두 기법이 있다. 청크 프리필(chunked prefill) 은 긴 프리필을 작은 조각으로 분할해 디코드 스텝 사이사이에 삽입해, 프리필이 디코드를 오래 막지 않게 한다. 프리필/디코드 분리(disaggregation) 는 아예 프리필 전용 GPU 풀과 디코드 전용 풀을 나눠, 연산 바운드인 프리필과 대역폭 바운드인 디코드를 각자에 최적인 하드웨어·배치로 실행한다. 둘 다 5.3·5.5에서 다루지만, 그 동기가 바로 이 절의 "프리필과 디코드는 성격이 반대라 한 GPU에서 섞으면 서로를 갉아먹는다"에 있다.

5.1.10 처리량과 지연의 맞바꿈

배칭은 처리량을 살리지만 개별 요청의 지연을 악화시킨다. 이 긴장이 서빙 설정의 근본이다. 배치를 키우면 가중치를 재사용해 시스템 처리량이 오르지만, 세 가지 방식으로 개별 지연이 나빠진다. 배치가 커지면 스텝당 연산이 늘어 연산 지붕에 가까워질수록 TPOT가 길어진다. 요청이 배치에 실리려 큐에서 기다리는 시간이 TTFT에 더해진다. 그리고 한 배치의 모든 요청이 함께 스텝을 밟으므로 느린 요청 하나가 배치 전체를 늦춘다.

배치 크기 ↑ 가중치 재사용↑ → 시스템 처리량 ↑ 스텝당 연산↑ · 큐 대기↑ → 개별 요청 지연 ↑ KV 캐시 메모리↑ → VRAM 한계에서 배치 상한

어느 쪽을 택할지는 워크로드가 정한다. 대화형 챗이나 자동완성처럼 사람이 실시간으로 기다리는 경우에는 지연이 우선이다. TTFT와 TPOT를 SLO 안에 넣는 것이 목표이니 배치를 낮게 설정해 GPU 활용도를 다소 포기하고 응답성을 산다 — 토큰당 비용은 높아진다. 반대로 문서 대량 요약이나 임베딩 생성, 야간 배치처럼 개별 지연이 중요하지 않은 오프라인 작업에서는 처리량이 우선이다. 배치를 최대로 키워 GPU를 꽉 채우고 토큰당 비용을 최소화하며, 개별 요청은 느려도 무방하다. 실서비스의 흔한 답은 그 사이의 균형이다 — "TTFT와 TPOT의 SLO를 지키는 한도에서 배치를 최대로." 이것이 goodput 최적화이고, 연속 배칭과 우선순위 큐, 프리필/디코드 분리 같은 기법(5.3·5.5)이 이 균형을 자동화한다.

태도배치최적화 대상맞는 워크로드대가
지연 우선작게TTFT·TPOT실시간 챗·코딩 보조비용/토큰↑, GPU 놀림
처리량 우선최대tok/s·비용오프라인 배치·임베딩개별 지연↑
균형SLO 한도 최대goodput대다수 실서비스운영 복잡도↑

여기서 "배치를 키우면 항상 이득"이라는 흔한 오해를 분명히 깨 두자. 대역폭 바운드 구간을 벗어나 연산 지붕에 닿으면 배치를 더 키워도 처리량은 안 늘고 지연만 는다. KV 메모리 상한을 넘으면 애초에 배치를 못 키운다. 어디까지가 대역폭 바운드인지는 모델·하드웨어·컨텍스트 길이에 따라 달라 실측이 필요하다 — 이론은 방향을 주고, 프로파일링이 값을 준다.

5.1.11 왜 정적 배칭으로는 부족한가

배치를 키우고 싶어도, 순진하게 배치를 묶으면 대부분의 이득이 손실된다. 이유는 요청들의 길이가 제각각이기 때문이다. 정적 배칭(static batching)은 요청 여럿을 한 배치로 묶어 시작하고, 그 배치의 모든 요청이 끝날 때까지 함께 스텝을 밟는다. 그런데 어떤 요청은 20 토큰을 생성하고 끝나는데 같은 배치의 다른 요청은 500 토큰을 생성한다면, 먼저 끝난 요청의 슬롯은 나머지가 끝날 때까지 빈 채로 스텝을 낭비한다 — 배치가 채워졌다가 요청이 하나둘 끝나며 점점 비어 가고, 그 빈 슬롯만큼 GPU가 유휴 상태다. 게다가 새로 도착한 요청은 지금 실행 중인 배치가 다 끝날 때까지 큐에서 기다려야 해서 TTFT가 나빠진다.

연속 배칭(continuous batching, in-flight batching)이 이것을 고친다. 배치를 요청 단위가 아니라 토큰(스텝) 단위로 관리해서, 한 요청이 끝나면 그 슬롯에 즉시 대기 중인 새 요청을 채워 넣는다. 배치가 항상 가득 찬 상태로 유지되니 GPU가 유휴 상태가 되지 않고, 새 요청도 다음 스텝에 곧바로 합류해 큐 대기가 줄어든다. 이것이 5.1.8에서 본 "배치를 크게 유지해 능선점 쪽으로 민다"를 실제 서빙에서 구현하는 방법이고, vLLM 같은 현대 서빙 엔진이 기본으로 하는 일이다. 메커니즘의 상세와 PagedAttention과의 결합은 5.3에서 다룬다 — 여기서는 "정적 배칭은 제각각인 길이 때문에 새고, 연속 배칭이 그걸 막는다"는 동기만 이해해 두면 된다.

5.1.12 이 기본 모형의 예외들

지금까지 세운 것은 dense 자기회귀 모델의 기본 모형이다. 이 위에 추가되는 몇몇 기법은 이 모형을 바꾸는 것이 아니라 이용하는데, 그것들이 어떻게 그림을 비트는지 알아 두면 나중에 혼동하지 않는다.

전문가 혼합(MoE, 1.6)은 산술강도 계산을 바꾼다. MoE는 파라미터가 많아도 각 토큰이 그중 일부 전문가만 활성화하므로, FLOP은 활성 파라미터에 비례해 적지만 서빙에서 읽어야 할 가중치는 어느 전문가가 뽑힐지 몰라 사실상 전부다. 즉 MoE는 디코드에서 FLOP 대비 메모리 읽기가 dense보다 더 불리해질 수 있고, 전문가를 여러 GPU에 흩어 놓으면 라우팅에 통신까지 더해진다. 투기적 디코딩(5.3)은 디코드의 직렬성과 대역폭 장벽을 부분적으로 우회한다 — 작고 빠른 초안 모델이 여러 토큰을 한 번에 추측하고 큰 모델이 그것들을 한 번의 순전파로 검증하는데, 이렇게 하면 큰 모델의 가중치를 한 번 읽어 여러 토큰을 진행시키므로 대역폭 활용이 좋아진다. 수락률이 높을수록 이득이 크고, 분포를 보존하도록 설계돼 품질 손실이 없다는 것이 핵심이다. 프리필/디코드 분리는 5.1.9에서 봤듯 두 국면을 다른 하드웨어 풀로 나눈다.

이 예외들의 공통점은, 전부 이 장의 기본 모형 — 자기회귀는 직렬이고, 디코드는 대역폭 바운드이며, KV가 병목이다 — 을 전제로 하고 그 위에서 특정 항을 공략한다는 것이다. 기본 모형을 확실히 쥐고 있으면 이 기법들이 무엇을 어떻게 개선하는지가 명료해진다.

5.1.13 용량 계획 — 수계산

마지막으로 이 장의 산식을 실무 질문에 적용해 보자. "초당 10건, 요청당 500 토큰 생성, TPOT 20 ms를 지키려면 GPU가 몇 장 필요한가?" 리틀의 법칙이 다리를 놓는다. 한 요청은 500 토큰을 TPOT 20 ms로 생성하니 디코드 슬롯을 약 $500\times 20\text{ms} = 10$ 초 점유한다. 초당 10건이 들어오고 각자 10초를 머무르면, 정상 상태에서 시스템에 동시에 살아 있는 요청 수는 $10\ \text{req/s}\times 10\ \text{s} = 100$ 건이다. 즉 디코드 배치가 항상 100가량 유지돼야 한다.

이제 이 배치 100이 한 GPU에 들어가는지 두 장벽으로 확인한다. 메모리 장벽: 8B GQA 모델을 컨텍스트 2000으로 서빙하면 요청당 KV가 약 0.25 GB이니 100건이면 25 GB, 가중치 16 GB를 더해 41 GB로 80 GB GPU에 여유 있게 들어간다. 대역폭 장벽: 배치 100이면 산술강도가 약 100으로 능선점(수백)에 아직 못 미쳐 여전히 대역폭 바운드에 가깝고, 그렇다면 배치 100의 스텝 시간이 배치 1과 크게 다르지 않아(가중치 읽기가 지배) TPOT 20 ms 목표를 지킬 만하다. 이 계산대로면 한 장으로 감당할 수 있다는 결론이 나온다 — 다만 이것은 개략 산정이고, 트래픽의 피크, 프리필 부담, p99 SLO, 컨텍스트 길이 분포를 넣으면 값이 달라지므로 반드시 실측으로 검증해야 한다(5.5). 요점은 방법이다. 목표 QPS와 SLO에서 리틀의 법칙으로 필요한 동시 배치를 구하고, 그 배치가 KV 메모리와 대역폭 두 장벽에 걸리는지 확인하면, "GPU 몇 장"이 산식에서 나온다.

이 장은 추론의 물리와 지표를 세웠다. 기억할 문장은 처음의 그것이다 — 생성의 시간은 연산이 아니라 메모리에서 온다. 이 하나를 쥐면 제5부의 나머지가 전부 "메모리와 대역폭의 벽을 어떻게 깎고 우회하느냐"의 변주로 읽힌다. 바로 다음 장은 정밀도 $b$ 를 낮춰 가중치와 KV의 메모리·대역폭을 직접 줄이는 양자화이고(5.2), 그다음은 유휴 상태인 디코드 GPU를 연속 배칭으로 채우고 KV 단편화를 PagedAttention으로 없애며 대역폭 장벽을 투기적 디코딩으로 우회하는 기법들이다(5.3).