2026. 8. 30. 08:26ㆍML-LLM serving
LLM 추론 최적화 7가지: 하나의 지도로 읽기
FlashAttention, KV 캐시 오프로딩, 추측 디코딩, Prefill-Decode 분리, 멀티 GPU/노드 병렬화. 이름만 늘어놓으면 서로 상관없는 일곱 개의 기법처럼 보입니다. 하지만 이들은 결국 Attention 알고리즘의 단점과 자원 소모를 보완하기 위한 각각 다른 접근 방식입니다.
추론은 왜 느리고 비싼가
LLM 추론 최적화를 위해서는 "현재 상황에서 무엇이 병목인가"를 도출해야 합니다.
hw적으로는 연산(compute) vs 메모리 대역폭(memory bandwidth) 입니다. 하나는 행렬 곱 같은 연산을 텐서 코어로 밀어붙이는 일(compute-bound), 다른 하나는 가중치나 캐시를 메모리에서 읽어오는 일(memory-bandwidth-bound)입니다. 그런데 현대 GPU는 연산 능력(FLOPS)이 메모리 대역폭보다 훨씬 빠르게 성장해왔습니다. 그래서 실제로는 "계산할 게 없어서" 노는 게 아니라 "데이터를 못 읽어와서" 노는 경우가 훨씬 잦습니다. 많은 워크로드가 memory-bound인 이유입니다.
모델의 구조적으로는 prefill vs decode로 나뉩니다. LLM의 생성은 성격이 정반대인 두 단계로 나뉩니다.
- Prefill: 입력 프롬프트 전체를 한 번에 처리해 첫 토큰을 만드는 단계. 토큰이 많고 병렬 처리가 가능해 텐서 코어가 꽉 차는 compute-bound 구간입니다. 여기서 나오는 지표가 TTFT(Time To First Token) 입니다.
- Decode: 그 뒤로 토큰을 하나씩 자기회귀적으로 이어 만드는 단계. 매 스텝마다 거대한 가중치와 KV 캐시를 통째로 읽어야 하는데 정작 계산량은 토큰 한 개 분량뿐이라, 메모리 대역폭이 병목이 되는 memory-bound 구간입니다. 여기서 나오는 지표가 ITL(Inter-Token Latency).
이 두 축을 포개면 최적화 기법들의 자리가 선명해집니다. 어떤 기법은 memory-bound인 decode를 노리고, 어떤 기법은 compute-bound인 prefill을 노리며, 또 어떤 기법은 이 둘이 같은 GPU에서 부딪히는 상황 자체를 없애려 합니다.

목차
서론에 언급한 일곱 가지 기법을 병목별로 정리했습니다. 순서는 "단일 GPU 내 최적화 → 스케일 아웃"으로 잡고 하나씩 다뤄 보겠습니다.
| 기법 | 겨냥하는 병목 | 한 줄 요약 |
|---|---|---|
| FlashAttention | Attention의 메모리 이동 | 스코어 행렬을 통째로 만들지 않고 블록 단위로 처리 |
| Advanced KV Caching | KV 캐시 재계산 | 이미 계산한 KV를 재사용해 prefill을 건너뜀 |
| KV Cache Offloading | GPU 메모리 용량 | KV 캐시를 CPU/SSD로 계층화해 더 많이 담음 |
| Speculative Decoding | decode의 순차성 | 작은 모델이 추측하고 큰 모델이 병렬 검증 |
| Prefill-Decode 분리 | 두 단계의 상호 간섭 | prefill과 decode를 물리적으로 다른 GPU에 |
| Multi-GPU 추론 | 단일 GPU 메모리/속도 | 모델을 여러 GPU에 나눠 담고 가속 |
| Multi-Node 추론 | 단일 노드 한계 | 노드를 넘어 확장, 통신 위계를 설계 |
먼저 GPU 한 장 안에서 할 수 있는 최적화부터 봅니다. 세 기법 모두 메모리를 다룹니다. 하나는 attention 연산 중에 오가는 메모리 이동을, 나머지 둘은 KV 캐시라는 메모리 덩어리를 다룹니다.
접근 1: VRAM 절약
FlashAttention - Attention을 IO-aware하게
문제 정의
Self-attention의 공식은 softmax(QKᵀ/√d)·V 입니다. 골칫거리는 중간 산출물인 QKᵀ, 즉 어텐션 스코어 행렬입니다. 시퀀스 길이가 N이면 이 행렬의 크기는 N×N이 됩니다. N이 8,000이면 6천4백만 개, 32,000이면 10억 개가 넘는 값이 쏟아지는 셈입니다.
순진하게 구현하면 이 N×N 행렬을 GPU의 메인 메모리(HBM)에 통째로 쓰고, softmax를 위해 다시 읽고, V와 곱하려고 또 읽습니다. 정작 곱셈 연산보다 이 거대한 행렬을 HBM에 들락날락시키는 메모리 이동이 시간을 다 잡아먹습니다. Attention은 전형적인 memory-bound 연산이고, 컨텍스트가 길어질수록(N² 스케일) 이 문제가 폭발합니다.
핵심 아이디어
FlashAttention의 핵심 아이디어는 N×N 행렬을 애초에 통째로 만들지 않는 것입니다.
주방에 비유해보겠습니다. 100인분 요리를 하면서 모든 재료를 조리대에 한꺼번에 펼치면, 좁은 조리대(빠른 메모리)가 금세 부족해져 바닥 창고(느린 HBM)를 계속 오가야 합니다. 대신 재료를 한 접시 분량씩만 가져와 조리대 위에서 끝내고 다음 접시로 넘어가면, 창고를 오가는 횟수가 확 줄어듭니다.
FlashAttention은 두 가지 재료로 이걸 해냅니다.
- 타일링(tiling): Q, K, V를 작은 블록으로 쪼개, GPU의 빠른 메모리(SRAM)에 들어가는 만큼만 가져와 블록 단위로 attention을 계산합니다.
- 온라인 softmax(online softmax): softmax는 원래 "전체 행을 다 봐야" 정규화가 되는데, 블록으로 쪼개면 전체를 볼 수 없습니다. 그래서 블록을 하나씩 볼 때마다 지금까지의 최댓값(max)과 합(sum)을 누적 갱신하며 결과를 그때그때 보정(rescale)합니다. 전체를 보지 않아도 수학적으로 똑같은 softmax 결과가 나옵니다.
이 둘 덕분에 거대한 스코어 행렬을 HBM에 쓰지 않고 최종 출력만 한 번 씁니다. 연산량을 줄인 게 아니라 메모리 입출력을 줄여 속도를 얻는 것이죠.

