@@ -120,3 +120,191 @@ def test_max_requests_benchmark(server: VllmSimServer, tmp_path: Path):
120120 f"Expected { max_requests } successful requests, got { len (successful_requests )} "
121121 )
122122 assert_successful_requests_fields (successful_requests )
123+
124+
125+ @pytest .mark .timeout (30 )
126+ @pytest .mark .sanity
127+ def test_replay_profile_benchmark (server : VllmSimServer , tmp_path : Path ):
128+ """
129+ Test trace replay profile with a simple trace file.
130+ Validates that requests are replayed with correct timing from trace.
131+ Also tests time_scale (rate) functionality.
132+ """
133+ report_name = "replay_benchmarks.json"
134+ report_path = tmp_path / report_name
135+
136+ # Create trace file with 5 requests at 0.05s intervals
137+ trace_file = _create_trace_file (tmp_path , num_requests = 5 , interval = 0.05 )
138+
139+ # Create and configure the guidellm client with replay profile
140+ client = GuidellmClient (
141+ target = server .get_url (),
142+ output_dir = tmp_path ,
143+ outputs = report_name ,
144+ )
145+
146+ # Start the benchmark with replay profile
147+ # rate=2.0 means time_scale=2.0 (timestamps multiplied by 2)
148+ client .start_benchmark (
149+ profile = "replay" ,
150+ rate = 2.0 ,
151+ max_requests = 5 ,
152+ data = str (trace_file ),
153+ processor = "gpt2" ,
154+ )
155+
156+ # Wait for the benchmark to complete
157+ client .wait_for_completion (timeout = 30 )
158+
159+ # Assert no Python exceptions occurred
160+ assert_no_python_exceptions (client .stderr )
161+
162+ # Load and validate the report
163+ report = load_benchmark_report (report_path )
164+ assert len (report ["benchmarks" ]) == 1
165+
166+ benchmark = report ["benchmarks" ][0 ]
167+
168+ # Validate successful requests have all expected fields
169+ successful_requests = benchmark ["requests" ]["successful" ]
170+ assert len (successful_requests ) == 5 , (
171+ f"Expected 5 successful requests, got { len (successful_requests )} "
172+ )
173+ assert_successful_requests_fields (successful_requests )
174+
175+ # Verify scheduler state shows correct request count
176+ assert "scheduler_state" in benchmark
177+ scheduler_state = benchmark ["scheduler_state" ]
178+ assert scheduler_state ["processed_requests" ] == 5
179+
180+
181+ @pytest .mark .timeout (30 )
182+ @pytest .mark .sanity
183+ def test_replay_profile_max_requests_stronger_than_max_seconds (
184+ server : VllmSimServer , tmp_path : Path
185+ ):
186+ """
187+ Test replay profile where max_requests is the limiting constraint.
188+ Trace has 20 requests over 2 seconds, but max_requests=5 limits to 5.
189+ max_seconds=10 is not reached because max_requests triggers first.
190+ """
191+ report_name = "replay_max_requests_stronger.json"
192+ report_path = tmp_path / report_name
193+
194+ # Create trace with 20 requests at 0.1s intervals (total 1.9s)
195+ trace_file = _create_trace_file (tmp_path , num_requests = 20 , interval = 0.1 )
196+
197+ client = GuidellmClient (
198+ target = server .get_url (),
199+ output_dir = tmp_path ,
200+ outputs = report_name ,
201+ )
202+
203+ # max_requests=5 should be the limiting constraint
204+ # max_seconds=10 should NOT be reached
205+ client .start_benchmark (
206+ profile = "replay" ,
207+ rate = 1.0 ,
208+ max_requests = 5 ,
209+ max_seconds = 10 , # Very high, won't be reached
210+ data = str (trace_file ),
211+ processor = "gpt2" ,
212+ )
213+
214+ client .wait_for_completion (timeout = 30 )
215+ assert_no_python_exceptions (client .stderr )
216+
217+ report = load_benchmark_report (report_path )
218+ benchmark = report ["benchmarks" ][0 ]
219+
220+ # Should only have 5 requests (max_requests won)
221+ successful_requests = benchmark ["requests" ]["successful" ]
222+ assert len (successful_requests ) == 5 , (
223+ f"Expected 5 requests (max_requests limit), got { len (successful_requests )} "
224+ )
225+
226+ # Verify max_requests constraint was triggered
227+ assert_constraint_triggered (benchmark , "max_requests" , {"processed_exceeded" : True })
228+
229+
230+ @pytest .mark .timeout (30 )
231+ @pytest .mark .sanity
232+ def test_replay_profile_max_seconds_stronger_than_max_requests (
233+ server : VllmSimServer , tmp_path : Path
234+ ):
235+ """
236+ Test replay profile where max_seconds is the limiting constraint.
237+ Trace has 20 requests over 2 seconds, but max_seconds=0.3 limits to ~3 requests.
238+ max_requests=10 is not reached because max_seconds triggers first.
239+ """
240+ report_name = "replay_max_seconds_stronger.json"
241+ report_path = tmp_path / report_name
242+
243+ # Create trace with 20 requests at 0.1s intervals
244+ # With time_scale=1.0, timestamps are: 0.0, 0.1, 0.2, 0.3, 0.4, ...
245+ # max_seconds=0.25 should include: 0.0, 0.1, 0.2 (3 requests, 0.3 > 0.25)
246+ trace_file = _create_trace_file (tmp_path , num_requests = 20 , interval = 0.1 )
247+
248+ client = GuidellmClient (
249+ target = server .get_url (),
250+ output_dir = tmp_path ,
251+ outputs = report_name ,
252+ )
253+
254+ # max_seconds=0.25 should be the limiting constraint
255+ # Only timestamps <= 0.25 should be kept: 0.0, 0.1, 0.2
256+ client .start_benchmark (
257+ profile = "replay" ,
258+ rate = 1.0 ,
259+ max_requests = 10 , # High, won't be reached
260+ max_seconds = 0.25 ,
261+ data = str (trace_file ),
262+ processor = "gpt2" ,
263+ )
264+
265+ client .wait_for_completion (timeout = 30 )
266+ assert_no_python_exceptions (client .stderr )
267+
268+ report = load_benchmark_report (report_path )
269+ benchmark = report ["benchmarks" ][0 ]
270+
271+ # Should have 3 requests (0.0, 0.1, 0.2 where 0.2 <= 0.25)
272+ successful_requests = benchmark ["requests" ]["successful" ]
273+ assert len (successful_requests ) == 3 , (
274+ f"Expected 3 requests (max_seconds=0.25 filter), got { len (successful_requests )} "
275+ )
276+
277+ # Verify max_requests constraint was triggered
278+ # (max_seconds is converted to max_requests internally)
279+ assert_constraint_triggered (benchmark , "max_requests" , {"processed_exceeded" : True })
280+
281+
282+ # Helper functions for trace file creation
283+
284+
285+ def _create_trace_file (
286+ tmp_path : Path , num_requests : int = 5 , interval : float = 0.1
287+ ) -> Path :
288+ """Create a trace file with evenly spaced timestamps for testing."""
289+ trace_file = tmp_path / "trace.jsonl"
290+ lines = [
291+ f'{{"timestamp": { i * interval } , '
292+ f'"input_length": { 10 * (i + 1 )} , '
293+ f'"output_length": { 5 * (i + 1 )} }}'
294+ for i in range (num_requests )
295+ ]
296+ trace_file .write_text ("\n " .join (lines ))
297+ return trace_file
298+
299+
300+ def _create_burst_trace_file (tmp_path : Path , num_requests : int = 10 ) -> Path :
301+ """Create a trace file with all requests at the same timestamp."""
302+ trace_file = tmp_path / "trace_burst.jsonl"
303+ lines = [
304+ f'{{"timestamp": 0.0, '
305+ f'"input_length": { 20 * (i + 1 )} , '
306+ f'"output_length": { 10 * (i + 1 )} }}'
307+ for i in range (num_requests )
308+ ]
309+ trace_file .write_text ("\n " .join (lines ))
310+ return trace_file
0 commit comments