[CloudNeta] Hands-On LLM Serving 3주차 part 2 - 필수 LLM 최적화 기법

연재 안내

이 글은 3주차 연재의 두 번째 편입니다.

  1. part 1 - LLM 서빙의 최적화 (이 글)
  2. part 2 - 필수 LLM 최적화 기법

들어가며

6장에서는 필수 LLM 최적화 기법에 대해 살펴보고, 각 기법이 어떤 의미를 가지는지 살펴봅니다. 또한 KodeKloud의 vLLM을 이용한 LLM 추론 구동을 통해 어떤 식으로 문제를 해결하는지 직접 쫓아가봅니다.

필수 LLM 최적화 기법

필수 LLM 최적화 기법은 아래와 같습니다:

각 최적화 기법이 어떤 병목을 완화하는지 연결하면 다음과 같습니다.

그림 1 - 낮은 GPU 활용률과 KV 캐시·HBM 데이터 이동·모델 크기·반복 프롬프트 계산에 대응하는 LLM 서빙 최적화 기법

Batching과 스케줄링 레벨의 최적화

LLM 온라인 서빙의 목표는 지연시간 SLA를 지키면서 GPU 활용률과 처리량을 높이는 것입니다. 이를 위해 서로 다른 계층의 최적화 기법을 함께 적용합니다.

온라인 서빙에서 배칭은 처리량을 높이는 핵심 수단이지만 모든 환경에 필수적인 것은 아닙니다. 트래픽이 적거나 지연시간을 우선하는 서비스에서는 작은 배치를 사용하거나 요청을 개별적으로 처리할 수도 있습니다.

오프라인 서빙과 실시간 온라인 서빙

2장에서는 LLM 서빙을 오프라인 서빙과 실시간 온라인 서빙으로 구분했습니다.

여러 요청을 배치로 묶으면 GPU가 한 번에 수행할 작업이 늘어나 전체 처리량을 높일 수 있습니다. 다만 배치를 구성하는 대기시간과 한 번의 연산량도 증가하므로 개별 요청의 지연시간은 길어질 수 있습니다. 실시간 서빙에서는 처리량과 지연시간 사이의 균형이 중요합니다.

실시간 서빙에 배칭이 필요한 이유

LLM 추론은 Prefill과 Decode에서 서로 다른 형태의 행렬곱을 수행합니다.

배치 크기가 3이라면 한 번의 Decode 반복에서 각 요청의 새 토큰을 하나씩, 모두 세 개 생성할 수 있습니다. 요청별 생성 과정은 여전히 자기회귀적으로 진행되지만, 같은 반복에 포함된 요청은 GPU에서 함께 계산됩니다.

그림 2 - Prefill은 프롬프트 토큰을 함께 처리하고 Decode 배칭은 여러 요청에서 새 토큰 하나씩을 같은 반복에서 생성한다

배칭과 산술 강도

5장에서 살펴본 선형 레이어의 행렬곱에서는 이 한 번에 처리하는 토큰 수를 나타냅니다. Decode에서는 활성 요청마다 새 토큰 하나를 처리하므로 은 배치 크기 와 같습니다.

배치 크기가 커지면 하나의 행렬 연산에서 같은 모델 가중치를 여러 요청의 토큰 계산에 재사용할 수 있습니다. GPU 메모리에서 가져온 데이터당 수행하는 연산이 늘어나므로 산술 강도와 GPU 연산 장치 활용률도 높아집니다.

그림 4 - 배치를 키우면 한 번 읽은 가중치를 여러 토큰 계산에 재사용해 바이트당 유효 연산량이 증가한다

그림 4의 수치는 배칭과 산술 강도의 관계를 설명하기 위한 예시입니다. 실제 데이터 이동량과 달성 가능한 성능은 모델 구조와 정밀도, 배치 크기, 커널 구현과 하드웨어에 따라 달라집니다. 관련 과정은 배칭과 산술 강도 설명 영상에서도 확인할 수 있습니다.

Prefill과 Decode에서의 효과

배칭의 핵심은 모델 가중치를 읽는 비용을 여러 요청의 연산에 분산하는 것입니다. 특히 Decode 처리량을 높이는 데 효과적이지만, 배치 대기시간과 KV 캐시 사용량도 함께 증가합니다. 이후에는 이 트레이드오프를 다루는 동적 배칭과 스케줄링 기법을 살펴봅니다.

온라인 추론에서의 동적 배칭

온라인 서비스에서는 요청의 도착 시점을 미리 알 수 없습니다. 최대 배치 크기가 찰 때까지 계속 기다리면 먼저 도착한 요청의 대기시간이 길어집니다. 이 방식은 요청을 미리 확보할 수 있는 오프라인 서빙에는 적합하지만 지연시간이 중요한 온라인 서빙에는 사용하기 어렵습니다.

동적 배칭(dynamic batching)은 다음 두 조건 중 하나를 만족하면 현재까지 모인 요청을 모델에 전달합니다.

flowchart TD
    requestQueue["Request Queue"] --> maxBatch{"Max Batch Size 도달?"}
    maxBatch -->|예| gpuInference["GPU Inference"]
    maxBatch -->|아니요| maxDelay{"Max Delay 도달?"}
    maxDelay -->|예| gpuInference
    maxDelay -->|아니요| waitMore["조금 더 기다림"]
    waitMore --> maxBatch

최대 배치 크기나 최대 대기시간에 도달하면 현재까지 모인 요청을 GPU로 전달합니다.

10인승 운송 수단에 비유하면 승객 10명이 모였을 때 바로 출발하고, 10명이 모이지 않아도 정해진 대기시간이 지나면 현재 인원으로 출발하는 방식입니다. 요청이 하나뿐이어도 최대 대기시간을 넘기지 않으므로 지연시간의 상한을 관리할 수 있습니다.

NVIDIA Triton도 최대 배치 크기와 max_queue_delay_microseconds를 이용해 같은 방식으로 동적 배치를 구성합니다. 자세한 동작은 Triton Dynamic Batcher 문서에서 확인할 수 있습니다.

설정 값을 높일 때 값을 낮출 때
최대 배치 크기 처리량이 높아질 수 있지만 한 번의 처리시간과 메모리 사용량이 증가합니다. 너무 크면 OOM 위험이 커집니다. 요청당 지연시간은 줄어들 수 있지만 GPU 병렬성을 충분히 사용하지 못할 수 있습니다.
최대 대기시간 더 큰 배치를 만들기 쉬워지지만 대기열 지연시간이 증가합니다. 요청을 빨리 처리하지만 실제 배치 크기가 작아져 배칭 효과가 줄어듭니다.

튜닝 목표는 지연시간 SLA를 지키면서 실제 배치 크기를 가능한 한 크게 유지하는 것입니다. 평균 배치 크기와 대기열 지연시간, GPU 메모리 사용량을 함께 측정해야 합니다.

전통적인 동적 배칭은 한 번 구성한 배치를 일정한 작업 단위로 처리합니다. LLM은 요청마다 입력과 출력 길이가 달라 먼저 끝난 요청의 자리가 비어도 긴 요청은 계속 실행될 수 있습니다. 결과를 먼저 반환할 수 있는 구현도 있지만, 고정된 배치가 빈 슬롯을 새 요청으로 채우지 못하면 GPU 활용률은 점차 낮아집니다.

그림 6 - 고정된 배치에서는 짧은 요청이 끝난 자리가 가장 긴 요청의 완료 시점까지 유휴 상태로 남습니다

LLM 온라인 추론을 위한 연속 배칭

연속 배칭(continuous batching)은 inflight batching 또는 iteration-level batching이라고도 합니다. 스케줄러는 각 모델 실행 iteration이 끝날 때 활성 배치를 다시 구성합니다.

  1. 완료된 요청을 활성 배치에서 제거합니다.
  2. 비어 있는 슬롯에 대기열의 요청을 추가합니다.
  3. 남은 요청과 새 요청을 다음 iteration에서 함께 처리합니다.

새 요청은 iteration 경계에서 합류합니다. 실행 중인 GPU 커널에는 중간에 추가되지 않습니다. 짧은 요청이 끝난 자리를 바로 재사용하므로 가장 긴 요청이 끝날 때까지 빈 슬롯을 유지하는 문제를 줄일 수 있습니다. 요청 1이 끝나면 요청 4를 넣고, 요청 2가 끝나면 요청 5를 넣는 식으로 활성 배치의 크기를 유지합니다.

