在 Linux 6.6 上使用 Intel DG1 GPU 加速视频编解码

 

最近利用手头的闲暇计算资源攒了个 NAS,运行 TrueNAS 系统,并使用 Jellyfin 作为流媒体播放平台。Jellyfin 会根据客户端(通常是浏览器)的情况(如网络、硬件等)决定是否要对视频进行重新编解码。但即使是分配了 64 核的 7742,也对高码率的 HEVC 10bit 视频力不从心,更不用说 HDR 视频还需要额外做 tone mapping,一套组合拳下来帧率甚至不到个位数。因此,使用 GPU 加速迫在眉睫。

在 Intel 员工 @gaoyichuan 的推荐下,我从闲鱼花 260 元购买了一块刚拆封、还在保的蓝戟代工的 DG1 MAX 显卡。DG1 是 Intel 的第一代独立显卡,基于 Iris Xe (又叫 Gen12)架构。DG1 有几种不同的规格,我买的是 96EU 的版本(因此是 MAX),核心频率 1.5GHz,显存 4GB GDDR6,TDP 30W,PCIe 4.0 x4 接口。虽然卡本身是双宽,但实际上核心板、挡板都是单宽的,上层是一个装着风扇的塑料壳子。考虑到要装在服务器里面,我把风扇和塑料壳都拆了;不过铝制的散热片还是超出了单个 PCIe 的宽度,因此还是占了 riser 的两个槽。闲鱼也有单宽的 72EU 版本,甚至还有被动散热的,价格都差不多(本文书写时在 ¥200 出头),都可以考虑购买。

之所以推荐 DG1,当然是因为它的高性价比。虽然 DG1 的 3D 加速性能和兼容性都并不令人满意,但我也用不到它。但在编码方面,以一个星期的饭钱,买到的卡甚至可以支持 32 路并行的 1080P HEVC 10bit 视频流编码,还有硬件加速 BT.2390 tone mapping,那还要啥自行车?网上有很多具体的性能测试报告,包括和 NVENC / NVDEC 的对比,因此本文不多赘述。在阅读这些报告时,我还发现后续的 Arc GPU 编码性能更上一层楼。或许我两年前买的、天天让我蓝屏、现在已经吃灰的 A380 终于能有一些用武之地了。

虽然卡很容易买到,但我花了整个周末才把它成功在 TrueNAS 24.04 (Linux 6.6.16) 上用起来。踩了无数的坑之后,我决定把它整理下来,以便后人少走弯路。

DG1 Max 的 PID/VID 是 8086:4905,而 DG1 是 8086:4908;如果你使用的是不同的型号,请对应修改下面的命令或者配置。

第 0 步:硬件:ESXi 虚拟机 PCI Passthrough

这一步大部分人并不需要完成,不过我的 TrueNAS 运行在 ESXi 虚拟机上。为了进行设备直通,需要进行以下的配置:

  1. 确认 BIOS 中的 IOMMU、SR-IOV、Above 4G Decoding 等功能均已经打开。
  2. 在 ESXi 的系统配置的 PCI Devices 中找到 GPU (8086:4905),并启用 passthrough。
  3. 设置虚拟机为 EFI 模式、增加 GPU 设备、并在 vmx 文件中增加以下的配置:
pciPassthru.use64bitMMIO=TRUE
pciPassthru.64bitMMIOSizeGB=8

如果无法启动虚拟机电源,则可能还需要增加其他的配置,如 hypervisor.cpuid.v0=FALSE, pciPassthru0.msiEnabled=FALSE 等,也可以尝试更新 ESXi。具体可以查看其他博客,如 DG1 ESXi直通尝试Intel dg1显卡 不完全指北 等。

第 1 步:内核态:移植 i915 backport 驱动到 Linux 6.6

  • 2024/5 更新:经过测试,Linux 6.9 带的 xe 驱动可以支持 DG1,并且在内核、用户态软件都无需进行任何改动。在 TrueNAS 更新到相应版本内核后,这应该是最好地解决方案。
  • 2024/9 更新:六月发布的 backports 驱动已经支持到了 Linux 6.8(backports/main),据反馈可以在 6.6 上编译成功,无需应用我的 patch。

DG1 在内核使用的是 i915 模块驱动。不幸的是,DG1 的支持并没有合并到上游,强制使用 i915.force_probe=4905 很可能无法识别,即使失败了也无法使用硬件解码,甚至可能导致虚拟机初始化失败,卡在奇怪的状态,似乎除了重启物理机无法重置。因此,只能使用 Intel 提供的 intel-gpu-i915-backports OOT 模块

