Files
llmgw/deploy/bootstrap/add-node.sh
T
Ilker ManapandClaude Opus 5 634f87ba15 Provision nodes with the host CPU model
Proxmox defaults a VM to kvm64, which hides AVX, AVX2, FMA and even SSE4.2
from the guest. llama.cpp then loads libggml-cpu-x64.so — its scalar
baseline — instead of the backend built for the host's instruction set, and
an inference node is not slightly slower but unusable.

Measured on node05, an 8-core VM on a Xeon E5-2680 v4, same weights, same
thread count, the only change being --cpu host:

  Qwen2.5-Coder     kvm64   host
  1.5B q4_K_M        5.80  39.09  tok/s
  3B   q4_K_M        2.90  20.51
  7B   q4_K_M        1.33   9.88

Prompt processing went from 7.6 to 143.8, a factor of nineteen.

This cost an afternoon because nothing looks broken. The node registers, the
engine starts, the replies are correct; every number is just small, and the
conclusion that suggests itself is that CPU inference is hopeless rather
than that the VM cannot multiply efficiently. A 14B model at 1.7 tokens a
second was nearly written off on that basis.

The trade is live migration, which a VM pinned to the host CPU cannot do.
For a node whose job is arithmetic that is the right way round, and --cpu
overrides it where it is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:42:48 +02:00

114 lines
4.5 KiB
Bash
Executable File

#!/bin/sh
# add-node.sh — build a node and put it in the fleet, in one command.
#
# provision-node.sh and onboard-node.sh stay separate because the two halves
# need different access: one the hypervisor, one the certificate authority.
# That is a real boundary and this script does not erase it — it drives both
# where one person holds both, which is the common case and was the case here.
#
# The reason to have it is not keystrokes. The VM name, the hostname, the node
# id and the certificate's common name must all be the same string, and the
# address appears in three places; split across two invocations run minutes
# apart, that agreement is maintained by whoever is typing. Here it is stated
# once.
set -eu
usage() {
cat >&2 <<'USAGE'
usage: add-node.sh --name NODE --ip ADDR/CIDR --server URL [options]
--name NODE node id, hostname and VM name; also the certificate's CN
--ip ADDR/CIDR e.g. 10.0.0.145/24
--server URL control plane, e.g. https://10.0.0.140:7443
--vmid N Proxmox VMID (default: the last octet of --ip)
--cores N (default 4)
--cpu TYPE QEMU CPU model (default host; see provision-node.sh)
--memory MB (default 8192)
--disk GB (default 20)
--fabric NAME InfiniBand fabric; repeatable as NAME:hca1,hca2.
Omit for a node with no fabric.
--llamacpp-bin P path to llama-server on the node
--vllm-bin P path to vllm
--sglang-python P path to the python that has sglang
--binary PATH llmgw binary to install (default: the one on PATH)
--pki DIR certificate authority (default /etc/llmgw/pki)
--ssh-user U (default ilker)
--provision-only build the VM and stop
--onboard-only skip provisioning; the machine already exists
USAGE
exit 2
}
NAME= IP= SERVER= VMID= CORES=4 MEMORY=8192 DISK=20 CPU=
BINARY= PKI=/etc/llmgw/pki SSH_USER=ilker
PROVISION=1 ONBOARD=1
set -- "$@"
PASS_THROUGH=
while [ $# -gt 0 ]; do
case "$1" in
--name) NAME="$2"; shift 2;;
--ip) IP="$2"; shift 2;;
--server) SERVER="$2"; shift 2;;
--vmid) VMID="$2"; shift 2;;
--cores) CORES="$2"; shift 2;;
--cpu) CPU="$2"; shift 2;;
--memory) MEMORY="$2"; shift 2;;
--disk) DISK="$2"; shift 2;;
--binary) BINARY="$2"; shift 2;;
--pki) PKI="$2"; shift 2;;
--ssh-user) SSH_USER="$2"; shift 2;;
--provision-only) ONBOARD=0; shift;;
--onboard-only) PROVISION=0; shift;;
# Everything the onboarding step understands and this one does not need to.
--fabric|--llamacpp-bin|--vllm-bin|--sglang-python|--ib-root|--nvidia-smi|--mgmt-addr|--mgmt-if)
PASS_THROUGH="$PASS_THROUGH $1 $2"; shift 2;;
-h|--help) usage;;
*) echo "unknown option: $1" >&2; usage;;
esac
done
[ -n "$NAME" ] && [ -n "$IP" ] && [ -n "$SERVER" ] || usage
# The address without its prefix, which is what onboarding and ssh want.
ADDR="${IP%%/*}"
# The last octet as the VMID by default: it keeps `qm list` and `ctl nodes` in
# the same order, and one fewer number to choose is one fewer to get wrong.
[ -n "$VMID" ] || VMID="${ADDR##*.}"
here() { dirname "$0"; }
PROVISION_SH="$(here)/provision-node.sh"
ONBOARD_SH="$(here)/onboard-node.sh"
[ -x "$PROVISION_SH" ] || PROVISION_SH="$(command -v llmgw-provision-node || true)"
[ -x "$ONBOARD_SH" ] || ONBOARD_SH="$(command -v llmgw-onboard-node || true)"
if [ "$PROVISION" = 1 ]; then
[ -x "$PROVISION_SH" ] || { echo "cannot find provision-node.sh" >&2; exit 1; }
echo "== 1/2 building $NAME as VM $VMID"
# shellcheck disable=SC2086
"$PROVISION_SH" --vmid "$VMID" --name "$NAME" --ip "$IP" \
--cores "$CORES" --memory "$MEMORY" --disk "$DISK" ${CPU:+--cpu "$CPU"}
fi
if [ "$ONBOARD" = 0 ]; then
echo "$NAME built. Onboard it from the CA host when you are ready."
exit 0
fi
[ -x "$ONBOARD_SH" ] || { echo "cannot find onboard-node.sh" >&2; exit 1; }
echo
echo "== 2/2 onboarding $NAME"
# Onboarding waits for ssh itself, which is what makes running the two halves
# back to back safe: cloud-init is still regenerating host keys and restarting
# sshd for the first minute of a new machine's life.
# shellcheck disable=SC2086
"$ONBOARD_SH" --node "$NAME" --addr "$ADDR" --server "$SERVER" \
--pki "$PKI" --ssh-user "$SSH_USER" \
${BINARY:+--binary "$BINARY"} $PASS_THROUGH
echo
echo "Check it with: llmgw ctl nodes"
echo
echo "A node is not finished until the engines its profiles name are installed"
echo "and the weights are on disk. Onboarding checks that a configured engine"
echo "runs, but it installs neither — see docs/node-onboarding.md."