[Test] Add Kimi-K3 Prefill PP baseline search
This commit is contained in:
parent
60f77cd4ef
commit
d7381abe84
@ -0,0 +1,149 @@
|
||||
# Kimi-K3 Prefill PP baseline search
|
||||
|
||||
## Goal
|
||||
|
||||
Select the pipeline-parallel configuration for the next MoE A2A experiment on
|
||||
four RTX 6000D nodes. The comparison fixes every workload and backend variable
|
||||
except PP/TP:
|
||||
|
||||
- SGLang image: `local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5`
|
||||
- Model: Kimi-K3
|
||||
- GPUs: 32 GPUs on 601-604
|
||||
- MoE: FlashInfer MXFP4, EP4, A2A disabled
|
||||
- Workload: 16K input, one output token, C=8/16, 40 requests
|
||||
- Chunked prefill: 8K
|
||||
- Repeats: three per point
|
||||
|
||||
`run_pp_baseline_search.sh` is the only entry point. It uses the upstream
|
||||
SGLang PP implementation and does not apply a runtime PP patch.
|
||||
|
||||
## Result
|
||||
|
||||
Values are the medians of three runs. PP1 is the directly comparable existing
|
||||
run; PP2/4/8 come from the new search.
|
||||
|
||||
| PP / TP / EP | C | Input TPS | TTFT p50 | TTFT p95 | Input TPS vs PP1 | TTFT p50 vs PP1 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| 1 / 32 / 4 | 8 | 3,257.96 | 39.19 s | 41.62 s | baseline | baseline |
|
||||
| 2 / 16 / 4 | 8 | 5,015.95 | 25.22 s | 26.69 s | +53.96% | -35.65% |
|
||||
| 4 / 8 / 4 | 8 | 7,155.20 | 17.28 s | 18.62 s | +119.62% | -55.90% |
|
||||
| 8 / 4 / 4 | 8 | **8,809.50** | **13.47 s** | **16.45 s** | **+170.40%** | **-65.64%** |
|
||||
| 1 / 32 / 4 | 16 | 3,260.14 | 78.39 s | 80.88 s | baseline | baseline |
|
||||
| 2 / 16 / 4 | 16 | 5,018.38 | 50.70 s | 52.14 s | +53.93% | -35.32% |
|
||||
| 4 / 8 / 4 | 16 | 7,152.58 | 34.84 s | 35.95 s | +119.39% | -55.56% |
|
||||
| 8 / 4 / 4 | 16 | **8,824.12** | **27.31 s** | **29.77 s** | **+170.67%** | **-65.16%** |
|
||||
|
||||
All 18 new samples completed 40/40 requests. No OOM, traceback, NCCL failure,
|
||||
request error, or engine failure was found. The three repeats are stable: PP8
|
||||
Input TPS varies by less than 0.7% at both concurrency points.
|
||||
|
||||
## Memory
|
||||
|
||||
The table reports the highest per-GPU memory observed in the post-benchmark
|
||||
snapshot across all four nodes.
|
||||
|
||||
| PP / TP / EP | Peak GPU memory | Remaining from 85,651 MiB |
|
||||
|---|---:|---:|
|
||||
| 1 / 32 / 4 | 81,355 MiB | 4,296 MiB |
|
||||
| 2 / 16 / 4 | 80,335 MiB | 5,316 MiB |
|
||||
| 4 / 8 / 4 | 82,417 MiB | 3,234 MiB |
|
||||
| 8 / 4 / 4 | 79,407 MiB | 6,244 MiB |
|
||||
|
||||
PP stages do not have identical model/state allocations, so memory is less
|
||||
balanced than PP1. PP8 nevertheless has the best worst-rank headroom in this
|
||||
run, which matters because A2A backends allocate additional communication
|
||||
buffers.
|
||||
|
||||
## Why PP helps here
|
||||
|
||||
The existing PP1 Nsight profile measured a median 57.27% of the Prefill window
|
||||
inside exposed NCCL work, with about 557 AllReduce calls and effectively no
|
||||
compute/communication overlap. Its TP32 collectives span all four nodes.
|
||||
|
||||
Changing PP also changes the TP communication domain:
|
||||
|
||||
- PP2/TP16: each TP group spans two nodes.
|
||||
- PP4/TP8: each TP group fits on one node; only pipeline activation transfers
|
||||
cross node boundaries.
|
||||
- PP8/TP4: two pipeline stages fit per node and each TP/EP4 group is node-local.
|
||||
|
||||
This replaces frequent cross-node TP AllReduce with a smaller number of
|
||||
pipeline activation transfers. At C=8/16 there is enough request-level work to
|
||||
fill the deeper pipeline, so the communication saving is larger than the
|
||||
pipeline bubble cost.
|
||||
|
||||
The current run verifies the rank topology, successful NCCL P2P communicators,
|
||||
and absence of communication failures. It does not contain per-PP Nsight or HCA
|
||||
bandwidth samples; the 57.27% NCCL attribution belongs specifically to the
|
||||
directly comparable PP1 profile and is used to explain, not fabricate, the PP8
|
||||
timeline.
|
||||
|
||||
## Why older PP tests could be slower
|
||||
|
||||
The old configurations were not an apples-to-apples PP-only comparison. In
|
||||
particular, an old PP4 profile set:
|
||||
|
||||
```bash
|
||||
--pp-max-micro-batch-size 1
|
||||
```
|
||||
|
||||
Current SGLang leaves this value unset and computes:
|
||||
|
||||
```python
|
||||
pp_max_micro_batch_size = max(max_running_requests // pp_size, 1)
|
||||
```
|
||||
|
||||
The scheduler then limits admission with
|
||||
`pp_max_micro_batch_size - running_bs`. Setting it to one therefore permits
|
||||
only one running request per PP scheduler, serializing much of a C=8/16 load
|
||||
and exposing pipeline bubbles. Older tests also mixed Marlin, 16K chunks,
|
||||
EP1/EP8, PP16, older images, and a runtime PP-group patch. They cannot be used
|
||||
to conclude that PP itself is slower.
|
||||
|
||||
## Decision
|
||||
|
||||
Use **PP8 / TP4 / EP4** as the primary configuration for the next A2A
|
||||
compatibility and memory smoke test. Keep **PP4 / TP8 / EP4** as the fallback:
|
||||
it is slower and has less memory headroom here, but provides a useful check if
|
||||
an A2A backend imposes TP/PP constraints not exercised by the baseline.
|
||||
|
||||
Do not carry forward `--pp-max-micro-batch-size 1`. Leave it unset unless a
|
||||
separate controlled experiment establishes a reason to cap it.
|
||||
|
||||
## Reproduce and evidence
|
||||
|
||||
Run from 601:
|
||||
|
||||
```bash
|
||||
cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search
|
||||
RUN_ID=kimi3-pp-$(date +%Y%m%d-%H%M%S) \
|
||||
SUDO_PASSWORD_FILE=/path/to/password-file \
|
||||
bash run_pp_baseline_search.sh run
|
||||
```
|
||||
|
||||
New PP2/4/8 evidence:
|
||||
|
||||
```text
|
||||
results/kimi3-pp-upstream-20260821-120208/
|
||||
summary.csv
|
||||
results.csv
|
||||
metadata/
|
||||
bench/
|
||||
raw/
|
||||
gpu/
|
||||
service/
|
||||
```
|
||||
|
||||
Existing PP1 evidence:
|
||||
|
||||
```text
|
||||
../kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/
|
||||
results/kimi3-ep4-moe-full-20260818-151349/
|
||||
```
|
||||
|
||||
Existing PP1 communication profile:
|
||||
|
||||
```text
|
||||
../kimi3_pro6000_sglang_prefill_communication_profile/
|
||||
results/kimi3-prefill-comm-20260820-143749/nsys_analysis.json
|
||||
```
|
||||
@ -0,0 +1,351 @@
|
||||
#!/usr/bin/env bash
|
||||
# Search Kimi-K3 prefill pipeline-parallel baselines on 601-604.
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "${ROOT_DIR}/scripts/common/lib.sh"
|
||||
|
||||
ACTION="${1:-run}"
|
||||
EXPERIMENT="kimi3_pro6000_sglang_pp_baseline_search"
|
||||
RUN_ID="${RUN_ID:-kimi3-pp-baseline-$(date '+%Y%m%d-%H%M%S')}"
|
||||
RESULT_ROOT="${RESULT_ROOT:-${SCRIPT_DIR}/results/${RUN_ID}}"
|
||||
|
||||
MODEL_PATH="${MODEL_PATH:-/data/hf_models/Kimi-K3}"
|
||||
SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-kimi-k3}"
|
||||
DOCKER_IMAGE="${DOCKER_IMAGE:-local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5}"
|
||||
PORT="${PORT:-30000}"
|
||||
HEAD_HOST="${HEAD_HOST:-174.1.60.1}"
|
||||
NODE_SSH_USER="${NODE_SSH_USER:-user}"
|
||||
NODE_HOSTS=(174.1.60.1 174.1.60.2 174.1.60.3 174.1.60.4)
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10)
|
||||
DIST_PORT="${DIST_PORT:-20000}"
|
||||
|
||||
EP_SIZE=4
|
||||
PP_SIZES=(2 4 8)
|
||||
CONCURRENCIES=(8 16)
|
||||
INPUT_LEN=16384
|
||||
OUTPUT_LEN=1
|
||||
CHUNKED_PREFILL_SIZE=8192
|
||||
NUM_PROMPTS=40
|
||||
REPEATS="${REPEATS:-3}"
|
||||
WARMUP_REQUESTS=2
|
||||
HEALTH_WAIT_S="${HEALTH_WAIT_S:-2400}"
|
||||
CONTAINER_PREFIX="${EXPERIMENT}"
|
||||
|
||||
mkdir -p "${RESULT_ROOT}"/{service,raw,bench,gpu,metadata}
|
||||
log_init "${RESULT_ROOT}/orchestrator.log"
|
||||
|
||||
usage() {
|
||||
printf 'Usage: SUDO_PASSWORD=... %s {dry-run|run|summarize|stop}\n' "$0"
|
||||
}
|
||||
|
||||
require_password() {
|
||||
if [[ -z "${SUDO_PASSWORD:-}" && -n "${SUDO_PASSWORD_FILE:-}" ]]; then
|
||||
[[ -r "$SUDO_PASSWORD_FILE" ]] || {
|
||||
echo "ERROR: cannot read SUDO_PASSWORD_FILE=$SUDO_PASSWORD_FILE" >&2
|
||||
exit 2
|
||||
}
|
||||
IFS= read -r SUDO_PASSWORD <"$SUDO_PASSWORD_FILE"
|
||||
fi
|
||||
[[ -n "${SUDO_PASSWORD:-}" ]] || {
|
||||
echo "ERROR: set SUDO_PASSWORD or SUDO_PASSWORD_FILE" >&2
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
|
||||
is_head() {
|
||||
[[ "$1" == "$HEAD_HOST" ]]
|
||||
}
|
||||
|
||||
sudo_host() {
|
||||
local host="$1"
|
||||
shift
|
||||
require_password
|
||||
if is_head "$host"; then
|
||||
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' -- "$@"
|
||||
return
|
||||
fi
|
||||
local remote_cmd
|
||||
printf -v remote_cmd '%q ' "$@"
|
||||
printf '%s\n' "$SUDO_PASSWORD" | ssh "${SSH_OPTS[@]}" \
|
||||
"${NODE_SSH_USER}@${host}" "sudo -S -p '' -- ${remote_cmd}"
|
||||
}
|
||||
|
||||
container_name() {
|
||||
printf '%s_node%s' "$CONTAINER_PREFIX" "$1"
|
||||
}
|
||||
|
||||
tp_for_pp() {
|
||||
local pp="$1"
|
||||
(( 32 % pp == 0 )) || { echo "ERROR: PP=$pp does not divide 32" >&2; return 1; }
|
||||
echo $((32 / pp))
|
||||
}
|
||||
|
||||
preflight() {
|
||||
require_password
|
||||
[[ -d "$MODEL_PATH" ]] || { echo "ERROR: missing model $MODEL_PATH" >&2; exit 2; }
|
||||
local host
|
||||
for host in "${NODE_HOSTS[@]}"; do
|
||||
sudo_host "$host" docker image inspect "$DOCKER_IMAGE" >/dev/null
|
||||
sudo_host "$host" test -e /dev/infiniband/uverbs0
|
||||
done
|
||||
{
|
||||
echo "run_id=$RUN_ID"
|
||||
echo "image=$DOCKER_IMAGE"
|
||||
echo "model=$MODEL_PATH"
|
||||
echo "pp_sizes=${PP_SIZES[*]}"
|
||||
echo "tp_sizes=16 8 4"
|
||||
echo "ep_size=$EP_SIZE"
|
||||
echo "shape=${INPUT_LEN}->${OUTPUT_LEN}"
|
||||
echo "chunked_prefill_size=$CHUNKED_PREFILL_SIZE"
|
||||
echo "concurrencies=${CONCURRENCIES[*]}"
|
||||
echo "num_prompts=$NUM_PROMPTS"
|
||||
echo "repeats=$REPEATS"
|
||||
} >"${RESULT_ROOT}/metadata/manifest.env"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
local rank host name
|
||||
for rank in 0 1 2 3; do
|
||||
host="${NODE_HOSTS[$rank]}"
|
||||
name="$(container_name "$rank")"
|
||||
sudo_host "$host" docker rm -f "$name" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
collect_service_logs() {
|
||||
local label="$1" rank host name
|
||||
for rank in 0 1 2 3; do
|
||||
host="${NODE_HOSTS[$rank]}"
|
||||
name="$(container_name "$rank")"
|
||||
sudo_host "$host" docker logs "$name" \
|
||||
>"${RESULT_ROOT}/service/${label}_node${rank}.log" 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
collect_gpu_snapshot() {
|
||||
local label="$1" rank host
|
||||
for rank in 0 1 2 3; do
|
||||
host="${NODE_HOSTS[$rank]}"
|
||||
sudo_host "$host" nvidia-smi \
|
||||
--query-gpu=timestamp,index,memory.used,memory.total,utilization.gpu,power.draw \
|
||||
--format=csv,noheader,nounits \
|
||||
>"${RESULT_ROOT}/gpu/${label}_node${rank}.csv" 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
start_node() {
|
||||
local rank="$1" pp="$2" tp="$3"
|
||||
local host="${NODE_HOSTS[$rank]}" name bootstrap
|
||||
name="$(container_name "$rank")"
|
||||
bootstrap="export SGLANG_HOST_IP=174.1.60.$((rank + 1)) && exec python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name ${SERVED_MODEL_NAME} --tp-size ${tp} --pp-size ${pp} --ep-size ${EP_SIZE} --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend flashinfer_mxfp4 --chunked-prefill-size ${CHUNKED_PREFILL_SIZE} --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}"
|
||||
local -a cmd=(
|
||||
docker run -d --name "$name"
|
||||
--gpus all --network host --ipc=host --ulimit memlock=-1
|
||||
--device /dev/infiniband --shm-size 32g --entrypoint bash
|
||||
-v "${MODEL_PATH}:${MODEL_PATH}:ro"
|
||||
-e CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
|
||||
-e NCCL_SOCKET_IFNAME=bond0 -e GLOO_SOCKET_IFNAME=bond0
|
||||
-e NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
|
||||
-e NCCL_IB_GID_INDEX=3 -e NCCL_IB_TIMEOUT=22 -e NCCL_IB_RETRY_CNT=7
|
||||
-e NCCL_CUMEM_ENABLE=1 -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
||||
-e SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0 -e SGLANG_MOE_FUSED_GATE_RADIX=1
|
||||
-e FLASHINFER_DISABLE_JIT=1 -e FLASHINFER_DISABLE_VERSION_CHECK=1
|
||||
"$DOCKER_IMAGE" -lc "$bootstrap"
|
||||
)
|
||||
printf '%q ' "${cmd[@]}" >"${RESULT_ROOT}/service/pp${pp}_node${rank}.cmd.txt"
|
||||
printf '\n' >>"${RESULT_ROOT}/service/pp${pp}_node${rank}.cmd.txt"
|
||||
sudo_host "$host" "${cmd[@]}" >/dev/null
|
||||
}
|
||||
|
||||
wait_health() {
|
||||
local pp="$1" i
|
||||
for ((i = 1; i <= HEALTH_WAIT_S; i++)); do
|
||||
if curl --fail --silent --max-time 5 "http://${HEAD_HOST}:${PORT}/health" >/dev/null 2>&1; then
|
||||
log "service healthy PP=${pp} wait_s=${i}"
|
||||
return 0
|
||||
fi
|
||||
if (( i % 30 == 0 )); then
|
||||
log "waiting for PP=${pp} service elapsed_s=${i}"
|
||||
collect_service_logs "pp${pp}_starting"
|
||||
if grep -Eiq 'Traceback|CUDA out of memory|NCCL.*(error|failed)|RuntimeError|AssertionError' \
|
||||
"${RESULT_ROOT}/service/pp${pp}_starting_node"*.log; then
|
||||
log "ERROR: PP=${pp} startup log contains a fatal error"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_server() {
|
||||
local pp="$1" tp="$2" info="${RESULT_ROOT}/metadata/pp${pp}_server_info.json"
|
||||
curl --fail --silent "http://${HEAD_HOST}:${PORT}/get_server_info" >"$info"
|
||||
python3 - "$info" "$pp" "$tp" "$EP_SIZE" <<'PY'
|
||||
import json, sys
|
||||
|
||||
path, pp, tp, ep = sys.argv[1], *map(int, sys.argv[2:])
|
||||
info = json.load(open(path, encoding="utf-8"))
|
||||
expected = {"pp_size": pp, "tp_size": tp, "ep_size": ep,
|
||||
"moe_runner_backend": "flashinfer_mxfp4"}
|
||||
bad = [f"{k}={info.get(k)!r}, expected={v!r}" for k, v in expected.items()
|
||||
if info.get(k) != v]
|
||||
if bad:
|
||||
raise SystemExit("server configuration mismatch: " + "; ".join(bad))
|
||||
print(expected)
|
||||
PY
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local pp="$1" tp
|
||||
tp="$(tp_for_pp "$pp")"
|
||||
stop_service
|
||||
log "starting PP=${pp} TP=${tp} EP=${EP_SIZE} on 601-604"
|
||||
start_node 1 "$pp" "$tp"
|
||||
start_node 2 "$pp" "$tp"
|
||||
start_node 3 "$pp" "$tp"
|
||||
sleep 5
|
||||
start_node 0 "$pp" "$tp"
|
||||
if ! wait_health "$pp"; then
|
||||
collect_service_logs "pp${pp}_startup_failed"
|
||||
stop_service
|
||||
return 1
|
||||
fi
|
||||
verify_server "$pp" "$tp"
|
||||
collect_service_logs "pp${pp}_healthy"
|
||||
collect_gpu_snapshot "pp${pp}_healthy"
|
||||
}
|
||||
|
||||
run_one_bench() {
|
||||
local pp="$1" concurrency="$2" repeat="$3"
|
||||
local stem="pp${pp}_c${concurrency}_r${repeat}"
|
||||
local output_file="${RESULT_ROOT}/raw/${stem}.jsonl"
|
||||
local bench_log="${RESULT_ROOT}/bench/${stem}.log"
|
||||
rm -f "$output_file"
|
||||
log "bench PP=${pp} C=${concurrency} repeat=${repeat}/${REPEATS}"
|
||||
sudo_host "$HEAD_HOST" docker run --rm --network host \
|
||||
-v "${MODEL_PATH}:${MODEL_PATH}:ro" \
|
||||
-v "${RESULT_ROOT}:${RESULT_ROOT}" \
|
||||
-e PYTHONUNBUFFERED=1 --entrypoint python3 "$DOCKER_IMAGE" \
|
||||
-m sglang.benchmark.serving \
|
||||
--backend sglang --host "$HEAD_HOST" --port "$PORT" \
|
||||
--tokenizer "$MODEL_PATH" --dataset-name random-ids \
|
||||
--random-input-len "$INPUT_LEN" --random-output-len "$OUTPUT_LEN" \
|
||||
--random-range-ratio 1.0 --num-prompts "$NUM_PROMPTS" \
|
||||
--max-concurrency "$concurrency" --request-rate 10000 \
|
||||
--warmup-requests "$WARMUP_REQUESTS" --output-file "$output_file" \
|
||||
--output-details --disable-tqdm >"$bench_log" 2>&1
|
||||
python3 - "$output_file" "$NUM_PROMPTS" <<'PY'
|
||||
import json, sys
|
||||
|
||||
path, expected = sys.argv[1], int(sys.argv[2])
|
||||
rows = [json.loads(line) for line in open(path, encoding="utf-8") if line.strip()]
|
||||
assert len(rows) == 1, (path, len(rows))
|
||||
assert rows[0].get("completed") == expected, rows[0].get("completed")
|
||||
assert not any(rows[0].get("errors", [])), "benchmark contains request errors"
|
||||
PY
|
||||
}
|
||||
|
||||
summarize() {
|
||||
python3 - "$RESULT_ROOT" <<'PY'
|
||||
import csv, json, re, statistics, sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
pattern = re.compile(r"pp(\d+)_c(\d+)_r(\d+)\.jsonl$")
|
||||
metrics = [
|
||||
"request_throughput", "input_throughput", "output_throughput",
|
||||
"total_throughput", "mean_ttft_ms", "median_ttft_ms", "p95_ttft_ms",
|
||||
"p99_ttft_ms", "mean_e2e_latency_ms", "median_e2e_latency_ms",
|
||||
"p95_e2e_latency_ms", "p99_e2e_latency_ms",
|
||||
]
|
||||
rows = []
|
||||
for path in sorted((root / "raw").glob("*.jsonl")):
|
||||
match = pattern.match(path.name)
|
||||
if not match:
|
||||
continue
|
||||
data = next(json.loads(line) for line in path.open(encoding="utf-8") if line.strip())
|
||||
pp, concurrency, repeat = map(int, match.groups())
|
||||
row = {"pp_size": pp, "tp_size": 32 // pp, "ep_size": 4,
|
||||
"concurrency": concurrency, "repeat": repeat,
|
||||
"completed": data.get("completed")}
|
||||
row.update({name: data.get(name) for name in metrics})
|
||||
rows.append(row)
|
||||
|
||||
fields = list(rows[0]) if rows else []
|
||||
with (root / "results.csv").open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fields)
|
||||
if fields:
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
groups = {}
|
||||
for row in rows:
|
||||
groups.setdefault((row["pp_size"], row["concurrency"]), []).append(row)
|
||||
summary = []
|
||||
for (pp, concurrency), group in sorted(groups.items()):
|
||||
item = {"pp_size": pp, "tp_size": 32 // pp, "ep_size": 4,
|
||||
"concurrency": concurrency, "repeats": len(group)}
|
||||
for name in metrics:
|
||||
values = [float(row[name]) for row in group if row.get(name) is not None]
|
||||
item[f"median_{name}"] = statistics.median(values) if values else None
|
||||
summary.append(item)
|
||||
|
||||
summary_fields = list(summary[0]) if summary else []
|
||||
with (root / "summary.csv").open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=summary_fields)
|
||||
if summary_fields:
|
||||
writer.writeheader()
|
||||
writer.writerows(summary)
|
||||
print(f"wrote {len(rows)} result rows and {len(summary)} summary rows")
|
||||
PY
|
||||
}
|
||||
|
||||
run_matrix() {
|
||||
preflight
|
||||
collect_gpu_snapshot before_all
|
||||
local pp concurrency repeat
|
||||
for pp in "${PP_SIZES[@]}"; do
|
||||
if ! start_service "$pp"; then
|
||||
log "ERROR: PP=${pp} failed to start; preserving evidence and continuing"
|
||||
continue
|
||||
fi
|
||||
for concurrency in "${CONCURRENCIES[@]}"; do
|
||||
for ((repeat = 1; repeat <= REPEATS; repeat++)); do
|
||||
run_one_bench "$pp" "$concurrency" "$repeat"
|
||||
done
|
||||
done
|
||||
collect_service_logs "pp${pp}_completed"
|
||||
collect_gpu_snapshot "pp${pp}_completed"
|
||||
stop_service
|
||||
sleep 5
|
||||
done
|
||||
collect_gpu_snapshot after_all
|
||||
summarize
|
||||
}
|
||||
|
||||
dry_run() {
|
||||
local pp tp
|
||||
for pp in "${PP_SIZES[@]}"; do
|
||||
tp="$(tp_for_pp "$pp")"
|
||||
echo "PP=${pp} TP=${tp} EP=${EP_SIZE} ${INPUT_LEN}->${OUTPUT_LEN} C=${CONCURRENCIES[*]} chunk=${CHUNKED_PREFILL_SIZE}"
|
||||
done
|
||||
bash -n "$0"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
dry-run) dry_run ;;
|
||||
run)
|
||||
trap stop_service EXIT INT TERM
|
||||
run_matrix
|
||||
;;
|
||||
summarize) summarize ;;
|
||||
stop)
|
||||
require_password
|
||||
stop_service
|
||||
;;
|
||||
*) usage; exit 2 ;;
|
||||
esac
|
||||
Loading…
x
Reference in New Issue
Block a user