Files
llmgw/deploy/mirror/README.md
T
Ilker ManapandClaude Opus 5 cd7eedf09e
CI / test (push) Successful in 1m49s
Give the mirror a page, so managing it is not a curl command
/admin/mirror, behind the same admin session as the other admin pages. What is
held, what is arriving with its byte count, what failed and why; start a fetch,
retry a failed one, remove a version.

Not mounted at all without a mirror configured. The right page for a deployment
with no mirror is no page, rather than an empty one pointing at nothing.

It reloads itself only while something is downloading. A reload at any other
moment takes with it whatever is half-typed into the fetch form, and there is
nothing to watch when nothing is moving.

A failed row offers Retry, not Remove. A fetch that failed left nothing on disk,
so the remove could only answer "the mirror holds nothing for this" — a button
whose single outcome is a confusing error. Starting the same download again is
what somebody looking at that row wants, and the mirror allows it: only a
download still running is refused.

An unreachable mirror is said outright instead of rendering as an empty table.
The two look nearly identical and mean opposite things: one is a mirror waiting
to be filled, the other is a machine to go and look at.

Dashboard takes NavExtras rather than a second bool. Two adjacent booleans at a
call site get swapped silently and are discovered by somebody clicking the
wrong link.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 10:17:12 +02:00

263 lines
9.8 KiB
Markdown

