本页目录

本地 LLM 部署笔记

用消费级显卡跑本地大模型,核心是一道预算题:模型权重、KV cache、桌面环境在抢同一块显存,怎么分决定了能跑多大的模型、开多长的上下文。本文以一张 24G 的 RX 7900 XTX 为实例,给出选型方法和完整可复现配置,并延伸到 16G / 12G 显卡。所有命令兼容 Linux,WSL2 同理。

一、约束:一道不等式,不分卡

本地部署的所有决定——选哪个量化、KV cache 设什么精度、上下文开多长——都在解同一道题:

模型权重 + KV cache + 运行缓冲 + 桌面余量 ≤ 显存容量

这道不等式对 24G、16G、12G 都成立,只是右边的数不同。把它当预算来分,是整个部署的通用起点。本机是一张 7900 XTX(24GB),下文以它为实例展开。

二、第一步:按你的显卡选后端

llama.cpp 支持多种 GPU 后端,⁠不需要装任何厂商的 AI 训练栈⁠(CUDA Toolkit / ROCm / oneAPI),只走显卡驱动自带的运行时。按你的卡选:

你的显卡后端编译开关
NVIDIA(GTX 10 系及以上)CUDA-DGGML_CUDA=ON
AMD(RX 系列 / Radeon VII)Vulkan-DGGML_VULKAN=ON
Intel ArcVulkan-DGGML_VULKAN=ON
Apple Silicon(M 系)Metal-DGGML_METAL=ON

几个常见疑问:

"N 卡要不要装 CUDA Toolkit?"——不用。llama.cpp 只依赖显卡驱动自带的 CUDA 运行时,不需要装好几个 G 的 CUDA Toolkit。

"AMD 为什么是 Vulkan 不是 ROCm?"——ROCm 是给 PyTorch / vLLM 做训练的,安装麻烦(Windows 上尤其折腾),GGUF 推理完全用不上。Vulkan 走显卡驱动自带的运行时,Linux 和 Windows 都开箱即用。

"集显和独显都在怎么办?"——Vulkan 用户用 GGML_VK_VISIBLE_DEVICES=0 锁独显(0 = 第一块);CUDA 用户用 CUDA_VISIBLE_DEVICES=0。Metal 用户通常不需要。

Windows 用户⁠:下面的命令全部在 WSL2 里跑。WSL2 能直接访问 GPU(N 卡装官方驱动即用;A 卡需内核 5.15+ 且装 AMD 官方 WSL 驱动),编译安装流程和 Linux 一致。先 wsl --install 装好发行版,后续步骤照做即可。

下面进入核心问题:显存怎么分。

三、怎么选:把显存花在刀刃上

能调的旋钮就四个:参数量和权重量化在下载选文件时定死;上下文长度和 KV 精度是启动参数,每次可调。先看本机的答案,再讲怎么在自己的卡上做同样的决策。

本机的答案:省下的显存砸在上下文,不砸在 KV 精度

Qwen3.6-35B-A3B(MoE,总参数 35B 但每次只激活 3B,所以有小模型的速度、大模型的知识量)。真正的取舍在于省下的显存花在哪——花在"能记住多长的对话",而不是"KV 记得多精确":

24G 怎么花:省下的显存砸在上下文,不砸在 KV 精度 本机实测常驻(24G 卡 · idle) 24G 模型 IQ4_XS 16.96 GiB KV·q4_0 192K+缓冲 余2.2G 实测 used 21.8G / free 2.2G — 顶得从容,不是刚好塞满。 同样这张卡,KV 精度决定能开多长上下文(按位宽反比估算) f16 KV ≈ 48K Q8_0 KV ≈ 96K q4_0 KV(本机) 192K ✓ 关键取舍:agent / coding 吃长上下文。与其用高精度 KV 换来短窗口,不如用 q4_0 KV 把窗口拉到 192K —— 省下的显存花在"能记多长",而不是"KV 多精确"。flash-attn 是量化 KV 的前提。

