一、背景与环境
Ternary-Bonsai-2-27B 把 27B 级模型压成约 5.9GB 的三值量化(PTQ1_0)GGUF,权重只剩 -1 / 0 / +1 三档。27B 的 FP16 权重约 54GB,单张 4090 的 24GB 显存根本放不下,不量化就无法本地运行。
| 项目 | 配置 |
|---|---|
| GPU / CPU | RTX 4090 24GB / 96 线程 |
| 系统 | ubuntu |
| 推理引擎 | llama.cpp(sudoingX fork,最终使用 bonsai2 分支) |
| 模型 | 基础版 ...-PTQ1_0.gguf、MTP 版 ...-PTQ1_0-mtp.gguf(约 5.9GB) |
整个调优过程遇到四个问题,本文按实际顺序拆解:
| # | 问题 | 定位结论 |
|---|---|---|
| 1 | 模型加载失败 | 内核不认识 PTQ1_0 类型 → 换分支 |
| 2 | 投机解码起不来 / 速度只有 56 t/s | MTP 需要专用模型 + MTP 修复内核 |
| 3 | MTP 到底值不值 | 实测是负收益,短上下文 89 > 85 t/s |
| 4 | 性能上限在哪 | 实测带宽 954 GB/s → 短上下文理论 173 t/s |
二、问题一:模型加载失败
2.1 现象
最小复现命令:
bash
./llama-server -m xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf
输出:
text
0.00.288.476 E gguf_init_from_reader: tensor 'output.weight' has invalid ggml type 143. should be in [0, 43)
0.00.288.481 E gguf_init_from_reader: failed to read tensor info
0.00.293.636 E llama_model_load: error loading model: llama_model_loader: failed to load model from
xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf
0.00.293.648 E llama_model_load_from_file_impl: failed to load model
0.00.293.682 E common_fit_params: encountered an error while trying to fit params to free device memory:
failed to load model
0.00.346.905 E gguf_init_from_reader: tensor 'output.weight' has invalid ggml type 143. should be in [0, 43)
0.00.346.909 E gguf_init_from_reader: failed to read tensor info
0.00.351.282 E llama_model_load: error loading model: llama_model_loader: failed to load model from
xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf
0.00.351.287 E llama_model_load_from_file_impl: failed to load model
0.00.351.291 E cmn common_init_: failed to load model 'xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf'
0.00.351.294 E srv load_model: failed to load model, 'xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf'
0.00.351.296 I srv operator(): operator(): cleaning up before exit...
0.00.364.873 E srv llama_server: exiting due to model loading error
报错出现两轮(0.288 与 0.346),是第一次加载失败后 llama-server 又尝试探测剩余显存所致。
2.2 根因:GGML 类型注册表越界
GGUF 只是容器格式,不限制量化算法 。每种张量编码在 ggml 里对应一个整数 ID,读取时做断言检查 type < GGML_TYPE_COUNT。本模型张量 ID = 143(PTQ1_0) ,而内核 GGML_TYPE_COUNT = 43(合法 0~42),越界断言失败,进程根本没进入权重加载与显存分配阶段。
不是文件损坏,是「文件用了新格式,引擎太旧」。
2.3 排查:被排除的假设
| 假设 | 验证方式 | 结论 |
|---|---|---|
| 显存不足 | common_fit_params ... failed to load model 是加载失败的连锁反应,此时尚未分配显存 |
❌ 排除 |
| 内核不支持该类型 | GGML_TYPE_COUNT = 43 vs 张量 ID 143 |
✅ 命中 |
| 模型文件损坏 | 未做 SHA256 校验 | ⚠️ 待验证 |
2.4 解决:换内核分支
真正要弄清的是分支组合,而不是「换个仓库」:
| 分支 | 内容 |
|---|---|
prism |
PrismML 原始分支,仅支持 PTQ1_0 加载 |
pr-ptq1-mmv |
PTQ1_0 的 mat-vec 优化内核 |
pr-hadamard-mtp |
MTP draft graph 的 Hadamard 修复 |
bonsai2 |
上面两个的叠加版,最终选它 |
bash
git fetch origin bonsai2
git checkout -b bonsai2 FETCH_HEAD # fetch 不建跟踪分支,需显式基于 FETCH_HEAD 创建
rm -rf build
cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=89
cmake --build build -j$(nproc)
确认内核真的换了,看提交历史而不是分支名:
bash
git log --oneline -5 # 找 hadamard / mtp fix / ptq1 mmv 之类提交
三、问题二:MTP 起不来,速度卡在 56 t/s
3.1 现象
加载成功了,但按下面的参数启动会直接退出:
bash
GGML_CUDA_GRAPH_OPT=1 GGML_CUDA_BATCH_INVARIANT=1 ./build/bin/llama-server \
-m xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf \
--host 0.0.0.0 --port 8000 -ngl 99 -c 131072 \
--spec-type draft-mtp --spec-draft-n-max 1 \
-fa on --cache-type-k q8_0 --cache-type-v q8_0 --parallel 1
text
0.00.110.995 I srv load_model: loading model 'xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf'
0.01.353.029 W operator(): failed to measure the memory of the extra model, fitting without it: failed to create llama_context from model
0.03.376.531 I cmn init: llama threadpool init, n_threads = 96
0.03.527.315 I common_speculative_init_result: creating MTP draft context against the target model 'xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf'
0.03.527.325 W llama_init_from_model: context type MTP requested but model doesn't contain MTP layers
0.03.527.325 E common_speculative_init_result: failed to create MTP context
0.03.527.331 E srv load_model: failed to create MTP context
0.03.542.503 E srv llama_server: exiting due to model loading error
3.2 根因:模型和内核都要「带 MTP」
这里有两个独立的前提,缺一不可:
- 模型侧 :
--spec-type draft-mtp是自投机解码,草稿由目标模型自带的 MTP(Multi-Token Prediction)层生成。基础版权重里没有这些层,所以报model doesn't contain MTP layers。必须换成独立的 MTP 版模型文件。 - 内核侧 :MTP 草稿图存在已知缺陷,需要包含
pr-hadamard-mtp修复的分支。
3.3 解决与效果
换成 MTP 版模型 + bonsai2 内核后,效果是断崖式的:
| 指标 | 修复前 | 修复后 |
|---|---|---|
draft acceptance |
40 ~ 61% | 94.96% |
mean len |
2.41 ~ 3.43(抖动) | 2.64(稳定) |
tg 稳态 |
56 ~ 58 t/s | 64 ~ 69 t/s |
tg_3s 峰值 |
80 t/s | 90.26 t/s |
graphs reused |
148 ~ 304 | 420 |
text
1.27.582.535 I slot print_timing: id 0 | task 557 | n_gen = 467, tg = 77.23 t/s, tg_3s = 90.26 t/s
1.27.746.349 I slot print_timing: id 0 | task 557 | prompt eval time = 2033.45 ms / 2127 tokens ( 0.96 ms per token, 1046.01 tokens per second)
1.27.746.355 I slot print_timing: id 0 | task 557 | eval time = 6197.60 ms / 484 tokens ( 12.83 ms per token, 77.93 tokens per second)
1.27.746.356 I slot print_timing: id 0 | task 557 | total time = 8231.05 ms / 2611 tokens
1.27.746.357 I slot print_timing: id 0 | task 557 | graphs reused = 420
1.27.746.364 I slot print_timing: id 0 | task 557 | draft acceptance = 0.94961 ( 245 accepted / 258 generated), mean len = 2.64
接受率 95% 是核心变量:40~60% 时大量草稿被浪费,验证开销无法摊销;95% 时 MTP 的收益才真正释放。
一个重要的调参纪律:一次只改一个参数 。--spec-draft-n-max 的取值也不是越大越好------n-max 超过 2~3 后,多接受的 token 有限,被拒绝的草稿却大幅增加。
四、问题三:MTP 到底值不值
4.1 反直觉的实测结果
接受率 95%、峰值 90 t/s,看起来应该一直开着。但把同一台机器上的两组数据摆在一起,结论正好相反:
text
短上下文 + MTP(n-max=3)
0.10.850.019 I slot print_timing: id 0 | task 0 | prompt eval time = 412.41 ms / 62 tokens ( 6.65 ms per token, 150.33 tokens per second)
0.10.850.028 I slot print_timing: id 0 | task 0 | eval time = 2983.73 ms / 256 tokens ( 11.70 ms per token, 85.46 tokens per second)
0.10.850.057 I slot print_timing: id 0 | task 0 | draft acceptance = 0.78481 ( 124 accepted / 158 generated), mean len = 2.68
短上下文 + 无 MTP
0.21.349.976 I slot print_timing: id 0 | task 0 | prompt eval time = 385.89 ms / 62 tokens ( 6.22 ms per token, 160.67 tokens per second)
0.21.349.984 I slot print_timing: id 0 | task 0 | eval time = 2853.89 ms / 256 tokens ( 11.19 ms per token, 89.35 tokens per second)
0.21.349.986 I slot print_timing: id 0 | task 0 | total time = 3239.78 ms / 318 tokens
0.21.349.996 I slot print_timing: id 0 | task 0 | graphs reused = 254
| 配置 | 上下文 | decode 速度 |
|---|---|---|
| 带 MTP | 短(317 token) | 85.46 t/s |
| 不带 MTP | 短(317 token) | 89.35 t/s |
| 带 MTP | 60K | 55 ~ 58 t/s |
| 不带 MTP | 74K | 60.33 t/s |
两个场景下,不带 MTP 都更快。
4.2 为什么
短上下文时瓶颈是权重读取,每步时间几乎恒定(读 5.5GB 权重)。开 MTP 后,每步要额外跑草稿生成与验证------多接受 1~2 个 token 的收益,抵不上每步耗时增加的成本。
长上下文时,草稿 token 的验证同样要读全部 KV cache,上下文越长,验证开销越大,进一步吃掉了「减少步数」的收益。
4.3 结论
💡 在本机这套「4090 + PTQ1_0」组合下,MTP 是负收益,可以彻底关掉。
最终采用的最简启动命令:
bash
GGML_CUDA_GRAPH_OPT=1 GGML_CUDA_BATCH_INVARIANT=1 ./build/bin/llama-server \
-m xxx/Ternary-Bonsai-2-27B/Ternary-Bonsai-2-27B-PTQ1_0.gguf \
--host 0.0.0.0 --port 8000 -ngl 99 -c 131072 \
-fa on --cache-type-k q8_0 --cache-type-v q8_0 --parallel 1
⚠️ 顺带否掉的另一个方案:把 KV Cache 换成
q4_0。显存确实省,但decode 速度会掉约 37%(解量化开销),在显存本就富余(余量 13.5GB)的情况下得不偿失。
五、问题四:性能上限到底在哪
5.1 Prompt 处理:换内核后翻倍
新内核在 prefill 阶段的收益比 decode 更明显:
text
2.59.266.164 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 20809, progress = 0.28, t = 7.90 s / 2635.15 tokens per second
3.00.145.480 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 22857, progress = 0.31, t = 8.78 s / 2604.49 tokens per second
3.01.054.408 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 24905, progress = 0.34, t = 9.68 s / 2571.52 tokens per second
3.01.988.459 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 26953, progress = 0.36, t = 10.62 s / 2538.19 tokens per second
3.02.954.663 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 29001, progress = 0.39, t = 11.59 s / 2503.28 tokens per second
3.03.972.932 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 31049, progress = 0.42, t = 12.60 s / 2463.53 tokens per second
3.37.088.140 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 73933, progress = 1.00, t = 45.72 s / 1617.13 tokens per second
3.37.198.725 I slot print_timing: id 0 | task 259 | prompt processing, n_tokens = 73943, progress = 1.00, t = 45.83 s / 1613.45 tokens per second
3.40.333.406 I slot print_timing: id 0 | task 259 | n_gen = 181, tg = 59.98 t/s, tg_3s = 60.32 t/s
3.43.341.034 I slot print_timing: id 0 | task 259 | n_gen = 363, tg = 60.25 t/s, tg_3s = 60.51 t/s
3.49.350.629 I slot print_timing: id 0 | task 259 | n_gen = 726, tg = 60.33 t/s, tg_3s = 60.15 t/s
| 阶段 | 切内核前(prism) |
切内核后(bonsai2) |
|---|---|---|
| 长上下文 Prompt 处理 | 78137 token / 81.53 s = 958 t/s | 73943 token / 45.83 s = 1613 t/s |
| 短上下文 decode | 66.03 t/s | 89.35 t/s |
7.4 万 token 的 prompt 只用了 45.8 秒,速度随进度从 2635 衰减到 1613 t/s(−39%),这是注意力计算量随序列长度增长的必然结果,属于健康衰减。
5.2 用实测带宽算理论上限
「到底还能不能更快」不能靠感觉,先量出硬件的真实带宽。写一个最小的显存带宽测试(sm_89 对应 4090):
text
Read bandwidth: 954.1 GB/s
Write bandwidth: 930.4 GB/s
Copy bandwidth: 918.9 GB/s (read+write)
拿到了 954 GB/s,就可以反推理论极限:
| 场景 | 每步读取量 | 理论极限 | 实测 | 效率 |
|---|---|---|---|---|
| 短上下文(317 token) | 5.5GB 权重(KV 可忽略) | 173 t/s | 89.35 t/s | 51% |
| 长上下文(74K) | 5.5GB 权重 + 5.0GB KV | 91 t/s | 60.33 t/s | 66% |
反直觉之处:短上下文效率反而更低(51% < 66%)。 因为短上下文下 KV 读取几乎为零,每步的理论耗时只有 5.5GB / 954 GB/s ≈ 5.77 ms ,而实测每步 11.19 ms ------多出来的 5.4 ms 是每步固定开销(kernel 启动与同步、激活值读写、softmax、CUDA Graph 调度),它占了短上下文总耗时的一半。
长上下文时 KV 读取把固定开销「摊薄」了,效率自然更高。
结论:长上下文已接近带宽极限(66%);短上下文与 173 t/s 的差距主要来自固定开销,属于当前 llama.cpp 架构下难以再榨的部分。
5.3 一个隐藏的坑:Prompt Cache 逐出
最后一份日志里出现了之前一直被忽略的警告:
text
20.12.646.492 I slot release: id 0 | task 37118 | stop processing: n_tokens = 62401, truncated = 0
20.12.925.570 W srv alloc: - making room for prompt cache entry, removing oldest entry (size = 459.410 MiB)
20.12.966.339 W srv alloc: - making room for prompt cache entry, removing oldest entry (size = 6149.981 MiB)
20.18.054.939 I slot print_timing: id 0 | task 42227 | prompt processing, n_tokens = 8192, progress = 0.51, t = 3.05 s / 2685.39 tokens per second
20.18.837.889 I slot print_timing: id 0 | task 42227 | prompt processing, n_tokens = 10240, progress = 0.64, t = 3.83 s / 2671.17 tokens per second
为了给新的 prompt cache 腾空间,服务删掉了一个 6149.981 MiB(约 6GB)的旧缓存条目。按 2600 t/s 换算,这相当于 6000+ token 的已有计算结果被丢弃,下次命中该前缀时要重算 2~3 秒。
处理方式取决于使用模式:
bash
--cache-ram 0 # 单一长对话:前缀一直在变,复用收益低,直接关掉更稳
--cache-ram 2048 # 多请求共享系统提示词:留 2GB 够放前缀,不挤占主 KV cache
5.4 最终成绩与小结
| 指标 | 数值 |
|---|---|
| 短上下文 decode(无 MTP) | 89.35 t/s |
| 74K 长上下文 decode(无 MTP) | 60.33 t/s |
| Prompt 处理峰值 / 74K 时 | 2635 / 1613 t/s |
| 短上下文理论极限 / 效率 | 173 t/s / 51% |
| 长上下文理论极限 / 效率 | 91 t/s / 66% |
| 显存占用 / 总显存 | 约 11 GB / 24 GB |
| GPU 利用率 / 功耗 / 温度 | 93% / 390W / 68°C,未降频 |
五条结论:
invalid ggml type N= 内核不认识该量化类型,要换内核分支,不是重下模型。- 分支选择要看组合 (内核优化 + MTP 修复叠加),并用
git log验证而不是看分支名。 - MTP 需要模型与内核双侧支持 ,接受率 95% 才算真正生效;但在本机场景下它仍是负收益。
- 调优要一次只改一个参数 ,并用
graphs reused、draft acceptance这类指标验证是否真的生效。 - 判断「还能不能更快」的正确方式是先测硬件带宽、再算理论极限------短上下文 51% 的效率说明瓶颈是固定开销,不是配置。
📎 参考 :
sudoingX/llama.cpp(pr-ptq1-mmv/pr-hadamard-mtp/bonsai2分支)、PrismML-Eng/llama.cpp(prism分支)、上游 PR #29077。 下一篇 FreeToken------边缘原生 MoE 推理引擎:llama.cpp 教你「把模型塞进显存」,FreeToken 教你「塞不进去也能跑」。