- Implemented a bash script to run six benchmark experiments sequentially. - Added features for fault tolerance, out-of-memory recovery, and checkpoint management. - Included detailed logging for monitoring experiment progress and results. - Integrated GPU memory cleanup checks before each experiment. - Provided functionality to resume experiments from checkpoints.
dsv4_h200_vllm_tp2_custom_bench
目的
当 vLLM 使用 TP=2(Tensor Parallel = 2)部署时,单张 H200 上可以同时运行 4 个独立服务(8 卡 / 2 = 4 服务),每个服务独占 2 张 GPU:
| 服务 | 端口 | GPU 对 |
|---|---|---|
| 1 | 30005 | 0, 1 |
| 2 | 30006 | 2, 3 |
| 3 | 30007 | 4, 5 |
| 4 | 30008 | 6, 7 |
此时 sglang.bench_serving 存在以下问题:
- 低并发下无法测出真实吞吐:并发数不大时,请求可能只落在一个服务上,其他 3 个服务空闲,bench 报告的吞吐只是单服务的 1/4。
- TTFT 测量不够精确:
sglang.bench_serving的计时粒度较粗,对于低延迟场景(TP=2 下 TTFT 可能 < 100ms)误差较大。 - 不支持多服务负载均衡:只能测一个端口,无法反映整个 8 卡集群性能。
因此本实验使用 自定义压测客户端 bench_client.py,直接调用 OpenAI /v1/chat/completions API,通过 aiohttp 并发发送请求,并记录每个请求的精确时间戳。同时通过 负载均衡 将请求均匀分发到 4 个服务上。
与 sglang.bench_serving 的区别
| 特性 | sglang.bench_serving | bench_client.py (本实验) |
|---|---|---|
| 协议 | 内部协议 | OpenAI API (/v1/chat/completions) |
| 并发控制 | 进程级 | asyncio.Semaphore |
| TTFT 测量 | 粗略 | 精确到 time.perf_counter() |
| TPOT 测量 | 基于总时间估算 | 基于首 token 后每个 token 的间隔 |
| 多服务支持 | ❌ 单端口 | ✅ 轮询/随机分发到多个端口 |
| 低并发精度 | 一般 | 高(适合 TP=2 场景) |
| 集群吞吐 | 只能测单服务 | 可测 4 服务总吞吐 |
文件说明
| 文件 | 说明 |
|---|---|
bench_client.py |
自定义异步压测客户端,支持多服务负载均衡 |
config.env |
实验配置(场景、模型路径、端口列表、LB 策略等) |
run_bench.sh |
编排脚本:启动 4 个服务 → 运行压测 → 解析结果 |
start_server.sh |
同时启动 4 个 vLLM TP=2 服务(每服务 2 GPU) |
parse_results.py |
解析 bench_client.py 输出的 JSONL,生成 results.json + report.md |
快速运行
# 完整流程(自动启动 4 个服务、压测、生成报告)
bash experiments/dsv4_h200_vllm_tp2_custom_bench/run_bench.sh
# 跳过服务管理(已有 4 个服务在运行)
SKIP_MANAGE_SERVER=1 bash experiments/dsv4_h200_vllm_tp2_custom_bench/run_bench.sh
# 单独运行压测客户端(多服务,带健康检查)
python3 experiments/dsv4_h200_vllm_tp2_custom_bench/bench_client.py \
--host 127.0.0.1 \
--ports 30005,30006,30007,30008 \
--model deepseek-v4-flash \
--concurrency 32 --input-len 512 --output-len 256 --num-prompts 640 \
--lb-strategy round_robin \
--health-check-interval 5 \
--health-max-failures 2 \
--health-recovery-interval 10 \
--request-max-retries 2 \
--output-file /tmp/test.jsonl
# 单独运行压测客户端(单服务,向后兼容)
python3 experiments/dsv4_h200_vllm_tp2_custom_bench/bench_client.py \
--host 127.0.0.1 --port 30005 \
--model deepseek-v4-flash \
--concurrency 8 --input-len 512 --output-len 256 --num-prompts 160 \
--output-file /tmp/test_single.jsonl
# 解析已有结果
python3 experiments/dsv4_h200_vllm_tp2_custom_bench/parse_results.py \
experiments/dsv4_h200_vllm_tp2_custom_bench/results/<RUN_ID>
场景设计
本实验覆盖了从 单并发 到 128 并发 的梯度,以及不同输入/输出长度组合:
| 并发 | 输入长度 | 输出长度 | 请求数 | 说明 |
|---|---|---|---|---|
| 1 | 512 | 256 | 20 | 单并发基线,测纯延迟 |
| 4 | 512 | 256 | 80 | 低并发,4 服务各 1 req |
| 8 | 512 | 256 | 160 | 中低并发 |
| 16 | 512 | 256 | 320 | 中等并发 |
| 32 | 512 | 256 | 640 | 中高并发 |
| 64 | 512 | 256 | 1280 | 高并发 |
| 128 | 512 | 256 | 2560 | 极限并发,测集群吞吐上限 |
| 1 | 2048 | 512 | 20 | 长输入基线 |
| 8 | 2048 | 512 | 160 | 长输入中并发 |
| 32 | 2048 | 512 | 640 | 长输入高并发 |
| 128 | 2048 | 512 | 2560 | 长输入极限并发 |
| 1 | 4096 | 1024 | 20 | 超长输入基线 |
| 8 | 4096 | 1024 | 160 | 超长输入中并发 |
| 32 | 4096 | 1024 | 640 | 超长输入高并发 |
| 128 | 4096 | 1024 | 2560 | 超长输入极限并发 |
健康检查与故障自动跳过
bench_client.py 内置了 后台健康检查 + 请求级重试 机制,确保某个服务挂了不会导致整个 benchmark 失败。
健康检查机制
| 参数 | 默认值 | 说明 |
|---|---|---|
--health-check-interval |
5.0 | 每 N 秒检查一次所有服务 |
--health-max-failures |
2 | 连续失败 N 次才标记为不健康 |
--health-recovery-interval |
10.0 | 不健康服务 N 秒后自动重试恢复 |
--request-max-retries |
2 | 单个请求失败最多重试 N 次 |
故障处理流程
请求 → 负载均衡选择端口 → 发送请求
↓
成功 → 记录结果
↓
失败 → 换另一个端口重试(最多 --request-max-retries 次)
↓
全部失败 → 记录失败,继续下一个请求
后台同时运行健康检查:
- 每 5 秒对所有端口执行
/health探测 - 连续 2 次失败的服务被标记为 unhealthy,不再接收新请求
- 10 秒后自动尝试恢复,如果恢复成功则重新加入负载均衡池
输出中的故障信息
汇总 JSON 包含以下字段:
{
"completed": 638,
"failed": 2,
"retried": 5,
"healthy_ports_at_end": [30005, 30006, 30007],
"per_port_stats": {
"30005": {"count": 160, "failed": 0, ...},
"30006": {"count": 159, "failed": 1, ...},
"30007": {"count": 160, "failed": 0, ...},
"30008": {"count": 159, "failed": 1, ...}
}
}
failed: 最终失败的请求数(重试后仍失败)retried: 至少重试过一次的请求数healthy_ports_at_end: 压测结束时仍健康的服务端口per_port_stats[].failed: 每个端口的失败数
场景间健康检查
run_bench.sh 在每个场景结束后会打印一次服务健康状态:
[2026-07-10T08:45:00+00:00] service health check: healthy=[30005 30006 30007 30008] unhealthy=[]
[2026-07-10T08:45:30+00:00] service health check: healthy=[30005 30006 30007] unhealthy=[30008]
[2026-07-10T08:45:30+00:00] WARNING: 1 service(s) unhealthy: 30008
这样即使某个服务在压测中途 OOM 崩溃,其他服务仍能继续工作,benchmark 不会中断。
自定义故障参数
python3 bench_client.py \
--ports 30005,30006,30007,30008 \
--health-check-interval 3 \
--health-max-failures 1 \
--health-recovery-interval 5 \
--request-max-retries 3 \
...
- 更敏感:间隔 3 秒、1 次失败即标记不健康、5 秒恢复
- 更容错:单个请求最多重试 3 次
负载均衡策略
bench_client.py 支持三种负载均衡策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
round_robin |
请求按顺序轮询分配到各端口 | 默认,最公平,推荐 |
random |
随机选择一个端口 | 简单,分布可能不均 |
least_conn |
选择当前连接数最少的端口 | 需要追踪 in-flight,当前为 random 占位 |
通过 --lb-strategy 参数选择:
python3 bench_client.py --lb-strategy round_robin --ports 30005,30006,30007,30008 ...
输出格式
JSONL 原始数据 (raw_outputs/vllm_*.jsonl)
每行一个 JSON 对象,包含请求分发到的目标端口:
{
"request_id": 0,
"input_tokens": 512,
"output_tokens": 256,
"first_token_time_ms": 45.23,
"e2e_latency_ms": 5234.56,
"inter_token_latencies": [18.5, 19.2, ...],
"tpot_ms": 19.8,
"mean_itl_ms": 19.5,
"success": true,
"error": "",
"target_port": 30005
}
最后一行是汇总数据,包含 各端口统计:
{
"completed": 640,
"failed": 0,
"duration": 12.34,
"request_throughput": 51.88,
"mean_ttft_ms": 48.5,
"p95_ttft_ms": 62.3,
"per_port_stats": {
"30005": {"count": 160, "mean_ttft_ms": 47.2, "p95_ttft_ms": 58.1, "mean_e2e_ms": 5200.0},
"30006": {"count": 160, "mean_ttft_ms": 49.1, "p95_ttft_ms": 64.3, "mean_e2e_ms": 5250.0},
"30007": {"count": 160, "mean_ttft_ms": 48.8, "p95_ttft_ms": 61.5, "mean_e2e_ms": 5230.0},
"30008": {"count": 160, "mean_ttft_ms": 48.9, "p95_ttft_ms": 65.2, "mean_e2e_ms": 5270.0}
},
"lb_strategy": "round_robin",
"num_services": 4
}
结构化结果 (results.json)
遵循仓库统一的 JSON Schema。
人类可读报告 (report.md)
Markdown 表格,包含所有场景的 TTFT/TPOT/E2E 均值与分位数,以及 SLO 达标状态。
单独启动/停止 4 个服务
# 启动 4 个服务
bash experiments/dsv4_h200_vllm_tp2_custom_bench/start_server.sh
# 停止(run_bench.sh 会自动停止,但也可以手动)
for port in 30005 30006 30007 30008; do
pid_file="experiments/dsv4_h200_vllm_tp2_custom_bench/server_port${port}.pid"
[[ -f "$pid_file" ]] && kill "$(cat "$pid_file")" 2>/dev/null || true
rm -f "$pid_file"
done
pkill -9 -f "vllm serve.*DeepSeek-V4-Flash" 2>/dev/null || true
注意事项
bench_client.py优先使用aiohttp,如果未安装则自动回退到urllib(但会丢失 streaming 和精确 TTFT/ITL 测量)。- 默认
temperature=0.0以保证输出长度稳定。 - 输入 token 数通过空格分隔的单词数估算,实际 tokenizer 可能略有偏差。
- 4 个服务共享同一模型文件,确保
/data/models/DeepSeek-V4-Flash存在且可访问。 - 每个服务占用约 2 张 GPU 的 90% 显存,确保无其他进程占用 GPU。
- 健康检查依赖
/health端点,确保 vLLM 服务已启用该端点(vLLM 默认启用)。 - 如果某个服务频繁崩溃,检查 GPU 显存是否充足(OOM 是最常见原因)。