Merge llama-swap branch into main

#1
.env.example CHANGED
@@ -1,20 +1,56 @@
 
 
 
 
 
 
1
  MODELS_ROOT="/path/to/MODELS"
2
- PRESETS_DIR="${MODELS_ROOT}/MODELS/presets"
3
- LLAMA_DIR="${MODELS_ROOT}/MODELS/llama.cpp"
4
- MODELS_DIR="${MODELS_ROOT}/MODELS/models"
5
- RESULTS_DIR="${MODELS_ROOT}/MODELS/presets/bench-results"
6
- BENCH_STATE_DIR="${MODELS_ROOT}/MODELS/presets/.bench-state"
 
 
7
  CUDA_IMAGE="nvidia/cuda:12.9.1-devel-ubuntu24.04"
8
  BUILDER_IMAGE="llama-cuda12-builder:local"
 
 
 
 
 
 
 
9
  BUILD_CONTAINER_NAME="llama-build"
10
  SERVER_CONTAINER_NAME="llama-server"
11
  BENCH_CONTAINER_PREFIX="llama-bench"
 
 
 
 
 
12
  SERVER_HOST="0.0.0.0"
13
  SERVER_PORT="8080"
14
- CUDA_ARCHITECTURES="52;75"
15
- CUDA_FLAGS="-Wno-deprecated-gpu-targets"
16
- BENCH_REPETITIONS="3"
17
  SERVER_INTERNAL_PORT="8080"
18
- RPC_HOSTS="HOST:PORT,HOST:PORT"
 
 
 
 
 
19
  HF_TOKEN=""
20
- LLAMA_API_KEY=""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copy this file to .env and replace the placeholder values.
2
+ # Keep .env private: it may contain access tokens and API keys.
3
+
4
+ # -----------------------------------------------------------------------------
5
+ # Paths
6
+ # -----------------------------------------------------------------------------
7
  MODELS_ROOT="/path/to/MODELS"
8
+ PRESETS_DIR="${MODELS_ROOT}/presets"
9
+ LLAMA_DIR="${MODELS_ROOT}/llama.cpp"
10
+ MODELS_DIR="${MODELS_ROOT}/models"
11
+
12
+ # -----------------------------------------------------------------------------
13
+ # CUDA and Docker images
14
+ # -----------------------------------------------------------------------------
15
  CUDA_IMAGE="nvidia/cuda:12.9.1-devel-ubuntu24.04"
16
  BUILDER_IMAGE="llama-cuda12-builder:local"
17
+ CUDA_BUILD="12"
18
+ CUDA_ARCHITECTURES="52;75"
19
+ CUDA_FLAGS="-Wno-deprecated-gpu-targets"
20
+
21
+ # -----------------------------------------------------------------------------
22
+ # Container names
23
+ # -----------------------------------------------------------------------------
24
  BUILD_CONTAINER_NAME="llama-build"
25
  SERVER_CONTAINER_NAME="llama-server"
26
  BENCH_CONTAINER_PREFIX="llama-bench"
27
+ LLAMA_SWAP_CONTAINER_NAME="llama-swap"
28
+
29
+ # -----------------------------------------------------------------------------
30
+ # llama.cpp server and RPC server
31
+ # -----------------------------------------------------------------------------
32
  SERVER_HOST="0.0.0.0"
33
  SERVER_PORT="8080"
 
 
 
34
  SERVER_INTERNAL_PORT="8080"
35
+ RPC_HOSTS="HOST"
36
+ RPC_PORT="50052"
37
+
38
+ # -----------------------------------------------------------------------------
39
+ # Authentication and Hugging Face access
40
+ # -----------------------------------------------------------------------------
41
  HF_TOKEN=""
42
+ LLAMA_API_KEY=""
43
+
44
+ # -----------------------------------------------------------------------------
45
+ # llama-swap gateway
46
+ # -----------------------------------------------------------------------------
47
+ LLAMA_SWAP_VERSION="unified-cuda"
48
+ LLAMA_SWAP_IMAGE="ghcr.io/mostlygeek/llama-swap:${LLAMA_SWAP_VERSION}"
49
+ LLAMA_SWAP_PORT="8080"
50
+ LLAMA_SWAP_API_KEY="${LLAMA_API_KEY}"
51
+
52
+ # -----------------------------------------------------------------------------
53
+ # Service lifecycle
54
+ # -----------------------------------------------------------------------------
55
+ # Graceful Docker stop timeout used by server.sh and llama-swap.sh.
56
+ STOP_TIMEOUT="30"
.gitignore CHANGED
@@ -1,5 +1,7 @@
1
  __pycache__/
2
  *.log
 
 
3
 
4
  .env
5
  .bench-state/
 
1
  __pycache__/
2
  *.log
3
+ llama-swap-logs/
4
+ llama-swap-state/
5
 
6
  .env
7
  .bench-state/
README.md CHANGED
@@ -103,24 +103,116 @@ The image includes `libssl-dev`, so llama.cpp can download Hugging Face models o
103
  ### Server
104
 
105
  ```bash
106
- ./server.sh # Start llama.cpp server with preset.ini
 
 
 
 
 
107
  ```
