587 lines
27 KiB
HTML
587 lines
27 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="light">
|
||
<title>Phase 2 Code:DSV4-Pro 双机 Pro6000D SGLang 硬件竞争归因</title>
|
||
<style>
|
||
:root {
|
||
--canvas: #eef3f4;
|
||
--paper: #ffffff;
|
||
--ink: #182126;
|
||
--muted: #5a6970;
|
||
--line: #d4dee1;
|
||
--navy: #17363d;
|
||
--teal: #087c72;
|
||
--teal-soft: #e8f5f3;
|
||
--amber: #a64c14;
|
||
--amber-soft: #fff1e7;
|
||
--code-bg: #17252b;
|
||
--code-ink: #eaf2f3;
|
||
}
|
||
* { box-sizing: border-box; letter-spacing: 0; }
|
||
html { scroll-behavior: smooth; }
|
||
body {
|
||
margin: 0;
|
||
color: var(--ink);
|
||
background: var(--canvas);
|
||
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", Arial, sans-serif;
|
||
font-size: 16px;
|
||
line-height: 1.72;
|
||
}
|
||
header { color: #f6fbfb; background: var(--navy); border-bottom: 5px solid #d2692b; }
|
||
.header-inner, main { width: min(100% - 36px, 1120px); margin: 0 auto; }
|
||
.header-inner { padding: 34px 0 30px; }
|
||
.eyebrow { margin: 0 0 6px; color: #9edbd5; font-size: 13px; font-weight: 700; }
|
||
h1 { margin: 0; font-size: clamp(28px, 4vw, 42px); line-height: 1.25; }
|
||
.meta { margin-top: 15px; color: #d6e5e7; font-size: 14px; }
|
||
main {
|
||
margin-top: 30px;
|
||
margin-bottom: 70px;
|
||
padding: 38px 48px 58px;
|
||
background: var(--paper);
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
box-shadow: 0 12px 30px rgba(27, 45, 51, 0.07);
|
||
}
|
||
h2 {
|
||
margin: 46px 0 15px;
|
||
padding-bottom: 8px;
|
||
font-size: 25px;
|
||
line-height: 1.35;
|
||
border-bottom: 2px solid #adbbc0;
|
||
}
|
||
h2:first-of-type { margin-top: 18px; }
|
||
h3 { margin: 29px 0 10px; color: #21454d; font-size: 19px; }
|
||
h4 { margin: 22px 0 8px; font-size: 16px; }
|
||
p, ul, ol { margin-top: 0; margin-bottom: 16px; }
|
||
li + li { margin-top: 5px; }
|
||
a { color: var(--teal); text-underline-offset: 3px; }
|
||
code {
|
||
padding: 2px 5px;
|
||
color: #85380d;
|
||
background: var(--amber-soft);
|
||
border-radius: 3px;
|
||
font-family: "SFMono-Regular", Consolas, monospace;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
pre {
|
||
margin: 14px 0 22px;
|
||
padding: 16px 18px;
|
||
overflow: auto;
|
||
color: var(--code-ink);
|
||
background: var(--code-bg);
|
||
border-radius: 5px;
|
||
font: 13px/1.62 "SFMono-Regular", Consolas, monospace;
|
||
}
|
||
pre code { padding: 0; color: inherit; background: transparent; }
|
||
table { width: 100%; margin: 16px 0 26px; border-collapse: collapse; font-size: 14px; }
|
||
th, td {
|
||
padding: 9px 11px;
|
||
vertical-align: top;
|
||
text-align: left;
|
||
border: 1px solid var(--line);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
th { color: #153b41; background: #eaf2f2; }
|
||
tbody tr:nth-child(even) { background: #fafcfc; }
|
||
.callout { margin: 18px 0 26px; padding: 14px 18px; background: var(--teal-soft); border-left: 4px solid var(--teal); }
|
||
.warning { margin: 18px 0 26px; padding: 14px 18px; background: var(--amber-soft); border-left: 4px solid var(--amber); }
|
||
.toc { columns: 2; column-gap: 38px; margin: 16px 0 24px; padding-left: 22px; }
|
||
.toc li { break-inside: avoid; }
|
||
.path { font-family: "SFMono-Regular", Consolas, monospace; font-size: 13px; }
|
||
footer { margin-top: 48px; padding-top: 18px; color: var(--muted); border-top: 1px solid var(--line); }
|
||
@media (max-width: 760px) {
|
||
main { padding: 28px 20px 42px; }
|
||
.toc { columns: 1; }
|
||
table { display: block; overflow-x: auto; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="header-inner">
|
||
<p class="eyebrow">Standalone Code Walkthrough / Phase 2</p>
|
||
<h1>DSV4-Pro 双机 Pro6000D SGLang 硬件竞争归因:代码详解</h1>
|
||
<div class="meta">
|
||
行号基线:<code>ca1f2f63375c</code>
|
||
生成时间:2026-07-31 13:04:21 CST
|
||
入口:<code>run_hardware_contention_attribution.sh</code>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<div class="callout">
|
||
<strong>文档边界:</strong>这是一份独立代码档案。它解释 Phase 2 如何复用 Phase 1 请求、
|
||
同步采集两节点监控,并按真实 Case 时间窗做硬件归因。所有行号绑定提交
|
||
<code>ca1f2f63375c</code>。
|
||
</div>
|
||
|
||
<h2 id="read">1. 阅读导航</h2>
|
||
<ul class="toc">
|
||
<li><a href="#flow">总体控制流</a></li>
|
||
<li><a href="#files">文件职责</a></li>
|
||
<li><a href="#reuse">Phase 1 复用边界</a></li>
|
||
<li><a href="#preflight">预检与时钟</a></li>
|
||
<li><a href="#collectors">采集器实现</a></li>
|
||
<li><a href="#cases">Case 编排</a></li>
|
||
<li><a href="#alignment">时间窗对齐</a></li>
|
||
<li><a href="#outputs">结构化输出</a></li>
|
||
<li><a href="#index">函数行号索引</a></li>
|
||
</ul>
|
||
|
||
<h2 id="flow">2. 总体控制流</h2>
|
||
<pre><code>main "$@"
|
||
└─ ACTION=all → run_all
|
||
├─ validate_config
|
||
├─ preflight_node_tools + preflight_clock_sync
|
||
├─ start_service
|
||
│ └─ 委托 Phase 1 的 start
|
||
├─ capture_static_snapshots before
|
||
├─ start_collectors
|
||
│ └─ Head 与 Worker 各启动 9 类采集器
|
||
├─ idle baseline
|
||
├─ 固定诊断 Case
|
||
│ └─ 每个 Case 委托 Phase 1 的 fixed
|
||
├─ 混合 Prefill/Decode A/B
|
||
│ └─ 委托 Phase 1 的 mixed
|
||
├─ cooldown
|
||
├─ stop_collectors
|
||
├─ capture_static_snapshots after
|
||
├─ stop_service
|
||
├─ summarize_results
|
||
│ └─ 按每个 Case 的 started_at/ended_at 切监控窗口
|
||
└─ finish_manifest</code></pre>
|
||
<p>
|
||
Phase 2 不复制模型部署和 benchmark 生成代码。它新增的是诊断编排层:
|
||
在同一个服务生命周期中,让两节点的 GPU、CPU、NUMA、网络和 RDMA 时间序列包围 benchmark,
|
||
最后按 Case 时间切片。
|
||
</p>
|
||
|
||
<h2 id="files">3. 文件职责</h2>
|
||
<table>
|
||
<thead><tr><th>文件</th><th>行数</th><th>职责</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td class="path">run_hardware_contention_attribution.sh</td><td>689</td>
|
||
<td>唯一入口,调用 Phase 1 服务/请求,管理所有采集器、静态快照、Case marker 与清理。</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="path">config.env</td><td>40</td>
|
||
<td>定义 Phase 1 相对路径、诊断 Case、采样间隔、采集命令和结果路径。</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="path">hardware_contention_attribution.py</td><td>574</td>
|
||
<td>记录 Manifest/marker,汇总 GPU 与 RDMA,按 Case 时间窗切片并生成报告。</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="path">tests/test_hardware_contention_attribution.py</td><td>197</td>
|
||
<td>验证 GPU/RDMA 汇总、时间窗切片和结果生成的纯 Python 逻辑。</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>3.1 Phase 2 与 Phase 1 的文件关系</h3>
|
||
<pre><code>用户
|
||
└─ bash Phase2/run_hardware_contention_attribution.sh all
|
||
├─ source Phase2/config.env
|
||
│ └─ PHASE1_ENTRY 指向 Phase1/run_quick_map.sh
|
||
├─ env ... bash "${PHASE1_ENTRY}" start/fixed/mixed/stop
|
||
│ └─ Phase1/run_quick_map.sh
|
||
│ ├─ source Phase1/config.env
|
||
│ ├─ 读取 Phase1/quick_map_scenarios.tsv
|
||
│ └─ 调用 Phase1/quick_map_results.py
|
||
├─ 自己启动两节点硬件采集器
|
||
└─ 调用 Phase2/hardware_contention_attribution.py
|
||
├─ 读取 Phase1 生成的 bench/*/cases/*/meta.json
|
||
├─ 读取 Phase2 生成的 GPU/RDMA 时间序列
|
||
└─ 按 Case 时间窗生成归因汇总</code></pre>
|
||
<table>
|
||
<thead><tr><th>上游</th><th>下游</th><th>代码连接点</th><th>关系</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Phase 2 <code>config.env</code></td><td>Phase 2 Shell</td>
|
||
<td><code>run_hardware_contention_attribution.sh:L8</code></td><td>提供诊断 Case、采样策略和 Phase 1 相对入口。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Phase 2 Shell</td><td>Phase 1 Shell</td>
|
||
<td><code>run_hardware_contention_attribution.sh:L195-L213</code></td><td>通过环境变量和 action 委托服务与请求。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Phase 1 <code>config.env</code></td><td>Phase 1 Shell</td>
|
||
<td><code>run_quick_map.sh:L8</code></td><td>提供实际模型服务参数,包括 <code>MEM_FRACTION_STATIC</code>。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Phase 1 Case 结果</td><td>Phase 2 Python</td>
|
||
<td><code>hardware_contention_attribution.py:L213-L260</code></td><td>提供 benchmark 指标和精确开始/结束时间。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Phase 2 Shell 采集器</td><td>Phase 2 Python</td>
|
||
<td><code>hardware_contention_attribution.py:L276-L321</code></td><td>提供两节点 GPU/RDMA 时间序列供 Case 切片。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Phase 2 单元测试</td><td>Phase 2 Python</td>
|
||
<td><code>tests/test_hardware_contention_attribution.py</code></td><td>用合成监控数据验证 delta、速率和时间窗。</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p>
|
||
这里有两套 <code>config.env</code>,职责不同。Phase 2 的配置控制“测哪些 Case、如何监控”;
|
||
Phase 1 的配置控制“模型如何部署、请求如何生成”。Phase 2 没有复制
|
||
<code>MEM_FRACTION_STATIC</code>,因此它最终仍从 Phase 1 的
|
||
<code>config.env:L38</code> 取得默认值。
|
||
</p>
|
||
|
||
<h3>3.1 config.env 分区</h3>
|
||
<table>
|
||
<thead><tr><th>范围</th><th>内容</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>config.env:L3-L8</code></td><td>实验名与 Phase 1 入口</td><td>通过相对路径复用 Phase 1,不依赖启动命令当前目录。</td></tr>
|
||
<tr><td><code>config.env:L10-L16</code></td><td>节点、端口、容器名</td><td>用于健康检查、PID 映射和容器级监控。</td></tr>
|
||
<tr><td><code>config.env:L18-L20</code></td><td>诊断 Case</td><td>五个固定 Case,加一个混合 A/B 开关。</td></tr>
|
||
<tr><td><code>config.env:L22-L32</code></td><td>采集策略</td><td>1 秒采样、空闲基线、冷却、DCGM 字段、perf 事件、RDMA HCA 和时钟容差。</td></tr>
|
||
<tr><td><code>config.env:L34-L40</code></td><td>路径与运行模式</td><td>Repo/Result/Runtime、Dry-run 和是否容忍部分采集器失败。</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h2 id="reuse">4. Phase 1 复用边界</h2>
|
||
<h3>4.1 委托函数</h3>
|
||
<p>
|
||
<code>run_phase1_action</code> 位于
|
||
<code>run_hardware_contention_attribution.sh:L195-L214</code>。它向 Phase 1 注入:
|
||
</p>
|
||
<ul>
|
||
<li>相同 <code>RUN_ID</code> 体系下的独立 benchmark 子目录。</li>
|
||
<li>Phase 2 自己的 <code>service/</code> 证据目录。</li>
|
||
<li>指定 <code>CASE_IDS</code>,使 Phase 1 只跑诊断 Case。</li>
|
||
<li><code>CASE_COOLDOWN_S=0</code>,由 Phase 2 统一控制 Case 间隔。</li>
|
||
<li><code>DRY_RUN</code> 原样传递,确保 Dry-run 不会偷偷启动模型。</li>
|
||
</ul>
|
||
<pre><code>run_phase1_action start
|
||
run_phase1_action fixed # 通过 CASE_IDS 只跑一个 Case
|
||
run_phase1_action mixed
|
||
run_phase1_action stop</code></pre>
|
||
|
||
<h3>4.2 用户只运行哪个入口</h3>
|
||
<p>
|
||
正式执行只运行 Phase 2:
|
||
<code>bash run_hardware_contention_attribution.sh all</code>。
|
||
<code>start_service</code> 和 <code>stop_service</code>
|
||
位于 <code>L215-L229</code>,是编排器内部调用,不需要手工先执行 Phase 1。
|
||
</p>
|
||
<div class="warning">
|
||
<strong>设计不变量:</strong>Phase 2 不修改 Phase 1 的服务参数和请求口径。
|
||
如果模型启动或请求生成需要修复,应改 Phase 1;如果采集、时间对齐或归因需要修复,应改 Phase 2。
|
||
</div>
|
||
|
||
<h3>4.3 Phase 2 中如何查服务参数的实际值</h3>
|
||
<p>
|
||
例如 <code>MEM_FRACTION_STATIC</code> 的完整传递链是:
|
||
</p>
|
||
<pre><code>调用命令环境(可选覆盖)
|
||
→ Phase1/config.env:L38,默认 0.9
|
||
→ Phase1/run_quick_map.sh:L258
|
||
→ SGLang --mem-fraction-static 0.9
|
||
→ Phase2/results/<RUN_ID>/service/head_server_cmd.txt
|
||
→ Phase2/results/<RUN_ID>/bench/<sub-run>/run_manifest.json</code></pre>
|
||
<p>
|
||
Phase 2 的顶层 <code>manifest.json</code> 记录监控配置,不重复记录全部服务参数。
|
||
查“模型实际怎么起的”应看 <code>service/head_server_cmd.txt</code> 与
|
||
<code>service/worker_server_cmd.txt</code>;查结构化值应看任一 Phase 1 子 Run 的
|
||
<code>run_manifest.json</code>。
|
||
</p>
|
||
<pre><code># 查看 Phase 1 默认值
|
||
grep '^MEM_FRACTION_STATIC=' \
|
||
../dsv4pro_pro6000d_2node_sglang_tp16_quick_map/config.env
|
||
|
||
# 查看某次 Phase 2 Run 的实际服务命令
|
||
grep -- '--mem-fraction-static' \
|
||
results/<RUN_ID>/service/head_server_cmd.txt
|
||
|
||
# 从 Phase 1 子 Run Manifest 读取结构化值
|
||
python3 -c 'import glob,json; p=glob.glob(
|
||
"results/<RUN_ID>/bench/*/run_manifest.json")[0];
|
||
print(json.load(open(p))["mem_fraction_static"])'</code></pre>
|
||
<p>
|
||
如果这样启动:
|
||
<code>MEM_FRACTION_STATIC=0.85 bash run_hardware_contention_attribution.sh all</code>,
|
||
外部变量会由 Phase 2 进程继承给 Phase 1,Phase 1 的
|
||
<code>${MEM_FRACTION_STATIC:-0.9}</code> 会保留 <code>0.85</code>。
|
||
因此只看默认配置不足以证明某次实验用了什么,必须查看 Run 证据。
|
||
</p>
|
||
|
||
<h2 id="preflight">5. 预检、Manifest 与时钟</h2>
|
||
<h3>5.1 配置和工具预检</h3>
|
||
<p>
|
||
<code>validate_config</code> 在
|
||
<code>run_hardware_contention_attribution.sh:L66-L106</code>
|
||
检查 Phase 1 入口、节点、Case 和关键数值。
|
||
<code>preflight_node_tools</code> 在 <code>L107-L134</code>
|
||
对两节点检查 Docker、NVIDIA、RDMA、sysstat、perf 与 NUMA 工具。
|
||
</p>
|
||
<p>
|
||
采集器可用性不能在运行半小时后才发现。预检默认 fail-closed;
|
||
只有显式允许部分采集器缺失时,才降级继续。
|
||
</p>
|
||
|
||
<h3>5.2 为什么检查两机时钟</h3>
|
||
<p>
|
||
<code>preflight_clock_sync</code> 位于 <code>L135-L151</code>。
|
||
Phase 2 用 wall clock 把 benchmark 的 <code>started_at/ended_at</code>
|
||
与两节点采样行对齐。如果两机时钟偏差超过配置容差,同一 Case 在 Worker 上会切到错误窗口。
|
||
</p>
|
||
|
||
<h3>5.3 运行元数据</h3>
|
||
<p>
|
||
Shell 的 <code>write_manifest</code> 位于 <code>L152-L176</code>,
|
||
Python 的 <code>create_manifest</code> 位于
|
||
<code>hardware_contention_attribution.py:L367-L390</code>。
|
||
Manifest 记录 Git commit、是否 dirty、Phase 1 入口、节点、Case、采样周期、时钟容差和 Dry-run。
|
||
</p>
|
||
<p>
|
||
<code>mark_event</code> 位于 Shell <code>L177-L194</code>,
|
||
Python 的 <code>append_marker</code> 位于 <code>L338-L365</code>。
|
||
每个 idle/case/cooldown 边界写入纳秒级 wall time,作为人工审计时间线。
|
||
</p>
|
||
|
||
<h2 id="collectors">6. 采集器实现</h2>
|
||
<h3>6.1 静态快照</h3>
|
||
<p>
|
||
<code>capture_command</code>、<code>static_snapshot_command</code> 和
|
||
<code>capture_static_snapshots</code> 位于 Shell <code>L230-L283</code>。
|
||
服务启动后和实验结束前分别采集:
|
||
</p>
|
||
<ul>
|
||
<li><code>nvidia-smi</code> 与 GPU topology。</li>
|
||
<li><code>lscpu</code>、<code>numactl --hardware</code>、<code>numastat</code>。</li>
|
||
<li><code>ip</code>、<code>ethtool</code> 的 <code>eth0/eth3</code> 状态与计数器。</li>
|
||
<li><code>ibdev2netdev</code>、<code>ibstat</code>、<code>rdma</code> 设备信息。</li>
|
||
<li>容器列表与 inspect。</li>
|
||
</ul>
|
||
<p>
|
||
前后快照回答“实验是否改变了设备状态”;时间序列回答“Case 运行期间发生了什么”。
|
||
</p>
|
||
|
||
<h3>6.2 通用采集器包装</h3>
|
||
<p>
|
||
<code>start_stream_collector</code> 位于 Shell
|
||
<code>L284-L318</code>。每个采集器都具备:
|
||
</p>
|
||
<ol>
|
||
<li>保存完整命令到 <code>collector_commands/</code>。</li>
|
||
<li>用唯一 tag 标记远端进程,便于精准停止。</li>
|
||
<li>受 <code>COLLECTOR_TIMEOUT_S</code> 限制,避免永久悬挂。</li>
|
||
<li>保存 PID、日志与退出状态到 <code>collector_status.csv</code>。</li>
|
||
</ol>
|
||
|
||
<h3>6.3 GPU 采样</h3>
|
||
<p>
|
||
<code>gpu_sampler_command</code> 位于 Shell <code>L319-L332</code>,
|
||
每秒调用 <code>nvidia-smi --query-gpu</code>,采集利用率、显存利用率、显存占用、
|
||
功耗、温度、SM 时钟和显存时钟,并补上 <code>wall_time_ns,node,gpu</code>。
|
||
</p>
|
||
<p>
|
||
Python 的 <code>summarize_gpu_rows</code> 位于
|
||
<code>hardware_contention_attribution.py:L101-L135</code>。
|
||
它按 node + GPU 分组,对每个字段输出 mean、P95、max。
|
||
</p>
|
||
|
||
<h3>6.4 RDMA 采样</h3>
|
||
<p>
|
||
Shell 的 <code>rdma_sampler_command</code> 与 <code>read_counter</code>
|
||
位于 <code>L333-L359</code>,读取 <code>mlx5_0/mlx5_3</code> 的端口发送/接收数据、
|
||
包、错误、丢弃与恢复计数器。
|
||
</p>
|
||
<p>
|
||
Python 的 <code>summarize_rdma_rows</code> 位于 <code>L148-L206</code>。
|
||
它按 node + HCA 排序,使用最后值减第一值,并注意 IB
|
||
<code>port_xmit_data/port_rcv_data</code> 的单位是 4 octets:
|
||
</p>
|
||
<pre><code>xmit_bytes = (last_xmit_data - first_xmit_data) × 4
|
||
xmit_gbps = xmit_bytes × 8 / duration_s / 1e9</code></pre>
|
||
<p>
|
||
同时保留错误计数器 delta,因此“带宽低”可以与“链路错误增加”分开判断。
|
||
</p>
|
||
|
||
<h3>6.5 CPU、进程和 NUMA</h3>
|
||
<p>
|
||
<code>start_node_collectors</code> 位于 Shell <code>L405-L457</code>,
|
||
每个节点启动九类采集器:
|
||
</p>
|
||
<table>
|
||
<thead><tr><th>采集器</th><th>回答的问题</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>nvidia-smi</code> / DCGM</td><td>GPU 是否算力、显存带宽、功耗或时钟受限。</td></tr>
|
||
<tr><td><code>mpstat</code></td><td>CPU 总体与逐核是否繁忙。</td></tr>
|
||
<tr><td><code>pidstat</code></td><td>SGLang/容器进程的 CPU、内存和上下文切换。</td></tr>
|
||
<tr><td><code>sar -n DEV,EDEV</code></td><td><code>eth0/eth3</code> 吞吐和错误。</td></tr>
|
||
<tr><td><code>perf stat</code></td><td>容器主进程的 CPU cycles、instructions、cache miss 等。</td></tr>
|
||
<tr><td><code>docker top</code></td><td>容器 PID 与宿主机 PID 映射。</td></tr>
|
||
<tr><td><code>numastat</code></td><td>进程内存是否跨 NUMA 节点访问。</td></tr>
|
||
<tr><td>RDMA counters</td><td>两条 HCA 的真实数据量和错误增量。</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p>
|
||
<code>container_pid_preamble</code> 与 <code>numastat_command</code>
|
||
位于 <code>L372-L404</code>,先解析容器主 PID,再让 perf/numastat 对准实际服务进程。
|
||
</p>
|
||
|
||
<h3>6.6 停止与残留清理</h3>
|
||
<p>
|
||
<code>check_collectors</code> 和 <code>stop_collectors</code>
|
||
位于 Shell <code>L469-L519</code>。
|
||
除了等待已知 PID,还会按唯一 tag 清理远端残留采集器。
|
||
<code>cleanup</code> 在 <code>L594-L602</code> 由 trap 调用,先停监控再停模型服务。
|
||
</p>
|
||
|
||
<h2 id="cases">7. Case 编排</h2>
|
||
<h3>7.1 固定 Case</h3>
|
||
<p>
|
||
<code>run_fixed_case</code> 位于 Shell <code>L529-L556</code>。
|
||
它为 Case 生成独立子 Run ID,写 <code>case_start</code> marker,
|
||
委托 Phase 1 的 <code>fixed</code>,再检查该子 Run 的 <code>summary.csv</code>,
|
||
最后写 <code>case_end</code> marker。
|
||
</p>
|
||
<p>
|
||
默认固定集合来自 <code>config.env:L18</code>,用于区分:
|
||
单请求长 Prefill、并发 Prefill、普通 Decode、持续长 Decode、长上下文 Decode。
|
||
Phase 2 不把所有 Phase 1 点再跑一遍,只保留对资源归因有辨识度的负载。
|
||
</p>
|
||
|
||
<h3>7.2 混合 Case</h3>
|
||
<p>
|
||
<code>run_mixed_case</code> 位于 Shell <code>L557-L582</code>。
|
||
它复用 Phase 1 已经保证真实时间重叠的 mixed A/B,并把整段监控留在同一采集窗口内。
|
||
Phase 2 自己不重新实现后台请求与注入逻辑。
|
||
</p>
|
||
|
||
<h3>7.3 run_all 的失败策略</h3>
|
||
<p>
|
||
<code>run_all</code> 位于 Shell <code>L604-L668</code>:
|
||
</p>
|
||
<ul>
|
||
<li>服务健康时,即使一个 Case 失败,也继续后续诊断 Case,并累计 failures。</li>
|
||
<li>每个固定 Case 后检查 <code>/health</code>;服务失活则中止剩余 Case。</li>
|
||
<li>服务已失活时跳过 mixed,避免无意义错误。</li>
|
||
<li>不论成功失败,最终尝试 cooldown、停止采集、后快照、停止服务和汇总。</li>
|
||
<li>Run 状态区分 <code>COMPLETED</code>、<code>COMPLETED_WITH_FAILURES</code>、<code>DRY_RUN</code>。</li>
|
||
</ul>
|
||
|
||
<h2 id="alignment">8. 按真实 Case 时间窗归因</h2>
|
||
<h3>8.1 时间窗从哪里来</h3>
|
||
<p>
|
||
<code>load_case_windows</code> 位于
|
||
<code>hardware_contention_attribution.py:L233-L260</code>。
|
||
它读取 Phase 1 每个 <code>meta.json</code> 中的
|
||
<code>started_at</code> 与 <code>ended_at</code>,转换为纳秒时间戳。
|
||
这比用“Case marker 前后大概几秒”更精确,因为它对齐的是 benchmark 进程实际测量区间。
|
||
</p>
|
||
|
||
<h3>8.2 如何切 GPU/RDMA 数据</h3>
|
||
<p>
|
||
<code>rows_in_window</code> 位于 <code>L263-L274</code>,
|
||
只保留 <code>started_ns ≤ wall_time_ns ≤ ended_ns</code> 的采样行。
|
||
<code>summarize_case_hardware</code> 位于 <code>L276-L319</code>,
|
||
对每个 Case、每个节点分别切 GPU 与 RDMA,再复用全局汇总函数。
|
||
</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>
|
||
<pre><code>results/<RUN_ID>/
|
||
manifest.json
|
||
markers.csv
|
||
collector_status.csv
|
||
service/
|
||
commands/
|
||
bench/<phase1-sub-run>/
|
||
head/
|
||
gpu_samples.csv
|
||
rdma.csv
|
||
dcgm.log
|
||
mpstat.log
|
||
pidstat.log
|
||
sar_network.log
|
||
perf.log
|
||
docker_top.log
|
||
numastat.log
|
||
collector_commands/
|
||
worker/
|
||
...同上...
|
||
gpu_summary.csv
|
||
rdma_summary.csv
|
||
bench_summary.csv
|
||
case_windows.csv
|
||
case_gpu_summary.csv
|
||
case_rdma_summary.csv
|
||
summary.json
|
||
report.md</code></pre>
|
||
<p>
|
||
Python 的 <code>summarize</code> 位于
|
||
<code>hardware_contention_attribution.py:L405-L504</code>。
|
||
它汇总全局 GPU/RDMA、Phase 1 bench、Case 时间窗和 Case 级硬件数据,
|
||
同时统计采集器状态与文件大小。
|
||
</p>
|
||
<p>
|
||
自动报告只整理证据,不自动宣布“瓶颈就是 GPU/NCCL/CPU”。
|
||
<code>L498-L499</code> 明确要求最终结论结合 markers、原始 DCGM/sysstat 和 SGLang 日志。
|
||
这是有意的保守边界,避免单个指标被机械误判。
|
||
</p>
|
||
|
||
<h2 id="index">10. 函数行号索引</h2>
|
||
<h3>10.1 run_hardware_contention_attribution.sh</h3>
|
||
<table>
|
||
<thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>L25-L65</td><td>日志、远端执行、命令文件、结果日志</td><td>编排器基础设施。</td></tr>
|
||
<tr><td>L66-L151</td><td>配置、工具、时钟预检</td><td>正式启动前 fail-fast。</td></tr>
|
||
<tr><td>L152-L194</td><td>Manifest 与 marker</td><td>记录运行身份和事件边界。</td></tr>
|
||
<tr><td>L195-L229</td><td>Phase 1 委托与服务生命周期</td><td>复用服务和 benchmark,不复制实现。</td></tr>
|
||
<tr><td>L230-L283</td><td>静态快照</td><td>保存实验前后硬件、网络、RDMA 和容器状态。</td></tr>
|
||
<tr><td>L284-L318</td><td><code>start_stream_collector</code></td><td>统一包装远端长时间采集器。</td></tr>
|
||
<tr><td>L319-L404</td><td>GPU、RDMA、PID、NUMA 命令</td><td>生成各类采集命令。</td></tr>
|
||
<tr><td>L405-L468</td><td>启动所有采集器</td><td>两节点各启动九类监控。</td></tr>
|
||
<tr><td>L469-L519</td><td>检查与停止采集器</td><td>收集状态并清理残留。</td></tr>
|
||
<tr><td>L520-L582</td><td>sleep、固定 Case、混合 Case</td><td>诊断负载编排。</td></tr>
|
||
<tr><td>L583-L603</td><td>汇总、Manifest 完成、cleanup</td><td>结果收口与异常清理。</td></tr>
|
||
<tr><td>L604-L668</td><td><code>run_all</code></td><td>Phase 2 完整状态机。</td></tr>
|
||
<tr><td>L669-L689</td><td><code>main</code></td><td>分发 <code>all/summarize/stop</code>。</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>10.2 hardware_contention_attribution.py</h3>
|
||
<table>
|
||
<thead><tr><th>行号</th><th>函数组</th><th>职责</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>L54-L100</td><td>时间、JSON、数字、CSV</td><td>结果工具基础函数。</td></tr>
|
||
<tr><td>L101-L141</td><td>GPU 汇总</td><td>按 node/GPU 输出 mean、P95、max。</td></tr>
|
||
<tr><td>L142-L212</td><td>RDMA 汇总</td><td>计数器 delta、字节换算、Gbps 和错误增量。</td></tr>
|
||
<tr><td>L213-L260</td><td>Bench 与 Case 时间窗读取</td><td>从 Phase 1 子 Run 建立诊断索引。</td></tr>
|
||
<tr><td>L263-L321</td><td>时间切片</td><td>按每个 Case 的真实运行窗口汇总 GPU/RDMA。</td></tr>
|
||
<tr><td>L322-L366</td><td>CSV 与 marker</td><td>输出结构化表和事件时间线。</td></tr>
|
||
<tr><td>L367-L404</td><td>Manifest 与 bench 校验</td><td>维护 Run 状态,确认子 Run 全部完成。</td></tr>
|
||
<tr><td>L405-L506</td><td><code>summarize</code></td><td>生成全部汇总表、summary JSON 和 report。</td></tr>
|
||
<tr><td>L507-L574</td><td>CLI</td><td>向 Shell 提供 marker/manifest/check/summarize 子命令。</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<footer>
|
||
本文只描述提交 <code>ca1f2f63375c</code> 的实现。后续增加 Nsight、SGLang profiler
|
||
或新的硬件计数器时,应先明确它属于 Phase 2 采集层还是后续深度剖析层,再更新本档案。
|
||
</footer>
|
||
</main>
|
||
</body>
|
||
</html>
|