32GB 맥에서 큰 로컬 LLM 돌릴 때 메모리 나누는 법: GPU 한도와 서버 캐시
- 맥은 CPU와 GPU가 메모리 하나를 나눠 쓴다. 그래도 macOS는 GPU가 쓸 수 있는 몫에 기본 상한을 둔다. 32GB 맥이면 대략 25GB다. 큰 모델이 안 올라가면 이 상한부터 확인한다.
- 모델 서버는 모델 말고도 메모리를 쓴다. 대화를 빨리 이어 가려고 지난 계산 결과를 보관하는 캐시가 대표적이다. 스왑의 원인은 이 캐시였다.
- 그래서 순서를 뒤집는다. OS와 다른 앱이 쓰는 몫을 먼저 떼고, 서버에는 그 나머지만 준다. 캐시에는 따로 상한을 건다.
- 서버의 메모리 설정은 딱 떨어지는 벽이 아니다. 설정보다 조금 넘게 쓸 수 있으니 여유를 남긴다.
32GB 메모리의 맥 미니(M6)에서 Qwen3.8-27B 모델을 돌렸다. 서버는 애플의 머신러닝 프레임워크 MLX 위에서 도는 오픈소스 추론 서버 MTPLX다. Ollama, LM Studio, llama.cpp 같은 다른 서버도 원리는 같지만 설정 이름과 기본값은 다르다.
- 가중치: 모델이 학습한 숫자들로, 모델 파일의 대부분이다. 4bit로 줄였다(양자화)는 숫자 하나를 4비트로 저장해 크기를 원래보다 크게 줄였다는 뜻이다.
- 문맥 캐시(KV 캐시): 지금 처리 중인 요청의 앞 내용을 기억해 두는 메모리. 요청이 끝나면 필요 없다.
- 대화 캐시: 끝난 요청의 문맥 캐시를 버리지 않고 보관했다가, 다음 요청이 같은 앞부분으로 시작하면 다시 쓰는 것. 대화마다 항목이 하나씩 쌓인다.
1.큰 모델을 올리면 메모리는 어디로 가나
4bit로 줄인 27B 모델은 20GB 정도다. 32GB 맥이면 넉넉해 보인다. 그런데 막상 오래 돌리면 느려지고, 활성 상태 보기를 열면 스왑이 쌓여 있다. 나머지 12GB는 누가 쓰고 있는 걸까.
하나씩 세어 보면 이렇다.
| 쓰는 곳 | 대략 크기 (27B 4bit 기준) | 설명 |
|---|---|---|
| 모델 가중치 | 약 19GB | 모델 파일 크기와 거의 같다 |
| 문맥 캐시 (KV 캐시) | 1만6천 토큰에 약 0.5GB | 지금 대화의 앞 내용을 기억하는 몫. 모델 구조에 따라 크게 다르다 |
| 서버 작업 공간 | 수 GB | 계산 중 잠깐 쓰는 메모리 |
| 대화 캐시 | 항목 하나에 1~2GB, 여러 개가 쌓인다 | 끝난 요청의 문맥 캐시를 다음 요청을 위해 보관 |
| macOS와 다른 앱 | 4~5GB 이상 | 브라우저·편집기를 띄우면 더 는다 |
흔히 먼저 의심하는 것은 문맥 캐시다. 대화가 길수록 커지니 그럴듯하다. 그런데 요즘 모델 중에는 문맥 캐시가 작은 구조가 많다. 모델 설정 파일의 층 수와 헤드 수로 직접 계산할 수 있으니 추측하지 말고 계산해 본다.
토큰당 크기 = 어텐션 층 수 × 2 × KV 헤드 수 × 헤드 차원 × 2바이트 # 이 글의 모델은 64층 중 16층만 이 캐시를 쓴다 # (나머지 48층은 크기가 고정된 선형 어텐션). 1만6천 토큰에 약 1GB, # 8bit로 줄이면 약 0.5GB였다. 스왑을 만들 크기가 아니었다.
이 환경에서는 분석을 돌리는 동안 10초마다 수백 MB씩 스왑으로 밀려났다. 원인은 문맥 캐시가 아니라 표의 네 번째 줄, 대화 캐시였다. 항목 하나는 1~2GB지만 서버가 끝난 대화의 항목을 여러 개 쌓아 두어 합치면 5GB가 넘었다.
2.맥의 GPU는 메모리를 얼마나 쓸 수 있나
애플 실리콘은 CPU와 GPU가 메모리 하나를 같이 쓰는 통합 메모리 구조다. 그렇다고 GPU가 32GB를 다 쓸 수 있는 것은 아니다. macOS는 시스템이 굶지 않도록 GPU가 붙잡아 둘 수 있는 양에 기본 상한을 둔다. 모델은 GPU 메모리에 올라가야 하므로, 이 상한이 사실상 모델이 쓸 수 있는 천장이다.
상한은 기기와 macOS 버전마다 다르니 직접 확인한다. 설정 이름의 wired는 "스왑으로 내보내지 않고 GPU용으로 붙잡아 두는 메모리"라는 뜻이다. 32GB 맥 미니에서는 대략 25GB였다.
$ sysctl iogpu.wired_limit_mb iogpu.wired_limit_mb: 0 # 0이면 macOS 기본값을 쓰는 중 # 올리기 — 재부팅하면 기본값으로 돌아간다 $ sudo sysctl -w iogpu.wired_limit_mb=26624 # 26GB
이 값은 예약이 아니라 상한이다. 올려 두어도 모델을 띄우지 않으면 시스템은 평소처럼 메모리를 쓴다. 너무 높게 잡으면 모델이 OS 몫까지 가져가 시스템 전체가 느려지므로, 4절의 순서대로 정한다.
3.상한을 올렸는데 아무것도 안 바뀐다면
상한을 올리고 서버를 다시 띄웠는데, 서버가 받아 주는 입력 길이나 메모리 사용량이 그대로다. 이럴 때는 서버가 macOS 상한을 읽는지 확인한다.
서버 중에는 macOS 상한과 별개로 자기 기준(예: 전체 메모리의 몇 %)으로 쓸 메모리를 정하는 것이 있다. 그러면 macOS 쪽만 올려서는 소용이 없고, 서버 쪽 설정도 함께 올려야 한다. 반대로 서버 설정만 올리면 macOS 상한에 막힌다. 이 글에서 쓴 서버가 그랬다.
- 서버 문서에서 메모리 한도 설정을 찾는다. 환경 변수나 실행 옵션인 경우가 많다.
- 서버를 띄울 때 로그에 실제로 잡은 메모리 한도가 찍히는지 본다. 바꾼 값이 반영됐는지는 거기서 확인한다.
- 두 설정(macOS 상한, 서버 한도)은 같은 값으로 짝을 맞춰 둔다.
4.서버에 메모리를 더 줬더니 오히려 느려졌다면
여유 있게 서버에 28GB를 줬다. 모델이 편하게 돌 것 같았다. 실제로는 반대였다. 짧은 요청 하나에도 서버 메모리가 쭉 올라갔고, 나머지 프로그램이 스왑으로 밀려났다.
원인은 서버가 받은 메모리의 빈자리를 대화 캐시로 채우도록 만들어져 있다는 데 있었다. 끝난 요청의 계산 결과를 보관해 두면 다음 요청이 같은 앞부분으로 시작할 때 빨라진다. 좋은 기능이지만, 그 크기가 "모델 + 작업 공간을 빼고 남는 전부"였다. 서버에 준 메모리는 서버가 거의 다 쓴다고 보는 편이 맞다.
그래서 순서를 뒤집는다.
- OS와 함께 띄울 앱의 메모리를 먼저 잰다. 활성 상태 보기나
footprint명령으로 본다. 개발 중이라면 브라우저·편집기까지 넣는다. - 남은 양을 서버에 준다. 32GB 맥에서 27B 모델이라면 서버 26GB, 나머지 6GB 정도였다.
- 대화 캐시에 따로 상한을 건다. 상한이 없으면 서버 몫을 조금만 늘려도 늘어난 만큼 다시 캐시가 채운다. 실제로 재사용되는 가장 긴 대화 하나가 들어갈 정도면 충분하다.
5.설정을 지켰는데도 넘친다면
서버 메모리를 25GB로 정했는데, 실제 최고 사용량이 28GB 가까이 나왔다. 설정이 무시된 것이 아니다. 이 서버가 쓰는 MLX의 메모리 한도는 넘으면 막히는 벽이 아니라 목표에 가깝다. 계산 도중 잠깐 더 쓰는 순간이 있다.
두 가지를 챙긴다.
- 여유를 남긴다. 서버 설정과 실제 최고 사용량의 차이만큼은 OS와 다른 앱 몫에 더해 둔다.
- "메모리가 모자라다"는 거절에 대비한다. 서버는 요청을 받기 전에 메모리가 넘칠 것 같으면 거절하기도 한다. 이 응답은 요청이 잘못된 게 아니라 지금 자리가 없다는 뜻이므로, 클라이언트는 잠시 뒤 다시 시도하면 된다.
반대로 서버 몫을 지나치게 줄이면 긴 요청 하나가 들어갈 자리도 없어 매번 거절된다. 이 모델에서는 25GB에서 1만5천 토큰 요청이 거절됐고, 26GB로 올리자 들어갔다. 최소치는 실제로 보낼 가장 긴 요청으로 확인한다.
6.긴 입력은 따로 본다
서버가 입력을 6만 토큰까지 받아 준다고 해서 그만큼 쓸 수 있는 것은 아니다. 긴 입력을 처음 읽는 동안 메모리를 평소보다 훨씬 많이 쓰기 때문이다. 이 모델에서 3만 토큰을 넣었을 때 최고 사용량이 서버 한도 근처까지 올라갔고 다른 프로그램이 스왑으로 밀렸다.
문맥 캐시를 8bit로 줄이는 옵션이 이 순간을 줄여 줄 것 같지만, 이 서버의 설명서에는 답을 쓰는 동안의 메모리만 줄이고 입력을 읽는 순간의 최고치는 그대로라고 적혀 있다. 속도를 높이는 옵션도 아니다.
그래서 받아 줄 입력 길이(문맥 창)는 실제로 보낼 가장 긴 입력 + 답의 길이로 줄여 둔다. 이 환경에서는 약 3만7천 토큰으로 정했다. 입력이 길 때 속도가 어떻게 떨어지는지는 따로 정리했다.
맥에서 로컬 LLM 속도 미리 가늠하기: 메모리 대역폭과 투기적 디코딩
답을 쓰는 속도는 메모리 대역폭이 정한다. 입력이 3만 토큰이면 첫 글자가 나오기까지 2분이 걸렸다.
7.이 결론이 안 통하는 조건
- 대화 캐시를 미리 채우지 않는 서버. 4절은 받은 메모리를 캐시로 채우는 서버에 해당한다. 자기 서버가 남는 메모리를 무엇에 쓰는지 문서나 로그로 먼저 본다.
- 문맥 캐시가 큰 구조의 모델. 모든 층이 문맥 캐시를 쓰는 모델은 이 글보다 몇 배 크다. 긴 대화에서는 그쪽이 실제 원인이 될 수 있으니 1절의 식으로 계산한다.
- 메모리가 넉넉한 맥. 64GB, 128GB라면 OS가 굶는 일은 드물다.
- 여러 요청을 동시에 받는 서버. 요청마다 문맥 캐시가 따로 잡힌다.
8.정리
- OS와 함께 띄울 앱의 메모리부터 잰다.
- macOS의 GPU 메모리 상한을 확인한다. 필요하면 올린다.
- 서버가 자기 메모리 한도를 따로 두는지 확인하고, 두 값을 짝으로 맞춘다.
- 서버 몫은 1번의 나머지로 정하고, 대화 캐시에는 따로 상한을 건다.
- 설정과 실제 최고 사용량의 차이만큼 여유를 남긴다.
- 받아 줄 입력 길이는 실제 최대 입력 + 답의 길이로 줄인다.
서버에 메모리를 크게 주는 것으로는 풀리지 않았다. 늘린 만큼 캐시가 채웠다. 다른 프로그램 몫을 먼저 떼어야 모델 몫이 정해진다.
'프로그래밍 > AI' 카테고리의 다른 글
| 로컬 LLM이 매번 입력을 처음부터 다시 읽을 때: prefix 캐시 확인하는 법 (0) | 2026.09.25 |
|---|---|
| 맥에서 로컬 LLM 속도 미리 가늠하기: 메모리 대역폭과 투기적 디코딩 (0) | 2026.09.25 |
| AI가 만든 JSON이 계속 퇴짜맞을 때: LLM 호출을 75% 줄인 세 가지 처방 (0) | 2026.08.23 |
| 서브에이전트 위임의 기준 — 조직도가 아니라 작업 경계 (0) | 2026.07.20 |
| 하네스와 루프 — AI에게 일을 시키는 시스템 설계 (0) | 2026.07.20 |
댓글