Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a885c10
Fix FP64 Gumbel precision coverage
tianyu-z May 19, 2026
6ba4c9b
Merge branch 'main' into gumbel-fix
tianyu-z May 29, 2026
1e79c14
Update vllm/v1/sample/ops/topk_topp_sampler.py
tianyu-z May 29, 2026
10978cb
Update vllm/v1/sample/ops/topk_topp_sampler.py
tianyu-z May 29, 2026
c1fc88f
Restore Gumbel comment details
tianyu-z May 29, 2026
3a9737e
Merge branch 'main' into gumbel-fix
tianyu-z May 29, 2026
d1fe72d
Merge branch 'main' into gumbel-fix
tianyu-z May 29, 2026
63a7b0e
Merge branch 'main' into gumbel-fix
tianyu-z Jun 2, 2026
bb1240c
Merge branch 'main' into gumbel-fix
tianyu-z Jun 2, 2026
819e51f
Merge branch 'main' into gumbel-fix
tianyu-z Jun 2, 2026
3d8b387
Merge remote-tracking branch 'origin/main' into gumbel-fix
tianyu-z Jun 3, 2026
8581dc0
Merge stale remote gumbel-fix history into up-to-date resolution
tianyu-z Jun 3, 2026
7c6d74c
Merge branch 'main' into gumbel-fix
tianyu-z Jun 4, 2026
0627889
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
58fdc59
Fix ruff import ordering
tianyu-z Jun 5, 2026
b9aa0d8
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
e82cfa0
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
f452bac
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
7621799
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
e5021a9
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
a979025
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
a75ea52
Fix probabilistic draft test mock signature
tianyu-z Jun 5, 2026
78c6ec4
Merge branch 'main' into gumbel-fix
tianyu-z Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion tests/v1/sample/test_rejection_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,13 +544,15 @@ def native_sample_recovered_tokens(
target_probs: torch.Tensor, # [num_tokens, vocab_size]
sampling_metadata: SamplingMetadata,
device: torch.device,
use_fp64_gumbel: bool = False,
) -> torch.Tensor:
batch_size = len(num_draft_tokens)
vocab_size = target_probs.shape[-1]
q_dtype = torch.float64 if use_fp64_gumbel else torch.float32

