> ## Documentation Index
> Fetch the complete documentation index at: https://se7en.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 第三章：GPU 环境下的 OS、Docker 与 Kubernetes 调优

## 录屏回看

<iframe src="https://player.bilibili.com/player.html?bvid=BV1ToNC6QE2k&autoplay=0&high_quality=1" width="100%" height="480" scrolling="no" frameBorder="0" allowFullScreen />

## 本章概要

即使 GPU kernel 和库已经调到极致，系统层的瓶颈仍会拖累大规模训练与推理。真正决定 GPU 利用率的，往往是操作系统、驱动运行时、容器和集群调度的配置。本章逐层来看这些配置：

* **CPU / 操作系统**：保证 CPU 持续为 GPU 供给数据和任务——NUMA 绑定、pinned memory、透明大页、调度与中断亲和性、虚拟内存与 swap、文件系统缓存、CPU 频率与内存分配器；
* **GPU 驱动与运行时**：提升单卡的性能与利用率——持久化模式、MPS / MIG 共享与切分、GPU 时钟与 ECC；
* **容器运行时**：让容器以接近裸机的性能访问 GPU——NVIDIA Container Toolkit、overlay FS 与网络开销；
* **Kubernetes**：在集群层面分配 GPU——GPU Operator、Device Plugin、DRA、资源隔离与 QoS，以及从节点内 NUMA 到跨节点网络的拓扑感知调度。

***

## 1. CPU 与操作系统调优

**GPU 利用率不足的一个常见根源在 CPU 端：CPU 没能及时、持续地为 GPU 供给数据和任务，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 等待时间。

### 1.1 NUMA 感知与 CPU 绑定

#### **1.1.1 什么是 NUMA**

数据中心服务器通常是多路的 NUMA（Non-Uniform Memory Access，非统一内存访问）系统：每颗物理 CPU 拥有自己的内存控制器，直接连接一部分本地内存。每个 CPU 可以通过自己的内存控制器快速访问本地内存，也可以通过节点间链路访问另一个 CPU 的远程内存，但**本地和远程内存的访问延迟和带宽不同**，这就是"非统一"的含义。

<img src="https://mintcdn.com/se7en/hcKRUHOdFZvUgFQo/books/ai-systems-performance-engineering/images/numa.png?fit=max&auto=format&n=hcKRUHOdFZvUgFQo&q=85&s=19493d87d3540f888d424a341789655b" alt="双 socket NUMA 架构：每个 CPU Package 拥有独立的内存（DIMM）和核心，构成一个 NUMA 节点" width="2064" height="452" data-path="books/ai-systems-performance-engineering/images/numa.png" />

**NUMA 节点**是 CPU、GPU、NIC 和内存的**逻辑分组**，这些组件在物理上彼此靠近。访问同一 NUMA 节点内的资源比跨节点访问要快得多。例如，一个运行在 NUMA 节点 0 的 CPU 上的进程如果需要访问 NUMA 节点 1 的 GPU，数据就必须通过节点间链路传输，会产生更高的延迟。实际上，**跨 NUMA 节点的内存访问延迟几乎可以翻倍**。

#### **1.1.2 查看系统 NUMA 拓扑**

你可以用以下两个命令结合查看系统的完整 NUMA 拓扑：

```bash theme={null}
# 查看 CPU/内存的 NUMA 分布
numactl -H

# 查看 GPU 与 NUMA 节点的对应关系
nvidia-smi topo -m
```

`numactl -H` 的输出示例（4 个 NUMA 节点的服务器）：

```bash theme={null}
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` 绑定时需要的值**

#### **1.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 节点内完成。

```bash theme={null}
# 语法：numactl --cpunodebind=<node> --membind=<node> <command>
# 假设 GPU 4 连接在 NUMA 节点 0 上，将 CPU 和内存也绑定到节点 0
numactl --cpunodebind=0 --membind=0 python train.py --gpu 4
```

#### **1.1.4 验证 NUMA 绑定的性能影响**

可以使用下面的脚本对比 NUMA 绑定对 CPU→GPU 数据拷贝带宽的影响，保存为 `numa_bench.sh` 后运行：

```bash theme={null}
# 参数：<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
```

<Accordion title="numa_bench.sh：NUMA 绑定性能对比脚本">
  ```bash theme={null}
  #!/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+），差异会更显著。
</Accordion>

#### **1.1.5 在 PyTorch DataLoader 中绑定 NUMA**

书中给出了一个完整的 Python 示例（约 100 行），配套代码 [`ch03/bind_numa_affinity.py`](https://github.com/cfregly/ai-performance-engineering/blob/main/code/ch03/bind_numa_affinity.py) 是其完整实现。整个流程分三步：

**第一步：查询 GPU 所在的 NUMA 节点**

启动时，进程需要知道当前 GPU 连接在哪个 NUMA 节点上。`get_gpu_numa_node()` 按优先级依次尝试四种方式：

1. **NVML 直接查询** — 调用 [`nvmlDeviceGetNumaNodeId()`](https://docs.nvidia.com/deploy/nvml-api/group__nvmlAffinity.html#group__nvmlAffinity_1g292897eebc9e9151387399fcd7dbd423)，直接获取 GPU 的 NUMA 节点编号。仅适用于 GPU 本身是 NUMA 节点的平台（如 Grace Hopper/Grace Blackwell 超级芯片）

<Accordion title="验证：用 pynvml 调用 nvmlDeviceGetNumaNodeId">
  ```bash theme={null}
  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
  ...
  ```

  参考资料：

  * [NVIDIA Grace Hopper Superchip Architecture In-Depth](https://developer.nvidia.com/blog/nvidia-grace-hopper-superchip-architecture-in-depth/)
  * [Unification of Memory on the Grace Hopper Nodes](https://blog.hpc.qmul.ac.uk/grace-hopper-unified-memory)
  * [Data Movement in the NVIDIA GH200 Grace Hopper Superchip](https://datacrunch.io/blog/data-movement-in-the-nvidia-gh200-grace-hopper-superchip)
</Accordion>

2. **NVML CPU 亲和性推断** — 如果上一步不可用，调用 `nvmlDeviceGetCpuAffinity()` 获取该 GPU 关联的 CPU 掩码（bitmask），然后与 sysfs 中的 CPU→NUMA 映射表对照，取出现次数最多的 NUMA 节点

<Accordion title="验证：用 nvidia-smi topo -m 查看 CPU Affinity 列">
  ```bash theme={null}
  # 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 之间的互连类型
  ```
</Accordion>

3. **sysfs 内核接口** — 如果 NVML 完全不可用，直接读 `/sys/bus/pci/devices/<PCI_ID>/numa_node`，这是 Linux 内核为每个 PCI 设备维护的 NUMA 节点信息

<Accordion title="验证：从 sysfs 读取每个 GPU 的 NUMA 节点">
  ```bash theme={null}
  # 注意两个格式差异：
  #   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 列一致。
</Accordion>

4. **当前进程策略兜底** — 如果以上都失败，运行 `numactl --show` 读取当前进程的 preferred node。例如如果外部已经用 `numactl` 命令启动了脚本（如 `numactl --cpunodebind=0`），进程会继承这些策略，`preferred node` 就是正确的值

<Accordion title="验证：对比有无 numactl 指定 NUMA 策略时的 preferred node">
  ```bash theme={null}
  # 不包裹：preferred node 是 current（OS 自己决定）
  numactl --show | grep "preferred node"

  # 用 numactl 显式指定 NUMA 策略后：preferred node 变成指定的节点
  numactl --cpunodebind=0 --membind=0 numactl --show | grep "preferred node"
  ```
</Accordion>

```python theme={null}
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` 只能在启动进程时从外部指定策略，无法在已运行的子进程内部重新绑定：

```python theme={null}
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 启动时显式重新绑定：

```python theme={null}
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}")
```

把三步串起来：

```python theme={null}
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 全程本地访问。

```bash theme={null}
# 单 GPU 模式：自动检测 GPU 0 的 NUMA 节点并绑定
cd ai-performance-engineering/code
python -m ch03.bind_numa_affinity
```

<Accordion title="单 GPU 模式输出示例">
  ```
  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)
  ```
</Accordion>

```bash theme={null}
# 多 GPU DDP 模式：--nproc_per_node 指定在本机启动几个进程（通常等于 GPU 数量），
# 每个进程分配一张 GPU 并自动绑定到该 GPU 的 NUMA 节点
torchrun --nproc_per_node=4 -m ch03.bind_numa_affinity
```

<Accordion title="多 GPU DDP 模式输出示例">
  在一台双 socket（2 个 NUMA 节点、每节点挂 2 张 GPU）的机器上，每个 rank 会绑到自己 GPU 所在的节点，输出类似：

  ```
  # 4 个 rank 的主进程（一卡一个）
  PID 4021 bound to NUMA node 0 (CPUs=[0-31])    # rank 0 → GPU 0
  PID 4022 bound to NUMA node 0 (CPUs=[0-31])    # rank 1 → GPU 1
  PID 4023 bound to NUMA node 1 (CPUs=[32-63])   # rank 2 → GPU 2
  PID 4024 bound to NUMA node 1 (CPUs=[32-63])   # rank 3 → GPU 3
  # 每个 rank 的 DataLoader worker 跟随该 rank 落到同一节点
  Worker 0 (PID=4110) bound to NUMA node 0       # rank 0 的 worker → node 0
  Worker 0 (PID=4133) bound to NUMA node 1       # rank 2 的 worker → node 1
  ...
  step=0 loss=2.3125
  ```

  前四行是 4 个 DDP rank 的主进程，各自绑到自己 GPU 所在的节点（rank 0、1 → node 0，rank 2、3 → node 1）；后面的 `Worker` 是每个 rank 的 DataLoader worker 进程，由所属 rank 绑定，因此跟随该 rank 落到同一个节点。这样每个 rank 连同它的 worker 都和自己的 GPU 就近对齐。
</Accordion>

### 1.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。

下图展示了这条路径里的三类内存区域：

