Section 8.1: Orin NX 上给 RVM 测 FPS 基线

Orin NX 上给 RVM 测 FPS 基线

状态:骨架,待填肉。数字已核对,散文全部待写。 归属:Helmsman / CUDA 平台线(与现有 7 章 Rockchip 线并列,勿插入编号) 数据来源:deploy/cuda/day3-docker-pytorch-and-rvm-fp32.md(2026-08-26/27 实测)


〇 · 流水账

  • 镜像选型 → 容器起来 → CUDA 可用性验证 → RVM 仓库 + 权重就位
  • 出图成功:363 帧 1080p,vlc 确认抠图正常、发丝边缘干净
  • 端到端 1.83 it/s

ClaudeCode用来建立课程,尤其是自己不太熟悉的领域,效果不错。

虽然RVM的模型部署对我来说并不陌生了,但是毕竟Jetson是陌生的,所以最初的基础还是CC给我建立的。

拿到硬件的检查,打印外壳,模组压力测试,温度曲线,GPU使用率曲线等都属于BSP的内容,我在此就以BSP环节都完成为起点,直接展开RVM的部署记载。

第一件事,选择什么容器镜像

虽然我觉得在arm板上再部署docker,对于最终性能的损耗总会有担心,但AI给的回答是:

  1. 现在以快速上线为第一,而英伟达给的容器镜像就把这部分依赖环境都给建立好,所以用容器的学习成本是小于投入时间研究环境的代价的。
  2. 容器不会留到最终生产阶段,所以性能的微弱影响可以在此忽略不计。
  3. Jetson上部署容器,以及nvidia-container-toolkit关系是什么,能用docker官网给出的安装命令吗?(反正都是apt去做)为什么?

带着这些问题,我决定跟着课程走。第一件事,就是选择什么镜像呢?

1. 打开 https://catalog.ngc.nvidia.com/
        ↓
2. 左侧筛选栏先勾 【Containers】     ← ⭐ 第一下就勾这个
        ↓  (NGC 上还有 Models / Resources / Helm Charts / Collections,
        ↓    跟找镜像无关,勾掉之后页面立刻清爽)
        ↓
3. 搜索框输 "pytorch"               ← ⭐ 只输容器名,永远不要输版本号
        ↓
4. 点进 【PyTorch】 那张卡片(因为rvm的模型用的就是pytorch)
        ↓
5. 点页面上的 【Tags】 标签页        ← ⭐ 关键就是这一下,tag 只在这里
        ↓
6. 在 tag 列表里找名字带 -igpu 的
        ↓
7. 有N个带-igpu的镜像,选择哪一个得看pytorch对应的cuda版本
     a. ssh到jetson,输入'ls -lhat /usr/local/cuda-*',此处我显示出是cuda-12.6。因此接下来就是要找到对应cuda12.6的pytorch-xx-igpu镜像
     b. 打开 https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/
     c. 左侧目录点 "PyTorch Release 24.12"
     (或直接改 URL:.../rel-24-12.html  ← 把年月换成我要查的那个版本)
     d. 页面开头 "This container image contains..." 那段,看两行:
          NVIDIA CUDA xxxx      ← ⭐ 这个跟宿主比
          PyTorch 2.6.yyyy
     e. 如果不匹配,就继续回到b,寻找其他pytorch的版本,直到找到为止;
        如果已找到匹配,那就记下这个版本号,例如24.12

8. 回到5的tags标签页,找到你要的标签,例如上面举例的24.12,然后用每行右边的 【Copy】 按钮取完整 pull 路径(别手打,容易出错)

第二件事. 初始化开发环境

cd /workspace/rvm
git clone https://github.com/PeterL1n/RobustVideoMatting.git
cd RobustVideoMatting.git

# 下预训练权重
wget https://github.com/PeterL1n/RobustVideoMatting/releases/download/v1.0.0/rvm_mobilenetv3.pth \
     -O /workspace/rvm/weights/rvm_mobilenetv3.pth

