상태: 초안 (2026-10-05), 일부 실측 반영. 공식 문서로 확인한 서술을 바탕으로 쓴 글이며, 필자의 실험 환경에서 직접 잰 실측은 「실측」 절(9절)에 담았고 나머지는 문서 기준입니다. 글 끝의 「확인한 버전과 날짜」와 「확인하지 못한 것」을 먼저 읽으십시오. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.
LLM 추론의 기본과 vLLM 내부
LLM 서빙의 처리량은 연산 속도보다 두 가지가 정합니다. GPU 메모리에 KV 캐시를 얼마나 담을 수 있는가, 그리고 토큰 하나를 만들 때 읽는 가중치를 몇 개의 요청이 나눠 쓰는가입니다. 이 글은 그 이유를 산술 강도로 설명하고, PagedAttention 의 블록 관리, 접두사 캐시, chunked prefill, 선점, 운영 파라미터, 지표를 vLLM v0.30.0 의 소스와 공식 문서에서 확인한 대로 정리합니다. 글쓴이는 대규모 프로덕션 서빙을 운영한 적이 없고, 소비자용 GPU 에서 측정한 경험만 있습니다.
이 글을 읽고 나면 면접에서 설명할 수 있는 것
- prefill 은 연산 한계, decode 는 메모리 대역폭 한계인 이유를 산술 강도로 말하고, 그래서 배칭과 KV 용량이 처리량을 정한다고 설명할 수 있습니다.
- KV 캐시 크기를 식으로 계산하고, GQA, 블록(PagedAttention), 접두사 캐시, 선점이 그 용량을 어떻게 쓰는지 설명할 수 있습니다.
- 운영 파라미터 여섯 개와
/metrics를 연결해 병목을 진단하는 순서를 말할 수 있습니다.
1. 두 단계와 산술 강도
추론은 프롬프트 전체를 한 번에 처리해 KV 캐시를 채우는 prefill 과, 토큰을 하나씩 만드는 decode 로 나뉩니다. 산술 강도는 메모리에서 1바이트를 읽을 때 수행하는 연산 수(FLOP/byte)입니다. 파라미터 수가 P, 가중치가 2바이트일 때 토큰 n 개를 한 번에 처리하는 선형층의 연산량은 약 2·P·n FLOP 이고 가중치는 한 번만 읽으므로 읽는 양은 약 2·P 바이트입니다. 따라서 강도는 약 n FLOP/byte 입니다(어텐션과 KV 읽기를 뺀 근사입니다).
prefill 은 n 이 프롬프트 길이라 연산 한계입니다. decode 는 요청 하나가 n=1 이므로 동시에 처리하는 요청 수 B 가 곧 n 입니다. B 가 GPU 의 연산 성능을 메모리 대역폭으로 나눈 값(ridge point)보다 작으면 메모리 대역폭 한계입니다. 설명용 예시로 NVIDIA 가 H100 SXM 에 표기한 3.35 TB/s 와 FP16 텐서 코어 1,979 TFLOPS(희소성 포함 표기)를 쓰고 밀집 성능을 표기의 절반으로 가정하면 ridge point 는 약 295 FLOP/byte 입니다. 동시 요청이 수백 개에 이르지 않는 한 decode 시간은 가중치를 읽는 시간이 지배합니다.
필자의 실험 환경 기록에 따르면, 메모리 대역폭이 896 GB/s(NVIDIA 표기)인 노트북 GPU 에서 Qwen3.8-27B NVFP4 는 적재 크기 16.2GB 에 단일 요청 39 tok/s, 동시 4요청 합산 138 tok/s 였습니다. 39 × 16.2GB 는 약 632 GB/s 로 표기 대역폭의 70% 정도이고, 합산이 약 3.5배인 것은 스텝마다 읽는 가중치를 네 요청이 나눠 쓴 결과로 보는 것이 자연스럽습니다. 적재 크기를 스텝당 읽는 양으로 본 어림이며 측정 때의 프롬프트와 출력 길이는 기록이 없어 검증하지 못했습니다.
2. KV 캐시와 크기 식
어텐션은 앞선 모든 토큰의 key 와 value 를 다시 보므로 층마다 저장해 둡니다. 토큰 하나의 크기는 다음과 같습니다.
토큰당 바이트 = 2(K와 V) × 층 수 L × KV 헤드 수 H_kv × head 차원 d × 원소 바이트 b
값은 Hugging Face 의 config.json 에서 확인했습니다(num_hidden_layers, num_key_value_heads). 모두 BF16 이라 b=2 이고, head 차원 128 은 설정에 head_dim 이 없어 hidden_size ÷ num_attention_heads 로 계산한 값입니다.
| 모델 | L | H_kv | 토큰당 | 32,768 토큰 요청 1개 |
|---|---|---|---|---|
| Qwen2.5-1.5B-Instruct | 28 | 2 | 28 KiB | 0.875 GiB |
| Qwen2.5-7B-Instruct | 28 | 4 | 56 KiB | 1.75 GiB |
| Qwen2.5-72B-Instruct | 80 | 8 | 320 KiB | 10 GiB |
| (설명용 가정) 7B 가 MHA 라면 | 28 | 28 | 392 KiB | 12.25 GiB |
MQA(Multi-Query Attention, Shazeer 2019)는 모든 쿼리 헤드가 K, V 를 하나만 공유하고(H_kv=1), GQA(Grouped-Query Attention, Ainslie 외 2023)는 쿼리 헤드를 묶어 그룹마다 하나씩 두는 중간안입니다. GQA 논문은 MHA 체크포인트를 원래 사전학습 연산의 5%로 추가 학습하면 MQA 에 가까운 속도와 MHA 에 가까운 품질을 얻는다고 주장합니다. H_kv 가 줄면 담을 수 있는 요청 수가 늘고 decode 때 읽는 바이트도 줄며, KV 를 FP8 로 저장하면(--kv-cache-dtype fp8) b=1 이 되어 다시 절반입니다.
식은 필자의 실험 로그로 맞춰 보았습니다. Qwen2.5-1.5B 는 토큰당 28,672바이트이고, LMCache 로그의 "4,864 토큰, size 0.1299 gb" 는 4,864 × 28,672 ÷ 2^30 = 0.12988 과 일치합니다(gb 를 GiB 로 읽을 때). 같은 식으로 GPU 의 21,520 토큰은 약 0.575 GiB 인데, 서버의 가용 KV 메모리 로그 줄은 저장하지 않아 직접 대조하지 못했습니다.
3. 정적 배칭에서 continuous batching 으로
정적 배칭은 요청 묶음을 함께 시작해 함께 끝내므로 짧은 요청이 끝난 자리가 가장 긴 요청이 끝날 때까지 비어 있습니다. Orca(OSDI 2022)는 스케줄링 단위를 요청이 아니라 반복(토큰 한 번 생성)으로 바꾸어 매 반복마다 끝난 요청을 빼고 새 요청을 넣게 했습니다. 이것이 continuous batching 이며, 논문은 GPT-3 175B 에서 FasterTransformer 대비 같은 지연으로 36.9배 처리량을 주장합니다(논문 조건의 수치). vLLM V1 스케줄러 소스의 주석은 더 일반화합니다. decoding 단계도 prefill 단계도 없고 요청마다 num_computed_tokens 가 num_tokens_with_spec 을 따라잡도록 토큰을 배정할 뿐이며, 이 표현으로 chunked prefill, 접두사 캐시, 추측 디코딩을 모두 다룬다고 적혀 있습니다.
4. PagedAttention 과 블록 관리
기존 서버는 요청마다 최대 길이만큼 연속 메모리를 미리 잡았고, 논문의 그림 2(6.2절 실험의 평균 메모리 낭비 비율)에서 KV 메모리 중 실제 토큰 상태에 쓰인 비율은 저자들이 직접 구현한 Orca 세 변형(Max, Pow2, Oracle)에서 20.4~38.2%였고, vLLM 은 약 96%로 읽힙니다. Orca 의 공개 구현이 없어 변형은 저자들의 구현이며, 이 비율을 모든 기존 서버의 값으로 일반화할 수 없습니다. 같은 논문의 처리량 향상(1.7~2.7배는 Orca Oracle 대비, 2.7~8배는 Orca Max 대비)도 이 비교 대상 기준입니다. PagedAttention(SOSP 2023)은 운영체제의 가상 메모리처럼 KV 를 고정 크기 블록(기본 16 토큰)으로 나누고, 요청마다 논리 블록에서 물리 블록으로 가는 표(block table)를 두어 물리 블록을 필요할 때 하나씩 받습니다. 낭비는 요청당 마지막 블록의 빈 칸으로 줄고, 같은 접두사는 물리 블록 하나를 공유할 수 있습니다(참조 횟수와 copy-on-write).
요청 A 논리 블록 block table(A) 물리 블록 풀(GPU)
B0 [토큰 0-15] -> 물리 7 ---+
B1 [토큰 16-31] -> 물리 1 | 물리 7 : ref_cnt=2 (A, B 가 공유)
B2 [토큰 32-37] -> 물리 3 | 물리 3 : 6/16 만 참 <- 낭비는 여기뿐
요청 B: B0 -> 물리 7 -----------------+ (같은 접두사)
논문의 성능은 주장으로 구분합니다. A100 에서 OPT 13B/66B/175B 와 LLaMA 13B 를 ShareGPT, Alpaca 트레이스로 잰 결과 같은 지연에서 FasterTransformer, Orca 대비 처리량 2~4배이고, 블록 표를 거치는 탓에 어텐션 커널은 FasterTransformer 보다 20~26% 느렸다고 논문이 밝힙니다. vLLM 설계 문서의 paged_attention 쪽은 스스로 원 논문에 기반한 역사 문서이며 오늘의 코드를 기술하지 않는다고 적으므로, 개념은 유효하지만 커널 구현은 달라졌습니다. 블록 풀(소스 v0.30.0)은 빈 블록을 이중 연결 리스트 큐로 관리하고 0번 블록을 null block 으로 예약합니다. 실험 로그의 21,520 토큰은 블록 크기를 기본값 16 으로 볼 때 1,345 블록이고 쓸 수 있는 것은 1,344 블록입니다.
5. 접두사 캐시
블록의 해시는 부모 블록의 해시, 이 블록의 토큰, 추가 키(LoRA ID, 멀티모달 입력 해시, cache_salt)로 만들어 사슬을 이룹니다. 앞부분이 한 토큰만 달라도 뒤 블록이 모두 달라지므로 접두사만 적중합니다. 가득 찬 블록만 캐시되어 적중은 16 토큰의 배수이고, 전부 적중해도 logits 를 얻으려고 마지막 토큰은 다시 계산합니다(소스 주석). 요청이 끝나면 블록을 빈 큐에 반납하되 해시를 남기고 큐 앞쪽부터 LRU 로 재사용하는데, 꼬리 블록이 먼저 방출되도록 뒤에서부터 반납합니다(소스 주석). 앞쪽 블록일수록 재사용될 가능성이 높기 때문이라는 이유는 소스가 밝히지 않은 제 해석입니다.
v0.30.0 에서 enable_prefix_caching 은 모델이 지원하면 기본으로 켜집니다. V1 블로그에 따르면 V0 는 적중률이 낮을 때 CPU 부담이 커서 기본이 꺼짐이었고, V1 은 적중 0%에서도 저하가 거의 없다고 주장합니다. 접두사 캐시는 prefill 만 줄이고 decode 시간은 줄이지 못합니다(공식 문서). 멀티 테넌트에서는 cache_salt 로 신뢰 그룹을 나눕니다.
6. chunked prefill, 스케줄러, 선점
스케줄러는 매 스텝 토큰 예산(max_num_batched_tokens)을 씁니다. 소스 순서는 이렇습니다. 먼저 실행 중인 요청(decode 와 진행 중인 prefill 조각)에 토큰을 배정하고, 남은 예산으로 대기열 앞에서부터 새 요청을 받으며, 예산보다 긴 프롬프트는 잘라서 일부만 넣습니다(chunked prefill, 공식 문서상 가능한 모든 경우에 기본으로 켜짐). 긴 prefill 하나가 다른 요청의 decode 를 여러 스텝 막지 않게 하여 토큰 간 지연을 안정시키는 것이 목적입니다. Sarathi-Serve 논문(2024)은 이 아이디어로 당시 vLLM 대비 지연 SLO 하의 서빙 용량이 Mistral-7B/A100 한 장에서 2.6배라고 주장합니다.
flowchart TD
A[스텝 시작: 토큰 예산 = max_num_batched_tokens] --> B[실행 중 요청에 토큰 배정]
B --> C{새 블록을 받을 수 있는가}
C -- 예 --> D[남은 예산으로 대기열 앞에서 새 요청 받기]
C -- 아니오 --> E[가장 늦게 들어온 실행 요청 선점: 블록 반납, 대기열 맨 앞으로]
E --> C
E -.-> F[선점이 있던 스텝은 새 요청을 받지 않음]
D --> G[긴 프롬프트는 예산만큼 잘라 넣고 GPU 한 스텝 실행]
KV 블록이 모자라면 선점이 일어납니다. 소스(FCFS 정책)는 실행 목록의 맨 뒤, 즉 가장 늦게 들어온 요청을 골라 블록을 모두 반납시키고 num_computed_tokens 를 0 으로 되돌려 대기열 맨 앞에 넣습니다. 재계산(recompute)입니다. 공식 문서도 V1 은 swap 이 아니라 recompute 가 기본이며 오버헤드가 더 낮다고 설명합니다. 선점은 같은 일을 두 번 하므로 TTFT 와 처리량을 함께 해칩니다. 문서가 권하는 완화는 gpu_memory_utilization 을 올리고, max_num_seqs 나 max_num_batched_tokens 를 줄이고, tensor_parallel_size 를 늘리는 것입니다.
7. 운영 파라미터 여섯 개
기본값은 v0.30.0 소스 기준이며 버전마다 달라질 수 있습니다. --performance-mode throughput 을 주면 사용자가 지정하지 않은 두 기본값이 두 배가 됩니다(소스).
| 옵션 | 바꾸는 것 | 기본값 | 방향 |
|---|---|---|---|
--gpu-memory-utilization |
이 인스턴스가 쓸 GPU 메모리 비율. 가중치와 활성화를 뺀 나머지가 KV 풀 | 0.92 | 올리면 KV 증가, OOM 위험. 필자의 WSL2 환경에서는 WSL 이 1.3GB 를 먼저 잡아 0.95 가 기동 실패 |
--max-model-len |
요청 하나의 최대 길이 | 모델 설정에서 유도 | 줄이면 같은 KV 로 더 많은 요청을 받음. 필자의 실험에서는 262K 를 16K 로 줄임 |
--max-num-seqs |
동시 실행 요청 수 상한 | 256 (70GiB 이상 비 A100 은 1024) | 크면 처리량 증가, KV 압박과 토큰 간 지연 증가 |
--max-num-batched-tokens |
스텝당 토큰 예산 | 2048 (70GiB 이상 비 A100 은 8192, 160GiB 이상은 16384) | 크면 TTFT 감소, 같이 도는 decode 의 지연 증가 |
--tensor-parallel-size |
층 안의 가중치를 GPU 여러 장에 분할 | 1 | KV 공간 증가, 가중치 읽기 시간 감소, 통신 비용 발생 |
--enable-prefix-caching |
접두사 캐시 | 켜짐(모델이 지원하면) | 출력이 긴 워크로드에서는 이득이 작음 |
필자의 실험 배포 설정은 --gpu-memory-utilization=0.93 --max-model-len=16384 --max-num-seqs=4 --max-num-batched-tokens=2048 --kv-cache-dtype=fp8 이고, 설정에 적어 둔 실측으로 KV 캐시는 2.9GB(약 57,000 토큰)입니다. 16,384 토큰 요청이 약 3.5개 들어가는 크기라 동시 4개가 모두 길어지면 선점이 일어날 수 있다는 것은 산술 추정이며 관찰한 것이 아닙니다.
8. 지표와 /metrics
TTFT 는 요청을 보낸 뒤 첫 출력을 받기까지의 시간, TPOT 는 (전체 지연 - TTFT) ÷ (출력 토큰 수 - 1), ITL 은 스트림으로 온 출력 사이의 간격입니다. 추측 디코딩에서는 한 출력에 토큰이 여럿 담겨 ITL 과 TPOT 가 달라집니다(vLLM 벤치마크 문서). goodput 은 TTFT, TPOT 같은 SLO 를 만족한 요청만 센 처리량입니다(DistServe 논문, OSDI 2024). 평균이 아니라 p50, p95, p99 를 봅니다.
v0.30.0 소스에서 확인한 지표입니다(Prometheus 카운터는 _total 이 붙습니다).
vllm:num_requests_running,vllm:num_requests_waiting: 대기가 늘면 포화 신호입니다.vllm:kv_cache_usage_perc: 1 에 가까우면 KV 포화입니다.vllm:num_preemptions_total이 늘면 재계산 중입니다.vllm:prefix_cache_queries_total,vllm:prefix_cache_hits_total: 적중률은 hits ÷ queries 입니다.vllm:time_to_first_token_seconds,vllm:inter_token_latency_seconds,vllm:request_queue_time_seconds,vllm:request_prefill_time_seconds,vllm:request_decode_time_seconds: 지연을 대기, prefill, decode 로 나눕니다.
대기가 늘고 kv_cache_usage_perc 가 1 에 가까우며 선점이 늘면 KV 부족입니다. 대기가 늘지만 사용률이 낮으면 max_num_seqs 나 토큰 예산이 병목입니다. 히스토그램은 버킷 경계로 나뉘어 있어(TTFT 는 0.5, 0.75, 1.0초 등) histogram_quantile 의 p99 는 버킷 폭만큼 거칩니다.
9. 실측: 순환 접근에서 LRU 가 무너졌다
필자가 잰 실측입니다. 8GB급 노트북 GPU 한 장, 이미지 lmcache/vllm-openai:latest(vLLM 0.29.0, LMCache 0.5.5), 모델 Qwen2.5-1.5B-Instruct, --gpu-memory-utilization 0.60 --max-model-len 8192 --max-num-seqs 4 --enable-prefix-caching 입니다. 서버 로그의 GPU KV 캐시는 21,520 토큰이었습니다. 난수 낱말로 만들어 서로 접두사가 겹치지 않는 문서 10개(각 약 5,011 토큰, 합 약 50,000 토큰)를 순서대로, 같은 순서로 다시, 거꾸로 읽혔고 요청은 하나씩 보냈습니다.
| 설정 | 패스 | TTFT p50 (ms) | 접두사 캐시 적중 (토큰) |
|---|---|---|---|
| baseline | 1 처음 | 575.5 | 0 |
| baseline | 2 같은 순서로 다시 | 576.2 | 0 |
| baseline | 3 거꾸로 | 505.4 | 21,488 |
| LMCache(CPU 1.5GB) | 2 같은 순서로 다시 | 48.6 | GPU 0, 외부 48,640 |
문서 합계가 GPU 용량의 2.3배라서, LRU 에서 같은 순서로 다시 읽으면 필요한 순간마다 그 문서가 방금 방출된 뒤여서 적중이 0 이 됩니다. 거꾸로 읽는 3차의 적중 21,488 토큰은 쓸 수 있는 1,344 블록 중 1,343 블록(×16)입니다. 이 설명을 10절의 블록 LRU 모형으로 확인했고 모형도 1차 0, 2차 0, 3차 21,488 을 냈습니다. 모형이 실측과 맞았다는 것은 설명이 일관된다는 증거이지 증명은 아닙니다.
LMCache 가 KV 를 CPU 메모리에 내려 두면 2차 TTFT 는 48.6ms 였습니다. 실험 기록은 문서당 4,864 토큰을 CPU 에서 불러오는 데 로그 시각 기준 약 8~9ms, 같은 토큰을 새로 계산하는 데 약 560ms 가 걸렸다고 적었습니다. 처음 읽을 때는 저장 비용으로 TTFT p50 이 575.5 에서 597.8ms 로 약 4% 늘었습니다.
한계를 그대로 밝힙니다. 동시성이 1 이라 요청이 겹치는 상황은 재지 않았습니다. 모델이 1.5B 이고 GPU 가 한 장(8GB)이며 문서가 합성이어서, 큰 모델에서 이득이 더 클 것이라는 실험 기록의 예상은 이 측정이 보여 주는 것이 아닙니다. 한 번 잰 값이고 표준편차가 없으며 패스당 요청이 10개라 p95 는 사실상 최댓값입니다. 이득은 PagedAttention 이 아니라 CPU 오프로딩이 낸 것이고 접두사가 길게 반복되는 최적 시나리오입니다. 실험 뒤에 vLLM 문서에서 자체 OffloadingConnector 를 확인했지만 같은 조건으로 재지 않았습니다.
10. 직접 해 보기
1, 2번은 로컬 계산이거나 읽기 전용입니다.
# 1) 클러스터 없이: KV 크기 계산
curl -sL https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/resolve/main/config.json | python3 -c '
import json,sys; c=json.load(sys.stdin)
d=c["hidden_size"]//c["num_attention_heads"]
b=2*c["num_hidden_layers"]*c["num_key_value_heads"]*d*2
print(b, "바이트/토큰", b*32768/2**30, "GiB/32K 토큰")'
# 2) 읽기 전용 확인(vLLM 서버가 떠 있는 클러스터에서. 서버를 새로 켜는 일은 변경이라 하지 않는다)
kubectl logs <vLLM 파드 이름> | grep -E "GPU KV cache size|Maximum concurrency"
kubectl exec <vLLM 파드 이름> -- python3 -c "import urllib.request as u;print(u.urlopen('http://127.0.0.1:8000/metrics').read().decode())" | grep -E "^vllm:(num_requests|kv_cache_usage|num_preemptions|prefix_cache)"
3번은 블록 LRU 모형입니다. 실측이 아니라 제가 단순화한 모형입니다(블록 16토큰, 꼬리 먼저 방출, 부분 블록은 해시 없음).
from collections import OrderedDict
BS, TOTAL, DOCS, N = 16, 1344, 10, 5033 # 쓸 수 있는 블록 = 전체 1345 - null block
free, fresh = OrderedDict(), TOTAL # free: 해시가 붙은 빈 블록, 앞쪽이 먼저 방출됨
def serve(d):
global fresh
hs = [(d, i) for i in range(N // BS)] # 가득 찬 블록의 해시
hit = 0
while hit < len(hs) and hs[hit] in free and (hit + 1) * BS <= N - 1:
hit += 1
for h in hs[:hit]:
del free[h] # 적중한 블록은 사용 중
for _ in range(-(-N // BS) - hit): # 모자란 블록을 새로 받는다
if fresh: fresh -= 1
else: free.popitem(last=False) # 가장 오래된 블록을 내보냄
for h in reversed(hs):
free[h] = 1 # 끝나면 꼬리부터 큐에 넣는다
fresh += N % BS > 0 # 부분 블록은 바로 재사용
return hit * BS
for name, order in (("1차", range(DOCS)), ("2차", range(DOCS)), ("3차 거꾸로", reversed(range(DOCS)))):
print(name, sum(serve(d) for d in order))
4번은 임시 파드 재현입니다. 원 실험은 GPU 1장과 메모리 6Gi 를 쓰는 임시 파드로 돌렸습니다. 위험은 같은 노드의 다른 워크로드와의 메모리 경합과 GPU 점유입니다. 파드는 남더라도 1시간 뒤 스스로 종료되게 하고, 객체 삭제는 사람이 직접 실행합니다. 이 글은 이 재현을 다시 실행하지 않았습니다.
11. 면접에서 나올 만한 질문
- decode 가 왜 메모리 대역폭 한계입니까? 요청 하나가 토큰 하나를 만들 때 모든 가중치를 읽지만 가중치당 곱셈은 한 번 정도라 산술 강도가 동시 요청 수 B 와 비슷합니다. ridge point(H100 기준 수백)보다 작아 연산 유닛이 유휴 상태이고, 요청을 모아 가중치 읽기를 나눠 쓰는 배칭이 처리량을 키웁니다.
- KV 캐시 크기를 계산하고 GQA 의 효과를 말해 보십시오. 2 × 층 수 × KV 헤드 수 × head 차원 × 바이트가 토큰당 크기입니다. Qwen2.5-7B 는 28 × 4 × 128 이라 56KiB 이고, 쿼리 헤드 28개 전부가 KV 를 가진 MHA 였다면 7배인 392KiB 이므로 GQA 가 동시에 담을 요청 수를 그만큼 늘립니다.
- PagedAttention 은 무엇을 해결하고 블록 크기는 어떻게 정합니까? 최대 길이로 연속 메모리를 미리 잡던 방식의 조각화와 낭비를 블록 단위 동적 할당으로 없앴습니다. 블록이 크면 커널 효율은 좋지만 마지막 블록의 낭비가 커지고 작으면 반대이며, 논문은 이 절충으로 기본 16 을 택했습니다.
- KV 캐시가 가득 차면 vLLM 은 어떻게 하고 어떻게 알아챕니까? 가장 늦게 들어온 실행 요청을 선점해 블록을 반납시키고 나중에 처음부터 재계산합니다.
kv_cache_usage_perc가 1 에 가까워지고num_preemptions_total이 늘며 대기열이 길어지고, 대응은 사용률 상향,max_num_seqs축소, KV FP8, 텐서 병렬 증설입니다. - 접두사 캐시를 켰는데 적중이 안 납니다. 무엇을 봅니까?
prefix_cache_hits_total ÷ queries_total로 실제 적중률을 보고, 프롬프트 앞부분에 타임스탬프나 요청 ID 같은 가변 토큰이 있는지 봅니다. 해시 사슬이라 앞이 다르면 뒤가 모두 어긋납니다. 재사용 간격 동안 읽는 총량이 KV 용량을 넘어 LRU 가 무너졌을 수도 있고, 제 실험이 그 경우였습니다.
12. 흔한 오해
- "PagedAttention 은 어텐션 계산을 빠르게 하는 알고리즘이다." 메모리 관리 기법입니다. 논문도 커널은 20~26% 느려졌다고 밝히며, 이득은 낭비 없는 메모리로 더 큰 배치를 돌리는 데서 나옵니다.
- "
--gpu-memory-utilization을 올리면 빨라진다." KV 풀이 커질 뿐이어서 동시 요청이 많아 KV 가 모자랄 때만 효과가 있습니다. - "접두사 캐시는 응답 전체를 빠르게 한다." prefill 만 줄이므로 출력이 긴 워크로드에서는 이득이 작습니다.
- "실험에서 나온 12배는 vLLM 의 성능이다." 최적 시나리오에서 LMCache 의 CPU 오프로딩이 낸 이득이고 동시성 1, 1.5B 모델에서 한 번 잰 값입니다.
- "평균 TTFT 가 낮으면 충분하다." 서비스 품질은 p99 가 정하고, 평균은 선점으로 늘어진 꼬리를 숨깁니다.
참고 자료
실제로 열어 본 것만 적었습니다.
- 논문: PagedAttention https://arxiv.org/abs/2309.06180 (본문은 ar5iv 로 확인), Orca https://www.usenix.org/conference/osdi22/presentation/yu , Sarathi-Serve https://arxiv.org/abs/2403.02310 , DistServe https://arxiv.org/abs/2401.09670 , GQA https://arxiv.org/abs/2305.13245 , MQA https://arxiv.org/abs/1911.02150
- vLLM V1 블로그: https://vllm.ai/blog/2025-01-27-v1-alpha-release
- vLLM 문서: https://docs.vllm.ai/en/latest/ 아래 configuration/optimization, design/prefix_caching, design/paged_attention, design/metrics, benchmarking/cli, features/kv_offloading_usage, serving/parallelism_scaling
- vLLM v0.30.0 소스(https://github.com/vllm-project/vllm/tree/v0.30.0):
vllm/config/cache.py,config/scheduler.py,engine/arg_utils.py,v1/core/sched/scheduler.py,v1/core/block_pool.py,v1/core/kv_cache_manager.py,v1/metrics/loggers.py - 모델 설정: https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct 외 7B, 72B. NVIDIA: https://www.nvidia.com/en-us/data-center/h100/ , https://www.nvidia.com/en-us/geforce/laptops/50-series/
확인한 버전과 날짜, 확인하지 못한 것
- 확인일은 2026-10-05 입니다. vLLM 최신 안정 릴리스는 v0.30.0(2026-09-22)이고 소스와 기본값은 이 태그 기준입니다. 필자의 실험은 0.29.0 으로 돌렸습니다. v0.29.0 에서 Model Runner V2 가 모든 모델의 기본이 되었고 V1 러너는 v0.32 제거를 목표로 폐기 예고 상태입니다(릴리스 노트).
- 확인하지 못한 것: H100 의 밀집 FP16 수치(NVIDIA 페이지에는 희소성 포함 값만 있어 절반으로 가정), 27B 실측의 측정 조건, 로그의
gb가 GiB 라는 점(수치 일치로 추정), 실험 서버의 블록 크기(기본 16 으로 가정), 3차 적중이 1,343 블록인 이유의 로그 확인(모형으로만 설명), 접두사 캐시를 끄는 옵션의 정확한 형식.