본문 바로가기
AI2026년 9월 3일16분 읽기

로컬 LLM 서빙 — Ollama·vLLM·llama.cpp 비교와 운영 기준

YS
김영삼
조회 8
로컬 LLM 서빙 — Ollama·vLLM·llama.cpp 비교와 운영 기준

혼자 노트북에서 굴릴 거면 Ollama, 서버 한 대에 여러 사용자가 붙어 동시 요청을 쳐야 하면 vLLM, 그 중간의 유연함·CPU 혼합·임베디드가 필요하면 llama.cpp다. 셋은 경쟁 관계라기보다 서로 다른 부하 지점을 위한 도구다.

Ollama는 사실 llama.cpp를 감싼 편의 래퍼고, vLLM은 PagedAttention과 연속 배칭으로 동시 처리량을 극단까지 끌어올린 서빙 엔진이다. 나는 셋 다 프로덕션과 개인 장비에서 굴려봤는데, 고르는 기준은 "모델 성능"이 아니라 "동시 요청 수와 GPU 메모리"였다. 이 글은 그 판단 근거를 수치로 정리한다.

먼저 한 가지 오해부터 풀자. "vLLM이 제일 빠르다"는 말은 반은 맞고 반은 틀리다. 요청이 한 번에 하나씩 들어오는 상황에서는 세 도구의 단일 요청 지연(latency)이 크게 다르지 않다. 차이는 동시 요청이 쌓일 때 폭발한다. 그래서 도구 선택의 첫 질문은 언제나 "동시에 몇 개나?"여야 한다.

항목대략적 수치 / 특성
7B 모델 FP16 가중치약 14GB (params 70억 x 2바이트)
7B 모델 4비트 양자화약 4~4.5GB (Q4_K_M / AWQ 기준)
단일 vs 동시 처리량 차이동시 요청 다수 시 vLLM이 수 배~10배대 총처리량
Ollama 기본 컨텍스트num_ctx 기본 2048 (자주 틀리는 함정)
vLLM 기본 GPU 점유율gpu_memory_utilization 0.9 (VRAM의 90%)
OpenAI 호환 엔드포인트세 도구 모두 /v1/chat/completions 제공

Ollama, vLLM, llama.cpp는 각각 무엇인가

세 도구는 계층이 다르다. llama.cpp는 C/C++로 짠 추론 엔진이고, Ollama는 그 엔진을 감싸 모델 다운로드·태그·API를 자동화한 사용자 편의 레이어이며, vLLM은 파이썬 기반의 고처리량 서빙 엔진으로 출발점 자체가 다르다.

  • llama.cpp — GGUF 포맷, CPU만으로도 돌아가고 CUDA·Metal·ROCm·Vulkan 백엔드를 두루 지원한다. 맥북에서 Metal로 굴리는 실사용의 대부분이 사실 이 위에서 돈다. llama-server가 OpenAI 호환 API를 제공하고 연속 배칭(--parallel)도 있다.
  • Ollama — 내부적으로 llama.cpp를 쓴다(현재는 자체 엔진 병행). ollama run llama3 한 줄이면 끝나는 설치·모델 관리 경험이 핵심 가치다. 개인 개발·프로토타입·데스크톱 앱 연동에 최적.
  • vLLM — PagedAttention으로 KV 캐시를 페이지 단위로 관리해 메모리 낭비를 줄이고, 연속 배칭으로 GPU를 쉬지 않게 채운다. GPU(주로 NVIDIA) 전제. 동시 사용자가 많은 프로덕션 API 서버의 기본값에 가깝다.
참고 Ollama와 llama.cpp를 "둘 중 뭐가 빠르냐"로 비교하는 글이 많은데, Ollama가 llama.cpp를 쓰는 이상 순수 추론 속도는 본질적으로 비슷하다. Ollama의 오버헤드는 속도가 아니라 기본 설정(짧은 컨텍스트, 보수적 동시성)에서 온다. 그걸 튜닝하면 거의 같아진다.

처리량은 왜 배칭에서 갈리는가

LLM 추론은 메모리 대역폭에 묶인(memory-bound) 작업이라, GPU가 가중치를 한 번 읽어올 때 여러 요청을 같이 처리하면 그 읽기 비용을 나눠 쓸 수 있다. 이게 배칭이 총처리량을 끌어올리는 이유다. 그런데 실제 트래픽은 요청 길이도 제각각이고 도착 시점도 흩어져 있어서, 단순 정적 배칭은 짧은 요청이 긴 요청을 기다리느라 GPU가 논다.

