2倍 tok/s(在1块MI50上从19.4 tok/s提升到38.1 tok/s)尝试类似推测解码的假设……但不是用额外的侧模型,而是利用我可以同时运行多个计算,就好像内存里加载了两份Qwen3.6-27B一样——小量化不占用所有可用算力。
摘要
打包双推理(PTI)是一种通过单批解码中运行多个token序列来实现约2倍LLM吞吐量的技术,它利用了llama.cpp中的权重共享,无需草稿模型或额外VRAM。
查看缓存全文
缓存时间: 2026/06/09 08:40
bigattichouse/packed-twin-inference 源码:https://github.com/bigattichouse/packed-twin-inference
打包双子推理(PTI)
一次权重加载。N个token流。零质量损失。
PTI 现已实现 ~2× LLM 吞吐量(在 Qwen3.6-27B / MI50 上实测),仅利用 llama.cpp 现有的批处理解码基础设施。无需草稿模型,无需额外 VRAM 占用,无质量差距。完整的 4–5× 吞吐量 路径已在下文记录。
注意
4seq(1.96× 版本)是当前的工作基准。我正在进行 MCP+llama CPP 的集成。这将使我们在 temperature=0 时达到理论最大值 4×。一旦准备就绪,我会推送更新,预期可达到该最大值的 70%~80%:即对比裸模型提速 2.5–3.5×。
主要结果(MI50,Qwen3.6-27B,实测)
| 配置 | 模型 | tok/s | 对比基线 | GPU VRAM |
|---|---|---|---|---|
| 基线 | Q5_K_M | 21.1 | 1.00× | 18.1 GiB |
| PTI 4-seq | UD-Q6_K_XL | 38.1 | 1.96× | 24.5 GiB |
| PTI 3-seq | UD-Q6_K_XL | 33.9 | 1.75× | 24.3 GiB |
| PTI 2-seq | UD-Q6_K_XL | 30.5 | 1.57× | 24.0 GiB |
| PTI 2-seq | Q5_K_M | 28.9 | 1.38× | 18.4 GiB |
| 基线 | UD-Q6_K_XL | 19.4 | 1.00× | 23.7 GiB |
4-seq 运行使用 pti_4seq.cpp —— 150 行 C 代码,封装 llama.cpp 的公开 API。无需修改 llama.cpp,无需自定义内核。每一步只需一次 llama_decode 调用,批次大小为 4 个 token——每个序列一个。
# 复现 38.1 tok/s 的结果:
make 4seq && make 4seq-run
# bin/pti_4seq -m ../gguf/Qwen3.6-27B-UD-Q6_K_XL.gguf \
# -p "The key to faster LLM inference is" -n 80 -ngl 99
工作原理
LLM 解码在 batch=1 时受到内存带宽限制。每个 token 都需要从 VRAM 加载所有模型权重:
tokens/sec ≈ HBM_bandwidth / model_size
PTI 利用了一个事实:llama.cpp 已经将权重加载共享给批次中的所有 token。将 N 个序列作为一个批次运行,成本大约相当于单序列步骤的 1.0–2.0 倍,而不是 N 倍。收益来自于每步产生 N 个 token,而只需支付大约 2 倍的单 token 步骤成本。
单序列解码(基线):加载 25 GB 权重 → 产生 1 个 token → 19.4 tok/s
PTI 4序列解码: 加载 25 GB 权重 → 产生 4 个 token → 38.1 tok/s
(权重在同一个 llama_decode 调用中共享给所有 4 个序列)
开销缩放(实测,UD-Q6_K_XL,MI50):
| N 序列 | tok/s | 倍率(vs基线) | 步骤开销 | 开销增量 | GPU VRAM |
|---|---|---|---|---|---|
| 1(基线) | 19.4 | 1.00× | 1.00× | — | 23.7 GiB |
| 2 | 30.5 | 1.57× | 1.27× | +0.27 | 24.0 GiB |
| 3 | 33.9 | 1.75× | 1.71× | +0.44 | 24.3 GiB |
| 4 | 38.1 | 1.96× | 2.04× | +0.33 | 24.5 GiB |
每增加一个序列,开销呈亚线性增长。在 N=4 时,我们遇到第一个效率平台:用 2.04× 的步骤成本获得 4 个 token ≈ 1.96×。开销来源于 llama.cpp 的批次 GEMM 路径相比其单 token GEMV 路径的内存效率较低(参见下面的带宽分析)。
VRAM 成本极低:PTI 仅需为每个额外序列增加约 0.2 GiB(仅 KV 缓存),因为模型权重是共享的。标准推测解码需要完整的第二个模型(额外 4–19 GiB)。PTI 的额外成本几乎为零。
接受机制如何工作
PTI 是 自推测解码 —— 同一个模型既作为草稿器又作为验证器,以交错位置运行。输入给序列 1–3 的猜测是由这些相同序列在上一步采样得到的:
步骤 k:
Seq 0: model(confirmed_tok, pos n) → actual_next(真实结果)
Seq 1: model(tok_b_guess, pos n+1) → next_from_b (tok_b_guess 来自步骤 k-1)
Seq 2: model(tok_c_guess, pos n+2) → next_from_c (tok_c_guess 来自步骤 k-1)
Seq 3: model(tok_d_guess, pos n+3) → next_from_d (tok_d_guess 来自步骤 k-1)
贪婪(temp=0): 相同前缀的同一模型总是产生相同的 argmax token。步骤 k-1 中的 Seq 1 的猜测必然等于步骤 k 中 Seq 0 产生的结果 → 100% 接受,每步 N 个 token,无需回滚。
温度 > 0: 接受检查(tok_b_guess == actual_next)询问的是两次独立同分布采样是否相等。概率为 Σ_x p(x)² —— 输出分布的碰撞概率。由于前缀匹配时分布相同,无需拒绝采样修正,但接受率随温度下降:
p(接受每个位置) ≈ Σ_x p(x)²
预期 token/步 = 1 + p + p² + p³
p=1.00(贪婪): 4.00 tok/步
p=0.75(低熵任务): 2.74 tok/步
p=0.60(典型文本): 2.16 tok/步
p=0.40(高创造力/高T):1.64 tok/步
在 Qwen3 推荐的 temp=0.6–0.7 下,结构化任务(代码、事实性问答)倾向于高 p 端;开放生成倾向于低 p 端。
| 解码模式 | 近似接受/位置 | 预期 tok/步 |
|---|---|---|
| 贪婪(temp=0) | 100% | 4.00 |
| temp=0.6–0.7,结构化 | ~70–80% | 2.5–3.0 |
| temp=0.6–0.7,通用文本 | ~50–65% | 2.0–2.5 |
| TSQ 微调变种(同架构双子) | 75–90% | 2.7–3.6 |
MTP 集成(Qwen3.6-27B UD 模型)
UD-Q6_K_XL 模型包含一个多 token 预测头(nextn_predict_layers=1)。这就是为什么它在 PTI 中优于 Q5_K_M(2-seq 时 1.57× vs 1.38×):MTP 头使模型能够以近乎零成本生成额外的草稿 token。pti_mtp.cpp 在拒绝后使用 MTP 头更快地重新初始化状态。实测的 38.1 tok/s 使用 pti_4seq.cpp(无需特殊 MTP 处理——UD 模型的批次效率本身就更高)。
带宽分析:为什么会有开销
本节解释实测的 2× 开销,并映射出消除它的路径。
MI50 带宽上限(实测)
| 内核 | GB/s | 占 D2D 百分比 | 备注 |
|---|---|---|---|
| HipMemcpy D2D | 383 | 100% | 理论上限 |
| 原始 int8 流 | 330 | 86% | 纯 HBM 流式 |
| 仅权重 GEMV | 254 | 66% | 权重+缩放,不含激活 |
| 平坦 Q8 GEMV (N=1) | 92 | 24% | 激活 L2 流量瓶颈 |
| 向量化 Q8 (N=1) | 100 | 26% | 128位加载,收益极小 |
| 寄存器分块 Q8 (N=4) | 127 | 33% | M_REG=4,warp-shuffle |
| 交错 float4 (N=4) | 130 | 34% | 最佳自定义内核 |
| llama.cpp Q5_K_M (N=1) | 393 | 103% | 超过了我们的 Q8 上限 |
llama.cpp Q5_K_M 实现了 393 GB/s 的“有效”带宽,这是因为 Q5_K_M 每权重打包 5 位——每缓存线比 Q8_0 的 8 位/权重多 60% 的权重。较小的模型(18.6 GB vs 27 GB)在 HBM 上传输每个 token 更快。
为什么自定义 Q8 内核表现不佳(根本原因)
对于 Qwen3.6-27B 在位置 80 的解码:
权重 HBM 流量(Q8):17408 行 × 5120 列 × 1 字节 = 89 MB
激活 L2 流量: 17408 块 × 4 序列 × 5120×4B = 1.36 GB
激活读取在 L2 流量中是权重读取的 15 倍,尽管激活张量总共只有 80 KB。每 17408 个行块都必须读取完整的激活向量 → L2 抖动。寄存器分块 (M_REG=4) 将块减少到 4353,但仅达到 D2D 上限的 34%。
为什么 llama.cpp Q5_K_M GEMV 已经是最优的
llama.cpp 的 mul_mat_vec_q 对于 Q5_K_M 实现了 393 GB/s,因为:
- 5 位/权重 → 模型更小 → 每个 token 更少的 HBM 流量
- 内核已经实现了多行分块,并共享激活
- GCN (MI50) 路径:
nwarps=2,rows_per_cuda_block=2—— 正是我们构建的分块方式
为什么 N=4 批次会导致 2× 开销
当 ncols_dst=4 时,mul_mat_vec_q 在每个权重组迭代中,每个激活块被加载 4 次(每个序列一次)。L2 流量中激活张量增加 4 倍,而权重的读取保持不变。在具有 16 MB L2 的 MI50 上,4 × 20 KB 激活(80 KB)仍然适用,但增加的 L2 压力和更长的归约树导致了实测的 2.04× 开销。
解决方案:一个 PTI 感知的内核,同时读取 4 个激活向量(交错 float4 加载),并使用 warp-shuffle 跨所有 4 个流进行归约——该方法已在 pti_kernel.hip 中以 130 GB/s(Q8 格式)验证。移植到 Q5_K_M 的原生格式后,这将消除多批次开销,并接近理论极限 4×/2.04× = 1.96× → 约 4× 的改进。
集成路线图:llama.cpp(2× → 4–5×)
当前状态
PTI 在 2× 状态下今日即可使用 llam.cpp 的公开 API。无需补丁。源码为 pti_4seq.cpp(约 150 行)。限制我们达到 2× 的开销来自于 llama.cpp 将多 token 批次路由到批次 GEMM 路径,而不是最优的单次多流 GEMV。
集成架构
llama.cpp
├── ggml/src/ggml-cuda/mmvq.cu ← 目标:在此添加 PTI GEMV 变体
│ └── mul_mat_vec_q ← 已具有 N 列模板
└── src/llama.cpp ← 添加 --pti N 标志到上下文创建
mul_mat_vec_q 内核已经接受 c_ncols_dst 模板参数用于多个输出列。GCN (MI50) 路径对 ncols_dst=1..4 使用 nwarps=2。
对 llama.cpp 的三项针对性修改:
-
添加 PTI 多流内核变体(
mmvq.cu):将 N=4 的 GEMM 分发替换为 PTI 感知的内核,该内核一次加载每个权重组,并通过交错激活读取同时计算 4 个点积。预期:消除 2.04× 开销 → 接近 4×。 -
添加 PTI 上下文 API(
llama.h/llama.cpp):
// 提议的 API 添加
llama_context_set_pti_streams(ctx, n_streams);
// 在解码循环中启用 N 流批处理,并自动进行验证/接受
- 添加 PTI 解码循环(
src/llama.cpp):验证/接受逻辑(目前在pti_4seq.cpp)作为可选模式迁移到llama_decode()中。用户通过向 llama-cli 传递--pti 4即可使用 PTI。
集成后的预期吞吐量
| 阶段 | 技术 | 预期 tok/s | 相比今日 |
|---|---|---|---|
| 今日 | pti_4seq.cpp(外部) | 38.1 | — |
| 步骤 1 | mmvq.cu 中的 PTI 内核 | ~50–60 | +30–60% |
| 步骤 2 | PTI + Q5_K_M 原生格式 | ~65–75 | +70–100% |
| 步骤 3 | PTI × MTP(UD 模型) | 80–100 | +110–160% |
步骤 1 估算:消除 GEMM 开销(2.04× → ~1.1×)在 4-seq 下给出 4 / 1.1 × 19.4 ≈ 70 tok/s。保守估计 50–60 考虑了剩余的 L2 激活压力。
步骤 3 上限(PTI + MTP 在 Q5_K_M 上):
步骤成本:18.6 GB(一次 Q5_K_M 模型加载,原生格式)
输出 token:2 流 × ~1.88(MTP 接受率)≈ 3.76 token/步
每 token 带宽:18.6 / 3.76 = 4.9 GB/token
在 393 GB/s(llama.cpp GEMV 效率)下:393 / 4.9 ≈ 80 tok/s
与标准推测解码的对比
| 标准推测解码 | PTI 今日 | PTI + 集成 | |
|---|---|---|---|
| 草稿模型 | 小型独立模型 | 同一模型 | 同一模型 |
| 草稿 VRAM | +4–19 GiB | +0.2 GiB/序列 | +0.2 GiB/序列 |
| 接受率(贪婪) | ~60–70% | 100% | 100% |
| 质量 | 草稿模型质量 | 与基线相同 | 与基线相同 |
| 吞吐量增益 | ~1.6× | 1.96×(实测) | 4–5×(预估) |
| llama.cpp 补丁 | 无需 | 无需 | 3项针对性修改 |
架构图
| 图 | 说明 |
|---|---|
diagram-pti-4seq-step.svg | 一个完整的 PTI 步骤:4-token 批次,权重共享,验证/接受/拒绝路径,KV 布局 |
diagram-pti-overhead.svg | 为什么开销是 2× 而不是 4×:权重负载共享,激活读取是成本,吞吐量柱状图 |
diagram-memory-layout.svg | SSQ uint16 块格式——两个 int8 权重如何打包到一个字中 |
diagram-workflow.svg | 从预填充到接受/拒绝的 PTI 推理循环高层图 |
文件
| 文件 | 用途 | 状态 |
|---|---|---|
pti_4seq.cpp | 4 序列 PTI —— 公开 llama.cpp API | ✓ 实测:38.1 tok/s |
pti_mtp.cpp | 3 序列 PTI + MTP 重新初始化 | ✓ 实测:33.9 tok/s |
pti_llama.c | 2 序列 PTI —— C API(基线) | ✓ 实测:28.9–30.5 tok/s |
pti_kernel.hip | HIP/ROCm Q8 多流内核 + 基准测试 | ✓ 带宽上限分析已完成 |
pti_hip.py | HIP 内核的 Python 封装 | ✓ |
infer.py | PTI 推理循环 —— Python 参考实现 | ✓ |
benchmark.py | 接受率和输出正确性 | ✓ |
pack.py | 将模型与自身打包为 SSQ 双子流 | ✓ |
Makefile | 为 MI50 / MI100 / RX 7900 构建 | ✓ |
DESIGN.md | 完整设计原理、数学推导、实现 | ✓ |
使用的模型(MI50,32 GiB VRAM)
| 文件 | 大小 | 格式 | 备注 |
|---|---|---|---|
Qwen3.6-27B-Q5_K_M.gguf | 18.6 GB | 5 位 K-quant | 最佳带宽/质量权衡 |
Qwen3.6-27B-UD-Q6_K_XL.gguf | 25 GB | 6 位 K-quant | 带有 MTP 头;最佳 PTI 结果 |
Qwen3.6-27B-Q8_0.gguf | 27 GB | 8 位 | 自定义内核目标 |
构建与运行
# 构建 pti_4seq → bin/pti_4seq(需在 ../llama.cpp/build 中预先构建 llama.cpp)
make 4seq
# 运行 4 序列 PTI —— 复现 38.1 tok/s 的主要结果
make 4seq-run
# 等价于:bin/pti_4seq -m ../gguf/Qwen3.6-27B-UD-Q6_K_XL.gguf \
# -p "The key to faster LLM inference is" -n 80 -ngl 99
# 基线用于直接对比
make 4seq-run-base
# 3 序列 PTI + MTP
make mtp && make mtp-run
# 2 序列 PTI(C 实现)
make llama && make llama-run-pti
# 一次性构建所有三个 llama.cpp 二进制文件
make all-llama
# HIP 内核带宽基准测试(需要 ROCm)
make && bin/pti_test
# 清理所有构建产物
make clean
先决条件:
- 在
../llama.cpp/build/中构建的 llama.cpp,支持 ROCm/HIP - AMD MI50 (gfx906) 或兼容 GPU,内存 ≥32 GiB
Qwen3.6-27B-UD-Q6_K_XL.gguf放在../gguf/中
与 SSQ(Side-by-Side Quantization)的关系
达到 4–5× 的更深路径使用 SSQ 格式,该格式将两个 int8 权重值打包到一个 uint16 字中。一次从 VRAM 加载即可获得两个流的权重:
uint16_t pw = W_packed[i];
int8_t wa = (int8_t)(pw >> 8); // 流 A 权重
int8_t wb = (int8_t)(pw & 0xFF); // 流 B 权重
acc_a += scale_a * (float)wa * x_a[i];
acc_b += scale_b * (float)wb * x_b[i];
// → 一次内存事务,两个计算流
SSQ 实现了 TSQ(任务特定量化) 变体:将基础模型和微调模型打包为双子对。在微调目标域上接受率很高,接受率下降表示离域查询路由。SSQ 内核(pti_kernel.hip)已构建并进行了基准测试。下一步是将其集成到 llama.cpp 的内核分发路径中,用融合的单次多流 GEMV 替换多批次 GEMM 的开销。
总结:已实现与下一步
今日已实现(可复现):
- 在 Qwen3.6-27B 上使用公开 llama.cpp API 实现 38.1 tok/s —— 1.96× 基线
- 贪婪模式下 100% 接受率 —— 与基线相比无质量损失
- 仅增加 +0.8 GiB VRAM 开销(而标准推测解码需要 +4–19 GiB)
- 已测量的带宽上限,确认了开销来源(批次 GEMM 分发)
下一步(有针对性的 llama.cpp 集成):
- 在
mmvq.cu(量化 GEMV 内核文件)中添加 PTI 内核变体 - 模板已支持
ncols_dst=N;PTI 需要交错激活读取 - 消除了 2.04× 的开销 → 预计在质量不变的情况下达到 50–70 tok/s
- 完整的 PTI × MTP 路径预计在 MI50 上达到 80–100 tok/s
硬件:AMD MI50(16 nm,gfx906,32 GiB HBM2,峰值约 1 TB/s)。模型:Qwen3.6-27B。
所有吞吐量数据均为总输出 token/秒,使用 -n 80 生成。
相似文章
Qwen-3.6-27B + llamacpp 投机解码效果惊艳
Reddit 用户展示了 llamacpp 的投机解码功能将 Qwen-3.6-27B 的生成速度从 13.6 提升至 136.75 t/s,并分享了完整的命令参数和硬件配置。
我在 vLLM 和 llama.cpp 上对 Gemma 4 和 Qwen 3.6 测试了 MTP —— 推理速度提升 3.34 倍,这是我的发现(RTX 6000 PRO)。
使用 vLLM 和 llama.cpp 对 Gemma 4 31B 和 Qwen 3.6 27B 进行的多令牌预测(MTP)基准测试显示,推理速度最高提升 3.34 倍,最优推测令牌数量因模型和引擎而异。
在 12GB 显存下,使用 Qwen3.6 35B A3B 与 llama.cpp MTP 实现 80 tok/sec 的速度和 128K 上下文
一名用户分享了一份配置方案,该方案在使用 llama.cpp 和多令牌预测(MTP)的情况下,能在 12GB 显存的 GPU 上让 Qwen3.6 35B A3B 模型实现超过每秒 80 个令牌的生成速度。帖子中包含了基准测试结果以及用于优化性能的具体命令行参数。
@iotcoi:Qwen3.6-27B-FP8 + Dflash + DDTree,256k 上下文,10 个智能体,单颗 49W GB10 上峰值 200 tokens/s,平均解码 136 tokens/s
量化版 27B Qwen3.6 在单颗 49W GB10 GPU 上借助 Dflash+DDTree 优化,256k 上下文、10 智能体并发,峰值达 200 tok/s,平均 136 tok/s。
成功运行 MTP + TurboQuant — Qwen3.6-27B 在单 RTX 4090 上实现 262K 上下文 80+ token/秒
开发者通过将 MTP(多 Token 预测)与 TurboQuant 的无损 KV缓存压缩技术相结合,在单张 RTX 4090 上实现了 Qwen3.6-27B 模型在 262K 上下文下 80+ token/秒的推理速度,并分享了实现分支和技术细节。