T-Rex2模型部署实战:ONNX导出与TensorRT推理优化指南
2026/9/4 21:46:27 网站建设 项目流程

1. 先把实际问题说清楚:T-Rex2的ONNX和TensorRT部署到底要解决什么

1.1 不是“再跑一遍Demo”,而是给模型一个稳定的生产形态

我第一次把T-Rex2跑起来的时候,说实话体验很爽。输入一句“a person riding a bike”或者直接在图上点几个框,它能识别出对话里压根没见过的目标类别,这种开放词汇检测能力比当年做YOLO系列时“训死几百个类”的体验超前太多了。可真到要把它当一个服务部署上线,问题就全冒出来了:PyTorch推理显存占用不可控,预热慢,接口散在Python脚本里,每次调用都得把整套推理代码再走一遍。更麻烦的是,T-Rex2不是一个“输入图片、输出框”的单一模型,它内部有图像侧、文本侧、提示侧多条链路。于是我开始认真考虑把T-Rex2推到ONNX,再用TensorRT构建成可复用的推理引擎。

这篇内容我围绕T-Rex2在ONNX和TensorRT上的完整落地路径来写。适合谁看呢?主要是那些已经跑通T-Rex2开源仓库,但下一步要把它塞进自己业务系统的同学;也包括把T-Rex2当作候选方案、想提前评估部署成本的人。我会把导出过程中为什么要拆模型、算子坑在哪、TensorRT怎么配、量化怎么做、推理结果怎么保存这些事一次讲清楚。

1.2 T-Rex2推理链路比普通检测模型长一截

一个常规的YOLO系列模型,推理链路是“图像预处理 → 单模型前向 → NMS后处理”,边界非常清楚。T-Rex2不是这个玩法,它的输入除了图像,还有文本提示(text prompt)和视觉提示(visual prompt)。文本提示先要经过文本编码器变成向量,视觉提示也要经过提示编码变成向量,图像本身要过一个比较大的主干网络提取特征,最后这些信息汇总到解码器里做目标框预测。

这条链路意味着两件事。第一,如果你不做拆分,指望把“整个T-Rex2”导成一个ONNX文件,通常会遇到大量动态分支、多模态输入混合、中间结果类型不统一的问题,导出难度会成倍上升。第二,文本侧和视觉侧的计算量差异很大,实际业务里文本提示往往可以缓存,视觉提示也不是每次都传。把这些能力分开部署,在接口设计上会灵活得多。所以,基于我踩过的坑,从一开始就应该接受一个原则:T-Rex2的部署形态是多ONNX文件、多Engine文件,而不是一个大而全的单独模型。

1.3 部署的目标是“可插拔”,不是“能在GPU上跑”

很多人误以为模型导出成TensorRT Engine就万事大吉,其实背后真正的目标是得到一个“可插拔”的推理组件。什么叫可插拔?外部服务传进来一张图、一行类别文本,经过若干次推理后返回标准化的目标框;业务方不关心你内部是不是分了三段,也不关心你用的是FP16还是INT8。

这一点决定了我在后续所有技术决策里的优先级:宁可多花时间把输入输出边界定义清楚,也不要图省事把预处理、后处理和模型前向搅在一起。T-Rex2本身支持文本提示和视觉提示,但真实业务里很多场景只用文本提示就够,我们不一定需要把视觉提示Encoder塞进主链路。如果哪天真的需要点选、框选能力,再单独把它作为一个可选模块挂上去,这样系统复杂度不会一上来就爆掉。

2. 导出前必须做的一次“结构拆解”:别把整个模型塞进一个ONNX

2.1 图像编码器:整条链路上“最重的一块砖”

T-Rex2在图像侧用的是一套类DINOv2的主干网络,负责把输入图变成多尺度的视觉特征。这一块算力消耗最大,也是TensorRT优化收益最明显的地方。导出时你需要先确认两件事:一是你的业务输入分辨率是否固定;二是你需要主干输出哪几层特征。