그림 7 - 연속 배칭은 완료된 요청을 제거하고 대기 요청을 넣어 활성 슬롯을 계속 활용합니다

연속 배칭에서는 고정된 시간 창으로 실행 배치를 유지하지 않습니다. 대기열 관리나 입장 제어를 위한 지연 설정은 구현에 따라 존재할 수 있지만, 실행 중인 배치의 핵심 제약은 다음 세 가지입니다.

vLLM 설정 제어 범위 의미
--max-num-seqs 요청 단위 한 iteration에서 함께 처리할 수 있는 최대 시퀀스 수
--max-num-batched-tokens 토큰 단위 한 iteration에서 스케줄러가 처리하도록 허용하는 전체 토큰 수
--max-model-len 요청별 길이 프롬프트와 생성 토큰을 합친 시퀀스 하나의 최대 길이

그림 8 - 최대 시퀀스 수와 요청별 최대 길이, iteration별 전체 토큰 예산은 서로 다른 범위를 제한합니다

Prefill은 요청 하나가 많은 입력 토큰을 사용하므로 max_num_batched_tokens의 영향을 크게 받습니다. Decode에서는 활성 요청마다 일반적으로 토큰 하나를 처리하므로 병렬성은 max_num_seqs의 영향을 크게 받습니다. 요청 수만 제한하면 짧은 프롬프트 여러 개와 매우 긴 프롬프트 몇 개의 차이를 표현하기 어려우므로 두 설정을 함께 사용합니다.

다음은 각 값의 관계를 보여주기 위한 설정 예시입니다. 실제 값은 모델과 GPU 메모리, 트래픽 분포와 지연시간 목표에 맞춰 조정해야 합니다.

청크 프리필(Chunked Prefill)을 이용한 연속 배칭

연속 배칭은 출력 길이가 다른 요청 때문에 배치 슬롯이 비는 문제를 줄입니다. 그러나 긴 Prefill과 짧은 Decode가 같은 스케줄러에서 경쟁하는 문제는 남습니다.

그림 9 - 요청의 시작 시점과 길이가 같으면 Prefill과 Decode를 각각 정렬된 배치로 처리할 수 있습니다

그림 9는 요청의 입력 길이와 출력 길이, 시작 시점이 같은 이상적인 경우입니다. 실제 온라인 트래픽에서는 새 요청의 Prefill이 실행 중인 요청의 Decode 사이에 합류합니다.

그림 10 - 긴 Prefill을 한 번에 처리하면 실행 중인 요청의 다음 Decode가 그 시간만큼 지연됩니다

청크 프리필(chunked prefill)은 긴 프롬프트를 여러 토큰 조각으로 나누어 여러 iteration에 걸쳐 처리합니다. 스케줄러는 Decode 요청을 처리하면서 남은 토큰 예산에 Prefill 조각을 배치할 수 있습니다. 긴 Prefill 하나가 Decode를 오랫동안 막는 현상이 줄고, 성격이 다른 두 워크로드를 한 배치에서 함께 활용할 수 있습니다.

그림 11 - 청크 프리필은 긴 Prefill을 나누어 기존 요청의 Decode와 번갈아 배치합니다

현재 vLLM V1의 청크 프리필 스케줄러는 다음 순서로 동작합니다.

  1. 대기 중인 Decode 요청을 먼저 배치합니다.
  2. max_num_batched_tokens에서 남은 토큰 예산에 Prefill 요청을 추가합니다.
  3. 남은 예산보다 Prefill이 길면 들어갈 수 있는 만큼만 청크로 나눕니다.
  4. 처리하지 못한 Prefill 토큰은 다음 iteration에서 이어서 계산합니다.

max_num_batched_tokens는 한 iteration의 전체 토큰 예산입니다. 고정된 청크 크기를 직접 지정하는 옵션은 아닙니다. Decode를 먼저 배치하고 남은 예산에 따라 실제 Prefill 청크 크기가 결정됩니다.

토큰 예산 장점 주의할 점
작은 값 Prefill이 Decode를 막는 시간이 짧아져 ITL에 유리합니다. iteration 횟수와 스케줄링 오버헤드가 늘고 Prefill 진행이 느려져 TTFT와 처리량에 불리할 수 있습니다.
큰 값 한 번에 더 많은 Prefill 토큰을 처리해 TTFT와 Prefill 처리 효율에 유리합니다. 긴 Prefill 조각이 Decode를 더 오래 지연시켜 ITL이 나빠질 수 있습니다.

그림 12 - 큰 Prefill 토큰 예산은 한 iteration을 길게 만들어 실행 중인 요청의 ITL을 악화시킬 수 있습니다

그림 13 - Prefill을 작은 청크로 나누면 Decode를 수행할 기회가 iteration 사이에 생깁니다

그림 12와 그림 13의 수치는 특정 설정을 비교한 예시입니다. 일반적인 결론은 청크를 작게 나눌수록 Decode가 긴 Prefill에 막히는 시간이 짧아지고, 그만큼 Prefill iteration과 스케줄링 횟수가 늘어난다는 점입니다.

따라서 청크 프리필의 효과는 하나의 지표로 고정되지 않습니다. 작은 토큰 예산은 ITL을 우선하고, 큰 토큰 예산은 TTFT와 Prefill 처리 효율을 우선합니다. 처리량과 종단 지연시간은 모델, GPU와 입력·출력 길이 분포를 기준으로 측정해야 합니다.

청크 프리필은 긴 컨텍스트가 많은 환경에서 Decode 지연을 완화하고 GPU 활용률을 높이는 데 유용합니다. 스케줄링과 KV 캐시 관리가 복잡해지는 비용도 있으므로 TTFT, ITL, 처리량과 메모리 사용량을 함께 측정해 토큰 예산을 정해야 합니다. 더 큰 규모에서는 Prefill과 Decode를 서로 다른 GPU나 노드로 분리하는 방식도 사용할 수 있으며, 이는 이후 장에서 다룹니다.

스케일링 어텐션과 (GPU) 커널 최적화

트랜스포머 블록은 크게 어텐션 레이어와 피드포워드 레이어(FFN 또는 MLP)로 구성됩니다. 이 절에서는 두 구성요소 중 어텐션을 긴 컨텍스트와 높은 동시 요청 수에 맞게 효율적으로 확장하는 방법을 살펴봅니다.

어텐션 최적화는 다음 세 가지 흐름으로 나눌 수 있습니다.

  1. 어텐션 구조 최적화: MHA(Multi-Head Attention) 이후 등장한 MQA(Multi-Query Attention), GQA(Grouped-Query Attention)와 MLA(Multi-head Latent Attention)를 살펴봅니다. 이 구조들은 K와 V의 공유 또는 압축 방식을 바꿔 모델 품질 저하를 줄이면서 KV 캐시 크기와 메모리 접근량을 낮추는 것을 목표로 합니다.
  2. GPU 커널 최적화: 여러 연산을 합치는 커널 퓨전부터 어텐션과 하드웨어 특성에 맞춰 메모리 접근을 줄이는 FlashAttention 등의 커널을 살펴봅니다.
  3. KV 캐시 메모리 관리: PagedAttention이 KV 캐시를 고정된 블록 단위로 관리해 메모리 파편화와 과도한 사전 할당을 줄이는 원리를 살펴봅니다.

먼저 MQA, GQA와 MLA가 KV 캐시의 논리적인 크기를 줄이는 방법을 알아봅니다. 이어서 커스텀 GPU 커널이 어텐션 연산과 메모리 접근을 개선하는 방식을 살펴보고, 마지막으로 PagedAttention이 KV 캐시의 GPU 메모리 저장과 할당 방식을 개선하는 과정을 다룹니다.

확장 가능한 어텐션 메커니즘[1]

Decode 단계에서는 새 토큰을 생성할 때마다 이전 토큰의 K와 V를 KV 캐시에서 읽습니다. 컨텍스트가 길어질수록 HBM에서 온칩 메모리로 가져와야 할 데이터가 늘어나므로 KV 캐시의 크기는 메모리 용량과 대역폭 모두에 영향을 줍니다.

