查看 Markdown

NVIDIA DeepStream

NVIDIA DeepStream 通过 nvinfer 元件跑推理,而 nvinfer 需要一个 ONNX 图、一份与之匹配的配置文件和一个检测框解析器。在 ONNX 导出上设置 deepstream=True 会写出前两个,并把它们接到第三个上。

开关
export(format="onnx", deepstream=True)
输出
一个 ONNX 图、config_infer_primary_<stem>.txt 和 <stem>_labels.txt
覆盖范围
九项任务下的 43 个家族与任务组合
解析器
NvDsInferParseYolo,来自 Marcos Luciano 的 DeepStream-Yolo 项目,采用 MIT 许可。每台设备编译一次。 github.com/marcoslucianops/DeepStream-Yolo
可用性
随 v1.5.0 发布。2026-08-08 通过 pull request 728 合入 dev。 pull request 728, issue 648
运行时已验证
DeepStream 8.0.0,跑在 RTX 5070 Ti 上,仅检测,2026-08-08

可用性

DeepStream 导出随 v1.5.0 发布。它在 2026-08-08 通过 pull request 728 合入了 dev,所以现在装的版本里已经有它,不需要固定到某个分支。

安装
pip install "libreyolo[onnx]"

如果你在 2026-08-08 之前克隆过 deepstream-export 分支,把它换掉。那个分支做过 rebase 和强制推送,旧历史里缺了一个修复,没有它这些导出在 CUDA 机器上根本跑不起来。

导出会写出什么

model.export(format="onnx", deepstream=True) 会并排写出三个文件。对 libreyolo9s.pt 来说:

  • libreyolo9s.onnx,检测图,一个形状为 (batch, num_detections, 6) 的输出张量, 每行是网络输入像素坐标下的 [x1, y1, x2, y2, score, class_id]
  • config_infer_primary_libreyolo9s.txt,一份 nvinfer 配置,带着该家族的预处理 常数、类别数、阈值和解析器接线。
  • libreyolo9s_labels.txt,每行一个类别名。

只要检查点(checkpoint)带类别名,就会有一个标签文件。深度模型没有类别名,所以既 不会有这个文件,也不会有 labelfile-path 键。

LibreYOLO 不产出 .so。DeepStream 加载的那个 .so 是来自 marcoslucianops/DeepStream-Yolo 的检测框解析器,每台设备编译一次,而且不管你把它 对到哪个 LibreYOLO 检测器上,都是同一个二进制。模型是那份 ONNX。分类和语义分割完全 不需要解析器,因为 nvinfer 自己会做后处理。

导出模型