然而,此模块仅对有限的内核版本提供支持,即使是最新的分支 backport/main,也只支持 Ubuntu 22.04 对应的 6.5.x LTS 内核,而 TrueNAS 24.04 使用的是 6.6.x 内核。如果尝试直接把 make i915dkmsdeb-pkg 产生的 DEB 安装到 Linux 6.6 上,只会获得一大堆编译错误。Chiphell 有篇文章进行了一些讨论,但也没有结果。

还好,它们并不难修复,只是比较花时间。我基于 backport/RELEASE_2405_23.10 分支,也就是 I915_23.10.32_PSB_231129.32 这个版本进行了一些快速的 dirty fix,就能成功通过了编译。具体的 patch 上传到了 我的 gist

在 DKMS 成功安装模块后,加载模块时 i915 会抱怨缺少某些固件 blob,此时从 https://github.com/intel-gpu/intel-gpu-firmware 下载对应文件,放置在 /lib/fiwmare/i915 后重新加载模块即可。

在安装模块成功后,应该能看到 /dev/dri/renderD128 出现。如果有多个 GPU,则可能未必是此文件名,下同。

第 2 步:用户态:Intel Media Driver 与 OpenCL ICD

Intel 的用户态驱动(UMD)是 Intel Media Driver,各大发行版均有打包。在 TrueNAS 也就是 Debian 上,包名是 intel-media-va-driver-non-free。同时需要安装的还有(版本对应的)libva2va-utils。在安装后,可以通过 vainfo --display drm --device /dev/dri/renderD128 查看是否能正常识别 DG1(如果使用 VMWare 则一定需要指定单一 device,否则会检测到虚拟显卡而后出错)。

然而在我刚移植完 i915 模块时,这个命令运行时会在加载 Intel iHD driver 后抛出出奇怪的 Segmentation Fault。我一度怀疑是 UMD 与 KMD 有 ABI 不兼容,于是对两者都编译并更换了多个版本进行交叉测试,均无效果。于是我又花了整整一下午进行 debug,发现了问题的源头。具体来说,在 UMD 的硬件检测与初始化阶段,代码会使用 ioctl 向 i915 发出一系列请求,其中有这样一段(来自 mos_bufmgr.c):

if (bufmgr_gem->has_lmem)
{
    mmap_arg.flags = I915_MMAP_OFFSET_FIXED;
}
else
{
    mmap_arg.flags = I915_MMAP_OFFSET_WB;
}
ret = drmIoctl(bufmgr_gem->fd,
        DRM_IOCTL_I915_GEM_MMAP_OFFSET,
        &mmap_arg);

由于 DG1 存在显存,因此使用了 I915_MMAP_OFFSET_FIXED 的 flag。然而在 KMD 的对应处理函数 i915_gem_mmap_offset_ioctl 中,并没有这个 flag。于是 ioctl 失败后,用户态驱动开始进行一些后处理,而 segfault 其实来自于一些写得不好的资源释放逻辑,而不是问题本身。

继续溯源可以发现,这段处理逻辑自从 OOT 驱动的早期版本就存在,从来没有被更改过。但在 upstream 驱动的对应函数中,是存在这个 flag 的。看来这是专属于 backport 驱动的问题,照道理应当一直存在。不知道为什么此前没有这样的问题,可能是 UMD 的逻辑有所变化,但我没有找到到能工作的版本。

那要怎么办呢?直接返回成功而什么都不干是不行的,因为后续 UMD 真的需要进行一些 mmap 操作。但从 upstream 再 backport 能工作的代码显然太负责了,我选择摆烂,进行了一些胡乱修改:

diff --git a/drivers/gpu/drm/i915/gem/i915_gem_mman.c b/drivers/gpu/drm/i915/gem/i915_gem_mman.c
--- a/drivers/gpu/drm/i915/gem/i915_gem_mman.c
+++ b/drivers/gpu/drm/i915/gem/i915_gem_mman.c
@@ -811,6 +811,9 @@ i915_gem_mmap_offset_ioctl(struct drm_device *dev, void *data,
 		type = I915_MMAP_TYPE_UC;
 		break;
 
+	case 4:
+		type = 0;
+		break;
 	default:
 		return -EINVAL;
 	}

简而言之,绕过了失败的分支。很神奇,内核模块经过这样的修改后,用户态驱动就能工作了。

免责声明:我完全不知道这样的修改会造成什么负面作用,或许可能导致 GPU 挂起、内核内存泄漏甚至 PANIC。如果要使用,请自担风险。我不为此负任何责任。

到这里,我们终于可以回到本节的主题——用户态驱动了。正常情况下,运行 vainfo 应该能得到这样的输出:

