Skip to content

Commit 30a006b

Browse files
committed
[Misc] cxs: update param and docs
Signed-off-by: Sohei Koyama <skoyama@ddn.com>
1 parent 95b2939 commit 30a006b

5 files changed

Lines changed: 116 additions & 103 deletions

File tree

real-multi-round-qa/README.md

Lines changed: 79 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,74 @@
22

33
## Overview
44

5-
This benchmark is designed to identify **the maximum harmonic mean of user sessions $(C,S)$ that can be kept active while maintaining a steady-state TTFT ≤ 2 s (95-th percentile)**. By sweeping the concurrency ($C$) and sequential ($S$) independently, it isolates whether compute capacity or KV-cache pressure is the first limiting factor.
6-
5+
This benchmark is designed to explore how TTFT changes across different $(C, S)$ combinations by sweeping concurrency ($C$) and session depth ($S$) independently. This helps isolate whether compute capacity or KV-cache pressure is the primary limiting factor.
76

87
We highly recommend monitoring vLLM/LMCache/GPU/storage metrics at the same time.
98

109
This benchmark feeds full‑length novels to your LLM server and asks many follow‑up questions, just like a book critic. It is handy for testing long‑context handling and KV‑cache tools such as LMCache.
1110

12-
The benchmark is called CxS (pronounced six for simplicity), referring to the product of Concurrent $\times$ Sequential users.
13-
14-
### Definition
15-
16-
Let us define the set of candidate pairs:
17-
18-
$$
19-
\mathcal{D} = \{ (C_i, S_i) \mid \mathrm{TTFT}_{95}^{(i)} \leq 2 \}
20-
$$
21-
22-
### Objective
23-
24-
More precisely, we aim to find the pair that maximizes the harmonic mean among all candidates in $\mathcal{D}$:
25-
26-
27-
$$
28-
\underset{(C_i, S_i) \in \mathcal{D}}{\arg\max} \left( \frac{2 C_i S_i}{C_i + S_i} \right)
29-
$$
30-
31-
We use the harmonic mean to compare scores.
32-
As a business metric, we report the product, CxS.
33-
For example, we say "Our system can keep up to {C×S} user sessions active!"
11+
The benchmark is called CxS (pronounced six for simplicity), referring to the product of Concurrent $\times$ Session Depth.
3412

3513
## Two simple knobs
3614

3715
| Option | What it means |
3816
| ---- | ---- |
39-
| `--num-users-concurrent` (C) | How many threads run in parallel. |
40-
| `--num-users-sequential` (S) | How many users each thread serves in turn. |
17+
| `--concurrent` (C) | How many threads run in parallel. |
18+
| `--session-depth` (S) | How many sessions each thread serves in turn. |
4119

4220
You can:
43-
* raise concurrent to test compute-side capability (higher GPU utilization; total KV footprint also rises).
44-
* raise sequential to test KV-cache pressure (larger resident KV per GPU, little change in instantaneous GPU utilization).
21+
* raise $C$ to test compute-side capability (higher GPU utilization; total KV footprint also rises).
22+
* raise $S$ to test KV-cache pressure (larger resident KV per GPU, little change in instantaneous GPU utilization).
4523

4624
## Execution model
4725

4826
```
49-
Concurrent USER: {A,B}
50-
Sequential USER: {X,Y}
51-
All USER: {AX,AY,BX,BY}
27+
Concurrent: {A,B}
28+
Session Depth: {X,Y}
29+
All Session: {AX,AY,BX,BY}
5230
5331
Timeline
5432
-------------------------------------------------
5533
Thread A:
56-
Turn 0 → UserAX: Q1 "Read and summarize this novel. {AX novel contents}" → Get Response
57-
Turn 0 → UserAY: Q1 "Read and summarize this novel. {AY novel contents}" → Get Response
58-
Turn 1 → UserAX: Q2 "Write down the author's feelings." → Get Response
59-
Turn 1 → UserAY: Q2 "Write down the author's feelings." → Get Response
34+
Turn 0 → SessionAX: Q1 "Read and summarize this novel. {AX novel contents}" → Get Response
35+
Turn 0 → SessionAY: Q1 "Read and summarize this novel. {AY novel contents}" → Get Response
36+
Turn 1 → SessionAX: Q2 "Write down the author's feelings." → Get Response
37+
Turn 1 → SessionAY: Q2 "Write down the author's feelings." → Get Response
6038
...
6139
Thread B:
62-
Turn 0 → UserBX: Q1 "Read and summarize this novel. {BX novel contents}" → Get Response
63-
Turn 0 → UserBY: Q1 "Read and summarize this novel. {BY novel contents}" → Get Response
64-
Turn 1 → UserBX: Q2 "Write down the author's feelings." → Get Response
65-
Turn 1 → UserBY: Q2 "Write down the author's feelings." → Get Response
40+
Turn 0 → SessionBX: Q1 "Read and summarize this novel. {BX novel contents}" → Get Response
41+
Turn 0 → SessionBY: Q1 "Read and summarize this novel. {BY novel contents}" → Get Response
42+
Turn 1 → SessionBX: Q2 "Write down the author's feelings." → Get Response
43+
Turn 1 → SessionBY: Q2 "Write down the author's feelings." → Get Response
6644
...
6745
```
6846

