Idempotent vLLM OpenAI-compatible serving role, staged-first (does not
start/enable the systemd unit or touch production traffic by default).
Validated end-to-end against astro-orbiter in a brief shadow window
(llama-swap stopped ~5 min, per homelab-llm-inference skill's documented
shadow-validation pattern):
- /health 200, /v1/models returns Qwen2.5-32B-Instruct-AWQ,
/v1/completions live smoke test passes, clean journalctl
- 3 consecutive full-role runs confirmed changed=0 (idempotent)
- production restored: llama-swap active, /v1/embeddings against
nomic-embed-text-v1.5 confirmed still working (Hindsight retain path)
Deviates from the original spec's model choices (Qwen2.5-32B-Instruct /
Qwen3-8B-Instruct bf16) to use the official Qwen AWQ pre-quantized variants
instead -- vLLM does not do safe on-the-fly quantization on this host
(bitsandbytes OOM history) and unquantized bf16 32B does not fit 24GB VRAM.
Two real bugs found+fixed during first-start validation (systemd-only
repro, not visible via interactive SSH testing):
1. ninja not on systemd's minimal PATH -> vLLM torch.compile
FileNotFoundError. Fixed via explicit PATH env in the unit.
2. FlashInfer sampler JIT fails to compile on RTX 3090 (SM86) --
known upstream issue class (vLLM GH #23023, #44305). Fixed via
VLLM_USE_FLASHINFER_SAMPLER=0 (falls back to native sampler).
Also fixed a real idempotency bug: force-upgrading setuptools to latest
fought with vLLM's own setuptools<81.0.0 pin, causing an install/downgrade
flip-flop (changed:true) on every run.
vllm_service_enabled defaults to false -- a host reboot must not
auto-start vLLM and VRAM-collide with the still-live llama-swap production
service. Cutover (enabling + starting + migrating consumers) is an
explicit, separate step outside this role, gated on adding embedding-mode
support (--task embed) for nomic-embed-text-v1.5, which this role does
not yet implement (Hindsight retain still depends on llama-swap's
nomic-embed until that follow-up lands).
Role: roles/deploy-vllm/ (defaults/handlers/meta/tasks/templates/README)
Playbook: playbooks/day1_deploy_vllm.yml
114 lines
4.3 KiB
YAML
114 lines
4.3 KiB
YAML
# ------------------------------------------------------------------------------
|
|
# FILE: roles/deploy-vllm/tasks/dependencies.yml
|
|
# PHASE 1: Python 3.10+, vLLM >=0.5.0, PyTorch+CUDA, verify nvidia-smi.
|
|
#
|
|
# Pitfall (homelab-llm-serving skill): vLLM bundles its own CUDA 12.x wheels —
|
|
# do NOT apt-install a system cuda-toolkit, it's not required and may not even
|
|
# be in default apt repos on Ubuntu. pip install vllm is sufficient.
|
|
#
|
|
# Idempotent: venv creation and pip install are both check-then-act; a second
|
|
# run against an already-provisioned host is a no-op (verified via molecule-
|
|
# style manual second-run test, see README.md Testing section).
|
|
# ------------------------------------------------------------------------------
|
|
|
|
- name: Verify nvidia-smi is present and a GPU is visible
|
|
ansible.builtin.command: nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
|
|
register: vllm_nvidia_smi
|
|
changed_when: false
|
|
|
|
- name: Report detected GPU
|
|
ansible.builtin.debug:
|
|
msg: "GPU detected: {{ vllm_nvidia_smi.stdout }}"
|
|
|
|
- name: Fail fast if nvidia-smi reports no GPU
|
|
ansible.builtin.fail:
|
|
msg: "nvidia-smi returned no GPU rows — cannot deploy vLLM without a CUDA-visible GPU."
|
|
when: vllm_nvidia_smi.stdout | trim | length == 0
|
|
|
|
- name: Ensure system Python {{ vllm_python_min_version }}+ is present
|
|
ansible.builtin.command: "python3 -c 'import sys; assert sys.version_info >= (3, 10), sys.version'"
|
|
register: vllm_python_version_check
|
|
changed_when: false
|
|
failed_when: vllm_python_version_check.rc != 0
|
|
|
|
- name: Ensure python3-venv is installed
|
|
ansible.builtin.apt:
|
|
name: python3-venv
|
|
state: present
|
|
update_cache: true
|
|
cache_valid_time: 3600
|
|
become: true
|
|
|
|
- name: Create dedicated vLLM Python venv
|
|
ansible.builtin.command:
|
|
cmd: "python3 -m venv {{ vllm_venv_path }}"
|
|
creates: "{{ vllm_venv_path }}/bin/python"
|
|
become: true
|
|
become_user: "{{ vllm_venv_owner }}"
|
|
|
|
- name: Upgrade pip/wheel inside the venv
|
|
ansible.builtin.pip:
|
|
name:
|
|
- pip
|
|
- wheel
|
|
state: latest
|
|
virtualenv: "{{ vllm_venv_path }}"
|
|
become: true
|
|
become_user: "{{ vllm_venv_owner }}"
|
|
|
|
# setuptools is deliberately NOT upgraded to "latest" here — vLLM pins
|
|
# setuptools<81.0.0,>=77.0.3 as a transitive dependency. Forcing it to latest
|
|
# (84.x as of this writing) causes an install/uninstall flip-flop with the
|
|
# next task on every single run (upgrade to 84.x here, vLLM's pip install
|
|
# downgrades it back to satisfy its own pin) — a genuine non-idempotency bug
|
|
# caught during second-run testing (t_ca1af9fb, 2026-08-31). Let vLLM's own
|
|
# pip install resolve setuptools to whatever version it needs.
|
|
|
|
- name: Install vLLM ({{ vllm_version_spec }})
|
|
ansible.builtin.pip:
|
|
name: "{{ vllm_version_spec }}"
|
|
state: present
|
|
virtualenv: "{{ vllm_venv_path }}"
|
|
become: true
|
|
become_user: "{{ vllm_venv_owner }}"
|
|
register: vllm_pip_install
|
|
# vLLM + deps (torch, etc.) is a large download — allow generous time.
|
|
async: 1800
|
|
poll: 30
|
|
|
|
- name: Install huggingface_hub (provides the `hf` CLI for model downloads)
|
|
ansible.builtin.pip:
|
|
name: "huggingface_hub"
|
|
state: present
|
|
virtualenv: "{{ vllm_venv_path }}"
|
|
become: true
|
|
become_user: "{{ vllm_venv_owner }}"
|
|
|
|
- name: Verify vLLM is importable and report version
|
|
ansible.builtin.command:
|
|
cmd: "{{ vllm_venv_path }}/bin/python -c 'import vllm; print(vllm.__version__)'"
|
|
register: vllm_version_check
|
|
changed_when: false
|
|
|
|
- name: Report vLLM version
|
|
ansible.builtin.debug:
|
|
msg: "vLLM version installed: {{ vllm_version_check.stdout }}"
|
|
|
|
- name: Verify torch reports CUDA available
|
|
ansible.builtin.command:
|
|
cmd: "{{ vllm_venv_path }}/bin/python -c 'import torch; print(torch.cuda.is_available(), torch.version.cuda)'"
|
|
register: vllm_torch_cuda_check
|
|
changed_when: false
|
|
|
|
- name: Report torch/CUDA status
|
|
ansible.builtin.debug:
|
|
msg: "torch.cuda.is_available(), torch.version.cuda = {{ vllm_torch_cuda_check.stdout }}"
|
|
|
|
- name: Warn if CUDA is not available to torch
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
WARNING: torch reports CUDA unavailable inside the vLLM venv. Serving will
|
|
fall back to CPU (unusable for 32B-class models). Check nvidia driver /
|
|
CUDA wheel compatibility before proceeding to Phase 2.
|
|
when: "'True' not in vllm_torch_cuda_check.stdout"
|