InferenceX is a self-hosted LLM inference platform built incrementally on top of vLLM. It provides an OpenAI-compatible POST /v1/chat/completions endpoint, a model registry, an observability pipeline, and an interactive Textual playground — all designed to run on a single WSL2 machine with one consumer-grade GPU.
InferenceX runs on WSL2 Ubuntu (tested on Ubuntu 22.04). Enable WSL2 in Windows and install Ubuntu from the Microsoft Store. The GPU must be accessible inside WSL2:
nvidia-smi # should return GPU info; if not, update your NVIDIA Windows drivervLLM requires CUDA. The recommended approach is to use the bundled CUDA wheels installed
by uv sync. If you need a system-level CUDA toolkit (e.g. for FlashInfer JIT), install
CUDA 12.x:
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update && sudo apt-get install -y cuda-toolkit-12-4Then export CUDA_HOME=/usr/local/cuda in your shell profile.
WSL2 note: FlashInfer JIT often fails on WSL2 due to missing system nvcc. InferenceX automatically sets
VLLM_USE_FLASHINFER_SAMPLER=0on WSL2 to avoid this.
InferenceX uses uv for dependency management:
curl -LsSf https://astral.sh/uv/install.sh | sh# Clone the repository
git clone https://github.com/coeusyk/inference-x.git
cd inference-x
# Install all dependencies (creates .venv automatically)
uv sync
# Verify unit tests pass (no GPU required)
uv run pytest tests/unit -qImportant: Always use
uv runto invoke scripts and tools. Barepythonoruvicornfrom your shell PATH will bypass.venvand vllm will appear missing.
Models are downloaded from HuggingFace on first use. Pre-downloading avoids a silent stall during server startup:
# Using the hf CLI (recommended — huggingface-cli is deprecated)
uv run hf download Qwen/Qwen2.5-0.5B-Instruct
uv run hf download Qwen/Qwen2.5-1.5B-Instruct
uv run hf download Qwen/Qwen1.5-1.8B-Chat
uv run hf download openbmb/MiniCPM5-1B
uv run hf download TinyLlama/TinyLlama-1.1B-Chat-v1.0
uv run hf download facebook/opt-125m
# Or using the Python API
uv run python -c "
from huggingface_hub import snapshot_download
snapshot_download('Qwen/Qwen2.5-0.5B-Instruct')
snapshot_download('TinyLlama/TinyLlama-1.1B-Chat-v1.0')
"For gated models (not in the default 8 GiB registry — add manually to config/models.yaml if you have ≥16 GiB VRAM and HuggingFace access):
- Request access on the model page and wait for approval.
- Add an entry to
config/models.yaml(seetests/unit/test_vllm_gated.pyfor shape), then authenticate locally:
uv run hf auth login
# or: export HF_TOKEN=hf_... (see .env.example)
uv run hf download meta-llama/Meta-Llama-3-8B-Instruct
INFERENCE_X_DEFAULT_MODEL=llama3-8b ./scripts/dev.sh serveWithout a token, startup fails immediately with a short message instead of a HuggingFace stack trace. If you have a token but access is not yet approved, startup fails with a separate message pointing you to the model page — no multi-minute vLLM stack trace.
make chat and make playground launch the TUI first. After you choose model(s), the
server starts automatically with INFERENCE_X_LOADED_MODELS set to your selection
(one model for chat, two for compare).
You do not need to run scripts/dev.sh serve first.
# Daily-driver: multi-turn chat CLI
make chat# Compare two models side-by-side
make playground
# Pre-selected compare pair (skips the two-model picker)
make playground-compare MODEL_A=qwen2.5-0.5b MODEL_B=tinyllama-chatWhile the model loads, a loading screen shows startup phases, step progress, and a
live tail of logs/playground-server.log. On failure, the banner and log feed show an
actionable error summary (e.g. insufficient VRAM) parsed from the server log — not a
generic fallback. Server logs also go to:
tail -f logs/playground-server.logUse scripts/dev.sh serve when you want a headless API server only. This is required
before running benchmarks (make benchmark, make benchmark-all, make advise) or
direct API calls from a second terminal.
# Terminal 1 — start server only (required for benchmarks and direct API access)
INFERENCE_X_DEFAULT_MODEL=qwen2.5-0.5b ./scripts/dev.sh serve# Terminal 2 — API smoke test
curl -s http://localhost:8000/health
curl -s http://localhost:8000/v1/models
curl -s -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen2.5-0.5b","messages":[{"role":"user","content":"Hello"}]}'# Benchmarks (server must already be running)
make benchmark MODEL=qwen2.5-0.5b
make benchmark-all
make adviseTo load multiple models for compare mode or benchmarks across models:
INFERENCE_X_LOADED_MODELS=qwen2.5-0.5b,tinyllama-chat ./scripts/dev.sh serveThe INFERENCE_X_LOADED_MODELS environment variable accepts a comma-separated list of
model names from config/models.yaml. All listed models are loaded at startup and served
simultaneously. VRAM is split automatically between them.
Run make help for a full list of make targets.
Interactive (self-contained — no manual server start):
| Command | Description |
|---|---|
make chat |
Start server + Claude-style multi-turn chat CLI (single model) |
make playground |
Compare two models side-by-side (interactive TUI) |
make playground-compare MODEL_A=… MODEL_B=… |
Compare with preset models (skips picker) |
make stop |
Stop background uvicorn and vLLM worker processes |
Server / API / benchmarks (start scripts/dev.sh serve first):
| Command | Description |
|---|---|
make benchmark MODEL=<name> |
Run standard prompt suite for one model |
make benchmark-all |
Benchmark qwen2.5-0.5b + tinyllama-chat |
make advise |
Print ranked model advisor report |
make client |
Batch CLI runner (rich terminal output) |
Copy .env.example to .env at the repo root — both ./scripts/dev.sh serve and the Python app load it automatically (shell variables already exported take precedence).
| File | Purpose |
|---|---|
config/models.yaml |
Model registry — name, model_path, gpu_memory_utilization, max_model_len |
config/vram_tiers.yaml |
VRAM tier definitions (6gb/12gb/24gb) — utilization ceiling, max_model_len cap, batching knobs |
config/logging.yaml |
Logging config — rotating file handler + console |
Key environment variables:
| Variable | Default | Description |
|---|---|---|
INFERENCE_X_DEFAULT_MODEL |
qwen2.5-0.5b |
Model to load when LOADED_MODELS is unset; also accepts a registered model family name, resolved to its best-fitting variant at startup (e.g. INFERENCE_X_DEFAULT_MODEL=qwen2.5-7b) |
INFERENCE_X_LOADED_MODELS |
(default model) | Comma-separated list of models to load at startup |
INFERENCE_X_CONFIG_DIR |
config |
Path to config directory |
INFERENCE_X_METRICS_FILE |
(unset) | If set, enables NDJSON metrics export to this path |
INFERENCE_X_STREAM_TIMEOUT_S |
120 |
Per-token SSE timeout in seconds (0 = disabled) |
The benchmark runner measures throughput (tokens/sec), time-to-first-token (TTFT),
latency percentiles (p50/p95/p99), and VRAM footprint per model (peak_vram_delta_gb:
total GPU memory minus the minimum free VRAM observed before and after the run). This
works when the model is already loaded on the server — the common workflow. Each result
JSON also stores a hardware snapshot (GPU name, VRAM total/free, CPU, RAM) from run time.
The model advisor ranks results against your current hardware profile. It skips
results whose saved hardware does not match the current GPU (with a warning) and emits
a soft warning for legacy results that lack a hardware field — re-run
make benchmark MODEL=… to refresh them.
Prerequisites: a running server with the target model loaded. Start the server
separately via ./scripts/dev.sh serve (or restart with make stop then serve again).
make chat / make playground start their own background server but are not used for
the benchmark CLI workflow below.
# Terminal 1 — server must be running (not started by make benchmark)
INFERENCE_X_DEFAULT_MODEL=qwen2.5-0.5b ./scripts/dev.sh serve
# Terminal 2
make benchmark MODEL=qwen2.5-0.5b # writes benchmarks/results/results-<model>-<timestamp>.json
make benchmark-all # benchmarks qwen2.5-0.5b and tinyllama-chat
make advise # ranked table; static VRAM estimates if no results yetOptional hardware profiling deps (improves VRAM accuracy on WSL2):
uv sync --extra hardware # installs nvidia-ml-py + psutilAPI endpoints (read-only, return empty lists when no results exist yet):
| Endpoint | Description |
|---|---|
GET /v1/benchmark/results |
Stored benchmark results + current hardware profile |
GET /v1/benchmark/advise |
Ranked advisor output, current hardware, and warnings |
Benchmarks run from the CLI (make benchmark, make advise) or API — not from the
compare playground TUI. Use make chat for daily single-model chat and make playground
for side-by-side model comparison.
Prompt suite: benchmarks/prompts/standard.json — 10 fixed prompts (factual,
generation, code, reasoning, chat). Results are comparable across runs via
suite_version (SHA256 of the prompt list).
Symptom: Server appears to hang after printing Using FlashAttention version 2.
Cause: HuggingFace is downloading the model weights in the background with no progress indicator (~2–7 GB depending on model). This is normal.
Fix: Pre-download the model weights before starting the server (see above). You can watch the HuggingFace cache directory to see download progress:
watch -n 2 "du -sh ~/.cache/huggingface/hub/"If the server was interrupted mid-download, clear the incomplete cache entry:
rm -rf ~/.cache/huggingface/hub/models--<org>--<model>Symptom: RuntimeError: vLLM FlashInfer JIT requires nvcc or
CUDA_HOME not set and nvcc not found.
Fix 1 (recommended): Always start the server via uv run or ./scripts/dev.sh serve.
The bundled nvidia-cuda-nvcc wheel provides nvcc in .venv.
Fix 2: Set CUDA_HOME to your CUDA toolkit root:
export CUDA_HOME=/usr/local/cuda
export PATH=$CUDA_HOME/bin:$PATHFix 3 (WSL2-specific): InferenceX automatically disables FlashInfer on WSL2
(VLLM_USE_FLASHINFER_SAMPLER=0). If you see this error anyway, run:
export VLLM_USE_FLASHINFER_SAMPLER=0
./scripts/dev.sh serveSymptom: Server exits on startup with gated on HuggingFace, not yet approved, or 403 Forbidden for meta-llama/....
Fix:
- Open https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct and request access (Meta license).
- Wait for approval email from HuggingFace (a token alone is not enough until access is granted).
- After approval:
uv run hf auth login - Add
llama3-8btoconfig/models.yamlif not present, then retry serve.
If you do not have access yet, use a registry model instead:
INFERENCE_X_DEFAULT_MODEL=qwen2.5-0.5b ./scripts/dev.sh serveSymptom: Startup fails with Free memory on device cuda:0 ... less than desired GPU memory utilization, Insufficient GPU memory, or KV-cache / Mamba-cache errors.
Fix:
- Stop other GPU processes (
make stop, or kill leftoveruvicorn/ vLLM workers). - Use a smaller registry model (
qwen2.5-0.5b,minicpm5-1b, orqwen2.5-1.5batmax_model_len: 8192). - Lower
max_model_len(e.g. 2048) for tight VRAM; hybrid/Mamba models may also needmax_num_seqs. - Dense bf16 models up to ~2B (e.g.
qwen1.5-1.8b) run reliably on an 8 GiB card withgpu_memory_utilization: auto. 3B+ dense bf16 or 7B+ needs quantization (seeqwen2.5-7b-awq) or more VRAM — 7B+ bf16 is omitted from the default registry on 8 GiB cards.
Symptom: OSError: [Errno 98] Address already in use on port 8000.
Fix: Find and kill the existing process:
make stop
# or manually:
lsof -ti:8000 | xargs kill -9A previous make playground may have left a background uvicorn process running.
Always run make stop before restarting.
Symptom: Second server fails with
Available KV cache memory: -0.04 GiB or similar negative cache error.
Fix: Only one vLLM server per GPU. Kill all existing processes first:
pkill -f "uvicorn inference_x"
pkill -f "VLLM::EngineCore"- Windows host with WSL2 Ubuntu as the primary runtime
- Cursor as the editor (with agent-assisted development via
.cursor/rules/) - Python 3.13+ managed by uv
- vLLM for inference (must be run with
uv run)