vLLM이 유명해진 건 연속 배칭(continuous batching) 때문이다. 토큰 단위로 배치를 재구성해서, 한 요청이 끝나면 즉시 대기 중인 다른 요청을 그 자리에 밀어 넣는다. GPU가 거의 쉬지 않는다. 여기에 PagedAttention이 KV 캐시를 4KB짜리 블록으로 쪼개 관리해 단편화를 없애니, 같은 VRAM으로 더 많은 동시 시퀀스를 담는다. llama.cpp도 연속 배칭을 지원하지만(--parallel, --cont-batching), 메모리 관리의 정교함과 커널 최적화에서 대규모 동시성으로 가면 vLLM이 앞선다.

현장 팁 동시성이 1~2에 불과한데 vLLM을 쓰면 오히려 손해다. vLLM은 배치를 채워야 진가가 나오는데, 요청이 하나뿐이면 그냥 무거운 파이썬 스택만 짊어지는 셈이다. 나는 사내 데모 하나를 vLLM으로 띄웠다가, 동시 사용자가 사실상 1명이라 llama.cpp보다 첫 토큰이 더 느려서 갈아탄 적이 있다.

양자화: GGUF와 AWQ/GPTQ는 어디가 다른가

양자화는 가중치를 FP16 대신 4·8비트 같은 저정밀로 저장해 VRAM과 대역폭을 줄이는 기법이고, 도구마다 선호하는 포맷이 다르다. llama.cpp·Ollama는 GGUF(내부에 Q4_K_M, Q5_K_M, Q8_0 등), vLLM은 AWQ·GPTQ·FP8·bitsandbytes 계열을 쓴다.

  • Q4_K_M — GGUF에서 가장 무난한 4비트 변형. 품질 손실이 체감상 작고 크기는 FP16의 약 1/3.5. 뭘 골라야 할지 모르겠으면 이거다.
  • Q8_0 — 거의 무손실에 가깝지만 크기는 8비트라 절감폭이 작다. 품질 민감한 작업에.
  • AWQ / GPTQ — GPU 추론용 4비트. vLLM에서 커널 최적화가 잘 되어 있어 동시 서빙 시 속도·메모리 균형이 좋다.
  • FP8 — Ada/Hopper급 최신 GPU(H100 등)에서 하드웨어 가속되는 8비트. 품질 손실이 매우 작으면서 속도 이득이 크다.

개인적으로는 로컬 데스크톱이면 GGUF Q4_K_M을 거의 기본으로 쓰고, GPU 서버에서 동시 서빙할 땐 AWQ나 FP8을 쓴다. 한 가지 문서에 잘 안 나오는 함정: 양자화는 가중치만 줄인다. 뒤에 나올 KV 캐시는 별개라, 컨텍스트를 길게 쓰면 양자화해도 VRAM이 터질 수 있다.

GPU 메모리는 어떻게 계산하나

필요 VRAM은 크게 모델 가중치 + KV 캐시 + 오버헤드(활성값·프레임워크) 셋의 합이다. 가중치는 파라미터 수 x 바이트로 바로 나온다. 7B를 FP16으로 올리면 7e9 x 2 = 14GB, 4비트면 약 4~4.5GB.

놓치기 쉬운 게 KV 캐시다. 대략적인 공식은 이렇다:

# KV 캐시 크기(바이트) 근사식
# 2(K와 V) x 레이어수 x KV헤드수 x head_dim x 시퀀스길이 x 배치 x dtype바이트

# 예: 7B급(레이어 32, KV헤드 32, head_dim 128), FP16(2바이트),
#     컨텍스트 4096, 동시 시퀀스 8개일 때
2 * 32 * 32 * 128 * 4096 * 8 * 2
#  = 약 17.2 GB  -> 가중치 14GB 위에 이만큼이 더 필요

즉 24GB짜리 카드(RTX 4090)에 7B FP16을 올리면 가중치 14GB를 빼고 남는 10GB로 KV 캐시를 감당해야 하는데, 컨텍스트 4K에 동시 8개를 받으면 이미 초과다. 그래서 실서비스에선 양자화로 가중치를 4.5GB로 줄여 KV 캐시 여유를 확보하거나, GQA(Grouped-Query Attention)로 KV헤드가 적은 모델을 고른다. 최신 모델들이 KV헤드를 8개 안팎으로 줄인 건 이 캐시 비용 때문이다.

