研发手记
← 返回博客

MTP 让本地编码更快,但能放心开启吗?

• 10 分钟阅读
本文同时提供英文版本: 🌐 Read in English →

开启 MTP 后,这张 RTX 3090 的生成吞吐量从 36.0 提升到 56.2 tokens/s,约快 56%。两次双方都完成的缓存任务,也分别省下了约 20% 和 40% 的时间。

不过,一组转账任务出现了补丁质量差异。我会继续测试 MTP,暂时保留关闭它的默认配置。

这轮实验来自上一篇本地部署文章下的读者建议:试试 MTP,或带 MTP 的量化版本。那张挖以太坊留下的 3090,已经通过 OpenCode 跑起了编码 Agent;这次我想知道,提速之后能否更早交付正确补丁。

为此,我们用同一份模型文件做了 8 组配对、共 16 次运行。每组保持任务、随机种子和初始环境相同,只切换 MTP 开关。

MTP 先拟草稿,再由主模型验证

普通生成通常一步产生一个 token。MTP(Multi-Token Prediction,多 token 预测)用内置预测头先提出接下来几个 token,再让主模型验证。连续接受多个候选时,就能减少逐 token 解码的步骤。这是 llama.cpp 的一种推测解码路径。

主模型仍负责验证。推测解码论文说明,正确的算法可以保留目标模型的输出分布。因此,加速不必天然以降低模型能力为代价。

现有 Qwen3.8-27B Q4_K_M GGUF 已包含 MTP 预测层,这次直接使用它,没有换成另一份 Unsloth 量化。比较中的两条服务器命令,只有 --spec-type none 和 --spec-type draft-mtp 这一处不同。

生成快了 56%,完成任务省时 20%–40%

生成吞吐量衡量每秒输出多少 token,其中也包括模型的思考输出。任务总耗时则包含读源码、调用工具和跑测试,更接近拿到补丁前实际等了多久。

指标MTP 关闭MTP 开启
编码运行累计生成吞吐量36.0 tokens/s56.2 tokens/s
预设独立检查合计通过35/8026/80
在时限内完成并通过预设检查2/82/8

双方都完成的两次缓存任务,给了我们最直接的省时证据:

配对运行MTP 关闭MTP 开启总耗时减少
配对一192.0 秒153.5 秒20.1%
配对二431.2 秒259.0 秒39.9%

这两次都更早拿到了通过预设检查的补丁。失败的任务即使更快结束,也不算成功任务提速。后续提示会随 Agent 的操作分叉,所以累计吞吐量也不是全程生成相同文本的纯速度对照。

短回复速度与生成计时口径

相同首轮请求的短回复,关闭 MTP 为 38.9–39.8 tokens/s,开启为 74.0–84.2 tokens/s。它们只是较短的工具选择响应,不能据此推断整个编码任务耗时减半。

累计吞吐量按所有已完成生成的目标输出 token 总数,除以对应服务端生成时间总和计算。包括 reasoning(模型的思考输出)和工具调用;不计输入处理或被拒绝的草稿。超时中断、没有最终计时记录的生成被排除。

任务总耗时包括输入处理、工具调用和 Agent 自己跑测试;排除服务启动与事后独立评分。

九项检查的差距,来自同一次转账修复

35/80 对 26/80 的差距,全部集中在同一道转账题、同一个随机种子。缓存、账本和构建规划题的配对得分均相同。

关闭 MTP 时,Agent 写出了生产代码补丁,事后通过全部十项独立检查。但它在 480.1 秒达到截止时间,没有完成最终交付。

开启 MTP 时,Agent 在 186.1 秒结束,没有修改生产代码,只通过原始实现就能通过的一项检查。

这组的关闭候选在检查中表现更好,两边却都没达到完整交付标准。所以,我们分别记录补丁正确性和按时完成交付:完整交付数仍是 2/8 对 2/8。九项相关测试的差异,也不能当作九个独立任务的回退。

各任务得分,以及早期探索中没有重现的差异
任务MTP 关闭,两次得分MTP 开启,两次得分
TTL 缓存10/10、10/1010/10、10/10
CSV 账本1/10、1/101/10、1/10
增量构建规划1/10、1/101/10、1/10
SQLite 转账1/10、10/101/10、1/10

“完成并通过”要求未超时、进程成功退出、受保护的规格和配置未修改、公开及新增测试通过,且十项独立检查全部通过。它不表示已经覆盖所有可能的缺陷。

早期探索中,构建规划题也曾出现两种模式分数不同的现象;在这轮更严格的控制下,它没有重现。旧探索分数没有混入这轮结果。