108
 
 
 
 
 
 
109
  `server.sh` uses the prebuilt CUDA 12.9 image, exposes port `8080`, and serves both GPUs. From another computer:
110
 
111
  ```text
112
  http://<host-ip>:8080
113
  ```
114
 
115
- The server runs in router mode and loads models on demand. Set an API key before exposing it beyond a trusted LAN; current presets intentionally have no key configured.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  ## Quick Start
118
 
119
  ```bash
120
- cd /mnt/MODELS/presets
121
  ./create-build-image.sh
122
  ./build-llama.cpp.sh
123
- ./server.sh
 
 
 
 
 
 
 
 
 
 
124
  ```
125
 
126
- Then use the OpenAI-compatible API on port 8080.
 
103
  ### Server
104
 
105
  ```bash
106
+ ./server.sh # Start in the background with preset.ini
107
+ ./server.sh start # Same as above
108
+ ./server.sh start custom.ini
109
+ ./server.sh status
110
+ ./server.sh restart custom.ini
111
+ ./server.sh stop
112
  ```
113
 
114
+ `start` and `daemon` launch the Docker container in detached mode, so the
115
+ script returns while the server continues running in the background. The
116
+ container name is used for lifecycle tracking; no host PID file is needed.
117
+ The legacy form `./server.sh custom.ini` is also supported.
118
+
119
  `server.sh` uses the prebuilt CUDA 12.9 image, exposes port `8080`, and serves both GPUs. From another computer:
120
 
121
  ```text
122
  http://<host-ip>:8080
123
  ```
124
 
125
+ The server runs in router mode and loads models on demand. Set `LLAMA_API_KEY`
126
+ before exposing the legacy server beyond a trusted LAN.
127
+
128
+ ### llama-swap gateway
129
+
130
+ `llama-swap.sh` runs the five active models from `llama-swap.yaml` through the
131
+ custom CUDA build, swapping the corresponding `llama-server` process on demand:
132
+
133
+ - `Qwen3.6-35B-MTP`
134
+ - `Ornith-1.5-35B-A3B-MTP`
135
+ - `gemma-4-12B-it-qat`
136
+ - `gemma-4-26B-A4B-it-qat-MTP`
137
+ - `Qwopus3.6-35B-A3B-Coder-MTP`
138
+
139
+ On the target server, from the presets directory configured in `.env`:
140
+
141
+ ```bash
142
+ ./llama-swap.sh start
143
+ ./llama-swap.sh status
144
+ ./llama-swap.sh restart
145
+ ./llama-swap.sh stop
146
+ ```
147
+
148
+ `./llama-swap.sh` and `./llama-swap.sh daemon` are aliases for `start`. The
149
+ gateway runs as a detached Docker container and remains active after the
150
+ shell script exits.
151
+
152
+ The gateway is available at `http://<server-host>:<port>/v1`. Its API key is read
153
+ from `LLAMA_API_KEY`; the key is passed to llama-swap only, while the upstream
154
+ llama-server processes remain bound to the container loopback interface. Each
155
+ model receives an available internal port from llama-swap, and the proxy uses
156
+ that same assigned port.
157
+
158
+ Authentication is API-key based; there is no separate llama-swap username or
159
+ password. The web UI and API endpoints require the key:
160
+
161
+ - In a browser prompt, enter any non-empty username and use the value of
162
+ `LLAMA_API_KEY` as the password.
163
+ - For OpenAI-compatible clients, set the base URL to
164
+ `http://<server-host>:<port>/v1` and use the same value as the API key. The client
165
+ should send it as `Authorization: Bearer <API key>`.
166
+ - `http://<server-host>:<port>/health` is unauthenticated and returns `OK`.
167
+
168
+ The derived single-model presets are in `llama-swap-presets/`, and model logs
169
+ are written to `${PRESETS_DIR}/llama-swap-logs/`. Activity history and metrics
170
+ are stored in `${PRESETS_DIR}/llama-swap-state/activity.sqlite`, so the
171
+ llama-swap Activity page retains previous requests and runs across restarts.
172
+
173
+ The state directory is created automatically by `llama-swap.sh` and is mounted
174
+ into the container through the existing `/presets` volume. The currently loaded
175
+ model process is still intentionally restarted and must be loaded again after a
176
+ container restart; only historical activity and metrics are persisted.
177
+
178
+ Models are automatically unloaded after 30 minutes of inactivity because
179
+ `globalTTL` is set to `1800` seconds. Increase this value to reduce reloads, or
180
+ set it to `0` to keep the active model loaded indefinitely.
181
+
182
+ The gateway is configured with an exclusive `single-model` group. The Pi agent
183
+ selects the model by sending its exact model ID in each OpenAI-compatible
184
+ request. When that ID changes, llama-swap unloads the current model before
185
+ starting the requested one; the Pi agent must not call `/models/sse`.
186
+
187
+ To discover the available IDs, call `GET /v1/models` with the API key. A chat
188
+ request should use the selected ID in its `model` field:
189
+
190
+ ```json
191
+ {
192
+ "model": "Qwen3.6-35B-MTP",
193
+ "messages": [
194
+ {"role": "user", "content": "Hello"}
195
+ ]
196
+ }
197
+ ```
198
 
