Skip to content

第三部分 GPU 虚拟化与共享技术(第 9~12 章)

在前两部分中,我们已经认识了 GPU 硬件(SXM/PCIe 形态、NVLink、显存架构),也装好了驱动、CUDA、Docker 与 NVIDIA Container Toolkit。从本部分开始,我们正式进入"运维工程师的日常工作":一张 GPU 卡很贵,如何让多个人、多个业务安全高效地共享它?

这是 GPU 集群运维中最核心、面试中最常被问到的话题之一。我们会从硬件到软件、从最"重"的虚拟化层到最"轻"的 API 拦截层,把所有主流共享方案系统过一遍,并给出选型方法。

本部分地图:一张总览对比表

方案工作层次隔离性性能损耗是否需要 License典型适用场景
PCIe 直通(Passthrough)虚拟化层(KVM/VMware)强(整卡独占)几乎无损虚拟机内跑 GPU 业务、整卡专用
NVIDIA vGPU虚拟化层强(官方支持)很小需要(按 license 计费)企业桌面虚拟化、VDI、合规要求高的虚机 GPU
MIG(Multi-Instance GPU)GPU 硬件级最强(硬件级隔离)几乎无损(按实例规格分摊)数据中心新卡的多租户推理、故障域隔离
MPS(Multi-Process Service)CUDA 运行时层弱(共享上下文)很小,可提升小任务吞吐同一节点上多个推理/小计算进程复用一张卡
时间片共享(Time-Slicing)K8s Device Plugin 层弱(纯调度层声明)上下文切换有开销开发测试环境"一卡多 Pod"
软件显存/算力切分(HAMi、cGPU、qGPU 等)CUDA API 拦截层中(显存硬限制,算力软限制)有一定开销(约几个百分点,依实现而异)视方案而定(HAMi 开源免费)大规模推理集群的显存级细粒度共享

⚠️ 重要提醒:GPU 型号迭代非常快,文中涉及的 MIG 规格、vGPU 支持卡型、license 价格等具体信息,一律以 NVIDIA 官方文档(docs.nvidia.com)为准。我们讲原理和操作方法,具体数字请以官方最新文档核对。

选型的三个灵魂问题(读完本部分你要能回答):

  1. 我的业务是跑在虚拟机里还是容器里?
  2. 我需要的是强隔离(多租户、防干扰)还是高利用率(尽量塞满)?
  3. 我的卡是什么型号、什么代际(决定了 MIG 是否可用)?

第 9 章 GPU 直通与 vGPU(虚拟化层方案)

本章目标

  • 理解 PCIe Passthrough(直通)的原理,能独立完成 KVM 虚机绑定 GPU 的全流程
  • 理解 NVIDIA vGPU 的架构、license 类型和安装配置流程
  • 掌握两者的优缺点和运维要点(尤其是 license 管理)

9.1 PCIe Passthrough 直通原理

是什么、为什么需要它

很多企业的存量基础设施是虚拟化平台(KVM、VMware ESXi、OpenStack),业务必须跑在虚机里。而 PCIe Passthrough(PCIe 直通) 的做法是:把一块物理 PCIe 设备(这里是整张 GPU 卡)从宿主机上"摘下来",直接、独占式地交给某一个虚拟机使用。虚拟机里的操作系统会看到一块真实的 GPU,照常安装 NVIDIA 驱动、跑 CUDA,几乎和物理机没有区别——这是让虚机用上 GPU 最简单直接、性能几乎无损的方式。

两个关键技术:IOMMU 与 VFIO

  • IOMMU(I/O 内存管理单元):Intel 平台叫 VT-d,AMD 平台叫 AMD-Vi。它就像 CPU 世界里 MMU 的"设备版",负责把设备的 DMA(直接内存访问)地址翻译/隔离到指定虚机的地址空间。没有 IOMMU,直通设备就可以读写宿主机全部内存,既不安全也不可用,所以直通必须开启 IOMMU。
  • VFIO(Virtual Function I/O):Linux 内核提供的一套用户态驱动框架。它把 PCI 设备从宿主机驱动上"解绑",暴露给用户态的 QEMU/KVM 使用,同时通过 IOMMU 做隔离。你可以把它理解为"设备的中介人":宿主机不再碰这张卡,由 VFIO 托管并转交给虚机。

完整流程示例(以 KVM + Ubuntu 为例)

前置条件:服务器 BIOS 中已开启 VT-d(Intel)或 AMD-Vi(AMD),以及 SR-IOV 相关选项(如有)。

第 1 步:开启 IOMMU 内核参数

编辑 /etc/default/grub

bash
# Intel 平台
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt"
# AMD 平台
GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt"

iommu=pt 表示宿主机自身设备使用 passthrough 模式,减少 IOMMU 的性能开销。然后更新引导并重启:

bash
sudo update-grub
sudo reboot

重启后验证 IOMMU 是否生效(Intel 平台应看到 DMAR: IOMMU enabled 字样):

bash
sudo dmesg | grep -i -e DMAR -e IOMMU

第 2 步:找到 GPU 的 PCI 地址并确认 IOMMU 分组