缓存任务还提醒我,预设检查全绿后仍要看实际代码:一个 MTP 候选新增的 TTL 验证,在极大整数上触发了溢出。同一配对的关闭候选通过了额外探针。这是极端 API 边界,值得记录,但不能据此推断日常错误频率或确定 MTP 是根因。

额外审查:缓存整数溢出与双方共同遗漏

缓存题的四次运行都通过十项预设检查。其中一个 MTP 候选新增 math.isfinite(ttl)。任务允许有限的正整数 TTL;探针传入 10**400,并使用每次递增的整数时钟时,验证触发了整数转浮点的 OverflowError。同一配对中关闭 MTP 的候选通过。

账本的精确大金额、表头和读取错误探针,在双方、两个种子下均失败。转账的目标账户整数溢出探针,在第二组配对中关闭通过、开启失败;第一组双方均失败。

额外审查在评分之后进行,没有反馈给模型,也没有追加入原来的 80 项。配对记录保留了细节。

更快输出,仍然会用完同样的预算

多数失败运行有一个共同现象:模型把 8,192-token 单次输出预算耗在思考上,以 length 结束,没有进入实际编辑。许多 1/10 是原始代码的得分,不能理解成完成了十分之一的修复。

增加上下文容量解决不了这个限制。上下文决定一次能容纳多少输入与输出;单次输出上限决定这一轮能生成多少内容。MTP 可以更快输出,却不会自动增加这份预算。

这也让质量比较难以下定论:两种模式都经常没写出补丁,可供比较的有效修复很少。每题只有两个种子、共四个小型 Python 项目;80 项检查彼此相关,不是 80 个独立样本。

转账配对的差异值得追查,目前尚未定位根因。批量验证的数值行为、状态处理和采样细节都是排查方向。Agent 的一次不同操作,也可能改变后续输入和补丁;这些仍是可能解释,没有被这轮实验确认。

实验控制了什么,结论适用于哪里

每题使用两个随机种子,分别开启和关闭 MTP,第二组反转顺序。每次重建相同初始代码,固定工作目录、提示、工具、权限与生成设置。十项独立测试放在 Agent 工作区之外,结果不反馈给模型。

代理核对了实际发出的完整首轮请求:8 组全部一致,初始文件哈希也一致。每次重启服务,双方都关闭前缀缓存复用。因此,这轮还不能预测日常缓存续聊的收益。

完整配置、请求核对与适用范围
项目固定设置
模型与 GPUQwen3.8-27B Q4_K_M,同一份 GGUF;RTX 3090 24GB
软件llama.cpp b11146 / CUDA 12.8;OpenCode 2.0.20
服务131,072-token 上下文容量;单槽位;模型层全部在 GPU
缓存与计算K/V 均为 q8_0;Flash attention;batch 512 / microbatch 256
生成medium thinking;temperature 1.0;top_p 0.95;top_k 20
预算每次生成最多 8,192 tokens;每任务最多 480 秒
配对种子4242、8675309
推测解码关闭 none / 开启 draft-mtp;草稿上限固定为 3

耗时表中的配对一、二分别对应种子 4242、8675309。

两边均关闭 target 和 draft 的 backend sampling,不使用合成接受率。每次运行的初始提交、Agent 标题、项目配置与评分器相同;实际请求报告的前缀缓存 token 数均为零。第一组按关→开,第二组按开→关运行。

这是每次重启服务、无前缀缓存复用的实验。任务计时排除服务启动,但包含输入处理、工具调用和 Agent 自己跑测试的时间;事后独立评分不计入。桌面和其他背景 GPU 活动未完全控制。

最大实际输入是 20,057 tokens。128K 是配置容量,这轮没有测满 128K 的编码表现,也没有测日常缓存续聊的加速幅度。

暂时保留默认配置,下一轮先放宽输出预算

对这套环境,我会继续保留关闭 MTP 的默认配置。两次缓存任务省时 20%–40% 的收益很有价值;在扩大使用前,我想先核查转账补丁的差异。

下一轮先同步提高两边的输出预算,再增加配对种子和更接近日常工作的任务。缓存续聊另测。这样才能判断,提速是否稳定转化为更早完成正确补丁。

如果你也在试 MTP,可以同时记录生成速度、任务耗时、独立测试和最终交付,再结合自己最常做的任务决定是否开启。

数据与参考

分享文章

微信扫一扫分享

用微信扫描上方二维码,即可在手机端阅读或分享到朋友圈。

链接已复制到剪贴板!