Files
llmgw/deploy/examples/agent.yaml
T
Ilker ManapandClaude Opus 5.5 20deb22dfa Let a node describe itself instead of being described by hand
Every agent file on the cluster named its own node, its own address, the
path to llama-server and two weight directories — different on every node,
kept true by hand, and not true: node05 carried "ram: 32g" beside a measured
31 GB, looked for weights in an empty directory while fetching them into
another, and the other four never looked in theirs at all, so four nodes
reported no disk. Growing a node's RAM would have gone unnoticed, because
memory was read once, at registration.

The agent now finds these out. Its id is the hostname, which is also the
certificate's common name. Its address is the one on the route to the
control plane, which the kernel picks for a UDP dial without sending
anything. llama-server is looked for on PATH and then where release archives
are unpacked. A missing nvidia-smi is a node without NVIDIA cards rather
than a failed discovery — which also cut discovery short before InfiniBand
on every CPU node not told to skip GPUs. Fetched weights land in
/var/lib/llmgw-weights by default, and wherever they land is always looked
in.

Memory is read on every heartbeat, and installed engines are probed again
each minute and sent when they change, so a VM given more RAM or a node
given vLLM becomes a candidate without anyone restarting its agent. The
control plane logs both changes, since "the node now qualifies" is the
answer to why a model placed an hour after it was asked for.

What is written still wins. The agent file shrinks to the control plane's
addresses and the TLS files, the same on every node; onboarding writes only
the overrides it was given. What cannot be found stays configuration: which
fabric an adapter is on, because OpenSM's default prefix is the same on all
of them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 10:13:42 +02:00

106 lines
4.3 KiB
YAML

# llmgw agent configuration, one per GPU node.
#
# The agent dials out to the control plane and keeps one stream open. Engines
# keep serving if the agent restarts: the router talks to them directly, and
# orphan adoption picks them up again on the next start.
# Everything a node can find out about itself, it does: its id is its hostname,
# its address is the one on the route to the control plane, llama-server is
# looked for on PATH and in /opt/llama.cpp, GPUs are there when nvidia-smi is,
# weights are fetched into /var/lib/llmgw-weights and looked for there, and
# memory, disk and installed engines are re-read while the agent runs. So a
# plain CPU node needs only the two blocks below, and the same file on every
# node. What follows them are overrides, for when the defaults are wrong.
server: "http://llmgw-server.internal:7443"
# The agent's half of mutual TLS. The certificate's common name must be this
# node's id — its hostname, unless node_id says otherwise — because the control
# plane compares the two and refuses the stream when they disagree:
#
# llmgw ctl cert issue-client gpu-a-01 --dir /etc/llmgw/pki
#
# With this set, `server` above must be an https:// address — an agent holding
# certificates and dialling http:// is refused at startup rather than connecting
# in the clear without noticing.
#
# tls:
# ca: /etc/llmgw/ca.pem
# cert: /etc/llmgw/agent.pem
# key: /etc/llmgw/agent-key.pem
# ---- overrides ---------------------------------------------------------------
# node_id: gpu-a-01 # when it is not the hostname
# The address engines are reachable on, and the address NCCL bootstraps over.
# Never a fabric address: NCCL rendezvouses over TCP before moving to RDMA.
# mgmt_addr: "10.0.0.11"
# mgmt_interface: eth0
# Free-form, for a profile's node_selector. Nothing needs them; do not restate
# what is measured (memory, GPUs, engines), because a label goes stale.
# labels:
# rack: r12
heartbeat_interval: 5s
reconnect_base: 1s
reconnect_max: 30s
# Fabric labelling is explicit and there is no way around 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 (plan §5.2).
#
# A node with no fabrics is its own single-node island: it can host single-node
# replicas and will never be given a multi-node one.
fabrics:
- name: ib-a
hcas: [mlx5_0, mlx5_1, mlx5_2, mlx5_3]
rate_gbps: 200
discovery:
# Worth setting where the driver lives outside the default paths, which
# containerized driver installs often do.
nvidia_smi: ""
ib_root: /sys/class/infiniband
# NVML is the only source of NVLink topology, which is what keeps a
# tensor-parallel group off the PCIe path inside a node. It needs a binary
# built with `-tags nvml`; otherwise this silently falls back to nvidia-smi.
prefer_nvml: true
# Where this node keeps model weights besides /var/lib/llmgw-weights, which is
# where fetched weights land and is always looked in. Storage differs at
# every installation and llmgw deliberately does not own it: a Lustre mount, an
# image exported over the fabric with SRP or NVMe-oF, local NVMe, or a copy
# made by hand all look identical from here.
#
# What it buys is placement preferring a node that already holds the model over
# one that must first move tens of gigabytes. That matters most where fabrics
# are isolated from one another, because weights in one fabric are not weights
# in another and the only path between them is the management network.
#
# model_dirs:
# - /var/lib/llmgw-models
# - /mnt/lustre/models
engines:
vllm:
bin: /opt/venvs/vllm/bin/vllm
# Which multi-node mechanism works depends on the vLLM version, and the
# flags have changed more than once. Check the version you deploy.
launch_mode: ray # "ray" or "mp"
ray_bin: /opt/venvs/vllm/bin/ray
sglang:
# SGLang is launched as a module, which is its documented entry point.
python: /opt/venvs/sglang/bin/python
supervisor:
work_dir: /var/lib/llmgw-agent
# Engine output is the only diagnosis when a model fails to load, so keep a
# useful tail of it.
log_lines: 512
# Engines need time to free GPU memory cleanly before being killed.
stop_timeout: 60s
health_interval: 2s