카테고리 없음

LLMSO - W4 - Optimization & Frameworks

푸른바다거북이14 2026. 8. 30. 01:33

1. LLM Optimization

1-1. 요청 배칭 및 스케줄링

배칭과 스케줄링은 "GPU를 놀리지 않으면서, 동시에 개별 요청의 응답 지연도 지키는" 문제입니다.

Static Batching (정적 배칭)

  • 고정된 배치 크기로 요청을 모아 한 번에 처리
  • 한계: 배치 내 가장 긴 시퀀스가 끝날 때까지 짧은 요청들도 대기해야 함 (Head-of-Line Blocking)
  • GPU 활용률이 낮음 — 짧은 응답이 끝난 슬롯이 idle 상태로 남음

Dynamic Batching (동적 배칭)

  • 일정 시간 윈도우 또는 배치 크기 임계값 동안 들어오는 요청을 모음
  • Static보다는 낫지만, 여전히 배치 단위로 생성이 진행되어 짧은 요청이 긴 요청에 발목 잡힘

Continuous Batching (= In-flight Batching, Iteration-level Scheduling)

  • 핵심 아이디어: 배치를 요청 단위가 아니라 디코딩 스텝(iteration) 단위로 관리
  • 각 iteration마다 완료된 시퀀스는 배치에서 제거하고, 대기 중인 새 요청을 즉시 삽입
  • Orca 논문(2022)에서 제안한 개념이 vLLM, TGI, TensorRT-LLM 등 주요 서빙 엔진의 표준이 됨
  • GPU 활용률과 처리량(throughput)을 크게 개선 — vLLM 기준 static batching 대비 수십 배 처리량 향상 보고됨
  • 트레이드오프: 스케줄링 오버헤드 증가, 구현 복잡도 상승 (KV 캐시 관리와 긴밀히 결합)

가로축은 디코딩 스텝(iteration), 세로축은 배치 슬롯입니다. 정적 배칭에서는 한 번 배치가 만들어지면 그 안의 요청이 전부 끝날 때까지 새 요청을 넣을 수 없습니다.

 

위 그림에서 정적 배칭은 28칸 중 12칸이 비어 있습니다. GPU는 그 시간에도 전력을 쓰고 메모리를 붙들고 있지만 아무 토큰도 만들지 않습니다. 이게 Head-of-Line Blocking입니다.

