如何让 SAM3 同时检测人和车?从 ORT 拆分方案的 197ms,到最终基于 DART 在4090ti上2类别检测达到 43ms(注意这里没有mask只有目标框),中间经历了哪些坑、为什么踩、怎么解决的记录下来。
一、背景
SAM3(Segment Anything Model 3) 是 Meta 发布的通用分割模型,848M 参数,能通过文本提示(如 "person")对图片进行像素级分割。我们的项目 SAM3-TensorRT 已经实现了单类别的 TensorRT FP16 加速推理:
| 方案 | 延迟 | 加速比 |
|---|---|---|
| PyTorch FP32 | ~300ms | 1× |
| ONNX + TensorRT FP16 | 62ms | 4.46× |
| C++ TensorRT FP16 | 53ms | 5.66× |
但有一个限制:SAM3 每次只接受一个文本提示,只能检测一个类别。项目需求是同时检测多个类别(如 person + car)。
二、探索之路:三次尝试
尝试 1:Python 拆分方案(ORT + TRT EP)
思路
参考 DART 论文的核心观察:ViT Backbone 是类别无关的(class-agnostic)------它只处理图像,不关心文字。所以多类别检测的瓶颈在于 Backbone 被重复计算了 N 次。
方案是把模型拆成两个引擎:
python
图片 → [Engine 1: Backbone] → FPN 特征(只跑一次)
↓
┌───────────────┼───────────────┐
↓ ↓ ↓
"person" "car" "bicycle"
└───────┬───────┴───────────────┘
↓
[Engine 2: EncDec (batch=N)]
↓
N 组 masks + logits
实现
sam3_split_wrappers.py:将 SAM3 拆成Sam3Engine1Backbone和Sam3Engine2EncDecexport_split_engines.py:导出两个 ONNX 引擎infer_multiclass.py:多类别推理脚本
ONNX 引擎:
sam3_engine1_backbone.onnx:1.85 GBsam3_engine2_encdec.onnx:95.9 MB
结果
功能正确,person 和 car 都能检出。但速度反而更慢:
| 指标 | 单类别 monolithic | 拆分方案(2 类) |
|---|---|---|
| 延迟 | 62ms | 197ms |
| FPS | 16.0 | 5.1 |
问题分析
197ms 远超预期的 124ms(62ms × 2)。根本原因是 FPN 中间张量在 CPU 和 GPU 之间来回搬运:
python
GPU Engine1 跑 Backbone ~50ms ← GPU 计算
GPU → CPU 下载 FPN 特征 ~30ms ← 117MB 数据
CPU 上 np.repeat 扩展 ~20ms ← 117MB → 234MB
CPU → GPU 上传扩展后特征 ~40ms ← 234MB
GPU Engine2 跑 EncDec(batch=2) ~50ms ← GPU 计算
GPU → CPU 下载结果 ~7ms
─────
197ms
GPU 计算只占 ~100ms,另外 ~100ms 全在搬数据。
核心问题 :ONNX Runtime 的 Python API sess.run() 返回值一定是 CPU 上的 numpy 数组,两个 ORT Session 之间无法直接传递 GPU 显存指针。
尝试 2:C++ ONNX Runtime API
思路
用 C++ 版 ORT 的 IoBinding API,让 Engine 1 输出留在 GPU 上,通过 cudaMemcpy(DeviceToDevice) 做 expand,再传给 Engine 2。
实现
在 cpp_multiclass/ 目录下创建了:
sam3_multiclass_ort.cu:ORT C++ 推理实现CMakeLists.txt:构建配置
遇到的问题
- TensorRT 版本混乱:系统有 TRT 10.16(libnvinfer.so.10)和 TRT 11.0(libnvinfer.so.11)共存,pip 装的是 TRT 10.15.1。ORT 的 TensorrtExecutionProvider 依赖系统 libnvinfer,版本不匹配导致引擎无法加载
- TRT C++ 头文件缺失 :尝试安装
libnvinfer-dev时 apt 卡住 - 转用 ORT C++ API :用 ORT 的 CUDA allocator 分配 GPU buffer,但遇到
GetTensorData()返回的指针在session.Run析构后失效的问题 - ORT IoBinding GPU buffer 管理复杂:需要手动管理 CUDA stream 同步、GPU buffer 生命周期,调试困难
最终C++ 方案未能完成。
尝试 3:DART(Detect Anything in Real Time)
转折点
在研究 DART 论文时发现了一个关键事实:DART 是纯 Python 项目,不是 C++!
它的核心做法:
python
Backbone TRT 引擎 → PyTorch CUDA tensor(留在 GPU)
↓
tensor.expand(N, -1, -1, -1) ← PyTorch 操作,GPU 上零拷贝
↓
EncDec TRT 引擎 ← 直接接收 GPU tensor,零搬运
关键区别:
| 操作 | ORT Python API | DART 方案(tensorrt pip + PyTorch) |
|---|---|---|
| Engine 输出到 GPU | ❌ sess.run() 只返回 CPU numpy |
✅ TRT 输出直接写入 PyTorch CUDA tensor |
| GPU 上 expand | ❌ 没有 API | ✅ tensor.expand() 零拷贝 |
| Engine 间传 GPU 数据 | ❌ 不支持 | ✅ PyTorch CUDA tensor 直接传递 |
DART 用 tensorrt pip 包加载 TRT 引擎,用 PyTorch CUDA tensor 在引擎间传递数据,完美绕过了 ORT 的 API 限制。
三、DART 方案详解
3.1 环境要求
| 依赖 | 版本要求 | 我们的环境 |
|---|---|---|
| Python | 3.11+ | 3.12 ✅ |
| PyTorch | 2.7.0+ (CUDA 12.6+) | 2.10.0+cu128 ✅ |
| torchvision | 0.22.0+ | 0.25.0 ✅ |
| tensorrt (pip) | 10.9.0+ | 10.15.1 ✅ |
| numpy | < 2.0 | 1.26.4(需从 2.4.6 降级) |
| CUDA | 12.6+ | 12.8 ✅ |
唯一需要操作的是降级 numpy :pip install "numpy<2.0"
3.2 核心原理
双引擎架构
python
┌─────────────────────────────────────────────────┐
│ TRT FP16 Pipeline │
│ │
│ ┌──────────────┐ PyTorch CUDA tensor │
│ │ Backbone TRT │───(留在 GPU)─────────┐ │
│ │ (ViT-H/14) │ .expand(N,...) │ │
│ └──────────────┘ 零拷贝扩展 │ │
│ ↑ ↓ │
│ pixel_values ┌──────────────┐ │
│ (CPU→GPU) │ EncDec TRT │ │
│ │ (DETR 6+6层) │ │
│ └──────┬───────┘ │
│ │ │
│ scores + boxes │
│ (N类 × 200 queries) │
└─────────────────────────────────────────────────┘
数据流(零 CPU 搬运)
python
pixel_values (CPU) → GPU upload
↓
Backbone TRT → fpn_features (PyTorch CUDA tensor, GPU)
↓
.expand(N, -1, -1, -1) ← 仅改 stride,不复制数据
↓
EncDec TRT ← 直接读 GPU 显存
↓
scores + boxes → CPU download
EncDec 引擎的 I/O 格式
python
输入:
img_feat float32 [max_classes, 256, 72, 72] FPN 特征
img_pos float32 [max_classes, 256, 72, 72] 位置编码
text_feats float32 [32, max_classes, 256] 每类文字嵌入
text_mask float32 [max_classes, 32] 文字 padding mask
输出:
scores float32 [max_classes, 200, 1] 检测 logits
boxes float32 [max_classes, 200, 4] cxcywh 格式
注意:max_classes 是构建引擎时固定的(如 --max-classes 4),运行时传少于 max_classes 的类别也能用。
3.3 核心技术
1. 显式注意力结构(解决 FP16 精度问题)
ViT-H 有 32 层 residual block,FP16 累积误差会导致特征完全不可用(cosine similarity = 0.058)。DART 的 HuggingFace 导出路径将注意力重构为标准 Q·K^T 形式,让 TRT 正确匹配 fused kernel → cos > 0.999。
| 方法 | 延迟 | 余弦相似度 | 状态 |
|---|---|---|---|
| 显式注意力 TRT FP16 | 30ms | 0.999 | ✅ 推荐 |
| Fused SDPA TRT FP16 | 26ms | 0.058 | ❌ 不可用 |
| Fused SDPA 混合精度 | 128ms | 0.999 | ⚠️ 慢 |
| PyTorch eager FP16 | 87ms | 1.000 | ✅ 正确 |
2. PyTorch CUDA Tensor 作为引擎间的桥梁
这是 DART 最精妙的设计:
python
# Backbone TRT → PyTorch CUDA tensor
backbone_out = trt_backbone(pixel_values) # 输出是 GPU 上的 PyTorch tensor
# Expand: 零拷贝(只改 stride,不复制数据)
fpn_expanded = backbone_out.expand(N, -1, -1, -1) # GPU 上瞬间完成
# EncDec TRT ← 直接读 GPU tensor
scores, boxes = trt_encdec(fpn_expanded, text_features) # 零搬运
对比 ORT 方案需要 CPU 中转 ~350MB 数据,DART 的中间数据始终在 GPU 上。
3. 检测专用模式(detection-only)
SAM3 原生输出包含 mask(200 × 288 × 288 的分割图),非常耗时。DART 的 --detection-only 模式只输出检测框和分数,跳过 mask decoder,速度提升显著。
4. 文字嵌入缓存
文字编码器(CLIP text encoder)的结果可以预计算并缓存:
bash
# 首次运行:计算并保存
python demo_video.py --text-cache text_cache.pt ...
# 后续运行:直接加载(不需要加载完整模型)
python demo_video.py --text-cache text_cache.pt ... # 跳过 20s 模型加载
3.4 使用步骤
Step 1: 构建共享的 Encoder-Decoder 引擎(一次性)
bash
# 导出 ONNX(max-classes 控制最大类别数)
python -m sam3.trt.export_enc_dec --checkpoint sam3.pt \
--output enc_dec_4cls.onnx --max-classes 4 --imgsz 1008
# 构建 TRT FP16 引擎(~80s)
python -m sam3.trt.build_engine --onnx enc_dec_4cls.onnx \
--output enc_dec_4cls_fp16.engine --fp16 --mixed-precision none
重要 :--mixed-precision none 是必须的,自动检测的启发式规则对 EncDec 不适用。
Step 2: 构建 Backbone 引擎(一次性)
bash
PYTHONIOENCODING=utf-8 python scripts/export_hf_backbone.py \
--image test.jpg --imgsz 1008
自动完成 ONNX 导出 + TRT 构建 + 精度验证。
Step 3: 运行检测
bash
python demo_multiclass.py --image test.jpg \
--classes person car bicycle dog \
--trt hf_backbone_fp16.engine \
--trt-enc-dec enc_dec_4cls_fp16.engine \
--checkpoint sam3.pt --fast --detection-only
加类别只改 --classes 参数即可,不需要改代码或重新导出引擎。
3.5 注意事项
| 事项 | 说明 |
|---|---|
| numpy 版本 | DART 要求 numpy < 2.0,新环境默认装 numpy 2.x 需降级 |
| TRT 引擎不跨 GPU | TRT 引擎与 GPU 架构和 TRT 版本绑定,换机器需重建 |
--mixed-precision none |
EncDec 引擎构建必须加此参数 |
| HuggingFace 网络 | export_hf_backbone.py 默认从 HuggingFace 下载,网络不通需改成本地路径 |
| max-classes | EncDec 引擎的类别上限是固定的,超了需重建更大的引擎 |
| detection-only | 不生成 mask,只输出框 + 分数。需要 mask 则去掉此参数(会变慢) |
| VRAM 需求 | EncDec 构建时每 4 个类别约需 4GB VRAM |
| FP16 精度 | ViT backbone 必须用 DART 的 HuggingFace 导出路径(显式注意力),否则 FP16 精度崩坏 |
四、性能对比
测试环境
| 项目 | 规格 |
|---|---|
| GPU | NVIDIA RTX 4090 (24GB) |
| 内存 | 251 GB |
| OS | Ubuntu 24.04 |
| 测试图片 | 76 张,分辨率 2248×1758 |
| 检测类别 | person + car |
延迟对比(2 类,1008px,TRT FP16)
| 方案 | 平均延迟 | FPS | 说明 |
|---|---|---|---|
| 单类别 monolithic ORT+TRT | 62ms | 16.0 | 仅 1 类,基准 |
| 拆分方案 ORT+TRT(2 类) | 197ms | 5.1 | CPU↔GPU 搬运拖垮性能 |
| DART TRT FP16(2 类) | 43.3ms | 23.1 | 零搬运,接近单类别延迟 |
DART 详细性能
| 指标 | 数值 |
|---|---|
| 平均总延迟 | 43.3ms |
| Backbone 平均 | 31.7ms(占 85%) |
| EncDec 平均 | 11.6ms(占 15%) |
| FPS | 23.1 |
| 总检出目标 | person: 2770, car: 1273 |
延迟随类别数变化(DART 论文数据,RTX 4080)
| 类别数 | Backbone | EncDec | 总延迟 | Pipelined FPS |
|---|---|---|---|---|
| 1 | 53ms | 8ms | ~61ms | 18.7 |
| 2 | 53ms | 11ms | ~64ms | 17.6 |
| 4 | 53ms | 19ms | ~72ms | 15.8 |
| 8 | 53ms | 35ms | ~88ms | 12.5 |
Backbone 时间恒定(只跑一次),EncDec 线性增长。类别越多,拆分方案优势越大。
五、经验总结
为什么 ORT Python 方案失败?
不是架构问题,是 API 限制。 ORT 的 sess.run() 设计上就是返回 CPU numpy 数组,IoBinding 虽然可以让输出留在 GPU,但无法在 Python 层面对 GPU 上的 OrtValue 做张量操作(如 expand)。这是 ORT Python API 的根本局限。
为什么 C++ 方案失败?
环境问题多于技术问题:
- TRT 版本碎片化(10.15 / 10.16 / 11.0 共存)
- ORT C++ API 的 GPU buffer 生命周期管理复杂
- 调试周期长
DART 为什么能成功?
选对了工具链 :用 tensorrt pip 包(不是 ORT)+ PyTorch CUDA tensor 作为中间格式。PyTorch 原生支持 GPU 上的所有张量操作,TRT pip 包可以直接接受 PyTorch tensor 作为输入输出,两者无缝衔接。