| import numpy as np
|
| import torch
|
| from collections import OrderedDict
|
| import platform
|
| from queue import Queue
|
| from threading import Lock
|
| from typing import Dict, Any, OrderedDict as OrderedDictType
|
|
|
| try:
|
| from torch.cuda import nvtx
|
| import tensorrt as trt
|
| import ctypes
|
| except ModuleNotFoundError:
|
| pass
|
|
|
|
|
| numpy_to_torch_dtype_dict = {
|
| np.uint8: torch.uint8,
|
| np.int8: torch.int8,
|
| np.int16: torch.int16,
|
| np.int32: torch.int32,
|
| np.int64: torch.int64,
|
| np.float16: torch.float16,
|
| np.float32: torch.float32,
|
| np.float64: torch.float64,
|
| np.complex64: torch.complex64,
|
| np.complex128: torch.complex128,
|
| }
|
| if np.version.full_version >= "1.24.0":
|
| numpy_to_torch_dtype_dict[np.bool_] = torch.bool
|
| else:
|
| numpy_to_torch_dtype_dict[np.bool] = torch.bool
|
|
|
| if 'trt' in globals():
|
|
|
| TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
| else:
|
| TRT_LOGGER = None
|
|
|
|
|
| class TensorRTPredictor:
|
| """
|
| Implementa l'inferenza su un engine TensorRT, utilizzando un pool di execution context
|
| ognuno dei quali possiede i propri buffer per garantire la sicurezza in ambiente multithread.
|
| """
|
|
|
| def __init__(self, **kwargs) -> None:
|
| """
|
| :param model_path: Percorso al file dell'engine serializzato.
|
| :param pool_size: Numero di execution context da mantenere nel pool.
|
| :param custom_plugin_path: (Opzionale) percorso a eventuali plugin personalizzati.
|
| :param device: Device su cui allocare i tensori (default 'cuda').
|
| :param debug: Se True, stampa informazioni di debug.
|
| """
|
| self.device = kwargs.get("device", 'cuda')
|
| self.debug = kwargs.get("debug", False)
|
| self.pool_size = kwargs.get("pool_size", 10)
|
|
|
|
|
| custom_plugin_path = kwargs.get("custom_plugin_path", None)
|
| if custom_plugin_path is not None:
|
| try:
|
| if platform.system().lower() == 'linux':
|
| ctypes.CDLL(custom_plugin_path, mode=ctypes.RTLD_GLOBAL)
|
| else:
|
|
|
| ctypes.CDLL(custom_plugin_path, mode=ctypes.RTLD_GLOBAL, winmode=0)
|
| except Exception as e:
|
| raise RuntimeError(f"Errore nel caricamento del plugin personalizzato: {e}")
|
|
|
|
|
| engine_path = kwargs.get("model_path", None)
|
| if not engine_path:
|
| raise ValueError("Il parametro 'model_path' è obbligatorio.")
|
|
|
|
|
| try:
|
| with open(engine_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
|
| engine_data = f.read()
|
| self.engine = runtime.deserialize_cuda_engine(engine_data)
|
| except Exception as e:
|
| raise RuntimeError(f"Errore nella deserializzazione dell'engine: {e}")
|
|
|
| if self.engine is None:
|
| raise RuntimeError("La deserializzazione dell'engine è fallita.")
|
|
|
|
|
| self.inputs = []
|
| self.outputs = []
|
| for idx in range(self.engine.num_io_tensors):
|
| name = self.engine.get_tensor_name(idx)
|
| mode = self.engine.get_tensor_mode(name)
|
| shape = list(self.engine.get_tensor_shape(name))
|
| dtype = trt.nptype(self.engine.get_tensor_dtype(name))
|
| binding = {
|
| "index": idx,
|
| "name": name,
|
| "dtype": dtype,
|
| "shape": shape,
|
| }
|
| if mode == trt.TensorIOMode.INPUT:
|
| self.inputs.append(binding)
|
| else:
|
| self.outputs.append(binding)
|
|
|
| if len(self.inputs) == 0 or len(self.outputs) == 0:
|
| raise RuntimeError("L'engine deve avere almeno un input e un output.")
|
|
|
|
|
| self.context_pool = Queue(maxsize=self.pool_size)
|
|
|
| self.lock = Lock()
|
| for _ in range(self.pool_size):
|
| context = self.engine.create_execution_context()
|
| buffers = self._allocate_buffers()
|
| self.context_pool.put({"context": context, "buffers": buffers})
|
|
|
| def _allocate_buffers(self) -> OrderedDictType[str, torch.Tensor]:
|
| """
|
| Alloca un dizionario di tensori per tutti gli I/O del modello, tenendo conto di eventuali
|
| dimensioni dinamiche. Viene restituito un OrderedDict in cui la chiave è il nome del tensore.
|
| """
|
| nvtx.range_push("allocate_max_buffers")
|
| buffers = OrderedDict()
|
|
|
| batch_size = 1
|
| for idx in range(self.engine.num_io_tensors):
|
| name = self.engine.get_tensor_name(idx)
|
| shape = list(self.engine.get_tensor_shape(name))
|
| is_input = self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT
|
| if -1 in shape:
|
| if is_input:
|
|
|
| profile_shape = self.engine.get_tensor_profile_shape(name, 0)[-1]
|
| shape = list(profile_shape)
|
| batch_size = shape[0]
|
| else:
|
| shape[0] = batch_size
|
| dtype = trt.nptype(self.engine.get_tensor_dtype(name))
|
| if dtype not in numpy_to_torch_dtype_dict:
|
| raise TypeError(f"Tipo numpy non supportato: {dtype}")
|
| tensor = torch.empty(tuple(shape),
|
| dtype=numpy_to_torch_dtype_dict[dtype],
|
| device=self.device)
|
| buffers[name] = tensor
|
| nvtx.range_pop()
|
| return buffers
|
|
|
| def input_spec(self) -> list:
|
| """
|
| Restituisce le specifiche degli input (nome, shape, dtype) utili per preparare gli array.
|
| """
|
| specs = []
|
| for i, inp in enumerate(self.inputs):
|
| specs.append((inp["name"], inp["shape"], inp["dtype"]))
|
| if self.debug:
|
| print(f"trt input {i} -> {inp['name']} -> {inp['shape']} -> {inp['dtype']}")
|
| return specs
|
|
|
| def output_spec(self) -> list:
|
| """
|
| Restituisce le specifiche degli output (nome, shape, dtype) utili per preparare gli array.
|
| """
|
| specs = []
|
| for i, out in enumerate(self.outputs):
|
| specs.append((out["name"], out["shape"], out["dtype"]))
|
| if self.debug:
|
| print(f"trt output {i} -> {out['name']} -> {out['shape']} -> {out['dtype']}")
|
| return specs
|
|
|
| def adjust_buffer(self, feed_dict: Dict[str, Any], context: Any, buffers: OrderedDictType[str, torch.Tensor]) -> None:
|
| """
|
| Regola le dimensioni dei buffer di input e copia i dati dal feed_dict nei tensori allocati.
|
| Se l’input è un array NumPy, lo converte in tensore Torch (sul device corretto).
|
| Imposta inoltre la shape di input nel contesto di esecuzione.
|
| """
|
| nvtx.range_push("adjust_buffer")
|
| for name, buf in feed_dict.items():
|
| if name not in buffers:
|
| raise KeyError(f"Input '{name}' non trovato nei buffer allocati.")
|
| input_tensor = buffers[name]
|
|
|
| if isinstance(buf, np.ndarray):
|
| buf_tensor = torch.from_numpy(buf).to(input_tensor.device)
|
| elif isinstance(buf, torch.Tensor):
|
| buf_tensor = buf.to(input_tensor.device)
|
| else:
|
| raise TypeError(f"Tipo di dato per '{name}' non supportato: {type(buf)}")
|
| current_shape = list(buf_tensor.shape)
|
|
|
| slices = tuple(slice(0, dim) for dim in current_shape)
|
| input_tensor[slices].copy_(buf_tensor)
|
|
|
| context.set_input_shape(name, current_shape)
|
| nvtx.range_pop()
|
|
|
| def predict(self, feed_dict: Dict[str, Any]) -> OrderedDictType[str, torch.Tensor]:
|
| """
|
| Esegue l'inferenza in modalità sincrona usando execute_v2().
|
|
|
| :param feed_dict: Dizionario di input (array numpy o tensori Torch).
|
| :return: Dizionario dei tensori (input e output) aggiornati.
|
| """
|
| pool_entry = self.context_pool.get()
|
| context = pool_entry["context"]
|
| buffers = pool_entry["buffers"]
|
|
|
| try:
|
| nvtx.range_push("set_tensors")
|
| self.adjust_buffer(feed_dict, context, buffers)
|
|
|
| for name, tensor in buffers.items():
|
|
|
| context.set_tensor_address(name, tensor.data_ptr())
|
| nvtx.range_pop()
|
|
|
|
|
| bindings = [tensor.data_ptr() for tensor in buffers.values()]
|
|
|
| nvtx.range_push("execute")
|
| noerror = context.execute_v2(bindings)
|
| nvtx.range_pop()
|
| if not noerror:
|
| raise RuntimeError("ERROR: inference failed.")
|
|
|
|
|
| return buffers
|
|
|
| finally:
|
|
|
| torch.cuda.synchronize()
|
| self.context_pool.put(pool_entry)
|
|
|
| def predict_async(self, feed_dict: Dict[str, Any], stream: torch.cuda.Stream) -> OrderedDictType[str, torch.Tensor]:
|
| """
|
| Esegue l'inferenza in modalità asincrona usando execute_async_v3().
|
|
|
| :param feed_dict: Dizionario di input (array numpy o tensori Torch).
|
| :param stream: Un CUDA stream per l'esecuzione asincrona.
|
| :return: Dizionario dei tensori (input e output) aggiornati.
|
| """
|
| pool_entry = self.context_pool.get()
|
| context = pool_entry["context"]
|
| buffers = pool_entry["buffers"]
|
|
|
| try:
|
| nvtx.range_push("set_tensors")
|
| self.adjust_buffer(feed_dict, context, buffers)
|
| for name, tensor in buffers.items():
|
| context.set_tensor_address(name, tensor.data_ptr())
|
| nvtx.range_pop()
|
|
|
|
|
| input_consumed_event = torch.cuda.Event()
|
| context.set_input_consumed_event(input_consumed_event.cuda_event)
|
|
|
| nvtx.range_push("execute_async")
|
| noerror = context.execute_async_v3(stream.cuda_stream)
|
| nvtx.range_pop()
|
| if not noerror:
|
| raise RuntimeError("ERROR: inference failed.")
|
|
|
| input_consumed_event.synchronize()
|
|
|
| return buffers
|
|
|
| finally:
|
|
|
| if stream != torch.cuda.current_stream():
|
| stream.synchronize()
|
| else:
|
| torch.cuda.synchronize()
|
| self.context_pool.put(pool_entry)
|
|
|
| def cleanup(self) -> None:
|
| """
|
| Libera tutte le risorse associate al TensorRTPredictor.
|
| Questo metodo deve essere chiamato esplicitamente prima di eliminare l'oggetto.
|
| """
|
|
|
| if hasattr(self, 'engine') and self.engine is not None:
|
| del self.engine
|
| self.engine = None
|
|
|
|
|
| if hasattr(self, 'context_pool') and self.context_pool is not None:
|
| while not self.context_pool.empty():
|
| pool_entry = self.context_pool.get()
|
| context = pool_entry.get("context", None)
|
| buffers = pool_entry.get("buffers", None)
|
| if context is not None:
|
| del context
|
| if buffers is not None:
|
| for t in buffers.values():
|
| del t
|
| self.context_pool = None
|
|
|
| self.inputs = None
|
| self.outputs = None
|
| self.pool_size = None
|
|
|
| def __del__(self) -> None:
|
|
|
| self.cleanup()
|
|
|