@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 5569f5218d
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 7, 06:30 UTC

[torch.compile] Stop lazily compiling (#35472)

Signed-off-by: Richard Zou <zou3519@gmail.com>

Richard Zou committed Mar 4, 2026 at 15:13 UTC 5569f5218d3b8a08cfbb9fd51c9f01852f16ddbc
7 files changed +177 -150
tests/compile/test_compile_ranges.py
+4 -3
@@ -73,6 +73,7 @@ def test_compile_ranges(use_fresh_inductor_cache):
73 Range(start=16, end=16),
74 Range(start=9, end=32),
75 Range(start=64, end=64),
76 + Range(start=128, end=128),
77 Range(start=33, end=8192),
78 ]
79 )
@@ -95,16 +96,16 @@ def test_compile_ranges(use_fresh_inductor_cache):
96
97 with set_current_vllm_config(vllm_config):
98 model = TestModel(vllm_config=vllm_config, prefix="").eval()
98 - # Number of compilations: 3 for each compile range + 2 compile sizes
99 + # Number of compilations: 3 compile ranges + 3 compile sizes
100 batch_sizes = [1, 4, 16, 24, 48, 64, 8192]
101
102 with compilation_counter.expect(
103 num_graphs_seen=1,
104 num_piecewise_graphs_seen=1,
104 - num_backend_compilations=5,
105 + num_backend_compilations=6,
106 ):
107 run_model(vllm_config, model, batch_sizes)
107 - assert post_grad_range_checker.num_calls == 5
108 + assert post_grad_range_checker.num_calls == 6
109
110
111 def test_compile_config_get_compile_ranges():
tests/compile/test_structured_logging.py
+3 -3
@@ -109,9 +109,9 @@ def test_vllm_structured_logging_artifacts(use_fresh_inductor_cache):
109 f"got {len(vllm_piecewise_split_graph)}"
110 )
111 compile_start_artifacts = capture.get("artifact", "vllm_piecewise_compile_start")
112 - assert len(compile_start_artifacts) == 2, (
113 - "Expected 2 vllm_piecewise_compile_start "
114 - "(one for dynamic ranges, one for compile size), "
112 + assert len(compile_start_artifacts) == 4, (
113 + "Expected 4 vllm_piecewise_compile_start "
114 + "(2 subgraphs x 2 ranges each: dynamic + compile size), "
115 f"got {len(compile_start_artifacts)}"
116 )
117 submod_dumps = capture.get("graph_dump", r"vllm_submod_.*")
vllm/compilation/backends.py
+20 -31
@@ -2,7 +2,6 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import ast
5 -import contextvars
5 import dataclasses
6 import hashlib
7 import json
@@ -18,7 +17,7 @@ from typing import Any
17
18 import torch
19 import torch.fx as fx
21 -from torch._dispatch.python import enable_python_dispatcher
20 +from torch._dynamo.utils import dynamo_timed
21 from torch._logging._internal import trace_structured
22
23 import vllm.envs as envs
@@ -510,9 +509,9 @@ def wrap_with_cudagraph_if_needed(
509
510 class PiecewiseCompileInterpreter(torch.fx.Interpreter): # type: ignore[misc]
511 """Code adapted from `torch.fx.passes.shape_prop.ShapeProp`.
513 - It runs the given graph with fake inputs, and compile some
514 - submodules specified by `compile_submod_names` with the given
515 - compilation configs.
512 + It runs the given split graph interpreter, and for each submodule in
513 + `compile_submod_names`, creates a PiecewiseBackend and compiles all
514 + ranges up front.
515
516 NOTE: the order in `compile_submod_names` matters, because
517 it will be used to determine the order of the compiled piecewise
@@ -540,9 +539,6 @@ class PiecewiseCompileInterpreter(torch.fx.Interpreter): # type: ignore[misc]
539 vllm_backend: "VllmBackend",
540 ) -> None:
541 super().__init__(module)
543 - from torch._guards import detect_fake_mode
544 -
545 - self.fake_mode = detect_fake_mode()
542 self.compile_submod_names = compile_submod_names
543 self.compilation_config = vllm_config.compilation_config
544 self.vllm_config = vllm_config
@@ -552,13 +548,7 @@ class PiecewiseCompileInterpreter(torch.fx.Interpreter): # type: ignore[misc]
548
549 @instrument(span_name="Inductor compilation")
550 def run(self, *args: Any) -> Any:
555 - # maybe instead just assert inputs are fake?
556 - fake_args = [
557 - self.fake_mode.from_tensor(t) if isinstance(t, torch.Tensor) else t
558 - for t in args
559 - ]
560 - with self.fake_mode, enable_python_dispatcher():
561 - return super().run(*fake_args)
551 + return super().run(*args)
552
553 def call_module(
554 self,
@@ -614,21 +604,6 @@ class PiecewiseCompileInterpreter(torch.fx.Interpreter): # type: ignore[misc]
604 model_tag: str = "backbone"
605 model_is_encoder: bool = False
606
617 -_on_compilation_complete_callback: contextvars.ContextVar[Callable[[], None] | None] = (
618 - contextvars.ContextVar("on_compilation_complete_callback", default=None)
619 -)
620 -
621 -
622 -@contextmanager
623 -def set_on_compilation_complete(
624 - callback: Callable[[], None],
625 -) -> Generator[None, None, None]:
626 - token = _on_compilation_complete_callback.set(callback)
627 - try:
628 - yield
629 - finally:
630 - _on_compilation_complete_callback.reset(token)
631 -
607
608 @contextmanager
609 def set_model_tag(tag: str, is_encoder: bool = False) -> Generator[None, None, None]:
@@ -846,6 +821,7 @@ class VllmBackend:
821 ),
822 )
823
824 + @dynamo_timed("vllm_backend")
825 def __call__(self, graph: fx.GraphModule, example_inputs: Sequence[Any]) -> Any:
826 from .caching import (
827 VllmSerializableFunction,
@@ -1036,11 +1012,24 @@ class VllmBackend:
1012 ]
1013
1014 # propagate the split graph to the piecewise backend,
1039 - # compile submodules with symbolic shapes
1015 + # compile submodules with symbolic shapes, and compile all ranges
1016 + # up front so that compilation is complete before the callable
1017 + # is returned.
1018 PiecewiseCompileInterpreter(
1019 self.split_gm, submod_names_to_compile, self.vllm_config, self
1020 ).run(*fake_args)
1021
1022 + # All compilation is done. Save the cache.
1023 + time_before_saving = time.perf_counter()
1024 + self.compiler_manager.save_to_file()
1025 + elapsed = time.perf_counter() - time_before_saving
1026 + if elapsed > 1:
1027 + logger.info_once(
1028 + "Saved compiler manager cache in %.2f seconds.",
1029 + elapsed,
1030 + scope="local",
1031 + )
1032 +
1033 from torch._guards import detect_fake_mode
1034
1035 fake_mode = detect_fake_mode()
vllm/compilation/caching.py
+11 -15
@@ -313,30 +313,26 @@ class VllmSerializableFunction(SerializableCallable): # type: ignore[misc]
313
314 return fn
315
316 - # Fall back to standard VllmBackend
316 + # Fall back to standard VllmBackend.
317 + # Use a lazy closure: the backend needs traced_files for cache
318 + # dir computation, but those are only populated after
319 + # _verify_source_unchanged runs in decorators.py (which happens
320 + # after deserialization completes).
321 from vllm.compilation.backends import VllmBackend
322
323 is_encoder = state.get("is_encoder", False)
320 - vllm_backend: VllmBackend = VllmBackend(
321 - get_current_vllm_config(), state["prefix"], is_encoder
322 - )
324 + vllm_config = get_current_vllm_config()
325 + compile_inputs = list(state["example_inputs"])
326
327 def optimized_call(*example_inputs: Any) -> Any:
325 - """
326 - On the first run of the optimized call, we rerun the compiler
327 - backend which should result in a cache hit. After the backend
328 - call returns, we just do a one-time replacement of the optimized
329 - call with the compiled function, so that subsequent calls are on
330 - the AOT compiled path.
331 - """
332 - compile_inputs = [
333 - inp if inp is not None else example_inputs[i]
334 - for i, inp in enumerate(fn.example_inputs)
335 - ]
328 + vllm_backend: VllmBackend = VllmBackend(
329 + vllm_config, state["prefix"], is_encoder
330 + )
331 with tracing(TracingContext(fake_mode)):
332 fn.optimized_call = vllm_backend(
333 state["graph_module"], compile_inputs
334 ).optimized_call
335 + fn.vllm_backend = vllm_backend
336 return fn.optimized_call(*example_inputs)
337
338 fn = cls(**state, optimized_call=optimized_call)
vllm/compilation/decorators.py
+12 -7
@@ -466,8 +466,12 @@ def _support_torch_compile(
466 "Directly load AOT compilation from path %s", aot_compilation_path
467 )
468 # Apply partition wrapper context for proper CUDA graph capture
469 + from .monitor import end_monitoring_torch_compile
470 +
471 with maybe_use_cudagraph_partition_wrapper(self.vllm_config):
470 - return self.aot_compiled_fn(self, *args, **kwargs)
472 + output = self.aot_compiled_fn(self, *args, **kwargs)
473 + end_monitoring_torch_compile(self.vllm_config)
474 + return output
475
476 if self.compiled:
477 assert (
@@ -552,18 +556,19 @@ def _support_torch_compile(
556 logger.warning("Detected eager backend, disabling AOT compile.")
557 use_aot_compile = False
558 if use_aot_compile:
555 - from vllm.compilation.backends import set_on_compilation_complete
556 -
559 # store the path for saving after warmup
560 self._aot_compilation_path = aot_compilation_path
561 self._aot_cache_dir = cache_dir
560 - # set callback in context so it's available when compilation completes
561 - with set_on_compilation_complete(self.save_aot_compiled_function):
562 - self.aot_compiled_fn = self.aot_compile(*args, **kwargs)
563 - output = self.aot_compiled_fn(self, *args, **kwargs)
562 + self.aot_compiled_fn = self.aot_compile(*args, **kwargs)
563 + # All compilation is done at this point, save the AOT artifact.
564 + self.save_aot_compiled_function()
565 + output = self.aot_compiled_fn(self, *args, **kwargs)
566 else:
567 output = TorchCompileWithNoGuardsWrapper.__call__(self, *args, **kwargs) # type: ignore[arg-type]
568
569 + from .monitor import end_monitoring_torch_compile
570 +
571 + end_monitoring_torch_compile(self.vllm_config)
572 self.compiled = True
573 return output
574
vllm/compilation/monitor.py
+1 -1
@@ -33,7 +33,7 @@ def end_monitoring_torch_compile(vllm_config: VllmConfig) -> None:
33 total_compile_time: float = time.perf_counter() - torch_compile_start_time
34 if compilation_config.mode == CompilationMode.VLLM_COMPILE:
35 logger.info_once(
36 - "torch.compile takes %.2f s in total",
36 + "torch.compile and initial profiling run took %.2f s in total",
37 total_compile_time,
38 scope="local",
39 )
vllm/compilation/piecewise_backend.py
+126 -90
@@ -5,7 +5,6 @@ import dataclasses
5 import io
6 import json
7 import pickle
8 -import time
8 from collections.abc import Callable
9 from pickle import Pickler
10 from typing import Any
@@ -16,7 +15,6 @@ from torch._inductor.runtime.triton_heuristics import CachingAutotuner
15 from torch._logging._internal import trace_structured
16
17 from vllm.compilation.backends import VllmBackend
19 -from vllm.compilation.monitor import end_monitoring_torch_compile
18 from vllm.config import VllmConfig
19 from vllm.config.utils import Range
20 from vllm.logger import init_logger
@@ -24,6 +22,55 @@ from vllm.logger import init_logger
22 logger = init_logger(__name__)
23
24
25 +def get_fake_args_from_graph(graph: fx.GraphModule) -> list[Any]:
26 + """Get fake args directly from graph placeholder nodes."""
27 + fake_args = []
28 + for node in graph.graph.nodes:
29 + if node.op == "placeholder":
30 + fake_args.append(node.meta["example_value"])
31 + else:
32 + break
33 + return fake_args
34 +
35 +
36 +def create_concrete_args(graph: fx.GraphModule, size: int) -> list[Any]:
37 + """Create example inputs with symbolic dims replaced by a concrete size.
38 +
39 + Used for single-size eager compilation where we need concrete-shaped
40 + inputs but don't have real runtime tensors yet.
41 + """
42 + from torch._prims_common import compute_required_storage_length
43 + from torch.fx.experimental.symbolic_shapes import is_symbolic
44 +
45 + def concretize(sym_val: Any) -> int:
46 + """Replace all symbolic variables in a SymInt expression with size."""
47 + if not is_symbolic(sym_val):
48 + return int(sym_val)
49 + expr = sym_val.node.expr
50 + return int(expr.subs({s: size for s in expr.free_symbols}))
51 +
52 + args: list[Any] = []
53 + for node in graph.graph.nodes:
54 + if node.op != "placeholder":
55 + break
56 + val = node.meta["example_value"]
57 + if isinstance(val, torch.SymInt):
58 + args.append(concretize(val))
59 + elif isinstance(val, torch.Tensor):
60 + new_shape = tuple(concretize(d) for d in val.shape)
61 + new_strides = tuple(concretize(s) for s in val.stride())
62 + new_storage_offset = concretize(val.storage_offset())
63 + needed_size = compute_required_storage_length(
64 + new_shape, new_strides, new_storage_offset
65 + )
66 + t = torch.empty(needed_size, dtype=val.dtype, device=val.device)
67 + t = t.as_strided(new_shape, new_strides, new_storage_offset)
68 + args.append(t)
69 + else:
70 + args.append(val)
71 + return args
72 +
73 +
74 @dataclasses.dataclass
75 class RangeEntry:
76 compile_range: Range
@@ -109,10 +156,6 @@ class PiecewiseBackend:
156 # the entries for ranges that we need to either
157 self.range_entries: dict[Range, RangeEntry] = {}
158
112 - # to_be_compiled_ranges tracks the remaining ranges to compile,
113 - # and updates during the compilation process, so we need to copy it
114 - self.to_be_compiled_ranges: set[Range] = set(self.compile_ranges)
115 -
159 # We only keep compilation management inside this class directly.
160 if self.compile_sizes is not None:
161 for size in self.compile_sizes:
@@ -129,7 +172,6 @@ class PiecewiseBackend:
172 self.range_entries[range] = RangeEntry(
173 compile_range=range,
174 )
132 - self.to_be_compiled_ranges.add(range)
175
176 for range in self.compile_ranges:
177 self.range_entries[range] = RangeEntry(
@@ -139,12 +181,10 @@ class PiecewiseBackend:
181 # Track whether we've logged the graph for this subgraph (only log once)
182 self._graph_logged = False
183
142 - # get the on_compilation_complete callback from context...
143 - # PiecewiseBackend is created during the first call,
144 - # which is when the context is set (see compilation/decorators.py)
145 - from vllm.compilation.backends import _on_compilation_complete_callback
146 -
147 - self.on_compilation_complete = _on_compilation_complete_callback.get()
184 + if self.graph is not None:
185 + self.compile_all_ranges()
186 + else:
187 + self.load_all_ranges()
188
189 def get_compiled_graph_wrapper(
190 self, compiled_graph: Callable[..., Any]
@@ -161,25 +201,6 @@ class PiecewiseBackend:
201
202 return compiled_graph_wrapper
203
164 - def check_for_ending_compilation(self) -> None:
165 - if self.is_last_graph and not self.to_be_compiled_ranges:
166 - # no specific sizes to compile
167 - # save the hash of the inductor graph for the next run
168 - time_before_saving = time.perf_counter()
169 - self.vllm_backend.compiler_manager.save_to_file()
170 - elapsed = time.perf_counter() - time_before_saving
171 - if elapsed > 1:
172 - logger.info_once(
173 - "Saved compiler manager cache in %.2f seconds.",
174 - elapsed,
175 - scope="local",
176 - )
177 -
178 - end_monitoring_torch_compile(self.vllm_config)
179 - # Call the completion callback (e.g., to save AOT compiled function)
180 - if self.on_compilation_complete is not None:
181 - self.on_compilation_complete()
182 -
204 def to_bytes(self) -> dict[str, bytes]:
205 class StandaloneCompiledArtifactsPickler(Pickler):
206 def reducer_override(self, obj: object) -> Any:
@@ -216,27 +237,54 @@ class PiecewiseBackend:
237
238 return out
239
219 - def _fakify_args(self, args: tuple[Any, ...]) -> list[Any]:
220 - # We need to pass fake example_inputs, otherwise torch.compile
221 - # will fakify the example_inputs potentially causing some non dynamic
222 - # dimension to be be duck shaped to other existing shapes that have hints
223 - # matching their values.
224 - # This is problem because it can lead to unintended specializations!
225 - # if the new wrongly dynamic dim is specialized
226 - # it will force specializing the whole shape
227 - # torch.compile probably should not accept
228 - # non fake tensors as example inputs!
229 - # See issue https://github.com/vllm-project/vllm/issues/27899
230 - fake_example_inputs = []
231 - assert self.graph is not None
232 - for node in self.graph.graph.nodes:
233 - # All place holders come first
234 - if node.op == "placeholder":
235 - fake_example_inputs.append(node.meta["example_value"])
240 + def compile_all_ranges(self) -> None:
241 + """Compile all range entries for this piecewise subgraph up front."""
242 + assert self.graph is not None, (
243 + "Cannot compile without a graph. "
244 + "When loading from cache/AOT artifacts, "
245 + "compile_all_ranges should not be called."
246 + )
247 +
248 + for range_entry in self.range_entries.values():
249 + if range_entry.compiled:
250 + continue
251 +
252 + self._log_compile_start(range_entry.compile_range)
253 +
254 + if range_entry.compile_range.is_single_size():
255 + args_list = create_concrete_args(
256 + self.graph, range_entry.compile_range.start
257 + )
258 else:
237 - break
238 - assert len(fake_example_inputs) == len(args)
239 - return fake_example_inputs
259 + args_list = get_fake_args_from_graph(self.graph)
260 +
261 + # TODO(https://github.com/vllm-project/vllm/issues/35766)
262 + # Can we remove strict_autograd_cache and
263 + # force_non_lazy_backward_lowering overrides?
264 + # I added them explicitly because this is what they are
265 + # set to before the refactor
266 + # (https://github.com/vllm-project/vllm/pull/35472).
267 + # They affect the aotautograd cache key computation
268 + # but they shouldn't have any effect on the actual
269 + # compilation.
270 + config_patches = dict(
271 + bundled_autograd_cache=True,
272 + strict_autograd_cache=False,
273 + )
274 + if hasattr(torch._functorch.config, "force_non_lazy_backward_lowering"):
275 + config_patches["force_non_lazy_backward_lowering"] = False
276 + with torch._functorch.config.patch(**config_patches):
277 + range_entry.runnable = self.vllm_backend.compiler_manager.compile(
278 + self.graph,
279 + args_list,
280 + self.vllm_backend.inductor_config,
281 + self.compilation_config,
282 + compile_range=range_entry.compile_range,
283 + graph_index=self.piecewise_compile_index,
284 + num_graphs=self.total_piecewise_compiles,
285 + )
286 +
287 + range_entry.compiled = True
288
289 def _log_compile_start(self, compile_range: Range):
290 """Log compilation event for TORCH_TRACE/tlparse."""
@@ -277,44 +325,29 @@ class PiecewiseBackend:
325 payload_fn=lambda: self.graph.print_readable(print_output=False),
326 )
327
280 - def _maybe_compile_for_range_entry(
281 - self, range_entry: RangeEntry, args: tuple[Any, ...]
282 - ) -> Any:
283 - if not range_entry.compiled:
284 - if self.compiled_runnables is not None:
285 - range_entry.runnable = self.get_compiled_graph_wrapper(
286 - self.compiled_runnables[str(range_entry.compile_range)]
287 - )
288 - else:
289 - self._log_compile_start(range_entry.compile_range)
290 -
291 - # args are real arguments
292 - # fakify for range, real args for concrete size.
293 - # For concrete size, we clear the shape env in
294 - # compiler_manager.compile() so no need to fakify.
295 - args_list = (
296 - self._fakify_args(args)
297 - if not range_entry.compile_range.is_single_size()
298 - else list(args)
299 - )
300 -
301 - with (
302 - torch._functorch.config.patch("bundled_autograd_cache", True),
303 - ):
304 - range_entry.runnable = self.vllm_backend.compiler_manager.compile(
305 - self.graph,
306 - args_list,
307 - self.vllm_backend.inductor_config,
308 - self.compilation_config,
309 - compile_range=range_entry.compile_range,
310 - graph_index=self.piecewise_compile_index,
311 - num_graphs=self.total_piecewise_compiles,
312 - )
328 + def load_all_ranges(self) -> None:
329 + """Load all pre-compiled runnables for this piecewise subgraph.
330
331 + Called during warm start to wrap all cached compiled_runnables
332 + into range_entry.runnable up front, analogous to compile_all_ranges()
333 + for the cold start path.
334 + """
335 + assert self.compiled_runnables is not None, (
336 + "load_all_ranges should only be called when compiled_runnables "
337 + "is set (warm start / cache loading path)."
338 + )
339 + for range_entry in self.range_entries.values():
340 + if range_entry.compiled:
341 + continue
342 + key = str(range_entry.compile_range)
343 + assert key in self.compiled_runnables, (
344 + f"Missing compiled runnable for range {range_entry.compile_range}. "
345 + f"Available keys: {list(self.compiled_runnables.keys())}"
346 + )
347 + range_entry.runnable = self.get_compiled_graph_wrapper(
348 + self.compiled_runnables[key]
349 + )
350 range_entry.compiled = True
315 - self.to_be_compiled_ranges.remove(range_entry.compile_range)
316 -
317 - self.check_for_ending_compilation()
351
352 def _find_range_for_shape(self, runtime_shape: int) -> RangeEntry | None:
353 # First we try to find the range entry for the concrete compile size
@@ -338,6 +371,9 @@ class PiecewiseBackend:
371 assert range_entry is not None, (
372 f"Shape: {runtime_shape} out of considered ranges: {self.compile_ranges}"
373 )
341 -
342 - self._maybe_compile_for_range_entry(range_entry, args)
374 + assert range_entry.compiled, (
375 + "All ranges should be compiled or loaded up front in "
376 + "PiecewiseBackend.__init__. "
377 + f"range_entry={range_entry.compile_range}"
378 + )
379 return range_entry.runnable(*args)