KV 캐시를 줄이면 다음과 같은 이점이 있습니다.

그림 14 - MHA와 GQA, MQA는 KV 헤드 공유 범위가 다르고 MLA는 압축된 latent KV를 캐싱합니다

그림 14의 GQA 50%와 MQA 25%는 쿼리 헤드 4개에 KV 헤드를 각각 2개와 1개 사용한 예시입니다. 실제 절감률은 전체 쿼리 헤드 수와 KV 헤드 수, 헤드 차원과 모델 구조에 따라 달라집니다. MLA의 GQA 2.25그룹 수준도 DeepSeek-V2의 구성을 GQA와 비교한 값입니다.

MHA(Multi-Head Attention)

MHA는 각 쿼리 헤드에 대응하는 K와 V 헤드를 따로 둡니다. 쿼리 헤드 수와 KV 헤드 수가 같으므로 각 헤드의 표현력을 독립적으로 유지할 수 있지만, 네 방식 중 KV 캐시가 가장 큽니다.

MQA(Multi-Query Attention)

MQA는 모든 쿼리 헤드가 하나의 K 헤드와 V 헤드를 공유합니다. 쿼리 헤드가 개이고 헤드 차원이 같다면 KV 캐시는 MHA의 약 로 줄어듭니다. 그만큼 Decode의 메모리 전송량도 줄지만, 공유 범위가 커지므로 모델과 학습 방식에 따라 품질 손실이 발생할 수 있습니다. 자세한 구조는 MQA 원 논문에서 확인할 수 있습니다.

그림 15 - MHA는 토큰마다 헤드별 K와 V를 저장하지만 MQA는 모든 쿼리 헤드가 하나의 K와 V를 공유합니다

GQA(Grouped-Query Attention)

GQA는 쿼리 헤드를 여러 그룹으로 나누고 같은 그룹의 쿼리들이 하나의 K와 V 헤드를 공유합니다. KV 헤드 수가 이고 쿼리 헤드 수가 라면, 헤드 차원이 같을 때 MHA 대비 KV 캐시 비율은 대략 입니다. MHA와 MQA 사이에서 KV 캐시 절감과 모델 품질의 균형을 조절할 수 있으며, 원 논문에서는 MHA에 가까운 품질과 MQA에 가까운 속도를 보고했습니다. 자세한 내용은 GQA 원 논문에서 확인할 수 있습니다.

그림 16 - GQA는 쿼리 헤드를 그룹으로 묶고 각 그룹 안에서 K와 V를 공유합니다

그림 17 - 쿼리 헤드 4개를 가정한 예시에서 KV 헤드 수에 따라 MHA와 GQA, MQA의 KV 캐시가 달라집니다

KV 헤드가 줄면 캐시 용량과 Decode 시 읽을 데이터가 함께 감소하므로 추론 성능도 개선될 수 있습니다. 다만 그림 18의 시간은 특정 실험에서 얻은 예시이며, 일반적인 속도 비율은 아닙니다. 실제 결과는 모델과 GPU, 컨텍스트 길이, 배치 크기와 커널 구현에 따라 달라집니다.

그림 18 - 특정 실험에서 MHA와 GQA, MQA의 추론시간을 비교한 예시

MLA(Multi-head Latent Attention)

MLA는 KV 헤드 수를 공유하는 대신 K와 V를 저차원 latent 표현으로 함께 압축하고, Decode에 필요한 압축 표현을 캐싱합니다. 연산 시에는 이 표현을 투영해 각 헤드가 사용할 정보를 구성합니다. DeepSeek-V2는 MLA를 도입해 기존 DeepSeek 67B보다 KV 캐시를 93.3% 줄였다고 보고했습니다. 구조와 측정 조건은 DeepSeek-V2 기술 보고서에서 확인할 수 있습니다.

MHA, MQA와 GQA는 KV 헤드의 공유 범위를 조정하고, MLA는 캐싱할 표현 자체를 압축합니다. 따라서 이 구조들을 하나의 단순한 우열 관계로 보기보다 모델 품질, KV 캐시 크기, 지원 커널과 하드웨어를 함께 비교해야 합니다.

Hybrid Attention

긴 컨텍스트에서는 전체 토큰 쌍을 비교하는 전역 어텐션의 계산량과 KV 캐시가 계속 증가합니다. 이를 줄이기 위해 Linear Attention과 recurrent state, sliding-window attention 등은 중간 상태를 압축하거나 어텐션 범위를 제한합니다. 압축 과정에서 세부 정보가 희석될 수 있으므로 DeltaNet 계열은 상태를 갱신하고, 게이트를 추가한 구조는 오래된 정보의 감쇠와 새로운 정보의 반영 정도를 조절합니다.

Kimi K3는 Kimi Delta Attention(KDA) 3개 층과 Gated MLA 1개 층을 반복하는 Hybrid Attention을 사용합니다. KDA는 고정 크기의 recurrent state로 긴 시퀀스를 효율적으로 처리하고, Gated MLA는 주기적으로 전역 어텐션을 수행해 압축된 상태만으로 처리하기 어려운 정보를 보완합니다. 이 구성은 Kimi K3 공식 설명과 공식 모델 저장소에서 확인할 수 있습니다.

모델 설정에서 어텐션 방식 확인하기

일반적인 MHA, GQA와 MQA 계열은 Hugging Face 모델의 config.json에서 num_attention_heads와 num_key_value_heads를 비교해 구분할 수 있습니다.

# MHA: 쿼리 헤드 수와 KV 헤드 수가 같습니다.
"num_attention_heads": 32,
"num_key_value_heads": 32

# GQA: 쿼리 헤드 수보다 KV 헤드 수가 적고 1보다 큽니다.
"num_attention_heads": 32,
"num_key_value_heads": 8
# KV 헤드 하나를 쿼리 헤드 4개가 공유합니다.

# MQA: KV 헤드가 하나입니다.
"num_attention_heads": 32,
"num_key_value_heads": 1

이 두 값만으로 판단할 수 있는 범위는 일반적인 MHA, GQA와 MQA 계열입니다. MLA나 Hybrid Attention은 모델별 설정과 구현을 추가로 확인해야 합니다.

어텐션 구조는 서빙 단계에서 임의로 교체하는 옵션이 아니라 모델 아키텍처의 일부입니다. 따라서 모델을 선택할 때 품질뿐 아니라 목표 컨텍스트 길이, 동시 요청 수, KV 캐시 사용량과 서빙 프레임워크의 커널 지원까지 함께 평가해야 합니다.

커널 퓨전과 커스텀 어텐션 커널

GPU 커널(kernel)은 GPU에서 실행되는 작고 특화된 프로그램입니다. 행렬곱, 정규화와 Softmax처럼 모델을 구성하는 연산은 하나 이상의 커널로 실행됩니다. 같은 수식을 계산하더라도 모델 구조와 입력 shape, 데이터 타입과 GPU 세대에 맞는 커널을 사용하면 메모리 이동과 실행 오버헤드를 줄여 GPU 활용률과 처리량을 높일 수 있습니다.

CUDA의 소프트웨어 실행 단위는 thread, warp, thread block과 grid의 계층으로 구성됩니다. thread 32개가 일반적으로 하나의 warp를 이루고, 여러 warp로 구성된 thread block은 하나의 SM(Streaming Multiprocessor)에 배치됩니다. 같은 block의 thread는 해당 SM의 레지스터와 shared memory를 이용해 데이터를 빠르게 공유할 수 있습니다.

그림 19 - CUDA의 thread와 warp, thread block과 grid가 GPU의 ALU, SM과 device에서 실행되는 관계

커널 정의와 실행 과정은 사용하는 프레임워크에 따라 다릅니다. Triton처럼 JIT 컴파일을 사용하는 경우에는 host인 CPU에서 커널을 정의하고 첫 호출에 컴파일한 뒤, GPU에 grid를 실행하도록 요청합니다. GPU는 grid의 block을 사용 가능한 SM에 배치해 병렬로 실행합니다.

그림 20 - CPU에서 정의·컴파일·실행한 커널의 block grid가 GPU의 여러 SM에 배치되는 과정

이 구조의 성능 비용에는 연산 시간, 반복적인 커널 실행과 중간 결과를 HBM에 쓰고 다시 읽는 시간이 모두 포함됩니다. 커널 퓨전과 FlashAttention은 이 데이터 이동을 줄이는 대표적인 방법입니다.

