SAM3 多类别实时检测的完整实践

如何让 SAM3 同时检测人和车?从 ORT 拆分方案的 197ms,到最终基于 DART 在4090ti上2类别检测达到 43ms(注意这里没有mask只有目标框),中间经历了哪些坑、为什么踩、怎么解决的记录下来。

一、背景

SAM3(Segment Anything Model 3) 是 Meta 发布的通用分割模型,848M 参数,能通过文本提示(如 "person")对图片进行像素级分割。我们的项目 SAM3-TensorRT 已经实现了单类别的 TensorRT FP16 加速推理:

方案 延迟 加速比
PyTorch FP32 ~300ms
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 拆成 Sam3Engine1BackboneSam3Engine2EncDec
  • export_split_engines.py:导出两个 ONNX 引擎
  • infer_multiclass.py:多类别推理脚本

ONNX 引擎:

  • sam3_engine1_backbone.onnx:1.85 GB
  • sam3_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:构建配置
遇到的问题
  1. TensorRT 版本混乱:系统有 TRT 10.16(libnvinfer.so.10)和 TRT 11.0(libnvinfer.so.11)共存,pip 装的是 TRT 10.15.1。ORT 的 TensorrtExecutionProvider 依赖系统 libnvinfer,版本不匹配导致引擎无法加载
  2. TRT C++ 头文件缺失 :尝试安装 libnvinfer-dev 时 apt 卡住
  3. 转用 ORT C++ API :用 ORT 的 CUDA allocator 分配 GPU buffer,但遇到 GetTensorData() 返回的指针在 session.Run 析构后失效的问题
  4. 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 ✅

唯一需要操作的是降级 numpypip 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 作为输入输出,两者无缝衔接。

相关推荐
Dust-Chasing1 小时前
Claude Code源码剖析 - 权限系统
人工智能·python·ai
茉莉玫瑰花茶1 小时前
综合案例 - AI 智能租房助手 [ 4 ]
数据库·python·ai·langgraph
组合缺一1 小时前
SolonCode(编码智能体)支持鸿蒙 PC
java·华为·ai·ai编程·harmonyos·solon·soloncode
极客老王说Agent2 小时前
即时配送每日账单人工对账全攻略:结算误差如何快速排查修正?
大数据·人工智能·ai·chatgpt
虎妞05002 小时前
PyTorch 2.0 生产级部署与性能优化指南
pytorch·深度学习·ai·模型部署·cuda
让我上个超影吧2 小时前
Cluade code:Subagents (子代理)
java·ai
Dust-Chasing2 小时前
Claude Code源码剖析 - ShellTool与真实动作
人工智能·python·ai
木白CPP2 小时前
Claude Code 自用高效插件
ai·ai编程
吴佳浩 Alben3 小时前
Hermes vs OpenClaw:基于源码的 Agent Loop 全面分析
人工智能·ai·transformer