vLLM으로 모델을 띄워 놓고 "이 GPU 한 장으로 동시 사용자를 몇 명까지 받을 수 있지?", "gpu_memory_utilization이나 kv_cache_dtype을 바꾸면 얼마나 달라지지?"를 계산하고 계신가요? 결론부터 말하면 동시 처리 수는 (GPU 메모리 × 사용 비율 − 가중치 − 활성값) ÷ 토큰당 KV 바이트 ÷ 요청당 평균 토큰 수로 거의 정해져요. H100 80GB 한 장에 Qwen3-8B를 올리면 요청 평균 1,500토큰 기준 약 263개, KV 캐시를 fp8로 저장하면 약 526개예요.
블록 테이블이 어떻게 돌아가는지는 알고 있다고 보고, 이 글은 설정을 고르는 데 필요한 숫자만 다뤄요. config.json으로 토큰당 KV 크기를 계산하고, 공식 가격표 단가로 월 GPU 비용을 낸 뒤, 어떤 값을 언제 바꿀지 정리해요.
토큰당 KV 캐시 크기 계산 공식
디코드 단계에서 모델은 지금까지 나온 모든 토큰의 Key·Value 벡터를 층마다 보관해요. 그래서 토큰 하나가 차지하는 KV 메모리는 다음과 같아요.
토큰당 KV 바이트 = 2(K와 V) × 층 수 × KV 헤드 수 × 헤드 차원 × 자료형 바이트
여기서 쓰는 값은 모두 모델의 config.json에 있어요. num_hidden_layers(층 수), num_key_value_heads(KV 헤드 수), head_dim 또는 hidden_size ÷ num_attention_heads(헤드 차원)예요. 자료형은 bf16·fp16이면 2바이트, fp8이면 1바이트예요. 주의할 점은 어텐션 헤드 수가 아니라 KV 헤드 수를 넣어야 한다는 거예요. GQA(그룹 쿼리 어텐션)를 쓰는 요즘 모델은 KV 헤드가 어텐션 헤드보다 훨씬 적어서, 어텐션 헤드 수를 넣으면 몇 배 크게 계산돼요.
Hugging Face에 공개된 설정 파일(Qwen3-8B config.json, Qwen2.5-7B-Instruct config.json 등)로 직접 계산한 값이에요. 비교용으로 PagedAttention 논문(Kwon 외, SOSP 2023)이 예로 든 OPT-13B도 넣었어요. OPT-13B는 GQA가 없는 옛 구조라 KV 헤드가 40개 그대로예요.
| 모델 | 층 | KV 헤드 | 헤드 차원 | 토큰당 KV (bf16) | 1,500토큰 요청 1개 | 32,768토큰 요청 1개 |
|---|---|---|---|---|---|---|
| Qwen2.5-7B | 28 | 4 | 128 | 56KiB | 82MiB | 1.75GiB |
| Mistral-7B v0.3 | 32 | 8 | 128 | 128KiB | 188MiB | 4.0GiB |
| Qwen3-8B | 36 | 8 | 128 | 144KiB | 211MiB | 4.5GiB |
| Qwen2.5-32B | 64 | 8 | 128 | 256KiB | 375MiB | 8.0GiB |
| Qwen2.5-72B | 80 | 8 | 128 | 320KiB | 469MiB | 10.0GiB |
| OPT-13B (논문 예시) | 40 | 40 | 128 | 800KB | 약 1.2GB | 약 26.8GB |
MiB·GiB(1024 단위)와 MB·GB(1000 단위)를 섞으면 수십 % 어긋난 용량 계획이 나와요. 단위가 헷갈리면 데이터 용량 변환기에서 1000 기준과 1024 기준을 나란히 확인해 보세요. 한국어 서비스라면 요청당 토큰 수 자체가 영어보다 많게 나오는 경우가 흔해서, 평균 토큰 수는 한국어 LLM 토큰 비용 비교에서 정리한 것처럼 실제 토크나이저로 세야 해요.
GPU 한 장에 동시 요청이 몇 개 들어가나
vLLM은 시작할 때 가중치를 올리고 메모리를 측정한 뒤, gpu_memory_utilization 한도 안에서 남은 공간을 전부 KV 블록으로 잘라 둬요. vLLM Engine Arguments 문서에 따르면 이 값의 기본은 0.92이고, 인스턴스마다 따로 적용돼요.
H100 80GB 한 장에 Qwen3-8B(bf16 가중치 8,190,735,360개 × 2바이트 ≈ 15.26GiB)를 올리는 경우로 계산해 볼게요. 계산을 단순하게 하려고 GPU 메모리를 80GiB로, 활성값·CUDA 그래프 몫을 4GiB로 가정했어요. 활성값은 모델·배치 설정마다 달라서 실제 값은 시작 로그로 확인해야 해요.
- 쓸 수 있는 메모리: 80 × 0.92 = 73.6GiB
- KV 블록 몫: 73.6 − 15.26 − 4 = 약 54.34GiB
- 담을 수 있는 토큰: 54.34GiB ÷ 144KiB = 약 395,717토큰
- 요청당 평균 입력 1,000 + 출력 500 = 1,500토큰이면 16토큰 블록 94개(1,504슬롯)가 필요해요. 395,717 ÷ 1,504 = 약 263개
같은 메모리로 요청마다 최대 길이를 통째로 예약하는 방식이라면 숫자가 확 줄어요. 아래 차트가 그 비교예요.
이 차이는 이론만이 아니에요. PagedAttention 논문은 기존 시스템의 KV 메모리 중 실제 토큰이 쓴 비율이 20.4~38.2%뿐이었다고 측정했고, OPT-13B·ShareGPT 실험에서 vLLM이 동시에 처리한 요청 수가 출력 길이를 미리 안다고 가정한 Orca(Oracle)보다 2.2배, 최대 길이로 예약하는 Orca(Max)보다 4.3배 많았다고 적었어요. vLLM 공식 블로그는 블록 방식의 낭비가 4% 미만이라고 밝혀요.
위 계산은 모든 요청이 마지막 길이까지 자란 순간 기준이라 보수적이에요. 반대로 max_num_seqs나 연산량이 먼저 막히면 이 숫자까지 못 가요. "이보다 많이는 못 받는다"는 기준선으로 쓰세요.
GPU 크기가 작으면 가중치가 다 먹는다
같은 Qwen2.5-7B라도 GPU 크기에 따라 KV 몫이 크게 달라져요. Lambda GPU 클라우드 가격표의 1장 인스턴스 단가(2026-10-25 확인, 세금 별도)로 계산했어요.
| GPU (1장) | 시간당 요금 | KV 블록 몫 | 동시 요청 (bf16) | 동시 요청 (fp8 KV) |
|---|---|---|---|---|
| A10 24GB | 1.29달러 | 약 3.9GiB | 48 | 96 |
| H100 SXM 80GB | 4.29달러 | 약 55.4GiB | 689 | 1,379 |
A10에서는 메모리의 60% 가까이를 가중치(약 14.2GiB)가 차지해서 KV 몫이 4GiB가 안 돼요. 요금은 H100의 30%인데 메모리 기준 동시 처리는 7% 수준이라, 동시 요청이 많은 서비스라면 슬롯 하나당 단가가 오히려 비싸요. 연산 속도는 H100이 훨씬 빠르니 이 표는 메모리 쪽 비교로만 읽으세요. 같은 식으로 32B를 H100 한 장에 bf16으로 올리면 KV 몫이 약 8.6GiB라 평균 1,500토큰 요청 23개가 한계예요.
월 GPU 비용으로 바꿔 보기
메모리 상한을 GPU 대수로 바꾸면 비용 차이가 바로 보여요. 피크 때 평균 1,500토큰 요청 500개를 동시에 담아야 한다고 두고, Qwen3-8B·H100 단가 시간당 4.29달러 × 730시간(한 달)으로 계산했어요. 필요한 GPU 수는 500을 앞 차트의 장당 동시 요청 수로 나눠 올림한 값이에요.
32K 통째 예약은 극단적인 예지만, 출력 길이를 모를 때 넉넉히 잡는 습관이 얼마나 비싼지 보여 줘요.
처리량 쪽으로는 논문 초록이 같은 지연에서 FasterTransformer·Orca 대비 2~4배라고 밝혀요. 같은 GPU 시간으로 토큰을 2~4배 만드니 출력 토큰당 비용은 25~50% 수준이 되는 셈이에요. vLLM 블로그의 Hugging Face Transformers 대비 최대 24배는 2023년 당시 버전끼리 비교한 숫자라 지금 그대로 대입하면 안 돼요.
지연과 품질에서 내는 값
지연. 블록 테이블을 따라가는 어텐션 커널은 공짜가 아니에요. 논문 측정에서 어텐션 커널 지연이 FasterTransformer 구현보다 20~26% 길었어요. 다만 이 비용은 어텐션 연산에만 붙고, 배치를 키워 얻는 이득이 훨씬 커서 같은 지연에서 처리량이 2~4배 나왔어요. 요청 하나를 혼자 돌리는 벤치마크로만 재면 이 이득이 보이지 않으니, 동시 부하로 재야 해요.
선점 비용. 블록을 미리 예약하지 않으니 생성 도중 빈 블록이 동날 수 있어요. 그러면 vLLM은 일부 요청을 내보내고, 나중에 다시 계산해요. vLLM 최적화 문서는 V1에서 선점된 요청을 재계산(RECOMPUTE)으로 되살린다고 적고, 선점이 잦으면 gpu_memory_utilization을 올리거나 max_num_seqs·max_num_batched_tokens를 낮추라고 안내해요. 재계산은 그때까지의 토큰을 프롬프트처럼 다시 프리필하는 거라, 1,200토큰까지 생성하다 밀려난 요청은 1,200토큰을 다시 처리해요. 그 요청의 지연이 튀고, 다른 요청이 쓸 연산도 그만큼 빼앗겨요. 논문 실험에서는 블록이 작을수록 스왑(CPU로 복사) 비용이 커졌고, 재계산 비용은 블록 크기와 무관하게 일정했어요.
품질. 블록 단위 할당 자체는 근사가 아니라서 출력이 바뀌지 않아요. 품질이 걸리는 건 kv_cache_dtype=fp8이에요. vLLM Quantized KV Cache 문서는 보정 없이 fp8만 지정하면 모든 스케일이 1.0으로 잡히고, 정확도를 높이려면 llm-compressor로 데이터셋 보정을 하라고 권장해요. FlashAttention 3 백엔드에서는 쿼리까지 FP8로 계산한다는 점, 슬라이딩 윈도 층처럼 양자화에 민감한 층은 --kv-cache-dtype-skip-layers로 뺄 수 있다는 점도 적혀 있어요. 문서에 품질 저하 폭이 숫자로 나와 있지 않으니, 켜기 전후로 기존 평가 세트를 꼭 다시 돌려야 해요.
설정값별 선택 가이드
| 설정 | 바꾸면 생기는 일 | 추천 |
|---|---|---|
block_size | 작으면 커널 병렬성 손해, 크면 마지막 블록 낭비 증가 | 기본값 유지 (논문 기본 16) |
gpu_memory_utilization | 0.01 올릴 때마다 H100에서 약 0.8GiB, Qwen3-8B 기준 약 5,825토큰 추가 | 혼자 쓰는 GPU면 기본 0.92, 같이 쓰면 나눠 배정 |
kv_cache_dtype | fp8이면 토큰당 바이트 절반, 담는 토큰 2배 | 용량이 모자랄 때 1순위 후보, 품질 평가 필수 |
max_model_len | 최대 길이 요청 하나가 KV에 다 들어가야 서버가 떠요 | 실제로 필요한 길이로 낮춰 두기 |
max_num_seqs | 한 스텝에 묶는 시퀀스 상한 | 선점이 잦으면 낮추기 |
| 접두사 캐싱 | 같은 앞부분의 KV 블록을 재사용 | 공통 시스템 프롬프트가 길면 켜 두기 |
tensor_parallel_size | 가중치를 여러 GPU로 나눠 장당 KV 몫이 늘어요 | 큰 모델이 한 장에서 KV 몫이 너무 작을 때 |
블록 크기는 손댈 이유가 거의 없어요. 마지막 블록 빈칸은 평균 (B − 1) ÷ 2토큰이라, 직접 계산하면 블록 16에서 요청당 7.5토큰(1,500토큰 요청의 0.5%, 300토큰 요청의 2.5%)이에요. 블록 128이면 63.5토큰으로 각각 4.2%, 21.2%까지 올라가요. 짧은 요청이 많을수록 큰 블록이 손해예요.
논문 7.2절도 긴 대화(ShareGPT)에서는 16~128, 짧은 요청(Alpaca)에서는 16·32만 좋았다며 기본값을 16으로 정했어요. Engine Arguments 문서상 block_size는 지정하지 않으면 기본값을 쓰니, 특별한 이유가 없으면 비워 두세요.
병렬 샘플링(n>1)과 빔 서치. 한 프롬프트에서 답을 여러 개 뽑으면 프롬프트 블록을 공유해서 메모리가 n배로 늘지 않아요. 논문 측정(OPT-13B)에서 공유로 아낀 블록 비율은 병렬 샘플링 6.1~9.8%(Alpaca)·16.2~30.5%(ShareGPT), 빔 서치 37.6~55.2%(Alpaca)·44.3~66.3%(ShareGPT)였어요. 용량을 계산할 때 n개 답의 출력 토큰은 따로, 프롬프트 토큰은 한 번만 세면 돼요.
초안 모델을 같이 올린다면. 투기적 디코딩용 초안 모델은 가중치와 KV를 한 벌 더 써서 타깃 모델의 KV 몫을 줄여요. 그 비용 비교는 투기적 디코딩 방식 고르기에 정리해 두었어요.
상황별 추천
| 상황 | 먼저 할 일 | 이유 |
|---|---|---|
| 사내 도구, 동시 사용자 수십 명 | 기본값 그대로 | H100 한 장 메모리 상한이 수백 개라 여유가 커요 |
| 동시 요청이 메모리 상한에 닿음 | kv_cache_dtype=fp8 시험 → 품질 평가 | 같은 GPU에서 담는 토큰이 2배예요 |
| 긴 문서 RAG, 입력이 수만 토큰 | max_model_len을 실제 필요 길이로, 접두사 캐싱 확인 | 요청 하나가 수 GiB라 동시 수가 한 자릿수로 떨어져요 |
| 32B 이상을 GPU 한 장에 | 가중치 양자화 또는 텐서 병렬 2장 | bf16 32B면 KV 몫이 8.6GiB뿐이에요 |
| 다른 프로세스와 GPU 공유 | 인스턴스마다 gpu_memory_utilization 나눠 배정 | 문서상 이 값은 인스턴스별 한도예요 |
| 선점 경고가 계속 찍힘 | 사용 비율 올리기 또는 max_num_seqs 낮추기 | 재계산은 밀려난 토큰을 처음부터 다시 처리해요 |
도입 체크리스트
- 토큰당 KV 바이트를 계산해요. 어텐션 헤드 수가 아니라 KV 헤드 수를 넣었는지 다시 봐요.
- 요청당 평균 토큰과 피크 동시 요청 수를 실제 데이터로 재요. 한국어 비중이 높으면 실제 토크나이저로 세고, 평균과 함께 p95도 봐 둬요.
- 시작 로그의 KV 캐시 크기·최대 동시 처리 배수를 계산값과 비교해요. 크게 다르면 활성값 가정이나 단위(GiB/GB)가 틀린 거예요.
- 설정은 하나씩 바꾸고 같은 부하로 재요. TTFT·TPOT의 p50·p99와 초당 토큰을 기록해요.
- 운영 지표를 걸어 둬요. KV 사용률이 피크에 붙어 있거나 선점 횟수가 늘면 용량을 늘릴 때예요. fp8을 켰다면 품질 평가도 다시 돌려요.
자주 묻는 질문
nvidia-smi에 메모리가 90% 넘게 차 있는데 꽉 찬 건가요?
아니에요. vLLM은 시작할 때 gpu_memory_utilization(기본 0.92)만큼 미리 잡아서 KV 블록으로 잘라 두기 때문에, 요청이 하나도 없어도 사용량이 높게 보여요. 실제로 블록이 얼마나 쓰이는지는 vLLM의 통계 로그나 Prometheus 지표의 KV 캐시 사용률로 봐야 해요.
메모리를 아끼려고 gpu_memory_utilization을 낮추면 좋은가요?
다른 프로세스와 GPU를 나눠 써야 하는 게 아니면 손해예요. H100에서 0.01 낮출 때마다 약 0.8GiB, Qwen3-8B 기준 약 5,825토큰분의 블록이 사라져 동시 처리 수가 줄고 선점이 잦아져요. 공식 문서도 선점이 잦으면 이 값을 올리라고 안내해요.
kv_cache_dtype을 fp8로 바꾸면 품질이 떨어지나요?
모델과 보정 방법에 따라 달라서 미리 숫자로 말하기 어려워요. vLLM 문서는 보정 없이 fp8만 지정하면 스케일이 1.0으로 잡히고, 정확도를 원하면 llm-compressor로 데이터셋 보정을 하라고 권장해요. 켜기 전후로 같은 평가 세트를 돌려 비교하는 게 가장 확실해요.
block_size를 1로 줄이면 낭비가 0이 되니 가장 좋지 않나요?
낭비는 줄지만 커널이 블록 테이블을 토큰마다 따라가야 해서 GPU 병렬성이 떨어져요. 논문 실험에서도 너무 작은 블록은 성능이 나빴고, 스왑 비용도 블록이 작을수록 컸어요. 기본값 16에서 요청당 평균 낭비는 7.5토큰이라 1,500토큰 요청이면 0.5%뿐이에요.
n=4로 답을 네 개 뽑으면 KV 메모리도 네 배가 드나요?
아니에요. 프롬프트 블록은 참조 횟수로 공유되고, 각 답이 새로 만드는 출력 토큰만 따로 블록을 써요. 마지막 프롬프트 블록에 처음 쓸 때 그 블록 하나만 복사돼요. 용량 계획은 프롬프트 1벌 + 출력 n벌로 계산하면 돼요.
정리
- 동시 처리 수의 메모리 상한은 (GPU 메모리 × 사용 비율 − 가중치 − 활성값) ÷ 토큰당 KV 바이트 ÷ 요청당 평균 토큰이에요. 토큰당 KV는 config.json의 층·KV 헤드·헤드 차원으로 계산해요.
- H100 한 장, Qwen3-8B, 평균 1,500토큰 기준으로 블록 단위 할당은 약 263개, fp8 KV는 약 526개를 담아요. 최대 길이 32K를 통째로 예약하면 12개라, 500명 동시 처리에 드는 GPU가 42장과 2장으로 갈려요.
- 블록 크기는 16 그대로 두고, 용량이 모자라면 fp8 KV(품질 평가 필수) →
max_model_len조정 → 텐서 병렬 순서로 검토하세요. - MiB·GiB 단위가 헷갈리면 데이터 용량 변환기로 먼저 맞춘 뒤 계산하면 실수가 줄어요.