# 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` 存在以下问题: 1. **低并发下无法测出真实吞吐**:并发数不大时,请求可能只落在一个服务上,其他 3 个服务空闲,bench 报告的吞吐只是单服务的 1/4。 2. **TTFT 测量不够精确**:`sglang.bench_serving` 的计时粒度较粗,对于低延迟场景(TP=2 下 TTFT 可能 < 100ms)误差较大。 3. **不支持多服务负载均衡**:只能测一个端口,无法反映整个 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` | ## 快速运行 ```bash # 完整流程(自动启动 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/ ``` ## 场景设计 本实验覆盖了从 **单并发** 到 **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 包含以下字段: ```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 不会中断。 ### 自定义故障参数 ```bash 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` 参数选择: ```bash python3 bench_client.py --lb-strategy round_robin --ports 30005,30006,30007,30008 ... ``` ## 输出格式 ### JSONL 原始数据 (`raw_outputs/vllm_*.jsonl`) 每行一个 JSON 对象,包含请求分发到的目标端口: ```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 } ``` 最后一行是汇总数据,包含 **各端口统计**: ```json { "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](../../BENCHMARK_WORKFLOW.md#final-json-schema)。 ### 人类可读报告 (`report.md`) Markdown 表格,包含所有场景的 TTFT/TPOT/E2E 均值与分位数,以及 SLO 达标状态。 ## 单独启动/停止 4 个服务 ```bash # 启动 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 是最常见原因)。