第六章:【重难点】性能调优(上):系统与网卡参数

第五章的基准测试给了我们一把精确的尺子,现在是时候用这把尺子来衡量并优化我们系统的每一个细节了。如果你的ib_write_bw没有跑满线速,或者ib_write_lat的延迟高于预期,甚至nccl-testsbusbw不尽人-意,那么问题很可能就隐藏在本章将要探讨的系统和网卡参数之中。

性能调优是一门科学,更是一门艺术。它不是随机地修改配置,而是基于对系统架构深刻理解的系统性工程。本章将为你建立一个清晰的调优框架——“性能调优金字塔”,并从最底层、影响最广泛的操作系统层面开始,逐步深入到网卡和驱动层面,为你揭示那些能够显著影响RDMA性能的核心参数和“开关”。


6.1 调优金字塔:一个系统性的调优框架

在开始任何调优之前,我们必须建立一个正确的思维模型。一个常见的错误是直接跳到最细枝末节的参数进行修改,而忽略了更高层面的、更具决定性的因素。我们推荐遵循一个自下而上的“调优金字塔”模型:

  1. 第一层(地基):操作系统 (OS) 层面

    • 描述: 这是最基础、影响最广的一层。操作系统的调度策略、中断处理、内存管理和电源管理等行为,为上层所有应用设定了性能的“天花板”。一个“嘈杂”或配置不当的OS,会让任何上层调优都收效甚微。
    • 目标: 为RDMA流量创造一个低干扰、高效率、可预测的执行环境。
  2. 第二层(承重墙):网卡/驱动 (NIC/Driver) 层面

    • 描述: 这一层直接控制着数据包如何被硬件处理和传输。这里的参数(如MTU、缓冲区大小、中断合并策略)直接决定了网络I/O的效率。
    • 目标: 使网卡和驱动的配置与我们的网络环境(IB/RoCE)和应用负载(高吞吐/低延迟)相匹配。
  3. 第三层(屋顶):协议与上层应用层面

    • 描述: 这是最顶层,涉及RDMA协议本身的参数,以及像Kubernetes、Docker、NCCL这些上层应用的配置。
    • 目标: 确保上层应用能够正确、高效地利用底下两层已经优化好的RDMA能力。

本章将重点关注金字塔的第一层和第二层,因为它们是后续所有性能表现的基础。


6.2 OS 层面调优:为性能打造一块宁静的基石

操作系统为了通用性和公平性,其默认配置往往不适合需要极致性能和低抖动的专用场景(如AI训练)。我们的任务就是关掉这些“通用性”的设置,为RDMA开辟一条VIP通道。

6.2.1 【重难点】CPU 亲和性 (Affinity) 与 IRQ 中断绑定

