Skip to content

Commit 1c593e1

Browse files
Fix boolean nested params, add dict format support, and enhance plotting for vllm bench sweep (#29025)
Signed-off-by: Luka Govedič <luka.govedic@gmail.com> Signed-off-by: Luka Govedič <ProExpertProg@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ProExpertProg <11367180+ProExpertProg@users.noreply.github.com> Co-authored-by: Luka Govedič <luka.govedic@gmail.com> Co-authored-by: Luka Govedič <ProExpertProg@users.noreply.github.com>
1 parent a2b053d commit 1c593e1

File tree

5 files changed

+614
-22
lines changed

5 files changed

+614
-22
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
import json
4+
import tempfile
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from vllm.benchmarks.sweep.param_sweep import ParameterSweep, ParameterSweepItem
10+
11+
12+
class TestParameterSweepItem:
13+
"""Test ParameterSweepItem functionality."""
14+
15+
@pytest.mark.parametrize(
16+
"input_dict,expected",
17+
[
18+
(
19+
{"compilation_config.use_inductor_graph_partition": False},
20+
"--compilation-config.use_inductor_graph_partition=false",
21+
),
22+
(
23+
{"compilation_config.use_inductor_graph_partition": True},
24+
"--compilation-config.use_inductor_graph_partition=true",
25+
),
26+
(
27+
{"compilation_config.use_inductor": False},
28+
"--compilation-config.use_inductor=false",
29+
),
30+
(
31+
{"compilation_config.use_inductor": True},
32+
"--compilation-config.use_inductor=true",
33+
),
34+
],
35+
)
36+
def test_nested_boolean_params(self, input_dict, expected):
37+
"""Test that nested boolean params use =true/false syntax."""
38+
item = ParameterSweepItem.from_record(input_dict)
39+
cmd = item.apply_to_cmd(["vllm", "serve", "model"])
40+
assert expected in cmd
41+
42+
@pytest.mark.parametrize(
43+
"input_dict,expected",
44+
[
45+
({"enable_prefix_caching": False}, "--no-enable-prefix-caching"),
46+
({"enable_prefix_caching": True}, "--enable-prefix-caching"),
47+
({"disable_log_stats": False}, "--no-disable-log-stats"),
48+
({"disable_log_stats": True}, "--disable-log-stats"),
49+
],
50+
)
51+
def test_non_nested_boolean_params(self, input_dict, expected):
52+
"""Test that non-nested boolean params use --no- prefix."""
53+
item = ParameterSweepItem.from_record(input_dict)
54+
cmd = item.apply_to_cmd(["vllm", "serve", "model"])
55+
assert expected in cmd
56+
57+
@pytest.mark.parametrize(
58+
"compilation_config",
59+
[
60+
{"cudagraph_mode": "full", "mode": 2, "use_inductor_graph_partition": True},
61+
{
62+
"cudagraph_mode": "piecewise",
63+
"mode": 3,
64+
"use_inductor_graph_partition": False,
65+
},
66+
],
67+
)
68+
def test_nested_dict_value(self, compilation_config):
69+
"""Test that nested dict values are serialized as JSON."""
70+
item = ParameterSweepItem.from_record(
71+
{"compilation_config": compilation_config}
72+
)
73+
cmd = item.apply_to_cmd(["vllm", "serve", "model"])
74+
assert "--compilation-config" in cmd
75+
# The dict should be JSON serialized
76+
idx = cmd.index("--compilation-config")
77+
assert json.loads(cmd[idx + 1]) == compilation_config
78+
79+
@pytest.mark.parametrize(
80+
"input_dict,expected_key,expected_value",
81+
[
82+
({"model": "test-model"}, "--model", "test-model"),
83+
({"max_tokens": 100}, "--max-tokens", "100"),
84+
({"temperature": 0.7}, "--temperature", "0.7"),
85+
],
86+
)
87+
def test_string_and_numeric_values(self, input_dict, expected_key, expected_value):
88+
"""Test that string and numeric values are handled correctly."""
89+
item = ParameterSweepItem.from_record(input_dict)
90+
cmd = item.apply_to_cmd(["vllm", "serve"])
91+
assert expected_key in cmd
92+
assert expected_value in cmd
93+
94+
@pytest.mark.parametrize(
95+
"input_dict,expected_key,key_idx_offset",
96+
[
97+
({"max_tokens": 200}, "--max-tokens", 1),
98+
({"enable_prefix_caching": False}, "--no-enable-prefix-caching", 0),
99+
],
100+
)
101+
def test_replace_existing_parameter(self, input_dict, expected_key, key_idx_offset):
102+
"""Test that existing parameters in cmd are replaced."""
103+
item = ParameterSweepItem.from_record(input_dict)
104+
105+
if key_idx_offset == 1:
106+
# Key-value pair
107+
cmd = item.apply_to_cmd(["vllm", "serve", "--max-tokens", "100", "model"])
108+
assert expected_key in cmd
109+
idx = cmd.index(expected_key)
110+
assert cmd[idx + 1] == "200"
111+
assert "100" not in cmd
112+
else:
113+
# Boolean flag
114+
cmd = item.apply_to_cmd(
115+
["vllm", "serve", "--enable-prefix-caching", "model"]
116+
)
117+
assert expected_key in cmd
118+
assert "--enable-prefix-caching" not in cmd
119+
120+
121+
class TestParameterSweep:
122+
"""Test ParameterSweep functionality."""
123+
124+
def test_from_records_list(self):
125+
"""Test creating ParameterSweep from a list of records."""
126+
records = [
127+
{"max_tokens": 100, "temperature": 0.7},
128+
{"max_tokens": 200, "temperature": 0.9},
129+
]
130+
sweep = ParameterSweep.from_records(records)
131+
assert len(sweep) == 2
132+
assert sweep[0]["max_tokens"] == 100
133+
assert sweep[1]["max_tokens"] == 200
134+
135+
def test_read_from_dict(self):
136+
"""Test creating ParameterSweep from a dict format."""
137+
data = {
138+
"experiment1": {"max_tokens": 100, "temperature": 0.7},
139+
"experiment2": {"max_tokens": 200, "temperature": 0.9},
140+
}
141+
sweep = ParameterSweep.read_from_dict(data)
142+
assert len(sweep) == 2
143+
144+
# Check that items have the _benchmark_name field
145+
names = {item["_benchmark_name"] for item in sweep}
146+
assert names == {"experiment1", "experiment2"}
147+
148+
# Check that parameters are preserved
149+
for item in sweep:
150+
if item["_benchmark_name"] == "experiment1":
151+
assert item["max_tokens"] == 100
152+
assert item["temperature"] == 0.7
153+
elif item["_benchmark_name"] == "experiment2":
154+
assert item["max_tokens"] == 200
155+
assert item["temperature"] == 0.9
156+
157+
def test_read_json_list_format(self):
158+
"""Test reading JSON file with list format."""
159+
records = [
160+
{"max_tokens": 100, "temperature": 0.7},
161+
{"max_tokens": 200, "temperature": 0.9},
162+
]
163+
164+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
165+
json.dump(records, f)
166+
temp_path = Path(f.name)
167+
168+
try:
169+
sweep = ParameterSweep.read_json(temp_path)
170+
assert len(sweep) == 2
171+
assert sweep[0]["max_tokens"] == 100
172+
assert sweep[1]["max_tokens"] == 200
173+
finally:
174+
temp_path.unlink()
175+
176+
def test_read_json_dict_format(self):
177+
"""Test reading JSON file with dict format."""
178+
data = {
179+
"experiment1": {"max_tokens": 100, "temperature": 0.7},
180+
"experiment2": {"max_tokens": 200, "temperature": 0.9},
181+
}
182+
183+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
184+
json.dump(data, f)
185+
temp_path = Path(f.name)
186+
187+
try:
188+
sweep = ParameterSweep.read_json(temp_path)
189+
assert len(sweep) == 2
190+
191+
# Check that items have the _benchmark_name field
192+
names = {item["_benchmark_name"] for item in sweep}
193+
assert names == {"experiment1", "experiment2"}
194+
finally:
195+
temp_path.unlink()
196+
197+
def test_unique_benchmark_names_validation(self):
198+
"""Test that duplicate _benchmark_name values raise an error."""
199+
# Test with duplicate names in list format
200+
records = [
201+
{"_benchmark_name": "exp1", "max_tokens": 100},
202+
{"_benchmark_name": "exp1", "max_tokens": 200},
203+
]
204+
205+
with pytest.raises(ValueError, match="Duplicate _benchmark_name values"):
206+
ParameterSweep.from_records(records)
207+
208+
def test_unique_benchmark_names_multiple_duplicates(self):
209+
"""Test validation with multiple duplicate names."""
210+
records = [
211+
{"_benchmark_name": "exp1", "max_tokens": 100},
212+
{"_benchmark_name": "exp1", "max_tokens": 200},
213+
{"_benchmark_name": "exp2", "max_tokens": 300},
214+
{"_benchmark_name": "exp2", "max_tokens": 400},
215+
]
216+
217+
with pytest.raises(ValueError, match="Duplicate _benchmark_name values"):
218+
ParameterSweep.from_records(records)
219+
220+
def test_no_benchmark_names_allowed(self):
221+
"""Test that records without _benchmark_name are allowed."""
222+
records = [
223+
{"max_tokens": 100, "temperature": 0.7},
224+
{"max_tokens": 200, "temperature": 0.9},
225+
]
226+
sweep = ParameterSweep.from_records(records)
227+
assert len(sweep) == 2
228+
229+
def test_mixed_benchmark_names_allowed(self):
230+
"""Test that mixing records with and without _benchmark_name is allowed."""
231+
records = [
232+
{"_benchmark_name": "exp1", "max_tokens": 100},
233+
{"max_tokens": 200, "temperature": 0.9},
234+
]
235+
sweep = ParameterSweep.from_records(records)
236+
assert len(sweep) == 2
237+
238+
239+
class TestParameterSweepItemKeyNormalization:
240+
"""Test key normalization in ParameterSweepItem."""
241+
242+
def test_underscore_to_hyphen_conversion(self):
243+
"""Test that underscores are converted to hyphens in CLI."""
244+
item = ParameterSweepItem.from_record({"max_tokens": 100})
245+
cmd = item.apply_to_cmd(["vllm", "serve"])
246+
assert "--max-tokens" in cmd
247+
248+
def test_nested_key_preserves_suffix(self):
249+
"""Test that nested keys preserve the suffix format."""
250+
# The suffix after the dot should preserve underscores
251+
item = ParameterSweepItem.from_record(
252+
{"compilation_config.some_nested_param": "value"}
253+
)
254+
cmd = item.apply_to_cmd(["vllm", "serve"])
255+
# The prefix (compilation_config) gets converted to hyphens,
256+
# but the suffix (some_nested_param) is preserved
257+
assert any("compilation-config.some_nested_param" in arg for arg in cmd)

0 commit comments

Comments
 (0)