연속 배칭(continuous batching, in-flight batching)이 바꾼 지점은 단 하나입니다. 스케줄링 단위를 "요청"에서 "iteration"으로 내렸다는 것. Orca(OSDI '22)에서 iteration-level scheduling으로 제안됐고, 지금은 vLLM · TensorRT-LLM · TGI · SGLang 모두 이 방식을 씁니다.

 

Prefill-Decode 분리 스케줄링

  • Prefill: prompt 전체를 한 번에 처리 (compute-bound, TTFT에 직결)
  • Decode: 토큰을 하나씩 생성 (memory-bound, TPOT에 직결)
  • 같은 배치에 prefill과 decode가 섞이면 서로 성능 간섭 발생
    • Prefill이 배치에 끼어들면 decode 중인 요청들의 TPOT이 튀는 "generation stall" 발생
  • 대응 전략:
    • Chunked Prefill: 긴 prompt를 여러 청크로 나눠 decode 스텝 사이사이에 끼워 넣음 (vLLM의 --enable-chunked-prefill) → TTFT/TPOT 균형 조절
    • Prefill-Decode Disaggregation: 아예 물리적으로 다른 GPU/노드에서 prefill과 decode를 분리 수행 (예: DistServe, Mooncake, SplitWise) → 두 단계의 서로 다른 리소스 특성(compute vs memory bandwidth)에 맞춰 독립적으로 스케일링 가능

 

구현상 매 스텝마다 스케줄러가 하는 일은 이렇습니다.

  1. 직전 스텝에서 EOS 또는 max_tokens에 도달한 시퀀스를 배치에서 제거하고 KV 블록 반환
  2. 남은 KV 캐시 여유(free block 수)를 계산
  3. waiting 큐에서 들어갈 수 있는 만큼 요청을 꺼내 admit
  4. 새 배치 텐서를 구성해 forward 1회 실행

핵심 전제는 가변 길이 배치를 한 커널로 처리할 수 있어야 한다는 점입니다. 그래서 연속 배칭은 PagedAttention 같은 비연속 KV 캐시 관리, 그리고 varlen 어텐션 커널(FlashAttention의 varlen 계열)과 사실상 한 세트로 움직입니다. 요청마다 시퀀스 길이가 다르므로 패딩 없이 cu_seqlens 오프셋으로 이어붙이는 방식이죠.

다만 여기엔 아직 안 풀린 문제가 남습니다. 배치에 들어오는 연산이 두 종류라는 것입니다.

 

 

Decode는 배치를 키울수록 가중치 로딩 비용을 나눠 쓰므로 효율이 오르지만, prefill은 이미 GPU를 포화시키므로 배치를 키워도 이득이 없습니다. 여기에 prefill 하나가 배치에 들어오면 그 스텝의 실행 시간이 통째로 늘어나, 같이 있던 decode 요청들의 TPOT이 튀는 generation stall이 생깁니다.

Chunked prefill은 이걸 시간축에서 완화합니다. vLLM에서는 --enable-chunked-prefill과 --max-num-batched-tokens로 제어하며, 스케줄러가 매 스텝 "이번 배치의 총 토큰 예산" 안에서 먼저 decode 요청을 채우고 남은 예산만큼만 prefill 토큰을 잘라 넣습니다.

  • max_num_batched_tokens를 작게 → decode 우선, TPOT 안정·TTFT 악화
  • max_num_batched_tokens를 크게 → prefill 덩어리가 커짐, TTFT 개선·TPOT 불안정

이건 순수한 트레이드오프 노브라서, TTFT/TPOT 목표치를 먼저 정하고 스윕해서 잡는 게 맞습니다. 대략 2048~8192 범위에서 워크로드의 평균 prompt 길이에 맞춰 잡는 경우가 많습니다.

그런데 chunked prefill은 여전히 같은 GPU에서 두 연산을 섞습니다. 아예 물리적으로 떼어놓는 게 다음 단계입니다.

 

 

Preemption (선점)

  • GPU 메모리(특히 KV 캐시) 부족 시 우선순위 낮은 요청을 중단
  • 방식: Swapping (CPU 메모리로 KV 캐시 이동) vs Recomputation (중단 후 처음부터 재계산)
  • vLLM은 두 방식 모두 지원, 상황에 따라 선택

선점(preemption) 은 KV 캐시가 부족할 때 발생합니다. running 중인 요청의 시퀀스가 길어지면 블록을 계속 더 요구하는데, 새로 할당할 블록이 없으면 스케줄러는 우선순위가 낮은 요청(vLLM 기본은 가장 늦게 도착한 것)을 밀어냅니다.

  • Swap: KV 캐시를 CPU 메모리로 복사해 두었다가 재개 시 되돌림. PCIe 대역폭에 좌우되며, 시퀀스가 길수록 전송량이 커집니다.
  • Recompute: KV 캐시를 버리고 재개 시 prefill을 다시 실행. 전송은 없지만 연산을 반복합니다.

일반적으로 짧은 시퀀스는 recompute, 긴 시퀀스는 swap이 유리합니다. 선점이 자주 관측된다면 그건 스케줄러 튜닝 문제가 아니라 gpu_memory_utilization / max_num_seqs가 워크로드 대비 과하게 잡혀 있다는 신호에 가깝습니다. 운영 지표로는 vLLM의 num_preemptions_total을 대시보드에 올려두면 좋습니다.

앞의 세 번째 그림이 보여준 Prefill-Decode Disaggregation(DistServe, Splitwise, Mooncake, 그리고 vLLM의 kv_transfer 계열)은 이 모든 간섭을 아예 제거하는 접근입니다. 두 풀을 독립적으로 스케일링할 수 있고 병렬화 전략(TP/PP 차수)도 따로 잡을 수 있습니다. 대신 KV 캐시를 노드 간에 옮겨야 하므로 NVLink/InfiniBand급 인터커넥트가 사실상 전제 조건이고, 라우팅 계층과 장애 도메인이 하나 더 늘어납니다. 2노드 규모에서는 오버헤드가 이득을 잡아먹는 경우가 많아, 보통은 chunked prefill 튜닝으로 충분합니다.

 

 

스케줄링 정책

FCFS (First-Come-First-Served)

  • 가장 단순한 방식, vLLM 기본 스케줄러의 기저 정책
  • 문제: 긴 요청이 먼저 들어오면 뒤의 짧은 요청들의 지연시간(latency)이 증가

SLO-aware / Priority 스케줄링

  • 요청별 SLO(TTFT, TPOT, E2E latency 목표)를 고려해 우선순위 부여
  • 예: 대화형(interactive) 요청은 우선, 배치성(batch) 요청은 후순위
  • Deadline-aware 스케줄링, Shortest-Job-First 근사(출력 길이 예측 기반) 등의 연구도 존재

FCFS가 기본입니다. 단순하고 기아 상태가 없지만, 긴 요청이 앞에 서면 뒤의 짧은 요청까지 대기시간이 늘어납니다.

Priority / SLO-aware 스케줄링은 요청에 등급을 매깁니다. 대화형 트래픽은 앞으로, 배치 요약·임베딩 같은 백그라운드 작업은 뒤로 보내는 식입니다. vLLM은 --scheduling-policy priority로 요청별 priority 필드를 지원합니다. 게이트웨이 계층에서 처리한다면 LiteLLM의 라우팅/우선순위 기능으로 앞단에서 나누는 방법도 있습니다.

SJF 근사는 출력 길이를 예측해 짧은 요청을 먼저 처리하는 연구 계열(예: 길이 예측기 기반 스케줄링)입니다. 평균 지연은 확실히 줄지만 예측이 틀리면 기아가 생기고, 예측기 자체가 추가 비용이라 프로덕션 채택은 아직 제한적입니다.

Prefix-aware 라우팅은 멀티 레플리카 환경에서 중요합니다. 같은 system prompt를 쓰는 요청을 같은 레플리카로 보내면 prefix cache 적중률이 오릅니다. 라운드로빈으로 흩뿌리면 캐시가 매번 미스가 나서, 게이트웨이가 무엇을 기준으로 분배하는지가 서빙 엔진 튜닝만큼 성능에 영향을 줍니다.

 

 

1-2. 어텐션 최적화

FlashAttention — I/O를 줄이는 방향

교과서적인 어텐션 구현은 S = QKᵀ(N×N 행렬)를 만들어 HBM에 쓰고, 다시 읽어 softmax를 적용하고, 또 쓰고 읽어 V와 곱합니다. 시퀀스가 4K만 되어도 헤드당 16M개 원소짜리 중간 행렬이 생깁니다. 실제 병목은 FLOPs가 아니라 이 왕복입니다.

핵심 트릭은 online softmax입니다. Softmax는 원래 분모를 구하려면 행 전체를 봐야 하는데, 실행 중인 최댓값 m과 누적 합 ℓ을 들고 다니면서 새 타일이 올 때마다 이전 결과를 exp(m_old − m_new)로 재스케일하면 행 전체를 한 번에 보지 않아도 정확한 값이 나옵니다. 근사가 아니라 수학적으로 동일한 결과라는 점이 중요합니다 — 정밀도 손실 없이 N×N 행렬 자체를 만들지 않게 됩니다.

메모리 복잡도가 O(N²) → O(N)으로 떨어지면서 긴 컨텍스트가 실용화됐습니다

 

FlashDecoding — decode 단계의 병렬성 확보

FlashAttention은 prefill에 최적화된 물건입니다. Decode에서는 쿼리가 토큰 1개뿐이라 병렬화할 축이 batch × heads밖에 없습니다. 배치가 작으면 H100의 SM 132개 중 극히 일부만 일하고 나머지는 놀게 됩니다.

KV 시퀀스를 여러 청크로 나눠 각 SM이 부분 어텐션을 계산하고, 마지막에 각 청크의 max와 sum을 이용해 보정하며 합칩니다. Online softmax의 재스케일 규칙을 병렬 축에 그대로 쓰는 셈이라 결과는 여전히 정확합니다. 배치가 작고 컨텍스트가 긴 상황(챗봇 단건 요청, 긴 RAG 프롬프트)에서 특히 효과가 큽니다. 

PagedAttention — 메모리 단편화를 없애는 커널

기존 구현은 시퀀스마다 KV 캐시를 연속된 메모리에 잡아야 했습니다. 최대 길이 기준으로 미리 할당하다 보니 실제 사용량과의 차이가 전부 낭비였고, vLLM 논문은 이 낭비가 60~80%에 달한다고 보고했습니다. PagedAttention은 OS의 가상 메모리 페이징을 그대로 가져옵니다.

 

PagedAttention — 메모리 단편화를 없애는 커널

기존 구현은 시퀀스마다 KV 캐시를 연속된 메모리에 잡아야 했습니다. 최대 길이 기준으로 미리 할당하다 보니 실제 사용량과의 차이가 전부 낭비였고, vLLM 논문은 이 낭비가 60~80%에 달한다고 보고했습니다. PagedAttention은 OS의 가상 메모리 페이징을 그대로 가져옵니다.

 

시퀀스는 자기 KV가 연속이라고 믿지만, 커널은 블록 테이블을 참조해 흩어진 물리 블록을 모읍니다. 얻는 것이 세 가지입니다.

  • 단편화 제거: 낭비가 마지막 블록의 내부 파편(보통 블록 크기 16토큰 이하)으로 한정됩니다. 같은 GPU에서 동시에 처리할 수 있는 시퀀스 수가 늘고, 이게 곧 배치 크기 상한이 되므로 처리량으로 직결됩니다.
  • Copy-on-write 공유: 같은 프롬프트에서 n>1로 여러 후보를 샘플링하거나 beam search를 할 때, 공통 접두부 블록을 물리적으로 공유하고 갈라지는 지점부터만 복사합니다.
  • Prefix caching의 기반: 블록 단위 해시로 이전 요청의 접두부 블록을 재사용할 수 있습니다. vLLM의 --enable-prefix-caching이 이걸 씁니다. 긴 system prompt를 공유하는 서비스라면 prefill 연산 자체가 통째로 사라지므로 TTFT 개선폭이 가장 큰 옵션입니다.

단점도 있습니다. 커널이 매번 블록 테이블을 간접 참조하므로 순수 연속 메모리 대비 약간의 오버헤드가 있고, 커스텀 커널이 필요해서 새로운 어텐션 변형이 나올 때마다 별도 구현이 뒤따라야 합니다.

 

KV 헤드 줄이기 — MHA → GQA → MQA → MLA

앞의 기법들이 "어떻게 계산할까"였다면, 이건 "무엇을 저장할까"를 바꿉니다. KV 캐시 크기는 다음 식을 따릅니다.

 
KV 캐시 = 2 × layers × kv_heads × head_dim × dtype_bytes × seq_len × batch

여기서 kv_heads만 줄이면 나머지는 그대로 두고 캐시를 선형으로 줄일 수 있습니다.

 

Decode 단계에서 GPU가 매 토큰마다 읽어야 하는 것은 모델 가중치와 KV 캐시입니다. GQA로 kv_heads를 8분의 1로 줄이면 읽을 바이트 수가 그만큼 줄어들어 TPOT이 직접 개선됩니다. 배치를 키울 여유도 함께 생기므로 처리량과 지연이 동시에 좋아지는 드문 경우입니다.

  • MQA (Shazeer, 2019): KV 헤드 1개. 캐시는 최소지만 품질 저하와 학습 불안정이 보고됨
  • GQA (Ainslie 등, 2023): 그룹 단위 공유. 현재 사실상 표준 — Llama 3, Mistral, Qwen 등 대부분이 KV 헤드 8개 안팎을 씁니다
  • MLA (DeepSeek-V2): KV를 저차원 잠재 벡터로 압축해 저장하고, 어텐션 계산 시 복원. 캐시는 GQA보다 더 줄이면서 품질은 MHA에 가깝게 유지하는 것을 목표로 함. 다만 전용 커널이 필요해 서빙 엔진 지원 여부를 먼저 확인해야 합니다

TP 설정과 직접 얽히는 지점이 하나 있습니다. KV 헤드는 TP 랭크에 나눠 배치되는데, TP degree > kv_heads가 되면 KV 헤드를 랭크마다 복제해야 합니다. KV 헤드 8개짜리 모델을 TP=8로 돌리면 랭크당 1개로 깔끔하지만, TP=16이면 캐시 총량이 기대만큼 줄지 않습니다. 2 GPU 환경에서 TP=2를 쓴다면 이 문제는 없지만, 모델을 바꿀 때 num_key_value_heads를 확인해 두면 좋습니다.

 

1-3. 모델 압축

GPU VRAM에 모델의 가중치가 차지하는 비율이 크다면 모델의 가중치 크기를 줄일 필요가 있다.

언제 양자화하는가

  • PTQ (Post-Training Quantization): 학습 완료된 모델을 소량의 캘리브레이션 데이터로 변환. 수십 분~수 시간. 서빙에서 쓰는 건 거의 전부 이쪽입니다.
  • QAT (Quantization-Aware Training): 학습 과정에 양자화 오차를 반영. 품질은 좋지만 재학습 비용이 커서 모델 제공자 몫입니다.

Weight-only(W4A16) 는 가중치를 4비트로 저장했다가 연산 직전에 FP16으로 복원합니다. HBM에서 읽는 바이트가 줄어드니 memory-bound인 decode가 빨라집니다. 반대로 prefill은 이미 compute-bound라 개선이 없고, 오히려 역양자화(dequant) 오버헤드로 느려질 수 있습니다.

W8A8 / FP8 은 활성값까지 8비트로 낮춰 Tensor Core의 저정밀 경로를 씁니다. 연산량 자체가 줄어드니 prefill도 빨라지고, TTFT 개선 가능합니다.

가지치기와 희소성

가중치 일부를 0으로 만들어 저장·연산량을 줄이는 접근입니다. 서빙 관점에서는 양자화보다 실효성이 떨어집니다.

  • 비구조적 가지치기: 개별 가중치를 임의로 0으로. 압축률은 높지만 GPU가 불규칙한 0을 건너뛰지 못해 실제 속도 이득이 거의 없습니다.
  • 구조적 가지치기: 헤드·채널·레이어 단위로 통째로 제거. 속도 이득은 확실하지만 품질 손실이 크고 보통 회복 학습이 필요합니다.
  • 2:4 반구조적 희소성: 연속된 가중치 4개 중 2개를 0으로 강제. Ampere 이후 Sparse Tensor Core가 이 패턴을 하드웨어로 지원합니다. 이론상 2배지만 실제로는 1.2~1.5배 수준이고, 여기에도 품질 손실이 따릅니다.

증류

큰 교사 모델의 출력 분포를 작은 학생 모델이 모방하도록 학습시키는 방식입니다.

1-4. 프리픽스 캐싱

프리픽스 캐싱은 "이미 계산한 걸 다시 계산하지 말자" 입니다. 앞서 다룬 PagedAttention의 블록 구조가 그대로 기반이 됩니다

 

동작 방식은 단순합니다. 각 KV 블록에 대해 hash(이전 블록들의 해시 + 이 블록의 토큰 ID들)를 계산해 두고, 새 요청이 오면 앞에서부터 블록 해시를 조회합니다. 일치하는 동안은 이미 계산된 물리 블록을 그대로 참조하고, 처음 어긋나는 지점부터만 prefill을 수행합니다.

해시에 이전 블록들의 해시를 포함하는 게 핵심입니다. 어텐션은 인과적이라 블록의 KV 값은 그 앞에 무엇이 왔는지에 의존하기 때문입니다. 같은 토큰 묶음이라도 앞부분이 다르면 KV가 다릅니다.

vLLM에서는 --enable-prefix-caching으로 켜며(최근 버전은 기본 활성), SGLang은 RadixAttention이라는 이름으로 접두사 트리(radix tree)를 써서 같은 일을 합니다. 트리 구조라 여러 갈래로 분기하는 프롬프트(에이전트의 병렬 탐색, few-shot 변형)에서 공유 지점을 더 정교하게 잡습니다.

2. Advanced LLM Optimization

2-1. Speculative Decoding 추측적 디코딩

메모리를 아끼거나 계산을 재사용하는 게 아니라, 놀고 있는 연산 자원을 투기적으로 써서 지연을 줄이는 방식입니다.

디코딩이 memory-bound라는 사실을 뒤집어 보면 이렇습니다. 배치 1로 토큰 하나를 만들 때 GPU는 모델 가중치 전체를 HBM에서 읽지만, 실제 연산기는 거의 놀고 있습니다. 산술 강도가 극도로 낮은 상태죠.

여기서 중요한 관찰이 나옵니다. 토큰 1개를 forward 하는 시간과 토큰 5개를 한 번에 forward 하는 시간이 거의 같습니다. 가중치를 읽는 비용이 지배적이고, 토큰 5개분의 추가 연산은 놀던 연산기가 흡수해 버리기 때문입니다.

앞으로 나올 토큰 몇 개를 싸게 추측한 뒤, 큰 모델로 한 번에 검증하는 방식입니다. 

"작은 모델이 만든 토큰을 쓴다"고 하면 품질이 떨어질 것 같지만, 그렇지 않습니다. 검증 단계의 알고리즘이 출력 분포가 타깃 모델과 동일하도록 설계되어 있습니다. 추측이 맞으면 여러 토큰을 한 스텝에 확정하고, 틀리면 그 지점부터 버리면 됩니다.

 

핵심은 거부 샘플링(rejection sampling) 입니다. 타깃 분포를 p, 드래프트 분포를 q라 할 때, 드래프트가 제안한 토큰 x를 다음 확률로 수용합니다.

accept  with probability  min(1, p(x) / q(x))

거부되면 보정 분포 norm(max(0, p − q))에서 새 토큰을 뽑습니다. 이 두 단계를 합치면 최종 분포가 정확히 p가 된다는 것이 증명되어 있습니다 (Leviathan 등 2023, Chen 등 2023). 드래프트 모델이 아무리 나빠도 품질은 타깃 모델과 동일하고, 나쁜 드래프트는 수용률만 떨어뜨려 속도 이득을 잃게 할 뿐입니다.

거부되면 그 이후 제안 토큰은 전부 버립니다. 어텐션이 인과적이라 앞 토큰이 바뀌면 뒤 토큰의 전제가 무너지기 때문입니다. 그래서 검증 스텝당 확정 토큰 수는 1 ~ k+1 사이가 됩니다. 거부되더라도 재샘플링으로 최소 1토큰은 확정되므로, 표준 디코딩보다 느려지는 최악의 경우는 없습니다(드래프트 비용을 무시하면).

탐욕적 디코딩(temperature=0)에서는 더 단순합니다. 드래프트 토큰이 타깃의 argmax와 일치하면 수용, 아니면 거부. 이 경우 출력이 표준 탐욕적 디코딩과 토큰 단위로 완전히 동일합니다.

 

 

드래프트를 만드는 네 가지 방식

 

 

별도 드래프트 모델 — 가장 원형에 가까운 방식입니다. 70B 타깃에 1B 드래프트를 붙이는 식으로, 같은 계열·같은 토크나이저여야 합니다. 장점은 개념이 단순하고 어떤 모델에나 붙는다는 것, 단점은 GPU 메모리를 추가로 먹고 별도 모델을 관리해야 한다는 것입니다. 드래프트가 타깃 대비 충분히 작지 않으면(경험적으로 1/10 이하) 이득이 사라집니다.

Medusa — 타깃 모델 위에 예측 헤드를 여러 개 붙여, 다음 토큰뿐 아니라 그 다음, 또 그 다음 위치까지 동시에 예측합니다. 별도 모델이 필요 없고 헤드만 추가 학습하면 됩니다. 여러 후보를 트리로 만들어 트리 어텐션으로 한 번에 검증하는 구조도 함께 씁니다. 다만 통상적인 구현은 엄밀한 거부 샘플링 대신 완화된 수용 기준을 쓰므로, 출력 분포가 타깃과 정확히 같지는 않을 수 있습니다.

EAGLE — 토큰이 아니라 마지막 은닉층의 특징 벡터 수준에서 자기회귀를 수행합니다. 토큰 샘플링의 불확실성을 우회하므로 수용률이 상당히 높게 보고됩니다. EAGLE-2는 문맥에 따라 드래프트 트리를 동적으로 조정하고, EAGLE-3는 여러 계층의 특징을 함께 씁니다. 현재 실용적인 선택지 중 가장 성능이 좋은 편이지만, 모델별로 드래프트 헤드를 학습해야 하고 서빙 엔진의 지원 여부를 확인해야 합니다.

N-gram / 프롬프트 조회 — 학습이 전혀 필요 없습니다. 최근 생성한 토큰 패턴을 프롬프트나 이전 출력에서 찾아 그 다음에 나왔던 토큰들을 그대로 제안합니다. 출력이 입력을 많이 복사하는 작업에서 놀라울 만큼 잘 듭니다.

  • 문서 요약, 인용, 번역
  • 코드 편집 — 파일 전체를 주고 일부만 고쳐 출력하게 하는 경우
  • 구조화된 출력 — 정해진 JSON 스키마 키를 반복 생성할 때

거꾸로 자유 생성 작업에서는 수용률이 거의 0에 가깝습니다. 도입 비용이 사실상 없으니, 가장 먼저 시도해 볼 카드입니다.

 

2-2. 멀티 GPU / 멀티 노드 서빙

 

  • 용량: 모델이 GPU 한 장에 안 들어감 (70B FP16 = 약 140GB)
  • 지연: 들어가긴 하는데 더 빠르게 만들고 싶음
  • 처리량: 레플리카를 늘려 더 많은 요청을 받고 싶음

Tensor Parallelism (TP) 은 각 행렬 곱을 쪼갭니다. 어텐션은 헤드 단위로, MLP는 중간 차원 기준으로 나누고, 각 GPU가 부분 결과를 계산한 뒤 합칩니다. 트랜스포머 레이어 하나당 all-reduce가 두 번 발생합니다 — 어텐션 출력 프로젝션 뒤, MLP 다운 프로젝션 뒤.

80레이어 모델이면 토큰 하나 만드는 데 all-reduce가 160번입니다. 그래서 TP는 대역폭보다 지연에 민감하고, 노드 경계를 넘으면 급격히 나빠집니다.

  • num_attention_heads가 TP 크기로 나누어떨어져야 합니다
  • num_key_value_heads도 마찬가지입니다. 나누어떨어지지 않으면 KV 헤드를 랭크마다 복제해야 해서, GQA로 아낀 KV 캐시 이득이 사라집니다. KV 헤드 8개짜리 모델에 TP=16을 주면 이 문제가 생깁니다

Pipeline Parallelism (PP) 은 레이어를 스테이지로 나눠 GPU에 배치합니다. 스테이지 경계에서 활성값 텐서 하나만 point-to-point로 넘기면 되므로 통신량이 TP보다 훨씬 적고, 노드 간 링크로도 견딜 만합니다.

대신 버블이 생깁니다. 스테이지 0이 일하는 동안 스테이지 1은 놀아야 하니까요. 학습에서는 마이크로배치를 흘려 버블을 메우는데, 서빙에서는 앞서 다룬 연속 배칭이 그 역할을 합니다. 여러 배치를 파이프라인에 겹쳐 흘리면 버블이 상당히 채워집니다. 그래도 동시성이 낮으면 채울 게 없어서 버블이 그대로 드러납니다.

 

 

 

 

  • NVLink가 없는 노드에서 TP를 크게 잡지 마세요. PCIe만으로 TP=8을 돌리면 all-reduce가 병목이 되어 GPU를 늘린 만큼 안 빨라집니다
  • 노드를 넘어야 한다면 그 경계는 PP로 잡습니다. TP = 노드당 GPU 수, PP = 노드 수가 기본 형태입니다
  • 이더넷만 있는 환경이라면 애초에 모델을 쪼개지 말고, 노드당 하나씩 온전한 레플리카를 올리는 편이 거의 항상 낫습니다

Data Parallelism (DP) — 완전한 모델 복제본을 여러 개 두고 요청을 분배합니다. 가장 단순하고 확장성이 좋으며, 통신이 없습니다. 지연이 아니라 처리량이 목표라면 이게 정답인 경우가 많습니다. 다만 MoE 모델에서는 조금 다르게 쓰여서, 어텐션 레이어는 DP로 두고 전문가 레이어는 EP로 두는 조합이 있습니다.

Expert Parallelism (EP) — MoE 모델에서 전문가들을 GPU에 나눠 배치합니다. 토큰을 담당 전문가가 있는 GPU로 보내고 받아오는 all-to-all 통신이 발생하며, 이게 MoE 서빙의 주 병목입니다. 전문가별 부하 불균형(특정 전문가로 토큰이 몰림)도 함께 다뤄야 합니다.

Context / Sequence Parallelism — 매우 긴 컨텍스트의 prefill을 시퀀스 축으로 나눠 여러 GPU가 분담합니다. Ring Attention 계열이 여기 해당합니다. 128K 이상을 다루지 않는다면 당장 필요하진 않습니다.

 

 

2-3. 프리필-디코드 분리(PD Disaggregation)

같은 GPU에서 prefill과 decode를 섞으면 chunked prefill로 완화는 되지만 간섭 자체는 남습니다. 물리적으로 나누면 세 가지가 열립니다.

  • 독립 스케일링: 입력이 길고 출력이 짧은 워크로드(문서 요약, 분류)는 prefill 노드가 더 필요하고, 반대(창작, 긴 답변)는 decode 노드가 더 필요합니다. 비율을 트래픽에 맞춰 따로 조정할 수 있습니다
  • 구성 최적화: prefill 풀은 compute-bound이므로 TP를 크게 잡아 연산을 분산하는 게 유리하고, decode 풀은 KV 캐시 용량과 배치가 중요하므로 다른 TP 차수나 다른 GPU 등급이 나을 수 있습니다
  • SLO 분리: TTFT 튜닝이 TPOT을 망치지 않습니다. 두 목표를 독립적으로 잡을 수 있는 게 실무적으로 가장 큰 이점입니다

분리의 성패는 KV 캐시를 얼마나 싸게 옮기느냐로 결정됩니다. 크기부터 계산해 봅시다.

 
토큰당 KV = 2 × layers × kv_heads × head_dim × dtype_bytes

80레이어, KV 헤드 8개, head_dim 128, FP16이면 토큰당 약 320KB입니다. 8K 프롬프트면 약 2.6GB. 400Gb/s급 링크(약 50GB/s)로 50ms 남짓 걸립니다. 같은 구간의 prefill이 수백 ms라면 감당할 만하지만, 짧은 프롬프트에서는 전송 오버헤드가 이득을 먹습니다.

그래서 나온 게 레이어별 전송 파이프라이닝입니다.

 

 

레이어 0의 KV는 레이어 1을 계산하는 동안 이미 보낼 수 있습니다. 레이어별로 흘려보내면 마지막 레이어 전송분만 노출되고 나머지는 연산 뒤에 숨습니다. 여기에 GPU-to-GPU 직접 전송(RDMA, GPUDirect)을 쓰면 CPU 메모리 경유 복사를 없앨 수 있습니다.

vLLM은 KV 커넥터 인터페이스를 두어 이 전송 계층을 교체할 수 있게 했고, NIXL이나 LMCache 같은 백엔드가 붙습니다. NVIDIA Dynamo, Mooncake처럼 분리 서빙 전체를 오케스트레이션하는 프레임워크도 이 위에 얹힙니다.

 

Prefill과 decode 노드를 몇 대 몇으로 둘지는 입력 길이와 출력 길이의 비율이 결정합니다.

  • 입력 4000 · 출력 100 (요약, 추출) → prefill 부하가 압도적, prefill 노드를 두텁게
  • 입력 200 · 출력 2000 (창작, 에이전트 추론) → decode 노드를 두텁게

문제는 이 비율이 시간대와 기능별로 변한다는 점입니다. 고정 비율로 두면 한쪽이 놀고 한쪽이 밀립니다. 그래서 실제로는 두 풀의 큐 길이를 각각 관측하고 독립적으로 오토스케일하되, 어느 쪽이 병목인지 판단하는 지표를 미리 정해 두는 게 중요합니다. Prefill 큐 대기 시간은 TTFT로, decode 배치 점유율은 TPOT으로 드러납니다.

 

2-4. 고급 KV 캐싱 기법

PagedAttention과 프리픽스 캐싱은 손실 없는 기법이었습니다. 여기서 다룰 것들은 대부분 품질과 메모리를 맞바꾸므로, 검증 없이 켜면 안 됩니다.

 

FP8 KV는 앞서 다뤘고, 더 밀면 INT4/INT2까지 갑니다. 여기서 알아둘 게 하나 있습니다. Key와 Value는 분포가 다릅니다.

Key 캐시는 특정 채널에 이상치가 몰리는 경향이 있어 채널별(per-channel) 스케일이 유리하고, Value 캐시는 그런 구조가 약해 토큰별(per-token) 스케일이 낫습니다. KIVI 같은 연구가 이 비대칭을 이용해 2비트까지 내리면서 품질을 유지합니다. 실무에서 쓸 때는 어떤 그래뉼래리티로 스케일을 잡는 구현인지 확인해야 하고, 그룹 크기가 클수록 메타데이터는 줄지만 품질이 떨어집니다.

 

컨텍스트가 아무리 길어도 실제로 어텐션이 몰리는 토큰은 소수라는 관찰에서 출발합니다.

 

 

  • StreamingLLM / Attention Sink: 맨 앞 몇 개 토큰 + 최근 윈도우만 유지. 규칙이 고정이라 구현이 단순하고 예측 가능합니다
  • H2O (Heavy Hitter Oracle): 누적 어텐션 점수가 높은 토큰을 남기고 나머지를 축출. 동적이라 성능은 낫지만 점수 추적 비용이 붙습니다
  • SnapKV: prefill 단계에서 프롬프트 끝부분(관측 윈도우)의 어텐션 패턴을 보고 중요한 프롬프트 토큰만 골라 남깁니다. 긴 프롬프트 + 짧은 출력 워크로드에 잘 맞습니다
  • PyramidKV: 레이어마다 예산을 다르게 배분합니다. 하위 레이어는 어텐션이 넓게 퍼지고 상위 레이어는 소수에 집중되는 경향을 이용합니다

위험을 분명히 해두면, 이 기법들은 요약이나 QA에서는 잘 버티지만 긴 문서에서 특정 정보를 찾는 작업(needle-in-a-haystack)에서 가장 먼저 깨집니다. RAG처럼 검색된 문맥의 특정 부분을 정확히 인용해야 하는 서비스에서는 특히 조심스럽게 접근해야 합니다.

 

구조를 바꾸는 방향

모델 아키텍처 차원에서 KV 자체를 줄이는 접근입니다. 서빙 팀이 선택할 수 있는 건 "이런 모델을 고를지"뿐이지만, 후보 모델을 비교할 때 알아두면 유용합니다.

  • GQA / MQA: 헤드 간 공유 (앞서 다룸)
  • MLA: 저차원 잠재 표현으로 압축 (앞서 다룸)
  • CLA (Cross-Layer Attention): 인접 레이어끼리 KV를 공유합니다. 레이어 수만큼 곱해지던 캐시가 절반 이하로 줄어듭니다
  • 하이브리드 아키텍처: 일부 레이어만 전역 어텐션을 쓰고 나머지는 슬라이딩 윈도우를 쓰는 구성. 최근 모델들이 자주 채택하는 방식이며, 긴 컨텍스트에서 KV가 선형으로 안 늘어납니다

공유를 넓히는 방향

프리픽스 캐싱을 한 단계 더 밀면 두 갈래가 나옵니다.

공유 캐시 풀 — 여러 레플리카가 하나의 KV 저장소를 바라보게 합니다. LMCache나 Mooncake Store 계열이 이걸 합니다. 앞서 다룬 프리픽스 인식 라우팅의 한계(부하 불균형)를 우회할 수 있다는 게 장점입니다. 어느 레플리카로 가든 캐시를 찾을 수 있으니까요. 대신 조회 지연과 네트워크 전송이 추가되므로, 프리픽스가 충분히 길어야 수지가 맞습니다.

캐시 블렌딩 — RAG의 고유한 문제를 다룹니다. 검색된 청크 A, B, C를 조합해 프롬프트를 만들 때, 조합이 매번 달라서 프리픽스 캐싱이 거의 안 먹습니다. 각 청크의 KV를 따로 계산해 두고 이어 붙이면 되지 않을까 싶지만, 어텐션은 인과적이라 앞 문맥이 다르면 KV가 틀립니다. CacheBlend 같은 접근은 오차가 큰 소수의 토큰만 선택적으로 재계산해서 이 간극을 메웁니다. 아직 연구 단계에 가깝지만, RAG 중심 서비스라면 지켜볼 만한 방향입니다.