0x00 前言

在容器中复用宿主机显卡有诸多好处,比如,只需要在宿主机安装驱动即可便捷传入容器,使得容器部署方便且环境干净;同时,容器部署带来了天然的显卡复用优势,使得多个实例都能够调用显卡。当然,这种方式也有一定的缺陷,会在一定程度上使宿主机环境变重,增加了 Idle 负担。结合需求使用即可。

在虚拟机尝试进行显卡直通失败后,我选择了容器部署的方式。

0x01 参考资料

  1. Enabling GPU passthrough post launch? --discuss.linuxcontainers.org
  2. NVIDIA CUDA Installation Guide for Linux --docs.nvidia.com
  3. Installing the NVIDIA Container Toolkit --docs.nvidia.com

0x02 驱动安装

由于 Nvidia 驱动的特殊性,可能会破坏桌面环境,需要做好备份工作与救援准备。

我的系统环境是:

  • Debian GNU/Linux 12 (bookworm)
  • Incus 6.0.1 (从 APT 的 bookworm-backports 源安装)
  • GPU: Tesla P4

如前所述,我们需要在宿主机安装显卡驱动与 CUDA 工具包。当前 (2024/08/20) 它们的最新版分别是:

  • NVIDIA Driver Version: 560.28.03
  • CUDA Version: 12.6

参考资料 2 中给出了驱动的下载地址:CUDA Toolkit 12.6 Downloads,下载页面中也给出了安装方式,摘录如下。其中,第 4 行的 add-apt-repository 操作依赖 software-properties-common 包,其实这一步我们自己编辑 /etc/apt/sources.list 就可以,确保其中包括 contrib 源即可。

1
2
3
4
5
6
wget https://developer.download.nvidia.com/compute/cuda/12.6.0/local_installers/cuda-repo-debian12-12-6-local_12.6.0-560.28.03-1_amd64.deb
sudo apt install ./cuda-repo-debian12-12-6-local_12.6.0-560.28.03-1_amd64.deb
sudo cp /var/cuda-repo-debian12-12-6-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo add-apt-repository contrib
sudo apt update
sudo apt install cuda-toolkit-12-6

以上仅安装了 cuda-toolkit,我们还需要安装驱动。下载页面同样给出了安装方式,包含两个选项,分别是新的开源内核模块 nvidia-open 和旧的闭源模块 cuda-drivers,按显卡支持情况安装其中之一。由于 Tesla P4 属于较老的 Pascal 架构,新模块尚未支持,因此这里选择后者。

1
sudo apt install cuda-drivers

安装程序会帮助我们禁用默认的 nouveau 驱动。安装完成后,可能需要重启才能够驱动显卡。之后,通过 nvidia-smi 查看显卡驱动状态。

另外,可以依照参考资料 2 中的推荐操作部分,尝试编译运行一些调用 CUDA 的实例,以确保 CUDA 能够正常运行。

最后,我们需要安装 nvidia-container-toolkit。粗略地说,这个工具可以控制容器使用的显卡功能,并将必要的设备信息、工具接口传入容器中,使得我们可以在容器中不额外安装驱动便可以调用显卡,并正常使用 nvidia-smi 等工具。

参考资料 3 中给出了其下载方式与链接,但我在网络良好的情况下仍会出现 Handshake Error,可能是官方源存在一些问题,因此采用 USTC 的镜像

1
2
3
4
5
6
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://mirrors.ustc.edu.cn/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://nvidia.github.io#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://mirrors.ustc.edu.cn#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update
sudo apt install nvidia-container-toolkit

至此,所有驱动与工具均安装完毕,可以开始进行容器的部署。

0x03 容器部署

首先,我们需要获取显卡的 PCI 插槽。以 P4 为例:

1
incus info --resources | grep -in -A5 -B5 -- 'p4'

查询得到的插槽标识是 0000:04:00.0,这个写法是 Incus 要求的标准格式,不接受其他写法。

接着,创建一个容器。其中,nvidia.driver.capabilities 配置项对应着 NVIDIA_DRIVER_CAPABILITIES 变量 (详见 Driver Capabilities),Incus 会将这个值透传给 nvidia-container-toolkit,以配置我们的容器。由于我将利用该显卡进行视频转码,所以 video 值是必要的,注意 Incus 的默认设置utility,compute,并不包含 video,因此默认配置会导致我们无法进行转码。方便地写,也可以选择全都要,配置为 all

1
2
incus create images:debian/12 jelly -c nvidia.runtime=true -c nvidia.driver.capabilities=all
incus config device add jelly tesla-p4 gpu pci="0000:04:00.0"

启动该容器,应当可以在容器内执行 nvidia-smi 并获取显卡驱动状态。

1
watch -n 0.5 nvidia-smi

通过容器内部署 Jellyfin 并进行视频转码来验证显卡可用性,不再赘述。读者可以自行验证,以 ffmpeg 为例:

1
2
3
4
# Example ffmpeg command
ffmpeg -init_hw_device cuda=cu:0 -filter_hw_device cu \
-hwaccel cuda -hwaccel_output_format cuda \
-i "sample.mp4" -c:v hevc_nvenc -f mp4 "out.mp4"

0x04 记录/小插曲

部署后,出现过 ffmpeg 转码失败的现象,提示 Unknown Error... caused by external library 字样。最初,在容器内安装 nvidia-container-toolkit 后问题消失了,而在容器内卸载这个包之后问题也没有再出现,新建容器也没能复现这个问题。在之后的尝试中发现,这个问题往往发生在宿主机重启之后。事实上,在长时间运行的宿主机上,这个问题并不会中途发生,原因大概率是宿主机启动时 LXC 未能成功挂载设备 (比如 /dev/nvidia_uvm)。这可能是不完美的启动顺序导致的,但我没有深入试验。

目前来看,这个问题与容器无关,仅与 Incus daemon 有关,注意仅重启 incus.service 并不能解决问题,需要完全关闭并重新启动 Incus daemon:

1
incus admin shutdown && incus start --all

需要注意的是,在 Incus 尚未完全启动 (如容器正在启动) 时进行 shutdown 可能会导致 Incus 卡住,因此最好不要在刚启动宿主机时操作。

更暴力地,也可以尝试运行几次下面的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash

echo '1/5 Stopping all instances'
incus stop --all

echo '2/5 Stopping Incus daemon and all the container instances'
incus admin shutdown

echo '3/5 Stopping incus.service & Sleep 10s'
sudo systemctl stop incus
sleep 10

echo '4/5 Restarting incus.service & Sleep 10s'
sudo systemctl start incus
sleep 10

echo '5/5 Restarting instances'
incus start --all

echo 'Done'
# incus list