pseudocode 로 같이 살펴보겠습니다.
# Q, K, V: [N, d] → 블록으로 분할
O = 0 # 출력 누적
m = -inf # running max (각 쿼리 행별)
l = 0 # running sum (정규화 분모)
for K_j, V_j in K, V 블록들: # KV를 블록 단위로 순회
S = Q @ K_j.T / sqrt(d) # 이 블록만큼의 스코어 (작음)
m_new = max(m, rowmax(S)) # 최댓값 갱신
P = exp(S - m_new) # 안정화된 지수
l = l * exp(m - m_new) + rowsum(P) # 이전 합을 보정 후 누적
O = O * exp(m - m_new) + P @ V_j # 이전 출력을 보정 후 누적
m = m_new
O = O / l # 마지막에 한 번만 정규화
핵심은 exp(m - m_new)로 이전까지의 결과를 계속 보정하는 대목입니다. 새 블록에서 더 큰 최댓값이 나오면, 지금까지 쌓아온 값의 스케일을 거기에 맞춰줍니다. 이 덕분에 N×N 행렬 S는 어디에도 통째로 저장되지 않습니다.
Tradeoffs
얻는 것은 분명합니다. 메모리 사용량이 N²에서 선형(N)으로 줄고, HBM 왕복이 급감해 실제 속도가 몇 배 빨라지며, 정확도 손실은 없습니다(수학적으로 동일한 결과). 반대로 커널 구현이 복잡해집니다. 타일 크기, 블록 순회, 보정 로직을 하드웨어에 맞춰 정교하게 짜야 합니다. 게다가 학습(backward)에서는 저장하지 않은 중간값을 다시 계산(recomputation)해야 합니다. 메모리를 아끼는 대신 약간의 재연산을 감수하는 셈입니다.
FlashAttention 알고리즘은 GPU 하드웨어와 나란히 진화해왔다는 것입니다. "타일링 + 온라인 softmax"라는 같은 뼈대 위에서, 세대마다 하드웨어의 새 기능을 흡수합니다.
- FlashAttention-1 (2022): IO-aware 타일링으로 개념을 확립.
- FlashAttention-2 (2023): 시퀀스 방향 병렬화와 워프 간 작업 분할을 개선해 GPU 점유율을 끌어올림(대략 2배).
- FlashAttention-3 (2024): Hopper(H100)의 비동기성을 활용. 워프 특화(warp specialization)와 TMA로 데이터 로딩·행렬곱·softmax를 겹쳐 돌리고, FP8 저정밀 연산을 지원.
- FlashAttention-4 (2025): Blackwell(SM100)을 겨냥. 워프를 로드/행렬곱/softmax/보정/출력 단계로 더 잘게 특화해 파이프라인으로 흘리고, 지수 함수를 3차 다항식으로 근사(Schraudolph 계열)해 특수연산유닛 병목을 우회하며, 최댓값이 유의미하게 바뀔 때만 보정하도록 rescale 횟수를 줄였습니다. NVIDIA cuDNN 어텐션 커널보다 약 20% 빠르다고 보고됩니다.
실무에서 이 커널을 직접 짤 일은 거의 없습니다. vLLM, TensorRT-LLM, SGLang 같은 서빙 프레임워크가 GPU 세대에 맞는 FlashAttention 백엔드를 알아서 붙여줍니다. 우리는 "attention은 memory-bound이고, FlashAttention이 그 메모리 이동을 없애준다"라고 생각하고 알고 있으면 됩니다.
Advanced KV Caching: 이미 계산한 것을 다시 계산하지 않기
문제 정의
Decode 단계에서 매 토큰을 만들 때, 모델은 앞선 모든 토큰의 Key/Value 벡터가 필요합니다. 이걸 매번 다시 계산하면 O(N²)짜리 낭비가 되므로, 한 번 계산한 K/V를 KV 캐시에 저장해두고 재사용합니다. 이것이 KV 캐시의 출발점입니다.
문제는 이 캐시가 어마어마하게 크다는 것입니다. 16비트 정밀도에 128K 컨텍스트라면 요청 하나당 수십 GB가 필요합니다(예를 들어 어떤 구성에서는 요청당 43GB). GPU 메모리에서 모델 가중치를 빼고 나면 가장 큰 소비자가 바로 KV 캐시이고, decode가 memory-bound인 근본 이유도 여기에 있습니다.
여기서 고급 최적화가 두 갈래로 갈립니다. 캐시를 효율적으로 관리하기(단편화 제거)와, 캐시를 요청 간에 재사용하기(prefix caching)입니다.
핵심 아이디어 1: PagedAttention