很多开源推理脚本里,图像编码器输出的是多尺度特征列表,因为T-Rex2的解码器要靠不同尺度的特征做目标检测。如果你在导出时只留最后一层,后面解码器会缺输入,整个推理链就断了。所以我做导出时的习惯是,先跑一遍PyTorch原模型,把张量形状和名称用代码打印出来,整理成一张“形状映射表”,再决定ONNX的输入输出怎么命名。你别嫌这一步麻烦,后面所有排错都建立在“我知道每一段输出应该是什么形状”上面。

2.2 文本编码器:很多精度问题其实出在文本侧

文本编码器是T-Rex2里最容易轻视的一个模块。它接收的是tokenized之后的类别文本,输出的是文本特征,在纯视觉检测任务里你可能觉得文本侧很简单,但实际上一旦文本模板和训练时不匹配,检测效果会肉眼可见地变差。

T-Rex2的文本提示通常不是直接把“cat”丢进模型,而是要套一层带上下文的prompt模板,比如类CLIP风格的一句描述。我在部署时一定复用官方推理脚本里的tokenizer和template拼接逻辑,绝不在前端自己重新发明一套文本处理。关于导出,我建议把输入定义为input_idsattention_mask两个张量,输出直接给解码器需要的文本特征。如果你在文本编码器导出时偷懒,把tokenizer也一起包进ONNX,后面换业务类别时会非常痛苦,因为每次新增类别都要重新跑一遍图。

2.3 解码器与提示编码:想清楚哪些东西要跟着一起进图

T-Rex2的解码器负责做跨模态融合和框预测。它接收图像特征、文本特征,以及可选的视觉提示特征,最终输出归一化的候选框和置信度。视觉提示部分,如果你只是做文本指代检测,那视觉提示Encoder可以暂时不导出,留在Python侧甚至暂时不启用;但如果你要做点选、框选交互,提示特征就必须进入解码器的输入。

从工程角度,我的建议是:解码器单独导成一个ONNX,视觉提示Encoder如果业务需要就单独再导一个,不要尝试把解码器和视觉提示Encoder强行揉成一个文件。多一个Engine只是多一次加载和调用,不会显著增加延迟,但会让每个模块的职责清晰很多。我自己实际部署时用的是“图像编码器 + 文本编码器 + 解码器”三段式,视觉提示能力只在需要交互式检测的场合才启用。

2.4 我的默认拆分方案和命名习惯

模块输入输出是否必须
Text Encoderinput_ids, attention_mask文本特征必须
Image Encoder图像Tensor多尺度图像特征必须
Visual Prompt Encoder点/框坐标、对应图像特征提示特征按需
Decoder图像特征、文本特征、提示特征boxes, scores必须

我给ONNX文件命名是这样的:trex2_text.onnxtrex2_image.onnxtrex2_decoder.onnx,如果后面加了视觉提示就是trex2_prompt.onnx。小写加下划线,避免容器之间拷贝文件时出现大小写问题。这个习惯帮我少踩了很多坑,特别是文件一多以后,你能从文件名一眼看出这个Engine是干嘛的,不会出现“到底该加载哪个”的困惑。

3. ONNX导出实操:Opset、动态Shape和算子兼容性排坑记录

3.1 先给出一份能跑通的导出模板

PyTorch转ONNX,最常用的是torch.onnx.export。我一般在模型eval()模式下,固定随机种子,准备好一个真实形状的样例输入,然后逐个模块导出。以文本编码器为例:

import torch text_encoder.eval() sample_input_ids = torch.randint(0, 1000, (1, 77), dtype=torch.long) sample_attention_mask = torch.ones(1, 77, dtype=torch.long) with torch.no_grad(): torch.onnx.export( text_encoder, (sample_input_ids, sample_attention_mask), "trex2_text.onnx", input_names=["input_ids", "attention_mask"], output_names=["text_embeddings"], dynamic_axes={ "input_ids": {0: "batch", 1: "seq_len"}, "attention_mask": {0: "batch", 1: "seq_len"}, }, opset_version=17, do_constant_folding=True, )

