Skip to main content

本章概要

本章围绕一个核心问题:即使 GPU 代码和库已经高度优化,系统层面的瓶颈仍然会限制大规模 AI 训练的性能。最快的 GPU 也只有在数据和指令能高效到达的前提下才能发挥全部潜力。系统级调优常被忽视,但在大集群上,仅 OS 配置的小改动就可能带来两位数百分比的性能提升,换算到大型 AI 项目就是数十万甚至上百万美元的计算成本节约。 全章从底向上覆盖了四个层次的优化:操作系统 → GPU 驱动与运行时 → 容器运行时 → Kubernetes 编排。每层的目标一致——最小化延迟、最大化吞吐量,让 GPU 始终处于满负荷工作状态。

章节详解

1. NVIDIA 软件栈全景

GPU 集群的运行远不止编写 PyTorch 代码。完整的软件栈从底到顶:
PyTorch → GPU 调用链路:从 Python 代码到 GPU 执行的完整软件栈
层级组件职责
高层框架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 特性。

1.2 CUDA Toolkit & Runtime

在 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 绑定
cuTiletile 化矩阵运算抽象,简化分块计算
cuPyNumericNumPy 的 GPU 替代品(import cupynumeric as np
CuPyGPU 加速的数组编程
WarpPython 中编写 GPU kernel
CUTLASScuBLAS 底层使用的 C++ 模板库,提供可组合的 GEMM/卷积原语
TritonOpenAI 的 Python DSL,已集成进 PyTorch 编译器后端

1.4 PyTorch 与高层框架

PyTorch 的编译器栈(torch.compile)由三部分组成:
  1. TorchDynamo — 捕获 Python 代码的计算图
  2. AOT Autograd — 提前生成反向传播图
  3. TorchInductor — 后端代码生成,使用 Triton 做 kernel 融合和自动调优
PyTorch 抽象了 CUDA 编程的复杂性——你写直观的 Python 代码,底层调用高度优化的 CUDA 例程。

2. CPU 与操作系统调优

GPU 利用率不高的最常见原因:CPU 没能及时喂数据给 GPU。CPU 负责数据加载、tokenize、变换、分发内核、线程协调——任何一步慢了,GPU 就空闲。

2.1 NUMA 感知与 CPU 绑定

2.1.1 什么是 NUMA
在笔记本和小型台式机中,通常只有一个 CPU 和一组内存,所有内存访问速度相同——这称为 UMA(Uniform Memory Access,统一内存访问)。但大多数数据中心服务器是 NUMA(Non-Uniform Memory Access,非统一内存访问)系统:每个 CPU 拥有自己的内存控制器,直接连接一部分本地内存。每个 CPU 可以通过自己的内存控制器快速访问本地内存,也可以通过节点间链路访问另一个 CPU 的远程内存,但本地和远程内存的访问延迟和带宽不同,这就是”非统一”的含义。 双 socket NUMA 架构:每个 CPU Package 拥有独立的内存(DIMM)和核心,构成一个 NUMA 节点 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
#!/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() 按优先级依次尝试四种方式:
  1. NVML 直接查询 — 调用 nvmlDeviceGetNumaNodeId(),直接获取 GPU 的 NUMA 节点编号。仅适用于 GPU 本身是 NUMA 节点的平台(如 Grace Hopper/Grace Blackwell 超级芯片)
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
...
参考资料:
  1. NVML CPU 亲和性推断 — 如果上一步不可用,调用 nvmlDeviceGetCpuAffinity() 获取该 GPU 关联的 CPU 掩码(bitmask),然后与 sysfs 中的 CPU→NUMA 映射表对照,取出现次数最多的 NUMA 节点
# 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 之间的互连类型
  1. sysfs 内核接口 — 如果 NVML 完全不可用,直接读 /sys/bus/pci/devices/<PCI_ID>/numa_node,这是 Linux 内核为每个 PCI 设备维护的 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 列一致。
  1. 当前进程策略兜底 — 如果以上都失败,运行 numactl --show 读取当前进程的 preferred node。例如如果外部已经用 numactl 命令启动了脚本(如 numactl --cpunodebind=0),进程会继承这些策略,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 子进程中可继承,但 spawnexec 模式下不会继承。Python 框架管理的 worker 进程必须显式重新设置 CPU 和内存策略。
性能影响: 仅消除跨 NUMA 流量和 CPU 核心迁移,训练吞吐量就可提升 5%–10%,同时减少性能抖动。
ch03/baseline_numa_unaware.py 使用约 512 MB 的 pageable 内存和阻塞拷贝;ch03/optimized_numa_unaware.py 使用 pinned memory + 双缓冲 + 异步拷贝流。运行对比:
cd ai-performance-engineering/code
python -m ch03.compare --examples numa_unaware
你也可以用 nvidia-smi topo -m 查看你的系统拓扑,再用 numactl --show 确认当前进程的绑定状态。

2.2 内存绑定与 Pinned Memory

--membind 强制在指定 NUMA 节点分配内存,保证 memory → CPU → GPU 链路在同一 NUMA 节点内。 Pinned(page-locked)memory 是高效 GPU 访问的基础:
  • OS 不会 swap 或移动它。
  • GPU/NIC 可直接 DMA。
  • 拷贝速度比 pageable memory 快 2–3×
  • PyTorch DataLoader(pin_memory=True) 配合 tensor.to(device, non_blocking=True) 可提升 10%–20% 性能。
# 用 CUDA 自带工具测量 pinned vs pageable 传输带宽
bandwidthTest --memory=pinned
bandwidthTest --memory=pageable
这也是 GPUDirect RDMA(NIC 直通 GPU 内存)和 GPUDirect Storage(NVMe 直通 GPU 内存)的基础。
OS 对用户可锁定内存有限制(ulimit -l),容器环境中需通过 --ulimit memlock=-1 调整。AI 训练通常设为 unlimited
ch03/baseline_docker.py 使用非 pinned 内存 + 阻塞拷贝ch03/optimized_docker.py 使用 pinned 内存 + 双缓冲预取 + copy stream 异步重叠
python -m ch03.compare --examples docker
optimized 版本的核心优化——Prefetcher 类:
class Prefetcher:
    def __init__(self, device, host_batches, targets):
        self.copy_stream = torch.cuda.Stream()
        self.buffers = [torch.empty_like(..., device=device), ...]  # 双缓冲
        self._prefetch()

    def _prefetch(self):
        with torch.cuda.stream(self.copy_stream):
            self.buffers[self.next_slot].copy_(host_batch, non_blocking=True)

    def next(self):
        torch.cuda.current_stream().wait_stream(self.copy_stream)
        self.cur_slot, self.next_slot = self.next_slot, self.cur_slot
        self._prefetch()
        return self.buffers[self.cur_slot], ...
README 中报告的实测结果:baseline 4.456 ms → optimized 1.225 ms3.64× 加速

2.3 Transparent Hugepages (THP)

Linux 默认使用 4 KB 页,但管理数百万小页对大内存进程效率低。Hugepages(2 MB / 1 GB)可减少 TLB miss 和 page fault。
场景建议原因
训练(关注吞吐)echo always > /sys/kernel/mm/transparent_hugepage/enabled~3%–5% 吞吐提升
推理(关注延迟)echo never > /sys/kernel/mm/transparent_hugepage/enabled避免后台 compaction 引起的不可预测停顿
折中方案echo madvise > ...按需使用
对于非常大的 pinned buffer(如 I/O 预分配),可考虑用 vm.nr_hugepageshugetlbfs 手动分配显式 hugepages 以获得更确定性的性能。
ch03/system_tuning.sh 包含了 THP 配置。查看当前状态和切换:
cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出示例: [always] madvise never

# 切换为训练模式
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

# 切换为推理模式
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled

2.4 调度器与中断亲和性

  • 使用 isolcpusnohz_full 内核参数或 cgroup cpuset 隔离计算核心,防止 OS 调度器抢占。
  • 将 GPU/NIC 硬件中断绑定到同一 NUMA 节点的 CPU 核心,避免跨节点中断处理。
  • 禁用或定制 irqbalance;手动设置 /proc/irq/*/smp_affinity
# 绑定 GPU 中断到特定 CPU(来自 system_tuning.sh)
for irq in $(grep nvidia /proc/interrupts | cut -d: -f1); do
    echo 2 > /proc/irq/$irq/smp_affinity
done
ch03/cpu_gpu_numa_optimizations.sh 是为 GB200/GB300(Grace CPU + Blackwell GPU)定制的完整调优脚本,覆盖 CPU governor、NUMA balancing、THP、persistence mode、IRQ 亲和性。

2.5 虚拟内存与 Swap

GPU 程序分配大量 host memory。如果 OS 把数据 swap 到磁盘,GPU 会经历多个数量级的延迟增加
# 设置 swappiness 为 0(仅极端内存压力时才 swap)
echo 0 | sudo tee /proc/sys/vm/swappiness

# 完全禁用 swap(确保有足够 RAM)
sudo swapoff -a

# 监控 swap 使用
free -m | grep Swap

2.6 文件系统缓存与 Checkpoint 写入

大训练任务需要频繁写 checkpoint。multi-GB 的 checkpoint 可能填满 page cache 并导致 stall。
# 调整 dirty ratio,让 OS 在 RAM 中缓冲更多数据再刷盘
echo 20 | sudo tee /proc/sys/vm/dirty_ratio
echo 10 | sudo tee /proc/sys/vm/dirty_background_ratio
对延迟敏感的场景,可以:
  • O_DIRECT 绕过 page cache。
  • io_uring 异步 I/O。
  • 写完 checkpoint 后用 posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) 立即释放 cache。
  • PyTorch 支持分布式 checkpoint 分区写入。

2.7 CPU 频率与 C-states

默认省电模式下,空闲 CPU 会降频或进入深度睡眠。唤醒延迟虽然只有几微秒,但积累起来会导致 GPU bubble(GPU 等 CPU 恢复数据处理的空闲期)。
# 设置 CPU 为高性能模式
cpupower frequency-set -g performance

# 内核启动参数限制 C-states(需修改 GRUB)
# GRUB_CMDLINE_LINUX="... processor.max_cstate=1 intel_idle.max_cstate=0"

2.8 Host 内存分配器调优

jemalloctcmalloc 比 glibc 默认 malloc 更适合 GPU 服务器的多线程数据准备场景。
# jemalloc 调优
export MALLOC_CONF="narenas:8,dirty_decay_ms:10000,muzzy_decay_ms:10000,background_thread:true"

# tcmalloc 调优
export TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES=$((512*1024*1024))
export TCMALLOC_RELEASE_RATE=16
ch03/gpu_setup_commands.sh 汇集了上述所有 OS 级命令:persistence mode、CPU governor、swapoff、jemalloc/tcmalloc 配置、ulimit、GPUDirect RDMA/GDS、NUMA 绑定和 Docker 示例。可作为 GPU 服务器初始化的 checklist。

3. GPU 驱动与运行时设置

3.1 GPU Persistence Mode

默认 GPU 空闲时驱动会卸载上下文,下次使用需 1-2 秒初始化。
# 启用 persistence mode
nvidia-smi -pm 1
systemctl enable nvidia-persistenced
不加速计算本身,但消除冷启动延迟。对频繁启停的训练集群和低流量推理场景很重要。

3.2 Multi-Process Service (MPS)

默认多进程共享 GPU 时采用时间片轮转——GPU 在进程间 “ping-pong” 切换,利用率低。MPS 将多个进程的 context 合并为一个调度上下文,允许内核并发执行 场景示例: 4 个推理任务共享一个 40 GB GPU,每个只用 5-10 GB 和 30% 计算。默认 time-slicing 下,任一时刻只有一个任务在运行,GPU 平均 70% 空闲。启用 MPS 后,多个任务的 kernel 可交错执行填满 GPU——如果两个进程各使用 40% GPU 计算,MPS 可将整体利用率提升到 80%–90%。 时间效果: 两个各需 1 小时的训练任务在同一 GPU 上——顺序执行需约 2 小时,MPS 并发后仅需约 1 小时多。
# 启动 MPS daemon
nvidia-cuda-mps-control -d

# 限制单个 client 使用 50% SM(QoS 保证)
export CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=50
局限:
  • MPS 不隔离 GPU 内存——一个进程 OOM 会导致共享该 GPU 的所有进程终止。
  • 默认所有 MPS client 必须为同一 Unix 用户(现代驱动已支持多用户 MPS,但仍无内存隔离)。
  • 单个进程已 100% 占满 GPU 时,MPS 无法提供额外加速——仅在有空闲资源可填充时有效。
  • Kubernetes 中还有另一种替代方案:GPU time-slicing(通过 device plugin 的复制因子实现),不重叠执行但提供更快速的切换。
python -m ch03.mig_mps_tool --device 0
报告当前 GPU 的 MIG 模式状态、MIG 设备列表、MPS 进程信息。

3.3 Multi-Instance GPU (MIG)

硬件级分区,将一个 GPU 最多切分为 7 个独立实例,每个有独立的 SM、内存、引擎上下文和 L2 cache 份额。 Blackwell B200 MIG profiles:
Profile内存占比SM 占比L2 cacheCopy Engines
1g.23gb1/81/7 (~19 SMs)1/82
1g.45gb2/81/7 (~19 SMs)2/82
2g.45gb2/82/7 (~38 SMs)2/83
3g.90gb4/83/74/86
4g.90gb4/84/74/88
7g.180gbFullFullFull16
命名规则:<X>g = SM 组数量(计算),<Y>gb = HBM 容量(内存)。这种双维度方案将计算容量与内存容量分离——管理员可组合不同 profile,但只能使用硬件支持的固定 profile,不能自创新的切分大小。 运维注意: 启用/禁用 MIG 模式需要重置 GPU;但在 MIG 模式下,可动态创建和销毁 MIG 分区而无需重启整个系统(需先排空现有工作负载)。管理员可通过 nvidia-smi -mig 或 NVIDIA Kubernetes GPU Operator 的 nvidia.com/mig.config config map 配置 MIG profile。 MPS vs MIG 选择:
维度MPSMIG
隔离性无内存隔离强隔离(独立 SM + 内存 + 引擎)
灵活性动态共享资源硬分区,空闲资源不可借用
GPU 间通信正常 NVLink/PCIe P2PMIG 模式下 P2P 被禁用
适用场景多推理任务填充 GPU 利用率多租户需要资源保证
大规模训练不需要(一进程一 GPU)不适用(需要全 GPU + 快速互连)
MIG 模式下 GPU 间的 peer-to-peer 通信(包括 NVLink)被禁用。CUDA IPC 也受限。大规模分布式训练和 MoE 稀疏推理不适合 MIG。

3.4 GPU 时钟与 ECC

  • GPU Boost 自动在功耗/温度限制内调整频率。
  • Benchmark 时必须锁定时钟,否则后续 run 可能因热节流而变慢,导致错误结论。
# 锁定 GPU 核心时钟(benchmark 用)
nvidia-smi -lgc <min_clock>,<max_clock>
# 锁定显存时钟
nvidia-smi -ac <mem_clock>,<sm_clock>
# 设置功耗上限(低于 TDP)减少热节流
nvidia-smi -pl <watts>
  • ECC 内存在数据中心 GPU 上默认启用,应保持启用。长训练中单 bit 错误可能 crash 或悄悄污染模型。
python -m ch03.power_tuning_tool --power-limits 300,350 --iterations 5 --warmup 1
扫描不同功耗上限下的 GEMM 性能,输出 Time、TFLOP/s、Avg/Max Power、TFLOP/J(性能功耗比)。

3.5 GPU 内存管理

  • PyTorch 按需分配 GPU 内存;TensorFlow 默认占满(TF_FORCE_GPU_ALLOW_GROWTH=true 改为按需)。
  • CUDA Unified Memory + Hopper/Blackwell 的 Page Migration Engine (PME) 支持 GPU-CPU 自动页面迁移——但有性能惩罚,是安全网不是优化手段。
  • PyTorch caching allocator(通过 PYTORCH_ALLOC_CONF 配置)减少碎片和重复分配。
  • GPU OOM 时:用 torch.cuda.memory_stats()torch.cuda.memory_summary() 诊断;NVIDIA Nsight Systems 可显示内存使用模式;Nsight Compute 提供 kernel 级占用率、吞吐量和 NVLink 使用分析。
  • Docker --gpus 可选择和暴露 GPU,但不支持设置 GPU 内存上限——如需硬隔离 GPU 内存/计算,使用 MIG 分区或 MPS 配合 CUDA_MPS_ACTIVE_THREAD_PERCENTAGE
  • 建议始终保持 GPU driver loaded(类似 persistence mode),避免 Job 间重载 driver 和重建 MIG 配置的开销。

4. 容器运行时优化

4.1 NVIDIA Container Toolkit

容器共享宿主 OS 内核,没有 hypervisor 层。GPU 性能与裸机几乎无差别(<2% 差异)——MLPerf Inference v5.0 就是在 OpenShift/Kubernetes 容器中提交的。 工作原理:
  • 容器内提供 CUDA runtime 库(libcudart.so)。
  • NVIDIA Container Toolkit 在容器启动时注入宿主的 driver 库(libcuda.solibnvidia-ml.so)。
  • 关键规则:宿主 NVIDIA driver 版本必须 ≥ 容器内 CUDA 版本要求的最低 driver 版本(如 CUDA 13.x 要求 R580+,CUDA 12.x 要求 R525+)。
NVIDIA Container Toolkit 不仅支持 Docker,也支持 containerd 和 Podman。在使用 containerd 作为默认容器运行时的现代 Kubernetes 环境中同样适用。推荐使用 NGC 官方 Docker 镜像(如 nvcr.io/nvidia/pytorch),内置正确版本的 CUDA runtime、cuDNN、NCCL 等,避免依赖问题。

4.2 避免 Overlay FS 开销

容器的 union filesystem(OverlayFS)在多层查找和 Copy-on-Write 时有额外开销。训练数据应通过 bind mount 挂载:
docker run --gpus all \
  -v /data/dataset:/mnt/dataset:ro \
  -v /output:/mnt/output \
  nvcr.io/nvidia/pytorch:25.09-py3 python train.py
配套代码提供了完整的多阶段 Dockerfile 示例:
FROM nvcr.io/nvidia/pytorch:25.11-py3 AS base
RUN apt-get update && apt-get install -y numactl libnuma-dev tcmalloc-minimal jemalloc

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"]
CMD ["train.py"]
关键点:基于 NGC 官方镜像、安装 numactl/jemalloc/tcmalloc、设置所有环境变量、使用 numactl --interleave=all 作为入口点。

4.3 减小镜像体积

容器镜像过大会导致拉取和启动变慢。虽然对于长时间运行的训练任务来说,几分钟的启动时间可以忽略,但仍建议不要在镜像中包含不必要的构建工具和临时文件,以节省磁盘空间。一些 HPC 中心偏好使用 Singularity(Apptainer)替代 Docker,因为它可在用户空间运行、无需 root daemon,并直接使用宿主文件系统。

5. Kubernetes 编排与拓扑感知调度

5.1 GPU Operator 与 Device Plugin

  • NVIDIA Device Plugin 向 K8s 暴露 GPU 设备(nvidia.com/gpu),自动挂载设备节点到 Pod。
  • GPU Operator 自动化安装 driver、device plugin、Container Toolkit、DCGM 监控;通过 GPU Feature Discovery 为每个 GPU 打上 NUMA/NVLink 拓扑标签。

5.2 Topology Manager

默认 K8s 不感知拓扑。 请求 4 个 GPU 可能被分配到不同 NVLink domain,通信带宽减半。 对于 NVL72 系统(72 GPU,rack 级 ~130 TB/s NVLink 带宽,1.8 TB/s/GPU),不感知拓扑的调度完全浪费了 NVLink 优势。 配置 --topology-manager-policy
策略行为
best-effort尽力满足拓扑对齐,不保证
restricted拓扑不满足则拒绝调度
single-numa-node所有资源必须在同一 NUMA 节点
配套代码提供了拓扑感知 Pod 的完整 YAML 示例:
spec:
  hostNetwork: true  # 直通 InfiniBand
  topologyManager:
    policy: "best-effort"
  containers:
  - name: training-container
    resources:
      limits:
        nvidia.com/gpu: "4"
        cpu: "16"
        memory: "64Gi"
    env:
    - name: NCCL_SOCKET_IFNAME
      value: "ib0"
    - name: NCCL_NET_GDR_LEVEL
      value: "3"           # GPUDirect RDMA
    - name: NCCL_MNNVL_ENABLE
      value: "1"           # Multi-Node NVLink
    securityContext:
      capabilities:
        add: [IPC_LOCK]    # pinned memory 必需
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: gpu.nvidia.com/nvlink-domain
            operator: Exists
关键点:host networking 直通 IB、NCCL 环境变量(NCCL_SOCKET_IFNAMENCCL_NET_GDR_LEVELNCCL_MNNVL_ENABLE)、IPC_LOCK 权限(pinned memory 必需)、NVLink domain 节点亲和性。实际部署时还需挂载 shared memory(如 emptyDir: medium: Memory, sizeLimit: 8Gi)供 NCCL 使用。

5.3 网络优化

  • 性能敏感 GPU 作业使用 host networkinghostNetwork: true),让容器直接使用宿主网络接口,避免 overlay network 开销。对 MPI 作业尤其有用,免去为每个 rank 配置端口映射。
  • 如果因安全策略无法使用 host networking,需确保 CNI 和 overlay network 能承载所需流量——延迟需保持在低水平,且运行在内核空间,避免用户空间代理限流。
  • 安装 Kubernetes RDMA device plugin(Mellanox),启用 GPUDirect RDMA,让 GPU 与 NIC 直接交换数据,绕过 CPU。
  • 配置 NCCL_PORT_RANGENCCL_SOCKET_IFNAME 帮助 NCCL 在 overlay 网络下建立连接。

5.4 资源隔离与 QoS

Kubernetes 通过 Linux cgroups 实现资源隔离。可为 Pod 定义 requests(保证的最小资源)和 limits(允许的最大资源),超出 limit 的容器会被 throttle 或 OOM kill。可使用 CPU Manager 将核心 pin 到特定 Pod,避免上下文切换。
QoS 级别条件驱逐优先级
Guaranteed每个容器的 requests == limits(CPU + memory)最后
Burstable至少设了一个 request 或 limit,但不满足 Guaranteed 条件中间
BestEffort无 requests/limits最先被驱逐
OOM Killer 风险: Linux OOM Killer 使用启发式决定终止哪个进程——有时会选择内存占用最大的 Pod,即你的大训练/推理 Job。建议:
  • 不要为训练容器设过低的 memory limit,留出足够 headroom 避免 OOM Killer 在长训练中途杀掉 Job。
  • 如果不设 memory limit,需配合监控和告警确保 Job 不会意外过度分配。
  • 性能敏感作业独占整个节点的 CPU 和 GPU,防止其他容器抢占资源。
编排抖动: 每个节点上运行的 kubelet、容器运行时守护进程和监控 agent 会消耗少量 CPU(通常几个百分比),不会显著影响 GPU 训练。但如果同一节点混合运行训练和推理负载,共享 CPU/I/O 资源可能导致不可预测的性能抖动。同构工作负载(全训练或全推理)在系统调优和调试方面远比异构混合简单。 I/O 隔离: Kubernetes 原生不支持 I/O 隔离。如需防止重 I/O 工作负载间的相互干扰,需手动配置 cgroup v2 I/O controller 或在 OS 层面分区 I/O 资源。

5.5 MIG 在 Kubernetes 中的使用

MIG 分区以 nvidia.com/mig-2g.45gb 等资源类型暴露给调度器。
resources:
  limits:
    nvidia.com/mig-2g.45gb: "2"  # 2 个 MIG 实例
关键约束: MIG 资源必须在同一节点上满足,不能跨节点。如果没有单个节点有两个空闲 2g.45gb 实例,Pod 将永远 Pending——即使集群中其他节点合计有足够的 MIG 容量。因此需提前规划 MIG 分区大小以匹配典型工作负载需求。
建议在使用 MIG 时启用 persistence mode,确保 MIG 配置在无 Job 运行时也保持激活状态。NVIDIA GPU Operator 的 MIG Manager 可自动配置和持久化节点上的 MIG 分区。可通过 Kubernetes 标签(如 mig-enabled: "true")区分 MIG 节点和非 MIG 节点。
配套代码 ch03/kubernetes_mig_pod.yaml 示范了请求两个 2g.45gb MIG 实例的 Pod 配置,包含 nodeSelector: mig-enabled: "true" 节点选择器。

5.6 调度器:Kubernetes 与 SLURM

调度器常用场景GPU 拓扑支持
SLURM训练集群generic resources,可关联 NUMA/NVLink
Kubernetes推理集群device plugin + Topology Manager
Slinky混合集群SLURM + K8s 集成

6. Grace Blackwell 超级芯片架构

书中多次提到 NVIDIA CPU-GPU 超级芯片(Grace Blackwell、Vera Rubin)通过 NVLink-C2C(~900 GB/s)将 CPU 和 GPU 紧密耦合,从根本上缓解 CPU-GPU 数据传输瓶颈。 但即使在这种架构下:
  • Linux 仍将 CPU DRAM 和 GPU HBM 视为独立内存池。
  • NUMA 绑定和数据局部性优化依然有意义。
  • 仍需配置 hugepages、prefetching、memory pinning。
配套代码 ch03/grace_blackwell_topology.py 提供了 Grace Blackwell 系统的 NIC/GPU/NUMA 拓扑发现工具,可生成 IRQ/RPS/XPS 亲和性配置脚本。

核心要点

核心思想:让 GPU 永远不等待

本章所有优化围绕同一目标——消除导致 GPU 空闲的一切因素:
  1. CPU 不要拖后腿 — NUMA 绑定、CPU pinning、关闭 swap、performance 频率模式。
  2. 数据传输不要成为瓶颈 — pinned memory、hugepages、GPUDirect RDMA/GDS、双缓冲预取。
  3. 驱动不要引入额外延迟 — persistence mode、MPS/MIG 合理使用。
  4. 容器不要增加开销 — bind mount、host networking、正确 CUDA/driver 版本匹配。
  5. 调度不要分配错资源 — Topology Manager、NUMA/NVLink 对齐。

FAQ

因为 GPU 最常见的空闲原因不是 GPU 自身的问题,而是 CPU 没能及时准备好数据。数据加载、tokenize、H2D 传输、内核分发都发生在 CPU 侧,任何一步慢了 GPU 就只能等待。
--cpunodebind 控制 CPU 线程在哪些核心上跑,--membind 控制内存从哪个节点分配。只用 --cpunodebind 但不限制内存分配,OS 可能把内存分配到远端节点,每次访问仍需跨节点。两者需要配合使用。
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 实现拓扑感知调度。
vm.swappiness=0 告诉内核尽量避免 swap,但在极端内存压力下仍可能 swap。swapoff -a 完全禁用所有 swap 设备,任何情况都不会 swap——但如果内存不够,OOM killer 会直接终止进程。训练场景通常用 swapoff -a 配合足够的物理内存。