先盘一下这事的来龙去脉。我最近在一个工业视觉项目里,需要把目标检测服务同时部署到三台设备上:一台是带 N 卡的 Windows 工作站,一台是实验室的 Ubuntu 服务器,还有一台是 RK3588 的开发板。模型选来选去,最后定了 RT-DETR-R18,原因很简单:精度够用,推理速度比同尺寸 YOLO 系列要好,而且部署起来不算折腾。但真正让我头疼的不是模型本身,而是“同一套代码在不同环境里跑出同样效果”这件事。Windows 上的 Python 环境、Ubuntu 上的 CUDA 版本、ARM 板子上的算子支持,稍有偏差就给你颜色看。
折腾了两天后,我干脆把所有东西塞进 Docker 镜像,用一套统一的环境跑通了三个平台。这篇文章就把完整的部署过程、架构设计、性能调优思路和踩坑记录写出来,给后面要做 RT-DETR-R18 跨环境部署的朋友一个可以直接参考的路线。
1. 项目整体拆解:从模型到跨环境服务
1.1 RT-DETR-R18 到底是个什么模型
RT-DETR 全称是 Real-Time Detection Transformer,百度在 2023 年开源的一套实时目标检测模型。它和传统 YOLO 系列最大的区别在于:把 Transformer 结构引入检测头,但通过高效混合编码器和 IoU 感知查询选择机制,把推理延迟压到了和 YOLO 一个量级。R18 指的是使用 ResNet-18 作为骨干网络,是 RT-DETR 系列里体积最小、速度最快的一个变体。
我实测过 COCO 验证集上的表现,RT-DETR-R18 的 mAP 大概在 53% 左右,单张 640x640 输入在 NVIDIA 3080 上推理耗时约 3-5ms,在 RK3588 的 NPU 上经过转换后也能跑到 30-50ms 的水平。这个精度和速度的组合,让它非常适合做工业质检、安防监控、边缘计算这类对延迟敏感、又不想牺牲太多精度的场景。
它的部署生态比 YOLO 要复杂一些,因为 PaddleDetection 原生的导出格式是 Paddle 的推理模型,需要你转成 ONNX 或者通过其他推理引擎加载。这就引出了 Docker 部署的一个天然优势:你可以在镜像里提前把所有转换工具链、运行时依赖、模型文件全部固化下来,换一台机器不需要重新配环境。
1.2 为什么要用 Docker 部署检测服务
很多人第一反应是“直接 pip install 一把梭不就行了”。理论上确实可以,但实际操作中你会发现几个很现实的问题。
依赖冲突是第一个坑。RT-DETR 需要 PaddlePaddle 或 ONNX Runtime,这两个库对 CUDA、cuDNN、Python 版本都有严格的要求。比如 PaddlePaddle 2.5 在 CUDA 11.8 下编译的 wheel 包,放到 CUDA 12 的环境里可能直接报算子错误。而项目里如果还有其他服务需要 TensorFlow 或 PyTorch,三套依赖挤在一个 Python 环境里,版本互相打架是迟早的事。
部署环境不一致是第二个问题。Windows 上默认用的可能是 CUDA 12.x,Ubuntu 服务器上是 CUDA 11.7,RK3588 上压根没有 NVIDIA 的东西,只有 RKNN 工具链。每换一个环境就要重新装驱动、配 CUDA、编译算子,至少折腾半天起步。
Docker 的价值就在于把“环境”本身变成代码的一部分。Dockerfile 里写了什么基础镜像、装了什么依赖、设了什么环境变量,所有设备上跑起来就是一样的。我只需要在开发机上构建一次镜像,然后推到镜像仓库或者导出成 tar 包,就能在任意目标设备上用同一条命令启动服务。这也是“跨环境部署”最核心的诉求。
1.3 跨环境部署的目标与适用范围
我这次部署要覆盖三种硬件平台:
- x86_64 + NVIDIA GPU 的 Windows/Linux 工作站,用于开发和性能测试
- x86_64 + CPU 的普通服务器,用于低并发场景的降级运行
- ARM64 + RK3588 NPU 的边缘设备,用于现场实时检测
目标很明确:同一份代码、同一个模型、同一套推理接口,在三个平台上输出一致的结果。差异只允许出现在推理后端上:NVIDIA 平台用 GPU 加速,CPU 平台用 ONNX Runtime CPU 版,RK3588 用 RKNN 加速。对上层 API 调用方来说,接口路径、请求参数、返回格式完全一致。
这个方案不仅适用于 RT-DETR,也适用于其他 Paddle 系模型的 Docker 部署。只要把本文的模型导出部分替换成你自己的模型,整条链路可以照搬。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署方案设计与镜像构建
2.1 整体架构:模型服务、推理引擎与接口层
在实际动手之前,我先画了一条清晰的分层架构,避免在 Dockerfile 里堆一堆乱七八糟的依赖。
code复制┌─────────────────────────────────────────────┐
│ 客户端 / 业务系统 │
│ 通过 HTTP/REST 或 gRPC 调用 │
└───────────────────┬─────────────────────────┘
│
┌───────────────────▼─────────────────────────┐
│ 检测服务容器(FastAPI + Uvicorn) │
│ - 接收图像数据(Base64 / 文件路径 / URL) │
│ - 调用推理核心,返回检测框、类别、置信度 │
└───────────────────┬─────────────────────────┘
│
┌───────────────────▼─────────────────────────┐
│ 推理引擎适配层(统一接口) │
│ - Paddle Inference(x86 + GPU) │
│ - ONNX Runtime CPU(x86 CPU) │
│ - RKNN Runtime(ARM64 + NPU) │
└───────────────────┬─────────────────────────┘
│
┌───────────────────▼─────────────────────────┐
│ 模型文件层 │
│ - RT-DETR-R18 推理模型(Paddle/ONNX/RKNN) │
└─────────────────────────────────────────────┘
我的核心原则是:推理引擎通过一个统一的 Python 接口封装,对外暴露 detect(image) -> List[DetResult]。这样上层服务不关心底层用的是 Paddle 还是 ONNX,也方便后续替换模型版本。
2.2 Dockerfile 设计与依赖选型
基础镜像的选择是第一个关键决策点。我的基础镜像选型逻辑:
- 如果要用 GPU,基础镜像需要包含 CUDA 运行时,但不能盲目用
nvidia/cuda:12.2-base这种纯净镜像,因为缺少 Python 环境 - 如果兼顾 CPU 和 GPU,优先选择
nvidia/cuda:12.2-runtime-ubuntu22.04,再自己装 Python 3.10 - 如果只要 CPU,直接
python:3.10-slim就够了
我最终写出了这样一个 Dockerfile,支持 CPU 和 GPU 两种模式:
dockerfile复制# 基础镜像,使用 Ubuntu 22.04 + CUDA 12.2 运行时
# 纯 CPU 场景可以替换为 python:3.10-slim,并跳过 CUDA 相关层
FROM nvidia/cuda:12.2-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
# 安装 Python 3.10 和基础工具
RUN apt-get update && apt-get install -y \
python3.10 \
python3.10-dev \
python3-pip \
libgl1 \
libglib2.0-0 \
wget \
&& rm -rf /var/lib/apt/lists/*
# 创建工作目录
WORKDIR /app
# 先拷贝依赖文件,利用 Docker 层缓存
COPY requirements.txt .
RUN pip3 install --upgrade pip \
&& pip3 install -r requirements.txt
# 拷贝模型服务代码
COPY src/ ./src/
COPY models/ ./models/
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
EXPOSE 8000
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt 我做了精简,只装核心的推理和 API 依赖:
txt复制fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
numpy==1.26.2
opencv-python-headless==4.8.1.78
pillow==10.1.0
python-multipart==0.0.6
注意,我没有把 PaddlePaddle 或 onnxruntime 直接写进 requirements.txt,因为不同平台的安装包名和索引不一样。GPU 版 Paddle 需要指定 paddlepaddle-gpu,CPU 版只需要 paddlepaddle,RKNN 的依赖更是要走独立工具链。这些我放在后面的多阶段构建里处理。
2.3 模型文件与权重管理
模型文件的管理是部署中最容易忽略但又最影响稳定性的环节。我的做法是:
- 模型不打包进代码仓库,而是放在独立的
models/目录,通过.gitignore排除 - 每次训练或微调后,给模型一个语义版本号,比如
rtdetr_r18_v1.2.0.onnx,避免“最后版”“最终版”这种魔鬼命名 - Docker 构建时通过
COPY把指定版本拷贝进镜像,保证镜像内容可审计
如果你是从 PaddleDetection 导出的推理模型,目录结构通常是:
code复制model.pdmodel # 模型结构
model.pdiparams # 模型参数
要转成 ONNX 格式,用 Paddle 官方提供的工具:
bash复制paddle2onnx --model_dir ./output/rtdetr_r18 \
--model_filename model.pdmodel \
--params_filename model.pdiparams \
--save_file ./models/rtdetr_r18.onnx \
--opset_version 11 \
--enable_onnx_checker True
转换时有几个参数值得注意:opset_version 建议用 11 或 12,太高的话某些国产推理卡不支持;如果后续要走 RKNN,ONNX 版本太高还可能导致 RKNN-Toolkit2 的算子映射失败。
3. 跨环境实操:从 x86 到 ARM64
3.1 基于 Docker Desktop 的本地开发环境
开发阶段我是在 Windows 上进行的。Docker Desktop 装好后,遇到最多的就是 WSL2 内核版本问题。如果你的 Windows 版本较旧,启动 Docker Desktop 时可能会报 “Virtualization support not detected” 之类的错误,通常需要执行三条命令:
powershell复制dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
wsl --set-default-version 2
然后重启系统,在 控制面板 -> 程序和功能 -> 启用或关闭 Windows 功能 里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。这一步做完,Docker Desktop 基本就不会再闹脾气了。
开发时我会用 docker-compose 把检测服务和测试客户端串起来,方便本地快速验证:
yaml复制version: "3.8"
services:
detector:
build:
context: .
dockerfile: Dockerfile
image: rtdetr-detector:dev
ports:
- "8000:8000"
volumes:
- ./models:/app/models:ro
- ./sample_images:/app/sample_images:ro
environment:
- INFERENCE_BACKEND=onnx
- MODEL_PATH=/app/models/rtdetr_r18.onnx
- DEVICE=cpu
3.2 多架构镜像构建与导出
RK3588 是 ARM64 架构,而开发机是 x86_64,直接在同一平台构建出的镜像在 ARM 上跑不了。有两种解决办法:
第一种是使用 Docker Buildx 做多平台构建。在 Docker Desktop 里开启 Experimental 特性后,可以一次性构建 linux/amd64 和 linux/arm64 两个平台的镜像:
bash复制docker buildx create --name mybuilder --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/rtdetr-detector:v1.0.0 \
--push .
但这种方式对于包含编译型依赖的项目可能会踩坑,因为部分 Python 包在 ARM 上没有预编译 wheel 包。我的经验是:构建 ARM64 镜像时,把 pip install 改为源码安装,并加上 BUILD_DEPS 层,比如 gcc, g++, python3-dev。
第二种是直接在 ARM 设备上用本机 Docker 构建。这种方法更简单,也避免了交叉编译的不确定性。我在 RK3588 开发板上装好 Ubuntu 22.04 和 Docker 后,直接把源码和 Dockerfile 拉过去执行 docker build,等待时间虽然长一点,但构建成功率几乎是 100%。
构建完成后导出 tar 包,给离线设备使用:
bash复制docker save rtdetr-detector:v1.0.0 | gzip > rtdetr-detector-v1.0.0.tar.gz
目标设备上导入:
bash复制docker load < rtdetr-detector-v1.0.0.tar.gz
这个 tar 包就是真正的“跨环境交付物”,只要有 Docker 就能跑。
3.3 目标设备部署与验证
在目标设备上,我通常会写一个 run.sh 脚本来统一管理启动参数,避免每次敲一长串 docker run:
bash复制#!/bin/bash
docker run -d --name rtdetr-detector \
--restart unless-stopped \
-p 8000:8000 \
-e INFERENCE_BACKEND=onnx \
-e MODEL_PATH=/app/models/rtdetr_r18.onnx \
-e DEVICE=cpu \
-v /etc/localtime:/etc/localtime:ro \
rtdetr-detector:v1.0.0
部署完成后,我会先用简单的 HTTP 请求做健康验证:
bash复制curl -X POST http://localhost:8000/detect \
-H "Content-Type: application/json" \
-d '{"image_base64": "'$(base64 -w0 test.jpg)'"}'
正常返回的 JSON 结构类似:
json复制{
"detections": [
{"label": "person", "score": 0.87, "bbox": [120, 45, 320, 410]},
{"label": "car", "score": 0.72, "bbox": [30, 200, 280, 350]}
],
"inference_time_ms": 12.3
}
这里我要特别强调 base64 传图的坑:如果图片过大,HTTP 请求体可能超出默认的 body 大小限制,需要给 Uvicorn 设置 --limit-max-requests 或直接在应用里限制图片尺寸。我通常在上传前先做 resize,把最长边压到 1280 以内,既能控制带宽,也能减少推理耗时。
4. 性能调优、资源限制与稳定性保障
4.1 CPU/内存限制与模型批处理参数
在 CPU 设备上跑 RT-DETR-R18,如果不做资源限制,很容易把整台服务器拖垮。我的做法是在 docker run 或 compose 文件里明确限制 CPU 和内存:
yaml复制services:
detector:
deploy:
resources:
limits:
cpus: "4.0"
memory: 4G
reservations:
cpus: "2.0"
memory: 2G
模型侧也有一些值得调的参数。RT-DETR 的输入分辨率直接影响推理速度和精度,640x640 是速度和精度的平衡点。如果对速度要求更高,可以降到 480x480,mAP 大约损失 1-2 个点,但推理延迟能降低 30% 以上。如果对精度要求高,可以升到 800x800,但显存占用会翻倍。
ONNX Runtime 的线程数也需要手动控制,默认线程数等于 CPU 核数,在容器里可能导致 CPU 争抢。我习惯设为物理核数的一半:
python复制import onnxruntime as ort
sess_options = ort.SessionOptions()
sess_options.intra_op_num_threads = 4
sess_options.inter_op_num_threads = 1
sess = ort.InferenceSession(model_path, sess_options, providers=["CPUExecutionProvider"])
4.2 GPU 加速与 nvidia-container-toolkit
在 NVIDIA GPU 上跑 Docker,光装 Docker 是不够的,还需要装 NVIDIA Container Toolkit,不然容器里根本看不到 GPU。Ubuntu 上的安装命令:
bash复制curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
然后 docker run 时加上 --gpus all:
bash复制docker run -d --gpus all --name rtdetr-gpu \
-p 8000:8000 \
-e INFERENCE_BACKEND=paddle \
-e DEVICE=gpu \
rtdetr-detector:v1.0.0
验证 GPU 是否被容器识别,可以进容器跑:
bash复制docker exec -it rtdetr-gpu nvidia-smi
如果输出正常的 GPU 列表,那就说明 toolkit 配置成功。
这里我想提醒一个容易踩到的版本问题:PaddlePaddle 的 GPU 版对 CUDA 版本极其敏感。如果你的宿主机驱动支持 CUDA 12.2,但容器里跑的是 paddlepaddle-gpu==2.5.2,它编译时对应的是 CUDA 11.8,运行时大概率会报 libcudart.so.11.0: cannot open shared object file。解决办法是选择与你基础镜像 CUDA 版本匹配的 Paddle 版本,比如基础镜像用 CUDA 11.8,对应 paddlepaddle-gpu==2.5.2,或者直接升级到支持 CUDA 12 的 Paddle 版本。
4.3 服务自恢复与健康检查
工业场景下服务挂掉的影响很大,所以自恢复机制必须提前做好。我在 compose 文件里配置了 restart: unless-stopped,同时给容器加了 healthcheck。如果检测服务进程崩溃或接口卡死,Docker 会自动重启。
还有一种情况是模型加载失败导致容器一直处于 CrashLoopBackOff。我后来在代码里加了模型加载熔断逻辑:启动时如果模型文件不存在或加载失败,先把健康检查接口返回 503,然后写日志,而不是直接让进程退出。这样方便运维排查,也不会因为反复重启把日志刷爆。
下面是我写的健康检查接口逻辑:
python复制from fastapi import FastAPI, Response
app = FastAPI()
model_ready = False
@app.on_event("startup")
def load_model():
global model_ready
try:
# 根据环境变量选择推理后端
init_inference()
model_ready = True
except Exception as e:
print(f"Model load failed: {e}")
model_ready = False
@app.get("/health")
def health():
if not model_ready:
return Response(status_code=503, content="Model not ready")
return {"status": "ok"}
5. 常见问题排查与避坑清单
5.1 镜像拉取/构建失败类问题
这类问题在跨环境部署时最频繁,我整理了几个典型场景。
Docker Desktop 启动失败的报错信息五花八门,最常见的是 Virtualization support not detected 和 failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine。前者基本就是 WSL2 没开,后者一般是 Docker 引擎还没完全启动,打开 Docker Desktop 后等 10-20 秒再重试即可。如果一直连不上,尝试在 PowerShell 里执行 wsl --shutdown 然后重启 Docker Desktop。
镜像拉取超时是另一个高频问题。遇到这种情况,检查网络代理设置、DNS 配置,以及是否配置了镜像加速器。生产环境建议搭建内网镜像仓库,开发机构建后推送到内网 registry,目标设备从内网拉取,稳定性和速度都远胜公网。
5.2 Docker 网络与服务互通问题
容器内访问宿主机服务、跨容器访问服务,这两类网络问题经常让人摸不着头脑。
如果检测服务容器需要访问宿主机上的某个数据库,或者宿主机上有授权服务,直接用 localhost 是访问不通的。在 Linux 上可以设置 network_mode: host,让容器直接使用宿主机网络栈,简单粗暴;在 Windows/Mac 的 Docker Desktop 上,可以使用 host.docker.internal 这个特殊域名:
python复制import os
DB_HOST = os.getenv("DB_HOST", "host.docker.internal")
如果是两个容器之间互相通信,建议建一个自定义 bridge 网络,用服务名互相访问,而不是依赖 IP:
yaml复制services:
detector:
networks: [appnet]
redis:
networks: [appnet]
networks:
appnet:
driver: bridge
在服务代码里把 Redis 地址写成 redis:6379 而不是 127.0.0.1:6379,Docker 内置 DNS 会自动解析。
容器访问外网失败时,先检查宿主机的防火墙规则,再查看容器网络的 DNS 配置:
bash复制docker exec rtdetr-detector cat /etc/resolv.conf
如果 DNS 不对,可以在 compose 文件的 dns 字段里指定,比如 dns: [114.114.114.114, 8.8.8.8]。这里我不建议在生产环境盲目换成 8.8.8.8,最好和公司内部 DNS 共存,否则内网服务名解析会失败。
5.3 推理结果异常与精度问题
部署完成后,如果发现检测结果和本地调试时不一致,不要急着怀疑模型,先按下面顺序排查。
第一,输入预处理是否一致。RT-DETR 的训练 pipeline 包含 normalize、resize、letterbox 等操作,如果推理服务里没有做完全相同的前处理,结果肯定有偏差。我习惯把前处理代码单独抽出来,在 Docker 镜像和本地训练环境里共用同一个模块,保证不会出现两套逻辑。
第二,ONNX 转换后输出解析差异。Paddle 的检测输出是 [N, 6] 的格式,每行是 [class_id, score, x1, y1, x2, y2],但 ONNX 导出后可能会有额外的后处理节点或输出维度改变。我对比过同一个模型在 Paddle 和 ONNX Runtime 下的输出,坐标归一化方式不完全一致,必须统一坐标是绝对的像素值还是 0-1 的归一化值。
第三,模型量化导致的精度退化。如果 RK3588 上用了 INT8 量化,精度掉 1-2 个点是很正常的。如果掉得太多,优先检查量化时用的校准数据集有没有覆盖目标场景的典型样本。我用 500 张现场图片做校准集,INT8 量化后 mAP 只掉了 1.1 个点,这个代价对边缘部署来说完全可接受。
第 4 个值得注意的点是:在 RK3588 上不要把 RKNN 推理和 OpenCV 的图像编解码放在同一个 Python 进程里跑高并发。RKNN 的 NPU 调用本身是阻塞的,Python 的多线程并发反而会带来上下文切换的开销。我最终用 processes 而不是 threads,或者在前端加一个请求队列,把并发压力留在 HTTP 层。
附:一套可直接复用的关键排查表
| 场景 | 现象 | 排查方向 | 推荐处理 |
|---|---|---|---|
| Docker Desktop 启动失败 | 报 Virtualization support not detected | WSL2 是否开启 | 按 3.1 节三条命令开启后重启 |
| 容器内无法访问宿主机 | Connection refused | Docker 网络模式 | Linux 用 host 模式,跨平台用 host.docker.internal |
| GPU 容器无法用显卡 | nvidia-smi 报错 |
nvidia-container-toolkit 未装 | 参考 4.2 节安装并重启 Docker |
| Paddle 运行报 CUDA 版本错 | libcudart 找不到 | Paddle 与 CUDA 版本不匹配 | 基础镜像与 Paddle 均使用 CUDA 11.8 或 12.x |
| ONNX 推理结果偏差大 | 坐标偏移/置信度异常 | 前处理不一致 | 训练和部署共用同一前处理模块 |
| ARM 平台构建失败 | pip 安装包无 wheel | 部分依赖无 ARM64 预编译 | 增加 build-essential 依赖源码安装 |
| 高并发下服务卡顿 | 请求超时 | Python 线程阻塞 | 换多进程或加请求队列 |
最后的一点体会
这套 RT-DETR-R18 Docker 部署方案前前后后用了不到一周,但把跨环境问题彻底解决了。个人最大的感触是:Docker 的价值在单机部署上体现得还不够明显,一旦你同时面对 Windows、Linux、ARM64 三套环境,它才是真正的救星。你不需要再花时间写“环境搭建文档”,不需要在每一台机器上重复“装依赖—试错—补依赖”的循环,所有关于运行环境的信息都固化成了一份 Dockerfile 和几个环境变量。
后续我在考虑把这套部署流程和 CI/CD 整合起来:代码推送到 Git 仓库后,自动触发多架构镜像构建,然后滚动更新到边缘设备上。这套流程搭好之后,模型更新一次只需要跑流水线,现场设备甚至不需要人工干预。如果你也在做类似的检测服务部署,建议先把本文的镜像构建和基于 ONNX Runtime 的推理封装跑通,再根据实际设备情况逐步引入 GPU 加速和 NPU 量化。这样踩坑最少,见效也最快。
