main
py 892 lines 31.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import hashlib
5 import os
6 import pickle
7 import tempfile
8 from contextlib import contextmanager
9 from pathlib import Path
10 from unittest.mock import Mock, patch
11
12 import pytest
13 import torch
14
15 import vllm.envs as envs
16 import vllm.kernels # noqa: F401 to register kernels
17 from vllm import ir
18 from vllm.compilation.backends import VllmBackend
19 from vllm.compilation.caching import (
20 StandaloneCompiledArtifacts,
21 VllmSerializableFunction,
22 )
23 from vllm.compilation.counter import compilation_counter
24 from vllm.compilation.decorators import support_torch_compile
25 from vllm.config import (
26 CompilationConfig,
27 CompilationMode,
28 VllmConfig,
29 set_current_vllm_config,
30 )
31 from vllm.envs import disable_envs_cache
32 from vllm.forward_context import set_forward_context
33 from vllm.ir import ops
34 from vllm.model_executor.layers.activation import GeluAndMulSparse
35 from vllm.platforms import current_platform
36 from vllm.utils.torch_utils import is_torch_equal_or_newer
37
38 from ..utils import create_new_process_for_each_test
39
40
41 @pytest.fixture
42 def vllm_tmp_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
43 """Fixture that sets VLLM_CACHE_ROOT to a temporary directory."""
44 monkeypatch.setenv("VLLM_CACHE_ROOT", str(tmp_path / "vllm_cache"))
45 return tmp_path
46
47
48 def reference_fn(x: torch.Tensor):
49 assert x.shape[0] <= 42
50 assert x.shape[0] % 2 == 0
51 for _ in range(30):
52 x = x + x.shape[0]
53 return x
54
55
56 def reference_fn_tuple(x: torch.Tensor):
57 """Reference function that returns a tuple of tensors."""
58 assert x.shape[0] <= 42
59 assert x.shape[0] % 2 == 0
60 for _ in range(30):
61 x = x + x.shape[0]
62 return x, x * 2
63
64
65 @support_torch_compile
66 class CompiledMod(torch.nn.Module):
67 def __init__(self, **kwargs):
68 super().__init__()
69
70 def forward(self, x: torch.Tensor):
71 return reference_fn(x)
72
73
74 @support_torch_compile
75 class CompiledModTuple(torch.nn.Module):
76 """A compiled module that returns a tuple of tensors."""
77
78 def __init__(self, **kwargs):
79 super().__init__()
80
81 def forward(self, x: torch.Tensor):
82 return reference_fn_tuple(x)
83
84
85 @support_torch_compile(dynamic_arg_dims={"x": 0})
86 class SparseActivationAotModel(torch.nn.Module):
87 def __init__(self, **kwargs):
88 super().__init__()
89 self.up_proj = torch.nn.Linear(32, 256, bias=False)
90 self.activation = GeluAndMulSparse(0.95, "tanh")
91 self.down_proj = torch.nn.Linear(128, 16, bias=False)
92
93 def forward(self, x: torch.Tensor):
94 return self.down_proj(self.activation(self.up_proj(x)))
95
96
97 def make_vllm_config() -> VllmConfig:
98 return VllmConfig(
99 compilation_config=CompilationConfig(
100 mode=CompilationMode.VLLM_COMPILE,
101 backend="inductor",
102 )
103 )
104
105
106 @contextmanager
107 def use_vllm_config(vllm_config: VllmConfig):
108 with set_forward_context({}, vllm_config), set_current_vllm_config(vllm_config):
109 yield
110
111
112 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
113 def test_no_dynamo_cache_entry(monkeypatch: pytest.MonkeyPatch):
114 with monkeypatch.context() as m:
115 vllm_config = make_vllm_config()
116 args = (torch.randn(10, 10),)
117 expected = reference_fn(*args)
118 with use_vllm_config(vllm_config):
119 m.setenv("VLLM_USE_AOT_COMPILE", "0")
120 m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
121 m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
122 with (
123 pytest.raises(RuntimeError, match="Detected recompile"),
124 torch.compiler.set_stance("fail_on_recompile"),
125 ):
126 CompiledMod(vllm_config=vllm_config)(*args)
127 disable_envs_cache()
128
129 m.setenv("VLLM_USE_AOT_COMPILE", "1")
130 torch._dynamo.reset()
131 with torch.compiler.set_stance("fail_on_recompile"):
132 actual = CompiledMod(vllm_config=vllm_config)(*args)
133 assert torch.allclose(actual, expected)
134
135
136 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
137 def test_force_aot_load(monkeypatch: pytest.MonkeyPatch):
138 with tempfile.TemporaryDirectory() as tmpdirname, monkeypatch.context() as m:
139 args = (torch.randn(10, 10),)
140 m.setenv("VLLM_USE_AOT_COMPILE", "1")
141 m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
142 m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
143 m.setenv("VLLM_FORCE_AOT_LOAD", "1")
144 m.setenv("VLLM_CACHE_ROOT", tmpdirname)
145 vllm_config = make_vllm_config()
146 with use_vllm_config(vllm_config), pytest.raises(FileNotFoundError):
147 CompiledMod(vllm_config=vllm_config)(*args)
148
149
150 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
151 def test_save_and_load(monkeypatch: pytest.MonkeyPatch):
152 with monkeypatch.context() as m:
153 args = (torch.randn(10, 10),)
154
155 with tempfile.TemporaryDirectory() as tmpdirname:
156 m.setenv("VLLM_CACHE_ROOT", tmpdirname)
157 m.setenv("VLLM_USE_AOT_COMPILE", "1")
158 m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
159 m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
160 disable_envs_cache()
161 vllm_config = make_vllm_config()
162 with (
163 use_vllm_config(vllm_config),
164 compilation_counter.expect(
165 num_aot_compiles=1,
166 num_aot_artifacts_saved=1,
167 num_aot_artifacts_loaded=0,
168 ),
169 ):
170 compiled_mod = CompiledMod(vllm_config=vllm_config)
171 expected = compiled_mod(*args)
172 assert isinstance(expected, torch.Tensor)
173
174 disable_envs_cache()
175
176 m.setenv("VLLM_FORCE_AOT_LOAD", "1")
177 vllm_config = make_vllm_config()
178 with (
179 use_vllm_config(vllm_config),
180 compilation_counter.expect(
181 num_aot_compiles=0,
182 num_aot_artifacts_saved=0,
183 num_aot_artifacts_loaded=1,
184 ),
185 ):
186 cached_mod = CompiledMod(vllm_config=vllm_config)
187 ret = cached_mod(*args)
188 assert isinstance(ret, torch.Tensor)
189 assert cached_mod.was_aot_compile_fn_loaded_from_disk, (
190 "Expected was_aot_compile_fn_loaded_from_disk to be True"
191 )
192 assert torch.allclose(ret, expected)
193
194
195 @pytest.mark.skipif(
196 not current_platform.is_cuda() or not is_torch_equal_or_newer("2.10.0"),
197 reason="requires CUDA and torch 2.10",
198 )
199 def test_sparse_activation_standalone_aot_preserves_dependencies(
200 monkeypatch: pytest.MonkeyPatch, vllm_tmp_cache: Path
201 ):
202 def native_reference(model: SparseActivationAotModel, x: torch.Tensor):
203 projected = torch.nn.functional.linear(x, model.up_proj.weight)
204 activated = ops.gelu_and_mul_sparse.impls["native"].impl_fn(
205 projected, model.activation.std_multiplier, "tanh"
206 )
207 return torch.nn.functional.linear(activated, model.down_proj.weight)
208
209 with monkeypatch.context() as m:
210 m.setenv("VLLM_USE_AOT_COMPILE", "1")
211 m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
212 m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
213 m.setenv("VLLM_CACHE_ROOT", str(vllm_tmp_cache / "vllm_cache"))
214
215 vllm_config = make_vllm_config()
216 torch.manual_seed(0)
217 inputs = [
218 torch.randn(4, 32, device="cuda", dtype=torch.bfloat16),
219 torch.randn(7, 32, device="cuda", dtype=torch.bfloat16),
220 ]
221
222 with (
223 use_vllm_config(vllm_config),
224 vllm_config.kernel_config.ir_op_priority.set_priority(),
225 ir.enable_torch_wrap(True),
226 ):
227 compiled = SparseActivationAotModel(vllm_config=vllm_config).to(
228 device="cuda", dtype=torch.bfloat16
229 )
230 compiled.requires_grad_(False)
231 expected = [native_reference(compiled, x) for x in inputs]
232 actual = [compiled(x) for x in inputs]
233
234 for result, reference in zip(actual, expected):
235 assert torch.isfinite(result).all()
236 torch.testing.assert_close(
237 result,
238 reference,
239 **ops.gelu_and_mul_sparse.get_tolerance(result.dtype),
240 )
241
242
243 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
244 def test_save_and_load_slice(monkeypatch: pytest.MonkeyPatch):
245 from torch._subclasses import FakeTensorMode
246 from torch.fx.experimental.symbolic_shapes import ShapeEnv
247
248 def foo(x: torch.Tensor):
249 return x[slice(0, x.shape[0])]
250
251 vllm_config = make_vllm_config()
252
253 example_input = torch.randn(10, 10)
254 torch._dynamo.mark_dynamic(example_input, 0)
255 gm = torch.fx.symbolic_trace(foo)
256 assert "getitem_1 = x[slice(0, getitem, None)]" in gm.code
257 with use_vllm_config(vllm_config):
258 payload = VllmSerializableFunction.serialize_graph_module(gm)
259 fake_mode = FakeTensorMode(shape_env=ShapeEnv())
260 loaded_gm = VllmSerializableFunction.deserialize_graph_module(
261 payload, fake_mode
262 )
263
264 assert gm.code == loaded_gm.code
265
266
267 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
268 def test_cache_load_returns_tuple_consistency_tuple_output(
269 monkeypatch: pytest.MonkeyPatch,
270 ):
271 """Test that cache loading correctly handles models that return tuples.
272
273 This verifies that when a model returns a tuple of tensors, the output
274 type is preserved as a tuple between fresh compilation and cache load.
275 """
276 with monkeypatch.context() as m:
277 args = (torch.randn(10, 10),)
278
279 with tempfile.TemporaryDirectory() as tmpdirname:
280 m.setenv("VLLM_CACHE_ROOT", tmpdirname)
281 m.setenv("VLLM_USE_AOT_COMPILE", "1")
282 m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
283 m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
284 vllm_config = make_vllm_config()
285
286 # Fresh compilation with tuple-returning model
287 with use_vllm_config(vllm_config):
288 compiled_mod = CompiledModTuple(vllm_config=vllm_config)
289 fresh_result = compiled_mod(*args)
290 fresh_result_type = type(fresh_result)
291
292 # Verify fresh result is a tuple
293 assert isinstance(fresh_result, tuple), (
294 f"Fresh compile should return tuple, got {fresh_result_type}"
295 )
296 assert len(fresh_result) == 2, (
297 f"Fresh compile should return 2-tuple, got {len(fresh_result)}"
298 )
299
300 disable_envs_cache()
301
302 # Load from cache
303 m.setenv("VLLM_FORCE_AOT_LOAD", "1")
304 vllm_config = make_vllm_config()
305 with use_vllm_config(vllm_config):
306 cached_mod = CompiledModTuple(vllm_config=vllm_config)
307 cached_result = cached_mod(*args)
308 cached_result_type = type(cached_result)
309
310 # Verify cache was actually loaded
311 assert cached_mod.was_aot_compile_fn_loaded_from_disk, (
312 "Expected was_aot_compile_fn_loaded_from_disk to be True after "
313 "loading from cache"
314 )
315
316 # Verify cached result is also a tuple
317 assert isinstance(cached_result, tuple), (
318 f"Cache load should return tuple, got {cached_result_type}. "
319 "This indicates the returns_tuple logic is not preserving "
320 "tuple outputs when loading from cache."
321 )
322 assert len(cached_result) == 2, (
323 f"Cache load should return 2-tuple, got {len(cached_result)}"
324 )
325
326 # Verify values match
327 assert torch.allclose(cached_result[0], fresh_result[0]), (
328 "Cached result[0] values should match fresh compilation"
329 )
330 assert torch.allclose(cached_result[1], fresh_result[1]), (
331 "Cached result[1] values should match fresh compilation"
332 )
333
334
335 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
336 def test_shape_env(monkeypatch: pytest.MonkeyPatch):
337 """Test that the shape environment is correctly serialized and preserved
338 when loading from cache.
339 """
340 with monkeypatch.context() as m:
341 args = (torch.randn(10, 10),)
342
343 with tempfile.TemporaryDirectory() as tmpdirname:
344 m.setenv("VLLM_CACHE_ROOT", tmpdirname)
345 m.setenv("VLLM_USE_AOT_COMPILE", "1")
346 m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
347 m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
348 vllm_config = make_vllm_config()
349 with use_vllm_config(vllm_config):
350 compiled_mod = CompiledMod(vllm_config=vllm_config)
351 compiled_mod(*args)
352 artifacts = compiled_mod.aot_compiled_fn._artifacts
353 guards_string = artifacts.compiled_fn.shape_env.format_guards()
354 assert guards_string == " - s77 <= 42\n - Eq(Mod(s77, 2), 0)"
355
356 disable_envs_cache()
357
358 m.setenv("VLLM_FORCE_AOT_LOAD", "1")
359 vllm_config = make_vllm_config()
360 with use_vllm_config(vllm_config):
361 compiled_mod = CompiledMod(vllm_config=vllm_config)
362 compiled_mod(*args)
363 assert compiled_mod.was_aot_compile_fn_loaded_from_disk, (
364 "Expected was_aot_compile_fn_loaded_from_disk to be True"
365 )
366 artifacts = compiled_mod.aot_compiled_fn._artifacts
367 guards_string = artifacts.compiled_fn.shape_env.format_guards()
368 assert guards_string == " - s77 <= 42\n - Eq(Mod(s77, 2), 0)"
369
370
371 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
372 def test_partition_wrapper_applied_on_aot_load(
373 monkeypatch: pytest.MonkeyPatch, vllm_tmp_cache: Path, mocker
374 ):
375 """Test that partition wrappers are applied when loading AOT cached functions.
376
377 This test verifies the fix for GitHub issue #31439 where AOT compile
378 caused 2x latency regression when use_inductor_graph_partition=True.
379 The root cause was that partition wrapper context was bypassed when
380 loading from AOT cache.
381 """
382 from vllm.config import CUDAGraphMode
383
384 args = (torch.randn(10, 10),)
385 monkeypatch.setenv("VLLM_USE_AOT_COMPILE", "1")
386
387 # Create config with partition enabled
388 vllm_config = VllmConfig(
389 compilation_config=CompilationConfig(
390 mode=CompilationMode.VLLM_COMPILE,
391 use_inductor_graph_partition=True,
392 cudagraph_mode=CUDAGraphMode.PIECEWISE,
393 )
394 )
395
396 # First compilation - save to cache
397 with use_vllm_config(vllm_config):
398 compiled_mod = CompiledMod(vllm_config=vllm_config)
399 compiled_mod(*args)
400
401 disable_envs_cache()
402
403 # Second run - load from cache, verify partition wrapper applied
404 monkeypatch.setenv("VLLM_FORCE_AOT_LOAD", "1")
405 vllm_config = VllmConfig(
406 compilation_config=CompilationConfig(
407 mode=CompilationMode.VLLM_COMPILE,
408 use_inductor_graph_partition=True,
409 cudagraph_mode=CUDAGraphMode.PIECEWISE,
410 )
411 )
412
413 # Use mocker to spy on set_customized_partition_wrappers
414 spy = mocker.spy(torch._inductor.utils, "set_customized_partition_wrappers")
415
416 with use_vllm_config(vllm_config):
417 compiled_mod = CompiledMod(vllm_config=vllm_config)
418
419 # First call after restart: loads from AOT cache.
420 # This tests the fix for the first call after a restart.
421 compiled_mod(*args)
422
423 # Verify cache was loaded
424 assert compiled_mod.was_aot_compile_fn_loaded_from_disk, (
425 "Expected was_aot_compile_fn_loaded_from_disk to be True"
426 )
427
428 # Verify partition wrapper was called on AOT load.
429 assert spy.call_count >= 2, (
430 "Expected partition wrapper to be set and cleared on AOT load, "
431 f"got {spy.call_count} calls"
432 )
433 # First call should set a wrapper, last call should clear it
434 assert spy.call_args_list[0][0][0] is not None, (
435 "First call on AOT load should set a wrapper function"
436 )
437 assert spy.call_args_list[-1][0][0] is None, (
438 "Last call on AOT load should clear the wrapper"
439 )
440
441 # Reset for the next check.
442 spy.reset_mock()
443
444 # Subsequent call: uses the cached `aot_compiled_fn`.
445 # This tests the fix for subsequent calls.
446 compiled_mod(*args)
447
448 # Verify partition wrapper was called on the subsequent call.
449 assert spy.call_count >= 2, (
450 "Expected partition wrapper set and cleared on subsequent "
451 f"call, got {spy.call_count} calls"
452 )
453 assert spy.call_args_list[0][0][0] is not None, (
454 "First call on subsequent call should set a wrapper function"
455 )
456 assert spy.call_args_list[-1][0][0] is None, (
457 "Last call on subsequent call should clear the wrapper"
458 )
459
460
461 @create_new_process_for_each_test("spawn")
462 def test_standalone_compile_correctness():
463 """Outputs must match regardless of VLLM_USE_STANDALONE_COMPILE."""
464 import json
465
466 from ..utils import compare_two_settings
467
468 compilation_config = json.dumps(
469 {
470 "mode": CompilationMode.VLLM_COMPILE,
471 }
472 )
473
474 common_args = [
475 "--dtype",
476 "float16",
477 "--max-model-len",
478 "256",
479 "--compilation_config",
480 compilation_config,
481 ]
482
483 compare_two_settings(
484 "facebook/opt-125m",
485 common_args,
486 common_args,
487 env1={"VLLM_USE_STANDALONE_COMPILE": "1"},
488 env2={
489 "VLLM_USE_STANDALONE_COMPILE": "0",
490 "VLLM_USE_MEGA_AOT_ARTIFACT": "0",
491 },
492 )
493
494
495 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
496 @create_new_process_for_each_test("spawn")
497 def test_gpt2_cache_hit(monkeypatch: pytest.MonkeyPatch):
498 """Test that compiling gpt2 twice results in a cache hit.
499
500 Counter values are read from the EngineCore subprocess via
501 ``LLM.collective_rpc`` so the test works under default V1
502 multiprocessing (no shared memory between test and engine).
503 """
504 from vllm import LLM
505
506 def _snap(self):
507 from vllm.compilation.counter import compilation_counter
508
509 return (
510 compilation_counter.num_aot_compiles,
511 compilation_counter.num_aot_artifacts_saved,
512 compilation_counter.num_aot_artifacts_loaded,
513 )
514
515 # collective_rpc(callable) requires pickle-based serialization.
516 monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
517
518 with monkeypatch.context() as m, tempfile.TemporaryDirectory() as tmpdirname:
519 m.setenv("VLLM_CACHE_ROOT", tmpdirname)
520 m.setenv("VLLM_USE_AOT_COMPILE", "1")
521 # First compilation - initialize model and generate
522 llm_model = LLM(
523 model="openai-community/gpt2",
524 compilation_config=CompilationConfig(
525 mode=CompilationMode.VLLM_COMPILE,
526 ),
527 max_model_len=256,
528 )
529
530 llm_model.generate("Hello, my name is")
531 assert llm_model.collective_rpc(_snap)[0] == (1, 1, 0)
532
533 # Clean up first model
534 del llm_model
535 disable_envs_cache()
536
537 # Second compilation - should hit cache
538 m.setenv("VLLM_FORCE_AOT_LOAD", "1")
539 llm_model = LLM(
540 model="openai-community/gpt2",
541 compilation_config=CompilationConfig(
542 mode=CompilationMode.VLLM_COMPILE,
543 ),
544 max_model_len=256,
545 )
546 llm_model.generate("Hello, my name is")
547 assert llm_model.collective_rpc(_snap)[0] == (0, 0, 1)
548
549
550 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
551 class TestStandaloneCompiledArtifacts:
552 def test_init(self):
553 cache = StandaloneCompiledArtifacts()
554 assert cache.submodule_bytes == {}
555 assert cache.submodule_bytes_store == {}
556 assert cache.loaded_submodule_store == {}
557
558 def test_insert_new_artifact(self):
559 cache = StandaloneCompiledArtifacts()
560 test_data = b"test_artifact_data"
561 submod_name = "test_submod"
562 shape = "s1"
563
564 hasher = hashlib.sha256()
565 hasher.update(test_data)
566 expected_hash = hasher.hexdigest()
567
568 cache.insert(submod_name, shape, test_data)
569
570 assert f"{submod_name}_{shape}" in cache.submodule_bytes
571 assert cache.submodule_bytes[f"{submod_name}_{shape}"] == expected_hash
572 assert expected_hash in cache.submodule_bytes_store
573 assert cache.submodule_bytes_store[expected_hash] == test_data
574
575 def test_insert_duplicate_artifact(self):
576 cache = StandaloneCompiledArtifacts()
577
578 test_data = b"duplicate_test_data"
579 submod_name1 = "submod1"
580 submod_name2 = "submod2"
581 shape = "s2"
582
583 cache.insert(submod_name1, shape, test_data)
584 cache.insert(submod_name2, shape, test_data)
585
586 hash1 = cache.submodule_bytes[f"{submod_name1}_{shape}"]
587 hash2 = cache.submodule_bytes[f"{submod_name2}_{shape}"]
588 assert hash1 == hash2
589
590 assert len(cache.submodule_bytes_store) == 1
591 assert len(cache.submodule_bytes) == 2
592
593 def test_get_artifact(self):
594 cache = StandaloneCompiledArtifacts()
595 test_data = b"retrievable_data"
596 submod_name = "mod1"
597 shape = "shape16"
598
599 cache.insert(submod_name, shape, test_data)
600 retrieved_data = cache.get(submod_name, shape)
601
602 assert retrieved_data == test_data
603
604 def test_get_nonexistent_artifact(self):
605 cache = StandaloneCompiledArtifacts()
606
607 with pytest.raises(KeyError):
608 cache.get("nonexistent", "shape")
609
610 def test_size_bytes(self):
611 cache = StandaloneCompiledArtifacts()
612
613 assert cache.size_bytes() == 0
614
615 data1 = b"x" * 100
616 data2 = b"y" * 200
617 cache.insert("mod1", "shape1", data1)
618 cache.insert("mod2", "shape2", data2)
619
620 assert cache.size_bytes() == 300
621
622 def test_num_artifacts_and_entries(self):
623 cache = StandaloneCompiledArtifacts()
624
625 assert cache.num_artifacts() == 0
626 assert cache.num_entries() == 0
627
628 cache.insert("mod1", "shape1", b"data1")
629 cache.insert("mod2", "shape2", b"data2")
630 assert cache.num_artifacts() == 2
631 assert cache.num_entries() == 2
632
633 cache.insert("mod3", "shape3", b"data1")
634 assert cache.num_artifacts() == 2
635 assert cache.num_entries() == 3
636
637 @patch("torch._inductor.standalone_compile.AOTCompiledArtifact.deserialize")
638 def test_load_all_success(self, mock_deserialize):
639 """Test successful loading of all artifacts."""
640 cache = StandaloneCompiledArtifacts()
641
642 mock_artifact1 = Mock()
643 mock_artifact2 = Mock()
644 mock_deserialize.side_effect = [mock_artifact1, mock_artifact2]
645
646 cache.insert("mod1", "shape1", pickle.dumps(b"data1"))
647 cache.insert("mod2", "shape2", pickle.dumps(b"data2"))
648
649 cache.load_all()
650
651 assert len(cache.loaded_submodule_store) == 2
652 assert mock_deserialize.call_count == 2
653
654 @patch("torch._inductor.standalone_compile.AOTCompiledArtifact.deserialize")
655 def test_load_all_already_loaded(self, mock_deserialize):
656 """Test that load_all skips if already loaded."""
657 cache = StandaloneCompiledArtifacts()
658
659 mock_artifact = Mock()
660 cache.submodule_bytes_store["hash1"] = pickle.dumps(b"data1")
661 cache.loaded_submodule_store["hash1"] = mock_artifact
662
663 cache.load_all()
664
665 mock_deserialize.assert_not_called()
666
667 @patch("torch._inductor.standalone_compile.AOTCompiledArtifact.deserialize")
668 def test_get_loaded_artifact(self, mock_deserialize):
669 """Test retrieving loaded artifacts."""
670 cache = StandaloneCompiledArtifacts()
671
672 mock_artifact = Mock()
673 mock_deserialize.return_value = mock_artifact
674
675 submod_name = "test_mod"
676 shape = "test_shape"
677 cache.insert(submod_name, shape, pickle.dumps(b"test_data"))
678 cache.load_all()
679
680 retrieved_artifact = cache.get_loaded(submod_name, shape)
681 assert retrieved_artifact == mock_artifact
682
683 def test_getstate_setstate(self):
684 cache = StandaloneCompiledArtifacts()
685
686 cache.insert("mod1", "shape1", b"data1")
687 cache.insert("mod2", "shape2", b"data2")
688
689 cache.loaded_submodule_store["hash1"] = Mock()
690
691 state = cache.__getstate__()
692
693 assert "submodule_bytes" in state
694 assert "submodule_bytes_store" in state
695 assert "loaded_submodule_store" not in state
696
697 new_cache = StandaloneCompiledArtifacts()
698 new_cache.__setstate__(state)
699
700 assert new_cache.submodule_bytes == cache.submodule_bytes
701 assert new_cache.submodule_bytes_store == cache.submodule_bytes_store
702 assert new_cache.loaded_submodule_store == {}
703
704 def test_pickle_roundtrip(self):
705 cache = StandaloneCompiledArtifacts()
706
707 test_data1 = b"pickle_test_data_1"
708 test_data2 = b"pickle_test_data_2"
709 cache.insert("mod1", "shape1", test_data1)
710 cache.insert("mod2", "shape2", test_data2)
711
712 pickled_data = pickle.dumps(cache)
713 restored_cache = pickle.loads(pickled_data)
714
715 assert restored_cache.get("mod1", "shape1") == test_data1
716 assert restored_cache.get("mod2", "shape2") == test_data2
717 assert restored_cache.num_artifacts() == cache.num_artifacts()
718 assert restored_cache.num_entries() == cache.num_entries()
719 assert restored_cache.size_bytes() == cache.size_bytes()
720
721 assert len(restored_cache.loaded_submodule_store) == 0
722
723
724 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
725 class TestStandaloneCompiledArtifactsIntegration:
726 def test_add_pickle_unpickle(self):
727 cache = StandaloneCompiledArtifacts()
728
729 artifacts = {
730 ("mod1", "shape1"): b"m1s1_artifact",
731 ("mod1", "shape2"): b"m1s2_artifact",
732 ("mod2", "shape1"): b"m2s1_artifact",
733 ("mod2", "shape2"): b"m2s2_artifact",
734 }
735
736 for (submod, shape), data in artifacts.items():
737 cache.insert(submod, shape, data)
738
739 assert cache.num_entries() == 4
740 assert cache.num_artifacts() == 4
741
742 for (submod, shape), expected_data in artifacts.items():
743 retrieved_data = cache.get(submod, shape)
744 assert retrieved_data == expected_data
745
746 pickled = pickle.dumps(cache)
747 restored_cache = pickle.loads(pickled)
748
749 for (submod, shape), expected_data in artifacts.items():
750 retrieved_data = restored_cache.get(submod, shape)
751 assert retrieved_data == expected_data
752
753 def test_deduplication(self):
754 cache = StandaloneCompiledArtifacts()
755
756 shared_data = b"shared_artifact_data" * 1000
757
758 cache.insert("mod1", "shape1", shared_data)
759 cache.insert("mod2", "shape1", shared_data)
760 cache.insert("mod1", "shape2", shared_data)
761 cache.insert("mod3", "shape3", shared_data)
762
763 assert cache.num_entries() == 4
764 assert cache.num_artifacts() == 1
765 assert cache.size_bytes() == len(shared_data)
766
767 for submod, shape in [
768 ("mod1", "shape1"),
769 ("mod2", "shape1"),
770 ("mod1", "shape2"),
771 ("mod3", "shape3"),
772 ]:
773 assert cache.get(submod, shape) == shared_data
774
775 @pytest.mark.skipif(
776 envs.VLLM_USE_MEGA_AOT_ARTIFACT,
777 reason="There's no AOT Autograd run with mega artifact",
778 )
779 def test_functorch_config(self):
780 vllm_config = make_vllm_config()
781 example_inputs = (torch.randn(10, 10),)
782
783 def add_1(x: torch.Tensor):
784 return x + 1
785
786 gm = torch._dynamo.functional_export.dynamo_graph_capture_for_export(add_1)(
787 *example_inputs
788 )
789
790 gm.graph._codegen = torch.fx.graph.CodeGen()
791 gm._dynamo_bytecode_flatten = None
792 gm._dynamo_bytecode_unflatten = None
793
794 with (
795 torch._functorch.config.patch(bundled_autograd_cache=False),
796 set_current_vllm_config(vllm_config),
797 ):
798 with torch._functorch.config.patch(bundled_autograd_cache=True):
799 fn = VllmSerializableFunction(gm, example_inputs, "", add_1)
800
801 payload = VllmSerializableFunction.serialize_compile_artifacts(fn)
802
803 config = None
804
805 def backend(*args, **kwargs) -> VllmSerializableFunction:
806 nonlocal config
807 # bundled_autograd_cache should be True even compiler backend
808 # runs with bundled_autograd_cache=False in ambient context.
809 config = torch._functorch.config.save_config_portable()
810 return fn
811
812 loaded_fn = VllmSerializableFunction.deserialize_compile_artifacts(payload)
813 with patch.object(VllmBackend, "__call__", backend):
814 loaded_fn(*example_inputs)
815
816 assert isinstance(config, dict)
817 assert "bundled_autograd_cache" in config
818 assert config["bundled_autograd_cache"] is True
819
820
821 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
822 def test_disable_compile_cache_skips_aot_save(
823 monkeypatch: pytest.MonkeyPatch, fresh_vllm_cache: str
824 ):
825 """When VLLM_DISABLE_COMPILE_CACHE=1, AOT artifacts must not be saved."""
826 monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
827 monkeypatch.setenv("VLLM_USE_AOT_COMPILE", "1")
828 disable_envs_cache()
829
830 args = (torch.randn(10, 10),)
831 expected = reference_fn(*args)
832 vllm_config = make_vllm_config()
833
834 with (
835 use_vllm_config(vllm_config),
836 compilation_counter.expect(
837 num_aot_compiles=1,
838 num_aot_artifacts_saved=0,
839 num_aot_artifacts_loaded=0,
840 ),
841 ):
842 mod = CompiledMod(vllm_config=vllm_config)
843 actual = mod(*args)
844
845 assert torch.allclose(actual, expected)
846
847 # No cached artifact should exist on disk
848 aot_dir = os.path.join(fresh_vllm_cache, "torch_compile_cache", "torch_aot_compile")
849 if os.path.isdir(aot_dir):
850 for root, _dirs, files in os.walk(aot_dir):
851 for f in files:
852 assert f != "model", (
853 f"AOT artifact unexpectedly saved at {os.path.join(root, f)}"
854 )
855
856
857 @pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
858 def test_disable_compile_cache_skips_aot_load(
859 monkeypatch: pytest.MonkeyPatch, fresh_vllm_cache: str
860 ):
861 """When VLLM_DISABLE_COMPILE_CACHE=1, AOT artifacts must not be loaded."""
862 # Phase 1: compile and save with cache enabled
863 monkeypatch.setenv("VLLM_USE_AOT_COMPILE", "1")
864 disable_envs_cache()
865
866 args = (torch.randn(10, 10),)
867 vllm_config = make_vllm_config()
868
869 with (
870 use_vllm_config(vllm_config),
871 compilation_counter.expect(num_aot_artifacts_saved=1),
872 ):
873 CompiledMod(vllm_config=vllm_config)(*args)
874
875 # Phase 2: disable cache, compile again — should NOT load from disk
876 monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
877 disable_envs_cache()
878 torch._dynamo.reset()
879
880 vllm_config = make_vllm_config()
881 with (
882 use_vllm_config(vllm_config),
883 compilation_counter.expect(
884 num_aot_compiles=1,
885 num_aot_artifacts_saved=0,
886 num_aot_artifacts_loaded=0,
887 ),
888 ):
889 mod = CompiledMod(vllm_config=vllm_config)
890 mod(*args)
891
892 assert not mod.was_aot_compile_fn_loaded_from_disk