关于opset版本,T-Rex2这种带Transformer结构、大量Attention和Resize操作的模型,我用opset 17比较多。不是越高越好,新版opset有时会引入TensorRT还没有完全覆盖的算子写法。opset 17在TensorRT 8.6及以上版本里兼容性比较稳。如果你本地的TensorRT版本比较老,可以先降到16甚至15试试,不要一上来就追最新。

3.2 我实际踩过的“算子墙”:Resize、Einsum和Attention系

第一次导出解码器时,我在ONNX Runtime里跑得很正常,一拿到trtexec就报算子不支持。后来逐个定位,发现主要卡在几类算子上。

第一类是Resize。DINOv2和Decoder里都有多尺度特征上采样的逻辑,PyTorch导出Resize时,不同opset版本对coordinate_transformation_mode的默认处理不一样,TensorRT又对Resize的模式很挑剔。解决办法是手动指定mode="bilinear"和坐标变换模式,或者干脆把预处理里的resize搬到CPU/GPU上的通用图像处理库去,不依赖模型内部去改变输入尺寸。

第二类是Einsum。部分注意力实现里会出现Einsum算子,这个算子在PyTorch里很好用,但ONNX导出后不一定被TensorRT原生支持。我的经验是,在导出前把模型里的Einsum改成显式的matmultransposereshape组合,或者直接在模型代码里改forward的实现。改的时候要小心,改完最好先跑一遍PyTorch确认输出一致。其实这不算T-Rex2特有的问题,所有Transformer系模型转TRT都会遇到这个坎,提前改能省很多调试时间。

第三类是Attention算子里对attention_mask的处理。T-Rex2的文本编码器有padding mask,如果导出时mask的形状、dtype和原始实现不一致,很容易出现结果全对、只有padding位置出错的情况,肉眼很难看出来,但对最终框的精度会有一点影响。我的做法是让attention_mask保持int32bool类型进入ONNX,同时在导出前用全1的mask和带mask的输入对比一遍输出。

3.3 用onnxsim和onnx-graphsurgeon做减负

模型导出后的原始ONNX往往包含大量冗余节点。自注意力内部的transpose、reshape链更是又多又碎。直接拿这样的ONNX去构建TensorRT Engine,不仅编译时间变长,还可能出现“Engine构建成功但显存占用很高”的情况。

我的做法是先过一遍onnxsim

pip install onnxsim python -m onnxsim trex2_decoder_raw.onnx trex2_decoder.onnx

onnxsim能做常量折叠、公共子表达式消除,对DINOv2这种结构比较规整的主干尤其有效。但onnxsim不会帮你裁剪模型,如果T-Rex2的官方代码在推理时取主干中间的多个输出,你需要用onnx-graphsurgeon手动指定ONNX的输入输出节点,把用不到的分支裁掉。这个裁剪过程有点繁琐,但能明显降低后续TensorRT编译的复杂度和运行时显存占用,值得做。

3.4 导完先做数值验证,别急着进TensorRT

每导出一个ONNX,我都先让ONNX Runtime跑一遍,再和PyTorch输出对比,确认没问题才继续。

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("trex2_text.onnx", providers=["CPUExecutionProvider"]) onnx_out = sess.run( ["text_embeddings"], { "input_ids": sample_input_ids.numpy(), "attention_mask": sample_attention_mask.numpy(), }, )[0] torch_out = text_encoder(sample_input_ids, sample_attention_mask).detach().numpy() np.testing.assert_allclose(onnx_out, torch_out, rtol=1e-3, atol=1e-4)

这里注意,T-Rex2的输出经过softmax之前或之后,其实数值范围差别很大,我通常会在解码器输出层用更松一点的容差。做这一步的目的不是追求零误差,而是尽早发现“结构不对、算子语义不一致”这种大问题。如果这一关没过就急着去构建TensorRT Engine,后面排错会非常难定位。

