[watermarking] Add DFlash support to dual-key gumbel-max watermarking (#59554)
Signed-off-by: Shern Shiou Tan <shernshiou@gmail.com> Signed-off-by: Simon Veitner <sveitner@redhat.com> Signed-off-by: Raphael Rialland <raphael.rialland@mistral.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Simon Veitner <sveitner@redhat.com> Co-authored-by: Raphael Rialland <raphael.rialland@mistral.ai> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Simon Veitner
Raphael Rialland
Lucas Wilkinson
parent
e711723352
commit
d92a4f25f2
@@ -125,8 +125,10 @@ generation.
|
||||
## Speculative decoding
|
||||
|
||||
Watermarking requires speculative decoding to use probabilistic draft sampling,
|
||||
standard rejection sampling, and an autoregressive model-based method (`dspark`,
|
||||
`eagle`, `eagle3`, or `mtp`). Parallel drafting is supported only by `dspark`.
|
||||
standard rejection sampling, and a model-based method (`dflash`, `dspark`,
|
||||
`eagle`, `eagle3`, or `mtp`). Parallel drafting is supported only by `dflash`
|
||||
and `dspark`. DFlash drafters with a candidate head (`DFlash2DraftModel`,
|
||||
`LiLiCorrDraftModel`) are not supported.
|
||||
|
||||
A watermarking algorithm without native speculative-decoding support is
|
||||
rejected before model loading. Set
|
||||
|
||||
+22
-8
@@ -3732,34 +3732,48 @@ def test_dual_key_gumbel_requires_probabilistic_drafting():
|
||||
config._check_supports_watermarking()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["eagle", "eagle3", "mtp"])
|
||||
def test_dual_key_gumbel_supports_probabilistic_speculative_decoding(method):
|
||||
@pytest.mark.parametrize(
|
||||
("method", "parallel_drafting"),
|
||||
[
|
||||
("eagle", False),
|
||||
("eagle3", False),
|
||||
("mtp", False),
|
||||
("dspark", True),
|
||||
("dflash", True),
|
||||
],
|
||||
)
|
||||
def test_dual_key_gumbel_supports_probabilistic_speculative_decoding(
|
||||
method, parallel_drafting
|
||||
):
|
||||
config = _watermarked_vllm_config()
|
||||
config.watermark_config = WatermarkConfig(algorithm="dual_key_gumbel", key=42)
|
||||
config.speculative_config = SimpleNamespace(
|
||||
method=method,
|
||||
draft_sample_method="probabilistic",
|
||||
rejection_sample_method="standard",
|
||||
parallel_drafting=False,
|
||||
parallel_drafting=parallel_drafting,
|
||||
)
|
||||
|
||||
config._check_supports_watermarking()
|
||||
|
||||
|
||||
def test_dual_key_gumbel_supports_dspark():
|
||||
@pytest.mark.parametrize("architecture", ["DFlash2DraftModel", "LiLiCorrDraftModel"])
|
||||
def test_dual_key_gumbel_rejects_dflash_candidate_drafts(architecture):
|
||||
config = _watermarked_vllm_config()
|
||||
config.watermark_config = WatermarkConfig(algorithm="dual_key_gumbel", key=42)
|
||||
config.speculative_config = SimpleNamespace(
|
||||
method="dspark",
|
||||
method="dflash",
|
||||
draft_sample_method="probabilistic",
|
||||
rejection_sample_method="standard",
|
||||
parallel_drafting=True,
|
||||
draft_model_config=SimpleNamespace(architectures=[architecture]),
|
||||
)
|
||||
|
||||
config._check_supports_watermarking()
|
||||
with pytest.raises(ValueError, match="candidate-head drafters"):
|
||||
config._check_supports_watermarking()
|
||||
|
||||
|
||||
def test_dual_key_gumbel_rejects_non_autoregressive_speculation():
|
||||
def test_dual_key_gumbel_rejects_unsupported_speculative_method():
|
||||
config = _watermarked_vllm_config()
|
||||
config.watermark_config = WatermarkConfig(algorithm="dual_key_gumbel", key=42)
|
||||
config.speculative_config = SimpleNamespace(
|
||||
@@ -3769,7 +3783,7 @@ def test_dual_key_gumbel_rejects_non_autoregressive_speculation():
|
||||
parallel_drafting=False,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="autoregressive model-based"):
|
||||
with pytest.raises(ValueError, match="Watermarking supports only"):
|
||||
config._check_supports_watermarking()
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from vllm.v1.worker.gpu.sample.watermark import (
|
||||
philox_gumbel_sample,
|
||||
repeated_context_mask,
|
||||
)
|
||||
from vllm.v1.worker.gpu.spec_decode.dflash.speculator import DFlashSpeculator
|
||||
from vllm.v1.worker.gpu.spec_decode.dspark.speculator import DSparkSpeculator
|
||||
from vllm.v1.worker.gpu.spec_decode.speculator import DraftModelSpeculator
|
||||
|
||||
@@ -254,8 +255,8 @@ def test_speculative_decoding_uses_fixed_dual_key_roles():
|
||||
assert draft is not None
|
||||
assert draft.watermarker.prf.key == derive_watermark_key(42, b"key_a")
|
||||
assert draft.num_speculative_steps == 3
|
||||
assert draft.deduplicate_contexts == "all"
|
||||
assert draft.deduplicate_contexts_max_history is None
|
||||
assert draft.state.deduplicate_contexts == "all"
|
||||
assert draft.state.deduplicate_contexts_max_history is None
|
||||
|
||||
|
||||
def test_recovery_key_rejects_the_unsplit_dual_key_watermarker():
|
||||
@@ -1324,8 +1325,8 @@ def test_draft_sampler_uses_draft_key_and_advances_context(monkeypatch):
|
||||
deduplicate_contexts="none",
|
||||
deduplicate_contexts_max_history=None,
|
||||
)
|
||||
draft_watermarker.contexts.copy_(torch.tensor([[1, 2], [3, 4]]))
|
||||
draft_watermarker.enabled = torch.tensor([True, False])
|
||||
draft_watermarker.state.contexts.copy_(torch.tensor([[1, 2], [3, 4]]))
|
||||
draft_watermarker.state.enabled = torch.tensor([True, False])
|
||||
speculator.draft_watermarker = draft_watermarker
|
||||
monkeypatch.setattr(
|
||||
"vllm.v1.watermarking.watermarker.gumbel_sample",
|
||||
@@ -1343,7 +1344,7 @@ def test_draft_sampler_uses_draft_key_and_advances_context(monkeypatch):
|
||||
)
|
||||
|
||||
assert torch.equal(sampled, torch.tensor([7, 4]))
|
||||
assert torch.equal(draft_watermarker.contexts, torch.tensor([[2, 7], [4, 4]]))
|
||||
assert torch.equal(draft_watermarker.state.contexts, torch.tensor([[2, 7], [4, 4]]))
|
||||
|
||||
|
||||
def test_draft_sampler_uses_ordinary_samples_for_repeated_contexts(monkeypatch):
|
||||
@@ -1602,11 +1603,12 @@ def test_draft_watermarker_deduplicates_across_a_block_on_cuda(
|
||||
[True, True],
|
||||
]
|
||||
assert torch.equal(
|
||||
draft_watermarker.prior_contexts[:num_reqs].cpu(),
|
||||
draft_watermarker.state.prior_contexts[:num_reqs].cpu(),
|
||||
torch.tensor([[[5, 6], [6, 6], [6, 6]], [[6, 3], [3, 6], [6, 6]]]),
|
||||
)
|
||||
assert torch.equal(
|
||||
draft_watermarker.contexts[:num_reqs].cpu(), torch.tensor([[6, 6], [6, 6]])
|
||||
draft_watermarker.state.contexts[:num_reqs].cpu(),
|
||||
torch.tensor([[6, 6], [6, 6]]),
|
||||
)
|
||||
for step, logits in enumerate(step_logits):
|
||||
# The 0-d draft step selects the logits cache column.
|
||||
@@ -1661,6 +1663,154 @@ def test_draft_watermarker_cuda_graph_reads_current_prompt_lens():
|
||||
assert mask.item()
|
||||
|
||||
|
||||
def test_dflash_draft_sampler_watermarks_drafts_in_step_order(monkeypatch):
|
||||
class NextTokenWatermarker:
|
||||
context_width = 2
|
||||
|
||||
@staticmethod
|
||||
def _try_sample_block(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def sample(logits, contexts, random_sampler=None, skip_mask=None):
|
||||
watermarked = contexts[:, -1] + 1
|
||||
return WatermarkSample(
|
||||
torch.where(skip_mask, random_sampler(logits), watermarked), logits
|
||||
)
|
||||
|
||||
def ordinary_sample(logits, idx_mapping, temperature, seed, pos, **kwargs):
|
||||
assert kwargs["use_fp64"]
|
||||
cache, col = kwargs["logits_cache"], kwargs["logits_cache_col"]
|
||||
cache[idx_mapping, col] = kwargs["logits_cache_source"]
|
||||
return seed[idx_mapping] + pos
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.v1.watermarking.watermarker.gumbel_sample", ordinary_sample
|
||||
)
|
||||
num_reqs, num_steps, vocab_size = 3, 2, 4
|
||||
draft_watermarker = DraftWatermarker(
|
||||
NextTokenWatermarker(),
|
||||
max_num_reqs=8,
|
||||
device=torch.device("cpu"),
|
||||
num_speculative_steps=num_steps,
|
||||
deduplicate_contexts="none",
|
||||
deduplicate_contexts_max_history=None,
|
||||
)
|
||||
# Batch rows hold request slots 2, 0, 1. Slot 0 is greedy and row 2 opted out.
|
||||
draft_watermarker.state.contexts[:num_reqs] = torch.tensor([[1, 2], [3, 4], [5, 6]])
|
||||
draft_watermarker.state.enabled[:num_reqs] = torch.tensor([True, True, False])
|
||||
speculator = object.__new__(DFlashSpeculator)
|
||||
speculator.model = SimpleNamespace(
|
||||
compute_logits=lambda hidden_states: hidden_states
|
||||
)
|
||||
speculator.draft_watermarker = draft_watermarker
|
||||
speculator.use_fp64_gumbel = True
|
||||
speculator.acceptance_estimator = None
|
||||
batch_slots = torch.tensor([2, 0, 1])
|
||||
logits = torch.arange(num_reqs * num_steps * vocab_size, dtype=torch.float32)
|
||||
draft_logits = torch.zeros(num_reqs, num_steps, vocab_size)
|
||||
|
||||
sampled = speculator.sample_draft(
|
||||
hidden_states=logits.view(-1, vocab_size),
|
||||
sample_src_positions=torch.arange(num_reqs * num_steps),
|
||||
idx_mapping=batch_slots.repeat_interleave(num_steps),
|
||||
temperature=torch.tensor([0.0, 1.0, 0.5]),
|
||||
seeds=torch.tensor([100, 200, 300]),
|
||||
draft_step=torch.arange(num_steps).repeat(num_reqs),
|
||||
draft_logits=draft_logits,
|
||||
num_steps=num_steps,
|
||||
)
|
||||
|
||||
assert torch.equal(sampled, torch.tensor([3, 4, 102, 103, 204, 205]))
|
||||
assert torch.equal(draft_logits[batch_slots], logits.view(num_reqs, num_steps, -1))
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(), reason="requires a CUDA-like accelerator"
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
|
||||
@pytest.mark.parametrize("deduplicate_contexts", ["none", "single_turn", "all"])
|
||||
def test_dflash_draft_sampler_matches_step_loop_on_cuda(deduplicate_contexts, dtype):
|
||||
"""The fused CUDA `sample_block` is bit-exact with sampling step by step.
|
||||
|
||||
Covers repeated contexts, a NaN logit, and greedy, opted-out and padding
|
||||
rows; padding rows are discarded, so only valid rows are compared.
|
||||
"""
|
||||
num_reqs, num_steps, vocab_size, max_num_reqs = 4, 5, 1500, 6
|
||||
generator = torch.Generator().manual_seed(0)
|
||||
logits = torch.randn(num_reqs, num_steps, vocab_size, generator=generator) * 3
|
||||
logits[..., :3] += 6
|
||||
logits[0, 1, 5] = float("nan")
|
||||
logits = logits.to(dtype).cuda()
|
||||
idx_mapping = torch.tensor([4, 2, 0, -1], dtype=torch.int32)
|
||||
valid = (idx_mapping >= 0).cuda()
|
||||
idx_mapping = idx_mapping.repeat_interleave(num_steps).cuda()
|
||||
temperature = torch.tensor([0.7, 1.0, 0.0, 1.3, 0.6, 0.9]).cuda()
|
||||
seeds = torch.arange(max_num_reqs).cuda() * 1000
|
||||
pos = (torch.arange(num_steps).repeat(num_reqs) + 20).cuda()
|
||||
draft_step = torch.arange(num_steps).repeat(num_reqs).cuda()
|
||||
contexts = torch.randint(0, 3, (num_reqs, 2), generator=generator)
|
||||
contexts[0, 0] = -1
|
||||
all_token_ids = torch.randint(0, 3, (max_num_reqs, 32), generator=generator)
|
||||
|
||||
def run(fused: bool):
|
||||
draft_watermarker = DraftWatermarker(
|
||||
GumbelWatermarker(key=42, context_width=2),
|
||||
max_num_reqs=max_num_reqs,
|
||||
device=torch.device("cuda"),
|
||||
num_speculative_steps=num_steps,
|
||||
deduplicate_contexts=deduplicate_contexts,
|
||||
deduplicate_contexts_max_history=None,
|
||||
)
|
||||
draft_watermarker.prepare(
|
||||
contexts=contexts.cuda(),
|
||||
enabled=torch.tensor([True, False, True, True]).cuda(),
|
||||
all_token_ids=all_token_ids.cuda(),
|
||||
prompt_lens=torch.full((max_num_reqs,), 4).cuda(),
|
||||
total_lens=torch.full((max_num_reqs,), 24).cuda(),
|
||||
)
|
||||
draft_logits = torch.zeros(max_num_reqs, num_steps, vocab_size, dtype=dtype)
|
||||
draft_logits = draft_logits.cuda()
|
||||
args = (temperature, seeds)
|
||||
if fused:
|
||||
sampled = draft_watermarker.sample_block(
|
||||
logits.view(-1, vocab_size),
|
||||
idx_mapping,
|
||||
*args,
|
||||
pos,
|
||||
logits_cache=draft_logits,
|
||||
logits_cache_col=draft_step,
|
||||
)
|
||||
else:
|
||||
by_step = [t.view(num_reqs, num_steps) for t in (idx_mapping, pos)]
|
||||
steps = draft_step.view(num_reqs, num_steps)
|
||||
sampled = torch.stack(
|
||||
[
|
||||
draft_watermarker.sample(
|
||||
logits[:, step],
|
||||
by_step[0][:, step],
|
||||
*args,
|
||||
by_step[1][:, step],
|
||||
logits_cache=draft_logits,
|
||||
logits_cache_col=steps[:, step],
|
||||
apply_temperature=True,
|
||||
is_drafting=True,
|
||||
)
|
||||
for step in range(num_steps)
|
||||
],
|
||||
dim=1,
|
||||
).view(-1)
|
||||
return (
|
||||
sampled.view(num_reqs, num_steps)[valid],
|
||||
draft_watermarker.state.contexts[:num_reqs][valid],
|
||||
draft_watermarker.state.prior_contexts,
|
||||
draft_logits,
|
||||
)
|
||||
|
||||
for fused, reference in zip(run(fused=True), run(fused=False)):
|
||||
torch.testing.assert_close(fused, reference, rtol=0, atol=0, equal_nan=True)
|
||||
|
||||
|
||||
def test_dspark_reduced_vocab_draft_sampler_applies_watermarking(monkeypatch):
|
||||
speculator = object.__new__(DSparkSpeculator)
|
||||
speculator.draft_logits = torch.zeros(2, 1, 8)
|
||||
|
||||
+15
-4
@@ -1318,15 +1318,26 @@ class VllmConfig:
|
||||
)
|
||||
if (
|
||||
speculative_config.parallel_drafting
|
||||
and speculative_config.method != "dspark"
|
||||
and speculative_config.method not in ("dflash", "dspark")
|
||||
):
|
||||
raise ValueError(
|
||||
"Parallel speculative drafting is not supported with watermarking."
|
||||
)
|
||||
if speculative_config.method not in ("dspark", "eagle", "eagle3", "mtp"):
|
||||
if speculative_config.method not in (
|
||||
"dflash",
|
||||
"dspark",
|
||||
"eagle",
|
||||
"eagle3",
|
||||
"mtp",
|
||||
):
|
||||
raise ValueError(
|
||||
"Watermarking supports only autoregressive model-based "
|
||||
"speculative decoding."
|
||||
"Watermarking supports only dflash, dspark, eagle, eagle3 and "
|
||||
"mtp speculative decoding."
|
||||
)
|
||||
if self._is_dflash_candidate_draft():
|
||||
raise ValueError(
|
||||
"Watermarking does not support DFlash2 and LiLiCorr "
|
||||
"candidate-head drafters yet."
|
||||
)
|
||||
if (
|
||||
not watermark_config.allow_target_only_watermarking
|
||||
|
||||
@@ -14,6 +14,7 @@ from vllm.v1.watermarking.detector import (
|
||||
)
|
||||
from vllm.v1.watermarking.prfs import PhiloxPRF, WatermarkPRF, create_prf
|
||||
from vllm.v1.watermarking.watermarker import (
|
||||
DraftContextState,
|
||||
RandomSampler,
|
||||
SupportsSpeculativeDecoding,
|
||||
Watermarker,
|
||||
@@ -101,6 +102,42 @@ class GumbelWatermarker(Watermarker):
|
||||
)
|
||||
return WatermarkSample(token_ids, logits)
|
||||
|
||||
def _try_sample_block(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
num_steps: int,
|
||||
random_sampler: RandomSampler,
|
||||
state: DraftContextState,
|
||||
) -> torch.Tensor | None:
|
||||
if type(self.prf) is not PhiloxPRF or logits.device.type != "cuda":
|
||||
return None
|
||||
|
||||
from vllm.v1.worker.gpu.sample.watermark import draft_philox_gumbel_sample
|
||||
|
||||
assert random_sampler.logits_cache is not None
|
||||
assert random_sampler.logits_cache_col is not None
|
||||
return draft_philox_gumbel_sample(
|
||||
logits,
|
||||
state.contexts,
|
||||
self.prf.key,
|
||||
num_steps=num_steps,
|
||||
expanded_idx_mapping=random_sampler.expanded_idx_mapping,
|
||||
temperatures=random_sampler.temperatures,
|
||||
seeds=random_sampler.seeds,
|
||||
positions=random_sampler.positions,
|
||||
enabled=state.enabled,
|
||||
logits_cache=random_sampler.logits_cache,
|
||||
logits_cache_col=random_sampler.logits_cache_col,
|
||||
use_fp64=random_sampler.use_fp64,
|
||||
prior_contexts=state.prior_contexts,
|
||||
all_token_ids=state.all_token_ids,
|
||||
prompt_lens=state.prompt_lens,
|
||||
total_lens=state.total_lens,
|
||||
deduplicate=state.deduplicate_contexts != "none",
|
||||
max_history=state.deduplicate_contexts_max_history,
|
||||
include_prompt=state.deduplicate_contexts == "all",
|
||||
)
|
||||
|
||||
|
||||
class DualKeyGumbelWatermarker(Watermarker, SupportsSpeculativeDecoding):
|
||||
def __init__(
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 (
|
||||
DraftContextState,
|
||||
RandomSampler,
|
||||
SupportsSpeculativeDecoding,
|
||||
Watermarker,
|
||||
@@ -28,25 +29,25 @@ class DraftWatermarker:
|
||||
) -> None:
|
||||
self.watermarker = watermarker
|
||||
self.num_speculative_steps = num_speculative_steps
|
||||
self.deduplicate_contexts = deduplicate_contexts
|
||||
self.deduplicate_contexts_max_history = deduplicate_contexts_max_history
|
||||
self.contexts = torch.zeros(
|
||||
max_num_reqs,
|
||||
watermarker.context_width,
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
self.state = DraftContextState(
|
||||
contexts=torch.zeros(
|
||||
max_num_reqs,
|
||||
watermarker.context_width,
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
),
|
||||
enabled=torch.zeros(max_num_reqs, dtype=torch.bool, device=device),
|
||||
prior_contexts=torch.zeros(
|
||||
max_num_reqs,
|
||||
num_speculative_steps,
|
||||
watermarker.context_width,
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
),
|
||||
prompt_lens=torch.zeros(max_num_reqs, dtype=torch.int32, device=device),
|
||||
deduplicate_contexts=deduplicate_contexts,
|
||||
deduplicate_contexts_max_history=deduplicate_contexts_max_history,
|
||||
)
|
||||
self.enabled = torch.zeros(max_num_reqs, dtype=torch.bool, device=device)
|
||||
self.prior_contexts = torch.zeros(
|
||||
max_num_reqs,
|
||||
num_speculative_steps,
|
||||
watermarker.context_width,
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
)
|
||||
self.prompt_lens = torch.zeros(max_num_reqs, dtype=torch.int32, device=device)
|
||||
self.all_token_ids: torch.Tensor | None = None
|
||||
self.total_lens: torch.Tensor | None = None
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
@@ -57,11 +58,12 @@ class DraftWatermarker:
|
||||
total_lens: torch.Tensor,
|
||||
) -> None:
|
||||
num_reqs = contexts.shape[0]
|
||||
self.contexts[:num_reqs].copy_(contexts)
|
||||
self.enabled[:num_reqs].copy_(enabled)
|
||||
self.all_token_ids = all_token_ids
|
||||
self.prompt_lens.copy_(prompt_lens)
|
||||
self.total_lens = total_lens
|
||||
state = self.state
|
||||
state.contexts[:num_reqs].copy_(contexts)
|
||||
state.enabled[:num_reqs].copy_(enabled)
|
||||
state.all_token_ids = all_token_ids
|
||||
state.prompt_lens.copy_(prompt_lens)
|
||||
state.total_lens = total_lens
|
||||
|
||||
def _sampling_state(
|
||||
self,
|
||||
@@ -75,22 +77,23 @@ class DraftWatermarker:
|
||||
steps = torch.as_tensor(draft_step, device=logits.device, dtype=torch.int64)
|
||||
if steps.ndim == 0:
|
||||
steps = steps.expand(num_rows)
|
||||
enabled = self.enabled[:num_rows] & (request_temperatures != 0)
|
||||
if self.deduplicate_contexts == "none":
|
||||
state = self.state
|
||||
enabled = state.enabled[:num_rows] & (request_temperatures != 0)
|
||||
if state.deduplicate_contexts == "none":
|
||||
return steps, enabled
|
||||
assert self.all_token_ids is not None
|
||||
assert self.total_lens is not None
|
||||
assert state.all_token_ids is not None
|
||||
assert state.total_lens is not None
|
||||
enabled = draft_watermarking_mask(
|
||||
self.all_token_ids,
|
||||
state.all_token_ids,
|
||||
idx_mapping,
|
||||
self.prompt_lens,
|
||||
self.total_lens,
|
||||
self.prior_contexts,
|
||||
state.prompt_lens,
|
||||
state.total_lens,
|
||||
state.prior_contexts,
|
||||
contexts,
|
||||
steps,
|
||||
enabled,
|
||||
self.deduplicate_contexts_max_history,
|
||||
include_prompt=self.deduplicate_contexts == "all",
|
||||
state.deduplicate_contexts_max_history,
|
||||
include_prompt=state.deduplicate_contexts == "all",
|
||||
)
|
||||
return steps, enabled
|
||||
|
||||
@@ -107,14 +110,26 @@ class DraftWatermarker:
|
||||
logits_cache_col: torch.Tensor | None = None,
|
||||
use_fp64: bool = False,
|
||||
logits_cache_source: torch.Tensor | None = None,
|
||||
num_steps: int = 1,
|
||||
) -> torch.Tensor:
|
||||
if num_steps > 1:
|
||||
return self.sample_block(
|
||||
logits,
|
||||
idx_mapping,
|
||||
temperature,
|
||||
seed,
|
||||
pos,
|
||||
logits_cache=logits_cache,
|
||||
logits_cache_col=logits_cache_col,
|
||||
use_fp64=use_fp64,
|
||||
)
|
||||
assert apply_temperature
|
||||
assert is_drafting
|
||||
assert logits_cache is not None
|
||||
assert logits_cache_col is not None
|
||||
num_rows = logits.shape[0]
|
||||
request_temperatures = temperature[idx_mapping]
|
||||
contexts = self.contexts[:num_rows]
|
||||
contexts = self.state.contexts[:num_rows]
|
||||
steps, enabled = self._sampling_state(
|
||||
logits, idx_mapping, request_temperatures, logits_cache_col, contexts
|
||||
)
|
||||
@@ -145,6 +160,68 @@ class DraftWatermarker:
|
||||
contexts.copy_(torch.cat((contexts[:, 1:], sampled.unsqueeze(-1)), dim=-1))
|
||||
return sampled
|
||||
|
||||
def sample_block(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
idx_mapping: torch.Tensor,
|
||||
temperature: torch.Tensor,
|
||||
seed: torch.Tensor,
|
||||
pos: torch.Tensor,
|
||||
logits_cache: torch.Tensor | None = None,
|
||||
logits_cache_col: torch.Tensor | None = None,
|
||||
use_fp64: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Sample drafts flattened as (request, step), one step at a time.
|
||||
The target verifies each draft with a context that includes the earlier
|
||||
drafts of its request, so steps must be sampled in order even when
|
||||
their logits were computed in parallel.
|
||||
"""
|
||||
assert logits_cache_col is not None
|
||||
num_steps = self.num_speculative_steps
|
||||
sampled_block = self.watermarker._try_sample_block(
|
||||
logits,
|
||||
num_steps,
|
||||
RandomSampler(
|
||||
expanded_idx_mapping=idx_mapping,
|
||||
temperatures=temperature,
|
||||
seeds=seed,
|
||||
positions=pos,
|
||||
use_fp64=use_fp64,
|
||||
is_drafting=True,
|
||||
logits_cache=logits_cache,
|
||||
logits_cache_col=logits_cache_col,
|
||||
),
|
||||
self.state,
|
||||
)
|
||||
if sampled_block is not None:
|
||||
return sampled_block
|
||||
num_reqs = logits.shape[0] // num_steps
|
||||
|
||||
def by_step(tensor: torch.Tensor) -> torch.Tensor:
|
||||
return tensor.view(num_reqs, num_steps, *tensor.shape[1:])
|
||||
|
||||
logits = by_step(logits)
|
||||
idx_mapping = by_step(idx_mapping)
|
||||
pos = by_step(pos)
|
||||
logits_cache_col = by_step(logits_cache_col)
|
||||
sampled = torch.empty(
|
||||
num_reqs, num_steps, dtype=torch.int64, device=logits.device
|
||||
)
|
||||
for step in range(num_steps):
|
||||
sampled[:, step] = self.sample(
|
||||
logits[:, step],
|
||||
idx_mapping[:, step],
|
||||
temperature,
|
||||
seed,
|
||||
pos[:, step],
|
||||
apply_temperature=True,
|
||||
is_drafting=True,
|
||||
logits_cache=logits_cache,
|
||||
logits_cache_col=logits_cache_col[:, step],
|
||||
use_fp64=use_fp64,
|
||||
)
|
||||
return sampled.view(-1)
|
||||
|
||||
|
||||
def create_speculative_target_watermarker(watermarker: Watermarker) -> Watermarker:
|
||||
if isinstance(watermarker, SupportsSpeculativeDecoding):
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Protocol, runtime_checkable
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.config.watermarking import WatermarkContextScope
|
||||
from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
|
||||
|
||||
|
||||
@@ -44,6 +45,18 @@ class RandomSampler:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DraftContextState:
|
||||
contexts: torch.Tensor
|
||||
enabled: torch.Tensor
|
||||
prior_contexts: torch.Tensor
|
||||
prompt_lens: torch.Tensor
|
||||
deduplicate_contexts: WatermarkContextScope
|
||||
deduplicate_contexts_max_history: int | None
|
||||
all_token_ids: torch.Tensor | None = None
|
||||
total_lens: torch.Tensor | None = None
|
||||
|
||||
|
||||
class Watermarker(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
@@ -94,6 +107,20 @@ class Watermarker(ABC):
|
||||
) -> WatermarkSample | None:
|
||||
return None
|
||||
|
||||
def _try_sample_block(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
num_steps: int,
|
||||
random_sampler: RandomSampler,
|
||||
state: DraftContextState,
|
||||
) -> torch.Tensor | None:
|
||||
"""Sample a (request, step) draft block in one fused pass.
|
||||
|
||||
Returns None when there is no fused implementation, in which case the
|
||||
caller samples the block one step at a time.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsSpeculativeDecoding(Protocol):
|
||||
|
||||
@@ -530,8 +530,10 @@ def _philox_gumbel_from_logits(logits, word):
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _gumbel_value(logits_ptr, output, mask):
|
||||
def _gumbel_value(logits_ptr, output, mask, temperature):
|
||||
logits = tl.load(logits_ptr, mask=mask, other=float("-inf")).to(tl.float32)
|
||||
if temperature is not None:
|
||||
logits = tl_math.div_rn(logits, temperature)
|
||||
return _philox_gumbel_from_logits(logits, output)
|
||||
|
||||
|
||||
@@ -606,6 +608,7 @@ def _philox_gumbel_kernel(
|
||||
USE_FP64: tl.constexpr,
|
||||
IS_DRAFTING: tl.constexpr,
|
||||
PER_TOKEN_COL: tl.constexpr,
|
||||
APPLY_TEMPERATURE: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
block_index = tl.program_id(1)
|
||||
@@ -619,6 +622,13 @@ def _philox_gumbel_kernel(
|
||||
req_state_idx = tl.load(expanded_idx_mapping_ptr + row).to(tl.int64)
|
||||
valid_req = req_state_idx >= 0
|
||||
|
||||
row_temperature = None
|
||||
if APPLY_TEMPERATURE:
|
||||
row_temperature = tl.load(
|
||||
temp_ptr + req_state_idx, mask=valid_req, other=0.0
|
||||
).to(tl.float32)
|
||||
row_temperature = tl.where(row_temperature == 0, 1.0, row_temperature)
|
||||
|
||||
if logits_cache_ptr is not None:
|
||||
if PER_TOKEN_COL:
|
||||
col = tl.load(logits_cache_col_ptr + row)
|
||||
@@ -650,6 +660,8 @@ def _philox_gumbel_kernel(
|
||||
logits = tl.load(logits_row + candidate, mask=mask, other=float("-inf")).to(
|
||||
tl.float32
|
||||
)
|
||||
if row_temperature is not None:
|
||||
logits = tl_math.div_rn(logits, row_temperature)
|
||||
value, index = gumbel_noised_argmax(
|
||||
logits,
|
||||
candidate,
|
||||
@@ -683,16 +695,16 @@ def _philox_gumbel_kernel(
|
||||
candidate_3 = candidate_0 + 3
|
||||
logits_row = logits_ptr + row * logits_stride
|
||||
value_0 = _gumbel_value(
|
||||
logits_row + candidate_0, output_0, candidate_0 < vocab_size
|
||||
logits_row + candidate_0, output_0, candidate_0 < vocab_size, row_temperature
|
||||
)
|
||||
value_1 = _gumbel_value(
|
||||
logits_row + candidate_1, output_1, candidate_1 < vocab_size
|
||||
logits_row + candidate_1, output_1, candidate_1 < vocab_size, row_temperature
|
||||
)
|
||||
value_2 = _gumbel_value(
|
||||
logits_row + candidate_2, output_2, candidate_2 < vocab_size
|
||||
logits_row + candidate_2, output_2, candidate_2 < vocab_size, row_temperature
|
||||
)
|
||||
value_3 = _gumbel_value(
|
||||
logits_row + candidate_3, output_3, candidate_3 < vocab_size
|
||||
logits_row + candidate_3, output_3, candidate_3 < vocab_size, row_temperature
|
||||
)
|
||||
best_value = value_0
|
||||
best_token = candidate_0
|
||||
@@ -799,6 +811,238 @@ def philox_gumbel_sample(
|
||||
USE_FP64=use_fp64,
|
||||
IS_DRAFTING=is_drafting,
|
||||
PER_TOKEN_COL=logits_cache_col is not None and logits_cache_col.dim() > 0,
|
||||
APPLY_TEMPERATURE=False,
|
||||
)
|
||||
max_block_index = local_max.argmax(dim=-1, keepdim=True)
|
||||
return local_argmax.gather(dim=-1, index=max_block_index).view(-1)
|
||||
|
||||
|
||||
@triton.jit(do_not_specialize=["step"])
|
||||
def _draft_step_kernel(
|
||||
sampled_ptr,
|
||||
sampled_stride,
|
||||
step,
|
||||
local_argmax_ptr,
|
||||
local_max_ptr,
|
||||
num_blocks,
|
||||
contexts_ptr,
|
||||
context_stride,
|
||||
skip_mask_ptr,
|
||||
next_req_indices_ptr,
|
||||
next_steps_ptr,
|
||||
temp_ptr,
|
||||
enabled_ptr,
|
||||
all_token_ids_ptr,
|
||||
all_token_ids_stride,
|
||||
prompt_lens_ptr,
|
||||
total_lens_ptr,
|
||||
prior_contexts_ptr,
|
||||
prior_contexts_stride_0,
|
||||
prior_contexts_stride_1,
|
||||
CONTEXT_WIDTH: tl.constexpr,
|
||||
NUM_SPECULATIVE_STEPS: tl.constexpr,
|
||||
MAX_HISTORY: tl.constexpr,
|
||||
INCLUDE_PROMPT: tl.constexpr,
|
||||
DEDUPLICATE: tl.constexpr,
|
||||
SAMPLE: tl.constexpr,
|
||||
PREPARE_NEXT: tl.constexpr,
|
||||
BLOCKS_POW2: tl.constexpr,
|
||||
):
|
||||
"""Finish draft step `step` (if SAMPLE) and build the next step's skip mask."""
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
contexts_row_ptr = contexts_ptr + row * context_stride
|
||||
if SAMPLE:
|
||||
# Same pick as local_max.argmax(dim=-1): the first NaN, else the first max.
|
||||
blocks = tl.arange(0, BLOCKS_POW2)
|
||||
values = tl.load(
|
||||
local_max_ptr + row * num_blocks + blocks,
|
||||
mask=blocks < num_blocks,
|
||||
other=float("-inf"),
|
||||
)
|
||||
is_nan = values != values
|
||||
best = tl.max(tl.where(is_nan, float("-inf"), values), axis=0)
|
||||
first_nan = tl.min(tl.where(is_nan, blocks, BLOCKS_POW2), axis=0)
|
||||
first_max = tl.min(
|
||||
tl.where((values == best) & (blocks < num_blocks), blocks, BLOCKS_POW2),
|
||||
axis=0,
|
||||
)
|
||||
block = tl.where(first_nan < BLOCKS_POW2, first_nan, first_max)
|
||||
token = tl.load(local_argmax_ptr + row * num_blocks + block)
|
||||
tl.store(sampled_ptr + row * sampled_stride + step, token)
|
||||
|
||||
offsets = tl.arange(0, BLOCKS_POW2)
|
||||
context = tl.load(
|
||||
contexts_row_ptr + offsets + 1,
|
||||
mask=offsets < CONTEXT_WIDTH - 1,
|
||||
other=token,
|
||||
)
|
||||
tl.debug_barrier()
|
||||
tl.store(contexts_row_ptr + offsets, context, mask=offsets < CONTEXT_WIDTH)
|
||||
tl.debug_barrier()
|
||||
|
||||
if PREPARE_NEXT:
|
||||
req_idx = tl.load(next_req_indices_ptr + row).to(tl.int64)
|
||||
temperature = tl.load(temp_ptr + req_idx, mask=req_idx >= 0, other=0.0)
|
||||
active = tl.load(enabled_ptr + row) & (temperature != 0)
|
||||
if DEDUPLICATE:
|
||||
# The dedup kernel body runs in place on the skip mask buffer.
|
||||
tl.store(skip_mask_ptr + row, active)
|
||||
tl.debug_barrier()
|
||||
_repeated_context_mask_kernel(
|
||||
skip_mask_ptr,
|
||||
all_token_ids_ptr,
|
||||
all_token_ids_stride,
|
||||
next_req_indices_ptr,
|
||||
prompt_lens_ptr,
|
||||
total_lens_ptr,
|
||||
next_steps_ptr,
|
||||
1,
|
||||
contexts_ptr,
|
||||
context_stride,
|
||||
None,
|
||||
prior_contexts_ptr,
|
||||
prior_contexts_stride_0,
|
||||
prior_contexts_stride_1,
|
||||
next_steps_ptr,
|
||||
1,
|
||||
skip_mask_ptr,
|
||||
CONTEXT_WIDTH=CONTEXT_WIDTH,
|
||||
NUM_SPECULATIVE_STEPS=NUM_SPECULATIVE_STEPS,
|
||||
MAX_HISTORY=MAX_HISTORY,
|
||||
INCLUDE_PROMPT=INCLUDE_PROMPT,
|
||||
SKIP_PARTIAL_CONTEXT=False,
|
||||
HAS_HISTORY_OFFSETS=True,
|
||||
SPECULATIVE_CONTEXTS=False,
|
||||
DRAFT_CONTEXTS=True,
|
||||
BLOCK=512,
|
||||
)
|
||||
tl.debug_barrier()
|
||||
active = tl.load(skip_mask_ptr + row)
|
||||
tl.store(skip_mask_ptr + row, active == 0)
|
||||
|
||||
|
||||
def draft_philox_gumbel_sample(
|
||||
logits: torch.Tensor,
|
||||
contexts: torch.Tensor,
|
||||
key: int,
|
||||
*,
|
||||
num_steps: int,
|
||||
expanded_idx_mapping: torch.Tensor,
|
||||
temperatures: torch.Tensor,
|
||||
seeds: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
enabled: torch.Tensor,
|
||||
logits_cache: torch.Tensor,
|
||||
logits_cache_col: torch.Tensor,
|
||||
use_fp64: bool,
|
||||
prior_contexts: torch.Tensor,
|
||||
all_token_ids: torch.Tensor | None,
|
||||
prompt_lens: torch.Tensor,
|
||||
total_lens: torch.Tensor | None,
|
||||
deduplicate: bool,
|
||||
max_history: int | None,
|
||||
include_prompt: bool,
|
||||
) -> torch.Tensor:
|
||||
"""Sample `num_steps` draft steps flattened as (request, step), in step order.
|
||||
|
||||
Bit-exact with sampling each step by `DraftWatermarker.sample`, but with two
|
||||
kernel launches per step: the Philox block argmax (with the temperature
|
||||
divide and the logits cache write), then a per-row kernel that picks the
|
||||
token, shifts `contexts` and builds the next step's skip mask. Padding rows
|
||||
(index -1) read temperature 0 and are never watermarked, so they may differ.
|
||||
"""
|
||||
num_rows, vocab_size = logits.shape
|
||||
num_reqs = num_rows // num_steps
|
||||
assert logits.stride(-1) == 1 and contexts.stride(-1) == 1
|
||||
assert logits.dtype == logits_cache.dtype, (
|
||||
"logits cache source and destination must have the same dtype"
|
||||
)
|
||||
assert logits_cache.size(-1) >= vocab_size, (
|
||||
f"draft logits cache vocab dim ({logits_cache.size(-1)}) is narrower "
|
||||
f"than the sampled logits ({vocab_size}). Cached logits would be truncated."
|
||||
)
|
||||
assert not deduplicate or (all_token_ids is not None and total_lens is not None), (
|
||||
"context deduplication requires all_token_ids and total_lens"
|
||||
)
|
||||
logits = logits.view(num_reqs, num_steps, vocab_size)
|
||||
# Step-major copies, so each step reads contiguous per-request slices.
|
||||
expanded_idx_mapping, positions, logits_cache_col = (
|
||||
tensor.view(num_reqs, num_steps).t().contiguous()
|
||||
for tensor in (expanded_idx_mapping, positions, logits_cache_col)
|
||||
)
|
||||
block_size = 1024
|
||||
num_blocks = triton.cdiv(vocab_size, block_size)
|
||||
local_argmax = logits.new_empty(num_reqs, num_blocks, dtype=torch.int64)
|
||||
local_max = logits.new_empty(
|
||||
num_reqs, num_blocks, dtype=torch.float64 if use_fp64 else torch.float32
|
||||
)
|
||||
skip_mask = torch.empty(num_reqs, dtype=torch.bool, device=logits.device)
|
||||
sampled = logits.new_empty(num_reqs, num_steps, dtype=torch.int64)
|
||||
|
||||
def finish_step(step: int) -> None:
|
||||
next_step = min(step + 1, num_steps - 1)
|
||||
_draft_step_kernel[(num_reqs,)](
|
||||
sampled,
|
||||
sampled.stride(0),
|
||||
step,
|
||||
local_argmax,
|
||||
local_max,
|
||||
num_blocks,
|
||||
contexts,
|
||||
contexts.stride(0),
|
||||
skip_mask,
|
||||
expanded_idx_mapping[next_step],
|
||||
logits_cache_col[next_step],
|
||||
temperatures,
|
||||
enabled,
|
||||
all_token_ids,
|
||||
0 if all_token_ids is None else all_token_ids.stride(0),
|
||||
prompt_lens,
|
||||
total_lens,
|
||||
prior_contexts,
|
||||
prior_contexts.stride(0),
|
||||
prior_contexts.stride(1),
|
||||
CONTEXT_WIDTH=contexts.shape[-1],
|
||||
NUM_SPECULATIVE_STEPS=prior_contexts.shape[1],
|
||||
MAX_HISTORY=0 if max_history is None else max_history,
|
||||
INCLUDE_PROMPT=include_prompt,
|
||||
DEDUPLICATE=deduplicate,
|
||||
SAMPLE=step >= 0,
|
||||
PREPARE_NEXT=step + 1 < num_steps,
|
||||
BLOCKS_POW2=triton.next_power_of_2(max(num_blocks, contexts.shape[-1])),
|
||||
)
|
||||
|
||||
finish_step(-1)
|
||||
for step in range(num_steps):
|
||||
_philox_gumbel_kernel[(num_reqs, num_blocks)](
|
||||
local_argmax,
|
||||
local_argmax.stride(0),
|
||||
local_max,
|
||||
local_max.stride(0),
|
||||
logits[:, step],
|
||||
logits.stride(0),
|
||||
contexts,
|
||||
contexts.stride(0),
|
||||
skip_mask,
|
||||
expanded_idx_mapping[step],
|
||||
seeds,
|
||||
positions[step],
|
||||
temperatures,
|
||||
logits_cache,
|
||||
logits_cache.stride(0),
|
||||
logits_cache.stride(1),
|
||||
logits_cache_col[step],
|
||||
logits[:, step],
|
||||
logits.stride(0),
|
||||
key & _UINT32_MASK_VALUE,
|
||||
key >> 32,
|
||||
vocab_size,
|
||||
CONTEXT_WIDTH=contexts.shape[-1],
|
||||
BLOCK_SIZE=block_size,
|
||||
USE_FP64=use_fp64,
|
||||
IS_DRAFTING=True,
|
||||
PER_TOKEN_COL=True,
|
||||
APPLY_TEMPERATURE=True,
|
||||
)
|
||||
finish_step(step)
|
||||
return sampled.view(-1)
|
||||
|
||||
@@ -366,6 +366,7 @@ class DFlashSpeculator(DraftModelSpeculator):
|
||||
self.seeds,
|
||||
self.sample_col[:num_sample],
|
||||
self.draft_logits,
|
||||
num_steps=self.num_speculative_steps,
|
||||
)
|
||||
self.draft_tokens[:num_reqs] = draft_tokens.view(
|
||||
num_reqs, self.num_speculative_steps
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import replace
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
@@ -446,6 +447,7 @@ class DraftModelSpeculator(BaseSpeculator):
|
||||
draft_step: torch.Tensor,
|
||||
draft_logits: torch.Tensor | None,
|
||||
spec_step_idx: int = 0,
|
||||
num_steps: int = 1,
|
||||
) -> torch.Tensor:
|
||||
if draft_logits is None and self.use_local_argmax_reduction:
|
||||
return self.get_draft_top_tokens(hidden_states, spec_step_idx)
|
||||
@@ -455,7 +457,7 @@ class DraftModelSpeculator(BaseSpeculator):
|
||||
sampler = (
|
||||
gumbel_sample
|
||||
if self.draft_watermarker is None
|
||||
else self.draft_watermarker.sample
|
||||
else partial(self.draft_watermarker.sample, num_steps=num_steps)
|
||||
)
|
||||
sampled = sampler(
|
||||
logits,
|
||||
|
||||
Reference in New Issue
Block a user