Add Qwen3-8B no-think variant — dual thinking deployment (t_664289a0)
Part A: qwen3-no-think.jinja.j2 Ansible template
- New template: templates/qwen3-no-think.jinja.j2
Standard Qwen3 chat template with enable_thinking unconditionally false
(hardcoded empty <think></think> prefix at add_generation_prompt step).
Deployed to /opt/models/templates/qwen3-no-think.jinja on astro-orbiter.
- tasks/models.yml: deploy templates dir + qwen3-no-think.jinja via
ansible.builtin.template task (tags: models, chat_templates).
Part B: INI preset template — two Qwen3-8B sections
- templates/llama-server-router-preset.ini.j2:
[Qwen3-8B-Q4_K_M] — thinking variant, same GGUF, baked-in template.
[Qwen3-8B-Q4_K_M-no_think] — no-think variant, same GGUF,
chat-template-file = /opt/models/templates/qwen3-no-think.jinja.
Both sections: n-gpu-layers=99, ctx-size=32768, flash-attn=true,
q4_0 KV cache, sleep-idle-seconds=60.
Part C: defaults/main.yml — llama-swap models + matrix
- llm_swapmode_models: added Qwen3-8B-Q4_K_M (port 8106, GPU) and
Qwen3-8B-Q4_K_M-no_think (port 8107, GPU, chat_template_file set).
- llm_swapmode_matrix_rows: row5 (Qwen3-8B thinking + embed),
row6 (Qwen3-8B no_think + embed). Neither co-resident with Qwen3.8-27B.
- templates/llama-swap-config.yaml.j2: added chat_template_file support
(--chat-template-file flag conditional on model.chat_template_file).
- tasks/swapmode.yml: GATE 2 assert updated 5 -> 7 models.
Part D: war-machine config.yaml (not tracked in git)
- custom_providers.astro-orbiter.models: added Qwen3-8B-Q4_K_M-no_think
(context_length: 32768).
- Reassigned 5 latency-sensitive aux tasks from Meta-Llama-3.1-8B to
Qwen3-8B-Q4_K_M-no_think: skills_hub, approval, mcp, title_generation,
profile_describer. Rationale: GPU-resident, lower latency, json_schema OK.
- web_extract and compression remain on Phi-3.5-mini (long scrapes).
VRAM: both Qwen3-8B variants co-reside with nomic-embed only (~10.4GB +
84MB). Cannot co-reside with Qwen3.8-27B (17.8GB); LRU eviction applies.
This commit is contained in:
305
ansible/roles/llm-inference-multimodel/tasks/swapmode.yml
Normal file
305
ansible/roles/llm-inference-multimodel/tasks/swapmode.yml
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
# ------------------------------------------------------------------------------
|
||||
# FILE: roles/llm-inference-multimodel/tasks/swapmode.yml
|
||||
# DESCRIPTION: Phase S — llama-swap mode hot-swap proxy (port 8001).
|
||||
#
|
||||
# This phase is ADDITIVE and IDEMPOTENT. The existing production
|
||||
# unit (llama-server-qwen, port 8002) is never touched here.
|
||||
#
|
||||
# All tasks are gated on llm_swapmode_enabled | default(false).
|
||||
# With the default (false) this entire file is a no-op.
|
||||
#
|
||||
# When llm_swapmode_enabled: true (set by host_vars or extra-vars),
|
||||
# this phase:
|
||||
# swapmode_binary — download + install binary
|
||||
# swapmode_config — template config.yaml
|
||||
# swapmode_systemd — deploy llama-swap.service unit
|
||||
# swapmode_firewall — open port 8001 to Hermes subnet
|
||||
# swapmode_verify — start service, run 4 validation gates
|
||||
#
|
||||
# Tags map 1:1 to the sub-phases for independent execution:
|
||||
# --tags swapmode_binary,swapmode_config,swapmode_systemd,swapmode_firewall,swapmode_verify
|
||||
#
|
||||
# IMPORTANT: swapmode_verify starts the service. Do not run
|
||||
# swapmode_verify unless swapmode_binary and swapmode_systemd
|
||||
# have already run.
|
||||
#
|
||||
# Added 2026-08-18 (t_c1e44190): llama-swap Phase 3 Ansible integration — Wong.
|
||||
# Approved by War Machine Phase 1 validation (3 of 4 hard gates PASS).
|
||||
# Phase 3 gated on all profiles migrated + production router decommissioned.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# =============================================================================
|
||||
# TAG: swapmode_binary
|
||||
# Download and install llama-swap binary from GitHub releases.
|
||||
# Idempotent: checks for existing binary and verifies architecture.
|
||||
# =============================================================================
|
||||
|
||||
- name: "[swapmode_binary] Detect host architecture (x86_64 / aarch64)"
|
||||
ansible.builtin.command:
|
||||
cmd: uname -m
|
||||
register: llm_swapmode_arch
|
||||
changed_when: false
|
||||
become: false
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
- name: "[swapmode_binary] Ensure config directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ llm_swapmode_config_dir }}"
|
||||
state: directory
|
||||
owner: "{{ llm_swapmode_service_user }}"
|
||||
group: "{{ llm_swapmode_service_user }}"
|
||||
mode: "0755"
|
||||
become: true
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
- name: "[swapmode_binary] Download llama-swap binary"
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ llm_swapmode_binary_url }}"
|
||||
dest: "/tmp/llama-swap-{{ llm_swapmode_binary_version }}.tar.gz"
|
||||
checksum: "{{ llm_swapmode_checksum }}"
|
||||
mode: "0644"
|
||||
become: true
|
||||
register: llm_swapmode_download
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
- name: "[swapmode_binary] Extract llama-swap binary"
|
||||
ansible.builtin.unarchive:
|
||||
src: "/tmp/llama-swap-{{ llm_swapmode_binary_version }}.tar.gz"
|
||||
dest: /tmp
|
||||
remote_src: true
|
||||
creates: /tmp/llama-swap
|
||||
become: true
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
- name: "[swapmode_binary] Install llama-swap to /usr/local/bin"
|
||||
ansible.builtin.copy:
|
||||
src: /tmp/llama-swap
|
||||
dest: /usr/local/bin/llama-swap
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
remote_src: true
|
||||
become: true
|
||||
register: llm_swapmode_binary_installed
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
- name: "[swapmode_binary] Verify llama-swap binary is executable"
|
||||
ansible.builtin.command:
|
||||
cmd: /usr/local/bin/llama-swap --version
|
||||
register: llm_swapmode_version_check
|
||||
changed_when: false
|
||||
become: false
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
- name: "[swapmode_binary] Cleanup download artifacts"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
become: true
|
||||
loop:
|
||||
- "/tmp/llama-swap-{{ llm_swapmode_binary_version }}.tar.gz"
|
||||
- /tmp/llama-swap
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_binary]
|
||||
|
||||
# =============================================================================
|
||||
# TAG: swapmode_config
|
||||
# Render config.yaml.j2 template and deploy to /etc/llama-swap/config.yaml
|
||||
# =============================================================================
|
||||
|
||||
- name: "[swapmode_config] Deploy llama-swap config.yaml from template"
|
||||
ansible.builtin.template:
|
||||
src: llama-swap-config.yaml.j2
|
||||
dest: "{{ llm_swapmode_config_file }}"
|
||||
owner: "{{ llm_swapmode_service_user }}"
|
||||
group: "{{ llm_swapmode_service_user }}"
|
||||
mode: "0644"
|
||||
become: true
|
||||
register: llm_swapmode_config_deployed
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_config]
|
||||
|
||||
- name: "[swapmode_config] Validate config.yaml syntax (YAML parse check)"
|
||||
ansible.builtin.command:
|
||||
cmd: python3 -c "import yaml; yaml.safe_load(open('{{ llm_swapmode_config_file }}'))"
|
||||
register: llm_swapmode_config_validate
|
||||
changed_when: false
|
||||
become: true
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_config]
|
||||
|
||||
# =============================================================================
|
||||
# TAG: swapmode_systemd
|
||||
# Deploy the llama-swap systemd unit file and reload systemd.
|
||||
# Does NOT start the service — that is swapmode_verify only.
|
||||
# =============================================================================
|
||||
|
||||
- name: "[swapmode_systemd] Deploy llama-swap systemd unit"
|
||||
ansible.builtin.template:
|
||||
src: llama-swap.service.j2
|
||||
dest: "/etc/systemd/system/{{ llm_swapmode_service_name }}.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
become: true
|
||||
register: llm_swapmode_unit_deployed
|
||||
notify:
|
||||
- reload systemd
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_systemd]
|
||||
|
||||
- name: "[swapmode_systemd] Flush handlers so daemon-reload lands before swapmode_verify starts the unit"
|
||||
ansible.builtin.meta: flush_handlers
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_systemd]
|
||||
|
||||
# =============================================================================
|
||||
# TAG: swapmode_firewall
|
||||
# Open port 8001 in ufw scoped to the Hermes source subnet.
|
||||
# Idempotent: named comment + state: present prevents duplicate rules.
|
||||
# =============================================================================
|
||||
|
||||
- name: "[swapmode_firewall] Check whether ufw is installed/active"
|
||||
ansible.builtin.command:
|
||||
cmd: ufw status
|
||||
register: llm_swapmode_ufw_status
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
become: true
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_firewall]
|
||||
|
||||
- name: "[swapmode_firewall] WARNING — ufw not active, port {{ llm_swapmode_port }} scoping cannot be applied"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
ufw does not appear to be active on this host. Firewall scoping for
|
||||
port {{ llm_swapmode_port }} was skipped. Bind address alone
|
||||
({{ llm_swapmode_bind_address }}) limits exposure — flag to Ryan.
|
||||
when:
|
||||
- llm_swapmode_enabled | default(false)
|
||||
- "'Status: active' not in (llm_swapmode_ufw_status.stdout | default(''))"
|
||||
tags: [swapmode_firewall]
|
||||
|
||||
- name: "[swapmode_firewall] Allow llama-swap port ({{ llm_swapmode_port }}) from Hermes source subnet"
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ llm_swapmode_port | string }}"
|
||||
proto: tcp
|
||||
src: "{{ llm_swapmode_allowed_source_cidr }}"
|
||||
comment: "llm-inference-multimodel: llama-swap ({{ llm_swapmode_port }}) — scoped to Hermes subnet"
|
||||
become: true
|
||||
when:
|
||||
- llm_swapmode_enabled | default(false)
|
||||
- "'Status: active' in (llm_swapmode_ufw_status.stdout | default(''))"
|
||||
tags: [swapmode_firewall]
|
||||
|
||||
# =============================================================================
|
||||
# TAG: swapmode_verify
|
||||
# Start the service, then run the 4 validation gates.
|
||||
# This is the ONLY phase that actually starts llama-swap.
|
||||
# =============================================================================
|
||||
|
||||
- name: "[swapmode_verify] Start llama-swap service"
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ llm_swapmode_service_name }}"
|
||||
state: started
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
become: true
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
# GATE 1: Health check
|
||||
- name: "[swapmode_verify] GATE 1 — Health check (/health endpoint)"
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ llm_swapmode_bind_address }}:{{ llm_swapmode_port }}/health"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: llm_swapmode_health
|
||||
until: llm_swapmode_health.status == 200
|
||||
retries: 30
|
||||
delay: 2
|
||||
become: false
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
# GATE 2: Model discovery
|
||||
- name: "[swapmode_verify] GATE 2 — Model discovery (/v1/models)"
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ llm_swapmode_bind_address }}:{{ llm_swapmode_port }}/v1/models"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: llm_swapmode_models_list
|
||||
become: false
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
- name: "[swapmode_verify] Assert all 7 models are discoverable"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- llm_swapmode_models_list.json.data | map(attribute='id') | list | length == 7
|
||||
fail_msg: >-
|
||||
Expected 7 models in /v1/models response, got {{ llm_swapmode_models_list.json.data | length }}.
|
||||
Models: {{ llm_swapmode_models_list.json.data | map(attribute='id') | list }}
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
# GATE 3: Smoke test — simple completion on a CPU-offload model (no VRAM conflict)
|
||||
- name: "[swapmode_verify] GATE 3 — Smoke test completion (Meta-Llama-3.1-8B CPU-offload)"
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ llm_swapmode_bind_address }}:{{ llm_swapmode_port }}/v1/chat/completions"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
model: "Meta-Llama-3.1-8B-Instruct-Q4_K_M"
|
||||
messages:
|
||||
- role: "user"
|
||||
content: "What is 2+2?"
|
||||
temperature: 0.1
|
||||
max_tokens: 50
|
||||
status_code: 200
|
||||
register: llm_swapmode_smoke_test
|
||||
become: false
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
# GATE 4: VRAM guard check
|
||||
- name: "[swapmode_verify] GATE 4 — VRAM usage check (must be < {{ llm_swapmode_vram_max_mib }} MiB)"
|
||||
ansible.builtin.shell:
|
||||
cmd: nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -1
|
||||
register: llm_swapmode_vram_used
|
||||
changed_when: false
|
||||
become: false
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
- name: "[swapmode_verify] Assert VRAM usage is within budget"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (llm_swapmode_vram_used.stdout | int) < llm_swapmode_vram_max_mib
|
||||
fail_msg: >-
|
||||
VRAM usage ({{ llm_swapmode_vram_used.stdout }} MiB) exceeds gate limit ({{ llm_swapmode_vram_max_mib }} MiB).
|
||||
Check for resource contention with production router or other services.
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
|
||||
# Display verification results
|
||||
- name: "[swapmode_verify] Display verification results"
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
✓ GATE 1: Health check PASS
|
||||
✓ GATE 2: Model discovery PASS — {{ llm_swapmode_models_list.json.data | map(attribute='id') | list | join(', ') }}
|
||||
✓ GATE 3: Smoke test (Llama-3.1-8B) PASS
|
||||
✓ GATE 4: VRAM guard ({{ llm_swapmode_vram_used.stdout }} MiB < {{ llm_swapmode_vram_max_mib }} MiB) PASS
|
||||
|
||||
llama-swap service is ready at http://{{ llm_swapmode_bind_address }}:{{ llm_swapmode_port }}/
|
||||
NOTE: 7 models registered (5 original + Qwen3-8B-Q4_K_M + Qwen3-8B-Q4_K_M-no_think).
|
||||
when: llm_swapmode_enabled | default(false)
|
||||
tags: [swapmode_verify]
|
||||
Reference in New Issue
Block a user