4. 从ONNX到TensorRT:FP16、动态Shape与INT8量化的实测取舍

4.1 为什么跳过ONNXRuntime直接上TensorRT

有人会问,既然ONNX Runtime的GPU后端也能跑,为什么非要费劲转TensorRT?从我实际测试看,T-Rex2这种模型,主干部分卷积和Transformer结构都有大量算子,TensorRT的层融合和kernel自动调优带来的收益比ONNXRuntime明显高不少。后者更像一个兼容性很好的基础运行库,前者则专注于在NVIDIA GPU上把性能榨干。

如果你只是做原型验证,ONNXRuntime完全够用。但生产环境里延迟和显存都有硬指标,TensorRT通常更合适。另外,TensorRT还支持将Engine序列化保存,服务启动时直接反序列化加载,不用每次启动都重新做图优化,这也是工程上很舒服的一点。

4.2 trtexec构建还是自己写构建器

我一般在探索阶段用trtexec快速验证,因为它不需要写代码,一条命令就能看出ONNX能不能构建成功。

trtexec --onnx=trex2_text.onnx \ --saveEngine=trex2_text_fp16.engine \ --fp16 \ --minShapes=input_ids:1x77,attention_mask:1x77 \ --optShapes=input_ids:4x77,attention_mask:4x77 \ --maxShapes=input_ids:8x77,attention_mask:8x77

如果模型没有动态维度,直接把--minShapes--optShapes--maxShapes去掉就行。需要注意,动态Shape模式会在优化时考虑多种Shape组合,Engine会更大,显存占用也更高。如果你的业务分辨率是固定的,比如图像侧统一缩放后送到模型,我强烈建议图像编码器直接固定Shape导出,这样TensorRT能针对固定尺寸做出更激进的优化,延迟表现会更好。只有文本编码器这种真正需要动态batch或动态长度的模块,才值得开动态Shape。

还有一种情况是构建时需要指定每层精度策略,用trtexec就不够灵活了。这时候可以写一个Python构建脚本,用TensorRT的OnnxParser加载ONNX,设置网络层精度。个人经验是,T-Rex2整条链路上90%的情况用全网络FP16就够了,个别精度敏感的层可以单独设回FP32。不要一开始就在代码里做大量逐层精度控制,先把FP16整体跑通,再根据精度对齐结果去调少数层。

4.3 FP16和INT8的取舍建议

很多热词里都在问“onnx量化int8”,但T-Rex2这种开放词汇检测模型,我不建议一上来就上INT8。原因很简单:模型里大量LayerNorm、softmax、sigmoid这些操作对低精度非常敏感,主干部分用INT8以后,检测框的置信度往往会偏移,甚至出现小目标漏检。

我的建议顺序是:先跑FP32得到基线,再跑FP16。FP16在绝大多数GPU上都能带来可观的加速,而且精度损失很小,肉眼几乎看不出差别。如果FP16已经能满足业务延迟要求,那就不需要INT8。只有当FP16还是慢,且目标场景对功耗、显存有硬性要求时,才考虑INT8。

做INT8时,校准数据的质量比数量更重要。我用过的校准集在500到1000张图左右,覆盖了业务里真正的目标类别、光照条件和成像角度。校准集如果只选几十张公开图,类别和实际场景差得很远,校准出来的量化参数往往会严重拉低精度。校准最好用TensorRT的EntropyCalibrator2,输入数据直接用预处理后的模型输入格式,不要在图像读取格式上出岔子。

4.4 Docker镜像解决版本依赖问题

TensorRT最折磨人的一点是版本依赖。构建Engine时的TensorRT版本、CUDA版本如果和运行时不一致,常常会碰到反序列化失败或者算子行为不同的问题。

我现在的做法是,在NVIDIA官方的TensorRT容器里统一做构建。这样省去了自己折腾CUDA和TensorRT安装的时间,也避免了我本机“装了一套CUDA,结果TensorRT链接到另一套”的尴尬。容器运行时需要带GPU:

