vLLM|约束解码 + 高性能吞吐,从原理到实战
2026/8/19 9:05:14 网站建设 项目流程

第一章:底层基石 —— 主流大模型分词器详解

1.1 为什么分词器对LLM推理很重要

模型只能处理数字,文本必须先经过 Tokenizer 转化为 Token ID 序列。

原始文本 “Hello, world!” → Tokenizer 切分 + 编码 → Token IDs [15496, 995, 0] → Embedding 向量查表 → 语言模型 Transformer

有三种切分粒度:字符级、词级、子词级。现在的llm都用的子词级,字符级词表极小但是序列极长、并且字符本身没有语义;词级序列最短但是词表巨大并且新词是UNK。

理想的Tokenizer要满足三个目标:零 UNK 覆盖率(不要出现[UNK]);高压缩率 (token尽量少);多语言公平性(避免中文几个字就消耗大量token,而英文同样语义只消耗很少token)。

分词器直接决定 token 序列长短:token 越多,上下文窗口消耗越大,KV‑Cache 占用的显存就越高;出现[UNK]会丢失原始文本信息;而后面 vLLM 的约束解码能力,更是完全运行在 token 粒度之上。理解分词算法,是看懂 LLM 推理底层逻辑的第一步。

1.2 BPE 原始算法

BPE(Byte‑Pair Encoding,字节对编码)最早并不是为大模型发明的,它是一种无损数据压缩算法,后来被引入NLP领域做子词分词。

核心思想:自底向上贪心合并

  1. 先把训练集全部文本,按空格切分成独立单词;再把每个单词拆解成最基础的字符序列。
  2. 统计所有相邻字符对的出现频次。
  3. 选出出现频率最高的字符对,合并成一个新子词单元,加入词表。
  4. 重复上面统计‑合并的过程,直到词表达到预设大小。

⚠️关键前置约束:原始BPE必须依赖空格预先切分单词,单词边界是提前确定好的,算法本身不会处理句子。

优势

  • 实现简单,训练、编码解码速度快;
  • 高频片段会被合并为完整token,取得不错的压缩效果;
  • 子词由训练数据统计得来,贴合真实文本分布。

劣势

  1. 强依赖预分词:需要依靠空格切分单词,对中文、日文这类没有天然空格分隔的语言处理很麻烦。
  2. 存在OOV/UNK问题:遇到训练中从未见过的生僻字符、特殊Unicode符号(比如这个表情:🤣),如果词表里没有对应单元,就只能输出未知token[UNK],丢失信息。
  3. 单词边界被固化:切分行为高度依赖预分词结果,预分词出错,后续BPE合并结果也跟着出错。

早期部分开源模型使用原始BPE,现在生成式大模型基本不再直接使用,它更多是作为BBPE的理论原型。

1.3 BBPE(Byte‑Level BPE,字节级BPE)

BPE的缺陷:

  • Unicode 覆盖不完全。边缘字符仍出现UNK
  • 词表大小爆炸。多语言模型要覆盖所有的字符,难以管理
  • 新 emoji ,在部署后会出现UNK

Unicode字符,不管是中文、日文、emoji还是生僻符号,最终都能编码成1~4个UTF‑8字节。而字节总共只有256种可能,全部在初始词表里。

BBPE就是在BPE的基础上,把初始词表换成了256字节值。实现了零UNK。

比如说同样语义的问候语,英文hello占 5 个 UTF‑8 字节,中文你好占 6 个 UTF‑8 字节,阿拉伯语问候则要 10 个 UTF‑8 字节,BBPE 会直接在这些原始字节之上执行 BPE 合并操作。

1.4 WordPiece

BPE、BBPE都是按共现频次选择子词对进行合并,而WordPiece走了另一条路线:它不再看单纯出现次数,而是以最大化训练集语言模型似然概率作为合并判断标准,由Google提出,最知名使用者就是BERT系列编码器模型。

WordPiece使用打分公式:
score(A,B)=freq(AB)freq(A)×freq(B)score(A,B)=\frac{freq(AB)}{freq(A) \times freq(B)}score(A,B)=freq(A)×freq(B)freq(AB)

分子越大(AB 常见),分母越小(A、B 单独罕见),说明合并这对更能减少不确定性。

1.5 SentencePiece

BPE / WordPiece 需要先按空格/标点预切分文本(预分词步骤),这一步依赖语言规则:中文无空格。

