main
py 169 lines 5.56 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import gc
4 import json
5 import time
6 from collections import Counter
7 from contextlib import contextmanager, suppress
8 from typing import Any
9
10 import vllm.envs as envs
11 from vllm.logger import init_logger
12
13 logger = init_logger(__name__)
14
15
16 class GCDebugConfig:
17 """Config for GC Debugger.
18 - 0: disable GC debugger
19 - 1: enable GC debugger with gc.collect elapsed times
20 - '{"top_objects":5}': enable GC debugger with top 5 collected objects
21 """
22
23 def __init__(self, gc_debug_conf: str | None = None) -> None:
24 self.enabled: bool = False
25 self.top_objects: int = -1
26
27 if not gc_debug_conf or gc_debug_conf == "0":
28 pass
29 elif gc_debug_conf == "1":
30 self.enabled = True
31 else:
32 try:
33 json_conf = json.loads(gc_debug_conf)
34 self.enabled = True
35 self.top_objects = json_conf.get("top_objects", -1)
36 except Exception:
37 self.enabled = False
38 logger.error("Failed to parse VLLM_GC_DEBUG(%s)", envs.VLLM_GC_DEBUG)
39 logger.debug("GC Debug Config. %s", str(self))
40
41 def __repr__(self) -> str:
42 return f"enabled:{self.enabled},top_objects:{self.top_objects}"
43
44
45 class GCDebugger:
46 """Debugger for GC which logs helpful information for GC understanding.
47 To enable, you should call maybe_attach_gc_debug_callback in the process.
48 """
49
50 def __init__(self, config: GCDebugConfig) -> None:
51 self.config = config
52 # Start time in micro second of this GC cycle
53 self.start_time_ns: int = time.monotonic_ns()
54 self.num_objects: int = 0
55 # If config.top_objects is positive,
56 # compute top collected objects by object types
57 self.gc_top_collected_objects: str = ""
58
59 def handle(self, phase: str, info: dict[str, int]) -> None:
60 """Handles a GC event (e.g. GC start or GC finish)."""
61 generation = info.get("generation")
62 if generation is None:
63 return
64 if phase == "start":
65 # Before GC started, record GC start time
66 # and top collected objects
67 self.start_time_ns = time.monotonic_ns()
68 objects = gc.get_objects(generation)
69 self.num_objects = len(objects)
70 self.gc_top_collected_objects = _compute_top_gc_collected_objects(
71 objects, self.config.top_objects
72 )
73 elif phase == "stop":
74 # After GC finished, Record GC elapsed time and
75 # optionally top collected objects
76 elpased_ms = (time.monotonic_ns() - self.start_time_ns) / 1e6
77 logger.info(
78 "GC took %.3fms to complete. "
79 "Collected %s objects (out of %d) in GC generation %d.%s",
80 elpased_ms,
81 str(info.get("collected", "?")),
82 self.num_objects,
83 generation,
84 (
85 f" Top collected objects: \n{self.gc_top_collected_objects}"
86 if self.gc_top_collected_objects
87 else ""
88 ),
89 )
90
91
92 @contextmanager
93 def freeze_gc_for_cudagraph_capture():
94 """Freeze and disable gc for the duration of bulk CUDA graph capture.
95
96 A gc cycle during stream capture can invalidate the captured graph, e.g.
97 a finalized Triton kernel unloads its module. Opt out with
98 VLLM_ENABLE_CUDAGRAPH_GC=1.
99 """
100 gc_was_enabled = gc.isenabled()
101 gc.collect()
102 should_freeze = not envs.VLLM_ENABLE_CUDAGRAPH_GC
103 if should_freeze:
104 gc.freeze()
105 gc.disable()
106 try:
107 yield
108 finally:
109 if should_freeze:
110 try:
111 gc.unfreeze()
112 gc.collect()
113 finally:
114 if gc_was_enabled:
115 gc.enable()
116 else:
117 gc.disable()
118
119
120 def freeze_gc_heap() -> None:
121 """Freeze all objects tracked by the garbage collector. It should be invoked
122 after server init / warmup, to reduce GC overhead from static objects
123 during serving time.
124 """
125 # Ensure all static objects are pushed down to the oldest generation for
126 # freeze
127 gc.collect(0)
128 gc.collect(1)
129 gc.collect(2)
130 # Freeze all GC tracked objects
131 gc.freeze()
132
133
134 def maybe_attach_gc_debug_callback() -> None:
135 """Attached a callback for GC debug when VLLM_GC_DEBUG is enabled."""
136 config = GCDebugConfig(envs.VLLM_GC_DEBUG)
137 if config.enabled:
138 debugger: GCDebugger = GCDebugger(config)
139
140 def gc_callback(phase: str, info: dict[str, int]) -> None:
141 debugger.handle(phase, info)
142
143 gc.callbacks.append(gc_callback)
144
145
146 def _compute_detailed_type(o: Any) -> str:
147 """Detailed object type.
148
149 TODO(Jialin): Further enhance the detailed type with element types for
150 easier debugging. We tried but occasionally it would run into signals
151 which kills the engine.
152 """
153 size_str: str = ""
154 # Object doesn't support len() - this can happen with type objects
155 # or other objects that don't implement __len__ properly
156 with suppress(Exception):
157 size_str = f"(size:{len(o)})"
158 return f"{str(type(o))}{size_str}"
159
160
161 def _compute_top_gc_collected_objects(objects: list[Any], top: int) -> str:
162 """Group collected objects by types."""
163 if top <= 0:
164 return ""
165 object_types = [_compute_detailed_type(o) for o in objects]
166 return "\n".join(
167 f"{count:>5}:{object_type}"
168 for object_type, count in Counter(object_types).most_common(top)
169 )