전통적인 방식은 요청마다 "최대 컨텍스트 길이"만큼 연속된 메모리를 미리 통째로 잡아두었습니다. 실제로는 그만큼 쓰지도 않으면서 말이죠. 프로그램마다 연속된 물리 메모리를 통째로 내주던 옛 운영체제의 비효율과 똑같습니다. 내부 단편화로 메모리가 줄줄 샙니다.
PagedAttention은 운영체제의 가상 메모리 페이징을 그대로 빌려옵니다. KV 캐시를 고정 크기의 블록(페이지) 으로 쪼개고, 물리적으로 흩어진 블록들을 논리적으로 이어 씁니다. 필요할 때 블록 단위로 할당하니 예약 낭비가 사라지고, 같은 프리픽스를 공유하는 요청끼리는 블록을 함께 쓸 수도 있습니다. vLLM이 이 방식으로 유명해졌습니다.
핵심 아이디어 2: Prefix Caching
많은 요청이 앞부분을 똑같이 공유합니다. 같은 시스템 프롬프트, 같은 few-shot 예시, 같은 문서 컨텍스트를 여러 요청이 반복해서 붙이죠. 이 공통 프리픽스의 KV 캐시를 매번 다시 계산하는 건 순전한 낭비입니다.
Prefix caching은 이 정적 프리픽스의 KV 캐시를 저장해두고 재사용해서, prefill을 통째로 건너뛰고 TTFT를 확 낮춥니다. SGLang의 RadixAttention은 여기서 한 발 더 나아가, 프리픽스들을 tree로 관리해 부분적으로만 겹치는 프리픽스까지 효율적으로 공유합니다. 이 아이디어가 RAG와 접목되어 CAG(Cache-Augmented Generation) 가 제안되었습니다. RAG가 매 쿼리마다 관련 문서를 검색해 프롬프트에 넣는다면, CAG는 관련 컨텍스트 대부분을 아예 KV 캐시로 미리 캐싱해두고 재사용합니다. Prefix caching은 사실 CAG의 가장 소박한 형태입니다. 지식을 정적 프리픽스로 두고, 실제 질문만 접미사로 붙이는 것이죠. 기존에는 컨텍스트 윈도우가 작아서 고려되지 않았던 방법이지만 100K~1M 토큰까지 늘어나면서 CAG가 실용적인 선택지로 떠오르고 있습니다.
Tradeoffs
PagedAttention은 메모리 단편화를 없애 같은 GPU로 더 많은 요청을 동시에 처리하게 해줍니다(처리량↑). Prefix caching은 캐시가 적중하면 TTFT를 몇 배씩 줄여줍니다.
단, 이 이득은 캐시 적중률에 전적으로 달려 있습니다. 프리픽스가 매번 다른 워크로드(적중률이 낮은)에서는 캐시를 관리하는 오버헤드만 남고 이득은 없습니다. 게다가 CAG의 비용 계산에는 반전이 있습니다. 캐싱에도 비용이 발생하기 때문에 워크로드 특성에 따라 캐싱하는 컨텍스트가 지나치게 크면 오히려 RAG보다 비싸질 수 있습니다.
KV Cache Offloading
문제정의
Prefix caching과 CAG가 도입되었으나 여전히 VRAM 은 너무 작고 비쌉니다. 장문 컨텍스트 KV 캐시 하나만 담아도 인스턴스가 꽉 차버립니다. 그리고 여러 테넌트나 여러 긴 문서를 캐싱하기 위해 필요한 모델 replica 별로 복제본을 여러 개 띄워야 합니다.
핵심 아이디어