拆开这 24G:

  • 模型权重 16.96 GiB(IQ4_XS)。MoE 让它跑得快,IQ4_XS 让质量够用。
  • KV cache 用 q4_0 + flash-attn,把 192K 上下文压进约 5G。若换 f16,同样上下文要四倍显存直接溢出;换 Q8 也只够一半。KV 精度对生成质量的影响远小于它对上下文长度的挤压⁠——所以这笔预算全投给长度。
  • 实测常驻 21.8G,余 2.2G,idle 掉到低功耗档,桌面无压力。

早期跑过 Q3_K_XL + 短上下文的保守配置;换 IQ4_XS + q4_0 KV 后,质量、速度、上下文三者反而更平衡。桌面偶尔重载吃紧时切一个 Q3 档兜底即可。

旋钮在哪调、什么时候定

套到自己的卡上,四个旋钮分两拨:

选型分两步:下载时定「参数量+量化」,启动时定「上下文+KV」 ① 下载时 · 刻进文件、事后改不了 参数量 选模型仓库 · 8B / 14B / 35B-A3B 权重量化 选哪个 .gguf · Q4_K_M / IQ4_XS / Q8_0 GGUF:~/models/…-IQ4_XS.gguf ② 启动时 · 每次可调 上下文 -c 196608(192K) KV 精度 -ctk / -ctv q4_0(需 --flash-attn) llama-server 跑起来 想换更大模型 / 更高质量 → 重新下文件;想要更长上下文 / 更省显存 → 改启动参数即可。

第一步:选参数量 + 权重量化(下载时定)

选参数量靠后面的「分档表」——24G 上 30–35B,16G 上 14B,12G 上 8B。

选权重量化⁠:同一个模型在 HF 上会有一排 GGUF,文件名后缀就是量化级别:

  • 数字 ≈ bit 数:Q8_0(8bit,近无损、最大)> Q6_K > Q5_K_M > Q4_K_M(4bit,最通用的平衡点)> Q3_K_M(省显存、质量下降)。
  • IQ4_XS / IQ3_XXS 是 i-quant:同 bit 更小、质量接近,需 imatrix(发布者已附)。显存紧就用它。
  • _S / _M / _L = 小 / 中 / 大变体,同 bit 里 _M 最常用。

怎么挑⁠:GGUF 文件体积 ≈ 它占的显存。拿权重预算对着文件大小,挑最大能装下的:

权重预算 = 显存 − KV cache − 桌面余量(留 ~2G)
例:24 − 5(192K q4_0) − 2 ≈ 17G → 35B 用 IQ4_XS(实际 16.96G)正好;
      换 Q5_K_M(~24G)就装不下。

经验优先级:先奔 Q4_K_M;装不下往下降 IQ4_XSIQ3;有富余往上升 Q5_K_M / Q6_K。别用 Q3 以下,除非别无选择。

第二步:定 KV 精度(启动时调)

KV 精度和上下文长度都是启动参数,每次可调。KV 靠三个开关:

开关作用常用值
--flash-attn / -fa量化 KV 的前提,必开开 / 关
--cache-type-k / -ctkK cache 精度f16(默认)/ q8_0 / q4_0
--cache-type-v / -ctvV cache 精度同上

K 和 V 分开设⁠,精度阶梯 f16 → q8_0 → q4_0,占显存大致 1 → 1/2 → 1/4——这就是为什么 q4_0 能开到 f16 约 4 倍的上下文。三种典型档:

# 激进:长上下文优先
llama-server ... -fa -ctk q4_0 -ctv q4_0 -c 196608
# 保守:质量优先,上下文减半
llama-server ... -fa -ctk q8_0 -ctv q8_0 -c 98304
# 非对称:K 对量化更敏感,极限省显存时保 K、压 V
llama-server ... -fa -ctk q8_0 -ctv q4_0

怎么验证调了多少⁠:启动日志会打印 KV self size = ...,直接看占多大。质量上 q8_0 基本无损,q4_0 在日常对话和 coding 里也难感知,只有长文本精确检索偶尔露怯。查显存占用的命令因平台而异:nvidia-smi(N 卡)、cat /sys/class/drm/card0/device/mem_info_vram_used(AMD Linux)、任务管理器 → 性能 → GPU(Windows)。

坑:不开 -fa 时设 -ctv q4_0 会报错或被忽略——量化 V cache 依赖 flash attention。

