NVIDIA GPU Operator分析一:NVIDIA驱动安装

背景

我们知道,如果在Kubernetes中支持GPU设备调度,需要做如下的工作:

  • 节点上安装nvidia驱动
  • 节点上安装nvidia-docker
  • 集群部署gpu device plugin,用于为调度到该节点的pod分配GPU设备。

除此之外,如果你需要监控集群GPU资源使用情况,你可能还需要安装DCCM exporter结合Prometheus输出GPU资源监控信息。

要安装和管理这么多的组件,对于运维人员来说压力不小。基于此,NVIDIA开源了一款叫NVIDIA GPU Operator的工具,该工具基于Operator Framework实现,用于自动化管理上面我们提到的这些组件。

NVIDIA GPU Operator有以下的组件构成:

  • 安装nvidia driver的组件
  • 安装nvidia container toolkit的组件
  • 安装nvidia devcie plugin的组件
  • 安装nvidia dcgm exporter组件
  • 安装gpu feature discovery组件

本系列文章不打算一上来就开始讲NVIDIA GPU Operator,而是先把各个组件的安装详细的分析一下,然后手动安装这些组件,最后再来分析NVIDIA GPU Operator就比较简单了。

在本篇文章中,我们将详细介绍NVIDIA GPU Operator安装NVIDIA驱动的原理——基于容器安装NVIDIA驱动。

基于容器安装NVIDIA GPU驱动

大多数时候,运维人员都是直接将NVIDIA驱动直接安装在GPU节点上,但是这会有如下的一些缺点:

  • 驱动程序安装容易出错。
  • 驱动的安装方式与操作系统类型相关,不同的操作系统安装驱动不同。
  • 不可移植性。
  • 难以大规模部署或在容器平台上部署(例如:Kubernetes、Openshift等)。

基于此,NVIDIA官方提供了一种基于容器安装NVIDIA驱动的方式,而且NVIDIA GPU Operator安装nvidia驱动也是采用的这种方式。基于容器安装NVIDIA驱动有如下的一些优点:

  • 速度快(一般只要几秒就能安装完成)。
  • 安全性(安全启动,受信任启动,内核锁定)。
  • 便携性(安装脚本被打包成镜像,便于分发和移植)。
  • 易于使用(安装/卸载驱动=启动/停止容器)。
  • 重现性(驱动程序受限制,防止sysadmin错误操作)。

当NVIDIA驱动基于容器化安装后,整个架构将演变成图中描述的样子。

NVIDIA GPU Operator分析一:NVIDIA驱动安装

基于容器安装NVIDIA驱动的整个流程可以分为如下的两个部分:

  • 镜像构建阶段
  • 容器启动阶段

镜像构建阶段

要想了解在构建nvidia驱动镜像过程中都有哪些操作,可以阅读构建镜像的Dockerfile,NVIDIA官方将这些Dockerfile存放在Gitlab上,我们以https://gitlab.com/nvidia/container-images/driver/-/blob/master/centos7/Dockerfile为例进行说明。

1.环境准备

首先是准备环境,环境准备的主要是安装一些工具,比如curl、gcc等。

FROM nvidia/cuda:11.2.1-base-centos7

