main
py 629 lines 26.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import unittest
5 from unittest.mock import MagicMock
6
7 import numpy as np
8 import torch
9
10 from vllm.config import DeviceConfig, VllmConfig
11 from vllm.multimodal.inputs import (
12 MultiModalFeatureSpec,
13 MultiModalKwargsItem,
14 PlaceholderRange,
15 )
16 from vllm.sampling_params import SamplingParams
17 from vllm.v1.core.sched.scheduler import Scheduler
18 from vllm.v1.engine import FinishReason
19 from vllm.v1.kv_cache_interface import (
20 FullAttentionSpec,
21 KVCacheConfig,
22 KVCacheGroupSpec,
23 )
24 from vllm.v1.outputs import LogprobsLists, ModelRunnerOutput
25 from vllm.v1.request import Request, RequestStatus, StreamingUpdate
26 from vllm.v1.structured_output import StructuredOutputManager
27
28 STOP_TOKEN = 128001
29
30
31 class DummyRequest(Request):
32 def __init__(
33 self,
34 request_id,
35 resumable=True,
36 prompt_token_ids=None,
37 mm_features: list[MultiModalFeatureSpec] | None = None,
38 max_tokens: int | None = 16,
39 ):
40 super().__init__(
41 request_id=request_id,
42 prompt_token_ids=prompt_token_ids if prompt_token_ids is not None else [],
43 sampling_params=SamplingParams(
44 stop_token_ids=[STOP_TOKEN], max_tokens=max_tokens
45 ),
46 pooling_params=None,
47 mm_features=mm_features,
48 resumable=resumable,
49 )
50
51
52 def create_scheduler() -> Scheduler:
53 vllm_config = VllmConfig(device_config=DeviceConfig("cpu"))
54 vllm_config.model_config = MagicMock()
55 vllm_config.model_config.skip_tokenizer_init = True
56 vllm_config.model_config.is_multimodal_model = False
57 vllm_config.model_config.supports_multimodal_inputs = False
58 vllm_config.model_config.is_encoder_decoder = False
59 vllm_config.model_config.is_diffusion = False
60 vllm_config.model_config.max_model_len = 1024
61 vllm_config.model_config.enable_return_routed_experts = False
62 vllm_config.cache_config = MagicMock()
63 vllm_config.cache_config.num_gpu_blocks = 1000
64 vllm_config.cache_config.enable_prefix_caching = False
65 kv_cache_config = KVCacheConfig(
66 num_blocks=1000,
67 kv_cache_tensors=[],
68 kv_cache_groups=[
69 KVCacheGroupSpec(
70 ["layer"],
71 FullAttentionSpec(
72 block_size=16, num_kv_heads=1, head_size=1, dtype=torch.float32
73 ),
74 )
75 ],
76 )
77 return Scheduler(
78 vllm_config=vllm_config,
79 kv_cache_config=kv_cache_config,
80 log_stats=True,
81 structured_output_manager=StructuredOutputManager(vllm_config),
82 block_size=16,
83 hash_block_size=16,
84 )
85
86
87 class TestStreamingScheduler(unittest.TestCase):
88 def test_chunk_logprobs_do_not_depend_on_continuation_arrival(self):
89 """A queued continuation must not change the finishing chunk's logprobs."""
90 for continuation_queued in (False, True):
91 with self.subTest(continuation_queued=continuation_queued):
92 scheduler = create_scheduler()
93 session = DummyRequest("session", prompt_token_ids=[1, 2, 3])
94 session.sampling_params = SamplingParams(
95 stop_token_ids=[STOP_TOKEN], max_tokens=16, logprobs=0
96 )
97 scheduler.add_request(session)
98 scheduled = scheduler.schedule()
99
100 continuation = DummyRequest("session", prompt_token_ids=[4, 5])
101 assert continuation.sampling_params.logprobs is None
102 if continuation_queued:
103 scheduler.add_request(continuation)
104
105 logprobs = LogprobsLists(
106 logprob_token_ids=np.array([[STOP_TOKEN]], dtype=np.int32),
107 logprobs=np.array([[-0.25]], dtype=np.float32),
108 sampled_token_ranks=np.array([1], dtype=np.int32),
109 )
110 runner_output = ModelRunnerOutput(
111 req_ids=[session.request_id],
112 req_id_to_index={session.request_id: 0},
113 sampled_token_ids=[[STOP_TOKEN]],
114 logprobs=logprobs,
115 prompt_logprobs_dict={},
116 pooler_output=[],
117 )
118 outputs = scheduler.update_from_output(scheduled, runner_output)
119 [output] = outputs[session.client_index].outputs
120
121 if not continuation_queued:
122 assert session.status == RequestStatus.WAITING_FOR_STREAMING_REQ
123 scheduler.add_request(continuation)
124 assert session.status == RequestStatus.WAITING
125 assert session.sampling_params is continuation.sampling_params
126 assert output.new_token_ids == [STOP_TOKEN]
127 assert output.finish_reason == FinishReason.STOP
128 assert output.new_logprobs is not None, (
129 "The finishing chunk requested logprobs; the continuation's "
130 "settings must not suppress them"
131 )
132 np.testing.assert_array_equal(
133 output.new_logprobs.logprob_token_ids, logprobs.logprob_token_ids
134 )
135 np.testing.assert_array_equal(
136 output.new_logprobs.logprobs, logprobs.logprobs
137 )
138 np.testing.assert_array_equal(
139 output.new_logprobs.sampled_token_ranks,
140 logprobs.sampled_token_ranks,
141 )
142
143 def test_add_request(self):
144 scheduler = create_scheduler()
145
146 request = DummyRequest(
147 request_id="test_request",
148 resumable=True,
149 )
150
151 scheduler.add_request(request)
152
153 assert "test_request" in scheduler.requests
154 assert request.status == RequestStatus.WAITING
155 assert len(scheduler.waiting) == 1
156
157 next_request = DummyRequest(
158 request_id="test_request",
159 resumable=True,
160 )
161 scheduler.add_request(next_request)
162
163 assert next_request.status == RequestStatus.WAITING
164 assert len(scheduler.requests["test_request"].streaming_queue) == 1
165
166 def test_update_request_as_session_max_token(self):
167 scheduler = create_scheduler()
168
169 session = DummyRequest(
170 request_id="session",
171 prompt_token_ids=[1, 2, 3],
172 )
173 session.num_computed_tokens = len(session.prompt_token_ids)
174 session.max_tokens = 2
175
176 new_request = DummyRequest(
177 request_id="session",
178 prompt_token_ids=[4, 5, 6],
179 )
180 new_request.sampling_params = SamplingParams(max_tokens=10)
181 new_request.max_tokens = 10 # Additional max_tokens from new request
182
183 update = StreamingUpdate.from_request(new_request)
184 scheduler._update_request_as_session(session, update)
185
186 assert session.sampling_params.max_tokens == 10
187 assert session.max_tokens == 10
188
189 session.num_computed_tokens = len(session.prompt_token_ids)
190
191 new_request2 = DummyRequest(
192 request_id="session",
193 prompt_token_ids=[7, 8, 9],
194 )
195 new_request2.sampling_params = SamplingParams(max_tokens=4)
196 new_request2.max_tokens = 4
197 update2 = StreamingUpdate.from_request(new_request2)
198 scheduler._update_request_as_session(session, update2)
199
200 assert session.sampling_params.max_tokens == 4
201 assert session.max_tokens == 4
202
203 def test_update_request_as_session(self):
204 scheduler = create_scheduler()
205
206 session = DummyRequest(
207 request_id="session",
208 prompt_token_ids=[1, 2, 3],
209 )
210 session.num_computed_tokens = len(session.prompt_token_ids)
211
212 new_request = DummyRequest(
213 request_id="session",
214 prompt_token_ids=[4, 5, 6],
215 )
216 new_request.sampling_params = SamplingParams(max_tokens=10)
217
218 update = StreamingUpdate.from_request(new_request)
219 scheduler._update_request_as_session(session, update)
220
221 assert session.prompt_token_ids == [1, 2, 3, 4, 5, 6]
222 assert session._all_token_ids == [1, 2, 3, 4, 5, 6]
223 assert session.sampling_params.max_tokens == 10
224 assert session.status == RequestStatus.WAITING
225
226 def test_update_request_as_session_with_multimodal(self):
227 scheduler = create_scheduler()
228
229 mm_feature = MultiModalFeatureSpec(
230 data=MultiModalKwargsItem.dummy(),
231 modality="audio",
232 identifier="",
233 mm_position=PlaceholderRange(offset=1, length=1),
234 )
235 session = DummyRequest(
236 request_id="session",
237 prompt_token_ids=[1, 2, 3],
238 mm_features=[mm_feature],
239 )
240 session.num_computed_tokens = len(session.prompt_token_ids)
241
242 mm_feature = MultiModalFeatureSpec(
243 data=MultiModalKwargsItem.dummy(),
244 modality="audio",
245 identifier="",
246 mm_position=PlaceholderRange(offset=2, length=1),
247 )
248 new_request = DummyRequest(
249 request_id="session",
250 prompt_token_ids=[4, 5, 6, 7],
251 mm_features=[mm_feature],
252 )
253 update = StreamingUpdate.from_request(new_request)
254 scheduler._update_request_as_session(session, update)
255
256 assert len(session.mm_features) == 2
257 assert session.mm_features[0].mm_position.offset == 1
258 # 2 + len([1, 2, 3])
259 assert session.mm_features[1].mm_position.offset == 5
260
261 def test_process_streaming_requests_with_finish_session(self):
262 """Test that a non-resumable request signals stream completion.
263
264 With the new streaming API, completion is signaled by closing/finishing
265 the input generator. When a non-resumable request is added to a session
266 in WAITING_FOR_STREAMING_REQ state, the session is finished immediately
267 with FINISHED_ABORTED status.
268 """
269 scheduler = create_scheduler()
270
271 session = DummyRequest(
272 request_id="session",
273 prompt_token_ids=[1, 2, 3],
274 resumable=True,
275 )
276 scheduler.add_request(session)
277 session.status = RequestStatus.WAITING_FOR_STREAMING_REQ
278 session.num_computed_tokens = len(session.prompt_token_ids)
279
280 # A non-resumable request signals stream completion
281 close_request = DummyRequest(
282 request_id="session",
283 prompt_token_ids=[0],
284 resumable=False,
285 max_tokens=1,
286 )
287 scheduler.add_request(close_request)
288
289 # The session should be immediately finished (stream completed)
290 assert session.status == RequestStatus.FINISHED_ABORTED
291 # The session should be removed from the scheduler
292 assert session.request_id not in scheduler.requests
293
294 def test_streaming_request_session_update(self):
295 """Test that a resumable request updates a waiting session directly.
296
297 When a session is in WAITING_FOR_STREAMING_REQ state and a new resumable
298 request arrives, the update is applied directly via _update_request_as_session,
299 not queued.
300 """
301 scheduler = create_scheduler()
302
303 session = DummyRequest(
304 request_id="session",
305 prompt_token_ids=[1, 2, 3],
306 resumable=True,
307 )
308 scheduler.add_request(session)
309 session.status = RequestStatus.WAITING_FOR_STREAMING_REQ
310 session.num_computed_tokens = len(session.prompt_token_ids)
311
312 next_request = DummyRequest(
313 request_id="session",
314 prompt_token_ids=[4, 5],
315 resumable=True,
316 )
317
318 scheduler.add_request(next_request)
319
320 # With the new behavior, when session is in WAITING_FOR_STREAMING_REQ,
321 # the update is applied directly (not queued), and session status
322 # becomes WAITING
323 assert session.status == RequestStatus.WAITING
324 assert session.prompt_token_ids == [1, 2, 3, 4, 5]
325
326 _ = scheduler.schedule()
327
328 assert session.status == RequestStatus.RUNNING
329
330 def test_update_request_as_session_with_output_tokens(self):
331 scheduler = create_scheduler()
332
333 session = DummyRequest(
334 request_id="session",
335 prompt_token_ids=[1, 2, 3], # 3 prompt tokens
336 )
337 session.append_output_token_ids([10, 11])
338 """
339 The last output token (11) hasn't been "scheduled" yet, so `num_computed_tokens`
340 only includes: 3 prompt + 1 output (the 10) = 4
341 """
342 session.num_computed_tokens = 4
343
344 new_request = DummyRequest(
345 request_id="session",
346 prompt_token_ids=[4, 5],
347 )
348
349 update = StreamingUpdate.from_request(new_request)
350 scheduler._update_request_as_session(session, update)
351
352 # _update_request_as_session keeps computed output tokens (they become
353 # part of the prompt) and only discards the final uncomputed sampled
354 # token. Computed output token 10 is kept, uncomputed token 11 is
355 # discarded.
356 assert session._all_token_ids == [1, 2, 3, 10, 4, 5]
357 assert session.prompt_token_ids == [1, 2, 3, 10, 4, 5]
358 # Output tokens list is cleared
359 assert session._output_token_ids == []
360 # num_computed_tokens is unchanged (KV cache still valid for computed
361 # tokens)
362 assert session.num_computed_tokens == 4
363 # Verify that the next schedule will only process the new prompt tokens
364 # num_new_tokens = num_tokens - num_computed_tokens = 6 - 4 = 2
365 num_new_tokens = session.num_tokens - session.num_computed_tokens
366 assert num_new_tokens == 2
367
368 def test_streaming_e2e_lifecycle(self):
369 """Comprehensive integration test covering complete streaming request lifecycle
370 including scheduler state management and aliasing bug prevention.
371
372 FULL LIFECYCLE:
373 ================
374 CYCLE 1 (Initial Decode):
375 1. Add streaming request (seq_id=0) with prompt tokens [1,2,3]
376 2. Schedule() creates NewRequestData with prompt_token_ids
377 3. Model runner caches this prompt_token_ids reference (simulated)
378 4. Model executes and generates output token 10
379 5. update_from_output() appends token 10 to request._all_token_ids
380 6. Request transitions to RUNNING state
381
382 CYCLE 2 (Continue Decode):
383 7. Schedule() again - request is now in scheduled_cached_reqs (not new)
384 8. Model runner uses CACHED state to calculate num_tokens
385 9. Model generates output token (STOP_TOKEN)
386 10. update_from_output() appends STOP_TOKEN to request._all_token_ids
387 11. Request transitions to WAITING_FOR_STREAMING_REQ
388
389 CYCLE 3 (New Streaming Request):
390 12. Add new streaming request (seq_id=1) with prompt tokens [4,5]
391 13. Scheduler merges into session, creates NewRequestData again
392 14. Model runner caches new prompt_token_ids reference
393 15. Verify cached state from Cycle 1 wasn't corrupted by mutations
394
395 CRITICAL BUG PREVENTION:
396 ========================
397 Without .copy() in _create_new_request_data():
398 - Cycle 1 Step 3: cached_state["prompt_token_ids"] aliases
399 request._all_token_ids
400 - Cycle 1 Step 5: When appending token 10, cached state mutates:
401 [1,2,3] -> [1,2,3,10]
402 - Cycle 2 Step 8: num_tokens = len([1,2,3,10]) + len([10])
403 = 5 (WRONG! Should be 4)
404 - Cycle 2: Discard logic would see seq_lens=4 < num_tokens=5
405 -> INCORRECTLY DISCARDS
406
407 With .copy() in _create_new_request_data():
408 - Cycle 1 Step 3: cached_state["prompt_token_ids"] is independent copy
409 - Cycle 1 Step 5: Only request._all_token_ids mutates, cached stays [1,2,3]
410 - Cycle 2 Step 8: num_tokens = len([1,2,3]) + len([10]) = 4 (CORRECT)
411 - Cycle 2: Discard logic works correctly
412 """
413 scheduler = create_scheduler()
414
415 # ═══════════════════════════════════════════════════════════════════
416 # CYCLE 1: Initial Request Scheduling and First Decode
417 # ═══════════════════════════════════════════════════════════════════
418
419 session = DummyRequest(
420 request_id="session",
421 prompt_token_ids=[1, 2, 3],
422 )
423 scheduler.add_request(session)
424
425 # Step 2: Schedule creates NewRequestData
426 scheduler_output_cycle1 = scheduler.schedule()
427
428 # Verify request is in scheduled_new_reqs (first time scheduling)
429 assert len(scheduler_output_cycle1.scheduled_new_reqs) == 1
430 new_req_data_cycle1 = scheduler_output_cycle1.scheduled_new_reqs[0]
431 assert new_req_data_cycle1.prompt_token_ids == [1, 2, 3]
432 assert (
433 scheduler_output_cycle1.num_scheduled_tokens[session.request_id] == 3
434 ) # [1, 2, 3]
435 assert (
436 session.request_id
437 not in scheduler_output_cycle1.scheduled_cached_reqs.req_ids
438 )
439
440 # Step 3: Simulate model runner caching the prompt_token_ids
441 # This simulates gpu_model_runner.py:706-720 CachedRequestState creation
442 # The model runner makes a copy of prompt_token_ids when creating
443 # CachedRequestState
444 cached_state_cycle1 = {
445 "req_id": session.request_id,
446 "prompt_token_ids": list(
447 new_req_data_cycle1.prompt_token_ids
448 ), # Explicit copy
449 "output_token_ids": [],
450 "num_computed_tokens": 0,
451 }
452
453 # Store original for verification
454 original_cached_prompt_cycle1 = cached_state_cycle1["prompt_token_ids"].copy()
455
456 # Step 4-5: Model execution generates token, scheduler updates request
457 output_token_1 = 10
458 cached_state_cycle1["output_token_ids"].append(output_token_1)
459
460 mro_cycle1 = ModelRunnerOutput(
461 req_ids=[session.request_id],
462 req_id_to_index={session.request_id: 0},
463 sampled_token_ids=[[output_token_1]],
464 logprobs=None,
465 prompt_logprobs_dict={session.request_id: None},
466 pooler_output=[],
467 )
468 session.num_computed_tokens = len(session.prompt_token_ids)
469 eco_dict_cycle1 = scheduler.update_from_output(
470 scheduler_output_cycle1, mro_cycle1
471 )
472
473 # Step 6: Verify request state after Cycle 1
474 eco_cycle1 = eco_dict_cycle1[session.client_index].outputs[0]
475 assert eco_cycle1.finish_reason is None # Not stopped yet
476 assert session.status == RequestStatus.RUNNING
477 assert session in scheduler.running
478 assert session._all_token_ids == [1, 2, 3, 10] # Mutation happened here
479
480 # CRITICAL ASSERTION: Cached prompt_token_ids must NOT have changed
481 assert (
482 cached_state_cycle1["prompt_token_ids"] == original_cached_prompt_cycle1
483 ), (
484 f"ALIASING BUG DETECTED in Cycle 1! "
485 f"cached_state['prompt_token_ids'] was mutated from "
486 f"{original_cached_prompt_cycle1} to "
487 f"{cached_state_cycle1['prompt_token_ids']}. "
488 f"This means _create_new_request_data() didn't call .copy()!"
489 )
490 assert cached_state_cycle1["prompt_token_ids"] is not session._all_token_ids, (
491 "ALIASING BUG! cached_state['prompt_token_ids'] is the same object as "
492 "session._all_token_ids. They must be independent copies."
493 )
494
495 # ═══════════════════════════════════════════════════════════════════
496 # CYCLE 2: Continue Decoding (Using Cached State)
497 # ═══════════════════════════════════════════════════════════════════
498
499 # Step 7: Schedule again - now request uses cached state
500 scheduler_output_cycle2 = scheduler.schedule()
501
502 # Verify request is NOT in scheduled_new_reqs (already cached)
503 assert not scheduler_output_cycle2.scheduled_new_reqs
504 assert (
505 session.request_id in scheduler_output_cycle2.scheduled_cached_reqs.req_ids
506 )
507 assert (
508 scheduler_output_cycle2.num_scheduled_tokens[session.request_id] == 1
509 ) # Only the output token [10]
510
511 # Step 8: Calculate num_tokens like gpu_model_runner.py:1284 does
512 # This is where the bug would manifest!
513 num_tokens_cycle2 = len(cached_state_cycle1["prompt_token_ids"]) + len(
514 cached_state_cycle1["output_token_ids"]
515 )
516
517 # CRITICAL ASSERTION: num_tokens must be correct (3 prompt + 1 output = 4)
518 # Without .copy(), cached_state["prompt_token_ids"] would be [1,2,3,10]
519 # and num_tokens would incorrectly be 5, causing the discard bug
520 expected_num_tokens_cycle2 = 4
521 assert num_tokens_cycle2 == expected_num_tokens_cycle2, (
522 f"DISCARD BUG WOULD TRIGGER! num_tokens calculation is wrong. "
523 f"Expected {expected_num_tokens_cycle2}, got {num_tokens_cycle2}. "
524 f"cached_state['prompt_token_ids'] = "
525 f"{cached_state_cycle1['prompt_token_ids']} (should be [1,2,3], not [1,2,3,"
526 f"10]). Without .copy(), this would be 5 = len([1,2,3,10]) + len([10]). "
527 f"Discard logic would see: seq_lens={session.num_computed_tokens} "
528 f"< num_tokens={num_tokens_cycle2}, triggering incorrect discard!"
529 )
530
531 # Step 9-10: Model generates STOP_TOKEN, scheduler updates
532 output_token_2 = STOP_TOKEN
533 cached_state_cycle1["output_token_ids"].append(output_token_2)
534
535 mro_cycle2 = ModelRunnerOutput(
536 req_ids=[session.request_id],
537 req_id_to_index={session.request_id: 0},
538 sampled_token_ids=[[output_token_2]],
539 logprobs=None,
540 prompt_logprobs_dict={session.request_id: None},
541 pooler_output=[],
542 )
543 eco_dict_cycle2 = scheduler.update_from_output(
544 scheduler_output_cycle2, mro_cycle2
545 )
546
547 # Step 11: Verify request transitioned to WAITING_FOR_STREAMING_REQ
548 eco_cycle2 = eco_dict_cycle2[session.client_index].outputs[0]
549 assert eco_cycle2.finish_reason == FinishReason.STOP
550 assert session.status == RequestStatus.WAITING_FOR_STREAMING_REQ
551 # Sessions paused for streaming input keep their KV blocks, so they
552 # live in the kv_holding_waiting queue.
553 assert session in scheduler.kv_holding_waiting
554 assert session._all_token_ids == [1, 2, 3, 10, STOP_TOKEN]
555
556 # CRITICAL ASSERTION: Cached prompt_token_ids STILL must not have changed
557 assert cached_state_cycle1["prompt_token_ids"] == [1, 2, 3], (
558 f"ALIASING BUG DETECTED in Cycle 2! "
559 f"cached_state['prompt_token_ids'] = "
560 f"{cached_state_cycle1['prompt_token_ids']} (should still be [1,2,3]). "
561 f"Mutations from update_from_output() leaked through!"
562 )
563
564 # ═══════════════════════════════════════════════════════════════════
565 # CYCLE 3: New Streaming Request (Session Continuation)
566 # ═══════════════════════════════════════════════════════════════════
567
568 # Step 12: Add new streaming request with seq_id=1
569 new_request = DummyRequest(
570 request_id="session",
571 prompt_token_ids=[4, 5],
572 )
573 scheduler.add_request(new_request)
574
575 # With the new streaming API, when session is in WAITING_FOR_STREAMING_REQ,
576 # the update is applied directly via _update_request_as_session (not queued).
577 # The session status becomes WAITING after the update is applied.
578 assert session.status == RequestStatus.WAITING
579
580 # Step 13: Scheduler schedules the updated session
581 scheduler_output_cycle3 = scheduler.schedule()
582
583 # Verify scheduler created NewRequestData with merged prompt_token_ids
584 assert len(scheduler_output_cycle3.scheduled_new_reqs) == 1
585 assert (
586 scheduler_output_cycle3.scheduled_new_reqs[0].prompt_token_ids
587 == session.prompt_token_ids
588 )
589 assert (
590 scheduler_output_cycle3.num_scheduled_tokens[session.request_id] == 2
591 ) # Only new tokens [4, 5]
592 # Computed output tokens are kept (become part of prompt), only the
593 # final uncomputed sampled token (STOP_TOKEN) is discarded
594 assert session._all_token_ids == [1, 2, 3, 10, 4, 5]
595 assert session.prompt_token_ids == [1, 2, 3, 10, 4, 5] # Includes kept output
596 assert session._output_token_ids == [] # Output tokens are cleared
597
598 # Step 14: Model runner caches NEW prompt_token_ids reference
599 # The model runner makes a copy of prompt_token_ids when creating
600 # CachedRequestState
601 new_req_data_cycle3 = scheduler_output_cycle3.scheduled_new_reqs[0]
602 cached_state_cycle3 = {
603 "req_id": session.request_id,
604 "prompt_token_ids": list(
605 new_req_data_cycle3.prompt_token_ids
606 ), # Explicit copy
607 "output_token_ids": [],
608 "num_computed_tokens": session.num_computed_tokens,
609 }
610
611 # Step 15: FINAL CRITICAL VERIFICATION
612 # The old cached state from Cycle 1 must still be unchanged
613 assert cached_state_cycle1["prompt_token_ids"] == [1, 2, 3], (
614 f"PERSISTENT ALIASING BUG! Even after new scheduling cycle, "
615 f"old cached_state was mutated to "
616 f"{cached_state_cycle1['prompt_token_ids']}. This proves the aliasing bug "
617 f"exists!"
618 )
619
620 # The new cached state must be independent
621 assert cached_state_cycle3["prompt_token_ids"] is not session._all_token_ids, (
622 "ALIASING BUG in Cycle 3! Cached state is aliased to _all_token_ids."
623 )
624
625 # Both cached states must be independent of each other
626 assert (
627 cached_state_cycle1["prompt_token_ids"]
628 is not cached_state_cycle3["prompt_token_ids"]
629 ), "Cached states from different cycles should be independent objects."