"GPU 메모리가 부족하면, 저장 계층을 아래로 늘려라." 라는 발상의 전환입니다.
KV 캐시를 GPU 메모리에만 가두지 말고, 컴퓨터의 메모리 계층을 그대로 활용해 계층화하는 것입니다. 자주 쓰는 캐시는 GPU에 두고, 덜 쓰는 캐시는 아래 계층으로 내려두었다가, 요청이 오면 필요한 캐시를 위로 끌어올립니다(스와핑).
이미 셀프 서빙 중인 인스턴스라면, 남는 CPU 메모리는 사실상 공짜 공간입니다. 대표적인 프레임워크가 LMCache이고, vLLM·SGLang과 함께 씁니다. 여기에 보조 기법 두 가지가 얹힙니다.
- KV 캐시 압축: 전송·저장할 데이터 자체를 줄입니다. 전통적 양자화나 LMCache의 CacheGen(분포 기반 비트스트림 인코딩)으로 크기를 줄이면, 계층 간 이동이 빨라지고 GPU에도 여유가 생깁니다.
- KV 캐시 블렌딩: RAG의 약점을 메웁니다. RAG 청크는 순서와 조합이 매번 달라 프리픽스 매칭에 실패하고 전체 재계산을 유발합니다. 그렇다고 서로 다른 캐시를 그냥 concat하면, self-attention이 학습한 교차 토큰 관계가 깨져 품질이 나빠집니다. LMCache의 CacheBlend는 전체를 다시 계산하는 대신, 교차 관계를 지키는 데 필요한 일부(기본 15%)만 골라 재계산해 재계산 비용은 피하면서 품질 저하를 최소로 막습니다.
트레이드오프
하지만 LMCache가 만능은 아닙니다. 캐시 적중이 거의 없는 콜드 스타트에서는 KV 캐시를 처리하는 오버헤드 탓에 오히려 일반 vLLM보다 느립니다. 프리픽스가 짧거나 요청이 제각각인 워크로드라면 오프로딩을 안 쓰는 편이 낫습니다. "캐시 재사용이 많을 것으로 예상될 때만" 도입하라는 게 핵심입니다.
접근 2: 디코딩을 빠르게
Part 1이 "메모리를 어떻게 아낄까"였다면, Part 2에서는 디코딩 최적화에 집중합니다
Speculative Decoding
문제정의
Decode의 근본 한계는 자기회귀(autoregressive) 입니다. t+1번째 토큰을 만들려면 t번째 토큰이 먼저 확정돼야 합니다. 그래서 큰 모델은 토큰 하나를 뽑을 때마다 수십~수백 GB의 가중치를 통째로 읽어야 하는데, 정작 계산하는 건 토큰 하나뿐입니다. GPU는 놀고 메모리 대역폭만 빨리는, 전형적인 저활용 memory-bound 상태죠. 따라서 어차피 컴퓨팅 파워를 더 사용할 수 있다면 그 남는 연산으로 여러 토큰을 한꺼번에 검증하면 안 될까? 라는 아이디어에서 착아안합니다.
핵심 아이디어