bash
lspci -nn | grep -i nvidia
# 示例输出:
# 81:00.0 3D controller [0302]: NVIDIA Corporation GA100 [A100 PCIe 80GB] [10de:20f1] (rev a1)
# 81:00.1 Audio device [0403]: NVIDIA Corporation GA100 High Definition Audio Controller [10de:20f1] (rev a1)

记下 PCI 地址 81:00.0 和厂商:设备 ID 10de:20f1。注意 GPU 常带一个同组的音频控制器(81:00.1),同一 IOMMU 组的设备必须整体直通,所以两个都要处理。

第 3 步:把 GPU 从宿主机驱动解绑,绑定到 vfio-pci

方式一(推荐,持久生效):通过驱动黑名单 + vfio 绑定 ID:

bash
# 禁止宿主机加载 nvidia 相关驱动
cat <<EOF | sudo tee /etc/modprobe.d/blacklist-nvidia.conf
blacklist nvidia
blacklist nvidiafb
blacklist nouveau
blacklist nvidia_drm
blacklist nvidia_modeset
EOF

# 让 vfio-pci 在启动时认领这些设备(替换为你的实际 ID)
echo "options vfio-pci ids=10de:20f1" | sudo tee /etc/modprobe.d/vfio.conf
sudo update-initramfs -u && sudo reboot

临时验证也可手动操作:echo 0000:81:00.0 | sudo tee /sys/bus/pci/devices/0000:81:00.0/driver/unbind 解绑,再 echo "10de 20f1" | sudo tee /sys/bus/pci/drivers/vfio-pci/new_id 绑定到 vfio-pci。

验证绑定结果(Kernel driver in use 应显示 vfio-pci):

bash
lspci -nnk -s 81:00.0

第 4 步:把 GPU 挂给 KVM 虚拟机

virsh edit <虚机名> 在 XML 的 <devices> 段中加入:

xml
<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x81' slot='0x00' function='0x0'/>
  </source>
</hostdev>
<!-- 同 IOMMU 组的音频控制器 81:00.1 按同法再加一段 hostdev -->

然后启动虚机,在虚机内安装 NVIDIA 驱动,执行 nvidia-smi 应能看到这张卡。

💡 小技巧:部分数据中心卡(如 Tesla 系列)在虚机里可以直接使用;某些消费级卡会有"驱动检测是否在虚机中运行"的限制,需要额外隐藏 KVM 特征(如 XML 中 <kvm><hidden state='on'/></kvm>)。生产环境建议直接使用数据中心卡。

9.2 直通的优缺点

优点

  • 性能几乎无损:虚机直接操作硬件,和物理机差距通常在 1~3% 以内。
  • 兼容性好:虚机内照常装驱动、CUDA,应用无感知。
  • 隔离性强:整卡独占,虚机之间互不影响。

缺点

  • 一卡一虚机:一张物理卡只能直通给一个虚机,GPU 这种昂贵资源的利用率上不去。
  • 缺乏灵活性:不能在线迁移(vMotion/Live Migration 基本不可用),不能动态调整分配。
  • 宿主机失去对该卡的控制:监控、调度都要进虚机里做。

9.3 NVIDIA vGPU 原理与架构

NVIDIA vGPU 是 NVIDIA 官方的 GPU 虚拟化方案:一张物理 GPU 被切分成多个 vGPU 实例,每个实例分配给一个虚机,各虚机共享物理卡的算力,同时获得官方支持的显存和故障隔离。正因为直通"一卡一虚机"太浪费,vGPU 应运而生。

架构:三个核心组件

 VM1 / VM2 / VM3:各装一个 vGPU Guest Driver
═══════ Hypervisor(KVM / VMware ESXi ...)═══════
        NVIDIA vGPU Manager(装在宿主机,
        负责切分、调度,新版基于 SR-IOV VF)
════════════ 物理 GPU(如 A10、L40)════════════
  • vGPU Manager:安装在宿主机(Hypervisor 层)的特殊驱动,替代普通的 NVIDIA 驱动。它负责把物理卡虚拟成多个 vGPU(新版本基于 SR-IOV 的 VF 实现,旧版本叫 vGPU 类型/mdev 设备)。
  • vGPU Guest Driver:安装在每个虚机里的驱动,与 vGPU Manager 配套(版本必须匹配)。
  • License Server / License 服务:vGPU 是收费功能,Guest Driver 启动时必须拿到 license 授权,否则性能会被限制。

License 类型(概念性了解)

License 类型定位典型场景
vApps应用虚拟化单个应用远程交付(如远程跑一个 3D 软件)
vPC(原 GRID)虚拟桌面办公 VDI、轻度图形
RTX vWS(原 Quadro vDWS)专业工作站CAD、渲染、设计类重图形负载
vCS计算AI 训练/推理、HPC,数据中心算力场景

做 GPU 算力运维最常打交道的是 vCS(NVIDIA Virtual Compute Server)。具体产品名称和授权模式随版本演进可能调整,以 NVIDIA 官方文档为准。

9.4 vGPU 的安装配置流程概述