47+
## For system competition
48+
49+
The CxS benchmark provides a scalar score to encourage healthy competition, but its use is not mandatory.
50+
51+
### Definition
52+
53+
Let us define the set of candidate pairs:
54+
55+
$$
56+
\mathcal{D} = {\{ (C_i, S_i) \mid \mathrm{TTFT}_{95}^{(i)} \leq 2 \}}
57+
$$
58+
59+
### Objective
60+
61+
More precisely, we aim to find the pair that maximizes the harmonic mean among all candidates in $\mathcal{D}$:
62+
63+
64+
$$
65+
\underset{(C_i, S_i) \in \mathcal{D}}{\arg\max} \left( \frac{2 C_i S_i}{C_i + S_i} \right)
66+
$$
67+
68+
## For business metric
69+
70+
As a business metric, we report the product, CxS.
71+
For example, we say "Our system can keep up to {C×S} user sessions active!"
72+
6973
## Getting Started
7074

7175
```bash
@@ -86,52 +90,52 @@ for c in {1..4}; do # You can change c and s to any value you like.
8690
for s in {1..4}; do
8791
TIMESTAMP=$(date +%s)
8892
OUTPUT_FILE="${OUTPUT_DIR}/bench_c${c}_s${s}_${TIMESTAMP}.json"
89-
echo "Running benchmark: concurrent=${c}, sequential=${s}"
90-
python multi-round-qa.py --num-users-concurrent "$c" --num-users-sequential "$s" --num-rounds "$NUM_ROUNDS" --model "$MODEL" --base-url "$BASE_URL" --output "$OUTPUT_FILE" --src-dir "$SRC_DIR"
93+
echo "Running benchmark: C=${c}, S=${s}"
94+
python multi-round-qa.py -c "$c" -s "$s" --num-rounds "$NUM_ROUNDS" --model "$MODEL" --base-url "$BASE_URL" --output "$OUTPUT_FILE" --src-dir "$SRC_DIR"
9195
done
9296
done
9397
```
9498

