第一次接触TensorRT的开发者往往会被其复杂的部署流程吓退,但当你真正理解其核心工作原理后,会发现它就像一台精密的推理引擎。我在实际工业部署中发现,TensorRT通过层融合(Layer Fusion)和精度校准(Precision Calibration)两大核心技术,能够将模型推理速度提升3-10倍。比如在部署ResNet50时,原始PyTorch模型在T4显卡上只能跑到120FPS,而经过TensorRT优化后轻松突破600FPS。
这个性能飞跃的关键在于TensorRT的运行时优化策略。它会分析计算图结构,将多个操作合并为单个核函数(kernel),大幅减少内存访问开销。我曾用Nsight工具对比过优化前后的内核调用情况,一个普通的卷积-BN-ReLU序列,优化后从7次显存读写降为1次,这就是为什么同样的硬件能跑出截然不同的性能。
模型转换是TensorRT部署的第一道门槛。以PyTorch为例,使用torch.onnx.export时最容易踩的坑是动态维度设置。我在部署人脸检测模型时曾遇到这样的错误:
python复制# 错误示范:未指定动态维度
torch.onnx.export(model, dummy_input, "model.onnx")
# 正确做法:显式声明动态维度
dynamic_axes = {
'input': {0: 'batch_size', 2: 'height', 3: 'width'},
'output': {0: 'batch_size'}
}
torch.onnx.export(
model,
dummy_input,
"model.onnx",
dynamic_axes=dynamic_axes
)
经验之谈:务必使用Netron可视化检查导出的ONNX结构,我曾发现过因为PyTorch版本差异导致reshape节点异常的情况。
创建builder时需要特别关注三个参数:
python复制builder = trt.Builder(logger)
builder_config = builder.create_builder_config()
builder_config.max_workspace_size = 1 << 30 # 1GB显存 workspace
builder_config.set_flag(trt.BuilderFlag.FP16) # 启用FP16加速
实测表明,workspace_size设置过小会导致某些优化策略无法展开。下表是不同模型需要的典型workspace大小:
| 模型类型 | 推荐workspace大小 | FP16加速比 |
|---|---|---|
| 分类模型(ResNet) | 512MB | 1.8-2.5x |
| 检测模型(YOLO) | 1GB | 2.0-3.0x |
| 分割模型(UNet) | 2GB | 1.5-2.0x |
模型序列化(plan文件生成)是部署的关键环节。我推荐采用以下健壮性处理方案:
python复制def build_engine(onnx_path):
explicit_batch = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
with trt.Builder(TRT_LOGGER) as builder, \
builder.create_network(explicit_batch) as network, \
trt.OnnxParser(network, TRT_LOGGER) as parser:
# 解析ONNX模型
with open(onnx_path, 'rb') as model:
if not parser.parse(model.read()):
for error in range(parser.num_errors):
print(parser.get_error(error))
return None
# 构建优化引擎
builder.max_batch_size = max_batch_size
config = builder.create_builder_config()
config.max_workspace_size = 1 << 30
engine = builder.build_engine(network, config)
# 序列化保存
with open("model.engine", "wb") as f:
f.write(engine.serialize())
return engine
避坑指南:遇到"Unsupported ONNX data type"错误时,通常是PyTorch导出时产生了不支持的opset,建议使用opset_version=11。
工业场景中经常需要处理可变尺寸输入,TensorRT 8.x之后的版本对此有完善支持。以动态batch为例:
python复制profile = builder.create_optimization_profile()
profile.set_shape(
"input_name",
min=(1, 3, 224, 224), # 最小shape
opt=(8, 3, 224, 224), # 最优shape(用于优化)
max=(32, 3, 224, 224) # 最大shape
)
config.add_optimization_profile(profile)
我在视频分析项目中实测发现,动态shape会导致约15%的性能损失,因此对于固定场景建议尽量使用静态shape。
TensorRT推理中最容易出问题的就是内存管理。正确的流程应该是:
python复制# 创建执行上下文
context = engine.create_execution_context()
# 分配输入输出缓冲区
inputs, outputs, bindings = [], [], []
stream = cuda.Stream()
for binding in engine:
size = trt.volume(engine.get_binding_shape(binding)) * engine.max_batch_size
dtype = trt.nptype(engine.get_binding_dtype(binding))
# 分配页锁定内存(pinned memory)
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
bindings.append(int(device_mem))
if engine.binding_is_input(binding):
inputs.append({'host': host_mem, 'device': device_mem})
else:
outputs.append({'host': host_mem, 'device': device_mem})
# 执行推理
def infer(batch_input):
np.copyto(inputs[0]['host'], batch_input.ravel())
cuda.memcpy_htod_async(inputs[0]['device'], inputs[0]['host'], stream)
context.execute_async_v2(bindings=bindings, stream_handle=stream.handle)
cuda.memcpy_dtoh_async(outputs[0]['host'], outputs[0]['device'], stream)
stream.synchronize()
return outputs[0]['host']
血泪教训:忘记stream.synchronize()会导致随机推理错误,这种bug最难排查!
对于高吞吐场景,需要采用多CUDA流并行处理:
python复制class InferRunner:
def __init__(self, engine_path):
self.engine = load_engine(engine_path)
self.contexts = [self.engine.create_execution_context()
for _ in range(4)] # 4个并行上下文
self.streams = [cuda.Stream() for _ in range(4)]
# 初始化内存(同上略)
def infer(self, batch_input, stream_id=0):
context = self.contexts[stream_id]
stream = self.streams[stream_id]
# ...执行逻辑与单流相同...
实测数据显示,在T4显卡上使用4个流处理1080p视频,吞吐量从45FPS提升到170FPS。但要注意每个流需要独立的host/device内存,显存占用会线性增长。
当遇到FP16模式精度下降严重时,可以按以下步骤诊断:
python复制# 在builder_config中设置逐层调试标记
config.profiling_verbosity = trt.ProfilingVerbosity.DETAILED
python复制layer.precision = trt.DataType.FLOAT
layer.set_output_type(0, trt.DataType.FLOAT)
当推理速度不达预期时,检查这些关键点:
python复制config.set_tactic_sources(1 << int(trt.TacticSource.CUBLAS) |
1 << int(trt.TacticSource.CUBLAS_LT))
python复制config.set_flag(trt.BuilderFlag.FP16)
config.set_flag(trt.BuilderFlag.STRICT_TYPES) # 强制使用FP16
[ ] 输入数据是否4字节对齐(对某些架构很关键)
[ ] 是否禁用了调试模式
python复制builder_config.debug_sync = False # 默认就是False
我在部署EfficientNet时曾遇到因为忘记禁用调试模式,导致推理速度只有预期1/10的情况。
当遇到不支持的算子时,需要开发自定义插件。以实现Swish激活为例:
python复制class SwishPlugin(trt.IPluginV2):
def __init__(self):
super().__init__()
self.num_outputs = 1
self.plugin_namespace = ""
self.plugin_version = "1"
def initialize(self):
return 0
def terminate(self):
pass
def get_output_datatype(self, index, input_types):
return input_types[0]
def configure_plugin(self, input_specs, output_specs):
pass
def enqueue(self, batch_size, inputs, outputs, stream):
# CUDA核函数实现
swish_kernel(batch_size, inputs[0], outputs[0], stream)
return 0
# 其他必须实现的方法...
开发建议:使用CUDA 11.4+的jit编译特性可以避免不同GPU架构的兼容问题。
对于复杂业务场景,可以采用模型级联:
python复制class MultiModelPipeline:
def __init__(self, det_engine, cls_engine):
self.det_engine = det_engine
self.cls_engine = cls_engine
# 初始化两个引擎的内存和流
def run(self, image):
det_results = self.det_engine.infer(image)
roi_images = crop_roi(image, det_results)
cls_results = []
for roi in roi_images:
cls_results.append(self.cls_engine.infer(roi))
return cls_results
在智慧园区项目中,这种检测+分类的流水线设计让整体吞吐量提升了40%,因为可以针对不同模型分别优化batch size。