커널 퓨전

커널 퓨전(kernel fusion)은 연속된 여러 연산을 하나의 커널로 합치는 기법입니다. 개별 커널로 실행하면 각 단계의 중간 결과를 GPU 글로벌 메모리에 쓴 뒤 다음 커널이 다시 읽어야 합니다. 연산을 합치면 중간값을 레지스터나 shared memory에 유지한 채 다음 계산에 사용할 수 있습니다.

그림 21 - 세 커널을 따로 실행할 때 발생하는 세 번의 메모리 왕복을 하나의 퓨전 커널이 한 번으로 줄입니다

예를 들어 SiLU와 곱셈을 따로 실행하면 SiLU 결과를 HBM에 기록하고 다시 가져와야 합니다. 하나의 퓨전 커널에서는 입력을 읽은 뒤 SiLU 결과를 레지스터에 유지하고 곧바로 곱셈을 수행해 최종 출력만 HBM에 기록합니다.

그림 22 - SiLU와 곱셈을 합친 커널은 입력을 한 번 읽고 중간값을 레지스터에서 재사용한 뒤 출력만 기록합니다

어텐션에서도 QK 행렬곱, Softmax와 PV 행렬곱을 별도 커널로 실행하면 중간 행렬을 쓰고 읽는 동안 연산 장치가 대기할 수 있습니다. 그림 23은 이 과정을 하나의 실행 흐름으로 합쳤을 때 줄어드는 메모리 I/O와 유휴 시간을 단순화해 보여줍니다. 실제 퓨전 범위와 커널 구성은 백엔드와 GPU에 따라 달라집니다.

그림 23 - 어텐션 연산을 퓨전하면 중간 행렬의 반복적인 읽기·쓰기가 줄고 연산 사이의 유휴 시간이 짧아집니다

FlashAttention

표준 어텐션을 여러 커널로 구현하면 와 처럼 시퀀스 길이에 따라 으로 커지는 중간 행렬을 HBM에 구체화할 수 있습니다. 이 행렬을 쓰고 다시 읽는 과정이 GPU 메모리 대역폭과 메모리 사용량에 부담을 줍니다.

FlashAttention은 HBM과 온칩 SRAM 사이의 I/O를 고려해 설계한 정확한 어텐션 알고리즘입니다. Q, K와 V를 작은 tile로 나누고, tile 단위의 행렬곱과 online softmax를 레지스터와 shared memory에서 처리합니다. 전체 어텐션 행렬을 HBM에 저장하지 않고 최종 출력만 기록하므로 HBM 접근량을 줄일 수 있습니다. 원리와 측정 결과는 FlashAttention 원 논문에서 확인할 수 있습니다.

그림 24 - FlashAttention은 Q·K·V tile을 SRAM에서 계산하고 최종 출력만 HBM에 기록합니다

그림 24 오른쪽의 실행시간은 논문에 제시된 GPT-2 어텐션 사례를 단순화한 값입니다. 모든 모델과 GPU에서 같은 비율의 속도 향상을 보장하지 않으며, 입력 길이와 데이터 타입, GPU와 비교 대상 커널에 따라 결과가 달라집니다.

FlashAttention이 줄이는 병목을 기존 구현의 흐름으로 보면 그림 25와 같습니다. 와 는 SRAM에 통째로 담기 어려운 행렬이므로 각 연산 사이에서 HBM을 오갈 수 있습니다. FlashAttention은 이 중간 행렬을 tile 단위로 처리해 전체 형태로 저장하는 과정을 피합니다.

그림 25 - 표준 어텐션 구현에서는 N×N 크기의 S와 P를 HBM에 쓰고 다시 읽는 과정이 실행시간을 지배할 수 있습니다

FlashAttention-2는 thread block과 warp 사이의 작업 분할을 개선하고 비행렬곱 연산을 줄여 병렬성과 GPU 점유율을 높였습니다. FlashAttention-3는 Hopper GPU의 비동기 Tensor Core와 TMA를 활용해 데이터 이동과 연산을 겹치고, block 단위 행렬곱과 Softmax를 중첩합니다. 자세한 차이는 FlashAttention-2 논문과 FlashAttention-3 논문에서 확인할 수 있습니다.

커널 구현에는 CUDA와 GPU 아키텍처, 컴파일러와 성능 프로파일링에 대한 전문성이 필요합니다. 모델을 서빙하는 입장에서는 커널을 직접 작성하기보다 vLLM이나 SGLang이 제공하는 자동 선택을 기본값으로 사용하고, 목표 워크로드를 벤치마크한 뒤 필요할 때 백엔드를 변경하는 편이 일반적입니다. FlashInfer, xFormers와 Triton도 사용할 수 있으며, 커널 작성 언어인 Triton은 NVIDIA Triton Inference Server와 다른 프로젝트입니다.

vLLM은 하드웨어와 모델 구성을 기준으로 어텐션 백엔드와 FlashAttention 버전을 자동 선택합니다. 특정 백엔드를 검증할 때는 다음과 같이 명시할 수 있습니다.

# 기본값: 지원되는 백엔드를 자동 선택합니다.
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --max-model-len 4096 \
  --max-num-batched-tokens 8192 \
  --max-num-seqs 128

# 비교 실험에서 FlashAttention과 버전을 명시하는 예시입니다.
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --attention-backend FLASH_ATTN \
  --attention-config.flash_attn_version=3 \
  --max-model-len 4096 \
  --max-num-batched-tokens 8192 \
  --max-num-seqs 128

FlashAttention 2, 3과 4의 지원 범위는 GPU compute capability와 데이터 타입에 따라 다릅니다. 현재 옵션과 자동 선택 기준은 vLLM Attention Backend 문서에서 확인해야 합니다. 처음에는 자동 선택으로 측정하고, 동일한 모델·정밀도·트래픽에서 TTFT, ITL과 처리량을 비교해 변경 여부를 결정합니다.

PagedAttention

KV 캐시는 요청이 생성한 토큰 수에 따라 실행 중에도 계속 증가합니다. 입력 길이는 요청마다 다르고 출력 길이는 미리 알 수 없으므로, 요청별 최대 크기를 연속된 메모리로 미리 할당하면 사용하지 않는 예약 공간과 내·외부 파편화가 커집니다.

PagedAttention은 운영체제의 페이징에서 아이디어를 가져와 KV 캐시를 고정 크기의 논리 block으로 나눕니다. block table은 각 논리 block을 GPU 메모리의 물리 block에 매핑합니다. 물리 block은 서로 붙어 있을 필요가 없고 토큰이 늘어날 때 필요한 만큼 추가할 수 있습니다.

그림 26 - PagedAttention은 연속된 논리 KV 캐시를 block table을 통해 흩어진 물리 KV 캐시 block에 매핑합니다

그림 26에서는 논리 block 0, 1, 2가 물리 block 7, 1, 3에 저장됩니다. block 하나에는 최대 네 토큰이 들어가며 마지막 block은 생성 중이라 두 칸만 사용합니다. 이 배치는 예시이고 실제 block 크기는 서빙 엔진 설정과 구현에 따라 달라집니다.

PagedAttention 원 논문은 기존 시스템에서 예약된 KV 캐시 메모리 중 실제 토큰 상태를 저장하는 비율이 20.4%에서 38.2%에 불과했으며, vLLM과 PagedAttention의 block 단위 관리로 KV 캐시 낭비를 거의 0에 가깝게 줄였다고 보고했습니다. 측정 조건과 구현은 PagedAttention 원 논문에서 확인할 수 있습니다.

PagedAttention은 KV 캐시가 GPU 메모리에 저장되고 할당되는 방식을 바꾸는 메모리 관리 기법입니다. 연산을 합치는 커널 퓨전이나 타일링을 적용하는 FlashAttention과 최적화 대상이 다르며, paged KV cache를 읽을 수 있는 어텐션 커널과 함께 동작합니다. vLLM에서는 이 block 기반 KV 캐시 관리가 기본 구조이므로 일반적인 서빙에서 별도의 기능 플래그로 켜지 않습니다.

세 기법은 서로 다른 관점에서 GPU 메모리 병목을 완화합니다.

