Models & AlgorithmsEN

llama.cpp KV 캐시 양자화, A100 한 장에서 실측 — q8_0은 4K에서 공짜이고 64K에서는 디코드 속도 절반을 냅니다

본류 llama.cpp, Qwen3-8B, A100 한 장. -ctk q8_0 -ctv q8_0은 퍼플렉시티가 f16과 같고 32K 캐시를 2.11 GiB 줄이지만, 64K 깊이 디코드는 f16의 55%로 떨어집니다(q4_0은 50%). q5_1과 K q8_0/V q4_0 혼합은 프리필이 초당 43·63토큰으로 조용히 CPU에서 돌았습니다.

llama.cpp KV 캐시 양자화, A100 한 장에서 실측 — q8_0은 4K에서 공짜이고 64K에서는 디코드 속도 절반을 냅니다

llama.cpp KV 캐시 양자화, A100 한 장에서 실측 — q8_0은 4K에서 공짜이고 64K에서는 디코드 속도 절반을 냅니다

Search Console을 보니 TurboQuant 실전에 들어오는 분들 대부분은 TurboQuant를 찾는 게 아니었습니다. 검색어가 llama.cpp kv cache quantization, llama.cpp ctk ctv, cache-type-k입니다. 지금 본류 llama.cpp에 들어 있는 기능, 그러니까 -fa on 뒤에 붙이는 -ctk q8_0 -ctv q8_0이나 q4_0을 찾는 분들입니다. 그 글은 업스트림이 끝내 머지하지 않은 포크 이야기라서, 맞는 질문에 엉뚱한 답을 하고 있었습니다.

이 글이 그 질문의 답입니다. 같은 카드, 같은 모델, 포크 글에서 돌린 것과 같은 네 가지 측정이라 두 결과가 마지막에 한 표에 들어갑니다.

무엇으로 쟀는지

A100 80GB PCIe 한 장, 드라이버 535. llama.cpp 본류(ggml-org master 69320fe, 2026년 9월 2일)를 sm80용으로 -DGGML_CUDA=ON 빌드했습니다. 모델은 Qwen3-8B Q4_K_M(가중치 4.68 GiB)입니다. 모든 실행에 -fa 1 -ngl 99를 붙였습니다. llama.cpp에서 양자화된 V 캐시는 플래시 어텐션이 있어야 돌아가므로 플래시 어텐션 없는 비교는 애초에 성립하지 않습니다. llama-bench 값은 2회 평균이고, 퍼플렉시티는 wikitext-2를 4K 컨텍스트에서 40청크, 8K·16K·32K에서 3청크씩 쟀습니다.

KV 타입은 다섯 가지입니다. 기본값 f16, q8_0, q4_0, q5_1, 그리고 포럼에서 자주 권하는 K q8_0/V q4_0 혼합입니다. 이 중 둘은 하드웨어에 올리자마자 탈락했고, 그게 첫 번째 결과입니다.

다섯 설정 중 둘은 프리필을 CPU에서 돌립니다

KV프리필 8K프리필 32K생성 128
f164,539 tok/s3,362151.2
q8_04,411 (−3%)3,228 (−4%)142.0 (−6%)
q4_04,422 (−3%)3,229 (−4%)142.1 (−6%)
q5_14310.991.6
K q8_0 / V q4_062.7측정 중단측정 중단

q8_0q4_0은 프리필에서 3~4%, 생성에서 6%를 냅니다. q5_1은 8K 토큰 프리필이 초당 43토큰으로 f16의 100분의 1이고, 혼합은 63입니다. 다른 타입은 1분이면 끝나는 측정을 q5_1 하나가 2시간 41분 끌었습니다. 혼합은 첫 행만 받고 중단했습니다.

8K 프리필 처리량을 로그 축에 그린 막대 그래프입니다. f16, q8_0, q4_0은 모두 초당 4,400토큰 위에 있고, q5_1은 43, K q8_0/V q4_0 혼합은 63입니다.

