跳转至

ClusterHealthDetect A3 Performance

导言

这篇文章记录一次从“pod4 比 pod8 跑 Qwen3.5 397B SFT 慢约 10%”出发的集群健康定位。普通 allgather 打流没有复现差异,并不等于训练链路健康;真实慢点可能在 CPU 绑核、H2D、固定两卡 D2D、背景设备负载、rank-to-core 放置和框架调度之间。ClusterHealthDetect 的作用,是把这些变量拆成可复现实验矩阵,成为训练性能模型里的校准账本

问题边界

这次问题的直觉很容易滑向一个错误结论:既然生产 SFT 慢在 allgather,而维护侧单独 allgather 打流发现两个 pod 差不多,那就说明“不是通信问题”。这个推理缺了一层:训练里的通信不是裸通信。它发生在特定 rank、特定 CPU 调度、特定 H2D staging、特定设备背景负载和特定框架路径上。

allgather drawer matrix

自绘小黑图:裸 allgather 只是左边的单据,慢点可能藏在 CPU core、H2D、D2D 和训练态负载组成的抽屉矩阵里。

ClusterHealthDetect 因此不追求替代生产 SFT,而是回答一个更窄的问题:

  1. 这台机器哪些 CPU core 真的能被绑定,绑定后实际 affinity 是否落到指定 core。
  2. 每个 CPU core 到每张 NPU 的 H2D 带宽上限是多少,是否存在按 core 或 NPU 列分布的结构性慢点。
  3. 固定两张 NPU 时,两个 rank 绑到不同 core-pair 后 allgather/D2D 是否变化
  4. idle、cpu:N、device 背景负载下,空载链路与训练共存链路是否出现不同退化

这也是它和 Training Performance Model 的关系:MFU、显存、单步耗时给出模型级账本;ClusterHealthDetect 给出机器级校准项。没有校准项时,模型估算里的 comm_eff、H2D 下界、rank placement penalty 都只是猜测。

cluster calibration logic

自绘逻辑图:把裸 allgather、H2D、core-pair D2D 和背景负载放进同一个校准流程。

测量口径

本仓基于 torchrun 和 PyTorch distributed API。PyTorch 文档里 all_gather() 的语义是把整个 group 的 tensor 收集到列表;在 Ascend 上,HCCL 文档说明 PyTorch 用户可以通过 PyTorch 原生集合通信 API 使用嵌入在 Ascend Extension for PyTorch 后端里的 HCCL 能力,集合通信包括 AllReduce、AllGather、Broadcast 等。67

绑核使用 Linux sched_setaffinity。man page 对 affinity mask 的定义是:它决定线程可以在哪些 CPU 上运行;这种 mask 可以用于获得性能收益。8 这也是为什么只看 lscpu 的 NUMA 列表不够:虚拟环境、cpuset、容器和调度策略都可能让“看起来存在的 core”不能真的绑定,或者绑定后不是预期运行位置。

ClusterHealthDetect 的 H2D 核心逻辑是先绑到一个 CPU core,再在这个 core 上创建 host buffer,随后扫所有 NPU:

for repeat in range(args.repeats):
    for cpu_idx, cpu in enumerate(cpu_ids, 1):
        ok, actual, err = set_current_affinity({cpu})
        if not ok or cpu not in actual:
            metrics.append(metric_base(cpu, None, repeat, status="error"))
            continue
        for size_mb in sizes_mb:
            src, pinned, numel, element_size = make_host_buffer(torch, args.dtype, size_mb)
            for device_id in device_ids:
                device = set_device(torch, device_kind, device_id)
                result = measure_h2d_from_host(
                    torch, src, pinned, numel, element_size, device,
                    size_mb, args.iters, args.warmup,
                )

这段代码有两个刻意选择:一是每个 CPU core 显式 sched_setaffinity;二是 host buffer 在当前绑定 core 下触页初始化,避免把“某个旧线程分配的页”误当成当前 core 的 H2D 路径。4