먼저 값싸고 부정확한 모델로 후보를 수천 개로 좁히고, 그다음 비싸고 정확한 모델로 최종 순위를 매기는 2단계 구조입니다. 대규모 추천·검색 시스템에 많이 횔용되는 패턴인데, speculative decoding은 이걸 토큰 단위로 합니다.
- 작고 빠른 드래프트(draft) 모델이 다음에 올 K개 토큰을 빠르게 추측합니다.
- 크고 정확한 타깃(target) 모델은 이 K개를 하나씩 순차 생성하는 대신, 단 한 번의 forward pass로 병렬 검증합니다.
GPU는 순차 연산보다 병렬 연산에 훨씬 강합니다. 그래서 "토큰 하나씩 K번" 대신 "K개를 한 번에 검증"하는 것만으로 실질적인 속도(특히 ITL)를 얻습니다. 놀고 있던 연산 자원을 검증에 돌리는 것이라, memory-bound 상황에서는 이 여분의 연산이 거의 공짜입니다.
어떻게 동작하나 — 정확도를 훼손하지 않는 검증
가장 중요한 지점은, 이 기법이 정확도를 조금도 떨어뜨리지 않는다는 것입니다. 최종 출력은 추측 디코딩을 안 쓴 일반 자기회귀 생성과 수학적으로 같은 분포에서 나옵니다. 비결은 검증 단계의 rejection sampling입니다.
각 위치에서 타깃 모델은 드래프트의 토큰을 이렇게 처리합니다.
- 타깃이 매긴 확률 ≥ 드래프트가 매긴 확률이면 → 그대로 수락.
- 타깃 확률 < 드래프트 확률이면 →
타깃/드래프트비율만큼 확률적으로 수락하고, 아니면 거부. - 한 토큰이 거부되는 순간, 그 뒤 토큰은 모두 폐기합니다(자기회귀 특성상 앞이 바뀌면 뒤는 무의미하니까요). 대신 타깃이 자신의 보정된 분포에서 새 토큰을 직접 샘플링해 이어갑니다.
예를 들어 "The soccer team of the United" 다음에 드래프트가 "States"를 0.6으로 추측했는데 타깃이 0.8로 본다면, 0.8 ≥ 0.6이니 그대로 수락합니다. 반대로 타깃이 0.4로 본다면 0.4/0.6 확률로만 수락하고, 거부되면 타깃이 직접 다음 토큰을 만듭니다. 이 규칙 덕분에 속도는 얻으면서 품질은 원본과 똑같습니다.
결국 스텝당 수락되는 토큰 개수가 속도를 좌우합니다. 그래서 추측 디코딩은 드래프트와 타깃이 잘 "정렬(align)"되어 수락률이 높을 때 가장 강력합니다.
드래프트를 어떻게 마련하나
드래프트 모델을 얻는 방법은 네 가지이고, 저마다 트레이드오프가 다릅니다.
- 기존 소형 모델: 같은 계열(family)의 작은 모델을 그대로 씁니다(토크나이저·사전학습을 공유해 수락률↑). 구현은 간단하지만 정렬이 안 맞으면 수락률이 떨어집니다. 드래프트는 어차피 타깃이 폴백이니 공격적으로 양자화해도 괜찮습니다.
- 증류(distillation): 타깃으로부터 드래프트를 직접 학습합니다. 수락률은 높아지지만 별도 학습이 필요합니다.
- 셀프 드래프팅(Medusa, EAGLE): 외부 모델 없이 타깃 모델 자체에 경량 예측 헤드나 보조 모듈을 붙여 여러 스텝 앞을 내다봅니다. GPU 메모리를 아끼고 정렬도 우수합니다. Medusa는 여러 헤드로 토큰을 병렬 예측하고, EAGLE은 토큰이 아니라 은닉 상태(hidden state) 를 예측해 더 안정적입니다. 다만 추가 학습·튜닝이 필요합니다.
- N-gram: 프롬프트 안의 반복 패턴을 테이블로 만들어 매칭합니다. 오버헤드가 거의 0이라, JSON·SQL 같은 구조화된 출력이나 템플릿 채우기처럼 반복이 많을 때 강력합니다. 가장 먼저 시도해볼 만한 카드입니다.
핵심 튜닝 변수는 K(드래프트 토큰 개수)입니다. K가 크면 속도 상한이 높아지지만, 수락률이 낮으면 버려지는 토큰만 늘어 오히려 손해입니다. 보통 4~8에서 시작해서 structured output 처럼 예측가능성이 높으면 보다 공격적으로 16 이상을 적용합니다. 위치별 수락률(예: [0.8, 0.7, 0.6, 0.5, 0.10, 0.02])을 보고 이득 없는 뒷부분에서 K를 잘라내는 게 실전 튜닝법입니다.
vLLM 활용
vLLM은 --speculative-config 플래그로 방법(method)과 K를 지정합니다.
# n-gram
vllm serve Qwen/Qwen3-32B \
--speculative-config '{"method":"ngram","num_speculative_tokens":4,
"prompt_lookup_min":2,"prompt_lookup_max":128}'
# EAGLE-3
vllm serve Qwen/Qwen3-32B \
--speculative-config '{"method":"eagle3",
"model":"RedHatAI/Qwen3-32B-speculator.eagle3",
"num_speculative_tokens":3}'
EAGLE 계열은 vLLM·EAGLE 팀 협업으로 꾸준히 발전하고 있어서(2026년 EAGLE 3.1 등), 셀프 드래프팅이 지금 가장 성능 좋은 추측 디코딩 방법 중 하나로 자리 잡았습니다. 다만 최적의 방법과 K는 데이터셋·트래픽 패턴에 크게 좌우되므로, 이론값이 아니라 실제 트래픽으로 수락률을 재보며 튜닝해야 합니다.
접근 3: 스케일 아웃
지금까지는 GPU 한 장(또는 그 안의 커널) 이야기였습니다. 하지만 현대 LLM은 종종 단일 GPU 메모리를 넘어서고, 낮은 지연으로 대규모 서비스하려면 여러 GPU와 노드에 걸친 분산이 필수입니다.
Prefill-Decode Disaggregation: 각각 단계를 다른 GPU로
문제 정의
병렬화(TP/PP)는 "거대한 모델을 여러 GPU에 어떻게 나눠 담을까"만 해결합니다. prefill은 compute-bound, decode는 memory-bound 라는 추론의 근본 특성 에서 오는 비효율은 건드리지 못합니다.
문제는 이 둘이 같은 GPU를 공유할 때 터집니다. 자원 사용 프로파일이 정반대라 서로 간섭(interference) 합니다. 긴 프롬프트의 prefill이 GPU를 붙들고 있는 동안 decode 요청들은 기다리고, 그 반대도 마찬가지입니다. 6장의 청크드 프리필(chunked prefill)로 어느 정도 완화되긴 하지만 근본 해결책은 아닙니다.
핵심 아이디어

