본문 바로가기
프로그래밍/AI

로컬 LLM이 매번 입력을 처음부터 다시 읽을 때: prefix 캐시 확인하는 법

by me_in_sk 2026. 9. 25.
반응형
로컬 LLM

로컬 LLM이 매번 입력을 처음부터 다시 읽을 때: prefix 캐시 확인하는 법

대화가 길어질수록 첫 글자가 늦어진다면, 서버의 캐시가 실제로 쓰이고 있는지부터 본다
핵심 요약
  • 대화나 에이전트 작업은 요청마다 앞부분이 같다. 서버가 그 앞부분의 계산을 기억해 두면(prefix 캐시) 새로 붙은 부분만 읽으면 된다. 이게 안 되면 대화가 길어질수록 첫 글자가 늦어진다.
  • 캐시가 쓰이는지는 입력을 읽는 시간으로 안다. 대화가 길어져도 거의 일정하면 쓰이고 있고, 길이를 따라 늘면 안 쓰이고 있다.
  • 안 쓰이는 흔한 이유는 셋이다. 서버가 같은 대화인지 모른다, 캐시 항목이 너무 커서 버려진다, 끝난 대화의 캐시가 자리를 차지하고 안 비워진다.
  • 디스크에 캐시를 두는 기능은 켜기 전에 실제로 다시 쓰이는지 잰다. 순서대로 한 번씩 처리하는 작업에서는 한 번도 쓰이지 않았다.
이 글의 환경과 용어

32GB 메모리의 맥 미니(M6)에서 Qwen3.8-27B 모델을 돌렸다. 서버는 애플의 머신러닝 프레임워크 MLX 위에서 도는 오픈소스 추론 서버 MTPLX다. Ollama, LM Studio, llama.cpp 같은 다른 서버도 원리는 같지만 설정 이름과 기본값은 다르다.

  • 입력을 읽는 시간(prefill): 모델이 답을 쓰기 전에 입력 전체를 처리하는 단계. 첫 글자가 나오기까지의 대기가 대부분 이것이다.
  • prefix 캐시: 앞부분이 같은 요청이 다시 오면, 그 앞부분을 처리한 결과(문맥 캐시, KV 캐시)를 보관해 두었다가 다시 쓰는 기능. 이 서버는 대화 단위로 보관한다.

1.캐시가 쓰이고 있는지 어떻게 아나

로컬 모델로 에이전트를 돌린다. 요청마다 지시문, 도구 설명, 지난 대화가 그대로 들어가고 뒤에 새 메시지가 하나 붙는다. 대화가 길어질수록 답이 시작되기까지 점점 오래 걸린다.

모델은 답을 쓰기 전에 입력 전체를 읽는다. 요청마다 앞부분이 같다면 서버가 그 부분의 계산 결과를 기억해 두었다가 다시 쓸 수 있다. 이것이 prefix 캐시다. 잘 쓰이면 새로 읽는 것은 뒤에 붙은 몇백 토큰뿐이다.

응답 시간 전체로는 판단하기 어렵다. 답의 길이에 따라 들쑥날쑥하기 때문이다. 대신 입력을 읽는 시간(첫 글자가 나오기까지의 시간)을 본다. 같은 대화에서 요청을 몇 번 이어 보내고 이 시간이 어떻게 변하는지 보면 된다.

같은 대화의 2·3·4번째 요청이 입력을 읽는 데 걸린 시간 (초) 캐시가 안 먹으면 대화가 길어질수록 늘고, 먹으면 새로 붙은 부분만 읽어 거의 평평하다 0 30 60 90 120 58s 29s 2번째 호출 88s 33s 3번째 호출 120s 35s 4번째 호출 캐시 안 먹음 캐시 먹음
캐시가 안 먹으면 대화가 길어질수록 대기가 늘어 네 번째 요청에서 2분이 됐다. 먹으면 30초대에서 거의 그대로다.
이 글의 측정

