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 作为输入输出,两者无缝衔接。

相关推荐
JXJD200410 小时前
【无标题】
人工智能·ai·自动化
aichitang202410 小时前
希尔伯特空间中的正交性
人工智能·学习·机器学习·ai·泛函分析
张忠琳10 小时前
【deepseek-harness】Cordis 时空可组合性编程范式 — 三段式精读笔记(二)
ai·agent·deepseek·harness·cordis·dsh
coft11 小时前
Pi Agent 架构与关键功能全解析
ai·ai编程
哥不是小萝莉12 小时前
AI 编码 Agent 从原理到可运行代码
ai·agent
图王大胜12 小时前
万物演化论00(序章) 从宇宙到AI
人工智能·ai·宇宙·演化·文明·生命科学
技术小事12 小时前
How To Use Claude 怎么使用Claude
ai·claude
笑霸final13 小时前
不用上传服务器!用 Vue3 + ONNX Runtime Web 在浏览器本地实现智能抠图
运维·前端·图像处理·ai·onnx·canva可画·web性能优化
一切皆是因缘际会13 小时前
多时空虚拟中心—— 全域同源基座与脑机谐振应用架构
ai·系统架构·脑机接口·虚拟全球网络控制中心·元宇宙世界底层架构·原生多时空架构
效率工作实验室13 小时前
AI Agent开发平台有哪些?2026年平台对比
ai·agent开发平台·ai智能体开发平台·智能体平台对比·agent平台选型