글을 쓰면서..
로컬 LLM 환경 구축을 검색하면 열에 아홉은 curl | sh 로 Ollama 깔고 ollama run 한 줄 치면 끝, 이라고 말합니다. 나도 처음엔 그렇게 시작했다. 그런데 막상 내 머신에서 조금만 욕심을 내니까 “왜 이 모델은 안 뜨지”, “컨텍스트 좀 늘렸다고 갑자기 왜 이렇게 느려지지” 같은 질문이 줄줄이 나왔고, ollama run 가이드는 거기에 아무 답도 주지 못했다.
그래서 순서를 뒤집기로 했다. 설치부터 하지 말고, 메모리 예산부터 계산한 다음 그 결과로 모델과 양자화를 고르고, Ollama(빠른 길)와 llama.cpp 소스 빌드(다 뜯어보는 길)를 같은 GGUF 파일로 연결하는 흐름.
범위는 미리 정직하게 밝혀 둔다. 하드웨어 점검과 Ollama 는 이 머신에서 실제로 돌렸고, 뒤에 나오는 수치는 전부 그때 실제로 얻은 값이고, 중간에 넣은 터미널 그림은 그 출력을 그대로 옮겨 그린 것이다(화면 캡처가 아니라 출력 텍스트를 다시 렌더한 그림이다). 반면 llama.cpp 소스 빌드는 이번에 하지 않았다. 그쪽 명령과 옵션은 공식 빌드 문서를 기준으로 정리한 것이지 내 터미널 기록이 아니다. 어디까지가 내 실측이고 어디부터가 문서인지는 그때그때 표시해 두겠다.
내 환경부터 밝히면 이렇다.
- OS: Windows 위의 WSL2(Ubuntu). 순수 리눅스도 흐름은 같고, macOS(Apple Silicon, Metal)는 llama.cpp 가 기본 지원하니 빌드 옵션만 다르다.
- GPU: RTX 4060 Ti 8GB. 소비자용 카드고 VRAM 이 딱 8GB라 “어디까지 되나”를 계속 따져야 하는, 오히려 이 글에 딱 맞는 조건이다.
- 나: 셸이랑
cmake빌드는 익숙한데, 클라우드 API 만 써봤지 로컬로 모델을 띄워본 적은 없던 사람.
솔직히 관련 자료야 널렸지만, 정작 “왜 안 되는지”를 시스템 자원 관점에서 짚어 주는 글은 잘 없어서 나도 복습할 겸 정리했다. 인용하는 옵션 이름·기본값·파일 크기는 2026년 9월에 공식 문서랑 Hugging Face 에서 직접 확인한 값이지만, 버전 타니까 --help 와 문서를 같이 보시길.
로컬 LLM, 굳이 왜 깔았나와 스택 뜯어보기
클라우드 API 가 이렇게 싸고 편한데 왜 로컬에 띄우나. 내 경우엔 자동화 파이프라인 백엔드가 컸다. CLI 도구나 스크립트가 하루 종일 모델을 두들기는 워크로드는 토큰 과금이 무섭게 쌓이는데, 이미 GPU 가 책상 밑에 있으면 한계 비용이 전기료뿐이다. 여기에 사내 코드나 로그를 밖으로 못 보내는 프라이버시 문제, 네트워크 없이도 돌아야 하는 오프라인·재현 실험까지 더해지면 로컬로 내리는 게 답이 된다.
깔기 전에 스택이 어떻게 쌓이는지부터 봐야 나중에 “어느 층이 문제인지”를 짚을 수 있다. 나는 이걸 네 층으로 그려 놓고 시작했다.