SentencePiece的做法是把空格替换为特殊字符 ▁(U+2581),文本变成纯字符流。算法直接在原始 Unicode 上运行,不需要任何语言特定预处理。比如输入“我 like 大模型”,子词切分结果示例:["我", "▁like", "▁大", "模型"]

1.6 Unigram

BPE 从小词表出发,不断合并(自下而上);Unigram 从大词表出发,不断剪枝(自上而下)

核心思想是优先删掉那些删掉之后,整体效果几乎没怎么变坏的子词。

1.7 分词算法横向对比小结

bbpe分词方法现在是主流。

对比维度BPE(Sennrich原版)BBPE(字节级BPE)WordPieceUnigram
初始词表字符256字节字符大词表剪枝
合并策略频次最大频次最大似然最大EM + 剪枝
UNK风险极低(存在UNK)零UNK极低极低(存在UNK)
分词确定性确定确定确定可概率化
多语言友好

第二章:KV Cache——vLLM高吞吐背后的底层根基

2.1 推理性能的关键指标

指标全称含义参考标准/说明
TTFTTime to First Token从发送请求到收到第一个token的时间< 500ms 为优秀,影响用户体验感知
TPOTTime Per Output TokenDecode阶段每生成一个token的平均耗时< 50ms 流畅体验
Throughput系统吞吐量单位时间内处理的总token数(所有并发请求之和)高并发部署核心指标,单位:token/s
MFUModel FLOPs UtilizationGPU实际算力利用率 vs 峰值算力Decode通常 < 20%,Prefill可达40%+

核心矛盾:低延迟 / 高吞吐 / 低显存三者难以同时最优 —— 推理优化的本质是在三角约束中找最佳平衡点。

2.2 LLM自回归推理两个阶段:Prefill 和 Decode

推理的流程是这样:

Prefill 阶段:处理 prompt
输入:整段 prompt(n 个 token)一次性进入模型
计算:对所有 n 个位置并行做 Attention
输出:下一个 token 的概率分布
特点:算力密集(compute-bound)

Decode 阶段:逐 token 生成
输入:上一步生成的 1 个 token
计算:对当前位置做 Attention(看前面所有 token)
输出:下一个 token,循环到 EOS
特点:访存密集(memory-bound)

2.3 朴素推理的致命冗余:每一步重复计算K/V

朴素推理过程:

  1. 把已有的 t 个 token 重新输入模型
  2. 每一层都重新计算所有 t 个位置的 Q、K、V
  3. 做完整 Attention,取最后一个位置的输出

代价:前 t-1 个位置的 K、V 在上一步早就算过,这一步又算了一遍 → 纯粹浪费

2.4 KV Cache引入的问题

问题类型具体表现产生原因影响
显存压力巨大KV Cache占用随序列长度线性增长,长上下文/高并发场景下显存占用甚至超过模型权重每个请求都需要存储全部历史token的K、V向量显存成为推理瓶颈,限制并发数和上下文长度
内部碎片按max_seq_len预分配连续显存,但实际生成长度远小于最大值,大量空间闲置传统实现为每个请求预留最大长度的连续内存块显存利用率低,实际情况可能浪费一半的显存
调度低效(静态Batch)一批请求必须全部生成结束才统一释放KV缓存,短请求完成后原地等待长请求传统静态批处理同进同出的调度机制GPU算力大量空转闲置,吞吐量上不去

2.5 vLLM如何改造KV Cache:PagedAttention 分页注意力

复刻OS虚拟内存分页机制,不再连续分配一整块的显存。先把显存按size划分,在需要用到显存的时候按需分配显存块。

2.6 Continuous Batching 连续批处理

一个token生成后,立刻开始运行新的请求

2.7 本章小结

  • KV Cache解决自回归重复计算问题,是LLM推理基础;但原生连续分配+静态批处理带来严重碎片、低利用率、调度低效
  • PagedAttention借鉴OS虚拟内存分页:解决显存碎片、利用率、前缀缓存复用,拉高并发上限
  • Continuous Batching借鉴OS抢占式调度:解决GPU算力空转,拉高整体吞吐量

一句话分工:PagedAttention负责省显存,让更多请求装下;Continuous Batching负责吃满GPU,让请求跑得更快

第三章:vLLM实战——本地项目复现

3.1 环境搭建

我在autodl云平台租的5090显卡做的实验。用到社区镜像

3.2 吞吐量对比实验(bench_throughput.py)

[A] transformers 串行

