Serve a machine's models from one shared llama-server
A llama.cpp session had its own `llama-server`: two sessions on one model held two copies of it in memory, a model change bought a load only that session benefited from, and the process was a session's to end. A machine's models are now served by one `llama-server` in **router mode** -- no `-m`, a preset file naming models and their flags, a child server per model asked for, and each request routed by its `model` field. So one server per model with that model's own settings is what a machine runs, while this backend has one process, one port and one record per machine to keep track of. The record is the mechanism every other driver already uses, so a restart adopts it; a session records the same pid in its own directory as `Detail::Shared`, and `process::signal` refuses to signal one of those -- which is what keeps stopping, deleting or cleaning up after one session from unloading a model every other session is using. Nothing stops a router on its own. That is deliberate (a loaded model is minutes of disk) and it is why the machines tab now has a card per provider that opens its own screen: how each model is loaded, how many stay in memory, Unload, and Stop. How a model is *loaded* therefore belongs to the model on its machine rather than to a session -- context size, GPU layers, threads, slots, speculative decoding -- written into the preset as llama-server's own argument names. Saving them re-reads that file, which unloads the model; that is the change taking effect, and the dialog says so before you save. What stays a session's is everything that rides on a request, including which tools it offers: the router hosts one set for the machine and the choice is a filter applied here, so it costs no reload (2,181 tokens of prompt with all seven, 698 with none). Verified end to end against the scratch backend and the emulator: two sessions sharing one loaded model with one child process, a second session joining it with a 26ms prefill, a backend restart adopting the router and answering with the prompt cache intact, the same over ssh to this VM, a model's settings reaching the running server, Unload, and Stop leaving every session `exited` with no error line.
This commit is contained in:
1 parent
74cda485e5
commit
8c323fc7a9
19 files changed
+2601
-511
No files matched your search
@@ -338,8 +338,9 @@ thread, never that thread's own assistant reply.
|
||||
|
||||
### The llama driver
|
||||
|
||||
One `llama-server` per session, started through the same `Transport` as any
|
||||
other process and then reached over HTTP on a loopback port. Two things are
|
||||
One `llama-server` per **machine**, in router mode, shared by every session on
|
||||
it and reached over HTTP on a loopback port through the same `Transport` as
|
||||
any other process. A session has no process of its own. Two things are
|
||||
deliberate and easy to undo by accident:
|
||||
|
||||
- **The conversation is rebuilt from the transcript**, not kept in the
|
||||
@@ -347,6 +348,50 @@ deliberate and easy to undo by accident:
|
||||
when the process restarts. That leaves the Claude driver as the odd one
|
||||
out rather than this one — the CLI's memory is a cache in front of the same
|
||||
transcript. Resolve any inconsistency in this direction.
|
||||
- **The machine's server is shared, outlives this backend, and stops only
|
||||
when somebody says so** (2026-09-19, `session/llama/router.rs`). It was one
|
||||
`llama-server` per session until then, which meant two sessions on one model
|
||||
held two copies of it in memory and a model change bought a load only the
|
||||
session that asked for it benefited from. `llama-server` started with no
|
||||
`-m` is a **router**: it reads a preset file naming models and their flags,
|
||||
starts a child server per model that is asked for, and routes each request
|
||||
by the `model` field in it. So "one server per model, with that model's own
|
||||
settings" is what a machine runs, while this backend has one process, one
|
||||
port and one record to keep track of.
|
||||
- **The record is the same mechanism every other driver uses**, so a
|
||||
restart adopts it: `process.json` in the router's own directory beside
|
||||
the session directories. A session records the *same* pid in its own
|
||||
directory as a `Detail::Shared`, which is what makes "is the thing I am
|
||||
talking to still there?" one question with one answer — and `process::
|
||||
stop` refuses to signal a `Shared` record, so stopping, deleting or
|
||||
cleaning up after one session cannot take a model out of memory for
|
||||
every other session on that machine. A flag would have been a rule to
|
||||
remember in five places; the variant is checked in the one function that
|
||||
signals.
|
||||
- **Nothing unloads a model on its own.** The last session closing leaves
|
||||
it loaded on purpose — the next session to want it would otherwise pay
|
||||
the load again — so the memory is freed from the machine's provider
|
||||
settings, where what it costs everybody is visible. `--models-max`
|
||||
(default 1 here, editable) is the one automatic eviction, and it is LRU:
|
||||
a machine with one GPU wants the second model to replace the first.
|
||||
- **How a model is loaded belongs to the model, not to the session**
|
||||
(`ProviderConfig::model_settings`, `LLAMA_MODEL_PARAMS`). Context size,
|
||||
GPU layers, threads, slots, speculative decoding: one loaded copy answers
|
||||
several sessions, so a session cannot own these without one of them being
|
||||
silently ignored. They are written into the preset file as
|
||||
`llama-server`'s own argument names, and saving them re-reads that file —
|
||||
which **unloads** the model if it was loaded. That is the change taking
|
||||
effect rather than a side effect, and the dialog says so before you save.
|
||||
What stays the session's is everything that rides on a request:
|
||||
temperature and the rest, thinking, the permission mode, and which tools
|
||||
it offers.
|
||||
- **The preset file lives on the machine that serves the models**, written
|
||||
over the same transport (base64 through `sh`, so an INI value never
|
||||
passes through quoting twice). It is read back before every edit rather
|
||||
than remembered: a router adopted from a previous run is already serving
|
||||
models whose sections this process has never seen, and rewriting the file
|
||||
without them would unload them at the next re-read. Only a text that
|
||||
actually differs is written, for the same reason.
|
||||
- **A llama session runs on its configured machine** (2026-09-04,
|
||||
the last of phase 5). A transport is "run this" plus "reach this port", and
|
||||
the second half is `Transport::reserve_port` — the port the server binds
|
||||
@@ -388,8 +433,13 @@ deliberate and easy to undo by accident:
|
||||
will not load, a port already taken, a flag an older build does not know:
|
||||
all exit within a second and none will ever answer `/health`, so waiting
|
||||
out the 300s timeout turned the server's own account of the problem into
|
||||
"gave up". The failure carries the tail of `llama-server.log`, which on a
|
||||
remote session is the only copy anybody reading the phone can see.
|
||||
"gave up". The failure carries the tail of `llama-router.log`, which on a
|
||||
remote machine is the only copy anybody reading the phone can see — the
|
||||
router's children write into it too, so a model that would not load says
|
||||
why. A load is two questions since the router arrived: the router
|
||||
answering at all (30s, because it loads nothing), and the model reaching
|
||||
`loaded` in `GET /models` (300s, because it is disk). A model the router
|
||||
reports `unloaded` with an exit code is a failure now rather than a wait.
|
||||
- **Loading is a state of its own** (2026-09-19, `SessionStatus::Loading`).
|
||||
A multi-gigabyte model takes tens of seconds to reach memory and refuses
|
||||
everything until it has, and the session used to report `running` for that
|
||||
@@ -401,6 +451,16 @@ deliberate and easy to undo by accident:
|
||||
not ready" is a fact only a driver can have. The third state matters as
|
||||
much as the first two: a model that will never load has to answer a waiting
|
||||
message with what went wrong rather than holding it for ever.
|
||||
- **Which tools a session offers is a filter here, not a flag there**
|
||||
(2026-09-19). The router is always started with `--tools all` and hosts one
|
||||
set of tools for the machine — one per session is not a thing a shared
|
||||
server can have — so the `tools` param picks from the definitions this
|
||||
backend sends with each request. It costs no reload, and it is worth
|
||||
choosing: all seven are ~2,000 tokens of every prompt, measured at 2,181
|
||||
against 698 with none, which on a small context window is the difference
|
||||
between a usable session and one that overruns. `POST /tools` carries an
|
||||
`x-tool-cwd` header, which is what lets one shared server run each
|
||||
session's tools in that session's own directory.
|
||||
- **The driver runs the agent loop, and therefore owns the permission gate**
|
||||
(2026-09-19). `llama-server --tools all` *hosts* the built-in tools —
|
||||
`GET /tools` is their definitions, `POST /tools` runs one — but it does not
|
||||
@@ -422,15 +482,21 @@ deliberate and easy to undo by accident:
|
||||
out, not the machine with the GPU. `llama-server`'s own `--mcp-servers-json`
|
||||
is deliberately not used — it can only spawn local commands, so a remote
|
||||
server would mean a Node bridge on whichever machine serves the model.
|
||||
- **A model change reloads the server rather than being refused** (2026-09-19).
|
||||
A `llama-server` holds one model, so switching stops it and starts another;
|
||||
the conversation survives because the conversation was never in the server.
|
||||
What is lost is the prompt cache, which is exactly what the phone already
|
||||
warns about before a switch.
|
||||
- **One slot, and the draft head where the file has one** (measured
|
||||
2026-09-19). `-np 1` always: a session is one conversation making one
|
||||
request at a time, so the other three slots `llama-server` picks on its own
|
||||
are context this session could have had. It is also what decides whether
|
||||
- **A model change asks for another model rather than being refused**
|
||||
(2026-09-19). Nothing is stopped: the machine's server holds whichever
|
||||
models it has been asked for, and the one this session is leaving may be
|
||||
somebody else's. It costs a load where nobody had that model open and a
|
||||
round trip where somebody did. The conversation survives either way because
|
||||
it was never in the server. What is lost is the prompt cache, which is
|
||||
exactly what the phone already warns about before a switch.
|
||||
- **One slot by default, and the draft head where the file has one**
|
||||
(measured 2026-09-19). `parallel = 1` unless that model is told otherwise:
|
||||
a session is one conversation making one request at a time, so the other
|
||||
three slots `llama-server` picks on its own are context nobody asked for.
|
||||
Sharing one server makes the number a real choice — a second session's turn
|
||||
waits behind the first at one slot — which is why it is a per-model setting
|
||||
rather than a constant, with the trade-off measured below. It is also what
|
||||
decides whether
|
||||
multi-token prediction pays — on the 27B here, **41.5 tok/s** plain at any
|
||||
slot count, **61.4** with `--spec-type draft-mtp` at one slot, and **28**
|
||||
with the head at four. Speculating against a split KV cache is worse than
|
||||
@@ -1262,6 +1328,22 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
|
||||
row that has just moved ignores taps for `SETTLE_MS`.
|
||||
3. **Models** and **Machines** — browsing and downloading GGUFs; adding,
|
||||
renaming, re-probing and removing machines.
|
||||
**A provider is a card that opens** (2026-09-19, `ProviderScreen.kt`).
|
||||
Settings that belong to a *machine* had nowhere to live until one
|
||||
`llama-server` came to serve every session on one: how each of its models
|
||||
is loaded, how many it keeps in memory, and the only control that takes a
|
||||
loaded model out of memory again. So each of a machine's providers is a
|
||||
card, and tapping it opens that provider on that machine. What it shows is
|
||||
fetched rather than carried from the card, because a stale copy of it
|
||||
would be a second version of the same truth.
|
||||
**Stop is shown and disabled rather than hidden**, with the confirmation
|
||||
saying plainly what it costs: every model unloaded, every session on that
|
||||
machine showing as exited, and the next message to one paying the load
|
||||
again. Hiding the destructive option would not prevent the outcome, only
|
||||
move it somewhere with no warning attached.
|
||||
**A model with no status line is one nobody could ask about** — the server
|
||||
is not running — rather than one that is unloaded. The two are different
|
||||
facts and only one of them was measured.
|
||||
4. **Session screen** — the core:
|
||||
- The transcript rendered from the event stream: markdown, inline images,
|
||||
tool cards, question cards.
|
||||
|
||||
Reference in new issue
Block a user