llm
BF16이 이긴 콜드 캐시 서빙 비교 - Qwen3-14B AWQ의 더 큰 KV 캐시가 TTFT를 줄이지 못한 이유
Qwen3-14B를 BF16과 AWQ로 각각 서빙해 콜드 캐시에서 처리량과 TTFT를 재봤습니다
2026.09.06 · 9 min read
가중치를 줄이면 서빙도 빨라진다는 기대
AWQ는 가중치를 4bit로 압축합니다. 같은 GPU 메모리에서 가중치가 차지하는 자리가 줄면 남는 자리는 KV 캐시로 갑니다. 여기서 나오는 자연스러운 기대는 두 가지입니다.
- KV 캐시가 커지면 더 많은 요청을 동시에 담을 수 있습니다.
- 캐시가 크면 프리픽스 재사용이 늘어나 첫 토큰까지 걸리는 시간(TTFT)도 줄어들 것입니다.
이번 실험은 이 두 번째 기대만 따로 떼어 검증합니다. Qwen3-14B의 BF16 버전과 AWQ 버전에 정확히 같은 트래픽을 보내고, 처리량과 TTFT·TPOT를 나란히 잽니다.
실험 설계 - 콜드 캐시를 어떻게 통제했나
워크로드와 시드
두 조건 모두 vllm bench serve의 prefix_repetition 데이터셋을 씁니다.
- 프리픽스 길이 3,072 토큰
- 서픽스 64 토큰
- 프리픽스 종류 160개
- 출력 길이 32 토큰
- 프롬프트 480개
- request-rate 10 req/s(포아송, burstiness 1.0)
- max-concurrency 32
- temperature 0 완전히 동일합니다.
--ignore-eos=True가 켜져 있어 출력이 32 토큰으로 강제 고정됩니다. 그 결과 이번 트래픽은 입력 1,505,280 토큰 대 출력 15,360 토큰, 대략 98:1로 prefill이 지배하는 워크로드입니다. TTFT 쪽 결과가 이 글의 핵심이 되는 이유이기도 합니다.
서버 환경
- 두 조건 모두 이미지
docker.io/vllm/vllm-openai:v0.11.0을 쓰고, 모델명만 다를 뿐--max-model-len=8192,--gpu-memory-utilization=0.90,--enable-prefix-caching,--block-size=16, replica 2개·GPU 1개씩까지 동일한 설정입니다.
Replica 분포와 경로
이번 벤치마크에서는 클라이언트가 파드를 직접 겨냥하지 않고 k8s Service 하나에 480개 요청을 전부 보내고 Service가 2개 replica로 분산시켜 진행했습니다.
| 조건 | Replica 요청 분포 | 집계 prefix-cache 히트율 |
|---|---|---|
| BF16 | 243 / 237 | 30.4% |
| AWQ | 238 / 242 | 39.2% |
(BF16: 7w92m 243건·히트율 31.7%, pm7wp 237건·히트율 28.9%. AWQ: 4qf68 238건·히트율 38.4%, t9p25 242건·히트율 40.1%. 집계는 두 pod의 hits/queries를 합산한 값입니다.)
클라이언트 (seed 1788646381 · 480 requests)
│
├──▶ k8s Service vllm-qwen3-14b
│ ├──▶ BF16 replica 7w92m
│ └──▶ BF16 replica pm7wp
│
└──▶ k8s Service vllm-qwen3-14b-awq
├──▶ AWQ replica 4qf68
└──▶ AWQ replica t9p25
4개 replica 각각에서
└──▶ vLLM /metrics 카운터 before·after 스냅샷
클라이언트는 파드가 아니라 k8s Service를 호출하고, Service가 2개 replica로 요청을 나눕니다. 관측은 Service 바깥에서 클라이언트가 보는 지표(TTFT·TPOT·처리량)와 각 replica 안에서 찍는 /metrics 카운터, 두 갈래로 수집됩니다.
결과 - 처리량과 지연
| 지표 | BF16 | AWQ | AWQ 변화 |
|---|---|---|---|
| 성공 요청 수 | 480 | 480 | 동일 |
| 벤치마크 소요 시간 | 85.51 s | 91.40 s | +6.9% |
| 총 토큰 처리량 | 17,782.44 tok/s | 16,637.20 tok/s | -6.4% |
| Median TTFT | 621.90 ms | 796.22 ms | +28.0% |
| P99 TTFT | 3,481.34 ms | 5,914.17 ms | +69.9% |
| Mean TPOT | 154.86 ms | 159.14 ms | +2.8% |
| Median TPOT | 160.89 ms | 161.32 ms | +0.3% |
| Mean E2E 지연 | 5,558.82 ms | 5,965.20 ms | +7.3% |
| P99 E2E 지연 | 9,680.65 ms | 14,629.57 ms | +51.1% |
위 표의 모든 지표에서 BF16 쪽이 낫습니다. 격차는 TPOT보다 TTFT에서 훨씬 큽니다. Mean TPOT는 2.8% 차이인데 Median TTFT는 28.0%, P99 TTFT는 69.9% 차이입니다.
관찰 - 히트율은 AWQ가 높았지만 TTFT는 더 느렸다
AWQ는 두 replica 모두 BF16보다 prefix-cache 히트율이 높습니다(집계 39.2% vs 30.4%). 프리픽스를 재사용한 비율이 더 높았다는 뜻인데, 그럼에도 TTFT는 AWQ가 더 나쁩니다. 캐시를 더 잘 맞혔는데 응답은 더 늦었습니다.
아직 모르는 것 - AWQ의 토큰 간 간격이 이상하게 갈라집니다. AWQ의 median ITL은 21.05 ms인데 mean ITL은 159.14 ms입니다. BF16은 median 190.12 ms, mean 154.86 ms로 이런 괴리가 없습니다. AWQ 쪽 토큰 간 간격 분포가 강하게 이봉(bimodal)이라는 뜻으로 읽히지만, 저장된 아티팩트에 개별 요청 단위 타임라인이 없어 원인은 확인하지 못합니다.
아직 모르는 것 - 실측 최대 동시 요청 수가 설정값을 넘습니다. 벤치마크 결과 JSON의 .max_concurrent_requests는 BF16 42, AWQ 47입니다. 벤치 설정은 max_concurrency=32였는데 실측값이 그보다 큽니다. 클라이언트 세마포어 32와 이 지표가 정확히 무엇을 세는지(요청 정의, 집계 시점 등)가 어떻게 다른지 확인할 자료가 없어, 왜 32를 넘겼는지는 미해결로 남겨둡니다.
주의 - 두 개의 "토큰 수"를 섞지 않습니다. 벤치마크 결과 JSON의 total_input_tokens는 1,505,280입니다. 같은 두 replica의 prefix_cache_queries_total 합계는 1,505,293으로 13 토큰이 다릅니다. 정의가 다른 두 카운터라 상호대체하지 않고, 처리량·지연 계산에는 JSON 값을, 히트율 계산에는 /metrics 카운터 값을 각각 그대로 씁니다.
KV 캐시 용량 - AWQ 402,400 토큰 vs BF16 283,408 토큰
AWQ의 KV 캐시 용량은 402,400 토큰, BF16은 283,408 토큰입니다.
이번 요청 하나는 prefix 3,072 + suffix 64에 출력 32를 더해 총 3,168 토큰입니다. 이 크기 기준으로 BF16은 약 89.4개, AWQ는 약 127.0개 요청분을 담습니다. 설정 동시성 32는 물론 실측 최대 동시 요청 수(BF16 42, AWQ 47)보다도 훨씬 큽니다. 이번 워크로드에서는 두 조건 모두 KV 캐시 용량이 병목이 아니었던 것으로 보입니다.
AWQ 4bit 양자화
│
▼
가중치 메모리 절감 (관찰)
│
▼
KV 캐시 용량 증가 283,408 → 402,400 토큰
│
├──▶ 요청 3,168토큰 기준 동시 여유
│ BF16 89.4개 · AWQ 127.0개
│ (설정 32, 실측 42/47보다 큼)
│
├╌╌▶ TTFT·처리량 개선 ← 가설, 검증 안 됨
│
└──▶ 실측: Median TTFT +28.0%
P99 TTFT +69.9% · 처리량 −6.4%
실선은 관찰(가중치 절감→캐시 용량 증가, 동시 여유 계산)입니다. 점선은 "캐시가 크면 지연도 준다"는 검증되지 않은 가설이고, 실제 측정은 그 가설과 반대 방향입니다.
해석 - KV 캐시 용량과 요청 지연은 다른 질문
KV 캐시 용량은 "동시에 얼마나 많은 요청의 상태를 담아둘 수 있는가"를 답합니다. TTFT는 "한 요청이 첫 토큰을 받기까지 얼마나 걸리는가"를 답합니다. 두 질문은 서로 다른 축입니다. 캐시가 커도 이미 32개 동시성 안에서 여유가 남아 있다면, 그 여유는 지연을 줄이는 데 쓰이지 않고 그냥 미사용 상태로 남습니다.
이번 결과가 보여주는 것은 AWQ의 캐시 용량 우위가 이 워크로드(동시성 32, 프리필 지배적, 480 요청)에서는 지연 개선으로 이어지지 않았다는 것 입니다.
미확인 사항 왜 AWQ의 TTFT가 더 나빴는지는 이 자료로 단정할 수 없습니다. GPU 커널 시간, CPU 스케줄링 시간, 큐 대기 시간이 분해되어 저장돼 있지 않습니다. 이 질문에 답하려면 같은 워크로드로 BF16·AWQ 각각의 Nsight 트레이스를 떠서 GEMM/어텐션 커널 시간과 CUDA API 갭을 비교해야 합니다.
[관찰]
총 토큰 처리량 BF16 17,782 > AWQ 16,637 tok/s
Median TTFT BF16 621.9 < AWQ 796.2 ms
P99 TTFT BF16 3,481 < AWQ 5,914 ms
Mean TPOT BF16 154.9 ≈ AWQ 159.1 ms
히트율 BF16 30.4% < AWQ 39.2%
│
├╌╌▶ [해석] 이 워크로드(동시성 32)에서
│ AWQ 캐시 여유는 미사용 용량으로 보임
│ (근거: 히트율)
│
└╌╌▶ [미해결] TTFT 격차의
커널·큐잉·스케줄링 비중
(근거: Median/P99 TTFT)
관찰(위)은 전부 벤치 JSON과 /metrics 카운터에서 직접 나온 값입니다. 해석과 미해결(아래)은 그 관찰을 넘어서는 설명이라 실선이 아니라 별도 칸으로 분리했습니다.
결론 - 이 조건에 한정
이 A100 계열로 추정되는 노드(a100-002, manifest에 GPU 기종 고정 없음) 위, vLLM 0.11.0, Qwen3-14B와 Qwen3-14B-AWQ, k8s Service를 경유한 480개 prefix_repetition 요청(프리필 지배적, 동시성 32) 조건에서는 BF16이 처리량·TTFT·TPOT·E2E 지연 모든 지표에서 AWQ를 이겼습니다.
이 결론은 이 하드웨어·모델·워크로드 조합에 한정됩니다. 다른 GPU, 다른 모델 크기, 더 긴 출력, 더 높은 동시성, 또는 pod 직접 분할 경로에서는 결과가 달라질 수 있습니다.
LLM serving 인사이트
- 양자화의 가치는 지연보다 메모리 용량과 배포 가능성일 수 있습니다.
- 이번 실험에서 AWQ의 KV 캐시 용량은 BF16보다 컸지만(실험 리포트 기준 402,400 vs 283,408 토큰) 이번 워크로드의 TTFT는 AWQ가 더 나빴습니다.
- 반대로 같은 GPU 메모리에 더 큰 모델을 올리거나, 같은 모델을 더 적은 GPU로 서빙해야 하는 상황에서는 AWQ의 가치가 지연이 아니라 "애초에 배포 가능한가"에서 나올 수 있습니다.
- prompt/output 비율, 동시성, 캐시 히트율이 결과를 뒤집을 수 있습니다.
- 이번 워크로드는 입력 대 출력이 약 98:1로 prefill이 지배적이고 max-concurrency는 32였습니다.
- 출력이 훨씬 긴 워크로드나 동시성이 KV 캐시 용량에 근접하는 워크로드였다면 TPOT나 처리량 쪽에서 AWQ의 여유 용량이 실제로 쓰였을 가능성이 있습니다.
- 평균만이 아니라 median·P99, replica 분포, 캐시 상태를 함께 봐야 합니다.
- 이번 실험에서 Mean TPOT 차이는 2.8%로 작았지만 P99 TTFT 차이는 69.9%로 컸습니다.
- 평균 하나만 봤다면 두 조건이 비슷하다고 잘못 읽었을 것입니다.
- replica별 요청 분포를 확인하지 않았다면 "한쪽 replica가 트래픽을 독차지해서 생긴 왜곡"이라는 의심을 배제하지 못했을 것입니다.
- benchmark는 "무엇을 재는지"가 모델 형식만큼 결론을 좌우합니다.
- 같은 두 서버라도 Service 경로로 재는지 pod 직접 경로로 재는지,
ignore_eos로 출력 길이를 고정하는지에 따라 같은 실행에서도 다른 숫자와 다른 신뢰도가 나옵니다. - 이번 결과의 신뢰도는 콜드 캐시 전제 확인에서 나옵니다.
- 같은 두 서버라도 Service 경로로 재는지 pod 직접 경로로 재는지,