k3s: llama.cpp-Server als Deployments (Task 6.1)
ci / test (push) Failing after 22s

Ein Pod je GPU-VM (Embed+Rerank bzw. Chat), Host-Netz mit den Ports aus dem
Compose, P2000 fest per UUID (NVIDIA_VISIBLE_DEVICES). Das Device Plugin
vergibt nur noch Index 0 (M4000). Rolle gpu-llamacpp: gemeinsame Vorbereitung
(prepare.yml), k3s.yml mit Image-Import und Compose-Stopp; gpu-llamacpp.yml
bricht ohne -e llamacpp_compose=true ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Rapp
2026-10-19 17:27:23 +02:00
co-authored by Claude Opus 5.5
parent 24f4066aaf
commit 986f1116fe
9 changed files with 278 additions and 37 deletions
@@ -48,3 +48,34 @@ llamacpp_models:
- dest: Qwen3-4B-Q4_K_M.gguf
url: "{{ _hf }}/Qwen/Qwen3-4B-GGUF/resolve/bc640142c66e1fdd12af0bd68f40445458f3869b/Qwen3-4B-Q4_K_M.gguf"
checksum: sha256:7485fe6f11af29433bc51cab58009521f205840f5b4ae3a32fa7f92e8534fdf5
# --- k3s (Task 6.1, D6): tasks/k3s.yml, Playbook playbooks/gpu-llamacpp-k3s.yml ---
llamacpp_image: "knaster/llamacpp-server:{{ llamacpp_env.LLAMACPP_REF }}-cu{{ llamacpp_env.CUDA_VERSION }}"
llamacpp_k3s_template: "{{ playbook_dir }}/../../../../deploy/k8s/llamacpp/llamacpp.yaml.j2"
llamacpp_k3s_namespace: "{{ playbook_dir }}/../../../../deploy/k8s/llamacpp/namespace.yaml"
# Argumente wie im Compose (deploy/compose/gpu-llamacpp); gpu_var wählt die Karte aus llamacpp_cards
llamacpp_k3s_services:
embed:
- name: llamacpp-embed
port: 8081
gpu_var: EMBED_GPU
memory_limit: 3Gi
args: ["-m", "/models/{{ llamacpp_env.EMBED_GGUF }}", "--embedding", "--pooling", "{{ llamacpp_env.EMBED_POOLING }}",
"-c", "8192", "-b", "8192", "-ub", "8192", "-np", "1", "-ngl", "99", "--alias", "embed-default",
"--metrics", "--no-slots", "--no-webui", "--host", "0.0.0.0", "--port", "8081"]
- name: llamacpp-rerank
port: 8082
gpu_var: RERANK_GPU
memory_limit: 3Gi
args: ["-m", "/models/{{ llamacpp_env.RERANK_GGUF }}", "--reranking",
"-c", "8192", "-b", "8192", "-ub", "8192", "-np", "1", "-ngl", "99", "--alias", "rerank-default",
"--metrics", "--no-slots", "--no-webui", "--host", "0.0.0.0", "--port", "8082"]
chat:
- name: llamacpp-chat
port: 8080
gpu_var: CHAT_GPU
memory_limit: 6Gi
args: ["-m", "/models/{{ llamacpp_env.CHAT_GGUF }}", "-c", "8192", "-np", "1", "-ngl", "99",
"--jinja", "--chat-template-kwargs", "{\"enable_thinking\": false}",
"--alias", "verifier-small", "--metrics", "--no-slots", "--no-webui",
"--host", "0.0.0.0", "--port", "8080"]
@@ -0,0 +1,74 @@
# llama.cpp in k3s statt Compose (Task 6.1, D6). Aufruf über playbooks/gpu-llamacpp-k3s.yml (je VM nacheinander).
# Rückweg je VM: kubectl -n knaster delete deploy llamacpp-<vm>, dann in der VM
# docker compose --project-directory /opt/knaster/gpu-llamacpp start
- name: Profile, Karten, Modelle
ansible.builtin.include_tasks: prepare.yml
# 1. Image aus Docker in das containerd von k3s übernehmen (kein Neubau, kein Registry-Umweg)
- name: Image in k3s vorhanden?
ansible.builtin.command: /usr/local/bin/k3s ctr images ls -q
register: llamacpp_k3s_images
changed_when: false
- name: Image aus Docker importieren
ansible.builtin.shell: set -o pipefail; docker save {{ llamacpp_image }} | /usr/local/bin/k3s ctr images import -
args:
executable: /bin/bash
when: ("docker.io/" ~ llamacpp_image) not in llamacpp_k3s_images.stdout
async: 1800
poll: 15
# 2. Compose-Stack anhalten (Host-Ports und VRAM werden frei), Dateien bleiben für den Rückweg
- name: Compose-Projekt vorhanden?
ansible.builtin.stat:
path: "{{ llamacpp_dir }}/docker-compose.yml"
register: llamacpp_compose_file
- name: Compose-Stack anhalten
community.docker.docker_compose_v2:
project_src: "{{ llamacpp_dir }}"
state: stopped
when: llamacpp_compose_file.stat.exists
# 3. Manifest je VM rendern (UUID der P2000) und anwenden
- name: Namespace knaster
ansible.builtin.command: kubectl apply -f {{ llamacpp_k3s_namespace }}
register: llamacpp_ns
changed_when: "'created' in llamacpp_ns.stdout"
delegate_to: localhost
become: false
- name: Deployment anwenden
ansible.builtin.command:
cmd: kubectl apply -f -
stdin: "{{ lookup('template', llamacpp_k3s_template) }}"
vars:
llamacpp_k3s_containers: "{{ llamacpp_profiles | map('extract', llamacpp_k3s_services) | flatten }}"
register: llamacpp_apply
changed_when: "'configured' in llamacpp_apply.stdout or 'created' in llamacpp_apply.stdout"
delegate_to: localhost
become: false
- name: Warten, bis der Pod bereit ist
ansible.builtin.command: kubectl -n knaster rollout status deployment/llamacpp-{{ inventory_hostname }} --timeout=600s
changed_when: false
delegate_to: localhost
become: false
# 4. Prüfen in der VM: Dienste gesund, Prozesse nur auf der P2000
- name: Gesundheit in der VM prüfen
ansible.builtin.uri:
url: "http://127.0.0.1:{{ item.port }}/health"
status_code: 200
loop: "{{ llamacpp_profiles | map('extract', llamacpp_k3s_services) | flatten }}"
loop_control:
label: "{{ item.name }}:{{ item.port }}"
- name: Belegung der Karten
ansible.builtin.command: nvidia-smi --query-gpu=index,name,memory.used,memory.total --format=csv,noheader
register: llamacpp_vram
changed_when: false
- name: Ergebnis
ansible.builtin.debug:
msg: "{{ llamacpp_vram.stdout_lines }}"
@@ -1,11 +1,7 @@
# 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)"
- name: Profile, Karten, Modelle
ansible.builtin.include_tasks: prepare.yml
# Compose baut nur mit buildx (BuildKit, auch für den Git-Kontext mit Commit-Pin)
- name: docker-buildx installieren
@@ -13,37 +9,6 @@
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:
@@ -0,0 +1,39 @@
# Gemeinsam für Compose (main.yml) und k3s (k3s.yml): Profile prüfen, P2000 per Name finden, Modelle laden.
- 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)"
# --- 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 }}"