vGPU 的完整部署细节与 Hypervisor 强相关(VMware、KVM、Citrix 流程不同),这里给出通用骨架,理解每一步在干什么即可:

  1. 确认硬件与授权:确认 GPU 型号在 vGPU 支持列表中(数据中心卡为主),购买对应 license。
  2. 宿主机安装 vGPU Manager:用 NVIDIA 提供的 vGPU Manager 驱动包替换普通驱动(例如 KVM 下安装 NVIDIA-GRID-Linux-KVM-*.run 包)。装完后 nvidia-smi 会显示支持 vGPU 的信息。
  3. 配置 vGPU 实例
    • 新版本(基于 SR-IOV):在 BIOS 开启 SR-IOV,通过 nvidia-smi vgpu 或 sysfs 启用 VF,并把 VF 挂给虚机;
    • 旧版本(mdev):通过 mdevctl 创建指定类型的 vGPU 设备(如 nvidia-xxx 类型对应不同显存大小)。
  4. 虚机安装 vGPU Guest Driver:版本号必须与宿主机 vGPU Manager 配套(同一大版本分支)。
  5. 配置 License:在虚机内编辑 /etc/nvidia/gridd.conf 指向 License Server(新版为 NVIDIA License System,NLS),验证授权:
bash
# 虚机内验证 license 状态(Licensed 应为 Yes)
nvidia-smi -q | grep -A5 -i "vGPU Software Licensed"
  1. 验证:虚机内跑 nvidia-smi 确认 vGPU 规格,跑一个 CUDA 样例程序确认计算正常。

9.5 运维要点

  • License 过期的影响:Guest Driver 拿不到 license 时,通常表现为帧率/算力被限制、功能降级(不同版本行为不同,以官方文档为准)。运维上要把 License Server 当作关键基础设施做高可用,并监控 license 余量和过期时间。
  • 版本配套关系:vGPU Manager(宿主机)与 Guest Driver(虚机)必须同分支配套;Hypervisor 版本也在 NVIDIA 的支持矩阵里。升级任何一方前先查兼容性矩阵,否则会出现驱动装不上或 vGPU 起不来的问题。
  • 排障套路:先看宿主机 nvidia-smi → 再看虚机 nvidia-smi → 再看 license 状态 → 最后查宿主机和虚机的驱动日志(/var/log/ 下 nvidia 相关日志)。
  • 容量规划:一张卡切几个 vGPU、每个多大显存,要在采购时就算好;vGPU 类型一旦确定,变更通常需要重新配置并影响在运行的虚机。

本章小结

  • 直通 = 整卡给一个虚机,性能无损但一卡一机,适合"虚机里的整卡专用"场景。
  • vGPU = NVIDIA 官方的"一卡多虚机"方案,隔离性官方背书,但要花钱买 license,且版本配套管理是运维重头戏。
  • 两者都工作在虚拟化层。如果业务已经全面容器化(跑在 K8s 上),往下看 MIG、MPS、时间片这些更轻量的方案。

动手实验

  1. 在实验机 BIOS 中确认 VT-d/AMD-Vi 已开启,按 9.1 流程开启 IOMMU 并用 dmesg 验证。
  2. lspci -nn 找出实验 GPU 的 PCI 地址和 IOMMU 分组(find /sys/kernel/iommu_groups/ -type l 可查看分组),完成一次 GPU 到 vfio-pci 的绑定与解绑,观察 lspci -nnk 的变化。
  3. 思考题:为什么直通场景下 GPU 的热迁移(Live Migration)几乎不可行?vGPU 的新版本(SR-IOV 架构)为什么开始支持有限的热迁移?

第 10 章 MIG(Multi-Instance GPU)

本章目标

  • 理解 MIG 的硬件级切分原理与 GI/CI 两层概念
  • 掌握 MIG 的开启、实例创建、查看、销毁全流程命令
  • 清楚 MIG 的限制和适用场景

10.1 MIG 原理

是什么

MIG(Multi-Instance GPU,多实例 GPU) 是 NVIDIA 从 Ampere 架构(A100)开始提供的能力:把一张物理 GPU 在硬件层面切成多个完全隔离的小 GPU 实例。每个实例拥有自己专用的:

  • 显存(一块固定的显存分区,带独立的显存控制器路径)
  • L2 缓存(划分好的缓存条带)
  • 内存带宽(按切分比例分配)
  • 计算单元(SM)(独占分配给该实例)

关键在于"硬件级"三个字:隔离是由 GPU 芯片内部的硬件机制保证的,不是靠软件约定。一个实例里的程序崩溃、显存写爆、算力打满,都不会影响同一张卡上的其他实例——连性能抖动都不会传导过去(这是它和后面所有软件方案的本质区别)。

为什么需要它

真实业务里有大量"吃不下一张卡"的负载:推理服务只要 8GB 显存但卡是 80GB;Jupyter 开发环境大部分时间在写代码;多个小训练任务互相抢卡拖慢彼此。MIG 让一张大卡变成几张"小卡",按需分配,各得其所,既安全又不浪费。

⚠️ MIG 仅支持数据中心级的新架构 GPU(Ampere 及以后的 A100、A30、H100、H200、B 系列等,具体型号以 NVIDIA 官方文档为准)。消费级卡(如 RTX 4090)和上一代数据中心卡(V100、T4)不支持 MIG

