9 月 24 日,vLLM 发布博客《Watermarking in vLLM》,作者为 Mistral 的 Raphaël Rialland 与 Red Hat 的 Simon Veitner、Tomas Ruiz,把这套能力背后的原理与工程取舍一次性讲透。水印在 v0.30.0 中已成为一等能力,这篇博文解释了它为什么这样实现、代价在哪里,以及用户怎么用起来。

为什么文本水印难做

数字内容要建立可追溯与可问责,水印是常用手段:给作品嵌入一段关于来源的信息。图像和音频可以把水印做成微弱扰动,但文本是离散的,没法直接改字。可行的思路是不改文本本身,而是影响生成过程,让信号可检测、又不改变期望的输出分布。

博文列出了四条互相拉扯的要求:

要求含义主要难点
非失真不系统性偏爱某些词、文风或解法红绿名单等方法会扭曲输出分布
鲁棒抗删改、混编,甚至被另一个 LLM 改写改写会破坏被改动处附近的分数
快速生成与检测的延迟、显存开销极小高吞吐引擎对调度延迟和大词表尤其敏感
检测依赖最少检测时不需要创建时间、模型来源等额外信息可选算法空间被大幅压缩

元数据会被剥离且复制即失效,Unicode 替换容易被篡改,基于句长与用词的文本建模不可靠,锦标赛采样(Tournament Sampling)需要多轮处理、难以在不牺牲推理速度的前提下实现。最终他们选择利用语言模型本身的一个自然属性:随机性。

把随机数换成可复现的随机数

普通采样在每一步都取用并丢弃新的随机值,事后无从复原。水印改为从「近期上下文 + 候选 token」推导出可复现的带密钥值:上下文和被选中的 token 都留在输出里,持有密钥的检测方就能在生成后重建这些值,水印文本会系统性地与之对齐,信号随序列变长而累积。

关键是既要可复现,又不能改变分布。Gumbel-max 技巧正好满足:给每个候选 token 的对数概率加上独立的 Gumbel 噪声后取 argmax,得到的分布与普通随机采样完全一致,而且可以直接作用在 logits 上,省掉一次 softmax、便于并行。vLLM 的 Model Runner v2 标准采样路径本来就在用这套流程,所以水印可以复用同一条管线。

噪声的来源换成伪随机函数(PRF):同一密钥、同一输入永远得到同一输出,因而检测方可复现。参与计算的三样东西各有分工——token ID 让每个位置拿到不同噪声,默认最近 4 个 token 的上下文让噪声几乎每步都变,密钥则让没有它的人看不出任何规律。

检测:把分数加起来,对照 Gamma 分布

检测时不需要模型权重,也不需要 logits,只要密钥和 tokenizer。检测方把文本重新切成 token ID,用前文上下文和密钥复算每个位置的伪随机值:未加水印的文本里,观测到的 token 与该值无关,得分服从均值 1 的均匀分布;水印文本则系统性偏向得分更高的 token。

由于重复的上下文会复现同一组密钥值,检测方对每个上下文只算一次。未加水印时,单 token 得分服从指数分布,求和后服从 Gamma 分布,由此可以算出单侧 p 值——「未经水印的文本得到至少这么高分数的概率」。p 值很小,就是支持水印存在的证据。

几个实践上的边界:

  • 阈值在误报与漏报之间权衡,p < 0.01 时约有 1% 的未加水印序列被误判为阳性
  • 实际往往要同时试多个密钥、tokenizer 或水印配置,需要多重检验校正;候选越多阈值越严,检测力越弱
  • 证据靠长度与熵积累:创意写作在约 100 个 token 时接近 100% 检出率(1% 误报率下),MBPP 这类可预测代码在 400 token 时分别只有约 69%、49%、43%(对应 1、10、100 组候选)
  • 复制粘贴足够长的段落会保留证据;改动会破坏改动处附近的分数,但等这些 token 滑出后续上下文窗口,信号仍然完好