같은 대화에 요청을 네 번 이어 보냈다. 처음 설정에서는 캐시가 한 번도 쓰이지 않았다. 아래 세 가지를 차례로 고친 뒤에는 요청마다 앞부분 7천~2만 토큰을 다시 쓰게 됐다.

2.서버가 같은 대화인지 알아보나

캐시가 안 쓰이면 먼저 확인할 것은 서버가 두 요청을 같은 대화로 알아보는지다. 서버는 요청을 받을 때 이전의 어느 대화를 이어 가는지 알아야 그 캐시를 꺼낸다.

서버마다 방법이 다르다. 대화 식별자를 헤더나 필드로 받는 서버라면 그걸 보내는 것이 가장 확실하다. 식별자가 없으면 서버는 보관 중인 캐시 가운데 앞부분이 가장 길게 일치하는 것을 골라 추측한다. 추측은 앞부분이 조금만 달라져도 빗나간다.

대화마다 고정 식별자를 보낸다 (헤더 이름은 서버 문서를 따른다)python
client.chat.completions.create(
    model=..., messages=...,
    # 대화(또는 에이전트 역할)마다 하나
    extra_headers={"X-Session-Id": conversation_id},
)
  • 식별자의 단위는 실제로 앞부분이 이어지는 범위다. 여러 역할이 번갈아 도는 에이전트라면 역할마다 지시문이 다르니 역할마다 하나씩 준다.
  • 앞부분을 바꾸지 않는다. 클라이언트가 지난 메시지를 다시 가공해서 보내면(생각 과정을 지우거나 도구 호출 표기를 바꾸거나) 앞부분이 달라져 캐시가 맞지 않는다.

3.식별자를 보냈는데도 캐시가 비어 있다면

식별자를 보내자 두 번째 요청에서 한 번 쓰였지만, 그 뒤로는 다시 쓰이지 않았다. 서버 로그에는 "디스크 캐시에서 못 찾았다"는 줄만 있었다. 디스크 문제처럼 보였지만 원인은 그 앞이었다. 캐시에 넣는 단계에서 이미 버려지고 있었다.

캐시에는 항목 하나가 차지할 수 있는 크기에 한도가 있다. 대화가 길어지면 항목이 커지고, 모델 구조에 따라서는 문맥 캐시 말고도 부가 정보를 함께 저장한다. 이 글의 모델은 층 일부가 "지금까지 읽은 내용을 요약한 상태"를 들고 가는 방식(선형 어텐션)이라, 대화 중간부터 이어 갈 수 있도록 그 상태를 여러 지점에서 찍어 함께 저장했다. 그래서 항목이 예상보다 훨씬 커졌다. 이 서버는 한도를 넘는 항목을 일부만 넣지 않고 통째로 버렸다. 그러면 다음 요청은 찾을 것이 없고, 로그에는 마지막 단계의 실패("못 찾았다")만 남는다.

  • 로그의 마지막 줄보다 "저장 단계"의 기록을 찾는다. 항목이 거부됐는지, 왜 거부됐는지가 거기 있다.
  • 항목 크기와 캐시 한도를 나란히 놓는다. 이 환경에서는 7천 토큰 대화의 항목이 약 1.8GB였는데 캐시 한 칸의 한도가 1GB였다.
  • 서버에 부가 정보를 덜어내고 저장하는 옵션이 있는지 본다. 이 서버에는 있었지만 기본으로 꺼져 있었다. 캐시에 줄 메모리를 늘리는 방법도 있다.

캐시에 줄 메모리를 늘리는 것은 결국 모델 서버의 전체 메모리와 얽힌다. 그 이야기는 메모리를 나누는 글에 정리했다.

캐시가 차지하는 메모리 →

32GB 맥에서 큰 로컬 LLM 돌릴 때 메모리 나누는 법: GPU 한도와 서버 캐시

서버는 받은 메모리의 빈자리를 대화 캐시로 채운다. OS와 다른 앱 몫을 먼저 떼고, 캐시에는 따로 상한을 건다.

4.끝난 대화의 캐시가 자리를 안 비운다면

