LLM-Server: M2-Vertical-Slice in Betrieb - Swift-Stack läuft, Inferenz nachgewiesen
Erster produktiver Start des llama.cpp-Testing-Stacks auf ghostnet-ki (31.10.2026). Fix-Liste aus den Live-Läufen: - Quadlet-Exec ohne Binärnamen (AMD-Image hat ENTRYPOINT /app/llama-server; GNAI-010 v0.6 dokumentiert den Befund) - Deterministischer daemon-reload vor Unit-Zugriff (Handler allein griff nicht, wenn ein vorheriger Lauf die Quadlets gerendert hatte) - Netz-Abhängigkeit Wants/After im Container-Quadlet (Netzwerk-Unit wurde nie mitgestartet -> network not found) - Stopp-Bedingung für Profilwechsel korrigiert (nur aktive Units stoppen) - Health-Tool-Kette curl/wget/bash-/dev/tcp mit Pfad-Normalisierung; curl -f als Gesundheitssignal (Statuszeile stand nie im stdout) - chdir für alle runuser-Aufrufe der Health-Checks Nachweise: Health 200 OK, /v1/models Alias korrekt, Idempotenz changed=0, VRAM 12,6/13,0 GiB von je 16 GiB auf beiden Karten (Layer-Split 1:1), echte Chat-Completion mit System-Fingerprint des gepinnten Builds, Reasoning getrennt von der Antwort. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
| 2026-09-21 | 0.2 | spektr / Codex | Unsloth UD-Q6_K als zweites installiertes Modellprofil und kontrollierten Profilwechsel für A/B-Tests ergänzt |
|
||||
| 2026-09-21 | 0.1 | spektr / Codex | erstes technisches Profil für Swift Q6_K, 128k, Q8-KV, rootless Quadlet und gestufte MTP-Abnahme erstellt |
|
||||
| 2026-10-31 | 0.5 | spektr / Mistral Vibe | Swift-Repository-Revision aktualisiert: `eb0e3a7…` wurde vom Herausgeber aus dem Repository entfernt (resolve liefert 404); Repin auf den aktuellen `main`-Commit `98a6100…`. Die Q6_K-Datei ist laut Herausgeber-SHA256SUMS unverändert (`7f4de8…`), der Vision-Projektor ebenfalls (`daa111…`); beide Prüfsummen bleiben unverändert und werden beim Download verifiziert. |
|
||||
| 2026-10-31 | 0.6 | spektr / Mistral Vibe | Implementierungsbefund vom ersten Live-Start: Das gepinnte AMD-Image definiert `ENTRYPOINT /app/llama-server`; `Exec=` im Quadlet trägt deshalb nur die Argumente ohne Binärnamen. Das Image enthält weder `curl` noch `wget` – die Health-Prüfung darf nicht von einem im Image vorhandenen HTTP-Werkzeug ausgehen (Abschnitt 8 bestätigt). |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -86,3 +86,24 @@ inference_engine_profiles:
|
||||
|
||||
# Derived paths (do not override).
|
||||
inference_engine_quadlet_dir: "{{ inference_engine_quadlet_user_dir }}/{{ inference_engine_uid }}"
|
||||
|
||||
# Health-check commands per available tool (GNAI-010 section 8). curl -f
|
||||
# fails on HTTP error statuses, so rc == 0 is a valid health signal; the
|
||||
# bash fallback prints the status line, the wget variant fails on errors.
|
||||
inference_engine_curl_health_cmd: >-
|
||||
curl -fsS -o /dev/null http://127.0.0.1:{{ inference_engine_container_port }}/health
|
||||
inference_engine_curl_models_cmd: >-
|
||||
curl -fsS http://127.0.0.1:{{ inference_engine_container_port }}/v1/models
|
||||
inference_engine_wget_health_cmd: >-
|
||||
wget -qO- http://127.0.0.1:{{ inference_engine_container_port }}/health
|
||||
inference_engine_wget_models_cmd: >-
|
||||
wget -qO- http://127.0.0.1:{{ inference_engine_container_port }}/v1/models
|
||||
|
||||
# bash /dev/tcp fallback for health checks when the image contains neither
|
||||
# curl nor wget. printf receives the backslash escapes as literal text.
|
||||
inference_engine_bash_health_script: >-
|
||||
exec 3<>/dev/tcp/127.0.0.1:{{ inference_engine_container_port }};
|
||||
printf 'GET /health HTTP/1.0\r\n\r\n' >&3; cat <&3
|
||||
inference_engine_bash_models_script: >-
|
||||
exec 3<>/dev/tcp/127.0.0.1:{{ inference_engine_container_port }};
|
||||
printf 'GET /v1/models HTTP/1.0\r\n\r\n' >&3; cat <&3
|
||||
|
||||
@@ -234,6 +234,36 @@
|
||||
tags:
|
||||
- service
|
||||
|
||||
- name: Apply rendered quadlet changes before touching units
|
||||
# The daemon-reload handler must run before unit states are read or units
|
||||
# are started; otherwise systemd does not know the freshly rendered units
|
||||
# and `is-active` exits with rc 4 (unknown unit).
|
||||
ansible.builtin.meta: flush_handlers
|
||||
tags:
|
||||
- service
|
||||
|
||||
- name: Ensure the workload user manager knows the rendered units
|
||||
# Deterministic reload: handler notifications only fire when a quadlet
|
||||
# changed in the same run, but a previous run may have rendered units
|
||||
# without reloading (failed run) — so reload unconditionally.
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/runuser
|
||||
- --user
|
||||
- "{{ inference_engine_user }}"
|
||||
- --
|
||||
- /usr/bin/env
|
||||
- "HOME={{ inference_engine_user_home }}"
|
||||
- "XDG_RUNTIME_DIR=/run/user/{{ inference_engine_uid }}"
|
||||
- /usr/bin/systemctl
|
||||
- --user
|
||||
- daemon-reload
|
||||
chdir: "{{ inference_engine_user_home }}"
|
||||
changed_when: false
|
||||
when: not ansible_check_mode
|
||||
tags:
|
||||
- service
|
||||
|
||||
- name: Read the current state of every engine unit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
@@ -250,7 +280,7 @@
|
||||
- "{{ inference_engine_unit_names[item.key] }}"
|
||||
chdir: "{{ inference_engine_user_home }}"
|
||||
changed_when: false
|
||||
failed_when: inference_engine_unit_states.rc not in [0, 3]
|
||||
failed_when: inference_engine_unit_states.rc not in [0, 3, 4]
|
||||
check_mode: false
|
||||
loop: "{{ inference_engine_profiles | dict2items }}"
|
||||
loop_control:
|
||||
@@ -277,7 +307,7 @@
|
||||
chdir: "{{ inference_engine_user_home }}"
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- item.stdout | trim != 'active'
|
||||
- item.stdout | trim == 'active'
|
||||
- item.item.key != inference_engine_active_profile
|
||||
loop: "{{ inference_engine_unit_states.results }}"
|
||||
loop_control:
|
||||
@@ -324,7 +354,7 @@
|
||||
- "{{ inference_engine_unit_names[inference_engine_active_profile] }}"
|
||||
- /bin/sh
|
||||
- -c
|
||||
- "command -v curl || command -v wget || true"
|
||||
- "command -v curl || command -v wget || command -v bash || true"
|
||||
chdir: "{{ inference_engine_user_home }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
@@ -333,29 +363,40 @@
|
||||
tags:
|
||||
- service
|
||||
|
||||
- name: Record the HTTP client for health checks
|
||||
ansible.builtin.set_fact:
|
||||
inference_engine_http_tool: "{{ inference_engine_http_client.stdout | split('/') | last | trim }}"
|
||||
when: not ansible_check_mode
|
||||
tags:
|
||||
- service
|
||||
|
||||
- name: Require an HTTP client in the pinned engine image
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- inference_engine_http_client.stdout | trim in ['curl', 'wget']
|
||||
- inference_engine_http_tool in ['curl', 'wget', 'bash']
|
||||
fail_msg: >-
|
||||
The pinned engine image contains neither curl nor wget; health checks
|
||||
cannot run (GNAI-010 section 8 requires checking the image tool first).
|
||||
The pinned engine image contains none of curl, wget or bash; health
|
||||
checks cannot run (GNAI-010 section 8 requires checking the image
|
||||
tool first; confirmed for this image 31.10.2026).
|
||||
when: not ansible_check_mode
|
||||
tags:
|
||||
- service
|
||||
|
||||
- name: Wait for the engine health endpoint
|
||||
ansible.builtin.command:
|
||||
argv: "{{ [ '/usr/sbin/runuser', '--user', inference_engine_user, '--',
|
||||
'/usr/bin/env', 'HOME=' ~ inference_engine_user_home,
|
||||
'XDG_RUNTIME_DIR=/run/user/' ~ inference_engine_uid,
|
||||
'/usr/bin/podman', 'exec',
|
||||
inference_engine_unit_names[inference_engine_active_profile],
|
||||
'/bin/sh', '-c' ] +
|
||||
(['curl', '-fsS', health_url] if inference_engine_http_client.stdout | trim == 'curl'
|
||||
else ['wget', '-qO-', health_url]) }}"
|
||||
argv: >-
|
||||
{{ ['/usr/sbin/runuser', '--user', inference_engine_user, '--',
|
||||
'/usr/bin/env', 'HOME=' ~ inference_engine_user_home,
|
||||
'XDG_RUNTIME_DIR=/run/user/' ~ inference_engine_uid,
|
||||
'/usr/bin/podman', 'exec',
|
||||
inference_engine_unit_names[inference_engine_active_profile],
|
||||
'/bin/sh', '-c', health_cmd] }}
|
||||
chdir: "{{ inference_engine_user_home }}"
|
||||
vars:
|
||||
health_url: "http://127.0.0.1:{{ inference_engine_container_port }}/health"
|
||||
health_cmd: >-
|
||||
{{ inference_engine_curl_health_cmd if inference_engine_http_tool == 'curl'
|
||||
else inference_engine_wget_health_cmd if inference_engine_http_tool == 'wget'
|
||||
else inference_engine_bash_health_script }}
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
register: inference_engine_health
|
||||
@@ -368,16 +409,19 @@
|
||||
|
||||
- name: Verify the served model alias
|
||||
ansible.builtin.command:
|
||||
argv: "{{ [ '/usr/sbin/runuser', '--user', inference_engine_user, '--',
|
||||
'/usr/bin/env', 'HOME=' ~ inference_engine_user_home,
|
||||
'XDG_RUNTIME_DIR=/run/user/' ~ inference_engine_uid,
|
||||
'/usr/bin/podman', 'exec',
|
||||
inference_engine_unit_names[inference_engine_active_profile],
|
||||
'/bin/sh', '-c' ] +
|
||||
(['curl', '-fsS', models_url] if inference_engine_http_client.stdout | trim == 'curl'
|
||||
else ['wget', '-qO-', models_url]) }}"
|
||||
argv: >-
|
||||
{{ ['/usr/sbin/runuser', '--user', inference_engine_user, '--',
|
||||
'/usr/bin/env', 'HOME=' ~ inference_engine_user_home,
|
||||
'XDG_RUNTIME_DIR=/run/user/' ~ inference_engine_uid,
|
||||
'/usr/bin/podman', 'exec',
|
||||
inference_engine_unit_names[inference_engine_active_profile],
|
||||
'/bin/sh', '-c', models_cmd] }}
|
||||
chdir: "{{ inference_engine_user_home }}"
|
||||
vars:
|
||||
models_url: "http://127.0.0.1:{{ inference_engine_container_port }}/v1/models"
|
||||
models_cmd: >-
|
||||
{{ inference_engine_curl_models_cmd if inference_engine_http_tool == 'curl'
|
||||
else inference_engine_wget_models_cmd if inference_engine_http_tool == 'wget'
|
||||
else inference_engine_bash_models_script }}
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
register: inference_engine_models_endpoint
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
[Unit]
|
||||
Description=LLM inference engine ({{ entry.profile_key }})
|
||||
Wants={{ inference_engine_network_name }}-network.service
|
||||
After={{ inference_engine_network_name }}-network.service
|
||||
{% for other in entry.other_units %}
|
||||
Conflicts={{ other }}
|
||||
{% endfor %}
|
||||
@@ -19,7 +21,10 @@ GroupAdd=keep-groups
|
||||
Volume={{ model_store_root }}/{{ entry.profile.model_directory }}:/models:ro
|
||||
Network={{ inference_engine_network_name }}
|
||||
NetworkAlias={{ inference_engine_network_alias }}
|
||||
Exec=llama-server --host 0.0.0.0 --port {{ inference_engine_container_port }} --model /models/{{ entry.profile.model_file }} --alias {{ entry.profile.alias }} --parallel {{ entry.profile.parallel }} --ctx-size {{ entry.ctx_size }} --n-predict {{ entry.profile.n_predict }} --n-gpu-layers {{ inference_engine_common.n_gpu_layers }} --split-mode {{ inference_engine_common.split_mode }} --tensor-split {{ inference_engine_common.tensor_split }} --flash-attn {{ inference_engine_common.flash_attn }} --cache-type-k {{ inference_engine_common.cache_type_k }} --cache-type-v {{ inference_engine_common.cache_type_v }} --jinja --reasoning {{ inference_engine_common.reasoning }} --reasoning-effort {{ entry.profile.reasoning_effort }} --reasoning-budget {{ inference_engine_common.reasoning_budget }} --temp {{ entry.profile.temp }} --top-p {{ entry.profile.top_p }} --top-k {{ entry.profile.top_k }} --min-p {{ entry.profile.min_p }} --presence-penalty {{ entry.profile.presence_penalty }} --repeat-penalty {{ entry.profile.repeat_penalty }}{% if entry.profile.mtp.enabled %} --spec-type draft-mtp --spec-draft-n-max {{ entry.profile.mtp.spec_draft_n_max }}{% endif %} --no-webui --metrics
|
||||
# The pinned AMD image defines ENTRYPOINT /app/llama-server; Exec= therefore
|
||||
# carries only the arguments (observed 31.10.2026: a leading "llama-server"
|
||||
# is passed as an invalid positional argument and aborts within a second).
|
||||
Exec=--host 0.0.0.0 --port {{ inference_engine_container_port }} --model /models/{{ entry.profile.model_file }} --alias {{ entry.profile.alias }} --parallel {{ entry.profile.parallel }} --ctx-size {{ entry.ctx_size }} --n-predict {{ entry.profile.n_predict }} --n-gpu-layers {{ inference_engine_common.n_gpu_layers }} --split-mode {{ inference_engine_common.split_mode }} --tensor-split {{ inference_engine_common.tensor_split }} --flash-attn {{ inference_engine_common.flash_attn }} --cache-type-k {{ inference_engine_common.cache_type_k }} --cache-type-v {{ inference_engine_common.cache_type_v }} --jinja --reasoning {{ inference_engine_common.reasoning }} --reasoning-effort {{ entry.profile.reasoning_effort }} --reasoning-budget {{ inference_engine_common.reasoning_budget }} --temp {{ entry.profile.temp }} --top-p {{ entry.profile.top_p }} --top-k {{ entry.profile.top_k }} --min-p {{ entry.profile.min_p }} --presence-penalty {{ entry.profile.presence_penalty }} --repeat-penalty {{ entry.profile.repeat_penalty }}{% if entry.profile.mtp.enabled %} --spec-type draft-mtp --spec-draft-n-max {{ entry.profile.mtp.spec_draft_n_max }}{% endif %} --no-webui --metrics
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
|
||||
Reference in New Issue
Block a user