docker pull nvcr.io/nvidia/tensorrt:23.12-py3 docker run -it --gpus all -v $(pwd):/workspace nvcr.io/nvidia/tensorrt:23.12-py3 bash

构建好的Engine文件可以拷出来,放到实际服务环境里运行。但运行环境的TensorRT主版本最好和构建环境保持一致,至少也要保证Engine文件的序列化版本兼容。如果你换了一台GPU型号更老的机器,就别指望Engine还能直接反序列化,基本都要重新构建一次。

5. 端到端推理框架:Engine加载、三段调用与结果保存

5.1 一次完整推理的调用顺序

有了三个独立的TensorRT Engine以后,接下来要解决的是编排问题。以文本提示检测为例,一次推理的逻辑顺序是:

  1. 文本侧:类别文本经过tokenizer和模板拼接,得到input_idsattention_mask,送入Text Encoder,得到文本特征。
  2. 图像侧:原始图像预处理成Tensor,送入Image Encoder,得到多尺度图像特征。
  3. 解码:把文本特征、图像特征一起送入Decoder,得到候选框和置信度。
  4. 后处理:把归一化坐标还原到原图坐标,做置信度过滤和NMS,保存可视化结果或JSON。

这个顺序在PyTorch里很自然,但换到Engine推理后,你要额外注意两个问题。第一,文本特征可能只和类别有关、和图像无关,如果同一批类别要反复用,可以把文本特征缓存起来,不用每次都跑Text Encoder。第二,多尺度图像特征不要提前concat,Decoder的输入名如果分成了image_feat_smallimage_feat_large,你在组织输入字典时就要按Engine实际绑定的名字给,千万别靠位置猜。

5.2 TensorRT Engine封装类:三段调用的代码骨架

TensorRT推理本身不复杂,但对着Python API写封装时容易犯一个低级错误:每次推理都重新分配显存。我建议写一个极简的引擎类,在初始化时分配好所有bindings的显存,后续推理只需要把输入拷进去、执行、再把输出拷回来。

import tensorrt as trt import numpy as np import pycuda.driver as cuda class TRTEngine: def __init__(self, engine_path): logger = trt.Logger(trt.Logger.WARNING) runtime = trt.Runtime(logger) with open(engine_path, "rb") as f: engine_bytes = f.read() self.engine = runtime.deserialize_cuda_engine(engine_bytes) self.context = self.engine.create_execution_context() self.stream = cuda.Stream() self._allocate_buffers() def _allocate_buffers(self): self.inputs = {} self.outputs = {} self.bindings = [] for i in range(self.engine.num_bindings): name = self.engine.get_binding_name(i) shape = self.engine.get_binding_shape(i) size = trt.volume(shape) dtype = trt.nptype(self.engine.get_binding_dtype(i)) host_mem = cuda.pagelocked_empty(size, dtype) device_mem = cuda.mem_alloc(host_mem.nbytes) self.bindings.append(int(device_mem)) if self.engine.binding_is_input(i): self.inputs[name] = {"host": host_mem, "device": device_mem, "shape": shape} else: self.outputs[name] = {"host": host_mem, "device": device_mem, "shape": shape} def infer(self, feeds): for name, arr in feeds.items(): arr = np.ascontiguousarray(arr) cuda.memcpy_htod(self.inputs[name]["device"], arr) self.context.set_binding_shape(self.engine.get_binding_index(name), arr.shape) self.context.execute_async_v2(bindings=self.bindings, stream_handle=self.stream.handle) cuda.stream.synchronize(self.stream) results = {} for name, out in self.outputs.items(): cuda.memcpy_dtoh(out["host"], out["device"]) results[name] = out["host"].copy() return results

这个类是通用的,但有几个前提你要自己确认:输入输出的Shape、dtype和Engine构建时定义的一致;动态Shape的话需要调用set_binding_shape之后再跑。T-Rex2三段模型各自的输入输出都不一样,但用同一个封装类没问题。