不同显存的起点

同一套预算公式往下推(⁠这些是估算,按自己卡实测校准⁠):

显存现实选择
24G30–35B MoE(IQ4/Q4)或 32B dense IQ4;长上下文靠 q4_0 KV
16G14B dense Q4–Q5,或 30B MoE Q3 + 适度层 offload;中等上下文
12G8–9B Q4–Q5 舒适(如 Qwen3-8B GGUF ~5.4G),或 14B Q4 紧凑
8G7–8B Q4,短上下文

显存不够时的砍法,按优先级⁠:先降 KV 精度(f16→q8_0→q4_0)保上下文 → 再压上下文长度 → 最后才换更小的模型或更狠的权重量化。别忘了桌面和游戏也在抢这张卡,留几 G 给它。

决策做完,现在把模型拉下来、跑起来。

四、跑起来:从安装到第一句回复

装 llama.cpp(要带 server + 对应后端,按优先级):

  1. 官方 release 二进制——全平台通用,下载解压即用,已含各后端。
  2. Linux 发行版有现成包:apt install llama-cpp(Debian/Ubuntu)、pacman -S llama.cpp-vulkan(Arch)。
  3. 以上都没,从源码编译(把 <YOUR_BACKEND> 换成第二节选的开关,如 -DGGML_CUDA=ON):
git clone https://github.com/ggerganov/llama.cpp --depth 1
cd llama.cpp
cmake -B build <YOUR_BACKEND> -DLLAMA_CURL=ON
cmake --build build --config Release -j
# 产物在 build/bin/

下载模型到本地模型目录:

pip install -U "huggingface_hub[cli]"
# 在 HF 搜 "<模型名> GGUF",找 unsloth / bartowski 的量化仓库
hf download unsloth/Qwen3.6-35B-A3B-MTP-GGUF \
  --include "*IQ4_XS*.gguf" --local-dir ~/models

大模型 GGUF 常分片(…-00001-of-0000N.gguf),--include "*IQ4_XS*" 一次下全;llama.cpp 指向第一片会自动加载其余。

启动⁠(单模型,适合初次试用):

llama-server \
  -m ~/models/Qwen3.6-35B-A3B-MTP-UD-IQ4_XS.gguf \
  -ngl 99 -c 196608 \
  --flash-attn --cache-type-k q4_0 --cache-type-v q4_0 \
  --host 0.0.0.0 --port 18080

Vulkan 用户如果有多块 GPU,在前面加 GGML_VK_VISIBLE_DEVICES=0 锁独显;CUDA 用户跳过。

http://localhost:18080 是 OpenAI 兼容端点。验证跑通了没:

curl http://localhost:18080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"hi"}]}'

路径里的 MTP 是什么、为什么用 unsloth 的 GGUF、不用行不行——下面第五节专门讲。

单模型命令适合试用;日常常驻推荐 router 模式⁠(models.ini 集中管多个模型,按需 LRU 换出),MTP 加速和潮汐调度也都基于 router 模式,下面展开。

五、提速:MTP 投机解码

这是什么

MTP(Multi-Token Prediction)是一种投机解码技术:一次前向预测多个 token 而非逐 token 串行生成。本机用 Qwen3.6-35B-A3B(IQ4_XS)实测:不开 MTP 约 75 t/s,开了 ~111 t/s,提速约 50%。你的模型和量化不同,实际提升会有差异。

标准 llama.cpp 模型没有这个能力——它来自 unsloth(一个专门做 GGUF 量化和加速的团队)在量化时额外往 GGUF 里嵌入了草稿头(draft head)。草稿头先"猜"接下来 2 个 token,主模型一次前向并行验证;猜对白赚,猜错回退。无需独立的草稿模型,也不占额外显存:

MTP 投机解码:一次前向产出多个 token 无 MTP 逐 token 前向 → t₁ 前向 → t₂ 前向 → t₃ … 每次前向只出 1 个 MTP 草稿头+验证 草稿头预测 t₁, t₂(一次提 2 个) 主模型一次前向 并行验证 t₁,t₂ 接受 ✓✓ / ✓✗ 接受率 ~52% `spec-draft-n-max=2`,draft 接受率 ~52% → 实测 tg ~111 t/s。草稿头内置 GGUF,不占额外槽位。