Python
from libreyolo import LibreYOLO9, LibreDFINE # 在工作目录下写出 libreyolo9s.onnx、config_infer_primary_libreyolo9s.txt# 和 libreyolo9s_labels.txtLibreYOLO9("libreyolo9s.pt", size="s").export(format="onnx", deepstream=True) # 每个检测模型放在各自的目录里:所有检测配置指向的引擎缓存文件名都一样# 参见「已知陷阱」LibreDFINE("LibreDFINEs.pt", size="s").export(format="onnx", deepstream=True)
参数
model.export(    format="onnx",     # 其他任何格式都会拒绝 deepstream=True    deepstream=True,    conf=0.25,         # 填 pre-cluster-threshold(以及相应任务上的 classifier-threshold、                       # segmentation-threshold)    iou=0.45,          # 填 nms-iou-threshold,cluster-mode=4 时省略    batch=1,           # 填 batch-size 和引擎缓存文件名    half=False,        # True 会把配置标成 network-mode=2(fp16 构建)    int8=False,        # True 会把配置标成 network-mode=1    dynamic=True,      # ONNX 图里的动态 batch 轴    imgsz=640,         # 填 infer-dims=3;H;W) # deepstream=True 和 nms=True 互斥:DeepStream 在自己的聚类阶段做抑制,# 所以图里不内嵌任何东西
先下载 D-FINE 权重
curl -L -o LibreDFINEs.pt \  https://huggingface.co/LibreYOLO/LibreDFINEs/resolve/main/LibreDFINEs.pt

文件不在磁盘上时,LibreDFINE._load_weights 会抛 FileNotFoundError,不会尝试下载, 所以要先自己把 LibreDFINEs.pt 取下来。这个缺口记在 issue #727 里。YOLO9 的权重会在 首次使用时下载。

这个开关只有 Python 有。这个分支上的 libreyolo export 没有 deepstream 选项,而且 CLI 是按一份固定列表拼出导出参数的,不会把未知的键透传下去。

构建检测框解析器

检测需要这个解析器库,实例分割需要另一个,其余任务都不需要。DeepStream 8.0 镜像上有 两件事会让文档里那条构建命令失败,两件都是环境问题,不是 LibreYOLO 的问题。

镜像在 /usr/local 下自带 cudacuda-12cuda-12.5cuda-12.8cuda-12.9。只有 cuda-12.5 的 toolkit 是完整的。它还自带 libcublas.so.12libcublas.so.12.8.4.1,却没有 -lcublas 要解析到的无版本号 libcublas.so。下面这段 脚本把两件事都绕开了。

build_parser.sh,在 DeepStream 容器内运行
set -egit clone --depth 1 https://github.com/marcoslucianops/DeepStream-Yolo.git # 这个镜像里的 /usr/local/cuda-12 是个 stub,编译会死在它上面,报# "fatal error: crt/host_defines.h: No such file or directory"。找一个真正# 带这个头文件的 toolkit;在 8.0 镜像上就是 cuda-12.5CUDA_DIR=$(readlink -f /usr/local/cuda)[ -f "$CUDA_DIR/include/crt/host_defines.h" ] || \  CUDA_DIR=$(ls -d /usr/local/cuda-*.* | sort -Vr | \             while read d; do [ -f "$d/include/crt/host_defines.h" ] && echo "$d" && break; done) # 镜像自带 libcublas.so.12 和 libcublas.so.12.8.4.1,却没有 -lcublas 需要的# 无版本号 libcublas.so,所以链接一步会失败并报# "/usr/bin/ld: cannot find -lcublas"。把链接器想要的名字给它mkdir -p /tmp/cudalibsfor lib in cublas cublasLt cudart; do  real=$(find /usr/local -name "lib${lib}.so.1*" | grep -v stubs | sort -V | tail -1)  ln -sf "$real" "/tmp/cudalibs/lib${lib}.so"doneexport LIBRARY_PATH="/tmp/cudalibs:$LIBRARY_PATH" make -C DeepStream-Yolo/nvdsinfer_custom_impl_Yolo CUDA_VER="${CUDA_DIR##*/cuda-}"
实例分割用的是另一个解析器
git clone --depth 1 https://github.com/marcoslucianops/DeepStream-Yolo-Seg.gitmake -C DeepStream-Yolo-Seg/nvdsinfer_custom_impl_Yolo_seg \  CUDA_VER="${CUDA_DIR##*/cuda-}"

然后把生成的配置里的 custom-lib-path 指向编译出来的 libnvdsinfer_custom_impl_Yolo.so。生成的值是相对路径 nvdsinfer_custom_impl_Yolo/libnvdsinfer_custom_impl_Yolo.sodeepstream-appDeepStream-Yolo 的检出目录里运行时它能解析到,其他情况下需要改。

运行流水线

在把时间花到别的事情上之前,先确认容器能看见 GPU。这也是那次验证运行最先做的检查, 跑在 WSL2 下的一块 Blackwell 卡上。

动手之前先确认 GPU 直通
docker run --rm --gpus all nvcr.io/nvidia/tritonserver:26.04-py3 \  nvidia-smi --query-gpu=name,driver_version,compute_cap --format=csv

Output

name, driver_version, compute_cap
NVIDIA GeForce RTX 5070 Ti, 591.86, 12.0

那次验证运行用一个文件源驱动 deepstream-app,没有显示 sink,开着屏幕显示,并设了 gie-kitti-output-dir,让每一帧的检测结果都以 KITTI 文本落盘。带这些设置的配置:

deepstream_app_config.txt
[application]enable-perf-measurement=1perf-measurement-interval-sec=5gie-kitti-output-dir=kitti [tiled-display]enable=0 [source0]enable=1type=3uri=file:///opt/nvidia/deepstream/deepstream/samples/streams/sample_1080p_h264.mp4num-sources=1gpu-id=0 [streammux]gpu-id=0batch-size=1batched-push-timeout=40000width=1920height=1080live-source=0 [primary-gie]enable=1gpu-id=0gie-unique-id=1config-file=config_infer_primary_libreyolo9s.txt [osd]enable=1border-width=2text-size=15 [sink0]enable=1type=1sync=0 [tests]file-loop=0
运行它
deepstream-app -c deepstream_app_config.txt

Output

App run successful
两步都在一个容器里跑
docker run --rm --gpus all -v "$PWD:/work" -w /work \  nvcr.io/nvidia/deepstream:8.0-samples-multiarch \  bash -c "bash build_parser.sh && deepstream-app -c deepstream_app_config.txt"

nvinfer 会在首次运行时从 ONNX 构建 TensorRT 引擎,并把它缓存在模型旁边,所以第一次 运行要付构建引擎的代价,后面的运行直接加载缓存。

生成的配置

下面两份配置都是导出器为那次验证运行写出来的,之后没有改过。

YOLO9-sD-FINE-s
net-scale-factor0.0039215686270.003921568627
model-color-format00
infer-dims3;640;6403;640;640
maintain-aspect-ratio10
symmetric-padding00
network-type00
num-detected-classes8080
cluster-mode24
parse-bbox-func-nameNvDsInferParseYoloNvDsInferParseYolo
pre-cluster-threshold0.250.25
nms-iou-threshold0.45
topk300300

两份配置有三处不同:maintain-aspect-ratiocluster-mode,以及 nms-iou-threshold 到底在不在。D-FINE 的配置把那个键整个省掉了,cluster-mode=4 要的就是这样。

每个目标最多只出一个预测的 head 会拿到 cluster-mode=4,DeepStream 就不对它们做聚类; 聚类会把本来就彼此不同的检测结果合并掉。这覆盖 rfdetrdfinedeimdeimv2ecrtdetrrtdetrv2rtdetrv4yolo9_e2e。网格 head 和锚框 head 拿到的是 cluster-mode=2nms-iou-threshold

检测配置里还带着 engine-create-func-name=NvDsInferYoloCudaEngineGet,它把构建引擎的 活儿交给解析器库。引擎缓存文件名被写死就是因为它,也是已知陷阱里那个冲突的来源。

支持的任务与家族

有 43 个家族与任务组合能导出。libreyolo/export/deepstream.py 里的 deepstream_supported_tasks()deepstream_supported_families(task) 在运行时返回 同样的列表。

任务network-type解析器库家族
检测0DeepStream-Yoloyolo9, yolo9_p2, yolo9_e2e, yolo1, yolo2, yolo3, yolo4, yolo7, yolox, yolonas, rtmdet, picodet, rfdetr, dfine, deim, deimv2, ec, rtdetr, rtdetrv2, rtdetrv4
分类1不需要mobilenetv4, convnext, efficientnetv2, resnet, dinov2
语义分割2不需要pidnet, eomt, dinov2, lingbotvision
实例分割3DeepStream-Yolo-Segrfdetr, dfine, ec
姿态100不需要yolo9, yolonas, rfdetr, ec
深度100不需要depth_anything, zipdepth
复原100不需要nafnet, realesrgan, swinir
抠图100不需要birefnet
视线100不需要l2cs

network-type=100 表示 DeepStream 对这个任务没有后处理器。这些配置会设 output-tensor-meta=1,图的原生输出原封不动地透传出去,由应用从张量元数据里解码。 多输出的图在这里没问题:每个输出层都带着和普通 ONNX 导出一样的输出名和动态轴进入 元数据。

实例分割的每一行是检测行后面跟上该实例的掩码,按 (netH / 4, netW / 4) 展平,也就是 seg 解析器写死的那个分辨率,以概率的形式供 segmentation-threshold 使用。

分类和视线估计以二级推理的方式运行。在生成的配置里设 process-mode=2operate-on-gie-id,就能把分类器接在检测器后面。视线估计是只有 head 的契约,每个输入 一张人脸裁剪图,所以它前面需要一个人脸检测器。

有三个家族是故意缺席的。segformer 没有接到共用的语义导出契约上,任何格式都导不出 ONNX。RTMDet-Ins 和 YOLO9 的实例分割导出在 LibreYOLO 内部就被挡住了。 depth_anything3 没有导出实现。

表里有两行背后是检查点缺口。EoMT 的语义检查点只发布了 l 一个,DINOv2 分类根本没有 发布过检查点,所以那个组合需要你自己微调的权重。

预处理差异

nvinfer 按通道算 net-scale-factor * (x - offsets),缩放是个标量,表达不了逐通道的 标准差。需要逐通道标准差的家族(rfdetrec、用 DINO 骨干的 deimv2 尺寸、 rtmdetpicodet,以及所有分类家族)把归一化烘进了导出的图里,生成的配置则给图喂上 与之匹配的原始输入空间。

几何上,LibreYOLO 自己的 Python 流水线和 nvinfer 仍然有分歧:

  • letterbox 家族(yolo9yoloxyolonasrtmdetyolo2yolo3yolo4yolo7)原生用灰色填充。nvinfer 填黑色。
  • yolonas 检测原生是在 640 的画布里把最长边缩到 636。nvinfermaintain-aspect-ratio 用满 640。
  • 分类原生是先缩短边再中心裁剪。nvinfer 把整帧或目标 ROI 拉伸到网络输入尺寸,所以 裁得很紧的主体会有差别。
  • EoMT 原生对语义分割跑滑窗切块。导出的图是单张拉伸后的画布,更快,也更不准。
  • pidnet 输出的类别图是输入分辨率的 1/8,lingbotvision 是 1/16。DeepStream 会把 类别图上采样后再显示。

ONNX 一致性关卡喂进去的是已经预处理好的张量,所以它检查的是图的输出,抓不到配置里搞错 的通道顺序或填充策略。在部署要求严格一致的工作负载之前,先在你自己的数据上验证。

已知陷阱

同一目录下的两个检测模型会加载彼此的引擎

每一份检测配置都带着同一行:

ini
model-engine-file=model_b1_gpu0_fp32.engine

解析器的引擎构建器要求这个文件名,而且它不随模型变化。把第二个检测模型导出到同一个 目录里,第二次运行就会加载第一个模型缓存下来的引擎。什么都不会崩,只是检测框是错的。 给每个检测模型各自一个目录。那次验证运行必须先把 D-FINE 隔离到单独目录里,才谈得上 测它。

一个框只能带一个类别

nvinfer 的行格式是 [x1, y1, x2, y2, score, class_id],一个框一个类别,所以导出会把 类别分数塌缩到 argmax。predict 下报成两个类别的框,到这里只留下一个。实测的例子: LibreYOLO 在同一个框上报 vase 0.773bottle 0.383,DeepStream 的图留下了 vase。 这是解析器行格式的直接结果,不脱离那份契约就改不了,所以它是预期行为,不是回归。

已验证

在两种检测器 head 类型上,deepstream-app 都跑到了 EOS 并打出 App run successful, 素材是英伟达自带的 sample_1080p_h264.mp4(1443 帧),并开启了逐帧 KITTI 转储。

YOLO9-sD-FINE-s
head 类型网格一对一
cluster-mode24
maintain-aspect-ratio10
有检测结果的帧数14431443
检测总数1803171105

两个模型在全部 1443 帧上的类别直方图都是汽车第一、人第二,对街景来说这是对的。检测数上 四倍的差距,正是 cluster-mode 的差异在起作用:D-FINE 在 cluster-mode=4 下不做聚类, 所以每个超过阈值的 query 都会留下来,几乎重复的那些也在内。

两个各自独立训练出来的模型,把画面里最主要的目标放在了同一个位置:

YOLO9  bus  [706.72,  0.82, 1916.34, 1062.97]  conf 0.965
D-FINE bus  [702.73,  2.93, 1916.24, 1069.32]  conf 0.965

那次运行确立了五件事:TensorRT 能在 sm_120 上从导出的 ONNX 构建引擎,nvinfer 接受 生成配置里的每一个键,NvDsInferParseYolo 能正确读出张量布局,检测框落在源分辨率 1920x1080 的坐标里,标签能在生成的标签文件里解析到。

它运行的环境:

组件
宿主机操作系统Windows 11 Pro 26200
GPUNVIDIA GeForce RTX 5070 Ti,16 GB
驱动591.86
计算能力12.0(Blackwell,sm_120)
容器运行时Docker Desktop 29.4.3,WSL2 后端
DeepStream 镜像nvcr.io/nvidia/deepstream:8.0-samples-multiarch
DeepStream 版本8.0.0
容器内 CUDA12.8.1
解析器marcoslucianops/DeepStream-Yolo 的 HEAD

除了这次流水线运行之外,tests/unit/test_deepstream_export.py 覆盖了图适配器和生成的 配置键,它的 35 个测试在这个 commit 上全部通过。

未验证

写在这里,是为了不让上面的范围被读得比它实际的更宽。

  • Jetson 和 aarch64。导出契约不依赖架构,但流水线只在 x86 独立 GPU 上跑过。
  • 43 个组合里的 41 个。只有 yolo9 检测和 dfine 检测真正走过 DeepStream。分类、 语义分割、实例分割和输出原始张量的那些任务由单元测试和 ONNX 一致性检查覆盖,不是靠 一次流水线运行。
  • FP16 和 INT8。只跑过 network-mode=0
  • 多路流和批处理。一路源,batch-size=1
  • 在真值(ground truth)数据集上的精度。检测结果只做了语义合理性和跨模型一致性的检查, 没有在 DeepStream 里评成 mAP。

根据 2026-08-08 的运行时验证写成。家族列表、配置键和默认值读自 commit 5f81e11e 的 libreyolo/export/deepstream.py 与 libreyolo/export/exporter.py,该 commit 当天通过 pull request 728 合入 dev。