gpu-llamacpp: Compose auf P2000 per device_ids, Rolle und Playbook (Task 6.1)
ci / test (push) Successful in 27s

- Build-Pin wie im Benchmark (b6000 = 4762ad7, CUDA 12.4, sm_52;61), Host-Netz,
  Log-Rotation, /slots und Web-Oberfläche aus, Qwen3 ohne Denkphase.
- Exporter aus dem Compose entfernt (laufen nativ, Rolle gpu-exporters).
- Rolle gpu-llamacpp: Karten-UUID über den Namen, Modelle mit Prüfsumme,
  ufw-Regeln nur vorgemerkt. Inventargruppe gpu_llamacpp (ohne gpu-pve52).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Rapp
2026-10-12 21:28:33 +02:00
co-authored by Claude Opus 5.5
parent d8797a9433
commit 5c790cc8d4
6 changed files with 233 additions and 39 deletions
@@ -0,0 +1,50 @@
# llama.cpp-Rollen in den GPU-VMs (Task 6.1): Compose aus deploy/compose/gpu-llamacpp.
# Profile je VM kommen aus dem Inventar (Gruppe gpu_llamacpp, Variable llamacpp_profiles).
llamacpp_dir: /opt/knaster/gpu-llamacpp
llamacpp_models_dir: /srv/models
llamacpp_profiles: []
# Karte je Dienst, gewählt über den Namen aus `nvidia-smi` (ADR-0006: alles auf die P2000, M4000 bleibt frei).
# Passt Rerank nicht neben Embedding auf die P2000, hier "Quadro M4000" eintragen.
llamacpp_cards:
EMBED_GPU: Quadro P2000
RERANK_GPU: Quadro P2000
CHAT_GPU: Quadro P2000
# Dienste und Ports je Profil (feste Ports im Compose, Host-Netz)
llamacpp_services:
embed:
- {name: llamacpp-embed, port: 8081}
- {name: llamacpp-rerank, port: 8082}
chat:
- {name: llamacpp-chat, port: 8080}
# ufw: nur Regeln vormerken (Kommentar knaster-llamacpp), ufw nie ein- oder ausschalten (Firewall: Task 4.4)
llamacpp_allow_from: 192.168.100.0/24
# Werte für .env (keine Geheimnisse); Build-Stand wie im Benchmark (ADR-0006)
llamacpp_env:
LLAMACPP_REF: b6000
LLAMACPP_COMMIT: 4762ad7316dcdec20016ab5985fb46a27902204d
CUDA_VERSION: 12.4.0
CUDA_DOCKER_ARCH: "52;61"
MODELS_DIR: "{{ llamacpp_models_dir }}"
EMBED_GGUF: Qwen3-Embedding-0.6B-Q8_0.gguf
EMBED_POOLING: last
RERANK_GGUF: bge-reranker-v2-m3-Q8_0.gguf
CHAT_GGUF: Qwen3-4B-Q4_K_M.gguf
# Modelle je Profil mit fester Revision und Prüfsumme (sha256 = LFS-OID laut Hugging Face, ADR-0006)
_hf: https://huggingface.co
llamacpp_models:
embed:
- dest: Qwen3-Embedding-0.6B-Q8_0.gguf
url: "{{ _hf }}/Qwen/Qwen3-Embedding-0.6B-GGUF/resolve/370f27d7550e0def9b39c1f16d3fbaa13aa67728/Qwen3-Embedding-0.6B-Q8_0.gguf"
checksum: sha256:06507c7b42688469c4e7298b0a1e16deff06caf291cf0a5b278c308249c3e439
- dest: bge-reranker-v2-m3-Q8_0.gguf
url: "{{ _hf }}/gpustack/bge-reranker-v2-m3-GGUF/resolve/3093af03b1a635e67b084b1d8c03c5f5e020fd05/bge-reranker-v2-m3-Q8_0.gguf"
checksum: sha256:a43c7c9b11a4c1517e5bf95151960e1621d1b72f7a493364b01e386cf1aaa1d3
chat:
- dest: Qwen3-4B-Q4_K_M.gguf
url: "{{ _hf }}/Qwen/Qwen3-4B-GGUF/resolve/bc640142c66e1fdd12af0bd68f40445458f3869b/Qwen3-4B-Q4_K_M.gguf"
checksum: sha256:7485fe6f11af29433bc51cab58009521f205840f5b4ae3a32fa7f92e8534fdf5
@@ -0,0 +1,111 @@
# llama.cpp-Server je Profil (Task 6.1). Idempotent: Modelle per Prüfsumme, Image nur bauen, wenn es fehlt
# (Image-Tag enthält Stand und CUDA-Version), compose ändert nur, was sich geändert hat.
- name: Profile prüfen
ansible.builtin.assert:
that:
- llamacpp_profiles | length > 0
- llamacpp_profiles | difference(llamacpp_services.keys() | list) | length == 0
fail_msg: "llamacpp_profiles muss aus {{ llamacpp_services.keys() | list }} bestehen (Inventar, Gruppe gpu_llamacpp)"
# Compose baut nur mit buildx (BuildKit, auch für den Git-Kontext mit Commit-Pin)
- name: docker-buildx installieren
ansible.builtin.apt:
name: docker-buildx
state: present
# --- Kartenwahl: UUID statt Index, geprüft über den Kartennamen ---
- name: Karten in der VM lesen
ansible.builtin.command: nvidia-smi --query-gpu=index,uuid,name --format=csv,noheader
register: llamacpp_smi
changed_when: false
- name: UUID je Dienst aus dem Kartennamen bestimmen
ansible.builtin.set_fact:
llamacpp_gpu_env: >-
{{ llamacpp_gpu_env | default({}) | combine({item.key: (llamacpp_smi.stdout_lines
| map('split', ', ') | selectattr(2, 'equalto', item.value) | map(attribute=1) | first | default(''))}) }}
loop: "{{ llamacpp_cards | dict2items }}"
- name: Jede gewünschte Karte ist vorhanden
ansible.builtin.assert:
that: llamacpp_gpu_env[item.key] is match('GPU-')
fail_msg: "{{ item.value }} für {{ item.key }} nicht gefunden: {{ llamacpp_smi.stdout_lines }}"
loop: "{{ llamacpp_cards | dict2items }}"
# --- Modelle ---
- name: Modelle laden (Prüfsumme gegen Hugging Face)
ansible.builtin.get_url:
url: "{{ item.url }}"
dest: "{{ llamacpp_models_dir }}/{{ item.dest }}"
checksum: "{{ item.checksum }}"
mode: "0644"
timeout: 120
loop: "{{ llamacpp_profiles | map('extract', llamacpp_models) | flatten }}"
loop_control:
label: "{{ item.dest }}"
# --- Compose-Projekt ---
- name: Projektverzeichnis
ansible.builtin.file:
path: "{{ llamacpp_dir }}"
state: directory
mode: "0755"
- name: Compose-Datei kopieren
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../../../deploy/compose/gpu-llamacpp/docker-compose.yml"
dest: "{{ llamacpp_dir }}/docker-compose.yml"
mode: "0644"
- name: .env schreiben (keine Geheimnisse)
ansible.builtin.copy:
dest: "{{ llamacpp_dir }}/.env"
mode: "0644"
content: |
# Verwaltet von Ansible (Rolle gpu-llamacpp)
COMPOSE_PROFILES={{ llamacpp_profiles | join(',') }}
{% for k, v in (llamacpp_env | combine(llamacpp_gpu_env)).items() %}
{{ k }}={{ v }}
{% endfor %}
# ufw-Regeln wirken auch bei inaktivem ufw (werden vorgemerkt). Fremde Regeln bleiben unberührt.
- name: Ist ufw vorhanden?
ansible.builtin.stat:
path: /usr/sbin/ufw
register: llamacpp_ufw
- name: ufw – llama.cpp-Ports aus dem LAN vormerken
community.general.ufw:
rule: allow
src: "{{ llamacpp_allow_from }}"
port: "{{ item.port | string }}"
proto: tcp
comment: knaster-llamacpp
loop: "{{ llamacpp_profiles | map('extract', llamacpp_services) | flatten }}"
loop_control:
label: "{{ item.name }}:{{ item.port }}"
when: llamacpp_ufw.stat.exists
# Der erste Build dauert lange (llama.cpp mit CUDA, sm_52/61, alle CPU-Varianten auf 4 vCPU)
- name: Image bauen (falls es fehlt) und Dienste starten
community.docker.docker_compose_v2:
project_src: "{{ llamacpp_dir }}"
profiles: "{{ llamacpp_profiles }}"
build: policy
state: present
remove_orphans: true
wait: false
async: 7200
poll: 30
- name: Warten, bis alle Dienste gesund sind
ansible.builtin.uri:
url: "http://127.0.0.1:{{ item.port }}/health"
status_code: 200
register: llamacpp_health
until: llamacpp_health.status == 200
retries: 60
delay: 10
loop: "{{ llamacpp_profiles | map('extract', llamacpp_services) | flatten }}"
loop_control:
label: "{{ item.name }}:{{ item.port }}"