· 약 7분 읽기

MLX KV Cache와 컨텍스트 길이 가이드 - Qwen3.5 Mac Mini 실전 경험


최근 며칠 동안 OpenCode를 로컬 MLX 서버에 붙여서 Qwen3.5 9B와 27B 모델을 번갈아 돌려봤다. 처음에는 단순히 “클라우드 대신 로컬에서도 코딩 같은 멀티턴 작업이 가능할까?” 정도의 호기심이었다.

그런데 실제로 써보니, 생각보다 빨리 한계에 부딪혔다. 모델이 느린 것보다 더 먼저 체감된 건 왜 갑자기 죽는지, 왜 어떤 때는 멀쩡한데 어떤 때는 같은 작업에서 터지는지, 캐시를 줄이면 품질이 나빠지는 건지 아닌지 같은 질문이었다.

이 글은 그 과정을 정리한 기록이다. 단순한 설정 팁보다, 로컬 LLM이 실제로 어떻게 움직이는지 이해하게 된 포인트에 더 가깝다.


시작은 단순한 OOM 로그였다

처음 본 에러는 익숙한 형태였다.

[METAL] Command buffer execution failed: Insufficient Memory

문제는 숫자가 애매했다는 점이다.

  • Mac Mini M4 24GB
  • Qwen3.5 9B 4bit
  • 컨텍스트 약 42k 토큰
  • KV cache 12GB+

겉으로 보면 “24GB인데 9B 모델도 못 버티나?” 같은 의문이 바로 든다. 나도 처음엔 그렇게 생각했다.

그런데 로그를 조금 더 뜯어보니 문제는 모델 파라미터 크기 하나가 아니었다.

  • 모델 가중치 메모리
  • KV cache 메모리
  • prefill 중 임시 작업 메모리
  • macOS 자체 사용 메모리
  • 서버가 동시에 붙잡고 있는 여러 시퀀스

이 다섯 개가 겹쳐 있었다.

즉, “9B라서 안전할 것”이라는 감각이 틀린 게 아니라, 긴 컨텍스트와 누적 캐시가 붙는 순간 별개의 문제가 시작된다는 걸 뒤늦게 이해했다.


KV cache는 대화 기록이 아니라 계산 결과였다

초반에 가장 헷갈렸던 개념이 KV cache였다.

이전에는 막연하게 “대화 기록을 캐시한다” 정도로 생각했다. 틀린 말은 아니지만, 정확히는 아니다.

  • 대화 기록 자체는 텍스트다
  • KV cache는 그 텍스트를 모델이 이미 읽고 만들어둔 내부 상태다

이 차이를 이해하고 나서 로그가 다르게 보이기 시작했다.

KV Caches: 2 seq, 3.11 GB, latest user cache 42477 tokens

이건 “이전 대화 두 개를 저장했다”가 아니라,

  • 재사용 가능한 시퀀스 캐시가 2개 있고
  • 그 총 메모리가 3.11GB이며
  • 가장 최근 사용자 기준 캐시 길이가 약 42k 토큰

이라는 뜻이었다.

이걸 이해하니, 캐시를 줄이는 게 곧바로 품질 저하라는 생각도 정리됐다. 캐시가 없어지면 같은 텍스트를 다시 계산할 뿐이고, 텍스트 자체가 유지되면 품질은 원칙적으로 같다.


캐시 제한은 품질보다 시간을 희생하는 쪽에 가깝다

여기서 두 번째로 배운 건, 캐시 제한의 본질이 무엇인가였다.

mlx_lm server를 다음처럼 더 보수적으로 실행했다.

python -m mlx_lm server \
  --prompt-cache-size 2 \
  --prompt-cache-bytes 4GB \
  --prompt-concurrency 2 \
  --decode-concurrency 4 \
  --prefill-step-size 1024

이 설정의 효과는 꽤 명확했다.

  • KV cache가 예전처럼 10개 이상 쌓이지 않음
  • 총 캐시 메모리가 3~4GB대로 통제됨
  • 긴 세션도 이전보다 오래 버팀

대신 손해도 있었다.

  • 같은 긴 프롬프트를 다시 계산하는 일이 생김
  • 응답 시작 전 prompt processing 시간이 길어짐

즉, 메모리를 아끼기 위해 시간을 쓰는 구조였다.

처음에는 이게 곧 모델 품질 저하로 이어지는 줄 알았다. 그런데 실제로는 다르다.

  • 캐시가 줄어든다 = 주로 속도 손해
  • 컨텍스트 텍스트를 잘라낸다 = 품질 손해 가능

이 구분이 생각보다 중요했다.


prompt processing progress가 의미하는 것

로컬 모델을 처음 오래 돌려보면 자주 보게 되는 로그가 있다.

Prompt processing progress: 1024/42480
Prompt processing progress: 2048/42480
Prompt processing progress: 3072/42480

처음엔 “왜 답을 안 하고 저 숫자만 올라가지?” 싶었다.

이건 답변 생성이 아니라 prefill, 즉 모델이 입력 프롬프트를 읽어 KV cache를 쌓는 과정이다.

  • 뒤 숫자: 전체 입력 토큰 수
  • 앞 숫자: 지금까지 처리한 토큰 수

내가 --prefill-step-size 1024로 줄여둔 상태였기 때문에, 긴 컨텍스트를 한 번에 읽지 않고 1024 토큰 단위로 나눠 처리하고 있었다.