성격이 다른 두 일을 물리적으로 다른 GPU 풀에 맡기는 발상입니다.
연산 집약적인 prefill은 Prefill 풀에서, 메모리 대역폭 집약적인 decode는 Decode 풀에서 처리합니다. 그 사이를 잇는 게 KV 캐시 전송입니다. prefill이 만든 KV 캐시를 decode 풀로 넘겨주면, decode가 이어받아 토큰을 생성합니다.
이렇게 나누면 이점이 네 가지 생깁니다.
- TTFT와 ITL을 따로 최적화: 입력이 긴 워크로드는 prefill에 자원을 몰아 TTFT를 줄이고, 출력이 긴 워크로드는 decode에 몰아 ITL을 줄입니다. 간섭이 없으니 ITL도 더 안정적입니다.
- 워크로드별 독립 최적화: prefill은 배치를 키워 텐서 코어를 포화시키고, decode는 배치를 키워 메모리 대역폭 병목을 완화합니다. 병렬화(TP/PP)도 단계별로 다르게 걸 수 있습니다(이종 병렬화).
- 하드웨어 선택 다양화: FLOPS가 같아도 메모리 대역폭이 다른 GPU를 짝지을 수 있습니다. 예컨대 prefill엔 H100, decode엔 대역폭이 큰 H200(또는 저렴한 L40S)을 붙여 비용을 최적화합니다.
- 비대칭 스케일링: prefill은 버스트성이라 공격적 오토스케일링에, decode는 예측 가능해 안정적 스케일링에 어울립니다.
관건은 KV 캐시 전송
PD disaggregation이 효과적이려면 반드시 KV 캐시 전송 오버헤드를 넘어서야 합니다. KV 캐시 크기는 입력 길이에 거의 선형으로 비례합니다. 8B 모델에 1,024 토큰이면 요청당 0.15GB 인데, 입력이 10배면 11.15GB가 되고 1.5GB가 되고, 16 Request Per Sec 상황을 가정하면 초당 약 25GB를 전송해야 합니다. 이 양을 어떤 인터커넥트가 감당하느냐가 배포 설계의 핵심입니다.
| 인터커넥트 | 대역폭 | 25GB/s 감당 |
|---|---|---|
| NVLink (노드 내) | ~900GB/s+ | 여유 있게 |
| InfiniBand (노드 간, RDMA) | ~50–100GB/s | 그럭저럭 |
| PCIe (노드 간, RDMA 없음) | ~10GB/s | 불가 → 병목 |
그래서 prefill과 decode를 같은 노드 안(NVLink)에 두면 안전하지만, 이는 동일 하드웨어를 강요하고 prefill의 8-GPU TP 같은 큰 병렬화를 가로막습니다. 결국 노드 간 배치 + RDMA(InfiniBand) 가 필수 하드웨어 사양이가되고, 그 위에 최적화를 얹습니다. 청크 전송, 비동기·논블로킹(연산과 겹쳐 숨기기), 레이어 단위 전송(끝난 레이어부터 먼저 보내 decode를 일찍 시작), KV 압축 등 다양한 방법을 사용합니다.
Tradeoffs
워크로드 특성과 P/D 노드 비율을 맞게 튜닝해야 합니다. PD 분리는 켜기만 하면 좋아지는 기법이 아닙니다. 어떤 벤치마크에서는, prefill 요청이 많은데 decode 노드 비중이 지나치게 높으면(1p/3d) prefill이 병목이 되어 아예 실패(타임아웃) 했습니다. prefill 노드를 충분히 늘리면(2p/2d, 3p/1d) ITL과 전체 지연은 개선됐지만 TTFT는 오히려 나빠졌고, decode-heavy 워크로드에서는 이득이 거의 없이 지연만 악화됐습니다.
Frameworks
DistServe가 이 아키텍처의 대표 사례이고, 실무에서는 vLLM + llm-d + NVIDIA Dynamo 조합이 널리 쓰입니다. 게이트웨이(Envoy)와 스케줄러가 요청마다 P/D 분리 여부를 판단하고 Prefill/Decode 워커를 골라 라우팅합니다. KV 캐시 전송은 NIXL(NVIDIA Inference Xfer Library)이 표준 전송 계층으로 맡는데, GPU Direct RDMA로 CPU를 거치지 않고 필요한 KV 블록만 골라 당겨오는(zero-copy) 방식입니다. 고급 TTFT/ITL 튜닝이 필요한 대형 모델에만 PD 분리를 활용하고, 소형 모델은 더 단순한 통합 서빙을 유지하는 것이 좋습니다.
Multi-GPU 추론: 하나의 모델을 여러 GPU에
문제 정의
단일 GPU로는 두 개의 벽에 부딪힙니다. 모델이 너무 커서 메모리에 안 들어가거나, 요청이 너무 많아 처리량이 모자라거나. 여기서 짚어둘 사실이 하나 있습니다. 멀티 GPU 환경은 'Compute Only'에서 'Compute + 통신' 환경으로 바뀝니다. GPU를 나눈 대가로 GPU 간 통신이라는 새 비용이 등장하고, 이 통신을 어떻게 설계하느냐가 승부를 가릅니다.

