deploy both qwen3.6 versions

This commit is contained in:
Roger Oriol
2026-07-23 22:10:44 +02:00
parent 8ef9db1e37
commit 04bd4d8e5e
16 changed files with 906 additions and 53 deletions

View File

@@ -13,9 +13,15 @@ in-cluster Services instead of the external `10.88.20.12:11434` Ollama endpoint.
One Deployment + Service **per model**, all in namespace `llamacpp`, all pinned
to the NUCBox (`nodeSelector: {kubernetes.io/arch: amd64, hardware: high-memory}`):
| Model | GGUF | Service | litellm alias |
|----------|---------------------------------------------|----------------------------------|---------------|
| qwen3.6 | unsloth/Qwen3.6-27B-MTP-GGUF (Q4_K_XL) | `llamacpp-qwen36.llamacpp:80` | `qwen3.6` |
| Alias | Model | GGUF | Service |
|-------------------|----------------------------|-------------------------------------------------------|-------------------------------------------|
| `qwen3.6-27b` | Qwen3.6-27B (dense) | unsloth/Qwen3.6-27B-MTP-GGUF (Q4_K_XL, ~16 GiB) | `llamacpp-qwen36-27b.llamacpp:80` |
| `qwen3.6-36b-a3b` | Qwen3.6-35B-A3B (MoE/flash)| unsloth/Qwen3.6-35B-A3B-MTP-GGUF (Q4_K_XL, ~20 GiB) | `llamacpp-qwen36-36b-a3b.llamacpp:80` |
The 35B-A3B is a Mixture-of-Experts model (3B active params per token), so
inference is significantly faster than the dense 27B despite more total weights —
hence the "flash" label. Use it for latency-sensitive workloads; use the 27B for
deeper reasoning.
Model files are downloaded idempotently by an initContainer into a shared
hostPath PVC (`/data/llamacpp/models` on the NUCBox), so pods survive reboots
@@ -24,9 +30,8 @@ without re-downloading.
## GPU / Vulkan
The `server-vulkan` image (`ghcr.io/ggml-org/llama.cpp:server-vulkan`) bundles
the Mesa/RADV Vulkan driver, which supports
the Radeon 8060S (RDNA 3.5). Full layer offload (`-ngl 999`) puts the ~16 GiB
Q4 model entirely in the 96 GiB VRAM pool.
the Mesa/RADV Vulkan driver, which supports the Radeon 8060S (RDNA 3.5). Full
layer offload (`-ngl 999`) puts model weights entirely in the 96 GiB VRAM pool.
The container mounts `/dev/dri` and runs `privileged: true` — the simplest
reliable way to give Vulkan access to the DRM render node on k3s without a
@@ -36,26 +41,38 @@ group GID) if desired.
### Verify the GPU is actually used
```bash
kubectl exec -n llamacpp deploy/llamacpp-qwen36 -- \
llama-server --list-devices -m /models/Qwen3.6-27B-UD-Q4_K_XL.gguf
# or check the startup logs for a "vulkan" device line + ngl offload count
kubectl logs -n llamacpp deploy/llamacpp-qwen36 | grep -iE 'vulkan|gpu|offload|device'
kubectl logs -n llamacpp deploy/llamacpp-qwen36-27b | grep -iE 'vulkan|gpu|offload|device'
kubectl logs -n llamacpp deploy/llamacpp-qwen36-36b-a3b | grep -iE 'vulkan|gpu|offload|device'
```
If only a CPU device shows up, the container can't see the GPU — check that
`/dev/dri/renderD128` exists on the NUCBox and that the `amdgpu` module is loaded.
## VRAM budget (both models co-resident)
Both models run simultaneously on the same 96 GiB VRAM pool. Approximate usage:
| Model | Weights | KV cache (32k×4) | Subtotal |
|-------------------|----------|-------------------|----------|
| qwen3.6-27b | ~16 GiB | ~34 GiB | ~50 GiB |
| qwen3.6-36b-a3b | ~20 GiB | ~10 GiB | ~30 GiB |
| **Total** | | | **~80 GiB** |
~16 GiB headroom — comfortable but not infinite. If VRAM is exhausted ( Vulkan
allocation failures in logs), reduce `-c` on the 27B (its KV cache dominates) or
drop `-np` to 2 on either model.
## Tuning
The key knobs (in `deployment-qwen36.yaml`):
The key knobs (in each `deployment-*.yaml`):
- `-ngl 999` — offload all layers to GPU. Reduce only if VRAM is tight (it
isn't, with 96 GiB).
- `-ngl 999` — offload all layers to GPU. Reduce only if VRAM is tight.
- `-c 32768` — total KV-cache context. With `-np 4` this is 8192 tokens per
concurrent request. For a 27B model the full 32k×4 KV cache is ~32 GiB of
VRAM; raise or lower `-c` to trade context length for VRAM headroom.
- `-np 4` — parallel slots (concurrent requests). Matches the requested
concurrency. Each extra slot multiplies KV-cache VRAM usage.
concurrent request. The 27B's dense KV cache is the larger consumer (~34 GiB
at 32k×4); the MoE's is much smaller (~10 GiB).
- `-np 4` — parallel slots (concurrent requests). Each extra slot multiplies
KV-cache VRAM usage. Bump higher on the flash model if you need more
throughput (it has VRAM headroom).
- `--threads 8` — CPU threads for sampling/overhead. Mostly irrelevant under
full GPU offload; tune if CPU-bound.
@@ -69,11 +86,11 @@ during load. If the pod is OOM-killed during model load, raise the memory limit.
## Adding a model
1. Create `deployment-<model>.yaml` + `service-<model>.yaml` (copy the qwen3.6
pair; change `model:` label, the GGUF URL/file, `--alias`, and Service name).
1. Copy `deployment-qwen36-27b.yaml` `deployment-<new>.yaml`; change the
`model:` label, GGUF URL/file, `--alias`, and Service name.
2. Point LiteLLM at it in `litellm/litellm.yaml`:
```yaml
- model_name: <alias> # keep the alias so consumers don't change
- model_name: <alias>
litellm_params:
model: openai/<alias>
api_base: http://<service>.llamacpp/v1
@@ -81,9 +98,4 @@ during load. If the pod is OOM-killed during model load, raise the memory limit.
```
3. (No gen-apps.sh change needed — the `llamacpp` app already syncs the whole
directory recursively.)
## TODO
- `glm-4.7-flash`: still served by the external Ollama at `10.88.20.12:11434`
in `litellm/litellm.yaml`. Migrate once a GGUF source is confirmed (add a
`deployment-glm47-flash.yaml` + Service and flip the litellm entry).
4. Check the VRAM budget table above — two large models may not coexist.