落地 vLLM:融合内核与按行掩码

实现最初来自 watermarking RFC,随后进入 Model Runner v2 的采样管线(PR #54053):GPUWatermarkSampler 连接到与算法绑定的 Watermarker,不同水印方案共享同一套请求与批次处理。

朴素写法会物化一个 [batch, vocabulary] 的噪声张量,带来可观的临时显存与访存压力。vLLM 把伪随机数生成、Gumbel 变换与 argmax 归约融合进单个 GPU 内核:Philox 每次调用产出 4 个值,内核因此可以一次处理 4 个连续 token ID。再用一个按行掩码,让同一个融合采样器对水印请求用带密钥噪声、对未水印请求或重复上下文用普通随机数。

性能上,Qwen3.5-27B + MTP-3、512 输出 token、单张 H100 的测试中,batch size 1–256 跨 8 个密钥的平均吞吐变化在 −1.1% 到 +2.0% 之间,没有显著变慢。

batch size未水印中位数(tok/s)水印中位数(tok/s)平均变化
1113.9113.9−0.23%
4420.5413.7−1.11%
321378.81407.9+2.03%
1281568.51574.5+0.36%
2561590.71586.5−0.58%

两个必须解决的坑

投机解码:草稿分布与目标分布各自被同一套水印保持,但两者的重叠会下降,接受率随之降低。vLLM 的解法是双密钥(PR #56122)——一个密钥负责被接受的草稿 token,另一个负责目标分布的残差与 bonus token,从而保住普通接受率,也让目标采样与被拒绝的草稿提议无关。代价是最终文本里两个密钥的水印混在一起,检测时必须同时用两把密钥打分再合并,信号被稀释,因此需要按「每个 token 更可能来自哪一方、携带多少信号」来校准权重。

输出多样性:单 token 非失真只保证每一步的分布,不保证整条序列。若某个上下文重复出现,密钥噪声也会重复,本该独立的 token 选择被关联起来,极端情况下会陷入 1 + 1 + 1 + 1 + … 的死循环,通常则表现为序列内多样性下降、退化行为变多、生成变长、质量下降。vLLM 用生成期上下文去重(PR #56233)处理:上下文重复时跳过水印,从而保住序列级非失真;尽管每步都要检查前文是否重复,Qwen3.5-27B 上端到端吞吐最多只降 0.19%。此外,双密钥的随机性本身也能缓解这一问题,因此在不开投机解码时也可以随机选密钥(见 TextSeal 一文的做法)。

质量侧的影响不大:Qwen3.5-27B 上,带双密钥水印与不带水印的 GSM8K 为 93.0% 对 94.2%,MBPP 为 79.2% 对 77.2%,IFEval 为 90.7% 对 91.9%,误差棒相互重叠。

怎么用

启动服务时打开 Gumbel-max 水印:

vllm serve mistralai/Mistral-7B-Instruct-v0.3 \
  --watermark-config '{"algorithm":"gumbel","key":42}'

仓库同时提供了一个最小的 HTTP 检测服务器示例,用对应的 tokenizer 与水印密钥校验文本,配置与检测细节见官方文档。

核心总结

  • 改生成过程而不是改文本:带密钥的 PRF 替换掉每步的随机数,Gumbel-max 保证加噪取 argmax 后的分布与普通采样完全一致
  • 检测只依赖密钥与 tokenizer:分数求和服从 Gamma 分布,可直接给出 p 值;证据靠长度与熵积累,短而可预测的输出信号弱
  • 性能代价可忽略:PRF、Gumbel、argmax 融合进单个 GPU 内核,吞吐变化落在 −1.1% 到 +2.0%
  • 投机解码用双密钥换接受率,代价是信号稀释,需按 token 来源加权合并分数
  • 上下文去重守住序列级非失真,端到端吞吐最多降 0.19%,避免重复上下文导致的退化循环

原文:Watermarking in vLLM(vLLM Blog,2026-09-24)