Compare commits
2 Commits
24735f7e5c
...
5c0df8c73c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c0df8c73c | ||
|
|
5cf4468754 |
@@ -190,38 +190,6 @@ llm_router_nomic_ctx_size: 8192
|
|||||||
llm_router_nomic_batch_size: 4096
|
llm_router_nomic_batch_size: 4096
|
||||||
llm_router_nomic_ubatch_size: 4096
|
llm_router_nomic_ubatch_size: 4096
|
||||||
|
|
||||||
# --- Monitoring: VRAM exporter + Prometheus scrape + Grafana dashboard -------
|
|
||||||
# Phase 3: GPU/LLM monitoring deployment (Wong, 2026-08-18)
|
|
||||||
# Provides: VRAM textfile exporter, Prometheus scrape config for llama-swap
|
|
||||||
# /metrics endpoint, Grafana 6-panel dashboard, PrometheusRule alert rules.
|
|
||||||
#
|
|
||||||
# Ref: roles/llm-inference-multimodel/references/monitoring-llm-homelab-ciro-luciotta-2026.md
|
|
||||||
llm_monitoring_enabled: true # gate for monitoring tasks
|
|
||||||
llm_vram_exporter_script: /opt/llama-server-monitoring/nvidia-smi-vram-exporter.sh
|
|
||||||
llm_vram_exporter_cron_minute: "*" # run every minute
|
|
||||||
llm_vram_exporter_gpu_index: 0 # GPU 0 (RTX 3090 on astro-orbiter)
|
|
||||||
llm_vram_textfile_dir: /var/lib/node_exporter/textfile_collector
|
|
||||||
|
|
||||||
# Alert thresholds (per Ciro Luciotta pattern)
|
|
||||||
llm_vram_critical_mib: 24000 # ~90% of 24GB RTX 3090
|
|
||||||
llm_kv_cache_spill_ratio: 0.92 # KV-cache spill threshold
|
|
||||||
llm_throughput_baseline_tokens_per_min: 50 # baseline for degradation alert
|
|
||||||
|
|
||||||
# Grafana dashboard
|
|
||||||
llm_grafana_dashboard_uid: llama-swap-monitor
|
|
||||||
llm_grafana_dashboard_title: "llama-swap GPU/LLM Monitoring"
|
|
||||||
llm_grafana_dashboard_tags:
|
|
||||||
- llm
|
|
||||||
- llama-swap
|
|
||||||
- gpu-monitoring
|
|
||||||
- ciro-luciotta
|
|
||||||
llm_grafana_dashboard_refresh: "30s"
|
|
||||||
llm_grafana_dashboard_time_from: "now-24h"
|
|
||||||
|
|
||||||
# Prometheus scrape job
|
|
||||||
llm_prometheus_scrape_interval: "30s"
|
|
||||||
llm_prometheus_scrape_timeout: "10s"
|
|
||||||
|
|
||||||
# --- llama-swap mode (port 8001) -----------------------------------------------
|
# --- llama-swap mode (port 8001) -----------------------------------------------
|
||||||
# Deploy llama-swap — Go-based hot-swap proxy (v250+) for model orchestration.
|
# Deploy llama-swap — Go-based hot-swap proxy (v250+) for model orchestration.
|
||||||
# Replaces router mode entirely: single binary + YAML config.json, no --models-preset INI.
|
# Replaces router mode entirely: single binary + YAML config.json, no --models-preset INI.
|
||||||
@@ -317,6 +285,35 @@ llm_swapmode_models:
|
|||||||
sleep_idle_seconds: -1 # never idle (always ready for embeddings)
|
sleep_idle_seconds: -1 # never idle (always ready for embeddings)
|
||||||
load_on_startup: true
|
load_on_startup: true
|
||||||
|
|
||||||
|
# t_c5cef2b2 / t_664289a0 (2026-08-19): Qwen3-8B dual-thinking deployment.
|
||||||
|
# Both variants point to the same GGUF. GPU-resident (~5.2GB each).
|
||||||
|
# Cannot co-reside with Qwen3.8-27B-Q4_K_M; LRU eviction applies.
|
||||||
|
# chat_template_file for no_think variant: {{ llm_models_dir }}/templates/qwen3-no-think.jinja
|
||||||
|
- id: Qwen3-8B-Q4_K_M
|
||||||
|
gguf_path: "{{ llm_models_dir }}/Qwen3-8B-Q4_K_M.gguf"
|
||||||
|
port: 8106
|
||||||
|
n_gpu_layers: 99 # GPU-resident (thinking variant)
|
||||||
|
ctx_size: 32768
|
||||||
|
batch_size: 4096
|
||||||
|
ubatch_size: 4096
|
||||||
|
parallel: 1
|
||||||
|
cache_type: q4_0
|
||||||
|
flash_attn: "true"
|
||||||
|
sleep_idle_seconds: 60 # idle after 60s no requests
|
||||||
|
|
||||||
|
- id: Qwen3-8B-Q4_K_M-no_think
|
||||||
|
gguf_path: "{{ llm_models_dir }}/Qwen3-8B-Q4_K_M.gguf"
|
||||||
|
port: 8107
|
||||||
|
n_gpu_layers: 99 # GPU-resident (no-think variant)
|
||||||
|
ctx_size: 32768
|
||||||
|
batch_size: 4096
|
||||||
|
ubatch_size: 4096
|
||||||
|
parallel: 1
|
||||||
|
cache_type: q4_0
|
||||||
|
flash_attn: "true"
|
||||||
|
sleep_idle_seconds: 60
|
||||||
|
chat_template_file: "{{ llm_models_dir }}/templates/qwen3-no-think.jinja"
|
||||||
|
|
||||||
# llama-swap matrix routing configuration
|
# llama-swap matrix routing configuration
|
||||||
# Each row defines a set of models that can be co-resident and hot-swappable
|
# Each row defines a set of models that can be co-resident and hot-swappable
|
||||||
# Syntax: "model1 & model2" = both models in same row (via v250 expression DSL)
|
# Syntax: "model1 & model2" = both models in same row (via v250 expression DSL)
|
||||||
@@ -335,3 +332,14 @@ llm_swapmode_matrix_rows:
|
|||||||
|
|
||||||
- row: row4
|
- row: row4
|
||||||
expr: "Phi-3.5-mini-instruct-Q8_0 & nomic-embed-text-v1.5" # Mini + embed
|
expr: "Phi-3.5-mini-instruct-Q8_0 & nomic-embed-text-v1.5" # Mini + embed
|
||||||
|
|
||||||
|
# t_c5cef2b2 / t_664289a0 (2026-08-19): Qwen3-8B dual-thinking rows.
|
||||||
|
# Both Qwen3-8B variants co-reside with nomic-embed but NOT with
|
||||||
|
# Qwen3.8-27B-Q4_K_M (17.8GB). LRU eviction swaps between primary and
|
||||||
|
# Qwen3-8B when needed. They CAN co-reside with each other (~10.4GB total)
|
||||||
|
# but NOT simultaneously with Qwen3.8-27B.
|
||||||
|
- row: row5
|
||||||
|
expr: "Qwen3-8B-Q4_K_M & nomic-embed-text-v1.5" # Thinking variant + embed
|
||||||
|
|
||||||
|
- row: row6
|
||||||
|
expr: "Qwen3-8B-Q4_K_M-no_think & nomic-embed-text-v1.5" # No-think variant + embed
|
||||||
|
|||||||
@@ -74,3 +74,30 @@
|
|||||||
loop_control:
|
loop_control:
|
||||||
loop_var: staged_model
|
loop_var: staged_model
|
||||||
tags: [models]
|
tags: [models]
|
||||||
|
|
||||||
|
# --- Chat template overrides ---------------------------------------------------
|
||||||
|
# Deploy per-model chat template files used by llama-server via chat-template-file.
|
||||||
|
# These are static files dropped into {{ llm_models_dir }}/templates/.
|
||||||
|
# t_664289a0: qwen3-no-think.jinja — Qwen3 template with enable_thinking=false
|
||||||
|
# hardcoded. Used by [Qwen3-8B-Q4_K_M-no_think] in the router preset INI and
|
||||||
|
# the llama-swap config. The companion [Qwen3-8B-Q4_K_M] section uses the GGUF's
|
||||||
|
# baked-in template (thinking ON by default).
|
||||||
|
- name: Ensure chat template directory exists at {{ llm_models_dir }}/templates
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ llm_models_dir }}/templates"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ llm_service_user }}"
|
||||||
|
group: "{{ llm_service_user }}"
|
||||||
|
mode: "0755"
|
||||||
|
become: true
|
||||||
|
tags: [models, chat_templates]
|
||||||
|
|
||||||
|
- name: Deploy qwen3-no-think.jinja (thinking=false hard-switch for Qwen3-8B no_think variant)
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: qwen3-no-think.jinja.j2
|
||||||
|
dest: "{{ llm_models_dir }}/templates/qwen3-no-think.jinja"
|
||||||
|
owner: "{{ llm_service_user }}"
|
||||||
|
group: "{{ llm_service_user }}"
|
||||||
|
mode: "0644"
|
||||||
|
become: true
|
||||||
|
tags: [models, chat_templates]
|
||||||
|
|||||||
@@ -241,12 +241,12 @@
|
|||||||
when: llm_swapmode_enabled | default(false)
|
when: llm_swapmode_enabled | default(false)
|
||||||
tags: [swapmode_verify]
|
tags: [swapmode_verify]
|
||||||
|
|
||||||
- name: "[swapmode_verify] Assert all 5 models are discoverable"
|
- name: "[swapmode_verify] Assert all 7 models are discoverable"
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- llm_swapmode_models_list.json.data | map(attribute='id') | list | length == 5
|
- llm_swapmode_models_list.json.data | map(attribute='id') | list | length == 7
|
||||||
fail_msg: >-
|
fail_msg: >-
|
||||||
Expected 5 models in /v1/models response, got {{ llm_swapmode_models_list.json.data | length }}.
|
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 }}
|
Models: {{ llm_swapmode_models_list.json.data | map(attribute='id') | list }}
|
||||||
when: llm_swapmode_enabled | default(false)
|
when: llm_swapmode_enabled | default(false)
|
||||||
tags: [swapmode_verify]
|
tags: [swapmode_verify]
|
||||||
@@ -300,5 +300,6 @@
|
|||||||
✓ GATE 4: VRAM guard ({{ llm_swapmode_vram_used.stdout }} MiB < {{ llm_swapmode_vram_max_mib }} MiB) 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 }}/
|
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)
|
when: llm_swapmode_enabled | default(false)
|
||||||
tags: [swapmode_verify]
|
tags: [swapmode_verify]
|
||||||
|
|||||||
@@ -207,3 +207,56 @@ rope-scaling = yarn
|
|||||||
rope-freq-scale = 0.75
|
rope-freq-scale = 0.75
|
||||||
load-on-startup = true
|
load-on-startup = true
|
||||||
sleep-idle-seconds = -1
|
sleep-idle-seconds = -1
|
||||||
|
|
||||||
|
; --- Auxiliary model: Qwen3-8B-Q4_K_M (thinking variant) ----------------------
|
||||||
|
; GGUF: bartowski/Qwen_Qwen3-8B-GGUF (5,027,784,224 bytes)
|
||||||
|
; Thinking mode: ON BY DEFAULT (Qwen3 baked-in template, no override).
|
||||||
|
; - Use for complex aux tasks: long-context summarization, multi-step tool
|
||||||
|
; planning, structured extraction requiring CoT.
|
||||||
|
; - To suppress thinking at request time, send /no_think prefix in the prompt
|
||||||
|
; OR route to [Qwen3-8B-Q4_K_M-no_think] section below.
|
||||||
|
; n-gpu-layers=99: GPU-resident (~5.2GB VRAM). Cannot co-reside with
|
||||||
|
; Qwen3.8-27B-Q4_K_M (17.8GB). LRU eviction handles swapping.
|
||||||
|
; ctx-size=32768: 32K context, q4_0 KV cache.
|
||||||
|
; flash-attn=true: required for Qwen3 architecture at this context size.
|
||||||
|
; sleep-idle-seconds=60: evict after 60s idle (free VRAM for primary model).
|
||||||
|
; Added: t_c5cef2b2 (2026-08-19, War Machine) — initial Qwen3-8B deployment.
|
||||||
|
[Qwen3-8B-Q4_K_M]
|
||||||
|
model = {{ llm_models_dir }}/Qwen3-8B-Q4_K_M.gguf
|
||||||
|
n-gpu-layers = 99
|
||||||
|
ctx-size = 32768
|
||||||
|
flash-attn = true
|
||||||
|
cache-type-k = {{ llm_router_cache_type_k }}
|
||||||
|
cache-type-v = {{ llm_router_cache_type_v }}
|
||||||
|
batch-size = {{ llm_router_batch_size }}
|
||||||
|
ubatch-size = {{ llm_router_ubatch_size }}
|
||||||
|
parallel = {{ llm_router_parallel }}
|
||||||
|
sleep-idle-seconds = 60
|
||||||
|
|
||||||
|
; --- Auxiliary model: Qwen3-8B-Q4_K_M (no-think variant) ----------------------
|
||||||
|
; Secondary section serving the SAME GGUF with enable_thinking=False via an
|
||||||
|
; overridden Jinja2 template. This gives low-latency, non-reasoning inference
|
||||||
|
; for latency-sensitive aux tasks (intent classification, query rewriting,
|
||||||
|
; structured extraction, tool-call construction, draft generation).
|
||||||
|
;
|
||||||
|
; Both sections point to the same .gguf file — llama-server spawns independent
|
||||||
|
; child processes. VRAM cost: ~5.2GB per instance (~10.4GB total). Fits on
|
||||||
|
; RTX 3090 24GB alongside nomic-embed (84MB).
|
||||||
|
;
|
||||||
|
; Thinking variant (Qwen3-8B-Q4_K_M) remains available for complex tasks that
|
||||||
|
; benefit from CoT (long-context summarization, multi-step tool planning).
|
||||||
|
;
|
||||||
|
; Cannot co-reside with Qwen3.8-27B-Q4_K_M (17.8GB); LRU eviction applies.
|
||||||
|
; Added: t_664289a0 (2026-08-19, War Machine) — dual thinking deployment.
|
||||||
|
[Qwen3-8B-Q4_K_M-no_think]
|
||||||
|
model = {{ llm_models_dir }}/Qwen3-8B-Q4_K_M.gguf
|
||||||
|
n-gpu-layers = 99
|
||||||
|
ctx-size = 32768
|
||||||
|
flash-attn = true
|
||||||
|
cache-type-k = {{ llm_router_cache_type_k }}
|
||||||
|
cache-type-v = {{ llm_router_cache_type_v }}
|
||||||
|
batch-size = {{ llm_router_batch_size }}
|
||||||
|
ubatch-size = {{ llm_router_ubatch_size }}
|
||||||
|
parallel = {{ llm_router_parallel }}
|
||||||
|
chat-template-file = {{ llm_models_dir }}/templates/qwen3-no-think.jinja
|
||||||
|
sleep-idle-seconds = 60
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ models:
|
|||||||
--parallel {{ model.parallel }}
|
--parallel {{ model.parallel }}
|
||||||
{% if model.cache_type is defined %}--cache-type-k {{ model.cache_type }} --cache-type-v {{ model.cache_type }}{% endif %}
|
{% if model.cache_type is defined %}--cache-type-k {{ model.cache_type }} --cache-type-v {{ model.cache_type }}{% endif %}
|
||||||
{% if model.flash_attn is defined %}--flash-attn {{ model.flash_attn }}{% endif %}
|
{% if model.flash_attn is defined %}--flash-attn {{ model.flash_attn }}{% endif %}
|
||||||
|
{% if model.chat_template_file is defined %}--chat-template-file {{ model.chat_template_file }}{% endif %}
|
||||||
{% if model.sleep_idle_seconds is defined %}--sleep-idle-seconds {{ model.sleep_idle_seconds }}{% endif %}
|
{% if model.sleep_idle_seconds is defined %}--sleep-idle-seconds {{ model.sleep_idle_seconds }}{% endif %}
|
||||||
{% if model.load_on_startup is defined and model.load_on_startup %}--load-on-startup{% endif %}
|
{% if model.load_on_startup is defined and model.load_on_startup %}--load-on-startup{% endif %}
|
||||||
--host 127.0.0.1
|
--host 127.0.0.1
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
{#
|
||||||
|
FILE: roles/llm-inference-multimodel/templates/qwen3-no-think.jinja.j2
|
||||||
|
DESCRIPTION: Qwen3 chat template with enable_thinking unconditionally false.
|
||||||
|
|
||||||
|
This is a chat-template FILE deployed to {{ llm_models_dir }}/templates/qwen3-no-think.jinja
|
||||||
|
on astro-orbiter and referenced via chat-template-file in the INI preset for
|
||||||
|
[Qwen3-8B-Q4_K_M-no_think]. The [Qwen3-8B-Q4_K_M] section uses the model's
|
||||||
|
baked-in default template (thinking ON by default).
|
||||||
|
|
||||||
|
Mechanism: at the add_generation_prompt step, instead of checking
|
||||||
|
"enable_thinking is defined and enable_thinking is false"
|
||||||
|
we UNCONDITIONALLY emit the empty <think></think> prefix that suppresses CoT.
|
||||||
|
This is the hard-switch documented in the Qwen3 template spec and confirmed in
|
||||||
|
https://huggingface.co/blog/qwen-3-chat-template-deep-dive (section 1).
|
||||||
|
|
||||||
|
All other logic is identical to /opt/llama.cpp/models/templates/Qwen-Qwen3-0.6B.jinja
|
||||||
|
(the shipped template for Qwen3). Only the final add_generation_prompt block differs.
|
||||||
|
|
||||||
|
Added: t_664289a0 (2026-08-19, War Machine) — dual thinking deployment.
|
||||||
|
#}
|
||||||
|
{%- if tools %}
|
||||||
|
{{- '<|im_start|>system\n' }}
|
||||||
|
{%- if messages[0].role == 'system' %}
|
||||||
|
{{- messages[0].content + '\n\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
|
||||||
|
{%- for tool in tools %}
|
||||||
|
{{- "\n" }}
|
||||||
|
{{- tool | tojson }}
|
||||||
|
{%- endfor %}
|
||||||
|
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
|
||||||
|
{%- else %}
|
||||||
|
{%- if messages[0].role == 'system' %}
|
||||||
|
{{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
||||||
|
{%- for message in messages[::-1] %}
|
||||||
|
{%- set index = (messages|length - 1) - loop.index0 %}
|
||||||
|
{%- if ns.multi_step_tool and message.role == "user" and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
|
||||||
|
{%- set ns.multi_step_tool = false %}
|
||||||
|
{%- set ns.last_query_index = index %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- for message in messages %}
|
||||||
|
{%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
|
||||||
|
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
|
||||||
|
{%- elif message.role == "assistant" %}
|
||||||
|
{%- set content = message.content %}
|
||||||
|
{%- set reasoning_content = '' %}
|
||||||
|
{%- if message.reasoning_content is defined and message.reasoning_content is not none %}
|
||||||
|
{%- set reasoning_content = message.reasoning_content %}
|
||||||
|
{%- else %}
|
||||||
|
{%- if '</think>' in message.content %}
|
||||||
|
{%- set content = message.content.split('</think>')[-1].lstrip('\n') %}
|
||||||
|
{%- set reasoning_content = message.content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if loop.index0 > ns.last_query_index %}
|
||||||
|
{%- if loop.last or (not loop.last and reasoning_content) %}
|
||||||
|
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
|
||||||
|
{%- else %}
|
||||||
|
{{- '<|im_start|>' + message.role + '\n' + content }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- else %}
|
||||||
|
{{- '<|im_start|>' + message.role + '\n' + content }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if message.tool_calls %}
|
||||||
|
{%- for tool_call in message.tool_calls %}
|
||||||
|
{%- if (loop.first and content) or (not loop.first) %}
|
||||||
|
{{- '\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if tool_call.function %}
|
||||||
|
{%- set tool_call = tool_call.function %}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '<tool_call>\n{"name": "' }}
|
||||||
|
{{- tool_call.name }}
|
||||||
|
{{- '", "arguments": ' }}
|
||||||
|
{%- if tool_call.arguments is string %}
|
||||||
|
{{- tool_call.arguments }}
|
||||||
|
{%- else %}
|
||||||
|
{{- tool_call.arguments | tojson }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '}\n</tool_call>' }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- elif message.role == "tool" %}
|
||||||
|
{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
|
||||||
|
{{- '<|im_start|>user' }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '\n<tool_response>\n' }}
|
||||||
|
{{- message.content }}
|
||||||
|
{{- '\n</tool_response>' }}
|
||||||
|
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- if add_generation_prompt %}
|
||||||
|
{{- '<|im_start|>assistant\n' }}
|
||||||
|
{{- '<think>\n\n</think>\n\n' }}
|
||||||
|
{%- endif %}
|
||||||
Reference in New Issue
Block a user