KV 캐시가 GPU 에 다 안 들어갈 때: 순환 접근과 LRU, 그리고 CPU 로 내리기
한 줄 요약
GPU 의 KV 캐시 자리보다 많은 문서를 번갈아 읽히면, 가장 오래 쓰지 않은 것부터 내보내는 LRU 때문에 같은 순서로 다시 읽을 때 적중이 0 이 된다. 밀려나는 KV 를 CPU 메모리에 내려 두면(LMCache 의 아이디어) 다시 계산하지 않고 불러올 수 있고, 이 측정에서는 첫 토큰까지 걸리는 시간이 576 ms 에서 48.6 ms 로 줄었다(약 11.9배). 대가는 처음 읽을 때 약 4% 느려지는 것이다.
왜 이게 필요했나
vLLM 은 같은 앞부분(접두사)을 가진 요청의 KV 캐시를 GPU 에 두고 재사용한다(prefix caching). 긴 문서를 앞에 붙이고 질문만 바꾸는 요청이라면 문서 부분의 계산을 건너뛸 수 있다. 문제는 GPU 메모리가 작다는 것이다. 이 측정에서 1.5B 모델을 올리고 남은 KV 캐시 자리는 21,520 토큰뿐이었고(서버 로그의 GPU KV cache size), 문서 하나가 5,011 토큰이라 네 개 남짓이 들어간다. 문서가 열 개면 합계가 50,110 토큰으로 자리의 약 2.3배다.
자리가 모자라면 무엇인가를 내보내야 하고, 이 캐시는 가장 오래 쓰지 않은 것부터 내보낸다(LRU). 최근에 쓴 것이 곧 다시 쓰인다고 가정하는 규칙인데, 문서를 0번부터 9번까지 읽고 같은 순서로 다시 0번부터 읽는 순환 접근은 그 가정이 정확히 반대가 되는 경우다. 첫 패스가 끝났을 때 남은 것은 가장 최근에 읽은 6~9번이다. 두 번째 패스에서 0번은 이미 없으니 새로 계산해 넣는데, 그러면 가장 오래된 6번이 밀려난다. 1번을 넣으면 7번이 밀려난다. 다시 읽는 순서가 밀려난 순서와 같아서, 가장 오래된 것이 곧 가장 먼저 다시 필요한 것이 된다. 자리가 조금만 모자라도 적중이 0 이 되는 이유다. 실측에서도 vLLM 의 prefix_cache_hits 가 두 번째 패스에서 0 이었다.
어떻게 동작하나
LMCache 는 밀려나는 KV 를 CPU 메모리(디스크나 Redis 도 가능)에 내려 두었다가, 같은 접두사가 다시 오면 계산하는 대신 불러온다. 이 실습의 시뮬레이터는 그 흐름을 규칙 셋으로 줄였다.
- GPU 에서 적중하면 거기서 끝난다. CPU 는 건드리지 않는다.
- GPU 에서 놓쳤는데 CPU 에 있으면, 계산하지 않고 CPU 에서 GPU 로 불러온다.
- 둘 다 없으면 계산하고, GPU 와 CPU 양쪽에 저장한다.
1번은 측정 로그가 뒷받침한다. 거꾸로 읽는 세 번째 패스에서 vLLM 이 GPU 에서 해결한 접두사가 21,488 토큰이었고, LMCache 에 물어본 토큰은 28,755 개였다. 전체 50,243 에서 GPU 적중분을 뺀 값이다. 3번은 실제 동작을 줄인 것이다. 보고서는 처음 읽기가 약 4% 느려진 것을 읽으면서 KV 를 CPU 로 내리는 비용으로 해석하고, 문서 열 개(5만 토큰)의 KV 가 약 1.3GB 라서 설정한 상한 1.5GB 가 그것을 담았다고 적는다. CPU 쪽 자리에도 상한이 있어서, 상한보다 많은 문서를 읽히면 CPU 에서도 오래된 것부터 버려진다.
실측 결과
문서 10개를 첫 읽기, 같은 순서로 다시 읽기, 거꾸로 읽기의 세 패스로 읽혔다. 요청은 하나씩 보냈고 첫 토큰까지의 시간(TTFT, ms)을 쟀다. 값은 p50 과 평균이다.
| 설정 | 패스 | p50 | 평균 |
|---|---|---|---|
| vLLM 만 | 처음 | 575.5 | 581.2 |
| vLLM 만 | 같은 순서로 다시 | 576.2 | 575.9 |
| vLLM 만 | 거꾸로 | 505.4 | 343.6 |
| LMCache | 처음 | 597.8 | 605.4 |
| LMCache | 같은 순서로 다시 | 48.6 | 49.6 |
| LMCache | 거꾸로 | 47.2 | 40.9 |
같은 순서로 다시 읽을 때 p50 은 576 에서 48.6 ms 로 약 11.9배, 평균은 약 11.6배 빨라졌다. 패스 하나를 끝내는 시간은 6.2 초에서 0.9 초였다. 처음 읽을 때는 p50 이 575.5 에서 597.8 ms 로 약 4% 늘었다. vLLM 만 쓴 쪽의 두 번째 패스는 첫 패스와 사실상 같다(576 대 575). 캐시가 하나도 도움이 되지 않았다는 뜻이다.
LMCache 로그에서 두 번째 패스의 요청 하나는 약 5,000 토큰 가운데 4,864 토큰(256 의 배수 19개)을 CPU 에서 불러왔고 나머지 약 150 토큰만 새로 계산했다. 불러오는 데 8~9 ms 가 걸렸고, 같은 분량을 새로 계산하는 데는 약 560 ms 가 걸린다. 불러오기가 계산보다 60배 이상 싸다는 것이 이 격차의 전부다.
거꾸로 읽는 패스도 시뮬레이터가 맞힌다. vLLM 만 쓰면 방금 읽은 네 문서(9, 8, 7, 6번, 번호는 측정 파일의 doc 값이다)는 약 30 ms 로 GPU 에서 적중하고 나머지는 약 576 ms 를 낸다. LMCache 를 켜면 같은 네 문서는 약 30 ms, 나머지 여섯은 약 46~49 ms 로 CPU 에서 불러온다. 다만 vLLM 만 쓴 쪽의 5번 문서가 438 ms 로, 적중과 미적중 사이의 값이 나왔다. 문서를 통째로 넣고 빼는 시뮬레이터는 이런 부분 적중을 그리지 못한다.
이 숫자를 어디까지 믿을 것인가
- 이 시나리오는 LMCache 에 가장 유리하다. 같은 문서를 다시 읽고, 접두사가 5천 토큰으로 길고, GPU 캐시가 모자란다. 이득이 다시 쓰이는 긴 접두사의 비율에 달려 있다고 보고서는 해석하지만, 이번에 그 비율을 바꿔 가며 재 보지는 않았다.
- 요청을 하나씩 보냈다(동시성 1). 요청이 겹칠 때의 모습은 이 측정이 보여 주지 않는다.
- 모델이 1.5B 로 작고 GPU 한 장이다. 큰 모델에서 격차가 더 커질 것으로 예상하지만 이 측정으로 확인한 것은 아니다.
- 문서가 난수 낱말로 만든 합성 문서라 실제 문서처럼 접두사가 부분적으로 겹치지 않는다.
- 한 번 잰 값이고 표준편차는 구하지 않았다. 방향은 확실해도 소수점 이하는 믿지 않는 편이 좋다.
현장에서 만나는 모습
이번 측정은 LabHub 의 운영 llm 배포를 바꾸지 않았다. 적용하려면 이미지와 서버 인자와 환경변수를 바꿔야 하고, 그것은 별도 작업이다. 검토할 때 보고서가 짚은 조건은 문서 합계 토큰이 서버 로그의 GPU KV cache size 보다 커야 격차가 보인다는 것이다. 같은 문서가 다시 읽히지 않으면 불러올 것이 없다는 점도 같다. CPU 단계의 자리는 토큰당 KV 크기부터 계산해서 정해야 한다. 이 측정의 1.5GB 는 문서 열 개 분량(약 1.3GB)을 담았기 때문에 통했다.
다음 실습에서 할 것
GPU 없이 순수 파이썬으로 이 흐름을 직접 확인한다. LRU 캐시를 만들어 용량 21,520 토큰에 문서 10개를 같은 순서로 두 번 읽히고 적중이 0 인 것을 본다. 거꾸로 읽으면 어느 문서가 맞는지 보고, 자리를 어디까지 늘려야 적중이 생기는지 찾는다. 그다음 GPU 와 CPU 두 단계 캐시를 만들어 두 번째 패스가 모두 적중하는 것을 확인하고, CPU 자리가 모자랄 때를 예측한다. 마지막으로 위 표의 원본인 요청별 측정 파일을 읽어 p50 과 평균과 배율을 직접 계산한다.