main
py 523 lines 17.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test the piecewise compilation with a simple model, comparing the output
4 with and without the piecewise compilation.
5
6 This is a tractable model, the weights and computation are specially designed
7 if the config `tractable_init` is set to True. Otherwise, the weights are
8 initialized randomly with a fixed seed.
9 """
10
11 from copy import deepcopy
12 from dataclasses import dataclass
13 from typing import Any
14
15 import pytest
16 import torch
17 from torch import nn
18
19 from vllm.compilation.decorators import support_torch_compile
20 from vllm.config import (
21 CompilationConfig,
22 CompilationMode,
23 CUDAGraphMode,
24 VllmConfig,
25 set_current_vllm_config,
26 )
27 from vllm.forward_context import BatchDescriptor, set_forward_context
28 from vllm.utils.torch_utils import is_torch_equal_or_newer
29
30 from ...utils import create_new_process_for_each_test
31
32 # This import automatically registers `torch.ops.silly.attention`
33 from .. import silly_attention # noqa: F401
34
35
36 @dataclass
37 class LlamaConfig:
38 hidden_size: int = 128
39 mlp_size: int = 256
40 vocab_size: int = 128
41 num_layers: int = 2
42 init_value: float = 1.0
43 tractable_init: bool = False
44 random_seed: int = 0
45
46 def compute_hash(self) -> str:
47 factors: list[Any] = []
48 for k, v in self.__dict__.items():
49 if k == "random_seed":
50 continue
51 factors.append((k, v))
52 factors.sort()
53 import hashlib
54
55 return hashlib.md5(str(factors).encode(), usedforsecurity=False).hexdigest()
56
57 def __post_init__(self):
58 assert self.mlp_size >= self.hidden_size
59
60
61 class LlamaMLP(nn.Module):
62 def __init__(self, config: LlamaConfig) -> None:
63 super().__init__()
64 self.gate_up_projection = nn.Linear(
65 in_features=config.hidden_size,
66 out_features=config.mlp_size * 2,
67 bias=False,
68 )
69 self.down_projection = nn.Linear(
70 in_features=config.mlp_size,
71 out_features=config.hidden_size,
72 bias=False,
73 )
74
75 if config.tractable_init:
76 nn.init.eye_(self.gate_up_projection.weight.data[: config.mlp_size])
77 nn.init.eye_(self.gate_up_projection.weight.data[config.mlp_size :])
78 nn.init.eye_(self.down_projection.weight.data)
79 else:
80 nn.init.xavier_normal_(
81 self.gate_up_projection.weight.data,
82 generator=torch.Generator().manual_seed(config.random_seed),
83 gain=0.001,
84 )
85 nn.init.xavier_normal_(
86 self.down_projection.weight.data,
87 generator=torch.Generator().manual_seed(config.random_seed),
88 gain=0.001,
89 )
90
91 def forward(self, x):
92 # for tractable_init and positive input, this is
93 # essentially an elementwise-square
94 x = self.gate_up_projection(x)
95 x = x[:, : x.size(1) // 2] * torch.nn.functional.relu(x[:, x.size(1) // 2 :])
96 x = self.down_projection(x)
97 return x
98
99
100 class LlamaAttention(nn.Module):
101 def __init__(self, config: LlamaConfig) -> None:
102 super().__init__()
103 self.qkv_projection = nn.Linear(
104 in_features=config.hidden_size,
105 out_features=config.hidden_size * 3,
106 bias=False,
107 )
108
109 self.output_projection = nn.Linear(
110 in_features=config.hidden_size,
111 out_features=config.hidden_size,
112 bias=False,
113 )
114
115 if config.tractable_init:
116 nn.init.eye_(self.qkv_projection.weight.data[: config.hidden_size])
117 nn.init.eye_(
118 self.qkv_projection.weight.data[
119 config.hidden_size : 2 * config.hidden_size
120 ]
121 )
122 nn.init.eye_(self.qkv_projection.weight.data[2 * config.hidden_size :])
123 nn.init.eye_(self.output_projection.weight.data)
124 else:
125 nn.init.xavier_normal_(
126 self.qkv_projection.weight.data,
127 generator=torch.Generator().manual_seed(config.random_seed),
128 gain=0.001,
129 )
130 nn.init.xavier_normal_(
131 self.output_projection.weight.data,
132 generator=torch.Generator().manual_seed(config.random_seed),
133 gain=0.001,
134 )
135
136 def forward(
137 self,
138 positions: torch.Tensor,
139 hidden_states: torch.Tensor,
140 ) -> torch.Tensor:
141 # for tractable_init, this is:
142 # output = (hidden_states * 3 + positions * 2)
143 qkv = self.qkv_projection(hidden_states)
144 hidden_size = qkv.size(-1) // 3
145 q, k, v = qkv.split([hidden_size, hidden_size, hidden_size], dim=-1)
146
147 q = q + positions.unsqueeze(1)
148 k = k + positions.unsqueeze(1)
149
150 attn_output = torch.empty_like(q)
151 torch.ops.silly.attention(q, k, v, attn_output)
152
153 output = self.output_projection(attn_output)
154 return output
155
156
157 class LlamaDecoderLayer(nn.Module):
158 def __init__(self, config: LlamaConfig) -> None:
159 super().__init__()
160 self.self_attention = LlamaAttention(config)
161 self.mlp = LlamaMLP(config)
162
163 def forward(
164 self,
165 positions: torch.Tensor,
166 hidden_states: torch.Tensor,
167 residual: torch.Tensor | None,
168 ) -> tuple[torch.Tensor, torch.Tensor]:
169 """
170 For tractable computation:
171 - if residual is None, the outputs are:
172 - residual = (hidden_states + 1) * 3 + positions * 2 + hidden_states = hidden_states * 4 + positions * 2 + 3
173 - hidden_states = (residual + 1) ** 2
174 - if residual is not None, the outputs are:
175 - residual = (hidden_states + residual + 1) * 3 + positions * 2 + hidden_states + residual = (hidden_states + residual) * 4 + positions * 2 + 3
176 - hidden_states = (residual + 1) ** 2
177 """ # noqa
178 if residual is None:
179 residual = hidden_states
180 hidden_states = hidden_states + 1
181 else:
182 hidden_states = hidden_states + residual
183 residual = hidden_states
184 hidden_states = hidden_states + 1
185
186 hidden_states = self.self_attention(
187 positions=positions, hidden_states=hidden_states
188 )
189
190 hidden_states = hidden_states + residual
191 residual = hidden_states
192 hidden_states = hidden_states + 1
193 hidden_states = self.mlp(hidden_states)
194
195 return hidden_states, residual
196
197
198 @support_torch_compile
199 class LlamaModel(nn.Module):
200 def __init__(
201 self,
202 *,
203 vllm_config: VllmConfig,
204 config: LlamaConfig,
205 prefix: str = "",
206 **kwargs,
207 ) -> None:
208 super().__init__()
209 self.embedding_tokens = nn.Embedding(
210 num_embeddings=config.vocab_size,
211 embedding_dim=config.hidden_size,
212 )
213 self.layers = nn.ModuleList(
214 [LlamaDecoderLayer(config) for _ in range(config.num_layers)]
215 )
216
217 # this is the initial value of the hidden states
218 self.embedding_tokens.weight.data.fill_(config.init_value)
219
220 def forward(
221 self,
222 input_ids: torch.Tensor | None,
223 positions: torch.Tensor,
224 ) -> torch.Tensor:
225 hidden_states = self.embedding_tokens(input_ids)
226 residual = None
227 for layer in self.layers:
228 hidden_states, residual = layer(positions, hidden_states, residual)
229 return hidden_states
230
231
232 def tractable_computation(
233 input_ids: torch.Tensor,
234 positions: torch.Tensor,
235 config: LlamaConfig,
236 init_value: float = 1.0,
237 ) -> torch.Tensor:
238 hidden_states = (
239 torch.ones(
240 input_ids.size(0),
241 config.hidden_size,
242 device=input_ids.device,
243 dtype=input_ids.dtype,
244 )
245 * init_value
246 )
247
248 # first layer
249 residual = hidden_states * 4 + positions.unsqueeze(1) * 2 + 3
250 hidden_states = (residual + 1) ** 2
251
252 # following layers
253 for _ in range(config.num_layers - 1):
254 hidden_states = hidden_states + residual
255 residual = hidden_states * 4 + positions.unsqueeze(1) * 2 + 3
256 hidden_states = (residual + 1) ** 2
257
258 return hidden_states
259
260
261 @torch.inference_mode
262 def run_model(llama_config, compile_config: CompilationConfig) -> torch.Tensor:
263 # Start with a fresh copy to make sure there's no cache dir sharing
264 compile_config = deepcopy(compile_config)
265 cudagraph_runtime_mode = compile_config.cudagraph_mode
266
267 vllm_config = VllmConfig(
268 compilation_config=compile_config, additional_config=llama_config
269 )
270 with set_current_vllm_config(vllm_config):
271 model = (
272 LlamaModel(config=llama_config, vllm_config=vllm_config, prefix="")
273 .eval()
274 .cuda()
275 )
276
277 with set_forward_context({}, vllm_config=vllm_config): # background context
278 B = 16 # max batch size
279 input_ids = torch.randint(0, llama_config.vocab_size, (B,)).cuda()
280 positions = torch.arange(B).cuda()
281
282 # warmup for the model with cudagraph_mode NONE
283 model(input_ids, positions)
284
285 # simulate cudagraphs capturing
286 with set_forward_context(
287 {},
288 vllm_config=vllm_config,
289 cudagraph_runtime_mode=cudagraph_runtime_mode,
290 batch_descriptor=BatchDescriptor(
291 num_tokens=2,
292 ),
293 ):
294 model(input_ids[:2], positions[:2])
295 with set_forward_context(
296 {},
297 vllm_config=vllm_config,
298 cudagraph_runtime_mode=cudagraph_runtime_mode,
299 batch_descriptor=BatchDescriptor(
300 num_tokens=1,
301 ),
302 ):
303 model(input_ids[:1], positions[:1])
304
305 input_ids[:2].zero_()
306 # simulate cudagraphs replay
307 with set_forward_context(
308 {},
309 vllm_config=vllm_config,
310 cudagraph_runtime_mode=cudagraph_runtime_mode,
311 batch_descriptor=BatchDescriptor(
312 num_tokens=2,
313 ),
314 ):
315 output = model(input_ids[:2], positions[:2])
316
317 output = output.cpu()
318
319 if llama_config.tractable_init:
320 expected_output = tractable_computation(
321 input_ids[:2], positions[:2], llama_config
322 ).cpu()
323
324 assert torch.allclose(output, expected_output)
325 else:
326 return output.cpu()
327
328
329 @pytest.mark.parametrize(
330 "backend, use_inductor_graph_partition",
331 [
332 ("eager", False), # No inductor
333 ("inductor", False), # Inductor, Dynamo partition
334 ("inductor", True), # Inductor, Inductor partition
335 ],
336 )
337 @create_new_process_for_each_test("spawn")
338 def test_toy_llama(
339 backend: str, use_inductor_graph_partition: bool, monkeypatch, tmp_path
340 ):
341 from vllm.compilation.counter import compilation_counter
342
343 # We disable the vLLM compile cache into a new tmp dir for 1 reason:
344 # 1. To make sure we can properly track the number of Inductor compilations.
345 monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
346
347 if use_inductor_graph_partition and not is_torch_equal_or_newer("2.9.0.dev"):
348 pytest.skip("Inductor graph partition only supported in torch>=2.9")
349
350 # compare output with and without piecewise compilation
351
352 llama_config = LlamaConfig(
353 hidden_size=128, mlp_size=256, vocab_size=128, num_layers=12
354 )
355
356 tractable_config = LlamaConfig(
357 hidden_size=128, mlp_size=256, vocab_size=128, num_layers=2, tractable_init=True
358 )
359
360 compile_config_no_compile = CompilationConfig(
361 mode=CompilationMode.NONE,
362 cudagraph_mode=CUDAGraphMode.NONE,
363 backend="eager",
364 )
365
366 compile_config_no_split = CompilationConfig(
367 mode=CompilationMode.VLLM_COMPILE,
368 use_inductor_graph_partition=use_inductor_graph_partition,
369 cudagraph_mode=CUDAGraphMode.PIECEWISE,
370 backend=backend,
371 cudagraph_capture_sizes=[1, 2],
372 )
373
374 compile_config_split = deepcopy(compile_config_no_split)
375 compile_config_split.splitting_ops = ["silly::attention"]
376
377 outputs = []
378 with compilation_counter.expect(
379 num_graphs_seen=0,
380 num_piecewise_graphs_seen=0,
381 num_piecewise_capturable_graphs_seen=0,
382 num_backend_compilations=0,
383 num_cudagraph_captured=0,
384 ):
385 outputs.append(run_model(llama_config, compile_config_no_compile))
386
387 run_model(tractable_config, compile_config_no_compile)
388
389 if backend == "inductor":
390 kwargs = {"num_inductor_compiles": 1, "num_eager_compiles": 0}
391 else:
392 kwargs = {"num_eager_compiles": 1, "num_inductor_compiles": 0}
393
394 with compilation_counter.expect(
395 num_graphs_seen=1, # one graph for the model
396 num_piecewise_graphs_seen=1,
397 num_piecewise_capturable_graphs_seen=1,
398 num_backend_compilations=1, # num_piecewise_capturable_graphs_seen
399 num_cudagraph_captured=2,
400 **kwargs,
401 ):
402 outputs.append(run_model(llama_config, compile_config_no_split))
403
404 run_model(tractable_config, compile_config_no_split)
405
406 if use_inductor_graph_partition:
407 num_piecewise_fx = 1
408 num_piecewise_capturable_fx = 1
409 else:
410 num_piecewise_fx = 2 * llama_config.num_layers + 1
411 num_piecewise_capturable_fx = 1 + llama_config.num_layers
412
413 with compilation_counter.expect(
414 num_graphs_seen=1, # one graph for the model
415 num_piecewise_graphs_seen=num_piecewise_fx,
416 num_piecewise_capturable_graphs_seen=num_piecewise_capturable_fx,
417 num_backend_compilations=num_piecewise_capturable_fx,
418 # num_cudagraph_sizes * num_partitions
419 num_cudagraph_captured=2 * (1 + llama_config.num_layers),
420 ):
421 outputs.append(run_model(llama_config, compile_config_split))
422 run_model(tractable_config, compile_config_split)
423
424 for i in range(1, len(outputs)):
425 assert torch.allclose(outputs[0], outputs[i])
426
427
428 @torch.inference_mode
429 def benchmark():
430 from triton.testing import do_bench
431
432 # similar to llama 3.1-8B
433 llama_config = LlamaConfig(
434 hidden_size=4096, mlp_size=14336, vocab_size=128 * 1024, num_layers=32
435 )
436
437 # a tiny model to measure the overhead
438 # of piecewise cudagraph
439 llama_config = LlamaConfig(
440 hidden_size=40, mlp_size=80, vocab_size=128, num_layers=2
441 )
442
443 cudagraph_sizes = [1, 2, 4] + [i * 8 for i in range(1, 33)]
444
445 eager_time = {}
446 full_cudagraph_time = {}
447 piecewise_cudagraph_time = {}
448
449 pool = torch.cuda.graph_pool_handle()
450
451 for piecewise in [False, True]:
452 if piecewise:
453 compilation_config = CompilationConfig(
454 mode=CompilationMode.VLLM_COMPILE,
455 splitting_ops=["silly::attention"],
456 cudagraph_capture_sizes=cudagraph_sizes,
457 )
458 else:
459 compilation_config = CompilationConfig(
460 mode=CompilationMode.VLLM_COMPILE,
461 cudagraph_capture_sizes=cudagraph_sizes,
462 )
463
464 vllm_config = VllmConfig(compilation_config=compilation_config)
465 with set_current_vllm_config(vllm_config):
466 model = (
467 LlamaModel(config=llama_config, vllm_config=vllm_config, prefix="")
468 .eval()
469 .cuda()
470 .to(torch.bfloat16)
471 )
472
473 B = 256 # max batch size
474 input_ids = torch.randint(0, llama_config.vocab_size, (B,)).cuda()
475 positions = torch.arange(B).cuda().to(torch.bfloat16)
476
477 graphs = {}
478
479 model(input_ids, positions)
480 for b in cudagraph_sizes[::-1]:
481 if not piecewise:
482 graph = torch.cuda.CUDAGraph()
483 with torch.cuda.graph(graph, pool=pool):
484 output = model(input_ids[:b], positions[:b])
485 graphs[b] = (graph, output)
486 else:
487 output = model(input_ids[:b], positions[:b])
488 graphs[b] = (model, output)
489 for b in cudagraph_sizes:
490 if piecewise:
491 # noqa is for `Function definition does not bind loop variable`
492 # it will be problematic if we save the created lambda function
493 # and use it later, because it will look up the name `b` in the
494 # enclosing scope, and the value of `b` will always be 256.
495 # it is fine here, because we only use the lambda function once.
496 runtime = do_bench(
497 lambda: graphs[b][0]( # noqa
498 input_ids[:b], # noqa
499 positions[:b], # noqa
500 )
501 )
502 piecewise_cudagraph_time[b] = runtime
503 else:
504 runtime = do_bench(lambda: graphs[b][0].replay()) # noqa
505 eager_runtime = do_bench(lambda: model(input_ids[:b], positions[:b])) # noqa
506 full_cudagraph_time[b] = runtime
507 eager_time[b] = eager_runtime
508
509 # print in tabular format
510 print("batch size\teager mode\tfull cudagraph\tpiecewise cudagraph")
511 for b in cudagraph_sizes:
512 print(
513 f"{b}\t{eager_time[b]:.3f}\t{full_cudagraph_time[b]:.3f}"
514 f"\t{piecewise_cudagraph_time[b]:.3f}"
515 )
516
517
518 if __name__ == "__main__":
519 # Protect against subprocess reimport when using spawn_new_process_for_each_test
520 import os
521
522 if os.environ.get("RUNNING_IN_SUBPROCESS") != "1":
523 benchmark()