캐시가 쓰이기 시작하자 다른 문제가 생겼다. 에이전트의 한 역할이 끝나고 다음 역할이 첫 요청을 보내면, 서버가 메모리가 부족하다며 거절했다. 앞 역할의 캐시는 더는 쓸 일이 없는데 그대로 남아 있었다.

서버 입장에서는 합리적이다. 방금 쓴 대화는 곧 다시 올 가능성이 높으니 한동안 지우지 않고 보호한다. 서버는 그 대화가 끝났다는 것을 알 방법이 없다. 아는 것은 클라이언트다.

  • 서버에 대화를 지우는 기능이 있으면, 대화가 끝날 때 부른다.
  • 메모리 부족 거절은 잠시 뒤 다시 시도한다. 지운 직후에도 서버가 앞 대화의 마지막 저장을 뒤에서 마무리하는 중이라 한 번 더 거절될 수 있다. 몇 초에서 1분 정도 간격으로 몇 번 재시도하면 풀린다.
  • 보관 개수와 보관 시간은 실제로 다시 쓰이는 간격에 맞춘다. 이 환경에서는 캐시를 다시 쓴 것이 거의 항상 바로 직전 요청이었다. 기본값은 끝난 대화를 한 시간씩 들고 있어서, 개수를 줄이고 보관 시간을 10분으로 줄였다.

5.디스크 캐시는 켜 둘 가치가 있나

메모리에서 밀려난 캐시를 디스크에 두었다가 나중에 꺼내 쓰는 기능도 있다. 켜 두면 손해 볼 게 없어 보인다. 실제로는 디스크를 차지하고 저장하는 동안 입출력도 쓴다.

이 기능이 값을 하려면 지나간 대화로 다시 돌아오는 일이 있어야 한다. 채팅 앱에서 어제 대화를 다시 여는 경우가 그렇다. 반대로 작업을 순서대로 한 번씩 처리하고 지나가는 파이프라인은 돌아가지 않는다. 켜기 전에 실제로 꺼내 쓰는 비율을 잰다.

이 글의 측정

순서대로 분석하는 작업에서 디스크 캐시는 54GB를 차지했고, 22번 찾아서 한 번도 쓰이지 않았다. 다시 쓰인 캐시는 전부 바로 직전 요청의 것이었고 메모리에 있었다.

6.이 결론이 안 통하는 조건

  • 대화 단위가 아니라 토큰 묶음 단위로 캐시를 나누는 서버. 대화 식별자나 보호 시간이 없거나 다르게 동작한다. 자기 서버가 무엇을 단위로 캐시하는지부터 확인한다.
  • 사용자가 지난 대화로 자주 돌아오는 앱. 5절과 달리 디스크 캐시가 값을 할 수 있다. 여기서도 비율을 재 보는 것은 같다.
  • 여러 대화를 동시에 처리하는 서버. 보호 시간과 보관 개수가 서로 경쟁한다. 이 글은 요청을 하나씩 처리하는 구성을 전제한다.

7.정리

  1. 같은 대화에 요청을 이어 보내고, 입력을 읽는 시간이 늘어나는지 본다. 늘어나면 캐시가 안 쓰이는 것이다.
  2. 대화(또는 역할)마다 고정 식별자를 보낸다. 지난 메시지는 가공하지 않고 그대로 보낸다.
  3. 캐시가 비어 있으면 저장 단계에서 거부됐는지 찾는다. 항목 크기와 캐시 한도를 비교한다.
  4. 대화가 끝나면 서버에 알리고, 메모리 부족 거절은 재시도한다.
  5. 보관 개수·시간은 실제로 다시 쓰이는 간격에 맞춘다.
  6. 디스크 캐시는 다시 쓰이는 비율을 보고 켠다.

로그가 "디스크에서 못 찾았다"고 말할 때 실제 원인은 메모리 쪽에서 저장을 거부한 것이었다. 캐시 문제는 마지막에 실패한 곳이 아니라 처음 거부된 곳에서 찾는다.

로컬 LLM · Apple Silicon

반응형

댓글