주의 vLLM의 gpu_memory_utilization 기본값 0.9는 "VRAM의 90%를 통째로 잡아 KV 캐시 풀로 예약"한다는 뜻이다. 같은 GPU에 다른 프로세스(예: 임베딩 모델)를 함께 띄우면 OOM으로 죽는다. 공유 GPU면 이 값을 0.5~0.7로 낮춰라. 이거 몰라서 야밤에 재기동한 적 있다.

세 도구 정면 비교

한눈에 보라고 표로 정리했다. "최고"는 없고, 부하 특성에 맞는 선택만 있다.

기준Ollamallama.cppvLLM
주 용도개인·프로토타입·데스크톱임베디드·엣지·유연한 자체 서빙다중 사용자 프로덕션 API
동시 처리량중(설정 필요)중~상상 (연속 배칭+PagedAttention)
양자화 포맷GGUFGGUFAWQ/GPTQ/FP8/bnb, GGUF(실험적)
CPU 추론가능가능(장기)사실상 GPU 전제
하드웨어CUDA/Metal/ROCm/CPUCUDA/Metal/ROCm/Vulkan/CPU주로 NVIDIA(+AMD/일부 가속기)
멀티 GPU레이어 분할 수준레이어 분할텐서/파이프라인 병렬 본격 지원
설치 난이도매우 쉬움중(빌드/플래그)중(CUDA 의존성)
OpenAI 호환 APIO (/v1)O (llama-server /v1)O (/v1, 기능 가장 풍부)

OpenAI 호환 API로 그대로 붙이기

세 도구 모두 /v1/chat/completions를 제공해서, 기존 OpenAI SDK 코드에서 base_url만 바꾸면 그대로 돌아간다. 이게 로컬 LLM 전환의 실질적 진입장벽을 없앤 핵심이다. 각 서버를 띄우는 명령부터.

# vLLM: OpenAI 호환 서버 (기본 포트 8000)
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --gpu-memory-utilization 0.85 \
  --max-model-len 8192 \
  --quantization awq   # AWQ 가중치일 때

# llama.cpp: llama-server (기본 포트 8080)
llama-server -m ./qwen2.5-7b-instruct-q4_k_m.gguf \
  -c 8192 --parallel 4 --cont-batching -ngl 999   # -ngl: GPU에 올릴 레이어 수

# Ollama: 데몬이 11434에서 /v1 제공. 동시성·컨텍스트는 환경변수로
OLLAMA_NUM_PARALLEL=4 ollama serve

클라이언트는 셋 다 동일하다. 파이썬 openai 라이브러리 기준:

from openai import OpenAI

# vLLM이면 :8000/v1, llama-server면 :8080/v1, Ollama면 :11434/v1
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

resp = client.chat.completions.create(
    model="Qwen/Qwen2.5-7B-Instruct",   # Ollama면 "llama3" 처럼 태그명
    messages=[{"role": "user", "content": "KV 캐시가 뭔지 한 문장으로."}],
    temperature=0.2,
    stream=True,
)
for chunk in resp:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
참고 Ollama의 진짜 함정은 여기다. /v1로 호출하면 컨텍스트가 기본 2048 토큰으로 잘린다. 긴 문서를 넣었는데 답이 이상하면 십중팔구 이거다. Ollama에선 Modelfile의 PARAMETER num_ctx 8192로 늘리거나, 네이티브 /api/chat에서 options.num_ctx를 넘겨야 한다. OpenAI 호환 경로에선 이 조정이 안 먹는 버전이 있으니 로그를 꼭 확인하라.

그래서 언제 무엇을 쓸까

동시 요청 수와 배포 대상, 이 두 축이면 대부분 결정된다. 내 실무 기준을 정리하면 이렇다.

  • Ollama를 써라 — 내 노트북/워크스테이션에서 개발할 때, 데스크톱 앱에 로컬 LLM을 끼워 팔 때, 팀원이 각자 알아서 모델 받아 쓰게 할 때. 요청은 대체로 1~2개. "그냥 돌아가는 것"이 최우선일 때.
  • llama.cpp를 써라 — 맥에서 Metal로 최대 성능을 짜낼 때, 라즈베리파이·엣지 기기, CPU/GPU 혼합 오프로딩이 필요할 때, 바이너리 하나로 의존성 없이 배포하고 싶을 때. 세밀한 플래그 제어를 원하는 사람에게.
  • vLLM을 써라 — 여러 사용자가 동시에 치는 사내/외부 API, 초당 요청이 쌓이는 배치 처리, A100·H100 같은 데이터센터 GPU를 최대한 뽑아 써야 할 때, 텐서 병렬로 70B급을 올릴 때.