기법 최적화 대상 주요 효과
커널 퓨전 연속된 연산과 중간 결과 커널 실행과 글로벌 메모리 왕복 감소
FlashAttention 어텐션 알고리즘과 HBM I/O 중간 행렬의 HBM 구체화 방지
PagedAttention KV 캐시의 물리적 배치와 할당 메모리 파편화와 사전 예약 낭비 감소
스케일링 어텐션과 GPU 커널 최적화 정리

MHA·MQA·GQA·MLA는 모델 아키텍처에 포함된 구조입니다. 일반적인 서빙 옵션으로 교체하기 어려우므로 KV 캐시와 품질 특성을 고려해 적합한 모델을 선택해야 합니다. MHA·GQA·MQA 계열은 config.json의 num_attention_heads와 num_key_value_heads를 비교해 확인할 수 있습니다.

커널 퓨전과 FlashAttention은 서빙 프레임워크와 실행 백엔드에서 적용됩니다. 동일한 모델도 PyTorch 기본 커널, FlashAttention과 FlashInfer 등 어떤 백엔드로 실행하는지에 따라 성능이 달라질 수 있습니다. 모델 구조와 GPU, 데이터 타입의 호환성을 확인하고 자동 선택 결과와 후보 백엔드를 벤치마크합니다.

PagedAttention은 서빙 엔진의 KV 캐시 메모리 관리 방식입니다. vLLM 같은 엔진에서는 내부 기본 구조로 사용되며, 사용자는 max_model_len, max_num_seqs와 KV 캐시 메모리 설정을 조정해 동시 처리량과 컨텍스트 길이를 튜닝합니다.

실무에서는 KV 캐시가 효율적인 모델 선택 → 연속 배칭과 paged KV cache를 지원하는 서빙 엔진 선택 → 호환되는 어텐션 커널 확인 → TTFT·ITL·처리량 비교 순서로 접근할 수 있습니다.

모델 압축 - 양자화와 증류를 중심으로 살펴보기

앞에서는 배칭으로 요청을 처리하는 방법과 어텐션 연산·메모리 접근을 최적화하는 방법을 살펴봤습니다. 이제 모델 자체의 크기와 연산량을 줄이는 방법으로 범위를 넓혀보겠습니다.

LLM의 큰 크기는 높은 GPU 비용과 제한된 하드웨어 가용성으로 이어집니다. 모델 압축은 품질 손실을 허용 가능한 수준으로 관리하면서 메모리 사용량, 데이터 이동량과 연산량을 줄이는 프로덕션 최적화 전략입니다. 이를 통해 더 작은 GPU나 엣지 장치에 모델을 배포하고, 같은 하드웨어에서 더 많은 요청을 처리할 수 있습니다.

모델 압축은 크게 양자화, 증류, 가지치기로 나눌 수 있습니다.

그림 27 - 양자화, 증류, 가지치기의 적용 방법과 모델 압축 효과

따라서 이 절에서는 양자화를 중심으로 원리와 서빙 효과를 살펴보고, 이후 증류와 가지치기의 역할을 간단히 비교합니다.

양자화(Quantization)

양자화는 모델의 가중치, 활성화 또는 KV 캐시를 FP32·FP16·BF16 같은 고정밀 포맷에서 FP8·INT8·FP4·INT4 같은 저비트 포맷으로 변환하는 기법입니다. 하나의 값을 표현하는 비트 수를 줄여 메모리 사용량과 데이터 이동량을 절감하는 대신, 원래 값을 얼마나 잘 보존할 수 있는지 확인해야 합니다.

그림 28 - FP32 값을 더 적은 수의 INT4 값으로 대응시키는 양자화 과정

숫자 포맷의 범위와 정밀도

부동소수점 포맷은 부호, 지수부, 가수부로 구성됩니다.

포맷 전체 비트 부호 지수부 가수부 최대 유한값의 크기
FP32 32 1 8 23 약
BF16 16 1 8 7 약
FP16 16 1 5 10

FP16과 BF16은 모두 16비트지만 비트 배분이 다릅니다. BF16은 FP32와 같은 8비트 지수부를 사용해 비슷한 동적 범위를 유지하고, FP16은 더 많은 가수부 비트로 제한된 범위 안의 값을 더 세밀하게 표현합니다.

그림 29 - FP32, BF16, FP16의 동적 범위와 정밀도 비교

그림 30 - FP32, BF16, FP16과 두 FP8 포맷의 표현 범위 비교

같은 구간에서도 가수부가 짧을수록 표현 가능한 값 사이의 간격이 넓어집니다. 그림 31에서 FP8은 1과 2 사이를 FP16·BF16보다 훨씬 적은 값으로 표현합니다.

그림 31 - 1과 2 사이에서 부동소수점 포맷별로 표현할 수 있는 값의 밀도

양자화 오차와 스케일링

저비트 포맷으로 변환할 때는 주로 두 종류의 오차가 생깁니다.

정수 양자화는 보통 스케일 팩터 와 필요에 따라 영점 를 사용해 원래 값 를 양자화된 값 에 대응시킵니다.

여기서 는 역양자화한 근삿값입니다. 스케일을 작게 잡으면 중앙의 값을 세밀하게 구분할 수 있지만 극단값이 잘릴 가능성이 커지고, 크게 잡으면 범위는 넓어지지만 값 사이의 간격이 커집니다. 캘리브레이션은 이 오차의 균형을 잡을 스케일과 클리핑 범위를 정하는 과정입니다.

INT와 FP 기반 저비트 포맷

INT8·INT4는 스케일이 정해지면 양자화 구간 안의 값이 일정한 간격으로 배치됩니다. FP8·FP4는 지수부를 사용하므로 값의 간격이 일정하지 않습니다. 0에 가까운 영역은 비교적 촘촘하고 값의 크기가 커질수록 간격도 넓어집니다.

그림 32 - 일정한 간격의 INT8과 값의 크기에 따라 간격이 달라지는 FP8 비교

따라서 같은 비트 수라도 어느 포맷이 더 정확한지는 가중치와 활성화의 분포, 이상치의 크기, 스케일링 단위에 따라 달라집니다.

양자화가 서빙에 주는 효과

  1. 모델 크기 감소
    • 70억 개의 가중치를 BF16으로 저장하면 약 14GB가 필요합니다. 이론적인 원시 데이터 크기는 INT8에서 약 7GB, INT4에서 약 3.5GB로 줄어듭니다.
    • 실제 체크포인트에는 스케일과 메타데이터가 포함되므로 파일 크기는 이 계산보다 조금 커질 수 있습니다.
    • 남은 GPU 메모리는 KV 캐시와 더 큰 배치에 사용할 수 있습니다. 가중치만 양자화했을 때 KV 캐시 자체의 크기는 줄어들지 않습니다.
  2. 데이터 이동량 감소
    • 한 번의 디코드 iteration에서 HBM으로부터 읽어야 할 가중치가 작아집니다. 메모리 대역폭 제한이 강한 워크로드의 지연시간과 처리량을 개선할 수 있습니다.
    • 모델을 단일 GPU나 노드에 배치할 수 있게 되면 GPU·노드 간 통신도 피할 수 있습니다.
  3. 연산 처리량 향상 가능
    • 저정밀 행렬 연산을 지원하는 Tensor Core와 최적화된 커널을 사용하면 같은 시간에 더 많은 연산을 수행할 수 있습니다.
    • 지원되는 데이터 타입, GPU 세대, 커널과 행렬 크기가 맞아야 하므로 비트 수를 절반으로 줄였다고 실제 처리량이 항상 두 배가 되지는 않습니다.

Weight-only와 Weight-and-Activation 양자화

W4A16은 가중치를 4비트, 활성화를 16비트로 처리한다는 뜻입니다. W8A8은 가중치와 활성화를 모두 8비트로 처리합니다. KV 캐시 정밀도는 이 표기에 포함되지 않으며 별도로 설정합니다.

구분 W4A16 W8A8
양자화 대상 가중치 가중치와 활성화
FP16 대비 원시 가중치 크기 약 75% 감소 약 50% 감소
주된 이점 모델 적재 용량과 HBM 데이터 이동량 절감 데이터 이동량 절감과 저정밀 행렬 연산 활용
추가 비용 가중치 언패킹·역양자화 또는 혼합 정밀도 커널 활성화 스케일 계산과 양자화 오버헤드
대표 방식 AWQ, GPTQ INT8·FP8 기반 W8A8

