Documents
Home>Documents>AI>Inference

Prefill vs Decode 병목 해부: Roofline Model로 LLM 추론의 두 단계를 진단하다

10 min readAug 31, 2026Aug 31, 2026

Prefill과 Decode는 같은 모델, 같은 GPU 위에서 돌아가지만 포화시키는 자원이 다르다. Prefill은 Compute를 쥐어짜고, Decode는 메모리 대역폭을 긁어먹는다. 이 차이를 직관으로만 알고 있을 때와 수치로 계산해서 알 때는 설계 결정이 달라진다.

연산 구조의 차이

Prefill은 입력 토큰 시퀀스 전체를 한꺼번에 처리한다. Linear 레이어 하나를 보면, 가중치 행렬 W ∈ R^{d_out × d_in}에 B개 시퀀스 × S토큰짜리 입력 행렬 X ∈ R^{BS × d_in}을 곱한다 — 행렬-행렬 곱(GEMM)이다. GPU의 Tensor Core는 큰 행렬 곱에서 병렬도를 최대로 끌어올릴 수 있고, SM(Streaming Multiprocessor) 전체가 일감을 나눠 가진다.

Decode는 이미 생성된 KV 캐시를 읽고 다음 토큰 하나를 뽑는다. 현재 스텝에서 각 시퀀스가 처리하는 새 토큰은 정확히 1개다. 따라서 입력은 X ∈ R^{B × d_in}이 되고 가중치 행렬과의 곱은 행렬-벡터 곱(GEMV)으로 쪼그라든다. GEMM과 GEMV는 둘 다 행렬 곱이지만 GPU가 실제로 동원하는 자원의 비율이 전혀 다르다.

Arithmetic Intensity: 수치로 보는 두 단계의 간격

Williams et al.의 Roofline 논문이 정의하는 Arithmetic Intensity(AI)는 간단하다:

AI = FLOPs / Memory_Traffic  (단위: FLOPs/Byte)

GPU가 메모리에서 1 Byte를 끌어올 때 몇 번의 부동소수점 연산을 소화하는지를 나타내는 비율이다. AI가 높을수록 Compute가 병목이 되고, 낮을수록 메모리 대역폭이 병목이 된다.

Linear 레이어 하나의 AI를 계산해 보자. 가중치 행렬 W는 FP16으로 저장되므로 로드 비용은 2 × d_in × d_out Bytes다. 처리하는 토큰 수를 T라 하면:

FLOPs  = 2 × T × d_in × d_out
Memory = 2 × d_in × d_out  Bytes  (가중치 로드 기준)

AI = FLOPs / Memory = T  (FLOPs/Byte)

d_in × d_out이 약분되어 사라진다. AI는 처리하는 토큰 수 T와 같다. Prefill에서 T = B × S, Decode에서 T = B × 1이므로 둘의 AI 비는 시퀀스 길이 S에 정확히 비례한다.

LLaMA-3 8B의 Query projection(d_in = d_out = 4096)을 예로 구체화하면:

단계배치 / 시퀀스FLOPs가중치 로드AI
PrefillB=64, S=512≈ 1.10 TFLOPs32 MB32,768 FLOPs/Byte
DecodeB=1, S=1≈ 33.6 MFLOPs32 MB1 FLOPs/Byte

같은 가중치 행렬을 올리는 데 같은 비용(32 MB)을 쓰고, Prefill이 거기서 뽑아내는 연산은 Decode의 32,768배다. 가중치를 한 번 읽어서 얼마나 써먹느냐의 차이다.

Roofline Model: GPU 스펙 위에 두 단계를 올려보기

Roofline Model은 AI를 x축, 달성 가능한 성능(FLOPS)을 y축에 놓고 두 개의 "지붕"을 그린다. 하나는 GPU의 peak compute(수평선), 다른 하나는 메모리 대역폭 × AI(우상향 사선)다. 실제 성능은 두 지붕 중 낮은 쪽을 넘을 수 없다.

두 지붕이 만나는 점이 Ridge Point다:

