docs.na.id.au

Why two servers, not one

03 — Primary vs Secondary llama-server

The two-server pattern: a big “brain” card and a small “worker” card, and how to decide what lives on each.

Why two servers, not one

The temptation is one llama-server that sees both GPUs and holds everything. The two-server split is better for three reasons:

  1. Hardware isolation. CUDA_VISIBLE_DEVICES pins each server to one card, so a context-heavy request on the big card can never evict or starve the small one.
  2. Independent roles. The two cards do genuinely different jobs, with different models, context sizes, and loading behaviour — see below.
  3. Independent lifecycle. Each is its own systemd unit. You can restart, upgrade, or stop one without touching the other — and on a dual-use box, stop both to hand the GPUs to a desktop.

The division of labour

Primary (big card)Secondary (small card)
CardRTX 3090 (24 GB), CUDA_VISIBLE_DEVICES=0RTX 3070 (8 GB), CUDA_VISIBLE_DEVICES=1
Port80828083
RoleMain agent — the model the user primarily chats withWorker — sub-agent chat only
Modelse.g. a 12B and a 27B (big weights, big context)a small chat model (embeddings moved to a dedicated CPU server)
Context~185k–256k16k (chat); embeddings are 2k on the CPU helper
LoadingSwap-in (--models-max 1, load-on-startup = false)Resident (load-on-startup = true)
Restarton-failurealways

The split is by job, not by model count. The primary is where “big brain” work belongs — long documents, long tool conversations, spec-writing. The secondary handles high-volume, cheap, latency-sensitive work (short sub-agent turns) without ever touching the primary’s context memory, and the embedding model has its own home on the CPU helper (below).

The primary server (swap-in)

ExecStart=/usr/local/bin/llama-server \
    --models-preset /etc/llama/models-primary.ini \
    --models-max 1 \
    --no-mmproj \
    --metrics \
    --host :: \
    --port 8082

The secondary server (resident + embeddings)

ExecStart=/usr/local/bin/llama-server \
    --models-preset /etc/llama/models-secondary.ini \
    --no-mmproj \
    --embeddings \
    --metrics \
    --host :: \
    --port 8083

The embedding endpoint

A dedicated small embedding model (bge-m3, ~567M params, 1024-dim vectors, CLS pooling) — now hosted on a CPU-only helper server in the current setup (below) — is the clean way to serve memory/RAG. Two things must both be true:

  1. The server was started with --embeddings.
  2. The model profile has pooling = cls (BGE-M3 pools on the CLS token).

The memory service then calls http://host.docker.internal:8084/v1/embeddings with model: "bge-m3". Because it’s a small, resident, fast model, embeddings are essentially free and never compete with the primary’s big context.

The embedding escape hatch: a dedicated CPU-only server

When the small card gets crowded, move the resident embedding model to a third CPU-only llama-server (own systemd unit, own preset file, gpu-layers = 0, port 8084). Benefits: frees ~1.75 GB GPU VRAM, embeddings get an independent lifecycle (restart/dual-use desktop switching never touches them), and they no longer compete with the chat worker. Note the --embeddings flag remains on secondary as a cheap option for boxes without a third server.

Choosing the split for your own box

The general principle: put the model that must be fast and cheap on the small card; put the model that must be smart and deep on the big card. Concretely:


Source Disclaimer