Compare commits
2 Commits
a583c337ba
...
4892c0b14d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4892c0b14d | ||
|
|
30664faa41 |
@ -1,5 +1,9 @@
|
||||
# sskj — 多平台大模型推理性能基准测试项目
|
||||
|
||||
> **更新(2026-07-31 15:33:02 CST)**
|
||||
>
|
||||
> 完成 DeepSeek-V4-Pro 双机 Pro6000D SGLang Phase 2 最终采集代码。提交 `30664faa41f8` 新增正式 benchmark 精确时间窗、双节点 DCGM fail-closed 门禁、进程级 5 秒 Host 采样、机内 PCIe P2P 全矩阵、单机 8-rank 与双机 16-rank AllReduce、`NCCL_CROSS_NIC=0/1/2` A/B,以及与 Phase 2 第 5 节逐项对应的结构化 CSV 和 `report.md`。通信/NCCL 术语档案同步补充 DCGM、Host Engine、SM、Warp、NUMA、NIC、PIX/SYS、algbw/busbw 与 Linux 监控工具;最终双机复跑前不进入 Phase 3。
|
||||
>
|
||||
> **更新(2026-07-31 14:15:00 CST)**
|
||||
>
|
||||
> 恢复并完善 Phase 2 档案中的采集命令与指标解释。第 5 节现按实际实现记录时间 Marker、`nvidia-smi`、DCGM Field 1001–1005/1009/1010、`mpstat`、`pidstat`、`perf`、`numastat`、`sar`、`ethtool` 和 `mlx5_0/mlx5_3` HCA Counter,并逐项说明字段含义、分析方法及对应原始/汇总文件。同步记录首轮线程级 1 秒 `pidstat` 日志过重,后续应改为进程级 5 秒采样。
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>6000D 双机通信与 NCCL 术语入门</title>
|
||||
<title>6000D 双机通信、NCCL 与 Profiling 术语入门</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
@ -259,11 +259,11 @@
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<p class="eyebrow">Two-node communication primer</p>
|
||||
<h1>6000D 双机通信与 NCCL 术语入门</h1>
|
||||
<h1>6000D 双机通信、NCCL 与 Profiling 术语入门</h1>
|
||||
<div class="header-meta">
|
||||
<span>节点:174.1.51.5 + 174.1.51.7</span>
|
||||
<span>规模:16 GPU / TP16</span>
|
||||
<span>版本:2026-07-30 17:54 CST</span>
|
||||
<span>版本:2026-07-31 15:25 CST</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -447,6 +447,106 @@ mlx5_3 port 1 ==> eth3 (Up)</code></pre>
|
||||
<td>RoCE 网络控制拥塞和丢包的机制。</td>
|
||||
<td>RDMA 出现 retry、pause 或吞吐抖动时由运维检查。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>NIC</strong></td>
|
||||
<td>Network Interface Card,网卡的统称。它可以暴露普通 IP 接口,也可以提供 RDMA 能力。</td>
|
||||
<td><code>eth0/eth3</code> 是 Linux netdev 名;对应的 RDMA HCA 名是 <code>mlx5_0/mlx5_3</code>。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>NUMA</strong></td>
|
||||
<td>Non-Uniform Memory Access。双路 CPU 机器中,每个 CPU 访问本地内存更快,访问另一侧内存更慢。</td>
|
||||
<td>服务线程、GPU 和 NIC 若跨 NUMA 节点配合,可能增加 Host 侧延迟和 PCIe 路径长度。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>CUDA P2P / IPC</strong></td>
|
||||
<td>P2P 让同机 GPU 直接互访显存;IPC 让不同进程共享可访问的 GPU 内存句柄。</td>
|
||||
<td>6000D 无 NVLink,单机 8 卡的 NCCL P2P/IPC 实际经过 PCIe。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>PIX / SYS</strong></td>
|
||||
<td>NVIDIA 拓扑标签。PIX 表示 GPU 间只跨一个 PCIe Switch;SYS 表示还要跨 CPU/NUMA 互联。</td>
|
||||
<td>GPU0–3、GPU4–7 各自多为 PIX,两组之间为 SYS;P2P 微基准会分别汇总这两类路径。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>AllReduce</strong></td>
|
||||
<td>所有 rank 先归约数据,再让每个 rank 都拿到相同结果的 collective。</td>
|
||||
<td>TP16 高频使用;Phase 2 分别测单机 8 rank 和双机 16 rank。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>algbw / busbw</strong></td>
|
||||
<td><code>algbw</code> 是有效数据量除以操作时间;<code>busbw</code> 再按 collective 的理论链路流量换算,便于比较硬件通信效率。</td>
|
||||
<td>AllReduce 使用 <code>busbw = algbw × 2 × (N-1) / N</code>。两者单位通常为 GB/s,不能与 400 Gbit/s 直接混用。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>DCGM</strong></td>
|
||||
<td>NVIDIA Data Center GPU Manager,一套 GPU 健康、遥测和诊断框架。它比 <code>nvidia-smi</code> 提供更细的 GPU 活跃度计数器。</td>
|
||||
<td>Phase 2 用它采集 SM、Tensor、设备显存接口和 PCIe 活跃度;它不是 Nsight Timeline。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>DCGM Host Engine</strong></td>
|
||||
<td>DCGM 的后台服务,负责连接驱动、维护 GPU 清单并提供指标。systemd 服务通常叫 <code>nvidia-dcgm</code>,底层进程是 <code>nv-hostengine</code>。</td>
|
||||
<td>两节点都必须运行;否则 <code>dcgmi dmon</code> 客户端存在也无法采集。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>dcgmi / Field ID</strong></td>
|
||||
<td><code>dcgmi</code> 是 DCGM 命令行客户端;Field ID 是某个遥测指标的数字编号。</td>
|
||||
<td>Phase 2 使用 1001–1005、1009、1010,并把缺失样本保留为 <code>-</code>,不会当成 0。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>SM</strong></td>
|
||||
<td>Streaming Multiprocessor,GPU 执行 CUDA Warp、Tensor Core 指令和大部分计算的基本处理单元。</td>
|
||||
<td><code>sm_active</code> 高说明 SM 经常在工作,但不等于每个 SM 都满负载。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Warp</strong></td>
|
||||
<td>NVIDIA GPU 同步执行的一组线程,通常包含 32 个 CUDA 线程。</td>
|
||||
<td><code>sm_occupancy</code> 反映活跃 Warp 相对硬件可容纳 Warp 的比例。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>SM Active / Occupancy</strong></td>
|
||||
<td>前者回答“SM 有多少时间在工作”,后者回答“工作时驻留了多少 Warp”。</td>
|
||||
<td>Active 高、Occupancy 低可能来自小 Kernel、资源约束或同步,必须结合后续 Timeline 判断。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Tensor Active</strong></td>
|
||||
<td>Tensor Core 管线处于活跃状态的时间比例。</td>
|
||||
<td>用于判断矩阵计算单元是否被充分使用;它不是模型总 FLOPS 利用率。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>DRAM Active</strong></td>
|
||||
<td>DCGM 的历史字段名,表示 GPU 设备显存接口活跃比例,不限定显存必须是主机 DRAM 或 HBM。</td>
|
||||
<td>Pro6000D 使用 GDDR7;该指标仍用于观察设备显存带宽压力。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>测量窗口 / Epoch</strong></td>
|
||||
<td>Epoch 是统一的 Unix 时间基准;测量窗口是正式 benchmark 开始到结束的精确时间段。</td>
|
||||
<td>Phase 2 用 <code>Starting main benchmark run</code> 加 benchmark duration 切片,排除数据准备和 Warm-up。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>mpstat</strong></td>
|
||||
<td>查看整机和每个逻辑 CPU 的利用率、I/O Wait 等。</td>
|
||||
<td>回答是否整机 CPU 饱和,或只有少数核心成为热点。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>pidstat</strong></td>
|
||||
<td>按进程统计 CPU、内存、I/O、缺页和上下文切换。</td>
|
||||
<td>Phase 2 使用进程级 5 秒采样,避免旧版线程级 1 秒采样产生数百 MB 日志。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>sar</strong></td>
|
||||
<td>sysstat 套件中的系统活动记录工具,可采集网卡吞吐和错误。</td>
|
||||
<td>Phase 2 只看计算网 <code>eth0/eth3</code>,与 HCA RDMA Counter 分层比较。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>perf stat</strong></td>
|
||||
<td>Linux 性能计数器工具,统计 CPU cycles、instructions、cache miss、迁移和缺页。</td>
|
||||
<td>用于判断 Host 进程是否受 CPU 执行、Cache 或调度开销限制,不提供 GPU Kernel 时间线。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>numastat</strong></td>
|
||||
<td>查看系统或进程在各 NUMA 节点上的内存分布。</td>
|
||||
<td>Phase 2 每 5 秒保存结构化 Node0/Node1 MiB,寻找跨 NUMA 内存放置。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@ -105,481 +105,306 @@
|
||||
<p class="eyebrow">Standalone Code Walkthrough / Phase 2</p>
|
||||
<h1>DSV4-Pro 双机 Pro6000D SGLang 硬件竞争归因:代码详解</h1>
|
||||
<div class="meta">
|
||||
行号基线:<code>ca1f2f63375c</code>
|
||||
生成时间:2026-07-31 13:04:21 CST
|
||||
入口:<code>run_hardware_contention_attribution.sh</code>
|
||||
行号基线:<code>30664faa41f8</code>
|
||||
生成时间:2026-07-31 15:25:00 CST
|
||||
唯一入口:<code>run_hardware_contention_attribution.sh all</code>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="callout">
|
||||
<strong>文档边界:</strong>这是一份独立代码档案。它解释 Phase 2 如何复用 Phase 1 请求、
|
||||
同步采集两节点监控,并按真实 Case 时间窗做硬件归因。所有行号绑定提交
|
||||
<code>ca1f2f63375c</code>。
|
||||
<strong>文档边界:</strong>本文只解释提交 <code>30664faa41f8</code> 的 Phase 2
|
||||
代码和文件调用关系。Phase 1 负责模型服务与请求;Phase 2 负责通信基线、
|
||||
两节点监控、精确时间切片和逐指标报告。
|
||||
</div>
|
||||
|
||||
<h2 id="read">1. 阅读导航</h2>
|
||||
<ul class="toc">
|
||||
<li><a href="#flow">总体控制流</a></li>
|
||||
<li><a href="#files">文件职责</a></li>
|
||||
<li><a href="#reuse">Phase 1 复用边界</a></li>
|
||||
<li><a href="#preflight">预检与时钟</a></li>
|
||||
<li><a href="#files">文件职责与调用关系</a></li>
|
||||
<li><a href="#config">配置来源</a></li>
|
||||
<li><a href="#communication">通信微基准</a></li>
|
||||
<li><a href="#collectors">采集器实现</a></li>
|
||||
<li><a href="#cases">Case 编排</a></li>
|
||||
<li><a href="#alignment">时间窗对齐</a></li>
|
||||
<li><a href="#outputs">结构化输出</a></li>
|
||||
<li><a href="#alignment">精确测量窗口</a></li>
|
||||
<li><a href="#report">逐指标报告</a></li>
|
||||
<li><a href="#index">函数行号索引</a></li>
|
||||
</ul>
|
||||
|
||||
<h2 id="flow">2. 总体控制流</h2>
|
||||
<pre><code>main "$@"
|
||||
└─ ACTION=all → run_all
|
||||
├─ validate_config
|
||||
├─ preflight_node_tools + preflight_clock_sync
|
||||
├─ start_service
|
||||
│ └─ 委托 Phase 1 的 start
|
||||
├─ capture_static_snapshots before
|
||||
├─ start_collectors
|
||||
│ └─ Head 与 Worker 各启动 9 类采集器
|
||||
├─ idle baseline
|
||||
├─ 固定诊断 Case
|
||||
│ └─ 每个 Case 委托 Phase 1 的 fixed
|
||||
├─ 混合 Prefill/Decode A/B
|
||||
│ └─ 委托 Phase 1 的 mixed
|
||||
├─ cooldown
|
||||
├─ stop_collectors
|
||||
├─ capture_static_snapshots after
|
||||
├─ stop_service
|
||||
├─ summarize_results
|
||||
│ └─ 按每个 Case 的 started_at/ended_at 切监控窗口
|
||||
└─ finish_manifest</code></pre>
|
||||
<pre><code>main "$@" → run_all
|
||||
├─ validate_config
|
||||
├─ preflight_node_tools
|
||||
│ └─ 两节点 dcgmi discovery -l 必须成功
|
||||
├─ preflight_clock_sync + preflight_gpus_idle
|
||||
├─ run_communication_baseline
|
||||
│ ├─ 两节点 CUDA P2P 全矩阵
|
||||
│ ├─ 两节点各自 8-rank AllReduce
|
||||
│ └─ 16-rank AllReduce,CROSS_NIC=0/1/2
|
||||
├─ start_service → Phase 1 start
|
||||
├─ capture_static_snapshots before
|
||||
├─ start_collectors → Head/Worker 同时采集
|
||||
├─ idle → fixed cases → mixed A/B → cooldown
|
||||
├─ check_collectors + stop_collectors
|
||||
├─ capture_static_snapshots after
|
||||
├─ stop_service → Phase 1 stop
|
||||
├─ summarize_results
|
||||
│ └─ 按正式 benchmark 窗口生成第 5 节逐项数据表
|
||||
└─ finish_manifest</code></pre>
|
||||
<p>
|
||||
Phase 2 不复制模型部署和 benchmark 生成代码。它新增的是诊断编排层:
|
||||
在同一个服务生命周期中,让两节点的 GPU、CPU、NUMA、网络和 RDMA 时间序列包围 benchmark,
|
||||
最后按 Case 时间切片。
|
||||
<code>all</code> 是唯一正式入口。<code>communication</code>、<code>summarize</code>
|
||||
和 <code>stop</code> 是排错/恢复 action,不需要在正常执行前手工调用。
|
||||
</p>
|
||||
|
||||
<h2 id="files">3. 文件职责</h2>
|
||||
<h2 id="files">3. 文件职责与调用关系</h2>
|
||||
<table>
|
||||
<thead><tr><th>文件</th><th>行数</th><th>职责</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="path">run_hardware_contention_attribution.sh</td><td>689</td>
|
||||
<td>唯一入口,调用 Phase 1 服务/请求,管理所有采集器、静态快照、Case marker 与清理。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="path">config.env</td><td>40</td>
|
||||
<td>定义 Phase 1 相对路径、诊断 Case、采样间隔、采集命令和结果路径。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="path">hardware_contention_attribution.py</td><td>574</td>
|
||||
<td>记录 Manifest/marker,汇总 GPU 与 RDMA,按 Case 时间窗切片并生成报告。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="path">tests/test_hardware_contention_attribution.py</td><td>197</td>
|
||||
<td>验证 GPU/RDMA 汇总、时间窗切片和结果生成的纯 Python 逻辑。</td>
|
||||
</tr>
|
||||
<tr><td class="path">config.env</td><td>61</td><td>节点、Case、分层采样周期、通信尺寸、NCCL 选择和 fail-closed 策略。</td></tr>
|
||||
<tr><td class="path">run_hardware_contention_attribution.sh</td><td>968</td><td>唯一 Shell 编排器:预检、通信基线、Phase 1 委托、采集器、Case 和清理。</td></tr>
|
||||
<tr><td class="path">communication_baseline.py</td><td>227</td><td>CUDA P2P 全矩阵及 PyTorch/NCCL AllReduce 正确性、延迟和带宽测试。</td></tr>
|
||||
<tr><td class="path">hardware_contention_attribution.py</td><td>1480</td><td>解析所有原始采集器,按 Case 切片,聚合通信并生成 CSV/JSON/report.md。</td></tr>
|
||||
<tr><td class="path">tests/test_hardware_contention_attribution.py</td><td>308</td><td>8 项纯 Python 单元测试,覆盖精确窗口、解析器、RDMA 单位和通信聚合。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>3.1 Phase 2 与 Phase 1 的文件关系</h3>
|
||||
<pre><code>用户
|
||||
└─ bash Phase2/run_hardware_contention_attribution.sh all
|
||||
└─ Phase2/run_hardware_contention_attribution.sh all
|
||||
├─ source Phase2/config.env
|
||||
│ └─ PHASE1_ENTRY 指向 Phase1/run_quick_map.sh
|
||||
├─ env ... bash "${PHASE1_ENTRY}" start/fixed/mixed/stop
|
||||
│ └─ Phase1/run_quick_map.sh
|
||||
│ ├─ source Phase1/config.env
|
||||
│ ├─ 读取 Phase1/quick_map_scenarios.tsv
|
||||
│ └─ 调用 Phase1/quick_map_results.py
|
||||
├─ 自己启动两节点硬件采集器
|
||||
└─ 调用 Phase2/hardware_contention_attribution.py
|
||||
├─ 读取 Phase1 生成的 bench/*/cases/*/meta.json
|
||||
├─ 读取 Phase2 生成的 GPU/RDMA 时间序列
|
||||
└─ 按 Case 时间窗生成归因汇总</code></pre>
|
||||
<table>
|
||||
<thead><tr><th>上游</th><th>下游</th><th>代码连接点</th><th>关系</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Phase 2 <code>config.env</code></td><td>Phase 2 Shell</td>
|
||||
<td><code>run_hardware_contention_attribution.sh:L8</code></td><td>提供诊断 Case、采样策略和 Phase 1 相对入口。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phase 2 Shell</td><td>Phase 1 Shell</td>
|
||||
<td><code>run_hardware_contention_attribution.sh:L195-L213</code></td><td>通过环境变量和 action 委托服务与请求。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phase 1 <code>config.env</code></td><td>Phase 1 Shell</td>
|
||||
<td><code>run_quick_map.sh:L8</code></td><td>提供实际模型服务参数,包括 <code>MEM_FRACTION_STATIC</code>。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phase 1 Case 结果</td><td>Phase 2 Python</td>
|
||||
<td><code>hardware_contention_attribution.py:L213-L260</code></td><td>提供 benchmark 指标和精确开始/结束时间。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phase 2 Shell 采集器</td><td>Phase 2 Python</td>
|
||||
<td><code>hardware_contention_attribution.py:L276-L321</code></td><td>提供两节点 GPU/RDMA 时间序列供 Case 切片。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Phase 2 单元测试</td><td>Phase 2 Python</td>
|
||||
<td><code>tests/test_hardware_contention_attribution.py</code></td><td>用合成监控数据验证 delta、速率和时间窗。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
这里有两套 <code>config.env</code>,职责不同。Phase 2 的配置控制“测哪些 Case、如何监控”;
|
||||
Phase 1 的配置控制“模型如何部署、请求如何生成”。Phase 2 没有复制
|
||||
<code>MEM_FRACTION_STATIC</code>,因此它最终仍从 Phase 1 的
|
||||
<code>config.env:L38</code> 取得默认值。
|
||||
</p>
|
||||
├─ docker/torchrun → Phase2/communication_baseline.py
|
||||
├─ env ... bash Phase1/run_quick_map.sh start/fixed/mixed/stop
|
||||
│ └─ Phase1/quick_map_results.py 写 benchmark meta
|
||||
├─ Shell 采集 Head/Worker 原始时间序列
|
||||
└─ Phase2/hardware_contention_attribution.py summarize
|
||||
├─ 读取 Phase1 bench/cases/*/meta.json
|
||||
├─ 读取 Head/Worker 原始监控
|
||||
├─ 读取 communication/COMM_RESULT
|
||||
└─ 输出逐 Case、逐节点、逐指标表和 report.md</code></pre>
|
||||
|
||||
<h3>3.1 config.env 分区</h3>
|
||||
<h3>3.1 Phase 1 与 Phase 2 的边界</h3>
|
||||
<table>
|
||||
<thead><tr><th>范围</th><th>内容</th><th>说明</th></tr></thead>
|
||||
<thead><tr><th>问题</th><th>由哪个文件负责</th><th>证据</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>config.env:L3-L8</code></td><td>实验名与 Phase 1 入口</td><td>通过相对路径复用 Phase 1,不依赖启动命令当前目录。</td></tr>
|
||||
<tr><td><code>config.env:L10-L16</code></td><td>节点、端口、容器名</td><td>用于健康检查、PID 映射和容器级监控。</td></tr>
|
||||
<tr><td><code>config.env:L18-L20</code></td><td>诊断 Case</td><td>五个固定 Case,加一个混合 A/B 开关。</td></tr>
|
||||
<tr><td><code>config.env:L22-L32</code></td><td>采集策略</td><td>1 秒采样、空闲基线、冷却、DCGM 字段、perf 事件、RDMA HCA 和时钟容差。</td></tr>
|
||||
<tr><td><code>config.env:L34-L40</code></td><td>路径与运行模式</td><td>Repo/Result/Runtime、Dry-run 和是否容忍部分采集器失败。</td></tr>
|
||||
<tr><td>模型路径、镜像、TP16、EP、显存比例</td><td>Phase 1 <code>config.env</code> + <code>run_quick_map.sh</code></td><td><code>service/head_server_cmd.txt</code>、<code>worker_server_cmd.txt</code></td></tr>
|
||||
<tr><td>ISL/OSL/C、random 请求和 mixed A/B</td><td>Phase 1 场景表与 benchmark 函数</td><td><code>bench/*/bench_cmd.txt</code>、<code>bench.json</code></td></tr>
|
||||
<tr><td>通信基线、监控周期、Case 选择</td><td>Phase 2 <code>config.env</code></td><td>Phase 2 <code>manifest.json</code></td></tr>
|
||||
<tr><td>硬件归因和数值报告</td><td>Phase 2 Python 汇总器</td><td><code>case_*_summary.csv</code>、<code>report.md</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="reuse">4. Phase 1 复用边界</h2>
|
||||
<h3>4.1 委托函数</h3>
|
||||
<p>
|
||||
<code>run_phase1_action</code> 位于
|
||||
<code>run_hardware_contention_attribution.sh:L195-L214</code>。它向 Phase 1 注入:
|
||||
</p>
|
||||
<ul>
|
||||
<li>相同 <code>RUN_ID</code> 体系下的独立 benchmark 子目录。</li>
|
||||
<li>Phase 2 自己的 <code>service/</code> 证据目录。</li>
|
||||
<li>指定 <code>CASE_IDS</code>,使 Phase 1 只跑诊断 Case。</li>
|
||||
<li><code>CASE_COOLDOWN_S=0</code>,由 Phase 2 统一控制 Case 间隔。</li>
|
||||
<li><code>DRY_RUN</code> 原样传递,确保 Dry-run 不会偷偷启动模型。</li>
|
||||
</ul>
|
||||
<pre><code>run_phase1_action start
|
||||
run_phase1_action fixed # 通过 CASE_IDS 只跑一个 Case
|
||||
run_phase1_action mixed
|
||||
run_phase1_action stop</code></pre>
|
||||
|
||||
<h3>4.2 用户只运行哪个入口</h3>
|
||||
<p>
|
||||
正式执行只运行 Phase 2:
|
||||
<code>bash run_hardware_contention_attribution.sh all</code>。
|
||||
<code>start_service</code> 和 <code>stop_service</code>
|
||||
位于 <code>L215-L229</code>,是编排器内部调用,不需要手工先执行 Phase 1。
|
||||
</p>
|
||||
<div class="warning">
|
||||
<strong>设计不变量:</strong>Phase 2 不修改 Phase 1 的服务参数和请求口径。
|
||||
如果模型启动或请求生成需要修复,应改 Phase 1;如果采集、时间对齐或归因需要修复,应改 Phase 2。
|
||||
</div>
|
||||
|
||||
<h3>4.3 Phase 2 中如何查服务参数的实际值</h3>
|
||||
<p>
|
||||
例如 <code>MEM_FRACTION_STATIC</code> 的完整传递链是:
|
||||
</p>
|
||||
<pre><code>调用命令环境(可选覆盖)
|
||||
→ Phase1/config.env:L38,默认 0.9
|
||||
→ Phase1/run_quick_map.sh:L258
|
||||
→ SGLang --mem-fraction-static 0.9
|
||||
→ Phase2/results/<RUN_ID>/service/head_server_cmd.txt
|
||||
→ Phase2/results/<RUN_ID>/bench/<sub-run>/run_manifest.json</code></pre>
|
||||
<p>
|
||||
Phase 2 的顶层 <code>manifest.json</code> 记录监控配置,不重复记录全部服务参数。
|
||||
查“模型实际怎么起的”应看 <code>service/head_server_cmd.txt</code> 与
|
||||
<code>service/worker_server_cmd.txt</code>;查结构化值应看任一 Phase 1 子 Run 的
|
||||
<code>run_manifest.json</code>。
|
||||
</p>
|
||||
<pre><code># 查看 Phase 1 默认值
|
||||
grep '^MEM_FRACTION_STATIC=' \
|
||||
../dsv4pro_pro6000d_2node_sglang_tp16_quick_map/config.env
|
||||
|
||||
# 查看某次 Phase 2 Run 的实际服务命令
|
||||
grep -- '--mem-fraction-static' \
|
||||
results/<RUN_ID>/service/head_server_cmd.txt
|
||||
|
||||
# 从 Phase 1 子 Run Manifest 读取结构化值
|
||||
python3 -c 'import glob,json; p=glob.glob(
|
||||
"results/<RUN_ID>/bench/*/run_manifest.json")[0];
|
||||
print(json.load(open(p))["mem_fraction_static"])'</code></pre>
|
||||
<p>
|
||||
如果这样启动:
|
||||
<code>MEM_FRACTION_STATIC=0.85 bash run_hardware_contention_attribution.sh all</code>,
|
||||
外部变量会由 Phase 2 进程继承给 Phase 1,Phase 1 的
|
||||
<code>${MEM_FRACTION_STATIC:-0.9}</code> 会保留 <code>0.85</code>。
|
||||
因此只看默认配置不足以证明某次实验用了什么,必须查看 Run 证据。
|
||||
</p>
|
||||
|
||||
<h2 id="preflight">5. 预检、Manifest 与时钟</h2>
|
||||
<h3>5.1 配置和工具预检</h3>
|
||||
<p>
|
||||
<code>validate_config</code> 在
|
||||
<code>run_hardware_contention_attribution.sh:L66-L106</code>
|
||||
检查 Phase 1 入口、节点、Case 和关键数值。
|
||||
<code>preflight_node_tools</code> 在 <code>L107-L134</code>
|
||||
对两节点检查 Docker、NVIDIA、RDMA、sysstat、perf 与 NUMA 工具。
|
||||
</p>
|
||||
<p>
|
||||
采集器可用性不能在运行半小时后才发现。预检默认 fail-closed;
|
||||
只有显式允许部分采集器缺失时,才降级继续。
|
||||
</p>
|
||||
|
||||
<h3>5.2 为什么检查两机时钟</h3>
|
||||
<p>
|
||||
<code>preflight_clock_sync</code> 位于 <code>L135-L151</code>。
|
||||
Phase 2 用 wall clock 把 benchmark 的 <code>started_at/ended_at</code>
|
||||
与两节点采样行对齐。如果两机时钟偏差超过配置容差,同一 Case 在 Worker 上会切到错误窗口。
|
||||
</p>
|
||||
|
||||
<h3>5.3 运行元数据</h3>
|
||||
<p>
|
||||
Shell 的 <code>write_manifest</code> 位于 <code>L152-L176</code>,
|
||||
Python 的 <code>create_manifest</code> 位于
|
||||
<code>hardware_contention_attribution.py:L367-L390</code>。
|
||||
Manifest 记录 Git commit、是否 dirty、Phase 1 入口、节点、Case、采样周期、时钟容差和 Dry-run。
|
||||
</p>
|
||||
<p>
|
||||
<code>mark_event</code> 位于 Shell <code>L177-L194</code>,
|
||||
Python 的 <code>append_marker</code> 位于 <code>L338-L365</code>。
|
||||
每个 idle/case/cooldown 边界写入纳秒级 wall time,作为人工审计时间线。
|
||||
</p>
|
||||
|
||||
<h2 id="collectors">6. 采集器实现</h2>
|
||||
<h3>6.1 静态快照</h3>
|
||||
<p>
|
||||
<code>capture_command</code>、<code>static_snapshot_command</code> 和
|
||||
<code>capture_static_snapshots</code> 位于 Shell <code>L230-L283</code>。
|
||||
服务启动后和实验结束前分别采集:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>nvidia-smi</code> 与 GPU topology。</li>
|
||||
<li><code>lscpu</code>、<code>numactl --hardware</code>、<code>numastat</code>。</li>
|
||||
<li><code>ip</code>、<code>ethtool</code> 的 <code>eth0/eth3</code> 状态与计数器。</li>
|
||||
<li><code>ibdev2netdev</code>、<code>ibstat</code>、<code>rdma</code> 设备信息。</li>
|
||||
<li>容器列表与 inspect。</li>
|
||||
</ul>
|
||||
<p>
|
||||
前后快照回答“实验是否改变了设备状态”;时间序列回答“Case 运行期间发生了什么”。
|
||||
</p>
|
||||
|
||||
<h3>6.2 通用采集器包装</h3>
|
||||
<p>
|
||||
<code>start_stream_collector</code> 位于 Shell
|
||||
<code>L284-L318</code>。每个采集器都具备:
|
||||
</p>
|
||||
<ol>
|
||||
<li>保存完整命令到 <code>collector_commands/</code>。</li>
|
||||
<li>用唯一 tag 标记远端进程,便于精准停止。</li>
|
||||
<li>受 <code>COLLECTOR_TIMEOUT_S</code> 限制,避免永久悬挂。</li>
|
||||
<li>保存 PID、日志与退出状态到 <code>collector_status.csv</code>。</li>
|
||||
</ol>
|
||||
|
||||
<h3>6.3 GPU 采样</h3>
|
||||
<p>
|
||||
<code>gpu_sampler_command</code> 位于 Shell <code>L319-L332</code>,
|
||||
每秒调用 <code>nvidia-smi --query-gpu</code>,采集利用率、显存利用率、显存占用、
|
||||
功耗、温度、SM 时钟和显存时钟,并补上 <code>wall_time_ns,node,gpu</code>。
|
||||
</p>
|
||||
<p>
|
||||
Python 的 <code>summarize_gpu_rows</code> 位于
|
||||
<code>hardware_contention_attribution.py:L101-L135</code>。
|
||||
它按 node + GPU 分组,对每个字段输出 mean、P95、max。
|
||||
</p>
|
||||
|
||||
<h3>6.4 RDMA 采样</h3>
|
||||
<p>
|
||||
Shell 的 <code>rdma_sampler_command</code> 与 <code>read_counter</code>
|
||||
位于 <code>L333-L359</code>,读取 <code>mlx5_0/mlx5_3</code> 的端口发送/接收数据、
|
||||
包、错误、丢弃与恢复计数器。
|
||||
</p>
|
||||
<p>
|
||||
Python 的 <code>summarize_rdma_rows</code> 位于 <code>L148-L206</code>。
|
||||
它按 node + HCA 排序,使用最后值减第一值,并注意 IB
|
||||
<code>port_xmit_data/port_rcv_data</code> 的单位是 4 octets:
|
||||
</p>
|
||||
<pre><code>xmit_bytes = (last_xmit_data - first_xmit_data) × 4
|
||||
xmit_gbps = xmit_bytes × 8 / duration_s / 1e9</code></pre>
|
||||
<p>
|
||||
同时保留错误计数器 delta,因此“带宽低”可以与“链路错误增加”分开判断。
|
||||
</p>
|
||||
|
||||
<h3>6.5 CPU、进程和 NUMA</h3>
|
||||
<p>
|
||||
<code>start_node_collectors</code> 位于 Shell <code>L405-L457</code>,
|
||||
每个节点启动九类采集器:
|
||||
</p>
|
||||
<h2 id="config">4. 配置来源</h2>
|
||||
<table>
|
||||
<thead><tr><th>采集器</th><th>回答的问题</th></tr></thead>
|
||||
<thead><tr><th>行号</th><th>配置组</th><th>关键变量</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>nvidia-smi</code> / DCGM</td><td>GPU 是否算力、显存带宽、功耗或时钟受限。</td></tr>
|
||||
<tr><td><code>mpstat</code></td><td>CPU 总体与逐核是否繁忙。</td></tr>
|
||||
<tr><td><code>pidstat</code></td><td>SGLang/容器进程的 CPU、内存和上下文切换。</td></tr>
|
||||
<tr><td><code>sar -n DEV,EDEV</code></td><td><code>eth0/eth3</code> 吞吐和错误。</td></tr>
|
||||
<tr><td><code>perf stat</code></td><td>容器主进程的 CPU cycles、instructions、cache miss 等。</td></tr>
|
||||
<tr><td><code>docker top</code></td><td>容器 PID 与宿主机 PID 映射。</td></tr>
|
||||
<tr><td><code>numastat</code></td><td>进程内存是否跨 NUMA 节点访问。</td></tr>
|
||||
<tr><td>RDMA counters</td><td>两条 HCA 的真实数据量和错误增量。</td></tr>
|
||||
<tr><td><code>config.env:L3-L16</code></td><td>入口与节点</td><td><code>PHASE1_ENTRY</code>、Head/Worker、端口和容器名。</td></tr>
|
||||
<tr><td><code>L18-L21</code></td><td>诊断 Case</td><td>五个 fixed Case、mixed A/B 开关。</td></tr>
|
||||
<tr><td><code>L23-L39</code></td><td>采样与严格性</td><td>GPU/DCGM/RDMA 1 秒;CPU/进程/网络/NUMA/perf 5 秒;精确窗口和采集器 fail-closed。</td></tr>
|
||||
<tr><td><code>L40-L55</code></td><td>通信基线</td><td>镜像、消息尺寸、迭代次数、P2P 大小、CROSS_NIC 列表、Socket/HCA。</td></tr>
|
||||
<tr><td><code>L57-L61</code></td><td>路径与模式</td><td><code>RESULT_BASE</code>、Runtime、Dry-run、是否允许部分采集器。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<code>container_pid_preamble</code> 与 <code>numastat_command</code>
|
||||
位于 <code>L372-L404</code>,先解析容器主 PID,再让 perf/numastat 对准实际服务进程。
|
||||
<code>MEM_FRACTION_STATIC</code> 不在 Phase 2 重复定义。它仍来自 Phase 1,
|
||||
最终展开为 SGLang 的 <code>--mem-fraction-static</code>。判断某次 Run 的真实值,
|
||||
应读取 <code>service/head_server_cmd.txt</code>,不能只看默认配置。
|
||||
</p>
|
||||
|
||||
<h3>6.6 停止与残留清理</h3>
|
||||
<h2 id="communication">5. 通信微基准</h2>
|
||||
<h3>5.1 Shell 如何编排</h3>
|
||||
<p>
|
||||
<code>check_collectors</code> 和 <code>stop_collectors</code>
|
||||
位于 Shell <code>L469-L519</code>。
|
||||
除了等待已知 PID,还会按唯一 tag 清理远端残留采集器。
|
||||
<code>cleanup</code> 在 <code>L594-L602</code> 由 trap 调用,先停监控再停模型服务。
|
||||
</p>
|
||||
|
||||
<h2 id="cases">7. Case 编排</h2>
|
||||
<h3>7.1 固定 Case</h3>
|
||||
<p>
|
||||
<code>run_fixed_case</code> 位于 Shell <code>L529-L556</code>。
|
||||
它为 Case 生成独立子 Run ID,写 <code>case_start</code> marker,
|
||||
委托 Phase 1 的 <code>fixed</code>,再检查该子 Run 的 <code>summary.csv</code>,
|
||||
最后写 <code>case_end</code> marker。
|
||||
</p>
|
||||
<p>
|
||||
默认固定集合来自 <code>config.env:L18</code>,用于区分:
|
||||
单请求长 Prefill、并发 Prefill、普通 Decode、持续长 Decode、长上下文 Decode。
|
||||
Phase 2 不把所有 Phase 1 点再跑一遍,只保留对资源归因有辨识度的负载。
|
||||
</p>
|
||||
|
||||
<h3>7.2 混合 Case</h3>
|
||||
<p>
|
||||
<code>run_mixed_case</code> 位于 Shell <code>L557-L582</code>。
|
||||
它复用 Phase 1 已经保证真实时间重叠的 mixed A/B,并把整段监控留在同一采集窗口内。
|
||||
Phase 2 自己不重新实现后台请求与注入逻辑。
|
||||
</p>
|
||||
|
||||
<h3>7.3 run_all 的失败策略</h3>
|
||||
<p>
|
||||
<code>run_all</code> 位于 Shell <code>L604-L668</code>:
|
||||
<code>run_hardware_contention_attribution.sh:L269-L447</code> 负责 Docker 命令、
|
||||
两节点同步和清理。所有命令先写入 <code>commands/*.txt</code>:
|
||||
</p>
|
||||
<ul>
|
||||
<li>服务健康时,即使一个 Case 失败,也继续后续诊断 Case,并累计 failures。</li>
|
||||
<li>每个固定 Case 后检查 <code>/health</code>;服务失活则中止剩余 Case。</li>
|
||||
<li>服务已失活时跳过 mixed,避免无意义错误。</li>
|
||||
<li>不论成功失败,最终尝试 cooldown、停止采集、后快照、停止服务和汇总。</li>
|
||||
<li>Run 状态区分 <code>COMPLETED</code>、<code>COMPLETED_WITH_FAILURES</code>、<code>DRY_RUN</code>。</li>
|
||||
<li><code>L300-L323</code>:Head/Worker 各跑一次 P2P。</li>
|
||||
<li><code>L324-L353</code>:Head/Worker 各跑一次 8-rank AllReduce。</li>
|
||||
<li><code>L354-L416</code>:每个 CROSS_NIC 值先启动 Worker rank,再运行 Head rank。</li>
|
||||
<li><code>L417-L447</code>:只清理本实验前缀的通信容器。</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="alignment">8. 按真实 Case 时间窗归因</h2>
|
||||
<h3>8.1 时间窗从哪里来</h3>
|
||||
<p>
|
||||
<code>load_case_windows</code> 位于
|
||||
<code>hardware_contention_attribution.py:L233-L260</code>。
|
||||
它读取 Phase 1 每个 <code>meta.json</code> 中的
|
||||
<code>started_at</code> 与 <code>ended_at</code>,转换为纳秒时间戳。
|
||||
这比用“Case marker 前后大概几秒”更精确,因为它对齐的是 benchmark 进程实际测量区间。
|
||||
Docker 使用和 SGLang 一致的 CUDA 13 nightly 镜像,并显式透传
|
||||
<code>rdma_cm</code>、<code>uverbs0</code>、<code>uverbs3</code>。
|
||||
<code>NCCL_DEBUG=INFO</code> 只在微基准中打开,用于证明 NET/IB/GDRDMA 路径。
|
||||
</p>
|
||||
|
||||
<h3>8.2 如何切 GPU/RDMA 数据</h3>
|
||||
<h3>5.2 P2P 代码</h3>
|
||||
<p>
|
||||
<code>rows_in_window</code> 位于 <code>L263-L274</code>,
|
||||
只保留 <code>started_ns ≤ wall_time_ns ≤ ended_ns</code> 的采样行。
|
||||
<code>summarize_case_hardware</code> 位于 <code>L276-L319</code>,
|
||||
对每个 Case、每个节点分别切 GPU 与 RDMA,再复用全局汇总函数。
|
||||
<code>communication_baseline.py:L45-L106</code> 遍历所有源 GPU 和目标 GPU,
|
||||
先调用 <code>torch.cuda.can_device_access_peer</code>,再对 256 MiB FP16 Tensor
|
||||
做预热和 CUDA Event 计时。输出包括方向、P50/P95 latency 和 GB/s。
|
||||
汇总器按拓扑拆成同 PCIe Switch 的 PIX 与跨 NUMA 的 SYS。
|
||||
</p>
|
||||
<pre><code>Case meta.started_at / ended_at
|
||||
│
|
||||
├─ filter head/gpu_samples.csv
|
||||
├─ filter worker/gpu_samples.csv
|
||||
├─ filter head/rdma.csv
|
||||
└─ filter worker/rdma.csv
|
||||
↓
|
||||
case_gpu_summary.csv / case_rdma_summary.csv</code></pre>
|
||||
<div class="warning">
|
||||
1 秒采样意味着很短的请求可能只有少数样本。此时 P95 不稳定,应结合原始时间序列和 Case 持续时间,
|
||||
不能把单个采样峰值解释为稳定瓶颈。
|
||||
</div>
|
||||
|
||||
<h2 id="outputs">9. 输出和归因边界</h2>
|
||||
<h3>5.3 AllReduce 代码</h3>
|
||||
<p>
|
||||
<code>communication_baseline.py:L107-L198</code> 初始化 NCCL process group,
|
||||
对 1 MiB、64 MiB、1 GiB 分别预热和重复测量。每轮先把各 rank latency
|
||||
gather 到 rank 0,使用最慢 rank 作为 collective 完成时间,并检查归约结果:
|
||||
</p>
|
||||
<pre><code>algbw = message_bytes / latency
|
||||
busbw = algbw × 2 × (world_size - 1) / world_size
|
||||
wrong_values = count(output != expected_sum)</code></pre>
|
||||
<p>
|
||||
这样不会用某个提前返回 rank 的时间美化结果;<code>wrong_values=0</code>
|
||||
才算正确完成。
|
||||
</p>
|
||||
|
||||
<h2 id="collectors">6. 两节点采集器</h2>
|
||||
<h3>6.1 启动前门禁</h3>
|
||||
<p>
|
||||
Shell <code>L67-L199</code> 完成配置、工具、时钟和 GPU 空闲检查。
|
||||
<code>preflight_node_tools</code> 不只检查 <code>dcgmi</code> 文件存在,
|
||||
还实际运行 <code>dcgmi discovery -l</code>;两节点任一 Host Engine 不可用即退出。
|
||||
</p>
|
||||
|
||||
<h3>6.2 采集器包装</h3>
|
||||
<p>
|
||||
<code>start_stream_collector</code> 位于 Shell <code>L517-L551</code>。
|
||||
它保存完整命令、PID、唯一进程 tag 和日志;<code>check_collectors</code> 在
|
||||
<code>L722-L740</code> 检查采集器是否提前退出,默认不允许部分成功。
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>采集器</th><th>Shell 位置</th><th>周期</th><th>输出</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>nvidia-smi</code></td><td><code>L552-L565</code></td><td>1 秒</td><td><code>gpu_samples.csv</code></td></tr>
|
||||
<tr><td>RDMA HCA counters</td><td><code>L566-L592</code></td><td>1 秒</td><td><code>rdma.csv</code></td></tr>
|
||||
<tr><td>DCGM</td><td><code>L645-L655</code></td><td>1 秒</td><td><code>dcgm_dmon.log</code></td></tr>
|
||||
<tr><td><code>mpstat</code></td><td><code>L656-L663</code></td><td>5 秒</td><td><code>mpstat.log</code></td></tr>
|
||||
<tr><td><code>pidstat -durw</code></td><td><code>L664-L671</code></td><td>5 秒,进程级</td><td><code>pidstat.log</code></td></tr>
|
||||
<tr><td><code>sar -n DEV,EDEV</code></td><td><code>L672-L678</code></td><td>5 秒</td><td><code>sar_net.log</code></td></tr>
|
||||
<tr><td><code>perf stat</code></td><td><code>L680-L689</code></td><td>5 秒</td><td><code>perf_stat.log</code></td></tr>
|
||||
<tr><td><code>numastat</code></td><td><code>L618-L644</code></td><td>5 秒</td><td><code>numa_samples.csv</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
CPU、进程、perf 和 sar 的每行均由 Shell 增加
|
||||
<code>wall_time_ns TAB node TAB payload</code>。NUMA 直接转成结构化 CSV,
|
||||
避免旧版线程级 1 秒日志过大,也让所有指标能按 Case 切片。
|
||||
</p>
|
||||
|
||||
<h2 id="alignment">7. 精确测量窗口</h2>
|
||||
<h3>7.1 Phase 1 如何标记主测量</h3>
|
||||
<p>
|
||||
Phase 1 <code>run_quick_map.sh:L547-L564</code> 每 100 ms 观察 bench 日志;
|
||||
发现 <code>Starting main benchmark run</code> 后调用
|
||||
<code>quick_map_results.py mark-measurement-start</code>。
|
||||
<code>quick_map_results.py:L340-L385</code> 用这个起点和
|
||||
<code>bench.json.duration</code> 生成:
|
||||
</p>
|
||||
<pre><code>measurement_started_at
|
||||
measurement_ended_at
|
||||
measurement_duration_s
|
||||
measurement_window_source = bench_main_marker_plus_duration</code></pre>
|
||||
|
||||
<h3>7.2 Phase 2 如何使用</h3>
|
||||
<p>
|
||||
<code>hardware_contention_attribution.py:L509-L560</code> 优先读取上述字段。
|
||||
只有兼容旧结果时才可能使用进程级窗口;正式配置
|
||||
<code>REQUIRE_PRECISE_WINDOWS=1</code> 会拒绝任何 fallback。
|
||||
<code>L561-L841</code> 对 GPU、DCGM、CPU、进程、perf、NUMA、netdev 和 RDMA
|
||||
使用同一个 <code>started_ns ≤ sample ≤ ended_ns</code> 条件。
|
||||
</p>
|
||||
|
||||
<h2 id="report">8. 逐指标报告</h2>
|
||||
<p>
|
||||
Python <code>summarize</code> 位于
|
||||
<code>hardware_contention_attribution.py:L1036-L1378</code>。
|
||||
它不只生成一个抽象结论,而是按 Phase 2 第 5 节依次写出:
|
||||
</p>
|
||||
<table>
|
||||
<thead><tr><th>指标</th><th>解析函数</th><th>Case 汇总文件</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>GPU</td><td><code>summarize_gpu_rows L377-L413</code></td><td><code>case_gpu_summary.csv</code>、<code>case_gpu_node_summary.csv</code></td></tr>
|
||||
<tr><td>DCGM</td><td><code>parse_dcgm L167-L192</code></td><td><code>case_dcgm_summary.csv</code></td></tr>
|
||||
<tr><td>CPU</td><td><code>parse_mpstat L193-L222</code></td><td><code>case_cpu_summary.csv</code></td></tr>
|
||||
<tr><td>进程</td><td><code>parse_pidstat L223-L289</code></td><td><code>case_process_summary.csv</code></td></tr>
|
||||
<tr><td>perf</td><td><code>parse_perf L290-L310</code></td><td><code>case_perf_summary.csv</code></td></tr>
|
||||
<tr><td>NUMA</td><td>结构化 CSV + <code>summarize_case_metrics</code></td><td><code>case_numa_summary.csv</code></td></tr>
|
||||
<tr><td>Linux netdev</td><td><code>parse_sar_net L311-L358</code></td><td><code>case_netdev_summary.csv</code></td></tr>
|
||||
<tr><td>RDMA</td><td><code>summarize_rdma_rows L424-L484</code></td><td><code>case_rdma_summary.csv</code></td></tr>
|
||||
<tr><td>P2P/NCCL</td><td><code>load_communication_rows</code> + <code>aggregate_communication_rows L842-L928</code></td><td><code>communication_summary.csv</code>、<code>communication_aggregate.csv</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<code>report.md</code> 对每组都打印有效样本数、Mean/P95/Max、Head/Worker
|
||||
或 Case 间比较和源文件。解析不到的值保留为 <code>-</code>,不会被写成 0。
|
||||
</p>
|
||||
|
||||
<h2 id="outputs">9. 结果目录</h2>
|
||||
<pre><code>results/<RUN_ID>/
|
||||
manifest.json
|
||||
markers.csv
|
||||
collector_status.csv
|
||||
service/
|
||||
commands/
|
||||
communication/
|
||||
service/
|
||||
bench/<phase1-sub-run>/
|
||||
head/
|
||||
gpu_samples.csv
|
||||
rdma.csv
|
||||
dcgm.log
|
||||
dcgm_dmon.log
|
||||
mpstat.log
|
||||
pidstat.log
|
||||
sar_network.log
|
||||
perf.log
|
||||
docker_top.log
|
||||
numastat.log
|
||||
perf_stat.log
|
||||
sar_net.log
|
||||
numa_samples.csv
|
||||
rdma.csv
|
||||
collector_commands/
|
||||
worker/
|
||||
...同上...
|
||||
gpu_summary.csv
|
||||
rdma_summary.csv
|
||||
bench_summary.csv
|
||||
case_windows.csv
|
||||
bench_summary.csv
|
||||
case_gpu_summary.csv
|
||||
case_gpu_node_summary.csv
|
||||
case_dcgm_summary.csv
|
||||
case_cpu_summary.csv
|
||||
case_process_summary.csv
|
||||
case_perf_summary.csv
|
||||
case_numa_summary.csv
|
||||
case_netdev_summary.csv
|
||||
case_rdma_summary.csv
|
||||
communication_summary.csv
|
||||
communication_aggregate.csv
|
||||
summary.json
|
||||
report.md</code></pre>
|
||||
<p>
|
||||
Python 的 <code>summarize</code> 位于
|
||||
<code>hardware_contention_attribution.py:L405-L504</code>。
|
||||
它汇总全局 GPU/RDMA、Phase 1 bench、Case 时间窗和 Case 级硬件数据,
|
||||
同时统计采集器状态与文件大小。
|
||||
</p>
|
||||
<p>
|
||||
自动报告只整理证据,不自动宣布“瓶颈就是 GPU/NCCL/CPU”。
|
||||
<code>L498-L499</code> 明确要求最终结论结合 markers、原始 DCGM/sysstat 和 SGLang 日志。
|
||||
这是有意的保守边界,避免单个指标被机械误判。
|
||||
</p>
|
||||
|
||||
<h2 id="index">10. 函数行号索引</h2>
|
||||
<h3>10.1 run_hardware_contention_attribution.sh</h3>
|
||||
<h3>10.1 Shell 编排器</h3>
|
||||
<table>
|
||||
<thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>L25-L65</td><td>日志、远端执行、命令文件、结果日志</td><td>编排器基础设施。</td></tr>
|
||||
<tr><td>L66-L151</td><td>配置、工具、时钟预检</td><td>正式启动前 fail-fast。</td></tr>
|
||||
<tr><td>L152-L194</td><td>Manifest 与 marker</td><td>记录运行身份和事件边界。</td></tr>
|
||||
<tr><td>L195-L229</td><td>Phase 1 委托与服务生命周期</td><td>复用服务和 benchmark,不复制实现。</td></tr>
|
||||
<tr><td>L230-L283</td><td>静态快照</td><td>保存实验前后硬件、网络、RDMA 和容器状态。</td></tr>
|
||||
<tr><td>L284-L318</td><td><code>start_stream_collector</code></td><td>统一包装远端长时间采集器。</td></tr>
|
||||
<tr><td>L319-L404</td><td>GPU、RDMA、PID、NUMA 命令</td><td>生成各类采集命令。</td></tr>
|
||||
<tr><td>L405-L468</td><td>启动所有采集器</td><td>两节点各启动九类监控。</td></tr>
|
||||
<tr><td>L469-L519</td><td>检查与停止采集器</td><td>收集状态并清理残留。</td></tr>
|
||||
<tr><td>L520-L582</td><td>sleep、固定 Case、混合 Case</td><td>诊断负载编排。</td></tr>
|
||||
<tr><td>L583-L603</td><td>汇总、Manifest 完成、cleanup</td><td>结果收口与异常清理。</td></tr>
|
||||
<tr><td>L604-L668</td><td><code>run_all</code></td><td>Phase 2 完整状态机。</td></tr>
|
||||
<tr><td>L669-L689</td><td><code>main</code></td><td>分发 <code>all/summarize/stop</code>。</td></tr>
|
||||
<tr><td>L26-L66</td><td>日志、远端执行、命令证据</td><td>基础设施。</td></tr>
|
||||
<tr><td>L67-L199</td><td>配置、工具、时钟、GPU 空闲门禁</td><td>正式运行前 fail-fast。</td></tr>
|
||||
<tr><td>L200-L268</td><td>Manifest、marker、Phase 1 委托</td><td>运行身份与复用边界。</td></tr>
|
||||
<tr><td>L269-L447</td><td>通信基线</td><td>P2P、8/16-rank AllReduce、CROSS_NIC A/B。</td></tr>
|
||||
<tr><td>L448-L516</td><td>服务和静态快照</td><td>启停 Phase 1 双机服务并保存环境。</td></tr>
|
||||
<tr><td>L517-L710</td><td>采集命令与启动</td><td>两节点分层采样。</td></tr>
|
||||
<tr><td>L711-L772</td><td>采集器检查和停止</td><td>fail-closed 与残留清理。</td></tr>
|
||||
<tr><td>L782-L835</td><td>fixed/mixed Case</td><td>代表负载编排。</td></tr>
|
||||
<tr><td>L836-L858</td><td>汇总、Manifest、trap</td><td>结果收口。</td></tr>
|
||||
<tr><td>L859-L928</td><td><code>run_all</code></td><td>完整状态机。</td></tr>
|
||||
<tr><td>L929-L968</td><td>辅助 action 与 main</td><td><code>communication/all/summarize/stop</code> 分发。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>10.2 hardware_contention_attribution.py</h3>
|
||||
<h3>10.2 Python 文件</h3>
|
||||
<table>
|
||||
<thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead>
|
||||
<thead><tr><th>文件/行号</th><th>职责</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>L54-L100</td><td>时间、JSON、数字、CSV</td><td>结果工具基础函数。</td></tr>
|
||||
<tr><td>L101-L141</td><td>GPU 汇总</td><td>按 node/GPU 输出 mean、P95、max。</td></tr>
|
||||
<tr><td>L142-L212</td><td>RDMA 汇总</td><td>计数器 delta、字节换算、Gbps 和错误增量。</td></tr>
|
||||
<tr><td>L213-L260</td><td>Bench 与 Case 时间窗读取</td><td>从 Phase 1 子 Run 建立诊断索引。</td></tr>
|
||||
<tr><td>L263-L321</td><td>时间切片</td><td>按每个 Case 的真实运行窗口汇总 GPU/RDMA。</td></tr>
|
||||
<tr><td>L322-L366</td><td>CSV 与 marker</td><td>输出结构化表和事件时间线。</td></tr>
|
||||
<tr><td>L367-L404</td><td>Manifest 与 bench 校验</td><td>维护 Run 状态,确认子 Run 全部完成。</td></tr>
|
||||
<tr><td>L405-L506</td><td><code>summarize</code></td><td>生成全部汇总表、summary JSON 和 report。</td></tr>
|
||||
<tr><td>L507-L574</td><td>CLI</td><td>向 Shell 提供 marker/manifest/check/summarize 子命令。</td></tr>
|
||||
<tr><td><code>communication_baseline.py:L20-L44</code></td><td>尺寸解析、分位数和 JSON 结果协议。</td></tr>
|
||||
<tr><td><code>L45-L106</code></td><td>CUDA P2P 全矩阵。</td></tr>
|
||||
<tr><td><code>L107-L198</code></td><td>NCCL AllReduce 与正确性。</td></tr>
|
||||
<tr><td><code>hardware_contention_attribution.py:L76-L166</code></td><td>时间、CSV、数字统计基础函数。</td></tr>
|
||||
<tr><td><code>L167-L358</code></td><td>DCGM、mpstat、pidstat、perf、sar 解析器。</td></tr>
|
||||
<tr><td><code>L359-L508</code></td><td>通信、GPU、RDMA、bench 读取与汇总。</td></tr>
|
||||
<tr><td><code>L509-L841</code></td><td>精确窗口和全部 Case 指标切片。</td></tr>
|
||||
<tr><td><code>L842-L1035</code></td><td>通信聚合、CSV、Marker、Manifest。</td></tr>
|
||||
<tr><td><code>L1036-L1378</code></td><td>全部输出表和逐指标 <code>report.md</code>。</td></tr>
|
||||
<tr><td><code>L1379-L1480</code></td><td>CLI 子命令。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<footer>
|
||||
本文只描述提交 <code>ca1f2f63375c</code> 的实现。后续增加 Nsight、SGLang profiler
|
||||
或新的硬件计数器时,应先明确它属于 Phase 2 采集层还是后续深度剖析层,再更新本档案。
|
||||
本文只描述提交 <code>30664faa41f8</code>。Nsight Systems、SGLang Profiler 和
|
||||
Kernel Timeline 属于 Phase 3,不加入 Phase 2,避免重复采集和职责混淆。
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@ -135,7 +135,7 @@
|
||||
<div class="meta">
|
||||
<span>节点:174.1.51.5 + 174.1.51.7</span>
|
||||
<span>拓扑:SGLang TP16 / EP2</span>
|
||||
<span>更新:2026-07-31 14:15:00 CST</span>
|
||||
<span>更新:2026-07-31 15:25:00 CST</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -144,10 +144,11 @@
|
||||
<a class="back" href="./推理优化计划.html">返回推理优化主计划</a>
|
||||
|
||||
<p class="status">
|
||||
<strong>当前状态:Phase 2 首次正式双机 Run 已完成。</strong>
|
||||
Run <code>dsv4pro-phase2-20260731-130125</code> 在 26 分 26 秒内完成
|
||||
8/8 个 benchmark,无 OOM;GPU、CPU 和双 Rail RDMA 数据已完成首轮归因。
|
||||
本阶段在此停止,不自动进入 Phase 3。
|
||||
<strong>当前状态:Phase 2 最终采集代码已完成,等待最终双机复跑。</strong>
|
||||
首次 Run <code>dsv4pro-phase2-20260731-130125</code> 的 8/8 个 benchmark
|
||||
均成功;提交 <code>30664faa41f8</code> 已补齐正式测量窗口、双节点 DCGM 门禁、
|
||||
低开销进程采样、机内 PCIe P2P、单/双机 AllReduce 与逐指标自动报告。
|
||||
本阶段复跑完成并汇报后才进入 Phase 3。
|
||||
</p>
|
||||
|
||||
<h2>1. Phase 1 交接结果</h2>
|
||||
@ -175,7 +176,7 @@
|
||||
<ul>
|
||||
<li>只测试 SGLang,不测试 vLLM。</li>
|
||||
<li>保留模型、镜像、TP16、EP2、显存比例和已验证的双 Rail NCCL 配置。</li>
|
||||
<li>不启用 Nsight Systems、PyTorch Profiler、NCCL DEBUG 或投机解码。</li>
|
||||
<li>模型端到端 Case 不启用 Nsight Systems、PyTorch Profiler、NCCL DEBUG 或投机解码;独立通信基线临时启用 NCCL INFO 以保存实际路径证据。</li>
|
||||
<li>不调参,不尝试优化;先获得足以区分瓶颈类别的硬件证据。</li>
|
||||
<li>只重放五个固定代表负载和一组混合 A/B,不重复 Phase 1 全矩阵。</li>
|
||||
<li>采集器从请求开始前启动,到请求结束后停止,不能中途补采后声称完整。</li>
|
||||
@ -198,6 +199,7 @@
|
||||
|
||||
<h2>4. 诊断 Run</h2>
|
||||
<ol>
|
||||
<li>确认 16 张 GPU 空闲,先跑两节点 PCIe P2P、单机 8 rank AllReduce 和双机 16 rank AllReduce;双机分别测试 <code>NCCL_CROSS_NIC=0/1/2</code>。</li>
|
||||
<li>保存两节点静态快照:GPU/NIC/NUMA 拓扑、驱动、CUDA、镜像与服务命令。</li>
|
||||
<li>复用 Phase 1 已验证的 <code>run_quick_map.sh start</code> 启动同配置双机服务。</li>
|
||||
<li>在 Head 和 Worker 同时启动 GPU、CPU、网卡与 RDMA 采样,先记录 15 秒空闲基线。</li>
|
||||
@ -215,8 +217,8 @@ cooldown 15s
|
||||
|
||||
Head 与 Worker 的所有采集器覆盖完整诊断窗口。</code></pre>
|
||||
<p>
|
||||
Phase 1 中服务加载约 5 分 30 秒;五个固定负载加混合 A/B、静态快照、
|
||||
采样和清理,目标仍控制在约 30 分钟内。
|
||||
Phase 1 中服务加载约 5 分 30 秒;通信基线、五个固定负载、混合 A/B、
|
||||
静态快照、采样和清理组成一次完整 Phase 2 Run。
|
||||
</p>
|
||||
|
||||
<h3>4.1 你只需要运行的入口</h3>
|
||||
@ -255,7 +257,9 @@ tmux attach -t dsv4pro-phase2</code></pre>
|
||||
<code>systemctl enable nvidia-dcgm</code>。
|
||||
</p>
|
||||
<p><strong><code>all</code> 内部执行顺序:</strong></p>
|
||||
<pre><code>配置与工具门禁
|
||||
<pre><code>配置、工具、DCGM 与 GPU 空闲门禁
|
||||
→ 通信基线:两节点 P2P、两组单机 8-rank AllReduce、
|
||||
三组双机 16-rank NCCL_CROSS_NIC A/B
|
||||
→ Phase 1 start:启动同配置 TP16 服务
|
||||
→ 两节点静态快照
|
||||
→ 启动两节点采集器并记录 15 秒 idle
|
||||
@ -264,7 +268,7 @@ tmux attach -t dsv4pro-phase2</code></pre>
|
||||
→ 15 秒 cooldown
|
||||
→ 停止采集器并保存后快照
|
||||
→ Phase 1 stop:停止 Head/Worker
|
||||
→ 生成按 Case 对齐的 CSV、JSON 与 report.md</code></pre>
|
||||
→ 生成按第 5 节逐项对应的 CSV、JSON 与 report.md</code></pre>
|
||||
<p>
|
||||
Phase 1 的作用是提供已经验证过的双机 Docker 服务和 Benchmark 实现,
|
||||
不是第二个用户入口。实际展开的服务、Benchmark 和采集命令都会写入
|
||||
@ -314,9 +318,13 @@ python3 hardware_contention_attribution.py marker \
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
首轮实现的 Case 边界包含 Benchmark 客户端启动、数据生成、Warm-up、正式测量和退出;
|
||||
因此它适合判断硬件方向,但不是纯主测量窗口。后续实现需要围绕
|
||||
<code>Starting main benchmark run</code> 增加更精确的起止 Marker。
|
||||
最终实现由 Phase 1 监听 <code>bench.log</code> 中的
|
||||
<code>Starting main benchmark run</code>,立刻写入
|
||||
<code>measurement_start.json</code>;再使用 SGLang <code>bench.json</code>
|
||||
的正式 benchmark duration 计算结束时间。Phase 2 优先读取
|
||||
<code>measurement_started_at/measurement_ended_at</code>,不会把数据生成和
|
||||
Warm-up 混入硬件均值。<code>REQUIRE_PRECISE_WINDOWS=1</code> 时,任何 Case
|
||||
缺少精确窗口都会让汇总失败,而不是悄悄回退。
|
||||
</p>
|
||||
|
||||
<h3>5.2 GPU 基础状态:<code>nvidia-smi</code></h3>
|
||||
@ -356,7 +364,7 @@ dcgmi dmon \
|
||||
<tr><td>1002</td><td><code>sm_active</code></td><td>SM 至少有一个 Warp 活跃的比例</td></tr>
|
||||
<tr><td>1003</td><td><code>sm_occupancy</code></td><td>活跃 Warp 相对硬件可容纳 Warp 的比例</td></tr>
|
||||
<tr><td>1004</td><td><code>tensor_active</code></td><td>Tensor Core 指令活跃比例</td></tr>
|
||||
<tr><td>1005</td><td><code>dram_active</code></td><td>设备显存接口活跃比例,用于判断 HBM 压力</td></tr>
|
||||
<tr><td>1005</td><td><code>dram_active</code></td><td>设备显存接口活跃比例;Pro6000D 为 GDDR7,用于判断设备显存带宽压力</td></tr>
|
||||
<tr><td>1009</td><td><code>pcie_tx_bytes</code></td><td>GPU 经 PCIe 发出的字节速率</td></tr>
|
||||
<tr><td>1010</td><td><code>pcie_rx_bytes</code></td><td>GPU 经 PCIe 接收的字节速率</td></tr>
|
||||
</tbody>
|
||||
@ -364,31 +372,36 @@ dcgmi dmon \
|
||||
<p>
|
||||
<code>sm_active</code> 高而 <code>sm_occupancy</code> 低,表示 SM 经常有工作,
|
||||
但同时驻留的 Warp 不多;后续通过 Kernel Timeline 区分小 Kernel、
|
||||
寄存器/共享内存约束和同步。DCGM 依赖宿主
|
||||
<code>nvidia-dcgm</code> Host Engine;首轮 Worker 未启动该服务,所以只有
|
||||
Head 的 <code>dcgm_dmon.log</code> 有效。
|
||||
寄存器/共享内存约束和同步。DCGM 是 NVIDIA Data Center GPU Manager:
|
||||
<code>nvidia-dcgm</code>/<code>nv-hostengine</code> 是后台 Host Engine,
|
||||
<code>dcgmi</code> 是客户端,Field ID 是指标编号。最终代码在两节点预检
|
||||
<code>dcgmi discovery -l</code>,任一 Host Engine 不可用即 fail-fast;
|
||||
正式结果必须同时包含 Head 和 Worker 的 <code>case_dcgm_summary.csv</code>。
|
||||
</p>
|
||||
|
||||
<h3>5.4 CPU、进程与 Kernel Launch 侧证据</h3>
|
||||
<pre><code class="language-bash"># 全部逻辑 CPU,每秒输出一次
|
||||
mpstat -P ALL 1
|
||||
<pre><code class="language-bash"># 全部逻辑 CPU,每 5 秒输出一次
|
||||
mpstat -P ALL 5
|
||||
|
||||
# 找到容器内进程对应的宿主 PID
|
||||
docker top <container> -eo pid,ppid,psr,pcpu,pmem,stat,comm,args
|
||||
|
||||
# 首轮实际命令:CPU、I/O、缺页、上下文切换,并展开线程
|
||||
pidstat -durwt -p "<comma-separated-host-pids>" 1
|
||||
# 最终命令:进程级 CPU、I/O、缺页、上下文切换,不展开全部线程
|
||||
pidstat -durw -p "<comma-separated-host-pids>" 5
|
||||
|
||||
# 每秒输出一次硬件/软件计数器增量
|
||||
perf stat -p "<comma-separated-host-pids>" -I 1000 \
|
||||
# 每 5 秒输出一次硬件/软件计数器增量
|
||||
perf stat -p "<comma-separated-host-pids>" -I 5000 \
|
||||
-e cycles,instructions,cache-misses,context-switches,\
|
||||
cpu-migrations,page-faults</code></pre>
|
||||
cpu-migrations,page-faults
|
||||
|
||||
# mpstat/pidstat/perf 每行都由包装器增加:
|
||||
# wall_time_ns TAB node TAB 原始输出</code></pre>
|
||||
<table>
|
||||
<thead><tr><th>命令/字段</th><th>回答的问题</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>mpstat -P ALL</code></td><td>整机是否 CPU 饱和,是否只有少量核心接近 100%,是否存在 I/O Wait</td></tr>
|
||||
<tr><td><code>docker top</code></td><td>把容器进程映射为宿主 PID、CPU 核 <code>PSR</code> 和进程状态</td></tr>
|
||||
<tr><td><code>pidstat -u</code></td><td>服务进程和线程的用户态、内核态 CPU 时间</td></tr>
|
||||
<tr><td><code>pidstat -u</code></td><td>服务进程的用户态、内核态 CPU 时间</td></tr>
|
||||
<tr><td><code>pidstat -d</code></td><td>进程块设备 I/O</td></tr>
|
||||
<tr><td><code>pidstat -r</code></td><td>内存和 Page Fault 行为</td></tr>
|
||||
<tr><td><code>pidstat -w</code></td><td>主动/被动上下文切换,辅助发现线程阻塞或调度抖动</td></tr>
|
||||
@ -397,9 +410,10 @@ cpu-migrations,page-faults</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
首轮 <code>pidstat -durwt</code> 对所有线程做 1 秒采样,产生约 850 MB/735 MB
|
||||
的 Head/Worker 日志,信息量和扰动都过大。后续不照搬:默认改为进程级 5 秒采样;
|
||||
只有确认某个进程异常后,才在短窗口内开启线程级采样。
|
||||
最终实现使用进程级 5 秒采样,避免首轮线程级 1 秒采样产生数百 MB 日志。
|
||||
<code>case_cpu_summary.csv</code>、<code>case_process_summary.csv</code> 和
|
||||
<code>case_perf_summary.csv</code> 都按正式测量窗口切片;只有先发现异常进程,
|
||||
才在后续短窗口单独开启线程级采样。
|
||||
</p>
|
||||
|
||||
<h3>5.5 NUMA 与 CPU/内存亲和</h3>
|
||||
@ -409,19 +423,23 @@ numastat -m
|
||||
|
||||
# 每 5 秒按容器宿主 PID 查看本地/远端 NUMA 内存
|
||||
numastat -p <host-pid>
|
||||
# 解析为:
|
||||
# wall_time_ns,node,node0_mib,node1_mib,total_mib,process_count
|
||||
|
||||
# 同时保存 GPU、CPU、NIC 的拓扑关系
|
||||
nvidia-smi topo -m</code></pre>
|
||||
<p>
|
||||
NUMA 是多路 CPU 机器的“本地内存”结构。进程长期从远端 NUMA Node 取内存,
|
||||
或 GPU/NIC 对应的 CPU 线程被调度到另一侧,可能增加 Host 侧延迟。
|
||||
将 <code>numastat -p</code> 的内存分布与 CPU 核、GPU 活跃区间和 Timeline 对齐,
|
||||
用于定位跨 NUMA 访问。
|
||||
最终采集器把 <code>numastat -p</code> 解析为
|
||||
<code>numa_samples.csv</code>,再按正式测量窗口生成
|
||||
<code>case_numa_summary.csv</code>。这样可以直接比较 Node0/Node1 MiB,
|
||||
而不是依靠人工阅读不断刷新的文本。
|
||||
</p>
|
||||
|
||||
<h3>5.6 普通网卡统计与 RDMA 数据面</h3>
|
||||
<pre><code class="language-bash"># Linux netdev 层,每秒采样吞吐与错误
|
||||
sar -n DEV,EDEV 1
|
||||
<pre><code class="language-bash"># Linux netdev 层,每 5 秒采样吞吐与错误
|
||||
sar -n DEV,EDEV 5
|
||||
|
||||
# Case 前后保存物理端口状态和驱动计数器
|
||||
ethtool eth0
|
||||
@ -462,11 +480,38 @@ done</code></pre>
|
||||
<p>
|
||||
原始数据为 <code>head|worker/rdma.csv</code>;
|
||||
<code>case_rdma_summary.csv</code> 按 Case、节点和 HCA 计算吞吐及错误增量。
|
||||
它说明双 Rail 的实际流量、均衡性和错误增量;Phase 3 再把 NCCL Collective
|
||||
它说明双 Rail 的实际流量、均衡性和错误增量;
|
||||
<code>case_netdev_summary.csv</code> 同时保留 Linux netdev 层的
|
||||
<code>eth0/eth3</code> RX/TX 与错误。Phase 3 再把 NCCL Collective
|
||||
放到请求 Timeline 中分析持续时间和计算重叠。
|
||||
</p>
|
||||
|
||||
<h3>5.7 静态快照与结果关系</h3>
|
||||
<h3>5.7 机内 PCIe 与 NCCL 通信基线</h3>
|
||||
<pre><code class="language-bash"># 由 all 入口自动执行;不需要用户手工运行 torchrun
|
||||
# 每个节点:所有 GPU 源/目标对,FP16 256 MiB CUDA P2P copy
|
||||
python3 communication_baseline.py p2p \
|
||||
--size 256M --warmup 3 --iterations 10
|
||||
|
||||
# 每个节点:8 rank NCCL AllReduce
|
||||
torchrun --standalone --nproc-per-node=8 \
|
||||
communication_baseline.py all-reduce \
|
||||
--sizes 1M,64M,1G --repetitions 3 --warmup 5 --iterations 10
|
||||
|
||||
# 双节点:16 rank;分别设置 NCCL_CROSS_NIC=0、1、2
|
||||
torchrun --nnodes=2 --nproc-per-node=8 \
|
||||
--master-addr 10.101.0.11 --node-rank <0-or-1> \
|
||||
communication_baseline.py all-reduce \
|
||||
--sizes 1M,64M,1G --repetitions 3 --warmup 5 --iterations 10</code></pre>
|
||||
<p>
|
||||
P2P 结果按 <code>same_pcie_switch</code>(PIX)和
|
||||
<code>cross_numa_sys</code>(SYS)分别汇总,不用一个平均值掩盖跨 CPU 路径。
|
||||
AllReduce 同时报告 P50/P95 latency、<code>algbw</code>、
|
||||
<code>busbw</code>、正确性错误数和实际 NCCL 路径。1 MiB、64 MiB、1 GiB
|
||||
分别覆盖小消息延迟、中等消息和大消息带宽;双机 A/B 直接给出
|
||||
<code>NCCL_CROSS_NIC=0/1/2</code> 的数值比较。
|
||||
</p>
|
||||
|
||||
<h3>5.8 静态快照与结果关系</h3>
|
||||
<pre><code class="language-bash">nvidia-smi
|
||||
nvidia-smi topo -m
|
||||
lscpu
|
||||
@ -483,10 +528,42 @@ docker top <container> -eo pid,ppid,psr,pcpu,pmem,stat,comm,args</code></p
|
||||
<tr><td><code>case_windows.csv</code></td><td>每个 Benchmark Case 的起止时间</td><td>从连续硬件日志中切片</td></tr>
|
||||
<tr><td><code>bench_summary.csv</code></td><td>TPS、TTFT、TPOT、ITL、E2E</td><td>把硬件现象与用户侧性能对应</td></tr>
|
||||
<tr><td><code>case_gpu_summary.csv</code></td><td>每 Case、节点、GPU 的利用率、显存、功耗、频率</td><td>比较负载与节点/GPU 不均衡</td></tr>
|
||||
<tr><td><code>case_dcgm_summary.csv</code></td><td>每 Case、节点、GPU 的 SM/Tensor/显存接口/PCIe 指标</td><td>区分计算、设备显存和 PCIe 活跃度</td></tr>
|
||||
<tr><td><code>case_cpu_summary.csv</code></td><td>整机与逐核 CPU 利用率、I/O Wait</td><td>识别整机饱和和少数热点核</td></tr>
|
||||
<tr><td><code>case_process_summary.csv</code></td><td>服务进程 CPU、I/O、缺页、内存与上下文切换</td><td>定位 Host 进程开销与阻塞</td></tr>
|
||||
<tr><td><code>case_perf_summary.csv</code></td><td>cycles、instructions、cache miss、迁移与缺页</td><td>计算 IPC 并判断 Cache/调度压力</td></tr>
|
||||
<tr><td><code>case_numa_summary.csv</code></td><td>Node0/Node1 进程内存分布</td><td>识别跨 NUMA 放置</td></tr>
|
||||
<tr><td><code>case_netdev_summary.csv</code></td><td><code>eth0/eth3</code> 吞吐与错误</td><td>与 RDMA HCA Counter 做分层核对</td></tr>
|
||||
<tr><td><code>case_rdma_summary.csv</code></td><td>每 Case、节点、Rail 的吞吐和错误增量</td><td>判断双 Rail 使用、均衡和数据面错误</td></tr>
|
||||
<tr><td><code>communication_summary.csv</code></td><td>每次 P2P/AllReduce 原始测量</td><td>保留每条 GPU 对、消息尺寸、CROSS_NIC 和重复实验</td></tr>
|
||||
<tr><td><code>communication_aggregate.csv</code></td><td>PIX/SYS P2P 与单/双机 AllReduce 聚合</td><td>提供 P50/P95、algbw、busbw 和正确性比较</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>5.9 最终结果如何逐项汇报</h3>
|
||||
<p class="decision">
|
||||
最终 <code>report.md</code> 的章节顺序与本节一一对应。每一项必须同时给出
|
||||
<strong>原始文件、有效样本数、Head/Worker 数值、Case 间变化和解释</strong>;
|
||||
不能只写“GPU 较忙”“网络未饱和”这类抽象结论。
|
||||
</p>
|
||||
<table>
|
||||
<thead><tr><th>第 5 节指标</th><th>报告中的数值</th><th>最小分析动作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>5.1 时间窗</td><td>窗口来源、开始/结束、duration、采样数</td><td>确认全部为 <code>bench_main_marker_plus_duration</code></td></tr>
|
||||
<tr><td>5.2 GPU</td><td>利用率/显存/功耗/频率的 Mean、P95、Max</td><td>比较两节点、8 卡离散度和不同 Case</td></tr>
|
||||
<tr><td>5.3 DCGM</td><td>SM Active/Occupancy、Tensor/DRAM Active、PCIe TX/RX</td><td>比较计算、设备显存和 PCIe 哪一侧随负载上升</td></tr>
|
||||
<tr><td>5.4 CPU/进程/perf</td><td>整机/热点核、进程 CPU/I/O/缺页/切换、IPC/Cache miss</td><td>区分整机容量、单线程热点和 Host 调度开销</td></tr>
|
||||
<tr><td>5.5 NUMA</td><td>Node0/Node1 MiB 与比例</td><td>比较服务内存是否偏离 GPU/NIC 所在 NUMA</td></tr>
|
||||
<tr><td>5.6 Network/RDMA</td><td>eth0/eth3、mlx5_0/mlx5_3 Gbit/s 与错误增量</td><td>计算双 Rail 均衡比例并核对丢弃/重试</td></tr>
|
||||
<tr><td>5.7 Communication</td><td>PIX/SYS P2P、8/16 rank AllReduce P50/P95、algbw/busbw</td><td>比较跨 NUMA 损失与 CROSS_NIC 0/1/2</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
某个采集器无数据时报告显示 <code>-</code> 并附失败状态,不会把缺失值写成
|
||||
<code>0</code>。正式 Run 默认 <code>ALLOW_PARTIAL_COLLECTORS=0</code>,
|
||||
因此必需采集器提前退出会让 Run 失败。
|
||||
</p>
|
||||
|
||||
<h2>6. 精简代码设计</h2>
|
||||
<p>已新增目录:</p>
|
||||
<pre><code>/data/hzy/sskj/experiments/pro6000/
|
||||
@ -496,16 +573,17 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
|
||||
<tr><th>文件</th><th>职责</th><th>当前状态</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>run_hardware_contention_attribution.sh</code></td><td>唯一 Shell 入口;服务启停、双节点采集器、Case 编排、健康检查和 Trap 清理</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>config.env</code></td><td>Phase 1 相对路径、节点、五个固定 Case、混合 A/B 与采样策略</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>hardware_contention_attribution.py</code></td><td>Manifest、标记、Bench 校验、按 Case 时间窗切片和硬件摘要</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>tests/test_hardware_contention_attribution.py</code></td><td>GPU 统计、RDMA 单位、Marker、嵌套结果与时间窗测试</td><td class="pass">5/5 通过</td></tr>
|
||||
<tr><td><code>run_hardware_contention_attribution.sh</code></td><td>唯一 Shell 入口;通信基线、服务启停、双节点采集器、Case 编排、门禁和 Trap 清理</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>config.env</code></td><td>Phase 1 相对路径、节点、代表 Case、分层采样周期、通信基线与 fail-closed 策略</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>communication_baseline.py</code></td><td>CUDA P2P 全矩阵与 PyTorch/NCCL 8/16-rank AllReduce 微基准</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>hardware_contention_attribution.py</code></td><td>精确窗口、全部采集器解析、逐 Case 汇总、通信聚合和逐指标报告</td><td class="pass">已实现</td></tr>
|
||||
<tr><td><code>tests/test_hardware_contention_attribution.py</code></td><td>GPU/RDMA、精确窗口、DCGM/CPU 解析、通信聚合和结果生成测试</td><td class="pass">8/8 通过</td></tr>
|
||||
<tr><td><code>README.md</code></td><td>唯一入口、范围和结果目录说明</td><td class="pass">已实现</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="decision">
|
||||
Phase 2 不复制双机 Docker 启停实现。唯一入口在内部调用 Phase 1 的
|
||||
<code>run_quick_map.sh start/fixed/mixed/stop</code>,只新增硬件采集、时间对齐和代表负载编排。
|
||||
<code>run_quick_map.sh start/fixed/mixed/stop</code>,只新增通信基线、硬件采集、时间对齐和代表负载编排。
|
||||
顶层仍只保留一个 Shell 文件,不创建额外 tmux、launch、start 或 stop 脚本。
|
||||
</p>
|
||||
|
||||
@ -522,6 +600,14 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
|
||||
worker_server_cmd.txt
|
||||
head_server.log
|
||||
worker_server.log
|
||||
communication/
|
||||
p2p_head.log
|
||||
p2p_worker.log
|
||||
allreduce_head_8gpu.log
|
||||
allreduce_worker_8gpu.log
|
||||
allreduce_two_node_x0.log
|
||||
allreduce_two_node_x1.log
|
||||
allreduce_two_node_x2.log
|
||||
head/
|
||||
gpu_samples.csv
|
||||
dcgm_dmon.log
|
||||
@ -530,7 +616,7 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
|
||||
sar_net.log
|
||||
perf_stat.log
|
||||
docker_top.log
|
||||
numastat.log
|
||||
numa_samples.csv
|
||||
rdma.csv
|
||||
static_before.log
|
||||
static_after.log
|
||||
@ -543,19 +629,27 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
|
||||
gpu_summary.csv
|
||||
rdma_summary.csv
|
||||
case_windows.csv
|
||||
communication_summary.csv
|
||||
communication_aggregate.csv
|
||||
case_gpu_summary.csv
|
||||
case_gpu_node_summary.csv
|
||||
case_dcgm_summary.csv
|
||||
case_cpu_summary.csv
|
||||
case_process_summary.csv
|
||||
case_perf_summary.csv
|
||||
case_numa_summary.csv
|
||||
case_netdev_summary.csv
|
||||
case_rdma_summary.csv
|
||||
summary.json
|
||||
report.md</code></pre>
|
||||
|
||||
<h2>8. 验收结果</h2>
|
||||
<ul>
|
||||
<li class="pass">五个固定负载和混合 A/B 共 8 行全部成功,口径与 Phase 1 一致。</li>
|
||||
<li class="pass">Head/Worker GPU、CPU、NUMA、RDMA 采集完成,Case 时间窗和结构化摘要已生成。</li>
|
||||
<li class="pass">两端 NCCL 日志确认 <code>mlx5_0/mlx5_3</code> 双 Rail <code>NET/IB + GDRDMA</code>。</li>
|
||||
<li class="pass">服务和采集器完成清理,两节点 16 张 GPU 均已释放。</li>
|
||||
<li>Worker DCGM 因宿主 <code>nvidia-dcgm</code> 未启动而提前退出;本 Run 的 DCGM 分析只使用 Head 数据。</li>
|
||||
<li>当前 Case 时间窗包含客户端准备和 Warm-up;后续使用正式主测量起止 Marker 获得精确硬件窗口。</li>
|
||||
<li class="pass">最终代码固定到提交 <code>30664faa41f8</code>;Shell 语法检查和三个 Python 文件编译通过。</li>
|
||||
<li class="pass">Phase 1 精确窗口 4/4 单元测试通过;Phase 2 采集、解析与通信聚合 8/8 单元测试通过。</li>
|
||||
<li class="pass">完整 Dry-run 不启动服务即可展开全部模型、采集和通信命令;通信子流程生成 2 个 P2P、2 个单机 AllReduce、6 个双机 rank 命令。</li>
|
||||
<li class="pass">最终代码默认要求精确测量窗口、两端 DCGM 可用和全部必需采集器存活,缺失时 fail-closed。</li>
|
||||
<li>首次 Run 的性能数据保留在第 10 节;最终代码尚未在 16 卡上复跑,因此新通信基线和逐指标表暂不填写虚构数值。</li>
|
||||
</ul>
|
||||
|
||||
<h2>9. 实施记录</h2>
|
||||
@ -570,10 +664,11 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
|
||||
<tr><td>2026-07-31 12:26:00 CST</td><td>本地验证</td><td><code>bash -n</code>、Python 编译、5 项单元测试与全流程 Dry-run 通过;未占用 GPU</td></tr>
|
||||
<tr><td>2026-07-31 13:27:51 CST</td><td>完成正式双机 Run</td><td>Run <code>dsv4pro-phase2-20260731-130125</code>:8/8 benchmark 成功,总用时 26 分 26 秒,无 OOM</td></tr>
|
||||
<tr><td>2026-07-31 13:40:03 CST</td><td>完成首轮结果归因</td><td>排除原始双 Rail 带宽饱和、整机 CPU 饱和和频率塌陷作为首要原因;锁定 TP16 Kernel、调度与同步时间线</td></tr>
|
||||
<tr><td>2026-07-31 15:25:00 CST</td><td>完成最终 Phase 2 代码</td><td>提交 <code>30664faa41f8</code>:精确主测量窗口、双节点 DCGM 门禁、5 秒低开销 Host 采样、PCIe/NCCL 基线和逐指标自动报告均已通过本地验证</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>10. 真机结果</h2>
|
||||
<h2>10. 首次真机结果</h2>
|
||||
<p class="decision">
|
||||
<strong>Run:<code>dsv4pro-phase2-20260731-130125</code>,状态
|
||||
<code>COMPLETED</code>。</strong>运行时间为 13:01:25 至 13:27:51 CST,
|
||||
@ -634,17 +729,19 @@ tmux new-session -d -s dsv4pro-phase2 \
|
||||
<tr><th>范围</th><th>实际路径</th><th>本轮是否测量</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>单机 8 卡内部</td><td>无 NVLink;NCCL <code>P2P/IPC</code> 走 PCIe。GPU0–3、GPU4–7 各自在 PCIe Switch 内为 <code>PIX</code>,两组之间为 <code>SYS</code></td><td>采集了 DCGM PCIe 指标;未做独立 P2P 带宽/延迟微基准</td></tr>
|
||||
<tr><td>单机 8 卡内部</td><td>无 NVLink;NCCL <code>P2P/IPC</code> 走 PCIe。GPU0–3、GPU4–7 各自在 PCIe Switch 内为 <code>PIX</code>,两组之间为 <code>SYS</code></td><td>首次 Run 仅有 DCGM PCIe 指标;最终代码已加入所有 GPU 对的 256 MiB P2P 微基准</td></tr>
|
||||
<tr><td>两机之间</td><td><code>mlx5_0 + mlx5_3</code> 双 Rail <code>NET/IB + GDRDMA</code></td><td>已测量每 Case HCA 流量、均衡性和错误增量</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
在进入 Phase 3 前只需一次性补 <code>p2pBandwidthLatencyTest</code>、单机 8 卡
|
||||
<code>all_reduce_perf</code> 和双机 16 卡 <code>all_reduce_perf</code>,
|
||||
分开量化机内 PCIe 与跨机 RoCE 的硬件基线;后续阶段不重复跑这些微基准。
|
||||
最终代码使用当前 SGLang 镜像内的 PyTorch/CUDA/NCCL 实现等价微基准:
|
||||
两节点 P2P 全矩阵、两组单机 8-rank AllReduce 和三组双机 16-rank
|
||||
<code>NCCL_CROSS_NIC</code> A/B。结果统一写入
|
||||
<code>communication_summary.csv</code> 与 <code>communication_aggregate.csv</code>;
|
||||
完成 Phase 2 后,后续阶段不重复跑这些微基准。
|
||||
</p>
|
||||
|
||||
<h3>10.5 首轮结论与采集限制</h3>
|
||||
<h3>10.5 首轮结论与最终代码修正</h3>
|
||||
<p class="decision">
|
||||
当前证据支持把下一步缩到 TP16 的 Kernel、Scheduler、PCIe/RDMA Collective
|
||||
和 Rank 同步时间线。Phase 3 只分析真实请求中通信出现的位置、耗时和与计算的
|
||||
@ -652,9 +749,10 @@ tmux new-session -d -s dsv4pro-phase2 \
|
||||
CPU 容量和降频都不像首要瓶颈;Phase 3 将继续定位具体 Kernel 和同步根因。
|
||||
</p>
|
||||
<ul>
|
||||
<li>Worker 的 <code>nvidia-dcgm</code> Host Engine 未启动,导致 Worker DCGM 提前退出。重跑前只需在 <code>.7</code> 启动 DCGM;整套 Phase 2 仍只在 <code>.5</code> 执行。</li>
|
||||
<li>当前 Case 时间窗包含客户端启动、数据准备、Warm-up 和退出,硬件平均值被非正式测量阶段稀释;后续要增加主 benchmark 精确起止标记。</li>
|
||||
<li><code>pidstat -durwt</code> 一秒采样生成约 850/735 MB 日志;后续改为进程级或 5 秒采样,减少诊断扰动。</li>
|
||||
<li>Worker DCGM 缺失已改为双节点启动前门禁;最终 Run 不再接受单边 DCGM 数据。</li>
|
||||
<li>宽泛 Case 窗口已改为正式 benchmark 起点加 duration 的精确窗口,并由 <code>REQUIRE_PRECISE_WINDOWS=1</code> 强制执行。</li>
|
||||
<li><code>pidstat/mpstat/perf/sar/numastat</code> 已改为进程级 5 秒采样,保留时间戳并降低诊断扰动。</li>
|
||||
<li>最终 <code>report.md</code> 已按第 5 节逐项输出数值表;缺失采集显示为 <code>-</code>,不会解释为 0。</li>
|
||||
<li>完整归因见 <a href="./results/dsv4pro-phase2-20260731-130125/analysis.md"><code>analysis.md</code></a>,原始结构化摘要和两端 NCCL 证据已一并归档。</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@ -415,7 +415,7 @@
|
||||
<article id="document-content">
|
||||
<h1>6000D 双机 DeepSeek-V4-Pro 推理优化计划</h1>
|
||||
<blockquote>
|
||||
<p>适用环境:<code>174.1.51.5 + 174.1.51.7</code>,每台 8 张 RTX PRO 6000 Blackwell Server Edition<br>当前部署:DeepSeek-V4-Pro,16 张 GPU 组成一个完整实例<br>当前约束:模型暂时只能使用全部 16 张 GPU,无法额外复制一套模型进行 PD 分离<br>计划版本:2026-07-31 13:40:03 CST</p>
|
||||
<p>适用环境:<code>174.1.51.5 + 174.1.51.7</code>,每台 8 张 RTX PRO 6000 Blackwell Server Edition<br>当前部署:DeepSeek-V4-Pro,16 张 GPU 组成一个完整实例<br>当前约束:模型暂时只能使用全部 16 张 GPU,无法额外复制一套模型进行 PD 分离<br>计划版本:2026-07-31 15:25:00 CST</p>
|
||||
</blockquote>
|
||||
<h2>当前执行状态与阶段档案</h2>
|
||||
<table>
|
||||
@ -439,7 +439,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DeepSeek-V4-Pro / 双机 Pro6000D / SGLang 硬件与资源竞争归因</td>
|
||||
<td>首轮正式 Run 已完成;8/8 benchmark 成功,已获得 GPU、CPU 与双 Rail RDMA 瓶颈方向</td>
|
||||
<td>最终采集代码已完成;首轮 8/8 成功,精确窗口、双节点 DCGM、PCIe/NCCL 基线和逐指标报告等待最终复跑</td>
|
||||
<td><a href="./phase2_dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution.html">打开 Phase 2 档案</a></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
@ -480,8 +480,8 @@
|
||||
GPU0–3 与 GPU4–7 各自在本地 PCIe Switch 内通信,两组之间还要经过 Host Bridge
|
||||
和 CPU/NUMA 互联。NCCL 机内日志中的 <code>P2P/IPC</code> 是 CUDA P2P over PCIe;
|
||||
两机之间则使用 <code>mlx5_0/mlx5_3</code> 双 Rail
|
||||
<code>NET/IB + GDRDMA</code>。Phase 2 已测跨机 HCA 流量,但机内 PCIe 的独立
|
||||
GPU-to-GPU 带宽和延迟仍需微基准补齐。
|
||||
<code>NET/IB + GDRDMA</code>。Phase 2 最终代码已加入机内 PCIe P2P 全矩阵、
|
||||
单机 8-rank 和双机 16-rank NCCL 微基准;正式数值在最终复跑后写入阶段档案。
|
||||
</p>
|
||||
<h2>1. 目标与原则</h2>
|
||||
<h3>1.1 最终目标</h3>
|
||||
@ -539,7 +539,7 @@ GPU-to-GPU 带宽和延迟仍需微基准补齐。
|
||||
<tr>
|
||||
<td>H6</td>
|
||||
<td>KV Cache 容量、碎片或 Preemption 限制并发</td>
|
||||
<td>大模型权重占用高,剩余 HBM 决定上下文与并发容量</td>
|
||||
<td>大模型权重占用高,剩余设备显存决定上下文与并发容量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>H7</td>
|
||||
@ -735,9 +735,9 @@ TPOT P95 增加 66.55%。Phase 2 将围绕这两个现象采集硬件时间序
|
||||
<a href="./phase2_dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution.html">
|
||||
Phase 2:硬件与资源竞争归因档案</a>。本阶段重放长 Prefill、并发 Prefill、
|
||||
普通 Decode、长输出 Decode、长上下文 Decode,以及
|
||||
<code>1K → 1K, C=32</code> 的混合 A/B。正式 Run
|
||||
<code>dsv4pro-phase2-20260731-130125</code> 已在 26 分 26 秒内完成 8/8 个结果,
|
||||
无 OOM;本阶段先停在首轮归因,不自动进入 Phase 3。
|
||||
<code>1K → 1K, C=32</code> 的混合 A/B。首轮 Run
|
||||
<code>dsv4pro-phase2-20260731-130125</code> 在 26 分 26 秒内完成 8/8 个结果;
|
||||
最终采集代码固定到提交 <code>30664faa41f8</code>,完成最终复跑和汇报后才进入 Phase 3。
|
||||
</p>
|
||||
<h3>6.1 首轮归因结果</h3>
|
||||
<ul>
|
||||
@ -745,91 +745,34 @@ Phase 2:硬件与资源竞争归因档案</a>。本阶段重放长 Prefill、
|
||||
<li>128K Prefill 注入窗口两节点 GPU 平均利用率约 99%,功耗约 274 W,频率稳定;显存每卡约 83.2–83.4 GiB,只剩约 2.3 GiB 余量。</li>
|
||||
<li>整机 CPU 平均约 8%–11%,没有全机 CPU 或 I/O Wait 饱和,但少量 CPU 核持续高负载,Scheduler/Affinity/NUMA 热点仍需关注。</li>
|
||||
<li>双 Rail 流量对称且错误增量为 0;最高平均总发送约 140 Gbit/s,即每条 400G Rail 约 70 Gbit/s,原始 RoCE 带宽未饱和。</li>
|
||||
<li>下一步应捕获短 TP16 Timeline,区分 GPU Kernel、Scheduler gap、机内 PCIe、跨机 Collective 与 Rank 同步,而不是重跑完整矩阵。</li>
|
||||
</ul>
|
||||
<p>
|
||||
首轮仍有两个采集限制:Worker 的 DCGM Host Engine 未启动,因此只有 Head DCGM
|
||||
有效;Case 时间窗包含客户端准备和 Warm-up,硬件均值并非纯主测量窗口。重跑前应先在
|
||||
<code>.7</code> 启动 <code>nvidia-dcgm</code>,并增加主 benchmark 精确起止标记。
|
||||
最终代码已经把首轮的采集限制变成强制门禁:两节点
|
||||
<code>dcgmi discovery -l</code> 必须成功;每个 Case 使用
|
||||
<code>Starting main benchmark run</code> 加 SGLang duration 形成正式测量窗口;
|
||||
必需采集器提前退出会让 Run 失败。
|
||||
</p>
|
||||
<h3>6.2 GPU</h3>
|
||||
<p>测试期间持续记录:</p>
|
||||
<pre><code class="language-bash">nvidia-smi dmon -s pucvmt -d 1
|
||||
</code></pre>
|
||||
<p>重点观察:</p>
|
||||
<h3>6.2 最终采集范围</h3>
|
||||
<ul>
|
||||
<li>SM Utilization。</li>
|
||||
<li>HBM Utilization。</li>
|
||||
<li>显存占用。</li>
|
||||
<li>GPU Clock、Memory Clock。</li>
|
||||
<li>Power 与温度降频。</li>
|
||||
<li>PCIe RX/TX。</li>
|
||||
<li><code>nvidia-smi</code>:GPU/显存利用率、显存、功耗、温度、SM/Memory Clock 和 P-state,每秒一次。</li>
|
||||
<li>DCGM:SM Active/Occupancy、Tensor Active、设备显存接口 Active、PCIe TX/RX,每秒一次。</li>
|
||||
<li><code>mpstat/pidstat/perf/numastat</code>:整机、热点核、服务进程、CPU 计数器和 NUMA 内存,每 5 秒一次。</li>
|
||||
<li><code>sar</code> 与 HCA Counter:<code>eth0/eth3</code> 和 <code>mlx5_0/mlx5_3</code> 的吞吐、均衡、错误与重试。</li>
|
||||
<li>通信基线:两节点 CUDA P2P 全矩阵、两组 8-rank AllReduce、三组 16-rank <code>NCCL_CROSS_NIC=0/1/2</code>。</li>
|
||||
</ul>
|
||||
<p>如果有 DCGM,增加:</p>
|
||||
<ul>
|
||||
<li>Tensor Core Active。</li>
|
||||
<li>DRAM Active。</li>
|
||||
<li>SM Active。</li>
|
||||
<li>PCIe Throughput。</li>
|
||||
<li>GPU Stall 与 XID。</li>
|
||||
</ul>
|
||||
<h3>6.3 CPU</h3>
|
||||
<p>记录服务主进程与 Worker 线程:</p>
|
||||
<pre><code class="language-bash">pidstat -t -p <PID> 1
|
||||
mpstat -P ALL 1
|
||||
numastat -p <PID>
|
||||
</code></pre>
|
||||
<p>需要发现:</p>
|
||||
<ul>
|
||||
<li>单个 Scheduler Thread 是否满核。</li>
|
||||
<li>Tokenizer、HTTP Frontend 或 Python 线程是否阻塞。</li>
|
||||
<li>Worker 是否跨 NUMA 访问。</li>
|
||||
<li>CPU 空洞是否对应 GPU 空洞。</li>
|
||||
</ul>
|
||||
<h3>6.4 网络</h3>
|
||||
<h3>6.3 数据与报告规则</h3>
|
||||
<p>
|
||||
Phase 1 正式 Run 已从两端 NCCL 日志确认
|
||||
<code>NET/IB</code> 同时识别 <code>mlx5_0/mlx5_3</code>,跨节点 Channel
|
||||
使用 <code>GDRDMA</code>。Phase 2 继续保留相同 fail-closed 门禁,并采集两条
|
||||
Rail 的流量和错误计数。
|
||||
每种指标都生成独立的 <code>case_*_summary.csv</code>。最终
|
||||
<code>report.md</code> 必须逐项列出原始文件、有效样本数、Head/Worker 数值、
|
||||
Mean/P95/Max、Case 间变化和解释,不能只写抽象结论。缺失数据写
|
||||
<code>-</code> 并报告采集器状态,不能当成 0。
|
||||
</p>
|
||||
<p>当前拓扑中需要分别观察两条 Compute Rail,确认:</p>
|
||||
<ul>
|
||||
<li>两条 Rail 是否同时有流量。</li>
|
||||
<li>带宽是否均衡。</li>
|
||||
<li>是否有丢包、重传、PFC Pause 或错误计数。</li>
|
||||
<li>慢 Rank 是否固定绑定某个 NIC 或 NUMA 节点。</li>
|
||||
</ul>
|
||||
<p>基础监控可以使用:</p>
|
||||
<pre><code class="language-bash">sar -n DEV 1
|
||||
ethtool -S eth0
|
||||
ethtool -S eth3
|
||||
</code></pre>
|
||||
<p>通信调试 Run 可以临时开启:</p>
|
||||
<pre><code class="language-bash">NCCL_DEBUG=INFO
|
||||
NCCL_DEBUG_SUBSYS=INIT,NET,GRAPH,TUNING
|
||||
</code></pre>
|
||||
<p>该日志开销较高,不应在正式性能结果中长期启用。</p>
|
||||
<h3>6.5 NCCL_CROSS_NIC 快速 A/B</h3>
|
||||
<h3>6.4 与 Phase 3 的边界</h3>
|
||||
<p>
|
||||
Phase 1 已证明 <code>NCCL_CROSS_NIC=1</code> 可以稳定完成端到端 Run,
|
||||
但这不代表它是性能最优值。固定其余环境后比较 <code>0/1/2</code>,
|
||||
不能仅凭双 Rail 拓扑判断。
|
||||
Phase 2 回答“哪个硬件/Host/通信资源在什么 Case 中升高,以及硬件基线是多少”。
|
||||
Phase 3 只捕获短 TP16 Timeline,回答具体 Kernel、Scheduler gap、Collective、
|
||||
Rank 同步及计算/通信重叠,不重复 Phase 2 的长时间轻量采样。
|
||||
</p>
|
||||
<ol>
|
||||
<li>分别执行相同消息范围的 <code>all_reduce_perf</code>,每个值至少重复 3 次,检查错误、algbw 和 busbw。</li>
|
||||
<li>通过 NCCL 调试日志及两端 NIC 计数器确认实际 <code>mlx5_0/mlx5_3</code> 映射、双 Rail 使用率和流量均衡。</li>
|
||||
<li>每个值重启同配置 SGLang 服务,仅重放一个 Decode 高并发代表点,比较 Output TPS、TPOT P95 与稳定性。</li>
|
||||
<li>先排除不正确或不稳定的值,再比较 NCCL 中位带宽,最终以 SGLang 端到端结果决定生产值。</li>
|
||||
</ol>
|
||||
<h3>6.6 与其他阶段的组合边界</h3>
|
||||
<ul>
|
||||
<li>Phase 1 保留一份不启用 Profiler 的端到端基线,避免 TPS 和时延被诊断工具污染。</li>
|
||||
<li>GPU、CPU 和网络的轻量采样可以伴随后续基线运行,但必须从 Case 开始前启动,并使用统一时间戳与 Case ID 对齐。</li>
|
||||
<li>Phase 1 已完整结束,其无 Profiler 结果作为后续 A/B 的请求层基线。</li>
|
||||
<li>Phase 2 已重放五个固定代表负载和一组混合 A/B,并完成首轮轻量采集;下一步只需补齐 Worker DCGM 和精确时间窗,然后决定 Phase 3 的短 Timeline。</li>
|
||||
<li>联合诊断 Run 的吞吐和时延只用于解释时间线;正式性能变化仍与 Phase 1 的无 Profiler 结果比较。</li>
|
||||
</ul>
|
||||
<h2>7. Phase 3:时间线 Profiling(Nsight Systems 为主)</h2>
|
||||
<p>
|
||||
Nsight Systems、PyTorch Profiler 和 NVTX 是三件不同的东西:Nsight Systems
|
||||
@ -976,7 +919,7 @@ SGLang 进程,并绑定相同 Case ID 和时间窗口。正式执行前先验
|
||||
</tr>
|
||||
<tr>
|
||||
<td>KV Cache 长期接近满并发生重算</td>
|
||||
<td>HBM 容量不足</td>
|
||||
<td>设备显存容量不足</td>
|
||||
<td>FP8 KV、并发和 Context 上限</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small PCIe P2P and NCCL AllReduce baseline for the Phase 2 entry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
|
||||
RESULT_PREFIX = "COMM_RESULT "
|
||||
|
||||
|
||||
def parse_size(value: str) -> int:
|
||||
text = value.strip().upper()
|
||||
units = {"K": 1 << 10, "M": 1 << 20, "G": 1 << 30}
|
||||
if text[-1:] in units:
|
||||
return int(float(text[:-1]) * units[text[-1]])
|
||||
return int(text)
|
||||
|
||||
|
||||
def percentile(values: list[float], quantile: float) -> float:
|
||||
ordered = sorted(values)
|
||||
if not ordered:
|
||||
return math.nan
|
||||
if len(ordered) == 1:
|
||||
return ordered[0]
|
||||
rank = (len(ordered) - 1) * quantile
|
||||
lower = math.floor(rank)
|
||||
upper = math.ceil(rank)
|
||||
weight = rank - lower
|
||||
return ordered[lower] * (1 - weight) + ordered[upper] * weight
|
||||
|
||||
|
||||
def emit(value: dict[str, Any]) -> None:
|
||||
print(RESULT_PREFIX + json.dumps(value, sort_keys=True), flush=True)
|
||||
|
||||
|
||||
def run_p2p(args: argparse.Namespace) -> None:
|
||||
device_count = torch.cuda.device_count()
|
||||
size_bytes = parse_size(args.size)
|
||||
elements = max(1, size_bytes // torch.tensor([], dtype=torch.float16).element_size())
|
||||
for source in range(device_count):
|
||||
for destination in range(device_count):
|
||||
if source == destination:
|
||||
continue
|
||||
supported = torch.cuda.can_device_access_peer(source, destination)
|
||||
if not supported:
|
||||
emit(
|
||||
{
|
||||
"test": "p2p_copy",
|
||||
"node": args.node,
|
||||
"source_gpu": source,
|
||||
"destination_gpu": destination,
|
||||
"peer_access": False,
|
||||
"size_bytes": size_bytes,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
with torch.cuda.device(source):
|
||||
source_tensor = torch.ones(elements, dtype=torch.float16, device=source)
|
||||
with torch.cuda.device(destination):
|
||||
destination_tensor = torch.empty(
|
||||
elements,
|
||||
dtype=torch.float16,
|
||||
device=destination,
|
||||
)
|
||||
for _ in range(args.warmup):
|
||||
destination_tensor.copy_(source_tensor, non_blocking=True)
|
||||
torch.cuda.synchronize(destination)
|
||||
|
||||
samples_ms: list[float] = []
|
||||
for _ in range(args.iterations):
|
||||
start = torch.cuda.Event(enable_timing=True)
|
||||
end = torch.cuda.Event(enable_timing=True)
|
||||
start.record()
|
||||
destination_tensor.copy_(source_tensor, non_blocking=True)
|
||||
end.record()
|
||||
end.synchronize()
|
||||
samples_ms.append(start.elapsed_time(end))
|
||||
|
||||
mean_ms = statistics.fmean(samples_ms)
|
||||
emit(
|
||||
{
|
||||
"test": "p2p_copy",
|
||||
"node": args.node,
|
||||
"source_gpu": source,
|
||||
"destination_gpu": destination,
|
||||
"peer_access": True,
|
||||
"size_bytes": size_bytes,
|
||||
"iterations": args.iterations,
|
||||
"mean_ms": mean_ms,
|
||||
"p50_ms": percentile(samples_ms, 0.50),
|
||||
"p95_ms": percentile(samples_ms, 0.95),
|
||||
"bandwidth_GBps": size_bytes / (mean_ms / 1000.0) / 1e9,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def run_all_reduce(args: argparse.Namespace) -> None:
|
||||
dist.init_process_group("nccl")
|
||||
rank = dist.get_rank()
|
||||
world_size = dist.get_world_size()
|
||||
local_rank = int(os.environ["LOCAL_RANK"])
|
||||
torch.cuda.set_device(local_rank)
|
||||
|
||||
for size_text in args.sizes.split(","):
|
||||
size_bytes = parse_size(size_text)
|
||||
elements = max(
|
||||
1,
|
||||
size_bytes // torch.tensor([], dtype=torch.float32).element_size(),
|
||||
)
|
||||
tensor = torch.empty(elements, dtype=torch.float32, device=local_rank)
|
||||
expected = world_size * (world_size + 1) / 2
|
||||
|
||||
for repetition in range(1, args.repetitions + 1):
|
||||
for _ in range(args.warmup):
|
||||
tensor.fill_(rank + 1)
|
||||
dist.all_reduce(tensor)
|
||||
torch.cuda.synchronize(local_rank)
|
||||
|
||||
local_samples_ms: list[float] = []
|
||||
for _ in range(args.iterations):
|
||||
tensor.fill_(rank + 1)
|
||||
torch.cuda.synchronize(local_rank)
|
||||
dist.barrier()
|
||||
start = torch.cuda.Event(enable_timing=True)
|
||||
end = torch.cuda.Event(enable_timing=True)
|
||||
start.record()
|
||||
dist.all_reduce(tensor)
|
||||
end.record()
|
||||
end.synchronize()
|
||||
local_samples_ms.append(start.elapsed_time(end))
|
||||
|
||||
wrong_values = int(
|
||||
not torch.allclose(
|
||||
tensor[0],
|
||||
torch.tensor(expected, device=local_rank),
|
||||
)
|
||||
)
|
||||
gathered_samples: list[list[float] | None] = [None] * world_size
|
||||
gathered_wrong: list[int | None] = [None] * world_size
|
||||
dist.all_gather_object(gathered_samples, local_samples_ms)
|
||||
dist.all_gather_object(gathered_wrong, wrong_values)
|
||||
if rank == 0:
|
||||
per_iteration_max = [
|
||||
max(
|
||||
float(samples[index])
|
||||
for samples in gathered_samples
|
||||
if samples is not None
|
||||
)
|
||||
for index in range(args.iterations)
|
||||
]
|
||||
mean_ms = statistics.fmean(per_iteration_max)
|
||||
algbw = size_bytes / (mean_ms / 1000.0) / 1e9
|
||||
emit(
|
||||
{
|
||||
"test": "all_reduce",
|
||||
"scope": args.scope,
|
||||
"world_size": world_size,
|
||||
"size_bytes": size_bytes,
|
||||
"repetition": repetition,
|
||||
"iterations": args.iterations,
|
||||
"mean_ms": mean_ms,
|
||||
"p50_ms": percentile(per_iteration_max, 0.50),
|
||||
"p95_ms": percentile(per_iteration_max, 0.95),
|
||||
"algbw_GBps": algbw,
|
||||
"busbw_GBps": algbw * 2 * (world_size - 1) / world_size,
|
||||
"wrong_values": sum(
|
||||
int(value or 0) for value in gathered_wrong
|
||||
),
|
||||
"nccl_cross_nic": os.environ.get(
|
||||
"NCCL_CROSS_NIC",
|
||||
"",
|
||||
),
|
||||
"nccl_socket_ifname": os.environ.get(
|
||||
"NCCL_SOCKET_IFNAME",
|
||||
"",
|
||||
),
|
||||
"nccl_ib_hca": os.environ.get("NCCL_IB_HCA", ""),
|
||||
"torch_version": torch.__version__,
|
||||
"cuda_version": torch.version.cuda,
|
||||
"nccl_version": ".".join(
|
||||
str(part) for part in torch.cuda.nccl.version()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p2p = subparsers.add_parser("p2p")
|
||||
p2p.add_argument("--node", required=True)
|
||||
p2p.add_argument("--size", default="256M")
|
||||
p2p.add_argument("--warmup", type=int, default=3)
|
||||
p2p.add_argument("--iterations", type=int, default=10)
|
||||
|
||||
all_reduce = subparsers.add_parser("all-reduce")
|
||||
all_reduce.add_argument("--scope", required=True)
|
||||
all_reduce.add_argument("--sizes", default="1M,64M,1G")
|
||||
all_reduce.add_argument("--repetitions", type=int, default=3)
|
||||
all_reduce.add_argument("--warmup", type=int, default=5)
|
||||
all_reduce.add_argument("--iterations", type=int, default=10)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
if args.command == "p2p":
|
||||
run_p2p(args)
|
||||
else:
|
||||
run_all_reduce(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -21,6 +21,10 @@ RUN_MIXED_CASE="${RUN_MIXED_CASE:-1}"
|
||||
|
||||
# Monitoring policy.
|
||||
SAMPLE_INTERVAL_S="${SAMPLE_INTERVAL_S:-1}"
|
||||
CPU_SAMPLE_INTERVAL_S="${CPU_SAMPLE_INTERVAL_S:-5}"
|
||||
PROCESS_SAMPLE_INTERVAL_S="${PROCESS_SAMPLE_INTERVAL_S:-5}"
|
||||
NET_SAMPLE_INTERVAL_S="${NET_SAMPLE_INTERVAL_S:-5}"
|
||||
PERF_INTERVAL_MS="${PERF_INTERVAL_MS:-5000}"
|
||||
IDLE_BASELINE_S="${IDLE_BASELINE_S:-15}"
|
||||
POST_RUN_COOLDOWN_S="${POST_RUN_COOLDOWN_S:-15}"
|
||||
CASE_COOLDOWN_S="${CASE_COOLDOWN_S:-5}"
|
||||
@ -30,6 +34,23 @@ PERF_EVENTS="${PERF_EVENTS:-cycles,instructions,cache-misses,context-switches,cp
|
||||
RDMA_HCAS="${RDMA_HCAS:-mlx5_0 mlx5_3}"
|
||||
NUMASTAT_INTERVAL_S="${NUMASTAT_INTERVAL_S:-5}"
|
||||
CLOCK_SKEW_TOLERANCE_S="${CLOCK_SKEW_TOLERANCE_S:-2}"
|
||||
REQUIRE_PRECISE_WINDOWS="${REQUIRE_PRECISE_WINDOWS:-1}"
|
||||
|
||||
# One-time communication baseline. It runs before the model service starts.
|
||||
RUN_COMMUNICATION_BASELINE="${RUN_COMMUNICATION_BASELINE:-1}"
|
||||
COMMUNICATION_IMAGE="${COMMUNICATION_IMAGE:-lmsysorg/sglang:nightly-dev-cu13-20260720-b3570a45}"
|
||||
COMMUNICATION_MASTER_PORT="${COMMUNICATION_MASTER_PORT:-29620}"
|
||||
COMMUNICATION_SIZES="${COMMUNICATION_SIZES:-1M,64M,1G}"
|
||||
COMMUNICATION_REPETITIONS="${COMMUNICATION_REPETITIONS:-3}"
|
||||
COMMUNICATION_WARMUP="${COMMUNICATION_WARMUP:-5}"
|
||||
COMMUNICATION_ITERATIONS="${COMMUNICATION_ITERATIONS:-10}"
|
||||
P2P_SIZE="${P2P_SIZE:-256M}"
|
||||
P2P_WARMUP="${P2P_WARMUP:-3}"
|
||||
P2P_ITERATIONS="${P2P_ITERATIONS:-10}"
|
||||
CROSS_NIC_VALUES="${CROSS_NIC_VALUES:-0 1 2}"
|
||||
NCCL_SOCKET_IFNAME="${NCCL_SOCKET_IFNAME:-=eth0}"
|
||||
NCCL_IB_HCA="${NCCL_IB_HCA:-=mlx5_0:1,mlx5_3:1}"
|
||||
RDMA_DEVICE_PATHS="${RDMA_DEVICE_PATHS:-/dev/infiniband/rdma_cm /dev/infiniband/uverbs0 /dev/infiniband/uverbs3}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
@ -37,4 +58,4 @@ RESULT_BASE="${RESULT_BASE:-${SCRIPT_DIR}/results}"
|
||||
RUNTIME_BASE="${RUNTIME_BASE:-${SCRIPT_DIR}/runtime}"
|
||||
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
ALLOW_PARTIAL_COLLECTORS="${ALLOW_PARTIAL_COLLECTORS:-1}"
|
||||
ALLOW_PARTIAL_COLLECTORS="${ALLOW_PARTIAL_COLLECTORS:-0}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ ACTION="${1:-all}"
|
||||
RUN_ID="${RUN_ID:-dsv4pro-phase2-$(date +%Y%m%d-%H%M%S)}"
|
||||
RESULT_DIR="${RESULT_BASE}/${RUN_ID}"
|
||||
RESULT_TOOL="${SCRIPT_DIR}/hardware_contention_attribution.py"
|
||||
COMMUNICATION_TOOL="${SCRIPT_DIR}/communication_baseline.py"
|
||||
MARKERS_PATH="${RESULT_DIR}/markers.csv"
|
||||
SERVICE_DIR="${RESULT_DIR}/service"
|
||||
COMMAND_DIR="${RESULT_DIR}/commands"
|
||||
@ -72,6 +73,10 @@ validate_config() {
|
||||
log "ERROR: result tool is missing or not executable: ${RESULT_TOOL}"
|
||||
return 1
|
||||
}
|
||||
[[ -f "${COMMUNICATION_TOOL}" ]] || {
|
||||
log "ERROR: communication tool is missing: ${COMMUNICATION_TOOL}"
|
||||
return 1
|
||||
}
|
||||
case "${RUN_MIXED_CASE}" in
|
||||
0|1) ;;
|
||||
*)
|
||||
@ -86,6 +91,20 @@ validate_config() {
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "${REQUIRE_PRECISE_WINDOWS}" in
|
||||
0|1) ;;
|
||||
*)
|
||||
log "ERROR: REQUIRE_PRECISE_WINDOWS must be 0 or 1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "${RUN_COMMUNICATION_BASELINE}" in
|
||||
0|1) ;;
|
||||
*)
|
||||
log "ERROR: RUN_COMMUNICATION_BASELINE must be 0 or 1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
[[ "${SAMPLE_INTERVAL_S}" =~ ^[1-9][0-9]*$ ]] || {
|
||||
log "ERROR: SAMPLE_INTERVAL_S must be a positive integer"
|
||||
return 1
|
||||
@ -94,6 +113,14 @@ validate_config() {
|
||||
log "ERROR: NUMASTAT_INTERVAL_S must be a positive integer"
|
||||
return 1
|
||||
}
|
||||
local interval_name
|
||||
for interval_name in CPU_SAMPLE_INTERVAL_S PROCESS_SAMPLE_INTERVAL_S \
|
||||
NET_SAMPLE_INTERVAL_S PERF_INTERVAL_MS; do
|
||||
[[ "${!interval_name}" =~ ^[1-9][0-9]*$ ]] || {
|
||||
log "ERROR: ${interval_name} must be a positive integer"
|
||||
return 1
|
||||
}
|
||||
done
|
||||
[[ "${CLOCK_SKEW_TOLERANCE_S}" =~ ^[0-9]+$ ]] || {
|
||||
log "ERROR: CLOCK_SKEW_TOLERANCE_S must be a non-negative integer"
|
||||
return 1
|
||||
@ -121,6 +148,13 @@ preflight_node_tools() {
|
||||
log "ERROR: ${node} is missing required tool: dcgmi"
|
||||
return 1
|
||||
fi
|
||||
elif ! run_on_node "${node}" "dcgmi discovery -l >/dev/null 2>&1"; then
|
||||
if [[ "${ALLOW_PARTIAL_COLLECTORS}" == "1" ]]; then
|
||||
log "WARN: ${node} DCGM Host Engine is unavailable"
|
||||
else
|
||||
log "ERROR: ${node} DCGM Host Engine is unavailable; run systemctl start nvidia-dcgm"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
local hca
|
||||
for hca in ${RDMA_HCAS}; do
|
||||
@ -149,6 +183,20 @@ preflight_clock_sync() {
|
||||
log "node clock skew check passed: delta=${delta}s"
|
||||
}
|
||||
|
||||
preflight_gpus_idle() {
|
||||
local node="$1"
|
||||
local active
|
||||
active="$(
|
||||
run_on_node "${node}" \
|
||||
"nvidia-smi --query-compute-apps=pid --format=csv,noheader,nounits 2>/dev/null | sed '/^[[:space:]]*$/d'" \
|
||||
|| true
|
||||
)"
|
||||
if [[ -n "${active}" ]]; then
|
||||
log "ERROR: ${node} has active GPU compute processes: ${active//$'\n'/,}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_manifest() {
|
||||
local git_commit git_dirty
|
||||
git_commit="$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || printf unknown)"
|
||||
@ -167,10 +215,16 @@ write_manifest() {
|
||||
--fixed-case-ids "${FIXED_CASE_IDS}" \
|
||||
--run-mixed-case "${RUN_MIXED_CASE}" \
|
||||
--sample-interval-s "${SAMPLE_INTERVAL_S}" \
|
||||
--cpu-sample-interval-s "${CPU_SAMPLE_INTERVAL_S}" \
|
||||
--process-sample-interval-s "${PROCESS_SAMPLE_INTERVAL_S}" \
|
||||
--net-sample-interval-s "${NET_SAMPLE_INTERVAL_S}" \
|
||||
--perf-interval-ms "${PERF_INTERVAL_MS}" \
|
||||
--numastat-interval-s "${NUMASTAT_INTERVAL_S}" \
|
||||
--clock-skew-tolerance-s "${CLOCK_SKEW_TOLERANCE_S}" \
|
||||
--idle-baseline-s "${IDLE_BASELINE_S}" \
|
||||
--post-run-cooldown-s "${POST_RUN_COOLDOWN_S}" \
|
||||
--require-precise-windows "${REQUIRE_PRECISE_WINDOWS}" \
|
||||
--run-communication-baseline "${RUN_COMMUNICATION_BASELINE}" \
|
||||
--dry-run "${DRY_RUN}"
|
||||
}
|
||||
|
||||
@ -212,6 +266,185 @@ run_phase1_action() {
|
||||
"${command[@]}"
|
||||
}
|
||||
|
||||
build_communication_docker_command() {
|
||||
local output_name="$1"
|
||||
local container_name="$2"
|
||||
local entrypoint="$3"
|
||||
local cross_nic="$4"
|
||||
shift 4
|
||||
local -a docker_cmd=(
|
||||
docker run --rm
|
||||
--name "${container_name}"
|
||||
--gpus all
|
||||
--network host
|
||||
--ipc host
|
||||
--shm-size 20g
|
||||
--ulimit memlock=-1
|
||||
--ulimit stack=67108864
|
||||
-v "${REPO_ROOT}:${REPO_ROOT}:ro"
|
||||
-e "NCCL_SOCKET_IFNAME=${NCCL_SOCKET_IFNAME}"
|
||||
-e "NCCL_IB_HCA=${NCCL_IB_HCA}"
|
||||
-e "NCCL_CROSS_NIC=${cross_nic}"
|
||||
-e NCCL_DEBUG=INFO
|
||||
-e NCCL_DEBUG_SUBSYS=INIT,NET,GRAPH,TUNING
|
||||
-e TORCH_NCCL_ASYNC_ERROR_HANDLING=1
|
||||
)
|
||||
local device
|
||||
for device in ${RDMA_DEVICE_PATHS}; do
|
||||
docker_cmd+=(--device "${device}")
|
||||
done
|
||||
docker_cmd+=(--entrypoint "${entrypoint}" "${COMMUNICATION_IMAGE}" "$@")
|
||||
printf -v "${output_name}" '%q ' "${docker_cmd[@]}"
|
||||
}
|
||||
|
||||
run_p2p_baseline() {
|
||||
local role="$1"
|
||||
local node="$2"
|
||||
local container="${EXPERIMENT}_comm_p2p_${role}_${RUN_ID}"
|
||||
local command
|
||||
build_communication_docker_command \
|
||||
command "${container}" python3 2 \
|
||||
"${COMMUNICATION_TOOL}" p2p \
|
||||
--node "${role}" \
|
||||
--size "${P2P_SIZE}" \
|
||||
--warmup "${P2P_WARMUP}" \
|
||||
--iterations "${P2P_ITERATIONS}"
|
||||
printf '%s\n' "${command}" \
|
||||
> "${COMMAND_DIR}/communication_p2p_${role}.cmd.txt"
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
log "[DRY] P2P baseline role=${role}: ${command}"
|
||||
return 0
|
||||
fi
|
||||
log "START P2P baseline role=${role}"
|
||||
run_on_node "${node}" "${command}" \
|
||||
> "${RESULT_DIR}/communication/p2p_${role}.log" 2>&1
|
||||
log "DONE P2P baseline role=${role}"
|
||||
}
|
||||
|
||||
run_single_node_allreduce() {
|
||||
local role="$1"
|
||||
local node="$2"
|
||||
local master_port="$3"
|
||||
local container="${EXPERIMENT}_comm_ar8_${role}_${RUN_ID}"
|
||||
local command
|
||||
build_communication_docker_command \
|
||||
command "${container}" torchrun 2 \
|
||||
--standalone \
|
||||
--nnodes=1 \
|
||||
--nproc-per-node=8 \
|
||||
--master-port "${master_port}" \
|
||||
"${COMMUNICATION_TOOL}" all-reduce \
|
||||
--scope "${role}_8gpu" \
|
||||
--sizes "${COMMUNICATION_SIZES}" \
|
||||
--repetitions "${COMMUNICATION_REPETITIONS}" \
|
||||
--warmup "${COMMUNICATION_WARMUP}" \
|
||||
--iterations "${COMMUNICATION_ITERATIONS}"
|
||||
printf '%s\n' "${command}" \
|
||||
> "${COMMAND_DIR}/communication_allreduce_${role}_8gpu.cmd.txt"
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
log "[DRY] AllReduce baseline scope=${role}_8gpu: ${command}"
|
||||
return 0
|
||||
fi
|
||||
log "START AllReduce baseline scope=${role}_8gpu"
|
||||
run_on_node "${node}" "${command}" \
|
||||
> "${RESULT_DIR}/communication/allreduce_${role}_8gpu.log" 2>&1
|
||||
log "DONE AllReduce baseline scope=${role}_8gpu"
|
||||
}
|
||||
|
||||
run_two_node_allreduce() {
|
||||
local cross_nic="$1"
|
||||
local master_port="$(( COMMUNICATION_MASTER_PORT + cross_nic + 10 ))"
|
||||
local head_container="${EXPERIMENT}_comm_ar16_head_x${cross_nic}_${RUN_ID}"
|
||||
local worker_container="${EXPERIMENT}_comm_ar16_worker_x${cross_nic}_${RUN_ID}"
|
||||
local head_command worker_command
|
||||
local -a common_args=(
|
||||
--nnodes=2
|
||||
--nproc-per-node=8
|
||||
--master-addr "${HEAD_NODE}"
|
||||
--master-port "${master_port}"
|
||||
)
|
||||
build_communication_docker_command \
|
||||
worker_command "${worker_container}" torchrun "${cross_nic}" \
|
||||
"${common_args[@]}" \
|
||||
--node-rank=1 \
|
||||
"${COMMUNICATION_TOOL}" all-reduce \
|
||||
--scope two_node_16gpu \
|
||||
--sizes "${COMMUNICATION_SIZES}" \
|
||||
--repetitions "${COMMUNICATION_REPETITIONS}" \
|
||||
--warmup "${COMMUNICATION_WARMUP}" \
|
||||
--iterations "${COMMUNICATION_ITERATIONS}"
|
||||
build_communication_docker_command \
|
||||
head_command "${head_container}" torchrun "${cross_nic}" \
|
||||
"${common_args[@]}" \
|
||||
--node-rank=0 \
|
||||
"${COMMUNICATION_TOOL}" all-reduce \
|
||||
--scope two_node_16gpu \
|
||||
--sizes "${COMMUNICATION_SIZES}" \
|
||||
--repetitions "${COMMUNICATION_REPETITIONS}" \
|
||||
--warmup "${COMMUNICATION_WARMUP}" \
|
||||
--iterations "${COMMUNICATION_ITERATIONS}"
|
||||
printf '%s\n' "${head_command}" \
|
||||
> "${COMMAND_DIR}/communication_allreduce_16gpu_crossnic${cross_nic}_head.cmd.txt"
|
||||
printf '%s\n' "${worker_command}" \
|
||||
> "${COMMAND_DIR}/communication_allreduce_16gpu_crossnic${cross_nic}_worker.cmd.txt"
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
log "[DRY] AllReduce 16 GPU cross_nic=${cross_nic} Head: ${head_command}"
|
||||
log "[DRY] AllReduce 16 GPU cross_nic=${cross_nic} Worker: ${worker_command}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "START AllReduce baseline scope=two_node_16gpu cross_nic=${cross_nic}"
|
||||
set +e
|
||||
run_on_node "${WORKER_NODE}" "${worker_command}" \
|
||||
> "${RESULT_DIR}/communication/allreduce_16gpu_crossnic${cross_nic}_worker.log" \
|
||||
2>&1 &
|
||||
local worker_pid=$!
|
||||
sleep 3
|
||||
run_on_node "${HEAD_NODE}" "${head_command}" \
|
||||
> "${RESULT_DIR}/communication/allreduce_16gpu_crossnic${cross_nic}_head.log" \
|
||||
2>&1
|
||||
local head_rc=$?
|
||||
wait "${worker_pid}"
|
||||
local worker_rc=$?
|
||||
set -e
|
||||
if (( head_rc != 0 || worker_rc != 0 )); then
|
||||
log "ERROR: AllReduce cross_nic=${cross_nic} head_rc=${head_rc} worker_rc=${worker_rc}"
|
||||
return 1
|
||||
fi
|
||||
log "DONE AllReduce baseline scope=two_node_16gpu cross_nic=${cross_nic}"
|
||||
}
|
||||
|
||||
cleanup_communication_containers() {
|
||||
[[ "${DRY_RUN}" == "1" ]] && return 0
|
||||
local node
|
||||
for node in "${HEAD_NODE}" "${WORKER_NODE}"; do
|
||||
run_on_node "${node}" \
|
||||
"docker ps -aq --filter 'name=^/${EXPERIMENT}_comm_' | xargs -r docker rm -f >/dev/null 2>&1" \
|
||||
|| true
|
||||
done
|
||||
}
|
||||
|
||||
run_communication_baseline() {
|
||||
[[ "${RUN_COMMUNICATION_BASELINE}" == "1" ]] || {
|
||||
log "SKIP communication baseline by configuration"
|
||||
return 0
|
||||
}
|
||||
mkdir -p "${RESULT_DIR}/communication" "${COMMAND_DIR}"
|
||||
if [[ "${DRY_RUN}" != "1" ]]; then
|
||||
preflight_gpus_idle "${HEAD_NODE}"
|
||||
preflight_gpus_idle "${WORKER_NODE}"
|
||||
fi
|
||||
run_p2p_baseline head "${HEAD_NODE}"
|
||||
run_p2p_baseline worker "${WORKER_NODE}"
|
||||
run_single_node_allreduce head "${HEAD_NODE}" "${COMMUNICATION_MASTER_PORT}"
|
||||
run_single_node_allreduce worker "${WORKER_NODE}" "$(( COMMUNICATION_MASTER_PORT + 1 ))"
|
||||
local cross_nic
|
||||
for cross_nic in ${CROSS_NIC_VALUES}; do
|
||||
run_two_node_allreduce "${cross_nic}"
|
||||
done
|
||||
cleanup_communication_containers
|
||||
}
|
||||
|
||||
start_service() {
|
||||
mark_event service_start
|
||||
# Set this before launch so the EXIT trap also cleans a partially started pair.
|
||||
@ -364,7 +597,7 @@ trap 'exit 0' HUP TERM PIPE
|
||||
while :; do
|
||||
printf 'wall_time_ns=%s\\n' \"\$(date +%s%N)\"
|
||||
docker top '${container}' -eo pid,ppid,psr,pcpu,pmem,stat,comm,args
|
||||
sleep '${SAMPLE_INTERVAL_S}'
|
||||
sleep '${PROCESS_SAMPLE_INTERVAL_S}'
|
||||
done
|
||||
"
|
||||
}
|
||||
@ -383,20 +616,27 @@ fi
|
||||
}
|
||||
|
||||
numastat_command() {
|
||||
local container="$1"
|
||||
local role="$1"
|
||||
local container="$2"
|
||||
printf '%s' "
|
||||
trap 'exit 0' HUP TERM PIPE
|
||||
printf '%s\n' 'wall_time_ns,node,node0_mib,node1_mib,total_mib,processes'
|
||||
while :; do
|
||||
printf 'wall_time_ns=%s\\n' \"\$(date +%s%N)\"
|
||||
PIDS=\$(docker top '${container}' -eo pid 2>/dev/null |
|
||||
awk 'NR > 1 {print \$1}')
|
||||
if [[ -z \"\${PIDS}\" ]]; then
|
||||
printf 'container has no visible processes: %s\\n' '${container}' >&2
|
||||
exit 1
|
||||
fi
|
||||
values=\$(
|
||||
for pid in \${PIDS}; do
|
||||
numastat -p \"\${pid}\"
|
||||
done
|
||||
numastat -p \"\${pid}\" 2>/dev/null |
|
||||
awk '\$1 == \"Total\" {print \$2, \$3, \$4}'
|
||||
done |
|
||||
awk '{n0 += \$1; n1 += \$2; total += \$3; count += 1}
|
||||
END {printf \"%.2f,%.2f,%.2f,%d\", n0, n1, total, count}'
|
||||
)
|
||||
printf '%s,%s,%s\\n' \"\$(date +%s%N)\" '${role}' \"\${values}\"
|
||||
sleep '${NUMASTAT_INTERVAL_S}'
|
||||
done
|
||||
"
|
||||
@ -416,23 +656,36 @@ dcgmi dmon -e '${DCGM_FIELD_IDS}' -d '$(( SAMPLE_INTERVAL_S * 1000 ))' |
|
||||
"
|
||||
local mpstat_command="
|
||||
trap 'exit 0' HUP TERM PIPE
|
||||
mpstat -P ALL '${SAMPLE_INTERVAL_S}'
|
||||
LC_ALL=C stdbuf -oL -eL mpstat -P ALL '${CPU_SAMPLE_INTERVAL_S}' |
|
||||
while IFS= read -r line; do
|
||||
printf '%s\\t%s\\t%s\\n' \"\$(date +%s%N)\" '${role}' \"\${line}\"
|
||||
done
|
||||
"
|
||||
local pidstat_command
|
||||
pidstat_command="$(container_pid_preamble "${container}")"
|
||||
pidstat_command+="
|
||||
trap 'exit 0' HUP TERM PIPE
|
||||
pidstat -durwt -p \"\${PIDS}\" '${SAMPLE_INTERVAL_S}'
|
||||
LC_ALL=C stdbuf -oL -eL pidstat -durw -p \"\${PIDS}\" '${PROCESS_SAMPLE_INTERVAL_S}' |
|
||||
while IFS= read -r line; do
|
||||
printf '%s\\t%s\\t%s\\n' \"\$(date +%s%N)\" '${role}' \"\${line}\"
|
||||
done
|
||||
"
|
||||
local sar_command="
|
||||
trap 'exit 0' HUP TERM PIPE
|
||||
sar -n DEV,EDEV '${SAMPLE_INTERVAL_S}'
|
||||
LC_ALL=C stdbuf -oL -eL sar -n DEV,EDEV '${NET_SAMPLE_INTERVAL_S}' |
|
||||
while IFS= read -r line; do
|
||||
printf '%s\\t%s\\t%s\\n' \"\$(date +%s%N)\" '${role}' \"\${line}\"
|
||||
done
|
||||
"
|
||||
local perf_command
|
||||
perf_command="$(container_pid_preamble "${container}")"
|
||||
perf_command+="
|
||||
trap 'exit 0' HUP TERM PIPE
|
||||
perf stat -p \"\${PIDS}\" -I 1000 -e '${PERF_EVENTS}'
|
||||
LC_ALL=C stdbuf -oL -eL perf stat -p \"\${PIDS}\" \
|
||||
-I '${PERF_INTERVAL_MS}' -e '${PERF_EVENTS}' 2>&1 |
|
||||
while IFS= read -r line; do
|
||||
printf '%s\\t%s\\t%s\\n' \"\$(date +%s%N)\" '${role}' \"\${line}\"
|
||||
done
|
||||
"
|
||||
|
||||
start_stream_collector \
|
||||
@ -452,7 +705,7 @@ perf stat -p \"\${PIDS}\" -I 1000 -e '${PERF_EVENTS}'
|
||||
start_stream_collector \
|
||||
"${role}" "${node}" docker_top.log "$(docker_top_command "${container}")"
|
||||
start_stream_collector \
|
||||
"${role}" "${node}" numastat.log "$(numastat_command "${container}")"
|
||||
"${role}" "${node}" numa_samples.csv "$(numastat_command "${role}" "${container}")"
|
||||
}
|
||||
|
||||
start_collectors() {
|
||||
@ -581,7 +834,8 @@ run_mixed_case() {
|
||||
}
|
||||
|
||||
summarize_results() {
|
||||
python3 "${RESULT_TOOL}" summarize "${RESULT_DIR}"
|
||||
python3 "${RESULT_TOOL}" summarize "${RESULT_DIR}" \
|
||||
--require-precise-windows "${REQUIRE_PRECISE_WINDOWS}"
|
||||
}
|
||||
|
||||
finish_manifest() {
|
||||
@ -595,6 +849,7 @@ cleanup() {
|
||||
local rc=$?
|
||||
stop_collectors || true
|
||||
stop_service || true
|
||||
cleanup_communication_containers || true
|
||||
if (( rc != 0 )) && [[ -f "${RESULT_DIR}/manifest.json" ]]; then
|
||||
finish_manifest ABORTED || true
|
||||
fi
|
||||
@ -620,6 +875,7 @@ run_all() {
|
||||
fi
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
run_communication_baseline
|
||||
start_service
|
||||
capture_static_snapshots before
|
||||
start_collectors
|
||||
@ -652,6 +908,10 @@ run_all() {
|
||||
mark_event cooldown_start
|
||||
sleep_if_real "${POST_RUN_COOLDOWN_S}"
|
||||
mark_event cooldown_end
|
||||
if ! check_collectors; then
|
||||
log "ERROR: one or more required collectors exited during the run"
|
||||
((failures+=1))
|
||||
fi
|
||||
stop_collectors
|
||||
capture_static_snapshots after
|
||||
stop_service
|
||||
@ -666,6 +926,22 @@ run_all() {
|
||||
(( failures == 0 ))
|
||||
}
|
||||
|
||||
run_communication_only() {
|
||||
validate_config
|
||||
enable_result_logging
|
||||
mkdir -p "${RESULT_DIR}" "${COMMAND_DIR}" "${RESULT_DIR}/communication"
|
||||
if [[ "${DRY_RUN}" != "1" ]]; then
|
||||
preflight_node_tools "${HEAD_NODE}"
|
||||
preflight_node_tools "${WORKER_NODE}"
|
||||
preflight_clock_sync
|
||||
fi
|
||||
trap cleanup_communication_containers EXIT INT TERM
|
||||
run_communication_baseline
|
||||
python3 "${RESULT_TOOL}" summarize-communication "${RESULT_DIR}"
|
||||
trap - EXIT INT TERM
|
||||
log "Communication baseline complete: result=${RESULT_DIR}"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${ACTION}" in
|
||||
all)
|
||||
@ -675,12 +951,15 @@ main() {
|
||||
enable_result_logging
|
||||
summarize_results
|
||||
;;
|
||||
communication)
|
||||
run_communication_only
|
||||
;;
|
||||
stop)
|
||||
enable_result_logging
|
||||
stop_service
|
||||
;;
|
||||
*)
|
||||
printf 'Usage: %s {all|summarize|stop}\n' "$0" >&2
|
||||
printf 'Usage: %s {all|communication|summarize|stop}\n' "$0" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -113,6 +113,10 @@ class HardwareContentionAttributionTest(unittest.TestCase):
|
||||
rows = attribution.read_csv_rows(result_dir / "bench_summary.csv")
|
||||
self.assertEqual(rows[0]["phase2_bench_run"], "prefill")
|
||||
self.assertTrue((result_dir / "report.md").exists())
|
||||
report = (result_dir / "report.md").read_text(encoding="utf-8")
|
||||
self.assertIn("Samples CPU/process/perf", report)
|
||||
self.assertIn("Samples/repetitions", report)
|
||||
self.assertIn("missing samples are reported as `-`", report)
|
||||
|
||||
def test_case_hardware_is_cut_by_phase1_meta_window(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
@ -179,6 +183,113 @@ class HardwareContentionAttributionTest(unittest.TestCase):
|
||||
"long_prefill_latency_128k_c1",
|
||||
)
|
||||
|
||||
def test_case_window_prefers_precise_main_benchmark_fields(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
result_dir = Path(temporary)
|
||||
meta_path = (
|
||||
result_dir
|
||||
/ "bench"
|
||||
/ "decode"
|
||||
/ "cases"
|
||||
/ "decode_case"
|
||||
/ "rep1"
|
||||
/ "meta.json"
|
||||
)
|
||||
meta_path.parent.mkdir(parents=True)
|
||||
meta_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"case_id": "decode_case",
|
||||
"role": "",
|
||||
"stage": "decode",
|
||||
"repetition": 1,
|
||||
"status": "COMPLETED",
|
||||
"started_at": "2026-07-31T00:00:00+00:00",
|
||||
"ended_at": "2026-07-31T00:01:00+00:00",
|
||||
"measurement_started_at": "2026-07-31T00:00:10+00:00",
|
||||
"measurement_ended_at": "2026-07-31T00:00:30+00:00",
|
||||
"measurement_window_source": "bench_main_marker_plus_duration",
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
windows = attribution.load_case_windows(result_dir)
|
||||
|
||||
self.assertEqual(len(windows), 1)
|
||||
self.assertEqual(windows[0]["duration_s"], 20)
|
||||
self.assertEqual(
|
||||
windows[0]["window_source"],
|
||||
"bench_main_marker_plus_duration",
|
||||
)
|
||||
|
||||
def test_dcgm_and_timestamped_cpu_parsers(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
dcgm = root / "dcgm.log"
|
||||
dcgm.write_text(
|
||||
"wall_time_ns,node,dcgm_output\n"
|
||||
"1000000000,head,GPU 0 0.900 0.700 0.300 0.200 0.500 1000000000 2000000000\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
mpstat = root / "mpstat.log"
|
||||
mpstat.write_text(
|
||||
"1000000000\thead\t12:00:00 PM all 10.00 0.00 5.00 1.00 "
|
||||
"0.00 2.00 0.00 0.00 0.00 82.00\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
dcgm_rows = attribution.parse_dcgm(dcgm)
|
||||
cpu_rows = attribution.parse_mpstat(mpstat)
|
||||
|
||||
self.assertEqual(dcgm_rows[0]["sm_active"], 0.7)
|
||||
self.assertEqual(dcgm_rows[0]["pcie_rx_bytes_per_s"], 2_000_000_000)
|
||||
self.assertEqual(cpu_rows[0]["cpu_active_pct"], 18)
|
||||
self.assertEqual(cpu_rows[0]["iowait_pct"], 1)
|
||||
|
||||
def test_communication_aggregate_separates_local_and_cross_numa(self) -> None:
|
||||
rows = [
|
||||
{
|
||||
"test": "p2p_copy",
|
||||
"node": "head",
|
||||
"source_gpu": 0,
|
||||
"destination_gpu": 1,
|
||||
"size_bytes": 1024,
|
||||
"bandwidth_GBps": 20,
|
||||
},
|
||||
{
|
||||
"test": "p2p_copy",
|
||||
"node": "head",
|
||||
"source_gpu": 0,
|
||||
"destination_gpu": 4,
|
||||
"size_bytes": 1024,
|
||||
"bandwidth_GBps": 10,
|
||||
},
|
||||
{
|
||||
"test": "all_reduce",
|
||||
"scope": "two_node_16gpu",
|
||||
"nccl_cross_nic": "1",
|
||||
"size_bytes": 1024,
|
||||
"mean_ms": 1,
|
||||
"algbw_GBps": 1,
|
||||
"busbw_GBps": 1.875,
|
||||
"wrong_values": 0,
|
||||
},
|
||||
]
|
||||
|
||||
aggregate = attribution.aggregate_communication_rows(rows)
|
||||
|
||||
self.assertEqual(len(aggregate), 3)
|
||||
path_classes = {
|
||||
row.get("path_class")
|
||||
for row in aggregate
|
||||
if row["test"] == "p2p_copy"
|
||||
}
|
||||
self.assertEqual(
|
||||
path_classes,
|
||||
{"same_pcie_switch", "cross_numa_sys"},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _write_csv(
|
||||
path: Path,
|
||||
|
||||
@ -9,8 +9,9 @@ import json
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@ -33,6 +34,10 @@ SUMMARY_FIELDS = [
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"elapsed_s",
|
||||
"measurement_started_at",
|
||||
"measurement_ended_at",
|
||||
"measurement_duration_s",
|
||||
"measurement_window_source",
|
||||
"completed",
|
||||
"failed",
|
||||
"duration_s",
|
||||
@ -305,6 +310,7 @@ def parse_scenarios(path: Path) -> list[dict[str, Any]]:
|
||||
|
||||
|
||||
def write_case(args: argparse.Namespace) -> None:
|
||||
measurement = measurement_window(args.measurement_marker, args.bench_file)
|
||||
value = {
|
||||
"run_id": args.run_id,
|
||||
"suite": args.suite,
|
||||
@ -323,6 +329,7 @@ def write_case(args: argparse.Namespace) -> None:
|
||||
"started_at": args.started_at,
|
||||
"ended_at": args.ended_at,
|
||||
"elapsed_s": args.elapsed_s,
|
||||
**measurement,
|
||||
"bench_file": args.bench_file,
|
||||
"bench_log": args.bench_log,
|
||||
"note": args.note,
|
||||
@ -330,6 +337,51 @@ def write_case(args: argparse.Namespace) -> None:
|
||||
write_json(args.path, value)
|
||||
|
||||
|
||||
def write_measurement_start(path: Path) -> None:
|
||||
write_json(
|
||||
path,
|
||||
{
|
||||
"recorded_at": datetime.now()
|
||||
.astimezone()
|
||||
.isoformat(timespec="microseconds"),
|
||||
"wall_time_ns": time.time_ns(),
|
||||
"source": "bench_log_main_marker",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def measurement_window(marker_path: str, bench_file: str) -> dict[str, Any]:
|
||||
empty = {
|
||||
"measurement_started_at": None,
|
||||
"measurement_ended_at": None,
|
||||
"measurement_duration_s": None,
|
||||
"measurement_window_source": "unavailable",
|
||||
}
|
||||
marker = Path(marker_path) if marker_path else None
|
||||
bench = Path(bench_file) if bench_file else None
|
||||
if marker is None or bench is None or not marker.exists():
|
||||
return empty
|
||||
try:
|
||||
marker_value = read_json(marker)
|
||||
started_at = str(marker_value["recorded_at"])
|
||||
started = datetime.fromisoformat(started_at)
|
||||
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||
return empty
|
||||
bench_value = read_bench_output(bench)
|
||||
if bench_value is None:
|
||||
return empty
|
||||
duration_s = first_float(bench_value, "duration", "benchmark_duration")
|
||||
if duration_s is None or duration_s <= 0:
|
||||
return empty
|
||||
ended = started + timedelta(seconds=duration_s)
|
||||
return {
|
||||
"measurement_started_at": started_at,
|
||||
"measurement_ended_at": ended.isoformat(timespec="microseconds"),
|
||||
"measurement_duration_s": duration_s,
|
||||
"measurement_window_source": "bench_main_marker_plus_duration",
|
||||
}
|
||||
|
||||
|
||||
def write_manifest(args: argparse.Namespace) -> None:
|
||||
value: dict[str, Any] = {}
|
||||
if args.path.exists():
|
||||
@ -618,6 +670,7 @@ def add_case_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--started-at", required=True)
|
||||
parser.add_argument("--ended-at", required=True)
|
||||
parser.add_argument("--elapsed-s", type=float, required=True)
|
||||
parser.add_argument("--measurement-marker", default="")
|
||||
parser.add_argument("--bench-file", required=True)
|
||||
parser.add_argument("--bench-log", required=True)
|
||||
parser.add_argument("--note", default="")
|
||||
@ -678,6 +731,9 @@ def main() -> None:
|
||||
mark_failed_parser.add_argument("--error-type", required=True)
|
||||
mark_failed_parser.add_argument("--note", required=True)
|
||||
|
||||
measurement_start_parser = subparsers.add_parser("mark-measurement-start")
|
||||
measurement_start_parser.add_argument("--path", type=Path, required=True)
|
||||
|
||||
summarize_parser = subparsers.add_parser("summarize")
|
||||
summarize_parser.add_argument("result_dir", type=Path)
|
||||
|
||||
@ -708,6 +764,8 @@ def main() -> None:
|
||||
complete_manifest(args.path, args.status)
|
||||
elif args.command == "mark-case-failed":
|
||||
mark_case_failed(args.path, args.error_type, args.note)
|
||||
elif args.command == "mark-measurement-start":
|
||||
write_measurement_start(args.path)
|
||||
elif args.command == "summarize":
|
||||
summarize(args.result_dir)
|
||||
|
||||
|
||||
@ -483,6 +483,7 @@ write_case_meta() {
|
||||
local bench_file="${18}"
|
||||
local bench_log="${19}"
|
||||
local note="${20}"
|
||||
local measurement_marker="${21}"
|
||||
|
||||
python3 "${RESULT_TOOL}" write-case \
|
||||
--path "${meta_path}" \
|
||||
@ -503,6 +504,7 @@ write_case_meta() {
|
||||
--started-at "${started_at}" \
|
||||
--ended-at "${ended_at}" \
|
||||
--elapsed-s "${elapsed_s}" \
|
||||
--measurement-marker "${measurement_marker}" \
|
||||
--bench-file "${bench_file}" \
|
||||
--bench-log "${bench_log}" \
|
||||
--note "${note}"
|
||||
@ -542,6 +544,25 @@ detect_error_type() {
|
||||
fi
|
||||
}
|
||||
|
||||
watch_bench_main_start() {
|
||||
local bench_log="$1"
|
||||
local bench_pid="$2"
|
||||
local marker_path="$3"
|
||||
|
||||
while kill -0 "${bench_pid}" 2>/dev/null; do
|
||||
if grep -Fq "Starting main benchmark run" "${bench_log}" 2>/dev/null; then
|
||||
python3 "${RESULT_TOOL}" mark-measurement-start --path "${marker_path}"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if grep -Fq "Starting main benchmark run" "${bench_log}" 2>/dev/null; then
|
||||
python3 "${RESULT_TOOL}" mark-measurement-start --path "${marker_path}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_bench_case() {
|
||||
local suite="$1"
|
||||
local case_id="$2"
|
||||
@ -563,6 +584,7 @@ run_bench_case() {
|
||||
local bench_log="${case_dir}/bench.log"
|
||||
local meta_path="${case_dir}/meta.json"
|
||||
local command_file="${case_dir}/bench_cmd.txt"
|
||||
local measurement_marker="${case_dir}/measurement_start.json"
|
||||
|
||||
if case_already_completed "${meta_path}" "${bench_file}"; then
|
||||
log "SKIP completed case=${case_id} rep=${repetition}"
|
||||
@ -591,14 +613,22 @@ run_bench_case() {
|
||||
"${BENCH_CMD[@]}" > "${command_file}"
|
||||
|
||||
local started_at start_epoch ended_at elapsed_s rc status error_type
|
||||
local bench_pid marker_pid
|
||||
started_at="$(iso_now)"
|
||||
start_epoch="$(date +%s)"
|
||||
log "START case=${case_id} rep=${repetition} isl=${isl} osl=${osl} c=${concurrency}"
|
||||
rm -f "${measurement_marker}"
|
||||
|
||||
set +e
|
||||
timeout --signal=TERM --kill-after=30s "${timeout_s}s" \
|
||||
"${BENCH_CMD[@]}" > "${bench_log}" 2>&1
|
||||
"${BENCH_CMD[@]}" > "${bench_log}" 2>&1 &
|
||||
bench_pid=$!
|
||||
watch_bench_main_start \
|
||||
"${bench_log}" "${bench_pid}" "${measurement_marker}" &
|
||||
marker_pid=$!
|
||||
wait "${bench_pid}"
|
||||
rc=$?
|
||||
wait "${marker_pid}" >/dev/null 2>&1 || true
|
||||
set -e
|
||||
|
||||
ended_at="$(iso_now)"
|
||||
@ -618,7 +648,8 @@ run_bench_case() {
|
||||
"${meta_path}" "${suite}" "${case_id}" "${role}" "${stage}" \
|
||||
"${repetition}" "${isl}" "${osl}" "${concurrency}" "${num_prompts}" \
|
||||
"${warmup_requests}" "${status}" "${error_type}" "${rc}" "${started_at}" \
|
||||
"${ended_at}" "${elapsed_s}" "${bench_file}" "${bench_log}" "${note}"
|
||||
"${ended_at}" "${elapsed_s}" "${bench_file}" "${bench_log}" "${note}" \
|
||||
"${measurement_marker}"
|
||||
|
||||
LAST_CASE_STATUS="${status}"
|
||||
LAST_CASE_ERROR="${error_type}"
|
||||
|
||||
@ -4,6 +4,7 @@ import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@ -111,6 +112,35 @@ class QuickMapResultsTest(unittest.TestCase):
|
||||
self.assertIn("long_prefill_latency_128k_c1", report_text)
|
||||
self.assertIn("OOM", report_text)
|
||||
|
||||
def test_measurement_window_uses_main_marker_and_benchmark_duration(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
marker = root / "measurement_start.json"
|
||||
bench = root / "bench.jsonl"
|
||||
marker.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"recorded_at": "2026-07-31T12:00:00.250000+08:00",
|
||||
"wall_time_ns": 1,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
bench.write_text(json.dumps({"duration": 12.5}) + "\n", encoding="utf-8")
|
||||
|
||||
window = quick_map_results.measurement_window(str(marker), str(bench))
|
||||
|
||||
self.assertEqual(window["measurement_duration_s"], 12.5)
|
||||
self.assertEqual(
|
||||
window["measurement_window_source"],
|
||||
"bench_main_marker_plus_duration",
|
||||
)
|
||||
ended = datetime.fromisoformat(str(window["measurement_ended_at"]))
|
||||
self.assertEqual(
|
||||
ended,
|
||||
datetime.fromisoformat("2026-07-31T12:00:12.750000+08:00"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _write_meta(
|
||||
path: Path,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user