Weight-only 방식은 압축된 가중치를 실행 중에 풀어 계산 포맷으로 변환합니다. Marlin이나 Machete 같은 혼합 정밀도 커널은 이 과정을 행렬 곱셈과 가깝게 결합해 중간 데이터 이동과 오버헤드를 줄입니다. 이 경우에도 변환 자체가 사라지는 것은 아니며, 실제 성능은 GPU와 지원 커널에 따라 달라집니다.

활성화 값은 요청과 레이어마다 달라집니다. 동적 스케일링은 실행 중 관측한 값으로 스케일을 계산해 입력 변화에 잘 대응하지만 계산 비용이 추가됩니다. 정적 스케일링은 대표 캘리브레이션 데이터로 스케일을 미리 정해 실행 비용을 줄이며, 실제 트래픽 분포와 캘리브레이션 데이터가 다르면 오차가 커질 수 있습니다.

그림 33 - W4A16의 데이터 이동 절감과 W8A8·W4A4의 저정밀 연산 효과

FP8 포맷과 하드웨어 지원

FP8에는 대표적으로 E4M3와 E5M2가 있습니다. E4M3는 가수부가 하나 더 많아 정밀도가 높고, E5M2는 지수부가 하나 더 많아 동적 범위가 넓습니다. FP8 형식 논문은 두 포맷을 딥러닝 연산에 맞춰 제안했으며, NVIDIA Transformer Engine은 E4M3의 최대 유한값을 448, E5M2를 57,344로 설명합니다.

FP8 포맷 전체 비트 부호 지수부 가수부 특성
E4M3 8 1 4 3 높은 정밀도, 좁은 범위
E5M2 8 1 5 2 낮은 정밀도, 넓은 범위

FP8의 제한된 범위는 스케일링으로 보완합니다. 예를 들어 블록별 스케일링은 작은 데이터 블록마다 별도의 스케일을 적용해 이상치가 나머지 값의 정밀도를 지나치게 떨어뜨리는 문제를 줄입니다. 자세한 동작은 NVIDIA Transformer Engine의 FP8 스케일링 문서에서 확인할 수 있습니다.

FP8의 속도 이득을 얻으려면 네이티브 FP8 연산을 지원하는 GPU와 커널이 필요합니다. NVIDIA H100 같은 Hopper 계열과 이후 세대는 FP8 Tensor Core를 제공하지만, A100 같은 Ampere GPU는 네이티브 FP8 Tensor Core가 없습니다. 서빙 엔진의 양자화 호환성 표에서 모델 포맷과 GPU 세대가 실제로 지원되는지 먼저 확인해야 합니다.

양자화 전략 선택

모델을 한 GPU나 노드에 적재하기 어렵거나 디코드의 가중치 읽기가 병목이면 W4A16 같은 Weight-only 방식을 먼저 검토할 수 있습니다. 높은 배치의 Prefill과 행렬 연산 처리량까지 개선하려면 하드웨어가 지원하는 W8A8·FP8 방식을 함께 비교합니다.

최종 선택은 모델 품질 평가와 실제 워크로드 벤치마크로 결정합니다. 동일한 양자화 포맷도 모델, GPU, 커널, 배치 크기와 입력·출력 길이에 따라 TTFT·ITL·처리량이 달라질 수 있습니다.

다른 양자화 기법들

KV 캐시와 어텐션 양자화

앞에서 살펴본 W4A16과 W8A8은 주로 Transformer의 선형 레이어에서 사용하는 가중치와 활성화를 대상으로 합니다. KV 캐시는 별도의 데이터 타입과 스케일을 사용하므로 따로 양자화해야 합니다.

KV 캐시를 FP16·BF16에서 FP8로 낮추면 같은 GPU 메모리에 더 많은 토큰을 저장할 수 있습니다. 긴 컨텍스트를 유지하거나 동시 요청 수가 많은 서빙에서 배치 크기와 처리량을 높일 수 있고, 뒤에서 살펴볼 프리픽스 캐싱에도 더 많은 공간을 제공할 수 있습니다.

그림 34 - 컨텍스트 길이와 배치 크기에 따라 증가하는 KV 캐시의 압축 대상

KV 캐시 양자화만 적용하면 메모리 사용량과 HBM 데이터 이동량은 줄지만, 어텐션 연산이 고정밀도로 실행될 때는 K·V를 다시 변환하는 비용이 생깁니다. FP8 어텐션 커널까지 지원하는 백엔드는 Q·K·V와 어텐션 연산을 FP8 영역에서 처리해 추가 효과를 얻을 수 있습니다. 예를 들어 vLLM은 FlashAttention 3 백엔드에서 FP8 KV 캐시를 사용하면 어텐션 연산과 Query도 FP8로 처리한다고 설명합니다.

실무에서는 가중치·활성화 양자화로 모델 크기와 선형 레이어의 비용을 먼저 줄이고, KV 캐시 용량이 병목인 긴 컨텍스트·고동시성 워크로드에서 FP8 KV 캐시와 어텐션 양자화를 추가로 검토할 수 있습니다. KV 캐시의 스케일링 단위와 캘리브레이션 데이터에 따라 품질과 속도가 달라지므로 실제 컨텍스트 길이로 평가해야 합니다.

GGUF와 로컬 추론

GGUF는 특정 양자화 알고리즘의 이름이 아니며, GGML 기반 실행기가 모델의 텐서와 메타데이터를 한 파일에 저장하고 빠르게 불러오기 위한 바이너리 파일 형식입니다. GGUF 파일에는 Q4·Q5·Q8처럼 서로 다른 양자화 타입의 텐서를 담을 수 있습니다.

그림 35 - GGUF 파일의 헤더, 메타데이터, 텐서 정보와 실제 텐서 데이터 구조

GGUF는 llama.cpp 생태계에서 CPU와 Apple Silicon의 Metal, CUDA·Vulkan 등 다양한 백엔드로 로컬 추론할 때 널리 사용됩니다. 모델 일부를 GPU에 올리고 나머지를 CPU에서 실행하는 혼합 오프로딩도 가능해, 전체 모델을 GPU 메모리에 담기 어려운 환경에서 유용합니다. 다만 데이터센터 GPU에서 최대 처리량을 목표로 할 때는 해당 GPU와 서빙 엔진에 최적화된 FP8·AWQ·GPTQ 체크포인트가 더 적합할 수 있으므로 별도로 벤치마크합니다.

정확도 트레이드오프와 평가

양자화는 표현 가능한 값의 수를 줄이므로 모델 품질과 서빙 효율 사이에 트레이드오프가 생깁니다. 그림 36의 Llama 3.1 평가에서는 잘 조정된 W8A8-FP8, W8A8-INT8과 W4A16이 BF16에 가까운 평균 점수를 보였습니다. 이 결과는 특정 모델 계열, 양자화 레시피와 벤치마크에서 측정한 값이며 모든 모델과 작업에 그대로 적용되지는 않습니다.

그림 36 - Llama 3.1 모델 크기와 양자화 포맷별 Open LLM Leaderboard 평균 정확도

모델 크기만으로 양자화 민감도를 단정하기는 어렵습니다. 레이어별 이상치, 어텐션 구조, 스케일링 단위, 캘리브레이션 데이터와 평가 작업이 함께 영향을 줍니다. 특히 KV 캐시 양자화는 짧은 벤치마크에서 문제가 드러나지 않더라도 긴 컨텍스트의 검색·추론 품질에 영향을 줄 수 있습니다.

양자화 전후 모델은 동일한 프롬프트, 디코딩 설정과 평가 데이터로 비교합니다. LM Evaluation Harness를 사용한다면 다음처럼 모델 경로를 pretrained 인수로 전달할 수 있습니다.

lm_eval --model vllm \
  --model_args pretrained=Qwen/Qwen2-7B-Instruct \
  --tasks gsm8k_cot \
  --device cuda:0 \
  --batch_size auto

평균 점수만 확인하기보다는 서비스에서 중요한 정확성, 긴 컨텍스트 회수율, 도구 호출 형식과 생성 안정성을 함께 평가해야 합니다. TTFT·ITL·처리량과 GPU 메모리 사용량도 같은 부하 조건에서 측정합니다.

