Kestrel_VLLM - 49 · 投机解码为什么没收益:同卡同 prompt 拆开三笔账
49 · 投机解码为什么没收益:同卡同 prompt 拆开三笔账
一句话:同一块卡、同一族模型、同样 k=3,llama.cpp 的 MTP 投机是 1.92×、我们是负的;本篇不写一行引擎代码,用「同 prompt 跨引擎 + nsys 逐核」把每轮的账拆成 verify / 重放 / 草稿 三笔 —— H2(verify 成本)被证伪、H3(重放)有界、H1(接受率)是主杠杆但单独不够(H1 + H3 才 1.38×)。
前置:建议先读 第 47 篇(1.92× 的来源)与 第 45 篇(正是本轮的立场)。
环境:x86-64 + NVIDIA RTX 5090 · Qwen3.5-27B(.vqfq4)· 对照侧 llama.cpp CUDA 构建 +llama-server。
一、问题与结论
第 47 篇留了一个悬案:同一块卡、同一族模型、同样 k=3,llama.cpp 的 MTP 投机是 1.92×,我们是负的。当时只能记一句「方向一致、数值不可互算」。这一篇把它拆开:不写一行引擎代码,用「同 prompt 跨引擎 + nsys 逐核」把每轮的账拆成三笔 —— verify / 重放 / 草稿,看看到底哪一笔贵,以及「贵」到什么程度才够翻正。
| 假设 | 结论 | 数值 | 标注 |
|---|---|---|---|
| H2 verify 成本 | 证伪 | GDN 只占 verify 12.0%、占整档B 5.67% | 实测(nsys) |
| H3 重放 | 有界 | 重放 ≈ 36% of 档B;归零仍只有 0.88× | 推算(图内未展开) |
| H1 接受率 | 主杠杆,但单独不够 | 我方每轮接受 0.833 vs llama.cpp 1.385(1.66×);单换接受率仍 0.82×;H1 + H3 才 1.38× | 实测 + 推算 |
| verify 本身 | 不贵 | 每行 ≈5.7 ms,比自回归(13.232 ms/token)便宜 ~2.3× | 实测 |
逐条结论:
- 三个候选根因(上一轮冻结):H1 接受率 / H2 verify 成本 / H3 重放。
- H2 证伪:GDN 只占 verify 的 12.0%、占整档B 的 5.67% ⇒ 此前早已记下的「chunked GDN 只有 1.27 ms / 5.5%」上界,被实测确认(未证伪)。⇒ 「改用 chunked verify」这条 Phase 2 路线前提不成立,建议撤销。
- H3 有界:重放 ≈ 36% of 档B;但归零它仍只有 0.88×(与第 45 篇的 0.85× 同量级,均 <1)。
- H1 是主杠杆,但单独不够:同 prompt 实测,我方每轮接受 0.833 vs llama.cpp 1.385(1.66×);把接受率换成 llama.cpp 档,外推仍 0.82×;H1 + H3 一起才 1.38×(推算)。
- verify 本身不「贵」:每行 ≈5.7 ms,已比自回归(13.232 ms/token)便宜 ~2.3× —— 问题是它每轮只推进 1.476 行,被浪费掉了。
- ★ 零引擎改动;本轮只跑工具 + nsys + llama-server,并首次记录完整 prompt(补上「只记长度」的复现缺口)。
二、怎么拆:两个口径对齐
① 我方:q35_gpu_model --spec 64 --spec-k 3 --spec-tokens <96 个 id>(真实文本前缀),再按同配置跑一次 nsys。
- 档A(纯自回归)13.232 ms/token;档B(投机 k=3)23.563 ms/token ⇒ 自比 0.562×(负)。
- 42 轮;直方图
n_ok = 0:27 / 1:2 / 2:6 / 3:7;memcmp=0(位级 PASS)。
② llama.cpp:llama-server(第 47 篇的同一 CUDA 构建)+ 一次 /completion,同一个 96-token 前缀、n_predict=64、--temp 0 --seed 1。
- ★
prompt_n = 96与我们的前缀长度一致 ⇒ tokenizer 对齐 ⇒ 「同 prompt」是量出来的,不是假设的。
「verify」这一笔在驱动里就对应一次批量 step_m(kv, ...) 调用(kv 行草稿一起喂进去),并单独累计它的墙钟:
/* tools/q35_gpu_model.c:373-382 —— verify 段:一次批量 step_m(kv 行) + 单独计时 */
const double t_ver0 = now_ms(); /* verify 段墙钟起点 */
for (int i = 0; i < kv; i++)
if (q35_vqf_get(vq, SPEC_EMB, (long long)d[i + 1] * D, D,
hM + (size_t)i * D) != 0) goto done;
if (vllm_cuda_model_mark(cu) != 0) goto done;
if (vllm_cuda_model_step_m(cu, kv, p + 1, hM, hoM, lgM) != 0) {
vllm_cuda_model_rollback(cu); goto done;
}
ms_verify += now_ms() - t_ver0; /* verify 段累计 */
三、H2 证伪:GDN 只占 5.67%
nsys 逐核,关键技巧是认专属内核:M=3(verify 批)有它自己的内核实例(k_rq_q4_gemv_mr<*,3>、k_gdn_*_m),而它们的逐层实例数 ÷ 层数 = 42 = 轮数 ⇒ 可以直接求和得到 verify 的 GPU 时间,不必猜。
| 桶 | ms | 标注 |
|---|---|---|
| gemv(M=3) | 586.79 | 实测(nsys) |
GDN _m(6 个) | 85.51 | 实测(nsys) |
| verify 合计(含共享内核摊分,估) | ≈712 ⇒ 17.0 ms/次 | 实测 + 估算 |
⇒ GDN / verify = 12.0%、GDN / 档B = 5.67%。★ 这正好把此前早已写下的「1.27 ms / 5.5%」实测确认了 —— 一个老结论被新数据守住,也是好消息。
四、nsys 的一个限制:图里的账量不到
想把 H3(重放)也量出来,撞上一个坑:总内核时只有 815.24 ms,比档A 单独的 wall(846.8 ms)还小。一个解释立刻成立:走 CUDA Graph 的那些 M=1 步,其图内内核没有被展开。
数一数就对上:cudaGraphLaunch 299 次 = 档A 64 + 前缀 96×2 + 重放 41 + 收尾 2。
★ 但并非所有 M=1 内核都缺席:trace 里仍有 mr<1,1>(633 实例 / 71.85 ms)与 gemv3(380 / 19.12 ms),实例数 ÷ 126 草稿步 ≈ 5.0 / 3.0 ⇒ 疑为 MTP 草稿头(推断)⇒ 草稿可见、全模型 M=1 步不可见。
⇒ 重放只能用算术:由 spec_run 源码推出「n_ok→收尾步数」映射(n_ok==k→0 步;n_ok==0→1 步;否则 n_ok 步)⇒ 41 步 × 13.232 ≈ 542 ms ≈ 36% of 档B。这个映射就藏在收尾分支里:
/* tools/q35_gpu_model.c:403-429 —— 由 n_ok 决定收尾步数(重放算术的依据) */
if (n_ok >= kv) { /* 整批保留,无需回滚 → 0 步 */
/* 只做 host memcpy 更新 h_out/lg,不发 M=1 步 */
} else if (n_ok == 0) { /* 只回滚,不重放 → 1 步(n_ok==0 支) */
/* vllm_cuda_model_rollback + 1 次 model_step */
} else { /* 重放 n_ok 行(1..k-1)→ n_ok 步 */
if (vllm_cuda_model_rollback(cu) != 0) goto done;
for (int j = 1; j <= n_ok; j++) /* ★ 只能 M=1 单步,不能批量(批宽固定) */
if (vllm_cuda_model_step(cu, p + j, hM + (size_t)(j - 1) * D,
h_out, lg) != 0) goto done;
}
★ 这条是算术估计、非实测(已在规格里显式标注;要实测须 VLLM_CUDA_Q35_GRAPH=0 重采)。
五、H1:同 prompt 对照
| 项 | 本引擎 | llama.cpp | 标注 |
|---|---|---|---|
| 前缀 token | 96 | 96 | 实测 |
每轮接受(mean n_ok) | 0.833 | 1.385(36 接受 / 26 轮) | 实测 |
尺子2(A+1) | 1.833 | 2.38(官方 mean len) | 实测 |
| 轮数 / 64 token | 42 | 26 | 实测 |
draft acceptance | — | 0.46154(36 / 78) | 实测 |
接受率来自逐位置统计 —— 驱动把每个位置的接受率(p_j = P(n_ok ≥ j))单独打印,与 llama-server 的 acc per pos 同口径:
/* tools/q35_gpu_model.c:392 + 474-478 —— 逐位置接受率(分母 = 轮数) */
for (int j = 1; j <= K; j++) if (n_ok >= j) acc_pos[j]++;
...
printf("[spec] 逐位置接受率 acc per pos = (");
for (int j = 1; j <= K; j++)
printf("%s%.3f", j > 1 ? ", " : "", nround ? (double)acc_pos[j] / nround : 0.0);
printf(") ← p_j = P(n_ok ≥ j),与 llama-server `acc per pos` 同口径\n");
⇒ H1 差距 ≈ 1.66×(每轮接受)/ 1.30×(A+1) —— 我们确实接受得更少,但差距不到 2×。★ 口径提示:两侧量化格/工况不同(.vqf q4 ≈4.25 bpw vs Q4_0 4.60 bpw)⇒ 按「跨引擎只比方向/量级」的纪律,不得把这 1.66× 当成纯「接受率差」。
六、外推:单靠 H1 不够
- 把接受率换成 llama.cpp 档(
A+1≈2.38)⇒ 轮数 ≈ 32(按我方实测标定42 × 1.833/2.385;直接用A+1除会得 27,那偏乐观)。 B ≈ 32 × (草稿 2.2 + verify 17.0 + 重放 12.9) ≈ 1027 ms⇒ 16.0 ms/token = 0.82×,仍 <1。- 再叠加 H3 归零 ⇒
32 × 19.2 ≈ 614 ms⇒ 9.6 ms/token = 1.38×(才翻正)。
⇒ H1 单独不够,H1 + H3 才够。 ★ 这是推算(未做实测验证);且 H3 的实现属重放消除这条线的评估范围(上一轮已判「不做」)—— 要组合须先重启那条评估。
七、边界与已知限制
- n 小:42 轮 / 64 token(工具自印「n 太小不作结论」);单段文本、非评测集。
- nsys 扰动绝对时间(~1–3%)⇒ 占比用它、绝对 ms/token 用无 nsys 档。
- H3 的 542 ms 是算术(图内未展开);H1+H3 的 1.38× 是推算。
- 未做:H3 的图外实测;H1 的改进实现;外推的实测验证。
- ★ 环境坑留档:该 llama.cpp 构建的可执行需要
C:\cuda\v13.2-full\bin在PATH,否则0xC0000135。 - ★ 复现缺口补齐:本轮的 prompt 全量落盘(
build-x64\l24_ids.txt),补上此前「只记长度」的欠账。
CPU 对照(迁移前基线)
- CPU 参考:
src/model/vllm_q35_mtp.c:q35_mtp_step—— MTP 草稿头(stem/tail)的 CPU f32 参考,本篇 spec 驱动所对标的「草稿/verify」语义来源。 - 迁移要点:本篇零源码改动;把「同 prompt 跨引擎 + nsys 逐核」用到 CUDA 侧投机线,给每轮拆出 verify/重放/草稿三笔账(H2 证伪、H3 有界、H1 为主杠杆)。
- 真机验证:命中 E16(MTP 接受率
q35_mtp_accept ... --k 4:α₁=0.714(p₁=0.714)、α₂=0.364(p₂=0.254)、α₃=0.500(p₃=0.127)、α₄=0.750(p₄=0.095),E[len]=2.190 token/轮;T=63、teacher-forced 上界口径)+ E14(同卡对照侧 llama.cpp:pp512 = 4228.05 ± 180.19 t/s、tg128 = 85.24 ± 0.03 t/s)。nsys 逐核计数未独立复现:CUDA Graph 使全模型 M=1 内核在 nsys trace 内不可见。
八、小结(可复用结论)
- ★ 「别人的正收益」要拆成自己的账,逐笔定价。 llama.cpp 的 1.92× 不是我们的目标;能用的信息是「它每轮接受多少」—— 于是我们把「同 prompt」也量齐(
prompt_n=96两侧一致),差距才有意义。 - 认专属内核,能绕开「哪一桶算谁」的争论。 M=3 的 verify 内核实例数 ÷ 层数 = 轮数 ⇒ verify 的 GPU 时间可以直接求和,不必靠拆占比。
- 工具会挡住你看不见的地方 —— 先说清它挡住了什么。 CUDA Graph 让全模型 M=1 步在 trace 里「消失」(用「总内核时 < 单档 wall」就能自证)。把「量不到」如实写出来、把推算显式标注,比给一个漂亮但假的占比更有用。
相关篇目:第 47 篇(同卡对拍 —— 1.92× 的来源)、第 45 篇(外部收益不能照搬 —— 正是本轮的立场)、第 46 篇、第 31 / 32 篇(MTP 头与投机轮的锚定口径)。仓库
docs/的适配规格中有对应的工单冻结与 Phase 0 实测记录(H2 证伪 / H1 主导 / H3 有界),本轮把「不可廉价翻正」这条评估收口、并补齐 prompt 复现缺口。
源码与配套资源:本仓库 https://gitee.com/pei-xiaoguang/kestrel-llm-cuda.git;
CPU 推理源码 https://gitee.com/pei-xiaoguang/kestrel-llm
更多推荐



所有评论(0)