Lora Dynamic Loading#
The LoRA Model Adapter support is crucial for improving model density and reducing inference costs in large language models (LLMs). By enabling dynamic loading and unloading of LoRA adapters, it allows multiple models to be deployed more efficiently on a shared infrastructure.
This reduces resource consumption by reusing base models across different tasks while swapping in lightweight LoRA adapters for specific model tuning. The approach optimizes GPU memory usage, decreases cold start times, and enhances scalability, making it ideal for high-density deployment and cost-effective inference in production environments.
High Level Design#
Model Adapter Controller#
We develop a ModelAdapter controller to manage the lora model adapter lifecycle. User can submit a lora Custom Resource and controller will register to the matching pods, configure the service discovery and expose the adapter status.
ModelAdapter Lifecycle and Phase Transitions#
The ModelAdapter goes through several distinct phases during its lifecycle. Understanding these phases helps users monitor and troubleshoot their LoRA deployments:
Phase Transition Flow:
Pending → Scheduled → Running
↓ ↓ ↓
Waiting Pod Adapter loaded,
for pods selected ready for inference
Loading fails on every candidate pod → Failed (retried until one load succeeds)
Phase Details:
Pending: The controller has started reconciliation but no adapter instance is loaded yet. The
Readycondition says why: reasonNoReadyPodsmeans no ready pod matches thepodSelector(withreplicas: 1it also covers pods that became ready too recently to be scheduled); reasonModelAdapterUnavailablemeans candidate pods exist but the adapter is not loaded on any of them yet.Scheduled: Only with
replicas: 1. The controller has selected a pod that matches thepodSelectorand is loading the adapter on it. Pods are validated for readiness and stability before selection. Loading includes:Downloading the adapter from the specified
artifactURLRegistering the adapter with the vLLM engine
Retrying with exponential backoff if loading fails
Without
replicas(load on all matching pods) the adapter moves fromPendingstraight toRunning.Running: The adapter is loaded on at least one pod and the Kubernetes Service and EndpointSlice for service discovery exist. The LoRA model is accessible through the gateway using the adapter name.
readyReplicasout ofdesiredReplicasin the status shows how many candidate pods have the adapter loaded.Failed: Loading failed on every candidate pod. The
Readycondition carries reasonModelAdapterLoadingErrorand the per-pod errors in its message. The controller keeps retrying and the adapter returns toRunningas soon as one load succeeds.
ResourceCreated is not a step of the normal flow. It shows up as the phase only when creating the Service or EndpointSlice fails; the condition of the same name carries the error details, and the controller keeps retrying. Bound is a condition, not a phase: it is True (together with the Scheduled condition) while the adapter is loaded, and turns False with reason ModelAdapterLoadingError when a loading pass fails, while the phase stays Pending or Failed.
Error Handling and Reliability Features:
Retry: A failed load is retried on that pod. The first attempt is immediate. Later attempts wait 10s, 20s, 40s, 80s and 160s, then 5 minutes. The stored retry count stops growing at 6. The controller keeps trying at that cap.
Other pods: In the same reconcile, a pod that failed or is still in backoff is skipped and the next candidate is tried. Pods recorded in the scheduled-pods annotation are tried first. The controller does not wait for a retry cap before moving on.
Pod health: A pod must be Ready, and must have been Ready for at least 5 seconds, before it is selected.
Retriable errors:
connection refused,connection reset,timeout,no route to host,network is unreachable,temporary failure,service unavailableandbad gateway. Other errors, such as authentication or an invalid artifact, fail that pod immediately.
Monitoring Phase Transitions:
kubectl get modeladapter shows the phase, the replica counters and the reason of the Ready condition at a glance. While the base model pod is still starting:
$ kubectl get modeladapter
NAME PHASE DESIRED READY CANDIDATES REASON MODEL PATH AGE
qwen-code-lora Pending NoReadyPods huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora 5s
Once the adapter is loaded:
$ kubectl get modeladapter
NAME PHASE DESIRED READY CANDIDATES REASON MODEL PATH AGE
qwen-code-lora Running 1 1 1 ModelAdapterAvailable huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora 2m
Blank counter cells mean zero. -o wide adds the base model, the pods hosting the adapter and the full Ready condition message:
$ kubectl get modeladapter -o wide
NAME PHASE DESIRED READY CANDIDATES REASON BASE MODEL MODEL PATH AGE INSTANCES MESSAGE
qwen-code-lora Running 1 1 1 ModelAdapterAvailable qwen-coder-1-5b-instruct huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora 2m ["qwen-coder-1-5b-instruct-5587f4c57d-kml6s"] ModelAdapter default/qwen-code-lora is ready
For the full condition history with timestamps and reasons for each state change, use:
kubectl describe modeladapter <adapter-name>
Model Adapter Service Discovery#
We aim to reuse the Kubernetes Service as the abstraction layer for each lora model. Traditionally, a single pod belongs to one service. For LoRA, one pod hosts several adapters, which does not fit that model. To support lora cases in kubernetes native way, we customize the lora endpoints and allow a single pod with different LoRAs belong to multiple services.
vLLM Engine Changes#
High density Lora support can not be done solely in the control plane side, we also write an RFC about improving the Visibility of LoRA metadata, Dynamic Loading and Unloading, Remote Registry Support, Observability to enhance LoRA management for production grade serving. Please check [RFC]: Enhancing LoRA Management for Production Environments in vLLM for more details. Majority of the changes are done in vLLM, we just need to use the latest vLLM image and that would be ready for production.
Examples#
Here’s one example of how to create a lora adapter.
Prerequisites#
You have a base model deployed in the same namespace.
vLLM engine needs to enable VLLM_ALLOW_RUNTIME_LORA_UPDATING feature flag.
You have a lora model hosted on Huggingface or S3 compatible storage.
(Optional) You have a Huggingface token to enable faster model downloads from Hugging Face.
# Create a secret with the Hugging Face token kubectl create secret generic hf-token --from-literal=token=hf_xxxxxxxxxxx
Create base model#
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct # Note: The label value `model.aibrix.ai/name` here must match with the service name.
model.aibrix.ai/port: "8000"
adapter.model.aibrix.ai/enabled: "true" # Required for LoRA adapter support
name: qwen-coder-1-5b-instruct
namespace: default
spec:
# Increase replicas for better adapter reliability and load distribution
# Allows multiple LoRA adapters to be distributed across pods
replicas: 3
selector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
template:
metadata:
labels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
spec:
containers:
- command:
- vllm
- serve
- --host
- "0.0.0.0"
- --port
- "8000"
- --uvicorn-log-level
- warning
- --model
- Qwen/Qwen2.5-Coder-1.5B-Instruct
- --served-model-name
# Note: The `--served-model-name` argument value must also match the Service name and the Deployment label `model.aibrix.ai/name`
- qwen-coder-1-5b-instruct
- --enable-lora
# Allow at most four distinct LoRA adapters in one vLLM batch.
- --max-loras
- "4"
# Keep up to four adapters in the CPU LoRA cache as well.
- --max-cpu-loras
- "4"
image: vllm/vllm-openai:v0.24.0
imagePullPolicy: Always
name: vllm-openai
env:
# REQUIRED: Enable runtime LoRA updating for ModelAdapter support
- name: VLLM_ALLOW_RUNTIME_LORA_UPDATING
value: "True"
# Optional: Increase timeout for better reliability during adapter loading
- name: VLLM_LORA_MODULES_LOADING_TIMEOUT
value: "300"
# Optional: Add HF_TOKEN to enable faster model downloads from Hugging Face
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: token
optional: true
ports:
- containerPort: 8000
protocol: TCP
volumeMounts:
- name: adapter-storage
mountPath: /tmp/aibrix/adapters
resources:
limits:
nvidia.com/gpu: "1"
requests:
nvidia.com/gpu: "1"
# Health checks for better pod reliability
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
# Startup checks for model downloading and spinning up engine
startupProbe:
httpGet:
path: /health
port: 8000
scheme: HTTP
initialDelaySeconds: 120 # Waits 120 seconds before doing the first check
failureThreshold: 120 # Model downloads from Hugging Face is time consuming.
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
- name: aibrix-runtime
image: aibrix/runtime:v0.8.0-rc.1
command:
- aibrix_runtime
- --port
- "8080"
env:
- name: INFERENCE_ENGINE
value: vllm
- name: INFERENCE_ENGINE_ENDPOINT
value: http://localhost:8000
ports:
- containerPort: 8080
protocol: TCP
volumeMounts:
- name: adapter-storage
mountPath: /tmp/aibrix/adapters
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 2
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: adapter-storage
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
labels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
prometheus-discovery: "true"
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
name: qwen-coder-1-5b-instruct # Note: The Service name must match the label value `model.aibrix.ai/name` in the Deployment
namespace: default
spec:
ports:
- name: serve
port: 8000
protocol: TCP
targetPort: 8000
- name: http
port: 8080
protocol: TCP
targetPort: 8080
selector:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
type: ClusterIP
# Expose endpoint
LB_IP=$(kubectl get svc/envoy-aibrix-system-aibrix-eg-903790dc -n envoy-gateway-system -o=jsonpath='{.status.loadBalancer.ingress[0].ip}')
ENDPOINT="${LB_IP}:80"
# send request to base model
curl -v http://${ENDPOINT}/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-coder-1-5b-instruct",
"prompt": "San Francisco is a",
"max_tokens": 128,
"temperature": 0
}'
Create lora model adapter#
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: qwen-code-lora
namespace: default
labels:
model.aibrix.ai/name: "qwen-code-lora"
model.aibrix.ai/port: "8000"
spec:
# Base model that this LoRA adapter extends
baseModel: qwen-coder-1-5b-instruct
# Pod selector to find pods that can host this adapter
# Pods must have both labels: model.aibrix.ai/name and adapter.model.aibrix.ai/enabled=true
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
# URL for the LoRA adapter artifact
# Supported formats: huggingface://, s3://, or absolute local path
artifactURL: huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora
# Replicas controls adapter distribution across pods:
# - nil (omitted): Load adapter on ALL matching pods (recommended)
# - 1: Load adapter on a single pod selected by the scheduler
# Only nil or 1 are supported. Other values will be rejected.
# replicas: 1
# The controller does not read schedulerName. Pod selection uses
# --model-adapter-scheduler-policy on the controller manager
# (default leastAdapters). The CRD still defaults this field to "default".
schedulerName: default
# Lifecycle phases: Pending → Scheduled (replicas: 1 only) → Running, or Failed
# - Pending: No adapter instance loaded yet; the Ready condition reason says why
# - Scheduled: A pod was selected for the adapter (single-pod mode)
# - Running: Adapter loaded on at least one pod, ready for inference requests
# - Failed: Loading failed on every candidate pod; the controller keeps retrying
If you run kubectl describe modeladapter qwen-code-lora, you will see the status of the lora adapter progressing through different phases:
Phase 1: Pending
$ kubectl describe modeladapter qwen-code-lora
Status:
Conditions:
Last Transition Time: 2025-02-16T19:14:50Z
Message: Starting reconciliation
Reason: ModelAdapterPending
Status: Unknown
Type: Initialized
Phase: Pending
Phase 2: Scheduled (only with replicas: 1)
$ kubectl describe modeladapter qwen-code-lora
Status:
Conditions:
Last Transition Time: 2025-02-16T19:14:50Z
Message: Starting reconciliation
Reason: ModelAdapterPending
Status: Unknown
Type: Initialized
Last Transition Time: 2025-02-16T19:14:52Z
Message: ModelAdapter default/qwen-code-lora has selected 1 pods for scheduling: [qwen-coder-1-5b-instruct-5587f4c57d-kml6s]
Reason: Scheduled
Status: True
Type: Scheduled
Phase: Scheduled
Phase 3: Running
$ kubectl describe modeladapter qwen-code-lora
Status:
Candidates: 1
Conditions:
Last Transition Time: 2025-02-16T19:14:50Z
Message: Starting reconciliation
Reason: ModelAdapterPending
Status: Unknown
Type: Initialized
Last Transition Time: 2025-02-16T19:14:52Z
Message: ModelAdapter default/qwen-code-lora has selected 1 pods for scheduling: [qwen-coder-1-5b-instruct-5587f4c57d-kml6s]
Reason: Scheduled
Status: True
Type: Scheduled
Last Transition Time: 2025-02-16T19:14:55Z
Message: ModelAdapter default/qwen-code-lora is ready
Reason: ModelAdapterAvailable
Status: True
Type: Ready
Desired Replicas: 1
Instances:
qwen-coder-1-5b-instruct-5587f4c57d-kml6s
Phase: Running
Ready Replicas: 1
Send request using lora model name to the gateway.
# send request to base model
curl -v http://${ENDPOINT}/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-code-lora",
"prompt": "San Francisco is a",
"max_tokens": 128,
"temperature": 0
}'
Here’s the resources created associated with the lora custom resource.
A new Kubernetes service will be created with the exact same name as ModelAdapter name.
2. The podSelector is used to filter the matching pods. In this case, it will match pods with label model.aibrix.ai/name=qwen-coder-1-5b-instruct. Make sure your base model have this label.
This ensures that the LoRA adapter is correctly associated with the right pods.
Attention
Note: this is only working with vLLM engine. If you use other engine, feel free to open an issue.
Troubleshooting Phase Transitions#
If your ModelAdapter gets stuck in a particular phase, here are common issues and solutions:
Stuck in Pending Phase:
Check if pods matching
podSelectorexist and are runningVerify that pods have the correct labels (
model.aibrix.ai/nameandadapter.model.aibrix.ai/enabled=true)Ensure pods are in Ready state
Stuck in Scheduled Phase:
This was a known issue in earlier versions that has been fixed
Check controller logs for any loading errors:
kubectl logs -n aibrix-system deployment/aibrix-controller-managerVerify vLLM pods have
VLLM_ALLOW_RUNTIME_LORA_UPDATINGenabled
Stuck in Failed Phase (loading errors):
Check if the
artifactURLis accessible and validFor Hugging Face models, ensure the model path exists
Check authentication if using private repositories
Review controller logs for specific loading errors
A retriable error is retried on that pod with the backoff above. The same reconcile also tries the next candidate pod.
Failed to Reach Running Phase:
Check if Kubernetes Service creation succeeded:
kubectl get svc <adapter-name>Verify EndpointSlice creation:
kubectl get endpointslicesCheck pod readiness and health status
Useful Commands for Debugging:
# Check ModelAdapter status
kubectl describe modeladapter <adapter-name>
# Check controller logs
kubectl logs -n aibrix-system deployment/aibrix-controller-manager
# Check pod logs
kubectl logs <pod-name>
# Check services and endpoints
kubectl get svc,endpointslices -l model.aibrix.ai/name=<adapter-name>
Reliability Features#
AIBrix ModelAdapter includes several advanced reliability features to ensure robust LoRA adapter deployments in production environments:
Automatic Retry Mechanism#
The controller retries a failed load on the same pod:
The first attempt runs immediately. There is no wait before it.
After a failure the waits are 10s, 20s, 40s, 80s and 160s, then 5 minutes. The base interval is 5 seconds and the wait doubles, capped at 5 minutes.
The retry count stored on the adapter stops growing at 6 (
MaxLoadingRetries). Loading continues at the capped wait.Retriable errors are the connection and gateway errors listed above. Other errors fail that pod immediately.
# Retry behavior is automatic - no configuration needed
# Controller tracks retry state via annotations:
# - adapter.model.aibrix.ai/retry-count.<pod-name>
# - adapter.model.aibrix.ai/last-retry-time.<pod-name>
Pod Switching and Failover#
When a load fails, or the pod is still waiting out its backoff, the same reconcile tries the next candidate. Pods named in adapter.model.aibrix.ai/scheduled-pods are tried first. The search stops once desiredReplicas pods have loaded the adapter.
Pod choice for replicas: 1 uses one scheduler for the whole controller, set by --model-adapter-scheduler-policy (default leastAdapters). The other policies are random, binPack, leastLatency and leastThroughput. spec.schedulerName is not read. The CRD defaults that field to default, and the samples still set it.
Pod Health Validation#
The controller implements comprehensive pod health checks:
Readiness validation: Pods must be in Ready state before selection
Stability requirements: Pods must be stable (ready for 5+ seconds) to avoid flapping
Continuous monitoring: Unhealthy pods are automatically removed from instances list
Startup tolerance: Handles pod startup delays gracefully
Connection Error Handling#
Robust error handling for various network and startup conditions:
Retriable Errors (will trigger retry):
- "connection refused"
- "connection reset"
- "timeout"
- "no route to host"
- "network is unreachable"
- "temporary failure"
- "service unavailable"
- "bad gateway"
Non-Retriable Errors (will fail immediately):
- Authentication errors
- Invalid model format
- Configuration errors
Advanced Configuration Examples#
High-Availability Setup with Multi-Replica:
Here’s a production-ready configuration demonstrating load balancing across multiple pods:
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: sample-lora-multi-replica
namespace: default
labels:
model.aibrix.ai/name: "sample-lora-multi-replica"
model.aibrix.ai/port: "8000"
spec:
# REQUIRED: Base model that this LoRA adapter extends
baseModel: qwen-coder-1-5b-instruct
# Do not specify the replicas for the adapter
# The adapter will be loaded on all pods
# Provides high availability and load distribution
# replicas: 1
# Pod selector to identify which pods can host this adapter
# Requires pods to have both labels for proper selection
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
# URL for the LoRA adapter artifact
# Using a real Hugging Face model for this example
artifactURL: "huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora"
# The controller does not read schedulerName. With replicas omitted,
# the adapter is loaded on every matching pod, so no scheduler runs.
schedulerName: least-adapters
Testing Commands:
# 1. Apply the multi-replica adapter:
kubectl apply -f samples/adapter/adapter-multi-replica.yaml
# 2. Monitor phase transitions:
watch kubectl describe modeladapter sample-lora-multi-replica
# 3. Check controller logs for retry behavior:
kubectl logs -n aibrix-system deployment/aibrix-controller-manager -f
# 4. Test pod failure scenario (delete a pod to trigger switching):
kubectl delete pod <pod-name>
# Watch how controller handles pod loss and reschedules
# 5. Verify final state:
kubectl get modeladapter sample-lora-multi-replica -o yaml
Monitoring and Observability#
Phase Monitoring:
The reliability features are transparent through the standard ModelAdapter status:
# Monitor real-time phase transitions
watch kubectl describe modeladapter <adapter-name>
# Check for retry annotations (during loading failures)
kubectl get modeladapter <adapter-name> -o yaml | grep -A 5 annotations
Controller Logs:
The controller records retries in its log, not as Kubernetes Events. Useful messages are Selected pods for adapter scheduling, Retriable error loading adapter, Non-retriable error loading adapter on pod, Retry count at cap, retrying on max backoff and Successfully loaded adapter on pod.
kubectl logs -n aibrix-system deployment/aibrix-controller-manager -f | grep -E "loading adapter|Selected pods|Retry count"
Conditions:
The Ready and Scheduled conditions are the record of progress. kubectl describe modeladapter <adapter-name> shows them. The controller does not emit Events for retries.
Best Practices for Production#
Deploy Multiple Base Model Replicas:
# Ensure multiple pods are available for failover spec: replicas: 3 # At least 3 pods for reliability
Use Multiple Adapter Replicas:
# Distribute adapter across all pods spec: # Load adapter on all pods by omitting replicas field # replicas: 1
Configure Appropriate Health Checks:
# In base model deployment livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 3
Monitor Adapter Health:
# Regular health monitoring kubectl get modeladapters -o wide kubectl describe modeladapter <name> # Set up alerts on phase transitions kubectl get events --field-selector involvedObject.kind=ModelAdapter
More configurations#
Multi-LoRA Inference#
vLLM can serve requests for different LoRA adapters in a single batch. SGLang supports a similar feature.
The base model in samples/adapter/base.yaml uses the following arguments to allow up to four distinct adapters per batch:
- --enable-lora
- --max-loras
- "4"
- --max-cpu-loras
- "4"
The vLLM max_loras
setting limits the number of distinct adapters in one batch, not the total number of requests or ModelAdapter
resources. max_cpu_loras controls the CPU LoRA cache and must be greater than or equal to max_loras.
Create one ModelAdapter resource for each LoRA. The adapters must all be compatible with the base model.
For a readily reproducible example, the following manifest assigns four names to the same public LoRA artifact.
Consequently, their generated outputs may be similar. Replace the artifactURL values with different compatible
adapters when testing your own models. Each resource omits spec.replicas, so AIBrix loads every adapter on all
matching base-model pods.
# Create one ModelAdapter per LoRA. All adapters target the same base model
# deployment and can be served together because base.yaml sets --max-loras 4.
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: qwen-code-lora-a
namespace: default
labels:
model.aibrix.ai/name: qwen-code-lora-a
model.aibrix.ai/port: "8000"
spec:
baseModel: qwen-coder-1-5b-instruct
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
artifactURL: huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora
---
# These adapters intentionally reuse the same public artifact so that the
# documentation example works without finding four separate compatible LoRAs.
# Replace each URL with a different compatible adapter for your own workload.
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: qwen-code-lora-b
namespace: default
labels:
model.aibrix.ai/name: qwen-code-lora-b
model.aibrix.ai/port: "8000"
spec:
baseModel: qwen-coder-1-5b-instruct
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
artifactURL: huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora
---
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: qwen-code-lora-c
namespace: default
labels:
model.aibrix.ai/name: qwen-code-lora-c
model.aibrix.ai/port: "8000"
spec:
baseModel: qwen-coder-1-5b-instruct
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
artifactURL: huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora
---
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: qwen-code-lora-d
namespace: default
labels:
model.aibrix.ai/name: qwen-code-lora-d
model.aibrix.ai/port: "8000"
spec:
baseModel: qwen-coder-1-5b-instruct
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
artifactURL: huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora
From the AIBrix repository root, create or update the base model. If you completed the single-LoRA example above, delete its adapter first so that all four cache slots are available. Then create the four adapters and wait until they are ready:
kubectl apply -f samples/adapter/base.yaml
kubectl rollout status deployment/qwen-coder-1-5b-instruct --timeout=10m
kubectl delete modeladapter qwen-code-lora --ignore-not-found
kubectl apply -f samples/adapter/adapter-multi-lora.yaml
kubectl wait --for=jsonpath='{.status.phase}'=Running \
modeladapter/qwen-code-lora-a \
modeladapter/qwen-code-lora-b \
modeladapter/qwen-code-lora-c \
modeladapter/qwen-code-lora-d \
--timeout=10m
In one terminal, forward a local port to the AIBrix gateway and leave the command running:
ENVOY_SERVICE=$(kubectl get service -n envoy-gateway-system \
--selector=gateway.envoyproxy.io/owning-gateway-namespace=aibrix-system,gateway.envoyproxy.io/owning-gateway-name=aibrix-eg \
-o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n envoy-gateway-system \
"service/${ENVOY_SERVICE}" 8080:80
In a second terminal, send one request to each adapter concurrently. The response from each adapter is saved and then printed after all four requests finish:
ENDPOINT=localhost:8080
RESULT_DIR=$(mktemp -d)
for MODEL in qwen-code-lora-a qwen-code-lora-b \
qwen-code-lora-c qwen-code-lora-d; do
curl --fail-with-body --silent --show-error \
"http://${ENDPOINT}/v1/completions" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${MODEL}\",
\"prompt\": \"Implement merge sort in Python.\",
\"max_tokens\": 256,
\"temperature\": 0
}" > "${RESULT_DIR}/${MODEL}.json" &
done
wait
for MODEL in qwen-code-lora-a qwen-code-lora-b \
qwen-code-lora-c qwen-code-lora-d; do
echo "=== ${MODEL} ==="
cat "${RESULT_DIR}/${MODEL}.json"
echo
done
Model Registry#
Currently, we support Huggingface model registry, S3 compatible storage and local file system.
If your model is hosted on Huggingface, you can use the
artifactURLwithhuggingface://prefix to specify the model url. vLLM will download the model from Huggingface and load it into the pod in runtime.If you put your model in S3 compatible storage, you have to use AIBrix AI Runtime at the same time. You can use the
artifactURLwiths3://prefix to specify the model url. AIBrix AI Runtime will download the model from S3 on the pod and load it withlocal model pathin vLLM.If you use shared storage like NFS, you can use the
artifactURLwith a/absolute path to specify the model url (/models/yard1/llama-2-7b-sql-lora-testas an example). You must mount that path into the pod.
Model api-key Authentication#
User may pass in the argument --api-key or environment variable VLLM_API_KEY to enable the server to check for API key in the header.
vllm serve --api-key sk-kFJ12nKsFakefVmGpj3QzX65s4RbN2xJqWzPYCjYu7wT3BFake
We already have an example and you can kubectl apply -f samples/adapter/base-api-key.yaml.
In that case, lora model adapter can not query the vLLM server correctly, showing {"error":"Unauthorized"} error. You need to update additionalConfig field to pass in the API key.
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: qwen-code-lora-with-key
namespace: default
labels:
model.aibrix.ai/name: "qwen-code-lora-with-key"
model.aibrix.ai/port: "8000"
spec:
baseModel: qwen-coder-1-5b-instruct
podSelector:
matchLabels:
model.aibrix.ai/name: qwen-coder-1-5b-instruct
adapter.model.aibrix.ai/enabled: "true"
artifactURL: huggingface://ai-blond/Qwen-Qwen2.5-Coder-1.5B-Instruct-lora
additionalConfig:
api-key: sk-kFJ12nKsFakefVmGpj3QzX65s4RbN2xJqWzPYCjYu7wT3BFake
schedulerName: default
You need to send the request with --header 'Authorization: Bearer your-api-key'
# send request to base model
curl -v http://${ENDPOINT}/v1/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-kFJ12nKsFakefVmGpj3QzX65s4RbN2xJqWzPYCjYu7wT3BFake" \
-d '{
"model": "qwen-code-lora-with-key",
"prompt": "San Francisco is a",
"max_tokens": 128,
"temperature": 0
}'
Runtime Support Sidecar#
A standard install starts the controller manager with --enable-runtime-sidecar. With that flag, the ModelAdapter controller calls the runtime on port 8080 when the pod has an aibrix-runtime container, and calls the engine on port 8000 otherwise. The runtime then registers the adapter with the engine. See AI Engine Runtime.
The flag’s default in the binary is off. The install manifest turns it on. To load adapters by calling vLLM directly, remove --enable-runtime-sidecar from the controller manager:
kubectl edit deployment aibrix-controller-manager -n aibrix-system
args:
- --leader-elect
- --health-probe-bind-address=:8081
- --metrics-bind-address=:8080
- --enable-runtime-sidecar # remove this line
Private Artifact Storage Support#
AIBrix supports loading LoRA adapters from private cloud storage backends including AWS S3, Google Cloud Storage (GCS), and Volcano Engine TOS. This feature requires the AIBrix runtime sidecar to handle artifact delegation - downloading artifacts from private storage and forwarding them to the inference engine.
Supported Storage Backends:
AWS S3:
s3://bucket/path/to/adapterGoogle Cloud Storage:
gs://bucket/path/to/adapterVolcano Engine TOS:
tos://bucket/path/to/adapterPrivate HuggingFace:
huggingface://org/private-model(with token)HTTP(S) with Authentication: Any URL with custom headers
How It Works:
Without Runtime (Direct Mode):
Controller → Inference Engine (port 8000)
✓ Public HTTP(S), public HuggingFace
✗ Private S3, GCS (not supported)
With Runtime (Artifact Delegation):
Controller → Runtime (port 8080) → Downloads Artifact → Engine
✓ S3, GCS, private HuggingFace, all storage types
The runtime sidecar:
Receives the artifact URL and credentials from the controller
Downloads the artifact from private storage to local filesystem
Forwards the local path to the inference engine
Manages artifact lifecycle (cleanup on unload)
Prerequisites:
AIBrix runtime sidecar deployed alongside your inference engine
Controller flag
--enable-runtime-sidecar=trueenabledKubernetes secrets configured with storage credentials
Shared volume between runtime and engine containers
Note
When the AIBrix sidecar injection webhook is enabled, AIBrix injects a shared volume named
adapter-storage mounted at /tmp/aibrix/adapters into both the runtime and engine containers.
The runtime downloads LoRA adapter artifacts into this directory by default. See
DefaultAdapterVolumeName and DefaultAdapterMountPath in pkg/webhook/sidecar_injection.go.
Setup: AWS S3 Storage#
Step 1: Create S3 Credentials Secret
apiVersion: v1
kind: Secret
metadata:
name: s3-credentials
namespace: default
type: Opaque
stringData:
aws_access_key_id: "AKIAIOSFODNN7EXAMPLE"
aws_secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
aws_region: "us-west-2"
Step 2: Deploy Pod with Runtime Sidecar
apiVersion: v1
kind: Pod
metadata:
name: vllm-llama3
labels:
model.aibrix.ai/name: llama-3
adapter.model.aibrix.ai/enabled: "true"
spec:
containers:
# Inference engine
- name: vllm
image: vllm/vllm-openai:latest
ports:
- containerPort: 8000
env:
- name: VLLM_ALLOW_RUNTIME_LORA_UPDATING
value: "True"
volumeMounts:
- name: adapter-storage
mountPath: /tmp/aibrix/adapters
# AIBrix runtime sidecar
- name: aibrix-runtime
image: aibrix/runtime:latest
ports:
- containerPort: 8080
env:
- name: INFERENCE_ENGINE
value: "vllm"
- name: INFERENCE_ENGINE_ENDPOINT
value: "http://localhost:8000"
volumeMounts:
- name: adapter-storage
mountPath: /tmp/aibrix/adapters
- name: credentials
mountPath: /var/run/secrets/aibrix
readOnly: true
volumes:
- name: adapter-storage
emptyDir: {}
- name: credentials
secret:
secretName: s3-credentials
Step 3: Create ModelAdapter with S3 URL
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: sql-lora-s3
spec:
baseModel: llama-3
podSelector:
matchLabels:
model.aibrix.ai/name: llama-3
# S3 artifact URL
artifactURL: s3://my-company-bucket/lora-adapters/sql-adapter/
# Reference to S3 credentials
credentialsSecretRef:
name: s3-credentials
replicas: 1
Step 4: Verify Artifact Delegation
# Check ModelAdapter status
kubectl describe modeladapter sql-lora-s3
# Should show: Phase: Running
# Controller logs will show:
# "Using runtime path for artifact delegation"
# Runtime logs will show download progress:
kubectl logs <pod-name> -c aibrix-runtime
# "Downloading artifact from s3://... to /tmp/aibrix/adapters/sql-lora-s3"
# "Successfully downloaded artifact for sql-lora-s3"
# "Forwarding load request to engine with local path"
Setup: Google Cloud Storage (GCS)#
Step 1: Create GCS Credentials Secret
apiVersion: v1
kind: Secret
metadata:
name: gcs-credentials
type: Opaque
stringData:
gcp_service_account_json: |
{
"type": "service_account",
"project_id": "my-project",
"private_key_id": "key-id",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "service-account@my-project.iam.gserviceaccount.com",
"client_id": "123456789",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
}
Step 2: Create ModelAdapter with GCS URL
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: code-lora-gcs
spec:
baseModel: llama-3
podSelector:
matchLabels:
model.aibrix.ai/name: llama-3
# GCS artifact URL
artifactURL: gs://my-gcs-bucket/lora-adapters/code-adapter/
credentialsSecretRef:
name: gcs-credentials
replicas: 1
Setup: Private HuggingFace Models#
Step 1: Create HuggingFace Token Secret
apiVersion: v1
kind: Secret
metadata:
name: huggingface-credentials
type: Opaque
stringData:
huggingface_token: "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Step 2: Create ModelAdapter with Private HuggingFace URL
apiVersion: model.aibrix.ai/v1alpha1
kind: ModelAdapter
metadata:
name: private-hf-lora
spec:
baseModel: llama-3
podSelector:
matchLabels:
model.aibrix.ai/name: llama-3
# Private HuggingFace model
artifactURL: huggingface://my-org/private-lora-adapter
credentialsSecretRef:
name: huggingface-credentials
replicas: 1
Troubleshooting Artifact Delegation#
Runtime Not Receiving Requests:
# Check controller is configured for runtime
kubectl get deployment aibrix-controller-manager -n aibrix-system -o yaml | grep enable-runtime-sidecar
# Should show: - --enable-runtime-sidecar=true
# Check runtime pod is running
kubectl get pods -l app=vllm
kubectl logs <pod-name> -c aibrix-runtime
Download Failures:
# Check runtime logs for specific errors
kubectl logs <pod-name> -c aibrix-runtime
# Common issues:
# - "Access denied": Check credentials in secret
# - "Not found": Verify artifact URL is correct
# - "Invalid credentials": Verify secret format matches expected keys
Credential Issues:
# Verify secret exists and has correct keys
kubectl get secret s3-credentials -o yaml
# For S3, required keys:
# - aws_access_key_id
# - aws_secret_access_key
# - aws_region (optional)
# For GCS, required keys:
# - gcp_service_account_json
Engine Connection Errors:
# Verify runtime can reach engine
kubectl exec <pod-name> -c aibrix-runtime -- curl http://localhost:8000/health
# Check shared volume is mounted correctly
kubectl describe pod <pod-name>
# Both containers should have /tmp/aibrix/adapters mounted
Supported URL Schemes Summary:
URL Scheme |
Example |
Requires Runtime |
Requires Credentials |
|---|---|---|---|
|
|
Yes |
Yes |
|
|
Yes |
Yes |
|
|
Yes |
Yes (token) |
|
|
No |
No |
|
|
No |
Optional |
Best Practices:
Use Separate Secrets per Storage Type: Create dedicated secrets for S3, GCS credentials rather than mixing them
Test Credentials First: Verify credentials work by manually downloading from storage before creating ModelAdapter
Monitor Runtime Logs: Watch runtime logs during first deployment to catch credential or connectivity issues early
Shared Storage Volume: Ensure the
adapter-storagevolume is properly shared between runtime and engine containersArtifact Size: Large artifacts may take time to download; adjust timeout values if needed
Runtime Metrics#
The runtime’s /metrics endpoint, the aibrix: metric names, and METRICS_ENABLE_TRANSFORMATION / METRICS_RAW_PASSTHROUGH_MODE are documented in AI Engine Runtime. The runtime process only starts with INFERENCE_ENGINE=vllm. Rule sets for SGLang and TRT-LLM exist in the code and are not selectable today.