10.2 MIG 规格表与 GI/CI 概念

规格命名规则

MIG 实例规格命名形如 1g.5gb

  • 数字 + g:占整张卡的几分之几的计算切片(GPU Slice)。A100/H100 最多切成 7 份,1g 就是七分之一。
  • 数字 + gb:该实例拥有的显存大小。

A100 40GB 为例,典型的可用规格如下(不同显存容量/代际的卡规格表不同,以 NVIDIA 官方文档为准):

规格显存计算切片一张 A100 40GB 最多可切几个
1g.5gb5 GB1/77 个
2g.10gb10 GB2/73 个
3g.20gb20 GB3/72 个
4g.20gb20 GB4/71 个
7g.40gb40 GB7/7(整卡)1 个

注意两点:

  • 组合必须合法:切片在物理上是连续摆放的,不是任意数学组合都可行(例如不能同时存在两个 4g.20gb,因为 4+4 > 7)。常见用法:7 × 1g.5gb3 × 2g.10gb + 1 × 1g.5gb(需查官方布局)、2 × 3g.20gb 等。
  • 不同代际规格不同:H100 80GB 的规格是 1g.10gb2g.20gb3g.40gb7g.80gb 等,不要凭 A100 的经验套用到其他卡上。

GI 与 CI:两层实例

MIG 的配置分两步、两层:

物理 GPU
 └─ GPU Instance(GI)                    ← 第一层:切"卡"
     ├─ Compute Instance(CI)            ← 第二层:切"算力"
     └─ Compute Instance(CI)
  • GI(GPU Instance):显存 + 计算切片 + 带宽的完整硬件分区。创建 GI 就决定了"这块小 GPU"的规格(如 2g.10gb)。
  • CI(Compute Instance):在 GI 内部再细分计算单元。一个 GI 默认对应一个占满它的 CI;也可再切成多个更小的 CI(共享 GI 的显存)。

运维实践中 90% 的场景是:创建一个 GI,再在里面创建一个默认 CI(CI 规格等于 GI 规格),然后把 CI 分配给一个容器/Pod。记住这个套路即可。

10.3 实战:MIG 完整操作

前置:数据中心 GPU + 配套的较新驱动与 CUDA,且当前卡上没有正在运行的 GPU 任务。

第 1 步:开启 MIG 模式

bash
# 对 GPU 0 开启 MIG(-i 指定 GPU 索引,不带则对所有卡)
sudo nvidia-smi -i 0 -mig 1

# 查看当前 MIG 模式状态
nvidia-smi -i 0 --query-gpu=mig.mode.current --format=csv
# 输出: Enabled 表示已开启

注意:开启/关闭 MIG 是全局性变更。多数卡要求先 drain(排空)该卡上的所有任务,部分环境还需要重启才能生效。生产环境建议配合 K8s 的节点驱逐流程操作(见 10.4)。

第 2 步:创建 GPU Instance(GI)

bash
# 列出当前 GPU 支持的 GI 规格(-lgip)
nvidia-smi mig -i 0 -lgip

# 创建一个 1g.5gb 的 GI(也可以一次创建多个:-cgi 1g.5gb,1g.5gb,1g.5gb)
sudo nvidia-smi mig -i 0 -cgi 1g.5gb

第 3 步:创建 Compute Instance(CI)

bash
# 先查看刚创建的 GI,拿到 GI ID
nvidia-smi mig -i 0 -lgi

# 在 GI 1 中创建默认 CI(-C 表示创建与 GI 等大的 CI)
sudo nvidia-smi mig -i 0 -gi 1 -C

也可以一步到位:-cgi 1g.5gb -C 在创建 GI 的同时创建默认 CI。

第 4 步:查看与使用

bash
# 查看 GI 和 CI
nvidia-smi mig -lgi
nvidia-smi mig -lci

# nvidia-smi 的 MIG 视图(能看到每个实例的显存、利用率)
nvidia-smi

在容器里使用指定的 MIG 实例(通过其 UUID):

bash
# 获取 CI 的 UUID(输出类似: MIG 1g.5gb Device 0: (UUID: MIG-xxxx-xxxx))
nvidia-smi -L

# 用 Docker 挂载指定 MIG 实例
docker run --rm -it -e NVIDIA_VISIBLE_DEVICES=MIG-xxxx-xxxx \
  nvcr.io/nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi -L

在 K8s 中,通过 device plugin 的 mig-strategy 配置,MIG 实例会以 nvidia.com/mig-1g.5gb 之类的资源名暴露,Pod 直接按资源申请(详见第六部分调度章节)。

第 5 步:销毁

bash
# 先销毁 CI,再销毁 GI(顺序不能反)
sudo nvidia-smi mig -i 0 -gi 1 -ci 0 -dci
sudo nvidia-smi mig -i 0 -gi 1 -dgi

# 如需彻底关闭 MIG 模式(先排空所有实例)
sudo nvidia-smi -i 0 -mig 0