9599
```bash
96100
# Plot and Show Result
97101
$ python plot.py ./bench_dir_vllm vllm.png
98-
num_users_concurrent num_users_sequential ttft_95
99-
0 4 2 0.498404
100-
1 4 4 33.565437
101-
2 4 3 0.794144
102-
3 1 4 0.311046
103-
4 2 2 0.406148
104-
5 2 4 0.459704
105-
6 2 3 0.326396
106-
7 1 2 0.411317
107-
8 3 3 0.378674
108-
9 2 1 0.445499
109-
10 3 4 42.531053
110-
11 1 3 0.455651
111-
12 4 1 0.504505
112-
13 3 2 0.393902
113-
14 3 1 0.364927
114-
15 1 1 0.379049
102+
c s ttft_95
103+
0 4 2 0.498404
104+
1 4 4 33.565437
105+
2 4 3 0.794144
106+
3 1 4 0.311046
107+
4 2 2 0.406148
108+
5 2 4 0.459704
109+
6 2 3 0.326396
110+
7 1 2 0.411317
111+
8 3 3 0.378674
112+
9 2 1 0.445499
113+
10 3 4 42.531053
114+
11 1 3 0.455651
115+
12 4 1 0.504505
116+
13 3 2 0.393902
117+
14 3 1 0.364927
118+
15 1 1 0.379049
115119
Max harmonic mean (C,S) where TTFT_95 <= 2s: 3.43
116120
=> C=4.0, S=3.0, CxS=12.0
117121
$ python plot.py ./bench_dir_lmcache lmcache.png
118-
num_users_concurrent num_users_sequential ttft_95
119-
0 1 1 0.524989
120-
1 3 2 0.592148
121-
2 4 4 1.202544
122-
3 3 4 1.286755
123-
4 2 1 0.477370
124-
5 3 3 0.586793
125-
6 2 3 0.627655
126-
7 4 1 0.575724
127-
8 4 3 1.251918
128-
9 2 4 0.446477
129-
10 1 4 0.460711
130-
11 3 1 0.495073
131-
12 1 3 0.329389
132-
13 4 2 0.586223
133-
14 1 2 0.477946
134-
15 2 2 0.457463
122+
c s ttft_95
123+
0 1 1 0.524989
124+
1 3 2 0.592148
125+
2 4 4 1.202544
126+
3 3 4 1.286755
127+
4 2 1 0.477370
128+
5 3 3 0.586793
129+
6 2 3 0.627655
130+
7 4 1 0.575724
131+
8 4 3 1.251918
132+
9 2 4 0.446477
133+
10 1 4 0.460711
134+
11 3 1 0.495073
135+
12 1 3 0.329389
136+
13 4 2 0.586223
137+
14 1 2 0.477946
138+
15 2 2 0.457463
135139
Max harmonic mean (C,S) where TTFT_95 <= 2s: 4.00
136140
=> C=4.0, S=4.0, CxS=16.0
137141
```

real-multi-round-qa/lmcache.png

-28.6 KB
Loading

real-multi-round-qa/multi-round-qa.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ async def run_turn(session: ChatSession, client: openai.AsyncOpenAI) -> Result:
151151

152152
async def run_group(args) -> List[Result]:
153153
client = openai.AsyncOpenAI(base_url=args.base_url, api_key="EMPTY")
154-
sessions = [ChatSession(args) for _ in range(args.num_users_sequential)]
154+
sessions = [ChatSession(args) for _ in range(args.session_depth)]
155155
results = []
156156

157157
while any(not s.is_finished() for s in sessions):
@@ -164,14 +164,14 @@ async def run_group(args) -> List[Result]:
164164
return results
165165

166166
async def run_all_concurrent(args):
167-
tasks = [run_group(args) for _ in range(args.num_users_concurrent)]
167+
tasks = [run_group(args) for _ in range(args.concurrent)]
168168
all_results = await asyncio.gather(*tasks)
169169
return [asdict(r) for group in all_results for r in group]
170170

171171
def parse_args():
172172
parser = argparse.ArgumentParser()
173-
parser.add_argument("--num-users-concurrent", type=int, required=True)
174-
parser.add_argument("--num-users-sequential", type=int, required=True)
173+
parser.add_argument("-c", "--concurrent", type=int, required=True)
174+
parser.add_argument("-s", "--session-depth", type=int, required=True)
175175
parser.add_argument("--model", type=str, required=True)
176176
parser.add_argument("--base-url", type=str, required=True)
177177
parser.add_argument("--num-rounds", type=int, default=10)

real-multi-round-qa/plot.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
import pandas as pd
66
import numpy as np
77
import matplotlib.pyplot as plt
8+
from matplotlib.colors import LogNorm
89

910
def main():
1011
parser = argparse.ArgumentParser(description="Analyze number of user sessions from benchmark results.")
1112
parser.add_argument("input_dir", help="Directory containing JSON files")
12-
parser.add_argument("output", help="Output path for the 3D bar plot image")
13+
parser.add_argument("output", help="Output path for the contour plot image")
1314
args = parser.parse_args()
1415

1516
json_files = glob.glob(f"{args.input_dir}/*.json")
@@ -26,7 +27,7 @@ def main():
2627
results = data["results"]
2728

2829
params_fixed = {k: v for k, v in params.items()
29-
if k not in ["num_users_concurrent", "num_users_sequential", "output"]}
30+
if k not in ["concurrent", "session_depth", "output"]}
3031
all_params.append(params_fixed)
3132

3233
df = pd.DataFrame(results)
@@ -36,8 +37,8 @@ def main():
3637