编排调用时,Text Encoder先跑:

text_emb = text_engine.infer({ "input_ids": token_ids, "attention_mask": attn_mask, })["text_embeddings"]

图像Encoder跑一遍:

image_feats = image_engine.infer({"image": img_tensor})["multi_scale_features"]

我这里特地把输出名写成multi_scale_features,实际导出时它可能被拆成好几个输出。你不要照搬这个名字,一定要去看导出ONNX时定义的output_names。最靠谱的做法是导出前打印一遍官方模型forward的返回值结构,保证每个输出都和Decoder的输入完全对应。这一环没有捷径,代码里写错一个名字,跑起来报错都算好的,最怕名字对上了、顺序却是错的,那样出来的检测框会荒谬到起飞。

5.3 坐标还原:从模型输出到原图像素坐标

Decoder输出的框坐标通常是归一化的,还带有可能的letterbox或padding偏移。我在做后处理时,会先记录图像缩放和letterbox的参数,再把框还原到原图。

比如图像预处理时,你把原图长边缩放到1024,并且做了padding,那么模型预测的(cx, cy, w, h)要先把归一化坐标乘以模型输入尺寸,然后减去padding区域的偏移,最后再除以缩放系数,才能得到原图像素坐标。

这里有一个很容易踩的坑:如果你没有对图像做padding,只是单纯resize,那就不要画蛇添足减去偏移;如果你用了padding,但没有把偏移量存下来,后续画框就会整体错位。我的做法是把预处理封装成一个函数,返回处理后的Tensor、缩放系数scale、padding偏移量pad_xpad_y,让后处理永远只依赖这几个返回值,而不是在外部再算一遍。这样即使以后改了预处理逻辑,后处理也不容易崩。

置信度过滤和NMS也没什么特别。T-Rex2一次推理可能输出很多冗余框,设置一个较低的conf_threshold再配合IoU阈值做NMS,比只调高置信度阈值更稳妥。我在实际测试中发现,如果只用高置信度过滤,小目标比较容易漏;NMS都做完之后,再统一过滤一次置信度,比较合理。

5.4 结果落盘:可视化图片和JSON的常见坑

我习惯同时输出两种结果:一张画了框的图片,用于人工查看;一份结构化JSON,用于给上层业务消费。

保存图片时,先用OpenCV把原图读进来,注意此时图像是BGR格式,画框直接用BGR坐标画即可,不需要再转换。画框用的颜色、线宽要注意别把文字和框画得糊在一起,T-Rex2支持多类别同屏检测,我通常给不同类别分不同颜色,但就算只用一种颜色,也要在框上标注类别名和置信度,方便排查。

JSON保存时,一个常见的坑是置信度保留的小数位数不一致。后面做精度分析时,你可能会因为“同一个框两次推理结果差0.0001”这种噪声浪费半天时间。我的做法是统一保留6位小数,坐标保留2位小数,输出格式固定下来。另外一个坑是类别名和类别序号要同时保存,别只存序号,不然业务方拿到JSON还要自己查类别映射表。如果类别多,建议把所有支持的类别列表也一起写到JSON的meta字段里,方便回看时对得上。

6. 精度对齐、性能调优和几条没人会提醒你的工程细节

6.1 精度对齐别用肉眼,用IoU和距离指标说事

T-Rex2框架转到TensorRT之后,最容易被忽略的就是精度验证。很多人跑一两张图,看一眼框画得差不多就上线了。可等到业务方反馈“有些框位置不对”“置信度偏低”的时候,你很难判断是模型本身的问题、量化精度损失还是预处理不一致。