즉, 이 과정은 느려 보이지만 의미 없는 지연이 아니라, 메모리 피크를 낮추기 위한 안전장치였다.


24GB에서 9B는 “충분”하지만, 27B는 “가능은 한데 불안정”했다

9B는 캐시 정책을 조절하면 꽤 실용적이었다. 문제는 27B였다.

OpenCode 설정에 mlx-community/Qwen3.5-27B-Claude-4.6-Opus-Distilled-MLX-4bit를 추가하고 서버에 연결해봤다. 짧은 작업에서는 분명히 더 나은 응답이 나올 때가 있었다.

하지만 긴 세션으로 들어가면 양상이 달라졌다.

  • 약 25.8k 컨텍스트 prefill은 통과
  • KV cache는 3.5~3.8GB 수준으로 유지
  • 그 이후 짧은 후속 요청을 처리하다가 결국 OOM

여기서 중요한 걸 배웠다.

--prompt-cache-bytes 4GB저장된 캐시 총량 제한이지, 생성 중 순간적으로 필요한 메모리까지 막아주지 않는다. 즉,

  • 창고 크기는 4GB로 제한했지만
  • 작업대 위에 잠깐 올라오는 물건까지 포함하면
  • 전체 메모리는 여전히 터질 수 있다

이걸 직접 겪고 나서야, “캐시 제한을 줬는데 왜 또 죽지?”라는 의문이 정리됐다.

결론적으로 내 장비에서는:

  • 9B: 장시간 실사용 가능
  • 27B: 짧거나 중간 길이 작업은 가능, 긴 세션은 불안정

정도로 보는 게 맞았다.


긴 세션에서 반복 응답이 생기는 이유도 따로 있었다

메모리 문제와 별개로, 어느 시점부터는 모델이 비슷한 말을 반복하기 시작했다. 완전한 무한 루프라기보다는, 같은 중간 결론을 조금만 바꿔가며 다시 말하는 상태였다.

이때 세션 총 토큰은 70k를 넘어가고 있었다.

여기서 느낀 건, 긴 세션의 문제는 단순히 OOM만이 아니라는 점이다.

  • 작업 상태 추적이 흐려짐
  • 무엇이 완료됐는지, 남았는지 판단이 흔들림
  • 최근 패턴을 반복하는 쪽으로 기울기 쉬움

즉, 캐시를 잘 관리해도 세션이 너무 길어지면 모델의 운영 품질 자체가 흔들릴 수 있다.

이 문제는 의외로 메모리 튜닝보다 간단한 해결책이 있었다.

  • 세션을 끊고
  • 사람이 짧게 상태를 요약해서
  • 새 세션으로 넘긴다

이 방식이 무작정 오래 이어가는 것보다 훨씬 낫다.


결국 로컬 모델 운영은 “속도, 메모리, 문맥”의 균형 문제였다

이 과정을 지나며 가장 크게 바뀐 건, 로컬 LLM을 보는 관점이었다.

예전에는:

  • 좋은 모델을 고른다
  • 잘 연결한다
  • 잘 돌아가면 끝

정도로 생각했다.

지금은 다르게 본다.

  • 모델 크기
  • 양자화
  • KV cache 정책
  • prefill step
  • 동시성
  • 컨텍스트 누적 방식
  • 세션을 언제 끊을지

이 전부가 하나의 운영 문제다.

클라우드 모델을 쓸 때는 이 많은 것이 대부분 숨겨져 있다. 로컬에서 직접 겪어보면, 평소 추상적으로만 보이던 개념이 전부 실제 숫자로 바뀐다.

  • 왜 느린지
  • 왜 어떤 요청은 성공하고 어떤 요청은 죽는지
  • 왜 같은 모델도 세션이 길어지면 이상해지는지

이제는 이걸 설명할 수 있게 됐다.


내가 얻은 가장 현실적인 결론

내 장비 기준으로는 다음처럼 정리할 수 있다.

  • 9B는 캐시 제한과 컨텍스트 관리만 잘하면 실사용 가능
  • 27B는 “돌아간다”와 “안정적으로 운영된다”가 다르다
  • 캐시 제한은 주로 시간과 메모리의 트레이드오프다
  • 품질 저하는 캐시 제한보다 컨텍스트 손실에서 더 자주 온다
  • 긴 세션은 언젠가 메모리보다 먼저 상태 추적 문제를 일으킨다

그리고 무엇보다 중요한 건, 이런 과정을 통해 로컬 모델을 “그냥 굴려본 경험”이 아니라 LLM 시스템이 실제로 어떻게 동작하는지에 대한 감각을 얻게 된다는 점이다.

누군가는 이런 작업을 비효율이라고 볼 수도 있다. 클라우드 모델을 쓰면 훨씬 쉽게 끝나는 문제도 많다. 그 말도 맞다.

그래도 직접 로그를 보고, 캐시를 조절하고, OOM의 원인을 분해하고, 세션이 길어질수록 어떤 문제가 생기는지 체감해본 경험은 남는다. 그건 단순히 특정 모델 하나를 다루는 기술이 아니라, 앞으로 어떤 LLM 시스템을 다루더라도 통하는 운영 감각에 더 가깝다.

적어도 지금의 나는, 이 과정을 거치기 전보다 로컬 모델을 훨씬 덜 신비롭게 본다. 그게 가장 큰 수확이었다.