这是OS层面最重要、最有效的调优手段,没有之一。其核心思想是解决NUMA(Non-Uniform Memory Access)架构带来的性能问题。

  • NUMA 到底是什么?为什么它如此重要?
    现代多核服务器通常是NUMA架构。这意味着服务器内部有多个“节点”(通常一个CPU插槽及其直连的内存就是一个NUMA节点)。一个CPU核心访问其“本地”(Local)NUMA节点的内存和PCIe设备(比如我们的网卡)速度极快,而访问“远程”(Remote)NUMA节点的资源则需要跨越处理器之间的互联总线(如Intel的QPI或AMD的Infinity Fabric),这个跨越会带来显著的延迟增加。

  • 性能陷阱:
    当你的高性能网卡收到一个数据包时,它会产生一个硬件中断(IRQ)来通知CPU处理。如果操作系统将这个IRQ随机调度到一个远程NUMA节点的CPU核心上,那么这个CPU核心在处理数据时,所有对网卡内存(如Ring Buffer)的访问都将是缓慢的跨节点访问。这会极大地增加延迟,并降低吞-吐量。

  • 调优目标:
    我们必须将处理网卡中断的CPU核心,“钉死”在与网卡物理上处于同一个NUMA节点的那些核心上。

  • 【实践笔记】如何将网卡中断绑在正确的 NUMA 节点上?

    1. 第一步:确定网卡所在的NUMA节点

      # 找到你的网卡接口名,例如 ens1f0np0
      # 查看其设备信息中的 numa_node 文件
      cat /sys/class/net/ens1f0np0/device/numa_node
      

      这个命令会输出一个数字,比如 01,这就是你的网卡所在的NUMA节点ID。如果输出 -1,说明系统不支持或无法确定,但对于现代服务器这很少见。

    2. 第二步:确定该NUMA节点上有哪些CPU核心

      lscpu | grep "NUMA node"
      

      输出会类似:

      NUMA node0 CPU(s):   0-23,48-71
      NUMA node1 CPU(s):   24-47,72-95
      

      现在你知道了,如果你的网卡在NUMA node 0上,你应该使用0-23或48-71这些核心来处理它的中断。

    3. 第三步:找到网卡对应的所有IRQ号

      # ens1f0np0是接口名,mlx5_core是NVIDIA网卡的驱动模块名
      grep ens1f0np0 /proc/interrupts
      # 或者更通用地,grep mlx5_core /proc/interrupts
      

      你会看到一长串列表,第一列就是IRQ号。现代网卡为了并行处理,会有多个中断队列,因此会对应多个IRQ号。

      110: ... IRQ-PCI-MSI-X mlx5_comp0@pci:0000:81:00.0
      111: ... IRQ-PCI-MSI-X mlx5_comp1@pci:0000:81:00.0
      ...
      125: ... IRQ-PCI-MSI-X mlx5_comp15@pci:0000:81:00.0
      
    4. 第四步:编写脚本,将这些IRQ绑定到目标CPU核心
      手动一个个绑定非常繁琐。一个简单的脚本可以一劳永逸。

      #!/bin/bash
      # set_irq_affinity.sh
      
      # 你的网卡接口名
      IF="ens1f0np0"
      
      # 第一步:找到NUMA节点
      NUMA_NODE=$(cat /sys/class/net/$IF/device/numa_node)
      if [ $NUMA_NODE -lt 0 ]; then
          echo "Warning: Could not determine NUMA node for $IF."
          # 可以选择一个默认节点,例如 node 0
          NUMA_NODE=0
      fi
      
      # 第二步:获取该NUMA节点的CPU列表,并转换为smp_affinity可以识别的掩码
      CPU_MASK=$(numactl -H | grep "node $NUMA_NODE cpus" | awk -F': ' '{print $2}' | sed 's/ / /g')
      
      echo "Binding IRQs for $IF (NUMA node $NUMA_NODE) to CPUs: $CPU_MASK"
      
      # 第三步:找到所有相关的IRQ号
      IRQ_LIST=$(grep $IF /proc/interrupts | awk -F':' '{print $1}')
      
      # 第四步:循环绑定
      for IRQ in $IRQ_LIST
      do
          CPU_AFFINITY_MASK=$(cpulist-to-mask $CPU_MASK) # 需要安装 'numactl' 包来获取 cpulist-to-mask 工具
          echo "Binding IRQ $IRQ to mask $CPU_AFFINITY_MASK"
          echo $CPU_AFFINITY_MASK > /proc/irq/$IRQ/smp_affinity
      done
      
      echo "Done."
      ``` 将这个脚本设为可执行,并以root权限运行。运行后,你可以通过`cat /proc/irq/IRQ号/smp_affinity`来验证掩码是否已正确写入。
      
      
6.2.2 关闭 irqbalance:让你的手动配置生效

irqbalance 是一个Linux系统中的守护进程,它的作用是周期性地检查系统负载,并自动地将IRQ中断重新分配到各个CPU核心上,以求达到负载均衡。
这在通用服务器上是一个很好的功能,但对于我们精心进行了手动IRQ绑定的高性能计算节点来说,它就是个捣乱者。它会定期覆盖掉我们设置好的smp_affinity,让我们的努力付诸东流。

实践抓手:
必须无情地关闭并禁用它。

sudo systemctl stop irqbalance
sudo systemctl disable irqbalance
6.2.3 透明大页 (Transparent Huge Pages - THP) 的影响

THP是Linux内核的一项内存管理功能,它试图在后台自动地将标准的4KB内存页合并成2MB的“大页”。

  • 优点: 对于需要访问大块连续内存的应用程序,使用大页可以减少TLB(Translation Lookaside Buffer,地址转换后备缓冲器)的缓存未命中次数,从而提升内存访问性能。
  • 【重难点】缺点: THP的“透明”和“自动”是有代价的。内核中有一个名为khugepaged的线程,它会定期扫描进程的内存空间,寻找可以合并的页。这个扫描和合并的过程会消耗CPU,更糟糕的是,它可能会在合并时临时锁定内存页,导致应用程序出现不可预测的、长达毫秒级的延迟停顿(Latency Spike)

对于RDMA这种对延迟极其敏感的应用,这种不可预测的停顿是致命的。一次毫秒级的停顿,足以让ib_write_latt_max值飙升,也会严重影响NCCL的同步效率。

实践结论与抓手:
对于追求极致低延迟和稳定性的AI训练集群,强烈建议禁用THP

# 临时禁用
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 永久禁用 (添加到 /etc/rc.local 或通过grub内核参数)
# 在GRUB_CMDLINE_LINUX中添加 "transparent_hugepage=never"

你可以通过cat /sys/kernel/mm/transparent_hugepage/enabled来验证,输出应该是always madvise [never]


6.3 网卡/驱动层面调优:拧紧硬件的每一颗螺丝

当操作系统这个“地基”被打牢之后,我们就可以开始对网卡和驱动这栋“承重墙”进行精细化调整了。

6.3.1 关键参数:MTU (再次强调)

我们在第五章已经提到了MTU对带宽的重要性。这里我们深入探讨“为什么”以及“如何选择”。

  • 为什么IB用4096?
    InfiniBand协议本身定义了多种MTU尺寸,如256, 512, 1024, 2048, 4096字节。4096字节(4K)是其中最大的标准尺寸。选择它有几个好处:
    1. 高效率: 更大的MTU意味着更小的包头开销占比。发送同样多的数据,需要的包数量更少,处理的包头和中断也更少。
    2. 与内存页对齐: 4KB恰好是x86架构下标准的内存页面大小。这使得数据在内存和网卡之间的DMA传输可以更高效地对齐。
  • 为什么RoCE用1064/1500/9000?
    RoCEv2的MTU选择更复杂,因为它需要考虑底层以太网的限制和封装开销。
    • RoCEv2封装开销: 一个RoCEv2数据包= 以太网头(14B) + IP头(20B) + UDP头(8B) + IB BTH(12B) + … + Payload + CRC(4B)。总开销大约在50字节以上。
    • 1500 (标准以太网MTU): 如果你的RoCE流量需要通过不支持巨型帧(Jumbo Frames)的标准网络设备,你就只能使用1500。此时,RoCE的有效载荷(Payload)大小会被限制在 1500 - 开销 左右。
    • 9000 (巨型帧): 这是高性能RoCE网络的首选。将以太网MTU设为9000,可以极大地提高载荷效率,显著提升吞吐量。
    • 1064 等奇怪的值: 这些值通常是为了兼容一些特定的、MTU受限的设备或云环境。在自建的AI集群中,应尽量避免使用这些较小的非标准MTU。

调优建议:

  • IB: 保持默认的4096
  • RoCE: 在所有服务器和交换机上,统一配置并启用9000字节的巨型帧
6.3.2 关键参数:调整 Ring Buffer 大小 (ethtool -g)
  • 什么是Ring Buffer?
    Ring Buffer(环形缓冲区),也叫RX/TX Descriptor Queues,是位于主机内存中的一块区域。驱动程序将待发送(TX)或已接收(RX)数据包的描述符(地址、长度等信息)放入这个队列,网卡硬件通过DMA直接读写这个队列,从而实现CPU与网卡之间的高效解耦。
  • 性能陷阱:
    如果Ring Buffer的尺寸太小,在网络流量突发(Burst)时,它可能会被迅速填满。对于RX(接收)队列,如果CPU处理速度跟不上数据包到达速度,新来的包会因为没有描述符空间而被网卡驱动直接丢弃。这会导致上层协议(即使是无损网络)出现丢包,引发重传和性能下降。
  • 实践抓手:
    使用ethtool来检查和调整Ring Buffer的大小。
    1. 检查当前设置:

      ethtool -g ens1f0np0
      

      输出示例:

      Ring parameters for ens1f0np0:
      Pre-set maximums:
      RX:             8192
      TX:             8192
      Current hardware settings:
      RX:             1024
      TX:             1024
      

      Pre-set maximums是驱动和硬件支持的最大值,Current hardware settings是当前生效的值。在这个例子中,当前值远小于最大值,有很大的提升空间。

    2. 增大Ring Buffer:

      # 将RX和TX都设置为最大值8192
      sudo ethtool -G ens1f0np0 rx 8192 tx 8192
      
    调优建议:
    在进行高吞吐量、高PPS(每秒包数)的测试或应用前,将RX/TX Ring Buffer的大小尽量设置为硬件支持的最大值,可以有效地防止因突发流量造成的丢包,提升网络的稳定性。
6.3.3 关键参数:中断合并 (Interrupt Coalescing) 的取舍
  • 什么是中断合并?
    如前所述,网卡每收到一个包就中断一次CPU,开销巨大。中断合并是一种优化技术,它允许网卡在产生中断前“等待”一下,要么等累计收到一定数量的包(rx-frames),要么等过了一段固定的时间(rx-usecs),然后才向CPU发起一次中断,一次性处理多个包。

  • 【重难点】性能的取舍:高吞吐 vs. 低延迟
    中断合并是一个典型的性能权衡:

    • 提高合并程度rx-framesrx-usecs值设得更大):
      • 优点: 显著降低中断频率,减轻CPU负担。这使得CPU可以将更多周期用于处理数据本身,而不是响应中断,从而提高最大吞吐量
      • 缺点: 数据包在网卡缓冲区中等待的时间更长,导致延迟增加
    • 降低合并程度(值设得更小,甚至为0):
      • 优点: 数据包一到达就立即通知CPU,延迟最低
      • 缺点: 中断频率极高,大量消耗CPU资源,在极高流量下可能导致CPU成为瓶颈,反而限制最大吞吐量
  • 实践抓手与调优建议:
    使用ethtool -c检查,ethtool -C设置。

    # 检查当前中断合并设置
    ethtool -c ens1f0np0
    
    # 找到类似 rx-usecs, rx-frames, tx-usecs, tx-frames 的参数
    

    调优决策表:

你的主要目标 调优策略 示例命令 (ethtool -C ...)
极致的低延迟 关闭或最小化中断合并。 rx-usecs 0rx-frames 1
最大的吞吐量 增大中断合并参数。 rx-usecs 128 rx-frames 32 (具体值需实验)
平衡(AI训练) 启用自适应中断合并(Adaptive Coalescing) adaptive-rx on adaptive-tx on

自适应中断合并是现代NVIDIA网卡的一项优秀功能。启用后,驱动会根据网络流量的动态模式(是小包为主还是大包为主,是稀疏还是密集)来自动调整中断合并的程度。对于AI训练这种既有小消息同步(要求低延迟)又有大块数据传输(要求高吞-吐量)的混合负载,开启自适应中断合并通常是最佳的起点和默认配置

本章小结:
我们已经深入到系统性能调优的核心腹地。通过遵循“调优金字塔”模型,我们首先从操作系统层面解决了最影响性能稳定性的三大问题:通过IRQ绑定克服了NUMA陷阱,通过禁用irqbalance保障了配置的持久性,通过禁用THP消除了不可预测的延迟抖动。接着,我们深入到网卡和驱动层面,精细化地调整了三个关键参数:为RoCE选择了最优的MTU(9000),通过增大Ring Buffer增强了抗突发能力,并理解了中断合并中“吞吐量”与“延迟”的永恒权衡,最终为AI训练场景找到了最佳实践——启用自-适应合并。

完成了本章的调优后,你应该重新运行第五章的基准测试。在绝大多数情况下,你会看到带宽更接近线速,延迟更低且更稳定。但这还不是终点,下一章,我们将登上调优金字塔的顶端,探讨协议和上层应用(Kubernetes, NCCL)的专属调优技巧。

(大模型训练)高性能网络(InfiniBand/RoCE) 详细学习笔记 其它章节 链接

以下是目前找到的全部章节,点击章节标题即可跳转阅读,可直接访问:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