A video-heavy inference pipeline runs at half the frame rate you need. The instinct is to blame the model, or the Python glue around it, and start scoping a C++ or WASM port. That instinct is often wrong — and ffmpeg’s own benchmark instrumentation will tell you so, in a few minutes, before anyone writes a line of ported code. The failure we see most often in video inference work is not a slow model. It is a team that measured the whole pipeline end-to-end, saw a low frame rate, and concluded the inference path was the problem. When you run ffmpeg with its -benchmark and -benchmark_all flags, the wall-clock time frequently lands somewhere else entirely: decode, scaling, pixel-format conversion, or the encode back to disk. Porting the inference path in that situation moves the engineering cost — sometimes weeks of it — without touching the actual bottleneck. What does ffmpeg’s benchmark flag actually measure? ffmpeg -benchmark prints the elapsed real, user, and system time for the whole run, plus the maximum resident memory. It answers “how long did this transcode take, and how much RAM did it touch.” That is useful, but coarse. The more revealing flag is -benchmark_all, which reports timing per processing action — decode, filter, and encode — so you can see which stage owns the wall-clock. Add -progress (writing key-value progress to a file or pipe:1) and you get a live stream of frame, fps, bitrate, and out_time, which lets you watch where throughput stalls under sustained load rather than reading only the final summary. The combination is what turns ffmpeg from a transcoder into a profiler for the video-IO half of your pipeline. A concrete reading, framed as an example: if a 10-minute 1080p transcode reports 45 seconds of decode time, 30 seconds of scale/filter time, and 12 seconds of encode time on -benchmark_all, and your model does its work inside that same pass, then the video IO is consuming roughly 87 seconds of a pass that your inference step barely touches. No port of the Python inference path changes that 87 seconds. How do I read the real/user/sys and fps numbers? The three time figures come straight from the operating system’s accounting, and they mean different things: real — wall-clock elapsed. This is what your latency budget cares about. user — CPU time spent in user-space code (the actual codec and filter math). sys — CPU time in kernel calls (IO, memory mapping, syscalls). A real time much larger than user + sys points at waiting — disk, network, or a stalled pipe — not at compute. A user time close to real on a single-threaded run says the codec is CPU-bound and a hardware decoder might help. The fps line from -progress is your sustained-throughput signal; a headline fps that sags over a long clip usually means thermal throttling or an IO backlog rather than model cost. These are the same distinctions we walk through when profiling the Python inference path before a C++ or WASM port — ffmpeg just supplies the video-IO slice that a Python profiler cannot see cleanly. Separating transcode cost from model compute The core reframe is this: a video inference pipeline has at least two distinct cost centres — the video IO (decode, scale, convert, encode) and the model compute — and end-to-end timing collapses them into one number that tells you nothing about where to spend engineering effort. Reading them apart is the whole point of a benchmark pass. Run ffmpeg’s benchmark on the transcode stage alone, with no model in the loop, to get a clean video-IO baseline. Then run your full pipeline and profile the Python and model portions separately. The difference between the two is your inference-and-glue cost. If ffmpeg’s isolated transcode already accounts for most of your end-to-end wall-clock, the model is not the critical path, and neither is Python. This is also why “computationally expensive” is such a slippery phrase in these conversations — teams use it to mean the model when the expense is really in the pixel plumbing. We unpack that confusion in more depth in what “computationally expensive” means in an inference path and where the cost actually lives. Decision table: what each ffmpeg reading tells you to do next Benchmark reading Likely bottleneck Correct fix Not the fix Decode time dominates -benchmark_all Software codec on CPU Enable hardware decode (-hwaccel cuda, NVDEC, VideoToolbox, VAAPI) Porting the inference path Scale/filter time dominates Pixel-format conversion or resize on CPU Move scaling to GPU (scale_cuda, scale_npp) or match model input format upstream Rewriting the model glue Encode time dominates Software encoder (libx264/libx265) Hardware encoder (NVENC, QSV) if quality tolerance allows Optimising the decode path real ≫ user + sys IO wait — disk or network read of source Faster storage, prefetch, or local cache Any compute-side change Video IO small, model + Python large Genuine inference-path cost Now a port or model optimisation is justified Blaming ffmpeg The table is the whole argument in one surface: only the last row justifies the port that teams reach for first. When is hardware acceleration or codec choice the fix, not a port? Whenever decode or encode dominates the timing and you are running a software codec, the highest-leverage change is -hwaccel selection, not a rewrite of your inference code. NVIDIA’s NVDEC/NVENC (exposed in ffmpeg via -hwaccel cuda and the h264_nvenc/hevc_nvenc encoders), Intel Quick Sync Video, and Apple’s VideoToolbox all move the codec work off the CPU. In configurations we have profiled, switching a 1080p H.264 decode from libx264 software to NVDEC can drop decode wall-clock by a large multiple — the exact factor depends on the GPU generation and clip, so treat it as an observed pattern rather than a fixed number. There is a subtlety worth naming: hardware transcode changes the pixel path. If your model expects frames in a specific layout, a GPU-decoded frame that stays in device memory may avoid a costly copy back to host — or, done carelessly, may force an extra copy that eats the gain. The benchmark pass is what catches that. Codec choice matters too; HEVC decode is heavier than H.264, and AV1 heavier still, so a “slow pipeline” can sometimes be traced entirely to an input format decision made upstream. On the CPU side, instruction-set support shifts the numbers as well — see FFmpeg AVX-512 performance on AMD Ryzen for CPU-side asset encoding for how SIMD width changes software-encode timings. How does an ffmpeg benchmark feed the port-or-don’t-port decision? The benchmark pass is the profiling baseline that a port assessment stands on. Before deciding whether to move an inference path to C++ or WASM, you need to know whether Python is even in the critical path — and for a video workload, you cannot answer that without attributing the video-IO share of latency first. That attribution is exactly what ffmpeg supplies. We treat this as the video-IO front-end of a broader port-assessment discipline. Deciding to move a workload to new hardware or a new runtime is a measured decision, not a reflex; software porting is a defined engineering activity with its own risk profile, and skipping the baseline is how teams spend that budget on the wrong stage. The same reasoning drives our [inference cost-cut engagements](Inference Cost-Cut Pack), where the profiling-baseline step explicitly separates video IO from model compute before any optimisation is proposed. If you want the wider picture of how GPU-accelerated pipelines are measured and tuned, our GPU engineering practice is the anchor. One more caution on interpretation: even a clean inference-path finding does not automatically mean port. A batching change can move a GPU model from IO-bound to compute-bound without any language rewrite — the mechanics of that are covered in YOLO inference on GPU and what batching really changes. The benchmark tells you where the cost lives; the fix still has to be chosen against that map. FAQ How does ffmpeg benchmark actually work? The -benchmark flag makes ffmpeg print elapsed real, user, and system time plus peak memory for a run, and -benchmark_all breaks timing down per decode, filter, and encode action. In practice it turns a transcode command into a profiler for the video-IO half of a pipeline, letting you see which stage owns the wall-clock instead of guessing. How do I read ffmpeg’s -benchmark and -benchmark_all output (fps, real/user/sys, memory)? real is wall-clock elapsed, user is CPU time in codec/filter code, and sys is kernel/IO time; a real far larger than user + sys means the run is waiting on IO, not computing. The fps figure from -progress is your sustained-throughput signal, and the memory high-water mark from -benchmark_all shows the run’s peak resident footprint. How do I separate decode/transcode cost from model compute and Python overhead in a video inference pipeline? Run ffmpeg’s benchmark on the transcode stage with no model in the loop to get a clean video-IO baseline, then profile the full pipeline and its Python/model portions separately. The gap between the isolated transcode time and the end-to-end time is your inference-and-glue cost; if the transcode alone accounts for most of the wall-clock, the model is not the critical path. When do ffmpeg timings indicate that hardware acceleration or codec choice — not a port — is the fix? When decode or encode time dominates -benchmark_all and you are on a software codec, enabling -hwaccel (NVDEC/NVENC, Quick Sync, VideoToolbox) or changing the input codec is the high-leverage fix, not an inference-path rewrite. A heavier codec like HEVC or AV1 upstream can explain a “slow pipeline” entirely, so the timing points at codec/hwaccel selection rather than a port. How does an ffmpeg benchmark feed the port-or-don’t-port profiling baseline? The benchmark pass attributes the video-IO share of latency, which is the piece a Python profiler cannot see cleanly, so a port assessment can confirm whether Python is even in the critical path. Only when video IO is small and the model plus Python dominate does a C++ or WASM port become a justified spend rather than misplaced effort. When does video IO, rather than the inference path, dominate pipeline latency? Video IO dominates when decode, scaling, pixel-format conversion, or encode consume most of the -benchmark_all timing — typically with software codecs, high-resolution or high-bitrate sources, or heavy formats like HEVC and AV1. In those cases the model may barely touch the critical path, and the fix lives in codec choice, hardware acceleration, or a pipeline restructure, not in the inference code. The uncomfortable finding, when it lands, is usually that the port everyone wanted to build would have saved nothing. That is the value of the benchmark pass — it fails the port-decision test cheaply, on the transcode-dominance pattern, before the port becomes a commitment.