ca1f2f63375c。
1. 阅读导航
2. 总体控制流
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
Phase 2 不复制模型部署和 benchmark 生成代码。它新增的是诊断编排层: 在同一个服务生命周期中,让两节点的 GPU、CPU、NUMA、网络和 RDMA 时间序列包围 benchmark, 最后按 Case 时间切片。
3. 文件职责
| 文件 | 行数 | 职责 |
|---|---|---|
| run_hardware_contention_attribution.sh | 689 | 唯一入口,调用 Phase 1 服务/请求,管理所有采集器、静态快照、Case marker 与清理。 |
| config.env | 40 | 定义 Phase 1 相对路径、诊断 Case、采样间隔、采集命令和结果路径。 |
| hardware_contention_attribution.py | 574 | 记录 Manifest/marker,汇总 GPU 与 RDMA,按 Case 时间窗切片并生成报告。 |
| tests/test_hardware_contention_attribution.py | 197 | 验证 GPU/RDMA 汇总、时间窗切片和结果生成的纯 Python 逻辑。 |
3.1 Phase 2 与 Phase 1 的文件关系
用户
└─ 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 时间窗生成归因汇总
| 上游 | 下游 | 代码连接点 | 关系 |
|---|---|---|---|
Phase 2 config.env | Phase 2 Shell | run_hardware_contention_attribution.sh:L8 | 提供诊断 Case、采样策略和 Phase 1 相对入口。 |
| Phase 2 Shell | Phase 1 Shell | run_hardware_contention_attribution.sh:L195-L213 | 通过环境变量和 action 委托服务与请求。 |
Phase 1 config.env | Phase 1 Shell | run_quick_map.sh:L8 | 提供实际模型服务参数,包括 MEM_FRACTION_STATIC。 |
| Phase 1 Case 结果 | Phase 2 Python | hardware_contention_attribution.py:L213-L260 | 提供 benchmark 指标和精确开始/结束时间。 |
| Phase 2 Shell 采集器 | Phase 2 Python | hardware_contention_attribution.py:L276-L321 | 提供两节点 GPU/RDMA 时间序列供 Case 切片。 |
| Phase 2 单元测试 | Phase 2 Python | tests/test_hardware_contention_attribution.py | 用合成监控数据验证 delta、速率和时间窗。 |
这里有两套 config.env,职责不同。Phase 2 的配置控制“测哪些 Case、如何监控”;
Phase 1 的配置控制“模型如何部署、请求如何生成”。Phase 2 没有复制
MEM_FRACTION_STATIC,因此它最终仍从 Phase 1 的
config.env:L38 取得默认值。
3.1 config.env 分区
| 范围 | 内容 | 说明 |
|---|---|---|
config.env:L3-L8 | 实验名与 Phase 1 入口 | 通过相对路径复用 Phase 1,不依赖启动命令当前目录。 |
config.env:L10-L16 | 节点、端口、容器名 | 用于健康检查、PID 映射和容器级监控。 |
config.env:L18-L20 | 诊断 Case | 五个固定 Case,加一个混合 A/B 开关。 |
config.env:L22-L32 | 采集策略 | 1 秒采样、空闲基线、冷却、DCGM 字段、perf 事件、RDMA HCA 和时钟容差。 |
config.env:L34-L40 | 路径与运行模式 | Repo/Result/Runtime、Dry-run 和是否容忍部分采集器失败。 |
4. Phase 1 复用边界
4.1 委托函数
run_phase1_action 位于
run_hardware_contention_attribution.sh:L195-L214。它向 Phase 1 注入:
- 相同
RUN_ID体系下的独立 benchmark 子目录。 - Phase 2 自己的
service/证据目录。 - 指定
CASE_IDS,使 Phase 1 只跑诊断 Case。 CASE_COOLDOWN_S=0,由 Phase 2 统一控制 Case 间隔。DRY_RUN原样传递,确保 Dry-run 不会偷偷启动模型。
run_phase1_action start
run_phase1_action fixed # 通过 CASE_IDS 只跑一个 Case
run_phase1_action mixed
run_phase1_action stop
4.2 用户只运行哪个入口
正式执行只运行 Phase 2:
bash run_hardware_contention_attribution.sh all。
start_service 和 stop_service
位于 L215-L229,是编排器内部调用,不需要手工先执行 Phase 1。
4.3 Phase 2 中如何查服务参数的实际值
例如 MEM_FRACTION_STATIC 的完整传递链是:
调用命令环境(可选覆盖)
→ 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
Phase 2 的顶层 manifest.json 记录监控配置,不重复记录全部服务参数。
查“模型实际怎么起的”应看 service/head_server_cmd.txt 与
service/worker_server_cmd.txt;查结构化值应看任一 Phase 1 子 Run 的
run_manifest.json。
# 查看 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"])'
如果这样启动:
MEM_FRACTION_STATIC=0.85 bash run_hardware_contention_attribution.sh all,
外部变量会由 Phase 2 进程继承给 Phase 1,Phase 1 的
${MEM_FRACTION_STATIC:-0.9} 会保留 0.85。
因此只看默认配置不足以证明某次实验用了什么,必须查看 Run 证据。
5. 预检、Manifest 与时钟
5.1 配置和工具预检
validate_config 在
run_hardware_contention_attribution.sh:L66-L106
检查 Phase 1 入口、节点、Case 和关键数值。
preflight_node_tools 在 L107-L134
对两节点检查 Docker、NVIDIA、RDMA、sysstat、perf 与 NUMA 工具。
采集器可用性不能在运行半小时后才发现。预检默认 fail-closed; 只有显式允许部分采集器缺失时,才降级继续。
5.2 为什么检查两机时钟
preflight_clock_sync 位于 L135-L151。
Phase 2 用 wall clock 把 benchmark 的 started_at/ended_at
与两节点采样行对齐。如果两机时钟偏差超过配置容差,同一 Case 在 Worker 上会切到错误窗口。
5.3 运行元数据
Shell 的 write_manifest 位于 L152-L176,
Python 的 create_manifest 位于
hardware_contention_attribution.py:L367-L390。
Manifest 记录 Git commit、是否 dirty、Phase 1 入口、节点、Case、采样周期、时钟容差和 Dry-run。
mark_event 位于 Shell L177-L194,
Python 的 append_marker 位于 L338-L365。
每个 idle/case/cooldown 边界写入纳秒级 wall time,作为人工审计时间线。
6. 采集器实现
6.1 静态快照
capture_command、static_snapshot_command 和
capture_static_snapshots 位于 Shell L230-L283。
服务启动后和实验结束前分别采集:
nvidia-smi与 GPU topology。lscpu、numactl --hardware、numastat。ip、ethtool的eth0/eth3状态与计数器。ibdev2netdev、ibstat、rdma设备信息。- 容器列表与 inspect。
前后快照回答“实验是否改变了设备状态”;时间序列回答“Case 运行期间发生了什么”。
6.2 通用采集器包装
start_stream_collector 位于 Shell
L284-L318。每个采集器都具备:
- 保存完整命令到
collector_commands/。 - 用唯一 tag 标记远端进程,便于精准停止。
- 受
COLLECTOR_TIMEOUT_S限制,避免永久悬挂。 - 保存 PID、日志与退出状态到
collector_status.csv。
6.3 GPU 采样
gpu_sampler_command 位于 Shell L319-L332,
每秒调用 nvidia-smi --query-gpu,采集利用率、显存利用率、显存占用、
功耗、温度、SM 时钟和显存时钟,并补上 wall_time_ns,node,gpu。
Python 的 summarize_gpu_rows 位于
hardware_contention_attribution.py:L101-L135。
它按 node + GPU 分组,对每个字段输出 mean、P95、max。
6.4 RDMA 采样
Shell 的 rdma_sampler_command 与 read_counter
位于 L333-L359,读取 mlx5_0/mlx5_3 的端口发送/接收数据、
包、错误、丢弃与恢复计数器。
Python 的 summarize_rdma_rows 位于 L148-L206。
它按 node + HCA 排序,使用最后值减第一值,并注意 IB
port_xmit_data/port_rcv_data 的单位是 4 octets:
xmit_bytes = (last_xmit_data - first_xmit_data) × 4
xmit_gbps = xmit_bytes × 8 / duration_s / 1e9
同时保留错误计数器 delta,因此“带宽低”可以与“链路错误增加”分开判断。
6.5 CPU、进程和 NUMA
start_node_collectors 位于 Shell L405-L457,
每个节点启动九类采集器:
| 采集器 | 回答的问题 |
|---|---|
nvidia-smi / DCGM | GPU 是否算力、显存带宽、功耗或时钟受限。 |
mpstat | CPU 总体与逐核是否繁忙。 |
pidstat | SGLang/容器进程的 CPU、内存和上下文切换。 |
sar -n DEV,EDEV | eth0/eth3 吞吐和错误。 |
perf stat | 容器主进程的 CPU cycles、instructions、cache miss 等。 |
docker top | 容器 PID 与宿主机 PID 映射。 |
numastat | 进程内存是否跨 NUMA 节点访问。 |
| RDMA counters | 两条 HCA 的真实数据量和错误增量。 |
container_pid_preamble 与 numastat_command
位于 L372-L404,先解析容器主 PID,再让 perf/numastat 对准实际服务进程。
6.6 停止与残留清理
check_collectors 和 stop_collectors
位于 Shell L469-L519。
除了等待已知 PID,还会按唯一 tag 清理远端残留采集器。
cleanup 在 L594-L602 由 trap 调用,先停监控再停模型服务。
7. Case 编排
7.1 固定 Case
run_fixed_case 位于 Shell L529-L556。
它为 Case 生成独立子 Run ID,写 case_start marker,
委托 Phase 1 的 fixed,再检查该子 Run 的 summary.csv,
最后写 case_end marker。
默认固定集合来自 config.env:L18,用于区分:
单请求长 Prefill、并发 Prefill、普通 Decode、持续长 Decode、长上下文 Decode。
Phase 2 不把所有 Phase 1 点再跑一遍,只保留对资源归因有辨识度的负载。
7.2 混合 Case
run_mixed_case 位于 Shell L557-L582。
它复用 Phase 1 已经保证真实时间重叠的 mixed A/B,并把整段监控留在同一采集窗口内。
Phase 2 自己不重新实现后台请求与注入逻辑。
7.3 run_all 的失败策略
run_all 位于 Shell L604-L668:
- 服务健康时,即使一个 Case 失败,也继续后续诊断 Case,并累计 failures。
- 每个固定 Case 后检查
/health;服务失活则中止剩余 Case。 - 服务已失活时跳过 mixed,避免无意义错误。
- 不论成功失败,最终尝试 cooldown、停止采集、后快照、停止服务和汇总。
- Run 状态区分
COMPLETED、COMPLETED_WITH_FAILURES、DRY_RUN。
8. 按真实 Case 时间窗归因
8.1 时间窗从哪里来
load_case_windows 位于
hardware_contention_attribution.py:L233-L260。
它读取 Phase 1 每个 meta.json 中的
started_at 与 ended_at,转换为纳秒时间戳。
这比用“Case marker 前后大概几秒”更精确,因为它对齐的是 benchmark 进程实际测量区间。
8.2 如何切 GPU/RDMA 数据
rows_in_window 位于 L263-L274,
只保留 started_ns ≤ wall_time_ns ≤ ended_ns 的采样行。
summarize_case_hardware 位于 L276-L319,
对每个 Case、每个节点分别切 GPU 与 RDMA,再复用全局汇总函数。
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
9. 输出和归因边界
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
Python 的 summarize 位于
hardware_contention_attribution.py:L405-L504。
它汇总全局 GPU/RDMA、Phase 1 bench、Case 时间窗和 Case 级硬件数据,
同时统计采集器状态与文件大小。
自动报告只整理证据,不自动宣布“瓶颈就是 GPU/NCCL/CPU”。
L498-L499 明确要求最终结论结合 markers、原始 DCGM/sysstat 和 SGLang 日志。
这是有意的保守边界,避免单个指标被机械误判。
10. 函数行号索引
10.1 run_hardware_contention_attribution.sh
| 行号 | 函数组 | 职责 |
|---|---|---|
| L25-L65 | 日志、远端执行、命令文件、结果日志 | 编排器基础设施。 |
| L66-L151 | 配置、工具、时钟预检 | 正式启动前 fail-fast。 |
| L152-L194 | Manifest 与 marker | 记录运行身份和事件边界。 |
| L195-L229 | Phase 1 委托与服务生命周期 | 复用服务和 benchmark,不复制实现。 |
| L230-L283 | 静态快照 | 保存实验前后硬件、网络、RDMA 和容器状态。 |
| L284-L318 | start_stream_collector | 统一包装远端长时间采集器。 |
| L319-L404 | GPU、RDMA、PID、NUMA 命令 | 生成各类采集命令。 |
| L405-L468 | 启动所有采集器 | 两节点各启动九类监控。 |
| L469-L519 | 检查与停止采集器 | 收集状态并清理残留。 |
| L520-L582 | sleep、固定 Case、混合 Case | 诊断负载编排。 |
| L583-L603 | 汇总、Manifest 完成、cleanup | 结果收口与异常清理。 |
| L604-L668 | run_all | Phase 2 完整状态机。 |
| L669-L689 | main | 分发 all/summarize/stop。 |
10.2 hardware_contention_attribution.py
| 行号 | 函数组 | 职责 |
|---|---|---|
| L54-L100 | 时间、JSON、数字、CSV | 结果工具基础函数。 |
| L101-L141 | GPU 汇总 | 按 node/GPU 输出 mean、P95、max。 |
| L142-L212 | RDMA 汇总 | 计数器 delta、字节换算、Gbps 和错误增量。 |
| L213-L260 | Bench 与 Case 时间窗读取 | 从 Phase 1 子 Run 建立诊断索引。 |
| L263-L321 | 时间切片 | 按每个 Case 的真实运行窗口汇总 GPU/RDMA。 |
| L322-L366 | CSV 与 marker | 输出结构化表和事件时间线。 |
| L367-L404 | Manifest 与 bench 校验 | 维护 Run 状态,确认子 Run 全部完成。 |
| L405-L506 | summarize | 生成全部汇总表、summary JSON 和 report。 |
| L507-L574 | CLI | 向 Shell 提供 marker/manifest/check/summarize 子命令。 |