RUN yum install -y \
        ca-certificates \
        curl \
        gcc \
        glibc.i686 \
        make \
        kmod && \
    rm -rf /var/cache/yum/*

RUN curl -fsSL -o /usr/local/bin/donkey https://github.com/3XX0/donkey/releases/download/v1.1.0/donkey && \
    curl -fsSL -o /usr/local/bin/extract-vmlinux https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux && \
    chmod +x /usr/local/bin/donkey /usr/local/bin/extract-vmlinux

2.安装用户空间使用的工具

当环境准备好以后,接下来主要做如下几件事:

  • 下载NVIDIA驱动文件(以.run结尾),驱动版本可以在执行docker build时通过--build-arg DRIVER_VERSION=<驱动版本>指定。
  • 执行nvidia-installer(由NVIDIA驱动文件提供)安装用户空间使用的工具(例如:nvidia-smi、nvidia-uninstall、nvidia-settings等工具)到镜像的/usr/bin下。
  • 将需要编译内核模块有关的文件存放在/usr/src/nvidia-$DRIVER_VERSION这个目录下,后面编译和安装内核模块时需要用到这些文件。
#ARG BASE_URL=http://us.download.nvidia.com/XFree86/Linux-x86_64
ARG BASE_URL=https://us.download.nvidia.com/tesla
ARG DRIVER_VERSION
ENV DRIVER_VERSION=$DRIVER_VERSION

# Install the userspace components and copy the kernel module sources.
RUN cd /tmp && \
    curl -fSsl -O $BASE_URL/$DRIVER_VERSION/NVIDIA-Linux-x86_64-$DRIVER_VERSION.run && \
    sh NVIDIA-Linux-x86_64-$DRIVER_VERSION.run -x && \
    cd NVIDIA-Linux-x86_64-$DRIVER_VERSION* && \
    ./nvidia-installer --silent \
                       --no-kernel-module \
                       --install-compat32-libs \
                       --no-nouveau-check \
                       --no-nvidia-modprobe \
                       --no-rpms \
                       --no-backup \
                       --no-check-for-alternate-installs \
                       --no-libglx-indirect \
                       --no-install-libglvnd \
                       --x-prefix=/tmp/null \
                       --x-module-path=/tmp/null \
                       --x-library-path=/tmp/null \
                       --x-sysconfig-path=/tmp/null && \
    mkdir -p /usr/src/nvidia-$DRIVER_VERSION && \
    mv LICENSE mkprecompiled kernel /usr/src/nvidia-$DRIVER_VERSION && \
    sed '9,${/^\(kernel\|LICENSE\)/!d}' .manifest > /usr/src/nvidia-$DRIVER_VERSION/.manifest && \
    rm -rf /tmp/*

3.编译驱动内核模块

这一步主要操作是编译NVIDIA驱动的内核模块并且生成一个包,这个包将会在容器启动时,执行nvidia-installer安装驱动内核模块时用到。

在构建镜像过程中,KERNEL_VERSION可以通过docker build --build-arg  KERNEL_VERSION=<kernel版本>,而且可以指定多个内核版本(通过逗号分隔),然后在Dockerfile中通过一个for循环为每个一个内核版本编译一个NVIDIA驱动内核模块包,供容器启动时nvidia-installer使用。

ARG KERNEL_VERSION=latest

# Compile the kernel modules and generate precompiled packages for use by the nvidia-installer.
RUN yum makecache -y && \
    for version in $(echo $KERNEL_VERSION | tr ',' ' '); do \
        nvidia-driver update -k $version -t builtin ${PRIVATE_KEY:+"-s ${PRIVATE_KEY}"}; \
    done && \
    rm -rf /var/cache/yum/*

针对每一个内核版本,执行了“nvidia-driver update”这条命令,nvidia-driver这个脚本的内容,可以从https://gitlab.com/nvidia/container-images/driver/-/blob/master/centos7/nvidia-driver查看。

“nvidia-driver update”执行的是update()这个函数,函数内容如下:

update() {
    exec 3>&2
    if exec 2> /dev/null 4< ${PID_FILE}; then
        if ! flock -n 4 && read pid <&4 && kill -0 "${pid}"; then
            exec > >(tee -a "/proc/${pid}/fd/1")
            exec 2> >(tee -a "/proc/${pid}/fd/2" >&3)
        else
            exec 2>&3
        fi
        exec 4>&-
    fi
    exec 3>&-

    echo -e "\n========== NVIDIA Software Updater ==========\n"
    echo -e "Starting update of NVIDIA driver version ${DRIVER_VERSION} for Linux kernel version ${KERNEL_VERSION}\n"

    trap "echo 'Caught signal'; exit 1" HUP INT QUIT PIPE TERM
    
    # 1.更新yum源的cache,便于使用yum安装kernel-headers、kernel-devels等包。
    _update_package_cache
    # 2.用于重置KERNEL_VERSION这个环境变量
    _resolve_kernel_version || exit 1
    # 3.用于安装kernel-headers,kernel-devel和kernel三个包
    _install_prerequisites
    # 4.如果检测到/usr/src/nvidia-$DRIVER_VERSION/kernel/precompiled目录下存在对于某个内核版本(由KERNEL_VERSION指定)
    # 已经编译好的包,那么就不编译针对该内核版本所需的驱动内核包;如果没有找到,
    # 调用_create_driver_package构建针对该内核版本所需的驱动内核包,
    # 并存放在/usr/src/nvidia-$DRIVER_VERSION/kernel/precompiled目录下。
    if _kernel_requires_package; then
        _create_driver_package
    fi
    # 5.移除kernel-headers,kernel-devel包
    _remove_prerequisites
    # 6.清除yum源缓存
    _cleanup_package_cache

    echo "Done"
    exit 0
}

从_update_package_cache开始,该函数执行的逻辑如下:

  • _update_package_cache用于更新yum源的cache,便于使用yum安装kernel-headers、kernel-devels等包。
  • _resolve_kernel_version用于重置KERNEL_VERSION这个环境变量。
  • _install_prerequisites主要用于安装kernel-headers,kernel-devel和kernel三个包。
  • 如果检测到/usr/src/nvidia-$DRIVER_VERSION/kernel/precompiled目录下存在对于某个内核版本(由KERNEL_VERSION指定)已经编译好的包,那么就不编译针对该内核版本所需的驱动内核包;如果没有找到,调用_create_driver_package构建针对该内核版本所需的驱动内核包,并存放在/usr/src/nvidia-$DRIVER_VERSION/kernel/precompiled目录下。
  • _remove_prerequisites用于移除kernel-headers,kernel-devel包
  • _cleanup_package_cache用于清除yum源缓存。

下面将重点分析如下三个函数:

  • _resolve_kernel_version
  • _install_prerequisites
  • _create_driver_package

1. _resolve_kernel_version分析

_resolve_kernel_version作用是重新设置KERNEL_VERSION这个环境变量,KERNEL_VERSION这个环境变量非常重要,它的值的设置逻辑为:

  • 默认的值是执行uname -r的结果。
  • 如果在执行nvidia-driver update时指定了-k选项,那么-k选项的值会赋值给KERNEL_VERSION。
  • _resolve_kernel_version函数检查KERNEL_VERSION是否有效并重新赋值。

_resolve_kernel_version函数内容如下:

# Resolve the kernel version to the form major.minor.patch-revision.
# Detect the kernel version from 'yum list' command and set the env KERNEL_VERSION
_resolve_kernel_version() {
    local version=$(yum -q list available --show-duplicates kernel-headers |
      awk -v arch=$(uname -m) 'NR>1 {print $2"."arch}' | tac | grep -E -m1 "^${KERNEL_VERSION/latest/.*}")

    echo "Resolving Linux kernel version..."
    if [ -z "${version}" ]; then
        echo "Could not resolve Linux kernel version" >&2
        return 1
    fi
    KERNEL_VERSION="${version}"
    echo "Proceeding with Linux kernel version ${KERNEL_VERSION}"
    return 0
}

上述代码中比较难以理解的是第一条命令,也就是“local version=...”这条命令,这一条命令做了如下的事:

  • 使用yum list获取可用kernel-headers包版本,像下面这样:
kernel-headers.x86_64  3.10.0-1160.el7        base
kernel-headers.x86_64  3.10.0-1160.el7        updates
kernel-headers.x86_64  3.10.0-1160.2.1.el7    updates
kernel-headers.x86_64  3.10.0-1160.2.2.el7    updates
kernel-headers.x86_64  3.10.0-1160.6.1.el7    updates
kernel-headers.x86_64  3.10.0-1160.11.1.el7   updates
  • 获取中间的kernel一列,并降序排序:
3.10.0-1160.11.1.el7.x86_64
3.10.0-1160.6.1.el7.x86_64
3.10.0-1160.2.2.el7.x86_64
3.10.0-1160.2.1.el7.x86_64
3.10.0-1160.el7.x86_64
3.10.0-1160.el7.x86_64
  • 使用grep按如下逻辑匹配一个内核版本:
  • 如果$KERNEL_VERSION不为空,那么返回匹配$KERNEL_VERSION的结果(有可能匹配成功,也有可能匹配不成功)。
  • 如果上面条件不满足,那么匹配带有latest关键字的内核版本,如果匹配成功,那么返回结果。
  • 如果上面条件不满足,那么匹配第一条内核版本,即3.10.0-1160.11.1.el7.x86_64。
  • 将匹配到的内核版本赋值给version这个变量。

然后检查$version这个值是否为空,如果为空,那么直接报错退出;如果不为空那么将$version的值赋值给KERNEL_VERSION。

2. _install_prerequisites分析

_install_prerequisites主要是安装如下两个包(KERNEL_VERSION为具体的内核版本):

  • kernel-headers-${KERNEL_VERSION}
  • kernel-devel-${KERNEL_VERSION}
  • kernel-${KERNEL_VERSION}

为什么会对这个函数加以说明,先看看这三个包的安装方式:

    yum -q -y install kernel-headers-${KERNEL_VERSION} kernel-devel-${KERNEL_VERSION} > /dev/null
    # link the kernel files which are prebuilded
    ln -s /usr/src/kernels/${KERNEL_VERSION} /lib/modules/${KERNEL_VERSION}/build

    echo "Installing Linux kernel module files..."
    curl -fsSL $(repoquery --location kernel-${KERNEL_VERSION}) | rpm2cpio | cpio -idm --quie

可以看到kernel-headers和kernel-devel是用yum安装的,kernel是直接下载rpm包,然后用rpm2cpio和cpio处理安装的。如果指定的内核版本的三个包没有在yum源中出现,安装就会出错,此时应该怎么处理呢?可以先将三个包下载,在制作docker镜像传入镜像中,然后使用"yum localinstall"安装kernel-devel和kernel-headers,使用

“rpm2cpio <PATH_TO_KERNEL_RPM> | cpio -idm --quie”替换原有的命令。

3._create_driver_package分析

_create_driver_package函数用于编译运行驱动所需的内核模块并对这些内核模块执行签名操作,最后利用这些内核模块生成一个名为nvidia-modules-${KERNEL_VERSION}-buildin的文件,在执行nvidia-installer时需要用到该文件。

# Compile the kernel modules, optionally sign them, and generate a precompiled package for use by the nvidia-installer.
_create_driver_package() (
    local pkg_name="nvidia-modules-${KERNEL_VERSION%%-*}${PACKAGE_TAG:+-${PACKAGE_TAG}}"
    local nvidia_sign_args=""
    local nvidia_modeset_sign_args=""
    local nvidia_uvm_sign_args=""

    trap "make -s -j SYSSRC=/lib/modules/${KERNEL_VERSION}/build clean > /dev/null" EXIT

    echo "Compiling NVIDIA driver kernel modules..."
    cd /usr/src/nvidia-${DRIVER_VERSION}/kernel

    export IGNORE_CC_MISMATCH=1
    make -s -j SYSSRC=/lib/modules/${KERNEL_VERSION}/build nv-linux.o nv-modeset-linux.o > /dev/null

    echo "Relinking NVIDIA driver kernel modules..."
    rm -f nvidia.ko nvidia-modeset.ko
    ld -d -r -o nvidia.ko ./nv-linux.o ./nvidia/nv-kernel.o_binary
    ld -d -r -o nvidia-modeset.ko ./nv-modeset-linux.o ./nvidia-modeset/nv-modeset-kernel.o_binary

    if [ -n "${PRIVATE_KEY}" ]; then
        echo "Signing NVIDIA driver kernel modules..."
        donkey get ${PRIVATE_KEY} sh -c "PATH=${PATH}:/usr/src/linux-headers-${KERNEL_VERSION}/scripts && \
          sign-file sha512 \$DONKEY_FILE pubkey.x509 nvidia.ko nvidia.ko.sign &&                          \
          sign-file sha512 \$DONKEY_FILE pubkey.x509 nvidia-modeset.ko nvidia-modeset.ko.sign &&          \
          sign-file sha512 \$DONKEY_FILE pubkey.x509 nvidia-uvm.ko"
        nvidia_sign_args="--linked-module nvidia.ko --signed-module nvidia.ko.sign"
        nvidia_modeset_sign_args="--linked-module nvidia-modeset.ko --signed-module nvidia-modeset.ko.sign"
        nvidia_uvm_sign_args="--signed"
    fi

    echo "Building NVIDIA driver package ${pkg_name}..."
    ../mkprecompiled --pack ${pkg_name} --description ${KERNEL_VERSION}                              \
                                        --proc-mount-point /lib/modules/${KERNEL_VERSION}/proc       \
                                        --driver-version ${DRIVER_VERSION}                           \
                                        --kernel-interface nv-linux.o                                \
                                        --linked-module-name nvidia.ko                               \
                                        --core-object-name nvidia/nv-kernel.o_binary                 \
                                        ${nvidia_sign_args}                                          \
                                        --target-directory .                                         \
                                        --kernel-interface nv-modeset-linux.o                        \
                                        --linked-module-name nvidia-modeset.ko                       \
                                        --core-object-name nvidia-modeset/nv-modeset-kernel.o_binary \
                                        ${nvidia_modeset_sign_args}                                  \
                                        --target-directory .                                         \
                                        --kernel-module nvidia-uvm.ko                                \
                                        ${nvidia_uvm_sign_args}                                      \
                                        --target-directory .
    mkdir -p precompiled
    mv ${pkg_name} precompiled
)

容器启动阶段

在容器启动阶段,主要运行“nvidia-driver init”命令,该命令会调用nvidia-driver脚本中的init()函数,其内容如下:

init() {
    echo -e "\n========== NVIDIA Software Installer ==========\n"
    echo -e "Starting installation of NVIDIA driver version ${DRIVER_VERSION} for Linux kernel version ${KERNEL_VERSION}\n"

    exec 3> ${PID_FILE}
    if ! flock -n 3; then
        echo "An instance of the NVIDIA driver is already running, aborting"
        exit 1
    fi
    echo $$ >&3

    trap "echo 'Caught signal'; exit 1" HUP INT QUIT PIPE TERM
    trap "_shutdown" EXIT

    # 1.检查驱动是否已经挂载,如果已经挂载,那么需要卸载它
    _unload_driver || exit 1
    # 2.检查驱动的rootfs是否已经mount,如果已经mount,那么需要卸载驱动的rootfs
    _unmount_rootfs
    # 3.如果当前内核版本所需的驱动内核包不存在,那么需要编译该驱动内核包,编译的步骤与update()函数
    # 相同
    if _kernel_requires_package; then
        _update_package_cache
        _resolve_kernel_version || exit 1
        _install_prerequisites
        _create_driver_package
        _remove_prerequisites
        _cleanup_package_cache
    fi
    # 4.安装驱动
    _install_driver
    # 5.加载驱动
    _load_driver
    # 6.挂载驱动rootfs
    _mount_rootfs
    # 7.生成一个用于升级驱动的钩子脚本
    _write_kernel_update_hook

    echo "Done, now waiting for signal"
    sleep infinity &
    trap "echo 'Caught signal'; _shutdown && { kill $!; exit 0; }" HUP INT QUIT PIPE TERM
    trap - EXIT
    while true; do wait $! || continue; done
    exit 0
}

从 _unload_driver函数开始,函数的执行逻辑如下:

  •  _unload_driver:如果节点已经加载了驱动(可能由其他容器加载的),那么首先应该卸载驱动。
  • _unmount_rootfs:卸载/var/run下驱动的rootfs。
  • 检查/usr/src/nvidia-${DRIVER_VERSION}/kernel/precompiled是否有针对当前KERNEL_VERSION(由uname -r命令提供)已经编译好的驱动内核模块包,如果没有,执行编译操作,生成适合当前内核版本的驱动内核模块包。
  • 安装驱动所需的内核模块,这些内核模块由之前预先编译好的内核模块包提供。
  • 加载驱动。
  • 挂载驱动的rootfs到/var/run下。
  • 生成一个用于升级驱动的钩子脚本。
  • 设置一个陷阱,用户捕获当前进程退出时需要做哪些事情(卸载驱动、卸载/var/run下驱动的rootfs)。

_unload_driver函数分析

_unload_driver函数主要用于卸载驱动,每次加载驱动前先执行一个卸载操作。卸载操作的主要执行逻辑如下:

  • 如果/var/run/nvidia-persistenced/nvidia-persistenced.pid文件存在,那么读取文件中的进程号,然后使用kill命令杀死进程。执行kill操作后,在1秒内检查10次该进程是否存在,如果不存在,那么该进程已被杀死,可以执行后续操作;如果1秒后,该进程还存在,那么返回错误。
  • 检查是否有应用程序正在使用驱动,如果有应用程序正在使用驱动,那么返回错误,否则卸载驱动内核模块。
# Stop persistenced and unload the kernel modules if they are currently loaded.
_unload_driver() {
    local rmmod_args=()
    local nvidia_deps=0
    local nvidia_refs=0
    local nvidia_uvm_refs=0
    local nvidia_modeset_refs=0

    echo "Stopping NVIDIA persistence daemon..."
    # 如果/var/run/nvidia-persistenced/nvidia-persistenced.pid存在
    if [ -f /var/run/nvidia-persistenced/nvidia-persistenced.pid ]; then
        # 读取文件中进程号
        local pid=$(< /var/run/nvidia-persistenced/nvidia-persistenced.pid)
        # 杀死进程
        kill -SIGTERM "${pid}"
        # 检查10次,每次检查如果未发现被kill的进程,那么直接退出for循环,超时时间为1秒
        for i in $(seq 1 10); do
            kill -0 "${pid}" 2> /dev/null || break
            sleep 0.1
        done
        # 如果1秒后进程仍然存在,那么返回错误
        if [ $i -eq 10 ]; then
            echo "Could not stop NVIDIA persistence daemon" >&2
            return 1
        fi
    fi
    # 卸载驱动内核模块
    # 检查使用有应用程序正在使用驱动
    echo "Unloading NVIDIA driver kernel modules..."
    if [ -f /sys/module/nvidia_modeset/refcnt ]; then
        nvidia_modeset_refs=$(< /sys/module/nvidia_modeset/refcnt)
        rmmod_args+=("nvidia-modeset")
        ((++nvidia_deps))
    fi
    if [ -f /sys/module/nvidia_uvm/refcnt ]; then
        nvidia_uvm_refs=$(< /sys/module/nvidia_uvm/refcnt)
        rmmod_args+=("nvidia-uvm")
        ((++nvidia_deps))
    fi
    if [ -f /sys/module/nvidia/refcnt ]; then
        nvidia_refs=$(< /sys/module/nvidia/refcnt)
        rmmod_args+=("nvidia")
    fi
    # 如果以下任意一个条件满足,说明有应用程序正在使用驱动:
    # 1.变量nvidia_refs的值大于变量nvidia_deps
    # 2.变量nvidia_uvm_refs大于0
    # 3.变量nvidia_modeset_refs大于0
    if [ ${nvidia_refs} -gt ${nvidia_deps} ] || [ ${nvidia_uvm_refs} -gt 0 ] || [ ${nvidia_modeset_refs} -gt 0 ]; then
        echo "Could not unload NVIDIA driver kernel modules, driver is in use" >&2
        return 1
    fi
    #
    if [ ${#rmmod_args[@]} -gt 0 ]; then
        rmmod ${rmmod_args[@]}
    fi
    return 0
}

_unmount_rootfs函数分析

_unmount_rootfs函数用于卸载驱动的rootfs,函数逻辑比较简单。

# Unmount the driver rootfs from the run directory.
_unmount_rootfs() {
    echo "Unmounting NVIDIA driver rootfs..."
    # 驱动的rootfs路径为/run/nvidia/driver,如果发现驱动的rootfs已经挂载,就卸载/run/nvidia/driver目录
    if findmnt -r -o TARGET | grep "${RUN_DIR}/driver" > /dev/null; then
        umount -l -R ${RUN_DIR}/driver
    fi
}

_install_driver函数分析

_install_driver主要是使用nvidia-installer安装nvidia驱动内核模块,此函数依赖_create_driver_package函数编译生成的内核驱动包。

_install_driver() {
    local install_args=()

    echo "Installing NVIDIA driver kernel modules..."
    cd /usr/src/nvidia-${DRIVER_VERSION}
    rm -rf /lib/modules/${KERNEL_VERSION}/video

    if [ "${ACCEPT_LICENSE}" = "yes" ]; then
        install_args+=("--accept-license")
    fi
    nvidia-installer --kernel-module-only --no-drm --ui=none --no-nouveau-check ${install_args[@]+"${install_args[@]}"}
    # May need to add no-cc-check for Rhel, otherwise it complains about cc missing in path
    # /proc/version and lib/modules/KERNEL_VERSION/proc are different, by default installer looks at /proc/ so, added the proc-mount-point
    # TODO: remove the -a flag. its not needed. in the new driver version, license-acceptance is implicit
    #nvidia-installer --kernel-module-only --no-drm --ui=none --no-nouveau-check --no-cc-version-check --proc-mount-point /lib/modules/${KERNEL_VERSION}/proc ${install_args[@]+"${install_args[@]}"}
}

_load_driver函数分析

_load_driver用于加载nvidia驱动,主要有两步:

  • 加载驱动所需的内核模块。
  • 以persistence mode启动驱动。
# Load the kernel modules and start persistenced.
_load_driver() {
    echo "Loading IPMI kernel module..."
    modprobe ipmi_msghandler

    echo "Loading NVIDIA driver kernel modules..."
    modprobe -a nvidia nvidia-uvm nvidia-modeset

    echo "Starting NVIDIA persistence daemon..."
    nvidia-persistenced --persistence-mode
}

_mount_rootfs函数分析

_mount_rootfs用于挂载驱动的rootfs。

# Mount the driver rootfs into the run directory with the exception of sysfs.
_mount_rootfs() {
    echo "Mounting NVIDIA driver rootfs..."
    # 递归地将整个子树标记为不可绑定
    mount --make-runbindable /sys
    # 递归的将子树标记为私有
    mount --make-private /sys
    # 如果目录/run/nvidia/drvier不存在,那么创建该目录
    mkdir -p ${RUN_DIR}/driver
    # 将容器的根目录挂载到/run/nvidia/driver目录下,以实现对/run/nvidia/driver目录的操作
    # 等同于对根目录操作
    mount --rbind / ${RUN_DIR}/driver
}

除此之外宿主机的/run/nvidia目录也将会挂载到容器的/run/nvidia目录,并且挂载时会开启选项“mountPropagation: Bidirectional”,表示容器内操作/run/nvidia目录后,如果其他容器也挂载了该目录,它们对该目录的修改可见(之所以要这么配置,因为后面介绍的其他组件也会挂载宿主机的/run/nvidia目录到容器中)。

        volumeMounts:
          - name: run-nvidia
            mountPath: /run/nvidia
            mountPropagation: Bidirectional
      .....
      volumes:
        - name: run-nvidia
          hostPath:
            path: /run/nvidia

在Kubernetes集群中基于容器安装节点GPU驱动

下面将演示怎样在k8s集群中为GPU节点安装驱动。

前提条件

在为集群节点安装GPU驱动之前,有些条件需要满足:

  • 节点的操作系统类型为Centos7。
  • 节点上不能安装NVIDIA驱动,如果已安装,需要卸载驱动并重启机器。
  • k8s版本 >= 1.13(本次演示使用的集群版本为1.16.9)

同时,本次演示所使用的nvidia驱动镜像没有采用NVIDIA官方提供的镜像,而是修改了nvidia-driver脚本的自定义镜像。

操作步骤

1.下载gpu-operator项目源码。

$ git clone -b 1.6.2 https://github.com/NVIDIA/gpu-operator.git
$ cd gpu-operator
$ export GPU_OPERATOR=$(pwd)

2.修改镜像名称。

$ cd $GPU_OPERATOR/assets/state-driver
$ export CUSTOM_IMAGE=registry.cn-beijing.aliyuncs.com/kube-ai/nvidia-driver:450.102.04-centos7
$ sed -i "s@FILLED BY THE OPERATOR@$CUSTOM_IMAGE@g" 0500_daemonset.yaml

3.删除无关的yaml。

$  cd $GPU_OPERATOR/assets/state-driver
$ rm -rf 0410_scc.openshift.yaml

4.编辑0300_rolebinding.yaml。

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: nvidia-driver
  namespace: gpu-operator-resources
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nvidia-driver
  # 注释这一行
  # namespace: gpu-operator-resources
subjects:
- kind: ServiceAccount
  name: nvidia-driver
  namespace: gpu-operator-resources
# 注释这两行  
# userNames:
# - system:serviceaccount:gpu-operator-resources:nvidia-driver

5.创建gpu-operator-resources这个namespace。

$ kubectl create ns gpu-operator-resources

6.使用kubectl部署。

$ kubectl apply -f $GPU_OPERATOR/assets/state-driver

7.给GPU节点打上标签“nvidia.com/gpu.present=true”(节点只有打上该标签后,才会在该节点上安装驱动)。

$ kubectl label nodes <NODE_NAME> nvidia.com/gpu.present=true

验证

1.查看pod是否处于Running状态。

$ kubectl get po -n gpu-operator-resources
NAME                            READY   STATUS    RESTARTS   AGE
nvidia-driver-daemonset-mqklz   1/1     Running   5          35m
nvidia-driver-daemonset-w5sf4   1/1     Running   5          35m
nvidia-driver-daemonset-zghxj   1/1     Running   5          35m

2.进入pod执行nvdia-smi,观察该命令能否正常执行。

$ kubectl exec -ti nvidia-driver-daemonset-mqklz -n gpu-operator-resources -- nvidia-smi
Fri Mar 19 12:59:18 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.102.04   Driver Version: 450.102.04   CUDA Version: 11.0     |
|-------------------------------+----------------------+----------------------+
| 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 V100-SXM2...  On   | 00000000:00:07.0 Off |                    0 |
| N/A   34C    P0    24W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

3.登陆到节点上执行nvidia-smi,观察能否正常执行(如果是采用nvidia官方的镜像,无法在节点上执行nvidia-smi)。

$ [root@iZ2zeb7ywy6f2gnxrmjid1Z /]# nvidia-smi
Fri Mar 19 21:01:01 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.102.04   Driver Version: 450.102.04   CUDA Version: 11.0     |
|-------------------------------+----------------------+----------------------+
| 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 V100-SXM2...  Off  | 00000000:00:07.0 Off |                    0 |
| N/A   33C    P0    23W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

4.查看节点上/dev下有没有nvidia设备。

$  ls /dev/nvidia*
/dev/nvidia0  /dev/nvidiactl

/dev/nvidia-caps:
nvidia-cap1  nvidia-cap2

可以看到,有一张GPU卡,说明安装是成功的。

缺点

目前,基于容器安装NVIDIA驱动还具有如下的一些不足:

  • 集群中的节点必须保证是同一类操作系统(比如全是CentOS 或全是Ubuntu ),因为安装NVIDIA驱动是以daemonset方式部署到集群中,而该daemonset的应用容器只有一个,所以只能选择一个镜像。
  • 基于容器安装NVIDIA驱动的稳定性还有待提高,在挂载驱动的rootfs函数(_mount_rootfs函数)中可以看到,是将容器根目录挂载到/run/nvidia/driver目录,当容器被kill时,容器检测到退出信号会自动执行_unmount_rootfs函数进行unmount /run/nvidia/driver目录操作,如果此时有另一个安装驱动的容器启动并执行nvidia init就会造成挂载混乱,简单例子就是先使用“kubectl delete daemonset”删除安装驱动的daemonset,然后立即使用kubectl apply命令再部署该daemonset,这样就会导致同一个节点上,一个容器在挂载驱动,另一个容器再卸载驱动,挂载驱动的容器就会报错:驱动正在使用中。碰到这种问题,只能重启节点解决。

总结

本篇文章花了较多篇幅介绍基于容器安装NVIDIA GPU驱动的Dockerfile和nvidia-driver脚本,主要是希望读者阅读该文章后能够基于自己的场景需求,定制化镜像。

上一篇:NVIDIA GPU Operator分析三:NVIDIA Device Plugin安装


下一篇:RobotFrameWork接口项目分层及通用控制方式