feat(llm-inference): Day 1 playbook for RTX 3090 vLLM stack on astro-orbiter
- nvidia-driver-595-open (already installed 2026-08-03, idempotent) - Python venv + vLLM 0.26.0 (already installed, idempotent) - Gemma 2 27B model download via HuggingFace hub - systemd vllm-serve.service on port 8000 - Hermes provider integration on carousel-of-progress - vault_hf_token added to group_vars/all/vault - ansible.cfg: vault_password_file set to absolute path - inventory: astro_orbiter group added Run with: env -u ANSIBLE_VAULT_PASSWORD_FILE ansible-playbook -i inventory.yml playbooks/day1_deploy_llm_inference.yml
This commit is contained in:
25
ansible/roles/llm-inference/tasks/driver.yml
Normal file
25
ansible/roles/llm-inference/tasks/driver.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/driver.yml
|
||||
# DESCRIPTION: Phase 2 — NVIDIA driver.
|
||||
# Installs nvidia-driver-595-open via apt. Fully idempotent —
|
||||
# already installed on astro-orbiter on 2026-08-03, this is a no-op.
|
||||
# DKMS builds the kernel module automatically on install.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
- name: Install NVIDIA driver package
|
||||
ansible.builtin.apt:
|
||||
name: "{{ llm_nvidia_driver_package }}"
|
||||
state: present
|
||||
update_cache: false
|
||||
notify: reload systemd
|
||||
|
||||
- name: Verify nvidia-smi reports the GPU
|
||||
ansible.builtin.command: nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
|
||||
register: nvidia_smi_out
|
||||
changed_when: false
|
||||
failed_when: nvidia_smi_out.rc != 0
|
||||
|
||||
- name: Print nvidia-smi output
|
||||
ansible.builtin.debug:
|
||||
msg: "GPU detected: {{ nvidia_smi_out.stdout }}"
|
||||
45
ansible/roles/llm-inference/tasks/foundation.yml
Normal file
45
ansible/roles/llm-inference/tasks/foundation.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/foundation.yml
|
||||
# DESCRIPTION: Phase 1 — Foundation.
|
||||
# - Asserts vault secret is defined
|
||||
# - Adds jarvis user to nvidia GPU groups
|
||||
# - Creates HuggingFace cache directory
|
||||
# - Creates venv parent directory
|
||||
# NOTE: NVIDIA driver install (Phase 2) already performed manually on
|
||||
# 2026-08-03 (nvidia-driver-595-open, DKMS built, nvidia-smi verified).
|
||||
# Phase 2 tasks are idempotent and will no-op on astro-orbiter.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
- name: Assert HuggingFace token is defined in vault
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vault_hf_token is defined
|
||||
- vault_hf_token | length > 0
|
||||
fail_msg: >
|
||||
vault_hf_token is not defined. Add it to group_vars/all/vault:
|
||||
vault_hf_token: "hf_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
- name: Add jarvis user to nvidia GPU groups
|
||||
ansible.builtin.user:
|
||||
name: jarvis
|
||||
groups:
|
||||
- video
|
||||
- render
|
||||
append: true
|
||||
|
||||
- name: Create HuggingFace cache directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ llm_hf_cache_dir }}"
|
||||
state: directory
|
||||
owner: jarvis
|
||||
group: jarvis
|
||||
mode: "0755"
|
||||
|
||||
- name: Create venv parent directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ llm_venv_path | dirname }}"
|
||||
state: directory
|
||||
owner: jarvis
|
||||
group: jarvis
|
||||
mode: "0755"
|
||||
44
ansible/roles/llm-inference/tasks/integration.yml
Normal file
44
ansible/roles/llm-inference/tasks/integration.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/integration.yml
|
||||
# DESCRIPTION: Phase 6 — Wire astro-orbiter into Hermes as a secondary provider.
|
||||
# Writes a provider config fragment to carousel-of-progress
|
||||
# (the Hermes host) so FRIDAY crons can route to the local model.
|
||||
#
|
||||
# Hermes provider config lives at ~/.hermes/config.yaml on carousel.
|
||||
# This task uses the lineinfile/blockinfile approach to add the provider
|
||||
# entry idempotently without clobbering the existing config.
|
||||
#
|
||||
# NOTE: Hermes must be restarted on carousel after this task runs.
|
||||
# Manual step — JARVIS will notify Ryan.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
- name: Check if astro-orbiter provider already configured in Hermes
|
||||
ansible.builtin.command:
|
||||
cmd: grep -c "astro-orbiter" /home/wed/.hermes/config.yaml
|
||||
register: provider_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
delegate_to: carousel-of-progress
|
||||
|
||||
- name: Add astro-orbiter as Hermes secondary provider
|
||||
ansible.builtin.blockinfile:
|
||||
path: /home/wed/.hermes/config.yaml
|
||||
marker: "# {mark} ANSIBLE MANAGED — astro-orbiter vLLM provider"
|
||||
insertafter: "^providers:"
|
||||
block: |
|
||||
# astro-orbiter — local RTX 3090 vLLM inference
|
||||
- name: astro-orbiter
|
||||
type: openai-compatible
|
||||
base_url: http://{{ hostvars['astro-orbiter']['ansible_host'] }}:{{ llm_serve_port }}/v1
|
||||
model: {{ llm_hf_model }}
|
||||
api_key: none
|
||||
when: provider_check.stdout == "0"
|
||||
delegate_to: carousel-of-progress
|
||||
notify: restart hermes
|
||||
|
||||
- name: Remind operator to restart Hermes on carousel
|
||||
ansible.builtin.debug:
|
||||
msg: >
|
||||
Phase 6 complete. Hermes on carousel-of-progress has been updated.
|
||||
Restart Hermes manually or via: systemctl --user restart hermes-gateway hermes-dashboard
|
||||
24
ansible/roles/llm-inference/tasks/main.yml
Normal file
24
ansible/roles/llm-inference/tasks/main.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/main.yml
|
||||
# DESCRIPTION: Entry point — imports one task file per phase.
|
||||
# Phases are additive; re-running the full playbook is always safe.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Phase 1 — Foundation
|
||||
- import_tasks: foundation.yml
|
||||
|
||||
# Phase 2 — Driver
|
||||
- import_tasks: driver.yml
|
||||
|
||||
# Phase 3 — vLLM
|
||||
- import_tasks: vllm.yml
|
||||
|
||||
# Phase 4 — Model
|
||||
- import_tasks: model.yml
|
||||
|
||||
# Phase 5 — Serve
|
||||
- import_tasks: serve.yml
|
||||
|
||||
# Phase 6 — Integration
|
||||
- import_tasks: integration.yml
|
||||
42
ansible/roles/llm-inference/tasks/model.yml
Normal file
42
ansible/roles/llm-inference/tasks/model.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/model.yml
|
||||
# DESCRIPTION: Phase 4 — HuggingFace login and Gemma 2 27B model download.
|
||||
# Idempotent: snapshot_download skips files already present.
|
||||
# Requires vault_hf_token and Gemma 2 licence accepted at
|
||||
# huggingface.co/google/gemma-2-27b-it.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
- name: Write HuggingFace token to ~/.cache/huggingface/token
|
||||
ansible.builtin.copy:
|
||||
content: "{{ vault_hf_token }}"
|
||||
dest: "/home/{{ llm_venv_owner }}/.cache/huggingface/token"
|
||||
owner: "{{ llm_venv_owner }}"
|
||||
group: "{{ llm_venv_owner }}"
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Download Gemma 2 27B model via snapshot_download
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
{{ llm_venv_path }}/bin/python -c "
|
||||
from huggingface_hub import snapshot_download
|
||||
path = snapshot_download(
|
||||
'{{ llm_hf_model }}',
|
||||
cache_dir='{{ llm_hf_cache_dir }}',
|
||||
)
|
||||
print(path)
|
||||
"
|
||||
creates: "{{ llm_hf_cache_dir }}/models--{{ llm_hf_model | replace('/', '--') }}/snapshots"
|
||||
become: true
|
||||
become_user: "{{ llm_venv_owner }}"
|
||||
environment:
|
||||
HF_TOKEN: "{{ vault_hf_token }}"
|
||||
HOME: "/home/{{ llm_venv_owner }}"
|
||||
register: model_download
|
||||
timeout: 3600
|
||||
no_log: false
|
||||
|
||||
- name: Print model download path
|
||||
ansible.builtin.debug:
|
||||
msg: "Model available at: {{ model_download.stdout | default('already present') }}"
|
||||
48
ansible/roles/llm-inference/tasks/serve.yml
Normal file
48
ansible/roles/llm-inference/tasks/serve.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/serve.yml
|
||||
# DESCRIPTION: Phase 5 — systemd vllm-serve service.
|
||||
# Deploys the service unit, enables on boot, starts it, and
|
||||
# health-checks the OpenAI-compatible API endpoint.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
- name: Deploy vllm-serve systemd service unit
|
||||
ansible.builtin.template:
|
||||
src: vllm-serve.service.j2
|
||||
dest: /etc/systemd/system/vllm-serve.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify:
|
||||
- reload systemd
|
||||
- restart vllm-serve
|
||||
|
||||
- name: Flush handlers to reload systemd before enabling service
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Enable and start vllm-serve
|
||||
ansible.builtin.systemd:
|
||||
name: vllm-serve
|
||||
state: started
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
|
||||
- name: Wait for vLLM API to become available (model load can take ~60s)
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ llm_serve_port }}/health"
|
||||
status_code: 200
|
||||
register: vllm_health
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: vllm_health.status == 200
|
||||
|
||||
- name: Smoke-test — list available models
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ llm_serve_port }}/v1/models"
|
||||
status_code: 200
|
||||
return_content: true
|
||||
register: vllm_models
|
||||
|
||||
- name: Print available models
|
||||
ansible.builtin.debug:
|
||||
msg: "vLLM serving: {{ vllm_models.json.data | map(attribute='id') | list }}"
|
||||
41
ansible/roles/llm-inference/tasks/vllm.yml
Normal file
41
ansible/roles/llm-inference/tasks/vllm.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference/tasks/vllm.yml
|
||||
# DESCRIPTION: Phase 3 — Python venv + vLLM install.
|
||||
# Idempotent: venv creation and pip install only run if the
|
||||
# venv binary or vllm package is absent.
|
||||
# Already completed manually on 2026-08-03 — will no-op.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
- name: Create Python venv for vLLM
|
||||
ansible.builtin.command:
|
||||
cmd: python3 -m venv {{ llm_venv_path }}
|
||||
creates: "{{ llm_venv_path }}/bin/python"
|
||||
become: true
|
||||
become_user: "{{ llm_venv_owner }}"
|
||||
|
||||
- name: Upgrade pip inside venv
|
||||
ansible.builtin.pip:
|
||||
name: pip
|
||||
state: latest
|
||||
virtualenv: "{{ llm_venv_path }}"
|
||||
become: true
|
||||
become_user: "{{ llm_venv_owner }}"
|
||||
|
||||
- name: Install vLLM
|
||||
ansible.builtin.pip:
|
||||
name: vllm
|
||||
state: present
|
||||
virtualenv: "{{ llm_venv_path }}"
|
||||
become: true
|
||||
become_user: "{{ llm_venv_owner }}"
|
||||
|
||||
- name: Verify vLLM is importable
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ llm_venv_path }}/bin/python -c 'import vllm; print(vllm.__version__)'"
|
||||
register: vllm_version
|
||||
changed_when: false
|
||||
|
||||
- name: Print vLLM version
|
||||
ansible.builtin.debug:
|
||||
msg: "vLLM version: {{ vllm_version.stdout }}"
|
||||
Reference in New Issue
Block a user