main
py 290 lines 9.22 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from vllm.v1.core.sched.output import ScheduledEncoderInputStats, SchedulerOutput
4 from vllm.v1.engine import EngineCoreOutputs, FinishReason
5 from vllm.v1.metrics.stats import (
6 IterationStats,
7 PrefillStats,
8 PromptTokenStats,
9 RequestStateStats,
10 SchedulerIterationDetails,
11 SchedulerStats,
12 )
13 from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
14 from vllm.v1.utils import compute_iteration_details
15
16
17 def test_iteration_stats_repr():
18 iteration_stats = IterationStats()
19 assert repr(iteration_stats).startswith("IterationStats(")
20
21
22 def test_scheduler_iteration_details_serialization():
23 iteration_details = SchedulerIterationDetails(
24 iteration_index=1,
25 num_ctx_requests=2,
26 num_ctx_tokens=3,
27 num_generation_requests=4,
28 num_generation_tokens=5,
29 elapsed_ms=6.7,
30 num_encoder_inputs=2,
31 num_encoder_output_tokens=392,
32 )
33 outputs = EngineCoreOutputs(
34 scheduler_stats=SchedulerStats(
35 kv_cache_usage=0.5,
36 iteration_details=iteration_details,
37 )
38 )
39
40 encoded = MsgpackEncoder().encode(outputs)
41 decoded = MsgpackDecoder(EngineCoreOutputs).decode(encoded)
42
43 assert decoded.scheduler_stats is not None
44 assert decoded.scheduler_stats.kv_cache_usage == 0.5
45 assert decoded.scheduler_stats.iteration_details == iteration_details
46
47
48 def test_compute_iteration_details_includes_encoder_stats():
49 scheduler_output = SchedulerOutput.make_empty()
50 scheduler_output.scheduled_encoder_input_stats = ScheduledEncoderInputStats(
51 num_inputs=2,
52 output_tokens=392,
53 )
54
55 iteration_details = compute_iteration_details(scheduler_output)
56
57 assert iteration_details.num_encoder_inputs == 2
58 assert iteration_details.num_encoder_output_tokens == 392
59
60
61 def test_prefill_kv_computed_with_cache():
62 """Test that prefill KV compute correctly excludes cached tokens."""
63 iteration_stats = IterationStats()
64 req_stats = RequestStateStats(arrival_time=0.0)
65 req_stats.scheduled_ts = 0.1
66 req_stats.first_token_ts = 0.5
67 req_stats.last_token_ts = 5.0
68 req_stats.num_generation_tokens = 50
69
70 # Case 1: With prefix cache (1200 tokens cached)
71 iteration_stats.update_from_finished_request(
72 finish_reason=FinishReason.STOP,
73 request_id="test-req-001",
74 num_prompt_tokens=10000,
75 max_tokens_param=100,
76 req_stats=req_stats,
77 num_cached_tokens=1200,
78 )
79
80 finished_req = iteration_stats.finished_requests[0]
81 assert finished_req.num_prompt_tokens == 10000
82 assert finished_req.num_cached_tokens == 1200
83 assert finished_req.request_id == "test-req-001"
84
85 # Verify calculation: prefill KV = prompt tokens - cached tokens
86 prefill_kv_computed = finished_req.num_prompt_tokens - max(
87 finished_req.num_cached_tokens, 0
88 )
89 assert prefill_kv_computed == 8800 # 10000 - 1200
90
91
92 def test_prefill_kv_computed_no_cache():
93 """Test prefill KV compute without prefix caching."""
94 iteration_stats = IterationStats()
95 req_stats = RequestStateStats(arrival_time=0.0)
96 req_stats.scheduled_ts = 0.1
97 req_stats.first_token_ts = 0.5
98 req_stats.last_token_ts = 2.0
99 req_stats.num_generation_tokens = 10
100
101 # Case 2: No prefix cache
102 iteration_stats.update_from_finished_request(
103 finish_reason=FinishReason.STOP,
104 request_id="test-req-002",
105 num_prompt_tokens=2000,
106 max_tokens_param=100,
107 req_stats=req_stats,
108 num_cached_tokens=0,
109 )
110
111 finished_req = iteration_stats.finished_requests[0]
112 assert finished_req.num_prompt_tokens == 2000
113 assert finished_req.num_cached_tokens == 0
114 assert finished_req.request_id == "test-req-002"
115
116 # Verify calculation: prefill KV = full prompt when no cache
117 prefill_kv_computed = finished_req.num_prompt_tokens - max(
118 finished_req.num_cached_tokens, 0
119 )
120 assert prefill_kv_computed == 2000
121
122
123 def test_prefill_kv_computed_edge_cases():
124 """Test edge cases for prefill KV compute calculation."""
125 iteration_stats = IterationStats()
126 req_stats = RequestStateStats(arrival_time=0.0)
127 req_stats.scheduled_ts = 0.1
128 req_stats.first_token_ts = 0.5
129 req_stats.last_token_ts = 1.0
130 req_stats.num_generation_tokens = 1
131
132 # Case 3: Negative num_cached_tokens (shouldn't happen, but handle gracefully)
133 iteration_stats.update_from_finished_request(
134 finish_reason=FinishReason.STOP,
135 request_id="test-req-003",
136 num_prompt_tokens=100,
137 max_tokens_param=10,
138 req_stats=req_stats,
139 num_cached_tokens=-1,
140 )
141
142 finished_req = iteration_stats.finished_requests[0]
143 # max() should handle negative values
144 prefill_kv_computed = finished_req.num_prompt_tokens - max(
145 finished_req.num_cached_tokens, 0
146 )
147 assert prefill_kv_computed == 100 # Should treat negative as 0
148 assert finished_req.request_id == "test-req-003"
149
150 # Case 4: All tokens cached (shouldn't happen in practice)
151 iteration_stats2 = IterationStats()
152 iteration_stats2.update_from_finished_request(
153 finish_reason=FinishReason.STOP,
154 request_id="test-req-004",
155 num_prompt_tokens=100,
156 max_tokens_param=10,
157 req_stats=req_stats,
158 num_cached_tokens=100,
159 )
160
161 finished_req2 = iteration_stats2.finished_requests[0]
162 prefill_kv_computed2 = finished_req2.num_prompt_tokens - max(
163 finished_req2.num_cached_tokens, 0
164 )
165 assert prefill_kv_computed2 == 0 # All cached, nothing computed
166 assert finished_req2.request_id == "test-req-004"
167
168
169 def test_prompt_token_stats_all_computed():
170 """Test all tokens computed locally, no caching."""
171 stats = PromptTokenStats()
172
173 # Case 1: No caching (All tokens computed locally)
174 prefill_stats = PrefillStats()
175 prefill_stats.set(
176 num_prompt_tokens=1000,
177 num_local_cached_tokens=0,
178 num_external_cached_tokens=0,
179 )
180 stats.update_from_output(prefill_stats)
181
182 assert stats.computed == 1000
183 assert stats.local_cache_hit == 0
184 assert stats.external_kv_transfer == 0
185 assert stats.cached_tokens == 0
186 assert stats.total == 1000
187
188
189 def test_prompt_token_stats_partial_local_cache():
190 """Test partial local prefix cache hit."""
191 stats = PromptTokenStats()
192
193 # Case 2: Partial local cache
194 prefill_stats = PrefillStats()
195 prefill_stats.set(
196 num_prompt_tokens=1000,
197 num_local_cached_tokens=300,
198 num_external_cached_tokens=0,
199 )
200 stats.update_from_output(prefill_stats)
201
202 assert stats.computed == 700
203 assert stats.local_cache_hit == 300
204 assert stats.external_kv_transfer == 0
205 assert stats.cached_tokens == 300
206 assert stats.total == 1000
207
208
209 def test_prompt_token_stats_partial_external_transfer():
210 """Test partial external KV transfer."""
211 stats = PromptTokenStats()
212
213 # Case 3: Partial external transfer
214 prefill_stats = PrefillStats()
215 prefill_stats.set(
216 num_prompt_tokens=1000,
217 num_local_cached_tokens=0,
218 num_external_cached_tokens=500,
219 )
220 stats.update_from_output(prefill_stats)
221
222 assert stats.computed == 500
223 assert stats.local_cache_hit == 0
224 assert stats.external_kv_transfer == 500
225 assert stats.cached_tokens == 500
226 assert stats.total == 1000
227
228
229 def test_prompt_token_stats_mixed_sources():
230 """Test mix of local cache and external transfer."""
231 stats = PromptTokenStats()
232
233 # Case 4: Mixed sources
234 prefill_stats = PrefillStats()
235 prefill_stats.set(
236 num_prompt_tokens=1000,
237 num_local_cached_tokens=400,
238 num_external_cached_tokens=200,
239 )
240 stats.update_from_output(prefill_stats)
241
242 assert stats.computed == 400
243 assert stats.local_cache_hit == 400
244 assert stats.external_kv_transfer == 200
245 assert stats.cached_tokens == 600
246 assert stats.total == 1000
247
248
249 def test_prompt_token_stats_full_local_cache_recompute():
250 """Test full local cache triggers last token recomputation.
251
252 When all tokens are cached, the scheduler forces the model to recompute
253 the last token (num_computed_tokens=1), with the rest from cache.
254 """
255 stats = PromptTokenStats()
256
257 # Case 5: Full local cache (999 cached, 1 recomputed)
258 prefill_stats = PrefillStats()
259 prefill_stats.set(
260 num_prompt_tokens=1000,
261 num_local_cached_tokens=999,
262 num_external_cached_tokens=0,
263 )
264 stats.update_from_output(prefill_stats)
265
266 assert stats.computed == 1
267 assert stats.local_cache_hit == 999
268 assert stats.external_kv_transfer == 0
269 assert stats.cached_tokens == 999
270 assert stats.total == 1000
271
272
273 def test_prompt_token_stats_full_external_transfer_recompute():
274 """Test full external transfer triggers last token recomputation."""
275 stats = PromptTokenStats()
276
277 # Case 6: Full external transfer (999 from external, 1 recomputed)
278 prefill_stats = PrefillStats()
279 prefill_stats.set(
280 num_prompt_tokens=1000,
281 num_local_cached_tokens=0,
282 num_external_cached_tokens=999,
283 )
284 stats.update_from_output(prefill_stats)
285
286 assert stats.computed == 1
287 assert stats.local_cache_hit == 0
288 assert stats.external_kv_transfer == 999
289 assert stats.cached_tokens == 999
290 assert stats.total == 1000