Gemma 系列的 MoE 规格模型(总参数与激活参数分开标注,例如 26B-A4B 这类命名)主打的是“参数总量大、每 token 只激活一小部分”,在本地单卡上跑出可用的速度。这类模型通常自带“思考模式”:回答前先输出一段推理链,准确率上去了,首 token 延迟也跟着涨。真实业务里,很多请求(分类、抽取、格式转换、简单问答)根本不需要这段推理链。这篇教程做三件事:把模型在本机跑起来、把思考开关做成一个请求参数、用同一批题目量出“少思考”到底损失多少准确率、省下多少延迟。
需要先说明一点:模型的准确名称、参数规模、是否已公开发布,以官方模型卡和发布页面当前版本为准。如果你的环境里该规格尚未放出,下面的流程完全通用——把模型路径、模板字段名替换成你手上那个支持思考开关的 MoE 模型即可,命令结构不变。
适用场景
你有一张 24GB 显存的消费级卡(或统一内存 32GB 以上的机器),想在内网跑一个中文能力过得去的模型,给内部工具做问答、信息抽取和轻量 Agent。你不接受每次请求都等两三秒才出第一个字,愿意用可控的准确率损失换响应速度,并且希望能按请求逐个开关思考,而不是全局一刀切。
环境与前置条件
- 操作系统:Linux(Ubuntu 22.04/24.04 一类)最省心;macOS 走 Metal 也能跑,命令里的
--n-gpu-layers语义相同。Windows 建议走 WSL2。 - 推理后端:llama.cpp(GGUF 量化,单机首选)与 vLLM(服务端高并发,需要 NVIDIA GPU)。两个都装不冲突,本篇都会用到。
- 显存/内存估算:按 0.6 字节/参数估 Q4_K_M 的权重占用,26B 量级大约 15–16GB;Q8_0 约 26–27GB;再叠加 KV cache(随上下文长度线性增长)。24GB 卡跑 Q4_K_M + 8K 上下文是舒适区,Q8_0 需要 40GB 级别或双卡。
- 磁盘:预留 3 倍模型体积,用于原始权重、量化文件和备份。
- Python:3.10 以上,用于压测脚本。
- 模型可用性确认:先去官方模型卡确认权重是否开放下载、chat template 里控制思考的字段名是什么(常见为
enable_thinking一类布尔字段,或reasoning_budget一类数值字段)。这一步决定了后面能不能用参数关掉思考。
分步骤部署
步骤一:拿到权重并选定量化
如果官方直接提供 GGUF,跳过转换;只给 safetensors 的话,用 llama.cpp 仓库里的转换脚本先生成 f16 GGUF,再量化。
```bash
转换(在 llama.cpp 仓库根目录执行)
python convert_hf_to_gguf.py /path/to/model --outfile model-f16.gguf --outtype f16
量化到 Q4_K_M
./build/bin/llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M
```
量化选型建议:
| 量化 | 体积(26B 量级) | 适用 |
|---|---|---|
| Q4_K_M | 约 15–16GB | 24GB 单卡默认选择,速度与质量平衡 |
| Q5_K_M | 约 18–19GB | 显存有余、对质量敏感 |
| Q8_0 | 约 26–27GB | 40GB 以上显存,做基准对照 |
| AWQ/GPTQ(4bit) | 约 15GB | 走 vLLM 路线时用 |
MoE 模型的特点是激活参数少,Q4 下的吐字速度通常明显快于同总参数的稠密模型,所以不必为了速度一味往低比特压。
成功标志:ls -lh 能看到生成的文件,体积与上表量级吻合;./build/bin/llama-quantize 结束时会打印各 tensor 的量化结果,无 error。
步骤二:启动 llama-server 并验证基础可用
```bash
./build/bin/llama-server \
-m /models/model-Q4_K_M.gguf \
--ctx-size 8192 \
--n-gpu-layers 999 \
--jinja \
--host 127.0.0.1 \
--port 8080
```
--n-gpu-layers 999 表示所有层都丢到 GPU;显存不够时往下调。--jinja 是关键,它让服务端使用模型自带的 chat template,思考开关才能生效。
成功标志:日志里出现 server is listening on http://127.0.0.1:8080,并且能看到模型加载耗时和 n_ctx = 8192。
步骤三:把思考开关接通
先确认当前版本支持哪种方式:
```bash
./build/bin/llama-server --help | grep -iE "reasoning|chat-template|jinja"
```
按可靠性从高到低有三种手段:
1. 模板关键字(首选):请求体里传 chat_template_kwargs,字段名以模型 template 为准。
2. 推理预算:若版本支持 --reasoning-budget,传 0 表示不产生思考内容。
3. AI 词典:系统提示词">系统提示词兜底:在 system 消息里写“直接给出最终答案,不要输出推理过程”。这条对所有模型都有效,但属于软约束。
关掉思考的请求:
```bash
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "local-model",
"messages": [{"role": "user", "content": "把这句话改成正式书面语:这事儿咱明天再聊。"}],
"chat_template_kwargs": {"enable_thinking": false},
"max_tokens": 256,
"stream": true
}'
```
打开思考只改一个字段:
```json
"chat_template_kwargs": {"enable_thinking": true}
```
成功标志:同一条 prompt,false 时首个返回内容直接是答案,true 时先出现成段的推理文本,再出现最终答案。如果两种设置下输出完全一样,说明开关没生效,回到步骤三开头检查 --jinja 和字段名。
步骤四(可选):用 vLLM 起一个高并发服务
需要多路并发时换 vLLM,它和 llama.cpp 的 OpenAI 兼容接口可以共用同一套客户端代码。
```bash
vllm serve /path/to/model \
--served-model-name local-model \
--max-model-len 16384 \
--gpu-memory-utilization 0.90 \
--reasoning-parser <以 vLLM 官方文档支持的 parser 列表为准>
```
--reasoning-parser 负责把思考内容和最终答案拆成不同字段返回,解析器名称必须和模型匹配,写错会报参数错误。
成功标志:日志出现 Uvicorn running on http://0.0.0.0:8000,且 /v1/models 能列出 local-model。
步骤五:测首 token 延迟与吐字速度
```python
import time, json, requests
URL = "http://127.0.0.1:8080/v1/chat/completions"
def bench(prompt, thinking, repeat=5):
payload = {
"model": "local-model",
"messages": [{"role": "user", "content": prompt}],
"chat_template_kwargs": {"enable_thinking": thinking},
"max_tokens": 512,
"stream": True,
}
ttfts, totals, tps = [], [], []
for _ in range(repeat):
t0 = time.perf_counter()
first, n_chunk = None, 0
with requests.post(URL, json=payload, stream=True, timeout=300) as r:
for line in r.iter_lines():
if not line or not line.startswith(b"data: "):
continue
body = line[6:]
if body == b"[DONE]":
break
delta = json.loads(body)["choices"][0].get("delta", {})
if delta.get("content"):
if first is None:
first = time.perf_counter()
n_chunk += 1
end = time.perf_counter()
ttfts.append((first - t0) * 1000)
totals.append(end - t0)
tps.append(n_chunk / (end - first) if first else 0)
avg = lambda x: sum(x) / len(x)
return {"ttft_ms": round(avg(ttfts), 1),
"total_s": round(avg(totals), 2),
"tok_s": round(avg(tps), 1)}
prompt = "一个筐里有 12 个苹果,拿走 1/3 后又放进 5 个,现在几个?只给数字。"
print("thinking=on ", bench(prompt, True))
print("thinking=off", bench(prompt, False))
```
关注三个数字:ttft_ms(首 token 延迟,用户感知的“卡不卡”)、total_s(端到端)、tok_s(吐字速度,反映吞吐)。少思考模式下首 token 延迟通常显著下降,因为省掉了整段推理链的生成时间;tok_s 本身不会变,变的是总 token 数。
步骤六:跑准确率对照实验
自建 40–60 道有唯一标准答案的题,覆盖四类:数学计算、结构化抽取(从一段文本里抠字段)、分类打标、多跳常识。
```python
import json, re, requests
def ask(q, thinking):
r = requests.post(URL, json={
"model": "local-model",
"messages": [{"role": "user", "content": q + "\n最后一行只输出答案本身。"}],
"chat_template_kwargs": {"enable_thinking": thinking},
"max_tokens": 1024,
"temperature": 0,
}, timeout=300)
return r.json()["choices"][0]["message"]["content"]
cases = json.load(open("cases.json")) # [{"q": "...", "a": "..."}, ...]
for mode in (True, False):
ok = 0
for c in cases:
out = ask(c["q"], mode)
if c["a"].strip().lower() in out.strip().lower()[-200:]:
ok += 1
print(f"thinking={mode} 准确率 {ok/len(cases):.1%} ({ok}/{len(cases)})")
```
temperature=0 让结果可复现。判定用“标准答案是否出现在输出末尾”这种粗规则就够做趋势判断,不必上 LLM 评审。
怎么读结果:把两个模式的准确率差和步骤五的延迟差放在一起看。如果准确率只掉几个百分点、首 token 延迟降一半以上,那么分类、抽取、格式转换这类请求就可以默认关思考;数学和多跳推理保留开思考,或者做一个前置小模型判断题型再路由。
验证部署是否成功
三条命令逐条跑:
```bash
1. 服务存活
curl -s http://127.0.0.1:8080/health
2. 模型已注册
curl -s http://127.0.0.1:8080/v1/models | head -c 300
3. 端到端一问一答
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"local-model","messages":[{"role":"user","content":"用一句话说明什么是 MoE。"}],"max_tokens":128}'
```
预期:第 1 条返回 {"status":"ok"} 一类内容;第 2 条返回 JSON 里包含模型 id;第 3 条 choices[0].message.content 是一段通顺中文且非空。再补一条:把 enable_thinking 分别设 true/false 各请求一次,两次输出的长度形态应明显不同。
常见报错与解决
CUDA out of memory / ggml_backend_cuda_buffer_type_alloc_buffer: allocating ... failed
原因:权重加 KV cache 超过显存,常见于上下文开太大或量化选得太高。
解决:先降上下文再降层数,最后才降量化。
```bash
./build/bin/llama-server -m model-Q4_K_M.gguf --ctx-size 4096 --n-gpu-layers 40
```
思考开关不生效:true 和 false 输出一模一样
原因:启动时没加 --jinja,服务端走了默认模板;或 chat_template_kwargs 里的字段名与模型 template 里的变量名不一致。
解决:确认启动参数含 --jinja,然后打开模型目录下的 tokenizer_config.json,在 chat_template 字段里搜 thinking,用里面出现的变量名,而不是凭印象写。
```bash
grep -o "enable_thinking\|reasoning_budget\|thinking" tokenizer_config.json | sort -u
```
llama_model_load: error loading model: unknown model architecture
原因:llama.cpp 版本早于模型架构合入时间。
解决:更新到官方仓库当前 release 后重新编译;若仍不支持,改用官方已经适配的后端(如 vLLM)加载原始权重。
```bash
git pull && cmake -B build -DGGML_CUDA=ON && cmake --build build -j
```
HTTP 400: The model \xxx\ does not exist
原因:请求体里的 model 字段与 --served-model-name 不一致。
解决:统一名称,或直接查已注册列表。
```bash
curl -s http://127.0.0.1:8000/v1/models | python -m json.tool
```
vLLM 报 The model's max seq len is larger than max_model_len
原因:--max-model-len 小于模型原生上下文,客户端又请求了长输入。
解决:调大 --max-model-len 并同步下调 --gpu-memory-utilization,或限制客户端输入长度。
后续维护
备份:量化后的 GGUF 是可复现产物,但重新量化耗时,建议把最终文件连同 sha256sum 一起归档;cases.json 和压测脚本一并进 Git,它们是你判断“升级有没有变差”的唯一依据。
升级:llama.cpp 和 vLLM 迭代快,升级前先记录当前基线(首 token 延迟、吐字速度、准确率三个数),升级后重跑同一套脚本对比。模型侧的 chat template 变更会直接影响思考开关的字段名,换模型时优先检查这一点。
日志与监控:llama-server 用 --log-file 落盘;vLLM 暴露 /metrics,可直接接 Prometheus,重点看请求排队时长、TTFT 分位数和显存占用。把 TTFT 的 P95 设成告警项,比看平均值更能反映真实体验。
容量规划:并发上来后先看 KV cache 占用。上下文从 8K 提到 32K,KV cache 会成倍增长,原本宽裕的显存会突然吃紧。按“目标并发数 × 平均上下文长度”反推需要的显存,比按模型体积估算靠谱。