10.4 MIG 的限制

  1. 改配置成本高:开启/关闭 MIG、增删 GI 都要求实例上没有任务在跑,部分操作需要重启。生产中的标准做法是:K8s 驱逐该节点所有 Pod → 调整 MIG 布局 → 重新上线。MIG 布局不适合频繁变动,应按稳定的资源画像一次配好。
  2. 与 MPS 的兼容性受限:MIG 与 MPS 在同一 GPU 上同时使用存在限制(部分版本支持 CI 内启用 MPS,部分场景不支持),不要想当然混用,以官方文档为准
  3. 切分粒度固定:只有规格表里那几种,没有 1.5g、没有自定义显存大小。想要任意显存切分就得看第 12 章的软件方案。
  4. 仅新数据中心卡支持:采购时就要考虑。存量 V100/T4 集群用不了 MIG。
  5. 单实例内无 NVLink 跨卡能力:MIG 实例之间、实例与其他卡之间的某些互联特性受限,大模型多卡训练任务一般不切 MIG。

10.5 MIG 适用场景

  • 多租户推理:给每个租户/服务分一个 MIG 实例,SLA 互不干扰,还能精确计费。
  • 在线服务 + 离线小任务混布:大卡切成"在线 3g + 离线 4g",离线任务打满也不影响在线延迟。
  • 开发测试平台:一张 A100 切 7 个 1g.5gb,7 个开发者互不打扰。
  • 不适合的场景:大显存大模型训练(显存切片放不下模型)、需要极致互联性能的多卡训练、消费卡集群。

本章小结

  • MIG = 硬件级切卡,隔离性最强、性能无损,是数据中心新卡上多租户场景的首选。
  • 记住两层模型:GI(切卡)→ CI(切算力),日常操作就是 -cgi + -C
  • 代价是灵活性:规格固定、变更需要 drain 甚至重启、仅新卡支持。

动手实验

  1. 在实验卡上执行 nvidia-smi -i 0 -mig 1 开启 MIG,用 --query-gpu=mig.mode.current 确认状态;用 nvidia-smi mig -lgip 列出支持的 GI 规格,对照官方文档验证。
  2. 创建 2 个 1g.5gb(或你卡上最小的)GI + CI,用 NVIDIA_VISIBLE_DEVICES=<UUID> 分别在两个容器中跑 nvidia-smi -L,确认互相看不到对方的实例。
  3. 销毁所有 CI/GI,关闭 MIG 模式,记录哪些步骤要求先排空任务。
  4. 思考题:为什么 MIG 实例间的故障隔离比容器强?提示:从"谁在做隔离"的角度回答。

第 11 章 MPS 与时间片共享

本章目标

  • 理解 MPS 的原理:为什么多进程共享 GPU 上下文能提高小任务利用率
  • 掌握 MPS 服务的启动、停止、客户端使用与显存限制
  • 理解时间片共享(Time-Slicing)在 K8s 中的实现机制
  • 能够用"选型决策树"在 MPS / 时间片 / MIG 之间做选择

11.1 MPS(Multi-Process Service)原理

先理解问题:多进程独占上下文的浪费

默认情况下,每个使用 GPU 的进程会创建自己独立的 CUDA 上下文(Context)。当多个进程跑在同一张卡上时,GPU 在同一时刻只能执行其中一个上下文的任务,靠上下文切换轮转。这带来两个问题:

  1. 每次切换有微秒到毫秒级的开销,小任务切换频繁,浪费明显;
  2. 更重要的是:多个进程的计算无法真正"并发"执行。每个小推理请求只占用 10% 的算力,但它们是串行排队的,卡的利用率始终上不去。

MPS 的思路

MPS 在 GPU 前面加了一个"服务端":所有客户端进程的 CUDA 调用都汇集到 MPS Server,由它统一向 GPU 提交。效果是:

  • 多个进程共享同一个 GPU 上下文的空间,不再频繁切换上下文;
  • 来自不同进程的计算任务可以并发填充到 GPU 的 SM 上(硬件同时执行多个小 kernel);
  • 对小而多的负载(典型的如在线推理请求),整体吞吐和延迟都明显改善。

一句话总结:MPS 不改变"卡还是一张"的事实,它改变的是"多个进程能不能真正同时用这张卡"。

适合谁

适合大量小 kernel、小模型的推理服务(每请求算力需求远小于一张卡)与 MPI 多进程计算(MPS 最初就是为 MPI 设计的);不适合单个任务本身就能打满整张卡的大训练任务——用 MPS 没有意义,还引入额外复杂度。

11.2 MPS 实战

启动 / 停止 MPS 服务

MPS 的控制进程是 nvidia-cuda-mps-control(随驱动安装):

bash
# 以 daemon 方式启动 MPS control server
nvidia-cuda-mps-control -d

# 查看 MPS 服务状态(输出 server 的 PID 即表示运行中)
echo get_server_list | nvidia-cuda-mps-control

# 停止 MPS 服务
echo quit | nvidia-cuda-mps-control

生产环境建议用 systemd 托管 MPS(Type=forkingExecStart=/usr/bin/nvidia-cuda-mps-control -dExecStopecho quit | ...),并提前建好 /tmp/nvidia-mps(管道目录)与 /var/log/nvidia-mps(日志目录),保证开机自启、故障可拉起。