<div style={{ maxWidth: "520px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-pinned-memory-page-locked-flow.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=de27335588aa80848d0c5262d027165b" alt="普通 pageable tensor 先被 pin 到 page-locked CPU memory，再从 pinned memory 传输到 GPU memory" width="1562" height="1648" data-path="books/ai-systems-performance-engineering/images/ch03-pinned-memory-page-locked-flow.png" />
  </Frame>

  <p style={{ fontSize: "0.9rem", lineHeight: "1.6", marginTop: "0.5rem" }}>
    Pinned memory（也称 page-locked 或 nonpageable memory）是一类不能被换出到磁盘的内存。
  </p>
</div>

<Accordion title="内存区域、Tensor 位置与数据流">
  * **浅黄色区域：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。
</Accordion>

在 PyTorch 的数据加载中，给 `DataLoader` 传入 `pin_memory=True` 会自动把取回的 tensor 放到 pinned memory 中，从而让它们更快地传输到支持 CUDA 的 GPU。

此外，一旦 tensor 位于 pinned memory 中，就可以使用异步 GPU copy。只需要在 `.to()` 或 `.cuda()` 调用中额外传入 `non_blocking=True`。这可以用来让数据传输与计算重叠。

```python theme={null}
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)
```

参考资料：

* [PyTorch tutorial: A guide on good usage of `non_blocking` and `pin_memory()`](https://docs.pytorch.org/tutorials/intermediate/pinmem_nonblock.html)
* [PyTorch DataLoader: Memory Pinning](https://docs.pytorch.org/docs/2.11/data.html#memory-pinning)
* [PyTorch CUDA semantics: Use pinned memory buffers](https://docs.pytorch.org/docs/2.11/notes/cuda.html#cuda-memory-pinning)

### 1.3 透明大页（Transparent Hugepages）

Linux 内存管理通常使用 4 KB 的页；但当进程使用数十或数百 GB 内存时，例如深度学习数据集、预取批次、模型参数等，管理数以百万计的小页会非常低效。

透明大页（Transparent Hugepages，THP）是一种自动使用大页的机制。大页——2 MB 甚至 1 GB 的页——可以通过增大内存块来减少虚拟内存管理的开销。主要好处是减少缺页中断，并减轻 TLB（translation lookaside buffer）的压力。

#### 1.3.1 TLB 地址转换

TLB 是 CPU 用来把虚拟地址映射到物理地址的缓存。页更少、更大时，同样数量的 TLB 条目可以覆盖更多内存，从而减少 TLB miss。

<Accordion title="TLB 的地址转换过程">
  **TLB hit**

  一个虚拟内存地址（virtual memory address）进入后，需要被翻译成物理地址（physical address）。第一步总是把虚拟地址拆成两部分：虚拟页号（virtual page number）和页偏移（page offset）。页偏移由虚拟地址的最后几位组成。偏移位不会被翻译，而是直接传递到物理内存地址中；因为页表只负责把虚拟页号映射到物理页号，页内的相对位置保持不变。

  因此，偏移量会直接映射到物理内存层，而虚拟页号则与 TLB 中已有的标签相对应。这样一来，MMU 无需查询全局内存，就能立即知道该访问哪个物理内存页。

  <Frame>
    <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-tlb-hit.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=6787525ddb0e3a4019c9f641317c5926" alt="TLB hit：虚拟页号在 TLB 中找到匹配条目后直接得到物理页号" width="1069" height="591" data-path="books/ai-systems-performance-engineering/images/ch03-tlb-hit.png" />
  </Frame>

  在上图的例子中，虚拟页号在 TLB 中被找到，并立即转换成物理页号。

  **TLB miss**

  当虚拟页码在 TLB 中找不到时，就会发生所谓的“TLB 未命中”情况。此时，TLB 必须查询系统的物理内存，以确定对应的物理页码是多少。与 TLB 命中相比，这种查询方式会导致更高的延迟。如果 TLB 已满且发生 TLB 未命中，那么 TLB 中最近最少使用的数据条目会被清除，新的数据条目则会取而代之。在下面的例子中，虚拟页码在 TLB 中不存在，因此 TLB 必须查询物理内存来获取该页码。

  <Frame>
    <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-tlb-miss.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=5fb26526a8624b41ed7f493cebd2c61d" alt="TLB miss：虚拟页号未命中 TLB 后需要访问页表并把映射写回 TLB" width="1069" height="591" data-path="books/ai-systems-performance-engineering/images/ch03-tlb-miss.png" />
  </Frame>

  参考资料：[How is Virtual Memory Translated to Physical Memory?](https://blogs.vmware.com/cloud-foundation/2020/03/03/how-is-virtual-memory-translated-to-physical-memory/)
</Accordion>

#### 1.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 通常完全禁用。

```bash theme={null}
# 查看 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 明确使用。

```bash theme={null}
# 查看默认 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
```

### 1.4 调度器与中断亲和性

CPU 侧抖动通常来自三个方面：

* **线程调度**：关键数据管道线程或通信辅助线程如果长时间排队、频繁被抢占，就会形成 CPU 侧调度抖动，进而让 GPU 因等待输入或通信而空闲。
* **CPU 隔离**：这些线程如果和其他 workload 混跑在同一组 CPU 上，容易争抢 CPU core、cache 和内存带宽，响应时间会变得不稳定。
* **中断亲和性**：GPU/NIC 中断请求（Interrupt Request，IRQ）如果由远离设备的 CPU 处理，或者频繁打断关键线程，会增加额外延迟。

#### 1.4.1 线程调度

在繁忙的系统上，需要确保数据管道线程等重要线程不会被频繁中断。Linux 默认使用**完全公平调度器（Completely Fair Scheduler，CFS）**，对大多数情况都有效。

但如果有一个对延迟非常敏感、例如需要为 GPU 提供数据的线程，可以考虑对该线程使用实时的**先进先出（FIFO）**或**轮转（RR）** 优先级调度。这样可以确保高优先级线程在不被普通优先级线程抢占的情况下运行。

<div style={{ maxWidth: "980px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-fifo-rr-cfs.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=a4776ce5e452c82aded20e5994aeccc4" alt="CFS、FIFO、RR 三种调度策略的对比示意图" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-fifo-rr-cfs.png" />
  </Frame>
</div>

<Accordion title="CFS 中 vruntime 的计算方式">
  **完全公平调度器（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 |
  | ---- | ------ |
  | `-5` | `3121` |
  | `-4` | `2501` |
  | `-3` | `1991` |
  | `-2` | `1586` |
  | `-1` | `1277` |
  | `0`  | `1024` |
  | `1`  | `820`  |
  | `2`  | `655`  |
  | `3`  | `526`  |
  | `4`  | `423`  |
  | `5`  | `335`  |

  关键公式可以近似写成：

  ```text theme={null}
  vruntime 增量 = 实际运行时间 × (nice=0 的权重 / 当前线程权重)
  ```

  例如两个线程都实际运行 `10 ms`：

  ```text theme={null}
  nice=0： vruntime 增量 = 10 ms × (1024 / 1024) = 10 ms
  nice=-1：vruntime 增量 = 10 ms × (1024 / 1277) ≈ 8.0 ms
  ```

  低 nice 值线程的权重更大，同样运行 `10 ms`，`vruntime` 增加更少，因此更容易保持在红黑树左侧，也更早再次被 CFS 选中运行。
</Accordion>

<Accordion title="设置调度策略">
  先查看线程当前运行在哪些 CPU 上，以及调度策略、实时优先级和 nice 值：

  ```bash theme={null}
  ps -L -p <pid> -o pid,tid,psr,policy,rtprio,ni,comm
  ```

  示例输出：

  ```text theme={null}
      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 调整权重：

  ```bash theme={null}
  # 调高普通线程优先级，负 nice 值通常需要 sudo
  sudo renice -n -5 -p <pid>
  ```

  对极少数延迟敏感的线程，可以用 `chrt` 设置实时 FIFO 或 RR 策略：

  ```bash theme={null}
  # 设置为 SCHED_FIFO，实时优先级为 10
  sudo chrt -f -p 10 <tid>

  # 设置为 SCHED_RR，实时优先级为 10
  sudo chrt -r -p 10 <tid>

  # 查看某个线程当前调度策略
  chrt -p <tid>
  ```

  `chrt -p <tid>` 的输出示例：

  ```text theme={null}
  pid 18503's current scheduling policy: SCHED_FIFO
  pid 18503's current scheduling priority: 10
  ```

  不过要谨慎使用，因为实时线程如果管理不当，可能会使其他进程饥饿。实际中，如果已经将关键线程绑定到专用 CPU core，通常不需要再调整实时线程优先级。
</Accordion>

#### 1.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 灵活，适合非常明确的低延迟节点。

<Accordion title="CPU 隔离配置示例">
  ```bash theme={null}
  # 临时绑定：把已有进程绑定到 CPU 8-15
  sudo taskset -cp 8-15 <pid>

  # 生产隔离：创建 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 <pid> | 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 <image>
  ```

  如果需要比 cgroup 更强的启动期隔离，可以通过内核启动参数配置 `isolcpus`、`nohz_full`、`rcu_nocbs` 和 `irqaffinity`。这些参数需要写入 `/etc/default/grub` 中的 `GRUB_CMDLINE_LINUX`，更新 GRUB 并重启后生效。

  ```bash theme={null}
  # 备份 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`。
</Accordion>

#### 1.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 处理时，会引入跨节点通信和缓存一致性流量。

<Accordion title="中断亲和性配置示例">
  ```bash theme={null}
  # 查看 GPU / NIC IRQ 分布，先打印表头，再过滤设备行
  head -n 1 /proc/interrupts
  grep -Ei "nvidia|mlx|eth|ib" /proc/interrupts

  # 示例输出中，行首的 575 是 IRQ 编号，后面的计数表示各 CPU 处理该 IRQ 的次数
  # 575: ... 16 ... IR-PCI-MSI-0000:c1:00.0 0-edge nvidia
  # IR-PCI-MSI：PCIe 设备使用的 MSI 中断，IR 通常表示经过 interrupt remapping
  # 0000:c1:00.0：PCIe 设备地址，也叫 BDF（Bus:Device.Function），来自 IR-PCI-MSI-0000:c1:00.0
  # 0-edge：边沿触发的中断
  # nvidia：处理这个 IRQ 的驱动或设备名

  # CPU 很多时，可以只打印非零计数
  awk '
  NR==1 { for (i=1; i<=NF; i++) cpu[i]=$i; n=NF; next }
  /nvidia|mlx|eth|ib/ {
    irq=$1; sub(":", "", irq)
    printf "IRQ %s:", irq
    for (i=2; i<=n+1; i++) {
      if ($i != 0) printf " %s=%s", cpu[i-1], $i
    }
    print ""
  }
  ' /proc/interrupts

  # 示例输出：IRQ 575 主要由 CPU64 处理过 16 次
  # IRQ 575: CPU64=16

  # 查看和设置某个 IRQ 允许运行的 CPU 列表，<irq> 用行首的 IRQ 编号替换
  # 上面的示例中，<irq> 就是 575
  cat /proc/irq/<irq>/smp_affinity_list

  # 先确认设备所在 NUMA node，以及该 NUMA node 对应哪些 CPU
  # 1. 查询 GPU 的 BDF。也可以直接使用 /proc/interrupts 中的 0000:c1:00.0
  nvidia-smi --query-gpu=index,pci.bus_id --format=csv

  # 2. 用 BDF 确认具体设备和它所在的 NUMA node
  lspci -s 0000:c1:00.0
  cat /sys/bus/pci/devices/0000:c1:00.0/numa_node

  # 3. 查询 CPU 与 NUMA node 的对应关系
  lscpu -e=CPU,NODE,SOCKET,CORE

  # 如果设备在 NUMA node 0，可以查看 node0 对应的 CPU 列表
  cat /sys/devices/system/node/node0/cpulist

  # 确认 CPU 8-15 与该设备位于同一 NUMA node 后，把这个 IRQ 的处理 CPU 限制为 8-15
  echo 8-15 | sudo tee /proc/irq/<irq>/smp_affinity_list
  ```
</Accordion>

### 1.5 虚拟内存与 Swap

不言而喻，应尽量避免发生内存交换。一旦进程的部分内存被换出到磁盘，性能往往会出现灾难性的、跨数量级的下降。GPU 程序通常会分配大量主机内存用于数据缓存；如果操作系统将其中一部分换出到磁盘，那么当 GPU 相关流程再次需要访问这些数据时，就会遭遇巨大的延迟。

<Accordion title="swappiness 的含义">
  `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`。
</Accordion>

<Accordion title="Swap 配置示例">
  ```bash theme={null}
  # 查看当前 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/<pid>/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/<job>/memory.current
  cat /sys/fs/cgroup/<job>/memory.swap.current

  # 示例输出：memory.current 是当前内存用量；memory.swap.current 为 0 表示该 cgroup 没有使用 swap
  # 85899345920
  # 0

  # 对某个 cgroup 禁止 swap
  echo 0 | sudo tee /sys/fs/cgroup/<job>/memory.swap.max

  # 确认 swap 上限已经设置为 0
  cat /sys/fs/cgroup/<job>/memory.swap.max

  # Docker 示例：固定 CPU / NUMA node，并让容器没有额外 swap 空间
  docker run --cpuset-cpus=8-15 --cpuset-mems=0 --memory=128g --memory-swap=128g <image>
  ```
</Accordion>

### 1.6 文件系统缓存

对于大型训练任务，最佳实践是频繁将 checkpoint 写入磁盘，以便在需要时能从已知的良好 checkpoint 重新启动失败任务。然而，在进行 checkpoint 写入时，大量数据的突发写入可能会填满操作系统 page cache 并导致停顿。

常见解决方案包括：

**继续使用 page cache，但降低冲击**：

* **调整写回阈值**：调整 dirty page 阈值，控制内核什么时候开始后台写回，以及什么时候阻塞写入进程。
* **异步写 checkpoint**：使用 [PyTorch Distributed Checkpoint（DCP）async\_save](https://docs.pytorch.org/tutorials/recipes/distributed_async_checkpoint_recipe.html)，把 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。

```bash theme={null}
# 使用比例配置，适合内存规模差异不大的节点
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))
```

<Accordion title="PyTorch DCP 异步 checkpoint 示例">
  下面示例只保留训练 loop 中的核心结构。`AppState` 是对 model 和 optimizer 的 `Stateful` 包装，用来让 DCP 保存和恢复分布式 state dict。

  ```python theme={null}
  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 也放到后台线程中：

  ```python theme={null}
  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 本身也可能影响输入管道或通信辅助线程。
</Accordion>

<Accordion title="后台写回的含义">
  在 Linux page cache 语境里，writeback 可以翻译为**后台写回**。它指内核把 page cache 里的 dirty page 异步写回到磁盘或后端存储。

  应用写文件时，数据通常会先进入 page cache，写调用可以较快返回；随后内核在后台把 dirty page 写回存储。当 dirty page 太多，或者后端存储写入跟不上时，内核可能开始限制新的写入，训练进程就会看到 checkpoint 写入停顿。
</Accordion>

**完全绕过 page cache**：

* **Direct I/O**：延迟敏感的训练 workflow 可以评估 `O_DIRECT`、`io_uring` 和 [GPUDirect Storage](https://docs.nvidia.com/gpudirect-storage/)。
* **适用条件**：这类方式需要文件系统、存储设备和 I/O 栈支持 direct I/O，适合已经验证过 I/O 路径的场景。

### 1.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 可以减少额外的延迟尖峰。**

<Accordion title="CPU 频率的查看与设置">
  ```bash theme={null}
  # 查看 CPU governor、频率范围和当前频率
  cpupower frequency-info
  ```

  示例输出：

  ```text theme={null}
  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 会根据自己的策略选择使用哪个频率档位。

  ```bash theme={null}
  # 临时设置为 performance governor
  sudo cpupower frequency-set -g performance
  ```

  有些系统安装 `cpupower` 后支持配置文件，可以把 governor 持久化到服务配置中：

  ```bash theme={null}
  sudo vim /etc/default/cpupower
  ```

  设置：

  ```bash theme={null}
  governor='performance'
  ```

  然后启用服务：

  ```bash theme={null}
  sudo systemctl enable --now cpupower
  ```
</Accordion>

<Accordion title="C-states 的查看与设置">
  ```bash theme={null}
  # 查看 CPU 空闲状态；输出只包含 idle states，不包含 active 状态 C0
  cpupower idle-info
  ```

  示例输出：

  ```text theme={null}
  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 进入深层睡眠状态。

  ```bash theme={null}
  # 编辑 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
  ```
</Accordion>

### 1.8 Host 内存分配器调优

在 GPU 计算中，CPU 需要持续准备 batch 并及时送给 GPU。优化 memory allocator 可以减少内存分配带来的卡顿和抖动，避免 GPU 等待数据，从而维持高 GPU 利用率和整体吞吐。

<Accordion title="memory allocator 的作用">
  如果程序每次申请内存都直接向操作系统请求，会产生很大的系统调用开销，而且频繁分配和释放还会导致内存碎片、多线程锁竞争、cache 利用率低等问题。memory allocator 的作用就是在程序和操作系统之间做一层高效的内存管理：**它先一次性向系统申请大块内存，再按需切分、复用和回收**，从而减少系统调用、降低碎片、提升性能和并发效率。

  <div style={{ maxWidth: "900px", margin: "0 auto" }}>
    <Frame>
      <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-memory-allocator.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=e057abdcbc179ddbe5804020be095570" alt="Memory allocator 位于应用和操作系统之间，缓存并复用释放后的内存，减少频繁向 OS 申请和归还内存的开销" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-memory-allocator.png" />
    </Frame>
  </div>
</Accordion>

<Accordion title="ptmalloc、jemalloc 与 tcmalloc 对比">
  Linux 上常见的默认 allocator 是 glibc `malloc`，其底层实现通常称为 `ptmalloc`。它兼容性好，但在高并发数据管道中可能出现 arena 膨胀、碎片和 RSS（Resident Set Size，常驻集大小）不回收。

  `jemalloc` 和 `tcmalloc` 是常见替代 allocator，优势主要体现在降低多线程锁竞争、改善碎片控制，以及更灵活地管理释放后的内存。

  <div style={{ maxWidth: "900px", margin: "0 auto" }}>
    <Frame>
      <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-ptmalloc-tcmalloc-jemalloc.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=3b8d86afc0b8bb414937c200c782d643" alt="ptmalloc、jemalloc 与 tcmalloc 在 arena、thread cache、central cache 和后台回收策略上的对比" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-ptmalloc-tcmalloc-jemalloc.png" />
    </Frame>
  </div>

  **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 级别的大锁。**
</Accordion>

<Accordion title="jemalloc 的 arena、bin、slab、run 与 extent">
  jemalloc 的小对象分配通常按 size class 进入对应 `bin`，再从这个 bin 管理的 `slab` / `run` 中取一个空闲 `object`。large object 不走 slab，通常由 `extent` 管理。

  ```text theme={null}
  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 管理。
</Accordion>

<Accordion title="jemalloc 和 tcmalloc 配置">
  使用 `jemalloc` 或 `tcmalloc` 前，系统中需要有对应的动态库；随后可以通过 `LD_PRELOAD` 让目标进程在启动时优先加载对应 allocator。

  ```bash theme={null}
  # Ubuntu / Debian 示例
  sudo apt update
  sudo apt install -y libjemalloc2 google-perftools
  ```

  安装后可以通过动态库缓存确认系统是否能找到它们：

  ```bash theme={null}
  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，减少多线程分配时的锁竞争；也可以启用后台线程，在业务线程之外做内存清理。下面是一组常见配置：

  ```bash theme={null}
  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"
  ```

  <div style={{ maxWidth: "900px", margin: "0 auto" }}>
    <Frame>
      <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-active-dirty-myzzy-page.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=5cd85ffab2ccd84ba9b8cca7311ba475" alt="jemalloc 中 active、dirty、muzzy page 的状态流转：应用释放内存后，allocator 可先保留页面等待复用，再按 decay 策略逐步清理" width="1651" height="953" data-path="books/ai-systems-performance-engineering/images/ch03-active-dirty-myzzy-page.png" />
    </Frame>
  </div>

  * `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_*` 环境变量。

  ```bash theme={null}
  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 申请/归还内存的开销。

  ```bash theme={null}
  # 确认目标进程是否已经加载 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`。
</Accordion>

## 2. GPU 驱动与运行时设置

GPU 驱动与运行时里有一些会影响性能的设置，尤其在多 GPU、多用户场景下。这些设置配置得当，能降低额外开销，也能让多个负载更好地共享同一张 GPU。

### 2.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 空闲时功耗会略高**。

```bash theme={null}
# 以 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_gpu> -pm ENABLED

# 使用 nvidia-smi -q 查看指定 GPU 的详细状态，其中 Persistence Mode 应显示为 Enabled
nvidia-smi -i <target_gpu> -q

# systemd 环境中启动 nvidia-persistenced，并设置为开机自启
sudo systemctl enable --now nvidia-persistenced
```

<Accordion title="Kubernetes 中通过 GPU Operator 启用持久化模式">
  在 Kubernetes 中，安装 GPU Operator 时启用 `driver.enabled=true`，GPU Operator 会部署 `nvidia-driver-daemonset` 来管理节点侧 NVIDIA driver，并启动 `nvidia-persistenced`，从而启用持久化模式。

  ```bash theme={null}
  # 添加 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-pod> \
    -c nvidia-device-plugin -- \
    nvidia-smi --query-gpu=uuid,persistence_mode --format=csv
  ```
</Accordion>

参考资料：[NVIDIA Driver Persistence](https://docs.nvidia.com/deploy/driver-persistence/persistence-daemon.html)

### 2.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 本质上是把多个进程的上下文合并到一个调度上下文中。这样一来，就不必为独立进程之间的切换和空闲等待付出完整代价。**

<Frame>
  <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-mps.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=c15d337a013f9e95233700dccbb55662" alt="MPS 让多个 CUDA 进程通过共享调度上下文在同一张 GPU 上并发执行" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-mps.png" />
</Frame>

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 上运行的所有其他进程。

<Accordion title="MPS 配置示例">
  常用的 MPS 配置和查看命令如下：

  ```bash theme={null}
  # 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](https://github.com/ai-infra-learning/docs/blob/main/books/ai-systems-performance-engineering/chapters/ch03/mps/mps_demo.py) 会启动多个 Python 子进程，让它们在同一张 GPU 上反复执行矩阵乘法，模拟多个 CUDA 进程共享 GPU 的场景。[run\_basic\_comparison.sh](https://github.com/ai-infra-learning/docs/blob/main/books/ai-systems-performance-engineering/chapters/ch03/mps/run_basic_comparison.sh) 会跑两轮：第一轮不开 MPS，第二轮开启 MPS 后运行同一个 demo。两轮使用相同的 worker 数、矩阵大小和运行时长，最后比较所有 worker 完成的总矩阵乘法次数。

  ```bash theme={null}
  # 进入 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
  ```

  实际输出示例如下：

  ```text theme={null}
  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 分区常用的配置和查看命令如下：

  ```bash theme={null}
  # -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
  ```
</Accordion>

<Accordion title="GPU Operator 中使用 MPS">
  GPU Operator 通过 NVIDIA device plugin 暴露 MPS 共享 GPU 资源。GPU Operator 的 [GPU sharing 文档](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html#applying-one-cluster-wide-configuration)使用 `devicePlugin.config` 指定 device plugin 的 ConfigMap；MPS 使用同一个配置入口，但 ConfigMap 里的共享字段是 `sharing.mps`。

  [NVIDIA device plugin 文档](https://github.com/NVIDIA/k8s-device-plugin#with-cuda-mps)说明，MPS 支持仍是 experimental，并且 MPS 共享当前不支持启用 MIG 的 device。

  下面示例把每张完整 GPU 切成 `4` 个 MPS 共享访问入口，并用 `renameByDefault: true` 暴露为 `nvidia.com/gpu.shared`：

  ```yaml theme={null}
  # 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`。

  ```bash theme={null}
  # 创建 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-name> | grep -A8 -E "Capacity|Allocatable"
  ```

  工作负载申请 `nvidia.com/gpu.shared` 即可使用启用 MPS 共享的 GPU：

  ```yaml theme={null}
  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 让新配置生效：

  ```bash theme={null}
  kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
  ```
</Accordion>

参考资料：

* [NVIDIA MPS Introduction](https://docs.nvidia.com/deploy/mps/introduction.html)
* [NVIDIA MPS Common Tasks](https://docs.nvidia.com/deploy/mps/appendix-common-tasks.html)
* [NVIDIA MPS Tools and Interface Reference](https://docs.nvidia.com/deploy/mps/appendix-tools-and-interface-reference.html)
* [NVIDIA Kubernetes Device Plugin: With CUDA MPS](https://github.com/NVIDIA/k8s-device-plugin#with-cuda-mps)

### 2.3 Multi-Instance GPU (MIG)

[MIG](https://docs.nvidia.com/datacenter/tesla/mig-user-guide/) 会把支持的 GPU 划分为最多 7 个隔离实例，每个实例拥有专用计算和 GPU 内存资源。MIG 从 NVIDIA Ampere 架构开始支持，也就是 compute capability `>= 8.0` 的部分数据中心 GPU；具体型号以 NVIDIA 的 [Supported GPUs](https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-gpus.html) 列表为准。

#### **2.3.1 MIG 的资源隔离**

**MIG 适合多租户场景**，不同用户、容器、虚拟机或进程可以运行在不同 GPU 实例上，一个进程不会直接影响另一个进程的调度和资源边界。对云服务和共享集群来说，这种隔离能把一张大 GPU 切成多个可独立分配的小 GPU。

每个 MIG 实例在 GPU 内部访问内存时都有独立路径，包括内部互连、L2 缓存分区、内存控制器和 DRAM 地址总线。这样即使其他实例的任务占满自己的缓存或 DRAM 接口，当前实例仍能获得更稳定的吞吐和延迟。MIG 也可以切分 SM、数据拷贝引擎、解码器等 GPU 引擎，为不同进程提供确定的服务质量（QoS）和故障隔离。

<Frame>
  <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-mig.jpg?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=06005f458c93a0abc52641ee3459bcc5" alt="MIG 将一张 GPU 划分为多个彼此隔离的 GPU Instance" width="1920" height="1440" data-path="books/ai-systems-performance-engineering/images/ch03-mig.jpg" />
</Frame>

#### **2.3.2 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。

#### **2.3.3 MIG 设备命名规则**

[MIG 设备名称](https://docs.nvidia.com/datacenter/tesla/mig-user-guide/mig-device-names.html) 描述了实例的资源形态。`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。

<Frame>
  <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-mig-partitioning.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=e2800340027070001d0e158a28467567" alt="MIG profile 通过不同数量的计算切片和显存切片组合成 1g、2g、3g、7g 等 GPU Instance" width="546" height="171" data-path="books/ai-systems-performance-engineering/images/ch03-mig-partitioning.png" />
</Frame>

<Accordion title="MIG 配置示例">
  ```bash theme={null}
  # 查看 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 <script> &

  # 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。

  ```bash theme={null}
  # 查看 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_id> -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 <script> &
  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   <script>                         153MiB |
  |    0    1    1      59796      C   <script>                         153MiB |
  |    0    1    2      59885      C   <script>                         153MiB |
  +-----------------------------------------------------------------------------+
  ```
</Accordion>

<Accordion title="MIG 配置持久化">
  GI/CI 配置本身不会跨系统重启保留。生产环境通常使用 [`mig-parted`](https://github.com/NVIDIA/mig-parted) 重建固定的 MIG 切分形态。

  下面示例以 A100-SXM4-40GB 为例保存多套候选配置。`mig-devices` 里的 profile 名称来自 `nvidia-smi mig -lgip` 输出的 `Name` 列，例如 `MIG 3g.20gb` 对应配置里的 `"3g.20gb"`。

  ```yaml theme={null}
  # 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`

  常用操作如下：

  ```bash theme={null}
  # 应用固定的 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)
  ```
</Accordion>

<Accordion title="MIG 与 MPS 结合使用">
  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。

  ```bash theme={null}
  # 创建两个 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。

  ```bash theme={null}
  #!/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：

  ```text theme={null}
  +-----------------------------------------------------------------------------------------+
  | 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 |
  +-----------------------------------------------------------------------------------------+
  ```
</Accordion>

<Accordion title="在 GPU Operator 中使用 MIG">
  [NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-operator-mig.html) 通过 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`

  ```bash theme={null}
  # 安装时启用 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` 指定：

  ```yaml theme={null}
  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](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-operator-mig.html#example-custom-mig-configuration)。

  可以用下面的命令查看当前配置：

  ```bash theme={null}
  # 查看 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` 应用目标配置。

  ```bash theme={null}
  # profile 名称来自 default-mig-parted-config 里的 mig-configs
  kubectl label nodes <node-name> \
    nvidia.com/mig.config=all-1g.5gb \
    --overwrite
  ```

  工作负载申请资源时，根据 MIG strategy 使用对应的资源名：

  ```yaml theme={null}
  # single strategy: MIG device 通常作为 nvidia.com/gpu 暴露
  resources:
    limits:
      nvidia.com/gpu: 1
  ```

  ```yaml theme={null}
  # mixed strategy: 按 MIG profile 申请资源
  resources:
    limits:
      nvidia.com/mig-1g.5gb: 1
  ```
</Accordion>

### 2.4 GPU 时钟频率与 ECC

NVIDIA GPU 的 GPU Boost 会在功耗和温度限制内自动调整核心频率。大多数时候，让 GPU 按默认策略动态调频即可。

**需要确定性、可重复的结果时，比如做 benchmark，可以把核心频率锁到固定档位**。日常训练和推理建议保持 auto-boost 开启；只有观察到明显的性能波动或 GPU throttling 时，再考虑锁频排查。

锁频前先查看 GPU 支持的频率档位。这里关注 `Graphics`，它对应核心频率：

```bash theme={null}
nvidia-smi -q -d SUPPORTED_CLOCKS

# 输出结果
==============NVSMI LOG==============

Timestamp                                              : Sun Jun 21 03:34:51 2026
Driver Version                                         : 590.48.01
CUDA Version                                           : 13.1

Attached GPUs                                          : 5
GPU 00000000:01:00.0
    Supported Clocks
        Memory                                         : 1593 MHz
            Graphics                                   : 1410 MHz
            Graphics                                   : 1395 MHz
            Graphics                                   : 1380 MHz
            Graphics                                   : 1365 MHz
            Graphics                                   : 1350 MHz
            Graphics                                   : 1335 MHz
            Graphics                                   : 1320 MHz
......
```

下面的例子使用上面输出中的 1410MHz。实际操作时，应换成当前机器 `SUPPORTED_CLOCKS` 里列出的值。

```bash theme={null}
# 固定核心频率，相当于让 auto-boost 不再动态拉升
sudo nvidia-smi -lgc 1410,1410

# 解锁核心频率，恢复默认动态调频
sudo nvidia-smi -rgc
```

ECC（Error-Correcting Code，纠错码）用于保护显存数据。比如宇宙射线导致单比特内存错误时，ECC 可以在运行中纠正；如果出现双比特错误，ECC 会检测到，并向调用代码抛出错误。NVIDIA 数据中心 GPU 通常默认启用 ECC。

ECC 会占用一部分显存容量，并带来少量带宽开销。长训练中，未纠正错误比这点性能损耗更危险。可以用下面的命令分别查看 ECC 错误计数：

```bash theme={null}
# ECC 模式和 ECC 错误计数
$ nvidia-smi -q -d ECC
GPU 00000000:01:00.0
    ECC Mode
        Current                                        : Enabled  # 当前 ECC 已开启
        Pending                                        : Enabled
    ECC Errors
        Volatile
            SRAM Correctable                           : 0
            DRAM Correctable                           : 0
            DRAM Uncorrectable                         : 0        # 不可纠正错误应为 0
        Aggregate
            SRAM Correctable                           : 0
            DRAM Correctable                           : 9        # 可纠正错误，持续增长时需要观察
            DRAM Uncorrectable                         : 0
            SRAM Threshold Exceeded                    : No
```

## 3. 系统调优脚本

[`system_tuning.sh`](https://github.com/cfregly/ai-performance-engineering/blob/main/code/ch03/system_tuning.sh) 脚本里包含 CPU 频率策略、swap、透明大页（THP）、网络参数、中断亲和性等调优项。

<Accordion title="system_tuning.sh 示例">
  ```bash theme={null}
  #!/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."
  ```
</Accordion>

## 4. 容器运行时优化

AI 系统常用 Docker、Kubernetes 等容器运行时和编排工具来管理软件环境。容器能把 CUDA、Python package、系统库和推理/训练框架版本固定下来，减少“在我机器上能跑”的环境漂移问题。

容器会引入一点复杂度和少量开销，但配置正确时，GPU workload 可以接近裸机性能。容器不是传统虚拟机（VM）。和 VM 不同，容器共享宿主 OS kernel，CPU 和内存操作通常接近原生速度，没有额外的 hypervisor 虚拟化层。

**配合 NVIDIA Container Toolkit，Docker 容器内的 GPU 访问是直接的，不会引入传统虚拟化开销。对于现代 GPU 和较新的 NVIDIA Container Toolkit，只要环境配置正确，容器内运行和直接在裸机宿主上运行的 GPU 性能通常几乎一致，差异通常可以控制在 [2%](https://www.redhat.com/en/blog/mlperf-inference-v50-results) 以内**。

### 4.1 NVIDIA Container Toolkit

[NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/) 允许用户构建和运行 GPU 加速容器。它包含 [container runtime library](https://github.com/NVIDIA/libnvidia-container) 和相关工具，可以自动配置容器，让容器使用 NVIDIA GPU。

NVIDIA Container Toolkit 的组件可以分到两条路径里：传统的 OCI `prestart` hook 路径，以及标准化的 CDI 路径。

**Legacy OCI 路径**

* **NVIDIA Container Runtime（`nvidia-container-runtime`）**：OCI runtime wrapper。legacy mode 下，它会修改容器启动时传入的 OCI runtime spec，插入 NVIDIA `prestart` hook，再调用底层容器运行时（`runc` / `crun`）。
* **NVIDIA Container Runtime Hook（`nvidia-container-runtime-hook`）**：OCI `prestart` hook。在容器创建后、启动前读取 `config.json`，把 GPU device、driver capabilities 和约束转换成 `nvidia-container-cli` 参数。
* **NVIDIA Container CLI（`nvidia-container-cli`）**：`libnvidia-container` 提供的命令行入口。runtime hook 会调用它完成 GPU device、driver libraries 和权限配置。
* **NVIDIA Container Library（`libnvidia-container1`）**：底层库，依赖 Linux kernel primitives，提供自动配置 GNU/Linux 容器以使用 NVIDIA GPU 的能力。

**CDI 路径**

* **NVIDIA CDI Hooks（`nvidia-cdi-hook`）**：CDI spec 中可调用的辅助工具，用于调整权限、创建 symlink、更新动态链接器缓存等额外操作。

**公共组件**

* **NVIDIA Container Toolkit CLI（`nvidia-ctk`）**：通用配置与管理工具。legacy 路径下用 `nvidia-ctk runtime configure` 把 NVIDIA runtime 写入 Docker / containerd / CRI-O 配置；CDI 路径下用 `nvidia-ctk cdi generate` 生成 CDI spec。

下图展示了这些组件在传统 OCI hook 路径和 CDI 路径中的关系：

<Frame>
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-nvidia-container-toolkit-components.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=ec669ad0473bf4225936f6bc5f16b21f" alt="NVIDIA Container Toolkit 组件关系图，展示 nvidia-ctk、nvidia-container-runtime、runtime hook、nvidia-container-cli、libnvidia-container1、nvidia-cdi-hook 和宿主机 NVIDIA driver stack 的关系" width="1693" height="929" data-path="books/ai-systems-performance-engineering/images/ch03-nvidia-container-toolkit-components.png" />
</Frame>

<Accordion title="OCI 与 CDI">
  **[OCI（Open Container Initiative）](https://github.com/opencontainers/runtime-spec)** 定义容器运行时规范。OCI runtime spec 是底层运行时创建容器时读取的配置文件，里面包含要启动的进程、`rootfs`、`mounts`、`env`、`devices`、`hooks`、`namespaces`、`cgroups` 等运行时配置。`runc` / `crun` 这类底层运行时会按这个 spec 创建容器。

  **[CDI（Container Device Interface）](https://github.com/cncf-tags/container-device-interface)** 是设备注入规范。CDI spec 里的 `containerEdits` 字段描述需要对 OCI spec 做哪些修改，例如 `deviceNodes`、`mounts`、`env`、`hooks`、`permissions` 等。支持 CDI 的 runtime 会读取 CDI spec，并把这些 edits 应用到 OCI runtime spec。

  两者的关系是：OCI 描述容器整体如何启动，CDI 描述设备如何注入容器。支持 CDI 的 runtime 会把 CDI spec 里的设备配置应用到 OCI runtime spec，最后仍由 `runc` / `crun` 按 OCI spec 创建容器。
</Accordion>

<Accordion title="NVIDIA Container Runtime 注入了什么">
  GPU 访问本质上依赖 Linux 的设备文件。NVIDIA kernel driver（`nvidia.ko`）加载后，会在宿主机 `/dev/` 下创建一组字符设备：

  ```bash theme={null}
  $ ls -la /dev/nvidia*
  crw-rw-rw- 1 root root 195,   0 Mar 14 10:00 /dev/nvidia0
  crw-rw-rw- 1 root root 195,   1 Mar 14 10:00 /dev/nvidia1
  crw-rw-rw- 1 root root 195, 255 Mar 14 10:00 /dev/nvidiactl
  crw-rw-rw- 1 root root 510,   0 Mar 14 10:00 /dev/nvidia-uvm
  crw-rw-rw- 1 root root 510,   1 Mar 14 10:00 /dev/nvidia-uvm-tools
  ```

  CUDA 程序不会直接和 GPU 硬件通信。它会打开 `/dev/nvidia0` 这类设备文件，并通过 `ioctl()` 系统调用进入宿主机 kernel driver，由 kernel driver 负责和 GPU 硬件交互。

  一个裸容器（bare container）本身没有 GPU 访问能力。它的 `/dev/` 目录里只有标准设备，比如 `null`、`zero`、`pts`。NVIDIA Container Runtime（`nvidia-container-runtime`）会在应用启动前向容器注入三类内容：

  * **Device nodes**：从宿主机 bind mount `/dev/nvidia0`（具体 GPU 设备文件）、`/dev/nvidiactl`（NVIDIA driver 控制设备）、`/dev/nvidia-uvm`（Unified Virtual Memory）。
  * **Driver libraries**：bind mount `libcuda.so`（CUDA Driver API）、`libnvidia-ml.so`（NVML，用于 `nvidia-smi`、DCGM 和监控工具查询 GPU 状态）和其他与宿主机 driver 匹配的库。
  * **Device permissions**：配置 cgroup device controller，允许容器访问 NVIDIA 主设备号（device major number）。主设备号是 Linux 用来把设备文件路由到对应内核驱动的编号。
</Accordion>

<Accordion title="容器到宿主机 GPU 的调用路径">
  下图展示了 GPU 软件栈如何在容器和宿主机之间协同工作：

  * **应用层**：`PyTorch`、`TensorRT`、`JAX` 等框架运行在各自容器里。它们会调用容器内的 CUDA 用户态库（`libcudart.so`），也可能直接使用 CUDA Driver API（`libcuda.so`）。
  * **CUDA 用户态库**：`libcudart.so`、cuBLAS、cuDNN、NCCL 等通常来自容器镜像。不同容器可以携带不同 CUDA 版本，例如 CUDA 11.8、12.1、12.4。
  * **CUDA Driver API**：`libcuda.so` 是 driver API 库，通常由宿主机 driver bind mount 进容器。容器内的 CUDA 用户态库和框架代码最终通过它进入宿主机 driver 路径。
  * **设备文件**：`/dev/nvidia0` 是 GPU 设备文件，也由宿主机挂进容器。`libcuda.so` 通过它发起 `ioctl()` 系统调用，跨过 user / kernel 边界。
  * **Kernel driver**：`nvidia.ko` 运行在宿主机 kernel 中，负责处理 `ioctl()` 请求并和 GPU 硬件通信。所有容器共享同一个宿主机 kernel driver。
  * **GPU hardware**：真正执行计算的物理 GPU。

  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-gpu-container-driver-stack.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=b9be51ed90a6e26b53ad6065cdc61d7a" alt="多个容器使用不同 CUDA runtime，通过宿主机挂载的 libcuda.so、/dev/nvidia0 和共享的 nvidia.ko kernel driver 访问 GPU" width="1024" height="545" data-path="books/ai-systems-performance-engineering/images/ch03-gpu-container-driver-stack.png" />
  </Frame>

  图源：[How Docker Works with GPUs: Device Files, Bind Mounts, and Driver Stacks](https://www.abhik.ai/concepts/systems/gpu-containers)
</Accordion>

<Accordion title="安装与配置 NVIDIA Container Toolkit">
  安装步骤参考官方文档：[Installing the NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)。

  安装完成后，还需要把容器运行时接到 NVIDIA Container Runtime。NVIDIA Container Toolkit 支持 Docker、containerd、CRI-O 和 Podman。下面以 Docker 为例，使用 `nvidia-ctk` 命令配置 runtime：

  ```bash theme={null}
  sudo nvidia-ctk runtime configure --runtime=docker
  ```

  `nvidia-ctk` 命令会修改宿主机上的 `/etc/docker/daemon.json` 文件，把 `nvidia` runtime 注册进去，让 Docker 可以使用 NVIDIA Container Runtime，配置类似：

  ```json theme={null}
  {
    "runtimes": {
      "nvidia": {
        "path": "nvidia-container-runtime",
        "runtimeArgs": []
      }
    }
  }
  ```

  重启 Docker daemon：

  ```bash theme={null}
  sudo systemctl restart docker
  ```

  安装 GPU driver 并完成 runtime 配置后，可以运行一个 CUDA 容器验证 GPU 是否正确暴露给 Docker：

  ```bash theme={null}
  sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi
  ```

  输出应类似下面这样：

  ```text theme={null}
  +-----------------------------------------------------------------------------+
  | NVIDIA-SMI 535.86.10    Driver Version: 535.86.10    CUDA Version: 12.2     |
  |-------------------------------+----------------------+----------------------+
  | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
  | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
  |                               |                      |               MIG M. |
  |===============================+======================+======================|
  |   0  Tesla T4            On   | 00000000:00:1E.0 Off |                    0 |
  | N/A   34C    P8     9W /  70W |      0MiB / 15109MiB |      0%      Default |
  |                               |                      |                  N/A |
  +-------------------------------+----------------------+----------------------+

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

<Accordion title="使用 CDI 路径">
  CDI 是 vendor 中立的设备接口标准，也是 Podman、containerd 和较新 Docker 推荐的方式。用法分两步：先生成 CDI spec，再用 `--device` 按名字请求 GPU，不需要 `--gpus` 或 `--runtime=nvidia`。

  **1. 生成 CDI spec**（扫描本机 GPU / MIG 设备并写出 spec；`/etc/cdi` 为静态目录、`/var/run/cdi` 为临时目录）：

  ```bash theme={null}
  sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
  ```

  spec 里每个设备都有一个完全限定名：`nvidia.com/gpu=all`（全部）、`nvidia.com/gpu=gpu0`（按索引）、`nvidia.com/gpu=mig<GPU>:<MIG>`（MIG 实例）。用 `nvidia-ctk cdi list` 查看：

  ```bash theme={null}
  nvidia-ctk cdi list
  ```

  从 Container Toolkit v1.18 起，`nvidia-cdi-refresh` systemd 服务会在安装、driver 升级、重启时自动重新生成 spec；但**驱动卸载或 MIG 重新配置后需要手动重新生成**。

  **2. 运行容器**，用 `--device` 指定 CDI 设备名：

  ```bash theme={null}
  # Podman（v4.1+ 原生支持 CDI）
  podman run --rm --device nvidia.com/gpu=all ubuntu nvidia-smi

  # Docker：28.3.0 起默认开启 CDI；25.0–28.2 需先在 /etc/docker/daemon.json 打开
  #   { "features": { "cdi": true } }  再 sudo systemctl restart docker
  docker run --rm --device nvidia.com/gpu=all ubuntu nvidia-smi
  ```

  和 legacy 路径相比，CDI 不再需要 `nvidia-container-runtime-hook` → `nvidia-container-cli` 这条启动时注入链——设备节点、驱动库、权限都由 CDI spec 声明，再由支持 CDI 的 runtime 直接应用到容器。
</Accordion>

<Accordion title="CUDA 版本与 NVIDIA driver 兼容性">
  不同容器之间的 CUDA runtime、CUDA 版本可以不一样，但它们底层共享的是宿主机上的 NVIDIA driver。

  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-cuda-components.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=9918a4dee5d02f19ff65436159d7c98d" alt="CUDA Toolkit 与 NVIDIA driver package 的组成关系：CUDA Toolkit 位于上层，driver package 包含 libcuda.so 和 nvidia.ko" width="500" height="390" data-path="books/ai-systems-performance-engineering/images/ch03-cuda-components.png" />
  </Frame>

  图源：[CUDA Compatibility: CUDA Components](https://docs.nvidia.com/deploy/cuda-compatibility/latest/why-cuda-compatibility.html)

  一般规则是：**宿主机 NVIDIA driver 版本必须至少达到容器内 CUDA 版本要求的最低 driver 版本**。

  根据 NVIDIA 的 [CUDA Minor Version Compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/latest/minor-version-compatibility.html) 文档：

  * CUDA 13.x 需要 Linux 宿主机 NVIDIA driver **R580 驱动分支或更新**。
  * CUDA 12.x 需要 Linux 宿主机 NVIDIA driver **R525 驱动分支或更新**。
  * CUDA 11.x 需要 Linux 宿主机 NVIDIA driver **R450 驱动分支或更新**。

  当宿主机 driver 不方便整体升级，但应用或容器需要较新的 CUDA 用户态组件时，可以考虑通过 [Forward Compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/latest/forward-compatibility.html) 的方式进行升级。具体做法是：在宿主机 kernel-mode driver（`nvidia.ko`）不变的情况下，通过安装 CUDA Forward Compatibility Package，补上较新的 user-mode driver libraries，例如 `libcuda.so`，让较新的 CUDA 应用在满足条件的旧 driver 环境上运行。

  正常升级 NVIDIA driver 会同时更新 user-mode libraries 和 kernel-mode driver。Forward Compatibility Package 只补 user-mode 组件，不替换 `nvidia.ko`。如果新功能依赖新的 kernel driver 能力，仍然需要升级宿主机 driver。

  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-cuda-forward-compatibility-upgrade-path.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=ce8fcae7a6b5fe7c74def2e296254fb0" alt="CUDA Forward Compatibility 通过升级 CUDA user-mode 组件和 libcuda.so，保持 kernel-mode driver nvidia.ko 不变" width="624" height="467" data-path="books/ai-systems-performance-engineering/images/ch03-cuda-forward-compatibility-upgrade-path.png" />
  </Frame>

  图源：[CUDA Compatibility: Forward Compatibility Upgrade Path](https://docs.nvidia.com/deploy/cuda-compatibility/latest/forward-compatibility.html)
</Accordion>

### 4.2 避免 Overlay FS 开销

和直接在宿主机运行相比，Docker 容器更容易出现差异的地方通常是 I/O。容器常用联合文件系统（union filesystem），它的好处很直接：把只读镜像层和可写容器层透明叠加成一个统一视图，容器看起来像在使用一个完整的文件系统。

OverlayFS 在读写文件时会带来额外开销：

* 读取文件时：文件系统需要检查只读层和可写层，判断应该返回哪个版本的文件。额外的 metadata lookup 和层合并逻辑，会比单一文件系统多一点延迟。
* 修改文件时：如果修改的是只读层里的文件，会触发 copy-on-write。文件必须先复制到可写层，写入发生在这份副本上，而不是原始只读文件上。

<Accordion title="OverlayFS 与 Copy-on-Write（COW）">
  OverlayFS 是 Linux 文件系统能力，可以把一个文件系统叠在另一个文件系统之上，并提供一个合并后的视图。

  可以把它理解成两个目录：`upper directory` 可读可写，`lower directory` 只读。`upper directory` 一开始通常是空的，只保存挂载之后新增或修改过的文件。OverlayFS 会把它们组合成一个 `merged directory`，让进程看起来像是在访问一个完整的文件系统。

  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-overlay-filesystem.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=55bb0320b66d8cea3ee39b0da8c1ac42" alt="OverlayFS 把只读 lower filesystem 和可写 upper filesystem 合并为 merged filesystem，并在写入 lower 文件时执行 copy-on-write" width="2400" height="1350" data-path="books/ai-systems-performance-engineering/images/ch03-overlay-filesystem.png" />
  </Frame>

  容器镜像层通常对应只读的 `lower directory`，容器自己的 writable layer 对应 `upper directory`。这样启动容器时不需要复制整份镜像，只需要引用底层只读文件。

  当你想写入或修改来自 `lower directory` 的文件时，OverlayFS 会使用一种叫**写时复制（copy-on-write，COW）** 的技术。它不会修改原始文件，而是在 `upper directory` 中创建该文件的一份副本，并把修改应用到这份副本上。因此，如果修改来自 `lower directory` 的 `b.txt`，OverlayFS 会在 `upper directory` 中创建一份 `b.txt` 副本，后续修改都发生在这份副本上，原始的 lower 文件保持不变。这样既能保证基础系统的完整性，也能减少需要复制的数据量。

  参考资料：[Scaling Firecracker: Using OverlayFS to Save Disk Space](https://e2b.dev/blog/scaling-firecracker-using-overlayfs-to-save-disk-space)
</Accordion>

**模型训练经常涉及重 I/O 操作，例如读取数据集、加载模型和写入 checkpoint。为了解决这个问题，可以使用 bind mount 把宿主机目录或网络文件系统挂进容器。** Bind mount 会绕过 overlay，因此性能接近直接在宿主机上进行磁盘 I/O。如果宿主文件系统是 NVMe SSD 或 NFS mount，就能获得底层存储设备本身的性能。

```bash theme={null}
docker run --rm --gpus all \
  -v /data/dataset:/mnt/dataset:ro \
  nvcr.io/nvidia/pytorch:25.03-py3 \
  python train.py \
    --data /mnt/dataset
```

这里假设训练数据在宿主机的 `/data/dataset`。`-v /data/dataset:/mnt/dataset:ro` 会把它挂载到容器内的 `/mnt/dataset`，其中 `ro` 表示只读挂载。训练脚本从 `/mnt/dataset` 读取数据，相当于直接从宿主文件系统读取。

### 4.3 HPC 场景的容器运行时

HPC 集群对容器运行时提出了更高要求。集群通常遵循最小权限原则：用户应以自身身份运行作业，不能提升权限，也不应接触计算节点上由 root 管理的系统守护进程。同时，HPC 容器运行时还需要更便捷地接入 GPU、高速网络和并行文件系统等宿主资源。因此，在 HPC 场景中，有时候会选择 **Apptainer** 或 **Enroot** 这类面向 HPC 集群环境设计的容器运行时。

[Apptainer](https://apptainer.org/docs/user/latest/introduction.html) 最初是为了在 HPC 集群上以简单、可移植、可复现的方式运行复杂应用而创建的。它最早由劳伦斯伯克利国家实验室（Lawrence Berkeley National Laboratory）开发，很快在其他 HPC 中心、学术机构和更多场景中流行起来。

Apptainer 的设计更贴近 HPC 集群的使用方式：

* 可验证的复现性和安全性：支持加密签名、不可变容器镜像格式和内存中解密。
* 默认强调集成而不是隔离：更容易使用集群上的 GPU、高速网络和并行文件系统。
* 计算环境可移动：SIF 单文件镜像便于传输、共享和归档。
* 简单的安全模型：容器内外默认是同一个用户，默认不能因为进入容器而获得宿主机额外权限。

<Accordion title="Apptainer GPU 容器使用">
  Apptainer 的安装可参考官方文档：[Installing Apptainer](https://apptainer.org/docs/admin/main/installation.html)。

  Apptainer 运行 NVIDIA GPU 容器时，常用 `--nv`。这个选项会让容器看到宿主机的 NVIDIA device entries，并把基础 CUDA/NVIDIA driver libraries 绑定进容器。

  ```bash theme={null}
  # 拉取镜像
  # apptainer pull <image-name> docker://<registry>/<repo>:<tag>
  apptainer pull pytorch-cuda.sif docker://docker.io/pytorch/pytorch:2.2.2-cuda12.1-cudnn8-runtime

  # 在容器中运行命令
  # apptainer exec --nv <image-name> nvidia-smi
  apptainer exec --nv pytorch-cuda.sif nvidia-smi
  ```

  如果启动的是长期运行的 instance，可以用 `apptainer instance list` 查看，用 `instance://<name>` 进入或执行命令：

  ```bash theme={null}
  # 启动 instance
  # apptainer instance start --nv <image-name> <instance-name>
  apptainer instance start --nv pytorch-cuda.sif train-env

  # 查看 instance
  apptainer instance list

  # 进入 instance
  # apptainer shell instance://<instance-name>
  apptainer shell instance://train-env

  # 或直接在 instance 中执行命令
  # apptainer exec instance://<instance-name> nvidia-smi
  apptainer exec instance://train-env nvidia-smi

  # 停止 instance
  # apptainer instance stop <instance-name>
  apptainer instance stop train-env
  ```

  控制 GPU 可见性时，常见方式是在运行容器前设置 `APPTAINERENV_CUDA_VISIBLE_DEVICES`。Apptainer 会把它映射成容器内的 `CUDA_VISIBLE_DEVICES`，影响遵循该变量的 CUDA 程序：

  ```bash theme={null}
  export APPTAINERENV_CUDA_VISIBLE_DEVICES=0,1
  apptainer exec --nv pytorch-cuda.sif python - <<'PY'
  import os
  import torch

  print("CUDA_VISIBLE_DEVICES =", os.environ.get("CUDA_VISIBLE_DEVICES"))
  print("visible CUDA devices =", torch.cuda.device_count())
  for i in range(torch.cuda.device_count()):
      print(i, torch.cuda.get_device_name(i))
  PY

  # 输出结果
  visible CUDA devices = 2
  0 NVIDIA A100-SXM4-80GB
  1 NVIDIA A100-SXM4-80GB
  ```

  注意，`APPTAINERENV_CUDA_VISIBLE_DEVICES` 限制的是 CUDA 程序使用哪些 GPU，不一定限制 `nvidia-smi` 能枚举到的设备。Apptainer 也提供实验性的 `--nvccli`，通过 NVIDIA 的 `nvidia-container-cli` 完成 GPU 容器配置。它需要宿主机安装 `nvidia-container-cli`，并且通常要配合 `--writable-tmpfs` 或 writable sandbox 使用；在 setuid 安装模式下也有额外安全限制。

  在 `--nvccli` 模式下，如果需要更强的设备隔离，可以配合 `--contain` 和 `NVIDIA_VISIBLE_DEVICES` 控制实际绑定进容器的 GPU：

  ```bash theme={null}
  export NVIDIA_VISIBLE_DEVICES=1,2
  apptainer run --nvccli --writable-tmpfs --contain pytorch-cuda.sif nvidia-smi

  # 输出
  +-----------------------------------------------------------------------------------------+
  | NVIDIA-SMI 590.48.01              Driver Version: 590.48.01      CUDA Version: 13.1     |
  +-----------------------------------------+------------------------+----------------------+
  | GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
  | Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
  |                                         |                        |               MIG M. |
  |=========================================+========================+======================|
  |   0  NVIDIA A100-SXM4-80GB          On  |   00000000:01:00.0 Off |                    0 |
  | N/A   34C    P0             83W /  275W |       1MiB /  81920MiB |      0%      Default |
  |                                         |                        |             Disabled |
  +-----------------------------------------+------------------------+----------------------+
  |   1  NVIDIA A100-SXM4-80GB          On  |   00000000:47:00.0 Off |                    0 |
  | N/A   34C    P0             88W /  275W |       1MiB /  81920MiB |      0%      Default |
  |                                         |                        |             Disabled |
  +-----------------------------------------+------------------------+----------------------+

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

  参考资料：

  * [Apptainer GPU and other Device Support](https://apptainer.org/docs/user/latest/gpu.html)
  * [Container on HPC with Apptainer: GPUs](https://coderefinery.github.io/hpc-containers/gpus/)
</Accordion>

[Enroot](https://github.com/NVIDIA/enroot) 是 NVIDIA 面向高性能环境的轻量容器工具。它的核心特点包括：

* 遵循 KISS 原则和 Unix philosophy。
* 独立运行，不需要 daemon。
* 支持完全非特权和多用户使用：不需要 setuid binary，继承 cgroup，并使用每用户配置和容器存储。
* 使用简单：镜像格式简单，便于脚本化，也支持 root remapping。
* 几乎不做额外隔离：减少性能开销，也简化 HPC 部署。
* 可组合、可扩展：支持系统级和用户级配置。
* Docker 镜像导入速度快，官方 README 中提到大镜像导入可获得 3x 到 5x 加速。
* 内置基于 `libnvidia-container` 的 GPU 支持。
* 支持 bundles、in-memory containers 等协作和开发工作流。

<Accordion title="Enroot GPU 容器使用">
  安装 Enroot 可参考官方文档：[Enroot Installation](https://github.com/NVIDIA/enroot/blob/main/doc/installation.md#installation)。

  使用 Enroot 时，可以从 Docker/NGC registry 导入镜像，再创建并启动容器：

  ```bash theme={null}
  # 导入镜像
  # enroot import docker://<registry>#<repo>:<tag>
  enroot import docker://nvcr.io#nvidia/pytorch:25.03-py3

  # 创建容器
  # enroot create --name <container-name> <image>.sqsh
  enroot create --name pytorch nvidia+pytorch+25.03-py3.sqsh

  # 启动容器并运行命令
  # enroot start <container-name> <command>
  enroot start pytorch nvidia-smi
  ```

  控制 GPU 可见性时，需要同时设置 `NVIDIA_VISIBLE_DEVICES` 和 `ENROOT_RESTRICT_DEV` 两个环境变量：

  ```bash theme={null}
  ENROOT_RESTRICT_DEV=yes NVIDIA_VISIBLE_DEVICES=0,1 enroot start pytorch nvidia-smi

  # 输出
  +-----------------------------------------------------------------------------------------+
  | NVIDIA-SMI 590.48.01              Driver Version: 590.48.01      CUDA Version: 13.1     |
  +-----------------------------------------+------------------------+----------------------+
  | GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
  | Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
  |                                         |                        |               MIG M. |
  |=========================================+========================+======================|
  |   0  NVIDIA A100-SXM4-80GB          On  |   00000000:01:00.0 Off |                    0 |
  | N/A   34C    P0             83W /  275W |       1MiB /  81920MiB |      0%      Default |
  |                                         |                        |             Disabled |
  +-----------------------------------------+------------------------+----------------------+
  |   1  NVIDIA A100-SXM4-80GB          On  |   00000000:47:00.0 Off |                    0 |
  | N/A   34C    P0             88W /  275W |       1MiB /  81920MiB |      0%      Default |
  |                                         |                        |             Disabled |
  +-----------------------------------------+------------------------+----------------------+

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

  参考资料：[Enroot README](https://github.com/NVIDIA/enroot)
</Accordion>

## 5. Kubernetes 编排与调度

GPU 是集群里最贵、最稀缺的资源，调度方式直接影响利用率与训练/推理成本。

### 5.1 GPU Operator

在 Kubernetes 集群中管理 GPU 可能是一项艰巨的任务。传统方法往往需要手工安装和配置 GPU 驱动，既费时又容易出错。此外，要利用高级 GPU 特性、并确保 GPU 与其它系统组件之间的高效数据传输，还需要专门的知识和工具。缺少一套规范化的方法，这些挑战会拖累 AI/ML 工作负载的性能与可扩展性。

[NVIDIA GPU Operator](https://github.com/nvidia/gpu-operator) 提供了多种特性。它让在 Kubernetes 上设置 GPU 驱动及其配置变得轻而易举。在同一批节点上运行多个 AI 工作负载时，能够使用 vGPU、Multi-Instance GPU（MIG）、GPU Time-Slicing 等高级特性至关重要。此外，GPU 还需要与其它应用/GPU 以及存储之间具备快速通信能力，GPUDirect RDMA、GPUDirect Storage 和 GDR Copy 在其中扮演重要角色。GPU Operator 能帮助你轻松地把这些特性以及更多能力带到 Kubernetes 集群中。

<div style={{ maxWidth: "560px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-nvidia-gpu-operator.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=79d88b55e85a472db62c1f56967c4ac0" alt="NVIDIA GPU Operator 管理 Kubernetes 集群中的 NVIDIA driver、container runtime、device plugin、DCGM exporter、MIG Manager 等 GPU 软件组件" width="502" height="642" data-path="books/ai-systems-performance-engineering/images/ch03-nvidia-gpu-operator.png" />
  </Frame>
</div>

图源：[NVIDIA Developer Blog: NVIDIA GPU Operator](https://developer-blogs.nvidia.com/wp-content/uploads/2019/10/NV-GPU-Operator-1.png)

它管理的主要组件：

| 组件                                                                                                                            | 作用                                                                                                             |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| [NVIDIA GPU Driver](https://github.com/NVIDIA/gpu-driver-container/tree/main)                                                 | 在需要由 GPU Operator 管理 driver 时，它会在 GPU 节点上运行 driver container；如果节点已预装 driver，可以通过 `driver.enabled=false` 关闭这部分。 |
| [NVIDIA Driver Manager for Kubernetes](https://github.com/NVIDIA/k8s-driver-manager/tree/main)                                | 管理 driver 生命周期，包括安装、升级、重启相关服务，以及在升级前驱逐 GPU Pod 或 drain 节点。                                                     |
| [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/)                         | 配置 Docker、containerd、CRI-O 等容器运行时，让容器启动时能拿到 GPU device、driver libraries 和 CDI 设备描述。                            |
| [NVIDIA Kubernetes Device Plugin](https://github.com/NVIDIA/k8s-device-plugin/blob/v0.19.3/README.md)                         | 以 DaemonSet 运行并向 kubelet 注册 GPU 资源，例如 `nvidia.com/gpu`、MIG profile 资源或 sharing 资源。                             |
| [NVIDIA GPU Feature Discovery](https://github.com/NVIDIA/k8s-device-plugin/blob/v0.19.3/docs/gpu-feature-discovery/README.md) | 读取 NVIDIA GPU 信息并生成 GPU 专属 node labels，例如 GPU 型号、显存、driver 版本、MIG 能力和 `nvidia.com/gpu.clique` 等拓扑标签。           |
| [NVIDIA MIG Manager for Kubernetes](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/26.3/gpu-operator-mig.html)  | 根据节点上的 `nvidia.com/mig.config` 等标签配置 MIG mode 和 MIG profile。                                                   |
| [NVIDIA DCGM](https://docs.nvidia.com/datacenter/dcgm/latest/)                                                                | NVIDIA Data Center GPU Manager，负责采集和管理 GPU 健康状态、利用率、显存、温度、功耗、ECC、MIG 等底层指标。                                    |
| [NVIDIA DCGM Exporter](https://github.com/NVIDIA/dcgm-exporter/tree/main)                                                     | 把 DCGM 指标转换为 Prometheus 可抓取的 metrics，用于 GPU 监控和告警。                                                             |
| [Validator for NVIDIA GPU Operator](https://github.com/NVIDIA/gpu-operator/blob/v26.3.3/cmd/nvidia-validator/main.go)         | 在安装和升级后验证各个 operand 是否真正可用，例如 driver、toolkit、device plugin、CUDA 和 vfio/vGPU 相关路径。                              |

NVIDIA GPU Operator 的关键特性：

* **自动安装和维护 GPU driver**：NVIDIA GPU Operator 自动安装和维护 GPU driver，消除手动干预的需要。自动化可以确保 driver 始终保持最新并正确配置，让 AI/ML workload 平稳、高效地运行。
* **配置高级 GPU 特性**：
  * **vGPU（Virtual GPU）** 允许多个虚拟机共享单张 GPU，提升资源利用率和灵活性。
  * **MIG（Multi-Instance GPU）** 允许单张 GPU 被划分成多个彼此独立的实例，每个实例拥有专用资源，从而改善 workload 隔离和效率。
  * **GPU Time-Slicing** 在多个任务之间切分 GPU 时间，保证 GPU 资源在不同 workload 之间公平、高效地分配。
* **配置 GPUDirect RDMA 和 GPUDirect Storage**：
  * **GPUDirect RDMA（Remote Direct Memory Access）** 支持不同节点上的 GPU 直接通信，绕过 CPU 并降低延迟，这对高性能计算应用非常重要。
  * **GPUDirect Storage** 支持 GPU 与存储设备之间直接传输数据，显著加速数据密集型应用的数据访问和处理。
* **配置 GDR Copy**：GPUDirect RDMA（GDR）Copy 是一个基于 GPUDirect RDMA 技术的低延迟 GPU memory copy library，允许 CPU 直接 map 和访问 GPU memory。它可以提升 memory copy 操作的效率，降低开销，并改善整体性能。

安装步骤可以直接参考 [Installing the NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/getting-started.html)。

先添加 NVIDIA Helm repository：

```bash theme={null}
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia \
    && helm repo update
```

然后使用 Helm 安装 GPU Operator：

```bash theme={null}
helm install --wait --generate-name \
    -n gpu-operator --create-namespace \
    nvidia/gpu-operator \
    --version=v26.3.3
```

参考资料：

* [NVIDIA GPU Operator 官方文档](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html)
* [Installing the NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/getting-started.html)
* [InfraCloud：A Guide to NVIDIA GPU Operator in Kubernetes](https://www.infracloud.io/blogs/guide-to-nvidia-gpu-operator-in-kubernetes/)
* [NVIDIA Blog：Simplifying GPU Management in Kubernetes](https://developer.nvidia.com/blog/nvidia-gpu-operator-simplifying-gpu-management-in-kubernetes/)
* [Time-Slicing GPUs in Kubernetes（GPU Operator）](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html)
* [GPU Operator with MIG](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-operator-mig.html)

### 5.2 Device Plugin

GPU Operator 部署的组件里，真正把 GPU 暴露给 Kubernetes 调度的是 device plugin。NVIDIA 的 [k8s-device-plugin](https://github.com/NVIDIA/k8s-device-plugin) 以 DaemonSet 运行，向 kubelet 注册扩展资源 `nvidia.com/gpu`。启用 MIG 时，device plugin 可以按 profile 暴露 `nvidia.com/mig-*` 资源；启用 GPU sharing 时，则可以把共享后的访问入口暴露为 `nvidia.com/gpu.shared` 等资源。

<div style={{ maxWidth: "980px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-kubernetes-device-plugin-workflow.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=c334bcfde9db1a2f4520250d44f14e47" alt="Kubernetes Device Plugin 架构与工作流程：设备发现、资源注册、调度决策和设备分配挂载" width="2268" height="1234" data-path="books/ai-systems-performance-engineering/images/ch03-kubernetes-device-plugin-workflow.png" />
  </Frame>
</div>

虽然 device plugin 仍是 Kubernetes 集群中最常用的 GPU 资源管理方式，但它的资源模型仍有一些限制：

* **仅支持整数计数**：资源以不透明整数暴露，无法按显存容量、架构、compute capability 等属性筛选设备。
* **共享只能节点级配置**：默认整卡独占；time-slicing / MPS 共享只能按节点 ConfigMap 统一开启（time-slicing 无显存/故障隔离），无法在单个请求中按工作负载表达。
* **缺乏属性级调度**：调度器只感知数量，无法指定具体设备，需借助节点 label 与 `nodeSelector` 间接约束放置。
* **MIG 布局静态**：MIG profile 须在节点侧预先配置，调整布局需重新配置节点，无法按工作负载动态切分。
* **拓扑感知不足**：对 NVLink、NUMA 等拓扑距离缺乏感知。

### 5.3 Dynamic Resource Allocation (DRA)

[DRA](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/) 是 Kubernetes 的一项功能，用于在 Pod 之间请求和共享资源。这些资源通常是硬件加速器这类附加设备。
DRA 提供了一种灵活的方式，用于对集群中的设备进行分类、请求和使用。使用 DRA 可以带来如下收益：

* **灵活的设备过滤**：使用 Common Expression Language（CEL）对特定设备属性进行细粒度过滤。
* **设备共享**：通过引用对应的 `ResourceClaim`，让多个容器或 Pod 共享同一个资源。
* **集中式设备分类**：设备驱动和集群管理员可以使用 `DeviceClass`，为应用提供针对不同使用场景优化过的硬件类别。例如，可以为通用 workload 创建成本优化型 `DeviceClass`，为关键任务创建高性能 `DeviceClass`。
* **简化 Pod 请求**：使用 DRA 时，应用不需要在 Pod resource request 中指定设备数量。Pod 会引用一个 `ResourceClaim`，该 `ResourceClaim` 中的设备配置会应用到 Pod。

DRA 使用下面这些 Kubernetes API kind 提供核心分配功能：

| 资源                      | 解释                                                                                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `ResourceSlice`         | 表示集群中真实可用的设备资源清单，通常由 DRA driver 创建和维护。Scheduler 会用 `ResourceSlice` 判断哪些 Node 上有满足 `ResourceClaim` 要求的设备，并据此完成资源分配和 Pod 调度。                      |
| `DeviceClass`           | 定义一类可被申请的设备，以及申请时如何通过属性选择具体设备。它本身不代表具体设备，而是给 `ResourceClaim` 提供一个设备类别和选择规则。                                                                     |
| `ResourceClaim`         | 表示一次具体的资源申请，例如“我要一个满足某些条件的 GPU”。Pod 通过引用 `ResourceClaim` 来获得已分配的设备资源。                                                                           |
| `ResourceClaimTemplate` | 是 `ResourceClaim` 的模板，常用于 Deployment、StatefulSet 这类会创建多个 Pod 的 workload。Kubernetes 会根据模板为每个 Pod 自动生成独立的 `ResourceClaim`，Pod 删除时对应的 claim 也会被删除。 |

<Accordion title="从 CSI 看 DRA：同一套声明式设计">
  如果对 Kubernetes 比较熟悉，会发现 DRA 的设计思路和 CSI 很像：都把异构资源的管理拆成同样的三层——供给、分类、申请。

  * **CSI（存储）**：`PersistentVolume` 代表可用存储 → `StorageClass` 分类 → `PersistentVolumeClaim` 申请。
  * **DRA（设备）**：`ResourceSlice` 代表可用设备 → `DeviceClass` 分类 → `ResourceClaim` 申请。

  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-csi-vs-dra.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=037d56d52ca280f607eae2cb6be2980d" alt="CSI 与 DRA 的对比：PV / StorageClass / PVC 分别对应 ResourceSlice / DeviceClass / ResourceClaim，并从定位、管理对象、核心资源、典型用途、关注点、生态成熟度等维度对比两者" width="1024" height="682" data-path="books/ai-systems-performance-engineering/images/ch03-csi-vs-dra.png" />
  </Frame>

  > 严格说 `ResourceSlice` 和 `PV` 不能完全等同：`PV` 是一块具体的存储卷，`ResourceSlice` 更像节点上设备的目录清单。但从供需关系看，模式是一样的。

  DRA 三者按数据流方向串起来：

  ```text theme={null}
  ResourceSlice ──→ DeviceClass ──→ ResourceClaim ──→ 调度器分配
     (供给)           (分类)           (需求)           (决策)
  ```

  * **`ResourceSlice`（供给）**：DRA driver 发布节点上的设备信息，包括型号、显存、驱动版本等属性，以及可用容量。
  * **`DeviceClass`（分类）**：管理员用 CEL 表达式从设备属性里筛出一类设备，形成用户友好的分组，比如「所有 NVIDIA GPU」或「仅 A100」。
  * **`ResourceClaim`（需求）**：用户通过 `deviceClassName` 引用某个 `DeviceClass`，声明需要几个该类设备。
  * **调度器（决策）**：综合 `ResourceSlice` 的库存和 `ResourceClaim` 的需求，选出满足条件的节点和具体设备，完成分配。
</Accordion>

<Accordion title="完整流程：一个 Pod 如何用上 DRA 资源">
  以单 Pod、单容器申请一张整卡为例，串起 `ResourceSlice`、`DeviceClass`、`ResourceClaim` 与 Pod 之间的配合，看一次分配从设备注册到容器启动经历哪些阶段。

  **阶段一：设备注册（DRA Driver → ResourceSlice）**

  ```text theme={null}
  DRA Driver 启动
      │
      ├─ 1. 扫描节点上的 GPU 设备
      ├─ 2. 收集设备属性（型号、显存、驱动版本、MIG profile 等）
      └─ 3. 创建 / 更新 ResourceSlice
  ```

  DRA driver 以 DaemonSet 方式运行在每个节点上，持续 watch 设备状态。设备发生变化（新增、移除、健康状态变化）时，driver 更新对应的 `ResourceSlice` 并递增 `pool.generation`。

  一份真实的 `ResourceSlice`（A100 节点，保留一个 MIG 实例和一张整卡的完整字段，其余设备省略）：

  ```yaml theme={null}
  apiVersion: resource.k8s.io/v1
  kind: ResourceSlice
  metadata:
    name: 00000-gpu.nvidia.com-kind-dra-1-worker-nmcz2
    ownerReferences:                 # 归属该 Node，节点删除时 slice 一并回收
    - kind: Node
      name: kind-dra-1-worker
      controller: true
  spec:
    driver: gpu.nvidia.com           # 由哪个 DRA driver 发布
    nodeName: kind-dra-1-worker      # 这些设备位于哪个节点
    pool:
      name: kind-dra-1-worker
      generation: 1                  # 设备变化时递增
      resourceSliceCount: 1
    devices:
    # ① 一个 MIG 实例（完整字段）
    - name: gpu-1-mig-2g20gb-14-0
      attributes:
        addressingMode:
          string: HMM
        architecture:
          string: Ampere
        brand:
          string: Nvidia
        cudaComputeCapability:
          version: 8.0.0
        cudaDriverVersion:
          version: 13.1.0
        driverVersion:
          version: 590.48.1
        parentUUID:                  # 切自哪张物理卡（同卡的 MIG 共享此值）
          string: GPU-f707b56f-f3ae-f293-8278-b76d17e8adc4
        productName:
          string: NVIDIA A100-SXM4-80GB
        profile:
          string: 2g.20gb
        resource.kubernetes.io/pciBusID:
          string: "0000:01:00.0"
        resource.kubernetes.io/pcieRoot:
          string: pci0000:00
        type:
          string: mig
        uuid:
          string: MIG-0dc1dad2-ecb2-587c-8676-5b9e4d8e6397
      capacity:
        copyEngines:
          value: "2"
        decoders:
          value: "1"
        encoders:
          value: "0"
        jpegEngines:
          value: "0"
        memory:
          value: 19968Mi
        memorySlice0:                # 占用物理卡第 0、1 段显存 slice
          value: "1"
        memorySlice1:
          value: "1"
        multiprocessors:
          value: "28"
        ofaEngines:
          value: "0"
    # 同卡切出的其它 MIG（1g.10gb ×2、3g.40gb ×1）结构相同，省略
    # ② 一张未切分的整卡（完整字段）
    - name: gpu-2
      attributes:
        addressingMode:
          string: HMM
        architecture:
          string: Ampere
        brand:
          string: Nvidia
        cudaComputeCapability:
          version: 8.0.0
        cudaDriverVersion:
          version: 13.1.0
        driverVersion:
          version: 590.48.1
        productName:
          string: NVIDIA A100-SXM4-80GB
        resource.kubernetes.io/pciBusID:
          string: "0000:47:00.0"
        resource.kubernetes.io/pcieRoot:
          string: pci0000:40
        type:
          string: gpu
        uuid:
          string: GPU-104b5eee-4b35-e7a4-ab99-bc40a78a264b
      capacity:
        memory:                      # 整卡显存，没有 profile / memorySlice
          value: 80Gi
    # 另外两张整卡 gpu-3、gpu-4 同构，省略
  ```

  **阶段二：分类定义（Admin → DeviceClass）**

  ```text theme={null}
  管理员创建 DeviceClass
      │
      ├─ 1. 确定设备筛选规则（如“所有 NVIDIA GPU”）
      ├─ 2. 编写 CEL 表达式：device.driver == 'gpu.nvidia.com'
      └─ 3. 创建 DeviceClass，供用户引用
  ```

  `DeviceClass` 是管理员对 `ResourceSlice` 中设备的分类标准。driver 把原始设备信息发布到集群后，管理员用 `DeviceClass` 告诉用户「有哪些设备类别可用」：

  ```yaml theme={null}
  apiVersion: resource.k8s.io/v1
  kind: DeviceClass
  metadata:
    name: gpu.nvidia.com
  spec:
    selectors:
    - cel:
        expression: device.driver == 'gpu.nvidia.com'
  ```

  用户不需要理解 CEL，只需知道 `deviceClassName: gpu.nvidia.com` 代表「NVIDIA GPU」——跟 CSI 里选 `StorageClass` 一个道理：管理员写规则，用户选 class。

  **阶段三：声明需求（用户 → ResourceClaim / ResourceClaimTemplate）**

  用户基于 `DeviceClass` 声明要什么设备、要多少。有两种写法：直接建一个 `ResourceClaim`，或用 `ResourceClaimTemplate` 让 K8s 为每个 Pod 自动生成 claim。

  直接建 `ResourceClaim`：

  ```yaml theme={null}
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaim
  metadata:
    name: shared-gpu
    namespace: gpu-demo
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com   # 引用上面的 DeviceClass
          allocationMode: ExactCount
          count: 1
  ```

  用 `ResourceClaimTemplate`（同样的 `devices` 申请，只是被多包了一层 `spec`）：

  ```yaml theme={null}
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaimTemplate
  metadata:
    name: single-gpu
    namespace: gpu-demo
  spec:
    spec:
      devices:
        requests:
        - name: gpu
          exactly:
            deviceClassName: gpu.nvidia.com
            allocationMode: ExactCount
            count: 1
  ```

  `allocationMode` 除 `ExactCount` 外还支持 `All`：不指定数量，直接占用该节点该 class 下的全部设备，适合需要独占整机 GPU 的分布式训练。

  **阶段四：创建 Pod 并触发调度**

  Pod 在 `resourceClaims` 里引用上面声明的资源，容器再通过 `resources.claims` 使用它。引用直接建的 claim 用 `resourceClaimName`，引用模板则用 `resourceClaimTemplateName`：

  ```yaml theme={null}
  apiVersion: v1
  kind: Pod
  metadata:
    name: gpu-test-pod
    namespace: gpu-demo
  spec:
    containers:
    - name: cuda-container
      image: nvidia/cuda:12.1.0-base-ubuntu22.04
      resources:
        claims:
        - name: gpu
    resourceClaims:
    - name: gpu
      resourceClaimName: shared-gpu           # 引用已存在的 ResourceClaim
      # resourceClaimTemplateName: single-gpu # 或改用模板：每个 Pod 各生成独立 claim
  ```

  两种引用方式对应不同场景：

  | 引用方式                            | 效果                                            | 适用场景                                               |
  | ------------------------------- | --------------------------------------------- | -------------------------------------------------- |
  | `resourceClaimName`（直接 claim）   | 多个 Pod 引用同一个 claim，共享同一批设备；claim 生命周期需自己管理    | 跨 Pod 共享同一张卡、多容器协作                                 |
  | `resourceClaimTemplateName`（模板） | 每个 Pod 自动生成独立 claim、各拿各的设备，Pod 删除时 claim 一并回收 | Deployment / StatefulSet 等多副本 workload，每个副本要独立 GPU |

  ```text theme={null}
  Pod 提交到 API Server
      │
      ├─ 1. 用模板时，ResourceClaim Controller 按 Template 生成独立 ResourceClaim
      │        （直接引用已有 claim 则跳过这步）
      │
      ├─ 2. 调度器 Filter：遍历节点，基于 ResourceSlice / DeviceClass / ResourceClaim 判断
      │        - 用 DeviceClass 和 Claim 中的 selector / 容量 / 约束匹配设备
      │        - 为候选节点计算一组可能的具体设备分配结果
      │
      ├─ 3. 调度器 Reserve
      │        - 选定节点 + 选定具体设备
      │        - 在调度器内部缓存 pending allocation
      │
      ├─ 4. 调度器 PreBind
      │        - 写入 ResourceClaim.status.allocation（driver / pool / device）
      │        - 写入 ResourceClaim.status.reservedFor
      │        - 必要时等待 device binding conditions 满足
      │
      ├─ 5. 调度器 Bind
      │        - 绑定 Pod 到节点
      │
      └─ 6. Kubelet 启动 Pod
  ```

  调度器不只选节点，还选定了**具体设备**。`Filter` 阶段为候选节点计算设备分配结果，`Reserve` 阶段在调度器内部缓存 pending allocation，`PreBind` 阶段通过 `bindClaim` 把 `ResourceClaim.status.allocation` 和 `ResourceClaim.status.reservedFor` 写回 API Server；之后 driver 和 kubelet 都以这份结果为准。三个组件分工如下：

  | 组件        | 职责                                                       |
  | --------- | -------------------------------------------------------- |
  | Scheduler | 选节点 + 选具体设备，并在 `PreBind` 写入 `allocation` / `reservedFor` |
  | Driver    | 按 `allocation` 结果在节点上准备设备（`NodePrepareResources`）        |
  | Kubelet   | 调用 driver，并把设备注入容器                                       |

  **阶段五：设备准备与 Pod 启动**

  ```text theme={null}
  Kubelet 收到已绑定的 Pod
      │
      ├─ 1. 找到 Pod 引用的 ResourceClaim（已 allocated + reserved）
      │
      ├─ 2. 调用 DRA driver 的 NodePrepareResources
      │        - 传入 allocation 结果
      │        - driver 准备设备（配置 GPU、生成 CDI 描述文件）
      │        - 返回 CDI 设备 ID
      │
      ├─ 3. Kubelet 把 CDI ID 传给容器运行时
      │
      └─ 4. 运行时按 CDI 描述把设备注入容器
               → Pod 启动，容器内 nvidia-smi 可见 GPU
  ```
</Accordion>

#### 5.3.1 GPU 分配基本示例

最基础的用法是申请整卡：`ResourceClaimTemplate` 或 `ResourceClaim` 通过 `deviceClassName` 引用 `gpu.nvidia.com` 这个 DeviceClass，Pod 再引用该 claim 即可拿到一张 GPU。

```yaml theme={null}
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  namespace: gpu-demo
  name: single-gpu
spec:
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com
```

两者的区别在于：

* `ResourceClaimTemplate` 会为每个 Pod 各生成一个独立 claim、拿到不同 GPU；
* 共享的 `ResourceClaim`（Pod 以 `resourceClaimName` 引用）则可以让多个 Pod 共享同一张卡。

**两 Pod 各独占一卡**：各自的 `ResourceClaimTemplate` 生成独立 claim，分到不同 GPU。

<div style={{ maxWidth: "560px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-two-pods-one-gpu-each.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=db16272a9f7c0101e84e5dd5a34ad3d7" alt="两个 Pod 各含一个容器，Pod 1 的容器连到 GPU 0、Pod 2 的容器连到 GPU 1，彼此不共享" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-dra-two-pods-one-gpu-each.png" />
  </Frame>
</div>

<Accordion title="完整 YAML：quickstart/two-pods-one-gpu-each.yaml">
  ```yaml theme={null}
  apiVersion: v1
  kind: Namespace
  metadata:
    name: gpu-test1
  ---
  # 模板：每个引用它的 Pod 都会自动生成一份独立的 ResourceClaim
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaimTemplate
  metadata:
    namespace: gpu-test1
    name: single-gpu
  spec:
    spec:                                     # 模板比 ResourceClaim 多包一层 spec
      devices:
        requests:
        - name: gpu
          exactly:
            deviceClassName: gpu.nvidia.com   # 要一张这个 class 的设备
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: gpu-test1
    name: pod1
    labels:
      app: pod
  spec:
    containers:
    - name: container
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: gpu                    # 容器使用下面 resourceClaims 里名为 gpu 的申请
    resourceClaims:
    - name: gpu
      resourceClaimTemplateName: single-gpu   # 用模板 → 本 Pod 独得一个 claim（一张卡）
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ---
  # pod2 与 pod1 相同：同样引用模板，于是各自生成独立 claim → 分到不同 GPU
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: gpu-test1
    name: pod2
    labels:
      app: pod
  spec:
    containers:
    - name: container
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: gpu
    resourceClaims:
    - name: gpu
      resourceClaimTemplateName: single-gpu
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ```
</Accordion>

**一 Pod 两容器共享一卡**：同一个 Pod 里的两个容器引用同一个 claim，映射到同一张 GPU。

<div style={{ maxWidth: "560px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-two-containers-share-gpu.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=002669aaa729ff7e38d92dac71d9287b" alt="一个 Pod 内的 container0 和 container1 两个容器通过箭头汇聚到同一张 GPU 0" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-dra-two-containers-share-gpu.png" />
  </Frame>
</div>

<Accordion title="完整 YAML：quickstart/two-containers-share-one-gpu.yaml">
  ```yaml theme={null}
  apiVersion: v1
  kind: Namespace
  metadata:
    name: gpu-test2
  ---
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaimTemplate
  metadata:
    namespace: gpu-test2
    name: single-gpu
  spec:
    spec:
      devices:
        requests:
        - name: gpu
          exactly:
            deviceClassName: gpu.nvidia.com
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: gpu-test2
    name: pod
    labels:
      app: pod
  spec:
    containers:
    - name: container0
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: shared-gpu           # 两个容器引用同一个 claim 名
    - name: container1
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: shared-gpu           # → 落到同一张 GPU
    resourceClaims:
    - name: shared-gpu               # 本 Pod 只生成一个 claim，被两个容器共用
      resourceClaimTemplateName: single-gpu
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ```
</Accordion>

**两 Pod 共享一卡**：两个 Pod 引用同一个全局 `ResourceClaim`（`resourceClaimName`），落到同一张 GPU。

<div style={{ maxWidth: "560px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-two-pods-share-gpu.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=a4ab9ae73a8045f49e177dc4363a85f6" alt="两个 Pod 各含一个容器，箭头汇聚到同一张 GPU 0，表示两个 Pod 共享一张卡" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-dra-two-pods-share-gpu.png" />
  </Frame>
</div>

<Accordion title="完整 YAML：quickstart/two-pods-share-one-gpu.yaml">
  ```yaml theme={null}
  apiVersion: v1
  kind: Namespace
  metadata:
    name: gpu-test3
  ---
  # 关键区别：这是一个独立的 ResourceClaim 对象（不是 Template），可被多个 Pod 共享
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaim
  metadata:
    namespace: gpu-test3
    name: single-gpu
  spec:
    devices:                          # 直接就是 spec.devices，没有模板那层嵌套
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: gpu-test3
    name: pod1
    labels:
      app: pod
  spec:
    containers:
    - name: container
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: shared-gpu
    resourceClaims:
    - name: shared-gpu
      resourceClaimName: single-gpu       # 按名字引用已存在的 claim（不是 Template）
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ---
  # pod2 引用同一个 claim（single-gpu）→ 和 pod1 落到同一张 GPU
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: gpu-test3
    name: pod2
    labels:
      app: pod
  spec:
    containers:
    - name: container
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: shared-gpu
    resourceClaims:
    - name: shared-gpu
      resourceClaimName: single-gpu
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ```
</Accordion>

#### 5.3.2 TimeSlicing 与 MPS

DRA 把共享策略直接写进每个 claim 的 `GpuConfig`，因此能**按工作负载单独配置**；而 device plugin 的 TimeSlicing / MPS 由整节点共用一份 ConfigMap 决定，同一节点上所有 Pod 只能套用同一套。DRA 的粒度还能更细——同一个 `ResourceClaimTemplate` 里就能给不同 request 配不同策略：一个 request 走 TimeSlicing、另一个走 MPS，各自落到不同的 GPU。下面这个示例用一个 Pod、4 个容器演示：`ts-ctr0/ts-ctr1` 共享一张走 TimeSlicing 的 GPU，`mps-ctr0/mps-ctr1` 共享另一张走 MPS 的 GPU。

<div style={{ maxWidth: "620px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-timeslicing-mps.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=c5f541d4e990802513fc68934a3ddc62" alt="一个 Pod 内 4 个容器：ts-ctr0/ts-ctr1 汇聚到 GPU A（TimeSlicing），mps-ctr0/mps-ctr1 汇聚到 GPU B（MPS）" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-dra-timeslicing-mps.png" />
  </Frame>
</div>

<Accordion title="完整 YAML：mps-timeslicing/timeslicing-and-mps-sharing.yaml">
  ```yaml theme={null}
  apiVersion: v1
  kind: Namespace
  metadata:
    name: gpu-test5
  ---
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaimTemplate
  metadata:
    namespace: gpu-test5
    name: multiple-gpus
  spec:
    spec:
      devices:
        requests:
        - name: ts-gpu                    # 第一张卡：走 TimeSlicing
          exactly:
            deviceClassName: gpu.nvidia.com
        - name: mps-gpu                   # 第二张卡：走 MPS
          exactly:
            deviceClassName: gpu.nvidia.com
        config:                           # 按 request 分别配置共享策略
        - requests: ["ts-gpu"]
          opaque:
            driver: gpu.nvidia.com
            parameters:
              apiVersion: resource.nvidia.com/v1beta1
              kind: GpuConfig
              sharing:
                strategy: TimeSlicing     # 时间片轮流，无显存/故障隔离
                timeSlicingConfig:
                  interval: Long
        - requests: ["mps-gpu"]
          opaque:
            driver: gpu.nvidia.com
            parameters:
              apiVersion: resource.nvidia.com/v1beta1
              kind: GpuConfig
              sharing:
                strategy: MPS             # 多进程并发，可限线程/显存
                mpsConfig:
                  defaultActiveThreadPercentage: 50   # 每个进程最多用 50% SM
                  defaultPinnedDeviceMemoryLimit: 10Gi
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: gpu-test5
    name: pod0
  spec:
    containers:
    - name: ts-ctr0                       # 与 ts-ctr1 共享 GPU A（TimeSlicing）
      image: nvcr.io/nvidia/k8s/cuda-sample:nbody-cuda11.6.0-ubuntu18.04
      command: ["bash", "-c"]
      args: ["trap 'exit 0' TERM; /tmp/sample --benchmark --numbodies=4226000 & wait"]
      resources:
        claims:
        - name: shared-gpus
          request: ts-gpu               # 选择走 TimeSlicing 的那张卡
    - name: ts-ctr1
      image: nvcr.io/nvidia/k8s/cuda-sample:nbody-cuda11.6.0-ubuntu18.04
      command: ["bash", "-c"]
      args: ["trap 'exit 0' TERM; /tmp/sample --benchmark --numbodies=4226000 & wait"]
      resources:
        claims:
        - name: shared-gpus
          request: ts-gpu
    - name: mps-ctr0                      # 与 mps-ctr1 共享 GPU B（MPS）
      image: nvcr.io/nvidia/k8s/cuda-sample:nbody-cuda11.6.0-ubuntu18.04
      command: ["bash", "-c"]
      args: ["trap 'exit 0' TERM; /tmp/sample --benchmark --numbodies=4226000 & wait"]
      resources:
        claims:
        - name: shared-gpus
          request: mps-gpu              # 选择走 MPS 的那张卡
    - name: mps-ctr1
      image: nvcr.io/nvidia/k8s/cuda-sample:nbody-cuda11.6.0-ubuntu18.04
      command: ["bash", "-c"]
      args: ["trap 'exit 0' TERM; /tmp/sample --benchmark --numbodies=4226000 & wait"]
      resources:
        claims:
        - name: shared-gpus
          request: mps-gpu
    resourceClaims:
    - name: shared-gpus
      resourceClaimTemplateName: multiple-gpus
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ```
</Accordion>

GpuConfig 字段含义见官方 [GpuConfig API 参考](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/blob/main/site/content/docs/reference/api.md#gpuconfig)。

**启用 feature gate**：这两种策略分别需要 `TimeSlicingSettings`、`MPSSupport`，未开启时 kubelet 会在 `NodePrepareResources` 阶段拒绝。安装 DRA driver 时用 Helm 打开：

```bash theme={null}
helm upgrade -i dra-driver-nvidia-gpu \
  oci://registry.k8s.io/dra-driver-nvidia/charts/dra-driver-nvidia-gpu --version 0.4.0 \
  --create-namespace --namespace dra-driver-nvidia-gpu \
  --set gpuResourcesEnabledOverride=true \
  --set featureGates.TimeSlicingSettings=true \
  --set featureGates.MPSSupport=true
```

`TimeSlicingSettings` 不与任何特性冲突，可与 `MPSSupport` 同时开启；但 `MPSSupport` 与 `DynamicMIG` **互斥**，两者不能同开（想跑动态 MIG 时需去掉 `MPSSupport`）。完整的互斥组合见官方 [feature gate constraints](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/blob/main/site/content/docs/reference/feature-gates.md#constraints)。

#### 5.3.3 静态 MIG

静态 MIG 需要先在节点上把 GPU 提前做好 MIG 切分（比如用 [`nvidia-mig-parted`](https://github.com/NVIDIA/mig-parted)），之后 `ResourceClaim` / `ResourceClaimTemplate` 只能申请这些已经切好的 MIG 规格。

<div style={{ maxWidth: "620px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-static-mig.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=2e5f2544919ecae839215113bc652e0d" alt="静态 MIG：先用 nvidia-mig-parted 把 GPU 0 预切成 1g.10gb×2 + 2g.20gb + 3g.40gb，Pod 的 claim 再从中分到一个 1g.10gb MIG" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-dra-static-mig.png" />
  </Frame>
</div>

**第一步：预切 MIG。** 用一份 `nvidia-mig-parted` 布局文件描述每张卡切成什么。下面把 GPU 0 切成 `1g.10gb×2 + 2g.20gb + 3g.40gb`（compute slice 合计 7g，刚好切满 A100 80GB）：

```yaml theme={null}
# mig-parted-config.yaml
version: v1
mig-configs:
  balanced:
  - devices: [0]
    mig-enabled: true
    mig-devices:
      "1g.10gb": 2
      "2g.20gb": 1
      "3g.40gb": 1
```

在节点上开启 MIG 模式并切分 MIG：

```bash theme={null}
sudo nvidia-smi -i 0 -mig 1                                    # 开启 GPU 0 的 MIG 模式
sudo -E nvidia-mig-parted apply -f mig-parted-config.yaml -c balanced
nvidia-smi -L                                                 # 应列出切好的 MIG 设备
```

**第二步：申请 MIG 设备。** claim 引用 `mig.nvidia.com` DeviceClass，用 CEL selector 按 profile 选中已经切好的 MIG。完整清单见下：

<Accordion title="完整 YAML：static-mig-a100/mig-static.yaml">
  ```yaml theme={null}
  apiVersion: v1
  kind: Namespace
  metadata:
    name: static-mig-test
  ---
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaimTemplate
  metadata:
    namespace: static-mig-test
    name: mig-1g
  spec:
    spec:
      devices:
        requests:
        - name: mig
          exactly:
            deviceClassName: mig.nvidia.com          # MIG 用 mig.nvidia.com，不是 gpu.nvidia.com
            selectors:
            - cel:
                expression: "device.attributes['gpu.nvidia.com'].profile == '1g.10gb'"
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: static-mig-test
    name: mig-pod
  spec:
    containers:
    - name: ctr
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        - name: mig
    resourceClaims:
    - name: mig
      resourceClaimTemplateName: mig-1g
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ```
</Accordion>

#### 5.3.4 动态 MIG

H100 及更新架构可按 claim **现切** MIG，无需提前切分：开启 `DynamicMIG` feature gate 后，DRA driver 会直接依据 `ResourceClaim` 在卡上动态创建对应的 MIG 设备。

<div style={{ maxWidth: "720px", margin: "0 auto" }}>
  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-dynamic-mig.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=383e20b5c08e5db4581c4ec9a398d46b" alt="动态 MIG：Pod 的 claim 声明 1g.10gb + 2g.20gb + 4g.40gb，DRA driver 按需在同一张 H100 上现切出这三个 MIG" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-dra-dynamic-mig.png" />
  </Frame>
</div>

安装 DRA driver 时需打开 `DynamicMIG` feature gate（另外 Kubernetes 1.33–1.35 还需在 kube-apiserver、kube-scheduler 手动开启 [`DRAPartitionableDevices`](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/blob/main/site/content/docs/reference/feature-gates.md)，1.36 起该 feature gate 默认开启）：

```bash theme={null}
helm upgrade -i dra-driver-nvidia-gpu \
  oci://registry.k8s.io/dra-driver-nvidia/charts/dra-driver-nvidia-gpu --version 0.4.0 \
  --create-namespace --namespace dra-driver-nvidia-gpu \
  --set gpuResourcesEnabledOverride=true \
  --set featureGates.DynamicMIG=true
```

下面看怎么用动态 MIG 为 Pod 申请资源。对用户来说，写法和静态 MIG **完全一样**——同样是在 `ResourceClaim` 里用 CEL selector 按 profile 选 MIG；区别只在于**不用提前切分**，driver 会根据申请**按需现切**出对应的 MIG。

一个 claim 里可以声明多个 request，一次拿到多块 MIG。`constraints.matchAttribute: gpu.nvidia.com/parentUUID` 是可选的：加上它会把这些 MIG 约束在**同一张物理卡**上；不加则各 request 独立匹配，可能分散到多张 GPU。

<Accordion title="完整 YAML：dynamic-mig-h100/mig-multi-profile.yaml">
  ```yaml theme={null}
  apiVersion: v1
  kind: Namespace
  metadata:
    name: dynamic-mig-test
  ---
  apiVersion: resource.k8s.io/v1
  kind: ResourceClaimTemplate
  metadata:
    namespace: dynamic-mig-test
    name: mig-4-2-1
  spec:
    spec:
      devices:
        requests:
        - name: mig-1g
          exactly:
            deviceClassName: mig.nvidia.com
            selectors:
            - cel:
                expression: "device.attributes['gpu.nvidia.com'].profile == '1g.10gb'"
        - name: mig-2g
          exactly:
            deviceClassName: mig.nvidia.com
            selectors:
            - cel:
                expression: "device.attributes['gpu.nvidia.com'].profile == '2g.20gb'"
        - name: mig-4g
          exactly:
            deviceClassName: mig.nvidia.com
            selectors:
            - cel:
                expression: "device.attributes['gpu.nvidia.com'].profile == '4g.40gb'"
        constraints:
        - matchAttribute: "gpu.nvidia.com/parentUUID"   # 三个 MIG 必须来自同一张物理卡
  ---
  apiVersion: v1
  kind: Pod
  metadata:
    namespace: dynamic-mig-test
    name: mig-pod
  spec:
    containers:
    - name: ctr
      image: ubuntu:22.04
      command: ["bash", "-c"]
      args: ["nvidia-smi -L; trap 'exit 0' TERM; sleep 9999 & wait"]
      resources:
        claims:
        # 不带 request：注入 claim 里全部 3 个 MIG 设备；
        # 只想要其中一个时改成：{ name: mig, request: mig-1g }
        - name: mig
    resourceClaims:
    - name: mig
      resourceClaimTemplateName: mig-4-2-1
    tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  ```
</Accordion>

动态 MIG 靠 DRA 的 partitionable devices 机制（[KEP-4815](https://github.com/kubernetes/enhancements/tree/master/keps/sig-scheduling/4815-dra-partitionable-devices)）实现：

1. DRA driver 把一张 GPU 的有限资源（显存切片、SM 等）声明成一个 `CounterSet`（放在 `ResourceSlice` 的 `sharedCounters` 字段里，通常一张物理卡一个），记录各项资源的总量；
2. 再把这张卡上所有可能的 MIG 切分组合都作为设备列进 `ResourceSlice`，每个设备用 `consumesCounters` 声明要从对应 `CounterSet` 消耗多少；
3. 调度时，调度器按「池总量 − 已分配 `ResourceClaim` 的消耗」核对每个 `CounterSet` 的余量，某项 counter 不够时对应设备就无法被分配。

下面借 KubeCon 2025 的演讲 [Partitionable Devices: Putting the “Dynamic” Back in Dynamic Resource Allocation](https://kccncna2025.sched.com/event/b742642a3d9d914e51ef001b72ed596d)（Morten Jæger Torkildsen, Google & Jan-Philip Gehrcke, NVIDIA）的三张图看这套机制的实际过程。

图里是两个 `ResourceSlice`（同属一个资源池，按规范拆成两份）：右边的 `counterset-slice` 用 `sharedCounters` 声明每个 CounterSet 的总量；左边的 `devices-slice` 把所有可能的（互相重叠的）设备列出来，各用 `consumesCounters` 声明要从某个 CounterSet 扣掉多少。调度器为每个 CounterSet 跟踪可用 counter，只有余量足够时设备才可分配。

<Frame>
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-counter-1-tracking.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=e393087e333479822fdcba432d69fa55" alt="KubeCon 幻灯片：devices-slice 里的设备用 consumesCounters 引用 counterset-slice 里的 sharedCounters，调度器为每个 CounterSet 跟踪可用 counter" width="1024" height="579" data-path="books/ai-systems-performance-engineering/images/ch03-dra-counter-1-tracking.png" />
</Frame>

分配 `small-device-1` 时，从 `counterset-1` 扣掉 20Gi 显存 + 2 CPU，该 CounterSet 的可用 counter 相应减少。

<Frame>
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-counter-2-allocate.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=da647b9b27a512835ce94099036901e2" alt="KubeCon 幻灯片：分配 small-device-1 后从 counterset-1 消耗 20Gi 显存和 2 CPU，可用 counter 减少" width="1024" height="576" data-path="books/ai-systems-performance-engineering/images/ch03-dra-counter-2-allocate.png" />
</Frame>

扣减后 `large-device` 需要的 counter 已不够、无法再分配。注意 `ResourceSlice` 本身不会被改写——调度器是通过已分配的 `ResourceClaim` 反推每个 CounterSet 还剩多少。

<Frame>
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-dra-counter-3-unavailable.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=4cd23d83982f2f5e6b789686004cf484" alt="KubeCon 幻灯片：counter 被 small-device-1 占用后，large-device 因余量不足变为不可分配；ResourceSlice 不被改写，调度器靠已分配 ResourceClaim 反推余量" width="1024" height="575" data-path="books/ai-systems-performance-engineering/images/ch03-dra-counter-3-unavailable.png" />
</Frame>

#### 5.3.5 ComputeDomain：多节点 NVLink

早期 NVIDIA DGX 的做法是把尽可能多的 GPU 塞进**一台服务器**、用高带宽 NVLink 连起来：单机内扩展很强，但作业规模被限制在一台机器内。GB300 NVL72 和 GB200 NVL72 正是为打破这一限制而生：每一台都提供由 NVLink Switch 连接的密集 GPU fabric，在机架内支持 **NVIDIA Multi-Node NVLink（MNNVL）**，并包含具备 IMEX 能力的 compute tray，实现跨节点的 GPU 显存共享——整个机架因此变成一块**统一的 GPU fabric**，成为超大规模分布式训练与推理的基础。

<Frame caption="DGX GB200 系统——顶部 10 个、底部 8 个 compute tray，中间通过 9 个 NVLink Switch 相连，把 72 张 GPU 经 Multi-Node NVLink 连成一个全互连 mesh（chip-to-chip 1.8 TB/s，累计带宽超过 130 TB/s）">
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-gb200-nvl72-rack.webp?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=f14a2889b8bd580acbf1f2c6aefb02c3" alt="GB200 NVL72 机架构成：18 个 compute tray（每 tray 4 GPU）加 9 个 NVLink Switch tray（每 tray 2 个 72 端口 NVSwitch），GPU 间共 1296 条 NVLink 连接" width="1354" height="866" data-path="books/ai-systems-performance-engineering/images/ch03-gb200-nvl72-rack.webp" />
</Frame>

作为 [NVIDIA DRA driver for GPUs](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu) 的一部分，ComputeDomain 把底层的 GPU 构造（[NVIDIA NVLink](https://www.nvidia.com/en-us/data-center/nvlink/) 与 [NVIDIA IMEX](https://docs.nvidia.com/multi-node-nvlink-systems/imex-guide/overview.html)）与现代 Kubernetes 原生的调度概念 DRA 桥接起来，为在现代 GPU 硬件上运行分布式、多节点工作负载提供必要的基础支撑。

那么，在 Kubernetes 上支持多节点 NVLink 需要哪些东西、ComputeDomain 又如何帮上忙？关键是 **NVIDIA Internode Memory Exchange Service（IMEX）**——运行在 GPU driver 层的软件，让 GPU 能够跨节点通信。有了 IMEX，每一次 GPU 显存的导出/导入操作都会受到细粒度的访问控制。IMEX 作用在一组节点上，这组节点被称为 **IMEX domain**。

<Accordion title="什么是 IMEX">
  IMEX（Internode Memory Exchange Service）提供的能力是：让**一组 GPU 通过高带宽 NVLink 直接读写彼此的显存**。这种连接既可以是**同一节点内** GPU 之间的直连，也可以是**不同节点**间经 NVSwitch 连接的 GPU。

  要让不同机器上的 GPU 互相通信，它们必须共享一个叫 **IMEX channel** 的构造——可以理解成一个**跨节点资源**，需要被多节点作业的每个 worker 共享。

  IMEX 共享的对象是 **fabric-attached memory（fabric 显存）**——被映射进 NVLink fabric 地址空间的 GPU 显存：除了本地的「虚拟地址 → 物理地址」映射，它还多了一个 **fabric 地址（FA）**，因而同一 NVLink fabric 上的其它 GPU 都能经 NVLink 直接寻址、读写它（这正是 MNNVL「把整机架当作一块可共享的大显存」的底层）。

  一个 CUDA 进程要读写这样的 fabric 显存，需要同时满足：

  * 它与**创建原始 handle 的进程处在同一个 IMEX domain**；
  * 它能访问**创建原始 handle 的进程所用的同一个 IMEX channel**；
  * 它持有对该 `sharedHandle` 的引用（`sharedHandle` 的 export / import 具体方式见 [Supporting GB200 on Kubernetes](https://docs.google.com/presentation/d/1-qc_6W2w4D3zXu9LA6CSmzBdOwscRpEJydxu9BD89WI/edit?slide=id.g30d67777a5f_1_14#slide=id.g30d67777a5f_1_14)）。

  <Frame>
    <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-imex-shared-handle.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=19aff1f9ac598bf4532670016ee4fa5d" alt="IMEX 显存共享的 handle 模型：一端把本地 handle 导出为 sharedHandle，另一端导入后重建出 handle，两端的 ptr 指向同一块 fabric 显存" width="1024" height="315" data-path="books/ai-systems-performance-engineering/images/ch03-imex-shared-handle.png" />
  </Frame>

  上图：一端的 CUDA 进程分配显存、拿到本地 `handle`（指向 `ptr`），把它导出成一个 `sharedHandle`；另一端的进程凭这个 `sharedHandle` 导入、在本地重建出 `handle`（同样指向那块 fabric 显存的 `ptr`）。于是两端进程虽然在不同节点、各有独立的地址空间，却通过 NVLink 读写到**同一块物理显存**——这就是跨节点显存共享的本质。
</Accordion>

参考下图，可以更好地理解在多节点 NVLink 环境中，NVLink domain、IMEX domain 以及其它可能的 GPU 分区层级之间的关系：

<Frame>
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-mnnvl-concepts.webp?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=fee7fec595a55258f8da8e34d473182e" alt="NVLink Domain / NVLink Partition / IMEX Domain / IMEX Channel 的嵌套关系：Domain 是物理互连的全部 GPU，Partition 在 NVSwitch 层做硬件隔离，IMEX Domain 是运行 IMEX daemon 的节点组，IMEX Channel 是跨节点通信必须共享的软件通道" width="1999" height="989" data-path="books/ai-systems-performance-engineering/images/ch03-mnnvl-concepts.webp" />
</Frame>

图源：[Enabling Multi-Node NVLink on Kubernetes for NVIDIA GB200 NVL72 and Beyond](https://developer.nvidia.com/blog/enabling-multi-node-nvlink-on-kubernetes-for-gb200-and-beyond/)。

* **NVLink Domain（NVLink 域）**：通过 NVLink 物理互连的一组 GPU；GB200 上一个 rack（NVL72）就是一个 NVLink Domain（72 GPU），有相同的 **Cluster UUID**。
* **NVLink Partition（NVLink 分区）**：由 NVSwitch 硬件隔离出的分区，分区之间互不相通、用于多租户隔离；GPU 的 **Clique ID** 就对应它所属的分区（在 K8s 里，GFD 据此给节点打上 `nvidia.com/gpu.clique` 标签）。
* **IMEX Domain**：某个 NVLink Partition 内运行 `nvidia-imex` 的一组节点，必须共享同一 `<ClusterUUID, CliqueID>`；一个 NVLink Partition 内可以有多个 IMEX Domain。
* **IMEX Channel**：IMEX Domain 内的一条软件通道，跨节点作业的各个进程拿到同一个 channel 才能互访显存；不同作业用不同 channel，互相隔离。

| 概念               | 类型 | 由谁配置                                        | 描述                                                                                               |
| ---------------- | -- | ------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| NVLink Domain    | 硬件 | 物理拓扑决定                                      | 通过 NVLink 物理互连的一组 GPU（如一个 NVL72 机架）                                                              |
| NVLink Partition | 硬件 | NVSwitch Control Plane（NMX）                 | 在 NVSwitch 层把 NVLink Domain 隔成互不相通的分区，做多租户隔离                                                     |
| IMEX Domain      | 软件 | ComputeDomain / DRA driver（起 `nvidia-imex`） | 某个 Partition 内实际运行 `nvidia-imex` 的一组节点                                                           |
| IMEX Channel     | 软件 | ComputeDomain（按作业分配）                        | Domain 内做访问隔离的通道，同 channel 的进程才能互访显存；在 ComputeDomain 里，一个 domain 默认只有一个 channel，由 domain 的全部节点共用 |

下面看看 ComputeDomain 的工作原理，过程如下：

* 创建 `ComputeDomain`（图中 `compute-domain-0`）后，`compute-domain-controller` 会配套建出一个 DaemonSet 和一个 `ResourceClaimTemplate`，此时还没真正组网：
  * 一个 per-domain 的 DaemonSet，用来在节点上运行 `nvidia-imex`；它的 `nodeSelector` 只认带有 ComputeDomain UID 标签的节点——标签形如 `resource.nvidia.com/computeDomain: <ComputeDomain 的 UID>`。
  * 一个供 workload 申请 channel 的 `ResourceClaimTemplate`（`compute-domain-0-rct`，背后是 `compute-domain-default-channel.nvidia.com` 这个 DeviceClass）。
  * 此刻还没有节点带这个标签，所以没有 daemon Pod 在跑，也还没有 IMEX domain。
* workload Pod 引用该 `ResourceClaimTemplate` 并被调度到节点后，daemon 才被拉起来：
  * `compute-domain-kubelet-plugin` 给所在节点打上上述标签，DaemonSet 随即在这批节点上拉起 `compute-domain-daemon`。
  * 每个 daemon 运行 `nvidia-imex`（配置目录 `/imexd` 由 kubelet-plugin 挂载进 daemon 容器），管理本节点的 NVLink fabric 连接。
  * daemon 通过 driver namespace 里的 `ComputeDomainClique` CR 公布自己的 IP、clique 归属和就绪状态。
* daemon 起来后互相组网，channel 也随之注入容器：
  * 这些 daemon 组成**一个** IMEX domain，也就是这批节点能互访显存的范围。
  * workload Pod 申请到 channel 后，`compute-domain-kubelet-plugin` 向其容器注入对应的 IMEX channel 设备（`/dev/nvidia-caps-imex-channels/channel0`）。
  * 同一 ComputeDomain 下所有 Pod 拿到**同一个** channel（图中 `channel 0`），凭它跨节点读写彼此的 fabric 显存——真正能互访的，就是拿到同一 channel 的这批 Pod。

<Frame caption="一个 ComputeDomain 对应一个 IMEX domain：controller 先建出 ResourceClaimTemplate 和 per-domain DaemonSet，workload Pod 落到节点后由 nvidia-imex 组成一个 IMEX domain，并给这批 Pod 注入同一个 channel，跨节点经 NVLink/NVSwitch fabric 共享显存">
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-computedomain-imex.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=2a425184f0c6821b8b883a14fe042b94" alt="ComputeDomain 到 IMEX domain 的映射：左侧 Kubernetes 控制面里 ComputeDomain 创建 ResourceClaimTemplate 与 per-domain DaemonSet；右侧同一 NVLink Partition（同一 Clique ID）内的三个节点各跑一个 workload Pod、一个 compute-domain-daemon（nvidia-imex）和若干 GPU，daemon 组成一个 IMEX Domain，三个 Pod 拿到同一个 channel 0，底部经 NVLink/NVSwitch fabric 共享显存" width="1536" height="1024" data-path="books/ai-systems-performance-engineering/images/ch03-computedomain-imex.png" />
</Frame>

<Accordion title="示例：两节点 nvbandwidth 测试（验证跨节点 NVLink）">
  下面用一个真实的**两节点 `nvbandwidth` 测试**走一遍：每节点用 4 张 GPU，跨两个节点验证 GPU 之间能否以全 NVLink 带宽互访。MPI 作业的编排交给 MPI Operator（`MPIJob`），ComputeDomain 负责把这两个节点的 IMEX 打通。

  先装 MPI Operator：

  ```bash theme={null}
  kubectl create -f https://github.com/kubeflow/mpi-operator/releases/download/v0.6.0/mpi-operator.yaml
  ```

  同一份 spec 里声明两样东西：一个 `numNodes: 2` 的 `ComputeDomain`，和一个引用它 channel 的 `MPIJob`（1 个 launcher + 2 个 worker，每 worker 4 GPU）：

  ```yaml theme={null}
  ---
  apiVersion: resource.nvidia.com/v1beta1
  kind: ComputeDomain
  metadata:
    name: nvbandwidth-test-compute-domain
  spec:
    numNodes: 0                                   # 该字段已废弃，设为 0 即可（daemon 随 worker 落点自动组网）
    channel:
      resourceClaimTemplate:
        name: nvbandwidth-test-compute-domain-channel
  ---
  apiVersion: kubeflow.org/v2beta1
  kind: MPIJob
  metadata:
    name: nvbandwidth-test
  spec:
    slotsPerWorker: 4                             # 每个 worker 4 个 slot = 4 张 GPU
    launcherCreationPolicy: WaitForWorkersReady   # 等 worker 就绪再起 launcher
    runPolicy:
      cleanPodPolicy: Running
    sshAuthMountPath: /home/mpiuser/.ssh
    mpiReplicaSpecs:
      Launcher:
        replicas: 1
        template:
          metadata:
            labels:
              nvbandwidth-test-replica: mpi-launcher
          spec:
            affinity:
              nodeAffinity:                        # launcher 放到 control-plane 节点
                requiredDuringSchedulingIgnoredDuringExecution:
                  nodeSelectorTerms:
                  - matchExpressions:
                    - key: node-role.kubernetes.io/control-plane
                      operator: Exists
            containers:
            - image: ghcr.io/nvidia/k8s-samples:nvbandwidth-v0.7-8d103163
              name: mpi-launcher
              securityContext:
                runAsUser: 1000
              command:
              - mpirun
              args:                                # 8 rank、每节点 4 个，跑跨节点 D2D 读带宽
              - --bind-to
              - core
              - --map-by
              - ppr:4:node
              - -np
              - "8"
              - --report-bindings
              - -q
              - nvbandwidth
              - -t
              - multinode_device_to_device_memcpy_read_ce
      Worker:
        replicas: 2                                # 2 个 worker，各占一个节点
        template:
          metadata:
            labels:
              nvbandwidth-test-replica: mpi-worker
          spec:
            affinity:
              podAffinity:                         # 2 个 worker 落到同一 NVLink partition（同一 clique）
                requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector:
                    matchExpressions:
                    - key: nvbandwidth-test-replica
                      operator: In
                      values:
                      - mpi-worker
                  topologyKey: nvidia.com/gpu.clique
            containers:
            - image: ghcr.io/nvidia/k8s-samples:nvbandwidth-v0.7-8d103163
              name: mpi-worker
              securityContext:
                runAsUser: 1000
              command:                             # worker 只跑 sshd，等 launcher 经 MPI 拉起 rank
              - /usr/sbin/sshd
              args:
              - -De
              - -f
              - /home/mpiuser/.sshd_config
              resources:
                limits:
                  nvidia.com/gpu: 4                # 每 worker 4 张 GPU
                claims:
                - name: compute-domain-channel
            resourceClaims:
            - name: compute-domain-channel         # 加入上面的 ComputeDomain，拿到 IMEX channel
              resourceClaimTemplateName: nvbandwidth-test-compute-domain-channel
  ```

  应用上面的配置之后能看到三组 Pod：`MPIJob` 的 1 个 launcher + 2 个 worker，以及 ComputeDomain 按 worker 落点节点拉起的 2 个 daemon：

  ```bash theme={null}
  $ kubectl get pods
  NAME                              READY   STATUS      RESTARTS   AGE
  nvbandwidth-test-launcher-lzv84   1/1     Running     0          3s
  nvbandwidth-test-worker-0         1/1     Running     0          15s
  nvbandwidth-test-worker-1         1/1     Running     0          15s

  # ComputeDomain 的 per-domain daemon（每个 worker 节点一个）
  $ kubectl get pods -n nvidia-dra-driver-gpu -l resource.nvidia.com/computeDomain
  NAME                                          READY   STATUS    RESTARTS   AGE
  nvbandwidth-test-compute-domain-ht24d-9jhmj   1/1     Running   0          20s
  nvbandwidth-test-compute-domain-ht24d-rcn2c   1/1     Running   0          20s
  ```

  launcher 日志里，8 个 rank 分布在两个 worker 上（每节点 4 张 GB200），核心是那张带宽矩阵：

  ```text theme={null}
  Process 0 (nvbandwidth-test-worker-0): device 0: HGX GB200 ...
  Process 1 (nvbandwidth-test-worker-0): device 1: HGX GB200 ...
  Process 2 (nvbandwidth-test-worker-0): device 2: HGX GB200 ...
  Process 3 (nvbandwidth-test-worker-0): device 3: HGX GB200 ...
  Process 4 (nvbandwidth-test-worker-1): device 0: HGX GB200 ...
  Process 5 (nvbandwidth-test-worker-1): device 1: HGX GB200 ...
  Process 6 (nvbandwidth-test-worker-1): device 2: HGX GB200 ...
  Process 7 (nvbandwidth-test-worker-1): device 3: HGX GB200 ...

  Running multinode_device_to_device_memcpy_read_ce.
  memcpy CE GPU(row) -> GPU(column) bandwidth (GB/s)
             0         1         2         3         4         5         6         7
   0       N/A    798.02    798.25    798.02    798.02    797.88    797.73    797.95
   1    798.10       N/A    797.80    798.02    798.02    798.25    797.88    798.02
   2    797.95    797.95       N/A    797.73    797.80    797.95    797.95    797.65
   3    798.10    798.02    797.95       N/A    798.02    798.10    797.88    797.73
   4    797.80    798.02    798.02    798.02       N/A    797.95    797.80    798.02
   5    797.80    797.95    798.10    798.10    797.95       N/A    797.95    797.88
   6    797.73    797.95    798.10    798.02    797.95    797.88       N/A    797.80
   7    797.88    798.02    797.95    798.02    797.88    797.95    798.02       N/A

  SUM multinode_device_to_device_memcpy_read_ce 44685.29
  ```

  rank 0–3 在 worker-0、rank 4–7 在 worker-1。矩阵里**跨节点**的格子（例如 0↔4、3↔7）和**同节点**的格子一样都是 \~798 GB/s——分处两个节点的 GPU 之间达到了全 NVLink 带宽。这正是 ComputeDomain 的作用所在：它自动把两个节点的 IMEX 打通，让这 8 张 GPU 像在同一条 NVLink fabric 上一样互访显存。
</Accordion>

**参考资料**

* [ComputeDomain workloads](https://dra-driver-nvidia-gpu.sigs.k8s.io/docs/guides/compute-domain-workloads/)：官方 workload 示例，覆盖创建 `ComputeDomain`、申请 channel、查看状态和 feature gates。
* [Validate setup for ComputeDomain allocation](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/wiki/Validate-setup-for-ComputeDomain-allocation)：ComputeDomain 环境验证 checklist，包含 IMEX channel 注入测试和 `nvbandwidth` 多节点测试。
* [NVIDIA DRA Driver for GPUs - GPU Operator docs](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/dra-cds.html)：解释 ComputeDomain、IMEX 安全边界和 GPU Operator 集成路径。
* [Enabling Multi-Node NVLink on Kubernetes for NVIDIA GB200 NVL72 and Beyond](https://developer.nvidia.com/blog/enabling-multi-node-nvlink-on-kubernetes-for-gb200-and-beyond/)：NVIDIA 官方博客，解释 ComputeDomain 与 GB200 NVL72 / MNNVL 的关系。
* [Running AI Workloads on Rack-Scale Supercomputers](https://developer.nvidia.com/blog/running-ai-workloads-on-rack-scale-supercomputers-from-hardware-to-topology-aware-scheduling/)：解释 rack-scale NVLink fabric、Cluster UUID、Clique ID 与拓扑感知调度的关系。
* [NVIDIA GB200 NVL Partition](https://docs.nvidia.com/multi-node-nvlink-systems/partition-guide-v1-2.pdf)：说明 NVLink Domain、NVLink Partition 和平台侧 partition 管理。
* [Kubernetes support for GH200 / GB200](https://docs.google.com/document/d/1PrdDofsPFVJuZvcv-vtlI9n2eAh-YVf_fRQLIVmDwVY)：ComputeDomain 背景设计资料。
* [Supporting GB200 on Kubernetes](https://docs.google.com/presentation/d/1Xupr8IZVAjs5bNFKJnYaK0LE7QWETnJjkz6KOfLu87E)：GB200 / Kubernetes 支持相关 slides。

### 5.4 QoS

Kubernetes 按 Pod 的 `requests` / `limits` 把它分成三个 [**QoS 级别**](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/)：`Guaranteed`、`Burstable`、`BestEffort`。节点内存紧张时，kubelet 按 `BestEffort` → `Burstable` → `Guaranteed` 的顺序驱逐 Pod。

| QoS 级别       | 条件                                                                                            | GPU 作业建议     |
| ------------ | --------------------------------------------------------------------------------------------- | ------------ |
| `Guaranteed` | 每个容器的 CPU、memory `requests` 都等于 `limits`                                                      | 性能敏感训练、需要独占核 |
| `Burstable`  | 设了 CPU/memory 的 request 或 limit，但未全部满足 `requests == limits`（如 `requests` \< `limits`、或只设了一部分） | 弹性推理、实验作业    |
| `BestEffort` | 完全不设 `requests` / `limits`                                                                    | 不适合 GPU 生产作业 |

GPU 训练 Pod 应尽量落到 `Guaranteed`——每个容器的 CPU、memory `requests` 都等于 `limits`（CPU 取整数还能配合 CPU Manager `static` 独占核）。这样既不会在内存紧张时被优先驱逐，也是后续做 NUMA 保证分配的前提。

***

### 5.5 拓扑感知调度

为支撑延迟敏感、高吞吐的工作负载，Kubernetes 提供了一套 Resource Manager（资源管理器）。它们为那些对 CPU、设备、内存（hugepages）有特定要求的 Pod，协调并优化节点内资源的对齐。

Topology Manager 是 kubelet 的一个组件，负责协调这一组承担上述优化的管理器。这组管理器各管一类资源，都作为 Topology Manager 的 **hint provider**，与它协商来完成资源分配决策：

* **CPU Manager**：kubelet 的组件，为 CPU 资源提供独占式分配。详见 [Control CPU Management Policies on the Node](https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/)。
* **Memory Manager**：kubelet 的组件，为内存资源提供独占式分配。详见 [Control Memory Management Policies on a Node](https://kubernetes.io/docs/tasks/administer-cluster/memory-manager/)。
* **Device Manager**：kubelet 的组件，通过 device plugin API 把硬件设备分配给 Pod（拓扑信息由 device plugin 提供）。详见 [Device Plugin Integration with the Topology Manager](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/#device-plugin-integration-with-the-topology-manager)。

如下图，[Topology Manager](https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/) 自己不管具体资源，只在 **Pod 准入阶段**做协调：先向这三个 manager 各要一份 NUMA 提示（`GetTopologyHints`），合并后按策略决定是否准入该 Pod；准入后再让它们各自按选定的 NUMA 分配。这样 CPU、内存、设备就尽量落到**同一个 NUMA 节点**，减少跨 NUMA 访问。

<Frame caption="Topology Manager 在 Pod 准入阶段汇总 CPU / Memory / Device Manager 的 NUMA 提示，据此决定 Pod 能否被 kubelet 准入。图源：[Kubernetes Topology Manager](https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/)">
  <img src="https://mintcdn.com/se7en/eaMJaPAQPjy6IQ-7/books/ai-systems-performance-engineering/images/ch03-kubernetes-topology-manager.png?fit=max&auto=format&n=eaMJaPAQPjy6IQ-7&q=85&s=0c5541332eb9a4a62298c8f207b1eac2" alt="Kubernetes Topology Manager 汇总 CPU Manager、Device Manager 等 hint provider 的拓扑提示，并决定 Pod 是否可以被 kubelet 准入" width="1105" height="808" data-path="books/ai-systems-performance-engineering/images/ch03-kubernetes-topology-manager.png" />
</Frame>

Topology Manager 支持四种**策略**（通过 kubelet 的 `--topology-manager-policy` flag 或配置字段 `topologyManagerPolicy` 设置），对齐的严格程度依次递增：

* **`none`（默认）**：不做任何拓扑对齐。
* **`best-effort`**：为每个容器算出首选 NUMA 亲和；即使对不齐到首选，也照常准入（尽力而为）。
* **`restricted`**：算出首选亲和；若实际无法命中首选，则拒绝该 Pod。
* **`single-numa-node`**：判断资源能否落在**单个** NUMA node，能则准入，否则拒绝。

Topology Manager 还有两种**作用域**（通过配置字段 `topologyManagerScope` 设置），决定对齐是「按容器」还是「按 Pod」：

* **`container`（默认）**：逐容器分别对齐，容器之间不成组，各自被独立地对齐到 NUMA。
* **`pod`**：把 Pod 内所有容器当作整体，一起对齐到单个 NUMA node 或同一组 NUMA node（Pod 的资源总量按 effective requests/limits 计算，即取「所有 app 容器请求之和」与「最大的单个 init 容器请求」中的较大值——也就是 Pod 同一时刻的峰值需求）。配合 `single-numa-node` 可把整个 Pod 放到一个 NUMA node 上，消除 Pod 内的跨 NUMA 通信开销，适合延迟敏感或高吞吐 IPC 应用。

#### 5.5.1 CPU Manager

CPU Manager 是 kubelet 的组件，为 CPU 资源提供**独占式分配**能力。

默认情况下，kubelet 用 **CFS quota**（Linux CFS 调度器基于 cgroup 的 CPU 时间配额）来限制 Pod 的 CPU 上限；当节点上跑着很多 CPU 密集型 Pod 时，工作负载可能迁移到不同的 CPU 核上——取决于 Pod 是否被限流、以及调度时哪些核可用。多数负载对这种迁移不敏感，无需任何干预也能正常工作。但对 CPU cache 亲和、调度延迟明显影响性能的负载，kubelet 允许用 CPU 管理策略来影响核的放置。关键配置：

* CPU Manager 有两种 policy（通过 `cpuManagerPolicy` 配置）：
  * `none`（默认）：不做额外绑核，沿用 OS 调度器默认的 CPU 亲和（CPU 上限仍由 CFS quota 限制）。
  * `static`：让 Guaranteed Pod 中「整数 CPU 请求」的容器独占节点上的 CPU 核（独占性由 **cpuset cgroup 控制器**、即写 `cpuset.cpus` 强制）；其余容器共用 shared pool。
* 启用 `static` 时，kubelet 要求预留一份**大于零**的 CPU（否则独占核被占满后 shared pool 会变空，非独占容器无核可跑）；下面三个参数任选其一或组合，预留的核都会从独占分配池里排除：

| 命令行 flag            | 配置字段                 | 预留给谁                    | 形式                       | 说明                                           |
| ------------------- | -------------------- | ----------------------- | ------------------------ | -------------------------------------------- |
| `--kube-reserved`   | `kubeReserved`       | K8s 组件（kubelet、容器运行时等）  | CPU 量                    | 预留量从 Node Allocatable 中扣除                    |
| `--system-reserved` | `systemReserved`     | OS 系统进程（systemd、sshd 等） | CPU 量                    | 同样从 Node Allocatable 扣除，可与 `kubeReserved` 叠加 |
| `--reserved-cpus`   | `reservedSystemCPUs` | 系统 + kubernetes 线程      | 具体 CPU 核（cpuset，如 `0-3`） | CPU 预留的显式核形式，优先级高于上面两者的 CPU 量                |

`cpuManagerPolicyOptions` 用来控制 `static` 策略的行为，可选项（按字母序）：

| 选项                                 | 成熟度         | 起始版本          | 作用                                         |
| ---------------------------------- | ----------- | ------------- | ------------------------------------------ |
| `align-by-socket`                  | alpha（默认隐藏） | 1.25          | 按物理 package / socket 边界对齐 CPU，而非逻辑 NUMA 边界 |
| `distribute-cpus-across-cores`     | alpha（默认隐藏） | 1.31          | 把虚拟核（硬件线程）分散到不同物理核上                        |
| `distribute-cpus-across-numa`      | beta（默认可见）  | 1.23          | 把 CPU 分散到不同 NUMA 域，在选中的域间尽量均衡              |
| `full-pcpus-only`                  | GA（默认可见）    | 1.22（1.33 GA） | 总是分配整颗物理核                                  |
| `strict-cpu-reservation`           | GA（默认可见）    | 1.32（1.35 GA） | 不论 QoS，禁止所有 Pod 使用预留的 CPU                  |
| `prefer-align-cpus-by-uncorecache` | GA（默认可见）    | 1.32          | 尽力按 uncore（末级缓存 LLC）边界对齐 CPU               |

下面是一个启用 `static` 的 KubeletConfiguration 示例：

```yaml theme={null}
# KubeletConfiguration
cpuManagerPolicy: static
cpuManagerPolicyOptions:
  full-pcpus-only: "true"        # 只按整颗物理核分配

# CPU 预留（static 要求 > 0），两种方式二选一：
# 方式一：显式指定预留哪几个核，隔离最干净；一旦设置，优先级高于下面 kubeReserved / systemReserved 的 cpu
# reservedSystemCPUs: "0-3"
# 方式二：按「量」预留 CPU（不指定具体核）
kubeReserved:
  cpu: "1"                       # 留 1 核给 K8s 组件
  memory: "2Gi"
systemReserved:
  cpu: "1"                       # 留 1 核给系统进程
  memory: "1Gi"
```

#### 5.5.2 Memory Manager

Memory Manager 是 kubelet 的组件，为 **Guaranteed QoS** 的 Pod 提供内存 / hugepages 的 NUMA 保证分配：它生成内存的 NUMA 亲和 hint 交给 Topology Manager，并通过 cgroup 的 `cpuset.mems` 强制内存只从选定的 NUMA node 分配。关键配置：

* Memory Manager 有两种 policy（通过 `memoryManagerPolicy` 配置）：
  * `None`（默认）：不做内存 NUMA 对齐，一律返回默认 hint（等于没有 Memory Manager）。
  * `Static`（仅 Linux）：给 Guaranteed Pod 的内存 / hugepages 做 NUMA 保证分配并写 `cpuset.mems`；BestEffort / Burstable 仍返回默认 hint。
* 启用 `Static` 时**必须**配 `reservedMemory`（逐 NUMA node 预留），且预留总量必须**等于** `kubeReserved` + `systemReserved` + `evictionHard` 的 `memory.available` 三者之和，否则 kubelet 启动报错。计算公式如下：

$$
\sum_{i=0}^{n-1} \text{reservedMemory}[i] \;=\; \text{kubeReserved} + \text{systemReserved} + \text{evictionHard(memory.available)}
$$

其中 $i$ 是 NUMA node 下标，$n$ 是 NUMA node 总数。左边是**逐 NUMA node 预留量之和**，右边是**节点级别要留出的内存总量**（K8s 组件 + OS 系统进程 + eviction 缓冲，`memory.available` 默认 100Mi）。含义是：节点总共预留多少内存，就得原样按 NUMA node 拆分下去，两边必须严格相等——多一点少一点 kubelet 都会启动报错。

```yaml theme={null}
# KubeletConfiguration
memoryManagerPolicy: Static
kubeReserved:
  memory: "4Gi"                  # 给 K8s 组件留的内存
systemReserved:
  memory: "1Gi"                  # 给系统进程留的内存
# 总预留 = kubeReserved + systemReserved + evictionHard(默认 100Mi) = 5220Mi
# 按 NUMA node 拆分，求和必须等于该总量，否则 kubelet 启动报错
reservedMemory:
- numaNode: 0
  limits:
    memory: "3Gi"                # 3072Mi
- numaNode: 1
  limits:
    memory: "2148Mi"             # 3072 + 2148 = 5220Mi
```

#### 5.5.3 Device Manager

Device Manager 通过 device plugin API 把设备（GPU、NIC 等）分配给 Pod。为了让设备也能参与 Topology Manager 的 NUMA 对齐，device plugin API 专门扩展出了一个 `TopologyInfo` 结构：plugin 在 `ListAndWatch` 里逐个上报设备时带上它的 NUMA 归属，Device Manager 据此把设备的 NUMA 偏好转成 hint 交给 Topology Manager。

plugin 上报的单个设备（`Device`）长这样：

```go theme={null}
pluginapi.Device{
    ID:     "25102017",
    Health: pluginapi.Healthy,
    Topology: &pluginapi.TopologyInfo{
        Nodes: []*pluginapi.NUMANode{
            {ID: 0},
        },
    },
}
```

* `ID`：设备唯一标识，不透明字符串，由 plugin 自定义（GPU 常用 UUID / minor number）。
* `Health`：健康状态（`Healthy` / `Unhealthy`），只有 `Healthy` 的设备才计入该资源的 allocatable。
* `Topology.Nodes`：设备的 NUMA 归属。这里 `{ID: 0}` 表示这块设备挂在 **NUMA node 0** 上；可填多个表示能从多个 NUMA node 访问，留空（`Topology: nil`）则表示无 NUMA 偏好。

<h4 id="topology-aware-example">
  5.5.4 拓扑感知调度示例
</h4>

下面就演示一个拓扑感知调度的完整示例。

**节点拓扑假设（2 个 NUMA node）**

| NUMA node | CPU 核       | 内存    | GPU           |
| --------- | ----------- | ----- | ------------- |
| node 0    | 0–47（48 核）  | 512Gi | `gpu0`–`gpu3` |
| node 1    | 48–95（48 核） | 512Gi | `gpu4`–`gpu7` |

**1.** 先在每个节点的 `KubeletConfiguration` 里同时打开 CPU、Memory、Topology 三个 manager，并用 `single-numa-node` 策略把一个容器的所有资源都对齐到同一个 NUMA node：

```yaml theme={null}
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration

# —— CPU Manager：允许独占绑核 ——
cpuManagerPolicy: "static"
reservedSystemCPUs: "0-1,48-49"            # static 要求的 CPU 预留：每个 NUMA node 各留 2 核

# —— Memory Manager：内存 NUMA 保证分配 ——
memoryManagerPolicy: "Static"
kubeReserved:
  memory: "3Gi"
systemReserved:
  memory: "1Gi"
reservedMemory:                            # 总预留 3Gi + 1Gi + 100Mi = 4196Mi，按 NUMA 拆分
- numaNode: 0
  limits:
    memory: "2Gi"
- numaNode: 1
  limits:
    memory: "2148Mi"                       # 2048 + 2148 = 4196Mi

# —— Topology Manager：强制所有资源落到同一 NUMA node ——
topologyManagerPolicy: "single-numa-node"
topologyManagerScope: "container"          # 逐容器对齐
```

**2.** 假设要调度的 Pod 如下，申请 16 核 CPU、128Gi 内存和 4 张 GPU。它的 **CPU 与内存都设了 `requests == limits`**，因此属于 **Guaranteed QoS**（QoS 只看 CPU 和内存；GPU 这类扩展资源本身就要求 `requests == limits`，但不参与 QoS 判定）。只有 Guaranteed 的 Pod 才会触发三个 manager 的 NUMA 保证分配。

```yaml theme={null}
apiVersion: v1
kind: Pod
metadata:
  name: numa-aligned-app
spec:
  containers:
  - name: app
    image: my-cuda-app:latest
    resources:
      requests:
        cpu: "16"
        memory: "128Gi"
        nvidia.com/gpu: "4"
      limits:
        cpu: "16"
        memory: "128Gi"
        nvidia.com/gpu: "4"
```

**3. 三个 provider 各自上报 hint**：每个 manager 只为自己那类资源生成 hint——把「哪些 NUMA node 组合能满足请求」列成候选掩码，其中用**最少 node 数**就能满足的标 `Preferred: true`（`{n}` 是 NUMA 位掩码的简记，`{0,1}` 表示同时用到 node0 和 node1）。本例单个 node 就够任一资源（node0 有 46 个可分配核、约 510Gi 内存、4 张 GPU，都 ≥ 请求量），所以三者形状一致：单节点 `{0}`、`{1}` 是首选，跨节点 `{0,1}` 也够但非最优。

```go theme={null}
// CPU Manager 返回 map[string][]TopologyHint（请求 16 核）
"cpu": {
  {NUMANodeAffinity: {0},   Preferred: true},   // 16 核全从 node0 出 → 单节点，最优
  {NUMANodeAffinity: {1},   Preferred: true},   // 16 核全从 node1 出 → 单节点，最优
  {NUMANodeAffinity: {0,1}, Preferred: false},  // 两个 node 各出一部分 → 够但跨 NUMA，非最优
}
// Memory Manager 返回（请求 128Gi）
"memory": {
  {NUMANodeAffinity: {0},   Preferred: true},   // 128Gi 全从 node0 → 单节点，最优
  {NUMANodeAffinity: {1},   Preferred: true},   // 128Gi 全从 node1 → 单节点，最优
  {NUMANodeAffinity: {0,1}, Preferred: false},  // 跨两个 node → 够但非最优
}
// Device Manager 返回（请求 4 张 GPU）
"nvidia.com/gpu": {
  {NUMANodeAffinity: {0},   Preferred: true},   // gpu0–gpu3 都在 node0，正好 4 张 → 最优
  {NUMANodeAffinity: {1},   Preferred: true},   // gpu4–gpu7 都在 node1，正好 4 张 → 最优
  {NUMANodeAffinity: {0,1}, Preferred: false},  // 跨两个 node 凑 4 张 → 够但非最优
}
```

**4. Topology Manager 合并并决策**：从每个 provider 各取一条 hint 组成一个组合，把三者的 NUMA 掩码按位**与**（AND）、`Preferred` 也全部相与，得到合并结果，再按 `single-numa-node` 策略筛选。

| 组合                             | 掩码 AND  | Preferred | 合法?   |
| ------------------------------ | ------- | --------- | ----- |
| cpu`{0}` · mem`{0}` · gpu`{0}` | `{0}`   | true      | ✓ 单节点 |
| cpu`{1}` · mem`{1}` · gpu`{1}` | `{1}`   | true      | ✓ 单节点 |
| cpu`{0}` · mem`{1}` · …        | `{}`（空） | —         | ✗ 无交集 |

合法的单节点方案有 `{0}` 和 `{1}`，Topology Manager 取**最窄且最优**的一个（宽度相同时选低位），即 **NUMA node 0**。于是准入该 Pod，并让三个 provider 都在 node 0 上分配：

* CPU Manager：从 node0 的可分配核（`2–47`）里独占 16 核；
* Memory Manager：128Gi 内存全部来自 node0；
* Device Manager：分配 `gpu0`–`gpu3`。

最终 CPU、内存、GPU 全部落在 **NUMA node 0**，没有跨 NUMA 访问。

**无法单节点满足则拒绝**：若把请求改成 `nvidia.com/gpu: 8`，任何单个 node 都只有 4 张 GPU，合并后不存在成立的单节点组合，`single-numa-node` 会**拒绝准入**。

#### 5.5.5 DRA：把拓扑对齐提前到调度阶段

[5.5.4 拓扑感知调度示例](#topology-aware-example) 的对齐发生在**节点准入阶段**：scheduler 选节点时只核对**节点级资源总量**（够几核 CPU、多少内存、几张 GPU），只管数量够不够，不管这些资源在节点内的 NUMA 位置和互联。结果即便节点数量够，Pod 落上去后也可能**没按最优的单 NUMA 方式部署**，资源被迫跨 NUMA、拖慢性能。

DRA 把这层判断**提前到调度阶段**：driver 把设备拓扑写成属性（如 `resource.kubernetes.io/pcieRoot`、`gpu.nvidia.com/parentUUID`），claim 用 `constraints.matchAttribute` 表达对齐要求，scheduler 选节点时就强制满足，直接挑一个能对齐的节点。GPU、NIC 这类设备之间的互联对齐现已可用；设备之外，CPU 和内存也在被逐步纳入 DRA（[KEP-5517 DRANodeAllocatableResources](https://www.kubernetes.dev/resources/keps/5517/)，Kubernetes 1.36 alpha）。

* [`dra-driver-cpu`](https://github.com/kubernetes-sigs/dra-driver-cpu)：用 DRA 替代 kubelet CPU Manager 做独占绑核，可按 core 类型、NUMA、LLC 等属性在调度阶段选核；与 CPU Manager **节点级互斥**（需 `cpuManagerPolicy: none`）。
* [`dra-driver-memory`](https://github.com/ffromani/dra-driver-memory)：同思路的 Memory Manager 替代，管内存 / hugepages 的 NUMA 分配。

**参考资料**

* [Kubernetes Topology Manager](https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/)
* [Control CPU Management Policies on the Node](https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/)
* [Control Memory Management Policies on a Node](https://kubernetes.io/docs/tasks/administer-cluster/memory-manager/)
* [Resource Managers](https://kubernetes.io/docs/concepts/workloads/resource-managers/)
* [dra-driver-cpu](https://github.com/kubernetes-sigs/dra-driver-cpu)
* [dra-driver-memory](https://github.com/ffromani/dra-driver-memory)
* [KEP-5517 DRANodeAllocatableResources](https://www.kubernetes.dev/resources/keps/5517/)
* [NVIDIA GB200 NVL tuning guide](https://docs.nvidia.com/multi-node-nvlink-systems/multi-node-tuning-guide/overview.html)
* [NVIDIA Topograph](https://github.com/NVIDIA/topograph)

### 5.6 优化网络通信

多节点 GPU 作业里，Pod 之间要频繁通信。Kubernetes 默认给每个 Pod 分配独立 IP，跨节点 Pod 之间还可能隔着 overlay 网络或 NAT，给性能敏感的 GPU 通信带来额外开销。

对性能敏感作业，可以让 Pod 直接用宿主机网络（`hostNetwork: true`，Docker 里对应 `--network=host`），容器网络不再隔离，直接使用宿主机的网络接口：

```yaml theme={null}
apiVersion: v1
kind: Pod
metadata:
  name: nccl-worker
spec:
  hostNetwork: true          # 直接用宿主机网络接口，不做隔离 / NAT
  containers:
  - name: worker
    image: my-nccl-app:latest
```

这样容器就能像宿主机一样直接访问 InfiniBand 互连，没有额外的地址转换和防火墙层。对 MPI 作业尤其有用——省去为每个 MPI rank 配端口映射的麻烦。

跨节点通信的瓶颈往往在网络栈：**RDMA**（远程直接内存访问）让一台机器的网卡直接读写另一台的内存，绕过 CPU 和内核协议栈；**GPUDirect RDMA** 更进一步，让网卡直接 DMA 到 / 从 GPU 显存，省掉「GPU → CPU 内存 → 网卡」的中转拷贝——这对多机 NCCL all-reduce 的带宽至关重要。要在 Kubernetes 里启用 RDMA，可以装 [Mellanox Kubernetes RDMA device plugin](https://github.com/Mellanox/k8s-rdma-shared-dev-plugin)：它把 InfiniBand 与 GPUDirect RDMA 端点暴露到 Pod 的网络接口上，实现低延迟、零拷贝通信。

***

### 5.7 网络拓扑感知调度

数据中心网络是分层的交换机结构（节点 → 机架交换机 → spine → core），两个节点相距的交换机层级越多、通信越慢。分布式训练的多个 Pod 一旦被撒到不同机架、不同 spine 下，all-reduce、all-to-all 就会走慢速的跨交换机链路，拖垮整体吞吐。网络拓扑感知调度让调度器理解这套层级，把同一作业的 Pod（作为一个 gang）尽量放进同一个网络性能域，让集合通信走在跳数更少、带宽更高的近端链路上，降低延迟与拥塞。

#### 5.7.1 Kubernetes 官方方案

Kubernetes 官方原生提供的网络拓扑感知调度方案是 [Topology-Aware Workload Scheduling](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-aware-scheduling/)（v1.36 alpha）：在 kube-scheduler 上开启 `TopologyAwareWorkloadScheduling` feature gate，用 `PodGroup` 表达 gang 调度和拓扑约束——拓扑约束基于某个 node label key（如 `topology.kubernetes.io/rack`），调度器的 `TopologyPlacement` 插件按该 label 的取值把节点分组，挑一个能装下整组 Pod 的域：

```yaml theme={null}
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: training-group
spec:
  schedulingPolicy:
    gang:
      minCount: 4                          # 整组一起调度
  schedulingConstraints:
    topology:
      - key: topology.kubernetes.io/rack   # 所有 Pod 必须落在同一 rack
---
apiVersion: batch/v1
kind: Job
metadata:
  name: training-workers
spec:
  parallelism: 4                            # 副本数要 ≥ minCount
  completions: 4
  completionMode: Indexed
  template:
    spec:
      schedulingGroup:
        podGroupName: training-group        # 模板里指向上面的 PodGroup
      restartPolicy: Never
      containers:
      - name: worker
        image: my-nccl-app:latest
        resources:
          limits:
            nvidia.com/gpu: "8"
```

v1.36 的一个限制要留意：**每个 PodGroup 只能写一个拓扑约束**——即单层 label，不支持 block → rack → host 这样的多层级。需要多层级、或软约束（preferred）时，用下面的 KAI / Volcano。

#### 5.7.2 NVIDIA [KAI Scheduler](https://github.com/NVIDIA/KAI-Scheduler)

KAI Scheduler 支持网络拓扑感知调度，做法分两步：先用 `Topology` CRD 声明集群的拓扑层级，再在作业上用注解引用它、表达约束。第一步，`Topology` CRD 把一组 node label 从最外层（如 block）到最内层（单机）列成层级，scheduler 据此把节点组织成一棵树：

```yaml theme={null}
apiVersion: kai.scheduler/v1alpha1
kind: Topology
metadata:
  name: cluster-topology
spec:
  levels:                                      # 从最外层（如 block）到单机
  - nodeLabel: cloud.provider.com/topology-block
  - nodeLabel: cloud.provider.com/topology-rack
  - nodeLabel: kubernetes.io/hostname
```

第二步，在作业（这里是 `Job`）上用注解引用这个 topology 并指定约束层级：`kai.scheduler/topology` 选择哪个 topology，`topology-required-placement` 是硬边界（必须落在该层内），`topology-preferred-placement` 是软优先（在硬边界内尽量收得更紧，只有比 required 更靠下的层级才有意义）：

```yaml theme={null}
apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-training
  annotations:
    kai.scheduler/topology: cluster-topology
    kai.scheduler/topology-required-placement: cloud.provider.com/topology-block   # 硬边界：不出 block
    kai.scheduler/topology-preferred-placement: cloud.provider.com/topology-rack   # 软优先：块内尽量同 rack
spec:
  parallelism: 4
  completions: 4
  template:
    spec:
      schedulerName: kai-scheduler
      restartPolicy: Never
      containers:
      - name: worker
        image: my-nccl-app:latest
        resources:
          limits:
            nvidia.com/gpu: "1"
```

#### 5.7.3 [Volcano](https://github.com/volcano-sh/volcano)

Volcano 支持网络拓扑感知调度，做法是用 `HyperNode` CRD 把网络**显式建成交换机树**：一个 `HyperNode` 代表一个网络拓扑性能域，通常映射到一个交换机 / TOR；多个 `HyperNode` 按层级连成树。叶子 HyperNode（tier 1）的成员是真实节点，非叶 HyperNode（tier 2、3…）的成员是下层 HyperNode——`tier` 越低，域内节点间通信越快。

<Frame caption="Volcano HyperNode 把网络建成交换机树：叶子（Tier 1，如 s0 / s1）含真实节点，上层（Tier 2，s2）逐级聚合。图源：Volcano Network Topology Aware Scheduling">
  <img src="https://mintcdn.com/se7en/XjDQ9O7icaQalExC/books/ai-systems-performance-engineering/images/ch03-volcano-hypernode.png?fit=max&auto=format&n=XjDQ9O7icaQalExC&q=85&s=4a2cb2ace78d19883fabb6e59cc784a1" alt="Volcano HyperNode 树状拓扑：Tier 2 的 s2 聚合两个 Tier 1 的 s0 和 s1，s0 含 Node0/Node1、s1 含 Node2/Node3" width="1386" height="930" data-path="books/ai-systems-performance-engineering/images/ch03-volcano-hypernode.png" />
</Frame>

节点间的通信效率取决于跨越的 HyperNode 层级：node0 与 node1 同属 s0，效率最高；node1 与 node2 要跨两层（s0 → s2 → s1），效率较低。下面把这棵树建出来——`s0`、`s1` 是 tier 1 的叶子（各含两个节点），`s2` 是 tier 2、成员是 `s0` 和 `s1`：

```yaml theme={null}
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s0
spec:
  tier: 1                          # s0 位于 Tier 1
  members:
  - type: Node                     # 成员是真实节点
    selector:
      exactMatch:
        name: node0
  - type: Node
    selector:
      exactMatch:
        name: node1
---
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s1
spec:
  tier: 1                          # s1 位于 Tier 1
  members:
  - type: Node
    selector:
      exactMatch:
        name: node2
  - type: Node
    selector:
      exactMatch:
        name: node3
---
apiVersion: topology.volcano.sh/v1alpha1
kind: HyperNode
metadata:
  name: s2
spec:
  tier: 2                          # s2 位于 Tier 2
  members:
  - type: HyperNode                # 成员是下层 HyperNode
    selector:
      exactMatch:
        name: s0
  - type: HyperNode
    selector:
      exactMatch:
        name: s1
```

作业用 `networkTopology` 声明要落在哪一层内。下面这个作业把所有 Pod 严格锁进**同一个 leaf（tier 1，即单个 s0 或 s1）**：

```yaml theme={null}
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: training-job
spec:
  schedulerName: volcano
  minAvailable: 2
  networkTopology:
    mode: hard              # hard：必须全落进某个 tier ≤ N 的 HyperNode；soft：尽量
    highestTierAllowed: 1   # 只允许 tier 1：所有 Pod 必须挤进同一个 leaf（s0 或 s1）
  tasks:
  - name: worker
    replicas: 2             # 2 个 Pod = 2 节点，正好一个 leaf（s0/s1 各含 2 节点）装得下
    template:
      spec:
        schedulerName: volcano
        restartPolicy: OnFailure
        containers:
        - name: c0
          image: my-nccl-app:latest
          resources:
            requests:
              nvidia.com/gpu: "8"
            limits:
              nvidia.com/gpu: "8"
```

**参考资料**

* [Topology-Aware Workload Scheduling（Kubernetes）](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-aware-scheduling/)、[Kubernetes v1.36: Advancing Workload-Aware Scheduling](https://kubernetes.io/blog/2026/05/13/kubernetes-v1-36-advancing-workload-aware-scheduling/)
* [KAI Scheduler Topology 文档](https://github.com/NVIDIA/KAI-Scheduler/blob/main/docs/topology/README.md)
* [Volcano Network Topology Aware Scheduling](https://github.com/volcano-sh/website/blob/master/docs/KeyFeatures/NetworkTopologyAware.md)

***

## FAQ

<Accordion title="MIG 是专门针对单卡的，还是多卡也能用？">
  MIG 是**单卡内部**的硬件切分：把**一张 GPU** 切成多个彼此隔离的实例（各有独立的 SM、显存和 cache），供多个小任务分别使用。多卡场景下可以在**每张支持 MIG 的卡上分别开启 MIG**，但每个 MIG 实例只属于它所在的那张物理卡，**不能跨卡合并成更大的实例**。此外，开启 MIG 会禁用该卡的 NVLink P2P，所以依赖多卡 NVLink 互联的大规模训练不适合用 MIG（应整卡使用）。一句话：MIG 是「把单卡切小」，不是「把多卡拼大」。
</Accordion>

<Accordion title="线程运行时修改 nice 值会不会有问题？">
  不会。`renice`（底层 `setpriority` / `sched_setattr`）可以对**正在运行**的进程 / 线程动态调整 nice，内核会立即按新权重重新参与 CFS 调度，无需重启进程。几点注意：**提高优先级（nice 调为负）需要 root 或 `CAP_SYS_NICE`**，降低优先级普通用户即可；nice 只影响 CFS 普通线程之间**相对**的 CPU 时间分配，不改变程序正确性，也不等于独占 CPU；但不要把关键线程（如 DataLoader、通信辅助线程）的优先级调得过低，否则容易被其他进程抢占，反而拖慢对 GPU 的数据供给。
</Accordion>
