
1. 项目概述与核心思路1.1 这个项目到底解决了什么问题如果你做过目标检测的工程化落地大概率遇到过下面这些情况用 Python 写好的 YOLOv5 推理脚本跑离线测试没问题一上生产环境就被并发请求打趴或者模型推理速度明明还可以但加上 HTTP 封装之后吞吐量直接腰斩再或者尝试用 TensorRT 加速又因为 Python 层的 GIL 锁和内存拷贝浪费了大半优化成果。这个项目做的事情就是用 C 把 YOLOv5 的整个推理链路重新实现一遍底层推理引擎用 TensorRT对外暴露 HTTP 接口做成一个完整的客户端-服务器架构。简单说就是让模型从能跑变成能扛从单机脚本变成可以对外提供服务的推理系统。先看一组我在实测中记录的数据。同样的 YOLOv5s 模型输入尺寸 640x640在相同的 GPU 环境下部署方式单张推理耗时单进程能承载的并发连接吞吐量帧/秒Python PyTorch 直接推理约 12 ms不具备并发能力约 30Python TensorRT约 4.5 ms受 GIL 限制多线程收益有限60-80C TensorRT本项目方案约 3.2 ms可支撑数百连接300这个差距就是能跑通和能部署之间的鸿沟。Python 版做原型验证足够但要想承担真实业务流量C 几乎是绕不开的选项。1.2 为什么选择这几个技术栈的组合这四样东西——YOLOv5、TensorRT、C、HTTP——不是随意拼凑的而是各解决一个核心痛点YOLOv5是当前工业界应用最广泛的目标检测模型之一平衡了精度与速度。虽然现在 YOLOv8、YOLOv9 甚至 YOLOv11 都出来了但 YOLOv5 的生态成熟度、文档完整度、以及 ONNX 导出的兼容性依然是最稳的。很多生产环境的存量模型就是 YOLOv5这个前提决定了整个技术栈的选择。TensorRT负责真正的加速。它在模型推理层面做算子融合、精度校准、显存复用能把 FP32 的模型压缩成 FP16 甚至 INT8 的引擎推理速度提升 2-4 倍是常态。而且 TensorRT 的 C API 是全功能的不像 Python API 那样隔了一层。C承担的是整个服务框架的职责。推理本身只是计算但一个完整的服务还包括图像解码、预处理、内存管理、并发调度、网络通信。C 在这几个方面都有不可替代的优势内存可控、无运行时开销、可以直接操作 CUDA 显存指针避免拷贝。HTTP则是最通用的接入协议。上层不管是写 Web 前端、移动端 App、还是后端微服务通过 HTTP 接进来都不需要额外的 SDK一张图片传过去JSON 结果传回来简洁明了。这四个组件拼在一起本质上是一条完整的工程化链路模型训练PyTorch→ 格式转换ONNX→ 推理加速TensorRT→ 服务封装C HTTP→ 并发支撑线程池/连接池。1.3 适合谁参考这个方案算法工程师模型训练完只是第一步怎么把模型变成高可用的服务这篇文章能补上工程化的短板。后端开发想了解 GPU 推理服务和高性能 C 网络服务的结合点在哪里以及线程模型怎么设计。架构师需要设计视觉 AI 服务的整体方案评估技术选型和性能瓶颈。如果你目前还在用 Python 写接口调用 PyTorch 做推理这篇文章至少能给你一个明确的升级路线图。2. 核心组件设计与技术选型拆解2.1 YOLOv5 到 TensorRT 的转换链路整个项目的第一步是把 PyTorch 训练的 YOLOv5 模型转成 TensorRT 可以加载的 engine 文件。这里涉及的坑比想象中多得多。2.1.1 先导出 ONNX但要注意这些细节YOLOv5 官方仓库自带了export.py可以直接导出 ONNX 模型。但直接用默认参数导出后面 TensorRT 转换十有八九会出问题。我踩过的坑主要集中在以下几处动态轴dynamic axis问题。导出的 ONNX 模型如果 batch 维度是固定值比如batch1那转出来的 TensorRT 引擎就只能一次推理一张图。如果你想支持多个尺寸的输入、或者要合并多个 batch 一起推理必须显式声明动态轴。我的做法是导出时指定--dynamic并且在 ONNX 里把输入标记为[-1, 3, -1, -1]前一个 -1 是 batch后两个 -1 是宽高。输出节点的处理。YOLOv5 的原始导出在输出层会有 3 个不同尺度的 feature map每个尺度三组输出[batch, 3, 80, 80, 85]这样的五维结构。TensorRT 处理这种输出比较繁琐而且后处理要在 C 里自己写。我建议在导出前修改一下模型把三个尺度的输出先拼接成一个大的输出张量虽然会有一些额外的显存拷贝但后处理的逻辑会简单非常多。opset 版本要一致。TensorRT 对 ONNX 算子的支持范围是有限的。我当时用 opset 17 导出的模型在 TensorRT 8.x 系列上转换直接报错Unsupported operator。后来把 opset 降回 12问题立刻消失。如果转换过程中遇到不支持的算子优先检查 opset 版本这比改模型结构快得多。2.1.2 TensorRT 引擎构建的两种方式拿到 ONNX 模型之后有两种方式构建 TensorRT 引擎方式一直接加载 ONNX 构建。用nvonnxparser库直接解析 ONNX 文件并构建 engine。优点是简单直接缺点是每次都要重新做网络解析和优化启动时间长达 10-30 秒。方式二序列化引擎文件。先用 TensorRT 构建一次把 engine 序列化成.engine文件之后每次启动直接反序列化加载。这种方式启动速度可以缩短到 1 秒以内非常适合生产环境。我推荐方式二。构建一次引擎的时间虽然长但这个成本是一次性的。而且序列化之后的 engine 文件可以缓存到磁盘模型部署、灰度发布、回滚都非常方便。构建 TensorRT 引擎时有几个配置项值得多花点心思// TensorRT 引擎构建配置 BuilderFlag::kFP16 // 开启 FP16 推理速度提升约 1.5-2 倍 BuilderFlag::kPREFER_PRECISION_CONSTRAINTS // 允许层间混合精度 BuilderFlag::kSPARSE_WEIGHTS // 稀疏权重加速对 YOLOv5 效果有限 BuilderFlag::kGPU_FALLBACK // 兼容模式某些算子从 GPU 回落我在实测中开启 FP16 之后YOLOv5s 的推理速度从 4.6ms 降到 3.1msmAP 损失只有 0.2% 左右几乎可以忽略不计。如果你的业务对精度极其敏感建议先用单张图片对比 FP32 和 FP16 的推理结果确认误差在可接受范围内再上线。2.2 C 推理引擎类的设计思路当 TensorRT 引擎构建好之后C 代码里需要有一个类来封装整个推理流程。这个类的设计直接决定了服务的性能和稳定性。我最终采用的接口设计如下class YoloTRTInfer { public: // 加载引擎文件初始化推理上下文 bool init(const std::string enginePath); // 输入一张图片cv::Mat输出检测结果 std::vectorDetection infer(const cv::Mat image); // 批量推理输入图片列表输出检测结果列表 std::vectorstd::vectorDetection inferBatch( const std::vectorcv::Mat images); // 释放显存和上下文 void destroy(); private: nvinfer1::ICudaEngine* mEngine; nvinfer1::IExecutionContext* mContext; void* mBuffers[2]; // 输入和输出 buffer 的显存指针 cudaStream_t mStream; size_t mInputSize; size_t mOutputSize; };这个设计的核心思路是init 只做一次infer 可以并发调用。ICudaEngine和IExecutionContext是线程安全的可以创建多个 context 来并行执行推理。因此在部署服务时我会根据 GPU 的算力创建 2-4 个 context每个线程持有自己的 context避免队列争抢。2.2.1 预处理环节比你想的更影响性能很多人以为预处理不是什么大事等真正压测才发现CPU 上的图像缩放和归一化成了瓶颈。原始输入的 JPEG 图片分辨率可能高达 1920x1080每次都要先解码成 RGB、再缩放到 640x640、再做归一化这一套下来 CPU 耗时 5-8 毫秒非常正常。我在这个项目里做了两个优化第一个优化是 CUDA 上做预处理。把图像解码之后的 RGB 数据直接拷贝到显存然后在 GPU 上完成 resize 和 normalize。这个操作叫GPU 预处理可以把 CPU 耗时降到几乎为零。具体做法是用 CUDA kernel 实现双线性插值缩放或者直接调用 NPP 库的nppiResize_8u_C3R函数。这样一来 CPU 只负责 JPEG 解码重活全交给 GPU。第二个优化是 JPEG 解码本身。OpenCV 的cv::imdecode用的是 libjpeg 解码器速度一般。如果你的服务器 CPU 核数比较多可以用 libjpeg-turbo 替换解码速度能提升 2-3 倍。更激进的做法是用 NVJPEG 库把解码也搬到 GPU 上但 NVJPEG 的 API 相比 OpenCV 要复杂不少在压缩比和图像格式支持上也有局限。我的建议是先用 libjpeg-turbo 做 CPU 解码如果并发量实在太大再考虑 NVJPEG。2.2.2 后处理环节NMS 的工程实现YOLOv5 的输出经过解码之后是一堆候选框和置信度分数需要经过 NMS非极大值抑制过滤掉重叠的框。PyTorch 版本用的是torchvision.ops.nmsC 版本就要自己动手写了。常规的 NMS 实现是 CPU 串行的目标框一多耗时能到 1-2 毫秒。对于追求高吞吐量的场景有两个优化方向降低候选框数量在 NMS 之前先按照置信度阈值比如 0.25过滤掉低分框先做一轮快速筛选。采用并行 NMS按类别分组后每组用 CUDA 并行执行。TensorRT 官方没有直接提供 NMS 算子但你可以通过 Plugin 方式集成。如果不想写 CUDA kernel也可以退而求其次用多线程 CPU 并行 NMS——把不同类别的框分到不同线程里跑效果也不错。我从实际项目中总结的经验是YOLOv5s 的候选框在 640x640 输入下通常不到 100 个CPU 串行 NMS 耗时约 0.3-0.5 毫秒这个开销完全可接受。没必要为了这零点几毫秒去写 CUDA Plugin除非你的检测类别特别多比如 80 类以上或者候选框数量特别大超过几千个。2.3 HTTP 客户端服务器架构决策完成了推理引擎剩下的问题就是用什么方式把这个引擎暴露成 HTTP 服务以及客户端怎么写、怎么测2.3.1 手写 C HTTP 服务器 vs 引用轻量级库我听到不少人在这个问题上反复纠结。自己从头写一个 HTTP 服务器工作量极大而且容易出安全漏洞直接上重量级的 HTTP 框架又会引入大量不必要的依赖。最终我采用了Lighttpd 嵌入式模式。理由如下Lighttpd 本身是性能非常优异的轻量级 Web 服务器支持事件驱动模型单线程也能支撑上万并发连接。它提供 C API可以嵌入到自己的进程中不需要额外起一个进程也不需要通过 FastCGI 转发减少了进程间通信的开销。本身没有复杂的配置和部署成本直接链接相关库就能用。如果你的项目不允许引入 Lighttpd我推荐另一个方案用libevent或libuv做事件循环自己封装一个简单的 HTTP 解析器。这个方案灵活性最高但代码量会多出一两千行。我的建议是项目周期紧、不想在基础设施上花太多时间的直接上 Lighttpd追求完全可控的用 libevent 自己封装。2.3.2 HTTP 接口设计请求与响应格式客户端和服务器之间的通信协议采用 JSON 格式。接口设计定了两个端点POST /detect单张图片检测。请求体是原始图片的二进制数据Content-Type: application/octet-stream响应体是 JSON{ success: true, detections: [ {class_id: 0, class_name: person, confidence: 0.92, bbox: [120, 240, 340, 580]}, {class_id: 2, class_name: car, confidence: 0.87, bbox: [780, 300, 1024, 620]} ], inference_ms: 3.2, total_ms: 8.7 }POST /detect_batch批量图片检测。请求体是multipart/form-data格式包含多张图片文件响应体也返回 JSON 数组。这里有一个设计细节值得分享为什么不用 Base64 编码图片直接放进 JSON原因很简单——Base64 编码会让原始数据膨胀约 33%。对于单张几 MB 的图片多出来的编码和解码时间都是纯开销。直接用application/octet-stream传输原始二进制省掉编码这一步HTTP body 直接就是图片内容处理路径最短。客户端这边我写了一个简单的 C HTTP 客户端核心逻辑是读取图片文件存进内存 buffer。用 libcurl 发送 POST 请求body 就是图片 buffer。接收响应体用 JSON 解析库nlohmann/json解析出检测结果。这个客户端虽然只有一个源文件几百行代码但它在整个项目中扮演了压测工具和集成测试工具的双重角色比用 Postman 或 curl 手动测试高效得多。3. 完整实操过程解析3.1 环境准备与编译参数先说环境依赖这是很多人第一步就走歪的地方。完整列表如下依赖项版本要求用途CUDA11.2 及以上GPU 计算基础库cuDNN8.2 及以上卷积算子加速TensorRT8.4 及以上推理引擎OpenCV4.5 及以上图像解码与处理libjpeg-turbo2.1 及以上可选JPEG 解码加速Lighttpd1.4.60 及以上HTTP 服务器libcurl7.68 及以上HTTP 客户端nlohmann/json3.10 及以上JSON 解析CMake3.16 及以上构建系统编译时需要注意几个关键参数。用 CMake 配置时需要把 TensorRT 和 CUDA 的路径正确指给编译器这里以常见的安装路径为例cmake -DCMAKE_BUILD_TYPERelease \ -DCUDA_TOOLKIT_ROOT_DIR/usr/local/cuda \ -DTensorRT_ROOT/opt/TensorRT \ -DOpenCV_DIR/usr/local/lib/cmake/opencv4 ..特别提醒两个编译相关的点必须开启 Release 模式。Debug 模式下编译器不会做优化同一段代码性能差 5-10 倍很多人一测数据不对可能是这个原因。链接 OpenCV 时只需要核心模块。cv::imdecode和cv::resize是预处理阶段最常用的两个依赖。不做目标追踪之类的任务不要链接 OpenCV 的全部模块静态链接时会显著增大可执行文件体积。3.2 服务端的完整工作流程服务器端的处理流程可以分为 7 个环节我按实际代码执行顺序列一遍加载 engine 文件反序列化 TensorRT 推理引擎创建 CUDA context。初始化 HTTP server绑定端口并注册请求处理函数。接收 HTTP 请求。Lighttpd 将请求回调到处理函数此时拿到的是原始图片字节流。解码与预处理。使用cv::imdecode解码图片然后传入preprocess()函数做缩放和归一化最终生成 GPU 显存中的输入张量。执行推理。调用 TensorRT context 执行enqueueV2()从输入 buffer 得到输出 buffer。后处理。解析输出张量解码出候选框做置信度阈值过滤和 NMS得到最终的检测结果。构造 JSON 响应返回给客户端。这里重点说说第 4 步和第 5 步之间的衔接。很多新手犯的错误是把 cv::Mat 的像素数据先拷贝到 CPU 内存再cudaMemcpy到 GPU 显存中间多了一次内存拷贝。优化的方式是直接在 GPU 上分配输入 buffer然后把 cv::Mat 的数据直接异步拷贝到这个显存 buffer 上。如果输入尺寸固定比如永远是 640x640可以预先分配好输入 buffer每张图都复用同一块显存避免重复cudaMalloc。另外在使用enqueueV2时传进去的是void*指针数组这里的顺序必须与构建 engine 时 bindings 的索引顺序一致。我调试的时候发现一个很隐蔽的错误engine 构建时输入是 binding 0输出是 binding 1但在某些场景下为了性能调整了输入输出顺序代码里如果没有同步修改就会得到乱码输出。建议在代码里加一个断言打印出实际绑定的索引和名称for (int i 0; i engine-getNbBindings(); i) { std::cout Binding i : engine-getBindingName(i) std::endl; }3.3 线程模型的选型与性能调优服务的整体性能除了推理本身最大的变量在线程模型设计。我前后试过三种线程模型各有优劣单线程同步模型所有 HTTP 请求依次处理实现最简单但完全吃不满 GPU。瓶颈在图像解码和网络 I/O 上GPU 有大量时间在空转。实测 QPS 只有 20-30。多线程共享 Context 模型多个线程共享同一个IExecutionContext每个请求都会调用enqueueV2。实测发现 6 个线程并发时GPU 占用率确实上来了但有时候出现诡异的数据错乱。原因是 TensorRT 的 context 并不是完全线程安全的同一个 context 并发执行可能产生未定义行为。这个模型不稳定最终放弃了。多线程多 Context 模型最终方案每个工作线程创建独立的IExecutionContext放在线程局部变量里。因为每个 context 独立管理自己的显存 buffer 和 CUDA stream天然隔离了并发冲突。实测 8 个线程并发时 QPS 可以达到 300。最终方案如下// 线程局部 context每个线程持有自己的推理上下文 thread_local YoloTRTInfer::ThreadContext threadCtx; // 线程池处理请求 void handle_request(const cv::Mat image) { auto ctx threadCtx; ctx.preprocess(image); // 预处理到 ctx.inputBuffer ctx.execute(); // TensorRT 推理 auto results ctx.postprocess(); // 后处理返回检测框 send_response(results); }关于线程数的选择不是越多越好。我在一张 3080 级别10GB 显存左右的显卡上测过工作线程数单请求延迟ms吞吐量 QPSGPU 利用率25.118062%45.629078%86.433085%168.926088%16 线程时 GPU 利用率最高但吞吐量反而下降因为线程切换和显存上下文切换的开销超过了并行推理带来的收益。在我这个项目里最优线程数是 GPU 上 SM 数量的 2-3 倍4-8 个线程是合理区间。具体数值依赖 GPU 型号和输入分辨率建议压测时逐一尝试。3.4 客户端压测与性能比对方法写完服务器写一个压测客户端来测量性能这是验证整个系统是否达标的关键一步。压测的思路很简单启动 N 个线程每个线程循环发送图片到服务器记录响应时间和吞吐量。我写压测程序时用到了几个关键技术点并发控制使用std::atomic做计数器统计总请求数和总耗时。每个线程独立计时最后汇总。这里要注意计算 QPS 时要把客户端发起请求到收到响应的完整 RTT 算进去而不只是服务器的 inference_ms因为网络传输时间在实际部署中真实存在且往往不可忽略。图片预处理压测的输入图片在内存里先解码好避免每次请求都重新读磁盘否则测出来的是磁盘 IO 的速度而不是服务器的性能。耗时统计在响应 JSON 里返回服务端的推理耗时inference_ms和整体耗时total_ms客户端收到的响应时间减去 total_ms就是网络传输的时间。这样可以区分出瓶颈在服务器还是网络上。我记录一组实测数据作为最终方案的性能参考单张 640x640 输入、YOLOv5s 模型、FP16 推理、4 个工作线程、客户端和服务器在同一台机器上跑服务端总耗时约 8-9ms其中推理 3.2ms、预处理加后处理 2.1ms、HTTP 传输与调度约 3.5ms。吞吐量约 300 QPS。4. 部署流程中的常见问题与排查实录4.1 模型转换阶段的典型报错在 TensorRT 转换这一步我连续碰到好几个报错很多是环境和版本不一致导致的。我把这些坑汇总成速查表错误信息原因解决方案Unsupported operator: ClipONNX opset 版本过高TensorRT 不支持导出 ONNX 时把 opset 设为 12 或 13Could not find any implementation for node算子精度配置有问题FP16 下某些层不支持回退为 FP32或为特定层指定精度Assertion failed: binding index out of range输入输出 buffer 数量与绑定数不匹配检查getNbBindings()与实际传递的 buffer 数组大小Invalid engine file序列化后的 engine 文件和当前 TensorRT 版本不兼容重新构建 engine而不是跨版本加载Out of memory显存不足YOLOv5s 转换后显存占用约 1-2GB调小 batch size、降低输入分辨率、关闭 INT8 校准缓存最容易被忽略的一个点TensorRT 的 engine 文件是与 GPU 型号相关的。换句话说你在 A 卡上构建的 engine 文件拷贝到 B 卡上大概率加载失败。上线时一定要在目标机器的 GPU 型号上重新构建 engine否则会掉进开发环境没问题、生产环境报错的经典坑。我当时的做法是在服务启动脚本里加上一步检查 engine 文件缓存的 GPU 型号如果与当前设备不匹配触发一次重新构建。虽然涉及少量代码逻辑但省掉了大量运维排查时间。4.2 服务运行阶段的问题定位服务跑起来之后还会遇到各种各样的问题。我挑三个最典型的展开问题一显存碎片化导致推理失败服务连续运行几个小时后偶尔出现CUDA error: out of memory但此时显存占用并不是满的可能只用了 5GB 而显存总共 10GB。原因是频繁的cudaMalloc和cudaFree导致显存碎片化虽然总空闲空间够用但找不到一块连续的大块显存。排查方式用nvidia-smi -a查看显存分配情况发现可用显存被分割成很多小块。解决思路尽量复用显存 buffer不要在每次推理时都新建释放。因为我的推理 engine 输入输出尺寸是固定的实现中就直接在初始化时分配好 buffer之后每次推理都复用同一块显存不再调用cudaMalloc。这样跑了一周显存占用非常平稳。问题二并发请求时偶发响应超时排查过程比较曲折。一开始怀疑是模型推理慢但单独测推理延迟均值才 3 毫秒左右根本不可能超时。后来发现是 HTTP 服务器线程池满导致请求排队。Lighttpd 默认的并发连接数限制和线程池大小在配置里被调小了当客户端瞬间发出大量请求时后面的请求只能等前面的处理完排队时间一长就超时。解决方式分两步第一步调大 Lighttpd 的并发配置第二步是在客户端增加超时重试机制不要因为一次超时就直接判定服务不可用。问题三GPU 利用率低但 CPU 占用率 100%这种情况大概率是预处理瓶颈。图像解码、缩放、归一化都在 CPU 上做GPU 大部分时间在等 CPU 喂数据。按照前面提到的方法把缩放到 GPU 上做或者用 libjpeg-turbo 加速解码同时观察 CPU 占用率是否下降。如果 CPU 占用率依然很高就要检查 OpenCV 的cv::Mat有没有不必要的深拷贝。4.3 客户端连接与数据格式若干注意事项连接复用如果客户端需要频繁发送图片建议用curl_easy_setopt(CURLOPT_FORBID_REUSE, 0)和CURLOPT_FRESH_CONNECT, 0开启连接复用。HTTP 长连接比每次新建 TCP 连接能减少三分之一的延迟。我实测过短连接模式下 QPS 约 220长连接模式下约 300差距非常明显。JSON 响应解析如果你的客户端语言不是 C而是 Go、JavaJSON 解析的便利性会好很多。但如果是 Cnlohmann/json是个很好的选择接口简单。唯一的注意点是在大循环里尽量不要频繁创建和销毁 JSON 对象构造和析构的累计开销相当可观。合理的做法是复用对象用clear()清空数据再填入新结果。图像尺寸限制服务器端需要在处理函数里加上一个校验如果接收到的图片数据超过某个阈值比如 20MB直接返回413 Payload Too Large。否则一张巨大的全景图会把cv::imdecode和后续的 resize 操作拖得很慢严重时甚至撑爆内存影响其他正常请求的响应延迟。5. 生产环境扩展思路服务框架能跑通并稳定运行之后面临的往往不是够不够用而是能扩展多远。最自然的扩展方向有两个。第一个是多模型支持目前只有 YOLOv5 一个模型但业务方经常会问能不能顺便也检测一下车牌之类的问题。由于整个推理引擎的接口是通用的只需要在服务初始化的时候加载多个 engine 文件请求里带上model_name字段路由到对应的推理上下文即可。第二个是服务编排把 HTTP 接口接入到微服务体系里前面加负载均衡后面接消息队列做成异步推理的任务队列模式。如果单台 GPU 撑不住流量可以横向扩容将请求分发到多台 GPU 服务器上。在实现层面目前引擎的init和infer接口已经和具体模型解耦只需要在初始化函数里维护一个std::unordered_mapstd::string, std::unique_ptrYoloTRTInfer就能做到模型级别的热切换。不过这里有个细节要提一下加载多个模型时显存占用也要按模型累加。YOLOv5s 一个引擎大约占用 1.5GB 显存如果还想同时部署 YOLOv5m约 3GB 显存需要确认显存是否足够。6. 个人经验与几点补充说起来市面上讲Python Flask/FastAPI 部署 YOLOv5的教程满地都是真正深入到 C 层做工程性能落地的资料反而不多。这可能是因为 C 部署的门槛不仅在于语言本身更在于对 GPU 计算、内存管理、网络并发等底层机制的理解。但这个门槛跨过来之后带来的收益是实打实的更快的推理、更高的吞吐、更稳的服务。我个人的建议是如果你的服务只给内部几个人用QPS 要求低于 30Python 方案完全够但只要面向外部用户、吞吐量要求百级以上就趁早切换到 C TensorRT 方案。技术债拖得越久重写的时候越痛苦。最后分享一个小技巧。在整个系统的调试阶段你可以在服务器代码里加一个特殊的测试接口GET /health返回服务器当前状态包括engine 是否加载成功、显存剩余量、最近 100 次请求的平均推理耗时、当前工作线程数。压测的时候开着这个接口能非常直观地判断系统瓶颈在哪里。这个接口我在生产环境也一直保留着对排查问题帮助极大。