199
  ## Quick Start
200
 
201
  ```bash
202
+ cd <presets-directory>
203
  ./create-build-image.sh
204
  ./build-llama.cpp.sh
205
+ ./server.sh start
206
+ ./server.sh status
207
+ ```
208
+
209
+ Then use the OpenAI-compatible API on port 8080. Run `server.sh` and
210
+ `llama-swap.sh` separately; they both use port `8080` by default. To use the
211
+ gateway instead, stop the standalone server and run:
212
+
213
+ ```bash
214
+ ./server.sh stop
215
+ ./llama-swap.sh start
216
  ```
217
 
218
+ Then use the authenticated gateway at `http://<server-host>:<port>/v1`.
docker-compose.yml CHANGED
@@ -20,10 +20,10 @@ services:
20
  bash -c '
21
  set -euo pipefail
22
  git config --global --add safe.directory /src
23
- cmake -S /src -B /src/build-${CUDA_BUILD} \
24
  -DGGML_CUDA=ON -DGGML_CUDA_GRAPH=ON -DGGML_VULKAN=ON \
25
  -DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCHITECTURES}" \
26
  -DCMAKE_CUDA_FLAGS="${CUDA_FLAGS}" \
27
  -DBUILD_SHARED_LIBS=OFF -DGGML_RPC=ON
28
- cmake --build /src/build-${CUDA_BUILD} --config Release -j$$(nproc)
29
  '
 
20
  bash -c '
21
  set -euo pipefail
22
  git config --global --add safe.directory /src
23
+ cmake -S /src -B /src/build-${CUDA_BUILD:-12} \
24
  -DGGML_CUDA=ON -DGGML_CUDA_GRAPH=ON -DGGML_VULKAN=ON \
25
  -DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCHITECTURES}" \
26
  -DCMAKE_CUDA_FLAGS="${CUDA_FLAGS}" \
27
  -DBUILD_SHARED_LIBS=OFF -DGGML_RPC=ON
28
+ cmake --build /src/build-${CUDA_BUILD:-12} --config Release -j$$(nproc)
29
  '