客户端使用

客户端不需要改任何代码。只要设置了管道目录环境变量,CUDA 程序启动时会自动发现 MPS server 并接入:

bash
export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
export CUDA_MPS_LOG_DIRECTORY=/var/log/nvidia-mps
./my_cuda_app      # 自动走 MPS

在容器中使用时,把宿主机的 pipe 目录挂进容器即可(K8s 场景由官方 device plugin 或 GPU Operator 自动处理)。

资源限制(防止单个进程吃光)

MPS 本身隔离性弱(见 11.3),但提供了两个重要的软性限制环境变量,在客户端侧设置:

bash
# 限制该客户端最多使用的算力比例(百分比,针对每个 MPS 客户端)
# 实际通过 CUDA_MPS_ACTIVE_THREAD_PERCENTAGE 实现
export CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=50   # 最多用 50% 的 SM 线程容量

# 限制该客户端可使用的固定显存(pinned memory)上限
export CUDA_MPS_PINNED_DEVICE_MEM_LIMIT=0=4G  # GPU 0 上限 4GB

注意:这些是 MPS 机制提供的限制手段,生效粒度和语义与普通独占模式不同(例如 ACTIVE_THREAD_PERCENTAGE 限制的是活跃线程比例而非精确算力),它们是"防止失控"的护栏,不是硬隔离,语义细节以官方文档为准。

11.3 MPS 的隔离性弱点

必须清醒地认识 MPS 的边界:

  • 无显存硬隔离:各客户端共享显存地址空间的同一上下文区域,一个进程显存泄漏/超用,可能导致其他进程 OOM 甚至整个 MPS 域出问题。上面的显存限制只是缓解。
  • 故障传导:一个客户端进程崩溃,可能把 MPS server 拖入异常状态,波及同卡所有客户端。虽然新版 MPS 有容错改进,但隔离性仍远弱于 MIG。
  • 安全边界弱:不适合严格的多租户场景。

结论:MPS 是"同信任域内的性能优化工具",不是"多租户隔离工具"。 用它聚合"一家人"的小任务,不要用它隔离"陌生人"的任务。

11.4 时间片共享(Time-Slicing)

是什么

时间片共享是 K8s 调度层面的共享方案,由 NVIDIA device plugin 提供。核心思想一句话:告诉调度器"这张卡有 N 份",N 个 Pod 就都能调度上来,但 GPU 物理上什么都没有切——N 个进程跑在同一张卡上,靠 GPU 默认的时间片轮转共享算力。

它不创建任何隔离,只改变 K8s 的资源账本:原来 nvidia.com/gpu: 1 一张卡只能被一个 Pod 申请,现在声明成"5 份"就能被 5 个 Pod 各申请一份。

配置示例(ConfigMap)

device plugin 通过 ConfigMap 定义共享策略:

yaml
# time-slicing-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config
  namespace: nvidia-device-plugin
data:
  any: |-
    version: v1
    sharing:
      timeSlicing:
        resources:
        - name: nvidia.com/gpu
          replicas: 5        # 每张物理卡对外声明 5 份

部署 device plugin 时引用它:

bash
kubectl create -n nvidia-device-plugin -f time-slicing-config.yaml
# GPU Operator 方式:在 ClusterPolicy 的 devicePlugin.config 中指定该 ConfigMap

生效后节点上的可分配资源变为(8 张卡 × 5 份 = 40):

bash
kubectl describe node gpu-node-1 | grep -A3 "Allocatable"
#   nvidia.com/gpu:     40

Pod 侧照旧申请,完全无感知:

yaml
resources:
  limits:
    nvidia.com/gpu: 1     # 申请的其实是"1/5 张卡的时间片"

💡 经验法则:replicas 不是越大越好。时间片方案下多个进程共享上下文切换,Pod 太多会导致切换开销和显存争抢。开发测试环境常用 4~8;生产在线服务慎用。

隔离性:几乎没有

  • 显存:无任何限制,一个 Pod 可以把整张卡显存吃光,挤爆其他 Pod(OOM)。
  • 算力:纯时间片轮转,没有比例保证,一个重任务会拖慢所有人。
  • 适用边界:只适合同信任域、低水位的开发测试/教学/轻推理场景。

11.5 时间片 vs MPS vs MIG 对比与选型决策树

维度时间片共享MPSMIG
实现层次K8s 调度层(账本游戏)CUDA 运行时(共享上下文)硬件级切分
显存隔离弱(可设软限制)强(硬隔离)
算力隔离弱(比例护栏)
故障隔离弱(可能互相波及)
并发执行否(轮转切换)是(小 kernel 并发填充)是(各实例独立)
硬件要求任意卡任意卡仅新数据中心卡
运维成本极低(改个 ConfigMap)中(维护 MPS 服务)中(布局变更需 drain)

选型决策树