병렬화에는 네 개의 축이 있습니다.
데이터 병렬(DP): 모델을 쪼개지 않고 통째로 복제해, 각 복제본이 트래픽의 일부만 맡습니다. 소프트웨어의 수평 확장과 같은 개념이고, 추론에서는 복제본 간 통신이 전혀 없습니다(0 bytes). 학습과 달리 동기화할 그래디언트가 없기 때문입니다. 처리량과 고가용성을 얻지만, 복제를 늘려도 개별 요청의 지연은 그대로이고 모델이 GPU보다 크면 복제 자체가 불가능합니다. 여기서 라우팅이 중요해지는데, LLM은 요청별 비용 편차가 크고 하드웨어가 비싸서 단순 라운드로빈을 넘어 cache-aware 라우팅이 효과적입니다.
텐서 병렬(TP): 레이어 하나를 여러 GPU에 쪼갭니다. 각 GPU가 가중치의 1/N, 연산의 1/N만 맡죠. 대신 결과를 합치려면 레이어마다 all-reduce 통신이 필요합니다. 트랜스포머 한 레이어(Attention+FFN)당 2번, 80레이어면 토큰당 160번의 동기화가 일어납니다. 통신이 워낙 잦아 NVLink 같은 초고속 노드 내 연결이 필수이고, 그래서 "TP 그룹은 한 노드의 NVLink GPU 8개를 넘기지 말라"는 경험적 원칙이 있습니다.
파이프라인 병렬(PP): 레이어 덩어리를 GPU별로 나눕니다(GPU1: L1–20, GPU2: L21–40, …). 통신은 스테이지 경계에서만 일어나서, 같은 8-GPU/4-스테이지 기준으로 TP가 160회 통신할 때 PP는 단 3회면 됩니다. 통신이 적고 뜸해서 노드 간 연결에서도 잘 굴러갑니다. 대신 고유의 단점이 있습니다. 배치를 하나씩만 흘리면 GPU 대부분이 채워지거나 비워지길 기다리는 파이프라인 버블(유휴)이 생깁니다. 배치를 잘게 쪼개 연달아 흘리는 마이크로배칭을 도입하면 유휴 비율이 (p-1)/(m+p-1) 공식대로 줄어듭니다(마이크로배치 m을 키울수록 버블은 미미해집니다).
전문가 병렬(EP) - MoE 전용. MoE 모델은 토큰마다 전문가의 일부만 활성화합니다. 그 전문가들을 여러 GPU에 흩어놓으면, 토큰을 알맞은 전문가에게 보내는 all-to-all 통신이 MoE 레이어마다 2번(dispatch+combine) 발생합니다. 이 통신이 실제 전문가 연산보다 커져서, 노드를 넘어가면 레이어 시간의 70% 이상을 통신이 잡아먹기도 합니다. DeepSeek의 DeepEP 같은 전용 통신 라이브러리가 이를 완화합니다.
어떻게 고르나 (의사결정)
정리하면 우선순위는 이렇습니다.
- 되도록 GPU 1장으로. GPU 내부 메모리 대역폭이 NVLink보다 훨씬 빠릅니다. 나눌수록 통신 비용이 붙으니, 큰 단일 GPU가 작은 여러 GPU보다 낫습니다.
- 메모리가 부족하면 먼저 양자화. FP8(절반)이나 W4A16(1/4)로 줄여 단일 GPU에 들어가는지부터 봅니다.
- 그래도 안 되면 노드 내 멀티 GPU → TP. NVLink로 묶인 노드 안에서는 TP가 최선입니다(PCIe만 있으면 TP는 너무 느려 부적합).
Multi-Node 추론 — 노드를 넘어
문제 정의
모델이 노드 하나(보통 8-GPU)에도 안 들어가면 여러 노드로 확장해야 합니다. 여기서 핵심은 통신 위계입니다. 노드 내부는 NVLink로 900GB/s 이지만 노드간 연결은 Infiniband 를 도입해서 100GB/s 수준입니다.
핵심 아이디어: 레이어별 위계에 맞춰 병렬화

