throughput_benchmark.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # mypy: allow-untyped-defs
  2. import torch._C
  3. def format_time(time_us=None, time_ms=None, time_s=None) -> str:
  4. """Define time formatting."""
  5. if sum([time_us is not None, time_ms is not None, time_s is not None]) != 1:
  6. raise AssertionError("Expected only one of time_us, time_ms, time_s is given.")
  7. US_IN_SECOND = 1e6
  8. US_IN_MS = 1e3
  9. if time_us is None:
  10. if time_ms is not None:
  11. time_us = time_ms * US_IN_MS
  12. elif time_s is not None:
  13. time_us = time_s * US_IN_SECOND
  14. else:
  15. raise AssertionError("Shouldn't reach here :)")
  16. if time_us >= US_IN_SECOND:
  17. return f'{time_us / US_IN_SECOND:.3f}s'
  18. if time_us >= US_IN_MS:
  19. return f'{time_us / US_IN_MS:.3f}ms'
  20. return f'{time_us:.3f}us'
  21. class ExecutionStats:
  22. def __init__(self, c_stats, benchmark_config) -> None:
  23. self._c_stats = c_stats
  24. self.benchmark_config = benchmark_config
  25. @property
  26. def latency_avg_ms(self):
  27. return self._c_stats.latency_avg_ms
  28. @property
  29. def num_iters(self):
  30. return self._c_stats.num_iters
  31. @property
  32. def iters_per_second(self):
  33. """Return total number of iterations per second across all calling threads."""
  34. return self.num_iters / self.total_time_seconds
  35. @property
  36. def total_time_seconds(self):
  37. return self.num_iters * (
  38. self.latency_avg_ms / 1000.0) / self.benchmark_config.num_calling_threads
  39. def __str__(self) -> str:
  40. return '\n'.join([
  41. "Average latency per example: " + format_time(time_ms=self.latency_avg_ms),
  42. f"Total number of iterations: {self.num_iters}",
  43. f"Total number of iterations per second (across all threads): {self.iters_per_second:.2f}",
  44. "Total time: " + format_time(time_s=self.total_time_seconds)
  45. ])
  46. class ThroughputBenchmark:
  47. """
  48. This class is a wrapper around a c++ component throughput_benchmark::ThroughputBenchmark.
  49. This wrapper on the throughput_benchmark::ThroughputBenchmark component is responsible
  50. for executing a PyTorch module (nn.Module or ScriptModule) under an inference
  51. server like load. It can emulate multiple calling threads to a single module
  52. provided. In the future we plan to enhance this component to support inter and
  53. intra-op parallelism as well as multiple models running in a single process.
  54. Please note that even though nn.Module is supported, it might incur an overhead
  55. from the need to hold GIL every time we execute Python code or pass around
  56. inputs as Python objects. As soon as you have a ScriptModule version of your
  57. model for inference deployment it is better to switch to using it in this
  58. benchmark.
  59. Example::
  60. >>> # xdoctest: +SKIP("undefined vars")
  61. >>> from torch.utils import ThroughputBenchmark
  62. >>> bench = ThroughputBenchmark(my_module)
  63. >>> # Pre-populate benchmark's data set with the inputs
  64. >>> for input in inputs:
  65. ... # Both args and kwargs work, same as any PyTorch Module / ScriptModule
  66. ... bench.add_input(input[0], x2=input[1])
  67. >>> # Inputs supplied above are randomly used during the execution
  68. >>> stats = bench.benchmark(
  69. ... num_calling_threads=4,
  70. ... num_warmup_iters = 100,
  71. ... num_iters = 1000,
  72. ... )
  73. >>> print("Avg latency (ms): {}".format(stats.latency_avg_ms))
  74. >>> print("Number of iterations: {}".format(stats.num_iters))
  75. """
  76. def __init__(self, module) -> None:
  77. if isinstance(module, torch.jit.ScriptModule):
  78. self._benchmark = torch._C.ThroughputBenchmark(module._c)
  79. else:
  80. self._benchmark = torch._C.ThroughputBenchmark(module)
  81. def run_once(self, *args, **kwargs):
  82. """
  83. Given input id (input_idx) run benchmark once and return prediction.
  84. This is useful for testing that benchmark actually runs the module you
  85. want it to run. input_idx here is an index into inputs array populated
  86. by calling add_input() method.
  87. """
  88. return self._benchmark.run_once(*args, **kwargs)
  89. def add_input(self, *args, **kwargs) -> None:
  90. """
  91. Store a single input to a module into the benchmark memory and keep it there.
  92. During the benchmark execution every thread is going to pick up a
  93. random input from the all the inputs ever supplied to the benchmark via
  94. this function.
  95. """
  96. self._benchmark.add_input(*args, **kwargs)
  97. def benchmark(
  98. self,
  99. num_calling_threads=1,
  100. num_warmup_iters=10,
  101. num_iters=100,
  102. profiler_output_path=""):
  103. """
  104. Run a benchmark on the module.
  105. Args:
  106. num_warmup_iters (int): Warmup iters are used to make sure we run a module
  107. a few times before actually measuring things. This way we avoid cold
  108. caches and any other similar problems. This is the number of warmup
  109. iterations for each of the thread in separate
  110. num_iters (int): Number of iterations the benchmark should run with.
  111. This number is separate from the warmup iterations. Also the number is
  112. shared across all the threads. Once the num_iters iterations across all
  113. the threads is reached, we will stop execution. Though total number of
  114. iterations might be slightly larger. Which is reported as
  115. stats.num_iters where stats is the result of this function
  116. profiler_output_path (str): Location to save Autograd Profiler trace.
  117. If not empty, Autograd Profiler will be enabled for the main benchmark
  118. execution (but not the warmup phase). The full trace will be saved
  119. into the file path provided by this argument
  120. This function returns BenchmarkExecutionStats object which is defined via pybind11.
  121. It currently has two fields:
  122. - num_iters - number of actual iterations the benchmark have made
  123. - avg_latency_ms - average time it took to infer on one input example in milliseconds
  124. """
  125. config = torch._C.BenchmarkConfig()
  126. config.num_calling_threads = num_calling_threads
  127. config.num_warmup_iters = num_warmup_iters
  128. config.num_iters = num_iters
  129. config.profiler_output_path = profiler_output_path
  130. c_stats = self._benchmark.benchmark(config)
  131. return ExecutionStats(c_stats, config)