Documentation Index Fetch the complete documentation index at: https://se7en.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
本章概要
本章围绕一个核心问题:即使 GPU 代码和库已经高度优化,系统层面的瓶颈仍然会限制大规模 AI 训练的性能 。最快的 GPU 也只有在数据和指令能高效到达的前提下才能发挥全部潜力。系统级调优常被忽视,但在大集群上,仅 OS 配置的小改动就可能带来两位数百分比的性能提升,换算到大型 AI 项目就是数十万甚至上百万美元的计算成本节约。
全章从底向上覆盖了四个层次的优化:操作系统 → GPU 驱动与运行时 → 容器运行时 → Kubernetes 编排 。每层的目标一致——最小化延迟、最大化吞吐量,让 GPU 始终处于满负荷工作状态。
章节详解
1. NVIDIA 软件栈全景
GPU 集群的运行远不止编写 PyTorch 代码。完整的软件栈从底到顶:
图源:NVIDIA Data Center Drivers:Overview of CUDA Toolkit and Associated Products
层级 组件 职责 高层框架 PyTorch 用户代码入口,自动捕获和优化模型 Python CUDA 库 cuTile / cuPyNumeric / Triton / Warp … Kernel 编写与领域加速 编译与运行时 CUDA Toolkit 编译器、运行时、优化数学库 硬件接口 GPU Driver 内存分配、任务调度、设备管理
当你执行 torch.matmul(A, B) 时,调用链路为:PyTorch → cuBLAS → CUDA Runtime → GPU Driver → GPU Kernel 执行。如图右侧所示,每一层都可能引入瓶颈 。
下面用一个简单的矩阵乘法示例,展示这条调用链路中每一层实际发生了什么:
import torch
# ① Python 层:用户代码
A = torch.randn( 4096 , 4096 , device = "cuda" ) # 在 GPU 上分配并初始化张量
B = torch.randn( 4096 , 4096 , device = "cuda" )
# ② PyTorch 层:调度到正确的后端
# torch.matmul 检测到输入在 CUDA 设备上,
# 分发到 aten::mm 的 CUDA 实现
C = torch.matmul(A, B)
# 实际发生的调用链路:
#
# torch.matmul(A, B)
# → aten::mm (PyTorch ATen dispatcher)
# → at::cuda::blas::gemm (PyTorch CUDA 后端)
# → cublasSgemm / cublasGemmEx (cuBLAS 库)
# → cudaLaunchKernel (CUDA Runtime)
# → ioctl(/dev/nvidia0) (GPU Driver)
# → GPU SM 执行 GEMM kernel
# ③ 同步:确保 GPU 计算完成
torch.cuda.synchronize()
# 此时 C 包含 A × B 的结果,整个过程经历了上述所有层
print ( f "输入: { A.shape } × { B.shape } → 输出: { C.shape } " )
print ( f "设备: { C.device } , 数据类型: { C.dtype } " )
每一层的耗时都可以用 NVIDIA Nsight Systems(nsys)捕获和分析,这也是后续章节 profiling 的基础。
1.1 GPU Driver
GPU Driver 是 Linux 内核模块,管理 GPU 硬件的底层操作。安装后创建 /dev/nvidia0、/dev/nvidiactl、/dev/nvidia-uvm 等设备文件。核心职责:
内存分配 — 管理 GPU 显存(HBM)的分配与释放。
任务调度 — 将 kernel 分发到 GPU 的 SM 上执行。
多租户分区 — 通过 MIG/MPS 支持多进程共享 GPU。
监控工具 — nvidia-smi 查看温度、利用率、ECC 状态、GPU 模式。
保持 driver 更新很重要——新版本通常解锁性能优化并支持最新 GPU 架构和 CUDA 特性。
在 Driver 之上是 CUDA Toolkit,包含编译器和运行时库:
nvcc — CUDA 编译器,将 .cu 代码分离为 host 和 device 代码。
cudart — CUDA 运行时,负责 kernel 启动、内存管理、流同步。
优化库 — cuDNN(神经网络原语)、cuBLAS(线性代数)、NCCL(多 GPU 集合通信)。
始终使用支持你 GPU compute capability 的最新 CUDA Toolkit——最新版本包含针对你硬件的编译器优化和库改进。
CUDA 前向与后向兼容性: CUDA 编译输出同时包含 PTX (中间表示,可被新 GPU JIT 编译,提供前向兼容)和 CUBIN/SASS (特定架构的机器码,已知架构直接执行,提供后向兼容)。两者打包为 fatbinary ,同时支持当前和未来硬件。CUBIN 本身不具备前向兼容性,因此发布时应始终包含 PTX。
1.3 Python CUDA 库
NVIDIA 提供了一系列 Python 库,降低 GPU 编程门槛:
库 定位 CUDA Python 底层 driver/runtime API 的 Python 绑定 cuTile tile 化矩阵运算抽象,简化分块计算 cuPyNumeric NumPy 的 GPU 替代品(import cupynumeric as np) CuPy GPU 加速的数组编程 Warp Python 中编写 GPU kernel CUTLASS cuBLAS 底层使用的 C++ 模板库,提供可组合的 GEMM/卷积原语 Triton OpenAI 的 Python DSL,已集成进 PyTorch 编译器后端
1.4 PyTorch 与高层框架
PyTorch 的编译器栈(torch.compile)由三部分组成:
TorchDynamo — 捕获 Python 代码的计算图
AOT Autograd — 提前生成反向传播图
TorchInductor — 后端代码生成,使用 Triton 做 kernel 融合和自动调优
PyTorch 抽象了 CUDA 编程的复杂性——你写直观的 Python 代码,底层调用高度优化的 CUDA 例程。
2. CPU 与操作系统调优
GPU 利用率不足的一个常见根源,是 CPU 端未能及时、持续地为 GPU 提供有效的计算任务 。训练和推理的具体形态不同,但影响 GPU 计算的 CPU 侧任务主要集中在几类:
准备输入 :训练时读取样本、tokenize、图像解码、数据增强和 batch 组装;推理时处理请求解析、tokenize、padding 和动态 batching。输入准备跟不上时,GPU 只能等待下一批数据或请求。
提交 GPU 任务 :CPU 调用 CUDA API 完成 kernel launch、memory copy 和 stream synchronization,并把这些任务排进 CUDA stream。只有 CPU 完成提交后,GPU 才能开始执行对应 kernel。大量小 kernel 或频繁同步会放大 CPU 开销,导致 GPU 间歇性空闲。
搬运输入数据 :输入通常先在 CPU memory 中生成,再通过 CPU → GPU copy 放入 GPU memory。
分布式通信 :多 GPU 训练或分布式推理中,CPU 侧会发起 collective 或点对点通信,并依赖专门的通信辅助线程维护通信进度。这些线程被调度打断或阻塞时,GPU 可能因为等待其他 rank 或 device 而闲置。
处理设备中断 :NIC/GPU 相关 interrupt 也需要 CPU 响应。如果中断落在远离设备或关键输入/通信线程的 NUMA node 上,或者频繁打断这些关键线程,都会增加访问延迟和调度开销,进而拉长 GPU 等待时间。
如果这些 CPU 侧任务变慢、出现抖动,或者 OS 调度不稳定,昂贵的 GPU 就可能长时间处于空闲,CPU 与操作系统调优的目标,是让输入管道、GPU 任务提交和通信辅助线程更稳定、更高效地运行,从而减少 GPU 等待时间。
2.1 NUMA 感知与 CPU 绑定
2.1.1 什么是 NUMA
在笔记本和小型台式机中,通常只有一个 CPU 和一组内存,所有内存访问速度相同——这称为 UMA(Uniform Memory Access,统一内存访问)。但大多数数据中心服务器是 NUMA(Non-Uniform Memory Access,非统一内存访问)系统:每个 CPU 拥有自己的内存控制器,直接连接一部分本地内存。每个 CPU 可以通过自己的内存控制器快速访问本地内存,也可以通过节点间链路访问另一个 CPU 的远程内存,但本地和远程内存的访问延迟和带宽不同 ,这就是”非统一”的含义。
NUMA 节点 是 CPU、GPU、NIC 和内存的逻辑分组 ,这些组件在物理上彼此靠近。访问同一 NUMA 节点内的资源比跨节点访问要快得多。例如,一个运行在 NUMA 节点 0 的 CPU 上的进程如果需要访问 NUMA 节点 1 的 GPU,数据就必须通过节点间链路传输,会产生更高的延迟。实际上,跨 NUMA 节点的内存访问延迟几乎可以翻倍 。
2.1.2 查看系统 NUMA 拓扑
你可以用以下两个命令结合查看系统的完整 NUMA 拓扑:
# 查看 CPU/内存的 NUMA 分布
numactl -H
# 查看 GPU 与 NUMA 节点的对应关系
nvidia-smi topo -m
numactl -H 的输出示例(4 个 NUMA 节点的服务器):
available: 4 nodes (0-3) # 系统有 4 个 NUMA 节点
node 0 cpus: 0-15 64-79 # 节点 0 拥有这些 CPU 核心
node 0 size: 128827 MB # 节点 0 的本地内存大小
node 1 cpus: 16-31 80-95
node 1 size: 129012 MB
node 2 cpus: 32-47 96-111
node 2 size: 128968 MB
node 3 cpus: 48-63 112-127
node 3 size: 128993 MB
node distances: # 节点间相对访问距离(根据 BIOS 写入 ACPI SLIT 表)
node 0 1 2 3
0: 10 12 12 12
1: 12 10 12 12
2: 12 12 10 12
3: 12 12 12 10
nvidia-smi topo -m 的输出示例(5 个 GPU 的服务器):
GPU0 GPU1 GPU2 GPU3 GPU4 CPU Affinity NUMA Affinity
GPU0 X NV4 NV4 SYS NV4 48-63,112-127 3
GPU1 NV4 X NV4 SYS NV4 32-47,96-111 2
GPU2 NV4 NV4 X SYS NV4 16-31,80-95 1
GPU3 SYS SYS SYS X PHB 0-15,64-79 0
GPU4 NV4 NV4 NV4 PHB X 0-15,64-79 0
Legend:
X = Self
SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
PIX = Connection traversing at most a single PCIe bridge
NV# = Connection traversing a bonded set of # NVLinks
矩阵区域 (GPU 之间的交叉格)— GPU 间的互连类型,与 Legend 对应:
X = 自己跟自己
SYS = 经过 PCIe + 跨 NUMA 节点的 CPU 互连,如 QPI/UPI(最慢)
NODE = 经过 PCIe + 同一 NUMA 节点内的 Host Bridge 互连
PHB = 经过 PCIe Host Bridge,即经过 CPU(同一 NUMA 节点内)
PXB = 经过多个 PCIe bridge,但不经过 Host Bridge
PIX = 经过最多一个 PCIe bridge(同一 PCIe switch 下)
NV# = 通过 # 条 NVLink 直连(最快,如 NV4 = 4 条 NVLink)
CPU Affinity 列 — 该 GPU 关联的 CPU 核心编号,如 GPU0 对应核心 48-63 和 112-127
NUMA Affinity 列 — 该 GPU 属于哪个 NUMA 节点,这就是你做 numactl 绑定时需要的值
2.1.3 使用 numactl 进行 CPU pinning
确认了 GPU 所在的 NUMA 节点后,需要显式指定 NUMA 亲和性(NUMA affinity)——将进程或线程分配到与 GPU 相同 NUMA 节点上的 CPU 核心。这种做法称为 CPU pinning 。可以使用 numactl 实现,两个关键参数:
--cpunodebind=<node> — 将进程的 CPU 线程 限制在指定 NUMA 节点的核心上运行,防止 OS 调度器把线程迁移到其他节点。
--membind=<node> — 将进程的内存分配 限制在指定 NUMA 节点的本地 RAM 上,防止内存被分配到远程节点。
两者配合使用,确保 CPU 执行和内存访问都在 GPU 所在的本地 NUMA 节点内完成。
# 语法:numactl --cpunodebind=<node> --membind=<node> <command>
# 假设 GPU 4 连接在 NUMA 节点 0 上,将 CPU 和内存也绑定到节点 0
numactl --cpunodebind=0 --membind=0 python train.py --gpu 4
2.1.4 验证 NUMA 绑定的性能影响
可以使用下面的脚本对比 NUMA 绑定对 CPU→GPU 数据拷贝带宽的影响,保存为 numa_bench.sh 后运行:
# 参数:<GPU 编号> <GPU 所在的 NUMA 节点> <远端 NUMA 节点>
# 这里的 4 0 3 对应上面 topo 表中 GPU 4 在 NUMA 节点 0 的情况,
# 节点 3 是离 GPU 4 最远的 NUMA 节点。请根据你自己的 `nvidia-smi topo -m` 输出替换。
bash numa_bench.sh 4 0 3
numa_bench.sh:NUMA 绑定性能对比脚本
#!/bin/bash
# NUMA 绑定性能对比:纯 H2D 拷贝测试
# 用法:bash numa_bench.sh <GPU 编号> <GPU 所在 NUMA 节点> <远端 NUMA 节点>
# 示例:bash numa_bench.sh 4 0 3
#
# 只测 CPU→GPU 数据传输带宽,直观对比 NUMA 绑定的影响。
# 内存需求:CPU ~2 GB,GPU ~1 GB
GPU = ${1 :- 4}
LOCAL_NODE = ${2 :- 0}
REMOTE_NODE = ${3 :- 3}
SCRIPT = '
import torch, time
device = torch.device(f"cuda:GPU_ID")
# 512 MB pageable CPU 内存(不带 pin_memory,更能体现 NUMA 差异)
host = torch.randn(128_000_000, dtype=torch.float32)
buf = torch.empty_like(host, device=device)
# 200 轮纯 H2D 拷贝
start = time.perf_counter()
for _ in range(200):
buf.copy_(host)
torch.cuda.synchronize()
elapsed = time.perf_counter() - start
ms = elapsed / 200 * 1000
bw = 512 / (elapsed / 200) / 1000 # GB/s
print(f" 200 copies: {elapsed:.3f}s | {ms:.2f} ms/copy | {bw:.1f} GB/s")
'
RUN_SCRIPT = "${ SCRIPT // GPU_ID / $GPU }"
echo "=== NUMA H2D Bandwidth: GPU $GPU (512 MB per copy) ==="
echo ""
echo "① Local NUMA node $LOCAL_NODE (same node as GPU $GPU ):"
numactl --cpunodebind= $LOCAL_NODE --membind= $LOCAL_NODE \
python -c " $RUN_SCRIPT "
echo ""
echo "② Remote NUMA node $REMOTE_NODE (different node from GPU $GPU ):"
numactl --cpunodebind= $REMOTE_NODE --membind= $REMOTE_NODE \
python -c " $RUN_SCRIPT "
在我们的测试机器上输出如下: === NUMA H2D Bandwidth: GPU 4 (512 MB per copy) ===
① Local NUMA node 0 (same node as GPU 4):
200 copies: 8.285s | 41.42 ms/copy | 12.4 GB/s
② Remote NUMA node 3 (different node from GPU 4):
200 copies: 8.310s | 41.55 ms/copy | 12.3 GB/s
这台机器(AMD EPYC 7742)上两者差异很小(12.4 vs 12.3 GB/s,仅 ~0.3%),因为 4 个 NUMA 节点在同一个 CPU socket 内通过 Infinity Fabric 互连,节点间距离只有 10 vs 12,跨节点访问的额外开销很低。在真正的双 socket 系统上(节点间距离 10 vs 20+),差异会更显著。
2.1.5 在 PyTorch DataLoader 中绑定 NUMA
书中给出了一个完整的 Python 示例(约 100 行),配套代码 ch03/bind_numa_affinity.py 是其完整实现。整个流程分三步:
第一步:查询 GPU 所在的 NUMA 节点
启动时,进程需要知道当前 GPU 连接在哪个 NUMA 节点上。get_gpu_numa_node() 按优先级依次尝试四种方式:
NVML 直接查询 — 调用 nvmlDeviceGetNumaNodeId() ,直接获取 GPU 的 NUMA 节点编号。仅适用于 GPU 本身是 NUMA 节点的平台(如 Grace Hopper/Grace Blackwell 超级芯片)
验证:用 pynvml 调用 nvmlDeviceGetNumaNodeId
python -c "
import pynvml
pynvml.nvmlInit()
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
try:
print(f'GPU 0 NUMA node: {pynvml.nvmlDeviceGetNumaNodeId(handle)}')
except Exception as e:
print(f'nvmlDeviceGetNumaNodeId not available: {e}')
"
在传统架构(A100、H100 等)中,GPU 是一个 PCIe 设备,它的显存(HBM)对 Linux 来说是”设备内存”,不在系统 NUMA 拓扑里——numactl -H 看到的只有 CPU 和 CPU 内存的 NUMA 节点,看不到 GPU: # 传统架构(A100)的 numactl -H:只有 CPU 内存的节点
available: 4 nodes (0-3)
node 0 size: 128827 MB ← CPU DRAM
node 1 size: 129012 MB ← CPU DRAM
...
在 Grace Hopper(GH200)架构中,CPU 和 GPU 通过 NVLink-C2C 连接,GPU 的 HBM 被 Linux 内核直接注册为一个 NUMA 节点。numactl -H 会多出一个节点,那个节点的”内存”就是 GPU 的 HBM: # Grace Hopper(GH200)的 numactl -H:多出 GPU HBM 的节点
available: 3 nodes (0-2)
node 0 size: 480000 MB ← CPU LPDDR5X
node 1 size: 98304 MB ← GPU HBM3
...
参考资料:
NVML CPU 亲和性推断 — 如果上一步不可用,调用 nvmlDeviceGetCpuAffinity() 获取该 GPU 关联的 CPU 掩码(bitmask),然后与 sysfs 中的 CPU→NUMA 映射表对照,取出现次数最多的 NUMA 节点
验证:用 nvidia-smi topo -m 查看 CPU Affinity 列
# CPU Affinity 列就是 nvmlDeviceGetCpuAffinity() 的可读版本
nvidia-smi topo -m
输出示例: GPU0 GPU1 GPU2 GPU3 GPU4 CPU Affinity NUMA Affinity
↑ GPU 推荐的 ↑ GPU 所在的
CPU 核心列表 NUMA 节点
GPU0 X NV4 NV4 SYS NV4 48-63,112-127 3
GPU1 NV4 X NV4 SYS NV4 32-47,96-111 2
GPU2 NV4 NV4 X SYS NV4 16-31,80-95 1
GPU3 SYS SYS SYS X PHB 0-15,64-79 0
GPU4 NV4 NV4 NV4 PHB X 0-15,64-79 0
↑ 矩阵区域:GPU 之间的互连类型
sysfs 内核接口 — 如果 NVML 完全不可用,直接读 /sys/bus/pci/devices/<PCI_ID>/numa_node,这是 Linux 内核为每个 PCI 设备维护的 NUMA 节点信息
验证:从 sysfs 读取每个 GPU 的 NUMA 节点
# 注意两个格式差异:
# 1. nvidia-smi 输出 8 位域号(00000000:),sysfs 用 4 位(0000:)
# 2. nvidia-smi 输出大写字母(如 0000:C1:00.0),sysfs 用小写(0000:c1:00.0)
for GPU in 0 1 2 3 4 ; do
GPU_PCI = $( nvidia-smi --query-gpu=pci.bus_id --format=csv,noheader -i $GPU \
| sed 's/^0000//' | tr '[:upper:]' '[:lower:]' )
NODE = $( cat /sys/bus/pci/devices/ $GPU_PCI /numa_node )
echo "GPU $GPU -> PCI= $GPU_PCI -> NUMA= $NODE "
done
在我们的测试机器(AMD EPYC 7742 + 5 张 GPU)上输出如下: GPU 0 -> PCI=0000:01:00.0 -> NUMA=3 # GPU 0 在 NUMA 节点 3
GPU 1 -> PCI=0000:47:00.0 -> NUMA=2 # GPU 1 在 NUMA 节点 2
GPU 2 -> PCI=0000:81:00.0 -> NUMA=1 # GPU 2 在 NUMA 节点 1
GPU 3 -> PCI=0000:c1:00.0 -> NUMA=0 # GPU 3 和 GPU 4 都在 NUMA 节点 0
GPU 4 -> PCI=0000:c2:00.0 -> NUMA=0
可以看到 5 张 GPU 分布在 4 个 NUMA 节点上,其中 GPU 3 和 GPU 4 共享节点 0。这和前面 nvidia-smi topo -m 输出的 NUMA Affinity 列一致。
当前进程策略兜底 — 如果以上都失败,运行 numactl --show 读取当前进程的 preferred node。例如如果外部已经用 numactl 命令启动了脚本(如 numactl --cpunodebind=0),进程会继承这些策略,preferred node 就是正确的值
验证:对比有无 numactl 指定 NUMA 策略时的 preferred node
# 不包裹:preferred node 是 current(OS 自己决定)
numactl --show | grep "preferred node"
# 用 numactl 显式指定 NUMA 策略后:preferred node 变成指定的节点
numactl --cpunodebind=0 --membind=0 numactl --show | grep "preferred node"
def get_gpu_numa_node ( device_index : int ) -> int :
for resolver in (
_gpu_node_from_nvml, # 方式 1 + 2:NVML 直接查询 / CPU 亲和性推断
_gpu_node_from_sysfs, # 方式 3:sysfs 内核接口
):
node = resolver(device_index)
if node is not None :
return node
_, fallback = _current_numa_policy() # 方式 4:当前进程策略兜底
return fallback
第二步:绑定主训练进程的 CPU 和内存
bind_process_to_node() 做两件事——限制 CPU 核心 + 限制内存分配节点。其中 _libnuma 是 Linux 系统库 libnuma.so 的 Python 绑定(通过 ctypes.CDLL("libnuma.so") 加载),numactl 命令行工具底层也是调用它。在 Python 进程内部需要用它来动态设置 NUMA 内存策略,因为 numactl 只能在启动进程时从外部指定策略,无法在已运行的子进程内部重新绑定:
def bind_process_to_node ( node : int ) -> List[ int ]:
cpus = _cpus_for_node(node) # 从 sysfs 读取该节点的 CPU 列表
psutil.Process(os.getpid()).cpu_affinity(cpus) # 限制 CPU 线程只跑在这些核心上
if _HAS_LIBNUMA and _libnuma is not None : # 如果 libnuma 可用
_libnuma.numa_run_on_node(node) # 设置 libnuma 运行节点
_libnuma.numa_set_preferred(node) # 设置内存优先从该节点分配
print ( f "PID { os.getpid() } bound to NUMA node { node } (CPUs= { cpus } )" )
return cpus
第三步:在每个 DataLoader worker 中重新绑定
worker 是独立的子进程,不一定继承主进程的 NUMA 策略(尤其是 spawn 模式下)。通过 worker_init_fn 在每个 worker 启动时显式重新绑定:
def worker_init_fn ( worker_id : int , node : int , cpus : List[ int ]) -> None :
# 注意:这里不能调用任何 torch.cuda.* API,否则 worker 会初始化 CUDA context
psutil.Process(os.getpid()).cpu_affinity(cpus)
if _HAS_LIBNUMA and _libnuma is not None :
_libnuma.numa_run_on_node(node)
_libnuma.numa_set_preferred(node)
print ( f "Worker { worker_id } (PID= { os.getpid() } ) bound to NUMA node { node } " )
把三步串起来:
from functools import partial
gpu_node = get_gpu_numa_node(local_rank) # ① 查询该 GPU 所在的 NUMA 节点编号
cpus = bind_process_to_node(gpu_node) # ② 将主进程的 CPU 和内存绑定到该节点
dataloader = DataLoader(
dataset,
batch_size = 32 ,
num_workers = 4 ,
pin_memory = True ,
persistent_workers = True , # 避免 worker 重新 fork 丢失亲和性
worker_init_fn = partial(worker_init_fn, node = gpu_node, cpus = cpus), # ③ 绑定 worker
prefetch_factor = 2 ,
)
这样主进程、DataLoader worker、GPU 三者都在同一个 NUMA 节点上,数据从磁盘 → CPU 内存 → GPU 全程本地访问。
# 单 GPU 模式:自动检测 GPU 0 的 NUMA 节点并绑定
cd ai-performance-engineering/code
python -m ch03.bind_numa_affinity
WARNING: Running in single-process mode (distributed environment not detected)
PID 1264908 bound to NUMA node 0 (CPUs=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79])
Worker 0 (PID=1265037) bound to NUMA node 0
Worker 1 (PID=1265038) bound to NUMA node 0
[OK] NUMA binding sanity test passed (loss=2.4550)
# 多 GPU DDP 模式:--nproc_per_node 指定在本机启动几个进程(通常等于 GPU 数量),
# 每个进程分配一张 GPU 并自动绑定到该 GPU 的 NUMA 节点
torchrun --nproc_per_node=4 -m ch03.bind_numa_affinity
PID 1265546 bound to NUMA node 0 (CPUs=[0, 1, ..., 15, 64, ..., 79])
PID 1265547 bound to NUMA node 0 (CPUs=[0, 1, ..., 15, 64, ..., 79])
PID 1265548 bound to NUMA node 0 (CPUs=[0, 1, ..., 15, 64, ..., 79])
PID 1265549 bound to NUMA node 0 (CPUs=[0, 1, ..., 15, 64, ..., 79])
Worker 0 (PID=1265614) bound to NUMA node 0
Worker 1 (PID=1265618) bound to NUMA node 0
Worker 2 (PID=1265622) bound to NUMA node 0
Worker 3 (PID=1265626) bound to NUMA node 0
...
step=0 loss=2.3125
4 个主进程(每个对应一张 GPU)和各自的 DataLoader worker 都被绑定到了 NUMA 节点 0。在多 NUMA 节点的系统上,不同 rank 会绑定到各自 GPU 对应的不同节点。
numactl 的策略在 fork 子进程中可继承,但 spawn 或 exec 模式下不会继承 。Python 框架管理的 worker 进程必须显式重新设置 CPU 和内存策略。
性能影响: 仅消除跨 NUMA 流量和 CPU 核心迁移,训练吞吐量就可提升 5%–10% ,同时减少性能抖动。
验证方式:运行配套代码的 NUMA benchmark
配套代码里,ch03/numa_topology_script.sh 用于查看 GPU/NUMA 拓扑,ch03/bind_numa_affinity.py 演示进程绑定;H2D 传输对比由 baseline_pageable_copy.py 和 optimized_pageable_copy.py 承担。运行对比: cd ai-performance-engineering/code
python -m ch03.compare
你也可以用 nvidia-smi topo -m 查看你的系统拓扑,再用 numactl --show 确认当前进程的绑定状态。
2.2 页锁定内存(Pinned memory)
创建 CPU tensor 时,tensor 内容会被放入内存。这里的内存指的是由 MMU(内存管理单元,Memory Management Unit)管理的一套虚拟内存(virtual memory)抽象:
RAM 与交换空间(swap space) 共同组成虚拟内存。它让程序看到的可用空间大于单独的物理 RAM。
普通 CPU tensor 默认是 pageable 的 。Tensor 内容会被切成 page,这些 page 可以位于 RAM,也可以位于交换空间。
Page fault :通常,当程序试图访问不在 RAM 中的 page 时,就会发生 page fault。此时,OS 会将该 page 调入 RAM。相应地,为了给新 page 腾出空间,OS 可能不得不将另一个 page 调出 RAM。
Pinned memory 的作用是让这块内存稳定留在 RAM 中,不被换出到磁盘。
Pinned memory 也叫 page-locked 或 non-pageable memory 。它不能被换出到磁盘,因此访问时间更快、更可预测。
代价是容量更受限制 。Pinned memory 会减少系统可自由分页的内存,不能无限制使用。
CUDA 从 CPU 向 GPU 拷贝 tensor 时,对 pageable memory 和 pinned memory 的处理方式不同:
如果源数据在 page-locked memory 中 ,device 可以直接访问 RAM 中地址稳定的数据。
如果源数据在 pageable memory 中 ,相关 page 必须先回到 RAM,然后才能发送到 GPU。更准确地说,CUDA 会先为 pageable data 创建一份 page-locked copy,再执行 CPU → GPU transfer。
下图展示了这条路径里的三类内存区域:
Pinned memory(也称 page-locked 或 nonpageable memory)是一类不能被换出到磁盘的内存。
浅黄色区域:Pageable memory 。普通 CPU 内存,属于虚拟内存体系。它的页可能在 RAM,也可能被换出到 Disk,操作系统也可能移动这些页。
浅绿色区域:Pinned memory / page-locked memory 。仍然是 CPU RAM,但这些页被锁住,不能被 swap,也不能被随意迁移。
图右侧区域:GPU memory 。CUDA tensor 真正所在的显存。
图中的小块表示 tensor 当前所在的位置:
蓝色和紫色小块 :还在 pageable memory 里的 paged tensor。
红色小块 :已经 pin 住的 page-locked tensor。
橙色小块 :GPU 上的 CUDA tensor。
图中的箭头表示数据移动:
蓝色箭头 :pin_memory() 把 pageable tensor 锁进 pinned memory。
红色箭头 :.to("cuda") 把 pinned tensor 拷贝到 GPU。
在 PyTorch 的数据加载中,给 DataLoader 传入 pin_memory=True 会自动把取回的 tensor 放到 pinned memory 中,从而让它们更快地传输到支持 CUDA 的 GPU。
此外,一旦 tensor 位于 pinned memory 中,就可以使用异步 GPU copy。只需要在 .to() 或 .cuda() 调用中额外传入 non_blocking=True。这可以用来让数据传输与计算重叠。
import torch
from torch.utils.data import DataLoader
device = torch.device( "cuda" )
loader = DataLoader(
train_dataset,
batch_size = 64 ,
pin_memory = True ,
)
for x, y in loader:
x = x.to(device, non_blocking = True )
y = y.to(device, non_blocking = True )
参考资料:
2.3 透明大页(Transparent Hugepages)
Linux 内存管理通常使用 4 KB 的页;但当进程使用数十或数百 GB 内存时,例如深度学习数据集、预取批次、模型参数等,管理数以百万计的小页会非常低效。
透明大页(Transparent Hugepages,THP)是一种自动使用大页的机制。大页——2 MB 甚至 1 GB 的页——可以通过增大内存块来减少虚拟内存管理的开销。主要好处是减少缺页中断,并减轻 TLB(translation lookaside buffer)的压力。
2.3.1 TLB 地址转换
TLB 是 CPU 用来把虚拟地址映射到物理地址的缓存。页更少、更大时,同样数量的 TLB 条目可以覆盖更多内存,从而减少 TLB miss。
TLB hit 一个虚拟内存地址(virtual memory address)进入后,需要被翻译成物理地址(physical address)。第一步总是把虚拟地址拆成两部分:虚拟页号(virtual page number)和页偏移(page offset)。页偏移由虚拟地址的最后几位组成。偏移位不会被翻译,而是直接传递到物理内存地址中;因为页表只负责把虚拟页号映射到物理页号,页内的相对位置保持不变。 因此,偏移量会直接映射到物理内存层,而虚拟页号则与 TLB 中已有的标签相对应。这样一来,MMU 无需查询全局内存,就能立即知道该访问哪个物理内存页。 在上图的例子中,虚拟页号在 TLB 中被找到,并立即转换成物理页号。 TLB miss 当虚拟页码在 TLB 中找不到时,就会发生所谓的“TLB 未命中”情况。此时,TLB 必须查询系统的物理内存,以确定对应的物理页码是多少。与 TLB 命中相比,这种查询方式会导致更高的延迟。如果 TLB 已满且发生 TLB 未命中,那么 TLB 中最近最少使用的数据条目会被清除,新的数据条目则会取而代之。在下面的例子中,虚拟页码在 TLB 中不存在,因此 TLB 必须查询物理内存来获取该页码。 参考资料:How is Virtual Memory Translated to Physical Memory?
2.3.2 THP 配置
收益幅度 :Hugepages 通常带来的是中等幅度收益,吞吐提升常见在 3%–5% 左右。
Pinned memory 限制 :使用大块 pinned memory 时,需要把 ulimit -l 调高或设为 unlimited;如果这个限制过低,pin memory 可能失败,进而回退到可交换内存,或者触发 OOM。
推理延迟风险 :THP 的后台 compaction 可能引入不可预测的暂停,这对延迟敏感的 LLM 推理是高风险。
Defrag 策略 :defrag 控制内核是否为了分配 THP 做内存整理。THP 需要连续的物理内存,如果当前内存碎片较多,内核可能触发 compaction 来凑出连续空间,这个过程会增加延迟抖动。
训练与推理取舍 :Linux 默认会尽可能自动分配 2 MB 的 THP。吞吐优先的训练 workload 通常启用 THP;延迟优先的推理 workload 通常完全禁用。
# 查看 THP 当前策略和 defrag 策略
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
# 启用 THP
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
# 禁用 THP
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
# 设置 THP defrag 策略,减少同步 compaction 对延迟的影响
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
# 查看系统支持的显式 hugepage 大小
ls /sys/kernel/mm/hugepages/
# 查看 hugepage 当前状态
grep -E "HugePages|Hugepagesize|Hugetlb" /proc/meminfo
在内存池非常大的场景中,例如用于 I/O 的预分配固定缓冲区,也可以考虑使用显式 hugepages(HugeTLB),例如 vm.nr_hugepages 或 hugetlbfs,以获得更可预测的性能。它和 THP 不同:THP 由内核自动尝试分配大页;显式 hugepages 需要提前预留,并由应用或 runtime 明确使用。
# 查看默认 hugepage 大小
grep Hugepagesize /proc/meminfo
# 预留 1024 个 hugepages
sudo sysctl -w vm.nr_hugepages= 1024
# 挂载 hugetlbfs,供支持 HugeTLB 的应用显式使用
sudo mkdir -p /mnt/huge
sudo mount -t hugetlbfs none /mnt/huge
# 验证 hugepages 预留和使用情况
grep -E "HugePages_Total|HugePages_Free|Hugepagesize|Hugetlb" /proc/meminfo
2.4 调度器与中断亲和性
CPU 侧抖动通常来自三个方面:
线程调度 :关键数据管道线程或通信辅助线程如果长时间排队、频繁被抢占,就会形成 CPU 侧调度抖动,进而让 GPU 因等待输入或通信而空闲。
CPU 隔离 :这些线程如果和其他 workload 混跑在同一组 CPU 上,容易争抢 CPU core、cache 和内存带宽,响应时间会变得不稳定。
中断亲和性 :GPU/NIC 中断请求(Interrupt Request,IRQ)如果由远离设备的 CPU 处理,或者频繁打断关键线程,会增加额外延迟。
2.4.1 线程调度
在繁忙的系统上,需要确保数据管道线程等重要线程不会被频繁中断。Linux 默认使用完全公平调度器(Completely Fair Scheduler,CFS) ,对大多数情况都有效。
但如果有一个对延迟非常敏感、例如需要为 GPU 提供数据的线程,可以考虑对该线程使用实时的**先进先出(FIFO)或 轮转(RR)**优先级调度。这样可以确保高优先级线程在不被普通优先级线程抢占的情况下运行。
完全公平调度器(Completely Fair Scheduler,CFS) 面向普通线程,目标是让 runnable task 获得相对公平的 CPU 时间。它会跟踪每个 task 的虚拟运行时间(virtual runtime,vruntime);运行得越少的 task,vruntime 越小,越容易被选中。任务进入 runnable 状态后,会被放入红黑树(red-black tree)。nice 值通常范围是 -20 到 19,数值越小,普通线程的调度优先级越高。每个 nice 值对应一个权重,nice=0 的权重是 1024。Linux 使用预定义的 nice 权重表,相邻 nice 级别大约相差 1.25 倍:nice 每降 1,权重约增加 25%;nice 每升 1,权重约减少 20%。 nice 权重如下: nice weight -53121-42501-31991-21586-112770102418202655352644235335
关键公式可以近似写成: vruntime 增量 = 实际运行时间 × (nice=0 的权重 / 当前线程权重)
例如两个线程都实际运行 10 ms: nice=0: vruntime 增量 = 10 ms × (1024 / 1024) = 10 ms
nice=-1:vruntime 增量 = 10 ms × (1024 / 1277) ≈ 8.0 ms
低 nice 值线程的权重更大,同样运行 10 ms,vruntime 增加更少,因此更容易保持在红黑树左侧,也更早再次被 CFS 选中运行。
先查看线程当前运行在哪些 CPU 上,以及调度策略、实时优先级和 nice 值: ps -L -p < pi d > -o pid,tid,psr,policy,rtprio,ni,comm
示例输出: PID TID PSR POL RTPRIO NI COMMAND
18421 18421 12 TS - 0 python
18421 18488 14 TS - 0 pt_data_worker
18421 18503 16 FF 10 - nccl-helper
字段含义:
PSR:线程当前运行过的 CPU 编号。
POL:调度策略。TS 是普通 CFS 时间共享策略,FF 对应 SCHED_FIFO,RR 对应 SCHED_RR。
RTPRIO:实时优先级。普通 CFS 线程通常显示为 -。
NI:nice 值,只影响普通 CFS 线程的权重。
普通 CFS 线程通常先通过 nice 调整权重: # 调高普通线程优先级,负 nice 值通常需要 sudo
sudo renice -n -5 -p < pi d >
对极少数延迟敏感的线程,可以用 chrt 设置实时 FIFO 或 RR 策略: # 设置为 SCHED_FIFO,实时优先级为 10
sudo chrt -f -p 10 < ti d >
# 设置为 SCHED_RR,实时优先级为 10
sudo chrt -r -p 10 < ti d >
# 查看某个线程当前调度策略
chrt -p < ti d >
chrt -p <tid> 的输出示例:pid 18503's current scheduling policy: SCHED_FIFO
pid 18503's current scheduling priority: 10
不过要谨慎使用,因为实时线程如果管理不当,可能会使其他进程饥饿。实际中,如果已经将关键线程绑定到专用 CPU core,通常不需要再调整实时线程优先级。
2.4.2 CPU 隔离
CPU 隔离的目标,是将关键输入线程 、通信辅助线程与其他工作负载尽量分开运行,减少它们受到调度器负载均衡、内核工作队列、定时器和中断请求等系统噪声的干扰。
临时绑定 :taskset 适合排查和实验,可以把进程或线程绑定到指定 CPU。
生产隔离 :cgroup cpuset 更适合生产环境。cpuset.cpus 约束任务可运行的 CPU,cpuset.mems 约束可使用的 NUMA memory node。
容器约束 :容器环境中可以用 docker run --cpuset-cpus 固定 CPU 范围,用 --cpuset-mems 固定 memory node。
启动参数 :isolcpus、nohz_full、irqaffinity 可以做更强的启动期隔离,但运行期调整不如 cgroup 灵活,适合非常明确的低延迟节点。
# 临时绑定:把已有进程绑定到 CPU 8-15
sudo taskset -cp 8-15 < pi d >
# 生产隔离:创建 cgroup,并限制 CPU / NUMA memory node
# 下面假设 CPU 8-15 属于 NUMA node 0,可以使用 lscpu -e=CPU,NODE,SOCKET,CORE 命令确认
sudo mkdir -p /sys/fs/cgroup/gpu-workload
# 只允许该 cgroup 内的任务运行在 CPU 8-15 上
echo 8-15 | sudo tee /sys/fs/cgroup/gpu-workload/cpuset.cpus
# 只允许该 cgroup 使用 NUMA node 0 上的 memory
echo 0 | sudo tee /sys/fs/cgroup/gpu-workload/cpuset.mems
# 把目标进程加入这个 cgroup,使其受到上面的 CPU / memory node 约束
echo < pi d > | sudo tee /sys/fs/cgroup/gpu-workload/cgroup.procs
# 查看 cgroup 实际生效的 CPU / memory node
cat /sys/fs/cgroup/gpu-workload/cpuset.cpus.effective
cat /sys/fs/cgroup/gpu-workload/cpuset.mems.effective
# 容器约束:容器只使用 CPU 8-15 和 NUMA node 0
docker run --cpuset-cpus=8-15 --cpuset-mems=0 < imag e >
如果需要比 cgroup 更强的启动期隔离,可以通过内核启动参数配置 isolcpus、nohz_full、rcu_nocbs 和 irqaffinity。这些参数需要写入 /etc/default/grub 中的 GRUB_CMDLINE_LINUX,更新 GRUB 并重启后生效。 # 备份 GRUB 配置
sudo cp /etc/default/grub /etc/default/grub.bak
# 编辑 /etc/default/grub
sudo editor /etc/default/grub
# 在 /etc/default/grub 中修改或追加这一项
GRUB_CMDLINE_LINUX = "isolcpus=8-15 nohz_full=8-15 rcu_nocbs=8-15 irqaffinity=0-7"
# Ubuntu / Debian 更新 GRUB 配置并重启
sudo update-grub
sudo reboot
# 重启后确认启动参数已经生效
cat /proc/cmdline
参数含义可以这样理解:
isolcpus=8-15:让普通调度器尽量不要把普通任务放到 CPU 8-15 上。
nohz_full=8-15:减少这些 CPU 上的周期性调度时钟中断。
rcu_nocbs=8-15:把 RCU callback 从这些 CPU 上移走,减少内核后台干扰。
irqaffinity=0-7:默认把 IRQ 放到 CPU 0-7,让 CPU 8-15 更干净。
注意 isolcpus=8-15 不会自动把训练/推理进程放到 CPU 8-15。它只是减少普通系统任务对这些 CPU 的占用。真正把 workload 放进去,还是要配合 taskset、cgroup cpuset 或容器的 --cpuset-cpus。
2.4.3 中断亲和性
IRQ(Interrupt Request)是硬件或内核子系统通知 CPU 处理事件的一种机制。CPU 收到中断后,会暂停当前正在执行的普通任务,转去执行对应的中断处理逻辑。
常见 IRQ 类型包括:
设备中断 :来自 NIC、GPU、NVMe 等硬件设备。例如网卡收到数据、发送完成、RDMA 操作完成事件,或者 GPU 任务完成、错误和事件通知。
定时器中断 :来自系统 timer,用于调度器计时、时间片管理和周期性内核任务。
核间中断(Inter-Processor Interrupt,IPI) :一个 CPU core 向另一个 CPU core 发送的中断,常见于 TLB shootdown、调度唤醒等场景。
IRQ affinity :控制某个 IRQ 由哪些 CPU core 处理。
中断亲和性的调优目标,是把 GPU/NIC IRQ 绑定到设备所在 NUMA node 上的 CPU core,避免由远端 NUMA node 处理设备中断。否则,设备在 NUMA node 0 上触发中断,却由另一个 NUMA node 上的 CPU 处理时,会引入跨节点通信和缓存一致性流量。
2.5 虚拟内存与 Swap
不言而喻,应尽量避免发生内存交换。一旦进程的部分内存被换出到磁盘,性能往往会出现灾难性的、跨数量级的下降。GPU 程序通常会分配大量主机内存用于数据缓存;如果操作系统将其中一部分换出到磁盘,那么当 GPU 相关流程再次需要访问这些数据时,就会遭遇巨大的延迟。
vm.swappiness 控制 Linux 在内存压力下使用 swap 的倾向,取值通常是 0 到 100。数值越高,内核越倾向于把匿名页换出到 swap;数值越低,内核越倾向于把数据留在 RAM 中。建议设置 vm.swappiness=0,它告诉 Linux 除非遇到极端内存压力,否则应尽量避免 swap。 需要强调的是,vm.swappiness=0 不等于完全禁用 swap;它只是降低内核使用 swap 的倾向,在极端内存压力下仍然可能发生 swap。 如果需要完全关闭 swap,需要使用 swapoff -a,或者在 cgroup v2 中设置 memory.swap.max=0。
# 查看当前 swappiness
sysctl vm.swappiness
# 示例输出:当前值是 60
# vm.swappiness = 60
# swappiness 越高,Linux 越倾向于使用 swap;0 表示除非极端内存压力,否则尽量避免 swap
# 临时把 swappiness 设置为 0,重启后恢复
sudo sysctl -w vm.swappiness= 0
# 查看当前启用的 swap 设备或文件
swapon --show
# 示例输出:当前有一个 64G swapfile,但 USED 为 0
# NAME TYPE SIZE USED PRIO
# /swapfile file 64G 0B -2
# 临时关闭所有 swap 设备和文件,重启后按系统配置恢复
sudo swapoff -a
# 如需永久禁用,编辑 /etc/fstab,把 swap 条目注释掉
sudo vim /etc/fstab
# 例如把这一行:
# /swapfile none swap sw 0 0
# 改成:
# # /swapfile none swap sw 0 0
# 再次查看,若无输出,表示当前没有启用的 swap
swapon --show
# 查看内存和 swap 汇总
free -m
# 示例输出:重点看 Swap 行,total/used 都为 0 表示当前没有 swap
# total used free shared buff/cache available
# Mem: 257000 80000 20000 1024 157000 170000
# Swap: 0 0 0
# 持续观察 swap in / swap out
vmstat 1
# 示例输出:重点看 si / so,长期为 0 表示没有 swap in / swap out
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 1 0 0 204800 100000 800000 0 0 0 12 2000 5000 10 3 87 0 0
# 查看目标进程是否发生 swap,以及 locked/pinned memory 情况
grep -E "VmRSS|VmSwap|VmLck|VmPin" /proc/ < pi d > /status
# 示例输出:VmSwap 为 0 kB 表示该进程当前没有被换出到 swap
# VmRSS: 83886080 kB
# VmLck: 0 kB
# VmPin: 0 kB
# VmSwap: 0 kB
# 查看 cgroup v2 中任务当前内存和 swap 用量,单位是 bytes
cat /sys/fs/cgroup/ < jo b > /memory.current
cat /sys/fs/cgroup/ < jo b > /memory.swap.current
# 示例输出:memory.current 是当前内存用量;memory.swap.current 为 0 表示该 cgroup 没有使用 swap
# 85899345920
# 0
# 对某个 cgroup 禁止 swap
echo 0 | sudo tee /sys/fs/cgroup/ < jo b > /memory.swap.max
# 确认 swap 上限已经设置为 0
cat /sys/fs/cgroup/ < jo b > /memory.swap.max
# Docker 示例:固定 CPU / NUMA node,并让容器没有额外 swap 空间
docker run --cpuset-cpus=8-15 --cpuset-mems=0 --memory=128g --memory-swap=128g < imag e >
2.6 文件系统缓存
对于大型训练任务,最佳实践是频繁将 checkpoint 写入磁盘,以便在需要时能从已知的良好 checkpoint 重新启动失败任务。然而,在进行 checkpoint 写入时,大量数据的突发写入可能会填满操作系统 page cache 并导致停顿。
常见解决方案包括:
继续使用 page cache,但降低冲击 :
调整写回阈值 :调整 dirty page 阈值,控制内核什么时候开始后台写回,以及什么时候阻塞写入进程。
异步写 checkpoint :使用 PyTorch Distributed Checkpoint(DCP)async_save ,把 checkpoint 写入从训练主 loop 中移出去。async_save() 会返回 future,需要控制并发 checkpoint 数量,避免 CPU memory 压力叠加。
分片写 checkpoint :使用 PyTorch Distributed Checkpoint(DCP),让多个 rank 写各自的 checkpoint 分片,避免单 rank 或单文件形成过大的突发写入。
丢弃 checkpoint cache :写完后调用 posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED),提示内核尽快丢弃 checkpoint 对应的 page cache。
完全绕过 page cache :
Direct I/O :延迟敏感的训练 workflow 可以评估 O_DIRECT、io_uring 和 GPUDirect Storage 。
适用条件 :这类方式需要文件系统、存储设备和 I/O 栈支持 direct I/O,适合已经验证过 I/O 路径的场景。
PyTorch DCP 异步 checkpoint 示例
下面示例只保留训练 loop 中的核心结构。AppState 是对 model 和 optimizer 的 Stateful 包装,用来让 DCP 保存和恢复分布式 state dict。 import torch.distributed.checkpoint as dcp
checkpoint_future = None
for step in range (num_steps):
loss = train_step(model, optimizer)
if should_save(step):
# 通常限制同一时刻只有一个异步 checkpoint,避免 CPU memory 压力叠加
if checkpoint_future is not None :
checkpoint_future.result()
state_dict = { "app" : AppState(model, optimizer)}
checkpoint_future = dcp.async_save(
state_dict,
checkpoint_id = f "checkpoint_step_ { step } " ,
)
# 训练结束前等待最后一个 checkpoint 完成
if checkpoint_future is not None :
checkpoint_future.result()
DCP 的异步 checkpoint 会先把模型复制到内部 CPU buffer,保证 checkpoint 写入期间模型和 optimizer 权重不会继续变化;代价是 CPU memory 会随着 checkpoint_size_per_rank × rank 数量 增加。如果模型很大,普通 async_save() 的 GPU → CPU staging 仍可能阻塞训练 loop。 PyTorch 2.9 引入了 DefaultStager,可以把 state dict 创建和 GPU → CPU copy 也放到后台线程中: import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.staging import DefaultStager
checkpoint_future = None
for step in range (num_steps):
optimizer.zero_grad()
loss = model(batch).sum()
loss.backward()
# 等待上一个 checkpoint 的 staging 完成,再修改模型参数
if checkpoint_future is not None :
checkpoint_future.staging_completion.result()
optimizer.step()
# 避免同时排队多个 checkpoint upload
if checkpoint_future is not None :
checkpoint_future.upload_completion.result()
checkpoint_future = dcp.async_save(
{ "app" : AppState(model, optimizer)},
checkpoint_id = f "checkpoint_step_ { step } " ,
async_stager = DefaultStager(),
)
if checkpoint_future is not None :
checkpoint_future.upload_completion.result()
DefaultStager 会引入后台线程,占用额外 CPU 资源。训练节点需要预留足够 CPU core,否则异步 checkpoint 本身也可能影响输入管道或通信辅助线程。
在 Linux page cache 语境里,writeback 可以翻译为后台写回 。它指内核把 page cache 里的 dirty page 异步写回到磁盘或后端存储。 应用写文件时,数据通常会先进入 page cache,写调用可以较快返回;随后内核在后台把 dirty page 写回存储。当 dirty page 太多,或者后端存储写入跟不上时,内核可能开始限制新的写入,训练进程就会看到 checkpoint 写入停顿。
# 使用比例配置,适合内存规模差异不大的节点
sudo sysctl vm.dirty_background_ratio= 5
sudo sysctl vm.dirty_ratio= 20
# 使用绝对字节配置,适合大内存节点,更可控
sudo sysctl vm.dirty_background_bytes= $(( 8 * 1024 * 1024 * 1024 ))
sudo sysctl vm.dirty_bytes= $(( 32 * 1024 * 1024 * 1024 ))
2.7 CPU 频率与 C-states
许多计算节点默认会让 CPU 运行在省电模式下:CPU 空闲时可能被降频,或者进入低功耗睡眠状态。这可以节省能耗、降低发热和成本。训练过程中,GPU 在处理当前 batch 时,CPU 不一定始终处于 100% 利用率;但当新的数据准备、kernel launch 或通信任务到来时,CPU 从低频或睡眠状态恢复会引入额外延迟。
为了获得更高且更稳定的性能,AI 系统通常会把 CPU frequency governor 配置为 performance。 该模式会让 CPU 尽量保持在较高频率,减少频率切换带来的延迟抖动。这个配置可以通过 cpupower frequency-set -g performance 完成,也可以在 BIOS 中设置。
CPU 空闲状态(C-states)同样会影响延迟稳定性。C-states 是 ACPI 规范定义的 CPU 省电模式:CPU core 空闲时可以进入 C-state 来节省能耗。C0 表示 active 状态,C0 以上表示更深的睡眠状态。C-state 越深,省电越多,但新的工作到来时,core 唤醒所需时间也越长。限制或禁用深层 C-states 可以减少额外的延迟尖峰。
# 查看 CPU governor、频率范围和当前频率
cpupower frequency-info
示例输出: analyzing CPU 30:
driver: acpi-cpufreq
hardware limits: 1.50 GHz - 2.25 GHz
available frequency steps: 2.25 GHz, 2.00 GHz, 1.50 GHz
available cpufreq governors: conservative ondemand userspace powersave performance schedutil
current policy: frequency should be within 1.50 GHz and 2.25 GHz.
The governor "performance" may decide which speed to use
within this range.
current CPU frequency: 2.25 GHz (asserted by call to hardware)
boost state support:
Supported: yes
Active: yes
Total States: 3
Pstate-P0: 2250MHz
Pstate-P1: 2000MHz
Pstate-P2: 1500MHz
driver 表示 Linux 当前用来管理 CPU 频率的驱动,并执行升频、降频、切换 governor 这些操作,这里是 acpi-cpufreq。
hardware limits 表示硬件支持的频率范围。
available cpufreq governors 表示可选 governor。
current policy 表示当前频率策略;示例中 governor 是 performance。
current CPU frequency 表示当前 CPU 频率。
boost state support 表示 CPU 是否具备动态加速频率(boost)能力。动态加速频率是 CPU 官方支持的自动加速机制;在温度、功耗、电流等条件允许时,CPU 可以自动运行到更高频率。
Supported: yes 表示硬件和驱动支持动态加速频率。
Active: yes 表示动态加速频率已启用,CPU 在满足温度和功耗条件时可以临时把频率拉到更高档位。
Pstate-Px 列出了 CPU 可用的性能状态档位,频率从高到低;当前 governor 会根据自己的策略选择使用哪个频率档位。
# 临时设置为 performance governor
sudo cpupower frequency-set -g performance
有些系统安装 cpupower 后支持配置文件,可以把 governor 持久化到服务配置中: sudo vim /etc/default/cpupower
设置: 然后启用服务: sudo systemctl enable --now cpupower
# 查看 CPU 空闲状态;输出只包含 idle states,不包含 active 状态 C0
cpupower idle-info
示例输出: CPUidle driver: acpi_idle
CPUidle governor: menu
analyzing CPU 11:
Number of idle states: 3
Available idle states: POLL C1 C2
POLL:
Flags/Description: CPUIDLE CORE POLL IDLE
Latency: 0
Usage: 2762100
Duration: 86467608
C1:
Flags/Description: ACPI FFH MWAIT 0x0
Latency: 1
Usage: 1085803729
Duration: 316930767923
C2:
Flags/Description: ACPI IOPORT 0x814
Latency: 400
Usage: 715600804
Duration: 4256146765248
CPUidle driver 表示 CPU idle 驱动,这里是 acpi_idle。
CPUidle governor 表示 idle state 选择策略,这里是 menu。menu 是常见的默认策略,会根据下一次 timer 事件、历史 idle 时长、延迟需求等信息,预测该进入哪个 idle state。
Available idle states 表示当前 CPU 可进入的 idle states:
POLL 表示轮询状态。有任务执行时 CPU 处于 C0;没有任务执行时,CPU 可以进入 POLL 这种 idle state 轮询等待新任务 。它的 Latency: 0,唤醒延迟最低,但更耗电。
C1、C2 表示更深的空闲状态;示例中 C2 的 Latency: 400,唤醒延迟明显高于 C1。
Usage 表示进入该 idle state 的次数,Duration 表示累计停留时间。
Linux 下可以通过内核启动参数限制深层 C-states。常见做法是在 GRUB 中加入 processor.max_cstate=1 intel_idle.max_cstate=0,限制 CPU 进入深层睡眠状态。 # 编辑 GRUB 配置
sudo vim /etc/default/grub
# 示例:追加到 GRUB_CMDLINE_LINUX
GRUB_CMDLINE_LINUX = "processor.max_cstate=1 intel_idle.max_cstate=0"
# 更激进的低延迟配置:没有任务执行时,CPU 会采用类似 POLL 的轮询等待方式,而不是进入 C1/C2 这类空闲省电状态
GRUB_CMDLINE_LINUX = "processor.max_cstate=1 intel_idle.max_cstate=0 idle=poll"
# 重新生成 GRUB 配置并重启
sudo grub-mkconfig -o /boot/grub/grub.cfg
sudo reboot
# 重启后确认启动参数生效
cat /proc/cmdline
2.8 Host 内存分配器调优
在 GPU 计算中,CPU 需要持续准备 batch 并及时送给 GPU。优化 memory allocator 可以减少内存分配带来的卡顿和抖动,避免 GPU 等待数据,从而维持高 GPU 利用率和整体吞吐。
如果程序每次申请内存都直接向操作系统请求,会产生很大的系统调用开销,而且频繁分配和释放还会导致内存碎片、多线程锁竞争、cache 利用率低等问题。memory allocator 的作用就是在程序和操作系统之间做一层高效的内存管理:它先一次性向系统申请大块内存,再按需切分、复用和回收 ,从而减少系统调用、降低碎片、提升性能和并发效率。
ptmalloc、jemalloc 与 tcmalloc 对比
Linux 上常见的默认 allocator 是 glibc malloc,其底层实现通常称为 ptmalloc。它兼容性好,但在高并发数据管道中可能出现 arena 膨胀、碎片和 RSS(Resident Set Size,常驻集大小)不回收。 jemalloc 和 tcmalloc 是常见替代 allocator,优势主要体现在降低多线程锁竞争、改善碎片控制,以及更灵活地管理释放后的内存。ptmalloc(glibc malloc) 分配流程 :线程申请内存时,ptmalloc 会先尝试从当前线程的 tcache 命中。tcache 是线程本地缓存,主要缓存小对象;在 64-bit glibc 默认配置下,tcache 可服务的请求大小最大约为 1032B。如果 tcache 没命中,请求会进入对应的 arena,在 arena 的 bins 中查找合适的空闲 chunk。较小的 chunk 通常从 fastbins / small bins 复用,较大的 chunk 会从 large bins 查找。如果 bins 中没有合适空间,则通过 brk 扩展 heap,或通过 mmap 向操作系统申请新内存。大块分配通常更可能走 mmap,默认阈值从约 128KB 开始,并可能动态调整。 释放流程 :释放时,如果 chunk 大小适合且当前线程的 tcache 还有空间,会优先放回 tcache;否则回到对应 arena 的 bins。相邻空闲 chunk 在合适情况下会合并,较大的 mmap 内存块也可能直接归还给操作系统。锁竞争特点 :tcache 命中时很快,但 tcache miss、tcache flush、较大对象分配、跨线程释放等情况仍可能进入 arena。多个线程共享同一个 arena 时,即使操作不同 size class,也可能竞争 arena 相关锁,因此高并发下更容易出现性能抖动。 tcmalloc(Google) 分配流程 :tcmalloc 的核心是让小对象优先走线程本地缓存。小对象常见口径是 ≤32KB,会优先从当前线程的 ThreadCache 按 size class 获取。如果 ThreadCache 不够,会从共享的 CentralCache 批量补充;如果 CentralCache 也不够,再从 PageHeap 获取 span。span 是一段连续 page,可以被切成小对象,也可以承载大对象。大对象通常指 >32KB 的请求,一般绕过 ThreadCache,直接走 CentralCache / PageHeap 路径。 释放流程 :小对象释放时,会根据地址找到所属 span 和 size class,然后优先放回当前线程的 ThreadCache。如果 ThreadCache 超过预算,会批量归还一部分对象给 CentralCache。大对象释放时,会回到 PageHeap,并尝试和相邻空闲 span 合并。锁竞争特点 :tcmalloc 的优势是大量小对象分配可以在线程本地完成;缓存不够时,也是批量访问 CentralCache ,不是每次 malloc / free 都访问共享结构。同时,CentralCache 按 size class 分散管理,不同 size class 通常不会竞争同一把锁,因此锁竞争通常比 ptmalloc 更低。 jemalloc(FreeBSD / Facebook) 分配流程 :jemalloc 也有线程本地缓存,叫 TCache。小对象优先从 TCache 按 size class 获取;如果没有命中,再进入线程关联的 arena。jemalloc 不是每个线程一个 arena,多个线程可能共享同一个 arena。large object 不走 slab,而是由 arena 通过 extent 管理。释放流程 :小对象通常先回到当前线程的 TCache;如果不能缓存,就回到对象所属 arena 的 bin / slab。large object 会回到所属 arena 的 extent 管理结构。释放的内存通常回到对象原本所属的 arena,而不是简单回到当前调用 free 的线程对应的 arena。锁竞争特点 :jemalloc 和 ptmalloc 都可能多个线程共享 arena,但 jemalloc 的 arena 内部拆得更细:不同 size class 通常对应不同 bin,bin / slab / extent 的锁粒度也更细。因此两个线程即使共享同一个 arena,只要操作不同 size class,通常也不容易竞争同一把 arena 级别的大锁。
jemalloc 的 arena、bin、slab、run 与 extent
jemalloc 的小对象分配通常按 size class 进入对应 bin,再从这个 bin 管理的 slab / run 中取一个空闲 object。large object 不走 slab,通常由 extent 管理。 arena
├── bin(size class = 64B)
│ ├── slab / run
│ │ ├── object 64B # 返回给一次小对象分配
│ │ ├── object 64B
│ │ └── ...
│ └── slab / run
│ └── object 64B
│
├── bin(size class = 128B)
│ └── slab / run
│ ├── object 128B
│ └── ...
│
└── extent
├── backing pages for slabs / runs
└── large object
arena :allocator 内部的一个内存管理域,负责维护 bins、large allocation、锁和统计信息。多线程程序可以分散到多个 arena,减少锁竞争,但 arena 过多也可能让 RSS 变高。
bin :arena 内按 size class 划分的小对象管理结构。比如 64B bin、128B bin,每个 bin 通常管理一批对应尺寸的 slab / run。
slab :一段被切成固定大小 object 的内存块。一个 64B slab 只切 64B object,一个 128B slab 只切 128B object。一个 slab 里会有多个 object,用来批量服务同一 size class 的小对象分配,减少频繁向 OS 申请内存的开销。
run :可以理解为 slab 的近似概念,常用于描述一段连续内存被切成多个同尺寸 object 的结构;在不同 allocator 或不同版本文档里,run 和 slab 的命名可能不同。
object :slab / run 被切分后得到的固定大小内存槽位,也是最终返回给应用的一次小对象分配结果。
extent :jemalloc 中按 page 粒度管理的一段连续虚拟内存范围。小对象 slab / run 可以由 extent 支撑,large object 也可能直接由 extent 管理。
使用 jemalloc 或 tcmalloc 前,系统中需要有对应的动态库;随后可以通过 LD_PRELOAD 让目标进程在启动时优先加载对应 allocator。 # Ubuntu / Debian 示例
sudo apt update
sudo apt install -y libjemalloc2 google-perftools
安装后可以通过动态库缓存确认系统是否能找到它们: ldconfig -p | grep -E "jemalloc|tcmalloc"
# 输出类似
libtcmalloc_minimal.so.4 (libc6,x86-64) = > /lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4
libjemalloc.so.2 (libc6,x86-64) = > /lib/x86_64-linux-gnu/libjemalloc.so.2
jemalloc MALLOC_CONF 是 jemalloc 的配置环境变量。jemalloc 可以把分配分散到多个 arena,减少多线程分配时的锁竞争;也可以启用后台线程,在业务线程之外做内存清理。下面是一组常见配置:export LD_PRELOAD = / usr / lib / x86_64-linux-gnu / libjemalloc . so . 2
export MALLOC_CONF = "narenas:8,dirty_decay_ms:10000,muzzy_decay_ms:10000,background_thread:true"
narenas:8 设置 arena 数量为 8。arena 多一些可以降低多线程锁竞争,但太多也可能增加内存占用。
background_thread:true 启用后台线程做内存清理,避免释放后的清理工作直接卡在业务线程上。
dirty_decay_ms:10000 控制 dirty pages 延迟多久再回收或归还,10000 表示 10 秒。dirty page 指应用已经释放、但内容还保留的页面,jemalloc 可以直接复用。
muzzy_decay_ms:10000 控制 muzzy pages 的回收延迟,也设成 10 秒。muzzy page 指应用已释放、旧数据不需要保留,但虚拟地址范围仍留在进程里的页面。与 dirty pages 相比,muzzy pages 再次被分配时往往需要重新触发 page fault 并补入物理页,因此复用成本通常更高。
把 dirty / muzzy page 的回收延迟调长,可以减少频繁向 OS 申请/归还内存的开销,但 RSS 可能保持得更高。RSS(Resident Set Size)表示进程当前实际占用的物理内存;应用释放对象后,allocator 可能先把内存留在 arena 或 cache 里复用,不一定马上还给 OS。
tcmalloc tcmalloc 使用自己的 TCMALLOC_* 环境变量。export LD_PRELOAD = / usr / lib / x86_64-linux-gnu / libtcmalloc_minimal . so . 4
export TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES = $(( 512 * 1024 * 1024 ))
export TCMALLOC_RELEASE_RATE = 16
TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES 控制所有 thread cache 可占用的总大小。值越大,线程本地分配越容易命中 cache,但 RSS 可能更高。
TCMALLOC_RELEASE_RATE 控制 tcmalloc 把空闲内存归还给 OS 的积极程度。值越大,释放越积极;值太大可能增加向 OS 申请/归还内存的开销。
# 确认目标进程是否已经加载 allocator
cat /proc/ $PID /maps | grep -E "jemalloc|tcmalloc"
# 输出类似
7cb7b42fb000-7cb7b4306000 r--p 00000000 103:04 686402 /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.16
7cb7b4306000-7cb7b4318000 r-xp 0000b000 103:04 686402 /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.16
7cb7b4318000-7cb7b431f000 r--p 0001d000 103:04 686402 /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.16
7cb7b431f000-7cb7b4320000 r--p 00023000 103:04 686402 /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.16
7cb7b4320000-7cb7b4321000 rw-p 00024000 103:04 686402 /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.16
/proc/$PID/maps 查看某个进程实际映射到地址空间里的动态库。
这里必须换成目标训练或推理进程的 PID;查 /proc/1/maps 只是在看 PID 1 是否加载了 allocator。
如果没有输出,说明这个进程启动时没有加载 jemalloc 或 tcmalloc。
3. GPU 驱动与运行时设置
OS 调优保证 CPU 侧不拖慢 GPU;GPU 驱动与运行时设置决定 context、时钟、隔离和显存分配是否稳定。不同设置的目标不同,不适合一次性全部打开。
设置 主要作用 适合场景 避免场景 持久化模式(Persistence Mode) 保持 driver state 和 GPU 初始化状态 短作业、低流量推理、MIG 节点 对单个长训练作业收益有限 MPS 多进程共享 GPU 并发执行 小推理、多 MPI rank、小 kernel 组合 单进程已打满 GPU、强隔离多租户 MIG 硬件级 GPU 分区 多租户推理、教学/实验平台、资源保证 依赖 NVLink/P2P 的分布式训练 Clock / power limit 稳定 benchmark 和性能功耗比 性能回归测试、容量规划 未监控温度/功耗的生产脚本 CUDA allocator 减少显存碎片和分配同步 变长 batch、推理服务、长期训练 把 OOM 当成单纯 allocator 问题
3.1 持久化模式(Persistence Mode)
默认情况下,如果没有应用正在使用 GPU,driver 可能让 GPU 进入更低功耗状态,并卸载部分驱动上下文。下一次应用重新使用 GPU 时,driver 需要重新初始化 GPU,这个冷启动过程可能带来一两秒级别的延迟 。对于频繁启动/停止 job 的训练集群 ,或者低流量但延迟敏感的推理服务 ,这类初始化开销会影响整体性能。
在 Linux 上,client 通过打开 GPU device file 来 attach GPU;关闭 device file 时,相当于 detach GPU。只要还有 client 保持 device file 打开,GPU state 就会继续留在 driver 中。
nvidia-persistenced 的核心做法,是在后台运行并保持 GPU device file 打开。这样即使没有训练或推理进程正在使用 GPU,driver 也会继续保留 GPU state,从而避免下一个 job 到来时重新初始化。持久化模式不会让 GPU kernel 的数学计算更快,但能减少作业启动延迟 和 idle 后首次 CUDA 调用的冷启动停顿 ;代价是 GPU 空闲时功耗会略高 。
# 以 CSV 形式查看所有 GPU 的 UUID 和当前 persistence mode 状态
nvidia-smi --query-gpu=uuid,persistence_mode --format=csv
# 为所有 GPU 启用 legacy persistence mode
sudo nvidia-smi -pm 1
# 为指定 GPU 启用 persistence mode;<target_gpu> 可以是 GPU index,例如 0
sudo nvidia-smi -i < target_gp u > -pm ENABLED
# 使用 nvidia-smi -q 查看指定 GPU 的详细状态,其中 Persistence Mode 应显示为 Enabled
nvidia-smi -i < target_gp u > -q
# systemd 环境中启动 nvidia-persistenced,并设置为开机自启
sudo systemctl enable --now nvidia-persistenced
Kubernetes 中通过 GPU Operator 启用持久化模式
在 Kubernetes 中,安装 GPU Operator 时启用 driver.enabled=true,GPU Operator 会部署 nvidia-driver-daemonset 来管理节点侧 NVIDIA driver,并启动 nvidia-persistenced,从而启用持久化模式。 # 添加 NVIDIA Helm repo
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
# 安装或更新 GPU Operator,并让 Operator 管理节点侧 NVIDIA driver
helm upgrade --install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator \
--create-namespace \
--set driver.enabled= true
# 查看每个 GPU node 上的 device plugin pod
kubectl -n gpu-operator get pods | grep nvidia-device-plugin-daemonset
# 在 device plugin pod 中确认 GPU persistence mode
# 如果命令输出中 `persistence_mode` 为 `Enabled`,说明当前 GPU 已经启用持久化模式。
kubectl -n gpu-operator exec -it < nvidia-device-plugin-po d > \
-c nvidia-device-plugin -- \
nvidia-smi --query-gpu=uuid,persistence_mode --format=csv
参考资料:NVIDIA Driver Persistence
3.2 Multi-Process Service (MPS)
通常,当多个进程共享同一张 GPU 时,GPU 调度器会在它们之间做时间片切换。例如,如果两个 Python 进程各自都有一些 kernel 要在同一张 GPU 上运行,GPU 可能先执行一个进程的 kernel,再执行另一个进程的 kernel,如此往复。如果这些 kernel 很短,并且它们之间存在空闲间隔,GPU 就可能因为反复进行上下文切换、而不是把这些任务重叠执行,导致利用率不足。
NVIDIA MPS 提供了一种机制,让多个进程可以在同一张 GPU 上并发运行,而不必严格依赖时间片切换。启用 MPS 后,只要 GPU 资源可用,例如 streaming multiprocessors(SMs)、Tensor Cores 等,GPU 就可以同时执行来自不同进程的 kernel。MPS 本质上是把多个进程的上下文合并到一个调度上下文中。这样一来,就不必为独立进程之间的切换和空闲等待付出完整代价。
MPS 由以下几个组件组成:
Control daemon process :control daemon 负责启动和停止 server,并协调 client 与 server 之间的连接。
Client runtime :MPS client runtime 内置在 CUDA Driver library 中,任何 CUDA 应用都可以透明地使用它。
Server process :server 是多个 client 共享的 GPU 连接,并负责在 client 之间提供并发执行能力。
MPS 的典型场景是:多个 MPI 进程或多个 CUDA 进程单独都无法占满 GPU 时,通过共享同一块 GPU 并发执行 kernel 来提高 GPU 利用率。 如果某个程序本身就将 GPU 完全占满到 100%,MPS 并不能让它变得更快,因为利用率无法超过 100%。
请注意,MPS 并不对 GPU 内存进行分区,因此所有进程将共享整个 GPU 内存空间。MPS 主要负责计算共享和调度。 问题在于,某个进程可能会申请大量 GPU 内存,导致 GPU 上出现 OOM 错误,从而终止在该 GPU 上运行的所有其他进程。
常用的 MPS 配置和查看命令如下: # CUDA_VISIBLE_DEVICES 控制当前进程可见的 GPU;0 表示只看到 GPU 0
# 如果要设置多个 GPU,可以用逗号分隔,例如 0,1
# 启动 MPS control daemon,并让它只绑定 GPU 0
# CUDA client 连接时,会按需拉起 nvidia-cuda-mps-server
sudo env CUDA_VISIBLE_DEVICES= 0 nvidia-cuda-mps-control -d
# 查看当前 MPS client 和 server
echo ps | sudo nvidia-cuda-mps-control
# 多个 client 的 SERVER 相同,表示它们连接到同一个 MPS server
PID ID SERVER DEVICE NAMESPACE COMMAND
3817508 1 3817822 GPU-f707b56f-f3ae-f293 4026531836 .../.venv/bin/python3
3817500 2 3817822 GPU-f707b56f-f3ae-f293 4026531836 .../.venv/bin/python3
# 关闭 MPS
echo quit | sudo nvidia-cuda-mps-control
基本 MPS 示例 mps_demo.py 会启动多个 Python 子进程,让它们在同一张 GPU 上反复执行矩阵乘法,模拟多个 CUDA 进程共享 GPU 的场景。run_basic_comparison.sh 会跑两轮:第一轮不开 MPS,第二轮开启 MPS 后运行同一个 demo。两轮使用相同的 worker 数、矩阵大小和运行时长,最后比较所有 worker 完成的总矩阵乘法次数。# 进入 ch03 代码目录
cd books/ai-systems-performance-engineering/chapters/ch03
# 默认使用 GPU 0、4 个 worker、运行 30 秒、矩阵大小 1024
./mps/run_basic_comparison.sh
# 也可以通过环境变量调整参数
GPU_INDEX = 0 WORKERS = 4 RUN_SECONDS = 60 N = 1024 ./mps/run_basic_comparison.sh
实际输出示例如下: GPU_INDEX=0
WORKERS=4
RUN_SECONDS=60
N=1024
# GPU_INDEX 表示目标 GPU 编号;WORKERS 表示并发 Python worker 数;
# RUN_SECONDS 表示每轮运行时长;N 表示矩阵乘法的矩阵大小。
================================================================================
Running without MPS
================================================================================
# iters 表示该 worker 在 60 秒内完成的矩阵乘法次数。
worker=0 pid=3811977 iters=98663 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
worker=3 pid=3811980 iters=98640 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
worker=2 pid=3811979 iters=98640 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
worker=1 pid=3811978 iters=98660 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
================================================================================
Running with MPS
================================================================================
# echo ps | sudo nvidia-cuda-mps-control
--------------------------------------------------------------------------------
# SERVER 是 nvidia-cuda-mps-server 的 PID;这里 4 个 worker 都连接到同一个 SERVER。
PID ID SERVER DEVICE NAMESPACE COMMAND
3817508 1 3817822 GPU-f707b56f-f3ae-f293 4026531836 .../.venv/bin/python3
3817500 2 3817822 GPU-f707b56f-f3ae-f293 4026531836 .../.venv/bin/python3
3817499 3 3817822 GPU-f707b56f-f3ae-f293 4026531836 .../.venv/bin/python3
3817504 4 3817822 GPU-f707b56f-f3ae-f293 4026531836 .../.venv/bin/python3
# nvidia-smi -i 0
--------------------------------------------------------------------------------
Sat May 9 04:24:00 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.126.20 Driver Version: 580.126.20 CUDA Version: 13.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 NVIDIA A100-SXM4-80GB Off | 00000000:01:00.0 Off | 0 |
| N/A 45C P0 280W / 275W | 1915MiB / 81920MiB | 100% E. Process |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
# Compute M. 显示 E. Process,表示 GPU 处于 EXCLUSIVE_PROCESS compute mode。
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| 0 N/A N/A 3817499 M+C ...apters/ch03/.venv/bin/python3 466MiB |
| 0 N/A N/A 3817500 M+C ...apters/ch03/.venv/bin/python3 466MiB |
| 0 N/A N/A 3817504 M+C ...apters/ch03/.venv/bin/python3 466MiB |
| 0 N/A N/A 3817508 M+C ...apters/ch03/.venv/bin/python3 466MiB |
| 0 N/A N/A 3817822 C nvidia-cuda-mps-server 36MiB |
+-----------------------------------------------------------------------------------------+
# Processes 表里出现 nvidia-cuda-mps-server,说明 worker 正在通过 MPS server 使用 GPU。
worker=3 pid=3817508 iters=116200 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
worker=1 pid=3817500 iters=116200 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
worker=0 pid=3817499 iters=116180 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
worker=2 pid=3817504 iters=116200 cuda:0=NVIDIA A100-SXM4-80GB uuid=f707b56f-f3ae-f293-8278-b76d17e8adc4
================================================================================
Benchmark summary
================================================================================
mode total_iters
without_mps 394603
with_mps 464780
mps / without_mps = 1.18x
# total_iters 是所有 worker 完成的总矩阵乘法次数;这里 MPS 版本约为不开 MPS 的 1.18 倍。
这段输出里有几个地方值得注意:
吞吐提升 :两轮实验都使用 4 个 worker、N=1024、运行 60 秒,因此可以用总矩阵乘法次数近似比较吞吐。不开 MPS 时完成 394603 次,开启 MPS 后完成 464780 次,对应吞吐约为不开 MPS 的 1.18x 。
MPS client :实际运行 CUDA work 的应用进程。这里就是 4 个 python3 worker,它们的 SERVER 都是 3817822,说明它们连接到了同一个 MPS server。
MPS server :中间代理进程,也就是 nvidia-cuda-mps-server。它代表多个 client 统一持有 GPU context,并把这些 client 的 GPU work 提交给 GPU。
静态 SM 分区示例 MPS 还支持静态 SM 分区(static SM partitioning),主要有以下好处:
确定性的资源分配 :可以显式控制每个 client 能访问哪些 SM,不再完全依赖 MPS 的动态调度。
client 之间的空间隔离 :不同 client 可以被放到不同 SM partition,减少彼此干扰。
静态 SM 分区常用的配置和查看命令如下: # -S 是开启静态 SM 分区的关键参数。
# 启动支持静态 SM 分区的 MPS control daemon。
nvidia-cuda-mps-control -d -S
# 查看当前分区配置。
# 这里 GPU-74d43ed3 是示例 GPU UUID 的短显示;实际环境以 lspart 输出为准。
echo "lspart" | nvidia-cuda-mps-control
GPU Partition free used free used clients
chunk chunk SM SM
GPU-74d43ed3 - 10 0 92 92 -
# 创建 3 个不同大小的 SM 分区。
# 这里的 5、3、2 表示分配的 chunk 数。
# 这里的 chunk 是 MPS 用来分配 SM 的单位;一个 chunk 对应多少个 SM 由具体 GPU 决定。
# Hopper 之前的 GPU 通常是 1 chunk = 4 SM,Hopper 及更新架构的 GPU 通常是 1 chunk = 8 SM,最终以 lspart 输出里的 used SM 为准。
echo "sm_partition add GPU-74d43ed3 5" | nvidia-cuda-mps-control
GPU-74d43ed3-cdf7-e667-3644-bf5b4f46ed65/Dx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
echo "sm_partition add GPU-74d43ed3 3" | nvidia-cuda-mps-control
GPU-74d43ed3-cdf7-e667-3644-bf5b4f46ed65/Cx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
echo "sm_partition add GPU-74d43ed3 2" | nvidia-cuda-mps-control
GPU-74d43ed3-cdf7-e667-3644-bf5b4f46ed65/Bx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# CUDA_MPS_SM_PARTITION 用来指定当前 CUDA client 使用哪个静态 SM 分区。
# 这个变量必须在 CUDA 初始化前设置。
CUDA_MPS_SM_PARTITION = GPU-74d43ed3/Dx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ./large_workload &
CUDA_MPS_SM_PARTITION = GPU-74d43ed3/Cx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ./medium_workload &
CUDA_MPS_SM_PARTITION = GPU-74d43ed3/Bx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ./small_workload &
# 查看分区使用情况。
# clients=Yes 表示已有 client 使用该 partition。
echo "lspart" | nvidia-cuda-mps-control
GPU Partition free used free used clients
chunk chunk SM SM
GPU-74d43ed3 - 0 10 0 80 -
GPU-74d43ed3 Dx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - 5 - 40 Yes
GPU-74d43ed3 Cx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - 3 - 24 Yes
GPU-74d43ed3 Bx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - 2 - 28 Yes
GPU Operator 通过 NVIDIA device plugin 暴露 MPS 共享 GPU 资源。GPU Operator 的 GPU sharing 文档 使用 devicePlugin.config 指定 device plugin 的 ConfigMap;MPS 使用同一个配置入口,但 ConfigMap 里的共享字段是 sharing.mps。 NVIDIA device plugin 文档 说明,MPS 支持仍是 experimental,并且 MPS 共享当前不支持启用 MIG 的 device。下面示例把每张完整 GPU 切成 4 个 MPS 共享访问入口,并用 renameByDefault: true 暴露为 nvidia.com/gpu.shared: # mps-config.yaml
apiVersion : v1
kind : ConfigMap
metadata :
name : mps-config
namespace : gpu-operator
data :
mps-4 : | -
version: v1
sharing:
mps:
renameByDefault: true
resources:
- name: nvidia.com/gpu
replicas: 4
renameByDefault: true 表示共享后的资源会改名暴露。原始资源名是 nvidia.com/gpu,开启后会变成 nvidia.com/gpu.shared;Pod 也需要申请 nvidia.com/gpu.shared。# 创建 device plugin 配置
kubectl apply -f mps-config.yaml
# 让 GPU Operator 的 device plugin 使用 mps-config 里的 mps-4 配置
# 这里沿用 GPU sharing 文档里的 devicePlugin.config 配置入口
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator \
--type merge \
-p '{"spec":{"devicePlugin":{"config":{"name":"mps-config","default":"mps-4"}}}}'
# 查看节点是否暴露了 nvidia.com/gpu.shared
kubectl describe node < node-nam e > | grep -A8 -E "Capacity|Allocatable"
工作负载申请 nvidia.com/gpu.shared 即可使用启用 MPS 共享的 GPU: apiVersion : apps/v1
kind : Deployment
metadata :
name : mps-vectoradd
spec :
# 启动 4 个 Pod,对应前面每张 GPU 暴露的 4 个 MPS 共享访问入口
replicas : 4
selector :
matchLabels :
app : mps-vectoradd
template :
metadata :
labels :
app : mps-vectoradd
spec :
containers :
- name : cuda-sample-vectoradd
# CUDA sample 镜像,用来持续运行 vectorAdd 作为示例工作负载
image : nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda11.7.1-ubuntu20.04
command : [ "/bin/bash" , "-c" ]
args :
# 循环执行 vectorAdd,方便持续观察 MPS client
- while true; do /cuda-samples/vectorAdd; sleep 1; done
resources :
limits :
# renameByDefault: true 后,Pod 需要申请 nvidia.com/gpu.shared
# 这里的 1 表示申请一个 MPS 共享访问入口
nvidia.com/gpu.shared : 1
GPU Operator 不会监控 GPU sharing ConfigMap 的内容变化。后续如果修改 mps-config 这个 ConfigMap,需要手动重启 device plugin DaemonSet 让新配置生效: kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
参考资料:
3.3 Multi-Instance GPU (MIG)
MIG 会把支持的 GPU 划分为最多 7 个隔离实例,每个实例拥有专用计算和 GPU 内存资源。MIG 从 NVIDIA Ampere 架构开始支持,也就是 compute capability >= 8.0 的部分数据中心 GPU;具体型号以 NVIDIA 的 Supported GPUs 列表为准。
MIG 的资源隔离
MIG 适合多租户场景 ,不同用户、容器、虚拟机或进程可以运行在不同 GPU 实例上,一个进程不会直接影响另一个进程的调度和资源边界。对云服务和共享集群来说,这种隔离能把一张大 GPU 切成多个可独立分配的小 GPU。
每个 MIG 实例在 GPU 内部访问内存时都有独立路径,包括内部互连、L2 缓存分区、内存控制器和 DRAM 地址总线。这样即使其他实例的任务占满自己的缓存或 DRAM 接口,当前实例仍能获得更稳定的吞吐和延迟。MIG 也可以切分 SM、数据拷贝引擎、解码器等 GPU 引擎,为不同进程提供确定的服务质量(QoS)和故障隔离。
GPU Instance 和 Compute Instance
MIG 配置分两层:先创建 GPU Instance(GI),再创建 Compute Instance(CI)。
GPU Instance(GI) :GI 由一组 GPU 切片和 GPU 引擎组成,决定这个 MIG 实例的 GPU 内存容量、带宽和 QoS。
Compute Instance(CI) :CI 是在某个 GI 内继续划分出的计算实例,使用父 GI 的一部分 SM 切片。CI 主要隔离计算资源,也就是 SM;它不提供独立的 GPU 内存隔离。 多个 CI 可以共享同一个 GI 的 GPU 内存切片和 DMA、NVDEC 等引擎,但各自拥有独立的 SM 资源。
CUDA 应用会把 CI 及其所属 GI 视为一个 CUDA device;多数场景可以用 -C 参数直接创建覆盖整个 GI 的默认 CI。
MIG 设备命名规则
MIG 设备名称 描述了实例的资源形态。xg.ygb 表示一个 GPU Instance:xg 表示使用 x 份 GPU 计算切片,ygb 表示 GPU 内存容量档位。例如 3g.40gb 表示使用 3 份 GPU 计算切片和 40GB 档位的 GPU 内存。如果一个 GI 继续拆成多个 CI,名称会带上 c,格式为 xc.xg.ygb;例如 1c.3g.40gb 表示在 3g.40gb GI 内使用 1 份计算切片的 CI。
# 查看 GPU 0 当前 MIG 状态
# nvidia-smi 主表中的 MIG M. 会显示 Disabled 或 Enabled
nvidia-smi -i 0
# 也可以只查询 MIG mode 字段
nvidia-smi -i 0 --query-gpu=pci.bus_id,mig.mode.current --format=csv
# 开启 GPU 0 的 MIG mode
# A100 / A30 等 Ampere GPU 可能触发 GPU reset;执行前应确认该 GPU 上没有生产任务
sudo nvidia-smi -i 0 -mig 1
# 查看可创建的 GPU Instance profile
# profile ID、显存大小和 SM 数以这条命令输出为准
nvidia-smi mig -lgip
# 示例输出:A100 40GB 上,profile ID 9 对应 MIG 3g.20gb
+-----------------------------------------------------------------------------+
| GPU instance profiles: |
| GPU Name ID Instances Memory P2P SM DEC ENC |
| Free/Total GiB CE JPEG OFA |
| ============================================================================= |
| 0 MIG 1g.5gb 19 7/7 4.75 No 14 0 0 |
| 1 0 0 |
+-----------------------------------------------------------------------------+
| 0 MIG 1g.5gb+me 20 1/1 4.75 No 14 1 0 |
| 1 1 1 |
+-----------------------------------------------------------------------------+
| 0 MIG 1g.10gb 15 4/4 9.62 No 14 1 0 |
| 1 0 0 |
+-----------------------------------------------------------------------------+
| 0 MIG 2g.10gb 14 3/3 9.62 No 28 1 0 |
| 2 0 0 |
+-----------------------------------------------------------------------------+
| 0 MIG 3g.20gb 9 2/2 19.50 No 42 2 0 |
| 3 0 0 |
+-----------------------------------------------------------------------------+
| 0 MIG 4g.20gb 5 1/1 19.50 No 56 2 0 |
| 4 0 0 |
+-----------------------------------------------------------------------------+
| 0 MIG 7g.40gb 0 1/1 39.25 No 98 5 0 |
| 7 1 1 |
+-----------------------------------------------------------------------------+
# 查看这些 profile 可以放在 GPU 的哪些位置
# {0,4}:4 表示该 profile 占 4 个 GPU slice,可以从位置 0 或 4 开始放置
nvidia-smi mig -lgipp
GPU 0 Profile ID 19 Placements: {0,1,2,3,4,5,6}:1
GPU 0 Profile ID 20 Placements: {0,1,2,3,4,5,6}:1
GPU 0 Profile ID 15 Placements: {0,2,4,6}:2
GPU 0 Profile ID 14 Placements: {0,2,4}:2
GPU 0 Profile ID 9 Placements: {0,4}:4
GPU 0 Profile ID 5 Placement : { 0 }:4
GPU 0 Profile ID 0 Placement : { 0 }:8
# 创建两个 GPU Instance,并同时创建默认 Compute Instance
# -cgi 可以接收 profile ID、短名称或完整 profile 名,例如 9、3g.20gb、MIG 3g.20gb
# -cgi 里的 9 是 profile ID,来自 -lgip 输出的 ID 列
# -cgi 里的 3g.20gb 是 profile 名称,来自 -lgip 输出的 Name 列
# -C 表示为每个 GI 创建默认 CI;没有 CI 时,CUDA 程序还不能使用 MIG device
# 如果创建 GI 时没有带 -C,需要后续用 -lcip 查看 CI profile,再用 -cci 创建 CI
# 例如 sudo nvidia-smi mig -cci 0,0,0 -gi 1 表示在 GI 1 中创建 3 个 profile ID 为 0 的 CI
sudo nvidia-smi mig -cgi 9,3g.20gb -C
Successfully created GPU instance ID 2 on GPU 0 using profile MIG 3g.20gb (ID 9 )
Successfully created compute instance ID 0 on GPU 0 GPU instance ID 2 using profile MIG 3g.20gb (ID 2 )
Successfully created GPU instance ID 1 on GPU 0 using profile MIG 3g.20gb (ID 9 )
Successfully created compute instance ID 0 on GPU 0 GPU instance ID 1 using profile MIG 3g.20gb (ID 2 )
# 查看已经创建的 GI
sudo nvidia-smi mig -lgi
+----------------------------------------------------+
| GPU instances: |
| GPU Name Profile Instance Placement |
| ID ID Start:Size |
| ==================================================== |
| 0 MIG 3g.20gb 9 1 4:4 |
+----------------------------------------------------+
| 0 MIG 3g.20gb 9 2 0:4 |
+----------------------------------------------------+
# 查看 CUDA 可以使用的 MIG device UUID
nvidia-smi -L
GPU 0: A100-SXM4-40GB (UUID: GPU-e86cb44c-6756-fd30-cd4a-1e6da3caf9b0 )
MIG 3g.20gb Device 0: (UUID: MIG-c7384736-a75d-5afc-978f-d2f1294409fd )
MIG 3g.20gb Device 1: (UUID: MIG-a28ad590-3fda-56dd-84fc-0a0b96edc58d )
# 通过 CUDA_VISIBLE_DEVICES 指定某个 MIG device
# <script> 表示要运行的 CUDA 程序,例如推理服务、训练脚本或 benchmark
CUDA_VISIBLE_DEVICES = MIG-c7384736-a75d-5afc-978f-d2f1294409fd <script> &
CUDA_VISIBLE_DEVICES=MIG-a28ad590-3fda-56dd-84fc-0a0b96edc58d < scrip t > &
# nvidia-smi 可以看到进程分别运行在不同 GI / CI 上
nvidia-smi
+-----------------------------------------------------------------------------+
| MIG devices: |
+------------------+----------------------+-----------+-----------------------+
| GPU GI CI MIG | Memory-Usage | Vol | Shared |
| ID ID Dev | | SM Unc | CE ENC DEC OFA JPG |
| | | ECC | |
| ==================+======================+===========+======================= |
| 0 1 0 0 | 11MiB / 20224MiB | 42 0 | 3 0 2 0 0 |
+------------------+----------------------+-----------+-----------------------+
| 0 2 0 1 | 11MiB / 20096MiB | 42 0 | 3 0 2 0 0 |
+------------------+----------------------+-----------+-----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
| ============================================================================= |
| No running processes found |
+-----------------------------------------------------------------------------+
# 清理 MIG 配置前,先停止正在使用这些 MIG device 的进程
sudo nvidia-smi mig -dci && sudo nvidia-smi mig -dgi
# 关闭 GPU 0 的 MIG mode
sudo nvidia-smi -i 0 -mig 0
如果想进一步提升并发,可以将一个 GI 拆成多个 Compute Instance(CI),把 SM 分配给多个 CUDA 程序。每个 CI 拥有独立的 SM 资源,适合多个小任务共享同一个 GI。 # 查看 GPU Instance ID 1 支持的 Compute Instance profile(由 sudo nvidia-smi mig -cgi 9 创建得到)
# 这里的 GPU Instance ID 1 来自前面的 sudo nvidia-smi mig -lgi 输出
sudo nvidia-smi mig -lcip -gi 1
+--------------------------------------------------------------------------------------+
| Compute instance profiles: |
| GPU GPU Name Profile Instances Exclusive Shared |
| Instance ID Free/Total SM DEC ENC OFA |
| ID CE JPEG |
| ====================================================================================== |
| 0 1 MIG 1c.3g.20gb 0 3/3 14 2 0 0 |
| 3 0 |
+--------------------------------------------------------------------------------------+
| 0 1 MIG 2c.3g.20gb 1 1/1 28 2 0 0 |
| 3 0 |
+--------------------------------------------------------------------------------------+
| 0 1 MIG 3g.20gb 2 * 1/1 42 2 0 0 |
| 3 0 |
+--------------------------------------------------------------------------------------+
# Profile ID 0 对应 MIG 1c.3g.20gb,可以用 -cci 0 创建
# Profile ID 2* 里的 * 表示默认 CI profile,-C 会创建这个完整 CI
# Free/Total 显示 3/3 表示当前还能创建 3 个,理论最多 3 个
# 如果创建 GI 时带了 -C,需要先查看并删除默认 CI
sudo nvidia-smi mig -lci -gi 1
sudo nvidia-smi mig -dci -ci < compute_instance_i d > -gi 1
# 如果创建 GI 时没有带 -C,或者已经删除默认 CI,可以在 GI 1 里创建一个或多个 1c CI
# -gi 1 表示目标 GPU Instance ID 是 1
# -cci 0,0,0 是创建 3 个 1c CI 的例子,使用的 CI profile ID 都是 0
sudo nvidia-smi mig -cci 0,0,0 -gi 1
Successfully created compute instance ID 0 on GPU 0 GPU instance ID 1 using profile MIG 1c.3g.20gb (ID 0 )
Successfully created compute instance ID 1 on GPU 0 GPU instance ID 1 using profile MIG 1c.3g.20gb (ID 0 )
Successfully created compute instance ID 2 on GPU 0 GPU instance ID 1 using profile MIG 1c.3g.20gb (ID 0 )
# 创建后可以用 -lci 查看 GI 1 下的 CI
sudo nvidia-smi mig -lci -gi 1
+-------------------------------------------------------+
| Compute instances: |
| GPU GPU Name Profile Instance |
| Instance ID ID |
| ID |
| ======================================================= |
| 0 1 MIG 1c.3g.20gb 0 0 |
+-------------------------------------------------------+
| 0 1 MIG 1c.3g.20gb 0 1 |
+-------------------------------------------------------+
| 0 1 MIG 1c.3g.20gb 0 2 |
+-------------------------------------------------------+
# nvidia-smi 会把 3 个 CI 枚举成 3 个 MIG device
nvidia-smi
+-----------------------------------------------------------------------------+
| MIG devices: |
+------------------+----------------------+-----------+-----------------------+
| GPU GI CI MIG | Memory-Usage | Vol | Shared |
| ID ID Dev | | SM Unc | CE ENC DEC OFA JPG |
| | | ECC | |
| ==================+======================+===========+======================= |
| 0 1 0 0 | 11MiB / 20224MiB | 14 0 | 3 0 2 0 0 |
+------------------+ +-----------+-----------------------+
| 0 1 1 1 | | 14 0 | 3 0 2 0 0 |
+------------------+ +-----------+-----------------------+
| 0 1 2 2 | | 14 0 | 3 0 2 0 0 |
+------------------+----------------------+-----------+-----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
| ============================================================================= |
| No running processes found |
+-----------------------------------------------------------------------------+
# 分别指定 3 个 MIG device,启动 3 个 CUDA 程序
CUDA_VISIBLE_DEVICES = MIG-c7384736-a75d-5afc-978f-d2f1294409fd <script> &
CUDA_VISIBLE_DEVICES=MIG-c376546e-7559-5610-9721-124e8dbb1bc8 < scrip t > &
CUDA_VISIBLE_DEVICES = MIG-928edfb0-898f-53bd-bf24-c7e5d08a6852 <script> &
# 再次查看 nvidia-smi,可以看到进程分别运行在 3 个 CI 上
nvidia-smi
+-----------------------------------------------------------------------------+
| MIG devices: |
+------------------+----------------------+-----------+-----------------------+
| GPU GI CI MIG | Memory-Usage | Vol | Shared |
| ID ID Dev | | SM Unc | CE ENC DEC OFA JPG |
| | | ECC | |
| ==================+======================+===========+======================= |
| 0 1 0 0 | 476MiB / 20224MiB | 14 0 | 3 0 2 0 0 |
+------------------+ +-----------+-----------------------+
| 0 1 1 1 | | 14 0 | 3 0 2 0 0 |
+------------------+ +-----------+-----------------------+
| 0 1 2 2 | | 14 0 | 3 0 2 0 0 |
+------------------+----------------------+-----------+-----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
| ============================================================================= |
| 0 1 0 59785 C < scrip t > 153MiB |
| 0 1 1 59796 C < scrip t > 153MiB |
| 0 1 2 59885 C < scrip t > 153MiB |
+-----------------------------------------------------------------------------+
GI/CI 配置本身不会跨系统重启保留。生产环境通常使用 mig-parted 重建固定的 MIG 切分形态。 下面示例以 A100-SXM4-40GB 为例保存多套候选配置。mig-devices 里的 profile 名称来自 nvidia-smi mig -lgip 输出的 Name 列,例如 MIG 3g.20gb 对应配置里的 "3g.20gb"。 # config.yaml
version : v1
mig-configs :
all-disabled :
- devices : all
mig-enabled : false
all-enabled :
- devices : all
mig-enabled : true
mig-devices : {}
2-3g-20gb :
- devices : [ 0 ]
mig-enabled : true
mig-devices :
"3g.20gb" : 2
all-balanced :
- devices : all
mig-enabled : true
mig-devices :
"1g.5gb" : 2
"2g.10gb" : 1
"3g.20gb" : 1
配置含义:
mig-configs 下面可以保存多套候选配置。每次执行 nvidia-mig-parted apply -c <config-name> 时,只会应用 -c 指定的那一套配置
all-disabled、all-enabled、2-3g-20gb 和 all-balanced 都是配置名称,后续 apply 时通过 -c 引用
devices: [0] 表示只配置 GPU 0
devices: all 表示配置节点上的所有 GPU
mig-enabled: true 表示启用 MIG mode
mig-devices 表示要创建的 GPU Instance profile 和数量,例如 "3g.20gb": 2 表示创建两个 3g.20gb
常用操作如下: # 应用固定的 MIG 切分形态
sudo nvidia-mig-parted apply \
-f config.yaml \
-c 2-3g-20gb
# 校验当前节点是否已经符合这个配置
sudo nvidia-mig-parted assert \
-f config.yaml \
-c 2-3g-20gb
# 导出当前 MIG 配置,方便生成初始 YAML
sudo nvidia-mig-parted export
# 验证 CUDA 可见的 MIG device
nvidia-smi -L
GPU 0: NVIDIA A100-SXM4-40GB (UUID: GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx )
MIG 3g.20gb Device 0: (UUID: MIG-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa )
MIG 3g.20gb Device 1: (UUID: MIG-bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb )
CUDA Multi-Process Service(MPS)可以让多个 CUDA 进程在 GPU 上并发执行。MPS 和 MIG 可以结合使用,进一步提高某些 workload 的 GPU 利用率。 具体做法是:先用 MIG 把一张 GPU 划分成多个隔离的 GPU Instance,再在某个 MIG device 内使用 MPS,让多个 CUDA 进程通过同一个 MPS server 并发提交任务。 整体流程分三步:
配置目标 MIG 切分形态,例如两个 3g.40gb MIG device
为每个 MIG device 设置独立的 CUDA_MPS_PIPE_DIRECTORY,并启动一个独立的 MPS control daemon
启动 CUDA 程序时,用 CUDA_VISIBLE_DEVICES=<MIG_UUID> 指定目标 MIG device
MPS 文档通常建议使用 EXCLUSIVE_PROCESS,确保一张 GPU 只有一个 MPS server。MIG mode 下不使用这个模式,因为每个 MIG GPU Instance 需要独立的 MPS server。 # 创建两个 GPU Instance,并同时创建默认 Compute Instance
# 这里以 GPU 0 上的两个 MIG 3g.40gb 为例;profile ID 以 -lgip 输出为准
sudo nvidia-smi mig -i 0 -cgi 9,9 -C
Successfully created GPU instance ID 2 on GPU 0 using profile MIG 3g.40gb (ID 9 )
Successfully created compute instance ID 0 on GPU 0 GPU instance ID 2 using profile MIG 3g.40gb (ID 2 )
Successfully created GPU instance ID 1 on GPU 0 using profile MIG 3g.40gb (ID 9 )
Successfully created compute instance ID 0 on GPU 0 GPU instance ID 1 using profile MIG 3g.40gb (ID 2 )
# 查看 MIG device UUID,后面会传给 CUDA_VISIBLE_DEVICES
nvidia-smi -L
GPU 0: NVIDIA H100 80GB HBM3 (UUID: GPU-c08d91cb-e324-655c-71ba-7570956445bc )
MIG 3g.40gb Device 0: (UUID: MIG-405bbda1-6b05-535f-af26-79ccdc267be0 )
MIG 3g.40gb Device 1: (UUID: MIG-b0a55a70-b1b0-529f-af26-79ccdc267be0 )
下面是一个完整脚本。它会在两个 MIG device 上运行一个 demo 程序:每个 MIG device 启动一个独立的 MPS control daemon,并在对应 MIG device 上启动 workload。 #!/usr/bin/env bash
set -euo pipefail
# GPU 0: NVIDIA H100 80GB HBM3 (UUID: GPU-c08d91cb-e324-655c-71ba-7570956445bc)
# MIG 3g.40gb Device 0: (UUID: MIG-405bbda1-6b05-535f-af26-79ccdc267be0)
# MIG 3g.40gb Device 1: (UUID: MIG-b0a55a70-b1b0-529f-af26-79ccdc267be0)
# MIG_DEVICES 保存要使用的 MIG device UUID
# 每个 MIG UUID 对应一个独立的 MIG device
MIG_DEVICES = (
"MIG-405bbda1-6b05-535f-af26-79ccdc267be0"
"MIG-b0a55a70-b1b0-529f-af26-79ccdc267be0"
)
for mig_device in "${ MIG_DEVICES [ @ ]}" ; do
# CUDA_MPS_PIPE_DIRECTORY 指定 MPS control daemon 和 client 通信使用的 pipe directory
# 每个 MIG device 必须使用独立目录,避免不同 MPS server 混在一起
export CUDA_MPS_PIPE_DIRECTORY = / tmp / $mig_device
mkdir -p " $CUDA_MPS_PIPE_DIRECTORY "
# CUDA_VISIBLE_DEVICES 指定当前 MPS control daemon 绑定哪个 MIG device
sudo CUDA_VISIBLE_DEVICES= $mig_device \
CUDA_MPS_PIPE_DIRECTORY=/tmp/ $mig_device \
nvidia-cuda-mps-control -d
# 启动 demo 程序,并连接到同一个 MIG device 对应的 MPS server
CUDA_VISIBLE_DEVICES = $mig_device \
CUDA_MPS_PIPE_DIRECTORY=/tmp/ $mig_device \
./demo_cuda_app &
done
脚本运行后,可以用 nvidia-smi 看到每个 MIG device 上各有一个 MPS server,以及对应的 CUDA client: +-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| 0 1 0 3805 M+C ./demo_cuda_app 326MiB |
| 0 1 0 3809 C nvidia-cuda-mps-server 60MiB |
| 0 2 0 3817 M+C ./demo_cuda_app 326MiB |
| 0 2 0 3819 C nvidia-cuda-mps-server 60MiB |
+-----------------------------------------------------------------------------------------+
NVIDIA GPU Operator 通过 MIG Manager 管理 Kubernetes 节点上的 MIG 配置。安装 GPU Operator 时需要启用 MIG strategy,常见取值是 single 和 mixed:
single 适合节点上使用同一种 MIG profile 的场景,MIG device 通常继续通过 nvidia.com/gpu 这类资源暴露
mixed 适合同一节点上混合使用多种 MIG profile 的场景,资源会按 profile 暴露,例如 nvidia.com/mig-1g.5gb
# 安装时启用 MIG strategy
helm install --wait --generate-name \
-n gpu-operator --create-namespace \
nvidia/gpu-operator \
--set mig.strategy=single
# 已安装后也可以修改 ClusterPolicy
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
--type= 'json' \
-p= '[{"op":"replace","path":"/spec/mig/strategy","value":"single"}]'
MIG Manager 使用 mig-parted 配置文件。默认配置来源由 migManager.config 指定: migManager :
config :
default : all-disabled
name : default-mig-parted-config
这里的含义是:
name: default-mig-parted-config 表示 MIG Manager 读取这个 ConfigMap 里的 config.yaml
default: all-disabled 表示默认使用 mig-configs 里的 all-disabled 这套配置
节点上的 nvidia.com/mig.config=<config-name> label 会从这个 ConfigMap 里选择一套配置并应用
如果想要自定义 MIG 配置,可以参考 Example custom MIG configuration 。 可以用下面的命令查看当前配置: # 查看 ClusterPolicy 中 MIG Manager 使用哪个默认配置和 ConfigMap
kubectl get clusterpolicy cluster-policy -o yaml | grep -A5 migManager:
# 查看 default-mig-parted-config 这个 ConfigMap 里的 config.yaml
kubectl get cm -n gpu-operator default-mig-parted-config \
-o jsonpath='{.data.config\.yaml}'
配置 MIG 时,只需要在节点打上 nvidia.com/mig.config label,MIG Manager 会监听到 node label 变化,然后调用 mig-parted 应用目标配置。 # profile 名称来自 default-mig-parted-config 里的 mig-configs
kubectl label nodes < node-nam e > \
nvidia.com/mig.config=all-1g.5gb \
--overwrite
工作负载申请资源时,根据 MIG strategy 使用对应的资源名: # single strategy: MIG device 通常作为 nvidia.com/gpu 暴露
resources :
limits :
nvidia.com/gpu : 1
# mixed strategy: 按 MIG profile 申请资源
resources :
limits :
nvidia.com/mig-1g.5gb : 1
3.4 GPU 时钟与 ECC
GPU Boost 会根据功耗、温度和负载动态调频。benchmark 或容量规划时,动态时钟会让相同脚本在不同 run 之间出现假差异。nvidia-smi 文档 提供了 --lock-gpu-clocks、--lock-memory-clocks、--power-limit 和 ECC 查询接口。
# 查看支持的频率和当前 throttle reason
nvidia-smi -q -d SUPPORTED_CLOCKS,PERFORMANCE,POWER,TEMPERATURE
# 锁定核心时钟,Volta+ 支持
sudo nvidia-smi -lgc < min_cloc k > , < max_cloc k >
# 锁定显存时钟,不同架构支持情况不同
sudo nvidia-smi -lmc < min_mem_cloc k > , < max_mem_cloc k >
# 设置功耗上限
sudo nvidia-smi -pl < watt s >
# 恢复默认
sudo nvidia-smi -rgc
sudo nvidia-smi -rmc
ECC 在数据中心 GPU 上应保持启用。nvidia-smi -q -d ECC 可以看 correctable / uncorrectable error、page retirement 和 row remapper 状态。长训练中,未纠正错误比少量性能开销更危险。
3.5 GPU 内存管理
PyTorch 使用 caching allocator 来减少重复 cudaMalloc / cudaFree 和设备同步。PyTorch CUDA semantics 文档 说明,nvidia-smi 中看到的 reserved memory 不等于 tensor 实际占用;应同时看 memory_allocated() 与 memory_reserved()。
import torch
print (torch.cuda.memory_summary())
print (torch.cuda.memory_stats())
print (torch.cuda.memory_allocated() / 1024 ** 3 )
print (torch.cuda.memory_reserved() / 1024 ** 3 )
PYTORCH_ALLOC_CONF 只应在定位到碎片问题后使用。官方文档把 max_split_size_mb 描述为 OOM 且存在大量 inactive split blocks 时的最后手段;backend:cudaMallocAsync 需要 CUDA 11.4+,适合评估长期服务的碎片问题。
# 原生 allocator,限制大块拆分
export PYTORCH_ALLOC_CONF = max_split_size_mb : 512
# CUDA async allocator
export PYTORCH_ALLOC_CONF = backend : cudaMallocAsync
Unified Memory / UVM 能让数据在 CPU 与 GPU 间自动迁移,但深度学习工作负载通常应显式放置 tensor。PyTorch 文档也提示,对于能放进 GPU memory 的 DL workload,显式 placement 通常优于 UVM,因为 page fault 和双向迁移会引入不可预测开销。
Docker --gpus 或 NVIDIA_VISIBLE_DEVICES 控制 GPU 暴露范围,不提供硬 GPU memory limit。需要硬隔离时使用 MIG;需要软共享时再考虑 MPS 或 Kubernetes GPU sharing。
4. 系统调优脚本
system_tuning.sh 脚本里包含 CPU 频率策略、swap、透明大页(THP)、网络参数、中断亲和性等调优项。
#!/bin/bash
# GPU 环境的系统级性能调优示例
# 需要以 root 用户运行,或用 sudo 执行
echo "Applying system-wide GPU performance optimizations..."
# 1. CPU governor 与 C-states
echo "Setting CPU governor to performance mode..."
# 把 CPU governor 切到 performance,减少动态降频带来的唤醒和升频延迟
cpupower frequency-set -g performance
# 深层 C-states 通常需要在 BIOS 或内核启动参数中配置,这里只打印提醒
echo "Disabling deep C-states in BIOS (manual step required)"
# 2. 虚拟内存与 swap
echo "Disabling swap and setting swappiness to 0..."
# 临时关闭所有 swap 设备和 swap 文件,重启后会按系统配置恢复
swapoff -a
# 设置 swappiness=0,尽量避免在非极端内存压力下使用 swap
echo 0 > /proc/sys/vm/swappiness
# 3. Transparent Huge Pages:训练偏吞吐可以启用,推理偏延迟通常禁用
echo "Configuring Transparent Huge Pages..."
# 训练 workload:吞吐优先
# 启用 THP,让内核尽量使用 2MB transparent hugepages
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# 推理 workload:延迟优先时可改成 never
# 禁用 THP,减少 compaction 等后台行为带来的延迟抖动
# echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 4. 文件系统与 I/O 调优
echo "Tuning filesystem cache settings..."
# dirty_ratio 控制 dirty page 可占系统内存的最高比例,超过后写入进程可能被阻塞
echo 20 > /proc/sys/vm/dirty_ratio
# dirty_background_ratio 控制后台写回开始触发的比例
echo 10 > /proc/sys/vm/dirty_background_ratio
# 5. 网络参数调优,适用于 RDMA / InfiniBand 等高吞吐网络
echo "Optimizing network settings for RDMA/InfiniBand..."
# 提高 socket receive buffer 上限
echo 'net.core.rmem_max = 268435456' >> /etc/sysctl.conf
# 提高 socket send buffer 上限
echo 'net.core.wmem_max = 268435456' >> /etc/sysctl.conf
# 设置 TCP receive buffer 的 min / default / max
echo 'net.ipv4.tcp_rmem = 4096 87380 268435456' >> /etc/sysctl.conf
# 设置 TCP send buffer 的 min / default / max
echo 'net.ipv4.tcp_wmem = 4096 65536 268435456' >> /etc/sysctl.conf
# 重新加载 /etc/sysctl.conf,使上面的网络参数生效
sysctl -p
# 6. 中断亲和性示例:下面以 8-core 系统为例
echo "Setting interrupt affinity..."
# 实际生产中需要按 GPU / NIC 所在 NUMA node 和 CPU 拓扑定制
# 示例:把 NVIDIA GPU 相关 IRQ 绑定到指定 CPU core
# 从 /proc/interrupts 中找出 NVIDIA 相关 IRQ 编号
for irq in $( grep nvidia /proc/interrupts | cut -d: -f1 ); do
# smp_affinity 使用 bitmask;这里写入 2 表示绑定到 CPU 1
echo 2 > /proc/irq/ $irq /smp_affinity # 绑定到 CPU 1
done
# 7. locked memory 与文件描述符限制
echo "Setting unlimited locked memory..."
# 写入 PAM limits 配置,提高 memlock 和 nofile 限制;通常需要重新登录或重启服务后生效
cat >> /etc/security/limits.conf << EOF
* soft memlock unlimited
* hard memlock unlimited
* soft nofile 1048576
* hard nofile 1048576
EOF
# 8. GPU 相关设置
echo "Configuring GPU settings..."
# 为所有 GPU 启用 persistence mode
# 减少 GPU driver state 在任务间反复初始化的开销
nvidia-smi -pm 1
# 可选:启用 MPS,适用于多进程共享 GPU 的场景
# 指定 MPS daemon 使用的 pipe 目录
# export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
# 指定 MPS daemon 的日志目录
# export CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log
# 启动 MPS control daemon
# nvidia-cuda-mps-control -d
# 9. NUMA balancing
echo "Disabling automatic NUMA balancing..."
# 禁用自动 NUMA balancing,避免内核自动迁移 page 影响手工 NUMA 绑定策略
echo 0 > /proc/sys/kernel/numa_balancing
# 10. CPU isolation 示例:把 CPU 2-7 留给计算任务
# 这类设置需要通过内核启动参数生效,例如 isolcpus=2-7 nohz_full=2-7
echo "CPU isolation requires kernel boot parameters:"
# 打印需要加入 GRUB 的内核启动参数示例
echo "Add to GRUB: isolcpus=2-7 nohz_full=2-7 rcu_nocbs=2-7"
# 提醒:部分参数需要重启后才会完全生效
echo "System tuning complete. Reboot recommended for all changes to take effect."
# 提醒:如需 CPU isolation,需要手动更新 GRUB 配置
echo "Remember to update /etc/default/grub with CPU isolation parameters if needed."
5. 容器运行时优化
容器共享宿主 Linux kernel,CUDA kernel 仍在同一块 GPU 上执行。容器 GPU 性能通常接近裸机,真正容易出问题的是 driver/CUDA 版本、设备注入、文件系统和网络路径。
NVIDIA Container Toolkit 包含 nvidia-container-runtime、nvidia-ctk、nvidia-container-cli 和 libnvidia-container 等组件。容器启动时,它负责把 GPU device、driver library 和能力集暴露给容器。
图源:NVIDIA Container Toolkit Architecture Overview
# 最小验证
docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi
# 指定 GPU 和能力
docker run --rm \
--gpus '"device=1,2"' \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi
版本匹配规则以官方 NVIDIA Data Center Driver/CUDA support matrix 为准。写作时(2026-04-25)该矩阵中 R535 支持 CUDA 12.x,R580/R595 支持 CUDA 13.x;CUDA Compatibility 也说明 CUDA 12.x 工具链的最低 driver 是 525.60.13。实际生产不要只记大版本,镜像里的 NVIDIA_REQUIRE_CUDA 约束和宿主 driver 小版本都要检查。
5.2 避免 Overlay FS 开销
容器 writable layer 适合代码和少量日志,不适合训练数据、checkpoint、tokenized cache 和模型权重。大文件读写应通过 bind mount、PVC 或本地 NVMe cache 进入容器。
docker run --rm --gpus all \
--ipc=host \
--ulimit memlock=-1:-1 \
-v /data/dataset:/mnt/dataset:ro \
-v /nvme/checkpoints:/mnt/checkpoints \
nvcr.io/nvidia/pytorch:25.03-py3 \
python train.py \
--data /mnt/dataset \
--output /mnt/checkpoints
容器运行参数和前面的小节要连起来看:
--ipc=host 或足够大的 /dev/shm,避免 DataLoader/NCCL shared memory 不足。
--ulimit memlock=-1:-1,配合 pinned memory、RDMA 和某些通信库。
--network=host,只在 MPI/NCCL/RDMA 对网络路径敏感且安全策略允许时使用。
CUDA_DEVICE_ORDER=PCI_BUS_ID,让容器内 GPU 顺序与 PCIe 拓扑更容易对应。
numactl --cpunodebind/--membind,在容器入口或 launcher 中绑定 rank。
docker_gpu_optimized.dockerfile 使用 NGC PyTorch 镜像,安装 numactl、libnuma、tcmalloc、jemalloc,并设置 allocator、CUDA arch 和 PyTorch allocator 环境变量。ENV MALLOC_CONF= "narenas:8,dirty_decay_ms:10000,muzzy_decay_ms:10000,background_thread:true"
ENV TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES=536870912
ENV PYTORCH_ALLOC_CONF=max_split_size_mb:512
ENV CUDA_DEVICE_ORDER=PCI_BUS_ID
文件里的 ENTRYPOINT ["numactl", "--interleave=all", "python"] 是通用 fallback。已知每个 rank 对应的 GPU/NUMA 节点时,更推荐在 launcher 中使用 --cpunodebind=<node> --membind=<node>,避免本地性被 --interleave=all 稀释。
5.3 减小镜像体积
镜像体积主要影响拉取、启动和节点磁盘压力。长训练对启动时间不敏感,但大集群滚动升级、弹性推理和 spot 节点恢复会放大镜像体积的成本。
常见策略:
使用 NGC runtime 镜像作为基础镜像,构建工具放在 build stage。
把 wheel cache、apt cache、源码临时目录在同一 layer 清理。
权重、数据集、checkpoint 不进入镜像,通过对象存储、PVC、bind mount 或本地缓存加载。
HPC 环境可评估 Apptainer/Singularity,减少 root daemon 依赖并复用宿主文件系统。
6. Kubernetes 编排与拓扑感知调度
6.1 GPU Operator 与 Device Plugin
Kubernetes 通过 device plugin framework 暴露 GPU。NVIDIA 的 k8s-device-plugin 会注册 nvidia.com/gpu,也能在 MIG 或 sharing 配置下注册 nvidia.com/mig-*、nvidia.com/gpu.shared 等资源。
NVIDIA GPU Operator 把 driver、Container Toolkit、device plugin、DCGM exporter、GPU Feature Discovery 和 MIG Manager 这些组件变成可声明的集群状态。对生产集群来说,它的价值不只是“装驱动”,更是让所有 GPU 节点的软件栈一致、可升级、可观测。
图源:NVIDIA Technical Blog:NVIDIA GPU Operator: Simplifying GPU Management in Kubernetes
kubectl describe node < gpu-nod e > | grep -E "nvidia.com/gpu|nvidia.com/mig"
kubectl get pods -n gpu-operator
kubectl logs -n gpu-operator -l app=nvidia-device-plugin-daemonset --tail=100
DCGM exporter 能把 GPU 利用率、显存、温度、功耗和 MIG 指标导出到 Prometheus。排查“调度看似成功但吞吐不稳定”时,DCGM 指标要和 kubelet、node exporter、NCCL 日志一起看。
6.2 Topology Manager
默认 Kubernetes 调度器只知道资源数量,不理解 GPU、CPU、NUMA、NIC、NVLink domain 的距离。请求 4 张 GPU 成功,不代表这 4 张 GPU 在同一个 NVLink clique,也不代表 CPU 和 NIC 离它们最近。
Kubernetes Topology Manager 在 kubelet admission 阶段汇总 CPU Manager、Device Manager 等 hint provider 的拓扑提示。常用策略:
策略 行为 适用场景 none不做拓扑对齐 默认配置、非性能敏感作业 best-effort尽力对齐,不满足也放行 共享集群、推理服务 restricted不满足 preferred hint 则拒绝 拓扑敏感训练作业 single-numa-node资源必须落在单个 NUMA node CPU/NIC/GPU 本地性要求强
图源:Kubernetes Topology Manager
Topology Manager 要和 CPU Manager static policy 配合。只有 Guaranteed Pod 且 CPU request 是整数时,CPU Manager 才会分配 exclusive CPUs。
resources :
requests :
cpu : "16"
memory : "128Gi"
nvidia.com/gpu : "4"
limits :
cpu : "16"
memory : "128Gi"
nvidia.com/gpu : "4"
在 GB200 NVL72 这类系统上,拓扑感知尤其关键。NVIDIA GB200 NVL tuning guide 说明 GB200 Grace Blackwell Superchip 通过 900 GB/s NVLink-C2C 连接 Grace CPU 和两颗 Blackwell GPU;rack 级 NVLink domain 的价值只有在调度和通信路径也对齐时才能体现。
kubernetes_topology_pod.yaml 展示了完整的拓扑敏感 Pod 配置:GPU/CPU/memory requests 与 limits、hostNetwork、NCCL 环境变量、IPC_LOCK、/dev/shm memory volume、hostPath 数据目录和 node affinity。securityContext :
capabilities :
add : [ "IPC_LOCK" ]
volumes :
- name : dshm
emptyDir :
medium : Memory
sizeLimit : 8Gi
6.3 网络优化
多节点训练里,网络路径与 GPU 拓扑同等重要。NCCL 可能走 NVLink、PCIe P2P、InfiniBand/RDMA、RoCE 或普通 TCP socket;错误的 NIC、CNI overlay 或安全策略会直接压低 all-reduce 带宽。
常用检查项:
hostNetwork: true 用于 MPI/NCCL/RDMA 敏感任务,减少 CNI overlay 变量。
RDMA device plugin 暴露 HCA 资源,避免 Pod 看不到 InfiniBand device。
NCCL_SOCKET_IFNAME 指向 IB/RoCE 网卡,避免 NCCL 选到管理网口。
NCCL_NET_GDR_LEVEL 控制 GPU Direct RDMA 距离策略;NVIDIA NCCL environment variables 文档说明它用于控制 NIC 与 GPU 距离在什么范围内启用 GDR。
/dev/shm 足够大,避免 NCCL shared memory fallback 异常。
env :
- name : NCCL_SOCKET_IFNAME
value : "ib0"
- name : NCCL_NET_GDR_LEVEL
value : "3"
- name : NCCL_DEBUG
value : "INFO"
NCCL_DEBUG=INFO 和 NCCL_TOPO_DUMP_FILE=/tmp/nccl_topo.xml 是低成本诊断入口。训练性能异常时,先确认 NCCL 选到的 NIC、GPU bus id、P2P level 和 ring/tree 构造,再看模型代码。
6.4 资源隔离与 QoS
Kubernetes QoS 文档 定义了 Guaranteed、Burstable、BestEffort 三类。节点内存紧张时,Kubernetes 会优先驱逐 BestEffort,其次是 Burstable,最后是 Guaranteed。
QoS 级别 条件 GPU 作业建议 Guaranteed每个容器 CPU/memory request 等于 limit 性能敏感训练、CPU pinning Burstable设置了部分 request/limit 弹性推理、实验作业 BestEffort无 request/limit 不适合 GPU 生产作业
GPU 训练 Pod 通常应该做到:
CPU request/limit 使用整数并相等,配合 CPU Manager static policy。
memory request/limit 预留 DataLoader、pinned memory、page cache 和 checkpoint 峰值。
给 /dev/shm 显式 memory-backed emptyDir,不要依赖容器默认 64 MB。
训练节点预留 housekeeping CPU,避免 kubelet/containerd/监控 agent 与 DataLoader worker 抢核。
I/O 隔离用本地 NVMe 分区、cgroup v2 I/O controller 或存储层 QoS 补齐;Kubernetes 默认资源模型不直接表达磁盘带宽。
6.5 MIG 在 Kubernetes 中的使用
MIG 在 Kubernetes 中表现为扩展资源。例如配套 YAML 使用 nvidia.com/mig-2g.45gb 请求两个 MIG 实例:
resources :
requests :
nvidia.com/mig-2g.45gb : "2"
limits :
nvidia.com/mig-2g.45gb : "2"
GPU Operator MIG Manager 可根据节点标签和配置管理 MIG mode 与 profile。典型做法是把节点池分成 full-GPU、MIG-small、MIG-large 几类,避免在同一节点上频繁重配 profile。
MIG 资源不能跨节点拼接。一个 Pod 请求两个 2g.45gb 实例时,单个节点必须同时有两个可用实例;集群总量足够但分散在不同节点上,Pod 仍会 Pending。资源规划应从典型模型尺寸和实例并发数反推 MIG profile。
kubernetes_mig_pod.yaml 演示了 nvidia.com/mig-2g.45gb 的 requests/limits,以及用 nodeSelector: mig-enabled: "true" 约束到 MIG 节点。
6.6 调度器:Kubernetes、Slurm 与 Slinky
训练平台和推理平台的调度目标不同。训练更像 HPC batch:一组 rank 要同时拿到 GPU、CPU、内存、网络和文件系统带宽;推理更像在线服务:关注 rollout、autoscaling、灰度、SLO、可观测和多租户隔离。调度器选型不是“谁更先进”,而是资源模型和团队操作模型是否匹配。
维度 Slurm Kubernetes Slinky 核心模型 作业队列 + 节点分配 Pod/Service + 声明式控制面 把 Slurm 能力带入 Kubernetes 典型入口 sbatch、srun、sallocJob、Deployment、RayJob、KueueSlurm CRD、slurm-operator、slurm-bridge GPU 表达 GRES/TRES,如 gres/gpu extended resource,如 nvidia.com/gpu Slurm 语义或 Kubernetes workload 统一调度 队列与公平性 partition、QoS、fair-share、accounting 成熟 需要 Kueue/Volcano/自定义 scheduler 补齐 复用 Slurm 策略,同时由 K8s 管 Slurm 组件 Gang scheduling 原生适合多节点 batch Kubernetes v1.35 进入 alpha,默认关闭 由 Slurm 负责成组调度 服务化能力 不是主场 rollout、HPA、Service、Ingress 成熟 Slurm 作业与 K8s 服务共存 运维代价 HPC 体系成熟,但云原生集成弱 云原生生态成熟,但 GPU batch 语义弱 控制面更复杂,需要明确所有权
Slurm 的优势在 GPU batch 语义。 Slurm GRES 文档 说明 GPU、MIG、MPS 都可以通过 Generic Resources 建模。训练作业可以直接表达“每节点 8 张 GPU、每 GPU 多少 CPU、按节点启动 rank”:
srun \
--nodes=16 \
--ntasks-per-node=8 \
--gpus-per-node=8 \
--cpus-per-gpu=12 \
--mem-bind=local \
--gpu-bind=closest \
torchrun --nproc_per_node=8 train.py
Slurm 会给作业设置 CUDA_VISIBLE_DEVICES,并把 GPU 分配纳入 accounting。MIG 也能作为 GPU-like GRES 暴露,但文档明确提到 Slurm 期望 MIG 设备已经提前分区,不负责动态创建/销毁 MIG profile。Slurm 的 gres.conf 还存在一个容易误解的点:GPU affinity 在 scheduler 内部主要按 socket 处理,Cores= 配置不等于严格的 core-level 调度保证;rank 到 CPU/GPU/NIC 的精确绑定仍要结合 --cpu-bind、--gpu-bind、NUMA 策略和作业启动脚本验证。
Kubernetes 的优势在服务化和生态。 GPU 在 K8s 中通常是 nvidia.com/gpu 这类 extended resource。它适合 vLLM/Triton/KServe/Ray Serve 这类长期运行服务,也适合用 Operator 管理平台组件。Batch 训练需要额外补齐几件事:
用 Kueue 处理队列、quota、fair-sharing 和作业 admission。
用 Topology Manager、CPU Manager static policy、GPU Operator/GFD/DRA 处理节点内拓扑。
用 gang scheduling 或调度插件保证多 rank 训练不是“部分 Pod 先启动、部分 Pod Pending”。
用节点池、taint/toleration、priority class 和 preemption 管理训练、推理、平台组件的优先级。
Kubernetes Gang Scheduling 在 v1.35 仍是 alpha 且默认关闭。对大规模同步训练来说,这意味着默认 kube-scheduler 不是 Slurm 的直接替代品;需要 Kueue、Volcano 、scheduler extender 或厂商调度器补齐 batch 语义。
Slinky 适合已有 Slurm 投资但希望统一 Kubernetes 运维面的团队。 SchedMD 的 Slinky 页面 把它定义为一组 Slurm/Kubernetes 互操作项目:
图源:NVIDIA Technical Blog:Running Large-Scale GPU Workloads on Kubernetes with Slurm
组件 作用 slurm-operator在 Kubernetes 中运行 Slurm control plane 和 worker daemon,用 CRD/Helm 管理 Slurm 集群生命周期 slurm-bridge让 Slurm 作为 Kubernetes scheduler,调度 Slurm 与部分 Kubernetes workload slurm-client通过 Slurm REST API 访问 Slurm 的 Go library containersSlurm 组件容器镜像
这带来两种不同部署模式:
模式 用户体验 平台体验 适合场景 Slurm on Kubernetes 研究员仍然用 sbatch/srun Kubernetes 管理 Slurm daemon、升级、监控和自愈 训练集群已有 Slurm 脚本、QoS、accounting 积累 Slurm as Kubernetes scheduler 用户提交 Kubernetes workload Slurm 统一处理队列、优先级和资源分配 需要同一批 GPU 同时承载 Slurm job 和 K8s job
NVIDIA 的 Running Large-Scale GPU Workloads on Kubernetes with Slurm 报告了生产部署中 Slinky slurm-operator 管理超过 8,000 张 GPU、与非容器化 Slurm 集群保持 NCCL 通信性能一致、并把 Slurm metrics 纳入 Prometheus/Grafana 的案例。这个结论的工程含义不是“所有团队都应迁移到 Slinky”,而是:当平台团队已经选择 Kubernetes 作为节点生命周期、镜像、监控、升级和安全边界,Slurm 可以作为 batch 调度能力保留下来。
大规模训练的默认选择仍然可以是 Slurm;在线推理和平台服务的默认选择仍然可以是 Kubernetes。Slinky 的价值在于减少“两套集群、两套监控、两套节点生命周期”的重复建设,而不是把 Slurm 或 Kubernetes 其中一个完全替换掉。
Rack-scale GPU 系统还会把调度问题推到拓扑层。NVIDIA rack-scale scheduling 文章 指出,Slurm 和 Kubernetes 都需要额外机制理解 NVLink fabric、IMEX/ComputeDomain 这类跨节点高速互连。GB200 NVL72 上,调度器只分配“GPU 数量”已经不够,还要确保这些 GPU 属于正确的 NVLink partition,并且 NIC、CPU、IMEX daemon 生命周期与作业边界一致。
图源:NVIDIA Technical Blog:Running AI Workloads on Rack-Scale Supercomputers
问题 倾向选择 研究员已有大量 sbatch 脚本、partition/QoS/accounting 规则 Slurm 主要负载是 vLLM/Triton/KServe/Ray Serve 在线服务 Kubernetes 同步训练需要严格 gang scheduling、fair-share 和排队 Slurm 或 Kueue/Volcano 加强后的 Kubernetes 平台团队希望用 Kubernetes 管 Slurm daemon、镜像、升级和监控 Slinky slurm-operator 需要 Slurm 统一调度部分 Kubernetes workload Slinky slurm-bridge GPU 拓扑跨 rack、跨 NVLink partition,且作业强依赖 collective 性能 优先选择能表达拓扑域的调度方案,并用 NCCL benchmark 验证
核心思想:让 GPU 永远不等待
本章所有优化围绕同一目标:消除导致 GPU 空闲的一切因素。
CPU 不要拖后腿 :NUMA 绑定、CPU pinning、关闭 swap、performance 频率模式。
数据传输不要成为瓶颈 :pinned memory、hugepages、GPUDirect RDMA/GDS、双缓冲预取。
驱动不要引入额外延迟 :persistence mode、MPS/MIG 合理使用。
容器不要增加开销 :bind mount、host networking、正确 CUDA/driver 版本匹配。
调度不要分配错资源 :Topology Manager、NUMA/NVLink 对齐。
FAQ
GPU 最常见的空闲原因不是 GPU 自身的问题,而是 CPU 没能及时准备好数据。数据加载、tokenize、H2D 传输、内核分发都发生在 CPU 侧,任何一步慢了 GPU 就只能等待。
--cpunodebind 控制 CPU 线程在哪些核心上跑,--membind 控制内存从哪个节点分配。只用 --cpunodebind 但不限制内存分配,OS 可能把内存分配到远端节点,每次访问仍需跨节点。两者需要配合使用。
DataLoader worker 不初始化 CUDA context
DataLoader worker 是独立的 CPU 子进程,负责数据加载和预处理。如果在 worker 中调用 CUDA API,会在每个 worker 中初始化一个 CUDA context,浪费 GPU 内存并可能导致多进程竞争。worker 只做 CPU 工作,CUDA 操作留给主进程。
MPS 将多个进程的 GPU context 合并为一个,允许内核并发执行,适合多个小推理任务共享一个 GPU。MIG 在硬件级别将 GPU 分成独立实例,提供强隔离,适合多租户环境。大规模训练需要每个进程独占完整 GPU 并依赖 NVLink P2P 通信,而 MPS 无内存隔离、MIG 会禁用 P2P。
容器共享宿主 OS 内核,没有 hypervisor 虚拟化层。NVIDIA Container Toolkit 在容器启动时直接注入宿主的 driver 库,GPU kernel 的执行路径和裸机完全相同。性能差异来自 overlay FS 的 I/O 开销,通过 bind mount 可以绕过。
默认调度器不感知硬件拓扑。请求 4 个 GPU 可能被分配到不同 NVLink domain 或不同 NUMA 节点上的 GPU,导致 GPU 间通信走慢速的 PCIe/QPI 而非 NVLink。需要配置 Topology Manager 和 NVIDIA GPU Operator 实现拓扑感知调度。
Swap 策略:swappiness 与 swapoff
vm.swappiness=0 告诉内核尽量避免 swap,但在极端内存压力下仍可能 swap。swapoff -a 完全禁用所有 swap 设备,任何情况都不会 swap;如果内存不够,OOM killer 会直接终止进程。训练场景通常用 swapoff -a 配合足够的物理内存。