main
py 126 lines 4.63 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for the fused vocab-parallel embedding kernel
4 (``vllm._custom_ops.vocab_parallel_embedding``).
5
6 The guarantee: the kernel is a drop-in for the TP > 1 embedding path, i.e. it
7 returns bit-exactly what mask + shift + gather + ``masked_fill_`` returns on
8 each rank, and summing the per-rank outputs (what the all-reduce does)
9 reconstructs a full-table lookup.
10 """
11
12 import pytest
13 import torch
14
15 from vllm import _custom_ops as ops
16 from vllm.model_executor.layers.vocab_parallel_embedding import (
17 VocabParallelEmbedding,
18 get_masked_input_and_mask,
19 pad_vocab_size,
20 )
21 from vllm.platforms import current_platform
22 from vllm.utils.torch_utils import set_random_seed
23
24 requires_cuda = pytest.mark.skipif(
25 not current_platform.is_cuda(),
26 reason="the fused vocab-parallel embedding kernel is CUDA-only",
27 )
28
29
30 def _shard_indices(vocab_size, org_vocab_size, tp_rank, tp_size, pad=64):
31 return VocabParallelEmbedding._get_indices(
32 pad_vocab_size(vocab_size, pad),
33 pad_vocab_size(org_vocab_size, pad),
34 vocab_size,
35 org_vocab_size,
36 tp_rank,
37 tp_size,
38 )
39
40
41 def _reference(input_ids, weight, si):
42 """The path the kernel replaces."""
43 masked_input, input_mask = get_masked_input_and_mask(
44 input_ids,
45 si.org_vocab_start_index,
46 si.org_vocab_end_index,
47 si.num_org_vocab_padding,
48 si.added_vocab_start_index,
49 si.added_vocab_end_index,
50 )
51 out = torch.nn.functional.embedding(masked_input.long(), weight)
52 return out.masked_fill_(input_mask.unsqueeze(-1), 0)
53
54
55 def _fused(input_ids, weight, si):
56 return ops.vocab_parallel_embedding(
57 input_ids,
58 weight,
59 si.org_vocab_start_index,
60 si.org_vocab_end_index,
61 si.num_org_vocab_padding,
62 si.added_vocab_start_index,
63 si.added_vocab_end_index,
64 )
65
66
67 def _shard(full_table, si, rows_per_rank, dtype):
68 """Lay a rank's rows out the way the weight loader does."""
69 weight = torch.zeros(rows_per_rank, full_table.shape[1], dtype=dtype, device="cuda")
70 num_org = si.org_vocab_end_index - si.org_vocab_start_index
71 weight[:num_org] = full_table[si.org_vocab_start_index : si.org_vocab_end_index]
72 added_at = num_org + si.num_org_vocab_padding
73 num_added = si.added_vocab_end_index - si.added_vocab_start_index
74 weight[added_at : added_at + num_added] = full_table[
75 si.added_vocab_start_index : si.added_vocab_end_index
76 ]
77 return weight
78
79
80 @requires_cuda
81 @torch.inference_mode()
82 @pytest.mark.parametrize("tp_size", [2, 4, 8])
83 @pytest.mark.parametrize(
84 "vocab_size,org_vocab_size", [(128256, 128256), (32256, 32000)]
85 )
86 @pytest.mark.parametrize("hidden", [4096, 3584])
87 @pytest.mark.parametrize("id_dtype", [torch.int32, torch.int64])
88 def test_matches_masked_gather(tp_size, vocab_size, org_vocab_size, hidden, id_dtype):
89 """Every rank's partial output is bit-exact with the eager path, including
90 the added-vocab (LoRA) rows and the ids owned by another rank."""
91 set_random_seed(7)
92 dtype = torch.bfloat16
93 rows = pad_vocab_size(vocab_size, 64) // tp_size
94 full_table = torch.randn(pad_vocab_size(vocab_size, 64), hidden, dtype=dtype).cuda()
95 ids = torch.randint(0, vocab_size, (257,), dtype=id_dtype, device="cuda")
96
97 for tp_rank in range(tp_size):
98 si = _shard_indices(vocab_size, org_vocab_size, tp_rank, tp_size)
99 weight = _shard(full_table, si, rows, dtype)
100 torch.testing.assert_close(
101 _fused(ids, weight, si), _reference(ids, weight, si), atol=0.0, rtol=0.0
102 )
103
104
105 @requires_cuda
106 @torch.inference_mode()
107 @pytest.mark.parametrize("tp_size", [2, 8])
108 def test_rank_sum_is_full_lookup(tp_size):
109 """What the all-reduce sees: the partials add up to a full-table lookup."""
110 set_random_seed(11)
111 vocab_size, org_vocab_size, hidden = 32256, 32000, 2048
112 dtype = torch.bfloat16
113 rows = pad_vocab_size(vocab_size, 64) // tp_size
114 full_table = torch.randn(pad_vocab_size(vocab_size, 64), hidden, dtype=dtype).cuda()
115 # Ids from both the base and the added (LoRA) range.
116 ids = torch.randint(0, vocab_size, (129,), dtype=torch.int32, device="cuda")
117 ids[:16] = torch.randint(
118 org_vocab_size, vocab_size, (16,), dtype=torch.int32, device="cuda"
119 )
120
121 total = torch.zeros(ids.shape[0], hidden, dtype=dtype, device="cuda")
122 for tp_rank in range(tp_size):
123 si = _shard_indices(vocab_size, org_vocab_size, tp_rank, tp_size)
124 total += _fused(ids, _shard(full_table, si, rows, dtype), si)
125
126 torch.testing.assert_close(total, full_table[ids.long()], atol=0.0, rtol=0.0)