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(.vqf q4)· 对照侧 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×实测

逐条结论:

  1. 三个候选根因(上一轮冻结):H1 接受率 / H2 verify 成本 / H3 重放。
  2. H2 证伪:GDN 只占 verify 的 12.0%、占整档B 的 5.67% ⇒ 此前早已记下的「chunked GDN 只有 1.27 ms / 5.5%」上界,被实测确认(未证伪)。⇒ 「改用 chunked verify」这条 Phase 2 路线前提不成立,建议撤销。
  3. H3 有界:重放 ≈ 36% of 档B;但归零它仍只有 0.88×(与第 45 篇的 0.85× 同量级,均 <1)。
  4. H1 是主杠杆,但单独不够:同 prompt 实测,我方每轮接受 0.833 vs llama.cpp 1.385(1.66×);把接受率换成 llama.cpp 档,外推仍 0.82×;H1 + H3 一起才 1.38×(推算)。
  5. verify 本身不「贵」:每行 ≈5.7 ms,已比自回归(13.232 ms/token)便宜 ~2.3× —— 问题是它每轮只推进 1.476 行,被浪费掉了。
  6. ★ 零引擎改动;本轮只跑工具 + 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标注
前缀 token9696实测
每轮接受(mean n_ok)0.8331.385(36 接受 / 26 轮)实测
尺子2(A+1)1.8332.38(官方 mean len)实测
轮数 / 64 token4226实测
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 内不可见。

八、小结(可复用结论)

  1. ★ 「别人的正收益」要拆成自己的账,逐笔定价。 llama.cpp 的 1.92× 不是我们的目标;能用的信息是「它每轮接受多少」—— 于是我们把「同 prompt」也量齐(prompt_n=96 两侧一致),差距才有意义。
  2. 认专属内核,能绕开「哪一桶算谁」的争论。 M=3 的 verify 内核实例数 ÷ 层数 = 轮数 ⇒ verify 的 GPU 时间可以直接求和,不必靠拆占比。
  3. 工具会挡住你看不见的地方 —— 先说清它挡住了什么。 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

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