llama-swap-presets/gemma-4-12b-it-qat.ini ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ [*]
4
+ fa = on
5
+ jinja = true
6
+ metrics = false
7
+ reasoning = on
8
+ reasoning-budget-message = "OK, I've thought long enough. Let's answer."
9
+ np = 1
10
+ c = 131072
11
+ image-min-tokens = 1024
12
+ device = CUDA0,CUDA1
13
+ split-mode = layer
14
+ main-gpu = 0
15
+ device-draft = CUDA1
16
+ mmproj-device = CUDA1
17
+ models-max = 1
18
+
19
+ [gemma-4-12B-it-qat]
20
+ hf = unsloth/gemma-4-12B-it-qat-GGUF:UD-Q4_K_XL
21
+ spec-type = draft-mtp
22
+ spec-draft-n-max = 2
23
+ load-mode = none
24
+ lazy-mode = auto
llama-swap-presets/gemma-4-26b-a4b-it-qat-mtp.ini ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ [*]
4
+ fa = on
5
+ jinja = true
6
+ metrics = false
7
+ reasoning = on
8
+ reasoning-budget-message = "OK, I've thought long enough. Let's answer."
9
+ np = 1
10
+ c = 131072
11
+ image-min-tokens = 1024
12
+ device = CUDA0,CUDA1
13
+ split-mode = layer
14
+ main-gpu = 0
15
+ device-draft = CUDA1
16
+ mmproj-device = CUDA1
17
+ models-max = 1
18
+
19
+ [gemma-4-26B-A4B-it-qat-MTP]
20
+ hf = unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_K_XL
21
+ spec-type = draft-mtp
22
+ spec-draft-n-max = 2
23
+ load-mode = none
24
+ lazy-mode = auto
llama-swap-presets/ornith-1.5-35b-a3b-mtp.ini ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ [*]
4
+ fa = on
5
+ jinja = true
6
+ metrics = false
7
+ reasoning = on
8
+ reasoning-budget-message = "OK, I've thought long enough. Let's answer."
9
+ np = 1
10
+ c = 131072
11
+ image-min-tokens = 1024
12
+ device = CUDA0,CUDA1
13
+ split-mode = layer
14
+ main-gpu = 0
15
+ device-draft = CUDA1
16
+ mmproj-device = CUDA1
17
+ models-max = 1
18
+
19
+ [Ornith-1.5-35B-A3B-MTP]
20
+ hf = ornith-ai/Ornith-1.5-35B-A3B-GGUF:Q4_K_M
21
+ temp = 0.85
22
+ top-p = 0.95
23
+ top-k = 40
24
+ min-p = 0.05
25
+ repeat-penalty = 1.1
26
+ presence-penalty = 0.0
27
+ spec-type = draft-mtp
28
+ spec-draft-n-max = 2
29
+ load-mode = none
30
+ lazy-mode = auto
31
+ device = CUDA0
32
+ split-mode = layer
33
+ main-gpu = 0
llama-swap-presets/qwen3.6-35b-mtp.ini ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ [*]
4
+ fa = on
5
+ jinja = true
6
+ metrics = false
7
+ reasoning = on
8
+ reasoning-budget-message = "OK, I've thought long enough. Let's answer."
9
+ np = 1
10
+ c = 131072
11
+ image-min-tokens = 1024
12
+ device = CUDA0,CUDA1
13
+ split-mode = layer
14
+ main-gpu = 0
15
+ device-draft = CUDA1
16
+ mmproj-device = CUDA1
17
+ models-max = 1
18
+
19
+ [Qwen3.6-35B-MTP]
20
+ hf = unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL
21
+ temp = 0.6
22
+ top-p = 0.95
23
+ top-k = 20
24
+ presence-penalty = 0.0
25
+ min-p = 0.00
26
+ repeat-penalty = 1.0
27
+ spec-type = draft-mtp
28
+ spec-draft-n-max = 2
29
+ device = CUDA0
30
+ split-mode = layer
31
+ main-gpu = 0
32
+ load-mode = none
33
+ lazy-mode = auto
llama-swap-presets/qwopus3.5-35b-mtp.ini ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ [*]
4
+ fa = on
5
+ jinja = true
6
+ metrics = false
7
+ reasoning = on
8
+ reasoning-budget-message = "OK, I've thought long enough. Let's answer."
9
+ np = 1
10
+ c = 131072
11
+ image-min-tokens = 1024
12
+ device = CUDA0,CUDA1
13
+ split-mode = layer
14
+ main-gpu = 0
15
+ device-draft = CUDA1
16
+ mmproj-device = CUDA1
17
+ models-max = 1
18
+
19
+ [Qwopus3.6-35B-A3B-Coder-MTP-GGUF]
20
+ hf = Jackrong/Qwopus3.6-35B-A3B-Coder-MTP-GGUF:Q4_K_M
21
+ temp = 0.6
22
+ top-p = 0.95
23
+ top-k = 20
24
+ presence-penalty = 0.0
25
+ min-p = 0.00
26
+ repeat-penalty = 1.0
27
+ spec-type = draft-mtp
28
+ spec-draft-n-max = 2
29
+ device = CUDA0
30
+ split-mode = layer
31
+ main-gpu = 0
32
+ load-mode = none
33
+ lazy-mode = auto
llama-swap.sh ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ ENV_FILE="${SCRIPT_DIR}/.env"
6
+ if [[ ! -f "${ENV_FILE}" ]]; then
7
+ printf 'Missing environment file: %s\n' "${ENV_FILE}" >&2
8
+ exit 1
9
+ fi
10
+
11
+ set -a
12
+ # shellcheck disable=SC1090
13
+ source "${ENV_FILE}"
14
+ set +a
15
+
16
+ CUDA_BUILD="${CUDA_BUILD:-12}"
17
+ DOCKER=(sudo docker)
18
+
19
+ usage() {
20
+ cat <<EOF
21
+ Usage: $(basename "$0") [command]
22
+
23
+ Commands:
24
+ start Start llama-swap in the background (default)
25
+ daemon Alias for start
26
+ stop Gracefully stop llama-swap
27
+ restart Stop and start llama-swap
28
+ status Show the Docker container state and published port
29
+ EOF
30
+ }
31
+
32
+ container_state() {
33
+ local state
34
+ if state="$("${DOCKER[@]}" inspect --format='{{.State.Status}}' "${LLAMA_SWAP_CONTAINER_NAME}" 2>/dev/null)"; then
35
+ printf '%s\n' "${state}"
36
+ else
37
+ printf 'missing\n'
38
+ fi
39
+ }
40
+
41
+ remove_stale_container() {
42
+ if [[ "$(container_state)" != "missing" ]]; then
43
+ "${DOCKER[@]}" rm --force "${LLAMA_SWAP_CONTAINER_NAME}" >/dev/null
44
+ fi
45
+ }
46
+
47
+ start_llama_swap() {
48
+ : "${MODELS_ROOT:?MODELS_ROOT must be set in .env}"
49
+ : "${LLAMA_DIR:?LLAMA_DIR must be set in .env}"
50
+ : "${PRESETS_DIR:?PRESETS_DIR must be set in .env}"
51
+ : "${MODELS_DIR:?MODELS_DIR must be set in .env}"
52
+ : "${BUILDER_IMAGE:?BUILDER_IMAGE must be set in .env}"
53
+ : "${LLAMA_SWAP_CONTAINER_NAME:?LLAMA_SWAP_CONTAINER_NAME must be set in .env}"
54
+ : "${LLAMA_SWAP_PORT:?LLAMA_SWAP_PORT must be set in .env}"
55
+ : "${LLAMA_SWAP_API_KEY:?LLAMA_SWAP_API_KEY must be set in .env}"
56
+ : "${LLAMA_SWAP_IMAGE:?LLAMA_SWAP_IMAGE must be set in .env}"
57
+
58
+ if [[ ! -f "${PRESETS_DIR}/llama-swap.yaml" ]]; then
59
+ printf 'Missing llama-swap configuration: %s\n' "${PRESETS_DIR}/llama-swap.yaml" >&2
60
+ return 1
61
+ fi
62
+
63
+ if [[ ! -x "${LLAMA_DIR}/build-${CUDA_BUILD}/bin/llama-server" ]]; then
64
+ printf 'Missing custom llama-server: %s\n' "${LLAMA_DIR}/build-${CUDA_BUILD}/bin/llama-server" >&2
65
+ printf 'Build llama.cpp before starting llama-swap.\n' >&2
66
+ return 1
67
+ fi
68
+
69
+ if [[ "$(container_state)" == "running" ]]; then
70
+ printf '%s is already running\n' "${LLAMA_SWAP_CONTAINER_NAME}" >&2
71
+ return 1
72
+ fi
73
+
74
+ mkdir -p "${PRESETS_DIR}/llama-swap-logs" "${PRESETS_DIR}/llama-swap-state"
75
+ remove_stale_container
76
+
77
+ "${DOCKER[@]}" run --detach --rm \
78
+ --name "${LLAMA_SWAP_CONTAINER_NAME}" \
79
+ --gpus all \
80
+ --device=/dev/dri \
81
+ --ulimit memlock=-1:-1 \
82
+ --publish "${LLAMA_SWAP_PORT}:8080" \
83
+ --env "CUDA_BUILD=${CUDA_BUILD}" \
84
+ --env "HF_HOME=/models" \
85
+ --env "HF_TOKEN=${HF_TOKEN:-}" \
86
+ --env "LLAMA_SWAP_API_KEY=${LLAMA_SWAP_API_KEY}" \
87
+ --env "LLAMA_SWAP_CONFIG=/presets/llama-swap.yaml" \
88
+ --volume "${LLAMA_DIR}:/src:ro" \
89
+ --volume "${PRESETS_DIR}:/presets" \
90
+ --volume "${MODELS_DIR}:/models" \
91
+ --workdir /models \
92
+ "${LLAMA_SWAP_IMAGE}"
93
+ printf 'Started %s in the background\n' "${LLAMA_SWAP_CONTAINER_NAME}"
94
+ }
95
+
96
+ stop_llama_swap() {
97
+ local state timeout
98
+ state="$(container_state)"
99
+ timeout="${STOP_TIMEOUT:-30}"
100
+
101
+ if [[ "${state}" == "missing" ]]; then
102
+ printf '%s is not running\n' "${LLAMA_SWAP_CONTAINER_NAME}"
103
+ return 0
104
+ fi
105
+
106
+ if [[ "${state}" == "running" || "${state}" == "created" || "${state}" == "restarting" ]]; then
107
+ "${DOCKER[@]}" stop --time="${timeout}" "${LLAMA_SWAP_CONTAINER_NAME}" >/dev/null
108
+ else
109
+ remove_stale_container
110
+ fi
111
+ printf 'Stopped %s\n' "${LLAMA_SWAP_CONTAINER_NAME}"
112
+ }
113
+
114
+ status_llama_swap() {
115
+ local state port
116
+ state="$(container_state)"
117
+ if [[ "${state}" == "missing" ]]; then
118
+ printf '%s: stopped (container missing)\n' "${LLAMA_SWAP_CONTAINER_NAME}"
119
+ return 1
120
+ fi
121
+
122
+ port="$("${DOCKER[@]}" port "${LLAMA_SWAP_CONTAINER_NAME}" 8080/tcp 2>/dev/null || true)"
123
+ if [[ -n "${port}" ]]; then
124
+ printf '%s: %s (%s)\n' "${LLAMA_SWAP_CONTAINER_NAME}" "${state}" "${port}"
125
+ else
126
+ printf '%s: %s\n' "${LLAMA_SWAP_CONTAINER_NAME}" "${state}"
127
+ fi
128
+ [[ "${state}" == "running" ]]
129
+ }
130
+
131
+ if [[ $# -gt 0 ]]; then
132
+ COMMAND="$1"
133
+ else
134
+ COMMAND="start"
135
+ fi
136
+ case "${COMMAND}" in
137
+ start|daemon|restart)
138
+ if [[ $# -gt 1 ]]; then
139
+ printf 'Unexpected argument for %s: %s\n' "${COMMAND}" "$2" >&2
140
+ usage >&2
141
+ exit 2
142
+ fi
143
+ ;;
144
+ stop|status)
145
+ if [[ $# -gt 1 ]]; then
146
+ printf 'Unexpected argument for %s: %s\n' "${COMMAND}" "$2" >&2
147
+ usage >&2
148
+ exit 2
149
+ fi
150
+ ;;
151
+ help|-h|--help)
152
+ usage
153
+ exit 0
154
+ ;;
155
+ *)
156
+ printf 'Unknown command: %s\n' "${COMMAND}" >&2
157
+ usage >&2
158
+ exit 2
159
+ ;;
160
+ esac
161
+
162
+ : "${LLAMA_SWAP_CONTAINER_NAME:?LLAMA_SWAP_CONTAINER_NAME must be set in .env}"
163
+ case "${COMMAND}" in
164
+ start|daemon)
165
+ start_llama_swap
166
+ ;;
167
+ stop)
168
+ stop_llama_swap
169
+ ;;
170
+ restart)
171
+ stop_llama_swap
172
+ start_llama_swap
173
+ ;;
174
+ status)
175
+ status_llama_swap
176
+ ;;
177
+ esac
llama-swap.yaml ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/mostlygeek/llama-swap/refs/heads/main/config-schema.json
2
+ # The API key is injected by llama-swap.sh and is intentionally not stored here.
3
+ healthCheckTimeout: 1800
4
+ logLevel: info
5
+ # Store the Activity page's request history and metrics in a file-backed SQLite
6
+ # database so they survive llama-swap/container restarts.
7
+ store:
8
+ path: /presets/llama-swap-state/activity.sqlite
9
+ # Keep the active model warm for 30 minutes, then reclaim its VRAM when idle.
10
+ globalTTL: 1800
11
+ unloadTimeout: 30
12
+ globalConcurrencyLimit: 1
13
+ sendLoadingState: true
14
+ logToStdout: proxy
15
+ compat:
16
+ # ignoreWebsockets: do not let websocket connections load a stopped model
17
+ # or count toward concurrency, TTL activity, or swap decisions
18
+ # - optional, default: false
19
+ ignoreWebsockets: true
20
+ apiKeys:
21
+ - "${env.LLAMA_SWAP_API_KEY}"
22
+
23
+ # Shared command fragments keep model definitions consistent. The environment
24
+ # macro is expanded by llama-swap inside the container, and ${PORT} is assigned
25
+ # separately for each model.
26
+ macros:
27
+ llama_server: "/src/build-${env.CUDA_BUILD}/bin/llama-server"
28
+ llama_server_args: >-
29
+ --host 127.0.0.1
30
+ --port ${PORT}
31
+
32
+ groups:
33
+ single-model:
34
+ swap: true
35
+ exclusive: true
36
+ members:
37
+ - "Qwen3.6-35B-MTP"
38
+ - "Ornith-1.5-35B-A3B-MTP"
39
+ - "gemma-4-12B-it-qat"
40
+ - "gemma-4-26B-A4B-it-qat-MTP"
41
+ - "Qwopus3.6-35B-A3B-Coder-MTP"
42
+
43
+ models:
44
+ "Qwen3.6-35B-MTP":
45
+ cmd: |
46
+ ${llama_server}
47
+ ${llama_server_args}
48
+ --models-preset /presets/llama-swap-presets/qwen3.6-35b-mtp.ini
49
+ --log-file /presets/llama-swap-logs/qwen3.6-35b-mtp.log
50
+ proxy: http://127.0.0.1:${PORT}
51
+ checkEndpoint: /health
52
+ concurrencyLimit: 0
53
+ capabilities:
54
+ tools: true
55
+ disableAuto: false
56
+
57
+ "Qwopus3.6-35B-A3B-Coder-MTP":
58
+ cmd: |
59
+ ${llama_server}
60
+ ${llama_server_args}
61
+ --models-preset /presets/llama-swap-presets/qwopus3.6-35b-mtp.ini
62
+ --log-file /presets/llama-swap-logs/qwopus3.6-35b-mtp.log
63
+ proxy: http://127.0.0.1:${PORT}
64
+ checkEndpoint: /health
65
+ concurrencyLimit: 0
66
+ capabilities:
67
+ tools: true
68
+ disableAuto: false
69
+
70
+ "Ornith-1.5-35B-A3B-MTP":
71
+ cmd: |
72
+ ${llama_server}
73
+ ${llama_server_args}
74
+ --models-preset /presets/llama-swap-presets/ornith-1.5-35b-a3b-mtp.ini
75
+ --log-file /presets/llama-swap-logs/ornith-1.5-35b-a3b-mtp.log
76
+ proxy: http://127.0.0.1:${PORT}
77
+ checkEndpoint: /health
78
+ concurrencyLimit: 0
79
+ capabilities:
80
+ tools: true
81
+ disableAuto: false
82
+
83
+ "gemma-4-12B-it-qat":
84
+ cmd: |
85
+ ${llama_server}
86
+ ${llama_server_args}
87
+ --models-preset /presets/llama-swap-presets/gemma-4-12b-it-qat.ini
88
+ --log-file /presets/llama-swap-logs/gemma-4-12b-it-qat.log
89
+ proxy: http://127.0.0.1:${PORT}
90
+ checkEndpoint: /health
91
+ capabilities:
92
+ tools: true
93
+ disableAuto: false
94
+
95
+ "gemma-4-26B-A4B-it-qat-MTP":
96
+ cmd: |
97
+ ${llama_server}
98
+ ${llama_server_args}
99
+ --models-preset /presets/llama-swap-presets/gemma-4-26b-a4b-it-qat-mtp.ini
100
+ --log-file /presets/llama-swap-logs/gemma-4-26b-a4b-it-qat-mtp.log
101
+ proxy: http://127.0.0.1:${PORT}
102
+ checkEndpoint: /health
103
+ concurrencyLimit: 0
104
+ capabilities:
105
+ tools: true
106
+ disableAuto: false
preset.ini CHANGED
@@ -108,3 +108,19 @@ spec-type = draft-mtp
108
  spec-draft-n-max = 2
