做AI模型部署的同学,肯定都经历过这种场面:代码在开发机上跑得好好的,一到客户现场就崩,报错要么是CUDA版本对不上,要么是缺个libglib2.0,要么是Python包互相打架。我这次把RT-DETR-R18目标检测服务完整搬到Docker容器里,前后折腾了两天,最终实现了一个镜像走天下,GPU机器上直接拉起,CPU机器也能跑,换环境不再心惊胆战。
RT-DETR是百度提出的实时检测Transformer模型,R18版本用ResNet18做骨干网络,比R50、R101轻量不少,但精度在实时模型里依然能打,特别适合工业质检、安防摄像头、智慧交通这类对帧率有要求、算力卡得又紧的场景。Docker把这套模型、依赖、推理服务全部打包,交付时不再甩给对方一堆“自己装环境”的文档,而是直接一个镜像文件。
这篇东西主要写给两类人:一类是把RT-DETR-R18用在正式项目里、需要跨机器交付的工程同学;另一类是刚接触模型容器化,想了解完整链路怎么走的新手。里面会覆盖环境准备、镜像构建、推理服务封装、跨环境迁移几个核心环节,每一步都带命令、带代码、带踩坑记录。
1. RT-DETR-R18 为什么值得容器化部署
1.1 先说清楚 RT-DETR-R18 是什么(以及为什么选它)
RT-DETR全称Real-Time Detection Transformer,核心思路是把DETR系列的端到端检测能力和实时性需求结合到一起。传统YOLO系列走anchor + NMS的后处理路线,RT-DETR直接通过Transformer的query机制输出检测框和类别,推理阶段连NMS都不需要,省掉了一个麻烦的后处理环节。
R18后缀表示骨干网络是ResNet18。RT-DETR官方提供R18、R50、R101等几个版本,我这边最后选R18,理由很直接:项目现场是嵌入式级别的显卡(或者共享GPU资源),要求单帧处理时间控制在20ms以内,R50虽然精度更高,但GPU功耗和显存占用直接超了预算。R18版本在速度和精度之间取了一个很实用的平衡点。
几个版本的关键指标我放在下面,数字来自COCO val2017数据集,速度在T4 GPU上实测(细节后续章节再说):
| 模型版本 | 参数量 | FLOPs | COCO mAP | T4推理速度 |
|---|---|---|---|---|
| RT-DETR-R18 | 约20M | 约87G | 46.5% | 217 FPS |
| RT-DETR-R50 | 约42M | 约135G | 53.1% | 108 FPS |
| RT-DETR-R101 | 约58M | 约260G | 54.3% | 74 FPS |
实际业务中如果只需要检测少数几个类别,R18的精度完全够用。我做过对比,在自有数据集上R18比YOLOv5m略高一点,但推理速度快了大概30%,这还没算省掉NMS带来的整体延迟下降。
1.2 跨环境部署的三座大山
做过模型交付的朋友应该都有感触,模型文件本身不复杂,复杂的是它周围那一圈环境。总结起来就是三座大山:
第一座,Python包版本冲突。paddlepaddle、opencv、numpy、protobuf,这些包各自有自己的版本兼容矩阵,你在开发环境装的是paddlepaddle 2.6.1,到了现场可能因为系统Python版本不同,被迫换成一个不兼容的旧版本,然后各种API报错。
第二座,CUDA和cuDNN版本错位。GPU机器还好说,最坑的是有些现场机器驱动版本很老,只支持CUDA 11.4,而你在开发机上用的CUDA 11.7,这种情况下就算paddle能装上,也会在加载模型时直接报CUDA driver version is insufficient。
第三座,系统动态库缺失。opencv依赖libgl1、libglib2.0-0、libsm6这些系统库,很多精简版Linux服务器上压根没装,编译又没权限,只能干瞪眼。
Docker的价值正好体现在这里:把Python环境、CUDA运行库、系统动态库、模型权重全部打进一个只读镜像层,交付时用户不需要在自己机器上装任何深度学习环境,只要一个Docker运行时就能跑。安装依赖和测试推理咱们开发阶段已经做完了,现场跑不起来那是环境问题的概率,直接降到最低。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础镜像选型
2.1 宿主机准备:这套部署方案的运行基础
做跨环境交付,我建议先在Linux服务器上把整个流程跑通,Windows上做开发调试可以用Docker Desktop,但生产环境还是Linux更稳。Ubuntu 20.04或22.04都行,我用的是Ubuntu 20.04 LTS。
安装Docker CE的步骤很简单,几条命令搞定:
bash复制sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
装完验证一下:sudo docker run hello-world,能打印出Hello from Docker就说明没问题。
如果你在Windows上用Docker Desktop,有个常见坑是启动时提示virtualization support not detected,通常需要进BIOS打开VT-x虚拟化,或者在Windows功能里启用Hyper-V和“虚拟机平台”。这个问题排查起来很消耗时间,前面说的生产环境用Linux就是要避开这些坑。
GPU机器还需要装NVIDIA Container Toolkit,否则容器内用不了GPU。命令如下:
bash复制distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker
这里有个必须注意的点:宿主机必须要装NVIDIA显卡驱动,nvidia-container-toolkit只是让容器能访问GPU,底层的驱动它不负责装。验证方法是在宿主机执行nvidia-smi,能正常显示显卡信息再继续。
2.2 基础镜像到底怎么选
做深度学习容器化,基础镜像的选择是第一道分水岭。我对比了三个方案:
| 方案 | 镜像 | 体积 | GPU支持 | 构建成本 | 可控性 |
|---|---|---|---|---|---|
| A | paddlepaddle/paddle:2.6.1-gpu-cuda11.7-cudnn8.4 | 约6GB | 完整 | 低 | 中 |
| B | nvidia/cuda:11.7.1-cudnn8-runtime-ubuntu20.04 | 约3GB | 完整 | 高 | 高 |
| C | python:3.10-slim | 约0.2GB | 无 | 中 | 高 |
方案B适合需要精确控制每一个依赖的场景,但需要自己装Python、PaddlePaddle、各种系统库,构建容易翻车。方案C适合纯CPU离线推理,镜像小,但如果你现场有GPU就浪费了。
我最后选了方案A,paddle官方GPU镜像。理由很简单:官方镜像已经把paddle、CUDA、cuDNN的版本组合调好了,不用自己折腾兼容性匹配。镜像虽然大,但Docker镜像压缩后传输也就2GB多一点,还在可接受范围。
如果你的现场机器是纯CPU,或者像RK3588这样的ARM平台,情况会不一样。ARM架构无法直接跑x86的镜像,得用ARM基础镜像在ARM机器上重新构建。我之前在RK3588上部署过YOLOv8,踩过这个坑,后面专门写一节聊ARM平台的迁移。
2.3 模型权重和依赖清单准备
模型文件我用的PaddleDetection官方仓库里RT-DETR-R18在COCO上的预训练权重。如果代码库是PaddleDetection的RT-DETR实现,训练和推理都走paddle生态。
在准备Dockerfile之前,我先把以下文件准备好:
text复制model/
rtdetr_r18.pdmodel # 静态图模型结构
rtdetr_r18.pdiparams # 静态图模型参数
infer.py # 推理封装
server.py # HTTP服务
requirements.txt # Python依赖
entrypoint.sh # 容器启动脚本
requirements.txt内容如下:
text复制paddlepaddle-gpu==2.6.1
opencv-python-headless==4.8.1.78
fastapi==0.104.1
uvicorn==0.24.0
numpy==1.24.3
python-multipart==0.0.6
注意opencv我用的是headless版本,服务器上没有GUI需求,headless版体积更小,还能避免对libGL库的依赖。但实际跑起来还是会缺libglib2.0,所以Dockerfile里系统依赖还得装。
3. Dockerfile设计与镜像构建实战
3.1 为什么我用单阶段构建,放弃了多阶段
很多Docker最佳实践文章都会推荐多阶段构建,用来压缩最终镜像体积。多阶段的思路是:第一阶段装编译工具链,编译一些C扩展;第二阶段只复制编译产物和运行时依赖。
但对PaddlePaddle推理镜像来说,多阶段的收益非常有限。PaddlePaddle从pip安装时就是预编译的wheel包,不存在本地编译环节,系统库也从apt源直接装,多阶段解决不了核心问题,反而让Dockerfile可读性变差。我的最终镜像体积主要被paddle的.so文件和CUDA库占据,这部分无法通过构建方式压缩。
所以我这里直接用了单阶段构建,简单、直观、调试方便。
3.2 Dockerfile逐段解析
完整的Dockerfile如下:
dockerfile复制# 基础镜像:官方paddle GPU镜像,CUDA 11.7 + cuDNN 8.4 + TensorRT 8.4
FROM paddlepaddle/paddle:2.6.1-gpu-cuda11.7-cudnn8.4-trt8.4
# 避免apt安装时弹交互式界面
ENV DEBIAN_FRONTEND=noninteractive
# 安装opencv运行时依赖的系统库
RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1 \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 先拷贝requirements.txt,单独安装依赖,利用Docker缓存层
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 再拷贝代码和模型
COPY model/ ./model/
COPY infer.py server.py entrypoint.sh ./
# 给启动脚本执行权限
RUN chmod +x entrypoint.sh
# 推理服务默认端口
EXPOSE 8000
# 容器启动入口
ENTRYPOINT ["./entrypoint.sh"]
几个地方解释一下。
先装系统依赖再装Python依赖,这个顺序很关键。因为paddle和opencv在import时都依赖glib等系统库,如果先装Python包再装系统库,虽然最后结果一样,但Docker构建缓存会失效,每次改requirements都会把所有层重新构建。
rm -rf /var/lib/apt/lists/*是清理apt缓存,能减少一点镜像体积。官方镜像本身已经很大了,能省一点是一点。
workdir设置成/app,后面所有COPY都是相对这个路径,保持容器内路径清晰。实际运行时我习惯把宿主机上的挂载目录映射到/app/data,模型路径、日志路径统一相对app路径找。
3.3 构建命令与体积优化
构建命令很简单:
bash复制cd rtdetr-r18-docker
docker build -t rtdetr-r18:1.0.0 .
一顿操作下来,最终镜像体积大约6.5GB。第一次看到这个数字可能有点吓人,但都是paddle和CUDA库的锅,实际被压缩后传输时也就2GB左右。
如果想进一步减体积,有几个办法。
把pip安装参数加上--no-cache-dir,这我已经写进去了。pip缓存容易占硬盘,构建阶段无所谓,但会污染镜像层,每层都带一份缓存文件,体积会明显变大。基础镜像的paddle是官方做好的,这部分省不了。如果换成python:3.10-slim从零装paddle,镜像能压到2GB左右,但CPU推理速度要慢不少,还白白丢了GPU支持。
3.4 构建过程中容易忽略的几处细节
构建镜像最容易犯的错是:本地调试环境能跑,但容器内一启动就报错。我这次就碰到三个问题。
第一个是paddle本身在GPU容器里需要写缓存目录。PaddlePaddle默认会在HOME目录下创建.workspace和.py_cache之类文件夹,如果HOME没有写权限就报错。解决办法是Dockerfile里直接用ENV把HOME指向一个可写路径:
dockerfile复制ENV HOME=/app
这个细节官方文档写得不明显,但跑起来立刻暴露。
第二个是gunicorn和uvicorn的worker数量问题。如果用默认的并发模式,容器里会起多个进程,每个进程都往GPU上申请显存,小显存机器直接OOM。我后面线程模型改成了进程内线程,在FastAPI那里细说。
第三个是关于镜像标签。构建的时候最好带上明确的版本号,不要一直用latest。跨环境交付时,你无法保证用户机器上之前有没有拉过一个同名但内容不同的latest镜像,这会导致一个隐蔽的“还是旧版本”的bug。
4. 推理服务封装与核心代码实现
4.1 推理代码的结构设计
推理服务我分两层:底层是模型加载与推理引擎,上层是HTTP接口。底层代码独立成一个infer.py,方便在容器里做单元测试;server.py只负责接收图片、调用底层、返回结果,逻辑尽量薄。
RT-DETR的推理流程和YOLO类似,核心步骤是:图片解码 -> resize到640x640 -> 归一化 -> 模型推理 -> 解析输出。区别在于RT-DETR的模型输出结构更简单,因为它不需要NMS。
我用paddle.inference的Predictor接口加载静态图,静态图模型先把paddle动态图权重用PaddleDetection的export脚本导出。导出命令大致如下:
bash复制python tools/export_model.py \
-c configs/rtdetr/rtdetr_r18_6x_coco.yml \
-o weights=https://paddledet.bj.bcebos.com/models/rtdetr_r18_6x_coco.pdparams \
--output_dir=output_inference
导出后模型文件就是model.pdmodel和model.pdiparams。
推理代码主体如下:
python复制import cv2
import numpy as np
import paddle.inference as paddle_infer
class RTDETRInfer:
def __init__(self, model_dir, use_gpu=True, device_id=0):
model_file = f"{model_dir}/model.pdmodel"
params_file = f"{model_dir}/model.pdiparams"
config = paddle_infer.Config(model_file, params_file)
if use_gpu:
config.enable_use_gpu(256, device_id)
config.switch_ir_optim(True)
else:
config.enable_mkldnn()
config.set_cpu_math_library_num_threads(4)
config.enable_memory_optim()
self.predictor = paddle_infer.create_predictor(config)
self.input_names = self.predictor.get_input_names()
self.output_names = self.predictor.get_output_names()
self.input_handle = self.predictor.get_input_handle(self.input_names[0])
def preprocess(self, img, target_size=640):
# 保持宽高比resize,同时补边到640x640
h, w = img.shape[:2]
scale = min(target_size / h, target_size / w)
new_w, new_h = int(w * scale), int(h * scale)
resized = cv2.resize(img, (new_w, new_h))
canvas = np.zeros((target_size, target_size, 3), dtype=np.uint8)
canvas[:new_h, :new_w, :] = resized
# 归一化:ImageNet mean/std
canvas = canvas.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
canvas = (canvas - mean) / std
# HWC -> CHW,并加batch维
chw = np.transpose(canvas, (2, 0, 1))[None]
return chw.astype(np.float32), scale, w, h
def predict(self, img, score_threshold=0.4):
input_data, scale, origin_w, origin_h = self.preprocess(img)
self.input_handle.copy_from_cpu(input_data)
self.predictor.run()
outputs = []
for name in self.output_names:
out_handle = self.predictor.get_output_handle(name)
outputs.append(out_handle.copy_to_cpu())
# 输出格式统一处理,一般是 [N, 6]:class_id, score, x1, y1, x2, y2
result = outputs[0]
boxes, scores, labels = [], [], []
for det in result:
cls_id, score = int(det[0]), float(det[1])
if score < score_threshold:
continue
# 坐标映射回原图尺寸
x1, y1 = int(det[2] / scale), int(det[3] / scale)
x2, y2 = int(det[4] / scale), int(det[5] / scale)
boxes.append([x1, y1, x2, y2])
scores.append(score)
labels.append(cls_id)
return boxes, scores, labels
这里有个容易出错的点:RT-DETR的输出坐标是相对640x640的,必须用前面算好的scale映射回原图尺寸,否则画框和真实物体位置对不上。我在这个方法里把scale和原始宽高都返回了,就是为了后处理时方便。
后端处理用了内存优化enable_memory_optim(),实测能让多次推理时显存波动更稳定。对于服务端常驻进程来说,显存稳定比峰值低一点更重要。
4.2 用FastAPI封装HTTP服务接口
HTTP框架我选了FastAPI,原因很简单:性能不错,自带文档页面,调试方便。需要安装的包在requirements里都有。
server.py如下:
python复制import base64
import cv2
import numpy as np
from fastapi import FastAPI
from pydantic import BaseModel
from infer import RTDETRInfer
app = FastAPI()
model = RTDETRInfer("model", use_gpu=True)
class PredictRequest(BaseModel):
image: str # base64编码的图片
threshold: float = 0.4
class DetectBox(BaseModel):
label: int
score: float
box: list
@app.post("/predict")
def predict(req: PredictRequest):
try:
# 解码base64图片
img_bytes = base64.b64decode(req.image)
img_array = np.frombuffer(img_bytes, dtype=np.uint8)
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if img is None:
return {"code": 1, "msg": "image decode failed"}
boxes, scores, labels = model.predict(img, req.threshold)
detections = [
DetectBox(label=label, score=score, box=box)
for box, score, label in zip(boxes, scores, labels)
]
return {"code": 0, "count": len(detections), "detections": detections}
except Exception as e:
return {"code": 2, "msg": str(e)}
@app.get("/healthz")
def health():
return {"status": "ok"}
FastAPI的Pydantic模型可以直接做请求体校验,比直接收dict判断字段健壮得多,省去了一堆手动异常处理代码。
并发场景下有个优化点值得提。如果在def predict和@app.post("/predict")中间不加async,FastAPI会自动放到线程池执行,多线程同时调用底层推理函数时会存在显存竞争。比较稳妥的做法是加一个简单的线程锁,保证同一时间只有一个推理任务在GPU上执行,其他请求排队等。修改一下推断逻辑:
python复制import threading
infer_lock = threading.Lock()
@app.post("/predict")
def predict(req: PredictRequest):
with infer_lock:
... # 原有代码
实测在小显存卡上这个锁非常有用,否则并发到两个请求,显存直接爆掉。在大显存卡上可以按需放开限制,让线程池并发跑,但不够稳妥,现场还是保守优先。
4.3 启动脚本与容器运行
entrypoint.sh内容如下:
bash复制#!/bin/bash
set -e
# 模型预热:用一张黑色图片跑一次推理,把CUDA kernel加载好
python -c "
from infer import RTDETRInfer
import numpy as np
model = RTDETRInfer('model', use_gpu=True)
dummy = np.zeros((640, 640, 3), dtype=np.uint8)
model.predict(dummy, 0.99)
print('model warmup done')
"
# 正式启动HTTP服务
exec uvicorn server:app --host 0.0.0.0 --port 8000 --workers 1
预热这一步往往被很多人忽略。第一次推理时CUDA kernel的加载和paddle内部各种初始化会占用好几秒,如果服务一启动就接流量,前面的请求延迟会非常难看。用一张纯黑图先跑一次,等请求来时模型状态已经是热的了。
uvicorn启动参数里workers设成1,跟前面加锁的逻辑呼应。多进程模式下每个进程都会加载一份模型、占一份显存,资源吃紧,而且paddle推理服务多进程反而容易出显存分配异常。单进程配合线程锁,在吞吐量要求不高的场景下是最省心的组合。
启动容器:
bash复制docker run -d --name rtdetr \
--gpus all \
-p 8000:8000 \
-e NVIDIA_VISIBLE_DEVICES=0 \
-v /data/models:/app/model \
rtdetr-r18:1.0.0
-v参数把宿主机上的模型目录挂载进容器,这样模型更新时不需要重新构建镜像,重启容器即可。这个实践在生产环境里特别好用,权重迭代频繁时你就知道了。
5. 镜像导出、跨环境迁移与现场实操
5.1 镜像传输的三种方式
跨环境交付时,传输镜像有三种方式,各有各的使用场景。
最直接的是docker save导出tar包,拷贝到目标机后docker load加载。离线环境首选,但要注意大镜像tar包容易超过U盘或邮件附件大小限制,我现场用的8GB U盘刚好放下。
bash复制docker save -o rtdetr-r18-1.0.0.tar rtdetr-r18:1.0.0
目标机上:
bash复制docker load -i rtdetr-r18-1.0.0.tar
如果两台机器之间能通网络,也可以用私有registry。先docker tag再docker push,目标机docker pull即可。这个方式最适合重复交付的场景,但需要临时搭一个registry服务,或者已经有公司的镜像仓库。
第三种是用docker compose编排,适合目标机上有多个服务要一起启动的场景。比如检测服务、业务后端、数据库三个容器一起跑,compose文件能固定所有配置,一条命令拉起全套。我现场就遇到过客户要求连数据库一起部署,这种情况下compose省了大把的口头沟通。
yaml复制version: "3"
services:
rtdetr:
image: rtdetr-r18:1.0.0
container_name: rtdetr-r18
ports:
- "8000:8000"
environment:
- NVIDIA_VISIBLE_DEVICES=0
volumes:
- ./models:/app/model
restart: always
5.2 目标机运行检查与ARM平台特例
镜像拉到目标机后,别急着run,先做两个检查。第一个是宿主机驱动和Docker GPU支持:
bash复制nvidia-smi
docker info | grep -i runtime
如果docker info里看不到nvidia runtime,说明nvidia-container-toolkit没装好。常见的报错是:
text复制docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]]
原因就是缺少nvidia container toolkit,重新按前面2.1节的步骤安装即可。
第二个检查是端口和资源。ss -tlnp | grep 8000看看宿主机8000端口有没有被占用,占用的话docker run时改映射端口。
ARM平台要特别说一句。很多边缘设备比如RK3588、Jetson系列是ARM架构,不能直接跑本文构建的x86镜像。解决办法有两种:一是在目标ARM机器上重新构建(拉取ARM基础镜像、重新pip install),二是用docker buildx做交叉构建。我建议直接在ARM机器上构建,交叉构建时paddle的wheel包经常会出现manylinux兼容问题,处理起来比重新构建更麻烦。
我在RK3588上部署过YOLOv8,当时就是在板子上直接跑完整构建流程,算力不足导致pip安装很慢,但至少避免了一堆架构不兼容的编译坑。如果现场ARM机器性能太差,可以先在开发机上把依赖包预先下载好,再用离线wheel的方式在板子上装,速度会快很多。
5.3 常见问题排查速查表
部署过程里最容易遇到的几个问题,我整理成一张表,按项目现场踩坑频率排序:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| container exits with code 0 quickly | entrypoint脚本权限没加x | dockerfile里加RUN chmod +x |
| GPU容器内nvidia-smi报错 | nvidia-container-toolkit未装 | 重装及重启docker |
| 端口8000被占用 | 宿主机有其他服务 | 换端口或kill旧进程 |
| 容器内import cv2报错libGL.so | 系统依赖缺失 | 安装libgl1 libglib2.0-0 |
| 显存OOM | 并发太高或图像超大 | 加锁、降低并发、限制最大图像尺寸 |
| 模型加载报C++ exception | pdmodel和pdiparams版本不匹配 | 确认同一export过程导出 |
| 容器内网络请求慢 | 默认bridge网络DNS问题 | 加--dns 8.8.8.8或用host网络 |
| 镜像拉取超时 | 网络原因 | 配置镜像加速器或离线load |
| 服务启动但请求超时 | CPU或GPU型号太弱 | 检查日志,等待预热完成再压测 |
| 检测结果全部为空 | 默认阈值太高或模型类别不符 | 用低阈值如0.05测试 |
还有一个小概率但隐蔽的问题:宿主机有防火墙策略时,容器内访问外网下载模型会卡住。之前直播说过,容器网络默认走bridge,DNS解析偶尔会被内部防火墙拦截。这时候先docker run --rm --network host测试网络,能通就说明bridge网络的问题,加--dns参数或者直接用host网络解决。
5.4 性能调优和小显存适配
最后说说性能优化。我这次部署的机器是一张8GB的RTX 3060,单线程跑640x640输入,推理耗时大约15ms,加上图片解码和JSON序列化,整个HTTP请求耗时约25ms。这个表现完全够用了。
如果模型推理速度慢,第一件事看是不是CPU推理。用GPU时paddle日志会打印GPU compute capability: 8.9,如果没看到,就是config没启用GPU。config.enable_use_gpu(256, device_id)里的256是显存申请大小,单位MB,它只是一个预分配值,不写太大,避免和小卡片上其他进程抢显存。
输入尺寸也能调。实际业务如果检测大目标,把640改小到512甚至384,速度能提升到10ms以内,代价是小数目标容易漏检。我这边场景是设备面板巡检,目标中等大小,640可以接受;如果做的是卫星图像大图分析,就需要分块切片了,模型本身不会变,但预处理逻辑要重写。
还有一个提速思路是TensorRT推理。paddle官方镜像自带TensorRT 8.4,可以在开启GPU推理时加一行config.enable_tensorrt_engine(...)。但这个配置稍微复杂一点,要指定workspace大小、precision mode等,我这次没有启用,一是时间紧,二是容器里的TensorRT和宿主机驱动版本有时不匹配,一旦出现版本不符就得反复折腾。如果对延迟要求很苛刻而且硬件固定,可以后面专门研究TensorRT优化。
收个尾吧。我在实际部署中比较深的体会是:跨环境交付这件事,技术本身不复杂,复杂度全部来自“不确定性”。环境差异、驱动版本、系统库缺失,每一样都像一个隐藏的坑,等你到了客户现场才被发现。Docker帮我把这些不确定性全部圈在了镜像内部,开发阶段解决掉,交付阶段只暴露一个干净简洁的HTTP接口。
这套方案下一步可以有两条扩展路径。一是把检测结果叠加到一个简单的可视化页面上,现场调试更直观;二是接入更轻量的推理引擎比如ONNX Runtime或TensorRT,进一步缩小镜像体积和推理延迟。如果你的项目也遇到了类似的跨环境部署问题,希望这篇记录能帮你少走一些弯路。
