RTX 5060 8GB 上跑 Ternary Bonsai 2 27B:40K 上下文 + MTP,agent 任务全套跑通。
折腾了几天,把实测数据和踩坑记录整理一下,都是 8GB 卡上的真实数字。最大的感受:
8GB 上卡你的不是解码速度,是上下文。
-c 从 32768 提到 40960,agent 类任务就从"全部撞墙"变成"全部跑通"。差这 8K,
就是能干活和不能干活的分界。
仓库(预编译二进制 + 完整配方 + 编译踩坑):
https://github.com/snailium/bonsai2-8gb
一、模型背景:Ternary Bonsai 2 27B 是什么
先说清楚这是个什么东西,不然下面的数字没有参照。
一句话:Qwen3.8-27B 的三值量化版。权重被压到 {−1, 0, +1} 三个值(1.58-bit 那一类做法),
配合 group size 128 的缩放因子,实际是 1.75 bpw。
| 基座 | Qwen3.8-27B(阿里,Apache 2.0) |
| 量化 | 三值 {−1,0,+1},g128,1.75 bpw(PTQ1_0) |
| 发布方 | PrismML |
| 体积 | 5.54 GiB(原始 PTQ1_0)/ 5.87 GiB(带 MTP 头的 lean 版) |
| 架构特点 | 混合注意力:64 层里只有 16 层是 full attention,其余 48 层是 linear attention |
| 上下文 | 原生 262,144 |
为什么 8GB 能塞下 27B:就是靠 1.75 bpw。同一个基座,常规量化的体积对比:
| 量化 | 体积 | 8GB 能装? |
|---|---|---|
| Bonsai 2 PTQ1_0(三值) | 5.54 GiB | ![]() |
| Unsloth UD-IQ1_S | 5.77 GiB | ![]() |
| Unsloth UD-IQ2_XXS | 6.77 GiB | ![]() |
| Unsloth UD-Q2_K_XL | 9.15 GiB | ![]() |
| Unsloth UD-Q4_K_M | 15.33 GiB | ![]() |
也就是说:同一个 27B,4-bit 量化是 15.33 GiB,8GB 的卡连边都摸不到。
能跑起来完全是三值量化的功劳。
那个"只有 16 层是 full attention"很关键:KV cache 只在这 16 层上产生,
所以 KV 的增长比同规模的稠密模型小得多——这是能用 q4_0 量化 KV 开到 40K 的前提。
一个必须知道的坑:不能换 stock llama.cpp
三值量化需要旋转过的权重基底(Hadamard 变换),这些在 mainline llama.cpp 里
完全没有(PrismML 的三值类型是 fork 独有的;有人提过 Q2_0 的移植 PR,
2026-07 关掉了没合)。
后果:
- stock llama.cpp 直接拒绝加载 PTQ1_0 / PQ2_0
- 更坑的是旧版
Q2_0文件能加载但不报错,输出是乱码
所以必须用 PrismML 那条 fork 线(我们用的是 sudoingX/llama.cpp 的 bonsai2 分支,
在 PrismML fork 基础上叠了三个还没合入的 PR)。模型卡上也写了这一点。
MTP 是什么,为什么要它
Qwen3.8-27B 自带一个 nextn 头(多 token 预测),可以拿来做投机解码:
草稿头一次猜几个 token,主干批量验证。
8GB 上要它的理由有两层:
- 提速:短 prompt 下 decode 从 54 提到 65–73 tok/s,接受率 0.85 左右
- 更重要的:MTP 会影响显存布局。这个我们踩坑了——关掉 MTP 反而没法用
(见下面"坑 2")
另外 MTP 头有两种装法:单独一个 sidecar 文件,或者嫁接进同一个 GGUF(作为 blk.64)。
单独装会重复一份词表 embedding,代价可以直接从文件大小看出来:
| 文件 | 大小 |
|---|---|
| 原版 PTQ1_0 | 5,946,648,928 B(5.54 GiB) |
| mtp-lean(MTP 头 + 复用已有 embedding) | 6,297,658,848 B(5.87 GiB) |
| mtp fat(MTP 头 + 自带一份 embedding 副本) | 7,012,820,512 B(6.53 GiB) |
fat − lean = 682 MiB,就是那份重复的 embedding;lean − orig = 335 MiB,
是 MTP 头本身。8GB 的卡上 682 MiB 很值钱,所以我们用 lean 版。
(代价:lean 版需要 build 里有 Hadamard 修复,也就是 bonsai2 分支带的那几个 PR。
官方预编译的 release 二进制起不来 draft 图。)
二、平台
GPU RTX 5060 8GB 8151 MiB - 447 driver reserved = 7704 可用
sm_120 (Blackwell) —— 不是 Ampere/Ada,坑不太一样
模型 Ternary-Bonsai-2-27B-PTQ1_0-mtp-lean.gguf 5.87 GiB
后端 sudoingX/llama.cpp branch bonsai2 @ dcc3be7
(含 #218 专用 mat-vec + #217/#205 Hadamard 修复 + #220 GDN gather)
三、启动参数,逐条说一下为什么
GGML_CUDA_BATCH_INVARIANT=1
llama-server \
-m Ternary-Bonsai-2-27B-PTQ1_0-mtp-lean.gguf \
-ngl 99 -fa on -np 1 \
-c 40960 \
-ctk q4_0 -ctv q4_0 --kv-mean-center kv-mean-center.gguf \
--spec-type draft-mtp --spec-draft-n-max 1 \
--reasoning-effort low --reasoning-budget 4096 \
--jinja \
--temp 0.7 --top-p 0.80 --top-k 20 --presence-penalty 1.5
| 参数 | 为什么这么设 |
|---|---|
-c 40960 |
这是本文的重点。 带 MTP 时能加载的最大值,用满 7508 MiB。32K 不够跑 agent 任务,49K 直接 cudaMalloc 失败 |
-ctk q4_0 -ctv q4_0 |
不加这个 40K 装不下。KV 量化是能开到 40K 的前提 |
--kv-mean-center |
q4_0 的必需配套。它给 K cache 做均值居中校准,找回量化丢掉的精度。注意这个文件必须用相同的 cache 设置(-fa on -ctk q4_0)生成,否则服务端会拒绝加载——这是它有意的安全检查 |
--spec-type draft-mtp --spec-draft-n-max 1 |
MTP 投机解码。n-max 设 1 而不是 2/3:我们在自己卡上扫过,acceptance 随 n-max 单调下降(0.85 → 0.76 → 0.67),速度并不单调,1 最稳。另外 n-max 越大,MTP 的 compute buffer 越占显存,直接挤压上下文 |
GGML_CUDA_BATCH_INVARIANT=1 |
让"单发解码"和"投机 batch 内验证"的 logits 逐位一致,MTP 严格无损。不加的话批量验证会改变浮点累加顺序,近似解会不一致 |
-fa on |
flash attention,配合 q4_0 KV |
-np 1 |
MTP 要求单 slot |
--reasoning-effort low --reasoning-budget 4096 |
agent 任务用。low 让模型思考简短聚焦,4096 是服务端硬截断思考的预算(实测精确停在 2064–2070 tokens)。纯生成任务(写文档)直接 --reasoning off 更省——没有规划阶段,思考只是抢输出的 token |
--temp 0.7 --top-p 0.80 --presence-penalty 1.5 |
量化模型容易复读。presence-penalty 是关键——它的默认是 0.0,在这个模型上会出复读循环(我们实测过一次 1898 轮重复,正文出不来),1.5 正好。Unsloth 的模型卡也是这么建议的 |
显存占用:7508 MiB used / 198 free。
四、实测
llama-bench tg128:54.31 tok/s(官方 release 二进制同卡 39.56 → +37%)。
短 prompt 服务端解码:code 70.7 / bash 68.9 / prose 64.6 tok/s,MTP 接受率 0.85 / 0.78 / 0.67。
40960 上下文下跑一整套 agent 任务:
| 任务 | thinking | 结果 | decode |
|---|---|---|---|
| 生成单页 HTML | off | PASS | 65.9 tok/s |
| 生成 SVG | off | PASS | 67.4 tok/s |
| 27 文件安全审计(31 次工具调用) | low + budget 4096 | PASS | 38.7 tok/s |
| 采集主机配置 | low + budget 4096 | PASS | 45.1 tok/s |
| 多源检索计算(46 次工具调用) | low + budget 4096 | PASS | 33.8 tok/s |
同样的配置在 32K 下,后两个任务全部撞墙:研究任务每轮要 7–25 次 compaction
(其中 1–4 次失败,报 summary is not smaller than the shadowed content),审计任务
根本跑不完。只多 8K,就是"全挂"到"全过"。
agent 任务的 decode(33–45)明显低于生成任务(66–67),是上下文深度造成的——
这些 run 的 prompt 有 9–15K tokens。另外每轮对话都要完整重算 prefill
(上下文裁剪会打断前缀缓存),约 72 秒/次工具调用,这是长任务的真实墙钟成本。
五、两个坑,踩过的可以直接跳过
坑 1:带 MTP 的上下文上限是 40960,不是 64K。 同一张卡上逐档试出来的:
-c |
带 MTP | 无 MTP |
|---|---|---|
| 32768 | 7284 MiB ![]() |
— |
| 40960 | 7508 MiB ![]() |
— |
| 49152 | cudaMalloc OOM |
— |
| 65536 | ![]() |
7268 MiB ![]() |
| 81920 | — | 7636 MiB ![]() |
| 90112 | — | ![]() |
坑 2:这个 build 关掉 MTP 之后,模型输出会坍缩成 /。
上面那张表里"无 MTP"一列看着很诱人——能把窗口开到 80K,省显存也是真的
(7268 MiB,跟文档里的 7,266 几乎分毫不差)。但代价是模型直接不会写字了:
32K + MTP -> 正常
32K 无 MTP -> 全是 "/" (多次复现)
64K 无 MTP -> 全是 "/" (3/3)
无 MTP + 关 thinking -> 正文直接是 200 个连续 "/"
无 MTP + 原版 PTQ1_0 -> 同样坍缩
不是 -mtp-lean 文件的问题(原版 GGUF 一样),也不是 reasoning 路径的问题。
所以"拿 MTP 换上下文"这条路,至少在这个 build 上走不通。
六、能跑通还有一半功劳在 harness:dsh + 我们自己写的两个小插件
上面那些 agent 任务不是我手动盯着一步步喂的,是模型自己调工具、自己决定下一步,
跑在 DeepSeek Harness (dsh)
的 headless 容器里。
速度这块已经很多人卷了,但**"小窗口下 agent 怎么不把自己搞死"讲的人不多**——
而 8GB 卡上这就是生死线。我们前后写了两个 dsh 插件来治这个,都有点用:
插件一:一个不花模型调用的"后悔药" —— context-trim
上下文满了怎么办?常规操作是让模型自己压缩历史。问题是压缩本身也要跑一次模型,
而且它得把"要压缩的那段"塞进已经快满的窗口里——于是就经常压不动。我们实测撞到的:
summary is not smaller than the shadowed content (1180 >= 1180)
翻译一下:"我想把它变小,但变小之后还是一样大。" 32K 的卡上,到这一步基本就死了。
我们这个小东西的思路特别土:一次模型都不调,纯记账。 把最老最没用的那段对话
直接换成一行标记,或者把超长的工具输出掐头去尾留一段。腾地方不要钱,所以
它在所有请求都快挂掉的时候反而最管用。
效果:那次 27 个文件的安全审计,它出手 11 次,一共腾出约 3.7 万 tokens。
没它的话,这个任务在 40K 窗口里根本走不到"开始写报告"那一步。
一句话总结:别指望模型自己管上下文,裁剪这种事应该是 harness 的零成本杂活。
插件二:它钻牛角尖的时候,得有人踹一脚 —— repeat-tool-breaker
小模型 + 低显存,"原地打转"是高频死法。我们碰到的真实案例:
同一个 API 打了 29 次,每次参数都不一样,跑满一小时没结果。
最阴险的地方在于——从字面上看它每次都在做不同的事。所以任何"检测重复调用"的
工具都抓瞎,你只能干看着它烧时间。
我们的做法是不看字面、看语义:比如"往 weather.gc.ca 这个域名一共打了多少次"
单独计数。不管你 URL 参数怎么变,同一个目标的调用会攒起来,钻牛角尖就现形了。
喊停也不是一上来就拦死,分三级:
| 第几次 | 反应 |
|---|---|
| 7 次 | 轻轻提醒一句:"你在重复,考虑换条路" |
| 11 次 | 要求它写出进度摘要 + 至少两个还没试过的方案 |
| 12+ 次 | 才真拦 |
T5 那次的效果:模型连着 7 次 curl 同一个天气 API、5 次失败,第 7 次触发提醒,
下一次调用它就换思路了——从"怎么调通这个端点"变成去搜气象局官网数据,
最后算出正确答案。
一句话总结:小模型本来就更容易钻牛角尖,而且钻起来更慢更烧时间。
与其调 temperature / penalty 试图让它别钻,不如在 harness 层给它一句
"你该换个思路了"。 后半句是我们反复验证过的:光调采样参数,救不回来。
顺手记一个 bug:判断"失败"不能只看
isError。curl ... | python3 ... | head
的退出码是 0,脚本里把 HTTP 400 吃掉了也是 0——我们那次 7 个调用里 5 个失败,
全被判成了成功,所以提醒一直不吭声。最后得去读文本里的报错特征才行。
七、thinking 也有个方向性的坑
模板里 reasoning_effort 只接受 xhigh / medium / low 三个值。默认 xhigh 会注入
一段"仔细思考、验证假设、考虑替代方案"的系统提示,在量化模型上会失控——我们实测过
一次 24508 token 的回复里 86% 烧在 thinking,正文一个字没出来。
low 注入的是相反指令("Keep your thinking brief and focused"),配
--reasoning-budget 4096 在 agent 任务上表现最好;纯生成任务直接 --reasoning off。
方向性坑:服务端设了 --reasoning off 之后,客户端发 reasoning_effort: high
是压不过它的(实测 0 个 reasoning frame)。想两种模式共用一个 server,要反过来做:
服务端开着 thinking,需要关的请求带 chat_template_kwargs: {"enable_thinking": false}。
八、复现入口
仓库:https://github.com/snailium/bonsai2-8gb
bonsai2-universal.tar.gz— 预编译二进制,sm_75 → sm_120 全架构
(我们从源码编的:官方预编译包只含 Ampere/Ada,而 CUDA 12.4 编不了 sm_120)RECIPE.md— 完整配方、每个参数的依据、各项实测数据、12 条注意事项BUILD.md— 编译三个坑:CUDA 12.4 编不了 Blackwell、glibc 2.43 的rsqrt
声明冲突、cmake 会静默选错 nvcc(报ptxas fatal: Value 'sm_52' is not defined)
这两个插件是我们自己写的,源码和设计说明都公开:
- snailium/dsh-command-context-trim
—— 零模型调用的上下文裁剪 - snailium/dsh-repeat-tool-breaker
—— 语义指纹的重复/失败检测
写它们的原因很直接:8GB 这种小窗口跑 agent,光靠调模型参数是救不回来的,
必须在 harness 层动手。如果有人在别的低显存场景做 agent,这两个的思路应该能照搬——
插件的阈值和开关都是可配的,不绑定任何特定模型。