헷갈리면 이 한 줄만 기억하자. 사람 한 명이 쓰면 Ollama/llama.cpp, 사람 여럿이 쓰면 vLLM. 나머지는 디테일이다.

운영에서 데인 것들

문서엔 잘 안 나오지만 실제로 발목 잡는 것들을 모아둔다. 대부분 성능이 아니라 설정과 메모리 문제다.

  • 첫 요청이 느린 건 로딩이다 — 콜드 스타트에 모델을 VRAM에 올리는 시간이 수 초~수십 초 걸린다. 벤치마크할 때 워밍업 요청 한 번 날리고 재라. 안 그러면 숫자가 거짓말한다.
  • max-model-len을 무작정 키우지 마라 — vLLM에서 컨텍스트 길이를 늘리면 KV 캐시 풀이 커져 동시 처리 가능 시퀀스 수가 줄어든다. 실제 필요한 만큼만.
  • Ollama는 유휴 시 모델을 언로드한다 — 기본 5분 후 메모리에서 내린다. 다음 요청이 다시 느려진다. OLLAMA_KEEP_ALIVE=-1로 상주시키거나 헬스체크로 데워두라.
  • 양자화 품질은 작은 모델에서 더 아프다 — 3B 이하를 4비트로 조이면 체감 품질 저하가 크다. 작은 모델은 Q5/Q8을 쓰거나 애초에 양자화 폭을 줄여라.
  • 처리량 측정은 token/s가 아니라 전체 처리량으로 — 단일 요청 token/s만 보면 vLLM이 손해로 보인다. 동시 32요청을 걸고 총 output token/s를 재야 실제 서빙 비용이 보인다.

자주 묻는 질문

Ollama와 llama.cpp 중 뭐가 더 빠른가요?

순수 추론 속도는 거의 같습니다. Ollama가 내부적으로 llama.cpp를 쓰기 때문입니다. 차이는 Ollama의 보수적 기본 설정(짧은 컨텍스트, 낮은 동시성)에서 오며, 이를 튜닝하면 차이가 사라집니다. 대신 llama.cpp는 플래그로 더 세밀하게 제어할 수 있습니다.

GPU 없이 CPU만으로 로컬 LLM을 돌릴 수 있나요?

llama.cpp와 Ollama는 CPU만으로 돌아갑니다. 다만 토큰 생성 속도가 크게 떨어져 7B 4비트라도 초당 몇 토큰 수준일 수 있습니다. vLLM은 사실상 GPU 전제라 CPU 단독 운영에는 적합하지 않습니다.

7B 모델을 돌리려면 VRAM이 얼마나 필요한가요?

가중치만 보면 FP16은 약 14GB, 4비트 양자화는 약 4~4.5GB입니다. 여기에 컨텍스트 길이와 동시 요청 수에 비례하는 KV 캐시가 더해집니다. 4비트에 짧은 컨텍스트면 8GB 카드에서도 가능하고, 긴 컨텍스트로 동시 서빙하려면 16~24GB 이상이 안전합니다.

vLLM이 항상 제일 빠른가요?

아닙니다. 동시 요청이 많을 때의 총처리량은 vLLM이 앞서지만, 요청이 하나뿐인 상황의 단일 지연은 llama.cpp가 더 나을 수 있습니다. vLLM은 배치를 채워야 이득이 나므로 동시성이 낮으면 오히려 무겁습니다.

GGUF 파일을 vLLM에서 바로 쓸 수 있나요?

vLLM도 GGUF 로딩을 지원하지만 실험적이고 최적화가 덜 되어 있습니다. vLLM에서 성능을 뽑으려면 AWQ·GPTQ·FP8 같은 GPU 지향 포맷을 쓰는 편이 낫습니다. GGUF는 llama.cpp·Ollama의 본진입니다.

기존 OpenAI API 코드를 로컬로 옮기기 어렵나요?

세 도구 모두 OpenAI 호환 /v1/chat/completions를 제공하므로, 클라이언트에서 base_url과 모델명만 바꾸면 대부분 그대로 동작합니다. 스트리밍·함수호출 등 고급 기능의 호환 범위는 도구·버전마다 차이가 있으니 실제 사용하는 기능은 검증이 필요합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록