前两天我在一台消费级显卡上部署 Qwen 量化版跑推理,结果发现 INT4 模型居然只有 5 tokens/s,当时我第一反应是“这卡废了”。查了一圈之后才意识到,问题根本不在硬件,而在于推理引擎的调度方式和模型服务的负载特性。大模型推理加速里反复出现的“量化”“投机采样”“PD 分离”,其实分别对应着推理链路中最要命的三个瓶颈——带宽、串行生成和资源争抢。这篇文章我想把这三块内容讲透,连同我在 nano-vllm 上做实验的过程一起讲清楚,适合那些已经能跑通大模型、但对推理性能感到困惑,想系统理解加速原理的读者。
1. 先算清楚账:大模型推理到底慢在哪三处
想理解加速技术,第一步不是背概念,而是搞清楚推理为什么慢。很多优化方案看起来高大上,本质都指向同一个目标:用更少的资源、更快的时间,把一个 token 一个 token 地“挤”出来。
1.1 自回归生成机制让推理天然串行
大模型生成文本的核心机制是自回归:每生成一个 token,都要把当前已生成的全部 token 作为上下文,重新做一次前向计算,才能得到下一个 token 的概率分布。这个“生成下一个词依赖前面所有词”的循环,导致推理阶段无法像训练阶段那样大规模并行。
你可以把大模型想象成一个极其博学但极其啰嗦的教授,问它一个问题后,它必须先把之前写过的每个字都重新读一遍,然后才能写出下一个字。问题是这个“重读”的成本非常高——模型有多大,每次读一遍就要搬运多少数据。举个例子,一个 7B 参数的 FP16 模型,权重占 14GB 显存,每生成一个 token,都需要把全部 14GB 权重从显存搬到计算单元。按常见的 936GB/s 带宽算,光是搬运权重就需要 15ms 左右,理论极限也就是 60~70 token/s。实际跑起来往往只有一半甚至更低。
这个数据背后藏着一个反直觉的结论:推理阶段的计算单元大量时间在“等待数据”,而不是在做计算。大模型的单次前向乘法数量虽然不小,但相对于权重传输的时间来说,算力是过剩的。说白了,限制速度的是“运知识”的通道,而不是“用知识”的大脑。
1.2 三个瓶颈对应三条加速路线
我把推理慢这件事拆成三个独立的瓶颈来看,后面所有加速技术都是围绕它们展开的:
| 瓶颈 | 现象 | 对应加速技术 | 核心思路 |
|---|---|---|---|
| 权重搬运带宽有限 | 每个 token 都要读全量权重,显存带宽成为天花板 | 量化 | 减少权重和 KV Cache 的字节数,降低搬运量 |
| 自回归逐字串行 | 每次只能生成一个 token,无法并行产出 | 投机采样 | 用小模型“先猜一段”,大模型一次性验证多个位置 |
| Prefill 与 Decode 资源冲突 | 长输入预处理和短输出生成互相抢 GPU | PD 分离 | 把两个阶段拆到不同机器/进程,各自独立优化 |
这三个方向不是互斥的,生产环境里经常叠加使用。但如果你不知道瓶颈在哪,随便乱开优化开关,不仅提不了速,反而可能拖慢系统。下面我从量化讲起,因为它最容易理解和上手。
2. 量化:把“运知识”的负担减到四分之一
量化是这三项技术里我最推荐先学的。它不仅直观,而且几乎适用于任何硬件场景,从手机到数据中心都能看到它的身影。
2.1 为什么量化对推理有奇效,但对训练帮助不大
量化的大意是把模型权重从高精度(FP16/BF16)压缩到低精度(INT8/INT4/FP8),比如原来一个权重用 2 字节存,压缩到 0.5 字节后,整个模型的体积就缩成四分之一。
关键问题是:为什么这个操作对推理提速这么明显,却很少听说有人用它加速训练?因为训练时你会一次性喂进去大批量的数据,计算强度很高,算力才是主要瓶颈;而推理时每生成一个 token 只做一次不完整的前向计算,计算强度极低,系统始终被“搬运数据”卡着脖子。量化把每次搬运的数据量直接砍掉 75%,自然就能撬动好几倍的速度提升。
拿同一个 7B 模型举例,FP16 时每 token 要搬 14GB 数据;压成 INT4 后只要 3.5GB,理论上生成极限能从 60 token/s 提到 260 token/s 左右。虽然实际同时还有 KV Cache 读取、激活值计算和内核调度开销,速度不可能完全线性翻倍,但体感提升绝对明显。这也是为什么社区里许多人跑 27B 或 30B 级别大模型时,都会优先选 4-bit 量化版本——本质就是用精度换带宽,而且大多数场景下这波交易相当划算。
2.2 主流量化方案对比:GPTQ、AWQ、GGUF、MLX
量化不是简单地把 FP16 四舍五入成 INT4,不同方案对精度损失的把控、适用的推理引擎、部署场景差别很大。我用一个表总结一下目前常见的路径:
| 方案 | 压缩思路 | 典型使用场景 | 配套引擎/工具 | 特点 |
|---|---|---|---|---|
| GPTQ | 基于二阶梯度信息校准,按层量化到 INT4/INT3 | 数据中心 GPU 部署 | vLLM、AutoGPTQ、ExLlama | GPU 推理成熟,吞吐高 |
| AWQ | 根据激活值大小保护显著通道,减少量化损失 | GPU 部署,需要更好质量 | vLLM、AutoAWQ | 对激活值敏感场景更稳 |
| GGUF | 支持混合精度(Q2_K、Q4_K_M 等)和 CPU/GPU 混合推理 | 本地个人电脑、低显存环境 | llama.cpp、Ollama | CPU 友好,生态模型最多 |
| MLX | Apple Silicon 原生精度优化,支持 4-bit/8-bit | MacBook、Mac mini 本地部署 | MLX 社区推理 | 内存带宽利用充分,省电 |
选型时我的经验是三个问题定方向:你跑在什么硬件上?显存够不够装下 FP16 权重?你对精度损失敏感吗?如果是本地个人电脑且有 NVIDIA 显卡,优先考虑 GGUF 搭配 Ollama;如果是 API 服务、追求高吞吐,GPTQ 或 AWQ 走 vLLM 会更合适;如果手里是 Mac,别犹豫,直接用 MLX。
值得多说一句的是,量化社区现在也在分化:有些量化版本做得好,校准数据集覆盖广,几乎无损;有些只是简单转格式,跑复杂任务时明显变蠢。qwen3.8-27b这类热门模型的 4-bit 版本我试过几个,不同发布者做出来的效果差异很大,后面我会专门讲怎么避坑。
2.3 量化部署实操:从下载模型到跑起来的一次完整记录
以 GGUF 格式为例,我挑一个最典型的消费级部署链路,整条跑下来大概十分钟。
第一步,去 Hugging Face 或 ModelScope 上下载目标模型的 GGUF 量化版,注意文件名里的量化等级标识。Q4_K_M 是平衡之选,显存宽裕想保精度可以用 Q5_K_M,显存很紧就用 Q3_K_M 或 Q2_K。下载文件名里那个-q4_k_m.gguf后缀千万别看漏了,很多人栽在这。
第二步,用 llama.cpp 或 Ollama 直接加载。Ollama 更简单,写好一个Modelfile指向本地 GGUF 文件就能用;想折腾底层可以手动编译 llama.cpp,跑命令时加-ngl参数把层全部放进 GPU:
# llama.cpp 加载 Q4_K_M 量化模型,全部层放 GPU ./llama-cli -m ./models/qwen3-27b-q4_k_m.gguf -ngl 99 -c 8192 -f prompt.txt第三步,算一下显存是否够用。以 27B 模型 INT4 为例,权重约 17GB,8K 上下文的 KV Cache 大约 1~3GB,加上激活值和其他开销,24GB 显存的卡会卡在危险边缘;如果用 Q3 量化把权重压到 13~14GB,就能稳定塞进去。这也是为什么很多人用 27B 模型跑 4-bit 在单卡上磕磕碰碰——不是模型不行,是没算清楚 KV Cache 的账。
如果你的场景是 GPU 单机高并发,也可以走 GPTQ + vLLM 路线,加载起来同样透明:
# 伪代码示例:vLLM 加载 GPTQ 量化模型 from vllm import LLM llm = LLM( model="Qwen/Qwen3-7B-GPTQ-Int4", quantization="gptq", max_model_len=8192, )2.4 量化最容易翻车的三个地方
第一,校准数据集决定了量化模型的“智力下限”。GPTQ/AWQ 这类方案需要一组代表真实分布的文本做校准,如果发布者用的是和模型领域差距很大的数据,量化后的模型在专业任务上会明显退步。所以下载量化版尽量选官方发布的,或者社区口口相传、任务表现比自己转的好得多的版本。
第二,量化模型不是越大越好,也不是越小越强。对于 3B、7B 这类小模型,INT4 量化带来的精度损失会更明显,因为模型本身的冗余度低;而对 70B 级别的大模型,4-bit 量化损失往往可以忽略。这也是为什么我的建议是:大模型放心量化,小模型慎重量化。
第三,显存占用降了,推理不一定自动变快。有些量化实现的反量化(dequant)过程本身也有开销,特别是不支持低精度矩阵乘法的硬件上,实际收益会大打折扣。判断一个量化版好不好,不能只看“能不能跑”,还要看实测生成的 tokens/s。如果量化后速度没提升,试试换成 AWQ 或换推理引擎,很可能模型格式和引擎适配出了问题。
量化解决了“每次读太重”的问题,但还有一个待解难题:即使权重变轻了,自回归依然是逐字进行的。你每生成一个 token,都得等上一次完整前向结束。下面这个技术,专门从这个角度下手。
3. 投机采样:与其一个字一个字等,不如“先猜后验”
投机采样(Speculative Decoding)是我个人觉得大模型推理加速里最有意思的一个想法。它不改变模型本身,也不用降精度,而是改变了生成策略——让一个“快但不太聪明”的小模型打草稿,再让“慢但聪明”的大模型一次性验证一整段草稿。
3.1 先搞懂验证为什么比生成便宜
这背后的关键点在于并行度。大模型前向计算时,如果只输入一个 token,它只能输出一个 token;但如果一次输入一串 token(比如 5 个),它可以并行预测这 5 个位置各自的下一个 token——这次前向的计算量和输入长度成正比,但耗时不会同步增长,因为 GPU 本身就擅长并行矩阵运算。
所以投机采样的玩法是:先用一个小得多的草稿模型(Draft Model)快速生成 K 个候选 token,然后把这一段候选全部喂给大模型做一次并行验证。大模型会对每个位置给出自己的概率分布,如果草稿 token 落在接受范围内,就直接通过;如果某个位置被拒绝,就从这个位置重新开始生成,丢弃后面所有草稿。
你可以把它类比成工作场景中的“秘书先起草,领导一次性审阅”。如果每次草稿质量都很高,领导看一眼就能签完全文;如果草稿写得很烂,领导还得坐下来重写,反而浪费时间。从期望上看,只要草稿够好,一次大模型前向就能同时完成多个 token 的生成,推理速度就这样提上来了。
理论上,如果草稿长度 K=5、平均接受率 r=0.8,那么每次大模型验证平均能产出 (1 - r^(K+1)) / (1 - r) 约 3.3 个 token,潜在加速接近 3 倍。当然,实际还要扣除草稿模型本身的前向开销,通常体感是 1.5~2.5 倍,但已经很可观了。
3.2 草稿模型不是越强越好:选型要看“性价比”
投机采样的效果高度依赖草稿模型的选择。说白了,要找一个“猜得中率中肯、跑得足够快”的小模型,而不是随便拿个大模型当草稿。
草稿模型太弱,接受率低,多一次的快速预测变成了纯额外开销;草稿模型太强,比如直接拿一个 7B 模型给 27B 模型当草稿,它自己的生成时间就占了很大比重,省下的验证时间全赔进去。常见做法是选择同系列、同 tokenizer 的较小尺寸模型,比如主模型是 Qwen3-27B,草稿模型用 Qwen3-0.6B 或 1.7B。
这里有一个特别容易踩的坑:草稿模型和目标模型的 tokenizer 必须完全一致。如果 tokenizer 不同,草稿输出的 token 序列在大模型眼里根本不是一句话,投机采样直接失效。另外,草稿生成的 temperature 和顶部采样的参数最好和目标模型匹配,否则接受率会打折。
3.3 vLLM 环境下的投机采样配置参考
在 vLLM 里开启投机采样非常简单,核心参数就两个:speculative_model指定草稿模型,num_speculative_tokens指定草稿长度。官方建议通常在 3~5 之间,太小加速有限,太大草稿模型延迟尾随严重。
from vllm import LLM llm = LLM( model="Qwen/Qwen3-27B-BF16", speculative_model="Qwen/Qwen3-0.6B", num_speculative_tokens=5, )跑在线服务时也一样,用 OpenAI 兼容的启动参数即可:
vllm serve Qwen/Qwen3-27B-BF16 \ --speculative-model Qwen/Qwen3-0.6B \ --num-speculative-tokens 5启动后建议盯着服务日志里的acceptance rate指标。如果你看到接受率稳定在 0.4 以下,说明草稿模型选得太弱,换大一档试试;如果 tokens/s 反而下降了,则说明草稿模型太快了、但这也可能意味着草稿长度太长,把它调小几档对比一下。投机采样的调参本质就是一个“草稿收益减去草稿成本”的平衡问题。
值得注意的是,规模较小的本地模型做起投机采样来比较吃亏——草稿模型的额外开销会吃掉大部分收益。我实测下来,主模型至少 14B 级别以上,开启投机采样才有明显正收益;7B 以下开它真的属于“折腾自己”。
量化把权重搬得更轻了,投机采样把串行生成改成了并行验证,接下来还剩一个工程层面的大问题:在线服务里,很多请求在“读题”阶段和“答题”阶段会互相打架,这就是 PD 分离登场的理由。
4. PD 分离:给 Prefill 和 Decode 各开一条生产线
PD 分离全称是 Prefill-Decode Disaggregation,翻译过来就是“预填充与解码分离”。在阅读代码或者看部署方案时,你可能会看到 “Prefill” 和 “Decode” 这两个词,它们分别指代推理过程中两个性质截然不同的阶段。
4.1 Prefill 和 Decode 是性格完全不同的两种负载
Prefill(预填充)阶段:用户输入的 prompt 被一次性喂给模型,做并行计算,生成第一个 token 的同时产出所有层的 KV Cache。这个阶段计算量大、耗时与输入长度成正比,GPU 的利用率很高。
Decode(解码)阶段:从第一个 token 开始,每轮只输入上一个 token,逐步生成后续 token。这个阶段计算量小,主要受显存带宽限制,并且因为不同请求进度不一,需要不断调度批处理。
从算力视角看,它们俩几乎是两个方向的负载——一个算力饥渴,一个带宽饥渴;一个吃满 GPU 计算核心,一个闲着计算核心等数据。如果把它们塞在同一张卡上,典型的灾难场景是:某个用户发来一篇超长文档,Prefill 阶段瞬间把 GPU 占满,后面所有正在做 Decode 的在线请求全部被拖慢;而 Prefill 跑完以后,轮到 Decode 了,GPU 的计算核心又大把闲着。
| 维度 | Prefill | Decode |
|---|---|---|
| 输入特征 | 一次处理全部 prompt token | 每次只处理一个 token |
| 计算特征 | 计算密集型 | 访存密集型 |
| 耗时 | 与 prompt 长度强相关 | 与生成长度强相关 |
| 批处理弹性 | 差,长 prompt 会独占资源 | 好,可大量并发批处理 |
| 优化侧重 | 矩阵算力、KV Cache 预建 | 带宽利用、连续批处理调度 |
4.2 分离部署后发生了什么
PD 分离的思路很直接:不把这两个阶段放在同一块 GPU 上,而是分别部署专门的 Prefill 实例和 Decode 实例。请求进来后,先由负载均衡器分给某个 Prefill 节点,完成第一次前向和 KV Cache 构建;随后把 KV Cache 从 Prefill 节点传给 Decode 节点,Decode 节点根据这份 Cache 持续生成 token。
这样做的好处在于三层:
- 互不抢资源。长文档读题再也不会拖垮正在生成的请求,反之亦然。
- 可以独立缩放。如果在线请求普遍是短 prompt 长输出,就多扩 Decode 节点;如果是 Agent 场景大量长上下文,就多扩 Prefill 节点。
- 各阶段用不同优化。Prefill 节点可以选用更高算力的卡(比如 A100/H100),Decode 节点可以选用带宽高、性价比合适的卡。不同规格的硬件各司其职,整体单位成本反而下来了。
当然,工程实现不是嘴上说的这么简单。KV Cache 动辄几百 MB,从 Prefill 实例传到 Decode 实例这个动作本身也有延迟;集群里如果刚好没有空闲 Decode 实例,请求还得排队。所以生产环境下通常配合请求调度器做全局视角的流量分发,把 Prefill 和 Decode 的负载预测也纳入规划。
对于单机用户来说,PD 分离听起来有点遥远,但这里有一个共享同一个内核思想的“轻量版”优化:Chunked Prefill(分块预填充)。它把长 prompt 切成若干 chunk,每处理完一个 chunk 就去服务一批 Decode 请求,再回来处理下一个 chunk,相当于在单卡内部给 Prefill 和 Decode 做了时间片轮转,能显著缓解长上下文请求阻塞在线回答的问题。如果你跑的是 vLLM 这类引擎,也能直接体验这个思路。
5. 从原理到上手:怎么用 nano-vllm 把这些技术串起来
讲到这儿,量化、投机采样、PD 分离的理论部分都过了一遍。但是纸上得来终觉浅,真正理解这些机制,还是要在代码和实验里摸一把。我自己学习时用的是 nano-vllm,一个刻意保持精简的推理引擎实现,非常适合用来搞懂大模型推理的关键功能。
5.1 为什么建议用 nano-vllm 当学习入口
vLLM 本身功能全面,但代码量大,调度、分页、内存管理、模型加载、采样器全混在一起,新手看代码特别容易迷失。nano-vllm 的设计目标就是“小而全”——保留 vLLM 的核心机制,比如 PagedAttention(分页注意力)、Continuous Batching(连续批处理)、投机采样、量化加载等,但剥离了大量生产级细节,代码量小很多。
我建议的学习路径是这样的:先读调度器(Scheduler),看它如何管理请求状态;再读分页 KV Cache 管理器,理解显存块分配逻辑;接着看模型 worker 和采样器,弄懂量子化和投机采样在哪里接入;最后对照一个请求的完整生命周期,把 Prefill、Decode 的切换过程在代码里标记出来。
5.2 一套可复现的本地推理加速实验
我在一张 24GB 显卡上做过一组对比实验,实验对象是 Qwen3-4B 基础版与 INT4 量化版,同时记录开启投机采样前后的吞吐数据。这个规模不大,适合复现。
第一步,用uv或conda建一个干净环境,安装 nano-vllm 以及依赖。第二步,分别以 FP16、INT4、INT4+投机采样三种配置启动服务,执行同一组测试 prompt,记录 tokens/s 和首 token 延迟。第三步,把--enable-prefix-caching或 chunked prefill 开关打开再试一轮,观察长 prompt 对短请求的干扰是否下降。
启动命令大致长这样:
# 开启投机采样的 nano-vllm 服务示例 python -m nano_vllm.serve \ --model Qwen/Qwen3-4B \ --dtype float16 \ --speculative-model Qwen/Qwen3-0.6B \ --num-speculative-tokens 4 \ --max-model-len 8192实验记录表对一个参考,方便对比不同配置的收益:
| 配置 | 权重体积 | 实测吞吐(约) | 首 token 延迟 | 备注 |
|---|---|---|---|---|
| FP16 基线 | ~8GB | 低 | 正常 | 对照样本 |
| INT4 量化 | ~2.5GB | 明显提升 | 下降 | 带宽瓶颈缓解 |
| INT4+投机采样 | ~2.5GB | 进一步提升 | 略增 | 需要接受率正常 |
| INT4+分块 Prefill | ~2.5GB | 稳定 | 长 prompt 干扰降低 | 并发场景更稳 |
当然,具体的数字会因显卡型号和驱动差异而不同,但结论趋势是一致的:量化带来的收益几乎最大,投机采样在模型较大时才显著,分块 Prefill 对在线并发更有价值。
5.3 不同硬件条件下怎么组合这三项技术
结合我自己踩过的坑,给几个不同硬件规模下的组合参考:
- 16GB 消费级显卡:优先跑 INT4/GGUF 量化模型,先保证能装下。投机采样收益有限,但如果主模型在 14B 以上可以尝试;PD 分离不用想。
- 24GB 消费级显卡:量化是必须的,27B 模型 Q4 可以流畅跑;投机采样搭配 0.6B 或 1.7B 草稿模型,能在量化基础上再提速 20%~50%。如果你是 API 服务或处理长文档,考虑开 chunked prefill。
- 多卡/数据中心 GPU:量化、投机采样、PD 分离可以全上。生产环境通常还会配合前缀缓存、路由策略,让 Prefill 和 Decode 实例各自扩容。此时不光是推理引擎的调参问题,还有分布式 KV 传输和负载预测的全局设计。
- Apple Silicon 设备:优先 MLX 格式量化,投机采样在部分实现里也可以开,但由于 Mac 的内存带宽相对较大,收益不如 NVIDIA 平台明显。
最后说一点我个人的真实体会。学习这三项技术时,最容易犯的错误是“一上来全开”。我见过不少人十八般武艺全部加在同一套服务上,最后没快多少,反而增加了调度延迟和排错难度。从量化开始,确认瓶颈逐步释放后,再加投机采样;PD 分离属于架构级的改动,适合你对前两项的收益边界有清晰认知以后再考虑。如果你也在研究大模型推理加速,建议先从这篇提到的三个底层瓶颈入手,对照自己的部署环境一项一项测,比盲目堆参数有效得多。