- 모델 가중치 파일 — llama.cpp 계열은 단일 파일 포맷 GGUF, vLLM·Transformers 계열은 safetensors 를 쓴다. GGUF 는 양자화된 텐서에 토크나이저·아키텍처 메타데이터까지 한 파일에 들어 있어서, 이 파일 하나만 있으면 돈다. 이게 로컬에서 다루기 편한 결정적 이유다.
- 추론 엔진 — llama.cpp 는 C/C++ 로 짜인 추론 엔진이고 내부 텐서 라이브러리 이름이 ggml 이다. 의존성 없이 빌드되고 CPU(AVX2/AVX-512), CUDA, Metal 을 다 지원한다. vLLM 은 PyTorch 기반으로 PagedAttention·연속 배칭으로 동시 요청 처리량을 뽑는 서버인데, 데이터센터급 GPU 를 전제한다.
- 서빙 계층 — Ollama 는 다운로드·캐시·HTTP API 를 바이너리 하나로 묶은 래퍼고, 정작 추론은 내부의 llama.cpp/ggml 이 한다. llama.cpp 를 직접 빌드하면 같은 역할의
llama-server가 나온다. 그러니까 이 둘은 사실 남남이 아니다. - 클라이언트 — curl, Python
openaiSDK, 그리고 OpenAI 호환 주소만 받아먹는 CLI·에이전트 도구들.
세 선택지를 한 표로 눌러 보면 이렇다.
| 항목 | Ollama | llama.cpp (직접 빌드) | vLLM |
|---|---|---|---|
| 설치 난이도 | 스크립트 한 줄 | cmake 빌드 (5~15분) | pip 설치, CUDA/PyTorch 버전 맞추기 |
| 지원 하드웨어 | CPU, NVIDIA, AMD, Apple | CPU, NVIDIA, AMD, Apple, Vulkan 등 | NVIDIA(주), AMD 일부 |
| 모델 포맷 | GGUF (자체 레지스트리 + 로컬 파일) | GGUF | safetensors (HF Hub) |
| API | 자체 /api/* + OpenAI 호환 /v1/* |
OpenAI 호환 /v1/* + 자체 API |
OpenAI 호환 /v1/* |
| 튜닝 자유도 | Modelfile·환경변수 수준 | 모든 플래그 직접 제어, 양자화 도구 포함 | 서버 옵션 풍부 |
| 적합 용도 | 개인 개발, 빠른 실험 | 자원 통제·임베디드·학습 목적 | 다중 사용자 서비스, 고처리량 배치 |
나는 8GB 카드 한 장이라 vLLM 은 애초에 후보가 아니었고, 이 글도 앞의 두 경로만 깊게 판다. vLLM 은 24GB 이상 VRAM 으로 동시 요청을 많이 받아야 할 때 꺼내면 된다.
여기서 처음 막혔다 — 메모리 예산 계산과 WSL2 함정
로컬 LLM 환경 구축의 성패는 사실상 “모델이 메모리에 들어가느냐” 하나로 갈린다. 그래서 뭘 깔기 전에 내 머신부터 재봤다.
# GPU 이름과 VRAM 총량 (NVIDIA)
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv
# 시스템 RAM, CPU 코어 수, CPU 모델
free -h
nproc
lscpu | grep -E 'Model name|^CPU\(s\)'
grep -o -w -E 'avx2|avx512f' /proc/cpuinfo | sort -u # CPU 추론에 직결되는 SIMD 지원 여부
음.. 내 머신에서는 이렇게 나온다.

GPU 는 RTX 4060 Ti, VRAM 8188MiB, 드라이버 591.86. CPU 는 AMD Ryzen 5 7500F 6코어에 nproc 이 12로 나오니 스레드는 12개고, avx2 와 avx512f 가 둘 다 잡힌다. CPU 로 밀어야 할 때 최소한 SIMD 는 제대로 얹힌다는 뜻이라 여기까진 마음이 놓였다.
문제는 free -h 줄이었다. 여기서 뒤통수를 맞았다. 내 데스크톱에 꽂혀 있는 RAM 은 분명 32GB인데, WSL 안에서 free -h 를 치니까 total 이 태연하게 15Gi 라고 나온다. 처음엔 램 한 짝이 빠졌나 싶어서 Windows 작업 관리자까지 열어 봤다. 거긴 멀쩡히 32GB.
한참 헤매다 원인을 찾았는데, 내 착각이 아니라 WSL2 의 기본 동작이었다. .wslconfig 의 memory 기본값이 Windows 전체 RAM 의 50%다. Microsoft 의 WSL 설정 문서에 그렇게 박혀 있다. 32GB의 절반이면 16GB, 거기서 WSL 커널이 먹는 몫을 빼면 사용자에게 보이는 total 이 15Gi 로 찍힌다. 숫자가 정확히 맞아떨어졌다.
내 머신은 지금도 이 상한이 걸린 상태 그대로다. 8GB 카드에 올릴 모델만 만지는 동안엔 굳이 손댈 이유가 없어서 두고 쓰는 중이다. 다만 CPU 로 큰 모델을 올릴 생각이라면 이 15Gi 가 그대로 천장이 되니, 그땐 %UserProfile%\.wslconfig 에 이렇게 적고
[wsl2]
memory=24GB
PowerShell 에서 wsl --shutdown 으로 한 번 재시작해 주면 상한이 올라간다. (WSL 은 종료됐다가 다음 접속 때 새 설정으로 뜬다.) 이걸 모르면 “32GB 머신인데 왜 CPU 추론이 스왑 물고 기어가지” 하면서 며칠 날린다. 참고로 GPU VRAM 은 이 제한이랑 아무 상관이 없다 — .wslconfig 를 아무리 올려도 4060 Ti 의 8188MiB 는 8188MiB 그대로다.
가중치 메모리는 그냥 파일 크기다
가중치가 먹는 메모리는 거의 파일 크기와 같다. llama.cpp 가 mmap 으로 파일을 그대로 올리기 때문이다. 그래서 계산이 단순하다. 파라미터 수 × 파라미터당 바이트.
| 정밀도 | 파라미터당 바이트 | 8B 모델 환산 |
|---|---|---|
| FP16 / BF16 | 2 | 약 16GB |
| Q8_0 | 약 1.06 (8.5 bit) | 약 8.5GB |
| Q4_K_M | 약 0.61 (4.9 bit) | 약 4.9GB |
“약”이 붙는 건 K-quant 계열이 텐서마다 비트 수를 섞고 블록별 스케일 값이 더 붙기 때문이다. llama.cpp 의 quantize 문서에 실린 Llama 3.1 8B 실측이 Q4_K_M 4.89 bit/가중치, Q8_0 8.50 bit/가중치다. 손계산이 진짜 맞나 싶어서, Hugging Face 에 올라온 GGUF 파일 크기로 대조해 봤다(2026년 9월, GiB 단위).
| 모델 | FP16/BF16 | Q8_0 | Q4_K_M | Q4_K_M 이 들어가는 하드웨어 |
|---|---|---|---|---|
| Llama 3.2 3B | 5.99 | 3.19 | 1.88 | 거의 모든 GPU, 8GB RAM 노트북 |
| Llama 3.1 8B | 14.96 | 7.95 | 4.58 | 8GB VRAM(여유 약 2GB), CPU 16GB RAM |
| Qwen3 14B | 27.51 | 14.62 | 8.38 | 12~16GB VRAM, CPU 32GB RAM |
| Qwen3 32B | 61.03 | 32.43 | 18.40 | 24GB VRAM, CPU 64GB RAM |
크기 출처: Hugging Face 저장소
bartowski/Llama-3.2-3B-Instruct-GGUF,bartowski/Meta-Llama-3.1-8B-Instruct-GGUF,bartowski/Qwen_Qwen3-14B-GGUF,bartowski/Qwen_Qwen3-32B-GGUF에 올라온 파일 크기(GiB), 2026년 9월 확인.

내 4060 Ti 8GB 기준으로 보면 8B Q4_K_M(4.58GiB)이 상한선이라는 게 표에서 그대로 보인다. “8GB 카드엔 8B Q4 까지”라는 흔한 경험칙이 어디서 나오는지 이제 알겠다. 그런데 이 표에는 아직 안 들어간 놈이 하나 있다.
KV 캐시 — 이거 빼먹으면 컨텍스트 늘릴 때 터진다
추론 중엔 가중치 말고 KV 캐시가 또 필요하다. 지금까지 처리한 모든 토큰의 Key/Value 벡터를 레이어마다 쌓아 두는 공간이고, 대략 이렇게 잡는다.
KV 캐시(바이트) ≈ 2 (K와 V) × 레이어 수 × KV 헤드 수 × 헤드 차원 × 정밀도(바이트) × 컨텍스트 길이
Llama 3.1 8B 설정값(레이어 32, KV 헤드 8, 헤드 차원 128)을 FP16 으로 넣으면 토큰당 2 × 32 × 8 × 128 × 2 = 131,072 바이트, 즉 토큰당 128KiB. 8,192 토큰이면 딱 1GiB, 32K 면 4GiB, 131K 를 꽉 채우면 16GiB 로 가중치보다 커진다. 여기서 두 가지가 보인다.
하나, 손계산을 해보면 8B Q4_K_M 가중치 약 4.6GiB + 8K 컨텍스트 KV 캐시 약 1GiB + CUDA 컨텍스트·작업 버퍼 수백 MB 라서, 내 8GB 카드에 들어가긴 하는데 여유가 크지 않다. 컨텍스트를 16K 로 올리는 순간 넘친다. 이건 뒤에서 num_ctx 를 32K 까지 올려 실제로 눈으로 확인한다.
둘, 이 수치는 모델 아키텍처마다 다르다. 같은 8B라도 Qwen3 8B 는 레이어가 36개라 토큰당 144KiB 이고, GQA 안 쓰는 구형 모델은 몇 배가 된다. 그래서 “대략”이라고 쓸 수밖에 없고, 정확한 값은 서버 로그의 KV 버퍼 크기로 확인하는 게 맞다. 컨텍스트 늘려 놓고 메모리 왜 부족하냐 싶으면 십중팔구 이 항목이다.
VRAM 을 넘으면 — 오프로딩과 그 대가
가중치+KV 가 VRAM 을 넘으면 llama.cpp 는 레이어 일부만 GPU 에 올리고 나머지는 CPU 로 돌린다(-ngl, --n-gpu-layers). Ollama 도 알아서 같은 짓을 한다. 문제는 속도다. 토큰 하나 뽑을 때마다 모든 레이어 가중치를 한 번씩 읽어야 하는데, GPU 의 GDDR 은 수백 GB/s~1TB/s 급이고 CPU 의 DDR5 듀얼채널은 겨우 수십 GB/s 다. 레이어 절반이 CPU 로 넘어가면 그 절반이 열 배 이상 느린 메모리에서 읽히니까 전체 속도가 GPU 만 쓸 때의 몇 분의 일로 곤두박질친다. “GPU 있는데 왜 CPU 처럼 기어가지” 싶으면 제일 먼저 오프로딩부터 의심하면 된다.
경로 A — Ollama 로 10분 컷 (WSL2 드라이버는 건드리지 마라)
메모리 예산이 섰으니 제일 빠른 길부터. 다만 WSL2 라면 설치 전에 확인하고 갈 게 하나 있다.
WSL2 의 GPU 는 Windows 쪽 드라이버만 깔면 WSL 안에서 libcuda.so 가 자동으로 매핑되는 구조다. 그래서 GPU 를 붙이겠다고 WSL 안에서 sudo apt install nvidia-driver-550 같은 걸 때리면 그 매핑을 덮어써서 인식이 통째로 깨진다. NVIDIA 의 CUDA on WSL 가이드가 “WSL 안에 리눅스 디스플레이 드라이버를 설치하지 말라”고 못을 박아 둔 이유다. 리눅스에 익숙한 사람일수록 반사적으로 손이 가는 자리라, 여기서 흔히 막힌다.
원칙은 단순하다. Windows 쪽 NVIDIA 드라이버만 최신으로 유지하고, WSL 안에는 CUDA 툴킷만(그것도 WSL-Ubuntu 전용 패키지로) 깐다. 내 환경이 딱 이 상태라서 WSL 안에서 nvidia-smi 가 드라이버 591.86 / 8188MiB 를 정상으로 뱉고(앞의 하드웨어 점검 그림), Ollama 도 그 위에서 CUDA 백엔드로 GPU 를 그대로 잡았다.
# 1) 설치 (Linux/WSL2 공통). systemd 서비스로 등록되고 GPU 를 자동 감지한다.
curl -fsSL https://ollama.com/install.sh | sh
# 2) 모델 받고 대화. 태그를 생략하면 latest, 명시하면 크기/양자화를 고른다.
ollama pull llama3.1:8b
ollama run llama3.1:8b
# 3) 다른 터미널에서: 얼마나 GPU 에 올라갔는지 확인
ollama ps
ollama ps 의 PROCESSOR 열이 100% GPU 면 전부 VRAM 에 올라간 거고, xx%/yy% CPU/GPU 로 나오면 오프로딩이 일어난 거다. 그럼 더 작은 양자화나 모델로 내리거나 컨텍스트를 줄여야 한다. 내 4060 Ti 에선 8B Q4_K_M 이 기본 컨텍스트일 땐 100% GPU 로 잘 올라갔다.
기본 컨텍스트가 생각보다 짧다
내가 깐 Ollama 는 0.33.3 인데, 서버를 띄우면 로그에 이런 줄이 찍힌다.
msg="vram-based default context" total_vram="8.0 GiB" default_num_ctx=4096
VRAM 을 보고 기본 컨텍스트를 정하는 구조고, 내 8GB 카드에선 그 값이 4,096 토큰으로 잡혔다(Ollama FAQ 가 말하는 기본값과 같은 값이다). 긴 문서를 넣으면 이 창을 넘는 부분이 조용히 잘려 나가는데, 에러가 안 나니까 알아채기가 어렵다. 답이 이상한데 프롬프트를 아무리 고쳐도 그대로면 여기부터 의심하는 게 맞다. 조정은 세 갈래.
# (a) 서버 전체 기본값을 바꾸기
OLLAMA_CONTEXT_LENGTH=8192 ollama serve
# (b) API 요청마다 options.num_ctx 지정
curl http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "GGUF 가 무엇인지 두 문장으로 설명해줘",
"stream": false,
"options": { "num_ctx": 8192 }
}'
(c)는 Modelfile 로 파생 모델을 굽는 방법이다. 시스템 프롬프트까지 같이 박아 둘 수 있어서 “사내 코드 리뷰어” 같은 용도별 모델을 만들 때 편하다.
# Modelfile
FROM llama3.1:8b
PARAMETER num_ctx 8192
PARAMETER temperature 0.2
SYSTEM 당신은 한국어로 답하는 C/C++ 코드 리뷰어입니다. 근거 없는 추측은 하지 않습니다.
ollama create c-reviewer -f Modelfile
ollama run c-reviewer
앞에서 계산했듯 num_ctx 8192 는 8B 기준 KV 캐시 약 1GiB 를 더 요구한다. 그럼 실제로 얼마나 아픈지, 아예 8배로 올려서 재봤다.
컨텍스트를 8배로 올렸더니 속도가 반토막 났다
llama3.1:8b(ollama list 기준 4.9GB)를 기본값으로 띄워 ollama ps 와 nvidia-smi 를 같이 찍고, num_ctx 만 32768 로 올려서 똑같이 찍었다. 모델은 D 드라이브(/mnt/d/ollama-models)에 받아 뒀다.

숫자만 뽑으면 이렇다.
| 측정값 | 기본 (num_ctx 4096) |
num_ctx 32768 |
|---|---|---|
ollama ps SIZE |
5.3 GB | 9.5 GB |
| PROCESSOR | 100% GPU | 34%/66% CPU/GPU |
nvidia-smi 사용량 |
6022 MiB / 8188 MiB | 6958 MiB / 8188 MiB |
| 생성 속도(eval rate) | 46.39 tok/s | 20.70 tok/s |
컨텍스트를 8배로 올렸더니 모델이 요구하는 메모리가 5.3GB 에서 9.5GB 로 뛰었다. 8188MiB 카드에 9.5GB 가 들어갈 리 없으니 Ollama 가 34% 를 CPU 로 내려버렸고, 생성 속도는 46.39 tok/s 에서 20.70 tok/s 로 절반 이하가 됐다. 앞에서 손계산한 토큰당 128KiB 를 늘어난 28,672 토큰에 곱하면 3.5GiB 인데, 실제 증가분 4.2GB 와 얼추 맞아떨어진다. KV 캐시가 범인이라는 게 눈으로 확인된 셈이다.
정작 재미있는 건 nvidia-smi 쪽이다. VRAM 사용량은 6022MiB → 6958MiB 로 1GB 도 안 늘었다. GPU 가 꽉 차서 나머지를 시스템 RAM 으로 흘린 거니까 당연한데, VRAM 그래프만 보고 있으면 “아직 800MiB 남았네” 하고 착각하기 딱 좋다. 오프로딩이 일어났는지는 nvidia-smi 가 아니라 ollama ps 의 PROCESSOR 열로 봐야 한다. 컨텍스트를 올린 뒤엔 여기부터 다시 보는 습관을 들이자.
엔드포인트는, Ollama 가 기본으로 127.0.0.1:11434 에 바인딩하고, 자체 API(POST /api/generate, POST /api/chat) 외에 OpenAI 호환 POST /v1/chat/completions 를 준다. 그래서 기존 코드를 거의 그대로 꽂을 수 있다. 예제는 마지막 섹션에서 몰아서 다룬다.
경로 B — llama.cpp 소스 빌드 (옛 옵션 LLAMA_CUBLAS 주의)
Ollama 가 감춰 둔 걸 전부 손에 쥐고 싶으면 llama.cpp 를 직접 빌드한다. 빌드하면 build/bin/ 에 대화용 llama-cli, HTTP 서버 llama-server, 양자화 도구 llama-quantize, 벤치 llama-bench 가 나온다. 앞에서 밝혔듯 이 경로는 이번에 내가 빌드해 보지 않았다. 아래 명령과 옵션은 공식 빌드 문서를 정리한 것이니, 그렇게 읽어 달라.
먼저 옵션 이름부터 짚고 간다. 인터넷에 굴러다니는 오래된 글에는 CUDA 빌드가 cmake -B build -DLLAMA_CUBLAS=ON 으로 적혀 있는데, 이 이름은 지금 안 먹는다. 현재 공식 빌드 문서 기준 CUDA 옵션은 GGML_CUDA 다. 고약한 건 존재하지 않는 CMake 옵션을 줘도 빌드가 그냥 성공한다는 점이다. 경고 한 줄 없이 무시되고 CPU 빌드가 나오니, 나중에 “빌드는 됐는데 GPU 를 안 쓴다”로 돌아온다. 오래된 명령어를 복붙할 때 제일 조심할 대목이다.
sudo apt install -y build-essential cmake git
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
# CPU 전용 빌드
cmake -B build
cmake --build build --config Release -j "$(nproc)"
# CUDA 빌드 (WSL-Ubuntu 용 CUDA 툴킷이 깔려 있어야 nvcc 를 찾는다)
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j "$(nproc)"
기본 빌드는 빌드 머신 GPU 아키텍처에 맞춰 최적화(GGML_NATIVE)되니까, 다른 GPU 로 바이너리를 옮길 거면 -DGGML_NATIVE=OFF 를 붙이거나 -DCMAKE_CUDA_ARCHITECTURES="86;89" 로 대상을 명시하자.
GGUF 받아서 띄우기
Hugging Face GGUF 저장소에서 원하는 양자화 파일 하나만 받으면 된다. llama.cpp 도구들은 저장소:양자화 형식의 -hf 옵션으로 다운로드·캐시를 대신해 주기도 한다.
# 방법 1: llama.cpp 가 직접 받는다 (기본 양자화는 Q4_K_M)
./build/bin/llama-cli -hf bartowski/Meta-Llama-3.1-8B-Instruct-GGUF:Q4_K_M
# 방법 2: 파일을 직접 받아 둔다 (huggingface_hub 의 hf CLI, 구버전은 huggingface-cli)
pip install -U huggingface_hub
hf download bartowski/Meta-Llama-3.1-8B-Instruct-GGUF \
Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf --local-dir ./models
# OpenAI 호환 서버. -ngl 99 는 "가능한 모든 레이어를 GPU 로", -c 는 컨텍스트 길이
./build/bin/llama-server -m ./models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf -ngl 99 -c 8192
llama-server 는 기본으로 127.0.0.1:8080 에서 대기하고, 브라우저로 열면 내장 웹 UI 가 뜬다. 서버 로그에 모델 버퍼 크기랑 KV 캐시 버퍼 크기가 그대로 찍히니까, 앞에서 한 손계산이 맞았는지 여기서 눈으로 검증할 수 있다.
그리고 여기서 KV 캐시 얘기가 실전으로 돌아온다. -c 를 생략하면 llama-server 는 모델 메타데이터에 적힌 학습 컨텍스트 길이를 그대로 쓰려 드는데, Llama 3.1 은 그게 131,072 다. 앞의 손계산대로면 KV 캐시만 16GiB 를 요구하는 값이라 8GB 카드에 들어갈 방법이 없다. 컨텍스트는 반드시 -c 로 명시하자. 8GB VRAM 이면 -c 8192 정도가 무난한 출발점이다.
방금 Ollama 에서 본 게 정확히 이 현상의 순한 버전이다. 거기선 컨텍스트를 32K 로 올리자 카드에서 밀려난 만큼이 CPU 로 내려가 속도가 반토막 나는 선에서 끝났지만, -ngl 로 전부 GPU 에 올리라고 못박아 둔 상태라면 자리가 없을 때 로딩 단계에서 그냥 죽는다. 어느 쪽이든 원인은 하나, 컨텍스트 길이다.
직접 양자화해서 계산 검증하기
메모리 표 수치가 진짜 나오나 확인하는 가장 확실한 방법은 직접 양자화해 보는 거다. F16 GGUF 를 받아 llama-quantize 에 넣고 크기를 비교하면 된다.
./build/bin/llama-quantize ./models/model-f16.gguf ./models/model-Q4_K_M.gguf Q4_K_M
ls -lh ./models/
Llama 3.1 8B 기준 F16 약 15GiB 가 Q4_K_M 약 4.6GiB 로 줄어든다(파라미터당 2바이트 → 약 0.61바이트, 약 3.3배). 앞 표랑 딱 맞는다. 양자화는 CPU 만으로 몇 분이면 끝나고, 결과물은 Ollama 에서도 그대로 먹는다.
그래서 두 경로는 결국 하나로 만난다. Ollama 는 속에 llama.cpp/ggml 을 품고 있어서, 위에서 만든 GGUF 를 Modelfile 한 줄로 등록할 수 있다.
# Modelfile
FROM ./models/model-Q4_K_M.gguf
PARAMETER num_ctx 8192
ollama create my-q4 -f Modelfile
ollama run my-q4
정리하면 “모델 파일은 llama.cpp 도구로 만들고 관리하되, 일상 사용은 Ollama 편의성에 맡긴다”는 조합이 가능하다. 반대로 Ollama 가 받아 둔 blob 도 그냥 GGUF 라서 llama-server -m 으로 바로 열린다.
도구에 꽂고 성능 만지기
서버가 떴으면 나머지는 주소 문제다. Ollama 는 11434, llama-server 는 8080 이라는 것만 다르고 경로는 똑같다.
# llama-server (포트 8080). Ollama 면 11434 로 바꾸고 model 에 태그명을 넣는다.
curl -s http://localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "local",
"messages": [
{"role": "system", "content": "간결하게 한국어로 답한다."},
{"role": "user", "content": "mmap 으로 모델을 읽으면 왜 로딩이 빠른가?"}
],
"temperature": 0.2
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["choices"][0]["message"]["content"])'
Python 이면 openai SDK 의 base_url 만 바꾸면 된다. api_key 는 SDK 가 빈 값을 거부하니까 아무 문자열이나 넣는다(Ollama 문서도 “필수지만 무시됨”이라고 한다).
from openai import OpenAI
# Ollama 라면 base_url="http://localhost:11434/v1", model="llama3.1:8b"
client = OpenAI(base_url="http://localhost:8080/v1", api_key="local")
resp = client.chat.completions.create(
model="local",
messages=[
{"role": "system", "content": "간결하게 한국어로 답한다."},
{"role": "user", "content": "KV 캐시가 컨텍스트 길이에 비례하는 이유를 설명해줘."},
],
temperature=0.2,
)
print(resp.choices[0].message.content)
OpenAI 호환 API 를 받는 CLI 도구나 에이전트 프레임워크면 대개 OPENAI_BASE_URL(또는 동등한 설정)과 모델명만 바꿔 같은 식으로 붙는다. 다만 도구마다 function calling·스트리밍·구조화 출력 지원 범위가 다르니, 붙이기 전에 그 도구가 실제로 때리는 엔드포인트가 뭔지 확인하는 게 안전하다.
속도는 남의 벤치 말고 내가 잰다
토큰/초(tok/s) 절대치는 하드웨어마다 천차만별이라 남의 벤치는 참고만 하고 내 머신에서 직접 재는 게 맞다. 재는 방법은 두 가지다.
# 프롬프트 처리(pp512)와 생성(tg128) 속도를 t/s 로 출력
./build/bin/llama-bench -m ./models/model-Q4_K_M.gguf -ngl 99 -p 512 -n 128
# Ollama: 응답 끝에 prompt eval rate / eval rate 가 tokens/s 로 찍힌다
ollama run llama3.1:8b --verbose
--verbose 는 답변 끝에 측정치를 그대로 붙여 준다. 내 4060 Ti 에서 llama3.1:8b 를 기본 컨텍스트로 돌린 결과가 이거다.

맨 아랫줄 eval rate 46.64 tok/s 가 생성 속도(tg)다. 같은 조건으로 한 번 더 돌린 실행은 85토큰을 1.83초에 뽑아 46.39 tok/s 였으니, 이 머신의 8B Q4 생성 속도는 46 tok/s 언저리라고 보면 되겠다. 사람이 읽는 속도보다 한참 빨라서 체감상 답답할 일은 없다.
위쪽 prompt eval rate 가 3.90 tok/s 로 참담해 보이는데, 이건 겁먹을 숫자가 아니다. 프롬프트 21토큰 중 20토큰이 캐시에 맞아서(prompt eval cached: 20) 실제로 처리한 게 1토큰뿐이라 나온 값이다. 짧은 프롬프트의 pp 수치는 이런 식으로 의미가 없어서, pp 를 제대로 보려면 llama-bench 의 -p 512 같은 걸 써야 한다.
반대쪽 숫자는 앞 섹션에서 이미 봤다. num_ctx 를 32768 로 올려 34% 가 CPU 로 내려간 상태에선 91토큰 / 4.40초, 20.70 tok/s 였다. 레이어의 3분의 1이 DDR5 로 내려간 것만으로 속도가 절반 이하다. 아래 표의 -ngl 이 왜 맨 위에 있는지가 이 두 수치에 다 들어 있다.
llama-bench 는 두 수치를 따로 준다. 프롬프트 처리(pp) 는 연산 병목이라 GPU 코어 성능을, 토큰 생성(tg) 은 메모리 대역폭을 반영한다. 긴 문서 넣었을 때 답이 늦게 시작되면 pp 가, 답이 한 글자씩 느리게 나오면 tg 가 문제다. 내 두 측정도 결국 tg 얘기다. 전부 GPU 면 46 tok/s, 3분의 1이 CPU 로 내려가면 20 tok/s. 그 경계가 정확히 어디인지는 각자 카드에서 직접 재보는 수밖에 없다.
체감 차이 큰 옵션 세 개만 추리면 이렇다.
| 옵션 (llama.cpp / Ollama) | 효과 | 주의 |
|---|---|---|
-ngl N / 자동 |
GPU 에 올릴 레이어 수. 전부 올라가야 빠르다 | 넘치면 OOM 대신 조용히 느려질 수 있음 |
-t N / 자동 |
CPU 추론 스레드 수. 물리 코어 수에 맞추는 게 보통 최적 | 하이퍼스레드까지 채우면 오히려 느려지기도 |
-fa on / OLLAMA_FLASH_ATTENTION=1 |
플래시 어텐션. 긴 컨텍스트 메모리·속도 개선 | 기본값이 이미 켜는 경우가 많음 |
KV 캐시를 양자화하는 옵션(llama.cpp -ctk q8_0 -ctv q8_0, Ollama OLLAMA_KV_CACHE_TYPE=q8_0)도 있다. 플래시 어텐션이 켜진 상태에서 KV 캐시 메모리를 반으로 줄여 주니까, 컨텍스트가 커서 VRAM 이 빠듯할 때 제일 먼저 시도할 만하다. 8GB 카드론 특히 고마운 옵션이다.
자주 밟는 지뢰 정리
- 내가 직접 확인한 것과, 공식 문서가 “이러지 마라”고 못박은 것들을 한 표로 묶었다.
| 증상 | 원인 | 조치 |
|---|---|---|
| 로딩 중 OOM, 서버 즉시 종료 | 가중치 + KV 캐시 > VRAM | 컨텍스트(-c/num_ctx) 줄이기 → KV 캐시 양자화 → 더 낮은 양자화 → -ngl 줄여 일부 CPU 오프로딩 |
| GPU 있는데 CPU 처럼 느림 | 오프로딩 발생, 또는 CPU 빌드 사용 | ollama ps·서버 로그의 오프로드 레이어 수 확인, -DGGML_CUDA=ON 빌드인지 확인 |
| WSL2 에서 GPU 미인식 | WSL 안에 리눅스 드라이버를 깔았거나 Windows 드라이버가 구버전 | WSL 내 드라이버 제거, nvidia-smi WSL 동작 확인, Windows 드라이버 갱신 |
| 긴 입력이 잘려 나감 | 기본 컨텍스트 4,096 | num_ctx/-c 명시 |
| CPU 추론이 이상하게 느림 | WSL2 메모리 상한(50%)에 걸려 스왑 | .wslconfig 의 memory 상향 |
| CUDA 빌드했는데 GPU 안 씀 | 옛 옵션 LLAMA_CUBLAS 사용 |
-DGGML_CUDA=ON 으로 재빌드 |
마치며 — 어느 길로 갈지
메모리 계산부터 순서를 다시 밟아 보고 내가 내린 결론은 이렇다.
- Ollama 로 충분한 경우: 혼자 쓰는 개발 환경, 에디터·CLI 도구 백엔드, 모델을 자주 갈아 끼우며 실험할 때. 개인 사용은 대개 여기서 끝난다.
- llama.cpp 로 내려가야 하는 경우: 자원이 빠듯해서 레이어·스레드·KV 캐시 타입을 손으로 통제해야 할 때, 직접 양자화·변환이 필요할 때, 임베디드나 컨테이너에 최소 의존성으로 넣을 때, 그리고 추론 엔진 속을 공부하고 싶을 때. 내 8GB 카드처럼 여유가 없을수록 이쪽 통제력이 아쉬워진다.
- vLLM 을 볼 경우: 여러 사용자 동시 요청, 24GB 이상 VRAM, safetensors 원본을 그대로 써야 할 때.
어느 쪽이든 출발점은 똑같다. 파라미터 수와 양자화 비트로 가중치 크기를 구하고, 컨텍스트 길이로 KV 캐시를 더한 뒤, 그 합이 내 VRAM(또는 RAM) 안에 들어가는지 본다. 이 손계산 하나 해두면 로컬 LLM 환경 구축에서 마주치는 문제 대부분은 미리 설명이 된다.
이번에 num_ctx 하나 만졌다고 46.39 tok/s 가 20.70 tok/s 로 주저앉는 걸 직접 보고 나니 생각이 좀 바뀌었다. “돌아가긴 하는데 왜 느리지”의 답은 대개 이 계산 안에 있다.
다음엔 미뤄 둔 llama.cpp 를 실제로 빌드해서 llama-bench 로 pp/tg 를 갈라 재보고, KV 캐시 양자화가 8GB 카드에서 얼마나 벌어 주는지까지 실측으로 붙여 보겠다. 혹시 4060 Ti 급에서 30B 를 그럭저럭 굴리는 조합을 찾은 분 있으면 알려주시면 고맙겠습니다.
자주 묻는 질문
8GB VRAM 이면 몇 B 모델까지 되나요?
8B 를 Q4_K_M 으로 받으면 파일이 4.58GiB 고, 여기에 8K 컨텍스트 KV 캐시 약 1GiB 를 더해도 8188MiB 안에 들어갑니다. 실제로 내 RTX 4060 Ti 에서 llama3.1:8b 는 기본 컨텍스트 4096 일 때 ollama ps 가 100% GPU 로, nvidia-smi 가 6022MiB 로 찍혔다. 반면 14B Q4_K_M 은 8.38GiB 라 가중치만으로 이미 넘치니, 8GB 카드에선 8B Q4 가 사실상 상한선이라고 보면 된다.
GPU 가 있는데 왜 CPU 처럼 느린가요?
십중팔구 오프로딩입니다. 모델이 VRAM 을 넘으면 레이어 일부가 CPU 로 내려가고, 그 부분은 수십 GB/s 짜리 DDR5 에서 읽히니 전체가 같이 느려진다. 문제는 nvidia-smi 로는 이게 잘 안 보인다는 것이다 — 내 실험에서 VRAM 사용량은 6022MiB 에서 6958MiB 로 거의 그대로였다. ollama ps 의 PROCESSOR 열이 100% GPU 인지 34%/66% CPU/GPU 인지부터 확인하세요.
컨텍스트를 늘렸더니 왜 갑자기 느려지나요?
KV 캐시가 컨텍스트 길이에 비례해 늘기 때문입니다. num_ctx 를 4096 에서 32768 로 올렸더니 ollama ps 의 SIZE 가 5.3GB 에서 9.5GB 로 뛰었고, 생성 속도는 46.39 tok/s 에서 20.70 tok/s 로 절반 이하가 됐다. 늘어난 28,672 토큰에 8B 기준 토큰당 128KiB 를 곱하면 3.5GiB 니까 숫자도 대충 맞아떨어진다. 컨텍스트는 필요한 만큼만 올리는 게 맞다.
WSL2 인데 32GB 램이 15Gi 로 나옵니다
램이 빠진 게 아니라 WSL2 의 기본 동작입니다. .wslconfig 의 memory 기본값이 호스트 RAM 의 50% 라, 32GB 머신이면 16GB 가 상한이고 커널 몫을 빼면 free -h 에 15Gi 로 보인다. CPU 로 큰 모델을 올릴 거면 %UserProfile%\.wslconfig 에서 값을 올린 뒤 wsl --shutdown 으로 재시작하면 된다. GPU VRAM 은 이 설정과 무관하다.
Ollama 와 llama.cpp 중 뭘 써야 하나요?
혼자 쓰는 개발 환경이거나 모델을 자주 갈아 끼우며 실험하는 단계라면 Ollama 로 충분합니다. 레이어 배치·스레드 수·KV 캐시 타입을 손으로 붙잡아야 하거나, GGUF 를 직접 양자화·변환해야 하거나, 최소 의존성으로 컨테이너에 넣어야 할 때 llama.cpp 를 직접 빌드할 이유가 생긴다. 둘은 같은 GGUF 파일을 쓰니 나중에 갈아타도 모델을 다시 받을 필요는 없다.
핵심 요약
- 8B Q4_K_M 은 8GB VRAM(RTX 4060 Ti, 8188MiB)에 100% GPU 로 다 올라간다 — 기본 컨텍스트 4096 에서 적재 5.3GB, VRAM 6022MiB.
- 컨텍스트를 4096 에서 32768 로 올리는 순간 34%/66% CPU/GPU 로 쪼개지고, 생성 속도가 46.39 tok/s 에서 20.70 tok/s 로 절반 이하가 된다.
- 그래서 설치보다 메모리 예산이 먼저다. 가중치는 사실상 GGUF 파일 크기이고, 여기에 KV 캐시(8B·8K 컨텍스트 기준 약 1GiB)를 더해 VRAM 과 비교하면 된다.
- WSL2 는
.wslconfig의 메모리 기본 상한이 호스트 RAM 의 50% 라, 32GB 머신인데도free -h가 15Gi 로 나온다. - WSL 안에 리눅스 NVIDIA 드라이버를 깔면 GPU 인식이 통째로 깨진다. Windows 쪽 드라이버만 최신으로 두고, WSL 안에는 CUDA 툴킷만 깐다.
참고 자료
- Ollama 공식 문서 — 설치, FAQ(컨텍스트 길이·환경변수), Modelfile 레퍼런스
- Ollama API 문서 — 자체 API 와 OpenAI 호환
/v1/*지원 범위 - llama.cpp 빌드 문서 —
GGML_CUDA등 백엔드별 CMake 옵션 - llama-quantize README — 양자화 종류별 bit/가중치·파일 크기 실측표
- Hugging Face GGUF 문서 — GGUF 포맷 설명
- NVIDIA CUDA on WSL 가이드 — WSL2 GPU 드라이버 설치 원칙
- Microsoft WSL 설정 문서 —
.wslconfig의memory기본값
