TL;DR: Keep the MoE experts on the CPU and only the dense trunk on the GPU, add a quantized KV cache, MTP speculative decoding, and a few llama.cpp flags that are easy to get wrong. Full working recipe below.
What you need
Nothing exotic — a mid-range desktop from a few years ago is enough. This is the exact setup this guide was written against:
| Component | Specification |
|---|---|
| GPU | NVIDIA RTX 2070 SUPER · 8 GB VRAM · Turing (sm_75) |
| CPU | AMD Ryzen 7 5700X · 8 cores / 16 threads |
| RAM | 64 GB DDR4 |
| Storage | 1 TB NVMe SSD |
| OS | Ubuntu 26.04 LTS (server, headless) |
The model
Mixture-of-Experts (MoE) models activate only a fraction of their parameters per token. The Qwen3.6-35B-A3B has 35B total parameters but only ~3B active — that is what makes it feasible on consumer hardware. It also ships with a built-in MTP head (Multi-Token Prediction), which llama.cpp can use for speculative decoding.
Download the Q4_K_M GGUF (≈ 22.7 GB) — the sweet spot between size and quality:
wget -P ~/llamacpp/models https://huggingface.co/example-org/Qwen3.6-35B-A3B-UD-GGUF/resolve/main/Qwen3.6-35B-A3B-UD-Q4_K_M.ggufBuild llama.cpp with CUDA
Use the v0.4.1 release. Two things trip people up on Ubuntu 26.04: the system GCC is too new for CUDA (use GCC 12), and you need the CUDA dev libraries, not just the driver.
# 1. CUDA 13.3 from the NVIDIA repo (Ubuntu's own 13.1 breaks with glibc 2.43)
sudo apt install cuda-libraries-dev-13-3 cuda-nvcc-13-3 cuda-minimal-build-13-3
# 2. GCC 12 (GCC 15 is too new for the CUDA toolchain)
sudo apt install gcc-12 g++-12
# 3. Clone and build
git clone --depth 1 --branch v0.4.1 https://github.com/ggml-org/llama.cpp
cmake -B llama.cpp/build-cuda -G Ninja \
-DGGML_CUDA=ON -DLLAMA_CURL=ON \
-DGGML_CUDA_FA=ON \
-DCMAKE_CUDA_ARCHITECTURES=75 \
-DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-12 \
-DCMAKE_C_COMPILER=/usr/bin/gcc-12 \
-DCMAKE_CXX_COMPILER=/usr/bin/g++-12
ninja -C llama.cpp/build-cuda -j 16Note: -DCMAKE_CUDA_ARCHITECTURES=75 is for Turing (RTX 20-series). For Ampere use 86, for Ada 89.
The start script — every flag explained
This is the production start script (used via systemd, see below). The result of a lot of trial and error — each flag earns its place:
#!/bin/bash
# llama-server start script (systemd)
set -e
MODEL="/home/user/llamacpp/models/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf"
BIN="/home/user/llamacpp/llama.cpp/build-cuda/bin/llama-server"
LOG="/home/user/llamacpp/server.log"
API_KEY="$(cat /home/user/llamacpp/api_key.txt)"
exec "$BIN" \
-m "$MODEL" \
--n-cpu-moe 41 \
--load-mode none \
--cache-type-k q8_0 \
--cache-type-v q8_0 \
--spec-type draft-mtp -n 3 \
--no-reasoning-preserve \
--n-predict 4096 \
--kv-unified-per-slot 131072 \
--parallel 2 \
--threads 16 \
--threads-batch 16 \
--host 0.0.0.0 \
--port 8090 \
--api-key "$API_KEY" \
>> "$LOG" 2>&1| Flag | Why it's there |
|---|---|
--n-cpu-moe 41 | The core trick: pin the MoE experts to the CPU, keep only the dense trunk on the GPU. Almost free on a desktop with plenty of RAM — no PCIe bottleneck. |
--load-mode none | Load the whole model into RAM instead of OS paging — avoids mid-token page faults. Slower startup, stable runtime. |
--cache-type-k/v q8_0 | 8-bit KV cache: nearly lossless, halves KV memory. This is what makes 128K context fit in 8 GB VRAM. |
--spec-type draft-mtp -n 3 | Speculative decoding via the model's built-in MTP head. Draft cap 2–3 tokens — more hurts on MoE (expert union grows). |
--no-reasoning-preserve | Qwen3.6 emits a reasoning block; this keeps it out of the output budget so you don't get empty replies. |
--n-predict 4096 | Cap per-request generation. Without it the server default is tiny and reasoning models eat it all → empty responses. |
--kv-unified-per-slot 131072 | 128K context per slot via the unified KV cache. With 2 slots that's 256K total — plenty for agent workloads. |
--parallel 2 | Two concurrent request slots — enough for a single user with parallel agent calls. |
--threads 16 --threads-batch 16 | 16 CPU threads for decode and batch — matches the 8-core/16-thread CPU. |
Run it as a systemd service
A 22.7 GB model takes a while to load — systemd with a generous timeout and auto-restart is the right way to run it. The 30-second delay after boot gives the system time to settle:
# /etc/systemd/system/llama-server.service
[Unit]
Description=Local LLM Server (llama.cpp, Qwen3.6-35B-A3B MoE)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=user
Group=user
WorkingDirectory=/home/user/llamacpp
ExecStartPre=/bin/sleep 30
ExecStart=/home/user/llamacpp/llama-server-start.sh
Restart=on-failure
RestartSec=10
TimeoutStartSec=600
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable llama-server # autostart on boot
sudo systemctl start llama-server
systemctl is-active llama-server # → activeSecurity: the server binds
0.0.0.0— generate a key withopenssl rand -hex 24 > api_key.txt,chmod 600it, read it from the start script. Never hardcode it in the unit file (world-readable).
Verify it's working
# Health check (no auth needed)
curl http://server01.local:8090/health
# Chat completion
curl http://server01.local:8090/v1/chat/completions \
-H "Authorization: Bearer $(cat ~/llamacpp/api_key.txt)" \
-H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":"Say hello in one sentence."}]}'If the reply comes back empty, check server.log for finish: length — the reasoning block ate the output budget and --n-predict 4096 is missing.
Measured results
Real numbers from the server log (slot print_timing), Qwen3.6-35B-A3B Q4_K_M, 128K context, MTP speculative decoding on:
| Metric | Value |
|---|---|
| Decode (generation) | ≈ 33 tok/s average, peaks up to 42 tok/s |
| Prompt processing | ≈ 150 tokens/s |
| MTP draft acceptance | 64–80%, mean draft length ≈ 3 |
| VRAM usage | ≈ 7.0 GB of 8 GB |
Pitfalls we hit, so you don't have to
- CUDA 13.1 from Ubuntu repos breaks with glibc 2.43 — compile errors in
mathcalls.h(rsqrt conflict). Use CUDA 13.3 from the NVIDIA repo — fixed natively. - GCC 15 is too new for CUDA — install gcc-12/g++-12 and point CMake at them via
-DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-12. - MoE + speculative decoding: draft cap 2–3, not 8 — with MoE, the expert union grows with draft length — longer drafts are slower than no drafts at all.
- Empty replies with reasoning models — Qwen3.6's thinking block counts against the output budget. Symptom:
finish: lengthwith empty content. Fix:--n-predict 4096+--no-reasoning-preserve. - Don't trust the context length on the model card — 262K/1M are lab numbers. With KV quantization, ~128K is the realistic ceiling — still plenty.
- Port conflicts on a busy host — check
ss -tlnpanddocker psbefore binding — pick a free port (8090 worked here).
Bottom line: a 35B MoE model on an 8 GB GPU is not a hack — it's a sensible architecture choice. 33 tok/s is entirely usable for chat, agents, and tool-calling, and everything runs on your own hardware.