무슨 일인지 보려고 비어 있던 두 번째 A100에서 2K 프리필을 짧게 다시 돌리면서 GPU 사용률을 1초마다 찍었습니다. q4_0은 72~100%였습니다. q5_1은 2~4%, 혼합은 4~25%였습니다. GPU가 놀고 있었고, 어텐션 연산이 CPU에서 돌고 있었던 겁니다. 이럴 때 llama-bench는 경고를 한 줄도 찍지 않습니다. 생성은 여전히 GPU에서 돌기 때문에(q5_1 생성 91.6 tok/s) 대화형으로 쓰면 그냥 좀 굼뜬 정도로 느껴지고, 긴 프롬프트를 넣어 몇 분이 걸릴 때에야 알아차리게 됩니다.

이유를 ggml-cuda/fattn.cu에서 찾아봤습니다. 프리필에 쓰이는 배치 플래시 어텐션 커널은 f16 K/V로 동작하면서 캐시를 들어오는 길에 역양자화하고, 토큰 하나짜리 벡터 커널은 양자화 타입마다 전용 경로가 있습니다. 그런데 q8_0/q8_0q4_0/q4_0은 빠른 배치 경로를 타고 q5_1/q5_1q8_0/q4_0은 떨어지는 이유가 정확히 어디서 갈리는지는 제가 쓴 시간 안에서 특정하지 못했고, 추측으로 채우지는 않겠습니다. 측정 결과만으로 결론은 충분합니다. 이 빌드와 이 카드에서 저 두 설정은 프롬프트가 조금이라도 긴 작업에는 쓸 수 없습니다.

아래는 전부 살아남은 세 설정 이야기입니다.

메모리: 줄어든 만큼이 정확히 명목 비트 수입니다

32K 컨텍스트에서 퍼플렉시티 한 청크를 돌리는 동안 nvidia-smi로 찍은 최대 VRAM입니다.

KV최대 VRAMf16 대비
f169.63 GiB
q8_07.52 GiB−2.11 GiB
q4_06.39 GiB−3.23 GiB

최대치에는 가중치 4.68 GiB와 연산 버퍼가 들어 있으니 차이가 곧 캐시입니다. 32,768토큰의 f16 캐시는 레이어 36 × KV 헤드 8 × 128 × (K+V) × 2바이트 × 32,768 = 4.50 GiB로 계산됩니다. q8_0은 값 하나에 8.5비트를 쓰니 2.39 GiB, 절약 2.11 GiB가 나와야 하고, q4_0은 4.5비트라 1.27 GiB, 절약 3.23 GiB가 나와야 합니다. 측정한 차이가 둘 다 소수점 둘째 자리까지 맞습니다. 포크 글에서 같은 타입으로 나온 숫자와도 같은데, 캐시 배치는 커널이 아니라 타입이 정하니 당연한 결과입니다.

품질: q8_0은 손실이 없고 q4_0은 1% 안팎을 냅니다

KVctx 4Kctx 8Kctx 16Kctx 32K
f168.319 ± 0.0847.901 ± 0.2017.596 ± 0.1349.089 ± 0.118
q8_08.318 (−0.0%)7.905 (+0.0%)7.600 (+0.0%)9.096 (+0.1%)
q4_08.374 (+0.7%)7.958 (+0.7%)7.683 (+1.1%)9.133 (+0.5%)

q8_0은 모든 컨텍스트 길이에서 오차 범위 안입니다. q4_0은 f16보다 0.5~1.1% 위에 있고 컨텍스트가 길어져도 벌어지지 않습니다. 포크의 turbo3(4K에서는 멀쩡하다가 16K에서 +210%)과 갈리는 지점이 바로 이 성질입니다. 8K 이상은 3청크라 표본이 얇습니다. 저 표에서는 ± 열을 같이 읽어야 합니다.

깊이에서의 속도: 실제로 달라지는 숫자는 이것입니다

프리필과 빈 캐시 생성은 거의 안 변합니다. 변하는 건 캐시가 이미 차 있을 때의 디코드입니다. 그때는 매 스텝마다 캐시 전체를 읽기 때문입니다.

KV디코드 @8K@32K@64K@64K f16 대비
f16134.4 tok/s105.081.6100%
q8_0110.268.044.755%
q4_0107.263.641.250%
8K, 32K, 64K 깊이에서 초당 디코드 토큰 수를 그린 선 그래프입니다. 본류 f16은 134에서 82로, 본류 q8_0은 110에서 45로, q4_0은 107에서 41로 내려갑니다. 점선은 TurboQuant 포크의 q8_0과 q4_0이 23과 22까지 떨어지는 것을 보여줍니다.