View File

@@ -1,9 +1,8 @@
# Qwen3.6 (27B, Q4_K_XL) served by llama.cpp's llama-server on the NUCBox APU.
# Qwen3.6-27B (dense, Q4_K_XL) served by llama.cpp's llama-server on the NUCBox APU.
#
# Hardware: AMD Ryzen AI Max 395 (Strix Halo) — integrated Radeon 8060S,
# 128 GiB unified memory (32 GiB RAM / 96 GiB VRAM via firmware). The Q4 model
# (~16 GiB) is fully offloaded to the GPU via the Vulkan backend, leaving plenty
# of VRAM for the KV cache.
# (~16 GiB) is fully offloaded to the GPU via the Vulkan backend.
#
# Image: ghcr.io/ggml-org/llama.cpp:server-vulkan bundles the Mesa/RADV Vulkan
# driver, which supports the Radeon 8060S (RDNA 3.5). The project moved from the
@@ -18,11 +17,11 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: llamacpp-qwen36
name: llamacpp-qwen36-27b
namespace: llamacpp
labels:
app: llamacpp
model: qwen3.6
model: qwen3.6-27b
spec:
replicas: 1
strategy:
@@ -30,12 +29,12 @@ spec:
selector:
matchLabels:
app: llamacpp
model: qwen3.6
model: qwen3.6-27b
template:
metadata:
labels:
app: llamacpp
model: qwen3.6
model: qwen3.6-27b
spec:
nodeSelector:
kubernetes.io/arch: amd64
@@ -76,7 +75,7 @@ spec:
- -m # model file
- /models/Qwen3.6-27B-UD-Q4_K_XL.gguf
- --alias # /v1/models reports this name; matches the litellm alias
- qwen3.6
- qwen3.6-27b
- --host
- 0.0.0.0
- --port
@@ -144,16 +143,16 @@ spec:
apiVersion: v1
kind: Service
metadata:
name: llamacpp-qwen36
name: llamacpp-qwen36-27b
namespace: llamacpp
labels:
app: llamacpp
model: qwen3.6
model: qwen3.6-27b
spec:
type: ClusterIP
selector:
app: llamacpp
model: qwen3.6
model: qwen3.6-27b
ports:
- name: http
port: 80

View File

