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](https://pic.shaojiemike.top/shaojiemike/2026/07/9f42b5245c8cc832aaf7733ce94fa274.png){ width=90% }
自绘小黑图:裸 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](https://pic.shaojiemike.top/shaojiemike/2026/07/75e659648d63feed83a3484752398fec.png){ width=90% }
自绘逻辑图:把裸 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 等。[^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
2
3
4
5
6
7
8
9
10
11
12
13
14
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 路径。[^chd-h2d]

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

1
2
3
4
5
6
7
8
9
10
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 拖慢收尾。[^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
2
3
4
5
6
7
8
9
10
OUT_DIR=results/core-h2d-matrix-full-20260706 \
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 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
2
3
4
5
6
7
8
9
10
11
12
OUT_DIR=results/core-pair-d2d-matrix-full-20260706 \
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.523 GB/s,max 是 15.864 GB/s,极端慢点低到 0.175 GB/s

![A3 core matrix evidence](https://pic.shaojiemike.top/shaojiemike/2026/07/f2e8da28f172465dff9a7085cc78ff41.png){ width=95% }
证据图:左侧为 H2D 64MB 全量热力图,右侧为固定 NPU 0,5 的 D2D/allgather core-pair 热力图,右栏列出关键读数。

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
![A3 H2D core by NPU 64MB](https://pic.shaojiemike.top/shaojiemike/2026/07/b75bd5660ba9bb3d66447efc28a82538.png){ width=75% }
ClusterHealthDetect 生成的 H2D 64MB 热力图:行是 CPU core,列是 NPU id。NPU 0 在后半 CPU core 上形成连续低带宽列。

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 的单点结论会漏掉风险。

![A3 D2D core pair 8MB](https://pic.shaojiemike.top/shaojiemike/2026/07/fde2b851425093b32b4a41c76bee0661.png){ width=90% }
ClusterHealthDetect 生成的固定两卡 D2D/allgather 8MB 热力图:行是 rank0 CPU core,列是 rank1 CPU core。

不要把 cpu

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

建模含义

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 性能差异不应该只归因到“卡间通信”或“模型算力”。更稳的排查顺序是:

  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
2
3
4
5
6
7
8
请按集群健康校准方法诊断下面的训练性能差异:

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 是训练系统的一段路径,不是训练系统本身。当生产任务慢而裸打流正常时,下一步不是结束排查,而是把打流升级为矩阵化校准。

参考文献

[^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

Author

Shaojie Tan

Posted on

2026-07-07

Updated on

2026-07-07

Licensed under