Files
vLLM/vllm/v1/watermarking/spec_decode.py
T
7ee8a6dd01 [watermarking] Dual-key gumbel-max watermarking for speculative decoding support (#56122)
Signed-off-by: Raphael Rialland <raphael.rialland@mistral.ai>
Signed-off-by: Simon Veitner <sveitner@redhat.com>
Signed-off-by: Tomas Ruiz <tomas.ruiz.te@gmail.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Simon Veitner <sveitner@redhat.com>
Co-authored-by: Tomas Ruiz <tomas.ruiz.te@gmail.com>
2026-09-12 18:42:01 +00:00

154 lines
5.0 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import torch
from vllm.config.watermarking import WatermarkConfig
from vllm.v1.watermarking.factory import create_watermarker
from vllm.v1.watermarking.gumbel import GumbelWatermarker
from vllm.v1.watermarking.prfs import PhiloxPRF
from vllm.v1.watermarking.watermarker import (
SupportsSpeculativeDecoding,
Watermarker,
)
from vllm.v1.worker.gpu.spec_decode.rejection_sampler_utils import rejection_sample
class DraftWatermarker:
def __init__(
self,
watermarker: Watermarker,
max_num_reqs: int,
device: torch.device,
) -> None:
self.watermarker = watermarker
self.contexts = torch.zeros(
max_num_reqs,
watermarker.context_width,
dtype=torch.int64,
device=device,
)
self.enabled = torch.zeros(max_num_reqs, dtype=torch.bool, device=device)
def prepare(self, contexts: torch.Tensor, enabled: torch.Tensor) -> None:
num_reqs = contexts.shape[0]
self.contexts[:num_reqs].copy_(contexts)
self.enabled[:num_reqs].copy_(enabled)
def sample(
self,
logits: torch.Tensor,
ordinary_sampled: torch.Tensor,
idx_mapping: torch.Tensor,
temperature: torch.Tensor,
) -> torch.Tensor:
request_temperatures = temperature[idx_mapping]
processed_logits = logits / torch.where(
request_temperatures == 0, 1, request_temperatures
).unsqueeze(-1)
watermarked = self.watermarker.sample(
processed_logits,
self.contexts[: logits.shape[0]],
).token_ids
enabled = self.enabled[: logits.shape[0]] & (request_temperatures != 0)
sampled = torch.where(enabled, watermarked, ordinary_sampled)
contexts = self.contexts[: logits.shape[0]]
contexts.copy_(torch.cat((contexts[:, 1:], sampled.unsqueeze(-1)), dim=-1))
return sampled
def create_speculative_target_watermarker(watermarker: Watermarker) -> Watermarker:
if isinstance(watermarker, SupportsSpeculativeDecoding):
return watermarker.target_watermarker
return watermarker
def create_speculative_draft_watermarker(
watermarker: Watermarker,
max_num_reqs: int,
device: torch.device,
allow_target_only: bool,
) -> DraftWatermarker | None:
if isinstance(watermarker, SupportsSpeculativeDecoding):
return DraftWatermarker(watermarker.draft_watermarker, max_num_reqs, device)
if allow_target_only:
return None
raise ValueError(
f"The {type(watermarker).__name__} watermarking algorithm does not support "
"speculative decoding. Set allow_target_only_watermarking=true to "
"leave draft tokens unwatermarked."
)
def _philox_key(watermarker: Watermarker) -> int:
if not (
isinstance(watermarker, GumbelWatermarker)
and type(watermarker.prf) is PhiloxPRF
):
raise NotImplementedError(
"In-kernel watermarked recovery supports only the Philox PRF "
"GumbelWatermarker"
)
return watermarker.prf.key
def _resolve_watermark_key(watermarker: Watermarker) -> int:
"""Return the Philox recovery key."""
if isinstance(watermarker, SupportsSpeculativeDecoding):
raise ValueError(
f"{type(watermarker).__name__} keys the target role separately from "
"the draft. Pass create_speculative_target_watermarker(watermarker) "
"so the recovery draw carries the target's key."
)
return _philox_key(watermarker)
def speculative_target_watermark_key(
watermark_config: WatermarkConfig | None,
) -> int | None:
"""Resolve the resample kernel's Philox key from configuration."""
if watermark_config is None:
return None
return _resolve_watermark_key(
create_speculative_target_watermarker(create_watermarker(watermark_config))
)
def watermarked_rejection_sample(
target_logits: torch.Tensor,
draft_logits: torch.Tensor | None,
draft_sampled: torch.Tensor,
cu_num_logits: torch.Tensor,
pos: torch.Tensor,
idx_mapping: torch.Tensor,
expanded_idx_mapping: torch.Tensor,
expanded_local_pos: torch.Tensor,
temperature: torch.Tensor,
seed: torch.Tensor,
num_speculative_steps: int,
contexts: torch.Tensor,
watermarking: torch.Tensor,
watermarker: Watermarker,
use_fp64: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Use the target key for rejection recovery and bonus tokens."""
assert contexts.shape == (target_logits.shape[0], watermarker.context_width)
return rejection_sample(
target_logits,
draft_logits,
draft_sampled,
cu_num_logits,
pos,
idx_mapping,
expanded_idx_mapping,
expanded_local_pos,
temperature,
seed,
num_speculative_steps,
use_fp64=use_fp64,
contexts=contexts,
watermarking=watermarking,
watermark_key=_resolve_watermark_key(watermarker),
)