怎么找、不用怎么办

MTP 不是模型原生的功能,是 unsloth 在量化时额外加的。 所以:

  • 只有在 HF 上搜 unsloth/<模型名>-MTP-GGUF 才有 MTP 版⁠,例如 unsloth/Qwen3.6-35B-A3B-MTP-GGUF。bartowski 等常规量化仓库没有。
  • 文件名含 MTP 字样⁠,如 …-MTP-UD-IQ4_XS.gguf。不含就不带草稿头,无法投机解码。
  • 不是所有模型都有 unsloth 出 MTP 版⁠——unsloth 主要给 Qwen 系列加。没有就用普通 GGUF,照样跑,只是没有 MTP 加速。

不用 MTP 的配置⁠:下载命令里去掉 MTP,换普通仓库(如 bartowski/Qwen3.6-35B-A3B-GGUF);配置文件删掉 spec-typespec-draft-n-max 两行,其余不变。

router 模式与完整预设

如果想管理多个模型或常驻开机自启,升级为 router 模式⁠:将所有模型和参数写入 ~/.config/llama.cpp/models.ini,server 用 --models-preset 加载,--models-max 1 限制同时只驻留一个(24G 只装得下一个),LRU 自动换出:

# ~/.config/llama.cpp/models.ini
[qwen3.6-mtp-instruct]
model = ~/models/Qwen3.6-35B-A3B-MTP-UD-IQ4_XS.gguf
ngl = 99                      # 全部层上 GPU(99=all)
ctx-size = 196608             # 192K 上下文
chat-template = chatml        # OpenAI 对话模板,不用工具也建议设
chat-template-kwargs = {"enable_thinking": false}
flash-attn = 1
cache-type-k = q4_0
cache-type-v = q4_0
spec-type = draft-mtp          # 以下两行仅 MTP 模型需要
spec-draft-n-max = 2