Ridge Point = Peak FLOPs / Memory Bandwidth

A100 SXM 80GB과 H100 SXM 80GB의 스펙과 Ridge Point:

GPUPeak FP16메모리 대역폭Ridge Point
A100 SXM 80GB312 TFLOPS2.0 TB/s156 FLOPs/Byte
H100 SXM 80GB989 TFLOPS3.35 TB/s≈ 295 FLOPs/Byte

Prefill(B=64, S=512)의 AI=32,768은 두 GPU 모두에서 Ridge Point를 수십 배 초과한다 — Roofline 그래프 기준으로 수평 지붕 밑, 오른쪽 끝 Compute-Bound 영역이다. Decode(B=1)의 AI=1은 A100 Ridge Point의 1/156, H100 Ridge Point의 1/295에 위치한다.

H100이 A100보다 Ridge Point가 높다는 점은 Decode에 불리한 조건이다. Compute 증가(3.17×)가 메모리 대역폭 증가(1.68×)보다 크기 때문에, Decode처럼 AI가 낮은 워크로드는 H100에서 가격 대비 활용률이 상대적으로 더 낮아진다. "H100을 사면 서빙이 빨라진다"는 기대가 Decode 지연 시간(TPOT)에서는 대역폭 증가분인 1.68× 이상을 얻기 어렵다는 뜻이기도 하다.

배치를 키우면 Decode가 Compute-Bound로 넘어갈까

AI = B (Decode)이므로, B를 키우면 이론상 Ridge Point를 넘을 수 있다. 임계 배치 크기는 단순하다:

B_threshold ≈ Ridge Point

A100: B_threshold ≈ 156
H100: B_threshold ≈ 295

문제는 KV 캐시다. LLaMA-3 8B는 GQA 구조(KV head 8개)를 사용하므로 토큰당 KV 캐시 크기는:

KV/token = 2(K,V) × 8(KV heads) × 128(d_head) × 32(layers) × 2(FP16)
         = 131,072 Bytes  ≈  128 KB/token

A100 80GB에서 모델 가중치(FP16)는 16 GB를 차지한다. 남은 HBM은 64 GB. B=156 요청을 각 4096 토큰 컨텍스트로 서비스하면 KV 캐시만:

156 × 4096 × 128 KB ≈ 83.9 GB  →  OOM

컨텍스트를 절반(2048 토큰)으로 줄여도 약 41.9 GB — 여기에 activation, 스케줄러 오버헤드, 단편화 여유까지 더하면 실제 운영 가능한 배치는 훨씬 작다.

Compute-Bound 임계점에 닿기 전에 HBM이 바닥난다. 배치를 키워도 throughput이 선형으로 늘지 않는 이유의 한 축이 이것이다. Decode는 KV 캐시 경쟁이 시작되는 시점부터 이미 다른 제약에 묶인 채로 Memory-Bound 상태를 유지한다.

이 비대칭이 서빙 설계 결정의 공통 뿌리다

Disaggregated Prefill, Chunked Prefill, Speculative Decoding은 각기 다른 문제를 해결하는 것처럼 보이지만 모두 AI 비대칭을 출발점으로 삼는다.

Disaggregated Prefill은 AI 비대칭을 GPU 단위로 물리 분리한다. Prefill(Compute-Bound)은 Compute-dense한 GPU 풀로, Decode(Memory-Bound)는 대역폭이 높은 GPU 풀로 라우팅하고, 사이에서 KV 캐시를 P2P로 전달한다. vLLM이 P 노드와 D 노드를 분리해 설계한 동기가 정확히 이 AI 비대칭이다 — 같은 GPU 하나에 두 워크로드를 섞으면 어느 쪽도 최적화할 수 없다는 판단이다.

