Tesla P4 Linux NAS使用指北

0x00: 序
最近在闲鱼收了块Tesla P4给自己的NAS用,这里做下配置记录。宿主机依旧是Archlinux,别的发行版请自己想办法。

主要实现如下能力:
虚拟机GPU直通
容器能使用GPU加速(比如jellyfin的显卡硬解)

0x01: GPU虚拟机直通
参考:https://wiki.archlinux.org/title/PCI_passthrough_via_OVMF
首先进主板的BIOS把IOMMU功能打开,然后在内核参数中添加 iommu=pt ,intel平台还要再加上 intel_iommu=on ,然后重启,在dmesg中看到iommu相关就算成了。
如何配置内核启动参数请看这:https://wiki.archlinux.org/title/Kernel_parameters

我是用virt-manger + libvirt + qemu + kvm来实现和管理虚拟机,安装的包如下
$ yay -Qqe | grep -e virt -e qemu -e ovmf
edk2-ovmf
libvirt
qemu-emulators-full
qemu-full
virt-manager
virtio-win
以上的玩意装好后打开virt-manger,新建虚拟机时候记得固件一定选择UEFI。然后先正常的装个windows,装好virtio的驱动,开启自带的远程桌面后关机。然后再添加一个PCI主机设备选择的你的显卡,然后进win装好驱动即可(驱动下载,自备梯子:https://cloud.google.com/compute/docs/gpus/grid-drivers-table?hl=zh-cn#windows_drivers)。之后你就可以把显卡选为none,删掉显示器,之后只从rdp来访问虚拟机了。


0x02:GPU容器
参考:https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html
安装如下的包:
nvidia-docker
nvidia-dkms (这个驱动貌似不装也行?)
装nvidia-docker的时候可能会提示你/etc/docker/daemon.json已经存在,你可以先把之前的daemon.json备份下删掉,在装好这个包后在覆盖回来,然后记得执行 sudo nvidia-ctk runtime configure --runtime=docker 在daemon.json中添加nvidia的runtime即可。然后重启docker,执行 docker run --rm --runtime=nvidia --gpus all nvidia/cuda:11.6.2-base-ubuntu20.04 nvidia-smi 能看到GPU信息就算成功。
$ docker run --rm --runtime=nvidia --gpus all nvidia/cuda:11.6.2-base-ubuntu20.04 nvidia-smi
Sun Apr 23 01:03:51 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 530.41.03 Driver Version: 530.41.03 CUDA Version: 12.1 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 Tesla P4 Off| 00000000:04:00.0 Off | 0 |
| N/A 44C P8 7W / 75W| 0MiB / 7680MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| No running processes found |
+---------------------------------------------------------------------------------------+

0x03: Jellyfin容器硬解
以jellyfin为例来看看如何部署一个能使用GPU的容器,直接上docker-compose.yaml
version: '3.5'
services:
jellyfin:
# 这里选择nyanmisaka魔改的版本,官方镜像也是一样的
image: nyanmisaka/jellyfin:230414-amd64
restart: always
container_name: jellyfin
user: 1000:995
volumes:
# 根据个人习惯把/config和/cache目录挂载出去,jellyfin的配置存储在这里
- /home/chigusa/.jellyfin/config:/config
- /home/chigusa/.jellyfin/cache:/cache
environment: # 这俩玩意记得加上,要不找不到GPU
- NVIDIA_DRIVER_CAPABILITIES=all
- NVIDIA_VISIBLE_DEVICES=all
labels:
# 这个是我个人用来区分哪个容器用到GPU的,会和下文的脚本配合,不是必须
gpu: true
# runtime 和 deploy 直接照nv官方文档抄过来
runtime: nvidia
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
然后进jellyfin中在 控制台 - 播放 中硬件加速选择Nvidia NVENC就成了




0x04: GPU容器和虚拟机和谐共处
当有容器在使用GPU的时候,开启GPU直通的虚拟机是没法启动的,你需要先把容器停掉在启动虚拟机。每次都要人肉在虚拟机开启前关掉GPU容器、关机后再打开有点脑残,所以这里用libvirt的hook机制来做这个事情。你需要为所有使用GPU的容器打一个 gpu=true 的label(看上边jellyfin的例子),然后把下面这个脚本放到 /etc/libvirt/hooks/qemu ,赋予执行权限。

#!/usr/bin/env python3
import sys
from functools import partial
import subprocess
import json
_hooks = []
def hook(fn=None, domain: str = None, action: str = None, stage: str = None):
if fn is None:
return partial(hook, domain=domain, action=action, stage=stage)
_hooks.append((domain, action, stage, fn))
class DockerUtils:
@staticmethod
def is_docker_running():
'''
用 systemd 来判断 docker 是否在运行
'''
p = subprocess.run(
['systemctl', 'is-active', 'docker.service'], capture_output=True)
if p.returncode != 0:
return False
return p.stdout.decode().strip() == 'active'
@staticmethod
def get_gpu_containers(**kwargs):
'''
获取所有打了 gpu=true 的容器的信息
'''
def __filter(data):
for k, w in kwargs.items():
if k in data and data[k] != w:
return False
return True
p = subprocess.run(
['docker', 'container', 'ls',
'-f', 'label=gpu=true',
'--format=json', '--all',
'--no-trunc'
], capture_output=True, check=True)
for item in p.stdout.decode().splitlines():
data = json.loads(item)
if kwargs and not __filter(data):
continue
yield data
# 注意把domain改成你虚拟机的名字,支持数组
@hook(domain='win11', action='prepare', stage='begin')
def win11_prepaer_begin():
'''
在虚拟机启动前执行,停掉所有打了 gpu=true 的容器
'''
if not DockerUtils.is_docker_running():
return
for item in DockerUtils.get_gpu_containers(State='running'):
subprocess.run(['docker', 'stop', item['ID']])
# 注意把domain改成你虚拟机的名字,支持数组
@hook(domain=['win11'], action='release', stage='end')
def win11_release_end():
'''
在虚拟机关机后执行,启动所有打了 gpu=true 的容器
'''
if not DockerUtils.is_docker_running():
return
for item in DockerUtils.get_gpu_containers(State='exited'):
subprocess.run(['docker', 'start', item['ID']])
def main():
'''
libvirt 会传入的参数如下:
/etc/libvirt/hooks/qemu guest_name prepare begin -
其中 guest_name 是你虚拟机的名字,后两个参数的意义参考libvirt的官方文档:
https://libvirt.org/hooks.html#etc-libvirt-hooks-qemu
'''
if len(sys.argv) != 5:
return
domain, action, stage = sys.argv[1:4]
for _hook in _hooks:
if domain == _hook[0] if not isinstance(_hook[0], list) else domain in _hook[0] \
and action == _hook[1] and stage == _hook[2]:
_hook[3]()
if __name__ == '__main__':
main()

0x04: Prometheus + Grafana GPU监控
dcgm文档:https://docs.nvidia.com/datacenter/dcgm/latest/contents.html
grafana的板子:https://grafana.com/grafana/dashboards/12239-nvidia-dcgm-exporter-dashboard/
在容器中部署,同样打 gpu=true 的标,方便上边的hook识别,最终效果如下
version: "3"
services:
dcgm-exporter:
labels:
gpu: true
environment:
- NVIDIA_DRIVER_CAPABILITIES=all
- NVIDIA_VISIBLE_DEVICES=all
restart: always
image: nvcr.io/nvidia/k8s/dcgm-exporter:2.1.4-2.3.1-ubuntu20.04
runtime: nvidia
cap_add:
- SYS_ADMIN
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
