Multi-Engine Support#
AIBrix serves models on vLLM, SGLang, TRT-LLM and xLLM in one cluster. Set model.aibrix.ai/engine on the pod so the gateway can read that engine’s metrics under one set of names, then route each request to a ready pod of the requested model.
Key Features#
Run vLLM, SGLang, TRT-LLM and xLLM pods in the same cluster.
Select the engine with the
model.aibrix.ai/enginelabel on the pod template.Map each engine’s metric names onto one internal name used by routing.
Skip a pod, for a policy that needs an engine metric, when that engine does not export it.
System Overview#
The gateway discovers pods by model.aibrix.ai/name and reads model.aibrix.ai/engine from the same pod. It scrapes each pod’s metrics endpoint and translates the names with the mapping in pkg/metrics/metrics.go. Routing then uses those names, or signals the gateway tracks itself. Query-based metrics, described under Supported Metrics, are the ones computed by Prometheus.
To configure a specific engine, apply the following labels on the pod template:
labels:
model.aibrix.ai/name: deepseek-llm-7b-chat
model.aibrix.ai/engine: "sglang"
model.aibrix.ai/metric-port: "8000" # Configure this if Prometheus port is different from default port.
model.aibrix.ai/port: "8000"
An empty model.aibrix.ai/engine label is treated as vllm.
Supported engine label values: vllm, sglang, trtllm, xllm.
How it works#
The AIBrix cache watches pods that carry
model.aibrix.ai/nameand readsmodel.aibrix.ai/enginefrom the same pod. Every metric AIBrix uses internally has one abstract name (for examplenum_requests_waiting) and a per-engine mapping to the name the engine actually exports. The mapping lives in pkg/metrics/metrics.go and is reproduced in the table below.Metrics are scraped from each pod on the port given by
model.aibrix.ai/metric-port, or port8000when the label is absent, at/metrics(/prometheus/metricsfortrtllm). Requests are forwarded tomodel.aibrix.ai/port.Routing policies read either an engine metric by its abstract name or a signal the gateway tracks itself. A policy that scores pods on an engine metric skips a pod whose engine has no mapping for it, and routes the request to a random pod when no pod can be scored.
least-request,slo-least-loadandslo-pack-loaduse load the gateway tracks, so they behave the same on every engine.load-balancealso uses gateway-tracked load and output-token rate. It readskv_cache_usage_percfor KV pressure, and treats a pod with no reading as fully free instead of dropping it.least-kv-cachereadskv_cache_usage_perc, which every supported engine maps.least-latencyandthroughputneed query-based metrics written against vLLM metric names (see Supported Metrics), so use them with vLLM only.least-utilizationreadsengine_utilization, which onlyxllmmaps, so on vLLM, SGLang and TRT-LLM it falls back to a random pod.least-busy-timereadsgpu_busy_time_ratio. No engine maps that metric, so it always falls back to a random pod.
The optional AI Engine Runtime sidecar is a separate mechanism and works with vLLM only today. It
serves the engine’s metrics unchanged on its own port and adds engine-neutral aibrix: copies,
which the metric mapping below does not read. The engine label works without it.
Configuration reference#
Everything is configured with pod labels. Set them on the pod template of the Deployment
(or of the StormService role) rather than on the workload object itself.
Label |
Default |
Meaning |
|---|---|---|
|
required |
Model name used in requests and for pod discovery. |
|
|
Engine type. One of |
|
|
Port the engine serves requests on. |
|
|
Port the engine exposes |
Supported Metrics#
Only a limited set of metrics is mapped for each engine, and more will be added over time. For a routing policy implemented through the routing policy API, use a metric your target engine exports. Most policies fall back to random when they cannot score a pod; see How it works above for the exceptions.
AIBrix metric |
vLLM |
SGLang |
TRT-LLM |
xLLM |
|---|---|---|---|---|
|
|
|
N/A |
N/A |
|
|
|
N/A |
N/A |
|
|
|
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
|
|
N/A |
|
N/A |
|
N/A |
N/A |
|
N/A |
|
N/A |
N/A |
|
|
|
|
N/A |
|
|
N/A |
|
N/A |
|
|
N/A |
N/A |
N/A |
|
N/A |
|
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
|
|
N/A |
|
|
|
|
N/A |
|
|
|
|
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
|
N/A |
|
|
|
N/A |
N/A |
N/A |
|
|
|
|
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
|
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
|
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
|
|
N/A |
N/A |
N/A |
The SGLang entries for gpu_cache_usage_perc and kv_cache_usage_perc map to sglang:token_usage [1]. The xLLM names have no engine prefix: both cache metrics map to kv_cache_utilization, and engine_utilization maps to itself.
The table lists metrics read directly from each engine. AIBrix also defines query-based metrics that Prometheus computes, such as avg_prompt_toks_per_req, avg_generation_toks_per_req, p95_ttft_5m and avg_e2e_latency_pod. Their PromQL is written against vLLM metric names, so they are available for vLLM only. drain_rate_1m is the exception: it is written against SGLang metric names.
TRT-LLM Quickstart#
To use TRT-LLM as the inference engine, set the model.aibrix.ai/engine: trtllm label on your deployment. TRT-LLM must be configured to expose performance metrics by enabling return_perf_metrics: true, enable_iter_perf_stats: true, and enable_iter_req_stats: true in its server config.
Sample configurations are available at:
samples/quickstart/tensorrt/tensor-rt.yaml — standard single-instance deployment
samples/quickstart/tensorrt/tensor-rt-pd.yaml — prefill/decode disaggregated deployment using StormService
Example deployment label configuration for TRT-LLM:
labels:
model.aibrix.ai/name: Qwen3-8B
model.aibrix.ai/engine: trtllm
model.aibrix.ai/port: "8000"
TRT-LLM Limitations#
No queue-depth metrics: TRT-LLM does not expose
num_requests_runningornum_requests_waiting, so anything that reads engine queue depth sees zero for TRT-LLM pods, such as the optional queued-request term ofload-balance.least-requestis not affected: it counts in-flight requests at the gateway.Metrics require explicit config: Performance metrics are only emitted when
return_perf_metrics: true,enable_iter_perf_stats: true, andenable_iter_req_stats: trueare set in the TRT-LLM server configuration.
SGLang Decisions API#
SGLang serves POST /v1/decisions, which answers typed choice, score and yes_no
questions about an input by scoring answer labels, without generating text. The gateway routes
it to the pods of the requested model, like the other model endpoints:
curl http://${GATEWAY}/v1/decisions \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-llm-7b-chat",
"input": "The customer reports a double charge on their last invoice.",
"questions": [{
"id": "team",
"type": "choice",
"question": "Which team should handle this?",
"options": [{"name": "billing"}, {"name": "support"}]
}]
}'
What differs from the other endpoints:
modelis required. SGLang treats it as optional and echoes a default, but the gateway needs it to select a pod, so a request without one is rejected with400.inputis required and may be a string, an object or an array. The gateway uses it, with objects and arrays compacted to JSON text, as the routing text for prefix-cache-aware routing.questionsmust be a non-empty array. The question schema itself is validated by SGLang.There is no streaming and no generation. Usage reports
prompt_tokensandtotal_tokensonly, withcompletion_tokensat0.imagesare forwarded as sent. The gateway buffers the whole request body, so large base64 images count against thebufferLimitof the gateway’sClientTrafficPolicy(4 MiB by default). Raise it, together withAIBRIX_GRPC_MAX_MESSAGE_SIZE_BYTES, if you send big images.
The gateway does not check the engine, so a decisions request for a model served by another engine
is forwarded and fails at the pod. Only route it to model.aibrix.ai/engine: sglang models.
SGLang System One API#
SGLang’s POST /v1/systemone serves the same decisions in the System One request shape: a
state and a map of noul, choice and score questions keyed by your own ids. The
gateway routes it to the pods of the requested model, like /v1/decisions:
curl http://${GATEWAY}/v1/systemone \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-llm-7b-chat",
"state": "The customer reports a double charge on their last invoice.",
"questions": {
"team": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {"billing": null, "support": "Bugs or integration problems"}
},
"urgent": {"type": "noul", "instructions": "The customer needs an answer today."}
}
}'
What differs from /v1/decisions:
modelis required and must be a model the gateway serves. SGLang accepts any name, but the gateway selects a pod with it, so a model that is not deployed is rejected with400model_not_found. A client library that sends a default model name, such as the TypeSafe SDKs’jev-latest, has to be told to send your model’s name instead.stateis required and may be a string, an object or an array. Unlike a decisionsinputit may be empty, because SGLang accepts an empty state. The gateway uses it, with objects and arrays compacted to JSON text, as the routing text for prefix-cache-aware routing: SGLang puts the state at the head of every question’s prompt, so it is the part the questions share. A pod serving a decision checkpoint renders an object state with spaces after the JSON separators, so the routing text then differs from the prompt by whitespace; this only affects the cache-affinity estimate.questionsmust be a non-empty object keyed by your ids. An array, the/v1/decisionsshape, is rejected with400. The question schema itself is validated by SGLang.There is no streaming and no generation. Usage reports
input_tokens, which counts the prompt of every question, so the state is counted once per question, andoutput_tokensat0. There is nototal_tokens: the gateway counts input plus output as the total for rate-limit and request metrics, and forwards the usage block unchanged.imagesare forwarded as sent, with the samebufferLimitcaveat as for/v1/decisions.The TypeSafe SDKs time out after 10 seconds and retry. Each retry reaches the gateway as a separate request, so request counts and token usage include the retries.
The same engine caveat applies: a request for a model served by another engine is forwarded and fails at the pod. SGLang also serves the route only for models with a Jinja chat template; its decision-models documentation lists what it refuses.
SGLang Score API#
SGLang serves POST /v1/score, which scores items against a query without generating text:
label-token probabilities for a causal LM, or class logits for a sequence-classification model.
The gateway routes it to the pods of the requested model, like the other model endpoints:
curl http://${GATEWAY}/v1/score \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3-reranker",
"query": "Is this passage about billing?",
"items": ["The invoice was charged twice.", "The app crashes on start."],
"label_token_ids": [9454, 2753],
"apply_softmax": true
}'
What differs from the other endpoints:
modelis required. SGLang falls back to a default model, but the gateway needs the name to select a pod, so a request without one is rejected with400.itemsis required and cannot be empty. It may be a string, an array of strings or an array of token-ID arrays.queryis optional and takes text or token IDs.Every other field, such as
label_token_ids,apply_softmax,item_firstand the setwisescore_extraction_token, is forwarded as sent and validated by SGLang.The routing text for prefix-cache-aware routing is the
querywhen it is text, otherwise the first item when the items are text. Token-ID input gives no routing text.There is no streaming, and
stream: trueis rejected with400. The response, including nested setwisescoresandpooled_hidden_states, is forwarded unchanged. Usage is counted when SGLang reports it.
The gateway does not check the engine, so a score request for a model served by another engine is
forwarded and fails at the pod. Only route it to model.aibrix.ai/engine: sglang models.
Adding New Engines#
To support a new engine or metric:
Add the engine’s metric names to the mapping in pkg/metrics/metrics.go.
Set
model.aibrix.ai/engineon the deployment’s pod template to that engine name.
For more details, see the cache_metrics.go and metrics.go in: