main
py 325 lines 10.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test (piecewise) compilation with a simple model where multiple submodules
4 are compiled and graph captured separately.
5 """
6
7 import pytest
8 import torch
9 from torch import nn
10
11 from vllm.compilation.backends import set_model_tag
12 from vllm.compilation.counter import compilation_counter
13 from vllm.compilation.decorators import ignore_torch_compile, support_torch_compile
14 from vllm.config import (
15 CompilationConfig,
16 CompilationMode,
17 CUDAGraphMode,
18 VllmConfig,
19 set_current_vllm_config,
20 )
21 from vllm.forward_context import BatchDescriptor, set_forward_context
22 from vllm.utils.torch_utils import is_torch_equal_or_newer
23
24 from ...utils import create_new_process_for_each_test
25
26 # This import automatically registers `torch.ops.silly.attention`
27 from .. import silly_attention # noqa: F401
28
29 BATCH_SIZE = 32
30 MLP_SIZE = 128
31 HIDDEN_SIZE = 1024
32 RANDOM_SEED = 0
33
34
35 @support_torch_compile
36 class ParentModel(nn.Module):
37 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "", **kwargs) -> None:
38 super().__init__()
39
40 def forward(self, x: torch.Tensor) -> torch.Tensor:
41 return x
42
43
44 class Attention(nn.Module):
45 def __init__(self, mlp_size: int, hidden_size: int) -> None:
46 super().__init__()
47 self.pre_attn = nn.Linear(mlp_size, hidden_size, bias=False)
48 self.post_attn = nn.Linear(hidden_size, mlp_size, bias=False)
49 self.rms_norm_weight = nn.Parameter(torch.ones(hidden_size))
50
51 # Initialize to same weights for testing
52 nn.init.xavier_normal_(
53 self.pre_attn.weight.data,
54 generator=torch.Generator().manual_seed(RANDOM_SEED),
55 gain=0.001,
56 )
57 nn.init.xavier_normal_(
58 self.post_attn.weight.data,
59 generator=torch.Generator().manual_seed(RANDOM_SEED),
60 gain=0.001,
61 )
62
63 def rms_norm_ref(self, x: torch.Tensor) -> torch.Tensor:
64 x_f32 = x.float()
65 return (
66 x_f32
67 * torch.rsqrt(torch.mean(x_f32.square(), dim=-1, keepdim=True) + 1e-6)
68 * self.rms_norm_weight
69 ).to(x.dtype)
70
71 def forward(self, x: torch.Tensor) -> torch.Tensor:
72 x = self.pre_attn(x)
73 x = self.rms_norm_ref(x)
74 attn_output = torch.empty_like(x)
75 torch.ops.silly.attention(x, x, x, attn_output)
76 x = attn_output
77 x = self.rms_norm_ref(x)
78 x = self.post_attn(x)
79 return x
80
81
82 @support_torch_compile
83 class CompiledAttention(nn.Module):
84 def __init__(
85 self,
86 *,
87 mlp_size: int,
88 hidden_size: int,
89 vllm_config: VllmConfig,
90 prefix: str = "",
91 **kwargs,
92 ) -> None:
93 super().__init__()
94 self.attn = Attention(mlp_size, hidden_size)
95
96 def forward(self, x: torch.Tensor) -> torch.Tensor:
97 return self.attn(x)
98
99
100 @support_torch_compile
101 class CompiledAttentionTwo(CompiledAttention):
102 def forward(self, x: torch.Tensor) -> torch.Tensor:
103 return self.attn(x) + x
104
105
106 @ignore_torch_compile
107 class SimpleModelWithTwoGraphs(ParentModel):
108 def __init__(
109 self,
110 *,
111 mlp_size: int,
112 hidden_size: int,
113 vllm_config: VllmConfig,
114 prefix: str = "",
115 **kwargs,
116 ) -> None:
117 super().__init__(vllm_config=vllm_config, prefix=prefix)
118 # Test will fail without set_model_tag here with error:
119 # "ValueError: too many values to unpack (expected 3)"
120 # This is because CompiledAttention and CompiledAttentionTwo
121 # have different implementations but the same torch.compile
122 # cache dir will be used as default prefix is 'model_tag'
123 with set_model_tag("attn_one"):
124 self.attn_one = CompiledAttention(
125 mlp_size=mlp_size,
126 hidden_size=hidden_size,
127 vllm_config=vllm_config,
128 prefix=f"{prefix}.attn_one",
129 )
130 with set_model_tag("attn_two"):
131 self.attn_two = CompiledAttentionTwo(
132 mlp_size=mlp_size,
133 hidden_size=hidden_size,
134 vllm_config=vllm_config,
135 prefix=f"{prefix}.attn_two",
136 )
137
138 self.hidden_states = torch.zeros((BATCH_SIZE, MLP_SIZE)).cuda()
139
140 def forward(self, x: torch.Tensor) -> torch.Tensor:
141 bsz = x.shape[0]
142 # CUDAGraph expects same tensor addresses for each run
143 self.hidden_states[:bsz].copy_(x)
144 x = self.attn_one(self.hidden_states[:bsz])
145 self.hidden_states[:bsz].copy_(x)
146 x = self.attn_two(self.hidden_states[:bsz])
147 return x
148
149
150 @torch.inference_mode
151 def run_model(
152 vllm_config: VllmConfig,
153 model: nn.Module,
154 inputs: torch.Tensor,
155 cudagraph_runtime_mode: CUDAGraphMode,
156 ):
157 with set_forward_context({}, vllm_config=vllm_config):
158 # warmup for the model with cudagraph_mode NONE
159 model(inputs)
160
161 # simulate cudagraphs capturing
162 with set_forward_context(
163 {},
164 vllm_config=vllm_config,
165 cudagraph_runtime_mode=cudagraph_runtime_mode,
166 batch_descriptor=BatchDescriptor(
167 num_tokens=2,
168 ),
169 ):
170 model(inputs[:2])
171 with set_forward_context(
172 {},
173 vllm_config=vllm_config,
174 cudagraph_runtime_mode=cudagraph_runtime_mode,
175 batch_descriptor=BatchDescriptor(
176 num_tokens=1,
177 ),
178 ):
179 model(inputs[:1])
180
181 # simulate cudagraphs replay
182 with set_forward_context(
183 {},
184 vllm_config=vllm_config,
185 cudagraph_runtime_mode=cudagraph_runtime_mode,
186 batch_descriptor=BatchDescriptor(
187 num_tokens=2,
188 ),
189 ):
190 output = model(inputs[:2])
191
192 output = output.cpu()
193 return output.cpu()
194
195
196 @pytest.mark.parametrize("use_inductor_graph_partition", [False, True])
197 @pytest.mark.parametrize("use_bytecode_hook", [True, False])
198 @create_new_process_for_each_test("spawn")
199 def test_multi_graph_piecewise_compile(
200 use_inductor_graph_partition: bool, use_bytecode_hook: bool, monkeypatch
201 ):
202 # Set the environment variable for this test
203 monkeypatch.setenv("VLLM_USE_BYTECODE_HOOK", "1" if use_bytecode_hook else "0")
204
205 if use_inductor_graph_partition and not is_torch_equal_or_newer("2.9.0.dev"):
206 pytest.skip("inductor graph partition is only available in PyTorch 2.9+")
207
208 outputs = []
209
210 # vllmcompile compile
211 vllm_config = VllmConfig(
212 compilation_config=CompilationConfig(
213 mode=CompilationMode.VLLM_COMPILE,
214 cudagraph_mode=CUDAGraphMode.PIECEWISE,
215 splitting_ops=["silly::attention"],
216 cudagraph_capture_sizes=[1, 2],
217 use_inductor_graph_partition=use_inductor_graph_partition,
218 )
219 )
220 cudagraph_runtime_mode = CUDAGraphMode.PIECEWISE
221
222 with set_current_vllm_config(vllm_config):
223 model = (
224 SimpleModelWithTwoGraphs(
225 mlp_size=MLP_SIZE,
226 hidden_size=HIDDEN_SIZE,
227 vllm_config=vllm_config,
228 prefix="",
229 )
230 .eval()
231 .cuda()
232 )
233
234 # Pre-allocate memory for CUDAGraph which expects
235 # static tensor addresses
236 inputs = torch.randn(BATCH_SIZE, MLP_SIZE).cuda()
237
238 if use_inductor_graph_partition:
239 # Splitting happens at Inductor lowering level,
240 # total piecewise fx graphs is equal to total graphs
241 num_piecewise_fx = 2
242 num_piecewise_capturable_fx = 2
243 else:
244 # attn_one, attn_two each has 3 piecewise graphs
245 # (pre attn, post attn, silly_attention) each
246 num_piecewise_fx = 6
247 # attn_one, attn_two has pre attn and post attn each, total=4
248 num_piecewise_capturable_fx = 4
249
250 with compilation_counter.expect(
251 num_graphs_seen=2, # two graphs for the model
252 num_piecewise_graphs_seen=num_piecewise_fx,
253 num_piecewise_capturable_graphs_seen=num_piecewise_capturable_fx,
254 num_backend_compilations=num_piecewise_capturable_fx,
255 num_cudagraph_captured=8, # num_cudagraph_sizes * num_partitions
256 ):
257 outputs.append(run_model(vllm_config, model, inputs, cudagraph_runtime_mode))
258
259 # no compile or cudagraph
260 vllm_config = VllmConfig(
261 compilation_config=CompilationConfig(
262 mode=CompilationMode.NONE,
263 )
264 )
265 cudagraph_runtime_mode = CUDAGraphMode.NONE
266
267 with set_current_vllm_config(vllm_config):
268 model = (
269 SimpleModelWithTwoGraphs(
270 mlp_size=MLP_SIZE,
271 hidden_size=HIDDEN_SIZE,
272 vllm_config=vllm_config,
273 prefix="",
274 )
275 .eval()
276 .cuda()
277 )
278
279 with compilation_counter.expect(
280 num_graphs_seen=0,
281 num_piecewise_graphs_seen=0,
282 num_piecewise_capturable_graphs_seen=0,
283 num_backend_compilations=0,
284 num_cudagraph_captured=0,
285 ):
286 outputs.append(run_model(vllm_config, model, inputs, cudagraph_runtime_mode))
287
288 # piecewise compile without CUDA graph
289 vllm_config = VllmConfig(
290 compilation_config=CompilationConfig(
291 mode=CompilationMode.VLLM_COMPILE,
292 cudagraph_mode=CUDAGraphMode.NONE,
293 splitting_ops=["silly::attention"],
294 use_inductor_graph_partition=use_inductor_graph_partition,
295 )
296 )
297 cudagraph_runtime_mode = CUDAGraphMode.PIECEWISE
298
299 with set_current_vllm_config(vllm_config):
300 model = (
301 SimpleModelWithTwoGraphs(
302 mlp_size=MLP_SIZE,
303 hidden_size=HIDDEN_SIZE,
304 vllm_config=vllm_config,
305 prefix="",
306 )
307 .eval()
308 .cuda()
309 )
310
311 with compilation_counter.expect(
312 num_graphs_seen=2,
313 num_piecewise_graphs_seen=num_piecewise_fx,
314 num_piecewise_capturable_graphs_seen=num_piecewise_capturable_fx,
315 num_backend_compilations=num_piecewise_capturable_fx,
316 num_cudagraph_captured=0, # no cudagraph captured
317 ):
318 outputs.append(run_model(vllm_config, model, inputs, cudagraph_runtime_mode))
319
320 # Generally don't expect outputs with and without inductor
321 # to be bitwise equivalent
322 assert torch.allclose(outputs[0], outputs[1])
323
324 # Expect bitwise equivalence using inductor w/ and w/o cudagraph
325 assert torch.equal(outputs[0], outputs[2])