| 1 | # Text watermarking |
| 2 | |
| 3 | Text watermarking embeds a statistical signal in generated token choices. A |
| 4 | detector that knows the generation parameters can test for this signal without |
| 5 | access to the model weights. |
| 6 | |
| 7 | The design and scope are discussed in |
| 8 | [RFC #53916](https://github.com/vllm-project/vllm/issues/53916). |
| 9 | |
| 10 | ## Configuration |
| 11 | |
| 12 | Enable watermarking by configuring an algorithm and secret key at engine |
| 13 | startup: |
| 14 | |
| 15 | ```bash |
| 16 | vllm serve MODEL \ |
| 17 | --watermark-config '{"algorithm":"gumbel","key":42}' |
| 18 | ``` |
| 19 | |
| 20 | Watermarking is disabled when `--watermark-config` is omitted. Gumbel-max (see |
| 21 | `gumbel`) is the default algorithm within an enabled `WatermarkConfig`. When |
| 22 | watermarking is configured, it is enabled for requests by default. |
| 23 | |
| 24 | Requests can opt out without changing the engine-level algorithm or key: |
| 25 | |
| 26 | ```python |
| 27 | from vllm import SamplingParams |
| 28 | |
| 29 | sampling_params = SamplingParams(watermarking=False) |
| 30 | ``` |
| 31 | |
| 32 | The Python OpenAI-compatible APIs and the Rust chat/completions APIs accept the |
| 33 | same `watermarking: false` request field. The Rust token API accepts it in |
| 34 | `sampling_params`, and the Rust gRPC `GenerateRequest` accepts |
| 35 | the optional `watermarking` field. Omitting the field keeps watermarking enabled |
| 36 | when the engine has a watermark configuration. |
| 37 | |
| 38 | Deployments that require watermarking must restrict this field to trusted |
| 39 | callers, or strip and validate it at the ingress boundary, so untrusted clients |
| 40 | cannot opt out. |
| 41 | |
| 42 | `context_width` controls how many prior tokens seed each watermark decision |
| 43 | and defaults to 4. Larger values make the watermark less robust to |
| 44 | edits because an insertion, deletion, or substitution changes more subsequent |
| 45 | contexts. Values above 16 are allowed but emit a warning. |
| 46 | |
| 47 | `allow_target_only_watermarking` defaults to false and only has an effect when |
| 48 | speculative decoding is enabled. It permits speculative decoding with a |
| 49 | watermarking algorithm that does not support it natively, at the cost of |
| 50 | weaker detectability. See |
| 51 | [Speculative decoding](#speculative-decoding). |
| 52 | |
| 53 | ## Architecture |
| 54 | |
| 55 | `WatermarkConfig` selects an algorithm and PRF. Model Runner V2 constructs the |
| 56 | corresponding `Watermarker`, and `GPUWatermarkSampler` invokes it for the final |
| 57 | stochastic token selection after temperature, min-p, top-k, and top-p are |
| 58 | applied. A watermarker can either select a token directly or transform logits |
| 59 | and delegate to vLLM's random sampler. |
| 60 | |
| 61 | Detection is separate from generation. vLLM provides detector primitives for |
| 62 | the reference algorithms. `WatermarkDetector` consumes token IDs, so callers |
| 63 | remain responsible for using the tokenizer and watermark profile that match |
| 64 | generation. |
| 65 | |
| 66 | ## Speculative decoding |
| 67 | |
| 68 | Watermarking requires speculative decoding to use probabilistic draft sampling, |
| 69 | standard rejection sampling, and an autoregressive model-based method (`dspark`, |
| 70 | `eagle`, `eagle3`, or `mtp`). Parallel drafting is supported only by `dspark`. |
| 71 | |
| 72 | A watermarking algorithm without native speculative-decoding support is |
| 73 | rejected before model loading. Set |
| 74 | `"allow_target_only_watermarking": true` to allow it: accepted draft tokens are |
| 75 | not watermarked, while target-side rejection recovery and bonus sampling remain |
| 76 | watermarked. The watermark signal is diluted in proportion to the share of |
| 77 | output tokens supplied by accepted drafts; rejected drafts do not dilute it |
| 78 | because their recovery tokens are watermarked. |
| 79 | |
| 80 | For `dual_key_gumbel`, `alpha` has no effect under speculative decoding. The |
| 81 | speculative protocol selects the key for each token instead. |
| 82 | |
| 83 | ## Context deduplication |
| 84 | |
| 85 | When a token context is repeated, generation can use ordinary sampling for that |
| 86 | occurrence. Reusing the repeated context leads to a bias over the sequence, as |
| 87 | certain token choices would be correlated. Ordinary sampling at these positions |
| 88 | allows for single-sequence non-distortion (see section G.3 of the |
| 89 | [SynthID-Text supplementary materials](https://media.springernature.com/original/springer-static/esm/art%3A10.1038%2Fs41586-024-08025-4/MediaObjects/41586_2024_8025_MOESM1_ESM.pdf)). |
| 90 | This also applies to drafter watermarking during speculative decoding. |
| 91 | The detector independently deduplicates contexts so repeated keyed random |
| 92 | vectors are not treated as independent evidence, meaning that context |
| 93 | deduplication at generation time does not reduce the watermarking signal, unless |
| 94 | a user tampers with the output and removes the first occurrence while leaving an |
| 95 | unwatermarked repetition. |
| 96 | |
| 97 | `deduplicate_contexts` controls which history is searched for a repeated |
| 98 | context: |
| 99 | |
| 100 | - `"none"` disables context deduplication. |
| 101 | - `"single_turn"`, the default, searches the tokens generated for the current |
| 102 | request. This gives single-turn non-distortion. |
| 103 | - `"all"` also searches the prompt, which extends non-distortion across the |
| 104 | turns of a conversation. The first `context_width` generated tokens use |
| 105 | ordinary sampling, and subsequent tokens whose context already occurs in the |
| 106 | prompt are not watermarked. Previous turns of a session, or a prompt that |
| 107 | already contains the structure of the answer such as a tool result the model |
| 108 | extends, may therefore leave little of the answer watermarked. Given the |
| 109 | impact of `"all"` on both performance (sampling steps must scan over a |
| 110 | potentially large amount of previous context) and watermarking detectability |
| 111 | (we may significantly reduce the amount of watermarked tokens), you should |
| 112 | only use `"all"` when non-distortion across multiple turns is required. |
| 113 | |
| 114 | `deduplicate_contexts_max_history` limits the search to the most recent |
| 115 | positions and defaults to 8,192. Each position is compared over the |
| 116 | `context_width` tokens before it, so the window of tokens read is that much |
| 117 | longer. A smaller value reduces scanning cost but only provides the guarantee |
| 118 | within that window. Set it to `null` to search back to the start of the |
| 119 | generation for `"single_turn"`, or of the request for `"all"`; an unbounded |
| 120 | search costs more as the sequence grows. The setting has no effect when |
| 121 | `deduplicate_contexts` is `"none"`. Values below 1,024 emit a warning because a |
| 122 | short window can miss repetition loops whose contexts recur farther apart. |
| 123 | |
| 124 | For example, this checks prompt and completion history within the default |
| 125 | 8,192-position window: |
| 126 | |
| 127 | ```bash |
| 128 | vllm serve MODEL \ |
| 129 | --watermark-config \ |
| 130 | '{"algorithm":"gumbel","key":42,"deduplicate_contexts":"all"}' |
| 131 | ``` |
| 132 | |
| 133 | ## Algorithms |
| 134 | |
| 135 | ### Gumbel-max |
| 136 | |
| 137 | Gumbel-max derives a deterministic pseudorandom value from the key, prior token |
| 138 | context, and every candidate token, then uses the resulting Gumbel noise for |
| 139 | categorical sampling. See |
| 140 | [Aaronson's original presentation](https://simons.berkeley.edu/sites/default/files/2024-10/LLM24-2%20Slides%20-%20Scott%20Aaronson.pdf). |
| 141 | |
| 142 | Gumbel-max requires stochastic sampling. Greedy requests (`temperature=0`) |
| 143 | bypass watermarking and emit a warning once per worker. |
| 144 | |
| 145 | ### Dual-key Gumbel-max |
| 146 | |
| 147 | Dual-key Gumbel-max derives independent keys A and B from one configured master |
| 148 | key. During ordinary generation, each token uses key A with probability |
| 149 | `1 - alpha` and key B with probability `alpha`. The algorithm-specific `alpha` |
| 150 | parameter defaults to 0.1. Detection scores every token against both keys. |
| 151 | |
| 152 | The same two key streams support speculative decoding without changing its |
| 153 | acceptance rate. In this mode, the speculative protocol selects the key instead |
| 154 | of `alpha`: draft tokens use key A, while rejection recovery and bonus tokens |
| 155 | use key B. The ordinary target-to-draft probability-ratio test remains unchanged, |
| 156 | implementing [SynthID-Text Supplementary Algorithm |
| 157 | 6](https://media.springernature.com/original/springer-static/esm/art%3A10.1038%2Fs41586-024-08025-4/MediaObjects/41586_2024_8025_MOESM1_ESM.pdf). |
| 158 | |
| 159 | Select `dual_key_gumbel` together with probabilistic drafting: |
| 160 | |
| 161 | ```bash |
| 162 | vllm serve MODEL \ |
| 163 | --speculative-config \ |
| 164 | '{"method":"mtp","num_speculative_tokens":3,"draft_sample_method":"probabilistic"}' \ |
| 165 | --watermark-config '{"algorithm":"dual_key_gumbel","key":42,"alpha":0.1}' |
| 166 | ``` |
| 167 | |
| 168 | ### SynthID-Text |
| 169 | |
| 170 | [SynthID-Text](https://www.nature.com/articles/s41586-024-08025-4) is planned but |
| 171 | not currently implemented. |
| 172 | |
| 173 | ## Pseudorandom functions |
| 174 | |
| 175 | A watermark PRF turns the secret key, token context, and candidate token into |
| 176 | reproducible random values. Generation and detection must produce identical |
| 177 | values across devices and releases. The values should also be uniform and |
| 178 | independent enough for the sampling algorithm and detector statistics. PRF |
| 179 | selection is an advanced compatibility and performance setting; most users |
| 180 | should keep the default. |
| 181 | |
| 182 | Watermarked generation currently supports the `philox` PRF: |
| 183 | |
| 184 | - `philox` is based on the counter-based Philox4x32-10 |
| 185 | generator from the [Random123 paper](https://doi.org/10.1145/2063384.2063405). |
| 186 | It is parallel, vectorizes on accelerators, and avoids CPU transfers, but is |
| 187 | not a cryptographic PRF and does not provide key-recovery or forgery |
| 188 | resistance. vLLM versions its input mapping and provides compatibility |
| 189 | vectors so generation and detection remain interoperable. |
| 190 | |
| 191 | ## Detection |
| 192 | |
| 193 | The detector primitives operate on token IDs and do not require model weights: |
| 194 | |
| 195 | ```python |
| 196 | from transformers import AutoTokenizer |
| 197 | |
| 198 | from vllm.v1.watermarking import GumbelWatermarkDetector |
| 199 | |
| 200 | tokenizer = AutoTokenizer.from_pretrained(MODEL) |
| 201 | token_ids = tokenizer.encode(text, add_special_tokens=False) |
| 202 | result = GumbelWatermarkDetector(key=42, prf="philox").detect(token_ids) |
| 203 | print(result.p_value, result.is_watermarked) |
| 204 | ``` |
| 205 | |
| 206 | The detection configuration must match the generation configuration, including |
| 207 | the tokenizer, PRF, watermarking algorithm, algorithm-specific watermarking |
| 208 | configuration, and key. In practice, this information is often unavailable |
| 209 | when checking a piece of text. Deployments should therefore retain the set of |
| 210 | candidate configurations they have served, test the text against each |
| 211 | candidate, and correct for multiple testing, for example with a Bonferroni |
| 212 | correction to the resulting p-values. |
| 213 | |
| 214 | Gumbel-max detection scores repeated contexts once by default so identical PRF |
| 215 | random vectors are not treated as independent evidence. Keep |
| 216 | `deduplicate_contexts=True` unless the detector's calibration has been adjusted |
| 217 | for correlated scores. |
| 218 | |
| 219 | The reported p-value is calibrated under the assumption that scored PRF inputs |
| 220 | are independent. A deployment uses one fixed key, so repeated structures across |
| 221 | documents reuse the same PRF values and can make the realized false-positive |
| 222 | rate key-dependent even when contexts are deduplicated within each document. |
| 223 | Measure the false-positive rate on representative unwatermarked traffic with the |
| 224 | deployed key before relying on `is_watermarked` for decisions. |
| 225 | |
| 226 | A minimal HTTP detector is available in |
| 227 | `examples/basic/online_serving/watermark_detection_server.py`: |
| 228 | |
| 229 | ```bash |
| 230 | python examples/basic/online_serving/watermark_detection_server.py \ |
| 231 | --tokenizer MODEL --key 42 --prf philox |
| 232 | ``` |
| 233 | |
| 234 | ```bash |
| 235 | curl http://localhost:8000/detect \ |
| 236 | -H 'Content-Type: application/json' \ |
| 237 | -d '{"text":"Text to inspect"}' |
| 238 | ``` |
| 239 | |
| 240 | Scores and p-values expose information about the per-token watermark signal. |
| 241 | Repeated queries can use this information to construct text that imitates |
| 242 | watermarked output or to modify watermarked text so it is no longer detected. |
| 243 | |
| 244 | ## Limitations |
| 245 | |
| 246 | - Watermarking is currently available only with Model Runner V2. |
| 247 | - Not all watermarking algorithms have native speculative-decoding support. |
| 248 | - Beam search expands candidates from model log probabilities and does not apply |
| 249 | Gumbel-max watermarking. |
| 250 | - Models that replace the vLLM sampler with a custom sampler cannot use |
| 251 | configured watermarking. |