그래서 멀티 노드의 표준 전략은 하이브리드입니다. 통신이 잦은 것은 빠른 링크에, 뜸한 것은 느린 링크에 짝지어 붙이는 것이죠.
- TP는 노드 내부(NVLink) 에만: 레이어마다 all-reduce가 필요하니 빠른 링크가 아니면 감당이 안 됩니다.
- PP는 노드 간(InfiniBand) 에: 스테이지 경계에서만 point-to-point 한 번이니 느린 링크의 단점의 최소화됩니다. 게다가 이 전송을 연산과 겹쳐(overlap) 숨기면 추가 지연이 사라집니다.
예를 들어 TP=8, PP=2라면, 노드 하나 안의 8개 GPU를 NVLink로 묶어 TP를 돌리고 두 노드를 InfiniBand로 이어 PP를 겁니다. 이렇게 통신 패턴(all-reduce는 NVLink, point-to-point는 IB)을 하드웨어 위계에 정렬시키는 것이 대규모 추론 설계의 뼈대입니다.
NVL72처럼 72개 GPU를 하나의 고속 도메인으로 묶는 하드웨어는, 이 "빠른 도메인"을 키워 느린 노드 간 통신을 최소화하려는 방향입니다. 실제 대규모 시스템은 안쪽에서 TP/PP/EP로 모델을 쪼개 하나의 샤딩 유닛을 만들고, 그 유닛 전체를 DP로 복제하는 계층 구조로 이들을 함께 씁니다. DP가 가장 바깥, 그 안에 나머지가 들어가는 식입니다.
모든 결정에는 tradeoff가 발생합니다. 거부된 draft 토큰, TP의 all-reduce 통신, PD 분리의 KV 전송 대역폭, LMCache의 콜드 스타트 손해까지. 모든 기법이 "핸드오프 비용"을 대가로 치릅니다. 그러니 어느 하나도 만능이 아니고, 어느 하나도 혼자 켠다고 빨라지지 않습니다. 워크로드(입력/출력 비율, 배치 크기, 캐시 재사용률)를 읽고, 여러 계층을 함께 튜닝하며, 이론값이 아니라 내 워크로드에 맞는 실측으로 판단해야 합니다.
'ML-LLM serving' 카테고리의 다른 글
| LLM 서빙 스터디(3) Qwen3.5 모델 T4에서 서빙 최적화 하기 (0) | 2026.08.23 |
|---|---|
| LLM 서빙 스터디 (1) ML/LLM 서빙 개요 (0) | 2026.08.09 |
| Transformer 모델 서빙 메트릭 톺아보기 (0) | 2026.06.14 |
| Kubernetes에서 Apache Airflow 배포 트러블슈팅 (0) | 2025.09.14 |
| [EfficientML.ai] 3. Pruning and Sparsity 1 (2) | 2025.07.10 |