109
  load-mode = none
110
  lazy-mode = auto
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  spec-draft-n-max = 2
109
  load-mode = none
110
  lazy-mode = auto
111
+
112
+ [Qwopus3.6-35B-A3B-Coder-MTP]
113
+ hf = Jackrong/Qwopus3.6-35B-A3B-Coder-MTP-GGUF:Q4_K_M
114
+ temp = 0.6
115
+ top-p = 0.95
116
+ top-k = 20
117
+ presence-penalty = 0.0
118
+ min-p = 0.00
119
+ repeat-penalty = 1.0
120
+ spec-type = draft-mtp
121
+ spec-draft-n-max = 2
122
+ device = CUDA0
123
+ split-mode = layer
124
+ main-gpu = 0
125
+ load-mode = none
126
+ lazy-mode = auto
server.sh CHANGED
@@ -1,29 +1,190 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
 
3
  SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
 
 
 
 
 
 
4
  set -a
5
- source "${SCRIPT_DIR}/.env"
 
6
  set +a
7
 
8
- SRC_DIR="${LLAMA_DIR}"
9
- IMAGE="${BUILDER_IMAGE}"
10
- PRESET="${1:-"preset.ini"}"
11
-
12
- sudo docker run -d --rm --name "${SERVER_CONTAINER_NAME}" --gpus all \
13
- --device=/dev/dri \
14
- --ulimit memlock=-1:-1 \
15
- -p "${SERVER_PORT}:${SERVER_INTERNAL_PORT}" \
16
- -e "HF_HOME=/models" \
17
- -e "LLAMA_API_KEY=${LLAMA_API_KEY}" \
18
- -e "HF_TOKEN=${HF_TOKEN}" \
19
- -v "${SRC_DIR}:/src" \
20
- -v "${PRESETS_DIR}:/presets" \
21
- -v "${MODELS_DIR}:/models" \
22
- -w /models \
23
- "${IMAGE}" \
24
- /src/build-"${CUDA_BUILD}"/bin/llama-server \
25
- --host "${SERVER_HOST}" \
26
- --port "${SERVER_PORT}" \
27
- --models-preset "/presets/${PRESET}" \
28
- --log-file /presets/llama-server.log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  # --rpc "${RPC_HOSTS}:${RPC_PORT}"
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
+
4
  SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ ENV_FILE="${SCRIPT_DIR}/.env"
