vLLM 支持无失真文本水印了:Gumbel-max 是怎么混进采样管线的
原文:vLLM Blog - Watermarking in vLLM(https://vllm.ai/blog/2026-09-24-watermarking-in-vllm)
给 LLM 输出加水印这件事,学术界喊了很久,真正落到主流推理引擎里的方案一直不多。2026 年 9 月 24 日,vLLM 官方博客宣布支持基于 Gumbel-max 算法的无失真文本水印,直接集成进 Model Runner v2 的采样管线,由 Mistral 与 Red Hat 的工程师合作实现,并通过多个 PR 融合了 GPU kernel、双键方案和上下文去重,兼容投机解码。这篇带你把它的原理和工程实现拆开看清楚。
一、为什么文本水印难做
图片、音频可以靠人眼听不出来的微小扰动夹带水印,但文本是离散的,改一个词就是另一个词。vLLM 的思路是反过来:不去扰动文本本身,而是影响生成过程,让这种影响事后可检测,却不改变模型输出的期望分布。
官方给可行方案列了四个要求:非失真(Non-distortion)、鲁棒性(Robustness)、速度(Speed)、检测依赖最小化(Minimal detection dependencies)。Gumbel-max 方案在前三条上都有干净的答案。
二、核心原理:Gumbel-max trick
模型给每个候选 token 一个概率。采样时,为每个候选独立抽一个均匀随机数,变换成 Gumbel 噪声,加到 log 概率上取 argmax:
一个很漂亮的结论是:这样选出来的 token,分布与按原始概率直接采样完全一致。事实上 vLLM 的标准采样路径本来就在用这套过程------水印要做的,只是把"真随机"换成"可复现的伪随机"。
用一段示意代码表达(下面是我自己写的演示版本,非官方实现,仅帮助理解):
python
import numpy as np
def gumbel_max_sample(logits, key, ctx, prf):
# logits: 候选 token 的原始 logit,不需要先过 softmax
n = len(logits)
# 用伪随机函数 PRF 为每个候选 token 生成均匀随机数
u = np.array([prf(key, ctx, i) for i in range(n)])
# 均匀随机数变换为 Gumbel 分布噪声
g = -np.log(-np.log(u))
# 加噪后取 argmax,等价于按模型概率采样
return int(np.argmax(logits + g))
参数说明:logits 不经过 softmax 直接加噪,避免额外的归一化开销且更易并行;key 是水印密钥;ctx 是最近的 token 上下文(默认取最后 4 个 token),保证几乎每一步的噪声都不同;prf 返回值在 (0,1) 之间均匀分布。函数返回选中 token 的下标。
关键的改造在这一行:独立的均匀随机数被替换为 PRF(伪随机函数)的输出,输入是密钥、token id 和最近上下文。三个成分各司其职------token id 让每个候选噪声不同,上下文让每步噪声不同,密钥让没有 key 的人无法预测这个模式。
于是非失真有了精确的保证:对密钥取期望,选中某个 token 的概率恰好等于模型给它的概率。水印既不偏爱某些词,也不改变写作风格。
而检测之所以可行,是因为同样的 key 和上下文能完整重放生成时的噪声序列。检测器拿回文本,重新计算每个位置的伪随机值,做统计检验即可,不需要访问模型本身。
三、工程落地:采样管线里的三件事
原理一句话,落地是另一回事。vLLM 的实现(初始 PR #54053)有三个值得注意的点。
第一,接口分层。初始实现把 GPUWatermarkSampler 接到一个算法特定的 Watermarker 抽象上,不同水印方案可以共享同一套请求与批处理逻辑。
第二,融合 GPU kernel。朴素实现要为一个 batch 物化形如 batch, vocab 的噪声张量,临时存储和显存流量都不小。vLLM 把伪随机数生成、Gumbel 变换和 argmax 归约融合成单个 GPU kernel,其中 Philox 生成器每次调用产出四个随机值,kernel 一次处理四个连续 token id。
第三,per-row mask。同一个融合采样器同时服务两类请求:水印请求走带密钥的噪声,普通请求或重复上下文走普通随机性,靠每行一个掩码区分。
四、与投机解码共存:双键方案
投机解码会让事情变复杂:draft 分布 propose token,target 分布决定接受与否。如果对两个分布施加同一个水印,各自分布虽然不变,但重叠度下降,接受率会被拉低。
vLLM 的解法是双键实现(PR #56122):一个 key 用于被接受的草稿 token,另一个 key 用于 target 残差和 bonus token。两个分布各自独立采样,接受率保持正常。
代价在检测端:最终文本里两种 key 水印的 token 混在一起,检测时必须用两个 key 分别打分再合并,信号会被稀释。合并权重可以校准------草稿 token 往往在"容易"的位置被接受,熵更低,携带的水印信号也更弱,权重就应相应调低。
五、质量与吞吐表现
官方博客给出的数据(以官方文档为准):Qwen3.5-27B 开启双键水印后,GSM8K 为 93.0%(无水印 94.2%)、MBPP 为 79.2%(无水印 77.2%)、IFEval 为 90.7%(无水印 91.9%),误差条相互重叠,质量没有系统性下降。
吞吐方面,Qwen3.5-27B 配 MTP-3、batch size 从 1 到 256,水印与无水印曲线基本重叠;八个 key 的平均匹配吞吐变化在 -1.1% 到 +2.0% 之间,没有明显减速。
需要提醒的是:单 token 层面的非失真不等于完整序列层面的多样性不受影响,官方博客在 "Maintaining output diversity" 一节专门讨论了这个限制及对策,细节以官方文档为准。
六、怎么用
启动服务时通过 watermark-config 开启:
vllm serve mistralai/Mistral-7B-Instruct-v0.3 \
--watermark-config '{"algorithm":"gumbel","key":42}'
参数解释:algorithm 指定水印算法为 gumbel,key 是整数形式的水印密钥,检测端必须持有相同的 key 和匹配的 tokenizer。vLLM 同时提供了一个最小 HTTP 检测服务示例,用于对文本做水印校验,配置与检测示例见官方水印文档(版本细节未验证最新版本)。
七、给开发者的三点启发
- 无失真不是"输出几乎不变",而是分布层面的严格等价,这是 Gumbel-max 方案最值得学的地方。
- 可检测性来自随机源可复现。PRF 把"随机"变成"带密钥的确定性",是整个方案从学术走向工程的那座桥。
- 水印放在推理引擎的采样层,而不是生成后的文本后处理,才可能做到无损、低开销且兼容投机解码。如果你在做推理服务或 LLM 应用平台,这个集成位置的选择值得参考。