Files
torzdf d21781264d Extraction Overhaul (#1533)
* Extract refactor
2026-03-20 23:52:37 +00:00

777 lines
30 KiB
Python

#! /usr/env/bin/python3
"""GPU profiling for throughput optimization"""
from __future__ import annotations
import logging
import math
import typing as T
from dataclasses import dataclass, InitVar, field
from operator import itemgetter
from threading import Event, Lock
from time import perf_counter
import numpy as np
import torch
from tqdm import tqdm
from lib.logger import parse_class_init
from lib.multithreading import FSThread
from lib.utils import get_module_objects
from plugins.extract import extract_config as cfg
from .runner import get_pipeline
from .plugin_utils import get_torch_modules, random_input_from_plugin, warmup_plugin
if T.TYPE_CHECKING:
import numpy.typing as npt
from lib.multithreading import ErrorState
from plugins.extract.base import ExtractPlugin
from .handler import ExtractHandler
from .runner import ExtractRunner
logger = logging.getLogger(__name__)
# TODO roll back to max and refine
class ModelProfile():
"""Benchmark a single PyTorch GPU plugin for inference
Parameters
----------
plugin
The plugin to benchmark for inference
max_batch_size
The maximum batch size to benchmark to
channels_last
``True`` if the input to the plugin is channels last
run_time
The amount of time, in seconds, to benchmark the plugin at each batch size
"""
# TODO This is not currently used as information from single model profiling is limited and
# adds additional time to profiling. However this is likely to be useful for deciding on device
# allocation if/when multi-gpu support is added
def __init__(self,
plugin: ExtractPlugin,
max_batch_size: int = 128,
channels_last: bool = False,
run_time: int = 10) -> None:
logger.debug(parse_class_init(locals()))
self.plugin = plugin
self._max_batch_size = max_batch_size
self.channels_last = channels_last
"""True if the plugin expects channels last input"""
self._run_time = run_time
num_tests = int(math.log2(self._max_batch_size)) + 1
self.batch_sizes = np.fromiter((2 ** i for i in range(num_tests)), dtype=np.int64)
self.iterations = np.zeros((num_tests, ), dtype=np.int64) - 1
self.vram = np.zeros((2, num_tests), dtype=np.int64) - 1
torch.cuda.empty_cache()
plugin.batch_size = 1
plugin.model = plugin.load_model()
@property
def run_time(self) -> int:
"""The amount of time, in seconds, that benchmarks were ran per batch"""
return self._run_time
def __repr__(self) -> str:
"""Pretty print for logging"""
params = {k[1:]: repr(v) for k, v in self.__dict__.items()
if k in ("_plugin", "_max_batch_size", "_channels_last", "_run_time")}
results = {k: v.tolist() for k, v in self.__dict__.items()
if k in ("batch_sizes", "iterations", "vram")}
s_params = ", ".join(f"{k}={v}" for k, v in params.items())
return f"{self.__class__.__name__}({s_params}) {results}"
def _predict(self, inputs: np.ndarray, seconds: float) -> int:
"""Run inference on a plugin for the given number of seconds
Parameters
----------
inputs
The input to use for benchmarking the plugin
seconds
The number of seconds to run benchmarking
Returns
-------
The number of iterations that were processed through the plugin
"""
start = perf_counter()
iters = 0
while perf_counter() - start < seconds:
self.plugin.process(inputs)
iters += 1
torch.cuda.synchronize()
return iters
def _output_stats(self) -> None:
"""Print the benchmark results to screen in a format that is easy to read and can be
copy and pasted (fixed width)"""
egs = [(i * b) / self._run_time for i, b in zip(self.iterations, self.batch_sizes)]
bs_str = [str(i) for i in self.batch_sizes]
eg_str = ["N/A" if i < 0 else f"{i:.1f}" for i in egs]
vram_alloc_str = ["N/A" if i < 0 else str(int(round(i / (1024 * 1024))))
for i in self.vram[0]]
vram_res_str = ["N/A" if i < 0 else str(int(round(i / (1024 * 1024))))
for i in self.vram[1]]
labels = ["BatchSize", "EG/S", "VRAM(MB) Allocated", "VRAM(MB) Reserved"]
lbl_width = max(len(i) for i in labels)
col_width = max(len(i) for i in bs_str + eg_str + vram_alloc_str + vram_res_str) + 2
for lbl, data in zip(labels, (bs_str, eg_str, vram_alloc_str, vram_res_str)):
dat = "".join([d.rjust(col_width) for d in data])
print(f" {lbl.ljust(lbl_width)}{dat}")
def __call__(self) -> None:
"""Runs benchmarking through the plugin, stores the data and outputs stats"""
logger.info("Profiling %s", self.plugin.name)
prog_bar = tqdm(self.batch_sizes, desc="Batch size 1", leave=False, smoothing=0)
for idx, batch_size in enumerate(prog_bar):
inputs = random_input_from_plugin(self.plugin, batch_size, self.channels_last)
try:
torch.cuda.empty_cache()
self._predict(inputs, 2) # warmup
torch.cuda.reset_peak_memory_stats()
iters = self._predict(inputs, self._run_time)
self.iterations[idx] = iters
self.vram[0, idx] = torch.cuda.max_memory_allocated()
self.vram[1, idx] = torch.cuda.max_memory_reserved()
except torch.cuda.OutOfMemoryError:
logger.debug("Exiting benchmark early as out of VRAM")
break
prog_bar.set_description(f"Batch size {batch_size}")
self._output_stats()
del self.plugin.model
@dataclass
class Events:
"""Holds thread events for communicating between main thread and plugins during benchmarking
Parameters
----------
ready
List of events for each plugin in the pipeline to be tested
"""
ready: list[Event]
start = Event()
stop = Event()
continue_ = Event()
def set_ready(self, index: int):
"""Set the ready event for the given index
Parameters
----------
index
The index of the ready event to set
"""
self.ready[index].set()
def wait_ready(self) -> None:
"""Wait for all ready events to set their ready flag and clear the flag"""
for ready in self.ready:
ready.wait()
ready.clear()
@dataclass
class DataTracker: # pylint:disable=too-many-instance-attributes
"""Stores data from the benchmarking process
Parameters
----------
size
The number of plugins that data is being tracked for
face_scaling
The amount of scaling to apply to downstream non-detection plugins
has_detector
``True`` if the first plugin in the pipeline is a detector
max_vram
The maximum amount of total VRAM to allow Cuda to reserve when profiling
"""
size: InitVar[int]
max_vram: InitVar[float]
face_scaling: int
has_detector: bool
vram: list[tuple[int, int]] = field(init=False, default_factory=list)
"""list of (max allocated, max reserved) VRAM for each testing phase"""
vram_limit: float = field(init=False)
"""The limit that Cuda reserved memory must remain within"""
combos_exhausted: bool = field(init=False, default=False)
"""``True`` if we have run out of possible combinations to attempt"""
_all_batch_sizes: npt.NDArray[np.int64] = field(init=False)
"""The processed batch size configurations including failed tests"""
_iterations: npt.NDArray[np.int64] = field(init=False)
"""Iterations put through each plugin at each testing phase"""
_batch_size_adjust: npt.NDArray[np.int64] = field(init=False)
"""Amount to adjust batch sizes by when we are approaching VRAM limits"""
_success: npt.NDArray[np.bool_] = field(init=False)
"""Booleans that show if each test successfully completed or OOM'd"""
_lock: Lock = field(init=False, default_factory=Lock)
"""Threading lock for updating iteration counts"""
_name: str = field(init=False, default="Profile.DataTracker")
"""Name of dataclass for logging"""
def __post_init__(self, size: int, max_vram: float) -> None:
"""Create the data storage arrays for the given input size
Parameters
----------
size
The number of plugins that data is being tracked for
max_vram
The maximum amount of total VRAM to allow Cuda to reserve when profiling
"""
self.vram_limit = torch.cuda.get_device_properties().total_memory * max_vram
self._all_batch_sizes = np.ones((1, size), dtype=np.int64)
self._success = np.array([True], dtype=bool)
self._batch_size_adjust = np.zeros((size, ), dtype=np.int64) - 1
self._iterations = np.zeros((1, size, ), dtype="int") - 1
@property
def has_oom(self) -> bool:
"""``True`` if the last iteration hit an OOM or fell outside our max VRAM threshold"""
if not self.vram:
return False
return any([np.any(self._iterations[-1] < 0), self.vram[-1][1] > self.vram_limit])
@property
def batch_sizes(self) -> npt.NDArray[np.int64]:
"""All batch size combinations that did not OOM"""
return self._all_batch_sizes[self._success]
def update_iterations(self, iterations: int, matrix_id: int) -> None:
"""Update the iteration count from a plugin runner in a thread-safe way
Parameters
----------
iterations
The iteration count for the plugin
matrix_id
The column id that belongs to the plugin
"""
with self._lock:
self._iterations[-1, matrix_id] = iterations
def add_iterations_row(self) -> None:
"""Add a new row to the iterations list"""
with self._lock:
new_row = np.zeros((1, len(self._iterations[-1])), dtype="int") - 1
self._iterations = np.concatenate([self._iterations, new_row])
def collect_vram(self) -> None:
"""Store the currently allocated and reserved Cuda VRAM stats"""
self.vram.append((torch.cuda.max_memory_allocated(), torch.cuda.max_memory_reserved()))
logger.debug("[%s] VRAM collected: %s", self._name, self.vram[-1])
def get_samples(self, index: int | None = None, adjusted: bool = False
) -> npt.NDArray[np.float64]:
"""Obtain the number of sample processed by each plugin for a certain valid batch size
combination
Parameters
----------
index
The testing index to obtain the samples for or ``None`` for all tests
adjusted
``True`` to obtain results adjusted for any non-detector scaling. Default: ``False``
Returns
-------
The number of samples processed by each plugin
"""
iters = self._iterations[self._success]
batches = self.batch_sizes
if index is not None:
iters = iters[index]
batches = batches[index]
retval = (iters * batches).astype(np.float64)
if adjusted and self.has_detector and self.face_scaling > 1:
if index is None:
retval[:, 1:] /= self.face_scaling
else:
retval[1:] /= self.face_scaling
logger.debug("[%s] Calculated samples/plugin: %s", self._name, retval.tolist())
return retval
def get_samples_stats(self,
method: T.Literal["mean", "min"],
index: int | None = None,
adjusted: bool = False) -> npt.NDArray[np.float64]:
"""Obtain the average or minimum samples processed for all plugins for a certain batch size
combination
Parameters
----------
method
``mean`` to obtain the mean number of samples for all plugins. ``min`` to obtain the
minimum number of samples processed by a plugin
index
The testing index to obtain the average samples for or ``None`` for all tests.
Default: ``None``
adjusted
``True`` to obtain results adjusted for any non-detector scaling. Default: ``False``
Returns
-------
The average number of samples processed by all plugins
"""
samples = self.get_samples(index=index, adjusted=adjusted)
dim = 1 if index is None else 0
if method == "mean":
retval = samples.mean(axis=dim)
else:
retval = samples.min(axis=dim)
logger.debug("[%s] Calculated Average samples/plugin: %s", self._name, retval.tolist())
return retval
def _handle_oom(self) -> None:
"""Update :attr:`_batch_size_adjust` in cases when we hit an OOM or exceeded our VRAM
threshold. In these instances we will either shrink our search window, or exit if we have
gone as far as we can"""
if not self.has_oom:
return
self._success[-1] = False
changed_mask = self._all_batch_sizes[-1] != self._all_batch_sizes[-2]
diff = abs(self._all_batch_sizes[-1][changed_mask] -
self._all_batch_sizes[-2][changed_mask])
if diff <= 4:
logger.debug("[%s] Minimum batch size adjustment hit. All combos exhausted",
self._name)
self.combos_exhausted = True
return
self._batch_size_adjust[changed_mask] = diff // 2
logger.debug("[%s] batch_size_adjust updated to: %s",
self._name, self._batch_size_adjust.tolist())
def add_next_batch_sizes(self) -> None:
"""Add the next batch size configuration to the batch size array based on the output from
the last test"""
self._handle_oom()
if self.combos_exhausted:
return
samples = self.get_samples(-1, adjusted=True)
p_idx = samples.argmin()
_batch_size_adjust = self._batch_size_adjust[p_idx]
next_batch = self.batch_sizes[-1].copy()
if _batch_size_adjust == -1:
next_batch[p_idx] *= 2
else:
next_batch[p_idx] += _batch_size_adjust
logger.debug("[%s] next batch sizes: %s", self._name, next_batch.tolist())
self._all_batch_sizes = np.concatenate([self._all_batch_sizes, next_batch[None]])
self._success = np.concatenate([self._success, [True]])
class Output:
"""Handles outputting of information at each test step
Parameters
----------
plugin_names
The list of plugin names in the order that they are executed
data
The DataTracker object that collects stats
run_time
The amount of time, in seconds, that each test is run
"""
def __init__(self, plugin_names: list[str], data: DataTracker, run_time: int):
logger.debug(parse_class_init(locals()))
self._data = data
self._run_time = run_time
self._header_row = [" " * 18] + plugin_names + ["Average", "Min"]
self._spacer = " "
self._label_widths = [len(h) + 2 for h in self._header_row]
def _write(self, message_list: list[str], left_justify: bool = False) -> None:
"""TQDM write a message with leading indentation
Parameters
----------
message_list
The message to write split over columns
left_justify
``True`` to left justify the data, ``False`` to right justify the data.
Default: ``False``
"""
label = message_list[0].ljust(self._label_widths[0])
message_list = message_list[1:]
if left_justify:
msg = " ".join(m.ljust(l) for m, l in zip(message_list, self._label_widths[1:]))
else:
msg = " ".join(m.rjust(l) for m, l in zip(message_list, self._label_widths[1:]))
tqdm.write(f"{self._spacer}{label}{msg}")
def __call__(self):
"""Output the latest test stats"""
if self._data.has_oom:
return
self._write(self._header_row)
self._write(["Batch Size"] + [str(int(b)) for b in self._data.batch_sizes[-1]])
egs = [f"{e:.1f}" for e in self._data.get_samples(-1) / self._run_time]
avg_egs = [f"{(self._data.get_samples_stats('mean', -1) / self._run_time):.1f}"]
min_egs = [f"{(self._data.get_samples_stats('min', -1) / self._run_time):.1f}"]
self._write(["EG/S"] + egs + avg_egs + min_egs)
if self._data.has_detector and self._data.face_scaling > 1:
lbl = [f"Scaled EG/S ({self._data.face_scaling}x)"]
egs = [f"{e:.1f}" for e in self._data.get_samples(-1, adjusted=True) / self._run_time]
avg_egs = [
f"{self._data.get_samples_stats('mean', -1, adjusted=True) / self._run_time:.1f}"]
min_egs = [
f"{self._data.get_samples_stats('min', -1, adjusted=True) / self._run_time:.1f}"]
self._write(lbl + egs + avg_egs + min_egs)
vram_alloc, vram_res = (str(int(round(v / 1024 / 1024))) for v in self._data.vram[-1])
vram_res = f"{vram_res}/{str(int(round(self._data.vram_limit / 1024 / 1024)))}"
self._write(["VRAM(MB) Allocated", vram_alloc], left_justify=True)
self._write(["VRAM(MB) Reserved", vram_res], left_justify=True)
line = "-" * (sum(self._label_widths) + (len(self._label_widths) - 2))
tqdm.write(f"{self._spacer}{line}")
class PipelineProfile():
"""Benchmark multiple PyTorch GPU plugins running simultaneously for inference
Parameters
----------
plugins
The plugins to benchmark for inference
error_state
The global FSThread error state object for the pipeline
channels_last
List indicating whether each model is channels first or last
warmup_time
The amount of time, in seconds, to warmup the plugin at each batch size
run_time
The amount of time, in seconds, to benchmark the plugin at each batch size
has_detector
``True`` if the first plugin in the pipeline is a detector
face_scaling
The amount of scaling to apply to downstream plugins (ie estimate of average number of
faces per frame). Default: 2
max_vram
The maximum percentage of total VRAM to allow Cuda to reserve when profiling, Default: 90
"""
def __init__(self,
plugins: list[ExtractPlugin],
error_state: ErrorState,
channels_last: list[bool],
warmup_time: int,
run_time: int,
has_detector: bool,
face_scaling: int = 2,
max_vram: int = 90) -> None:
logger.debug(parse_class_init(locals()))
self._warmup_time = warmup_time
self._run_time = run_time
self._current_index = 0
self._plugins = plugins
self._error_state = error_state
self._events = Events(ready=[Event() for _ in range(len(plugins))])
self._data = DataTracker(len(plugins),
max_vram / 100.,
face_scaling,
has_detector)
self._output_stats = Output([p.name for p in plugins], self._data, run_time)
self._threads = [FSThread(self._plugin_runner,
name=f"{p.name}_thread",
args=(p, i, c))
for i, (p, c) in enumerate(zip(plugins, channels_last))]
@classmethod
def _predict(cls, plugin: ExtractPlugin, inputs: np.ndarray, seconds: float) -> int:
"""Run inference on a plugin for the given number of seconds
Parameters
----------
plugin
The plugin to run inference through
inputs
The input to use for benchmarking the plugin
seconds
The number of seconds to run benchmarking
Returns
-------
The number of iterations that were processed through the plugin
"""
start = perf_counter()
iters = 0
while perf_counter() - start < seconds:
plugin.process(inputs)
iters += 1
torch.cuda.synchronize()
return iters
def _plugin_runner(self, plugin: ExtractPlugin, matrix_id: int, channels_last: bool) -> None:
"""Runs a plugin inside a thread, waits and reports to main thread by means of events
Parameters
----------
plugin
The plugin that this thread will run
matrix_id
The column id to obtain the batch size for this plugin from :attr:`matrix`
channels_last
``True`` if the input to the plugin is channels last
"""
name = plugin.name
logger.debug("[PipelineProfile] Loading '%s' (id: %s)", name, matrix_id)
plugin.batch_size = 1
plugin.model = plugin.load_model()
while True:
if self._error_state.has_error:
self._error_state.re_raise()
self._events.start.wait()
if self._events.stop.is_set():
break
batch_size = self._data.batch_sizes[-1][matrix_id]
inputs = random_input_from_plugin(plugin, batch_size, channels_last)
logger.debug("[PipelineProfile] Running test '%s'. input: %s", name, inputs.shape)
try:
self._predict(plugin, inputs, self._warmup_time) # warmup
self._events.set_ready(matrix_id)
self._events.continue_.wait()
iters = self._predict(plugin, inputs, self._run_time)
self._data.update_iterations(iters, matrix_id)
self._events.set_ready(matrix_id)
except torch.cuda.OutOfMemoryError:
logger.debug("[PipelineProfile] Exiting benchmark early as out of VRAM")
self._events.set_ready(matrix_id)
if self._events.stop.is_set():
break
del plugin.model
def _update_batch_sizes(self) -> None:
"""Output final batch sizes and update the plugins"""
best_idx = self._data.get_samples(adjusted=True).min(axis=1).argmax()
best_batch_sizes = self._data.batch_sizes[best_idx]
plugin_names = [p.name for p in self._plugins]
logger.info("[Profiler] Setting optimal batch sizes: %s",
", ".join(f"{p}: {b}" for p, b in zip(plugin_names,
self._data.batch_sizes[best_idx])))
for plugin, batch_size in zip(self._plugins, best_batch_sizes):
logger.debug("[PipelineProfile] Updating batch size for '%s': %s",
plugin.name, batch_size)
plugin.batch_size = int(batch_size)
def __call__(self) -> None:
"""Runs benchmarking through all plugins concurrently, store the data and output stats"""
prog_length = 5
for thread in self._threads:
thread.start()
while True:
if self._error_state.has_error:
self._error_state.re_raise()
msg = f"[{self._current_index}] Batches {tuple(self._data.batch_sizes[-1].tolist())}"
prog_bar = tqdm(desc=f"Benchmarking Pipeline {msg}", total=prog_length, leave=False)
torch.cuda.empty_cache()
# Warmup
prog_bar.update()
self._events.start.set()
self._events.wait_ready()
prog_bar.update()
self._events.start.clear()
# Benchmark
torch.cuda.reset_peak_memory_stats()
self._events.continue_.set()
prog_bar.update()
self._events.wait_ready()
prog_bar.update()
self._events.continue_.clear()
self._data.collect_vram()
self._output_stats()
prog_bar.update()
self._data.add_next_batch_sizes()
if self._data.combos_exhausted:
prog_bar.close()
break
self._data.add_iterations_row()
self._current_index += 1
prog_bar.close()
self._events.stop.set()
self._events.start.set()
for thread in self._threads:
thread.join()
self._update_batch_sizes()
class Profiler:
"""Profiles plugins within a pipeline
Parameters
----------
runner
The output runner from an extract pipeline that is to be profiled
"""
def __init__(self, runner: ExtractRunner) -> None:
logger.debug(parse_class_init(locals()))
logger.info("Profiling models...")
self._chain = T.cast("list[ExtractRunner[ExtractHandler]]", # For intellisense purposes
get_pipeline(runner))
self._channels_last: list[bool] = []
self._torch_runners = self._get_torch_indices()
def _check_for_torch(self, plugin: ExtractPlugin) -> bool:
"""Check whether the given runner uses PyTorch. We wait until the plugin is initialized
then recurse through it's :attr:`model` property looking for Torch Modules
Parameters
----------
plugin
The plugin to check for PyTorch usage
Returns
-------
bool
``True`` if the runner uses PyTorch
"""
model = plugin.load_model()
logger.debug("[Profiler] Scanning for torch Module: %s(%s)",
plugin.name, model.__class__.__name__)
modules = get_torch_modules(model)
if not modules:
return False
plugin.model = model
channels_last = warmup_plugin(plugin, 1)
assert channels_last is not None
self._channels_last.append(channels_last)
del plugin.model
return True
def _get_torch_indices(self) -> list[int]:
"""Obtain the indices within :attr:`_chain` that contain models running on pyTorch on the
GPU
Returns
-------
The list of indices of the runners that are running PyTorch models on the GPU
"""
retval: list[int] = []
for idx, runner in enumerate(self._chain):
if runner.handler.plugin.device.type == "cpu":
logger.debug("[Profiler] Skipping CPU model: '%s'", runner.handler.plugin_name)
continue
if self._check_for_torch(runner.handler.plugin):
logger.debug("[Profiler] Adding: '%s'", runner.handler.plugin.name)
retval.append(idx)
continue
logger.debug("[Profiler] Skipping: '%s'", runner.handler.plugin.name)
logger.debug("[Profiler] Torch runners indices: %s", retval)
if len(self._channels_last) != len(retval):
raise RuntimeError("Failed to get all channels_last information")
return retval
def _profile_isolated(self) -> list[ModelProfile]:
"""Benchmark the models in isolation and return the benchmark objects
Returns
-------
The benchmark object for each plugin tested
"""
retval: list[ModelProfile] = []
for idx, chan_last in zip(self._torch_runners, self._channels_last):
plugin = self._chain[idx].handler.plugin
profile = ModelProfile(plugin, channels_last=chan_last)
logger.debug("Benchmarking %s (%s/%s)", plugin.name, idx + 1, len(self._torch_runners))
profile()
retval.append(profile)
return retval
@classmethod
def _update_config_file(cls, plugins: list[ExtractPlugin]):
"""Update the config file if requested in settings
Parameters
----------
The plugins that have had their throughput profiled
"""
if not cfg.profile_save_config():
return
conf = cfg.load_config()
f_names = [".".join(p.__class__.__module__.rsplit(".", maxsplit=2)[-2:]) for p in plugins]
is_updated = False
for plugin_name, plugin in zip(f_names, plugins):
opts = conf.sections[plugin_name].options
opt = opts.get("batch_size", opts.get("batch_size"))
if not opt:
logger.warning("Could not update Config file for '%s' as no 'batch_size' "
"entry found", plugin.name)
continue
old_val = opt()
new_val = plugin.batch_size
if old_val == new_val:
logger.debug("[Profiler] Skipping unchanged batch size %s for '%s'",
old_val, plugin_name)
continue
logger.debug("[Profiler] Updating batch size from %s to %s for '%s'",
old_val, new_val, plugin_name)
is_updated = True
opt.set(new_val)
if not is_updated:
logger.info("No batch sizes were updated from their saved values. "
"Not saving config file")
return
logger.info("Saving config file with updated batch sizes")
conf.save_config()
def __call__(self) -> None:
"""Call the profiler"""
# model_benchmarks = self._profile_isolated() # Unused. Kept for if/when multi-gpu support
plugins = [r.handler.plugin for r in itemgetter(*self._torch_runners)(self._chain)]
has_detector = self._chain[self._torch_runners[0]].handler.plugin_type == "detect"
pipeline_benchmarks = PipelineProfile(plugins,
self._chain[0]._threads.error_state,
self._channels_last,
cfg.profile_warmup_time(),
cfg.profile_test_time(),
has_detector,
cfg.profile_num_faces(),
cfg.profile_max_vram())
pipeline_benchmarks()
self._update_config_file(plugins)
torch.cuda.empty_cache()
logger.debug("[Profiler] Starting plugin threads")
for runner in self._chain:
runner.start()
__all__ = get_module_objects(__name__)