固定两卡 D2D/allgather 矩阵则把 rank0 和 rank1 分别绑到一个 core,形成 rank0_cpu x rank1_cpu 的二维矩阵:

target_cpu = cpu0 if info["local_rank"] == 0 else cpu1
bind_ok, actual, bind_err = set_current_affinity({target_cpu})
rank_barrier(torch)
for size_mb in sizes_mb:
    tensor, output, bytes_per_rank = buffers[size_mb]
    result = measure_all_gather(
        torch, device, tensor, output, bytes_per_rank,
        size_mb, args.iters, args.warmup,
    )
rank_barrier(torch)

这里测到的是固定两卡、固定 tensor size 下,两个进程的 CPU 放置对 allgather algorithm GB/s 的影响。全量 640 x 640 时,默认只记录 rank0 的 collective 计时,rank1 仍参与通信和绑核,避免写双倍 JSON 拖慢收尾。5

A3 全量结果

本次 A3 机器上先跑到了一组共享环境矩阵,随后发现 H2D 和 D2D 运行时存在自动占卡任务;这些任务的工作目录包括 VeOmniMindSpeed-MM/root/g00510989/xllm_th 下的评测/训练进程。清掉 train_test_1 容器、MindSpeed-MM longcat 进程树和 xllm eval 进程树后,我在 A3-AK-182 上重跑了 H2D 与固定两卡 D2D/allgather 全量矩阵。下面的全量矩阵结论以 2026-07-07 的 clean run 为准;旧共享环境结果只作为“背景负载会污染基线”的对照。23

在全量 core matrix 之前,先用 2 rank / 2 NPU 的基础验证看 idle、device 和 cpu:1 profile 的量级。这个表回答的是“这台 A3 在当前环境下的基础路径能跑到多少”,不是逐 core 绑核结论。1

profile metric size max min 说明
idle NPU matmul TFLOPS 512 29.121 26.893 空载设备计算
device NPU matmul TFLOPS 512 24.848 15.766 设备背景负载下 rank 离散变大
idle H2D GB/s 8 MB 54.087 54.039 空载 H2D
device H2D GB/s 8 MB 47.236 46.234 设备背景负载下下降
idle D2D copy GB/s 8 MB 420.763 241.793 空载同 rank device copy,rank 间差异明显
device D2D copy GB/s 8 MB 257.846 226.128 设备背景负载下下降
idle all_gather local_host alg GB/s 8 MB 28.669 28.600 本机 2 NPU HCCL allgather
idle all_gather global alg GB/s 8 MB 27.137 27.097 单机 2 rank 下 global≈local
device all_gather local_host alg GB/s 8 MB 25.171 23.944 设备背景负载下下降
device all_gather global alg GB/s 8 MB 20.784 20.248 设备背景负载下下降
cpu:1 all_gather global alg GB/s 8 MB 30.929 30.903 CPU 背景负载未导致下降

第一组是 H2D:

OUT_DIR=results/core-h2d-matrix-clean-20260707-1552 \
CPUS=auto \
DEVICES=auto \
SIZES_MB=16,64,256 \
ITERS=3 \
WARMUP=1 \
REPEATS=1 \
CHECKPOINT_EVERY_CPUS=16 \
DEVICE_KIND=auto \
bash scripts/run_core_h2d_matrix.sh

范围是 640 CPU core x 16 NPU x 16/64/256MB,共 30720 个点,全部 ok

size min GB/s p10 GB/s median GB/s p90 GB/s max GB/s max/min
16MB 1.297 6.531 56.132 57.240 58.503 45.104
64MB 4.739 37.252 58.376 58.848 59.356 12.526
256MB 15.791 58.404 59.129 59.621 59.926 3.795

第二组是固定两卡 D2D/allgather:

