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、特定设备背景负载和特定框架路径上。
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 等。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 运行时存在自动占卡任务;这些任务的工作目录包括 VeOmni、MindSpeed-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,5,640 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。
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 |
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 的单点结论会漏掉风险。
不要把 cpu:1 当成绑核
cpu:1 profile 只是每个 rank 旁边多启动一个 CPU burner,制造调度、cache 和内存带宽压力。它不是把进程绑到 CPU 1。若 cpu:1 比 idle 更高,常见解释是采样窗口、预热、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 性能差异不应该只归因到“卡间通信”或“模型算力”。更稳的排查顺序是:
- 裸通信先验: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 和模型并行调度。
如何使用热力图
不要先相信 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 是训练系统的一段路径,不是训练系统本身。当生产任务慢而裸打流正常时,下一步不是结束排查,而是把打流升级为矩阵化校准。
参考文献¶
-
ClusterHealthDetect A3 validation report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-validation.md ↩
-
ClusterHealthDetect A3 clean core matrix retest report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-clean-core-matrix-report.md ↩
-
ClusterHealthDetect A3 full core matrix report. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/main/reports/A3-full-core-matrix-report.md ↩
-
ClusterHealthDetect
core_h2d_matrix.py, commitfd21db14964815c31a1588926a0d3a9da87e358e. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/fd21db14964815c31a1588926a0d3a9da87e358e/cluster_health_detect/core_h2d_matrix.py ↩ -
ClusterHealthDetect
core_pair_d2d_matrix.py, commitfd21db14964815c31a1588926a0d3a9da87e358e. https://github.com/Kirrito-k423/ClusterHealthDetect/blob/fd21db14964815c31a1588926a0d3a9da87e358e/cluster_health_detect/core_pair_d2d_matrix.py ↩ -
PyTorch distributed communication package documentation,
all_gather()section. https://docs.pytorch.org/docs/2.12/distributed.html#torch.distributed.all_gather ↩ -
Ascend CANN HCCL communication development guide. https://www.hiascend.com/document/detail/zh/canncommercial/80RC3/developmentguide/hccl/hcclug/hcclug_000008.html ↩
-
Linux man-pages,
sched_setaffinity(2). https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html ↩ -
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 ↩
-
Narayanan et al., "Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM". https://arxiv.org/abs/2104.04473 ↩




