Standalone Code Walkthrough / Phase 2

DSV4-Pro 双机 Pro6000D SGLang 硬件竞争归因:代码详解

行号基线:ca1f2f63375c  生成时间:2026-07-31 13:04:21 CST  入口:run_hardware_contention_attribution.sh
文档边界:这是一份独立代码档案。它解释 Phase 2 如何复用 Phase 1 请求、 同步采集两节点监控,并按真实 Case 时间窗做硬件归因。所有行号绑定提交 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.sh689 唯一入口,调用 Phase 1 服务/请求,管理所有采集器、静态快照、Case marker 与清理。
config.env40 定义 Phase 1 相对路径、诊断 Case、采样间隔、采集命令和结果路径。
hardware_contention_attribution.py574 记录 Manifest/marker,汇总 GPU 与 RDMA,按 Case 时间窗切片并生成报告。
tests/test_hardware_contention_attribution.py197 验证 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.envPhase 2 Shell run_hardware_contention_attribution.sh:L8提供诊断 Case、采样策略和 Phase 1 相对入口。
Phase 2 ShellPhase 1 Shell run_hardware_contention_attribution.sh:L195-L213通过环境变量和 action 委托服务与请求。
Phase 1 config.envPhase 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_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 allstart_servicestop_service 位于 L215-L229,是编排器内部调用,不需要手工先执行 Phase 1。

设计不变量:Phase 2 不修改 Phase 1 的服务参数和请求口径。 如果模型启动或请求生成需要修复,应改 Phase 1;如果采集、时间对齐或归因需要修复,应改 Phase 2。

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.txtservice/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_configrun_hardware_contention_attribution.sh:L66-L106 检查 Phase 1 入口、节点、Case 和关键数值。 preflight_node_toolsL107-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_commandstatic_snapshot_commandcapture_static_snapshots 位于 Shell L230-L283。 服务启动后和实验结束前分别采集:

前后快照回答“实验是否改变了设备状态”;时间序列回答“Case 运行期间发生了什么”。

6.2 通用采集器包装

start_stream_collector 位于 Shell L284-L318。每个采集器都具备:

  1. 保存完整命令到 collector_commands/
  2. 用唯一 tag 标记远端进程,便于精准停止。
  3. COLLECTOR_TIMEOUT_S 限制,避免永久悬挂。
  4. 保存 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_commandread_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 / DCGMGPU 是否算力、显存带宽、功耗或时钟受限。
mpstatCPU 总体与逐核是否繁忙。
pidstatSGLang/容器进程的 CPU、内存和上下文切换。
sar -n DEV,EDEVeth0/eth3 吞吐和错误。
perf stat容器主进程的 CPU cycles、instructions、cache miss 等。
docker top容器 PID 与宿主机 PID 映射。
numastat进程内存是否跨 NUMA 节点访问。
RDMA counters两条 HCA 的真实数据量和错误增量。

container_pid_preamblenumastat_command 位于 L372-L404,先解析容器主 PID,再让 perf/numastat 对准实际服务进程。

6.6 停止与残留清理

check_collectorsstop_collectors 位于 Shell L469-L519。 除了等待已知 PID,还会按唯一 tag 清理远端残留采集器。 cleanupL594-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

8. 按真实 Case 时间窗归因

8.1 时间窗从哪里来

load_case_windows 位于 hardware_contention_attribution.py:L233-L260。 它读取 Phase 1 每个 meta.json 中的 started_atended_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
1 秒采样意味着很短的请求可能只有少数样本。此时 P95 不稳定,应结合原始时间序列和 Case 持续时间, 不能把单个采样峰值解释为稳定瓶颈。

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-L194Manifest 与 marker记录运行身份和事件边界。
L195-L229Phase 1 委托与服务生命周期复用服务和 benchmark,不复制实现。
L230-L283静态快照保存实验前后硬件、网络、RDMA 和容器状态。
L284-L318start_stream_collector统一包装远端长时间采集器。
L319-L404GPU、RDMA、PID、NUMA 命令生成各类采集命令。
L405-L468启动所有采集器两节点各启动九类监控。
L469-L519检查与停止采集器收集状态并清理残留。
L520-L582sleep、固定 Case、混合 Case诊断负载编排。
L583-L603汇总、Manifest 完成、cleanup结果收口与异常清理。
L604-L668run_allPhase 2 完整状态机。
L669-L689main分发 all/summarize/stop

10.2 hardware_contention_attribution.py

行号函数组职责
L54-L100时间、JSON、数字、CSV结果工具基础函数。
L101-L141GPU 汇总按 node/GPU 输出 mean、P95、max。
L142-L212RDMA 汇总计数器 delta、字节换算、Gbps 和错误增量。
L213-L260Bench 与 Case 时间窗读取从 Phase 1 子 Run 建立诊断索引。
L263-L321时间切片按每个 Case 的真实运行窗口汇总 GPU/RDMA。
L322-L366CSV 与 marker输出结构化表和事件时间线。
L367-L404Manifest 与 bench 校验维护 Run 状态,确认子 Run 全部完成。
L405-L506summarize生成全部汇总表、summary JSON 和 report。
L507-L574CLI向 Shell 提供 marker/manifest/check/summarize 子命令。