patch_vllm_spark_kv_cache_cleanup.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env python3
  2. """Clean profiling allocations before vLLM sizes and creates the KV cache."""
  3. import re
  4. import sys
  5. from pathlib import Path
  6. source_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
  7. target = source_root / "vllm/v1/worker/gpu_worker.py"
  8. if not target.exists():
  9. raise SystemExit(f"{target} not found; cannot apply KV cache cleanup patch")
  10. text = target.read_text()
  11. lines = text.splitlines(keepends=True)
  12. changed = False
  13. # r18: newer vLLM (tip a8c796f3, 2026-09-05) replaced the
  14. # profile_result.after_profile flow in gpu_worker.py with the
  15. # MemorySnapshot/request_memory flow (vllm/v1/worker/utils.py), which
  16. # already runs gc.collect() + torch.accelerator.empty_cache() BEFORE the
  17. # snapshot is taken. The post-profile cleanup this patch adds is now
  18. # native upstream — skip.
  19. if (
  20. "request_memory(init_snapshot" in text
  21. and "gc.collect()" in text
  22. and "empty_cache()" in text
  23. ):
  24. print("New MemorySnapshot/request_memory flow already cleans up before the snapshot; skipping")
  25. sys.exit(0)
  26. profile_cleanup_present = (
  27. "profile_result.after_profile.measure()" in text
  28. and "diff_from_create.non_torch_memory" in text
  29. )
  30. prealloc_cleanup_present = (
  31. "memory_reserved(self.device)" in text
  32. and "memory_allocated(self.device)" in text
  33. and "empty_cache()" in text
  34. )
  35. needs_cleanup = not (profile_cleanup_present and prealloc_cleanup_present)
  36. if needs_cleanup and not re.search(r"(?m)^import gc$", text):
  37. insert_at = None
  38. last_future_import = None
  39. for i, line in enumerate(lines):
  40. if line.startswith("from __future__ import "):
  41. last_future_import = i
  42. elif insert_at is None and (
  43. line.startswith("import ") or line.startswith("from ")
  44. ):
  45. insert_at = i
  46. if last_future_import is not None:
  47. lines.insert(last_future_import + 1, "import gc\n")
  48. elif insert_at is not None:
  49. lines.insert(insert_at, "import gc\n")
  50. else:
  51. lines.insert(0, "import gc\n")
  52. changed = True
  53. def find_line(pattern: str) -> tuple[int, re.Match[str]]:
  54. regex = re.compile(pattern)
  55. for index, line in enumerate(lines):
  56. match = regex.match(line)
  57. if match:
  58. return index, match
  59. raise SystemExit(f"Could not find expected vLLM pattern: {pattern}")
  60. def insert_after_docstring(func_index: int, func_indent: str, block: list[str]) -> None:
  61. insert_at = func_index + 1
  62. if insert_at < len(lines):
  63. stripped = lines[insert_at].lstrip()
  64. quote = None
  65. if stripped.startswith(chr(34) * 3):
  66. quote = chr(34) * 3
  67. elif stripped.startswith(chr(39) * 3):
  68. quote = chr(39) * 3
  69. if quote is not None:
  70. if stripped.count(quote) >= 2 and not stripped.startswith(quote * 2):
  71. insert_at += 1
  72. else:
  73. insert_at += 1
  74. while insert_at < len(lines):
  75. if quote in lines[insert_at]:
  76. insert_at += 1
  77. break
  78. insert_at += 1
  79. lines[insert_at:insert_at] = block
  80. if not profile_cleanup_present:
  81. snapshot_line = (
  82. r"^(?P<indent>[ \t]+)free_gpu_memory = "
  83. r"profile_result\.after_profile\.free_memory\n$"
  84. )
  85. index, match = find_line(snapshot_line)
  86. indent = match.group("indent")
  87. lines[index:index] = [
  88. f"{indent}# spark-vllm-docker: post-profile cleanup before KV sizing\n",
  89. f'{indent}if self.device_config.device_type == "cuda":\n',
  90. f"{indent} before_cleanup = profile_result.after_profile.free_memory\n",
  91. f'{indent} if hasattr(self.model_runner, "_cleanup_profiling_kv_cache"):\n',
  92. f"{indent} self.model_runner._cleanup_profiling_kv_cache()\n",
  93. f"{indent} gc.collect()\n",
  94. f"{indent} torch.cuda.synchronize(self.device)\n",
  95. f"{indent} torch.cuda.empty_cache()\n",
  96. f"{indent} profile_result.after_profile.measure()\n",
  97. f"{indent} diff_from_create = (\n",
  98. f"{indent} profile_result.after_profile "
  99. "- profile_result.before_create\n",
  100. f"{indent} )\n",
  101. f"{indent} profile_result.non_torch_increase = (\n",
  102. f"{indent} diff_from_create.non_torch_memory\n",
  103. f"{indent} )\n",
  104. f"{indent} profile_result.non_kv_cache_memory = (\n",
  105. f"{indent} profile_result.non_torch_increase\n",
  106. f"{indent} + profile_result.torch_peak_increase\n",
  107. f"{indent} + profile_result.weights_memory\n",
  108. f"{indent} )\n",
  109. f"{indent} cleanup_freed = (\n",
  110. f"{indent} profile_result.after_profile.free_memory - before_cleanup\n",
  111. f"{indent} )\n",
  112. f"{indent} if cleanup_freed > 0:\n",
  113. f"{indent} logger.info_once(\n",
  114. f'{indent} "Freed %.2f GiB before KV cache sizing; "\n',
  115. f'{indent} "non-torch profile increase is %.2f GiB.",\n',
  116. f"{indent} cleanup_freed / (1024**3),\n",
  117. f"{indent} profile_result.non_torch_increase / (1024**3),\n",
  118. f"{indent} )\n",
  119. "\n",
  120. ]
  121. changed = True
  122. if not prealloc_cleanup_present:
  123. func_index = None
  124. func_indent = None
  125. for i, line in enumerate(lines):
  126. match = re.match(
  127. r"^(?P<indent>[ \t]+)def initialize_from_config"
  128. r"\(self,\s*kv_cache_config\b",
  129. line,
  130. )
  131. if match:
  132. func_index = i
  133. func_indent = match.group("indent")
  134. break
  135. if func_index is None or func_indent is None:
  136. raise SystemExit("Could not find initialize_from_config in vLLM gpu_worker.py")
  137. body_indent = func_indent + " "
  138. block = [
  139. f"{body_indent}# spark-vllm-docker: pre-KV cache allocator cleanup\n",
  140. f'{body_indent}if self.device_config.device_type == "cuda":\n',
  141. f"{body_indent} gc.collect()\n",
  142. f"{body_indent} torch.cuda.synchronize(self.device)\n",
  143. f"{body_indent} cached_memory = max(\n",
  144. f"{body_indent} torch.cuda.memory_reserved(self.device)\n",
  145. f"{body_indent} - torch.cuda.memory_allocated(self.device),\n",
  146. f"{body_indent} 0,\n",
  147. f"{body_indent} )\n",
  148. f"{body_indent} torch.cuda.empty_cache()\n",
  149. f"{body_indent} if cached_memory > 0:\n",
  150. f"{body_indent} logger.info_once(\n",
  151. f'{body_indent} "Cleared %.2f GiB of cached CUDA allocator '
  152. 'memory before "\n',
  153. f'{body_indent} "KV cache allocation.",\n',
  154. f"{body_indent} cached_memory / (1024**3),\n",
  155. f"{body_indent} )\n",
  156. "\n",
  157. ]
  158. insert_after_docstring(func_index, func_indent, block)
  159. changed = True
  160. if changed:
  161. target.write_text("".join(lines))
  162. print("Applied Spark KV cache cleanup patch")
  163. else:
  164. print("Equivalent Spark KV cache cleanup already present; skipping")