A KServe-based model server that serves vLLM-compatible transformer models with an OpenAI-compatible API.
- Python 3.12+
- CUDA-capable GPU
- uv (recommended) or pip
uv syncpython -m vllmserver --model_dir ./hf-models/Vishva007/Qwen3.5-0.8B-W4A16-AutoRound-AWQLoad a model directly from the Hugging Face Hub:
python -m vllmserver --model_id Qwen/Qwen2-1.5B-InstructHot-reload mode lets the server swap models on demand without restarting. When a request arrives for a model that isn't loaded, the server unloads the current model, loads the requested one, and then serves the request. Only one model is resident in GPU memory at a time.
Activate it with --hot-reload-config pointing to a YAML file that declares all available models:
# config.yaml
models:
- name: model1
model_dir: ./hf-models/Vishva007/Qwen3.5-0.8B-W4A16-AutoRound-AWQ
default: true
vllm_args:
reasoning_parser: qwen3
enable_auto_tool_choice: true
tool_call_parser: qwen3_coder
- name: model2
model_dir: ./hf-models/some-other-modelStart the server:
python -m vllmserver --hot-reload-config config.yamlAny vLLM engine flag still applies:
python -m vllmserver --hot-reload-config config.yaml --dtype bfloat16 --enforce-eagerBehavior:
- The server boots on the
default: truemodel. - Sending
{"model": "model2", ...}to any endpoint triggers a swap tomodel2. Subsequent requests formodel2are served immediately. - If a model fails to load, the server falls back to the default model automatically.
- The config file is re-read every 60 seconds. Newly added models become available for routing without a restart; removals are not applied until the next server start.
- All model directories are validated at startup — a missing path exits immediately.
Use POST /swap to trigger a model swap in the background. The call returns immediately — poll GET /swap/status to know when the model is ready.
# Trigger swap (returns immediately)
curl -X POST http://localhost:8080/swap \
-H "Content-Type: application/json" \
-d '{"model": "model2"}'
# 200 → {"status": "swapping", "model": "model2"}
# 404 → model name not in the registry
# 409 → a swap is already in progress
# Poll until ready
curl http://localhost:8080/swap/status
# while swapping → {"model": "model1", "ready": true, "swapping": true, "swapping_to": "model2", "error": null}
# when done → {"model": "model2", "ready": true, "swapping": false, "swapping_to": null, "error": null}
# on failure → {"model": "model1", "ready": true, "swapping": false, "swapping_to": null, "error": "..."}Recommended workflow when switching models:
POST /swap→ 200 (swap starts in background)- Poll
GET /swap/statusuntil"swapping": false - Check
"model"matches the target and"error"is null - Update your load balancer / routing rules
- Inference requests are served immediately with no swap delay
Each model entry in the config can include a vllm_args map to override vLLM engine options on a per-model basis. Keys use Python attribute names (underscores, not dashes):
models:
- name: qwen3-27b
model_dir: ./hf-models/Qwen3.5-27B-AWQ
default: true
vllm_args:
reasoning_parser: qwen3
enable_auto_tool_choice: true
tool_call_parser: qwen3_coder
- name: bge-m3
model_dir: ./hf-models/bge-m3
# no vllm_args — uses CLI defaultsAny option not listed in vllm_args falls back to the value provided on the command line. CLI flags such as --dtype bfloat16 still apply globally unless explicitly overridden per model. Settings never leak between models: each swap resets to the CLI baseline first, then applies the model's own overrides.
| Argument | Default | Description |
|---|---|---|
--model_dir |
/mnt/models |
Path to a local model directory |
--model_id |
— | Hugging Face model ID (overrides --model_dir) |
--model_revision |
— | Hugging Face model revision |
--max_model_len |
— | Maximum number of tokens the model can process |
--trust_remote_code |
false |
Allow loading models with custom code |
--disable_log_requests |
false |
Disable per-request logging |
--hot-reload-config |
— | Path to a YAML config for on-demand model hot-reload |
All vLLM engine arguments (e.g. --dtype, --tensor-parallel-size, --enable-auto-tool-choice) are also accepted.
The server listens on port 8080 and exposes an OpenAI-compatible API under /openai/v1.
| Endpoint | Description |
|---|---|
GET /v2/health/ready |
Readiness health check |
POST /openai/v1/chat/completions |
Chat completions (streaming supported) |
POST /openai/v1/completions |
Text completions (streaming supported) |
POST /openai/v1/embeddings |
Embeddings (requires an embedding model) |
POST /openai/v1/rerank |
Reranking (requires a reranking model) |
POST /swap |
Trigger a background model swap (hot-reload mode only) |
GET /swap/status |
Swap progress and currently loaded model (hot-reload mode only) |
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/openai/v1", api_key="placeholder")
response = client.chat.completions.create(
model="Qwen3.5-0.8B-W4A16-AutoRound-AWQ",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)The model field must match the model name reported by the server. By default this is the directory or Hub ID basename; override it with --served-model-name.
The container image is built and uploaded to ghcr.io automatically whenever a tag is pushed to the repository. You can build it locally with:
docker build -t localhost/vllmserver:latest --build-arg BUILD_VERSION=0.0.0-local .The image takes ~30 GB of disk space after the build is complete.
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
gabrielmscampos/vllmserver:latest 29ab31479f3e 30.2GB 8.13GB
You can override the image's entrypoint to inspect the image and debug its contents:
docker run --rm -it --device nvidia.com/gpu=all --entrypoint bash docker.io/gabrielmscampos/vllmserver:latestFrom within the container, you can start the server as normal:
python -m vllmserver --model_id Qwen/Qwen3.5-0.8BSimply specify the arguments directly after the image identifier:
docker run --rm -it --device nvidia.com/gpu=all -p 8080:8080 -v ./hf-models:/mnt/hf-models docker.io/gabrielmscampos/vllmserver:latest --model_dir /mnt/hf-models/Qwen/Qwen3.5-0.8BThe model above was loaded from a local directory, you can download it with:
hf download Qwen/Qwen3.5-0.8B --local-dir ./hf-models/Qwen/Qwen3.5-0.8BCreate an InferenceService that runs the vllmserver container. The example below deploys a Qwen3.5-0.8B model with reasoning and tool-calling enabled:
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: vllmserver
annotations:
sidecar.istio.io/inject: "false"
spec:
predictor:
containers:
- name: kserve-container
image: ghcr.io/gabrielmscampos/vllmserver:latest
args:
- --model_name=qwen3.5-0.8b
- --model_id=Qwen/Qwen3.5-0.8B
- --language-model-only
- --enable-auto-tool-choice
- --reasoning-parser=qwen3
- --tool-call-parser=qwen3_coder
resources:
limits:
cpu: "10"
memory: 8Gi
nvidia.com/gpu: "1"
requests:
cpu: "1"
memory: 4Gi
nvidia.com/gpu: "1"
ports:
- containerPort: 8080
protocol: TCPCreate an InferenceService that runs the vllmserver container, but reads multiple models from a config file.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: vllmserver
annotations:
sidecar.istio.io/inject: "false"
spec:
predictor:
containers:
- name: kserve-container
image: ghcr.io/gabrielmscampos/vllmserver:v0.1.0
args:
- --hot-reload-config=/mnt/config.yaml
resources:
requests:
cpu: "1"
memory: 4Gi
nvidia.com/gpu: "1"
limits:
cpu: "10"
memory: 8Gi
nvidia.com/gpu: "1"
ports:
- containerPort: 8080
protocol: TCPThe config file may look like:
models:
- name: qwen3.5-0.8b
model_dir: /mnt/models/Qwen/Qwen3.5-0.8B
default: true
vllm_args:
language_model_only: true
enable_auto_tool_choice: true
reasoning_parser: qwen3
tool_call_parser: qwen3_coder
- name: qwen3.6-27b-fp8
model_dir: /mnt/models/Qwen/Qwen3.6-27B-FP8
vllm_args:
language_model_only: true
enable_auto_tool_choice: true
reasoning_parser: qwen3
tool_call_parser: qwen3_coder
- name: qwen3.6-35b-a3b-fp8
model_dir: /mnt/models/Qwen/Qwen3.6-35B-A3B-FP8
vllm_args:
language_model_only: true
enable_auto_tool_choice: true
reasoning_parser: qwen3
tool_call_parser: qwen3_coder
- name: glm-4.7-flash-awq-4bit
model_dir: /mnt/cyankiwi/GLM-4.7-Flash-AWQ-4bit
vllm_args:
enable_auto_tool_choice: true
tool_call_parser: glm47
reasoning_parser: glm45
speculative_config_method:
method: mtp
num_speculative_tokens: 1The /mnt directory can be a persistent volume defined in another resource, where models can be pre-downloaded and the config file updated on-demand.
You may consider deploying the vllmserver in RawDeployment mode to completely skip KServe's serverless features. It might be a good option in heavy constrained environments. You just need to patch the InferenceService with the following annotation:
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: vllmserver
annotations:
sidecar.istio.io/inject: "false"
serving.kserve.io/deploymentMode: RawDeployment
spec:
predictor:
minReplicas: 1 # Ensures the pod never scales to zeroNote that bootstraping and deleting the InferenceService will be faster than usual.
You may want to patch the InferenceService progress-deadline and add a startupProbe to avoid havin the InferecenService's pod killed due to a slow startup:
spec:
predictor:
annotations:
# 1. Increase the Knative deployment timeout (default is 10m/600s)
serving.knative.dev/progress-deadline: "45m"
containers:
- name: kserve-container
image: ghcr.io/gabrielmscampos/vllmserver:v0.1.0
startupProbe:
tcpSocket:
port: 8080
failureThreshold: 240 # 240 checks * 10 seconds = up to 40 minutes to start
periodSeconds: 10- Tensor parallelism is configured automatically based on the number of available GPUs