Load-Balancing Algorithms¶
HTTP/2 selector boundary
The selector descriptions below apply to the HTTP/1.1 fullproxy path. HTTP/2 implements selector-8 CHWBL with a different rule: it can apply the load cap even when a prefix hash is present. Current HTTP/2 forwarding makes selectors 9 and 10 round-robin and does not connect P/D, KV-exact, or model-aware lookup. Validate HTTP/2 parity in the released artifact before enabling it.
The sel field on an LB rule chooses how the gateway picks a backend endpoint. This page covers the full sel enum and drills into the algorithms that matter for LLM serving — CHWBL, GPU-aware, weighted-round-robin-with-hash, and persist-based session affinity.
The sel enum¶
sel is an integer on the LB rule's serviceArguments. It defaults to 0 (round-robin). The full enum:
sel |
Algorithm | AI-relevant | Summary |
|---|---|---|---|
0 |
Round-robin (rr) | ✅ | Even rotation across endpoints. |
1 |
Hash | — | Static tuple hash (L4). |
2 |
Weighted round-robin (wrr) | ✅ | Rotation biased by endpoint weight. |
3 |
Persist | ✅ | Session affinity — pins a session to one endpoint. |
4 |
Least-connections (lc) | — | Fewest active connections wins. |
5 |
N2 | — | Userspace selector for the SCTP N2 interface; requires fullproxy. |
6 |
N3 | — | N3 interface selector; accepted only for UDP services. |
7 |
reserved | — | Not used. |
8 |
CHWBL | ✅ | Consistent hash with bounded loads — prompt/prefix-aware cache locality. |
9 |
GPU-aware (topology-dependent) | ✅ | Plain pool: prefix/session modulo placement. P/D capacity-aware activation is currently release-blocked. |
10 |
WRR-hash | ✅ | Weighted consistent hash — same prefix hashing as CHWBL, distributed by weight. |
AI routing needs fullproxy
All of the AI-aware behaviors below require the rule to run in mode: 4 (fullproxy) so the gateway can inspect HTTP bodies and headers. In any other mode the rule performs L4 load balancing only. See Opt-in AI Routing Model and Running Modes.
Algorithms for LLM serving¶
Each advanced selector below combines its behavior, recommended use, a concrete HTTP/1.1 fullproxy scenario, and checks that an operator can run during validation.
sel: 0 — Round-robin¶
Rotates evenly across all healthy endpoints. It has no awareness of KV cache, GPU load, or session context, so every turn of a multi-turn conversation can land on a different GPU and pay a cold-cache recompute penalty.
When to use for LLM serving: stateless, single-shot completions where every backend is interchangeable and no prompt reuse is expected — for example a batch classification or embedding pool. As soon as conversations or shared system prompts enter the picture, prefer sel: 8 or sel: 3.
sel: 2 — Weighted round-robin¶
Round-robin biased by each endpoint's weight. A backend with weight: 3 receives roughly three times the requests of a weight: 1 peer.
When to use for LLM serving: heterogeneous GPU fleets where you want a fixed traffic split — for example directing more traffic to larger-VRAM nodes — without cache or load feedback. It distributes volume, not cache affinity.
sel: 3 — Persist (session affinity)¶
Pins a logical session to a single backend so every request in that session reaches the GPU that already holds its context. The session key is taken from session_header_name (see Session affinity below). With no session_header_name set, persist falls back to source-IP stickiness.
When to use for LLM serving: conversational or agentic workloads that carry an explicit session identifier (a chat thread ID, an MCP session header, a cookie). It is the most direct way to keep a conversation's KV cache warm on one endpoint.
sel: 8 — CHWBL (consistent hash with bounded loads)¶
On HTTP/1.1, this selector hashes a stable feature of the request onto a consistent-hash ring. When a real prefix hash is available, it preserves strict prefix affinity and skips unhealthy ring owners; it does not apply the bounded-load spill check. When it instead uses a conversation or fallback-derived key, the bounded-load ceiling can move selection away from an overloaded ring owner. Adding or removing a backend remaps only part of the ring.
When to use for LLM serving: conversational and RAG workloads that benefit from strong prefix locality without requiring clients to send a session header. Monitor hot shared prefixes because strict prefix affinity can concentrate them on one endpoint. Tuning fields and their current runtime boundary are covered in CHWBL tuning below.
Example: shared support-agent prompt on equal GPUs¶
Three clients use the same model and support-agent system prompt. GPU-A, GPU-B, and GPU-C have equal capacity, so reusing one GPU's warm KV cache matters more than evenly spreading these three requests.
flowchart LR
C1["Client 1<br/>same model and system prompt"] --> KEY["Same request-derived<br/>prefix hash"]
C2["Client 2<br/>same model and system prompt"] --> KEY
C3["Client 3<br/>same model and system prompt"] --> KEY
KEY --> RING["CHWBL<br/>consistent-hash ring"]
RING -->|Ring owner is healthy| A["GPU-A<br/>warm prefix cache"]
RING -.->|GPU-A unavailable| B["Next healthy<br/>ring owner"]
A --> RESULT["More KV-cache reuse"]
B --> RESULT
style KEY fill:#e1f5fe,stroke:#0288d1
style A fill:#e8f5e9,stroke:#43a047
style B fill:#fff9c4,stroke:#f9a825
style RESULT fill:#e8f5e9,stroke:#43a047
Operator checks:
- Replay the same prefix and use request or access logs to confirm that it reaches the same healthy endpoint.
- Alert on a hot shared prefix that concentrates requests on one GPU.
- Simulate an endpoint failure and verify selection of the next healthy ring owner, or HTTP 503 when none is available.
sel: 9 — GPU-aware¶
The current fullproxy implementation has two distinct laws behind this historical name:
- On a plain single pool, a request with a prefix hash uses
prefix_hash % endpoint_count; otherwise a conversation ID is hashed the same way, followed by a health-aware fallback when neither value is available. Pushed worker metrics are not read by this userspace selector. - On a P/D pool, the intended capacity-aware branch exists, but its current activation gate
checks the mutable endpoint cursor rather than the configured selector. A configured
sel: 9therefore does not reliably activate it. Normal Tier 2 uses active connections plus queued requests.
The separate GPU status and worker-metrics APIs provide observability; they are not evidence of least-loaded selection on the plain path. See vLLM Integration.
P/D capacity-aware activation is currently release-blocked
Configuring sel: 9 does not reliably enable the capacity-aware scorer. Do not claim that
behavior until the activation gate is corrected and the exact release artifact passes an
integration test. On HTTP/2, selector 9 reduces to round-robin.
When to use for LLM serving: use sel: 9 only when you deliberately want plain-pool modulo
placement and have tested how endpoint additions, removals, and failures remap traffic. Prefer
CHWBL for a stable ring with bounded-load protection, round-robin for independent requests, and
the normal routing hierarchy for a P/D pool.
Example: deterministic placement in a plain GPU pool¶
A non-P/D pool has three equal GPUs. Two requests carry the same model and prompt prefix. The
resulting prefix hash maps to endpoint index 1 when divided by the three endpoints.
flowchart LR
REQ1["Request 1<br/>shared prefix"] --> HASH["Prefix hash"]
REQ2["Request 2<br/>same prefix"] --> HASH
HASH --> MOD["hash modulo 3<br/>result: endpoint 1"]
MOD --> B["GPU-B<br/>endpoint index 1"]
B -->|Connect succeeds| RESP["Serve both requests"]
B -.->|Connect fails| FALLBACK["Walk healthy endpoints<br/>using generic failover"]
FALLBACK -->|Another endpoint connects| RESP
FALLBACK -->|No endpoint accepts connection| R502["HTTP 502<br/>backend_unreachable"]
style HASH fill:#e1f5fe,stroke:#0288d1
style MOD fill:#fff3e0,stroke:#f57c00
style B fill:#e8f5e9,stroke:#43a047
style FALLBACK fill:#fff9c4,stroke:#f9a825
style RESP fill:#e1f5fe,stroke:#0288d1
style R502 fill:#ffebee,stroke:#e53935
Operator checks:
- Replay the same prefix and confirm the same endpoint index while endpoint count and order remain unchanged.
- Add or remove a test endpoint and measure the wider remapping caused by modulo placement.
- Force connection failure and distinguish connection-exhaustion
502 backend_unreachablefrom selection-time503 no_healthy_backend.
sel: 10 — WRR-hash (weighted consistent hash)¶
Uses the same prefix-hashing machinery as CHWBL, but distributes the virtual nodes on the ring proportionally to endpoint weight. You get CHWBL's prefix-cache locality and a deliberate, weight-driven traffic split across uneven backends.
On HTTP/1.1, a real prefix hash remains strictly affine to its weighted ring owner, subject to health handling. The bounded-load check applies only when selection uses a conversation or fallback-derived key.
When to use for LLM serving: mixed or transitional fleets where you want cache-aware routing but the endpoints are not equal — for example while draining an old GPU tier, or when blending large- and small-VRAM nodes under one VIP. Selector 10 uses endpoint weights, but submitted chwbl_* tuning values are not currently propagated to the proxy.
Example: weighted cache affinity across different-sized GPUs¶
GPU-A is the largest worker, GPU-B is medium, and GPU-C is smallest. Their endpoint weights are
4, 2, and 1, and many different prompt prefixes arrive over time.
flowchart LR
TRAFFIC["Many different<br/>prompt prefixes"] --> HASH["Compute one hash<br/>per prefix"]
REPEAT["Repeated prefix"] --> HASH
HASH --> RING["Weighted<br/>consistent-hash ring"]
RING --> A["GPU-A<br/>weight 4<br/>largest ring share"]
RING --> B["GPU-B<br/>weight 2<br/>medium ring share"]
RING --> C["GPU-C<br/>weight 1<br/>smallest ring share"]
A --> RESULT["Repeated prefix returns to<br/>the same healthy ring owner"]
B --> RESULT
C --> RESULT
style HASH fill:#e1f5fe,stroke:#0288d1
style RING fill:#e8f5e9,stroke:#43a047
style A fill:#e8f5e9,stroke:#43a047
style B fill:#fff9c4,stroke:#f9a825
style C fill:#fff3e0,stroke:#f57c00
style RESULT fill:#e1f5fe,stroke:#0288d1
Operator checks:
- Test many distinct prefixes; do not expect a small sample to produce an exact
4:2:1request ratio. - Replay one prefix and confirm that it returns to the same healthy ring owner.
- Read back the configured endpoint weights and verify the intended static ownership distribution; weight is not a live GPU-load signal.
CHWBL tuning (sel: 8)¶
The API accepts the CHWBL knobs for sel: 8 and sel: 10, but the current rule-to-data-plane
path does not propagate them. The live proxy uses mean-load factor 175, replication 256,
prefix flags 0, and cache-salt enforcement disabled. Treat the fields below as an intended
configuration contract, not working production tuning, until runtime metrics or a distribution
test proves propagation in the released artifact.
| Field | Type | Default | Range / values | What it controls |
|---|---|---|---|---|
chwbl_prefix_hash_level |
int | 1 |
1, 2, 3 |
Stored/read back; the runtime currently derives prefix scope from request content. |
chwbl_prefix_hash_flags |
int | 0 |
0–255 bitflags |
Stored/read back; the runtime currently programs flags 0. |
chwbl_mean_load_factor |
int | schema 125; runtime 175 |
100–300 |
Intended bounded-load ceiling; currently not propagated to the proxy. |
chwbl_replication |
int | schema 100; runtime 256 |
1–1024 |
Intended vnode count; currently not propagated to the proxy. |
chwbl_enable_cache_salt |
bool | false |
— | Intended salt requirement; currently not propagated, so it must not be used as a tenant-isolation control. |
Do not claim strict tenant isolation from CHWBL salt
Prefix scope is inferred from request content and the runtime salt switch remains off. Separate untrusted tenants into independently authorized pools or listeners. A hash input is an affinity hint, not an authorization boundary.
chwbl_prefix_hash_flags bits¶
These bits describe the API contract. The current proxy receives flags 0 and infers prefix
scope from each request; setting a nonzero value does not currently change the live hash input.
| Bit | Value | Feature |
|---|---|---|
| 0 | 1 |
LoRA adapter |
| 1 | 2 |
Image input |
| 2 | 4 |
Audio input |
| 3 | 8 |
Cache salt |
| 4 | 16 |
Tools / function definitions |
| 5 | 32 |
Session |
| 6 | 64 |
RAG template |
| 7 | 128 |
RAG documents |
For example, the schema defines 192 as bits 6 and 7. This is descriptive API metadata only in
the current build; setting it does not change the live hash input.
Configure CHWBL¶
Protect the management API
The curl examples use plain HTTP for an isolated lab. In production, use an authenticated,
TLS-protected management endpoint and keep authorization values out of command arguments.
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 8080,
"protocol": "tcp",
"mode": 4,
"sel": 8,
"backend_protocol": "http1"
},
"endpoints": [
{"endpointIP": "192.0.2.1", "targetPort": 8080, "weight": 1},
{"endpointIP": "198.51.100.1", "targetPort": 8080, "weight": 1},
{"endpointIP": "203.0.113.1", "targetPort": 8080, "weight": 1}
]
}'
Session affinity (sel: 3)¶
For clients that carry an explicit session identifier, sel: 3 (persist) pins each session to one backend. The session_header_name field tells the gateway where to read the session key from. It supports four extraction methods:
session_header_name value |
Extraction | Example client |
|---|---|---|
X-Session-ID (any header name) |
Full header value | Custom agents, MCP (mcp-session-id) |
cookie:JSESSIONID |
Named cookie from the Cookie: header |
Java / Spring web apps |
query:session_id |
Named parameter from the URL query string | REST clients with URL-encoded sessions |
basic-auth |
Username from an Authorization: Basic header |
Internal per-user services |
If session_header_name is empty and sel: 3 is set, persistence falls back to source-IP stickiness.
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 8080,
"protocol": "tcp",
"mode": 4,
"sel": 3,
"backend_protocol": "http1",
"session_header_name": "X-Session-ID"
},
"endpoints": [
{"endpointIP": "192.0.2.1", "targetPort": 8080, "weight": 1},
{"endpointIP": "198.51.100.1", "targetPort": 8080, "weight": 1},
{"endpointIP": "203.0.113.1", "targetPort": 8080, "weight": 1}
]
}'
Verify¶
List the LB rules and confirm the sel value in effect:
Troubleshoot¶
Uneven distribution across endpoints
- Confirm the intended
selvalue on the rule viaGET /config/loadbalancer/all. - For
sel: 2orsel: 10, check that endpointweightvalues reflect the split you expect. - For HTTP/1.1
sel: 8orsel: 10, a hot shared prefix concentrating on one endpoint is expected because real prefix hashes bypass bounded-load spill. For non-prefix keys, the current proxy uses factor 175 and replication 256; changing the stored REST fields does not tune them.
CHWBL / WRR-hash knobs appear to have no effect
- Verify
selis8or10, then test the observed distribution. Thechwbl_*fields are stored but are not currently propagated to the proxy. - Confirm the rule is in
mode: 4(fullproxy); L4 modes cannot inspect the request body to compute prefix hashes.
Session not sticking under sel: 3
- Verify
session_header_namematches exactly what the client sends (cookie and query names are case-sensitive). - With no
session_header_name, persistence is source-IP based — clients behind a shared NAT will collapse onto one endpoint.
GPU-aware (sel: 9) sending everything to one GPU
- On a plain pool, repeated prefixes are deliberately mapped by modulo and can concentrate traffic. Pushed worker metrics do not change that law. Use CHWBL for bounded prefix affinity or round-robin for independent requests. Do not diagnose P/D capacity scoring from selector 9 until its activation gate is corrected; normal P/D Tier 2 uses active connections plus queued requests. See vLLM Integration.
See also¶
- Opt-in AI Routing Model — how per-rule AI routing is enabled
- Running Modes — why
mode: 4(fullproxy) is required - Routing Hierarchy — the layered prefix → KV → load selection ladder in depth
- LLM Routing — CHWBL and selector-9 topology behavior in the AI Gateway
- Configuration Reference — every
serviceArgumentsfield