逐条循环处理请求,每次只向模型输入 1 条 prompt,这条完整生成完毕之后,才处理下一条。

  • GPU 同一时刻只在计算 1 个请求,大部分算力处于空闲状态。
  • KV 缓存处理完一条就完整释放,没有批处理加速。
[B] transformers 静态 batch=24

把 50 条请求按每 24 条切分成若干个固定分组,一组一组送入model.generate()

  • 同一组最多 24 条同时推理;组内请求做 padding 补齐长度。
  • 必须等待本组内全部 24 条请求全部生成结束,才释放显存、再执行下一组
  • 只要组内存在生成较慢的长请求,其余已经完成的请求会占用位置和 KV 缓存,GPU 算力空等,产生尾部算力空洞。
  • batch 开到 24,已经充分利用 32GB 显存,相比串行有巨大提升,但依然受静态批处理机制限制。
[C] vLLM 批处理(Continuous Batching + PagedAttention)

把全部 50 条请求一次性提交给 vLLM。

  1. 连续批处理(Continuous Batching):不以整组请求为调度单位,以单步 token 迭代做调度。某条请求生成完成,立刻将它剔除,马上补入排队的新请求,不需要等待一整批全部跑完。
  2. PagedAttention 分页 KV 缓存:KV 缓存拆成独立小块 block 管理,请求结束立刻回收 block,消除内存碎片,显存利用率大幅提升。
  • 内部动态调整同时活跃的请求数量,充分打满 GPU 算力。
结果对比
模式总耗时QPS(请求/秒)tokens/s相对 vLLM
[A] transformers 串行43.10s1.16870.02×
[B] transformers batch=244.76s10.517750.18×
[C] vLLM 批处理0.84s59.1941341.00×

3.3 约束解码实战:四大模式逐个拆解

3.3.1 guided_choice 枚举约束

每步解码时屏蔽非法 token 的 logits
输出了“查”之后,就只能输出“股价”、“财报”、“新闻”了。