6
+ if [[ ! -f "${ENV_FILE}" ]]; then
7
+ printf 'Missing environment file: %s\n' "${ENV_FILE}" >&2
8
+ exit 1
9
+ fi
10
+
11
  set -a
12
+ # shellcheck disable=SC1090
13
+ source "${ENV_FILE}"
14
  set +a
15
 
16
+ CUDA_BUILD="${CUDA_BUILD:-12}"
17
+ DOCKER=(sudo docker)
18
+
19
+ usage() {
20
+ cat <<EOF
21
+ Usage: $(basename "$0") [command] [preset]
22
+
23
+ Commands:
24
+ start [preset] Start the server in the background (default)
25
+ daemon [preset] Alias for start
26
+ stop Gracefully stop the server
27
+ restart [preset] Stop and start the server
28
+ status Show the Docker container state and published port
29
+
30
+ Legacy form:
31
+ $(basename "$0") [preset]
32
+ EOF
33
+ }
34
+
35
+ container_state() {
36
+ local state
37
+ if state="$("${DOCKER[@]}" inspect --format='{{.State.Status}}' "${SERVER_CONTAINER_NAME}" 2>/dev/null)"; then
38
+ printf '%s\n' "${state}"
39
+ else
40
+ printf 'missing\n'
41
+ fi
42
+ }
43
+
44
+ remove_stale_container() {
45
+ if [[ "$(container_state)" != "missing" ]]; then
46
+ "${DOCKER[@]}" rm --force "${SERVER_CONTAINER_NAME}" >/dev/null
47
+ fi
48
+ }
49
+
50
+ start_server() {
51
+ : "${LLAMA_DIR:?LLAMA_DIR must be set in .env}"
52
+ : "${PRESETS_DIR:?PRESETS_DIR must be set in .env}"
53
+ : "${MODELS_DIR:?MODELS_DIR must be set in .env}"
54
+ : "${BUILDER_IMAGE:?BUILDER_IMAGE must be set in .env}"
55
+ : "${SERVER_HOST:?SERVER_HOST must be set in .env}"
56
+ : "${SERVER_PORT:?SERVER_PORT must be set in .env}"
57
+ : "${SERVER_INTERNAL_PORT:?SERVER_INTERNAL_PORT must be set in .env}"
58
+
59
+ if [[ "$(container_state)" == "running" ]]; then
60
+ printf '%s is already running\n' "${SERVER_CONTAINER_NAME}" >&2
61
+ return 1
62
+ fi
63
+
64
+ if [[ ! -f "${PRESETS_DIR}/${PRESET}" ]]; then
65
+ printf 'Missing server preset: %s\n' "${PRESETS_DIR}/${PRESET}" >&2
66
+ return 1
67
+ fi
68
+
69
+ if [[ ! -x "${LLAMA_DIR}/build-${CUDA_BUILD}/bin/llama-server" ]]; then
70
+ printf 'Missing custom llama-server: %s\n' \
71
+ "${LLAMA_DIR}/build-${CUDA_BUILD}/bin/llama-server" >&2
72
+ printf 'Build llama.cpp before starting the server.\n' >&2
73
+ return 1
74
+ fi
75
+
76
+ remove_stale_container
77
+
78
+ "${DOCKER[@]}" run --detach --rm \
79
+ --name "${SERVER_CONTAINER_NAME}" \
80
+ --gpus all \
81
+ --device=/dev/dri \
82
+ --ulimit memlock=-1:-1 \
83
+ --publish "${SERVER_PORT}:${SERVER_INTERNAL_PORT}" \
84
+ --env "HF_HOME=/models" \
85
+ --env "LLAMA_API_KEY=${LLAMA_API_KEY:-}" \
86
+ --env "HF_TOKEN=${HF_TOKEN:-}" \
87
+ --volume "${LLAMA_DIR}:/src" \
88
+ --volume "${PRESETS_DIR}:/presets" \
89
+ --volume "${MODELS_DIR}:/models" \
90
+ --workdir /models \
91
+ "${BUILDER_IMAGE}" \
92
+ "/src/build-${CUDA_BUILD}/bin/llama-server" \
93
+ --host "${SERVER_HOST}" \
94
+ --port "${SERVER_PORT}" \
95
+ --models-preset "/presets/${PRESET}" \
96
+ --log-file /presets/llama-server.log
97
+ printf 'Started %s in the background with preset %s\n' \
98
+ "${SERVER_CONTAINER_NAME}" "${PRESET}"
99
+ }
100
+
101
+ stop_server() {
102
+ local state timeout
103
+ state="$(container_state)"
104
+ timeout="${STOP_TIMEOUT:-30}"
105
+
106
+ if [[ "${state}" == "missing" ]]; then
107
+ printf '%s is not running\n' "${SERVER_CONTAINER_NAME}"
108
+ return 0
109
+ fi
110
+
111
+ if [[ "${state}" == "running" || "${state}" == "created" || "${state}" == "restarting" ]]; then
112
+ "${DOCKER[@]}" stop --time="${timeout}" "${SERVER_CONTAINER_NAME}" >/dev/null
113
+ else
114
+ remove_stale_container
115
+ fi
116
+ printf 'Stopped %s\n' "${SERVER_CONTAINER_NAME}"
117
+ }
118
+
119
+ status_server() {
120
+ local state port
121
+ state="$(container_state)"
122
+ if [[ "${state}" == "missing" ]]; then
123
+ printf '%s: stopped (container missing)\n' "${SERVER_CONTAINER_NAME}"
124
+ return 1
125
+ fi
126
+
127
+ port="$("${DOCKER[@]}" port "${SERVER_CONTAINER_NAME}" "${SERVER_INTERNAL_PORT}/tcp" 2>/dev/null || true)"
128
+ if [[ -n "${port}" ]]; then
129
+ printf '%s: %s (%s)\n' "${SERVER_CONTAINER_NAME}" "${state}" "${port}"
130
+ else
131
+ printf '%s: %s\n' "${SERVER_CONTAINER_NAME}" "${state}"
132
+ fi
133
+ [[ "${state}" == "running" ]]
134
+ }
135
+
136
+ if [[ $# -gt 0 ]]; then
137
+ COMMAND="$1"
138
+ else
139
+ COMMAND="start"
140
+ fi
141
+ PRESET="preset.ini"
142
+ case "${COMMAND}" in
143
+ start|daemon|restart)
144
+ if [[ $# -gt 0 ]]; then
145
+ shift
146
+ fi
147
+ PRESET="${1:-preset.ini}"
148
+ ;;
149
+ stop|status)
150
+ shift
151
+ if [[ $# -gt 0 ]]; then
152
+ printf 'Unexpected argument for %s: %s\n' "${COMMAND}" "$1" >&2
153
+ usage >&2
154
+ exit 2
155
+ fi
156
+ ;;
157
+ help|-h|--help)
158
+ usage
159
+ exit 0
160
+ ;;
161
+ *)
162
+ # Preserve the original ./server.sh [preset] interface.
163
+ PRESET="${COMMAND}"
164
+ COMMAND="start"
165
+ ;;
166
+ esac
167
+
168
+ if [[ ("${COMMAND}" == "start" || "${COMMAND}" == "daemon" || "${COMMAND}" == "restart") && $# -gt 1 ]]; then
169
+ printf 'Too many arguments for %s\n' "${COMMAND}" >&2
170
+ usage >&2
171
+ exit 2
172
+ fi
173
+
174
+ : "${SERVER_CONTAINER_NAME:?SERVER_CONTAINER_NAME must be set in .env}"
175
+ case "${COMMAND}" in
176
+ start|daemon)
177
+ start_server
178
+ ;;
179
+ stop)
180
+ stop_server
181
+ ;;
182
+ restart)
183
+ stop_server
184
+ start_server
185
+ ;;
186
+ status)
187
+ status_server
188
+ ;;
189
+ esac
190
  # --rpc "${RPC_HOSTS}:${RPC_PORT}"