Ilker ManapandClaude Opus 5 9d8cd5e284
CI / test (push) Successful in 1m28s
Track which replicas are actually being used, and summarise the fleet
Two things a cluster with many nodes and many GPUs needs that were missing,
and they are the same thing seen from two distances.

Idle tracking. The control plane knew what was placed and whether it was
healthy, never whether anyone wanted it — so on a fleet with more models
than cards there was no way to answer the first question anyone asks, which
is what can I stop. The router is the only thing that knows, so it now
reports it: a timestamp written in Acquire, where a request already meets an
endpoint under a lock already held, and collected once a tick as a map. That
shape is the point. Per-request notification would be a firehose carrying
one bit — the newest timestamp wins — and this costs the same on four
replicas as on four hundred.

It travels through an interface in the direction opposite EndpointSink, for
the same reason that one exists: the control plane decides what should run,
the router knows whether anyone wants it, and neither should need to be in
the other's process to say so.

Unknown is kept distinct from idle throughout. A router that has just
restarted knows nothing, and a zero timestamp would be the oldest idle
replica in every comparison — so one restart would nominate an entire fleet
for eviction. Absent from the map, "-" in the table, negative in the JSON,
and IdleFor returns a second value rather than a duration anything could
act on by mistake.

ctl status. A per-node listing is right for four nodes and wrong for two
hundred: how full am I, what is broken, what is holding cards nobody is
using are aggregates, and reading them off a long table is a chore, which
means it gets skipped. Capacity per GPU class with usage bars, free whole
nodes per fabric — the number that decides whether a multi-node replica can
land, since free cards on the wrong fabric are not capacity for a model that
spans nodes — replica and model counts, models that cannot be placed, and
what has gone unasked-for. Derived from the same state the tables show, so
it cannot disagree with them.

Unhealthy GPUs count as neither used nor free, because counting them free is
how a cluster looks like it has room it does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 00:52:34 +02:00
2026-09-16 01:00:03 +02:00

llmgw

A single-binary LLM gateway for clusters with multiple InfiniBand fabrics and heterogeneous GPUs.

It serves LLMs running on your own GPU servers (vLLM / SGLang) behind one OpenAI-compatible endpoint. Design document: llmgw-plan.md.

Status

Releases track the roadmap phases; see CHANGELOG.md.

Version Phase Scope State
v0.1.0 0 Skeleton and sim-engine released
v0.2.0 1 Static router (filter/score/pick, streaming proxy) released
v0.3.0 1.5 Admission control (priority bands) released
v0.4.0 2 Agent: GPU/IB discovery, engine supervision released
v0.5.0 3 Control plane: catalog, placement, reconciler, ctl released
v0.6.0 4 Multi-node placement and fabric awareness planned
v1.0.0 5 Tenancy, prefix affinity, benchmarking planned

As of v0.3.0 the router serves traffic and protects it: a batch workload no longer starves interactive requests. Still missing before v1.0.0: authentication, rate limits and budgets, and the control plane that replaces the static endpoint list.

Building

make build      # bin/llmgw
make test       # unit tests
make race       # with the race detector

Operating a cluster

llmgw ctl nodes                      # capacity, health, what is running where
llmgw ctl models                     # catalog and replica counts
llmgw ctl replicas --model llama     # replicas and their shards
llmgw ctl explain-placement llama    # why a model has not been placed
llmgw ctl drain gpu-a-01             # move work off a node, one replica at a time
llmgw ctl cordon gpu-a-01            # stop new placement, leave what runs

ctl talks to the admin port (:9090 by default), not the client-facing one. Add --json to any command for machine-readable output.

explain-placement is the one worth knowing about: "unschedulable" on its own says nothing about which of six constraints bit, so it prints every candidate considered and the reason it was rejected.

Running the router

# Two fake engines and a router in front of them.
./bin/llmgw sim-engine --listen :8001 --model llama &
./bin/llmgw sim-engine --listen :8002 --model llama &
./bin/llmgw server \
  --endpoint e1=http://127.0.0.1:8001=llama \
  --endpoint e2=http://127.0.0.1:8002=llama

curl -N localhost:8080/v1/chat/completions \
  -H 'content-type: application/json' \
  -H 'x-session-id: demo' \
  -d '{"model":"llama","messages":[{"role":"user","content":"hello"}],"stream":true}'

For anything real use a config file; deploy/examples/server.yaml is commented throughout. Metrics, health and pprof are on the admin port (:9090 by default), deliberately separate from the client-facing one.

Requests carry a priority band (x-llmgw-band). Under load, low bands wait at the gateway while high bands keep flowing, so the cluster degrades from the bottom up instead of everywhere at once. Measured effect: interactive p99 stays within 1.24x of idle while 500 concurrent batch requests run, against 58x with admission off.

Routing profiles select how endpoints are scored: chat favours session affinity, batch favours the shortest queue, long-context favours free KV cache. Pick one per model in the config, or per request with the x-llmgw-profile header.

sim-engine

Simulates the inference-engine behaviours the router cares about, so the router can be developed and tested without GPUs. What it models:

  • continuous batching (bounded number of concurrent sequences)
  • KV cache block accounting, queueing once blocks run out (head-of-line blocking included)
  • automatic prefix caching (chained block hashes plus an LRU) and its effect on time to first token
  • per-token latency growing with batch size
  • dropping a request and freeing its blocks when the client disconnects
  • /metrics using vLLM's metric names
./bin/llmgw sim-engine --listen :8000 --model llama-3.3-70b

curl -N localhost:8000/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"hello"}],"max_tokens":8,"stream":true}'

Tune load and latency behaviour with --max-num-seqs, --num-gpu-blocks, --prefill-tps, --decode-tps, --batch-contention and --prefix-cache-blocks. For chaos testing use --fail-rate, and --seed for deterministic runs.

The engine metric contract

For the router to make routing decisions an engine must expose these three metrics (plan §7.6). sim-engine publishes them under vLLM's real names:

vllm:num_requests_running
vllm:num_requests_waiting
vllm:kv_cache_usage_perc

Adding support for a new engine means satisfying that contract. Metric names drift between versions, so the name mapping is configurable.

Module path

gitea.mynodes.xyz/ilker/llmgw. To change it:

make rename NEW=git.example.com/team/llmgw

Language

Code, comments, tests, commit messages and documentation are in English.

S
Description
No description provided
Readme
26 MiB
0 Stars 2 Watchers 1 Forks
v0.16.3
Latest
2026-11-03 09:56:29 +01:00
Languages
Go 81.1%
JavaScript 15.2%
HTML 2.6%
CSS 0.6%
Python 0.2%
Other 0.2%