OUT_DIR=results/core-pair-d2d-matrix-clean-20260707-1700 \
DEVICE_PAIR=0,5 \
RANK0_CPUS=auto \
RANK1_CPUS=auto \
SIZES_MB=8 \
ITERS=1 \
WARMUP=0 \
REPEATS=1 \
CHECKPOINT_EVERY_PAIRS=32768 \
RECORD_RANKS=rank0 \
DEVICE_KIND=auto \
bash scripts/run_core_pair_d2d_matrix.sh

范围是固定 NPU pair 0,5640 rank0 core x 640 rank1 core x 8MB,共 409600 个 core-pair,全部 ok。总体 median 是 13.714 GB/s,p10 到 p90 为 13.000-14.409 GB/s,max 是 15.967 GB/s,极端慢点低到 0.196 GB/s

A3 core matrix evidence

证据图:清理自动占卡任务后的 H2D 64MB 全量热力图、固定 NPU 0,5 的 D2D/allgather core-pair 热力图,以及 clean run 关键读数。

H2D 64MB 的最重要信号不是平均值,而是 NPU 0 的列在所有 CPU core 段上都明显低于其他 NPU。清理后旧共享环境里的“后半 core 才明显下降”变成了更清楚的 NPU0 低列:NPU0 各段 median 约 11.205-13.946 GB/s,而同段最高 NPU median 约 58.841-59.012 GB/s

CPU core 段 NPU0 median GB/s 同段最高 NPU median GB/s
0-79 12.205 59.012
80-159 12.322 58.945
160-239 12.764 58.873
240-319 11.388 58.866
320-399 12.634 58.842
400-479 11.205 58.899
480-559 13.612 58.841
560-639 13.946 58.861

A3 H2D core by NPU 64MB

ClusterHealthDetect 生成的 clean H2D 64MB 热力图:行是 CPU core,列是 NPU id。NPU 0 在全 core 范围内形成连续低带宽列。

D2D/allgather 的 640 x 640 图则说明,固定两卡通信并不是只由两张卡决定;rank0 与 rank1 的 CPU core-pair 组合也会形成块状结构。清理后,按单侧 core 聚合的慢 rank0 core median 前列包括 108, 110, 106, 107, 104;慢 rank1 core 包括 80, 0, 240, 400, 320。这不等于这些 core 永远“坏”,但足以说明生产 launch map 如果落在这些区域,裸 allgather 的单点结论会漏掉风险。

A3 D2D core pair 8MB

ClusterHealthDetect 生成的 clean 固定两卡 D2D/allgather 8MB 热力图:行是 rank0 CPU core,列是 rank1 CPU core。

不要把 cpu:1 当成绑核

cpu:1 profile 只是每个 rank 旁边多启动一个 CPU burner,制造调度、cache 和内存带宽压力。它不是把进程绑到 CPU 1。若 cpu:1idle 更高,常见解释是采样窗口、预热、HCCL 算法状态或调度时机变化;绑核结论只能看显式 sched_setaffinity 的矩阵。

建模含义

Roofline 模型把性能上限拆成 compute ceiling、memory bandwidth ceiling 和 operational intensity,重点不是“画一条线”,而是用物理上限解释实际程序落在哪个瓶颈区。9 对训练系统也是类似的:ClusterHealthDetect 给出的不是端到端 step time,而是通信账和校准账里的上限与异常块。

我会把这类结果放入性能模型的四个字段:

字段 ClusterHealthDetect 产物 用途
h2d_gbps_by_core_device core-h2d-matrix.xlsx/png 校准 host staging、CPU/NPU 距离和 H2D 下界
d2d_alg_gbps_by_core_pair core-pair-d2d-matrix.xlsx/png 校准固定两卡 collective 对 rank placement 的敏感性
affinity_bindable_cpus affinity.json / Affinity sheet 判断 launch map 是否真的能绑到目标 core
load_profile_delta idle/cpu:N/device profile 区分空载链路与训练共存链路

