把YOLO模型送进生产环境这件事,我在多个项目里踩了不少坑。训练跑出来的mAP再好看,部署时延迟一高、吞吐一降,业务方立马就会找上门。TensorRT加速配合Triton服务器做统一推理服务,这套组合是目前我在GPU服务端做YOLO部署时最顺手、也最稳的路线。这篇文章就把完整的部署链路拆开来讲,从ONNX导出、TensorRT转engine,到Triton模型仓库搭建、动态批处理和压测调优,每一步该怎么做、为什么这么做、容易在哪翻车,全部记录在这里。适合已经能跑通YOLO训练、正准备把模型真正上线到服务端的同学参考。
1. 部署方案选型:为什么是TensorRT配合Triton
1.1 训练框架推理撑不起线上流量
很多刚接触部署的同学第一反应是直接用PyTorch的TorchServe,或者干脆在Flask里load一个PyTorch模型跑推理。这么做在demo阶段确实没问题,但一旦并发上来就露馅:PyTorch的推理路径里有大量动态图和框架自身的调度开销,单张卡上能支撑的并发非常有限;显存占用也偏高,一个几十兆的模型跑起来显存轻松吃掉几百兆。更麻烦的是,PyTorch生态里对多模型管理、动态批处理、模型版本切换这些生产级需求几乎没有原生支持,全得自己写。
我们要部署的是YOLO这种计算密集型模型,它本质上是卷积和矩阵运算的堆叠,非常适合在GPU上用TensorRT这种专用优化器重新编译一遍。TensorRT会把模型的计算图做层融合、精度校准、内核自动调优,推理延迟通常能比PyTorch原生下降一半以上。而Triton Inference Server则解决模型服务化的问题:模型仓库管理、并发调度、请求排队、动态批处理、多模型共用GPU,这些生产环境的标准需求它都替你干好了。
1.2 TensorRT在推理链路里扮演的角色
TensorRT是NVIDIA推出的高性能深度学习推理优化器,它不吃PyTorch的模型文件,而是吃ONNX、TensorFlow等中间格式,然后通过一系列优化手段生成一个高度优化的推理引擎文件。它做的事情可以粗略理解成:把计算图中能被合并的层合并掉,把精度从FP32降到FP16甚至INT8,把卷积、激活、归一化这些算子重新组合成更适合GPU执行的模式,然后针对你的具体GPU型号做内核层面的自动调优。
实际部署YOLOv8时,一个典型的感受是:原模型在FP32下推理一张640x640的图,单卡延迟可能在8到12毫秒;转成TensorRT的FP16引擎后,同样条件下延迟降到3到5毫秒是常规操作。如果显卡支持且对精度损失有充分验证,再开INT8量化,延迟还能进一步压低。这个差距在需要处理实时视频流或者高并发图片服务的场景里,直接决定了服务器要买几张卡。
1.3 Triton服务器解决的是服务化问题
Triton Inference Server是NVIDIA开源的推理服务器,名字很多人可能和TensorRT混淆,两者的分工完全不同。TensorRT管的是“单个模型怎么在GPU上跑得更快”,Triton管的是“模型的入口处怎么让客户端请求更合理地打到GPU上”。Triton本身不优化模型,但它支持把TensorRT引擎作为后端加载,同时支持PyTorch、ONNX Runtime、OpenVINO等多种后端,多个模型可以共存在一个模型仓库里,由Triton统一调度。
我选择Triton的核心原因有三个。第一,它原生支持动态批处理,多个客户端请求可以在服务端自动拼成一批再喂给GPU,这在GPU利用率提升上非常关键。第二,它提供标准的HTTP和gRPC接口,客户端不用关心模型推理细节,拿到的是一个稳定的API。第三,它支持模型版本管理,上线新模型时可以平滑切换、随时回滚,不会出现部署新版本就要停服的尴尬。
1.4 这套方案的适用边界
TensorRT加Triton这套组合不是万能的。如果项目规模很小,比如内部工具每天只跑几百次推理,杀鸡用牛刀反而增加维护成本;如果推理环境是纯CPU,TensorRT基本发挥不了作用,不如用OpenVINO;如果模型迭代极其频繁、每天都要训练新模型且没有完善的自动化转换流水线,TensorRT每转一次引擎都是额外时间开销,这时候就要权衡收益。
从我的经验看,这台组合最舒服的场景是:已经跑通了YOLO训练,目标是在一台或几台GPU服务器上提供稳定的推理API,且对延迟和吞吐有硬性指标要求。后面我会按照实际部署流程的顺序逐个环节展开,每个环节都会附上直接能用、可以抄作业的配置。
2. 前置环节:从YOLO权重到干净的ONNX模型
2.1 导出ONNX的详细流程
TensorRT不能直接吃.pt权重文件,需要先通过ONNX这个中间格式过渡。这一步看起来简单,实际有大讲究。我以YOLOv8为例,导出命令大致是:
yolo export model=yolov8s.pt format=onnx opset=14 simplify=Trueopset版本建议不要低于14,太老的操作集在后续TensorRT转换时会有算子不兼容的问题。simplify参数很关键,它会用ONNX Simplifier做一轮常数折叠和冗余节点清理,导出的模型更干净,TensorRT解析起来也更顺。
如果你用的是YOLOv5,统一用项目自带的export.py脚本:
python export.py --weights yolov5s.pt --include onnx --opset 14 --simplify导出的ONNX文件命名一般是yolov5s.onnx。这里有个不太起眼但很重要的细节:默认导出的模型输入是动态shape,也就是输入维度带有batch、height、width可变范围。动态shape在灵活性上有优势,但到了TensorRT阶段会增加转换复杂度,如果你的模型输入尺寸在生产环境是固定的,建议在导出时直接把尺寸定死。
2.2 固定输入尺寸还是动态尺寸
YOLO的输入尺寸直接决定计算量。以YOLOv8s为例,640x640输入和1280x1280输入,FLOPs差4倍;在TensorRT里engine的性能调优参数也会跟着尺寸走。如果你的业务图片尺寸相对统一,强烈建议在生产环境固定一个推理尺寸,比如640x640或者1280x1280,让TensorRT能把内核优化得更彻底。
固定尺寸的导出方式:
yolo export model=yolov8s.pt format=onnx opset=14 imgsz=640动态尺寸也不是不能用,TensorRT支持动态shape,它要求你在构建engine时给出min、opt、max三个档位的shape。但动态shape会让TensorRT为每个可能的尺寸都预留优化空间,构建时间变长,实际推理性能也不如固定尺寸。我做过一次对比,同样的模型在固定640时推理延迟3.8毫秒,动态shape的opt档设成640时延迟4.2毫秒左右。如果对性能敏感,固定尺寸是更好的选择。
2.3 理解ONNX输出张量的含义
拿到ONNX后直接用Netron打开看一眼,能省后面很多排查时间。YOLOv8s的输出节点通常是一个或两个,标准版本输出形状是[1, 84, 8400]。这个84的含义是4个边界框坐标加80个类别得分,8400是三个检测尺度下所有候选框的总数。也就是说模型输出的是密密麻麻的原始预测框,必须经过后处理才能得到最终的目标框。
这里要做一个关键决策:后处理逻辑放在模型外还是模型内。放在模型外,意味着Triton返回原始的84x8400或25200x85张量,由客户端或额外的后处理服务来解码、过滤、做NMS;放在模型内,需要在ONNX里直接集成NMS节点,让模型直接输出最终框坐标、类别和置信度。
2.4 NMS放在模型内还是模型外
如果你在Triton上做标准部署,我更推荐NMS放在模型外,也就是使用不包含NMS的原始ONNX,推理完拿原始预测张量,由应用侧处理。原因很实际:第一,集成NMS的模型在TensorRT里依赖高效的NMS插件,不同版本的TensorRT对NMS算子的支持有差异,转换时容易踩坑;第二,NMS属于不可并行化的逻辑,塞进模型里会拉长单次推理时间,放在模型外还能通过异步处理进一步提升吞吐。
当然也可以借助Triton的Python后端,在Triton内部实现一个后处理逻辑,输入是TensorRT的推理结果,输出是最终检测框。这种方式的好处是客户端调用逻辑简单,后处理在服务端完成,整体延迟里多出一小段后处理时间。两种方案我都会在后续章节给出具体参考实现。
2.5 预处理细节:Letterbox和归一化
YOLO系列输入都有一个经典前置操作:Letterbox。它的目的是把任意宽高比的图片等比缩放后填充到固定尺寸,比如640x640,避免直接拉伸导致目标变形影响检测精度。很多部署翻车案例都出在这一步没有用和训练时一致的Letterbox方式,导致推理结果偏移严重。
实际部署时有两种做法。一种是在客户端做Letterbox,图片预处理后把处理后的像素矩阵通过网络发送给服务端;另一种是客户端只传原始图片,服务端用Triton的Python后端或预处理模型统一做Letterbox。我个人的经验是:把Letterbox放在服务端更利于多端统一,因为不同客户端的图片尺寸可能五花八门,统一后处理能保证所有请求都按照同一套逻辑处理。
归一化也不能漏。YOLO训练时一般会把像素值从0到255归一化到0到1,也就是除以255,推理时如果不做同样的归一化,检测精度会直线下降。如果你使用TensorRT的FP16或INT8,输入数据的数值范围更得严格对齐训练时的分布。
3. TensorRT模型转换实操
3.1 使用trtexec生成引擎文件
TensorRT从ONNX生成engine文件的常用工具有trtexec命令行工具、Python API和Triton内置的模型转换机制。我建议先用trtexec验证转换通不通,确认无误后才进入部署环境。trtexec在TensorRT安装后的bin目录下,一条最常用的转换命令如下:
trtexec --onnx=yolov8s.onnx \ --saveEngine=yolov8s_fp16.engine \ --fp16 \ --workspace=4096注意新版TensorRT已经把--workspace参数替换成了--memPoolSize,如果你的版本较新,用这个写法:
trtexec --onnx=yolov8s.onnx \ --saveEngine=yolov8s_fp16.engine \ --fp16 \ --memPoolSize=4096workspace指的是TensorRT构建和运行时可以使用的显存上限,单位是MB。设置太小可能导致某些层因为显存不够而无法融合,设置太大也没有必要,通常4GB到8GB够用。构建完成后会生成一个.engine文件,这就是在特定显卡型号、特定TensorRT版本下不再依赖ONNX的推理引擎。
3.2 FP16和INT8量化怎么选
转换时选择FP16还是INT8,取决于你的业务对精度和延迟的平衡。我实测下来看,FP16基本是无损的,绝大多数检测任务的mAP下降不到0.5%,几乎可以忽略。如果你没有大量精力去校验精度,FP16是性价比最高的选择。
INT8量化则激进得多,延迟能再降30%到50%,但需要校准数据集。TensorRT在INT8模式下会使用动态范围校准来确定每个激活值的合理量化区间,校准数据集必须能代表真实业务场景中的图片分布。校准数据太少或太偏,会导致某些层的量化范围估计错误,表现就是检测精度掉得厉害或者某些目标直接检测不出来。
INT8转换命令大致是:
trtexec --onnx=yolov8s.onnx \ --saveEngine=yolov8s_int8.engine \ --int8 \ --calib=calib.txt校准文件里的每行是一个图片的二进制文件路径,TensorRT会读取并统计激活值分布。实际操作中建议准备200到1000张有代表性的图片,最好包含各种光照条件、目标类别和背景场景。第一次转换INT8后务必在验证集上跑一遍mAP对比,掉点超过1到2个百分点的话,优先检查校准数据集质量,不要盲目上线。
3.3 动态shape与显式batch的取舍
前面说推荐固定尺寸,在大方向上这基本不会错。但如果业务方明确要求支持多种输入分辨率,那就必须走动态shape路线。动态shape在构建引擎时的命令会复杂一些,需要指定输入维度范围:
trtexec --onnx=yolov8s_dynamic.onnx \ --minShapes=images:1x3x640x640 \ --optShapes=images:8x3x640x640 \ --maxShapes=images:16x3x640x640 \ --saveEngine=yolov8s_dynamic_fp16.engine \ --fp16动态shape构建时TensorRT会在min到max的范围内为每个可能的shape都做优化准备,构建时间显著变长。Triton加载这种engine时,必须在模型配置里声明每个输入输出的动态维度范围,否则会报shape不匹配错误。
从稳定性和性能角度出发,如果在Triton上配置了动态批处理,我建议构建时固定batch至一个合理范围,让Triton来拼批会导致更好的性能。engine层面的batch size不要设得过大,8到16一般足够应对大多数线上流量;设得过大反而会占用更多显存,同时会让单次推理等待拼批的时间变长。
3.4 验证engine输出的正确性
engine不是转完就能放心用的。我在实际项目中踩过一次很深的坑:INT8量化后模型在测试集上mAP看着还行,但一上线到夜间场景检测率骤降,后来排查发现是校准数据集里夜间图片太少,某个卷积层的激活值范围估计失准。从那以后我养成了一个习惯:engine转完必须在真实测试数据上跑一批推理,把检测结果可视化或和原始模型输出做对比,确定没有黑点之后才进下一步。
验证时可以用TensorRT自带的Python API加载engine,对同一张图分别用PyTorch原模型和engine推理一次,对比输出张量的差异。如果两份输出的最大绝对误差在FP16下超过0.5,就得怀疑转换过程出了什么问题。INT8的话误差会大一些,但要确保检测框最终结果没有明显退化。
4. Triton模型仓库搭建与服务配置
4.1 模型仓库目录结构
Triton启动后会加载一个模型仓库目录,这个目录的组织方式有严格约定。一个典型的仓库结构如下:
models/ └── yolov8s/ ├── 1/ │ └── model.plan └── config.pbtxt每个模型独占一个文件夹,文件夹名字就是Triton对外暴露的模型名。模型名下必须有一个config.pbtxt配置文件和一个以版本号命名的版本目录,版本目录里放实际的engine文件。Triton加载后默认启用最新版本,访问接口路径自然包含模型名和版本号。
对于TensorRT后端,engine文件通常命名为model.plan,Triton通过config.pbtxt里的platform字段识别后端类型。我见过不少人把engine文件命名为model.engine,然后在加载时报错找不到文件,实际上Triton只找约定好的文件名,所以这一块一开始就要按规范来。
4.2 config.pbtxt配置逐项解析
config.pbtxt是整个Triton部署的核心,它的格式是Protocol Buffers的文本表示。下面给出一个我常用的YOLOv8s配置模板:
name: "yolov8s" platform: "tensorrt_plan" max_batch_size: 8 input [ { name: "images" data_type: TYPE_FP32 dims: [3, 640, 640] } ] output [ { name: "output0" data_type: TYPE_FP32 dims: [84, 8400] } ] dynamic_batching { preferred_batch_size: [1, 2, 4, 8] max_queue_delay_microseconds: 100 } instance_group [ { count: 1 kind: KIND_GPU } ]name、platform、input、output这些字段比较好理解,需要特别指出的是max_batch_size。对tensorrt_plan后端来说,max_batch_size必须小于等于构建engine时设置的max batch,同时因为Triton的动态批处理是在模型层做拼批,这个值也决定了它能拼到多大的一批。配置里dims字段写的是不包含batch维度的张量形状,例如images那维实际是[3, 640, 640],Triton会自动把batch维度拼接在最前面。
4.3 加载engine时的常见不一致问题
Triton加载TensorRT engine时,最常遇到的问题就是配置里的dims和engine内部实际dims不一致。比如engine是用动态shape构建的,输入范围是[1,3,640,640]到[16,3,640,640],那么在config.pbtxt里需要写出完整的范围:
input [ { name: "images" data_type: TYPE_FP32 dims: [3, 640, 640] allow_batching: true reshape: { shape: [3, 640, 640] } } ]如果engine是固定shape构建的,config.pbtxt里的dims也必须和engine完全一致。Triton启动时会逐一校验引擎信息和配置信息,一旦对不上会直接报错并拒绝加载该模型。排查的时候可以用Triton自带的模型检查接口,在启动日志里查看具体的报错原因,通常它会明确指出哪个维度不一致,不用瞎猜。
加载方式方面,可以让Triton启动时直接加载本地engine文件,这个模式最简单,适合已经把engine构建好、不希望每次启动都重复做模型转换的场景。另一种方式是配置Triton每次对ONNX做自动转换,但我不太推荐,因为生产环境每次启动如果都要重新转一次就比较费时,还会引入版本不一致的隐患。
4.4 动态批处理配置详解
Triton的动态批处理可以说是提升GPU利用率最有效的机制。它的原理很直白:多个客户端请求到达服务器后,Triton会在一个极短的等待窗口内把它们拼成一整批送给后端,让GPU一次处理多张图。相比每个请求单独推理,拼批后显卡的计算单元利用率会显著提高。
配置里的preferred_batch_size是Triton优先尝试拼到的batch大小,max_queue_delay_microseconds是每个请求愿意等待多长时间。100微秒是比较激进的值,适合低延迟场景;如果业务允许几十毫秒的延迟,可以适当把等待时间调到500到1000微秒,提高拼到整批的概率。
我实际测试过一个场景:单张图推理延迟4毫秒,如果一次只处理一个请求,GPU利用率可能只有30%左右;打开动态批处理并把preferred_batch_size设为8后,请求在并发量大的时候能拼到8,总吞吐量提升约三倍,单请求延迟也因排队等待而有所增加,但仍在业务可接受范围内。动态批处理的参数调优必须结合线上实际的并发特征,没有一劳永逸的通用配置。
5. 服务部署与性能压测调优
5.1 用perf_analyzer做性能摸底
Triton自带的perf_analyzer工具可以很方便地模拟客户端请求并统计延迟和吞吐指标。启动Triton后,用一条命令就能开始压测:
perf_analyzer -m yolov8s \ --concurrency-range 1:8 \ --percentile=95 \ --measurement-mode=time_windows \ --measurement-interval=10000这个工具会按照指定的并发度持续发送请求,并在每个测量窗口内统计平均延迟、P95延迟、请求吞吐量等指标。concurrency-range在这里比较关键,它模拟的是同时有多少个请求在进行中,而不是QPS。P95延迟是线上体验的重要参考,比平均延迟更能反映极端情况下的表现。
压测开始前务必确认输入数据格式正确。perf_analyzer默认会生成随机数据作为输入,数据分布和真实数据差异很大,所以压测数值只能作为基线参考。如果需要更真实的压测结果,可以用一个自定义的客户端脚本,从真实图片数据集构造请求负载。
5.2 调优的顺序和方法论
性能调优的顺序我总结为:先看engine本身,再看Triton层配置,最后看网络链路。如果engine推理本身慢,后续一切调优都是空中楼阁。确认engine单次推理延迟达标后,才去调整Triton的dynamic_batching和instance_group。
instance_group里的count参数控制Triton创建几个模型实例,每个实例独占一份显存和计算资源。对于GPU后端,count通常设为1就够,不要把同一个GPU同时塞太多实例,否则显存不够会有问题。如果有多张GPU,可以按需增加实例数并在config里指定device_ids让不同实例落到不同卡上。
顺序调整的原则是:先确认动态批处理已开启且生效,观察压测时GPU利用率是否上升到80%以上;如果并发很高但GPU利用率依旧很低,再考虑增加实例数或调整preferred_batch_size。网络请求的序列化和反序列化开销容易被人忽视,建议先试用gRPC接口,它的protobuf二进制编码比HTTP的JSON格式更省网络开销。
5.3 一次完整的压测调优记录
某次实际项目中,我用一张消费级GPU部署了YOLOv8n模型。最初配置是instance_group count等于1、不开动态批处理、只测单路请求,得到的P95延迟约5.2毫秒。接着我开启dynamic_batching,把preferred_batch_size写成[1,2,4,8],并把并发度提到4,P95上升到约7.8毫秒,但吞吐量从每秒180帧提升到每秒520帧,GPU利用率从25%涨到80%以上。
再把并发度提高到8,P95进一步涨到11毫秒,但吞吐量到了每秒780帧,说明GPU已经接近满载。这个结果对业务方来说完全可以接受,最终将并发上限设置为8并在客户端做了超时保护,避免过度压榨导致延迟波动加剧。
这组数据的意义在于:延迟和吞吐在部署层面是跷跷板,你要根据业务需求来确定平衡点。实时视频分析场景往往更看重单帧延迟,能接受较低的并发吞吐;离线批量检测则更在意总吞吐,可以牺牲一些延迟换取更高处理量。
5.4 客户端调用的配置与优化
Triton的客户端接口支持HTTP和gRPC两种协议,gRPC在性能和连接复用方面更占优势。客户端请求的body结构很固定,gRPC时通过生成好的protobuf定义直接构造请求;HTTP时则用JSON格式包装输入数据。
{ "inputs": [ { "name": "images", "shape": [1, 3, 640, 640], "datatype": "FP32", "data": [[[...]]] } ], "outputs": [ { "name": "output0" } ] }客户端侧有几个优化点。首先是复用连接,不要每个请求都新建一条HTTP或gRPC连接,连接建立的开销在频繁请求下会被放大。其次是设置合理的超时时间,Triton默认的请求超时和排队逻辑可能会让有些请求在队列里等过长时间,超出业务容忍度。最后是尽量用异步请求而不是同步阻塞,异步方式能够让客户端更好地吸收批处理带来的延迟波动。
6. 常见问题排查与部署避坑速查
6.1 典型报错与解决方案对照
长期部署过程中我积累了一批高频问题,整理成速查表方便检索。
| 问题表现 | 可能原因 | 解决方案 |
|---|---|---|
| Triton启动报模型加载失败,日志显示shape mismatch | config.pbtxt的dims和engine内部dims不一致 | 用trtexec --dumpProfile或TensorRT API打印engine的输入输出维度,核对配置 |
| INT8量化后检测率明显下降 | 校准数据集分布和真实业务场景偏差大 | 从真实业务数据中抽样200到1000张重新生成校准文件 |
| 请求延迟抖动剧烈 | dynamic_batching的max_queue_delay设置过长,或instance_group配置不合理 | 缩短max_queue_delay至100微秒以内,检查是否多实例抢占显存 |
| engine构建时间过长 | 使用动态shape且opt、max范围过大 | 固定输入尺寸或缩小动态范围,避免不必要的shape组合 |
| GPU利用率低但延迟正常 | 并发不够或动态批处理未生效 | 检查config里dynamic_batching配置,提高压测并发度 |
| 输出框位置偏移明显 | Letterbox预处理和训练时不一致 | 核对缩放和填充逻辑,确保训练和推理的预处理完全相同 |
6.2 部署前后必须自查的细节清单
部署前先走一遍checklist能省掉很多半夜被叫起来排查问题的时间。第一项是TensorRT和Triton的版本兼容性,低版本TensorRT不支持新版ONNX的操作符时,转换会中断或生成的engine在Triton里加载报错。第二项是输入数据的dtype,YOLO的输入在ONNX里一般是float32,如果Triton配置写成了FP16而客户端传的是float32的numpy数组,接口会直接报类型不匹配。
第三项是显存规划。TensorRT engine在构建时申请了一部分workspace显存,运行时也需要一定的固定显存,模型加载后Triton会报告每个模型的显存占用。如果在同一张卡上部署多个模型,要提前算好总显存是否合适,留出20%左右的余量给Triton的系统开销和请求缓冲。
第四项是模型文件的版本管理。我习惯把engine文件名带上前缀标识,并在模型仓库里保留上一版本目录,方便出现问题快速回滚。Triton的模型目录版本号就是简单数字,一个目录一个版本,多个目录并存就是多版本共存。紧急回滚时只需要把版本号调整一下再重新加载Triton。
6.3 后处理逻辑在Triton里的落地思路
如果把NMS放在模型外,就需要一个额外的服务处理原始输出。一种非常实用的做法是利用Triton的Python后端,把它当作一个轻量级后处理节点。模型仓库可以配置一个模型名为yolov8s_post的Python后端模型,输入是TensorRT模型的原始输出,内部做解码和NMS,输出是最终的检测结果。
name: "yolov8s_post" backend: "python" max_batch_size: 8 input [ { name: "output0" data_type: TYPE_FP32 dims: [84, 8400] } ] output [ { name: "detections" data_type: TYPE_FP32 dims: [100, 6] } ] instance_group [ { kind: KIND_GPU } ]Python后端里的执行逻辑会读取output0张量,按照YOLOv8的格式解码出框坐标、类别和置信度,然后用非极大值抑制去掉冗余框,最后拼成固定形状的输出张量。这个方案虽然增加了一段Python调用的开销,但换来的是客户端只需要拿最终检测框,整个服务链路对调用方来说干净利落。如果对性能要求极高,可以把这段后处理逻辑写进TensorRT引擎内部的NMS插件,但可维护性会差一些。
还有一种更简单粗暴的方式是让Triton同时加载一个ONNX Runtime后端的后处理模型,ONNX直接支持非极大值抑制算子,配置层面更统一,性能也不差。这个要看团队技术栈,选了哪种就保持一致,别在仓库里混用出问题。
6.4 关于模型热更新与多模型共享
线上模型需要迭代时,Triton的多版本目录机制能派上大用场。新训练好的模型转成engine后放到一个新版本目录里,Triton会自动加载它并把流量切到新版本,老版本保留一段时间作为备用。如果新版本出现问题,可以直接通过Triton模型管理接口把流量切回旧版本,整个切换过程不需要重启服务。
多模型共享同一张GPU时,要给每个模型合理分配instance_group和显存资源。如果几个模型同时承载流量,它们之间会竞争GPU算力,互相拖慢。我的做法是对不同模型设置不同数量的实例,核心模型可以独占一个GPU实例,非核心的则共用一个实例,再通过Triton的调度策略限制每个模型的最大并发,避免一个模型打满整卡拖垮其它模型。
6.5 监控和日志方面的经验
上线后运维层面的准备同样重要。Triton自带Prometheus格式的监控指标,包括每个模型的推理请求数、平均延迟、GPU利用率、显存占用等,接入标准监控系统后能直观掌握部署状态。我习惯重点关注两个指标,一是P95延迟的波动,二是排队请求占比,后者过高说明需要扩容或者调大动态批处理窗口。
日志方面,Triton的默认日志输出比较详细,启动时能看到每个模型加载是否成功、资源占用情况、请求处理时的错误信息。排查问题时先看启动日志的模型加载部分,再结合请求日志定位是输入格式问题还是推理内部报错,绝大多数问题都能在这两个地方找到线索。
结尾
从ONNX导出到TensorRT引擎构建,再到Triton模型仓库配置和压测调优,这条部署链路每一步都有各自的坑,也有各自的技巧。我个人最深的体会是:部署工作不能指望一把梭哈一次成功,每一个环节都要做验证和记录,尤其是量化精度验证和Triton的配置文件检查,前者决定准确率、后者决定稳定性。把部署流程标准化之后,后续换模型、调参数、扩容都是机械操作,真正能把时间省下来去处理更贴合业务的逻辑。
最后再分享一个小技巧:遇到模型出了问题,先把动态批处理和INT8量化全部关掉,确认最朴素配置下推理结果是否正常;结果正常再逐步打开优化项,这样能很快定位是哪一步引入的异常。这套排查顺序帮我在多次线上问题里快速定位到了罪魁祸首。部署链路跑通只是第一步,后续的持续调优和监控体系建设才是让系统长期稳定运行的关键。