FFmpeg硬件解码报错Cannot allocate memory:动态库路径问题排查与解决
发布时间:2026/10/4 7:01:42 锦皓数字建站

折腾了快一个下午才把这个坑填平记录一下。场景很常见用 FFmpeg 做硬件解码调用av_hwdevice_ctx_create创建 CUDA 设备上下文的时候直接给你甩一句Cannot allocate memory日志里看不到任何更细的提示。这个报错典型的“表面上是内存问题实际上全是套路”排查了半天跟系统内存、显存没半毛钱关系最后发现是动态库运行时路径把 FFmpeg 引到了一条错误的路。这篇文章就把完整的现场、排查思路、根因和解决方案都摊开来说适合所有用 FFmpeg 硬编解码、NVIDIA 硬件转码、或者在自己编译 FFmpeg 时被类似报错卡住的朋友。1. 问题现场与复现步骤1.1 报错时的代码与日志先说代码。业务上需要调用 NVIDIA 的 CUDA 硬件解码器最简化的调用长这样AVBufferRef *hw_device_ctx nullptr; int ret av_hwdevice_ctx_create(hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0); if (ret 0) { av_log(nullptr, AV_LOG_ERROR, Failed to create CUDA device context: %s\n, av_err2str(ret)); return ret; }这段代码单独看没有任何问题av_hwdevice_ctx_create是 FFmpeg 创建硬件设备上下文的入口第三个参数可以指定设备名称比如0表示第一张 GPU传nullptr会让 FFmpeg 自己去找默认设备。结果运行到这一行控制台直接输出[h264_cuvid 0x55f3a1c12840] Cannot allocate memory Failed to create CUDA device context: Cannot allocate memory注意第一行日志实际失败的位置在 FFmpeg 内部打开h264_cuvid解码器的时候而av_hwdevice_ctx_create只是把内部错误原样转了出来。av_err2str(ret)输出的字符串是Cannot allocate memory对应的错误码是AVERROR(ENOMEM)也就是 FFmpeg 在把底层库返回的错误码统一转换成自己的错误码体系时几乎是无脑把 CUDA 的错误映射成了“内存分配失败”。1.2 硬件与软件环境为了让这个 bug 可以复现列一下当时的完整环境项目版本/配置操作系统Ubuntu 22.04 LTS内核 5.15.0-91-genericGPUNVIDIA RTX 3080 10GB显卡驱动535.54.03CUDA Toolkit12.2安装在 /usr/local/cuda-12.2FFmpeg5.1.2手动编译启用了 --enable-cuda --enable-nvenc --enable-ffnvcodec编译工具链gcc 11.4.0CMake 3.24运行方式编译出的二进制可执行程序动态链接 FFmpeg 的 .so这套环境很常见很多做流媒体转码的同学都差不多。但恰恰是这种“看起来很标准”的环境里坑往往藏在路径细节中。1.3 表面症状与真实问题的反差一开始我以为是系统内存不足先用free -h看了一眼total used free shared buff/cache available Mem: 62Gi 18Gi 28Gi 2.1Gi 15Gi 41Gi内存完全够用。又用nvidia-smi看显存| GPU Name Persistence-M | Bus-Id | Util | | NVIDIA GeForce RTX 3080 | 00000000:01:00.0 | 5% | | 10GB / 10GB | | |显存也是空的根本不是显存不足。那问题到底在哪只能层层往里剥。2. 排查过程从内存怀疑到动态库路径2.1 第一步开启 FFmpeg 调试日志遇到 FFmpeg 的报错第一件事就是调高日志级别把底层细节打出来。在初始化代码前加一行av_log_set_level(AV_LOG_DEBUG);重新编译运行日志多了一堆信息但关键的几行还是没给出明确方向只看到创建 CUDA context 失败前后的若干库内部调用。这就像是去医院看病只看外科门诊解决不了必须拍个片子看内部结构。2.2 第二步用 strace 跟踪库文件打开路径既然 FFmpeg 只是简单地把错误码映射成 ENOMEM那真正的失败点可能在它加载的动态库里。用 strace 跟踪一下进程实际打开了哪些文件strace -f -e traceopenat,mmap,close /path/to/your_app 21 | grep libcuda | head -50输出里出现了非常关键的线索openat(AT_FDCWD, /usr/local/cuda-12.2/lib64/libcuda.so.1, O_RDONLY|O_CLOEXEC) 6 openat(AT_FDCWD, /usr/local/cuda-12.2/lib64/libcudart.so.12, O_RDONLY|O_CLOEXEC) 7注意看程序加载的是/usr/local/cuda-12.2/lib64/libcuda.so.1但系统驱动自带的驱动库在/lib/x86_64-linux-gnu/libcuda.so.1。这里有一个很大的疑点FFmpeg 在编译时是通过ffnvcodec头文件来调用 CUDA 的这个头文件只负责跟libcuda.so对接但运行时到底加载哪个路径下的库取决于动态链接器的搜索顺序并不是编译时的路径。2.3 第三步用 ldd 确认依赖加载顺序直接看可执行文件的动态库依赖ldd your_app | grep -E avcodec|avutil|cuda|nvidia输出libavcodec.so.59 /opt/ffmpeg/lib/libavcodec.so.59 libavutil.so.56 /opt/ffmpeg/lib/libavutil.so.56 libcuda.so.1 /usr/local/cuda-12.2/lib64/libcuda.so.1问题已经很明显了你的 FFmpeg 库是自己装在/opt/ffmpeg/lib的但是运行时环境变量LD_LIBRARY_PATH里把/usr/local/cuda-12.2/lib64放在了更靠前的位置。这导致 FFmpeg 在加载依赖时优先从 CUDA Toolkit 的目录里找到了libcuda.so.1而不是系统驱动目录里的那个。一般来说系统驱动目录里的libcuda.so.1是 NVIDIA 驱动安装时同步生成的它的版本与驱动严格匹配。而/usr/local/cuda-12.2/lib64里的libcuda.so.1可能是 CUDA Toolkit 自带的 stub 库或者是后来手动拷贝进去的兼容库。stub 库在真正初始化驱动上下文时会因为内部接口不对、驱动版本不匹配而返回奇怪的错误码FFmpeg 封装后统一变成了Cannot allocate memory。2.4 为什么偏偏映射成 Cannot allocate memory这一步值得多说一点。CUDA 初始化失败时底层错误码可能是CUDA_ERROR_OUT_OF_MEMORY也可能是CUDA_ERROR_DRIVER_VERSION_MISMATCH。FFmpeg 的ffnvcodec封装层处理这些错误码时会做一个粗糙的转换凡是底层表示为“设备上下文创建失败”的错误多数时候会被映射为AVERROR(ENOMEM)。这就产生了极大的误导性让人以为 GPU 内存不够用。从设计上看这其实是 FFmpeg 的一个坑错误码信息粒度太粗。av_hwdevice_ctx_create内部调用链很长涉及设备枚举、上下文创建、驱动初始化任何一个环节失败返回的错误码对外层来说都只是一个数字。如果不借助AV_LOG_DEBUG或者更低层的工具根本看不到真正的失败原点。3. 根因分析与解决步骤3.1 根因多版本动态库共存导致加载了错误的 libcuda.so归根结底这不是 FFmpeg 的算法问题也不是系统内存不足而是动态链接器在解析libcuda.so.1时选错了文件。FFmpeg 的ffnvcodec头文件在编译时会去查找cuda.h并在运行时通过dlsym等方式加载libcuda.so.1。如果运行时找到的是 CUDA Toolkit 里的 stub 库那么这个库只包含编译链接用的符号表并没有完整的驱动后端一旦真正调用cuDevicePrimaryCtxRetain这类函数就会失败。这类问题在以下情况特别容易发生机器上安装了多个 CUDA Toolkit/usr/local/cuda软链接来回切导致libcuda.so.1被覆盖。手动设置了LD_LIBRARY_PATH把 CUDA Toolkit 的lib64目录放在最前面而系统路径排在后面。使用 conda 环境或者虚拟环境时环境里的库目录覆盖了系统库。自定义编译 FFmpeg 时--extra-cflags和--extra-ldflags里写死了/usr/local/cuda/lib64导致 RPATH 里也写入了错误路径。3.2 解决步骤一清理 LD_LIBRARY_PATH 中的冲突路径最直接的办法是去掉环境变量里的 CUDA Toolkit 目录让系统驱动库优先。如果没有特殊需求直接临时清空或重设unset LD_LIBRARY_PATH export LD_LIBRARY_PATH/opt/ffmpeg/lib:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu这里把/opt/ffmpeg/lib放在最前面是因为 FFmpeg 的自定义库路径必须保留否则会找不到 FFmpeg 的动态库。然后重新跑程序如果成功说明问题确认就是路径冲突。3.3 解决步骤二用 patchelf 修改可执行文件的 RPATH/RUNPATH临时用LD_LIBRARY_PATH能解决问题但不适合生产环境。因为你不可能每次启动前都去设置环境变量而且LD_LIBRARY_PATH优先级太高容易影响同进程里的其他库。更稳妥的做法是直接修改可执行文件自身的 RPATH/RUNPATHpatchelf --set-rpath /opt/ffmpeg/lib:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu your_app改完以后用readelf -d your_app确认Dynamic section at offset 0x2a0 contain entries: 0x000000000000001d (RUNPATH) Library runpath: [/opt/ffmpeg/lib:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu]这里要注意RPATH和RUNPATH的优先级顺序。动态链接器的搜索顺序是RPATHDT_RPATH如果存在且没有 DT_RUNPATH则在 LD_LIBRARY_PATH 之前搜索LD_LIBRARY_PATH 环境变量指定的目录RUNPATHDT_RUNPATH在 LD_LIBRARY_PATH 之后搜索/etc/ld.so.cache 中缓存的路径默认系统路径 /lib 和 /usr/lib所以如果你原来是 RPATH 里写死了/usr/local/cuda/lib64靠LD_LIBRARY_PATH不一定能盖过它。用patchelf --set-rpath会同时移除 RPATH 并写入 RUNPATH 吗实际上patchelf --set-rpath设置的是 DT_RUNPATH如果原文件已有 RUNPATH但如果原文件是 DT_RPATH直接--set-rpath会更新 RPATH 字段。稳妥起见可以直接用--remove-rpath再--set-rpathpatchelf --remove-rpath your_app patchelf --set-rpath /opt/ffmpeg/lib:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu your_app这样既清掉了历史包袱也保证了新的运行路径是干净的。3.4 解决步骤三如果还编译失败检查 ffnvcodec 与驱动版本如果路径调整完之后问题依然存在那就要回头检查 FFmpeg 编译时所用的ffnvcodec头文件版本和运行时驱动版本的匹配关系。ffnvcodec是 FFmpeg 与 NVIDIA 视频编解码硬件对接的桥梁它的头文件会声明 CUDA 和 NVDEC/NVENC 的接口版本。驱动太旧而头文件太新可能导致上层调用底层接口时拿到一个预期之外的错误码。我一般用这个命令看当前驱动的支持的 CUDA 版本nvidia-smi | grep CUDA Version输出显示CUDA Version: 12.2表示驱动支持最高 12.2 的 CUDA runtime。但这里说的是libcuda.so的接口兼容性和/usr/local/cuda目录里的 CUDA runtime 是两码事。FFmpeg 只依赖驱动提供的libcuda.so并不依赖 CUDA Toolkit所以只要驱动本身的libcuda.so能被正确加载接口版本匹配就没问题。3.5 验证从报错到硬件解码成功重新编译并运行后日志变成这样[AVHWDeviceContext 0x55ff1d4cb640] Successfully created CUDA context. [AVHWDeviceContext 0x55ff1d4cb640] Using CUDA device: NVIDIA GeForce RTX 3080 [h264_cuvid 0x55ff1d54abc0] Using device 0 (NVIDIA GeForce RTX 3080)能正常输出设备信息说明 CUDA 设备上下文创建成功。再用nvidia-smi看进程显存占用也能看到 FFmpeg 进程已经吃了几十 MB 显存这就是硬件解码器初始化成功的特征。为了确认不是偶然成功我连续跑了十几遍相同的程序每次都稳定通过。这才算真正“已解决”。4. 排查清单与常见坑位4.1 先分清各种“Cannot allocate memory”的变体这个报错太有迷惑性了我总结了几种容易混淆的情况列成一张速查表现象常见原因快速验证方法av_hwdevice_ctx_create返回 Cannot allocate memory但系统内存和显存都充足动态库加载路径错误libcuda.so.1加载了 stub 或旧版本ldd your_app/strace -f -e openat返回 Cannot allocate memory同时free -g显示内存耗尽系统内存不足进程被 OOM 杀掉或 mmap 失败free -h看看有没有其他进程吃内存返回 Cannot allocate memorynvidia-smi显示显存占用 99%显存耗尽比如多个进程占满了显存fuser -v /dev/nvidia*查占用的进程程序在容器内运行报错同时/dev/nvidia*不存在或权限不足Docker 启动时未加--gpus all设备文件缺失ls -l /dev/nvidia*使用--enable-cuda编译的 FFmpeg在av_hwdevice_ctx_create调用前一切正常调用后立刻失败ffnvcodec 头文件版本与驱动不匹配更换匹配的 ffnvcodec header 重新编译这几类问题表象都一样但排查方向完全不同。先说最常踩的两个。4.2 容器场景的独立坑位很多人的程序其实是在 Docker 容器里跑的。如果启动容器时没有加--gpus all那么容器内/dev/nvidia0、/dev/nvidiactl、/dev/nvidia-modeset这些设备文件根本不存在。FFmpeg 初始化 CUDA 上下文时底层尝试访问设备文件失败错误码被抽象成 ENOMEM。这种时候去查LD_LIBRARY_PATH是没用的应该先看设备文件ls -l /dev/nvidia*如果输出是“No such file or directory”那就不是库的问题而是容器启动参数的问题。一定要用类似这样的参数重启容器docker run --gpus all --shm-size8g -it your_image /bin/bash注意--shm-size这个参数也很关键FFmpeg 硬件转码时数据频繁在内存里搬运容器默认的 64MB/dev/shm经常会不够用表现为莫名其妙的内存或 I/O 错误但并不直接显示为这一句。4.3 库路径配置的三个铁律通过这次排查我总结出三条跟动态库路径打交道的经验基本可以写成铁律第一不要在生产环境里依赖LD_LIBRARY_PATH去修复库冲突。这个变量的优先级太高会无差别影响整个进程空间很可能修好了libcuda.so的问题又把libavcodec.so的路径搞坏了。正确的做法是把所有要用到的库目录用patchelf写进可执行文件的 RUNPATH让程序自带一套稳定的运行环境。第二/usr/local/cuda/lib64里的libcuda.so.1很多时候是 stub 库不能用它运行硬件编码。CUDA Toolkit 安装时会在lib64/stubs目录下放一堆 stub 库主要用于编译链接不包含真正驱动实现。如果你把stubs目录或者整个lib64目录加进LD_LIBRARY_PATH运行时就会加载到假的libcuda.so各类奇怪的返回码都会冒出来。正确的做法是运行时让动态链接器去/lib/x86_64-linux-gnu或驱动目录找libcuda.so.1这个文件是 NVIDIA 驱动安装时生成的真实驱动库。第三FFmpeg 自定义编译的库目录要固定不要随便往系统路径塞。很多人编译完 FFmpeg 后随手make install到/usr/local/lib而系统更新或其他软件可能覆盖这个目录下的同名库。我建议编译时直接指定安装前缀./configure --prefix/opt/ffmpeg --enable-cuda --enable-nvenc --enable-ffnvcodec make -j$(nproc) make install然后把/opt/ffmpeg/lib写进可执行文件的 RUNPATH系统路径里的/usr/lib、/lib保持不变。这样既隔离了版本又不会跟系统库打架。4.4 快速定位同类问题的四步模板以后再碰到av_hwdevice_ctx_create或类似硬件设备初始化失败我建议按下面这个流程走能省不少时间先开 FFmpeg debug 日志av_log_set_level(AV_LOG_DEBUG)确认失败点是在 FFmpeg 内部哪个阶段是在设备枚举、上下文创建、还是解码器打开。用ldd your_app查看可执行文件依赖的libcuda.so/libva.so/libvdpau.so到底来自哪个路径如果路径不是预期值优先怀疑环境变量和 RPATH。用strace -f -e openat -p抓实际打开的设备文件和驱动库确认程序运行时访问的是不是正确的/dev/nvidia*。如果步骤 2、3 都正常再回头想内存问题nvidia-smi看显存free -h看系统内存cat /proc/你的pid/limits看虚拟内存限制。这个方法不止适用于 CUDA对 VAAPI、QSV、VDPAU 基本同理。因为 FFmpeg 的硬件抽象层设计思路一致底层错误码映射到上层时都会出现“失真”。5. 最后再分享一个小技巧我后来在代码里给av_hwdevice_ctx_create包了一层封装每次创建设备上下文前主动打印当前进程实际加载的库路径const char* path getenv(LD_LIBRARY_PATH); av_log(nullptr, AV_LOG_INFO, LD_LIBRARY_PATH%s\n, path ? path : (null)); av_log(nullptr, AV_LOG_INFO, Trying to create %s device context.\n, av_hwdevice_get_type_name(type));配合AV_LOG_DEBUG后续再遇到类似问题日志里直接能看到库路径的线索不用每次去 strace。这个方法虽然简单但在多环境部署的场景下真的能救命。希望大家下次遇到这个报错能少走点弯路。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。