캐시에 8K가 들어 있을 때 양자화 KV의 비용은 20% 정도입니다. 64K가 들어 있으면 절반입니다. 양자화된 캐시를 읽는다는 것은 디코드 스텝마다 역양자화를 한다는 뜻이고, 그 일은 컨텍스트에 비례해 늘어나는 반면 f16 경로는 바이트를 그냥 흘려보냅니다. 긴 컨텍스트를 메모리에 들어가게 해 준 그 절약을, 바로 그 긴 컨텍스트에서 시간으로 다시 내는 구조입니다.

포크 글에서는 같은 두 타입이 64K에서 f16의 27~29%였습니다. 본류는 50~55%입니다. 포크의 base 커밋과 이 커밋 사이에 양자화 KV 디코드 경로가 대략 두 배 빨라진 셈인데, 그래도 여전히 절반입니다.

그래서 무엇을 켤 것인가

상황설정이유
f16으로도 캐시가 VRAM에 들어감f16 그대로양자화해서 얻는 건 없고 생성 6%만 냄
32K에서 2 GiB쯤 돌려받아야 하고 프롬프트·답이 대체로 짧음-ctk q8_0 -ctv q8_0 -fa on손실 없음. 깊이 비용은 캐시에 16K쯤 쌓여야 체감됨
3 GiB를 돌려받아야 하고 문서가 길며 품질이 중요함그래도 -ctk q8_0 -ctv q8_0q4_0은 1 GiB 더 아끼는 대가로 PPL +1%, 디코드 비용은 같음
긴 컨텍스트 디코드 처리량이 중요함f16, 아니면 더 작은 모델64K에서는 어떤 양자화 캐시든 절반 속도
q5_1, 또는 K/V 타입 섞기이 빌드에서는 금지프리필이 CPU로 감

q8_0q4_0 사이의 선택은 결국 메모리만의 문제로 드러났습니다. 어느 깊이에서든 디코드 속도가 같으니, q4_0이 값을 하는 경우는 1.1 GiB 차이가 들어가고 못 들어가고를 가를 때뿐입니다.

이 양자화기들을 플래그로 켜는 데서 그치지 않고 GPTQ, AWQ, GGUF, KV 캐시 포맷까지 직접 만들어 보고 싶다면 LLM Quantization and Compression 강의에 24강 분량으로 정리해뒀습니다. 3강까지는 무료입니다.

확인하지 못한 범위

모델 하나, 가중치 양자화 하나입니다. Qwen3-8B은 KV 헤드 8개의 4:1 GQA라서, KV 헤드가 더 많은 모델은 캐시도 크고 깊이 비용도 아마 더 클 겁니다. 배치 크기는 내내 1입니다. llama-bench의 디코드는 스트림 하나이고, 배치 서빙은 대역폭 그림이 달라집니다. 8K 이상 퍼플렉시티는 3청크입니다. 그리고 CPU 폴백은 sm80에서 69320fe 커밋에 대한 사실이라, 이후 커밋이나 Hopper 카드는 다르게 동작할 수 있고 둘 다 확인하지 않았습니다.

포크 비교는 TurboQuant llama.cpp 실측에 있고, 어떤 KV 기법이 어떤 단위로 값을 치르는지는 KV 시리즈 1편에 있습니다. 이 글은 그 표의 양자화 행이 됩니다.

새 측정은 Paper of the Week로 매주 나갑니다. 다음 편을 받으시려면 아래에서 구독하세요.

리그: A100 80GB PCIe × 1, 드라이버 535. 두 번째 A100은 사용률 프로브에만 썼습니다. llama.cpp 69320fe(ggml-org master, 2026-09-02), sm80 CUDA 빌드. Qwen3-8B Q4_K_M, -fa 1 -ngl 99. 하네스 scripts/bench-llamacpp-main.sh, 원시 JSON과 로그는 drafts/llamacpp-kv-bench/, 표는 scripts/parse-llamacpp-kv-bench.py로 뽑았습니다. 2026-09-15 확인.

더 많은 콘텐츠를 받아보세요

SNS에서 새로운 글과 튜토리얼 소식을 가장 먼저 받아보세요

이메일로 받아보기

관련 포스트