Compare commits

...

2 Commits

Author SHA1 Message Date
Zhiyi Hong
4892c0b14d [Docs] document final Phase 2 attribution workflow 2026-07-31 15:47:10 +08:00
Zhiyi Hong
30664faa41 [Feat] finalize Phase 2 hardware attribution pipeline 2026-07-31 15:40:20 +08:00
13 changed files with 2197 additions and 564 deletions

View File

@ -1,5 +1,9 @@
# sskj — 多平台大模型推理性能基准测试项目 # 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** > **更新2026-07-31 14:15:00 CST**
> >
> 恢复并完善 Phase 2 档案中的采集命令与指标解释。第 5 节现按实际实现记录时间 Marker、`nvidia-smi`、DCGM Field 10011005/1009/1010、`mpstat``pidstat``perf``numastat``sar``ethtool``mlx5_0/mlx5_3` HCA Counter并逐项说明字段含义、分析方法及对应原始/汇总文件。同步记录首轮线程级 1 秒 `pidstat` 日志过重,后续应改为进程级 5 秒采样。 > 恢复并完善 Phase 2 档案中的采集命令与指标解释。第 5 节现按实际实现记录时间 Marker、`nvidia-smi`、DCGM Field 10011005/1009/1010、`mpstat``pidstat``perf``numastat``sar``ethtool``mlx5_0/mlx5_3` HCA Counter并逐项说明字段含义、分析方法及对应原始/汇总文件。同步记录首轮线程级 1 秒 `pidstat` 日志过重,后续应改为进程级 5 秒采样。

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>6000D 双机通信与 NCCL 术语入门</title> <title>6000D 双机通信、NCCL 与 Profiling 术语入门</title>
<style> <style>
:root { :root {
color-scheme: light; color-scheme: light;
@ -259,11 +259,11 @@
<header> <header>
<div class="header-inner"> <div class="header-inner">
<p class="eyebrow">Two-node communication primer</p> <p class="eyebrow">Two-node communication primer</p>
<h1>6000D 双机通信与 NCCL 术语入门</h1> <h1>6000D 双机通信、NCCL 与 Profiling 术语入门</h1>
<div class="header-meta"> <div class="header-meta">
<span>节点174.1.51.5 + 174.1.51.7</span> <span>节点174.1.51.5 + 174.1.51.7</span>
<span>规模16 GPU / TP16</span> <span>规模16 GPU / TP16</span>
<span>版本2026-07-30 17:54 CST</span> <span>版本2026-07-31 15:25 CST</span>
</div> </div>
</div> </div>
</header> </header>
@ -447,6 +447,106 @@ mlx5_3 port 1 ==> eth3 (Up)</code></pre>
<td>RoCE 网络控制拥塞和丢包的机制。</td> <td>RoCE 网络控制拥塞和丢包的机制。</td>
<td>RDMA 出现 retry、pause 或吞吐抖动时由运维检查。</td> <td>RDMA 出现 retry、pause 或吞吐抖动时由运维检查。</td>
</tr> </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 SwitchSYS 表示还要跨 CPU/NUMA 互联。</td>
<td>GPU03、GPU47 各自多为 PIX两组之间为 SYSP2P 微基准会分别汇总这两类路径。</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 使用 10011005、1009、1010并把缺失样本保留为 <code>-</code>,不会当成 0。</td>
</tr>
<tr>
<td><strong>SM</strong></td>
<td>Streaming MultiprocessorGPU 执行 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> </tbody>
</table> </table>

View File

@ -105,481 +105,306 @@
<p class="eyebrow">Standalone Code Walkthrough / Phase 2</p> <p class="eyebrow">Standalone Code Walkthrough / Phase 2</p>
<h1>DSV4-Pro 双机 Pro6000D SGLang 硬件竞争归因:代码详解</h1> <h1>DSV4-Pro 双机 Pro6000D SGLang 硬件竞争归因:代码详解</h1>
<div class="meta"> <div class="meta">
行号基线:<code>ca1f2f63375c</code>  行号基线:<code>30664faa41f8</code> 
生成时间2026-07-31 13:04:21 CST  生成时间2026-07-31 15:25:00 CST 
入口:<code>run_hardware_contention_attribution.sh</code> 唯一入口:<code>run_hardware_contention_attribution.sh all</code>
</div> </div>
</div> </div>
</header> </header>
<main> <main>
<div class="callout"> <div class="callout">
<strong>文档边界:</strong>这是一份独立代码档案。它解释 Phase 2 如何复用 Phase 1 请求、 <strong>文档边界:</strong>本文只解释提交 <code>30664faa41f8</code> 的 Phase 2
同步采集两节点监控,并按真实 Case 时间窗做硬件归因。所有行号绑定提交 代码和文件调用关系。Phase 1 负责模型服务与请求Phase 2 负责通信基线、
<code>ca1f2f63375c</code> 两节点监控、精确时间切片和逐指标报告
</div> </div>
<h2 id="read">1. 阅读导航</h2> <h2 id="read">1. 阅读导航</h2>
<ul class="toc"> <ul class="toc">
<li><a href="#flow">总体控制流</a></li> <li><a href="#flow">总体控制流</a></li>
<li><a href="#files">文件职责</a></li> <li><a href="#files">文件职责与调用关系</a></li>
<li><a href="#reuse">Phase 1 复用边界</a></li> <li><a href="#config">配置来源</a></li>
<li><a href="#preflight">预检与时钟</a></li> <li><a href="#communication">通信微基准</a></li>
<li><a href="#collectors">采集器实现</a></li> <li><a href="#collectors">采集器实现</a></li>
<li><a href="#cases">Case 编排</a></li> <li><a href="#alignment">精确测量窗口</a></li>
<li><a href="#alignment">时间窗对齐</a></li> <li><a href="#report">逐指标报告</a></li>
<li><a href="#outputs">结构化输出</a></li>
<li><a href="#index">函数行号索引</a></li> <li><a href="#index">函数行号索引</a></li>
</ul> </ul>
<h2 id="flow">2. 总体控制流</h2> <h2 id="flow">2. 总体控制流</h2>
<pre><code>main "$@" <pre><code>main "$@" → run_all
└─ ACTION=all → run_all ├─ validate_config
├─ validate_config ├─ preflight_node_tools
├─ preflight_node_tools + preflight_clock_sync │ └─ 两节点 dcgmi discovery -l 必须成功
├─ start_service ├─ preflight_clock_sync + preflight_gpus_idle
│ └─ 委托 Phase 1 的 start ├─ run_communication_baseline
├─ capture_static_snapshots before │ ├─ 两节点 CUDA P2P 全矩阵
├─ start_collectors │ ├─ 两节点各自 8-rank AllReduce
│ └─ Head 与 Worker 各启动 9 类采集器 │ └─ 16-rank AllReduceCROSS_NIC=0/1/2
├─ idle baseline ├─ start_service → Phase 1 start
├─ 固定诊断 Case ├─ capture_static_snapshots before
│ └─ 每个 Case 委托 Phase 1 的 fixed ├─ start_collectors → Head/Worker 同时采集
├─ 混合 Prefill/Decode A/B ├─ idle → fixed cases → mixed A/B → cooldown
│ └─ 委托 Phase 1 的 mixed ├─ check_collectors + stop_collectors
├─ cooldown ├─ capture_static_snapshots after
├─ stop_collectors ├─ stop_service → Phase 1 stop
├─ capture_static_snapshots after ├─ summarize_results
├─ stop_service │ └─ 按正式 benchmark 窗口生成第 5 节逐项数据表
├─ summarize_results └─ finish_manifest</code></pre>
│ └─ 按每个 Case 的 started_at/ended_at 切监控窗口
└─ finish_manifest</code></pre>
<p> <p>
Phase 2 不复制模型部署和 benchmark 生成代码。它新增的是诊断编排层: <code>all</code> 是唯一正式入口。<code>communication</code><code>summarize</code>
在同一个服务生命周期中,让两节点的 GPU、CPU、NUMA、网络和 RDMA 时间序列包围 benchmark <code>stop</code> 是排错/恢复 action不需要在正常执行前手工调用。
最后按 Case 时间切片。
</p> </p>
<h2 id="files">3. 文件职责</h2> <h2 id="files">3. 文件职责与调用关系</h2>
<table> <table>
<thead><tr><th>文件</th><th>行数</th><th>职责</th></tr></thead> <thead><tr><th>文件</th><th>行数</th><th>职责</th></tr></thead>
<tbody> <tbody>
<tr> <tr><td class="path">config.env</td><td>61</td><td>节点、Case、分层采样周期、通信尺寸、NCCL 选择和 fail-closed 策略。</td></tr>
<td class="path">run_hardware_contention_attribution.sh</td><td>689</td> <tr><td class="path">run_hardware_contention_attribution.sh</td><td>968</td><td>唯一 Shell 编排器预检、通信基线、Phase 1 委托、采集器、Case 和清理。</td></tr>
<td>唯一入口,调用 Phase 1 服务/请求管理所有采集器、静态快照、Case marker 与清理。</td> <tr><td class="path">communication_baseline.py</td><td>227</td><td>CUDA P2P 全矩阵及 PyTorch/NCCL AllReduce 正确性、延迟和带宽测试。</td></tr>
</tr> <tr><td class="path">hardware_contention_attribution.py</td><td>1480</td><td>解析所有原始采集器,按 Case 切片,聚合通信并生成 CSV/JSON/report.md。</td></tr>
<tr> <tr><td class="path">tests/test_hardware_contention_attribution.py</td><td>308</td><td>8 项纯 Python 单元测试覆盖精确窗口、解析器、RDMA 单位和通信聚合。</td></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>
</tbody> </tbody>
</table> </table>
<h3>3.1 Phase 2 与 Phase 1 的文件关系</h3>
<pre><code>用户 <pre><code>用户
└─ bash Phase2/run_hardware_contention_attribution.sh all └─ Phase2/run_hardware_contention_attribution.sh all
├─ source Phase2/config.env ├─ source Phase2/config.env
│ └─ PHASE1_ENTRY 指向 Phase1/run_quick_map.sh ├─ docker/torchrun → Phase2/communication_baseline.py
├─ env ... bash "${PHASE1_ENTRY}" start/fixed/mixed/stop ├─ env ... bash Phase1/run_quick_map.sh start/fixed/mixed/stop
│ └─ Phase1/run_quick_map.sh │ └─ Phase1/quick_map_results.py 写 benchmark meta
│ ├─ source Phase1/config.env ├─ Shell 采集 Head/Worker 原始时间序列
│ ├─ 读取 Phase1/quick_map_scenarios.tsv └─ Phase2/hardware_contention_attribution.py summarize
│ └─ 调用 Phase1/quick_map_results.py ├─ 读取 Phase1 bench/cases/*/meta.json
├─ 自己启动两节点硬件采集器 ├─ 读取 Head/Worker 原始监控
└─ 调用 Phase2/hardware_contention_attribution.py ├─ 读取 communication/COMM_RESULT
├─ 读取 Phase1 生成的 bench/*/cases/*/meta.json └─ 输出逐 Case、逐节点、逐指标表和 report.md</code></pre>
├─ 读取 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>
<h3>3.1 config.env 分区</h3> <h3>3.1 Phase 1 与 Phase 2 的边界</h3>
<table> <table>
<thead><tr><th>范围</th><th>内容</th><th>说明</th></tr></thead> <thead><tr><th>问题</th><th>由哪个文件负责</th><th>证据</th></tr></thead>
<tbody> <tbody>
<tr><td><code>config.env:L3-L8</code></td><td>实验名与 Phase 1 入口</td><td>通过相对路径复用 Phase 1不依赖启动命令当前目录。</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><code>config.env:L10-L16</code></td><td>节点、端口、容器名</td><td>用于健康检查、PID 映射和容器级监控。</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><code>config.env:L18-L20</code></td><td>诊断 Case</td><td>五个固定 Case加一个混合 A/B 开关。</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><code>config.env:L22-L32</code></td><td>采集策略</td><td>1 秒采样、空闲基线、冷却、DCGM 字段、perf 事件、RDMA HCA 和时钟容差。</td></tr> <tr><td>硬件归因和数值报告</td><td>Phase 2 Python 汇总器</td><td><code>case_*_summary.csv</code><code>report.md</code></td></tr>
<tr><td><code>config.env:L34-L40</code></td><td>路径与运行模式</td><td>Repo/Result/Runtime、Dry-run 和是否容忍部分采集器失败。</td></tr>
</tbody> </tbody>
</table> </table>
<h2 id="reuse">4. Phase 1 复用边界</h2> <h2 id="config">4. 配置来源</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/&lt;RUN_ID&gt;/service/head_server_cmd.txt
→ Phase2/results/&lt;RUN_ID&gt;/bench/&lt;sub-run&gt;/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/&lt;RUN_ID&gt;/service/head_server_cmd.txt
# 从 Phase 1 子 Run Manifest 读取结构化值
python3 -c 'import glob,json; p=glob.glob(
"results/&lt;RUN_ID&gt;/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 1Phase 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>
<table> <table>
<thead><tr><th>采集器</th><th>回答的问题</th></tr></thead> <thead><tr><th>行号</th><th>配置组</th><th>关键变量</th></tr></thead>
<tbody> <tbody>
<tr><td><code>nvidia-smi</code> / DCGM</td><td>GPU 是否算力、显存带宽、功耗或时钟受限。</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>mpstat</code></td><td>CPU 总体与逐核是否繁忙。</td></tr> <tr><td><code>L18-L21</code></td><td>诊断 Case</td><td>五个 fixed Case、mixed A/B 开关。</td></tr>
<tr><td><code>pidstat</code></td><td>SGLang/容器进程的 CPU、内存和上下文切换。</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>sar -n DEV,EDEV</code></td><td><code>eth0/eth3</code> 吞吐和错误。</td></tr> <tr><td><code>L40-L55</code></td><td>通信基线</td><td>镜像、消息尺寸、迭代次数、P2P 大小、CROSS_NIC 列表、Socket/HCA。</td></tr>
<tr><td><code>perf stat</code></td><td>容器主进程的 CPU cycles、instructions、cache miss 等。</td></tr> <tr><td><code>L57-L61</code></td><td>路径与模式</td><td><code>RESULT_BASE</code>、Runtime、Dry-run、是否允许部分采集器。</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>
</tbody> </tbody>
</table> </table>
<p> <p>
<code>container_pid_preamble</code><code>numastat_command</code> <code>MEM_FRACTION_STATIC</code> 不在 Phase 2 重复定义。它仍来自 Phase 1
位于 <code>L372-L404</code>,先解析容器主 PID再让 perf/numastat 对准实际服务进程。 最终展开为 SGLang 的 <code>--mem-fraction-static</code>。判断某次 Run 的真实值,
应读取 <code>service/head_server_cmd.txt</code>,不能只看默认配置。
</p> </p>
<h3>6.6 停止与残留清理</h3> <h2 id="communication">5. 通信微基准</h2>
<h3>5.1 Shell 如何编排</h3>
<p> <p>
<code>check_collectors</code><code>stop_collectors</code> <code>run_hardware_contention_attribution.sh:L269-L447</code> 负责 Docker 命令、
位于 Shell <code>L469-L519</code> 两节点同步和清理。所有命令先写入 <code>commands/*.txt</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>
</p> </p>
<ul> <ul>
<li>服务健康时,即使一个 Case 失败,也继续后续诊断 Case并累计 failures。</li> <li><code>L300-L323</code>Head/Worker 各跑一次 P2P。</li>
<li>每个固定 Case 后检查 <code>/health</code>;服务失活则中止剩余 Case。</li> <li><code>L324-L353</code>Head/Worker 各跑一次 8-rank AllReduce。</li>
<li>服务已失活时跳过 mixed避免无意义错误。</li> <li><code>L354-L416</code>:每个 CROSS_NIC 值先启动 Worker rank再运行 Head rank。</li>
<li>不论成功失败,最终尝试 cooldown、停止采集、后快照、停止服务和汇总。</li> <li><code>L417-L447</code>:只清理本实验前缀的通信容器。</li>
<li>Run 状态区分 <code>COMPLETED</code><code>COMPLETED_WITH_FAILURES</code><code>DRY_RUN</code></li>
</ul> </ul>
<h2 id="alignment">8. 按真实 Case 时间窗归因</h2>
<h3>8.1 时间窗从哪里来</h3>
<p> <p>
<code>load_case_windows</code> 位于 Docker 使用和 SGLang 一致的 CUDA 13 nightly 镜像,并显式透传
<code>hardware_contention_attribution.py:L233-L260</code> <code>rdma_cm</code><code>uverbs0</code><code>uverbs3</code>
它读取 Phase 1 每个 <code>meta.json</code> 中的 <code>NCCL_DEBUG=INFO</code> 只在微基准中打开,用于证明 NET/IB/GDRDMA 路径。
<code>started_at</code><code>ended_at</code>,转换为纳秒时间戳。
这比用“Case marker 前后大概几秒”更精确,因为它对齐的是 benchmark 进程实际测量区间。
</p> </p>
<h3>8.2 如何切 GPU/RDMA 数据</h3> <h3>5.2 P2P 代码</h3>
<p> <p>
<code>rows_in_window</code> 位于 <code>L263-L274</code> <code>communication_baseline.py:L45-L106</code> 遍历所有源 GPU 和目标 GPU
只保留 <code>started_ns ≤ wall_time_ns ≤ ended_ns</code> 的采样行。 先调用 <code>torch.cuda.can_device_access_peer</code>,再对 256 MiB FP16 Tensor
<code>summarize_case_hardware</code> 位于 <code>L276-L319</code> 做预热和 CUDA Event 计时。输出包括方向、P50/P95 latency 和 GB/s。
对每个 Case、每个节点分别切 GPU 与 RDMA再复用全局汇总函数 汇总器按拓扑拆成同 PCIe Switch 的 PIX 与跨 NUMA 的 SYS。
</p> </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/&lt;RUN_ID&gt;/ <pre><code>results/&lt;RUN_ID&gt;/
manifest.json manifest.json
markers.csv
collector_status.csv
service/
commands/ commands/
communication/
service/
bench/&lt;phase1-sub-run&gt;/ bench/&lt;phase1-sub-run&gt;/
head/ head/
gpu_samples.csv gpu_samples.csv
rdma.csv dcgm_dmon.log
dcgm.log
mpstat.log mpstat.log
pidstat.log pidstat.log
sar_network.log perf_stat.log
perf.log sar_net.log
docker_top.log numa_samples.csv
numastat.log rdma.csv
collector_commands/ collector_commands/
worker/ worker/
...同上... ...同上...
gpu_summary.csv
rdma_summary.csv
bench_summary.csv
case_windows.csv case_windows.csv
bench_summary.csv
case_gpu_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 case_rdma_summary.csv
communication_summary.csv
communication_aggregate.csv
summary.json summary.json
report.md</code></pre> 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> <h2 id="index">10. 函数行号索引</h2>
<h3>10.1 run_hardware_contention_attribution.sh</h3> <h3>10.1 Shell 编排器</h3>
<table> <table>
<thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead> <thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead>
<tbody> <tbody>
<tr><td>L25-L65</td><td>日志、远端执行、命令文件、结果日志</td><td>编排器基础设施。</td></tr> <tr><td>L26-L66</td><td>日志、远端执行、命令证据</td><td>基础设施。</td></tr>
<tr><td>L66-L151</td><td>配置、工具、时钟预检</td><td>正式启动前 fail-fast。</td></tr> <tr><td>L67-L199</td><td>配置、工具、时钟、GPU 空闲门禁</td><td>正式运行前 fail-fast。</td></tr>
<tr><td>L152-L194</td><td>Manifest 与 marker</td><td>记录运行身份和事件边界。</td></tr> <tr><td>L200-L268</td><td>Manifest、marker、Phase 1 委托</td><td>运行身份与复用边界。</td></tr>
<tr><td>L195-L229</td><td>Phase 1 委托与服务生命周期</td><td>复用服务和 benchmark不复制实现。</td></tr> <tr><td>L269-L447</td><td>通信基线</td><td>P2P、8/16-rank AllReduce、CROSS_NIC A/B。</td></tr>
<tr><td>L230-L283</td><td>静态快照</td><td>保存实验前后硬件、网络、RDMA 和容器状态。</td></tr> <tr><td>L448-L516</td><td>服务和静态快照</td><td>启停 Phase 1 双机服务并保存环境。</td></tr>
<tr><td>L284-L318</td><td><code>start_stream_collector</code></td><td>统一包装远端长时间采集器。</td></tr> <tr><td>L517-L710</td><td>采集命令与启动</td><td>两节点分层采样。</td></tr>
<tr><td>L319-L404</td><td>GPU、RDMA、PID、NUMA 命令</td><td>生成各类采集命令。</td></tr> <tr><td>L711-L772</td><td>采集器检查和停止</td><td>fail-closed 与残留清理。</td></tr>
<tr><td>L405-L468</td><td>启动所有采集器</td><td>两节点各启动九类监控。</td></tr> <tr><td>L782-L835</td><td>fixed/mixed Case</td><td>代表负载编排。</td></tr>
<tr><td>L469-L519</td><td>检查与停止采集器</td><td>收集状态并清理残留。</td></tr> <tr><td>L836-L858</td><td>汇总、Manifest、trap</td><td>结果收口。</td></tr>
<tr><td>L520-L582</td><td>sleep、固定 Case、混合 Case</td><td>诊断负载编排。</td></tr> <tr><td>L859-L928</td><td><code>run_all</code></td><td>完整状态机。</td></tr>
<tr><td>L583-L603</td><td>汇总、Manifest 完成、cleanup</td><td>结果收口与异常清理。</td></tr> <tr><td>L929-L968</td><td>辅助 action 与 main</td><td><code>communication/all/summarize/stop</code> 分发。</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>
</tbody> </tbody>
</table> </table>
<h3>10.2 hardware_contention_attribution.py</h3> <h3>10.2 Python 文件</h3>
<table> <table>
<thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead> <thead><tr><th>文件/行号</th><th>职责</th></tr></thead>
<tbody> <tbody>
<tr><td>L54-L100</td><td>时间、JSON、数字、CSV</td><td>结果工具基础函数。</td></tr> <tr><td><code>communication_baseline.py:L20-L44</code></td><td>尺寸解析、分位数和 JSON 结果协议。</td></tr>
<tr><td>L101-L141</td><td>GPU 汇总</td><td>按 node/GPU 输出 mean、P95、max。</td></tr> <tr><td><code>L45-L106</code></td><td>CUDA P2P 全矩阵。</td></tr>
<tr><td>L142-L212</td><td>RDMA 汇总</td><td>计数器 delta、字节换算、Gbps 和错误增量。</td></tr> <tr><td><code>L107-L198</code></td><td>NCCL AllReduce 与正确性。</td></tr>
<tr><td>L213-L260</td><td>Bench 与 Case 时间窗读取</td><td>从 Phase 1 子 Run 建立诊断索引。</td></tr> <tr><td><code>hardware_contention_attribution.py:L76-L166</code></td><td>时间、CSV、数字统计基础函数。</td></tr>
<tr><td>L263-L321</td><td>时间切片</td><td>按每个 Case 的真实运行窗口汇总 GPU/RDMA。</td></tr> <tr><td><code>L167-L358</code></td><td>DCGM、mpstat、pidstat、perf、sar 解析器。</td></tr>
<tr><td>L322-L366</td><td>CSV 与 marker</td><td>输出结构化表和事件时间线。</td></tr> <tr><td><code>L359-L508</code></td><td>通信、GPU、RDMA、bench 读取与汇总。</td></tr>
<tr><td>L367-L404</td><td>Manifest 与 bench 校验</td><td>维护 Run 状态,确认子 Run 全部完成。</td></tr> <tr><td><code>L509-L841</code></td><td>精确窗口和全部 Case 指标切片。</td></tr>
<tr><td>L405-L506</td><td><code>summarize</code></td><td>生成全部汇总表、summary JSON 和 report。</td></tr> <tr><td><code>L842-L1035</code></td><td>通信聚合、CSV、Marker、Manifest。</td></tr>
<tr><td>L507-L574</td><td>CLI</td><td>向 Shell 提供 marker/manifest/check/summarize 子命令。</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> </tbody>
</table> </table>
<footer> <footer>
本文只描述提交 <code>ca1f2f63375c</code> 的实现。后续增加 Nsight、SGLang profiler 本文只描述提交 <code>30664faa41f8</code>。Nsight Systems、SGLang Profiler 和
或新的硬件计数器时,应先明确它属于 Phase 2 采集层还是后续深度剖析层,再更新本档案。 Kernel Timeline 属于 Phase 3不加入 Phase 2避免重复采集和职责混淆
</footer> </footer>
</main> </main>
</body> </body>

View File

@ -135,7 +135,7 @@
<div class="meta"> <div class="meta">
<span>节点174.1.51.5 + 174.1.51.7</span> <span>节点174.1.51.5 + 174.1.51.7</span>
<span>拓扑SGLang TP16 / EP2</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>
</div> </div>
</header> </header>
@ -144,10 +144,11 @@
<a class="back" href="./推理优化计划.html">返回推理优化主计划</a> <a class="back" href="./推理优化计划.html">返回推理优化主计划</a>
<p class="status"> <p class="status">
<strong>当前状态Phase 2 首次正式双机 Run 已完成。</strong> <strong>当前状态Phase 2 最终采集代码已完成,等待最终双机复跑。</strong>
Run <code>dsv4pro-phase2-20260731-130125</code> 在 26 分 26 秒内完成 首次 Run <code>dsv4pro-phase2-20260731-130125</code> 的 8/8 个 benchmark
8/8 个 benchmark无 OOMGPU、CPU 和双 Rail RDMA 数据已完成首轮归因。 均成功;提交 <code>30664faa41f8</code> 已补齐正式测量窗口、双节点 DCGM 门禁、
本阶段在此停止,不自动进入 Phase 3。 低开销进程采样、机内 PCIe P2P、单/双机 AllReduce 与逐指标自动报告。
本阶段复跑完成并汇报后才进入 Phase 3。
</p> </p>
<h2>1. Phase 1 交接结果</h2> <h2>1. Phase 1 交接结果</h2>
@ -175,7 +176,7 @@
<ul> <ul>
<li>只测试 SGLang不测试 vLLM。</li> <li>只测试 SGLang不测试 vLLM。</li>
<li>保留模型、镜像、TP16、EP2、显存比例和已验证的双 Rail NCCL 配置。</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>不调参,不尝试优化;先获得足以区分瓶颈类别的硬件证据。</li>
<li>只重放五个固定代表负载和一组混合 A/B不重复 Phase 1 全矩阵。</li> <li>只重放五个固定代表负载和一组混合 A/B不重复 Phase 1 全矩阵。</li>
<li>采集器从请求开始前启动,到请求结束后停止,不能中途补采后声称完整。</li> <li>采集器从请求开始前启动,到请求结束后停止,不能中途补采后声称完整。</li>
@ -198,6 +199,7 @@
<h2>4. 诊断 Run</h2> <h2>4. 诊断 Run</h2>
<ol> <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>保存两节点静态快照GPU/NIC/NUMA 拓扑、驱动、CUDA、镜像与服务命令。</li>
<li>复用 Phase 1 已验证的 <code>run_quick_map.sh start</code> 启动同配置双机服务。</li> <li>复用 Phase 1 已验证的 <code>run_quick_map.sh start</code> 启动同配置双机服务。</li>
<li>在 Head 和 Worker 同时启动 GPU、CPU、网卡与 RDMA 采样,先记录 15 秒空闲基线。</li> <li>在 Head 和 Worker 同时启动 GPU、CPU、网卡与 RDMA 采样,先记录 15 秒空闲基线。</li>
@ -215,8 +217,8 @@ cooldown 15s
Head 与 Worker 的所有采集器覆盖完整诊断窗口。</code></pre> Head 与 Worker 的所有采集器覆盖完整诊断窗口。</code></pre>
<p> <p>
Phase 1 中服务加载约 5 分 30 秒;五个固定负载加混合 A/B、静态快照 Phase 1 中服务加载约 5 分 30 秒;通信基线、五个固定负载、混合 A/B
采样和清理,目标仍控制在约 30 分钟内 静态快照、采样和清理组成一次完整 Phase 2 Run
</p> </p>
<h3>4.1 你只需要运行的入口</h3> <h3>4.1 你只需要运行的入口</h3>
@ -255,7 +257,9 @@ tmux attach -t dsv4pro-phase2</code></pre>
<code>systemctl enable nvidia-dcgm</code> <code>systemctl enable nvidia-dcgm</code>
</p> </p>
<p><strong><code>all</code> 内部执行顺序:</strong></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 服务 → Phase 1 start启动同配置 TP16 服务
→ 两节点静态快照 → 两节点静态快照
→ 启动两节点采集器并记录 15 秒 idle → 启动两节点采集器并记录 15 秒 idle
@ -264,7 +268,7 @@ tmux attach -t dsv4pro-phase2</code></pre>
→ 15 秒 cooldown → 15 秒 cooldown
→ 停止采集器并保存后快照 → 停止采集器并保存后快照
→ Phase 1 stop停止 Head/Worker → Phase 1 stop停止 Head/Worker
→ 生成按 Case 对齐的 CSV、JSON 与 report.md</code></pre> → 生成按第 5 节逐项对应的 CSV、JSON 与 report.md</code></pre>
<p> <p>
Phase 1 的作用是提供已经验证过的双机 Docker 服务和 Benchmark 实现, Phase 1 的作用是提供已经验证过的双机 Docker 服务和 Benchmark 实现,
不是第二个用户入口。实际展开的服务、Benchmark 和采集命令都会写入 不是第二个用户入口。实际展开的服务、Benchmark 和采集命令都会写入
@ -314,9 +318,13 @@ python3 hardware_contention_attribution.py marker \
</tbody> </tbody>
</table> </table>
<p> <p>
首轮实现的 Case 边界包含 Benchmark 客户端启动、数据生成、Warm-up、正式测量和退出 最终实现由 Phase 1 监听 <code>bench.log</code> 中的
因此它适合判断硬件方向,但不是纯主测量窗口。后续实现需要围绕 <code>Starting main benchmark run</code>,立刻写入
<code>Starting main benchmark run</code> 增加更精确的起止 Marker。 <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> </p>
<h3>5.2 GPU 基础状态:<code>nvidia-smi</code></h3> <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>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>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>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>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> <tr><td>1010</td><td><code>pcie_rx_bytes</code></td><td>GPU 经 PCIe 接收的字节速率</td></tr>
</tbody> </tbody>
@ -364,31 +372,36 @@ dcgmi dmon \
<p> <p>
<code>sm_active</code> 高而 <code>sm_occupancy</code> 低,表示 SM 经常有工作, <code>sm_active</code> 高而 <code>sm_occupancy</code> 低,表示 SM 经常有工作,
但同时驻留的 Warp 不多;后续通过 Kernel Timeline 区分小 Kernel、 但同时驻留的 Warp 不多;后续通过 Kernel Timeline 区分小 Kernel、
寄存器/共享内存约束和同步。DCGM 依赖宿主 寄存器/共享内存约束和同步。DCGM 是 NVIDIA Data Center GPU Manager
<code>nvidia-dcgm</code> Host Engine首轮 Worker 未启动该服务,所以只有 <code>nvidia-dcgm</code>/<code>nv-hostengine</code> 是后台 Host Engine
Head 的 <code>dcgm_dmon.log</code> 有效。 <code>dcgmi</code> 是客户端Field ID 是指标编号。最终代码在两节点预检
<code>dcgmi discovery -l</code>,任一 Host Engine 不可用即 fail-fast
正式结果必须同时包含 Head 和 Worker 的 <code>case_dcgm_summary.csv</code>
</p> </p>
<h3>5.4 CPU、进程与 Kernel Launch 侧证据</h3> <h3>5.4 CPU、进程与 Kernel Launch 侧证据</h3>
<pre><code class="language-bash"># 全部逻辑 CPU每秒输出一次 <pre><code class="language-bash"># 全部逻辑 CPU 5 秒输出一次
mpstat -P ALL 1 mpstat -P ALL 5
# 找到容器内进程对应的宿主 PID # 找到容器内进程对应的宿主 PID
docker top &lt;container&gt; -eo pid,ppid,psr,pcpu,pmem,stat,comm,args docker top &lt;container&gt; -eo pid,ppid,psr,pcpu,pmem,stat,comm,args
# 首轮实际命令CPU、I/O、缺页、上下文切换并展开线程 # 最终命令:进程级 CPU、I/O、缺页、上下文切换不展开全部线程
pidstat -durwt -p "&lt;comma-separated-host-pids&gt;" 1 pidstat -durw -p "&lt;comma-separated-host-pids&gt;" 5
# 每秒输出一次硬件/软件计数器增量 # 每 5 秒输出一次硬件/软件计数器增量
perf stat -p "&lt;comma-separated-host-pids&gt;" -I 1000 \ perf stat -p "&lt;comma-separated-host-pids&gt;" -I 5000 \
-e cycles,instructions,cache-misses,context-switches,\ -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> <table>
<thead><tr><th>命令/字段</th><th>回答的问题</th></tr></thead> <thead><tr><th>命令/字段</th><th>回答的问题</th></tr></thead>
<tbody> <tbody>
<tr><td><code>mpstat -P ALL</code></td><td>整机是否 CPU 饱和,是否只有少量核心接近 100%,是否存在 I/O Wait</td></tr> <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>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 -d</code></td><td>进程块设备 I/O</td></tr>
<tr><td><code>pidstat -r</code></td><td>内存和 Page Fault 行为</td></tr> <tr><td><code>pidstat -r</code></td><td>内存和 Page Fault 行为</td></tr>
<tr><td><code>pidstat -w</code></td><td>主动/被动上下文切换,辅助发现线程阻塞或调度抖动</td></tr> <tr><td><code>pidstat -w</code></td><td>主动/被动上下文切换,辅助发现线程阻塞或调度抖动</td></tr>
@ -397,9 +410,10 @@ cpu-migrations,page-faults</code></pre>
</tbody> </tbody>
</table> </table>
<p> <p>
首轮 <code>pidstat -durwt</code> 对所有线程做 1 秒采样,产生约 850 MB/735 MB 最终实现使用进程级 5 秒采样,避免首轮线程级 1 秒采样产生数百 MB 日志。
的 Head/Worker 日志,信息量和扰动都过大。后续不照搬:默认改为进程级 5 秒采样; <code>case_cpu_summary.csv</code><code>case_process_summary.csv</code>
只有确认某个进程异常后,才在短窗口内开启线程级采样。 <code>case_perf_summary.csv</code> 都按正式测量窗口切片;只有先发现异常进程,
才在后续短窗口单独开启线程级采样。
</p> </p>
<h3>5.5 NUMA 与 CPU/内存亲和</h3> <h3>5.5 NUMA 与 CPU/内存亲和</h3>
@ -409,19 +423,23 @@ numastat -m
# 每 5 秒按容器宿主 PID 查看本地/远端 NUMA 内存 # 每 5 秒按容器宿主 PID 查看本地/远端 NUMA 内存
numastat -p &lt;host-pid&gt; numastat -p &lt;host-pid&gt;
# 解析为:
# wall_time_ns,node,node0_mib,node1_mib,total_mib,process_count
# 同时保存 GPU、CPU、NIC 的拓扑关系 # 同时保存 GPU、CPU、NIC 的拓扑关系
nvidia-smi topo -m</code></pre> nvidia-smi topo -m</code></pre>
<p> <p>
NUMA 是多路 CPU 机器的“本地内存”结构。进程长期从远端 NUMA Node 取内存, NUMA 是多路 CPU 机器的“本地内存”结构。进程长期从远端 NUMA Node 取内存,
或 GPU/NIC 对应的 CPU 线程被调度到另一侧,可能增加 Host 侧延迟。 或 GPU/NIC 对应的 CPU 线程被调度到另一侧,可能增加 Host 侧延迟。
<code>numastat -p</code> 的内存分布与 CPU 核、GPU 活跃区间和 Timeline 对齐, 最终采集器把 <code>numastat -p</code> 解析为
用于定位跨 NUMA 访问。 <code>numa_samples.csv</code>,再按正式测量窗口生成
<code>case_numa_summary.csv</code>。这样可以直接比较 Node0/Node1 MiB
而不是依靠人工阅读不断刷新的文本。
</p> </p>
<h3>5.6 普通网卡统计与 RDMA 数据面</h3> <h3>5.6 普通网卡统计与 RDMA 数据面</h3>
<pre><code class="language-bash"># Linux netdev 层,每秒采样吞吐与错误 <pre><code class="language-bash"># Linux netdev 层,每 5 秒采样吞吐与错误
sar -n DEV,EDEV 1 sar -n DEV,EDEV 5
# Case 前后保存物理端口状态和驱动计数器 # Case 前后保存物理端口状态和驱动计数器
ethtool eth0 ethtool eth0
@ -462,11 +480,38 @@ done</code></pre>
<p> <p>
原始数据为 <code>head|worker/rdma.csv</code> 原始数据为 <code>head|worker/rdma.csv</code>
<code>case_rdma_summary.csv</code> 按 Case、节点和 HCA 计算吞吐及错误增量。 <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 中分析持续时间和计算重叠。 放到请求 Timeline 中分析持续时间和计算重叠。
</p> </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 &lt;0-or-1&gt; \
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 <pre><code class="language-bash">nvidia-smi
nvidia-smi topo -m nvidia-smi topo -m
lscpu lscpu
@ -483,10 +528,42 @@ docker top &lt;container&gt; -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>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>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_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>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> </tbody>
</table> </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> <h2>6. 精简代码设计</h2>
<p>已新增目录:</p> <p>已新增目录:</p>
<pre><code>/data/hzy/sskj/experiments/pro6000/ <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> <tr><th>文件</th><th>职责</th><th>当前状态</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td><code>run_hardware_contention_attribution.sh</code></td><td>唯一 Shell 入口服务启停、双节点采集器、Case 编排、健康检查和 Trap 清理</td><td class="pass">已实现</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、混合 A/B 与采样策略</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>hardware_contention_attribution.py</code></td><td>Manifest、标记、Bench 校验、按 Case 时间窗切片和硬件摘要</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>tests/test_hardware_contention_attribution.py</code></td><td>GPU 统计、RDMA 单位、Marker、嵌套结果与时间窗测试</td><td class="pass">5/5 通过</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> <tr><td><code>README.md</code></td><td>唯一入口、范围和结果目录说明</td><td class="pass">已实现</td></tr>
</tbody> </tbody>
</table> </table>
<p class="decision"> <p class="decision">
Phase 2 不复制双机 Docker 启停实现。唯一入口在内部调用 Phase 1 的 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 脚本。 顶层仍只保留一个 Shell 文件,不创建额外 tmux、launch、start 或 stop 脚本。
</p> </p>
@ -522,6 +600,14 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
worker_server_cmd.txt worker_server_cmd.txt
head_server.log head_server.log
worker_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/ head/
gpu_samples.csv gpu_samples.csv
dcgm_dmon.log dcgm_dmon.log
@ -530,7 +616,7 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
sar_net.log sar_net.log
perf_stat.log perf_stat.log
docker_top.log docker_top.log
numastat.log numa_samples.csv
rdma.csv rdma.csv
static_before.log static_before.log
static_after.log static_after.log
@ -543,19 +629,27 @@ dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution/</code></pre>
gpu_summary.csv gpu_summary.csv
rdma_summary.csv rdma_summary.csv
case_windows.csv case_windows.csv
communication_summary.csv
communication_aggregate.csv
case_gpu_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 case_rdma_summary.csv
summary.json summary.json
report.md</code></pre> report.md</code></pre>
<h2>8. 验收结果</h2> <h2>8. 验收结果</h2>
<ul> <ul>
<li class="pass">五个固定负载和混合 A/B 共 8 行全部成功,口径与 Phase 1 一致。</li> <li class="pass">最终代码固定到提交 <code>30664faa41f8</code>Shell 语法检查和三个 Python 文件编译通过。</li>
<li class="pass">Head/Worker GPU、CPU、NUMA、RDMA 采集完成Case 时间窗和结构化摘要已生成。</li> <li class="pass">Phase 1 精确窗口 4/4 单元测试通过Phase 2 采集、解析与通信聚合 8/8 单元测试通过。</li>
<li class="pass">两端 NCCL 日志确认 <code>mlx5_0/mlx5_3</code> 双 Rail <code>NET/IB + GDRDMA</code></li> <li class="pass">完整 Dry-run 不启动服务即可展开全部模型、采集和通信命令;通信子流程生成 2 个 P2P、2 个单机 AllReduce、6 个双机 rank 命令。</li>
<li class="pass">服务和采集器完成清理,两节点 16 张 GPU 均已释放。</li> <li class="pass">最终代码默认要求精确测量窗口、两端 DCGM 可用和全部必需采集器存活,缺失时 fail-closed。</li>
<li>Worker DCGM 因宿主 <code>nvidia-dcgm</code> 未启动而提前退出;本 Run 的 DCGM 分析只使用 Head 数据。</li> <li>首次 Run 的性能数据保留在第 10 节;最终代码尚未在 16 卡上复跑,因此新通信基线和逐指标表暂不填写虚构数值。</li>
<li>当前 Case 时间窗包含客户端准备和 Warm-up后续使用正式主测量起止 Marker 获得精确硬件窗口。</li>
</ul> </ul>
<h2>9. 实施记录</h2> <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 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: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 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> </tbody>
</table> </table>
<h2>10. 真机结果</h2> <h2>10. 首次真机结果</h2>
<p class="decision"> <p class="decision">
<strong>Run<code>dsv4pro-phase2-20260731-130125</code>,状态 <strong>Run<code>dsv4pro-phase2-20260731-130125</code>,状态
<code>COMPLETED</code></strong>运行时间为 13:01:25 至 13:27:51 CST <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> <tr><th>范围</th><th>实际路径</th><th>本轮是否测量</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>单机 8 卡内部</td><td>无 NVLinkNCCL <code>P2P/IPC</code> 走 PCIe。GPU03、GPU47 各自在 PCIe Switch 内为 <code>PIX</code>,两组之间为 <code>SYS</code></td><td>采集了 DCGM PCIe 指标;未做独立 P2P 带宽/延迟微基准</td></tr> <tr><td>单机 8 卡内部</td><td>无 NVLinkNCCL <code>P2P/IPC</code> 走 PCIe。GPU03、GPU47 各自在 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> <tr><td>两机之间</td><td><code>mlx5_0 + mlx5_3</code> 双 Rail <code>NET/IB + GDRDMA</code></td><td>已测量每 Case HCA 流量、均衡性和错误增量</td></tr>
</tbody> </tbody>
</table> </table>
<p> <p>
在进入 Phase 3 前只需一次性补 <code>p2pBandwidthLatencyTest</code>、单机 8 卡 最终代码使用当前 SGLang 镜像内的 PyTorch/CUDA/NCCL 实现等价微基准:
<code>all_reduce_perf</code> 和双机 16 卡 <code>all_reduce_perf</code> 两节点 P2P 全矩阵、两组单机 8-rank AllReduce 和三组双机 16-rank
分开量化机内 PCIe 与跨机 RoCE 的硬件基线;后续阶段不重复跑这些微基准。 <code>NCCL_CROSS_NIC</code> A/B。结果统一写入
<code>communication_summary.csv</code><code>communication_aggregate.csv</code>
完成 Phase 2 后,后续阶段不重复跑这些微基准。
</p> </p>
<h3>10.5 首轮结论与采集限制</h3> <h3>10.5 首轮结论与最终代码修正</h3>
<p class="decision"> <p class="decision">
当前证据支持把下一步缩到 TP16 的 Kernel、Scheduler、PCIe/RDMA Collective 当前证据支持把下一步缩到 TP16 的 Kernel、Scheduler、PCIe/RDMA Collective
和 Rank 同步时间线。Phase 3 只分析真实请求中通信出现的位置、耗时和与计算的 和 Rank 同步时间线。Phase 3 只分析真实请求中通信出现的位置、耗时和与计算的
@ -652,9 +749,10 @@ tmux new-session -d -s dsv4pro-phase2 \
CPU 容量和降频都不像首要瓶颈Phase 3 将继续定位具体 Kernel 和同步根因。 CPU 容量和降频都不像首要瓶颈Phase 3 将继续定位具体 Kernel 和同步根因。
</p> </p>
<ul> <ul>
<li>Worker 的 <code>nvidia-dcgm</code> Host Engine 未启动,导致 Worker DCGM 提前退出。重跑前只需在 <code>.7</code> 启动 DCGM整套 Phase 2 仍只在 <code>.5</code> 执行。</li> <li>Worker DCGM 缺失已改为双节点启动前门禁;最终 Run 不再接受单边 DCGM 数据。</li>
<li>当前 Case 时间窗包含客户端启动、数据准备、Warm-up 和退出,硬件平均值被非正式测量阶段稀释;后续要增加主 benchmark 精确起止标记。</li> <li>宽泛 Case 窗口已改为正式 benchmark 起点加 duration 的精确窗口,并由 <code>REQUIRE_PRECISE_WINDOWS=1</code> 强制执行。</li>
<li><code>pidstat -durwt</code> 一秒采样生成约 850/735 MB 日志;后续改为进程级或 5 秒采样,减少诊断扰动。</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> <li>完整归因见 <a href="./results/dsv4pro-phase2-20260731-130125/analysis.md"><code>analysis.md</code></a>,原始结构化摘要和两端 NCCL 证据已一并归档。</li>
</ul> </ul>

View File

@ -415,7 +415,7 @@
<article id="document-content"> <article id="document-content">
<h1>6000D 双机 DeepSeek-V4-Pro 推理优化计划</h1> <h1>6000D 双机 DeepSeek-V4-Pro 推理优化计划</h1>
<blockquote> <blockquote>
<p>适用环境:<code>174.1.51.5 + 174.1.51.7</code>,每台 8 张 RTX PRO 6000 Blackwell Server Edition<br>当前部署DeepSeek-V4-Pro16 张 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-Pro16 张 GPU 组成一个完整实例<br>当前约束:模型暂时只能使用全部 16 张 GPU无法额外复制一套模型进行 PD 分离<br>计划版本2026-07-31 15:25:00 CST</p>
</blockquote> </blockquote>
<h2>当前执行状态与阶段档案</h2> <h2>当前执行状态与阶段档案</h2>
<table> <table>
@ -439,7 +439,7 @@
</tr> </tr>
<tr> <tr>
<td>DeepSeek-V4-Pro / 双机 Pro6000D / SGLang 硬件与资源竞争归因</td> <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> <td><a href="./phase2_dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution.html">打开 Phase 2 档案</a></td>
</tr> </tr>
</tbody></table> </tbody></table>
@ -480,8 +480,8 @@
GPU03 与 GPU47 各自在本地 PCIe Switch 内通信,两组之间还要经过 Host Bridge GPU03 与 GPU47 各自在本地 PCIe Switch 内通信,两组之间还要经过 Host Bridge
和 CPU/NUMA 互联。NCCL 机内日志中的 <code>P2P/IPC</code> 是 CUDA P2P over PCIe 和 CPU/NUMA 互联。NCCL 机内日志中的 <code>P2P/IPC</code> 是 CUDA P2P over PCIe
两机之间则使用 <code>mlx5_0/mlx5_3</code> 双 Rail 两机之间则使用 <code>mlx5_0/mlx5_3</code> 双 Rail
<code>NET/IB + GDRDMA</code>。Phase 2 已测跨机 HCA 流量,但机内 PCIe 的独立 <code>NET/IB + GDRDMA</code>。Phase 2 最终代码已加入机内 PCIe P2P 全矩阵、
GPU-to-GPU 带宽和延迟仍需微基准补齐 单机 8-rank 和双机 16-rank NCCL 微基准;正式数值在最终复跑后写入阶段档案
</p> </p>
<h2>1. 目标与原则</h2> <h2>1. 目标与原则</h2>
<h3>1.1 最终目标</h3> <h3>1.1 最终目标</h3>
@ -539,7 +539,7 @@ GPU-to-GPU 带宽和延迟仍需微基准补齐。
<tr> <tr>
<td>H6</td> <td>H6</td>
<td>KV Cache 容量、碎片或 Preemption 限制并发</td> <td>KV Cache 容量、碎片或 Preemption 限制并发</td>
<td>大模型权重占用高,剩余 HBM 决定上下文与并发容量</td> <td>大模型权重占用高,剩余设备显存决定上下文与并发容量</td>
</tr> </tr>
<tr> <tr>
<td>H7</td> <td>H7</td>
@ -735,9 +735,9 @@ TPOT P95 增加 66.55%。Phase 2 将围绕这两个现象采集硬件时间序
<a href="./phase2_dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution.html"> <a href="./phase2_dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution.html">
Phase 2硬件与资源竞争归因档案</a>。本阶段重放长 Prefill、并发 Prefill、 Phase 2硬件与资源竞争归因档案</a>。本阶段重放长 Prefill、并发 Prefill、
普通 Decode、长输出 Decode、长上下文 Decode以及 普通 Decode、长输出 Decode、长上下文 Decode以及
<code>1K → 1K, C=32</code> 的混合 A/B。正式 Run <code>1K → 1K, C=32</code> 的混合 A/B。首轮 Run
<code>dsv4pro-phase2-20260731-130125</code> 已在 26 分 26 秒内完成 8/8 个结果, <code>dsv4pro-phase2-20260731-130125</code> 在 26 分 26 秒内完成 8/8 个结果;
无 OOM本阶段先停在首轮归因不自动进入 Phase 3。 最终采集代码固定到提交 <code>30664faa41f8</code>,完成最终复跑和汇报后才进入 Phase 3。
</p> </p>
<h3>6.1 首轮归因结果</h3> <h3>6.1 首轮归因结果</h3>
<ul> <ul>
@ -745,91 +745,34 @@ Phase 2硬件与资源竞争归因档案</a>。本阶段重放长 Prefill、
<li>128K Prefill 注入窗口两节点 GPU 平均利用率约 99%,功耗约 274 W频率稳定显存每卡约 83.283.4 GiB只剩约 2.3 GiB 余量。</li> <li>128K Prefill 注入窗口两节点 GPU 平均利用率约 99%,功耗约 274 W频率稳定显存每卡约 83.283.4 GiB只剩约 2.3 GiB 余量。</li>
<li>整机 CPU 平均约 8%11%,没有全机 CPU 或 I/O Wait 饱和,但少量 CPU 核持续高负载Scheduler/Affinity/NUMA 热点仍需关注。</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>双 Rail 流量对称且错误增量为 0最高平均总发送约 140 Gbit/s即每条 400G Rail 约 70 Gbit/s原始 RoCE 带宽未饱和。</li>
<li>下一步应捕获短 TP16 Timeline区分 GPU Kernel、Scheduler gap、机内 PCIe、跨机 Collective 与 Rank 同步,而不是重跑完整矩阵。</li>
</ul> </ul>
<p> <p>
首轮仍有两个采集限制Worker 的 DCGM Host Engine 未启动,因此只有 Head DCGM 最终代码已经把首轮的采集限制变成强制门禁:两节点
有效Case 时间窗包含客户端准备和 Warm-up硬件均值并非纯主测量窗口。重跑前应先在 <code>dcgmi discovery -l</code> 必须成功;每个 Case 使用
<code>.7</code> 启动 <code>nvidia-dcgm</code>,并增加主 benchmark 精确起止标记。 <code>Starting main benchmark run</code> 加 SGLang duration 形成正式测量窗口;
必需采集器提前退出会让 Run 失败。
</p> </p>
<h3>6.2 GPU</h3> <h3>6.2 最终采集范围</h3>
<p>测试期间持续记录:</p>
<pre><code class="language-bash">nvidia-smi dmon -s pucvmt -d 1
</code></pre>
<p>重点观察:</p>
<ul> <ul>
<li>SM Utilization。</li> <li><code>nvidia-smi</code>GPU/显存利用率、显存、功耗、温度、SM/Memory Clock 和 P-state每秒一次。</li>
<li>HBM Utilization。</li> <li>DCGMSM Active/Occupancy、Tensor Active、设备显存接口 Active、PCIe TX/RX每秒一次。</li>
<li>显存占用。</li> <li><code>mpstat/pidstat/perf/numastat</code>整机、热点核、服务进程、CPU 计数器和 NUMA 内存,每 5 秒一次。</li>
<li>GPU Clock、Memory Clock。</li> <li><code>sar</code> 与 HCA Counter<code>eth0/eth3</code><code>mlx5_0/mlx5_3</code> 的吞吐、均衡、错误与重试。</li>
<li>Power 与温度降频。</li> <li>通信基线:两节点 CUDA P2P 全矩阵、两组 8-rank AllReduce、三组 16-rank <code>NCCL_CROSS_NIC=0/1/2</code></li>
<li>PCIe RX/TX。</li>
</ul> </ul>
<p>如果有 DCGM增加</p> <h3>6.3 数据与报告规则</h3>
<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 &lt;PID&gt; 1
mpstat -P ALL 1
numastat -p &lt;PID&gt;
</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>
<p> <p>
Phase 1 正式 Run 已从两端 NCCL 日志确认 每种指标都生成独立的 <code>case_*_summary.csv</code>。最终
<code>NET/IB</code> 同时识别 <code>mlx5_0/mlx5_3</code>,跨节点 Channel <code>report.md</code> 必须逐项列出原始文件、有效样本数、Head/Worker 数值、
使用 <code>GDRDMA</code>。Phase 2 继续保留相同 fail-closed 门禁,并采集两条 Mean/P95/Max、Case 间变化和解释,不能只写抽象结论。缺失数据写
Rail 的流量和错误计数 <code>-</code> 并报告采集器状态,不能当成 0。
</p> </p>
<p>当前拓扑中需要分别观察两条 Compute Rail确认</p> <h3>6.4 与 Phase 3 的边界</h3>
<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>
<p> <p>
Phase 1 已证明 <code>NCCL_CROSS_NIC=1</code> 可以稳定完成端到端 Run Phase 2 回答“哪个硬件/Host/通信资源在什么 Case 中升高,以及硬件基线是多少”。
但这不代表它是性能最优值。固定其余环境后比较 <code>0/1/2</code> Phase 3 只捕获短 TP16 Timeline回答具体 Kernel、Scheduler gap、Collective、
不能仅凭双 Rail 拓扑判断 Rank 同步及计算/通信重叠,不重复 Phase 2 的长时间轻量采样。
</p> </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时间线 ProfilingNsight Systems 为主)</h2> <h2>7. Phase 3时间线 ProfilingNsight Systems 为主)</h2>
<p> <p>
Nsight Systems、PyTorch Profiler 和 NVTX 是三件不同的东西Nsight Systems Nsight Systems、PyTorch Profiler 和 NVTX 是三件不同的东西Nsight Systems
@ -976,7 +919,7 @@ SGLang 进程,并绑定相同 Case ID 和时间窗口。正式执行前先验
</tr> </tr>
<tr> <tr>
<td>KV Cache 长期接近满并发生重算</td> <td>KV Cache 长期接近满并发生重算</td>
<td>HBM 容量不足</td> <td>设备显存容量不足</td>
<td>FP8 KV、并发和 Context 上限</td> <td>FP8 KV、并发和 Context 上限</td>
</tr> </tr>
<tr> <tr>

View File

@ -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()

View File

@ -21,6 +21,10 @@ RUN_MIXED_CASE="${RUN_MIXED_CASE:-1}"
# Monitoring policy. # Monitoring policy.
SAMPLE_INTERVAL_S="${SAMPLE_INTERVAL_S:-1}" 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}" IDLE_BASELINE_S="${IDLE_BASELINE_S:-15}"
POST_RUN_COOLDOWN_S="${POST_RUN_COOLDOWN_S:-15}" POST_RUN_COOLDOWN_S="${POST_RUN_COOLDOWN_S:-15}"
CASE_COOLDOWN_S="${CASE_COOLDOWN_S:-5}" 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}" RDMA_HCAS="${RDMA_HCAS:-mlx5_0 mlx5_3}"
NUMASTAT_INTERVAL_S="${NUMASTAT_INTERVAL_S:-5}" NUMASTAT_INTERVAL_S="${NUMASTAT_INTERVAL_S:-5}"
CLOCK_SKEW_TOLERANCE_S="${CLOCK_SKEW_TOLERANCE_S:-2}" 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)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && 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}" RUNTIME_BASE="${RUNTIME_BASE:-${SCRIPT_DIR}/runtime}"
DRY_RUN="${DRY_RUN:-0}" DRY_RUN="${DRY_RUN:-0}"
ALLOW_PARTIAL_COLLECTORS="${ALLOW_PARTIAL_COLLECTORS:-1}" ALLOW_PARTIAL_COLLECTORS="${ALLOW_PARTIAL_COLLECTORS:-0}"

View File

@ -11,6 +11,7 @@ ACTION="${1:-all}"
RUN_ID="${RUN_ID:-dsv4pro-phase2-$(date +%Y%m%d-%H%M%S)}" RUN_ID="${RUN_ID:-dsv4pro-phase2-$(date +%Y%m%d-%H%M%S)}"
RESULT_DIR="${RESULT_BASE}/${RUN_ID}" RESULT_DIR="${RESULT_BASE}/${RUN_ID}"
RESULT_TOOL="${SCRIPT_DIR}/hardware_contention_attribution.py" RESULT_TOOL="${SCRIPT_DIR}/hardware_contention_attribution.py"
COMMUNICATION_TOOL="${SCRIPT_DIR}/communication_baseline.py"
MARKERS_PATH="${RESULT_DIR}/markers.csv" MARKERS_PATH="${RESULT_DIR}/markers.csv"
SERVICE_DIR="${RESULT_DIR}/service" SERVICE_DIR="${RESULT_DIR}/service"
COMMAND_DIR="${RESULT_DIR}/commands" COMMAND_DIR="${RESULT_DIR}/commands"
@ -72,6 +73,10 @@ validate_config() {
log "ERROR: result tool is missing or not executable: ${RESULT_TOOL}" log "ERROR: result tool is missing or not executable: ${RESULT_TOOL}"
return 1 return 1
} }
[[ -f "${COMMUNICATION_TOOL}" ]] || {
log "ERROR: communication tool is missing: ${COMMUNICATION_TOOL}"
return 1
}
case "${RUN_MIXED_CASE}" in case "${RUN_MIXED_CASE}" in
0|1) ;; 0|1) ;;
*) *)
@ -86,6 +91,20 @@ validate_config() {
return 1 return 1
;; ;;
esac 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]*$ ]] || { [[ "${SAMPLE_INTERVAL_S}" =~ ^[1-9][0-9]*$ ]] || {
log "ERROR: SAMPLE_INTERVAL_S must be a positive integer" log "ERROR: SAMPLE_INTERVAL_S must be a positive integer"
return 1 return 1
@ -94,6 +113,14 @@ validate_config() {
log "ERROR: NUMASTAT_INTERVAL_S must be a positive integer" log "ERROR: NUMASTAT_INTERVAL_S must be a positive integer"
return 1 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]+$ ]] || { [[ "${CLOCK_SKEW_TOLERANCE_S}" =~ ^[0-9]+$ ]] || {
log "ERROR: CLOCK_SKEW_TOLERANCE_S must be a non-negative integer" log "ERROR: CLOCK_SKEW_TOLERANCE_S must be a non-negative integer"
return 1 return 1
@ -121,6 +148,13 @@ preflight_node_tools() {
log "ERROR: ${node} is missing required tool: dcgmi" log "ERROR: ${node} is missing required tool: dcgmi"
return 1 return 1
fi 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 fi
local hca local hca
for hca in ${RDMA_HCAS}; do for hca in ${RDMA_HCAS}; do
@ -149,6 +183,20 @@ preflight_clock_sync() {
log "node clock skew check passed: delta=${delta}s" 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() { write_manifest() {
local git_commit git_dirty local git_commit git_dirty
git_commit="$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || printf unknown)" 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}" \ --fixed-case-ids "${FIXED_CASE_IDS}" \
--run-mixed-case "${RUN_MIXED_CASE}" \ --run-mixed-case "${RUN_MIXED_CASE}" \
--sample-interval-s "${SAMPLE_INTERVAL_S}" \ --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}" \ --numastat-interval-s "${NUMASTAT_INTERVAL_S}" \
--clock-skew-tolerance-s "${CLOCK_SKEW_TOLERANCE_S}" \ --clock-skew-tolerance-s "${CLOCK_SKEW_TOLERANCE_S}" \
--idle-baseline-s "${IDLE_BASELINE_S}" \ --idle-baseline-s "${IDLE_BASELINE_S}" \
--post-run-cooldown-s "${POST_RUN_COOLDOWN_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}" --dry-run "${DRY_RUN}"
} }
@ -212,6 +266,185 @@ run_phase1_action() {
"${command[@]}" "${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() { start_service() {
mark_event service_start mark_event service_start
# Set this before launch so the EXIT trap also cleans a partially started pair. # 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 while :; do
printf 'wall_time_ns=%s\\n' \"\$(date +%s%N)\" printf 'wall_time_ns=%s\\n' \"\$(date +%s%N)\"
docker top '${container}' -eo pid,ppid,psr,pcpu,pmem,stat,comm,args docker top '${container}' -eo pid,ppid,psr,pcpu,pmem,stat,comm,args
sleep '${SAMPLE_INTERVAL_S}' sleep '${PROCESS_SAMPLE_INTERVAL_S}'
done done
" "
} }
@ -383,20 +616,27 @@ fi
} }
numastat_command() { numastat_command() {
local container="$1" local role="$1"
local container="$2"
printf '%s' " printf '%s' "
trap 'exit 0' HUP TERM PIPE trap 'exit 0' HUP TERM PIPE
printf '%s\n' 'wall_time_ns,node,node0_mib,node1_mib,total_mib,processes'
while :; do while :; do
printf 'wall_time_ns=%s\\n' \"\$(date +%s%N)\"
PIDS=\$(docker top '${container}' -eo pid 2>/dev/null | PIDS=\$(docker top '${container}' -eo pid 2>/dev/null |
awk 'NR > 1 {print \$1}') awk 'NR > 1 {print \$1}')
if [[ -z \"\${PIDS}\" ]]; then if [[ -z \"\${PIDS}\" ]]; then
printf 'container has no visible processes: %s\\n' '${container}' >&2 printf 'container has no visible processes: %s\\n' '${container}' >&2
exit 1 exit 1
fi fi
values=\$(
for pid in \${PIDS}; do for pid in \${PIDS}; do
numastat -p \"\${pid}\" numastat -p \"\${pid}\" 2>/dev/null |
done 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}' sleep '${NUMASTAT_INTERVAL_S}'
done done
" "
@ -416,23 +656,36 @@ dcgmi dmon -e '${DCGM_FIELD_IDS}' -d '$(( SAMPLE_INTERVAL_S * 1000 ))' |
" "
local mpstat_command=" local mpstat_command="
trap 'exit 0' HUP TERM PIPE 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 local pidstat_command
pidstat_command="$(container_pid_preamble "${container}")" pidstat_command="$(container_pid_preamble "${container}")"
pidstat_command+=" pidstat_command+="
trap 'exit 0' HUP TERM PIPE 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=" local sar_command="
trap 'exit 0' HUP TERM PIPE 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 local perf_command
perf_command="$(container_pid_preamble "${container}")" perf_command="$(container_pid_preamble "${container}")"
perf_command+=" perf_command+="
trap 'exit 0' HUP TERM PIPE 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 \ start_stream_collector \
@ -452,7 +705,7 @@ perf stat -p \"\${PIDS}\" -I 1000 -e '${PERF_EVENTS}'
start_stream_collector \ start_stream_collector \
"${role}" "${node}" docker_top.log "$(docker_top_command "${container}")" "${role}" "${node}" docker_top.log "$(docker_top_command "${container}")"
start_stream_collector \ start_stream_collector \
"${role}" "${node}" numastat.log "$(numastat_command "${container}")" "${role}" "${node}" numa_samples.csv "$(numastat_command "${role}" "${container}")"
} }
start_collectors() { start_collectors() {
@ -581,7 +834,8 @@ run_mixed_case() {
} }
summarize_results() { summarize_results() {
python3 "${RESULT_TOOL}" summarize "${RESULT_DIR}" python3 "${RESULT_TOOL}" summarize "${RESULT_DIR}" \
--require-precise-windows "${REQUIRE_PRECISE_WINDOWS}"
} }
finish_manifest() { finish_manifest() {
@ -595,6 +849,7 @@ cleanup() {
local rc=$? local rc=$?
stop_collectors || true stop_collectors || true
stop_service || true stop_service || true
cleanup_communication_containers || true
if (( rc != 0 )) && [[ -f "${RESULT_DIR}/manifest.json" ]]; then if (( rc != 0 )) && [[ -f "${RESULT_DIR}/manifest.json" ]]; then
finish_manifest ABORTED || true finish_manifest ABORTED || true
fi fi
@ -620,6 +875,7 @@ run_all() {
fi fi
trap cleanup EXIT INT TERM trap cleanup EXIT INT TERM
run_communication_baseline
start_service start_service
capture_static_snapshots before capture_static_snapshots before
start_collectors start_collectors
@ -652,6 +908,10 @@ run_all() {
mark_event cooldown_start mark_event cooldown_start
sleep_if_real "${POST_RUN_COOLDOWN_S}" sleep_if_real "${POST_RUN_COOLDOWN_S}"
mark_event cooldown_end mark_event cooldown_end
if ! check_collectors; then
log "ERROR: one or more required collectors exited during the run"
((failures+=1))
fi
stop_collectors stop_collectors
capture_static_snapshots after capture_static_snapshots after
stop_service stop_service
@ -666,6 +926,22 @@ run_all() {
(( failures == 0 )) (( 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() { main() {
case "${ACTION}" in case "${ACTION}" in
all) all)
@ -675,12 +951,15 @@ main() {
enable_result_logging enable_result_logging
summarize_results summarize_results
;; ;;
communication)
run_communication_only
;;
stop) stop)
enable_result_logging enable_result_logging
stop_service stop_service
;; ;;
*) *)
printf 'Usage: %s {all|summarize|stop}\n' "$0" >&2 printf 'Usage: %s {all|communication|summarize|stop}\n' "$0" >&2
return 2 return 2
;; ;;
esac esac

View File

@ -113,6 +113,10 @@ class HardwareContentionAttributionTest(unittest.TestCase):
rows = attribution.read_csv_rows(result_dir / "bench_summary.csv") rows = attribution.read_csv_rows(result_dir / "bench_summary.csv")
self.assertEqual(rows[0]["phase2_bench_run"], "prefill") self.assertEqual(rows[0]["phase2_bench_run"], "prefill")
self.assertTrue((result_dir / "report.md").exists()) 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: def test_case_hardware_is_cut_by_phase1_meta_window(self) -> None:
with tempfile.TemporaryDirectory() as temporary: with tempfile.TemporaryDirectory() as temporary:
@ -179,6 +183,113 @@ class HardwareContentionAttributionTest(unittest.TestCase):
"long_prefill_latency_128k_c1", "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 @staticmethod
def _write_csv( def _write_csv(
path: Path, path: Path,

View File

@ -9,8 +9,9 @@ import json
import math import math
import re import re
import statistics import statistics
import time
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -33,6 +34,10 @@ SUMMARY_FIELDS = [
"started_at", "started_at",
"ended_at", "ended_at",
"elapsed_s", "elapsed_s",
"measurement_started_at",
"measurement_ended_at",
"measurement_duration_s",
"measurement_window_source",
"completed", "completed",
"failed", "failed",
"duration_s", "duration_s",
@ -305,6 +310,7 @@ def parse_scenarios(path: Path) -> list[dict[str, Any]]:
def write_case(args: argparse.Namespace) -> None: def write_case(args: argparse.Namespace) -> None:
measurement = measurement_window(args.measurement_marker, args.bench_file)
value = { value = {
"run_id": args.run_id, "run_id": args.run_id,
"suite": args.suite, "suite": args.suite,
@ -323,6 +329,7 @@ def write_case(args: argparse.Namespace) -> None:
"started_at": args.started_at, "started_at": args.started_at,
"ended_at": args.ended_at, "ended_at": args.ended_at,
"elapsed_s": args.elapsed_s, "elapsed_s": args.elapsed_s,
**measurement,
"bench_file": args.bench_file, "bench_file": args.bench_file,
"bench_log": args.bench_log, "bench_log": args.bench_log,
"note": args.note, "note": args.note,
@ -330,6 +337,51 @@ def write_case(args: argparse.Namespace) -> None:
write_json(args.path, value) 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: def write_manifest(args: argparse.Namespace) -> None:
value: dict[str, Any] = {} value: dict[str, Any] = {}
if args.path.exists(): 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("--started-at", required=True)
parser.add_argument("--ended-at", required=True) parser.add_argument("--ended-at", required=True)
parser.add_argument("--elapsed-s", type=float, 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-file", required=True)
parser.add_argument("--bench-log", required=True) parser.add_argument("--bench-log", required=True)
parser.add_argument("--note", default="") 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("--error-type", required=True)
mark_failed_parser.add_argument("--note", 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 = subparsers.add_parser("summarize")
summarize_parser.add_argument("result_dir", type=Path) summarize_parser.add_argument("result_dir", type=Path)
@ -708,6 +764,8 @@ def main() -> None:
complete_manifest(args.path, args.status) complete_manifest(args.path, args.status)
elif args.command == "mark-case-failed": elif args.command == "mark-case-failed":
mark_case_failed(args.path, args.error_type, args.note) 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": elif args.command == "summarize":
summarize(args.result_dir) summarize(args.result_dir)

View File

@ -483,6 +483,7 @@ write_case_meta() {
local bench_file="${18}" local bench_file="${18}"
local bench_log="${19}" local bench_log="${19}"
local note="${20}" local note="${20}"
local measurement_marker="${21}"
python3 "${RESULT_TOOL}" write-case \ python3 "${RESULT_TOOL}" write-case \
--path "${meta_path}" \ --path "${meta_path}" \
@ -503,6 +504,7 @@ write_case_meta() {
--started-at "${started_at}" \ --started-at "${started_at}" \
--ended-at "${ended_at}" \ --ended-at "${ended_at}" \
--elapsed-s "${elapsed_s}" \ --elapsed-s "${elapsed_s}" \
--measurement-marker "${measurement_marker}" \
--bench-file "${bench_file}" \ --bench-file "${bench_file}" \
--bench-log "${bench_log}" \ --bench-log "${bench_log}" \
--note "${note}" --note "${note}"
@ -542,6 +544,25 @@ detect_error_type() {
fi 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() { run_bench_case() {
local suite="$1" local suite="$1"
local case_id="$2" local case_id="$2"
@ -563,6 +584,7 @@ run_bench_case() {
local bench_log="${case_dir}/bench.log" local bench_log="${case_dir}/bench.log"
local meta_path="${case_dir}/meta.json" local meta_path="${case_dir}/meta.json"
local command_file="${case_dir}/bench_cmd.txt" 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 if case_already_completed "${meta_path}" "${bench_file}"; then
log "SKIP completed case=${case_id} rep=${repetition}" log "SKIP completed case=${case_id} rep=${repetition}"
@ -591,14 +613,22 @@ run_bench_case() {
"${BENCH_CMD[@]}" > "${command_file}" "${BENCH_CMD[@]}" > "${command_file}"
local started_at start_epoch ended_at elapsed_s rc status error_type local started_at start_epoch ended_at elapsed_s rc status error_type
local bench_pid marker_pid
started_at="$(iso_now)" started_at="$(iso_now)"
start_epoch="$(date +%s)" start_epoch="$(date +%s)"
log "START case=${case_id} rep=${repetition} isl=${isl} osl=${osl} c=${concurrency}" log "START case=${case_id} rep=${repetition} isl=${isl} osl=${osl} c=${concurrency}"
rm -f "${measurement_marker}"
set +e set +e
timeout --signal=TERM --kill-after=30s "${timeout_s}s" \ 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=$? rc=$?
wait "${marker_pid}" >/dev/null 2>&1 || true
set -e set -e
ended_at="$(iso_now)" ended_at="$(iso_now)"
@ -618,7 +648,8 @@ run_bench_case() {
"${meta_path}" "${suite}" "${case_id}" "${role}" "${stage}" \ "${meta_path}" "${suite}" "${case_id}" "${role}" "${stage}" \
"${repetition}" "${isl}" "${osl}" "${concurrency}" "${num_prompts}" \ "${repetition}" "${isl}" "${osl}" "${concurrency}" "${num_prompts}" \
"${warmup_requests}" "${status}" "${error_type}" "${rc}" "${started_at}" \ "${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_STATUS="${status}"
LAST_CASE_ERROR="${error_type}" LAST_CASE_ERROR="${error_type}"

View File

@ -4,6 +4,7 @@ import json
import sys import sys
import tempfile import tempfile
import unittest import unittest
from datetime import datetime
from pathlib import Path from pathlib import Path
@ -111,6 +112,35 @@ class QuickMapResultsTest(unittest.TestCase):
self.assertIn("long_prefill_latency_128k_c1", report_text) self.assertIn("long_prefill_latency_128k_c1", report_text)
self.assertIn("OOM", 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 @staticmethod
def _write_meta( def _write_meta(
path: Path, path: Path,