PTQ와 양자화 인식 훈련

훈련 후 양자화(Post-Training Quantization, PTQ)는 이미 학습된 모델에 양자화를 적용합니다. 양자화 인식 훈련(Quantization-Aware Training, QAT)은 학습이나 파인튜닝 중 양자화와 역양자화 오차를 모사해 저비트 표현에 적응시킵니다.

구분 PTQ QAT
적용 시점 훈련 완료 후 훈련 또는 파인튜닝 중
준비 작업 변환, 필요시 캘리브레이션 학습 파이프라인과 데이터 필요
장점 빠른 도입과 높은 배포 유연성 공격적인 저비트에서 품질 회복 가능
고려 사항 모델과 레시피에 따라 저비트 오차 증가 학습 비용과 특정 양자화 방식 의존성 증가

PTQ는 8비트와 잘 조정된 4비트 Weight-only 양자화에서도 좋은 품질을 낼 수 있어 프로덕션에서 널리 사용됩니다. QAT는 PTQ가 목표 품질을 만족하지 못하거나 FP4 이하의 강한 압축을 모델이 학습 과정에서 직접 견디도록 만들 때 검토합니다.

OpenAI의 gpt-oss는 양자화를 모델 배포 형식에 포함한 사례입니다. 공식 모델 카드에 따르면 두 모델은 후속 학습 과정에서 전체 파라미터의 90% 이상을 차지하는 MoE 가중치를 파라미터당 4.25비트의 MXFP4로 양자화했습니다.

모델 전체 파라미터 활성 파라미터 체크포인트 크기 메모리 목표
gpt-oss-120b 116.8B 5.1B 60.8GiB 단일 80GB GPU
gpt-oss-20b 20.9B 3.6B 12.8GiB 16GB 메모리 시스템

이미 양자화된 체크포인트를 제공하므로 사용자가 별도의 양자화 레시피를 선택할 필요가 없습니다. 다만 체크포인트가 메모리에 들어가는 것과 MXFP4 연산을 네이티브로 가속하는 것은 구분해야 합니다.

하드웨어와 실행 백엔드에 따라 MXFP4를 직접 계산하거나 다른 포맷으로 변환할 수 있으므로, 실제 배포 환경의 지원 여부와 성능을 확인해야 합니다.

OpenAI가 발표한 모델 특성과 성능 비교는 gpt-oss 소개에서 확인할 수 있습니다.

다른 양자화 기법 선택 순서

가중치와 활성화 양자화로 모델 적재 크기와 기본 서빙 성능을 먼저 개선합니다.

긴 컨텍스트와 동시 요청 때문에 KV 캐시가 병목이면 KV 캐시·어텐션 양자화를 검토하고, 로컬 CPU·Apple Silicon 배포가 목표면 GGUF와 llama.cpp 계열을 비교합니다.

목표 품질을 PTQ로 달성하기 어려울 때는 QAT가 적용된 모델이나 별도의 QAT 파이프라인을 고려합니다.

증류(distillation)

증류는 큰 교사(teacher) 모델의 행동과 지식을 더 작은 학생(student) 모델에 학습시키는 방법입니다. 양자화가 같은 모델의 숫자 표현을 줄이는 방식이라면, 증류는 파라미터 수와 구조가 다른 새로운 학생 모델을 만듭니다. 학생 모델이 충분히 작아지면 필요한 GPU 수와 모델 가중치 읽기 자체가 크게 줄어 지연시간과 처리량을 개선할 수 있습니다.

그림 37 - DeepSeek-R1의 추론 능력을 더 작은 배포 대상에 옮기는 증류의 역할

그림 38 - 671B 교사 모델과 1.5B부터 70B까지의 증류 모델 크기 비교

그림 37과 38의 장치·GPU 구성은 모델 크기 차이를 보여주는 예시입니다. 실제 적재 가능 여부와 속도는 정밀도, KV 캐시, 컨텍스트 길이와 실행 백엔드에 따라 달라집니다.

증류 파이프라인

증류에는 교사 모델로부터 얻을 수 있는 정보에 따라 여러 방식이 있습니다.

그림 39 - 교사 모델이 생성한 응답을 저장해 작은 학생 모델을 파인튜닝하는 출력 증류

그림 40 - 교사의 soft target과 hard label을 함께 사용하는 지식 증류 학습

로짓 증류에서는 보통 학생의 정답 손실과 교사 분포를 모방하는 손실을 함께 사용합니다.

Soft target은 정답 외 토큰에 교사가 부여한 상대적인 확률까지 전달합니다. 학생은 hard label만 학습할 때보다 클래스와 토큰 사이의 유사성을 더 많이 배울 수 있습니다. 출력 증류는 이 정보가 적은 대신 폐쇄형 교사 API로도 데이터셋을 만들 수 있다는 장점이 있습니다.

DeepSeek-R1 증류 모델

DeepSeek-R1은 671B 전체 파라미터 중 토큰마다 약 37B가 활성화되는 MoE 모델입니다. DeepSeek는 R1이 생성한 80만 개 샘플로 Qwen2.5와 Llama 계열의 dense 학생 모델을 파인튜닝하고, 1.5B·7B·8B·14B·32B·70B 체크포인트를 공개했습니다. 이는 교사의 텍스트 출력과 추론 데이터를 이용한 출력 증류 사례입니다.

벤치마크 DeepSeek-R1 671B DeepSeek-R1-Distill-Llama-70B
MATH-500 pass@1 97.3 94.5
GPQA Diamond pass@1 71.5 65.2
LiveCodeBench pass@1 65.9 57.5

70B 학생 모델은 교사보다 파라미터가 약 10분의 1로 줄었으며, 표의 수학·과학·코딩 벤치마크에서도 상당한 성능을 유지했습니다. 수치는 DeepSeek-R1 공식 저장소의 평가 조건에 따른 결과이므로 실제 서비스 작업에서도 별도로 검증해야 합니다.

양자화와 증류의 선택 순서

구분 양자화 증류
변경 대상 기존 모델의 숫자 표현 새로 학습하는 작은 학생 모델
도입 비용 PTQ는 비교적 낮음 데이터 생성과 학습 비용이 큼
품질 위험 잘 조정된 8·4비트에서 비교적 작을 수 있음 학생 크기와 데이터에 따라 더 커질 수 있음
서빙 효과 메모리와 데이터 이동량 감소 파라미터와 전체 연산량 감소

이미 검증된 증류 모델이 있다면 먼저 품질 기준을 확인하고 채택할 수 있습니다. 이후 학생 모델에 양자화를 추가하면 모델 크기와 데이터 이동량을 더 줄일 수 있습니다. 적합한 학생 모델이 준비되지 않았다면 PTQ를 먼저 시도하는 편이 도입 비용이 낮습니다. 직접 증류해야 할 때는 교사 출력 생성 비용, 학습 데이터 품질과 학생 모델 학습 비용까지 함께 계산합니다.

증류의 핵심

증류는 모델 구조 자체를 작게 만들어 큰 서빙 성능 향상을 얻을 수 있지만, 품질은 학생 모델의 크기와 증류 데이터에 좌우됩니다. 이미 만들어진 학생 모델을 평가하고, 품질 기준을 통과하면 양자화까지 조합하는 순서가 실용적입니다.

가지치기(pruning)

가지치기는 중요도가 낮은 가중치, 채널, 뉴런이나 어텐션 헤드를 제거해 모델을 희소하게 만드는 압축 기법입니다. LLM에는 중복된 파라미터가 많을 수 있지만, 값을 0으로 만드는 것만으로는 메모리 사용량이나 실행 시간이 자동으로 줄지 않습니다. 희소한 구조를 저장하고 계산할 수 있는 포맷, 커널과 하드웨어가 함께 필요합니다.

가지치기는 제거 단위에 따라 나눌 수 있습니다.

2:4 구조적 희소성

NVIDIA Ampere와 이후 아키텍처의 Sparse Tensor Core는 연속된 네 가중치 중 두 개만 남기는 2:4 패턴을 지원합니다. 이는 50% 희소성에 해당합니다.

그림 41 - 2:4 구조적 희소 행렬을 non-zero 값과 위치 메타데이터로 압축하는 방식