业务跑在哪张卡上?
├─ 消费级卡 / 老数据中心卡(无 MIG)
│   ├─ 开发测试、自己人、负载不高 → 时间片共享(最简单)
│   └─ 在线推理、小 kernel 多、追求吞吐 → MPS(加显存护栏)
└─ 新数据中心卡(A100/H100/更新)
    ├─ 多租户 / 需要 SLA 与故障隔离 → MIG(首选)
    ├─ 同信任域密集小推理、追求极致吞吐 → MPS
    └─ 大模型训练 → 不共享,整卡独占

本章小结

  • MPS 解决的是"多进程小任务并发填充 GPU"的性能问题,不是隔离问题。
  • 时间片共享解决的是"K8s 调度账本"问题,让一卡多 Pod 成为可能,但隔离性为零。
  • 要隔离选 MIG,要并发吞吐选 MPS,要省事做开发测试选时间片。

动手实验

  1. 启动 MPS 服务,写两个简单 CUDA 程序(或用两个 gpu-burn/推理压测进程)同时跑,对比开/关 MPS 时的总吞吐。
  2. CUDA_MPS_PINNED_DEVICE_MEM_LIMIT 给一个客户端设置显存上限,故意超申请,观察报错行为。
  3. 在 K8s 集群按 11.4 配置 replicas: 4 的时间片共享,跑 4 个 Pod 各申请 1 份 nvidia.com/gpu,确认能同时调度上同一节点;再让一个 Pod 大量占显存,观察其他 Pod 的 OOM 现象(验证"无隔离")。
  4. 思考题:MPS 和时间片可以叠加使用吗?叠加后各自解决什么问题?

第 12 章 软件层显存/算力切分方案

本章目标

  • 理解 CUDA 拦截/劫持(hook)实现显存与算力切分的基本原理
  • 了解 HAMi、cGPU、qGPU 等代表性方案及其差异
  • 掌握 HAMi 的核心资源概念与使用方式
  • 清醒认识这类方案的风险,能给出生产使用建议

12.1 思路:CUDA 拦截/劫持原理

原理:Hook CUDA API

第 10、11 章的方案各有短板:MIG 规格固定且仅新卡支持;时间片没有显存限制;MPS 隔离弱。能不能在任意卡上,按任意显存大小切分 GPU? 软件层方案的回答是:能,代价是"侵入"。通用套路如下:

应用程序
   │ 调用 CUDA API(如 cudaMalloc、cuLaunchKernel)

┌─ 拦截层(注入的动态库,LD_PRELOAD / 替换 libcuda.so 等方式)
│   - cudaMalloc(1GB) → 检查配额,超了直接返回 OOM
│   - kernel 启动 → 按配额限流(算力 %)
└──────────────────────────────────────
   │ 通过检查的调用才下发

真正的 NVIDIA 驱动 / GPU 硬件
  • 显存限制:拦截所有显存分配类 API(cudaMalloccuMemAlloc 等),维护"本容器已用显存"账本,超过配额就返回 CUDA_ERROR_OUT_OF_MEMORY。这样即使卡有 80GB,容器看到的"可用显存"也只有分给它的额度——显存维度的硬限制
  • 算力限制:拦截 kernel 启动 API,通过限制单位时间内下发的 kernel 数量/占用 SM 的时间比例来做限流(不同项目实现不同)。这属于软性限制,精度和开销取决于实现。
  • 容器适配:通常配合 K8s device plugin + 调度器扩展,把"申请 4GB 显存"翻译成"调度到某卡 + 注入拦截库 + 设置配额"。

一句话:软件方案用"欺骗 + 拦截"的方式,在不做硬件切分的前提下,实现了显存记账和算力限流。

12.2 代表项目简介

项目背景开源情况特点简述
HAMi(原 4paradigm k8s-vgpu)第四范式发起,CNCF Sandbox 项目开源K8s 原生,支持按显存(MB 级)和算力比例申请,支持 N 卡/国产卡多设备统一管理,社区活跃
cGPU阿里云容器服务内核态方案,随阿里云 ACK 提供内核模块实现显存隔离与算力调度,性能好,绑定阿里云生态
qGPU腾讯云随腾讯云 TKE 提供类似思路,与腾讯云 GPU 调度体系集成,支持显存/算力双维隔离
百度等厂商方案各云/大厂内部多为内部/云上产品思路一致:拦截 + 记账 + 限流,差异在实现层次和生态集成

共同趋势:云上托管 K8s 基本都有自家 GPU 共享方案;自建集群、想要开源可控的,主流选择是 HAMi。下面我们以 HAMi 为例详细讲,理解一个就理解一类。

12.3 HAMi 核心概念与使用

架构速览

HAMi 由三部分组成:

  1. 调度器扩展(Scheduler Extender):决定 Pod 落在哪台节点、用哪张卡(按显存余量挑卡);
  2. Device Plugin:向 K8s 注册 nvidia.com/gpumemnvidia.com/gpucores 等扩展资源;
  3. 注入的 vGPU 库:进入容器后拦截 CUDA 调用、执行配额。

核心资源:按显存和算力申请

yaml
apiVersion: v1
kind: Pod
metadata:
  name: vgpu-demo
