CI / test (push) Successful in 1m26s
Building node05 by hand turned up three things worth fixing, in the order they bit. Onboarding died on "Host key verification failed" — an error about ssh, in the middle of a script whose entire job is to talk to a machine created sixty seconds ago. It now uses accept-new, which learns an unknown host key and still refuses a changed one; the risk being guarded against is a substituted machine, not a new one. Then ssh started refusing connections on a node that had answered a moment before: cloud-init regenerates the host keys and restarts sshd partway through a new machine's first minute. The agent was registered and healthy throughout. Onboarding now waits for ssh rather than failing on it, which is also what makes running the two halves back to back safe. Then llama-server: present, executable, and unable to start for want of libgomp1. The node registered ready and would have stayed ready until a replica was placed on it and the shard died with a missing shared library three logs away. The docs already warned about this shape of failure; nothing checked for it. Onboarding now runs a configured engine and says plainly when it will not start. add-node.sh drives both halves. The reason is not keystrokes: the VM name, the hostname, the node id and the certificate's CN must all be the same string, and the address appears in three places. Split across two commands run minutes apart, that agreement is held together by whoever is typing. Here it is stated once, and the VMID defaults to the last octet of the address so qm list and ctl nodes stay in the same order. The two halves remain separately runnable, because needing the hypervisor and needing the CA really are different access. Verified by building a throwaway node06 with it: 45 seconds from nothing to registered, and the engine check caught that node06 had no llama-server. Destroyed afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
8.3 KiB
Markdown
203 lines
8.3 KiB
Markdown
# Adding a node
|
|
|
|
Written for whoever runs the cluster.
|
|
|
|
## The short version
|
|
|
|
Where one person holds both the hypervisor and the certificate authority —
|
|
which is the common case — one command does the whole thing in about
|
|
45 seconds:
|
|
|
|
```
|
|
llmgw-add-node \
|
|
--name llmgw-node05 --ip 10.0.0.145/24 \
|
|
--server https://10.0.0.140:7443 \
|
|
--cores 8 --memory 32768 --disk 60 \
|
|
--llamacpp-bin /opt/llama.cpp/llama-server
|
|
```
|
|
|
|
The VMID defaults to the last octet of the address, so `qm list` and
|
|
`llmgw ctl nodes` stay in the same order. `--provision-only` and
|
|
`--onboard-only` split it back into the two halves below when the two kinds
|
|
of access belong to different people.
|
|
|
|
The rest of this page is what that command does, and what it still does not.
|
|
|
|
## The two halves
|
|
|
|
Two scripts, because the two halves of onboarding are done by different people
|
|
with different access: one needs the hypervisor, the other needs the
|
|
certificate authority.
|
|
|
|
## 1. The machine
|
|
|
|
On the Proxmox host:
|
|
|
|
```
|
|
llmgw-provision-node --vmid 144 --name llmgw-node04 --ip 10.0.0.144/24
|
|
```
|
|
|
|
Name it after the node id. **The VM name, the hostname, the node id and the
|
|
certificate's common name are all the same string.** They do not have to be, and
|
|
every time they are not, somebody spends an afternoon working out which of the
|
|
four a message is talking about.
|
|
|
|
It imports a cloud image and lets cloud-init do the rest, which is why a node
|
|
takes a minute rather than an afternoon. It refuses to touch an existing VMID:
|
|
adopting a machine is not a decision a provisioning script should make.
|
|
|
|
Releases carry static `linux/amd64` and `linux/arm64` binaries. Download one and
|
|
pass it with `--binary` rather than copying whatever happens to be on the
|
|
machine you are running onboarding from — otherwise whoever runs the script
|
|
quietly becomes the build server, and the fleet ends up running whatever was in
|
|
their working tree.
|
|
|
|
## 2. The identity
|
|
|
|
On the host that holds the certificate authority — this is the part that cannot
|
|
be delegated, because it issues the credential by which the control plane
|
|
believes the node is who it says it is:
|
|
|
|
```
|
|
llmgw-onboard-node \
|
|
--node llmgw-node04 \
|
|
--addr 10.0.0.144 \
|
|
--server https://10.0.0.140:7443 \
|
|
--fabric ib-b
|
|
```
|
|
|
|
It issues a client certificate whose common name is the node id, installs it
|
|
along with the CA root, trusts that root system-wide so other tooling on the
|
|
machine stops treating llmgw's certificates as strangers, installs the binary
|
|
and the unit, and starts the agent. The node appears in `llmgw ctl nodes` within
|
|
a few seconds.
|
|
|
|
If it does not, the control plane's log says why. A certificate whose common
|
|
name does not match the claimed node id is refused by design — that check is
|
|
what makes a node's identity something it proves rather than something it picks.
|
|
|
|
**It needs ssh from the CA host to the node.** Provisioning copies that host's
|
|
`authorized_keys` into the new machine, so a node built by the script above
|
|
already satisfies it; an older machine may need the key added.
|
|
|
|
It waits for ssh rather than failing on it, and learns the host key of a machine
|
|
it has not seen (`accept-new`, which still refuses a *changed* key — the risk
|
|
being guarded against is a substituted machine, not a new one). Both are needed
|
|
to run the halves back to back: for the first minute of its life a new machine
|
|
is still regenerating host keys and restarting sshd, so it refuses connections
|
|
it accepted a moment earlier.
|
|
|
|
It also checks that a configured engine actually runs, which is cheap here and
|
|
expensive later — see below.
|
|
|
|
## What onboarding does not do
|
|
|
|
It gives a machine an identity and an agent. It does **not** install inference
|
|
engines or model weights, and a node missing either looks healthy in
|
|
`llmgw ctl nodes` right up until a replica is placed on it and the shard dies
|
|
with "executable file not found".
|
|
|
|
Onboarding does check that an engine you point it at can be executed, and says
|
|
so loudly when it cannot. That check exists because the failure is invisible
|
|
otherwise: `llama-server` on node05 was present, executable, and could not start
|
|
for want of `libgomp1`, and the node reported `ready` in `llmgw ctl nodes` the
|
|
whole time. Being told at onboarding beats finding out when a replica is placed
|
|
and the reason is three logs away.
|
|
|
|
So the node is not finished until:
|
|
|
|
- the engines its catalog profiles name are installed (`vllm`, `sglang`,
|
|
`llama-server`), at the paths the agent configuration gives
|
|
- the weights are on disk, at the `source` each model declares
|
|
|
|
Both are deliberately outside this script. Engines are a platform decision with
|
|
versions and drivers attached, and distributing weights is its own problem —
|
|
hundreds of gigabytes that want a shared filesystem or a cache, not an scp from
|
|
a laptop. llmgw has no answer for it yet; that is a known gap, not an oversight.
|
|
|
|
The `sim` engine needs neither, which is what makes it useful for exercising
|
|
everything else.
|
|
|
|
## Checking an engine after an upgrade
|
|
|
|
Engines rename and remove flags between versions, and when that happens the
|
|
failure arrives minutes into a model load, in the engine's own error message, on
|
|
a node nobody was watching.
|
|
|
|
```
|
|
llmgw ctl engine-check vllm
|
|
llmgw ctl engine-check llamacpp --help-output "$(llama-server --help 2>&1)"
|
|
```
|
|
|
|
It builds the widest command llmgw would ever produce — a multi-node one, since
|
|
a flag that only appears there is exactly the one nobody notices has gone — and
|
|
checks every flag against the engine's own `--help`. No GPU required: a CLI
|
|
prints its help on any machine.
|
|
|
|
Some programs do not give up their whole help for `--help`. vLLM prints a
|
|
summary of section names and expects `--help=all`; checking against the summary
|
|
reports every flag as missing, confidently and wrongly. `engine-check` knows
|
|
that one. If you add an engine, check that what you get back is the full list.
|
|
|
|
vLLM also refuses to do anything at all on a machine with no accelerator,
|
|
including print its help, unless `VLLM_TARGET_DEVICE` says what to assume.
|
|
`engine-check` sets it.
|
|
|
|
Give it the **whole** help. Truncating llama.cpp's 725 lines to 400 reports five
|
|
flags as missing that the running cluster uses; the command warns when the text
|
|
it was given does not document `--help` itself, which is the cheapest sign it
|
|
was cut short.
|
|
|
|
## Fabrics
|
|
|
|
`--fabric` is how a node says which InfiniBand fabric it is attached to, and
|
|
there is no way around stating it. OpenSM's default subnet prefix is `fe80::` on
|
|
every fabric, so a matching prefix proves nothing and a wrong guess produces a
|
|
replica that starts, passes health checks, and then hangs on its first
|
|
all-reduce.
|
|
|
|
Omit it for a node with no fabric. Such a node is its own island: it can host
|
|
single-node replicas and will never be given a multi-node one.
|
|
|
|
Two adapters per fabric is the default; `--fabric ib-b:mlx5_2,mlx5_3` names them
|
|
explicitly. An adapter must never appear under two fabrics — the agent refuses
|
|
that configuration, because the consequence is the hang above.
|
|
|
|
## Where the driver is
|
|
|
|
```
|
|
--nvidia-smi /opt/nvidia/bin/nvidia-smi
|
|
--ib-root /sys/class/infiniband
|
|
```
|
|
|
|
Both default to the usual places. Containerized driver installs routinely put
|
|
`nvidia-smi` somewhere else, and an agent that cannot find it registers with no
|
|
GPUs — which looks exactly like a node that has none.
|
|
|
|
## Removing a node
|
|
|
|
```
|
|
llmgw ctl drain llmgw-node04 # move its replicas off, one at a time
|
|
llmgw ctl nodes # wait until it holds none
|
|
```
|
|
|
|
Then stop the agent. Draining brings each replacement up before stopping the
|
|
original, which is the difference between a rolling move and an outage.
|
|
|
|
The node's certificate stays valid until it expires. There is no revocation list
|
|
— if a machine is decommissioned rather than retired, reissue the authority or
|
|
accept that a certificate naming a node that no longer exists can still connect
|
|
and claim to be it.
|
|
|
|
## Testing without hardware
|
|
|
|
Every part of this works on machines with no GPUs and no InfiniBand. Point
|
|
`--nvidia-smi` at a script that prints the CSV discovery asks for, point
|
|
`--ib-root` at a sysfs-shaped directory, and use `engine: sim` in the catalog.
|
|
|
|
The simulated engine rendezvouses for real: rank 0 binds the dist-init port and
|
|
does not serve until every worker has joined, and workers serve nothing. That
|
|
exercises the multi-node launch sequence, the fabric constraint, and failure and
|
|
rebuild — which is most of what goes wrong — without exercising NCCL, which is
|
|
the rest.
|