2倍 tok/s(在1块MI50上从19.4 tok/s提升到38.1 tok/s)尝试类似推测解码的假设……但不是用额外的侧模型,而是利用我可以同时运行多个计算,就好像内存里加载了两份Qwen3.6-27B一样——小量化不占用所有可用算力。

Reddit r/LocalLLaMA 工具

摘要

打包双推理(PTI)是一种通过单批解码中运行多个token序列来实现约2倍LLM吞吐量的技术,它利用了llama.cpp中的权重共享,无需草稿模型或额外VRAM。

MODS:如果你想删掉这个垃圾内容,没问题——等我有了类似llama.cpp补丁的东西后,我会重新发布一个有用的内容。我会在Medium账号上写一篇完整文章说明原理。只是太兴奋了想分享一下。 *** 请原谅README中的Claude摘要,但基础功能是可行的。我还在开发HIP内核,并尝试将其与MTP结合。希望能接近80 tok/s。这一切源于我意识到每个Q8(INT8或F8)计算都使用了f32的计算资源,只用了可用数字的1/4……所以,每个加载的值我们可以运行4次操作。然后推测解码的思路是用一个旁边的小模型运行预测,大模型进行投票/否决——为什么不直接用同一个模型来做这些判断呢?KV缓存增加了一点开销,但很小。请看README中的表格和一些SVG图。基准测试是在我的单张MI50上运行的。由于我们利用了较小量化(Q8或更小)的特性,这实际上只适用于这些量化。
查看原文
查看缓存全文

缓存时间: 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_M21.11.00×18.1 GiB
PTI 4-seqUD-Q6_K_XL38.11.96×24.5 GiB
PTI 3-seqUD-Q6_K_XL33.91.75×24.3 GiB
PTI 2-seqUD-Q6_K_XL30.51.57×24.0 GiB
PTI 2-seqQ5_K_M28.91.38×18.4 GiB
基线UD-Q6_K_XL19.41.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.41.00×1.00×23.7 GiB
230.51.57×1.27×+0.2724.0 GiB
333.91.75×1.71×+0.4424.3 GiB
438.11.96×2.04×+0.3324.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 D2D383100%理论上限
原始 int8 流33086%纯 HBM 流式
仅权重 GEMV25466%权重+缩放,不含激活
平坦 Q8 GEMV (N=1)9224%激活 L2 流量瓶颈
向量化 Q8 (N=1)10026%128位加载,收益极小
寄存器分块 Q8 (N=4)12733%M_REG=4,warp-shuffle
交错 float4 (N=4)13034%最佳自定义内核
llama.cpp Q5_K_M (N=1)393103%超过了我们的 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,因为:

  1. 5 位/权重 → 模型更小 → 每个 token 更少的 HBM 流量
  2. 内核已经实现了多行分块,并共享激活
  3. GCN (MI50) 路径:nwarps=2rows_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 的三项针对性修改:

  1. 添加 PTI 多流内核变体mmvq.cu):将 N=4 的 GEMM 分发替换为 PTI 感知的内核,该内核一次加载每个权重组,并通过交错激活读取同时计算 4 个点积。预期:消除 2.04× 开销 → 接近 4×。

  2. 添加 PTI 上下文 APIllama.h / llama.cpp):

// 提议的 API 添加
llama_context_set_pti_streams(ctx, n_streams);
// 在解码循环中启用 N 流批处理,并自动进行验证/接受
  1. 添加 PTI 解码循环src/llama.cpp):验证/接受逻辑(目前在 pti_4seq.cpp)作为可选模式迁移到 llama_decode() 中。用户通过向 llama-cli 传递 --pti 4 即可使用 PTI。

集成后的预期吞吐量

阶段技术预期 tok/s相比今日
今日pti_4seq.cpp(外部)38.1
步骤 1mmvq.cu 中的 PTI 内核~50–60+30–60%
步骤 2PTI + Q5_K_M 原生格式~65–75+70–100%
步骤 3PTI × 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.svgSSQ uint16 块格式——两个 int8 权重如何打包到一个字中
diagram-workflow.svg从预填充到接受/拒绝的 PTI 推理循环高层图

文件

文件用途状态
pti_4seq.cpp4 序列 PTI —— 公开 llama.cpp API✓ 实测:38.1 tok/s
pti_mtp.cpp3 序列 PTI + MTP 重新初始化✓ 实测:33.9 tok/s
pti_llama.c2 序列 PTI —— C API(基线)✓ 实测:28.9–30.5 tok/s
pti_kernel.hipHIP/ROCm Q8 多流内核 + 基准测试✓ 带宽上限分析已完成
pti_hip.pyHIP 内核的 Python 封装
infer.pyPTI 推理循环 —— Python 参考实现
benchmark.py接受率和输出正确性
pack.py将模型与自身打包为 SSQ 双子流
Makefile为 MI50 / MI100 / RX 7900 构建
DESIGN.md完整设计原理、数学推导、实现

使用的模型(MI50,32 GiB VRAM)

文件大小格式备注
Qwen3.6-27B-Q5_K_M.gguf18.6 GB5 位 K-quant最佳带宽/质量权衡
Qwen3.6-27B-UD-Q6_K_XL.gguf25 GB6 位 K-quant带有 MTP 头;最佳 PTI 结果
Qwen3.6-27B-Q8_0.gguf27 GB8 位自定义内核目标

构建与运行

# 构建 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 生成。

相似文章