我的做法是写一个自动对齐脚本:读取同一批测试图,分别用PyTorch原模型和TensorRT推理链跑一遍,对输出的框做匹配,计算匹配框的IoU和中心点距离。如果匹配框IoU低于0.9,再单独打印出来看是哪些类别、什么尺寸的目标。这个环节看着麻烦,但一旦跑起来,后面改动任何精度策略,比如从FP16换到INT8,都能用同一套脚本快速评估。FP16通常不会引起大面积掉点,如果某个类别掉得特别明显,优先怀疑预处理不一致;如果所有类别的框置信度都整体下降,再怀疑量化和精度策略。

6.2 性能测试要用真实业务输入,别只测单张图

T-Rex2的性能和文本长度、图像分辨率、目标数量都有关。单张图测出来的毫秒数只能用来做初级参考,不能直接用来评估线上容量。

我一般会准备一组模拟线上分布的测试集,包含不同分辨率、不同目标密度的图片,然后统计P50、P95和P99延迟。这样做的好处是能暴露出某些图片特别慢的问题。目标数量多的时候,解码器输出的候选框数量也会变多,如果你在CPU上做的NMS成为瓶颈,会把整体耗时拉高一大截。这种情况下,把NMS放到CUDA上做,或者用TensorRT的BatchNMS插件,会有明显改善。

另一个很容易被忽视的坑是预热。TensorRT第一次推理时会做kernel选择和显存初始化,耗时明显偏高。生产中服务启动后必须做一次“假推理”预热,否则第一个真实请求的延迟会非常难看。我把预热逻辑放在服务启动阶段,用一张固定尺寸的假图,跑两三轮,确保所有kernel都被触发后再对外提供流量。

6.3 多路部署与显存复用

如果你的服务需要同时处理多路视频流或者多个请求,建议直接在进程内共享同一个Engine实例并复用上下文执行,而不是为每个线程单独加载Engine。同一个Engine可以被多个线程并发执行推理,只要上下文各自创建或者按顺序提交即可。不过要留意TensorRT的Python API在并发时的GIL问题,实际并发吞吐要求高的话,我建议把推理放到单独的C++服务里,或者用子进程的方式隔离,避免Python GIL拖累整体的多路吞吐。

显存方面,T-Rex2主干模型在FP16下显存占用可控,但如果你同时加载了Text Encoder、Image Encoder、Decoder三个Engine,显存占用要提前算好。我的经验是,把不常用的Text Encoder和Visual Prompt Encoder的Engine在空闲时释放,在需要时重新反序列化加载。不过频繁加载Engine也有反序列化开销,更稳妥的做法是保活一个最小集合,把图像编码器这种最重的Engine常驻,文本侧如果特征做了缓存,就不需要一直持有。

6.4 如果整条链路慢了,先查CPU端数据搬运

有几次我做性能优化,盯着Decoder Engine的耗时看了很久,优化了个寂寞,最后发现瓶颈在CPU端的图像预处理和四次显存拷贝上。T-Rex2三段推理意味着输入输出张量要在CPU和GPU之间来回搬运,如果预处理、Engine推理、后处理没有做好流水线,GPU会有大量时间在空等。

我在这类部署里的习惯是,尽量把图像预处理放到GPU上做,至少也要用带GPU后端的预处理库,让Camera采集到的数据直接在显存里完成缩放和归一化,避免先从GPU拷到CPU、处理完再拷回GPU。文本侧的特征如果被缓存了,整体链路会短很多,大多数请求只需要跑一次Image Encoder和Decoder,显存拷贝次数也能降下来。

这套东西做完之后,我把T-Rex2从PyTorch脚本慢慢改造成了一个三个Engine组成的推理服务,整个过程里最花时间的其实不是算子排错,而是每个环节的输入输出边界定义。如果你最近也在折腾类似的项目,我给你的建议是:先花半天时间把每个模块的张量形状和数值范围理清楚,再去碰ONNX和TensorRT,你后面会少走很多弯路。文本编码器这种小模块能缓存就缓存,图像侧能固定Shape就固定Shape,量化的事放到FP16验证完之后再纠结,一步一步来,这个模型的生产落地没有想象中那么玄乎。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询