# 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/<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 包含以下字段：

```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 是最常见原因）。
