main
py 1,854 lines 53.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import threading
5 from contextlib import nullcontext
6 from dataclasses import dataclass, field
7 from types import SimpleNamespace
8 from unittest.mock import Mock
9
10 import numpy as np
11 import pytest
12 import torch
13
14 from vllm.distributed.aux_output_connector.connector import (
15 AuxOutputConnectorMetadata,
16 AuxOutputSchedulerConnector,
17 AuxRequestOutput,
18 PackedBlockHashes,
19 )
20 from vllm.distributed.aux_output_connector.routed_experts import (
21 RoutedExpertsBuffer,
22 materialize_routed_experts,
23 publish_routed_experts,
24 routed_experts_keys,
25 )
26 from vllm.distributed.aux_output_connector.store import (
27 BackgroundBlockObjectStore,
28 BlockObject,
29 BlockObjectStore,
30 BlockObjectStoreError,
31 )
32 from vllm.distributed.aux_output_connector.worker import (
33 AuxOutputWorkerConnector,
34 )
35 from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput
36 from vllm.v1.outputs import ModelRunnerOutput
37 from vllm.v1.worker.gpu import async_utils
38 from vllm.v1.worker.gpu.sample.output import SamplerOutput
39
40 pytestmark = pytest.mark.cpu_test
41
42 _SHAPE = (3, 2)
43 _DTYPE = np.dtype("uint8")
44 _BLOCK_SIZE = 4
45
46
47 def test_background_store_publishes_without_blocking_caller():
48 started = threading.Event()
49 release = threading.Event()
50 underlying = Mock()
51
52 def put(*_args, **_kwargs):
53 started.set()
54 release.wait()
55
56 underlying.put.side_effect = put
57 store = BackgroundBlockObjectStore(underlying, max_pending_batches=1)
58
59 store.put([BlockObject("key", b"value")])
60 assert started.wait(timeout=1)
61 release.set()
62 store.close()
63
64 underlying.close.assert_called_once_with()
65
66
67 def _make_connector():
68 return AuxOutputSchedulerConnector()
69
70
71 @dataclass
72 class _Execution:
73 request_id: str
74 token_start: int
75 num_tokens: int
76 emit_start: int
77 block_hashes: list[bytes] | PackedBlockHashes
78
79
80 @dataclass
81 class _WorkerStep:
82 metadata: AuxOutputConnectorMetadata
83 executions: list[_Execution]
84
85
86 @dataclass
87 class _SchedulerRequest:
88 request_id: str
89 block_hashes: list[bytes]
90 num_tokens: int
91 num_output_tokens: int = 1
92 num_computed_tokens: int = 0
93 num_in_flight_tokens: int = 0
94 finished: bool = False
95 sampling_params: SimpleNamespace = field(
96 default_factory=lambda: SimpleNamespace(routed_experts_prompt_start=0)
97 )
98
99 def is_finished(self) -> bool:
100 return self.finished
101
102
103 def _request_metadata(
104 request_id: str,
105 token_start: int,
106 num_tokens: int,
107 emit_start: int,
108 block_hashes,
109 ) -> _Execution:
110 return _Execution(request_id, token_start, num_tokens, emit_start, block_hashes)
111
112
113 def _metadata(
114 generation: int,
115 requests: list[_Execution],
116 finished_requests: dict[str, list[bytes] | PackedBlockHashes],
117 hash_updates: dict[str, list[bytes] | PackedBlockHashes] | None = None,
118 ) -> _WorkerStep:
119 block_hashes = {
120 request.request_id: request.block_hashes
121 for request in requests
122 if request.block_hashes
123 }
124 if hash_updates:
125 block_hashes.update(
126 (request_id, hashes)
127 for request_id, hashes in hash_updates.items()
128 if hashes
129 )
130 block_hashes.update(
131 (request_id, hashes)
132 for request_id, hashes in finished_requests.items()
133 if hashes
134 )
135 return _WorkerStep(
136 AuxOutputConnectorMetadata(
137 generation,
138 {request.request_id: request.emit_start for request in requests},
139 block_hashes,
140 tuple(finished_requests),
141 ),
142 requests,
143 )
144
145
146 def _execution_ranges(metadata, request_ids):
147 by_request = {request.request_id: request for request in metadata.executions}
148 token_starts = tuple(
149 by_request[request_id].token_start for request_id in request_ids
150 )
151 num_tokens = tuple(by_request[request_id].num_tokens for request_id in request_ids)
152 return token_starts, num_tokens
153
154
155 def _begin_step(worker: AuxOutputWorkerConnector, step: _WorkerStep) -> None:
156 worker.begin_step(step.metadata)
157
158
159 def _make_worker(
160 max_num_seqs: int,
161 max_num_batched_tokens: int | None = None,
162 max_concurrent_batches: int = 2,
163 max_store_blocks: int | None = None,
164 ) -> AuxOutputWorkerConnector:
165 object_nbytes = _BLOCK_SIZE * int(np.prod(_SHAPE))
166 store = _make_store(
167 max_bytes=(
168 1 << 20 if max_store_blocks is None else max_store_blocks * object_nbytes
169 ),
170 object_nbytes=object_nbytes,
171 )
172 worker = object.__new__(AuxOutputWorkerConnector)
173 worker._store = store
174 worker._buffer = RoutedExpertsBuffer(
175 _DTYPE,
176 _SHAPE,
177 _BLOCK_SIZE,
178 max_num_seqs,
179 max_num_batched_tokens or max_num_seqs * _BLOCK_SIZE,
180 max_concurrent_batches,
181 )
182 worker._requests = {}
183 worker._generation = 0
184 worker._step_metadata = None
185 worker._pending_outputs = []
186 worker._lock = threading.Lock()
187 worker._max_concurrent_batches = max_concurrent_batches
188 return worker
189
190
191 def test_worker_rejects_metadata_generation_rollback():
192 worker = _make_worker(1)
193 _begin_step(worker, _metadata(1, [], {}))
194
195 with pytest.raises(AssertionError, match="generation moved backwards"):
196 _begin_step(worker, _metadata(0, [], {}))
197
198 worker.close()
199
200
201 def test_worker_rejects_run_and_finish_in_one_step():
202 worker = _make_worker(1)
203 request = _request_metadata("request", 0, 1, 0, [])
204 metadata = _metadata(
205 0,
206 [request],
207 {"request": []},
208 )
209
210 with pytest.raises(AssertionError, match="cannot run and finish"):
211 _begin_step(worker, metadata)
212
213 worker.close()
214
215
216 def _input_batch(request_ids, token_starts, query_start_loc):
217 return SimpleNamespace(
218 req_ids=request_ids,
219 num_computed_tokens_np=token_starts,
220 query_start_loc_np=query_start_loc,
221 )
222
223
224 def test_non_output_rank_skips_capture_snapshot():
225 worker = object.__new__(AuxOutputWorkerConnector)
226 worker._store = None
227 worker._buffer = None
228 worker._capturer = Mock()
229 worker._step_metadata = Mock()
230 assert worker.prepare_output(_input_batch([], np.array([]), np.array([]))) is None
231 worker._capturer.snapshot_routing_data.assert_not_called()
232
233
234 def test_worker_skips_aux_outputs_for_internal_warmup_step():
235 worker = _make_worker(1)
236 worker._capturer = Mock()
237
238 worker.begin_step(None)
239
240 assert worker.prepare_output(_input_batch([], np.array([]), np.array([]))) is None
241 worker._capturer.snapshot_routing_data.assert_not_called()
242 worker.close()
243
244
245 def test_next_step_does_not_consume_pending_output(monkeypatch):
246 """begin_step must not wait for or consume an unconsumed step output."""
247 event = Mock()
248 monkeypatch.setattr(torch.cuda, "Event", lambda **kwargs: event)
249 monkeypatch.setattr(async_utils, "stream", lambda *args: nullcontext())
250 monkeypatch.setattr(
251 async_utils, "async_copy_to_np", lambda tensor: tensor.numpy().copy()
252 )
253 worker = _make_worker(1)
254 worker.begin_step(
255 _metadata(0, [_request_metadata("request", 0, 1, 0, [])], {}).metadata
256 )
257 rows = torch.ones((1, *_SHAPE), dtype=torch.uint8)
258 worker._capturer = Mock()
259 worker._capturer.snapshot_routing_data.return_value = rows
260 process_output = Mock(wraps=worker.process_output)
261 monkeypatch.setattr(worker, "process_output", process_output)
262 pending = worker.prepare_output(
263 _input_batch(["request"], np.array([0]), np.array([0, 1]))
264 )
265 assert pending is not None
266 output = async_utils.AsyncOutput(
267 ModelRunnerOutput(["request"], {"request": 0}),
268 SamplerOutput(
269 torch.tensor([[7]]), None, None, None, num_rejected=torch.tensor([0])
270 ),
271 torch.tensor([1]),
272 Mock(),
273 Mock(),
274 check_ep_fault=False,
275 pending_aux_output=pending,
276 )
277 event.record.assert_called_once()
278 assert worker._pending_outputs == [pending]
279
280 worker.begin_step(_metadata(0, [], {}).metadata)
281 process_output.assert_not_called()
282 assert worker._pending_outputs == [pending]
283
284 result = output.get_output()
285
286 assert result.sampled_token_ids == [[7]]
287 np.testing.assert_array_equal(
288 result.aux_output_connector_output["request"].rows, rows.numpy()
289 )
290 process_output.assert_called_once()
291 assert worker._pending_outputs == []
292 worker.close()
293
294
295 @pytest.mark.parametrize("num_pending", [1, 2])
296 def test_finished_request_teardown_waits_for_pending_output(num_pending):
297 """Terminal cleanup waits for every outstanding output, not just the first."""
298 worker = _make_worker(1)
299 rows = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, *_SHAPE)
300 worker._capturer = Mock()
301 worker._capturer.snapshot_routing_data.return_value = torch.from_numpy(rows)
302 pending_outputs = []
303 for i in range(num_pending):
304 request = _request_metadata("request", 4 * i, 4, 4 * i, [bytes([i]) * 4])
305 worker.begin_step(_metadata(0, [request], {}).metadata)
306 pending = worker.prepare_output(
307 _input_batch(["request"], np.array([4 * i]), np.array([0, 4]))
308 )
309 assert pending is not None
310 pending.enqueue_cpu_copy(np.array([1]), np.array([0]))
311 pending_outputs.append(pending)
312
313 # The next step finishes the request while its output is unconsumed.
314 worker.begin_step(_metadata(0, [], {"request": []}).metadata)
315 assert "request" in worker._requests
316
317 for i, pending in enumerate(pending_outputs):
318 output = worker.process_output(pending)
319 np.testing.assert_array_equal(output["request"].rows, rows)
320 if i + 1 < num_pending:
321 assert "request" in worker._requests
322 assert worker._store._references
323 assert "request" not in worker._requests
324 # The finished request's store keys must be released after commit.
325 assert not worker._store._references
326 worker.close()
327
328
329 def test_pending_outputs_bounded_by_max_concurrent_batches():
330 worker = _make_worker(1, max_concurrent_batches=2)
331 worker._capturer = Mock()
332 worker._capturer.snapshot_routing_data.side_effect = lambda num_rows: torch.zeros(
333 (num_rows, *_SHAPE), dtype=torch.uint8
334 )
335 metadata = _metadata(0, [_request_metadata("request", 0, 1, 0, [])], {}).metadata
336 batch = _input_batch(["request"], np.array([0]), np.array([0, 1]))
337 for _ in range(2):
338 worker.begin_step(metadata)
339 assert worker.prepare_output(batch) is not None
340
341 worker.begin_step(metadata)
342 with pytest.raises(AssertionError, match="not consumed in order"):
343 worker.prepare_output(batch)
344
345 worker.close()
346
347
348 def test_worker_rejects_invalid_rejected_token_count():
349 worker = _make_worker(1)
350 metadata = _metadata(
351 0,
352 [_request_metadata("request", 0, 1, 0, [])],
353 {},
354 )
355 with pytest.raises(AssertionError, match="rejected-token count is invalid"):
356 _process_output(
357 worker,
358 metadata,
359 np.zeros((1, *_SHAPE), dtype=_DTYPE),
360 ["request"],
361 np.array([-1]),
362 token_starts=(0,),
363 num_tokens=(1,),
364 )
365 assert worker._pending_outputs == []
366
367 worker.close()
368
369
370 def _process_output(
371 worker,
372 step,
373 rows,
374 request_ids,
375 num_rejected,
376 num_sampled=None,
377 *,
378 token_starts=None,
379 num_tokens=None,
380 query_start_loc=None,
381 ):
382 if num_sampled is None:
383 num_sampled = np.ones(len(request_ids), dtype=np.int32)
384 if token_starts is None or num_tokens is None:
385 assert isinstance(step, _WorkerStep)
386 token_starts, num_tokens = _execution_ranges(step, request_ids)
387 worker._capturer = Mock()
388 worker._capturer.snapshot_routing_data.return_value = torch.from_numpy(rows)
389 metadata = step.metadata if isinstance(step, _WorkerStep) else step
390 worker.begin_step(metadata)
391 if query_start_loc is None:
392 query_start_loc = np.concatenate(
393 (np.zeros(1, dtype=np.int32), np.cumsum(num_tokens, dtype=np.int32))
394 )
395 pending = worker.prepare_output(
396 _input_batch(request_ids, np.asarray(token_starts), query_start_loc)
397 )
398 assert pending is not None
399 pending.enqueue_cpu_copy(num_sampled, num_rejected)
400 return worker.process_output(pending)
401
402
403 def test_worker_ignores_cudagraph_query_padding():
404 worker = _make_worker(2)
405 logical = np.arange(2 * 3 * 2, dtype=np.uint8).reshape(2, 3, 2)
406 metadata = _metadata(
407 0,
408 [
409 _request_metadata("first", 0, 1, 0, []),
410 _request_metadata("second", 0, 1, 0, []),
411 ],
412 {},
413 )
414
415 output = _process_output(
416 worker,
417 metadata,
418 logical,
419 ["first", "second"],
420 np.zeros(2, dtype=np.int32),
421 query_start_loc=np.array([0, 1, 2, 2], dtype=np.int32),
422 )
423
424 np.testing.assert_array_equal(output["first"].rows, logical[:1])
425 np.testing.assert_array_equal(output["second"].rows, logical[1:])
426 worker.close()
427
428
429 def test_worker_rejects_scheduler_emit_cursor_ahead():
430 worker = _make_worker(1)
431 _begin_step(
432 worker,
433 _metadata(
434 0,
435 [_request_metadata("request", 0, 1, 0, [])],
436 {},
437 ),
438 )
439
440 with pytest.raises(AssertionError, match="Scheduler emit cursor moved ahead"):
441 _begin_step(
442 worker,
443 _metadata(
444 0,
445 [_request_metadata("request", 1, 1, 1, [])],
446 {},
447 ),
448 )
449
450 worker.close()
451
452
453 @pytest.mark.parametrize(
454 "routing",
455 [
456 np.zeros((1, _SHAPE[0], _SHAPE[1] + 1), dtype=_DTYPE),
457 np.zeros((1, *_SHAPE), dtype=np.int32),
458 ],
459 ids=["shape", "dtype"],
460 )
461 def test_worker_rejects_mismatched_capture_profile(routing):
462 worker = _make_worker(1)
463 metadata = _metadata(
464 0,
465 [_request_metadata("request", 0, 1, 0, [])],
466 {},
467 )
468
469 with pytest.raises(AssertionError, match="capture profile changed"):
470 _process_output(
471 worker,
472 metadata,
473 routing,
474 ["request"],
475 np.array([0]),
476 )
477 worker.close()
478
479
480 @pytest.mark.parametrize(
481 ("initial_start", "initial_rows", "invalid_start"),
482 [
483 (4, [4, 5, 6], 6),
484 (0, [0], 2),
485 (None, [], 3),
486 ],
487 ids=["overlap", "gap", "unaligned-initial-capture"],
488 )
489 def test_logical_buffer_rejects_noncontiguous_capture(
490 initial_start, initial_rows, invalid_start
491 ):
492 buffer = RoutedExpertsBuffer(np.dtype("uint8"), (1,), 4, 1, 4, 1)
493 if initial_start is not None:
494 rows = np.asarray(initial_rows, dtype=np.uint8).reshape(-1, 1)
495 assert buffer.capture("request", initial_start, rows) == []
496
497 with pytest.raises(AssertionError, match="not contiguous"):
498 buffer.capture(
499 "request", invalid_start, np.array([[invalid_start]], dtype=np.uint8)
500 )
501
502
503 def test_logical_buffer_captures_one_row_per_decode_step():
504 buffer = RoutedExpertsBuffer(_DTYPE, _SHAPE, _BLOCK_SIZE, 1, 8, 2)
505 logical = np.arange(_BLOCK_SIZE * 3 * 2, dtype=_DTYPE).reshape(_BLOCK_SIZE, 3, 2)
506
507 completed = []
508 for step in range(_BLOCK_SIZE):
509 completed += buffer.capture("request", step, logical[step : step + 1])
510
511 assert len(completed) == 1
512 np.testing.assert_array_equal(completed[0][1], logical)
513
514
515 def test_logical_buffer_copies_borrowed_block_when_retained():
516 buffer = RoutedExpertsBuffer(_DTYPE, _SHAPE, _BLOCK_SIZE, 1, 4, 2)
517 logical = np.arange(_BLOCK_SIZE * 3 * 2, dtype=_DTYPE).reshape(_BLOCK_SIZE, *_SHAPE)
518
519 expected = logical.copy()
520 completed = buffer.capture("request", 0, logical)
521 retained = buffer.retain_block(completed[0][1])
522 logical[:] = 0
523
524 assert not np.shares_memory(retained, logical)
525 np.testing.assert_array_equal(retained, expected)
526 buffer.release_block(retained)
527
528
529 def _make_store(
530 *,
531 max_bytes: int = 1 << 20,
532 object_nbytes: int = 4,
533 ):
534 return BlockObjectStore(
535 max_bytes=max_bytes,
536 object_nbytes=object_nbytes,
537 )
538
539
540 def test_publish_routed_experts_publishes_full_blocks():
541 store = _make_store(object_nbytes=_BLOCK_SIZE * int(np.prod(_SHAPE)))
542 buffer = RoutedExpertsBuffer(_DTYPE, _SHAPE, _BLOCK_SIZE, 1, 8, 2)
543 logical = np.arange(8 * 3 * 2, dtype=np.uint8).reshape(8, 3, 2)
544 hashes = [b"a" * 32, b"b" * 32]
545 keys = routed_experts_keys(hashes, "0")
546 blocks = buffer.capture("request", 0, logical)
547 publish_routed_experts(
548 store,
549 batches=[(keys, blocks)],
550 block_size=_BLOCK_SIZE,
551 )
552 expected = logical.copy()
553 logical[:] = 0
554
555 np.testing.assert_array_equal(
556 materialize_routed_experts(
557 store,
558 keys,
559 shape_per_token=_SHAPE,
560 dtype=_DTYPE,
561 ),
562 expected,
563 )
564 store.close()
565
566
567 def test_worker_data_plane_publishes_blocks_and_reuses_prefix():
568 worker = _make_worker(2)
569
570 hashes = [b"a" * 32, b"b" * 32, b"c" * 32]
571 logical = np.arange(10 * 3 * 2, dtype=np.uint8).reshape(10, 3, 2)
572 first = _metadata(
573 generation=0,
574 requests=[_request_metadata("first", 0, 8, 0, hashes)],
575 finished_requests={},
576 )
577 output = _process_output(worker, first, logical[:8], ["first"], np.array([0]))
578 assert output is not None
579 np.testing.assert_array_equal(output["first"].rows, logical[:8])
580
581 second = _metadata(
582 generation=0,
583 requests=[_request_metadata("second", 8, 2, 0, hashes)],
584 finished_requests={"first": []},
585 )
586 output = _process_output(worker, second, logical[8:], ["second"], np.array([0]))
587 assert output is not None
588 np.testing.assert_array_equal(output["second"].rows, logical)
589 worker.close()
590
591
592 def test_worker_matches_kv_eviction_order_after_requests_finish():
593 worker = _make_worker(2, max_store_blocks=3)
594 hashes = [bytes([value]) * 32 for value in range(5)]
595 blocks = [
596 np.full((_BLOCK_SIZE, *_SHAPE), value, dtype=_DTYPE) for value in range(5)
597 ]
598
599 def publish_and_finish(request_id, block_indexes):
600 rows = np.concatenate([blocks[index] for index in block_indexes])
601 step = _metadata(
602 0,
603 [
604 _request_metadata(
605 request_id,
606 0,
607 len(rows),
608 0,
609 [hashes[index] for index in block_indexes],
610 )
611 ],
612 {},
613 )
614 _process_output(worker, step, rows, [request_id], np.array([0]))
615 _begin_step(worker, _metadata(0, [], {request_id: []}))
616
617 publish_and_finish("s", [0])
618 publish_and_finish("ab", [1, 2])
619 publish_and_finish("c", [3])
620 publish_and_finish("d", [4])
621
622 assert worker._store is not None
623 np.testing.assert_array_equal(
624 materialize_routed_experts(
625 worker._store,
626 routed_experts_keys([hashes[1], hashes[3], hashes[4]], "0"),
627 shape_per_token=_SHAPE,
628 dtype=_DTYPE,
629 ),
630 np.concatenate([blocks[1], blocks[3], blocks[4]]),
631 )
632 with pytest.raises(BlockObjectStoreError, match="does not exist"):
633 materialize_routed_experts(
634 worker._store,
635 routed_experts_keys([hashes[2]], "0"),
636 shape_per_token=_SHAPE,
637 dtype=_DTYPE,
638 )
639 worker.close()
640
641
642 def test_worker_rejects_unbacked_capture_gap():
643 worker = _make_worker(1)
644 logical = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, 3, 2)
645 first = _metadata(
646 0,
647 [_request_metadata("request", 0, 1, 0, [])],
648 {},
649 )
650 _process_output(
651 worker,
652 first,
653 logical[:1],
654 ["request"],
655 np.array([0]),
656 np.zeros(1, dtype=np.int32),
657 )
658 second = _metadata(
659 0,
660 [_request_metadata("request", 2, 1, 0, [])],
661 {},
662 )
663
664 with pytest.raises(AssertionError, match="unbacked token gap"):
665 _process_output(
666 worker,
667 second,
668 logical[2:3],
669 ["request"],
670 np.array([0]),
671 np.zeros(1, dtype=np.int32),
672 )
673 worker.close()
674
675
676 def test_worker_replays_rejected_speculative_gap():
677 worker = _make_worker(1)
678 logical = np.arange(5 * 3 * 2, dtype=np.uint8).reshape(5, 3, 2)
679 first = _metadata(
680 0,
681 [_request_metadata("request", 0, 5, 0, [])],
682 {},
683 )
684 first_output = _process_output(
685 worker, first, logical[:5], ["request"], np.array([4])
686 )
687
688 queued = _metadata(
689 0,
690 [_request_metadata("request", 5, 4, 0, [])],
691 {},
692 )
693 queued_output = _process_output(
694 worker,
695 queued,
696 logical[1:],
697 ["request"],
698 np.array([3]),
699 )
700
701 rolled_back = _metadata(
702 0,
703 [_request_metadata("request", 6, 3, 0, [b"a" * 32])],
704 {},
705 )
706
707 rolled_back_output = _process_output(
708 worker,
709 rolled_back,
710 logical[2:],
711 ["request"],
712 np.array([0]),
713 )
714
715 np.testing.assert_array_equal(first_output["request"].rows, logical[:1])
716 assert queued_output["request"].token_start == 1
717 np.testing.assert_array_equal(
718 queued_output["request"].rows,
719 logical[1:2],
720 )
721 np.testing.assert_array_equal(
722 rolled_back_output["request"].rows,
723 logical[2:],
724 )
725 assert worker._requests["request"].capture_cursor == len(logical)
726 worker.close()
727
728
729 def test_worker_rejects_overlapping_capture_rows():
730 worker = _make_worker(1)
731 logical = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, 3, 2)
732 first = _metadata(
733 0,
734 [_request_metadata("request", 0, 3, 0, [])],
735 {},
736 )
737 _process_output(
738 worker,
739 first,
740 logical[:3],
741 ["request"],
742 np.array([0]),
743 np.zeros(1, dtype=np.int32),
744 )
745 stale_overlap = logical[2:].copy()
746 stale_overlap[0] += 100
747 second = _metadata(
748 0,
749 [_request_metadata("request", 2, 2, 0, [b"a" * 32])],
750 {},
751 )
752 with pytest.raises(AssertionError, match="capture moved backwards"):
753 _process_output(
754 worker,
755 second,
756 stale_overlap,
757 ["request"],
758 np.array([0]),
759 )
760 worker.close()
761
762
763 def test_worker_publishes_entire_batch_before_materializing_prefix():
764 worker = _make_worker(2)
765 hashes = [b"a" * 32, b"b" * 32]
766 logical = np.arange(8 * 3 * 2, dtype=np.uint8).reshape(8, 3, 2)
767 metadata = _metadata(
768 generation=0,
769 requests=[
770 _request_metadata("consumer", 4, 4, 0, hashes),
771 _request_metadata("producer", 0, 4, 0, hashes),
772 ],
773 finished_requests={},
774 )
775
776 output = _process_output(
777 worker,
778 metadata,
779 np.concatenate((logical[4:], logical[:4])),
780 ["consumer", "producer"],
781 np.array([0, 0]),
782 )
783
784 assert output is not None
785 np.testing.assert_array_equal(output["consumer"].rows, logical)
786 np.testing.assert_array_equal(output["producer"].rows, logical[:4])
787 worker.close()
788
789
790 def test_worker_defers_full_block_until_kv_hash_arrives():
791 worker = _make_worker(1)
792 logical = np.arange(5 * 3 * 2, dtype=np.uint8).reshape(5, 3, 2)
793 first = _metadata(
794 0,
795 [_request_metadata("request", 0, 4, 0, [])],
796 {},
797 )
798
799 output = _process_output(
800 worker,
801 first,
802 logical[:4],
803 ["request"],
804 np.array([0]),
805 )
806
807 assert output is not None
808 np.testing.assert_array_equal(output["request"].rows, logical[:4])
809 assert worker._requests["request"].pending_blocks[0][0] == 0
810
811 block_hash = b"a" * 32
812 second = _metadata(
813 0,
814 [_request_metadata("request", 4, 1, 4, [block_hash])],
815 {},
816 )
817 output = _process_output(worker, second, logical[4:], ["request"], np.array([0]))
818
819 assert output is not None
820 np.testing.assert_array_equal(output["request"].rows, logical[4:])
821 assert not worker._requests["request"].pending_blocks
822 assert worker._store is not None
823 np.testing.assert_array_equal(
824 materialize_routed_experts(
825 worker._store,
826 routed_experts_keys([block_hash], "0"),
827 shape_per_token=_SHAPE,
828 dtype=_DTYPE,
829 ),
830 logical[:4],
831 )
832 worker.close()
833
834
835 def test_worker_releases_newly_keyed_blocks_before_capture():
836 worker = _make_worker(1, max_num_batched_tokens=16)
837 connector = _make_connector()
838 block_hashes = [b"a" * 32]
839 request = _scheduler_request("request", block_hashes, num_tokens=100)
840 logical = np.arange(52 * 3 * 2, dtype=np.uint8).reshape(52, 3, 2)
841
842 for token_start, num_tokens in [(0, 4), (4, 16), (20, 16)]:
843 request.num_computed_tokens = token_start
844 metadata = connector.build_connector_meta(
845 _step_output([request.request_id], [token_start], [num_tokens]),
846 {request.request_id: request},
847 )
848 _process_output(
849 worker,
850 metadata,
851 logical[token_start : token_start + num_tokens],
852 [request.request_id],
853 np.array([0]),
854 token_starts=(token_start,),
855 num_tokens=(num_tokens,),
856 )
857
858 block_hashes.extend(bytes([value]) * 32 for value in range(1, 5))
859 request.num_computed_tokens = 36
860 metadata = connector.build_connector_meta(
861 _step_output([request.request_id], [36], [16]),
862 {request.request_id: request},
863 )
864 _process_output(
865 worker,
866 metadata,
867 logical[36:52],
868 [request.request_id],
869 np.array([0]),
870 token_starts=(36,),
871 num_tokens=(16,),
872 )
873
874 state = worker._requests[request.request_id]
875 assert [start for start, _ in state.pending_blocks] == [
876 20,
877 24,
878 28,
879 32,
880 36,
881 40,
882 44,
883 48,
884 ]
885 worker.close()
886
887
888 def test_worker_publishes_pending_block_without_forward():
889 worker = _make_worker(1)
890 _begin_step(
891 worker,
892 _metadata(
893 0,
894 [_request_metadata("request", 0, 1, 0, [])],
895 {},
896 ),
897 )
898 logical = np.arange(_BLOCK_SIZE * 3 * 2, dtype=np.uint8).reshape(_BLOCK_SIZE, 3, 2)
899 assert worker._buffer is not None
900 worker._requests["request"].pending_blocks = [
901 (0, worker._buffer.retain_block(logical))
902 ]
903 block_hash = b"a" * 32
904
905 _begin_step(worker, _metadata(0, [], {}, {"request": [block_hash]}))
906
907 assert not worker._requests["request"].pending_blocks
908 assert worker._store is not None
909 np.testing.assert_array_equal(
910 materialize_routed_experts(
911 worker._store,
912 routed_experts_keys([block_hash], "0"),
913 shape_per_token=_SHAPE,
914 dtype=_DTYPE,
915 ),
916 logical,
917 )
918 worker.close()
919
920
921 def test_worker_releases_unscheduled_request_blocks_before_capture():
922 worker = _make_worker(2, 20, max_concurrent_batches=1)
923 logical = np.arange(48 * 3 * 2, dtype=np.uint8).reshape(48, 3, 2)
924 prompt_hashes = [b"a" * 32, b"b" * 32]
925 prompt = _metadata(
926 0,
927 [
928 _request_metadata("first", 0, 4, 0, prompt_hashes[:1]),
929 _request_metadata("second", 0, 4, 0, prompt_hashes[1:]),
930 ],
931 {},
932 )
933 _process_output(
934 worker,
935 prompt,
936 logical[:8],
937 ["first", "second"],
938 np.zeros(2),
939 np.zeros(2, dtype=np.int32),
940 )
941
942 first_decode = _metadata(
943 0,
944 [_request_metadata("first", 4, 20, 0, [])],
945 {},
946 )
947 _process_output(
948 worker,
949 first_decode,
950 logical[8:28],
951 ["first"],
952 np.zeros(1),
953 np.zeros(1, dtype=np.int32),
954 )
955 assert len(worker._requests["first"].pending_blocks) == 5
956
957 first_hashes = [bytes([value]) * 32 for value in range(2, 7)]
958 second_decode = _metadata(
959 0,
960 [_request_metadata("second", 4, 20, 0, [])],
961 {},
962 {"first": first_hashes},
963 )
964 _process_output(
965 worker,
966 second_decode,
967 logical[28:48],
968 ["second"],
969 np.zeros(1),
970 np.zeros(1, dtype=np.int32),
971 )
972
973 assert not worker._requests["first"].pending_blocks
974 assert len(worker._requests["second"].pending_blocks) == 5
975 worker.close()
976
977
978 def test_worker_does_not_rematerialize_emitted_rows():
979 worker = _make_worker(1)
980 hashes = [b"a" * 32, b"b" * 32]
981 logical = np.arange(8 * 3 * 2, dtype=np.uint8).reshape(8, 3, 2)
982
983 first = _metadata(
984 0,
985 [_request_metadata("request", 0, 4, 0, hashes)],
986 {},
987 )
988 output = _process_output(worker, first, logical[:4], ["request"], np.array([0]))
989 assert output is not None
990
991 assert worker._store is not None
992 worker._store.get_concatenated = Mock(wraps=worker._store.get_concatenated)
993 second = _metadata(
994 0,
995 # Async scheduling may build this before the scheduler consumes first.
996 [_request_metadata("request", 4, 4, 0, [])],
997 {},
998 )
999 output = _process_output(worker, second, logical[4:], ["request"], np.array([0]))
1000
1001 assert output is not None
1002 np.testing.assert_array_equal(output["request"].rows, logical[4:])
1003 worker._store.get_concatenated.assert_not_called()
1004 worker.close()
1005
1006
1007 def test_worker_resumes_from_scheduler_emit_boundary():
1008 worker = _make_worker(1)
1009 logical = np.arange(5 * 3 * 2, dtype=np.uint8).reshape(5, 3, 2)
1010 resumed = _metadata(
1011 0,
1012 [_request_metadata("request", 4, 1, 4, [b"a" * 32])],
1013 {},
1014 )
1015
1016 output = _process_output(worker, resumed, logical[4:], ["request"], np.array([0]))
1017
1018 assert output["request"].token_start == 4
1019 np.testing.assert_array_equal(output["request"].rows, logical[4:])
1020 assert worker._requests["request"].emit_cursor == 5
1021 worker.close()
1022
1023
1024 def test_worker_emits_mid_block_chunked_prefill():
1025 worker = _make_worker(1)
1026 logical = np.arange(3 * 3 * 2, dtype=np.uint8).reshape(3, 3, 2)
1027
1028 first = _metadata(
1029 0,
1030 [_request_metadata("request", 0, 2, 0, [])],
1031 {},
1032 )
1033 output = _process_output(
1034 worker,
1035 first,
1036 logical[:2],
1037 ["request"],
1038 np.array([0]),
1039 np.zeros(1, dtype=np.int32),
1040 )
1041 assert output is not None and not output
1042
1043 second = _metadata(
1044 0,
1045 [_request_metadata("request", 2, 1, 0, [])],
1046 {},
1047 )
1048 output = _process_output(worker, second, logical[2:], ["request"], np.array([0]))
1049
1050 np.testing.assert_array_equal(output["request"].rows, logical)
1051 worker.close()
1052
1053
1054 def test_worker_emits_published_block_and_mid_block_tail():
1055 worker = _make_worker(1)
1056 logical = np.arange(7 * 3 * 2, dtype=np.uint8).reshape(7, 3, 2)
1057
1058 first = _metadata(
1059 0,
1060 [_request_metadata("request", 0, 6, 0, [b"a" * 32])],
1061 {},
1062 )
1063 output = _process_output(
1064 worker,
1065 first,
1066 logical[:6],
1067 ["request"],
1068 np.array([0]),
1069 np.zeros(1, dtype=np.int32),
1070 )
1071 assert output is not None and not output
1072
1073 second = _metadata(
1074 0,
1075 [_request_metadata("request", 6, 1, 0, [])],
1076 {},
1077 )
1078 output = _process_output(worker, second, logical[6:], ["request"], np.array([0]))
1079
1080 np.testing.assert_array_equal(output["request"].rows, logical)
1081 worker.close()
1082
1083
1084 def test_worker_output_does_not_alias_released_tail_buffer():
1085 worker = _make_worker(1)
1086 logical = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, 3, 2)
1087 first = _metadata(
1088 0,
1089 [_request_metadata("request", 0, 2, 0, [b"a" * 32])],
1090 {},
1091 )
1092 _process_output(
1093 worker,
1094 first,
1095 logical[:2],
1096 ["request"],
1097 np.array([0]),
1098 np.zeros(1, dtype=np.int32),
1099 )
1100 second = _metadata(
1101 0,
1102 [_request_metadata("request", 2, 2, 0, [])],
1103 {},
1104 )
1105
1106 output = _process_output(worker, second, logical[2:], ["request"], np.array([0]))
1107 emitted = output["request"].rows
1108 expected = logical.copy()
1109
1110 replacement = np.full_like(logical, 99)
1111 reuse = _metadata(
1112 0,
1113 [_request_metadata("reuse", 0, 4, 0, [b"b" * 32])],
1114 {},
1115 )
1116 _process_output(
1117 worker,
1118 reuse,
1119 replacement,
1120 ["reuse"],
1121 np.array([0]),
1122 np.zeros(1, dtype=np.int32),
1123 )
1124
1125 np.testing.assert_array_equal(emitted, expected)
1126 worker.close()
1127
1128
1129 def test_worker_merges_block_hash_deltas():
1130 worker = _make_worker(1)
1131 _begin_step(
1132 worker,
1133 _metadata(
1134 0,
1135 [_request_metadata("request", 0, 1, 0, [b"a" * 32, b"b" * 32])],
1136 {},
1137 ),
1138 )
1139 _begin_step(
1140 worker,
1141 _metadata(
1142 0,
1143 [
1144 _request_metadata(
1145 "request",
1146 1,
1147 1,
1148 0,
1149 [b"c" * 32],
1150 )
1151 ],
1152 {},
1153 ),
1154 )
1155
1156 assert worker._requests["request"].aux_output_keys == [
1157 "vllm-artifact/0/" + (b"a" * 32).hex(),
1158 "vllm-artifact/0/" + (b"b" * 32).hex(),
1159 "vllm-artifact/0/" + (b"c" * 32).hex(),
1160 ]
1161 worker.close()
1162
1163
1164 def test_worker_finish_publishes_keyed_block_and_discards_request_state():
1165 worker = _make_worker(1)
1166 logical = np.arange(2 * _BLOCK_SIZE * 3 * 2, dtype=np.uint8).reshape(
1167 2 * _BLOCK_SIZE, 3, 2
1168 )
1169 running = _metadata(
1170 0,
1171 [_request_metadata("request", 0, len(logical), 0, [])],
1172 {},
1173 )
1174 _process_output(
1175 worker,
1176 running,
1177 logical,
1178 ["request"],
1179 np.array([0]),
1180 np.array([0]),
1181 )
1182 block_hash = b"a" * 32
1183
1184 _begin_step(
1185 worker,
1186 _metadata(
1187 0,
1188 [],
1189 {"request": [block_hash]},
1190 ),
1191 )
1192
1193 assert "request" not in worker._requests
1194 assert worker._buffer is not None
1195 assert "request" not in worker._buffer._requests
1196 assert worker._store is not None
1197 np.testing.assert_array_equal(
1198 materialize_routed_experts(
1199 worker._store,
1200 routed_experts_keys([block_hash], "0"),
1201 shape_per_token=_SHAPE,
1202 dtype=_DTYPE,
1203 ),
1204 logical[:_BLOCK_SIZE],
1205 )
1206 worker.close()
1207
1208
1209 def test_worker_fails_when_cached_aux_output_is_missing():
1210 worker = _make_worker(1)
1211 worker._generation = 0
1212 metadata = _metadata(
1213 0,
1214 [
1215 _request_metadata(
1216 "request",
1217 8,
1218 1,
1219 0,
1220 [b"a" * 32, b"b" * 32],
1221 )
1222 ],
1223 {},
1224 )
1225
1226 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1227 _process_output(
1228 worker,
1229 metadata,
1230 np.zeros((1, *_SHAPE), dtype=_DTYPE),
1231 ["request"],
1232 np.array([0]),
1233 )
1234 worker.close()
1235
1236
1237 def test_store_rejects_oversized_batch_without_partial_write():
1238 store = _make_store(max_bytes=6, object_nbytes=3)
1239 store.put([BlockObject("retained", b"rrr")])
1240
1241 with pytest.raises(BlockObjectStoreError, match="cannot retain"):
1242 store.put(
1243 [
1244 BlockObject("first", b"111"),
1245 BlockObject("second", b"222"),
1246 BlockObject("third", b"333"),
1247 ]
1248 )
1249
1250 assert store.get_concatenated(["retained"]) == b"rrr"
1251 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1252 store.get_concatenated(["first"])
1253 store.close()
1254
1255
1256 def test_store_lru_and_immutable_put():
1257 store = _make_store(max_bytes=8)
1258 store.put([BlockObject("first", b"1111"), BlockObject("second", b"2222")])
1259 assert store.get_concatenated(["first"]) == b"1111"
1260
1261 store.put([BlockObject("first", b"1111"), BlockObject("third", b"3333")])
1262
1263 assert store.get_concatenated(["first", "third"]) == b"11113333"
1264 with pytest.raises(BlockObjectStoreError, match="Increase aux_output_config"):
1265 store.get_concatenated(["second"])
1266 store.close()
1267
1268
1269 def test_store_does_not_evict_referenced_aux_outputs():
1270 store = _make_store(max_bytes=8)
1271 store.put([BlockObject("first", b"1111"), BlockObject("second", b"2222")])
1272 store.put([], retain_keys=["first"])
1273 store.put([], retain_keys=["first"], release_keys=["first"])
1274
1275 store.put([BlockObject("third", b"3333")])
1276
1277 assert store.get_concatenated(["first", "third"]) == b"11113333"
1278 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1279 store.get_concatenated(["second"])
1280 store.put([], release_keys=["first"])
1281 store.close()
1282
1283
1284 def test_store_terminal_put_preserves_tail_first_eviction_order():
1285 store = _make_store(max_bytes=8)
1286 keys = ["first", "second", "third"]
1287 store.put(
1288 [BlockObject("first", b"1111"), BlockObject("second", b"2222")],
1289 retain_keys=keys,
1290 )
1291
1292 store.put(
1293 [BlockObject("third", b"3333")],
1294 release_keys=reversed(keys),
1295 )
1296
1297 assert store.get_concatenated(keys[:2]) == b"11112222"
1298 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1299 store.get_concatenated(["third"])
1300 store.close()
1301
1302
1303 def test_store_terminal_put_orders_survivors_for_later_eviction():
1304 store = _make_store(max_bytes=12)
1305 keys = ["first", "second", "third"]
1306 store.put(
1307 [BlockObject("first", b"1111"), BlockObject("second", b"2222")],
1308 retain_keys=keys,
1309 )
1310
1311 store.put(
1312 [BlockObject("third", b"3333")],
1313 release_keys=reversed(keys),
1314 )
1315 store.put([BlockObject("fourth", b"4444")])
1316
1317 assert store.get_concatenated(["first", "second", "fourth"]) == b"111122224444"
1318 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1319 store.get_concatenated(["third"])
1320 store.close()
1321
1322
1323 def test_store_terminal_put_keeps_a_shared_incoming_key():
1324 store = _make_store(max_bytes=8)
1325 keys = ["first", "second", "third"]
1326 store.put(
1327 [BlockObject("first", b"1111"), BlockObject("second", b"2222")],
1328 retain_keys=(*keys, "third"),
1329 )
1330
1331 store.put(
1332 [BlockObject("third", b"3333")],
1333 release_keys=reversed(keys),
1334 )
1335
1336 assert store.get_concatenated(["first", "third"]) == b"11113333"
1337 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1338 store.get_concatenated(["second"])
1339 store.put([], release_keys=["third"])
1340 store.close()
1341
1342
1343 def test_store_rejects_access_after_close():
1344 store = _make_store(max_bytes=8)
1345 store.close()
1346
1347 with pytest.raises(RuntimeError, match="closed"):
1348 store.put([BlockObject("first", b"1111")])
1349 with pytest.raises(RuntimeError, match="closed"):
1350 store.get_concatenated(["first"])
1351
1352
1353 def test_store_reuses_evicted_slot_without_moving_live_objects():
1354 store = _make_store(max_bytes=12)
1355 store.put(
1356 [
1357 BlockObject("first", b"1111"),
1358 BlockObject("second", b"2222"),
1359 BlockObject("third", b"3333"),
1360 ]
1361 )
1362 assert store.get_concatenated(["second"]) == b"2222"
1363 store.put([BlockObject("fourth", b"4444")])
1364
1365 assert store.get_concatenated(["second", "third", "fourth"]) == b"222233334444"
1366 store.close()
1367
1368
1369 def _scheduler_request(
1370 request_id: str,
1371 block_hashes: list[bytes],
1372 *,
1373 num_tokens: int = 10,
1374 num_output_tokens: int = 1,
1375 prompt_start: int = 0,
1376 ):
1377 return _SchedulerRequest(
1378 request_id=request_id,
1379 block_hashes=block_hashes,
1380 num_tokens=num_tokens,
1381 num_output_tokens=num_output_tokens,
1382 num_computed_tokens=num_tokens,
1383 sampling_params=SimpleNamespace(routed_experts_prompt_start=prompt_start),
1384 )
1385
1386
1387 def _step_output(
1388 request_ids: list[str], token_starts: list[int], token_counts: list[int]
1389 ) -> SchedulerOutput:
1390 output = SchedulerOutput.make_empty()
1391 output.scheduled_cached_reqs = CachedRequestData(
1392 req_ids=request_ids,
1393 resumed_req_ids=set(),
1394 new_token_ids=[[] for _ in request_ids],
1395 all_token_ids={},
1396 new_block_ids=[None for _ in request_ids],
1397 num_computed_tokens=token_starts,
1398 num_output_tokens=[0 for _ in request_ids],
1399 )
1400 output.num_scheduled_tokens = dict(zip(request_ids, token_counts, strict=True))
1401 output.total_num_scheduled_tokens = sum(token_counts)
1402 return output
1403
1404
1405 def test_scheduler_connector_builds_worker_metadata_and_forwards_output():
1406 connector = _make_connector()
1407 request = _scheduler_request("request", [b"a" * 32], num_tokens=5)
1408 request.num_computed_tokens = 0
1409 scheduler_output = _step_output([request.request_id], [0], [4])
1410
1411 metadata = connector.build_connector_meta(
1412 scheduler_output, {request.request_id: request}
1413 )
1414
1415 assert metadata.generation == 0
1416 assert metadata.requests == {request.request_id: 4}
1417 assert list(metadata.block_hashes[request.request_id]) == [b"a" * 32]
1418
1419 routing = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, 3, 2)
1420 output = {"request": AuxRequestOutput(0, routing)}
1421 request.num_computed_tokens = 4
1422 np.testing.assert_array_equal(connector.take_output(request, output), routing)
1423
1424
1425 def test_scheduler_starts_worker_output_at_requested_prompt_token():
1426 connector = _make_connector()
1427 request = _scheduler_request(
1428 "request",
1429 [b"a" * 32],
1430 num_tokens=4,
1431 num_output_tokens=0,
1432 prompt_start=2,
1433 )
1434
1435 metadata = connector.build_connector_meta(
1436 _step_output([request.request_id], [0], [4]),
1437 {request.request_id: request},
1438 )
1439
1440 assert metadata.requests == {request.request_id: 2}
1441
1442
1443 def test_scheduler_connector_preserves_request_finish_order():
1444 connector = _make_connector()
1445 first = _scheduler_request("first", [b"a" * 32])
1446 second = _scheduler_request("second", [b"b" * 32])
1447 connector.build_connector_meta(
1448 _step_output([first.request_id, second.request_id], [0, 0], [0, 0]),
1449 {first.request_id: first, second.request_id: second},
1450 )
1451 connector.request_finished(second)
1452 connector.request_finished(first)
1453
1454 metadata = connector.build_connector_meta(_step_output([], [], []), {})
1455
1456 assert metadata.finished_requests == (second.request_id, first.request_id)
1457 assert not metadata.block_hashes
1458
1459
1460 def test_worker_preemption_terminal_keeps_inflight_hashes_for_publish():
1461 worker = _make_worker(1)
1462 block_hashes = [b"a" * 32, b"b" * 32]
1463 logical = np.arange(2 * _BLOCK_SIZE * 3 * 2, dtype=np.uint8).reshape(
1464 2 * _BLOCK_SIZE, 3, 2
1465 )
1466 first = _metadata(
1467 0,
1468 [
1469 _request_metadata(
1470 "request",
1471 0,
1472 _BLOCK_SIZE,
1473 0,
1474 block_hashes[:1],
1475 )
1476 ],
1477 {},
1478 )
1479 _process_output(
1480 worker,
1481 first,
1482 logical[:_BLOCK_SIZE],
1483 ["request"],
1484 np.array([0]),
1485 )
1486 inflight = _metadata(
1487 0,
1488 [
1489 _request_metadata(
1490 "request",
1491 _BLOCK_SIZE,
1492 _BLOCK_SIZE,
1493 0,
1494 block_hashes[1:],
1495 )
1496 ],
1497 {},
1498 )
1499 _process_output(
1500 worker,
1501 inflight,
1502 logical[_BLOCK_SIZE:],
1503 ["request"],
1504 np.array([0]),
1505 np.zeros(1, dtype=np.int32),
1506 token_starts=(_BLOCK_SIZE,),
1507 num_tokens=(_BLOCK_SIZE,),
1508 )
1509 _begin_step(
1510 worker,
1511 _metadata(
1512 0,
1513 [],
1514 {"request": []},
1515 ),
1516 )
1517
1518 assert worker._store is not None
1519 np.testing.assert_array_equal(
1520 materialize_routed_experts(
1521 worker._store,
1522 routed_experts_keys(block_hashes[:1], "0"),
1523 shape_per_token=_SHAPE,
1524 dtype=_DTYPE,
1525 ),
1526 logical[:_BLOCK_SIZE],
1527 )
1528 np.testing.assert_array_equal(
1529 materialize_routed_experts(
1530 worker._store,
1531 routed_experts_keys(block_hashes[1:], "0"),
1532 shape_per_token=_SHAPE,
1533 dtype=_DTYPE,
1534 ),
1535 logical[_BLOCK_SIZE:],
1536 )
1537 worker.close()
1538
1539
1540 def test_worker_releases_terminal_pins_before_same_step_publish():
1541 worker = _make_worker(2, max_store_blocks=1)
1542 victim_hash = b"a" * 32
1543 producer_hash = b"b" * 32
1544 victim_rows = np.zeros((_BLOCK_SIZE, *_SHAPE), dtype=_DTYPE)
1545 producer_rows = np.ones((_BLOCK_SIZE, *_SHAPE), dtype=_DTYPE)
1546 running = _metadata(
1547 0,
1548 [
1549 _request_metadata("victim", 0, _BLOCK_SIZE, 0, [victim_hash]),
1550 _request_metadata("producer", 0, _BLOCK_SIZE, 0, []),
1551 ],
1552 {},
1553 )
1554 _process_output(
1555 worker,
1556 running,
1557 np.concatenate((victim_rows, producer_rows)),
1558 ["victim", "producer"],
1559 np.zeros(2, dtype=np.int32),
1560 np.zeros(2, dtype=np.int32),
1561 )
1562
1563 _begin_step(
1564 worker,
1565 _metadata(
1566 0,
1567 [],
1568 {"victim": []},
1569 {"producer": [producer_hash]},
1570 ),
1571 )
1572
1573 assert worker._store is not None
1574 np.testing.assert_array_equal(
1575 materialize_routed_experts(
1576 worker._store,
1577 routed_experts_keys([producer_hash], "0"),
1578 shape_per_token=_SHAPE,
1579 dtype=_DTYPE,
1580 ),
1581 producer_rows,
1582 )
1583 with pytest.raises(BlockObjectStoreError, match="does not exist"):
1584 worker._store.get_concatenated(routed_experts_keys([victim_hash], "0"))
1585 worker.close()
1586
1587
1588 def test_terminal_request_publishes_hash_discovered_after_last_schedule():
1589 connector = _make_connector()
1590 worker = _make_worker(1)
1591 block_hashes: list[bytes] = []
1592 request = _scheduler_request("request", block_hashes, num_tokens=5)
1593 request.num_computed_tokens = 0
1594 metadata = connector.build_connector_meta(
1595 _step_output([request.request_id], [0], [4]),
1596 {request.request_id: request},
1597 )
1598 logical = np.arange(_BLOCK_SIZE * 3 * 2, dtype=np.uint8).reshape(_BLOCK_SIZE, 3, 2)
1599
1600 _process_output(
1601 worker,
1602 metadata,
1603 logical,
1604 [request.request_id],
1605 np.array([0]),
1606 token_starts=(0,),
1607 num_tokens=(_BLOCK_SIZE,),
1608 )
1609 assert worker._requests[request.request_id].pending_blocks
1610
1611 block_hash = b"a" * 32
1612 block_hashes.append(block_hash)
1613 request.num_computed_tokens = _BLOCK_SIZE
1614 connector.request_finished(request)
1615 cleanup = connector.build_connector_meta(_step_output([], [], []), {})
1616 worker.begin_step(cleanup)
1617
1618 assert worker._store is not None
1619 np.testing.assert_array_equal(
1620 materialize_routed_experts(
1621 worker._store,
1622 routed_experts_keys([block_hash], "0"),
1623 shape_per_token=_SHAPE,
1624 dtype=_DTYPE,
1625 ),
1626 logical,
1627 )
1628 worker.close()
1629
1630
1631 def test_scheduler_connector_recreates_preempted_request_state():
1632 connector = _make_connector()
1633 request = _scheduler_request("request", [b"a" * 32], num_tokens=5)
1634 connector.build_connector_meta(
1635 _step_output([request.request_id], [0], [4]),
1636 {request.request_id: request},
1637 )
1638 connector.request_finished(request)
1639 cleanup = connector.build_connector_meta(
1640 _step_output([], [], []),
1641 {request.request_id: request},
1642 )
1643 assert request.request_id in cleanup.finished_requests
1644 assert request.request_id not in cleanup.block_hashes
1645
1646 resumed = connector.build_connector_meta(
1647 _step_output([request.request_id], [0], [4]),
1648 {request.request_id: request},
1649 )
1650 assert list(resumed.block_hashes[request.request_id]) == [b"a" * 32]
1651
1652
1653 def test_scheduler_consumes_ordered_stale_aux_outputs():
1654 connector = _make_connector()
1655 request = _scheduler_request("request", [], num_tokens=6)
1656 connector.build_connector_meta(
1657 _step_output([request.request_id], [0], [4]),
1658 {request.request_id: request},
1659 )
1660 first_rows = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, 3, 2)
1661 second_rows = np.arange(4 * 3 * 2, dtype=np.uint8).reshape(4, 3, 2) + 40
1662 first = {"request": AuxRequestOutput(0, first_rows)}
1663 second = {"request": AuxRequestOutput(4, second_rows)}
1664 connector.request_finished(request)
1665
1666 request.num_tokens = 5
1667 np.testing.assert_array_equal(connector.take_output(request, first), first_rows)
1668 request.num_tokens = 6
1669 np.testing.assert_array_equal(
1670 connector.take_output(request, second), second_rows[:1]
1671 )
1672
1673
1674 def test_scheduler_rejects_stale_output_without_aux_outputs():
1675 connector = _make_connector()
1676 request = _scheduler_request("request", [], num_tokens=1)
1677 connector.build_connector_meta(
1678 _step_output([request.request_id], [0], [1]),
1679 {request.request_id: request},
1680 )
1681
1682 with pytest.raises(
1683 AssertionError, match="auxiliary output worker output is missing"
1684 ):
1685 connector.take_output(
1686 request,
1687 {},
1688 )
1689
1690
1691 def test_scheduler_rejects_missing_accepted_aux_output_rows():
1692 connector = _make_connector()
1693 request = _scheduler_request("request", [], num_tokens=2)
1694 connector.build_connector_meta(
1695 _step_output([request.request_id], [0], [1]),
1696 {request.request_id: request},
1697 )
1698 output = {
1699 "request": AuxRequestOutput(
1700 0,
1701 np.empty((0, *_SHAPE), dtype=_DTYPE),
1702 )
1703 }
1704
1705 with pytest.raises(AssertionError, match="invalid token range"):
1706 connector.take_output(request, output)
1707
1708
1709 def test_scheduler_rejects_aux_output_past_finished_request():
1710 connector = _make_connector()
1711 request = _scheduler_request("request", [], num_tokens=1)
1712 request.finished = True
1713 connector.build_connector_meta(
1714 _step_output([request.request_id], [0], [1]),
1715 {request.request_id: request},
1716 )
1717 output = {
1718 "request": AuxRequestOutput(
1719 1,
1720 np.empty((0, *_SHAPE), dtype=_DTYPE),
1721 )
1722 }
1723
1724 with pytest.raises(
1725 AssertionError, match="finished auxiliary output output has no accepted"
1726 ):
1727 connector.take_output(request, output)
1728
1729
1730 @pytest.mark.parametrize("chunk_size", [2, 4])
1731 @pytest.mark.parametrize("max_tokens", [1, 2])
1732 def test_prompt_end_offset_through_worker_and_scheduler(chunk_size, max_tokens):
1733 """Omit prompt R3 without losing the empty first output or later decode rows."""
1734 connector = _make_connector()
1735 worker = _make_worker(1)
1736 request = _scheduler_request(
1737 "request", [b"a" * 32], num_tokens=4, num_output_tokens=0, prompt_start=4
1738 )
1739 rows = np.arange(5 * 3 * 2, dtype=_DTYPE).reshape(5, *_SHAPE)
1740 try:
1741 for start in range(0, 4, chunk_size):
1742 metadata = connector.build_connector_meta(
1743 _step_output([request.request_id], [start], [chunk_size]),
1744 {request.request_id: request},
1745 )
1746 output = _process_output(
1747 worker,
1748 metadata,
1749 rows[start : start + chunk_size],
1750 [request.request_id],
1751 np.array([0]),
1752 num_sampled=np.array([int(start + chunk_size == 4)]),
1753 token_starts=(start,),
1754 num_tokens=(chunk_size,),
1755 )
1756 if start + chunk_size < 4:
1757 assert output == {}
1758 request.num_tokens = 5
1759 request.num_output_tokens = 1
1760 request.finished = max_tokens == 1
1761 result = connector.take_output(request, output)
1762 np.testing.assert_array_equal(result, rows[:0])
1763
1764 if max_tokens == 2:
1765 metadata = connector.build_connector_meta(
1766 _step_output([request.request_id], [4], [1]),
1767 {request.request_id: request},
1768 )
1769 output = _process_output(
1770 worker,
1771 metadata,
1772 rows[4:],
1773 [request.request_id],
1774 np.array([0]),
1775 token_starts=(4,),
1776 num_tokens=(1,),
1777 )
1778 request.num_tokens = 6
1779 request.finished = True
1780 np.testing.assert_array_equal(
1781 connector.take_output(request, output), rows[4:]
1782 )
1783 finally:
1784 worker.close()
1785
1786
1787 def test_scheduler_connector_sends_each_block_hash_once():
1788 connector = _make_connector()
1789 request = _scheduler_request("request", [b"a" * 32, b"b" * 32], num_tokens=12)
1790 scheduler_output = _step_output([request.request_id], [0], [4])
1791
1792 first = connector.build_connector_meta(
1793 scheduler_output, {request.request_id: request}
1794 )
1795 request.block_hashes.append(b"c" * 32)
1796 second = connector.build_connector_meta(
1797 _step_output([request.request_id], [4], [4]),
1798 {request.request_id: request},
1799 )
1800 third = connector.build_connector_meta(
1801 _step_output([request.request_id], [8], [4]),
1802 {request.request_id: request},
1803 )
1804
1805 assert list(first.block_hashes[request.request_id]) == [b"a" * 32, b"b" * 32]
1806 assert list(second.block_hashes[request.request_id]) == [b"c" * 32]
1807 assert request.request_id not in third.block_hashes
1808
1809
1810 def test_scheduler_connector_sends_unscheduled_hash_update():
1811 connector = _make_connector()
1812 request = _scheduler_request("request", [b"a" * 32], num_tokens=4)
1813 connector.build_connector_meta(
1814 _step_output([request.request_id], [0], [4]),
1815 {request.request_id: request},
1816 )
1817
1818 request.block_hashes.append(b"b" * 32)
1819 request.num_tokens = 9
1820 request.num_computed_tokens = 8
1821 metadata = connector.build_connector_meta(
1822 _step_output([], [], []),
1823 {request.request_id: request},
1824 )
1825
1826 assert not metadata.requests
1827 assert list(metadata.block_hashes[request.request_id]) == [b"b" * 32]
1828
1829
1830 def test_scheduler_connector_reset_derives_emit_start_from_request():
1831 connector = _make_connector()
1832 request = _scheduler_request("request", [b"a" * 32], num_tokens=5)
1833 request.num_computed_tokens = 0
1834 before_reset = connector.build_connector_meta(
1835 _step_output([request.request_id], [0], [4]),
1836 {request.request_id: request},
1837 )
1838 assert list(before_reset.block_hashes[request.request_id]) == [b"a" * 32]
1839
1840 connector.reset()
1841 empty = connector.build_connector_meta(
1842 _step_output([], [], []),
1843 {request.request_id: request},
1844 )
1845 assert not empty.block_hashes
1846 request.num_computed_tokens = 4
1847 scheduler_output = _step_output([request.request_id], [4], [1])
1848 metadata = connector.build_connector_meta(
1849 scheduler_output, {request.request_id: request}
1850 )
1851
1852 assert metadata.generation == 1
1853 assert metadata.requests[request.request_id] == 4
1854 assert list(metadata.block_hashes[request.request_id]) == [b"a" * 32]