main
py 120 lines 4.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import tracemalloc
5
6 import pytest
7 import torch
8
9 from vllm.platforms import current_platform
10
11 if not current_platform.is_cpu():
12 pytest.skip("skipping CPU-only tests", allow_module_level=True)
13
14 import vllm._C # noqa: F401, E402
15
16 VOCAB_SIZES = [32000, 49152, 128256]
17 BATCH_SIZES = [1, 4, 16]
18
19
20 def reference_greedy_sample(logits: torch.Tensor) -> torch.Tensor:
21 return logits.argmax(dim=-1).view(-1)
22
23
24 class TestGreedyArgmax:
25 @pytest.mark.parametrize("vocab_size", VOCAB_SIZES)
26 @pytest.mark.parametrize("batch_size", BATCH_SIZES)
27 def test_exact_match(self, vocab_size: int, batch_size: int):
28 logits = torch.randn(batch_size, vocab_size, dtype=torch.float32)
29 expected = reference_greedy_sample(logits)
30 result = torch.ops._C.greedy_argmax(logits)
31 torch.testing.assert_close(result, expected)
32
33 def test_single_dominant(self):
34 logits = torch.full((1, 50000), -1e9, dtype=torch.float32)
35 logits[0, 42] = 100.0
36 assert torch.ops._C.greedy_argmax(logits).item() == 42
37
38 def test_negative_logits(self):
39 logits = torch.randn(8, 32000, dtype=torch.float32) - 10.0
40 expected = reference_greedy_sample(logits)
41 result = torch.ops._C.greedy_argmax(logits)
42 torch.testing.assert_close(result, expected)
43
44
45 class TestFusedGumbelArgmax:
46 @pytest.mark.parametrize("vocab_size", VOCAB_SIZES)
47 def test_distribution_chi_squared(self, vocab_size: int):
48 """Verify sampling distribution via chi-squared goodness of fit."""
49 small_vocab = min(vocab_size, 100)
50 logits = torch.randn(1, small_vocab, dtype=torch.float32)
51 probs = logits.softmax(dim=-1).squeeze(0)
52
53 n_samples = 100_000
54 counts = torch.zeros(small_vocab)
55 for trial in range(n_samples):
56 seed = torch.tensor([trial * 7 + 13], dtype=torch.long)
57 tile = logits.expand(1, -1).contiguous()
58 idx = torch.ops._C.fused_gumbel_argmax(tile, seed)
59 counts[idx.item()] += 1
60
61 expected = probs * n_samples
62 mask = expected > 5
63 chi2 = ((counts[mask] - expected[mask]) ** 2 / expected[mask]).sum()
64 dof = mask.sum().item() - 1
65 from scipy.stats import chi2 as chi2_dist
66
67 p_value = 1.0 - chi2_dist.cdf(chi2.item(), dof)
68 assert p_value > 0.001, (
69 f"Chi-squared test failed: chi2={chi2.item():.1f}, "
70 f"dof={dof}, p={p_value:.6f}"
71 )
72
73 def test_deterministic_same_seed(self):
74 """Same seed produces same result."""
75 logits = torch.randn(4, 32000, dtype=torch.float32)
76 seeds = torch.tensor([42, 123, 456, 789], dtype=torch.long)
77 r1 = torch.ops._C.fused_gumbel_argmax(logits, seeds)
78 r2 = torch.ops._C.fused_gumbel_argmax(logits, seeds)
79 torch.testing.assert_close(r1, r2)
80
81 def test_different_seeds_differ(self):
82 logits = torch.zeros(16, 50000, dtype=torch.float32)
83 seeds_a = torch.arange(16, dtype=torch.long)
84 seeds_b = torch.arange(16, dtype=torch.long) + 1_000_000
85 r_a = torch.ops._C.fused_gumbel_argmax(logits, seeds_a)
86 r_b = torch.ops._C.fused_gumbel_argmax(logits, seeds_b)
87 assert not torch.equal(r_a, r_b)
88
89 @pytest.mark.parametrize("vocab_size", VOCAB_SIZES)
90 @pytest.mark.parametrize("batch_size", BATCH_SIZES)
91 def test_output_in_range(self, vocab_size: int, batch_size: int):
92 logits = torch.randn(batch_size, vocab_size, dtype=torch.float32)
93 seeds = torch.arange(batch_size, dtype=torch.long)
94 result = torch.ops._C.fused_gumbel_argmax(logits, seeds)
95 assert result.min() >= 0
96 assert result.max() < vocab_size
97
98
99 class TestMemory:
100 def test_fused_no_intermediate_allocs(self):
101 """Verify that the fused kernel does not allocate large intermediates."""
102 logits = torch.randn(16, 128256, dtype=torch.float32)
103 seeds = torch.arange(16, dtype=torch.long)
104
105 torch.ops._C.fused_gumbel_argmax(logits, seeds)
106
107 tracemalloc.start()
108 snap_before = tracemalloc.take_snapshot()
109 for _ in range(50):
110 torch.ops._C.fused_gumbel_argmax(logits, seeds)
111 snap_after = tracemalloc.take_snapshot()
112 tracemalloc.stop()
113
114 diff = snap_after.compare_to(snap_before, "lineno")
115 total_new_bytes = sum(s.size_diff for s in diff if s.size_diff > 0)
116 vocab_bytes = 16 * 128256 * 4
117 assert total_new_bytes < vocab_bytes, (
118 f"Fused kernel allocated {total_new_bytes} bytes, "
119 f"expected < {vocab_bytes} (one intermediate tensor)"
120 )