q = torch.empty(
(batch_size, vocab_size),
dtype=torch.float32,
dtype=q_dtype,
device=device,
)
q.exponential_()
Expand Down Expand Up @@ -935,6 +937,67 @@ def test_sample_recovered_tokens(
assert torch.equal(recovered_token_ids, ref_recovered_token_ids)


def test_sample_recovered_tokens_uses_fp64_exponential_race_when_requested():
batch_size = 2
vocab_size = 64
max_spec_len = 2
num_tokens = batch_size * max_spec_len

draft_probs = torch.rand(
num_tokens,
vocab_size,
dtype=torch.float32,
device=DEVICE_TYPE,
)
draft_probs = F.softmax(draft_probs, dim=-1)
target_probs = torch.rand(
num_tokens,
vocab_size,
dtype=torch.float32,
device=DEVICE_TYPE,
)
target_probs = F.softmax(target_probs, dim=-1)
draft_token_ids = torch.multinomial(draft_probs, num_samples=1).to(torch.int32)

generators = {
i: torch.Generator(device=DEVICE_TYPE).manual_seed(i) for i in range(batch_size)
}
sampling_metadata = create_sampling_metadata(
all_greedy=False,
temperature=torch.ones(batch_size, dtype=torch.float32, device=DEVICE_TYPE),
generators=generators,
)
spec_decode_metadata = create_spec_decode_metadata(
draft_token_ids.reshape(batch_size, max_spec_len).tolist(),
target_probs.log(),
)

expected = native_sample_recovered_tokens(
max_spec_len,
spec_decode_metadata.num_draft_tokens,
spec_decode_metadata.cu_num_draft_tokens,
draft_token_ids,
draft_probs,
target_probs,
sampling_metadata,
device=torch.device(DEVICE_TYPE),
use_fp64_gumbel=True,
)
actual = sample_recovered_tokens(
max_spec_len,
spec_decode_metadata.num_draft_tokens,
spec_decode_metadata.cu_num_draft_tokens,
draft_token_ids,
draft_probs,
target_probs,
sampling_metadata,
device=torch.device(DEVICE_TYPE),
use_fp64_gumbel=True,
)

assert torch.equal(actual, expected)


########################### Tests for Synthetic Rejection Sampling #########


Expand Down
40 changes: 39 additions & 1 deletion tests/v1/sample/test_topk_topp_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from torch import Generator

from vllm.platforms import current_platform
from vllm.utils.torch_utils import set_random_seed
from vllm.v1.sample.ops.topk_topp_sampler import (
apply_top_k_top_p_pytorch,
random_sample,
)
from vllm.v1.sample.sampler import Sampler
from vllm.triton_utils import HAS_TRITON
from vllm.v1.sample.ops.topk_topp_sampler import apply_top_k_top_p_pytorch

DEVICE_TYPE = current_platform.device_type

Expand Down Expand Up @@ -38,6 +43,10 @@ def _flashinfer_topk_topp_supported() -> bool:
FLASHINFER_TOPK_TOPP_SUPPORTED = _flashinfer_topk_topp_supported()


def _seed_default_generator(seed: int) -> None:
set_random_seed(seed)


@pytest.fixture(autouse=True)
def reset_default_device():
"""
Expand All @@ -49,6 +58,35 @@ def reset_default_device():
torch.set_default_device(original_device)


def test_sampler_threads_fp64_gumbel_to_topk_topp_sampler():
sampler = Sampler(use_fp64_gumbel=True)

assert sampler.topk_topp_sampler.use_fp64_gumbel


def test_random_sample_uses_fp64_exponential_race_when_requested():
torch.set_default_device(DEVICE_TYPE)
probs = torch.tensor(
[
[0.70, 0.20, 0.10],
[0.05, 0.15, 0.80],
[0.25, 0.25, 0.50],
],
dtype=torch.float32,
device=DEVICE_TYPE,
)

_seed_default_generator(12345)
q = torch.empty(probs.shape, dtype=torch.float64, device=probs.device)
q.exponential_()
expected = q.reciprocal_().mul_(probs).argmax(dim=-1).view(-1)

_seed_default_generator(12345)
actual = random_sample(probs.clone(), {}, use_fp64_gumbel=True)

assert torch.equal(actual, expected)


def test_topk_impl_equivalence():
torch.set_default_device(DEVICE_TYPE)
generator = Generator(device=DEVICE_TYPE).manual_seed(33)
Expand Down
70 changes: 70 additions & 0 deletions tests/v1/spec_decode/test_llm_base_proposer_sampling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import torch

from vllm.platforms import current_platform
from vllm.utils.torch_utils import set_random_seed
from vllm.v1.sample.logits_processor import LogitsProcessors
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.spec_decode.llm_base_proposer import (
compute_probs_and_sample_next_token,
)

DEVICE_TYPE = current_platform.device_type


def _seed_default_generator(seed: int) -> None:
set_random_seed(seed)


def _make_sampling_metadata(batch_size: int) -> SamplingMetadata:
return SamplingMetadata(
temperature=torch.ones(batch_size, dtype=torch.float32, device=DEVICE_TYPE),
all_greedy=False,
all_random=True,
top_p=None,
top_k=None,
generators={},
max_num_logprobs=None,
no_penalties=True,
prompt_token_ids=None,
frequency_penalties=torch.empty(0, device=DEVICE_TYPE),
presence_penalties=torch.empty(0, device=DEVICE_TYPE),
repetition_penalties=torch.empty(0, device=DEVICE_TYPE),
output_token_ids=[[] for _ in range(batch_size)],
spec_token_ids=[[] for _ in range(batch_size)],
allowed_token_ids_mask=None,
bad_words_token_ids={},
logitsprocs=LogitsProcessors(),
)


def test_compute_probs_and_sample_next_token_uses_fp64_exponential_race():
batch_size = 4
vocab_size = 32
generator = torch.Generator(device=DEVICE_TYPE).manual_seed(11)
logits = torch.randn(
batch_size,
vocab_size,
dtype=torch.float32,
device=DEVICE_TYPE,
generator=generator,
)
metadata = _make_sampling_metadata(batch_size)

_seed_default_generator(12345)
probs = logits.softmax(dim=-1, dtype=torch.float32)
q = torch.empty(probs.shape, dtype=torch.float64, device=probs.device)
q.exponential_()
expected_ids = q.reciprocal_().mul_(probs).argmax(dim=-1).view(-1)

_seed_default_generator(12345)
actual_ids, actual_probs = compute_probs_and_sample_next_token(
logits.clone(),
metadata,
use_fp64_gumbel=True,
)

assert torch.equal(actual_ids, expected_ids)
assert torch.allclose(actual_probs, probs)
141 changes: 141 additions & 0 deletions tools/gumbel_precision/prove_exponential_race_precision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CUDA proof for fp32 exponential-race tail truncation.

This script is intentionally not a unit test. It is a reproducible, GPU-only
statistical proof for the hidden Gumbel-max idiom:

q.exponential_()
sample = (probs / q).argmax()

For q ~ Exp(1), this is equivalent to argmax(log(probs) + Gumbel). On CUDA,
fp32 exponential samples inherit a 24-bit uniform lower-tail cutoff, so very
small q values are impossible. The many-tail experiment below chooses a case
where a correct sampler should select a low-probability tail token dozens of
times, while fp32 q cannot select one.
"""

from __future__ import annotations

import argparse
import math
import time

import torch


def _seed(seed: int) -> None:
torch.manual_seed(seed)


def measure_exponential_lower_tail(
*,
device: torch.device,
samples: int,
chunk_size: int,
seed: int,
) -> None:
threshold = 2.0**-24
print(f"lower-tail threshold: {threshold:.18e}")
for dtype in (torch.float32, torch.float64):
_seed(seed)
count_below = 0
min_q = float("inf")
max_q = 0.0
start = time.perf_counter()
remaining = samples
while remaining > 0:
n = min(chunk_size, remaining)
q = torch.empty((n,), dtype=dtype, device=device)
q.exponential_()
count_below += int((q < threshold).sum().item())
min_q = min(min_q, float(q.min().item()))
max_q = max(max_q, float(q.max().item()))
remaining -= n
torch.accelerator.synchronize()
elapsed = time.perf_counter() - start
print(
f"{dtype}: samples={samples} count(q < 2^-24)={count_below} "
f"min={min_q:.18e} max={max_q:.6f} elapsed={elapsed:.2f}s"
)


def run_many_tail_race(
*,
device: torch.device,
trials: int,
num_tail_tokens: int,
gap: float,
chunk_trials: int,
seed: int,
) -> None:
p_tail = math.exp(-gap)
expected_tail_hits = (
trials * (num_tail_tokens * p_tail) / (1.0 + num_tail_tokens * p_tail)
)
print(
"many-tail race: "
f"trials={trials} num_tail_tokens={num_tail_tokens} gap={gap} "
f"expected_tail_hits={expected_tail_hits:.4f}"
)

for dtype in (torch.float32, torch.float64):
_seed(seed)
hits = 0
p0 = torch.tensor(1.0, dtype=dtype, device=device)
pt = torch.tensor(p_tail, dtype=dtype, device=device)
start = time.perf_counter()
remaining = trials
while remaining > 0:
batch = min(chunk_trials, remaining)
q0 = torch.empty((batch,), dtype=dtype, device=device)
q0.exponential_()
qt = torch.empty((batch, num_tail_tokens), dtype=dtype, device=device)
qt.exponential_()
head_score = p0 / q0
tail_score = (pt / qt).amax(dim=-1)
hits += int((tail_score > head_score).sum().item())
remaining -= batch
torch.accelerator.synchronize()
elapsed = time.perf_counter() - start
print(f"{dtype}: tail_hits={hits} elapsed={elapsed:.2f}s")


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--lower-tail-samples", type=int, default=200_000_000)
parser.add_argument("--lower-tail-chunk-size", type=int, default=10_000_000)
parser.add_argument("--race-trials", type=int, default=100_000)
parser.add_argument("--race-tail-tokens", type=int, default=262_144)
parser.add_argument("--race-gap", type=float, default=20.5)
parser.add_argument("--race-chunk-trials", type=int, default=64)
parser.add_argument("--seed", type=int, default=2026)
args = parser.parse_args()

if not torch.accelerator.is_available():
raise RuntimeError("CUDA is required for this proof.")

device = torch.accelerator.current_accelerator()
if device.type != "cuda":
raise RuntimeError("CUDA is required for this proof.")

print(f"torch={torch.__version__} cuda={torch.version.cuda}")
print(f"device={device}")
measure_exponential_lower_tail(
device=device,
samples=args.lower_tail_samples,
chunk_size=args.lower_tail_chunk_size,
seed=args.seed,
)
run_many_tail_race(
device=device,
trials=args.race_trials,
num_tail_tokens=args.race_tail_tokens,
gap=args.race_gap,
chunk_trials=args.race_chunk_trials,
seed=args.seed,
)


if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions vllm/config/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ class ModelConfig:
temperature and top_k/top_p.
"""
use_fp64_gumbel: bool = False
"""Whether to use FP64 (instead of FP32) for the Gumbel noise used by the
sampler. FP64 reduces the chance of ties in Gumbel-max sampling at the cost
of significantly lower kernel throughput on most GPUs."""
"""Whether to use FP64 (instead of FP32) random noise for Gumbel-max and
equivalent exponential-race sampling. FP64 preserves lower-tail sampling
events that fp32 uniform/exponential draws can truncate, at the cost of
significantly lower throughput on most GPUs."""
disable_sliding_window: bool = False
"""Whether to disable sliding window. If True, we will disable the sliding
window functionality of the model, capping to sliding window size. If the
Expand Down
Loading
Loading