libva info: VA-API version 1.20.0
libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
libva info: Found init function __vaDriverInit_1_20
libva info: va_openDriver() returns 0
vainfo: VA-API version: 1.20 (libva 2.20.0)
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0 (647708cc)
vainfo: Supported profile and entrypoints
      VAProfileNone                   :	VAEntrypointVideoProc
      VAProfileNone                   :	VAEntrypointStats
      VAProfileMPEG2Simple            :	VAEntrypointVLD
      VAProfileMPEG2Simple            :	VAEntrypointEncSlice
      ......
      VAProfileHEVCMain444_10         :	VAEntrypointVLD
      VAProfileHEVCMain444_10         :	VAEntrypointEncSliceLP
      VAProfileHEVCMain444_12         :	VAEntrypointVLD

这就说明已经成功获得了硬件解码能力。除 Media Driver 提供的 VAAPI 支持外,还需要额外安装 intel-opencl-icd 以提供 OpenCL 支持,它会被用在 tone mapping 上。

第 3 步:Jellyfin 配置与测试

我使用的是 LinuxServer.io 提供的 Jellyfin Docker 镜像。根据 相关说明,只需要将 /dev/dri 设备映射到容器中,它会自动配置相应的权限。如果使用官方镜像,由于许可证原因,则可能还需要额外安装上述提到的软件包。

容器启动后,首先需要额外安装 intel-opencl-icd。而后进入容器,在 /usr/lib/jellyfin-ffmpeg 下有 Jellyfin 使用的 ffmpegvainfo 等支持文件。除了上面的 vainfo 外,还应该按照 Jellyfin 文档 的说明使用 ffmpeg 进行检查。正常的输出应该如下:

root@jellyfin:/# /usr/lib/jellyfin-ffmpeg/ffmpeg -v verbose -init_hw_device vaapi=va:/dev/dri/renderD128 -init_hw_device opencl@va
ffmpeg version 5.1.4-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers
......
[AVHWDeviceContext @ 0x563dc3e8b4c0] libva: VA-API version 1.20.0
[AVHWDeviceContext @ 0x563dc3e8b4c0] libva: Trying to open /usr/lib/jellyfin-ffmpeg/lib/dri/iHD_drv_video.so
[AVHWDeviceContext @ 0x563dc3e8b4c0] libva: Found init function __vaDriverInit_1_20
[AVHWDeviceContext @ 0x563dc3e8b4c0] libva: va_openDriver() returns 0
[AVHWDeviceContext @ 0x563dc3e8b4c0] Initialised VAAPI connection: version 1.20
[AVHWDeviceContext @ 0x563dc3e8b4c0] VAAPI driver: Intel iHD driver for Intel(R) Gen Graphics - 24.1.1 (f5f09c4).
[AVHWDeviceContext @ 0x563dc3e8b4c0] Driver not found in known nonstandard list, using standard behaviour.
[AVHWDeviceContext @ 0x563dc3eb8b00] 0.0: Intel(R) OpenCL HD Graphics / Intel(R) Iris(R) Xe MAX Graphics [0x4905]
[AVHWDeviceContext @ 0x563dc3eb8b00] Intel QSV to OpenCL mapping function found (clCreateFromVA_APIMediaSurfaceINTEL).
[AVHWDeviceContext @ 0x563dc3eb8b00] Intel QSV in OpenCL acquire function found (clEnqueueAcquireVA_APIMediaSurfacesINTEL).
[AVHWDeviceContext @ 0x563dc3eb8b00] Intel QSV in OpenCL release function found (clEnqueueReleaseVA_APIMediaSurfacesINTEL).

测试完成后,在 Jellyfin 的转码设置中可以打开硬件加速,选择 Intel QuickSync 即可(注意不要选 VAAPI,有较大的性能差异)。此时可以打开除 VP8 外的所有硬件解码功能、VPP 色调映射(同时也需要启用色调映射),还可以启用低电压模式(可能需要模块选项 i915.enable_guc=2)。保存设置后打开一个视频,如果一切顺利,应当可以立竿见影地体会到硬件加速的效果。在我的测试下,大范围拖动时间轴的延时几乎只有 2-3 秒,对 80Mbps HEVC 10bit HDR 视频的实时转码帧率可以到 120 FPS 左右。

如果需要检测 GPU 的利用率,可以(在 Docker 外即可)安装 intel-gpu-tools 软件包。它提供了 intel_gpu_top 命令,可以实时查看 GPU 的各个组件的利用率。不过在我的测试中,无论有多少负载,GPU 的 Video 组件利用率都显示得很高,似乎没有太多参考的价值。

结语

经过这一个周末的折腾,当我的视频第一次能顺畅无比地播放时,我终于切身体会到了垃圾佬的快乐:两百块的显卡,两千块的体验,也让我对 Linux 的图形栈有了更深的认识,何乐而不为呢?

乐子

在折腾 i915 模块的过程中,发现它的报错信息很多时候会跟着一句 Please contact your Intel representative.,来源于这些代码于是我听话地找了 @gaoyichuan 很多次,但好像并没有什么用。