[Feat] add Phase 2.5 RDMA demand modeling
This commit is contained in:
parent
119701a417
commit
c5fa700c50
@ -1,5 +1,9 @@
|
|||||||
# sskj — 多平台大模型推理性能基准测试项目
|
# sskj — 多平台大模型推理性能基准测试项目
|
||||||
|
|
||||||
|
> **更新(2026-08-01 02:40:00 CST)**
|
||||||
|
>
|
||||||
|
> 新增 DeepSeek-V4-Pro 双机 Pro6000D SGLang Phase 2.5 RDMA 需求建模唯一入口。实验保持现有 TP16/EP2 服务参数不变,先以 `64K -> 1` 的 C=1/4/16/32/64 建立 Input TPS 与每 Rail HCA 带宽关系,再自动选择平台前、拐点和最大稳定并发,对 `64K -> 1K` 重复确认。结果将给出每 Token 跨机字节数、400G 所需 Token TPS、并发饱和曲线和“模型计算先饱和还是 RDMA 先饱和”的机器可读结论;正式结果尚未生成,因此暂不创建 Phase 2.5 HTML 档案。
|
||||||
|
>
|
||||||
> **更新(2026-07-31 19:02:00 CST)**
|
> **更新(2026-07-31 19:02:00 CST)**
|
||||||
>
|
>
|
||||||
> Phase 3 Nsight smoke 已在 Head/Worker 各生成一份可解析报告;首份时间线确认 NCCL AllReduce、Sparse MLA、CUTLASS GEMM 与 MoE kernel 均可见。根据 smoke 修正正式 capture:Decode 对照和混合负载必须等服务实际进入 `Decode batch` 后才触发;每段等待本节点全部 8 个 rank 完成,而非任一 rank;双节点近同步停止以减少 Gloo 断链噪声。`nsys stats` 改为可写临时 SQLite、保留 Kernel/API/NVTX 高价值汇总并删除临时库,结果与报告数量均 fail-closed。
|
> Phase 3 Nsight smoke 已在 Head/Worker 各生成一份可解析报告;首份时间线确认 NCCL AllReduce、Sparse MLA、CUTLASS GEMM 与 MoE kernel 均可见。根据 smoke 修正正式 capture:Decode 对照和混合负载必须等服务实际进入 `Decode batch` 后才触发;每段等待本节点全部 8 个 rank 完成,而非任一 rank;双节点近同步停止以减少 Gloo 断链噪声。`nsys stats` 改为可写临时 SQLite、保留 Kernel/API/NVTX 高价值汇总并删除临时库,结果与报告数量均 fail-closed。
|
||||||
|
|||||||
@ -400,7 +400,7 @@
|
|||||||
<div class="document-header__meta">
|
<div class="document-header__meta">
|
||||||
<span>节点:174.1.51.5 + 174.1.51.7</span>
|
<span>节点:174.1.51.5 + 174.1.51.7</span>
|
||||||
<span>资源:16 × RTX PRO 6000 Blackwell</span>
|
<span>资源:16 × RTX PRO 6000 Blackwell</span>
|
||||||
<span>版本:2026-07-31 13:40:03 CST</span>
|
<span>版本:2026-08-01 02:45:00 CST</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -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-Pro,16 张 GPU 组成一个完整实例<br>当前约束:模型暂时只能使用全部 16 张 GPU,无法额外复制一套模型进行 PD 分离<br>计划版本:2026-07-31 16:46:05 CST</p>
|
<p>适用环境:<code>174.1.51.5 + 174.1.51.7</code>,每台 8 张 RTX PRO 6000 Blackwell Server Edition<br>当前部署:DeepSeek-V4-Pro,16 张 GPU 组成一个完整实例<br>当前约束:模型暂时只能使用全部 16 张 GPU,无法额外复制一套模型进行 PD 分离<br>计划版本:2026-08-01 02:45:00 CST</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<h2>当前执行状态与阶段档案</h2>
|
<h2>当前执行状态与阶段档案</h2>
|
||||||
<table>
|
<table>
|
||||||
@ -448,6 +448,11 @@
|
|||||||
<a href="./phase2_code.html">打开 Phase 2 代码详解</a>
|
<a href="./phase2_code.html">打开 Phase 2 代码详解</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DeepSeek-V4-Pro / 双机 Pro6000D / SGLang RDMA 需求建模与并发拐点</td>
|
||||||
|
<td>Phase 2.5 代码已实现;等待正式 Run,以 64K Scout 拟合 bytes/token、带宽平台和 400G 所需 Token TPS</td>
|
||||||
|
<td>阶段尚未完成,按门禁暂不创建实验/代码 HTML</td>
|
||||||
|
</tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
<p>
|
<p>
|
||||||
<strong>阶段档案生成门禁:</strong>Phase 尚未产出正式阶段结果、完成汇总和汇报确认前,
|
<strong>阶段档案生成门禁:</strong>Phase 尚未产出正式阶段结果、完成汇总和汇报确认前,
|
||||||
@ -785,6 +790,21 @@ Phase 2 回答“哪个硬件/Host/通信资源在什么 Case 中升高,以及
|
|||||||
Phase 3 只捕获短 TP16 Timeline,回答具体 Kernel、Scheduler gap、Collective、
|
Phase 3 只捕获短 TP16 Timeline,回答具体 Kernel、Scheduler gap、Collective、
|
||||||
Rank 同步及计算/通信重叠,不重复 Phase 2 的长时间轻量采样。
|
Rank 同步及计算/通信重叠,不重复 Phase 2 的长时间轻量采样。
|
||||||
</p>
|
</p>
|
||||||
|
<h2>6.5 Phase 2.5:RDMA 需求建模与并发拐点</h2>
|
||||||
|
<p>
|
||||||
|
Phase 2 已证明当前代表负载最高约 83.5 Gbit/s/rail,但单个固定 Case 不能回答
|
||||||
|
“提高并发后是否还能继续逼近 400G”。Phase 2.5 保持 TP16/EP2 和服务参数不变,
|
||||||
|
先用 <code>64K → 1</code> 的 C=1/4/16/32/64 隔离 Prefill,拟合
|
||||||
|
<code>rail_gbps ≈ input_tps × bytes_per_input_token_per_rail × 8</code>;
|
||||||
|
再自动选择平台前、拐点和最大稳定并发,对 <code>64K → 1K</code> 重复两次确认。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
本阶段必须同时给出每 Rail Mean/P95/Max、双 Rail 单向合计、Input/Output TPS、
|
||||||
|
GPU 利用率、Rail 不均衡和 RDMA 错误增量。最终结论不是只报告“最大 C”,而是判断
|
||||||
|
模型计算吞吐和 RDMA 链路哪个先进入平台,并形成一套换模型、TP/EP 或输入输出形状后
|
||||||
|
可重复使用的带宽需求估算方法。正式结果完成前不创建
|
||||||
|
<code>phase2_5_exp.html</code> 或 <code>phase2_5_code.html</code>。
|
||||||
|
</p>
|
||||||
<h2>7. Phase 3:时间线 Profiling(Nsight Systems 为主)</h2>
|
<h2>7. Phase 3:时间线 Profiling(Nsight Systems 为主)</h2>
|
||||||
<p>
|
<p>
|
||||||
Nsight Systems、PyTorch Profiler 和 NVTX 是三件不同的东西:Nsight Systems
|
Nsight Systems、PyTorch Profiler 和 NVTX 是三件不同的东西:Nsight Systems
|
||||||
|
|||||||
3
experiments/pro6000/dsv4pro_pro6000d_2node_sglang_rdma_demand_modeling/.gitignore
vendored
Normal file
3
experiments/pro6000/dsv4pro_pro6000d_2node_sglang_rdma_demand_modeling/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
results/
|
||||||
|
runtime/
|
||||||
|
*.log
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
# DeepSeek-V4-Pro 双机 SGLang RDMA 需求建模
|
||||||
|
|
||||||
|
Phase 2.5 在不改变 TP16/EP2 服务参数的前提下回答三个问题:
|
||||||
|
|
||||||
|
1. 64K 输入下,哪个并发是 Token TPS 与 RDMA 带宽的拐点;
|
||||||
|
2. `64K -> 1K` 真实请求最多产生多少双 Rail 流量;
|
||||||
|
3. 当前部署会先达到 GPU/模型吞吐平台,还是先打满 400G rail。
|
||||||
|
|
||||||
|
唯一入口:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/hzy/sskj/experiments/pro6000/dsv4pro_pro6000d_2node_sglang_rdma_demand_modeling
|
||||||
|
|
||||||
|
# 只展开命令,不启动服务
|
||||||
|
DRY_RUN=1 RUN_ID=dsv4pro-phase2_5-dryrun-$(date +%Y%m%d-%H%M%S) \
|
||||||
|
bash run_rdma_demand_modeling.sh all
|
||||||
|
|
||||||
|
# 正式运行
|
||||||
|
RUN_ID=dsv4pro-phase2_5-$(date +%Y%m%d-%H%M%S)
|
||||||
|
tmux new-session -d -s dsv4pro-phase2_5 \
|
||||||
|
"RUN_ID=${RUN_ID} bash run_rdma_demand_modeling.sh all \
|
||||||
|
2>&1 | tee /data/hzy/${RUN_ID}.log"
|
||||||
|
tmux attach -t dsv4pro-phase2_5
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本先跑 `64K -> 1` 的 C=1/4/16/32/64 探路点,再自动选择三个代表并发,
|
||||||
|
对 `64K -> 1K` 重复两次。服务、benchmark 和 HCA 采集复用已经验证的
|
||||||
|
Phase 1/2 实现;本目录只负责工作负载编排与带宽模型拟合。
|
||||||
|
|
||||||
|
最终高价值结果:
|
||||||
|
|
||||||
|
- `rdma_case_metrics.csv`:每个 Case 的 TPS、每 Rail/双 Rail 带宽和 bytes/token;
|
||||||
|
- `rdma_demand_model.json`:拟合参数、并发拐点和理论需求;
|
||||||
|
- `rdma_demand_report.md`:可直接用于阶段汇报的结论;
|
||||||
|
- `recommendation.env`:Scout 自动选择的 Confirm 并发。
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
# DeepSeek-V4-Pro two-node SGLang RDMA demand modeling.
|
||||||
|
|
||||||
|
EXPERIMENT="dsv4pro_pro6000d_2node_sglang_rdma_demand_modeling"
|
||||||
|
|
||||||
|
PHASE1_DIR="${PHASE1_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../dsv4pro_pro6000d_2node_sglang_tp16_quick_map" && pwd)}"
|
||||||
|
PHASE2_DIR="${PHASE2_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../dsv4pro_pro6000d_2node_sglang_hardware_contention_attribution" && pwd)}"
|
||||||
|
PHASE2_ENTRY="${PHASE2_ENTRY:-${PHASE2_DIR}/run_hardware_contention_attribution.sh}"
|
||||||
|
|
||||||
|
# Workload policy. Scout isolates Prefill; confirm preserves the requested 64K -> 1K shape.
|
||||||
|
ISL="${ISL:-65536}"
|
||||||
|
SCOUT_OSL="${SCOUT_OSL:-1}"
|
||||||
|
CONFIRM_OSL="${CONFIRM_OSL:-1024}"
|
||||||
|
SCOUT_CONCURRENCIES="${SCOUT_CONCURRENCIES:-1 4 16 32 64}"
|
||||||
|
DRY_RUN_CONFIRM_CONCURRENCIES="${DRY_RUN_CONFIRM_CONCURRENCIES:-16 32 64}"
|
||||||
|
SCOUT_REPETITIONS="${SCOUT_REPETITIONS:-1}"
|
||||||
|
CONFIRM_REPETITIONS="${CONFIRM_REPETITIONS:-2}"
|
||||||
|
|
||||||
|
# A point is a plateau candidate when both bandwidth and Input TPS gains stay below this value.
|
||||||
|
PLATEAU_GAIN_PCT="${PLATEAU_GAIN_PCT:-5}"
|
||||||
|
TARGET_RAIL_GBPS="${TARGET_RAIL_GBPS:-400}"
|
||||||
|
PRACTICAL_RAIL_GBPS="${PRACTICAL_RAIL_GBPS:-360}"
|
||||||
|
|
||||||
|
# Reuse the verified Phase 2 collectors but skip its one-time communication microbenchmark.
|
||||||
|
SAMPLE_INTERVAL_S="${SAMPLE_INTERVAL_S:-1}"
|
||||||
|
IDLE_BASELINE_S="${IDLE_BASELINE_S:-5}"
|
||||||
|
POST_RUN_COOLDOWN_S="${POST_RUN_COOLDOWN_S:-5}"
|
||||||
|
CASE_COOLDOWN_S="${CASE_COOLDOWN_S:-3}"
|
||||||
|
COLLECTOR_MAX_SECONDS="${COLLECTOR_MAX_SECONDS:-14400}"
|
||||||
|
SCENARIO_TIMEOUT_S="${SCENARIO_TIMEOUT_S:-7200}"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
RESULT_BASE="${RESULT_BASE:-${SCRIPT_DIR}/results}"
|
||||||
|
RUNTIME_BASE="${RUNTIME_BASE:-${SCRIPT_DIR}/runtime}"
|
||||||
|
|
||||||
|
DRY_RUN="${DRY_RUN:-0}"
|
||||||
@ -0,0 +1,563 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fit model-serving RDMA demand from aligned benchmark and HCA counters."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import statistics
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
||||||
|
def read_csv(path: Path) -> list[dict[str, str]]:
|
||||||
|
if not path.is_file():
|
||||||
|
return []
|
||||||
|
with path.open(encoding="utf-8", newline="") as handle:
|
||||||
|
return list(csv.DictReader(handle))
|
||||||
|
|
||||||
|
|
||||||
|
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not rows:
|
||||||
|
path.write_text("", encoding="utf-8")
|
||||||
|
return
|
||||||
|
fields: list[str] = []
|
||||||
|
for row in rows:
|
||||||
|
for field in row:
|
||||||
|
if field not in fields:
|
||||||
|
fields.append(field)
|
||||||
|
with path.open("w", encoding="utf-8", newline="") as handle:
|
||||||
|
writer = csv.DictWriter(handle, fieldnames=fields)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def number(value: Any) -> float | None:
|
||||||
|
if value in (None, ""):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
return parsed if math.isfinite(parsed) else None
|
||||||
|
|
||||||
|
|
||||||
|
def integer(value: Any) -> int:
|
||||||
|
parsed = number(value)
|
||||||
|
return int(parsed) if parsed is not None else 0
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: Iterable[float], fraction: float) -> float | None:
|
||||||
|
ordered = sorted(values)
|
||||||
|
if not ordered:
|
||||||
|
return None
|
||||||
|
position = (len(ordered) - 1) * fraction
|
||||||
|
lower = math.floor(position)
|
||||||
|
upper = math.ceil(position)
|
||||||
|
if lower == upper:
|
||||||
|
return ordered[lower]
|
||||||
|
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
|
||||||
|
|
||||||
|
|
||||||
|
def mean(values: Iterable[float | None]) -> float | None:
|
||||||
|
present = [value for value in values if value is not None]
|
||||||
|
return statistics.fmean(present) if present else None
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(value: Any, digits: int = 2) -> str:
|
||||||
|
parsed = number(value)
|
||||||
|
return "-" if parsed is None else f"{parsed:.{digits}f}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_iso_ns(value: str) -> int:
|
||||||
|
return int(datetime.fromisoformat(value).timestamp() * 1_000_000_000)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaseKey:
|
||||||
|
case_id: str
|
||||||
|
repetition: int
|
||||||
|
|
||||||
|
|
||||||
|
def load_bench(result_dir: Path) -> dict[CaseKey, dict[str, str]]:
|
||||||
|
rows = read_csv(result_dir / "bench_summary.csv")
|
||||||
|
result: dict[CaseKey, dict[str, str]] = {}
|
||||||
|
for row in rows:
|
||||||
|
key = CaseKey(row.get("case_id", ""), integer(row.get("repetition")))
|
||||||
|
if key.case_id:
|
||||||
|
result[key] = row
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_windows(result_dir: Path) -> dict[CaseKey, dict[str, str]]:
|
||||||
|
rows = read_csv(result_dir / "case_windows.csv")
|
||||||
|
result: dict[CaseKey, dict[str, str]] = {}
|
||||||
|
for row in rows:
|
||||||
|
key = CaseKey(row.get("case_id", ""), integer(row.get("repetition")))
|
||||||
|
if key.case_id:
|
||||||
|
result[key] = row
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_case_rdma(
|
||||||
|
result_dir: Path,
|
||||||
|
) -> dict[CaseKey, list[dict[str, str]]]:
|
||||||
|
grouped: dict[CaseKey, list[dict[str, str]]] = {}
|
||||||
|
for row in read_csv(result_dir / "case_rdma_summary.csv"):
|
||||||
|
key = CaseKey(row.get("case_id", ""), integer(row.get("repetition")))
|
||||||
|
grouped.setdefault(key, []).append(row)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
|
||||||
|
def interval_rates(
|
||||||
|
rows: list[dict[str, str]], started_ns: int, ended_ns: int
|
||||||
|
) -> tuple[list[float], list[float]]:
|
||||||
|
selected = []
|
||||||
|
for row in rows:
|
||||||
|
timestamp = integer(row.get("wall_time_ns"))
|
||||||
|
if started_ns <= timestamp <= ended_ns:
|
||||||
|
selected.append(row)
|
||||||
|
selected.sort(key=lambda row: integer(row.get("wall_time_ns")))
|
||||||
|
|
||||||
|
xmit_rates: list[float] = []
|
||||||
|
rcv_rates: list[float] = []
|
||||||
|
for previous, current in zip(selected, selected[1:]):
|
||||||
|
delta_ns = integer(current.get("wall_time_ns")) - integer(
|
||||||
|
previous.get("wall_time_ns")
|
||||||
|
)
|
||||||
|
if delta_ns <= 0:
|
||||||
|
continue
|
||||||
|
duration_s = delta_ns / 1e9
|
||||||
|
for field, output in (
|
||||||
|
("port_xmit_data", xmit_rates),
|
||||||
|
("port_rcv_data", rcv_rates),
|
||||||
|
):
|
||||||
|
first = number(previous.get(field))
|
||||||
|
last = number(current.get(field))
|
||||||
|
if first is None or last is None or last < first:
|
||||||
|
continue
|
||||||
|
# IB port data counters use 4-octet units.
|
||||||
|
output.append((last - first) * 4 * 8 / duration_s / 1e9)
|
||||||
|
return xmit_rates, rcv_rates
|
||||||
|
|
||||||
|
|
||||||
|
def raw_rdma_by_node_hca(
|
||||||
|
result_dir: Path,
|
||||||
|
) -> dict[tuple[str, str], list[dict[str, str]]]:
|
||||||
|
grouped: dict[tuple[str, str], list[dict[str, str]]] = {}
|
||||||
|
for node in ("head", "worker"):
|
||||||
|
for row in read_csv(result_dir / node / "rdma.csv"):
|
||||||
|
hca = row.get("hca", "")
|
||||||
|
if hca:
|
||||||
|
grouped.setdefault((node, hca), []).append(row)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
|
||||||
|
def load_gpu_util(result_dir: Path) -> dict[CaseKey, float]:
|
||||||
|
grouped: dict[CaseKey, list[float]] = {}
|
||||||
|
for row in read_csv(result_dir / "case_gpu_node_summary.csv"):
|
||||||
|
key = CaseKey(row.get("case_id", ""), integer(row.get("repetition")))
|
||||||
|
value = number(row.get("gpu_util_pct_mean"))
|
||||||
|
if value is not None:
|
||||||
|
grouped.setdefault(key, []).append(value)
|
||||||
|
return {key: statistics.fmean(values) for key, values in grouped.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_stage(result_dir: Path, stage_name: str) -> list[dict[str, Any]]:
|
||||||
|
bench = load_bench(result_dir)
|
||||||
|
windows = load_windows(result_dir)
|
||||||
|
case_rdma = load_case_rdma(result_dir)
|
||||||
|
raw_rdma = raw_rdma_by_node_hca(result_dir)
|
||||||
|
gpu_util = load_gpu_util(result_dir)
|
||||||
|
output: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for key, bench_row in sorted(
|
||||||
|
bench.items(), key=lambda item: (integer(item[1].get("concurrency")), item[0].repetition)
|
||||||
|
):
|
||||||
|
window = windows.get(key)
|
||||||
|
rdma_rows = case_rdma.get(key, [])
|
||||||
|
if not window or not rdma_rows:
|
||||||
|
continue
|
||||||
|
started_ns = integer(window.get("started_ns")) or parse_iso_ns(
|
||||||
|
window.get("started_at", "")
|
||||||
|
)
|
||||||
|
ended_ns = integer(window.get("ended_ns")) or parse_iso_ns(
|
||||||
|
window.get("ended_at", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
rail_averages: list[float] = []
|
||||||
|
rail_p95s: list[float] = []
|
||||||
|
rail_maxima: list[float] = []
|
||||||
|
node_averages: dict[str, float] = {}
|
||||||
|
xmit_bytes: list[float] = []
|
||||||
|
error_delta = 0.0
|
||||||
|
for rdma_row in rdma_rows:
|
||||||
|
node = rdma_row.get("node", "")
|
||||||
|
hca = rdma_row.get("hca", "")
|
||||||
|
average = number(rdma_row.get("xmit_gbps"))
|
||||||
|
total_bytes = number(rdma_row.get("xmit_bytes"))
|
||||||
|
if average is not None:
|
||||||
|
rail_averages.append(average)
|
||||||
|
node_averages[node] = node_averages.get(node, 0.0) + average
|
||||||
|
if total_bytes is not None:
|
||||||
|
xmit_bytes.append(total_bytes)
|
||||||
|
rates, _ = interval_rates(raw_rdma.get((node, hca), []), started_ns, ended_ns)
|
||||||
|
p95 = percentile(rates, 0.95)
|
||||||
|
if p95 is not None:
|
||||||
|
rail_p95s.append(p95)
|
||||||
|
if rates:
|
||||||
|
rail_maxima.append(max(rates))
|
||||||
|
for field in (
|
||||||
|
"port_xmit_wait_delta",
|
||||||
|
"port_xmit_discards_delta",
|
||||||
|
"port_rcv_errors_delta",
|
||||||
|
"req_transport_retries_exceeded_delta",
|
||||||
|
"req_rnr_retries_exceeded_delta",
|
||||||
|
"roce_adp_retrans_delta",
|
||||||
|
"roce_adp_retrans_to_delta",
|
||||||
|
):
|
||||||
|
error_delta += number(rdma_row.get(field)) or 0.0
|
||||||
|
|
||||||
|
total_input_tokens = number(bench_row.get("total_input_tokens"))
|
||||||
|
total_output_tokens = number(bench_row.get("total_output_tokens"))
|
||||||
|
total_tokens = (
|
||||||
|
total_input_tokens + total_output_tokens
|
||||||
|
if total_input_tokens is not None and total_output_tokens is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
mean_xmit_bytes = mean(xmit_bytes)
|
||||||
|
bytes_per_input = (
|
||||||
|
mean_xmit_bytes / total_input_tokens
|
||||||
|
if mean_xmit_bytes is not None and total_input_tokens
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
bytes_per_total = (
|
||||||
|
mean_xmit_bytes / total_tokens
|
||||||
|
if mean_xmit_bytes is not None and total_tokens
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
min_rail = min(rail_averages) if rail_averages else None
|
||||||
|
max_rail = max(rail_averages) if rail_averages else None
|
||||||
|
rail_imbalance = (
|
||||||
|
(max_rail - min_rail) / statistics.fmean(rail_averages) * 100
|
||||||
|
if min_rail is not None and max_rail is not None and rail_averages
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
"stage": stage_name,
|
||||||
|
"case_id": key.case_id,
|
||||||
|
"repetition": key.repetition,
|
||||||
|
"status": bench_row.get("status", ""),
|
||||||
|
"isl": integer(bench_row.get("isl")),
|
||||||
|
"osl": integer(bench_row.get("osl")),
|
||||||
|
"concurrency": integer(bench_row.get("concurrency")),
|
||||||
|
"completed": integer(bench_row.get("completed")),
|
||||||
|
"duration_s": number(bench_row.get("duration_s")),
|
||||||
|
"input_tps": number(bench_row.get("input_token_throughput")),
|
||||||
|
"output_tps": number(bench_row.get("output_token_throughput")),
|
||||||
|
"total_tps": number(bench_row.get("total_token_throughput")),
|
||||||
|
"ttft_p95_ms": number(bench_row.get("ttft_p95_ms")),
|
||||||
|
"tpot_p95_ms": number(bench_row.get("tpot_p95_ms")),
|
||||||
|
"gpu_util_mean_pct": gpu_util.get(key),
|
||||||
|
"rail_xmit_avg_gbps": mean(rail_averages),
|
||||||
|
"rail_xmit_p95_gbps": mean(rail_p95s),
|
||||||
|
"rail_xmit_max_gbps": max(rail_maxima) if rail_maxima else None,
|
||||||
|
"dual_rail_xmit_avg_gbps": mean(node_averages.values()),
|
||||||
|
"rail_imbalance_pct": rail_imbalance,
|
||||||
|
"xmit_bytes_per_input_token_per_rail": bytes_per_input,
|
||||||
|
"xmit_bytes_per_total_token_per_rail": bytes_per_total,
|
||||||
|
"rdma_error_delta": error_delta,
|
||||||
|
"measurement_started_at": window.get("started_at", ""),
|
||||||
|
"measurement_ended_at": window.get("ended_at", ""),
|
||||||
|
"source_result_dir": str(result_dir),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def average_by_concurrency(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
grouped: dict[int, list[dict[str, Any]]] = {}
|
||||||
|
for row in rows:
|
||||||
|
if row.get("status") == "COMPLETED":
|
||||||
|
grouped.setdefault(integer(row.get("concurrency")), []).append(row)
|
||||||
|
output = []
|
||||||
|
metrics = (
|
||||||
|
"input_tps",
|
||||||
|
"output_tps",
|
||||||
|
"total_tps",
|
||||||
|
"ttft_p95_ms",
|
||||||
|
"tpot_p95_ms",
|
||||||
|
"gpu_util_mean_pct",
|
||||||
|
"rail_xmit_avg_gbps",
|
||||||
|
"rail_xmit_p95_gbps",
|
||||||
|
"rail_xmit_max_gbps",
|
||||||
|
"dual_rail_xmit_avg_gbps",
|
||||||
|
"rail_imbalance_pct",
|
||||||
|
"xmit_bytes_per_input_token_per_rail",
|
||||||
|
"xmit_bytes_per_total_token_per_rail",
|
||||||
|
"rdma_error_delta",
|
||||||
|
)
|
||||||
|
for concurrency, group in sorted(grouped.items()):
|
||||||
|
item: dict[str, Any] = {
|
||||||
|
"stage": group[0].get("stage"),
|
||||||
|
"concurrency": concurrency,
|
||||||
|
"repetitions": len(group),
|
||||||
|
}
|
||||||
|
for metric in metrics:
|
||||||
|
item[metric] = mean(number(row.get(metric)) for row in group)
|
||||||
|
output.append(item)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def fit_hyperbola(rows: list[dict[str, Any]], field: str) -> dict[str, float | None]:
|
||||||
|
points = [
|
||||||
|
(float(integer(row.get("concurrency"))), number(row.get(field)))
|
||||||
|
for row in rows
|
||||||
|
if integer(row.get("concurrency")) > 0 and number(row.get(field)) is not None
|
||||||
|
]
|
||||||
|
if len(points) < 2:
|
||||||
|
return {"asymptote": None, "half_saturation_c": None, "rmse": None}
|
||||||
|
|
||||||
|
best: tuple[float, float, float] | None = None
|
||||||
|
for exponent_step in range(-20, 121):
|
||||||
|
k = 10 ** (exponent_step / 20)
|
||||||
|
factors = [concurrency / (k + concurrency) for concurrency, _ in points]
|
||||||
|
denominator = sum(factor * factor for factor in factors)
|
||||||
|
if denominator == 0:
|
||||||
|
continue
|
||||||
|
asymptote = sum(
|
||||||
|
factor * value for factor, (_, value) in zip(factors, points)
|
||||||
|
) / denominator
|
||||||
|
errors = [
|
||||||
|
value - asymptote * factor
|
||||||
|
for factor, (_, value) in zip(factors, points)
|
||||||
|
]
|
||||||
|
rmse = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||||
|
if best is None or rmse < best[0]:
|
||||||
|
best = (rmse, asymptote, k)
|
||||||
|
assert best is not None
|
||||||
|
return {
|
||||||
|
"asymptote": best[1],
|
||||||
|
"half_saturation_c": best[2],
|
||||||
|
"rmse": best[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def linear_slope(rows: list[dict[str, Any]]) -> float | None:
|
||||||
|
points = [
|
||||||
|
(number(row.get("input_tps")), number(row.get("rail_xmit_avg_gbps")))
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
present = [(x, y) for x, y in points if x is not None and y is not None]
|
||||||
|
denominator = sum(x * x for x, _ in present)
|
||||||
|
return sum(x * y for x, y in present) / denominator if denominator else None
|
||||||
|
|
||||||
|
|
||||||
|
def select_concurrencies(
|
||||||
|
scout: list[dict[str, Any]], plateau_gain_pct: float
|
||||||
|
) -> tuple[list[int], int | None, int | None]:
|
||||||
|
ordered = sorted(scout, key=lambda row: integer(row.get("concurrency")))
|
||||||
|
if not ordered:
|
||||||
|
return [], None, None
|
||||||
|
plateau_c = None
|
||||||
|
for previous, current in zip(ordered, ordered[1:]):
|
||||||
|
previous_bw = number(previous.get("rail_xmit_avg_gbps")) or 0
|
||||||
|
current_bw = number(current.get("rail_xmit_avg_gbps")) or 0
|
||||||
|
previous_tps = number(previous.get("input_tps")) or 0
|
||||||
|
current_tps = number(current.get("input_tps")) or 0
|
||||||
|
bw_gain = (current_bw / previous_bw - 1) * 100 if previous_bw else math.inf
|
||||||
|
tps_gain = (current_tps / previous_tps - 1) * 100 if previous_tps else math.inf
|
||||||
|
if bw_gain < plateau_gain_pct and tps_gain < plateau_gain_pct:
|
||||||
|
plateau_c = integer(current.get("concurrency"))
|
||||||
|
break
|
||||||
|
|
||||||
|
best = max(
|
||||||
|
ordered,
|
||||||
|
key=lambda row: number(row.get("rail_xmit_avg_gbps")) or -1,
|
||||||
|
)
|
||||||
|
best_c = integer(best.get("concurrency"))
|
||||||
|
cs = [integer(row.get("concurrency")) for row in ordered]
|
||||||
|
anchor = plateau_c or best_c
|
||||||
|
anchor_index = cs.index(anchor)
|
||||||
|
candidates = {anchor, cs[max(0, anchor_index - 1)], cs[-1]}
|
||||||
|
if len(candidates) < 3:
|
||||||
|
candidates.update(cs[-3:])
|
||||||
|
return sorted(candidates), plateau_c, best_c
|
||||||
|
|
||||||
|
|
||||||
|
def build_model(
|
||||||
|
scout_rows: list[dict[str, Any]],
|
||||||
|
confirm_rows: list[dict[str, Any]],
|
||||||
|
plateau_gain_pct: float,
|
||||||
|
target_rail_gbps: float,
|
||||||
|
practical_rail_gbps: float,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
scout = average_by_concurrency(scout_rows)
|
||||||
|
confirm = average_by_concurrency(confirm_rows)
|
||||||
|
selected, plateau_c, best_scout_c = select_concurrencies(
|
||||||
|
scout, plateau_gain_pct
|
||||||
|
)
|
||||||
|
slope = linear_slope(scout)
|
||||||
|
bytes_per_token = slope * 1e9 / 8 if slope is not None else None
|
||||||
|
required_target_tps = target_rail_gbps / slope if slope else None
|
||||||
|
required_practical_tps = practical_rail_gbps / slope if slope else None
|
||||||
|
required_dual_400_tps = (target_rail_gbps / 2) / slope if slope else None
|
||||||
|
bandwidth_fit = fit_hyperbola(scout, "rail_xmit_avg_gbps")
|
||||||
|
input_tps_fit = fit_hyperbola(scout, "input_tps")
|
||||||
|
max_confirm = (
|
||||||
|
max(confirm, key=lambda row: number(row.get("rail_xmit_avg_gbps")) or -1)
|
||||||
|
if confirm
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
asymptote = number(bandwidth_fit.get("asymptote"))
|
||||||
|
if asymptote is None:
|
||||||
|
verdict = "INSUFFICIENT_DATA"
|
||||||
|
elif asymptote < practical_rail_gbps:
|
||||||
|
verdict = "COMPUTE_OR_MODEL_THROUGHPUT_LIMITED_BEFORE_RDMA_SATURATION"
|
||||||
|
else:
|
||||||
|
verdict = "RDMA_SATURATION_POSSIBLE_WITH_MODEL_LOAD"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"generated_at": datetime.now().astimezone().isoformat(),
|
||||||
|
"formula": "rail_gbps ~= input_tps * bytes_per_input_token_per_rail * 8 / 1e9",
|
||||||
|
"plateau_gain_pct": plateau_gain_pct,
|
||||||
|
"target_rail_gbps": target_rail_gbps,
|
||||||
|
"practical_rail_gbps": practical_rail_gbps,
|
||||||
|
"scout": scout,
|
||||||
|
"confirm": confirm,
|
||||||
|
"selected_confirm_concurrencies": selected,
|
||||||
|
"observed_plateau_concurrency": plateau_c,
|
||||||
|
"best_scout_bandwidth_concurrency": best_scout_c,
|
||||||
|
"linear_gbps_per_input_tps": slope,
|
||||||
|
"modeled_bytes_per_input_token_per_rail": bytes_per_token,
|
||||||
|
"required_input_tps_for_400g_per_rail": required_target_tps,
|
||||||
|
"required_input_tps_for_360g_per_rail": required_practical_tps,
|
||||||
|
"required_input_tps_for_400g_dual_rail_aggregate": required_dual_400_tps,
|
||||||
|
"bandwidth_saturation_fit": bandwidth_fit,
|
||||||
|
"input_tps_saturation_fit": input_tps_fit,
|
||||||
|
"best_confirm_point": max_confirm,
|
||||||
|
"verdict": verdict,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_recommendation(output_dir: Path, model: dict[str, Any]) -> None:
|
||||||
|
values = model.get("selected_confirm_concurrencies", [])
|
||||||
|
joined = " ".join(str(value) for value in values)
|
||||||
|
(output_dir / "recommendation.env").write_text(
|
||||||
|
f"CONFIRM_CONCURRENCIES='{joined}'\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_report(output_dir: Path, model: dict[str, Any]) -> None:
|
||||||
|
scout = model.get("scout", [])
|
||||||
|
confirm = model.get("confirm", [])
|
||||||
|
lines = [
|
||||||
|
"# Phase 2.5 RDMA Demand Model",
|
||||||
|
"",
|
||||||
|
f"- Generated: `{model['generated_at']}`",
|
||||||
|
f"- Verdict: `{model['verdict']}`",
|
||||||
|
f"- Model: `rail_gbps ~= input_tps x {fmt(model.get('modeled_bytes_per_input_token_per_rail') / 1e6 if model.get('modeled_bytes_per_input_token_per_rail') else None, 3)} MB/token/rail x 8`",
|
||||||
|
f"- Required Input TPS for 400G per rail: `{fmt(model.get('required_input_tps_for_400g_per_rail'), 0)}`",
|
||||||
|
f"- Required Input TPS for 360G per rail: `{fmt(model.get('required_input_tps_for_360g_per_rail'), 0)}`",
|
||||||
|
f"- Required Input TPS for 400G dual-rail aggregate: `{fmt(model.get('required_input_tps_for_400g_dual_rail_aggregate'), 0)}`",
|
||||||
|
f"- Fitted rail bandwidth ceiling: `{fmt(model.get('bandwidth_saturation_fit', {}).get('asymptote'))} Gbit/s`",
|
||||||
|
f"- Observed plateau concurrency: `{model.get('observed_plateau_concurrency')}`",
|
||||||
|
"",
|
||||||
|
"## Scout: 64K -> 1",
|
||||||
|
"",
|
||||||
|
"| C | Input TPS | Rail avg | Rail P95 | Dual-rail avg | MB/input-token/rail | GPU util |",
|
||||||
|
"|---:|---:|---:|---:|---:|---:|---:|",
|
||||||
|
]
|
||||||
|
for row in scout:
|
||||||
|
bytes_per_token = number(row.get("xmit_bytes_per_input_token_per_rail"))
|
||||||
|
lines.append(
|
||||||
|
f"| {row['concurrency']} | {fmt(row.get('input_tps'))} | "
|
||||||
|
f"{fmt(row.get('rail_xmit_avg_gbps'))} | {fmt(row.get('rail_xmit_p95_gbps'))} | "
|
||||||
|
f"{fmt(row.get('dual_rail_xmit_avg_gbps'))} | "
|
||||||
|
f"{fmt(bytes_per_token / 1e6 if bytes_per_token else None, 3)} | "
|
||||||
|
f"{fmt(row.get('gpu_util_mean_pct'))} |"
|
||||||
|
)
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## Confirm: 64K -> 1K",
|
||||||
|
"",
|
||||||
|
"| C | Reps | Input TPS | Output TPS | Rail avg | Rail P95 | Dual-rail avg | TTFT P95 ms | TPOT P95 ms |",
|
||||||
|
"|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for row in confirm:
|
||||||
|
lines.append(
|
||||||
|
f"| {row['concurrency']} | {row['repetitions']} | {fmt(row.get('input_tps'))} | "
|
||||||
|
f"{fmt(row.get('output_tps'))} | {fmt(row.get('rail_xmit_avg_gbps'))} | "
|
||||||
|
f"{fmt(row.get('rail_xmit_p95_gbps'))} | {fmt(row.get('dual_rail_xmit_avg_gbps'))} | "
|
||||||
|
f"{fmt(row.get('ttft_p95_ms'))} | {fmt(row.get('tpot_p95_ms'))} |"
|
||||||
|
)
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## Evidence",
|
||||||
|
"",
|
||||||
|
"- Per-case metrics: `rdma_case_metrics.csv`",
|
||||||
|
"- Machine-readable model: `rdma_demand_model.json`",
|
||||||
|
"- Scout HCA source: `scout/{head,worker}/rdma.csv`",
|
||||||
|
"- Confirm HCA source: `confirm/{head,worker}/rdma.csv`",
|
||||||
|
"- Exact windows: `scout/case_windows.csv`, `confirm/case_windows.csv`",
|
||||||
|
"- Benchmark summaries: `scout/bench_summary.csv`, `confirm/bench_summary.csv`",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
(output_dir / "rdma_demand_report.md").write_text(
|
||||||
|
"\n".join(lines) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("mode", choices=("scout", "final"))
|
||||||
|
parser.add_argument("--scout-dir", required=True, type=Path)
|
||||||
|
parser.add_argument("--confirm-dir", type=Path)
|
||||||
|
parser.add_argument("--output-dir", required=True, type=Path)
|
||||||
|
parser.add_argument("--plateau-gain-pct", type=float, default=5.0)
|
||||||
|
parser.add_argument("--target-rail-gbps", type=float, default=400.0)
|
||||||
|
parser.add_argument("--practical-rail-gbps", type=float, default=360.0)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
scout_rows = aggregate_stage(args.scout_dir, "scout")
|
||||||
|
if not scout_rows:
|
||||||
|
raise SystemExit(f"no aligned scout rows found in {args.scout_dir}")
|
||||||
|
confirm_rows: list[dict[str, Any]] = []
|
||||||
|
if args.mode == "final":
|
||||||
|
if args.confirm_dir is None:
|
||||||
|
raise SystemExit("--confirm-dir is required in final mode")
|
||||||
|
confirm_rows = aggregate_stage(args.confirm_dir, "confirm")
|
||||||
|
if not confirm_rows:
|
||||||
|
raise SystemExit(f"no aligned confirm rows found in {args.confirm_dir}")
|
||||||
|
|
||||||
|
model = build_model(
|
||||||
|
scout_rows,
|
||||||
|
confirm_rows,
|
||||||
|
args.plateau_gain_pct,
|
||||||
|
args.target_rail_gbps,
|
||||||
|
args.practical_rail_gbps,
|
||||||
|
)
|
||||||
|
write_csv(args.output_dir / "rdma_case_metrics.csv", scout_rows + confirm_rows)
|
||||||
|
(args.output_dir / "rdma_demand_model.json").write_text(
|
||||||
|
json.dumps(model, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
write_recommendation(args.output_dir, model)
|
||||||
|
write_report(args.output_dir, model)
|
||||||
|
print(json.dumps(model, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -0,0 +1,264 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Measure how model token throughput translates into two-node RDMA bandwidth.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${SCRIPT_DIR}/config.env"
|
||||||
|
|
||||||
|
ACTION="${1:-all}"
|
||||||
|
RUN_ID="${RUN_ID:-dsv4pro-phase2_5-$(date +%Y%m%d-%H%M%S)}"
|
||||||
|
RESULT_DIR="${RESULT_BASE}/${RUN_ID}"
|
||||||
|
COMMAND_DIR="${RESULT_DIR}/commands"
|
||||||
|
SCENARIO_DIR="${RESULT_DIR}/scenarios"
|
||||||
|
ANALYZER="${SCRIPT_DIR}/rdma_demand_model.py"
|
||||||
|
RUN_LOG_ACTIVE=0
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_command() {
|
||||||
|
printf '%q ' "$@"
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_result_logging() {
|
||||||
|
[[ "${DRY_RUN}" == "1" || "${RUN_LOG_ACTIVE}" == "1" ]] && return 0
|
||||||
|
mkdir -p "${RESULT_DIR}"
|
||||||
|
exec > >(tee -a "${RESULT_DIR}/run.log") 2>&1
|
||||||
|
RUN_LOG_ACTIVE=1
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_concurrencies() {
|
||||||
|
tr ', ' '\n\n' <<< "$1" |
|
||||||
|
sed '/^[[:space:]]*$/d' |
|
||||||
|
awk '!seen[$0]++' |
|
||||||
|
sort -n |
|
||||||
|
paste -sd' ' -
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_config() {
|
||||||
|
[[ -x "${PHASE2_ENTRY}" ]] || {
|
||||||
|
log "ERROR: Phase 2 entry is missing: ${PHASE2_ENTRY}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
[[ -f "${ANALYZER}" ]] || {
|
||||||
|
log "ERROR: analyzer is missing: ${ANALYZER}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local name value
|
||||||
|
for name in ISL SCOUT_OSL CONFIRM_OSL SCOUT_REPETITIONS CONFIRM_REPETITIONS \
|
||||||
|
SAMPLE_INTERVAL_S COLLECTOR_MAX_SECONDS SCENARIO_TIMEOUT_S; do
|
||||||
|
value="${!name}"
|
||||||
|
[[ "${value}" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
|
log "ERROR: ${name} must be a positive integer: ${value}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
[[ "${PLATEAU_GAIN_PCT}" =~ ^[0-9]+([.][0-9]+)?$ ]] || {
|
||||||
|
log "ERROR: PLATEAU_GAIN_PCT must be numeric"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local concurrency
|
||||||
|
for concurrency in $(normalize_concurrencies "${SCOUT_CONCURRENCIES}"); do
|
||||||
|
[[ "${concurrency}" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
|
log "ERROR: invalid scout concurrency: ${concurrency}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
write_scenario_file() {
|
||||||
|
local stage="$1"
|
||||||
|
local osl="$2"
|
||||||
|
local concurrencies="$3"
|
||||||
|
local output="$4"
|
||||||
|
mkdir -p "$(dirname "${output}")"
|
||||||
|
printf '# case_id\tstage\tisl\tosl\tconcurrency\tprompt_multiplier\tmin_prompts\twarmup_requests\tnote\n' > "${output}"
|
||||||
|
local concurrency case_id
|
||||||
|
for concurrency in $(normalize_concurrencies "${concurrencies}"); do
|
||||||
|
case_id="rdma_${stage}_i${ISL}_o${osl}_c${concurrency}"
|
||||||
|
printf '%s\t%s\t%s\t%s\t%s\t1\t1\t0\t%s\n' \
|
||||||
|
"${case_id}" "rdma_${stage}" "${ISL}" "${osl}" "${concurrency}" \
|
||||||
|
"Phase 2.5 ${stage}: correlate model token rate with dual-rail RDMA" \
|
||||||
|
>> "${output}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
case_ids_for() {
|
||||||
|
local stage="$1"
|
||||||
|
local osl="$2"
|
||||||
|
local concurrencies="$3"
|
||||||
|
local result=() concurrency
|
||||||
|
for concurrency in $(normalize_concurrencies "${concurrencies}"); do
|
||||||
|
result+=("rdma_${stage}_i${ISL}_o${osl}_c${concurrency}")
|
||||||
|
done
|
||||||
|
local joined
|
||||||
|
joined="$(IFS=,; printf '%s' "${result[*]}")"
|
||||||
|
printf '%s\n' "${joined}"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_phase2_stage() {
|
||||||
|
local stage="$1"
|
||||||
|
local scenario_file="$2"
|
||||||
|
local case_ids="$3"
|
||||||
|
local repetitions="$4"
|
||||||
|
local stage_result="${RESULT_DIR}/${stage}"
|
||||||
|
local command_file="${COMMAND_DIR}/${stage}.cmd.txt"
|
||||||
|
local -a command=(
|
||||||
|
env
|
||||||
|
"DRY_RUN=${DRY_RUN}"
|
||||||
|
"RUN_ID=${stage}"
|
||||||
|
"RESULT_BASE=${RESULT_DIR}"
|
||||||
|
"SCENARIO_FILE=${scenario_file}"
|
||||||
|
"FIXED_CASE_IDS=${case_ids}"
|
||||||
|
"RUN_MIXED_CASE=0"
|
||||||
|
"RUN_COMMUNICATION_BASELINE=0"
|
||||||
|
"NUM_REPETITIONS=${repetitions}"
|
||||||
|
"SAMPLE_INTERVAL_S=${SAMPLE_INTERVAL_S}"
|
||||||
|
"IDLE_BASELINE_S=${IDLE_BASELINE_S}"
|
||||||
|
"POST_RUN_COOLDOWN_S=${POST_RUN_COOLDOWN_S}"
|
||||||
|
"CASE_COOLDOWN_S=${CASE_COOLDOWN_S}"
|
||||||
|
"COLLECTOR_MAX_SECONDS=${COLLECTOR_MAX_SECONDS}"
|
||||||
|
"SCENARIO_TIMEOUT_S=${SCENARIO_TIMEOUT_S}"
|
||||||
|
bash "${PHASE2_ENTRY}" all
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "${COMMAND_DIR}"
|
||||||
|
print_command "${command[@]}" > "${command_file}"
|
||||||
|
log "START stage=${stage} result=${stage_result}"
|
||||||
|
"${command[@]}"
|
||||||
|
log "DONE stage=${stage} result=${stage_result}"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_scout() {
|
||||||
|
local scenario_file="${SCENARIO_DIR}/scout.tsv"
|
||||||
|
local concurrencies case_ids
|
||||||
|
concurrencies="$(normalize_concurrencies "${SCOUT_CONCURRENCIES}")"
|
||||||
|
write_scenario_file scout "${SCOUT_OSL}" "${concurrencies}" "${scenario_file}"
|
||||||
|
case_ids="$(case_ids_for scout "${SCOUT_OSL}" "${concurrencies}")"
|
||||||
|
run_phase2_stage scout "${scenario_file}" "${case_ids}" "${SCOUT_REPETITIONS}"
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN}" != "1" ]]; then
|
||||||
|
python3 "${ANALYZER}" scout \
|
||||||
|
--scout-dir "${RESULT_DIR}/scout" \
|
||||||
|
--output-dir "${RESULT_DIR}" \
|
||||||
|
--plateau-gain-pct "${PLATEAU_GAIN_PCT}" \
|
||||||
|
--target-rail-gbps "${TARGET_RAIL_GBPS}" \
|
||||||
|
--practical-rail-gbps "${PRACTICAL_RAIL_GBPS}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_concurrencies() {
|
||||||
|
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||||
|
normalize_concurrencies "${DRY_RUN_CONFIRM_CONCURRENCIES}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${RESULT_DIR}/recommendation.env"
|
||||||
|
normalize_concurrencies "${CONFIRM_CONCURRENCIES}"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_confirm() {
|
||||||
|
local scenario_file="${SCENARIO_DIR}/confirm.tsv"
|
||||||
|
local concurrencies case_ids
|
||||||
|
concurrencies="$(confirm_concurrencies)"
|
||||||
|
write_scenario_file confirm "${CONFIRM_OSL}" "${concurrencies}" "${scenario_file}"
|
||||||
|
case_ids="$(case_ids_for confirm "${CONFIRM_OSL}" "${concurrencies}")"
|
||||||
|
run_phase2_stage confirm "${scenario_file}" "${case_ids}" "${CONFIRM_REPETITIONS}"
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN}" != "1" ]]; then
|
||||||
|
python3 "${ANALYZER}" final \
|
||||||
|
--scout-dir "${RESULT_DIR}/scout" \
|
||||||
|
--confirm-dir "${RESULT_DIR}/confirm" \
|
||||||
|
--output-dir "${RESULT_DIR}" \
|
||||||
|
--plateau-gain-pct "${PLATEAU_GAIN_PCT}" \
|
||||||
|
--target-rail-gbps "${TARGET_RAIL_GBPS}" \
|
||||||
|
--practical-rail-gbps "${PRACTICAL_RAIL_GBPS}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_manifest() {
|
||||||
|
local commit dirty
|
||||||
|
commit="$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || printf unknown)"
|
||||||
|
dirty=0
|
||||||
|
[[ -n "$(git -C "${REPO_ROOT}" status --porcelain 2>/dev/null || true)" ]] && dirty=1
|
||||||
|
mkdir -p "${RESULT_DIR}"
|
||||||
|
cat > "${RESULT_DIR}/run_manifest.txt" <<EOF
|
||||||
|
run_id=${RUN_ID}
|
||||||
|
experiment=${EXPERIMENT}
|
||||||
|
created_at=$(date '+%Y-%m-%dT%H:%M:%S%z')
|
||||||
|
git_commit=${commit}
|
||||||
|
git_dirty=${dirty}
|
||||||
|
isl=${ISL}
|
||||||
|
scout_osl=${SCOUT_OSL}
|
||||||
|
confirm_osl=${CONFIRM_OSL}
|
||||||
|
scout_concurrencies=$(normalize_concurrencies "${SCOUT_CONCURRENCIES}")
|
||||||
|
scout_repetitions=${SCOUT_REPETITIONS}
|
||||||
|
confirm_repetitions=${CONFIRM_REPETITIONS}
|
||||||
|
target_rail_gbps=${TARGET_RAIL_GBPS}
|
||||||
|
practical_rail_gbps=${PRACTICAL_RAIL_GBPS}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
env \
|
||||||
|
"RUN_ID=${RUN_ID}-stop" \
|
||||||
|
"RESULT_BASE=${RESULT_DIR}" \
|
||||||
|
"RUN_COMMUNICATION_BASELINE=0" \
|
||||||
|
bash "${PHASE2_ENTRY}" stop || true
|
||||||
|
}
|
||||||
|
|
||||||
|
run_all() {
|
||||||
|
validate_config
|
||||||
|
enable_result_logging
|
||||||
|
write_manifest
|
||||||
|
trap 'stop_service' INT TERM
|
||||||
|
run_scout
|
||||||
|
run_confirm
|
||||||
|
trap - INT TERM
|
||||||
|
log "Phase 2.5 complete: ${RESULT_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
case "${ACTION}" in
|
||||||
|
audit)
|
||||||
|
validate_config
|
||||||
|
log "configuration valid"
|
||||||
|
;;
|
||||||
|
scout)
|
||||||
|
validate_config
|
||||||
|
enable_result_logging
|
||||||
|
write_manifest
|
||||||
|
run_scout
|
||||||
|
;;
|
||||||
|
confirm)
|
||||||
|
validate_config
|
||||||
|
enable_result_logging
|
||||||
|
run_confirm
|
||||||
|
;;
|
||||||
|
summarize)
|
||||||
|
python3 "${ANALYZER}" final \
|
||||||
|
--scout-dir "${RESULT_DIR}/scout" \
|
||||||
|
--confirm-dir "${RESULT_DIR}/confirm" \
|
||||||
|
--output-dir "${RESULT_DIR}" \
|
||||||
|
--plateau-gain-pct "${PLATEAU_GAIN_PCT}" \
|
||||||
|
--target-rail-gbps "${TARGET_RAIL_GBPS}" \
|
||||||
|
--practical-rail-gbps "${PRACTICAL_RAIL_GBPS}"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_service
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
run_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Usage: %s {audit|scout|confirm|summarize|stop|all}\n' "$0" >&2
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).with_name("rdma_demand_model.py")
|
||||||
|
SPEC = importlib.util.spec_from_file_location("rdma_demand_model", MODULE_PATH)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
MODEL = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[SPEC.name] = MODEL
|
||||||
|
SPEC.loader.exec_module(MODEL)
|
||||||
|
|
||||||
|
|
||||||
|
class RdmaDemandModelTest(unittest.TestCase):
|
||||||
|
def test_interval_rate_uses_four_octet_counter_units(self):
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"wall_time_ns": "1000000000",
|
||||||
|
"port_xmit_data": "100",
|
||||||
|
"port_rcv_data": "200",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wall_time_ns": "2000000000",
|
||||||
|
"port_xmit_data": "100000100",
|
||||||
|
"port_rcv_data": "100000200",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
xmit, rcv = MODEL.interval_rates(rows, 0, 3_000_000_000)
|
||||||
|
self.assertAlmostEqual(xmit[0], 3.2)
|
||||||
|
self.assertAlmostEqual(rcv[0], 3.2)
|
||||||
|
|
||||||
|
def test_linear_slope_recovers_bytes_per_token(self):
|
||||||
|
rows = [
|
||||||
|
{"input_tps": 1000, "rail_xmit_avg_gbps": 26.8},
|
||||||
|
{"input_tps": 2000, "rail_xmit_avg_gbps": 53.6},
|
||||||
|
{"input_tps": 3000, "rail_xmit_avg_gbps": 80.4},
|
||||||
|
]
|
||||||
|
slope = MODEL.linear_slope(rows)
|
||||||
|
self.assertAlmostEqual(slope, 0.0268)
|
||||||
|
self.assertAlmostEqual(slope * 1e9 / 8, 3_350_000)
|
||||||
|
|
||||||
|
def test_plateau_selects_previous_plateau_and_max(self):
|
||||||
|
rows = [
|
||||||
|
{"concurrency": 1, "input_tps": 1000, "rail_xmit_avg_gbps": 27},
|
||||||
|
{"concurrency": 4, "input_tps": 2400, "rail_xmit_avg_gbps": 64},
|
||||||
|
{"concurrency": 16, "input_tps": 3100, "rail_xmit_avg_gbps": 83},
|
||||||
|
{"concurrency": 32, "input_tps": 3150, "rail_xmit_avg_gbps": 84},
|
||||||
|
{"concurrency": 64, "input_tps": 3160, "rail_xmit_avg_gbps": 84.2},
|
||||||
|
]
|
||||||
|
selected, plateau, best = MODEL.select_concurrencies(rows, 5.0)
|
||||||
|
self.assertEqual(plateau, 32)
|
||||||
|
self.assertEqual(best, 64)
|
||||||
|
self.assertEqual(selected, [16, 32, 64])
|
||||||
|
|
||||||
|
def test_report_and_recommendation_are_created(self):
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"stage": "scout",
|
||||||
|
"concurrency": 16,
|
||||||
|
"repetitions": 1,
|
||||||
|
"input_tps": 3000,
|
||||||
|
"rail_xmit_avg_gbps": 80,
|
||||||
|
"rail_xmit_p95_gbps": 90,
|
||||||
|
"dual_rail_xmit_avg_gbps": 160,
|
||||||
|
"xmit_bytes_per_input_token_per_rail": 3_333_333,
|
||||||
|
"gpu_util_mean_pct": 99,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
model = {
|
||||||
|
"generated_at": "2026-08-01T00:00:00+08:00",
|
||||||
|
"verdict": "TEST",
|
||||||
|
"modeled_bytes_per_input_token_per_rail": 3_333_333,
|
||||||
|
"required_input_tps_for_400g_per_rail": 15000,
|
||||||
|
"required_input_tps_for_360g_per_rail": 13500,
|
||||||
|
"required_input_tps_for_400g_dual_rail_aggregate": 7500,
|
||||||
|
"bandwidth_saturation_fit": {"asymptote": 100},
|
||||||
|
"observed_plateau_concurrency": 32,
|
||||||
|
"scout": rows,
|
||||||
|
"confirm": [],
|
||||||
|
"selected_confirm_concurrencies": [16, 32, 64],
|
||||||
|
}
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
output = Path(temporary)
|
||||||
|
MODEL.write_recommendation(output, model)
|
||||||
|
MODEL.write_report(output, model)
|
||||||
|
self.assertIn("16 32 64", (output / "recommendation.env").read_text())
|
||||||
|
self.assertIn("15000", (output / "rdma_demand_report.md").read_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user