坑:chat-template=chatml 必须显式设——用模型原生模板时,工具调用前缀处可能生成错误 token 导致提前 EOS(llama.cpp #19513)。不用 MTP、不用工具调用也一样要设。

加新模型只需在 INI 里加一条,Open WebUI 列表自动更新。systemd unit 和启用命令见「完整配置」一节。

六、常驻调度:模型在线,功耗潮汐(Linux)

跑得快是一时,常驻不浪费才是日常。以下基于 Linux systemd 和 AMD sysfs 接口(WSL2 用户直接跟本节走,发行版自带 systemd;纯 Windows 用户跳过,核心思路一样:开后台进程托管 llama-server,按 GPU 负载切功耗档):

显存潮汐:模型常驻,功耗与占用随负载涨落 空闲常驻 模型在显存 · GPU 档位 0 低功耗待命 推理中 gpu_busy ≥ 50% → 档位 5 COMPUTE 全速 游戏中 gamemode → 停 llama-server 整卡显存让给游戏 来请求 空闲 > 90s → 降档 游戏启动 结束 → warmup ~6s

三个机制:

① 开机 warmup。 router 懒加载,靠 systemd ExecStartPost 启动后主动打一发请求灌模型进显存。warmup payload 必须 -d @文件——内联 JSON 会被 systemd 吞掉引号。warmup.json 就一句话:{"model":"qwen3.6-mtp-instruct","messages":[{"role":"user","content":"hi"}],"max_tokens":1}

② 功耗按真实负载切档。 llama-gpu-sync.timer 每 15s 触发,判据是 gpu_busy_percent ≥ 50% 才切 COMPUTE(档 5),闲置超过 90s 降回 BOOTUP_DEFAULT(档 0),只碰 0/5、不碰游戏用的 3d_full_screen。关键在用 GPU 真实占用而非"模型是否加载"——常驻但不推理就该待在省电档。AMD 通过 sysfs pp_power_profile_mode 实现,N 卡走 nvidia-smi -pl

③ 游戏时整卡让出。 gamemode(多数发行版有包)检测到 Steam 游戏启动即停 llama-server,显存全让给游戏,结束再拉起。模型还在 page cache 里,warmup ~6s 恢复:

# ~/.config/gamemode.ini
[custom]
start = systemctl --user stop llama-server
end   = systemctl --user start llama-server

完整 systemd unit、功耗切档脚本、sudoers 免密、enable 命令见「完整配置」一节。

七、性能基线

llama.cpp 跑分术语:

  • pp512(prompt processing):并行处理 512 个输入 token 的速度(token/s),决定"读长上下文"有多快。
  • tg128(text generation):连续生成 128 个输出 token 的速度,即直观的"吐字"速度——人阅读约 5–10 t/s,100+ 就是"话没说完已刷屏"。
  • MTP draft 接受率⁠:草稿头猜的 token 被主模型验证通过的比例,越高等效提速越多。

本机实测(Qwen3.6-35B-A3B · IQ4_XS · 192K):

指标数值什么概念
pp512~3150 t/s长上下文预填充不卡
tg128~111 t/s远超阅读速度,体感即时
MTP 接受率~52%草稿头约一半命中
常驻显存21.8 / 24 GB余 2.2G

自己测⁠:llama-bench 一条命令,换卡换量化对比:

llama-bench -m ~/models/Qwen3.6-35B-A3B-MTP-UD-IQ4_XS.gguf -ngl 99
# 输出即 pp512 / tg128 两行
# 常驻服务跑着时需先停: systemctl --user stop llama-server(Linux)

八、跑通之后:接客户端

llama-server 暴露 OpenAI 兼容端点⁠(http://localhost:18080/v1),任何能填自定义 base_url 的客户端都能接——Open WebUI、IDE 插件、CLI agent,把 base_url 指过来即可。

到这里模型已经能用了。但⁠"拿它干嘛、哪些活该交给它、哪些该留给 Claude Opus、要不要配云端 fallback"是另一个话题⁠——那才是本地模型真正值得琢磨的地方,单独一篇讲:见 local-llm-usage.md

九、踩坑合集

  • systemd 内联 JSON 被吞引号 → warmup 必须 -d @文件
  • 核显和独显共存时模型跑到了核显上 → 用 GGML_VK_VISIBLE_DEVICES=0(Vulkan)或 CUDA_VISIBLE_DEVICES=0(CUDA)锁独显。
  • 量化 KV 没开 flash-attn--flash-attncache-type q4_0/q8_0 的前提,否则被忽略或报错。
  • 工具调用提前 EOSchat-template=chatml 显式指定,避开原生模板 bug(llama.cpp #19513)。
  • 以为常驻=一直高功耗 → 用 gpu_busy_percent(AMD)或 nvidia-smi(N 卡)切档,空闲降到省电档。
  • 显存预算算错 → 先估 模型体积 + KV(层数×ctx×精度) + 缓冲 + 桌面,留 1–2G 余量,别顶到零。

十、完整配置(Linux)

前面各节为讲清"做什么",只贴了关键片段;这里把整条链路的所有文件完整列出,按顺序创建,最后 enable。

WSL2 用户直接跟本节走;纯 Windows 用户用任务计划程序替代 systemd,功耗切档按 AMD/N 卡分别处理。

前置:脚本 + 免密 sudo

llama-profile-sync 会调用 gpu-profile-set,后者需要 root 写 pp_power_profile_mode(AMD GPU sysfs);systemctl 的 ExecStopPost 同理。user unit 无法弹 sudo 密码提示,必须配免密:

sudo tee /usr/local/bin/gpu-profile-set <<'EOF'
#!/bin/sh
case "$1" in
    bootup_default) idx=0 ;;
    3d_full_screen) idx=1 ;;
    power_saving)   idx=2 ;;
    video)          idx=3 ;;
    vr)             idx=4 ;;
    compute)        idx=5 ;;
    [0-5])          idx=$1 ;;
    *) echo "Unknown profile: $1" >&2; exit 1 ;;
esac
printf "%s\n" "$idx" > /sys/class/drm/card0/device/pp_power_profile_mode
EOF
sudo chmod 755 /usr/local/bin/gpu-profile-set