Chunked Prefill은 긴 Prefill 시퀀스를 작은 청크로 자르고 Decode 배치와 인터리빙한다. 청크 크기 C를 기준으로 Prefill의 AI ≈ C(단일 시퀀스 기준). A100에서는 C=256이면 Ridge Point(156)를 초과해 Compute-Bound로 진입하지만, H100에서는 Ridge Point가 ≈295로 높아 C=256으로는 여전히 Memory-Bound다. C=512가 돼야 안정적으로 Compute-Bound 구간에 들어온다. Chunked Prefill의 최적 청크 크기가 GPU 스펙에 따라 달리 설정되어야 하는 이유가 이것이다.

Speculative Decoding은 Draft 모델이 k개의 후보 토큰을 한 번에 생성하면 Target 모델이 이를 병렬로 검증한다. Target 모델의 검증 스텝에서 B=1 GEMV 여러 개를 길이 k인 시퀀스로 묶어 Prefill에 가까운 GEMM으로 처리한다 — AI를 직접 올리려는 시도다. k=4면 AI가 4배로 뛰지만 여전히 Decode 영역에 머문다. 그래서 Draft 모델의 수락률(acceptance rate)이 높아야 효과가 나고, 수락률이 낮으면 오버헤드만 커진다.

세 전략 모두 "Decode의 AI를 올리거나, Decode의 비율을 줄이거나, AI가 다른 두 단계를 분리하거나" 중 하나다.

실측: Prefill과 Decode 구간을 프로파일러로 분리하면

LLaMA-3 8B 추론에서 두 구간을 따로 보는 가장 빠른 방법은 dcgmi다. SM 활용률(필드 1002)과 DRAM 활용률(필드 1005)을 100ms 간격으로 출력한다:

# GPU 0 기준, 100ms 간격으로 SM 활용률과 DRAM 활용률 출력
dcgmi dmon -e 1002,1005 -d 100

구간별 타임라인이 필요하면 nsight systems에 NVTX 마커를 조합한다:

nsys profile \
  --trace=cuda,nvtx \
  --gpu-metrics-device=0 \
  --output=llama3_profile \
  python run_inference.py
import nvtx

with nvtx.annotate("prefill", color="blue"):
    output = model.forward(input_ids, use_cache=False)

with nvtx.annotate("decode", color="red"):
    for _ in range(max_new_tokens):
        output = model.forward(next_token, use_cache=True, past_key_values=kv)

실측에서 나오는 수치는 이론과 잘 맞는다. Prefill 구간: SM utilization ~80-90%, DRAM BW utilization ~25-35%. Decode 구간: SM utilization ~10-20%, DRAM BW utilization ~85-95%. 두 숫자가 구간마다 반전되는 것이 보인다. 시퀀스가 짧거나 배치가 작을수록 Prefill에서 SM util이 내려오고, Decode는 배치 크기와 무관하게 대역폭 포화 상태를 유지한다.

단순화의 한계

짧은 프롬프트(S < 64)에서는 Prefill도 AI가 Ridge Point 아래로 떨어진다. S=32, B=1이면 AI=32 — A100 Ridge Point(156)의 1/5 수준이다. RAG 파이프라인이나 Tool Calling처럼 프롬프트가 짧고 배치도 작은 환경에서는 Prefill도 메모리 대역폭에 발목이 잡힐 수 있다.

Flash Attention은 이 계산을 복잡하게 만든다. Flash Attention은 Attention 레이어의 중간 행렬을 HBM에 쓰지 않고 SRAM 내에서 tiling해 처리하므로 HBM 트래픽이 naive 구현 대비 크게 줄어든다. Attention 레이어의 유효 AI는 위 분석이 제시하는 값보다 높다. Q, K, V, O 투영이나 FFN 같은 Linear 레이어에서는 가중치 로드가 지배적이어서 위 계산이 그대로 적용되지만, Attention 연산 자체는 Flash Attention 덕분에 Compute-Bound에 더 가까이 당겨진다.

이 두 예외를 알고 있으면 "Prefill=Compute-Bound, Decode=Memory-Bound"를 기본값으로 쓰되, 짧은 시퀀스 / Attention 비중이 높은 구조에서는 dcgmi 수치를 먼저 확인하게 된다.

Tags
LLMInferenceGPU서빙아키텍처KV 캐시메모리Roofline