4. 模型导出与优化:ONNX导出、FP16/INT8量化、TensorRT引擎构建、精度验证
好,咱们进入实战环节最核心的一步——模型导出与优化。这一步说白了,就是把训练好的PyTorch模型,变成能在车规级芯片上真正跑起来的“子弹”。我个人习惯把这步叫做“炼金术”,因为处理得好,模型又快又准;处理不好,直接废掉。
4.1 ONNX导出:打通PyTorch到TensorRT的桥梁
ONNX(Open Neural Network Exchange)就像一个通用语言。PyTorch训练好的模型,得先翻译成ONNX格式,TensorRT才能读懂。我在项目中遇到过最头疼的问题,就是导出时算子不兼容。
torch.where操作,导致ONNX导出后多出三个Reshape节点,推理速度直接翻倍。后来我改用torch.clamp + 掩码乘法替代,问题才解决。
导出代码其实不复杂,但有几个关键点要注意:
import torch
import torch.onnx
def export_onnx(model, dummy_input, save_path):
model.eval()
# 设置动态轴,这对ADAS场景很重要
# 因为实际部署时,输入图像的宽高可能变化
dynamic_axes = {
'input': {0: 'batch_size', 2: 'height', 3: 'width'},
'output': {0: 'batch_size'}
}
torch.onnx.export(
model,
dummy_input,
save_path,
opset_version=11, # 我建议用11或12,兼容性好
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes=dynamic_axes
)
print(f"ONNX模型已保存至: {save_path}")
嗯,这里要注意opset_version的选择。版本太低,有些新算子不支持;版本太高,老版本的TensorRT可能不认。我个人习惯用opset 11,兼容性最好。
4.2 FP16/INT8量化:精度与速度的博弈
量化,说白了就是把模型从FP32的“高精度慢跑”,变成FP16或INT8的“低精度快跑”。在ADAS里,速度就是生命。你想想看,一个行人检测模型,FP32跑30ms,INT8跑8ms,这差距意味着什么?
4.2.1 FP16量化
FP16量化最简单,几乎无痛。大部分GPU都原生支持FP16计算。我建议你先把模型转成FP16试试,如果精度损失在1%以内,那就直接用。
# TensorRT中启用FP16
import tensorrt as trt
def build_fp16_engine(onnx_path, engine_path):
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, logger)
with open(onnx_path, 'rb') as f:
parser.parse(f.read())
config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.FP16) # 启用FP16
config.max_workspace_size = 1 << 30 # 1GB
engine = builder.build_engine(network, config)
with open(engine_path, 'wb') as f:
f.write(engine.serialize())
print(f"FP16引擎已保存至: {engine_path}")
set_precision接口可以做到。
4.2.2 INT8量化
INT8量化就复杂多了。它需要校准数据集,而且对模型结构敏感。我在项目中遇到过,同一个YOLOv8模型,在白天场景下INT8量化后mAP只掉0.5%,但到了夜间场景直接掉3%。
为什么会这样?因为校准数据集没有覆盖夜间场景。所以,校准数据一定要有代表性。
# INT8量化需要校准器
class Calibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, calibration_files, batch_size):
trt.IInt8EntropyCalibrator2.__init__(self)
self.calibration_files = calibration_files
self.batch_size = batch_size
self.index = 0
# 分配GPU内存
self.device_input = cuda.mem_alloc(1 * 3 * 640 * 640 * 4)
def get_batch(self, names):
if self.index < len(self.calibration_files):
# 读取并预处理图像
batch = self._load_batch()
cuda.memcpy_htod(self.device_input, batch)
self.index += 1
return [int(self.device_input)]
else:
return None
def get_batch_size(self):
return self.batch_size
def read_calibration_cache(self):
return None
def write_calibration_cache(self, cache):
with open('calibration.cache', 'wb') as f:
f.write(cache)
# 构建INT8引擎
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = Calibrator(calibration_files, batch_size=8)
4.3 TensorRT引擎构建:从ONNX到可执行引擎
TensorRT引擎构建,就是把ONNX模型编译成针对特定GPU优化的可执行文件。这个过程会做很多优化:层融合、内存复用、内核自动调优等。
我个人习惯把引擎构建和推理分开。引擎构建一次,推理多次。毕竟构建引擎很耗时,在车上不可能每次启动都重新构建。
def build_engine(onnx_path, engine_path, precision='fp16'):
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, logger)
# 解析ONNX
with open(onnx_path, 'rb') as f:
if not parser.parse(f.read()):
for error in range(parser.num_errors):
print(parser.get_error(error))
return None
config = builder.create_builder_config()
config.max_workspace_size = 1 << 30 # 1GB
if precision == 'fp16':
config.set_flag(trt.BuilderFlag.FP16)
elif precision == 'int8':
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = Calibrator(calibration_files, batch_size=8)
# 设置最大batch size
profile = builder.create_optimization_profile()
profile.set_shape('input', (1, 3, 640, 640), (4, 3, 640, 640), (8, 3, 640, 640))
config.add_optimization_profile(profile)
engine = builder.build_engine(network, config)
with open(engine_path, 'wb') as f:
f.write(engine.serialize())
return engine
4.4 精度验证:别让优化毁了模型
精度验证是最后一道防线。我见过太多人优化完模型,跑起来飞快,但检测结果一塌糊涂。所以,一定要量化评估。
我建议至少做三件事:
- 逐层精度对比:对比FP32和优化后模型每一层的输出差异
- 整体mAP评估:在验证集上计算mAP,确保下降在可接受范围内
- 可视化检查:随机抽几张图,肉眼对比检测结果
def validate_precision(original_model, optimized_model, dataloader):
original_mAP = evaluate(original_model, dataloader)
optimized_mAP = evaluate(optimized_model, dataloader)
drop = original_mAP - optimized_mAP
print(f"原始模型mAP: {original_mAP:.4f}")
print(f"优化后mAP: {optimized_mAP:.4f}")
print(f"精度下降: {drop:.4f}")
if drop > 0.05: # 下降超过5%
print("⚠️ 警告:精度下降过大,建议检查量化策略")
else:
print("✅ 精度验证通过")
4.5 总结:优化流程一览
| 步骤 | 操作 | 关键点 | 耗时 |
|---|---|---|---|
| 1 | ONNX导出 | 算子兼容性、动态轴设置 | 几分钟 |
| 2 | FP16量化 | 几乎无痛,直接启用 | 几分钟 |
| 3 | INT8量化 | 校准数据集要有代表性 | 几十分钟 |
| 4 | TensorRT引擎构建 | GPU架构匹配、优化profile | 几十分钟 |
| 5 | 精度验证 | 逐层对比、mAP评估、可视化 | 几小时 |
嗯,整个流程走下来,快的话半天,慢的话一两天。但别嫌麻烦,这一步做好了,后面部署就稳了。我见过太多项目,前面训练花了一个月,优化只花了一天,结果上线就出问题。说白了,优化这一步,值得你花时间。