INTENT_CHOICES=["查股价","查财报","查新闻","对比分析","其他"]defrun_with_guided_choice(user_msg:str)->tuple[str,float]:"""guided_choice 约束:底层 FSM 屏蔽非法 token"""t0=time.time()resp=client.chat.completions.create(model=MODEL,messages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":user_msg},],temperature=0,max_tokens=10,extra_body={"guided_choice":INTENT_CHOICES},# ★ 关键参数)returnresp.choices[0].message.content.strip(),time.time()-t0

运行结果

指标 裸 prompt guided_choice ---------------------------------------------------------------------- 输出合法(在枚举内) 248/257 (96%) 257/257 (100%) 预测正确 55/257 (21%) 57/257 (22%) 平均延迟(秒) 0.041 0.042
3.3.2 guided_regex 正则约束
DATE_REGEX=r"\d{4}-\d{2}-\d{2}"defrun_generate(system:str,user:str,regex:str|None=None)->tuple[str,float]:t0=time.time()extra={"guided_regex":regex}ifregexelse{}resp=client.chat.completions.create(model=MODEL,messages=[{"role":"system","content":system},{"role":"user","content":user},],temperature=0,max_tokens=30,extra_body=extra,)returnresp.choices[0].message.content.strip(),time.time()-t0

运行结果:

====================================================================== 输入 裸 prompt guided_regex --------------------------------------------------------------------------- 2024年5月12日 ✓ 2024-05-12 ✓ 2024-05-12 2023/12/1 下午开会 ✗ 2023-12-01 14:00:00 ✓ 2023-12-01 三月三号我去北京 ✓ 2017-03-03 ✓ 2017-03-03 2024.11.30 是截止日期 ✓ 2024-11-30 ✓ 2024-11-30 明天(假设今天是2026-05-11) ✓ 2027-04-12 ✓ 2027-04-12 2024 年 10 月的第一天 ✓ 2024-10-01 ✓ 2024-10-01 --------------------------------------------------------------------------- 格式合法率:裸 prompt 5/6 (83%) | guided_regex 6/6 (100%)
3.3.3 response_format JSON 约束

保证输出json格式

defrun(user:str,mode:str)->tuple[str,float]:"""mode: 'raw' | 'json_object'"""kwargs={}ifmode=="json_object":kwargs["response_format"]={"type":"json_object"}t0=time.time()resp=client.chat.completions.create(model=MODEL,messages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":user},],temperature=0,max_tokens=150,**kwargs,)returnresp.choices[0].message.content.strip(),time.time()-t0

运行结果:

=========================================================================== 200 条测试结果 =========================================================================== 指标 裸 prompt response_format ------------------------------------------------------------ 合法 JSON 200/200 (100%) 200/200 (100%) 有 sentiment 字段 200/200 (100%) 200/200 (100%) sentiment 值合法 200/200 (100%) 200/200 (100%) 有 confidence 字段 200/200 (100%) 200/200 (100%) 有 keywords 字段 200/200 (100%) 200/200 (100%) ===========================================================================

裸 prompt也是100%,但是依靠模型自觉

3.3.3 guided_json JSON Schema约束

比json约束更定制化,不光保证json格式,还能约束取值范围等内容

INTENT_SCHEMA={"type":"object","properties":{"company":{"type":"string","description":"公司全称,如 招商银行、贵州茅台",},"year":{"type":"integer","minimum":2015,"maximum":2025,},"metric":{"type":"string","enum":["营收","净利润","ROE","毛利率","总资产","经营现金流"],},},"required":["company","year","metric"],"additionalProperties":False,}defrun_generate(user_msg:str,mode:str)->tuple[str,float]:"""mode: 'raw' | 'guided_json' | 'response_format'"""extra={}kwargs={}ifmode=="guided_json":extra={"guided_json":INTENT_SCHEMA}elifmode=="response_format":kwargs={"response_format":{"type":"json_object"}}t0=time.time()resp=client.chat.completions.create(model=MODEL,messages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":user_msg},],temperature=0,max_tokens=120,extra_body=extra,**kwargs,)returnresp.choices[0].message.content.strip(),time.time()-t0

运行结果

指标 裸 prompt response_format guided_json ------------------------------------------------------------------------------ 合法 JSON 9/9 (100%) 9/9 (100%) 9/9 (100%) 字段齐全 9/9 (100%) 9/9 (100%) 9/9 (100%) year 在 2015~2025 8/9 (89%) 8/9 (89%) 9/9 (100%) metric 在枚举内 9/9 (100%) 9/9 (100%) 9/9 (100%) jsonschema 完全通过 8/9 (89%) 8/9 (89%) 9/9 (100%) ============================================================================== 结论: response_format 只保证是 JSON,不保证字段名、类型、枚举正确 guided_json 是唯一 100% 保证 schema 合法的方式 ==============================================================================

vllm在function call的例子里很有用,可以做到约束模型给出合规的json输出

3.4 约束解码原理简述:FSM有限状态机

  • 在每一步token采样时,用FSM过滤掉不符合规则的候选token
  • 从生成源头杜绝非法输出
  • 首次构建Schema有FSM初始化开销,缓存后速度恢复正常

第四章:总结

本文从分词器底层讲起,深入KV Cache与vLLM两大核心优化技术,并通过本地实验复现,验证了vLLM的两大工程价值:

第一,极致的推理吞吐性能。
PagedAttention借鉴操作系统虚拟内存分页思想,将KV Cache切分为离散的内存块,按需分配、即时回收,大幅缓解传统连续内存分配带来的显存碎片问题;Continuous Batching则以单步token迭代为调度单位,完成的请求立刻退出、新请求随时补入,避免静态批处理的算力空转。
本地实测数据显示,在Qwen2-0.5B模型、50条请求的场景下,vLLM的吞吐速度达到原生transformers串行的47倍,是transformers静态batch=24的5.3倍,充分体现了两项技术协同带来的性能收益。

第二,可靠的约束解码能力。
vLLM支持枚举、正则、JSON、JSON Schema等多种约束模式,通过FSM有限状态机在token生成的每一步屏蔽非法候选,从源头保证输出格式合规。
实验表明,裸prompt和OpenAI标准的response_format虽然能保证JSON语法合法,但在字段取值范围、枚举约束等细节上仍有89%左右的通过率;而guided_json可以做到100%符合Schema定义。对于Agent工具调用、结构化数据抽取等对输出格式有严格要求的场景,这是公有API无法提供的工程价值。

最后,回到一个实际问题:要不要自建vLLM?

  • 如果你的业务有高并发、大批量、数据敏感、强结构化输出的需求,vLLM是非常值得投入的方案;
  • 如果只是小流量原型验证、追求快速上线、不需要强格式约束,公有API依然是更省事的选择。

vLLM不是银弹,但它在"性能"和"可控性"两个维度上,给了开发者一个非常有竞争力的自建推理选项。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询