fill_benchmark_raw_metrics.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. #!/usr/bin/env python3
  2. """
  3. Redump complete benchmark detail JSON from GPUStack into local benchmark result files.
  4. Examples:
  5. python3 hack/perf/fill_benchmark_raw_metrics.py \
  6. --dir $RESULTS_DIR \
  7. --gpustack-url $GPUSTACK_URL \
  8. --gpustack-token $GPUSTACK_TOKEN
  9. python3 hack/perf/fill_benchmark_raw_metrics.py \
  10. --dir $RESULTS_DIR \
  11. --gpustack-url $GPUSTACK_URL \
  12. --gpustack-token $GPUSTACK_TOKEN \
  13. --force
  14. """
  15. from __future__ import annotations
  16. import argparse
  17. import json
  18. import ssl
  19. import urllib.error
  20. import urllib.request
  21. from pathlib import Path
  22. from typing import Any, Dict
  23. METRIC_FIELDS = [
  24. "raw_metrics",
  25. "requests_per_second_mean",
  26. "request_latency_mean",
  27. "time_per_output_token_mean",
  28. "inter_token_latency_mean",
  29. "time_to_first_token_mean",
  30. "tokens_per_second_mean",
  31. "output_tokens_per_second_mean",
  32. "input_tokens_per_second_mean",
  33. "request_concurrency_mean",
  34. "request_concurrency_max",
  35. "request_total",
  36. "request_successful",
  37. "request_errored",
  38. "request_incomplete",
  39. ]
  40. def build_parser() -> argparse.ArgumentParser:
  41. parser = argparse.ArgumentParser(
  42. description=(
  43. "Scan local benchmark result JSON files and redump the full benchmark "
  44. "detail from the GPUStack server using each file's benchmark id."
  45. )
  46. )
  47. parser.add_argument(
  48. "--dir",
  49. required=True,
  50. help="Directory containing benchmark result JSON files. The scan is recursive.",
  51. )
  52. parser.add_argument("--gpustack-url", required=True, help="GPUStack base URL.")
  53. parser.add_argument(
  54. "--gpustack-token", required=True, help="GPUStack API token for authentication."
  55. )
  56. parser.add_argument(
  57. "--force",
  58. action="store_true",
  59. help="Redump files even if local metrics are already populated.",
  60. )
  61. parser.add_argument(
  62. "--timeout",
  63. type=int,
  64. default=60,
  65. help="HTTP timeout in seconds for each benchmark detail request.",
  66. )
  67. parser.add_argument(
  68. "--insecure",
  69. action="store_true",
  70. help="Disable TLS certificate verification. Use only for self-signed servers.",
  71. )
  72. return parser
  73. def create_ssl_context(insecure: bool) -> ssl.SSLContext:
  74. context = ssl.create_default_context()
  75. if insecure:
  76. context.check_hostname = False
  77. context.verify_mode = ssl.CERT_NONE
  78. return context
  79. def fetch_benchmark_detail(
  80. base_url: str,
  81. token: str,
  82. benchmark_id: int,
  83. timeout: int,
  84. ssl_context: ssl.SSLContext,
  85. ) -> Dict[str, Any]:
  86. url = f"{base_url.rstrip('/')}/v2/benchmarks/{benchmark_id}"
  87. request = urllib.request.Request(
  88. url=url,
  89. headers={
  90. "Accept": "application/json, text/plain, */*",
  91. "Authorization": f"Bearer {token}",
  92. },
  93. method="GET",
  94. )
  95. try:
  96. with urllib.request.urlopen(
  97. request, timeout=timeout, context=ssl_context
  98. ) as response:
  99. return json.loads(response.read().decode("utf-8"))
  100. except urllib.error.HTTPError as exc:
  101. details = exc.read().decode("utf-8", errors="replace").strip()
  102. raise RuntimeError(f"GET {url} failed: {exc.code} {details}") from exc
  103. def should_redump(payload: Dict[str, Any], force: bool) -> bool:
  104. if force:
  105. return True
  106. return any(payload.get(field) is None for field in METRIC_FIELDS)
  107. def process_file(
  108. file_path: Path,
  109. *,
  110. base_url: str,
  111. token: str,
  112. timeout: int,
  113. ssl_context: ssl.SSLContext,
  114. force: bool,
  115. ) -> str:
  116. with file_path.open("r", encoding="utf-8") as file:
  117. payload = json.load(file)
  118. benchmark_id = payload.get("id")
  119. if not isinstance(benchmark_id, int):
  120. return "skip: missing integer id"
  121. if not should_redump(payload, force):
  122. return "skip: metrics already populated"
  123. remote_payload = fetch_benchmark_detail(
  124. base_url=base_url,
  125. token=token,
  126. benchmark_id=benchmark_id,
  127. timeout=timeout,
  128. ssl_context=ssl_context,
  129. )
  130. if remote_payload.get("id") != benchmark_id:
  131. return "skip: server returned mismatched benchmark id"
  132. if payload == remote_payload:
  133. return "skip: local file already matches server"
  134. with file_path.open("w", encoding="utf-8") as file:
  135. json.dump(remote_payload, file, indent=2, ensure_ascii=False)
  136. file.write("\n")
  137. return "updated: full benchmark json"
  138. def main() -> None:
  139. args = build_parser().parse_args()
  140. directory = Path(args.dir).resolve()
  141. if not directory.is_dir():
  142. raise SystemExit(f"Directory not found: {directory}")
  143. ssl_context = create_ssl_context(args.insecure)
  144. json_files = sorted(directory.rglob("*.json"))
  145. if not json_files:
  146. raise SystemExit(f"No JSON files found under: {directory}")
  147. updated = 0
  148. skipped = 0
  149. failed = 0
  150. for file_path in json_files:
  151. try:
  152. result = process_file(
  153. file_path,
  154. base_url=args.gpustack_url,
  155. token=args.gpustack_token,
  156. timeout=args.timeout,
  157. ssl_context=ssl_context,
  158. force=args.force,
  159. )
  160. print(f"{file_path}: {result}")
  161. if result.startswith("updated:"):
  162. updated += 1
  163. else:
  164. skipped += 1
  165. except Exception as exc:
  166. failed += 1
  167. print(f"{file_path}: error: {exc}")
  168. print(
  169. f"Completed. total={len(json_files)} updated={updated} skipped={skipped} failed={failed}"
  170. )
  171. if __name__ == "__main__":
  172. main()