ClusterHealthDetect A3 Performance
问题边界
这次问题的直觉很容易滑向一个错误结论:既然生产 SFT 慢在 allgather,而维护侧单独 allgather 打流发现两个 pod 差不多,那就说明“不是通信问题”。这个推理缺了一层:训练里的通信不是裸通信。它发生在特定 rank、特定 CPU 调度、特定 H2D staging、特定设备背景负载和特定框架路径上。
ClusterHealthDetect 因此不追求替代生产 SFT,而是回答一个更窄的问题:
- 这台机器哪些 CPU core 真的能被绑定,绑定后实际 affinity 是否落到指定 core。
- 每个 CPU core 到每张 NPU 的 H2D 带宽上限是多少,是否存在按 core 或 NPU 列分布的结构性慢点。
- 固定两张 NPU 时,两个 rank 绑到不同 core-pair 后 allgather/D2D 是否变化。
- idle、cpu:N、device 背景负载下,空载链路与训练共存链路是否出现不同退化。
这也是它和 Training Performance Model 的关系:MFU、显存、单步耗时给出模型级账本;ClusterHealthDetect 给出机器级校准项。没有校准项时,模型估算里的 comm_eff、H2D 下界、rank placement penalty 都只是猜测。
测量口径
本仓基于 torchrun 和 PyTorch distributed API。PyTorch 文档里 all_gather() 的语义是把整个 group 的 tensor 收集到列表;在 Ascend 上,HCCL 文档说明 PyTorch 用户可以通过 PyTorch 原生集合通信 API 使用嵌入在 Ascend Extension for PyTorch 后端里的 HCCL 能力,集合通信包括 AllReduce、AllGather、Broadcast 等。[^pytorch-dist][^hccl-guide]
绑核使用 Linux sched_setaffinity。man page 对 affinity mask 的定义是:它决定线程可以在哪些 CPU 上运行;这种 mask 可以用于获得性能收益。[^sched-affinity] 这也是为什么只看 lscpu 的 NUMA 列表不够:虚拟环境、cpuset、容器和调度策略都可能让“看起来存在的 core”不能真的绑定,或者绑定后不是预期运行位置。
ClusterHealthDetect 的 H2D 核心逻辑是先绑到一个 CPU core,再在这个 core 上创建 host buffer,随后扫所有 NPU:
1 | for repeat in range(args.repeats): |
这段代码有两个刻意选择:一是每个 CPU core 显式 sched_setaffinity;二是 host buffer 在当前绑定 core 下触页初始化,避免把“某个旧线程分配的页”误当成当前 core 的 H2D 路径。[^chd-h2d]
固定两卡 D2D/allgather 矩阵则把 rank0 和 rank1 分别绑到一个 core,形成 rank0_cpu x rank1_cpu 的二维矩阵:
1 | target_cpu = cpu0 if info["local_rank"] == 0 else cpu1 |
这里测到的是固定两卡、固定 tensor size 下,两个进程的 CPU 放置对 allgather algorithm GB/s 的影响。全量 640 x 640 时,默认只记录 rank0 的 collective 计时,rank1 仍参与通信和绑核,避免写双倍 JSON 拖慢收尾。[^chd-d2d]
A3 全量结果
本次 A3 机器上跑了两组全量矩阵。注意:运行时 npu-smi 记录到部分 NPU 上有外部 xllm 进程,所以这些结果是共享环境实测,不是空载裸机峰值。它适合用于诊断这台机器当前的训练环境,而不适合当成 Ascend 910B A+X/A2 类机器的公开规格。[^chd-report]
在全量 core matrix 之前,先用 2 rank / 2 NPU 的基础验证看 idle、device 和 cpu:1 profile 的量级。这个表回答的是“这台 A3 在当前环境下的基础路径能跑到多少”,不是逐 core 绑核结论。[^chd-validation]
| 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:
1 | OUT_DIR=results/core-h2d-matrix-full-20260706 \ |
范围是 640 CPU core x 16 NPU x 16/64/256MB,共 30720 个点,全部 ok。
| size | min GB/s | median GB/s | max GB/s | max/min |
|---|---|---|---|---|
| 16MB | 0.545 | 52.549 | 56.490 | 103.736 |
| 64MB | 1.582 | 56.672 | 59.340 | 37.501 |
| 256MB | 7.748 | 58.380 | 59.952 | 7.738 |
第二组是固定两卡 D2D/allgather:
1 | OUT_DIR=results/core-pair-d2d-matrix-full-20260706 \ |
范围是固定 NPU pair 0,5,640 rank0 core x 640 rank1 core x 8MB,共 409600 个 core-pair,全部 ok。总体 median 是 13.523 GB/s,max 是 15.864 GB/s,极端慢点低到 0.175 GB/s。
H2D 64MB 的最重要信号不是平均值,而是 NPU 0 的列在 CPU core 320-639 区间出现连续低带宽:
| CPU core 段 | NPU0 median GB/s | 同段最高 NPU median GB/s |
|---|---|---|
| 0-79 | 55.674 | 57.975 |
| 80-159 | 55.505 | 57.684 |
| 160-239 | 55.082 | 57.888 |
| 240-319 | 55.282 | 57.801 |
| 320-399 | 12.362 | 57.555 |
| 400-479 | 5.819 | 57.491 |
| 480-559 | 6.189 | 57.652 |
| 560-639 | 5.875 | 57.524 |
D2D/allgather 的 640 x 640 图则说明,固定两卡通信并不是只由两张卡决定;rank0 与 rank1 的 CPU core-pair 组合也会形成块状结构。慢 rank0 core 的 median 排名前列包括 65, 316, 95, 93, 81;慢 rank1 core 包括 80, 320, 436, 218, 400。这不等于这些 core 永远“坏”,但足以说明生产 launch map 如果落在这些区域,裸 allgather 的单点结论会漏掉风险。
建模含义
Roofline 模型把性能上限拆成 compute ceiling、memory bandwidth ceiling 和 operational intensity,重点不是“画一条线”,而是用物理上限解释实际程序落在哪个瓶颈区。[^roofline] 对训练系统也是类似的: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 都会改变效率。[^megatron] 因此,一个 pod-to-pod 性能差异不应该只归因到“卡间通信”或“模型算力”。更稳的排查顺序是:
- 裸通信先验:local/global allgather、P2P、跨机链路是否已经不同。
- 主机路径校准:H2D/D2H 是否按 CPU core 和 NPU id 出现结构性差异。
- 进程放置校准:固定两卡 D2D/allgather 是否随 rank0/rank1 core-pair 变化。
- 训练态压力:CPU burner 或 device burner 是否让空载看不出的慢点出现。
- 框架路径:如果硬件矩阵平坦,再去看 padding、overlap、bucket、stream、graph capture、runtime launch 和模型并行调度。
复用流程
我把这次经验沉淀到了 obsidian-vault/wiki/meta/Cluster Health Calibration Rule.md。以后遇到 “训练慢,但单独打流正常” 的问题,可以直接复用下面这段提示词:
1 | 请按集群健康校准方法诊断下面的训练性能差异: |
这篇文章的核心判断可以压缩成一句话:allgather 是训练系统的一段路径,不是训练系统本身。当生产任务慢而裸打流正常时,下一步不是结束排查,而是把打流升级为矩阵化校准。
参考文献
[^chd-validation]: ClusterHealthDetect A3 validation report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-validation.md
[^chd-report]: ClusterHealthDetect A3 full core matrix report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-full-core-matrix-report.md
[^chd-h2d]: ClusterHealthDetect core_h2d_matrix.py, commit fd21db14964815c31a1588926a0d3a9da87e358e. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/fd21db14964815c31a1588926a0d3a9da87e358e/cluster_health_detect/core_h2d_matrix.py
[^chd-d2d]: ClusterHealthDetect core_pair_d2d_matrix.py, commit fd21db14964815c31a1588926a0d3a9da87e358e. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/fd21db14964815c31a1588926a0d3a9da87e358e/cluster_health_detect/core_pair_d2d_matrix.py
[^pytorch-dist]: PyTorch distributed communication package documentation, all_gather() section. https://docs.pytorch.org/docs/2.12/distributed.html#torch.distributed.all_gather
[^hccl-guide]: Ascend CANN HCCL communication development guide. https://www.hiascend.com/document/detail/zh/canncommercial/80RC3/developmentguide/hccl/hcclug/hcclug_000008.html
[^sched-affinity]: Linux man-pages, sched_setaffinity(2). https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html
[^roofline]: 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
[^megatron]: Narayanan et al., “Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM”. https://arxiv.org/abs/2104.04473
ClusterHealthDetect A3 Performance