# A model mirror
One machine that holds the model files, so nodes fetch over your own network
instead of over the internet.
This is the highest-value piece of infrastructure around llmgw and the cheapest.
Eight nodes each pulling forty gigabytes from Hugging Face is a day's work, and
on plenty of corporate networks it is not possible at all. From a mirror on the
same switch it is minutes.
llmgw needs nothing special for this: a model's `source` is a URL, and the URL
can be yours.
## What it is
An HTTP server serving a directory. That is the whole design.
```
/srv/models/
qwen2.5-14b-instruct/
2026-03/
q4_k_m.gguf
bge-m3/
2026-01/
model.safetensors
```
The date in the path is the point of the layout, not decoration. A model
repository that changes upstream changes what you are running, and the first
sign is a prompt that worked last week answering differently this week with
nobody able to say why. Pin it here and that cannot happen.
## Running it
`llmgw` is the mirror. Same binary as the control plane and the agents, one
subcommand:
```
sudo mkdir -p /srv/models && sudo chown llmgw:llmgw /srv/models
llmgw mirror serve --dir /srv/models --base http://models.internal:8081
```
There is a unit in `deploy/systemd/llmgw-mirror.service`. Set `--base` to the
address nodes reach this machine on — not `localhost`, and not a name only this
host knows itself by. It is the string that ends up in catalog entries, and a
wrong one makes `mirror unused` report files as unreferenced when they are in
use. The next step after that list is deletion.
### Two listeners
`--listen :8081` serves the weights. Read-only, unauthenticated, Range requests
supported so an interrupted fetch on a node resumes rather than restarts. This
is the path eight nodes pull forty gigabytes from.
`--api-listen :8082` is the management API. Separate on purpose: putting a
client certificate in front of the byte path would mean re-certifying every
agent's fetcher in order to change how models are administered.
The API is unauthenticated unless you give it the fleet's CA, and it says so at
startup rather than leaving you to find out. Anyone who can reach that port can
start downloads and delete models, so either keep it on a trusted network or
hand it certificates:
```
llmgw mirror serve --dir /srv/models --base http://models.internal:8081 \
--tls-ca /etc/llmgw/ca.pem \
--tls-cert /etc/llmgw/mirror.pem \
--tls-key /etc/llmgw/mirror-key.pem
```
That is the same CA the agents already use, so the certificates come from the
authority you have:
```
llmgw ctl cert issue-server mirror --host models.internal --host 10.0.0.146
llmgw ctl cert issue-client control-plane
```
`--host` has to cover every address the API is called on, because that is what
a client verifies against. Both sides are checked — the mirror will not accept a
caller without a certificate from this CA, the same rule the agent stream
follows.
### nginx instead
If you already run nginx, it serves a directory perfectly well and you do not
need `mirror serve` for the bytes:
```nginx
server {
listen 80;
server_name models.internal;
root /srv/models;
# Read-only. This is deliberate: a mirror that can be written to over HTTP
# is a way to replace a model with another one, and the nodes would fetch
# it without complaint.
location / {
autoindex on;
# Large files, and the client is a machine with a resumable fetcher.
# Sendfile and no buffering keep memory flat regardless of size.
sendfile on;
tcp_nopush on;
}
}
```
`autoindex` is there so a person can browse what is available; llmgw does not
need it.
## Filling it
From the mirror itself:
```
llmgw mirror add \
--model qwen2.5-14b-instruct --version 2026-03 \
"https://huggingface.co/<repo>/resolve/<revision>/q4_k_m.gguf"
```
Use a revision rather than `main` in the source URL. `main` is what moves.
It downloads to a temporary name and renames when complete, so an interrupted
download never leaves something a catalog could be pointed at, and it prints the
sha256 when it lands. Compare that against what upstream publishes — Hugging
Face gives it as `lfs.oid` from
`https://huggingface.co/api/models/<repo>/tree/<revision>`. A mirror is the one
place in this system where a corrupt file is copied to every node instead of
being caught once.
It refuses to overwrite a version that is already there. Add a new one beside
it: that refusal is what makes the old weights still available when the new ones
turn out to be worse.
## Driving it from elsewhere
The same fetch over the API, so the control plane — or anything else — can ask
for a model without anyone logging in:
```
curl -X POST http://models.internal:8082/v1/models -d '{
"source": "https://huggingface.co/<repo>/resolve/<revision>/q4_k_m.gguf",
"model": "qwen2.5-14b-instruct",
"version": "2026-03"
}'
```
The mirror does the downloading. A control plane that pulled the bytes itself
and pushed them here would carry every model twice over the network, through the
one machine least able to spare the bandwidth.
It answers `202` with the URL a catalog entry should use, and the bytes are not
here yet. Poll for them:
| | |
|---|---|
| `GET /v1/models` | everything held or arriving, and the base to build URLs from |
| `POST /v1/models` | fetch this source into model/version |
| `GET /v1/models/{model}/{version}` | `fetching` with a byte count, `present`, or `failed` with the reason |
| `DELETE /v1/models/{model}/{version}` | remove a version; refuses while it is downloading |
| `GET /v1/health` | up *and* able to read its own directory |
A failed fetch stays `failed` with what went wrong, rather than disappearing:
otherwise the thing waiting on that model waits forever with nothing to say why.
Asking twice for the same download joins the one already running instead of
starting a rival writer on the same file.
`DELETE` takes the whole version, never one file of it. A model split across
several files is useless with one missing, and half a version on disk looks
present and will not load.
## Retiring a model
```
llmgw mirror unused # what no catalog entry refers to
llmgw mirror remove --model <m> --version <v> --file <f>
```
`unused` compares against every `source` in the running catalog, so the answer
is what can go rather than what looks old. It refuses to answer when it cannot
reach the control plane: an empty catalog and an unreachable one produce the
same list — everything — and the obvious next step after that list is deletion.
Retire the catalog entry first, let the fleet settle, then remove. A node that
has to refetch a file you deleted cannot, and the failure arrives later, on the
node, looking like something else.
## From the control plane's admin page
Configure the mirror on the control plane and the same operations appear at
`/admin/mirror`, behind the admin session the other admin pages use:
```yaml
mirror:
api: https://models.internal:8082
tls:
ca: /etc/llmgw/ca.crt
cert: /etc/llmgw/control-plane.crt
key: /etc/llmgw/control-plane.key
```
Leave the block out and the page is not mounted at all, which is the right page
for a deployment with no mirror — not an empty one pointing at nothing.
The page lists what is held, what is arriving with its byte count, and what
failed with the reason; it starts a fetch, retries a failed one, and removes a
version. It reloads itself only while something is downloading, because a reload
at any other moment takes with it whatever is half-typed in the form.
Two things it deliberately will not do. It cannot point the control plane at a
different mirror — that is configuration, and changing it needs access to the
host rather than a session. And it does not write the catalog for you: it shows
the URL an entry should use, and putting it there is still a deliberate act,
because that is the moment a model starts being served.
The address is a mistake worth naming: `api` is the **management** port, not the
one nodes fetch bytes from. They are two listeners so this one can be locked
down without re-certifying every agent's fetcher.
## Pointing llmgw at it
```yaml
- name: qwen2.5-14b-instruct
source: http://models.internal/qwen2.5-14b-instruct/2026-03/q4_k_m.gguf
scaling: { min: 2, max: 2 }
profiles:
- name: gpu
engine: vllm
gpu_class: h100-80g
tp: 1
```
Each node fetches once, in the background, before its engine starts — and
`llmgw ctl replicas` shows how far along it is while it does. A killed download
resumes rather than restarting, and a partial file is never visible under the
model's name, so an interrupted fetch cannot be handed to an engine.
## What this is not
It is not a cache. Nothing populates it on its own: a model is there because
somebody asked for it, by hand or over the API, and one nobody asked for is not
there. That is on purpose. A mirror that fetched on demand would reach the
internet at the least convenient moment, and in a network where that is blocked
it would fail in a way that looks like llmgw being broken.
It is also not a replacement for a shared filesystem. If you already run one
that every node mounts, put the models on it and use a path as the `source` —
nothing is fetched at all, and llmgw notices the filesystem is shared and will
not count the same free space once per node.
## When this stops being enough
Somewhere above ten or twenty nodes, one mirror serving a forty gigabyte file
to all of them at once becomes the bottleneck instead of the fix. At that point
the options are a filer, a second mirror per rack, or distribution between the
nodes themselves.
llmgw knows which nodes already hold which weights — it is in `ctl status` as
"N models on X/Y nodes" — so distributing between them is a feature it is
positioned to add. It has not been built, because a mirror is enough for
everyone who has needed it so far.