Megatron-LM 论文提醒我们,大规模训练吞吐来自 tensor parallel、pipeline parallel 和 data parallel 等并行维度的组合,通信、bubble 和 microbatch 都会改变效率。10 因此,一个 pod-to-pod 性能差异不应该只归因到“卡间通信”或“模型算力”。更稳的排查顺序是:

  1. 裸通信先验:local/global allgather、P2P、跨机链路是否已经不同。
  2. 主机路径校准:H2D/D2H 是否按 CPU core 和 NPU id 出现结构性差异。
  3. 进程放置校准:固定两卡 D2D/allgather 是否随 rank0/rank1 core-pair 变化。
  4. 训练态压力:CPU burner 或 device burner 是否让空载看不出的慢点出现。
  5. 框架路径:如果硬件矩阵平坦,再去看 padding、overlap、bucket、stream、graph capture、runtime launch 和模型并行调度。

如何使用热力图

不要先相信 NUMA,再用实验去证明 NUMA。更好的顺序是:每个 core 先实测,再把行号按 0-79/80-159/... 这类 NUMA 段解释。如果图里有连续块状边界,NUMA 是候选解释;如果图里是稀疏红点,优先怀疑调度、背景进程、采样和局部资源竞争。

复用流程

我把这次经验沉淀到了 obsidian-vault/wiki/meta/Cluster Health Calibration Rule.md。以后遇到 “训练慢,但单独打流正常” 的问题,可以直接复用下面这段提示词:

请按集群健康校准方法诊断下面的训练性能差异:

1. 先记录生产现象:模型、训练阶段、并行配置、pod/节点、慢多少、发生日期。
2. 不要只看裸 allgather;同时测 CPU 绑核可行性、CPU core x NPU 的 H2D、固定两卡 rank0-core x rank1-core 的 D2D/allgather。
3. 对 idle、cpu:N、device 背景负载分别跑,区分空载链路和训练共存链路。
4. 输出 Excel 与 PNG 热力图,并用每个 core 的实测结果解释 NUMA,不要假设 NUMA 配置可信。
5. 把源码事实、实验命令、原始结果、热力图结论和仍未验证的推断分开写。
6. 如果裸 allgather 正常但训练慢,优先检查 H2D、绑核、rank placement、背景负载和框架调度。

这篇文章的核心判断可以压缩成一句话:allgather 是训练系统的一段路径,不是训练系统本身。当生产任务慢而裸打流正常时,下一步不是结束排查,而是把打流升级为矩阵化校准。

参考文献


  1. ClusterHealthDetect A3 validation report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-validation.md 

  2. ClusterHealthDetect A3 clean core matrix retest report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-clean-core-matrix-report.md 

  3. ClusterHealthDetect A3 full core matrix report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-full-core-matrix-report.md 

  4. ClusterHealthDetect core_h2d_matrix.py, commit fd21db14964815c31a1588926a0d3a9da87e358e. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/fd21db14964815c31a1588926a0d3a9da87e358e/cluster_health_detect/core_h2d_matrix.py 

  5. ClusterHealthDetect core_pair_d2d_matrix.py, commit fd21db14964815c31a1588926a0d3a9da87e358e. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/fd21db14964815c31a1588926a0d3a9da87e358e/cluster_health_detect/core_pair_d2d_matrix.py 

  6. PyTorch distributed communication package documentation, all_gather() section. https://docs.pytorch.org/docs/2.12/distributed.html#torch.distributed.all_gather 

  7. Ascend CANN HCCL communication development guide. https://www.hiascend.com/document/detail/zh/canncommercial/80RC3/developmentguide/hccl/hcclug/hcclug_000008.html 

  8. Linux man-pages, sched_setaffinity(2). https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html 

  9. Williams, Waterman, Patterson, "Roofline: An Insightful Visual Performance Model for Multicore Architectures", CACM 2009. https://people.eecs.berkeley.edu/~kubitron/courses/cs252-S12/papers/RooflineCACM.pdf 

  10. Narayanan et al., "Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM". https://arxiv.org/abs/2104.04473 

评论