sudo tee /usr/local/bin/llama-profile-sync <<'EOF'
#!/bin/bash
PROFILE_PATH="/sys/class/drm/card0/device/pp_power_profile_mode"
BUSY_PATH="/sys/class/drm/card0/device/gpu_busy_percent"
STATE="/tmp/llama-profile-last-busy"
BUSY_THRESHOLD=50
HOLD_SECONDS=90

current_idx() { grep '\*' "$PROFILE_PATH" | awk '{print $1}'; }

set_profile() {
    local want=$1 cur
    cur=$(current_idx)
    case "$cur" in 0|5) ;; *) return 0 ;; esac
    case "$want" in
        compute)        [ "$cur" = "5" ] && return 0 ;;
        bootup_default) [ "$cur" = "0" ] && return 0 ;;
    esac
    sudo /usr/local/bin/gpu-profile-set "$want"
}

if ! systemctl --user is-active --quiet llama-server 2>/dev/null; then
    set_profile bootup_default; exit 0
fi
busy=$(cat "$BUSY_PATH" 2>/dev/null || echo 0)
now=$(date +%s)
if [ "${busy:-0}" -ge "$BUSY_THRESHOLD" ]; then
    echo "$now" > "$STATE"
    set_profile compute
else
    last=$(cat "$STATE" 2>/dev/null || echo 0)
    [ $((now - last)) -lt "$HOLD_SECONDS" ] && set_profile compute || set_profile bootup_default
fi
EOF
sudo chmod 755 /usr/local/bin/llama-profile-sync

# 免密 sudo(username 换成自己的):
# echo "USERNAME ALL=(ALL) NOPASSWD: /usr/local/bin/gpu-profile-set" | sudo tee /etc/sudoers.d/llama-profile

pp_power_profile_mode / gpu_busy_percent 是 AMD GPU sysfs 接口。N 卡功耗管理走 nvidia-smi -plnvidia-persistenced,不适用本节脚本。

systemd user unit(三个文件)

# ~/.config/systemd/user/llama-server.service
[Unit]
Description=llama.cpp server (router mode, Vulkan)
After=network.target

[Service]
Environment=GGML_VK_VISIBLE_DEVICES=0     # Vulkan 锁独显; CUDA 用户删掉此行或换 CUDA_VISIBLE_DEVICES=0
ExecStart=/usr/bin/llama-server \
    --models-preset %h/.config/llama.cpp/models.ini \
    --models-max 1 \
    --host 0.0.0.0 \
    --port 18080 \
    --metrics
# warmup: 预加载模型进显存; -d @file 避开 systemd 引号吞掉
ExecStartPost=-/usr/bin/bash -c 'for i in $(seq 1 60); do curl -sf --max-time 90 http://127.0.0.1:18080/v1/chat/completions -H "Content-Type: application/json" -d @%h/.config/llama.cpp/warmup.json >/dev/null && exit 0; sleep 2; done'
# 服务停止时 GPU 回省电档
ExecStopPost=/usr/bin/sudo /usr/local/bin/gpu-profile-set bootup_default   # AMD 功耗管理; N 卡用户删掉此行
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
# ~/.config/systemd/user/llama-gpu-sync.service
[Unit]
Description=Sync GPU profile with llama-server model state
After=llama-server.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/llama-profile-sync
# ~/.config/systemd/user/llama-gpu-sync.timer
[Unit]
Description=Poll llama-server model state for GPU profile switching

[Timer]
OnBootSec=15s
OnUnitActiveSec=15s

[Install]
WantedBy=timers.target

warmup

echo '{"model":"qwen3.6-mtp-instruct","messages":[{"role":"user","content":"hi"}],"max_tokens":1}' \
  > ~/.config/llama.cpp/warmup.json

启用

# linger — 常驻模型的关键:退出登录后服务仍存活
sudo loginctl enable-linger "$USER"

# reload + enable
systemctl --user daemon-reload
systemctl --user enable --now llama-server.service
systemctl --user enable --now llama-gpu-sync.timer

# 验证
systemctl --user status llama-server
systemctl --user status llama-gpu-sync.timer

--user 不是 --system:GPU 上下文(DRM render node)在用户会话里,跑 system unit 反而可能打不开设备;权限也小得多。

相关文档