로컬 LLM 실행 완벽 가이드: Ollama·LM Studio·llama.cpp
내 GPU/맥북에서 LLM을 돌리는 세 가지 길과, GGUF 양자화를 제대로 고르는 법
클라우드 API는 편하지만, 로컬 LLM이 필요한 순간이 분명히 있다. 데이터가 외부로 나가면 안 되는 경우(의료·법무·사내 코드), 토큰 비용이 누적되는 대량 배치 작업, 오프라인/에어갭 환경, 그리고 레이턴시·프라이버시를 직접 통제하고 싶을 때다. 한편으로 로컬 실행은 "모델을 다운로드해서 실행"만으로 끝나지 않는다. 같은 7B 모델이라도 양자화 방식 하나로 메모리 8GB에 들어가기도 하고 16GB를 넘기기도 하며, 정확도가 눈에 띄게 무너지기도 한다.
이 가이드는 로컬 LLM 실행의 사실상 표준이 된 세 도구를 다룬다. llama.cpp는 C/C++로 작성된 추론 엔진이자 이 생태계 전체의 뿌리다. Ollama는 llama.cpp를 감싸 docker pull처럼 모델을 관리하게 해주는 CLI/서버다. LM Studio는 같은 엔진 위에 GUI와 모델 탐색기를 얹은 데스크톱 앱이다. 세 도구는 모두 GGUF라는 단일 파일 포맷과 **양자화(quantization)**라는 공통 개념을 공유한다. 이 둘을 이해하면 도구 선택은 취향 문제가 된다.
전체 흐름은 이렇게 잡으면 된다.
| 단계 | 핵심 질문 | 이 가이드의 섹션 |
|---|---|---|
| 1. 하드웨어 파악 | 내 VRAM/통합 메모리가 몇 GB인가 | VRAM 계산 |
| 2. 양자화 선택 | Q4_K_M인가 Q8_0인가 | 양자화 심화 |
| 3. 도구 선택 | CLI인가 GUI인가 서버인가 | 세 도구 비교 |
| 4. 실행·서빙 | OpenAI 호환 API로 띄우기 | 서버 모드 |
| 5. 튜닝 | 컨텍스트·GPU 오프로드·KV 캐시 | 성능 튜닝 |
아래 모든 명령과 수치는 실제 동작 기준이다. 단, 모델 파일 크기·토큰 속도는 하드웨어와 양자화에 따라 달라지므로 "대략의 범위"로 받아들이고, 정확한 값은 본인 환경에서 측정하라.
양자화란 무엇이고 왜 하는가 — bit 단위로 이해하기
LLM 가중치는 학습 시 보통 FP16(16비트 부동소수점) 또는 BF16으로 저장된다. 파라미터 1개당 2바이트다. 따라서 70억(7B) 파라미터 모델은 가중치만 7e9 × 2 = 14GB다. 여기에 추론 중 KV 캐시·활성값 메모리가 더 붙으니 FP16 그대로는 소비자 GPU에 잘 안 들어간다.
양자화는 가중치를 더 적은 비트로 표현해 메모리와 대역폭을 줄이는 것이다. 핵심은 "파라미터당 평균 비트 수(bpw, bits per weight)"다.
| 정밀도 | 파라미터당 바이트 | 7B 모델 대략 크기 | 품질 |
|---|---|---|---|
| FP16/BF16 | 2.0 | ~14 GB | 원본 |
| Q8_0 | ~1.06 | ~7.2 GB | 원본과 사실상 구분 불가 |
| Q6_K | ~0.82 | ~5.5 GB | 매우 우수 |
| Q5_K_M | ~0.69 | ~4.8 GB | 우수 |
| Q4_K_M | ~0.58 | ~4.1 GB | 권장 기본값 |
| Q3_K_M | ~0.46 | ~3.3 GB | 눈에 띄는 열화 시작 |
| Q2_K | ~0.36 | ~2.6 GB | 비상용·품질 붕괴 위험 |
양자화가 LLM에서 잘 작동하는 이유는, 가중치 분포가 대부분 0 근처에 몰려 있어 낮은 비트로도 상대적 크기 관계를 보존할 수 있기 때문이다. 다만 모든 가중치가 동등하게 중요하지는 않다. 어텐션의 일부 채널이나 임베딩처럼 민감한 텐서는 비트를 깎으면 출력이 크게 흔들린다. 그래서 현대 GGUF 양자화는 "모든 텐서를 4비트"가 아니라 텐서별로 비트를 다르게 주는 혼합(mixed) 방식(_K_M 등)을 쓴다.
흔한 함정: "양자화 = 무조건 손해"가 아니다. Q8_0과 Q5_K_M 사이는 대부분의 일반 태스크에서 체감 차이가 거의 없다. 반대로 "양자화 = 공짜"도 아니다. Q4 미만, 특히 Q3/Q2로 내려가면 수치 정확성·코드 생성·긴 추론 체인에서 무음으로 망가진다. 자연스러운 문장은 계속 뽑아내므로 "잘 도는 것처럼 보이지만 답이 틀린" 상태가 되기 쉽다. 정확도가 중요한 워크로드(코드, JSON 추출, 계산)는 검증 셋으로 양자화별 정답률을 직접 비교해야 한다.
베스트 프랙티스: 잘 모르겠으면 Q4_K_M으로 시작하라. 메모리가 남으면 Q5_K_M → Q6_K → Q8_0으로 올리며 품질을 사고, 메모리가 부족하면 Q4_K_M까지만 내리고 그 아래로는 "모델 크기 자체를 줄이는"(예: 7B→3B) 선택을 먼저 고려하라. 같은 메모리 예산이면 보통 더 큰 모델의 Q4가 더 작은 모델의 Q8보다 낫다.
GGUF 양자화 타입 해독 — Q4_K_M의 글자를 읽는 법
GGUF 파일 이름의 양자화 접미사는 규칙이 있다. 무작정 외우지 말고 글자를 파싱하라.
Q4_K_M
│ │ └─ 변형: S(small) / M(medium) / L(large) — 같은 비트군 안에서 품질-크기 트레이드오프
│ └─── K-quant: 블록 단위로 스케일/최소값을 따로 저장하는 현대식 방법
└───── 비트 수: 가중치당 약 4비트
주요 패밀리 정리:
- 레거시 양자(
Q4_0,Q4_1,Q8_0): 블록 단위 단순 스케일링.Q8_0은 8비트라 품질 손실이 거의 없어 여전히 널리 쓰인다.Q4_0/Q4_1은 K-quant 등장 이후 4비트대에서는 잘 안 쓴다(같은 비트면 K-quant가 더 정확). - K-quant(
Q3_K,Q4_K,Q5_K,Q6_K): super-block 안을 여러 sub-block으로 나눠 스케일을 더 촘촘히 잡는다. 같은 평균 비트에서 레거시보다 품질이 좋다._S/_M/_L은 중요 텐서(어텐션 V, FFN down 등)에 비트를 더 줄지 결정한다. 실무에서는_M이 가장 균형이 좋아 기본 선택이다. - IQ 양자(
IQ4_XS,IQ3_M,IQ2_XXS등): importance-matrix 기반의 더 정교한 저비트 양자화. 같은 비트(특히 2~4비트 저구간)에서 K-quant보다 품질이 좋은 경우가 많지만, 연산이 더 복잡해 CPU 추론이 느려질 수 있고 일부 백엔드에서 제약이 있다. VRAM이 빠듯해 저비트가 불가피할 때 후보다.
imatrix(importance matrix): 최신 GGUF에는 imatrix로 보정한 양자화가 많다. 캘리브레이션 텍스트로 "어떤 가중치가 출력에 더 큰 영향을 주는지"를 측정해 양자화 오차를 그쪽에 덜 싣는 기법이다. 같은 Q4_K_M이라도 imatrix로 만든 쪽이 보통 더 낫다. 모델 카드에 imatrix 사용이 명시돼 있으면 우선한다.
고르는 순서(실전 룰):
- VRAM/통합 메모리에서 KV 캐시·OS 여유를 빼고 가중치에 쓸 수 있는 예산을 계산한다(다음 섹션).
- 그 예산에 맞는 가장 큰 모델을 고른다.
- 그 모델에서 들어가는 가장 높은 K-quant를 고른다. 보통
Q4_K_M이 하한,Q6_K/Q8_0이 상한. - 2~4비트로 내려가야만 들어간다면 K-quant 대신 IQ 계열을 비교 후보로 둔다.
함정: 파일명만 보고 "Q5가 Q4보다 항상 낫다"고 단정하지 마라. 서로 다른 업로더가 만든 파일은 imatrix 유무·캘리브레이션 데이터가 달라, 어떤 Q4_K_M(imatrix)이 다른 Q5_K_M(no-imatrix)보다 나을 수도 있다. 평판 있는 배포자의 파일을 쓰고, 가능하면 직접 평가하라.
VRAM·메모리 계산 — 들어갈지 미리 따지기
실행 전에 메모리 예산을 산수로 따져두면 시행착오를 크게 줄인다. 총 사용량은 크게 세 덩어리다.
총 메모리 ≈ 모델 가중치 + KV 캐시 + (활성값·오버헤드 약 0.5~1GB)
1) 가중치: 파라미터 수 × 양자화 bpw / 8. 실무에서는 모델 파일(GGUF) 크기 자체가 가중치 메모리의 좋은 근사다. Q4_K_M 7B면 약 4.1GB.
2) KV 캐시: 컨텍스트 길이에 비례한다. 트랜스포머는 각 토큰의 key/value를 캐시한다. 근사식:
KV 캐시 바이트 ≈ 2 (K와 V) × n_layers × n_kv_heads × head_dim
× seq_len × bytes_per_element
bytes_per_element는 KV 캐시 정밀도다. 기본 FP16이면 2바이트, KV 양자화(예: q8_0)를 켜면 1바이트로 절반.- GQA(Grouped-Query Attention) 모델은
n_kv_heads가 작아 KV 캐시가 훨씬 가볍다. 최근 모델 대부분이 GQA라 긴 컨텍스트가 예전보다 저렴해졌다.
감을 잡는 수준의 예: 7B급 GQA 모델에서 컨텍스트 8K, FP16 KV 캐시면 KV가 대략 1~2GB대다. 컨텍스트를 32K로 키우면 그 4배로 불어난다. 즉 "모델은 들어가는데 긴 프롬프트에서 OOM"이 나는 건 거의 KV 캐시 탓이다.
3) 통합 메모리(Apple Silicon)의 특수성: M1~M4 맥은 CPU/GPU가 통합 메모리를 공유한다(UMA). 그래서 "VRAM" 대신 전체 RAM에서 모델을 올린다. macOS는 GPU에 할당 가능한 메모리에 상한이 있어, 24GB 통합 메모리라고 24GB를 다 모델에 쓸 수는 없다(시스템·다른 앱 몫을 남겨야 한다). 실무 기준은 **"통합 메모리의 약 70%를 안전 상한"**으로 잡는 것이다. 16GB 맥이면 ~11GB, 32GB면 ~22GB를 모델+KV에 배정하는 식으로 계획하라.
빠른 산정 표(여유 1GB 포함, 8K 컨텍스트 가정):
| 가용 메모리 | 권장 조합 |
|---|---|
| 8 GB | 7~8B Q4_K_M, 또는 3~4B Q6_K |
| 12 GB | 7~8B Q5_K_M / Q6_K |
| 16 GB | 7~8B Q8_0, 또는 13~14B Q4_K_M |
| 24 GB | 13~14B Q6_K, 또는 30B급 Q4_K_M(빠듯) |
| 32 GB+ | 30B급 Q4_K_M~Q5, 또는 70B Q4(타이트) |
베스트 프랙티스: 처음에는 컨텍스트를 4K~8K로 작게 잡아 모델이 GPU에 100% 올라가는지부터 확인하고, 그다음 컨텍스트를 키우며 KV 여유를 보라. GPU에 다 못 올라가 CPU로 일부가 새면(부분 오프로드) 속도가 급락하므로, "전부 GPU"를 우선 목표로 양자화/모델 크기를 조정하는 편이 체감 성능에 가장 효과적이다.
Ollama — 모델을 docker처럼 다루기
Ollama는 llama.cpp를 백엔드로 쓰면서, 모델 다운로드·버전·실행을 pull/run 명령으로 단순화한 도구다. 백그라운드 서버(데몬)가 떠 있고 CLI/HTTP로 그 서버에 말을 건다.
설치·기본 실행:
# macOS/Linux 설치 (공식 스크립트)
curl -fsSL https://ollama.com/install.sh | sh
# 모델 받아서 대화 (없으면 자동 pull)
ollama run llama3.1:8b
# 받기만 하고 실행은 나중에
ollama pull qwen2.5-coder:7b
# 설치된 모델·용량 확인
ollama list
# 메모리에 올라가 있는(로딩된) 모델 확인
ollama ps
태그로 양자화 지정: Ollama 모델은 이름:태그 형식이고, 태그에 파라미터 수·양자화가 들어간다. 기본 태그(예: llama3.1:8b)는 보통 Q4_K_M로 매핑된다. 다른 양자화를 원하면 명시하라.
ollama run llama3.1:8b-instruct-q5_K_M
ollama run qwen2.5:14b-instruct-q4_K_M
태그 목록은 각 모델의 라이브러리 페이지에서 확인할 수 있다. 함정: 태그를 생략하면 "적당한 기본값"이 받아지는데, 이게 본인 메모리에 안 맞을 수 있다. 용량이 빠듯하면 항상 양자화 태그를 명시하라.
Modelfile로 커스터마이즈: 시스템 프롬프트·파라미터·임의 GGUF를 묶어 "내 모델"을 만들 수 있다.
# Modelfile
FROM llama3.1:8b
PARAMETER temperature 0.3
PARAMETER num_ctx 8192
SYSTEM """You are a senior backend engineer. Answer in Korean. Be concise."""
ollama create my-backend -f Modelfile
ollama run my-backend
외부에서 받은 GGUF 파일을 직접 등록할 수도 있다. FROM ./mymodel.Q4_K_M.gguf 처럼 로컬 경로를 주면 된다.
중요한 함정 — num_ctx 기본값: Ollama는 기본 컨텍스트가 보수적으로 잡혀 있어, 긴 프롬프트를 넣으면 앞부분이 조용히 잘려나가 모델이 일부만 보고 답하는 일이 생긴다. 빌드 에러도 안 나고 그럴듯한 답이 나오므로 발견이 늦다. 긴 입력을 다루면 반드시 PARAMETER num_ctx(또는 API의 options.num_ctx)를 올려라.
모델 언로드/상주 제어: 기본적으로 일정 시간 미사용이면 모델이 메모리에서 내려간다(keep_alive). 서버에서 latency를 일정하게 유지하려면 keep_alive를 늘리거나 -1로 상주시키고, 메모리를 빨리 비우려면 짧게 잡는다.
# 5분 유휴 후 언로드(기본 류) → 항상 상주
curl http://localhost:11434/api/generate -d '{"model":"llama3.1:8b","keep_alive":-1,"prompt":""}'
베스트 프랙티스: Ollama는 "여러 모델을 빠르게 갈아끼우며 실험"하거나 "로컬 서버로 띄워 앱에 붙이는" 용도에 가장 편하다. 모델 저장소가 한곳(~/.ollama/models)에 모여 관리가 쉽고, 다음 섹션의 OpenAI 호환 엔드포인트도 기본 제공한다.
LM Studio — GUI로 모델 탐색·실행·서빙
LM Studio는 동일한 GGUF/llama.cpp 생태계 위에 데스크톱 GUI를 얹은 앱이다. CLI가 부담스럽거나, 모델을 시각적으로 탐색하고 양자화별로 비교하며 고르고 싶을 때 강점이 크다. (Apple Silicon에서는 MLX 백엔드도 지원해 일부 모델을 더 빠르게 돌릴 수 있다.)
핵심 워크플로:
- Discover(탐색) 탭에서 모델 검색. LM Studio는 각 양자화 파일에 대해 "이 머신에 들어갈 가능성"을 추정해 표시해준다. 본인 RAM/VRAM 기준으로 초록/노랑/빨강 같은 신호를 보고 고르면 메모리 미스를 줄일 수 있다.
- 원하는 양자화 변형을 선택해 다운로드(Q4_K_M, Q6_K 등 파일별로 받기).
- Chat 탭에서 로드. 로드 시 컨텍스트 길이, GPU 오프로드 레이어 수, KV 캐시 옵션을 슬라이더로 조정한다.
- Developer/Server 탭에서 로컬 OpenAI 호환 서버를 켠다(기본 포트
1234).
로컬 서버 사용 예: 서버를 켜면 OpenAI SDK를 그대로 쓸 수 있다.
from openai import OpenAI
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio") # 키는 아무 값
resp = client.chat.completions.create(
model="local-model", # 로드된 모델이면 식별자 무관하게 동작하는 경우가 많음
messages=[{"role": "user", "content": "파이썬으로 이진 탐색 짜줘"}],
temperature=0.2,
)
print(resp.choices[0].message.content)
CLI(lms)도 있다: LM Studio는 GUI 전용이 아니다. lms 커맨드로 모델 로드/언로드, 서버 제어를 스크립트화할 수 있어, GUI로 고르고 운영은 CLI로 자동화하는 조합이 가능하다.
lms ls # 설치된 모델
lms load <model> # 메모리에 로드
lms server start # 로컬 API 서버 시작
lms ps # 로드된 모델 확인
함정·주의:
- GPU 오프로드 슬라이더의 의미: "GPU에 올릴 레이어 수"다. 전부 못 올리면 나머지는 CPU로 가서 토큰 속도가 급락한다. VRAM이 충분하면 최대(또는 "max/all")로 두는 게 거의 항상 옳다. 메모리 초과로 로드 실패하면 레이어 수나 컨텍스트를 줄여라.
- 라이선스: LM Studio 앱 자체의 사용 약관(특히 상업적 사용 범위)은 모델 라이선스와 별개다. 사내·상업 환경에서 쓸 거면 앱 약관과 각 모델 라이선스를 모두 확인하라.
- 모델 디렉터리 중복: LM Studio와 Ollama는 모델 저장 경로가 다르다. 같은 GGUF를 양쪽에서 받으면 디스크를 두 배로 먹는다. 용량을 아끼려면 한 도구로 통일하거나, 가능한 경우 공유 경로를 설정하라.
언제 LM Studio인가: 비개발자·프로토타이핑·"여러 양자화를 눈으로 비교"가 잦은 경우. 운영(프로덕션 서빙)에는 보통 Ollama나 raw llama.cpp 서버가 더 가볍고 스크립트화하기 좋다.
llama.cpp — 엔진을 직접 쓰는 길
Ollama·LM Studio 모두 내부적으로 llama.cpp를 쓴다. 엔진을 직접 빌드해 쓰면 최신 기능·세밀한 플래그·최소 오버헤드를 얻는다. 대신 손이 더 간다.
빌드(현재 표준은 CMake):
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
# macOS(Metal은 기본 활성)
cmake -B build
cmake --build build --config Release -j
# NVIDIA CUDA
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j
빌드 산출물은 build/bin/에 들어간다. 핵심 바이너리는 llama-cli(대화/단발 실행), llama-server(OpenAI 호환 서버), llama-bench(벤치), llama-quantize(양자화)다. (예전 이름인 main, server는 llama-cli, llama-server로 바뀌었다.)
모델 받기: GGUF를 직접 받거나, Hugging Face 레포를 바로 가리킬 수도 있다.
# 단발 추론
./build/bin/llama-cli -m ./models/qwen2.5-7b-instruct-q4_k_m.gguf \
-p "파이썬으로 LRU 캐시 구현해줘" -n 512
# Hugging Face에서 바로 받아 실행(-hf)
./build/bin/llama-cli -hf bartowski/Qwen2.5-7B-Instruct-GGUF:Q4_K_M
자주 쓰는 핵심 플래그:
| 플래그 | 의미 | 메모 |
|---|---|---|
-m | 모델 GGUF 경로 | |
-c / --ctx-size | 컨텍스트 길이 | KV 캐시 메모리 직결 |
-ngl / --n-gpu-layers | GPU로 올릴 레이어 수 | 전부 올리려면 큰 값/999 |
-n / --predict | 생성 토큰 수 | |
-t | CPU 스레드 수 | CPU 추론·하이브리드 시 |
--temp, --top-p, --top-k | 샘플링 | |
-fa / --flash-attn | FlashAttention | 지원 빌드에서 KV·속도 이득 |
-ctk / -ctv | KV 캐시 양자화(예: q8_0) | 긴 컨텍스트 메모리 절감 |
직접 양자화하기: FP16 GGUF에서 원하는 양자화를 직접 만들 수 있다. imatrix를 만들어 적용하면 같은 비트에서 품질이 올라간다.
# 1) FP16 GGUF가 있다고 가정 (HF 변환 스크립트로 만들거나 받음)
# 2) (선택) importance matrix 생성
./build/bin/llama-imatrix -m model-f16.gguf -f calibration.txt -o model.imatrix
# 3) 양자화
./build/bin/llama-quantize --imatrix model.imatrix \
model-f16.gguf model-q4_k_m.gguf Q4_K_M
함정:
- 빌드 백엔드 불일치: CUDA 없이 빌드해놓고
-ngl을 줘봐야 GPU로 안 간다. 빌드 시 백엔드 플래그(-DGGML_CUDA=ON등)를 켰는지부터 확인하라. - 잘못된/오래된 GGUF: GGUF는 버전이 있고, 너무 오래된 파일은 최신 llama.cpp에서 안 열린다. 반대로 매우 새 모델은 엔진을 최신으로 올려야 토크나이저/아키텍처가 지원된다.
- 채팅 템플릿: 모델마다 프롬프트 포맷이 다르다.
llama-cli에서-cnv(대화 모드)를 쓰면 GGUF에 내장된 채팅 템플릿을 적용해주지만, 임의 raw 프롬프트로 호출하면 포맷이 틀어져 품질이 떨어진다. 가능하면 채팅 모드/서버의 메시지 API를 쓰라.
언제 raw llama.cpp인가: 최신 모델·실험 기능을 바로 써야 하거나, 컨테이너에 최소 의존성으로 서버만 박고 싶을 때. 직접 양자화를 만들거나 벤치를 정밀하게 돌릴 때.
로컬을 OpenAI 호환 API로 서빙하기 — 앱에 바로 붙이기
세 도구 모두 OpenAI Chat Completions 호환 엔드포인트를 제공한다. 덕분에 기존 코드를 거의 안 바꾸고 base_url만 로컬로 돌리면 된다. 포트 기본값만 알면 된다.
| 도구 | 기본 베이스 URL | 비고 |
|---|---|---|
| Ollama | http://localhost:11434/v1 | 네이티브 API는 /api/*, OpenAI 호환은 /v1/* |
| LM Studio | http://localhost:1234/v1 | 서버 탭에서 켬 |
| llama.cpp | http://localhost:8080/v1 | llama-server로 띄움 |
llama.cpp 서버 띄우기:
./build/bin/llama-server \
-m ./models/qwen2.5-7b-instruct-q4_k_m.gguf \
-c 8192 -ngl 999 --host 0.0.0.0 --port 8080
Ollama 서버는 이미 떠 있다(ollama serve 데몬). 받은 모델을 그대로 /v1로 부르면 된다.
공통 클라이언트 코드(어느 도구든 동일):
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1", # Ollama
api_key="ollama", # 로컬은 보통 아무 값
)
stream = client.chat.completions.create(
model="llama3.1:8b",
messages=[
{"role": "system", "content": "You are a coding assistant."},
{"role": "user", "content": "FastAPI로 헬스체크 엔드포인트 만들어줘"},
],
stream=True,
temperature=0.2,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
curl로 빠르게 확인:
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.1:8b",
"messages": [{"role":"user","content":"한 문장으로 자기소개"}]
}'
JSON 강제 출력(구조화 출력): 로컬 모델로 추출·분류를 할 때 자유 텍스트가 섞이면 파싱이 깨진다. 호환 엔드포인트들은 대체로 response_format이나 GBNF/JSON 스키마 기반 제약을 지원한다(도구별 지원 범위는 다름). llama.cpp 서버는 GBNF 문법으로 출력 형태를 강제할 수 있어, "반드시 유효한 JSON"을 토큰 수준에서 보장할 수 있다. 이게 프롬프트로 "JSON만 내줘"라고 비는 것보다 훨씬 안정적이다.
함정·주의:
- 함수 호출(tool calling) 지원 차이: 모델·도구마다 tool calling 품질이 천차만별이다. 클라우드 모델 수준의 안정적 함수 호출을 기대하면 실망할 수 있다. tool 호출이 핵심이면 해당 기능을 잘 지원하는 모델(예: 그 용도로 파인튜닝된 instruct 모델)을 골라 실측하라.
- 외부 노출 보안:
--host 0.0.0.0으로 열면 같은 네트워크의 다른 기기에서 접근 가능하다. 로컬 LLM 서버는 보통 인증이 약하므로, 외부망에 그대로 노출하지 말고 리버스 프록시·방화벽·터널로 감싸라. - 동시 요청·큐: 로컬 서버는 기본적으로 동시성이 제한적이다. 배치/병렬로 때리면 큐잉돼 느려지거나 OOM이 날 수 있다. 서버별 병렬 슬롯 옵션(예: llama.cpp
-np)을 이해하고 KV 캐시 메모리를 그만큼 더 잡아야 한다.
성능 튜닝 — GPU 오프로드·KV 캐시·FlashAttention
로컬 추론 속도를 좌우하는 건 결국 **"모델이 GPU에 다 올라갔는가"**와 **"메모리 대역폭"**이다. 토큰 생성은 메모리 바운드라, 양자화로 가중치를 줄이는 것 자체가 속도에도 도움이 된다.
1) GPU 레이어 전량 오프로드를 최우선으로. 모델 일부가 CPU에 남으면(부분 오프로드) 매 토큰마다 CPU↔GPU를 오가며 속도가 몇 배로 느려진다. -ngl(llama.cpp)·GPU offload 슬라이더(LM Studio)·Ollama의 자동 배치를 통해 전부 GPU를 목표로 하고, 안 들어가면 컨텍스트를 줄이거나 한 단계 낮은 양자화/작은 모델로 내려라.
2) KV 캐시 양자화로 긴 컨텍스트 비용을 절감. 긴 컨텍스트에서 KV 캐시가 VRAM을 잡아먹어 모델 레이어를 GPU에서 밀어낼 때, KV를 FP16 대신 8비트로 양자화하면 KV 메모리가 절반이 된다.
# llama.cpp: KV 캐시 K/V를 q8_0로
./build/bin/llama-server -m model.gguf -c 32768 -ngl 999 \
-fa -ctk q8_0 -ctv q8_0
KV 양자화는 품질에 거의 영향이 없는 편이지만(특히 q8_0), 극단적으로 낮추면(q4) 긴 문맥 정확도가 흔들릴 수 있으니 q8_0을 기본으로 하라. FlashAttention(-fa)을 켜야 KV 양자화 효과·속도 이득을 제대로 본다.
3) 컨텍스트는 "필요한 만큼만". "혹시 몰라서" 컨텍스트를 32K로 잡아두면 실제로 짧은 프롬프트만 써도 KV 캐시용 메모리를 미리 잡아 모델이 GPU에 덜 올라갈 수 있다. 워크로드의 실제 최대 입력에 맞춰 잡아라.
4) 배치/병렬 슬롯. 동시 요청을 받는 서버라면 llama.cpp의 -np(병렬 시퀀스 수)로 슬롯을 늘리되, KV 캐시 메모리가 슬롯 수에 비례해 늘어난다는 점을 계산에 넣어라. 8K 컨텍스트 × 4슬롯이면 사실상 32K치 KV가 필요하다.
5) 벤치로 측정하라. 추측 대신 llama-bench로 prompt(프리필) 속도와 generation(디코드) 속도를 따로 측정하라.
./build/bin/llama-bench -m model-q4_k_m.gguf -ngl 999
결과의 pp(prompt처리)와 tg(토큰 생성)를 보면, 양자화/오프로드 설정을 바꿀 때 무엇이 좋아지는지 정량 비교할 수 있다.
Apple Silicon 팁: Metal 백엔드는 -ngl을 충분히 크게 주면 전 레이어가 GPU로 간다. 통합 메모리라 OOM이 나면 시스템 전체가 느려지니, 다른 무거운 앱(브라우저 탭 다수 등)을 닫고 안전 상한(약 70%)을 지켜라. MLX 기반(LM Studio의 MLX 모델)은 일부 모델에서 GGUF보다 빠를 수 있어 비교 가치가 있다.
함정: "스레드 수(-t)를 코어 수만큼 올리면 빨라진다"는 GPU 추론에서는 대체로 무의미하다. -t는 CPU 추론·하이브리드 구간에만 의미가 크고, 물리 코어 수를 넘기면 오히려 느려질 수 있다.
양자화별 품질 검증 — "그럴듯하지만 틀린" 출력 잡아내기
로컬 LLM의 가장 위험한 실패 모드는 크래시가 아니라 무음의 품질 저하다. Q3로 내린 모델도 문장은 매끄럽게 뽑는다. 그래서 "잘 돈다"는 인상과 "실제로 맞다"는 사실을 분리해서 검증해야 한다.
1) perplexity는 참고지표일 뿐. llama.cpp의 llama-perplexity로 양자화별 perplexity를 잴 수 있다(낮을수록 좋음). 양자화 손상의 거친 신호로는 유용하지만, perplexity가 비슷해도 코드·수치 정확도는 차이날 수 있다. 최종 판단은 실제 태스크 기준으로 하라.
2) 본인 워크로드의 골든 셋을 만들어라. 가장 신뢰할 수 있는 검증은 "내가 실제로 시킬 일"의 정답 셋이다. 예를 들어 코드 어시스턴트라면:
import json, requests
cases = [
{"prompt": "파이썬으로 두 정렬 리스트 병합", "check": lambda o: "def " in o},
{"prompt": "이 JSON에서 email만 뽑아 배열로: {...}", "check": lambda o: valid_json(o)},
# ... 도메인 케이스 20~50개
]
def run(model, prompt):
r = requests.post("http://localhost:11434/v1/chat/completions", json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0, # 결정적으로
})
return r.json()["choices"][0]["message"]["content"]
for model in ["qwen2.5:7b-instruct-q4_K_M", "qwen2.5:7b-instruct-q8_0"]:
passed = sum(c["check"](run(model, c["prompt"])) for c in cases)
print(f"{model}: {passed}/{len(cases)}")
**핵심은 temperature=0**으로 두고 비교하는 것이다. 온도가 높으면 양자화 차이인지 샘플링 운인지 구분이 안 된다.
3) 두 양자화를 같은 입력으로 나란히 비교(diff). 같은 프롬프트를 Q4와 Q8에 넣고 출력 차이를 직접 보면, 어디서 무너지는지 감이 온다. 특히 **긴 추론 체인·다단계 계산·정밀 포맷(JSON/표)**에서 저비트가 먼저 깨진다.
4) 결정해야 할 트레이드오프 정리:
| 워크로드 | 권장 하한 | 이유 |
|---|---|---|
| 일반 대화·요약·브레인스토밍 | Q4_K_M | 체감 차이 작음, 메모리 효율 |
| 코드 생성·리팩터링 | Q5_K_M~Q6_K | 구문/논리 민감, 저비트서 미묘한 버그 |
| 구조화 추출(JSON 스키마) | Q5_K_M + 문법 제약 | 포맷 일탈이 파이프라인을 깸 |
| 수치 계산·정밀 추론 | Q6_K 이상 | 저비트서 산술 오류 급증 |
| 메모리 극빈 + 비핵심 | IQ4_XS / Q4_K_M | 마지노선, 반드시 정답률 측정 |
베스트 프랙티스: 양자화를 "한 번 고르고 끝"이 아니라, 모델 교체·태스크 변경 때마다 골든 셋으로 재검증하라. 특히 정확도가 비즈니스에 직결되는 파이프라인(데이터 추출·코드 자동수정)에서는, 저비트로 아낀 메모리보다 틀린 출력 한 건의 비용이 더 크다.
모델·도구 선택 의사결정 — 무엇을 언제 쓰나
세 도구는 경쟁이라기보다 같은 엔진의 다른 포장이다. 상황별로 정리한다.
도구 선택:
| 상황 | 추천 | 이유 |
|---|---|---|
| 빠르게 여러 모델 실험·로컬 서버로 앱에 붙이기 | Ollama | pull/run·OpenAI 호환·스크립트화 쉬움, 모델 한곳 관리 |
| GUI로 탐색·양자화 비교·비개발자 사용 | LM Studio | 메모리 적합도 표시·시각적 로딩 옵션, MLX 지원 |
| 최신 기능·세밀 제어·직접 양자화·최소 의존 컨테이너 | llama.cpp | 엔진 직접, 모든 플래그·GBNF·imatrix |
실무에서는 "LM Studio로 고르고, Ollama/llama.cpp로 운영" 또는 "개인 PC는 Ollama, 서버 컨테이너는 llama.cpp" 식 조합이 흔하다.
모델 패밀리 고르기(용도 우선): "좋은 모델"은 절대값이 아니라 용도 함수다.
- 코드: 코드 특화 instruct 모델(예: Qwen 코더 계열, 코드 특화 라인업)이 범용보다 코드 태스크에서 낫다.
- 한국어: 다국어 성능이 좋은 최신 모델을 우선하고, 반드시 한국어 골든 셋으로 직접 검증하라. 영어 벤치 점수가 한국어 품질을 보장하지 않는다.
- 긴 문맥: 공시 컨텍스트 길이와 "실제로 그 길이에서 성능이 유지되는지"는 다르다. 본인 긴 입력으로 확인하라.
- 작은 메모리: 3~4B급 instruct 모델이 의외로 쓸 만하다. 7B Q8보다 "더 큰 모델 Q4"가 보통 낫지만, 메모리가 정말 빠듯하면 작은 모델 + 높은 양자화가 안정적이다.
라이선스를 반드시 확인하라. 로컬에서 돈다고 다 자유 사용은 아니다. 모델마다 라이선스가 다르고(완전 오픈, 상업 사용 제한, 사용량/매출 기준 조건 등), 상업 서비스에 넣을 거면 각 모델 라이선스 원문을 확인해야 한다. "오픈 웨이트"가 "오픈 소스 라이선스"를 의미하지 않는 경우가 많다.
클라우드 vs 로컬 결정선:
- 로컬이 유리: 데이터 반출 금지·대량 반복 배치·오프라인·고정 비용 선호·소규모 모델로 충분한 태스크.
- 클라우드가 유리: 최고 성능 프런티어 모델이 필요·간헐적 사용·운영 부담 회피·안정적 tool calling/긴 추론.
- 현실적 절충: 민감/대량은 로컬, 어려운 소수 케이스는 클라우드로 라우팅하는 하이브리드. 같은 OpenAI 호환 인터페이스라
base_url만 바꿔 라우팅하기 쉽다.
흔한 함정 총정리 — 디버깅 체크리스트
로컬 LLM에서 반복적으로 사람들을 잡는 함정을 한곳에 모았다. 문제가 생기면 위에서부터 점검하라.
메모리·로딩
- OOM/로드 실패: 양자화를 한 단계 낮추거나 컨텍스트(
-c/num_ctx)를 줄여라. 통합 메모리(Mac)는 70% 상한을 넘기지 마라. - "분명 들어가는데 긴 프롬프트서 OOM": 거의 KV 캐시 탓. 컨텍스트를 줄이거나 KV 양자화(
-ctk/-ctv q8_0+-fa)를 켜라. - 느림(토큰/초가 급락): 부분 오프로드 의심.
-ngl을 올려 전 레이어 GPU 적재를 확인하라. CUDA/Metal 백엔드로 빌드됐는지도 확인.
출력 품질
- 그럴듯한데 답이 틀림: 양자화가 너무 낮을 수 있다(Q3/Q2). 한 단계 올려 재검증. 또는 채팅 템플릿이 안 맞아 모델이 포맷을 오인하는 경우.
- 앞부분을 무시한 답: 컨텍스트 초과로 입력이 잘렸다(특히 Ollama 기본 num_ctx). 컨텍스트를 키워라.
- JSON/포맷이 깨짐: 프롬프트로 비는 대신 문법 제약(GBNF)·
response_format을 써라. - 이상한 토큰·반복: 잘못된/손상 GGUF, 또는 엔진-모델 버전 불일치. 엔진을 최신으로, 평판 있는 배포자의 파일로.
도구·환경
-
-ngl줘도 GPU 안 씀: GPU 백엔드 없이 빌드됨.-DGGML_CUDA=ON등으로 재빌드. - 새 모델이 안 열림: 엔진(llama.cpp/Ollama/LM Studio)을 최신으로 업데이트. 새 아키텍처/토크나이저는 엔진 지원이 선행돼야 한다.
- 디스크 폭발: 같은 모델을 여러 도구·여러 양자화로 받아 중복. 한 도구로 통일하거나 안 쓰는 양자화 정리.
- tool calling 불안정: 모델·도구 지원 한계. 함수 호출이 핵심이면 그 용도로 검증된 모델을 골라 실측.
운영·보안
- 외부 노출:
--host 0.0.0.0은 같은 망에 공개. 인증 약한 로컬 서버를 인터넷에 직접 열지 마라. - 동시성 OOM: 병렬 슬롯(
-np)만큼 KV 캐시가 곱으로 늘어난다. 슬롯 수 × 컨텍스트로 메모리를 재계산. - 라이선스 위반: 로컬 실행 ≠ 상업 사용 허가. 모델·앱 약관을 따로 확인.
황금률 3가지로 압축하면: (1) 전부 GPU에 올려라(부분 오프로드를 피하라), (2) 컨텍스트는 필요한 만큼만, 길면 KV 양자화, (3) 양자화는 골든 셋으로 검증하고 Q4_K_M을 기본선으로 삼아라. 이 셋만 지켜도 로컬 LLM 운영 사고의 대부분을 피한다.