| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Benchmark the TP > 1 input-embedding path of VocabParallelEmbedding. |
| 4 | |
| 5 | Two providers, both computing one rank's partial embedding (the all-reduce |
| 6 | that follows is identical for each and is not measured): |
| 7 | |
| 8 | unfused mask + id shift + int64 cast + F.embedding + masked_fill_ |
| 9 | fused the single fused CUDA kernel, _C::vocab_parallel_embedding |
| 10 | |
| 11 | With --use-cuda-graph each provider is captured into a CUDA graph and the |
| 12 | replay is timed, which is what a decode step actually costs. |
| 13 | """ |
| 14 | |
| 15 | import itertools |
| 16 | |
| 17 | import torch |
| 18 | |
| 19 | from vllm import _custom_ops as vllm_ops |
| 20 | from vllm.model_executor.layers.vocab_parallel_embedding import ( |
| 21 | VocabParallelEmbedding, |
| 22 | get_masked_input_and_mask, |
| 23 | pad_vocab_size, |
| 24 | ) |
| 25 | from vllm.triton_utils import triton |
| 26 | |
| 27 | |
| 28 | def get_shard_indices(vocab_size, org_vocab_size, tp_rank, tp_size): |
| 29 | return VocabParallelEmbedding._get_indices( |
| 30 | pad_vocab_size(vocab_size), |
| 31 | pad_vocab_size(org_vocab_size), |
| 32 | vocab_size, |
| 33 | org_vocab_size, |
| 34 | tp_rank, |
| 35 | tp_size, |
| 36 | ) |
| 37 | |
| 38 | |
| 39 | def embedding_reference(input_ids, weight, shard_indices): |
| 40 | masked_input, input_mask = get_masked_input_and_mask( |
| 41 | input_ids, |
| 42 | shard_indices.org_vocab_start_index, |
| 43 | shard_indices.org_vocab_end_index, |
| 44 | shard_indices.num_org_vocab_padding, |
| 45 | shard_indices.added_vocab_start_index, |
| 46 | shard_indices.added_vocab_end_index, |
| 47 | ) |
| 48 | output = torch.nn.functional.embedding(masked_input.long(), weight) |
| 49 | return output.masked_fill_(input_mask.unsqueeze(-1), 0) |
| 50 | |
| 51 | |
| 52 | def embedding_fused(input_ids, weight, shard_indices): |
| 53 | return vllm_ops.vocab_parallel_embedding( |
| 54 | input_ids, |
| 55 | weight, |
| 56 | shard_indices.org_vocab_start_index, |
| 57 | shard_indices.org_vocab_end_index, |
| 58 | shard_indices.num_org_vocab_padding, |
| 59 | shard_indices.added_vocab_start_index, |
| 60 | shard_indices.added_vocab_end_index, |
| 61 | ) |
| 62 | |
| 63 | |
| 64 | PROVIDERS = { |
| 65 | "unfused": embedding_reference, |
| 66 | "fused": embedding_fused, |
| 67 | } |
| 68 | |
| 69 | |
| 70 | def make_inputs(vocab_size, hidden_size, tp_size, num_tokens, dtype, tp_rank=0): |
| 71 | shard_indices = get_shard_indices(vocab_size, vocab_size, tp_rank, tp_size) |
| 72 | weight = torch.randn( |
| 73 | pad_vocab_size(vocab_size) // tp_size, |
| 74 | hidden_size, |
| 75 | dtype=dtype, |
| 76 | device="cuda", |
| 77 | ) |
| 78 | input_ids = torch.randint( |
| 79 | 0, vocab_size, (num_tokens,), dtype=torch.int32, device="cuda" |
| 80 | ) |
| 81 | return input_ids, weight, shard_indices |
| 82 | |
| 83 | |
| 84 | def graph_replay(fn): |
| 85 | """Capture fn into a CUDA graph and return its replay.""" |
| 86 | stream = torch.cuda.Stream() |
| 87 | stream.wait_stream(torch.cuda.current_stream()) |
| 88 | with torch.cuda.stream(stream): |
| 89 | for _ in range(3): |
| 90 | fn() |
| 91 | torch.cuda.current_stream().wait_stream(stream) |
| 92 | graph = torch.cuda.CUDAGraph() |
| 93 | with torch.cuda.graph(graph): |
| 94 | fn() |
| 95 | return graph.replay |
| 96 | |
| 97 | |
| 98 | def calculate_diff(vocab_size, hidden_size, tp_size, num_tokens): |
| 99 | """The fused kernel must be bit-exact with the path it replaces, on every |
| 100 | rank of the shard.""" |
| 101 | dtype = torch.bfloat16 |
| 102 | for tp_rank in range(tp_size): |
| 103 | input_ids, weight, shard_indices = make_inputs( |
| 104 | vocab_size, hidden_size, tp_size, num_tokens, dtype, tp_rank |
| 105 | ) |
| 106 | output_reference = embedding_reference(input_ids, weight, shard_indices) |
| 107 | output_fused = embedding_fused(input_ids, weight, shard_indices) |
| 108 | if not torch.equal(output_reference, output_fused): |
| 109 | print(f"❌ rank {tp_rank}: fused output differs") |
| 110 | return |
| 111 | print(f"✅ fused matches the reference path on all {tp_size} ranks") |
| 112 | |
| 113 | |
| 114 | tp_size_range = [2, 4, 8] |
| 115 | num_tokens_range = [1, 8, 64, 512, 2048, 8192, 32768] |
| 116 | configs = list(itertools.product(tp_size_range, num_tokens_range)) |
| 117 | |
| 118 | |
| 119 | def get_benchmark(vocab_size, hidden_size, dtype, use_cuda_graph): |
| 120 | @triton.testing.perf_report( |
| 121 | triton.testing.Benchmark( |
| 122 | x_names=["tp_size", "num_tokens"], |
| 123 | x_vals=[list(_) for _ in configs], |
| 124 | line_arg="provider", |
| 125 | line_vals=list(PROVIDERS), |
| 126 | line_names=["Unfused PyTorch", "Fused kernel"], |
| 127 | styles=[("green", "-"), ("red", "-")], |
| 128 | ylabel="us", |
| 129 | plot_name=( |
| 130 | f"vocab-parallel-embedding-perf-vocab{vocab_size}-" |
| 131 | f"hidden{hidden_size}{'-cudagraph' if use_cuda_graph else ''}" |
| 132 | ), |
| 133 | args={}, |
| 134 | ) |
| 135 | ) |
| 136 | def benchmark(tp_size, num_tokens, provider): |
| 137 | input_ids, weight, shard_indices = make_inputs( |
| 138 | vocab_size, hidden_size, tp_size, num_tokens, dtype |
| 139 | ) |
| 140 | fn = PROVIDERS[provider] |
| 141 | |
| 142 | def run(): |
| 143 | return fn(input_ids, weight, shard_indices) |
| 144 | |
| 145 | if use_cuda_graph: |
| 146 | run = graph_replay(run) |
| 147 | |
| 148 | ms, min_ms, max_ms = triton.testing.do_bench(run, quantiles=[0.5, 0.2, 0.8]) |
| 149 | return 1000 * ms, 1000 * max_ms, 1000 * min_ms |
| 150 | |
| 151 | return benchmark |
| 152 | |
| 153 | |
| 154 | if __name__ == "__main__": |
| 155 | import argparse |
| 156 | |
| 157 | parser = argparse.ArgumentParser() |
| 158 | parser.add_argument( |
| 159 | "--vocab-size", type=int, default=128256, help="Vocabulary size" |
| 160 | ) |
| 161 | parser.add_argument( |
| 162 | "--hidden-size", type=int, default=4096, help="Embedding dimension" |
| 163 | ) |
| 164 | parser.add_argument( |
| 165 | "--dtype", type=str, default="bfloat16", help="Embedding table dtype" |
| 166 | ) |
| 167 | parser.add_argument( |
| 168 | "--tp-size", |
| 169 | type=int, |
| 170 | default=4, |
| 171 | help="TP size used for the correctness check", |
| 172 | ) |
| 173 | parser.add_argument( |
| 174 | "--use-cuda-graph", |
| 175 | action="store_true", |
| 176 | help="Time CUDA graph replay instead of eager launches", |
| 177 | ) |
| 178 | parser.add_argument( |
| 179 | "--save-path", |
| 180 | type=str, |
| 181 | default=None, |
| 182 | help="Optional path to save benchmark results", |
| 183 | ) |
| 184 | args = parser.parse_args() |
| 185 | dtype = getattr(torch, args.dtype) |
| 186 | |
| 187 | calculate_diff( |
| 188 | vocab_size=args.vocab_size, |
| 189 | hidden_size=args.hidden_size, |
| 190 | tp_size=args.tp_size, |
| 191 | num_tokens=1024, |
| 192 | ) |
| 193 | |
| 194 | benchmark = get_benchmark( |
| 195 | args.vocab_size, args.hidden_size, dtype, args.use_cuda_graph |
| 196 | ) |
| 197 | benchmark.run(print_data=True, save_path=args.save_path) |