spec:
  containers:
  - name: app
    image: nvcr.io/nvidia/pytorch:24.05-py3
    resources:
      limits:
        nvidia.com/gpu: 1        # 使用 1 张卡(上的配额)
        nvidia.com/gpumem: 4096  # 分配 4096 MB 显存
        nvidia.com/gpucores: 30  # 分配 30% 算力(可选)
  • nvidia.com/gpumem显存申请,单位 MB。这是 HAMi 的核心卖点——A100 80GB 可以切成十几份按 MB 分配。
  • nvidia.com/gpucores:算力百分比(0~100)。
  • 只写 gpumem 不写 gpucores 时,算力不做限流(共享竞争)。

超卖与节点视角

HAMi 默认允许一张卡的显存被超分(各 Pod 申请之和可以超过物理显存),赌的是大家不会同时打满——和云厂商卖虚机的逻辑一样(超分比例可配置,以 HAMi 当前版本文档为准)。

节点上查看分配情况:

bash
kubectl describe node gpu-node-1 | grep -A5 "nvidia.com"

运维建议:生产推理集群按申请值 ≈ 峰值显存来规划,不要激进超卖;超卖留给开发测试集群。

12.4 这类方案的风险与生产建议

风险清单

  1. 侵入性:拦截层要注入/替换 CUDA 库,应用环境被修改。个别对动态库加载链敏感的程序(静态链接 CUDA、特殊 launcher)可能不兼容。
  2. CUDA/驱动升级兼容性:拦截层 Hook 的是特定版本的 CUDA API。CUDA 大版本升级、驱动升级后,拦截库可能需要同步升级适配,升级窗口要特别小心,先在灰度节点验证
  3. 故障排查难度增加:出问题时会多一层嫌疑对象——"是应用 bug、驱动问题,还是拦截层记账错了?"。显存 OOM 报错可能来自配额而非物理耗尽,排障时第一反应要先查配额。
  4. 算力限制是软性的:限流精度和性能开销因实现而异,别期待 MIG 级别的确定性 SLA。
  5. 项目依赖风险:开源项目的维护节奏、商业版的 license 都要纳入评估。

生产使用建议

  • 定位:用它解决"MIG 切不了(老卡/消费卡)但又要显存硬限制"的中间地带,尤其是大规模推理集群的显存精细化管理
  • 上线前:用真实的业务镜像做兼容性测试(PyTorch/TF/vLLM/TRT-LLM 都要过一遍),压测注入后的性能损耗。
  • 运维上:把拦截库版本纳入变更管理;监控"配额 OOM"和"物理 OOM"两类指标;保留"不启用切分"的兜底调度池。
  • 选型上:云上用云厂商方案(cGPU/qGPU,与平台集成深);自建开源选 HAMi;能上 MIG 的场景优先 MIG。

12.5 本章小结:如何根据业务选择共享方案

把本部分所有方案串起来,按业务类型给结论:

业务类型推荐方案理由
大模型训练(多卡、大显存)不共享,整卡独占共享只会引入干扰和复杂度
小任务训练/微调MIG(新卡) / HAMi 按显存切(老卡)需要显存确定性,防止互相挤爆
在线推理(SLA 敏感)MIG 首选;老卡用 HAMi + 保守配额隔离性 = SLA 的保障
在线推理(同信任域、追吞吐)MPS小 kernel 并发填充,吞吐最优
开发测试/Jupyter时间片共享 或 HAMi成本低、够用、坏了不心疼
虚机里的 GPU 业务直通(整卡)或 vGPU(一卡多虚机)虚拟化层原生方案
国产化/多云异构集群HAMi 等多设备统一管理层一套接口管多种卡

最终选型口诀:先问"能不能整卡独占"(能就独占,最简单);再问"要不要硬隔离"(要 → MIG / vGPU);再问"卡支不支持 MIG"(不支持 → HAMi/cGPU 类软件方案);最后"同信任域追性能"才考虑 MPS,"开发测试图省事"才用时间片。

动手实验

  1. 部署 HAMi(参考其官方文档的 Helm 安装),提交一个 nvidia.com/gpumem: 4096 的 Pod,进入容器用 PyTorch 尝试分配 8GB 显存张量,观察 4GB 处的 OOM 报错——验证显存记账生效。
  2. 查看该 Pod 所在卡的物理显存(nvidia-smi),对比"容器看到的可用显存"与物理显存的差异,理解拦截层如何"欺骗"应用。
  3. 思考题:为什么软件切分方案对静态链接 CUDA 的程序可能失效?从 hook 的实现机制解释。
  4. 综合题:给你一个集群——8 张 A100 80GB、4 张 T4 16GB,业务有:LLM 训练、在线推理(SLA 敏感)、内部 Jupyter 平台。请为三类业务分别设计共享方案并说明理由。

本部分回顾:我们从虚拟化层(直通/vGPU)讲到硬件层(MIG)、运行时层(MPS)、调度层(时间片)再到 API 拦截层(HAMi 等软件方案)。记住一个规律:越靠近硬件,隔离越强但灵活性越差;越靠近软件,灵活性越强但隔离和稳定性越需要你自己把关。 下一部分我们将进入 GPU 集群的调度与资源管理,把这些单机能力组织成集群级能力。