# 依赖
pip3 install numpy==1.26.4 av==12.3.0 pims==0.7 slicerator==1.1.0 imageio==2.34.2 tqdm==4.67.1 tifffile==2024.8.30 --proxy="http://192.168.3.164:7890"

这些依赖都在我的docker of jetson里跑通了。

第三件事. 官方脚本跑端到端

time python3 inference.py     --variant mobilenetv3     --checkpoint /workspace/rvm/weights/rvm_mobilenetv3.pth     --device cuda     --input-source /workspace/rvm/media/showreel/dance.mp4     --output-type video     --output-composition /workspace/rvm/out/day3_comp_10mbps.mp4     --output-alpha      /workspace/rvm/out/day3_pha_10mbps.mp4     --output-foreground /workspace/rvm/out/day3_fgr_10mbps.mp4     --output-video-mbps 10     --downsample-ratio 0.25     --seq-chunk 1
/workspace/rvm/RobustVideoMatting.git/inference.py:163: FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.
  self.model.load_state_dict(torch.load(checkpoint, map_location=device))
100%|████████████████████████████████████████████████████| 363/363 [03:21<00:00,  1.80it/s]

real	3m31.280s
user	7m23.524s
sys	0m10.772s

只有1.8it/s,我的视频有363frames,耗时211秒,帧率约1.71。 跑了多次,后来结合’tegrastats –interval 1000’的log发现,gpu出现周期性的起伏,基本上可以判断是因为编解码的原因,导致gpu实际大部分时间都在等待数据处理。

第四件事. 跑官方测速脚本’inference_speed_test.py'

照官方脚本(‘inference_speed_test.py’)跑,拿到一个不该出现的排序

--precision float16  →  27.74 it/s
--precision float32  →  28.95 it/s

排序反常的两条原因是 AI 给出的。我没有独立想到,也不打算冒认 —— 我的工作是判断它说得对不对,然后决定要不要照着改。两条我都觉得成立,于是采纳。

一、脚本没有 warmup。 官方脚本开了 cudnn.benchmark = True,首次遇到一个卷积形状会试跑各种算法挑最快的;没有 warmup,这笔一次性开销就被算进了均值。FP16 的候选算法更多(多一批 Tensor Core 的),挑起来更贵,于是恰好造出「FP16 更慢」。首帧耗时对得上:FP16 是三档里最高的(1641 / 1444 ms)。

二、「FP32」那一档其实不是 FP32。 容器里 allow_tf32 默认是开的,卷积实际走 TF32。跑 check_tf32.py 三组对照确认了:默认档与显式关闭 TF32 的结果差 6.5e-04,而 NVIDIA_TF32_OVERRIDE=0 与显式关闭只差 2.1e-06。所以原来那组对比比的是 TF32 vs FP16,不是 FP32 vs FP16。

留下两条我以后会自己用的检查:拿到 benchmark 数字先问有没有 warmup;看到精度标签先验证它是不是真的。

第五件事 · 重测纯模型推理速度:48 / 42 / 39

基于上面的测试有发现,因此改动了rvm仓库里的原生’inference_speed_test.py’脚本。 改动了三处,其余全部保持与官方脚本一致(改动越少,跨平台可比性越强):

问题改法
无 warmup--warmup 200,warmup 帧不计入;单独打印首帧耗时以暴露 autotune 成本
TF32 未隔离--tf32 on/off 显式控制,测 FP16 / TF32 / 纯 FP32 三档
动态调频测前 jetson_clocks 锁 1173MHz,测后还原

结果(median,1000 iters,三档 × 2 轮交错,轮间偏差 < 0.4%):

精度单帧FPS
FP1620.83 ms48
TF3223.60 ms42
纯 FP32—39

收获 1:瓶颈根本不在模型(最重要)

