6. 性能测试
6.1 测试脚本
存放到 ~/llama-bin/bench.py:
#!/usr/bin/env python3
import time, json, urllib.request, concurrent.futures, statistics
API = "http://localhost:8090/v1/chat/completions"
MODEL = "qwen3.8-27b"
def chat(prompt, max_tokens, temp=0):
payload = {"model": MODEL, "messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens, "temperature": temp, "stream": True}
req = urllib.request.Request(API, data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
start = time.time(); first = None; tok = 0
try:
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data: "):
d = line[6:]
if d == "[DONE]": break
try:
o = json.loads(d)
c = o.get("choices", [{}])[0].get("delta", {}).get("content")
if c:
if first is None: first = time.time()
tok += 1
except: pass
except: pass
total = time.time() - start
ttft = first - start if first else 0
return ttft, total, tok
def sec(t):
print(f"\n{chr(61)*64}\n {t}\n{chr(61)*64}")
# ① 吞吐量
sec("① 吞吐量测试")
tr = []
for label, prompt, mx in [
("短输出 100", "用一句话介绍人工智能。", 100),
("中输出 500", "请写一段关于春天的短文,约200字。", 500),
("长输出 1000", "请写一篇关于AI发展历程的文章,尽量详细。", 1000),
("超长 2000", "请详细解释量子计算原理,写3000字文章。", 2000),
]:
ttft, total, tok = chat(prompt, mx)
gt = total - ttft
tps = tok / gt if gt > 0 else 0
tr.append({"l": label, "ttft": ttft, "tps": tps})
print(f" {label:<12} TTFT:{ttft*1000:>6.0f}ms {gt:>6.2f}s {tok:>5}tok {tps:>6.1f}tok/s")
print(f" 平均: {statistics.mean(r['tps'] for r in tr):.1f} tok/s")
# ② 并发
sec("② 并发测试")
for n in [2, 4, 8]:
def w(i): return chat(f"{i}+{i}等于多少?", 50)
s = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=n) as p:
results = [f.result() for f in [p.submit(w, i) for i in range(n)]]
wall = time.time() - s
total_tok = sum(r[2] for r in results)
print(f" 并发={n} 耗时:{wall:.2f}s tokens:{total_tok} 吞吐:{total_tok/wall:.1f}tok/s")
# ③ 稳定性
sec("⑥ 稳定性测试 (5次)")
st = []
for i in range(5):
ttft, total, tok = chat("用一句话介绍机器学习。", 100)
gt = total - ttft
tps = tok / gt if gt > 0 else 0
st.append({"ttft": ttft, "tps": tps})
print(f" 第{i+1}次: TTFT:{ttft*1000:.0f}ms {tok}tok {tps:.1f}tok/s")
print(f" TTFT: min={min(r['ttft'] for r in st)*1000:.0f} max={max(r['ttft'] for r in st)*1000:.0f}")
print(f" 速度: min={min(r['tps'] for r in st):.1f} max={max(r['tps'] for r in st):.1f} avg={statistics.mean(r['tps'] for r in st):.1f}")
6.2 实测结果
吞吐量:
| 输出长度 | TTFT | 生成耗时 | Tokens | 速度 |
|---|---|---|---|---|
| 短 (100) | 5132ms* | 0.58s | 32 | 55.2 tok/s |
| 中 (500) | 619ms | 2.62s | 147 | 56.2 tok/s |
| 长 (1000) | 623ms | 17.35s | 1000 | 57.7 tok/s |
| 超长 (2000) | 660ms | 34.68s | 1839 | 53.0 tok/s |
*首次含冷启动加载,后续 TTFT 稳定 ~600ms。平均 55.5 tok/s
并发:
| 并发数 | 总耗时 | 总Tokens | 吞吐 |
|---|---|---|---|
| 2 | 1.49s | 7 | 4.7 tok/s |
| 4 | 2.64s | 19 | 7.2 tok/s |
| 8 | 4.39s | 46 | 10.5 tok/s |
并发下 TTFT 线性增长(串行处理,--parallel 1)
温度影响:
| 温度 | TTFT | 速度 |
|---|---|---|
| 0 | 333ms | 53.4 tok/s |
| 0.5 | 451ms | 43.0 tok/s |
| 1.0 | 450ms | 46.2 tok/s |
长上下文:
| 上下文 | TTFT | 速度 |
|---|---|---|
| 短 (~200字) | 622ms | 46.4 tok/s |
| 长 (~1600字) | 1439ms | 55.1 tok/s |
稳定性 (5次相同请求):
| 指标 | 最小 | 最大 | 平均 |
|---|---|---|---|
| TTFT | 153ms | 624ms | 305ms |
| 速度 | 48.7 | 52.7 | 51.8 tok/s |
速度波动 <8%,非常稳定
口径说明:上表测于未启用 k4v 时期,且为纯文本负载。启用 k4v 后的解码表现见 §6.3。
两组数字不可直接比较——负载类型不同(散文 vs 逐字重现)。
6.3 投机解码专项(k4v)
llama.cpp 的投机解码分支支持多个投机器同时启用,--spec-type 吃逗号清单。
在原有 draft-mtp(模型内建 MTP 头自投机)之上叠加 ngram-map-k4v(从已生成上下文做
n-gram 查表,命中一次吐最多 48 个 draft token),二者互补:n-gram 命中时是"猜一大段",
没命中就退回 MTP,所以不会拖慢其他内容。
--spec-type ngram-map-k4v,draft-mtp \
--spec-draft-n-max 3 \
--spec-ngram-map-k4v-size-n 32 \
--spec-ngram-map-k4v-size-m 48 \
--spec-ngram-map-k4v-min-hits 1
零 VRAM 成本、零重编,已在 llama.sh 中默认开启(K4V=0 可关)。
实测结果
方法:temp=0、每项 2 轮取中位数、读 timings.predicted_per_second。
两配置 n_gen 完全一致(407/365/500/271/630),故无「输出长度不同造成的假信号」。
| 工作负载 | 仅 draft-mtp | +k4v (n=32) | 变化 | 接受率变化 |
|---|---|---|---|---|
code_refactor |
86.1 | 112.6 | +30.7% | 1.00 → 0.96 |
repeat_synth |
86.0 | 122.4 | +42.3% | 1.00 → 0.99 |
template_batch |
47.2 | 46.7 | −1.1% | 0.37 → 0.37 |
reasoning |
72.9 | 72.1 | −1.1% | 0.79 → 0.79 |
prose |
47.2 | 46.7 | −1.1% | 0.38 → 0.38 |
负项均落在 ±1.1%,与同配置重跑的噪音水平一致。经 systemd 重启后复验:112.3 / 122.7。
两个反直觉点
- 接受率下降不是退化。
code_refactor接受率 1.00 → 0.96 看着像变差,实则
草稿 token 从 306 涨到 400、总吞吐 86 → 112。接受率必须与drafted一起看,
孤立看会误判(参见 §9.6 记录的同类教训)。 - 收益来自「逐字重现」,不是「格式相似」。
| 你的任务长这样 | k4v 收益 |
|---|---|
| 让模型把整份文件/文章原样吐回、只改几行(改代码、翻译校对、逐条批注) | +30~60%,最值钱 |
| 大量重复的结构化输出(JSON/YAML/SRT/表格批次) | 中等 |
| 逐条照抄原文再加判定(规格检查、RAG 引述) | 小幅或持平 |
| 格式固定但内容全新(套模板产新数据) | 持平(size-n 取 12 时倒扣) |
| 纯创作、思考链、对话 | 无感(也不会变慢) |
对 dsh 而言第一行才是主场:agent 改文件时经常要把整个文件回吐一遍,这就是 +30.7% 那档。
为什么 size-n 必须是 32
size-n 决定「要比对多长的前文才算命中」。拉长 = 更严格 = 少开火但更准。
| size-n | code_refactor | template_batch | 字幕重排类 | 合成重复 |
|---|---|---|---|---|
| 关闭 | 86.1 | 47.2 | 106.1 | 86.0 |
| 12 | 166.4 | 68.4 −5% |
95.8 −10% |
178.6 |
| 24 | 169.1 | 70.2 | 100.9 ![]() |
— |
| 32 | 157.0 | 71.1 | 105.4 | 237.0 |
| 40 | 136.0 | 71.9 | 104.8 | 236.9 |
(上表为来源帖在 RTX 4080S / 7900 XTX 上的扫描值,用于说明趋势,本机仅实测 n=32 一档。)
size-n 太小 → n-gram 一直开火、一直猜错,白做的 draft 计算比不开启还多。
典型反例是"文字照抄、时间轴改掉"的字幕重排:n-gram 看到前文就把旧时间戳一起预测出来,
每到时间轴就撞墙,draft 992 个只接受 510。格式重复但内容要换的任务,n-gram 是负资产。
不要动 min-hits
来源帖试过 --spec-ngram-map-k4v-min-hits 2(要求 n-gram 出现两次才采用),
结果各项 draft 计数与完全关闭 k4v 逐项相同——等于直接关掉功能。保持 1。
不要设 p_min,n_max 保持 3
n_max=5 + p_min=0.4 在来源帖实测中「重复内容看起来更快,但推理 −22%、散文 −37%」:
draft 猜长了接受率上不去(散文只有 0.39),每步都在做白工。n_max 越大,
可预测内容越快、不可预测内容越慢——纯创作偏多用 3,结构化输出偏多可试 4~5。
来源:https://lcz.me/topic/1398(7900 XTX / Vulkan 实测报告,附 RTX 交叉验证)
7. 日常运维
7.1 常用命令
# 启动模型(自动带 shim;有 systemd 单元且无覆盖变量时自动走 systemctl)
~/llama-bin/llama.sh 38
# systemd 托管时的启动/重启(推荐;走 llama.sh 38 --fg,会继承脚本内全部参数)
systemctl --user start qwen38
systemctl --user restart qwen38
systemctl --user stop qwen38 # 手动停,Restart=on-failure 不会拉回
# 查看状态
~/llama-bin/llama.sh status
# 停止
~/llama-bin/llama.sh stop
# 切换模型
~/llama-bin/llama.sh 36 --use
# 临时覆盖参数(单次生效)
K4V=0 ~/llama-bin/llama.sh 38 # 关掉 ngram-k4v,只留 MTP
NMAX=2 CRAM=0 ~/llama-bin/llama.sh 38 # 关掉 KV 驻内存
CTX=65536 ~/llama-bin/llama.sh 38 # 加大窗口前先读 §9.8:逼近上限会掉进 GTT
# shim 单独管理
~/llama-bin/shim.sh status
~/llama-bin/shim.sh restart
# 查看日志
tail -f /tmp/qwen38-vulkan.log # llama.cpp
tail -f /tmp/dsh-llm-shim.log # shim
journalctl --user -u qwen38 -f # systemd 下 llama.cpp 的日志
改
llama.sh后不需要动 systemd 单元 —— 单元的ExecStart是llama.sh 38 --fg,
会自动继承脚本里的新参数。systemctl --user restart qwen38即可生效。
7.2 健康检查
# llama.cpp
curl -s http://127.0.0.1:8080/health | python3 -m json.tool
# shim
curl -s http://127.0.0.1:8090/health | python3 -m json.tool
# 模型列表
curl -s http://127.0.0.1:8090/v1/models | python3 -m json.tool
# 快速测试
curl -s http://127.0.0.1:8090/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.8-27b","messages":[{"role":"user","content":"你好"}],"max_tokens":50}'
8. 故障排查
8.1 常见问题
| 症状 | 原因 | 解决 |
|---|---|---|
| shim: UPSTREAM_UNREACHABLE | llama.cpp 未启动 | ~/llama-bin/llama.sh 38 |
| CONTEXT_OVERFLOW | 上下文超限 | /compact 压缩或新开会话 |
| 工具调用参数错乱 | 绕过 shim 直连 llama.cpp | 确保 baseURL 指向 :8090 |
| 模型跑飞 (32k token) | llama.cpp 解析器 bug | 必须经 shim,不能直连 |
| 端口冲突 | 8080/8081/8090 被占用 | 改 P38_PORT/SHIM_PORT 环境变量 |
| 显存不足 OOM | 两模型同时运行 | 只能跑一个,先 stop 再启另一个 |
| MTP 头被忽略 | --spec-type 未生效 | 检查日志 grep "nextn.*ignoring" |
8.2 日志位置
| 服务 | 日志文件 |
|---|---|
| llama.cpp 3.8 | /tmp/qwen38-vulkan.log |
| llama.cpp 3.6 | /tmp/qwen36-vulkan.log |
| shim | /tmp/dsh-llm-shim.log |
| DSH | ~/.dsh/logs/ |
8.3 完全重置
# 停止所有服务
~/llama-bin/llama.sh stop
~/llama-bin/shim.sh stop
# 验证端口已释放
ss -tlnp | grep -E "8080|8081|8090"
# 重新启动
~/llama-bin/llama.sh 38
9. 诊断与实战记录
9.1 直连失败的具体表现与根因
直连 llama.cpp 的 OpenAI 端点时:
assistant/message: content: [] stopReason: "length"
usage: output 32768 token 耗时 7.7 分钟 → 0 可用输出
根因不在模型,而在 llama.cpp b11223 自带的 tool-call 解析器(该 build 报 chat_format: peg-native)。用 /apply-template + /completion 绕过解析器取模型原始输出:
<tool_call> <function=bash> <parameter=command> ls -1 /home/hnz/桌面 | wc -l </parameter>
<parameter=description> Count entries in Desktop directory </parameter> </function> </tool_call>
49 token、格式完全正确、正常 EOS。但同一个请求走 /v1/chat/completions(带 tools)时,解析结果是:
{"command": "ls -1 /home/hnz/桌面 | wc -l </parameter> <parameter=description> ... </tool_call>\n<tool_call>..."}
即第二个参数起的整段被吞进第一个参数的值里;而且这种失败会诱发模型把同一个 <tool_call> 重复几百遍直到打满 max_tokens。模型自带模板明确允许参数值跨行("that can span multiple lines"),解析器却处理不了。
对照实验(同一份真实 DSH 请求,24 个工具):
| 路径 | 结果 |
|---|---|
直连 :8081 |
0/10 成功,每次跑飞约 30s / 满 max_tokens |
| 经 shim | 10/10 成功,每次约 1s / 49~51 token |
采样侧只能缓解(--dry-multiplier 0.8 能让它不再无限重复,但参数照样被解析坏),换 build 可能有效但会丢掉这套 MTP 调优(69 tok/s),所以选了中间层方案。
后续复核推翻了这组对照(2026-09-29,见 §1.2 与 §9.7):当前配置下直连可正常完成
工具调用,0/10 复现不出来。下文保留为历史记录,不代表当前实际行为。该故障的现场记录已归档:本目录
local_model.sh.txt
(模型输出的原始抓取,含 4058 个零宽字符 U+200B 残留的重复<tool_call>标签,
bash -n直接语法错误 —— 正是「重复到打满max_tokens」的产物)。
对照另一个模型:qwen3.8-27b(2026-09-29 实测,当时 n_ctx 还是 262144;现已改为 131072,见 §9.8)经 shim 的干净工具调用 5/5、每次 1~2 秒;纯文本回复在流式与非流式下都与直连逐字节一致(temperature 0 + 固定 seed 对齐)。3.6/3.8 两个实例的严格模式提示双向验证通过。
9.2 验证记录
- 解析单测
node ~/llama-bin/shim-parser-test.mjs→ 7/7(含<bash>漏写function=的容错)。 - 真实请求重放 10/10 干净
tool_calls,每次 1s。 - headless 端到端(
dsh --profile headless --patch dsh-shim-overlay.yml "…"):- 单步任务:
tool-call bash → "3" → 最终回答 → turn/end: completed - 多步任务:一轮里并行发出 3 个 bash + 1 个 grep,再补一次 bash,最终答案
(9 个文件 / 1 个子目录 /SHIM_PORT默认 8090)全部正确。 - 缓存命中正常(cacheRead 4928 / 5402 token)。
- 单步任务:
- 失败路径:上游不可用时返回
HTTP 502+ 明确错误信息;/health只反映 shim 自身。 - k4v 启用后的复验(2026-09-29):经 systemd
llama.sh 38 --fg启动后,
投机解码基准复现手工启动的数值(112.3 / 122.7),dsh 经 shim 端到端冒烟正常。 - 默认模型切换:
dsh-shim-overlay.yml的默认模型已由qwen3.6-27b改为qwen3.8-27b,
与llama.sh默认保持一致。改前若 3.6 未启动,shim 会返回
502 UPSTREAM_UNREACHABLE并提示当前实际在跑哪个实例。
9.3 修复记录(都是 shim / 脚本侧的问题,DSH 走流式不受影响)
| 症状 | 原因 | 修法 |
|---|---|---|
| 非流式纯文本回复只剩最后 ~11 个字符 | 防标签跨 chunk 的 sent 指针在非流式路径也前进,但那条路并不发送 |
只在真正流式时推进 sent |
| 工具调用之前的正文被丢掉 | 流式分支没把保留尾巴里的正文发出去 | 发 tool_calls 前先补发 prefixText |
shim.sh stop 误杀无关进程(连调用它的 shell 都被杀过) |
pkill -f "dsh-llm-shim.mjs" 按文件名裸匹配,vim 该文件 也会中招 |
启动写 PID 文件,停止时按 PID + 只认 argv[0] 含 node 的进程 |
llama.sh status 偶尔显示「停止」而模型其实在跑 |
pgrep 在某些环境下看不到别的进程 | 状态行加端口健康检查兜底 |
9.4 上下文限制与超限处理
- 本地模型的上下文比云端小得多,切模型前要看一眼历史长度。
qwen3.8-27b的
n_ctx现在是 131072(不再是 262144,见 §9.8),maxTokens: 32768,
所以留给 prompt 的预算约 9.8 万 token——本地会话比想象的短得多。
把一个已经很长的云端会话直接切到本地,llama.cpp 会回400 exceed_context_size_error。正确做法:- 另开一个新会话用本地模型(最稳);或
- 先在当前会话
/compact压缩,再/model切到本地; - 确实需要更长窗口,见 §9.8 关于
CTX与显存的取舍,不要盲目调大。
现在这种情况会明确报CONTEXT_OVERFLOW并给出 token 数,不会再静默无响应。
实测 DSH 侧呈现为dsh: INVALID_REQUEST: 400: 本地模型上下文不足:请求 N token,而模型 ctx 只有 131072。请在 DSH 里 /compact 压缩会话,或另开一个新会话。,
并且不再重试 5 次(INVALID_REQUEST不在llm-retry的重试类别里,立刻失败并给出原因)。
超限之后的"压缩死循环"(2026-09-29 实测):会话一旦超过本地窗口,DSH 会自动
触发压缩,但压缩请求本身也要把整段历史发给同一个模型,于是照样超限:
而13:08:09 provider=local model=qwen3.8-27b ctx=262144 ← 当时的配置,现为 131072 13:08:11 compaction/start 13:08:13 compaction/end error: 400 请求 330277 token > ctx 262144 13:08:15 turn/end error: 400 CONTEXT_OVERFLOW(请求 329927 token)/compact不接受参数、用当前选中的模型,所以在本地模型上手动压缩同样会失败。
正确顺序(二选一):- 另开新会话再选本地模型(已验证:provider=local/qwen3.8-27b,2 次工具调用、
turn/end: completed,prompt 仅约 5.5k token); - 想保留本会话:先
/model切回 deepseek-flash → 发一条消息让切换生效 →/compact
(云端 100 万窗口才压得动)→ 确认压缩成功后,再/model切回本地。
注意切换生效前触发的压缩仍会用旧模型(实测踩过),所以要先发一条消息。
- 另开新会话再选本地模型(已验证:provider=local/qwen3.8-27b,2 次工具调用、
9.5 日常使用注意
- 一条命令就够:
llama.sh 36|38在模型就绪后会自动shim.sh start(幂等,已在跑就跳过);
llama.sh stop与llama.sh 36 --use会连带停掉 shim。llama.sh status也会打印 shim 状态行。
(改动前已备份为~/llama-bin/llama.sh.bak-*、shim.sh.bak-*。) - 单独控制 shim:
~/llama-bin/shim.sh start|stop|restart|status。
升级过dsh-llm-shim.mjs之后要shim.sh restart才会生效(下次llama.sh 36|38起模型也会自动重启它)。 - 端口占用兜底:若 :8090 已被别的实例服务(例如别的终端拉起的),
shim.sh start会跳过启动
而不是抢端口;llama.sh status/shim.sh status也会把这种情况算作“运行”。 - 模型要对上:选 3.6 就得跑
llama.sh 36,选 3.8 就得跑llama.sh 38(两个实例互斥)。
对不上时 shim 会直接报错并说明现在跑的是哪个、该用哪条命令,不会拿另一个模型顶替。
llama.sh 36 --use切换模型时 shim 不用重启(它按模型名转发)。 - shim 没启动时本地路由会 502;此时 GUI 里
/model切回deepseek-flash即可。 - 本会话里曾临时拉起过一个 shim 实例;
llama.sh stop/shim.sh stop都能把它停掉,
之后由llama.sh正常接管。 - 日志:
/tmp/dsh-llm-shim.log(SHIM_VERBOSE=1时记录每次解析结果与重试)。
9.6 治本方向(上游修复)
- 换/升级 llama.cpp build —— 该 build 的 PEG 工具解析器是根因;上游修好后 shim 可以直接摘掉。
- 若换 build:本方案与 MTP 调优无关,shim 不依赖
--spec-type draft-mtp。 - 上报点:模板文档明示参数值可跨行,而 PEG 解析器处理不了跨行值,且失败会诱发重复生成。
- 另外值得反馈 DSH 上游:从大窗口模型(deepseek 100 万)切到小窗口模型(本地 26.2 万)时,
自动压缩会用切换前的模型调度、且压缩请求本身可能超过目标窗口 —— 于是"越超限越压不动"。
建议改成按目标模型窗口判断、并允许指定压缩用的模型。
9.7 补充复核:shim A/B 与多轮稳定性
2026-09-29 在当时配置(Qwen3.8-27B @262144、k4v 开启)下重做 A/B,
两份 overlay 仅 baseURL 端口不同。
A/B 结果
| 路径 | 结果 |
|---|---|
经 shim :8090 |
66 个文件 / ubuntu-ai ✓ |
直连 :8080 |
66 个文件 / ubuntu-ai ✓(并多解释了 find -type f 的口径) |
coerce() 类型强转专项
shim 的 coerceCall() 只在模型把数字/布尔发成带引号字符串时才触发修补
(dsh-llm-shim.mjs 中 coerce():number→Number()、布尔映射 true/yes/1
false/no/0、
array/object→JSON.parse(),且仅处理 typeof v === "string")。
为最大化触发机会,构造 7 字段全部 required 的工具
(string / integer / integer / boolean / number / array / object),逼模型逐个输出:
| 路径 | 样本 | 类型全对 | 字符串化 |
|---|---|---|---|
直连 :8080 |
12 | 12/12 | 0 |
经 shim :8090 |
6 | 6/6 | 0 |
直连返回 {"task_name":"backup","delay_ms":1500,"repeat":3,"enabled":true,"threshold":0.75,"tags":["system","disk"],"opts":{"mode":"full"}}
—— 全是原生 JSON 类型,18 次采样零次需要 coerce 修补。
这不能证明
coerce永远无用。 它是"模型偶尔把数字发成字符串"的兜底,
而本次采样温度固定 0.3、提示固定,输出高度一致,未触发该边缘情况。
真实长会话的输出分布更杂。建议保留 shim、切直连观察几天,再决定是否下线。
多轮对话不存在「越聊越慢」
社区方案建议加稳定性四件套
--no-kv-unified --parallel 1 --ctx-checkpoints 2 --cache-ram 4096 来修多轮掉速。
本机 4 轮实测(cache_prompt:true)显示该问题不存在:
| 轮次 | cache_n(复用前缀) |
prompt_n(本轮新算) |
decode t/s |
|---|---|---|---|
| 1 | 0 | 67 | 64.1 |
| 2 | 366 | 60 | 66.4 |
| 3 | 725 | 49 | 70.9 |
| 4 | 1073 | 47 | 64.8 |
第 2 轮起每轮只重算 47~67 个 token,decode 无衰减(甚至微升)。
原因:本机一直是 --parallel 1,单槽下不存在 kv_unified 跨槽共享导致的状态失效。
那套参数「免费」的前提是解决一个本机没有的问题,故不加。
复现命令
DSH=~/.npm/_npx/1e7f6d9597241db0/node_modules/.bin/dsh
# 两份 overlay 仅 baseURL 端口不同(:8080 / :8090),其余完全一致
$DSH --profile headless --patch ~/llama-bin/dsh-shim-overlay.yml \
headless "统计 ~/llama-bin 下的文件数并读出 /etc/hostname"
# 类型强转压测脚本原为 /tmp/opencode/coerce-test.py(临时目录,已随重启丢失)
# 需要复现时按 §9.7 的 7 字段 schema 描述重写即可
9.8 CTX 从 256K 降到 128K 的根因(Vulkan GTT 回落)
2026-09-30 发现 llama.sh 38 慢到不可用(decode 0.49 tok/s),根因不是 shim、不是投机解码,
而是**-c 262144 把权重从显存挤进了 GTT**。
机制:模型权重 16.4GB + 256K 的 KV cache ≈ 26.3GB,超出 25.75GB 显存在 DEVICE_LOCAL 上
能分配到的额度,vkAllocateMemory 失败后 Vulkan 后端静默改用 GTT(系统内存)映射。
服务照常起来、/health 报 ok、n_ctx 也如实报 262144 —— 但每个 token 都要走 PCIe。
| 指标 | -c 262144 |
-c 131072 |
|---|---|---|
vis_vram_used |
567 MiB | 权重完整驻留 |
gtt_used |
23490 MiB | ~2.5 GB(KV) |
| decode | 0.49 tok/s | 正常(数十 tok/s) |
| prefill | 30 t/s | 400+ t/s |
日志里的特征行:print_timing: ... 0.49 tokens per second(decode 阶段)、
prompt processing ... 30.08 tokens per second(prefill 阶段)。
"多分配的 KV 不花钱"是错的。 只有在 KV 仍能装进 DEVICE_LOCAL 时这句话才成立;
一旦溢出到 GTT,代价是每个 token 都走系统内存带宽。换句话说,判断依据是"装不装得下",
不是"填不填得满"。
显存扫描(-cram -1 下,KV 在系统内存,只算权重+运行时开销):
| ctx | 显存占用 | 备注 |
|---|---|---|
| 32K | ~17.4 GB | 安全 |
| 128K | ~21.2 GB | 当前默认,留 ~4.5 GB 余量 |
| 256K | ~26.3 GB | 爆,触发 GTT 回落 |
下界 64K:Hermes Agent 硬性要求 context >= 64000,低于此直接拒绝连接。
顺带加了两个参数:
-cram -1:KV cache 显式放系统内存,不与权重抢 DEVICE_LOCAL。
对混合架构(本模型仅 16 层真注意力)代价很小,却能给权重留出余量。P38_CTX默认 131072:CTX=65536 ./llama.sh 38可临时覆盖,
但每次加大都必须重测llama.sh status的显存占用,逼近 ~24GB 时立刻退回。
P36_CTX 仍是 196608,因为 Qwen3.6 的实测配置一直如此;若 3.6 也出现 0.49 tok/s,
按同样逻辑降级即可。
教训:llama.cpp 的性能参数(
-c/-ngl/-ub)互相耦合,改任何一个都要
看最终显存落点(vis_vram_used/gtt_used),不能只看单个参数的账面数字。
相关实测记录见~/llama-bin/OPTIMIZATION.md。
10. 附录:文件清单
| 文件 | 用途 |
|---|---|
~/llama-bin/llama.sh |
模型服务统一入口(36/38 启停、切换、状态;k4v 参数见 §6.3) |
~/llama-bin/OPTIMIZATION.md |
推理性能优化完整记录(显存账、ubatch、prefill 曲线、decode 溯源、各外部方案复核) |
~/llama-bin/dsh-llm-shim.mjs |
shim 本体 |
~/llama-bin/shim.sh |
start / stop / restart / status(风格与 llama.sh 一致) |
~/llama-bin/shim-parser-test.mjs |
解析逻辑离线单测(7 项,改动后跑一下) |
~/llama-bin/dsh-shim-overlay.yml |
headless 端到端验证用的 --patch 覆盖层(默认 qwen3.8-27b) |
~/llama-bin/dsh-direct-overlay.yml |
同上的直连版(:8080,无 shim),供 A/B 用 |
~/llama-bin/bench.py |
性能测试脚本(第 6 章) |
~/.config/qwen38/api.env |
服务端密钥(独立文件,不进脚本、不进 shell 环境) |
~/.config/systemd/user/qwen38.service |
systemd 单元,ExecStart=llama.sh 38 --fg(改 llama.sh 会自动继承) |
~/llama-bin/llama.sh.bak-* / shim.sh.bak-* |
改动前的脚本备份 |
~/文档/7900_dsh/README.md |
本文档目录的维护说明与归档清单 |
~/文档/7900_dsh/local_model.sh.txt |
§9.1「无限重复 tool_call」故障的现场抓取(非脚本,不可执行) |
| 目录 | 内容 |
| --- | --- |
~/models/ |
Qwen3.8-27B-UD-Q4_K_M.gguf、Qwen3.6-27B-Q4_K_M-mtp.gguf、mmproj-model-bf16.gguf |
~/.dsh/profiles/web/cordis.patch.yml |
DSH provider 路由(local → :8090/v1) |
/tmp/qwen38-vulkan.log / /tmp/qwen36-vulkan.log |
llama.cpp 日志 |
/tmp/dsh-llm-shim.log |
shim 日志(SHIM_VERBOSE=1 时记录每次解析结果与重试) |
~/.dsh/logs/ |
DSH 日志 |
−5%