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:
Hermes Agent service account
2026-08-03 11:51:34 -05:00
parent 265d3f8fd6
commit dda6b91330
15 changed files with 758 additions and 361 deletions

View 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 }}"

View 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"

View 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

View 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

View 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') }}"

View 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 }}"

View 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 }}"