로컬에서 모델을 여러 개 돌리다 보면 모델마다 llama-server 프로세스를 따로 띄우고 포트를 나눠 쓰게 된다. 채팅용, 코드용, 임베딩용으로 세 개를 올려 두면 쓰지도 않는 모델이 VRAM을 깔고 앉아 있다. 최근 llama.cpp에 들어온 라우터 모드를 쓰면 이 구조를 엔드포인트 하나로 접을 수 있다.
라우터 모드는 모델을 지정하지 않고 llama-server를 띄우면 켜진다. 모델 디렉터리나 프리셋 파일을 읽어 두고, 요청의 model 필드를 보고 해당 모델을 그 자리에서 로드해 응답을 돌려준다. 동시에 올라가는 모델 수는 --models-max(기본 4개)로 제한하고, 한도를 넘으면 가장 오래 안 쓴 모델부터 내린다. 아래에서 디렉터리로 띄우는 방법부터 프리셋 설정, 그리고 네 개가 한꺼번에 올라가 VRAM을 넘겨 버리는 함정까지 순서대로 다룬다.
1. 라우터 모드가 하는 일
기존 llama-server는 프로세스 하나에 모델 하나다. 라우터 모드는 이 프로세스를 관리자 역할로 돌려서, 내부에 여러 모델 인스턴스를 띄우고 요청을 맞는 인스턴스로 넘긴다. OpenAI 호환 API(/v1/chat/completions 등)는 그대로 쓰고, 요청 본문의 model 값만 바꾸면 백엔드 모델이 바뀐다.
모델을 처음 요청하면 그때 로드된다(lazy load). 쓰지 않으면 메모리에 올라가지 않고, --models-max 한도에 걸리면 LRU(least recently used, 가장 오래 안 쓴 것) 순으로 언로드한다. 모델별로 프로세스와 포트를 나눠 관리하던 걸 설정 파일 한 개로 줄여 준다.
라우터 모드는 비교적 최근 빌드에 들어왔고 플래그 이름이 버전에 따라 조금씩 바뀌었다. 먼저 llama-server --help로 --models-dir, --models-max 항목이 있는지 확인하고 시작하는 편을 권한다.
$ llama-server --help | grep -A1 models-
--models-dir PATH for router server, directory containing models (default: disabled)
--models-preset PATH for router server, path to INI file containing model presets
--models-max N for router server, maximum number of models to load simultaneously
(default: 4, 0 = unlimited)
--models-memory-margin N for router server, MiB of device memory to keep free per device,
unload LRU models until a new one fits (default: 0, 0 = disabled)
--no-models-autoload for router server, whether to automatically load models (default: enabled)
2. 디렉터리 하나로 띄우기
가장 간단한 방식은 GGUF 파일을 한 디렉터리에 모아 두고 --models-dir로 가리키는 것이다.
$ ls ./models qwen2.5-7b-instruct-q4_k_m.gguf qwen2.5-coder-7b-q4_k_m.gguf nomic-embed-text-v1.5.Q8_0.gguf $ llama-server --models-dir ./models -c 8192 -ngl 99 --host 0.0.0.0 --port 8080
여기서 -c(컨텍스트 크기)와 -ngl(GPU에 올릴 레이어 수)는 라우터 레벨 설정이라, 이 디렉터리의 모든 모델이 같은 값을 물려받는다. 임베딩 모델까지 컨텍스트 8192에 전부 GPU로 올리면 모델별 최적값과 어긋날 수 있다. 모델마다 다르게 주고 싶으면 다음 절의 프리셋을 쓴다.
모델 목록은 GET /models로 확인한다. 디스커버리된 모델과 현재 로드 상태가 같이 나온다.
$ curl -s http://localhost:8080/models | jq '.data[] | {id, status}'
{
"id": "qwen2.5-7b-instruct-q4_k_m.gguf",
"status": "unloaded"
}
{
"id": "qwen2.5-coder-7b-q4_k_m.gguf",
"status": "unloaded"
}
처음엔 전부 unloaded다. 채팅 요청을 한 번 던지면 해당 모델만 로드된다.
$ curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5-7b-instruct-q4_k_m.gguf",
"messages": [{"role": "user", "content": "ping"}]
}' | jq -r '.choices[0].message.content'
pong
첫 요청은 모델을 디스크에서 읽어 GPU에 올리는 시간 때문에 느리다. 모델 크기와 디스크 속도에 따라 다르지만 첫 응답은 수 초가 걸릴 수 있고, 두 번째부터는 이미 올라가 있으니 평소 속도가 나온다.
3. 프리셋으로 모델마다 다르게
모델별 컨텍스트 크기나 GPU 레이어 수를 따로 주려면 INI 프리셋 파일을 만들고 --models-preset으로 넘긴다. 섹션 이름(대괄호 안)이 클라이언트가 model 필드에 쓰는 식별자가 된다.
$ cat models.ini [chat] model = /opt/models/qwen2.5-7b-instruct-q4_k_m.gguf ctx-size = 16384 ngl = 99 load-on-startup = true [coder] model = /opt/models/qwen2.5-coder-7b-q4_k_m.gguf ctx-size = 32768 ngl = 99 [embed] model = /opt/models/nomic-embed-text-v1.5.Q8_0.gguf ctx-size = 2048 ngl = 99
$ llama-server --models-preset ./models.ini --host 0.0.0.0 --port 8080
이제 클라이언트는 짧은 이름으로 모델을 부른다. "model": "coder"로 보내면 코더 모델이, "embed"로 보내면 임베딩 모델이 응답한다.
load-on-startup = true를 준 chat 모델은 서버가 뜨면서 미리 로드된다. 자주 쓰는 모델은 이걸 켜 두면 첫 요청의 로드 지연을 피한다. 단 이 설정은 서버 기동 시점에만 적용된다. 프리셋을 나중에 고쳐 모델을 추가하면 목록에는 보여도 자동으로 올라가지는 않는다.
전역 플래그 --no-models-autoload를 주면 자동 로드 자체가 꺼진다. 이 플래그는 프리셋의 load-on-startup = true보다 우선한다. 자동 로드를 끄면 모든 모델을 POST /models/load로 직접 올려야 한다.
$ curl -s -X POST http://localhost:8080/models/load \
-H "Content-Type: application/json" \
-d '{"model": "coder"}'
$ curl -s -X POST http://localhost:8080/models/unload \
-H "Content-Type: application/json" \
-d '{"model": "coder"}'
INI 포맷은 아직 완전히 표준화되지 않았고 키 이름이 빌드에 따라 달라진 적이 있다. ngl이 안 먹으면 n-gpu-layers로, ctx-size가 안 먹으면 c로 바꿔 보고, 막히면 역시 --help로 현재 빌드의 키를 확인한다.
4. 네 개가 한꺼번에 올라가면 VRAM이 터진다
여기서 가장 많이 걸리는 지점이다. --models-max의 기본값은 4인데, 이 숫자는 "개수" 제한이지 "용량" 제한이 아니다. 라우터는 네 번째 모델까지는 VRAM에 들어가든 말든 일단 올리려 든다. 각 모델이 혼자서는 들어가도, 서로 다른 모델 네 개를 동시에 요청받으면 합쳐서 VRAM을 넘겨 OOM이 난다.
예를 들어 24GB GPU에서 7B Q4 모델은 하나에 5~6GB 정도를 쓴다. 세 개까지는 들어가지만, 네 번째로 14B 모델을 요청하면 남은 공간이 모자라 로드가 실패한다. 이때 전형적으로 이런 에러가 뜬다.
ggml_backend_cuda_buffer_type_alloc_buffer: allocating 8192.00 MiB on device 0: cudaMalloc failed: out of memory llama_model_load: error loading model: unable to allocate CUDA0 buffer llama_load_model_from_file: failed to load model
첫 번째 대응은 --models-max를 실제로 동시에 올려도 되는 개수로 낮추는 것이다. 24GB에 7B Q4만 쓴다면 3 정도가 현실적이다. 한도를 낮추면 네 번째 모델을 요청할 때 LRU 순으로 하나를 내리고 자리를 만든 뒤 새 모델을 올린다.
$ llama-server --models-preset ./models.ini --models-max 3 --port 8080
다만 개수만으로는 크기가 제각각인 모델을 못 막는다. 7B 세 개는 들어가도 14B 두 개와 7B 하나는 안 들어갈 수 있다. 그래서 더 확실한 안전장치는 --models-memory-margin이다. 디바이스마다 지정한 MiB만큼 여유를 남겨 두고, 새 모델이 그 여유 안에 들어갈 때까지 LRU 모델을 내린다. 개수가 아니라 실제 남은 메모리를 보고 판단한다.
$ llama-server --models-preset ./models.ini \
--models-max 4 \
--models-memory-margin 2048 \
--port 8080
이렇게 주면 라우터는 GPU에 최소 2GB를 비워 두려 하고, 새 모델이 안 들어가면 오래된 모델을 먼저 내린다. 멀티 GPU면 디바이스별로 적용된다. 실무에선 --models-max로 개수 상한을 두고, --models-memory-margin으로 용량 바닥을 받쳐 두는 조합을 쓴다.
로드 상태와 VRAM은 로드 직후 nvidia-smi로 같이 보면 감이 잡힌다. 모델을 하나씩 요청해 가며 메모리가 얼마나 차는지 재 보고, 그 수치에 맞춰 --models-max와 마진을 정하면 된다.
$ nvidia-smi --query-gpu=memory.used,memory.total --format=csv memory.used [MiB], memory.total [MiB] 17820 MiB, 24564 MiB
5. systemd로 상주시키기
라우터를 상시 띄워 둘 거라면 유닛 파일로 묶는다. 모델 경로와 마진 값만 환경에 맞게 바꾸면 된다.
$ cat /etc/systemd/system/llama-router.service [Unit] Description=llama.cpp router server After=network.target [Service] ExecStart=/opt/llama.cpp/llama-server \ --models-preset /opt/models/models.ini \ --models-max 3 \ --models-memory-margin 2048 \ --host 0.0.0.0 --port 8080 Restart=on-failure [Install] WantedBy=multi-user.target $ sudo systemctl daemon-reload $ sudo systemctl enable --now llama-router $ systemctl status llama-router
프리셋을 고쳤을 때는 재시작해야 반영된다. 모델을 추가하고 sudo systemctl restart llama-router를 하면 새 목록으로 올라온다. 다음으로 해 볼 만한 것은 자주 쓰는 모델 조합의 VRAM 합을 실제로 재서, 그 GPU에서 --models-max를 몇으로 둘지 숫자를 확정하는 일이다.