我们的转码服务上了 GPU 机器,我把命令改成
-hwaccel cuda -c:v h264_nvenc,满怀期待地跑了一次。结果:
- 纯转码(不滤镜):快了 3.2 倍,不错;
- 加缩放:
scale滤镜 → 快了 1.4 倍,还行;- 加字幕(烧录):比纯 CPU 慢 15%,完全没道理。
第三个结果让我很困惑,查了 ffmpeg 的日志才明白:
subtitles滤镜只能在 CPU 上跑,所以帧必须从显存拷回内存(hwdownload),滤镜处理完再拷回显存(hwupload)给 NVENC。这两次拷贝 + 中间的 CPU 处理,开销比省下的解码时间还大。这篇把这个机制讲清楚:帧到底在哪里、什么时候会被拷贝、怎么避免,以及什么时候干脆别用硬件加速。
TL;DR:硬件加速的三个环节(解码/滤镜/编码)里,滤镜是短板——大部分滤镜只有 CPU 实现,一旦用上就必须在显存和内存之间来回拷贝(
hwdownload/hwupload),这个拷贝能吃掉全部收益。三种管线的实测:纯转码(无滤镜)快 3.2 倍;只用 GPU 滤镜(scale_cuda等)快 2.6 倍;用 CPU 滤镜(如 subtitles)反而慢 15%。另外两个坑:消费级显卡的 NVENC 有并发路数限制(3~8 路);同码率下 NVENC 画质不如 x264(要多给 15~25% 码率)。
目录
- 一、先搞清楚帧在哪
- 二、三种管线的数据流
- 三、hwdownload / hwupload 是什么
- 四、哪些滤镜有 GPU 版本
- 五、三种管线的实测命令
- 六、解码端:硬解的限制
- 七、编码端:NVENC 的参数与画质
- 八、并发路数限制:消费级卡的坑
- 九、QSV / VAAPI 简述
- 十、什么时候不该用硬件加速
- 十一、坑清单
一、先搞清楚帧在哪
视频帧有两个"家":
| 位置 | 谁用它 | 特点 |
|---|---|---|
| 系统内存(RAM) | CPU 解码器、绝大多数滤镜、CPU 编码器(libx264/libx265) | 通用、灵活,但慢 |
| 显存(GPU VRAM) | 硬件解码器(NVDEC)、GPU 滤镜(scale_cuda 等)、硬件编码器(NVENC) | 快,但格式受限、滤镜种类少 |
关键认知:帧在两边之间搬运是有成本的。
一次 1080p 帧(NV12 格式)大约是 1920×1080×1.5 ≈ 3.1 MB。PCIe 传输本身很快(几 GB/s),但:
- 每次传输有固定开销(同步、内存分配);
- 传输是阻塞的(要等 GPU 完成);
- 每秒 25~50 帧 × 3MB = 75~150 MB/s 的持续搬运。
单次拷贝看起来不多,但如果每帧要拷两次(进去 + 出来),加上 CPU 滤镜的处理时间,总账就不划算了。
二、三种管线的数据流
管线 A:全 CPU(传统)
解码(CPU) → [内存] → 滤镜(CPU) → 编码(CPU)
全程在内存里,没有拷贝。慢,但可预测。
管线 B:GPU 解码 + CPU 滤镜 + GPU 编码(最常见也最容易踩坑)
解码(GPU) → [显存] --hwdownload--> [内存] → 滤镜(CPU) --hwupload--> [显存] → 编码(GPU)
↑ 拷贝 1 ↑ 拷贝 2
这就是我踩的坑。省了解码和编码的 CPU 时间,但多了两次拷贝 + 滤镜仍在 CPU。
管线 C:全 GPU(零拷贝)
解码(GPU) → [显存] → 滤镜(GPU) → 编码(GPU)
全程在显存里,没有拷贝。这是理想情况,但要求所有滤镜都有 GPU 版本。
三、hwdownload / hwupload 是什么
这两个是显式的格式转换滤镜:
| 滤镜 | 作用 |
|---|---|
hwupload |
把帧从内存上传到显存 |
hwupload_cuda |
同上(指定 CUDA 设备类型) |
hwdownload |
把帧从显存下载到内存 |
hwdownload,format=nv12 |
下载并指定像素格式 |
ffmpeg 会自动插入它们:当你用 -hwaccel cuda 解码,然后接一个只能跑 CPU 的滤镜时,ffmpeg 会在滤镜链里自动插入 hwdownload(以及后面的 hwupload)。
怎么知道有没有被自动插入?看日志(-v verbose 或者看滤镜图的输出):
ffmpeg -v verbose -hwaccel cuda -i in.mp4 -vf "subtitles=sub.ass" -c:v h264_nvenc out.mp4
会看到滤镜图里出现:
[Parsed_hwdownload_1 @ ...]
[Parsed_hwupload_3 @ ...]
看到这两个就说明发生了拷贝。想避免,就得换掉那个 CPU 滤镜。
四、哪些滤镜有 GPU 版本
这是选管线时的关键清单(CUDA 平台):
| 功能 | CPU 滤镜 | GPU 版本 |
|---|---|---|
| 缩放 | scale |
scale_cuda(还有 scale_npp,NPP 版) |
| 裁剪 | crop |
crop_cuda |
| 叠加/水印 | overlay |
overlay_cuda |
| 去隔行 | yadif / bwdif |
yadif_cuda / bwdif_cuda |
| 色调映射(HDR→SDR) | tonemap |
tonemap_cuda |
| 缩略图 | thumbnail |
thumbnail_cuda |
| 格式转换 | format |
format(在显存上下文里可用) |
| 色彩空间 | colorspace |
colorspace_cuda |
| 锐化 | unsharp |
sharpen_npp / 部分 NPP |
| 降噪 | hqdn3d / nlmeans |
nlmeans_npp(部分) |
| 转场/混合 | blend |
blend_cuda |
没有 GPU 版本的常见滤镜(用了就必然回 CPU):
subtitles(烧字幕)——最常见的元凶;drawtext(画文字/时间码);ass(老的字幕滤镜);- 所有音频滤镜(音频本来就不在 GPU 上);
concat、trim、fps(部分有替代方案);- 大部分"智能"滤镜(场景检测、去水印)。
判断方法:
ffmpeg -filters 2>/dev/null | grep -E '_cuda|_npp|_qsv|_vaapi'
如果找不到你要的滤镜的 GPU 版本,那就要么接受回 CPU,要么换方案。
五、三种管线的实测命令
管线 A:全 CPU(基线)
ffmpeg -i in.mp4 \
-vf scale=1280:720 \
-c:v libx264 -crf 23 -preset medium \
-c:a aac -b:a 128k out_cpu.mp4
管线 B:GPU 解码 + CPU 滤镜 + GPU 编码(有拷贝)
ffmpeg -hwaccel cuda -i in.mp4 \
-vf "scale=1280:720,subtitles=sub.ass" \ # subtitles 强制回 CPU
-c:v h264_nvenc -preset p4 -cq 23 \
-c:a aac -b:a 128k out_mixed.mp4
管线 C:全 GPU(零拷贝)
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i in.mp4 \
-vf "scale_cuda=1280:720" \ # GPU 缩放,不回内存
-c:v h264_nvenc -preset p4 -cq 23 \
-c:a aac -b:a 128k out_gpu.mp4
-hwaccel_output_format cuda 这一行很重要:它告诉 ffmpeg"解码后的帧就留在显存里,别拷回内存"。不加的话,默认会把解码结果拷回内存(ffmpeg 的传统行为),白浪费一次拷贝。
折中方案:字幕场景
烧字幕必须用 CPU 滤镜,但仍可以让解码和编码留在 GPU:
ffmpeg -hwaccel cuda -i in.mp4 \
-vf "hwdownload,format=nv12,subtitles=sub.ass,hwupload" \
-c:v h264_nvenc -preset p4 -cq 23 out_sub.mp4
显式写出来比让 ffmpeg 自动插入更好(行为可预测)。而且 format=nv12 要明确——hwdownload 之后帧是硬件格式,CPU 滤镜不认,必须转成正常的像素格式。
实测结论(1080p → 720p + 烧字幕,10 分钟素材):
| 管线 | 耗时 | CPU 占用 | 结论 |
|---|---|---|---|
| A 全 CPU | 4 分 12 秒 | 780% | 基线 |
| B 混合(自动拷贝) | 4 分 48 秒 | 320% | 更慢 |
| C 全 GPU(无字幕) | 1 分 18 秒 | 110% | 快 3.2 倍 |
| 折中(显式拷贝 + 字幕) | 4 分 35 秒 | 350% | 略慢于纯 CPU |
结论很清楚:有 CPU 滤镜时,硬件加速没有收益。什么时候用 GPU,取决于你的滤镜链。
六、解码端:硬解的限制
硬件解码器(NVDEC)不是什么都能解:
| 限制 | 说明 |
|---|---|
| 格式支持按显卡代次不同 | 老显卡(比如 GTX 10 系之前)不支持 HEVC 硬解,更不支持 10bit HEVC |
| 10bit / 4:2:2 支持有限 | 很多消费级卡只支持 8bit 4:2:0 的硬解 |
| 分辨率上限 | 老卡可能不支持 4K 硬解 |
| 不支持的格式会静默回退 | ffmpeg 会退回软解(不报错,但你以为在用硬解) |
查你的显卡支持什么:
ffmpeg -hwaccels # 列出支持的 hwaccel 类型
# cuda, vaapi, qsv, ...
# 看 NVENC/NVDEC 的能力矩阵(NVIDIA 官网有表)
验证是否真的在用硬解:
ffmpeg -v verbose -hwaccel cuda -i in.mp4 -f null - 2>&1 | grep -i 'cuda\|nvdec'
或者在转码时看 GPU 利用率:
nvidia-smi dmon -s u # 持续看 GPU 利用率
# 如果 GPU 利用率是 0%,说明根本没用上
我踩过的坑:一批 HEVC 10bit 的素材在老卡上"硬解失败",ffmpeg 静默回退到软解,CPU 打满但我们还以为在加速。后来加了日志检查才发现的。
七、编码端:NVENC 的参数与画质
NVENC 的常用参数:
-c:v h264_nvenc \
-preset p1~p7 \ # p1 最快、p7 最慢质量最好
-tune hq \ # 高质量模式(还有 ll=低延迟、ull=超低延迟)
-rc vbr \ # 码率控制:vbr / cbr / constqp
-cq 23 \ # 类似 CRF(constqp 模式下是 QP 值)
-b:v 5M -maxrate 6M -bufsize 10M \
-profile:v high -level 4.1
preset 的选择(NVENC 的 preset 跟 x264 的完全不同):
| preset | 说明 |
|---|---|
| p1 | 最快("Ultra Fast") |
| p2~p4 | 平衡 |
| p4 | 常用("Medium") |
| p5~p7 | 慢,质量更好(p7 是 "Max Quality") |
注意:NVENC 的 preset 跟 x264 的 ultrafast/veryslow 不是一回事,不要按名字类比。
画质对比(同码率,VMAF):
| 编码器 | VMAF(5 Mbps 1080p) | 说明 |
|---|---|---|
| libx264 medium | 94.2 | CPU 软编,画质基准 |
| libx264 slow | 94.8 | |
| h264_nvenc p4 | 91.5 | 同码率下低 2.7 分 |
| h264_nvenc p7 | 92.8 | 慢一些,接近但仍不如 |
| h264_nvenc p1 | 88.1 | 明显差 |
实用结论:NVENC 要比 x264 多给 15~25% 的码率才能达到相近的画质。
这意味着"省算力"和"省带宽"是矛盾的:
- 追求吞吐(大量内容、时效紧)→ NVENC + 多给码率;
- 追求存储和带宽(长期归档、播放量大)→ CPU 软编(慢但省)。
我的选择:
- 实时/准实时转码、直播转码 → NVENC(没得选,要时效性);
- 批量归档、母版 → libx265 CPU(画质和压缩率优先);
- 中间地带(量大、质量要求一般)→ NVENC + 提高码率。
八、并发路数限制:消费级卡的坑
这是很多人在生产上才发现的坑:
NVIDIA 的消费级(GeForce)驱动对同时进行的 NVENC 会话数有限制(历史上是 2~3 路,后来放宽到 5~8 路,取决于驱动版本和卡型)。专业卡(Tesla / A 系列数据中心卡 / RTX A 系列)没有这个限制。
表现:
[h264_nvenc @ 0x...] OpenEncodeSessionEx failed: out of memory (10)
[h264_nvenc @ 0x...] No capable devices found
不是真的显存不够,是驱动层面的并发限制。
应对:
- 控制并发数(最简单):worker 数 ≤ 限制数;
- 用专业卡(如果规模大,算下来更划算);
- Linux 上有非官方的驱动补丁(不推荐生产用,也不合规)。
查你的卡的限制:
nvidia-smi --query-gpu=name,encoder.stats.sessionCount --format=csv
或者干脆实测:同时起 N 路转码,看第几路开始失败。
我们的做法:一张消费级卡最多跑 4 路并发(留余量),并且给 NVENC 任务单独一个队列(前面分布式转码那篇讲过队列拆分)。
九、QSV / VAAPI 简述
除了 NVIDIA,还有:
| 方案 | 平台 | 说明 |
|---|---|---|
| NVENC/NVDEC | NVIDIA 显卡 | 生态最好,ffmpeg 支持最全 |
| QSV | Intel 核显 / Arc | 服务器和桌面上常见,省电,画质一般 |
| VAAPI | Linux 通用(Intel/AMD) | 开源驱动的统一接口,ffmpeg 支持好 |
| VideoToolbox | macOS | Apple 平台的硬编 |
| AMF | AMD 显卡 | Windows 上 |
QSV 的例子:
ffmpeg -hwaccel qsv -qsv_device /dev/dri/renderD128 -i in.mp4 \
-vf "scale_qsv=1280:720" \
-c:v h264_qsv -preset medium out.mp4
选择建议:
- 有 NVIDIA 卡 → CUDA/NVENC(支持最全);
- Intel 核显的服务器(很多云主机都有)→ QSV,能省很多 CPU;
- Linux + AMD → VAAPI。
一个实际情况:云上的很多"入门级"GPU 实例其实是消费级卡,要注意前面说的并发限制。而 Intel 核显的 QSV 虽然画质一般,但没有并发限制、成本低,对"量大、质量要求不高"的批量转码反而更合适。
十、什么时候不该用硬件加速
基于上面的实测,我的判断清单:
适合用:
- ✅ 纯转码(解码 → 编码),没有 CPU-only 滤镜;
- ✅ 滤镜链里所有滤镜都有 GPU 版本;
- ✅ 实时/准实时要求(直播转码、低延迟);
- ✅ 吞吐优先、画质要求可以放宽(多给码率);
- ✅ 量大、时效紧(批量处理,能多给 20% 码率换 3 倍速度)。
不适合:
- ❌ 需要烧字幕、画文字水印、复杂 CPU 滤镜;
- ❌ 归档母版、追求最高画质/压缩率;
- ❌ 源格式不在硬解支持范围(会静默回退);
- ❌ 显卡太老(不支持目标格式的硬解/硬编);
- ❌ 只需要处理几个文件(配置调试的时间比省下的多)。
一个务实的做法:按任务类型选管线。我们的转码服务里有两条队列:
queue:transcode_gpu → 纯转码任务,全 GPU 管线(快 3 倍)
queue:transcode_cpu → 带字幕/复杂滤镜的任务,全 CPU 管线
提交任务时根据"有没有 CPU-only 滤镜"决定进哪个队列。这个分流比"全部上 GPU"效率高得多。
十一、坑清单
- 以为加了
-hwaccel就一定加速 → 有 CPU 滤镜时更慢。看滤镜链。 - 没加
-hwaccel_output_format cuda→ 解码结果被拷回内存,白浪费一次拷贝。 - 用了
subtitles/drawtext还期待加速 → 必然回 CPU。 hwdownload后忘了format=nv12→ 后续 CPU 滤镜不认硬件格式,报错。- 不检查是否真的在用硬解 → 格式不支持时 ffmpeg 静默回退到软解。用 nvidia-smi 验证 GPU 利用率。
- NVENC 用和 x264 相同的码率期待相同画质 → 要多给 15~25%。
- NVENC preset 按 x264 的理解来选 → 两者体系完全不同。
- 消费级卡的并发限制没考虑 → 超过限制报 "out of memory" 的假错误。
- 多进程共享一张卡没做并发控制 → 同上。
- 显存不够(多路并发 + 高分辨率)→ 真的 OOM。要算:每路大约 200~500MB 显存(不含模型)。
- 硬件编码器不支持的格式(比如某些 4:2:2)→ 会失败或回退。
- 音频也走 GPU → 音频永远在 CPU,无所谓但不要误以为有加速。
-hwaccel放在-i后面 → 必须在输入之前(它是输入选项)。- 多线程/多进程时 CUDA 上下文初始化开销 → 短任务(几秒)反而更慢,因为有初始化开销。短任务用 GPU 不划算。
- 驱动/CUDA 版本和 ffmpeg 编译时的版本不匹配 → 报奇怪的错误。升级后要重新验证。
最后说说我对硬件加速的整体看法。
硬件加速不是"开了就快"的开关,它是一条完全不同的流水线,要求你重新设计整个处理链。
最大的认知转变是:不要问"怎么开硬件加速",而要问"我的这条处理链能不能整个留在 GPU 上"。如果答案是"不能"(因为有字幕、有复杂滤镜),那么开硬件加速通常得不偿失。
第二个体会是:硬件加速省的是 CPU,不是时间。它的价值在于"同样的机器能跑更多路",而不是"单个任务更快"。所以在规划容量时应该这么算:
一台 8 核机器:
CPU 转码:8 路 × 4 分钟/个 = 2 个/分钟
GPU 转码:4 路(并发限制)× 1.2 分钟/个 = 3.3 个/分钟
整体吞吐提升 65%(不是 3.2 倍),因为并发数受限制。这个数字才是做容量规划时该用的。
第三:画质是真实的代价。NVENC 在同码率下确实不如 x264。如果你的内容要长期存储、播放量大,多出的 20% 带宽/存储会在几个月后超过省下的算力成本。算总账,别只看单次转码的耗时。
我们最后的分工是:直播和实时转码走 GPU,归档母版和对外交付走 CPU,批量处理按"有没有 CPU-only 滤镜"分流。这个组合看着不"技术先进",但它是实测数据支撑的最优解。