做完上面的实验,我把端到端和纯模型放在一起:

单帧占比
端到端(1.83 it/s)546.4 ms100%
其中纯模型20.9 ms3.8%
其余(解码 + 三路编码 + 拷贝)525.5 ms96.2%

tegrastats 同期观测:

  • GR3D_FREQ 未饱和
  • CPU 频率在 729 ↔ 1984 MHz 之间反复跳(若 CPU 是瓶颈会锁死在最高频不动)
  • EMC_FREQ 8–9% —— 内存带宽远远没跑满
  • NVDEC off / NVENC off —— 硬件编解码器全程闲置

三项资源都没饱和 ⇒ 瓶颈不是「算不过来」,是各环节在互相等(串行化)。

优化前先量占比:模型只占 3.8%,就算 TensorRT 把它优化到零,端到端也只从 1.83 到 1.90。

⇒ 优化顺序因此定了:

第 1 步  换 NVDEC/NVENC + 让解码/推理/编码三级重叠   → 动的是 96.2%
第 2 步  上 TensorRT / CUDA Graph                    → 动的是 3.8%

顺序不能反。 先上 TRT 的话,端到端从 1.83 变成 1.90,会得出「TRT 没用」的错误结论。


还不知道什么

  • 端到端实时未验证 —— 目前 1.83 FPS,距 24 fps 差 13 倍。修完流水线才有资格谈实时
  • 不锁频的稳态没系统测过 —— 29 FPS 是按频率比折算的,不是实测
  • 长时间运行的热节流未测 —— 现有数据都是短时运行,没跑到降频
  • 流水线重叠之后,模型会重新变成瓶颈 —— 那时 TRT 才开始有意义,48 → 约 55 是待验证的预期
  • 跨平台对比口径未统一 —— 3090 那个 78 FPS 同样来自无 warmup、默认开 TF32 的官方脚本, 要严格对比得在 3090 上用同一套脚本重测

附录A:中途被砍掉的计划

中间其实我做过一个计划,要直接在docker里把推理的视频显示出来,都把docker里显示UI的链路调通了,用x11-apps的xeyes程序来测试,成功了。

sudo docker run -it -d \
	--name rvm \
	--restart unless-stopped \
    --runtime nvidia \
    --network host \
    --shm-size=8g \
    --ulimit memlock=-1 \
	--ulimit stack=67108864 \
    -v /home/$USER/Development/rvm:/workspace/rvm \
    -w /workspace/rvm \
    -e DISPLAY=:1 \
    -v /tmp/.X11-unix:/tmp/.X11-unix \
    -e QT_X11_NO_MITSHM=1 \
    nvcr.io/nvidia/pytorch:24.12-py3-igpu \
	sleep infinity

但是遇到了问题是:镜像自带的 opencv 编译时没带 GUI 模块,cv2.imshow 用不了。按常规做法 pip install opencv-python 装完,getBuildInformation() 里 GUI 仍然是 NONE。

卡点在这里:pip list 显示 opencv-python 4.14.0.94,但 cv2.__version__ 是 4.10.0。装的和用的不是同一个 —— pip 没失败,只是新装的没被加载。镜像那个和 pip 那个落在同一个 cv2/ 目录里,.so 命名规范不同,Python 优先加载了镜像自带的;pip 也不报冲突,因为两者注册的包名不一样。

所以判据不是重查 getBuildInformation()(查到的还是旧那个,怎么装都是 NONE),而是先比 pip list 的版本号和 cv2.__version__ 是否一致。

这条机制是 AI 指出的,我提供的是现象。给的修法是把镜像那个 .so 改名,让 Python 回退到 pip 版 —— 没执行,因为这时已经判定容器出画不是必要项。

为什么放弃:出画是为了看流畅度,但当时端到端只有 1.83 FPS。修通了也只能得到一个「确认它确实很卡」的窗口,不构成新证据。等硬件编解码接通再做。