3738
ttft_95 = df["ttft"].quantile(0.95)
3839
summary_records.append({
39-
"num_users_concurrent": params["num_users_concurrent"],
40-
"num_users_sequential": params["num_users_sequential"],
40+
"c": params["concurrent"],
41+
"s": params["session_depth"],
4142
"ttft_95": ttft_95
4243
})
4344

@@ -52,38 +53,46 @@ def main():
5253
print("No valid TTFT data to visualize.")
5354
return
5455

55-
summary_df_sorted = summary_df.sort_values(by=['num_users_concurrent', 'num_users_sequential'])
56+
C_vals = summary_df["c"].values
57+
S_vals = summary_df["s"].values
58+
TTFT_95_vals = summary_df["ttft_95"].values
5659

57-
fig = plt.figure()
58-
ax = fig.add_subplot(111, projection='3d')
60+
plt.figure(figsize=(9, 7))
61+
contour = plt.tricontourf(
62+
C_vals, S_vals, TTFT_95_vals,
63+
levels=np.logspace(np.log10(0.01), np.log10(100), 30),
64+
cmap='plasma',
65+
norm=LogNorm(vmin=0.01, vmax=100)
66+
)
5967

60-
x = summary_df_sorted['num_users_concurrent']
61-
y = summary_df_sorted['num_users_sequential']
62-
z = np.zeros_like(x)
63-
dz = summary_df_sorted['ttft_95']
68+
cbar = plt.colorbar(contour)
69+
ticks = [0.01, 0.1, 1, 2, 4, 8, 16, 32, 64, 100]
70+
cbar.set_ticks(ticks)
71+
cbar.set_ticklabels([str(t) for t in ticks])
72+
cbar.set_label('TTFT_95 (s)', fontsize=12)
6473

65-
colors = ['blue' if v <= 2 else 'red' for v in dz]
74+
plt.tricontour(C_vals, S_vals, TTFT_95_vals, levels=[2.0], colors='white', linewidths=2, linestyles='dashed')
6675

67-
ax.bar3d(x, y, z, dx=0.5, dy=0.5, dz=dz, color=colors, shade=True)
68-
ax.set_xlabel('Concurrent Users (C)')
69-
ax.set_ylabel('Sequential Users (S)')
70-
ax.set_zlabel('95% Tail TTFT (s)')
71-
plt.title('TTFT 95% Tail vs Concurrent/Sequential Users')
72-
ax.invert_xaxis()
73-
plt.savefig(args.output)
76+
plt.xlabel('Concurrent (C)', fontsize=12)
77+
plt.ylabel('Session Depth(S)', fontsize=12)
78+
plt.title('TTFT_95 Contour across (C, S)', fontsize=14)
79+
plt.grid(True, which='both', linestyle='--', alpha=0.3)
7480

75-
# Max harmonic mean under 2s TTFT
7681
summary_under_2s = summary_df[summary_df["ttft_95"] <= 2].copy()
7782
if not summary_under_2s.empty:
78-
summary_under_2s["harmonic_mean"] = 2 * summary_under_2s["num_users_concurrent"] * summary_under_2s["num_users_sequential"] / (
79-
summary_under_2s["num_users_concurrent"] + summary_under_2s["num_users_sequential"]
83+
summary_under_2s["harmonic_mean"] = 2 * summary_under_2s["c"] * summary_under_2s["s"] / (
84+
summary_under_2s["c"] + summary_under_2s["s"]
8085
)
8186
best_row = summary_under_2s.sort_values("harmonic_mean", ascending=False).iloc[0]
82-
product = best_row["num_users_concurrent"] * best_row["num_users_sequential"]
87+
product = best_row["c"] * best_row["s"]
8388
print(f"Max harmonic mean (C,S) where TTFT_95 <= 2s: {best_row['harmonic_mean']:.2f}")
84-
print(f" => C={best_row['num_users_concurrent']}, S={best_row['num_users_sequential']}, CxS={product}")
89+
print(f" => C={best_row['c']}, S={best_row['s']}, CxS={product}")
90+
plt.scatter([best_row['c']], [best_row['s']], c='cyan', edgecolors='black', s=120, marker='*', label='Best (C,S)')
91+
plt.legend()
92+
plt.autoscale()
8593
else:
8694
print("No data points with TTFT_95 <= 2s.")
95+
plt.savefig(args.output)
8796

8897
if __name__ == "__main__":
8998
main()

real-multi-round-qa/vllm.png

-31.2 KB
Loading

0 commit comments

Comments
 (0)