압축된 행렬에는 non-zero 값과 원래 위치를 나타내는 인덱스 메타데이터가 저장됩니다. 연산 전에 dense 행렬 전체를 복원하지 않으며, Sparse Tensor Core가 메타데이터로 필요한 활성화를 선택하고 0인 가중치의 곱셈을 건너뜁니다.

지원되는 행렬 곱셈에서 이론적인 연산 처리량은 dense Tensor Core 대비 최대 2배가 될 수 있습니다. 실제 모델의 종단 성능은 희소 커널이 적용되는 레이어 비율, 행렬 크기, 메모리 이동과 다른 연산의 비중에 따라 더 낮습니다. TensorRT도 문제 크기에 따라 dense 경로가 더 빠르면 이를 선택할 수 있습니다.

가지치기 직후에는 모델 품질이 떨어질 수 있으므로 남은 가중치를 파인튜닝하거나, 학습 중 희소 패턴을 적용해 품질을 회복하는 과정이 흔히 필요합니다. 이 추가 학습과 전용 실행 경로 때문에 범용 LLM 서빙에서 가지치기의 도입 범위는 양자화보다 좁습니다.

가지치기의 핵심

압축률만 확인해서는 실제 가속을 판단할 수 없습니다. 목표 GPU와 서빙 엔진이 해당 희소 패턴을 지원하는지 확인하고, 품질 회복 학습 후 dense 모델과 종단 지연시간·처리량을 비교해야 합니다.

Prefix caching

캐싱은 자주 사용하는 데이터를 빠른 저장소에 보관해 지연 시간을 줄이는 기법입니다. ML 서빙에서는 동일한 요청의 모델 출력을 저장해 두었다가 재사용하는 방식이 흔합니다[2]. 하지만 LLM은 자유형 텍스트를 입력으로 받으므로 같은 의도의 질문도 표현이 달라질 수 있고, 요청 전체를 기준으로 하면 캐시 적중률이 낮아지기 쉽습니다.

그림 42 - 일반 캐싱과 프리픽스 캐싱의 매칭 방식 및 KV 캐시 수명 비교

RadixCaching - 프롬프트 앞 부분을 캐시처리[3]

RadixAttention은 SGLang에서 사용하는 프리픽스 캐싱 구현입니다. 프롬프트 접두사를 라딕스 트리(radix tree, trie와 유사한 자료구조)로 관리해 여러 요청이 공유하는 구간을 빠르게 찾습니다.

그림 43 - 공통 프롬프트 접두사를 라딕스 트리로 관리하는 RadixAttention

Prefix Caching 실전 사례

프리픽스 캐싱은 여러 요청이 동일한 토큰 접두사를 공유할 때 효과가 큽니다. 문장의 의미가 비슷한지만으로는 캐시 적중이 보장되지 않으므로, 공통으로 유지할 내용을 프롬프트 앞쪽에 배치하는 것이 중요합니다.

그림 44 - 멀티턴 대화에서 이전 프롬프트 접두사의 KV 캐시를 재사용하는 예시

멀티턴 채팅

이전 대화 기록이 다음 요청의 앞부분에 그대로 이어지면, 이미 처리한 대화 구간의 KV 캐시를 재사용할 수 있습니다. 대화가 길어질수록 매번 다시 수행해야 하는 프리필이 줄어들어 TTFT 개선 효과가 커집니다.

긴 컨텍스트와 RAG

<system>
You are a helpful assistant.
<context>
Document: {stable context}
<user>
{dynamic question}

Prefix Caching 확장하기

단일 인스턴스의 캐시 용량

프리픽스 캐싱은 GPU 메모리에 KV 캐시를 오래 유지하므로, 캐시할 접두사가 길거나 종류가 많으면 모델 가중치와 함께 저장할 공간이 부족해질 수 있습니다. 캐시 용량과 적중률, KV 캐시 축출 빈도를 함께 확인해야 합니다.

여러 인스턴스와 캐시 인지 라우팅

트래픽이 늘면 여러 모델 인스턴스로 수평 확장합니다. 단순 라운드 로빈이나 최소 연결 방식은 같은 접두사를 서로 다른 인스턴스로 분산시켜 캐시 적중률을 낮출 수 있습니다. 캐시 인지 라우터는 요청 접두사와 각 인스턴스의 캐시 상태를 확인해 이미 해당 접두사를 보유한 인스턴스로 요청을 보냅니다.

그림 45 - 캐시 상태를 고려해 요청을 모델 인스턴스에 분배하는 캐시 인지 라우팅

멀티테넌시 보안

여러 고객이 같은 인스턴스를 공유하면 캐시 적중 여부나 응답 지연을 통해 다른 고객의 접두사 존재를 추측할 가능성이 있습니다. 시스템 프롬프트와 고객별 컨텍스트 사이에 사용자 또는 세션별 고유 ID를 넣으면 고객 간 접두사 공유를 해당 시스템 프롬프트 구간으로 제한할 수 있습니다.

구현체별 차이

현대적인 LLM 서빙 엔진에서는 프리픽스 캐싱을 기본 기능으로 제공하는 경우가 많습니다. 실제 워크로드에서는 캐시 적중률, TTFT, GPU 메모리 압력을 함께 측정해 캐시 용량과 라우팅 정책을 조정해야 합니다.

vLLM을 이용한 LLM 추론

무료 온라인 실습 랩

해당 링크를 통해 접속하실 수 있습니다 - https://learn.kodekloud.com/learn/courses/youtube-labs-vllm
실습에 대한 소개 영상도 먼저 살펴보시면 좋습니다 - https://www.youtube.com/watch?v=qdPkA5mxLhg

끝으로

이번 장에서는 실제 배포 환경에서 LLM 서비스의 성능과 비용을 개선하는 방법을 살펴보았습니다. 요청을 어떻게 묶고, 어텐션과 메모리 접근을 어떻게 최적화하며, 모델 자체의 크기를 어떻게 줄이는지가 주요 주제였습니다.

온라인 서빙에서는 배칭으로 산술 강도를 높여 GPU 활용률과 처리량을 개선할 수 있습니다. 동적 배칭에서 출발해 연속 배칭으로 요청을 유연하게 교체하고, 긴 프롬프트는 청크 프리필로 나누어 TTFT와 ITL 사이의 균형을 조정합니다.

어텐션 측면에서는 MHA, MQA, GQA, MLA처럼 KV 캐시를 줄이는 구조를 비교했고, 커널 퓨전과 FlashAttention으로 메모리 왕복과 HBM 접근을 줄이는 방법을 살펴보았습니다. PagedAttention은 KV 캐시를 블록 단위로 관리해 메모리 파편화를 줄이고, 높은 메모리 활용률을 지원합니다.

모델 압축에서는 양자화, 증류, 가지치기의 차이와 적용 조건을 정리했습니다. 실무에서는 적용이 쉬운 사후 학습 양자화를 먼저 검토하고, 이미 검증된 증류 모델이나 하드웨어가 지원하는 희소성 패턴이 있으면 추가로 활용할 수 있습니다.

프리픽스 캐싱은 반복되는 프롬프트 접두사의 KV 캐시를 재사용해 프리필 계산과 TTFT를 줄이는 방법입니다. 멀티턴 채팅, 긴 컨텍스트, 반복되는 시스템 프롬프트와 RAG 문서에서 효과가 크며, 캐시 인지 라우팅을 사용하면 여러 모델 인스턴스에서도 재사용률을 유지할 수 있습니다.

이제 단일 GPU에서 소형 LLM을 효율적으로 서빙하는 데 필요한 기본 원리를 갖추었습니다. 다음 장에서는 여러 GPU와 노드에 대규모 LLM을 분산하는 방법을 살펴보고, 개별 모델 복제본을 넘어 시스템 전체의 성능과 운영을 최적화하는 방안을 살펴보겠습니다.


  1. 참고링크: https://medium.com/@dk02315/llm-why-attention-is-expensive-and-how-gqa-mla-kv-cache-economize-the-cost-81459f8d6623 ↩︎

  2. 예를 들어 Triton Inference Server는 요청 전체를 해시해 캐시 키로 사용할 수 있습니다. ↩︎

  3. SGLang의 구현체 용어입니다. SGLang 공식문서 , vLLM-Production-Stack 문서 ↩︎