로컬에서 LLM을 돌릴 때 llama.cpp를 쓰는 사람은 많은데, 이미지를 넣는 멀티모달 쪽은 한동안 손이 잘 안 갔다. 예전엔 llava용 바이너리, clip 변환 스크립트가 모델마다 따로 있어서 세팅이 번거로웠다. 최근 빌드에서는 멀티모달이 libmtmd라는 라이브러리 하나로 정리되면서 이 과정이 많이 간단해졌다. 그래서 다시 로컬 비전 서빙을 붙여봤다.
결론부터 말하자면, Qwen2.5-VL이나 Gemma 3 같은 비전 모델을 GGUF로 받아 llama-server 하나로 OpenAI 호환 /v1/chat/completions에 이미지를 넣을 수 있다. 필요한 건 모델 본체 GGUF와 비전 인코더인 mmproj GGUF 두 개다. 허깅페이스 저장소에서 -hf로 받으면 둘 다 자동으로 내려온다.
mmproj가 따로 있는 이유
libmtmd는 llama.cpp에 텍스트 외에 이미지, 오디오, 비디오 입력을 붙여 주는 멀티모달 라이브러리다. 2025년에 모델별로 흩어져 있던 코드를 하나로 합치면서 들어왔고, llama-server와 llama-mtmd-cli 두 도구가 이 라이브러리를 쓴다.
동작 구조는 두 단계다. 먼저 이미지를 비전 인코더가 임베딩으로 바꾸고, 그 임베딩을 언어 모델에 토큰처럼 밀어 넣는다. 이 비전 인코더와 투영 레이어를 담은 파일이 mmproj(multimodal projector)다. 언어 모델 가중치와는 별개 파일이라, 비전 모델을 돌리려면 보통 GGUF 두 개가 짝으로 필요하다. 하나는 모델 본체, 하나는 mmproj다.
mmproj를 빼면 같은 서버가 텍스트 전용으로 뜬다. 이미지를 보내도 무시되거나 에러가 난다. 로컬 파일로 직접 띄울 때 이 파일을 깜빡하는 게 가장 흔한 실수다.
설치와 바이너리 위치
맥이면 Homebrew가 제일 빠르다.
brew install llama.cpp
리눅스에서 GPU를 쓰려면 소스 빌드가 깔끔하다. NVIDIA면 CUDA 백엔드를 켜서 빌드한다.
git clone https://github.com/ggml-org/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDA=ON cmake --build build -j --config Release
빌드가 끝나면 build/bin/ 아래에 llama-server, llama-mtmd-cli가 생긴다. 버전을 확인해 둔다.
$ ./build/bin/llama-server --version version: 4589 (a5f2e3c) built with cc (Ubuntu) for x86_64-linux-gnu
빌드 번호(앞의 숫자)는 저장소 상태에 따라 달라진다. 멀티모달은 아직 변동이 잦은 영역이라, 명령 옵션이 어긋나면 그 빌드의 --help를 먼저 확인하는 편이 빠르다.
모델 내려받고 서버 띄우기
가장 간단한 방식은 허깅페이스 저장소 ID를 -hf로 넘기는 것이다. 이러면 모델 본체와 mmproj를 알아서 같이 받는다.
./build/bin/llama-server -hf ggml-org/Qwen2.5-VL-7B-Instruct-GGUF \ -ngl 99 --port 8080
-ngl 99는 가능한 레이어를 전부 GPU에 올리라는 뜻이다. GPU가 없으면 빼면 되고, CPU로도 돈다. 가볍게 먼저 확인하고 싶으면 Gemma 3 4B가 부담이 적다.
./build/bin/llama-server -hf ggml-org/gemma-3-4b-it-GGUF -ngl 99
이미 받아 둔 GGUF가 있다면 로컬 경로로 띄운다. 이때는 --mmproj를 반드시 함께 준다.
./build/bin/llama-server \ -m Qwen2.5-VL-7B-Instruct-Q4_K_M.gguf \ --mmproj mmproj-Qwen2.5-VL-7B-Instruct-f16.gguf \ -ngl 99 --port 8080
서버가 뜨면 로그 끝에 다음과 같은 줄이 보인다.
main: server is listening on http://127.0.0.1:8080 - starting the main loop srv update_slots: all slots are idle
기본 포트는 8080이고, http://localhost:8080으로 들어가면 내장 웹 UI도 열린다. 거기서 이미지를 끌어다 놓고 바로 테스트해 볼 수 있다.
OpenAI 호환 엔드포인트에 이미지 넣기
API는 OpenAI chat completions 형식을 그대로 따른다. 이미지는 메시지의 content를 배열로 바꾸고, 그 안에 image_url 항목을 넣는 방식이다. URL을 직접 주거나, 파일을 base64로 인코딩해 data URI로 넣는다. 로컬 파일이면 data URI가 편하다.
IMG=$(base64 -w0 cat.jpg)
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": [
{"type": "text", "text": "이 사진에 무엇이 있는지 설명해줘"},
{"type": "image_url", "image_url":
{"url": "data:image/jpeg;base64,'"$IMG"'"}}
]}
]
}'
맥의 base64는 -w0 옵션이 없으니 base64 -i cat.jpg로 바꾸고 줄바꿈을 지운다. 응답은 일반 chat completions와 같은 JSON으로 온다.
{
"choices": [{
"message": {
"role": "assistant",
"content": "회색 고양이 한 마리가 창틀에 앉아 바깥을 보고 있습니다..."
},
"finish_reason": "stop"
}],
"model": "Qwen2.5-VL-7B-Instruct",
"usage": { "prompt_tokens": 312, "completion_tokens": 48 }
}
엔드포인트가 OpenAI 호환이라, 파이썬 openai SDK에서 base_url만 http://localhost:8080/v1로 바꾸면 기존 코드가 거의 그대로 돈다. api_key는 아무 문자열이나 넣어도 받는다.
터미널에서 바로 던져 보기
서버 없이 한 장만 빠르게 물어보려면 llama-mtmd-cli가 낫다. 대화 세션 안에서 /image로 파일을 먼저 물린 뒤 질문한다.
./build/bin/llama-mtmd-cli -hf ggml-org/gemma-3-4b-it-GGUF -ngl 99 > /image cat.jpg > 이 사진을 한 문장으로 설명해줘
이미지 인코딩이 제대로 붙었는지, mmproj가 맞는지 확인하는 용도로 쓰기 좋다. 서버 설정을 건드리기 전에 여기서 먼저 걸러낸다.
자주 걸리는 지점
로컬 파일로 띄웠는데 모델이 이미지를 못 본다고 하면, 열에 아홉은 --mmproj를 안 준 경우다. -hf로 받을 땐 자동이지만 -m으로 직접 띄우면 수동이다. mmproj 없이 뜨면 서버 로그에 vision 관련 초기화 줄이 아예 없다.
mmproj는 f16이나 f32 그대로 쓰는 걸 권한다. 용량을 줄이겠다고 mmproj까지 저강도로 양자화하면 이미지 인식 품질이 눈에 띄게 떨어진다. 양자화는 언어 모델 본체(Q4_K_M 등)에만 걸고, 비전 인코더는 건드리지 않는 게 안전하다.
VRAM이 빠듯하면 --no-mmproj-offload로 비전 인코더만 CPU에 남길 수 있다. 이미지 인코딩이 조금 느려지는 대신 GPU 메모리를 아낀다. 반대로 멀티모달을 아예 끄고 텍스트로만 쓰려면 --no-mmproj를 준다.
이미지는 컨텍스트를 많이 먹는다. 특히 Qwen2.5-VL은 해상도가 큰 이미지를 많은 토큰으로 펼친다. 큰 사진을 넣고 컨텍스트가 넘친다는 에러가 나면 -c 8192처럼 컨텍스트 길이를 늘리거나, 이미지를 미리 줄여서 보낸다.
오디오 쪽은 Ultravox, Voxtral 같은 모델과 Qwen2.5-Omni처럼 이미지와 소리를 같이 받는 모델도 -hf로 받을 수 있다. 비전과 같은 구조라 mmproj가 음성 인코더 역할을 한다. 다만 이 영역은 변동이 잦으니 쓰기 전에 해당 빌드 문서를 확인하는 편이 낫다.