Ternary-Bonsai-2-27B(PTQ1_0) 在 RTX 4090 上的部署与调优实录

一、背景与环境

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」

这里有两个独立的前提,缺一不可:

  1. 模型侧 :--spec-type draft-mtp 是自投机解码,草稿由目标模型自带的 MTP(Multi-Token Prediction)层生成。基础版权重里没有这些层,所以报 model doesn't contain MTP layers。必须换成独立的 MTP 版模型文件。
  2. 内核侧 :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,未降频

五条结论:

  1. invalid ggml type N = 内核不认识该量化类型,要换内核分支,不是重下模型。
  2. 分支选择要看组合 (内核优化 + MTP 修复叠加),并用 git log 验证而不是看分支名。
  3. MTP 需要模型与内核双侧支持 ,接受率 95% 才算真正生效;但在本机场景下它仍是负收益。
  4. 调优要一次只改一个参数 ,并用 graphs reused、draft acceptance 这类指标验证是否真的生效。
  5. 判断「还能不能更快」的正确方式是先测硬件带宽、再算理论极限------短上下文 51% 的效率说明瓶颈是固定开销,不是配置。

📎 参考 :sudoingX/llama.cpp(pr-ptq1-mmv / pr-hadamard-mtp / bonsai2 分支)、PrismML-Eng/llama.cpp(prism 分支)、上游 PR #29077。 下一篇 FreeToken------边缘原生 MoE 推理引擎:llama.cpp 教你「把模型塞进显存」,FreeToken 教你「塞不进去也能跑」。

相关推荐
考虑考虑1 小时前
SQL中的 CASE WHEN
数据库·后端·sql
IT_陈寒1 小时前
Python的列表推导式差点让我加班到凌晨
前端·人工智能·后端
掘金者阿豪1 小时前
MySQL 迁移金仓,SQL 能正常执行,为什么查询结果却不一样?
后端
机器之心1 小时前
突发!GPT-6 Sol与Claude Opus 5.5同日开打,谁是「性价比之王」
前端·人工智能·后端
isfox1 小时前
Python 常用模块:别从头造轮子,这些内置的和第三方的直接拿来用
后端
Jiude1 小时前
从“超级 Agent”到能力平台:多 Agent 系统的架构取舍
前端·后端·架构
yunwei371 小时前
超越 eBPF 的极限:在内核模块中定义自定义 kfunc
linux·后端·性能优化
国奉1 小时前
iOS 音频格式转换怎么实现?从 AVAudioFile、AAC、MP3 到 FLAC 与批量转码架构
前端·后端
旺仔不是程序员1 小时前
索引膨胀与重建:PostgreSQL REINDEX 生产实践
数据库·后端·sql