@@ -0,0 +1,158 @@
# Qwen3.6-35B-A3B (MoE: 35B total / 3B active, Q4_K_XL) — the "flash" variant.
#
# Despite having more total parameters than the 27B dense model, only 3B are
# active per token (Mixture-of-Experts), so inference is much faster. The full
# ~20 GiB of Q4 weights is still loaded into VRAM but only a small fraction is
# computed per token.
#
# Hardware: AMD Ryzen AI Max 395 (Strix Halo) — integrated Radeon 8060S,
# 128 GiB unified memory (32 GiB RAM / 96 GiB VRAM via firmware). Full GPU
# offload via the Vulkan backend. Shares the 96 GiB VRAM pool with the 27B
# model — see llamacpp/README.md for the combined VRAM budget.
#
# Image: ghcr.io/ggml-org/llama.cpp:server-vulkan (Mesa/RADV Vulkan driver,
# supports the Radeon 8060S / RDNA 3.5). Pin to a build tag for production.
#
# GPU access: mounts /dev/dri + privileged (simplest reliable path on k3s).
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: llamacpp-qwen36-36b-a3b
namespace: llamacpp
labels:
app: llamacpp
model: qwen3.6-36b-a3b
spec:
replicas: 1
strategy:
type: Recreate # never run two pods loading the same model into VRAM
selector:
matchLabels:
app: llamacpp
model: qwen3.6-36b-a3b
template:
metadata:
labels:
app: llamacpp
model: qwen3.6-36b-a3b
spec:
nodeSelector:
kubernetes.io/arch: amd64
hardware: high-memory
initContainers:
# Idempotently download the GGUF into the shared models PVC on first boot.
# Exits immediately if the file is already present (pod restart / recreate).
- name: fetch-model
image: alpine:3.20
command: ["/bin/sh", "-c"]
args:
- |
set -e
if [ -f "/models/$MODEL_FILE" ]; then
echo "Model $MODEL_FILE already present — skipping download."
exit 0
fi
echo "Installing curl..."
apk add --no-cache curl
echo "Downloading $MODEL_FILE from $MODEL_URL ..."
curl -fL --retry 5 --retry-delay 5 -o "/models/$MODEL_FILE" "$MODEL_URL"
echo "Download complete: $(ls -lh /models/$MODEL_FILE)"
env:
- name: MODEL_URL
value: "https://huggingface.co/unsloth/Qwen3.6-35B-A3B-MTP-GGUF/resolve/main/Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf"
- name: MODEL_FILE
value: "Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf"
volumeMounts:
- name: models
mountPath: /models
containers:
- name: llama-server
image: ghcr.io/ggml-org/llama.cpp:server-vulkan
imagePullPolicy: IfNotPresent
# llama.cpp's CLI parser does NOT split on '=' — every value flag must be a
# separate argv element (flag, then value). See common/arg.cpp in the repo.
args:
- -m # model file
- /models/Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf
- --alias # /v1/models reports this name; matches the litellm alias
- qwen3.6-36b-a3b
- --host
- 0.0.0.0
- --port
- "8080"
- --jinja # use the GGUF's chat template (Qwen3 thinking format)
- -ngl # offload ALL layers to the GPU (fits in 96 GiB VRAM)
- "999"
- -c # total KV-cache context, split across parallel slots
- "32768"
- -np # 4 parallel slots => 8192 tokens per concurrent request
- "4"
- --cont-batching # continuous batching across slots
- --threads # CPU threads for sampling/overhead (GPU does the heavy lifting)
- "8"
ports:
- name: http
containerPort: 8080
resources:
# The model weights + KV cache live in GPU VRAM (96 GiB pool) and are
# NOT counted against the cgroup memory limit. This limit only covers
# CPU-side overhead + the mmap'd GGUF file pages during load (~20 GiB,
# reclaimable). k8s sees ~32 GiB as the node's allocatable system RAM.
# If the pod OOM-kills during load, raise the limit.
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "24Gi"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 180 # model load + Vulkan init can take a few minutes
periodSeconds: 30
failureThreshold: 5
securityContext:
# Vulkan on the AMD APU needs /dev/dri + the driver. Privileged is the
# simplest reliable path on k3s without a device plugin.
privileged: true
volumeMounts:
- name: models
mountPath: /models
readOnly: true
- name: dri
mountPath: /dev/dri
volumes:
- name: models
persistentVolumeClaim:
claimName: llamacpp-models
- name: dri
hostPath:
path: /dev/dri
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: llamacpp-qwen36-36b-a3b
namespace: llamacpp
labels:
app: llamacpp
model: qwen3.6-36b-a3b
spec:
type: ClusterIP
selector:
app: llamacpp
model: qwen3.6-36b-a3b
ports:
- name: http
port: 80
targetPort: 8080