
GPU 利用率很低不一定是模型慢利用率很高也不代表整条视频任务足够快。用户等待的是从输入视频到可播放结果的总时间而这段时间由读取解码、预处理、推理、后处理、绘制和编码共同组成。本文用固定视频任务说明怎样定位瓶颈而不是只报一个模型 FPS。目录先拆开端到端耗时不同瓶颈该怎么处理最小测量实现、测试与 SQL验收边界性能结论怎样才能比较小结一、先拆开端到端耗时总耗时 探测/解码 预处理 推理 后处理 绘制/合成 编码/写盘同一模型在图片基准中很快放进视频任务仍可能受解码、CPU 内存复制或编码限制。性能记录必须按阶段保留帧数、总耗时、平均耗时和长尾耗时。图1模型 FPS 不是用户等待时间性能必须落到整条视频处理链。二、不同瓶颈该怎么处理瓶颈典型症状首先检查可行方向不应直接做解码/读取GPU 空闲、读帧不足编码、磁盘、读取耗时批量读取、合适解码器盲目换更大 GPU预处理CPU 高、推理等待resize、颜色转换、内存复制合并操作、减少复制降模型精度掩盖问题推理GPU 阶段占比最高输入尺寸、batch、运行时选择模型规格与运行时只报单图 FPS后处理/绘制模型快但整体慢NMS、轨迹、文字绘制减少重复计算丢掉必要证据编码/写盘处理后仍长时间卡住编码器、码率、磁盘合适编码与异步交付把临时文件当结果表格只能提供第一眼的判断真正排查时应从任务时间线倒推而不是看到 GPU 利用率低就先怀疑模型。下面以固定的VP-20261005-001视频任务为例输入为 90 秒、1080P、25fps 的 MP4输出为同分辨率标注 MP4。先记录每个阶段处理了多少帧、花了多少时间、是否出现重试再只改一个变量复测避免一次同时改模型、编码器和 batch 后无法判断收益来自哪里。1. 解码慢先确认不是输入媒体或读取路径在拖后腿如果VideoCapture.read()经常等待、CPU/GPU 都不忙或同一视频在不同机器耗时差异极大先检查输入编码、磁盘位置和实际读帧速度。不要只看文件大小高码率、长 GOP、网络盘读取、损坏帧重试都可能让解码成为主耗时。ffprobe用于记录编码、帧率、时长和流信息FFmpeg 的-benchmark_all可辅助观察解码/编码阶段耗时。ffprobe-verror-select_streamsv:0\-show_entriesstreamcodec_name,width,height,r_frame_rate,avg_frame_rate\-show_entriesformatduration,size-ofjson source.mp4 ffmpeg-benchmark_all-isource.mp4-fnull -若媒体侧耗时占比高优先比较本地磁盘与网络路径、输入编码差异、目标帧抽取策略若业务只需每 5 帧分析一次应在任务定义中明确采样策略而不是悄悄跳帧后仍按全帧结果宣传速度。2. 预处理慢查重复转换与 CPU-GPU 数据搬运视觉模型通常需要固定尺寸和颜色排列例如 BGR 转 RGB、缩放、归一化、HWC 转 CHW再把数组复制到 GPU。逐帧创建新数组、重复cvtColor、每个小步骤都在 CPU/GPU 间同步会让模型等待数据。先在日志中把read、resize、color_convert、to_device单独计时再检查是否有两次缩放或不必要的numpy - tensor - numpy往返。frametimer.measure(decode,capture.read)rgbtimer.measure(bgr_to_rgb,lambda:cv2.cvtColor(frame,cv2.COLOR_BGR2RGB))input_tensortimer.measure(preprocess,lambda:to_model_tensor(rgb,size640))input_tensortimer.measure(host_to_device,lambda:input_tensor.to(cuda,non_blockingTrue))这里的目标不是把代码压成一行而是让每次内存搬运都有名字。若host_to_device占比异常才进一步检查 batch、固定输入形状、页锁定内存或运行时配置不能先假定 GPU 算力不足。3. 推理慢区分模型算子、输入形状与运行时开销确认推理是真瓶颈后再比较模型大小、输入尺寸、batch 和运行时。输入边长从 640 提升到 1280计算量并非简单翻倍动态输入形状也可能引入额外的运行时开销。PyTorch 可用 Profiler 查算子和显存TensorRT 可用trtexec或逐层信息查引擎执行但任何 FP16/INT8、TensorRT、ONNX 的速度收益都必须配合关键类别的 recall 和标注帧复检。不要把“推理时间下降”直接写成“系统性能提高”。若量化后小目标漏检增多或动态形状在真实输入上出现首帧长尾整体任务并没有真正变好。4. 后处理与编码慢模型完成以后仍有大量工作NMS、跟踪关联、掩膜缩放、文字绘制和将帧写回视频都发生在模型之后。特别是逐帧绘制大量中文文本、逐帧创建编码器、或把临时图片落盘再读回常会让“模型只占 30%”的任务被误诊为推理慢。编码阶段还要区分 CPU 编码、GPU 编码、码率控制和写盘等待优化时保持输出分辨率、帧率和媒体校验一致不能以降低交付要求换取好看的数字。推荐排查顺序媒体探测 - 分阶段计时 - 确认最大阶段 - 只改一个变量 - 重新检查输出帧数、检测结果和媒体属性 - 写入新的基线或回滚。图2瓶颈不同优化方向不同模型推理不是唯一可优化对象。三、最小测量实现、测试与 SQLfromtimeimportperf_counterclassStageTimer:def__init__(self):self.total{}defmeasure(self,name,fn):beginperf_counter();resultfn()self.total[name]self.total.get(name,0.0)(perf_counter()-begin)*1000returnresultdefbottleneck(total:dict[str,float])-str:returnmax(total,keytotal.get)GPU 计时不能只包住一行model(frame)CUDA 调用通常是异步提交的CPU 代码继续往下走并不代表 GPU 已完成计算。因此直接用perf_counter()包住推理调用可能得到的只是提交开销。基准前需要预热计时边界需要同步并将解码、拷贝、推理、后处理分开记录importtimeimporttorchdeftimed_inference(model,batch):for_inrange(10):model(batch)# 预热不计入结果torch.cuda.synchronize()startedtime.perf_counter()outputmodel(batch)torch.cuda.synchronize()returnoutput,(time.perf_counter()-started)*1000这段示例只适用于 CUDA 可用时CPU、ONNX Runtime 或不同推理引擎应采用各自的同步与计时方式。一次基准还应记录 P50、P95 和最大值。平均 20ms、但每 100 帧有一次 400ms 卡顿的系统对实时预览和视频队列都是不同的问题。用 Profiler 找到“推理慢”背后的具体阶段阶段计时告诉我们哪一段慢torch.profiler可以进一步观察 CPU、CUDA、显存、算子输入形状和调用栈。Profiler 本身有开销应只对有限帧采样不能混入正式性能数字withtorch.profiler.profile(activities[torch.profiler.ProfilerActivity.CPU,torch.profiler.ProfilerActivity.CUDA],record_shapesTrue,profile_memoryTrue,)asprofiler:for_inrange(20):model(batch)print(profiler.key_averages().table(sort_bycuda_time_total,row_limit12))看到resize、颜色转换或 CPU 到 GPU 的复制占比高时换更大模型没有意义看到 NMS 或绘制占比高时应审查后处理若 GPU 算子本身占主导才进入模型规格、输入尺寸、batch、运行时和精度的选择。deftest_bottleneck_is_largest_stage():assertbottleneck({decode:12,infer:50,encode:20})infer固定基准与预期输出基准任务VP-20261005-001使用同一份 90 秒、25fps、1080P 脱敏视频固定模型版本、输入尺寸、阈值和输出编码。每次运行输出以下摘要而不是只有“总用时”{frameCount:2250,decodeMs:18400,preprocessMs:6200,inferenceMs:33200,postprocessMs:5800,encodeMs:19100,outputFrames:2250}例如推理占比不足 30% 时优先优化模型很可能没有收益若编码耗时占比最高则需要检查交付编码策略和存储而不是压低检测阈值。基准输入和输出质量不变是任何前后对比成立的前提。媒体基准与推理基准必须分开看FFmpeg 可以通过-benchmark或-benchmark_all输出编码、解码相关的时间信息它适合确认媒体侧是否成为瓶颈但不能证明模型算子是否高效。相反TensorRT 或 ONNX 等推理运行时的基准也不应拿来代表整条 MP4 工作流。# 只用于收集媒体侧基准信息实际编码参数按交付要求确定。ffmpeg-benchmark-isource.mp4-c:vlibx264-c:aaac output.mp4# 运行时基准要固定输入形状、batch、精度和设备不能与端到端时长混写。trtexec--onnxmodel.onnx--shapesimages:1x3x640x640--useCudaGraph运行时选择也不是“TensorRT 一定更快”。TensorRT 可使用 FP16、INT8 等精度和图优化来改善 NVIDIA GPU 推理但需要对量化或精度切换后的关键类别结果重新验证CPU 部署则可能更适合 ONNX 或 OpenVINO 等路径。先建立统一输出质量的基线再比较运行时才能避免用速度掩盖感知退化。CREATETABLEvisual_performance_profile(idBIGINTPRIMARYKEYAUTO_INCREMENT,run_noVARCHAR(64)NOTNULL,frame_countINTNOTNULL,decode_msDECIMAL(12,3)NOTNULL,preprocess_msDECIMAL(12,3)NOTNULL,inference_msDECIMAL(12,3)NOTNULL,postprocess_msDECIMAL(12,3)NOTNULL,encode_msDECIMAL(12,3)NOTNULL,UNIQUEKEYuk_visual_profile_run_no(run_no));SELECTrun_noFROMvisual_performance_profileWHEREframe_count0ORdecode_ms0ORinference_ms0ORencode_ms0;Transactional(rollbackForException.class)publicPerformanceSummaryrecord(PerformanceCommandcommand,StageCostscosts){if(costs.frameCount()!command.expectedFrames()){thrownewBizException(性能结果帧数不完整);}profileRepository.save(command.runNo(),costs);returnPerformanceSummary.of(costs,costs.largestStage());}自动测试除最大阶段识别外还应覆盖帧数减少、阶段耗时为负、输出编码失败和运行时版本改变四种情况。任何一项发生时不能把结果和上一轮基准放进同一张性能趋势图。图3相同输入、输出质量与阶段数据是性能比较的基础。四、验收边界优化后仍应核对输入帧数、输出帧数、模型/阈值、最终媒体流和视觉抽检。报告需要区分平均值与长尾耗时避免偶发解码或写盘阻塞被均值掩盖。速度提升不能以少处理帧、丢失证据或损坏成片为代价。性能通过不等于结果通过一次优化可以改变运行时、batch、精度、编解码器或并行策略但不能悄悄改变任务的业务含义。验收时至少把“速度是否提升”和“结果是否等价”拆成两张检查表前者关注总耗时、P50、P95、显存和失败率后者关注处理帧范围、关键类别结果、事件数量、掩膜/标注质量和最终媒体属性。验收层优化后必须保持或重新证明的事实常见伪优化输入范围源文件摘要、起止时间、采样策略、实际读取帧数悄悄少读尾部帧或提高抽帧间隔感知结果关键类别 recall、置信度阈值、检测/跟踪/掩膜抽检降低输入尺寸或量化后漏检增加事件结果事件数、唯一性、时间窗口与证据引用后处理跳过导致重复事件或少记事件视频交付宽高、fps、时长、视频/音频流、可播放性降分辨率、去音频或输出损坏文件稳定性连续多次运行的 P95、失败率、显存峰值只挑一次最快运行作为结论例如若为提升吞吐把输入从逐帧改为每 5 帧分析必须将其登记为采样策略变更重新评估快速移动目标的 recall 和事件时间偏差它不是同等条件下的性能优化。若从 FP32 改为 INT8也必须在关键类别、困难样本和输出视频上重新验收不能只看引擎的毫秒数。三道验收门自动完整性、视觉抽检、媒体探测第一道门是自动完整性输入和输出帧数、任务状态、阶段记录、异常码是否齐全。第二道门是视觉抽检在固定时间点对照优化前后的标注画面检查检测框、轨迹编号、掩膜边缘或事件标记是否改变。第三道门是媒体探测用ffprobe核对正式文件存在预期视频流、时长和分辨率必要时再抽样播放确认浏览器或目标播放器没有黑屏、无声或时间轴异常。defperformance_result_acceptable(before,after):same_framesbefore[output_frames]after[output_frames]recall_okafter[key_recall]before[min_key_recall]latency_okafter[p95_ms]before[p95_ms]media_okafter[video_stream]andafter[duration_delta_ms]100returnsame_framesandrecall_okandlatency_okandmedia_okdeftest_faster_run_with_missing_frames_is_rejected():before{output_frames:2250,min_key_recall:0.90,p95_ms:80}after{output_frames:2200,key_recall:0.93,p95_ms:40,video_stream:True,duration_delta_ms:0}assertnotperformance_result_acceptable(before,after)什么时候可以更新性能基线只有当三道门都通过且连续多次运行没有出现异常长尾、显存泄漏或媒体失败才应将该版本写为新基线。基线记录应同时包含源文件摘要、模型和运行时版本、设备信息、输出配置、均值/P95、关键质量指标和验收日期。任何一项改变都应创建新行而不是覆盖旧记录这样性能回退时才能知道是模型、驱动、输入还是交付策略发生了变化。图4提速后仍要证明输出与优化前具有相同的处理范围和交付质量。五、性能结论怎样才能比较一份性能报告至少要说明“比较了什么”和“没有比较什么”。例如把 FP32 的小模型、INT8 的大模型、不同输入尺寸和不同编码器混在一张 FPS 表里数字即使真实也不能帮助读者或团队作出选择。性能优化前后必须冻结输入视频、帧数、模型版本、阈值、运行时、设备、输出分辨率和编码要求。比较项必须保持一致为什么输入同一视频、同一帧范围、同一解码策略避免素材复杂度改变造成假提升感知结果同一模型/类别/阈值或明确记录改变防止靠放宽阈值减少后处理工作交付要求同一输出分辨率、帧率、编码和媒体校验防止靠降低成片质量换速度运行环境设备、驱动、运行时、批次策略让其他人能够复现结论指标总时长、各阶段均值与 P95平均值不能掩盖长尾卡顿当确实需要改变模型、输入尺寸或量化精度时应把它记录为一次方案切换而不是把它混成“同一方案优化”。此时除了比较速度还要重新比较关键类别的 recall、标注帧抽检和最终输出帧数。只有速度和视觉质量同时满足目标才允许写入新的性能基线。defcomparable(run_a,run_b):keys(source_sha256,frame_count,input_size,runtime,output_profile)returnall(run_a[k]run_b[k]forkinkeys)deftest_changed_output_profile_is_not_same_baseline():assertnotcomparable({source_sha256:a,frame_count:100,input_size:640,runtime:trt,output_profile:h264-1080p},{source_sha256:a,frame_count:100,input_size:640,runtime:trt,output_profile:h264-720p},)图5性能结论必须同时说明快在哪里、慢在哪里以及输出是否仍可靠。六、小结性能优化不是让某个 GPU 指标变漂亮而是缩短可交付视频任务的真实等待时间。先测量再定位最大阶段优化后保持可比条件并用同一输出质量重新验收才能得到可复用的性能结论。参考资料PyTorch Profiler、PyTorch Profiler Tutorial、NVIDIA TensorRT Performance Benchmarking、NVIDIA TensorRT Best Practices、FFmpeg Benchmark Options。