VisoMaster / app /processors /utils /tensorrt_predictor.py
romanfratric234's picture
Upload 123 files
2cc7a05 verified
Raw
History Blame Contribute Delete
12.9 kB
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
# Dizionario per la conversione dei tipi di dati numpy a torch
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():
# Creazione di un’istanza globale di logger di TensorRT
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)
# Caricamento del plugin personalizzato (se fornito)
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:
# Su Windows eventualmente usare WinDLL o parametri specifici
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}")
# Verifica che il percorso del modello sia fornito
engine_path = kwargs.get("model_path", None)
if not engine_path:
raise ValueError("Il parametro 'model_path' è obbligatorio.")
# Caricamento dell'engine TensorRT
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.")
# Setup delle specifiche di I/O (input e output)
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.")
# Creazione del pool di execution context
self.context_pool = Queue(maxsize=self.pool_size)
# (Opzionale) Lock per eventuali operazioni critiche
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 predefinito
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)) # assicuriamoci di avere una lista
is_input = self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT
if -1 in shape:
if is_input:
# Ottiene la shape massima per il profilo 0
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]
# Converte in tensore se necessario
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)
# Copia solo la porzione effettivamente utilizzata nel buffer preallocato
slices = tuple(slice(0, dim) for dim in current_shape)
input_tensor[slices].copy_(buf_tensor)
# Imposta la shape dell'input nel contesto
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() # La Queue è thread-safe
context = pool_entry["context"]
buffers = pool_entry["buffers"]
try:
nvtx.range_push("set_tensors")
self.adjust_buffer(feed_dict, context, buffers)
# Imposta gli indirizzi dei buffer
for name, tensor in buffers.items():
# Se necessario, si può controllare che il tipo del tensore sia quello atteso
context.set_tensor_address(name, tensor.data_ptr())
nvtx.range_pop()
# Prepara i binding (lista degli indirizzi dei buffer)
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.")
# (Opzionalmente, si potrebbero restituire solo gli output)
return buffers
finally:
# Sincronizza il flusso CUDA prima di restituire il contesto
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()
# Creazione di un evento CUDA per monitorare il consumo dell'input
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:
# Sincronizza lo stream usato se diverso da quello corrente
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.
"""
# Libera l'engine TensorRT
if hasattr(self, 'engine') and self.engine is not None:
del self.engine
self.engine = None
# Libera il pool di execution context e relativi buffer
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:
# Per maggiore sicurezza, chiama cleanup nel distruttore
self.cleanup()