30 分钟
本页目录

本地 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,所以有小模型的速度、大模型的容量)。真正的取舍不是在 35B 与 8B 之间反复横跳,而是保住 35B 的表达与一致性,再为同一模型准备两档 KV:⁠日常交互用 Q8_0 + 128K,真正需要超长输入时切 q4_0 + 192K

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 ✓ 同一个 35B 保留两档:交互档用 Q8_0 兼顾稳定性,容量档用 q4_0 把窗口拉到 192K。 窗口规格不等于实时可用长度:越长越慢,真正吃到 100K 以上时再切容量档。flash-attn 是量化 KV 的前提。

拆开这 24G:

  • 模型权重 16.96 GiB(IQ4_XS)。MoE 让它跑得快,IQ4_XS 让质量够用。
  • 容量档用 q4_0 + flash-attn,把 192K 上下文压进约 5G。若换 f16,同样上下文要四倍显存直接溢出;Q8_0 在真正吃到 100K 左右时会开始挤占余量并显著降速。
  • 交互档用 Q8_0 + 128K 上限⁠,客户端只放出 120K,并在约 55K 开始压缩旧消息。平时不会真的把 128K 全部填满,换来更稳的生成轨迹;偶尔超过 100K 仍能跑,只是首字延迟已不适合实时聊天。
  • 容量档实测常驻 21.8G,余 2.2G,idle 掉到低功耗档,桌面无压力。

早期跑过 Q3_K_XL + 短上下文的保守配置;现在固定 IQ4_XS 权重,只按负载切 KV 档。⁠默认 Q8 交互档,超长输入才切 Q4 容量档⁠;桌面偶尔重载吃紧时再切一个 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
# 保守:质量优先,约 96K 是本机全显存舒适档;服务上限可再留余量
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短基准预填充很快;不可线性外推到 100K
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)

角色扮演负载实测:8B 大窗口、35B 与 KV 精度

2026-08-24 为 SillyTavern 的长对话负载做了一轮本机 A/B。问题不是"哪个模型标称窗口更大",而是三个更实际的问题:

  1. 为大窗口换成 8B,角色扮演质量会损失多少?
  2. 35B 把 KV 从 Q8_0 降到 q4_0,是否会稳定损伤文笔或事实保持?
  3. 100K–192K 虽然能配置,每轮重新读取这么长的历史是否仍适合实时聊天?

模型质量与长上下文 A/B 只使用合成场景与随机事实码。短场景使用相同提示与采样条件;长文本把 8 个目标事实分散在全文不同位置,按严格 JSON 映射计分;主观质量由交换 A/B 顺序的双盲评分汇总。后文的轮数估算只对本地既有记录做 token 聚合,不输出或保留正文。样本量不大,分数只用于本机选型,不是通用模型排行榜。

模型规模:8B 的大窗口补不回质量差距

指标35B-A3B8B解读
短场景双盲综合分8.646.36差距主要在文笔、角色可信、连续性与避免重复
暖机生成速度126.7 t/s107.4 t/sMoE 的激活参数小,35B 总参不等于每 token 都算 35B
中位完整响应延迟3.74s3.53s实际体感基本同档
冷加载12.73s3.47s8B 的明确优势只有首次加载快

8B 并没有以更快的暖机推理换回质量;本机后端下,35B-A3B 反而有更高的生成吞吐。若任务是沉浸式长对话,尤其要求模型主动推进、保持人物状态且避免在同一场景打转,⁠不应仅为窗口规格把主模型降到 8B

长上下文:能放进去不等于用得稳、等得起

下表时间是整次非流式请求的墙钟时间,上下文长度为约数;不同模型分词器会让实际输入有少量偏差。

目标上下文35B · Q8_0 KV35B · q4_0 KV8B · Q8_0 KV
22K8/8 · 约 11.3s8/8 · 约 14–15s6/8 · 约 23.5s
52K8/8 · 约 39.9s8/8 · 约 72.5s
100K8/8 · 约 111.5s8/8 · 约 101.7s6/8 · 约 231.8s
160K8/8 · 约 215.1s连接超过约 5 分钟后中断,后端仅处理到约 73%

这里有三个反直觉结果:

  • 8B 的标称长窗口不是稳定利用能力。 它在 52K 全对,却在 22K 与 100K 都只有 6/8,说明小模型对事实位置和具体输入更敏感,不是简单地"越短越准"。
  • 8B 在长输入上反而更慢。 100K 比 35B q4_0 慢约 2.3 倍;160K 在当前路由连接时限内已不可用。
  • 超长窗口的主要代价是每轮首字等待。 35B q4_0 的 160K 虽然全对,但约 215 秒才完成。聊天每轮都重读历史,因此 192K 更像容量上限或离线文档档,不是应该长期顶满的实时工作区。

KV 精度:会改变生成轨迹,但没有稳定降质证据

同一个 35B、同一权重、相同种子与提示,只切换 KV cache 精度:

对照Q8_0q4_0
22K 精确检索8/88/8
100K 精确检索8/88/8
短场景双盲分9.138.38
22K 长场景双盲分7.909.30
思考格式标签泄漏0/51/5

短场景 Q8_0 更好,长场景 q4_0 反而更好,评分方向发生反转。这说明 KV 量化足以让确定性生成走上不同 token 轨迹,单次输出可能变好也可能变差;但结合 22K–160K 检索全对,⁠本轮没有观察到 q4_0 会持续降低文本质量或上下文精度⁠。另一方面,q4_0 的一次格式标签泄漏和小样本波动也说明它不应被称为"完全无损"。

因此采取双档而非押注单档:

用途服务端上下文KV客户端预算选择理由
日常 SillyTavern / 角色扮演131,072Q8_0 / Q8_0122,880默认档;保留生成稳定性,同时远大于 32K
超长文档 / 极长历史临时续接196,608q4_0 / q4_0按任务临时切换100K–160K 检索稳定,容量与带宽更划算

SillyTavern 的实际压缩与输出预算

当前交互档不是等到 120K 塞满才总结:

  • 摘要调用使用独立的 8,192 上下文 / 160 输出 token 非流式预设,但仍指向同一个 35B 路由,避免为了摘要反复卸载和重载模型。
  • 自动总结开启,延迟 2、每批 2;最早在前几轮就开始准备逐段摘要。
  • 真正排除旧原文并注入摘要的阈值是客户端窗口的 45%,即 122880 × 45% = 55296 token。
  • 达到阈值后保留最后一条用户消息,旧原文由短期/长期摘要接管;因此会话不是到固定轮数就结束,而是逐渐从逐字历史过渡到压缩记忆。
  • 本机一个只统计 token、不查看正文的极小样本中,完整一轮中位数约 425 token。纯数学约 130 轮触发;扣掉角色卡、世界书、系统提示并考虑回复波动,⁠保守按 80–120 轮原文历史理解⁠。长回复会更早,短回复会更晚。
  • 中文并非 1 字 = 1 token。本机分词样本约为 1 token ≈ 1.62 个中文字符;原先 1200 token 已约等于 1900 字,当前 1600 token 上限约 2600 字。max_tokens 是上限而非目标长度,给到 1600 是为了避免自然转场被硬截断,不是鼓励每轮灌满。

最终决策可以压成一句话:

保住 35B 的角色扮演质量;日常用 128K 级 Q8 + 约 55K 提前压缩,真正需要读取 100K 以上时才切 192K Q4。不要为了标称大窗口换成 8B,也不要把能启动的上下文上限误当成适合实时聊天的长度。

八、跑通之后:接客户端

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 反而可能打不开设备;权限也小得多。

相关文档